aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormax42 <max42@yandex-team.com>2023-06-30 03:37:03 +0300
committermax42 <max42@yandex-team.com>2023-06-30 03:37:03 +0300
commitfac2bd72b4b31ec3238292caf8fb2a8aaa6d6c4a (patch)
treeb8cbc1deb00309c7f1a7ab6df520a76cf0b5c6d7
parent7bf166b1a7ed0af927f230022b245af618e998c1 (diff)
downloadydb-fac2bd72b4b31ec3238292caf8fb2a8aaa6d6c4a.tar.gz
YT-19324: move YT provider to ydb/library/yql
This commit is formed by the following script: https://paste.yandex-team.ru/6f92e4b8-efc5-4d34-948b-15ee2accd7e7/text. This commit has zero effect on all projects that depend on YQL. The summary of changes: - `yql/providers/yt -> ydb/library/yql/providers/yt `- the whole implementation of YT provider is moved into YDB code base for further export as a part of YT YQL plugin shared library; - `yql/providers/stat/{expr_nodes,uploader} -> ydb/library/yql/providers/stat/{expr_nodes,uploader}` - a small interface without implementation and the description of stat expr nodes; - `yql/core/extract_predicate/ut -> ydb/library/yql/core/extract_predicate/ut`; - `yql/core/{ut,ut_common} -> ydb/library/yql/core/{ut,ut_common}`; - `yql/core` is gone; - `yql/library/url_preprocessing -> ydb/library/yql/core/url_preprocessing`. **NB**: all new targets inside `ydb/` are under `IF (NOT CMAKE_EXPORT)` clause which disables them from open-source cmake generation and ya make build. They will be enabled in the subsequent commits.
-rw-r--r--contrib/libs/backtrace/LICENSE29
-rw-r--r--contrib/libs/backtrace/README.md36
-rw-r--r--contrib/libs/backtrace/atomic.c113
-rw-r--r--contrib/libs/backtrace/backtrace-supported.h66
-rw-r--r--contrib/libs/backtrace/backtrace.c129
-rw-r--r--contrib/libs/backtrace/backtrace.h189
-rw-r--r--contrib/libs/backtrace/config-armv7a.h2
-rw-r--r--contrib/libs/backtrace/config-linux.h173
-rw-r--r--contrib/libs/backtrace/config-osx.h7
-rw-r--r--contrib/libs/backtrace/config.h11
-rw-r--r--contrib/libs/backtrace/dwarf.c4402
-rw-r--r--contrib/libs/backtrace/elf.c4924
-rw-r--r--contrib/libs/backtrace/fileline.c346
-rw-r--r--contrib/libs/backtrace/filenames.h52
-rw-r--r--contrib/libs/backtrace/internal.h380
-rw-r--r--contrib/libs/backtrace/mmap.c331
-rw-r--r--contrib/libs/backtrace/mmapio.c110
-rw-r--r--contrib/libs/backtrace/posix.c104
-rw-r--r--contrib/libs/backtrace/print.c92
-rw-r--r--contrib/libs/backtrace/simple.c108
-rw-r--r--contrib/libs/backtrace/sort.c108
-rw-r--r--contrib/libs/backtrace/state.c72
-rw-r--r--contrib/libs/backtrace/ya.make49
-rw-r--r--contrib/libs/flatbuffers/CONTRIBUTING.md42
-rw-r--r--contrib/libs/flatbuffers/LICENSE.txt202
-rw-r--r--contrib/libs/flatbuffers/flatc/ya.make56
-rw-r--r--contrib/libs/flatbuffers/grpc/README.md43
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/config.h40
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.cc1780
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.h138
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/go_generator.cc501
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/go_generator.h64
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/java_generator.cc1135
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/java_generator.h85
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/python_generator.cc149
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/python_generator.h33
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/schema_interface.h120
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.cc438
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.h55
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.cc523
-rw-r--r--contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.h61
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/code_generators.h235
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/flatbuffers_iter.h640
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/flatc.h100
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/flexbuffers.h1636
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/hash.h127
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/idl.h1208
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/reflection.h502
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/reflection_generated.h1278
-rw-r--r--contrib/libs/flatbuffers/include/flatbuffers/util.h698
-rw-r--r--contrib/libs/flatbuffers/samples/monster.fbs33
-rw-r--r--contrib/libs/flatbuffers/samples/sample_binary.cpp104
-rw-r--r--contrib/libs/flatbuffers/samples/ya.make18
-rw-r--r--contrib/libs/flatbuffers/src/code_generators.cpp395
-rw-r--r--contrib/libs/flatbuffers/src/flatc.cpp554
-rw-r--r--contrib/libs/flatbuffers/src/flatc_main.cpp121
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_cpp.cpp3514
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_cpp_yandex_maps_iter.cpp731
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_csharp.cpp2100
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_dart.cpp955
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_fbs.cpp154
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_go.cpp1374
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_grpc.cpp557
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_java.cpp1244
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_json_schema.cpp292
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_kotlin.cpp1527
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_lobster.cpp391
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_lua.cpp745
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_php.cpp939
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_python.cpp1782
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_rust.cpp2817
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_swift.cpp1575
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_text.cpp414
-rw-r--r--contrib/libs/flatbuffers/src/idl_gen_ts.cpp1583
-rw-r--r--contrib/libs/flatbuffers/src/idl_parser.cpp3986
-rw-r--r--contrib/libs/flatbuffers/src/reflection.cpp713
-rw-r--r--contrib/libs/flatbuffers/src/util.cpp287
-rw-r--r--contrib/libs/flatbuffers/ya.make40
-rw-r--r--contrib/libs/isa-l/erasure_code/ec_base.c371
-rw-r--r--contrib/libs/isa-l/erasure_code/ec_base.h6680
-rw-r--r--contrib/libs/isa-l/erasure_code/ec_highlevel_func.c336
-rw-r--r--contrib/libs/isa-l/erasure_code/ec_multibinary.asm95
-rw-r--r--contrib/libs/isa-l/erasure_code/ec_multibinary_darwin.asm96
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx.asm341
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx2.asm360
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx512.asm250
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_sse.asm343
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx.asm240
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx2.asm251
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx512.asm235
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_2vect_mad_sse.asm244
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx.asm382
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx2.asm402
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx512.asm275
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_sse.asm383
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx.asm292
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx2.asm322
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx512.asm252
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_3vect_mad_sse.asm303
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx.asm446
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx2.asm466
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx512.asm306
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_sse.asm448
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx.asm341
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx2.asm347
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx512.asm272
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_4vect_mad_sse.asm347
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx.asm308
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx2.asm320
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_sse.asm309
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx.asm370
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx2.asm368
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_5vect_mad_sse.asm378
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx.asm320
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx2.asm331
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_sse.asm320
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx.asm399
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx2.asm405
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_6vect_mad_sse.asm411
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx.asm276
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx2.asm285
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx512.asm245
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_sse.asm276
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mad_avx.asm201
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mad_avx2.asm208
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mad_avx512.asm198
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mad_sse.asm202
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mul_avx.asm168
-rw-r--r--contrib/libs/isa-l/erasure_code/gf_vect_mul_sse.asm175
-rw-r--r--contrib/libs/isa-l/erasure_code/ya.make81
-rw-r--r--contrib/libs/isa-l/include/erasure_code.h944
-rw-r--r--contrib/libs/isa-l/include/gf_vect_mul.h152
-rw-r--r--contrib/libs/isa-l/include/multibinary.asm399
-rw-r--r--contrib/libs/isa-l/include/reg_sizes.asm248
-rw-r--r--contrib/libs/linux-headers/linux/bpf_common.h57
-rw-r--r--contrib/libs/linux-headers/linux/filter.h90
-rw-r--r--contrib/libs/linux-headers/linux/hw_breakpoint.h35
-rw-r--r--contrib/libs/pfr/LICENSE_1_0.txt23
-rw-r--r--contrib/libs/pfr/README.md104
-rw-r--r--contrib/libs/pfr/include/pfr/detail/config.hpp91
-rw-r--r--contrib/libs/pfr/include/pfr/detail/fields_count.hpp330
-rw-r--r--contrib/libs/pfr/include/pfr/detail/make_integer_sequence.hpp90
-rw-r--r--contrib/libs/pfr/include/pfr/detail/sequence_tuple.hpp127
-rw-r--r--contrib/libs/pfr/include/pfr/detail/size_t_.hpp18
-rw-r--r--contrib/libs/pfr/include/pfr/detail/unsafe_declval.hpp36
-rw-r--r--contrib/libs/pfr/include/pfr/tuple_size.hpp48
-rw-r--r--contrib/libs/pfr/ya.make20
-rw-r--r--contrib/libs/yajl/api/yajl_common.h75
-rw-r--r--contrib/libs/yajl/api/yajl_gen.h180
-rw-r--r--contrib/libs/yajl/api/yajl_parse.h231
-rw-r--r--contrib/libs/yajl/api/yajl_tree.h193
-rw-r--r--contrib/libs/yajl/api/yajl_version.h23
-rw-r--r--contrib/libs/yajl/ya.make24
-rw-r--r--contrib/libs/yajl/yajl.c195
-rw-r--r--contrib/libs/yajl/yajl_alloc.c52
-rw-r--r--contrib/libs/yajl/yajl_alloc.h34
-rw-r--r--contrib/libs/yajl/yajl_buf.c108
-rw-r--r--contrib/libs/yajl/yajl_buf.h60
-rw-r--r--contrib/libs/yajl/yajl_bytestack.h69
-rw-r--r--contrib/libs/yajl/yajl_encode.c220
-rw-r--r--contrib/libs/yajl/yajl_encode.h34
-rw-r--r--contrib/libs/yajl/yajl_gen.c392
-rw-r--r--contrib/libs/yajl/yajl_lex.c766
-rw-r--r--contrib/libs/yajl/yajl_lex.h123
-rw-r--r--contrib/libs/yajl/yajl_parser.c498
-rw-r--r--contrib/libs/yajl/yajl_parser.cpp29
-rw-r--r--contrib/libs/yajl/yajl_parser.h89
-rw-r--r--contrib/libs/yajl/yajl_tree.c510
-rw-r--r--contrib/libs/yajl/yajl_version.c7
-rw-r--r--contrib/restricted/http-parser/AUTHORS68
-rw-r--r--contrib/restricted/http-parser/LICENSE-MIT19
-rw-r--r--contrib/restricted/http-parser/README.md246
-rw-r--r--contrib/restricted/http-parser/http_parser.c2568
-rw-r--r--contrib/restricted/http-parser/http_parser.h443
-rw-r--r--contrib/restricted/http-parser/ya.make30
-rw-r--r--contrib/tools/flatc/bin/ya.make23
-rw-r--r--contrib/tools/flatc/ya.make11
-rw-r--r--library/cpp/containers/concurrent_hash/concurrent_hash.h128
-rw-r--r--library/cpp/disjoint_sets/disjoint_sets.cpp1
-rw-r--r--library/cpp/disjoint_sets/disjoint_sets.h81
-rw-r--r--library/cpp/disjoint_sets/ya.make7
-rw-r--r--library/cpp/dwarf_backtrace/backtrace.cpp62
-rw-r--r--library/cpp/dwarf_backtrace/backtrace.h36
-rw-r--r--library/cpp/dwarf_backtrace/ya.make19
-rw-r--r--library/cpp/erasure/README.md9
-rw-r--r--library/cpp/erasure/codec.cpp1
-rw-r--r--library/cpp/erasure/codec.h80
-rw-r--r--library/cpp/erasure/helpers.cpp80
-rw-r--r--library/cpp/erasure/helpers.h30
-rw-r--r--library/cpp/erasure/isa_erasure.cpp1
-rw-r--r--library/cpp/erasure/isa_erasure.h170
-rw-r--r--library/cpp/erasure/lrc.cpp1
-rw-r--r--library/cpp/erasure/lrc.h326
-rw-r--r--library/cpp/erasure/lrc_isa.cpp1
-rw-r--r--library/cpp/erasure/lrc_isa.h77
-rw-r--r--library/cpp/erasure/public.cpp1
-rw-r--r--library/cpp/erasure/public.h57
-rw-r--r--library/cpp/erasure/reed_solomon.cpp1
-rw-r--r--library/cpp/erasure/reed_solomon.h60
-rw-r--r--library/cpp/erasure/reed_solomon_isa.cpp1
-rw-r--r--library/cpp/erasure/reed_solomon_isa.h69
-rw-r--r--library/cpp/erasure/ya.make35
-rw-r--r--library/cpp/skiff/public.h63
-rw-r--r--library/cpp/skiff/skiff-inl.h39
-rw-r--r--library/cpp/skiff/skiff.cpp591
-rw-r--r--library/cpp/skiff/skiff.h259
-rw-r--r--library/cpp/skiff/skiff_schema-inl.h61
-rw-r--r--library/cpp/skiff/skiff_schema.cpp164
-rw-r--r--library/cpp/skiff/skiff_schema.h121
-rw-r--r--library/cpp/skiff/skiff_validator.cpp396
-rw-r--r--library/cpp/skiff/skiff_validator.h39
-rw-r--r--library/cpp/skiff/unittests/skiff_schema_ut.cpp148
-rw-r--r--library/cpp/skiff/unittests/skiff_ut.cpp627
-rw-r--r--library/cpp/skiff/unittests/ya.make12
-rw-r--r--library/cpp/skiff/ya.make16
-rw-r--r--library/cpp/skiff/zerocopy_output_writer-inl.h51
-rw-r--r--library/cpp/skiff/zerocopy_output_writer.cpp38
-rw-r--r--library/cpp/skiff/zerocopy_output_writer.h41
-rw-r--r--library/cpp/testing/gtest/friend.h5
-rw-r--r--library/cpp/threading/blocking_queue/blocking_queue.cpp3
-rw-r--r--library/cpp/threading/blocking_queue/blocking_queue.h158
-rw-r--r--library/cpp/threading/blocking_queue/blocking_queue_ut.cpp211
-rw-r--r--library/cpp/threading/blocking_queue/ut/ya.make7
-rw-r--r--library/cpp/threading/blocking_queue/ya.make9
-rw-r--r--library/cpp/threading/cron/cron.cpp69
-rw-r--r--library/cpp/threading/cron/cron.h18
-rw-r--r--library/cpp/threading/cron/ya.make11
-rw-r--r--library/cpp/type_info/Readme.md9
-rw-r--r--library/cpp/type_info/builder.cpp458
-rw-r--r--library/cpp/type_info/builder.h346
-rw-r--r--library/cpp/type_info/error.cpp1
-rw-r--r--library/cpp/type_info/error.h33
-rw-r--r--library/cpp/type_info/fwd.h123
-rw-r--r--library/cpp/type_info/test-data/bad-types.txt229
-rw-r--r--library/cpp/type_info/test-data/good-types.txt478
-rw-r--r--library/cpp/type_info/type.cpp1662
-rw-r--r--library/cpp/type_info/type.h2421
-rw-r--r--library/cpp/type_info/type_complexity.cpp78
-rw-r--r--library/cpp/type_info/type_complexity.h18
-rw-r--r--library/cpp/type_info/type_constructors.h110
-rw-r--r--library/cpp/type_info/type_equivalence.cpp286
-rw-r--r--library/cpp/type_info/type_equivalence.h36
-rw-r--r--library/cpp/type_info/type_factory.cpp495
-rw-r--r--library/cpp/type_info/type_factory.h906
-rw-r--r--library/cpp/type_info/type_info.cpp1
-rw-r--r--library/cpp/type_info/type_info.h10
-rw-r--r--library/cpp/type_info/type_io.cpp1186
-rw-r--r--library/cpp/type_info/type_io.h115
-rw-r--r--library/cpp/type_info/type_list.cpp1
-rw-r--r--library/cpp/type_info/type_list.h183
-rw-r--r--library/cpp/type_info/ut/builder.cpp125
-rw-r--r--library/cpp/type_info/ut/test_data.cpp91
-rw-r--r--library/cpp/type_info/ut/type_basics.cpp381
-rw-r--r--library/cpp/type_info/ut/type_complexity_ut.cpp33
-rw-r--r--library/cpp/type_info/ut/type_constraints.cpp53
-rw-r--r--library/cpp/type_info/ut/type_deserialize.cpp528
-rw-r--r--library/cpp/type_info/ut/type_equivalence.cpp394
-rw-r--r--library/cpp/type_info/ut/type_factory.cpp121
-rw-r--r--library/cpp/type_info/ut/type_factory_raw.cpp365
-rw-r--r--library/cpp/type_info/ut/type_io.cpp535
-rw-r--r--library/cpp/type_info/ut/type_list.cpp64
-rw-r--r--library/cpp/type_info/ut/type_serialize.cpp251
-rw-r--r--library/cpp/type_info/ut/type_show.cpp199
-rw-r--r--library/cpp/type_info/ut/type_strip_tags.cpp31
-rw-r--r--library/cpp/type_info/ut/utils.h73
-rw-r--r--library/cpp/type_info/ut/ya.make32
-rw-r--r--library/cpp/type_info/ya.make26
-rw-r--r--library/cpp/yson_pull/bridge.h34
-rw-r--r--library/cpp/yson_pull/yson.h14
-rw-r--r--library/cpp/yt/backtrace/backtrace-inl.h36
-rw-r--r--library/cpp/yt/backtrace/backtrace.cpp18
-rw-r--r--library/cpp/yt/backtrace/backtrace.h45
-rw-r--r--library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.cpp22
-rw-r--r--library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.h17
-rw-r--r--library/cpp/yt/backtrace/cursors/dummy/ya.make9
-rw-r--r--library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.cpp146
-rw-r--r--library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h39
-rw-r--r--library/cpp/yt/backtrace/cursors/frame_pointer/ya.make9
-rw-r--r--library/cpp/yt/backtrace/cursors/interop/interop.cpp102
-rw-r--r--library/cpp/yt/backtrace/cursors/interop/interop.h25
-rw-r--r--library/cpp/yt/backtrace/cursors/interop/ya.make14
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp70
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.h33
-rw-r--r--library/cpp/yt/backtrace/cursors/libunwind/ya.make13
-rw-r--r--library/cpp/yt/backtrace/symbolizers/dummy/dummy_symbolizer.cpp25
-rw-r--r--library/cpp/yt/backtrace/symbolizers/dwarf/dwarf_symbolizer.cpp64
-rw-r--r--library/cpp/yt/backtrace/symbolizers/dwarf/ya.make18
-rw-r--r--library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp113
-rw-r--r--library/cpp/yt/backtrace/unittests/backtrace_ut.cpp61
-rw-r--r--library/cpp/yt/backtrace/unittests/ya.make20
-rw-r--r--library/cpp/yt/backtrace/ya.make44
-rw-r--r--library/cpp/yt/containers/sharded_set-inl.h217
-rw-r--r--library/cpp/yt/containers/sharded_set.h69
-rw-r--r--library/cpp/yt/containers/unittests/sharded_set_ut.cpp121
-rw-r--r--library/cpp/yt/containers/unittests/ya.make15
-rw-r--r--library/cpp/yt/cpu_clock/benchmark/benchmark.cpp41
-rw-r--r--library/cpp/yt/cpu_clock/benchmark/ya.make11
-rw-r--r--library/cpp/yt/cpu_clock/unittests/clock_ut.cpp46
-rw-r--r--library/cpp/yt/cpu_clock/unittests/ya.make13
-rw-r--r--library/cpp/yt/farmhash/farm_hash.h63
-rw-r--r--library/cpp/yt/logging/logger-inl.h303
-rw-r--r--library/cpp/yt/logging/logger.cpp289
-rw-r--r--library/cpp/yt/logging/logger.h351
-rw-r--r--library/cpp/yt/logging/public.h39
-rw-r--r--library/cpp/yt/logging/unittests/logger_ut.cpp38
-rw-r--r--library/cpp/yt/logging/unittests/ya.make18
-rw-r--r--library/cpp/yt/logging/ya.make20
-rw-r--r--library/cpp/yt/memory/leaky_ref_counted_singleton-inl.h43
-rw-r--r--library/cpp/yt/memory/leaky_ref_counted_singleton.h22
-rw-r--r--library/cpp/yt/misc/arcadia_enum-inl.h49
-rw-r--r--library/cpp/yt/misc/arcadia_enum.h18
-rw-r--r--library/cpp/yt/misc/property.h306
-rw-r--r--library/cpp/yt/string/raw_formatter.h212
-rw-r--r--library/cpp/yt/threading/unittests/count_down_latch_ut.cpp78
-rw-r--r--library/cpp/yt/threading/unittests/recursive_spin_lock_ut.cpp88
-rw-r--r--library/cpp/yt/threading/unittests/spin_wait_ut.cpp48
-rw-r--r--library/cpp/yt/threading/unittests/ya.make17
-rw-r--r--library/cpp/yt/user_job_statistics/user_job_statistics.cpp133
-rw-r--r--library/cpp/yt/user_job_statistics/user_job_statistics.h58
-rw-r--r--library/cpp/yt/user_job_statistics/ya.make11
-rw-r--r--ydb/library/yql/core/extract_predicate/ut/extract_predicate_ut.cpp858
-rw-r--r--ydb/library/yql/core/extract_predicate/ut/ya.make35
-rw-r--r--ydb/library/yql/core/url_preprocessing/pattern_group.cpp28
-rw-r--r--ydb/library/yql/core/url_preprocessing/pattern_group.h23
-rw-r--r--ydb/library/yql/core/url_preprocessing/url_mapper.cpp19
-rw-r--r--ydb/library/yql/core/url_preprocessing/url_mapper.h32
-rw-r--r--ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp56
-rw-r--r--ydb/library/yql/core/url_preprocessing/url_preprocessing.h37
-rw-r--r--ydb/library/yql/core/url_preprocessing/ya.make24
-rw-r--r--ydb/library/yql/core/ut/ya.make42
-rw-r--r--ydb/library/yql/core/ut/yql_csv_ut.cpp312
-rw-r--r--ydb/library/yql/core/ut/yql_execution_ut.cpp792
-rw-r--r--ydb/library/yql/core/ut/yql_expr_constraint_ut.cpp3217
-rw-r--r--ydb/library/yql/core/ut/yql_expr_discover_ut.cpp152
-rw-r--r--ydb/library/yql/core/ut/yql_expr_optimize_ut.cpp655
-rw-r--r--ydb/library/yql/core/ut/yql_expr_providers_ut.cpp283
-rw-r--r--ydb/library/yql/core/ut/yql_expr_type_annotation_ut.cpp53
-rw-r--r--ydb/library/yql/core/ut/yql_library_compiler_ut.cpp223
-rw-r--r--ydb/library/yql/core/ut/yql_udf_index_ut.cpp368
-rw-r--r--ydb/library/yql/core/ut_common/ya.make19
-rw-r--r--ydb/library/yql/core/ut_common/yql_ut_common.cpp55
-rw-r--r--ydb/library/yql/core/ut_common/yql_ut_common.h23
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/ya.make52
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp1
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h67
-rw-r--r--ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json111
-rw-r--r--ydb/library/yql/providers/stat/uploader/ya.make11
-rw-r--r--ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp1
-rw-r--r--ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h39
-rw-r--r--ydb/library/yql/providers/stat/ya.make5
-rw-r--r--ydb/library/yql/providers/ya.make8
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/ut/ya.make23
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/ut/yt_codec_cg_ut.cpp893
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/ya.make85
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp296
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp906
-rw-r--r--ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.h61
-rw-r--r--ydb/library/yql/providers/yt/codec/ut/ya.make20
-rw-r--r--ydb/library/yql/providers/yt/codec/ya.make45
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec.cpp657
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec.h246
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_io.cpp2263
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_io.h161
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_io_ut.cpp308
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_job.cpp65
-rw-r--r--ydb/library/yql/providers/yt/codec/yt_codec_job.h52
-rw-r--r--ydb/library/yql/providers/yt/common/ya.make24
-rw-r--r--ydb/library/yql/providers/yt/common/yql_configuration.cpp1
-rw-r--r--ydb/library/yql/providers/yt/common/yql_configuration.h63
-rw-r--r--ydb/library/yql/providers/yt/common/yql_names.cpp11
-rw-r--r--ydb/library/yql/providers/yt/common/yql_names.h68
-rw-r--r--ydb/library/yql/providers/yt/common/yql_yt_settings.cpp516
-rw-r--r--ydb/library/yql/providers/yt/common/yql_yt_settings.h333
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.cpp24
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.h10
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.cpp119
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.h11
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader_impl.h202
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.cpp328
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.h80
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.cpp282
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.h10
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/dq/ya.make35
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/ut/ya.make29
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/ut/yql_mkql_output_ut.cpp319
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/ya.make30
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp74
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h74
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp41
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h78
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp393
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.h15
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp37
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.h42
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp226
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h13
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp108
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h42
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp107
-rw-r--r--ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.h10
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/ya.make52
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp7
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h66
-rw-r--r--ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json462
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/ya.make49
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file.cpp1429
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h29
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.cpp537
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.h18
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.cpp1076
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.h24
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.cpp88
-rw-r--r--ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h82
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp379
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/query_cache.h112
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp19
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/temp_files.h20
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp464
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h146
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/user_files.cpp97
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/user_files.h50
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/ya.make41
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp645
-rw-r--r--ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h70
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/ya.make65
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp410
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.h295
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp159
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.h47
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp4907
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_native.h26
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp224
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.h49
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp152
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.h34
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp64
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_session.h60
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp580
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.h118
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp530
-rw-r--r--ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.h94
-rw-r--r--ydb/library/yql/providers/yt/gateway/profile/ya.make14
-rw-r--r--ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.cpp172
-rw-r--r--ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.h9
-rw-r--r--ydb/library/yql/providers/yt/gateway/ya.make6
-rw-r--r--ydb/library/yql/providers/yt/job/ya.make40
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_base.cpp298
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_base.h106
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_calc.cpp105
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_calc.h39
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_factory.cpp57
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_factory.h17
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp49
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_infer_schema.h20
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_registry.h15
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp41
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_stats_writer.h20
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_table_content.cpp78
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_table_content.h15
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_user.cpp236
-rw-r--r--ydb/library/yql/providers/yt/job/yql_job_user.h107
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp110
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h41
-rw-r--r--ydb/library/yql/providers/yt/lib/config_clusters/ya.make12
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/ya.make20
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp436
-rw-r--r--ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h32
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/ya.make18
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp358
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.h45
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp276
-rw-r--r--ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.h47
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/ya.make16
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp27
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h45
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp137
-rw-r--r--ydb/library/yql/providers/yt/lib/hash/yql_op_hash.h47
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp112
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h33
-rw-r--r--ydb/library/yql/providers/yt/lib/infer_schema/ya.make15
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp96
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/init.h10
-rw-r--r--ydb/library/yql/providers/yt/lib/init_yt_api/ya.make14
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/ya.make16
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp177
-rw-r--r--ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.h61
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp223
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h117
-rw-r--r--ydb/library/yql/providers/yt/lib/lambda_builder/ya.make19
-rw-r--r--ydb/library/yql/providers/yt/lib/log/ya.make14
-rw-r--r--ydb/library/yql/providers/yt/lib/log/yt_logger.cpp152
-rw-r--r--ydb/library/yql/providers/yt/lib/log/yt_logger.h12
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp97
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h27
-rw-r--r--ydb/library/yql/providers/yt/lib/mkql_helpers/ya.make16
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp74
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h81
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp60
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h37
-rw-r--r--ydb/library/yql/providers/yt/lib/res_pull/ya.make20
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/ya.make24
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp1616
-rw-r--r--ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h155
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/schema.cpp1018
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/schema.h30
-rw-r--r--ydb/library/yql/providers/yt/lib/schema/ya.make16
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/ya.make15
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp384
-rw-r--r--ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h18
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/ya.make14
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp65
-rw-r--r--ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h24
-rw-r--r--ydb/library/yql/providers/yt/lib/ya.make17
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/ya.make15
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp310
-rw-r--r--ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h81
-rw-r--r--ydb/library/yql/providers/yt/mkql_dq/ya.make18
-rw-r--r--ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.cpp130
-rw-r--r--ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.h9
-rw-r--r--ydb/library/yql/providers/yt/opt/ya.make19
-rw-r--r--ydb/library/yql/providers/yt/opt/yql_yt_join.cpp676
-rw-r--r--ydb/library/yql/providers/yt/opt/yql_yt_join.h91
-rw-r--r--ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp327
-rw-r--r--ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h73
-rw-r--r--ydb/library/yql/providers/yt/provider/ut/ya.make33
-rw-r--r--ydb/library/yql/providers/yt/provider/ut/yql_yt_dq_integration_ut.cpp126
-rw-r--r--ydb/library/yql/providers/yt/provider/ut/yql_yt_epoch_ut.cpp519
-rw-r--r--ydb/library/yql/providers/yt/provider/ya.make100
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp549
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp461
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp920
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp38
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp62
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp2001
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp838
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp198
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp242
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp1004
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp645
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp582
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.h13
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp467
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp4
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_gateway.h559
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp2121
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_helpers.h138
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp1933
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.h174
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp468
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp775
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp4761
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h67
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_key.cpp246
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_key.h157
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp230
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp298
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp2653
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp535
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h41
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp228
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_op_hash.h28
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp1092
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h177
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp674
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_optimize.h37
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp64
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp2544
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp7383
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp504
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_provider.h111
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp84
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.h44
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table.cpp2983
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table.h352
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp366
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h76
-rw-r--r--ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp256
-rw-r--r--ydb/library/yql/providers/yt/ya.make17
-rw-r--r--ydb/library/yql/ya.make15
-rw-r--r--yt/cpp/mapreduce/client/abortable_registry.cpp125
-rw-r--r--yt/cpp/mapreduce/client/abortable_registry.h81
-rw-r--r--yt/cpp/mapreduce/client/batch_request_impl.cpp198
-rw-r--r--yt/cpp/mapreduce/client/batch_request_impl.h137
-rw-r--r--yt/cpp/mapreduce/client/client.cpp1361
-rw-r--r--yt/cpp/mapreduce/client/client.h506
-rw-r--r--yt/cpp/mapreduce/client/client_reader.cpp232
-rw-r--r--yt/cpp/mapreduce/client/client_reader.h65
-rw-r--r--yt/cpp/mapreduce/client/client_writer.cpp69
-rw-r--r--yt/cpp/mapreduce/client/client_writer.h42
-rw-r--r--yt/cpp/mapreduce/client/dummy_job_profiler.cpp26
-rw-r--r--yt/cpp/mapreduce/client/file_reader.cpp243
-rw-r--r--yt/cpp/mapreduce/client/file_reader.h105
-rw-r--r--yt/cpp/mapreduce/client/file_writer.cpp60
-rw-r--r--yt/cpp/mapreduce/client/file_writer.h38
-rw-r--r--yt/cpp/mapreduce/client/format_hints.cpp84
-rw-r--r--yt/cpp/mapreduce/client/format_hints.h27
-rw-r--r--yt/cpp/mapreduce/client/fwd.h16
-rw-r--r--yt/cpp/mapreduce/client/init.cpp280
-rw-r--r--yt/cpp/mapreduce/client/init.h22
-rw-r--r--yt/cpp/mapreduce/client/job_profiler.h27
-rw-r--r--yt/cpp/mapreduce/client/lock.cpp105
-rw-r--r--yt/cpp/mapreduce/client/lock.h31
-rw-r--r--yt/cpp/mapreduce/client/operation.cpp2981
-rw-r--r--yt/cpp/mapreduce/client/operation.h203
-rw-r--r--yt/cpp/mapreduce/client/operation_helpers.cpp91
-rw-r--r--yt/cpp/mapreduce/client/operation_helpers.h20
-rw-r--r--yt/cpp/mapreduce/client/operation_preparer.cpp881
-rw-r--r--yt/cpp/mapreduce/client/operation_preparer.h129
-rw-r--r--yt/cpp/mapreduce/client/operation_tracker.cpp34
-rw-r--r--yt/cpp/mapreduce/client/operation_tracker.h27
-rw-r--r--yt/cpp/mapreduce/client/prepare_operation.cpp286
-rw-r--r--yt/cpp/mapreduce/client/prepare_operation.h93
-rw-r--r--yt/cpp/mapreduce/client/py_helpers.cpp112
-rw-r--r--yt/cpp/mapreduce/client/py_helpers.h25
-rw-r--r--yt/cpp/mapreduce/client/retry_heavy_write_request.cpp87
-rw-r--r--yt/cpp/mapreduce/client/retry_heavy_write_request.h21
-rw-r--r--yt/cpp/mapreduce/client/retry_transaction.h71
-rw-r--r--yt/cpp/mapreduce/client/retryful_writer.cpp163
-rw-r--r--yt/cpp/mapreduce/client/retryful_writer.h130
-rw-r--r--yt/cpp/mapreduce/client/retryless_writer.cpp45
-rw-r--r--yt/cpp/mapreduce/client/retryless_writer.h73
-rw-r--r--yt/cpp/mapreduce/client/skiff.cpp396
-rw-r--r--yt/cpp/mapreduce/client/skiff.h72
-rw-r--r--yt/cpp/mapreduce/client/structured_table_formats.cpp572
-rw-r--r--yt/cpp/mapreduce/client/structured_table_formats.h146
-rw-r--r--yt/cpp/mapreduce/client/transaction.cpp195
-rw-r--r--yt/cpp/mapreduce/client/transaction.h95
-rw-r--r--yt/cpp/mapreduce/client/transaction_pinger.cpp321
-rw-r--r--yt/cpp/mapreduce/client/transaction_pinger.h39
-rw-r--r--yt/cpp/mapreduce/client/ya.make75
-rw-r--r--yt/cpp/mapreduce/client/yt_poller.cpp132
-rw-r--r--yt/cpp/mapreduce/client/yt_poller.h86
-rw-r--r--yt/cpp/mapreduce/common/debug_metrics.cpp62
-rw-r--r--yt/cpp/mapreduce/common/debug_metrics.h22
-rw-r--r--yt/cpp/mapreduce/common/fwd.h11
-rw-r--r--yt/cpp/mapreduce/common/helpers.cpp126
-rw-r--r--yt/cpp/mapreduce/common/helpers.h37
-rw-r--r--yt/cpp/mapreduce/common/node_builder.h4
-rw-r--r--yt/cpp/mapreduce/common/node_visitor.h4
-rw-r--r--yt/cpp/mapreduce/common/retry_lib.cpp267
-rw-r--r--yt/cpp/mapreduce/common/retry_lib.h100
-rw-r--r--yt/cpp/mapreduce/common/wait_proxy.cpp118
-rw-r--r--yt/cpp/mapreduce/common/wait_proxy.h53
-rw-r--r--yt/cpp/mapreduce/common/ya.make23
-rw-r--r--yt/cpp/mapreduce/http/abortable_http_response.cpp223
-rw-r--r--yt/cpp/mapreduce/http/abortable_http_response.h142
-rw-r--r--yt/cpp/mapreduce/http/context.cpp25
-rw-r--r--yt/cpp/mapreduce/http/context.h31
-rw-r--r--yt/cpp/mapreduce/http/core.h27
-rw-r--r--yt/cpp/mapreduce/http/fwd.h26
-rw-r--r--yt/cpp/mapreduce/http/helpers.cpp88
-rw-r--r--yt/cpp/mapreduce/http/helpers.h25
-rw-r--r--yt/cpp/mapreduce/http/host_manager.cpp140
-rw-r--r--yt/cpp/mapreduce/http/host_manager.h37
-rw-r--r--yt/cpp/mapreduce/http/http.cpp1014
-rw-r--r--yt/cpp/mapreduce/http/http.h256
-rw-r--r--yt/cpp/mapreduce/http/http_client.cpp603
-rw-r--r--yt/cpp/mapreduce/http/http_client.h76
-rw-r--r--yt/cpp/mapreduce/http/requests.cpp66
-rw-r--r--yt/cpp/mapreduce/http/requests.h29
-rw-r--r--yt/cpp/mapreduce/http/retry_request.cpp149
-rw-r--r--yt/cpp/mapreduce/http/retry_request.h52
-rw-r--r--yt/cpp/mapreduce/http/ya.make29
-rw-r--r--yt/cpp/mapreduce/interface/batch_request.cpp15
-rw-r--r--yt/cpp/mapreduce/interface/batch_request.h222
-rw-r--r--yt/cpp/mapreduce/interface/client.cpp19
-rw-r--r--yt/cpp/mapreduce/interface/client.h568
-rw-r--r--yt/cpp/mapreduce/interface/client_method_options.cpp34
-rw-r--r--yt/cpp/mapreduce/interface/client_method_options.h1452
-rw-r--r--yt/cpp/mapreduce/interface/common.cpp664
-rw-r--r--yt/cpp/mapreduce/interface/common.h1301
-rw-r--r--yt/cpp/mapreduce/interface/common_ut.cpp303
-rw-r--r--yt/cpp/mapreduce/interface/common_ut.h1
-rw-r--r--yt/cpp/mapreduce/interface/config.cpp321
-rw-r--r--yt/cpp/mapreduce/interface/config.h228
-rw-r--r--yt/cpp/mapreduce/interface/config_ut.cpp20
-rw-r--r--yt/cpp/mapreduce/interface/constants.h19
-rw-r--r--yt/cpp/mapreduce/interface/cypress.cpp24
-rw-r--r--yt/cpp/mapreduce/interface/cypress.h252
-rw-r--r--yt/cpp/mapreduce/interface/error_codes.h468
-rw-r--r--yt/cpp/mapreduce/interface/error_ut.cpp81
-rw-r--r--yt/cpp/mapreduce/interface/errors.cpp437
-rw-r--r--yt/cpp/mapreduce/interface/errors.h290
-rw-r--r--yt/cpp/mapreduce/interface/finish_or_die.h41
-rw-r--r--yt/cpp/mapreduce/interface/fluent.h678
-rw-r--r--yt/cpp/mapreduce/interface/format.cpp135
-rw-r--r--yt/cpp/mapreduce/interface/format.h122
-rw-r--r--yt/cpp/mapreduce/interface/format_ut.cpp235
-rw-r--r--yt/cpp/mapreduce/interface/fwd.h397
-rw-r--r--yt/cpp/mapreduce/interface/init.h71
-rw-r--r--yt/cpp/mapreduce/interface/io-inl.h1015
-rw-r--r--yt/cpp/mapreduce/interface/io.cpp47
-rw-r--r--yt/cpp/mapreduce/interface/io.h586
-rw-r--r--yt/cpp/mapreduce/interface/job_counters.cpp164
-rw-r--r--yt/cpp/mapreduce/interface/job_counters.h74
-rw-r--r--yt/cpp/mapreduce/interface/job_counters_ut.cpp103
-rw-r--r--yt/cpp/mapreduce/interface/job_statistics.cpp361
-rw-r--r--yt/cpp/mapreduce/interface/job_statistics.h268
-rw-r--r--yt/cpp/mapreduce/interface/job_statistics_ut.cpp257
-rw-r--r--yt/cpp/mapreduce/interface/logging/logger.cpp188
-rw-r--r--yt/cpp/mapreduce/interface/logging/logger.h43
-rw-r--r--yt/cpp/mapreduce/interface/logging/ya.make16
-rw-r--r--yt/cpp/mapreduce/interface/logging/yt_log.cpp126
-rw-r--r--yt/cpp/mapreduce/interface/logging/yt_log.h17
-rw-r--r--yt/cpp/mapreduce/interface/mpl.h73
-rw-r--r--yt/cpp/mapreduce/interface/node.h7
-rw-r--r--yt/cpp/mapreduce/interface/operation-inl.h928
-rw-r--r--yt/cpp/mapreduce/interface/operation.cpp663
-rw-r--r--yt/cpp/mapreduce/interface/operation.h3494
-rw-r--r--yt/cpp/mapreduce/interface/operation_ut.cpp269
-rw-r--r--yt/cpp/mapreduce/interface/proto3_ut.proto17
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_file_options_ut.cpp271
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_file_options_ut.proto142
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_format.cpp1498
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_format.h106
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_table_schema_ut.cpp451
-rw-r--r--yt/cpp/mapreduce/interface/protobuf_table_schema_ut.proto402
-rw-r--r--yt/cpp/mapreduce/interface/public.h10
-rw-r--r--yt/cpp/mapreduce/interface/retry_policy.h47
-rw-r--r--yt/cpp/mapreduce/interface/serialize.cpp553
-rw-r--r--yt/cpp/mapreduce/interface/serialize.h90
-rw-r--r--yt/cpp/mapreduce/interface/serialize_ut.cpp49
-rw-r--r--yt/cpp/mapreduce/interface/skiff_row.cpp1
-rw-r--r--yt/cpp/mapreduce/interface/skiff_row.h127
-rw-r--r--yt/cpp/mapreduce/interface/tvm.cpp1
-rw-r--r--yt/cpp/mapreduce/interface/tvm.h35
-rw-r--r--yt/cpp/mapreduce/interface/ut/ya.make25
-rw-r--r--yt/cpp/mapreduce/interface/wait_proxy.h54
-rw-r--r--yt/cpp/mapreduce/interface/ya.make46
-rw-r--r--yt/cpp/mapreduce/io/counting_raw_reader.cpp38
-rw-r--r--yt/cpp/mapreduce/io/counting_raw_reader.h31
-rw-r--r--yt/cpp/mapreduce/io/helpers.h130
-rw-r--r--yt/cpp/mapreduce/io/job_reader.cpp46
-rw-r--r--yt/cpp/mapreduce/io/job_reader.h38
-rw-r--r--yt/cpp/mapreduce/io/job_writer.cpp68
-rw-r--r--yt/cpp/mapreduce/io/job_writer.h43
-rw-r--r--yt/cpp/mapreduce/io/lenval_table_reader.cpp198
-rw-r--r--yt/cpp/mapreduce/io/lenval_table_reader.h67
-rw-r--r--yt/cpp/mapreduce/io/node_table_reader.cpp375
-rw-r--r--yt/cpp/mapreduce/io/node_table_reader.h91
-rw-r--r--yt/cpp/mapreduce/io/node_table_writer.cpp72
-rw-r--r--yt/cpp/mapreduce/io/node_table_writer.h33
-rw-r--r--yt/cpp/mapreduce/io/proto_helpers.cpp101
-rw-r--r--yt/cpp/mapreduce/io/proto_helpers.h36
-rw-r--r--yt/cpp/mapreduce/io/proto_table_reader.cpp305
-rw-r--r--yt/cpp/mapreduce/io/proto_table_reader.h76
-rw-r--r--yt/cpp/mapreduce/io/proto_table_writer.cpp184
-rw-r--r--yt/cpp/mapreduce/io/proto_table_writer.h61
-rw-r--r--yt/cpp/mapreduce/io/skiff_row_table_reader.cpp232
-rw-r--r--yt/cpp/mapreduce/io/skiff_row_table_reader.h67
-rw-r--r--yt/cpp/mapreduce/io/skiff_table_reader.cpp293
-rw-r--r--yt/cpp/mapreduce/io/skiff_table_reader.h65
-rw-r--r--yt/cpp/mapreduce/io/stream_raw_reader.cpp59
-rw-r--r--yt/cpp/mapreduce/io/stream_table_reader.h65
-rw-r--r--yt/cpp/mapreduce/io/ya.make33
-rw-r--r--yt/cpp/mapreduce/io/yamr_table_reader.cpp145
-rw-r--r--yt/cpp/mapreduce/io/yamr_table_reader.h48
-rw-r--r--yt/cpp/mapreduce/io/yamr_table_writer.cpp53
-rw-r--r--yt/cpp/mapreduce/io/yamr_table_writer.h31
-rw-r--r--yt/cpp/mapreduce/library/table_schema/protobuf.cpp1
-rw-r--r--yt/cpp/mapreduce/library/table_schema/protobuf.h3
-rw-r--r--yt/cpp/mapreduce/library/table_schema/ya.make14
-rw-r--r--yt/cpp/mapreduce/raw_client/raw_batch_request.cpp687
-rw-r--r--yt/cpp/mapreduce/raw_client/raw_batch_request.h190
-rw-r--r--yt/cpp/mapreduce/raw_client/raw_requests.cpp1027
-rw-r--r--yt/cpp/mapreduce/raw_client/raw_requests.h397
-rw-r--r--yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp873
-rw-r--r--yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h231
-rw-r--r--yt/cpp/mapreduce/raw_client/ya.make19
-rw-r--r--yt/cpp/mapreduce/skiff/skiff_schema.h3
-rw-r--r--yt/cpp/mapreduce/skiff/unchecked_parser.h1
-rw-r--r--yt/cpp/mapreduce/skiff/wire_type.h1
-rw-r--r--yt/cpp/mapreduce/skiff/ya.make9
-rw-r--r--yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h194
-rw-r--r--yt/yt/client/api/client.cpp573
-rw-r--r--yt/yt/client/api/client.h2543
-rw-r--r--yt/yt/client/api/client_cache.cpp45
-rw-r--r--yt/yt/client/api/client_cache.h56
-rw-r--r--yt/yt/client/api/config.cpp190
-rw-r--r--yt/yt/client/api/config.h256
-rw-r--r--yt/yt/client/api/connection.h75
-rw-r--r--yt/yt/client/api/delegating_client.cpp993
-rw-r--r--yt/yt/client/api/delegating_client.h613
-rw-r--r--yt/yt/client/api/file_reader.h28
-rw-r--r--yt/yt/client/api/file_writer.h38
-rw-r--r--yt/yt/client/api/helpers.cpp24
-rw-r--r--yt/yt/client/api/helpers.h13
-rw-r--r--yt/yt/client/api/internal_client.cpp27
-rw-r--r--yt/yt/client/api/internal_client.h136
-rw-r--r--yt/yt/client/api/journal_reader.h32
-rw-r--r--yt/yt/client/api/journal_writer.h36
-rw-r--r--yt/yt/client/api/operation_archive_schema.cpp127
-rw-r--r--yt/yt/client/api/operation_archive_schema.h133
-rw-r--r--yt/yt/client/api/persistent_queue.cpp1054
-rw-r--r--yt/yt/client/api/persistent_queue.h137
-rw-r--r--yt/yt/client/api/private.h16
-rw-r--r--yt/yt/client/api/public.cpp10
-rw-r--r--yt/yt/client/api/public.h213
-rw-r--r--yt/yt/client/api/rowset.cpp197
-rw-r--r--yt/yt/client/api/rowset.h49
-rw-r--r--yt/yt/client/api/rpc_proxy/address_helpers.cpp72
-rw-r--r--yt/yt/client/api/rpc_proxy/address_helpers.h52
-rw-r--r--yt/yt/client/api/rpc_proxy/api_service_proxy.h174
-rw-r--r--yt/yt/client/api/rpc_proxy/client_base.cpp1020
-rw-r--r--yt/yt/client/api/rpc_proxy/client_base.h192
-rw-r--r--yt/yt/client/api/rpc_proxy/client_impl.cpp2039
-rw-r--r--yt/yt/client/api/rpc_proxy/client_impl.h499
-rw-r--r--yt/yt/client/api/rpc_proxy/config.cpp125
-rw-r--r--yt/yt/client/api/rpc_proxy/config.h79
-rw-r--r--yt/yt/client/api/rpc_proxy/connection.cpp18
-rw-r--r--yt/yt/client/api/rpc_proxy/connection.h23
-rw-r--r--yt/yt/client/api/rpc_proxy/connection_impl.cpp506
-rw-r--r--yt/yt/client/api/rpc_proxy/connection_impl.h90
-rw-r--r--yt/yt/client/api/rpc_proxy/discovery_service_proxy.h28
-rw-r--r--yt/yt/client/api/rpc_proxy/file_reader.cpp71
-rw-r--r--yt/yt/client/api/rpc_proxy/file_reader.h15
-rw-r--r--yt/yt/client/api/rpc_proxy/file_writer.cpp94
-rw-r--r--yt/yt/client/api/rpc_proxy/file_writer.h15
-rw-r--r--yt/yt/client/api/rpc_proxy/helpers.cpp1844
-rw-r--r--yt/yt/client/api/rpc_proxy/helpers.h280
-rw-r--r--yt/yt/client/api/rpc_proxy/journal_reader.cpp73
-rw-r--r--yt/yt/client/api/rpc_proxy/journal_reader.h15
-rw-r--r--yt/yt/client/api/rpc_proxy/journal_writer.cpp91
-rw-r--r--yt/yt/client/api/rpc_proxy/journal_writer.h15
-rw-r--r--yt/yt/client/api/rpc_proxy/private.h35
-rw-r--r--yt/yt/client/api/rpc_proxy/protocol_version.h3
-rw-r--r--yt/yt/client/api/rpc_proxy/protocol_version_variables.h.in10
-rw-r--r--yt/yt/client/api/rpc_proxy/public.cpp12
-rw-r--r--yt/yt/client/api/rpc_proxy/public.h42
-rw-r--r--yt/yt/client/api/rpc_proxy/row_stream.cpp138
-rw-r--r--yt/yt/client/api/rpc_proxy/row_stream.h66
-rw-r--r--yt/yt/client/api/rpc_proxy/table_mount_cache.cpp138
-rw-r--r--yt/yt/client/api/rpc_proxy/table_mount_cache.h21
-rw-r--r--yt/yt/client/api/rpc_proxy/table_reader.cpp257
-rw-r--r--yt/yt/client/api/rpc_proxy/table_reader.h14
-rw-r--r--yt/yt/client/api/rpc_proxy/table_writer.cpp106
-rw-r--r--yt/yt/client/api/rpc_proxy/table_writer.h15
-rw-r--r--yt/yt/client/api/rpc_proxy/timestamp_provider.cpp69
-rw-r--r--yt/yt/client/api/rpc_proxy/timestamp_provider.h21
-rw-r--r--yt/yt/client/api/rpc_proxy/transaction.cpp45
-rw-r--r--yt/yt/client/api/rpc_proxy/transaction.h31
-rw-r--r--yt/yt/client/api/rpc_proxy/transaction_impl-inl.h30
-rw-r--r--yt/yt/client/api/rpc_proxy/transaction_impl.cpp1075
-rw-r--r--yt/yt/client/api/rpc_proxy/transaction_impl.h292
-rw-r--r--yt/yt/client/api/rpc_proxy/wire_row_stream.cpp141
-rw-r--r--yt/yt/client/api/rpc_proxy/wire_row_stream.h16
-rw-r--r--yt/yt/client/api/skynet.cpp49
-rw-r--r--yt/yt/client/api/skynet.h32
-rw-r--r--yt/yt/client/api/sticky_transaction_pool.cpp191
-rw-r--r--yt/yt/client/api/sticky_transaction_pool.h33
-rw-r--r--yt/yt/client/api/table_reader.h51
-rw-r--r--yt/yt/client/api/table_writer.h37
-rw-r--r--yt/yt/client/api/transaction-inl.h42
-rw-r--r--yt/yt/client/api/transaction.cpp164
-rw-r--r--yt/yt/client/api/transaction.h193
-rw-r--r--yt/yt/client/arrow/arrow_row_stream_decoder.cpp20
-rw-r--r--yt/yt/client/arrow/arrow_row_stream_decoder.h19
-rw-r--r--yt/yt/client/arrow/arrow_row_stream_encoder.cpp1061
-rw-r--r--yt/yt/client/arrow/arrow_row_stream_encoder.h20
-rw-r--r--yt/yt/client/arrow/fbs/Message.fbs140
-rw-r--r--yt/yt/client/arrow/fbs/Schema.fbs353
-rw-r--r--yt/yt/client/arrow/fbs/SparseTensor.fbs218
-rw-r--r--yt/yt/client/arrow/fbs/Tensor.fbs54
-rw-r--r--yt/yt/client/arrow/public.cpp10
-rw-r--r--yt/yt/client/arrow/public.h13
-rw-r--r--yt/yt/client/arrow/ya.make20
-rw-r--r--yt/yt/client/chaos_client/config.h29
-rw-r--r--yt/yt/client/chaos_client/helpers.cpp95
-rw-r--r--yt/yt/client/chaos_client/public.h42
-rw-r--r--yt/yt/client/chaos_client/replication_card.cpp590
-rw-r--r--yt/yt/client/chaos_client/replication_card.h160
-rw-r--r--yt/yt/client/chaos_client/replication_card_cache.cpp33
-rw-r--r--yt/yt/client/chaos_client/replication_card_cache.h38
-rw-r--r--yt/yt/client/chaos_client/replication_card_serialization.cpp465
-rw-r--r--yt/yt/client/chaos_client/replication_card_serialization.h76
-rw-r--r--yt/yt/client/chunk_client/chunk_replica-inl.h290
-rw-r--r--yt/yt/client/chunk_client/chunk_replica.cpp260
-rw-r--r--yt/yt/client/chunk_client/chunk_replica.h260
-rw-r--r--yt/yt/client/chunk_client/config.cpp434
-rw-r--r--yt/yt/client/chunk_client/config.h500
-rw-r--r--yt/yt/client/chunk_client/data_statistics.cpp217
-rw-r--r--yt/yt/client/chunk_client/data_statistics.h75
-rw-r--r--yt/yt/client/chunk_client/helpers.cpp48
-rw-r--r--yt/yt/client/chunk_client/helpers.h22
-rw-r--r--yt/yt/client/chunk_client/public.cpp23
-rw-r--r--yt/yt/client/chunk_client/public.h201
-rw-r--r--yt/yt/client/chunk_client/read_limit.cpp1028
-rw-r--r--yt/yt/client/chunk_client/read_limit.h248
-rw-r--r--yt/yt/client/chunk_client/reader_base.h24
-rw-r--r--yt/yt/client/chunk_client/ready_event_reader_base.cpp39
-rw-r--r--yt/yt/client/chunk_client/ready_event_reader_base.h51
-rw-r--r--yt/yt/client/chunk_client/writer_base.h25
-rw-r--r--yt/yt/client/complex_types/check_type_compatibility.cpp444
-rw-r--r--yt/yt/client/complex_types/check_type_compatibility.h20
-rw-r--r--yt/yt/client/complex_types/check_yson_token-inl.h25
-rw-r--r--yt/yt/client/complex_types/check_yson_token.cpp22
-rw-r--r--yt/yt/client/complex_types/check_yson_token.h28
-rw-r--r--yt/yt/client/complex_types/infinite_entity.cpp39
-rw-r--r--yt/yt/client/complex_types/infinite_entity.h41
-rw-r--r--yt/yt/client/complex_types/public.h16
-rw-r--r--yt/yt/client/complex_types/scanner_factory.h231
-rw-r--r--yt/yt/client/complex_types/time_text.cpp62
-rw-r--r--yt/yt/client/complex_types/time_text.h15
-rw-r--r--yt/yt/client/complex_types/uuid_text.cpp125
-rw-r--r--yt/yt/client/complex_types/uuid_text.h19
-rw-r--r--yt/yt/client/complex_types/yson_format_conversion.cpp1267
-rw-r--r--yt/yt/client/complex_types/yson_format_conversion.h61
-rw-r--r--yt/yt/client/cypress_client/public.cpp12
-rw-r--r--yt/yt/client/cypress_client/public.h49
-rw-r--r--yt/yt/client/driver/admin_commands.cpp440
-rw-r--r--yt/yt/client/driver/admin_commands.h257
-rw-r--r--yt/yt/client/driver/authentication_commands.cpp97
-rw-r--r--yt/yt/client/driver/authentication_commands.h73
-rw-r--r--yt/yt/client/driver/chaos_commands.cpp58
-rw-r--r--yt/yt/client/driver/chaos_commands.h41
-rw-r--r--yt/yt/client/driver/command-inl.h272
-rw-r--r--yt/yt/client/driver/command.cpp119
-rw-r--r--yt/yt/client/driver/command.h321
-rw-r--r--yt/yt/client/driver/config.cpp76
-rw-r--r--yt/yt/client/driver/config.h56
-rw-r--r--yt/yt/client/driver/cypress_commands.cpp491
-rw-r--r--yt/yt/client/driver/cypress_commands.h266
-rw-r--r--yt/yt/client/driver/driver.cpp682
-rw-r--r--yt/yt/client/driver/driver.h167
-rw-r--r--yt/yt/client/driver/etc_commands.cpp436
-rw-r--r--yt/yt/client/driver/etc_commands.h223
-rw-r--r--yt/yt/client/driver/file_commands.cpp171
-rw-r--r--yt/yt/client/driver/file_commands.h74
-rw-r--r--yt/yt/client/driver/helpers.cpp66
-rw-r--r--yt/yt/client/driver/helpers.h31
-rw-r--r--yt/yt/client/driver/internal_commands.cpp142
-rw-r--r--yt/yt/client/driver/internal_commands.h95
-rw-r--r--yt/yt/client/driver/journal_commands.cpp343
-rw-r--r--yt/yt/client/driver/journal_commands.h60
-rw-r--r--yt/yt/client/driver/private.h20
-rw-r--r--yt/yt/client/driver/proxy_discovery_cache.cpp204
-rw-r--r--yt/yt/client/driver/proxy_discovery_cache.h58
-rw-r--r--yt/yt/client/driver/public.h22
-rw-r--r--yt/yt/client/driver/query_commands.cpp198
-rw-r--r--yt/yt/client/driver/query_commands.h112
-rw-r--r--yt/yt/client/driver/queue_commands.cpp200
-rw-r--r--yt/yt/client/driver/queue_commands.h115
-rw-r--r--yt/yt/client/driver/scheduler_commands.cpp611
-rw-r--r--yt/yt/client/driver/scheduler_commands.h382
-rw-r--r--yt/yt/client/driver/table_commands.cpp1378
-rw-r--r--yt/yt/client/driver/table_commands.h518
-rw-r--r--yt/yt/client/driver/transaction_commands.cpp141
-rw-r--r--yt/yt/client/driver/transaction_commands.h67
-rw-r--r--yt/yt/client/driver/ya.make31
-rw-r--r--yt/yt/client/election/public.cpp12
-rw-r--r--yt/yt/client/election/public.h38
-rw-r--r--yt/yt/client/federated/client.cpp714
-rw-r--r--yt/yt/client/federated/client.h28
-rw-r--r--yt/yt/client/federated/config.cpp35
-rw-r--r--yt/yt/client/federated/config.h51
-rw-r--r--yt/yt/client/federated/connection.cpp137
-rw-r--r--yt/yt/client/federated/connection.h25
-rw-r--r--yt/yt/client/federated/private.h15
-rw-r--r--yt/yt/client/federated/public.h14
-rw-r--r--yt/yt/client/federated/unittests/client_ut.cpp480
-rw-r--r--yt/yt/client/federated/unittests/connection_ut.cpp75
-rw-r--r--yt/yt/client/federated/unittests/ya.make21
-rw-r--r--yt/yt/client/federated/ya.make17
-rw-r--r--yt/yt/client/file_client/config.h33
-rw-r--r--yt/yt/client/file_client/public.h13
-rw-r--r--yt/yt/client/formats/config.cpp355
-rw-r--r--yt/yt/client/formats/config.h419
-rw-r--r--yt/yt/client/formats/dsv_parser.cpp236
-rw-r--r--yt/yt/client/formats/dsv_parser.h35
-rw-r--r--yt/yt/client/formats/dsv_writer.cpp278
-rw-r--r--yt/yt/client/formats/dsv_writer.h89
-rw-r--r--yt/yt/client/formats/escape.cpp249
-rw-r--r--yt/yt/client/formats/escape.h94
-rw-r--r--yt/yt/client/formats/format.cpp660
-rw-r--r--yt/yt/client/formats/format.h131
-rw-r--r--yt/yt/client/formats/helpers.cpp104
-rw-r--r--yt/yt/client/formats/helpers.h37
-rw-r--r--yt/yt/client/formats/lenval_control_constants.h18
-rw-r--r--yt/yt/client/formats/parser.cpp28
-rw-r--r--yt/yt/client/formats/parser.h29
-rw-r--r--yt/yt/client/formats/private.h20
-rw-r--r--yt/yt/client/formats/protobuf.cpp1633
-rw-r--r--yt/yt/client/formats/protobuf.h384
-rw-r--r--yt/yt/client/formats/protobuf_options.cpp503
-rw-r--r--yt/yt/client/formats/protobuf_options.h99
-rw-r--r--yt/yt/client/formats/protobuf_parser.cpp734
-rw-r--r--yt/yt/client/formats/protobuf_parser.h17
-rw-r--r--yt/yt/client/formats/protobuf_writer.cpp1078
-rw-r--r--yt/yt/client/formats/protobuf_writer.h35
-rw-r--r--yt/yt/client/formats/public.h91
-rw-r--r--yt/yt/client/formats/schemaful_dsv_parser.cpp198
-rw-r--r--yt/yt/client/formats/schemaful_dsv_parser.h31
-rw-r--r--yt/yt/client/formats/schemaful_dsv_writer.cpp397
-rw-r--r--yt/yt/client/formats/schemaful_dsv_writer.h50
-rw-r--r--yt/yt/client/formats/schemaful_writer.cpp119
-rw-r--r--yt/yt/client/formats/schemaful_writer.h52
-rw-r--r--yt/yt/client/formats/schemaless_writer_adapter.cpp440
-rw-r--r--yt/yt/client/formats/schemaless_writer_adapter.h157
-rw-r--r--yt/yt/client/formats/skiff_parser.cpp631
-rw-r--r--yt/yt/client/formats/skiff_parser.h29
-rw-r--r--yt/yt/client/formats/skiff_writer.cpp1112
-rw-r--r--yt/yt/client/formats/skiff_writer.h37
-rw-r--r--yt/yt/client/formats/skiff_yson_converter-inl.h157
-rw-r--r--yt/yt/client/formats/skiff_yson_converter.cpp1899
-rw-r--r--yt/yt/client/formats/skiff_yson_converter.h113
-rw-r--r--yt/yt/client/formats/unversioned_value_yson_writer.cpp84
-rw-r--r--yt/yt/client/formats/unversioned_value_yson_writer.h28
-rw-r--r--yt/yt/client/formats/versioned_writer.cpp138
-rw-r--r--yt/yt/client/formats/versioned_writer.h48
-rw-r--r--yt/yt/client/formats/web_json_writer.cpp772
-rw-r--r--yt/yt/client/formats/web_json_writer.h33
-rw-r--r--yt/yt/client/formats/ya.make45
-rw-r--r--yt/yt/client/formats/yamr_parser.cpp98
-rw-r--r--yt/yt/client/formats/yamr_parser.h28
-rw-r--r--yt/yt/client/formats/yamr_parser_base.cpp453
-rw-r--r--yt/yt/client/formats/yamr_parser_base.h160
-rw-r--r--yt/yt/client/formats/yamr_writer.cpp211
-rw-r--r--yt/yt/client/formats/yamr_writer.h34
-rw-r--r--yt/yt/client/formats/yamr_writer_base.cpp90
-rw-r--r--yt/yt/client/formats/yamr_writer_base.h45
-rw-r--r--yt/yt/client/formats/yamred_dsv_parser.cpp130
-rw-r--r--yt/yt/client/formats/yamred_dsv_parser.h29
-rw-r--r--yt/yt/client/formats/yamred_dsv_writer.cpp251
-rw-r--r--yt/yt/client/formats/yamred_dsv_writer.h35
-rw-r--r--yt/yt/client/formats/yql_yson_converter.cpp855
-rw-r--r--yt/yt/client/formats/yql_yson_converter.h41
-rw-r--r--yt/yt/client/formats/yson_map_to_unversioned_value.cpp181
-rw-r--r--yt/yt/client/formats/yson_map_to_unversioned_value.h59
-rw-r--r--yt/yt/client/formats/yson_parser.cpp56
-rw-r--r--yt/yt/client/formats/yson_parser.h18
-rw-r--r--yt/yt/client/hedging/cache.cpp93
-rw-r--r--yt/yt/client/hedging/cache.h50
-rw-r--r--yt/yt/client/hedging/config.cpp67
-rw-r--r--yt/yt/client/hedging/config.h69
-rw-r--r--yt/yt/client/hedging/counter.cpp51
-rw-r--r--yt/yt/client/hedging/counter.h50
-rw-r--r--yt/yt/client/hedging/hedging.cpp283
-rw-r--r--yt/yt/client/hedging/hedging.h65
-rw-r--r--yt/yt/client/hedging/hedging_executor.cpp80
-rw-r--r--yt/yt/client/hedging/hedging_executor.h110
-rw-r--r--yt/yt/client/hedging/logger.cpp11
-rw-r--r--yt/yt/client/hedging/logger.h13
-rw-r--r--yt/yt/client/hedging/options.cpp48
-rw-r--r--yt/yt/client/hedging/options.h19
-rw-r--r--yt/yt/client/hedging/penalty_provider.cpp230
-rw-r--r--yt/yt/client/hedging/penalty_provider.h41
-rw-r--r--yt/yt/client/hedging/public.h17
-rw-r--r--yt/yt/client/hedging/rpc.cpp156
-rw-r--r--yt/yt/client/hedging/rpc.h50
-rw-r--r--yt/yt/client/hedging/unittests/cache_ut.cpp110
-rw-r--r--yt/yt/client/hedging/unittests/counters_ut.cpp241
-rw-r--r--yt/yt/client/hedging/unittests/hedging_ut.cpp533
-rw-r--r--yt/yt/client/hedging/unittests/hook.cpp20
-rw-r--r--yt/yt/client/hedging/unittests/options_ut.cpp83
-rw-r--r--yt/yt/client/hedging/unittests/penalty_provider_ut.cpp224
-rw-r--r--yt/yt/client/hedging/unittests/rpc_ut.cpp35
-rw-r--r--yt/yt/client/hedging/unittests/ya.make30
-rw-r--r--yt/yt/client/hedging/ya.make28
-rw-r--r--yt/yt/client/hive/public.h41
-rw-r--r--yt/yt/client/hive/timestamp_map.cpp77
-rw-r--r--yt/yt/client/hive/timestamp_map.h32
-rw-r--r--yt/yt/client/hive/transaction_participant.h60
-rw-r--r--yt/yt/client/hydra/public.h62
-rw-r--r--yt/yt/client/hydra/version.cpp112
-rw-r--r--yt/yt/client/hydra/version.h80
-rw-r--r--yt/yt/client/job_tracker_client/helpers.cpp29
-rw-r--r--yt/yt/client/job_tracker_client/helpers.h14
-rw-r--r--yt/yt/client/job_tracker_client/public.cpp13
-rw-r--r--yt/yt/client/job_tracker_client/public.h76
-rw-r--r--yt/yt/client/journal_client/config.cpp17
-rw-r--r--yt/yt/client/journal_client/config.h34
-rw-r--r--yt/yt/client/journal_client/public.cpp10
-rw-r--r--yt/yt/client/journal_client/public.h17
-rw-r--r--yt/yt/client/misc/config.cpp15
-rw-r--r--yt/yt/client/misc/config.h28
-rw-r--r--yt/yt/client/misc/io_tags.cpp32
-rw-r--r--yt/yt/client/misc/io_tags.h54
-rw-r--r--yt/yt/client/misc/method_helpers.cpp12
-rw-r--r--yt/yt/client/misc/method_helpers.h59
-rw-r--r--yt/yt/client/misc/public.h53
-rw-r--r--yt/yt/client/misc/workload.cpp187
-rw-r--r--yt/yt/client/misc/workload.h73
-rw-r--r--yt/yt/client/node_tracker_client/helpers.cpp58
-rw-r--r--yt/yt/client/node_tracker_client/helpers.h29
-rw-r--r--yt/yt/client/node_tracker_client/node_directory.cpp751
-rw-r--r--yt/yt/client/node_tracker_client/node_directory.h195
-rw-r--r--yt/yt/client/node_tracker_client/private.h15
-rw-r--r--yt/yt/client/node_tracker_client/public.cpp13
-rw-r--r--yt/yt/client/node_tracker_client/public.h68
-rw-r--r--yt/yt/client/object_client/helpers-inl.h186
-rw-r--r--yt/yt/client/object_client/helpers.cpp263
-rw-r--r--yt/yt/client/object_client/helpers.h191
-rw-r--r--yt/yt/client/object_client/public.cpp68
-rw-r--r--yt/yt/client/object_client/public.h412
-rw-r--r--yt/yt/client/query_client/public.h20
-rw-r--r--yt/yt/client/query_client/query_builder.cpp159
-rw-r--r--yt/yt/client/query_client/query_builder.h70
-rw-r--r--yt/yt/client/query_client/query_statistics.cpp117
-rw-r--r--yt/yt/client/query_client/query_statistics.h41
-rw-r--r--yt/yt/client/query_tracker_client/public.cpp1
-rw-r--r--yt/yt/client/query_tracker_client/public.h44
-rw-r--r--yt/yt/client/query_tracker_client/ya.make13
-rw-r--r--yt/yt/client/queue_client/common.cpp71
-rw-r--r--yt/yt/client/queue_client/common.h39
-rw-r--r--yt/yt/client/queue_client/config.cpp45
-rw-r--r--yt/yt/client/queue_client/config.h60
-rw-r--r--yt/yt/client/queue_client/consumer_client.cpp583
-rw-r--r--yt/yt/client/queue_client/consumer_client.h123
-rw-r--r--yt/yt/client/queue_client/partition_reader.cpp481
-rw-r--r--yt/yt/client/queue_client/partition_reader.h47
-rw-r--r--yt/yt/client/queue_client/private.h13
-rw-r--r--yt/yt/client/queue_client/public.h29
-rw-r--r--yt/yt/client/queue_client/queue_rowset.cpp110
-rw-r--r--yt/yt/client/queue_client/queue_rowset.h48
-rw-r--r--yt/yt/client/scheduler/operation_cache.cpp40
-rw-r--r--yt/yt/client/scheduler/operation_cache.h39
-rw-r--r--yt/yt/client/scheduler/operation_id_or_alias-inl.h57
-rw-r--r--yt/yt/client/scheduler/operation_id_or_alias.cpp62
-rw-r--r--yt/yt/client/scheduler/operation_id_or_alias.h46
-rw-r--r--yt/yt/client/scheduler/private.h16
-rw-r--r--yt/yt/client/scheduler/public.h158
-rw-r--r--yt/yt/client/security_client/access_control.cpp64
-rw-r--r--yt/yt/client/security_client/access_control.h57
-rw-r--r--yt/yt/client/security_client/acl.cpp201
-rw-r--r--yt/yt/client/security_client/acl.h60
-rw-r--r--yt/yt/client/security_client/helpers.cpp81
-rw-r--r--yt/yt/client/security_client/helpers.h31
-rw-r--r--yt/yt/client/security_client/public.cpp37
-rw-r--r--yt/yt/client/security_client/public.h94
-rw-r--r--yt/yt/client/table_client/adapters.cpp208
-rw-r--r--yt/yt/client/table_client/adapters.h49
-rw-r--r--yt/yt/client/table_client/blob_reader.cpp219
-rw-r--r--yt/yt/client/table_client/blob_reader.h36
-rw-r--r--yt/yt/client/table_client/check_schema_compatibility.cpp235
-rw-r--r--yt/yt/client/table_client/check_schema_compatibility.h22
-rw-r--r--yt/yt/client/table_client/chunk_stripe_statistics.cpp68
-rw-r--r--yt/yt/client/table_client/chunk_stripe_statistics.h39
-rw-r--r--yt/yt/client/table_client/column_rename_descriptor.cpp53
-rw-r--r--yt/yt/client/table_client/column_rename_descriptor.h27
-rw-r--r--yt/yt/client/table_client/column_sort_schema.cpp153
-rw-r--r--yt/yt/client/table_client/column_sort_schema.h52
-rw-r--r--yt/yt/client/table_client/columnar-inl.h378
-rw-r--r--yt/yt/client/table_client/columnar.cpp838
-rw-r--r--yt/yt/client/table_client/columnar.h247
-rw-r--r--yt/yt/client/table_client/columnar_statistics.cpp71
-rw-r--r--yt/yt/client/table_client/columnar_statistics.h51
-rw-r--r--yt/yt/client/table_client/comparator-inl.h58
-rw-r--r--yt/yt/client/table_client/comparator.cpp509
-rw-r--r--yt/yt/client/table_client/comparator.h180
-rw-r--r--yt/yt/client/table_client/composite_compare.cpp279
-rw-r--r--yt/yt/client/table_client/composite_compare.h18
-rw-r--r--yt/yt/client/table_client/config.cpp402
-rw-r--r--yt/yt/client/table_client/config.h363
-rw-r--r--yt/yt/client/table_client/helpers-inl.h533
-rw-r--r--yt/yt/client/table_client/helpers.cpp1510
-rw-r--r--yt/yt/client/table_client/helpers.h343
-rw-r--r--yt/yt/client/table_client/key.cpp190
-rw-r--r--yt/yt/client/table_client/key.h84
-rw-r--r--yt/yt/client/table_client/key_bound.cpp530
-rw-r--r--yt/yt/client/table_client/key_bound.h190
-rw-r--r--yt/yt/client/table_client/key_bound_compressor.cpp164
-rw-r--r--yt/yt/client/table_client/key_bound_compressor.h73
-rw-r--r--yt/yt/client/table_client/logical_type-inl.h145
-rw-r--r--yt/yt/client/table_client/logical_type.cpp2214
-rw-r--r--yt/yt/client/table_client/logical_type.h447
-rw-r--r--yt/yt/client/table_client/name_table.cpp304
-rw-r--r--yt/yt/client/table_client/name_table.h113
-rw-r--r--yt/yt/client/table_client/pipe.cpp292
-rw-r--r--yt/yt/client/table_client/pipe.h52
-rw-r--r--yt/yt/client/table_client/public.cpp19
-rw-r--r--yt/yt/client/table_client/public.h396
-rw-r--r--yt/yt/client/table_client/record_codegen_cpp.cpp36
-rw-r--r--yt/yt/client/table_client/record_codegen_cpp.h24
-rw-r--r--yt/yt/client/table_client/record_helpers-inl.h166
-rw-r--r--yt/yt/client/table_client/record_helpers.cpp20
-rw-r--r--yt/yt/client/table_client/record_helpers.h91
-rw-r--r--yt/yt/client/table_client/row_base.cpp189
-rw-r--r--yt/yt/client/table_client/row_base.h458
-rw-r--r--yt/yt/client/table_client/row_batch-inl.h70
-rw-r--r--yt/yt/client/table_client/row_batch.cpp144
-rw-r--r--yt/yt/client/table_client/row_batch.h269
-rw-r--r--yt/yt/client/table_client/row_buffer.cpp310
-rw-r--r--yt/yt/client/table_client/row_buffer.h114
-rw-r--r--yt/yt/client/table_client/schema-inl.h227
-rw-r--r--yt/yt/client/table_client/schema.cpp2002
-rw-r--r--yt/yt/client/table_client/schema.h604
-rw-r--r--yt/yt/client/table_client/schema_serialization_helpers.cpp343
-rw-r--r--yt/yt/client/table_client/schema_serialization_helpers.h51
-rw-r--r--yt/yt/client/table_client/schemaful_reader_adapter.cpp207
-rw-r--r--yt/yt/client/table_client/schemaful_reader_adapter.h21
-rw-r--r--yt/yt/client/table_client/schemaless_row_reorderer.cpp86
-rw-r--r--yt/yt/client/table_client/schemaless_row_reorderer.h39
-rw-r--r--yt/yt/client/table_client/serialize.cpp16
-rw-r--r--yt/yt/client/table_client/serialize.h35
-rw-r--r--yt/yt/client/table_client/table_consumer.cpp601
-rw-r--r--yt/yt/client/table_client/table_consumer.h159
-rw-r--r--yt/yt/client/table_client/table_output.cpp36
-rw-r--r--yt/yt/client/table_client/table_output.h29
-rw-r--r--yt/yt/client/table_client/unittests/columnar_ut.cpp717
-rw-r--r--yt/yt/client/table_client/unittests/helpers/helpers.cpp260
-rw-r--r--yt/yt/client/table_client/unittests/helpers/helpers.h231
-rw-r--r--yt/yt/client/table_client/unittests/helpers/ya.make14
-rw-r--r--yt/yt/client/table_client/unittests/serialization_ut.cpp71
-rw-r--r--yt/yt/client/table_client/unittests/ya.make24
-rw-r--r--yt/yt/client/table_client/unordered_schemaful_reader.cpp312
-rw-r--r--yt/yt/client/table_client/unordered_schemaful_reader.h26
-rw-r--r--yt/yt/client/table_client/unversioned_reader.h41
-rw-r--r--yt/yt/client/table_client/unversioned_row.cpp2097
-rw-r--r--yt/yt/client/table_client/unversioned_row.h965
-rw-r--r--yt/yt/client/table_client/unversioned_value.cpp239
-rw-r--r--yt/yt/client/table_client/unversioned_value.h117
-rw-r--r--yt/yt/client/table_client/unversioned_writer.h53
-rw-r--r--yt/yt/client/table_client/validate_logical_type-inl.h209
-rw-r--r--yt/yt/client/table_client/validate_logical_type.cpp635
-rw-r--r--yt/yt/client/table_client/validate_logical_type.h40
-rw-r--r--yt/yt/client/table_client/value_consumer.cpp423
-rw-r--r--yt/yt/client/table_client/value_consumer.h157
-rw-r--r--yt/yt/client/table_client/versioned_reader.cpp82
-rw-r--r--yt/yt/client/table_client/versioned_reader.h63
-rw-r--r--yt/yt/client/table_client/versioned_row.cpp602
-rw-r--r--yt/yt/client/table_client/versioned_row.h666
-rw-r--r--yt/yt/client/table_client/versioned_writer.h37
-rw-r--r--yt/yt/client/table_client/wire_protocol.cpp1291
-rw-r--r--yt/yt/client/table_client/wire_protocol.h323
-rw-r--r--yt/yt/client/tablet_client/config.cpp122
-rw-r--r--yt/yt/client/tablet_client/config.h118
-rw-r--r--yt/yt/client/tablet_client/helpers.cpp24
-rw-r--r--yt/yt/client/tablet_client/helpers.h14
-rw-r--r--yt/yt/client/tablet_client/public.cpp25
-rw-r--r--yt/yt/client/tablet_client/public.h224
-rw-r--r--yt/yt/client/tablet_client/table_mount_cache.cpp182
-rw-r--r--yt/yt/client/tablet_client/table_mount_cache.h172
-rw-r--r--yt/yt/client/tablet_client/table_mount_cache_detail.cpp266
-rw-r--r--yt/yt/client/tablet_client/table_mount_cache_detail.h80
-rw-r--r--yt/yt/client/transaction_client/batching_timestamp_provider.cpp170
-rw-r--r--yt/yt/client/transaction_client/batching_timestamp_provider.h17
-rw-r--r--yt/yt/client/transaction_client/config.cpp34
-rw-r--r--yt/yt/client/transaction_client/config.h41
-rw-r--r--yt/yt/client/transaction_client/helpers.cpp101
-rw-r--r--yt/yt/client/transaction_client/helpers.h63
-rw-r--r--yt/yt/client/transaction_client/noop_timestamp_provider.cpp34
-rw-r--r--yt/yt/client/transaction_client/noop_timestamp_provider.h13
-rw-r--r--yt/yt/client/transaction_client/private.h13
-rw-r--r--yt/yt/client/transaction_client/public.h102
-rw-r--r--yt/yt/client/transaction_client/remote_timestamp_provider.cpp111
-rw-r--r--yt/yt/client/transaction_client/remote_timestamp_provider.h34
-rw-r--r--yt/yt/client/transaction_client/timestamp_provider.h32
-rw-r--r--yt/yt/client/transaction_client/timestamp_provider_base.cpp101
-rw-r--r--yt/yt/client/transaction_client/timestamp_provider_base.h41
-rw-r--r--yt/yt/client/transaction_client/timestamp_service_proxy.h26
-rw-r--r--yt/yt/client/unittests/check_schema_compatibility_ut.cpp349
-rw-r--r--yt/yt/client/unittests/check_type_compatibility_ut.cpp477
-rw-r--r--yt/yt/client/unittests/chunk_replica_ut.cpp47
-rw-r--r--yt/yt/client/unittests/column_sort_schema_ut.cpp70
-rw-r--r--yt/yt/client/unittests/comparator_ut.cpp365
-rw-r--r--yt/yt/client/unittests/composite_compare_ut.cpp76
-rw-r--r--yt/yt/client/unittests/connection_ut.cpp48
-rw-r--r--yt/yt/client/unittests/dsv_parser_ut.cpp365
-rw-r--r--yt/yt/client/unittests/dsv_writer_ut.cpp316
-rw-r--r--yt/yt/client/unittests/farm_fingerprint_stability_ut.cpp99
-rw-r--r--yt/yt/client/unittests/format_writer_ut.h36
-rw-r--r--yt/yt/client/unittests/key_bound_compressor_ut.cpp88
-rw-r--r--yt/yt/client/unittests/key_bound_ut.cpp258
-rw-r--r--yt/yt/client/unittests/key_helpers.cpp28
-rw-r--r--yt/yt/client/unittests/key_helpers.h17
-rw-r--r--yt/yt/client/unittests/key_ut.cpp63
-rw-r--r--yt/yt/client/unittests/logical_type_shortcuts.h135
-rw-r--r--yt/yt/client/unittests/logical_type_ut.cpp1059
-rw-r--r--yt/yt/client/unittests/mock/client.h613
-rw-r--r--yt/yt/client/unittests/mock/connection.h44
-rw-r--r--yt/yt/client/unittests/mock/table_value_consumer.h49
-rw-r--r--yt/yt/client/unittests/mock/transaction.h209
-rw-r--r--yt/yt/client/unittests/mock/ya.make18
-rw-r--r--yt/yt/client/unittests/named_yson_token_ut.cpp301
-rw-r--r--yt/yt/client/unittests/node_directory_ut.cpp33
-rw-r--r--yt/yt/client/unittests/protobuf_format_ut.cpp4657
-rw-r--r--yt/yt/client/unittests/protobuf_format_ut.proto255
-rw-r--r--yt/yt/client/unittests/query_builder_ut.cpp48
-rw-r--r--yt/yt/client/unittests/read_limit_ut.cpp196
-rw-r--r--yt/yt/client/unittests/replication_progress_ut.cpp410
-rw-r--r--yt/yt/client/unittests/row_helpers.cpp70
-rw-r--r--yt/yt/client/unittests/row_helpers.h111
-rw-r--r--yt/yt/client/unittests/row_ut.cpp165
-rw-r--r--yt/yt/client/unittests/schema_ut.cpp506
-rw-r--r--yt/yt/client/unittests/schemaful_dsv_parser_ut.cpp259
-rw-r--r--yt/yt/client/unittests/schemaful_dsv_writer_ut.cpp344
-rw-r--r--yt/yt/client/unittests/skiff_format_ut.cpp3006
-rw-r--r--yt/yt/client/unittests/skiff_yson_converter_ut.cpp728
-rw-r--r--yt/yt/client/unittests/table_consumer_ut.cpp131
-rw-r--r--yt/yt/client/unittests/time_text_ut.cpp67
-rw-r--r--yt/yt/client/unittests/unordered_reader_ut.cpp104
-rw-r--r--yt/yt/client/unittests/unversioned_row_ut.cpp250
-rw-r--r--yt/yt/client/unittests/uuid_text_ut.cpp64
-rw-r--r--yt/yt/client/unittests/validate_logical_type_ut.cpp467
-rw-r--r--yt/yt/client/unittests/value_examples.cpp147
-rw-r--r--yt/yt/client/unittests/value_examples.h24
-rw-r--r--yt/yt/client/unittests/web_json_writer_ut.cpp1570
-rw-r--r--yt/yt/client/unittests/wire_protocol_ut.cpp285
-rw-r--r--yt/yt/client/unittests/ya.make84
-rw-r--r--yt/yt/client/unittests/yamr_parser_ut.cpp606
-rw-r--r--yt/yt/client/unittests/yamr_writer_ut.cpp644
-rw-r--r--yt/yt/client/unittests/yamred_dsv_parser_ut.cpp187
-rw-r--r--yt/yt/client/unittests/yamred_dsv_writer_ut.cpp425
-rw-r--r--yt/yt/client/unittests/ypath_ut.cpp951
-rw-r--r--yt/yt/client/unittests/yson_helpers.cpp29
-rw-r--r--yt/yt/client/unittests/yson_helpers.h13
-rw-r--r--yt/yt/client/unittests/zookeeper_bus_ut.cpp165
-rw-r--r--yt/yt/client/unittests/zookeeper_protocol_ut.cpp106
-rw-r--r--yt/yt/client/ya.make207
-rw-r--r--yt/yt/client/ypath/parser_detail.cpp460
-rw-r--r--yt/yt/client/ypath/parser_detail.h13
-rw-r--r--yt/yt/client/ypath/public.h21
-rw-r--r--yt/yt/client/ypath/rich.cpp741
-rw-r--r--yt/yt/client/ypath/rich.h194
-rw-r--r--yt/yt/client/zookeeper/packet.cpp299
-rw-r--r--yt/yt/client/zookeeper/packet.h15
-rw-r--r--yt/yt/client/zookeeper/protocol.cpp204
-rw-r--r--yt/yt/client/zookeeper/protocol.h59
-rw-r--r--yt/yt/client/zookeeper/public.h19
-rw-r--r--yt/yt/client/zookeeper/requests.cpp28
-rw-r--r--yt/yt/client/zookeeper/requests.h42
-rw-r--r--yt/yt/core/actions/bind-inl.h603
-rw-r--r--yt/yt/core/actions/bind.h233
-rw-r--r--yt/yt/core/actions/callback.h261
-rw-r--r--yt/yt/core/actions/callback_internal-inl.h71
-rw-r--r--yt/yt/core/actions/callback_internal.h116
-rw-r--r--yt/yt/core/actions/cancelable_context-inl.h20
-rw-r--r--yt/yt/core/actions/cancelable_context.cpp150
-rw-r--r--yt/yt/core/actions/cancelable_context.h70
-rw-r--r--yt/yt/core/actions/current_invoker.cpp52
-rw-r--r--yt/yt/core/actions/current_invoker.h36
-rw-r--r--yt/yt/core/actions/future-inl.h2519
-rw-r--r--yt/yt/core/actions/future.cpp258
-rw-r--r--yt/yt/core/actions/future.h724
-rw-r--r--yt/yt/core/actions/invoker-inl.h29
-rw-r--r--yt/yt/core/actions/invoker.h95
-rw-r--r--yt/yt/core/actions/invoker_detail.cpp68
-rw-r--r--yt/yt/core/actions/invoker_detail.h54
-rw-r--r--yt/yt/core/actions/invoker_pool-inl.h74
-rw-r--r--yt/yt/core/actions/invoker_pool.cpp32
-rw-r--r--yt/yt/core/actions/invoker_pool.h131
-rw-r--r--yt/yt/core/actions/invoker_util.cpp187
-rw-r--r--yt/yt/core/actions/invoker_util.h45
-rw-r--r--yt/yt/core/actions/new_with_offloaded_dtor-inl.h29
-rw-r--r--yt/yt/core/actions/new_with_offloaded_dtor.h21
-rw-r--r--yt/yt/core/actions/public.h52
-rw-r--r--yt/yt/core/actions/signal-inl.h188
-rw-r--r--yt/yt/core/actions/signal.h272
-rw-r--r--yt/yt/core/actions/unittests/actions_ut.cpp98
-rw-r--r--yt/yt/core/actions/unittests/invoker_ut.cpp244
-rw-r--r--yt/yt/core/actions/unittests/new_with_offloaded_dtor_ut.cpp68
-rw-r--r--yt/yt/core/actions/unittests/ya.make45
-rw-r--r--yt/yt/core/bus/bus.h169
-rw-r--r--yt/yt/core/bus/client.h48
-rw-r--r--yt/yt/core/bus/private.h31
-rw-r--r--yt/yt/core/bus/public.cpp13
-rw-r--r--yt/yt/core/bus/public.h49
-rw-r--r--yt/yt/core/bus/server.h40
-rw-r--r--yt/yt/core/bus/tcp/client.cpp222
-rw-r--r--yt/yt/core/bus/tcp/client.h18
-rw-r--r--yt/yt/core/bus/tcp/config.cpp146
-rw-r--r--yt/yt/core/bus/tcp/config.h140
-rw-r--r--yt/yt/core/bus/tcp/connection.cpp1596
-rw-r--r--yt/yt/core/bus/tcp/connection.h330
-rw-r--r--yt/yt/core/bus/tcp/dispatcher.cpp63
-rw-r--r--yt/yt/core/bus/tcp/dispatcher.h81
-rw-r--r--yt/yt/core/bus/tcp/dispatcher_impl.cpp319
-rw-r--r--yt/yt/core/bus/tcp/dispatcher_impl.h107
-rw-r--r--yt/yt/core/bus/tcp/packet.cpp531
-rw-r--r--yt/yt/core/bus/tcp/packet.h84
-rw-r--r--yt/yt/core/bus/tcp/private.h14
-rw-r--r--yt/yt/core/bus/tcp/public.h24
-rw-r--r--yt/yt/core/bus/tcp/server.cpp492
-rw-r--r--yt/yt/core/bus/tcp/server.h17
-rw-r--r--yt/yt/core/bus/unittests/bus_ut.cpp367
-rw-r--r--yt/yt/core/bus/unittests/ya.make41
-rw-r--r--yt/yt/core/compression/brotli.cpp69
-rw-r--r--yt/yt/core/compression/brotli.h15
-rw-r--r--yt/yt/core/compression/bzip2.cpp133
-rw-r--r--yt/yt/core/compression/bzip2.h14
-rw-r--r--yt/yt/core/compression/codec.cpp457
-rw-r--r--yt/yt/core/compression/codec.h43
-rw-r--r--yt/yt/core/compression/lz.cpp303
-rw-r--r--yt/yt/core/compression/lz.h15
-rw-r--r--yt/yt/core/compression/lzma.cpp219
-rw-r--r--yt/yt/core/compression/lzma.h16
-rw-r--r--yt/yt/core/compression/private.h15
-rw-r--r--yt/yt/core/compression/public.cpp10
-rw-r--r--yt/yt/core/compression/public.h96
-rw-r--r--yt/yt/core/compression/snappy.cpp104
-rw-r--r--yt/yt/core/compression/snappy.h14
-rw-r--r--yt/yt/core/compression/stream.cpp107
-rw-r--r--yt/yt/core/compression/stream.h96
-rw-r--r--yt/yt/core/compression/unittests/codec_ut.cpp123
-rw-r--r--yt/yt/core/compression/unittests/stream_ut.cpp93
-rw-r--r--yt/yt/core/compression/unittests/ya.make40
-rw-r--r--yt/yt/core/compression/zlib.cpp155
-rw-r--r--yt/yt/core/compression/zlib.h15
-rw-r--r--yt/yt/core/compression/zstd.cpp177
-rw-r--r--yt/yt/core/compression/zstd.h15
-rw-r--r--yt/yt/core/concurrency/action_queue.cpp722
-rw-r--r--yt/yt/core/concurrency/action_queue.h111
-rw-r--r--yt/yt/core/concurrency/async_barrier.cpp87
-rw-r--r--yt/yt/core/concurrency/async_barrier.h64
-rw-r--r--yt/yt/core/concurrency/async_rw_lock-inl.h43
-rw-r--r--yt/yt/core/concurrency/async_rw_lock.cpp129
-rw-r--r--yt/yt/core/concurrency/async_rw_lock.h93
-rw-r--r--yt/yt/core/concurrency/async_semaphore.cpp286
-rw-r--r--yt/yt/core/concurrency/async_semaphore.h144
-rw-r--r--yt/yt/core/concurrency/async_stream.cpp1058
-rw-r--r--yt/yt/core/concurrency/async_stream.h229
-rw-r--r--yt/yt/core/concurrency/async_stream_pipe.cpp55
-rw-r--r--yt/yt/core/concurrency/async_stream_pipe.h42
-rw-r--r--yt/yt/core/concurrency/config.cpp84
-rw-r--r--yt/yt/core/concurrency/config.h89
-rw-r--r--yt/yt/core/concurrency/coroutine-inl.h118
-rw-r--r--yt/yt/core/concurrency/coroutine.cpp51
-rw-r--r--yt/yt/core/concurrency/coroutine.h119
-rw-r--r--yt/yt/core/concurrency/delayed_executor.cpp441
-rw-r--r--yt/yt/core/concurrency/delayed_executor.h109
-rw-r--r--yt/yt/core/concurrency/execution_stack.cpp247
-rw-r--r--yt/yt/core/concurrency/execution_stack.h89
-rw-r--r--yt/yt/core/concurrency/fair_share_action_queue-inl.h61
-rw-r--r--yt/yt/core/concurrency/fair_share_action_queue.cpp181
-rw-r--r--yt/yt/core/concurrency/fair_share_action_queue.h54
-rw-r--r--yt/yt/core/concurrency/fair_share_invoker_pool.cpp318
-rw-r--r--yt/yt/core/concurrency/fair_share_invoker_pool.h58
-rw-r--r--yt/yt/core/concurrency/fair_share_invoker_queue.cpp158
-rw-r--r--yt/yt/core/concurrency/fair_share_invoker_queue.h84
-rw-r--r--yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp39
-rw-r--r--yt/yt/core/concurrency/fair_share_queue_scheduler_thread.h36
-rw-r--r--yt/yt/core/concurrency/fair_share_thread_pool.cpp588
-rw-r--r--yt/yt/core/concurrency/fair_share_thread_pool.h30
-rw-r--r--yt/yt/core/concurrency/fair_throttler.cpp965
-rw-r--r--yt/yt/core/concurrency/fair_throttler.h163
-rw-r--r--yt/yt/core/concurrency/fiber.cpp297
-rw-r--r--yt/yt/core/concurrency/fiber.h67
-rw-r--r--yt/yt/core/concurrency/fiber_scheduler_thread.cpp1029
-rw-r--r--yt/yt/core/concurrency/fiber_scheduler_thread.h35
-rw-r--r--yt/yt/core/concurrency/fls-inl.h118
-rw-r--r--yt/yt/core/concurrency/fls.cpp85
-rw-r--r--yt/yt/core/concurrency/fls.h61
-rw-r--r--yt/yt/core/concurrency/invoker_alarm.cpp96
-rw-r--r--yt/yt/core/concurrency/invoker_alarm.h58
-rw-r--r--yt/yt/core/concurrency/invoker_queue.cpp592
-rw-r--r--yt/yt/core/concurrency/invoker_queue.h214
-rw-r--r--yt/yt/core/concurrency/lease_manager.cpp145
-rw-r--r--yt/yt/core/concurrency/lease_manager.h53
-rw-r--r--yt/yt/core/concurrency/moody_camel_concurrent_queue.h3742
-rw-r--r--yt/yt/core/concurrency/new_fair_share_thread_pool.cpp1164
-rw-r--r--yt/yt/core/concurrency/new_fair_share_thread_pool.h19
-rw-r--r--yt/yt/core/concurrency/nonblocking_batch-inl.h150
-rw-r--r--yt/yt/core/concurrency/nonblocking_batch.h76
-rw-r--r--yt/yt/core/concurrency/nonblocking_queue-inl.h50
-rw-r--r--yt/yt/core/concurrency/nonblocking_queue.h40
-rw-r--r--yt/yt/core/concurrency/notify_manager.cpp205
-rw-r--r--yt/yt/core/concurrency/notify_manager.h64
-rw-r--r--yt/yt/core/concurrency/periodic_executor.cpp296
-rw-r--r--yt/yt/core/concurrency/periodic_executor.h109
-rw-r--r--yt/yt/core/concurrency/periodic_yielder.cpp43
-rw-r--r--yt/yt/core/concurrency/periodic_yielder.h33
-rw-r--r--yt/yt/core/concurrency/pollable_detail.cpp24
-rw-r--r--yt/yt/core/concurrency/pollable_detail.h25
-rw-r--r--yt/yt/core/concurrency/poller.h124
-rw-r--r--yt/yt/core/concurrency/private.h66
-rw-r--r--yt/yt/core/concurrency/profiling_helpers.cpp44
-rw-r--r--yt/yt/core/concurrency/profiling_helpers.h22
-rw-r--r--yt/yt/core/concurrency/propagating_storage-inl.h69
-rw-r--r--yt/yt/core/concurrency/propagating_storage.cpp270
-rw-r--r--yt/yt/core/concurrency/propagating_storage.h153
-rw-r--r--yt/yt/core/concurrency/public.h124
-rw-r--r--yt/yt/core/concurrency/quantized_executor.cpp287
-rw-r--r--yt/yt/core/concurrency/quantized_executor.h56
-rw-r--r--yt/yt/core/concurrency/scheduler.h3
-rw-r--r--yt/yt/core/concurrency/scheduler_api-inl.h85
-rw-r--r--yt/yt/core/concurrency/scheduler_api.h134
-rw-r--r--yt/yt/core/concurrency/scheduler_thread.cpp92
-rw-r--r--yt/yt/core/concurrency/scheduler_thread.h59
-rw-r--r--yt/yt/core/concurrency/single_queue_scheduler_thread.cpp152
-rw-r--r--yt/yt/core/concurrency/single_queue_scheduler_thread.h80
-rw-r--r--yt/yt/core/concurrency/suspendable_action_queue.cpp106
-rw-r--r--yt/yt/core/concurrency/suspendable_action_queue.h36
-rw-r--r--yt/yt/core/concurrency/system_invokers.cpp76
-rw-r--r--yt/yt/core/concurrency/system_invokers.h17
-rw-r--r--yt/yt/core/concurrency/thread_affinity-inl.h32
-rw-r--r--yt/yt/core/concurrency/thread_affinity.cpp61
-rw-r--r--yt/yt/core/concurrency/thread_affinity.h111
-rw-r--r--yt/yt/core/concurrency/thread_pool.cpp229
-rw-r--r--yt/yt/core/concurrency/thread_pool.h36
-rw-r--r--yt/yt/core/concurrency/thread_pool_detail.cpp139
-rw-r--r--yt/yt/core/concurrency/thread_pool_detail.h56
-rw-r--r--yt/yt/core/concurrency/thread_pool_poller.cpp437
-rw-r--r--yt/yt/core/concurrency/thread_pool_poller.h27
-rw-r--r--yt/yt/core/concurrency/throughput_throttler.cpp1184
-rw-r--r--yt/yt/core/concurrency/throughput_throttler.h151
-rw-r--r--yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp717
-rw-r--r--yt/yt/core/concurrency/two_level_fair_share_thread_pool.h43
-rw-r--r--yt/yt/core/concurrency/unittests/async_barrier_ut.cpp101
-rw-r--r--yt/yt/core/concurrency/unittests/async_rw_lock_ut.cpp128
-rw-r--r--yt/yt/core/concurrency/unittests/async_stream_pipe_ut.cpp62
-rw-r--r--yt/yt/core/concurrency/unittests/async_stream_ut.cpp151
-rw-r--r--yt/yt/core/concurrency/unittests/async_yson_writer_ut.cpp155
-rw-r--r--yt/yt/core/concurrency/unittests/coroutines_ut.cpp157
-rw-r--r--yt/yt/core/concurrency/unittests/count_down_latch_ut.cpp81
-rw-r--r--yt/yt/core/concurrency/unittests/delayed_executor_ut.cpp128
-rw-r--r--yt/yt/core/concurrency/unittests/fair_share_invoker_pool_ut.cpp592
-rw-r--r--yt/yt/core/concurrency/unittests/fair_share_thread_pool_ut.cpp41
-rw-r--r--yt/yt/core/concurrency/unittests/fair_throttler_ut.cpp317
-rw-r--r--yt/yt/core/concurrency/unittests/fls_ut.cpp113
-rw-r--r--yt/yt/core/concurrency/unittests/invoker_alarm_ut.cpp89
-rw-r--r--yt/yt/core/concurrency/unittests/invoker_pool_ut.cpp291
-rw-r--r--yt/yt/core/concurrency/unittests/nonblocking_batch_ut.cpp163
-rw-r--r--yt/yt/core/concurrency/unittests/nonblocking_queue_ut.cpp102
-rw-r--r--yt/yt/core/concurrency/unittests/periodic_ut.cpp229
-rw-r--r--yt/yt/core/concurrency/unittests/propagating_storage_ut.cpp156
-rw-r--r--yt/yt/core/concurrency/unittests/quantized_executor_ut.cpp248
-rw-r--r--yt/yt/core/concurrency/unittests/scheduler_ut.cpp1637
-rw-r--r--yt/yt/core/concurrency/unittests/suspendable_action_queue_ut.cpp253
-rw-r--r--yt/yt/core/concurrency/unittests/thread_affinity_ut.cpp146
-rw-r--r--yt/yt/core/concurrency/unittests/thread_pool_poller_ut.cpp210
-rw-r--r--yt/yt/core/concurrency/unittests/thread_pool_ut.cpp51
-rw-r--r--yt/yt/core/concurrency/unittests/throughput_throttler_ut.cpp601
-rw-r--r--yt/yt/core/concurrency/unittests/two_level_fair_share_thread_pool_ut.cpp45
-rw-r--r--yt/yt/core/concurrency/unittests/ya.make64
-rw-r--r--yt/yt/core/crypto/config.cpp38
-rw-r--r--yt/yt/core/crypto/config.h30
-rw-r--r--yt/yt/core/crypto/crypto.cpp331
-rw-r--r--yt/yt/core/crypto/crypto.h128
-rw-r--r--yt/yt/core/crypto/public.h16
-rw-r--r--yt/yt/core/crypto/tls.cpp720
-rw-r--r--yt/yt/core/crypto/tls.h57
-rw-r--r--yt/yt/core/crypto/unittests/crypto_ut.cpp83
-rw-r--r--yt/yt/core/crypto/unittests/tls_ut.cpp114
-rw-r--r--yt/yt/core/crypto/unittests/ya.make42
-rw-r--r--yt/yt/core/crypto/ya.make17
-rw-r--r--yt/yt/core/dns/ares_dns_resolver.cpp545
-rw-r--r--yt/yt/core/dns/ares_dns_resolver.h19
-rw-r--r--yt/yt/core/dns/dns_resolver.cpp22
-rw-r--r--yt/yt/core/dns/dns_resolver.h37
-rw-r--r--yt/yt/core/dns/private.h18
-rw-r--r--yt/yt/core/dns/public.h15
-rw-r--r--yt/yt/core/http/client.cpp324
-rw-r--r--yt/yt/core/http/client.h83
-rw-r--r--yt/yt/core/http/config.cpp81
-rw-r--r--yt/yt/core/http/config.h106
-rw-r--r--yt/yt/core/http/connection_pool.cpp95
-rw-r--r--yt/yt/core/http/connection_pool.h60
-rw-r--r--yt/yt/core/http/connection_reuse_helpers-inl.h29
-rw-r--r--yt/yt/core/http/connection_reuse_helpers.cpp27
-rw-r--r--yt/yt/core/http/connection_reuse_helpers.h48
-rw-r--r--yt/yt/core/http/helpers.cpp466
-rw-r--r--yt/yt/core/http/helpers.h61
-rw-r--r--yt/yt/core/http/http.cpp209
-rw-r--r--yt/yt/core/http/http.h294
-rw-r--r--yt/yt/core/http/mock/http.cpp47
-rw-r--r--yt/yt/core/http/mock/http.h77
-rw-r--r--yt/yt/core/http/mock/ya.make14
-rw-r--r--yt/yt/core/http/private.h23
-rw-r--r--yt/yt/core/http/public.h35
-rw-r--r--yt/yt/core/http/server.cpp520
-rw-r--r--yt/yt/core/http/server.h144
-rw-r--r--yt/yt/core/http/stream.cpp857
-rw-r--r--yt/yt/core/http/stream.h269
-rw-r--r--yt/yt/core/http/unittests/http_ut.cpp1398
-rw-r--r--yt/yt/core/http/unittests/ya.make45
-rw-r--r--yt/yt/core/http/ya.make21
-rw-r--r--yt/yt/core/https/client.cpp139
-rw-r--r--yt/yt/core/https/client.h19
-rw-r--r--yt/yt/core/https/config.cpp42
-rw-r--r--yt/yt/core/https/config.h76
-rw-r--r--yt/yt/core/https/public.h16
-rw-r--r--yt/yt/core/https/server.cpp105
-rw-r--r--yt/yt/core/https/server.h24
-rw-r--r--yt/yt/core/https/ya.make18
-rw-r--r--yt/yt/core/json/config.cpp42
-rw-r--r--yt/yt/core/json/config.h55
-rw-r--r--yt/yt/core/json/helpers.cpp14
-rw-r--r--yt/yt/core/json/helpers.h13
-rw-r--r--yt/yt/core/json/json_callbacks.cpp414
-rw-r--r--yt/yt/core/json/json_callbacks.h127
-rw-r--r--yt/yt/core/json/json_parser.cpp242
-rw-r--r--yt/yt/core/json/json_parser.h44
-rw-r--r--yt/yt/core/json/json_writer.cpp684
-rw-r--r--yt/yt/core/json/json_writer.h97
-rw-r--r--yt/yt/core/json/public.h16
-rw-r--r--yt/yt/core/json/unittests/parser_ut.cpp806
-rw-r--r--yt/yt/core/json/unittests/writer_ut.cpp844
-rw-r--r--yt/yt/core/json/unittests/ya.make40
-rw-r--r--yt/yt/core/logging/compression.cpp150
-rw-r--r--yt/yt/core/logging/compression.h84
-rw-r--r--yt/yt/core/logging/config-inl.h26
-rw-r--r--yt/yt/core/logging/config.cpp483
-rw-r--r--yt/yt/core/logging/config.h212
-rw-r--r--yt/yt/core/logging/file_log_writer.cpp254
-rw-r--r--yt/yt/core/logging/file_log_writer.h19
-rw-r--r--yt/yt/core/logging/fluent_log-inl.h58
-rw-r--r--yt/yt/core/logging/fluent_log.cpp71
-rw-r--r--yt/yt/core/logging/fluent_log.h87
-rw-r--r--yt/yt/core/logging/formatter.cpp285
-rw-r--r--yt/yt/core/logging/formatter.h81
-rw-r--r--yt/yt/core/logging/log-inl.h35
-rw-r--r--yt/yt/core/logging/log.cpp37
-rw-r--r--yt/yt/core/logging/log.h9
-rw-r--r--yt/yt/core/logging/log_manager.cpp1597
-rw-r--r--yt/yt/core/logging/log_manager.h116
-rw-r--r--yt/yt/core/logging/log_writer.h36
-rw-r--r--yt/yt/core/logging/log_writer_detail.cpp200
-rw-r--r--yt/yt/core/logging/log_writer_detail.h76
-rw-r--r--yt/yt/core/logging/log_writer_factory.h53
-rw-r--r--yt/yt/core/logging/logger_owner.cpp32
-rw-r--r--yt/yt/core/logging/logger_owner.h24
-rw-r--r--yt/yt/core/logging/pattern.cpp144
-rw-r--r--yt/yt/core/logging/pattern.h25
-rw-r--r--yt/yt/core/logging/private.h23
-rw-r--r--yt/yt/core/logging/public.h44
-rw-r--r--yt/yt/core/logging/random_access_gzip.cpp119
-rw-r--r--yt/yt/core/logging/random_access_gzip.h49
-rw-r--r--yt/yt/core/logging/serializable_logger.cpp60
-rw-r--r--yt/yt/core/logging/serializable_logger.h22
-rw-r--r--yt/yt/core/logging/stream_log_writer.cpp94
-rw-r--r--yt/yt/core/logging/stream_log_writer.h22
-rw-r--r--yt/yt/core/logging/stream_output.cpp32
-rw-r--r--yt/yt/core/logging/stream_output.h39
-rw-r--r--yt/yt/core/logging/unittests/logging_ut.cpp1224
-rw-r--r--yt/yt/core/logging/unittests/ya.make40
-rw-r--r--yt/yt/core/logging/zstd_compression.cpp157
-rw-r--r--yt/yt/core/logging/zstd_compression.h18
-rw-r--r--yt/yt/core/misc/arithmetic_formula.cpp1139
-rw-r--r--yt/yt/core/misc/arithmetic_formula.h208
-rw-r--r--yt/yt/core/misc/assert.cpp65
-rw-r--r--yt/yt/core/misc/async_expiring_cache-inl.h691
-rw-r--r--yt/yt/core/misc/async_expiring_cache.h171
-rw-r--r--yt/yt/core/misc/async_slru_cache-inl.h1410
-rw-r--r--yt/yt/core/misc/async_slru_cache.h478
-rw-r--r--yt/yt/core/misc/atomic_object-inl.h89
-rw-r--r--yt/yt/core/misc/atomic_object.h57
-rw-r--r--yt/yt/core/misc/atomic_ptr-inl.h216
-rw-r--r--yt/yt/core/misc/atomic_ptr.h82
-rw-r--r--yt/yt/core/misc/backoff_strategy.cpp69
-rw-r--r--yt/yt/core/misc/backoff_strategy.h67
-rw-r--r--yt/yt/core/misc/backoff_strategy_api.h19
-rw-r--r--yt/yt/core/misc/backoff_strategy_config.cpp35
-rw-r--r--yt/yt/core/misc/backoff_strategy_config.h39
-rw-r--r--yt/yt/core/misc/bit_packed_unsigned_vector-inl.h338
-rw-r--r--yt/yt/core/misc/bit_packed_unsigned_vector.cpp32
-rw-r--r--yt/yt/core/misc/bit_packed_unsigned_vector.h70
-rw-r--r--yt/yt/core/misc/bit_packing-inl.h150
-rw-r--r--yt/yt/core/misc/bit_packing.cpp341
-rw-r--r--yt/yt/core/misc/bit_packing.h89
-rw-r--r--yt/yt/core/misc/bitmap.cpp84
-rw-r--r--yt/yt/core/misc/bitmap.h223
-rw-r--r--yt/yt/core/misc/blob.h1
-rw-r--r--yt/yt/core/misc/blob_output.cpp105
-rw-r--r--yt/yt/core/misc/blob_output.h49
-rw-r--r--yt/yt/core/misc/bloom_filter.cpp222
-rw-r--r--yt/yt/core/misc/bloom_filter.h98
-rw-r--r--yt/yt/core/misc/cache_config.cpp125
-rw-r--r--yt/yt/core/misc/cache_config.h144
-rw-r--r--yt/yt/core/misc/checksum.cpp796
-rw-r--r--yt/yt/core/misc/checksum.h58
-rw-r--r--yt/yt/core/misc/checksum_helpers.h107
-rw-r--r--yt/yt/core/misc/collection_helpers-inl.h351
-rw-r--r--yt/yt/core/misc/collection_helpers.h161
-rw-r--r--yt/yt/core/misc/common.h38
-rw-r--r--yt/yt/core/misc/concurrent_cache-inl.h212
-rw-r--r--yt/yt/core/misc/concurrent_cache.h106
-rw-r--r--yt/yt/core/misc/config.cpp128
-rw-r--r--yt/yt/core/misc/config.h114
-rw-r--r--yt/yt/core/misc/copyable_atomic.h55
-rw-r--r--yt/yt/core/misc/coro_pipe.cpp82
-rw-r--r--yt/yt/core/misc/coro_pipe.h29
-rw-r--r--yt/yt/core/misc/crash_handler-inl.h41
-rw-r--r--yt/yt/core/misc/crash_handler.cpp622
-rw-r--r--yt/yt/core/misc/crash_handler.h71
-rw-r--r--yt/yt/core/misc/default_map-inl.h28
-rw-r--r--yt/yt/core/misc/default_map.h31
-rw-r--r--yt/yt/core/misc/digest.cpp173
-rw-r--r--yt/yt/core/misc/digest.h51
-rw-r--r--yt/yt/core/misc/dnf.cpp212
-rw-r--r--yt/yt/core/misc/dnf.h85
-rw-r--r--yt/yt/core/misc/ema_counter.cpp96
-rw-r--r--yt/yt/core/misc/ema_counter.h56
-rw-r--r--yt/yt/core/misc/error-inl.h279
-rw-r--r--yt/yt/core/misc/error.cpp1241
-rw-r--r--yt/yt/core/misc/error.h381
-rw-r--r--yt/yt/core/misc/error_code.cpp160
-rw-r--r--yt/yt/core/misc/error_code.h91
-rw-r--r--yt/yt/core/misc/fair_scheduler-inl.h150
-rw-r--r--yt/yt/core/misc/fair_scheduler.h33
-rw-r--r--yt/yt/core/misc/farm_hash.h1
-rw-r--r--yt/yt/core/misc/fenwick_tree-inl.h173
-rw-r--r--yt/yt/core/misc/fenwick_tree.h77
-rw-r--r--yt/yt/core/misc/finally.h60
-rw-r--r--yt/yt/core/misc/fs.cpp1165
-rw-r--r--yt/yt/core/misc/fs.h242
-rw-r--r--yt/yt/core/misc/guid-inl.h35
-rw-r--r--yt/yt/core/misc/guid.cpp16
-rw-r--r--yt/yt/core/misc/guid.h27
-rw-r--r--yt/yt/core/misc/hazard_ptr-inl.h192
-rw-r--r--yt/yt/core/misc/hazard_ptr.cpp447
-rw-r--r--yt/yt/core/misc/hazard_ptr.h92
-rw-r--r--yt/yt/core/misc/heap-inl.h255
-rw-r--r--yt/yt/core/misc/heap.h70
-rw-r--r--yt/yt/core/misc/hedging_manager.cpp189
-rw-r--r--yt/yt/core/misc/hedging_manager.h43
-rw-r--r--yt/yt/core/misc/histogram.cpp191
-rw-r--r--yt/yt/core/misc/histogram.h50
-rw-r--r--yt/yt/core/misc/historic_usage_aggregator.cpp124
-rw-r--r--yt/yt/core/misc/historic_usage_aggregator.h80
-rw-r--r--yt/yt/core/misc/hr_timer.cpp95
-rw-r--r--yt/yt/core/misc/hr_timer.h53
-rw-r--r--yt/yt/core/misc/hyperloglog.h139
-rw-r--r--yt/yt/core/misc/hyperloglog_bias.h83
-rw-r--r--yt/yt/core/misc/id_generator.cpp31
-rw-r--r--yt/yt/core/misc/id_generator.h26
-rw-r--r--yt/yt/core/misc/intern_registry-inl.h233
-rw-r--r--yt/yt/core/misc/intern_registry.h123
-rw-r--r--yt/yt/core/misc/intrusive_ptr.h1
-rw-r--r--yt/yt/core/misc/isa_crc64/checksum.cpp85
-rw-r--r--yt/yt/core/misc/isa_crc64/checksum.h37
-rw-r--r--yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm589
-rw-r--r--yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c151
-rw-r--r--yt/yt/core/misc/isa_crc64/include/reg_sizes.asm248
-rw-r--r--yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/crc64_reference_test.c308
-rw-r--r--yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/ya.make17
-rw-r--r--yt/yt/core/misc/isa_crc64/unittests/ya.make23
-rw-r--r--yt/yt/core/misc/isa_crc64/ya.make29
-rw-r--r--yt/yt/core/misc/lazy_ptr.h74
-rw-r--r--yt/yt/core/misc/linear_probe-inl.h43
-rw-r--r--yt/yt/core/misc/linear_probe.cpp88
-rw-r--r--yt/yt/core/misc/linear_probe.h59
-rw-r--r--yt/yt/core/misc/lock_free_hash_table-inl.h210
-rw-r--r--yt/yt/core/misc/lock_free_hash_table.h142
-rw-r--r--yt/yt/core/misc/maybe_inf-inl.h89
-rw-r--r--yt/yt/core/misc/maybe_inf.h53
-rw-r--r--yt/yt/core/misc/memory_reference_tracker.cpp35
-rw-r--r--yt/yt/core/misc/memory_reference_tracker.h44
-rw-r--r--yt/yt/core/misc/memory_usage_tracker.cpp184
-rw-r--r--yt/yt/core/misc/memory_usage_tracker.h70
-rw-r--r--yt/yt/core/misc/mpl.h75
-rw-r--r--yt/yt/core/misc/mpsc_fair_share_queue-inl.h392
-rw-r--r--yt/yt/core/misc/mpsc_fair_share_queue.h180
-rw-r--r--yt/yt/core/misc/mpsc_queue-inl.h116
-rw-r--r--yt/yt/core/misc/mpsc_queue.h59
-rw-r--r--yt/yt/core/misc/mpsc_sharded_queue-inl.h92
-rw-r--r--yt/yt/core/misc/mpsc_sharded_queue.h60
-rw-r--r--yt/yt/core/misc/mpsc_stack-inl.h120
-rw-r--r--yt/yt/core/misc/mpsc_stack.h46
-rw-r--r--yt/yt/core/misc/numeric_helpers-inl.h44
-rw-r--r--yt/yt/core/misc/numeric_helpers.h28
-rw-r--r--yt/yt/core/misc/object_pool-inl.h111
-rw-r--r--yt/yt/core/misc/object_pool.h93
-rw-r--r--yt/yt/core/misc/optional.h72
-rw-r--r--yt/yt/core/misc/parser_helpers.cpp37
-rw-r--r--yt/yt/core/misc/parser_helpers.h11
-rw-r--r--yt/yt/core/misc/pattern_formatter.cpp54
-rw-r--r--yt/yt/core/misc/pattern_formatter.h24
-rw-r--r--yt/yt/core/misc/persistent_queue-inl.h194
-rw-r--r--yt/yt/core/misc/persistent_queue.h135
-rw-r--r--yt/yt/core/misc/phoenix-inl.h297
-rw-r--r--yt/yt/core/misc/phoenix.cpp85
-rw-r--r--yt/yt/core/misc/phoenix.h310
-rw-r--r--yt/yt/core/misc/pool_allocator-inl.h110
-rw-r--r--yt/yt/core/misc/pool_allocator.cpp49
-rw-r--r--yt/yt/core/misc/pool_allocator.h95
-rw-r--r--yt/yt/core/misc/proc.cpp1587
-rw-r--r--yt/yt/core/misc/proc.h367
-rw-r--r--yt/yt/core/misc/property.h1
-rw-r--r--yt/yt/core/misc/protobuf_helpers-inl.h586
-rw-r--r--yt/yt/core/misc/protobuf_helpers.cpp542
-rw-r--r--yt/yt/core/misc/protobuf_helpers.h436
-rw-r--r--yt/yt/core/misc/public.cpp20
-rw-r--r--yt/yt/core/misc/public.h175
-rw-r--r--yt/yt/core/misc/random-inl.h72
-rw-r--r--yt/yt/core/misc/random.cpp23
-rw-r--r--yt/yt/core/misc/random.h47
-rw-r--r--yt/yt/core/misc/range.h1
-rw-r--r--yt/yt/core/misc/range_formatters.h34
-rw-r--r--yt/yt/core/misc/ref_counted.h1
-rw-r--r--yt/yt/core/misc/ref_counted_tracker-inl.h159
-rw-r--r--yt/yt/core/misc/ref_counted_tracker.cpp467
-rw-r--r--yt/yt/core/misc/ref_counted_tracker.h143
-rw-r--r--yt/yt/core/misc/ref_counted_tracker_profiler.cpp44
-rw-r--r--yt/yt/core/misc/ref_counted_tracker_profiler.h13
-rw-r--r--yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp42
-rw-r--r--yt/yt/core/misc/ref_counted_tracker_statistics_producer.h15
-rw-r--r--yt/yt/core/misc/ref_tracked.cpp56
-rw-r--r--yt/yt/core/misc/ref_tracked.h1
-rw-r--r--yt/yt/core/misc/relaxed_mpsc_queue-inl.h77
-rw-r--r--yt/yt/core/misc/relaxed_mpsc_queue.cpp71
-rw-r--r--yt/yt/core/misc/relaxed_mpsc_queue.h96
-rw-r--r--yt/yt/core/misc/ring_queue.h379
-rw-r--r--yt/yt/core/misc/serialize-inl.h1998
-rw-r--r--yt/yt/core/misc/serialize.cpp181
-rw-r--r--yt/yt/core/misc/serialize.h299
-rw-r--r--yt/yt/core/misc/serialize_dump.h261
-rw-r--r--yt/yt/core/misc/shared_range.h1
-rw-r--r--yt/yt/core/misc/shutdown.cpp271
-rw-r--r--yt/yt/core/misc/shutdown.h78
-rw-r--r--yt/yt/core/misc/shutdown_priorities.h14
-rw-r--r--yt/yt/core/misc/signal_registry.cpp189
-rw-r--r--yt/yt/core/misc/signal_registry.h75
-rw-r--r--yt/yt/core/misc/singleton.h2
-rw-r--r--yt/yt/core/misc/skip_list-inl.h306
-rw-r--r--yt/yt/core/misc/skip_list.h148
-rw-r--r--yt/yt/core/misc/slab_allocator.cpp463
-rw-r--r--yt/yt/core/misc/slab_allocator.h62
-rw-r--r--yt/yt/core/misc/sliding_window-inl.h99
-rw-r--r--yt/yt/core/misc/sliding_window.h102
-rw-r--r--yt/yt/core/misc/source_location.h1
-rw-r--r--yt/yt/core/misc/spsc_queue-inl.h95
-rw-r--r--yt/yt/core/misc/spsc_queue.h53
-rw-r--r--yt/yt/core/misc/statistics-inl.h205
-rw-r--r--yt/yt/core/misc/statistics.cpp469
-rw-r--r--yt/yt/core/misc/statistics.h169
-rw-r--r--yt/yt/core/misc/string_builder.h1
-rw-r--r--yt/yt/core/misc/sync_cache-inl.h676
-rw-r--r--yt/yt/core/misc/sync_cache.h240
-rw-r--r--yt/yt/core/misc/sync_expiring_cache-inl.h262
-rw-r--r--yt/yt/core/misc/sync_expiring_cache.h66
-rw-r--r--yt/yt/core/misc/tls_expiring_cache-inl.h108
-rw-r--r--yt/yt/core/misc/tls_expiring_cache.h63
-rw-r--r--yt/yt/core/misc/topological_ordering-inl.h86
-rw-r--r--yt/yt/core/misc/topological_ordering.h40
-rw-r--r--yt/yt/core/misc/unittests/algorithm_helpers_ut.cpp108
-rw-r--r--yt/yt/core/misc/unittests/arithmetic_formula_ut.cpp312
-rw-r--r--yt/yt/core/misc/unittests/async_expiring_cache_ut.cpp377
-rw-r--r--yt/yt/core/misc/unittests/async_slru_cache_ut.cpp1115
-rw-r--r--yt/yt/core/misc/unittests/atomic_ptr_ut.cpp83
-rw-r--r--yt/yt/core/misc/unittests/bind_ut.cpp1158
-rw-r--r--yt/yt/core/misc/unittests/bit_packed_integer_vector_ut.cpp152
-rw-r--r--yt/yt/core/misc/unittests/boolean_formula_ut.cpp201
-rw-r--r--yt/yt/core/misc/unittests/callback_ut.cpp179
-rw-r--r--yt/yt/core/misc/unittests/checksum_ut.cpp126
-rw-r--r--yt/yt/core/misc/unittests/codicil_ut.cpp46
-rw-r--r--yt/yt/core/misc/unittests/concurrent_cache_ut.cpp150
-rw-r--r--yt/yt/core/misc/unittests/default_map_ut.cpp26
-rw-r--r--yt/yt/core/misc/unittests/digest_ut.cpp252
-rw-r--r--yt/yt/core/misc/unittests/dnf_ut.cpp80
-rw-r--r--yt/yt/core/misc/unittests/ema_counter_ut.cpp139
-rw-r--r--yt/yt/core/misc/unittests/enum_ut.cpp47
-rw-r--r--yt/yt/core/misc/unittests/error_code_ut.cpp120
-rw-r--r--yt/yt/core/misc/unittests/error_ut.cpp219
-rw-r--r--yt/yt/core/misc/unittests/fair_scheduler_ut.cpp78
-rw-r--r--yt/yt/core/misc/unittests/fenwick_tree_ut.cpp191
-rw-r--r--yt/yt/core/misc/unittests/finally_ut.cpp57
-rw-r--r--yt/yt/core/misc/unittests/format_ut.cpp17
-rw-r--r--yt/yt/core/misc/unittests/fs_ut.cpp87
-rw-r--r--yt/yt/core/misc/unittests/future_ut.cpp1614
-rw-r--r--yt/yt/core/misc/unittests/guid_ut.cpp22
-rw-r--r--yt/yt/core/misc/unittests/hash_filter_ut.cpp122
-rw-r--r--yt/yt/core/misc/unittests/hazard_ptr_ut.cpp381
-rw-r--r--yt/yt/core/misc/unittests/heap_ut.cpp123
-rw-r--r--yt/yt/core/misc/unittests/hedging_manager_ut.cpp109
-rw-r--r--yt/yt/core/misc/unittests/histogram_ut.cpp55
-rw-r--r--yt/yt/core/misc/unittests/historic_usage_aggregator_ut.cpp105
-rw-r--r--yt/yt/core/misc/unittests/hyperloglog_ut.cpp78
-rw-r--r--yt/yt/core/misc/unittests/intern_registry_ut.cpp75
-rw-r--r--yt/yt/core/misc/unittests/job_signaler_ut.cpp71
-rw-r--r--yt/yt/core/misc/unittests/lock_free_hash_table_and_concurrent_cache_helpers.h34
-rw-r--r--yt/yt/core/misc/unittests/lock_free_hash_table_ut.cpp139
-rw-r--r--yt/yt/core/misc/unittests/lru_cache_ut.cpp146
-rw-r--r--yt/yt/core/misc/unittests/maybe_inf_ut.cpp43
-rw-r--r--yt/yt/core/misc/unittests/memory_tag_ut.cpp244
-rw-r--r--yt/yt/core/misc/unittests/mpl_ut.cpp23
-rw-r--r--yt/yt/core/misc/unittests/mpsc_fair_share_queue_ut.cpp392
-rw-r--r--yt/yt/core/misc/unittests/mpsc_queue_ut.cpp140
-rw-r--r--yt/yt/core/misc/unittests/mpsc_stack_ut.cpp119
-rw-r--r--yt/yt/core/misc/unittests/pattern_formatter_ut.cpp94
-rw-r--r--yt/yt/core/misc/unittests/persistent_queue_ut.cpp133
-rw-r--r--yt/yt/core/misc/unittests/phoenix_ut.cpp625
-rw-r--r--yt/yt/core/misc/unittests/pool_allocator_ut.cpp71
-rw-r--r--yt/yt/core/misc/unittests/proc_ut.cpp193
-rw-r--r--yt/yt/core/misc/unittests/proto/ref_counted_tracker_ut.proto14
-rw-r--r--yt/yt/core/misc/unittests/random_ut.cpp37
-rw-r--r--yt/yt/core/misc/unittests/ref_counted_tracker_ut.cpp300
-rw-r--r--yt/yt/core/misc/unittests/relaxed_mpsc_queue_ut.cpp92
-rw-r--r--yt/yt/core/misc/unittests/ring_queue_ut.cpp127
-rw-r--r--yt/yt/core/misc/unittests/skip_list_ut.cpp147
-rw-r--r--yt/yt/core/misc/unittests/slab_allocator_ut.cpp63
-rw-r--r--yt/yt/core/misc/unittests/sliding_window_ut.cpp120
-rw-r--r--yt/yt/core/misc/unittests/spsc_queue_ut.cpp104
-rw-r--r--yt/yt/core/misc/unittests/statistics_ut.cpp184
-rw-r--r--yt/yt/core/misc/unittests/string_ut.cpp52
-rw-r--r--yt/yt/core/misc/unittests/sync_cache_ut.cpp61
-rw-r--r--yt/yt/core/misc/unittests/sync_expiring_cache_ut.cpp92
-rw-r--r--yt/yt/core/misc/unittests/time_formula_ut.cpp97
-rw-r--r--yt/yt/core/misc/unittests/tls_expiring_cache_ut.cpp66
-rw-r--r--yt/yt/core/misc/unittests/topological_ordering_ut.cpp112
-rw-r--r--yt/yt/core/misc/unittests/ya.make105
-rw-r--r--yt/yt/core/misc/unittests/yverify_ut.cpp60
-rw-r--r--yt/yt/core/misc/unittests/zerocopy_output_writer_ut.cpp145
-rw-r--r--yt/yt/core/misc/utf8_decoder.cpp83
-rw-r--r--yt/yt/core/misc/utf8_decoder.h24
-rw-r--r--yt/yt/core/misc/zerocopy_output_writer-inl.h120
-rw-r--r--yt/yt/core/misc/zerocopy_output_writer.cpp42
-rw-r--r--yt/yt/core/misc/zerocopy_output_writer.h52
-rw-r--r--yt/yt/core/net/address.cpp1275
-rw-r--r--yt/yt/core/net/address.h279
-rw-r--r--yt/yt/core/net/config.cpp65
-rw-r--r--yt/yt/core/net/config.h68
-rw-r--r--yt/yt/core/net/connection.cpp1309
-rw-r--r--yt/yt/core/net/connection.h117
-rw-r--r--yt/yt/core/net/dialer.cpp418
-rw-r--r--yt/yt/core/net/dialer.h68
-rw-r--r--yt/yt/core/net/helpers.cpp65
-rw-r--r--yt/yt/core/net/helpers.h22
-rw-r--r--yt/yt/core/net/listener.cpp236
-rw-r--r--yt/yt/core/net/listener.h31
-rw-r--r--yt/yt/core/net/local_address.cpp183
-rw-r--r--yt/yt/core/net/local_address.h48
-rw-r--r--yt/yt/core/net/mock/dialer.cpp19
-rw-r--r--yt/yt/core/net/mock/dialer.h27
-rw-r--r--yt/yt/core/net/mock/ya.make14
-rw-r--r--yt/yt/core/net/packet_connection.h34
-rw-r--r--yt/yt/core/net/private.h15
-rw-r--r--yt/yt/core/net/public.cpp9
-rw-r--r--yt/yt/core/net/public.h34
-rw-r--r--yt/yt/core/net/socket.cpp528
-rw-r--r--yt/yt/core/net/socket.h51
-rw-r--r--yt/yt/core/net/unittests/local_address_ut.cpp30
-rw-r--r--yt/yt/core/net/unittests/net_ut.cpp377
-rw-r--r--yt/yt/core/net/unittests/network_address_ut.cpp362
-rw-r--r--yt/yt/core/net/unittests/ya.make43
-rw-r--r--yt/yt/core/profiling/public.h43
-rw-r--r--yt/yt/core/profiling/timing-inl.h53
-rw-r--r--yt/yt/core/profiling/timing.cpp128
-rw-r--r--yt/yt/core/profiling/timing.h130
-rw-r--r--yt/yt/core/profiling/tscp-inl.h33
-rw-r--r--yt/yt/core/profiling/tscp.h35
-rw-r--r--yt/yt/core/profiling/unittests/timer_ut.cpp133
-rw-r--r--yt/yt/core/profiling/unittests/timing_ut.cpp52
-rw-r--r--yt/yt/core/profiling/unittests/ya.make40
-rw-r--r--yt/yt/core/rpc/authentication_identity.cpp140
-rw-r--r--yt/yt/core/rpc/authentication_identity.h69
-rw-r--r--yt/yt/core/rpc/authenticator.cpp85
-rw-r--r--yt/yt/core/rpc/authenticator.h67
-rw-r--r--yt/yt/core/rpc/balancing_channel.cpp327
-rw-r--r--yt/yt/core/rpc/balancing_channel.h31
-rw-r--r--yt/yt/core/rpc/bus/channel.cpp1259
-rw-r--r--yt/yt/core/rpc/bus/channel.h22
-rw-r--r--yt/yt/core/rpc/bus/public.h11
-rw-r--r--yt/yt/core/rpc/bus/server.cpp286
-rw-r--r--yt/yt/core/rpc/bus/server.h15
-rw-r--r--yt/yt/core/rpc/caching_channel_factory.cpp277
-rw-r--r--yt/yt/core/rpc/caching_channel_factory.h18
-rw-r--r--yt/yt/core/rpc/channel.h141
-rw-r--r--yt/yt/core/rpc/channel_detail.cpp261
-rw-r--r--yt/yt/core/rpc/channel_detail.h103
-rw-r--r--yt/yt/core/rpc/client-inl.h166
-rw-r--r--yt/yt/core/rpc/client.cpp783
-rw-r--r--yt/yt/core/rpc/client.h508
-rw-r--r--yt/yt/core/rpc/config.cpp306
-rw-r--r--yt/yt/core/rpc/config.h409
-rw-r--r--yt/yt/core/rpc/dispatcher.cpp152
-rw-r--r--yt/yt/core/rpc/dispatcher.h54
-rw-r--r--yt/yt/core/rpc/dynamic_channel_pool.cpp913
-rw-r--r--yt/yt/core/rpc/dynamic_channel_pool.h55
-rw-r--r--yt/yt/core/rpc/grpc/channel.cpp728
-rw-r--r--yt/yt/core/rpc/grpc/channel.h19
-rw-r--r--yt/yt/core/rpc/grpc/config.cpp88
-rw-r--r--yt/yt/core/rpc/grpc/config.h142
-rw-r--r--yt/yt/core/rpc/grpc/dispatcher.cpp230
-rw-r--r--yt/yt/core/rpc/grpc/dispatcher.h68
-rw-r--r--yt/yt/core/rpc/grpc/helpers-inl.h108
-rw-r--r--yt/yt/core/rpc/grpc/helpers.cpp567
-rw-r--r--yt/yt/core/rpc/grpc/helpers.h302
-rw-r--r--yt/yt/core/rpc/grpc/private.h39
-rw-r--r--yt/yt/core/rpc/grpc/proto/grpc.proto18
-rw-r--r--yt/yt/core/rpc/grpc/public.cpp27
-rw-r--r--yt/yt/core/rpc/grpc/public.h40
-rw-r--r--yt/yt/core/rpc/grpc/server.cpp1095
-rw-r--r--yt/yt/core/rpc/grpc/server.h15
-rw-r--r--yt/yt/core/rpc/grpc/ya.make27
-rw-r--r--yt/yt/core/rpc/hedging_channel-inl.h24
-rw-r--r--yt/yt/core/rpc/hedging_channel.cpp425
-rw-r--r--yt/yt/core/rpc/hedging_channel.h38
-rw-r--r--yt/yt/core/rpc/helpers-inl.h50
-rw-r--r--yt/yt/core/rpc/helpers.cpp608
-rw-r--r--yt/yt/core/rpc/helpers.h127
-rw-r--r--yt/yt/core/rpc/indexed_hash_map.h141
-rw-r--r--yt/yt/core/rpc/local_channel.cpp368
-rw-r--r--yt/yt/core/rpc/local_channel.h14
-rw-r--r--yt/yt/core/rpc/local_server.cpp25
-rw-r--r--yt/yt/core/rpc/local_server.h15
-rw-r--r--yt/yt/core/rpc/message.cpp466
-rw-r--r--yt/yt/core/rpc/message.h116
-rw-r--r--yt/yt/core/rpc/message_format.cpp167
-rw-r--r--yt/yt/core/rpc/message_format.h27
-rw-r--r--yt/yt/core/rpc/null_channel.cpp74
-rw-r--r--yt/yt/core/rpc/null_channel.h14
-rw-r--r--yt/yt/core/rpc/per_user_request_queue_provider.cpp128
-rw-r--r--yt/yt/core/rpc/per_user_request_queue_provider.h58
-rw-r--r--yt/yt/core/rpc/private.h21
-rw-r--r--yt/yt/core/rpc/protocol_version.cpp59
-rw-r--r--yt/yt/core/rpc/protocol_version.h32
-rw-r--r--yt/yt/core/rpc/public.cpp24
-rw-r--r--yt/yt/core/rpc/public.h189
-rw-r--r--yt/yt/core/rpc/request_queue_provider.cpp20
-rw-r--r--yt/yt/core/rpc/request_queue_provider.h40
-rw-r--r--yt/yt/core/rpc/response_keeper.cpp390
-rw-r--r--yt/yt/core/rpc/response_keeper.h110
-rw-r--r--yt/yt/core/rpc/retrying_channel.cpp358
-rw-r--r--yt/yt/core/rpc/retrying_channel.h32
-rw-r--r--yt/yt/core/rpc/roaming_channel.cpp194
-rw-r--r--yt/yt/core/rpc/roaming_channel.h51
-rw-r--r--yt/yt/core/rpc/serialized_channel.cpp160
-rw-r--r--yt/yt/core/rpc/serialized_channel.h18
-rw-r--r--yt/yt/core/rpc/server.h59
-rw-r--r--yt/yt/core/rpc/server_detail.cpp929
-rw-r--r--yt/yt/core/rpc/server_detail.h288
-rw-r--r--yt/yt/core/rpc/service-inl.h73
-rw-r--r--yt/yt/core/rpc/service.cpp113
-rw-r--r--yt/yt/core/rpc/service.h348
-rw-r--r--yt/yt/core/rpc/service_detail-inl.h21
-rw-r--r--yt/yt/core/rpc/service_detail.cpp2456
-rw-r--r--yt/yt/core/rpc/service_detail.h1096
-rw-r--r--yt/yt/core/rpc/static_channel_factory.cpp26
-rw-r--r--yt/yt/core/rpc/static_channel_factory.h26
-rw-r--r--yt/yt/core/rpc/stream-inl.h57
-rw-r--r--yt/yt/core/rpc/stream.cpp873
-rw-r--r--yt/yt/core/rpc/stream.h300
-rw-r--r--yt/yt/core/rpc/throttling_channel.cpp89
-rw-r--r--yt/yt/core/rpc/throttling_channel.h24
-rw-r--r--yt/yt/core/rpc/unittests/bin/main.cpp42
-rw-r--r--yt/yt/core/rpc/unittests/bin/ya.make13
-rw-r--r--yt/yt/core/rpc/unittests/lib/common.cpp11
-rw-r--r--yt/yt/core/rpc/unittests/lib/common.h443
-rw-r--r--yt/yt/core/rpc/unittests/lib/my_service.cpp345
-rw-r--r--yt/yt/core/rpc/unittests/lib/my_service.h70
-rw-r--r--yt/yt/core/rpc/unittests/lib/my_service.proto210
-rw-r--r--yt/yt/core/rpc/unittests/lib/no_baggage_service.cpp48
-rw-r--r--yt/yt/core/rpc/unittests/lib/no_baggage_service.h29
-rw-r--r--yt/yt/core/rpc/unittests/lib/no_baggage_service.proto9
-rw-r--r--yt/yt/core/rpc/unittests/lib/ya.make19
-rw-r--r--yt/yt/core/rpc/unittests/main/ya.make34
-rw-r--r--yt/yt/core/rpc/unittests/mock/service.cpp15
-rw-r--r--yt/yt/core/rpc/unittests/mock/service.h369
-rw-r--r--yt/yt/core/rpc/unittests/mock/ya.make14
-rw-r--r--yt/yt/core/rpc/unittests/roaming_channel_ut.cpp192
-rw-r--r--yt/yt/core/rpc/unittests/rpc_shutdown_ut.cpp53
-rw-r--r--yt/yt/core/rpc/unittests/rpc_ut.cpp1342
-rw-r--r--yt/yt/core/rpc/unittests/shutdown/ya.make32
-rw-r--r--yt/yt/core/rpc/unittests/viable_peer_registry_ut.cpp654
-rw-r--r--yt/yt/core/rpc/unittests/ya.make10
-rw-r--r--yt/yt/core/rpc/viable_peer_registry.cpp578
-rw-r--r--yt/yt/core/rpc/viable_peer_registry.h70
-rw-r--r--yt/yt/core/service_discovery/public.h22
-rw-r--r--yt/yt/core/service_discovery/service_discovery.cpp9
-rw-r--r--yt/yt/core/service_discovery/service_discovery.h54
-rw-r--r--yt/yt/core/service_discovery/yp/config.cpp35
-rw-r--r--yt/yt/core/service_discovery/yp/config.h36
-rw-r--r--yt/yt/core/service_discovery/yp/public.h13
-rw-r--r--yt/yt/core/service_discovery/yp/service_discovery.h27
-rw-r--r--yt/yt/core/service_discovery/yp/service_discovery_dummy.cpp20
-rw-r--r--yt/yt/core/service_discovery/yp/ya.make33
-rw-r--r--yt/yt/core/test_framework/fixed_growth_string_output.cpp26
-rw-r--r--yt/yt/core/test_framework/fixed_growth_string_output.h31
-rw-r--r--yt/yt/core/test_framework/framework-inl.h33
-rw-r--r--yt/yt/core/test_framework/framework.cpp168
-rw-r--r--yt/yt/core/test_framework/framework.h228
-rw-r--r--yt/yt/core/test_framework/mock_http.cpp183
-rw-r--r--yt/yt/core/test_framework/mock_http.h97
-rw-r--r--yt/yt/core/test_framework/test_key.h119
-rw-r--r--yt/yt/core/test_framework/ya.make20
-rw-r--r--yt/yt/core/test_framework/yson_consumer_mock.h36
-rw-r--r--yt/yt/core/threading/private.h13
-rw-r--r--yt/yt/core/threading/public.h19
-rw-r--r--yt/yt/core/threading/spin_wait_slow_path_logger.cpp47
-rw-r--r--yt/yt/core/threading/spin_wait_slow_path_logger.h13
-rw-r--r--yt/yt/core/threading/thread-inl.h36
-rw-r--r--yt/yt/core/threading/thread.cpp268
-rw-r--r--yt/yt/core/threading/thread.h92
-rw-r--r--yt/yt/core/tracing/allocation_hooks.cpp67
-rw-r--r--yt/yt/core/tracing/allocation_tags.cpp49
-rw-r--r--yt/yt/core/tracing/allocation_tags.h51
-rw-r--r--yt/yt/core/tracing/config.cpp15
-rw-r--r--yt/yt/core/tracing/config.h26
-rw-r--r--yt/yt/core/tracing/private.h15
-rw-r--r--yt/yt/core/tracing/public.cpp10
-rw-r--r--yt/yt/core/tracing/public.h36
-rw-r--r--yt/yt/core/tracing/trace_context-inl.h237
-rw-r--r--yt/yt/core/tracing/trace_context.cpp717
-rw-r--r--yt/yt/core/tracing/trace_context.h399
-rw-r--r--yt/yt/core/utilex/random.cpp5
-rw-r--r--yt/yt/core/utilex/random.h10
-rw-r--r--yt/yt/core/ya.make386
-rw-r--r--yt/yt/core/ypath/helpers-inl.h56
-rw-r--r--yt/yt/core/ypath/helpers.cpp80
-rw-r--r--yt/yt/core/ypath/helpers.h32
-rw-r--r--yt/yt/core/ypath/public.h18
-rw-r--r--yt/yt/core/ypath/stack.cpp91
-rw-r--r--yt/yt/core/ypath/stack.h41
-rw-r--r--yt/yt/core/ypath/token-inl.h27
-rw-r--r--yt/yt/core/ypath/token.cpp104
-rw-r--r--yt/yt/core/ypath/token.h51
-rw-r--r--yt/yt/core/ypath/tokenizer.cpp273
-rw-r--r--yt/yt/core/ypath/tokenizer.h56
-rw-r--r--yt/yt/core/ypath/unittests/tokenizer_ut.cpp233
-rw-r--r--yt/yt/core/ypath/unittests/ya.make39
-rw-r--r--yt/yt/core/yson/async_consumer.cpp93
-rw-r--r--yt/yt/core/yson/async_consumer.h57
-rw-r--r--yt/yt/core/yson/async_writer.cpp152
-rw-r--r--yt/yt/core/yson/async_writer.h63
-rw-r--r--yt/yt/core/yson/attribute_consumer.cpp250
-rw-r--r--yt/yt/core/yson/attribute_consumer.h89
-rw-r--r--yt/yt/core/yson/attributes_stripper.cpp139
-rw-r--r--yt/yt/core/yson/attributes_stripper.h11
-rw-r--r--yt/yt/core/yson/building_consumer.h23
-rw-r--r--yt/yt/core/yson/consumer.cpp16
-rw-r--r--yt/yt/core/yson/consumer.h35
-rw-r--r--yt/yt/core/yson/depth_limiting_yson_consumer.cpp173
-rw-r--r--yt/yt/core/yson/depth_limiting_yson_consumer.h16
-rw-r--r--yt/yt/core/yson/detail.h1008
-rw-r--r--yt/yt/core/yson/format.h35
-rw-r--r--yt/yt/core/yson/forwarding_consumer.cpp330
-rw-r--r--yt/yt/core/yson/forwarding_consumer.h96
-rw-r--r--yt/yt/core/yson/lexer.cpp32
-rw-r--r--yt/yt/core/yson/lexer.h32
-rw-r--r--yt/yt/core/yson/lexer_detail.h305
-rw-r--r--yt/yt/core/yson/null_consumer.cpp62
-rw-r--r--yt/yt/core/yson/null_consumer.h51
-rw-r--r--yt/yt/core/yson/parser.cpp151
-rw-r--r--yt/yt/core/yson/parser.h70
-rw-r--r--yt/yt/core/yson/parser_detail.h527
-rw-r--r--yt/yt/core/yson/producer-inl.h31
-rw-r--r--yt/yt/core/yson/producer.cpp33
-rw-r--r--yt/yt/core/yson/producer.h74
-rw-r--r--yt/yt/core/yson/protobuf_interop-inl.h57
-rw-r--r--yt/yt/core/yson/protobuf_interop.cpp2902
-rw-r--r--yt/yt/core/yson/protobuf_interop.h289
-rw-r--r--yt/yt/core/yson/protobuf_interop_options.cpp16
-rw-r--r--yt/yt/core/yson/protobuf_interop_options.h38
-rw-r--r--yt/yt/core/yson/protobuf_interop_unknown_fields.cpp181
-rw-r--r--yt/yt/core/yson/protobuf_interop_unknown_fields.h59
-rw-r--r--yt/yt/core/yson/public.h68
-rw-r--r--yt/yt/core/yson/pull_parser-inl.h956
-rw-r--r--yt/yt/core/yson/pull_parser.cpp689
-rw-r--r--yt/yt/core/yson/pull_parser.h344
-rw-r--r--yt/yt/core/yson/pull_parser_deserialize-inl.h375
-rw-r--r--yt/yt/core/yson/pull_parser_deserialize.cpp242
-rw-r--r--yt/yt/core/yson/pull_parser_deserialize.h172
-rw-r--r--yt/yt/core/yson/stream.cpp80
-rw-r--r--yt/yt/core/yson/stream.h57
-rw-r--r--yt/yt/core/yson/string.cpp69
-rw-r--r--yt/yt/core/yson/string.h42
-rw-r--r--yt/yt/core/yson/string_filter.cpp390
-rw-r--r--yt/yt/core/yson/string_filter.h35
-rw-r--r--yt/yt/core/yson/string_merger.cpp194
-rw-r--r--yt/yt/core/yson/string_merger.h24
-rw-r--r--yt/yt/core/yson/syntax_checker-inl.h394
-rw-r--r--yt/yt/core/yson/syntax_checker.cpp80
-rw-r--r--yt/yt/core/yson/syntax_checker.h127
-rw-r--r--yt/yt/core/yson/token.cpp254
-rw-r--r--yt/yt/core/yson/token.h89
-rw-r--r--yt/yt/core/yson/token_writer-inl.h127
-rw-r--r--yt/yt/core/yson/token_writer.cpp250
-rw-r--r--yt/yt/core/yson/token_writer.h125
-rw-r--r--yt/yt/core/yson/tokenizer.cpp49
-rw-r--r--yt/yt/core/yson/tokenizer.h32
-rw-r--r--yt/yt/core/yson/unittests/depth_limiting_yson_consumer_ut.cpp359
-rw-r--r--yt/yt/core/yson/unittests/filter_ut.cpp176
-rw-r--r--yt/yt/core/yson/unittests/lexer_ut.cpp189
-rw-r--r--yt/yt/core/yson/unittests/proto/protobuf_yson_casing_ut.proto12
-rw-r--r--yt/yt/core/yson/unittests/proto/protobuf_yson_ut.proto167
-rw-r--r--yt/yt/core/yson/unittests/protobuf_yson_ut.cpp2585
-rw-r--r--yt/yt/core/yson/unittests/ya.make51
-rw-r--r--yt/yt/core/yson/unittests/ypath_designated_yson_consumer_ut.cpp276
-rw-r--r--yt/yt/core/yson/unittests/yson_parser_ut.cpp749
-rw-r--r--yt/yt/core/yson/unittests/yson_pull_parser_ut.cpp1354
-rw-r--r--yt/yt/core/yson/unittests/yson_token_writer_ut.cpp267
-rw-r--r--yt/yt/core/yson/unittests/yson_ut.cpp498
-rw-r--r--yt/yt/core/yson/unittests/yson_writer_ut.cpp447
-rw-r--r--yt/yt/core/yson/writer.cpp524
-rw-r--r--yt/yt/core/yson/writer.h161
-rw-r--r--yt/yt/core/yson/ypath_designated_consumer.cpp206
-rw-r--r--yt/yt/core/yson/ypath_designated_consumer.h22
-rw-r--r--yt/yt/core/ytalloc/bindings.cpp376
-rw-r--r--yt/yt/core/ytalloc/bindings.h36
-rw-r--r--yt/yt/core/ytalloc/config.cpp54
-rw-r--r--yt/yt/core/ytalloc/config.h53
-rw-r--r--yt/yt/core/ytalloc/public.h14
-rw-r--r--yt/yt/core/ytalloc/statistics_producer.cpp24
-rw-r--r--yt/yt/core/ytalloc/statistics_producer.h15
-rw-r--r--yt/yt/core/ytree/attribute_consumer.cpp86
-rw-r--r--yt/yt/core/ytree/attribute_consumer.h47
-rw-r--r--yt/yt/core/ytree/attribute_filter.cpp503
-rw-r--r--yt/yt/core/ytree/attribute_filter.h164
-rw-r--r--yt/yt/core/ytree/attribute_owner.h19
-rw-r--r--yt/yt/core/ytree/attributes.cpp82
-rw-r--r--yt/yt/core/ytree/attributes.h102
-rw-r--r--yt/yt/core/ytree/convert-inl.h298
-rw-r--r--yt/yt/core/ytree/convert.cpp65
-rw-r--r--yt/yt/core/ytree/convert.h59
-rw-r--r--yt/yt/core/ytree/default_building_consumer-inl.h82
-rw-r--r--yt/yt/core/ytree/default_building_consumer.h18
-rw-r--r--yt/yt/core/ytree/ephemeral_attribute_owner.cpp36
-rw-r--r--yt/yt/core/ytree/ephemeral_attribute_owner.h27
-rw-r--r--yt/yt/core/ytree/ephemeral_node_factory.cpp517
-rw-r--r--yt/yt/core/ytree/ephemeral_node_factory.h19
-rw-r--r--yt/yt/core/ytree/exception_helpers.cpp153
-rw-r--r--yt/yt/core/ytree/exception_helpers.h40
-rw-r--r--yt/yt/core/ytree/fluent.h794
-rw-r--r--yt/yt/core/ytree/helpers-inl.h133
-rw-r--r--yt/yt/core/ytree/helpers.cpp344
-rw-r--r--yt/yt/core/ytree/helpers.h77
-rw-r--r--yt/yt/core/ytree/interned_attributes-inl.h18
-rw-r--r--yt/yt/core/ytree/interned_attributes.cpp81
-rw-r--r--yt/yt/core/ytree/interned_attributes.h57
-rw-r--r--yt/yt/core/ytree/node-inl.h69
-rw-r--r--yt/yt/core/ytree/node.cpp110
-rw-r--r--yt/yt/core/ytree/node.h417
-rw-r--r--yt/yt/core/ytree/node_detail.cpp665
-rw-r--r--yt/yt/core/ytree/node_detail.h245
-rw-r--r--yt/yt/core/ytree/permission.cpp22
-rw-r--r--yt/yt/core/ytree/permission.h74
-rw-r--r--yt/yt/core/ytree/public.h132
-rw-r--r--yt/yt/core/ytree/request_complexity_limiter.cpp261
-rw-r--r--yt/yt/core/ytree/request_complexity_limiter.h109
-rw-r--r--yt/yt/core/ytree/serialize-inl.h643
-rw-r--r--yt/yt/core/ytree/serialize.cpp331
-rw-r--r--yt/yt/core/ytree/serialize.h265
-rw-r--r--yt/yt/core/ytree/service_combiner.cpp383
-rw-r--r--yt/yt/core/ytree/service_combiner.h35
-rw-r--r--yt/yt/core/ytree/static_service_dispatcher.cpp63
-rw-r--r--yt/yt/core/ytree/static_service_dispatcher.h46
-rw-r--r--yt/yt/core/ytree/system_attribute_provider.cpp73
-rw-r--r--yt/yt/core/ytree/system_attribute_provider.h161
-rw-r--r--yt/yt/core/ytree/tree_builder.cpp180
-rw-r--r--yt/yt/core/ytree/tree_builder.h43
-rw-r--r--yt/yt/core/ytree/tree_visitor.cpp189
-rw-r--r--yt/yt/core/ytree/tree_visitor.h29
-rw-r--r--yt/yt/core/ytree/unittests/attribute_filter_ut.cpp92
-rw-r--r--yt/yt/core/ytree/unittests/attributes_ut.cpp111
-rw-r--r--yt/yt/core/ytree/unittests/proto/test.proto7
-rw-r--r--yt/yt/core/ytree/unittests/resolver_ut.cpp124
-rw-r--r--yt/yt/core/ytree/unittests/serialize_ut.cpp538
-rw-r--r--yt/yt/core/ytree/unittests/serialize_ut.h16
-rw-r--r--yt/yt/core/ytree/unittests/service_combiner_ut.cpp198
-rw-r--r--yt/yt/core/ytree/unittests/tree_builder_ut.cpp198
-rw-r--r--yt/yt/core/ytree/unittests/ya.make53
-rw-r--r--yt/yt/core/ytree/unittests/ypath_designated_service_ut.cpp293
-rw-r--r--yt/yt/core/ytree/unittests/yson_serializable_ut.cpp1171
-rw-r--r--yt/yt/core/ytree/unittests/yson_struct_ut.cpp1599
-rw-r--r--yt/yt/core/ytree/unittests/ytree_fluent_ut.cpp432
-rw-r--r--yt/yt/core/ytree/unittests/ytree_ut.cpp241
-rw-r--r--yt/yt/core/ytree/virtual-inl.h112
-rw-r--r--yt/yt/core/ytree/virtual.cpp610
-rw-r--r--yt/yt/core/ytree/virtual.h125
-rw-r--r--yt/yt/core/ytree/ypath_client-inl.h37
-rw-r--r--yt/yt/core/ytree/ypath_client.cpp930
-rw-r--r--yt/yt/core/ytree/ypath_client.h385
-rw-r--r--yt/yt/core/ytree/ypath_detail-inl.h21
-rw-r--r--yt/yt/core/ytree/ypath_detail.cpp1858
-rw-r--r--yt/yt/core/ytree/ypath_detail.h394
-rw-r--r--yt/yt/core/ytree/ypath_proxy.h26
-rw-r--r--yt/yt/core/ytree/ypath_resolver.cpp370
-rw-r--r--yt/yt/core/ytree/ypath_resolver.h27
-rw-r--r--yt/yt/core/ytree/ypath_service.cpp972
-rw-r--r--yt/yt/core/ytree/ypath_service.h172
-rw-r--r--yt/yt/core/ytree/yson_serializable-inl.h1038
-rw-r--r--yt/yt/core/ytree/yson_serializable.cpp385
-rw-r--r--yt/yt/core/ytree/yson_serializable.h268
-rw-r--r--yt/yt/core/ytree/yson_serialize_common.h27
-rw-r--r--yt/yt/core/ytree/yson_struct-inl.h296
-rw-r--r--yt/yt/core/ytree/yson_struct.cpp219
-rw-r--r--yt/yt/core/ytree/yson_struct.h313
-rw-r--r--yt/yt/core/ytree/yson_struct_detail-inl.h911
-rw-r--r--yt/yt/core/ytree/yson_struct_detail.cpp339
-rw-r--r--yt/yt/core/ytree/yson_struct_detail.h283
2270 files changed, 539847 insertions, 0 deletions
diff --git a/contrib/libs/backtrace/LICENSE b/contrib/libs/backtrace/LICENSE
new file mode 100644
index 0000000000..097d2774e5
--- /dev/null
+++ b/contrib/libs/backtrace/LICENSE
@@ -0,0 +1,29 @@
+# Copyright (C) 2012-2016 Free Software Foundation, Inc.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+# (1) Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+
+# (2) Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+
+# (3) The name of the author may not be used to
+# endorse or promote products derived from this software without
+# specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
diff --git a/contrib/libs/backtrace/README.md b/contrib/libs/backtrace/README.md
new file mode 100644
index 0000000000..c82834d174
--- /dev/null
+++ b/contrib/libs/backtrace/README.md
@@ -0,0 +1,36 @@
+# libbacktrace
+A C library that may be linked into a C/C++ program to produce symbolic backtraces
+
+Initially written by Ian Lance Taylor <iant@golang.org>.
+
+This is version 1.0.
+It is likely that this will always be version 1.0.
+
+The libbacktrace library may be linked into a program or library and
+used to produce symbolic backtraces.
+Sample uses would be to print a detailed backtrace when an error
+occurs or to gather detailed profiling information.
+In general the functions provided by this library are async-signal-safe,
+meaning that they may be safely called from a signal handler.
+
+The libbacktrace library is provided under a BSD license.
+See the source files for the exact license text.
+
+The public functions are declared and documented in the header file
+backtrace.h, which should be #include'd by a user of the library.
+
+Building libbacktrace will generate a file backtrace-supported.h,
+which a user of the library may use to determine whether backtraces
+will work.
+See the source file backtrace-supported.h.in for the macros that it
+defines.
+
+As of October 2020, libbacktrace supports ELF, PE/COFF, Mach-O, and
+XCOFF executables with DWARF debugging information.
+In other words, it supports GNU/Linux, *BSD, macOS, Windows, and AIX.
+The library is written to make it straightforward to add support for
+other object file and debugging formats.
+
+The library relies on the C++ unwind API defined at
+https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html
+This API is provided by GCC and clang.
diff --git a/contrib/libs/backtrace/atomic.c b/contrib/libs/backtrace/atomic.c
new file mode 100644
index 0000000000..fcac485b23
--- /dev/null
+++ b/contrib/libs/backtrace/atomic.c
@@ -0,0 +1,113 @@
+/* atomic.c -- Support for atomic functions if not present.
+ Copyright (C) 2013-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <sys/types.h>
+
+#include "backtrace.h"
+#include "backtrace-supported.h"
+#include "internal.h"
+
+/* This file holds implementations of the atomic functions that are
+ used if the host compiler has the sync functions but not the atomic
+ functions, as is true of versions of GCC before 4.7. */
+
+#if !defined (HAVE_ATOMIC_FUNCTIONS) && defined (HAVE_SYNC_FUNCTIONS)
+
+/* Do an atomic load of a pointer. */
+
+void *
+backtrace_atomic_load_pointer (void *arg)
+{
+ void **pp;
+ void *p;
+
+ pp = (void **) arg;
+ p = *pp;
+ while (!__sync_bool_compare_and_swap (pp, p, p))
+ p = *pp;
+ return p;
+}
+
+/* Do an atomic load of an int. */
+
+int
+backtrace_atomic_load_int (int *p)
+{
+ int i;
+
+ i = *p;
+ while (!__sync_bool_compare_and_swap (p, i, i))
+ i = *p;
+ return i;
+}
+
+/* Do an atomic store of a pointer. */
+
+void
+backtrace_atomic_store_pointer (void *arg, void *p)
+{
+ void **pp;
+ void *old;
+
+ pp = (void **) arg;
+ old = *pp;
+ while (!__sync_bool_compare_and_swap (pp, old, p))
+ old = *pp;
+}
+
+/* Do an atomic store of a size_t value. */
+
+void
+backtrace_atomic_store_size_t (size_t *p, size_t v)
+{
+ size_t old;
+
+ old = *p;
+ while (!__sync_bool_compare_and_swap (p, old, v))
+ old = *p;
+}
+
+/* Do an atomic store of a int value. */
+
+void
+backtrace_atomic_store_int (int *p, int v)
+{
+ size_t old;
+
+ old = *p;
+ while (!__sync_bool_compare_and_swap (p, old, v))
+ old = *p;
+}
+
+#endif
diff --git a/contrib/libs/backtrace/backtrace-supported.h b/contrib/libs/backtrace/backtrace-supported.h
new file mode 100644
index 0000000000..1ece38888f
--- /dev/null
+++ b/contrib/libs/backtrace/backtrace-supported.h
@@ -0,0 +1,66 @@
+/* backtrace-supported.h.in -- Whether stack backtrace is supported.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+/* The file backtrace-supported.h.in is used by configure to generate
+ the file backtrace-supported.h. The file backtrace-supported.h may
+ be #include'd to see whether the backtrace library will be able to
+ get a backtrace and produce symbolic information. */
+
+
+/* BACKTRACE_SUPPORTED will be #define'd as 1 if the backtrace library
+ should work, 0 if it will not. Libraries may #include this to make
+ other arrangements. */
+
+#define BACKTRACE_SUPPORTED 1
+
+/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace
+ library will call malloc as it works, 0 if it will call mmap
+ instead. This may be used to determine whether it is safe to call
+ the backtrace functions from a signal handler. In general this
+ only applies to calls like backtrace and backtrace_pcinfo. It does
+ not apply to backtrace_simple, which never calls malloc. It does
+ not apply to backtrace_print, which always calls fprintf and
+ therefore malloc. */
+
+#define BACKTRACE_USES_MALLOC 0
+
+/* BACKTRACE_SUPPORTS_THREADS will be #define'd as 1 if the backtrace
+ library is configured with threading support, 0 if not. If this is
+ 0, the threaded parameter to backtrace_create_state must be passed
+ as 0. */
+
+#define BACKTRACE_SUPPORTS_THREADS 1
+
+/* BACKTRACE_SUPPORTS_DATA will be #defined'd as 1 if the backtrace_syminfo
+ will work for variables. It will always work for functions. */
+
+#define BACKTRACE_SUPPORTS_DATA 1
diff --git a/contrib/libs/backtrace/backtrace.c b/contrib/libs/backtrace/backtrace.c
new file mode 100644
index 0000000000..7b62900852
--- /dev/null
+++ b/contrib/libs/backtrace/backtrace.c
@@ -0,0 +1,129 @@
+/* backtrace.c -- Entry point for stack backtrace library.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <sys/types.h>
+
+#include "unwind.h"
+#include "backtrace.h"
+#include "internal.h"
+
+/* The main backtrace_full routine. */
+
+/* Data passed through _Unwind_Backtrace. */
+
+struct backtrace_data
+{
+ /* Number of frames to skip. */
+ int skip;
+ /* Library state. */
+ struct backtrace_state *state;
+ /* Callback routine. */
+ backtrace_full_callback callback;
+ /* Error callback routine. */
+ backtrace_error_callback error_callback;
+ /* Data to pass to callback routines. */
+ void *data;
+ /* Value to return from backtrace_full. */
+ int ret;
+ /* Whether there is any memory available. */
+ int can_alloc;
+};
+
+/* Unwind library callback routine. This is passed to
+ _Unwind_Backtrace. */
+
+static _Unwind_Reason_Code
+unwind (struct _Unwind_Context *context, void *vdata)
+{
+ struct backtrace_data *bdata = (struct backtrace_data *) vdata;
+ uintptr_t pc;
+ int ip_before_insn = 0;
+
+#ifdef HAVE_GETIPINFO
+ pc = _Unwind_GetIPInfo (context, &ip_before_insn);
+#else
+ pc = _Unwind_GetIP (context);
+#endif
+
+ if (bdata->skip > 0)
+ {
+ --bdata->skip;
+ return _URC_NO_REASON;
+ }
+
+ if (!ip_before_insn)
+ --pc;
+
+ if (!bdata->can_alloc)
+ bdata->ret = bdata->callback (bdata->data, pc, NULL, 0, NULL);
+ else
+ bdata->ret = backtrace_pcinfo (bdata->state, pc, bdata->callback,
+ bdata->error_callback, bdata->data);
+ if (bdata->ret != 0)
+ return _URC_END_OF_STACK;
+
+ return _URC_NO_REASON;
+}
+
+/* Get a stack backtrace. */
+
+int __attribute__((noinline))
+backtrace_full (struct backtrace_state *state, int skip,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ struct backtrace_data bdata;
+ void *p;
+
+ bdata.skip = skip + 1;
+ bdata.state = state;
+ bdata.callback = callback;
+ bdata.error_callback = error_callback;
+ bdata.data = data;
+ bdata.ret = 0;
+
+ /* If we can't allocate any memory at all, don't try to produce
+ file/line information. */
+ p = backtrace_alloc (state, 4096, NULL, NULL);
+ if (p == NULL)
+ bdata.can_alloc = 0;
+ else
+ {
+ backtrace_free (state, p, 4096, NULL, NULL);
+ bdata.can_alloc = 1;
+ }
+
+ _Unwind_Backtrace (unwind, &bdata);
+ return bdata.ret;
+}
diff --git a/contrib/libs/backtrace/backtrace.h b/contrib/libs/backtrace/backtrace.h
new file mode 100644
index 0000000000..69cea4ca1e
--- /dev/null
+++ b/contrib/libs/backtrace/backtrace.h
@@ -0,0 +1,189 @@
+/* backtrace.h -- Public header file for stack backtrace library.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#ifndef BACKTRACE_H
+#define BACKTRACE_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* The backtrace state. This struct is intentionally not defined in
+ the public interface. */
+
+struct backtrace_state;
+
+/* The type of the error callback argument to backtrace functions.
+ This function, if not NULL, will be called for certain error cases.
+ The DATA argument is passed to the function that calls this one.
+ The MSG argument is an error message. The ERRNUM argument, if
+ greater than 0, holds an errno value. The MSG buffer may become
+ invalid after this function returns.
+
+ As a special case, the ERRNUM argument will be passed as -1 if no
+ debug info can be found for the executable, or if the debug info
+ exists but has an unsupported version, but the function requires
+ debug info (e.g., backtrace_full, backtrace_pcinfo). The MSG in
+ this case will be something along the lines of "no debug info".
+ Similarly, ERRNUM will be passed as -1 if there is no symbol table,
+ but the function requires a symbol table (e.g., backtrace_syminfo).
+ This may be used as a signal that some other approach should be
+ tried. */
+
+typedef void (*backtrace_error_callback) (void *data, const char *msg,
+ int errnum);
+
+/* Create state information for the backtrace routines. This must be
+ called before any of the other routines, and its return value must
+ be passed to all of the other routines. FILENAME is the path name
+ of the executable file; if it is NULL the library will try
+ system-specific path names. If not NULL, FILENAME must point to a
+ permanent buffer. If THREADED is non-zero the state may be
+ accessed by multiple threads simultaneously, and the library will
+ use appropriate atomic operations. If THREADED is zero the state
+ may only be accessed by one thread at a time. This returns a state
+ pointer on success, NULL on error. If an error occurs, this will
+ call the ERROR_CALLBACK routine.
+
+ Calling this function allocates resources that cannot be freed.
+ There is no backtrace_free_state function. The state is used to
+ cache information that is expensive to recompute. Programs are
+ expected to call this function at most once and to save the return
+ value for all later calls to backtrace functions. */
+
+extern struct backtrace_state *backtrace_create_state (
+ const char *filename, int threaded,
+ backtrace_error_callback error_callback, void *data);
+
+/* The type of the callback argument to the backtrace_full function.
+ DATA is the argument passed to backtrace_full. PC is the program
+ counter. FILENAME is the name of the file containing PC, or NULL
+ if not available. LINENO is the line number in FILENAME containing
+ PC, or 0 if not available. FUNCTION is the name of the function
+ containing PC, or NULL if not available. This should return 0 to
+ continuing tracing. The FILENAME and FUNCTION buffers may become
+ invalid after this function returns. */
+
+typedef int (*backtrace_full_callback) (void *data, uintptr_t pc,
+ const char *filename, int lineno,
+ const char *function);
+
+/* Get a full stack backtrace. SKIP is the number of frames to skip;
+ passing 0 will start the trace with the function calling
+ backtrace_full. DATA is passed to the callback routine. If any
+ call to CALLBACK returns a non-zero value, the stack backtrace
+ stops, and backtrace returns that value; this may be used to limit
+ the number of stack frames desired. If all calls to CALLBACK
+ return 0, backtrace returns 0. The backtrace_full function will
+ make at least one call to either CALLBACK or ERROR_CALLBACK. This
+ function requires debug info for the executable. */
+
+extern int backtrace_full (struct backtrace_state *state, int skip,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* The type of the callback argument to the backtrace_simple function.
+ DATA is the argument passed to simple_backtrace. PC is the program
+ counter. This should return 0 to continue tracing. */
+
+typedef int (*backtrace_simple_callback) (void *data, uintptr_t pc);
+
+/* Get a simple backtrace. SKIP is the number of frames to skip, as
+ in backtrace. DATA is passed to the callback routine. If any call
+ to CALLBACK returns a non-zero value, the stack backtrace stops,
+ and backtrace_simple returns that value. Otherwise
+ backtrace_simple returns 0. The backtrace_simple function will
+ make at least one call to either CALLBACK or ERROR_CALLBACK. This
+ function does not require any debug info for the executable. */
+
+extern int backtrace_simple (struct backtrace_state *state, int skip,
+ backtrace_simple_callback callback,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* Print the current backtrace in a user readable format to a FILE.
+ SKIP is the number of frames to skip, as in backtrace_full. Any
+ error messages are printed to stderr. This function requires debug
+ info for the executable. */
+
+extern void backtrace_print (struct backtrace_state *state, int skip, FILE *);
+
+/* Given PC, a program counter in the current program, call the
+ callback function with filename, line number, and function name
+ information. This will normally call the callback function exactly
+ once. However, if the PC happens to describe an inlined call, and
+ the debugging information contains the necessary information, then
+ this may call the callback function multiple times. This will make
+ at least one call to either CALLBACK or ERROR_CALLBACK. This
+ returns the first non-zero value returned by CALLBACK, or 0. */
+
+extern int backtrace_pcinfo (struct backtrace_state *state, uintptr_t pc,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* The type of the callback argument to backtrace_syminfo. DATA and
+ PC are the arguments passed to backtrace_syminfo. SYMNAME is the
+ name of the symbol for the corresponding code. SYMVAL is the
+ value and SYMSIZE is the size of the symbol. SYMNAME will be NULL
+ if no error occurred but the symbol could not be found. */
+
+typedef void (*backtrace_syminfo_callback) (void *data, uintptr_t pc,
+ const char *symname,
+ uintptr_t symval,
+ uintptr_t symsize);
+
+/* Given ADDR, an address or program counter in the current program,
+ call the callback information with the symbol name and value
+ describing the function or variable in which ADDR may be found.
+ This will call either CALLBACK or ERROR_CALLBACK exactly once.
+ This returns 1 on success, 0 on failure. This function requires
+ the symbol table but does not require the debug info. Note that if
+ the symbol table is present but ADDR could not be found in the
+ table, CALLBACK will be called with a NULL SYMNAME argument.
+ Returns 1 on success, 0 on error. */
+
+extern int backtrace_syminfo (struct backtrace_state *state, uintptr_t addr,
+ backtrace_syminfo_callback callback,
+ backtrace_error_callback error_callback,
+ void *data);
+
+#ifdef __cplusplus
+} /* End extern "C". */
+#endif
+
+#endif
diff --git a/contrib/libs/backtrace/config-armv7a.h b/contrib/libs/backtrace/config-armv7a.h
new file mode 100644
index 0000000000..2e036a2b5e
--- /dev/null
+++ b/contrib/libs/backtrace/config-armv7a.h
@@ -0,0 +1,2 @@
+#include "config-linux.h"
+#undef HAVE_GETIPINFO
diff --git a/contrib/libs/backtrace/config-linux.h b/contrib/libs/backtrace/config-linux.h
new file mode 100644
index 0000000000..8cec45b181
--- /dev/null
+++ b/contrib/libs/backtrace/config-linux.h
@@ -0,0 +1,173 @@
+/* config.h. Generated from config.h.in by configure. */
+/* config.h.in. Generated from configure.ac by autoheader. */
+
+/* ELF size: 32 or 64 */
+#define BACKTRACE_ELF_SIZE 64
+
+/* XCOFF size: 32 or 64 */
+#define BACKTRACE_XCOFF_SIZE unused
+
+/* Define to 1 if you have the __atomic functions */
+#define HAVE_ATOMIC_FUNCTIONS 1
+
+/* Define to 1 if you have the `clock_gettime' function. */
+#define HAVE_CLOCK_GETTIME 1
+
+/* Define to 1 if you have the declaration of `getpagesize', and to 0 if you
+ don't. */
+#define HAVE_DECL_GETPAGESIZE 1
+
+/* Define to 1 if you have the declaration of `strnlen', and to 0 if you
+ don't. */
+#define HAVE_DECL_STRNLEN 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define if dl_iterate_phdr is available. */
+#define HAVE_DL_ITERATE_PHDR 1
+
+/* Define to 1 if you have the fcntl function */
+#define HAVE_FCNTL 1
+
+/* Define if getexecname is available. */
+/* #undef HAVE_GETEXECNAME */
+
+/* Define if _Unwind_GetIPInfo is available. */
+#define HAVE_GETIPINFO 1
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have KERN_PROC and KERN_PROC_PATHNAME in <sys/sysctl.h>.
+ */
+/* #undef HAVE_KERN_PROC */
+
+/* Define to 1 if you have KERN_PROCARGS and KERN_PROC_PATHNAME in
+ <sys/sysctl.h>. */
+/* #undef HAVE_KERN_PROC_ARGS */
+
+/* Define if -llzma is available. */
+/* #undef HAVE_LIBLZMA */
+
+/* Define to 1 if you have the <link.h> header file. */
+#define HAVE_LINK_H 1
+
+/* Define if AIX loadquery is available. */
+/* #undef HAVE_LOADQUERY */
+
+/* Define to 1 if you have the `lstat' function. */
+#define HAVE_LSTAT 1
+
+/* Define to 1 if you have the <mach-o/dyld.h> header file. */
+/* #undef HAVE_MACH_O_DYLD_H */
+
+/* Define to 1 if you have the <memory.h> header file. */
+#define HAVE_MEMORY_H 1
+
+/* Define to 1 if you have the `readlink' function. */
+#define HAVE_READLINK 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have the __sync functions */
+#define HAVE_SYNC_FUNCTIONS 1
+
+/* Define to 1 if you have the <sys/ldr.h> header file. */
+/* #undef HAVE_SYS_LDR_H */
+
+/* Define to 1 if you have the <sys/link.h> header file. */
+/* #undef HAVE_SYS_LINK_H */
+
+/* Define to 1 if you have the <sys/mman.h> header file. */
+#define HAVE_SYS_MMAN_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define if -lz is available. */
+/* #undef HAVE_ZLIB */
+
+/* Define to the sub-directory in which libtool stores uninstalled libraries.
+ */
+#define LT_OBJDIR ".libs/"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT ""
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME "package-unused"
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING "package-unused version-unused"
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME "libbacktrace"
+
+/* Define to the home page for this package. */
+#define PACKAGE_URL ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION "version-unused"
+
+/* Define to 1 if you have the ANSI C header files. */
+#define STDC_HEADERS 1
+
+/* Enable extensions on AIX 3, Interix. */
+#ifndef _ALL_SOURCE
+# define _ALL_SOURCE 1
+#endif
+/* Enable GNU extensions on systems that have them. */
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE 1
+#endif
+/* Enable threading extensions on Solaris. */
+#ifndef _POSIX_PTHREAD_SEMANTICS
+# define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+/* Enable extensions on HP NonStop. */
+#ifndef _TANDEM_SOURCE
+# define _TANDEM_SOURCE 1
+#endif
+/* Enable general extensions on Solaris. */
+#ifndef __EXTENSIONS__
+# define __EXTENSIONS__ 1
+#endif
+
+
+/* Enable large inode numbers on Mac OS X 10.5. */
+#ifndef _DARWIN_USE_64_BIT_INODE
+# define _DARWIN_USE_64_BIT_INODE 1
+#endif
+
+/* Number of bits in a file offset, on hosts where this is settable. */
+/* #undef _FILE_OFFSET_BITS */
+
+/* Define for large files, on AIX-style hosts. */
+/* #undef _LARGE_FILES */
+
+/* Define to 1 if on MINIX. */
+/* #undef _MINIX */
+
+/* Define to 2 if the system does not provide POSIX.1 features except with
+ this defined. */
+/* #undef _POSIX_1_SOURCE */
+
+/* Define to 1 if you need to in order for `stat' and other things to work. */
+/* #undef _POSIX_SOURCE */
diff --git a/contrib/libs/backtrace/config-osx.h b/contrib/libs/backtrace/config-osx.h
new file mode 100644
index 0000000000..ab7295ea4c
--- /dev/null
+++ b/contrib/libs/backtrace/config-osx.h
@@ -0,0 +1,7 @@
+#pragma once
+#include "config-linux.h"
+
+/* Set the defines to use dyld instead of ld on macOS */
+#undef HAVE_DL_ITERATE_PHDR
+#undef HAVE_LINK_H
+#define HAVE_MACH_O_DYLD_H 1
diff --git a/contrib/libs/backtrace/config.h b/contrib/libs/backtrace/config.h
new file mode 100644
index 0000000000..44ded9375a
--- /dev/null
+++ b/contrib/libs/backtrace/config.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#if defined(__APPLE__)
+# include "config-osx.h"
+#else
+# include "config-linux.h"
+#endif
+
+#if defined(__arm__) || defined(__ARM__)
+# include "config-armv7a.h"
+#endif
diff --git a/contrib/libs/backtrace/dwarf.c b/contrib/libs/backtrace/dwarf.c
new file mode 100644
index 0000000000..5b2724e6a7
--- /dev/null
+++ b/contrib/libs/backtrace/dwarf.c
@@ -0,0 +1,4402 @@
+/* dwarf.c -- Get file/line information from DWARF for backtraces.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "filenames.h"
+
+#include "backtrace.h"
+#include "internal.h"
+
+/* DWARF constants. */
+
+enum dwarf_tag {
+ DW_TAG_entry_point = 0x3,
+ DW_TAG_compile_unit = 0x11,
+ DW_TAG_inlined_subroutine = 0x1d,
+ DW_TAG_subprogram = 0x2e,
+ DW_TAG_skeleton_unit = 0x4a,
+};
+
+enum dwarf_form {
+ DW_FORM_addr = 0x01,
+ DW_FORM_block2 = 0x03,
+ DW_FORM_block4 = 0x04,
+ DW_FORM_data2 = 0x05,
+ DW_FORM_data4 = 0x06,
+ DW_FORM_data8 = 0x07,
+ DW_FORM_string = 0x08,
+ DW_FORM_block = 0x09,
+ DW_FORM_block1 = 0x0a,
+ DW_FORM_data1 = 0x0b,
+ DW_FORM_flag = 0x0c,
+ DW_FORM_sdata = 0x0d,
+ DW_FORM_strp = 0x0e,
+ DW_FORM_udata = 0x0f,
+ DW_FORM_ref_addr = 0x10,
+ DW_FORM_ref1 = 0x11,
+ DW_FORM_ref2 = 0x12,
+ DW_FORM_ref4 = 0x13,
+ DW_FORM_ref8 = 0x14,
+ DW_FORM_ref_udata = 0x15,
+ DW_FORM_indirect = 0x16,
+ DW_FORM_sec_offset = 0x17,
+ DW_FORM_exprloc = 0x18,
+ DW_FORM_flag_present = 0x19,
+ DW_FORM_ref_sig8 = 0x20,
+ DW_FORM_strx = 0x1a,
+ DW_FORM_addrx = 0x1b,
+ DW_FORM_ref_sup4 = 0x1c,
+ DW_FORM_strp_sup = 0x1d,
+ DW_FORM_data16 = 0x1e,
+ DW_FORM_line_strp = 0x1f,
+ DW_FORM_implicit_const = 0x21,
+ DW_FORM_loclistx = 0x22,
+ DW_FORM_rnglistx = 0x23,
+ DW_FORM_ref_sup8 = 0x24,
+ DW_FORM_strx1 = 0x25,
+ DW_FORM_strx2 = 0x26,
+ DW_FORM_strx3 = 0x27,
+ DW_FORM_strx4 = 0x28,
+ DW_FORM_addrx1 = 0x29,
+ DW_FORM_addrx2 = 0x2a,
+ DW_FORM_addrx3 = 0x2b,
+ DW_FORM_addrx4 = 0x2c,
+ DW_FORM_GNU_addr_index = 0x1f01,
+ DW_FORM_GNU_str_index = 0x1f02,
+ DW_FORM_GNU_ref_alt = 0x1f20,
+ DW_FORM_GNU_strp_alt = 0x1f21
+};
+
+enum dwarf_attribute {
+ DW_AT_sibling = 0x01,
+ DW_AT_location = 0x02,
+ DW_AT_name = 0x03,
+ DW_AT_ordering = 0x09,
+ DW_AT_subscr_data = 0x0a,
+ DW_AT_byte_size = 0x0b,
+ DW_AT_bit_offset = 0x0c,
+ DW_AT_bit_size = 0x0d,
+ DW_AT_element_list = 0x0f,
+ DW_AT_stmt_list = 0x10,
+ DW_AT_low_pc = 0x11,
+ DW_AT_high_pc = 0x12,
+ DW_AT_language = 0x13,
+ DW_AT_member = 0x14,
+ DW_AT_discr = 0x15,
+ DW_AT_discr_value = 0x16,
+ DW_AT_visibility = 0x17,
+ DW_AT_import = 0x18,
+ DW_AT_string_length = 0x19,
+ DW_AT_common_reference = 0x1a,
+ DW_AT_comp_dir = 0x1b,
+ DW_AT_const_value = 0x1c,
+ DW_AT_containing_type = 0x1d,
+ DW_AT_default_value = 0x1e,
+ DW_AT_inline = 0x20,
+ DW_AT_is_optional = 0x21,
+ DW_AT_lower_bound = 0x22,
+ DW_AT_producer = 0x25,
+ DW_AT_prototyped = 0x27,
+ DW_AT_return_addr = 0x2a,
+ DW_AT_start_scope = 0x2c,
+ DW_AT_bit_stride = 0x2e,
+ DW_AT_upper_bound = 0x2f,
+ DW_AT_abstract_origin = 0x31,
+ DW_AT_accessibility = 0x32,
+ DW_AT_address_class = 0x33,
+ DW_AT_artificial = 0x34,
+ DW_AT_base_types = 0x35,
+ DW_AT_calling_convention = 0x36,
+ DW_AT_count = 0x37,
+ DW_AT_data_member_location = 0x38,
+ DW_AT_decl_column = 0x39,
+ DW_AT_decl_file = 0x3a,
+ DW_AT_decl_line = 0x3b,
+ DW_AT_declaration = 0x3c,
+ DW_AT_discr_list = 0x3d,
+ DW_AT_encoding = 0x3e,
+ DW_AT_external = 0x3f,
+ DW_AT_frame_base = 0x40,
+ DW_AT_friend = 0x41,
+ DW_AT_identifier_case = 0x42,
+ DW_AT_macro_info = 0x43,
+ DW_AT_namelist_items = 0x44,
+ DW_AT_priority = 0x45,
+ DW_AT_segment = 0x46,
+ DW_AT_specification = 0x47,
+ DW_AT_static_link = 0x48,
+ DW_AT_type = 0x49,
+ DW_AT_use_location = 0x4a,
+ DW_AT_variable_parameter = 0x4b,
+ DW_AT_virtuality = 0x4c,
+ DW_AT_vtable_elem_location = 0x4d,
+ DW_AT_allocated = 0x4e,
+ DW_AT_associated = 0x4f,
+ DW_AT_data_location = 0x50,
+ DW_AT_byte_stride = 0x51,
+ DW_AT_entry_pc = 0x52,
+ DW_AT_use_UTF8 = 0x53,
+ DW_AT_extension = 0x54,
+ DW_AT_ranges = 0x55,
+ DW_AT_trampoline = 0x56,
+ DW_AT_call_column = 0x57,
+ DW_AT_call_file = 0x58,
+ DW_AT_call_line = 0x59,
+ DW_AT_description = 0x5a,
+ DW_AT_binary_scale = 0x5b,
+ DW_AT_decimal_scale = 0x5c,
+ DW_AT_small = 0x5d,
+ DW_AT_decimal_sign = 0x5e,
+ DW_AT_digit_count = 0x5f,
+ DW_AT_picture_string = 0x60,
+ DW_AT_mutable = 0x61,
+ DW_AT_threads_scaled = 0x62,
+ DW_AT_explicit = 0x63,
+ DW_AT_object_pointer = 0x64,
+ DW_AT_endianity = 0x65,
+ DW_AT_elemental = 0x66,
+ DW_AT_pure = 0x67,
+ DW_AT_recursive = 0x68,
+ DW_AT_signature = 0x69,
+ DW_AT_main_subprogram = 0x6a,
+ DW_AT_data_bit_offset = 0x6b,
+ DW_AT_const_expr = 0x6c,
+ DW_AT_enum_class = 0x6d,
+ DW_AT_linkage_name = 0x6e,
+ DW_AT_string_length_bit_size = 0x6f,
+ DW_AT_string_length_byte_size = 0x70,
+ DW_AT_rank = 0x71,
+ DW_AT_str_offsets_base = 0x72,
+ DW_AT_addr_base = 0x73,
+ DW_AT_rnglists_base = 0x74,
+ DW_AT_dwo_name = 0x76,
+ DW_AT_reference = 0x77,
+ DW_AT_rvalue_reference = 0x78,
+ DW_AT_macros = 0x79,
+ DW_AT_call_all_calls = 0x7a,
+ DW_AT_call_all_source_calls = 0x7b,
+ DW_AT_call_all_tail_calls = 0x7c,
+ DW_AT_call_return_pc = 0x7d,
+ DW_AT_call_value = 0x7e,
+ DW_AT_call_origin = 0x7f,
+ DW_AT_call_parameter = 0x80,
+ DW_AT_call_pc = 0x81,
+ DW_AT_call_tail_call = 0x82,
+ DW_AT_call_target = 0x83,
+ DW_AT_call_target_clobbered = 0x84,
+ DW_AT_call_data_location = 0x85,
+ DW_AT_call_data_value = 0x86,
+ DW_AT_noreturn = 0x87,
+ DW_AT_alignment = 0x88,
+ DW_AT_export_symbols = 0x89,
+ DW_AT_deleted = 0x8a,
+ DW_AT_defaulted = 0x8b,
+ DW_AT_loclists_base = 0x8c,
+ DW_AT_lo_user = 0x2000,
+ DW_AT_hi_user = 0x3fff,
+ DW_AT_MIPS_fde = 0x2001,
+ DW_AT_MIPS_loop_begin = 0x2002,
+ DW_AT_MIPS_tail_loop_begin = 0x2003,
+ DW_AT_MIPS_epilog_begin = 0x2004,
+ DW_AT_MIPS_loop_unroll_factor = 0x2005,
+ DW_AT_MIPS_software_pipeline_depth = 0x2006,
+ DW_AT_MIPS_linkage_name = 0x2007,
+ DW_AT_MIPS_stride = 0x2008,
+ DW_AT_MIPS_abstract_name = 0x2009,
+ DW_AT_MIPS_clone_origin = 0x200a,
+ DW_AT_MIPS_has_inlines = 0x200b,
+ DW_AT_HP_block_index = 0x2000,
+ DW_AT_HP_unmodifiable = 0x2001,
+ DW_AT_HP_prologue = 0x2005,
+ DW_AT_HP_epilogue = 0x2008,
+ DW_AT_HP_actuals_stmt_list = 0x2010,
+ DW_AT_HP_proc_per_section = 0x2011,
+ DW_AT_HP_raw_data_ptr = 0x2012,
+ DW_AT_HP_pass_by_reference = 0x2013,
+ DW_AT_HP_opt_level = 0x2014,
+ DW_AT_HP_prof_version_id = 0x2015,
+ DW_AT_HP_opt_flags = 0x2016,
+ DW_AT_HP_cold_region_low_pc = 0x2017,
+ DW_AT_HP_cold_region_high_pc = 0x2018,
+ DW_AT_HP_all_variables_modifiable = 0x2019,
+ DW_AT_HP_linkage_name = 0x201a,
+ DW_AT_HP_prof_flags = 0x201b,
+ DW_AT_HP_unit_name = 0x201f,
+ DW_AT_HP_unit_size = 0x2020,
+ DW_AT_HP_widened_byte_size = 0x2021,
+ DW_AT_HP_definition_points = 0x2022,
+ DW_AT_HP_default_location = 0x2023,
+ DW_AT_HP_is_result_param = 0x2029,
+ DW_AT_sf_names = 0x2101,
+ DW_AT_src_info = 0x2102,
+ DW_AT_mac_info = 0x2103,
+ DW_AT_src_coords = 0x2104,
+ DW_AT_body_begin = 0x2105,
+ DW_AT_body_end = 0x2106,
+ DW_AT_GNU_vector = 0x2107,
+ DW_AT_GNU_guarded_by = 0x2108,
+ DW_AT_GNU_pt_guarded_by = 0x2109,
+ DW_AT_GNU_guarded = 0x210a,
+ DW_AT_GNU_pt_guarded = 0x210b,
+ DW_AT_GNU_locks_excluded = 0x210c,
+ DW_AT_GNU_exclusive_locks_required = 0x210d,
+ DW_AT_GNU_shared_locks_required = 0x210e,
+ DW_AT_GNU_odr_signature = 0x210f,
+ DW_AT_GNU_template_name = 0x2110,
+ DW_AT_GNU_call_site_value = 0x2111,
+ DW_AT_GNU_call_site_data_value = 0x2112,
+ DW_AT_GNU_call_site_target = 0x2113,
+ DW_AT_GNU_call_site_target_clobbered = 0x2114,
+ DW_AT_GNU_tail_call = 0x2115,
+ DW_AT_GNU_all_tail_call_sites = 0x2116,
+ DW_AT_GNU_all_call_sites = 0x2117,
+ DW_AT_GNU_all_source_call_sites = 0x2118,
+ DW_AT_GNU_macros = 0x2119,
+ DW_AT_GNU_deleted = 0x211a,
+ DW_AT_GNU_dwo_name = 0x2130,
+ DW_AT_GNU_dwo_id = 0x2131,
+ DW_AT_GNU_ranges_base = 0x2132,
+ DW_AT_GNU_addr_base = 0x2133,
+ DW_AT_GNU_pubnames = 0x2134,
+ DW_AT_GNU_pubtypes = 0x2135,
+ DW_AT_GNU_discriminator = 0x2136,
+ DW_AT_GNU_locviews = 0x2137,
+ DW_AT_GNU_entry_view = 0x2138,
+ DW_AT_VMS_rtnbeg_pd_address = 0x2201,
+ DW_AT_use_GNAT_descriptive_type = 0x2301,
+ DW_AT_GNAT_descriptive_type = 0x2302,
+ DW_AT_GNU_numerator = 0x2303,
+ DW_AT_GNU_denominator = 0x2304,
+ DW_AT_GNU_bias = 0x2305,
+ DW_AT_upc_threads_scaled = 0x3210,
+ DW_AT_PGI_lbase = 0x3a00,
+ DW_AT_PGI_soffset = 0x3a01,
+ DW_AT_PGI_lstride = 0x3a02,
+ DW_AT_APPLE_optimized = 0x3fe1,
+ DW_AT_APPLE_flags = 0x3fe2,
+ DW_AT_APPLE_isa = 0x3fe3,
+ DW_AT_APPLE_block = 0x3fe4,
+ DW_AT_APPLE_major_runtime_vers = 0x3fe5,
+ DW_AT_APPLE_runtime_class = 0x3fe6,
+ DW_AT_APPLE_omit_frame_ptr = 0x3fe7,
+ DW_AT_APPLE_property_name = 0x3fe8,
+ DW_AT_APPLE_property_getter = 0x3fe9,
+ DW_AT_APPLE_property_setter = 0x3fea,
+ DW_AT_APPLE_property_attribute = 0x3feb,
+ DW_AT_APPLE_objc_complete_type = 0x3fec,
+ DW_AT_APPLE_property = 0x3fed
+};
+
+enum dwarf_line_number_op {
+ DW_LNS_extended_op = 0x0,
+ DW_LNS_copy = 0x1,
+ DW_LNS_advance_pc = 0x2,
+ DW_LNS_advance_line = 0x3,
+ DW_LNS_set_file = 0x4,
+ DW_LNS_set_column = 0x5,
+ DW_LNS_negate_stmt = 0x6,
+ DW_LNS_set_basic_block = 0x7,
+ DW_LNS_const_add_pc = 0x8,
+ DW_LNS_fixed_advance_pc = 0x9,
+ DW_LNS_set_prologue_end = 0xa,
+ DW_LNS_set_epilogue_begin = 0xb,
+ DW_LNS_set_isa = 0xc,
+};
+
+enum dwarf_extended_line_number_op {
+ DW_LNE_end_sequence = 0x1,
+ DW_LNE_set_address = 0x2,
+ DW_LNE_define_file = 0x3,
+ DW_LNE_set_discriminator = 0x4,
+};
+
+enum dwarf_line_number_content_type {
+ DW_LNCT_path = 0x1,
+ DW_LNCT_directory_index = 0x2,
+ DW_LNCT_timestamp = 0x3,
+ DW_LNCT_size = 0x4,
+ DW_LNCT_MD5 = 0x5,
+ DW_LNCT_lo_user = 0x2000,
+ DW_LNCT_hi_user = 0x3fff
+};
+
+enum dwarf_range_list_entry {
+ DW_RLE_end_of_list = 0x00,
+ DW_RLE_base_addressx = 0x01,
+ DW_RLE_startx_endx = 0x02,
+ DW_RLE_startx_length = 0x03,
+ DW_RLE_offset_pair = 0x04,
+ DW_RLE_base_address = 0x05,
+ DW_RLE_start_end = 0x06,
+ DW_RLE_start_length = 0x07
+};
+
+enum dwarf_unit_type {
+ DW_UT_compile = 0x01,
+ DW_UT_type = 0x02,
+ DW_UT_partial = 0x03,
+ DW_UT_skeleton = 0x04,
+ DW_UT_split_compile = 0x05,
+ DW_UT_split_type = 0x06,
+ DW_UT_lo_user = 0x80,
+ DW_UT_hi_user = 0xff
+};
+
+#if !defined(HAVE_DECL_STRNLEN) || !HAVE_DECL_STRNLEN
+
+/* If strnlen is not declared, provide our own version. */
+
+static size_t
+xstrnlen (const char *s, size_t maxlen)
+{
+ size_t i;
+
+ for (i = 0; i < maxlen; ++i)
+ if (s[i] == '\0')
+ break;
+ return i;
+}
+
+#define strnlen xstrnlen
+
+#endif
+
+/* A buffer to read DWARF info. */
+
+struct dwarf_buf
+{
+ /* Buffer name for error messages. */
+ const char *name;
+ /* Start of the buffer. */
+ const unsigned char *start;
+ /* Next byte to read. */
+ const unsigned char *buf;
+ /* The number of bytes remaining. */
+ size_t left;
+ /* Whether the data is big-endian. */
+ int is_bigendian;
+ /* Error callback routine. */
+ backtrace_error_callback error_callback;
+ /* Data for error_callback. */
+ void *data;
+ /* Non-zero if we've reported an underflow error. */
+ int reported_underflow;
+};
+
+/* A single attribute in a DWARF abbreviation. */
+
+struct attr
+{
+ /* The attribute name. */
+ enum dwarf_attribute name;
+ /* The attribute form. */
+ enum dwarf_form form;
+ /* The attribute value, for DW_FORM_implicit_const. */
+ int64_t val;
+};
+
+/* A single DWARF abbreviation. */
+
+struct abbrev
+{
+ /* The abbrev code--the number used to refer to the abbrev. */
+ uint64_t code;
+ /* The entry tag. */
+ enum dwarf_tag tag;
+ /* Non-zero if this abbrev has child entries. */
+ int has_children;
+ /* The number of attributes. */
+ size_t num_attrs;
+ /* The attributes. */
+ struct attr *attrs;
+};
+
+/* The DWARF abbreviations for a compilation unit. This structure
+ only exists while reading the compilation unit. Most DWARF readers
+ seem to a hash table to map abbrev ID's to abbrev entries.
+ However, we primarily care about GCC, and GCC simply issues ID's in
+ numerical order starting at 1. So we simply keep a sorted vector,
+ and try to just look up the code. */
+
+struct abbrevs
+{
+ /* The number of abbrevs in the vector. */
+ size_t num_abbrevs;
+ /* The abbrevs, sorted by the code field. */
+ struct abbrev *abbrevs;
+};
+
+/* The different kinds of attribute values. */
+
+enum attr_val_encoding
+{
+ /* No attribute value. */
+ ATTR_VAL_NONE,
+ /* An address. */
+ ATTR_VAL_ADDRESS,
+ /* An index into the .debug_addr section, whose value is relative to
+ * the DW_AT_addr_base attribute of the compilation unit. */
+ ATTR_VAL_ADDRESS_INDEX,
+ /* A unsigned integer. */
+ ATTR_VAL_UINT,
+ /* A sigd integer. */
+ ATTR_VAL_SINT,
+ /* A string. */
+ ATTR_VAL_STRING,
+ /* An index into the .debug_str_offsets section. */
+ ATTR_VAL_STRING_INDEX,
+ /* An offset to other data in the containing unit. */
+ ATTR_VAL_REF_UNIT,
+ /* An offset to other data within the .debug_info section. */
+ ATTR_VAL_REF_INFO,
+ /* An offset to other data within the alt .debug_info section. */
+ ATTR_VAL_REF_ALT_INFO,
+ /* An offset to data in some other section. */
+ ATTR_VAL_REF_SECTION,
+ /* A type signature. */
+ ATTR_VAL_REF_TYPE,
+ /* An index into the .debug_rnglists section. */
+ ATTR_VAL_RNGLISTS_INDEX,
+ /* A block of data (not represented). */
+ ATTR_VAL_BLOCK,
+ /* An expression (not represented). */
+ ATTR_VAL_EXPR,
+};
+
+/* An attribute value. */
+
+struct attr_val
+{
+ /* How the value is stored in the field u. */
+ enum attr_val_encoding encoding;
+ union
+ {
+ /* ATTR_VAL_ADDRESS*, ATTR_VAL_UINT, ATTR_VAL_REF*. */
+ uint64_t uint;
+ /* ATTR_VAL_SINT. */
+ int64_t sint;
+ /* ATTR_VAL_STRING. */
+ const char *string;
+ /* ATTR_VAL_BLOCK not stored. */
+ } u;
+};
+
+/* The line number program header. */
+
+struct line_header
+{
+ /* The version of the line number information. */
+ int version;
+ /* Address size. */
+ int addrsize;
+ /* The minimum instruction length. */
+ unsigned int min_insn_len;
+ /* The maximum number of ops per instruction. */
+ unsigned int max_ops_per_insn;
+ /* The line base for special opcodes. */
+ int line_base;
+ /* The line range for special opcodes. */
+ unsigned int line_range;
+ /* The opcode base--the first special opcode. */
+ unsigned int opcode_base;
+ /* Opcode lengths, indexed by opcode - 1. */
+ const unsigned char *opcode_lengths;
+ /* The number of directory entries. */
+ size_t dirs_count;
+ /* The directory entries. */
+ const char **dirs;
+ /* The number of filenames. */
+ size_t filenames_count;
+ /* The filenames. */
+ const char **filenames;
+};
+
+/* A format description from a line header. */
+
+struct line_header_format
+{
+ int lnct; /* LNCT code. */
+ enum dwarf_form form; /* Form of entry data. */
+};
+
+/* Map a single PC value to a file/line. We will keep a vector of
+ these sorted by PC value. Each file/line will be correct from the
+ PC up to the PC of the next entry if there is one. We allocate one
+ extra entry at the end so that we can use bsearch. */
+
+struct line
+{
+ /* PC. */
+ uintptr_t pc;
+ /* File name. Many entries in the array are expected to point to
+ the same file name. */
+ const char *filename;
+ /* Line number. */
+ int lineno;
+ /* Index of the object in the original array read from the DWARF
+ section, before it has been sorted. The index makes it possible
+ to use Quicksort and maintain stability. */
+ int idx;
+};
+
+/* A growable vector of line number information. This is used while
+ reading the line numbers. */
+
+struct line_vector
+{
+ /* Memory. This is an array of struct line. */
+ struct backtrace_vector vec;
+ /* Number of valid mappings. */
+ size_t count;
+};
+
+/* A function described in the debug info. */
+
+struct function
+{
+ /* The name of the function. */
+ const char *name;
+ /* If this is an inlined function, the filename of the call
+ site. */
+ const char *caller_filename;
+ /* If this is an inlined function, the line number of the call
+ site. */
+ int caller_lineno;
+ /* Map PC ranges to inlined functions. */
+ struct function_addrs *function_addrs;
+ size_t function_addrs_count;
+};
+
+/* An address range for a function. This maps a PC value to a
+ specific function. */
+
+struct function_addrs
+{
+ /* Range is LOW <= PC < HIGH. */
+ uint64_t low;
+ uint64_t high;
+ /* Function for this address range. */
+ struct function *function;
+};
+
+/* A growable vector of function address ranges. */
+
+struct function_vector
+{
+ /* Memory. This is an array of struct function_addrs. */
+ struct backtrace_vector vec;
+ /* Number of address ranges present. */
+ size_t count;
+};
+
+/* A DWARF compilation unit. This only holds the information we need
+ to map a PC to a file and line. */
+
+struct unit
+{
+ /* The first entry for this compilation unit. */
+ const unsigned char *unit_data;
+ /* The length of the data for this compilation unit. */
+ size_t unit_data_len;
+ /* The offset of UNIT_DATA from the start of the information for
+ this compilation unit. */
+ size_t unit_data_offset;
+ /* Offset of the start of the compilation unit from the start of the
+ .debug_info section. */
+ size_t low_offset;
+ /* Offset of the end of the compilation unit from the start of the
+ .debug_info section. */
+ size_t high_offset;
+ /* DWARF version. */
+ int version;
+ /* Whether unit is DWARF64. */
+ int is_dwarf64;
+ /* Address size. */
+ int addrsize;
+ /* Offset into line number information. */
+ off_t lineoff;
+ /* Offset of compilation unit in .debug_str_offsets. */
+ uint64_t str_offsets_base;
+ /* Offset of compilation unit in .debug_addr. */
+ uint64_t addr_base;
+ /* Offset of compilation unit in .debug_rnglists. */
+ uint64_t rnglists_base;
+ /* Primary source file. */
+ const char *filename;
+ /* Compilation command working directory. */
+ const char *comp_dir;
+ /* Absolute file name, only set if needed. */
+ const char *abs_filename;
+ /* The abbreviations for this unit. */
+ struct abbrevs abbrevs;
+
+ /* The fields above this point are read in during initialization and
+ may be accessed freely. The fields below this point are read in
+ as needed, and therefore require care, as different threads may
+ try to initialize them simultaneously. */
+
+ /* PC to line number mapping. This is NULL if the values have not
+ been read. This is (struct line *) -1 if there was an error
+ reading the values. */
+ struct line *lines;
+ /* Number of entries in lines. */
+ size_t lines_count;
+ /* PC ranges to function. */
+ struct function_addrs *function_addrs;
+ size_t function_addrs_count;
+};
+
+/* An address range for a compilation unit. This maps a PC value to a
+ specific compilation unit. Note that we invert the representation
+ in DWARF: instead of listing the units and attaching a list of
+ ranges, we list the ranges and have each one point to the unit.
+ This lets us do a binary search to find the unit. */
+
+struct unit_addrs
+{
+ /* Range is LOW <= PC < HIGH. */
+ uint64_t low;
+ uint64_t high;
+ /* Compilation unit for this address range. */
+ struct unit *u;
+};
+
+/* A growable vector of compilation unit address ranges. */
+
+struct unit_addrs_vector
+{
+ /* Memory. This is an array of struct unit_addrs. */
+ struct backtrace_vector vec;
+ /* Number of address ranges present. */
+ size_t count;
+};
+
+/* A growable vector of compilation unit pointer. */
+
+struct unit_vector
+{
+ struct backtrace_vector vec;
+ size_t count;
+};
+
+/* The information we need to map a PC to a file and line. */
+
+struct dwarf_data
+{
+ /* The data for the next file we know about. */
+ struct dwarf_data *next;
+ /* The data for .gnu_debugaltlink. */
+ struct dwarf_data *altlink;
+ /* The base address for this file. */
+ uintptr_t base_address;
+ /* A sorted list of address ranges. */
+ struct unit_addrs *addrs;
+ /* Number of address ranges in list. */
+ size_t addrs_count;
+ /* A sorted list of units. */
+ struct unit **units;
+ /* Number of units in the list. */
+ size_t units_count;
+ /* The unparsed DWARF debug data. */
+ struct dwarf_sections dwarf_sections;
+ /* Whether the data is big-endian or not. */
+ int is_bigendian;
+ /* A vector used for function addresses. We keep this here so that
+ we can grow the vector as we read more functions. */
+ struct function_vector fvec;
+};
+
+/* Report an error for a DWARF buffer. */
+
+static void
+dwarf_buf_error (struct dwarf_buf *buf, const char *msg, int errnum)
+{
+ char b[200];
+
+ snprintf (b, sizeof b, "%s in %s at %d",
+ msg, buf->name, (int) (buf->buf - buf->start));
+ buf->error_callback (buf->data, b, errnum);
+}
+
+/* Require at least COUNT bytes in BUF. Return 1 if all is well, 0 on
+ error. */
+
+static int
+require (struct dwarf_buf *buf, size_t count)
+{
+ if (buf->left >= count)
+ return 1;
+
+ if (!buf->reported_underflow)
+ {
+ dwarf_buf_error (buf, "DWARF underflow", 0);
+ buf->reported_underflow = 1;
+ }
+
+ return 0;
+}
+
+/* Advance COUNT bytes in BUF. Return 1 if all is well, 0 on
+ error. */
+
+static int
+advance (struct dwarf_buf *buf, size_t count)
+{
+ if (!require (buf, count))
+ return 0;
+ buf->buf += count;
+ buf->left -= count;
+ return 1;
+}
+
+/* Read one zero-terminated string from BUF and advance past the string. */
+
+static const char *
+read_string (struct dwarf_buf *buf)
+{
+ const char *p = (const char *)buf->buf;
+ size_t len = strnlen (p, buf->left);
+
+ /* - If len == left, we ran out of buffer before finding the zero terminator.
+ Generate an error by advancing len + 1.
+ - If len < left, advance by len + 1 to skip past the zero terminator. */
+ size_t count = len + 1;
+
+ if (!advance (buf, count))
+ return NULL;
+
+ return p;
+}
+
+/* Read one byte from BUF and advance 1 byte. */
+
+static unsigned char
+read_byte (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 1))
+ return 0;
+ return p[0];
+}
+
+/* Read a signed char from BUF and advance 1 byte. */
+
+static signed char
+read_sbyte (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 1))
+ return 0;
+ return (*p ^ 0x80) - 0x80;
+}
+
+/* Read a uint16 from BUF and advance 2 bytes. */
+
+static uint16_t
+read_uint16 (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 2))
+ return 0;
+ if (buf->is_bigendian)
+ return ((uint16_t) p[0] << 8) | (uint16_t) p[1];
+ else
+ return ((uint16_t) p[1] << 8) | (uint16_t) p[0];
+}
+
+/* Read a 24 bit value from BUF and advance 3 bytes. */
+
+static uint32_t
+read_uint24 (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 3))
+ return 0;
+ if (buf->is_bigendian)
+ return (((uint32_t) p[0] << 16) | ((uint32_t) p[1] << 8)
+ | (uint32_t) p[2]);
+ else
+ return (((uint32_t) p[2] << 16) | ((uint32_t) p[1] << 8)
+ | (uint32_t) p[0]);
+}
+
+/* Read a uint32 from BUF and advance 4 bytes. */
+
+static uint32_t
+read_uint32 (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 4))
+ return 0;
+ if (buf->is_bigendian)
+ return (((uint32_t) p[0] << 24) | ((uint32_t) p[1] << 16)
+ | ((uint32_t) p[2] << 8) | (uint32_t) p[3]);
+ else
+ return (((uint32_t) p[3] << 24) | ((uint32_t) p[2] << 16)
+ | ((uint32_t) p[1] << 8) | (uint32_t) p[0]);
+}
+
+/* Read a uint64 from BUF and advance 8 bytes. */
+
+static uint64_t
+read_uint64 (struct dwarf_buf *buf)
+{
+ const unsigned char *p = buf->buf;
+
+ if (!advance (buf, 8))
+ return 0;
+ if (buf->is_bigendian)
+ return (((uint64_t) p[0] << 56) | ((uint64_t) p[1] << 48)
+ | ((uint64_t) p[2] << 40) | ((uint64_t) p[3] << 32)
+ | ((uint64_t) p[4] << 24) | ((uint64_t) p[5] << 16)
+ | ((uint64_t) p[6] << 8) | (uint64_t) p[7]);
+ else
+ return (((uint64_t) p[7] << 56) | ((uint64_t) p[6] << 48)
+ | ((uint64_t) p[5] << 40) | ((uint64_t) p[4] << 32)
+ | ((uint64_t) p[3] << 24) | ((uint64_t) p[2] << 16)
+ | ((uint64_t) p[1] << 8) | (uint64_t) p[0]);
+}
+
+/* Read an offset from BUF and advance the appropriate number of
+ bytes. */
+
+static uint64_t
+read_offset (struct dwarf_buf *buf, int is_dwarf64)
+{
+ if (is_dwarf64)
+ return read_uint64 (buf);
+ else
+ return read_uint32 (buf);
+}
+
+/* Read an address from BUF and advance the appropriate number of
+ bytes. */
+
+static uint64_t
+read_address (struct dwarf_buf *buf, int addrsize)
+{
+ switch (addrsize)
+ {
+ case 1:
+ return read_byte (buf);
+ case 2:
+ return read_uint16 (buf);
+ case 4:
+ return read_uint32 (buf);
+ case 8:
+ return read_uint64 (buf);
+ default:
+ dwarf_buf_error (buf, "unrecognized address size", 0);
+ return 0;
+ }
+}
+
+/* Return whether a value is the highest possible address, given the
+ address size. */
+
+static int
+is_highest_address (uint64_t address, int addrsize)
+{
+ switch (addrsize)
+ {
+ case 1:
+ return address == (unsigned char) -1;
+ case 2:
+ return address == (uint16_t) -1;
+ case 4:
+ return address == (uint32_t) -1;
+ case 8:
+ return address == (uint64_t) -1;
+ default:
+ return 0;
+ }
+}
+
+/* Read an unsigned LEB128 number. */
+
+static uint64_t
+read_uleb128 (struct dwarf_buf *buf)
+{
+ uint64_t ret;
+ unsigned int shift;
+ int overflow;
+ unsigned char b;
+
+ ret = 0;
+ shift = 0;
+ overflow = 0;
+ do
+ {
+ const unsigned char *p;
+
+ p = buf->buf;
+ if (!advance (buf, 1))
+ return 0;
+ b = *p;
+ if (shift < 64)
+ ret |= ((uint64_t) (b & 0x7f)) << shift;
+ else if (!overflow)
+ {
+ dwarf_buf_error (buf, "LEB128 overflows uint64_t", 0);
+ overflow = 1;
+ }
+ shift += 7;
+ }
+ while ((b & 0x80) != 0);
+
+ return ret;
+}
+
+/* Read a signed LEB128 number. */
+
+static int64_t
+read_sleb128 (struct dwarf_buf *buf)
+{
+ uint64_t val;
+ unsigned int shift;
+ int overflow;
+ unsigned char b;
+
+ val = 0;
+ shift = 0;
+ overflow = 0;
+ do
+ {
+ const unsigned char *p;
+
+ p = buf->buf;
+ if (!advance (buf, 1))
+ return 0;
+ b = *p;
+ if (shift < 64)
+ val |= ((uint64_t) (b & 0x7f)) << shift;
+ else if (!overflow)
+ {
+ dwarf_buf_error (buf, "signed LEB128 overflows uint64_t", 0);
+ overflow = 1;
+ }
+ shift += 7;
+ }
+ while ((b & 0x80) != 0);
+
+ if ((b & 0x40) != 0 && shift < 64)
+ val |= ((uint64_t) -1) << shift;
+
+ return (int64_t) val;
+}
+
+/* Return the length of an LEB128 number. */
+
+static size_t
+leb128_len (const unsigned char *p)
+{
+ size_t ret;
+
+ ret = 1;
+ while ((*p & 0x80) != 0)
+ {
+ ++p;
+ ++ret;
+ }
+ return ret;
+}
+
+/* Read initial_length from BUF and advance the appropriate number of bytes. */
+
+static uint64_t
+read_initial_length (struct dwarf_buf *buf, int *is_dwarf64)
+{
+ uint64_t len;
+
+ len = read_uint32 (buf);
+ if (len == 0xffffffff)
+ {
+ len = read_uint64 (buf);
+ *is_dwarf64 = 1;
+ }
+ else
+ *is_dwarf64 = 0;
+
+ return len;
+}
+
+/* Free an abbreviations structure. */
+
+static void
+free_abbrevs (struct backtrace_state *state, struct abbrevs *abbrevs,
+ backtrace_error_callback error_callback, void *data)
+{
+ size_t i;
+
+ for (i = 0; i < abbrevs->num_abbrevs; ++i)
+ backtrace_free (state, abbrevs->abbrevs[i].attrs,
+ abbrevs->abbrevs[i].num_attrs * sizeof (struct attr),
+ error_callback, data);
+ backtrace_free (state, abbrevs->abbrevs,
+ abbrevs->num_abbrevs * sizeof (struct abbrev),
+ error_callback, data);
+ abbrevs->num_abbrevs = 0;
+ abbrevs->abbrevs = NULL;
+}
+
+/* Read an attribute value. Returns 1 on success, 0 on failure. If
+ the value can be represented as a uint64_t, sets *VAL and sets
+ *IS_VALID to 1. We don't try to store the value of other attribute
+ forms, because we don't care about them. */
+
+static int
+read_attribute (enum dwarf_form form, uint64_t implicit_val,
+ struct dwarf_buf *buf, int is_dwarf64, int version,
+ int addrsize, const struct dwarf_sections *dwarf_sections,
+ struct dwarf_data *altlink, struct attr_val *val)
+{
+ /* Avoid warnings about val.u.FIELD may be used uninitialized if
+ this function is inlined. The warnings aren't valid but can
+ occur because the different fields are set and used
+ conditionally. */
+ memset (val, 0, sizeof *val);
+
+ switch (form)
+ {
+ case DW_FORM_addr:
+ val->encoding = ATTR_VAL_ADDRESS;
+ val->u.uint = read_address (buf, addrsize);
+ return 1;
+ case DW_FORM_block2:
+ val->encoding = ATTR_VAL_BLOCK;
+ return advance (buf, read_uint16 (buf));
+ case DW_FORM_block4:
+ val->encoding = ATTR_VAL_BLOCK;
+ return advance (buf, read_uint32 (buf));
+ case DW_FORM_data2:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_uint16 (buf);
+ return 1;
+ case DW_FORM_data4:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_uint32 (buf);
+ return 1;
+ case DW_FORM_data8:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_uint64 (buf);
+ return 1;
+ case DW_FORM_data16:
+ val->encoding = ATTR_VAL_BLOCK;
+ return advance (buf, 16);
+ case DW_FORM_string:
+ val->encoding = ATTR_VAL_STRING;
+ val->u.string = read_string (buf);
+ return val->u.string == NULL ? 0 : 1;
+ case DW_FORM_block:
+ val->encoding = ATTR_VAL_BLOCK;
+ return advance (buf, read_uleb128 (buf));
+ case DW_FORM_block1:
+ val->encoding = ATTR_VAL_BLOCK;
+ return advance (buf, read_byte (buf));
+ case DW_FORM_data1:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_byte (buf);
+ return 1;
+ case DW_FORM_flag:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_byte (buf);
+ return 1;
+ case DW_FORM_sdata:
+ val->encoding = ATTR_VAL_SINT;
+ val->u.sint = read_sleb128 (buf);
+ return 1;
+ case DW_FORM_strp:
+ {
+ uint64_t offset;
+
+ offset = read_offset (buf, is_dwarf64);
+ if (offset >= dwarf_sections->size[DEBUG_STR])
+ {
+ dwarf_buf_error (buf, "DW_FORM_strp out of range", 0);
+ return 0;
+ }
+ val->encoding = ATTR_VAL_STRING;
+ val->u.string =
+ (const char *) dwarf_sections->data[DEBUG_STR] + offset;
+ return 1;
+ }
+ case DW_FORM_line_strp:
+ {
+ uint64_t offset;
+
+ offset = read_offset (buf, is_dwarf64);
+ if (offset >= dwarf_sections->size[DEBUG_LINE_STR])
+ {
+ dwarf_buf_error (buf, "DW_FORM_line_strp out of range", 0);
+ return 0;
+ }
+ val->encoding = ATTR_VAL_STRING;
+ val->u.string =
+ (const char *) dwarf_sections->data[DEBUG_LINE_STR] + offset;
+ return 1;
+ }
+ case DW_FORM_udata:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_ref_addr:
+ val->encoding = ATTR_VAL_REF_INFO;
+ if (version == 2)
+ val->u.uint = read_address (buf, addrsize);
+ else
+ val->u.uint = read_offset (buf, is_dwarf64);
+ return 1;
+ case DW_FORM_ref1:
+ val->encoding = ATTR_VAL_REF_UNIT;
+ val->u.uint = read_byte (buf);
+ return 1;
+ case DW_FORM_ref2:
+ val->encoding = ATTR_VAL_REF_UNIT;
+ val->u.uint = read_uint16 (buf);
+ return 1;
+ case DW_FORM_ref4:
+ val->encoding = ATTR_VAL_REF_UNIT;
+ val->u.uint = read_uint32 (buf);
+ return 1;
+ case DW_FORM_ref8:
+ val->encoding = ATTR_VAL_REF_UNIT;
+ val->u.uint = read_uint64 (buf);
+ return 1;
+ case DW_FORM_ref_udata:
+ val->encoding = ATTR_VAL_REF_UNIT;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_indirect:
+ {
+ uint64_t form;
+
+ form = read_uleb128 (buf);
+ if (form == DW_FORM_implicit_const)
+ {
+ dwarf_buf_error (buf,
+ "DW_FORM_indirect to DW_FORM_implicit_const",
+ 0);
+ return 0;
+ }
+ return read_attribute ((enum dwarf_form) form, 0, buf, is_dwarf64,
+ version, addrsize, dwarf_sections, altlink,
+ val);
+ }
+ case DW_FORM_sec_offset:
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_offset (buf, is_dwarf64);
+ return 1;
+ case DW_FORM_exprloc:
+ val->encoding = ATTR_VAL_EXPR;
+ return advance (buf, read_uleb128 (buf));
+ case DW_FORM_flag_present:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = 1;
+ return 1;
+ case DW_FORM_ref_sig8:
+ val->encoding = ATTR_VAL_REF_TYPE;
+ val->u.uint = read_uint64 (buf);
+ return 1;
+ case DW_FORM_strx: case DW_FORM_strx1: case DW_FORM_strx2:
+ case DW_FORM_strx3: case DW_FORM_strx4:
+ {
+ uint64_t offset;
+
+ switch (form)
+ {
+ case DW_FORM_strx:
+ offset = read_uleb128 (buf);
+ break;
+ case DW_FORM_strx1:
+ offset = read_byte (buf);
+ break;
+ case DW_FORM_strx2:
+ offset = read_uint16 (buf);
+ break;
+ case DW_FORM_strx3:
+ offset = read_uint24 (buf);
+ break;
+ case DW_FORM_strx4:
+ offset = read_uint32 (buf);
+ break;
+ default:
+ /* This case can't happen. */
+ return 0;
+ }
+ val->encoding = ATTR_VAL_STRING_INDEX;
+ val->u.uint = offset;
+ return 1;
+ }
+ case DW_FORM_addrx: case DW_FORM_addrx1: case DW_FORM_addrx2:
+ case DW_FORM_addrx3: case DW_FORM_addrx4:
+ {
+ uint64_t offset;
+
+ switch (form)
+ {
+ case DW_FORM_addrx:
+ offset = read_uleb128 (buf);
+ break;
+ case DW_FORM_addrx1:
+ offset = read_byte (buf);
+ break;
+ case DW_FORM_addrx2:
+ offset = read_uint16 (buf);
+ break;
+ case DW_FORM_addrx3:
+ offset = read_uint24 (buf);
+ break;
+ case DW_FORM_addrx4:
+ offset = read_uint32 (buf);
+ break;
+ default:
+ /* This case can't happen. */
+ return 0;
+ }
+ val->encoding = ATTR_VAL_ADDRESS_INDEX;
+ val->u.uint = offset;
+ return 1;
+ }
+ case DW_FORM_ref_sup4:
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_uint32 (buf);
+ return 1;
+ case DW_FORM_ref_sup8:
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_uint64 (buf);
+ return 1;
+ case DW_FORM_implicit_const:
+ val->encoding = ATTR_VAL_UINT;
+ val->u.uint = implicit_val;
+ return 1;
+ case DW_FORM_loclistx:
+ /* We don't distinguish this from DW_FORM_sec_offset. It
+ * shouldn't matter since we don't care about loclists. */
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_rnglistx:
+ val->encoding = ATTR_VAL_RNGLISTS_INDEX;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_GNU_addr_index:
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_GNU_str_index:
+ val->encoding = ATTR_VAL_REF_SECTION;
+ val->u.uint = read_uleb128 (buf);
+ return 1;
+ case DW_FORM_GNU_ref_alt:
+ val->u.uint = read_offset (buf, is_dwarf64);
+ if (altlink == NULL)
+ {
+ val->encoding = ATTR_VAL_NONE;
+ return 1;
+ }
+ val->encoding = ATTR_VAL_REF_ALT_INFO;
+ return 1;
+ case DW_FORM_strp_sup: case DW_FORM_GNU_strp_alt:
+ {
+ uint64_t offset;
+
+ offset = read_offset (buf, is_dwarf64);
+ if (altlink == NULL)
+ {
+ val->encoding = ATTR_VAL_NONE;
+ return 1;
+ }
+ if (offset >= altlink->dwarf_sections.size[DEBUG_STR])
+ {
+ dwarf_buf_error (buf, "DW_FORM_strp_sup out of range", 0);
+ return 0;
+ }
+ val->encoding = ATTR_VAL_STRING;
+ val->u.string =
+ (const char *) altlink->dwarf_sections.data[DEBUG_STR] + offset;
+ return 1;
+ }
+ default:
+ dwarf_buf_error (buf, "unrecognized DWARF form", -1);
+ return 0;
+ }
+}
+
+/* If we can determine the value of a string attribute, set *STRING to
+ point to the string. Return 1 on success, 0 on error. If we don't
+ know the value, we consider that a success, and we don't change
+ *STRING. An error is only reported for some sort of out of range
+ offset. */
+
+static int
+resolve_string (const struct dwarf_sections *dwarf_sections, int is_dwarf64,
+ int is_bigendian, uint64_t str_offsets_base,
+ const struct attr_val *val,
+ backtrace_error_callback error_callback, void *data,
+ const char **string)
+{
+ switch (val->encoding)
+ {
+ case ATTR_VAL_STRING:
+ *string = val->u.string;
+ return 1;
+
+ case ATTR_VAL_STRING_INDEX:
+ {
+ uint64_t offset;
+ struct dwarf_buf offset_buf;
+
+ offset = val->u.uint * (is_dwarf64 ? 8 : 4) + str_offsets_base;
+ if (offset + (is_dwarf64 ? 8 : 4)
+ > dwarf_sections->size[DEBUG_STR_OFFSETS])
+ {
+ error_callback (data, "DW_FORM_strx value out of range", 0);
+ return 0;
+ }
+
+ offset_buf.name = ".debug_str_offsets";
+ offset_buf.start = dwarf_sections->data[DEBUG_STR_OFFSETS];
+ offset_buf.buf = dwarf_sections->data[DEBUG_STR_OFFSETS] + offset;
+ offset_buf.left = dwarf_sections->size[DEBUG_STR_OFFSETS] - offset;
+ offset_buf.is_bigendian = is_bigendian;
+ offset_buf.error_callback = error_callback;
+ offset_buf.data = data;
+ offset_buf.reported_underflow = 0;
+
+ offset = read_offset (&offset_buf, is_dwarf64);
+ if (offset >= dwarf_sections->size[DEBUG_STR])
+ {
+ dwarf_buf_error (&offset_buf,
+ "DW_FORM_strx offset out of range",
+ 0);
+ return 0;
+ }
+ *string = (const char *) dwarf_sections->data[DEBUG_STR] + offset;
+ return 1;
+ }
+
+ default:
+ return 1;
+ }
+}
+
+/* Set *ADDRESS to the real address for a ATTR_VAL_ADDRESS_INDEX.
+ Return 1 on success, 0 on error. */
+
+static int
+resolve_addr_index (const struct dwarf_sections *dwarf_sections,
+ uint64_t addr_base, int addrsize, int is_bigendian,
+ uint64_t addr_index,
+ backtrace_error_callback error_callback, void *data,
+ uint64_t *address)
+{
+ uint64_t offset;
+ struct dwarf_buf addr_buf;
+
+ offset = addr_index * addrsize + addr_base;
+ if (offset + addrsize > dwarf_sections->size[DEBUG_ADDR])
+ {
+ error_callback (data, "DW_FORM_addrx value out of range", 0);
+ return 0;
+ }
+
+ addr_buf.name = ".debug_addr";
+ addr_buf.start = dwarf_sections->data[DEBUG_ADDR];
+ addr_buf.buf = dwarf_sections->data[DEBUG_ADDR] + offset;
+ addr_buf.left = dwarf_sections->size[DEBUG_ADDR] - offset;
+ addr_buf.is_bigendian = is_bigendian;
+ addr_buf.error_callback = error_callback;
+ addr_buf.data = data;
+ addr_buf.reported_underflow = 0;
+
+ *address = read_address (&addr_buf, addrsize);
+ return 1;
+}
+
+/* Compare a unit offset against a unit for bsearch. */
+
+static int
+units_search (const void *vkey, const void *ventry)
+{
+ const size_t *key = (const size_t *) vkey;
+ const struct unit *entry = *((const struct unit *const *) ventry);
+ size_t offset;
+
+ offset = *key;
+ if (offset < entry->low_offset)
+ return -1;
+ else if (offset >= entry->high_offset)
+ return 1;
+ else
+ return 0;
+}
+
+/* Find a unit in PU containing OFFSET. */
+
+static struct unit *
+find_unit (struct unit **pu, size_t units_count, size_t offset)
+{
+ struct unit **u;
+ u = bsearch (&offset, pu, units_count, sizeof (struct unit *), units_search);
+ return u == NULL ? NULL : *u;
+}
+
+/* Compare function_addrs for qsort. When ranges are nested, make the
+ smallest one sort last. */
+
+static int
+function_addrs_compare (const void *v1, const void *v2)
+{
+ const struct function_addrs *a1 = (const struct function_addrs *) v1;
+ const struct function_addrs *a2 = (const struct function_addrs *) v2;
+
+ if (a1->low < a2->low)
+ return -1;
+ if (a1->low > a2->low)
+ return 1;
+ if (a1->high < a2->high)
+ return 1;
+ if (a1->high > a2->high)
+ return -1;
+ return strcmp (a1->function->name, a2->function->name);
+}
+
+/* Compare a PC against a function_addrs for bsearch. We always
+ allocate an entra entry at the end of the vector, so that this
+ routine can safely look at the next entry. Note that if there are
+ multiple ranges containing PC, which one will be returned is
+ unpredictable. We compensate for that in dwarf_fileline. */
+
+static int
+function_addrs_search (const void *vkey, const void *ventry)
+{
+ const uintptr_t *key = (const uintptr_t *) vkey;
+ const struct function_addrs *entry = (const struct function_addrs *) ventry;
+ uintptr_t pc;
+
+ pc = *key;
+ if (pc < entry->low)
+ return -1;
+ else if (pc > (entry + 1)->low)
+ return 1;
+ else
+ return 0;
+}
+
+/* Add a new compilation unit address range to a vector. This is
+ called via add_ranges. Returns 1 on success, 0 on failure. */
+
+static int
+add_unit_addr (struct backtrace_state *state, void *rdata,
+ uint64_t lowpc, uint64_t highpc,
+ backtrace_error_callback error_callback, void *data,
+ void *pvec)
+{
+ struct unit *u = (struct unit *) rdata;
+ struct unit_addrs_vector *vec = (struct unit_addrs_vector *) pvec;
+ struct unit_addrs *p;
+
+ /* Try to merge with the last entry. */
+ if (vec->count > 0)
+ {
+ p = (struct unit_addrs *) vec->vec.base + (vec->count - 1);
+ if ((lowpc == p->high || lowpc == p->high + 1)
+ && u == p->u)
+ {
+ if (highpc > p->high)
+ p->high = highpc;
+ return 1;
+ }
+ }
+
+ p = ((struct unit_addrs *)
+ backtrace_vector_grow (state, sizeof (struct unit_addrs),
+ error_callback, data, &vec->vec));
+ if (p == NULL)
+ return 0;
+
+ p->low = lowpc;
+ p->high = highpc;
+ p->u = u;
+
+ ++vec->count;
+
+ return 1;
+}
+
+/* Compare unit_addrs for qsort. When ranges are nested, make the
+ smallest one sort last. */
+
+static int
+unit_addrs_compare (const void *v1, const void *v2)
+{
+ const struct unit_addrs *a1 = (const struct unit_addrs *) v1;
+ const struct unit_addrs *a2 = (const struct unit_addrs *) v2;
+
+ if (a1->low < a2->low)
+ return -1;
+ if (a1->low > a2->low)
+ return 1;
+ if (a1->high < a2->high)
+ return 1;
+ if (a1->high > a2->high)
+ return -1;
+ if (a1->u->lineoff < a2->u->lineoff)
+ return -1;
+ if (a1->u->lineoff > a2->u->lineoff)
+ return 1;
+ return 0;
+}
+
+/* Compare a PC against a unit_addrs for bsearch. We always allocate
+ an entry entry at the end of the vector, so that this routine can
+ safely look at the next entry. Note that if there are multiple
+ ranges containing PC, which one will be returned is unpredictable.
+ We compensate for that in dwarf_fileline. */
+
+static int
+unit_addrs_search (const void *vkey, const void *ventry)
+{
+ const uintptr_t *key = (const uintptr_t *) vkey;
+ const struct unit_addrs *entry = (const struct unit_addrs *) ventry;
+ uintptr_t pc;
+
+ pc = *key;
+ if (pc < entry->low)
+ return -1;
+ else if (pc > (entry + 1)->low)
+ return 1;
+ else
+ return 0;
+}
+
+/* Sort the line vector by PC. We want a stable sort here to maintain
+ the order of lines for the same PC values. Since the sequence is
+ being sorted in place, their addresses cannot be relied on to
+ maintain stability. That is the purpose of the index member. */
+
+static int
+line_compare (const void *v1, const void *v2)
+{
+ const struct line *ln1 = (const struct line *) v1;
+ const struct line *ln2 = (const struct line *) v2;
+
+ if (ln1->pc < ln2->pc)
+ return -1;
+ else if (ln1->pc > ln2->pc)
+ return 1;
+ else if (ln1->idx < ln2->idx)
+ return -1;
+ else if (ln1->idx > ln2->idx)
+ return 1;
+ else
+ return 0;
+}
+
+/* Find a PC in a line vector. We always allocate an extra entry at
+ the end of the lines vector, so that this routine can safely look
+ at the next entry. Note that when there are multiple mappings for
+ the same PC value, this will return the last one. */
+
+static int
+line_search (const void *vkey, const void *ventry)
+{
+ const uintptr_t *key = (const uintptr_t *) vkey;
+ const struct line *entry = (const struct line *) ventry;
+ uintptr_t pc;
+
+ pc = *key;
+ if (pc < entry->pc)
+ return -1;
+ else if (pc >= (entry + 1)->pc)
+ return 1;
+ else
+ return 0;
+}
+
+/* Sort the abbrevs by the abbrev code. This function is passed to
+ both qsort and bsearch. */
+
+static int
+abbrev_compare (const void *v1, const void *v2)
+{
+ const struct abbrev *a1 = (const struct abbrev *) v1;
+ const struct abbrev *a2 = (const struct abbrev *) v2;
+
+ if (a1->code < a2->code)
+ return -1;
+ else if (a1->code > a2->code)
+ return 1;
+ else
+ {
+ /* This really shouldn't happen. It means there are two
+ different abbrevs with the same code, and that means we don't
+ know which one lookup_abbrev should return. */
+ return 0;
+ }
+}
+
+/* Read the abbreviation table for a compilation unit. Returns 1 on
+ success, 0 on failure. */
+
+static int
+read_abbrevs (struct backtrace_state *state, uint64_t abbrev_offset,
+ const unsigned char *dwarf_abbrev, size_t dwarf_abbrev_size,
+ int is_bigendian, backtrace_error_callback error_callback,
+ void *data, struct abbrevs *abbrevs)
+{
+ struct dwarf_buf abbrev_buf;
+ struct dwarf_buf count_buf;
+ size_t num_abbrevs;
+
+ abbrevs->num_abbrevs = 0;
+ abbrevs->abbrevs = NULL;
+
+ if (abbrev_offset >= dwarf_abbrev_size)
+ {
+ error_callback (data, "abbrev offset out of range", 0);
+ return 0;
+ }
+
+ abbrev_buf.name = ".debug_abbrev";
+ abbrev_buf.start = dwarf_abbrev;
+ abbrev_buf.buf = dwarf_abbrev + abbrev_offset;
+ abbrev_buf.left = dwarf_abbrev_size - abbrev_offset;
+ abbrev_buf.is_bigendian = is_bigendian;
+ abbrev_buf.error_callback = error_callback;
+ abbrev_buf.data = data;
+ abbrev_buf.reported_underflow = 0;
+
+ /* Count the number of abbrevs in this list. */
+
+ count_buf = abbrev_buf;
+ num_abbrevs = 0;
+ while (read_uleb128 (&count_buf) != 0)
+ {
+ if (count_buf.reported_underflow)
+ return 0;
+ ++num_abbrevs;
+ // Skip tag.
+ read_uleb128 (&count_buf);
+ // Skip has_children.
+ read_byte (&count_buf);
+ // Skip attributes.
+ while (read_uleb128 (&count_buf) != 0)
+ {
+ uint64_t form;
+
+ form = read_uleb128 (&count_buf);
+ if ((enum dwarf_form) form == DW_FORM_implicit_const)
+ read_sleb128 (&count_buf);
+ }
+ // Skip form of last attribute.
+ read_uleb128 (&count_buf);
+ }
+
+ if (count_buf.reported_underflow)
+ return 0;
+
+ if (num_abbrevs == 0)
+ return 1;
+
+ abbrevs->abbrevs = ((struct abbrev *)
+ backtrace_alloc (state,
+ num_abbrevs * sizeof (struct abbrev),
+ error_callback, data));
+ if (abbrevs->abbrevs == NULL)
+ return 0;
+ abbrevs->num_abbrevs = num_abbrevs;
+ memset (abbrevs->abbrevs, 0, num_abbrevs * sizeof (struct abbrev));
+
+ num_abbrevs = 0;
+ while (1)
+ {
+ uint64_t code;
+ struct abbrev a;
+ size_t num_attrs;
+ struct attr *attrs;
+
+ if (abbrev_buf.reported_underflow)
+ goto fail;
+
+ code = read_uleb128 (&abbrev_buf);
+ if (code == 0)
+ break;
+
+ a.code = code;
+ a.tag = (enum dwarf_tag) read_uleb128 (&abbrev_buf);
+ a.has_children = read_byte (&abbrev_buf);
+
+ count_buf = abbrev_buf;
+ num_attrs = 0;
+ while (read_uleb128 (&count_buf) != 0)
+ {
+ uint64_t form;
+
+ ++num_attrs;
+ form = read_uleb128 (&count_buf);
+ if ((enum dwarf_form) form == DW_FORM_implicit_const)
+ read_sleb128 (&count_buf);
+ }
+
+ if (num_attrs == 0)
+ {
+ attrs = NULL;
+ read_uleb128 (&abbrev_buf);
+ read_uleb128 (&abbrev_buf);
+ }
+ else
+ {
+ attrs = ((struct attr *)
+ backtrace_alloc (state, num_attrs * sizeof *attrs,
+ error_callback, data));
+ if (attrs == NULL)
+ goto fail;
+ num_attrs = 0;
+ while (1)
+ {
+ uint64_t name;
+ uint64_t form;
+
+ name = read_uleb128 (&abbrev_buf);
+ form = read_uleb128 (&abbrev_buf);
+ if (name == 0)
+ break;
+ attrs[num_attrs].name = (enum dwarf_attribute) name;
+ attrs[num_attrs].form = (enum dwarf_form) form;
+ if ((enum dwarf_form) form == DW_FORM_implicit_const)
+ attrs[num_attrs].val = read_sleb128 (&abbrev_buf);
+ else
+ attrs[num_attrs].val = 0;
+ ++num_attrs;
+ }
+ }
+
+ a.num_attrs = num_attrs;
+ a.attrs = attrs;
+
+ abbrevs->abbrevs[num_abbrevs] = a;
+ ++num_abbrevs;
+ }
+
+ backtrace_qsort (abbrevs->abbrevs, abbrevs->num_abbrevs,
+ sizeof (struct abbrev), abbrev_compare);
+
+ return 1;
+
+ fail:
+ free_abbrevs (state, abbrevs, error_callback, data);
+ return 0;
+}
+
+/* Return the abbrev information for an abbrev code. */
+
+static const struct abbrev *
+lookup_abbrev (struct abbrevs *abbrevs, uint64_t code,
+ backtrace_error_callback error_callback, void *data)
+{
+ struct abbrev key;
+ void *p;
+
+ /* With GCC, where abbrevs are simply numbered in order, we should
+ be able to just look up the entry. */
+ if (code - 1 < abbrevs->num_abbrevs
+ && abbrevs->abbrevs[code - 1].code == code)
+ return &abbrevs->abbrevs[code - 1];
+
+ /* Otherwise we have to search. */
+ memset (&key, 0, sizeof key);
+ key.code = code;
+ p = bsearch (&key, abbrevs->abbrevs, abbrevs->num_abbrevs,
+ sizeof (struct abbrev), abbrev_compare);
+ if (p == NULL)
+ {
+ error_callback (data, "invalid abbreviation code", 0);
+ return NULL;
+ }
+ return (const struct abbrev *) p;
+}
+
+/* This struct is used to gather address range information while
+ reading attributes. We use this while building a mapping from
+ address ranges to compilation units and then again while mapping
+ from address ranges to function entries. Normally either
+ lowpc/highpc is set or ranges is set. */
+
+struct pcrange {
+ uint64_t lowpc; /* The low PC value. */
+ int have_lowpc; /* Whether a low PC value was found. */
+ int lowpc_is_addr_index; /* Whether lowpc is in .debug_addr. */
+ uint64_t highpc; /* The high PC value. */
+ int have_highpc; /* Whether a high PC value was found. */
+ int highpc_is_relative; /* Whether highpc is relative to lowpc. */
+ int highpc_is_addr_index; /* Whether highpc is in .debug_addr. */
+ uint64_t ranges; /* Offset in ranges section. */
+ int have_ranges; /* Whether ranges is valid. */
+ int ranges_is_index; /* Whether ranges is DW_FORM_rnglistx. */
+};
+
+/* Update PCRANGE from an attribute value. */
+
+static void
+update_pcrange (const struct attr* attr, const struct attr_val* val,
+ struct pcrange *pcrange)
+{
+ switch (attr->name)
+ {
+ case DW_AT_low_pc:
+ if (val->encoding == ATTR_VAL_ADDRESS)
+ {
+ pcrange->lowpc = val->u.uint;
+ pcrange->have_lowpc = 1;
+ }
+ else if (val->encoding == ATTR_VAL_ADDRESS_INDEX)
+ {
+ pcrange->lowpc = val->u.uint;
+ pcrange->have_lowpc = 1;
+ pcrange->lowpc_is_addr_index = 1;
+ }
+ break;
+
+ case DW_AT_high_pc:
+ if (val->encoding == ATTR_VAL_ADDRESS)
+ {
+ pcrange->highpc = val->u.uint;
+ pcrange->have_highpc = 1;
+ }
+ else if (val->encoding == ATTR_VAL_UINT)
+ {
+ pcrange->highpc = val->u.uint;
+ pcrange->have_highpc = 1;
+ pcrange->highpc_is_relative = 1;
+ }
+ else if (val->encoding == ATTR_VAL_ADDRESS_INDEX)
+ {
+ pcrange->highpc = val->u.uint;
+ pcrange->have_highpc = 1;
+ pcrange->highpc_is_addr_index = 1;
+ }
+ break;
+
+ case DW_AT_ranges:
+ if (val->encoding == ATTR_VAL_UINT
+ || val->encoding == ATTR_VAL_REF_SECTION)
+ {
+ pcrange->ranges = val->u.uint;
+ pcrange->have_ranges = 1;
+ }
+ else if (val->encoding == ATTR_VAL_RNGLISTS_INDEX)
+ {
+ pcrange->ranges = val->u.uint;
+ pcrange->have_ranges = 1;
+ pcrange->ranges_is_index = 1;
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+/* Call ADD_RANGE for a low/high PC pair. Returns 1 on success, 0 on
+ error. */
+
+static int
+add_low_high_range (struct backtrace_state *state,
+ const struct dwarf_sections *dwarf_sections,
+ uintptr_t base_address, int is_bigendian,
+ struct unit *u, const struct pcrange *pcrange,
+ int (*add_range) (struct backtrace_state *state,
+ void *rdata, uint64_t lowpc,
+ uint64_t highpc,
+ backtrace_error_callback error_callback,
+ void *data, void *vec),
+ void *rdata,
+ backtrace_error_callback error_callback, void *data,
+ void *vec)
+{
+ uint64_t lowpc;
+ uint64_t highpc;
+
+ lowpc = pcrange->lowpc;
+ if (pcrange->lowpc_is_addr_index)
+ {
+ if (!resolve_addr_index (dwarf_sections, u->addr_base, u->addrsize,
+ is_bigendian, lowpc, error_callback, data,
+ &lowpc))
+ return 0;
+ }
+
+ highpc = pcrange->highpc;
+ if (pcrange->highpc_is_addr_index)
+ {
+ if (!resolve_addr_index (dwarf_sections, u->addr_base, u->addrsize,
+ is_bigendian, highpc, error_callback, data,
+ &highpc))
+ return 0;
+ }
+ if (pcrange->highpc_is_relative)
+ highpc += lowpc;
+
+ /* Add in the base address of the module when recording PC values,
+ so that we can look up the PC directly. */
+ lowpc += base_address;
+ highpc += base_address;
+
+ return add_range (state, rdata, lowpc, highpc, error_callback, data, vec);
+}
+
+/* Call ADD_RANGE for each range read from .debug_ranges, as used in
+ DWARF versions 2 through 4. */
+
+static int
+add_ranges_from_ranges (
+ struct backtrace_state *state,
+ const struct dwarf_sections *dwarf_sections,
+ uintptr_t base_address, int is_bigendian,
+ struct unit *u, uint64_t base,
+ const struct pcrange *pcrange,
+ int (*add_range) (struct backtrace_state *state, void *rdata,
+ uint64_t lowpc, uint64_t highpc,
+ backtrace_error_callback error_callback, void *data,
+ void *vec),
+ void *rdata,
+ backtrace_error_callback error_callback, void *data,
+ void *vec)
+{
+ struct dwarf_buf ranges_buf;
+
+ if (pcrange->ranges >= dwarf_sections->size[DEBUG_RANGES])
+ {
+ error_callback (data, "ranges offset out of range", 0);
+ return 0;
+ }
+
+ ranges_buf.name = ".debug_ranges";
+ ranges_buf.start = dwarf_sections->data[DEBUG_RANGES];
+ ranges_buf.buf = dwarf_sections->data[DEBUG_RANGES] + pcrange->ranges;
+ ranges_buf.left = dwarf_sections->size[DEBUG_RANGES] - pcrange->ranges;
+ ranges_buf.is_bigendian = is_bigendian;
+ ranges_buf.error_callback = error_callback;
+ ranges_buf.data = data;
+ ranges_buf.reported_underflow = 0;
+
+ while (1)
+ {
+ uint64_t low;
+ uint64_t high;
+
+ if (ranges_buf.reported_underflow)
+ return 0;
+
+ low = read_address (&ranges_buf, u->addrsize);
+ high = read_address (&ranges_buf, u->addrsize);
+
+ if (low == 0 && high == 0)
+ break;
+
+ if (is_highest_address (low, u->addrsize))
+ base = high;
+ else
+ {
+ if (!add_range (state, rdata,
+ low + base + base_address,
+ high + base + base_address,
+ error_callback, data, vec))
+ return 0;
+ }
+ }
+
+ if (ranges_buf.reported_underflow)
+ return 0;
+
+ return 1;
+}
+
+/* Call ADD_RANGE for each range read from .debug_rnglists, as used in
+ DWARF version 5. */
+
+static int
+add_ranges_from_rnglists (
+ struct backtrace_state *state,
+ const struct dwarf_sections *dwarf_sections,
+ uintptr_t base_address, int is_bigendian,
+ struct unit *u, uint64_t base,
+ const struct pcrange *pcrange,
+ int (*add_range) (struct backtrace_state *state, void *rdata,
+ uint64_t lowpc, uint64_t highpc,
+ backtrace_error_callback error_callback, void *data,
+ void *vec),
+ void *rdata,
+ backtrace_error_callback error_callback, void *data,
+ void *vec)
+{
+ uint64_t offset;
+ struct dwarf_buf rnglists_buf;
+
+ if (!pcrange->ranges_is_index)
+ offset = pcrange->ranges;
+ else
+ offset = u->rnglists_base + pcrange->ranges * (u->is_dwarf64 ? 8 : 4);
+ if (offset >= dwarf_sections->size[DEBUG_RNGLISTS])
+ {
+ error_callback (data, "rnglists offset out of range", 0);
+ return 0;
+ }
+
+ rnglists_buf.name = ".debug_rnglists";
+ rnglists_buf.start = dwarf_sections->data[DEBUG_RNGLISTS];
+ rnglists_buf.buf = dwarf_sections->data[DEBUG_RNGLISTS] + offset;
+ rnglists_buf.left = dwarf_sections->size[DEBUG_RNGLISTS] - offset;
+ rnglists_buf.is_bigendian = is_bigendian;
+ rnglists_buf.error_callback = error_callback;
+ rnglists_buf.data = data;
+ rnglists_buf.reported_underflow = 0;
+
+ if (pcrange->ranges_is_index)
+ {
+ offset = read_offset (&rnglists_buf, u->is_dwarf64);
+ offset += u->rnglists_base;
+ if (offset >= dwarf_sections->size[DEBUG_RNGLISTS])
+ {
+ error_callback (data, "rnglists index offset out of range", 0);
+ return 0;
+ }
+ rnglists_buf.buf = dwarf_sections->data[DEBUG_RNGLISTS] + offset;
+ rnglists_buf.left = dwarf_sections->size[DEBUG_RNGLISTS] - offset;
+ }
+
+ while (1)
+ {
+ unsigned char rle;
+
+ rle = read_byte (&rnglists_buf);
+ if (rle == DW_RLE_end_of_list)
+ break;
+ switch (rle)
+ {
+ case DW_RLE_base_addressx:
+ {
+ uint64_t index;
+
+ index = read_uleb128 (&rnglists_buf);
+ if (!resolve_addr_index (dwarf_sections, u->addr_base,
+ u->addrsize, is_bigendian, index,
+ error_callback, data, &base))
+ return 0;
+ }
+ break;
+
+ case DW_RLE_startx_endx:
+ {
+ uint64_t index;
+ uint64_t low;
+ uint64_t high;
+
+ index = read_uleb128 (&rnglists_buf);
+ if (!resolve_addr_index (dwarf_sections, u->addr_base,
+ u->addrsize, is_bigendian, index,
+ error_callback, data, &low))
+ return 0;
+ index = read_uleb128 (&rnglists_buf);
+ if (!resolve_addr_index (dwarf_sections, u->addr_base,
+ u->addrsize, is_bigendian, index,
+ error_callback, data, &high))
+ return 0;
+ if (!add_range (state, rdata, low + base_address,
+ high + base_address, error_callback, data,
+ vec))
+ return 0;
+ }
+ break;
+
+ case DW_RLE_startx_length:
+ {
+ uint64_t index;
+ uint64_t low;
+ uint64_t length;
+
+ index = read_uleb128 (&rnglists_buf);
+ if (!resolve_addr_index (dwarf_sections, u->addr_base,
+ u->addrsize, is_bigendian, index,
+ error_callback, data, &low))
+ return 0;
+ length = read_uleb128 (&rnglists_buf);
+ low += base_address;
+ if (!add_range (state, rdata, low, low + length,
+ error_callback, data, vec))
+ return 0;
+ }
+ break;
+
+ case DW_RLE_offset_pair:
+ {
+ uint64_t low;
+ uint64_t high;
+
+ low = read_uleb128 (&rnglists_buf);
+ high = read_uleb128 (&rnglists_buf);
+ if (!add_range (state, rdata, low + base + base_address,
+ high + base + base_address,
+ error_callback, data, vec))
+ return 0;
+ }
+ break;
+
+ case DW_RLE_base_address:
+ base = read_address (&rnglists_buf, u->addrsize);
+ break;
+
+ case DW_RLE_start_end:
+ {
+ uint64_t low;
+ uint64_t high;
+
+ low = read_address (&rnglists_buf, u->addrsize);
+ high = read_address (&rnglists_buf, u->addrsize);
+ if (!add_range (state, rdata, low + base_address,
+ high + base_address, error_callback, data,
+ vec))
+ return 0;
+ }
+ break;
+
+ case DW_RLE_start_length:
+ {
+ uint64_t low;
+ uint64_t length;
+
+ low = read_address (&rnglists_buf, u->addrsize);
+ length = read_uleb128 (&rnglists_buf);
+ low += base_address;
+ if (!add_range (state, rdata, low, low + length,
+ error_callback, data, vec))
+ return 0;
+ }
+ break;
+
+ default:
+ dwarf_buf_error (&rnglists_buf, "unrecognized DW_RLE value", -1);
+ return 0;
+ }
+ }
+
+ if (rnglists_buf.reported_underflow)
+ return 0;
+
+ return 1;
+}
+
+/* Call ADD_RANGE for each lowpc/highpc pair in PCRANGE. RDATA is
+ passed to ADD_RANGE, and is either a struct unit * or a struct
+ function *. VEC is the vector we are adding ranges to, and is
+ either a struct unit_addrs_vector * or a struct function_vector *.
+ Returns 1 on success, 0 on error. */
+
+static int
+add_ranges (struct backtrace_state *state,
+ const struct dwarf_sections *dwarf_sections,
+ uintptr_t base_address, int is_bigendian,
+ struct unit *u, uint64_t base, const struct pcrange *pcrange,
+ int (*add_range) (struct backtrace_state *state, void *rdata,
+ uint64_t lowpc, uint64_t highpc,
+ backtrace_error_callback error_callback,
+ void *data, void *vec),
+ void *rdata,
+ backtrace_error_callback error_callback, void *data,
+ void *vec)
+{
+ if (pcrange->have_lowpc && pcrange->have_highpc)
+ return add_low_high_range (state, dwarf_sections, base_address,
+ is_bigendian, u, pcrange, add_range, rdata,
+ error_callback, data, vec);
+
+ if (!pcrange->have_ranges)
+ {
+ /* Did not find any address ranges to add. */
+ return 1;
+ }
+
+ if (u->version < 5)
+ return add_ranges_from_ranges (state, dwarf_sections, base_address,
+ is_bigendian, u, base, pcrange, add_range,
+ rdata, error_callback, data, vec);
+ else
+ return add_ranges_from_rnglists (state, dwarf_sections, base_address,
+ is_bigendian, u, base, pcrange, add_range,
+ rdata, error_callback, data, vec);
+}
+
+/* Find the address range covered by a compilation unit, reading from
+ UNIT_BUF and adding values to U. Returns 1 if all data could be
+ read, 0 if there is some error. */
+
+static int
+find_address_ranges (struct backtrace_state *state, uintptr_t base_address,
+ struct dwarf_buf *unit_buf,
+ const struct dwarf_sections *dwarf_sections,
+ int is_bigendian, struct dwarf_data *altlink,
+ backtrace_error_callback error_callback, void *data,
+ struct unit *u, struct unit_addrs_vector *addrs,
+ enum dwarf_tag *unit_tag)
+{
+ while (unit_buf->left > 0)
+ {
+ uint64_t code;
+ const struct abbrev *abbrev;
+ struct pcrange pcrange;
+ struct attr_val name_val;
+ int have_name_val;
+ struct attr_val comp_dir_val;
+ int have_comp_dir_val;
+ size_t i;
+
+ code = read_uleb128 (unit_buf);
+ if (code == 0)
+ return 1;
+
+ abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data);
+ if (abbrev == NULL)
+ return 0;
+
+ if (unit_tag != NULL)
+ *unit_tag = abbrev->tag;
+
+ memset (&pcrange, 0, sizeof pcrange);
+ memset (&name_val, 0, sizeof name_val);
+ have_name_val = 0;
+ memset (&comp_dir_val, 0, sizeof comp_dir_val);
+ have_comp_dir_val = 0;
+ for (i = 0; i < abbrev->num_attrs; ++i)
+ {
+ struct attr_val val;
+
+ if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val,
+ unit_buf, u->is_dwarf64, u->version,
+ u->addrsize, dwarf_sections, altlink, &val))
+ return 0;
+
+ switch (abbrev->attrs[i].name)
+ {
+ case DW_AT_low_pc: case DW_AT_high_pc: case DW_AT_ranges:
+ update_pcrange (&abbrev->attrs[i], &val, &pcrange);
+ break;
+
+ case DW_AT_stmt_list:
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && (val.encoding == ATTR_VAL_UINT
+ || val.encoding == ATTR_VAL_REF_SECTION))
+ u->lineoff = val.u.uint;
+ break;
+
+ case DW_AT_name:
+ if (abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ {
+ name_val = val;
+ have_name_val = 1;
+ }
+ break;
+
+ case DW_AT_comp_dir:
+ if (abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ {
+ comp_dir_val = val;
+ have_comp_dir_val = 1;
+ }
+ break;
+
+ case DW_AT_str_offsets_base:
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && val.encoding == ATTR_VAL_REF_SECTION)
+ u->str_offsets_base = val.u.uint;
+ break;
+
+ case DW_AT_addr_base:
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && val.encoding == ATTR_VAL_REF_SECTION)
+ u->addr_base = val.u.uint;
+ break;
+
+ case DW_AT_rnglists_base:
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && val.encoding == ATTR_VAL_REF_SECTION)
+ u->rnglists_base = val.u.uint;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ // Resolve strings after we're sure that we have seen
+ // DW_AT_str_offsets_base.
+ if (have_name_val)
+ {
+ if (!resolve_string (dwarf_sections, u->is_dwarf64, is_bigendian,
+ u->str_offsets_base, &name_val,
+ error_callback, data, &u->filename))
+ return 0;
+ }
+ if (have_comp_dir_val)
+ {
+ if (!resolve_string (dwarf_sections, u->is_dwarf64, is_bigendian,
+ u->str_offsets_base, &comp_dir_val,
+ error_callback, data, &u->comp_dir))
+ return 0;
+ }
+
+ if (abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_subprogram
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ {
+ if (!add_ranges (state, dwarf_sections, base_address,
+ is_bigendian, u, pcrange.lowpc, &pcrange,
+ add_unit_addr, (void *) u, error_callback, data,
+ (void *) addrs))
+ return 0;
+
+ /* If we found the PC range in the DW_TAG_compile_unit or
+ DW_TAG_skeleton_unit, we can stop now. */
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && (pcrange.have_ranges
+ || (pcrange.have_lowpc && pcrange.have_highpc)))
+ return 1;
+ }
+
+ if (abbrev->has_children)
+ {
+ if (!find_address_ranges (state, base_address, unit_buf,
+ dwarf_sections, is_bigendian, altlink,
+ error_callback, data, u, addrs, NULL))
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+/* Build a mapping from address ranges to the compilation units where
+ the line number information for that range can be found. Returns 1
+ on success, 0 on failure. */
+
+static int
+build_address_map (struct backtrace_state *state, uintptr_t base_address,
+ const struct dwarf_sections *dwarf_sections,
+ int is_bigendian, struct dwarf_data *altlink,
+ backtrace_error_callback error_callback, void *data,
+ struct unit_addrs_vector *addrs,
+ struct unit_vector *unit_vec)
+{
+ struct dwarf_buf info;
+ struct backtrace_vector units;
+ size_t units_count;
+ size_t i;
+ struct unit **pu;
+ size_t unit_offset = 0;
+ struct unit_addrs *pa;
+
+ memset (&addrs->vec, 0, sizeof addrs->vec);
+ memset (&unit_vec->vec, 0, sizeof unit_vec->vec);
+ addrs->count = 0;
+ unit_vec->count = 0;
+
+ /* Read through the .debug_info section. FIXME: Should we use the
+ .debug_aranges section? gdb and addr2line don't use it, but I'm
+ not sure why. */
+
+ info.name = ".debug_info";
+ info.start = dwarf_sections->data[DEBUG_INFO];
+ info.buf = info.start;
+ info.left = dwarf_sections->size[DEBUG_INFO];
+ info.is_bigendian = is_bigendian;
+ info.error_callback = error_callback;
+ info.data = data;
+ info.reported_underflow = 0;
+
+ memset (&units, 0, sizeof units);
+ units_count = 0;
+
+ while (info.left > 0)
+ {
+ const unsigned char *unit_data_start;
+ uint64_t len;
+ int is_dwarf64;
+ struct dwarf_buf unit_buf;
+ int version;
+ int unit_type;
+ uint64_t abbrev_offset;
+ int addrsize;
+ struct unit *u;
+ enum dwarf_tag unit_tag;
+
+ if (info.reported_underflow)
+ goto fail;
+
+ unit_data_start = info.buf;
+
+ len = read_initial_length (&info, &is_dwarf64);
+ unit_buf = info;
+ unit_buf.left = len;
+
+ if (!advance (&info, len))
+ goto fail;
+
+ version = read_uint16 (&unit_buf);
+ if (version < 2 || version > 5)
+ {
+ dwarf_buf_error (&unit_buf, "unrecognized DWARF version", -1);
+ goto fail;
+ }
+
+ if (version < 5)
+ unit_type = 0;
+ else
+ {
+ unit_type = read_byte (&unit_buf);
+ if (unit_type == DW_UT_type || unit_type == DW_UT_split_type)
+ {
+ /* This unit doesn't have anything we need. */
+ continue;
+ }
+ }
+
+ pu = ((struct unit **)
+ backtrace_vector_grow (state, sizeof (struct unit *),
+ error_callback, data, &units));
+ if (pu == NULL)
+ goto fail;
+
+ u = ((struct unit *)
+ backtrace_alloc (state, sizeof *u, error_callback, data));
+ if (u == NULL)
+ goto fail;
+
+ *pu = u;
+ ++units_count;
+
+ if (version < 5)
+ addrsize = 0; /* Set below. */
+ else
+ addrsize = read_byte (&unit_buf);
+
+ memset (&u->abbrevs, 0, sizeof u->abbrevs);
+ abbrev_offset = read_offset (&unit_buf, is_dwarf64);
+ if (!read_abbrevs (state, abbrev_offset,
+ dwarf_sections->data[DEBUG_ABBREV],
+ dwarf_sections->size[DEBUG_ABBREV],
+ is_bigendian, error_callback, data, &u->abbrevs))
+ goto fail;
+
+ if (version < 5)
+ addrsize = read_byte (&unit_buf);
+
+ switch (unit_type)
+ {
+ case 0:
+ break;
+ case DW_UT_compile: case DW_UT_partial:
+ break;
+ case DW_UT_skeleton: case DW_UT_split_compile:
+ read_uint64 (&unit_buf); /* dwo_id */
+ break;
+ default:
+ break;
+ }
+
+ u->low_offset = unit_offset;
+ unit_offset += len + (is_dwarf64 ? 12 : 4);
+ u->high_offset = unit_offset;
+ u->unit_data = unit_buf.buf;
+ u->unit_data_len = unit_buf.left;
+ u->unit_data_offset = unit_buf.buf - unit_data_start;
+ u->version = version;
+ u->is_dwarf64 = is_dwarf64;
+ u->addrsize = addrsize;
+ u->filename = NULL;
+ u->comp_dir = NULL;
+ u->abs_filename = NULL;
+ u->lineoff = 0;
+ u->str_offsets_base = 0;
+ u->addr_base = 0;
+ u->rnglists_base = 0;
+
+ /* The actual line number mappings will be read as needed. */
+ u->lines = NULL;
+ u->lines_count = 0;
+ u->function_addrs = NULL;
+ u->function_addrs_count = 0;
+
+ if (!find_address_ranges (state, base_address, &unit_buf, dwarf_sections,
+ is_bigendian, altlink, error_callback, data,
+ u, addrs, &unit_tag))
+ goto fail;
+
+ if (unit_buf.reported_underflow)
+ goto fail;
+ }
+ if (info.reported_underflow)
+ goto fail;
+
+ /* Add a trailing addrs entry, but don't include it in addrs->count. */
+ pa = ((struct unit_addrs *)
+ backtrace_vector_grow (state, sizeof (struct unit_addrs),
+ error_callback, data, &addrs->vec));
+ if (pa == NULL)
+ goto fail;
+ pa->low = 0;
+ --pa->low;
+ pa->high = pa->low;
+ pa->u = NULL;
+
+ unit_vec->vec = units;
+ unit_vec->count = units_count;
+ return 1;
+
+ fail:
+ if (units_count > 0)
+ {
+ pu = (struct unit **) units.base;
+ for (i = 0; i < units_count; i++)
+ {
+ free_abbrevs (state, &pu[i]->abbrevs, error_callback, data);
+ backtrace_free (state, pu[i], sizeof **pu, error_callback, data);
+ }
+ backtrace_vector_free (state, &units, error_callback, data);
+ }
+ if (addrs->count > 0)
+ {
+ backtrace_vector_free (state, &addrs->vec, error_callback, data);
+ addrs->count = 0;
+ }
+ return 0;
+}
+
+/* Add a new mapping to the vector of line mappings that we are
+ building. Returns 1 on success, 0 on failure. */
+
+static int
+add_line (struct backtrace_state *state, struct dwarf_data *ddata,
+ uintptr_t pc, const char *filename, int lineno,
+ backtrace_error_callback error_callback, void *data,
+ struct line_vector *vec)
+{
+ struct line *ln;
+
+ /* If we are adding the same mapping, ignore it. This can happen
+ when using discriminators. */
+ if (vec->count > 0)
+ {
+ ln = (struct line *) vec->vec.base + (vec->count - 1);
+ if (pc == ln->pc && filename == ln->filename && lineno == ln->lineno)
+ return 1;
+ }
+
+ ln = ((struct line *)
+ backtrace_vector_grow (state, sizeof (struct line), error_callback,
+ data, &vec->vec));
+ if (ln == NULL)
+ return 0;
+
+ /* Add in the base address here, so that we can look up the PC
+ directly. */
+ ln->pc = pc + ddata->base_address;
+
+ ln->filename = filename;
+ ln->lineno = lineno;
+ ln->idx = vec->count;
+
+ ++vec->count;
+
+ return 1;
+}
+
+/* Free the line header information. */
+
+static void
+free_line_header (struct backtrace_state *state, struct line_header *hdr,
+ backtrace_error_callback error_callback, void *data)
+{
+ if (hdr->dirs_count != 0)
+ backtrace_free (state, hdr->dirs, hdr->dirs_count * sizeof (const char *),
+ error_callback, data);
+ backtrace_free (state, hdr->filenames,
+ hdr->filenames_count * sizeof (char *),
+ error_callback, data);
+}
+
+/* Read the directories and file names for a line header for version
+ 2, setting fields in HDR. Return 1 on success, 0 on failure. */
+
+static int
+read_v2_paths (struct backtrace_state *state, struct unit *u,
+ struct dwarf_buf *hdr_buf, struct line_header *hdr)
+{
+ const unsigned char *p;
+ const unsigned char *pend;
+ size_t i;
+
+ /* Count the number of directory entries. */
+ hdr->dirs_count = 0;
+ p = hdr_buf->buf;
+ pend = p + hdr_buf->left;
+ while (p < pend && *p != '\0')
+ {
+ p += strnlen((const char *) p, pend - p) + 1;
+ ++hdr->dirs_count;
+ }
+
+ /* The index of the first entry in the list of directories is 1. Index 0 is
+ used for the current directory of the compilation. To simplify index
+ handling, we set entry 0 to the compilation unit directory. */
+ ++hdr->dirs_count;
+ hdr->dirs = ((const char **)
+ backtrace_alloc (state,
+ hdr->dirs_count * sizeof (const char *),
+ hdr_buf->error_callback,
+ hdr_buf->data));
+ if (hdr->dirs == NULL)
+ return 0;
+
+ hdr->dirs[0] = u->comp_dir;
+ i = 1;
+ while (*hdr_buf->buf != '\0')
+ {
+ if (hdr_buf->reported_underflow)
+ return 0;
+
+ hdr->dirs[i] = read_string (hdr_buf);
+ if (hdr->dirs[i] == NULL)
+ return 0;
+ ++i;
+ }
+ if (!advance (hdr_buf, 1))
+ return 0;
+
+ /* Count the number of file entries. */
+ hdr->filenames_count = 0;
+ p = hdr_buf->buf;
+ pend = p + hdr_buf->left;
+ while (p < pend && *p != '\0')
+ {
+ p += strnlen ((const char *) p, pend - p) + 1;
+ p += leb128_len (p);
+ p += leb128_len (p);
+ p += leb128_len (p);
+ ++hdr->filenames_count;
+ }
+
+ /* The index of the first entry in the list of file names is 1. Index 0 is
+ used for the DW_AT_name of the compilation unit. To simplify index
+ handling, we set entry 0 to the compilation unit file name. */
+ ++hdr->filenames_count;
+ hdr->filenames = ((const char **)
+ backtrace_alloc (state,
+ hdr->filenames_count * sizeof (char *),
+ hdr_buf->error_callback,
+ hdr_buf->data));
+ if (hdr->filenames == NULL)
+ return 0;
+ hdr->filenames[0] = u->filename;
+ i = 1;
+ while (*hdr_buf->buf != '\0')
+ {
+ const char *filename;
+ uint64_t dir_index;
+
+ if (hdr_buf->reported_underflow)
+ return 0;
+
+ filename = read_string (hdr_buf);
+ if (filename == NULL)
+ return 0;
+ dir_index = read_uleb128 (hdr_buf);
+ if (IS_ABSOLUTE_PATH (filename)
+ || (dir_index < hdr->dirs_count && hdr->dirs[dir_index] == NULL))
+ hdr->filenames[i] = filename;
+ else
+ {
+ const char *dir;
+ size_t dir_len;
+ size_t filename_len;
+ char *s;
+
+ if (dir_index < hdr->dirs_count)
+ dir = hdr->dirs[dir_index];
+ else
+ {
+ dwarf_buf_error (hdr_buf,
+ ("invalid directory index in "
+ "line number program header"),
+ 0);
+ return 0;
+ }
+ dir_len = strlen (dir);
+ filename_len = strlen (filename);
+ s = ((char *) backtrace_alloc (state, dir_len + filename_len + 2,
+ hdr_buf->error_callback,
+ hdr_buf->data));
+ if (s == NULL)
+ return 0;
+ memcpy (s, dir, dir_len);
+ /* FIXME: If we are on a DOS-based file system, and the
+ directory or the file name use backslashes, then we
+ should use a backslash here. */
+ s[dir_len] = '/';
+ memcpy (s + dir_len + 1, filename, filename_len + 1);
+ hdr->filenames[i] = s;
+ }
+
+ /* Ignore the modification time and size. */
+ read_uleb128 (hdr_buf);
+ read_uleb128 (hdr_buf);
+
+ ++i;
+ }
+
+ return 1;
+}
+
+/* Read a single version 5 LNCT entry for a directory or file name in a
+ line header. Sets *STRING to the resulting name, ignoring other
+ data. Return 1 on success, 0 on failure. */
+
+static int
+read_lnct (struct backtrace_state *state, struct dwarf_data *ddata,
+ struct unit *u, struct dwarf_buf *hdr_buf,
+ const struct line_header *hdr, size_t formats_count,
+ const struct line_header_format *formats, const char **string)
+{
+ size_t i;
+ const char *dir;
+ const char *path;
+
+ dir = NULL;
+ path = NULL;
+ for (i = 0; i < formats_count; i++)
+ {
+ struct attr_val val;
+
+ if (!read_attribute (formats[i].form, 0, hdr_buf, u->is_dwarf64,
+ u->version, hdr->addrsize, &ddata->dwarf_sections,
+ ddata->altlink, &val))
+ return 0;
+ switch (formats[i].lnct)
+ {
+ case DW_LNCT_path:
+ if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64,
+ ddata->is_bigendian, u->str_offsets_base,
+ &val, hdr_buf->error_callback, hdr_buf->data,
+ &path))
+ return 0;
+ break;
+ case DW_LNCT_directory_index:
+ if (val.encoding == ATTR_VAL_UINT)
+ {
+ if (val.u.uint >= hdr->dirs_count)
+ {
+ dwarf_buf_error (hdr_buf,
+ ("invalid directory index in "
+ "line number program header"),
+ 0);
+ return 0;
+ }
+ dir = hdr->dirs[val.u.uint];
+ }
+ break;
+ default:
+ /* We don't care about timestamps or sizes or hashes. */
+ break;
+ }
+ }
+
+ if (path == NULL)
+ {
+ dwarf_buf_error (hdr_buf,
+ "missing file name in line number program header",
+ 0);
+ return 0;
+ }
+
+ if (dir == NULL)
+ *string = path;
+ else
+ {
+ size_t dir_len;
+ size_t path_len;
+ char *s;
+
+ dir_len = strlen (dir);
+ path_len = strlen (path);
+ s = (char *) backtrace_alloc (state, dir_len + path_len + 2,
+ hdr_buf->error_callback, hdr_buf->data);
+ if (s == NULL)
+ return 0;
+ memcpy (s, dir, dir_len);
+ /* FIXME: If we are on a DOS-based file system, and the
+ directory or the path name use backslashes, then we should
+ use a backslash here. */
+ s[dir_len] = '/';
+ memcpy (s + dir_len + 1, path, path_len + 1);
+ *string = s;
+ }
+
+ return 1;
+}
+
+/* Read a set of DWARF 5 line header format entries, setting *PCOUNT
+ and *PPATHS. Return 1 on success, 0 on failure. */
+
+static int
+read_line_header_format_entries (struct backtrace_state *state,
+ struct dwarf_data *ddata,
+ struct unit *u,
+ struct dwarf_buf *hdr_buf,
+ struct line_header *hdr,
+ size_t *pcount,
+ const char ***ppaths)
+{
+ size_t formats_count;
+ struct line_header_format *formats;
+ size_t paths_count;
+ const char **paths;
+ size_t i;
+ int ret;
+
+ formats_count = read_byte (hdr_buf);
+ if (formats_count == 0)
+ formats = NULL;
+ else
+ {
+ formats = ((struct line_header_format *)
+ backtrace_alloc (state,
+ (formats_count
+ * sizeof (struct line_header_format)),
+ hdr_buf->error_callback,
+ hdr_buf->data));
+ if (formats == NULL)
+ return 0;
+
+ for (i = 0; i < formats_count; i++)
+ {
+ formats[i].lnct = (int) read_uleb128(hdr_buf);
+ formats[i].form = (enum dwarf_form) read_uleb128 (hdr_buf);
+ }
+ }
+
+ paths_count = read_uleb128 (hdr_buf);
+ if (paths_count == 0)
+ {
+ *pcount = 0;
+ *ppaths = NULL;
+ ret = 1;
+ goto exit;
+ }
+
+ paths = ((const char **)
+ backtrace_alloc (state, paths_count * sizeof (const char *),
+ hdr_buf->error_callback, hdr_buf->data));
+ if (paths == NULL)
+ {
+ ret = 0;
+ goto exit;
+ }
+ for (i = 0; i < paths_count; i++)
+ {
+ if (!read_lnct (state, ddata, u, hdr_buf, hdr, formats_count,
+ formats, &paths[i]))
+ {
+ backtrace_free (state, paths,
+ paths_count * sizeof (const char *),
+ hdr_buf->error_callback, hdr_buf->data);
+ ret = 0;
+ goto exit;
+ }
+ }
+
+ *pcount = paths_count;
+ *ppaths = paths;
+
+ ret = 1;
+
+ exit:
+ if (formats != NULL)
+ backtrace_free (state, formats,
+ formats_count * sizeof (struct line_header_format),
+ hdr_buf->error_callback, hdr_buf->data);
+
+ return ret;
+}
+
+/* Read the line header. Return 1 on success, 0 on failure. */
+
+static int
+read_line_header (struct backtrace_state *state, struct dwarf_data *ddata,
+ struct unit *u, int is_dwarf64, struct dwarf_buf *line_buf,
+ struct line_header *hdr)
+{
+ uint64_t hdrlen;
+ struct dwarf_buf hdr_buf;
+
+ hdr->version = read_uint16 (line_buf);
+ if (hdr->version < 2 || hdr->version > 5)
+ {
+ dwarf_buf_error (line_buf, "unsupported line number version", -1);
+ return 0;
+ }
+
+ if (hdr->version < 5)
+ hdr->addrsize = u->addrsize;
+ else
+ {
+ hdr->addrsize = read_byte (line_buf);
+ /* We could support a non-zero segment_selector_size but I doubt
+ we'll ever see it. */
+ if (read_byte (line_buf) != 0)
+ {
+ dwarf_buf_error (line_buf,
+ "non-zero segment_selector_size not supported",
+ -1);
+ return 0;
+ }
+ }
+
+ hdrlen = read_offset (line_buf, is_dwarf64);
+
+ hdr_buf = *line_buf;
+ hdr_buf.left = hdrlen;
+
+ if (!advance (line_buf, hdrlen))
+ return 0;
+
+ hdr->min_insn_len = read_byte (&hdr_buf);
+ if (hdr->version < 4)
+ hdr->max_ops_per_insn = 1;
+ else
+ hdr->max_ops_per_insn = read_byte (&hdr_buf);
+
+ /* We don't care about default_is_stmt. */
+ read_byte (&hdr_buf);
+
+ hdr->line_base = read_sbyte (&hdr_buf);
+ hdr->line_range = read_byte (&hdr_buf);
+
+ hdr->opcode_base = read_byte (&hdr_buf);
+ hdr->opcode_lengths = hdr_buf.buf;
+ if (!advance (&hdr_buf, hdr->opcode_base - 1))
+ return 0;
+
+ if (hdr->version < 5)
+ {
+ if (!read_v2_paths (state, u, &hdr_buf, hdr))
+ return 0;
+ }
+ else
+ {
+ if (!read_line_header_format_entries (state, ddata, u, &hdr_buf, hdr,
+ &hdr->dirs_count,
+ &hdr->dirs))
+ return 0;
+ if (!read_line_header_format_entries (state, ddata, u, &hdr_buf, hdr,
+ &hdr->filenames_count,
+ &hdr->filenames))
+ return 0;
+ }
+
+ if (hdr_buf.reported_underflow)
+ return 0;
+
+ return 1;
+}
+
+/* Read the line program, adding line mappings to VEC. Return 1 on
+ success, 0 on failure. */
+
+static int
+read_line_program (struct backtrace_state *state, struct dwarf_data *ddata,
+ const struct line_header *hdr, struct dwarf_buf *line_buf,
+ struct line_vector *vec)
+{
+ uint64_t address;
+ unsigned int op_index;
+ const char *reset_filename;
+ const char *filename;
+ int lineno;
+
+ address = 0;
+ op_index = 0;
+ if (hdr->filenames_count > 1)
+ reset_filename = hdr->filenames[1];
+ else
+ reset_filename = "";
+ filename = reset_filename;
+ lineno = 1;
+ while (line_buf->left > 0)
+ {
+ unsigned int op;
+
+ op = read_byte (line_buf);
+ if (op >= hdr->opcode_base)
+ {
+ unsigned int advance;
+
+ /* Special opcode. */
+ op -= hdr->opcode_base;
+ advance = op / hdr->line_range;
+ address += (hdr->min_insn_len * (op_index + advance)
+ / hdr->max_ops_per_insn);
+ op_index = (op_index + advance) % hdr->max_ops_per_insn;
+ lineno += hdr->line_base + (int) (op % hdr->line_range);
+ add_line (state, ddata, address, filename, lineno,
+ line_buf->error_callback, line_buf->data, vec);
+ }
+ else if (op == DW_LNS_extended_op)
+ {
+ uint64_t len;
+
+ len = read_uleb128 (line_buf);
+ op = read_byte (line_buf);
+ switch (op)
+ {
+ case DW_LNE_end_sequence:
+ /* FIXME: Should we mark the high PC here? It seems
+ that we already have that information from the
+ compilation unit. */
+ address = 0;
+ op_index = 0;
+ filename = reset_filename;
+ lineno = 1;
+ break;
+ case DW_LNE_set_address:
+ address = read_address (line_buf, hdr->addrsize);
+ break;
+ case DW_LNE_define_file:
+ {
+ const char *f;
+ unsigned int dir_index;
+
+ f = read_string (line_buf);
+ if (f == NULL)
+ return 0;
+ dir_index = read_uleb128 (line_buf);
+ /* Ignore that time and length. */
+ read_uleb128 (line_buf);
+ read_uleb128 (line_buf);
+ if (IS_ABSOLUTE_PATH (f))
+ filename = f;
+ else
+ {
+ const char *dir;
+ size_t dir_len;
+ size_t f_len;
+ char *p;
+
+ if (dir_index < hdr->dirs_count)
+ dir = hdr->dirs[dir_index];
+ else
+ {
+ dwarf_buf_error (line_buf,
+ ("invalid directory index "
+ "in line number program"),
+ 0);
+ return 0;
+ }
+ dir_len = strlen (dir);
+ f_len = strlen (f);
+ p = ((char *)
+ backtrace_alloc (state, dir_len + f_len + 2,
+ line_buf->error_callback,
+ line_buf->data));
+ if (p == NULL)
+ return 0;
+ memcpy (p, dir, dir_len);
+ /* FIXME: If we are on a DOS-based file system,
+ and the directory or the file name use
+ backslashes, then we should use a backslash
+ here. */
+ p[dir_len] = '/';
+ memcpy (p + dir_len + 1, f, f_len + 1);
+ filename = p;
+ }
+ }
+ break;
+ case DW_LNE_set_discriminator:
+ /* We don't care about discriminators. */
+ read_uleb128 (line_buf);
+ break;
+ default:
+ if (!advance (line_buf, len - 1))
+ return 0;
+ break;
+ }
+ }
+ else
+ {
+ switch (op)
+ {
+ case DW_LNS_copy:
+ add_line (state, ddata, address, filename, lineno,
+ line_buf->error_callback, line_buf->data, vec);
+ break;
+ case DW_LNS_advance_pc:
+ {
+ uint64_t advance;
+
+ advance = read_uleb128 (line_buf);
+ address += (hdr->min_insn_len * (op_index + advance)
+ / hdr->max_ops_per_insn);
+ op_index = (op_index + advance) % hdr->max_ops_per_insn;
+ }
+ break;
+ case DW_LNS_advance_line:
+ lineno += (int) read_sleb128 (line_buf);
+ break;
+ case DW_LNS_set_file:
+ {
+ uint64_t fileno;
+
+ fileno = read_uleb128 (line_buf);
+ if (fileno >= hdr->filenames_count)
+ {
+ dwarf_buf_error (line_buf,
+ ("invalid file number in "
+ "line number program"),
+ 0);
+ return 0;
+ }
+ filename = hdr->filenames[fileno];
+ }
+ break;
+ case DW_LNS_set_column:
+ read_uleb128 (line_buf);
+ break;
+ case DW_LNS_negate_stmt:
+ break;
+ case DW_LNS_set_basic_block:
+ break;
+ case DW_LNS_const_add_pc:
+ {
+ unsigned int advance;
+
+ op = 255 - hdr->opcode_base;
+ advance = op / hdr->line_range;
+ address += (hdr->min_insn_len * (op_index + advance)
+ / hdr->max_ops_per_insn);
+ op_index = (op_index + advance) % hdr->max_ops_per_insn;
+ }
+ break;
+ case DW_LNS_fixed_advance_pc:
+ address += read_uint16 (line_buf);
+ op_index = 0;
+ break;
+ case DW_LNS_set_prologue_end:
+ break;
+ case DW_LNS_set_epilogue_begin:
+ break;
+ case DW_LNS_set_isa:
+ read_uleb128 (line_buf);
+ break;
+ default:
+ {
+ unsigned int i;
+
+ for (i = hdr->opcode_lengths[op - 1]; i > 0; --i)
+ read_uleb128 (line_buf);
+ }
+ break;
+ }
+ }
+ }
+
+ return 1;
+}
+
+/* Read the line number information for a compilation unit. Returns 1
+ on success, 0 on failure. */
+
+static int
+read_line_info (struct backtrace_state *state, struct dwarf_data *ddata,
+ backtrace_error_callback error_callback, void *data,
+ struct unit *u, struct line_header *hdr, struct line **lines,
+ size_t *lines_count)
+{
+ struct line_vector vec;
+ struct dwarf_buf line_buf;
+ uint64_t len;
+ int is_dwarf64;
+ struct line *ln;
+
+ memset (&vec.vec, 0, sizeof vec.vec);
+ vec.count = 0;
+
+ memset (hdr, 0, sizeof *hdr);
+
+ if (u->lineoff != (off_t) (size_t) u->lineoff
+ || (size_t) u->lineoff >= ddata->dwarf_sections.size[DEBUG_LINE])
+ {
+ error_callback (data, "unit line offset out of range", 0);
+ goto fail;
+ }
+
+ line_buf.name = ".debug_line";
+ line_buf.start = ddata->dwarf_sections.data[DEBUG_LINE];
+ line_buf.buf = ddata->dwarf_sections.data[DEBUG_LINE] + u->lineoff;
+ line_buf.left = ddata->dwarf_sections.size[DEBUG_LINE] - u->lineoff;
+ line_buf.is_bigendian = ddata->is_bigendian;
+ line_buf.error_callback = error_callback;
+ line_buf.data = data;
+ line_buf.reported_underflow = 0;
+
+ len = read_initial_length (&line_buf, &is_dwarf64);
+ line_buf.left = len;
+
+ if (!read_line_header (state, ddata, u, is_dwarf64, &line_buf, hdr))
+ goto fail;
+
+ if (!read_line_program (state, ddata, hdr, &line_buf, &vec))
+ goto fail;
+
+ if (line_buf.reported_underflow)
+ goto fail;
+
+ if (vec.count == 0)
+ {
+ /* This is not a failure in the sense of a generating an error,
+ but it is a failure in that sense that we have no useful
+ information. */
+ goto fail;
+ }
+
+ /* Allocate one extra entry at the end. */
+ ln = ((struct line *)
+ backtrace_vector_grow (state, sizeof (struct line), error_callback,
+ data, &vec.vec));
+ if (ln == NULL)
+ goto fail;
+ ln->pc = (uintptr_t) -1;
+ ln->filename = NULL;
+ ln->lineno = 0;
+ ln->idx = 0;
+
+ if (!backtrace_vector_release (state, &vec.vec, error_callback, data))
+ goto fail;
+
+ ln = (struct line *) vec.vec.base;
+ backtrace_qsort (ln, vec.count, sizeof (struct line), line_compare);
+
+ *lines = ln;
+ *lines_count = vec.count;
+
+ return 1;
+
+ fail:
+ backtrace_vector_free (state, &vec.vec, error_callback, data);
+ free_line_header (state, hdr, error_callback, data);
+ *lines = (struct line *) (uintptr_t) -1;
+ *lines_count = 0;
+ return 0;
+}
+
+static const char *read_referenced_name (struct dwarf_data *, struct unit *,
+ uint64_t, backtrace_error_callback,
+ void *);
+
+/* Read the name of a function from a DIE referenced by ATTR with VAL. */
+
+static const char *
+read_referenced_name_from_attr (struct dwarf_data *ddata, struct unit *u,
+ struct attr *attr, struct attr_val *val,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ switch (attr->name)
+ {
+ case DW_AT_abstract_origin:
+ case DW_AT_specification:
+ break;
+ default:
+ return NULL;
+ }
+
+ if (attr->form == DW_FORM_ref_sig8)
+ return NULL;
+
+ if (val->encoding == ATTR_VAL_REF_INFO)
+ {
+ struct unit *unit
+ = find_unit (ddata->units, ddata->units_count,
+ val->u.uint);
+ if (unit == NULL)
+ return NULL;
+
+ uint64_t offset = val->u.uint - unit->low_offset;
+ return read_referenced_name (ddata, unit, offset, error_callback, data);
+ }
+
+ if (val->encoding == ATTR_VAL_UINT
+ || val->encoding == ATTR_VAL_REF_UNIT)
+ return read_referenced_name (ddata, u, val->u.uint, error_callback, data);
+
+ if (val->encoding == ATTR_VAL_REF_ALT_INFO)
+ {
+ struct unit *alt_unit
+ = find_unit (ddata->altlink->units, ddata->altlink->units_count,
+ val->u.uint);
+ if (alt_unit == NULL)
+ return NULL;
+
+ uint64_t offset = val->u.uint - alt_unit->low_offset;
+ return read_referenced_name (ddata->altlink, alt_unit, offset,
+ error_callback, data);
+ }
+
+ return NULL;
+}
+
+/* Read the name of a function from a DIE referenced by a
+ DW_AT_abstract_origin or DW_AT_specification tag. OFFSET is within
+ the same compilation unit. */
+
+static const char *
+read_referenced_name (struct dwarf_data *ddata, struct unit *u,
+ uint64_t offset, backtrace_error_callback error_callback,
+ void *data)
+{
+ struct dwarf_buf unit_buf;
+ uint64_t code;
+ const struct abbrev *abbrev;
+ const char *ret;
+ size_t i;
+
+ /* OFFSET is from the start of the data for this compilation unit.
+ U->unit_data is the data, but it starts U->unit_data_offset bytes
+ from the beginning. */
+
+ if (offset < u->unit_data_offset
+ || offset - u->unit_data_offset >= u->unit_data_len)
+ {
+ error_callback (data,
+ "abstract origin or specification out of range",
+ 0);
+ return NULL;
+ }
+
+ offset -= u->unit_data_offset;
+
+ unit_buf.name = ".debug_info";
+ unit_buf.start = ddata->dwarf_sections.data[DEBUG_INFO];
+ unit_buf.buf = u->unit_data + offset;
+ unit_buf.left = u->unit_data_len - offset;
+ unit_buf.is_bigendian = ddata->is_bigendian;
+ unit_buf.error_callback = error_callback;
+ unit_buf.data = data;
+ unit_buf.reported_underflow = 0;
+
+ code = read_uleb128 (&unit_buf);
+ if (code == 0)
+ {
+ dwarf_buf_error (&unit_buf,
+ "invalid abstract origin or specification",
+ 0);
+ return NULL;
+ }
+
+ abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data);
+ if (abbrev == NULL)
+ return NULL;
+
+ ret = NULL;
+ for (i = 0; i < abbrev->num_attrs; ++i)
+ {
+ struct attr_val val;
+
+ if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val,
+ &unit_buf, u->is_dwarf64, u->version, u->addrsize,
+ &ddata->dwarf_sections, ddata->altlink, &val))
+ return NULL;
+
+ switch (abbrev->attrs[i].name)
+ {
+ case DW_AT_name:
+ /* Third name preference: don't override. A name we found in some
+ other way, will normally be more useful -- e.g., this name is
+ normally not mangled. */
+ if (ret != NULL)
+ break;
+ if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64,
+ ddata->is_bigendian, u->str_offsets_base,
+ &val, error_callback, data, &ret))
+ return NULL;
+ break;
+
+ case DW_AT_linkage_name:
+ case DW_AT_MIPS_linkage_name:
+ /* First name preference: override all. */
+ {
+ const char *s;
+
+ s = NULL;
+ if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64,
+ ddata->is_bigendian, u->str_offsets_base,
+ &val, error_callback, data, &s))
+ return NULL;
+ if (s != NULL)
+ return s;
+ }
+ break;
+
+ case DW_AT_specification:
+ /* Second name preference: override DW_AT_name, don't override
+ DW_AT_linkage_name. */
+ {
+ const char *name;
+
+ name = read_referenced_name_from_attr (ddata, u, &abbrev->attrs[i],
+ &val, error_callback, data);
+ if (name != NULL)
+ ret = name;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return ret;
+}
+
+/* Add a range to a unit that maps to a function. This is called via
+ add_ranges. Returns 1 on success, 0 on error. */
+
+static int
+add_function_range (struct backtrace_state *state, void *rdata,
+ uint64_t lowpc, uint64_t highpc,
+ backtrace_error_callback error_callback, void *data,
+ void *pvec)
+{
+ struct function *function = (struct function *) rdata;
+ struct function_vector *vec = (struct function_vector *) pvec;
+ struct function_addrs *p;
+
+ if (vec->count > 0)
+ {
+ p = (struct function_addrs *) vec->vec.base + (vec->count - 1);
+ if ((lowpc == p->high || lowpc == p->high + 1)
+ && function == p->function)
+ {
+ if (highpc > p->high)
+ p->high = highpc;
+ return 1;
+ }
+ }
+
+ p = ((struct function_addrs *)
+ backtrace_vector_grow (state, sizeof (struct function_addrs),
+ error_callback, data, &vec->vec));
+ if (p == NULL)
+ return 0;
+
+ p->low = lowpc;
+ p->high = highpc;
+ p->function = function;
+
+ ++vec->count;
+
+ return 1;
+}
+
+/* Read one entry plus all its children. Add function addresses to
+ VEC. Returns 1 on success, 0 on error. */
+
+static int
+read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata,
+ struct unit *u, uint64_t base, struct dwarf_buf *unit_buf,
+ const struct line_header *lhdr,
+ backtrace_error_callback error_callback, void *data,
+ struct function_vector *vec_function,
+ struct function_vector *vec_inlined)
+{
+ while (unit_buf->left > 0)
+ {
+ uint64_t code;
+ const struct abbrev *abbrev;
+ int is_function;
+ struct function *function;
+ struct function_vector *vec;
+ size_t i;
+ struct pcrange pcrange;
+ int have_linkage_name;
+
+ code = read_uleb128 (unit_buf);
+ if (code == 0)
+ return 1;
+
+ abbrev = lookup_abbrev (&u->abbrevs, code, error_callback, data);
+ if (abbrev == NULL)
+ return 0;
+
+ is_function = (abbrev->tag == DW_TAG_subprogram
+ || abbrev->tag == DW_TAG_entry_point
+ || abbrev->tag == DW_TAG_inlined_subroutine);
+
+ if (abbrev->tag == DW_TAG_inlined_subroutine)
+ vec = vec_inlined;
+ else
+ vec = vec_function;
+
+ function = NULL;
+ if (is_function)
+ {
+ function = ((struct function *)
+ backtrace_alloc (state, sizeof *function,
+ error_callback, data));
+ if (function == NULL)
+ return 0;
+ memset (function, 0, sizeof *function);
+ }
+
+ memset (&pcrange, 0, sizeof pcrange);
+ have_linkage_name = 0;
+ for (i = 0; i < abbrev->num_attrs; ++i)
+ {
+ struct attr_val val;
+
+ if (!read_attribute (abbrev->attrs[i].form, abbrev->attrs[i].val,
+ unit_buf, u->is_dwarf64, u->version,
+ u->addrsize, &ddata->dwarf_sections,
+ ddata->altlink, &val))
+ return 0;
+
+ /* The compile unit sets the base address for any address
+ ranges in the function entries. */
+ if ((abbrev->tag == DW_TAG_compile_unit
+ || abbrev->tag == DW_TAG_skeleton_unit)
+ && abbrev->attrs[i].name == DW_AT_low_pc)
+ {
+ if (val.encoding == ATTR_VAL_ADDRESS)
+ base = val.u.uint;
+ else if (val.encoding == ATTR_VAL_ADDRESS_INDEX)
+ {
+ if (!resolve_addr_index (&ddata->dwarf_sections,
+ u->addr_base, u->addrsize,
+ ddata->is_bigendian, val.u.uint,
+ error_callback, data, &base))
+ return 0;
+ }
+ }
+
+ if (is_function)
+ {
+ switch (abbrev->attrs[i].name)
+ {
+ case DW_AT_call_file:
+ if (val.encoding == ATTR_VAL_UINT)
+ {
+ if (val.u.uint >= lhdr->filenames_count)
+ {
+ dwarf_buf_error (unit_buf,
+ ("invalid file number in "
+ "DW_AT_call_file attribute"),
+ 0);
+ return 0;
+ }
+ function->caller_filename = lhdr->filenames[val.u.uint];
+ }
+ break;
+
+ case DW_AT_call_line:
+ if (val.encoding == ATTR_VAL_UINT)
+ function->caller_lineno = val.u.uint;
+ break;
+
+ case DW_AT_abstract_origin:
+ case DW_AT_specification:
+ /* Second name preference: override DW_AT_name, don't override
+ DW_AT_linkage_name. */
+ if (have_linkage_name)
+ break;
+ {
+ const char *name;
+
+ name
+ = read_referenced_name_from_attr (ddata, u,
+ &abbrev->attrs[i], &val,
+ error_callback, data);
+ if (name != NULL)
+ function->name = name;
+ }
+ break;
+
+ case DW_AT_name:
+ /* Third name preference: don't override. */
+ if (function->name != NULL)
+ break;
+ if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64,
+ ddata->is_bigendian,
+ u->str_offsets_base, &val,
+ error_callback, data, &function->name))
+ return 0;
+ break;
+
+ case DW_AT_linkage_name:
+ case DW_AT_MIPS_linkage_name:
+ /* First name preference: override all. */
+ {
+ const char *s;
+
+ s = NULL;
+ if (!resolve_string (&ddata->dwarf_sections, u->is_dwarf64,
+ ddata->is_bigendian,
+ u->str_offsets_base, &val,
+ error_callback, data, &s))
+ return 0;
+ if (s != NULL)
+ {
+ function->name = s;
+ have_linkage_name = 1;
+ }
+ }
+ break;
+
+ case DW_AT_low_pc: case DW_AT_high_pc: case DW_AT_ranges:
+ update_pcrange (&abbrev->attrs[i], &val, &pcrange);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ /* If we couldn't find a name for the function, we have no use
+ for it. */
+ if (is_function && function->name == NULL)
+ {
+ backtrace_free (state, function, sizeof *function,
+ error_callback, data);
+ is_function = 0;
+ }
+
+ if (is_function)
+ {
+ if (pcrange.have_ranges
+ || (pcrange.have_lowpc && pcrange.have_highpc))
+ {
+ if (!add_ranges (state, &ddata->dwarf_sections,
+ ddata->base_address, ddata->is_bigendian,
+ u, base, &pcrange, add_function_range,
+ (void *) function, error_callback, data,
+ (void *) vec))
+ return 0;
+ }
+ else
+ {
+ backtrace_free (state, function, sizeof *function,
+ error_callback, data);
+ is_function = 0;
+ }
+ }
+
+ if (abbrev->has_children)
+ {
+ if (!is_function)
+ {
+ if (!read_function_entry (state, ddata, u, base, unit_buf, lhdr,
+ error_callback, data, vec_function,
+ vec_inlined))
+ return 0;
+ }
+ else
+ {
+ struct function_vector fvec;
+
+ /* Gather any information for inlined functions in
+ FVEC. */
+
+ memset (&fvec, 0, sizeof fvec);
+
+ if (!read_function_entry (state, ddata, u, base, unit_buf, lhdr,
+ error_callback, data, vec_function,
+ &fvec))
+ return 0;
+
+ if (fvec.count > 0)
+ {
+ struct function_addrs *p;
+ struct function_addrs *faddrs;
+
+ /* Allocate a trailing entry, but don't include it
+ in fvec.count. */
+ p = ((struct function_addrs *)
+ backtrace_vector_grow (state,
+ sizeof (struct function_addrs),
+ error_callback, data,
+ &fvec.vec));
+ if (p == NULL)
+ return 0;
+ p->low = 0;
+ --p->low;
+ p->high = p->low;
+ p->function = NULL;
+
+ if (!backtrace_vector_release (state, &fvec.vec,
+ error_callback, data))
+ return 0;
+
+ faddrs = (struct function_addrs *) fvec.vec.base;
+ backtrace_qsort (faddrs, fvec.count,
+ sizeof (struct function_addrs),
+ function_addrs_compare);
+
+ function->function_addrs = faddrs;
+ function->function_addrs_count = fvec.count;
+ }
+ }
+ }
+ }
+
+ return 1;
+}
+
+/* Read function name information for a compilation unit. We look
+ through the whole unit looking for function tags. */
+
+static void
+read_function_info (struct backtrace_state *state, struct dwarf_data *ddata,
+ const struct line_header *lhdr,
+ backtrace_error_callback error_callback, void *data,
+ struct unit *u, struct function_vector *fvec,
+ struct function_addrs **ret_addrs,
+ size_t *ret_addrs_count)
+{
+ struct function_vector lvec;
+ struct function_vector *pfvec;
+ struct dwarf_buf unit_buf;
+ struct function_addrs *p;
+ struct function_addrs *addrs;
+ size_t addrs_count;
+
+ /* Use FVEC if it is not NULL. Otherwise use our own vector. */
+ if (fvec != NULL)
+ pfvec = fvec;
+ else
+ {
+ memset (&lvec, 0, sizeof lvec);
+ pfvec = &lvec;
+ }
+
+ unit_buf.name = ".debug_info";
+ unit_buf.start = ddata->dwarf_sections.data[DEBUG_INFO];
+ unit_buf.buf = u->unit_data;
+ unit_buf.left = u->unit_data_len;
+ unit_buf.is_bigendian = ddata->is_bigendian;
+ unit_buf.error_callback = error_callback;
+ unit_buf.data = data;
+ unit_buf.reported_underflow = 0;
+
+ while (unit_buf.left > 0)
+ {
+ if (!read_function_entry (state, ddata, u, 0, &unit_buf, lhdr,
+ error_callback, data, pfvec, pfvec))
+ return;
+ }
+
+ if (pfvec->count == 0)
+ return;
+
+ /* Allocate a trailing entry, but don't include it in
+ pfvec->count. */
+ p = ((struct function_addrs *)
+ backtrace_vector_grow (state, sizeof (struct function_addrs),
+ error_callback, data, &pfvec->vec));
+ if (p == NULL)
+ return;
+ p->low = 0;
+ --p->low;
+ p->high = p->low;
+ p->function = NULL;
+
+ addrs_count = pfvec->count;
+
+ if (fvec == NULL)
+ {
+ if (!backtrace_vector_release (state, &lvec.vec, error_callback, data))
+ return;
+ addrs = (struct function_addrs *) pfvec->vec.base;
+ }
+ else
+ {
+ /* Finish this list of addresses, but leave the remaining space in
+ the vector available for the next function unit. */
+ addrs = ((struct function_addrs *)
+ backtrace_vector_finish (state, &fvec->vec,
+ error_callback, data));
+ if (addrs == NULL)
+ return;
+ fvec->count = 0;
+ }
+
+ backtrace_qsort (addrs, addrs_count, sizeof (struct function_addrs),
+ function_addrs_compare);
+
+ *ret_addrs = addrs;
+ *ret_addrs_count = addrs_count;
+}
+
+/* See if PC is inlined in FUNCTION. If it is, print out the inlined
+ information, and update FILENAME and LINENO for the caller.
+ Returns whatever CALLBACK returns, or 0 to keep going. */
+
+static int
+report_inlined_functions (uintptr_t pc, struct function *function,
+ backtrace_full_callback callback, void *data,
+ const char **filename, int *lineno)
+{
+ struct function_addrs *p;
+ struct function_addrs *match;
+ struct function *inlined;
+ int ret;
+
+ if (function->function_addrs_count == 0)
+ return 0;
+
+ /* Our search isn't safe if pc == -1, as that is the sentinel
+ value. */
+ if (pc + 1 == 0)
+ return 0;
+
+ p = ((struct function_addrs *)
+ bsearch (&pc, function->function_addrs,
+ function->function_addrs_count,
+ sizeof (struct function_addrs),
+ function_addrs_search));
+ if (p == NULL)
+ return 0;
+
+ /* Here pc >= p->low && pc < (p + 1)->low. The function_addrs are
+ sorted by low, so if pc > p->low we are at the end of a range of
+ function_addrs with the same low value. If pc == p->low walk
+ forward to the end of the range with that low value. Then walk
+ backward and use the first range that includes pc. */
+ while (pc == (p + 1)->low)
+ ++p;
+ match = NULL;
+ while (1)
+ {
+ if (pc < p->high)
+ {
+ match = p;
+ break;
+ }
+ if (p == function->function_addrs)
+ break;
+ if ((p - 1)->low < p->low)
+ break;
+ --p;
+ }
+ if (match == NULL)
+ return 0;
+
+ /* We found an inlined call. */
+
+ inlined = match->function;
+
+ /* Report any calls inlined into this one. */
+ ret = report_inlined_functions (pc, inlined, callback, data,
+ filename, lineno);
+ if (ret != 0)
+ return ret;
+
+ /* Report this inlined call. */
+ ret = callback (data, pc, *filename, *lineno, inlined->name);
+ if (ret != 0)
+ return ret;
+
+ /* Our caller will report the caller of the inlined function; tell
+ it the appropriate filename and line number. */
+ *filename = inlined->caller_filename;
+ *lineno = inlined->caller_lineno;
+
+ return 0;
+}
+
+/* Look for a PC in the DWARF mapping for one module. On success,
+ call CALLBACK and return whatever it returns. On error, call
+ ERROR_CALLBACK and return 0. Sets *FOUND to 1 if the PC is found,
+ 0 if not. */
+
+static int
+dwarf_lookup_pc (struct backtrace_state *state, struct dwarf_data *ddata,
+ uintptr_t pc, backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data,
+ int *found)
+{
+ struct unit_addrs *entry;
+ int found_entry;
+ struct unit *u;
+ int new_data;
+ struct line *lines;
+ struct line *ln;
+ struct function_addrs *p;
+ struct function_addrs *fmatch;
+ struct function *function;
+ const char *filename;
+ int lineno;
+ int ret;
+
+ *found = 1;
+
+ /* Find an address range that includes PC. Our search isn't safe if
+ PC == -1, as we use that as a sentinel value, so skip the search
+ in that case. */
+ entry = (ddata->addrs_count == 0 || pc + 1 == 0
+ ? NULL
+ : bsearch (&pc, ddata->addrs, ddata->addrs_count,
+ sizeof (struct unit_addrs), unit_addrs_search));
+
+ if (entry == NULL)
+ {
+ *found = 0;
+ return 0;
+ }
+
+ /* Here pc >= entry->low && pc < (entry + 1)->low. The unit_addrs
+ are sorted by low, so if pc > p->low we are at the end of a range
+ of unit_addrs with the same low value. If pc == p->low walk
+ forward to the end of the range with that low value. Then walk
+ backward and use the first range that includes pc. */
+ while (pc == (entry + 1)->low)
+ ++entry;
+ found_entry = 0;
+ while (1)
+ {
+ if (pc < entry->high)
+ {
+ found_entry = 1;
+ break;
+ }
+ if (entry == ddata->addrs)
+ break;
+ if ((entry - 1)->low < entry->low)
+ break;
+ --entry;
+ }
+ if (!found_entry)
+ {
+ *found = 0;
+ return 0;
+ }
+
+ /* We need the lines, lines_count, function_addrs,
+ function_addrs_count fields of u. If they are not set, we need
+ to set them. When running in threaded mode, we need to allow for
+ the possibility that some other thread is setting them
+ simultaneously. */
+
+ u = entry->u;
+ lines = u->lines;
+
+ /* Skip units with no useful line number information by walking
+ backward. Useless line number information is marked by setting
+ lines == -1. */
+ while (entry > ddata->addrs
+ && pc >= (entry - 1)->low
+ && pc < (entry - 1)->high)
+ {
+ if (state->threaded)
+ lines = (struct line *) backtrace_atomic_load_pointer (&u->lines);
+
+ if (lines != (struct line *) (uintptr_t) -1)
+ break;
+
+ --entry;
+
+ u = entry->u;
+ lines = u->lines;
+ }
+
+ if (state->threaded)
+ lines = backtrace_atomic_load_pointer (&u->lines);
+
+ new_data = 0;
+ if (lines == NULL)
+ {
+ struct function_addrs *function_addrs;
+ size_t function_addrs_count;
+ struct line_header lhdr;
+ size_t count;
+
+ /* We have never read the line information for this unit. Read
+ it now. */
+
+ function_addrs = NULL;
+ function_addrs_count = 0;
+ if (read_line_info (state, ddata, error_callback, data, entry->u, &lhdr,
+ &lines, &count))
+ {
+ struct function_vector *pfvec;
+
+ /* If not threaded, reuse DDATA->FVEC for better memory
+ consumption. */
+ if (state->threaded)
+ pfvec = NULL;
+ else
+ pfvec = &ddata->fvec;
+ read_function_info (state, ddata, &lhdr, error_callback, data,
+ entry->u, pfvec, &function_addrs,
+ &function_addrs_count);
+ free_line_header (state, &lhdr, error_callback, data);
+ new_data = 1;
+ }
+
+ /* Atomically store the information we just read into the unit.
+ If another thread is simultaneously writing, it presumably
+ read the same information, and we don't care which one we
+ wind up with; we just leak the other one. We do have to
+ write the lines field last, so that the acquire-loads above
+ ensure that the other fields are set. */
+
+ if (!state->threaded)
+ {
+ u->lines_count = count;
+ u->function_addrs = function_addrs;
+ u->function_addrs_count = function_addrs_count;
+ u->lines = lines;
+ }
+ else
+ {
+ backtrace_atomic_store_size_t (&u->lines_count, count);
+ backtrace_atomic_store_pointer (&u->function_addrs, function_addrs);
+ backtrace_atomic_store_size_t (&u->function_addrs_count,
+ function_addrs_count);
+ backtrace_atomic_store_pointer (&u->lines, lines);
+ }
+ }
+
+ /* Now all fields of U have been initialized. */
+
+ if (lines == (struct line *) (uintptr_t) -1)
+ {
+ /* If reading the line number information failed in some way,
+ try again to see if there is a better compilation unit for
+ this PC. */
+ if (new_data)
+ return dwarf_lookup_pc (state, ddata, pc, callback, error_callback,
+ data, found);
+ return callback (data, pc, NULL, 0, NULL);
+ }
+
+ /* Search for PC within this unit. */
+
+ ln = (struct line *) bsearch (&pc, lines, entry->u->lines_count,
+ sizeof (struct line), line_search);
+ if (ln == NULL)
+ {
+ /* The PC is between the low_pc and high_pc attributes of the
+ compilation unit, but no entry in the line table covers it.
+ This implies that the start of the compilation unit has no
+ line number information. */
+
+ if (entry->u->abs_filename == NULL)
+ {
+ const char *filename;
+
+ filename = entry->u->filename;
+ if (filename != NULL
+ && !IS_ABSOLUTE_PATH (filename)
+ && entry->u->comp_dir != NULL)
+ {
+ size_t filename_len;
+ const char *dir;
+ size_t dir_len;
+ char *s;
+
+ filename_len = strlen (filename);
+ dir = entry->u->comp_dir;
+ dir_len = strlen (dir);
+ s = (char *) backtrace_alloc (state, dir_len + filename_len + 2,
+ error_callback, data);
+ if (s == NULL)
+ {
+ *found = 0;
+ return 0;
+ }
+ memcpy (s, dir, dir_len);
+ /* FIXME: Should use backslash if DOS file system. */
+ s[dir_len] = '/';
+ memcpy (s + dir_len + 1, filename, filename_len + 1);
+ filename = s;
+ }
+ entry->u->abs_filename = filename;
+ }
+
+ return callback (data, pc, entry->u->abs_filename, 0, NULL);
+ }
+
+ /* Search for function name within this unit. */
+
+ if (entry->u->function_addrs_count == 0)
+ return callback (data, pc, ln->filename, ln->lineno, NULL);
+
+ p = ((struct function_addrs *)
+ bsearch (&pc, entry->u->function_addrs,
+ entry->u->function_addrs_count,
+ sizeof (struct function_addrs),
+ function_addrs_search));
+ if (p == NULL)
+ return callback (data, pc, ln->filename, ln->lineno, NULL);
+
+ /* Here pc >= p->low && pc < (p + 1)->low. The function_addrs are
+ sorted by low, so if pc > p->low we are at the end of a range of
+ function_addrs with the same low value. If pc == p->low walk
+ forward to the end of the range with that low value. Then walk
+ backward and use the first range that includes pc. */
+ while (pc == (p + 1)->low)
+ ++p;
+ fmatch = NULL;
+ while (1)
+ {
+ if (pc < p->high)
+ {
+ fmatch = p;
+ break;
+ }
+ if (p == entry->u->function_addrs)
+ break;
+ if ((p - 1)->low < p->low)
+ break;
+ --p;
+ }
+ if (fmatch == NULL)
+ return callback (data, pc, ln->filename, ln->lineno, NULL);
+
+ function = fmatch->function;
+
+ filename = ln->filename;
+ lineno = ln->lineno;
+
+ ret = report_inlined_functions (pc, function, callback, data,
+ &filename, &lineno);
+ if (ret != 0)
+ return ret;
+
+ return callback (data, pc, filename, lineno, function->name);
+}
+
+
+/* Return the file/line information for a PC using the DWARF mapping
+ we built earlier. */
+
+static int
+dwarf_fileline (struct backtrace_state *state, uintptr_t pc,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ struct dwarf_data *ddata;
+ int found;
+ int ret;
+
+ if (!state->threaded)
+ {
+ for (ddata = (struct dwarf_data *) state->fileline_data;
+ ddata != NULL;
+ ddata = ddata->next)
+ {
+ ret = dwarf_lookup_pc (state, ddata, pc, callback, error_callback,
+ data, &found);
+ if (ret != 0 || found)
+ return ret;
+ }
+ }
+ else
+ {
+ struct dwarf_data **pp;
+
+ pp = (struct dwarf_data **) (void *) &state->fileline_data;
+ while (1)
+ {
+ ddata = backtrace_atomic_load_pointer (pp);
+ if (ddata == NULL)
+ break;
+
+ ret = dwarf_lookup_pc (state, ddata, pc, callback, error_callback,
+ data, &found);
+ if (ret != 0 || found)
+ return ret;
+
+ pp = &ddata->next;
+ }
+ }
+
+ /* FIXME: See if any libraries have been dlopen'ed. */
+
+ return callback (data, pc, NULL, 0, NULL);
+}
+
+/* Initialize our data structures from the DWARF debug info for a
+ file. Return NULL on failure. */
+
+static struct dwarf_data *
+build_dwarf_data (struct backtrace_state *state,
+ uintptr_t base_address,
+ const struct dwarf_sections *dwarf_sections,
+ int is_bigendian,
+ struct dwarf_data *altlink,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ struct unit_addrs_vector addrs_vec;
+ struct unit_addrs *addrs;
+ size_t addrs_count;
+ struct unit_vector units_vec;
+ struct unit **units;
+ size_t units_count;
+ struct dwarf_data *fdata;
+
+ if (!build_address_map (state, base_address, dwarf_sections, is_bigendian,
+ altlink, error_callback, data, &addrs_vec,
+ &units_vec))
+ return NULL;
+
+ if (!backtrace_vector_release (state, &addrs_vec.vec, error_callback, data))
+ return NULL;
+ if (!backtrace_vector_release (state, &units_vec.vec, error_callback, data))
+ return NULL;
+ addrs = (struct unit_addrs *) addrs_vec.vec.base;
+ units = (struct unit **) units_vec.vec.base;
+ addrs_count = addrs_vec.count;
+ units_count = units_vec.count;
+ backtrace_qsort (addrs, addrs_count, sizeof (struct unit_addrs),
+ unit_addrs_compare);
+ /* No qsort for units required, already sorted. */
+
+ fdata = ((struct dwarf_data *)
+ backtrace_alloc (state, sizeof (struct dwarf_data),
+ error_callback, data));
+ if (fdata == NULL)
+ return NULL;
+
+ fdata->next = NULL;
+ fdata->altlink = altlink;
+ fdata->base_address = base_address;
+ fdata->addrs = addrs;
+ fdata->addrs_count = addrs_count;
+ fdata->units = units;
+ fdata->units_count = units_count;
+ fdata->dwarf_sections = *dwarf_sections;
+ fdata->is_bigendian = is_bigendian;
+ memset (&fdata->fvec, 0, sizeof fdata->fvec);
+
+ return fdata;
+}
+
+/* Build our data structures from the DWARF sections for a module.
+ Set FILELINE_FN and STATE->FILELINE_DATA. Return 1 on success, 0
+ on failure. */
+
+int
+backtrace_dwarf_add (struct backtrace_state *state,
+ uintptr_t base_address,
+ const struct dwarf_sections *dwarf_sections,
+ int is_bigendian,
+ struct dwarf_data *fileline_altlink,
+ backtrace_error_callback error_callback,
+ void *data, fileline *fileline_fn,
+ struct dwarf_data **fileline_entry)
+{
+ struct dwarf_data *fdata;
+
+ fdata = build_dwarf_data (state, base_address, dwarf_sections, is_bigendian,
+ fileline_altlink, error_callback, data);
+ if (fdata == NULL)
+ return 0;
+
+ if (fileline_entry != NULL)
+ *fileline_entry = fdata;
+
+ if (!state->threaded)
+ {
+ struct dwarf_data **pp;
+
+ for (pp = (struct dwarf_data **) (void *) &state->fileline_data;
+ *pp != NULL;
+ pp = &(*pp)->next)
+ ;
+ *pp = fdata;
+ }
+ else
+ {
+ while (1)
+ {
+ struct dwarf_data **pp;
+
+ pp = (struct dwarf_data **) (void *) &state->fileline_data;
+
+ while (1)
+ {
+ struct dwarf_data *p;
+
+ p = backtrace_atomic_load_pointer (pp);
+
+ if (p == NULL)
+ break;
+
+ pp = &p->next;
+ }
+
+ if (__sync_bool_compare_and_swap (pp, NULL, fdata))
+ break;
+ }
+ }
+
+ *fileline_fn = dwarf_fileline;
+
+ return 1;
+}
diff --git a/contrib/libs/backtrace/elf.c b/contrib/libs/backtrace/elf.c
new file mode 100644
index 0000000000..77a1a728fd
--- /dev/null
+++ b/contrib/libs/backtrace/elf.c
@@ -0,0 +1,4924 @@
+/* elf.c -- Get debug data from an ELF file for backtraces.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef HAVE_DL_ITERATE_PHDR
+ #ifdef HAVE_LINK_H
+ #include <link.h>
+ #endif
+ #ifdef HAVE_SYS_LINK_H
+ #error #include <sys/link.h>
+ #endif
+#endif
+
+#include "backtrace.h"
+#include "internal.h"
+
+#ifndef S_ISLNK
+ #ifndef S_IFLNK
+ #define S_IFLNK 0120000
+ #endif
+ #ifndef S_IFMT
+ #define S_IFMT 0170000
+ #endif
+ #define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
+#endif
+
+#ifndef __GNUC__
+#define __builtin_prefetch(p, r, l)
+#define unlikely(x) (x)
+#else
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#endif
+
+#if !defined(HAVE_DECL_STRNLEN) || !HAVE_DECL_STRNLEN
+
+/* If strnlen is not declared, provide our own version. */
+
+static size_t
+xstrnlen (const char *s, size_t maxlen)
+{
+ size_t i;
+
+ for (i = 0; i < maxlen; ++i)
+ if (s[i] == '\0')
+ break;
+ return i;
+}
+
+#define strnlen xstrnlen
+
+#endif
+
+#ifndef HAVE_LSTAT
+
+/* Dummy version of lstat for systems that don't have it. */
+
+static int
+xlstat (const char *path ATTRIBUTE_UNUSED, struct stat *st ATTRIBUTE_UNUSED)
+{
+ return -1;
+}
+
+#define lstat xlstat
+
+#endif
+
+#ifndef HAVE_READLINK
+
+/* Dummy version of readlink for systems that don't have it. */
+
+static ssize_t
+xreadlink (const char *path ATTRIBUTE_UNUSED, char *buf ATTRIBUTE_UNUSED,
+ size_t bufsz ATTRIBUTE_UNUSED)
+{
+ return -1;
+}
+
+#define readlink xreadlink
+
+#endif
+
+#ifndef HAVE_DL_ITERATE_PHDR
+
+/* Dummy version of dl_iterate_phdr for systems that don't have it. */
+
+#define dl_phdr_info x_dl_phdr_info
+#define dl_iterate_phdr x_dl_iterate_phdr
+
+struct dl_phdr_info
+{
+ uintptr_t dlpi_addr;
+ const char *dlpi_name;
+};
+
+static int
+dl_iterate_phdr (int (*callback) (struct dl_phdr_info *,
+ size_t, void *) ATTRIBUTE_UNUSED,
+ void *data ATTRIBUTE_UNUSED)
+{
+ return 0;
+}
+
+#endif /* ! defined (HAVE_DL_ITERATE_PHDR) */
+
+/* The configure script must tell us whether we are 32-bit or 64-bit
+ ELF. We could make this code test and support either possibility,
+ but there is no point. This code only works for the currently
+ running executable, which means that we know the ELF mode at
+ configure time. */
+
+#if BACKTRACE_ELF_SIZE != 32 && BACKTRACE_ELF_SIZE != 64
+#error "Unknown BACKTRACE_ELF_SIZE"
+#endif
+
+/* <link.h> might #include <elf.h> which might define our constants
+ with slightly different values. Undefine them to be safe. */
+
+#undef EI_NIDENT
+#undef EI_MAG0
+#undef EI_MAG1
+#undef EI_MAG2
+#undef EI_MAG3
+#undef EI_CLASS
+#undef EI_DATA
+#undef EI_VERSION
+#undef ELF_MAG0
+#undef ELF_MAG1
+#undef ELF_MAG2
+#undef ELF_MAG3
+#undef ELFCLASS32
+#undef ELFCLASS64
+#undef ELFDATA2LSB
+#undef ELFDATA2MSB
+#undef EV_CURRENT
+#undef ET_DYN
+#undef EM_PPC64
+#undef EF_PPC64_ABI
+#undef SHN_LORESERVE
+#undef SHN_XINDEX
+#undef SHN_UNDEF
+#undef SHT_PROGBITS
+#undef SHT_SYMTAB
+#undef SHT_STRTAB
+#undef SHT_DYNSYM
+#undef SHF_COMPRESSED
+#undef STT_OBJECT
+#undef STT_FUNC
+#undef NT_GNU_BUILD_ID
+#undef ELFCOMPRESS_ZLIB
+
+/* Basic types. */
+
+typedef uint16_t b_elf_half; /* Elf_Half. */
+typedef uint32_t b_elf_word; /* Elf_Word. */
+typedef int32_t b_elf_sword; /* Elf_Sword. */
+
+#if BACKTRACE_ELF_SIZE == 32
+
+typedef uint32_t b_elf_addr; /* Elf_Addr. */
+typedef uint32_t b_elf_off; /* Elf_Off. */
+
+typedef uint32_t b_elf_wxword; /* 32-bit Elf_Word, 64-bit ELF_Xword. */
+
+#else
+
+typedef uint64_t b_elf_addr; /* Elf_Addr. */
+typedef uint64_t b_elf_off; /* Elf_Off. */
+typedef uint64_t b_elf_xword; /* Elf_Xword. */
+typedef int64_t b_elf_sxword; /* Elf_Sxword. */
+
+typedef uint64_t b_elf_wxword; /* 32-bit Elf_Word, 64-bit ELF_Xword. */
+
+#endif
+
+/* Data structures and associated constants. */
+
+#define EI_NIDENT 16
+
+typedef struct {
+ unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
+ b_elf_half e_type; /* Identifies object file type */
+ b_elf_half e_machine; /* Specifies required architecture */
+ b_elf_word e_version; /* Identifies object file version */
+ b_elf_addr e_entry; /* Entry point virtual address */
+ b_elf_off e_phoff; /* Program header table file offset */
+ b_elf_off e_shoff; /* Section header table file offset */
+ b_elf_word e_flags; /* Processor-specific flags */
+ b_elf_half e_ehsize; /* ELF header size in bytes */
+ b_elf_half e_phentsize; /* Program header table entry size */
+ b_elf_half e_phnum; /* Program header table entry count */
+ b_elf_half e_shentsize; /* Section header table entry size */
+ b_elf_half e_shnum; /* Section header table entry count */
+ b_elf_half e_shstrndx; /* Section header string table index */
+} b_elf_ehdr; /* Elf_Ehdr. */
+
+#define EI_MAG0 0
+#define EI_MAG1 1
+#define EI_MAG2 2
+#define EI_MAG3 3
+#define EI_CLASS 4
+#define EI_DATA 5
+#define EI_VERSION 6
+
+#define ELFMAG0 0x7f
+#define ELFMAG1 'E'
+#define ELFMAG2 'L'
+#define ELFMAG3 'F'
+
+#define ELFCLASS32 1
+#define ELFCLASS64 2
+
+#define ELFDATA2LSB 1
+#define ELFDATA2MSB 2
+
+#define EV_CURRENT 1
+
+#define ET_DYN 3
+
+#define EM_PPC64 21
+#define EF_PPC64_ABI 3
+
+typedef struct {
+ b_elf_word sh_name; /* Section name, index in string tbl */
+ b_elf_word sh_type; /* Type of section */
+ b_elf_wxword sh_flags; /* Miscellaneous section attributes */
+ b_elf_addr sh_addr; /* Section virtual addr at execution */
+ b_elf_off sh_offset; /* Section file offset */
+ b_elf_wxword sh_size; /* Size of section in bytes */
+ b_elf_word sh_link; /* Index of another section */
+ b_elf_word sh_info; /* Additional section information */
+ b_elf_wxword sh_addralign; /* Section alignment */
+ b_elf_wxword sh_entsize; /* Entry size if section holds table */
+} b_elf_shdr; /* Elf_Shdr. */
+
+#define SHN_UNDEF 0x0000 /* Undefined section */
+#define SHN_LORESERVE 0xFF00 /* Begin range of reserved indices */
+#define SHN_XINDEX 0xFFFF /* Section index is held elsewhere */
+
+#define SHT_PROGBITS 1
+#define SHT_SYMTAB 2
+#define SHT_STRTAB 3
+#define SHT_DYNSYM 11
+
+#define SHF_COMPRESSED 0x800
+
+#if BACKTRACE_ELF_SIZE == 32
+
+typedef struct
+{
+ b_elf_word st_name; /* Symbol name, index in string tbl */
+ b_elf_addr st_value; /* Symbol value */
+ b_elf_word st_size; /* Symbol size */
+ unsigned char st_info; /* Symbol binding and type */
+ unsigned char st_other; /* Visibility and other data */
+ b_elf_half st_shndx; /* Symbol section index */
+} b_elf_sym; /* Elf_Sym. */
+
+#else /* BACKTRACE_ELF_SIZE != 32 */
+
+typedef struct
+{
+ b_elf_word st_name; /* Symbol name, index in string tbl */
+ unsigned char st_info; /* Symbol binding and type */
+ unsigned char st_other; /* Visibility and other data */
+ b_elf_half st_shndx; /* Symbol section index */
+ b_elf_addr st_value; /* Symbol value */
+ b_elf_xword st_size; /* Symbol size */
+} b_elf_sym; /* Elf_Sym. */
+
+#endif /* BACKTRACE_ELF_SIZE != 32 */
+
+#define STT_OBJECT 1
+#define STT_FUNC 2
+
+typedef struct
+{
+ uint32_t namesz;
+ uint32_t descsz;
+ uint32_t type;
+ char name[1];
+} b_elf_note;
+
+#define NT_GNU_BUILD_ID 3
+
+#if BACKTRACE_ELF_SIZE == 32
+
+typedef struct
+{
+ b_elf_word ch_type; /* Compresstion algorithm */
+ b_elf_word ch_size; /* Uncompressed size */
+ b_elf_word ch_addralign; /* Alignment for uncompressed data */
+} b_elf_chdr; /* Elf_Chdr */
+
+#else /* BACKTRACE_ELF_SIZE != 32 */
+
+typedef struct
+{
+ b_elf_word ch_type; /* Compression algorithm */
+ b_elf_word ch_reserved; /* Reserved */
+ b_elf_xword ch_size; /* Uncompressed size */
+ b_elf_xword ch_addralign; /* Alignment for uncompressed data */
+} b_elf_chdr; /* Elf_Chdr */
+
+#endif /* BACKTRACE_ELF_SIZE != 32 */
+
+#define ELFCOMPRESS_ZLIB 1
+
+/* Names of sections, indexed by enum dwarf_section in internal.h. */
+
+static const char * const dwarf_section_names[DEBUG_MAX] =
+{
+ ".debug_info",
+ ".debug_line",
+ ".debug_abbrev",
+ ".debug_ranges",
+ ".debug_str",
+ ".debug_addr",
+ ".debug_str_offsets",
+ ".debug_line_str",
+ ".debug_rnglists"
+};
+
+/* Information we gather for the sections we care about. */
+
+struct debug_section_info
+{
+ /* Section file offset. */
+ off_t offset;
+ /* Section size. */
+ size_t size;
+ /* Section contents, after read from file. */
+ const unsigned char *data;
+ /* Whether the SHF_COMPRESSED flag is set for the section. */
+ int compressed;
+};
+
+/* Information we keep for an ELF symbol. */
+
+struct elf_symbol
+{
+ /* The name of the symbol. */
+ const char *name;
+ /* The address of the symbol. */
+ uintptr_t address;
+ /* The size of the symbol. */
+ size_t size;
+};
+
+/* Information to pass to elf_syminfo. */
+
+struct elf_syminfo_data
+{
+ /* Symbols for the next module. */
+ struct elf_syminfo_data *next;
+ /* The ELF symbols, sorted by address. */
+ struct elf_symbol *symbols;
+ /* The number of symbols. */
+ size_t count;
+};
+
+/* A view that works for either a file or memory. */
+
+struct elf_view
+{
+ struct backtrace_view view;
+ int release; /* If non-zero, must call backtrace_release_view. */
+};
+
+/* Information about PowerPC64 ELFv1 .opd section. */
+
+struct elf_ppc64_opd_data
+{
+ /* Address of the .opd section. */
+ b_elf_addr addr;
+ /* Section data. */
+ const char *data;
+ /* Size of the .opd section. */
+ size_t size;
+ /* Corresponding section view. */
+ struct elf_view view;
+};
+
+/* Create a view of SIZE bytes from DESCRIPTOR/MEMORY at OFFSET. */
+
+static int
+elf_get_view (struct backtrace_state *state, int descriptor,
+ const unsigned char *memory, size_t memory_size, off_t offset,
+ uint64_t size, backtrace_error_callback error_callback,
+ void *data, struct elf_view *view)
+{
+ if (memory == NULL)
+ {
+ view->release = 1;
+ return backtrace_get_view (state, descriptor, offset, size,
+ error_callback, data, &view->view);
+ }
+ else
+ {
+ if ((uint64_t) offset + size > (uint64_t) memory_size)
+ {
+ error_callback (data, "out of range for in-memory file", 0);
+ return 0;
+ }
+ view->view.data = (const void *) (memory + offset);
+ view->view.base = NULL;
+ view->view.len = size;
+ view->release = 0;
+ return 1;
+ }
+}
+
+/* Release a view read by elf_get_view. */
+
+static void
+elf_release_view (struct backtrace_state *state, struct elf_view *view,
+ backtrace_error_callback error_callback, void *data)
+{
+ if (view->release)
+ backtrace_release_view (state, &view->view, error_callback, data);
+}
+
+/* Compute the CRC-32 of BUF/LEN. This uses the CRC used for
+ .gnu_debuglink files. */
+
+static uint32_t
+elf_crc32 (uint32_t crc, const unsigned char *buf, size_t len)
+{
+ static const uint32_t crc32_table[256] =
+ {
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419,
+ 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4,
+ 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07,
+ 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
+ 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856,
+ 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
+ 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+ 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3,
+ 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a,
+ 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599,
+ 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190,
+ 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
+ 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e,
+ 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+ 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed,
+ 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3,
+ 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
+ 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
+ 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5,
+ 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010,
+ 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17,
+ 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6,
+ 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
+ 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
+ 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344,
+ 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a,
+ 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+ 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1,
+ 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c,
+ 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
+ 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe,
+ 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31,
+ 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c,
+ 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+ 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b,
+ 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1,
+ 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
+ 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
+ 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7,
+ 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66,
+ 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
+ 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8,
+ 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b,
+ 0x2d02ef8d
+ };
+ const unsigned char *end;
+
+ crc = ~crc;
+ for (end = buf + len; buf < end; ++ buf)
+ crc = crc32_table[(crc ^ *buf) & 0xff] ^ (crc >> 8);
+ return ~crc;
+}
+
+/* Return the CRC-32 of the entire file open at DESCRIPTOR. */
+
+static uint32_t
+elf_crc32_file (struct backtrace_state *state, int descriptor,
+ backtrace_error_callback error_callback, void *data)
+{
+ struct stat st;
+ struct backtrace_view file_view;
+ uint32_t ret;
+
+ if (fstat (descriptor, &st) < 0)
+ {
+ error_callback (data, "fstat", errno);
+ return 0;
+ }
+
+ if (!backtrace_get_view (state, descriptor, 0, st.st_size, error_callback,
+ data, &file_view))
+ return 0;
+
+ ret = elf_crc32 (0, (const unsigned char *) file_view.data, st.st_size);
+
+ backtrace_release_view (state, &file_view, error_callback, data);
+
+ return ret;
+}
+
+/* A dummy callback function used when we can't find a symbol
+ table. */
+
+static void
+elf_nosyms (struct backtrace_state *state ATTRIBUTE_UNUSED,
+ uintptr_t addr ATTRIBUTE_UNUSED,
+ backtrace_syminfo_callback callback ATTRIBUTE_UNUSED,
+ backtrace_error_callback error_callback, void *data)
+{
+ error_callback (data, "no symbol table in ELF executable", -1);
+}
+
+/* A callback function used when we can't find any debug info. */
+
+static int
+elf_nodebug (struct backtrace_state *state, uintptr_t pc,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ if (state->syminfo_fn != NULL && state->syminfo_fn != elf_nosyms)
+ {
+ struct backtrace_call_full bdata;
+
+ /* Fetch symbol information so that we can least get the
+ function name. */
+
+ bdata.full_callback = callback;
+ bdata.full_error_callback = error_callback;
+ bdata.full_data = data;
+ bdata.ret = 0;
+ state->syminfo_fn (state, pc, backtrace_syminfo_to_full_callback,
+ backtrace_syminfo_to_full_error_callback, &bdata);
+ return bdata.ret;
+ }
+
+ error_callback (data, "no debug info in ELF executable", -1);
+ return 0;
+}
+
+/* Compare struct elf_symbol for qsort. */
+
+static int
+elf_symbol_compare (const void *v1, const void *v2)
+{
+ const struct elf_symbol *e1 = (const struct elf_symbol *) v1;
+ const struct elf_symbol *e2 = (const struct elf_symbol *) v2;
+
+ if (e1->address < e2->address)
+ return -1;
+ else if (e1->address > e2->address)
+ return 1;
+ else
+ return 0;
+}
+
+/* Compare an ADDR against an elf_symbol for bsearch. We allocate one
+ extra entry in the array so that this can look safely at the next
+ entry. */
+
+static int
+elf_symbol_search (const void *vkey, const void *ventry)
+{
+ const uintptr_t *key = (const uintptr_t *) vkey;
+ const struct elf_symbol *entry = (const struct elf_symbol *) ventry;
+ uintptr_t addr;
+
+ addr = *key;
+ if (addr < entry->address)
+ return -1;
+ else if (addr >= entry->address + entry->size)
+ return 1;
+ else
+ return 0;
+}
+
+/* Initialize the symbol table info for elf_syminfo. */
+
+static int
+elf_initialize_syminfo (struct backtrace_state *state,
+ uintptr_t base_address,
+ const unsigned char *symtab_data, size_t symtab_size,
+ const unsigned char *strtab, size_t strtab_size,
+ backtrace_error_callback error_callback,
+ void *data, struct elf_syminfo_data *sdata,
+ struct elf_ppc64_opd_data *opd)
+{
+ size_t sym_count;
+ const b_elf_sym *sym;
+ size_t elf_symbol_count;
+ size_t elf_symbol_size;
+ struct elf_symbol *elf_symbols;
+ size_t i;
+ unsigned int j;
+
+ sym_count = symtab_size / sizeof (b_elf_sym);
+
+ /* We only care about function symbols. Count them. */
+ sym = (const b_elf_sym *) symtab_data;
+ elf_symbol_count = 0;
+ for (i = 0; i < sym_count; ++i, ++sym)
+ {
+ int info;
+
+ info = sym->st_info & 0xf;
+ if ((info == STT_FUNC || info == STT_OBJECT)
+ && sym->st_shndx != SHN_UNDEF)
+ ++elf_symbol_count;
+ }
+
+ elf_symbol_size = elf_symbol_count * sizeof (struct elf_symbol);
+ elf_symbols = ((struct elf_symbol *)
+ backtrace_alloc (state, elf_symbol_size, error_callback,
+ data));
+ if (elf_symbols == NULL)
+ return 0;
+
+ sym = (const b_elf_sym *) symtab_data;
+ j = 0;
+ for (i = 0; i < sym_count; ++i, ++sym)
+ {
+ int info;
+
+ info = sym->st_info & 0xf;
+ if (info != STT_FUNC && info != STT_OBJECT)
+ continue;
+ if (sym->st_shndx == SHN_UNDEF)
+ continue;
+ if (sym->st_name >= strtab_size)
+ {
+ error_callback (data, "symbol string index out of range", 0);
+ backtrace_free (state, elf_symbols, elf_symbol_size, error_callback,
+ data);
+ return 0;
+ }
+ elf_symbols[j].name = (const char *) strtab + sym->st_name;
+ /* Special case PowerPC64 ELFv1 symbols in .opd section, if the symbol
+ is a function descriptor, read the actual code address from the
+ descriptor. */
+ if (opd
+ && sym->st_value >= opd->addr
+ && sym->st_value < opd->addr + opd->size)
+ elf_symbols[j].address
+ = *(const b_elf_addr *) (opd->data + (sym->st_value - opd->addr));
+ else
+ elf_symbols[j].address = sym->st_value;
+ elf_symbols[j].address += base_address;
+ elf_symbols[j].size = sym->st_size;
+ ++j;
+ }
+
+ backtrace_qsort (elf_symbols, elf_symbol_count, sizeof (struct elf_symbol),
+ elf_symbol_compare);
+
+ sdata->next = NULL;
+ sdata->symbols = elf_symbols;
+ sdata->count = elf_symbol_count;
+
+ return 1;
+}
+
+/* Add EDATA to the list in STATE. */
+
+static void
+elf_add_syminfo_data (struct backtrace_state *state,
+ struct elf_syminfo_data *edata)
+{
+ if (!state->threaded)
+ {
+ struct elf_syminfo_data **pp;
+
+ for (pp = (struct elf_syminfo_data **) (void *) &state->syminfo_data;
+ *pp != NULL;
+ pp = &(*pp)->next)
+ ;
+ *pp = edata;
+ }
+ else
+ {
+ while (1)
+ {
+ struct elf_syminfo_data **pp;
+
+ pp = (struct elf_syminfo_data **) (void *) &state->syminfo_data;
+
+ while (1)
+ {
+ struct elf_syminfo_data *p;
+
+ p = backtrace_atomic_load_pointer (pp);
+
+ if (p == NULL)
+ break;
+
+ pp = &p->next;
+ }
+
+ if (__sync_bool_compare_and_swap (pp, NULL, edata))
+ break;
+ }
+ }
+}
+
+/* Return the symbol name and value for an ADDR. */
+
+static void
+elf_syminfo (struct backtrace_state *state, uintptr_t addr,
+ backtrace_syminfo_callback callback,
+ backtrace_error_callback error_callback ATTRIBUTE_UNUSED,
+ void *data)
+{
+ struct elf_syminfo_data *edata;
+ struct elf_symbol *sym = NULL;
+
+ if (!state->threaded)
+ {
+ for (edata = (struct elf_syminfo_data *) state->syminfo_data;
+ edata != NULL;
+ edata = edata->next)
+ {
+ sym = ((struct elf_symbol *)
+ bsearch (&addr, edata->symbols, edata->count,
+ sizeof (struct elf_symbol), elf_symbol_search));
+ if (sym != NULL)
+ break;
+ }
+ }
+ else
+ {
+ struct elf_syminfo_data **pp;
+
+ pp = (struct elf_syminfo_data **) (void *) &state->syminfo_data;
+ while (1)
+ {
+ edata = backtrace_atomic_load_pointer (pp);
+ if (edata == NULL)
+ break;
+
+ sym = ((struct elf_symbol *)
+ bsearch (&addr, edata->symbols, edata->count,
+ sizeof (struct elf_symbol), elf_symbol_search));
+ if (sym != NULL)
+ break;
+
+ pp = &edata->next;
+ }
+ }
+
+ if (sym == NULL)
+ callback (data, addr, NULL, 0, 0);
+ else
+ callback (data, addr, sym->name, sym->address, sym->size);
+}
+
+/* Return whether FILENAME is a symlink. */
+
+static int
+elf_is_symlink (const char *filename)
+{
+ struct stat st;
+
+ if (lstat (filename, &st) < 0)
+ return 0;
+ return S_ISLNK (st.st_mode);
+}
+
+/* Return the results of reading the symlink FILENAME in a buffer
+ allocated by backtrace_alloc. Return the length of the buffer in
+ *LEN. */
+
+static char *
+elf_readlink (struct backtrace_state *state, const char *filename,
+ backtrace_error_callback error_callback, void *data,
+ size_t *plen)
+{
+ size_t len;
+ char *buf;
+
+ len = 128;
+ while (1)
+ {
+ ssize_t rl;
+
+ buf = backtrace_alloc (state, len, error_callback, data);
+ if (buf == NULL)
+ return NULL;
+ rl = readlink (filename, buf, len);
+ if (rl < 0)
+ {
+ backtrace_free (state, buf, len, error_callback, data);
+ return NULL;
+ }
+ if ((size_t) rl < len - 1)
+ {
+ buf[rl] = '\0';
+ *plen = len;
+ return buf;
+ }
+ backtrace_free (state, buf, len, error_callback, data);
+ len *= 2;
+ }
+}
+
+#define SYSTEM_BUILD_ID_DIR "/usr/lib/debug/.build-id/"
+
+/* Open a separate debug info file, using the build ID to find it.
+ Returns an open file descriptor, or -1.
+
+ The GDB manual says that the only place gdb looks for a debug file
+ when the build ID is known is in /usr/lib/debug/.build-id. */
+
+static int
+elf_open_debugfile_by_buildid (struct backtrace_state *state,
+ const char *buildid_data, size_t buildid_size,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ const char * const prefix = SYSTEM_BUILD_ID_DIR;
+ const size_t prefix_len = strlen (prefix);
+ const char * const suffix = ".debug";
+ const size_t suffix_len = strlen (suffix);
+ size_t len;
+ char *bd_filename;
+ char *t;
+ size_t i;
+ int ret;
+ int does_not_exist;
+
+ len = prefix_len + buildid_size * 2 + suffix_len + 2;
+ bd_filename = backtrace_alloc (state, len, error_callback, data);
+ if (bd_filename == NULL)
+ return -1;
+
+ t = bd_filename;
+ memcpy (t, prefix, prefix_len);
+ t += prefix_len;
+ for (i = 0; i < buildid_size; i++)
+ {
+ unsigned char b;
+ unsigned char nib;
+
+ b = (unsigned char) buildid_data[i];
+ nib = (b & 0xf0) >> 4;
+ *t++ = nib < 10 ? '0' + nib : 'a' + nib - 10;
+ nib = b & 0x0f;
+ *t++ = nib < 10 ? '0' + nib : 'a' + nib - 10;
+ if (i == 0)
+ *t++ = '/';
+ }
+ memcpy (t, suffix, suffix_len);
+ t[suffix_len] = '\0';
+
+ ret = backtrace_open (bd_filename, error_callback, data, &does_not_exist);
+
+ backtrace_free (state, bd_filename, len, error_callback, data);
+
+ /* gdb checks that the debuginfo file has the same build ID note.
+ That seems kind of pointless to me--why would it have the right
+ name but not the right build ID?--so skipping the check. */
+
+ return ret;
+}
+
+/* Try to open a file whose name is PREFIX (length PREFIX_LEN)
+ concatenated with PREFIX2 (length PREFIX2_LEN) concatenated with
+ DEBUGLINK_NAME. Returns an open file descriptor, or -1. */
+
+static int
+elf_try_debugfile (struct backtrace_state *state, const char *prefix,
+ size_t prefix_len, const char *prefix2, size_t prefix2_len,
+ const char *debuglink_name,
+ backtrace_error_callback error_callback, void *data)
+{
+ size_t debuglink_len;
+ size_t try_len;
+ char *try;
+ int does_not_exist;
+ int ret;
+
+ debuglink_len = strlen (debuglink_name);
+ try_len = prefix_len + prefix2_len + debuglink_len + 1;
+ try = backtrace_alloc (state, try_len, error_callback, data);
+ if (try == NULL)
+ return -1;
+
+ memcpy (try, prefix, prefix_len);
+ memcpy (try + prefix_len, prefix2, prefix2_len);
+ memcpy (try + prefix_len + prefix2_len, debuglink_name, debuglink_len);
+ try[prefix_len + prefix2_len + debuglink_len] = '\0';
+
+ ret = backtrace_open (try, error_callback, data, &does_not_exist);
+
+ backtrace_free (state, try, try_len, error_callback, data);
+
+ return ret;
+}
+
+/* Find a separate debug info file, using the debuglink section data
+ to find it. Returns an open file descriptor, or -1. */
+
+static int
+elf_find_debugfile_by_debuglink (struct backtrace_state *state,
+ const char *filename,
+ const char *debuglink_name,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ int ret;
+ char *alc;
+ size_t alc_len;
+ const char *slash;
+ int ddescriptor;
+ const char *prefix;
+ size_t prefix_len;
+
+ /* Resolve symlinks in FILENAME. Since FILENAME is fairly likely to
+ be /proc/self/exe, symlinks are common. We don't try to resolve
+ the whole path name, just the base name. */
+ ret = -1;
+ alc = NULL;
+ alc_len = 0;
+ while (elf_is_symlink (filename))
+ {
+ char *new_buf;
+ size_t new_len;
+
+ new_buf = elf_readlink (state, filename, error_callback, data, &new_len);
+ if (new_buf == NULL)
+ break;
+
+ if (new_buf[0] == '/')
+ filename = new_buf;
+ else
+ {
+ slash = strrchr (filename, '/');
+ if (slash == NULL)
+ filename = new_buf;
+ else
+ {
+ size_t clen;
+ char *c;
+
+ slash++;
+ clen = slash - filename + strlen (new_buf) + 1;
+ c = backtrace_alloc (state, clen, error_callback, data);
+ if (c == NULL)
+ goto done;
+
+ memcpy (c, filename, slash - filename);
+ memcpy (c + (slash - filename), new_buf, strlen (new_buf));
+ c[slash - filename + strlen (new_buf)] = '\0';
+ backtrace_free (state, new_buf, new_len, error_callback, data);
+ filename = c;
+ new_buf = c;
+ new_len = clen;
+ }
+ }
+
+ if (alc != NULL)
+ backtrace_free (state, alc, alc_len, error_callback, data);
+ alc = new_buf;
+ alc_len = new_len;
+ }
+
+ /* Look for DEBUGLINK_NAME in the same directory as FILENAME. */
+
+ slash = strrchr (filename, '/');
+ if (slash == NULL)
+ {
+ prefix = "";
+ prefix_len = 0;
+ }
+ else
+ {
+ slash++;
+ prefix = filename;
+ prefix_len = slash - filename;
+ }
+
+ ddescriptor = elf_try_debugfile (state, prefix, prefix_len, "", 0,
+ debuglink_name, error_callback, data);
+ if (ddescriptor >= 0)
+ {
+ ret = ddescriptor;
+ goto done;
+ }
+
+ /* Look for DEBUGLINK_NAME in a .debug subdirectory of FILENAME. */
+
+ ddescriptor = elf_try_debugfile (state, prefix, prefix_len, ".debug/",
+ strlen (".debug/"), debuglink_name,
+ error_callback, data);
+ if (ddescriptor >= 0)
+ {
+ ret = ddescriptor;
+ goto done;
+ }
+
+ /* Look for DEBUGLINK_NAME in /usr/lib/debug. */
+
+ ddescriptor = elf_try_debugfile (state, "/usr/lib/debug/",
+ strlen ("/usr/lib/debug/"), prefix,
+ prefix_len, debuglink_name,
+ error_callback, data);
+ if (ddescriptor >= 0)
+ ret = ddescriptor;
+
+ done:
+ if (alc != NULL && alc_len > 0)
+ backtrace_free (state, alc, alc_len, error_callback, data);
+ return ret;
+}
+
+/* Open a separate debug info file, using the debuglink section data
+ to find it. Returns an open file descriptor, or -1. */
+
+static int
+elf_open_debugfile_by_debuglink (struct backtrace_state *state,
+ const char *filename,
+ const char *debuglink_name,
+ uint32_t debuglink_crc,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ int ddescriptor;
+
+ ddescriptor = elf_find_debugfile_by_debuglink (state, filename,
+ debuglink_name,
+ error_callback, data);
+ if (ddescriptor < 0)
+ return -1;
+
+ if (debuglink_crc != 0)
+ {
+ uint32_t got_crc;
+
+ got_crc = elf_crc32_file (state, ddescriptor, error_callback, data);
+ if (got_crc != debuglink_crc)
+ {
+ backtrace_close (ddescriptor, error_callback, data);
+ return -1;
+ }
+ }
+
+ return ddescriptor;
+}
+
+/* A function useful for setting a breakpoint for an inflation failure
+ when this code is compiled with -g. */
+
+static void
+elf_uncompress_failed(void)
+{
+}
+
+/* *PVAL is the current value being read from the stream, and *PBITS
+ is the number of valid bits. Ensure that *PVAL holds at least 15
+ bits by reading additional bits from *PPIN, up to PINEND, as
+ needed. Updates *PPIN, *PVAL and *PBITS. Returns 1 on success, 0
+ on error. */
+
+static int
+elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend,
+ uint64_t *pval, unsigned int *pbits)
+{
+ unsigned int bits;
+ const unsigned char *pin;
+ uint64_t val;
+ uint32_t next;
+
+ bits = *pbits;
+ if (bits >= 15)
+ return 1;
+ pin = *ppin;
+ val = *pval;
+
+ if (unlikely (pinend - pin < 4))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) \
+ && defined(__ORDER_BIG_ENDIAN__) \
+ && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ \
+ || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+ /* We've ensured that PIN is aligned. */
+ next = *(const uint32_t *)pin;
+
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ next = __builtin_bswap32 (next);
+#endif
+#else
+ next = pin[0] | (pin[1] << 8) | (pin[2] << 16) | (pin[3] << 24);
+#endif
+
+ val |= (uint64_t)next << bits;
+ bits += 32;
+ pin += 4;
+
+ /* We will need the next four bytes soon. */
+ __builtin_prefetch (pin, 0, 0);
+
+ *ppin = pin;
+ *pval = val;
+ *pbits = bits;
+ return 1;
+}
+
+/* Huffman code tables, like the rest of the zlib format, are defined
+ by RFC 1951. We store a Huffman code table as a series of tables
+ stored sequentially in memory. Each entry in a table is 16 bits.
+ The first, main, table has 256 entries. It is followed by a set of
+ secondary tables of length 2 to 128 entries. The maximum length of
+ a code sequence in the deflate format is 15 bits, so that is all we
+ need. Each secondary table has an index, which is the offset of
+ the table in the overall memory storage.
+
+ The deflate format says that all codes of a given bit length are
+ lexicographically consecutive. Perhaps we could have 130 values
+ that require a 15-bit code, perhaps requiring three secondary
+ tables of size 128. I don't know if this is actually possible, but
+ it suggests that the maximum size required for secondary tables is
+ 3 * 128 + 3 * 64 ... == 768. The zlib enough program reports 660
+ as the maximum. We permit 768, since in addition to the 256 for
+ the primary table, with two bytes per entry, and with the two
+ tables we need, that gives us a page.
+
+ A single table entry needs to store a value or (for the main table
+ only) the index and size of a secondary table. Values range from 0
+ to 285, inclusive. Secondary table indexes, per above, range from
+ 0 to 510. For a value we need to store the number of bits we need
+ to determine that value (one value may appear multiple times in the
+ table), which is 1 to 8. For a secondary table we need to store
+ the number of bits used to index into the table, which is 1 to 7.
+ And of course we need 1 bit to decide whether we have a value or a
+ secondary table index. So each entry needs 9 bits for value/table
+ index, 3 bits for size, 1 bit what it is. For simplicity we use 16
+ bits per entry. */
+
+/* Number of entries we allocate to for one code table. We get a page
+ for the two code tables we need. */
+
+#define HUFFMAN_TABLE_SIZE (1024)
+
+/* Bit masks and shifts for the values in the table. */
+
+#define HUFFMAN_VALUE_MASK 0x01ff
+#define HUFFMAN_BITS_SHIFT 9
+#define HUFFMAN_BITS_MASK 0x7
+#define HUFFMAN_SECONDARY_SHIFT 12
+
+/* For working memory while inflating we need two code tables, we need
+ an array of code lengths (max value 15, so we use unsigned char),
+ and an array of unsigned shorts used while building a table. The
+ latter two arrays must be large enough to hold the maximum number
+ of code lengths, which RFC 1951 defines as 286 + 30. */
+
+#define ZDEBUG_TABLE_SIZE \
+ (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \
+ + (286 + 30) * sizeof (uint16_t) \
+ + (286 + 30) * sizeof (unsigned char))
+
+#define ZDEBUG_TABLE_CODELEN_OFFSET \
+ (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \
+ + (286 + 30) * sizeof (uint16_t))
+
+#define ZDEBUG_TABLE_WORK_OFFSET \
+ (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t))
+
+#ifdef BACKTRACE_GENERATE_FIXED_HUFFMAN_TABLE
+
+/* Used by the main function that generates the fixed table to learn
+ the table size. */
+static size_t final_next_secondary;
+
+#endif
+
+/* Build a Huffman code table from an array of lengths in CODES of
+ length CODES_LEN. The table is stored into *TABLE. ZDEBUG_TABLE
+ is the same as for elf_zlib_inflate, used to find some work space.
+ Returns 1 on success, 0 on error. */
+
+static int
+elf_zlib_inflate_table (unsigned char *codes, size_t codes_len,
+ uint16_t *zdebug_table, uint16_t *table)
+{
+ uint16_t count[16];
+ uint16_t start[16];
+ uint16_t prev[16];
+ uint16_t firstcode[7];
+ uint16_t *next;
+ size_t i;
+ size_t j;
+ unsigned int code;
+ size_t next_secondary;
+
+ /* Count the number of code of each length. Set NEXT[val] to be the
+ next value after VAL with the same bit length. */
+
+ next = (uint16_t *) (((unsigned char *) zdebug_table)
+ + ZDEBUG_TABLE_WORK_OFFSET);
+
+ memset (&count[0], 0, 16 * sizeof (uint16_t));
+ for (i = 0; i < codes_len; ++i)
+ {
+ if (unlikely (codes[i] >= 16))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (count[codes[i]] == 0)
+ {
+ start[codes[i]] = i;
+ prev[codes[i]] = i;
+ }
+ else
+ {
+ next[prev[codes[i]]] = i;
+ prev[codes[i]] = i;
+ }
+
+ ++count[codes[i]];
+ }
+
+ /* For each length, fill in the table for the codes of that
+ length. */
+
+ memset (table, 0, HUFFMAN_TABLE_SIZE * sizeof (uint16_t));
+
+ /* Handle the values that do not require a secondary table. */
+
+ code = 0;
+ for (j = 1; j <= 8; ++j)
+ {
+ unsigned int jcnt;
+ unsigned int val;
+
+ jcnt = count[j];
+ if (jcnt == 0)
+ continue;
+
+ if (unlikely (jcnt > (1U << j)))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* There are JCNT values that have this length, the values
+ starting from START[j] continuing through NEXT[VAL]. Those
+ values are assigned consecutive values starting at CODE. */
+
+ val = start[j];
+ for (i = 0; i < jcnt; ++i)
+ {
+ uint16_t tval;
+ size_t ind;
+ unsigned int incr;
+
+ /* In the compressed bit stream, the value VAL is encoded as
+ J bits with the value C. */
+
+ if (unlikely ((val & ~HUFFMAN_VALUE_MASK) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ tval = val | ((j - 1) << HUFFMAN_BITS_SHIFT);
+
+ /* The table lookup uses 8 bits. If J is less than 8, we
+ don't know what the other bits will be. We need to fill
+ in all possibilities in the table. Since the Huffman
+ code is unambiguous, those entries can't be used for any
+ other code. */
+
+ for (ind = code; ind < 0x100; ind += 1 << j)
+ {
+ if (unlikely (table[ind] != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ table[ind] = tval;
+ }
+
+ /* Advance to the next value with this length. */
+ if (i + 1 < jcnt)
+ val = next[val];
+
+ /* The Huffman codes are stored in the bitstream with the
+ most significant bit first, as is required to make them
+ unambiguous. The effect is that when we read them from
+ the bitstream we see the bit sequence in reverse order:
+ the most significant bit of the Huffman code is the least
+ significant bit of the value we read from the bitstream.
+ That means that to make our table lookups work, we need
+ to reverse the bits of CODE. Since reversing bits is
+ tedious and in general requires using a table, we instead
+ increment CODE in reverse order. That is, if the number
+ of bits we are currently using, here named J, is 3, we
+ count as 000, 100, 010, 110, 001, 101, 011, 111, which is
+ to say the numbers from 0 to 7 but with the bits
+ reversed. Going to more bits, aka incrementing J,
+ effectively just adds more zero bits as the beginning,
+ and as such does not change the numeric value of CODE.
+
+ To increment CODE of length J in reverse order, find the
+ most significant zero bit and set it to one while
+ clearing all higher bits. In other words, add 1 modulo
+ 2^J, only reversed. */
+
+ incr = 1U << (j - 1);
+ while ((code & incr) != 0)
+ incr >>= 1;
+ if (incr == 0)
+ code = 0;
+ else
+ {
+ code &= incr - 1;
+ code += incr;
+ }
+ }
+ }
+
+ /* Handle the values that require a secondary table. */
+
+ /* Set FIRSTCODE, the number at which the codes start, for each
+ length. */
+
+ for (j = 9; j < 16; j++)
+ {
+ unsigned int jcnt;
+ unsigned int k;
+
+ jcnt = count[j];
+ if (jcnt == 0)
+ continue;
+
+ /* There are JCNT values that have this length, the values
+ starting from START[j]. Those values are assigned
+ consecutive values starting at CODE. */
+
+ firstcode[j - 9] = code;
+
+ /* Reverse add JCNT to CODE modulo 2^J. */
+ for (k = 0; k < j; ++k)
+ {
+ if ((jcnt & (1U << k)) != 0)
+ {
+ unsigned int m;
+ unsigned int bit;
+
+ bit = 1U << (j - k - 1);
+ for (m = 0; m < j - k; ++m, bit >>= 1)
+ {
+ if ((code & bit) == 0)
+ {
+ code += bit;
+ break;
+ }
+ code &= ~bit;
+ }
+ jcnt &= ~(1U << k);
+ }
+ }
+ if (unlikely (jcnt != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ }
+
+ /* For J from 9 to 15, inclusive, we store COUNT[J] consecutive
+ values starting at START[J] with consecutive codes starting at
+ FIRSTCODE[J - 9]. In the primary table we need to point to the
+ secondary table, and the secondary table will be indexed by J - 9
+ bits. We count down from 15 so that we install the larger
+ secondary tables first, as the smaller ones may be embedded in
+ the larger ones. */
+
+ next_secondary = 0; /* Index of next secondary table (after primary). */
+ for (j = 15; j >= 9; j--)
+ {
+ unsigned int jcnt;
+ unsigned int val;
+ size_t primary; /* Current primary index. */
+ size_t secondary; /* Offset to current secondary table. */
+ size_t secondary_bits; /* Bit size of current secondary table. */
+
+ jcnt = count[j];
+ if (jcnt == 0)
+ continue;
+
+ val = start[j];
+ code = firstcode[j - 9];
+ primary = 0x100;
+ secondary = 0;
+ secondary_bits = 0;
+ for (i = 0; i < jcnt; ++i)
+ {
+ uint16_t tval;
+ size_t ind;
+ unsigned int incr;
+
+ if ((code & 0xff) != primary)
+ {
+ uint16_t tprimary;
+
+ /* Fill in a new primary table entry. */
+
+ primary = code & 0xff;
+
+ tprimary = table[primary];
+ if (tprimary == 0)
+ {
+ /* Start a new secondary table. */
+
+ if (unlikely ((next_secondary & HUFFMAN_VALUE_MASK)
+ != next_secondary))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ secondary = next_secondary;
+ secondary_bits = j - 8;
+ next_secondary += 1 << secondary_bits;
+ table[primary] = (secondary
+ + ((j - 8) << HUFFMAN_BITS_SHIFT)
+ + (1U << HUFFMAN_SECONDARY_SHIFT));
+ }
+ else
+ {
+ /* There is an existing entry. It had better be a
+ secondary table with enough bits. */
+ if (unlikely ((tprimary & (1U << HUFFMAN_SECONDARY_SHIFT))
+ == 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ secondary = tprimary & HUFFMAN_VALUE_MASK;
+ secondary_bits = ((tprimary >> HUFFMAN_BITS_SHIFT)
+ & HUFFMAN_BITS_MASK);
+ if (unlikely (secondary_bits < j - 8))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ }
+ }
+
+ /* Fill in secondary table entries. */
+
+ tval = val | ((j - 8) << HUFFMAN_BITS_SHIFT);
+
+ for (ind = code >> 8;
+ ind < (1U << secondary_bits);
+ ind += 1U << (j - 8))
+ {
+ if (unlikely (table[secondary + 0x100 + ind] != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ table[secondary + 0x100 + ind] = tval;
+ }
+
+ if (i + 1 < jcnt)
+ val = next[val];
+
+ incr = 1U << (j - 1);
+ while ((code & incr) != 0)
+ incr >>= 1;
+ if (incr == 0)
+ code = 0;
+ else
+ {
+ code &= incr - 1;
+ code += incr;
+ }
+ }
+ }
+
+#ifdef BACKTRACE_GENERATE_FIXED_HUFFMAN_TABLE
+ final_next_secondary = next_secondary;
+#endif
+
+ return 1;
+}
+
+#ifdef BACKTRACE_GENERATE_FIXED_HUFFMAN_TABLE
+
+/* Used to generate the fixed Huffman table for block type 1. */
+
+#include <stdio.h>
+
+static uint16_t table[ZDEBUG_TABLE_SIZE];
+static unsigned char codes[288];
+
+int
+main ()
+{
+ size_t i;
+
+ for (i = 0; i <= 143; ++i)
+ codes[i] = 8;
+ for (i = 144; i <= 255; ++i)
+ codes[i] = 9;
+ for (i = 256; i <= 279; ++i)
+ codes[i] = 7;
+ for (i = 280; i <= 287; ++i)
+ codes[i] = 8;
+ if (!elf_zlib_inflate_table (&codes[0], 288, &table[0], &table[0]))
+ {
+ fprintf (stderr, "elf_zlib_inflate_table failed\n");
+ exit (EXIT_FAILURE);
+ }
+
+ printf ("static const uint16_t elf_zlib_default_table[%#zx] =\n",
+ final_next_secondary + 0x100);
+ printf ("{\n");
+ for (i = 0; i < final_next_secondary + 0x100; i += 8)
+ {
+ size_t j;
+
+ printf (" ");
+ for (j = i; j < final_next_secondary + 0x100 && j < i + 8; ++j)
+ printf (" %#x,", table[j]);
+ printf ("\n");
+ }
+ printf ("};\n");
+ printf ("\n");
+
+ for (i = 0; i < 32; ++i)
+ codes[i] = 5;
+ if (!elf_zlib_inflate_table (&codes[0], 32, &table[0], &table[0]))
+ {
+ fprintf (stderr, "elf_zlib_inflate_table failed\n");
+ exit (EXIT_FAILURE);
+ }
+
+ printf ("static const uint16_t elf_zlib_default_dist_table[%#zx] =\n",
+ final_next_secondary + 0x100);
+ printf ("{\n");
+ for (i = 0; i < final_next_secondary + 0x100; i += 8)
+ {
+ size_t j;
+
+ printf (" ");
+ for (j = i; j < final_next_secondary + 0x100 && j < i + 8; ++j)
+ printf (" %#x,", table[j]);
+ printf ("\n");
+ }
+ printf ("};\n");
+
+ return 0;
+}
+
+#endif
+
+/* The fixed tables generated by the #ifdef'ed out main function
+ above. */
+
+static const uint16_t elf_zlib_default_table[0x170] =
+{
+ 0xd00, 0xe50, 0xe10, 0xf18, 0xd10, 0xe70, 0xe30, 0x1230,
+ 0xd08, 0xe60, 0xe20, 0x1210, 0xe00, 0xe80, 0xe40, 0x1250,
+ 0xd04, 0xe58, 0xe18, 0x1200, 0xd14, 0xe78, 0xe38, 0x1240,
+ 0xd0c, 0xe68, 0xe28, 0x1220, 0xe08, 0xe88, 0xe48, 0x1260,
+ 0xd02, 0xe54, 0xe14, 0xf1c, 0xd12, 0xe74, 0xe34, 0x1238,
+ 0xd0a, 0xe64, 0xe24, 0x1218, 0xe04, 0xe84, 0xe44, 0x1258,
+ 0xd06, 0xe5c, 0xe1c, 0x1208, 0xd16, 0xe7c, 0xe3c, 0x1248,
+ 0xd0e, 0xe6c, 0xe2c, 0x1228, 0xe0c, 0xe8c, 0xe4c, 0x1268,
+ 0xd01, 0xe52, 0xe12, 0xf1a, 0xd11, 0xe72, 0xe32, 0x1234,
+ 0xd09, 0xe62, 0xe22, 0x1214, 0xe02, 0xe82, 0xe42, 0x1254,
+ 0xd05, 0xe5a, 0xe1a, 0x1204, 0xd15, 0xe7a, 0xe3a, 0x1244,
+ 0xd0d, 0xe6a, 0xe2a, 0x1224, 0xe0a, 0xe8a, 0xe4a, 0x1264,
+ 0xd03, 0xe56, 0xe16, 0xf1e, 0xd13, 0xe76, 0xe36, 0x123c,
+ 0xd0b, 0xe66, 0xe26, 0x121c, 0xe06, 0xe86, 0xe46, 0x125c,
+ 0xd07, 0xe5e, 0xe1e, 0x120c, 0xd17, 0xe7e, 0xe3e, 0x124c,
+ 0xd0f, 0xe6e, 0xe2e, 0x122c, 0xe0e, 0xe8e, 0xe4e, 0x126c,
+ 0xd00, 0xe51, 0xe11, 0xf19, 0xd10, 0xe71, 0xe31, 0x1232,
+ 0xd08, 0xe61, 0xe21, 0x1212, 0xe01, 0xe81, 0xe41, 0x1252,
+ 0xd04, 0xe59, 0xe19, 0x1202, 0xd14, 0xe79, 0xe39, 0x1242,
+ 0xd0c, 0xe69, 0xe29, 0x1222, 0xe09, 0xe89, 0xe49, 0x1262,
+ 0xd02, 0xe55, 0xe15, 0xf1d, 0xd12, 0xe75, 0xe35, 0x123a,
+ 0xd0a, 0xe65, 0xe25, 0x121a, 0xe05, 0xe85, 0xe45, 0x125a,
+ 0xd06, 0xe5d, 0xe1d, 0x120a, 0xd16, 0xe7d, 0xe3d, 0x124a,
+ 0xd0e, 0xe6d, 0xe2d, 0x122a, 0xe0d, 0xe8d, 0xe4d, 0x126a,
+ 0xd01, 0xe53, 0xe13, 0xf1b, 0xd11, 0xe73, 0xe33, 0x1236,
+ 0xd09, 0xe63, 0xe23, 0x1216, 0xe03, 0xe83, 0xe43, 0x1256,
+ 0xd05, 0xe5b, 0xe1b, 0x1206, 0xd15, 0xe7b, 0xe3b, 0x1246,
+ 0xd0d, 0xe6b, 0xe2b, 0x1226, 0xe0b, 0xe8b, 0xe4b, 0x1266,
+ 0xd03, 0xe57, 0xe17, 0xf1f, 0xd13, 0xe77, 0xe37, 0x123e,
+ 0xd0b, 0xe67, 0xe27, 0x121e, 0xe07, 0xe87, 0xe47, 0x125e,
+ 0xd07, 0xe5f, 0xe1f, 0x120e, 0xd17, 0xe7f, 0xe3f, 0x124e,
+ 0xd0f, 0xe6f, 0xe2f, 0x122e, 0xe0f, 0xe8f, 0xe4f, 0x126e,
+ 0x290, 0x291, 0x292, 0x293, 0x294, 0x295, 0x296, 0x297,
+ 0x298, 0x299, 0x29a, 0x29b, 0x29c, 0x29d, 0x29e, 0x29f,
+ 0x2a0, 0x2a1, 0x2a2, 0x2a3, 0x2a4, 0x2a5, 0x2a6, 0x2a7,
+ 0x2a8, 0x2a9, 0x2aa, 0x2ab, 0x2ac, 0x2ad, 0x2ae, 0x2af,
+ 0x2b0, 0x2b1, 0x2b2, 0x2b3, 0x2b4, 0x2b5, 0x2b6, 0x2b7,
+ 0x2b8, 0x2b9, 0x2ba, 0x2bb, 0x2bc, 0x2bd, 0x2be, 0x2bf,
+ 0x2c0, 0x2c1, 0x2c2, 0x2c3, 0x2c4, 0x2c5, 0x2c6, 0x2c7,
+ 0x2c8, 0x2c9, 0x2ca, 0x2cb, 0x2cc, 0x2cd, 0x2ce, 0x2cf,
+ 0x2d0, 0x2d1, 0x2d2, 0x2d3, 0x2d4, 0x2d5, 0x2d6, 0x2d7,
+ 0x2d8, 0x2d9, 0x2da, 0x2db, 0x2dc, 0x2dd, 0x2de, 0x2df,
+ 0x2e0, 0x2e1, 0x2e2, 0x2e3, 0x2e4, 0x2e5, 0x2e6, 0x2e7,
+ 0x2e8, 0x2e9, 0x2ea, 0x2eb, 0x2ec, 0x2ed, 0x2ee, 0x2ef,
+ 0x2f0, 0x2f1, 0x2f2, 0x2f3, 0x2f4, 0x2f5, 0x2f6, 0x2f7,
+ 0x2f8, 0x2f9, 0x2fa, 0x2fb, 0x2fc, 0x2fd, 0x2fe, 0x2ff,
+};
+
+static const uint16_t elf_zlib_default_dist_table[0x100] =
+{
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+ 0x800, 0x810, 0x808, 0x818, 0x804, 0x814, 0x80c, 0x81c,
+ 0x802, 0x812, 0x80a, 0x81a, 0x806, 0x816, 0x80e, 0x81e,
+ 0x801, 0x811, 0x809, 0x819, 0x805, 0x815, 0x80d, 0x81d,
+ 0x803, 0x813, 0x80b, 0x81b, 0x807, 0x817, 0x80f, 0x81f,
+};
+
+/* Inflate a zlib stream from PIN/SIN to POUT/SOUT. Return 1 on
+ success, 0 on some error parsing the stream. */
+
+static int
+elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table,
+ unsigned char *pout, size_t sout)
+{
+ unsigned char *porigout;
+ const unsigned char *pinend;
+ unsigned char *poutend;
+
+ /* We can apparently see multiple zlib streams concatenated
+ together, so keep going as long as there is something to read.
+ The last 4 bytes are the checksum. */
+ porigout = pout;
+ pinend = pin + sin;
+ poutend = pout + sout;
+ while ((pinend - pin) > 4)
+ {
+ uint64_t val;
+ unsigned int bits;
+ int last;
+
+ /* Read the two byte zlib header. */
+
+ if (unlikely ((pin[0] & 0xf) != 8)) /* 8 is zlib encoding. */
+ {
+ /* Unknown compression method. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ if (unlikely ((pin[0] >> 4) > 7))
+ {
+ /* Window size too large. Other than this check, we don't
+ care about the window size. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ if (unlikely ((pin[1] & 0x20) != 0))
+ {
+ /* Stream expects a predefined dictionary, but we have no
+ dictionary. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ val = (pin[0] << 8) | pin[1];
+ if (unlikely (val % 31 != 0))
+ {
+ /* Header check failure. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ pin += 2;
+
+ /* Align PIN to a 32-bit boundary. */
+
+ val = 0;
+ bits = 0;
+ while ((((uintptr_t) pin) & 3) != 0)
+ {
+ val |= (uint64_t)*pin << bits;
+ bits += 8;
+ ++pin;
+ }
+
+ /* Read blocks until one is marked last. */
+
+ last = 0;
+
+ while (!last)
+ {
+ unsigned int type;
+ const uint16_t *tlit;
+ const uint16_t *tdist;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ last = val & 1;
+ type = (val >> 1) & 3;
+ val >>= 3;
+ bits -= 3;
+
+ if (unlikely (type == 3))
+ {
+ /* Invalid block type. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (type == 0)
+ {
+ uint16_t len;
+ uint16_t lenc;
+
+ /* An uncompressed block. */
+
+ /* If we've read ahead more than a byte, back up. */
+ while (bits >= 8)
+ {
+ --pin;
+ bits -= 8;
+ }
+
+ val = 0;
+ bits = 0;
+ if (unlikely ((pinend - pin) < 4))
+ {
+ /* Missing length. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ len = pin[0] | (pin[1] << 8);
+ lenc = pin[2] | (pin[3] << 8);
+ pin += 4;
+ lenc = ~lenc;
+ if (unlikely (len != lenc))
+ {
+ /* Corrupt data. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ if (unlikely (len > (unsigned int) (pinend - pin)
+ || len > (unsigned int) (poutend - pout)))
+ {
+ /* Not enough space in buffers. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+ memcpy (pout, pin, len);
+ pout += len;
+ pin += len;
+
+ /* Align PIN. */
+ while ((((uintptr_t) pin) & 3) != 0)
+ {
+ val |= (uint64_t)*pin << bits;
+ bits += 8;
+ ++pin;
+ }
+
+ /* Go around to read the next block. */
+ continue;
+ }
+
+ if (type == 1)
+ {
+ tlit = elf_zlib_default_table;
+ tdist = elf_zlib_default_dist_table;
+ }
+ else
+ {
+ unsigned int nlit;
+ unsigned int ndist;
+ unsigned int nclen;
+ unsigned char codebits[19];
+ unsigned char *plenbase;
+ unsigned char *plen;
+ unsigned char *plenend;
+
+ /* Read a Huffman encoding table. The various magic
+ numbers here are from RFC 1951. */
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ nlit = (val & 0x1f) + 257;
+ val >>= 5;
+ ndist = (val & 0x1f) + 1;
+ val >>= 5;
+ nclen = (val & 0xf) + 4;
+ val >>= 4;
+ bits -= 14;
+ if (unlikely (nlit > 286 || ndist > 30))
+ {
+ /* Values out of range. */
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Read and build the table used to compress the
+ literal, length, and distance codes. */
+
+ memset(&codebits[0], 0, 19);
+
+ /* There are always at least 4 elements in the
+ table. */
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ codebits[16] = val & 7;
+ codebits[17] = (val >> 3) & 7;
+ codebits[18] = (val >> 6) & 7;
+ codebits[0] = (val >> 9) & 7;
+ val >>= 12;
+ bits -= 12;
+
+ if (nclen == 4)
+ goto codebitsdone;
+
+ codebits[8] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 5)
+ goto codebitsdone;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ codebits[7] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 6)
+ goto codebitsdone;
+
+ codebits[9] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 7)
+ goto codebitsdone;
+
+ codebits[6] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 8)
+ goto codebitsdone;
+
+ codebits[10] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 9)
+ goto codebitsdone;
+
+ codebits[5] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 10)
+ goto codebitsdone;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ codebits[11] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 11)
+ goto codebitsdone;
+
+ codebits[4] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 12)
+ goto codebitsdone;
+
+ codebits[12] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 13)
+ goto codebitsdone;
+
+ codebits[3] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 14)
+ goto codebitsdone;
+
+ codebits[13] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 15)
+ goto codebitsdone;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ codebits[2] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 16)
+ goto codebitsdone;
+
+ codebits[14] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 17)
+ goto codebitsdone;
+
+ codebits[1] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ if (nclen == 18)
+ goto codebitsdone;
+
+ codebits[15] = val & 7;
+ val >>= 3;
+ bits -= 3;
+
+ codebitsdone:
+
+ if (!elf_zlib_inflate_table (codebits, 19, zdebug_table,
+ zdebug_table))
+ return 0;
+
+ /* Read the compressed bit lengths of the literal,
+ length, and distance codes. We have allocated space
+ at the end of zdebug_table to hold them. */
+
+ plenbase = (((unsigned char *) zdebug_table)
+ + ZDEBUG_TABLE_CODELEN_OFFSET);
+ plen = plenbase;
+ plenend = plen + nlit + ndist;
+ while (plen < plenend)
+ {
+ uint16_t t;
+ unsigned int b;
+ uint16_t v;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ t = zdebug_table[val & 0xff];
+
+ /* The compression here uses bit lengths up to 7, so
+ a secondary table is never necessary. */
+ if (unlikely ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK;
+ val >>= b + 1;
+ bits -= b + 1;
+
+ v = t & HUFFMAN_VALUE_MASK;
+ if (v < 16)
+ *plen++ = v;
+ else if (v == 16)
+ {
+ unsigned int c;
+ unsigned int prev;
+
+ /* Copy previous entry 3 to 6 times. */
+
+ if (unlikely (plen == plenbase))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* We used up to 7 bits since the last
+ elf_zlib_fetch, so we have at least 8 bits
+ available here. */
+
+ c = 3 + (val & 0x3);
+ val >>= 2;
+ bits -= 2;
+ if (unlikely ((unsigned int) (plenend - plen) < c))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ prev = plen[-1];
+ switch (c)
+ {
+ case 6:
+ *plen++ = prev;
+ ATTRIBUTE_FALLTHROUGH;
+ case 5:
+ *plen++ = prev;
+ ATTRIBUTE_FALLTHROUGH;
+ case 4:
+ *plen++ = prev;
+ }
+ *plen++ = prev;
+ *plen++ = prev;
+ *plen++ = prev;
+ }
+ else if (v == 17)
+ {
+ unsigned int c;
+
+ /* Store zero 3 to 10 times. */
+
+ /* We used up to 7 bits since the last
+ elf_zlib_fetch, so we have at least 8 bits
+ available here. */
+
+ c = 3 + (val & 0x7);
+ val >>= 3;
+ bits -= 3;
+ if (unlikely ((unsigned int) (plenend - plen) < c))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ switch (c)
+ {
+ case 10:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 9:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 8:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 7:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 6:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 5:
+ *plen++ = 0;
+ ATTRIBUTE_FALLTHROUGH;
+ case 4:
+ *plen++ = 0;
+ }
+ *plen++ = 0;
+ *plen++ = 0;
+ *plen++ = 0;
+ }
+ else if (v == 18)
+ {
+ unsigned int c;
+
+ /* Store zero 11 to 138 times. */
+
+ /* We used up to 7 bits since the last
+ elf_zlib_fetch, so we have at least 8 bits
+ available here. */
+
+ c = 11 + (val & 0x7f);
+ val >>= 7;
+ bits -= 7;
+ if (unlikely ((unsigned int) (plenend - plen) < c))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ memset (plen, 0, c);
+ plen += c;
+ }
+ else
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ }
+
+ /* Make sure that the stop code can appear. */
+
+ plen = plenbase;
+ if (unlikely (plen[256] == 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Build the decompression tables. */
+
+ if (!elf_zlib_inflate_table (plen, nlit, zdebug_table,
+ zdebug_table))
+ return 0;
+ if (!elf_zlib_inflate_table (plen + nlit, ndist, zdebug_table,
+ zdebug_table + HUFFMAN_TABLE_SIZE))
+ return 0;
+ tlit = zdebug_table;
+ tdist = zdebug_table + HUFFMAN_TABLE_SIZE;
+ }
+
+ /* Inflate values until the end of the block. This is the
+ main loop of the inflation code. */
+
+ while (1)
+ {
+ uint16_t t;
+ unsigned int b;
+ uint16_t v;
+ unsigned int lit;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ t = tlit[val & 0xff];
+ b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK;
+ v = t & HUFFMAN_VALUE_MASK;
+
+ if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0)
+ {
+ lit = v;
+ val >>= b + 1;
+ bits -= b + 1;
+ }
+ else
+ {
+ t = tlit[v + 0x100 + ((val >> 8) & ((1U << b) - 1))];
+ b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK;
+ lit = t & HUFFMAN_VALUE_MASK;
+ val >>= b + 8;
+ bits -= b + 8;
+ }
+
+ if (lit < 256)
+ {
+ if (unlikely (pout == poutend))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ *pout++ = lit;
+
+ /* We will need to write the next byte soon. We ask
+ for high temporal locality because we will write
+ to the whole cache line soon. */
+ __builtin_prefetch (pout, 1, 3);
+ }
+ else if (lit == 256)
+ {
+ /* The end of the block. */
+ break;
+ }
+ else
+ {
+ unsigned int dist;
+ unsigned int len;
+
+ /* Convert lit into a length. */
+
+ if (lit < 265)
+ len = lit - 257 + 3;
+ else if (lit == 285)
+ len = 258;
+ else if (unlikely (lit > 285))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ else
+ {
+ unsigned int extra;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ /* This is an expression for the table of length
+ codes in RFC 1951 3.2.5. */
+ lit -= 265;
+ extra = (lit >> 2) + 1;
+ len = (lit & 3) << extra;
+ len += 11;
+ len += ((1U << (extra - 1)) - 1) << 3;
+ len += val & ((1U << extra) - 1);
+ val >>= extra;
+ bits -= extra;
+ }
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ t = tdist[val & 0xff];
+ b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK;
+ v = t & HUFFMAN_VALUE_MASK;
+
+ if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0)
+ {
+ dist = v;
+ val >>= b + 1;
+ bits -= b + 1;
+ }
+ else
+ {
+ t = tdist[v + 0x100 + ((val >> 8) & ((1U << b) - 1))];
+ b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK;
+ dist = t & HUFFMAN_VALUE_MASK;
+ val >>= b + 8;
+ bits -= b + 8;
+ }
+
+ /* Convert dist to a distance. */
+
+ if (dist == 0)
+ {
+ /* A distance of 1. A common case, meaning
+ repeat the last character LEN times. */
+
+ if (unlikely (pout == porigout))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (unlikely ((unsigned int) (poutend - pout) < len))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ memset (pout, pout[-1], len);
+ pout += len;
+ }
+ else if (unlikely (dist > 29))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ else
+ {
+ if (dist < 4)
+ dist = dist + 1;
+ else
+ {
+ unsigned int extra;
+
+ if (!elf_zlib_fetch (&pin, pinend, &val, &bits))
+ return 0;
+
+ /* This is an expression for the table of
+ distance codes in RFC 1951 3.2.5. */
+ dist -= 4;
+ extra = (dist >> 1) + 1;
+ dist = (dist & 1) << extra;
+ dist += 5;
+ dist += ((1U << (extra - 1)) - 1) << 2;
+ dist += val & ((1U << extra) - 1);
+ val >>= extra;
+ bits -= extra;
+ }
+
+ /* Go back dist bytes, and copy len bytes from
+ there. */
+
+ if (unlikely ((unsigned int) (pout - porigout) < dist))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (unlikely ((unsigned int) (poutend - pout) < len))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (dist >= len)
+ {
+ memcpy (pout, pout - dist, len);
+ pout += len;
+ }
+ else
+ {
+ while (len > 0)
+ {
+ unsigned int copy;
+
+ copy = len < dist ? len : dist;
+ memcpy (pout, pout - dist, copy);
+ len -= copy;
+ pout += copy;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /* We should have filled the output buffer. */
+ if (unlikely (pout != poutend))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ return 1;
+}
+
+/* Verify the zlib checksum. The checksum is in the 4 bytes at
+ CHECKBYTES, and the uncompressed data is at UNCOMPRESSED /
+ UNCOMPRESSED_SIZE. Returns 1 on success, 0 on failure. */
+
+static int
+elf_zlib_verify_checksum (const unsigned char *checkbytes,
+ const unsigned char *uncompressed,
+ size_t uncompressed_size)
+{
+ unsigned int i;
+ unsigned int cksum;
+ const unsigned char *p;
+ uint32_t s1;
+ uint32_t s2;
+ size_t hsz;
+
+ cksum = 0;
+ for (i = 0; i < 4; i++)
+ cksum = (cksum << 8) | checkbytes[i];
+
+ s1 = 1;
+ s2 = 0;
+
+ /* Minimize modulo operations. */
+
+ p = uncompressed;
+ hsz = uncompressed_size;
+ while (hsz >= 5552)
+ {
+ for (i = 0; i < 5552; i += 16)
+ {
+ /* Manually unroll loop 16 times. */
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ }
+ hsz -= 5552;
+ s1 %= 65521;
+ s2 %= 65521;
+ }
+
+ while (hsz >= 16)
+ {
+ /* Manually unroll loop 16 times. */
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+
+ hsz -= 16;
+ }
+
+ for (i = 0; i < hsz; ++i)
+ {
+ s1 = s1 + *p++;
+ s2 = s2 + s1;
+ }
+
+ s1 %= 65521;
+ s2 %= 65521;
+
+ if (unlikely ((s2 << 16) + s1 != cksum))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ return 1;
+}
+
+/* Inflate a zlib stream from PIN/SIN to POUT/SOUT, and verify the
+ checksum. Return 1 on success, 0 on error. */
+
+static int
+elf_zlib_inflate_and_verify (const unsigned char *pin, size_t sin,
+ uint16_t *zdebug_table, unsigned char *pout,
+ size_t sout)
+{
+ if (!elf_zlib_inflate (pin, sin, zdebug_table, pout, sout))
+ return 0;
+ if (!elf_zlib_verify_checksum (pin + sin - 4, pout, sout))
+ return 0;
+ return 1;
+}
+
+/* Uncompress the old compressed debug format, the one emitted by
+ --compress-debug-sections=zlib-gnu. The compressed data is in
+ COMPRESSED / COMPRESSED_SIZE, and the function writes to
+ *UNCOMPRESSED / *UNCOMPRESSED_SIZE. ZDEBUG_TABLE is work space to
+ hold Huffman tables. Returns 0 on error, 1 on successful
+ decompression or if something goes wrong. In general we try to
+ carry on, by returning 1, even if we can't decompress. */
+
+static int
+elf_uncompress_zdebug (struct backtrace_state *state,
+ const unsigned char *compressed, size_t compressed_size,
+ uint16_t *zdebug_table,
+ backtrace_error_callback error_callback, void *data,
+ unsigned char **uncompressed, size_t *uncompressed_size)
+{
+ size_t sz;
+ size_t i;
+ unsigned char *po;
+
+ *uncompressed = NULL;
+ *uncompressed_size = 0;
+
+ /* The format starts with the four bytes ZLIB, followed by the 8
+ byte length of the uncompressed data in big-endian order,
+ followed by a zlib stream. */
+
+ if (compressed_size < 12 || memcmp (compressed, "ZLIB", 4) != 0)
+ return 1;
+
+ sz = 0;
+ for (i = 0; i < 8; i++)
+ sz = (sz << 8) | compressed[i + 4];
+
+ if (*uncompressed != NULL && *uncompressed_size >= sz)
+ po = *uncompressed;
+ else
+ {
+ po = (unsigned char *) backtrace_alloc (state, sz, error_callback, data);
+ if (po == NULL)
+ return 0;
+ }
+
+ if (!elf_zlib_inflate_and_verify (compressed + 12, compressed_size - 12,
+ zdebug_table, po, sz))
+ return 1;
+
+ *uncompressed = po;
+ *uncompressed_size = sz;
+
+ return 1;
+}
+
+/* Uncompress the new compressed debug format, the official standard
+ ELF approach emitted by --compress-debug-sections=zlib-gabi. The
+ compressed data is in COMPRESSED / COMPRESSED_SIZE, and the
+ function writes to *UNCOMPRESSED / *UNCOMPRESSED_SIZE.
+ ZDEBUG_TABLE is work space as for elf_uncompress_zdebug. Returns 0
+ on error, 1 on successful decompression or if something goes wrong.
+ In general we try to carry on, by returning 1, even if we can't
+ decompress. */
+
+static int
+elf_uncompress_chdr (struct backtrace_state *state,
+ const unsigned char *compressed, size_t compressed_size,
+ uint16_t *zdebug_table,
+ backtrace_error_callback error_callback, void *data,
+ unsigned char **uncompressed, size_t *uncompressed_size)
+{
+ const b_elf_chdr *chdr;
+ unsigned char *po;
+
+ *uncompressed = NULL;
+ *uncompressed_size = 0;
+
+ /* The format starts with an ELF compression header. */
+ if (compressed_size < sizeof (b_elf_chdr))
+ return 1;
+
+ chdr = (const b_elf_chdr *) compressed;
+
+ if (chdr->ch_type != ELFCOMPRESS_ZLIB)
+ {
+ /* Unsupported compression algorithm. */
+ return 1;
+ }
+
+ if (*uncompressed != NULL && *uncompressed_size >= chdr->ch_size)
+ po = *uncompressed;
+ else
+ {
+ po = (unsigned char *) backtrace_alloc (state, chdr->ch_size,
+ error_callback, data);
+ if (po == NULL)
+ return 0;
+ }
+
+ if (!elf_zlib_inflate_and_verify (compressed + sizeof (b_elf_chdr),
+ compressed_size - sizeof (b_elf_chdr),
+ zdebug_table, po, chdr->ch_size))
+ return 1;
+
+ *uncompressed = po;
+ *uncompressed_size = chdr->ch_size;
+
+ return 1;
+}
+
+/* This function is a hook for testing the zlib support. It is only
+ used by tests. */
+
+int
+backtrace_uncompress_zdebug (struct backtrace_state *state,
+ const unsigned char *compressed,
+ size_t compressed_size,
+ backtrace_error_callback error_callback,
+ void *data, unsigned char **uncompressed,
+ size_t *uncompressed_size)
+{
+ uint16_t *zdebug_table;
+ int ret;
+
+ zdebug_table = ((uint16_t *) backtrace_alloc (state, ZDEBUG_TABLE_SIZE,
+ error_callback, data));
+ if (zdebug_table == NULL)
+ return 0;
+ ret = elf_uncompress_zdebug (state, compressed, compressed_size,
+ zdebug_table, error_callback, data,
+ uncompressed, uncompressed_size);
+ backtrace_free (state, zdebug_table, ZDEBUG_TABLE_SIZE,
+ error_callback, data);
+ return ret;
+}
+
+/* Number of LZMA states. */
+#define LZMA_STATES (12)
+
+/* Number of LZMA position states. The pb value of the property byte
+ is the number of bits to include in these states, and the maximum
+ value of pb is 4. */
+#define LZMA_POS_STATES (16)
+
+/* Number of LZMA distance states. These are used match distances
+ with a short match length: up to 4 bytes. */
+#define LZMA_DIST_STATES (4)
+
+/* Number of LZMA distance slots. LZMA uses six bits to encode larger
+ match lengths, so 1 << 6 possible probabilities. */
+#define LZMA_DIST_SLOTS (64)
+
+/* LZMA distances 0 to 3 are encoded directly, larger values use a
+ probability model. */
+#define LZMA_DIST_MODEL_START (4)
+
+/* The LZMA probability model ends at 14. */
+#define LZMA_DIST_MODEL_END (14)
+
+/* LZMA distance slots for distances less than 127. */
+#define LZMA_FULL_DISTANCES (128)
+
+/* LZMA uses four alignment bits. */
+#define LZMA_ALIGN_SIZE (16)
+
+/* LZMA match length is encoded with 4, 5, or 10 bits, some of which
+ are already known. */
+#define LZMA_LEN_LOW_SYMBOLS (8)
+#define LZMA_LEN_MID_SYMBOLS (8)
+#define LZMA_LEN_HIGH_SYMBOLS (256)
+
+/* LZMA literal encoding. */
+#define LZMA_LITERAL_CODERS_MAX (16)
+#define LZMA_LITERAL_CODER_SIZE (0x300)
+
+/* LZMA is based on a large set of probabilities, each managed
+ independently. Each probability is an 11 bit number that we store
+ in a uint16_t. We use a single large array of probabilities. */
+
+/* Lengths of entries in the LZMA probabilities array. The names used
+ here are copied from the Linux kernel implementation. */
+
+#define LZMA_PROB_IS_MATCH_LEN (LZMA_STATES * LZMA_POS_STATES)
+#define LZMA_PROB_IS_REP_LEN LZMA_STATES
+#define LZMA_PROB_IS_REP0_LEN LZMA_STATES
+#define LZMA_PROB_IS_REP1_LEN LZMA_STATES
+#define LZMA_PROB_IS_REP2_LEN LZMA_STATES
+#define LZMA_PROB_IS_REP0_LONG_LEN (LZMA_STATES * LZMA_POS_STATES)
+#define LZMA_PROB_DIST_SLOT_LEN (LZMA_DIST_STATES * LZMA_DIST_SLOTS)
+#define LZMA_PROB_DIST_SPECIAL_LEN (LZMA_FULL_DISTANCES - LZMA_DIST_MODEL_END)
+#define LZMA_PROB_DIST_ALIGN_LEN LZMA_ALIGN_SIZE
+#define LZMA_PROB_MATCH_LEN_CHOICE_LEN 1
+#define LZMA_PROB_MATCH_LEN_CHOICE2_LEN 1
+#define LZMA_PROB_MATCH_LEN_LOW_LEN (LZMA_POS_STATES * LZMA_LEN_LOW_SYMBOLS)
+#define LZMA_PROB_MATCH_LEN_MID_LEN (LZMA_POS_STATES * LZMA_LEN_MID_SYMBOLS)
+#define LZMA_PROB_MATCH_LEN_HIGH_LEN LZMA_LEN_HIGH_SYMBOLS
+#define LZMA_PROB_REP_LEN_CHOICE_LEN 1
+#define LZMA_PROB_REP_LEN_CHOICE2_LEN 1
+#define LZMA_PROB_REP_LEN_LOW_LEN (LZMA_POS_STATES * LZMA_LEN_LOW_SYMBOLS)
+#define LZMA_PROB_REP_LEN_MID_LEN (LZMA_POS_STATES * LZMA_LEN_MID_SYMBOLS)
+#define LZMA_PROB_REP_LEN_HIGH_LEN LZMA_LEN_HIGH_SYMBOLS
+#define LZMA_PROB_LITERAL_LEN \
+ (LZMA_LITERAL_CODERS_MAX * LZMA_LITERAL_CODER_SIZE)
+
+/* Offsets into the LZMA probabilities array. This is mechanically
+ generated from the above lengths. */
+
+#define LZMA_PROB_IS_MATCH_OFFSET 0
+#define LZMA_PROB_IS_REP_OFFSET \
+ (LZMA_PROB_IS_MATCH_OFFSET + LZMA_PROB_IS_MATCH_LEN)
+#define LZMA_PROB_IS_REP0_OFFSET \
+ (LZMA_PROB_IS_REP_OFFSET + LZMA_PROB_IS_REP_LEN)
+#define LZMA_PROB_IS_REP1_OFFSET \
+ (LZMA_PROB_IS_REP0_OFFSET + LZMA_PROB_IS_REP0_LEN)
+#define LZMA_PROB_IS_REP2_OFFSET \
+ (LZMA_PROB_IS_REP1_OFFSET + LZMA_PROB_IS_REP1_LEN)
+#define LZMA_PROB_IS_REP0_LONG_OFFSET \
+ (LZMA_PROB_IS_REP2_OFFSET + LZMA_PROB_IS_REP2_LEN)
+#define LZMA_PROB_DIST_SLOT_OFFSET \
+ (LZMA_PROB_IS_REP0_LONG_OFFSET + LZMA_PROB_IS_REP0_LONG_LEN)
+#define LZMA_PROB_DIST_SPECIAL_OFFSET \
+ (LZMA_PROB_DIST_SLOT_OFFSET + LZMA_PROB_DIST_SLOT_LEN)
+#define LZMA_PROB_DIST_ALIGN_OFFSET \
+ (LZMA_PROB_DIST_SPECIAL_OFFSET + LZMA_PROB_DIST_SPECIAL_LEN)
+#define LZMA_PROB_MATCH_LEN_CHOICE_OFFSET \
+ (LZMA_PROB_DIST_ALIGN_OFFSET + LZMA_PROB_DIST_ALIGN_LEN)
+#define LZMA_PROB_MATCH_LEN_CHOICE2_OFFSET \
+ (LZMA_PROB_MATCH_LEN_CHOICE_OFFSET + LZMA_PROB_MATCH_LEN_CHOICE_LEN)
+#define LZMA_PROB_MATCH_LEN_LOW_OFFSET \
+ (LZMA_PROB_MATCH_LEN_CHOICE2_OFFSET + LZMA_PROB_MATCH_LEN_CHOICE2_LEN)
+#define LZMA_PROB_MATCH_LEN_MID_OFFSET \
+ (LZMA_PROB_MATCH_LEN_LOW_OFFSET + LZMA_PROB_MATCH_LEN_LOW_LEN)
+#define LZMA_PROB_MATCH_LEN_HIGH_OFFSET \
+ (LZMA_PROB_MATCH_LEN_MID_OFFSET + LZMA_PROB_MATCH_LEN_MID_LEN)
+#define LZMA_PROB_REP_LEN_CHOICE_OFFSET \
+ (LZMA_PROB_MATCH_LEN_HIGH_OFFSET + LZMA_PROB_MATCH_LEN_HIGH_LEN)
+#define LZMA_PROB_REP_LEN_CHOICE2_OFFSET \
+ (LZMA_PROB_REP_LEN_CHOICE_OFFSET + LZMA_PROB_REP_LEN_CHOICE_LEN)
+#define LZMA_PROB_REP_LEN_LOW_OFFSET \
+ (LZMA_PROB_REP_LEN_CHOICE2_OFFSET + LZMA_PROB_REP_LEN_CHOICE2_LEN)
+#define LZMA_PROB_REP_LEN_MID_OFFSET \
+ (LZMA_PROB_REP_LEN_LOW_OFFSET + LZMA_PROB_REP_LEN_LOW_LEN)
+#define LZMA_PROB_REP_LEN_HIGH_OFFSET \
+ (LZMA_PROB_REP_LEN_MID_OFFSET + LZMA_PROB_REP_LEN_MID_LEN)
+#define LZMA_PROB_LITERAL_OFFSET \
+ (LZMA_PROB_REP_LEN_HIGH_OFFSET + LZMA_PROB_REP_LEN_HIGH_LEN)
+
+#define LZMA_PROB_TOTAL_COUNT \
+ (LZMA_PROB_LITERAL_OFFSET + LZMA_PROB_LITERAL_LEN)
+
+/* Check that the number of LZMA probabilities is the same as the
+ Linux kernel implementation. */
+
+#if LZMA_PROB_TOTAL_COUNT != 1846 + (1 << 4) * 0x300
+ #error Wrong number of LZMA probabilities
+#endif
+
+/* Expressions for the offset in the LZMA probabilities array of a
+ specific probability. */
+
+#define LZMA_IS_MATCH(state, pos) \
+ (LZMA_PROB_IS_MATCH_OFFSET + (state) * LZMA_POS_STATES + (pos))
+#define LZMA_IS_REP(state) \
+ (LZMA_PROB_IS_REP_OFFSET + (state))
+#define LZMA_IS_REP0(state) \
+ (LZMA_PROB_IS_REP0_OFFSET + (state))
+#define LZMA_IS_REP1(state) \
+ (LZMA_PROB_IS_REP1_OFFSET + (state))
+#define LZMA_IS_REP2(state) \
+ (LZMA_PROB_IS_REP2_OFFSET + (state))
+#define LZMA_IS_REP0_LONG(state, pos) \
+ (LZMA_PROB_IS_REP0_LONG_OFFSET + (state) * LZMA_POS_STATES + (pos))
+#define LZMA_DIST_SLOT(dist, slot) \
+ (LZMA_PROB_DIST_SLOT_OFFSET + (dist) * LZMA_DIST_SLOTS + (slot))
+#define LZMA_DIST_SPECIAL(dist) \
+ (LZMA_PROB_DIST_SPECIAL_OFFSET + (dist))
+#define LZMA_DIST_ALIGN(dist) \
+ (LZMA_PROB_DIST_ALIGN_OFFSET + (dist))
+#define LZMA_MATCH_LEN_CHOICE \
+ LZMA_PROB_MATCH_LEN_CHOICE_OFFSET
+#define LZMA_MATCH_LEN_CHOICE2 \
+ LZMA_PROB_MATCH_LEN_CHOICE2_OFFSET
+#define LZMA_MATCH_LEN_LOW(pos, sym) \
+ (LZMA_PROB_MATCH_LEN_LOW_OFFSET + (pos) * LZMA_LEN_LOW_SYMBOLS + (sym))
+#define LZMA_MATCH_LEN_MID(pos, sym) \
+ (LZMA_PROB_MATCH_LEN_MID_OFFSET + (pos) * LZMA_LEN_MID_SYMBOLS + (sym))
+#define LZMA_MATCH_LEN_HIGH(sym) \
+ (LZMA_PROB_MATCH_LEN_HIGH_OFFSET + (sym))
+#define LZMA_REP_LEN_CHOICE \
+ LZMA_PROB_REP_LEN_CHOICE_OFFSET
+#define LZMA_REP_LEN_CHOICE2 \
+ LZMA_PROB_REP_LEN_CHOICE2_OFFSET
+#define LZMA_REP_LEN_LOW(pos, sym) \
+ (LZMA_PROB_REP_LEN_LOW_OFFSET + (pos) * LZMA_LEN_LOW_SYMBOLS + (sym))
+#define LZMA_REP_LEN_MID(pos, sym) \
+ (LZMA_PROB_REP_LEN_MID_OFFSET + (pos) * LZMA_LEN_MID_SYMBOLS + (sym))
+#define LZMA_REP_LEN_HIGH(sym) \
+ (LZMA_PROB_REP_LEN_HIGH_OFFSET + (sym))
+#define LZMA_LITERAL(code, size) \
+ (LZMA_PROB_LITERAL_OFFSET + (code) * LZMA_LITERAL_CODER_SIZE + (size))
+
+/* Read an LZMA varint from BUF, reading and updating *POFFSET,
+ setting *VAL. Returns 0 on error, 1 on success. */
+
+static int
+elf_lzma_varint (const unsigned char *compressed, size_t compressed_size,
+ size_t *poffset, uint64_t *val)
+{
+ size_t off;
+ int i;
+ uint64_t v;
+ unsigned char b;
+
+ off = *poffset;
+ i = 0;
+ v = 0;
+ while (1)
+ {
+ if (unlikely (off >= compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ b = compressed[off];
+ v |= (b & 0x7f) << (i * 7);
+ ++off;
+ if ((b & 0x80) == 0)
+ {
+ *poffset = off;
+ *val = v;
+ return 1;
+ }
+ ++i;
+ if (unlikely (i >= 9))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ }
+}
+
+/* Normalize the LZMA range decoder, pulling in an extra input byte if
+ needed. */
+
+static void
+elf_lzma_range_normalize (const unsigned char *compressed,
+ size_t compressed_size, size_t *poffset,
+ uint32_t *prange, uint32_t *pcode)
+{
+ if (*prange < (1U << 24))
+ {
+ if (unlikely (*poffset >= compressed_size))
+ {
+ /* We assume this will be caught elsewhere. */
+ elf_uncompress_failed ();
+ return;
+ }
+ *prange <<= 8;
+ *pcode <<= 8;
+ *pcode += compressed[*poffset];
+ ++*poffset;
+ }
+}
+
+/* Read and return a single bit from the LZMA stream, reading and
+ updating *PROB. Each bit comes from the range coder. */
+
+static int
+elf_lzma_bit (const unsigned char *compressed, size_t compressed_size,
+ uint16_t *prob, size_t *poffset, uint32_t *prange,
+ uint32_t *pcode)
+{
+ uint32_t bound;
+
+ elf_lzma_range_normalize (compressed, compressed_size, poffset,
+ prange, pcode);
+ bound = (*prange >> 11) * (uint32_t) *prob;
+ if (*pcode < bound)
+ {
+ *prange = bound;
+ *prob += ((1U << 11) - *prob) >> 5;
+ return 0;
+ }
+ else
+ {
+ *prange -= bound;
+ *pcode -= bound;
+ *prob -= *prob >> 5;
+ return 1;
+ }
+}
+
+/* Read an integer of size BITS from the LZMA stream, most significant
+ bit first. The bits are predicted using PROBS. */
+
+static uint32_t
+elf_lzma_integer (const unsigned char *compressed, size_t compressed_size,
+ uint16_t *probs, uint32_t bits, size_t *poffset,
+ uint32_t *prange, uint32_t *pcode)
+{
+ uint32_t sym;
+ uint32_t i;
+
+ sym = 1;
+ for (i = 0; i < bits; i++)
+ {
+ int bit;
+
+ bit = elf_lzma_bit (compressed, compressed_size, probs + sym, poffset,
+ prange, pcode);
+ sym <<= 1;
+ sym += bit;
+ }
+ return sym - (1 << bits);
+}
+
+/* Read an integer of size BITS from the LZMA stream, least
+ significant bit first. The bits are predicted using PROBS. */
+
+static uint32_t
+elf_lzma_reverse_integer (const unsigned char *compressed,
+ size_t compressed_size, uint16_t *probs,
+ uint32_t bits, size_t *poffset, uint32_t *prange,
+ uint32_t *pcode)
+{
+ uint32_t sym;
+ uint32_t val;
+ uint32_t i;
+
+ sym = 1;
+ val = 0;
+ for (i = 0; i < bits; i++)
+ {
+ int bit;
+
+ bit = elf_lzma_bit (compressed, compressed_size, probs + sym, poffset,
+ prange, pcode);
+ sym <<= 1;
+ sym += bit;
+ val += bit << i;
+ }
+ return val;
+}
+
+/* Read a length from the LZMA stream. IS_REP picks either LZMA_MATCH
+ or LZMA_REP probabilities. */
+
+static uint32_t
+elf_lzma_len (const unsigned char *compressed, size_t compressed_size,
+ uint16_t *probs, int is_rep, unsigned int pos_state,
+ size_t *poffset, uint32_t *prange, uint32_t *pcode)
+{
+ uint16_t *probs_choice;
+ uint16_t *probs_sym;
+ uint32_t bits;
+ uint32_t len;
+
+ probs_choice = probs + (is_rep
+ ? LZMA_REP_LEN_CHOICE
+ : LZMA_MATCH_LEN_CHOICE);
+ if (elf_lzma_bit (compressed, compressed_size, probs_choice, poffset,
+ prange, pcode))
+ {
+ probs_choice = probs + (is_rep
+ ? LZMA_REP_LEN_CHOICE2
+ : LZMA_MATCH_LEN_CHOICE2);
+ if (elf_lzma_bit (compressed, compressed_size, probs_choice,
+ poffset, prange, pcode))
+ {
+ probs_sym = probs + (is_rep
+ ? LZMA_REP_LEN_HIGH (0)
+ : LZMA_MATCH_LEN_HIGH (0));
+ bits = 8;
+ len = 2 + 8 + 8;
+ }
+ else
+ {
+ probs_sym = probs + (is_rep
+ ? LZMA_REP_LEN_MID (pos_state, 0)
+ : LZMA_MATCH_LEN_MID (pos_state, 0));
+ bits = 3;
+ len = 2 + 8;
+ }
+ }
+ else
+ {
+ probs_sym = probs + (is_rep
+ ? LZMA_REP_LEN_LOW (pos_state, 0)
+ : LZMA_MATCH_LEN_LOW (pos_state, 0));
+ bits = 3;
+ len = 2;
+ }
+
+ len += elf_lzma_integer (compressed, compressed_size, probs_sym, bits,
+ poffset, prange, pcode);
+ return len;
+}
+
+/* Uncompress one LZMA block from a minidebug file. The compressed
+ data is at COMPRESSED + *POFFSET. Update *POFFSET. Store the data
+ into the memory at UNCOMPRESSED, size UNCOMPRESSED_SIZE. CHECK is
+ the stream flag from the xz header. Return 1 on successful
+ decompression. */
+
+static int
+elf_uncompress_lzma_block (const unsigned char *compressed,
+ size_t compressed_size, unsigned char check,
+ uint16_t *probs, unsigned char *uncompressed,
+ size_t uncompressed_size, size_t *poffset)
+{
+ size_t off;
+ size_t block_header_offset;
+ size_t block_header_size;
+ unsigned char block_flags;
+ uint64_t header_compressed_size;
+ uint64_t header_uncompressed_size;
+ unsigned char lzma2_properties;
+ uint32_t computed_crc;
+ uint32_t stream_crc;
+ size_t uncompressed_offset;
+ size_t dict_start_offset;
+ unsigned int lc;
+ unsigned int lp;
+ unsigned int pb;
+ uint32_t range;
+ uint32_t code;
+ uint32_t lstate;
+ uint32_t dist[4];
+
+ off = *poffset;
+ block_header_offset = off;
+
+ /* Block header size is a single byte. */
+ if (unlikely (off >= compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ block_header_size = (compressed[off] + 1) * 4;
+ if (unlikely (off + block_header_size > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Block flags. */
+ block_flags = compressed[off + 1];
+ if (unlikely ((block_flags & 0x3c) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ off += 2;
+
+ /* Optional compressed size. */
+ header_compressed_size = 0;
+ if ((block_flags & 0x40) != 0)
+ {
+ *poffset = off;
+ if (!elf_lzma_varint (compressed, compressed_size, poffset,
+ &header_compressed_size))
+ return 0;
+ off = *poffset;
+ }
+
+ /* Optional uncompressed size. */
+ header_uncompressed_size = 0;
+ if ((block_flags & 0x80) != 0)
+ {
+ *poffset = off;
+ if (!elf_lzma_varint (compressed, compressed_size, poffset,
+ &header_uncompressed_size))
+ return 0;
+ off = *poffset;
+ }
+
+ /* The recipe for creating a minidebug file is to run the xz program
+ with no arguments, so we expect exactly one filter: lzma2. */
+
+ if (unlikely ((block_flags & 0x3) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (unlikely (off + 2 >= block_header_offset + block_header_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* The filter ID for LZMA2 is 0x21. */
+ if (unlikely (compressed[off] != 0x21))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ ++off;
+
+ /* The size of the filter properties for LZMA2 is 1. */
+ if (unlikely (compressed[off] != 1))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ ++off;
+
+ lzma2_properties = compressed[off];
+ ++off;
+
+ if (unlikely (lzma2_properties > 40))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* The properties describe the dictionary size, but we don't care
+ what that is. */
+
+ /* Block header padding. */
+ if (unlikely (off + 4 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ off = (off + 3) &~ (size_t) 3;
+
+ if (unlikely (off + 4 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Block header CRC. */
+ computed_crc = elf_crc32 (0, compressed + block_header_offset,
+ block_header_size - 4);
+ stream_crc = (compressed[off]
+ | (compressed[off + 1] << 8)
+ | (compressed[off + 2] << 16)
+ | (compressed[off + 3] << 24));
+ if (unlikely (computed_crc != stream_crc))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ off += 4;
+
+ /* Read a sequence of LZMA2 packets. */
+
+ uncompressed_offset = 0;
+ dict_start_offset = 0;
+ lc = 0;
+ lp = 0;
+ pb = 0;
+ lstate = 0;
+ while (off < compressed_size)
+ {
+ unsigned char control;
+
+ range = 0xffffffff;
+ code = 0;
+
+ control = compressed[off];
+ ++off;
+ if (unlikely (control == 0))
+ {
+ /* End of packets. */
+ break;
+ }
+
+ if (control == 1 || control >= 0xe0)
+ {
+ /* Reset dictionary to empty. */
+ dict_start_offset = uncompressed_offset;
+ }
+
+ if (control < 0x80)
+ {
+ size_t chunk_size;
+
+ /* The only valid values here are 1 or 2. A 1 means to
+ reset the dictionary (done above). Then we see an
+ uncompressed chunk. */
+
+ if (unlikely (control > 2))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* An uncompressed chunk is a two byte size followed by
+ data. */
+
+ if (unlikely (off + 2 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ chunk_size = compressed[off] << 8;
+ chunk_size += compressed[off + 1];
+ ++chunk_size;
+
+ off += 2;
+
+ if (unlikely (off + chunk_size > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ if (unlikely (uncompressed_offset + chunk_size > uncompressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ memcpy (uncompressed + uncompressed_offset, compressed + off,
+ chunk_size);
+ uncompressed_offset += chunk_size;
+ off += chunk_size;
+ }
+ else
+ {
+ size_t uncompressed_chunk_start;
+ size_t uncompressed_chunk_size;
+ size_t compressed_chunk_size;
+ size_t limit;
+
+ /* An LZMA chunk. This starts with an uncompressed size and
+ a compressed size. */
+
+ if (unlikely (off + 4 >= compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ uncompressed_chunk_start = uncompressed_offset;
+
+ uncompressed_chunk_size = (control & 0x1f) << 16;
+ uncompressed_chunk_size += compressed[off] << 8;
+ uncompressed_chunk_size += compressed[off + 1];
+ ++uncompressed_chunk_size;
+
+ compressed_chunk_size = compressed[off + 2] << 8;
+ compressed_chunk_size += compressed[off + 3];
+ ++compressed_chunk_size;
+
+ off += 4;
+
+ /* Bit 7 (0x80) is set.
+ Bits 6 and 5 (0x40 and 0x20) are as follows:
+ 0: don't reset anything
+ 1: reset state
+ 2: reset state, read properties
+ 3: reset state, read properties, reset dictionary (done above) */
+
+ if (control >= 0xc0)
+ {
+ unsigned char props;
+
+ /* Bit 6 is set, read properties. */
+
+ if (unlikely (off >= compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ props = compressed[off];
+ ++off;
+ if (unlikely (props > (4 * 5 + 4) * 9 + 8))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ pb = 0;
+ while (props >= 9 * 5)
+ {
+ props -= 9 * 5;
+ ++pb;
+ }
+ lp = 0;
+ while (props > 9)
+ {
+ props -= 9;
+ ++lp;
+ }
+ lc = props;
+ if (unlikely (lc + lp > 4))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ }
+
+ if (control >= 0xa0)
+ {
+ size_t i;
+
+ /* Bit 5 or 6 is set, reset LZMA state. */
+
+ lstate = 0;
+ memset (&dist, 0, sizeof dist);
+ for (i = 0; i < LZMA_PROB_TOTAL_COUNT; i++)
+ probs[i] = 1 << 10;
+ range = 0xffffffff;
+ code = 0;
+ }
+
+ /* Read the range code. */
+
+ if (unlikely (off + 5 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* The byte at compressed[off] is ignored for some
+ reason. */
+
+ code = ((compressed[off + 1] << 24)
+ + (compressed[off + 2] << 16)
+ + (compressed[off + 3] << 8)
+ + compressed[off + 4]);
+ off += 5;
+
+ /* This is the main LZMA decode loop. */
+
+ limit = off + compressed_chunk_size;
+ *poffset = off;
+ while (*poffset < limit)
+ {
+ unsigned int pos_state;
+
+ if (unlikely (uncompressed_offset
+ == (uncompressed_chunk_start
+ + uncompressed_chunk_size)))
+ {
+ /* We've decompressed all the expected bytes. */
+ break;
+ }
+
+ pos_state = ((uncompressed_offset - dict_start_offset)
+ & ((1 << pb) - 1));
+
+ if (elf_lzma_bit (compressed, compressed_size,
+ probs + LZMA_IS_MATCH (lstate, pos_state),
+ poffset, &range, &code))
+ {
+ uint32_t len;
+
+ if (elf_lzma_bit (compressed, compressed_size,
+ probs + LZMA_IS_REP (lstate),
+ poffset, &range, &code))
+ {
+ int short_rep;
+ uint32_t next_dist;
+
+ /* Repeated match. */
+
+ short_rep = 0;
+ if (elf_lzma_bit (compressed, compressed_size,
+ probs + LZMA_IS_REP0 (lstate),
+ poffset, &range, &code))
+ {
+ if (elf_lzma_bit (compressed, compressed_size,
+ probs + LZMA_IS_REP1 (lstate),
+ poffset, &range, &code))
+ {
+ if (elf_lzma_bit (compressed, compressed_size,
+ probs + LZMA_IS_REP2 (lstate),
+ poffset, &range, &code))
+ {
+ next_dist = dist[3];
+ dist[3] = dist[2];
+ }
+ else
+ {
+ next_dist = dist[2];
+ }
+ dist[2] = dist[1];
+ }
+ else
+ {
+ next_dist = dist[1];
+ }
+
+ dist[1] = dist[0];
+ dist[0] = next_dist;
+ }
+ else
+ {
+ if (!elf_lzma_bit (compressed, compressed_size,
+ (probs
+ + LZMA_IS_REP0_LONG (lstate,
+ pos_state)),
+ poffset, &range, &code))
+ short_rep = 1;
+ }
+
+ if (lstate < 7)
+ lstate = short_rep ? 9 : 8;
+ else
+ lstate = 11;
+
+ if (short_rep)
+ len = 1;
+ else
+ len = elf_lzma_len (compressed, compressed_size,
+ probs, 1, pos_state, poffset,
+ &range, &code);
+ }
+ else
+ {
+ uint32_t dist_state;
+ uint32_t dist_slot;
+ uint16_t *probs_dist;
+
+ /* Match. */
+
+ if (lstate < 7)
+ lstate = 7;
+ else
+ lstate = 10;
+ dist[3] = dist[2];
+ dist[2] = dist[1];
+ dist[1] = dist[0];
+ len = elf_lzma_len (compressed, compressed_size,
+ probs, 0, pos_state, poffset,
+ &range, &code);
+
+ if (len < 4 + 2)
+ dist_state = len - 2;
+ else
+ dist_state = 3;
+ probs_dist = probs + LZMA_DIST_SLOT (dist_state, 0);
+ dist_slot = elf_lzma_integer (compressed,
+ compressed_size,
+ probs_dist, 6,
+ poffset, &range,
+ &code);
+ if (dist_slot < LZMA_DIST_MODEL_START)
+ dist[0] = dist_slot;
+ else
+ {
+ uint32_t limit;
+
+ limit = (dist_slot >> 1) - 1;
+ dist[0] = 2 + (dist_slot & 1);
+ if (dist_slot < LZMA_DIST_MODEL_END)
+ {
+ dist[0] <<= limit;
+ probs_dist = (probs
+ + LZMA_DIST_SPECIAL(dist[0]
+ - dist_slot
+ - 1));
+ dist[0] +=
+ elf_lzma_reverse_integer (compressed,
+ compressed_size,
+ probs_dist,
+ limit, poffset,
+ &range, &code);
+ }
+ else
+ {
+ uint32_t dist0;
+ uint32_t i;
+
+ dist0 = dist[0];
+ for (i = 0; i < limit - 4; i++)
+ {
+ uint32_t mask;
+
+ elf_lzma_range_normalize (compressed,
+ compressed_size,
+ poffset,
+ &range, &code);
+ range >>= 1;
+ code -= range;
+ mask = -(code >> 31);
+ code += range & mask;
+ dist0 <<= 1;
+ dist0 += mask + 1;
+ }
+ dist0 <<= 4;
+ probs_dist = probs + LZMA_DIST_ALIGN (0);
+ dist0 +=
+ elf_lzma_reverse_integer (compressed,
+ compressed_size,
+ probs_dist, 4,
+ poffset,
+ &range, &code);
+ dist[0] = dist0;
+ }
+ }
+ }
+
+ if (unlikely (uncompressed_offset
+ - dict_start_offset < dist[0] + 1))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ if (unlikely (uncompressed_offset + len > uncompressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ if (dist[0] == 0)
+ {
+ /* A common case, meaning repeat the last
+ character LEN times. */
+ memset (uncompressed + uncompressed_offset,
+ uncompressed[uncompressed_offset - 1],
+ len);
+ uncompressed_offset += len;
+ }
+ else if (dist[0] + 1 >= len)
+ {
+ memcpy (uncompressed + uncompressed_offset,
+ uncompressed + uncompressed_offset - dist[0] - 1,
+ len);
+ uncompressed_offset += len;
+ }
+ else
+ {
+ while (len > 0)
+ {
+ uint32_t copy;
+
+ copy = len < dist[0] + 1 ? len : dist[0] + 1;
+ memcpy (uncompressed + uncompressed_offset,
+ (uncompressed + uncompressed_offset
+ - dist[0] - 1),
+ copy);
+ len -= copy;
+ uncompressed_offset += copy;
+ }
+ }
+ }
+ else
+ {
+ unsigned char prev;
+ unsigned char low;
+ size_t high;
+ uint16_t *lit_probs;
+ unsigned int sym;
+
+ /* Literal value. */
+
+ if (uncompressed_offset > 0)
+ prev = uncompressed[uncompressed_offset - 1];
+ else
+ prev = 0;
+ low = prev >> (8 - lc);
+ high = (((uncompressed_offset - dict_start_offset)
+ & ((1 << lp) - 1))
+ << lc);
+ lit_probs = probs + LZMA_LITERAL (low + high, 0);
+ if (lstate < 7)
+ sym = elf_lzma_integer (compressed, compressed_size,
+ lit_probs, 8, poffset, &range,
+ &code);
+ else
+ {
+ unsigned int match;
+ unsigned int bit;
+ unsigned int match_bit;
+ unsigned int idx;
+
+ sym = 1;
+ if (uncompressed_offset >= dist[0] + 1)
+ match = uncompressed[uncompressed_offset - dist[0] - 1];
+ else
+ match = 0;
+ match <<= 1;
+ bit = 0x100;
+ do
+ {
+ match_bit = match & bit;
+ match <<= 1;
+ idx = bit + match_bit + sym;
+ sym <<= 1;
+ if (elf_lzma_bit (compressed, compressed_size,
+ lit_probs + idx, poffset,
+ &range, &code))
+ {
+ ++sym;
+ bit &= match_bit;
+ }
+ else
+ {
+ bit &= ~ match_bit;
+ }
+ }
+ while (sym < 0x100);
+ }
+
+ if (unlikely (uncompressed_offset >= uncompressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ uncompressed[uncompressed_offset] = (unsigned char) sym;
+ ++uncompressed_offset;
+ if (lstate <= 3)
+ lstate = 0;
+ else if (lstate <= 9)
+ lstate -= 3;
+ else
+ lstate -= 6;
+ }
+ }
+
+ elf_lzma_range_normalize (compressed, compressed_size, poffset,
+ &range, &code);
+
+ off = *poffset;
+ }
+ }
+
+ /* We have reached the end of the block. Pad to four byte
+ boundary. */
+ off = (off + 3) &~ (size_t) 3;
+ if (unlikely (off > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ switch (check)
+ {
+ case 0:
+ /* No check. */
+ break;
+
+ case 1:
+ /* CRC32 */
+ if (unlikely (off + 4 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ computed_crc = elf_crc32 (0, uncompressed, uncompressed_offset);
+ stream_crc = (compressed[off]
+ | (compressed[off + 1] << 8)
+ | (compressed[off + 2] << 16)
+ | (compressed[off + 3] << 24));
+ if (computed_crc != stream_crc)
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ off += 4;
+ break;
+
+ case 4:
+ /* CRC64. We don't bother computing a CRC64 checksum. */
+ if (unlikely (off + 8 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ off += 8;
+ break;
+
+ case 10:
+ /* SHA. We don't bother computing a SHA checksum. */
+ if (unlikely (off + 32 > compressed_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ off += 32;
+ break;
+
+ default:
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ *poffset = off;
+
+ return 1;
+}
+
+/* Uncompress LZMA data found in a minidebug file. The minidebug
+ format is described at
+ https://sourceware.org/gdb/current/onlinedocs/gdb/MiniDebugInfo.html.
+ Returns 0 on error, 1 on successful decompression. For this
+ function we return 0 on failure to decompress, as the calling code
+ will carry on in that case. */
+
+static int
+elf_uncompress_lzma (struct backtrace_state *state,
+ const unsigned char *compressed, size_t compressed_size,
+ backtrace_error_callback error_callback, void *data,
+ unsigned char **uncompressed, size_t *uncompressed_size)
+{
+ size_t header_size;
+ size_t footer_size;
+ unsigned char check;
+ uint32_t computed_crc;
+ uint32_t stream_crc;
+ size_t offset;
+ size_t index_size;
+ size_t footer_offset;
+ size_t index_offset;
+ uint64_t index_compressed_size;
+ uint64_t index_uncompressed_size;
+ unsigned char *mem;
+ uint16_t *probs;
+ size_t compressed_block_size;
+
+ /* The format starts with a stream header and ends with a stream
+ footer. */
+ header_size = 12;
+ footer_size = 12;
+ if (unlikely (compressed_size < header_size + footer_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* The stream header starts with a magic string. */
+ if (unlikely (memcmp (compressed, "\375" "7zXZ\0", 6) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Next come stream flags. The first byte is zero, the second byte
+ is the check. */
+ if (unlikely (compressed[6] != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ check = compressed[7];
+ if (unlikely ((check & 0xf8) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Next comes a CRC of the stream flags. */
+ computed_crc = elf_crc32 (0, compressed + 6, 2);
+ stream_crc = (compressed[8]
+ | (compressed[9] << 8)
+ | (compressed[10] << 16)
+ | (compressed[11] << 24));
+ if (unlikely (computed_crc != stream_crc))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Now that we've parsed the header, parse the footer, so that we
+ can get the uncompressed size. */
+
+ /* The footer ends with two magic bytes. */
+
+ offset = compressed_size;
+ if (unlikely (memcmp (compressed + offset - 2, "YZ", 2) != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ offset -= 2;
+
+ /* Before that are the stream flags, which should be the same as the
+ flags in the header. */
+ if (unlikely (compressed[offset - 2] != 0
+ || compressed[offset - 1] != check))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ offset -= 2;
+
+ /* Before that is the size of the index field, which precedes the
+ footer. */
+ index_size = (compressed[offset - 4]
+ | (compressed[offset - 3] << 8)
+ | (compressed[offset - 2] << 16)
+ | (compressed[offset - 1] << 24));
+ index_size = (index_size + 1) * 4;
+ offset -= 4;
+
+ /* Before that is a footer CRC. */
+ computed_crc = elf_crc32 (0, compressed + offset, 6);
+ stream_crc = (compressed[offset - 4]
+ | (compressed[offset - 3] << 8)
+ | (compressed[offset - 2] << 16)
+ | (compressed[offset - 1] << 24));
+ if (unlikely (computed_crc != stream_crc))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ offset -= 4;
+
+ /* The index comes just before the footer. */
+ if (unlikely (offset < index_size + header_size))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ footer_offset = offset;
+ offset -= index_size;
+ index_offset = offset;
+
+ /* The index starts with a zero byte. */
+ if (unlikely (compressed[offset] != 0))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ ++offset;
+
+ /* Next is the number of blocks. We expect zero blocks for an empty
+ stream, and otherwise a single block. */
+ if (unlikely (compressed[offset] == 0))
+ {
+ *uncompressed = NULL;
+ *uncompressed_size = 0;
+ return 1;
+ }
+ if (unlikely (compressed[offset] != 1))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ ++offset;
+
+ /* Next is the compressed size and the uncompressed size. */
+ if (!elf_lzma_varint (compressed, compressed_size, &offset,
+ &index_compressed_size))
+ return 0;
+ if (!elf_lzma_varint (compressed, compressed_size, &offset,
+ &index_uncompressed_size))
+ return 0;
+
+ /* Pad to a four byte boundary. */
+ offset = (offset + 3) &~ (size_t) 3;
+
+ /* Next is a CRC of the index. */
+ computed_crc = elf_crc32 (0, compressed + index_offset,
+ offset - index_offset);
+ stream_crc = (compressed[offset]
+ | (compressed[offset + 1] << 8)
+ | (compressed[offset + 2] << 16)
+ | (compressed[offset + 3] << 24));
+ if (unlikely (computed_crc != stream_crc))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+ offset += 4;
+
+ /* We should now be back at the footer. */
+ if (unlikely (offset != footer_offset))
+ {
+ elf_uncompress_failed ();
+ return 0;
+ }
+
+ /* Allocate space to hold the uncompressed data. If we succeed in
+ uncompressing the LZMA data, we never free this memory. */
+ mem = (unsigned char *) backtrace_alloc (state, index_uncompressed_size,
+ error_callback, data);
+ if (unlikely (mem == NULL))
+ return 0;
+ *uncompressed = mem;
+ *uncompressed_size = index_uncompressed_size;
+
+ /* Allocate space for probabilities. */
+ probs = ((uint16_t *)
+ backtrace_alloc (state,
+ LZMA_PROB_TOTAL_COUNT * sizeof (uint16_t),
+ error_callback, data));
+ if (unlikely (probs == NULL))
+ {
+ backtrace_free (state, mem, index_uncompressed_size, error_callback,
+ data);
+ return 0;
+ }
+
+ /* Uncompress the block, which follows the header. */
+ offset = 12;
+ if (!elf_uncompress_lzma_block (compressed, compressed_size, check, probs,
+ mem, index_uncompressed_size, &offset))
+ {
+ backtrace_free (state, mem, index_uncompressed_size, error_callback,
+ data);
+ return 0;
+ }
+
+ compressed_block_size = offset - 12;
+ if (unlikely (compressed_block_size
+ != ((index_compressed_size + 3) &~ (size_t) 3)))
+ {
+ elf_uncompress_failed ();
+ backtrace_free (state, mem, index_uncompressed_size, error_callback,
+ data);
+ return 0;
+ }
+
+ offset = (offset + 3) &~ (size_t) 3;
+ if (unlikely (offset != index_offset))
+ {
+ elf_uncompress_failed ();
+ backtrace_free (state, mem, index_uncompressed_size, error_callback,
+ data);
+ return 0;
+ }
+
+ return 1;
+}
+
+/* This function is a hook for testing the LZMA support. It is only
+ used by tests. */
+
+int
+backtrace_uncompress_lzma (struct backtrace_state *state,
+ const unsigned char *compressed,
+ size_t compressed_size,
+ backtrace_error_callback error_callback,
+ void *data, unsigned char **uncompressed,
+ size_t *uncompressed_size)
+{
+ return elf_uncompress_lzma (state, compressed, compressed_size,
+ error_callback, data, uncompressed,
+ uncompressed_size);
+}
+
+/* Add the backtrace data for one ELF file. Returns 1 on success,
+ 0 on failure (in both cases descriptor is closed) or -1 if exe
+ is non-zero and the ELF file is ET_DYN, which tells the caller that
+ elf_add will need to be called on the descriptor again after
+ base_address is determined. */
+
+static int
+elf_add (struct backtrace_state *state, const char *filename, int descriptor,
+ const unsigned char *memory, size_t memory_size,
+ uintptr_t base_address, backtrace_error_callback error_callback,
+ void *data, fileline *fileline_fn, int *found_sym, int *found_dwarf,
+ struct dwarf_data **fileline_entry, int exe, int debuginfo,
+ const char *with_buildid_data, uint32_t with_buildid_size)
+{
+ struct elf_view ehdr_view;
+ b_elf_ehdr ehdr;
+ off_t shoff;
+ unsigned int shnum;
+ unsigned int shstrndx;
+ struct elf_view shdrs_view;
+ int shdrs_view_valid;
+ const b_elf_shdr *shdrs;
+ const b_elf_shdr *shstrhdr;
+ size_t shstr_size;
+ off_t shstr_off;
+ struct elf_view names_view;
+ int names_view_valid;
+ const char *names;
+ unsigned int symtab_shndx;
+ unsigned int dynsym_shndx;
+ unsigned int i;
+ struct debug_section_info sections[DEBUG_MAX];
+ struct debug_section_info zsections[DEBUG_MAX];
+ struct elf_view symtab_view;
+ int symtab_view_valid;
+ struct elf_view strtab_view;
+ int strtab_view_valid;
+ struct elf_view buildid_view;
+ int buildid_view_valid;
+ const char *buildid_data;
+ uint32_t buildid_size;
+ struct elf_view debuglink_view;
+ int debuglink_view_valid;
+ const char *debuglink_name;
+ uint32_t debuglink_crc;
+ struct elf_view debugaltlink_view;
+ int debugaltlink_view_valid;
+ const char *debugaltlink_name;
+ const char *debugaltlink_buildid_data;
+ uint32_t debugaltlink_buildid_size;
+ struct elf_view gnu_debugdata_view;
+ int gnu_debugdata_view_valid;
+ size_t gnu_debugdata_size;
+ unsigned char *gnu_debugdata_uncompressed;
+ size_t gnu_debugdata_uncompressed_size;
+ off_t min_offset;
+ off_t max_offset;
+ off_t debug_size;
+ struct elf_view debug_view;
+ int debug_view_valid;
+ unsigned int using_debug_view;
+ uint16_t *zdebug_table;
+ struct elf_view split_debug_view[DEBUG_MAX];
+ unsigned char split_debug_view_valid[DEBUG_MAX];
+ struct elf_ppc64_opd_data opd_data, *opd;
+ struct dwarf_sections dwarf_sections;
+
+ if (!debuginfo)
+ {
+ *found_sym = 0;
+ *found_dwarf = 0;
+ }
+
+ shdrs_view_valid = 0;
+ names_view_valid = 0;
+ symtab_view_valid = 0;
+ strtab_view_valid = 0;
+ buildid_view_valid = 0;
+ buildid_data = NULL;
+ buildid_size = 0;
+ debuglink_view_valid = 0;
+ debuglink_name = NULL;
+ debuglink_crc = 0;
+ debugaltlink_view_valid = 0;
+ debugaltlink_name = NULL;
+ debugaltlink_buildid_data = NULL;
+ debugaltlink_buildid_size = 0;
+ gnu_debugdata_view_valid = 0;
+ gnu_debugdata_size = 0;
+ debug_view_valid = 0;
+ memset (&split_debug_view_valid[0], 0, sizeof split_debug_view_valid);
+ opd = NULL;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size, 0, sizeof ehdr,
+ error_callback, data, &ehdr_view))
+ goto fail;
+
+ memcpy (&ehdr, ehdr_view.view.data, sizeof ehdr);
+
+ elf_release_view (state, &ehdr_view, error_callback, data);
+
+ if (ehdr.e_ident[EI_MAG0] != ELFMAG0
+ || ehdr.e_ident[EI_MAG1] != ELFMAG1
+ || ehdr.e_ident[EI_MAG2] != ELFMAG2
+ || ehdr.e_ident[EI_MAG3] != ELFMAG3)
+ {
+ error_callback (data, "executable file is not ELF", 0);
+ goto fail;
+ }
+ if (ehdr.e_ident[EI_VERSION] != EV_CURRENT)
+ {
+ error_callback (data, "executable file is unrecognized ELF version", 0);
+ goto fail;
+ }
+
+#if BACKTRACE_ELF_SIZE == 32
+#define BACKTRACE_ELFCLASS ELFCLASS32
+#else
+#define BACKTRACE_ELFCLASS ELFCLASS64
+#endif
+
+ if (ehdr.e_ident[EI_CLASS] != BACKTRACE_ELFCLASS)
+ {
+ error_callback (data, "executable file is unexpected ELF class", 0);
+ goto fail;
+ }
+
+ if (ehdr.e_ident[EI_DATA] != ELFDATA2LSB
+ && ehdr.e_ident[EI_DATA] != ELFDATA2MSB)
+ {
+ error_callback (data, "executable file has unknown endianness", 0);
+ goto fail;
+ }
+
+ /* If the executable is ET_DYN, it is either a PIE, or we are running
+ directly a shared library with .interp. We need to wait for
+ dl_iterate_phdr in that case to determine the actual base_address. */
+ if (exe && ehdr.e_type == ET_DYN)
+ return -1;
+
+ shoff = ehdr.e_shoff;
+ shnum = ehdr.e_shnum;
+ shstrndx = ehdr.e_shstrndx;
+
+ if ((shnum == 0 || shstrndx == SHN_XINDEX)
+ && shoff != 0)
+ {
+ struct elf_view shdr_view;
+ const b_elf_shdr *shdr;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size, shoff,
+ sizeof shdr, error_callback, data, &shdr_view))
+ goto fail;
+
+ shdr = (const b_elf_shdr *) shdr_view.view.data;
+
+ if (shnum == 0)
+ shnum = shdr->sh_size;
+
+ if (shstrndx == SHN_XINDEX)
+ {
+ shstrndx = shdr->sh_link;
+
+ /* Versions of the GNU binutils between 2.12 and 2.18 did
+ not handle objects with more than SHN_LORESERVE sections
+ correctly. All large section indexes were offset by
+ 0x100. There is more information at
+ http://sourceware.org/bugzilla/show_bug.cgi?id-5900 .
+ Fortunately these object files are easy to detect, as the
+ GNU binutils always put the section header string table
+ near the end of the list of sections. Thus if the
+ section header string table index is larger than the
+ number of sections, then we know we have to subtract
+ 0x100 to get the real section index. */
+ if (shstrndx >= shnum && shstrndx >= SHN_LORESERVE + 0x100)
+ shstrndx -= 0x100;
+ }
+
+ elf_release_view (state, &shdr_view, error_callback, data);
+ }
+
+ if (shnum == 0 || shstrndx == 0)
+ goto fail;
+
+ /* To translate PC to file/line when using DWARF, we need to find
+ the .debug_info and .debug_line sections. */
+
+ /* Read the section headers, skipping the first one. */
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shoff + sizeof (b_elf_shdr),
+ (shnum - 1) * sizeof (b_elf_shdr),
+ error_callback, data, &shdrs_view))
+ goto fail;
+ shdrs_view_valid = 1;
+ shdrs = (const b_elf_shdr *) shdrs_view.view.data;
+
+ /* Read the section names. */
+
+ shstrhdr = &shdrs[shstrndx - 1];
+ shstr_size = shstrhdr->sh_size;
+ shstr_off = shstrhdr->sh_offset;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size, shstr_off,
+ shstrhdr->sh_size, error_callback, data, &names_view))
+ goto fail;
+ names_view_valid = 1;
+ names = (const char *) names_view.view.data;
+
+ symtab_shndx = 0;
+ dynsym_shndx = 0;
+
+ memset (sections, 0, sizeof sections);
+ memset (zsections, 0, sizeof zsections);
+
+ /* Look for the symbol table. */
+ for (i = 1; i < shnum; ++i)
+ {
+ const b_elf_shdr *shdr;
+ unsigned int sh_name;
+ const char *name;
+ int j;
+
+ shdr = &shdrs[i - 1];
+
+ if (shdr->sh_type == SHT_SYMTAB)
+ symtab_shndx = i;
+ else if (shdr->sh_type == SHT_DYNSYM)
+ dynsym_shndx = i;
+
+ sh_name = shdr->sh_name;
+ if (sh_name >= shstr_size)
+ {
+ error_callback (data, "ELF section name out of range", 0);
+ goto fail;
+ }
+
+ name = names + sh_name;
+
+ for (j = 0; j < (int) DEBUG_MAX; ++j)
+ {
+ if (strcmp (name, dwarf_section_names[j]) == 0)
+ {
+ sections[j].offset = shdr->sh_offset;
+ sections[j].size = shdr->sh_size;
+ sections[j].compressed = (shdr->sh_flags & SHF_COMPRESSED) != 0;
+ break;
+ }
+ }
+
+ if (name[0] == '.' && name[1] == 'z')
+ {
+ for (j = 0; j < (int) DEBUG_MAX; ++j)
+ {
+ if (strcmp (name + 2, dwarf_section_names[j] + 1) == 0)
+ {
+ zsections[j].offset = shdr->sh_offset;
+ zsections[j].size = shdr->sh_size;
+ break;
+ }
+ }
+ }
+
+ /* Read the build ID if present. This could check for any
+ SHT_NOTE section with the right note name and type, but gdb
+ looks for a specific section name. */
+ if ((!debuginfo || with_buildid_data != NULL)
+ && !buildid_view_valid
+ && strcmp (name, ".note.gnu.build-id") == 0)
+ {
+ const b_elf_note *note;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shdr->sh_offset, shdr->sh_size, error_callback,
+ data, &buildid_view))
+ goto fail;
+
+ buildid_view_valid = 1;
+ note = (const b_elf_note *) buildid_view.view.data;
+ if (note->type == NT_GNU_BUILD_ID
+ && note->namesz == 4
+ && strncmp (note->name, "GNU", 4) == 0
+ && shdr->sh_size <= 12 + ((note->namesz + 3) & ~ 3) + note->descsz)
+ {
+ buildid_data = &note->name[0] + ((note->namesz + 3) & ~ 3);
+ buildid_size = note->descsz;
+ }
+
+ if (with_buildid_size != 0)
+ {
+ if (buildid_size != with_buildid_size)
+ goto fail;
+
+ if (memcmp (buildid_data, with_buildid_data, buildid_size) != 0)
+ goto fail;
+ }
+ }
+
+ /* Read the debuglink file if present. */
+ if (!debuginfo
+ && !debuglink_view_valid
+ && strcmp (name, ".gnu_debuglink") == 0)
+ {
+ const char *debuglink_data;
+ size_t crc_offset;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shdr->sh_offset, shdr->sh_size, error_callback,
+ data, &debuglink_view))
+ goto fail;
+
+ debuglink_view_valid = 1;
+ debuglink_data = (const char *) debuglink_view.view.data;
+ crc_offset = strnlen (debuglink_data, shdr->sh_size);
+ crc_offset = (crc_offset + 3) & ~3;
+ if (crc_offset + 4 <= shdr->sh_size)
+ {
+ debuglink_name = debuglink_data;
+ debuglink_crc = *(const uint32_t*)(debuglink_data + crc_offset);
+ }
+ }
+
+ if (!debugaltlink_view_valid
+ && strcmp (name, ".gnu_debugaltlink") == 0)
+ {
+ const char *debugaltlink_data;
+ size_t debugaltlink_name_len;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shdr->sh_offset, shdr->sh_size, error_callback,
+ data, &debugaltlink_view))
+ goto fail;
+
+ debugaltlink_view_valid = 1;
+ debugaltlink_data = (const char *) debugaltlink_view.view.data;
+ debugaltlink_name = debugaltlink_data;
+ debugaltlink_name_len = strnlen (debugaltlink_data, shdr->sh_size);
+ if (debugaltlink_name_len < shdr->sh_size)
+ {
+ /* Include terminating zero. */
+ debugaltlink_name_len += 1;
+
+ debugaltlink_buildid_data
+ = debugaltlink_data + debugaltlink_name_len;
+ debugaltlink_buildid_size = shdr->sh_size - debugaltlink_name_len;
+ }
+ }
+
+ if (!gnu_debugdata_view_valid
+ && strcmp (name, ".gnu_debugdata") == 0)
+ {
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shdr->sh_offset, shdr->sh_size, error_callback,
+ data, &gnu_debugdata_view))
+ goto fail;
+
+ gnu_debugdata_size = shdr->sh_size;
+ gnu_debugdata_view_valid = 1;
+ }
+
+ /* Read the .opd section on PowerPC64 ELFv1. */
+ if (ehdr.e_machine == EM_PPC64
+ && (ehdr.e_flags & EF_PPC64_ABI) < 2
+ && shdr->sh_type == SHT_PROGBITS
+ && strcmp (name, ".opd") == 0)
+ {
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ shdr->sh_offset, shdr->sh_size, error_callback,
+ data, &opd_data.view))
+ goto fail;
+
+ opd = &opd_data;
+ opd->addr = shdr->sh_addr;
+ opd->data = (const char *) opd_data.view.view.data;
+ opd->size = shdr->sh_size;
+ }
+ }
+
+ if (symtab_shndx == 0)
+ symtab_shndx = dynsym_shndx;
+ if (symtab_shndx != 0 && !debuginfo)
+ {
+ const b_elf_shdr *symtab_shdr;
+ unsigned int strtab_shndx;
+ const b_elf_shdr *strtab_shdr;
+ struct elf_syminfo_data *sdata;
+
+ symtab_shdr = &shdrs[symtab_shndx - 1];
+ strtab_shndx = symtab_shdr->sh_link;
+ if (strtab_shndx >= shnum)
+ {
+ error_callback (data,
+ "ELF symbol table strtab link out of range", 0);
+ goto fail;
+ }
+ strtab_shdr = &shdrs[strtab_shndx - 1];
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ symtab_shdr->sh_offset, symtab_shdr->sh_size,
+ error_callback, data, &symtab_view))
+ goto fail;
+ symtab_view_valid = 1;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ strtab_shdr->sh_offset, strtab_shdr->sh_size,
+ error_callback, data, &strtab_view))
+ goto fail;
+ strtab_view_valid = 1;
+
+ sdata = ((struct elf_syminfo_data *)
+ backtrace_alloc (state, sizeof *sdata, error_callback, data));
+ if (sdata == NULL)
+ goto fail;
+
+ if (!elf_initialize_syminfo (state, base_address,
+ symtab_view.view.data, symtab_shdr->sh_size,
+ strtab_view.view.data, strtab_shdr->sh_size,
+ error_callback, data, sdata, opd))
+ {
+ backtrace_free (state, sdata, sizeof *sdata, error_callback, data);
+ goto fail;
+ }
+
+ /* We no longer need the symbol table, but we hold on to the
+ string table permanently. */
+ elf_release_view (state, &symtab_view, error_callback, data);
+ symtab_view_valid = 0;
+ strtab_view_valid = 0;
+
+ *found_sym = 1;
+
+ elf_add_syminfo_data (state, sdata);
+ }
+
+ elf_release_view (state, &shdrs_view, error_callback, data);
+ shdrs_view_valid = 0;
+ elf_release_view (state, &names_view, error_callback, data);
+ names_view_valid = 0;
+
+ /* If the debug info is in a separate file, read that one instead. */
+
+ if (buildid_data != NULL)
+ {
+ int d;
+
+ d = elf_open_debugfile_by_buildid (state, buildid_data, buildid_size,
+ error_callback, data);
+ if (d >= 0)
+ {
+ int ret;
+
+ elf_release_view (state, &buildid_view, error_callback, data);
+ if (debuglink_view_valid)
+ elf_release_view (state, &debuglink_view, error_callback, data);
+ if (debugaltlink_view_valid)
+ elf_release_view (state, &debugaltlink_view, error_callback, data);
+ ret = elf_add (state, "", d, NULL, 0, base_address, error_callback,
+ data, fileline_fn, found_sym, found_dwarf, NULL, 0,
+ 1, NULL, 0);
+ if (ret < 0)
+ backtrace_close (d, error_callback, data);
+ else if (descriptor >= 0)
+ backtrace_close (descriptor, error_callback, data);
+ return ret;
+ }
+ }
+
+ if (buildid_view_valid)
+ {
+ elf_release_view (state, &buildid_view, error_callback, data);
+ buildid_view_valid = 0;
+ }
+
+ if (opd)
+ {
+ elf_release_view (state, &opd->view, error_callback, data);
+ opd = NULL;
+ }
+
+ if (debuglink_name != NULL)
+ {
+ int d;
+
+ d = elf_open_debugfile_by_debuglink (state, filename, debuglink_name,
+ debuglink_crc, error_callback,
+ data);
+ if (d >= 0)
+ {
+ int ret;
+
+ elf_release_view (state, &debuglink_view, error_callback, data);
+ if (debugaltlink_view_valid)
+ elf_release_view (state, &debugaltlink_view, error_callback, data);
+ ret = elf_add (state, "", d, NULL, 0, base_address, error_callback,
+ data, fileline_fn, found_sym, found_dwarf, NULL, 0,
+ 1, NULL, 0);
+ if (ret < 0)
+ backtrace_close (d, error_callback, data);
+ else if (descriptor >= 0)
+ backtrace_close(descriptor, error_callback, data);
+ return ret;
+ }
+ }
+
+ if (debuglink_view_valid)
+ {
+ elf_release_view (state, &debuglink_view, error_callback, data);
+ debuglink_view_valid = 0;
+ }
+
+ struct dwarf_data *fileline_altlink = NULL;
+ if (debugaltlink_name != NULL)
+ {
+ int d;
+
+ d = elf_open_debugfile_by_debuglink (state, filename, debugaltlink_name,
+ 0, error_callback, data);
+ if (d >= 0)
+ {
+ int ret;
+
+ ret = elf_add (state, filename, d, NULL, 0, base_address,
+ error_callback, data, fileline_fn, found_sym,
+ found_dwarf, &fileline_altlink, 0, 1,
+ debugaltlink_buildid_data, debugaltlink_buildid_size);
+ elf_release_view (state, &debugaltlink_view, error_callback, data);
+ debugaltlink_view_valid = 0;
+ if (ret < 0)
+ {
+ backtrace_close (d, error_callback, data);
+ return ret;
+ }
+ }
+ }
+
+ if (debugaltlink_view_valid)
+ {
+ elf_release_view (state, &debugaltlink_view, error_callback, data);
+ debugaltlink_view_valid = 0;
+ }
+
+ if (gnu_debugdata_view_valid)
+ {
+ int ret;
+
+ ret = elf_uncompress_lzma (state,
+ ((const unsigned char *)
+ gnu_debugdata_view.view.data),
+ gnu_debugdata_size, error_callback, data,
+ &gnu_debugdata_uncompressed,
+ &gnu_debugdata_uncompressed_size);
+
+ elf_release_view (state, &gnu_debugdata_view, error_callback, data);
+ gnu_debugdata_view_valid = 0;
+
+ if (ret)
+ {
+ ret = elf_add (state, filename, -1, gnu_debugdata_uncompressed,
+ gnu_debugdata_uncompressed_size, base_address,
+ error_callback, data, fileline_fn, found_sym,
+ found_dwarf, NULL, 0, 0, NULL, 0);
+ if (ret >= 0 && descriptor >= 0)
+ backtrace_close(descriptor, error_callback, data);
+ return ret;
+ }
+ }
+
+ /* Read all the debug sections in a single view, since they are
+ probably adjacent in the file. If any of sections are
+ uncompressed, we never release this view. */
+
+ min_offset = 0;
+ max_offset = 0;
+ debug_size = 0;
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ off_t end;
+
+ if (sections[i].size != 0)
+ {
+ if (min_offset == 0 || sections[i].offset < min_offset)
+ min_offset = sections[i].offset;
+ end = sections[i].offset + sections[i].size;
+ if (end > max_offset)
+ max_offset = end;
+ debug_size += sections[i].size;
+ }
+ if (zsections[i].size != 0)
+ {
+ if (min_offset == 0 || zsections[i].offset < min_offset)
+ min_offset = zsections[i].offset;
+ end = zsections[i].offset + zsections[i].size;
+ if (end > max_offset)
+ max_offset = end;
+ debug_size += zsections[i].size;
+ }
+ }
+ if (min_offset == 0 || max_offset == 0)
+ {
+ if (descriptor >= 0)
+ {
+ if (!backtrace_close (descriptor, error_callback, data))
+ goto fail;
+ }
+ return 1;
+ }
+
+ /* If the total debug section size is large, assume that there are
+ gaps between the sections, and read them individually. */
+
+ if (max_offset - min_offset < 0x20000000
+ || max_offset - min_offset < debug_size + 0x10000)
+ {
+ if (!elf_get_view (state, descriptor, memory, memory_size, min_offset,
+ max_offset - min_offset, error_callback, data,
+ &debug_view))
+ goto fail;
+ debug_view_valid = 1;
+ }
+ else
+ {
+ memset (&split_debug_view[0], 0, sizeof split_debug_view);
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ struct debug_section_info *dsec;
+
+ if (sections[i].size != 0)
+ dsec = &sections[i];
+ else if (zsections[i].size != 0)
+ dsec = &zsections[i];
+ else
+ continue;
+
+ if (!elf_get_view (state, descriptor, memory, memory_size,
+ dsec->offset, dsec->size, error_callback, data,
+ &split_debug_view[i]))
+ goto fail;
+ split_debug_view_valid[i] = 1;
+
+ if (sections[i].size != 0)
+ sections[i].data = ((const unsigned char *)
+ split_debug_view[i].view.data);
+ else
+ zsections[i].data = ((const unsigned char *)
+ split_debug_view[i].view.data);
+ }
+ }
+
+ /* We've read all we need from the executable. */
+ if (descriptor >= 0)
+ {
+ if (!backtrace_close (descriptor, error_callback, data))
+ goto fail;
+ descriptor = -1;
+ }
+
+ using_debug_view = 0;
+ if (debug_view_valid)
+ {
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ if (sections[i].size == 0)
+ sections[i].data = NULL;
+ else
+ {
+ sections[i].data = ((const unsigned char *) debug_view.view.data
+ + (sections[i].offset - min_offset));
+ ++using_debug_view;
+ }
+
+ if (zsections[i].size == 0)
+ zsections[i].data = NULL;
+ else
+ zsections[i].data = ((const unsigned char *) debug_view.view.data
+ + (zsections[i].offset - min_offset));
+ }
+ }
+
+ /* Uncompress the old format (--compress-debug-sections=zlib-gnu). */
+
+ zdebug_table = NULL;
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ if (sections[i].size == 0 && zsections[i].size > 0)
+ {
+ unsigned char *uncompressed_data;
+ size_t uncompressed_size;
+
+ if (zdebug_table == NULL)
+ {
+ zdebug_table = ((uint16_t *)
+ backtrace_alloc (state, ZDEBUG_TABLE_SIZE,
+ error_callback, data));
+ if (zdebug_table == NULL)
+ goto fail;
+ }
+
+ uncompressed_data = NULL;
+ uncompressed_size = 0;
+ if (!elf_uncompress_zdebug (state, zsections[i].data,
+ zsections[i].size, zdebug_table,
+ error_callback, data,
+ &uncompressed_data, &uncompressed_size))
+ goto fail;
+ sections[i].data = uncompressed_data;
+ sections[i].size = uncompressed_size;
+ sections[i].compressed = 0;
+
+ if (split_debug_view_valid[i])
+ {
+ elf_release_view (state, &split_debug_view[i],
+ error_callback, data);
+ split_debug_view_valid[i] = 0;
+ }
+ }
+ }
+
+ /* Uncompress the official ELF format
+ (--compress-debug-sections=zlib-gabi). */
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ unsigned char *uncompressed_data;
+ size_t uncompressed_size;
+
+ if (sections[i].size == 0 || !sections[i].compressed)
+ continue;
+
+ if (zdebug_table == NULL)
+ {
+ zdebug_table = ((uint16_t *)
+ backtrace_alloc (state, ZDEBUG_TABLE_SIZE,
+ error_callback, data));
+ if (zdebug_table == NULL)
+ goto fail;
+ }
+
+ uncompressed_data = NULL;
+ uncompressed_size = 0;
+ if (!elf_uncompress_chdr (state, sections[i].data, sections[i].size,
+ zdebug_table, error_callback, data,
+ &uncompressed_data, &uncompressed_size))
+ goto fail;
+ sections[i].data = uncompressed_data;
+ sections[i].size = uncompressed_size;
+ sections[i].compressed = 0;
+
+ if (debug_view_valid)
+ --using_debug_view;
+ else if (split_debug_view_valid[i])
+ {
+ elf_release_view (state, &split_debug_view[i], error_callback, data);
+ split_debug_view_valid[i] = 0;
+ }
+ }
+
+ if (zdebug_table != NULL)
+ backtrace_free (state, zdebug_table, ZDEBUG_TABLE_SIZE,
+ error_callback, data);
+
+ if (debug_view_valid && using_debug_view == 0)
+ {
+ elf_release_view (state, &debug_view, error_callback, data);
+ debug_view_valid = 0;
+ }
+
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ dwarf_sections.data[i] = sections[i].data;
+ dwarf_sections.size[i] = sections[i].size;
+ }
+
+ if (!backtrace_dwarf_add (state, base_address, &dwarf_sections,
+ ehdr.e_ident[EI_DATA] == ELFDATA2MSB,
+ fileline_altlink,
+ error_callback, data, fileline_fn,
+ fileline_entry))
+ goto fail;
+
+ *found_dwarf = 1;
+
+ return 1;
+
+ fail:
+ if (shdrs_view_valid)
+ elf_release_view (state, &shdrs_view, error_callback, data);
+ if (names_view_valid)
+ elf_release_view (state, &names_view, error_callback, data);
+ if (symtab_view_valid)
+ elf_release_view (state, &symtab_view, error_callback, data);
+ if (strtab_view_valid)
+ elf_release_view (state, &strtab_view, error_callback, data);
+ if (debuglink_view_valid)
+ elf_release_view (state, &debuglink_view, error_callback, data);
+ if (debugaltlink_view_valid)
+ elf_release_view (state, &debugaltlink_view, error_callback, data);
+ if (gnu_debugdata_view_valid)
+ elf_release_view (state, &gnu_debugdata_view, error_callback, data);
+ if (buildid_view_valid)
+ elf_release_view (state, &buildid_view, error_callback, data);
+ if (debug_view_valid)
+ elf_release_view (state, &debug_view, error_callback, data);
+ for (i = 0; i < (int) DEBUG_MAX; ++i)
+ {
+ if (split_debug_view_valid[i])
+ elf_release_view (state, &split_debug_view[i], error_callback, data);
+ }
+ if (opd)
+ elf_release_view (state, &opd->view, error_callback, data);
+ if (descriptor >= 0)
+ backtrace_close (descriptor, error_callback, data);
+ return 0;
+}
+
+/* Data passed to phdr_callback. */
+
+struct phdr_data
+{
+ struct backtrace_state *state;
+ backtrace_error_callback error_callback;
+ void *data;
+ fileline *fileline_fn;
+ int *found_sym;
+ int *found_dwarf;
+ const char *exe_filename;
+ int exe_descriptor;
+};
+
+/* Callback passed to dl_iterate_phdr. Load debug info from shared
+ libraries. */
+
+static int
+#ifdef __i386__
+__attribute__ ((__force_align_arg_pointer__))
+#endif
+phdr_callback (struct dl_phdr_info *info, size_t size ATTRIBUTE_UNUSED,
+ void *pdata)
+{
+ struct phdr_data *pd = (struct phdr_data *) pdata;
+ const char *filename;
+ int descriptor;
+ int does_not_exist;
+ fileline elf_fileline_fn;
+ int found_dwarf;
+
+ /* There is not much we can do if we don't have the module name,
+ unless executable is ET_DYN, where we expect the very first
+ phdr_callback to be for the PIE. */
+ if (info->dlpi_name == NULL || info->dlpi_name[0] == '\0')
+ {
+ if (pd->exe_descriptor == -1)
+ return 0;
+ filename = pd->exe_filename;
+ descriptor = pd->exe_descriptor;
+ pd->exe_descriptor = -1;
+ }
+ else
+ {
+ if (pd->exe_descriptor != -1)
+ {
+ backtrace_close (pd->exe_descriptor, pd->error_callback, pd->data);
+ pd->exe_descriptor = -1;
+ }
+
+ filename = info->dlpi_name;
+ descriptor = backtrace_open (info->dlpi_name, pd->error_callback,
+ pd->data, &does_not_exist);
+ if (descriptor < 0)
+ return 0;
+ }
+
+ if (elf_add (pd->state, filename, descriptor, NULL, 0, info->dlpi_addr,
+ pd->error_callback, pd->data, &elf_fileline_fn, pd->found_sym,
+ &found_dwarf, NULL, 0, 0, NULL, 0))
+ {
+ if (found_dwarf)
+ {
+ *pd->found_dwarf = 1;
+ *pd->fileline_fn = elf_fileline_fn;
+ }
+ }
+
+ return 0;
+}
+
+/* Initialize the backtrace data we need from an ELF executable. At
+ the ELF level, all we need to do is find the debug info
+ sections. */
+
+int
+backtrace_initialize (struct backtrace_state *state, const char *filename,
+ int descriptor, backtrace_error_callback error_callback,
+ void *data, fileline *fileline_fn)
+{
+ int ret;
+ int found_sym;
+ int found_dwarf;
+ fileline elf_fileline_fn = elf_nodebug;
+ struct phdr_data pd;
+
+ ret = elf_add (state, filename, descriptor, NULL, 0, 0, error_callback, data,
+ &elf_fileline_fn, &found_sym, &found_dwarf, NULL, 1, 0, NULL,
+ 0);
+ if (!ret)
+ return 0;
+
+ pd.state = state;
+ pd.error_callback = error_callback;
+ pd.data = data;
+ pd.fileline_fn = &elf_fileline_fn;
+ pd.found_sym = &found_sym;
+ pd.found_dwarf = &found_dwarf;
+ pd.exe_filename = filename;
+ pd.exe_descriptor = ret < 0 ? descriptor : -1;
+
+ dl_iterate_phdr (phdr_callback, (void *) &pd);
+
+ if (!state->threaded)
+ {
+ if (found_sym)
+ state->syminfo_fn = elf_syminfo;
+ else if (state->syminfo_fn == NULL)
+ state->syminfo_fn = elf_nosyms;
+ }
+ else
+ {
+ if (found_sym)
+ backtrace_atomic_store_pointer (&state->syminfo_fn, elf_syminfo);
+ else
+ (void) __sync_bool_compare_and_swap (&state->syminfo_fn, NULL,
+ elf_nosyms);
+ }
+
+ if (!state->threaded)
+ *fileline_fn = state->fileline_fn;
+ else
+ *fileline_fn = backtrace_atomic_load_pointer (&state->fileline_fn);
+
+ if (*fileline_fn == NULL || *fileline_fn == elf_nodebug)
+ *fileline_fn = elf_fileline_fn;
+
+ return 1;
+}
diff --git a/contrib/libs/backtrace/fileline.c b/contrib/libs/backtrace/fileline.c
new file mode 100644
index 0000000000..0472f4721a
--- /dev/null
+++ b/contrib/libs/backtrace/fileline.c
@@ -0,0 +1,346 @@
+/* fileline.c -- Get file and line number information in a backtrace.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if defined (HAVE_KERN_PROC_ARGS) || defined (HAVE_KERN_PROC)
+#include <sys/sysctl.h>
+#endif
+
+#ifdef HAVE_MACH_O_DYLD_H
+#include <mach-o/dyld.h>
+#endif
+
+#include "backtrace.h"
+#include "internal.h"
+
+#ifndef HAVE_GETEXECNAME
+#define getexecname() NULL
+#endif
+
+#if !defined (HAVE_KERN_PROC_ARGS) && !defined (HAVE_KERN_PROC)
+
+#define sysctl_exec_name1(state, error_callback, data) NULL
+#define sysctl_exec_name2(state, error_callback, data) NULL
+
+#else /* defined (HAVE_KERN_PROC_ARGS) || |defined (HAVE_KERN_PROC) */
+
+static char *
+sysctl_exec_name (struct backtrace_state *state,
+ int mib0, int mib1, int mib2, int mib3,
+ backtrace_error_callback error_callback, void *data)
+{
+ int mib[4];
+ size_t len;
+ char *name;
+ size_t rlen;
+
+ mib[0] = mib0;
+ mib[1] = mib1;
+ mib[2] = mib2;
+ mib[3] = mib3;
+
+ if (sysctl (mib, 4, NULL, &len, NULL, 0) < 0)
+ return NULL;
+ name = (char *) backtrace_alloc (state, len, error_callback, data);
+ if (name == NULL)
+ return NULL;
+ rlen = len;
+ if (sysctl (mib, 4, name, &rlen, NULL, 0) < 0)
+ {
+ backtrace_free (state, name, len, error_callback, data);
+ return NULL;
+ }
+ return name;
+}
+
+#ifdef HAVE_KERN_PROC_ARGS
+
+static char *
+sysctl_exec_name1 (struct backtrace_state *state,
+ backtrace_error_callback error_callback, void *data)
+{
+ /* This variant is used on NetBSD. */
+ return sysctl_exec_name (state, CTL_KERN, KERN_PROC_ARGS, -1,
+ KERN_PROC_PATHNAME, error_callback, data);
+}
+
+#else
+
+#define sysctl_exec_name1(state, error_callback, data) NULL
+
+#endif
+
+#ifdef HAVE_KERN_PROC
+
+static char *
+sysctl_exec_name2 (struct backtrace_state *state,
+ backtrace_error_callback error_callback, void *data)
+{
+ /* This variant is used on FreeBSD. */
+ return sysctl_exec_name (state, CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1,
+ error_callback, data);
+}
+
+#else
+
+#define sysctl_exec_name2(state, error_callback, data) NULL
+
+#endif
+
+#endif /* defined (HAVE_KERN_PROC_ARGS) || |defined (HAVE_KERN_PROC) */
+
+#ifdef HAVE_MACH_O_DYLD_H
+
+static char *
+macho_get_executable_path (struct backtrace_state *state,
+ backtrace_error_callback error_callback, void *data)
+{
+ uint32_t len;
+ char *name;
+
+ len = 0;
+ if (_NSGetExecutablePath (NULL, &len) == 0)
+ return NULL;
+ name = (char *) backtrace_alloc (state, len, error_callback, data);
+ if (name == NULL)
+ return NULL;
+ if (_NSGetExecutablePath (name, &len) != 0)
+ {
+ backtrace_free (state, name, len, error_callback, data);
+ return NULL;
+ }
+ return name;
+}
+
+#else /* !defined (HAVE_MACH_O_DYLD_H) */
+
+#define macho_get_executable_path(state, error_callback, data) NULL
+
+#endif /* !defined (HAVE_MACH_O_DYLD_H) */
+
+/* Initialize the fileline information from the executable. Returns 1
+ on success, 0 on failure. */
+
+static int
+fileline_initialize (struct backtrace_state *state,
+ backtrace_error_callback error_callback, void *data)
+{
+ int failed;
+ fileline fileline_fn;
+ int pass;
+ int called_error_callback;
+ int descriptor;
+ const char *filename;
+ char buf[64];
+
+ if (!state->threaded)
+ failed = state->fileline_initialization_failed;
+ else
+ failed = backtrace_atomic_load_int (&state->fileline_initialization_failed);
+
+ if (failed)
+ {
+ error_callback (data, "failed to read executable information", -1);
+ return 0;
+ }
+
+ if (!state->threaded)
+ fileline_fn = state->fileline_fn;
+ else
+ fileline_fn = backtrace_atomic_load_pointer (&state->fileline_fn);
+ if (fileline_fn != NULL)
+ return 1;
+
+ /* We have not initialized the information. Do it now. */
+
+ descriptor = -1;
+ called_error_callback = 0;
+ for (pass = 0; pass < 8; ++pass)
+ {
+ int does_not_exist;
+
+ switch (pass)
+ {
+ case 0:
+ filename = state->filename;
+ break;
+ case 1:
+ filename = getexecname ();
+ break;
+ case 2:
+ filename = "/proc/self/exe";
+ break;
+ case 3:
+ filename = "/proc/curproc/file";
+ break;
+ case 4:
+ snprintf (buf, sizeof (buf), "/proc/%ld/object/a.out",
+ (long) getpid ());
+ filename = buf;
+ break;
+ case 5:
+ filename = sysctl_exec_name1 (state, error_callback, data);
+ break;
+ case 6:
+ filename = sysctl_exec_name2 (state, error_callback, data);
+ break;
+ case 7:
+ filename = macho_get_executable_path (state, error_callback, data);
+ break;
+ default:
+ abort ();
+ }
+
+ if (filename == NULL)
+ continue;
+
+ descriptor = backtrace_open (filename, error_callback, data,
+ &does_not_exist);
+ if (descriptor < 0 && !does_not_exist)
+ {
+ called_error_callback = 1;
+ break;
+ }
+ if (descriptor >= 0)
+ break;
+ }
+
+ if (descriptor < 0)
+ {
+ if (!called_error_callback)
+ {
+ if (state->filename != NULL)
+ error_callback (data, state->filename, ENOENT);
+ else
+ error_callback (data,
+ "libbacktrace could not find executable to open",
+ 0);
+ }
+ failed = 1;
+ }
+
+ if (!failed)
+ {
+ if (!backtrace_initialize (state, filename, descriptor, error_callback,
+ data, &fileline_fn))
+ failed = 1;
+ }
+
+ if (failed)
+ {
+ if (!state->threaded)
+ state->fileline_initialization_failed = 1;
+ else
+ backtrace_atomic_store_int (&state->fileline_initialization_failed, 1);
+ return 0;
+ }
+
+ if (!state->threaded)
+ state->fileline_fn = fileline_fn;
+ else
+ {
+ backtrace_atomic_store_pointer (&state->fileline_fn, fileline_fn);
+
+ /* Note that if two threads initialize at once, one of the data
+ sets may be leaked. */
+ }
+
+ return 1;
+}
+
+/* Given a PC, find the file name, line number, and function name. */
+
+int
+backtrace_pcinfo (struct backtrace_state *state, uintptr_t pc,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ if (!fileline_initialize (state, error_callback, data))
+ return 0;
+
+ if (state->fileline_initialization_failed)
+ return 0;
+
+ return state->fileline_fn (state, pc, callback, error_callback, data);
+}
+
+/* Given a PC, find the symbol for it, and its value. */
+
+int
+backtrace_syminfo (struct backtrace_state *state, uintptr_t pc,
+ backtrace_syminfo_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ if (!fileline_initialize (state, error_callback, data))
+ return 0;
+
+ if (state->fileline_initialization_failed)
+ return 0;
+
+ state->syminfo_fn (state, pc, callback, error_callback, data);
+ return 1;
+}
+
+/* A backtrace_syminfo_callback that can call into a
+ backtrace_full_callback, used when we have a symbol table but no
+ debug info. */
+
+void
+backtrace_syminfo_to_full_callback (void *data, uintptr_t pc,
+ const char *symname,
+ uintptr_t symval ATTRIBUTE_UNUSED,
+ uintptr_t symsize ATTRIBUTE_UNUSED)
+{
+ struct backtrace_call_full *bdata = (struct backtrace_call_full *) data;
+
+ bdata->ret = bdata->full_callback (bdata->full_data, pc, NULL, 0, symname);
+}
+
+/* An error callback that corresponds to
+ backtrace_syminfo_to_full_callback. */
+
+void
+backtrace_syminfo_to_full_error_callback (void *data, const char *msg,
+ int errnum)
+{
+ struct backtrace_call_full *bdata = (struct backtrace_call_full *) data;
+
+ bdata->full_error_callback (bdata->full_data, msg, errnum);
+}
diff --git a/contrib/libs/backtrace/filenames.h b/contrib/libs/backtrace/filenames.h
new file mode 100644
index 0000000000..aa7bd7adff
--- /dev/null
+++ b/contrib/libs/backtrace/filenames.h
@@ -0,0 +1,52 @@
+/* btest.c -- Filename header for libbacktrace library
+ Copyright (C) 2012-2018 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#ifndef GCC_VERSION
+# define GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__)
+#endif
+
+#if (GCC_VERSION < 2007)
+# define __attribute__(x)
+#endif
+
+#ifndef ATTRIBUTE_UNUSED
+# define ATTRIBUTE_UNUSED __attribute__ ((__unused__))
+#endif
+
+#if defined(__MSDOS__) || defined(_WIN32) || defined(__OS2__) || defined (__CYGWIN__)
+# define IS_DIR_SEPARATOR(c) ((c) == '/' || (c) == '\\')
+# define HAS_DRIVE_SPEC(f) ((f)[0] != '\0' && (f)[1] == ':')
+# define IS_ABSOLUTE_PATH(f) (IS_DIR_SEPARATOR((f)[0]) || HAS_DRIVE_SPEC(f))
+#else
+# define IS_DIR_SEPARATOR(c) ((c) == '/')
+# define IS_ABSOLUTE_PATH(f) (IS_DIR_SEPARATOR((f)[0]))
+#endif
diff --git a/contrib/libs/backtrace/internal.h b/contrib/libs/backtrace/internal.h
new file mode 100644
index 0000000000..bb481f373b
--- /dev/null
+++ b/contrib/libs/backtrace/internal.h
@@ -0,0 +1,380 @@
+/* internal.h -- Internal header file for stack backtrace library.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#ifndef BACKTRACE_INTERNAL_H
+#define BACKTRACE_INTERNAL_H
+
+/* We assume that <sys/types.h> and "backtrace.h" have already been
+ included. */
+
+#ifndef GCC_VERSION
+# define GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__)
+#endif
+
+#if (GCC_VERSION < 2007)
+# define __attribute__(x)
+#endif
+
+#ifndef ATTRIBUTE_UNUSED
+# define ATTRIBUTE_UNUSED __attribute__ ((__unused__))
+#endif
+
+#ifndef ATTRIBUTE_MALLOC
+# if (GCC_VERSION >= 2096)
+# define ATTRIBUTE_MALLOC __attribute__ ((__malloc__))
+# else
+# define ATTRIBUTE_MALLOC
+# endif
+#endif
+
+#ifndef ATTRIBUTE_FALLTHROUGH
+# if (GCC_VERSION >= 7000)
+# define ATTRIBUTE_FALLTHROUGH __attribute__ ((__fallthrough__))
+# else
+# define ATTRIBUTE_FALLTHROUGH
+# endif
+#endif
+
+#ifndef HAVE_SYNC_FUNCTIONS
+
+/* Define out the sync functions. These should never be called if
+ they are not available. */
+
+#define __sync_bool_compare_and_swap(A, B, C) (abort(), 1)
+#define __sync_lock_test_and_set(A, B) (abort(), 0)
+#define __sync_lock_release(A) abort()
+
+#endif /* !defined (HAVE_SYNC_FUNCTIONS) */
+
+#ifdef HAVE_ATOMIC_FUNCTIONS
+
+/* We have the atomic builtin functions. */
+
+#define backtrace_atomic_load_pointer(p) \
+ __atomic_load_n ((p), __ATOMIC_ACQUIRE)
+#define backtrace_atomic_load_int(p) \
+ __atomic_load_n ((p), __ATOMIC_ACQUIRE)
+#define backtrace_atomic_store_pointer(p, v) \
+ __atomic_store_n ((p), (v), __ATOMIC_RELEASE)
+#define backtrace_atomic_store_size_t(p, v) \
+ __atomic_store_n ((p), (v), __ATOMIC_RELEASE)
+#define backtrace_atomic_store_int(p, v) \
+ __atomic_store_n ((p), (v), __ATOMIC_RELEASE)
+
+#else /* !defined (HAVE_ATOMIC_FUNCTIONS) */
+#ifdef HAVE_SYNC_FUNCTIONS
+
+/* We have the sync functions but not the atomic functions. Define
+ the atomic ones in terms of the sync ones. */
+
+extern void *backtrace_atomic_load_pointer (void *);
+extern int backtrace_atomic_load_int (int *);
+extern void backtrace_atomic_store_pointer (void *, void *);
+extern void backtrace_atomic_store_size_t (size_t *, size_t);
+extern void backtrace_atomic_store_int (int *, int);
+
+#else /* !defined (HAVE_SYNC_FUNCTIONS) */
+
+/* We have neither the sync nor the atomic functions. These will
+ never be called. */
+
+#define backtrace_atomic_load_pointer(p) (abort(), (void *) NULL)
+#define backtrace_atomic_load_int(p) (abort(), 0)
+#define backtrace_atomic_store_pointer(p, v) abort()
+#define backtrace_atomic_store_size_t(p, v) abort()
+#define backtrace_atomic_store_int(p, v) abort()
+
+#endif /* !defined (HAVE_SYNC_FUNCTIONS) */
+#endif /* !defined (HAVE_ATOMIC_FUNCTIONS) */
+
+/* The type of the function that collects file/line information. This
+ is like backtrace_pcinfo. */
+
+typedef int (*fileline) (struct backtrace_state *state, uintptr_t pc,
+ backtrace_full_callback callback,
+ backtrace_error_callback error_callback, void *data);
+
+/* The type of the function that collects symbol information. This is
+ like backtrace_syminfo. */
+
+typedef void (*syminfo) (struct backtrace_state *state, uintptr_t pc,
+ backtrace_syminfo_callback callback,
+ backtrace_error_callback error_callback, void *data);
+
+/* What the backtrace state pointer points to. */
+
+struct backtrace_state
+{
+ /* The name of the executable. */
+ const char *filename;
+ /* Non-zero if threaded. */
+ int threaded;
+ /* The master lock for fileline_fn, fileline_data, syminfo_fn,
+ syminfo_data, fileline_initialization_failed and everything the
+ data pointers point to. */
+ void *lock;
+ /* The function that returns file/line information. */
+ fileline fileline_fn;
+ /* The data to pass to FILELINE_FN. */
+ void *fileline_data;
+ /* The function that returns symbol information. */
+ syminfo syminfo_fn;
+ /* The data to pass to SYMINFO_FN. */
+ void *syminfo_data;
+ /* Whether initializing the file/line information failed. */
+ int fileline_initialization_failed;
+ /* The lock for the freelist. */
+ int lock_alloc;
+ /* The freelist when using mmap. */
+ struct backtrace_freelist_struct *freelist;
+};
+
+/* Open a file for reading. Returns -1 on error. If DOES_NOT_EXIST
+ is not NULL, *DOES_NOT_EXIST will be set to 0 normally and set to 1
+ if the file does not exist. If the file does not exist and
+ DOES_NOT_EXIST is not NULL, the function will return -1 and will
+ not call ERROR_CALLBACK. On other errors, or if DOES_NOT_EXIST is
+ NULL, the function will call ERROR_CALLBACK before returning. */
+extern int backtrace_open (const char *filename,
+ backtrace_error_callback error_callback,
+ void *data,
+ int *does_not_exist);
+
+/* A view of the contents of a file. This supports mmap when
+ available. A view will remain in memory even after backtrace_close
+ is called on the file descriptor from which the view was
+ obtained. */
+
+struct backtrace_view
+{
+ /* The data that the caller requested. */
+ const void *data;
+ /* The base of the view. */
+ void *base;
+ /* The total length of the view. */
+ size_t len;
+};
+
+/* Create a view of SIZE bytes from DESCRIPTOR at OFFSET. Store the
+ result in *VIEW. Returns 1 on success, 0 on error. */
+extern int backtrace_get_view (struct backtrace_state *state, int descriptor,
+ off_t offset, uint64_t size,
+ backtrace_error_callback error_callback,
+ void *data, struct backtrace_view *view);
+
+/* Release a view created by backtrace_get_view. */
+extern void backtrace_release_view (struct backtrace_state *state,
+ struct backtrace_view *view,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* Close a file opened by backtrace_open. Returns 1 on success, 0 on
+ error. */
+
+extern int backtrace_close (int descriptor,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* Sort without using memory. */
+
+extern void backtrace_qsort (void *base, size_t count, size_t size,
+ int (*compar) (const void *, const void *));
+
+/* Allocate memory. This is like malloc. If ERROR_CALLBACK is NULL,
+ this does not report an error, it just returns NULL. */
+
+extern void *backtrace_alloc (struct backtrace_state *state, size_t size,
+ backtrace_error_callback error_callback,
+ void *data) ATTRIBUTE_MALLOC;
+
+/* Free memory allocated by backtrace_alloc. If ERROR_CALLBACK is
+ NULL, this does not report an error. */
+
+extern void backtrace_free (struct backtrace_state *state, void *mem,
+ size_t size,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* A growable vector of some struct. This is used for more efficient
+ allocation when we don't know the final size of some group of data
+ that we want to represent as an array. */
+
+struct backtrace_vector
+{
+ /* The base of the vector. */
+ void *base;
+ /* The number of bytes in the vector. */
+ size_t size;
+ /* The number of bytes available at the current allocation. */
+ size_t alc;
+};
+
+/* Grow VEC by SIZE bytes. Return a pointer to the newly allocated
+ bytes. Note that this may move the entire vector to a new memory
+ location. Returns NULL on failure. */
+
+extern void *backtrace_vector_grow (struct backtrace_state *state, size_t size,
+ backtrace_error_callback error_callback,
+ void *data,
+ struct backtrace_vector *vec);
+
+/* Finish the current allocation on VEC. Prepare to start a new
+ allocation. The finished allocation will never be freed. Returns
+ a pointer to the base of the finished entries, or NULL on
+ failure. */
+
+extern void* backtrace_vector_finish (struct backtrace_state *state,
+ struct backtrace_vector *vec,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* Release any extra space allocated for VEC. This may change
+ VEC->base. Returns 1 on success, 0 on failure. */
+
+extern int backtrace_vector_release (struct backtrace_state *state,
+ struct backtrace_vector *vec,
+ backtrace_error_callback error_callback,
+ void *data);
+
+/* Free the space managed by VEC. This will reset VEC. */
+
+static inline void
+backtrace_vector_free (struct backtrace_state *state,
+ struct backtrace_vector *vec,
+ backtrace_error_callback error_callback, void *data)
+{
+ vec->alc += vec->size;
+ vec->size = 0;
+ backtrace_vector_release (state, vec, error_callback, data);
+}
+
+/* Read initial debug data from a descriptor, and set the
+ fileline_data, syminfo_fn, and syminfo_data fields of STATE.
+ Return the fileln_fn field in *FILELN_FN--this is done this way so
+ that the synchronization code is only implemented once. This is
+ called after the descriptor has first been opened. It will close
+ the descriptor if it is no longer needed. Returns 1 on success, 0
+ on error. There will be multiple implementations of this function,
+ for different file formats. Each system will compile the
+ appropriate one. */
+
+extern int backtrace_initialize (struct backtrace_state *state,
+ const char *filename,
+ int descriptor,
+ backtrace_error_callback error_callback,
+ void *data,
+ fileline *fileline_fn);
+
+/* An enum for the DWARF sections we care about. */
+
+enum dwarf_section
+{
+ DEBUG_INFO,
+ DEBUG_LINE,
+ DEBUG_ABBREV,
+ DEBUG_RANGES,
+ DEBUG_STR,
+ DEBUG_ADDR,
+ DEBUG_STR_OFFSETS,
+ DEBUG_LINE_STR,
+ DEBUG_RNGLISTS,
+
+ DEBUG_MAX
+};
+
+/* Data for the DWARF sections we care about. */
+
+struct dwarf_sections
+{
+ const unsigned char *data[DEBUG_MAX];
+ size_t size[DEBUG_MAX];
+};
+
+/* DWARF data read from a file, used for .gnu_debugaltlink. */
+
+struct dwarf_data;
+
+/* Add file/line information for a DWARF module. */
+
+extern int backtrace_dwarf_add (struct backtrace_state *state,
+ uintptr_t base_address,
+ const struct dwarf_sections *dwarf_sections,
+ int is_bigendian,
+ struct dwarf_data *fileline_altlink,
+ backtrace_error_callback error_callback,
+ void *data, fileline *fileline_fn,
+ struct dwarf_data **fileline_entry);
+
+/* A data structure to pass to backtrace_syminfo_to_full. */
+
+struct backtrace_call_full
+{
+ backtrace_full_callback full_callback;
+ backtrace_error_callback full_error_callback;
+ void *full_data;
+ int ret;
+};
+
+/* A backtrace_syminfo_callback that can call into a
+ backtrace_full_callback, used when we have a symbol table but no
+ debug info. */
+
+extern void backtrace_syminfo_to_full_callback (void *data, uintptr_t pc,
+ const char *symname,
+ uintptr_t symval,
+ uintptr_t symsize);
+
+/* An error callback that corresponds to
+ backtrace_syminfo_to_full_callback. */
+
+extern void backtrace_syminfo_to_full_error_callback (void *, const char *,
+ int);
+
+/* A test-only hook for elf_uncompress_zdebug. */
+
+extern int backtrace_uncompress_zdebug (struct backtrace_state *,
+ const unsigned char *compressed,
+ size_t compressed_size,
+ backtrace_error_callback, void *data,
+ unsigned char **uncompressed,
+ size_t *uncompressed_size);
+
+/* A test-only hook for elf_uncompress_lzma. */
+
+extern int backtrace_uncompress_lzma (struct backtrace_state *,
+ const unsigned char *compressed,
+ size_t compressed_size,
+ backtrace_error_callback, void *data,
+ unsigned char **uncompressed,
+ size_t *uncompressed_size);
+
+#endif
diff --git a/contrib/libs/backtrace/mmap.c b/contrib/libs/backtrace/mmap.c
new file mode 100644
index 0000000000..d7313be73f
--- /dev/null
+++ b/contrib/libs/backtrace/mmap.c
@@ -0,0 +1,331 @@
+/* mmap.c -- Memory allocation with mmap.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/mman.h>
+
+#include "backtrace.h"
+#include "internal.h"
+
+#ifndef HAVE_DECL_GETPAGESIZE
+extern int getpagesize (void);
+#endif
+
+/* Memory allocation on systems that provide anonymous mmap. This
+ permits the backtrace functions to be invoked from a signal
+ handler, assuming that mmap is async-signal safe. */
+
+#ifndef MAP_ANONYMOUS
+#define MAP_ANONYMOUS MAP_ANON
+#endif
+
+#ifndef MAP_FAILED
+#define MAP_FAILED ((void *)-1)
+#endif
+
+/* A list of free memory blocks. */
+
+struct backtrace_freelist_struct
+{
+ /* Next on list. */
+ struct backtrace_freelist_struct *next;
+ /* Size of this block, including this structure. */
+ size_t size;
+};
+
+/* Free memory allocated by backtrace_alloc. */
+
+static void
+backtrace_free_locked (struct backtrace_state *state, void *addr, size_t size)
+{
+ /* Just leak small blocks. We don't have to be perfect. Don't put
+ more than 16 entries on the free list, to avoid wasting time
+ searching when allocating a block. If we have more than 16
+ entries, leak the smallest entry. */
+
+ if (size >= sizeof (struct backtrace_freelist_struct))
+ {
+ size_t c;
+ struct backtrace_freelist_struct **ppsmall;
+ struct backtrace_freelist_struct **pp;
+ struct backtrace_freelist_struct *p;
+
+ c = 0;
+ ppsmall = NULL;
+ for (pp = &state->freelist; *pp != NULL; pp = &(*pp)->next)
+ {
+ if (ppsmall == NULL || (*pp)->size < (*ppsmall)->size)
+ ppsmall = pp;
+ ++c;
+ }
+ if (c >= 16)
+ {
+ if (size <= (*ppsmall)->size)
+ return;
+ *ppsmall = (*ppsmall)->next;
+ }
+
+ p = (struct backtrace_freelist_struct *) addr;
+ p->next = state->freelist;
+ p->size = size;
+ state->freelist = p;
+ }
+}
+
+/* Allocate memory like malloc. If ERROR_CALLBACK is NULL, don't
+ report an error. */
+
+void *
+backtrace_alloc (struct backtrace_state *state,
+ size_t size, backtrace_error_callback error_callback,
+ void *data)
+{
+ void *ret;
+ int locked;
+ struct backtrace_freelist_struct **pp;
+ size_t pagesize;
+ size_t asksize;
+ void *page;
+
+ ret = NULL;
+
+ /* If we can acquire the lock, then see if there is space on the
+ free list. If we can't acquire the lock, drop straight into
+ using mmap. __sync_lock_test_and_set returns the old state of
+ the lock, so we have acquired it if it returns 0. */
+
+ if (!state->threaded)
+ locked = 1;
+ else
+ locked = __sync_lock_test_and_set (&state->lock_alloc, 1) == 0;
+
+ if (locked)
+ {
+ for (pp = &state->freelist; *pp != NULL; pp = &(*pp)->next)
+ {
+ if ((*pp)->size >= size)
+ {
+ struct backtrace_freelist_struct *p;
+
+ p = *pp;
+ *pp = p->next;
+
+ /* Round for alignment; we assume that no type we care about
+ is more than 8 bytes. */
+ size = (size + 7) & ~ (size_t) 7;
+ if (size < p->size)
+ backtrace_free_locked (state, (char *) p + size,
+ p->size - size);
+
+ ret = (void *) p;
+
+ break;
+ }
+ }
+
+ if (state->threaded)
+ __sync_lock_release (&state->lock_alloc);
+ }
+
+ if (ret == NULL)
+ {
+ /* Allocate a new page. */
+
+ pagesize = getpagesize ();
+ asksize = (size + pagesize - 1) & ~ (pagesize - 1);
+ page = mmap (NULL, asksize, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (page == MAP_FAILED)
+ {
+ if (error_callback)
+ error_callback (data, "mmap", errno);
+ }
+ else
+ {
+ size = (size + 7) & ~ (size_t) 7;
+ if (size < asksize)
+ backtrace_free (state, (char *) page + size, asksize - size,
+ error_callback, data);
+
+ ret = page;
+ }
+ }
+
+ return ret;
+}
+
+/* Free memory allocated by backtrace_alloc. */
+
+void
+backtrace_free (struct backtrace_state *state, void *addr, size_t size,
+ backtrace_error_callback error_callback ATTRIBUTE_UNUSED,
+ void *data ATTRIBUTE_UNUSED)
+{
+ int locked;
+
+ /* If we are freeing a large aligned block, just release it back to
+ the system. This case arises when growing a vector for a large
+ binary with lots of debug info. Calling munmap here may cause us
+ to call mmap again if there is also a large shared library; we
+ just live with that. */
+ if (size >= 16 * 4096)
+ {
+ size_t pagesize;
+
+ pagesize = getpagesize ();
+ if (((uintptr_t) addr & (pagesize - 1)) == 0
+ && (size & (pagesize - 1)) == 0)
+ {
+ /* If munmap fails for some reason, just add the block to
+ the freelist. */
+ if (munmap (addr, size) == 0)
+ return;
+ }
+ }
+
+ /* If we can acquire the lock, add the new space to the free list.
+ If we can't acquire the lock, just leak the memory.
+ __sync_lock_test_and_set returns the old state of the lock, so we
+ have acquired it if it returns 0. */
+
+ if (!state->threaded)
+ locked = 1;
+ else
+ locked = __sync_lock_test_and_set (&state->lock_alloc, 1) == 0;
+
+ if (locked)
+ {
+ backtrace_free_locked (state, addr, size);
+
+ if (state->threaded)
+ __sync_lock_release (&state->lock_alloc);
+ }
+}
+
+/* Grow VEC by SIZE bytes. */
+
+void *
+backtrace_vector_grow (struct backtrace_state *state,size_t size,
+ backtrace_error_callback error_callback,
+ void *data, struct backtrace_vector *vec)
+{
+ void *ret;
+
+ if (size > vec->alc)
+ {
+ size_t pagesize;
+ size_t alc;
+ void *base;
+
+ pagesize = getpagesize ();
+ alc = vec->size + size;
+ if (vec->size == 0)
+ alc = 16 * size;
+ else if (alc < pagesize)
+ {
+ alc *= 2;
+ if (alc > pagesize)
+ alc = pagesize;
+ }
+ else
+ {
+ alc *= 2;
+ alc = (alc + pagesize - 1) & ~ (pagesize - 1);
+ }
+ base = backtrace_alloc (state, alc, error_callback, data);
+ if (base == NULL)
+ return NULL;
+ if (vec->base != NULL)
+ {
+ memcpy (base, vec->base, vec->size);
+ backtrace_free (state, vec->base, vec->size + vec->alc,
+ error_callback, data);
+ }
+ vec->base = base;
+ vec->alc = alc - vec->size;
+ }
+
+ ret = (char *) vec->base + vec->size;
+ vec->size += size;
+ vec->alc -= size;
+ return ret;
+}
+
+/* Finish the current allocation on VEC. */
+
+void *
+backtrace_vector_finish (
+ struct backtrace_state *state ATTRIBUTE_UNUSED,
+ struct backtrace_vector *vec,
+ backtrace_error_callback error_callback ATTRIBUTE_UNUSED,
+ void *data ATTRIBUTE_UNUSED)
+{
+ void *ret;
+
+ ret = vec->base;
+ vec->base = (char *) vec->base + vec->size;
+ vec->size = 0;
+ return ret;
+}
+
+/* Release any extra space allocated for VEC. */
+
+int
+backtrace_vector_release (struct backtrace_state *state,
+ struct backtrace_vector *vec,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ size_t size;
+ size_t alc;
+ size_t aligned;
+
+ /* Make sure that the block that we free is aligned on an 8-byte
+ boundary. */
+ size = vec->size;
+ alc = vec->alc;
+ aligned = (size + 7) & ~ (size_t) 7;
+ alc -= aligned - size;
+
+ backtrace_free (state, (char *) vec->base + aligned, alc,
+ error_callback, data);
+ vec->alc = 0;
+ if (vec->size == 0)
+ vec->base = NULL;
+ return 1;
+}
diff --git a/contrib/libs/backtrace/mmapio.c b/contrib/libs/backtrace/mmapio.c
new file mode 100644
index 0000000000..7f6fa8d274
--- /dev/null
+++ b/contrib/libs/backtrace/mmapio.c
@@ -0,0 +1,110 @@
+/* mmapio.c -- File views using mmap.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "backtrace.h"
+#include "internal.h"
+
+#ifndef HAVE_DECL_GETPAGESIZE
+extern int getpagesize (void);
+#endif
+
+#ifndef MAP_FAILED
+#define MAP_FAILED ((void *)-1)
+#endif
+
+/* This file implements file views and memory allocation when mmap is
+ available. */
+
+/* Create a view of SIZE bytes from DESCRIPTOR at OFFSET. */
+
+int
+backtrace_get_view (struct backtrace_state *state ATTRIBUTE_UNUSED,
+ int descriptor, off_t offset, uint64_t size,
+ backtrace_error_callback error_callback,
+ void *data, struct backtrace_view *view)
+{
+ size_t pagesize;
+ unsigned int inpage;
+ off_t pageoff;
+ void *map;
+
+ if ((uint64_t) (size_t) size != size)
+ {
+ error_callback (data, "file size too large", 0);
+ return 0;
+ }
+
+ pagesize = getpagesize ();
+ inpage = offset % pagesize;
+ pageoff = offset - inpage;
+
+ size += inpage;
+ size = (size + (pagesize - 1)) & ~ (pagesize - 1);
+
+ map = mmap (NULL, size, PROT_READ, MAP_PRIVATE, descriptor, pageoff);
+ if (map == MAP_FAILED)
+ {
+ error_callback (data, "mmap", errno);
+ return 0;
+ }
+
+ view->data = (char *) map + inpage;
+ view->base = map;
+ view->len = size;
+
+ return 1;
+}
+
+/* Release a view read by backtrace_get_view. */
+
+void
+backtrace_release_view (struct backtrace_state *state ATTRIBUTE_UNUSED,
+ struct backtrace_view *view,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ union {
+ const void *cv;
+ void *v;
+ } const_cast;
+
+ const_cast.cv = view->base;
+ if (munmap (const_cast.v, view->len) < 0)
+ error_callback (data, "munmap", errno);
+}
diff --git a/contrib/libs/backtrace/posix.c b/contrib/libs/backtrace/posix.c
new file mode 100644
index 0000000000..924631d2e6
--- /dev/null
+++ b/contrib/libs/backtrace/posix.c
@@ -0,0 +1,104 @@
+/* posix.c -- POSIX file I/O routines for the backtrace library.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "backtrace.h"
+#include "internal.h"
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+
+#ifndef FD_CLOEXEC
+#define FD_CLOEXEC 1
+#endif
+
+/* Open a file for reading. */
+
+int
+backtrace_open (const char *filename, backtrace_error_callback error_callback,
+ void *data, int *does_not_exist)
+{
+ int descriptor;
+
+ if (does_not_exist != NULL)
+ *does_not_exist = 0;
+
+ descriptor = open (filename, (int) (O_RDONLY | O_BINARY | O_CLOEXEC));
+ if (descriptor < 0)
+ {
+ /* If DOES_NOT_EXIST is not NULL, then don't call ERROR_CALLBACK
+ if the file does not exist. We treat lacking permission to
+ open the file as the file not existing; this case arises when
+ running the libgo syscall package tests as root. */
+ if (does_not_exist != NULL && (errno == ENOENT || errno == EACCES))
+ *does_not_exist = 1;
+ else
+ error_callback (data, filename, errno);
+ return -1;
+ }
+
+#ifdef HAVE_FCNTL
+ /* Set FD_CLOEXEC just in case the kernel does not support
+ O_CLOEXEC. It doesn't matter if this fails for some reason.
+ FIXME: At some point it should be safe to only do this if
+ O_CLOEXEC == 0. */
+ fcntl (descriptor, F_SETFD, FD_CLOEXEC);
+#endif
+
+ return descriptor;
+}
+
+/* Close DESCRIPTOR. */
+
+int
+backtrace_close (int descriptor, backtrace_error_callback error_callback,
+ void *data)
+{
+ if (close (descriptor) < 0)
+ {
+ error_callback (data, "close", errno);
+ return 0;
+ }
+ return 1;
+}
diff --git a/contrib/libs/backtrace/print.c b/contrib/libs/backtrace/print.c
new file mode 100644
index 0000000000..93d0d3abb4
--- /dev/null
+++ b/contrib/libs/backtrace/print.c
@@ -0,0 +1,92 @@
+/* print.c -- Print the current backtrace.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "backtrace.h"
+#include "internal.h"
+
+/* Passed to callbacks. */
+
+struct print_data
+{
+ struct backtrace_state *state;
+ FILE *f;
+};
+
+/* Print one level of a backtrace. */
+
+static int
+print_callback (void *data, uintptr_t pc, const char *filename, int lineno,
+ const char *function)
+{
+ struct print_data *pdata = (struct print_data *) data;
+
+ fprintf (pdata->f, "0x%lx %s\n\t%s:%d\n",
+ (unsigned long) pc,
+ function == NULL ? "???" : function,
+ filename == NULL ? "???" : filename,
+ lineno);
+ return 0;
+}
+
+/* Print errors to stderr. */
+
+static void
+error_callback (void *data, const char *msg, int errnum)
+{
+ struct print_data *pdata = (struct print_data *) data;
+
+ if (pdata->state->filename != NULL)
+ fprintf (stderr, "%s: ", pdata->state->filename);
+ fprintf (stderr, "libbacktrace: %s", msg);
+ if (errnum > 0)
+ fprintf (stderr, ": %s", strerror (errnum));
+ fputc ('\n', stderr);
+}
+
+/* Print a backtrace. */
+
+void __attribute__((noinline))
+backtrace_print (struct backtrace_state *state, int skip, FILE *f)
+{
+ struct print_data data;
+
+ data.state = state;
+ data.f = f;
+ backtrace_full (state, skip + 1, print_callback, error_callback,
+ (void *) &data);
+}
diff --git a/contrib/libs/backtrace/simple.c b/contrib/libs/backtrace/simple.c
new file mode 100644
index 0000000000..785e726e6b
--- /dev/null
+++ b/contrib/libs/backtrace/simple.c
@@ -0,0 +1,108 @@
+/* simple.c -- The backtrace_simple function.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include "unwind.h"
+#include "backtrace.h"
+
+/* The simple_backtrace routine. */
+
+/* Data passed through _Unwind_Backtrace. */
+
+struct backtrace_simple_data
+{
+ /* Number of frames to skip. */
+ int skip;
+ /* Library state. */
+ struct backtrace_state *state;
+ /* Callback routine. */
+ backtrace_simple_callback callback;
+ /* Error callback routine. */
+ backtrace_error_callback error_callback;
+ /* Data to pass to callback routine. */
+ void *data;
+ /* Value to return from backtrace. */
+ int ret;
+};
+
+/* Unwind library callback routine. This is passed to
+ _Unwind_Backtrace. */
+
+static _Unwind_Reason_Code
+simple_unwind (struct _Unwind_Context *context, void *vdata)
+{
+ struct backtrace_simple_data *bdata = (struct backtrace_simple_data *) vdata;
+ uintptr_t pc;
+ int ip_before_insn = 0;
+
+#ifdef HAVE_GETIPINFO
+ pc = _Unwind_GetIPInfo (context, &ip_before_insn);
+#else
+ pc = _Unwind_GetIP (context);
+#endif
+
+ if (bdata->skip > 0)
+ {
+ --bdata->skip;
+ return _URC_NO_REASON;
+ }
+
+ if (!ip_before_insn)
+ --pc;
+
+ bdata->ret = bdata->callback (bdata->data, pc);
+
+ if (bdata->ret != 0)
+ return _URC_END_OF_STACK;
+
+ return _URC_NO_REASON;
+}
+
+/* Get a simple stack backtrace. */
+
+int __attribute__((noinline))
+backtrace_simple (struct backtrace_state *state, int skip,
+ backtrace_simple_callback callback,
+ backtrace_error_callback error_callback, void *data)
+{
+ struct backtrace_simple_data bdata;
+
+ bdata.skip = skip + 1;
+ bdata.state = state;
+ bdata.callback = callback;
+ bdata.error_callback = error_callback;
+ bdata.data = data;
+ bdata.ret = 0;
+ _Unwind_Backtrace (simple_unwind, &bdata);
+ return bdata.ret;
+}
diff --git a/contrib/libs/backtrace/sort.c b/contrib/libs/backtrace/sort.c
new file mode 100644
index 0000000000..a60a980e65
--- /dev/null
+++ b/contrib/libs/backtrace/sort.c
@@ -0,0 +1,108 @@
+/* sort.c -- Sort without allocating memory
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <stddef.h>
+#include <sys/types.h>
+
+#include "backtrace.h"
+#include "internal.h"
+
+/* The GNU glibc version of qsort allocates memory, which we must not
+ do if we are invoked by a signal handler. So provide our own
+ sort. */
+
+static void
+swap (char *a, char *b, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++, a++, b++)
+ {
+ char t;
+
+ t = *a;
+ *a = *b;
+ *b = t;
+ }
+}
+
+void
+backtrace_qsort (void *basearg, size_t count, size_t size,
+ int (*compar) (const void *, const void *))
+{
+ char *base = (char *) basearg;
+ size_t i;
+ size_t mid;
+
+ tail_recurse:
+ if (count < 2)
+ return;
+
+ /* The symbol table and DWARF tables, which is all we use this
+ routine for, tend to be roughly sorted. Pick the middle element
+ in the array as our pivot point, so that we are more likely to
+ cut the array in half for each recursion step. */
+ swap (base, base + (count / 2) * size, size);
+
+ mid = 0;
+ for (i = 1; i < count; i++)
+ {
+ if ((*compar) (base, base + i * size) > 0)
+ {
+ ++mid;
+ if (i != mid)
+ swap (base + mid * size, base + i * size, size);
+ }
+ }
+
+ if (mid > 0)
+ swap (base, base + mid * size, size);
+
+ /* Recurse with the smaller array, loop with the larger one. That
+ ensures that our maximum stack depth is log count. */
+ if (2 * mid < count)
+ {
+ backtrace_qsort (base, mid, size, compar);
+ base += (mid + 1) * size;
+ count -= mid + 1;
+ goto tail_recurse;
+ }
+ else
+ {
+ backtrace_qsort (base + (mid + 1) * size, count - (mid + 1),
+ size, compar);
+ count = mid;
+ goto tail_recurse;
+ }
+}
diff --git a/contrib/libs/backtrace/state.c b/contrib/libs/backtrace/state.c
new file mode 100644
index 0000000000..0f368a2390
--- /dev/null
+++ b/contrib/libs/backtrace/state.c
@@ -0,0 +1,72 @@
+/* state.c -- Create the backtrace state.
+ Copyright (C) 2012-2021 Free Software Foundation, Inc.
+ Written by Ian Lance Taylor, Google.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ (1) Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ (2) Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ (3) The name of the author may not be used to
+ endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE. */
+
+#include "config.h"
+
+#include <string.h>
+#include <sys/types.h>
+
+#include "backtrace.h"
+#include "backtrace-supported.h"
+#include "internal.h"
+
+/* Create the backtrace state. This will then be passed to all the
+ other routines. */
+
+struct backtrace_state *
+backtrace_create_state (const char *filename, int threaded,
+ backtrace_error_callback error_callback,
+ void *data)
+{
+ struct backtrace_state init_state;
+ struct backtrace_state *state;
+
+#ifndef HAVE_SYNC_FUNCTIONS
+ if (threaded)
+ {
+ error_callback (data, "backtrace library does not support threads", 0);
+ return NULL;
+ }
+#endif
+
+ memset (&init_state, 0, sizeof init_state);
+ init_state.filename = filename;
+ init_state.threaded = threaded;
+
+ state = ((struct backtrace_state *)
+ backtrace_alloc (&init_state, sizeof *state, error_callback, data));
+ if (state == NULL)
+ return NULL;
+ *state = init_state;
+
+ return state;
+}
diff --git a/contrib/libs/backtrace/ya.make b/contrib/libs/backtrace/ya.make
new file mode 100644
index 0000000000..bdc150b69b
--- /dev/null
+++ b/contrib/libs/backtrace/ya.make
@@ -0,0 +1,49 @@
+# Generated by devtools/yamaker from nixpkgs 22.05.
+
+LIBRARY()
+
+LICENSE(BSD-3-Clause)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+VERSION(2022-07-08)
+
+ORIGINAL_SOURCE(https://github.com/ianlancetaylor/libbacktrace/archive/8602fda64e78f1f46563220f2ee9f7e70819c51d.tar.gz)
+
+ADDINCL(
+ contrib/libs/backtrace
+)
+
+NO_COMPILER_WARNINGS()
+
+NO_RUNTIME()
+
+CFLAGS(
+ -DHAVE_CONFIG_H
+)
+
+SRCS(
+ atomic.c
+ backtrace.c
+ dwarf.c
+ fileline.c
+ mmap.c
+ mmapio.c
+ posix.c
+ print.c
+ simple.c
+ sort.c
+ state.c
+)
+
+IF (OS_DARWIN)
+ SRCS(
+ macho.c
+ )
+ELSEIF (OS_LINUX OR OS_ANDROID)
+ SRCS(
+ elf.c
+ )
+ENDIF()
+
+END()
diff --git a/contrib/libs/flatbuffers/CONTRIBUTING.md b/contrib/libs/flatbuffers/CONTRIBUTING.md
new file mode 100644
index 0000000000..17428add54
--- /dev/null
+++ b/contrib/libs/flatbuffers/CONTRIBUTING.md
@@ -0,0 +1,42 @@
+Contributing {#contributing}
+============
+
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+# Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+# Code reviews
+All submissions, including submissions by project members, require review. We
+use Github pull requests for this purpose.
+
+Some tips for good pull requests:
+* Use our code
+ [style guide](https://google.github.io/styleguide/cppguide.html).
+ When in doubt, try to stay true to the existing code of the project.
+* Write a descriptive commit message. What problem are you solving and what
+ are the consequences? Where and what did you test? Some good tips:
+ [here](http://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message)
+ and [here](https://www.kernel.org/doc/Documentation/SubmittingPatches).
+* If your PR consists of multiple commits which are successive improvements /
+ fixes to your first commit, consider squashing them into a single commit
+ (`git rebase -i`) such that your PR is a single commit on top of the current
+ HEAD. This make reviewing the code so much easier, and our history more
+ readable.
+
+# The small print
+Contributions made by corporations are covered by a different agreement than
+the one above, the Software Grant and Corporate Contributor License Agreement.
diff --git a/contrib/libs/flatbuffers/LICENSE.txt b/contrib/libs/flatbuffers/LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/contrib/libs/flatbuffers/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/contrib/libs/flatbuffers/flatc/ya.make b/contrib/libs/flatbuffers/flatc/ya.make
new file mode 100644
index 0000000000..7186851a35
--- /dev/null
+++ b/contrib/libs/flatbuffers/flatc/ya.make
@@ -0,0 +1,56 @@
+# Generated by devtools/yamaker.
+
+LIBRARY()
+
+WITHOUT_LICENSE_TEXTS()
+
+LICENSE(Apache-2.0)
+
+ADDINCL(
+ contrib/libs/flatbuffers/grpc
+ contrib/libs/flatbuffers/include
+)
+
+NO_COMPILER_WARNINGS()
+
+NO_UTIL()
+
+CFLAGS(
+ -DFLATBUFFERS_LOCALE_INDEPENDENT=1
+)
+
+SRCDIR(contrib/libs/flatbuffers)
+
+SRCS(
+ grpc/src/compiler/cpp_generator.cc
+ grpc/src/compiler/go_generator.cc
+ grpc/src/compiler/java_generator.cc
+ grpc/src/compiler/python_generator.cc
+ grpc/src/compiler/swift_generator.cc
+ grpc/src/compiler/ts_generator.cc
+ src/code_generators.cpp
+ src/flatc.cpp
+ src/idl_gen_cpp.cpp
+ src/idl_gen_cpp_yandex_maps_iter.cpp
+ src/idl_gen_csharp.cpp
+ src/idl_gen_dart.cpp
+ src/idl_gen_fbs.cpp
+ src/idl_gen_go.cpp
+ src/idl_gen_grpc.cpp
+ src/idl_gen_java.cpp
+ src/idl_gen_json_schema.cpp
+ src/idl_gen_kotlin.cpp
+ src/idl_gen_lobster.cpp
+ src/idl_gen_lua.cpp
+ src/idl_gen_php.cpp
+ src/idl_gen_python.cpp
+ src/idl_gen_rust.cpp
+ src/idl_gen_swift.cpp
+ src/idl_gen_text.cpp
+ src/idl_gen_ts.cpp
+ src/idl_parser.cpp
+ src/reflection.cpp
+ src/util.cpp
+)
+
+END()
diff --git a/contrib/libs/flatbuffers/grpc/README.md b/contrib/libs/flatbuffers/grpc/README.md
new file mode 100644
index 0000000000..685003f92b
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/README.md
@@ -0,0 +1,43 @@
+GRPC implementation and test
+============================
+
+NOTE: files in `src/` are shared with the GRPC project, and maintained there
+(any changes should be submitted to GRPC instead). These files are copied
+from GRPC, and work with both the Protobuf and FlatBuffers code generator.
+
+`tests/` contains a GRPC specific test, you need to have built and installed
+the GRPC libraries for this to compile. This test will build using the
+`FLATBUFFERS_BUILD_GRPCTEST` option to the main FlatBuffers CMake project.
+
+## Building Flatbuffers with gRPC
+
+### Linux
+
+1. Download, build and install gRPC. See [instructions](https://github.com/grpc/grpc/tree/master/src/cpp).
+ * Lets say your gRPC clone is at `/your/path/to/grpc_repo`.
+ * Install gRPC in a custom directory by running `make install prefix=/your/path/to/grpc_repo/install`.
+2. `export GRPC_INSTALL_PATH=/your/path/to/grpc_repo/install`
+3. `export PROTOBUF_DOWNLOAD_PATH=/your/path/to/grpc_repo/third_party/protobuf`
+4. `mkdir build ; cd build`
+5. `cmake -DFLATBUFFERS_BUILD_GRPCTEST=ON -DGRPC_INSTALL_PATH=${GRPC_INSTALL_PATH} -DPROTOBUF_DOWNLOAD_PATH=${PROTOBUF_DOWNLOAD_PATH} ..`
+6. `make`
+
+For Bazel users:
+
+```shell
+$bazel test src/compiler/...
+```
+
+## Running FlatBuffer gRPC tests
+
+### Linux
+
+1. `ln -s ${GRPC_INSTALL_PATH}/lib/libgrpc++_unsecure.so.6 ${GRPC_INSTALL_PATH}/lib/libgrpc++_unsecure.so.1`
+2. `export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${GRPC_INSTALL_PATH}/lib`
+3. `make test ARGS=-V`
+
+For Bazel users:
+
+```shell
+$bazel test tests/...
+``` \ No newline at end of file
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/config.h b/contrib/libs/flatbuffers/grpc/src/compiler/config.h
new file mode 100644
index 0000000000..4adc594377
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/config.h
@@ -0,0 +1,40 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef SRC_COMPILER_CONFIG_H
+#define SRC_COMPILER_CONFIG_H
+
+// This file is here only because schema_interface.h, which is copied from gRPC,
+// includes it. There is nothing for Flatbuffers to configure.
+
+#endif // SRC_COMPILER_CONFIG_H
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.cc
new file mode 100644
index 0000000000..8dd408830c
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.cc
@@ -0,0 +1,1780 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include <map>
+
+#include "src/compiler/cpp_generator.h"
+#include "flatbuffers/util.h"
+
+#include <sstream>
+
+namespace grpc_cpp_generator {
+namespace {
+
+grpc::string message_header_ext() { return "_generated.h"; }
+grpc::string service_header_ext() { return ".grpc.fb.h"; }
+
+template <class T>
+grpc::string as_string(T x) {
+ std::ostringstream out;
+ out << x;
+ return out.str();
+}
+
+inline bool ClientOnlyStreaming(const grpc_generator::Method *method) {
+ return method->ClientStreaming() && !method->ServerStreaming();
+}
+
+inline bool ServerOnlyStreaming(const grpc_generator::Method *method) {
+ return !method->ClientStreaming() && method->ServerStreaming();
+}
+
+grpc::string FilenameIdentifier(const grpc::string &filename) {
+ grpc::string result;
+ for (unsigned i = 0; i < filename.size(); i++) {
+ char c = filename[i];
+ if (isalnum(c)) {
+ result.push_back(c);
+ } else {
+ static char hex[] = "0123456789abcdef";
+ result.push_back('_');
+ result.push_back(hex[(c >> 4) & 0xf]);
+ result.push_back(hex[c & 0xf]);
+ }
+ }
+ return result;
+}
+} // namespace
+
+template <class T, size_t N>
+T *array_end(T (&array)[N]) {
+ return array + N;
+}
+
+void PrintIncludes(grpc_generator::Printer *printer,
+ const std::vector<grpc::string> &headers,
+ const Parameters &params) {
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["l"] = params.use_system_headers ? '<' : '"';
+ vars["r"] = params.use_system_headers ? '>' : '"';
+
+ auto &s = params.grpc_search_path;
+ if (!s.empty()) {
+ vars["l"] += s;
+ if (s[s.size() - 1] != '/') {
+ vars["l"] += '/';
+ }
+ }
+
+ for (auto i = headers.begin(); i != headers.end(); i++) {
+ vars["h"] = *i;
+ printer->Print(vars, "#include $l$$h$$r$\n");
+ }
+}
+
+grpc::string GetHeaderPrologue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["filename"] = file->filename();
+ vars["filename_identifier"] = FilenameIdentifier(file->filename());
+ vars["filename_base"] = file->filename_without_ext();
+ vars["message_header_ext"] = message_header_ext();
+
+ printer->Print(vars, "// Generated by the gRPC C++ plugin.\n");
+ printer->Print(vars,
+ "// If you make any local change, they will be lost.\n");
+ printer->Print(vars, "// source: $filename$\n");
+ grpc::string leading_comments = file->GetLeadingComments("//");
+ if (!leading_comments.empty()) {
+ printer->Print(vars, "// Original file comments:\n");
+ printer->Print(leading_comments.c_str());
+ }
+ printer->Print(vars, "#ifndef GRPC_$filename_identifier$__INCLUDED\n");
+ printer->Print(vars, "#define GRPC_$filename_identifier$__INCLUDED\n");
+ printer->Print(vars, "\n");
+ printer->Print(vars, "#include \"$filename_base$$message_header_ext$\"\n");
+ printer->Print(vars, file->additional_headers().c_str());
+ printer->Print(vars, "\n");
+ }
+ return output;
+}
+
+grpc::string GetHeaderIncludes(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ static const char *headers_strs[] = {
+ "grpcpp/impl/codegen/async_stream.h",
+ "grpcpp/impl/codegen/async_unary_call.h",
+ "grpcpp/impl/codegen/method_handler.h",
+ "grpcpp/impl/codegen/proto_utils.h",
+ "grpcpp/impl/codegen/rpc_method.h",
+ "grpcpp/impl/codegen/service_type.h",
+ "grpcpp/impl/codegen/status.h",
+ "grpcpp/impl/codegen/stub_options.h",
+ "grpcpp/impl/codegen/sync_stream.h"};
+ std::vector<grpc::string> headers(headers_strs, array_end(headers_strs));
+ PrintIncludes(printer.get(), headers, params);
+ printer->Print(vars, "\n");
+ printer->Print(vars, "namespace grpc {\n");
+ printer->Print(vars, "class CompletionQueue;\n");
+ printer->Print(vars, "class Channel;\n");
+ printer->Print(vars, "class ServerCompletionQueue;\n");
+ printer->Print(vars, "class ServerContext;\n");
+ printer->Print(vars, "} // namespace grpc\n\n");
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.begin(); part != parts.end(); part++) {
+ vars["part"] = *part;
+ printer->Print(vars, "namespace $part$ {\n");
+ }
+ printer->Print(vars, "\n");
+ }
+ }
+ return output;
+}
+
+void PrintHeaderClientMethodInterfaces(
+ grpc_generator::Printer *printer, const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars, bool is_public) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+
+ struct {
+ grpc::string prefix;
+ grpc::string method_params; // extra arguments to method
+ grpc::string raw_args; // extra arguments to raw version of method
+ } async_prefixes[] = {{"Async", ", void* tag", ", tag"},
+ {"PrepareAsync", "", ""}};
+
+ if (is_public) {
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "virtual ::grpc::Status $Method$(::grpc::ClientContext* context, "
+ "const $Request$& request, $Response$* response) = 0;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< "
+ "::grpc::ClientAsyncResponseReaderInterface< $Response$>> "
+ "$AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "const $Request$& request, "
+ "::grpc::CompletionQueue* cq) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncResponseReaderInterface< $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, request, cq));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientWriterInterface< $Request$>>"
+ " $Method$("
+ "::grpc::ClientContext* context, $Response$* response) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< ::grpc::ClientWriterInterface< $Request$>>"
+ "($Method$Raw(context, response));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientAsyncWriterInterface< $Request$>>"
+ " $AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "$Response$* "
+ "response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncWriterInterface< $Request$>>("
+ "$AsyncPrefix$$Method$Raw(context, response, "
+ "cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientReaderInterface< $Response$>>"
+ " $Method$(::grpc::ClientContext* context, const $Request$& request)"
+ " {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< ::grpc::ClientReaderInterface< $Response$>>"
+ "($Method$Raw(context, request));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientAsyncReaderInterface< $Response$>> "
+ "$AsyncPrefix$$Method$("
+ "::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncReaderInterface< $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, request, cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(*vars,
+ "std::unique_ptr< ::grpc::ClientReaderWriterInterface< "
+ "$Request$, $Response$>> "
+ "$Method$(::grpc::ClientContext* context) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientReaderWriterInterface< $Request$, $Response$>>("
+ "$Method$Raw(context));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< "
+ "::grpc::ClientAsyncReaderWriterInterface< $Request$, $Response$>> "
+ "$AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncReaderWriterInterface< $Request$, $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ }
+ } else {
+ if (method->NoStreaming()) {
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientAsyncResponseReaderInterface< $Response$>* "
+ "$AsyncPrefix$$Method$Raw(::grpc::ClientContext* context, "
+ "const $Request$& request, "
+ "::grpc::CompletionQueue* cq) = 0;\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientWriterInterface< $Request$>*"
+ " $Method$Raw("
+ "::grpc::ClientContext* context, $Response$* response) = 0;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientAsyncWriterInterface< $Request$>*"
+ " $AsyncPrefix$$Method$Raw(::grpc::ClientContext* context, "
+ "$Response$* response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) = 0;\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientReaderInterface< $Response$>* "
+ "$Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request) = 0;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientAsyncReaderInterface< $Response$>* "
+ "$AsyncPrefix$$Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) = 0;\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(*vars,
+ "virtual ::grpc::ClientReaderWriterInterface< $Request$, "
+ "$Response$>* "
+ "$Method$Raw(::grpc::ClientContext* context) = 0;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ printer->Print(
+ *vars,
+ "virtual ::grpc::ClientAsyncReaderWriterInterface< "
+ "$Request$, $Response$>* "
+ "$AsyncPrefix$$Method$Raw(::grpc::ClientContext* context, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) = 0;\n");
+ }
+ }
+ }
+}
+
+void PrintHeaderClientMethod(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars,
+ bool is_public) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ struct {
+ grpc::string prefix;
+ grpc::string method_params; // extra arguments to method
+ grpc::string raw_args; // extra arguments to raw version of method
+ } async_prefixes[] = {{"Async", ", void* tag", ", tag"},
+ {"PrepareAsync", "", ""}};
+
+ if (is_public) {
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "::grpc::Status $Method$(::grpc::ClientContext* context, "
+ "const $Request$& request, $Response$* response) override;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientAsyncResponseReader< $Response$>> "
+ "$AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "const $Request$& request, "
+ "::grpc::CompletionQueue* cq) {\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncResponseReader< $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, request, cq));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientWriter< $Request$>>"
+ " $Method$("
+ "::grpc::ClientContext* context, $Response$* response) {\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "return std::unique_ptr< ::grpc::ClientWriter< $Request$>>"
+ "($Method$Raw(context, response));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(*vars,
+ "std::unique_ptr< ::grpc::ClientAsyncWriter< $Request$>>"
+ " $AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "$Response$* response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< ::grpc::ClientAsyncWriter< $Request$>>("
+ "$AsyncPrefix$$Method$Raw(context, response, "
+ "cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientReader< $Response$>>"
+ " $Method$(::grpc::ClientContext* context, const $Request$& request)"
+ " {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< ::grpc::ClientReader< $Response$>>"
+ "($Method$Raw(context, request));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientAsyncReader< $Response$>> "
+ "$AsyncPrefix$$Method$("
+ "::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< ::grpc::ClientAsyncReader< $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, request, cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "std::unique_ptr< ::grpc::ClientReaderWriter< $Request$, $Response$>>"
+ " $Method$(::grpc::ClientContext* context) {\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientReaderWriter< $Request$, $Response$>>("
+ "$Method$Raw(context));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(*vars,
+ "std::unique_ptr< ::grpc::ClientAsyncReaderWriter< "
+ "$Request$, $Response$>> "
+ "$AsyncPrefix$$Method$(::grpc::ClientContext* context, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "return std::unique_ptr< "
+ "::grpc::ClientAsyncReaderWriter< $Request$, $Response$>>("
+ "$AsyncPrefix$$Method$Raw(context, cq$AsyncRawArgs$));\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ }
+ }
+ } else {
+ if (method->NoStreaming()) {
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ printer->Print(
+ *vars,
+ "::grpc::ClientAsyncResponseReader< $Response$>* "
+ "$AsyncPrefix$$Method$Raw(::grpc::ClientContext* context, "
+ "const $Request$& request, "
+ "::grpc::CompletionQueue* cq) override;\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "::grpc::ClientWriter< $Request$>* $Method$Raw("
+ "::grpc::ClientContext* context, $Response$* response) "
+ "override;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "::grpc::ClientAsyncWriter< $Request$>* $AsyncPrefix$$Method$Raw("
+ "::grpc::ClientContext* context, $Response$* response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) override;\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "::grpc::ClientReader< $Response$>* $Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request)"
+ " override;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "::grpc::ClientAsyncReader< $Response$>* $AsyncPrefix$$Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) override;\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(*vars,
+ "::grpc::ClientReaderWriter< $Request$, $Response$>* "
+ "$Method$Raw(::grpc::ClientContext* context) override;\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncRawArgs"] = async_prefix.raw_args;
+ printer->Print(
+ *vars,
+ "::grpc::ClientAsyncReaderWriter< $Request$, $Response$>* "
+ "$AsyncPrefix$$Method$Raw(::grpc::ClientContext* context, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) override;\n");
+ }
+ }
+ }
+}
+
+void PrintHeaderClientMethodData(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ printer->Print(*vars,
+ "const ::grpc::internal::RpcMethod rpcmethod_$Method$_;\n");
+}
+
+void PrintHeaderServerMethodSync(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ printer->Print(method->GetLeadingComments("//").c_str());
+ if (method->NoStreaming()) {
+ printer->Print(*vars,
+ "virtual ::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "$Response$* response);\n");
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "virtual ::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReader< $Request$>* reader, "
+ "$Response$* response);\n");
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "virtual ::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "::grpc::ServerWriter< $Response$>* writer);\n");
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "virtual ::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReaderWriter< $Response$, $Request$>* stream);"
+ "\n");
+ }
+ printer->Print(method->GetTrailingComments("//").c_str());
+}
+
+void PrintHeaderServerMethodAsync(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ printer->Print(*vars, "template <class BaseClass>\n");
+ printer->Print(*vars,
+ "class WithAsyncMethod_$Method$ : public BaseClass {\n");
+ printer->Print(
+ " private:\n"
+ " void BaseClassMustBeDerivedFromService(const Service *service) {}\n");
+ printer->Print(" public:\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "WithAsyncMethod_$Method$() {\n"
+ " ::grpc::Service::MarkMethodAsync($Idx$);\n"
+ "}\n");
+ printer->Print(*vars,
+ "~WithAsyncMethod_$Method$() override {\n"
+ " BaseClassMustBeDerivedFromService(this);\n"
+ "}\n");
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "$Response$* response) final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "void Request$Method$("
+ "::grpc::ServerContext* context, $Request$* request, "
+ "::grpc::ServerAsyncResponseWriter< $Response$>* response, "
+ "::grpc::CompletionQueue* new_call_cq, "
+ "::grpc::ServerCompletionQueue* notification_cq, void *tag) {\n");
+ printer->Print(*vars,
+ " ::grpc::Service::RequestAsyncUnary($Idx$, context, "
+ "request, response, new_call_cq, notification_cq, tag);\n");
+ printer->Print("}\n");
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReader< $Request$>* reader, "
+ "$Response$* response) final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "void Request$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerAsyncReader< $Response$, $Request$>* reader, "
+ "::grpc::CompletionQueue* new_call_cq, "
+ "::grpc::ServerCompletionQueue* notification_cq, void *tag) {\n");
+ printer->Print(*vars,
+ " ::grpc::Service::RequestAsyncClientStreaming($Idx$, "
+ "context, reader, new_call_cq, notification_cq, tag);\n");
+ printer->Print("}\n");
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "::grpc::ServerWriter< $Response$>* writer) final override "
+ "{\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "void Request$Method$("
+ "::grpc::ServerContext* context, $Request$* request, "
+ "::grpc::ServerAsyncWriter< $Response$>* writer, "
+ "::grpc::CompletionQueue* new_call_cq, "
+ "::grpc::ServerCompletionQueue* notification_cq, void *tag) {\n");
+ printer->Print(
+ *vars,
+ " ::grpc::Service::RequestAsyncServerStreaming($Idx$, "
+ "context, request, writer, new_call_cq, notification_cq, tag);\n");
+ printer->Print("}\n");
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReaderWriter< $Response$, $Request$>* stream) "
+ "final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "void Request$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerAsyncReaderWriter< $Response$, $Request$>* stream, "
+ "::grpc::CompletionQueue* new_call_cq, "
+ "::grpc::ServerCompletionQueue* notification_cq, void *tag) {\n");
+ printer->Print(*vars,
+ " ::grpc::Service::RequestAsyncBidiStreaming($Idx$, "
+ "context, stream, new_call_cq, notification_cq, tag);\n");
+ printer->Print("}\n");
+ }
+ printer->Outdent();
+ printer->Print(*vars, "};\n");
+}
+
+void PrintHeaderServerMethodStreamedUnary(
+ grpc_generator::Printer *printer, const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ if (method->NoStreaming()) {
+ printer->Print(*vars, "template <class BaseClass>\n");
+ printer->Print(*vars,
+ "class WithStreamedUnaryMethod_$Method$ : "
+ "public BaseClass {\n");
+ printer->Print(
+ " private:\n"
+ " void BaseClassMustBeDerivedFromService(const Service *service) "
+ "{}\n");
+ printer->Print(" public:\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "WithStreamedUnaryMethod_$Method$() {\n"
+ " ::grpc::Service::MarkMethodStreamed($Idx$,\n"
+ " new ::grpc::internal::StreamedUnaryHandler< $Request$, "
+ "$Response$>(std::bind"
+ "(&WithStreamedUnaryMethod_$Method$<BaseClass>::"
+ "Streamed$Method$, this, std::placeholders::_1, "
+ "std::placeholders::_2)));\n"
+ "}\n");
+ printer->Print(*vars,
+ "~WithStreamedUnaryMethod_$Method$() override {\n"
+ " BaseClassMustBeDerivedFromService(this);\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "// disable regular version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "$Response$* response) final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(*vars,
+ "// replace default version of method with streamed unary\n"
+ "virtual ::grpc::Status Streamed$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerUnaryStreamer< "
+ "$Request$,$Response$>* server_unary_streamer)"
+ " = 0;\n");
+ printer->Outdent();
+ printer->Print(*vars, "};\n");
+ }
+}
+
+void PrintHeaderServerMethodSplitStreaming(
+ grpc_generator::Printer *printer, const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ if (ServerOnlyStreaming(method)) {
+ printer->Print(*vars, "template <class BaseClass>\n");
+ printer->Print(*vars,
+ "class WithSplitStreamingMethod_$Method$ : "
+ "public BaseClass {\n");
+ printer->Print(
+ " private:\n"
+ " void BaseClassMustBeDerivedFromService(const Service *service) "
+ "{}\n");
+ printer->Print(" public:\n");
+ printer->Indent();
+ printer->Print(
+ *vars,
+ "WithSplitStreamingMethod_$Method$() {\n"
+ " ::grpc::Service::MarkMethodStreamed($Idx$,\n"
+ " new ::grpc::internal::SplitServerStreamingHandler< $Request$, "
+ "$Response$>(std::bind"
+ "(&WithSplitStreamingMethod_$Method$<BaseClass>::"
+ "Streamed$Method$, this, std::placeholders::_1, "
+ "std::placeholders::_2)));\n"
+ "}\n");
+ printer->Print(*vars,
+ "~WithSplitStreamingMethod_$Method$() override {\n"
+ " BaseClassMustBeDerivedFromService(this);\n"
+ "}\n");
+ printer->Print(
+ *vars,
+ "// disable regular version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "::grpc::ServerWriter< $Response$>* writer) final override "
+ "{\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ printer->Print(*vars,
+ "// replace default version of method with split streamed\n"
+ "virtual ::grpc::Status Streamed$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerSplitStreamer< "
+ "$Request$,$Response$>* server_split_streamer)"
+ " = 0;\n");
+ printer->Outdent();
+ printer->Print(*vars, "};\n");
+ }
+}
+
+void PrintHeaderServerMethodGeneric(
+ grpc_generator::Printer *printer, const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ printer->Print(*vars, "template <class BaseClass>\n");
+ printer->Print(*vars,
+ "class WithGenericMethod_$Method$ : public BaseClass {\n");
+ printer->Print(
+ " private:\n"
+ " void BaseClassMustBeDerivedFromService(const Service *service) {}\n");
+ printer->Print(" public:\n");
+ printer->Indent();
+ printer->Print(*vars,
+ "WithGenericMethod_$Method$() {\n"
+ " ::grpc::Service::MarkMethodGeneric($Idx$);\n"
+ "}\n");
+ printer->Print(*vars,
+ "~WithGenericMethod_$Method$() override {\n"
+ " BaseClassMustBeDerivedFromService(this);\n"
+ "}\n");
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "$Response$* response) final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReader< $Request$>* reader, "
+ "$Response$* response) final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, const $Request$* request, "
+ "::grpc::ServerWriter< $Response$>* writer) final override "
+ "{\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "// disable synchronous version of this method\n"
+ "::grpc::Status $Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReaderWriter< $Response$, $Request$>* stream) "
+ "final override {\n"
+ " abort();\n"
+ " return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, \"\");\n"
+ "}\n");
+ }
+ printer->Outdent();
+ printer->Print(*vars, "};\n");
+}
+
+void PrintHeaderService(grpc_generator::Printer *printer,
+ const grpc_generator::Service *service,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Service"] = service->name();
+
+ printer->Print(service->GetLeadingComments("//").c_str());
+ printer->Print(*vars,
+ "class $Service$ final {\n"
+ " public:\n");
+ printer->Indent();
+
+ // Service metadata
+ printer->Print(*vars,
+ "static constexpr char const* service_full_name() {\n"
+ " return \"$Package$$Service$\";\n"
+ "}\n");
+
+ // Client side
+ printer->Print(
+ "class StubInterface {\n"
+ " public:\n");
+ printer->Indent();
+ printer->Print("virtual ~StubInterface() {}\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ printer->Print(service->method(i)->GetLeadingComments("//").c_str());
+ PrintHeaderClientMethodInterfaces(printer, service->method(i).get(), vars,
+ true);
+ printer->Print(service->method(i)->GetTrailingComments("//").c_str());
+ }
+ printer->Outdent();
+ printer->Print("private:\n");
+ printer->Indent();
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintHeaderClientMethodInterfaces(printer, service->method(i).get(), vars,
+ false);
+ }
+ printer->Outdent();
+ printer->Print("};\n");
+ printer->Print(
+ "class Stub final : public StubInterface"
+ " {\n public:\n");
+ printer->Indent();
+ printer->Print(
+ "Stub(const std::shared_ptr< ::grpc::ChannelInterface>& "
+ "channel);\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintHeaderClientMethod(printer, service->method(i).get(), vars, true);
+ }
+ printer->Outdent();
+ printer->Print("\n private:\n");
+ printer->Indent();
+ printer->Print("std::shared_ptr< ::grpc::ChannelInterface> channel_;\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintHeaderClientMethod(printer, service->method(i).get(), vars, false);
+ }
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintHeaderClientMethodData(printer, service->method(i).get(), vars);
+ }
+ printer->Outdent();
+ printer->Print("};\n");
+ printer->Print(
+ "static std::unique_ptr<Stub> NewStub(const std::shared_ptr< "
+ "::grpc::ChannelInterface>& channel, "
+ "const ::grpc::StubOptions& options = ::grpc::StubOptions());\n");
+
+ printer->Print("\n");
+
+ // Server side - base
+ printer->Print(
+ "class Service : public ::grpc::Service {\n"
+ " public:\n");
+ printer->Indent();
+ printer->Print("Service();\n");
+ printer->Print("virtual ~Service();\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintHeaderServerMethodSync(printer, service->method(i).get(), vars);
+ }
+ printer->Outdent();
+ printer->Print("};\n");
+
+ // Server side - Asynchronous
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintHeaderServerMethodAsync(printer, service->method(i).get(), vars);
+ }
+
+ printer->Print("typedef ");
+
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["method_name"] = service->method(i).get()->name();
+ printer->Print(*vars, "WithAsyncMethod_$method_name$<");
+ }
+ printer->Print("Service");
+ for (int i = 0; i < service->method_count(); ++i) {
+ printer->Print(" >");
+ }
+ printer->Print(" AsyncService;\n");
+
+ // Server side - Generic
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintHeaderServerMethodGeneric(printer, service->method(i).get(), vars);
+ }
+
+ // Server side - Streamed Unary
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintHeaderServerMethodStreamedUnary(printer, service->method(i).get(),
+ vars);
+ }
+
+ printer->Print("typedef ");
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["method_name"] = service->method(i).get()->name();
+ if (service->method(i)->NoStreaming()) {
+ printer->Print(*vars, "WithStreamedUnaryMethod_$method_name$<");
+ }
+ }
+ printer->Print("Service");
+ for (int i = 0; i < service->method_count(); ++i) {
+ if (service->method(i)->NoStreaming()) {
+ printer->Print(" >");
+ }
+ }
+ printer->Print(" StreamedUnaryService;\n");
+
+ // Server side - controlled server-side streaming
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintHeaderServerMethodSplitStreaming(printer, service->method(i).get(),
+ vars);
+ }
+
+ printer->Print("typedef ");
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["method_name"] = service->method(i).get()->name();
+ auto method = service->method(i);
+ if (ServerOnlyStreaming(method.get())) {
+ printer->Print(*vars, "WithSplitStreamingMethod_$method_name$<");
+ }
+ }
+ printer->Print("Service");
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ if (ServerOnlyStreaming(method.get())) {
+ printer->Print(" >");
+ }
+ }
+ printer->Print(" SplitStreamedService;\n");
+
+ // Server side - typedef for controlled both unary and server-side streaming
+ printer->Print("typedef ");
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["method_name"] = service->method(i).get()->name();
+ auto method = service->method(i);
+ if (ServerOnlyStreaming(method.get())) {
+ printer->Print(*vars, "WithSplitStreamingMethod_$method_name$<");
+ }
+ if (service->method(i)->NoStreaming()) {
+ printer->Print(*vars, "WithStreamedUnaryMethod_$method_name$<");
+ }
+ }
+ printer->Print("Service");
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ if (service->method(i)->NoStreaming() ||
+ ServerOnlyStreaming(method.get())) {
+ printer->Print(" >");
+ }
+ }
+ printer->Print(" StreamedService;\n");
+
+ printer->Outdent();
+ printer->Print("};\n");
+ printer->Print(service->GetTrailingComments("//").c_str());
+}
+
+grpc::string GetHeaderServices(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+ // Package string is empty or ends with a dot. It is used to fully qualify
+ // method names.
+ vars["Package"] = file->package();
+ if (!file->package().empty()) {
+ vars["Package"].append(".");
+ }
+
+ if (!params.services_namespace.empty()) {
+ vars["services_namespace"] = params.services_namespace;
+ printer->Print(vars, "\nnamespace $services_namespace$ {\n\n");
+ }
+
+ for (int i = 0; i < file->service_count(); ++i) {
+ PrintHeaderService(printer.get(), file->service(i).get(), &vars);
+ printer->Print("\n");
+ }
+
+ if (!params.services_namespace.empty()) {
+ printer->Print(vars, "} // namespace $services_namespace$\n\n");
+ }
+ }
+ return output;
+}
+
+grpc::string GetHeaderEpilogue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["filename"] = file->filename();
+ vars["filename_identifier"] = FilenameIdentifier(file->filename());
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.rbegin(); part != parts.rend(); part++) {
+ vars["part"] = *part;
+ printer->Print(vars, "} // namespace $part$\n");
+ }
+ printer->Print(vars, "\n");
+ }
+
+ printer->Print(vars, "\n");
+ printer->Print(vars, "#endif // GRPC_$filename_identifier$__INCLUDED\n");
+
+ printer->Print(file->GetTrailingComments("//").c_str());
+ }
+ return output;
+}
+
+grpc::string GetSourcePrologue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["filename"] = file->filename();
+ vars["filename_base"] = file->filename_without_ext();
+ vars["message_header_ext"] = message_header_ext();
+ vars["service_header_ext"] = service_header_ext();
+
+ printer->Print(vars, "// Generated by the gRPC C++ plugin.\n");
+ printer->Print(vars,
+ "// If you make any local change, they will be lost.\n");
+ printer->Print(vars, "// source: $filename$\n\n");
+
+ printer->Print(vars, "#include \"$filename_base$$message_header_ext$\"\n");
+ printer->Print(vars, "#include \"$filename_base$$service_header_ext$\"\n");
+ printer->Print(vars, "\n");
+ }
+ return output;
+}
+
+grpc::string GetSourceIncludes(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ static const char *headers_strs[] = {
+ "grpcpp/impl/codegen/async_stream.h",
+ "grpcpp/impl/codegen/async_unary_call.h",
+ "grpcpp/impl/codegen/channel_interface.h",
+ "grpcpp/impl/codegen/client_unary_call.h",
+ "grpcpp/impl/codegen/method_handler.h",
+ "grpcpp/impl/codegen/rpc_service_method.h",
+ "grpcpp/impl/codegen/service_type.h",
+ "grpcpp/impl/codegen/sync_stream.h"};
+ std::vector<grpc::string> headers(headers_strs, array_end(headers_strs));
+ PrintIncludes(printer.get(), headers, params);
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.begin(); part != parts.end(); part++) {
+ vars["part"] = *part;
+ printer->Print(vars, "namespace $part$ {\n");
+ }
+ }
+
+ printer->Print(vars, "\n");
+ }
+ return output;
+}
+
+void PrintSourceClientMethod(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ struct {
+ grpc::string prefix;
+ grpc::string start; // bool literal expressed as string
+ grpc::string method_params; // extra arguments to method
+ grpc::string create_args; // extra arguments to creator
+ } async_prefixes[] = {{"Async", "true", ", void* tag", ", tag"},
+ {"PrepareAsync", "false", "", ", nullptr"}};
+ if (method->NoStreaming()) {
+ printer->Print(*vars,
+ "::grpc::Status $ns$$Service$::Stub::$Method$("
+ "::grpc::ClientContext* context, "
+ "const $Request$& request, $Response$* response) {\n");
+ printer->Print(*vars,
+ " return ::grpc::internal::BlockingUnaryCall"
+ "(channel_.get(), rpcmethod_$Method$_, "
+ "context, request, response);\n}\n\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncStart"] = async_prefix.start;
+ printer->Print(*vars,
+ "::grpc::ClientAsyncResponseReader< $Response$>* "
+ "$ns$$Service$::Stub::$AsyncPrefix$$Method$Raw(::grpc::"
+ "ClientContext* context, "
+ "const $Request$& request, "
+ "::grpc::CompletionQueue* cq) {\n");
+ printer->Print(
+ *vars,
+ " return "
+ "::grpc::internal::ClientAsyncResponseReaderFactory< $Response$>"
+ "::Create(channel_.get(), cq, "
+ "rpcmethod_$Method$_, "
+ "context, request, $AsyncStart$);\n"
+ "}\n\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "::grpc::ClientWriter< $Request$>* "
+ "$ns$$Service$::Stub::$Method$Raw("
+ "::grpc::ClientContext* context, $Response$* response) {\n");
+ printer->Print(
+ *vars,
+ " return ::grpc::internal::ClientWriterFactory< $Request$>::Create("
+ "channel_.get(), "
+ "rpcmethod_$Method$_, "
+ "context, response);\n"
+ "}\n\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncStart"] = async_prefix.start;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncCreateArgs"] = async_prefix.create_args;
+ printer->Print(*vars,
+ "::grpc::ClientAsyncWriter< $Request$>* "
+ "$ns$$Service$::Stub::$AsyncPrefix$$Method$Raw("
+ "::grpc::ClientContext* context, $Response$* response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Print(
+ *vars,
+ " return ::grpc::internal::ClientAsyncWriterFactory< $Request$>"
+ "::Create(channel_.get(), cq, "
+ "rpcmethod_$Method$_, "
+ "context, response, $AsyncStart$$AsyncCreateArgs$);\n"
+ "}\n\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "::grpc::ClientReader< $Response$>* "
+ "$ns$$Service$::Stub::$Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request) {\n");
+ printer->Print(
+ *vars,
+ " return ::grpc::internal::ClientReaderFactory< $Response$>::Create("
+ "channel_.get(), "
+ "rpcmethod_$Method$_, "
+ "context, request);\n"
+ "}\n\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncStart"] = async_prefix.start;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncCreateArgs"] = async_prefix.create_args;
+ printer->Print(
+ *vars,
+ "::grpc::ClientAsyncReader< $Response$>* "
+ "$ns$$Service$::Stub::$AsyncPrefix$$Method$Raw("
+ "::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Print(
+ *vars,
+ " return ::grpc::internal::ClientAsyncReaderFactory< $Response$>"
+ "::Create(channel_.get(), cq, "
+ "rpcmethod_$Method$_, "
+ "context, request, $AsyncStart$$AsyncCreateArgs$);\n"
+ "}\n\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "::grpc::ClientReaderWriter< $Request$, $Response$>* "
+ "$ns$$Service$::Stub::$Method$Raw(::grpc::ClientContext* context) {\n");
+ printer->Print(*vars,
+ " return ::grpc::internal::ClientReaderWriterFactory< "
+ "$Request$, $Response$>::Create("
+ "channel_.get(), "
+ "rpcmethod_$Method$_, "
+ "context);\n"
+ "}\n\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncStart"] = async_prefix.start;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["AsyncCreateArgs"] = async_prefix.create_args;
+ printer->Print(*vars,
+ "::grpc::ClientAsyncReaderWriter< $Request$, $Response$>* "
+ "$ns$$Service$::Stub::$AsyncPrefix$$Method$Raw(::grpc::"
+ "ClientContext* context, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$) {\n");
+ printer->Print(*vars,
+ " return "
+ "::grpc::internal::ClientAsyncReaderWriterFactory< "
+ "$Request$, $Response$>::Create("
+ "channel_.get(), cq, "
+ "rpcmethod_$Method$_, "
+ "context, $AsyncStart$$AsyncCreateArgs$);\n"
+ "}\n\n");
+ }
+ }
+}
+
+void PrintSourceServerMethod(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ if (method->NoStreaming()) {
+ printer->Print(*vars,
+ "::grpc::Status $ns$$Service$::Service::$Method$("
+ "::grpc::ServerContext* context, "
+ "const $Request$* request, $Response$* response) {\n");
+ printer->Print(" (void) context;\n");
+ printer->Print(" (void) request;\n");
+ printer->Print(" (void) response;\n");
+ printer->Print(
+ " return ::grpc::Status("
+ "::grpc::StatusCode::UNIMPLEMENTED, \"\");\n");
+ printer->Print("}\n\n");
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "::grpc::Status $ns$$Service$::Service::$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReader< $Request$>* reader, "
+ "$Response$* response) {\n");
+ printer->Print(" (void) context;\n");
+ printer->Print(" (void) reader;\n");
+ printer->Print(" (void) response;\n");
+ printer->Print(
+ " return ::grpc::Status("
+ "::grpc::StatusCode::UNIMPLEMENTED, \"\");\n");
+ printer->Print("}\n\n");
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(*vars,
+ "::grpc::Status $ns$$Service$::Service::$Method$("
+ "::grpc::ServerContext* context, "
+ "const $Request$* request, "
+ "::grpc::ServerWriter< $Response$>* writer) {\n");
+ printer->Print(" (void) context;\n");
+ printer->Print(" (void) request;\n");
+ printer->Print(" (void) writer;\n");
+ printer->Print(
+ " return ::grpc::Status("
+ "::grpc::StatusCode::UNIMPLEMENTED, \"\");\n");
+ printer->Print("}\n\n");
+ } else if (method->BidiStreaming()) {
+ printer->Print(*vars,
+ "::grpc::Status $ns$$Service$::Service::$Method$("
+ "::grpc::ServerContext* context, "
+ "::grpc::ServerReaderWriter< $Response$, $Request$>* "
+ "stream) {\n");
+ printer->Print(" (void) context;\n");
+ printer->Print(" (void) stream;\n");
+ printer->Print(
+ " return ::grpc::Status("
+ "::grpc::StatusCode::UNIMPLEMENTED, \"\");\n");
+ printer->Print("}\n\n");
+ }
+}
+
+void PrintSourceService(grpc_generator::Printer *printer,
+ const grpc_generator::Service *service,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Service"] = service->name();
+
+ if (service->method_count() > 0) {
+ printer->Print(*vars,
+ "static const char* $prefix$$Service$_method_names[] = {\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Method"] = service->method(i).get()->name();
+ printer->Print(*vars, " \"/$Package$$Service$/$Method$\",\n");
+ }
+ printer->Print(*vars, "};\n\n");
+ }
+
+ printer->Print(*vars,
+ "std::unique_ptr< $ns$$Service$::Stub> $ns$$Service$::NewStub("
+ "const std::shared_ptr< ::grpc::ChannelInterface>& channel, "
+ "const ::grpc::StubOptions& options) {\n"
+ " std::unique_ptr< $ns$$Service$::Stub> stub(new "
+ "$ns$$Service$::Stub(channel));\n"
+ " return stub;\n"
+ "}\n\n");
+ printer->Print(*vars,
+ "$ns$$Service$::Stub::Stub(const std::shared_ptr< "
+ "::grpc::ChannelInterface>& channel)\n");
+ printer->Indent();
+ printer->Print(": channel_(channel)");
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ (*vars)["Method"] = method->name();
+ (*vars)["Idx"] = as_string(i);
+ if (method->NoStreaming()) {
+ (*vars)["StreamingType"] = "NORMAL_RPC";
+ // NOTE: There is no reason to consider streamed-unary as a separate
+ // category here since this part is setting up the client-side stub
+ // and this appears as a NORMAL_RPC from the client-side.
+ } else if (ClientOnlyStreaming(method.get())) {
+ (*vars)["StreamingType"] = "CLIENT_STREAMING";
+ } else if (ServerOnlyStreaming(method.get())) {
+ (*vars)["StreamingType"] = "SERVER_STREAMING";
+ } else {
+ (*vars)["StreamingType"] = "BIDI_STREAMING";
+ }
+ printer->Print(*vars,
+ ", rpcmethod_$Method$_("
+ "$prefix$$Service$_method_names[$Idx$], "
+ "::grpc::internal::RpcMethod::$StreamingType$, "
+ "channel"
+ ")\n");
+ }
+ printer->Print("{}\n\n");
+ printer->Outdent();
+
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintSourceClientMethod(printer, service->method(i).get(), vars);
+ }
+
+ printer->Print(*vars, "$ns$$Service$::Service::Service() {\n");
+ printer->Indent();
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ (*vars)["Idx"] = as_string(i);
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "AddMethod(new ::grpc::internal::RpcServiceMethod(\n"
+ " $prefix$$Service$_method_names[$Idx$],\n"
+ " ::grpc::internal::RpcMethod::NORMAL_RPC,\n"
+ " new ::grpc::internal::RpcMethodHandler< $ns$$Service$::Service, "
+ "$Request$, "
+ "$Response$>(\n"
+ " std::mem_fn(&$ns$$Service$::Service::$Method$), this)));\n");
+ } else if (ClientOnlyStreaming(method.get())) {
+ printer->Print(
+ *vars,
+ "AddMethod(new ::grpc::internal::RpcServiceMethod(\n"
+ " $prefix$$Service$_method_names[$Idx$],\n"
+ " ::grpc::internal::RpcMethod::CLIENT_STREAMING,\n"
+ " new ::grpc::internal::ClientStreamingHandler< "
+ "$ns$$Service$::Service, $Request$, $Response$>(\n"
+ " std::mem_fn(&$ns$$Service$::Service::$Method$), this)));\n");
+ } else if (ServerOnlyStreaming(method.get())) {
+ printer->Print(
+ *vars,
+ "AddMethod(new ::grpc::internal::RpcServiceMethod(\n"
+ " $prefix$$Service$_method_names[$Idx$],\n"
+ " ::grpc::internal::RpcMethod::SERVER_STREAMING,\n"
+ " new ::grpc::internal::ServerStreamingHandler< "
+ "$ns$$Service$::Service, $Request$, $Response$>(\n"
+ " std::mem_fn(&$ns$$Service$::Service::$Method$), this)));\n");
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "AddMethod(new ::grpc::internal::RpcServiceMethod(\n"
+ " $prefix$$Service$_method_names[$Idx$],\n"
+ " ::grpc::internal::RpcMethod::BIDI_STREAMING,\n"
+ " new ::grpc::internal::BidiStreamingHandler< "
+ "$ns$$Service$::Service, $Request$, $Response$>(\n"
+ " std::mem_fn(&$ns$$Service$::Service::$Method$), this)));\n");
+ }
+ }
+ printer->Outdent();
+ printer->Print(*vars, "}\n\n");
+ printer->Print(*vars,
+ "$ns$$Service$::Service::~Service() {\n"
+ "}\n\n");
+ for (int i = 0; i < service->method_count(); ++i) {
+ (*vars)["Idx"] = as_string(i);
+ PrintSourceServerMethod(printer, service->method(i).get(), vars);
+ }
+}
+
+grpc::string GetSourceServices(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+ // Package string is empty or ends with a dot. It is used to fully qualify
+ // method names.
+ vars["Package"] = file->package();
+ if (!file->package().empty()) {
+ vars["Package"].append(".");
+ }
+ if (!params.services_namespace.empty()) {
+ vars["ns"] = params.services_namespace + "::";
+ vars["prefix"] = params.services_namespace;
+ } else {
+ vars["ns"] = "";
+ vars["prefix"] = "";
+ }
+
+ for (int i = 0; i < file->service_count(); ++i) {
+ PrintSourceService(printer.get(), file->service(i).get(), &vars);
+ printer->Print("\n");
+ }
+ }
+ return output;
+}
+
+grpc::string GetSourceEpilogue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string temp;
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.begin(); part != parts.end(); part++) {
+ temp.append("} // namespace ");
+ temp.append(*part);
+ temp.append("\n");
+ }
+ temp.append("\n");
+ }
+
+ return temp;
+}
+
+// TODO(mmukhi): Make sure we need parameters or not.
+grpc::string GetMockPrologue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["filename"] = file->filename();
+ vars["filename_base"] = file->filename_without_ext();
+ vars["message_header_ext"] = message_header_ext();
+ vars["service_header_ext"] = service_header_ext();
+
+ printer->Print(vars, "// Generated by the gRPC C++ plugin.\n");
+ printer->Print(vars,
+ "// If you make any local change, they will be lost.\n");
+ printer->Print(vars, "// source: $filename$\n\n");
+
+ printer->Print(vars, "#include \"$filename_base$$message_header_ext$\"\n");
+ printer->Print(vars, "#include \"$filename_base$$service_header_ext$\"\n");
+ printer->Print(vars, file->additional_headers().c_str());
+ printer->Print(vars, "\n");
+ }
+ return output;
+}
+
+// TODO(mmukhi): Add client-stream and completion-queue headers.
+grpc::string GetMockIncludes(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+
+ static const char *headers_strs[] = {
+ "grpcpp/impl/codegen/async_stream.h",
+ "grpcpp/impl/codegen/sync_stream.h",
+ "gmock/gmock.h",
+ };
+ std::vector<grpc::string> headers(headers_strs, array_end(headers_strs));
+ PrintIncludes(printer.get(), headers, params);
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.begin(); part != parts.end(); part++) {
+ vars["part"] = *part;
+ printer->Print(vars, "namespace $part$ {\n");
+ }
+ }
+
+ printer->Print(vars, "\n");
+ }
+ return output;
+}
+
+void PrintMockClientMethods(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Method"] = method->name();
+ (*vars)["Request"] = method->input_type_name();
+ (*vars)["Response"] = method->output_type_name();
+
+ struct {
+ grpc::string prefix;
+ grpc::string method_params; // extra arguments to method
+ int extra_method_param_count;
+ } async_prefixes[] = {{"Async", ", void* tag", 1}, {"PrepareAsync", "", 0}};
+
+ if (method->NoStreaming()) {
+ printer->Print(
+ *vars,
+ "MOCK_METHOD3($Method$, ::grpc::Status(::grpc::ClientContext* context, "
+ "const $Request$& request, $Response$* response));\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ printer->Print(
+ *vars,
+ "MOCK_METHOD3($AsyncPrefix$$Method$Raw, "
+ "::grpc::ClientAsyncResponseReaderInterface< $Response$>*"
+ "(::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq));\n");
+ }
+ } else if (ClientOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "MOCK_METHOD2($Method$Raw, "
+ "::grpc::ClientWriterInterface< $Request$>*"
+ "(::grpc::ClientContext* context, $Response$* response));\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["MockArgs"] =
+ flatbuffers::NumToString(3 + async_prefix.extra_method_param_count);
+ printer->Print(*vars,
+ "MOCK_METHOD$MockArgs$($AsyncPrefix$$Method$Raw, "
+ "::grpc::ClientAsyncWriterInterface< $Request$>*"
+ "(::grpc::ClientContext* context, $Response$* response, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$));\n");
+ }
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(
+ *vars,
+ "MOCK_METHOD2($Method$Raw, "
+ "::grpc::ClientReaderInterface< $Response$>*"
+ "(::grpc::ClientContext* context, const $Request$& request));\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["MockArgs"] =
+ flatbuffers::NumToString(3 + async_prefix.extra_method_param_count);
+ printer->Print(
+ *vars,
+ "MOCK_METHOD$MockArgs$($AsyncPrefix$$Method$Raw, "
+ "::grpc::ClientAsyncReaderInterface< $Response$>*"
+ "(::grpc::ClientContext* context, const $Request$& request, "
+ "::grpc::CompletionQueue* cq$AsyncMethodParams$));\n");
+ }
+ } else if (method->BidiStreaming()) {
+ printer->Print(
+ *vars,
+ "MOCK_METHOD1($Method$Raw, "
+ "::grpc::ClientReaderWriterInterface< $Request$, $Response$>*"
+ "(::grpc::ClientContext* context));\n");
+ for (size_t i = 0; i < sizeof(async_prefixes)/sizeof(async_prefixes[0]); i ++) {
+ auto& async_prefix = async_prefixes[i];
+ (*vars)["AsyncPrefix"] = async_prefix.prefix;
+ (*vars)["AsyncMethodParams"] = async_prefix.method_params;
+ (*vars)["MockArgs"] =
+ flatbuffers::NumToString(2 + async_prefix.extra_method_param_count);
+ printer->Print(
+ *vars,
+ "MOCK_METHOD$MockArgs$($AsyncPrefix$$Method$Raw, "
+ "::grpc::ClientAsyncReaderWriterInterface<$Request$, $Response$>*"
+ "(::grpc::ClientContext* context, ::grpc::CompletionQueue* cq"
+ "$AsyncMethodParams$));\n");
+ }
+ }
+}
+
+void PrintMockService(grpc_generator::Printer *printer,
+ const grpc_generator::Service *service,
+ std::map<grpc::string, grpc::string> *vars) {
+ (*vars)["Service"] = service->name();
+
+ printer->Print(*vars,
+ "class Mock$Service$Stub : public $Service$::StubInterface {\n"
+ " public:\n");
+ printer->Indent();
+ for (int i = 0; i < service->method_count(); ++i) {
+ PrintMockClientMethods(printer, service->method(i).get(), vars);
+ }
+ printer->Outdent();
+ printer->Print("};\n");
+}
+
+grpc::string GetMockServices(grpc_generator::File *file,
+ const Parameters &params) {
+ grpc::string output;
+ {
+ // Scope the output stream so it closes and finalizes output to the string.
+ auto printer = file->CreatePrinter(&output);
+ std::map<grpc::string, grpc::string> vars;
+ // Package string is empty or ends with a dot. It is used to fully qualify
+ // method names.
+ vars["Package"] = file->package();
+ if (!file->package().empty()) {
+ vars["Package"].append(".");
+ }
+
+ if (!params.services_namespace.empty()) {
+ vars["services_namespace"] = params.services_namespace;
+ printer->Print(vars, "\nnamespace $services_namespace$ {\n\n");
+ }
+
+ for (int i = 0; i < file->service_count(); i++) {
+ PrintMockService(printer.get(), file->service(i).get(), &vars);
+ printer->Print("\n");
+ }
+
+ if (!params.services_namespace.empty()) {
+ printer->Print(vars, "} // namespace $services_namespace$\n\n");
+ }
+ }
+ return output;
+}
+
+grpc::string GetMockEpilogue(grpc_generator::File *file,
+ const Parameters & /*params*/) {
+ grpc::string temp;
+
+ if (!file->package().empty()) {
+ std::vector<grpc::string> parts = file->package_parts();
+
+ for (auto part = parts.begin(); part != parts.end(); part++) {
+ temp.append("} // namespace ");
+ temp.append(*part);
+ temp.append("\n");
+ }
+ temp.append("\n");
+ }
+
+ return temp;
+}
+
+} // namespace grpc_cpp_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.h
new file mode 100644
index 0000000000..6119ebe289
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/cpp_generator.h
@@ -0,0 +1,138 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef GRPC_INTERNAL_COMPILER_CPP_GENERATOR_H
+#define GRPC_INTERNAL_COMPILER_CPP_GENERATOR_H
+
+// cpp_generator.h/.cc do not directly depend on GRPC/ProtoBuf, such that they
+// can be used to generate code for other serialization systems, such as
+// FlatBuffers.
+
+#include <memory>
+#include <vector>
+
+#include "src/compiler/config.h"
+#include "src/compiler/schema_interface.h"
+
+#ifndef GRPC_CUSTOM_STRING
+#include <string>
+#define GRPC_CUSTOM_STRING std::string
+#endif
+
+namespace grpc {
+
+typedef GRPC_CUSTOM_STRING string;
+
+} // namespace grpc
+
+namespace grpc_cpp_generator {
+
+// Contains all the parameters that are parsed from the command line.
+struct Parameters {
+ // Puts the service into a namespace
+ grpc::string services_namespace;
+ // Use system includes (<>) or local includes ("")
+ bool use_system_headers;
+ // Prefix to any grpc include
+ grpc::string grpc_search_path;
+ // Generate GMOCK code to facilitate unit testing.
+ bool generate_mock_code;
+};
+
+// Return the prologue of the generated header file.
+grpc::string GetHeaderPrologue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the includes needed for generated header file.
+grpc::string GetHeaderIncludes(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the includes needed for generated source file.
+grpc::string GetSourceIncludes(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the epilogue of the generated header file.
+grpc::string GetHeaderEpilogue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the prologue of the generated source file.
+grpc::string GetSourcePrologue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the services for generated header file.
+grpc::string GetHeaderServices(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the services for generated source file.
+grpc::string GetSourceServices(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the epilogue of the generated source file.
+grpc::string GetSourceEpilogue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the prologue of the generated mock file.
+grpc::string GetMockPrologue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the includes needed for generated mock file.
+grpc::string GetMockIncludes(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the services for generated mock file.
+grpc::string GetMockServices(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the epilogue of generated mock file.
+grpc::string GetMockEpilogue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the prologue of the generated mock file.
+grpc::string GetMockPrologue(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the includes needed for generated mock file.
+grpc::string GetMockIncludes(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the services for generated mock file.
+grpc::string GetMockServices(grpc_generator::File *file,
+ const Parameters &params);
+
+// Return the epilogue of generated mock file.
+grpc::string GetMockEpilogue(grpc_generator::File *file,
+ const Parameters &params);
+
+} // namespace grpc_cpp_generator
+
+#endif // GRPC_INTERNAL_COMPILER_CPP_GENERATOR_H
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.cc
new file mode 100644
index 0000000000..d646451aa6
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.cc
@@ -0,0 +1,501 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation AN/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include <map>
+#include <cctype>
+#include <sstream>
+
+#include "src/compiler/go_generator.h"
+
+template <class T>
+grpc::string as_string(T x) {
+ std::ostringstream out;
+ out << x;
+ return out.str();
+}
+
+inline bool ClientOnlyStreaming(const grpc_generator::Method *method) {
+ return method->ClientStreaming() && !method->ServerStreaming();
+}
+
+inline bool ServerOnlyStreaming(const grpc_generator::Method *method) {
+ return !method->ClientStreaming() && method->ServerStreaming();
+}
+
+namespace grpc_go_generator {
+
+// Returns string with first letter to lowerCase
+grpc::string unexportName(grpc::string s) {
+ if (s.empty())
+ return s;
+ s[0] = static_cast<char>(std::tolower(s[0]));
+ return s;
+}
+
+// Returns string with first letter to uppercase
+grpc::string exportName(grpc::string s) {
+ if (s.empty())
+ return s;
+ s[0] = static_cast<char>(std::toupper(s[0]));
+ return s;
+}
+
+void GenerateError(grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars,
+ const bool multiple_return = true) {
+ printer->Print(vars, "if $Error_Check$ {\n");
+ printer->Indent();
+ vars["Return"] = multiple_return ? "nil, err" : "err";
+ printer->Print(vars, "return $Return$\n");
+ printer->Outdent();
+ printer->Print("}\n");
+}
+
+// Generates imports for the service
+void GenerateImports(grpc_generator::File *file, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars) {
+ vars["filename"] = file->filename();
+ printer->Print("//Generated by gRPC Go plugin\n");
+ printer->Print("//If you make any local changes, they will be lost\n");
+ printer->Print(vars, "//source: $filename$\n\n");
+ printer->Print(vars, "package $Package$\n\n");
+ printer->Print("import (\n");
+ printer->Indent();
+ printer->Print(vars, "$context$ \"context\"\n");
+ printer->Print("flatbuffers \"github.com/google/flatbuffers/go\"\n");
+ printer->Print(vars, "$grpc$ \"google.golang.org/grpc\"\n");
+ printer->Print("\"google.golang.org/grpc/codes\"\n");
+ printer->Print("\"google.golang.org/grpc/status\"\n");
+ printer->Outdent();
+ printer->Print(")\n\n");
+}
+
+// Generates Server method signature source
+void GenerateServerMethodSignature(const grpc_generator::Method *method, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars) {
+ vars["Method"] = exportName(method->name());
+ vars["Request"] = method->get_input_type_name();
+ vars["Response"] = (vars["CustomMethodIO"] == "") ? method->get_output_type_name() : vars["CustomMethodIO"];
+ if (method->NoStreaming()) {
+ printer->Print(vars, "$Method$($context$.Context, *$Request$) (*$Response$, error)$Ending$");
+ } else if (ServerOnlyStreaming(method)) {
+ printer->Print(vars, "$Method$(*$Request$, $Service$_$Method$Server) error$Ending$");
+ } else {
+ printer->Print(vars, "$Method$($Service$_$Method$Server) error$Ending$");
+ }
+}
+
+void GenerateServerMethod(const grpc_generator::Method *method, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars) {
+ vars["Method"] = exportName(method->name());
+ vars["Request"] = method->get_input_type_name();
+ vars["Response"] = (vars["CustomMethodIO"] == "") ? method->get_output_type_name() : vars["CustomMethodIO"];
+ vars["FullMethodName"] = "/" + vars["ServicePrefix"] + vars["Service"] + "/" + vars["Method"];
+ vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
+ if (method->NoStreaming()) {
+ printer->Print(vars, "func $Handler$(srv interface{}, ctx $context$.Context,\n\tdec func(interface{}) error, interceptor $grpc$.UnaryServerInterceptor) (interface{}, error) {\n");
+ printer->Indent();
+ printer->Print(vars, "in := new($Request$)\n");
+ vars["Error_Check"] = "err := dec(in); err != nil";
+ GenerateError(printer, vars);
+ printer->Print("if interceptor == nil {\n");
+ printer->Indent();
+ printer->Print(vars, "return srv.($Service$Server).$Method$(ctx, in)\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Print(vars, "info := &$grpc$.UnaryServerInfo{\n");
+ printer->Indent();
+ printer->Print("Server: srv,\n");
+ printer->Print(vars, "FullMethod: \"$FullMethodName$\",\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Outdent();
+ printer->Print("\n");
+ printer->Indent();
+ printer->Print(vars, "handler := func(ctx $context$.Context, req interface{}) (interface{}, error) {\n");
+ printer->Indent();
+ printer->Print(vars, "return srv.($Service$Server).$Method$(ctx, req.(*$Request$))\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Print("return interceptor(ctx, in, info, handler)\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ return;
+ }
+ vars["StreamType"] = vars["ServiceUnexported"] + vars["Method"] + "Server";
+ printer->Print(vars, "func $Handler$(srv interface{}, stream $grpc$.ServerStream) error {\n");
+ printer->Indent();
+ if (ServerOnlyStreaming(method)) {
+ printer->Print(vars, "m := new($Request$)\n");
+ vars["Error_Check"] = "err := stream.RecvMsg(m); err != nil";
+ GenerateError(printer, vars, false);
+ printer->Print(vars, "return srv.($Service$Server).$Method$(m, &$StreamType${stream})\n");
+ } else {
+ printer->Print(vars, "return srv.($Service$Server).$Method$(&$StreamType${stream})\n");
+ }
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ bool genSend = method->BidiStreaming() || ServerOnlyStreaming(method);
+ bool genRecv = method->BidiStreaming() || ClientOnlyStreaming(method);
+ bool genSendAndClose = ClientOnlyStreaming(method);
+
+ printer->Print(vars, "type $Service$_$Method$Server interface {\n");
+ printer->Indent();
+ if (genSend) {
+ printer->Print(vars, "Send(*$Response$) error\n");
+ }
+ if (genRecv) {
+ printer->Print(vars, "Recv() (*$Request$, error)\n");
+ }
+ if (genSendAndClose) {
+ printer->Print(vars, "SendAndClose(*$Response$) error\n");
+ }
+ printer->Print(vars, "$grpc$.ServerStream\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ printer->Print(vars, "type $StreamType$ struct {\n");
+ printer->Indent();
+ printer->Print(vars, "$grpc$.ServerStream\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ if (genSend) {
+ printer->Print(vars, "func (x *$StreamType$) Send(m *$Response$) error {\n");
+ printer->Indent();
+ printer->Print("return x.ServerStream.SendMsg(m)\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+ if (genRecv) {
+ printer->Print(vars, "func (x *$StreamType$) Recv() (*$Request$, error) {\n");
+ printer->Indent();
+ printer->Print(vars, "m := new($Request$)\n");
+ vars["Error_Check"] = "err := x.ServerStream.RecvMsg(m); err != nil";
+ GenerateError(printer, vars);
+ printer->Print("return m, nil\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+ if (genSendAndClose) {
+ printer->Print(vars, "func (x *$StreamType$) SendAndClose(m *$Response$) error {\n");
+ printer->Indent();
+ printer->Print("return x.ServerStream.SendMsg(m)\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+
+}
+
+// Generates Client method signature source
+void GenerateClientMethodSignature(const grpc_generator::Method *method, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars) {
+ vars["Method"] = exportName(method->name());
+ vars["Request"] = ", in *" + ((vars["CustomMethodIO"] == "") ? method->get_input_type_name() : vars["CustomMethodIO"]);
+ if (ClientOnlyStreaming(method) || method->BidiStreaming()) {
+ vars["Request"] = "";
+ }
+ vars["Response"] = "*" + method->get_output_type_name();
+ if (ClientOnlyStreaming(method) || method->BidiStreaming() || ServerOnlyStreaming(method)) {
+ vars["Response"] = vars["Service"] + "_" + vars["Method"] + "Client" ;
+ }
+ printer->Print(vars, "$Method$(ctx $context$.Context$Request$,\n\topts ...$grpc$.CallOption) ($Response$, error)$Ending$");
+}
+
+// Generates Client method source
+void GenerateClientMethod(const grpc_generator::Method *method, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> vars) {
+ printer->Print(vars, "func (c *$ServiceUnexported$Client) ");
+ vars["Ending"] = " {\n";
+ GenerateClientMethodSignature(method, printer, vars);
+ printer->Indent();
+ vars["Method"] = exportName(method->name());
+ vars["Request"] = (vars["CustomMethodIO"] == "") ? method->get_input_type_name() : vars["CustomMethodIO"];
+ vars["Response"] = method->get_output_type_name();
+ vars["FullMethodName"] = "/" + vars["ServicePrefix"] + vars["Service"] + "/" + vars["Method"];
+ if (method->NoStreaming()) {
+ printer->Print(vars, "out := new($Response$)\n");
+ printer->Print(vars, "err := c.cc.Invoke(ctx, \"$FullMethodName$\", in, out, opts...)\n");
+ vars["Error_Check"] = "err != nil";
+ GenerateError(printer, vars);
+ printer->Print("return out, nil\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ return;
+ }
+ vars["StreamType"] = vars["ServiceUnexported"] + vars["Method"] + "Client";
+ printer->Print(vars, "stream, err := c.cc.NewStream(ctx, &$MethodDesc$, \"$FullMethodName$\", opts...)\n");
+ vars["Error_Check"] = "err != nil";
+ GenerateError(printer, vars);
+
+ printer->Print(vars, "x := &$StreamType${stream}\n");
+ if (ServerOnlyStreaming(method)) {
+ vars["Error_Check"] = "err := x.ClientStream.SendMsg(in); err != nil";
+ GenerateError(printer, vars);
+ vars["Error_Check"] = "err := x.ClientStream.CloseSend(); err != nil";
+ GenerateError(printer, vars);
+ }
+ printer->Print("return x, nil\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ bool genSend = method->BidiStreaming() || ClientOnlyStreaming(method);
+ bool genRecv = method->BidiStreaming() || ServerOnlyStreaming(method);
+ bool genCloseAndRecv = ClientOnlyStreaming(method);
+
+ //Stream interface
+ printer->Print(vars, "type $Service$_$Method$Client interface {\n");
+ printer->Indent();
+ if (genSend) {
+ printer->Print(vars, "Send(*$Request$) error\n");
+ }
+ if (genRecv) {
+ printer->Print(vars, "Recv() (*$Response$, error)\n");
+ }
+ if (genCloseAndRecv) {
+ printer->Print(vars, "CloseAndRecv() (*$Response$, error)\n");
+ }
+ printer->Print(vars, "$grpc$.ClientStream\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ //Stream Client
+ printer->Print(vars, "type $StreamType$ struct {\n");
+ printer->Indent();
+ printer->Print(vars, "$grpc$.ClientStream\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ if (genSend) {
+ printer->Print(vars, "func (x *$StreamType$) Send(m *$Request$) error {\n");
+ printer->Indent();
+ printer->Print("return x.ClientStream.SendMsg(m)\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+
+ if (genRecv) {
+ printer->Print(vars, "func (x *$StreamType$) Recv() (*$Response$, error) {\n");
+ printer->Indent();
+ printer->Print(vars, "m := new($Response$)\n");
+ vars["Error_Check"] = "err := x.ClientStream.RecvMsg(m); err != nil";
+ GenerateError(printer, vars);
+ printer->Print("return m, nil\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+
+ if (genCloseAndRecv) {
+ printer->Print(vars, "func (x *$StreamType$) CloseAndRecv() (*$Response$, error) {\n");
+ printer->Indent();
+ vars["Error_Check"] = "err := x.ClientStream.CloseSend(); err != nil";
+ GenerateError(printer, vars);
+ printer->Print(vars, "m := new($Response$)\n");
+ vars["Error_Check"] = "err := x.ClientStream.RecvMsg(m); err != nil";
+ GenerateError(printer, vars);
+ printer->Print("return m, nil\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ }
+}
+
+// Generates client API for the service
+void GenerateService(const grpc_generator::Service *service, grpc_generator::Printer* printer,
+ std::map<grpc::string, grpc::string> vars) {
+ vars["Service"] = exportName(service->name());
+ // Client Interface
+ printer->Print(vars, "// Client API for $Service$ service\n");
+ printer->Print(vars, "type $Service$Client interface {\n");
+ printer->Indent();
+ vars["Ending"] = "\n";
+ for (int i = 0; i < service->method_count(); i++) {
+ GenerateClientMethodSignature(service->method(i).get(), printer, vars);
+ }
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ // Client structure
+ vars["ServiceUnexported"] = unexportName(vars["Service"]);
+ printer->Print(vars, "type $ServiceUnexported$Client struct {\n");
+ printer->Indent();
+ printer->Print(vars, "cc $grpc$.ClientConnInterface\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ // NewClient
+ printer->Print(vars, "func New$Service$Client(cc $grpc$.ClientConnInterface) $Service$Client {\n");
+ printer->Indent();
+ printer->Print(vars, "return &$ServiceUnexported$Client{cc}");
+ printer->Outdent();
+ printer->Print("\n}\n\n");
+
+ int unary_methods = 0, streaming_methods = 0;
+ vars["ServiceDesc"] = "_" + vars["Service"] + "_serviceDesc";
+ for (int i = 0; i < service->method_count(); i++) {
+ auto method = service->method(i);
+ if (method->NoStreaming()) {
+ vars["MethodDesc"] = vars["ServiceDesc"] + ".Method[" + as_string(unary_methods) + "]";
+ unary_methods++;
+ } else {
+ vars["MethodDesc"] = vars["ServiceDesc"] + ".Streams[" + as_string(streaming_methods) + "]";
+ streaming_methods++;
+ }
+ GenerateClientMethod(method.get(), printer, vars);
+ }
+
+ //Server Interface
+ printer->Print(vars, "// Server API for $Service$ service\n");
+ printer->Print(vars, "type $Service$Server interface {\n");
+ printer->Indent();
+ vars["Ending"] = "\n";
+ for (int i = 0; i < service->method_count(); i++) {
+ GenerateServerMethodSignature(service->method(i).get(), printer, vars);
+ }
+ printer->Print(vars, "mustEmbedUnimplemented$Service$Server()\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ printer->Print(vars, "type Unimplemented$Service$Server struct {\n");
+ printer->Print("}\n\n");
+
+ vars["Ending"] = " {\n";
+ for (int i = 0; i < service->method_count(); i++) {
+ auto method = service->method(i);
+ vars["Method"] = exportName(method->name());
+ vars["Nil"] = method->NoStreaming() ? "nil, " : "";
+ printer->Print(vars, "func (Unimplemented$Service$Server) ");
+ GenerateServerMethodSignature(method.get(), printer, vars);
+ printer->Indent();
+ printer->Print(vars, "return $Nil$status.Errorf(codes.Unimplemented, \"method $Method$ not implemented\")\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Print("\n");
+ }
+
+ printer->Print(vars, "func (Unimplemented$Service$Server) mustEmbedUnimplemented$Service$Server() {}");
+ printer->Print("\n\n");
+
+ printer->Print(vars, "type Unsafe$Service$Server interface {\n");
+ printer->Indent();
+ printer->Print(vars, "mustEmbedUnimplemented$Service$Server()\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+ // Server registration.
+ printer->Print(vars, "func Register$Service$Server(s $grpc$.ServiceRegistrar, srv $Service$Server) {\n");
+ printer->Indent();
+ printer->Print(vars, "s.RegisterService(&$ServiceDesc$, srv)\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+
+ for (int i = 0; i < service->method_count(); i++) {
+ GenerateServerMethod(service->method(i).get(), printer, vars);
+ }
+
+
+ //Service Descriptor
+ printer->Print(vars, "var $ServiceDesc$ = $grpc$.ServiceDesc{\n");
+ printer->Indent();
+ printer->Print(vars, "ServiceName: \"$ServicePrefix$$Service$\",\n");
+ printer->Print(vars, "HandlerType: (*$Service$Server)(nil),\n");
+ printer->Print(vars, "Methods: []$grpc$.MethodDesc{\n");
+ printer->Indent();
+ for (int i = 0; i < service->method_count(); i++) {
+ auto method = service->method(i);
+ vars["Method"] = exportName(method->name());
+ vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
+ if (method->NoStreaming()) {
+ printer->Print("{\n");
+ printer->Indent();
+ printer->Print(vars, "MethodName: \"$Method$\",\n");
+ printer->Print(vars, "Handler: $Handler$,\n");
+ printer->Outdent();
+ printer->Print("},\n");
+ }
+ }
+ printer->Outdent();
+ printer->Print("},\n");
+ printer->Print(vars, "Streams: []$grpc$.StreamDesc{\n");
+ printer->Indent();
+ for (int i = 0; i < service->method_count(); i++) {
+ auto method = service->method(i);
+ vars["Method"] = exportName(method->name());
+ vars["Handler"] = "_" + vars["Service"] + "_" + vars["Method"] + "_Handler";
+ if (!method->NoStreaming()) {
+ printer->Print("{\n");
+ printer->Indent();
+ printer->Print(vars, "StreamName: \"$Method$\",\n");
+ printer->Print(vars, "Handler: $Handler$,\n");
+ if (ClientOnlyStreaming(method.get())) {
+ printer->Print("ClientStreams: true,\n");
+ } else if (ServerOnlyStreaming(method.get())) {
+ printer->Print("ServerStreams: true,\n");
+ } else {
+ printer->Print("ServerStreams: true,\n");
+ printer->Print("ClientStreams: true,\n");
+ }
+ printer->Outdent();
+ printer->Print("},\n");
+ }
+ }
+ printer->Outdent();
+ printer->Print("},\n");
+ printer->Outdent();
+ printer->Print("}\n");
+
+}
+
+
+// Returns source for the service
+grpc::string GenerateServiceSource(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ grpc_go_generator::Parameters *parameters) {
+ grpc::string out;
+ auto p = file->CreatePrinter(&out, '\t');
+ p->SetIndentationSize(1);
+ auto printer = p.get();
+ std::map<grpc::string, grpc::string> vars;
+ vars["Package"] = parameters->package_name;
+ vars["ServicePrefix"] = parameters->service_prefix;
+ if (!parameters->service_prefix.empty())
+ vars["ServicePrefix"].append(".");
+ vars["grpc"] = "grpc";
+ vars["context"] = "context";
+ GenerateImports(file, printer, vars);
+ if (parameters->custom_method_io_type != "") {
+ vars["CustomMethodIO"] = parameters->custom_method_io_type;
+ }
+ GenerateService(service, printer, vars);
+ return out;
+}
+}// Namespace grpc_go_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.h
new file mode 100644
index 0000000000..baa94e0599
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/go_generator.h
@@ -0,0 +1,64 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef GRPC_INTERNAL_COMPILER_GO_GENERATOR_H
+#define GRPC_INTERNAL_COMPILER_GO_GENERATOR_H
+
+//go generator is used to generate GRPC code for serialization system, such as flatbuffers
+#include <memory>
+#include <vector>
+
+#include "src/compiler/schema_interface.h"
+
+namespace grpc_go_generator {
+
+struct Parameters {
+ //Defines the custom parameter types for methods
+ //eg: flatbuffers uses flatbuffers.Builder as input for the client and output for the server
+ grpc::string custom_method_io_type;
+
+ //Package name for the service
+ grpc::string package_name;
+
+ //Prefix for RPC Calls
+ grpc::string service_prefix;
+};
+
+// Return the source of the generated service file.
+grpc::string GenerateServiceSource(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ grpc_go_generator::Parameters *parameters);
+
+}
+
+#endif // GRPC_INTERNAL_COMPILER_GO_GENERATOR_H
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.cc
new file mode 100644
index 0000000000..d2cf5ccc14
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.cc
@@ -0,0 +1,1135 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/compiler/java_generator.h"
+
+#include <algorithm>
+#include <iostream>
+#include <iterator>
+#include <map>
+#include <utility>
+#include <vector>
+
+// just to get flatbuffer_version_string()
+#include <flatbuffers/flatbuffers.h>
+#include <flatbuffers/util.h>
+#define to_string flatbuffers::NumToString
+
+// Stringify helpers used solely to cast GRPC_VERSION
+#ifndef STR
+#define STR(s) #s
+#endif
+
+#ifndef XSTR
+#define XSTR(s) STR(s)
+#endif
+
+
+typedef grpc_generator::Printer Printer;
+typedef std::map<grpc::string, grpc::string> VARS;
+typedef grpc_generator::Service ServiceDescriptor;
+typedef grpc_generator::CommentHolder
+ DescriptorType; // base class of all 'descriptors'
+typedef grpc_generator::Method MethodDescriptor;
+
+namespace grpc_java_generator {
+typedef std::string string;
+// Generates imports for the service
+void GenerateImports(grpc_generator::File* file,
+ grpc_generator::Printer* printer, VARS& vars) {
+ vars["filename"] = file->filename();
+ printer->Print(
+ vars,
+ "//Generated by flatc compiler (version $flatc_version$)\n");
+ printer->Print("//If you make any local changes, they will be lost\n");
+ printer->Print(vars, "//source: $filename$.fbs\n\n");
+ printer->Print(vars, "package $Package$;\n\n");
+ vars["Package"] = vars["Package"] + ".";
+ if (!file->additional_headers().empty()) {
+ printer->Print(file->additional_headers().c_str());
+ printer->Print("\n\n");
+ }
+}
+
+// Adjust a method name prefix identifier to follow the JavaBean spec:
+// - decapitalize the first letter
+// - remove embedded underscores & capitalize the following letter
+static string MixedLower(const string& word) {
+ string w;
+ w += static_cast<string::value_type>(tolower(word[0]));
+ bool after_underscore = false;
+ for (size_t i = 1; i < word.length(); ++i) {
+ if (word[i] == '_') {
+ after_underscore = true;
+ } else {
+ w += after_underscore ? static_cast<string::value_type>(toupper(word[i]))
+ : word[i];
+ after_underscore = false;
+ }
+ }
+ return w;
+}
+
+// Converts to the identifier to the ALL_UPPER_CASE format.
+// - An underscore is inserted where a lower case letter is followed by an
+// upper case letter.
+// - All letters are converted to upper case
+static string ToAllUpperCase(const string& word) {
+ string w;
+ for (size_t i = 0; i < word.length(); ++i) {
+ w += static_cast<string::value_type>(toupper(word[i]));
+ if ((i < word.length() - 1) && islower(word[i]) && isupper(word[i + 1])) {
+ w += '_';
+ }
+ }
+ return w;
+}
+
+static inline string LowerMethodName(const MethodDescriptor* method) {
+ return MixedLower(method->name());
+}
+
+static inline string MethodPropertiesFieldName(const MethodDescriptor* method) {
+ return "METHOD_" + ToAllUpperCase(method->name());
+}
+
+static inline string MethodPropertiesGetterName(
+ const MethodDescriptor* method) {
+ return MixedLower("get_" + method->name() + "_method");
+}
+
+static inline string MethodIdFieldName(const MethodDescriptor* method) {
+ return "METHODID_" + ToAllUpperCase(method->name());
+}
+
+static inline string JavaClassName(VARS& vars, const string& name) {
+ // string name = google::protobuf::compiler::java::ClassName(desc);
+ return vars["Package"] + name;
+}
+
+static inline string ServiceClassName(const string& service_name) {
+ return service_name + "Grpc";
+}
+
+// TODO(nmittler): Remove once protobuf includes javadoc methods in
+// distribution.
+template <typename ITR>
+static void GrpcSplitStringToIteratorUsing(const string& full,
+ const char* delim, ITR& result) {
+ // Optimize the common case where delim is a single character.
+ if (delim[0] != '\0' && delim[1] == '\0') {
+ char c = delim[0];
+ const char* p = full.data();
+ const char* end = p + full.size();
+ while (p != end) {
+ if (*p == c) {
+ ++p;
+ } else {
+ const char* start = p;
+ while (++p != end && *p != c)
+ ;
+ *result++ = string(start, p - start);
+ }
+ }
+ return;
+ }
+
+ string::size_type begin_index, end_index;
+ begin_index = full.find_first_not_of(delim);
+ while (begin_index != string::npos) {
+ end_index = full.find_first_of(delim, begin_index);
+ if (end_index == string::npos) {
+ *result++ = full.substr(begin_index);
+ return;
+ }
+ *result++ = full.substr(begin_index, (end_index - begin_index));
+ begin_index = full.find_first_not_of(delim, end_index);
+ }
+}
+
+static void GrpcSplitStringUsing(const string& full, const char* delim,
+ std::vector<string>* result) {
+ std::back_insert_iterator<std::vector<string>> it(*result);
+ GrpcSplitStringToIteratorUsing(full, delim, it);
+}
+
+static std::vector<string> GrpcSplit(const string& full, const char* delim) {
+ std::vector<string> result;
+ GrpcSplitStringUsing(full, delim, &result);
+ return result;
+}
+
+// TODO(nmittler): Remove once protobuf includes javadoc methods in
+// distribution.
+static string GrpcEscapeJavadoc(const string& input) {
+ string result;
+ result.reserve(input.size() * 2);
+
+ char prev = '*';
+
+ for (string::size_type i = 0; i < input.size(); i++) {
+ char c = input[i];
+ switch (c) {
+ case '*':
+ // Avoid "/*".
+ if (prev == '/') {
+ result.append("&#42;");
+ } else {
+ result.push_back(c);
+ }
+ break;
+ case '/':
+ // Avoid "*/".
+ if (prev == '*') {
+ result.append("&#47;");
+ } else {
+ result.push_back(c);
+ }
+ break;
+ case '@':
+ // '@' starts javadoc tags including the @deprecated tag, which will
+ // cause a compile-time error if inserted before a declaration that
+ // does not have a corresponding @Deprecated annotation.
+ result.append("&#64;");
+ break;
+ case '<':
+ // Avoid interpretation as HTML.
+ result.append("&lt;");
+ break;
+ case '>':
+ // Avoid interpretation as HTML.
+ result.append("&gt;");
+ break;
+ case '&':
+ // Avoid interpretation as HTML.
+ result.append("&amp;");
+ break;
+ case '\\':
+ // Java interprets Unicode escape sequences anywhere!
+ result.append("&#92;");
+ break;
+ default:
+ result.push_back(c);
+ break;
+ }
+
+ prev = c;
+ }
+
+ return result;
+}
+
+static std::vector<string> GrpcGetDocLines(const string& comments) {
+ if (!comments.empty()) {
+ // TODO(kenton): Ideally we should parse the comment text as Markdown and
+ // write it back as HTML, but this requires a Markdown parser. For now
+ // we just use <pre> to get fixed-width text formatting.
+
+ // If the comment itself contains block comment start or end markers,
+ // HTML-escape them so that they don't accidentally close the doc comment.
+ string escapedComments = GrpcEscapeJavadoc(comments);
+
+ std::vector<string> lines = GrpcSplit(escapedComments, "\n");
+ while (!lines.empty() && lines.back().empty()) {
+ lines.pop_back();
+ }
+ return lines;
+ }
+ return std::vector<string>();
+}
+
+static std::vector<string> GrpcGetDocLinesForDescriptor(
+ const DescriptorType* descriptor) {
+ return descriptor->GetAllComments();
+ // return GrpcGetDocLines(descriptor->GetLeadingComments("///"));
+}
+
+static void GrpcWriteDocCommentBody(Printer* printer, VARS& vars,
+ const std::vector<string>& lines,
+ bool surroundWithPreTag) {
+ if (!lines.empty()) {
+ if (surroundWithPreTag) {
+ printer->Print(" * <pre>\n");
+ }
+
+ for (size_t i = 0; i < lines.size(); i++) {
+ // Most lines should start with a space. Watch out for lines that start
+ // with a /, since putting that right after the leading asterisk will
+ // close the comment.
+ vars["line"] = lines[i];
+ if (!lines[i].empty() && lines[i][0] == '/') {
+ printer->Print(vars, " * $line$\n");
+ } else {
+ printer->Print(vars, " *$line$\n");
+ }
+ }
+
+ if (surroundWithPreTag) {
+ printer->Print(" * </pre>\n");
+ }
+ }
+}
+
+static void GrpcWriteDocComment(Printer* printer, VARS& vars,
+ const string& comments) {
+ printer->Print("/**\n");
+ std::vector<string> lines = GrpcGetDocLines(comments);
+ GrpcWriteDocCommentBody(printer, vars, lines, false);
+ printer->Print(" */\n");
+}
+
+static void GrpcWriteServiceDocComment(Printer* printer, VARS& vars,
+ const ServiceDescriptor* service) {
+ printer->Print("/**\n");
+ std::vector<string> lines = GrpcGetDocLinesForDescriptor(service);
+ GrpcWriteDocCommentBody(printer, vars, lines, true);
+ printer->Print(" */\n");
+}
+
+void GrpcWriteMethodDocComment(Printer* printer, VARS& vars,
+ const MethodDescriptor* method) {
+ printer->Print("/**\n");
+ std::vector<string> lines = GrpcGetDocLinesForDescriptor(method);
+ GrpcWriteDocCommentBody(printer, vars, lines, true);
+ printer->Print(" */\n");
+}
+
+//outputs static singleton extractor for type stored in "extr_type" and "extr_type_name" vars
+static void PrintTypeExtractor(Printer* p, VARS& vars) {
+ p->Print(
+ vars,
+ "private static volatile FlatbuffersUtils.FBExtactor<$extr_type$> "
+ "extractorOf$extr_type_name$;\n"
+ "private static FlatbuffersUtils.FBExtactor<$extr_type$> "
+ "getExtractorOf$extr_type_name$() {\n"
+ " if (extractorOf$extr_type_name$ != null) return "
+ "extractorOf$extr_type_name$;\n"
+ " synchronized ($service_class_name$.class) {\n"
+ " if (extractorOf$extr_type_name$ != null) return "
+ "extractorOf$extr_type_name$;\n"
+ " extractorOf$extr_type_name$ = new "
+ "FlatbuffersUtils.FBExtactor<$extr_type$>() {\n"
+ " public $extr_type$ extract (ByteBuffer buffer) {\n"
+ " return "
+ "$extr_type$.getRootAs$extr_type_name$(buffer);\n"
+ " }\n"
+ " };\n"
+ " return extractorOf$extr_type_name$;\n"
+ " }\n"
+ "}\n\n");
+}
+static void PrintMethodFields(Printer* p, VARS& vars,
+ const ServiceDescriptor* service) {
+ p->Print("// Static method descriptors that strictly reflect the proto.\n");
+ vars["service_name"] = service->name();
+
+ //set of names of rpc input- and output- types that were already encountered.
+ //this is needed to avoid duplicating type extractor since it's possible that
+ //the same type is used as an input or output type of more than a single RPC method
+ std::set<std::string> encounteredTypes;
+
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ vars["arg_in_id"] = to_string(2L * i); //trying to make msvc 10 happy
+ vars["arg_out_id"] = to_string(2L * i + 1);
+ vars["method_name"] = method->name();
+ vars["input_type_name"] = method->get_input_type_name();
+ vars["output_type_name"] = method->get_output_type_name();
+ vars["input_type"] = JavaClassName(vars, method->get_input_type_name());
+ vars["output_type"] = JavaClassName(vars, method->get_output_type_name());
+ vars["method_field_name"] = MethodPropertiesFieldName(method.get());
+ vars["method_new_field_name"] = MethodPropertiesGetterName(method.get());
+ vars["method_method_name"] = MethodPropertiesGetterName(method.get());
+ bool client_streaming = method->ClientStreaming() || method->BidiStreaming();
+ bool server_streaming = method->ServerStreaming() || method->BidiStreaming();
+ if (client_streaming) {
+ if (server_streaming) {
+ vars["method_type"] = "BIDI_STREAMING";
+ } else {
+ vars["method_type"] = "CLIENT_STREAMING";
+ }
+ } else {
+ if (server_streaming) {
+ vars["method_type"] = "SERVER_STREAMING";
+ } else {
+ vars["method_type"] = "UNARY";
+ }
+ }
+
+ p->Print(
+ vars,
+ "@$ExperimentalApi$(\"https://github.com/grpc/grpc-java/issues/"
+ "1901\")\n"
+ "@$Deprecated$ // Use {@link #$method_method_name$()} instead. \n"
+ "public static final $MethodDescriptor$<$input_type$,\n"
+ " $output_type$> $method_field_name$ = $method_method_name$();\n"
+ "\n"
+ "private static volatile $MethodDescriptor$<$input_type$,\n"
+ " $output_type$> $method_new_field_name$;\n"
+ "\n");
+
+ if (encounteredTypes.insert(vars["input_type_name"]).second) {
+ vars["extr_type"] = vars["input_type"];
+ vars["extr_type_name"] = vars["input_type_name"];
+ PrintTypeExtractor(p, vars);
+ }
+
+ if (encounteredTypes.insert(vars["output_type_name"]).second) {
+ vars["extr_type"] = vars["output_type"];
+ vars["extr_type_name"] = vars["output_type_name"];
+ PrintTypeExtractor(p, vars);
+ }
+
+ p->Print(
+ vars,
+ "@$ExperimentalApi$(\"https://github.com/grpc/grpc-java/issues/"
+ "1901\")\n"
+ "public static $MethodDescriptor$<$input_type$,\n"
+ " $output_type$> $method_method_name$() {\n"
+ " $MethodDescriptor$<$input_type$, $output_type$> "
+ "$method_new_field_name$;\n"
+ " if (($method_new_field_name$ = "
+ "$service_class_name$.$method_new_field_name$) == null) {\n"
+ " synchronized ($service_class_name$.class) {\n"
+ " if (($method_new_field_name$ = "
+ "$service_class_name$.$method_new_field_name$) == null) {\n"
+ " $service_class_name$.$method_new_field_name$ = "
+ "$method_new_field_name$ = \n"
+ " $MethodDescriptor$.<$input_type$, "
+ "$output_type$>newBuilder()\n"
+ " .setType($MethodType$.$method_type$)\n"
+ " .setFullMethodName(generateFullMethodName(\n"
+ " \"$Package$$service_name$\", \"$method_name$\"))\n"
+ " .setSampledToLocalTracing(true)\n"
+ " .setRequestMarshaller(FlatbuffersUtils.marshaller(\n"
+ " $input_type$.class, "
+ "getExtractorOf$input_type_name$()))\n"
+ " .setResponseMarshaller(FlatbuffersUtils.marshaller(\n"
+ " $output_type$.class, "
+ "getExtractorOf$output_type_name$()))\n");
+
+ // vars["proto_method_descriptor_supplier"] = service->name() +
+ // "MethodDescriptorSupplier";
+ p->Print(vars, " .setSchemaDescriptor(null)\n");
+ //" .setSchemaDescriptor(new
+ //$proto_method_descriptor_supplier$(\"$method_name$\"))\n");
+
+ p->Print(vars, " .build();\n");
+ p->Print(vars,
+ " }\n"
+ " }\n"
+ " }\n"
+ " return $method_new_field_name$;\n"
+ "}\n");
+
+ p->Print("\n");
+ }
+}
+enum StubType {
+ ASYNC_INTERFACE = 0,
+ BLOCKING_CLIENT_INTERFACE = 1,
+ FUTURE_CLIENT_INTERFACE = 2,
+ BLOCKING_SERVER_INTERFACE = 3,
+ ASYNC_CLIENT_IMPL = 4,
+ BLOCKING_CLIENT_IMPL = 5,
+ FUTURE_CLIENT_IMPL = 6,
+ ABSTRACT_CLASS = 7,
+};
+
+enum CallType { ASYNC_CALL = 0, BLOCKING_CALL = 1, FUTURE_CALL = 2 };
+
+static void PrintBindServiceMethodBody(Printer* p, VARS& vars,
+ const ServiceDescriptor* service);
+
+// Prints a client interface or implementation class, or a server interface.
+static void PrintStub(Printer* p, VARS& vars, const ServiceDescriptor* service,
+ StubType type) {
+ const string service_name = service->name();
+ vars["service_name"] = service_name;
+ vars["abstract_name"] = service_name + "ImplBase";
+ string stub_name = service_name;
+ string client_name = service_name;
+ CallType call_type = ASYNC_CALL;
+ bool impl_base = false;
+ bool interface = false;
+ switch (type) {
+ case ABSTRACT_CLASS:
+ call_type = ASYNC_CALL;
+ impl_base = true;
+ break;
+ case ASYNC_CLIENT_IMPL:
+ call_type = ASYNC_CALL;
+ stub_name += "Stub";
+ break;
+ case BLOCKING_CLIENT_INTERFACE:
+ interface = true;
+ FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BLOCKING_CLIENT_IMPL:
+ call_type = BLOCKING_CALL;
+ stub_name += "BlockingStub";
+ client_name += "BlockingClient";
+ break;
+ case FUTURE_CLIENT_INTERFACE:
+ interface = true;
+ FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case FUTURE_CLIENT_IMPL:
+ call_type = FUTURE_CALL;
+ stub_name += "FutureStub";
+ client_name += "FutureClient";
+ break;
+ case ASYNC_INTERFACE:
+ call_type = ASYNC_CALL;
+ interface = true;
+ break;
+ default:
+ GRPC_CODEGEN_FAIL << "Cannot determine class name for StubType: " << type;
+ }
+ vars["stub_name"] = stub_name;
+ vars["client_name"] = client_name;
+
+ // Class head
+ if (!interface) {
+ GrpcWriteServiceDocComment(p, vars, service);
+ }
+ if (impl_base) {
+ p->Print(vars,
+ "public static abstract class $abstract_name$ implements "
+ "$BindableService$ {\n");
+ } else {
+ p->Print(vars,
+ "public static final class $stub_name$ extends "
+ "$AbstractStub$<$stub_name$> {\n");
+ }
+ p->Indent();
+
+ // Constructor and build() method
+ if (!impl_base && !interface) {
+ p->Print(vars, "private $stub_name$($Channel$ channel) {\n");
+ p->Indent();
+ p->Print("super(channel);\n");
+ p->Outdent();
+ p->Print("}\n\n");
+ p->Print(vars,
+ "private $stub_name$($Channel$ channel,\n"
+ " $CallOptions$ callOptions) {\n");
+ p->Indent();
+ p->Print("super(channel, callOptions);\n");
+ p->Outdent();
+ p->Print("}\n\n");
+ p->Print(vars,
+ "@$Override$\n"
+ "protected $stub_name$ build($Channel$ channel,\n"
+ " $CallOptions$ callOptions) {\n");
+ p->Indent();
+ p->Print(vars, "return new $stub_name$(channel, callOptions);\n");
+ p->Outdent();
+ p->Print("}\n");
+ }
+
+ // RPC methods
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ vars["input_type"] = JavaClassName(vars, method->get_input_type_name());
+ vars["output_type"] = JavaClassName(vars, method->get_output_type_name());
+ vars["lower_method_name"] = LowerMethodName(&*method);
+ vars["method_method_name"] = MethodPropertiesGetterName(&*method);
+ bool client_streaming = method->ClientStreaming() || method->BidiStreaming();
+ bool server_streaming = method->ServerStreaming() || method->BidiStreaming();
+
+ if (call_type == BLOCKING_CALL && client_streaming) {
+ // Blocking client interface with client streaming is not available
+ continue;
+ }
+
+ if (call_type == FUTURE_CALL && (client_streaming || server_streaming)) {
+ // Future interface doesn't support streaming.
+ continue;
+ }
+
+ // Method signature
+ p->Print("\n");
+ // TODO(nmittler): Replace with WriteMethodDocComment once included by the
+ // protobuf distro.
+ if (!interface) {
+ GrpcWriteMethodDocComment(p, vars, &*method);
+ }
+ p->Print("public ");
+ switch (call_type) {
+ case BLOCKING_CALL:
+ GRPC_CODEGEN_CHECK(!client_streaming)
+ << "Blocking client interface with client streaming is unavailable";
+ if (server_streaming) {
+ // Server streaming
+ p->Print(vars,
+ "$Iterator$<$output_type$> $lower_method_name$(\n"
+ " $input_type$ request)");
+ } else {
+ // Simple RPC
+ p->Print(vars,
+ "$output_type$ $lower_method_name$($input_type$ request)");
+ }
+ break;
+ case ASYNC_CALL:
+ if (client_streaming) {
+ // Bidirectional streaming or client streaming
+ p->Print(vars,
+ "$StreamObserver$<$input_type$> $lower_method_name$(\n"
+ " $StreamObserver$<$output_type$> responseObserver)");
+ } else {
+ // Server streaming or simple RPC
+ p->Print(vars,
+ "void $lower_method_name$($input_type$ request,\n"
+ " $StreamObserver$<$output_type$> responseObserver)");
+ }
+ break;
+ case FUTURE_CALL:
+ GRPC_CODEGEN_CHECK(!client_streaming && !server_streaming)
+ << "Future interface doesn't support streaming. "
+ << "client_streaming=" << client_streaming << ", "
+ << "server_streaming=" << server_streaming;
+ p->Print(vars,
+ "$ListenableFuture$<$output_type$> $lower_method_name$(\n"
+ " $input_type$ request)");
+ break;
+ }
+
+ if (interface) {
+ p->Print(";\n");
+ continue;
+ }
+ // Method body.
+ p->Print(" {\n");
+ p->Indent();
+ if (impl_base) {
+ switch (call_type) {
+ // NB: Skipping validation of service methods. If something is wrong,
+ // we wouldn't get to this point as compiler would return errors when
+ // generating service interface.
+ case ASYNC_CALL:
+ if (client_streaming) {
+ p->Print(vars,
+ "return "
+ "asyncUnimplementedStreamingCall($method_method_name$(), "
+ "responseObserver);\n");
+ } else {
+ p->Print(vars,
+ "asyncUnimplementedUnaryCall($method_method_name$(), "
+ "responseObserver);\n");
+ }
+ break;
+ default:
+ break;
+ }
+ } else if (!interface) {
+ switch (call_type) {
+ case BLOCKING_CALL:
+ GRPC_CODEGEN_CHECK(!client_streaming)
+ << "Blocking client streaming interface is not available";
+ if (server_streaming) {
+ vars["calls_method"] = "blockingServerStreamingCall";
+ vars["params"] = "request";
+ } else {
+ vars["calls_method"] = "blockingUnaryCall";
+ vars["params"] = "request";
+ }
+ p->Print(vars,
+ "return $calls_method$(\n"
+ " getChannel(), $method_method_name$(), "
+ "getCallOptions(), $params$);\n");
+ break;
+ case ASYNC_CALL:
+ if (server_streaming) {
+ if (client_streaming) {
+ vars["calls_method"] = "asyncBidiStreamingCall";
+ vars["params"] = "responseObserver";
+ } else {
+ vars["calls_method"] = "asyncServerStreamingCall";
+ vars["params"] = "request, responseObserver";
+ }
+ } else {
+ if (client_streaming) {
+ vars["calls_method"] = "asyncClientStreamingCall";
+ vars["params"] = "responseObserver";
+ } else {
+ vars["calls_method"] = "asyncUnaryCall";
+ vars["params"] = "request, responseObserver";
+ }
+ }
+ vars["last_line_prefix"] = client_streaming ? "return " : "";
+ p->Print(vars,
+ "$last_line_prefix$$calls_method$(\n"
+ " getChannel().newCall($method_method_name$(), "
+ "getCallOptions()), $params$);\n");
+ break;
+ case FUTURE_CALL:
+ GRPC_CODEGEN_CHECK(!client_streaming && !server_streaming)
+ << "Future interface doesn't support streaming. "
+ << "client_streaming=" << client_streaming << ", "
+ << "server_streaming=" << server_streaming;
+ vars["calls_method"] = "futureUnaryCall";
+ p->Print(vars,
+ "return $calls_method$(\n"
+ " getChannel().newCall($method_method_name$(), "
+ "getCallOptions()), request);\n");
+ break;
+ }
+ }
+ p->Outdent();
+ p->Print("}\n");
+ }
+
+ if (impl_base) {
+ p->Print("\n");
+ p->Print(
+ vars,
+ "@$Override$ public final $ServerServiceDefinition$ bindService() {\n");
+ vars["instance"] = "this";
+ PrintBindServiceMethodBody(p, vars, service);
+ p->Print("}\n");
+ }
+
+ p->Outdent();
+ p->Print("}\n\n");
+}
+
+static bool CompareMethodClientStreaming(
+ const std::unique_ptr<const grpc_generator::Method>& method1,
+ const std::unique_ptr<const grpc_generator::Method>& method2) {
+ return method1->ClientStreaming() < method2->ClientStreaming();
+}
+
+// Place all method invocations into a single class to reduce memory footprint
+// on Android.
+static void PrintMethodHandlerClass(Printer* p, VARS& vars,
+ const ServiceDescriptor* service) {
+ // Sort method ids based on ClientStreaming() so switch tables are compact.
+ std::vector<std::unique_ptr<const grpc_generator::Method>> sorted_methods(
+ service->method_count());
+ for (int i = 0; i < service->method_count(); ++i) {
+ sorted_methods[i] = service->method(i);
+ }
+ stable_sort(sorted_methods.begin(), sorted_methods.end(),
+ CompareMethodClientStreaming);
+ for (size_t i = 0; i < sorted_methods.size(); i++) {
+ auto& method = sorted_methods[i];
+ vars["method_id"] = to_string(i);
+ vars["method_id_name"] = MethodIdFieldName(&*method);
+ p->Print(vars,
+ "private static final int $method_id_name$ = $method_id$;\n");
+ }
+ p->Print("\n");
+ vars["service_name"] = service->name() + "ImplBase";
+ p->Print(vars,
+ "private static final class MethodHandlers<Req, Resp> implements\n"
+ " io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,\n"
+ " io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,\n"
+ " io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,\n"
+ " io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {\n"
+ " private final $service_name$ serviceImpl;\n"
+ " private final int methodId;\n"
+ "\n"
+ " MethodHandlers($service_name$ serviceImpl, int methodId) {\n"
+ " this.serviceImpl = serviceImpl;\n"
+ " this.methodId = methodId;\n"
+ " }\n\n");
+ p->Indent();
+ p->Print(vars,
+ "@$Override$\n"
+ "@java.lang.SuppressWarnings(\"unchecked\")\n"
+ "public void invoke(Req request, $StreamObserver$<Resp> "
+ "responseObserver) {\n"
+ " switch (methodId) {\n");
+ p->Indent();
+ p->Indent();
+
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ if (method->ClientStreaming() || method->BidiStreaming()) {
+ continue;
+ }
+ vars["method_id_name"] = MethodIdFieldName(&*method);
+ vars["lower_method_name"] = LowerMethodName(&*method);
+ vars["input_type"] = JavaClassName(vars, method->get_input_type_name());
+ vars["output_type"] = JavaClassName(vars, method->get_output_type_name());
+ p->Print(vars,
+ "case $method_id_name$:\n"
+ " serviceImpl.$lower_method_name$(($input_type$) request,\n"
+ " ($StreamObserver$<$output_type$>) responseObserver);\n"
+ " break;\n");
+ }
+ p->Print(
+ "default:\n"
+ " throw new AssertionError();\n");
+
+ p->Outdent();
+ p->Outdent();
+ p->Print(
+ " }\n"
+ "}\n\n");
+
+ p->Print(vars,
+ "@$Override$\n"
+ "@java.lang.SuppressWarnings(\"unchecked\")\n"
+ "public $StreamObserver$<Req> invoke(\n"
+ " $StreamObserver$<Resp> responseObserver) {\n"
+ " switch (methodId) {\n");
+ p->Indent();
+ p->Indent();
+
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ if (!(method->ClientStreaming() || method->BidiStreaming())) {
+ continue;
+ }
+ vars["method_id_name"] = MethodIdFieldName(&*method);
+ vars["lower_method_name"] = LowerMethodName(&*method);
+ vars["input_type"] = JavaClassName(vars, method->get_input_type_name());
+ vars["output_type"] = JavaClassName(vars, method->get_output_type_name());
+ p->Print(
+ vars,
+ "case $method_id_name$:\n"
+ " return ($StreamObserver$<Req>) serviceImpl.$lower_method_name$(\n"
+ " ($StreamObserver$<$output_type$>) responseObserver);\n");
+ }
+ p->Print(
+ "default:\n"
+ " throw new AssertionError();\n");
+
+ p->Outdent();
+ p->Outdent();
+ p->Print(
+ " }\n"
+ "}\n");
+
+ p->Outdent();
+ p->Print("}\n\n");
+}
+
+static void PrintGetServiceDescriptorMethod(Printer* p, VARS& vars,
+ const ServiceDescriptor* service) {
+ vars["service_name"] = service->name();
+ // vars["proto_base_descriptor_supplier"] = service->name() +
+ // "BaseDescriptorSupplier"; vars["proto_file_descriptor_supplier"] =
+ // service->name() + "FileDescriptorSupplier";
+ // vars["proto_method_descriptor_supplier"] = service->name() +
+ // "MethodDescriptorSupplier"; vars["proto_class_name"] =
+ // google::protobuf::compiler::java::ClassName(service->file());
+ // p->Print(
+ // vars,
+ // "private static abstract class
+ // $proto_base_descriptor_supplier$\n" " implements
+ // $ProtoFileDescriptorSupplier$,
+ // $ProtoServiceDescriptorSupplier$ {\n" "
+ // $proto_base_descriptor_supplier$() {}\n"
+ // "\n"
+ // " @$Override$\n"
+ // " public com.google.protobuf.Descriptors.FileDescriptor
+ // getFileDescriptor() {\n" " return
+ // $proto_class_name$.getDescriptor();\n" " }\n"
+ // "\n"
+ // " @$Override$\n"
+ // " public com.google.protobuf.Descriptors.ServiceDescriptor
+ // getServiceDescriptor() {\n" " return
+ // getFileDescriptor().findServiceByName(\"$service_name$\");\n"
+ // " }\n"
+ // "}\n"
+ // "\n"
+ // "private static final class
+ // $proto_file_descriptor_supplier$\n" " extends
+ // $proto_base_descriptor_supplier$ {\n" "
+ // $proto_file_descriptor_supplier$() {}\n"
+ // "}\n"
+ // "\n"
+ // "private static final class
+ // $proto_method_descriptor_supplier$\n" " extends
+ // $proto_base_descriptor_supplier$\n" " implements
+ // $ProtoMethodDescriptorSupplier$ {\n" " private final
+ // String methodName;\n"
+ // "\n"
+ // " $proto_method_descriptor_supplier$(String methodName)
+ // {\n" " this.methodName = methodName;\n" " }\n"
+ // "\n"
+ // " @$Override$\n"
+ // " public com.google.protobuf.Descriptors.MethodDescriptor
+ // getMethodDescriptor() {\n" " return
+ // getServiceDescriptor().findMethodByName(methodName);\n" "
+ // }\n"
+ // "}\n\n");
+
+ p->Print(
+ vars,
+ "private static volatile $ServiceDescriptor$ serviceDescriptor;\n\n");
+
+ p->Print(vars,
+ "public static $ServiceDescriptor$ getServiceDescriptor() {\n");
+ p->Indent();
+ p->Print(vars, "$ServiceDescriptor$ result = serviceDescriptor;\n");
+ p->Print("if (result == null) {\n");
+ p->Indent();
+ p->Print(vars, "synchronized ($service_class_name$.class) {\n");
+ p->Indent();
+ p->Print("result = serviceDescriptor;\n");
+ p->Print("if (result == null) {\n");
+ p->Indent();
+
+ p->Print(vars,
+ "serviceDescriptor = result = "
+ "$ServiceDescriptor$.newBuilder(SERVICE_NAME)");
+ p->Indent();
+ p->Indent();
+ p->Print(vars, "\n.setSchemaDescriptor(null)");
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ vars["method_method_name"] = MethodPropertiesGetterName(&*method);
+ p->Print(vars, "\n.addMethod($method_method_name$())");
+ }
+ p->Print("\n.build();\n");
+ p->Outdent();
+ p->Outdent();
+
+ p->Outdent();
+ p->Print("}\n");
+ p->Outdent();
+ p->Print("}\n");
+ p->Outdent();
+ p->Print("}\n");
+ p->Print("return result;\n");
+ p->Outdent();
+ p->Print("}\n");
+}
+
+static void PrintBindServiceMethodBody(Printer* p, VARS& vars,
+ const ServiceDescriptor* service) {
+ vars["service_name"] = service->name();
+ p->Indent();
+ p->Print(vars,
+ "return "
+ "$ServerServiceDefinition$.builder(getServiceDescriptor())\n");
+ p->Indent();
+ p->Indent();
+ for (int i = 0; i < service->method_count(); ++i) {
+ auto method = service->method(i);
+ vars["lower_method_name"] = LowerMethodName(&*method);
+ vars["method_method_name"] = MethodPropertiesGetterName(&*method);
+ vars["input_type"] = JavaClassName(vars, method->get_input_type_name());
+ vars["output_type"] = JavaClassName(vars, method->get_output_type_name());
+ vars["method_id_name"] = MethodIdFieldName(&*method);
+ bool client_streaming = method->ClientStreaming() || method->BidiStreaming();
+ bool server_streaming = method->ServerStreaming() || method->BidiStreaming();
+ if (client_streaming) {
+ if (server_streaming) {
+ vars["calls_method"] = "asyncBidiStreamingCall";
+ } else {
+ vars["calls_method"] = "asyncClientStreamingCall";
+ }
+ } else {
+ if (server_streaming) {
+ vars["calls_method"] = "asyncServerStreamingCall";
+ } else {
+ vars["calls_method"] = "asyncUnaryCall";
+ }
+ }
+ p->Print(vars, ".addMethod(\n");
+ p->Indent();
+ p->Print(vars,
+ "$method_method_name$(),\n"
+ "$calls_method$(\n");
+ p->Indent();
+ p->Print(vars,
+ "new MethodHandlers<\n"
+ " $input_type$,\n"
+ " $output_type$>(\n"
+ " $instance$, $method_id_name$)))\n");
+ p->Outdent();
+ p->Outdent();
+ }
+ p->Print(".build();\n");
+ p->Outdent();
+ p->Outdent();
+ p->Outdent();
+}
+
+static void PrintService(Printer* p, VARS& vars,
+ const ServiceDescriptor* service,
+ bool disable_version) {
+ vars["service_name"] = service->name();
+ vars["service_class_name"] = ServiceClassName(service->name());
+ vars["grpc_version"] = "";
+#ifdef GRPC_VERSION
+ if (!disable_version) {
+ vars["grpc_version"] = " (version " XSTR(GRPC_VERSION) ")";
+ }
+#else
+ (void)disable_version;
+#endif
+ // TODO(nmittler): Replace with WriteServiceDocComment once included by
+ // protobuf distro.
+ GrpcWriteServiceDocComment(p, vars, service);
+ p->Print(vars,
+ "@$Generated$(\n"
+ " value = \"by gRPC proto compiler$grpc_version$\",\n"
+ " comments = \"Source: $file_name$.fbs\")\n"
+ "public final class $service_class_name$ {\n\n");
+ p->Indent();
+ p->Print(vars, "private $service_class_name$() {}\n\n");
+
+ p->Print(vars,
+ "public static final String SERVICE_NAME = "
+ "\"$Package$$service_name$\";\n\n");
+
+ PrintMethodFields(p, vars, service);
+
+ // TODO(nmittler): Replace with WriteDocComment once included by protobuf
+ // distro.
+ GrpcWriteDocComment(
+ p, vars,
+ " Creates a new async stub that supports all call types for the service");
+ p->Print(vars,
+ "public static $service_name$Stub newStub($Channel$ channel) {\n");
+ p->Indent();
+ p->Print(vars, "return new $service_name$Stub(channel);\n");
+ p->Outdent();
+ p->Print("}\n\n");
+
+ // TODO(nmittler): Replace with WriteDocComment once included by protobuf
+ // distro.
+ GrpcWriteDocComment(
+ p, vars,
+ " Creates a new blocking-style stub that supports unary and streaming "
+ "output calls on the service");
+ p->Print(vars,
+ "public static $service_name$BlockingStub newBlockingStub(\n"
+ " $Channel$ channel) {\n");
+ p->Indent();
+ p->Print(vars, "return new $service_name$BlockingStub(channel);\n");
+ p->Outdent();
+ p->Print("}\n\n");
+
+ // TODO(nmittler): Replace with WriteDocComment once included by protobuf
+ // distro.
+ GrpcWriteDocComment(
+ p, vars,
+ " Creates a new ListenableFuture-style stub that supports unary calls "
+ "on the service");
+ p->Print(vars,
+ "public static $service_name$FutureStub newFutureStub(\n"
+ " $Channel$ channel) {\n");
+ p->Indent();
+ p->Print(vars, "return new $service_name$FutureStub(channel);\n");
+ p->Outdent();
+ p->Print("}\n\n");
+
+ PrintStub(p, vars, service, ABSTRACT_CLASS);
+ PrintStub(p, vars, service, ASYNC_CLIENT_IMPL);
+ PrintStub(p, vars, service, BLOCKING_CLIENT_IMPL);
+ PrintStub(p, vars, service, FUTURE_CLIENT_IMPL);
+
+ PrintMethodHandlerClass(p, vars, service);
+ PrintGetServiceDescriptorMethod(p, vars, service);
+ p->Outdent();
+ p->Print("}\n");
+}
+
+void PrintStaticImports(Printer* p) {
+ p->Print(
+ "import java.nio.ByteBuffer;\n"
+ "import static "
+ "io.grpc.MethodDescriptor.generateFullMethodName;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.asyncBidiStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.asyncClientStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.asyncServerStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.asyncUnaryCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.blockingServerStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.blockingUnaryCall;\n"
+ "import static "
+ "io.grpc.stub.ClientCalls.futureUnaryCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncBidiStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncClientStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncServerStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncUnaryCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall;\n"
+ "import static "
+ "io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall;\n\n");
+}
+
+void GenerateService(const grpc_generator::Service* service,
+ grpc_generator::Printer* printer, VARS& vars,
+ bool disable_version) {
+ // All non-generated classes must be referred by fully qualified names to
+ // avoid collision with generated classes.
+ vars["String"] = "java.lang.String";
+ vars["Deprecated"] = "java.lang.Deprecated";
+ vars["Override"] = "java.lang.Override";
+ vars["Channel"] = "io.grpc.Channel";
+ vars["CallOptions"] = "io.grpc.CallOptions";
+ vars["MethodType"] = "io.grpc.MethodDescriptor.MethodType";
+ vars["ServerMethodDefinition"] = "io.grpc.ServerMethodDefinition";
+ vars["BindableService"] = "io.grpc.BindableService";
+ vars["ServerServiceDefinition"] = "io.grpc.ServerServiceDefinition";
+ vars["ServiceDescriptor"] = "io.grpc.ServiceDescriptor";
+ vars["ProtoFileDescriptorSupplier"] =
+ "io.grpc.protobuf.ProtoFileDescriptorSupplier";
+ vars["ProtoServiceDescriptorSupplier"] =
+ "io.grpc.protobuf.ProtoServiceDescriptorSupplier";
+ vars["ProtoMethodDescriptorSupplier"] =
+ "io.grpc.protobuf.ProtoMethodDescriptorSupplier";
+ vars["AbstractStub"] = "io.grpc.stub.AbstractStub";
+ vars["MethodDescriptor"] = "io.grpc.MethodDescriptor";
+ vars["NanoUtils"] = "io.grpc.protobuf.nano.NanoUtils";
+ vars["StreamObserver"] = "io.grpc.stub.StreamObserver";
+ vars["Iterator"] = "java.util.Iterator";
+ vars["Generated"] = "javax.annotation.Generated";
+ vars["ListenableFuture"] =
+ "com.google.common.util.concurrent.ListenableFuture";
+ vars["ExperimentalApi"] = "io.grpc.ExperimentalApi";
+
+ PrintStaticImports(printer);
+
+ PrintService(printer, vars, service, disable_version);
+}
+
+grpc::string GenerateServiceSource(
+ grpc_generator::File* file, const grpc_generator::Service* service,
+ grpc_java_generator::Parameters* parameters) {
+ grpc::string out;
+ auto printer = file->CreatePrinter(&out);
+ VARS vars;
+ vars["flatc_version"] = grpc::string(
+ FLATBUFFERS_STRING(FLATBUFFERS_VERSION_MAJOR) "." FLATBUFFERS_STRING(
+ FLATBUFFERS_VERSION_MINOR) "." FLATBUFFERS_STRING(FLATBUFFERS_VERSION_REVISION));
+
+ vars["file_name"] = file->filename();
+
+ if (!parameters->package_name.empty()) {
+ vars["Package"] = parameters->package_name; // ServiceJavaPackage(service);
+ }
+ GenerateImports(file, &*printer, vars);
+ GenerateService(service, &*printer, vars, false);
+ return out;
+}
+
+} // namespace grpc_java_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.h
new file mode 100644
index 0000000000..b101fbf565
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/java_generator.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NET_GRPC_COMPILER_JAVA_GENERATOR_H_
+#define NET_GRPC_COMPILER_JAVA_GENERATOR_H_
+
+#include <stdlib.h> // for abort()
+#include <iostream>
+#include <map>
+#include <string>
+
+#include "src/compiler/schema_interface.h"
+
+class LogMessageVoidify {
+ public:
+ LogMessageVoidify() {}
+ // This has to be an operator with a precedence lower than << but
+ // higher than ?:
+ void operator&(std::ostream&) {}
+};
+
+class LogHelper {
+ std::ostream* os_;
+
+ public:
+ LogHelper(std::ostream* os) : os_(os) {}
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning( \
+ disable : 4722) // the flow of control terminates in a destructor
+ // (needed to compile ~LogHelper where destructor emits abort intentionally -
+ // inherited from grpc/java code generator).
+#endif
+ ~LogHelper() {
+ *os_ << std::endl;
+ ::abort();
+ }
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+ std::ostream& get_os() const { return *os_; }
+};
+
+// Abort the program after logging the mesage if the given condition is not
+// true. Otherwise, do nothing.
+#define GRPC_CODEGEN_CHECK(x) \
+ (x) ? (void)0 \
+ : LogMessageVoidify() & LogHelper(&std::cerr).get_os() \
+ << "CHECK FAILED: " << __FILE__ << ":" \
+ << __LINE__ << ": "
+
+// Abort the program after logging the mesage.
+#define GRPC_CODEGEN_FAIL GRPC_CODEGEN_CHECK(false)
+
+namespace grpc_java_generator {
+struct Parameters {
+ // //Defines the custom parameter types for methods
+ // //eg: flatbuffers uses flatbuffers.Builder as input for the client
+ // and output for the server grpc::string custom_method_io_type;
+
+ // Package name for the service
+ grpc::string package_name;
+};
+
+// Return the source of the generated service file.
+grpc::string GenerateServiceSource(grpc_generator::File* file,
+ const grpc_generator::Service* service,
+ grpc_java_generator::Parameters* parameters);
+
+} // namespace grpc_java_generator
+
+#endif // NET_GRPC_COMPILER_JAVA_GENERATOR_H_
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.cc
new file mode 100644
index 0000000000..8108db45d5
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.cc
@@ -0,0 +1,149 @@
+/*
+ *
+ * Copyright 2015 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <map>
+#include <sstream>
+
+#include "flatbuffers/util.h"
+#include "src/compiler/python_generator.h"
+
+namespace grpc_python_generator {
+
+grpc::string GenerateMethodType(const grpc_generator::Method *method) {
+
+ if (method->NoStreaming())
+ return "unary_unary";
+
+ if (method->ServerStreaming())
+ return "unary_stream";
+
+ if (method->ClientStreaming())
+ return "stream_unary";
+
+ return "stream_stream";
+}
+
+grpc::string GenerateMethodInput(const grpc_generator::Method *method) {
+
+ if (method->NoStreaming() || method->ServerStreaming())
+ return "self, request, context";
+
+ return "self, request_iterator, context";
+}
+
+void GenerateStub(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "class $ServiceName$Stub(object):\n");
+ printer->Indent();
+ printer->Print("\"\"\" Interface exported by the server. \"\"\"");
+ printer->Print("\n\n");
+ printer->Print("def __init__(self, channel):\n");
+ printer->Indent();
+ printer->Print("\"\"\" Constructor. \n\n");
+ printer->Print("Args: \n");
+ printer->Print("channel: A grpc.Channel. \n");
+ printer->Print("\"\"\"\n\n");
+
+ for (int j = 0; j < service->method_count(); j++) {
+ auto method = service->method(j);
+ vars["MethodName"] = method->name();
+ vars["MethodType"] = GenerateMethodType(&*method);
+ printer->Print(vars, "self.$MethodName$ = channel.$MethodType$(\n");
+ printer->Indent();
+ printer->Print(vars, "\"/$PATH$$ServiceName$/$MethodName$\"\n");
+ printer->Print(")\n");
+ printer->Outdent();
+ printer->Print("\n");
+ }
+ printer->Outdent();
+ printer->Outdent();
+ printer->Print("\n");
+}
+
+void GenerateServicer(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "class $ServiceName$Servicer(object):\n");
+ printer->Indent();
+ printer->Print("\"\"\" Interface exported by the server. \"\"\"");
+ printer->Print("\n\n");
+
+ for (int j = 0; j < service->method_count(); j++) {
+ auto method = service->method(j);
+ vars["MethodName"] = method->name();
+ vars["MethodInput"] = GenerateMethodInput(&*method);
+ printer->Print(vars, "def $MethodName$($MethodInput$):\n");
+ printer->Indent();
+ printer->Print("context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n");
+ printer->Print("context.set_details('Method not implemented!')\n");
+ printer->Print("raise NotImplementedError('Method not implemented!')\n");
+ printer->Outdent();
+ printer->Print("\n\n");
+ }
+ printer->Outdent();
+ printer->Print("\n");
+
+}
+
+void GenerateRegister(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "def add_$ServiceName$Servicer_to_server(servicer, server):\n");
+ printer->Indent();
+ printer->Print("rpc_method_handlers = {\n");
+ printer->Indent();
+ for (int j = 0; j < service->method_count(); j++) {
+ auto method = service->method(j);
+ vars["MethodName"] = method->name();
+ vars["MethodType"] = GenerateMethodType(&*method);
+ printer->Print(vars, "'$MethodName$': grpc.$MethodType$_rpc_method_handler(\n");
+ printer->Indent();
+ printer->Print(vars, "servicer.$MethodName$\n");
+ printer->Outdent();
+ printer->Print("),\n");
+ }
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Print(vars, "generic_handler = grpc.method_handlers_generic_handler(\n");
+ printer->Indent();
+ printer->Print(vars, "'$PATH$$ServiceName$', rpc_method_handlers)\n");
+ printer->Outdent();
+ printer->Print("server.add_generic_rpc_handlers((generic_handler,))");
+ printer->Outdent();
+ printer->Print("\n");
+}
+
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service) {
+ grpc::string output;
+ std::map<grpc::string, grpc::string> vars;
+ vars["PATH"] = file->package();
+ if (!file->package().empty()) { vars["PATH"].append("."); }
+ vars["ServiceName"] = service->name();
+ auto printer = file->CreatePrinter(&output);
+ GenerateStub(service, &*printer, &vars);
+ GenerateServicer(service, &*printer, &vars);
+ GenerateRegister(service, &*printer, &vars);
+ return output;
+}
+
+} // namespace grpc_python_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.h
new file mode 100644
index 0000000000..4f8f5cc806
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/python_generator.h
@@ -0,0 +1,33 @@
+/*
+ *
+ * Copyright 2015 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef GRPC_INTERNAL_COMPILER_PYTHON_GENERATOR_H
+#define GRPC_INTERNAL_COMPILER_PYTHON_GENERATOR_H
+
+#include <utility>
+
+#include "src/compiler/config.h"
+#include "src/compiler/schema_interface.h"
+
+namespace grpc_python_generator {
+
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service);
+} // namespace grpc_python_generator
+
+#endif // GRPC_INTERNAL_COMPILER_PYTHON_GENERATOR_H
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/schema_interface.h b/contrib/libs/flatbuffers/grpc/src/compiler/schema_interface.h
new file mode 100644
index 0000000000..0449498198
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/schema_interface.h
@@ -0,0 +1,120 @@
+/*
+ *
+ * Copyright 2015 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef GRPC_INTERNAL_COMPILER_SCHEMA_INTERFACE_H
+#define GRPC_INTERNAL_COMPILER_SCHEMA_INTERFACE_H
+
+#include <memory>
+#include <vector>
+
+#include "src/compiler/config.h"
+
+#ifndef GRPC_CUSTOM_STRING
+# include <string>
+# define GRPC_CUSTOM_STRING std::string
+#endif
+
+namespace grpc {
+
+typedef GRPC_CUSTOM_STRING string;
+
+} // namespace grpc
+
+namespace grpc_generator {
+
+// A common interface for objects having comments in the source.
+// Return formatted comments to be inserted in generated code.
+struct CommentHolder {
+ virtual ~CommentHolder() {}
+ virtual grpc::string GetLeadingComments(const grpc::string prefix) const = 0;
+ virtual grpc::string GetTrailingComments(const grpc::string prefix) const = 0;
+ virtual std::vector<grpc::string> GetAllComments() const = 0;
+};
+
+// An abstract interface representing a method.
+struct Method : public CommentHolder {
+ virtual ~Method() {}
+
+ virtual grpc::string name() const = 0;
+
+ virtual grpc::string input_type_name() const = 0;
+ virtual grpc::string output_type_name() const = 0;
+
+ virtual bool get_module_and_message_path_input(
+ grpc::string *str, grpc::string generator_file_name,
+ bool generate_in_pb2_grpc, grpc::string import_prefix) const = 0;
+ virtual bool get_module_and_message_path_output(
+ grpc::string *str, grpc::string generator_file_name,
+ bool generate_in_pb2_grpc, grpc::string import_prefix) const = 0;
+
+ virtual std::vector<grpc::string> get_input_namespace_parts() const = 0;
+ virtual grpc::string get_input_type_name() const = 0;
+ virtual std::vector<grpc::string> get_output_namespace_parts() const = 0;
+ virtual grpc::string get_output_type_name() const = 0;
+
+ virtual grpc::string get_fb_builder() const = 0;
+
+ virtual bool NoStreaming() const = 0;
+ virtual bool ClientStreaming() const = 0;
+ virtual bool ServerStreaming() const = 0;
+ virtual bool BidiStreaming() const = 0;
+};
+
+// An abstract interface representing a service.
+struct Service : public CommentHolder {
+ virtual ~Service() {}
+
+ virtual std::vector<grpc::string> namespace_parts() const = 0;
+ virtual grpc::string name() const = 0;
+ virtual bool is_internal() const = 0;
+
+ virtual int method_count() const = 0;
+ virtual std::unique_ptr<const Method> method(int i) const = 0;
+};
+
+struct Printer {
+ virtual ~Printer() {}
+
+ virtual void Print(const std::map<grpc::string, grpc::string> &vars,
+ const char *template_string) = 0;
+ virtual void Print(const char *string) = 0;
+ virtual void SetIndentationSize(const int size) = 0;
+ virtual void Indent() = 0;
+ virtual void Outdent() = 0;
+};
+
+// An interface that allows the source generated to be output using various
+// libraries/idls/serializers.
+struct File : public CommentHolder {
+ virtual ~File() {}
+
+ virtual grpc::string filename() const = 0;
+ virtual grpc::string filename_without_ext() const = 0;
+ virtual grpc::string package() const = 0;
+ virtual std::vector<grpc::string> package_parts() const = 0;
+ virtual grpc::string additional_headers() const = 0;
+
+ virtual int service_count() const = 0;
+ virtual std::unique_ptr<const Service> service(int i) const = 0;
+
+ virtual std::unique_ptr<Printer> CreatePrinter(
+ grpc::string *str, const char indentation_type = ' ') const = 0;
+};
+} // namespace grpc_generator
+
+#endif // GRPC_INTERNAL_COMPILER_SCHEMA_INTERFACE_H
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.cc
new file mode 100644
index 0000000000..403a803ef1
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.cc
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * NOTE: The following implementation is a translation for the Swift-grpc
+ * generator since flatbuffers doesnt allow plugins for now. if an issue arises
+ * please open an issue in the flatbuffers repository. This file should always
+ * be maintained according to the Swift-grpc repository
+ */
+#include <map>
+#include <sstream>
+
+#include "flatbuffers/util.h"
+#include "src/compiler/schema_interface.h"
+#include "src/compiler/swift_generator.h"
+
+namespace grpc_swift_generator {
+
+std::string WrapInNameSpace(const std::vector<std::string> &components,
+ const grpc::string &name) {
+ std::string qualified_name;
+ for (auto it = components.begin(); it != components.end(); ++it)
+ qualified_name += *it + "_";
+ return qualified_name + name;
+}
+
+grpc::string GenerateMessage(const std::vector<std::string> &components,
+ const grpc::string &name) {
+ return "Message<" + WrapInNameSpace(components, name) + ">";
+}
+
+// MARK: - Client
+
+void GenerateClientFuncName(const grpc_generator::Method *method,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ if (method->NoStreaming()) {
+ printer->Print(vars,
+ " $GenAccess$func $MethodName$(\n"
+ " _ request: $Input$\n"
+ " , callOptions: CallOptions?$isNil$\n"
+ " ) -> UnaryCall<$Input$, $Output$>");
+ return;
+ }
+
+ if (method->ServerStreaming()) {
+ printer->Print(vars,
+ " $GenAccess$func $MethodName$(\n"
+ " _ request: $Input$\n"
+ " , callOptions: CallOptions?$isNil$,\n"
+ " handler: @escaping ($Output$) -> Void\n"
+ " ) -> ServerStreamingCall<$Input$, $Output$>");
+ return;
+ }
+
+ if (method->ClientStreaming()) {
+ printer->Print(vars,
+ " $GenAccess$func $MethodName$(\n"
+ " callOptions: CallOptions?$isNil$\n"
+ " ) -> ClientStreamingCall<$Input$, $Output$>");
+ return;
+ }
+
+ printer->Print(vars,
+ " $GenAccess$func $MethodName$(\n"
+ " callOptions: CallOptions?$isNil$,\n"
+ " handler: @escaping ($Output$ ) -> Void\n"
+ " ) -> BidirectionalStreamingCall<$Input$, $Output$>");
+}
+
+void GenerateClientFuncBody(const grpc_generator::Method *method,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ vars["Interceptor"] =
+ "interceptors: self.interceptors?.make$MethodName$Interceptors() ?? []";
+ if (method->NoStreaming()) {
+ printer->Print(
+ vars,
+ " return self.makeUnaryCall(\n"
+ " path: \"/$PATH$$ServiceName$/$MethodName$\",\n"
+ " request: request,\n"
+ " callOptions: callOptions ?? self.defaultCallOptions,\n"
+ " $Interceptor$\n"
+ " )\n");
+ return;
+ }
+
+ if (method->ServerStreaming()) {
+ printer->Print(
+ vars,
+ " return self.makeServerStreamingCall(\n"
+ " path: \"/$PATH$$ServiceName$/$MethodName$\",\n"
+ " request: request,\n"
+ " callOptions: callOptions ?? self.defaultCallOptions,\n"
+ " $Interceptor$,\n"
+ " handler: handler\n"
+ " )\n");
+ return;
+ }
+
+ if (method->ClientStreaming()) {
+ printer->Print(
+ vars,
+ " return self.makeClientStreamingCall(\n"
+ " path: \"/$PATH$$ServiceName$/$MethodName$\",\n"
+ " callOptions: callOptions ?? self.defaultCallOptions,\n"
+ " $Interceptor$\n"
+ " )\n");
+ return;
+ }
+ printer->Print(vars,
+ " return self.makeBidirectionalStreamingCall(\n"
+ " path: \"/$PATH$$ServiceName$/$MethodName$\",\n"
+ " callOptions: callOptions ?? self.defaultCallOptions,\n"
+ " $Interceptor$,\n"
+ " handler: handler\n"
+ " )\n");
+}
+
+void GenerateClientProtocol(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(
+ vars,
+ "$ACCESS$ protocol $ServiceQualifiedName$ClientProtocol: GRPCClient {");
+ printer->Print("\n\n");
+ printer->Print(" var serviceName: String { get }");
+ printer->Print("\n\n");
+ printer->Print(
+ vars,
+ " var interceptors: "
+ "$ServiceQualifiedName$ClientInterceptorFactoryProtocol? { get }");
+ printer->Print("\n\n");
+
+ vars["GenAccess"] = "";
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ vars["isNil"] = "";
+ GenerateClientFuncName(method.get(), &*printer, &vars);
+ printer->Print("\n\n");
+ }
+ printer->Print("}\n\n");
+
+ printer->Print(vars, "extension $ServiceQualifiedName$ClientProtocol {");
+ printer->Print("\n\n");
+ printer->Print(vars,
+ " $ACCESS$ var serviceName: String { "
+ "\"$PATH$$ServiceName$\" }\n");
+
+ vars["GenAccess"] = service->is_internal() ? "internal " : "public ";
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ vars["isNil"] = " = nil";
+ printer->Print("\n");
+ GenerateClientFuncName(method.get(), &*printer, &vars);
+ printer->Print(" {\n");
+ GenerateClientFuncBody(method.get(), &*printer, &vars);
+ printer->Print(" }\n");
+ }
+ printer->Print("}\n\n");
+
+ printer->Print(vars,
+ "$ACCESS$ protocol "
+ "$ServiceQualifiedName$ClientInterceptorFactoryProtocol {\n");
+
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ printer->Print(
+ vars,
+ " /// - Returns: Interceptors to use when invoking '$MethodName$'.\n");
+ printer->Print(vars,
+ " func make$MethodName$Interceptors() -> "
+ "[ClientInterceptor<$Input$, $Output$>]\n\n");
+ }
+ printer->Print("}\n\n");
+}
+
+void GenerateClientClass(grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars,
+ "$ACCESS$ final class $ServiceQualifiedName$ServiceClient: "
+ "$ServiceQualifiedName$ClientProtocol {\n");
+ printer->Print(vars, " $ACCESS$ let channel: GRPCChannel\n");
+ printer->Print(vars, " $ACCESS$ var defaultCallOptions: CallOptions\n");
+ printer->Print(vars,
+ " $ACCESS$ var interceptors: "
+ "$ServiceQualifiedName$ClientInterceptorFactoryProtocol?\n");
+ printer->Print("\n");
+ printer->Print(
+ vars,
+ " $ACCESS$ init(\n"
+ " channel: GRPCChannel,\n"
+ " defaultCallOptions: CallOptions = CallOptions(),\n"
+ " interceptors: "
+ "$ServiceQualifiedName$ClientInterceptorFactoryProtocol? = nil\n"
+ " ) {\n");
+ printer->Print(" self.channel = channel\n");
+ printer->Print(" self.defaultCallOptions = defaultCallOptions\n");
+ printer->Print(" self.interceptors = interceptors\n");
+ printer->Print(" }");
+ printer->Print("\n");
+ printer->Print("}\n");
+}
+
+// MARK: - Server
+
+grpc::string GenerateServerFuncName(const grpc_generator::Method *method) {
+ if (method->NoStreaming()) {
+ return "func $MethodName$(request: $Input$"
+ ", context: StatusOnlyCallContext) -> EventLoopFuture<$Output$>";
+ }
+
+ if (method->ClientStreaming()) {
+ return "func $MethodName$(context: UnaryResponseCallContext<$Output$>) -> "
+ "EventLoopFuture<(StreamEvent<$Input$"
+ ">) -> Void>";
+ }
+
+ if (method->ServerStreaming()) {
+ return "func $MethodName$(request: $Input$"
+ ", context: StreamingResponseCallContext<$Output$>) -> "
+ "EventLoopFuture<GRPCStatus>";
+ }
+ return "func $MethodName$(context: StreamingResponseCallContext<$Output$>) "
+ "-> EventLoopFuture<(StreamEvent<$Input$>) -> Void>";
+}
+
+grpc::string GenerateServerExtensionBody(const grpc_generator::Method *method) {
+ grpc::string start = " case \"$MethodName$\":\n ";
+ grpc::string interceptors =
+ " interceptors: self.interceptors?.make$MethodName$Interceptors() "
+ "?? [],\n";
+ if (method->NoStreaming()) {
+ return start +
+ "return UnaryServerHandler(\n"
+ " context: context,\n"
+ " requestDeserializer: GRPCPayloadDeserializer<$Input$>(),\n"
+ " responseSerializer: GRPCPayloadSerializer<$Output$>(),\n" +
+ interceptors +
+ " userFunction: self.$MethodName$(request:context:))\n";
+ }
+ if (method->ServerStreaming()) {
+ return start +
+ "return ServerStreamingServerHandler(\n"
+ " context: context,\n"
+ " requestDeserializer: GRPCPayloadDeserializer<$Input$>(),\n"
+ " responseSerializer: GRPCPayloadSerializer<$Output$>(),\n" +
+ interceptors +
+ " userFunction: self.$MethodName$(request:context:))\n";
+ }
+ if (method->ClientStreaming()) {
+ return start +
+ "return ClientStreamingServerHandler(\n"
+ " context: context,\n"
+ " requestDeserializer: GRPCPayloadDeserializer<$Input$>(),\n"
+ " responseSerializer: GRPCPayloadSerializer<$Output$>(),\n" +
+ interceptors +
+ " observerFactory: self.$MethodName$(context:))\n";
+ }
+ if (method->BidiStreaming()) {
+ return start +
+ "return BidirectionalStreamingServerHandler(\n"
+ " context: context,\n"
+ " requestDeserializer: GRPCPayloadDeserializer<$Input$>(),\n"
+ " responseSerializer: GRPCPayloadSerializer<$Output$>(),\n" +
+ interceptors +
+ " observerFactory: self.$MethodName$(context:))\n";
+ }
+ return "";
+}
+
+void GenerateServerProtocol(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars,
+ "$ACCESS$ protocol $ServiceQualifiedName$Provider: "
+ "CallHandlerProvider {\n");
+ printer->Print(
+ vars,
+ " var interceptors: "
+ "$ServiceQualifiedName$ServerInterceptorFactoryProtocol? { get }\n");
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ printer->Print(" ");
+ auto func = GenerateServerFuncName(method.get());
+ printer->Print(vars, func.c_str());
+ printer->Print("\n");
+ }
+ printer->Print("}\n\n");
+
+ printer->Print(vars, "$ACCESS$ extension $ServiceQualifiedName$Provider {\n");
+ printer->Print("\n");
+ printer->Print(vars,
+ " var serviceName: Substring { return "
+ "\"$PATH$$ServiceName$\" }\n");
+ printer->Print("\n");
+ printer->Print(
+ " func handle(method name: Substring, context: "
+ "CallHandlerContext) -> GRPCServerHandlerProtocol? {\n");
+ printer->Print(" switch name {\n");
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ auto body = GenerateServerExtensionBody(method.get());
+ printer->Print(vars, body.c_str());
+ printer->Print("\n");
+ }
+ printer->Print(" default: return nil;\n");
+ printer->Print(" }\n");
+ printer->Print(" }\n\n");
+ printer->Print("}\n\n");
+
+ printer->Print(vars,
+ "$ACCESS$ protocol "
+ "$ServiceQualifiedName$ServerInterceptorFactoryProtocol {\n");
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Input"] = GenerateMessage(method->get_input_namespace_parts(),
+ method->get_input_type_name());
+ vars["Output"] = GenerateMessage(method->get_output_namespace_parts(),
+ method->get_output_type_name());
+ vars["MethodName"] = method->name();
+ printer->Print(
+ vars,
+ " /// - Returns: Interceptors to use when handling '$MethodName$'.\n"
+ " /// Defaults to calling `self.makeInterceptors()`.\n");
+ printer->Print(vars,
+ " func make$MethodName$Interceptors() -> "
+ "[ServerInterceptor<$Input$, $Output$>]\n\n");
+ }
+ printer->Print("}");
+}
+
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service) {
+ grpc::string output;
+ std::map<grpc::string, grpc::string> vars;
+ vars["PATH"] = file->package();
+ if (!file->package().empty()) { vars["PATH"].append("."); }
+ vars["ServiceQualifiedName"] =
+ WrapInNameSpace(service->namespace_parts(), service->name());
+ vars["ServiceName"] = service->name();
+ vars["ACCESS"] = service->is_internal() ? "internal" : "public";
+ auto printer = file->CreatePrinter(&output);
+ printer->Print(
+ vars,
+ "/// Usage: instantiate $ServiceQualifiedName$ServiceClient, then call "
+ "methods of this protocol to make API calls.\n");
+ GenerateClientProtocol(service, &*printer, &vars);
+ GenerateClientClass(&*printer, &vars);
+ printer->Print("\n");
+ GenerateServerProtocol(service, &*printer, &vars);
+ return output;
+}
+
+grpc::string GenerateHeader() {
+ grpc::string code;
+ code +=
+ "/// The following code is generated by the Flatbuffers library which "
+ "might not be in sync with grpc-swift\n";
+ code +=
+ "/// in case of an issue please open github issue, though it would be "
+ "maintained\n";
+ code += "\n";
+ code += "// swiftlint:disable all\n";
+ code += "// swiftformat:disable all\n";
+ code += "\n";
+ code += "import Foundation\n";
+ code += "import GRPC\n";
+ code += "import NIO\n";
+ code += "import NIOHTTP1\n";
+ code += "import FlatBuffers\n";
+ code += "\n";
+ code +=
+ "public protocol GRPCFlatBufPayload: GRPCPayload, FlatBufferGRPCMessage "
+ "{}\n";
+
+ code += "public extension GRPCFlatBufPayload {\n";
+ code += " init(serializedByteBuffer: inout NIO.ByteBuffer) throws {\n";
+ code +=
+ " self.init(byteBuffer: FlatBuffers.ByteBuffer(contiguousBytes: "
+ "serializedByteBuffer.readableBytesView, count: "
+ "serializedByteBuffer.readableBytes))\n";
+ code += " }\n";
+
+ code += " func serialize(into buffer: inout NIO.ByteBuffer) throws {\n";
+ code +=
+ " let buf = UnsafeRawBufferPointer(start: self.rawPointer, count: "
+ "Int(self.size))\n";
+ code += " buffer.writeBytes(buf)\n";
+ code += " }\n";
+ code += "}\n";
+ code += "extension Message: GRPCFlatBufPayload {}\n";
+ return code;
+}
+} // namespace grpc_swift_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.h
new file mode 100644
index 0000000000..1639cb07c8
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/swift_generator.h
@@ -0,0 +1,55 @@
+/*
+ *
+ * Copyright 2020, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include <memory>
+#include <vector>
+
+#include "src/compiler/config.h"
+#include "src/compiler/schema_interface.h"
+
+#ifndef GRPC_CUSTOM_STRING
+# include <string>
+# define GRPC_CUSTOM_STRING std::string
+#endif
+
+namespace grpc {
+
+typedef GRPC_CUSTOM_STRING string;
+
+} // namespace grpc
+
+namespace grpc_swift_generator {
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service);
+grpc::string GenerateHeader();
+} // namespace grpc_swift_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.cc b/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.cc
new file mode 100644
index 0000000000..e49fd8d925
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.cc
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * NOTE: The following implementation is a translation for the Swift-grpc
+ * generator since flatbuffers doesnt allow plugins for now. if an issue arises
+ * please open an issue in the flatbuffers repository. This file should always
+ * be maintained according to the Swift-grpc repository
+ */
+
+#include <map>
+#include <sstream>
+
+#include "flatbuffers/util.h"
+#include "src/compiler/schema_interface.h"
+#include "src/compiler/ts_generator.h"
+
+namespace grpc_ts_generator {
+
+grpc::string ToDasherizedCase(const grpc::string pascal_case) {
+ std::string dasherized_case;
+ char p = 0;
+ for (size_t i = 0; i < pascal_case.length(); i++) {
+ char const &c = pascal_case[i];
+ if (flatbuffers::is_alpha_upper(c)) {
+ if (i > 0 && p != flatbuffers::kPathSeparator) dasherized_case += "-";
+ dasherized_case += flatbuffers::CharToLower(c);
+ } else {
+ dasherized_case += c;
+ }
+ p = c;
+ }
+ return dasherized_case;
+}
+
+grpc::string GenerateNamespace(const std::vector<std::string> namepsace,
+ const std::string filename,
+ const bool include_separator) {
+ grpc::string path = "";
+ if (include_separator) path += ".";
+
+ for (auto it = namepsace.begin(); it < namepsace.end(); it++) {
+ if (include_separator) path += "/";
+ path += include_separator ? ToDasherizedCase(*it) : *it + "_";
+ }
+
+ if (include_separator) path += "/";
+ path += include_separator ? ToDasherizedCase(filename) : filename;
+ return path;
+}
+
+// MARK: - Shared code
+
+void GenerateImports(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary,
+ const bool grpc_var_import) {
+ auto vars = *dictonary;
+ printer->Print(
+ "// Generated GRPC code for FlatBuffers TS *** DO NOT EDIT ***\n");
+ printer->Print("import * as flatbuffers from 'flatbuffers';\n");
+
+ std::set<grpc::string> generated_imports;
+
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ auto output = method->get_output_type_name();
+ auto input = method->get_input_type_name();
+ auto input_namespace = method->get_input_namespace_parts();
+
+ vars["OUTPUT"] = output;
+ vars["INPUT"] = input;
+
+ if (generated_imports.find(output) == generated_imports.end()) {
+ generated_imports.insert(output);
+ vars["OUTPUT_DIR"] =
+ GenerateNamespace(method->get_output_namespace_parts(), output, true);
+ vars["Output_alias"] = GenerateNamespace(
+ method->get_output_namespace_parts(), output, false);
+ printer->Print(
+ vars, "import { $OUTPUT$ as $Output_alias$ } from '$OUTPUT_DIR$';\n");
+ }
+ if (generated_imports.find(input) == generated_imports.end()) {
+ generated_imports.insert(input);
+ vars["INPUT_DIR"] =
+ GenerateNamespace(method->get_output_namespace_parts(), input, true);
+ vars["Input_alias"] =
+ GenerateNamespace(method->get_output_namespace_parts(), input, false);
+ printer->Print(
+ vars, "import { $INPUT$ as $Input_alias$ } from '$INPUT_DIR$';\n");
+ }
+ }
+ printer->Print("\n");
+ if (grpc_var_import)
+ printer->Print("var grpc = require('grpc');\n");
+ else
+ printer->Print("import * as grpc from 'grpc';\n");
+ printer->Print("\n");
+}
+
+// MARK: - Generate Main GRPC Code
+
+void GetStreamType(grpc_generator::Printer *printer,
+ const grpc_generator::Method *method,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ auto client_streaming = method->ClientStreaming() || method->BidiStreaming();
+ auto server_streaming = method->ServerStreaming() || method->BidiStreaming();
+ vars["ClientStreaming"] = client_streaming ? "true" : "false";
+ vars["ServerStreaming"] = server_streaming ? "true" : "false";
+ printer->Print(vars, "requestStream: $ClientStreaming$,\n");
+ printer->Print(vars, "responseStream: $ServerStreaming$,\n");
+}
+
+void GenerateSerializeMethod(grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "function serialize_$Type$(buffer_args) {\n");
+ printer->Indent();
+ printer->Print(vars, "if (!(buffer_args instanceof $Type$)) {\n");
+ printer->Indent();
+ printer->Print(vars,
+ "throw new Error('Expected argument of type $VALUE$');\n");
+ printer->Outdent();
+ printer->Print("}\n");
+ printer->Print(vars, "return buffer_args.serialize();\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+}
+
+void GenerateDeserializeMethod(
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "function deserialize_$Type$(buffer) {\n");
+ printer->Indent();
+ printer->Print(vars,
+ "return $Type$.getRootAs$VALUE$(new "
+ "flatbuffers.ByteBuffer(buffer))\n");
+ printer->Outdent();
+ printer->Print("}\n\n");
+}
+
+void GenerateMethods(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+
+ std::set<grpc::string> generated_functions;
+
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ auto output = method->get_output_type_name();
+ auto input = method->get_input_type_name();
+
+ if (generated_functions.find(output) == generated_functions.end()) {
+ generated_functions.insert(output);
+ vars["VALUE"] = output;
+ vars["Type"] = GenerateNamespace(method->get_output_namespace_parts(),
+ output, false);
+ GenerateSerializeMethod(printer, &vars);
+ GenerateDeserializeMethod(printer, &vars);
+ }
+ printer->Print("\n");
+ if (generated_functions.find(input) == generated_functions.end()) {
+ generated_functions.insert(input);
+ vars["VALUE"] = input;
+ vars["Type"] =
+ GenerateNamespace(method->get_input_namespace_parts(), input, false);
+ GenerateSerializeMethod(printer, &vars);
+ GenerateDeserializeMethod(printer, &vars);
+ }
+ }
+}
+
+void GenerateService(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ vars["NAME"] = service->name() + "Service";
+
+ printer->Print(vars, "var $NAME$ = exports.$NAME$ = {\n");
+ printer->Indent();
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["MethodName"] = method->name();
+ vars["OUTPUT"] = GenerateNamespace(method->get_output_namespace_parts(),
+ method->get_output_type_name(), false);
+ vars["INPUT"] = GenerateNamespace(method->get_input_namespace_parts(),
+ method->get_input_type_name(), false);
+ printer->Print(vars, "$MethodName$: {\n");
+ printer->Indent();
+ printer->Print(vars, "path: '/$PATH$$ServiceName$/$MethodName$',\n");
+ GetStreamType(printer, &*method, &vars);
+ printer->Print(vars, "requestType: flatbuffers.ByteBuffer,\n");
+ printer->Print(vars, "responseType: $OUTPUT$,\n");
+ printer->Print(vars, "requestSerialize: serialize_$INPUT$,\n");
+ printer->Print(vars, "requestDeserialize: deserialize_$INPUT$,\n");
+ printer->Print(vars, "responseSerialize: serialize_$OUTPUT$,\n");
+ printer->Print(vars, "responseDeserialize: deserialize_$OUTPUT$,\n");
+ printer->Outdent();
+ printer->Print("},\n");
+ }
+ printer->Outdent();
+ printer->Print("};\n");
+ printer->Print(vars,
+ "exports.$ServiceName$Client = "
+ "grpc.makeGenericClientConstructor($NAME$);");
+}
+
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ const grpc::string &filename) {
+ grpc::string output;
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["PATH"] = file->package();
+
+ if (!file->package().empty()) { vars["PATH"].append("."); }
+
+ vars["ServiceName"] = service->name();
+ vars["FBSFile"] = service->name() + "_fbs";
+ vars["Filename"] = filename;
+ auto printer = file->CreatePrinter(&output);
+
+ GenerateImports(service, &*printer, &vars, true);
+ GenerateMethods(service, &*printer, &vars);
+ GenerateService(service, &*printer, &vars);
+ return output;
+}
+
+// MARK: - Generate Interface
+
+void FillInterface(grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars,
+ "interface I$ServiceName$Service_I$MethodName$ extends "
+ "grpc.MethodDefinition<$INPUT$, $OUTPUT$> {\n");
+ printer->Indent();
+ printer->Print(vars, "path: string; // /$PATH$$ServiceName$/$MethodName$\n");
+ printer->Print(vars, "requestStream: boolean; // $ClientStreaming$\n");
+ printer->Print(vars, "responseStream: boolean; // $ServerStreaming$\n");
+ printer->Print(vars, "requestSerialize: grpc.serialize<$INPUT$>;\n");
+ printer->Print(vars, "requestDeserialize: grpc.deserialize<$INPUT$>;\n");
+ printer->Print(vars, "responseSerialize: grpc.serialize<$OUTPUT$>;\n");
+ printer->Print(vars, "responseDeserialize: grpc.deserialize<$OUTPUT$>;\n");
+ printer->Outdent();
+ printer->Print("}\n");
+}
+
+void GenerateInterfaces(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ auto client_streaming =
+ method->ClientStreaming() || method->BidiStreaming();
+ auto server_streaming =
+ method->ServerStreaming() || method->BidiStreaming();
+ vars["ClientStreaming"] = client_streaming ? "true" : "false";
+ vars["ServerStreaming"] = server_streaming ? "true" : "false";
+ vars["MethodName"] = method->name();
+ vars["OUTPUT"] = GenerateNamespace(method->get_output_namespace_parts(),
+ method->get_output_type_name(), false);
+ vars["INPUT"] = GenerateNamespace(method->get_input_namespace_parts(),
+ method->get_input_type_name(), false);
+ FillInterface(printer, &vars);
+ printer->Print("\n");
+ }
+}
+
+void GenerateExportedInterface(
+ const grpc_generator::Service *service, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "export interface I$ServiceName$Server {\n");
+ printer->Indent();
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["Name"] = method->name();
+ vars["OUTPUT"] = GenerateNamespace(method->get_output_namespace_parts(),
+ method->get_output_type_name(), false);
+ vars["INPUT"] = GenerateNamespace(method->get_input_namespace_parts(),
+ method->get_input_type_name(), false);
+ if (method->BidiStreaming()) {
+ printer->Print(vars,
+ "$Name$: grpc.handleBidiStreamingCall<$INPUT$, "
+ "$OUTPUT$>;\n");
+ continue;
+ }
+ if (method->NoStreaming()) {
+ printer->Print(vars,
+ "$Name$: grpc.handleUnaryCall<$INPUT$, "
+ "$OUTPUT$>;\n");
+ continue;
+ }
+ if (method->ClientStreaming()) {
+ printer->Print(vars,
+ "$Name$: grpc.handleClientStreamingCall<$INPUT$, "
+ "$OUTPUT$>;\n");
+ continue;
+ }
+ if (method->ServerStreaming()) {
+ printer->Print(vars,
+ "$Name$: grpc.handleServerStreamingCall<$INPUT$, "
+ "$OUTPUT$>;\n");
+ continue;
+ }
+ }
+ printer->Outdent();
+ printer->Print("}\n");
+}
+
+void GenerateMainInterface(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(
+ vars,
+ "interface I$ServiceName$Service extends "
+ "grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {\n");
+ printer->Indent();
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["MethodName"] = method->name();
+ printer->Print(vars,
+ "$MethodName$: I$ServiceName$Service_I$MethodName$;\n");
+ }
+ printer->Outdent();
+ printer->Print("}\n");
+ GenerateInterfaces(service, printer, &vars);
+ printer->Print("\n");
+ printer->Print(vars,
+ "export const $ServiceName$Service: I$ServiceName$Service;\n");
+ printer->Print("\n");
+ GenerateExportedInterface(service, printer, &vars);
+}
+
+grpc::string GenerateMetaData() { return "metadata: grpc.Metadata"; }
+
+grpc::string GenerateOptions() { return "options: Partial<grpc.CallOptions>"; }
+
+void GenerateUnaryClientInterface(
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ grpc::string main = "$ISPUBLIC$$MethodName$(request: $INPUT$, ";
+ grpc::string callback =
+ "callback: (error: grpc.ServiceError | null, response: "
+ "$OUTPUT$) => void): grpc.ClientUnaryCall;\n";
+ auto meta_data = GenerateMetaData() + ", ";
+ auto options = GenerateOptions() + ", ";
+ printer->Print(vars, (main + callback).c_str());
+ printer->Print(vars, (main + meta_data + callback).c_str());
+ printer->Print(vars, (main + meta_data + options + callback).c_str());
+}
+
+void GenerateClientWriteStreamInterface(
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ grpc::string main = "$ISPUBLIC$$MethodName$(";
+ grpc::string callback =
+ "callback: (error: grpc.ServiceError | null, response: "
+ "$INPUT$) => void): "
+ "grpc.ClientWritableStream<$OUTPUT$>;\n";
+ auto meta_data = GenerateMetaData() + ", ";
+ auto options = GenerateOptions() + ", ";
+ printer->Print(vars, (main + callback).c_str());
+ printer->Print(vars, (main + meta_data + callback).c_str());
+ printer->Print(vars, (main + options + callback).c_str());
+ printer->Print(vars, (main + meta_data + options + callback).c_str());
+}
+
+void GenerateClientReadableStreamInterface(
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ grpc::string main = "$ISPUBLIC$$MethodName$(request: $INPUT$, ";
+ grpc::string end_function = "): grpc.ClientReadableStream<$OUTPUT$>;\n";
+ auto meta_data = GenerateMetaData();
+ auto options = GenerateOptions();
+ printer->Print(vars, (main + meta_data + end_function).c_str());
+ printer->Print(vars, (main + options + end_function).c_str());
+}
+
+void GenerateDepluxStreamInterface(
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ grpc::string main = "$ISPUBLIC$$MethodName$(";
+ grpc::string end_function =
+ "): grpc.ClientDuplexStream<$INPUT$, $OUTPUT$>;\n";
+ auto meta_data = GenerateMetaData();
+ auto options = GenerateOptions();
+ printer->Print(vars, (main + end_function).c_str());
+ printer->Print(vars, (main + options + end_function).c_str());
+ printer->Print(vars, (main + meta_data +
+ ", options?: Partial<grpc.CallOptions>" + end_function)
+ .c_str());
+}
+
+void GenerateClientInterface(const grpc_generator::Service *service,
+ grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars, "export interface I$ServiceName$Client {\n");
+ printer->Indent();
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["MethodName"] = method->name();
+ vars["OUTPUT"] = GenerateNamespace(method->get_output_namespace_parts(),
+ method->get_output_type_name(), false);
+ vars["INPUT"] = GenerateNamespace(method->get_input_namespace_parts(),
+ method->get_input_type_name(), false);
+ vars["ISPUBLIC"] = "";
+
+ if (method->NoStreaming()) {
+ GenerateUnaryClientInterface(printer, &vars);
+ continue;
+ }
+ if (method->BidiStreaming()) {
+ GenerateDepluxStreamInterface(printer, &vars);
+ continue;
+ }
+
+ if (method->ClientStreaming()) {
+ GenerateClientWriteStreamInterface(printer, &vars);
+ continue;
+ }
+
+ if (method->ServerStreaming()) {
+ GenerateClientReadableStreamInterface(printer, &vars);
+ continue;
+ }
+ }
+ printer->Outdent();
+ printer->Print("}\n");
+}
+
+void GenerateClientClassInterface(
+ const grpc_generator::Service *service, grpc_generator::Printer *printer,
+ std::map<grpc::string, grpc::string> *dictonary) {
+ auto vars = *dictonary;
+ printer->Print(vars,
+ "export class $ServiceName$Client extends grpc.Client "
+ "implements I$ServiceName$Client {\n");
+ printer->Indent();
+ printer->Print(
+ "constructor(address: string, credentials: grpc.ChannelCredentials, "
+ "options?: object);");
+ for (auto it = 0; it < service->method_count(); it++) {
+ auto method = service->method(it);
+ vars["MethodName"] = method->name();
+ vars["OUTPUT"] = GenerateNamespace(method->get_output_namespace_parts(),
+ method->get_output_type_name(), false);
+ vars["INPUT"] = GenerateNamespace(method->get_input_namespace_parts(),
+ method->get_input_type_name(), false);
+ vars["ISPUBLIC"] = "public ";
+ if (method->NoStreaming()) {
+ GenerateUnaryClientInterface(printer, &vars);
+ continue;
+ }
+ if (method->BidiStreaming()) {
+ GenerateDepluxStreamInterface(printer, &vars);
+ continue;
+ }
+
+ if (method->ClientStreaming()) {
+ GenerateClientWriteStreamInterface(printer, &vars);
+ continue;
+ }
+
+ if (method->ServerStreaming()) {
+ GenerateClientReadableStreamInterface(printer, &vars);
+ continue;
+ }
+ }
+ printer->Outdent();
+ printer->Print("}\n");
+}
+
+grpc::string GenerateInterface(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ const grpc::string &filename) {
+ grpc::string output;
+
+ std::set<grpc::string> generated_functions;
+ std::map<grpc::string, grpc::string> vars;
+
+ vars["PATH"] = file->package();
+
+ if (!file->package().empty()) { vars["PATH"].append("."); }
+
+ vars["ServiceName"] = service->name();
+ vars["FBSFile"] = service->name() + "_fbs";
+ vars["Filename"] = filename;
+ auto printer = file->CreatePrinter(&output);
+
+ GenerateImports(service, &*printer, &vars, false);
+ GenerateMainInterface(service, &*printer, &vars);
+ printer->Print("\n");
+ GenerateClientInterface(service, &*printer, &vars);
+ printer->Print("\n");
+ GenerateClientClassInterface(service, &*printer, &vars);
+ return output;
+}
+} // namespace grpc_ts_generator
diff --git a/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.h b/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.h
new file mode 100644
index 0000000000..a33bb3c5d2
--- /dev/null
+++ b/contrib/libs/flatbuffers/grpc/src/compiler/ts_generator.h
@@ -0,0 +1,61 @@
+/*
+ *
+ * Copyright 2020, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include <memory>
+#include <vector>
+#include <set>
+
+#include "src/compiler/config.h"
+#include "src/compiler/schema_interface.h"
+
+#ifndef GRPC_CUSTOM_STRING
+# include <string>
+# define GRPC_CUSTOM_STRING std::string
+#endif
+
+namespace grpc {
+
+typedef GRPC_CUSTOM_STRING string;
+
+} // namespace grpc
+
+namespace grpc_ts_generator {
+grpc::string Generate(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ const grpc::string &filename);
+
+grpc::string GenerateInterface(grpc_generator::File *file,
+ const grpc_generator::Service *service,
+ const grpc::string &filename);
+} // namespace grpc_ts_generator
+
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/code_generators.h b/contrib/libs/flatbuffers/include/flatbuffers/code_generators.h
new file mode 100644
index 0000000000..09b773a468
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/code_generators.h
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_CODE_GENERATORS_H_
+#define FLATBUFFERS_CODE_GENERATORS_H_
+
+#include <map>
+#include <sstream>
+
+#include "idl.h"
+
+namespace flatbuffers {
+
+// Utility class to assist in generating code through use of text templates.
+//
+// Example code:
+// CodeWriter code("\t");
+// code.SetValue("NAME", "Foo");
+// code += "void {{NAME}}() { printf("%s", "{{NAME}}"); }";
+// code.SetValue("NAME", "Bar");
+// code += "void {{NAME}}() { printf("%s", "{{NAME}}"); }";
+// std::cout << code.ToString() << std::endl;
+//
+// Output:
+// void Foo() { printf("%s", "Foo"); }
+// void Bar() { printf("%s", "Bar"); }
+class CodeWriter {
+ public:
+ CodeWriter(std::string pad = std::string())
+ : pad_(pad), cur_ident_lvl_(0), ignore_ident_(false) {}
+
+ // Clears the current "written" code.
+ void Clear() {
+ stream_.str("");
+ stream_.clear();
+ }
+
+ // Associates a key with a value. All subsequent calls to operator+=, where
+ // the specified key is contained in {{ and }} delimiters will be replaced by
+ // the given value.
+ void SetValue(const std::string &key, const std::string &value) {
+ value_map_[key] = value;
+ }
+
+ std::string GetValue(const std::string &key) const {
+ const auto it = value_map_.find(key);
+ return it == value_map_.end() ? "" : it->second;
+ }
+
+ // Appends the given text to the generated code as well as a newline
+ // character. Any text within {{ and }} delimiters is replaced by values
+ // previously stored in the CodeWriter by calling SetValue above. The newline
+ // will be suppressed if the text ends with the \\ character.
+ void operator+=(std::string text);
+
+ // Returns the current contents of the CodeWriter as a std::string.
+ std::string ToString() const { return stream_.str(); }
+
+ // Increase ident level for writing code
+ void IncrementIdentLevel() { cur_ident_lvl_++; }
+ // Decrease ident level for writing code
+ void DecrementIdentLevel() {
+ if (cur_ident_lvl_) cur_ident_lvl_--;
+ }
+
+ void SetPadding(const std::string &padding) { pad_ = padding; }
+
+ private:
+ std::map<std::string, std::string> value_map_;
+ std::stringstream stream_;
+ std::string pad_;
+ int cur_ident_lvl_;
+ bool ignore_ident_;
+
+ // Add ident padding (tab or space) based on ident level
+ void AppendIdent(std::stringstream &stream);
+};
+
+class BaseGenerator {
+ public:
+ virtual bool generate() = 0;
+
+ static std::string NamespaceDir(const Parser &parser, const std::string &path,
+ const Namespace &ns,
+ const bool dasherize = false);
+
+ static std::string ToDasherizedCase(const std::string pascal_case);
+
+ std::string GeneratedFileName(const std::string &path,
+ const std::string &file_name,
+ const IDLOptions &options) const;
+
+ protected:
+ BaseGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name, std::string qualifying_start,
+ std::string qualifying_separator, std::string default_extension)
+ : parser_(parser),
+ path_(path),
+ file_name_(file_name),
+ qualifying_start_(qualifying_start),
+ qualifying_separator_(qualifying_separator),
+ default_extension_(default_extension) {}
+ virtual ~BaseGenerator() {}
+
+ // No copy/assign.
+ BaseGenerator &operator=(const BaseGenerator &);
+ BaseGenerator(const BaseGenerator &);
+
+ std::string NamespaceDir(const Namespace &ns,
+ const bool dasherize = false) const;
+
+ static const char *FlatBuffersGeneratedWarning();
+
+ static std::string FullNamespace(const char *separator, const Namespace &ns);
+
+ static std::string LastNamespacePart(const Namespace &ns);
+
+ // tracks the current namespace for early exit in WrapInNameSpace
+ // c++, java and csharp returns a different namespace from
+ // the following default (no early exit, always fully qualify),
+ // which works for js and php
+ virtual const Namespace *CurrentNameSpace() const { return nullptr; }
+
+ // Ensure that a type is prefixed with its namespace even within
+ // its own namespace to avoid conflict between generated method
+ // names and similarly named classes or structs
+ std::string WrapInNameSpace(const Namespace *ns,
+ const std::string &name) const;
+
+ std::string WrapInNameSpace(const Definition &def) const;
+
+ std::string GetNameSpace(const Definition &def) const;
+
+ const Parser &parser_;
+ const std::string &path_;
+ const std::string &file_name_;
+ const std::string qualifying_start_;
+ const std::string qualifying_separator_;
+ const std::string default_extension_;
+};
+
+struct CommentConfig {
+ const char *first_line;
+ const char *content_line_prefix;
+ const char *last_line;
+};
+
+extern void GenComment(const std::vector<std::string> &dc,
+ std::string *code_ptr, const CommentConfig *config,
+ const char *prefix = "");
+
+class FloatConstantGenerator {
+ public:
+ virtual ~FloatConstantGenerator() {}
+ std::string GenFloatConstant(const FieldDef &field) const;
+
+ private:
+ virtual std::string Value(double v, const std::string &src) const = 0;
+ virtual std::string Inf(double v) const = 0;
+ virtual std::string NaN(double v) const = 0;
+
+ virtual std::string Value(float v, const std::string &src) const = 0;
+ virtual std::string Inf(float v) const = 0;
+ virtual std::string NaN(float v) const = 0;
+
+ template<typename T>
+ std::string GenFloatConstantImpl(const FieldDef &field) const;
+};
+
+class SimpleFloatConstantGenerator : public FloatConstantGenerator {
+ public:
+ SimpleFloatConstantGenerator(const char *nan_number,
+ const char *pos_inf_number,
+ const char *neg_inf_number);
+
+ private:
+ std::string Value(double v,
+ const std::string &src) const FLATBUFFERS_OVERRIDE;
+ std::string Inf(double v) const FLATBUFFERS_OVERRIDE;
+ std::string NaN(double v) const FLATBUFFERS_OVERRIDE;
+
+ std::string Value(float v, const std::string &src) const FLATBUFFERS_OVERRIDE;
+ std::string Inf(float v) const FLATBUFFERS_OVERRIDE;
+ std::string NaN(float v) const FLATBUFFERS_OVERRIDE;
+
+ const std::string nan_number_;
+ const std::string pos_inf_number_;
+ const std::string neg_inf_number_;
+};
+
+// C++, C#, Java like generator.
+class TypedFloatConstantGenerator : public FloatConstantGenerator {
+ public:
+ TypedFloatConstantGenerator(const char *double_prefix,
+ const char *single_prefix, const char *nan_number,
+ const char *pos_inf_number,
+ const char *neg_inf_number = "");
+
+ private:
+ std::string Value(double v,
+ const std::string &src) const FLATBUFFERS_OVERRIDE;
+ std::string Inf(double v) const FLATBUFFERS_OVERRIDE;
+
+ std::string NaN(double v) const FLATBUFFERS_OVERRIDE;
+
+ std::string Value(float v, const std::string &src) const FLATBUFFERS_OVERRIDE;
+ std::string Inf(float v) const FLATBUFFERS_OVERRIDE;
+ std::string NaN(float v) const FLATBUFFERS_OVERRIDE;
+
+ std::string MakeNaN(const std::string &prefix) const;
+ std::string MakeInf(bool neg, const std::string &prefix) const;
+
+ const std::string double_prefix_;
+ const std::string single_prefix_;
+ const std::string nan_number_;
+ const std::string pos_inf_number_;
+ const std::string neg_inf_number_;
+};
+
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_CODE_GENERATORS_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/flatbuffers_iter.h b/contrib/libs/flatbuffers/include/flatbuffers/flatbuffers_iter.h
new file mode 100644
index 0000000000..a770983dca
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/flatbuffers_iter.h
@@ -0,0 +1,640 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_ITER_H_
+#define FLATBUFFERS_ITER_H_
+
+#include "flatbuffers.h"
+#include <optional>
+
+/// @file
+namespace yandex {
+namespace maps {
+namespace flatbuffers_iter {
+
+#define FLATBUFFERS_FILE_IDENTIFIER_LENGTH 4
+
+using flatbuffers::uoffset_t;
+using flatbuffers::soffset_t;
+using flatbuffers::voffset_t;
+using flatbuffers::EndianScalar;
+
+// Wrapper for uoffset_t to allow safe template specialization.
+template<typename T> struct Offset {
+ uoffset_t o;
+ Offset() : o(0) {}
+ Offset(uoffset_t _o) : o(_o) {}
+ Offset<void> Union() const { return Offset<void>(o); }
+};
+
+template<typename Iter>
+inline bool hasContiguous(const Iter& spot, uoffset_t length)
+{
+ return spot.hasContiguous(length);
+}
+
+inline bool hasContiguous(const uint8_t* /* spot */, uoffset_t /* length */)
+{
+ return true;
+}
+
+template <typename Iter>
+inline const uint8_t* getRawPointer(const Iter& spot)
+{
+ return spot.rawPointer();
+}
+
+inline const uint8_t* getRawPointer(const uint8_t* spot)
+{
+ return spot;
+}
+
+template<typename T, typename Iter>
+typename std::enable_if<sizeof(T) == 1, T>::type extractValue(const Iter& spot)
+{
+ typename std::remove_cv<T>::type ret;
+ std::memcpy(&ret, getRawPointer(spot), 1);
+ return ret;
+}
+
+template<typename T, typename Iter>
+typename std::enable_if<sizeof(T) != 1, T>::type extractValue(const Iter& spot)
+{
+ if (hasContiguous(spot, sizeof(T))) {
+ typename std::remove_cv<T>::type ret;
+ std::memcpy(&ret, getRawPointer(spot), sizeof(T));
+ return ret;
+ }
+ Iter itr = spot;
+ alignas(T) uint8_t buf[sizeof(T)];
+ for (std::size_t i = 0; i < sizeof(T); ++i) {
+ buf[i] = *itr;
+ ++itr;
+ }
+ return *reinterpret_cast<T*>(buf);
+}
+
+template<typename T, typename Iter> T ReadScalar(Iter p) {
+ return EndianScalar(extractValue<T>(p));
+}
+
+// When we read serialized data from memory, in the case of most scalars,
+// we want to just read T, but in the case of Offset, we want to actually
+// perform the indirection and return a pointer.
+// The template specialization below does just that.
+// It is wrapped in a struct since function templates can't overload on the
+// return type like this.
+// The typedef is for the convenience of callers of this function
+// (avoiding the need for a trailing return decltype)
+template<typename T> struct IndirectHelper {
+ typedef T return_type;
+ typedef T mutable_return_type;
+ static const size_t element_stride = sizeof(T);
+ template<typename Iter>
+ static return_type Read(const Iter& p, uoffset_t i) {
+ return i ? EndianScalar(extractValue<return_type>(p+sizeof(return_type)*i)) : EndianScalar(extractValue<return_type>(p));
+ }
+};
+template<typename T> struct IndirectHelper<Offset<T>> {
+ typedef std::optional<T> return_type;
+ typedef std::optional<T> mutable_return_type;
+ static const size_t element_stride = sizeof(uoffset_t);
+ template<typename Iter>
+ static return_type Read(Iter p, uoffset_t i) {
+ p += i * sizeof(uoffset_t);
+ return return_type(T(p + ReadScalar<uoffset_t>(p)));
+ }
+};
+template<typename T> struct IndirectHelper<const T *> {
+};
+
+
+// An STL compatible iterator implementation for Vector below, effectively
+// calling Get() for every element.
+template<typename T, typename IT, typename Iter>
+struct VectorIterator
+ : public std::iterator<std::random_access_iterator_tag, IT, uoffset_t> {
+
+ typedef std::iterator<std::random_access_iterator_tag, IT, uoffset_t> super_type;
+
+public:
+ VectorIterator(const Iter& data, uoffset_t i) :
+ data_(data + IndirectHelper<T>::element_stride * i) {}
+ VectorIterator(const VectorIterator &other) : data_(other.data_) {}
+ #ifndef FLATBUFFERS_CPP98_STL
+ VectorIterator(VectorIterator &&other) : data_(std::move(other.data_)) {}
+ #endif
+
+ VectorIterator &operator=(const VectorIterator &other) {
+ data_ = other.data_;
+ return *this;
+ }
+
+ VectorIterator &operator=(VectorIterator &&other) {
+ data_ = other.data_;
+ return *this;
+ }
+
+ bool operator==(const VectorIterator &other) const {
+ return data_ == other.data_;
+ }
+
+ bool operator!=(const VectorIterator &other) const {
+ return data_ != other.data_;
+ }
+
+ ptrdiff_t operator-(const VectorIterator &other) const {
+ return (data_ - other.data_) / IndirectHelper<T>::element_stride;
+ }
+
+ typename super_type::value_type operator *() const {
+ return IndirectHelper<T>::Read(data_, 0);
+ }
+
+ typename super_type::value_type operator->() const {
+ return IndirectHelper<T>::Read(data_, 0);
+ }
+
+ VectorIterator &operator++() {
+ data_ += IndirectHelper<T>::element_stride;
+ return *this;
+ }
+
+ VectorIterator operator++(int) {
+ VectorIterator temp(data_, 0);
+ data_ += IndirectHelper<T>::element_stride;
+ return temp;
+ }
+
+ VectorIterator operator+(const uoffset_t &offset) {
+ return VectorIterator(data_ + offset * IndirectHelper<T>::element_stride, 0);
+ }
+
+ VectorIterator& operator+=(const uoffset_t &offset) {
+ data_ += offset * IndirectHelper<T>::element_stride;
+ return *this;
+ }
+
+ VectorIterator &operator--() {
+ data_ -= IndirectHelper<T>::element_stride;
+ return *this;
+ }
+
+ VectorIterator operator--(int) {
+ VectorIterator temp(data_, 0);
+ data_ -= IndirectHelper<T>::element_stride;
+ return temp;
+ }
+
+ VectorIterator operator-(const uoffset_t &offset) {
+ return VectorIterator(data_ - offset * IndirectHelper<T>::element_stride, 0);
+ }
+
+ VectorIterator& operator-=(const uoffset_t &offset) {
+ data_ -= offset * IndirectHelper<T>::element_stride;
+ return *this;
+ }
+
+private:
+ Iter data_;
+};
+
+// This is used as a helper type for accessing vectors.
+// Vector::data() assumes the vector elements start after the length field.
+template<typename T, typename Iter> class Vector {
+public:
+ typedef VectorIterator<T, typename IndirectHelper<T>::mutable_return_type, Iter>
+ iterator;
+ typedef VectorIterator<T, typename IndirectHelper<T>::return_type, Iter>
+ const_iterator;
+
+ Vector(Iter data):
+ data_(data)
+ {}
+
+ uoffset_t size() const { return EndianScalar(extractValue<uoffset_t>(data_)); }
+
+ // Deprecated: use size(). Here for backwards compatibility.
+ uoffset_t Length() const { return size(); }
+
+ typedef typename IndirectHelper<T>::return_type return_type;
+ typedef typename IndirectHelper<T>::mutable_return_type mutable_return_type;
+
+ return_type Get(uoffset_t i) const {
+ assert(i < size());
+ return IndirectHelper<T>::Read(Data(), i);
+ }
+
+ return_type operator[](uoffset_t i) const { return Get(i); }
+
+ // If this is a Vector of enums, T will be its storage type, not the enum
+ // type. This function makes it convenient to retrieve value with enum
+ // type E.
+ template<typename E> E GetEnum(uoffset_t i) const {
+ return static_cast<E>(Get(i));
+ }
+
+ const Iter GetStructFromOffset(size_t o) const {
+ return Data() + o;
+ }
+
+ iterator begin() { return iterator(Data(), 0); }
+ const_iterator begin() const { return const_iterator(Data(), 0); }
+
+ iterator end() { return iterator(Data(), size()); }
+ const_iterator end() const { return const_iterator(Data(), size()); }
+
+ // The raw data in little endian format. Use with care.
+ const Iter Data() const {
+ return data_ + sizeof(uoffset_t);
+ }
+
+ Iter Data() {
+ return data_ + sizeof(uoffset_t);
+ }
+
+ template<typename K> return_type LookupByKey(K key) const {
+ auto search_result = std::lower_bound(begin(), end(), key, KeyCompare<K>);
+
+ if (search_result == end() || (*search_result)->KeyCompareWithValue(key) != 0) {
+ return std::nullopt; // Key not found.
+ }
+
+ return *search_result;
+ }
+
+ operator Iter() const
+ {
+ return data_;
+ }
+
+protected:
+ Iter data_;
+
+private:
+ template<typename K> static int KeyCompare(const return_type& ap, const K& bp) {
+ return ap->KeyCompareWithValue(bp) < 0;
+ }
+};
+
+// Represent a vector much like the template above, but in this case we
+// don't know what the element types are (used with reflection.h).
+template <typename Iter>
+class VectorOfAny {
+public:
+ VectorOfAny(Iter data):
+ data_(data)
+ {}
+
+ uoffset_t size() const { return EndianScalar(extractValue<uoffset_t>(data_)); }
+
+ const Iter Data() const {
+ return data_;
+ }
+ Iter Data() {
+ return data_;
+ }
+protected:
+
+ Iter data_;
+};
+
+// Convenient helper function to get the length of any vector, regardless
+// of wether it is null or not (the field is not set).
+template<typename T, typename Iter> static inline size_t VectorLength(const std::optional<Vector<T, Iter>> &v) {
+ return v ? v->Length() : 0;
+}
+
+template <typename Iter> struct String : public Vector<char, Iter> {
+ using Vector<char,Iter>::Vector;
+ using Vector<char,Iter>::data_;
+
+ std::string str() const {
+ if (hasContiguous(data_, sizeof(uoffset_t) + this->Length()))
+ return std::string(reinterpret_cast<const char*>(getRawPointer(data_)) + sizeof(uoffset_t), this->Length());
+ return std::string(this->begin(), this->begin() + this->Length()); }
+
+ bool operator <(const String &o) const {
+ return str() < o.str();
+ }
+};
+
+// Converts a Field ID to a virtual table offset.
+inline voffset_t FieldIndexToOffset(voffset_t field_id) {
+ // Should correspond to what EndTable() below builds up.
+ const int fixed_fields = 2; // Vtable size and Object Size.
+ return static_cast<voffset_t>((field_id + fixed_fields) * sizeof(voffset_t));
+}
+
+/// @endcond
+
+/// @cond FLATBUFFERS_INTERNAL
+template<typename T, typename Iter> std::optional<T> GetMutableRoot(Iter begin) {
+ flatbuffers::EndianCheck();
+ return T(begin + EndianScalar(extractValue<uoffset_t>(begin)));
+}
+
+template<typename T, typename Iter> std::optional<T> GetRoot(Iter begin) {
+ return GetMutableRoot<T, Iter>(begin);
+}
+
+template<typename T, typename Iter> std::optional<T> GetSizePrefixedRoot(Iter buf) {
+ return GetRoot<T, Iter>(buf + sizeof(uoffset_t));
+}
+
+// Helper to see if the identifier in a buffer has the expected value.
+
+template <typename Iter> inline bool BufferHasIdentifier(const Iter& buf, const char *identifier) {
+ return std::equal(
+ identifier,
+ identifier + std::min(std::strlen(identifier) + 1, static_cast<std::size_t>(FLATBUFFERS_FILE_IDENTIFIER_LENGTH)),
+ buf + sizeof(uoffset_t));
+}
+
+// Helper class to verify the integrity of a FlatBuffer
+template <typename Iter>
+class Verifier FLATBUFFERS_FINAL_CLASS {
+ public:
+ Verifier(const Iter& buf, size_t buf_len, size_t _max_depth = 64,
+ size_t _max_tables = 1000000)
+ : buf_(buf), end_(buf + buf_len), depth_(0), max_depth_(_max_depth),
+ num_tables_(0), max_tables_(_max_tables)
+ #ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ , upper_bound_(buf)
+ #endif
+ {}
+
+ // Central location where any verification failures register.
+ bool Check(bool ok) const {
+ #ifdef FLATBUFFERS_DEBUG_VERIFICATION_FAILURE
+ assert(ok);
+ #endif
+ #ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ if (!ok)
+ upper_bound_ = buf_;
+ #endif
+ return ok;
+ }
+
+ // Verify any range within the buffer.
+ bool Verify(const Iter& elem, size_t elem_len) const {
+ #ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ auto upper_bound = elem + elem_len;
+ if (upper_bound_ < upper_bound)
+ upper_bound_ = upper_bound;
+ #endif
+ return Check(elem_len <= (size_t) (end_ - buf_) &&
+ elem >= buf_ &&
+ elem <= end_ - elem_len);
+ }
+
+ // Verify a range indicated by sizeof(T).
+ template<typename T> bool Verify(const Iter& elem) const {
+ return Verify(elem, sizeof(T));
+ }
+
+ template<typename T> bool VerifyTable(const std::optional<T>& table) {
+ return !table || table->Verify(*this);
+ }
+
+ template<typename T> bool Verify(const std::optional<Vector<T, Iter>>& vec) const {
+ Iter end;
+ return !vec ||
+ VerifyVector(static_cast<Iter>(*vec), sizeof(T),
+ &end);
+ }
+
+ template<typename T> bool Verify(const std::optional<Vector<const T, Iter>>& vec) const {
+ return Verify(*reinterpret_cast<const std::optional<Vector<T, Iter>> *>(&vec));
+ }
+
+ bool Verify(const std::optional<String<Iter>>& str) const {
+ Iter end;
+ return !str ||
+ (VerifyVector(static_cast<Iter>(*str), 1, &end) &&
+ Verify(end, 1) && // Must have terminator
+ Check(*end == '\0')); // Terminating byte must be 0.
+ }
+
+ // Common code between vectors and strings.
+ bool VerifyVector(const Iter& vec, size_t elem_size,
+ Iter *end) const {
+ // Check we can read the size field.
+ if (!Verify<uoffset_t>(vec)) return false;
+ // Check the whole array. If this is a string, the byte past the array
+ // must be 0.
+ auto size = ReadScalar<uoffset_t>(vec);
+ auto max_elems = FLATBUFFERS_MAX_BUFFER_SIZE / elem_size;
+ if (!Check(size < max_elems))
+ return false; // Protect against byte_size overflowing.
+ auto byte_size = sizeof(size) + elem_size * size;
+ *end = vec + byte_size;
+ return Verify(vec, byte_size);
+ }
+
+ // Special case for string contents, after the above has been called.
+ bool VerifyVectorOfStrings(const std::optional<Vector<Offset<String<Iter>>, Iter>>& vec) const {
+ if (vec) {
+ for (uoffset_t i = 0; i < vec->size(); i++) {
+ if (!Verify(vec->Get(i))) return false;
+ }
+ }
+ return true;
+ }
+
+ // Special case for table contents, after the above has been called.
+ template<typename T> bool VerifyVectorOfTables(const std::optional<Vector<Offset<T>, Iter>>& vec) {
+ if (vec) {
+ for (uoffset_t i = 0; i < vec->size(); i++) {
+ if (!vec->Get(i)->Verify(*this)) return false;
+ }
+ }
+ return true;
+ }
+
+ template<typename T> bool VerifyBufferFromStart(const char *identifier,
+ const Iter& start) {
+ if (identifier &&
+ (static_cast<std::size_t>(end_ - start) < 2 * sizeof(flatbuffers_iter::uoffset_t) ||
+ !BufferHasIdentifier(start, identifier))) {
+ return false;
+ }
+
+ // Call T::Verify, which must be in the generated code for this type.
+ return Verify<uoffset_t>(start) &&
+ T(start + ReadScalar<uoffset_t>(start)).
+ Verify(*this)
+ #ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ && GetComputedSize()
+ #endif
+ ;
+ }
+
+ // Verify this whole buffer, starting with root type T.
+ template<typename T> bool VerifyBuffer(const char *identifier) {
+ return VerifyBufferFromStart<T>(identifier, buf_);
+ }
+
+ template<typename T> bool VerifySizePrefixedBuffer(const char *identifier) {
+ return Verify<uoffset_t>(buf_) &&
+ ReadScalar<uoffset_t>(buf_) == end_ - buf_ - sizeof(uoffset_t) &&
+ VerifyBufferFromStart<T>(identifier, buf_ + sizeof(uoffset_t));
+ }
+
+ // Called at the start of a table to increase counters measuring data
+ // structure depth and amount, and possibly bails out with false if
+ // limits set by the constructor have been hit. Needs to be balanced
+ // with EndTable().
+ bool VerifyComplexity() {
+ depth_++;
+ num_tables_++;
+ return Check(depth_ <= max_depth_ && num_tables_ <= max_tables_);
+ }
+
+ // Called at the end of a table to pop the depth count.
+ bool EndTable() {
+ depth_--;
+ return true;
+ }
+
+ #ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ // Returns the message size in bytes
+ size_t GetComputedSize() const {
+ uintptr_t size = upper_bound_ - buf_;
+ // Align the size to uoffset_t
+ size = (size - 1 + sizeof(uoffset_t)) & ~(sizeof(uoffset_t) - 1);
+ return (buf_ + size > end_) ? 0 : size;
+ }
+ #endif
+
+ private:
+ const Iter buf_;
+ const Iter end_;
+ size_t depth_;
+ size_t max_depth_;
+ size_t num_tables_;
+ size_t max_tables_;
+#ifdef FLATBUFFERS_TRACK_VERIFIER_BUFFER_SIZE
+ mutable const Iter upper_bound_;
+#endif
+};
+
+// "structs" are flat structures that do not have an offset table, thus
+// always have all members present and do not support forwards/backwards
+// compatible extensions.
+template <typename Iter>
+class Struct FLATBUFFERS_FINAL_CLASS {
+ public:
+ template<typename T> T GetField(uoffset_t o) const {
+ return ReadScalar<T>(data_ + o);
+ }
+
+ template<typename T> T GetStruct(uoffset_t o) const {
+ return T(data_ + o);
+ }
+
+ private:
+ Iter data_;
+};
+
+// "tables" use an offset table (possibly shared) that allows fields to be
+// omitted and added at will, but uses an extra indirection to read.
+template<typename Iter>
+class Table {
+ public:
+ Table(Iter data): data_(data) {}
+
+ const Iter GetVTable() const {
+ return data_ - ReadScalar<soffset_t>(data_);
+ }
+
+ // This gets the field offset for any of the functions below it, or 0
+ // if the field was not present.
+ voffset_t GetOptionalFieldOffset(voffset_t field) const {
+ // The vtable offset is always at the start.
+ auto vtable = GetVTable();
+ // The first element is the size of the vtable (fields + type id + itself).
+ auto vtsize = ReadScalar<voffset_t>(vtable);
+ // If the field we're accessing is outside the vtable, we're reading older
+ // data, so it's the same as if the offset was 0 (not present).
+ return field < vtsize ? ReadScalar<voffset_t>(vtable + field) : 0;
+ }
+
+ template<typename T> T GetField(voffset_t field, T defaultval) const {
+ auto field_offset = GetOptionalFieldOffset(field);
+ return field_offset ? ReadScalar<T>(data_ + field_offset) : defaultval;
+ }
+
+ template<typename P> std::optional<P> GetPointer(voffset_t field) {
+ auto field_offset = GetOptionalFieldOffset(field);
+ auto p = data_ + field_offset;
+ return field_offset ? std::optional<P>(P(p + ReadScalar<uoffset_t>(p))) : std::nullopt;
+ }
+
+ template<typename P> std::optional<P> GetPointer(voffset_t field) const {
+ return const_cast<Table *>(this)->template GetPointer<P>(field);
+ }
+
+ template<typename P> P GetStruct(voffset_t field) const {
+ auto field_offset = GetOptionalFieldOffset(field);
+ auto p = data_ + field_offset;
+ return extractValue<P>(p);
+ }
+
+ bool CheckField(voffset_t field) const {
+ return GetOptionalFieldOffset(field) != 0;
+ }
+
+ // Verify the vtable of this table.
+ // Call this once per table, followed by VerifyField once per field.
+ bool VerifyTableStart(Verifier<Iter> &verifier) const {
+ // Check the vtable offset.
+ if (!verifier.template Verify<soffset_t>(data_)) return false;
+ auto vtable = GetVTable();
+ // Check the vtable size field, then check vtable fits in its entirety.
+ return verifier.VerifyComplexity() &&
+ verifier.template Verify<voffset_t>(vtable) &&
+ (ReadScalar<voffset_t>(vtable) & (sizeof(voffset_t) - 1)) == 0 &&
+ verifier.Verify(vtable, ReadScalar<voffset_t>(vtable));
+ }
+
+ // Verify a particular field.
+ template<typename T> bool VerifyField(const Verifier<Iter> &verifier,
+ voffset_t field) const {
+ // Calling GetOptionalFieldOffset should be safe now thanks to
+ // VerifyTable().
+ auto field_offset = GetOptionalFieldOffset(field);
+ // Check the actual field.
+ return !field_offset || verifier.template Verify<T>(data_ + field_offset);
+ }
+
+ // VerifyField for required fields.
+ template<typename T> bool VerifyFieldRequired(const Verifier<Iter> &verifier,
+ voffset_t field) const {
+ auto field_offset = GetOptionalFieldOffset(field);
+ return verifier.Check(field_offset != 0) &&
+ verifier.template Verify<T>(data_ + field_offset);
+ }
+
+ private:
+ Iter data_;
+};
+/// @endcond
+} // namespace flatbuffers_iter
+} // namespace maps
+} // namespace yandex
+
+#endif // FLATBUFFERS_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/flatc.h b/contrib/libs/flatbuffers/include/flatbuffers/flatc.h
new file mode 100644
index 0000000000..1466b3651d
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/flatc.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_FLATC_H_
+#define FLATBUFFERS_FLATC_H_
+
+#include <functional>
+#include <limits>
+#include <string>
+
+#include "flatbuffers.h"
+#include "idl.h"
+#include "util.h"
+
+namespace flatbuffers {
+
+extern void LogCompilerWarn(const std::string &warn);
+extern void LogCompilerError(const std::string &err);
+
+class FlatCompiler {
+ public:
+ // Output generator for the various programming languages and formats we
+ // support.
+ struct Generator {
+ typedef bool (*GenerateFn)(const flatbuffers::Parser &parser,
+ const std::string &path,
+ const std::string &file_name);
+ typedef std::string (*MakeRuleFn)(const flatbuffers::Parser &parser,
+ const std::string &path,
+ const std::string &file_name);
+
+ GenerateFn generate;
+ const char *generator_opt_short;
+ const char *generator_opt_long;
+ const char *lang_name;
+ bool schema_only;
+ GenerateFn generateGRPC;
+ flatbuffers::IDLOptions::Language lang;
+ const char *generator_help;
+ MakeRuleFn make_rule;
+ };
+
+ typedef void (*WarnFn)(const FlatCompiler *flatc, const std::string &warn,
+ bool show_exe_name);
+
+ typedef void (*ErrorFn)(const FlatCompiler *flatc, const std::string &err,
+ bool usage, bool show_exe_name);
+
+ // Parameters required to initialize the FlatCompiler.
+ struct InitParams {
+ InitParams()
+ : generators(nullptr),
+ num_generators(0),
+ warn_fn(nullptr),
+ error_fn(nullptr) {}
+
+ const Generator *generators;
+ size_t num_generators;
+ WarnFn warn_fn;
+ ErrorFn error_fn;
+ };
+
+ explicit FlatCompiler(const InitParams &params) : params_(params) {}
+
+ int Compile(int argc, const char **argv);
+
+ std::string GetUsageString(const char *program_name) const;
+
+ private:
+ void ParseFile(flatbuffers::Parser &parser, const std::string &filename,
+ const std::string &contents,
+ std::vector<const char *> &include_directories) const;
+
+ void LoadBinarySchema(Parser &parser, const std::string &filename,
+ const std::string &contents);
+
+ void Warn(const std::string &warn, bool show_exe_name = true) const;
+
+ void Error(const std::string &err, bool usage = true,
+ bool show_exe_name = true) const;
+
+ InitParams params_;
+};
+
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_FLATC_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/flexbuffers.h b/contrib/libs/flatbuffers/include/flatbuffers/flexbuffers.h
new file mode 100644
index 0000000000..d855b67731
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/flexbuffers.h
@@ -0,0 +1,1636 @@
+/*
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_FLEXBUFFERS_H_
+#define FLATBUFFERS_FLEXBUFFERS_H_
+
+#include <map>
+// Used to select STL variant.
+#include "base.h"
+// We use the basic binary writing functions from the regular FlatBuffers.
+#include "util.h"
+
+#ifdef _MSC_VER
+# include <intrin.h>
+#endif
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable : 4127) // C4127: conditional expression is constant
+#endif
+
+namespace flexbuffers {
+
+class Reference;
+class Map;
+
+// These are used in the lower 2 bits of a type field to determine the size of
+// the elements (and or size field) of the item pointed to (e.g. vector).
+enum BitWidth {
+ BIT_WIDTH_8 = 0,
+ BIT_WIDTH_16 = 1,
+ BIT_WIDTH_32 = 2,
+ BIT_WIDTH_64 = 3,
+};
+
+// These are used as the upper 6 bits of a type field to indicate the actual
+// type.
+enum Type {
+ FBT_NULL = 0,
+ FBT_INT = 1,
+ FBT_UINT = 2,
+ FBT_FLOAT = 3,
+ // Types above stored inline, types below store an offset.
+ FBT_KEY = 4,
+ FBT_STRING = 5,
+ FBT_INDIRECT_INT = 6,
+ FBT_INDIRECT_UINT = 7,
+ FBT_INDIRECT_FLOAT = 8,
+ FBT_MAP = 9,
+ FBT_VECTOR = 10, // Untyped.
+ FBT_VECTOR_INT = 11, // Typed any size (stores no type table).
+ FBT_VECTOR_UINT = 12,
+ FBT_VECTOR_FLOAT = 13,
+ FBT_VECTOR_KEY = 14,
+ // DEPRECATED, use FBT_VECTOR or FBT_VECTOR_KEY instead.
+ // Read test.cpp/FlexBuffersDeprecatedTest() for details on why.
+ FBT_VECTOR_STRING_DEPRECATED = 15,
+ FBT_VECTOR_INT2 = 16, // Typed tuple (no type table, no size field).
+ FBT_VECTOR_UINT2 = 17,
+ FBT_VECTOR_FLOAT2 = 18,
+ FBT_VECTOR_INT3 = 19, // Typed triple (no type table, no size field).
+ FBT_VECTOR_UINT3 = 20,
+ FBT_VECTOR_FLOAT3 = 21,
+ FBT_VECTOR_INT4 = 22, // Typed quad (no type table, no size field).
+ FBT_VECTOR_UINT4 = 23,
+ FBT_VECTOR_FLOAT4 = 24,
+ FBT_BLOB = 25,
+ FBT_BOOL = 26,
+ FBT_VECTOR_BOOL =
+ 36, // To Allow the same type of conversion of type to vector type
+};
+
+inline bool IsInline(Type t) { return t <= FBT_FLOAT || t == FBT_BOOL; }
+
+inline bool IsTypedVectorElementType(Type t) {
+ return (t >= FBT_INT && t <= FBT_STRING) || t == FBT_BOOL;
+}
+
+inline bool IsTypedVector(Type t) {
+ return (t >= FBT_VECTOR_INT && t <= FBT_VECTOR_STRING_DEPRECATED) ||
+ t == FBT_VECTOR_BOOL;
+}
+
+inline bool IsFixedTypedVector(Type t) {
+ return t >= FBT_VECTOR_INT2 && t <= FBT_VECTOR_FLOAT4;
+}
+
+inline Type ToTypedVector(Type t, size_t fixed_len = 0) {
+ FLATBUFFERS_ASSERT(IsTypedVectorElementType(t));
+ switch (fixed_len) {
+ case 0: return static_cast<Type>(t - FBT_INT + FBT_VECTOR_INT);
+ case 2: return static_cast<Type>(t - FBT_INT + FBT_VECTOR_INT2);
+ case 3: return static_cast<Type>(t - FBT_INT + FBT_VECTOR_INT3);
+ case 4: return static_cast<Type>(t - FBT_INT + FBT_VECTOR_INT4);
+ default: FLATBUFFERS_ASSERT(0); return FBT_NULL;
+ }
+}
+
+inline Type ToTypedVectorElementType(Type t) {
+ FLATBUFFERS_ASSERT(IsTypedVector(t));
+ return static_cast<Type>(t - FBT_VECTOR_INT + FBT_INT);
+}
+
+inline Type ToFixedTypedVectorElementType(Type t, uint8_t *len) {
+ FLATBUFFERS_ASSERT(IsFixedTypedVector(t));
+ auto fixed_type = t - FBT_VECTOR_INT2;
+ *len = static_cast<uint8_t>(fixed_type / 3 +
+ 2); // 3 types each, starting from length 2.
+ return static_cast<Type>(fixed_type % 3 + FBT_INT);
+}
+
+// TODO: implement proper support for 8/16bit floats, or decide not to
+// support them.
+typedef int16_t half;
+typedef int8_t quarter;
+
+// TODO: can we do this without conditionals using intrinsics or inline asm
+// on some platforms? Given branch prediction the method below should be
+// decently quick, but it is the most frequently executed function.
+// We could do an (unaligned) 64-bit read if we ifdef out the platforms for
+// which that doesn't work (or where we'd read into un-owned memory).
+template<typename R, typename T1, typename T2, typename T4, typename T8>
+R ReadSizedScalar(const uint8_t *data, uint8_t byte_width) {
+ return byte_width < 4
+ ? (byte_width < 2
+ ? static_cast<R>(flatbuffers::ReadScalar<T1>(data))
+ : static_cast<R>(flatbuffers::ReadScalar<T2>(data)))
+ : (byte_width < 8
+ ? static_cast<R>(flatbuffers::ReadScalar<T4>(data))
+ : static_cast<R>(flatbuffers::ReadScalar<T8>(data)));
+}
+
+inline int64_t ReadInt64(const uint8_t *data, uint8_t byte_width) {
+ return ReadSizedScalar<int64_t, int8_t, int16_t, int32_t, int64_t>(
+ data, byte_width);
+}
+
+inline uint64_t ReadUInt64(const uint8_t *data, uint8_t byte_width) {
+ // This is the "hottest" function (all offset lookups use this), so worth
+ // optimizing if possible.
+ // TODO: GCC apparently replaces memcpy by a rep movsb, but only if count is a
+ // constant, which here it isn't. Test if memcpy is still faster than
+ // the conditionals in ReadSizedScalar. Can also use inline asm.
+ // clang-format off
+ #if defined(_MSC_VER) && ((defined(_M_X64) && !defined(_M_ARM64EC)) || defined _M_IX86)
+ uint64_t u = 0;
+ __movsb(reinterpret_cast<uint8_t *>(&u),
+ reinterpret_cast<const uint8_t *>(data), byte_width);
+ return flatbuffers::EndianScalar(u);
+ #else
+ return ReadSizedScalar<uint64_t, uint8_t, uint16_t, uint32_t, uint64_t>(
+ data, byte_width);
+ #endif
+ // clang-format on
+}
+
+inline double ReadDouble(const uint8_t *data, uint8_t byte_width) {
+ return ReadSizedScalar<double, quarter, half, float, double>(data,
+ byte_width);
+}
+
+inline const uint8_t *Indirect(const uint8_t *offset, uint8_t byte_width) {
+ return offset - ReadUInt64(offset, byte_width);
+}
+
+template<typename T> const uint8_t *Indirect(const uint8_t *offset) {
+ return offset - flatbuffers::ReadScalar<T>(offset);
+}
+
+inline BitWidth WidthU(uint64_t u) {
+#define FLATBUFFERS_GET_FIELD_BIT_WIDTH(value, width) \
+ { \
+ if (!((u) & ~((1ULL << (width)) - 1ULL))) return BIT_WIDTH_##width; \
+ }
+ FLATBUFFERS_GET_FIELD_BIT_WIDTH(u, 8);
+ FLATBUFFERS_GET_FIELD_BIT_WIDTH(u, 16);
+ FLATBUFFERS_GET_FIELD_BIT_WIDTH(u, 32);
+#undef FLATBUFFERS_GET_FIELD_BIT_WIDTH
+ return BIT_WIDTH_64;
+}
+
+inline BitWidth WidthI(int64_t i) {
+ auto u = static_cast<uint64_t>(i) << 1;
+ return WidthU(i >= 0 ? u : ~u);
+}
+
+inline BitWidth WidthF(double f) {
+ return static_cast<double>(static_cast<float>(f)) == f ? BIT_WIDTH_32
+ : BIT_WIDTH_64;
+}
+
+// Base class of all types below.
+// Points into the data buffer and allows access to one type.
+class Object {
+ public:
+ Object(const uint8_t *data, uint8_t byte_width)
+ : data_(data), byte_width_(byte_width) {}
+
+ protected:
+ const uint8_t *data_;
+ uint8_t byte_width_;
+};
+
+// Object that has a size, obtained either from size prefix, or elsewhere.
+class Sized : public Object {
+ public:
+ // Size prefix.
+ Sized(const uint8_t *data, uint8_t byte_width)
+ : Object(data, byte_width), size_(read_size()) {}
+ // Manual size.
+ Sized(const uint8_t *data, uint8_t byte_width, size_t sz)
+ : Object(data, byte_width), size_(sz) {}
+ size_t size() const { return size_; }
+ // Access size stored in `byte_width_` bytes before data_ pointer.
+ size_t read_size() const {
+ return static_cast<size_t>(ReadUInt64(data_ - byte_width_, byte_width_));
+ }
+
+ protected:
+ size_t size_;
+};
+
+class String : public Sized {
+ public:
+ // Size prefix.
+ String(const uint8_t *data, uint8_t byte_width) : Sized(data, byte_width) {}
+ // Manual size.
+ String(const uint8_t *data, uint8_t byte_width, size_t sz)
+ : Sized(data, byte_width, sz) {}
+
+ size_t length() const { return size(); }
+ const char *c_str() const { return reinterpret_cast<const char *>(data_); }
+ std::string str() const { return std::string(c_str(), size()); }
+
+ static String EmptyString() {
+ static const char *empty_string = "";
+ return String(reinterpret_cast<const uint8_t *>(empty_string), 1, 0);
+ }
+ bool IsTheEmptyString() const { return data_ == EmptyString().data_; }
+};
+
+class Blob : public Sized {
+ public:
+ Blob(const uint8_t *data_buf, uint8_t byte_width)
+ : Sized(data_buf, byte_width) {}
+
+ static Blob EmptyBlob() {
+ static const uint8_t empty_blob[] = { 0 /*len*/ };
+ return Blob(empty_blob + 1, 1);
+ }
+ bool IsTheEmptyBlob() const { return data_ == EmptyBlob().data_; }
+ const uint8_t *data() const { return data_; }
+};
+
+class Vector : public Sized {
+ public:
+ Vector(const uint8_t *data, uint8_t byte_width) : Sized(data, byte_width) {}
+
+ Reference operator[](size_t i) const;
+
+ static Vector EmptyVector() {
+ static const uint8_t empty_vector[] = { 0 /*len*/ };
+ return Vector(empty_vector + 1, 1);
+ }
+ bool IsTheEmptyVector() const { return data_ == EmptyVector().data_; }
+};
+
+class TypedVector : public Sized {
+ public:
+ TypedVector(const uint8_t *data, uint8_t byte_width, Type element_type)
+ : Sized(data, byte_width), type_(element_type) {}
+
+ Reference operator[](size_t i) const;
+
+ static TypedVector EmptyTypedVector() {
+ static const uint8_t empty_typed_vector[] = { 0 /*len*/ };
+ return TypedVector(empty_typed_vector + 1, 1, FBT_INT);
+ }
+ bool IsTheEmptyVector() const {
+ return data_ == TypedVector::EmptyTypedVector().data_;
+ }
+
+ Type ElementType() { return type_; }
+
+ friend Reference;
+
+ private:
+ Type type_;
+
+ friend Map;
+};
+
+class FixedTypedVector : public Object {
+ public:
+ FixedTypedVector(const uint8_t *data, uint8_t byte_width, Type element_type,
+ uint8_t len)
+ : Object(data, byte_width), type_(element_type), len_(len) {}
+
+ Reference operator[](size_t i) const;
+
+ static FixedTypedVector EmptyFixedTypedVector() {
+ static const uint8_t fixed_empty_vector[] = { 0 /* unused */ };
+ return FixedTypedVector(fixed_empty_vector, 1, FBT_INT, 0);
+ }
+ bool IsTheEmptyFixedTypedVector() const {
+ return data_ == FixedTypedVector::EmptyFixedTypedVector().data_;
+ }
+
+ Type ElementType() { return type_; }
+ uint8_t size() { return len_; }
+
+ private:
+ Type type_;
+ uint8_t len_;
+};
+
+class Map : public Vector {
+ public:
+ Map(const uint8_t *data, uint8_t byte_width) : Vector(data, byte_width) {}
+
+ Reference operator[](const char *key) const;
+ Reference operator[](const std::string &key) const;
+
+ Vector Values() const { return Vector(data_, byte_width_); }
+
+ TypedVector Keys() const {
+ const size_t num_prefixed_fields = 3;
+ auto keys_offset = data_ - byte_width_ * num_prefixed_fields;
+ return TypedVector(Indirect(keys_offset, byte_width_),
+ static_cast<uint8_t>(
+ ReadUInt64(keys_offset + byte_width_, byte_width_)),
+ FBT_KEY);
+ }
+
+ static Map EmptyMap() {
+ static const uint8_t empty_map[] = {
+ 0 /*keys_len*/, 0 /*keys_offset*/, 1 /*keys_width*/, 0 /*len*/
+ };
+ return Map(empty_map + 4, 1);
+ }
+
+ bool IsTheEmptyMap() const { return data_ == EmptyMap().data_; }
+};
+
+template<typename T>
+void AppendToString(std::string &s, T &&v, bool keys_quoted) {
+ s += "[ ";
+ for (size_t i = 0; i < v.size(); i++) {
+ if (i) s += ", ";
+ v[i].ToString(true, keys_quoted, s);
+ }
+ s += " ]";
+}
+
+class Reference {
+ public:
+ Reference()
+ : data_(nullptr),
+ parent_width_(0),
+ byte_width_(BIT_WIDTH_8),
+ type_(FBT_NULL) {}
+
+ Reference(const uint8_t *data, uint8_t parent_width, uint8_t byte_width,
+ Type type)
+ : data_(data),
+ parent_width_(parent_width),
+ byte_width_(byte_width),
+ type_(type) {}
+
+ Reference(const uint8_t *data, uint8_t parent_width, uint8_t packed_type)
+ : data_(data), parent_width_(parent_width) {
+ byte_width_ = 1U << static_cast<BitWidth>(packed_type & 3);
+ type_ = static_cast<Type>(packed_type >> 2);
+ }
+
+ Type GetType() const { return type_; }
+
+ bool IsNull() const { return type_ == FBT_NULL; }
+ bool IsBool() const { return type_ == FBT_BOOL; }
+ bool IsInt() const { return type_ == FBT_INT || type_ == FBT_INDIRECT_INT; }
+ bool IsUInt() const {
+ return type_ == FBT_UINT || type_ == FBT_INDIRECT_UINT;
+ }
+ bool IsIntOrUint() const { return IsInt() || IsUInt(); }
+ bool IsFloat() const {
+ return type_ == FBT_FLOAT || type_ == FBT_INDIRECT_FLOAT;
+ }
+ bool IsNumeric() const { return IsIntOrUint() || IsFloat(); }
+ bool IsString() const { return type_ == FBT_STRING; }
+ bool IsKey() const { return type_ == FBT_KEY; }
+ bool IsVector() const { return type_ == FBT_VECTOR || type_ == FBT_MAP; }
+ bool IsUntypedVector() const { return type_ == FBT_VECTOR; }
+ bool IsTypedVector() const { return flexbuffers::IsTypedVector(type_); }
+ bool IsFixedTypedVector() const {
+ return flexbuffers::IsFixedTypedVector(type_);
+ }
+ bool IsAnyVector() const {
+ return (IsTypedVector() || IsFixedTypedVector() || IsVector());
+ }
+ bool IsMap() const { return type_ == FBT_MAP; }
+ bool IsBlob() const { return type_ == FBT_BLOB; }
+ bool AsBool() const {
+ return (type_ == FBT_BOOL ? ReadUInt64(data_, parent_width_)
+ : AsUInt64()) != 0;
+ }
+
+ // Reads any type as a int64_t. Never fails, does most sensible conversion.
+ // Truncates floats, strings are attempted to be parsed for a number,
+ // vectors/maps return their size. Returns 0 if all else fails.
+ int64_t AsInt64() const {
+ if (type_ == FBT_INT) {
+ // A fast path for the common case.
+ return ReadInt64(data_, parent_width_);
+ } else
+ switch (type_) {
+ case FBT_INDIRECT_INT: return ReadInt64(Indirect(), byte_width_);
+ case FBT_UINT: return ReadUInt64(data_, parent_width_);
+ case FBT_INDIRECT_UINT: return ReadUInt64(Indirect(), byte_width_);
+ case FBT_FLOAT:
+ return static_cast<int64_t>(ReadDouble(data_, parent_width_));
+ case FBT_INDIRECT_FLOAT:
+ return static_cast<int64_t>(ReadDouble(Indirect(), byte_width_));
+ case FBT_NULL: return 0;
+ case FBT_STRING: return flatbuffers::StringToInt(AsString().c_str());
+ case FBT_VECTOR: return static_cast<int64_t>(AsVector().size());
+ case FBT_BOOL: return ReadInt64(data_, parent_width_);
+ default:
+ // Convert other things to int.
+ return 0;
+ }
+ }
+
+ // TODO: could specialize these to not use AsInt64() if that saves
+ // extension ops in generated code, and use a faster op than ReadInt64.
+ int32_t AsInt32() const { return static_cast<int32_t>(AsInt64()); }
+ int16_t AsInt16() const { return static_cast<int16_t>(AsInt64()); }
+ int8_t AsInt8() const { return static_cast<int8_t>(AsInt64()); }
+
+ uint64_t AsUInt64() const {
+ if (type_ == FBT_UINT) {
+ // A fast path for the common case.
+ return ReadUInt64(data_, parent_width_);
+ } else
+ switch (type_) {
+ case FBT_INDIRECT_UINT: return ReadUInt64(Indirect(), byte_width_);
+ case FBT_INT: return ReadInt64(data_, parent_width_);
+ case FBT_INDIRECT_INT: return ReadInt64(Indirect(), byte_width_);
+ case FBT_FLOAT:
+ return static_cast<uint64_t>(ReadDouble(data_, parent_width_));
+ case FBT_INDIRECT_FLOAT:
+ return static_cast<uint64_t>(ReadDouble(Indirect(), byte_width_));
+ case FBT_NULL: return 0;
+ case FBT_STRING: return flatbuffers::StringToUInt(AsString().c_str());
+ case FBT_VECTOR: return static_cast<uint64_t>(AsVector().size());
+ case FBT_BOOL: return ReadUInt64(data_, parent_width_);
+ default:
+ // Convert other things to uint.
+ return 0;
+ }
+ }
+
+ uint32_t AsUInt32() const { return static_cast<uint32_t>(AsUInt64()); }
+ uint16_t AsUInt16() const { return static_cast<uint16_t>(AsUInt64()); }
+ uint8_t AsUInt8() const { return static_cast<uint8_t>(AsUInt64()); }
+
+ double AsDouble() const {
+ if (type_ == FBT_FLOAT) {
+ // A fast path for the common case.
+ return ReadDouble(data_, parent_width_);
+ } else
+ switch (type_) {
+ case FBT_INDIRECT_FLOAT: return ReadDouble(Indirect(), byte_width_);
+ case FBT_INT:
+ return static_cast<double>(ReadInt64(data_, parent_width_));
+ case FBT_UINT:
+ return static_cast<double>(ReadUInt64(data_, parent_width_));
+ case FBT_INDIRECT_INT:
+ return static_cast<double>(ReadInt64(Indirect(), byte_width_));
+ case FBT_INDIRECT_UINT:
+ return static_cast<double>(ReadUInt64(Indirect(), byte_width_));
+ case FBT_NULL: return 0.0;
+ case FBT_STRING: {
+ double d;
+ flatbuffers::StringToNumber(AsString().c_str(), &d);
+ return d;
+ }
+ case FBT_VECTOR: return static_cast<double>(AsVector().size());
+ case FBT_BOOL:
+ return static_cast<double>(ReadUInt64(data_, parent_width_));
+ default:
+ // Convert strings and other things to float.
+ return 0;
+ }
+ }
+
+ float AsFloat() const { return static_cast<float>(AsDouble()); }
+
+ const char *AsKey() const {
+ if (type_ == FBT_KEY || type_ == FBT_STRING) {
+ return reinterpret_cast<const char *>(Indirect());
+ } else {
+ return "";
+ }
+ }
+
+ // This function returns the empty string if you try to read something that
+ // is not a string or key.
+ String AsString() const {
+ if (type_ == FBT_STRING) {
+ return String(Indirect(), byte_width_);
+ } else if (type_ == FBT_KEY) {
+ auto key = Indirect();
+ return String(key, byte_width_,
+ strlen(reinterpret_cast<const char *>(key)));
+ } else {
+ return String::EmptyString();
+ }
+ }
+
+ // Unlike AsString(), this will convert any type to a std::string.
+ std::string ToString() const {
+ std::string s;
+ ToString(false, false, s);
+ return s;
+ }
+
+ // Convert any type to a JSON-like string. strings_quoted determines if
+ // string values at the top level receive "" quotes (inside other values
+ // they always do). keys_quoted determines if keys are quoted, at any level.
+ // TODO(wvo): add further options to have indentation/newlines.
+ void ToString(bool strings_quoted, bool keys_quoted, std::string &s) const {
+ if (type_ == FBT_STRING) {
+ String str(Indirect(), byte_width_);
+ if (strings_quoted) {
+ flatbuffers::EscapeString(str.c_str(), str.length(), &s, true, false);
+ } else {
+ s.append(str.c_str(), str.length());
+ }
+ } else if (IsKey()) {
+ auto str = AsKey();
+ if (keys_quoted) {
+ flatbuffers::EscapeString(str, strlen(str), &s, true, false);
+ } else {
+ s += str;
+ }
+ } else if (IsInt()) {
+ s += flatbuffers::NumToString(AsInt64());
+ } else if (IsUInt()) {
+ s += flatbuffers::NumToString(AsUInt64());
+ } else if (IsFloat()) {
+ s += flatbuffers::NumToString(AsDouble());
+ } else if (IsNull()) {
+ s += "null";
+ } else if (IsBool()) {
+ s += AsBool() ? "true" : "false";
+ } else if (IsMap()) {
+ s += "{ ";
+ auto m = AsMap();
+ auto keys = m.Keys();
+ auto vals = m.Values();
+ for (size_t i = 0; i < keys.size(); i++) {
+ keys[i].ToString(true, keys_quoted, s);
+ s += ": ";
+ vals[i].ToString(true, keys_quoted, s);
+ if (i < keys.size() - 1) s += ", ";
+ }
+ s += " }";
+ } else if (IsVector()) {
+ AppendToString<Vector>(s, AsVector(), keys_quoted);
+ } else if (IsTypedVector()) {
+ AppendToString<TypedVector>(s, AsTypedVector(), keys_quoted);
+ } else if (IsFixedTypedVector()) {
+ AppendToString<FixedTypedVector>(s, AsFixedTypedVector(), keys_quoted);
+ } else if (IsBlob()) {
+ auto blob = AsBlob();
+ flatbuffers::EscapeString(reinterpret_cast<const char *>(blob.data()),
+ blob.size(), &s, true, false);
+ } else {
+ s += "(?)";
+ }
+ }
+
+ // This function returns the empty blob if you try to read a not-blob.
+ // Strings can be viewed as blobs too.
+ Blob AsBlob() const {
+ if (type_ == FBT_BLOB || type_ == FBT_STRING) {
+ return Blob(Indirect(), byte_width_);
+ } else {
+ return Blob::EmptyBlob();
+ }
+ }
+
+ // This function returns the empty vector if you try to read a not-vector.
+ // Maps can be viewed as vectors too.
+ Vector AsVector() const {
+ if (type_ == FBT_VECTOR || type_ == FBT_MAP) {
+ return Vector(Indirect(), byte_width_);
+ } else {
+ return Vector::EmptyVector();
+ }
+ }
+
+ TypedVector AsTypedVector() const {
+ if (IsTypedVector()) {
+ auto tv =
+ TypedVector(Indirect(), byte_width_, ToTypedVectorElementType(type_));
+ if (tv.type_ == FBT_STRING) {
+ // These can't be accessed as strings, since we don't know the bit-width
+ // of the size field, see the declaration of
+ // FBT_VECTOR_STRING_DEPRECATED above for details.
+ // We change the type here to be keys, which are a subtype of strings,
+ // and will ignore the size field. This will truncate strings with
+ // embedded nulls.
+ tv.type_ = FBT_KEY;
+ }
+ return tv;
+ } else {
+ return TypedVector::EmptyTypedVector();
+ }
+ }
+
+ FixedTypedVector AsFixedTypedVector() const {
+ if (IsFixedTypedVector()) {
+ uint8_t len = 0;
+ auto vtype = ToFixedTypedVectorElementType(type_, &len);
+ return FixedTypedVector(Indirect(), byte_width_, vtype, len);
+ } else {
+ return FixedTypedVector::EmptyFixedTypedVector();
+ }
+ }
+
+ Map AsMap() const {
+ if (type_ == FBT_MAP) {
+ return Map(Indirect(), byte_width_);
+ } else {
+ return Map::EmptyMap();
+ }
+ }
+
+ template<typename T> T As() const;
+
+ // Experimental: Mutation functions.
+ // These allow scalars in an already created buffer to be updated in-place.
+ // Since by default scalars are stored in the smallest possible space,
+ // the new value may not fit, in which case these functions return false.
+ // To avoid this, you can construct the values you intend to mutate using
+ // Builder::ForceMinimumBitWidth.
+ bool MutateInt(int64_t i) {
+ if (type_ == FBT_INT) {
+ return Mutate(data_, i, parent_width_, WidthI(i));
+ } else if (type_ == FBT_INDIRECT_INT) {
+ return Mutate(Indirect(), i, byte_width_, WidthI(i));
+ } else if (type_ == FBT_UINT) {
+ auto u = static_cast<uint64_t>(i);
+ return Mutate(data_, u, parent_width_, WidthU(u));
+ } else if (type_ == FBT_INDIRECT_UINT) {
+ auto u = static_cast<uint64_t>(i);
+ return Mutate(Indirect(), u, byte_width_, WidthU(u));
+ } else {
+ return false;
+ }
+ }
+
+ bool MutateBool(bool b) {
+ return type_ == FBT_BOOL && Mutate(data_, b, parent_width_, BIT_WIDTH_8);
+ }
+
+ bool MutateUInt(uint64_t u) {
+ if (type_ == FBT_UINT) {
+ return Mutate(data_, u, parent_width_, WidthU(u));
+ } else if (type_ == FBT_INDIRECT_UINT) {
+ return Mutate(Indirect(), u, byte_width_, WidthU(u));
+ } else if (type_ == FBT_INT) {
+ auto i = static_cast<int64_t>(u);
+ return Mutate(data_, i, parent_width_, WidthI(i));
+ } else if (type_ == FBT_INDIRECT_INT) {
+ auto i = static_cast<int64_t>(u);
+ return Mutate(Indirect(), i, byte_width_, WidthI(i));
+ } else {
+ return false;
+ }
+ }
+
+ bool MutateFloat(float f) {
+ if (type_ == FBT_FLOAT) {
+ return MutateF(data_, f, parent_width_, BIT_WIDTH_32);
+ } else if (type_ == FBT_INDIRECT_FLOAT) {
+ return MutateF(Indirect(), f, byte_width_, BIT_WIDTH_32);
+ } else {
+ return false;
+ }
+ }
+
+ bool MutateFloat(double d) {
+ if (type_ == FBT_FLOAT) {
+ return MutateF(data_, d, parent_width_, WidthF(d));
+ } else if (type_ == FBT_INDIRECT_FLOAT) {
+ return MutateF(Indirect(), d, byte_width_, WidthF(d));
+ } else {
+ return false;
+ }
+ }
+
+ bool MutateString(const char *str, size_t len) {
+ auto s = AsString();
+ if (s.IsTheEmptyString()) return false;
+ // This is very strict, could allow shorter strings, but that creates
+ // garbage.
+ if (s.length() != len) return false;
+ memcpy(const_cast<char *>(s.c_str()), str, len);
+ return true;
+ }
+ bool MutateString(const char *str) { return MutateString(str, strlen(str)); }
+ bool MutateString(const std::string &str) {
+ return MutateString(str.data(), str.length());
+ }
+
+ private:
+ const uint8_t *Indirect() const {
+ return flexbuffers::Indirect(data_, parent_width_);
+ }
+
+ template<typename T>
+ bool Mutate(const uint8_t *dest, T t, size_t byte_width,
+ BitWidth value_width) {
+ auto fits = static_cast<size_t>(static_cast<size_t>(1U) << value_width) <=
+ byte_width;
+ if (fits) {
+ t = flatbuffers::EndianScalar(t);
+ memcpy(const_cast<uint8_t *>(dest), &t, byte_width);
+ }
+ return fits;
+ }
+
+ template<typename T>
+ bool MutateF(const uint8_t *dest, T t, size_t byte_width,
+ BitWidth value_width) {
+ if (byte_width == sizeof(double))
+ return Mutate(dest, static_cast<double>(t), byte_width, value_width);
+ if (byte_width == sizeof(float))
+ return Mutate(dest, static_cast<float>(t), byte_width, value_width);
+ FLATBUFFERS_ASSERT(false);
+ return false;
+ }
+
+ const uint8_t *data_;
+ uint8_t parent_width_;
+ uint8_t byte_width_;
+ Type type_;
+};
+
+// Template specialization for As().
+template<> inline bool Reference::As<bool>() const { return AsBool(); }
+
+template<> inline int8_t Reference::As<int8_t>() const { return AsInt8(); }
+template<> inline int16_t Reference::As<int16_t>() const { return AsInt16(); }
+template<> inline int32_t Reference::As<int32_t>() const { return AsInt32(); }
+template<> inline int64_t Reference::As<int64_t>() const { return AsInt64(); }
+
+template<> inline uint8_t Reference::As<uint8_t>() const { return AsUInt8(); }
+template<> inline uint16_t Reference::As<uint16_t>() const {
+ return AsUInt16();
+}
+template<> inline uint32_t Reference::As<uint32_t>() const {
+ return AsUInt32();
+}
+template<> inline uint64_t Reference::As<uint64_t>() const {
+ return AsUInt64();
+}
+
+template<> inline double Reference::As<double>() const { return AsDouble(); }
+template<> inline float Reference::As<float>() const { return AsFloat(); }
+
+template<> inline String Reference::As<String>() const { return AsString(); }
+template<> inline std::string Reference::As<std::string>() const {
+ return AsString().str();
+}
+
+template<> inline Blob Reference::As<Blob>() const { return AsBlob(); }
+template<> inline Vector Reference::As<Vector>() const { return AsVector(); }
+template<> inline TypedVector Reference::As<TypedVector>() const {
+ return AsTypedVector();
+}
+template<> inline FixedTypedVector Reference::As<FixedTypedVector>() const {
+ return AsFixedTypedVector();
+}
+template<> inline Map Reference::As<Map>() const { return AsMap(); }
+
+inline uint8_t PackedType(BitWidth bit_width, Type type) {
+ return static_cast<uint8_t>(bit_width | (type << 2));
+}
+
+inline uint8_t NullPackedType() { return PackedType(BIT_WIDTH_8, FBT_NULL); }
+
+// Vector accessors.
+// Note: if you try to access outside of bounds, you get a Null value back
+// instead. Normally this would be an assert, but since this is "dynamically
+// typed" data, you may not want that (someone sends you a 2d vector and you
+// wanted 3d).
+// The Null converts seamlessly into a default value for any other type.
+// TODO(wvo): Could introduce an #ifdef that makes this into an assert?
+inline Reference Vector::operator[](size_t i) const {
+ auto len = size();
+ if (i >= len) return Reference(nullptr, 1, NullPackedType());
+ auto packed_type = (data_ + len * byte_width_)[i];
+ auto elem = data_ + i * byte_width_;
+ return Reference(elem, byte_width_, packed_type);
+}
+
+inline Reference TypedVector::operator[](size_t i) const {
+ auto len = size();
+ if (i >= len) return Reference(nullptr, 1, NullPackedType());
+ auto elem = data_ + i * byte_width_;
+ return Reference(elem, byte_width_, 1, type_);
+}
+
+inline Reference FixedTypedVector::operator[](size_t i) const {
+ if (i >= len_) return Reference(nullptr, 1, NullPackedType());
+ auto elem = data_ + i * byte_width_;
+ return Reference(elem, byte_width_, 1, type_);
+}
+
+template<typename T> int KeyCompare(const void *key, const void *elem) {
+ auto str_elem = reinterpret_cast<const char *>(
+ Indirect<T>(reinterpret_cast<const uint8_t *>(elem)));
+ auto skey = reinterpret_cast<const char *>(key);
+ return strcmp(skey, str_elem);
+}
+
+inline Reference Map::operator[](const char *key) const {
+ auto keys = Keys();
+ // We can't pass keys.byte_width_ to the comparison function, so we have
+ // to pick the right one ahead of time.
+ int (*comp)(const void *, const void *) = nullptr;
+ switch (keys.byte_width_) {
+ case 1: comp = KeyCompare<uint8_t>; break;
+ case 2: comp = KeyCompare<uint16_t>; break;
+ case 4: comp = KeyCompare<uint32_t>; break;
+ case 8: comp = KeyCompare<uint64_t>; break;
+ }
+ auto res = std::bsearch(key, keys.data_, keys.size(), keys.byte_width_, comp);
+ if (!res) return Reference(nullptr, 1, NullPackedType());
+ auto i = (reinterpret_cast<uint8_t *>(res) - keys.data_) / keys.byte_width_;
+ return (*static_cast<const Vector *>(this))[i];
+}
+
+inline Reference Map::operator[](const std::string &key) const {
+ return (*this)[key.c_str()];
+}
+
+inline Reference GetRoot(const uint8_t *buffer, size_t size) {
+ // See Finish() below for the serialization counterpart of this.
+ // The root starts at the end of the buffer, so we parse backwards from there.
+ auto end = buffer + size;
+ auto byte_width = *--end;
+ auto packed_type = *--end;
+ end -= byte_width; // The root data item.
+ return Reference(end, byte_width, packed_type);
+}
+
+inline Reference GetRoot(const std::vector<uint8_t> &buffer) {
+ return GetRoot(flatbuffers::vector_data(buffer), buffer.size());
+}
+
+// Flags that configure how the Builder behaves.
+// The "Share" flags determine if the Builder automatically tries to pool
+// this type. Pooling can reduce the size of serialized data if there are
+// multiple maps of the same kind, at the expense of slightly slower
+// serialization (the cost of lookups) and more memory use (std::set).
+// By default this is on for keys, but off for strings.
+// Turn keys off if you have e.g. only one map.
+// Turn strings on if you expect many non-unique string values.
+// Additionally, sharing key vectors can save space if you have maps with
+// identical field populations.
+enum BuilderFlag {
+ BUILDER_FLAG_NONE = 0,
+ BUILDER_FLAG_SHARE_KEYS = 1,
+ BUILDER_FLAG_SHARE_STRINGS = 2,
+ BUILDER_FLAG_SHARE_KEYS_AND_STRINGS = 3,
+ BUILDER_FLAG_SHARE_KEY_VECTORS = 4,
+ BUILDER_FLAG_SHARE_ALL = 7,
+};
+
+class Builder FLATBUFFERS_FINAL_CLASS {
+ public:
+ Builder(size_t initial_size = 256,
+ BuilderFlag flags = BUILDER_FLAG_SHARE_KEYS)
+ : buf_(initial_size),
+ finished_(false),
+ has_duplicate_keys_(false),
+ flags_(flags),
+ force_min_bit_width_(BIT_WIDTH_8),
+ key_pool(KeyOffsetCompare(buf_)),
+ string_pool(StringOffsetCompare(buf_)) {
+ buf_.clear();
+ }
+
+#ifdef FLATBUFFERS_DEFAULT_DECLARATION
+ Builder(Builder &&) = default;
+ Builder &operator=(Builder &&) = default;
+#endif
+
+ /// @brief Get the serialized buffer (after you call `Finish()`).
+ /// @return Returns a vector owned by this class.
+ const std::vector<uint8_t> &GetBuffer() const {
+ Finished();
+ return buf_;
+ }
+
+ // Size of the buffer. Does not include unfinished values.
+ size_t GetSize() const { return buf_.size(); }
+
+ // Reset all state so we can re-use the buffer.
+ void Clear() {
+ buf_.clear();
+ stack_.clear();
+ finished_ = false;
+ // flags_ remains as-is;
+ force_min_bit_width_ = BIT_WIDTH_8;
+ key_pool.clear();
+ string_pool.clear();
+ }
+
+ // All value constructing functions below have two versions: one that
+ // takes a key (for placement inside a map) and one that doesn't (for inside
+ // vectors and elsewhere).
+
+ void Null() { stack_.push_back(Value()); }
+ void Null(const char *key) {
+ Key(key);
+ Null();
+ }
+
+ void Int(int64_t i) { stack_.push_back(Value(i, FBT_INT, WidthI(i))); }
+ void Int(const char *key, int64_t i) {
+ Key(key);
+ Int(i);
+ }
+
+ void UInt(uint64_t u) { stack_.push_back(Value(u, FBT_UINT, WidthU(u))); }
+ void UInt(const char *key, uint64_t u) {
+ Key(key);
+ UInt(u);
+ }
+
+ void Float(float f) { stack_.push_back(Value(f)); }
+ void Float(const char *key, float f) {
+ Key(key);
+ Float(f);
+ }
+
+ void Double(double f) { stack_.push_back(Value(f)); }
+ void Double(const char *key, double d) {
+ Key(key);
+ Double(d);
+ }
+
+ void Bool(bool b) { stack_.push_back(Value(b)); }
+ void Bool(const char *key, bool b) {
+ Key(key);
+ Bool(b);
+ }
+
+ void IndirectInt(int64_t i) { PushIndirect(i, FBT_INDIRECT_INT, WidthI(i)); }
+ void IndirectInt(const char *key, int64_t i) {
+ Key(key);
+ IndirectInt(i);
+ }
+
+ void IndirectUInt(uint64_t u) {
+ PushIndirect(u, FBT_INDIRECT_UINT, WidthU(u));
+ }
+ void IndirectUInt(const char *key, uint64_t u) {
+ Key(key);
+ IndirectUInt(u);
+ }
+
+ void IndirectFloat(float f) {
+ PushIndirect(f, FBT_INDIRECT_FLOAT, BIT_WIDTH_32);
+ }
+ void IndirectFloat(const char *key, float f) {
+ Key(key);
+ IndirectFloat(f);
+ }
+
+ void IndirectDouble(double f) {
+ PushIndirect(f, FBT_INDIRECT_FLOAT, WidthF(f));
+ }
+ void IndirectDouble(const char *key, double d) {
+ Key(key);
+ IndirectDouble(d);
+ }
+
+ size_t Key(const char *str, size_t len) {
+ auto sloc = buf_.size();
+ WriteBytes(str, len + 1);
+ if (flags_ & BUILDER_FLAG_SHARE_KEYS) {
+ auto it = key_pool.find(sloc);
+ if (it != key_pool.end()) {
+ // Already in the buffer. Remove key we just serialized, and use
+ // existing offset instead.
+ buf_.resize(sloc);
+ sloc = *it;
+ } else {
+ key_pool.insert(sloc);
+ }
+ }
+ stack_.push_back(Value(static_cast<uint64_t>(sloc), FBT_KEY, BIT_WIDTH_8));
+ return sloc;
+ }
+
+ size_t Key(const char *str) { return Key(str, strlen(str)); }
+ size_t Key(const std::string &str) { return Key(str.c_str(), str.size()); }
+
+ size_t String(const char *str, size_t len) {
+ auto reset_to = buf_.size();
+ auto sloc = CreateBlob(str, len, 1, FBT_STRING);
+ if (flags_ & BUILDER_FLAG_SHARE_STRINGS) {
+ StringOffset so(sloc, len);
+ auto it = string_pool.find(so);
+ if (it != string_pool.end()) {
+ // Already in the buffer. Remove string we just serialized, and use
+ // existing offset instead.
+ buf_.resize(reset_to);
+ sloc = it->first;
+ stack_.back().u_ = sloc;
+ } else {
+ string_pool.insert(so);
+ }
+ }
+ return sloc;
+ }
+ size_t String(const char *str) { return String(str, strlen(str)); }
+ size_t String(const std::string &str) {
+ return String(str.c_str(), str.size());
+ }
+ void String(const flexbuffers::String &str) {
+ String(str.c_str(), str.length());
+ }
+
+ void String(const char *key, const char *str) {
+ Key(key);
+ String(str);
+ }
+ void String(const char *key, const std::string &str) {
+ Key(key);
+ String(str);
+ }
+ void String(const char *key, const flexbuffers::String &str) {
+ Key(key);
+ String(str);
+ }
+
+ size_t Blob(const void *data, size_t len) {
+ return CreateBlob(data, len, 0, FBT_BLOB);
+ }
+ size_t Blob(const std::vector<uint8_t> &v) {
+ return CreateBlob(flatbuffers::vector_data(v), v.size(), 0, FBT_BLOB);
+ }
+
+ // TODO(wvo): support all the FlexBuffer types (like flexbuffers::String),
+ // e.g. Vector etc. Also in overloaded versions.
+ // Also some FlatBuffers types?
+
+ size_t StartVector() { return stack_.size(); }
+ size_t StartVector(const char *key) {
+ Key(key);
+ return stack_.size();
+ }
+ size_t StartMap() { return stack_.size(); }
+ size_t StartMap(const char *key) {
+ Key(key);
+ return stack_.size();
+ }
+
+ // TODO(wvo): allow this to specify an aligment greater than the natural
+ // alignment.
+ size_t EndVector(size_t start, bool typed, bool fixed) {
+ auto vec = CreateVector(start, stack_.size() - start, 1, typed, fixed);
+ // Remove temp elements and return vector.
+ stack_.resize(start);
+ stack_.push_back(vec);
+ return static_cast<size_t>(vec.u_);
+ }
+
+ size_t EndMap(size_t start) {
+ // We should have interleaved keys and values on the stack.
+ // Make sure it is an even number:
+ auto len = stack_.size() - start;
+ FLATBUFFERS_ASSERT(!(len & 1));
+ len /= 2;
+ // Make sure keys are all strings:
+ for (auto key = start; key < stack_.size(); key += 2) {
+ FLATBUFFERS_ASSERT(stack_[key].type_ == FBT_KEY);
+ }
+ // Now sort values, so later we can do a binary search lookup.
+ // We want to sort 2 array elements at a time.
+ struct TwoValue {
+ Value key;
+ Value val;
+ };
+ // TODO(wvo): strict aliasing?
+ // TODO(wvo): allow the caller to indicate the data is already sorted
+ // for maximum efficiency? With an assert to check sortedness to make sure
+ // we're not breaking binary search.
+ // Or, we can track if the map is sorted as keys are added which would be
+ // be quite cheap (cheaper than checking it here), so we can skip this
+ // step automatically when appliccable, and encourage people to write in
+ // sorted fashion.
+ // std::sort is typically already a lot faster on sorted data though.
+ auto dict =
+ reinterpret_cast<TwoValue *>(flatbuffers::vector_data(stack_) + start);
+ std::sort(dict, dict + len,
+ [&](const TwoValue &a, const TwoValue &b) -> bool {
+ auto as = reinterpret_cast<const char *>(
+ flatbuffers::vector_data(buf_) + a.key.u_);
+ auto bs = reinterpret_cast<const char *>(
+ flatbuffers::vector_data(buf_) + b.key.u_);
+ auto comp = strcmp(as, bs);
+ // We want to disallow duplicate keys, since this results in a
+ // map where values cannot be found.
+ // But we can't assert here (since we don't want to fail on
+ // random JSON input) or have an error mechanism.
+ // Instead, we set has_duplicate_keys_ in the builder to
+ // signal this.
+ // TODO: Have to check for pointer equality, as some sort
+ // implementation apparently call this function with the same
+ // element?? Why?
+ if (!comp && &a != &b) has_duplicate_keys_ = true;
+ return comp < 0;
+ });
+ // First create a vector out of all keys.
+ // TODO(wvo): if kBuilderFlagShareKeyVectors is true, see if we can share
+ // the first vector.
+ auto keys = CreateVector(start, len, 2, true, false);
+ auto vec = CreateVector(start + 1, len, 2, false, false, &keys);
+ // Remove temp elements and return map.
+ stack_.resize(start);
+ stack_.push_back(vec);
+ return static_cast<size_t>(vec.u_);
+ }
+
+ // Call this after EndMap to see if the map had any duplicate keys.
+ // Any map with such keys won't be able to retrieve all values.
+ bool HasDuplicateKeys() const { return has_duplicate_keys_; }
+
+ template<typename F> size_t Vector(F f) {
+ auto start = StartVector();
+ f();
+ return EndVector(start, false, false);
+ }
+ template<typename F, typename T> size_t Vector(F f, T &state) {
+ auto start = StartVector();
+ f(state);
+ return EndVector(start, false, false);
+ }
+ template<typename F> size_t Vector(const char *key, F f) {
+ auto start = StartVector(key);
+ f();
+ return EndVector(start, false, false);
+ }
+ template<typename F, typename T>
+ size_t Vector(const char *key, F f, T &state) {
+ auto start = StartVector(key);
+ f(state);
+ return EndVector(start, false, false);
+ }
+
+ template<typename T> void Vector(const T *elems, size_t len) {
+ if (flatbuffers::is_scalar<T>::value) {
+ // This path should be a lot quicker and use less space.
+ ScalarVector(elems, len, false);
+ } else {
+ auto start = StartVector();
+ for (size_t i = 0; i < len; i++) Add(elems[i]);
+ EndVector(start, false, false);
+ }
+ }
+ template<typename T>
+ void Vector(const char *key, const T *elems, size_t len) {
+ Key(key);
+ Vector(elems, len);
+ }
+ template<typename T> void Vector(const std::vector<T> &vec) {
+ Vector(flatbuffers::vector_data(vec), vec.size());
+ }
+
+ template<typename F> size_t TypedVector(F f) {
+ auto start = StartVector();
+ f();
+ return EndVector(start, true, false);
+ }
+ template<typename F, typename T> size_t TypedVector(F f, T &state) {
+ auto start = StartVector();
+ f(state);
+ return EndVector(start, true, false);
+ }
+ template<typename F> size_t TypedVector(const char *key, F f) {
+ auto start = StartVector(key);
+ f();
+ return EndVector(start, true, false);
+ }
+ template<typename F, typename T>
+ size_t TypedVector(const char *key, F f, T &state) {
+ auto start = StartVector(key);
+ f(state);
+ return EndVector(start, true, false);
+ }
+
+ template<typename T> size_t FixedTypedVector(const T *elems, size_t len) {
+ // We only support a few fixed vector lengths. Anything bigger use a
+ // regular typed vector.
+ FLATBUFFERS_ASSERT(len >= 2 && len <= 4);
+ // And only scalar values.
+ static_assert(flatbuffers::is_scalar<T>::value, "Unrelated types");
+ return ScalarVector(elems, len, true);
+ }
+
+ template<typename T>
+ size_t FixedTypedVector(const char *key, const T *elems, size_t len) {
+ Key(key);
+ return FixedTypedVector(elems, len);
+ }
+
+ template<typename F> size_t Map(F f) {
+ auto start = StartMap();
+ f();
+ return EndMap(start);
+ }
+ template<typename F, typename T> size_t Map(F f, T &state) {
+ auto start = StartMap();
+ f(state);
+ return EndMap(start);
+ }
+ template<typename F> size_t Map(const char *key, F f) {
+ auto start = StartMap(key);
+ f();
+ return EndMap(start);
+ }
+ template<typename F, typename T> size_t Map(const char *key, F f, T &state) {
+ auto start = StartMap(key);
+ f(state);
+ return EndMap(start);
+ }
+ template<typename T> void Map(const std::map<std::string, T> &map) {
+ auto start = StartMap();
+ for (auto it = map.begin(); it != map.end(); ++it)
+ Add(it->first.c_str(), it->second);
+ EndMap(start);
+ }
+
+ // If you wish to share a value explicitly (a value not shared automatically
+ // through one of the BUILDER_FLAG_SHARE_* flags) you can do so with these
+ // functions. Or if you wish to turn those flags off for performance reasons
+ // and still do some explicit sharing. For example:
+ // builder.IndirectDouble(M_PI);
+ // auto id = builder.LastValue(); // Remember where we stored it.
+ // .. more code goes here ..
+ // builder.ReuseValue(id); // Refers to same double by offset.
+ // LastValue works regardless of whether the value has a key or not.
+ // Works on any data type.
+ struct Value;
+ Value LastValue() { return stack_.back(); }
+ void ReuseValue(Value v) { stack_.push_back(v); }
+ void ReuseValue(const char *key, Value v) {
+ Key(key);
+ ReuseValue(v);
+ }
+
+ // Overloaded Add that tries to call the correct function above.
+ void Add(int8_t i) { Int(i); }
+ void Add(int16_t i) { Int(i); }
+ void Add(int32_t i) { Int(i); }
+ void Add(int64_t i) { Int(i); }
+ void Add(uint8_t u) { UInt(u); }
+ void Add(uint16_t u) { UInt(u); }
+ void Add(uint32_t u) { UInt(u); }
+ void Add(uint64_t u) { UInt(u); }
+ void Add(float f) { Float(f); }
+ void Add(double d) { Double(d); }
+ void Add(bool b) { Bool(b); }
+ void Add(const char *str) { String(str); }
+ void Add(const std::string &str) { String(str); }
+ void Add(const flexbuffers::String &str) { String(str); }
+
+ template<typename T> void Add(const std::vector<T> &vec) { Vector(vec); }
+
+ template<typename T> void Add(const char *key, const T &t) {
+ Key(key);
+ Add(t);
+ }
+
+ template<typename T> void Add(const std::map<std::string, T> &map) {
+ Map(map);
+ }
+
+ template<typename T> void operator+=(const T &t) { Add(t); }
+
+ // This function is useful in combination with the Mutate* functions above.
+ // It forces elements of vectors and maps to have a minimum size, such that
+ // they can later be updated without failing.
+ // Call with no arguments to reset.
+ void ForceMinimumBitWidth(BitWidth bw = BIT_WIDTH_8) {
+ force_min_bit_width_ = bw;
+ }
+
+ void Finish() {
+ // If you hit this assert, you likely have objects that were never included
+ // in a parent. You need to have exactly one root to finish a buffer.
+ // Check your Start/End calls are matched, and all objects are inside
+ // some other object.
+ FLATBUFFERS_ASSERT(stack_.size() == 1);
+
+ // Write root value.
+ auto byte_width = Align(stack_[0].ElemWidth(buf_.size(), 0));
+ WriteAny(stack_[0], byte_width);
+ // Write root type.
+ Write(stack_[0].StoredPackedType(), 1);
+ // Write root size. Normally determined by parent, but root has no parent :)
+ Write(byte_width, 1);
+
+ finished_ = true;
+ }
+
+ private:
+ void Finished() const {
+ // If you get this assert, you're attempting to get access a buffer
+ // which hasn't been finished yet. Be sure to call
+ // Builder::Finish with your root object.
+ FLATBUFFERS_ASSERT(finished_);
+ }
+
+ // Align to prepare for writing a scalar with a certain size.
+ uint8_t Align(BitWidth alignment) {
+ auto byte_width = 1U << alignment;
+ buf_.insert(buf_.end(), flatbuffers::PaddingBytes(buf_.size(), byte_width),
+ 0);
+ return static_cast<uint8_t>(byte_width);
+ }
+
+ void WriteBytes(const void *val, size_t size) {
+ buf_.insert(buf_.end(), reinterpret_cast<const uint8_t *>(val),
+ reinterpret_cast<const uint8_t *>(val) + size);
+ }
+
+ template<typename T> void Write(T val, size_t byte_width) {
+ FLATBUFFERS_ASSERT(sizeof(T) >= byte_width);
+ val = flatbuffers::EndianScalar(val);
+ WriteBytes(&val, byte_width);
+ }
+
+ void WriteDouble(double f, uint8_t byte_width) {
+ switch (byte_width) {
+ case 8: Write(f, byte_width); break;
+ case 4: Write(static_cast<float>(f), byte_width); break;
+ // case 2: Write(static_cast<half>(f), byte_width); break;
+ // case 1: Write(static_cast<quarter>(f), byte_width); break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+
+ void WriteOffset(uint64_t o, uint8_t byte_width) {
+ auto reloff = buf_.size() - o;
+ FLATBUFFERS_ASSERT(byte_width == 8 || reloff < 1ULL << (byte_width * 8));
+ Write(reloff, byte_width);
+ }
+
+ template<typename T> void PushIndirect(T val, Type type, BitWidth bit_width) {
+ auto byte_width = Align(bit_width);
+ auto iloc = buf_.size();
+ Write(val, byte_width);
+ stack_.push_back(Value(static_cast<uint64_t>(iloc), type, bit_width));
+ }
+
+ static BitWidth WidthB(size_t byte_width) {
+ switch (byte_width) {
+ case 1: return BIT_WIDTH_8;
+ case 2: return BIT_WIDTH_16;
+ case 4: return BIT_WIDTH_32;
+ case 8: return BIT_WIDTH_64;
+ default: FLATBUFFERS_ASSERT(false); return BIT_WIDTH_64;
+ }
+ }
+
+ template<typename T> static Type GetScalarType() {
+ static_assert(flatbuffers::is_scalar<T>::value, "Unrelated types");
+ return flatbuffers::is_floating_point<T>::value
+ ? FBT_FLOAT
+ : flatbuffers::is_same<T, bool>::value
+ ? FBT_BOOL
+ : (flatbuffers::is_unsigned<T>::value ? FBT_UINT
+ : FBT_INT);
+ }
+
+ public:
+ // This was really intended to be private, except for LastValue/ReuseValue.
+ struct Value {
+ union {
+ int64_t i_;
+ uint64_t u_;
+ double f_;
+ };
+
+ Type type_;
+
+ // For scalars: of itself, for vector: of its elements, for string: length.
+ BitWidth min_bit_width_;
+
+ Value() : i_(0), type_(FBT_NULL), min_bit_width_(BIT_WIDTH_8) {}
+
+ Value(bool b)
+ : u_(static_cast<uint64_t>(b)),
+ type_(FBT_BOOL),
+ min_bit_width_(BIT_WIDTH_8) {}
+
+ Value(int64_t i, Type t, BitWidth bw)
+ : i_(i), type_(t), min_bit_width_(bw) {}
+ Value(uint64_t u, Type t, BitWidth bw)
+ : u_(u), type_(t), min_bit_width_(bw) {}
+
+ Value(float f)
+ : f_(static_cast<double>(f)),
+ type_(FBT_FLOAT),
+ min_bit_width_(BIT_WIDTH_32) {}
+ Value(double f) : f_(f), type_(FBT_FLOAT), min_bit_width_(WidthF(f)) {}
+
+ uint8_t StoredPackedType(BitWidth parent_bit_width_ = BIT_WIDTH_8) const {
+ return PackedType(StoredWidth(parent_bit_width_), type_);
+ }
+
+ BitWidth ElemWidth(size_t buf_size, size_t elem_index) const {
+ if (IsInline(type_)) {
+ return min_bit_width_;
+ } else {
+ // We have an absolute offset, but want to store a relative offset
+ // elem_index elements beyond the current buffer end. Since whether
+ // the relative offset fits in a certain byte_width depends on
+ // the size of the elements before it (and their alignment), we have
+ // to test for each size in turn.
+ for (size_t byte_width = 1;
+ byte_width <= sizeof(flatbuffers::largest_scalar_t);
+ byte_width *= 2) {
+ // Where are we going to write this offset?
+ auto offset_loc = buf_size +
+ flatbuffers::PaddingBytes(buf_size, byte_width) +
+ elem_index * byte_width;
+ // Compute relative offset.
+ auto offset = offset_loc - u_;
+ // Does it fit?
+ auto bit_width = WidthU(offset);
+ if (static_cast<size_t>(static_cast<size_t>(1U) << bit_width) ==
+ byte_width)
+ return bit_width;
+ }
+ FLATBUFFERS_ASSERT(false); // Must match one of the sizes above.
+ return BIT_WIDTH_64;
+ }
+ }
+
+ BitWidth StoredWidth(BitWidth parent_bit_width_ = BIT_WIDTH_8) const {
+ if (IsInline(type_)) {
+ return (std::max)(min_bit_width_, parent_bit_width_);
+ } else {
+ return min_bit_width_;
+ }
+ }
+ };
+
+ private:
+ void WriteAny(const Value &val, uint8_t byte_width) {
+ switch (val.type_) {
+ case FBT_NULL:
+ case FBT_INT: Write(val.i_, byte_width); break;
+ case FBT_BOOL:
+ case FBT_UINT: Write(val.u_, byte_width); break;
+ case FBT_FLOAT: WriteDouble(val.f_, byte_width); break;
+ default: WriteOffset(val.u_, byte_width); break;
+ }
+ }
+
+ size_t CreateBlob(const void *data, size_t len, size_t trailing, Type type) {
+ auto bit_width = WidthU(len);
+ auto byte_width = Align(bit_width);
+ Write<uint64_t>(len, byte_width);
+ auto sloc = buf_.size();
+ WriteBytes(data, len + trailing);
+ stack_.push_back(Value(static_cast<uint64_t>(sloc), type, bit_width));
+ return sloc;
+ }
+
+ template<typename T>
+ size_t ScalarVector(const T *elems, size_t len, bool fixed) {
+ auto vector_type = GetScalarType<T>();
+ auto byte_width = sizeof(T);
+ auto bit_width = WidthB(byte_width);
+ // If you get this assert, you're trying to write a vector with a size
+ // field that is bigger than the scalars you're trying to write (e.g. a
+ // byte vector > 255 elements). For such types, write a "blob" instead.
+ // TODO: instead of asserting, could write vector with larger elements
+ // instead, though that would be wasteful.
+ FLATBUFFERS_ASSERT(WidthU(len) <= bit_width);
+ Align(bit_width);
+ if (!fixed) Write<uint64_t>(len, byte_width);
+ auto vloc = buf_.size();
+ for (size_t i = 0; i < len; i++) Write(elems[i], byte_width);
+ stack_.push_back(Value(static_cast<uint64_t>(vloc),
+ ToTypedVector(vector_type, fixed ? len : 0),
+ bit_width));
+ return vloc;
+ }
+
+ Value CreateVector(size_t start, size_t vec_len, size_t step, bool typed,
+ bool fixed, const Value *keys = nullptr) {
+ FLATBUFFERS_ASSERT(
+ !fixed ||
+ typed); // typed=false, fixed=true combination is not supported.
+ // Figure out smallest bit width we can store this vector with.
+ auto bit_width = (std::max)(force_min_bit_width_, WidthU(vec_len));
+ auto prefix_elems = 1;
+ if (keys) {
+ // If this vector is part of a map, we will pre-fix an offset to the keys
+ // to this vector.
+ bit_width = (std::max)(bit_width, keys->ElemWidth(buf_.size(), 0));
+ prefix_elems += 2;
+ }
+ Type vector_type = FBT_KEY;
+ // Check bit widths and types for all elements.
+ for (size_t i = start; i < stack_.size(); i += step) {
+ auto elem_width =
+ stack_[i].ElemWidth(buf_.size(), i - start + prefix_elems);
+ bit_width = (std::max)(bit_width, elem_width);
+ if (typed) {
+ if (i == start) {
+ vector_type = stack_[i].type_;
+ } else {
+ // If you get this assert, you are writing a typed vector with
+ // elements that are not all the same type.
+ FLATBUFFERS_ASSERT(vector_type == stack_[i].type_);
+ }
+ }
+ }
+ // If you get this assert, your fixed types are not one of:
+ // Int / UInt / Float / Key.
+ FLATBUFFERS_ASSERT(!fixed || IsTypedVectorElementType(vector_type));
+ auto byte_width = Align(bit_width);
+ // Write vector. First the keys width/offset if available, and size.
+ if (keys) {
+ WriteOffset(keys->u_, byte_width);
+ Write<uint64_t>(1ULL << keys->min_bit_width_, byte_width);
+ }
+ if (!fixed) Write<uint64_t>(vec_len, byte_width);
+ // Then the actual data.
+ auto vloc = buf_.size();
+ for (size_t i = start; i < stack_.size(); i += step) {
+ WriteAny(stack_[i], byte_width);
+ }
+ // Then the types.
+ if (!typed) {
+ for (size_t i = start; i < stack_.size(); i += step) {
+ buf_.push_back(stack_[i].StoredPackedType(bit_width));
+ }
+ }
+ return Value(static_cast<uint64_t>(vloc),
+ keys ? FBT_MAP
+ : (typed ? ToTypedVector(vector_type, fixed ? vec_len : 0)
+ : FBT_VECTOR),
+ bit_width);
+ }
+
+ // You shouldn't really be copying instances of this class.
+ Builder(const Builder &);
+ Builder &operator=(const Builder &);
+
+ std::vector<uint8_t> buf_;
+ std::vector<Value> stack_;
+
+ bool finished_;
+ bool has_duplicate_keys_;
+
+ BuilderFlag flags_;
+
+ BitWidth force_min_bit_width_;
+
+ struct KeyOffsetCompare {
+ explicit KeyOffsetCompare(const std::vector<uint8_t> &buf) : buf_(&buf) {}
+ bool operator()(size_t a, size_t b) const {
+ auto stra =
+ reinterpret_cast<const char *>(flatbuffers::vector_data(*buf_) + a);
+ auto strb =
+ reinterpret_cast<const char *>(flatbuffers::vector_data(*buf_) + b);
+ return strcmp(stra, strb) < 0;
+ }
+ const std::vector<uint8_t> *buf_;
+ };
+
+ typedef std::pair<size_t, size_t> StringOffset;
+ struct StringOffsetCompare {
+ explicit StringOffsetCompare(const std::vector<uint8_t> &buf)
+ : buf_(&buf) {}
+ bool operator()(const StringOffset &a, const StringOffset &b) const {
+ auto stra = reinterpret_cast<const char *>(
+ flatbuffers::vector_data(*buf_) + a.first);
+ auto strb = reinterpret_cast<const char *>(
+ flatbuffers::vector_data(*buf_) + b.first);
+ return strncmp(stra, strb, (std::min)(a.second, b.second) + 1) < 0;
+ }
+ const std::vector<uint8_t> *buf_;
+ };
+
+ typedef std::set<size_t, KeyOffsetCompare> KeyOffsetMap;
+ typedef std::set<StringOffset, StringOffsetCompare> StringOffsetMap;
+
+ KeyOffsetMap key_pool;
+ StringOffsetMap string_pool;
+};
+
+} // namespace flexbuffers
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+#endif // FLATBUFFERS_FLEXBUFFERS_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/hash.h b/contrib/libs/flatbuffers/include/flatbuffers/hash.h
new file mode 100644
index 0000000000..52cc628cdf
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/hash.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_HASH_H_
+#define FLATBUFFERS_HASH_H_
+
+#include <cstdint>
+#include <cstring>
+
+#include "flatbuffers.h"
+
+namespace flatbuffers {
+
+template<typename T> struct FnvTraits {
+ static const T kFnvPrime;
+ static const T kOffsetBasis;
+};
+
+template<> struct FnvTraits<uint32_t> {
+ static const uint32_t kFnvPrime = 0x01000193;
+ static const uint32_t kOffsetBasis = 0x811C9DC5;
+};
+
+template<> struct FnvTraits<uint64_t> {
+ static const uint64_t kFnvPrime = 0x00000100000001b3ULL;
+ static const uint64_t kOffsetBasis = 0xcbf29ce484222645ULL;
+};
+
+template<typename T> T HashFnv1(const char *input) {
+ T hash = FnvTraits<T>::kOffsetBasis;
+ for (const char *c = input; *c; ++c) {
+ hash *= FnvTraits<T>::kFnvPrime;
+ hash ^= static_cast<unsigned char>(*c);
+ }
+ return hash;
+}
+
+template<typename T> T HashFnv1a(const char *input) {
+ T hash = FnvTraits<T>::kOffsetBasis;
+ for (const char *c = input; *c; ++c) {
+ hash ^= static_cast<unsigned char>(*c);
+ hash *= FnvTraits<T>::kFnvPrime;
+ }
+ return hash;
+}
+
+template<> inline uint16_t HashFnv1<uint16_t>(const char *input) {
+ uint32_t hash = HashFnv1<uint32_t>(input);
+ return (hash >> 16) ^ (hash & 0xffff);
+}
+
+template<> inline uint16_t HashFnv1a<uint16_t>(const char *input) {
+ uint32_t hash = HashFnv1a<uint32_t>(input);
+ return (hash >> 16) ^ (hash & 0xffff);
+}
+
+template<typename T> struct NamedHashFunction {
+ const char *name;
+
+ typedef T (*HashFunction)(const char *);
+ HashFunction function;
+};
+
+const NamedHashFunction<uint16_t> kHashFunctions16[] = {
+ { "fnv1_16", HashFnv1<uint16_t> },
+ { "fnv1a_16", HashFnv1a<uint16_t> },
+};
+
+const NamedHashFunction<uint32_t> kHashFunctions32[] = {
+ { "fnv1_32", HashFnv1<uint32_t> },
+ { "fnv1a_32", HashFnv1a<uint32_t> },
+};
+
+const NamedHashFunction<uint64_t> kHashFunctions64[] = {
+ { "fnv1_64", HashFnv1<uint64_t> },
+ { "fnv1a_64", HashFnv1a<uint64_t> },
+};
+
+inline NamedHashFunction<uint16_t>::HashFunction FindHashFunction16(
+ const char *name) {
+ std::size_t size = sizeof(kHashFunctions16) / sizeof(kHashFunctions16[0]);
+ for (std::size_t i = 0; i < size; ++i) {
+ if (std::strcmp(name, kHashFunctions16[i].name) == 0) {
+ return kHashFunctions16[i].function;
+ }
+ }
+ return nullptr;
+}
+
+inline NamedHashFunction<uint32_t>::HashFunction FindHashFunction32(
+ const char *name) {
+ std::size_t size = sizeof(kHashFunctions32) / sizeof(kHashFunctions32[0]);
+ for (std::size_t i = 0; i < size; ++i) {
+ if (std::strcmp(name, kHashFunctions32[i].name) == 0) {
+ return kHashFunctions32[i].function;
+ }
+ }
+ return nullptr;
+}
+
+inline NamedHashFunction<uint64_t>::HashFunction FindHashFunction64(
+ const char *name) {
+ std::size_t size = sizeof(kHashFunctions64) / sizeof(kHashFunctions64[0]);
+ for (std::size_t i = 0; i < size; ++i) {
+ if (std::strcmp(name, kHashFunctions64[i].name) == 0) {
+ return kHashFunctions64[i].function;
+ }
+ }
+ return nullptr;
+}
+
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_HASH_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/idl.h b/contrib/libs/flatbuffers/include/flatbuffers/idl.h
new file mode 100644
index 0000000000..a82ff8a694
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/idl.h
@@ -0,0 +1,1208 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_IDL_H_
+#define FLATBUFFERS_IDL_H_
+
+#include <map>
+#include <memory>
+#include <stack>
+
+#include "base.h"
+#include "flatbuffers.h"
+#include "flexbuffers.h"
+#include "hash.h"
+#include "reflection.h"
+
+#if !defined(FLATBUFFERS_CPP98_STL)
+# include <functional>
+#endif // !defined(FLATBUFFERS_CPP98_STL)
+
+// This file defines the data types representing a parsed IDL (Interface
+// Definition Language) / schema file.
+
+// Limits maximum depth of nested objects.
+// Prevents stack overflow while parse scheme, or json, or flexbuffer.
+#if !defined(FLATBUFFERS_MAX_PARSING_DEPTH)
+# define FLATBUFFERS_MAX_PARSING_DEPTH 64
+#endif
+
+namespace flatbuffers {
+
+// The order of these matters for Is*() functions below.
+// Additionally, Parser::ParseType assumes bool..string is a contiguous range
+// of type tokens.
+// clang-format off
+#define FLATBUFFERS_GEN_TYPES_SCALAR(TD) \
+ TD(NONE, "", uint8_t, byte, byte, byte, uint8, u8, UByte, UInt8) \
+ TD(UTYPE, "", uint8_t, byte, byte, byte, uint8, u8, UByte, UInt8) /* begin scalar/int */ \
+ TD(BOOL, "bool", uint8_t, boolean,bool, bool, bool, bool, Boolean, Bool) \
+ TD(CHAR, "byte", int8_t, byte, int8, sbyte, int8, i8, Byte, Int8) \
+ TD(UCHAR, "ubyte", uint8_t, byte, byte, byte, uint8, u8, UByte, UInt8) \
+ TD(SHORT, "short", int16_t, short, int16, short, int16, i16, Short, Int16) \
+ TD(USHORT, "ushort", uint16_t, short, uint16, ushort, uint16, u16, UShort, UInt16) \
+ TD(INT, "int", int32_t, int, int32, int, int32, i32, Int, Int32) \
+ TD(UINT, "uint", uint32_t, int, uint32, uint, uint32, u32, UInt, UInt32) \
+ TD(LONG, "long", int64_t, long, int64, long, int64, i64, Long, Int64) \
+ TD(ULONG, "ulong", uint64_t, long, uint64, ulong, uint64, u64, ULong, UInt64) /* end int */ \
+ TD(FLOAT, "float", float, float, float32, float, float32, f32, Float, Float32) /* begin float */ \
+ TD(DOUBLE, "double", double, double, float64, double, float64, f64, Double, Double) /* end float/scalar */
+#define FLATBUFFERS_GEN_TYPES_POINTER(TD) \
+ TD(STRING, "string", Offset<void>, int, int, StringOffset, int, unused, Int, Offset<String>) \
+ TD(VECTOR, "", Offset<void>, int, int, VectorOffset, int, unused, Int, Offset<UOffset>) \
+ TD(STRUCT, "", Offset<void>, int, int, int, int, unused, Int, Offset<UOffset>) \
+ TD(UNION, "", Offset<void>, int, int, int, int, unused, Int, Offset<UOffset>)
+#define FLATBUFFERS_GEN_TYPE_ARRAY(TD) \
+ TD(ARRAY, "", int, int, int, int, int, unused, Int, Offset<UOffset>)
+// The fields are:
+// - enum
+// - FlatBuffers schema type.
+// - C++ type.
+// - Java type.
+// - Go type.
+// - C# / .Net type.
+// - Python type.
+// - Rust type.
+// - Kotlin type.
+
+// using these macros, we can now write code dealing with types just once, e.g.
+
+/*
+switch (type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, \
+ RTYPE, KTYPE) \
+ case BASE_TYPE_ ## ENUM: \
+ // do something specific to CTYPE here
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+}
+*/
+
+// If not all FLATBUFFERS_GEN_() arguments are necessary for implementation
+// of FLATBUFFERS_TD, you can use a variadic macro (with __VA_ARGS__ if needed).
+// In the above example, only CTYPE is used to generate the code, it can be rewritten:
+
+/*
+switch (type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ // do something specific to CTYPE here
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+}
+*/
+
+#define FLATBUFFERS_GEN_TYPES(TD) \
+ FLATBUFFERS_GEN_TYPES_SCALAR(TD) \
+ FLATBUFFERS_GEN_TYPES_POINTER(TD) \
+ FLATBUFFERS_GEN_TYPE_ARRAY(TD)
+
+// Create an enum for all the types above.
+#ifdef __GNUC__
+__extension__ // Stop GCC complaining about trailing comma with -Wpendantic.
+#endif
+enum BaseType {
+ #define FLATBUFFERS_TD(ENUM, ...) \
+ BASE_TYPE_ ## ENUM,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+};
+
+#define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ static_assert(sizeof(CTYPE) <= sizeof(largest_scalar_t), \
+ "define largest_scalar_t as " #CTYPE);
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+#undef FLATBUFFERS_TD
+
+inline bool IsScalar (BaseType t) { return t >= BASE_TYPE_UTYPE &&
+ t <= BASE_TYPE_DOUBLE; }
+inline bool IsInteger(BaseType t) { return t >= BASE_TYPE_UTYPE &&
+ t <= BASE_TYPE_ULONG; }
+inline bool IsFloat (BaseType t) { return t == BASE_TYPE_FLOAT ||
+ t == BASE_TYPE_DOUBLE; }
+inline bool IsLong (BaseType t) { return t == BASE_TYPE_LONG ||
+ t == BASE_TYPE_ULONG; }
+inline bool IsBool (BaseType t) { return t == BASE_TYPE_BOOL; }
+inline bool IsOneByte(BaseType t) { return t >= BASE_TYPE_UTYPE &&
+ t <= BASE_TYPE_UCHAR; }
+
+inline bool IsUnsigned(BaseType t) {
+ return (t == BASE_TYPE_UTYPE) || (t == BASE_TYPE_UCHAR) ||
+ (t == BASE_TYPE_USHORT) || (t == BASE_TYPE_UINT) ||
+ (t == BASE_TYPE_ULONG);
+}
+
+// clang-format on
+
+extern const char *const kTypeNames[];
+extern const char kTypeSizes[];
+
+inline size_t SizeOf(BaseType t) { return kTypeSizes[t]; }
+
+struct StructDef;
+struct EnumDef;
+class Parser;
+
+// Represents any type in the IDL, which is a combination of the BaseType
+// and additional information for vectors/structs_.
+struct Type {
+ explicit Type(BaseType _base_type = BASE_TYPE_NONE, StructDef *_sd = nullptr,
+ EnumDef *_ed = nullptr, uint16_t _fixed_length = 0)
+ : base_type(_base_type),
+ element(BASE_TYPE_NONE),
+ struct_def(_sd),
+ enum_def(_ed),
+ fixed_length(_fixed_length) {}
+
+ bool operator==(const Type &o) {
+ return base_type == o.base_type && element == o.element &&
+ struct_def == o.struct_def && enum_def == o.enum_def;
+ }
+
+ Type VectorType() const {
+ return Type(element, struct_def, enum_def, fixed_length);
+ }
+
+ Offset<reflection::Type> Serialize(FlatBufferBuilder *builder) const;
+
+ bool Deserialize(const Parser &parser, const reflection::Type *type);
+
+ BaseType base_type;
+ BaseType element; // only set if t == BASE_TYPE_VECTOR
+ StructDef *struct_def; // only set if t or element == BASE_TYPE_STRUCT
+ EnumDef *enum_def; // set if t == BASE_TYPE_UNION / BASE_TYPE_UTYPE,
+ // or for an integral type derived from an enum.
+ uint16_t fixed_length; // only set if t == BASE_TYPE_ARRAY
+};
+
+// Represents a parsed scalar value, it's type, and field offset.
+struct Value {
+ Value()
+ : constant("0"),
+ offset(static_cast<voffset_t>(~(static_cast<voffset_t>(0U)))) {}
+ Type type;
+ std::string constant;
+ voffset_t offset;
+};
+
+// Helper class that retains the original order of a set of identifiers and
+// also provides quick lookup.
+template<typename T> class SymbolTable {
+ public:
+ ~SymbolTable() {
+ for (auto it = vec.begin(); it != vec.end(); ++it) { delete *it; }
+ }
+
+ bool Add(const std::string &name, T *e) {
+ vector_emplace_back(&vec, e);
+ auto it = dict.find(name);
+ if (it != dict.end()) return true;
+ dict[name] = e;
+ return false;
+ }
+
+ void Move(const std::string &oldname, const std::string &newname) {
+ auto it = dict.find(oldname);
+ if (it != dict.end()) {
+ auto obj = it->second;
+ dict.erase(it);
+ dict[newname] = obj;
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ }
+
+ T *Lookup(const std::string &name) const {
+ auto it = dict.find(name);
+ return it == dict.end() ? nullptr : it->second;
+ }
+
+ public:
+ std::map<std::string, T *> dict; // quick lookup
+ std::vector<T *> vec; // Used to iterate in order of insertion
+};
+
+// A name space, as set in the schema.
+struct Namespace {
+ Namespace() : from_table(0) {}
+
+ // Given a (potentially unqualified) name, return the "fully qualified" name
+ // which has a full namespaced descriptor.
+ // With max_components you can request less than the number of components
+ // the current namespace has.
+ std::string GetFullyQualifiedName(const std::string &name,
+ size_t max_components = 1000) const;
+
+ std::vector<std::string> components;
+ size_t from_table; // Part of the namespace corresponds to a message/table.
+};
+
+inline bool operator<(const Namespace &a, const Namespace &b) {
+ size_t min_size = std::min(a.components.size(), b.components.size());
+ for (size_t i = 0; i < min_size; ++i) {
+ if (a.components[i] != b.components[i])
+ return a.components[i] < b.components[i];
+ }
+ return a.components.size() < b.components.size();
+}
+
+// Base class for all definition types (fields, structs_, enums_).
+struct Definition {
+ Definition()
+ : generated(false),
+ defined_namespace(nullptr),
+ serialized_location(0),
+ index(-1),
+ refcount(1) {}
+
+ flatbuffers::Offset<
+ flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>>
+ SerializeAttributes(FlatBufferBuilder *builder, const Parser &parser) const;
+
+ bool DeserializeAttributes(Parser &parser,
+ const Vector<Offset<reflection::KeyValue>> *attrs);
+
+ std::string name;
+ std::string file;
+ std::vector<std::string> doc_comment;
+ SymbolTable<Value> attributes;
+ bool generated; // did we already output code for this definition?
+ Namespace *defined_namespace; // Where it was defined.
+
+ // For use with Serialize()
+ uoffset_t serialized_location;
+ int index; // Inside the vector it is stored.
+ int refcount;
+};
+
+struct FieldDef : public Definition {
+ FieldDef()
+ : deprecated(false),
+ key(false),
+ shared(false),
+ native_inline(false),
+ flexbuffer(false),
+ presence(kDefault),
+ nested_flatbuffer(NULL),
+ padding(0) {}
+
+ Offset<reflection::Field> Serialize(FlatBufferBuilder *builder, uint16_t id,
+ const Parser &parser) const;
+
+ bool Deserialize(Parser &parser, const reflection::Field *field);
+
+ bool IsScalarOptional() const {
+ return IsScalar(value.type.base_type) && IsOptional();
+ }
+ bool IsOptional() const {
+ return presence == kOptional;
+ }
+ bool IsRequired() const {
+ return presence == kRequired;
+ }
+ bool IsDefault() const {
+ return presence == kDefault;
+ }
+
+ Value value;
+ bool deprecated; // Field is allowed to be present in old data, but can't be.
+ // written in new data nor accessed in new code.
+ bool key; // Field functions as a key for creating sorted vectors.
+ bool shared; // Field will be using string pooling (i.e. CreateSharedString)
+ // as default serialization behavior if field is a string.
+ bool native_inline; // Field will be defined inline (instead of as a pointer)
+ // for native tables if field is a struct.
+ bool flexbuffer; // This field contains FlexBuffer data.
+
+ enum Presence {
+ // Field must always be present.
+ kRequired,
+ // Non-presence should be signalled to and controlled by users.
+ kOptional,
+ // Non-presence is hidden from users.
+ // Implementations may omit writing default values.
+ kDefault,
+ };
+ Presence static MakeFieldPresence(bool optional, bool required) {
+ FLATBUFFERS_ASSERT(!(required && optional));
+ // clang-format off
+ return required ? FieldDef::kRequired
+ : optional ? FieldDef::kOptional
+ : FieldDef::kDefault;
+ // clang-format on
+ }
+ Presence presence;
+
+ StructDef *nested_flatbuffer; // This field contains nested FlatBuffer data.
+ size_t padding; // Bytes to always pad after this field.
+};
+
+struct StructDef : public Definition {
+ StructDef()
+ : fixed(false),
+ predecl(true),
+ sortbysize(true),
+ has_key(false),
+ minalign(1),
+ bytesize(0) {}
+
+ void PadLastField(size_t min_align) {
+ auto padding = PaddingBytes(bytesize, min_align);
+ bytesize += padding;
+ if (fields.vec.size()) fields.vec.back()->padding = padding;
+ }
+
+ Offset<reflection::Object> Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const;
+
+ bool Deserialize(Parser &parser, const reflection::Object *object);
+
+ SymbolTable<FieldDef> fields;
+
+ bool fixed; // If it's struct, not a table.
+ bool predecl; // If it's used before it was defined.
+ bool sortbysize; // Whether fields come in the declaration or size order.
+ bool has_key; // It has a key field.
+ size_t minalign; // What the whole object needs to be aligned to.
+ size_t bytesize; // Size if fixed.
+
+ flatbuffers::unique_ptr<std::string> original_location;
+};
+
+struct EnumDef;
+struct EnumValBuilder;
+
+struct EnumVal {
+ Offset<reflection::EnumVal> Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const;
+
+ bool Deserialize(const Parser &parser, const reflection::EnumVal *val);
+
+ uint64_t GetAsUInt64() const { return static_cast<uint64_t>(value); }
+ int64_t GetAsInt64() const { return value; }
+ bool IsZero() const { return 0 == value; }
+ bool IsNonZero() const { return !IsZero(); }
+
+ std::string name;
+ std::vector<std::string> doc_comment;
+ Type union_type;
+
+ private:
+ friend EnumDef;
+ friend EnumValBuilder;
+ friend bool operator==(const EnumVal &lhs, const EnumVal &rhs);
+
+ EnumVal(const std::string &_name, int64_t _val) : name(_name), value(_val) {}
+ EnumVal() : value(0) {}
+
+ int64_t value;
+};
+
+struct EnumDef : public Definition {
+ EnumDef() : is_union(false), uses_multiple_type_instances(false) {}
+
+ Offset<reflection::Enum> Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const;
+
+ bool Deserialize(Parser &parser, const reflection::Enum *values);
+
+ template<typename T> void ChangeEnumValue(EnumVal *ev, T new_val);
+ void SortByValue();
+ void RemoveDuplicates();
+
+ std::string AllFlags() const;
+ const EnumVal *MinValue() const;
+ const EnumVal *MaxValue() const;
+ // Returns the number of integer steps from v1 to v2.
+ uint64_t Distance(const EnumVal *v1, const EnumVal *v2) const;
+ // Returns the number of integer steps from Min to Max.
+ uint64_t Distance() const { return Distance(MinValue(), MaxValue()); }
+
+ EnumVal *ReverseLookup(int64_t enum_idx,
+ bool skip_union_default = false) const;
+ EnumVal *FindByValue(const std::string &constant) const;
+
+ std::string ToString(const EnumVal &ev) const {
+ return IsUInt64() ? NumToString(ev.GetAsUInt64())
+ : NumToString(ev.GetAsInt64());
+ }
+
+ size_t size() const { return vals.vec.size(); }
+
+ const std::vector<EnumVal *> &Vals() const { return vals.vec; }
+
+ const EnumVal *Lookup(const std::string &enum_name) const {
+ return vals.Lookup(enum_name);
+ }
+
+ bool is_union;
+ // Type is a union which uses type aliases where at least one type is
+ // available under two different names.
+ bool uses_multiple_type_instances;
+ Type underlying_type;
+
+ private:
+ bool IsUInt64() const {
+ return (BASE_TYPE_ULONG == underlying_type.base_type);
+ }
+
+ friend EnumValBuilder;
+ SymbolTable<EnumVal> vals;
+};
+
+inline bool IsString(const Type &type) {
+ return type.base_type == BASE_TYPE_STRING;
+}
+
+inline bool IsStruct(const Type &type) {
+ return type.base_type == BASE_TYPE_STRUCT && type.struct_def->fixed;
+}
+
+inline bool IsUnion(const Type &type) {
+ return type.enum_def != nullptr && type.enum_def->is_union;
+}
+
+inline bool IsVector(const Type &type) {
+ return type.base_type == BASE_TYPE_VECTOR;
+}
+
+inline bool IsArray(const Type &type) {
+ return type.base_type == BASE_TYPE_ARRAY;
+}
+
+inline bool IsSeries(const Type &type) {
+ return IsVector(type) || IsArray(type);
+}
+
+inline bool IsEnum(const Type &type) {
+ return type.enum_def != nullptr && IsInteger(type.base_type);
+}
+
+inline size_t InlineSize(const Type &type) {
+ return IsStruct(type)
+ ? type.struct_def->bytesize
+ : (IsArray(type)
+ ? InlineSize(type.VectorType()) * type.fixed_length
+ : SizeOf(type.base_type));
+}
+
+inline size_t InlineAlignment(const Type &type) {
+ if (IsStruct(type)) {
+ return type.struct_def->minalign;
+ } else if (IsArray(type)) {
+ return IsStruct(type.VectorType()) ? type.struct_def->minalign
+ : SizeOf(type.element);
+ } else {
+ return SizeOf(type.base_type);
+ }
+}
+inline bool operator==(const EnumVal &lhs, const EnumVal &rhs) {
+ return lhs.value == rhs.value;
+}
+inline bool operator!=(const EnumVal &lhs, const EnumVal &rhs) {
+ return !(lhs == rhs);
+}
+
+inline bool EqualByName(const Type &a, const Type &b) {
+ return a.base_type == b.base_type && a.element == b.element &&
+ (a.struct_def == b.struct_def ||
+ a.struct_def->name == b.struct_def->name) &&
+ (a.enum_def == b.enum_def || a.enum_def->name == b.enum_def->name);
+}
+
+struct RPCCall : public Definition {
+ Offset<reflection::RPCCall> Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const;
+
+ bool Deserialize(Parser &parser, const reflection::RPCCall *call);
+
+ StructDef *request, *response;
+};
+
+struct ServiceDef : public Definition {
+ Offset<reflection::Service> Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const;
+ bool Deserialize(Parser &parser, const reflection::Service *service);
+
+ SymbolTable<RPCCall> calls;
+};
+
+// Container of options that may apply to any of the source/text generators.
+struct IDLOptions {
+ bool gen_jvmstatic;
+ // Use flexbuffers instead for binary and text generation
+ bool use_flexbuffers;
+ bool strict_json;
+ bool output_default_scalars_in_json;
+ int indent_step;
+ bool output_enum_identifiers;
+ bool prefixed_enums;
+ bool scoped_enums;
+ bool include_dependence_headers;
+ bool mutable_buffer;
+ bool one_file;
+ bool proto_mode;
+ bool proto_oneof_union;
+ bool generate_all;
+ bool skip_unexpected_fields_in_json;
+ bool generate_name_strings;
+ bool generate_object_based_api;
+ bool gen_compare;
+ std::string cpp_object_api_pointer_type;
+ std::string cpp_object_api_string_type;
+ bool cpp_object_api_string_flexible_constructor;
+ bool cpp_direct_copy;
+ bool gen_nullable;
+ bool java_checkerframework;
+ bool gen_generated;
+ std::string object_prefix;
+ std::string object_suffix;
+ bool union_value_namespacing;
+ bool allow_non_utf8;
+ bool natural_utf8;
+ std::string include_prefix;
+ bool keep_include_path;
+ bool binary_schema_comments;
+ bool binary_schema_builtins;
+ bool binary_schema_gen_embed;
+ std::string go_import;
+ std::string go_namespace;
+ bool protobuf_ascii_alike;
+ bool size_prefixed;
+ std::string root_type;
+ bool force_defaults;
+ bool java_primitive_has_method;
+ bool cs_gen_json_serializer;
+ std::vector<std::string> cpp_includes;
+ std::string cpp_std;
+ bool cpp_static_reflection;
+ std::string proto_namespace_suffix;
+ std::string filename_suffix;
+ std::string filename_extension;
+ bool no_warnings;
+
+ // Possible options for the more general generator below.
+ enum Language {
+ kJava = 1 << 0,
+ kCSharp = 1 << 1,
+ kGo = 1 << 2,
+ kCpp = 1 << 3,
+ kPython = 1 << 5,
+ kPhp = 1 << 6,
+ kJson = 1 << 7,
+ kBinary = 1 << 8,
+ kTs = 1 << 9,
+ kJsonSchema = 1 << 10,
+ kDart = 1 << 11,
+ kLua = 1 << 12,
+ kLobster = 1 << 13,
+ kRust = 1 << 14,
+ kKotlin = 1 << 15,
+ kSwift = 1 << 16,
+ kCppYandexMapsIter = 1 << 17,
+ kMAX
+ };
+
+ Language lang;
+
+ enum MiniReflect { kNone, kTypes, kTypesAndNames };
+
+ MiniReflect mini_reflect;
+
+ // If set, require all fields in a table to be explicitly numbered.
+ bool require_explicit_ids;
+
+ // The corresponding language bit will be set if a language is included
+ // for code generation.
+ unsigned long lang_to_generate;
+
+ // If set (default behavior), empty string fields will be set to nullptr to
+ // make the flatbuffer more compact.
+ bool set_empty_strings_to_null;
+
+ // If set (default behavior), empty vector fields will be set to nullptr to
+ // make the flatbuffer more compact.
+ bool set_empty_vectors_to_null;
+
+ IDLOptions()
+ : gen_jvmstatic(false),
+ use_flexbuffers(false),
+ strict_json(false),
+ output_default_scalars_in_json(false),
+ indent_step(2),
+ output_enum_identifiers(true),
+ prefixed_enums(true),
+ scoped_enums(false),
+ include_dependence_headers(true),
+ mutable_buffer(false),
+ one_file(false),
+ proto_mode(false),
+ proto_oneof_union(false),
+ generate_all(false),
+ skip_unexpected_fields_in_json(false),
+ generate_name_strings(false),
+ generate_object_based_api(false),
+ gen_compare(false),
+ cpp_object_api_pointer_type("std::unique_ptr"),
+ cpp_object_api_string_flexible_constructor(false),
+ cpp_direct_copy(true),
+ gen_nullable(false),
+ java_checkerframework(false),
+ gen_generated(false),
+ object_suffix("T"),
+ union_value_namespacing(true),
+ allow_non_utf8(false),
+ natural_utf8(false),
+ keep_include_path(false),
+ binary_schema_comments(false),
+ binary_schema_builtins(false),
+ binary_schema_gen_embed(false),
+ protobuf_ascii_alike(false),
+ size_prefixed(false),
+ force_defaults(false),
+ java_primitive_has_method(false),
+ cs_gen_json_serializer(false),
+ cpp_static_reflection(false),
+ filename_suffix("_generated"),
+ filename_extension(),
+ no_warnings(false),
+ lang(IDLOptions::kJava),
+ mini_reflect(IDLOptions::kNone),
+ require_explicit_ids(false),
+ lang_to_generate(0),
+ set_empty_strings_to_null(true),
+ set_empty_vectors_to_null(true) {}
+};
+
+// This encapsulates where the parser is in the current source file.
+struct ParserState {
+ ParserState()
+ : cursor_(nullptr),
+ line_start_(nullptr),
+ line_(0),
+ token_(-1),
+ attr_is_trivial_ascii_string_(true) {}
+
+ protected:
+ void ResetState(const char *source) {
+ cursor_ = source;
+ line_ = 0;
+ MarkNewLine();
+ }
+
+ void MarkNewLine() {
+ line_start_ = cursor_;
+ line_ += 1;
+ }
+
+ int64_t CursorPosition() const {
+ FLATBUFFERS_ASSERT(cursor_ && line_start_ && cursor_ >= line_start_);
+ return static_cast<int64_t>(cursor_ - line_start_);
+ }
+
+ const char *cursor_;
+ const char *line_start_;
+ int line_; // the current line being parsed
+ int token_;
+
+ // Flag: text in attribute_ is true ASCII string without escape
+ // sequences. Only printable ASCII (without [\t\r\n]).
+ // Used for number-in-string (and base64 string in future).
+ bool attr_is_trivial_ascii_string_;
+ std::string attribute_;
+ std::vector<std::string> doc_comment_;
+};
+
+// A way to make error propagation less error prone by requiring values to be
+// checked.
+// Once you create a value of this type you must either:
+// - Call Check() on it.
+// - Copy or assign it to another value.
+// Failure to do so leads to an assert.
+// This guarantees that this as return value cannot be ignored.
+class CheckedError {
+ public:
+ explicit CheckedError(bool error)
+ : is_error_(error), has_been_checked_(false) {}
+
+ CheckedError &operator=(const CheckedError &other) {
+ is_error_ = other.is_error_;
+ has_been_checked_ = false;
+ other.has_been_checked_ = true;
+ return *this;
+ }
+
+ CheckedError(const CheckedError &other) {
+ *this = other; // Use assignment operator.
+ }
+
+ ~CheckedError() { FLATBUFFERS_ASSERT(has_been_checked_); }
+
+ bool Check() {
+ has_been_checked_ = true;
+ return is_error_;
+ }
+
+ private:
+ bool is_error_;
+ mutable bool has_been_checked_;
+};
+
+// Additionally, in GCC we can get these errors statically, for additional
+// assurance:
+// clang-format off
+#ifdef __GNUC__
+#define FLATBUFFERS_CHECKED_ERROR CheckedError \
+ __attribute__((warn_unused_result))
+#else
+#define FLATBUFFERS_CHECKED_ERROR CheckedError
+#endif
+// clang-format on
+
+class Parser : public ParserState {
+ public:
+ explicit Parser(const IDLOptions &options = IDLOptions())
+ : current_namespace_(nullptr),
+ empty_namespace_(nullptr),
+ flex_builder_(256, flexbuffers::BUILDER_FLAG_SHARE_ALL),
+ root_struct_def_(nullptr),
+ opts(options),
+ uses_flexbuffers_(false),
+ advanced_features_(0),
+ source_(nullptr),
+ anonymous_counter_(0),
+ parse_depth_counter_(0) {
+ if (opts.force_defaults) { builder_.ForceDefaults(true); }
+ // Start out with the empty namespace being current.
+ empty_namespace_ = new Namespace();
+ namespaces_.push_back(empty_namespace_);
+ current_namespace_ = empty_namespace_;
+ known_attributes_["deprecated"] = true;
+ known_attributes_["required"] = true;
+ known_attributes_["key"] = true;
+ known_attributes_["shared"] = true;
+ known_attributes_["hash"] = true;
+ known_attributes_["id"] = true;
+ known_attributes_["force_align"] = true;
+ known_attributes_["bit_flags"] = true;
+ known_attributes_["original_order"] = true;
+ known_attributes_["nested_flatbuffer"] = true;
+ known_attributes_["csharp_partial"] = true;
+ known_attributes_["streaming"] = true;
+ known_attributes_["idempotent"] = true;
+ known_attributes_["cpp_type"] = true;
+ known_attributes_["cpp_ptr_type"] = true;
+ known_attributes_["cpp_ptr_type_get"] = true;
+ known_attributes_["cpp_str_type"] = true;
+ known_attributes_["cpp_str_flex_ctor"] = true;
+ known_attributes_["native_inline"] = true;
+ known_attributes_["native_custom_alloc"] = true;
+ known_attributes_["native_type"] = true;
+ known_attributes_["native_type_pack_name"] = true;
+ known_attributes_["native_default"] = true;
+ known_attributes_["flexbuffer"] = true;
+ known_attributes_["private"] = true;
+ }
+
+ ~Parser() {
+ for (auto it = namespaces_.begin(); it != namespaces_.end(); ++it) {
+ delete *it;
+ }
+ }
+
+ // Parse the string containing either schema or JSON data, which will
+ // populate the SymbolTable's or the FlatBufferBuilder above.
+ // include_paths is used to resolve any include statements, and typically
+ // should at least include the project path (where you loaded source_ from).
+ // include_paths must be nullptr terminated if specified.
+ // If include_paths is nullptr, it will attempt to load from the current
+ // directory.
+ // If the source was loaded from a file and isn't an include file,
+ // supply its name in source_filename.
+ // All paths specified in this call must be in posix format, if you accept
+ // paths from user input, please call PosixPath on them first.
+ bool Parse(const char *_source, const char **include_paths = nullptr,
+ const char *source_filename = nullptr);
+
+ bool ParseJson(const char *json, const char *json_filename = nullptr);
+
+ // Set the root type. May override the one set in the schema.
+ bool SetRootType(const char *name);
+
+ // Mark all definitions as already having code generated.
+ void MarkGenerated();
+
+ // Get the files recursively included by the given file. The returned
+ // container will have at least the given file.
+ std::set<std::string> GetIncludedFilesRecursive(
+ const std::string &file_name) const;
+
+ // Fills builder_ with a binary version of the schema parsed.
+ // See reflection/reflection.fbs
+ void Serialize();
+
+ // Deserialize a schema buffer
+ bool Deserialize(const uint8_t *buf, const size_t size);
+
+ // Fills internal structure as if the schema passed had been loaded by parsing
+ // with Parse except that included filenames will not be populated.
+ bool Deserialize(const reflection::Schema *schema);
+
+ Type *DeserializeType(const reflection::Type *type);
+
+ // Checks that the schema represented by this parser is a safe evolution
+ // of the schema provided. Returns non-empty error on any problems.
+ std::string ConformTo(const Parser &base);
+
+ // Similar to Parse(), but now only accepts JSON to be parsed into a
+ // FlexBuffer.
+ bool ParseFlexBuffer(const char *source, const char *source_filename,
+ flexbuffers::Builder *builder);
+
+ StructDef *LookupStruct(const std::string &id) const;
+ StructDef *LookupStructThruParentNamespaces(const std::string &id) const;
+
+ std::string UnqualifiedName(const std::string &fullQualifiedName);
+
+ FLATBUFFERS_CHECKED_ERROR Error(const std::string &msg);
+
+ // @brief Verify that any of 'opts.lang_to_generate' supports Optional scalars
+ // in a schema.
+ // @param opts Options used to parce a schema and generate code.
+ static bool SupportsOptionalScalars(const flatbuffers::IDLOptions &opts);
+
+ private:
+ class ParseDepthGuard;
+
+ void Message(const std::string &msg);
+ void Warning(const std::string &msg);
+ FLATBUFFERS_CHECKED_ERROR ParseHexNum(int nibbles, uint64_t *val);
+ FLATBUFFERS_CHECKED_ERROR Next();
+ FLATBUFFERS_CHECKED_ERROR SkipByteOrderMark();
+ bool Is(int t) const;
+ bool IsIdent(const char *id) const;
+ FLATBUFFERS_CHECKED_ERROR Expect(int t);
+ std::string TokenToStringId(int t) const;
+ EnumDef *LookupEnum(const std::string &id);
+ FLATBUFFERS_CHECKED_ERROR ParseNamespacing(std::string *id,
+ std::string *last);
+ FLATBUFFERS_CHECKED_ERROR ParseTypeIdent(Type &type);
+ FLATBUFFERS_CHECKED_ERROR ParseType(Type &type);
+ FLATBUFFERS_CHECKED_ERROR AddField(StructDef &struct_def,
+ const std::string &name, const Type &type,
+ FieldDef **dest);
+ FLATBUFFERS_CHECKED_ERROR ParseField(StructDef &struct_def);
+ FLATBUFFERS_CHECKED_ERROR ParseString(Value &val, bool use_string_pooling);
+ FLATBUFFERS_CHECKED_ERROR ParseComma();
+ FLATBUFFERS_CHECKED_ERROR ParseAnyValue(Value &val, FieldDef *field,
+ size_t parent_fieldn,
+ const StructDef *parent_struct_def,
+ uoffset_t count,
+ bool inside_vector = false);
+ template<typename F>
+ FLATBUFFERS_CHECKED_ERROR ParseTableDelimiters(size_t &fieldn,
+ const StructDef *struct_def,
+ F body);
+ FLATBUFFERS_CHECKED_ERROR ParseTable(const StructDef &struct_def,
+ std::string *value, uoffset_t *ovalue);
+ void SerializeStruct(const StructDef &struct_def, const Value &val);
+ void SerializeStruct(FlatBufferBuilder &builder, const StructDef &struct_def,
+ const Value &val);
+ template<typename F>
+ FLATBUFFERS_CHECKED_ERROR ParseVectorDelimiters(uoffset_t &count, F body);
+ FLATBUFFERS_CHECKED_ERROR ParseVector(const Type &type, uoffset_t *ovalue,
+ FieldDef *field, size_t fieldn);
+ FLATBUFFERS_CHECKED_ERROR ParseArray(Value &array);
+ FLATBUFFERS_CHECKED_ERROR ParseNestedFlatbuffer(
+ Value &val, FieldDef *field, size_t fieldn,
+ const StructDef *parent_struct_def);
+ FLATBUFFERS_CHECKED_ERROR ParseMetaData(SymbolTable<Value> *attributes);
+ FLATBUFFERS_CHECKED_ERROR TryTypedValue(const std::string *name, int dtoken,
+ bool check, Value &e, BaseType req,
+ bool *destmatch);
+ FLATBUFFERS_CHECKED_ERROR ParseHash(Value &e, FieldDef *field);
+ FLATBUFFERS_CHECKED_ERROR TokenError();
+ FLATBUFFERS_CHECKED_ERROR ParseSingleValue(const std::string *name, Value &e,
+ bool check_now);
+ FLATBUFFERS_CHECKED_ERROR ParseFunction(const std::string *name, Value &e);
+ FLATBUFFERS_CHECKED_ERROR ParseEnumFromString(const Type &type,
+ std::string *result);
+ StructDef *LookupCreateStruct(const std::string &name,
+ bool create_if_new = true,
+ bool definition = false);
+ FLATBUFFERS_CHECKED_ERROR ParseEnum(bool is_union, EnumDef **dest);
+ FLATBUFFERS_CHECKED_ERROR ParseNamespace();
+ FLATBUFFERS_CHECKED_ERROR StartStruct(const std::string &name,
+ StructDef **dest);
+ FLATBUFFERS_CHECKED_ERROR StartEnum(const std::string &name, bool is_union,
+ EnumDef **dest);
+ FLATBUFFERS_CHECKED_ERROR ParseDecl();
+ FLATBUFFERS_CHECKED_ERROR ParseService();
+ FLATBUFFERS_CHECKED_ERROR ParseProtoFields(StructDef *struct_def,
+ bool isextend, bool inside_oneof);
+ FLATBUFFERS_CHECKED_ERROR ParseProtoOption();
+ FLATBUFFERS_CHECKED_ERROR ParseProtoKey();
+ FLATBUFFERS_CHECKED_ERROR ParseProtoDecl();
+ FLATBUFFERS_CHECKED_ERROR ParseProtoCurliesOrIdent();
+ FLATBUFFERS_CHECKED_ERROR ParseTypeFromProtoType(Type *type);
+ FLATBUFFERS_CHECKED_ERROR SkipAnyJsonValue();
+ FLATBUFFERS_CHECKED_ERROR ParseFlexBufferNumericConstant(
+ flexbuffers::Builder *builder);
+ FLATBUFFERS_CHECKED_ERROR ParseFlexBufferValue(flexbuffers::Builder *builder);
+ FLATBUFFERS_CHECKED_ERROR StartParseFile(const char *source,
+ const char *source_filename);
+ FLATBUFFERS_CHECKED_ERROR ParseRoot(const char *_source,
+ const char **include_paths,
+ const char *source_filename);
+ FLATBUFFERS_CHECKED_ERROR DoParse(const char *_source,
+ const char **include_paths,
+ const char *source_filename,
+ const char *include_filename);
+ FLATBUFFERS_CHECKED_ERROR DoParseJson();
+ FLATBUFFERS_CHECKED_ERROR CheckClash(std::vector<FieldDef *> &fields,
+ StructDef *struct_def,
+ const char *suffix, BaseType baseType);
+ FLATBUFFERS_CHECKED_ERROR ParseAlignAttribute(
+ const std::string &align_constant, size_t min_align, size_t *align);
+
+ bool SupportsAdvancedUnionFeatures() const;
+ bool SupportsAdvancedArrayFeatures() const;
+ bool SupportsOptionalScalars() const;
+ bool SupportsDefaultVectorsAndStrings() const;
+ Namespace *UniqueNamespace(Namespace *ns);
+
+ FLATBUFFERS_CHECKED_ERROR RecurseError();
+ template<typename F> CheckedError Recurse(F f);
+
+ public:
+ SymbolTable<Type> types_;
+ SymbolTable<StructDef> structs_;
+ SymbolTable<EnumDef> enums_;
+ SymbolTable<ServiceDef> services_;
+ std::vector<Namespace *> namespaces_;
+ Namespace *current_namespace_;
+ Namespace *empty_namespace_;
+ std::string error_; // User readable error_ if Parse() == false
+
+ FlatBufferBuilder builder_; // any data contained in the file
+ flexbuffers::Builder flex_builder_;
+ flexbuffers::Reference flex_root_;
+ StructDef *root_struct_def_;
+ std::string file_identifier_;
+ std::string file_extension_;
+
+ std::map<uint64_t, std::string> included_files_;
+ std::map<std::string, std::set<std::string>> files_included_per_file_;
+ std::vector<std::string> native_included_files_;
+
+ std::map<std::string, bool> known_attributes_;
+
+ IDLOptions opts;
+ bool uses_flexbuffers_;
+
+ uint64_t advanced_features_;
+
+ private:
+ const char *source_;
+
+ std::string file_being_parsed_;
+
+ std::vector<std::pair<Value, FieldDef *>> field_stack_;
+
+ int anonymous_counter_;
+ int parse_depth_counter_; // stack-overflow guard
+};
+
+// Utility functions for multiple generators:
+
+extern std::string MakeCamel(const std::string &in, bool first = true);
+
+extern std::string MakeScreamingCamel(const std::string &in);
+
+// Generate text (JSON) from a given FlatBuffer, and a given Parser
+// object that has been populated with the corresponding schema.
+// If ident_step is 0, no indentation will be generated. Additionally,
+// if it is less than 0, no linefeeds will be generated either.
+// See idl_gen_text.cpp.
+// strict_json adds "quotes" around field names if true.
+// If the flatbuffer cannot be encoded in JSON (e.g., it contains non-UTF-8
+// byte arrays in String values), returns false.
+extern bool GenerateTextFromTable(const Parser &parser, const void *table,
+ const std::string &tablename,
+ std::string *text);
+extern bool GenerateText(const Parser &parser, const void *flatbuffer,
+ std::string *text);
+extern bool GenerateTextFile(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Json schema to string
+// See idl_gen_json_schema.cpp.
+extern bool GenerateJsonSchema(const Parser &parser, std::string *json);
+
+// Generate binary files from a given FlatBuffer, and a given Parser
+// object that has been populated with the corresponding schema.
+// See code_generators.cpp.
+extern bool GenerateBinary(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a C++ header from the definitions in the Parser object.
+// See idl_gen_cpp.
+extern bool GenerateCPP(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate C# files from the definitions in the Parser object.
+// See idl_gen_csharp.cpp.
+extern bool GenerateCSharp(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+extern bool GenerateDart(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Java files from the definitions in the Parser object.
+// See idl_gen_java.cpp.
+extern bool GenerateJava(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate JavaScript or TypeScript code from the definitions in the Parser
+// object. See idl_gen_js.
+extern bool GenerateTS(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Go files from the definitions in the Parser object.
+// See idl_gen_go.cpp.
+extern bool GenerateGo(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Php code from the definitions in the Parser object.
+// See idl_gen_php.
+extern bool GeneratePhp(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Python files from the definitions in the Parser object.
+// See idl_gen_python.cpp.
+extern bool GeneratePython(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Lobster files from the definitions in the Parser object.
+// See idl_gen_lobster.cpp.
+extern bool GenerateLobster(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Lua files from the definitions in the Parser object.
+// See idl_gen_lua.cpp.
+extern bool GenerateLua(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Rust files from the definitions in the Parser object.
+// See idl_gen_rust.cpp.
+extern bool GenerateRust(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Json schema file
+// See idl_gen_json_schema.cpp.
+extern bool GenerateJsonSchema(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+extern bool GenerateKotlin(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate Swift classes.
+// See idl_gen_swift.cpp
+extern bool GenerateSwift(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a schema file from the internal representation, useful after
+// parsing a .proto schema.
+extern std::string GenerateFBS(const Parser &parser,
+ const std::string &file_name);
+extern bool GenerateFBS(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a C++ header for reading with templated file iterator from
+// the definitions in the Parser object.
+// See idl_gen_cpp_yandex_maps_iter.cpp.
+extern std::string GenerateCPPYandexMapsIter(const Parser &parser,
+ const std::string &include_guard_ident);
+extern bool GenerateCPPYandexMapsIter(const Parser &parser,
+ const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for the generated TypeScript code.
+// See idl_gen_ts.cpp.
+extern std::string TSMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for the generated C++ header.
+// See idl_gen_cpp.cpp.
+extern std::string CPPMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for the generated Dart code
+// see idl_gen_dart.cpp
+extern std::string DartMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for the generated Rust code.
+// See idl_gen_rust.cpp.
+extern std::string RustMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for generated Java or C# files.
+// See code_generators.cpp.
+extern std::string JavaCSharpMakeRule(const Parser &parser,
+ const std::string &path,
+ const std::string &file_name);
+
+// Generate a make rule for the generated text (JSON) files.
+// See idl_gen_text.cpp.
+extern std::string TextMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_names);
+
+// Generate a make rule for the generated binary files.
+// See code_generators.cpp.
+extern std::string BinaryMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate GRPC Cpp interfaces.
+// See idl_gen_grpc.cpp.
+bool GenerateCppGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate GRPC Go interfaces.
+// See idl_gen_grpc.cpp.
+bool GenerateGoGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate GRPC Java classes.
+// See idl_gen_grpc.cpp
+bool GenerateJavaGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate GRPC Python interfaces.
+// See idl_gen_grpc.cpp.
+bool GeneratePythonGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+// Generate GRPC Swift interfaces.
+// See idl_gen_grpc.cpp.
+extern bool GenerateSwiftGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+
+extern bool GenerateTSGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name);
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_IDL_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/reflection.h b/contrib/libs/flatbuffers/include/flatbuffers/reflection.h
new file mode 100644
index 0000000000..d268a3ffea
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/reflection.h
@@ -0,0 +1,502 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_REFLECTION_H_
+#define FLATBUFFERS_REFLECTION_H_
+
+// This is somewhat of a circular dependency because flatc (and thus this
+// file) is needed to generate this header in the first place.
+// Should normally not be a problem since it can be generated by the
+// previous version of flatc whenever this code needs to change.
+// See reflection/generate_code.sh
+#include "reflection_generated.h"
+
+// Helper functionality for reflection.
+
+namespace flatbuffers {
+
+// ------------------------- GETTERS -------------------------
+
+inline bool IsScalar(reflection::BaseType t) {
+ return t >= reflection::UType && t <= reflection::Double;
+}
+inline bool IsInteger(reflection::BaseType t) {
+ return t >= reflection::UType && t <= reflection::ULong;
+}
+inline bool IsFloat(reflection::BaseType t) {
+ return t == reflection::Float || t == reflection::Double;
+}
+inline bool IsLong(reflection::BaseType t) {
+ return t == reflection::Long || t == reflection::ULong;
+}
+
+// Size of a basic type, don't use with structs.
+inline size_t GetTypeSize(reflection::BaseType base_type) {
+ // This needs to correspond to the BaseType enum.
+ static size_t sizes[] = {
+ 0, // None
+ 1, // UType
+ 1, // Bool
+ 1, // Byte
+ 1, // UByte
+ 2, // Short
+ 2, // UShort
+ 4, // Int
+ 4, // UInt
+ 8, // Long
+ 8, // ULong
+ 4, // Float
+ 8, // Double
+ 4, // String
+ 4, // Vector
+ 4, // Obj
+ 4, // Union
+ 0, // Array. Only used in structs. 0 was chosen to prevent out-of-bounds
+ // errors.
+
+ 0 // MaxBaseType. This must be kept the last entry in this array.
+ };
+ static_assert(sizeof(sizes) / sizeof(size_t) == reflection::MaxBaseType + 1,
+ "Size of sizes[] array does not match the count of BaseType "
+ "enum values.");
+ return sizes[base_type];
+}
+
+// Same as above, but now correctly returns the size of a struct if
+// the field (or vector element) is a struct.
+inline size_t GetTypeSizeInline(reflection::BaseType base_type, int type_index,
+ const reflection::Schema &schema) {
+ if (base_type == reflection::Obj &&
+ schema.objects()->Get(type_index)->is_struct()) {
+ return schema.objects()->Get(type_index)->bytesize();
+ } else {
+ return GetTypeSize(base_type);
+ }
+}
+
+// Get the root, regardless of what type it is.
+inline Table *GetAnyRoot(uint8_t *flatbuf) {
+ return GetMutableRoot<Table>(flatbuf);
+}
+inline const Table *GetAnyRoot(const uint8_t *flatbuf) {
+ return GetRoot<Table>(flatbuf);
+}
+
+// Get a field's default, if you know it's an integer, and its exact type.
+template<typename T> T GetFieldDefaultI(const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(sizeof(T) == GetTypeSize(field.type()->base_type()));
+ return static_cast<T>(field.default_integer());
+}
+
+// Get a field's default, if you know it's floating point and its exact type.
+template<typename T> T GetFieldDefaultF(const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(sizeof(T) == GetTypeSize(field.type()->base_type()));
+ return static_cast<T>(field.default_real());
+}
+
+// Get a field, if you know it's an integer, and its exact type.
+template<typename T>
+T GetFieldI(const Table &table, const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(sizeof(T) == GetTypeSize(field.type()->base_type()));
+ return table.GetField<T>(field.offset(),
+ static_cast<T>(field.default_integer()));
+}
+
+// Get a field, if you know it's floating point and its exact type.
+template<typename T>
+T GetFieldF(const Table &table, const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(sizeof(T) == GetTypeSize(field.type()->base_type()));
+ return table.GetField<T>(field.offset(),
+ static_cast<T>(field.default_real()));
+}
+
+// Get a field, if you know it's a string.
+inline const String *GetFieldS(const Table &table,
+ const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(field.type()->base_type() == reflection::String);
+ return table.GetPointer<const String *>(field.offset());
+}
+
+// Get a field, if you know it's a vector.
+template<typename T>
+Vector<T> *GetFieldV(const Table &table, const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(field.type()->base_type() == reflection::Vector &&
+ sizeof(T) == GetTypeSize(field.type()->element()));
+ return table.GetPointer<Vector<T> *>(field.offset());
+}
+
+// Get a field, if you know it's a vector, generically.
+// To actually access elements, use the return value together with
+// field.type()->element() in any of GetAnyVectorElemI below etc.
+inline VectorOfAny *GetFieldAnyV(const Table &table,
+ const reflection::Field &field) {
+ return table.GetPointer<VectorOfAny *>(field.offset());
+}
+
+// Get a field, if you know it's a table.
+inline Table *GetFieldT(const Table &table, const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(field.type()->base_type() == reflection::Obj ||
+ field.type()->base_type() == reflection::Union);
+ return table.GetPointer<Table *>(field.offset());
+}
+
+// Get a field, if you know it's a struct.
+inline const Struct *GetFieldStruct(const Table &table,
+ const reflection::Field &field) {
+ // TODO: This does NOT check if the field is a table or struct, but we'd need
+ // access to the schema to check the is_struct flag.
+ FLATBUFFERS_ASSERT(field.type()->base_type() == reflection::Obj);
+ return table.GetStruct<const Struct *>(field.offset());
+}
+
+// Get a structure's field, if you know it's a struct.
+inline const Struct *GetFieldStruct(const Struct &structure,
+ const reflection::Field &field) {
+ FLATBUFFERS_ASSERT(field.type()->base_type() == reflection::Obj);
+ return structure.GetStruct<const Struct *>(field.offset());
+}
+
+// Raw helper functions used below: get any value in memory as a 64bit int, a
+// double or a string.
+// All scalars get static_cast to an int64_t, strings use strtoull, every other
+// data type returns 0.
+int64_t GetAnyValueI(reflection::BaseType type, const uint8_t *data);
+// All scalars static cast to double, strings use strtod, every other data
+// type is 0.0.
+double GetAnyValueF(reflection::BaseType type, const uint8_t *data);
+// All scalars converted using stringstream, strings as-is, and all other
+// data types provide some level of debug-pretty-printing.
+std::string GetAnyValueS(reflection::BaseType type, const uint8_t *data,
+ const reflection::Schema *schema, int type_index);
+
+// Get any table field as a 64bit int, regardless of what type it is.
+inline int64_t GetAnyFieldI(const Table &table,
+ const reflection::Field &field) {
+ auto field_ptr = table.GetAddressOf(field.offset());
+ return field_ptr ? GetAnyValueI(field.type()->base_type(), field_ptr)
+ : field.default_integer();
+}
+
+// Get any table field as a double, regardless of what type it is.
+inline double GetAnyFieldF(const Table &table, const reflection::Field &field) {
+ auto field_ptr = table.GetAddressOf(field.offset());
+ return field_ptr ? GetAnyValueF(field.type()->base_type(), field_ptr)
+ : field.default_real();
+}
+
+// Get any table field as a string, regardless of what type it is.
+// You may pass nullptr for the schema if you don't care to have fields that
+// are of table type pretty-printed.
+inline std::string GetAnyFieldS(const Table &table,
+ const reflection::Field &field,
+ const reflection::Schema *schema) {
+ auto field_ptr = table.GetAddressOf(field.offset());
+ return field_ptr ? GetAnyValueS(field.type()->base_type(), field_ptr, schema,
+ field.type()->index())
+ : "";
+}
+
+// Get any struct field as a 64bit int, regardless of what type it is.
+inline int64_t GetAnyFieldI(const Struct &st, const reflection::Field &field) {
+ return GetAnyValueI(field.type()->base_type(),
+ st.GetAddressOf(field.offset()));
+}
+
+// Get any struct field as a double, regardless of what type it is.
+inline double GetAnyFieldF(const Struct &st, const reflection::Field &field) {
+ return GetAnyValueF(field.type()->base_type(),
+ st.GetAddressOf(field.offset()));
+}
+
+// Get any struct field as a string, regardless of what type it is.
+inline std::string GetAnyFieldS(const Struct &st,
+ const reflection::Field &field) {
+ return GetAnyValueS(field.type()->base_type(),
+ st.GetAddressOf(field.offset()), nullptr, -1);
+}
+
+// Get any vector element as a 64bit int, regardless of what type it is.
+inline int64_t GetAnyVectorElemI(const VectorOfAny *vec,
+ reflection::BaseType elem_type, size_t i) {
+ return GetAnyValueI(elem_type, vec->Data() + GetTypeSize(elem_type) * i);
+}
+
+// Get any vector element as a double, regardless of what type it is.
+inline double GetAnyVectorElemF(const VectorOfAny *vec,
+ reflection::BaseType elem_type, size_t i) {
+ return GetAnyValueF(elem_type, vec->Data() + GetTypeSize(elem_type) * i);
+}
+
+// Get any vector element as a string, regardless of what type it is.
+inline std::string GetAnyVectorElemS(const VectorOfAny *vec,
+ reflection::BaseType elem_type, size_t i) {
+ return GetAnyValueS(elem_type, vec->Data() + GetTypeSize(elem_type) * i,
+ nullptr, -1);
+}
+
+// Get a vector element that's a table/string/vector from a generic vector.
+// Pass Table/String/VectorOfAny as template parameter.
+// Warning: does no typechecking.
+template<typename T>
+T *GetAnyVectorElemPointer(const VectorOfAny *vec, size_t i) {
+ auto elem_ptr = vec->Data() + sizeof(uoffset_t) * i;
+ return reinterpret_cast<T *>(elem_ptr + ReadScalar<uoffset_t>(elem_ptr));
+}
+
+// Get the inline-address of a vector element. Useful for Structs (pass Struct
+// as template arg), or being able to address a range of scalars in-line.
+// Get elem_size from GetTypeSizeInline().
+// Note: little-endian data on all platforms, use EndianScalar() instead of
+// raw pointer access with scalars).
+template<typename T>
+T *GetAnyVectorElemAddressOf(const VectorOfAny *vec, size_t i,
+ size_t elem_size) {
+ return reinterpret_cast<T *>(vec->Data() + elem_size * i);
+}
+
+// Similarly, for elements of tables.
+template<typename T>
+T *GetAnyFieldAddressOf(const Table &table, const reflection::Field &field) {
+ return reinterpret_cast<T *>(table.GetAddressOf(field.offset()));
+}
+
+// Similarly, for elements of structs.
+template<typename T>
+T *GetAnyFieldAddressOf(const Struct &st, const reflection::Field &field) {
+ return reinterpret_cast<T *>(st.GetAddressOf(field.offset()));
+}
+
+// ------------------------- SETTERS -------------------------
+
+// Set any scalar field, if you know its exact type.
+template<typename T>
+bool SetField(Table *table, const reflection::Field &field, T val) {
+ reflection::BaseType type = field.type()->base_type();
+ if (!IsScalar(type)) { return false; }
+ FLATBUFFERS_ASSERT(sizeof(T) == GetTypeSize(type));
+ T def;
+ if (IsInteger(type)) {
+ def = GetFieldDefaultI<T>(field);
+ } else {
+ FLATBUFFERS_ASSERT(IsFloat(type));
+ def = GetFieldDefaultF<T>(field);
+ }
+ return table->SetField(field.offset(), val, def);
+}
+
+// Raw helper functions used below: set any value in memory as a 64bit int, a
+// double or a string.
+// These work for all scalar values, but do nothing for other data types.
+// To set a string, see SetString below.
+void SetAnyValueI(reflection::BaseType type, uint8_t *data, int64_t val);
+void SetAnyValueF(reflection::BaseType type, uint8_t *data, double val);
+void SetAnyValueS(reflection::BaseType type, uint8_t *data, const char *val);
+
+// Set any table field as a 64bit int, regardless of type what it is.
+inline bool SetAnyFieldI(Table *table, const reflection::Field &field,
+ int64_t val) {
+ auto field_ptr = table->GetAddressOf(field.offset());
+ if (!field_ptr) return val == GetFieldDefaultI<int64_t>(field);
+ SetAnyValueI(field.type()->base_type(), field_ptr, val);
+ return true;
+}
+
+// Set any table field as a double, regardless of what type it is.
+inline bool SetAnyFieldF(Table *table, const reflection::Field &field,
+ double val) {
+ auto field_ptr = table->GetAddressOf(field.offset());
+ if (!field_ptr) return val == GetFieldDefaultF<double>(field);
+ SetAnyValueF(field.type()->base_type(), field_ptr, val);
+ return true;
+}
+
+// Set any table field as a string, regardless of what type it is.
+inline bool SetAnyFieldS(Table *table, const reflection::Field &field,
+ const char *val) {
+ auto field_ptr = table->GetAddressOf(field.offset());
+ if (!field_ptr) return false;
+ SetAnyValueS(field.type()->base_type(), field_ptr, val);
+ return true;
+}
+
+// Set any struct field as a 64bit int, regardless of type what it is.
+inline void SetAnyFieldI(Struct *st, const reflection::Field &field,
+ int64_t val) {
+ SetAnyValueI(field.type()->base_type(), st->GetAddressOf(field.offset()),
+ val);
+}
+
+// Set any struct field as a double, regardless of type what it is.
+inline void SetAnyFieldF(Struct *st, const reflection::Field &field,
+ double val) {
+ SetAnyValueF(field.type()->base_type(), st->GetAddressOf(field.offset()),
+ val);
+}
+
+// Set any struct field as a string, regardless of type what it is.
+inline void SetAnyFieldS(Struct *st, const reflection::Field &field,
+ const char *val) {
+ SetAnyValueS(field.type()->base_type(), st->GetAddressOf(field.offset()),
+ val);
+}
+
+// Set any vector element as a 64bit int, regardless of type what it is.
+inline void SetAnyVectorElemI(VectorOfAny *vec, reflection::BaseType elem_type,
+ size_t i, int64_t val) {
+ SetAnyValueI(elem_type, vec->Data() + GetTypeSize(elem_type) * i, val);
+}
+
+// Set any vector element as a double, regardless of type what it is.
+inline void SetAnyVectorElemF(VectorOfAny *vec, reflection::BaseType elem_type,
+ size_t i, double val) {
+ SetAnyValueF(elem_type, vec->Data() + GetTypeSize(elem_type) * i, val);
+}
+
+// Set any vector element as a string, regardless of type what it is.
+inline void SetAnyVectorElemS(VectorOfAny *vec, reflection::BaseType elem_type,
+ size_t i, const char *val) {
+ SetAnyValueS(elem_type, vec->Data() + GetTypeSize(elem_type) * i, val);
+}
+
+// ------------------------- RESIZING SETTERS -------------------------
+
+// "smart" pointer for use with resizing vectors: turns a pointer inside
+// a vector into a relative offset, such that it is not affected by resizes.
+template<typename T, typename U> class pointer_inside_vector {
+ public:
+ pointer_inside_vector(T *ptr, std::vector<U> &vec)
+ : offset_(reinterpret_cast<uint8_t *>(ptr) -
+ reinterpret_cast<uint8_t *>(flatbuffers::vector_data(vec))),
+ vec_(vec) {}
+
+ T *operator*() const {
+ return reinterpret_cast<T *>(
+ reinterpret_cast<uint8_t *>(flatbuffers::vector_data(vec_)) + offset_);
+ }
+ T *operator->() const { return operator*(); }
+
+ private:
+ size_t offset_;
+ std::vector<U> &vec_;
+};
+
+// Helper to create the above easily without specifying template args.
+template<typename T, typename U>
+pointer_inside_vector<T, U> piv(T *ptr, std::vector<U> &vec) {
+ return pointer_inside_vector<T, U>(ptr, vec);
+}
+
+inline const char *UnionTypeFieldSuffix() { return "_type"; }
+
+// Helper to figure out the actual table type a union refers to.
+inline const reflection::Object &GetUnionType(
+ const reflection::Schema &schema, const reflection::Object &parent,
+ const reflection::Field &unionfield, const Table &table) {
+ auto enumdef = schema.enums()->Get(unionfield.type()->index());
+ // TODO: this is clumsy and slow, but no other way to find it?
+ auto type_field = parent.fields()->LookupByKey(
+ (unionfield.name()->str() + UnionTypeFieldSuffix()).c_str());
+ FLATBUFFERS_ASSERT(type_field);
+ auto union_type = GetFieldI<uint8_t>(table, *type_field);
+ auto enumval = enumdef->values()->LookupByKey(union_type);
+ return *enumval->object();
+}
+
+// Changes the contents of a string inside a FlatBuffer. FlatBuffer must
+// live inside a std::vector so we can resize the buffer if needed.
+// "str" must live inside "flatbuf" and may be invalidated after this call.
+// If your FlatBuffer's root table is not the schema's root table, you should
+// pass in your root_table type as well.
+void SetString(const reflection::Schema &schema, const std::string &val,
+ const String *str, std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table = nullptr);
+
+// Resizes a flatbuffers::Vector inside a FlatBuffer. FlatBuffer must
+// live inside a std::vector so we can resize the buffer if needed.
+// "vec" must live inside "flatbuf" and may be invalidated after this call.
+// If your FlatBuffer's root table is not the schema's root table, you should
+// pass in your root_table type as well.
+uint8_t *ResizeAnyVector(const reflection::Schema &schema, uoffset_t newsize,
+ const VectorOfAny *vec, uoffset_t num_elems,
+ uoffset_t elem_size, std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table = nullptr);
+
+template<typename T>
+void ResizeVector(const reflection::Schema &schema, uoffset_t newsize, T val,
+ const Vector<T> *vec, std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table = nullptr) {
+ auto delta_elem = static_cast<int>(newsize) - static_cast<int>(vec->size());
+ auto newelems = ResizeAnyVector(
+ schema, newsize, reinterpret_cast<const VectorOfAny *>(vec), vec->size(),
+ static_cast<uoffset_t>(sizeof(T)), flatbuf, root_table);
+ // Set new elements to "val".
+ for (int i = 0; i < delta_elem; i++) {
+ auto loc = newelems + i * sizeof(T);
+ auto is_scalar = flatbuffers::is_scalar<T>::value;
+ if (is_scalar) {
+ WriteScalar(loc, val);
+ } else { // struct
+ *reinterpret_cast<T *>(loc) = val;
+ }
+ }
+}
+
+// Adds any new data (in the form of a new FlatBuffer) to an existing
+// FlatBuffer. This can be used when any of the above methods are not
+// sufficient, in particular for adding new tables and new fields.
+// This is potentially slightly less efficient than a FlatBuffer constructed
+// in one piece, since the new FlatBuffer doesn't share any vtables with the
+// existing one.
+// The return value can now be set using Vector::MutateOffset or SetFieldT
+// below.
+const uint8_t *AddFlatBuffer(std::vector<uint8_t> &flatbuf,
+ const uint8_t *newbuf, size_t newlen);
+
+inline bool SetFieldT(Table *table, const reflection::Field &field,
+ const uint8_t *val) {
+ FLATBUFFERS_ASSERT(sizeof(uoffset_t) ==
+ GetTypeSize(field.type()->base_type()));
+ return table->SetPointer(field.offset(), val);
+}
+
+// ------------------------- COPYING -------------------------
+
+// Generic copying of tables from a FlatBuffer into a FlatBuffer builder.
+// Can be used to do any kind of merging/selecting you may want to do out
+// of existing buffers. Also useful to reconstruct a whole buffer if the
+// above resizing functionality has introduced garbage in a buffer you want
+// to remove.
+// Note: this does not deal with DAGs correctly. If the table passed forms a
+// DAG, the copy will be a tree instead (with duplicates). Strings can be
+// shared however, by passing true for use_string_pooling.
+
+Offset<const Table *> CopyTable(FlatBufferBuilder &fbb,
+ const reflection::Schema &schema,
+ const reflection::Object &objectdef,
+ const Table &table,
+ bool use_string_pooling = false);
+
+// Verifies the provided flatbuffer using reflection.
+// root should point to the root type for this flatbuffer.
+// buf should point to the start of flatbuffer data.
+// length specifies the size of the flatbuffer data.
+bool Verify(const reflection::Schema &schema, const reflection::Object &root,
+ const uint8_t *buf, size_t length, uoffset_t max_depth = 64,
+ uoffset_t max_tables = 1000000);
+
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_REFLECTION_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/reflection_generated.h b/contrib/libs/flatbuffers/include/flatbuffers/reflection_generated.h
new file mode 100644
index 0000000000..93dc4b88b7
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/reflection_generated.h
@@ -0,0 +1,1278 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+
+#ifndef FLATBUFFERS_GENERATED_REFLECTION_REFLECTION_H_
+#define FLATBUFFERS_GENERATED_REFLECTION_REFLECTION_H_
+
+#include "flatbuffers.h"
+
+namespace reflection {
+
+struct Type;
+struct TypeBuilder;
+
+struct KeyValue;
+struct KeyValueBuilder;
+
+struct EnumVal;
+struct EnumValBuilder;
+
+struct Enum;
+struct EnumBuilder;
+
+struct Field;
+struct FieldBuilder;
+
+struct Object;
+struct ObjectBuilder;
+
+struct RPCCall;
+struct RPCCallBuilder;
+
+struct Service;
+struct ServiceBuilder;
+
+struct Schema;
+struct SchemaBuilder;
+
+enum BaseType {
+ None = 0,
+ UType = 1,
+ Bool = 2,
+ Byte = 3,
+ UByte = 4,
+ Short = 5,
+ UShort = 6,
+ Int = 7,
+ UInt = 8,
+ Long = 9,
+ ULong = 10,
+ Float = 11,
+ Double = 12,
+ String = 13,
+ Vector = 14,
+ Obj = 15,
+ Union = 16,
+ Array = 17,
+ MaxBaseType = 18
+};
+
+inline const BaseType (&EnumValuesBaseType())[19] {
+ static const BaseType values[] = {
+ None,
+ UType,
+ Bool,
+ Byte,
+ UByte,
+ Short,
+ UShort,
+ Int,
+ UInt,
+ Long,
+ ULong,
+ Float,
+ Double,
+ String,
+ Vector,
+ Obj,
+ Union,
+ Array,
+ MaxBaseType
+ };
+ return values;
+}
+
+inline const char * const *EnumNamesBaseType() {
+ static const char * const names[20] = {
+ "None",
+ "UType",
+ "Bool",
+ "Byte",
+ "UByte",
+ "Short",
+ "UShort",
+ "Int",
+ "UInt",
+ "Long",
+ "ULong",
+ "Float",
+ "Double",
+ "String",
+ "Vector",
+ "Obj",
+ "Union",
+ "Array",
+ "MaxBaseType",
+ nullptr
+ };
+ return names;
+}
+
+inline const char *EnumNameBaseType(BaseType e) {
+ if (flatbuffers::IsOutRange(e, None, MaxBaseType)) return "";
+ const size_t index = static_cast<size_t>(e);
+ return EnumNamesBaseType()[index];
+}
+
+enum AdvancedFeatures {
+ AdvancedArrayFeatures = 1ULL,
+ AdvancedUnionFeatures = 2ULL,
+ OptionalScalars = 4ULL,
+ DefaultVectorsAndStrings = 8ULL
+};
+
+inline const AdvancedFeatures (&EnumValuesAdvancedFeatures())[4] {
+ static const AdvancedFeatures values[] = {
+ AdvancedArrayFeatures,
+ AdvancedUnionFeatures,
+ OptionalScalars,
+ DefaultVectorsAndStrings
+ };
+ return values;
+}
+
+inline const char * const *EnumNamesAdvancedFeatures() {
+ static const char * const names[9] = {
+ "AdvancedArrayFeatures",
+ "AdvancedUnionFeatures",
+ "",
+ "OptionalScalars",
+ "",
+ "",
+ "",
+ "DefaultVectorsAndStrings",
+ nullptr
+ };
+ return names;
+}
+
+inline const char *EnumNameAdvancedFeatures(AdvancedFeatures e) {
+ if (flatbuffers::IsOutRange(e, AdvancedArrayFeatures, DefaultVectorsAndStrings)) return "";
+ const size_t index = static_cast<size_t>(e) - static_cast<size_t>(AdvancedArrayFeatures);
+ return EnumNamesAdvancedFeatures()[index];
+}
+
+struct Type FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef TypeBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_BASE_TYPE = 4,
+ VT_ELEMENT = 6,
+ VT_INDEX = 8,
+ VT_FIXED_LENGTH = 10
+ };
+ reflection::BaseType base_type() const {
+ return static_cast<reflection::BaseType>(GetField<int8_t>(VT_BASE_TYPE, 0));
+ }
+ reflection::BaseType element() const {
+ return static_cast<reflection::BaseType>(GetField<int8_t>(VT_ELEMENT, 0));
+ }
+ int32_t index() const {
+ return GetField<int32_t>(VT_INDEX, -1);
+ }
+ uint16_t fixed_length() const {
+ return GetField<uint16_t>(VT_FIXED_LENGTH, 0);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyField<int8_t>(verifier, VT_BASE_TYPE) &&
+ VerifyField<int8_t>(verifier, VT_ELEMENT) &&
+ VerifyField<int32_t>(verifier, VT_INDEX) &&
+ VerifyField<uint16_t>(verifier, VT_FIXED_LENGTH) &&
+ verifier.EndTable();
+ }
+};
+
+struct TypeBuilder {
+ typedef Type Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_base_type(reflection::BaseType base_type) {
+ fbb_.AddElement<int8_t>(Type::VT_BASE_TYPE, static_cast<int8_t>(base_type), 0);
+ }
+ void add_element(reflection::BaseType element) {
+ fbb_.AddElement<int8_t>(Type::VT_ELEMENT, static_cast<int8_t>(element), 0);
+ }
+ void add_index(int32_t index) {
+ fbb_.AddElement<int32_t>(Type::VT_INDEX, index, -1);
+ }
+ void add_fixed_length(uint16_t fixed_length) {
+ fbb_.AddElement<uint16_t>(Type::VT_FIXED_LENGTH, fixed_length, 0);
+ }
+ explicit TypeBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Type> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Type>(end);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Type> CreateType(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ reflection::BaseType base_type = reflection::None,
+ reflection::BaseType element = reflection::None,
+ int32_t index = -1,
+ uint16_t fixed_length = 0) {
+ TypeBuilder builder_(_fbb);
+ builder_.add_index(index);
+ builder_.add_fixed_length(fixed_length);
+ builder_.add_element(element);
+ builder_.add_base_type(base_type);
+ return builder_.Finish();
+}
+
+struct KeyValue FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef KeyValueBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_KEY = 4,
+ VT_VALUE = 6
+ };
+ const flatbuffers::String *key() const {
+ return GetPointer<const flatbuffers::String *>(VT_KEY);
+ }
+ bool KeyCompareLessThan(const KeyValue *o) const {
+ return *key() < *o->key();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(key()->c_str(), val);
+ }
+ const flatbuffers::String *value() const {
+ return GetPointer<const flatbuffers::String *>(VT_VALUE);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_KEY) &&
+ verifier.VerifyString(key()) &&
+ VerifyOffset(verifier, VT_VALUE) &&
+ verifier.VerifyString(value()) &&
+ verifier.EndTable();
+ }
+};
+
+struct KeyValueBuilder {
+ typedef KeyValue Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_key(flatbuffers::Offset<flatbuffers::String> key) {
+ fbb_.AddOffset(KeyValue::VT_KEY, key);
+ }
+ void add_value(flatbuffers::Offset<flatbuffers::String> value) {
+ fbb_.AddOffset(KeyValue::VT_VALUE, value);
+ }
+ explicit KeyValueBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<KeyValue> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<KeyValue>(end);
+ fbb_.Required(o, KeyValue::VT_KEY);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<KeyValue> CreateKeyValue(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> key = 0,
+ flatbuffers::Offset<flatbuffers::String> value = 0) {
+ KeyValueBuilder builder_(_fbb);
+ builder_.add_value(value);
+ builder_.add_key(key);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<KeyValue> CreateKeyValueDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *key = nullptr,
+ const char *value = nullptr) {
+ auto key__ = key ? _fbb.CreateString(key) : 0;
+ auto value__ = value ? _fbb.CreateString(value) : 0;
+ return reflection::CreateKeyValue(
+ _fbb,
+ key__,
+ value__);
+}
+
+struct EnumVal FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef EnumValBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_VALUE = 6,
+ VT_OBJECT = 8,
+ VT_UNION_TYPE = 10,
+ VT_DOCUMENTATION = 12
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ int64_t value() const {
+ return GetField<int64_t>(VT_VALUE, 0);
+ }
+ bool KeyCompareLessThan(const EnumVal *o) const {
+ return value() < o->value();
+ }
+ int KeyCompareWithValue(int64_t val) const {
+ return static_cast<int>(value() > val) - static_cast<int>(value() < val);
+ }
+ const reflection::Object *object() const {
+ return GetPointer<const reflection::Object *>(VT_OBJECT);
+ }
+ const reflection::Type *union_type() const {
+ return GetPointer<const reflection::Type *>(VT_UNION_TYPE);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyField<int64_t>(verifier, VT_VALUE) &&
+ VerifyOffset(verifier, VT_OBJECT) &&
+ verifier.VerifyTable(object()) &&
+ VerifyOffset(verifier, VT_UNION_TYPE) &&
+ verifier.VerifyTable(union_type()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ verifier.EndTable();
+ }
+};
+
+struct EnumValBuilder {
+ typedef EnumVal Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(EnumVal::VT_NAME, name);
+ }
+ void add_value(int64_t value) {
+ fbb_.AddElement<int64_t>(EnumVal::VT_VALUE, value, 0);
+ }
+ void add_object(flatbuffers::Offset<reflection::Object> object) {
+ fbb_.AddOffset(EnumVal::VT_OBJECT, object);
+ }
+ void add_union_type(flatbuffers::Offset<reflection::Type> union_type) {
+ fbb_.AddOffset(EnumVal::VT_UNION_TYPE, union_type);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(EnumVal::VT_DOCUMENTATION, documentation);
+ }
+ explicit EnumValBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<EnumVal> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<EnumVal>(end);
+ fbb_.Required(o, EnumVal::VT_NAME);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<EnumVal> CreateEnumVal(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ int64_t value = 0,
+ flatbuffers::Offset<reflection::Object> object = 0,
+ flatbuffers::Offset<reflection::Type> union_type = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0) {
+ EnumValBuilder builder_(_fbb);
+ builder_.add_value(value);
+ builder_.add_documentation(documentation);
+ builder_.add_union_type(union_type);
+ builder_.add_object(object);
+ builder_.add_name(name);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<EnumVal> CreateEnumValDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ int64_t value = 0,
+ flatbuffers::Offset<reflection::Object> object = 0,
+ flatbuffers::Offset<reflection::Type> union_type = 0,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateEnumVal(
+ _fbb,
+ name__,
+ value,
+ object,
+ union_type,
+ documentation__);
+}
+
+struct Enum FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef EnumBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_VALUES = 6,
+ VT_IS_UNION = 8,
+ VT_UNDERLYING_TYPE = 10,
+ VT_ATTRIBUTES = 12,
+ VT_DOCUMENTATION = 14
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ bool KeyCompareLessThan(const Enum *o) const {
+ return *name() < *o->name();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(name()->c_str(), val);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>> *values() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>> *>(VT_VALUES);
+ }
+ bool is_union() const {
+ return GetField<uint8_t>(VT_IS_UNION, 0) != 0;
+ }
+ const reflection::Type *underlying_type() const {
+ return GetPointer<const reflection::Type *>(VT_UNDERLYING_TYPE);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyOffsetRequired(verifier, VT_VALUES) &&
+ verifier.VerifyVector(values()) &&
+ verifier.VerifyVectorOfTables(values()) &&
+ VerifyField<uint8_t>(verifier, VT_IS_UNION) &&
+ VerifyOffsetRequired(verifier, VT_UNDERLYING_TYPE) &&
+ verifier.VerifyTable(underlying_type()) &&
+ VerifyOffset(verifier, VT_ATTRIBUTES) &&
+ verifier.VerifyVector(attributes()) &&
+ verifier.VerifyVectorOfTables(attributes()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ verifier.EndTable();
+ }
+};
+
+struct EnumBuilder {
+ typedef Enum Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(Enum::VT_NAME, name);
+ }
+ void add_values(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>>> values) {
+ fbb_.AddOffset(Enum::VT_VALUES, values);
+ }
+ void add_is_union(bool is_union) {
+ fbb_.AddElement<uint8_t>(Enum::VT_IS_UNION, static_cast<uint8_t>(is_union), 0);
+ }
+ void add_underlying_type(flatbuffers::Offset<reflection::Type> underlying_type) {
+ fbb_.AddOffset(Enum::VT_UNDERLYING_TYPE, underlying_type);
+ }
+ void add_attributes(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes) {
+ fbb_.AddOffset(Enum::VT_ATTRIBUTES, attributes);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(Enum::VT_DOCUMENTATION, documentation);
+ }
+ explicit EnumBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Enum> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Enum>(end);
+ fbb_.Required(o, Enum::VT_NAME);
+ fbb_.Required(o, Enum::VT_VALUES);
+ fbb_.Required(o, Enum::VT_UNDERLYING_TYPE);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Enum> CreateEnum(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>>> values = 0,
+ bool is_union = false,
+ flatbuffers::Offset<reflection::Type> underlying_type = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0) {
+ EnumBuilder builder_(_fbb);
+ builder_.add_documentation(documentation);
+ builder_.add_attributes(attributes);
+ builder_.add_underlying_type(underlying_type);
+ builder_.add_values(values);
+ builder_.add_name(name);
+ builder_.add_is_union(is_union);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<Enum> CreateEnumDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ std::vector<flatbuffers::Offset<reflection::EnumVal>> *values = nullptr,
+ bool is_union = false,
+ flatbuffers::Offset<reflection::Type> underlying_type = 0,
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> *attributes = nullptr,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto values__ = values ? _fbb.CreateVectorOfSortedTables<reflection::EnumVal>(values) : 0;
+ auto attributes__ = attributes ? _fbb.CreateVectorOfSortedTables<reflection::KeyValue>(attributes) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateEnum(
+ _fbb,
+ name__,
+ values__,
+ is_union,
+ underlying_type,
+ attributes__,
+ documentation__);
+}
+
+struct Field FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef FieldBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_TYPE = 6,
+ VT_ID = 8,
+ VT_OFFSET = 10,
+ VT_DEFAULT_INTEGER = 12,
+ VT_DEFAULT_REAL = 14,
+ VT_DEPRECATED = 16,
+ VT_REQUIRED = 18,
+ VT_KEY = 20,
+ VT_ATTRIBUTES = 22,
+ VT_DOCUMENTATION = 24,
+ VT_OPTIONAL = 26
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ bool KeyCompareLessThan(const Field *o) const {
+ return *name() < *o->name();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(name()->c_str(), val);
+ }
+ const reflection::Type *type() const {
+ return GetPointer<const reflection::Type *>(VT_TYPE);
+ }
+ uint16_t id() const {
+ return GetField<uint16_t>(VT_ID, 0);
+ }
+ uint16_t offset() const {
+ return GetField<uint16_t>(VT_OFFSET, 0);
+ }
+ int64_t default_integer() const {
+ return GetField<int64_t>(VT_DEFAULT_INTEGER, 0);
+ }
+ double default_real() const {
+ return GetField<double>(VT_DEFAULT_REAL, 0.0);
+ }
+ bool deprecated() const {
+ return GetField<uint8_t>(VT_DEPRECATED, 0) != 0;
+ }
+ bool required() const {
+ return GetField<uint8_t>(VT_REQUIRED, 0) != 0;
+ }
+ bool key() const {
+ return GetField<uint8_t>(VT_KEY, 0) != 0;
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool optional() const {
+ return GetField<uint8_t>(VT_OPTIONAL, 0) != 0;
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyOffsetRequired(verifier, VT_TYPE) &&
+ verifier.VerifyTable(type()) &&
+ VerifyField<uint16_t>(verifier, VT_ID) &&
+ VerifyField<uint16_t>(verifier, VT_OFFSET) &&
+ VerifyField<int64_t>(verifier, VT_DEFAULT_INTEGER) &&
+ VerifyField<double>(verifier, VT_DEFAULT_REAL) &&
+ VerifyField<uint8_t>(verifier, VT_DEPRECATED) &&
+ VerifyField<uint8_t>(verifier, VT_REQUIRED) &&
+ VerifyField<uint8_t>(verifier, VT_KEY) &&
+ VerifyOffset(verifier, VT_ATTRIBUTES) &&
+ verifier.VerifyVector(attributes()) &&
+ verifier.VerifyVectorOfTables(attributes()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ VerifyField<uint8_t>(verifier, VT_OPTIONAL) &&
+ verifier.EndTable();
+ }
+};
+
+struct FieldBuilder {
+ typedef Field Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(Field::VT_NAME, name);
+ }
+ void add_type(flatbuffers::Offset<reflection::Type> type) {
+ fbb_.AddOffset(Field::VT_TYPE, type);
+ }
+ void add_id(uint16_t id) {
+ fbb_.AddElement<uint16_t>(Field::VT_ID, id, 0);
+ }
+ void add_offset(uint16_t offset) {
+ fbb_.AddElement<uint16_t>(Field::VT_OFFSET, offset, 0);
+ }
+ void add_default_integer(int64_t default_integer) {
+ fbb_.AddElement<int64_t>(Field::VT_DEFAULT_INTEGER, default_integer, 0);
+ }
+ void add_default_real(double default_real) {
+ fbb_.AddElement<double>(Field::VT_DEFAULT_REAL, default_real, 0.0);
+ }
+ void add_deprecated(bool deprecated) {
+ fbb_.AddElement<uint8_t>(Field::VT_DEPRECATED, static_cast<uint8_t>(deprecated), 0);
+ }
+ void add_required(bool required) {
+ fbb_.AddElement<uint8_t>(Field::VT_REQUIRED, static_cast<uint8_t>(required), 0);
+ }
+ void add_key(bool key) {
+ fbb_.AddElement<uint8_t>(Field::VT_KEY, static_cast<uint8_t>(key), 0);
+ }
+ void add_attributes(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes) {
+ fbb_.AddOffset(Field::VT_ATTRIBUTES, attributes);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(Field::VT_DOCUMENTATION, documentation);
+ }
+ void add_optional(bool optional) {
+ fbb_.AddElement<uint8_t>(Field::VT_OPTIONAL, static_cast<uint8_t>(optional), 0);
+ }
+ explicit FieldBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Field> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Field>(end);
+ fbb_.Required(o, Field::VT_NAME);
+ fbb_.Required(o, Field::VT_TYPE);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Field> CreateField(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ flatbuffers::Offset<reflection::Type> type = 0,
+ uint16_t id = 0,
+ uint16_t offset = 0,
+ int64_t default_integer = 0,
+ double default_real = 0.0,
+ bool deprecated = false,
+ bool required = false,
+ bool key = false,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0,
+ bool optional = false) {
+ FieldBuilder builder_(_fbb);
+ builder_.add_default_real(default_real);
+ builder_.add_default_integer(default_integer);
+ builder_.add_documentation(documentation);
+ builder_.add_attributes(attributes);
+ builder_.add_type(type);
+ builder_.add_name(name);
+ builder_.add_offset(offset);
+ builder_.add_id(id);
+ builder_.add_optional(optional);
+ builder_.add_key(key);
+ builder_.add_required(required);
+ builder_.add_deprecated(deprecated);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<Field> CreateFieldDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ flatbuffers::Offset<reflection::Type> type = 0,
+ uint16_t id = 0,
+ uint16_t offset = 0,
+ int64_t default_integer = 0,
+ double default_real = 0.0,
+ bool deprecated = false,
+ bool required = false,
+ bool key = false,
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> *attributes = nullptr,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr,
+ bool optional = false) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto attributes__ = attributes ? _fbb.CreateVectorOfSortedTables<reflection::KeyValue>(attributes) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateField(
+ _fbb,
+ name__,
+ type,
+ id,
+ offset,
+ default_integer,
+ default_real,
+ deprecated,
+ required,
+ key,
+ attributes__,
+ documentation__,
+ optional);
+}
+
+struct Object FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef ObjectBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_FIELDS = 6,
+ VT_IS_STRUCT = 8,
+ VT_MINALIGN = 10,
+ VT_BYTESIZE = 12,
+ VT_ATTRIBUTES = 14,
+ VT_DOCUMENTATION = 16
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ bool KeyCompareLessThan(const Object *o) const {
+ return *name() < *o->name();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(name()->c_str(), val);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::Field>> *fields() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::Field>> *>(VT_FIELDS);
+ }
+ bool is_struct() const {
+ return GetField<uint8_t>(VT_IS_STRUCT, 0) != 0;
+ }
+ int32_t minalign() const {
+ return GetField<int32_t>(VT_MINALIGN, 0);
+ }
+ int32_t bytesize() const {
+ return GetField<int32_t>(VT_BYTESIZE, 0);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyOffsetRequired(verifier, VT_FIELDS) &&
+ verifier.VerifyVector(fields()) &&
+ verifier.VerifyVectorOfTables(fields()) &&
+ VerifyField<uint8_t>(verifier, VT_IS_STRUCT) &&
+ VerifyField<int32_t>(verifier, VT_MINALIGN) &&
+ VerifyField<int32_t>(verifier, VT_BYTESIZE) &&
+ VerifyOffset(verifier, VT_ATTRIBUTES) &&
+ verifier.VerifyVector(attributes()) &&
+ verifier.VerifyVectorOfTables(attributes()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ verifier.EndTable();
+ }
+};
+
+struct ObjectBuilder {
+ typedef Object Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(Object::VT_NAME, name);
+ }
+ void add_fields(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Field>>> fields) {
+ fbb_.AddOffset(Object::VT_FIELDS, fields);
+ }
+ void add_is_struct(bool is_struct) {
+ fbb_.AddElement<uint8_t>(Object::VT_IS_STRUCT, static_cast<uint8_t>(is_struct), 0);
+ }
+ void add_minalign(int32_t minalign) {
+ fbb_.AddElement<int32_t>(Object::VT_MINALIGN, minalign, 0);
+ }
+ void add_bytesize(int32_t bytesize) {
+ fbb_.AddElement<int32_t>(Object::VT_BYTESIZE, bytesize, 0);
+ }
+ void add_attributes(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes) {
+ fbb_.AddOffset(Object::VT_ATTRIBUTES, attributes);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(Object::VT_DOCUMENTATION, documentation);
+ }
+ explicit ObjectBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Object> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Object>(end);
+ fbb_.Required(o, Object::VT_NAME);
+ fbb_.Required(o, Object::VT_FIELDS);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Object> CreateObject(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Field>>> fields = 0,
+ bool is_struct = false,
+ int32_t minalign = 0,
+ int32_t bytesize = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0) {
+ ObjectBuilder builder_(_fbb);
+ builder_.add_documentation(documentation);
+ builder_.add_attributes(attributes);
+ builder_.add_bytesize(bytesize);
+ builder_.add_minalign(minalign);
+ builder_.add_fields(fields);
+ builder_.add_name(name);
+ builder_.add_is_struct(is_struct);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<Object> CreateObjectDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ std::vector<flatbuffers::Offset<reflection::Field>> *fields = nullptr,
+ bool is_struct = false,
+ int32_t minalign = 0,
+ int32_t bytesize = 0,
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> *attributes = nullptr,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto fields__ = fields ? _fbb.CreateVectorOfSortedTables<reflection::Field>(fields) : 0;
+ auto attributes__ = attributes ? _fbb.CreateVectorOfSortedTables<reflection::KeyValue>(attributes) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateObject(
+ _fbb,
+ name__,
+ fields__,
+ is_struct,
+ minalign,
+ bytesize,
+ attributes__,
+ documentation__);
+}
+
+struct RPCCall FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef RPCCallBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_REQUEST = 6,
+ VT_RESPONSE = 8,
+ VT_ATTRIBUTES = 10,
+ VT_DOCUMENTATION = 12
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ bool KeyCompareLessThan(const RPCCall *o) const {
+ return *name() < *o->name();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(name()->c_str(), val);
+ }
+ const reflection::Object *request() const {
+ return GetPointer<const reflection::Object *>(VT_REQUEST);
+ }
+ const reflection::Object *response() const {
+ return GetPointer<const reflection::Object *>(VT_RESPONSE);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyOffsetRequired(verifier, VT_REQUEST) &&
+ verifier.VerifyTable(request()) &&
+ VerifyOffsetRequired(verifier, VT_RESPONSE) &&
+ verifier.VerifyTable(response()) &&
+ VerifyOffset(verifier, VT_ATTRIBUTES) &&
+ verifier.VerifyVector(attributes()) &&
+ verifier.VerifyVectorOfTables(attributes()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ verifier.EndTable();
+ }
+};
+
+struct RPCCallBuilder {
+ typedef RPCCall Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(RPCCall::VT_NAME, name);
+ }
+ void add_request(flatbuffers::Offset<reflection::Object> request) {
+ fbb_.AddOffset(RPCCall::VT_REQUEST, request);
+ }
+ void add_response(flatbuffers::Offset<reflection::Object> response) {
+ fbb_.AddOffset(RPCCall::VT_RESPONSE, response);
+ }
+ void add_attributes(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes) {
+ fbb_.AddOffset(RPCCall::VT_ATTRIBUTES, attributes);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(RPCCall::VT_DOCUMENTATION, documentation);
+ }
+ explicit RPCCallBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<RPCCall> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<RPCCall>(end);
+ fbb_.Required(o, RPCCall::VT_NAME);
+ fbb_.Required(o, RPCCall::VT_REQUEST);
+ fbb_.Required(o, RPCCall::VT_RESPONSE);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<RPCCall> CreateRPCCall(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ flatbuffers::Offset<reflection::Object> request = 0,
+ flatbuffers::Offset<reflection::Object> response = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0) {
+ RPCCallBuilder builder_(_fbb);
+ builder_.add_documentation(documentation);
+ builder_.add_attributes(attributes);
+ builder_.add_response(response);
+ builder_.add_request(request);
+ builder_.add_name(name);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<RPCCall> CreateRPCCallDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ flatbuffers::Offset<reflection::Object> request = 0,
+ flatbuffers::Offset<reflection::Object> response = 0,
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> *attributes = nullptr,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto attributes__ = attributes ? _fbb.CreateVectorOfSortedTables<reflection::KeyValue>(attributes) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateRPCCall(
+ _fbb,
+ name__,
+ request,
+ response,
+ attributes__,
+ documentation__);
+}
+
+struct Service FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef ServiceBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_NAME = 4,
+ VT_CALLS = 6,
+ VT_ATTRIBUTES = 8,
+ VT_DOCUMENTATION = 10
+ };
+ const flatbuffers::String *name() const {
+ return GetPointer<const flatbuffers::String *>(VT_NAME);
+ }
+ bool KeyCompareLessThan(const Service *o) const {
+ return *name() < *o->name();
+ }
+ int KeyCompareWithValue(const char *val) const {
+ return strcmp(name()->c_str(), val);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::RPCCall>> *calls() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::RPCCall>> *>(VT_CALLS);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *attributes() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>> *>(VT_ATTRIBUTES);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *documentation() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *>(VT_DOCUMENTATION);
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_NAME) &&
+ verifier.VerifyString(name()) &&
+ VerifyOffset(verifier, VT_CALLS) &&
+ verifier.VerifyVector(calls()) &&
+ verifier.VerifyVectorOfTables(calls()) &&
+ VerifyOffset(verifier, VT_ATTRIBUTES) &&
+ verifier.VerifyVector(attributes()) &&
+ verifier.VerifyVectorOfTables(attributes()) &&
+ VerifyOffset(verifier, VT_DOCUMENTATION) &&
+ verifier.VerifyVector(documentation()) &&
+ verifier.VerifyVectorOfStrings(documentation()) &&
+ verifier.EndTable();
+ }
+};
+
+struct ServiceBuilder {
+ typedef Service Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_name(flatbuffers::Offset<flatbuffers::String> name) {
+ fbb_.AddOffset(Service::VT_NAME, name);
+ }
+ void add_calls(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::RPCCall>>> calls) {
+ fbb_.AddOffset(Service::VT_CALLS, calls);
+ }
+ void add_attributes(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes) {
+ fbb_.AddOffset(Service::VT_ATTRIBUTES, attributes);
+ }
+ void add_documentation(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation) {
+ fbb_.AddOffset(Service::VT_DOCUMENTATION, documentation);
+ }
+ explicit ServiceBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Service> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Service>(end);
+ fbb_.Required(o, Service::VT_NAME);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Service> CreateService(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::String> name = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::RPCCall>>> calls = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>> attributes = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>> documentation = 0) {
+ ServiceBuilder builder_(_fbb);
+ builder_.add_documentation(documentation);
+ builder_.add_attributes(attributes);
+ builder_.add_calls(calls);
+ builder_.add_name(name);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<Service> CreateServiceDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ const char *name = nullptr,
+ std::vector<flatbuffers::Offset<reflection::RPCCall>> *calls = nullptr,
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> *attributes = nullptr,
+ const std::vector<flatbuffers::Offset<flatbuffers::String>> *documentation = nullptr) {
+ auto name__ = name ? _fbb.CreateString(name) : 0;
+ auto calls__ = calls ? _fbb.CreateVectorOfSortedTables<reflection::RPCCall>(calls) : 0;
+ auto attributes__ = attributes ? _fbb.CreateVectorOfSortedTables<reflection::KeyValue>(attributes) : 0;
+ auto documentation__ = documentation ? _fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>(*documentation) : 0;
+ return reflection::CreateService(
+ _fbb,
+ name__,
+ calls__,
+ attributes__,
+ documentation__);
+}
+
+struct Schema FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+ typedef SchemaBuilder Builder;
+ enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+ VT_OBJECTS = 4,
+ VT_ENUMS = 6,
+ VT_FILE_IDENT = 8,
+ VT_FILE_EXT = 10,
+ VT_ROOT_TABLE = 12,
+ VT_SERVICES = 14,
+ VT_ADVANCED_FEATURES = 16
+ };
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *>(VT_OBJECTS);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *>(VT_ENUMS);
+ }
+ const flatbuffers::String *file_ident() const {
+ return GetPointer<const flatbuffers::String *>(VT_FILE_IDENT);
+ }
+ const flatbuffers::String *file_ext() const {
+ return GetPointer<const flatbuffers::String *>(VT_FILE_EXT);
+ }
+ const reflection::Object *root_table() const {
+ return GetPointer<const reflection::Object *>(VT_ROOT_TABLE);
+ }
+ const flatbuffers::Vector<flatbuffers::Offset<reflection::Service>> *services() const {
+ return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<reflection::Service>> *>(VT_SERVICES);
+ }
+ reflection::AdvancedFeatures advanced_features() const {
+ return static_cast<reflection::AdvancedFeatures>(GetField<uint64_t>(VT_ADVANCED_FEATURES, 0));
+ }
+ bool Verify(flatbuffers::Verifier &verifier) const {
+ return VerifyTableStart(verifier) &&
+ VerifyOffsetRequired(verifier, VT_OBJECTS) &&
+ verifier.VerifyVector(objects()) &&
+ verifier.VerifyVectorOfTables(objects()) &&
+ VerifyOffsetRequired(verifier, VT_ENUMS) &&
+ verifier.VerifyVector(enums()) &&
+ verifier.VerifyVectorOfTables(enums()) &&
+ VerifyOffset(verifier, VT_FILE_IDENT) &&
+ verifier.VerifyString(file_ident()) &&
+ VerifyOffset(verifier, VT_FILE_EXT) &&
+ verifier.VerifyString(file_ext()) &&
+ VerifyOffset(verifier, VT_ROOT_TABLE) &&
+ verifier.VerifyTable(root_table()) &&
+ VerifyOffset(verifier, VT_SERVICES) &&
+ verifier.VerifyVector(services()) &&
+ verifier.VerifyVectorOfTables(services()) &&
+ VerifyField<uint64_t>(verifier, VT_ADVANCED_FEATURES) &&
+ verifier.EndTable();
+ }
+};
+
+struct SchemaBuilder {
+ typedef Schema Table;
+ flatbuffers::FlatBufferBuilder &fbb_;
+ flatbuffers::uoffset_t start_;
+ void add_objects(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Object>>> objects) {
+ fbb_.AddOffset(Schema::VT_OBJECTS, objects);
+ }
+ void add_enums(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>>> enums) {
+ fbb_.AddOffset(Schema::VT_ENUMS, enums);
+ }
+ void add_file_ident(flatbuffers::Offset<flatbuffers::String> file_ident) {
+ fbb_.AddOffset(Schema::VT_FILE_IDENT, file_ident);
+ }
+ void add_file_ext(flatbuffers::Offset<flatbuffers::String> file_ext) {
+ fbb_.AddOffset(Schema::VT_FILE_EXT, file_ext);
+ }
+ void add_root_table(flatbuffers::Offset<reflection::Object> root_table) {
+ fbb_.AddOffset(Schema::VT_ROOT_TABLE, root_table);
+ }
+ void add_services(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Service>>> services) {
+ fbb_.AddOffset(Schema::VT_SERVICES, services);
+ }
+ void add_advanced_features(reflection::AdvancedFeatures advanced_features) {
+ fbb_.AddElement<uint64_t>(Schema::VT_ADVANCED_FEATURES, static_cast<uint64_t>(advanced_features), 0);
+ }
+ explicit SchemaBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+ : fbb_(_fbb) {
+ start_ = fbb_.StartTable();
+ }
+ flatbuffers::Offset<Schema> Finish() {
+ const auto end = fbb_.EndTable(start_);
+ auto o = flatbuffers::Offset<Schema>(end);
+ fbb_.Required(o, Schema::VT_OBJECTS);
+ fbb_.Required(o, Schema::VT_ENUMS);
+ return o;
+ }
+};
+
+inline flatbuffers::Offset<Schema> CreateSchema(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Object>>> objects = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>>> enums = 0,
+ flatbuffers::Offset<flatbuffers::String> file_ident = 0,
+ flatbuffers::Offset<flatbuffers::String> file_ext = 0,
+ flatbuffers::Offset<reflection::Object> root_table = 0,
+ flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<reflection::Service>>> services = 0,
+ reflection::AdvancedFeatures advanced_features = static_cast<reflection::AdvancedFeatures>(0)) {
+ SchemaBuilder builder_(_fbb);
+ builder_.add_advanced_features(advanced_features);
+ builder_.add_services(services);
+ builder_.add_root_table(root_table);
+ builder_.add_file_ext(file_ext);
+ builder_.add_file_ident(file_ident);
+ builder_.add_enums(enums);
+ builder_.add_objects(objects);
+ return builder_.Finish();
+}
+
+inline flatbuffers::Offset<Schema> CreateSchemaDirect(
+ flatbuffers::FlatBufferBuilder &_fbb,
+ std::vector<flatbuffers::Offset<reflection::Object>> *objects = nullptr,
+ std::vector<flatbuffers::Offset<reflection::Enum>> *enums = nullptr,
+ const char *file_ident = nullptr,
+ const char *file_ext = nullptr,
+ flatbuffers::Offset<reflection::Object> root_table = 0,
+ std::vector<flatbuffers::Offset<reflection::Service>> *services = nullptr,
+ reflection::AdvancedFeatures advanced_features = static_cast<reflection::AdvancedFeatures>(0)) {
+ auto objects__ = objects ? _fbb.CreateVectorOfSortedTables<reflection::Object>(objects) : 0;
+ auto enums__ = enums ? _fbb.CreateVectorOfSortedTables<reflection::Enum>(enums) : 0;
+ auto file_ident__ = file_ident ? _fbb.CreateString(file_ident) : 0;
+ auto file_ext__ = file_ext ? _fbb.CreateString(file_ext) : 0;
+ auto services__ = services ? _fbb.CreateVectorOfSortedTables<reflection::Service>(services) : 0;
+ return reflection::CreateSchema(
+ _fbb,
+ objects__,
+ enums__,
+ file_ident__,
+ file_ext__,
+ root_table,
+ services__,
+ advanced_features);
+}
+
+inline const reflection::Schema *GetSchema(const void *buf) {
+ return flatbuffers::GetRoot<reflection::Schema>(buf);
+}
+
+inline const reflection::Schema *GetSizePrefixedSchema(const void *buf) {
+ return flatbuffers::GetSizePrefixedRoot<reflection::Schema>(buf);
+}
+
+inline const char *SchemaIdentifier() {
+ return "BFBS";
+}
+
+inline bool SchemaBufferHasIdentifier(const void *buf) {
+ return flatbuffers::BufferHasIdentifier(
+ buf, SchemaIdentifier());
+}
+
+inline bool VerifySchemaBuffer(
+ flatbuffers::Verifier &verifier) {
+ return verifier.VerifyBuffer<reflection::Schema>(SchemaIdentifier());
+}
+
+inline bool VerifySizePrefixedSchemaBuffer(
+ flatbuffers::Verifier &verifier) {
+ return verifier.VerifySizePrefixedBuffer<reflection::Schema>(SchemaIdentifier());
+}
+
+inline const char *SchemaExtension() {
+ return "bfbs";
+}
+
+inline void FinishSchemaBuffer(
+ flatbuffers::FlatBufferBuilder &fbb,
+ flatbuffers::Offset<reflection::Schema> root) {
+ fbb.Finish(root, SchemaIdentifier());
+}
+
+inline void FinishSizePrefixedSchemaBuffer(
+ flatbuffers::FlatBufferBuilder &fbb,
+ flatbuffers::Offset<reflection::Schema> root) {
+ fbb.FinishSizePrefixed(root, SchemaIdentifier());
+}
+
+} // namespace reflection
+
+#endif // FLATBUFFERS_GENERATED_REFLECTION_REFLECTION_H_
diff --git a/contrib/libs/flatbuffers/include/flatbuffers/util.h b/contrib/libs/flatbuffers/include/flatbuffers/util.h
new file mode 100644
index 0000000000..4493c561c2
--- /dev/null
+++ b/contrib/libs/flatbuffers/include/flatbuffers/util.h
@@ -0,0 +1,698 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLATBUFFERS_UTIL_H_
+#define FLATBUFFERS_UTIL_H_
+
+#include <errno.h>
+
+#include "base.h"
+#include "stl_emulation.h"
+
+#ifndef FLATBUFFERS_PREFER_PRINTF
+# include <sstream>
+#else // FLATBUFFERS_PREFER_PRINTF
+# include <float.h>
+# include <stdio.h>
+#endif // FLATBUFFERS_PREFER_PRINTF
+
+#include <iomanip>
+#include <string>
+
+namespace flatbuffers {
+
+// @locale-independent functions for ASCII characters set.
+
+// Fast checking that character lies in closed range: [a <= x <= b]
+// using one compare (conditional branch) operator.
+inline bool check_ascii_range(char x, char a, char b) {
+ FLATBUFFERS_ASSERT(a <= b);
+ // (Hacker's Delight): `a <= x <= b` <=> `(x-a) <={u} (b-a)`.
+ // The x, a, b will be promoted to int and subtracted without overflow.
+ return static_cast<unsigned int>(x - a) <= static_cast<unsigned int>(b - a);
+}
+
+// Case-insensitive isalpha
+inline bool is_alpha(char c) {
+ // ASCII only: alpha to upper case => reset bit 0x20 (~0x20 = 0xDF).
+ return check_ascii_range(c & 0xDF, 'a' & 0xDF, 'z' & 0xDF);
+}
+
+// Check for uppercase alpha
+inline bool is_alpha_upper(char c) { return check_ascii_range(c, 'A', 'Z'); }
+
+// Check (case-insensitive) that `c` is equal to alpha.
+inline bool is_alpha_char(char c, char alpha) {
+ FLATBUFFERS_ASSERT(is_alpha(alpha));
+ // ASCII only: alpha to upper case => reset bit 0x20 (~0x20 = 0xDF).
+ return ((c & 0xDF) == (alpha & 0xDF));
+}
+
+// https://en.cppreference.com/w/cpp/string/byte/isxdigit
+// isdigit and isxdigit are the only standard narrow character classification
+// functions that are not affected by the currently installed C locale. although
+// some implementations (e.g. Microsoft in 1252 codepage) may classify
+// additional single-byte characters as digits.
+inline bool is_digit(char c) { return check_ascii_range(c, '0', '9'); }
+
+inline bool is_xdigit(char c) {
+ // Replace by look-up table.
+ return is_digit(c) || check_ascii_range(c & 0xDF, 'a' & 0xDF, 'f' & 0xDF);
+}
+
+// Case-insensitive isalnum
+inline bool is_alnum(char c) { return is_alpha(c) || is_digit(c); }
+
+inline char CharToUpper(char c) {
+ return static_cast<char>(::toupper(static_cast<unsigned char>(c)));
+}
+
+inline char CharToLower(char c) {
+ return static_cast<char>(::tolower(static_cast<unsigned char>(c)));
+}
+
+// @end-locale-independent functions for ASCII character set
+
+#ifdef FLATBUFFERS_PREFER_PRINTF
+template<typename T> size_t IntToDigitCount(T t) {
+ size_t digit_count = 0;
+ // Count the sign for negative numbers
+ if (t < 0) digit_count++;
+ // Count a single 0 left of the dot for fractional numbers
+ if (-1 < t && t < 1) digit_count++;
+ // Count digits until fractional part
+ T eps = std::numeric_limits<float>::epsilon();
+ while (t <= (-1 + eps) || (1 - eps) <= t) {
+ t /= 10;
+ digit_count++;
+ }
+ return digit_count;
+}
+
+template<typename T> size_t NumToStringWidth(T t, int precision = 0) {
+ size_t string_width = IntToDigitCount(t);
+ // Count the dot for floating point numbers
+ if (precision) string_width += (precision + 1);
+ return string_width;
+}
+
+template<typename T>
+std::string NumToStringImplWrapper(T t, const char *fmt, int precision = 0) {
+ size_t string_width = NumToStringWidth(t, precision);
+ std::string s(string_width, 0x00);
+ // Allow snprintf to use std::string trailing null to detect buffer overflow
+ snprintf(const_cast<char *>(s.data()), (s.size() + 1), fmt, string_width, t);
+ return s;
+}
+#endif // FLATBUFFERS_PREFER_PRINTF
+
+// Convert an integer or floating point value to a string.
+// In contrast to std::stringstream, "char" values are
+// converted to a string of digits, and we don't use scientific notation.
+template<typename T> std::string NumToString(T t) {
+ // clang-format off
+
+ #ifndef FLATBUFFERS_PREFER_PRINTF
+ std::stringstream ss;
+ ss << t;
+ return ss.str();
+ #else // FLATBUFFERS_PREFER_PRINTF
+ auto v = static_cast<long long>(t);
+ return NumToStringImplWrapper(v, "%.*lld");
+ #endif // FLATBUFFERS_PREFER_PRINTF
+ // clang-format on
+}
+// Avoid char types used as character data.
+template<> inline std::string NumToString<signed char>(signed char t) {
+ return NumToString(static_cast<int>(t));
+}
+template<> inline std::string NumToString<unsigned char>(unsigned char t) {
+ return NumToString(static_cast<int>(t));
+}
+template<> inline std::string NumToString<char>(char t) {
+ return NumToString(static_cast<int>(t));
+}
+#if defined(FLATBUFFERS_CPP98_STL)
+template<> inline std::string NumToString<long long>(long long t) {
+ char buf[21]; // (log((1 << 63) - 1) / log(10)) + 2
+ snprintf(buf, sizeof(buf), "%lld", t);
+ return std::string(buf);
+}
+
+template<>
+inline std::string NumToString<unsigned long long>(unsigned long long t) {
+ char buf[22]; // (log((1 << 63) - 1) / log(10)) + 1
+ snprintf(buf, sizeof(buf), "%llu", t);
+ return std::string(buf);
+}
+#endif // defined(FLATBUFFERS_CPP98_STL)
+
+// Special versions for floats/doubles.
+template<typename T> std::string FloatToString(T t, int precision) {
+ // clang-format off
+
+ #ifndef FLATBUFFERS_PREFER_PRINTF
+ // to_string() prints different numbers of digits for floats depending on
+ // platform and isn't available on Android, so we use stringstream
+ std::stringstream ss;
+ // Use std::fixed to suppress scientific notation.
+ ss << std::fixed;
+ // Default precision is 6, we want that to be higher for doubles.
+ ss << std::setprecision(precision);
+ ss << t;
+ auto s = ss.str();
+ #else // FLATBUFFERS_PREFER_PRINTF
+ auto v = static_cast<double>(t);
+ auto s = NumToStringImplWrapper(v, "%0.*f", precision);
+ #endif // FLATBUFFERS_PREFER_PRINTF
+ // clang-format on
+ // Sadly, std::fixed turns "1" into "1.00000", so here we undo that.
+ auto p = s.find_last_not_of('0');
+ if (p != std::string::npos) {
+ // Strip trailing zeroes. If it is a whole number, keep one zero.
+ s.resize(p + (s[p] == '.' ? 2 : 1));
+ }
+ return s;
+}
+
+template<> inline std::string NumToString<double>(double t) {
+ return FloatToString(t, 12);
+}
+template<> inline std::string NumToString<float>(float t) {
+ return FloatToString(t, 6);
+}
+
+// Convert an integer value to a hexadecimal string.
+// The returned string length is always xdigits long, prefixed by 0 digits.
+// For example, IntToStringHex(0x23, 8) returns the string "00000023".
+inline std::string IntToStringHex(int i, int xdigits) {
+ FLATBUFFERS_ASSERT(i >= 0);
+ // clang-format off
+
+ #ifndef FLATBUFFERS_PREFER_PRINTF
+ std::stringstream ss;
+ ss << std::setw(xdigits) << std::setfill('0') << std::hex << std::uppercase
+ << i;
+ return ss.str();
+ #else // FLATBUFFERS_PREFER_PRINTF
+ return NumToStringImplWrapper(i, "%.*X", xdigits);
+ #endif // FLATBUFFERS_PREFER_PRINTF
+ // clang-format on
+}
+
+// clang-format off
+// Use locale independent functions {strtod_l, strtof_l, strtoll_l, strtoull_l}.
+#if defined(FLATBUFFERS_LOCALE_INDEPENDENT) && (FLATBUFFERS_LOCALE_INDEPENDENT > 0)
+ class ClassicLocale {
+ #ifdef _MSC_VER
+ typedef _locale_t locale_type;
+ #else
+ typedef locale_t locale_type; // POSIX.1-2008 locale_t type
+ #endif
+ ClassicLocale();
+ ~ClassicLocale();
+ locale_type locale_;
+ static ClassicLocale instance_;
+ public:
+ static locale_type Get() { return instance_.locale_; }
+ };
+
+ #ifdef _MSC_VER
+ #define __strtoull_impl(s, pe, b) _strtoui64_l(s, pe, b, ClassicLocale::Get())
+ #define __strtoll_impl(s, pe, b) _strtoi64_l(s, pe, b, ClassicLocale::Get())
+ #define __strtod_impl(s, pe) _strtod_l(s, pe, ClassicLocale::Get())
+ #define __strtof_impl(s, pe) _strtof_l(s, pe, ClassicLocale::Get())
+ #else
+ #define __strtoull_impl(s, pe, b) strtoull_l(s, pe, b, ClassicLocale::Get())
+ #define __strtoll_impl(s, pe, b) strtoll_l(s, pe, b, ClassicLocale::Get())
+ #define __strtod_impl(s, pe) strtod_l(s, pe, ClassicLocale::Get())
+ #define __strtof_impl(s, pe) strtof_l(s, pe, ClassicLocale::Get())
+ #endif
+#else
+ #define __strtod_impl(s, pe) strtod(s, pe)
+ #define __strtof_impl(s, pe) static_cast<float>(strtod(s, pe))
+ #ifdef _MSC_VER
+ #define __strtoull_impl(s, pe, b) _strtoui64(s, pe, b)
+ #define __strtoll_impl(s, pe, b) _strtoi64(s, pe, b)
+ #else
+ #define __strtoull_impl(s, pe, b) strtoull(s, pe, b)
+ #define __strtoll_impl(s, pe, b) strtoll(s, pe, b)
+ #endif
+#endif
+
+inline void strtoval_impl(int64_t *val, const char *str, char **endptr,
+ int base) {
+ *val = __strtoll_impl(str, endptr, base);
+}
+
+inline void strtoval_impl(uint64_t *val, const char *str, char **endptr,
+ int base) {
+ *val = __strtoull_impl(str, endptr, base);
+}
+
+inline void strtoval_impl(double *val, const char *str, char **endptr) {
+ *val = __strtod_impl(str, endptr);
+}
+
+// UBSAN: double to float is safe if numeric_limits<float>::is_iec559 is true.
+__supress_ubsan__("float-cast-overflow")
+inline void strtoval_impl(float *val, const char *str, char **endptr) {
+ *val = __strtof_impl(str, endptr);
+}
+#undef __strtoull_impl
+#undef __strtoll_impl
+#undef __strtod_impl
+#undef __strtof_impl
+// clang-format on
+
+// Adaptor for strtoull()/strtoll().
+// Flatbuffers accepts numbers with any count of leading zeros (-009 is -9),
+// while strtoll with base=0 interprets first leading zero as octal prefix.
+// In future, it is possible to add prefixed 0b0101.
+// 1) Checks errno code for overflow condition (out of range).
+// 2) If base <= 0, function try to detect base of number by prefix.
+//
+// Return value (like strtoull and strtoll, but reject partial result):
+// - If successful, an integer value corresponding to the str is returned.
+// - If full string conversion can't be performed, 0 is returned.
+// - If the converted value falls out of range of corresponding return type, a
+// range error occurs. In this case value MAX(T)/MIN(T) is returned.
+template<typename T>
+inline bool StringToIntegerImpl(T *val, const char *const str,
+ const int base = 0,
+ const bool check_errno = true) {
+ // T is int64_t or uint64_T
+ FLATBUFFERS_ASSERT(str);
+ if (base <= 0) {
+ auto s = str;
+ while (*s && !is_digit(*s)) s++;
+ if (s[0] == '0' && is_alpha_char(s[1], 'X'))
+ return StringToIntegerImpl(val, str, 16, check_errno);
+ // if a prefix not match, try base=10
+ return StringToIntegerImpl(val, str, 10, check_errno);
+ } else {
+ if (check_errno) errno = 0; // clear thread-local errno
+ auto endptr = str;
+ strtoval_impl(val, str, const_cast<char **>(&endptr), base);
+ if ((*endptr != '\0') || (endptr == str)) {
+ *val = 0; // erase partial result
+ return false; // invalid string
+ }
+ // errno is out-of-range, return MAX/MIN
+ if (check_errno && errno) return false;
+ return true;
+ }
+}
+
+template<typename T>
+inline bool StringToFloatImpl(T *val, const char *const str) {
+ // Type T must be either float or double.
+ FLATBUFFERS_ASSERT(str && val);
+ auto end = str;
+ strtoval_impl(val, str, const_cast<char **>(&end));
+ auto done = (end != str) && (*end == '\0');
+ if (!done) *val = 0; // erase partial result
+ return done;
+}
+
+// Convert a string to an instance of T.
+// Return value (matched with StringToInteger64Impl and strtod):
+// - If successful, a numeric value corresponding to the str is returned.
+// - If full string conversion can't be performed, 0 is returned.
+// - If the converted value falls out of range of corresponding return type, a
+// range error occurs. In this case value MAX(T)/MIN(T) is returned.
+template<typename T> inline bool StringToNumber(const char *s, T *val) {
+ // Assert on `unsigned long` and `signed long` on LP64.
+ // If it is necessary, it could be solved with flatbuffers::enable_if<B,T>.
+ static_assert(sizeof(T) < sizeof(int64_t), "unexpected type T");
+ FLATBUFFERS_ASSERT(s && val);
+ int64_t i64;
+ // The errno check isn't needed, will return MAX/MIN on overflow.
+ if (StringToIntegerImpl(&i64, s, 0, false)) {
+ const int64_t max = (flatbuffers::numeric_limits<T>::max)();
+ const int64_t min = flatbuffers::numeric_limits<T>::lowest();
+ if (i64 > max) {
+ *val = static_cast<T>(max);
+ return false;
+ }
+ if (i64 < min) {
+ // For unsigned types return max to distinguish from
+ // "no conversion can be performed" when 0 is returned.
+ *val = static_cast<T>(flatbuffers::is_unsigned<T>::value ? max : min);
+ return false;
+ }
+ *val = static_cast<T>(i64);
+ return true;
+ }
+ *val = 0;
+ return false;
+}
+
+template<> inline bool StringToNumber<int64_t>(const char *str, int64_t *val) {
+ return StringToIntegerImpl(val, str);
+}
+
+template<>
+inline bool StringToNumber<uint64_t>(const char *str, uint64_t *val) {
+ if (!StringToIntegerImpl(val, str)) return false;
+ // The strtoull accepts negative numbers:
+ // If the minus sign was part of the input sequence, the numeric value
+ // calculated from the sequence of digits is negated as if by unary minus
+ // in the result type, which applies unsigned integer wraparound rules.
+ // Fix this behaviour (except -0).
+ if (*val) {
+ auto s = str;
+ while (*s && !is_digit(*s)) s++;
+ s = (s > str) ? (s - 1) : s; // step back to one symbol
+ if (*s == '-') {
+ // For unsigned types return the max to distinguish from
+ // "no conversion can be performed".
+ *val = (flatbuffers::numeric_limits<uint64_t>::max)();
+ return false;
+ }
+ }
+ return true;
+}
+
+template<> inline bool StringToNumber(const char *s, float *val) {
+ return StringToFloatImpl(val, s);
+}
+
+template<> inline bool StringToNumber(const char *s, double *val) {
+ return StringToFloatImpl(val, s);
+}
+
+inline int64_t StringToInt(const char *s, int base = 10) {
+ int64_t val;
+ return StringToIntegerImpl(&val, s, base) ? val : 0;
+}
+
+inline uint64_t StringToUInt(const char *s, int base = 10) {
+ uint64_t val;
+ return StringToIntegerImpl(&val, s, base) ? val : 0;
+}
+
+typedef bool (*LoadFileFunction)(const char *filename, bool binary,
+ std::string *dest);
+typedef bool (*FileExistsFunction)(const char *filename);
+
+LoadFileFunction SetLoadFileFunction(LoadFileFunction load_file_function);
+
+FileExistsFunction SetFileExistsFunction(
+ FileExistsFunction file_exists_function);
+
+// Check if file "name" exists.
+bool FileExists(const char *name);
+
+// Check if "name" exists and it is also a directory.
+bool DirExists(const char *name);
+
+// Load file "name" into "buf" returning true if successful
+// false otherwise. If "binary" is false data is read
+// using ifstream's text mode, otherwise data is read with
+// no transcoding.
+bool LoadFile(const char *name, bool binary, std::string *buf);
+
+// Save data "buf" of length "len" bytes into a file
+// "name" returning true if successful, false otherwise.
+// If "binary" is false data is written using ifstream's
+// text mode, otherwise data is written with no
+// transcoding.
+bool SaveFile(const char *name, const char *buf, size_t len, bool binary);
+
+// Save data "buf" into file "name" returning true if
+// successful, false otherwise. If "binary" is false
+// data is written using ifstream's text mode, otherwise
+// data is written with no transcoding.
+inline bool SaveFile(const char *name, const std::string &buf, bool binary) {
+ return SaveFile(name, buf.c_str(), buf.size(), binary);
+}
+
+// Functionality for minimalistic portable path handling.
+
+// The functions below behave correctly regardless of whether posix ('/') or
+// Windows ('/' or '\\') separators are used.
+
+// Any new separators inserted are always posix.
+FLATBUFFERS_CONSTEXPR char kPathSeparator = '/';
+
+// Returns the path with the extension, if any, removed.
+std::string StripExtension(const std::string &filepath);
+
+// Returns the extension, if any.
+std::string GetExtension(const std::string &filepath);
+
+// Return the last component of the path, after the last separator.
+std::string StripPath(const std::string &filepath);
+
+// Strip the last component of the path + separator.
+std::string StripFileName(const std::string &filepath);
+
+// Concatenates a path with a filename, regardless of whether the path
+// ends in a separator or not.
+std::string ConCatPathFileName(const std::string &path,
+ const std::string &filename);
+
+// Replaces any '\\' separators with '/'
+std::string PosixPath(const char *path);
+
+// This function ensure a directory exists, by recursively
+// creating dirs for any parts of the path that don't exist yet.
+void EnsureDirExists(const std::string &filepath);
+
+// Obtains the absolute path from any other path.
+// Returns the input path if the absolute path couldn't be resolved.
+std::string AbsolutePath(const std::string &filepath);
+
+// To and from UTF-8 unicode conversion functions
+
+// Convert a unicode code point into a UTF-8 representation by appending it
+// to a string. Returns the number of bytes generated.
+inline int ToUTF8(uint32_t ucc, std::string *out) {
+ FLATBUFFERS_ASSERT(!(ucc & 0x80000000)); // Top bit can't be set.
+ // 6 possible encodings: http://en.wikipedia.org/wiki/UTF-8
+ for (int i = 0; i < 6; i++) {
+ // Max bits this encoding can represent.
+ uint32_t max_bits = 6 + i * 5 + static_cast<int>(!i);
+ if (ucc < (1u << max_bits)) { // does it fit?
+ // Remaining bits not encoded in the first byte, store 6 bits each
+ uint32_t remain_bits = i * 6;
+ // Store first byte:
+ (*out) += static_cast<char>((0xFE << (max_bits - remain_bits)) |
+ (ucc >> remain_bits));
+ // Store remaining bytes:
+ for (int j = i - 1; j >= 0; j--) {
+ (*out) += static_cast<char>(((ucc >> (j * 6)) & 0x3F) | 0x80);
+ }
+ return i + 1; // Return the number of bytes added.
+ }
+ }
+ FLATBUFFERS_ASSERT(0); // Impossible to arrive here.
+ return -1;
+}
+
+// Converts whatever prefix of the incoming string corresponds to a valid
+// UTF-8 sequence into a unicode code. The incoming pointer will have been
+// advanced past all bytes parsed.
+// returns -1 upon corrupt UTF-8 encoding (ignore the incoming pointer in
+// this case).
+inline int FromUTF8(const char **in) {
+ int len = 0;
+ // Count leading 1 bits.
+ for (int mask = 0x80; mask >= 0x04; mask >>= 1) {
+ if (**in & mask) {
+ len++;
+ } else {
+ break;
+ }
+ }
+ if ((static_cast<unsigned char>(**in) << len) & 0x80)
+ return -1; // Bit after leading 1's must be 0.
+ if (!len) return *(*in)++;
+ // UTF-8 encoded values with a length are between 2 and 4 bytes.
+ if (len < 2 || len > 4) { return -1; }
+ // Grab initial bits of the code.
+ int ucc = *(*in)++ & ((1 << (7 - len)) - 1);
+ for (int i = 0; i < len - 1; i++) {
+ if ((**in & 0xC0) != 0x80) return -1; // Upper bits must 1 0.
+ ucc <<= 6;
+ ucc |= *(*in)++ & 0x3F; // Grab 6 more bits of the code.
+ }
+ // UTF-8 cannot encode values between 0xD800 and 0xDFFF (reserved for
+ // UTF-16 surrogate pairs).
+ if (ucc >= 0xD800 && ucc <= 0xDFFF) { return -1; }
+ // UTF-8 must represent code points in their shortest possible encoding.
+ switch (len) {
+ case 2:
+ // Two bytes of UTF-8 can represent code points from U+0080 to U+07FF.
+ if (ucc < 0x0080 || ucc > 0x07FF) { return -1; }
+ break;
+ case 3:
+ // Three bytes of UTF-8 can represent code points from U+0800 to U+FFFF.
+ if (ucc < 0x0800 || ucc > 0xFFFF) { return -1; }
+ break;
+ case 4:
+ // Four bytes of UTF-8 can represent code points from U+10000 to U+10FFFF.
+ if (ucc < 0x10000 || ucc > 0x10FFFF) { return -1; }
+ break;
+ }
+ return ucc;
+}
+
+#ifndef FLATBUFFERS_PREFER_PRINTF
+// Wraps a string to a maximum length, inserting new lines where necessary. Any
+// existing whitespace will be collapsed down to a single space. A prefix or
+// suffix can be provided, which will be inserted before or after a wrapped
+// line, respectively.
+inline std::string WordWrap(const std::string in, size_t max_length,
+ const std::string wrapped_line_prefix,
+ const std::string wrapped_line_suffix) {
+ std::istringstream in_stream(in);
+ std::string wrapped, line, word;
+
+ in_stream >> word;
+ line = word;
+
+ while (in_stream >> word) {
+ if ((line.length() + 1 + word.length() + wrapped_line_suffix.length()) <
+ max_length) {
+ line += " " + word;
+ } else {
+ wrapped += line + wrapped_line_suffix + "\n";
+ line = wrapped_line_prefix + word;
+ }
+ }
+ wrapped += line;
+
+ return wrapped;
+}
+#endif // !FLATBUFFERS_PREFER_PRINTF
+
+inline bool EscapeString(const char *s, size_t length, std::string *_text,
+ bool allow_non_utf8, bool natural_utf8) {
+ std::string &text = *_text;
+ text += "\"";
+ for (uoffset_t i = 0; i < length; i++) {
+ char c = s[i];
+ switch (c) {
+ case '\n': text += "\\n"; break;
+ case '\t': text += "\\t"; break;
+ case '\r': text += "\\r"; break;
+ case '\b': text += "\\b"; break;
+ case '\f': text += "\\f"; break;
+ case '\"': text += "\\\""; break;
+ case '\\': text += "\\\\"; break;
+ default:
+ if (c >= ' ' && c <= '~') {
+ text += c;
+ } else {
+ // Not printable ASCII data. Let's see if it's valid UTF-8 first:
+ const char *utf8 = s + i;
+ int ucc = FromUTF8(&utf8);
+ if (ucc < 0) {
+ if (allow_non_utf8) {
+ text += "\\x";
+ text += IntToStringHex(static_cast<uint8_t>(c), 2);
+ } else {
+ // There are two cases here:
+ //
+ // 1) We reached here by parsing an IDL file. In that case,
+ // we previously checked for non-UTF-8, so we shouldn't reach
+ // here.
+ //
+ // 2) We reached here by someone calling GenerateText()
+ // on a previously-serialized flatbuffer. The data might have
+ // non-UTF-8 Strings, or might be corrupt.
+ //
+ // In both cases, we have to give up and inform the caller
+ // they have no JSON.
+ return false;
+ }
+ } else {
+ if (natural_utf8) {
+ // utf8 points to past all utf-8 bytes parsed
+ text.append(s + i, static_cast<size_t>(utf8 - s - i));
+ } else if (ucc <= 0xFFFF) {
+ // Parses as Unicode within JSON's \uXXXX range, so use that.
+ text += "\\u";
+ text += IntToStringHex(ucc, 4);
+ } else if (ucc <= 0x10FFFF) {
+ // Encode Unicode SMP values to a surrogate pair using two \u
+ // escapes.
+ uint32_t base = ucc - 0x10000;
+ auto high_surrogate = (base >> 10) + 0xD800;
+ auto low_surrogate = (base & 0x03FF) + 0xDC00;
+ text += "\\u";
+ text += IntToStringHex(high_surrogate, 4);
+ text += "\\u";
+ text += IntToStringHex(low_surrogate, 4);
+ }
+ // Skip past characters recognized.
+ i = static_cast<uoffset_t>(utf8 - s - 1);
+ }
+ }
+ break;
+ }
+ }
+ text += "\"";
+ return true;
+}
+
+inline std::string BufferToHexText(const void *buffer, size_t buffer_size,
+ size_t max_length,
+ const std::string &wrapped_line_prefix,
+ const std::string &wrapped_line_suffix) {
+ std::string text = wrapped_line_prefix;
+ size_t start_offset = 0;
+ const char *s = reinterpret_cast<const char *>(buffer);
+ for (size_t i = 0; s && i < buffer_size; i++) {
+ // Last iteration or do we have more?
+ bool have_more = i + 1 < buffer_size;
+ text += "0x";
+ text += IntToStringHex(static_cast<uint8_t>(s[i]), 2);
+ if (have_more) { text += ','; }
+ // If we have more to process and we reached max_length
+ if (have_more &&
+ text.size() + wrapped_line_suffix.size() >= start_offset + max_length) {
+ text += wrapped_line_suffix;
+ text += '\n';
+ start_offset = text.size();
+ text += wrapped_line_prefix;
+ }
+ }
+ text += wrapped_line_suffix;
+ return text;
+}
+
+// Remove paired quotes in a string: "text"|'text' -> text.
+std::string RemoveStringQuotes(const std::string &s);
+
+// Change th global C-locale to locale with name <locale_name>.
+// Returns an actual locale name in <_value>, useful if locale_name is "" or
+// null.
+bool SetGlobalTestLocale(const char *locale_name,
+ std::string *_value = nullptr);
+
+// Read (or test) a value of environment variable.
+bool ReadEnvironmentVariable(const char *var_name,
+ std::string *_value = nullptr);
+
+// MSVC specific: Send all assert reports to STDOUT to prevent CI hangs.
+void SetupDefaultCRTReportMode();
+
+} // namespace flatbuffers
+
+#endif // FLATBUFFERS_UTIL_H_
diff --git a/contrib/libs/flatbuffers/samples/monster.fbs b/contrib/libs/flatbuffers/samples/monster.fbs
new file mode 100644
index 0000000000..af224512ee
--- /dev/null
+++ b/contrib/libs/flatbuffers/samples/monster.fbs
@@ -0,0 +1,33 @@
+// Example IDL file for our monster's schema.
+
+namespace MyGame.Sample;
+
+enum Color:byte { Red = 0, Green, Blue = 2 }
+
+union Equipment { Weapon } // Optionally add more tables.
+
+struct Vec3 {
+ x:float;
+ y:float;
+ z:float;
+}
+
+table Monster {
+ pos:Vec3;
+ mana:short = 150;
+ hp:short = 100;
+ name:string;
+ friendly:bool = false (deprecated);
+ inventory:[ubyte];
+ color:Color = Blue;
+ weapons:[Weapon];
+ equipped:Equipment;
+ path:[Vec3];
+}
+
+table Weapon {
+ name:string;
+ damage:short;
+}
+
+root_type Monster;
diff --git a/contrib/libs/flatbuffers/samples/sample_binary.cpp b/contrib/libs/flatbuffers/samples/sample_binary.cpp
new file mode 100644
index 0000000000..6bd1cdcf43
--- /dev/null
+++ b/contrib/libs/flatbuffers/samples/sample_binary.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <contrib/libs/flatbuffers/samples/monster.fbs.h>
+
+using namespace MyGame::Sample;
+
+// Example how to use FlatBuffers to create and read binary buffers.
+
+int main(int /*argc*/, const char * /*argv*/[]) {
+ // Build up a serialized buffer algorithmically:
+ flatbuffers::FlatBufferBuilder builder;
+
+ // First, lets serialize some weapons for the Monster: A 'sword' and an 'axe'.
+ auto weapon_one_name = builder.CreateString("Sword");
+ short weapon_one_damage = 3;
+
+ auto weapon_two_name = builder.CreateString("Axe");
+ short weapon_two_damage = 5;
+
+ // Use the `CreateWeapon` shortcut to create Weapons with all fields set.
+ auto sword = CreateWeapon(builder, weapon_one_name, weapon_one_damage);
+ auto axe = CreateWeapon(builder, weapon_two_name, weapon_two_damage);
+
+ // Create a FlatBuffer's `vector` from the `std::vector`.
+ std::vector<flatbuffers::Offset<Weapon>> weapons_vector;
+ weapons_vector.push_back(sword);
+ weapons_vector.push_back(axe);
+ auto weapons = builder.CreateVector(weapons_vector);
+
+ // Second, serialize the rest of the objects needed by the Monster.
+ auto position = Vec3(1.0f, 2.0f, 3.0f);
+
+ auto name = builder.CreateString("MyMonster");
+
+ unsigned char inv_data[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+ auto inventory = builder.CreateVector(inv_data, 10);
+
+ // Shortcut for creating monster with all fields set:
+ auto orc = CreateMonster(builder, &position, 150, 80, name, inventory,
+ Color_Red, weapons, Equipment_Weapon, axe.Union());
+
+ builder.Finish(orc); // Serialize the root of the object.
+
+ // We now have a FlatBuffer we can store on disk or send over a network.
+
+ // ** file/network code goes here :) **
+ // access builder.GetBufferPointer() for builder.GetSize() bytes
+
+ // Instead, we're going to access it right away (as if we just received it).
+
+ // Get access to the root:
+ auto monster = GetMonster(builder.GetBufferPointer());
+
+ // Get and test some scalar types from the FlatBuffer.
+ assert(monster->hp() == 80);
+ assert(monster->mana() == 150); // default
+ assert(monster->name()->str() == "MyMonster");
+
+ // Get and test a field of the FlatBuffer's `struct`.
+ auto pos = monster->pos();
+ assert(pos);
+ assert(pos->z() == 3.0f);
+ (void)pos;
+
+ // Get a test an element from the `inventory` FlatBuffer's `vector`.
+ auto inv = monster->inventory();
+ assert(inv);
+ assert(inv->Get(9) == 9);
+ (void)inv;
+
+ // Get and test the `weapons` FlatBuffers's `vector`.
+ std::string expected_weapon_names[] = { "Sword", "Axe" };
+ short expected_weapon_damages[] = { 3, 5 };
+ auto weps = monster->weapons();
+ for (unsigned int i = 0; i < weps->size(); i++) {
+ assert(weps->Get(i)->name()->str() == expected_weapon_names[i]);
+ assert(weps->Get(i)->damage() == expected_weapon_damages[i]);
+ }
+ (void)expected_weapon_names;
+ (void)expected_weapon_damages;
+
+ // Get and test the `Equipment` union (`equipped` field).
+ assert(monster->equipped_type() == Equipment_Weapon);
+ auto equipped = static_cast<const Weapon *>(monster->equipped());
+ assert(equipped->name()->str() == "Axe");
+ assert(equipped->damage() == 5);
+ (void)equipped;
+
+ printf("The FlatBuffer was successfully created and verified!\n");
+}
diff --git a/contrib/libs/flatbuffers/samples/ya.make b/contrib/libs/flatbuffers/samples/ya.make
new file mode 100644
index 0000000000..7855f8f461
--- /dev/null
+++ b/contrib/libs/flatbuffers/samples/ya.make
@@ -0,0 +1,18 @@
+PROGRAM()
+
+LICENSE(Apache-2.0)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+NO_UTIL()
+
+SRCS(
+ monster.fbs
+ sample_binary.cpp
+)
+
+PEERDIR(
+ contrib/libs/flatbuffers
+)
+
+END()
diff --git a/contrib/libs/flatbuffers/src/code_generators.cpp b/contrib/libs/flatbuffers/src/code_generators.cpp
new file mode 100644
index 0000000000..745406ba95
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/code_generators.cpp
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "flatbuffers/code_generators.h"
+
+#include <assert.h>
+
+#include <cmath>
+
+#include "flatbuffers/base.h"
+#include "flatbuffers/util.h"
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable : 4127) // C4127: conditional expression is constant
+#endif
+
+namespace flatbuffers {
+
+void CodeWriter::operator+=(std::string text) {
+ if (!ignore_ident_ && !text.empty()) AppendIdent(stream_);
+
+ while (true) {
+ auto begin = text.find("{{");
+ if (begin == std::string::npos) { break; }
+
+ auto end = text.find("}}");
+ if (end == std::string::npos || end < begin) { break; }
+
+ // Write all the text before the first {{ into the stream.
+ stream_.write(text.c_str(), begin);
+
+ // The key is between the {{ and }}.
+ const std::string key = text.substr(begin + 2, end - begin - 2);
+
+ // Find the value associated with the key. If it exists, write the
+ // value into the stream, otherwise write the key itself into the stream.
+ auto iter = value_map_.find(key);
+ if (iter != value_map_.end()) {
+ const std::string &value = iter->second;
+ stream_ << value;
+ } else {
+ FLATBUFFERS_ASSERT(false && "could not find key");
+ stream_ << key;
+ }
+
+ // Update the text to everything after the }}.
+ text = text.substr(end + 2);
+ }
+ if (!text.empty() && string_back(text) == '\\') {
+ text.pop_back();
+ ignore_ident_ = true;
+ stream_ << text;
+ } else {
+ ignore_ident_ = false;
+ stream_ << text << std::endl;
+ }
+}
+
+void CodeWriter::AppendIdent(std::stringstream &stream) {
+ int lvl = cur_ident_lvl_;
+ while (lvl--) {
+ stream.write(pad_.c_str(), static_cast<std::streamsize>(pad_.size()));
+ }
+}
+
+const char *BaseGenerator::FlatBuffersGeneratedWarning() {
+ return "automatically generated by the FlatBuffers compiler,"
+ " do not modify";
+}
+
+std::string BaseGenerator::NamespaceDir(const Parser &parser,
+ const std::string &path,
+ const Namespace &ns,
+ const bool dasherize) {
+ EnsureDirExists(path);
+ if (parser.opts.one_file) return path;
+ std::string namespace_dir = path; // Either empty or ends in separator.
+ auto &namespaces = ns.components;
+ for (auto it = namespaces.begin(); it != namespaces.end(); ++it) {
+ namespace_dir += !dasherize ? *it : ToDasherizedCase(*it);
+ namespace_dir += kPathSeparator;
+ EnsureDirExists(namespace_dir);
+ }
+ return namespace_dir;
+}
+
+std::string BaseGenerator::NamespaceDir(const Namespace &ns,
+ const bool dasherize) const {
+ return BaseGenerator::NamespaceDir(parser_, path_, ns, dasherize);
+}
+
+std::string BaseGenerator::ToDasherizedCase(const std::string pascal_case) {
+ std::string dasherized_case;
+ char p = 0;
+ for (size_t i = 0; i < pascal_case.length(); i++) {
+ char const &c = pascal_case[i];
+ if (is_alpha_upper(c)) {
+ if (i > 0 && p != kPathSeparator) dasherized_case += "-";
+ dasherized_case += CharToLower(c);
+ } else {
+ dasherized_case += c;
+ }
+ p = c;
+ }
+ return dasherized_case;
+}
+
+std::string BaseGenerator::FullNamespace(const char *separator,
+ const Namespace &ns) {
+ std::string namespace_name;
+ auto &namespaces = ns.components;
+ for (auto it = namespaces.begin(); it != namespaces.end(); ++it) {
+ if (namespace_name.length()) namespace_name += separator;
+ namespace_name += *it;
+ }
+ return namespace_name;
+}
+
+std::string BaseGenerator::LastNamespacePart(const Namespace &ns) {
+ if (!ns.components.empty())
+ return ns.components.back();
+ else
+ return std::string("");
+}
+
+// Ensure that a type is prefixed with its namespace.
+std::string BaseGenerator::WrapInNameSpace(const Namespace *ns,
+ const std::string &name) const {
+ std::string qualified_name = qualifying_start_;
+ for (auto it = ns->components.begin(); it != ns->components.end(); ++it)
+ qualified_name += *it + qualifying_separator_;
+ return qualified_name + name;
+}
+
+std::string BaseGenerator::WrapInNameSpace(const Definition &def) const {
+ return WrapInNameSpace(def.defined_namespace, def.name);
+}
+
+std::string BaseGenerator::GetNameSpace(const Definition &def) const {
+ const Namespace *ns = def.defined_namespace;
+ if (CurrentNameSpace() == ns) return "";
+ std::string qualified_name = qualifying_start_;
+ for (auto it = ns->components.begin(); it != ns->components.end(); ++it) {
+ qualified_name += *it;
+ if ((it + 1) != ns->components.end()) {
+ qualified_name += qualifying_separator_;
+ }
+ }
+
+ return qualified_name;
+}
+
+std::string BaseGenerator::GeneratedFileName(const std::string &path,
+ const std::string &file_name,
+ const IDLOptions &options) const {
+ return path + file_name + options.filename_suffix + "." +
+ (options.filename_extension.empty() ? default_extension_
+ : options.filename_extension);
+}
+
+// Generate a documentation comment, if available.
+void GenComment(const std::vector<std::string> &dc, std::string *code_ptr,
+ const CommentConfig *config, const char *prefix) {
+ if (dc.begin() == dc.end()) {
+ // Don't output empty comment blocks with 0 lines of comment content.
+ return;
+ }
+
+ std::string &code = *code_ptr;
+ if (config != nullptr && config->first_line != nullptr) {
+ code += std::string(prefix) + std::string(config->first_line) + "\n";
+ }
+ std::string line_prefix =
+ std::string(prefix) +
+ ((config != nullptr && config->content_line_prefix != nullptr)
+ ? config->content_line_prefix
+ : "///");
+ for (auto it = dc.begin(); it != dc.end(); ++it) {
+ code += line_prefix + *it + "\n";
+ }
+ if (config != nullptr && config->last_line != nullptr) {
+ code += std::string(prefix) + std::string(config->last_line) + "\n";
+ }
+}
+
+template<typename T>
+std::string FloatConstantGenerator::GenFloatConstantImpl(
+ const FieldDef &field) const {
+ const auto &constant = field.value.constant;
+ T v;
+ auto done = StringToNumber(constant.c_str(), &v);
+ FLATBUFFERS_ASSERT(done);
+ if (done) {
+#if (!defined(_MSC_VER) || (_MSC_VER >= 1800))
+ if (std::isnan(v)) return NaN(v);
+ if (std::isinf(v)) return Inf(v);
+#endif
+ return Value(v, constant);
+ }
+ return "#"; // compile time error
+}
+
+std::string FloatConstantGenerator::GenFloatConstant(
+ const FieldDef &field) const {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_FLOAT: return GenFloatConstantImpl<float>(field);
+ case BASE_TYPE_DOUBLE: return GenFloatConstantImpl<double>(field);
+ default: {
+ FLATBUFFERS_ASSERT(false);
+ return "INVALID_BASE_TYPE";
+ }
+ };
+}
+
+TypedFloatConstantGenerator::TypedFloatConstantGenerator(
+ const char *double_prefix, const char *single_prefix,
+ const char *nan_number, const char *pos_inf_number,
+ const char *neg_inf_number)
+ : double_prefix_(double_prefix),
+ single_prefix_(single_prefix),
+ nan_number_(nan_number),
+ pos_inf_number_(pos_inf_number),
+ neg_inf_number_(neg_inf_number) {}
+
+std::string TypedFloatConstantGenerator::MakeNaN(
+ const std::string &prefix) const {
+ return prefix + nan_number_;
+}
+std::string TypedFloatConstantGenerator::MakeInf(
+ bool neg, const std::string &prefix) const {
+ if (neg)
+ return !neg_inf_number_.empty() ? (prefix + neg_inf_number_)
+ : ("-" + prefix + pos_inf_number_);
+ else
+ return prefix + pos_inf_number_;
+}
+
+std::string TypedFloatConstantGenerator::Value(double v,
+ const std::string &src) const {
+ (void)v;
+ return src;
+}
+
+std::string TypedFloatConstantGenerator::Inf(double v) const {
+ return MakeInf(v < 0, double_prefix_);
+}
+
+std::string TypedFloatConstantGenerator::NaN(double v) const {
+ (void)v;
+ return MakeNaN(double_prefix_);
+}
+
+std::string TypedFloatConstantGenerator::Value(float v,
+ const std::string &src) const {
+ (void)v;
+ return src + "f";
+}
+
+std::string TypedFloatConstantGenerator::Inf(float v) const {
+ return MakeInf(v < 0, single_prefix_);
+}
+
+std::string TypedFloatConstantGenerator::NaN(float v) const {
+ (void)v;
+ return MakeNaN(single_prefix_);
+}
+
+SimpleFloatConstantGenerator::SimpleFloatConstantGenerator(
+ const char *nan_number, const char *pos_inf_number,
+ const char *neg_inf_number)
+ : nan_number_(nan_number),
+ pos_inf_number_(pos_inf_number),
+ neg_inf_number_(neg_inf_number) {}
+
+std::string SimpleFloatConstantGenerator::Value(double v,
+ const std::string &src) const {
+ (void)v;
+ return src;
+}
+
+std::string SimpleFloatConstantGenerator::Inf(double v) const {
+ return (v < 0) ? neg_inf_number_ : pos_inf_number_;
+}
+
+std::string SimpleFloatConstantGenerator::NaN(double v) const {
+ (void)v;
+ return nan_number_;
+}
+
+std::string SimpleFloatConstantGenerator::Value(float v,
+ const std::string &src) const {
+ return this->Value(static_cast<double>(v), src);
+}
+
+std::string SimpleFloatConstantGenerator::Inf(float v) const {
+ return this->Inf(static_cast<double>(v));
+}
+
+std::string SimpleFloatConstantGenerator::NaN(float v) const {
+ return this->NaN(static_cast<double>(v));
+}
+
+std::string JavaCSharpMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ FLATBUFFERS_ASSERT(parser.opts.lang == IDLOptions::kJava ||
+ parser.opts.lang == IDLOptions::kCSharp);
+
+ std::string file_extension =
+ (parser.opts.lang == IDLOptions::kJava) ? ".java" : ".cs";
+
+ std::string make_rule;
+
+ for (auto it = parser.enums_.vec.begin(); it != parser.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ if (!make_rule.empty()) make_rule += " ";
+ std::string directory =
+ BaseGenerator::NamespaceDir(parser, path, *enum_def.defined_namespace);
+ make_rule += directory + enum_def.name + file_extension;
+ }
+
+ for (auto it = parser.structs_.vec.begin(); it != parser.structs_.vec.end();
+ ++it) {
+ auto &struct_def = **it;
+ if (!make_rule.empty()) make_rule += " ";
+ std::string directory = BaseGenerator::NamespaceDir(
+ parser, path, *struct_def.defined_namespace);
+ make_rule += directory + struct_def.name + file_extension;
+ }
+
+ make_rule += ": ";
+ auto included_files = parser.GetIncludedFilesRecursive(file_name);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+std::string BinaryFileName(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ auto ext = parser.file_extension_.length() ? parser.file_extension_ : "bin";
+ return path + file_name + "." + ext;
+}
+
+bool GenerateBinary(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ if (parser.opts.use_flexbuffers) {
+ auto data_vec = parser.flex_builder_.GetBuffer();
+ auto data_ptr = reinterpret_cast<char *>(data(data_vec));
+ return !parser.flex_builder_.GetSize() ||
+ flatbuffers::SaveFile(
+ BinaryFileName(parser, path, file_name).c_str(), data_ptr,
+ parser.flex_builder_.GetSize(), true);
+ }
+ return !parser.builder_.GetSize() ||
+ flatbuffers::SaveFile(
+ BinaryFileName(parser, path, file_name).c_str(),
+ reinterpret_cast<char *>(parser.builder_.GetBufferPointer()),
+ parser.builder_.GetSize(), true);
+}
+
+std::string BinaryMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ if (!parser.builder_.GetSize()) return "";
+ std::string filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ std::string make_rule =
+ BinaryFileName(parser, path, filebase) + ": " + file_name;
+ auto included_files =
+ parser.GetIncludedFilesRecursive(parser.root_struct_def_->file);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
diff --git a/contrib/libs/flatbuffers/src/flatc.cpp b/contrib/libs/flatbuffers/src/flatc.cpp
new file mode 100644
index 0000000000..221b88676d
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/flatc.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "flatbuffers/flatc.h"
+
+#include <list>
+
+namespace flatbuffers {
+
+const char *FLATC_VERSION() { return FLATBUFFERS_VERSION(); }
+
+void FlatCompiler::ParseFile(
+ flatbuffers::Parser &parser, const std::string &filename,
+ const std::string &contents,
+ std::vector<const char *> &include_directories) const {
+ auto local_include_directory = flatbuffers::StripFileName(filename);
+ include_directories.push_back(local_include_directory.c_str());
+ include_directories.push_back(nullptr);
+ if (!parser.Parse(contents.c_str(), &include_directories[0],
+ filename.c_str())) {
+ Error(parser.error_, false, false);
+ }
+ if (!parser.error_.empty()) { Warn(parser.error_, false); }
+ include_directories.pop_back();
+ include_directories.pop_back();
+}
+
+void FlatCompiler::LoadBinarySchema(flatbuffers::Parser &parser,
+ const std::string &filename,
+ const std::string &contents) {
+ if (!parser.Deserialize(reinterpret_cast<const uint8_t *>(contents.c_str()),
+ contents.size())) {
+ Error("failed to load binary schema: " + filename, false, false);
+ }
+}
+
+void FlatCompiler::Warn(const std::string &warn, bool show_exe_name) const {
+ params_.warn_fn(this, warn, show_exe_name);
+}
+
+void FlatCompiler::Error(const std::string &err, bool usage,
+ bool show_exe_name) const {
+ params_.error_fn(this, err, usage, show_exe_name);
+}
+
+std::string FlatCompiler::GetUsageString(const char *program_name) const {
+ std::stringstream ss;
+ ss << "Usage: " << program_name << " [OPTION]... FILE... [-- FILE...]\n";
+ for (size_t i = 0; i < params_.num_generators; ++i) {
+ const Generator &g = params_.generators[i];
+
+ std::stringstream full_name;
+ full_name << std::setw(16) << std::left << g.generator_opt_long;
+ const char *name = g.generator_opt_short ? g.generator_opt_short : " ";
+ const char *help = g.generator_help;
+
+ ss << " " << full_name.str() << " " << name << " " << help << ".\n";
+ }
+ // clang-format off
+
+ // Output width
+ // 12345678901234567890123456789012345678901234567890123456789012345678901234567890
+ ss <<
+ " -o PATH Prefix PATH to all generated files.\n"
+ " -I PATH Search for includes in the specified path.\n"
+ " -M Print make rules for generated files.\n"
+ " --version Print the version number of flatc and exit.\n"
+ " --strict-json Strict JSON: field names must be / will be quoted,\n"
+ " no trailing commas in tables/vectors.\n"
+ " --allow-non-utf8 Pass non-UTF-8 input through parser and emit nonstandard\n"
+ " \\x escapes in JSON. (Default is to raise parse error on\n"
+ " non-UTF-8 input.)\n"
+ " --natural-utf8 Output strings with UTF-8 as human-readable strings.\n"
+ " By default, UTF-8 characters are printed as \\uXXXX escapes.\n"
+ " --defaults-json Output fields whose value is the default when\n"
+ " writing JSON\n"
+ " --unknown-json Allow fields in JSON that are not defined in the\n"
+ " schema. These fields will be discared when generating\n"
+ " binaries.\n"
+ " --no-prefix Don\'t prefix enum values with the enum type in C++.\n"
+ " --scoped-enums Use C++11 style scoped and strongly typed enums.\n"
+ " also implies --no-prefix.\n"
+ " --gen-includes (deprecated), this is the default behavior.\n"
+ " If the original behavior is required (no include\n"
+ " statements) use --no-includes.\n"
+ " --no-includes Don\'t generate include statements for included\n"
+ " schemas the generated file depends on (C++ / Python).\n"
+ " --gen-mutable Generate accessors that can mutate buffers in-place.\n"
+ " --gen-onefile Generate single output file for C# and Go.\n"
+ " --gen-name-strings Generate type name functions for C++ and Rust.\n"
+ " --gen-object-api Generate an additional object-based API.\n"
+ " --gen-compare Generate operator== for object-based API types.\n"
+ " --gen-nullable Add Clang _Nullable for C++ pointer. or @Nullable for Java\n"
+ " --java-checkerframe work Add @Pure for Java.\n"
+ " --gen-generated Add @Generated annotation for Java\n"
+ " --gen-jvmstatic Add @JvmStatic annotation for Kotlin methods\n"
+ " in companion object for interop from Java to Kotlin.\n"
+ " --gen-all Generate not just code for the current schema files,\n"
+ " but for all files it includes as well.\n"
+ " If the language uses a single file for output (by default\n"
+ " the case for C++ and JS), all code will end up in this one\n"
+ " file.\n"
+ " --cpp-include Adds an #include in generated file.\n"
+ " --cpp-ptr-type T Set object API pointer type (default std::unique_ptr).\n"
+ " --cpp-str-type T Set object API string type (default std::string).\n"
+ " T::c_str(), T::length() and T::empty() must be supported.\n"
+ " The custom type also needs to be constructible from std::string\n"
+ " (see the --cpp-str-flex-ctor option to change this behavior).\n"
+ " --cpp-str-flex-ctor Don't construct custom string types by passing std::string\n"
+ " from Flatbuffers, but (char* + length).\n"
+ " --cpp-std CPP_STD Generate a C++ code using features of selected C++ standard.\n"
+ " Supported CPP_STD values:\n"
+ " * 'c++0x' - generate code compatible with old compilers;\n"
+ " * 'c++11' - use C++11 code generator (default);\n"
+ " * 'c++17' - use C++17 features in generated code (experimental).\n"
+ " --cpp-static-reflection When using C++17, generate extra code to provide compile-time\n"
+ " (static) reflection of Flatbuffers types. Requires --cpp-std\n"
+ " to be \"c++17\" or higher.\n"
+ " --object-prefix Customise class prefix for C++ object-based API.\n"
+ " --object-suffix Customise class suffix for C++ object-based API.\n"
+ " Default value is \"T\".\n"
+ " --go-namespace Generate the overriding namespace in Golang.\n"
+ " --go-import Generate the overriding import for flatbuffers in Golang\n"
+ " (default is \"github.com/google/flatbuffers/go\").\n"
+ " --raw-binary Allow binaries without file_identifier to be read.\n"
+ " This may crash flatc given a mismatched schema.\n"
+ " --size-prefixed Input binaries are size prefixed buffers.\n"
+ " --proto Input is a .proto, translate to .fbs.\n"
+ " --proto-namespace-suffix Add this namespace to any flatbuffers generated\n"
+ " SUFFIX from protobufs.\n"
+ " --oneof-union Translate .proto oneofs to flatbuffer unions.\n"
+ " --grpc Generate GRPC interfaces for the specified languages.\n"
+ " --schema Serialize schemas instead of JSON (use with -b).\n"
+ " --bfbs-comments Add doc comments to the binary schema files.\n"
+ " --bfbs-builtins Add builtin attributes to the binary schema files.\n"
+ " --bfbs-gen-embed Generate code to embed the bfbs schema to the source.\n"
+ " --conform FILE Specify a schema the following schemas should be\n"
+ " an evolution of. Gives errors if not.\n"
+ " --conform-includes Include path for the schema given with --conform PATH\n"
+ " --filename-suffix The suffix appended to the generated file names.\n"
+ " Default is '_generated'.\n"
+ " --filename-ext The extension appended to the generated file names.\n"
+ " Default is language-specific (e.g., '.h' for C++)\n"
+ " --include-prefix Prefix this path to any generated include statements.\n"
+ " PATH\n"
+ " --keep-prefix Keep original prefix of schema include statement.\n"
+ " --reflect-types Add minimal type reflection to code generation.\n"
+ " --reflect-names Add minimal type/name reflection.\n"
+ " --root-type T Select or override the default root_type\n"
+ " --require-explicit-ids When parsing schemas, require explicit ids (id: x).\n"
+ " --force-defaults Emit default values in binary output from JSON\n"
+ " --force-empty When serializing from object API representation,\n"
+ " force strings and vectors to empty rather than null.\n"
+ " --force-empty-vectors When serializing from object API representation,\n"
+ " force vectors to empty rather than null.\n"
+ " --flexbuffers Used with \"binary\" and \"json\" options, it generates\n"
+ " data using schema-less FlexBuffers.\n"
+ " --no-warnings Inhibit all warning messages.\n"
+ "FILEs may be schemas (must end in .fbs), binary schemas (must end in .bfbs),\n"
+ "or JSON files (conforming to preceding schema). FILEs after the -- must be\n"
+ "binary flatbuffer format files.\n"
+ "Output files are named using the base file name of the input,\n"
+ "and written to the current directory or the path given by -o.\n"
+ "example: " << program_name << " -c -b schema1.fbs schema2.fbs data.json\n";
+ // 12345678901234567890123456789012345678901234567890123456789012345678901234567890
+ // clang-format on
+ return ss.str();
+}
+
+int FlatCompiler::Compile(int argc, const char **argv) {
+ if (params_.generators == nullptr || params_.num_generators == 0) {
+ return 0;
+ }
+
+ flatbuffers::IDLOptions opts;
+ std::string output_path;
+
+ bool any_generator = false;
+ bool print_make_rules = false;
+ bool raw_binary = false;
+ bool schema_binary = false;
+ bool grpc_enabled = false;
+ std::vector<std::string> filenames;
+ std::list<std::string> include_directories_storage;
+ std::vector<const char *> include_directories;
+ std::vector<const char *> conform_include_directories;
+ std::vector<bool> generator_enabled(params_.num_generators, false);
+ size_t binary_files_from = std::numeric_limits<size_t>::max();
+ std::string conform_to_schema;
+
+ for (int argi = 0; argi < argc; argi++) {
+ std::string arg = argv[argi];
+ if (arg[0] == '-') {
+ if (filenames.size() && arg[1] != '-')
+ Error("invalid option location: " + arg, true);
+ if (arg == "-o") {
+ if (++argi >= argc) Error("missing path following: " + arg, true);
+ output_path = flatbuffers::ConCatPathFileName(
+ flatbuffers::PosixPath(argv[argi]), "");
+ } else if (arg == "-I") {
+ if (++argi >= argc) Error("missing path following: " + arg, true);
+ include_directories_storage.push_back(
+ flatbuffers::PosixPath(argv[argi]));
+ include_directories.push_back(
+ include_directories_storage.back().c_str());
+ } else if (arg == "--conform") {
+ if (++argi >= argc) Error("missing path following: " + arg, true);
+ conform_to_schema = flatbuffers::PosixPath(argv[argi]);
+ } else if (arg == "--conform-includes") {
+ if (++argi >= argc) Error("missing path following: " + arg, true);
+ include_directories_storage.push_back(
+ flatbuffers::PosixPath(argv[argi]));
+ conform_include_directories.push_back(
+ include_directories_storage.back().c_str());
+ } else if (arg == "--include-prefix") {
+ if (++argi >= argc) Error("missing path following: " + arg, true);
+ opts.include_prefix = flatbuffers::ConCatPathFileName(
+ flatbuffers::PosixPath(argv[argi]), "");
+ } else if (arg == "--keep-prefix") {
+ opts.keep_include_path = true;
+ } else if (arg == "--strict-json") {
+ opts.strict_json = true;
+ } else if (arg == "--allow-non-utf8") {
+ opts.allow_non_utf8 = true;
+ } else if (arg == "--natural-utf8") {
+ opts.natural_utf8 = true;
+ } else if (arg == "--go-namespace") {
+ if (++argi >= argc) Error("missing golang namespace" + arg, true);
+ opts.go_namespace = argv[argi];
+ } else if (arg == "--go-import") {
+ if (++argi >= argc) Error("missing golang import" + arg, true);
+ opts.go_import = argv[argi];
+ } else if (arg == "--defaults-json") {
+ opts.output_default_scalars_in_json = true;
+ } else if (arg == "--unknown-json") {
+ opts.skip_unexpected_fields_in_json = true;
+ } else if (arg == "--no-prefix") {
+ opts.prefixed_enums = false;
+ } else if (arg == "--scoped-enums") {
+ opts.prefixed_enums = false;
+ opts.scoped_enums = true;
+ } else if (arg == "--no-union-value-namespacing") {
+ opts.union_value_namespacing = false;
+ } else if (arg == "--gen-mutable") {
+ opts.mutable_buffer = true;
+ } else if (arg == "--gen-name-strings") {
+ opts.generate_name_strings = true;
+ } else if (arg == "--gen-object-api") {
+ opts.generate_object_based_api = true;
+ } else if (arg == "--gen-compare") {
+ opts.gen_compare = true;
+ } else if (arg == "--cpp-include") {
+ if (++argi >= argc) Error("missing include following: " + arg, true);
+ opts.cpp_includes.push_back(argv[argi]);
+ } else if (arg == "--cpp-ptr-type") {
+ if (++argi >= argc) Error("missing type following: " + arg, true);
+ opts.cpp_object_api_pointer_type = argv[argi];
+ } else if (arg == "--cpp-str-type") {
+ if (++argi >= argc) Error("missing type following: " + arg, true);
+ opts.cpp_object_api_string_type = argv[argi];
+ } else if (arg == "--cpp-str-flex-ctor") {
+ opts.cpp_object_api_string_flexible_constructor = true;
+ } else if (arg == "--no-cpp-direct-copy") {
+ opts.cpp_direct_copy = false;
+ } else if (arg == "--gen-nullable") {
+ opts.gen_nullable = true;
+ } else if (arg == "--java-checkerframework") {
+ opts.java_checkerframework = true;
+ } else if (arg == "--gen-generated") {
+ opts.gen_generated = true;
+ } else if (arg == "--object-prefix") {
+ if (++argi >= argc) Error("missing prefix following: " + arg, true);
+ opts.object_prefix = argv[argi];
+ } else if (arg == "--object-suffix") {
+ if (++argi >= argc) Error("missing suffix following: " + arg, true);
+ opts.object_suffix = argv[argi];
+ } else if (arg == "--gen-all") {
+ opts.generate_all = true;
+ opts.include_dependence_headers = false;
+ } else if (arg == "--gen-includes") {
+ // Deprecated, remove this option some time in the future.
+ Warn("warning: --gen-includes is deprecated (it is now default)\n");
+ } else if (arg == "--no-includes") {
+ opts.include_dependence_headers = false;
+ } else if (arg == "--gen-onefile") {
+ opts.one_file = true;
+ } else if (arg == "--raw-binary") {
+ raw_binary = true;
+ } else if (arg == "--size-prefixed") {
+ opts.size_prefixed = true;
+ } else if (arg == "--") { // Separator between text and binary inputs.
+ binary_files_from = filenames.size();
+ } else if (arg == "--proto") {
+ opts.proto_mode = true;
+ } else if (arg == "--proto-namespace-suffix") {
+ if (++argi >= argc) Error("missing namespace suffix" + arg, true);
+ opts.proto_namespace_suffix = argv[argi];
+ } else if (arg == "--oneof-union") {
+ opts.proto_oneof_union = true;
+ } else if (arg == "--schema") {
+ schema_binary = true;
+ } else if (arg == "-M") {
+ print_make_rules = true;
+ } else if (arg == "--version") {
+ printf("flatc version %s\n", FLATC_VERSION());
+ exit(0);
+ } else if (arg == "--grpc") {
+ grpc_enabled = true;
+ } else if (arg == "--bfbs-comments") {
+ opts.binary_schema_comments = true;
+ } else if (arg == "--bfbs-builtins") {
+ opts.binary_schema_builtins = true;
+ } else if (arg == "--bfbs-gen-embed") {
+ opts.binary_schema_gen_embed = true;
+ } else if (arg == "--reflect-types") {
+ opts.mini_reflect = IDLOptions::kTypes;
+ } else if (arg == "--reflect-names") {
+ opts.mini_reflect = IDLOptions::kTypesAndNames;
+ } else if (arg == "--require-explicit-ids") {
+ opts.require_explicit_ids = true;
+ } else if (arg == "--root-type") {
+ if (++argi >= argc) Error("missing type following: " + arg, true);
+ opts.root_type = argv[argi];
+ } else if (arg == "--filename-suffix") {
+ if (++argi >= argc) Error("missing filename suffix: " + arg, true);
+ opts.filename_suffix = argv[argi];
+ } else if (arg == "--filename-ext") {
+ if (++argi >= argc) Error("missing filename extension: " + arg, true);
+ opts.filename_extension = argv[argi];
+ } else if (arg == "--force-defaults") {
+ opts.force_defaults = true;
+ } else if (arg == "--force-empty") {
+ opts.set_empty_strings_to_null = false;
+ opts.set_empty_vectors_to_null = false;
+ } else if (arg == "--force-empty-vectors") {
+ opts.set_empty_vectors_to_null = false;
+ } else if (arg == "--java-primitive-has-method") {
+ opts.java_primitive_has_method = true;
+ } else if (arg == "--cs-gen-json-serializer") {
+ opts.cs_gen_json_serializer = true;
+ } else if (arg == "--flexbuffers") {
+ opts.use_flexbuffers = true;
+ } else if (arg == "--gen-jvmstatic") {
+ opts.gen_jvmstatic = true;
+ } else if (arg == "--no-warnings") {
+ opts.no_warnings = true;
+ } else if (arg == "--cpp-std") {
+ if (++argi >= argc)
+ Error("missing C++ standard specification" + arg, true);
+ opts.cpp_std = argv[argi];
+ } else if (arg.rfind("--cpp-std=", 0) == 0) {
+ opts.cpp_std = arg.substr(std::string("--cpp-std=").size());
+ } else if (arg == "--cpp-static-reflection") {
+ opts.cpp_static_reflection = true;
+ } else {
+ for (size_t i = 0; i < params_.num_generators; ++i) {
+ if (arg == params_.generators[i].generator_opt_long ||
+ (params_.generators[i].generator_opt_short &&
+ arg == params_.generators[i].generator_opt_short)) {
+ generator_enabled[i] = true;
+ any_generator = true;
+ opts.lang_to_generate |= params_.generators[i].lang;
+ goto found;
+ }
+ }
+ Error("unknown commandline argument: " + arg, true);
+ found:;
+ }
+ } else {
+ filenames.push_back(flatbuffers::PosixPath(argv[argi]));
+ }
+ }
+
+ if (!filenames.size()) Error("missing input files", false, true);
+
+ if (opts.proto_mode) {
+ if (any_generator)
+ Error("cannot generate code directly from .proto files", true);
+ } else if (!any_generator && conform_to_schema.empty()) {
+ Error("no options: specify at least one generator.", true);
+ }
+
+ flatbuffers::Parser conform_parser;
+ if (!conform_to_schema.empty()) {
+ std::string contents;
+ if (!flatbuffers::LoadFile(conform_to_schema.c_str(), true, &contents))
+ Error("unable to load schema: " + conform_to_schema);
+
+ if (flatbuffers::GetExtension(conform_to_schema) ==
+ reflection::SchemaExtension()) {
+ LoadBinarySchema(conform_parser, conform_to_schema, contents);
+ } else {
+ ParseFile(conform_parser, conform_to_schema, contents,
+ conform_include_directories);
+ }
+ }
+
+ std::unique_ptr<flatbuffers::Parser> parser(new flatbuffers::Parser(opts));
+
+ for (auto file_it = filenames.begin(); file_it != filenames.end();
+ ++file_it) {
+ auto &filename = *file_it;
+ std::string contents;
+ if (!flatbuffers::LoadFile(filename.c_str(), true, &contents))
+ Error("unable to load file: " + filename);
+
+ bool is_binary =
+ static_cast<size_t>(file_it - filenames.begin()) >= binary_files_from;
+ auto ext = flatbuffers::GetExtension(filename);
+ auto is_schema = ext == "fbs" || ext == "proto";
+ auto is_binary_schema = ext == reflection::SchemaExtension();
+ if (is_binary) {
+ parser->builder_.Clear();
+ parser->builder_.PushFlatBuffer(
+ reinterpret_cast<const uint8_t *>(contents.c_str()),
+ contents.length());
+ if (!raw_binary) {
+ // Generally reading binaries that do not correspond to the schema
+ // will crash, and sadly there's no way around that when the binary
+ // does not contain a file identifier.
+ // We'd expect that typically any binary used as a file would have
+ // such an identifier, so by default we require them to match.
+ if (!parser->file_identifier_.length()) {
+ Error("current schema has no file_identifier: cannot test if \"" +
+ filename +
+ "\" matches the schema, use --raw-binary to read this file"
+ " anyway.");
+ } else if (!flatbuffers::BufferHasIdentifier(
+ contents.c_str(), parser->file_identifier_.c_str(),
+ opts.size_prefixed)) {
+ Error("binary \"" + filename +
+ "\" does not have expected file_identifier \"" +
+ parser->file_identifier_ +
+ "\", use --raw-binary to read this file anyway.");
+ }
+ }
+ } else {
+ // Check if file contains 0 bytes.
+ if (!opts.use_flexbuffers && !is_binary_schema &&
+ contents.length() != strlen(contents.c_str())) {
+ Error("input file appears to be binary: " + filename, true);
+ }
+ if (is_schema) {
+ // If we're processing multiple schemas, make sure to start each
+ // one from scratch. If it depends on previous schemas it must do
+ // so explicitly using an include.
+ parser.reset(new flatbuffers::Parser(opts));
+ }
+ if (is_binary_schema) {
+ LoadBinarySchema(*parser.get(), filename, contents);
+ }
+ if (opts.use_flexbuffers) {
+ if (opts.lang_to_generate == IDLOptions::kJson) {
+ parser->flex_root_ = flexbuffers::GetRoot(
+ reinterpret_cast<const uint8_t *>(contents.c_str()),
+ contents.size());
+ } else {
+ parser->flex_builder_.Clear();
+ ParseFile(*parser.get(), filename, contents, include_directories);
+ }
+ } else {
+ ParseFile(*parser.get(), filename, contents, include_directories);
+ if (!is_schema && !parser->builder_.GetSize()) {
+ // If a file doesn't end in .fbs, it must be json/binary. Ensure we
+ // didn't just parse a schema with a different extension.
+ Error("input file is neither json nor a .fbs (schema) file: " +
+ filename,
+ true);
+ }
+ }
+ if ((is_schema || is_binary_schema) && !conform_to_schema.empty()) {
+ auto err = parser->ConformTo(conform_parser);
+ if (!err.empty()) Error("schemas don\'t conform: " + err);
+ }
+ if (schema_binary || opts.binary_schema_gen_embed) {
+ parser->Serialize();
+ }
+ if (schema_binary) {
+ parser->file_extension_ = reflection::SchemaExtension();
+ }
+ }
+
+ std::string filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(filename));
+
+ for (size_t i = 0; i < params_.num_generators; ++i) {
+ parser->opts.lang = params_.generators[i].lang;
+ if (generator_enabled[i]) {
+ if (!print_make_rules) {
+ flatbuffers::EnsureDirExists(output_path);
+ if ((!params_.generators[i].schema_only ||
+ (is_schema || is_binary_schema)) &&
+ !params_.generators[i].generate(*parser.get(), output_path,
+ filebase)) {
+ Error(std::string("Unable to generate ") +
+ params_.generators[i].lang_name + " for " + filebase);
+ }
+ } else {
+ if (params_.generators[i].make_rule == nullptr) {
+ Error(std::string("Cannot generate make rule for ") +
+ params_.generators[i].lang_name);
+ } else {
+ std::string make_rule = params_.generators[i].make_rule(
+ *parser.get(), output_path, filename);
+ if (!make_rule.empty())
+ printf("%s\n",
+ flatbuffers::WordWrap(make_rule, 80, " ", " \\").c_str());
+ }
+ }
+ if (grpc_enabled) {
+ if (params_.generators[i].generateGRPC != nullptr) {
+ if (!params_.generators[i].generateGRPC(*parser.get(), output_path,
+ filebase)) {
+ Error(std::string("Unable to generate GRPC interface for") +
+ params_.generators[i].lang_name);
+ }
+ } else {
+ Warn(std::string("GRPC interface generator not implemented for ") +
+ params_.generators[i].lang_name);
+ }
+ }
+ }
+ }
+
+ if (!opts.root_type.empty()) {
+ if (!parser->SetRootType(opts.root_type.c_str()))
+ Error("unknown root type: " + opts.root_type);
+ else if (parser->root_struct_def_->fixed)
+ Error("root type must be a table");
+ }
+
+ if (opts.proto_mode) GenerateFBS(*parser.get(), output_path, filebase);
+
+ // We do not want to generate code for the definitions in this file
+ // in any files coming up next.
+ parser->MarkGenerated();
+ }
+ return 0;
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/flatc_main.cpp b/contrib/libs/flatbuffers/src/flatc_main.cpp
new file mode 100644
index 0000000000..31ccbc7185
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/flatc_main.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "flatbuffers/flatc.h"
+#include "flatbuffers/util.h"
+
+static const char *g_program_name = nullptr;
+
+static void Warn(const flatbuffers::FlatCompiler *flatc,
+ const std::string &warn, bool show_exe_name) {
+ (void)flatc;
+ if (show_exe_name) { printf("%s: ", g_program_name); }
+ printf("warning: %s\n", warn.c_str());
+}
+
+static void Error(const flatbuffers::FlatCompiler *flatc,
+ const std::string &err, bool usage, bool show_exe_name) {
+ if (show_exe_name) { printf("%s: ", g_program_name); }
+ printf("error: %s\n", err.c_str());
+ if (usage && flatc) {
+ printf("%s", flatc->GetUsageString(g_program_name).c_str());
+ }
+ exit(1);
+}
+
+namespace flatbuffers {
+void LogCompilerWarn(const std::string &warn) {
+ Warn(static_cast<const flatbuffers::FlatCompiler *>(nullptr), warn, true);
+}
+void LogCompilerError(const std::string &err) {
+ Error(static_cast<const flatbuffers::FlatCompiler *>(nullptr), err, false,
+ true);
+}
+} // namespace flatbuffers
+
+int main(int argc, const char *argv[]) {
+ // Prevent Appveyor-CI hangs.
+ flatbuffers::SetupDefaultCRTReportMode();
+
+ g_program_name = argv[0];
+
+ const flatbuffers::FlatCompiler::Generator generators[] = {
+ { flatbuffers::GenerateBinary, "-b", "--binary", "binary", false, nullptr,
+ flatbuffers::IDLOptions::kBinary,
+ "Generate wire format binaries for any data definitions",
+ flatbuffers::BinaryMakeRule },
+ { flatbuffers::GenerateTextFile, "-t", "--json", "text", false, nullptr,
+ flatbuffers::IDLOptions::kJson,
+ "Generate text output for any data definitions",
+ flatbuffers::TextMakeRule },
+ { flatbuffers::GenerateCPP, "-c", "--cpp", "C++", true,
+ flatbuffers::GenerateCppGRPC, flatbuffers::IDLOptions::kCpp,
+ "Generate C++ headers for tables/structs", flatbuffers::CPPMakeRule },
+ { flatbuffers::GenerateGo, "-g", "--go", "Go", true,
+ flatbuffers::GenerateGoGRPC, flatbuffers::IDLOptions::kGo,
+ "Generate Go files for tables/structs", nullptr },
+ { flatbuffers::GenerateJava, "-j", "--java", "Java", true,
+ flatbuffers::GenerateJavaGRPC, flatbuffers::IDLOptions::kJava,
+ "Generate Java classes for tables/structs",
+ flatbuffers::JavaCSharpMakeRule },
+ { flatbuffers::GenerateDart, "-d", "--dart", "Dart", true, nullptr,
+ flatbuffers::IDLOptions::kDart,
+ "Generate Dart classes for tables/structs", flatbuffers::DartMakeRule },
+ { flatbuffers::GenerateTS, "-T", "--ts", "TypeScript", true,
+ flatbuffers::GenerateTSGRPC, flatbuffers::IDLOptions::kTs,
+ "Generate TypeScript code for tables/structs", flatbuffers::TSMakeRule },
+ { flatbuffers::GenerateCSharp, "-n", "--csharp", "C#", true, nullptr,
+ flatbuffers::IDLOptions::kCSharp,
+ "Generate C# classes for tables/structs",
+ flatbuffers::JavaCSharpMakeRule },
+ { flatbuffers::GeneratePython, "-p", "--python", "Python", true,
+ flatbuffers::GeneratePythonGRPC, flatbuffers::IDLOptions::kPython,
+ "Generate Python files for tables/structs", nullptr },
+ { flatbuffers::GenerateLobster, nullptr, "--lobster", "Lobster", true,
+ nullptr, flatbuffers::IDLOptions::kLobster,
+ "Generate Lobster files for tables/structs", nullptr },
+ { flatbuffers::GenerateLua, "-l", "--lua", "Lua", true, nullptr,
+ flatbuffers::IDLOptions::kLua, "Generate Lua files for tables/structs",
+ nullptr },
+ { flatbuffers::GenerateRust, "-r", "--rust", "Rust", true, nullptr,
+ flatbuffers::IDLOptions::kRust, "Generate Rust files for tables/structs",
+ flatbuffers::RustMakeRule },
+ { flatbuffers::GeneratePhp, nullptr, "--php", "PHP", true, nullptr,
+ flatbuffers::IDLOptions::kPhp, "Generate PHP files for tables/structs",
+ nullptr },
+ { flatbuffers::GenerateKotlin, nullptr, "--kotlin", "Kotlin", true, nullptr,
+ flatbuffers::IDLOptions::kKotlin,
+ "Generate Kotlin classes for tables/structs", nullptr },
+ { flatbuffers::GenerateJsonSchema, nullptr, "--jsonschema", "JsonSchema",
+ true, nullptr, flatbuffers::IDLOptions::kJsonSchema,
+ "Generate Json schema", nullptr },
+ { flatbuffers::GenerateSwift, nullptr, "--swift", "swift", true,
+ flatbuffers::GenerateSwiftGRPC, flatbuffers::IDLOptions::kSwift,
+ "Generate Swift files for tables/structs", nullptr },
+ { flatbuffers::GenerateCPPYandexMapsIter, nullptr, "--yandex-maps-iter", "C++Iter",
+ true, nullptr, flatbuffers::IDLOptions::kCppYandexMapsIter,
+ "Generate C++ template headers for tables/structs", nullptr },
+ };
+
+ flatbuffers::FlatCompiler::InitParams params;
+ params.generators = generators;
+ params.num_generators = sizeof(generators) / sizeof(generators[0]);
+ params.warn_fn = Warn;
+ params.error_fn = Error;
+
+ flatbuffers::FlatCompiler flatc(params);
+ return flatc.Compile(argc - 1, argv + 1);
+}
diff --git a/contrib/libs/flatbuffers/src/idl_gen_cpp.cpp b/contrib/libs/flatbuffers/src/idl_gen_cpp.cpp
new file mode 100644
index 0000000000..a33697eaed
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_cpp.cpp
@@ -0,0 +1,3514 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/flatc.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+// Make numerical literal with type-suffix.
+// This function is only needed for C++! Other languages do not need it.
+static inline std::string NumToStringCpp(std::string val, BaseType type) {
+ // Avoid issues with -2147483648, -9223372036854775808.
+ switch (type) {
+ case BASE_TYPE_INT:
+ return (val != "-2147483648") ? val : ("(-2147483647 - 1)");
+ case BASE_TYPE_ULONG: return (val == "0") ? val : (val + "ULL");
+ case BASE_TYPE_LONG:
+ if (val == "-9223372036854775808")
+ return "(-9223372036854775807LL - 1LL)";
+ else
+ return (val == "0") ? val : (val + "LL");
+ default: return val;
+ }
+}
+
+static std::string GenIncludeGuard(const std::string &file_name,
+ const Namespace &name_space,
+ const std::string &postfix = "") {
+ // Generate include guard.
+ std::string guard = file_name;
+ // Remove any non-alpha-numeric characters that may appear in a filename.
+ struct IsAlnum {
+ bool operator()(char c) const { return !is_alnum(c); }
+ };
+ guard.erase(std::remove_if(guard.begin(), guard.end(), IsAlnum()),
+ guard.end());
+ guard = "FLATBUFFERS_GENERATED_" + guard;
+ guard += "_";
+ // For further uniqueness, also add the namespace.
+ for (auto it = name_space.components.begin();
+ it != name_space.components.end(); ++it) {
+ guard += *it + "_";
+ }
+ // Anything extra to add to the guard?
+ if (!postfix.empty()) { guard += postfix + "_"; }
+ guard += "H_";
+ std::transform(guard.begin(), guard.end(), guard.begin(), CharToUpper);
+ return guard;
+}
+
+namespace cpp {
+
+enum CppStandard { CPP_STD_X0 = 0, CPP_STD_11, CPP_STD_17 };
+
+// Define a style of 'struct' constructor if it has 'Array' fields.
+enum GenArrayArgMode {
+ kArrayArgModeNone, // don't generate initialization args
+ kArrayArgModeSpanStatic, // generate flatbuffers::span<T,N>
+};
+
+// Extension of IDLOptions for cpp-generator.
+struct IDLOptionsCpp : public IDLOptions {
+ // All fields start with 'g_' prefix to distinguish from the base IDLOptions.
+ CppStandard g_cpp_std; // Base version of C++ standard.
+ bool g_only_fixed_enums; // Generate underlaying type for all enums.
+
+ IDLOptionsCpp(const IDLOptions &opts)
+ : IDLOptions(opts), g_cpp_std(CPP_STD_11), g_only_fixed_enums(true) {}
+};
+
+class CppGenerator : public BaseGenerator {
+ public:
+ CppGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name, IDLOptionsCpp opts)
+ : BaseGenerator(parser, path, file_name, "", "::", "h"),
+ cur_name_space_(nullptr),
+ opts_(opts),
+ float_const_gen_("std::numeric_limits<double>::",
+ "std::numeric_limits<float>::", "quiet_NaN()",
+ "infinity()") {
+ static const char *const keywords[] = {
+ "alignas",
+ "alignof",
+ "and",
+ "and_eq",
+ "asm",
+ "atomic_cancel",
+ "atomic_commit",
+ "atomic_noexcept",
+ "auto",
+ "bitand",
+ "bitor",
+ "bool",
+ "break",
+ "case",
+ "catch",
+ "char",
+ "char16_t",
+ "char32_t",
+ "class",
+ "compl",
+ "concept",
+ "const",
+ "constexpr",
+ "const_cast",
+ "continue",
+ "co_await",
+ "co_return",
+ "co_yield",
+ "decltype",
+ "default",
+ "delete",
+ "do",
+ "double",
+ "dynamic_cast",
+ "else",
+ "enum",
+ "explicit",
+ "export",
+ "extern",
+ "false",
+ "float",
+ "for",
+ "friend",
+ "goto",
+ "if",
+ "import",
+ "inline",
+ "int",
+ "long",
+ "module",
+ "mutable",
+ "namespace",
+ "new",
+ "noexcept",
+ "not",
+ "not_eq",
+ "nullptr",
+ "operator",
+ "or",
+ "or_eq",
+ "private",
+ "protected",
+ "public",
+ "register",
+ "reinterpret_cast",
+ "requires",
+ "return",
+ "short",
+ "signed",
+ "sizeof",
+ "static",
+ "static_assert",
+ "static_cast",
+ "struct",
+ "switch",
+ "synchronized",
+ "template",
+ "this",
+ "thread_local",
+ "throw",
+ "true",
+ "try",
+ "typedef",
+ "typeid",
+ "typename",
+ "union",
+ "unsigned",
+ "using",
+ "virtual",
+ "void",
+ "volatile",
+ "wchar_t",
+ "while",
+ "xor",
+ "xor_eq",
+ nullptr,
+ };
+ for (auto kw = keywords; *kw; kw++) keywords_.insert(*kw);
+ }
+
+ void GenIncludeDependencies() {
+ int num_includes = 0;
+ if (opts_.generate_object_based_api) {
+ for (auto it = parser_.native_included_files_.begin();
+ it != parser_.native_included_files_.end(); ++it) {
+ code_ += "#include \"" + *it + "\"";
+ num_includes++;
+ }
+ }
+ for (auto it = parser_.included_files_.begin();
+ it != parser_.included_files_.end(); ++it) {
+ if (it->second.empty()) continue;
+ auto noext = flatbuffers::StripExtension(it->second);
+ auto basename = flatbuffers::StripPath(noext);
+ auto includeName =
+ GeneratedFileName(opts_.include_prefix,
+ opts_.keep_include_path ? noext : basename, opts_);
+ code_ += "#include \"" + includeName + "\"";
+ num_includes++;
+ }
+ if (num_includes) code_ += "";
+ }
+
+ void GenExtraIncludes() {
+ for (std::size_t i = 0; i < opts_.cpp_includes.size(); ++i) {
+ code_ += "#include \"" + opts_.cpp_includes[i] + "\"";
+ }
+ if (!opts_.cpp_includes.empty()) { code_ += ""; }
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+
+ std::string Name(const Definition &def) const {
+ return EscapeKeyword(def.name);
+ }
+
+ std::string Name(const EnumVal &ev) const { return EscapeKeyword(ev.name); }
+
+ bool generate_bfbs_embed() {
+ code_.Clear();
+ code_ += "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ // If we don't have a root struct definition,
+ if (!parser_.root_struct_def_) {
+ // put a comment in the output why there is no code generated.
+ code_ += "// Binary schema not generated, no root struct found";
+ } else {
+ auto &struct_def = *parser_.root_struct_def_;
+ const auto include_guard =
+ GenIncludeGuard(file_name_, *struct_def.defined_namespace, "bfbs");
+
+ code_ += "#ifndef " + include_guard;
+ code_ += "#define " + include_guard;
+ code_ += "";
+ if (parser_.opts.gen_nullable) {
+ code_ += "#pragma clang system_header\n\n";
+ }
+
+ SetNameSpace(struct_def.defined_namespace);
+ auto name = Name(struct_def);
+ code_.SetValue("STRUCT_NAME", name);
+
+ // Create code to return the binary schema data.
+ auto binary_schema_hex_text =
+ BufferToHexText(parser_.builder_.GetBufferPointer(),
+ parser_.builder_.GetSize(), 105, " ", "");
+
+ code_ += "struct {{STRUCT_NAME}}BinarySchema {";
+ code_ += " static const uint8_t *data() {";
+ code_ += " // Buffer containing the binary schema.";
+ code_ += " static const uint8_t bfbsData[" +
+ NumToString(parser_.builder_.GetSize()) + "] = {";
+ code_ += binary_schema_hex_text;
+ code_ += " };";
+ code_ += " return bfbsData;";
+ code_ += " }";
+ code_ += " static size_t size() {";
+ code_ += " return " + NumToString(parser_.builder_.GetSize()) + ";";
+ code_ += " }";
+ code_ += " const uint8_t *begin() {";
+ code_ += " return data();";
+ code_ += " }";
+ code_ += " const uint8_t *end() {";
+ code_ += " return data() + size();";
+ code_ += " }";
+ code_ += "};";
+ code_ += "";
+
+ if (cur_name_space_) SetNameSpace(nullptr);
+
+ // Close the include guard.
+ code_ += "#endif // " + include_guard;
+ }
+
+ // We are just adding "_bfbs" to the generated filename.
+ const auto file_path =
+ GeneratedFileName(path_, file_name_ + "_bfbs", opts_);
+ const auto final_code = code_.ToString();
+
+ return SaveFile(file_path.c_str(), final_code, false);
+ }
+
+ // Iterate through all definitions we haven't generate code for (enums,
+ // structs, and tables) and output them to a single file.
+ bool generate() {
+ code_.Clear();
+ code_ += "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ const auto include_guard =
+ GenIncludeGuard(file_name_, *parser_.current_namespace_);
+ code_ += "#ifndef " + include_guard;
+ code_ += "#define " + include_guard;
+ code_ += "";
+
+ if (opts_.gen_nullable) { code_ += "#pragma clang system_header\n\n"; }
+
+ code_ += "#include <contrib/libs/flatbuffers/include/flatbuffers/flatbuffers.h>";
+ if (parser_.uses_flexbuffers_) {
+ code_ += "#include <contrib/libs/flatbuffers/include/flatbuffers/flexbuffers.h>";
+ }
+ code_ += "";
+
+ if (opts_.include_dependence_headers) { GenIncludeDependencies(); }
+ GenExtraIncludes();
+
+ FLATBUFFERS_ASSERT(!cur_name_space_);
+
+ // Generate forward declarations for all structs/tables, since they may
+ // have circular references.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ code_ += "struct " + Name(struct_def) + ";";
+ if (!struct_def.fixed) {
+ code_ += "struct " + Name(struct_def) + "Builder;";
+ }
+ if (opts_.generate_object_based_api) {
+ auto nativeName = NativeName(Name(struct_def), &struct_def, opts_);
+ if (!struct_def.fixed) { code_ += "struct " + nativeName + ";"; }
+ }
+ code_ += "";
+ }
+ }
+
+ // Generate forward declarations for all equal operators
+ if (opts_.generate_object_based_api && opts_.gen_compare) {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ auto nativeName = NativeName(Name(struct_def), &struct_def, opts_);
+ code_ += "bool operator==(const " + nativeName + " &lhs, const " +
+ nativeName + " &rhs);";
+ code_ += "bool operator!=(const " + nativeName + " &lhs, const " +
+ nativeName + " &rhs);";
+ }
+ }
+ code_ += "";
+ }
+
+ // Generate preablmle code for mini reflection.
+ if (opts_.mini_reflect != IDLOptions::kNone) {
+ // To break cyclic dependencies, first pre-declare all tables/structs.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenMiniReflectPre(&struct_def);
+ }
+ }
+ }
+
+ // Generate code for all the enum declarations.
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ const auto &enum_def = **it;
+ if (!enum_def.generated) {
+ SetNameSpace(enum_def.defined_namespace);
+ GenEnum(enum_def);
+ }
+ }
+
+ // Generate code for all structs, then all tables.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (struct_def.fixed && !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenStruct(struct_def);
+ }
+ }
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.fixed && !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenTable(struct_def);
+ }
+ }
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.fixed && !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenTablePost(struct_def);
+ }
+ }
+
+ // Generate code for union verifiers.
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ const auto &enum_def = **it;
+ if (enum_def.is_union && !enum_def.generated) {
+ SetNameSpace(enum_def.defined_namespace);
+ GenUnionPost(enum_def);
+ }
+ }
+
+ // Generate code for mini reflection.
+ if (opts_.mini_reflect != IDLOptions::kNone) {
+ // Then the unions/enums that may refer to them.
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ const auto &enum_def = **it;
+ if (!enum_def.generated) {
+ SetNameSpace(enum_def.defined_namespace);
+ GenMiniReflect(nullptr, &enum_def);
+ }
+ }
+ // Then the full tables/structs.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenMiniReflect(&struct_def, nullptr);
+ }
+ }
+ }
+
+ // Generate convenient global helper functions:
+ if (parser_.root_struct_def_) {
+ auto &struct_def = *parser_.root_struct_def_;
+ SetNameSpace(struct_def.defined_namespace);
+ auto name = Name(struct_def);
+ auto qualified_name = cur_name_space_->GetFullyQualifiedName(name);
+ auto cpp_name = TranslateNameSpace(qualified_name);
+
+ code_.SetValue("STRUCT_NAME", name);
+ code_.SetValue("CPP_NAME", cpp_name);
+ code_.SetValue("NULLABLE_EXT", NullableExtension());
+
+ // The root datatype accessor:
+ code_ += "inline \\";
+ code_ +=
+ "const {{CPP_NAME}} *{{NULLABLE_EXT}}Get{{STRUCT_NAME}}(const void "
+ "*buf) {";
+ code_ += " return flatbuffers::GetRoot<{{CPP_NAME}}>(buf);";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline \\";
+ code_ +=
+ "const {{CPP_NAME}} "
+ "*{{NULLABLE_EXT}}GetSizePrefixed{{STRUCT_NAME}}(const void "
+ "*buf) {";
+ code_ += " return flatbuffers::GetSizePrefixedRoot<{{CPP_NAME}}>(buf);";
+ code_ += "}";
+ code_ += "";
+
+ if (opts_.mutable_buffer) {
+ code_ += "inline \\";
+ code_ += "{{STRUCT_NAME}} *GetMutable{{STRUCT_NAME}}(void *buf) {";
+ code_ += " return flatbuffers::GetMutableRoot<{{STRUCT_NAME}}>(buf);";
+ code_ += "}";
+ code_ += "";
+ }
+
+ if (parser_.file_identifier_.length()) {
+ // Return the identifier
+ code_ += "inline const char *{{STRUCT_NAME}}Identifier() {";
+ code_ += " return \"" + parser_.file_identifier_ + "\";";
+ code_ += "}";
+ code_ += "";
+
+ // Check if a buffer has the identifier.
+ code_ += "inline \\";
+ code_ += "bool {{STRUCT_NAME}}BufferHasIdentifier(const void *buf) {";
+ code_ += " return flatbuffers::BufferHasIdentifier(";
+ code_ += " buf, {{STRUCT_NAME}}Identifier());";
+ code_ += "}";
+ code_ += "";
+ }
+
+ // The root verifier.
+ if (parser_.file_identifier_.length()) {
+ code_.SetValue("ID", name + "Identifier()");
+ } else {
+ code_.SetValue("ID", "nullptr");
+ }
+
+ code_ += "inline bool Verify{{STRUCT_NAME}}Buffer(";
+ code_ += " flatbuffers::Verifier &verifier) {";
+ code_ += " return verifier.VerifyBuffer<{{CPP_NAME}}>({{ID}});";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline bool VerifySizePrefixed{{STRUCT_NAME}}Buffer(";
+ code_ += " flatbuffers::Verifier &verifier) {";
+ code_ +=
+ " return verifier.VerifySizePrefixedBuffer<{{CPP_NAME}}>({{ID}});";
+ code_ += "}";
+ code_ += "";
+
+ if (parser_.file_extension_.length()) {
+ // Return the extension
+ code_ += "inline const char *{{STRUCT_NAME}}Extension() {";
+ code_ += " return \"" + parser_.file_extension_ + "\";";
+ code_ += "}";
+ code_ += "";
+ }
+
+ // Finish a buffer with a given root object:
+ code_ += "inline void Finish{{STRUCT_NAME}}Buffer(";
+ code_ += " flatbuffers::FlatBufferBuilder &fbb,";
+ code_ += " flatbuffers::Offset<{{CPP_NAME}}> root) {";
+ if (parser_.file_identifier_.length())
+ code_ += " fbb.Finish(root, {{STRUCT_NAME}}Identifier());";
+ else
+ code_ += " fbb.Finish(root);";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline void FinishSizePrefixed{{STRUCT_NAME}}Buffer(";
+ code_ += " flatbuffers::FlatBufferBuilder &fbb,";
+ code_ += " flatbuffers::Offset<{{CPP_NAME}}> root) {";
+ if (parser_.file_identifier_.length())
+ code_ += " fbb.FinishSizePrefixed(root, {{STRUCT_NAME}}Identifier());";
+ else
+ code_ += " fbb.FinishSizePrefixed(root);";
+ code_ += "}";
+ code_ += "";
+
+ if (opts_.generate_object_based_api) {
+ // A convenient root unpack function.
+ auto native_name = WrapNativeNameInNameSpace(struct_def, opts_);
+ code_.SetValue("UNPACK_RETURN",
+ GenTypeNativePtr(native_name, nullptr, false));
+ code_.SetValue("UNPACK_TYPE",
+ GenTypeNativePtr(native_name, nullptr, true));
+
+ code_ += "inline {{UNPACK_RETURN}} UnPack{{STRUCT_NAME}}(";
+ code_ += " const void *buf,";
+ code_ += " const flatbuffers::resolver_function_t *res = nullptr) {";
+ code_ += " return {{UNPACK_TYPE}}\\";
+ code_ += "(Get{{STRUCT_NAME}}(buf)->UnPack(res));";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline {{UNPACK_RETURN}} UnPackSizePrefixed{{STRUCT_NAME}}(";
+ code_ += " const void *buf,";
+ code_ += " const flatbuffers::resolver_function_t *res = nullptr) {";
+ code_ += " return {{UNPACK_TYPE}}\\";
+ code_ += "(GetSizePrefixed{{STRUCT_NAME}}(buf)->UnPack(res));";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ if (cur_name_space_) SetNameSpace(nullptr);
+
+ // Close the include guard.
+ code_ += "#endif // " + include_guard;
+
+ const auto file_path = GeneratedFileName(path_, file_name_, opts_);
+ const auto final_code = code_.ToString();
+
+ // Save the file and optionally generate the binary schema code.
+ return SaveFile(file_path.c_str(), final_code, false) &&
+ (!parser_.opts.binary_schema_gen_embed || generate_bfbs_embed());
+ }
+
+ private:
+ CodeWriter code_;
+
+ std::unordered_set<std::string> keywords_;
+
+ // This tracks the current namespace so we can insert namespace declarations.
+ const Namespace *cur_name_space_;
+
+ const IDLOptionsCpp opts_;
+ const TypedFloatConstantGenerator float_const_gen_;
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+ // Translates a qualified name in flatbuffer text format to the same name in
+ // the equivalent C++ namespace.
+ static std::string TranslateNameSpace(const std::string &qualified_name) {
+ std::string cpp_qualified_name = qualified_name;
+ size_t start_pos = 0;
+ while ((start_pos = cpp_qualified_name.find('.', start_pos)) !=
+ std::string::npos) {
+ cpp_qualified_name.replace(start_pos, 1, "::");
+ }
+ return cpp_qualified_name;
+ }
+
+ bool TypeHasKey(const Type &type) {
+ if (type.base_type != BASE_TYPE_STRUCT) { return false; }
+ for (auto it = type.struct_def->fields.vec.begin();
+ it != type.struct_def->fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.key) { return true; }
+ }
+ return false;
+ }
+
+ bool VectorElementUserFacing(const Type &type) const {
+ return opts_.g_cpp_std >= cpp::CPP_STD_17 && opts_.g_only_fixed_enums &&
+ IsEnum(type);
+ }
+
+ void GenComment(const std::vector<std::string> &dc, const char *prefix = "") {
+ std::string text;
+ ::flatbuffers::GenComment(dc, &text, nullptr, prefix);
+ code_ += text + "\\";
+ }
+
+ // Return a C++ type from the table in idl.h
+ std::string GenTypeBasic(const Type &type, bool user_facing_type) const {
+ // clang-format off
+ static const char *const ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ #CTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ if (user_facing_type) {
+ if (type.enum_def) return WrapInNameSpace(*type.enum_def);
+ if (type.base_type == BASE_TYPE_BOOL) return "bool";
+ }
+ return ctypename[type.base_type];
+ }
+
+ // Return a C++ pointer type, specialized to the actual struct/table types,
+ // and vector element types.
+ std::string GenTypePointer(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: {
+ return "flatbuffers::String";
+ }
+ case BASE_TYPE_VECTOR: {
+ const auto type_name = GenTypeWire(
+ type.VectorType(), "", VectorElementUserFacing(type.VectorType()));
+ return "flatbuffers::Vector<" + type_name + ">";
+ }
+ case BASE_TYPE_STRUCT: {
+ return WrapInNameSpace(*type.struct_def);
+ }
+ case BASE_TYPE_UNION:
+ // fall through
+ default: {
+ return "void";
+ }
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) specifically for
+ // building a flatbuffer.
+ std::string GenTypeWire(const Type &type, const char *postfix,
+ bool user_facing_type) const {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, user_facing_type) + postfix;
+ } else if (IsStruct(type)) {
+ return "const " + GenTypePointer(type) + " *";
+ } else {
+ return "flatbuffers::Offset<" + GenTypePointer(type) + ">" + postfix;
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) that reflects its
+ // serialized size.
+ std::string GenTypeSize(const Type &type) const {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, false);
+ } else if (IsStruct(type)) {
+ return GenTypePointer(type);
+ } else {
+ return "flatbuffers::uoffset_t";
+ }
+ }
+
+ std::string NullableExtension() {
+ return opts_.gen_nullable ? " _Nullable " : "";
+ }
+
+ static std::string NativeName(const std::string &name, const StructDef *sd,
+ const IDLOptions &opts) {
+ return sd && !sd->fixed ? opts.object_prefix + name + opts.object_suffix
+ : name;
+ }
+
+ std::string WrapNativeNameInNameSpace(const StructDef &struct_def,
+ const IDLOptions &opts) {
+ return WrapInNameSpace(struct_def.defined_namespace,
+ NativeName(Name(struct_def), &struct_def, opts));
+ }
+
+ const std::string &PtrType(const FieldDef *field) {
+ auto attr = field ? field->attributes.Lookup("cpp_ptr_type") : nullptr;
+ return attr ? attr->constant : opts_.cpp_object_api_pointer_type;
+ }
+
+ const std::string NativeString(const FieldDef *field) {
+ auto attr = field ? field->attributes.Lookup("cpp_str_type") : nullptr;
+ auto &ret = attr ? attr->constant : opts_.cpp_object_api_string_type;
+ if (ret.empty()) { return "std::string"; }
+ return ret;
+ }
+
+ bool FlexibleStringConstructor(const FieldDef *field) {
+ auto attr = field
+ ? (field->attributes.Lookup("cpp_str_flex_ctor") != nullptr)
+ : false;
+ auto ret = attr ? attr : opts_.cpp_object_api_string_flexible_constructor;
+ return ret && NativeString(field) !=
+ "std::string"; // Only for custom string types.
+ }
+
+ std::string GenTypeNativePtr(const std::string &type, const FieldDef *field,
+ bool is_constructor) {
+ auto &ptr_type = PtrType(field);
+ if (ptr_type != "naked") {
+ return (ptr_type != "default_ptr_type"
+ ? ptr_type
+ : opts_.cpp_object_api_pointer_type) +
+ "<" + type + ">";
+ } else if (is_constructor) {
+ return "";
+ } else {
+ return type + " *";
+ }
+ }
+
+ std::string GenPtrGet(const FieldDef &field) {
+ auto cpp_ptr_type_get = field.attributes.Lookup("cpp_ptr_type_get");
+ if (cpp_ptr_type_get) return cpp_ptr_type_get->constant;
+ auto &ptr_type = PtrType(&field);
+ return ptr_type == "naked" ? "" : ".get()";
+ }
+
+ std::string GenOptionalNull() { return "flatbuffers::nullopt"; }
+
+ std::string GenOptionalDecl(const Type &type) {
+ return "flatbuffers::Optional<" + GenTypeBasic(type, true) + ">";
+ }
+
+ std::string GenTypeNative(const Type &type, bool invector,
+ const FieldDef &field) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: {
+ return NativeString(&field);
+ }
+ case BASE_TYPE_VECTOR: {
+ const auto type_name = GenTypeNative(type.VectorType(), true, field);
+ if (type.struct_def &&
+ type.struct_def->attributes.Lookup("native_custom_alloc")) {
+ auto native_custom_alloc =
+ type.struct_def->attributes.Lookup("native_custom_alloc");
+ return "std::vector<" + type_name + "," +
+ native_custom_alloc->constant + "<" + type_name + ">>";
+ } else
+ return "std::vector<" + type_name + ">";
+ }
+ case BASE_TYPE_STRUCT: {
+ auto type_name = WrapInNameSpace(*type.struct_def);
+ if (IsStruct(type)) {
+ auto native_type = type.struct_def->attributes.Lookup("native_type");
+ if (native_type) { type_name = native_type->constant; }
+ if (invector || field.native_inline) {
+ return type_name;
+ } else {
+ return GenTypeNativePtr(type_name, &field, false);
+ }
+ } else {
+ return GenTypeNativePtr(
+ WrapNativeNameInNameSpace(*type.struct_def, opts_), &field,
+ false);
+ }
+ }
+ case BASE_TYPE_UNION: {
+ auto type_name = WrapInNameSpace(*type.enum_def);
+ return type_name + "Union";
+ }
+ default: {
+ return field.IsScalarOptional() ? GenOptionalDecl(type)
+ : GenTypeBasic(type, true);
+ }
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) specifically for
+ // using a flatbuffer.
+ std::string GenTypeGet(const Type &type, const char *afterbasic,
+ const char *beforeptr, const char *afterptr,
+ bool user_facing_type) {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, user_facing_type) + afterbasic;
+ } else if (IsArray(type)) {
+ auto element_type = type.VectorType();
+ // Check if enum arrays are used in C++ without specifying --scoped-enums
+ if (IsEnum(element_type) && !opts_.g_only_fixed_enums) {
+ LogCompilerError(
+ "--scoped-enums must be enabled to use enum arrays in C++");
+ FLATBUFFERS_ASSERT(true);
+ }
+ return beforeptr +
+ (IsScalar(element_type.base_type)
+ ? GenTypeBasic(element_type, user_facing_type)
+ : GenTypePointer(element_type)) +
+ afterptr;
+ } else {
+ return beforeptr + GenTypePointer(type) + afterptr;
+ }
+ }
+
+ std::string GenTypeSpan(const Type &type, bool immutable, size_t extent) {
+ // Generate "flatbuffers::span<const U, extent>".
+ FLATBUFFERS_ASSERT(IsSeries(type) && "unexpected type");
+ auto element_type = type.VectorType();
+ std::string text = "flatbuffers::span<";
+ text += immutable ? "const " : "";
+ if (IsScalar(element_type.base_type)) {
+ text += GenTypeBasic(element_type, IsEnum(element_type));
+ } else {
+ switch (element_type.base_type) {
+ case BASE_TYPE_STRING: {
+ text += "char";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ FLATBUFFERS_ASSERT(type.struct_def);
+ text += WrapInNameSpace(*type.struct_def);
+ break;
+ }
+ default:
+ FLATBUFFERS_ASSERT(false && "unexpected element's type");
+ break;
+ }
+ }
+ if (extent != flatbuffers::dynamic_extent) {
+ text += ", ";
+ text += NumToString(extent);
+ }
+ text += "> ";
+ return text;
+ }
+
+ std::string GenEnumValDecl(const EnumDef &enum_def,
+ const std::string &enum_val) const {
+ return opts_.prefixed_enums ? Name(enum_def) + "_" + enum_val : enum_val;
+ }
+
+ std::string GetEnumValUse(const EnumDef &enum_def,
+ const EnumVal &enum_val) const {
+ if (opts_.scoped_enums) {
+ return Name(enum_def) + "::" + Name(enum_val);
+ } else if (opts_.prefixed_enums) {
+ return Name(enum_def) + "_" + Name(enum_val);
+ } else {
+ return Name(enum_val);
+ }
+ }
+
+ std::string StripUnionType(const std::string &name) {
+ return name.substr(0, name.size() - strlen(UnionTypeFieldSuffix()));
+ }
+
+ std::string GetUnionElement(const EnumVal &ev, bool native_type,
+ const IDLOptions &opts) {
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ auto name = ev.union_type.struct_def->name;
+ if (native_type) {
+ name = NativeName(name, ev.union_type.struct_def, opts);
+ }
+ return WrapInNameSpace(ev.union_type.struct_def->defined_namespace, name);
+ } else if (IsString(ev.union_type)) {
+ return native_type ? "std::string" : "flatbuffers::String";
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ return Name(ev);
+ }
+ }
+
+ std::string UnionVerifySignature(const EnumDef &enum_def) {
+ return "bool Verify" + Name(enum_def) +
+ "(flatbuffers::Verifier &verifier, const void *obj, " +
+ Name(enum_def) + " type)";
+ }
+
+ std::string UnionVectorVerifySignature(const EnumDef &enum_def) {
+ return "bool Verify" + Name(enum_def) + "Vector" +
+ "(flatbuffers::Verifier &verifier, " +
+ "const flatbuffers::Vector<flatbuffers::Offset<void>> *values, " +
+ "const flatbuffers::Vector<uint8_t> *types)";
+ }
+
+ std::string UnionUnPackSignature(const EnumDef &enum_def, bool inclass) {
+ return (inclass ? "static " : "") + std::string("void *") +
+ (inclass ? "" : Name(enum_def) + "Union::") +
+ "UnPack(const void *obj, " + Name(enum_def) +
+ " type, const flatbuffers::resolver_function_t *resolver)";
+ }
+
+ std::string UnionPackSignature(const EnumDef &enum_def, bool inclass) {
+ return "flatbuffers::Offset<void> " +
+ (inclass ? "" : Name(enum_def) + "Union::") +
+ "Pack(flatbuffers::FlatBufferBuilder &_fbb, " +
+ "const flatbuffers::rehasher_function_t *_rehasher" +
+ (inclass ? " = nullptr" : "") + ") const";
+ }
+
+ std::string TableCreateSignature(const StructDef &struct_def, bool predecl,
+ const IDLOptions &opts) {
+ return "flatbuffers::Offset<" + Name(struct_def) + "> Create" +
+ Name(struct_def) + "(flatbuffers::FlatBufferBuilder &_fbb, const " +
+ NativeName(Name(struct_def), &struct_def, opts) +
+ " *_o, const flatbuffers::rehasher_function_t *_rehasher" +
+ (predecl ? " = nullptr" : "") + ")";
+ }
+
+ std::string TablePackSignature(const StructDef &struct_def, bool inclass,
+ const IDLOptions &opts) {
+ return std::string(inclass ? "static " : "") + "flatbuffers::Offset<" +
+ Name(struct_def) + "> " + (inclass ? "" : Name(struct_def) + "::") +
+ "Pack(flatbuffers::FlatBufferBuilder &_fbb, " + "const " +
+ NativeName(Name(struct_def), &struct_def, opts) + "* _o, " +
+ "const flatbuffers::rehasher_function_t *_rehasher" +
+ (inclass ? " = nullptr" : "") + ")";
+ }
+
+ std::string TableUnPackSignature(const StructDef &struct_def, bool inclass,
+ const IDLOptions &opts) {
+ return NativeName(Name(struct_def), &struct_def, opts) + " *" +
+ (inclass ? "" : Name(struct_def) + "::") +
+ "UnPack(const flatbuffers::resolver_function_t *_resolver" +
+ (inclass ? " = nullptr" : "") + ") const";
+ }
+
+ std::string TableUnPackToSignature(const StructDef &struct_def, bool inclass,
+ const IDLOptions &opts) {
+ return "void " + (inclass ? "" : Name(struct_def) + "::") + "UnPackTo(" +
+ NativeName(Name(struct_def), &struct_def, opts) + " *" +
+ "_o, const flatbuffers::resolver_function_t *_resolver" +
+ (inclass ? " = nullptr" : "") + ") const";
+ }
+
+ void GenMiniReflectPre(const StructDef *struct_def) {
+ code_.SetValue("NAME", struct_def->name);
+ code_ += "inline const flatbuffers::TypeTable *{{NAME}}TypeTable();";
+ code_ += "";
+ }
+
+ void GenMiniReflect(const StructDef *struct_def, const EnumDef *enum_def) {
+ code_.SetValue("NAME", struct_def ? struct_def->name : enum_def->name);
+ code_.SetValue("SEQ_TYPE",
+ struct_def ? (struct_def->fixed ? "ST_STRUCT" : "ST_TABLE")
+ : (enum_def->is_union ? "ST_UNION" : "ST_ENUM"));
+ auto num_fields =
+ struct_def ? struct_def->fields.vec.size() : enum_def->size();
+ code_.SetValue("NUM_FIELDS", NumToString(num_fields));
+ std::vector<std::string> names;
+ std::vector<Type> types;
+
+ if (struct_def) {
+ for (auto it = struct_def->fields.vec.begin();
+ it != struct_def->fields.vec.end(); ++it) {
+ const auto &field = **it;
+ names.push_back(Name(field));
+ types.push_back(field.value.type);
+ }
+ } else {
+ for (auto it = enum_def->Vals().begin(); it != enum_def->Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ names.push_back(Name(ev));
+ types.push_back(enum_def->is_union ? ev.union_type
+ : Type(enum_def->underlying_type));
+ }
+ }
+ std::string ts;
+ std::vector<std::string> type_refs;
+ std::vector<uint16_t> array_sizes;
+ for (auto it = types.begin(); it != types.end(); ++it) {
+ auto &type = *it;
+ if (!ts.empty()) ts += ",\n ";
+ auto is_vector = IsVector(type);
+ auto is_array = IsArray(type);
+ auto bt = is_vector || is_array ? type.element : type.base_type;
+ auto et = IsScalar(bt) || bt == BASE_TYPE_STRING
+ ? bt - BASE_TYPE_UTYPE + ET_UTYPE
+ : ET_SEQUENCE;
+ int ref_idx = -1;
+ std::string ref_name =
+ type.struct_def
+ ? WrapInNameSpace(*type.struct_def)
+ : type.enum_def ? WrapInNameSpace(*type.enum_def) : "";
+ if (!ref_name.empty()) {
+ auto rit = type_refs.begin();
+ for (; rit != type_refs.end(); ++rit) {
+ if (*rit == ref_name) {
+ ref_idx = static_cast<int>(rit - type_refs.begin());
+ break;
+ }
+ }
+ if (rit == type_refs.end()) {
+ ref_idx = static_cast<int>(type_refs.size());
+ type_refs.push_back(ref_name);
+ }
+ }
+ if (is_array) { array_sizes.push_back(type.fixed_length); }
+ ts += "{ flatbuffers::" + std::string(ElementaryTypeNames()[et]) + ", " +
+ NumToString(is_vector || is_array) + ", " + NumToString(ref_idx) +
+ " }";
+ }
+ std::string rs;
+ for (auto it = type_refs.begin(); it != type_refs.end(); ++it) {
+ if (!rs.empty()) rs += ",\n ";
+ rs += *it + "TypeTable";
+ }
+ std::string as;
+ for (auto it = array_sizes.begin(); it != array_sizes.end(); ++it) {
+ as += NumToString(*it);
+ as += ", ";
+ }
+ std::string ns;
+ for (auto it = names.begin(); it != names.end(); ++it) {
+ if (!ns.empty()) ns += ",\n ";
+ ns += "\"" + *it + "\"";
+ }
+ std::string vs;
+ const auto consecutive_enum_from_zero =
+ enum_def && enum_def->MinValue()->IsZero() &&
+ ((enum_def->size() - 1) == enum_def->Distance());
+ if (enum_def && !consecutive_enum_from_zero) {
+ for (auto it = enum_def->Vals().begin(); it != enum_def->Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (!vs.empty()) vs += ", ";
+ vs += NumToStringCpp(enum_def->ToString(ev),
+ enum_def->underlying_type.base_type);
+ }
+ } else if (struct_def && struct_def->fixed) {
+ for (auto it = struct_def->fields.vec.begin();
+ it != struct_def->fields.vec.end(); ++it) {
+ const auto &field = **it;
+ vs += NumToString(field.value.offset);
+ vs += ", ";
+ }
+ vs += NumToString(struct_def->bytesize);
+ }
+ code_.SetValue("TYPES", ts);
+ code_.SetValue("REFS", rs);
+ code_.SetValue("ARRAYSIZES", as);
+ code_.SetValue("NAMES", ns);
+ code_.SetValue("VALUES", vs);
+ code_ += "inline const flatbuffers::TypeTable *{{NAME}}TypeTable() {";
+ if (num_fields) {
+ code_ += " static const flatbuffers::TypeCode type_codes[] = {";
+ code_ += " {{TYPES}}";
+ code_ += " };";
+ }
+ if (!type_refs.empty()) {
+ code_ += " static const flatbuffers::TypeFunction type_refs[] = {";
+ code_ += " {{REFS}}";
+ code_ += " };";
+ }
+ if (!as.empty()) {
+ code_ += " static const int16_t array_sizes[] = { {{ARRAYSIZES}} };";
+ }
+ if (!vs.empty()) {
+ // Problem with uint64_t values greater than 9223372036854775807ULL.
+ code_ += " static const int64_t values[] = { {{VALUES}} };";
+ }
+ auto has_names =
+ num_fields && opts_.mini_reflect == IDLOptions::kTypesAndNames;
+ if (has_names) {
+ code_ += " static const char * const names[] = {";
+ code_ += " {{NAMES}}";
+ code_ += " };";
+ }
+ code_ += " static const flatbuffers::TypeTable tt = {";
+ code_ += std::string(" flatbuffers::{{SEQ_TYPE}}, {{NUM_FIELDS}}, ") +
+ (num_fields ? "type_codes, " : "nullptr, ") +
+ (!type_refs.empty() ? "type_refs, " : "nullptr, ") +
+ (!as.empty() ? "array_sizes, " : "nullptr, ") +
+ (!vs.empty() ? "values, " : "nullptr, ") +
+ (has_names ? "names" : "nullptr");
+ code_ += " };";
+ code_ += " return &tt;";
+ code_ += "}";
+ code_ += "";
+ }
+
+ // Generate an enum declaration,
+ // an enum string lookup table,
+ // and an enum array of values
+
+ void GenEnum(const EnumDef &enum_def) {
+ code_.SetValue("ENUM_NAME", Name(enum_def));
+ code_.SetValue("BASE_TYPE", GenTypeBasic(enum_def.underlying_type, false));
+
+ GenComment(enum_def.doc_comment);
+ code_ +=
+ (opts_.scoped_enums ? "enum class " : "enum ") + Name(enum_def) + "\\";
+ if (opts_.g_only_fixed_enums) { code_ += " : {{BASE_TYPE}}\\"; }
+ code_ += " {";
+
+ code_.SetValue("SEP", ",");
+ auto add_sep = false;
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const auto &ev = **it;
+ if (add_sep) code_ += "{{SEP}}";
+ GenComment(ev.doc_comment, " ");
+ code_.SetValue("KEY", GenEnumValDecl(enum_def, Name(ev)));
+ code_.SetValue("VALUE",
+ NumToStringCpp(enum_def.ToString(ev),
+ enum_def.underlying_type.base_type));
+ code_ += " {{KEY}} = {{VALUE}}\\";
+ add_sep = true;
+ }
+ const EnumVal *minv = enum_def.MinValue();
+ const EnumVal *maxv = enum_def.MaxValue();
+
+ if (opts_.scoped_enums || opts_.prefixed_enums) {
+ FLATBUFFERS_ASSERT(minv && maxv);
+
+ code_.SetValue("SEP", ",\n");
+ if (enum_def.attributes.Lookup("bit_flags")) {
+ code_.SetValue("KEY", GenEnumValDecl(enum_def, "NONE"));
+ code_.SetValue("VALUE", "0");
+ code_ += "{{SEP}} {{KEY}} = {{VALUE}}\\";
+
+ code_.SetValue("KEY", GenEnumValDecl(enum_def, "ANY"));
+ code_.SetValue("VALUE",
+ NumToStringCpp(enum_def.AllFlags(),
+ enum_def.underlying_type.base_type));
+ code_ += "{{SEP}} {{KEY}} = {{VALUE}}\\";
+ } else { // MIN & MAX are useless for bit_flags
+ code_.SetValue("KEY", GenEnumValDecl(enum_def, "MIN"));
+ code_.SetValue("VALUE", GenEnumValDecl(enum_def, Name(*minv)));
+ code_ += "{{SEP}} {{KEY}} = {{VALUE}}\\";
+
+ code_.SetValue("KEY", GenEnumValDecl(enum_def, "MAX"));
+ code_.SetValue("VALUE", GenEnumValDecl(enum_def, Name(*maxv)));
+ code_ += "{{SEP}} {{KEY}} = {{VALUE}}\\";
+ }
+ }
+ code_ += "";
+ code_ += "};";
+
+ if (opts_.scoped_enums && enum_def.attributes.Lookup("bit_flags")) {
+ code_ +=
+ "FLATBUFFERS_DEFINE_BITMASK_OPERATORS({{ENUM_NAME}}, {{BASE_TYPE}})";
+ }
+ code_ += "";
+
+ // Generate an array of all enumeration values
+ auto num_fields = NumToString(enum_def.size());
+ code_ += "inline const {{ENUM_NAME}} (&EnumValues{{ENUM_NAME}}())[" +
+ num_fields + "] {";
+ code_ += " static const {{ENUM_NAME}} values[] = {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const auto &ev = **it;
+ auto value = GetEnumValUse(enum_def, ev);
+ auto suffix = *it != enum_def.Vals().back() ? "," : "";
+ code_ += " " + value + suffix;
+ }
+ code_ += " };";
+ code_ += " return values;";
+ code_ += "}";
+ code_ += "";
+
+ // Generate a generate string table for enum values.
+ // Problem is, if values are very sparse that could generate really big
+ // tables. Ideally in that case we generate a map lookup instead, but for
+ // the moment we simply don't output a table at all.
+ auto range = enum_def.Distance();
+ // Average distance between values above which we consider a table
+ // "too sparse". Change at will.
+ static const uint64_t kMaxSparseness = 5;
+ if (range / static_cast<uint64_t>(enum_def.size()) < kMaxSparseness) {
+ code_ += "inline const char * const *EnumNames{{ENUM_NAME}}() {";
+ code_ += " static const char * const names[" +
+ NumToString(range + 1 + 1) + "] = {";
+
+ auto val = enum_def.Vals().front();
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ auto ev = *it;
+ for (auto k = enum_def.Distance(val, ev); k > 1; --k) {
+ code_ += " \"\",";
+ }
+ val = ev;
+ code_ += " \"" + Name(*ev) + "\",";
+ }
+ code_ += " nullptr";
+ code_ += " };";
+
+ code_ += " return names;";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline const char *EnumName{{ENUM_NAME}}({{ENUM_NAME}} e) {";
+
+ code_ += " if (flatbuffers::IsOutRange(e, " +
+ GetEnumValUse(enum_def, *enum_def.MinValue()) + ", " +
+ GetEnumValUse(enum_def, *enum_def.MaxValue()) +
+ ")) return \"\";";
+
+ code_ += " const size_t index = static_cast<size_t>(e)\\";
+ if (enum_def.MinValue()->IsNonZero()) {
+ auto vals = GetEnumValUse(enum_def, *enum_def.MinValue());
+ code_ += " - static_cast<size_t>(" + vals + ")\\";
+ }
+ code_ += ";";
+
+ code_ += " return EnumNames{{ENUM_NAME}}()[index];";
+ code_ += "}";
+ code_ += "";
+ } else {
+ code_ += "inline const char *EnumName{{ENUM_NAME}}({{ENUM_NAME}} e) {";
+
+ code_ += " switch (e) {";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ code_ += " case " + GetEnumValUse(enum_def, ev) + ": return \"" +
+ Name(ev) + "\";";
+ }
+
+ code_ += " default: return \"\";";
+ code_ += " }";
+
+ code_ += "}";
+ code_ += "";
+ }
+
+ // Generate type traits for unions to map from a type to union enum value.
+ if (enum_def.is_union && !enum_def.uses_multiple_type_instances) {
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+
+ if (it == enum_def.Vals().begin()) {
+ code_ += "template<typename T> struct {{ENUM_NAME}}Traits {";
+ } else {
+ auto name = GetUnionElement(ev, false, opts_);
+ code_ += "template<> struct {{ENUM_NAME}}Traits<" + name + "> {";
+ }
+
+ auto value = GetEnumValUse(enum_def, ev);
+ code_ += " static const {{ENUM_NAME}} enum_value = " + value + ";";
+ code_ += "};";
+ code_ += "";
+ }
+ }
+
+ if (opts_.generate_object_based_api && enum_def.is_union) {
+ // Generate a union type
+ code_.SetValue("NAME", Name(enum_def));
+ FLATBUFFERS_ASSERT(enum_def.Lookup("NONE"));
+ code_.SetValue("NONE", GetEnumValUse(enum_def, *enum_def.Lookup("NONE")));
+
+ code_ += "struct {{NAME}}Union {";
+ code_ += " {{NAME}} type;";
+ code_ += " void *value;";
+ code_ += "";
+ code_ += " {{NAME}}Union() : type({{NONE}}), value(nullptr) {}";
+ code_ += " {{NAME}}Union({{NAME}}Union&& u) FLATBUFFERS_NOEXCEPT :";
+ code_ += " type({{NONE}}), value(nullptr)";
+ code_ += " { std::swap(type, u.type); std::swap(value, u.value); }";
+ code_ += " {{NAME}}Union(const {{NAME}}Union &);";
+ code_ += " {{NAME}}Union &operator=(const {{NAME}}Union &u)";
+ code_ +=
+ " { {{NAME}}Union t(u); std::swap(type, t.type); std::swap(value, "
+ "t.value); return *this; }";
+ code_ +=
+ " {{NAME}}Union &operator=({{NAME}}Union &&u) FLATBUFFERS_NOEXCEPT";
+ code_ +=
+ " { std::swap(type, u.type); std::swap(value, u.value); return "
+ "*this; }";
+ code_ += " ~{{NAME}}Union() { Reset(); }";
+ code_ += "";
+ code_ += " void Reset();";
+ code_ += "";
+ if (!enum_def.uses_multiple_type_instances) {
+ code_ += "#ifndef FLATBUFFERS_CPP98_STL";
+ code_ += " template <typename T>";
+ code_ += " void Set(T&& val) {";
+ code_ += " using RT = typename std::remove_reference<T>::type;";
+ code_ += " Reset();";
+ code_ +=
+ " type = {{NAME}}Traits<typename RT::TableType>::enum_value;";
+ code_ += " if (type != {{NONE}}) {";
+ code_ += " value = new RT(std::forward<T>(val));";
+ code_ += " }";
+ code_ += " }";
+ code_ += "#endif // FLATBUFFERS_CPP98_STL";
+ code_ += "";
+ }
+ code_ += " " + UnionUnPackSignature(enum_def, true) + ";";
+ code_ += " " + UnionPackSignature(enum_def, true) + ";";
+ code_ += "";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ const auto native_type = GetUnionElement(ev, true, opts_);
+ code_.SetValue("NATIVE_TYPE", native_type);
+ code_.SetValue("NATIVE_NAME", Name(ev));
+ code_.SetValue("NATIVE_ID", GetEnumValUse(enum_def, ev));
+
+ code_ += " {{NATIVE_TYPE}} *As{{NATIVE_NAME}}() {";
+ code_ += " return type == {{NATIVE_ID}} ?";
+ code_ += " reinterpret_cast<{{NATIVE_TYPE}} *>(value) : nullptr;";
+ code_ += " }";
+
+ code_ += " const {{NATIVE_TYPE}} *As{{NATIVE_NAME}}() const {";
+ code_ += " return type == {{NATIVE_ID}} ?";
+ code_ +=
+ " reinterpret_cast<const {{NATIVE_TYPE}} *>(value) : nullptr;";
+ code_ += " }";
+ }
+ code_ += "};";
+ code_ += "";
+
+ if (opts_.gen_compare) {
+ code_ += "";
+ code_ +=
+ "inline bool operator==(const {{NAME}}Union &lhs, const "
+ "{{NAME}}Union &rhs) {";
+ code_ += " if (lhs.type != rhs.type) return false;";
+ code_ += " switch (lhs.type) {";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ code_.SetValue("NATIVE_ID", GetEnumValUse(enum_def, ev));
+ if (ev.IsNonZero()) {
+ const auto native_type = GetUnionElement(ev, true, opts_);
+ code_.SetValue("NATIVE_TYPE", native_type);
+ code_ += " case {{NATIVE_ID}}: {";
+ code_ +=
+ " return *(reinterpret_cast<const {{NATIVE_TYPE}} "
+ "*>(lhs.value)) ==";
+ code_ +=
+ " *(reinterpret_cast<const {{NATIVE_TYPE}} "
+ "*>(rhs.value));";
+ code_ += " }";
+ } else {
+ code_ += " case {{NATIVE_ID}}: {";
+ code_ += " return true;"; // "NONE" enum value.
+ code_ += " }";
+ }
+ }
+ code_ += " default: {";
+ code_ += " return false;";
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+
+ code_ += "";
+ code_ +=
+ "inline bool operator!=(const {{NAME}}Union &lhs, const "
+ "{{NAME}}Union &rhs) {";
+ code_ += " return !(lhs == rhs);";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ if (enum_def.is_union) {
+ code_ += UnionVerifySignature(enum_def) + ";";
+ code_ += UnionVectorVerifySignature(enum_def) + ";";
+ code_ += "";
+ }
+ }
+
+ void GenUnionPost(const EnumDef &enum_def) {
+ // Generate a verifier function for this union that can be called by the
+ // table verifier functions. It uses a switch case to select a specific
+ // verifier function to call, this should be safe even if the union type
+ // has been corrupted, since the verifiers will simply fail when called
+ // on the wrong type.
+ code_.SetValue("ENUM_NAME", Name(enum_def));
+
+ code_ += "inline " + UnionVerifySignature(enum_def) + " {";
+ code_ += " switch (type) {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const auto &ev = **it;
+ code_.SetValue("LABEL", GetEnumValUse(enum_def, ev));
+
+ if (ev.IsNonZero()) {
+ code_.SetValue("TYPE", GetUnionElement(ev, false, opts_));
+ code_ += " case {{LABEL}}: {";
+ auto getptr =
+ " auto ptr = reinterpret_cast<const {{TYPE}} *>(obj);";
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ if (ev.union_type.struct_def->fixed) {
+ code_ +=
+ " return verifier.Verify<{{TYPE}}>(static_cast<const "
+ "uint8_t *>(obj), 0);";
+ } else {
+ code_ += getptr;
+ code_ += " return verifier.VerifyTable(ptr);";
+ }
+ } else if (IsString(ev.union_type)) {
+ code_ += getptr;
+ code_ += " return verifier.VerifyString(ptr);";
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ code_ += " }";
+ } else {
+ code_ += " case {{LABEL}}: {";
+ code_ += " return true;"; // "NONE" enum value.
+ code_ += " }";
+ }
+ }
+ code_ += " default: return true;"; // unknown values are OK.
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline " + UnionVectorVerifySignature(enum_def) + " {";
+ code_ += " if (!values || !types) return !values && !types;";
+ code_ += " if (values->size() != types->size()) return false;";
+ code_ += " for (flatbuffers::uoffset_t i = 0; i < values->size(); ++i) {";
+ code_ += " if (!Verify" + Name(enum_def) + "(";
+ code_ += " verifier, values->Get(i), types->GetEnum<" +
+ Name(enum_def) + ">(i))) {";
+ code_ += " return false;";
+ code_ += " }";
+ code_ += " }";
+ code_ += " return true;";
+ code_ += "}";
+ code_ += "";
+
+ if (opts_.generate_object_based_api) {
+ // Generate union Unpack() and Pack() functions.
+ code_ += "inline " + UnionUnPackSignature(enum_def, false) + " {";
+ code_ += " switch (type) {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ code_.SetValue("LABEL", GetEnumValUse(enum_def, ev));
+ code_.SetValue("TYPE", GetUnionElement(ev, false, opts_));
+ code_ += " case {{LABEL}}: {";
+ code_ += " auto ptr = reinterpret_cast<const {{TYPE}} *>(obj);";
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ if (ev.union_type.struct_def->fixed) {
+ code_ += " return new " +
+ WrapInNameSpace(*ev.union_type.struct_def) + "(*ptr);";
+ } else {
+ code_ += " return ptr->UnPack(resolver);";
+ }
+ } else if (IsString(ev.union_type)) {
+ code_ += " return new std::string(ptr->c_str(), ptr->size());";
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ code_ += " }";
+ }
+ code_ += " default: return nullptr;";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "inline " + UnionPackSignature(enum_def, false) + " {";
+ code_ += " switch (type) {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ code_.SetValue("LABEL", GetEnumValUse(enum_def, ev));
+ code_.SetValue("TYPE", GetUnionElement(ev, true, opts_));
+ code_ += " case {{LABEL}}: {";
+ code_ += " auto ptr = reinterpret_cast<const {{TYPE}} *>(value);";
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ if (ev.union_type.struct_def->fixed) {
+ code_ += " return _fbb.CreateStruct(*ptr).Union();";
+ } else {
+ code_.SetValue("NAME", ev.union_type.struct_def->name);
+ code_ +=
+ " return Create{{NAME}}(_fbb, ptr, _rehasher).Union();";
+ }
+ } else if (IsString(ev.union_type)) {
+ code_ += " return _fbb.CreateString(*ptr).Union();";
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ code_ += " }";
+ }
+ code_ += " default: return 0;";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ // Union copy constructor
+ code_ +=
+ "inline {{ENUM_NAME}}Union::{{ENUM_NAME}}Union(const "
+ "{{ENUM_NAME}}Union &u) : type(u.type), value(nullptr) {";
+ code_ += " switch (type) {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+ code_.SetValue("LABEL", GetEnumValUse(enum_def, ev));
+ code_.SetValue("TYPE", GetUnionElement(ev, true, opts_));
+ code_ += " case {{LABEL}}: {";
+ bool copyable = true;
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT &&
+ !ev.union_type.struct_def->fixed) {
+ // Don't generate code to copy if table is not copyable.
+ // TODO(wvo): make tables copyable instead.
+ for (auto fit = ev.union_type.struct_def->fields.vec.begin();
+ fit != ev.union_type.struct_def->fields.vec.end(); ++fit) {
+ const auto &field = **fit;
+ if (!field.deprecated && field.value.type.struct_def &&
+ !field.native_inline) {
+ copyable = false;
+ break;
+ }
+ }
+ }
+ if (copyable) {
+ code_ +=
+ " value = new {{TYPE}}(*reinterpret_cast<{{TYPE}} *>"
+ "(u.value));";
+ } else {
+ code_ +=
+ " FLATBUFFERS_ASSERT(false); // {{TYPE}} not copyable.";
+ }
+ code_ += " break;";
+ code_ += " }";
+ }
+ code_ += " default:";
+ code_ += " break;";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ // Union Reset() function.
+ FLATBUFFERS_ASSERT(enum_def.Lookup("NONE"));
+ code_.SetValue("NONE", GetEnumValUse(enum_def, *enum_def.Lookup("NONE")));
+
+ code_ += "inline void {{ENUM_NAME}}Union::Reset() {";
+ code_ += " switch (type) {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+ code_.SetValue("LABEL", GetEnumValUse(enum_def, ev));
+ code_.SetValue("TYPE", GetUnionElement(ev, true, opts_));
+ code_ += " case {{LABEL}}: {";
+ code_ += " auto ptr = reinterpret_cast<{{TYPE}} *>(value);";
+ code_ += " delete ptr;";
+ code_ += " break;";
+ code_ += " }";
+ }
+ code_ += " default: break;";
+ code_ += " }";
+ code_ += " value = nullptr;";
+ code_ += " type = {{NONE}};";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ // Generates a value with optionally a cast applied if the field has a
+ // different underlying type from its interface type (currently only the
+ // case for enums. "from" specify the direction, true meaning from the
+ // underlying type to the interface type.
+ std::string GenUnderlyingCast(const FieldDef &field, bool from,
+ const std::string &val) {
+ if (from && field.value.type.base_type == BASE_TYPE_BOOL) {
+ return val + " != 0";
+ } else if ((field.value.type.enum_def &&
+ IsScalar(field.value.type.base_type)) ||
+ field.value.type.base_type == BASE_TYPE_BOOL) {
+ return "static_cast<" + GenTypeBasic(field.value.type, from) + ">(" +
+ val + ")";
+ } else {
+ return val;
+ }
+ }
+
+ std::string GenFieldOffsetName(const FieldDef &field) {
+ std::string uname = Name(field);
+ std::transform(uname.begin(), uname.end(), uname.begin(), CharToUpper);
+ return "VT_" + uname;
+ }
+
+ void GenFullyQualifiedNameGetter(const StructDef &struct_def,
+ const std::string &name) {
+ if (!opts_.generate_name_strings) { return; }
+ auto fullname = struct_def.defined_namespace->GetFullyQualifiedName(name);
+ code_.SetValue("NAME", fullname);
+ code_.SetValue("CONSTEXPR", "FLATBUFFERS_CONSTEXPR");
+ code_ += " static {{CONSTEXPR}} const char *GetFullyQualifiedName() {";
+ code_ += " return \"{{NAME}}\";";
+ code_ += " }";
+ }
+
+ std::string GenDefaultConstant(const FieldDef &field) {
+ if (IsFloat(field.value.type.base_type))
+ return float_const_gen_.GenFloatConstant(field);
+ else
+ return NumToStringCpp(field.value.constant, field.value.type.base_type);
+ }
+
+ std::string GetDefaultScalarValue(const FieldDef &field, bool is_ctor) {
+ const auto &type = field.value.type;
+ if (field.IsScalarOptional()) {
+ return GenOptionalNull();
+ } else if (type.enum_def && IsScalar(type.base_type)) {
+ auto ev = type.enum_def->FindByValue(field.value.constant);
+ if (ev) {
+ return WrapInNameSpace(type.enum_def->defined_namespace,
+ GetEnumValUse(*type.enum_def, *ev));
+ } else {
+ return GenUnderlyingCast(
+ field, true, NumToStringCpp(field.value.constant, type.base_type));
+ }
+ } else if (type.base_type == BASE_TYPE_BOOL) {
+ return field.value.constant == "0" ? "false" : "true";
+ } else if (field.attributes.Lookup("cpp_type")) {
+ if (is_ctor) {
+ if (PtrType(&field) == "naked") {
+ return "nullptr";
+ } else {
+ return "";
+ }
+ } else {
+ return "0";
+ }
+ } else {
+ return GenDefaultConstant(field);
+ }
+ }
+
+ void GenParam(const FieldDef &field, bool direct, const char *prefix) {
+ code_.SetValue("PRE", prefix);
+ code_.SetValue("PARAM_NAME", Name(field));
+ if (direct && IsString(field.value.type)) {
+ code_.SetValue("PARAM_TYPE", "const char *");
+ code_.SetValue("PARAM_VALUE", "nullptr");
+ } else if (direct && IsVector(field.value.type)) {
+ const auto vtype = field.value.type.VectorType();
+ std::string type;
+ if (IsStruct(vtype)) {
+ type = WrapInNameSpace(*vtype.struct_def);
+ } else {
+ type = GenTypeWire(vtype, "", VectorElementUserFacing(vtype));
+ }
+ if (TypeHasKey(vtype)) {
+ code_.SetValue("PARAM_TYPE", "std::vector<" + type + "> *");
+ } else {
+ code_.SetValue("PARAM_TYPE", "const std::vector<" + type + "> *");
+ }
+ code_.SetValue("PARAM_VALUE", "nullptr");
+ } else {
+ const auto &type = field.value.type;
+ code_.SetValue("PARAM_VALUE", GetDefaultScalarValue(field, false));
+ if (field.IsScalarOptional())
+ code_.SetValue("PARAM_TYPE", GenOptionalDecl(type) + " ");
+ else
+ code_.SetValue("PARAM_TYPE", GenTypeWire(type, " ", true));
+ }
+ code_ += "{{PRE}}{{PARAM_TYPE}}{{PARAM_NAME}} = {{PARAM_VALUE}}\\";
+ }
+
+ // Generate a member, including a default value for scalars and raw pointers.
+ void GenMember(const FieldDef &field) {
+ if (!field.deprecated && // Deprecated fields won't be accessible.
+ field.value.type.base_type != BASE_TYPE_UTYPE &&
+ (field.value.type.base_type != BASE_TYPE_VECTOR ||
+ field.value.type.element != BASE_TYPE_UTYPE)) {
+ auto type = GenTypeNative(field.value.type, false, field);
+ auto cpp_type = field.attributes.Lookup("cpp_type");
+ auto full_type =
+ (cpp_type
+ ? (IsVector(field.value.type)
+ ? "std::vector<" +
+ GenTypeNativePtr(cpp_type->constant, &field,
+ false) +
+ "> "
+ : GenTypeNativePtr(cpp_type->constant, &field, false))
+ : type + " ");
+ // Generate default member initializers for >= C++11.
+ std::string field_di = "";
+ if (opts_.g_cpp_std >= cpp::CPP_STD_11) {
+ field_di = "{}";
+ auto native_default = field.attributes.Lookup("native_default");
+ // Scalar types get parsed defaults, raw pointers get nullptrs.
+ if (IsScalar(field.value.type.base_type)) {
+ field_di =
+ " = " + (native_default ? std::string(native_default->constant)
+ : GetDefaultScalarValue(field, true));
+ } else if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ if (IsStruct(field.value.type) && native_default) {
+ field_di = " = " + native_default->constant;
+ }
+ }
+ }
+ code_.SetValue("FIELD_TYPE", full_type);
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_DI", field_di);
+ code_ += " {{FIELD_TYPE}}{{FIELD_NAME}}{{FIELD_DI}};";
+ }
+ }
+
+ // Generate the default constructor for this struct. Properly initialize all
+ // scalar members with default values.
+ void GenDefaultConstructor(const StructDef &struct_def) {
+ code_.SetValue("NATIVE_NAME",
+ NativeName(Name(struct_def), &struct_def, opts_));
+ // In >= C++11, default member initializers are generated.
+ if (opts_.g_cpp_std >= cpp::CPP_STD_11) { return; }
+ std::string initializer_list;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated && // Deprecated fields won't be accessible.
+ field.value.type.base_type != BASE_TYPE_UTYPE) {
+ auto cpp_type = field.attributes.Lookup("cpp_type");
+ auto native_default = field.attributes.Lookup("native_default");
+ // Scalar types get parsed defaults, raw pointers get nullptrs.
+ if (IsScalar(field.value.type.base_type)) {
+ if (!initializer_list.empty()) { initializer_list += ",\n "; }
+ initializer_list += Name(field);
+ initializer_list +=
+ "(" +
+ (native_default ? std::string(native_default->constant)
+ : GetDefaultScalarValue(field, true)) +
+ ")";
+ } else if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ if (IsStruct(field.value.type)) {
+ if (native_default) {
+ if (!initializer_list.empty()) {
+ initializer_list += ",\n ";
+ }
+ initializer_list +=
+ Name(field) + "(" + native_default->constant + ")";
+ }
+ }
+ } else if (cpp_type && field.value.type.base_type != BASE_TYPE_VECTOR) {
+ if (!initializer_list.empty()) { initializer_list += ",\n "; }
+ initializer_list += Name(field) + "(0)";
+ }
+ }
+ }
+ if (!initializer_list.empty()) {
+ initializer_list = "\n : " + initializer_list;
+ }
+
+ code_.SetValue("INIT_LIST", initializer_list);
+
+ code_ += " {{NATIVE_NAME}}(){{INIT_LIST}} {";
+ code_ += " }";
+ }
+
+ void GenCompareOperator(const StructDef &struct_def,
+ std::string accessSuffix = "") {
+ std::string compare_op;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated && // Deprecated fields won't be accessible.
+ field.value.type.base_type != BASE_TYPE_UTYPE &&
+ (field.value.type.base_type != BASE_TYPE_VECTOR ||
+ field.value.type.element != BASE_TYPE_UTYPE)) {
+ if (!compare_op.empty()) { compare_op += " &&\n "; }
+ auto accessor = Name(field) + accessSuffix;
+ compare_op += "(lhs." + accessor + " == rhs." + accessor + ")";
+ }
+ }
+
+ std::string cmp_lhs;
+ std::string cmp_rhs;
+ if (compare_op.empty()) {
+ cmp_lhs = "";
+ cmp_rhs = "";
+ compare_op = " return true;";
+ } else {
+ cmp_lhs = "lhs";
+ cmp_rhs = "rhs";
+ compare_op = " return\n " + compare_op + ";";
+ }
+
+ code_.SetValue("CMP_OP", compare_op);
+ code_.SetValue("CMP_LHS", cmp_lhs);
+ code_.SetValue("CMP_RHS", cmp_rhs);
+ code_ += "";
+ code_ +=
+ "inline bool operator==(const {{NATIVE_NAME}} &{{CMP_LHS}}, const "
+ "{{NATIVE_NAME}} &{{CMP_RHS}}) {";
+ code_ += "{{CMP_OP}}";
+ code_ += "}";
+
+ code_ += "";
+ code_ +=
+ "inline bool operator!=(const {{NATIVE_NAME}} &lhs, const "
+ "{{NATIVE_NAME}} &rhs) {";
+ code_ += " return !(lhs == rhs);";
+ code_ += "}";
+ code_ += "";
+ }
+
+ void GenOperatorNewDelete(const StructDef &struct_def) {
+ if (auto native_custom_alloc =
+ struct_def.attributes.Lookup("native_custom_alloc")) {
+ code_ += " inline void *operator new (std::size_t count) {";
+ code_ += " return " + native_custom_alloc->constant +
+ "<{{NATIVE_NAME}}>().allocate(count / sizeof({{NATIVE_NAME}}));";
+ code_ += " }";
+ code_ += " inline void operator delete (void *ptr) {";
+ code_ += " return " + native_custom_alloc->constant +
+ "<{{NATIVE_NAME}}>().deallocate(static_cast<{{NATIVE_NAME}}*>("
+ "ptr),1);";
+ code_ += " }";
+ }
+ }
+
+ void GenNativeTable(const StructDef &struct_def) {
+ const auto native_name = NativeName(Name(struct_def), &struct_def, opts_);
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_.SetValue("NATIVE_NAME", native_name);
+
+ // Generate a C++ object that can hold an unpacked version of this table.
+ code_ += "struct {{NATIVE_NAME}} : public flatbuffers::NativeTable {";
+ code_ += " typedef {{STRUCT_NAME}} TableType;";
+ GenFullyQualifiedNameGetter(struct_def, native_name);
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ GenMember(**it);
+ }
+ GenOperatorNewDelete(struct_def);
+ GenDefaultConstructor(struct_def);
+ code_ += "};";
+ if (opts_.gen_compare) GenCompareOperator(struct_def);
+ code_ += "";
+ }
+
+ // Generate the code to call the appropriate Verify function(s) for a field.
+ void GenVerifyCall(const FieldDef &field, const char *prefix) {
+ code_.SetValue("PRE", prefix);
+ code_.SetValue("NAME", Name(field));
+ code_.SetValue("REQUIRED", field.IsRequired() ? "Required" : "");
+ code_.SetValue("SIZE", GenTypeSize(field.value.type));
+ code_.SetValue("OFFSET", GenFieldOffsetName(field));
+ if (IsScalar(field.value.type.base_type) || IsStruct(field.value.type)) {
+ code_ +=
+ "{{PRE}}VerifyField{{REQUIRED}}<{{SIZE}}>(verifier, {{OFFSET}})\\";
+ } else {
+ code_ += "{{PRE}}VerifyOffset{{REQUIRED}}(verifier, {{OFFSET}})\\";
+ }
+
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_UNION: {
+ code_.SetValue("ENUM_NAME", field.value.type.enum_def->name);
+ code_.SetValue("SUFFIX", UnionTypeFieldSuffix());
+ code_ +=
+ "{{PRE}}Verify{{ENUM_NAME}}(verifier, {{NAME}}(), "
+ "{{NAME}}{{SUFFIX}}())\\";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (!field.value.type.struct_def->fixed) {
+ code_ += "{{PRE}}verifier.VerifyTable({{NAME}}())\\";
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ code_ += "{{PRE}}verifier.VerifyString({{NAME}}())\\";
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ code_ += "{{PRE}}verifier.VerifyVector({{NAME}}())\\";
+
+ switch (field.value.type.element) {
+ case BASE_TYPE_STRING: {
+ code_ += "{{PRE}}verifier.VerifyVectorOfStrings({{NAME}}())\\";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (!field.value.type.struct_def->fixed) {
+ code_ += "{{PRE}}verifier.VerifyVectorOfTables({{NAME}}())\\";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ code_.SetValue("ENUM_NAME", field.value.type.enum_def->name);
+ code_ +=
+ "{{PRE}}Verify{{ENUM_NAME}}Vector(verifier, {{NAME}}(), "
+ "{{NAME}}_type())\\";
+ break;
+ }
+ default: break;
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ // Generate CompareWithValue method for a key field.
+ void GenKeyFieldMethods(const FieldDef &field) {
+ FLATBUFFERS_ASSERT(field.key);
+ const bool is_string = (IsString(field.value.type));
+
+ code_ += " bool KeyCompareLessThan(const {{STRUCT_NAME}} *o) const {";
+ if (is_string) {
+ // use operator< of flatbuffers::String
+ code_ += " return *{{FIELD_NAME}}() < *o->{{FIELD_NAME}}();";
+ } else {
+ code_ += " return {{FIELD_NAME}}() < o->{{FIELD_NAME}}();";
+ }
+ code_ += " }";
+
+ if (is_string) {
+ code_ += " int KeyCompareWithValue(const char *val) const {";
+ code_ += " return strcmp({{FIELD_NAME}}()->c_str(), val);";
+ code_ += " }";
+ } else {
+ FLATBUFFERS_ASSERT(IsScalar(field.value.type.base_type));
+ auto type = GenTypeBasic(field.value.type, false);
+ if (opts_.scoped_enums && field.value.type.enum_def &&
+ IsScalar(field.value.type.base_type)) {
+ type = GenTypeGet(field.value.type, " ", "const ", " *", true);
+ }
+ // Returns {field<val: -1, field==val: 0, field>val: +1}.
+ code_.SetValue("KEY_TYPE", type);
+ code_ += " int KeyCompareWithValue({{KEY_TYPE}} val) const {";
+ code_ +=
+ " return static_cast<int>({{FIELD_NAME}}() > val) - "
+ "static_cast<int>({{FIELD_NAME}}() < val);";
+ code_ += " }";
+ }
+ }
+
+ void GenTableUnionAsGetters(const FieldDef &field) {
+ const auto &type = field.value.type;
+ auto u = type.enum_def;
+
+ if (!type.enum_def->uses_multiple_type_instances)
+ code_ +=
+ " template<typename T> "
+ "const T *{{NULLABLE_EXT}}{{FIELD_NAME}}_as() const;";
+
+ for (auto u_it = u->Vals().begin(); u_it != u->Vals().end(); ++u_it) {
+ auto &ev = **u_it;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) { continue; }
+ auto full_struct_name = GetUnionElement(ev, false, opts_);
+
+ // @TODO: Mby make this decisions more universal? How?
+ code_.SetValue("U_GET_TYPE",
+ EscapeKeyword(field.name + UnionTypeFieldSuffix()));
+ code_.SetValue("U_ELEMENT_TYPE", WrapInNameSpace(u->defined_namespace,
+ GetEnumValUse(*u, ev)));
+ code_.SetValue("U_FIELD_TYPE", "const " + full_struct_name + " *");
+ code_.SetValue("U_FIELD_NAME", Name(field) + "_as_" + Name(ev));
+ code_.SetValue("U_NULLABLE", NullableExtension());
+
+ // `const Type *union_name_asType() const` accessor.
+ code_ += " {{U_FIELD_TYPE}}{{U_NULLABLE}}{{U_FIELD_NAME}}() const {";
+ code_ +=
+ " return {{U_GET_TYPE}}() == {{U_ELEMENT_TYPE}} ? "
+ "static_cast<{{U_FIELD_TYPE}}>({{FIELD_NAME}}()) "
+ ": nullptr;";
+ code_ += " }";
+ }
+ }
+
+ void GenTableFieldGetter(const FieldDef &field) {
+ const auto &type = field.value.type;
+ const auto offset_str = GenFieldOffsetName(field);
+
+ GenComment(field.doc_comment, " ");
+ // Call a different accessor for pointers, that indirects.
+ if (false == field.IsScalarOptional()) {
+ const bool is_scalar = IsScalar(type.base_type);
+ std::string accessor;
+ if (is_scalar)
+ accessor = "GetField<";
+ else if (IsStruct(type))
+ accessor = "GetStruct<";
+ else
+ accessor = "GetPointer<";
+ auto offset_type = GenTypeGet(type, "", "const ", " *", false);
+ auto call = accessor + offset_type + ">(" + offset_str;
+ // Default value as second arg for non-pointer types.
+ if (is_scalar) { call += ", " + GenDefaultConstant(field); }
+ call += ")";
+
+ std::string afterptr = " *" + NullableExtension();
+ code_.SetValue("FIELD_TYPE",
+ GenTypeGet(type, " ", "const ", afterptr.c_str(), true));
+ code_.SetValue("FIELD_VALUE", GenUnderlyingCast(field, true, call));
+ code_.SetValue("NULLABLE_EXT", NullableExtension());
+ code_ += " {{FIELD_TYPE}}{{FIELD_NAME}}() const {";
+ code_ += " return {{FIELD_VALUE}};";
+ code_ += " }";
+ } else {
+ auto wire_type = GenTypeBasic(type, false);
+ auto face_type = GenTypeBasic(type, true);
+ auto opt_value = "GetOptional<" + wire_type + ", " + face_type + ">(" +
+ offset_str + ")";
+ code_.SetValue("FIELD_TYPE", GenOptionalDecl(type));
+ code_ += " {{FIELD_TYPE}} {{FIELD_NAME}}() const {";
+ code_ += " return " + opt_value + ";";
+ code_ += " }";
+ }
+
+ if (type.base_type == BASE_TYPE_UNION) { GenTableUnionAsGetters(field); }
+ }
+
+ void GenTableFieldType(const FieldDef &field) {
+ const auto &type = field.value.type;
+ const auto offset_str = GenFieldOffsetName(field);
+ if (!field.IsScalarOptional()) {
+ std::string afterptr = " *" + NullableExtension();
+ code_.SetValue("FIELD_TYPE",
+ GenTypeGet(type, "", "const ", afterptr.c_str(), true));
+ code_ += " {{FIELD_TYPE}}\\";
+ } else {
+ code_.SetValue("FIELD_TYPE", GenOptionalDecl(type));
+ code_ += " {{FIELD_TYPE}}\\";
+ }
+ }
+
+ void GenStructFieldType(const FieldDef &field) {
+ const auto is_array = IsArray(field.value.type);
+ std::string field_type =
+ GenTypeGet(field.value.type, "", is_array ? "" : "const ",
+ is_array ? "" : " &", true);
+ code_.SetValue("FIELD_TYPE", field_type);
+ code_ += " {{FIELD_TYPE}}\\";
+ }
+
+ void GenFieldTypeHelper(const StructDef &struct_def) {
+ if (struct_def.fields.vec.empty()) { return; }
+ code_ += " template<size_t Index>";
+ code_ += " using FieldType = \\";
+ code_ += "decltype(std::declval<type>().get_field<Index>());";
+ }
+
+ void GenIndexBasedFieldGetter(const StructDef &struct_def) {
+ if (struct_def.fields.vec.empty()) { return; }
+ code_ += " template<size_t Index>";
+ code_ += " auto get_field() const {";
+
+ size_t index = 0;
+ bool need_else = false;
+ // Generate one index-based getter for each field.
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_INDEX",
+ std::to_string(static_cast<long long>(index++)));
+ if (need_else) {
+ code_ += " else \\";
+ } else {
+ code_ += " \\";
+ }
+ need_else = true;
+ code_ += "if constexpr (Index == {{FIELD_INDEX}}) \\";
+ code_ += "return {{FIELD_NAME}}();";
+ }
+ code_ += " else static_assert(Index != Index, \"Invalid Field Index\");";
+ code_ += " }";
+ }
+
+ // Sample for Vec3:
+ //
+ // static constexpr std::array<const char *, 3> field_names = {
+ // "x",
+ // "y",
+ // "z"
+ // };
+ //
+ void GenFieldNames(const StructDef &struct_def) {
+ auto non_deprecated_field_count = std::count_if(
+ struct_def.fields.vec.begin(), struct_def.fields.vec.end(),
+ [](const FieldDef *field) { return !field->deprecated; });
+ code_ += " static constexpr std::array<\\";
+ code_.SetValue(
+ "FIELD_COUNT",
+ std::to_string(static_cast<long long>(non_deprecated_field_count)));
+ code_ += "const char *, {{FIELD_COUNT}}> field_names = {\\";
+ if (struct_def.fields.vec.empty()) {
+ code_ += "};";
+ return;
+ }
+ code_ += "";
+ // Generate the field_names elements.
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_ += " \"{{FIELD_NAME}}\"\\";
+ if (it + 1 != struct_def.fields.vec.end()) { code_ += ","; }
+ }
+ code_ += "\n };";
+ }
+
+ void GenFieldsNumber(const StructDef &struct_def) {
+ auto non_deprecated_field_count = std::count_if(
+ struct_def.fields.vec.begin(), struct_def.fields.vec.end(),
+ [](const FieldDef *field) { return !field->deprecated; });
+ code_.SetValue(
+ "FIELD_COUNT",
+ std::to_string(static_cast<long long>(non_deprecated_field_count)));
+ code_ += " static constexpr size_t fields_number = {{FIELD_COUNT}};";
+ }
+
+ void GenTraitsStruct(const StructDef &struct_def) {
+ code_.SetValue(
+ "FULLY_QUALIFIED_NAME",
+ struct_def.defined_namespace->GetFullyQualifiedName(Name(struct_def)));
+ code_ += "struct {{STRUCT_NAME}}::Traits {";
+ code_ += " using type = {{STRUCT_NAME}};";
+ if (!struct_def.fixed) {
+ // We have a table and not a struct.
+ code_ += " static auto constexpr Create = Create{{STRUCT_NAME}};";
+ }
+ if (opts_.cpp_static_reflection) {
+ code_ += " static constexpr auto name = \"{{STRUCT_NAME}}\";";
+ code_ +=
+ " static constexpr auto fully_qualified_name = "
+ "\"{{FULLY_QUALIFIED_NAME}}\";";
+ GenFieldNames(struct_def);
+ GenFieldTypeHelper(struct_def);
+ GenFieldsNumber(struct_def);
+ }
+ code_ += "};";
+ code_ += "";
+ }
+
+ void GenTableFieldSetter(const FieldDef &field) {
+ const auto &type = field.value.type;
+ const bool is_scalar = IsScalar(type.base_type);
+ if (is_scalar && IsUnion(type))
+ return; // changing of a union's type is forbidden
+
+ auto offset_str = GenFieldOffsetName(field);
+ if (is_scalar) {
+ const auto wire_type = GenTypeWire(type, "", false);
+ code_.SetValue("SET_FN", "SetField<" + wire_type + ">");
+ code_.SetValue("OFFSET_NAME", offset_str);
+ code_.SetValue("FIELD_TYPE", GenTypeBasic(type, true));
+ code_.SetValue("FIELD_VALUE",
+ GenUnderlyingCast(field, false, "_" + Name(field)));
+
+ code_ +=
+ " bool mutate_{{FIELD_NAME}}({{FIELD_TYPE}} "
+ "_{{FIELD_NAME}}) {";
+ if (false == field.IsScalarOptional()) {
+ code_.SetValue("DEFAULT_VALUE", GenDefaultConstant(field));
+ code_ +=
+ " return {{SET_FN}}({{OFFSET_NAME}}, {{FIELD_VALUE}}, "
+ "{{DEFAULT_VALUE}});";
+ } else {
+ code_ += " return {{SET_FN}}({{OFFSET_NAME}}, {{FIELD_VALUE}});";
+ }
+ code_ += " }";
+ } else {
+ auto postptr = " *" + NullableExtension();
+ auto wire_type = GenTypeGet(type, " ", "", postptr.c_str(), true);
+ std::string accessor = IsStruct(type) ? "GetStruct<" : "GetPointer<";
+ auto underlying = accessor + wire_type + ">(" + offset_str + ")";
+ code_.SetValue("FIELD_TYPE", wire_type);
+ code_.SetValue("FIELD_VALUE", GenUnderlyingCast(field, true, underlying));
+
+ code_ += " {{FIELD_TYPE}}mutable_{{FIELD_NAME}}() {";
+ code_ += " return {{FIELD_VALUE}};";
+ code_ += " }";
+ }
+ }
+
+ // Generate an accessor struct, builder structs & function for a table.
+ void GenTable(const StructDef &struct_def) {
+ if (opts_.generate_object_based_api) { GenNativeTable(struct_def); }
+
+ // Generate an accessor struct, with methods of the form:
+ // type name() const { return GetField<type>(offset, defaultval); }
+ GenComment(struct_def.doc_comment);
+
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_ +=
+ "struct {{STRUCT_NAME}} FLATBUFFERS_FINAL_CLASS"
+ " : private flatbuffers::Table {";
+ if (opts_.generate_object_based_api) {
+ code_ += " typedef {{NATIVE_NAME}} NativeTableType;";
+ }
+ code_ += " typedef {{STRUCT_NAME}}Builder Builder;";
+ if (opts_.g_cpp_std >= cpp::CPP_STD_17) { code_ += " struct Traits;"; }
+ if (opts_.mini_reflect != IDLOptions::kNone) {
+ code_ +=
+ " static const flatbuffers::TypeTable *MiniReflectTypeTable() {";
+ code_ += " return {{STRUCT_NAME}}TypeTable();";
+ code_ += " }";
+ }
+
+ GenFullyQualifiedNameGetter(struct_def, Name(struct_def));
+
+ // Generate field id constants.
+ if (struct_def.fields.vec.size() > 0) {
+ // We need to add a trailing comma to all elements except the last one as
+ // older versions of gcc complain about this.
+ code_.SetValue("SEP", "");
+ code_ +=
+ " enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+
+ code_.SetValue("OFFSET_NAME", GenFieldOffsetName(field));
+ code_.SetValue("OFFSET_VALUE", NumToString(field.value.offset));
+ code_ += "{{SEP}} {{OFFSET_NAME}} = {{OFFSET_VALUE}}\\";
+ code_.SetValue("SEP", ",\n");
+ }
+ code_ += "";
+ code_ += " };";
+ }
+
+ // Generate the accessors.
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+
+ code_.SetValue("FIELD_NAME", Name(field));
+ GenTableFieldGetter(field);
+ if (opts_.mutable_buffer) { GenTableFieldSetter(field); }
+
+ auto nested = field.attributes.Lookup("nested_flatbuffer");
+ if (nested) {
+ std::string qualified_name = nested->constant;
+ auto nested_root = parser_.LookupStruct(nested->constant);
+ if (nested_root == nullptr) {
+ qualified_name = parser_.current_namespace_->GetFullyQualifiedName(
+ nested->constant);
+ nested_root = parser_.LookupStruct(qualified_name);
+ }
+ FLATBUFFERS_ASSERT(nested_root); // Guaranteed to exist by parser.
+ (void)nested_root;
+ code_.SetValue("CPP_NAME", TranslateNameSpace(qualified_name));
+
+ code_ += " const {{CPP_NAME}} *{{FIELD_NAME}}_nested_root() const {";
+ code_ +=
+ " return "
+ "flatbuffers::GetRoot<{{CPP_NAME}}>({{FIELD_NAME}}()->Data());";
+ code_ += " }";
+ }
+
+ if (field.flexbuffer) {
+ code_ +=
+ " flexbuffers::Reference {{FIELD_NAME}}_flexbuffer_root()"
+ " const {";
+ // Both Data() and size() are const-methods, therefore call order
+ // doesn't matter.
+ code_ +=
+ " return flexbuffers::GetRoot({{FIELD_NAME}}()->Data(), "
+ "{{FIELD_NAME}}()->size());";
+ code_ += " }";
+ }
+
+ // Generate a comparison function for this field if it is a key.
+ if (field.key) { GenKeyFieldMethods(field); }
+ }
+
+ if (opts_.cpp_static_reflection) { GenIndexBasedFieldGetter(struct_def); }
+
+ // Generate a verifier function that can check a buffer from an untrusted
+ // source will never cause reads outside the buffer.
+ code_ += " bool Verify(flatbuffers::Verifier &verifier) const {";
+ code_ += " return VerifyTableStart(verifier)\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) { continue; }
+ GenVerifyCall(field, " &&\n ");
+ }
+
+ code_ += " &&\n verifier.EndTable();";
+ code_ += " }";
+
+ if (opts_.generate_object_based_api) {
+ // Generate the UnPack() pre declaration.
+ code_ += " " + TableUnPackSignature(struct_def, true, opts_) + ";";
+ code_ += " " + TableUnPackToSignature(struct_def, true, opts_) + ";";
+ code_ += " " + TablePackSignature(struct_def, true, opts_) + ";";
+ }
+
+ code_ += "};"; // End of table.
+ code_ += "";
+
+ // Explicit specializations for union accessors
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated || field.value.type.base_type != BASE_TYPE_UNION) {
+ continue;
+ }
+
+ auto u = field.value.type.enum_def;
+ if (u->uses_multiple_type_instances) continue;
+
+ code_.SetValue("FIELD_NAME", Name(field));
+
+ for (auto u_it = u->Vals().begin(); u_it != u->Vals().end(); ++u_it) {
+ auto &ev = **u_it;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) { continue; }
+
+ auto full_struct_name = GetUnionElement(ev, false, opts_);
+
+ code_.SetValue(
+ "U_ELEMENT_TYPE",
+ WrapInNameSpace(u->defined_namespace, GetEnumValUse(*u, ev)));
+ code_.SetValue("U_FIELD_TYPE", "const " + full_struct_name + " *");
+ code_.SetValue("U_ELEMENT_NAME", full_struct_name);
+ code_.SetValue("U_FIELD_NAME", Name(field) + "_as_" + Name(ev));
+
+ // `template<> const T *union_name_as<T>() const` accessor.
+ code_ +=
+ "template<> "
+ "inline {{U_FIELD_TYPE}}{{STRUCT_NAME}}::{{FIELD_NAME}}_as"
+ "<{{U_ELEMENT_NAME}}>() const {";
+ code_ += " return {{U_FIELD_NAME}}();";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ GenBuilders(struct_def);
+
+ if (opts_.generate_object_based_api) {
+ // Generate a pre-declaration for a CreateX method that works with an
+ // unpacked C++ object.
+ code_ += TableCreateSignature(struct_def, true, opts_) + ";";
+ code_ += "";
+ }
+ }
+
+ // Generate code to force vector alignment. Return empty string for vector
+ // that doesn't need alignment code.
+ std::string GenVectorForceAlign(const FieldDef &field,
+ const std::string &field_size) {
+ FLATBUFFERS_ASSERT(IsVector(field.value.type));
+ // Get the value of the force_align attribute.
+ const auto *force_align = field.attributes.Lookup("force_align");
+ const int align = force_align ? atoi(force_align->constant.c_str()) : 1;
+ // Generate code to do force_align for the vector.
+ if (align > 1) {
+ const auto vtype = field.value.type.VectorType();
+ const auto type = IsStruct(vtype) ? WrapInNameSpace(*vtype.struct_def)
+ : GenTypeWire(vtype, "", false);
+ return "_fbb.ForceVectorAlignment(" + field_size + ", sizeof(" + type +
+ "), " + std::to_string(static_cast<long long>(align)) + ");";
+ }
+ return "";
+ }
+
+ void GenBuilders(const StructDef &struct_def) {
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+
+ // Generate a builder struct:
+ code_ += "struct {{STRUCT_NAME}}Builder {";
+ code_ += " typedef {{STRUCT_NAME}} Table;";
+ code_ += " flatbuffers::FlatBufferBuilder &fbb_;";
+ code_ += " flatbuffers::uoffset_t start_;";
+
+ bool has_string_or_vector_fields = false;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) continue;
+ const bool is_scalar = IsScalar(field.value.type.base_type);
+ const bool is_default_scalar = is_scalar && !field.IsScalarOptional();
+ const bool is_string = IsString(field.value.type);
+ const bool is_vector = IsVector(field.value.type);
+ if (is_string || is_vector) { has_string_or_vector_fields = true; }
+
+ std::string offset = GenFieldOffsetName(field);
+ std::string name = GenUnderlyingCast(field, false, Name(field));
+ std::string value = is_default_scalar ? GenDefaultConstant(field) : "";
+
+ // Generate accessor functions of the form:
+ // void add_name(type name) {
+ // fbb_.AddElement<type>(offset, name, default);
+ // }
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_TYPE", GenTypeWire(field.value.type, " ", true));
+ code_.SetValue("ADD_OFFSET", Name(struct_def) + "::" + offset);
+ code_.SetValue("ADD_NAME", name);
+ code_.SetValue("ADD_VALUE", value);
+ if (is_scalar) {
+ const auto type = GenTypeWire(field.value.type, "", false);
+ code_.SetValue("ADD_FN", "AddElement<" + type + ">");
+ } else if (IsStruct(field.value.type)) {
+ code_.SetValue("ADD_FN", "AddStruct");
+ } else {
+ code_.SetValue("ADD_FN", "AddOffset");
+ }
+
+ code_ += " void add_{{FIELD_NAME}}({{FIELD_TYPE}}{{FIELD_NAME}}) {";
+ code_ += " fbb_.{{ADD_FN}}(\\";
+ if (is_default_scalar) {
+ code_ += "{{ADD_OFFSET}}, {{ADD_NAME}}, {{ADD_VALUE}});";
+ } else {
+ code_ += "{{ADD_OFFSET}}, {{ADD_NAME}});";
+ }
+ code_ += " }";
+ }
+
+ // Builder constructor
+ code_ +=
+ " explicit {{STRUCT_NAME}}Builder(flatbuffers::FlatBufferBuilder "
+ "&_fbb)";
+ code_ += " : fbb_(_fbb) {";
+ code_ += " start_ = fbb_.StartTable();";
+ code_ += " }";
+
+ // Finish() function.
+ code_ += " flatbuffers::Offset<{{STRUCT_NAME}}> Finish() {";
+ code_ += " const auto end = fbb_.EndTable(start_);";
+ code_ += " auto o = flatbuffers::Offset<{{STRUCT_NAME}}>(end);";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("OFFSET_NAME", GenFieldOffsetName(field));
+ code_ += " fbb_.Required(o, {{STRUCT_NAME}}::{{OFFSET_NAME}});";
+ }
+ }
+ code_ += " return o;";
+ code_ += " }";
+ code_ += "};";
+ code_ += "";
+
+ // Generate a convenient CreateX function that uses the above builder
+ // to create a table in one go.
+ code_ +=
+ "inline flatbuffers::Offset<{{STRUCT_NAME}}> "
+ "Create{{STRUCT_NAME}}(";
+ code_ += " flatbuffers::FlatBufferBuilder &_fbb\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated) { GenParam(field, false, ",\n "); }
+ }
+ code_ += ") {";
+
+ code_ += " {{STRUCT_NAME}}Builder builder_(_fbb);";
+ for (size_t size = struct_def.sortbysize ? sizeof(largest_scalar_t) : 1;
+ size; size /= 2) {
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated && (!struct_def.sortbysize ||
+ size == SizeOf(field.value.type.base_type))) {
+ code_.SetValue("FIELD_NAME", Name(field));
+ if (field.IsScalarOptional()) {
+ code_ +=
+ " if({{FIELD_NAME}}) { "
+ "builder_.add_{{FIELD_NAME}}(*{{FIELD_NAME}}); }";
+ } else {
+ code_ += " builder_.add_{{FIELD_NAME}}({{FIELD_NAME}});";
+ }
+ }
+ }
+ }
+ code_ += " return builder_.Finish();";
+ code_ += "}";
+ code_ += "";
+
+ // Definition for type traits for this table type. This allows querying var-
+ // ious compile-time traits of the table.
+ if (opts_.g_cpp_std >= cpp::CPP_STD_17) { GenTraitsStruct(struct_def); }
+
+ // Generate a CreateXDirect function with vector types as parameters
+ if (opts_.cpp_direct_copy && has_string_or_vector_fields) {
+ code_ +=
+ "inline flatbuffers::Offset<{{STRUCT_NAME}}> "
+ "Create{{STRUCT_NAME}}Direct(";
+ code_ += " flatbuffers::FlatBufferBuilder &_fbb\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated) { GenParam(field, true, ",\n "); }
+ }
+ // Need to call "Create" with the struct namespace.
+ const auto qualified_create_name =
+ struct_def.defined_namespace->GetFullyQualifiedName("Create");
+ code_.SetValue("CREATE_NAME", TranslateNameSpace(qualified_create_name));
+ code_ += ") {";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated) {
+ code_.SetValue("FIELD_NAME", Name(field));
+ if (IsString(field.value.type)) {
+ if (!field.shared) {
+ code_.SetValue("CREATE_STRING", "CreateString");
+ } else {
+ code_.SetValue("CREATE_STRING", "CreateSharedString");
+ }
+ code_ +=
+ " auto {{FIELD_NAME}}__ = {{FIELD_NAME}} ? "
+ "_fbb.{{CREATE_STRING}}({{FIELD_NAME}}) : 0;";
+ } else if (IsVector(field.value.type)) {
+ const std::string force_align_code =
+ GenVectorForceAlign(field, Name(field) + "->size()");
+ if (!force_align_code.empty()) {
+ code_ += " if ({{FIELD_NAME}}) { " + force_align_code + " }";
+ }
+ code_ += " auto {{FIELD_NAME}}__ = {{FIELD_NAME}} ? \\";
+ const auto vtype = field.value.type.VectorType();
+ const auto has_key = TypeHasKey(vtype);
+ if (IsStruct(vtype)) {
+ const auto type = WrapInNameSpace(*vtype.struct_def);
+ code_ += (has_key ? "_fbb.CreateVectorOfSortedStructs<"
+ : "_fbb.CreateVectorOfStructs<") +
+ type + ">\\";
+ } else if (has_key) {
+ const auto type = WrapInNameSpace(*vtype.struct_def);
+ code_ += "_fbb.CreateVectorOfSortedTables<" + type + ">\\";
+ } else {
+ const auto type =
+ GenTypeWire(vtype, "", VectorElementUserFacing(vtype));
+ code_ += "_fbb.CreateVector<" + type + ">\\";
+ }
+ code_ +=
+ has_key ? "({{FIELD_NAME}}) : 0;" : "(*{{FIELD_NAME}}) : 0;";
+ }
+ }
+ }
+ code_ += " return {{CREATE_NAME}}{{STRUCT_NAME}}(";
+ code_ += " _fbb\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (!field.deprecated) {
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_ += ",\n {{FIELD_NAME}}\\";
+ if (IsString(field.value.type) || IsVector(field.value.type)) {
+ code_ += "__\\";
+ }
+ }
+ }
+ code_ += ");";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ std::string GenUnionUnpackVal(const FieldDef &afield,
+ const char *vec_elem_access,
+ const char *vec_type_access) {
+ auto type_name = WrapInNameSpace(*afield.value.type.enum_def);
+ return type_name + "Union::UnPack(" + "_e" + vec_elem_access + ", " +
+ EscapeKeyword(afield.name + UnionTypeFieldSuffix()) + "()" +
+ vec_type_access + ", _resolver)";
+ }
+
+ std::string GenUnpackVal(const Type &type, const std::string &val,
+ bool invector, const FieldDef &afield) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: {
+ if (FlexibleStringConstructor(&afield)) {
+ return NativeString(&afield) + "(" + val + "->c_str(), " + val +
+ "->size())";
+ } else {
+ return val + "->str()";
+ }
+ }
+ case BASE_TYPE_STRUCT: {
+ if (IsStruct(type)) {
+ const auto &struct_attrs = type.struct_def->attributes;
+ const auto native_type = struct_attrs.Lookup("native_type");
+ if (native_type) {
+ std::string unpack_call = "flatbuffers::UnPack";
+ const auto pack_name = struct_attrs.Lookup("native_type_pack_name");
+ if (pack_name) { unpack_call += pack_name->constant; }
+ unpack_call += "(*" + val + ")";
+ return unpack_call;
+ } else if (invector || afield.native_inline) {
+ return "*" + val;
+ } else {
+ const auto name = WrapInNameSpace(*type.struct_def);
+ const auto ptype = GenTypeNativePtr(name, &afield, true);
+ return ptype + "(new " + name + "(*" + val + "))";
+ }
+ } else {
+ const auto ptype = GenTypeNativePtr(
+ WrapNativeNameInNameSpace(*type.struct_def, opts_), &afield,
+ true);
+ return ptype + "(" + val + "->UnPack(_resolver))";
+ }
+ }
+ case BASE_TYPE_UNION: {
+ return GenUnionUnpackVal(
+ afield, invector ? "->Get(_i)" : "",
+ invector ? ("->GetEnum<" + type.enum_def->name + ">(_i)").c_str()
+ : "");
+ }
+ default: {
+ return val;
+ break;
+ }
+ }
+ }
+
+ std::string GenUnpackFieldStatement(const FieldDef &field,
+ const FieldDef *union_field) {
+ std::string code;
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_VECTOR: {
+ auto name = Name(field);
+ if (field.value.type.element == BASE_TYPE_UTYPE) {
+ name = StripUnionType(Name(field));
+ }
+ code += "{ _o->" + name + ".resize(_e->size()); ";
+ if (!field.value.type.enum_def && !IsBool(field.value.type.element) &&
+ IsOneByte(field.value.type.element)) {
+ // For vectors of bytes, std::copy is used to improve performance.
+ // This doesn't work for:
+ // - enum types because they have to be explicitly static_cast.
+ // - vectors of bool, since they are a template specialization.
+ // - multiple-byte types due to endianness.
+ code +=
+ "std::copy(_e->begin(), _e->end(), _o->" + name + ".begin()); }";
+ } else {
+ std::string indexing;
+ if (field.value.type.enum_def) {
+ indexing += "static_cast<" +
+ WrapInNameSpace(*field.value.type.enum_def) + ">(";
+ }
+ indexing += "_e->Get(_i)";
+ if (field.value.type.enum_def) { indexing += ")"; }
+ if (field.value.type.element == BASE_TYPE_BOOL) {
+ indexing += " != 0";
+ }
+ // Generate code that pushes data from _e to _o in the form:
+ // for (uoffset_t i = 0; i < _e->size(); ++i) {
+ // _o->field.push_back(_e->Get(_i));
+ // }
+ auto access =
+ field.value.type.element == BASE_TYPE_UTYPE
+ ? ".type"
+ : (field.value.type.element == BASE_TYPE_UNION ? ".value"
+ : "");
+
+ code += "for (flatbuffers::uoffset_t _i = 0;";
+ code += " _i < _e->size(); _i++) { ";
+ auto cpp_type = field.attributes.Lookup("cpp_type");
+ if (cpp_type) {
+ // Generate code that resolves the cpp pointer type, of the form:
+ // if (resolver)
+ // (*resolver)(&_o->field, (hash_value_t)(_e));
+ // else
+ // _o->field = nullptr;
+ code += "//vector resolver, " + PtrType(&field) + "\n";
+ code += "if (_resolver) ";
+ code += "(*_resolver)";
+ code += "(reinterpret_cast<void **>(&_o->" + name + "[_i]" +
+ access + "), ";
+ code +=
+ "static_cast<flatbuffers::hash_value_t>(" + indexing + "));";
+ if (PtrType(&field) == "naked") {
+ code += " else ";
+ code += "_o->" + name + "[_i]" + access + " = nullptr";
+ } else {
+ // code += " else ";
+ // code += "_o->" + name + "[_i]" + access + " = " +
+ // GenTypeNativePtr(cpp_type->constant, &field, true) + "();";
+ code += "/* else do nothing */";
+ }
+ } else {
+ code += "_o->" + name + "[_i]" + access + " = ";
+ code += GenUnpackVal(field.value.type.VectorType(), indexing, true,
+ field);
+ }
+ code += "; } }";
+ }
+ break;
+ }
+ case BASE_TYPE_UTYPE: {
+ FLATBUFFERS_ASSERT(union_field->value.type.base_type ==
+ BASE_TYPE_UNION);
+ // Generate code that sets the union type, of the form:
+ // _o->field.type = _e;
+ code += "_o->" + union_field->name + ".type = _e;";
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ // Generate code that sets the union value, of the form:
+ // _o->field.value = Union::Unpack(_e, field_type(), resolver);
+ code += "_o->" + Name(field) + ".value = ";
+ code += GenUnionUnpackVal(field, "", "");
+ code += ";";
+ break;
+ }
+ default: {
+ auto cpp_type = field.attributes.Lookup("cpp_type");
+ if (cpp_type) {
+ // Generate code that resolves the cpp pointer type, of the form:
+ // if (resolver)
+ // (*resolver)(&_o->field, (hash_value_t)(_e));
+ // else
+ // _o->field = nullptr;
+ code += "//scalar resolver, " + PtrType(&field) + " \n";
+ code += "if (_resolver) ";
+ code += "(*_resolver)";
+ code += "(reinterpret_cast<void **>(&_o->" + Name(field) + "), ";
+ code += "static_cast<flatbuffers::hash_value_t>(_e));";
+ if (PtrType(&field) == "naked") {
+ code += " else ";
+ code += "_o->" + Name(field) + " = nullptr;";
+ } else {
+ // code += " else ";
+ // code += "_o->" + Name(field) + " = " +
+ // GenTypeNativePtr(cpp_type->constant, &field, true) + "();";
+ code += "/* else do nothing */;";
+ }
+ } else {
+ // Generate code for assigning the value, of the form:
+ // _o->field = value;
+ code += "_o->" + Name(field) + " = ";
+ code += GenUnpackVal(field.value.type, "_e", false, field) + ";";
+ }
+ break;
+ }
+ }
+ return code;
+ }
+
+ std::string GenCreateParam(const FieldDef &field) {
+ std::string value = "_o->";
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) {
+ value += StripUnionType(Name(field));
+ value += ".type";
+ } else {
+ value += Name(field);
+ }
+ if (field.value.type.base_type != BASE_TYPE_VECTOR &&
+ field.attributes.Lookup("cpp_type")) {
+ auto type = GenTypeBasic(field.value.type, false);
+ value =
+ "_rehasher ? "
+ "static_cast<" +
+ type + ">((*_rehasher)(" + value + GenPtrGet(field) + ")) : 0";
+ }
+
+ std::string code;
+ switch (field.value.type.base_type) {
+ // String fields are of the form:
+ // _fbb.CreateString(_o->field)
+ // or
+ // _fbb.CreateSharedString(_o->field)
+ case BASE_TYPE_STRING: {
+ if (!field.shared) {
+ code += "_fbb.CreateString(";
+ } else {
+ code += "_fbb.CreateSharedString(";
+ }
+ code += value;
+ code.push_back(')');
+
+ // For optional fields, check to see if there actually is any data
+ // in _o->field before attempting to access it. If there isn't,
+ // depending on set_empty_strings_to_null either set it to 0 or an empty
+ // string.
+ if (!field.IsRequired()) {
+ auto empty_value = opts_.set_empty_strings_to_null
+ ? "0"
+ : "_fbb.CreateSharedString(\"\")";
+ code = value + ".empty() ? " + empty_value + " : " + code;
+ }
+ break;
+ }
+ // Vector fields come in several flavours, of the forms:
+ // _fbb.CreateVector(_o->field);
+ // _fbb.CreateVector((const utype*)_o->field.data(),
+ // _o->field.size()); _fbb.CreateVectorOfStrings(_o->field)
+ // _fbb.CreateVectorOfStructs(_o->field)
+ // _fbb.CreateVector<Offset<T>>(_o->field.size() [&](size_t i) {
+ // return CreateT(_fbb, _o->Get(i), rehasher);
+ // });
+ case BASE_TYPE_VECTOR: {
+ auto vector_type = field.value.type.VectorType();
+ switch (vector_type.base_type) {
+ case BASE_TYPE_STRING: {
+ if (NativeString(&field) == "std::string") {
+ code += "_fbb.CreateVectorOfStrings(" + value + ")";
+ } else {
+ // Use by-function serialization to emulate
+ // CreateVectorOfStrings(); this works also with non-std strings.
+ code +=
+ "_fbb.CreateVector<flatbuffers::Offset<flatbuffers::String>>"
+ " ";
+ code += "(" + value + ".size(), ";
+ code += "[](size_t i, _VectorArgs *__va) { ";
+ code +=
+ "return __va->__fbb->CreateString(__va->_" + value + "[i]);";
+ code += " }, &_va )";
+ }
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (IsStruct(vector_type)) {
+ const auto &struct_attrs =
+ field.value.type.struct_def->attributes;
+ const auto native_type = struct_attrs.Lookup("native_type");
+ if (native_type) {
+ code += "_fbb.CreateVectorOfNativeStructs<";
+ code += WrapInNameSpace(*vector_type.struct_def) + ", " +
+ native_type->constant + ">";
+ code += "(" + value;
+ const auto pack_name =
+ struct_attrs.Lookup("native_type_pack_name");
+ if (pack_name) {
+ code += ", flatbuffers::Pack" + pack_name->constant;
+ }
+ code += ")";
+ } else {
+ code += "_fbb.CreateVectorOfStructs";
+ code += "(" + value + ")";
+ }
+ } else {
+ code += "_fbb.CreateVector<flatbuffers::Offset<";
+ code += WrapInNameSpace(*vector_type.struct_def) + ">> ";
+ code += "(" + value + ".size(), ";
+ code += "[](size_t i, _VectorArgs *__va) { ";
+ code += "return Create" + vector_type.struct_def->name;
+ code += "(*__va->__fbb, __va->_" + value + "[i]" +
+ GenPtrGet(field) + ", ";
+ code += "__va->__rehasher); }, &_va )";
+ }
+ break;
+ }
+ case BASE_TYPE_BOOL: {
+ code += "_fbb.CreateVector(" + value + ")";
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ code +=
+ "_fbb.CreateVector<flatbuffers::"
+ "Offset<void>>(" +
+ value +
+ ".size(), [](size_t i, _VectorArgs *__va) { "
+ "return __va->_" +
+ value + "[i].Pack(*__va->__fbb, __va->__rehasher); }, &_va)";
+ break;
+ }
+ case BASE_TYPE_UTYPE: {
+ value = StripUnionType(value);
+ code += "_fbb.CreateVector<uint8_t>(" + value +
+ ".size(), [](size_t i, _VectorArgs *__va) { "
+ "return static_cast<uint8_t>(__va->_" +
+ value + "[i].type); }, &_va)";
+ break;
+ }
+ default: {
+ if (field.value.type.enum_def &&
+ !VectorElementUserFacing(vector_type)) {
+ // For enumerations, we need to get access to the array data for
+ // the underlying storage type (eg. uint8_t).
+ const auto basetype = GenTypeBasic(
+ field.value.type.enum_def->underlying_type, false);
+ code += "_fbb.CreateVectorScalarCast<" + basetype +
+ ">(flatbuffers::data(" + value + "), " + value +
+ ".size())";
+ } else if (field.attributes.Lookup("cpp_type")) {
+ auto type = GenTypeBasic(vector_type, false);
+ code += "_fbb.CreateVector<" + type + ">(" + value + ".size(), ";
+ code += "[](size_t i, _VectorArgs *__va) { ";
+ code += "return __va->__rehasher ? ";
+ code += "static_cast<" + type + ">((*__va->__rehasher)";
+ code += "(__va->_" + value + "[i]" + GenPtrGet(field) + ")) : 0";
+ code += "; }, &_va )";
+ } else {
+ code += "_fbb.CreateVector(" + value + ")";
+ }
+ break;
+ }
+ }
+
+ // If set_empty_vectors_to_null option is enabled, for optional fields,
+ // check to see if there actually is any data in _o->field before
+ // attempting to access it.
+ if (opts_.set_empty_vectors_to_null && !field.IsRequired()) {
+ code = value + ".size() ? " + code + " : 0";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ // _o->field.Pack(_fbb);
+ code += value + ".Pack(_fbb)";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (IsStruct(field.value.type)) {
+ const auto &struct_attribs = field.value.type.struct_def->attributes;
+ const auto native_type = struct_attribs.Lookup("native_type");
+ if (native_type) {
+ code += "flatbuffers::Pack";
+ const auto pack_name =
+ struct_attribs.Lookup("native_type_pack_name");
+ if (pack_name) { code += pack_name->constant; }
+ code += "(" + value + ")";
+ } else if (field.native_inline) {
+ code += "&" + value;
+ } else {
+ code += value + " ? " + value + GenPtrGet(field) + " : 0";
+ }
+ } else {
+ // _o->field ? CreateT(_fbb, _o->field.get(), _rehasher);
+ const auto type = field.value.type.struct_def->name;
+ code += value + " ? Create" + type;
+ code += "(_fbb, " + value + GenPtrGet(field) + ", _rehasher)";
+ code += " : 0";
+ }
+ break;
+ }
+ default: {
+ code += value;
+ break;
+ }
+ }
+ return code;
+ }
+
+ // Generate code for tables that needs to come after the regular definition.
+ void GenTablePost(const StructDef &struct_def) {
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_.SetValue("NATIVE_NAME",
+ NativeName(Name(struct_def), &struct_def, opts_));
+
+ if (opts_.generate_object_based_api) {
+ // Generate the X::UnPack() method.
+ code_ +=
+ "inline " + TableUnPackSignature(struct_def, false, opts_) + " {";
+
+ if (opts_.g_cpp_std == cpp::CPP_STD_X0) {
+ auto native_name = WrapNativeNameInNameSpace(struct_def, parser_.opts);
+ code_.SetValue("POINTER_TYPE",
+ GenTypeNativePtr(native_name, nullptr, false));
+ code_ +=
+ " {{POINTER_TYPE}} _o = {{POINTER_TYPE}}(new {{NATIVE_NAME}}());";
+ } else if (opts_.g_cpp_std == cpp::CPP_STD_11) {
+ code_ +=
+ " auto _o = std::unique_ptr<{{NATIVE_NAME}}>(new "
+ "{{NATIVE_NAME}}());";
+ } else {
+ code_ += " auto _o = std::make_unique<{{NATIVE_NAME}}>();";
+ }
+ code_ += " UnPackTo(_o.get(), _resolver);";
+ code_ += " return _o.release();";
+ code_ += "}";
+ code_ += "";
+ code_ +=
+ "inline " + TableUnPackToSignature(struct_def, false, opts_) + " {";
+ code_ += " (void)_o;";
+ code_ += " (void)_resolver;";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) { continue; }
+
+ // Assign a value from |this| to |_o|. Values from |this| are stored
+ // in a variable |_e| by calling this->field_type(). The value is then
+ // assigned to |_o| using the GenUnpackFieldStatement.
+ const bool is_union = field.value.type.base_type == BASE_TYPE_UTYPE;
+ const auto statement =
+ GenUnpackFieldStatement(field, is_union ? *(it + 1) : nullptr);
+
+ code_.SetValue("FIELD_NAME", Name(field));
+ auto prefix = " { auto _e = {{FIELD_NAME}}(); ";
+ auto check = IsScalar(field.value.type.base_type) ? "" : "if (_e) ";
+ auto postfix = " }";
+ code_ += std::string(prefix) + check + statement + postfix;
+ }
+ code_ += "}";
+ code_ += "";
+
+ // Generate the X::Pack member function that simply calls the global
+ // CreateX function.
+ code_ += "inline " + TablePackSignature(struct_def, false, opts_) + " {";
+ code_ += " return Create{{STRUCT_NAME}}(_fbb, _o, _rehasher);";
+ code_ += "}";
+ code_ += "";
+
+ // Generate a CreateX method that works with an unpacked C++ object.
+ code_ +=
+ "inline " + TableCreateSignature(struct_def, false, opts_) + " {";
+ code_ += " (void)_rehasher;";
+ code_ += " (void)_o;";
+
+ code_ +=
+ " struct _VectorArgs "
+ "{ flatbuffers::FlatBufferBuilder *__fbb; "
+ "const " +
+ NativeName(Name(struct_def), &struct_def, opts_) +
+ "* __o; "
+ "const flatbuffers::rehasher_function_t *__rehasher; } _va = { "
+ "&_fbb, _o, _rehasher}; (void)_va;";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) { continue; }
+ if (IsVector(field.value.type)) {
+ const std::string force_align_code =
+ GenVectorForceAlign(field, "_o->" + Name(field) + ".size()");
+ if (!force_align_code.empty()) { code_ += " " + force_align_code; }
+ }
+ code_ += " auto _" + Name(field) + " = " + GenCreateParam(field) + ";";
+ }
+ // Need to call "Create" with the struct namespace.
+ const auto qualified_create_name =
+ struct_def.defined_namespace->GetFullyQualifiedName("Create");
+ code_.SetValue("CREATE_NAME", TranslateNameSpace(qualified_create_name));
+
+ code_ += " return {{CREATE_NAME}}{{STRUCT_NAME}}(";
+ code_ += " _fbb\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) { continue; }
+
+ bool pass_by_address = false;
+ if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ if (IsStruct(field.value.type)) {
+ auto native_type =
+ field.value.type.struct_def->attributes.Lookup("native_type");
+ if (native_type) { pass_by_address = true; }
+ }
+ }
+
+ // Call the CreateX function using values from |_o|.
+ if (pass_by_address) {
+ code_ += ",\n &_" + Name(field) + "\\";
+ } else {
+ code_ += ",\n _" + Name(field) + "\\";
+ }
+ }
+ code_ += ");";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ static void GenPadding(
+ const FieldDef &field, std::string *code_ptr, int *id,
+ const std::function<void(int bits, std::string *code_ptr, int *id)> &f) {
+ if (field.padding) {
+ for (int i = 0; i < 4; i++) {
+ if (static_cast<int>(field.padding) & (1 << i)) {
+ f((1 << i) * 8, code_ptr, id);
+ }
+ }
+ FLATBUFFERS_ASSERT(!(field.padding & ~0xF));
+ }
+ }
+
+ static void PaddingDefinition(int bits, std::string *code_ptr, int *id) {
+ *code_ptr += " int" + NumToString(bits) + "_t padding" +
+ NumToString((*id)++) + "__;";
+ }
+
+ static void PaddingInitializer(int bits, std::string *code_ptr, int *id) {
+ (void)bits;
+ if (!code_ptr->empty()) *code_ptr += ",\n ";
+ *code_ptr += "padding" + NumToString((*id)++) + "__(0)";
+ }
+
+ static void PaddingNoop(int bits, std::string *code_ptr, int *id) {
+ (void)bits;
+ if (!code_ptr->empty()) *code_ptr += '\n';
+ *code_ptr += " (void)padding" + NumToString((*id)++) + "__;";
+ }
+
+ void GenStructDefaultConstructor(const StructDef &struct_def) {
+ std::string init_list;
+ std::string body;
+ bool first_in_init_list = true;
+ int padding_initializer_id = 0;
+ int padding_body_id = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto field = *it;
+ const auto field_name = field->name + "_";
+
+ if (first_in_init_list) {
+ first_in_init_list = false;
+ } else {
+ init_list += ",";
+ init_list += "\n ";
+ }
+
+ init_list += field_name;
+ if (IsStruct(field->value.type) || IsArray(field->value.type)) {
+ // this is either default initialization of struct
+ // or
+ // implicit initialization of array
+ // for each object in array it:
+ // * sets it as zeros for POD types (integral, floating point, etc)
+ // * calls default constructor for classes/structs
+ init_list += "()";
+ } else {
+ init_list += "(0)";
+ }
+ if (field->padding) {
+ GenPadding(*field, &init_list, &padding_initializer_id,
+ PaddingInitializer);
+ GenPadding(*field, &body, &padding_body_id, PaddingNoop);
+ }
+ }
+
+ if (init_list.empty()) {
+ code_ += " {{STRUCT_NAME}}()";
+ code_ += " {}";
+ } else {
+ code_.SetValue("INIT_LIST", init_list);
+ code_ += " {{STRUCT_NAME}}()";
+ code_ += " : {{INIT_LIST}} {";
+ if (!body.empty()) { code_ += body; }
+ code_ += " }";
+ }
+ }
+
+ void GenStructConstructor(const StructDef &struct_def,
+ GenArrayArgMode array_mode) {
+ std::string arg_list;
+ std::string init_list;
+ int padding_id = 0;
+ auto first = struct_def.fields.vec.begin();
+ // skip arrays if generate ctor without array assignment
+ const auto init_arrays = (array_mode != kArrayArgModeNone);
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ const auto &type = field.value.type;
+ const auto is_array = IsArray(type);
+ const auto arg_name = "_" + Name(field);
+ if (!is_array || init_arrays) {
+ if (it != first && !arg_list.empty()) { arg_list += ", "; }
+ arg_list += !is_array ? GenTypeGet(type, " ", "const ", " &", true)
+ : GenTypeSpan(type, true, type.fixed_length);
+ arg_list += arg_name;
+ }
+ // skip an array with initialization from span
+ if (false == (is_array && init_arrays)) {
+ if (it != first && !init_list.empty()) { init_list += ",\n "; }
+ init_list += Name(field) + "_";
+ if (IsScalar(type.base_type)) {
+ auto scalar_type = GenUnderlyingCast(field, false, arg_name);
+ init_list += "(flatbuffers::EndianScalar(" + scalar_type + "))";
+ } else {
+ FLATBUFFERS_ASSERT((is_array && !init_arrays) || IsStruct(type));
+ if (!is_array)
+ init_list += "(" + arg_name + ")";
+ else
+ init_list += "()";
+ }
+ }
+ if (field.padding)
+ GenPadding(field, &init_list, &padding_id, PaddingInitializer);
+ }
+
+ if (!arg_list.empty()) {
+ code_.SetValue("ARG_LIST", arg_list);
+ code_.SetValue("INIT_LIST", init_list);
+ if (!init_list.empty()) {
+ code_ += " {{STRUCT_NAME}}({{ARG_LIST}})";
+ code_ += " : {{INIT_LIST}} {";
+ } else {
+ code_ += " {{STRUCT_NAME}}({{ARG_LIST}}) {";
+ }
+ padding_id = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ const auto &type = field.value.type;
+ if (IsArray(type) && init_arrays) {
+ const auto &element_type = type.VectorType();
+ const auto is_enum = IsEnum(element_type);
+ FLATBUFFERS_ASSERT(
+ (IsScalar(element_type.base_type) || IsStruct(element_type)) &&
+ "invalid declaration");
+ const auto face_type = GenTypeGet(type, " ", "", "", is_enum);
+ std::string get_array =
+ is_enum ? "CastToArrayOfEnum<" + face_type + ">" : "CastToArray";
+ const auto field_name = Name(field) + "_";
+ const auto arg_name = "_" + Name(field);
+ code_ += " flatbuffers::" + get_array + "(" + field_name +
+ ").CopyFromSpan(" + arg_name + ");";
+ }
+ if (field.padding) {
+ std::string padding;
+ GenPadding(field, &padding, &padding_id, PaddingNoop);
+ code_ += padding;
+ }
+ }
+ code_ += " }";
+ }
+ }
+
+ void GenArrayAccessor(const Type &type, bool mutable_accessor) {
+ FLATBUFFERS_ASSERT(IsArray(type));
+ const auto is_enum = IsEnum(type.VectorType());
+ // The Array<bool,N> is a tricky case, like std::vector<bool>.
+ // It requires a specialization of Array class.
+ // Generate Array<uint8_t> for Array<bool>.
+ const auto face_type = GenTypeGet(type, " ", "", "", is_enum);
+ std::string ret_type = "flatbuffers::Array<" + face_type + ", " +
+ NumToString(type.fixed_length) + ">";
+ if (mutable_accessor)
+ code_ += " " + ret_type + " *mutable_{{FIELD_NAME}}() {";
+ else
+ code_ += " const " + ret_type + " *{{FIELD_NAME}}() const {";
+
+ std::string get_array =
+ is_enum ? "CastToArrayOfEnum<" + face_type + ">" : "CastToArray";
+ code_ += " return &flatbuffers::" + get_array + "({{FIELD_VALUE}});";
+ code_ += " }";
+ }
+
+ // Generate an accessor struct with constructor for a flatbuffers struct.
+ void GenStruct(const StructDef &struct_def) {
+ // Generate an accessor struct, with private variables of the form:
+ // type name_;
+ // Generates manual padding and alignment.
+ // Variables are private because they contain little endian data on all
+ // platforms.
+ GenComment(struct_def.doc_comment);
+ code_.SetValue("ALIGN", NumToString(struct_def.minalign));
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+
+ code_ +=
+ "FLATBUFFERS_MANUALLY_ALIGNED_STRUCT({{ALIGN}}) "
+ "{{STRUCT_NAME}} FLATBUFFERS_FINAL_CLASS {";
+ code_ += " private:";
+
+ int padding_id = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ const auto &field_type = field.value.type;
+ code_.SetValue("FIELD_TYPE", GenTypeGet(field_type, " ", "", " ", false));
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("ARRAY",
+ IsArray(field_type)
+ ? "[" + NumToString(field_type.fixed_length) + "]"
+ : "");
+ code_ += (" {{FIELD_TYPE}}{{FIELD_NAME}}_{{ARRAY}};");
+
+ if (field.padding) {
+ std::string padding;
+ GenPadding(field, &padding, &padding_id, PaddingDefinition);
+ code_ += padding;
+ }
+ }
+
+ // Generate GetFullyQualifiedName
+ code_ += "";
+ code_ += " public:";
+
+ if (opts_.g_cpp_std >= cpp::CPP_STD_17) { code_ += " struct Traits;"; }
+
+ // Make TypeTable accessible via the generated struct.
+ if (opts_.mini_reflect != IDLOptions::kNone) {
+ code_ +=
+ " static const flatbuffers::TypeTable *MiniReflectTypeTable() {";
+ code_ += " return {{STRUCT_NAME}}TypeTable();";
+ code_ += " }";
+ }
+
+ GenFullyQualifiedNameGetter(struct_def, Name(struct_def));
+
+ // Generate a default constructor.
+ GenStructDefaultConstructor(struct_def);
+
+ // Generate a constructor that takes all fields as arguments,
+ // excluding arrays.
+ GenStructConstructor(struct_def, kArrayArgModeNone);
+
+ auto arrays_num = std::count_if(struct_def.fields.vec.begin(),
+ struct_def.fields.vec.end(),
+ [](const flatbuffers::FieldDef *fd) {
+ return IsArray(fd->value.type);
+ });
+ if (arrays_num > 0) {
+ GenStructConstructor(struct_def, kArrayArgModeSpanStatic);
+ }
+
+ // Generate accessor methods of the form:
+ // type name() const { return flatbuffers::EndianScalar(name_); }
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ const auto &type = field.value.type;
+ const auto is_scalar = IsScalar(type.base_type);
+ const auto is_array = IsArray(type);
+
+ const auto field_type = GenTypeGet(type, " ", is_array ? "" : "const ",
+ is_array ? "" : " &", true);
+ auto member = Name(field) + "_";
+ auto value =
+ is_scalar ? "flatbuffers::EndianScalar(" + member + ")" : member;
+
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_TYPE", field_type);
+ code_.SetValue("FIELD_VALUE", GenUnderlyingCast(field, true, value));
+
+ GenComment(field.doc_comment, " ");
+
+ // Generate a const accessor function.
+ if (is_array) {
+ GenArrayAccessor(type, false);
+ } else {
+ code_ += " {{FIELD_TYPE}}{{FIELD_NAME}}() const {";
+ code_ += " return {{FIELD_VALUE}};";
+ code_ += " }";
+ }
+
+ // Generate a mutable accessor function.
+ if (opts_.mutable_buffer) {
+ auto mut_field_type =
+ GenTypeGet(type, " ", "", is_array ? "" : " &", true);
+ code_.SetValue("FIELD_TYPE", mut_field_type);
+ if (is_scalar) {
+ code_.SetValue("ARG", GenTypeBasic(type, true));
+ code_.SetValue("FIELD_VALUE",
+ GenUnderlyingCast(field, false, "_" + Name(field)));
+
+ code_ += " void mutate_{{FIELD_NAME}}({{ARG}} _{{FIELD_NAME}}) {";
+ code_ +=
+ " flatbuffers::WriteScalar(&{{FIELD_NAME}}_, "
+ "{{FIELD_VALUE}});";
+ code_ += " }";
+ } else if (is_array) {
+ GenArrayAccessor(type, true);
+ } else {
+ code_ += " {{FIELD_TYPE}}mutable_{{FIELD_NAME}}() {";
+ code_ += " return {{FIELD_VALUE}};";
+ code_ += " }";
+ }
+ }
+
+ // Generate a comparison function for this field if it is a key.
+ if (field.key) { GenKeyFieldMethods(field); }
+ }
+ code_.SetValue("NATIVE_NAME", Name(struct_def));
+ GenOperatorNewDelete(struct_def);
+
+ if (opts_.cpp_static_reflection) { GenIndexBasedFieldGetter(struct_def); }
+
+ code_ += "};";
+
+ code_.SetValue("STRUCT_BYTE_SIZE", NumToString(struct_def.bytesize));
+ code_ += "FLATBUFFERS_STRUCT_END({{STRUCT_NAME}}, {{STRUCT_BYTE_SIZE}});";
+ if (opts_.gen_compare) GenCompareOperator(struct_def, "()");
+ code_ += "";
+
+ // Definition for type traits for this table type. This allows querying var-
+ // ious compile-time traits of the table.
+ if (opts_.g_cpp_std >= cpp::CPP_STD_17) { GenTraitsStruct(struct_def); }
+ }
+
+ // Set up the correct namespace. Only open a namespace if the existing one is
+ // different (closing/opening only what is necessary).
+ //
+ // The file must start and end with an empty (or null) namespace so that
+ // namespaces are properly opened and closed.
+ void SetNameSpace(const Namespace *ns) {
+ if (cur_name_space_ == ns) { return; }
+
+ // Compute the size of the longest common namespace prefix.
+ // If cur_name_space is A::B::C::D and ns is A::B::E::F::G,
+ // the common prefix is A::B:: and we have old_size = 4, new_size = 5
+ // and common_prefix_size = 2
+ size_t old_size = cur_name_space_ ? cur_name_space_->components.size() : 0;
+ size_t new_size = ns ? ns->components.size() : 0;
+
+ size_t common_prefix_size = 0;
+ while (common_prefix_size < old_size && common_prefix_size < new_size &&
+ ns->components[common_prefix_size] ==
+ cur_name_space_->components[common_prefix_size]) {
+ common_prefix_size++;
+ }
+
+ // Close cur_name_space in reverse order to reach the common prefix.
+ // In the previous example, D then C are closed.
+ for (size_t j = old_size; j > common_prefix_size; --j) {
+ code_ += "} // namespace " + cur_name_space_->components[j - 1];
+ }
+ if (old_size != common_prefix_size) { code_ += ""; }
+
+ // open namespace parts to reach the ns namespace
+ // in the previous example, E, then F, then G are opened
+ for (auto j = common_prefix_size; j != new_size; ++j) {
+ code_ += "namespace " + ns->components[j] + " {";
+ }
+ if (new_size != common_prefix_size) { code_ += ""; }
+
+ cur_name_space_ = ns;
+ }
+};
+
+} // namespace cpp
+
+bool GenerateCPP(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ cpp::IDLOptionsCpp opts(parser.opts);
+ // The '--cpp_std' argument could be extended (like ASAN):
+ // Example: "flatc --cpp_std c++17:option1:option2".
+ auto cpp_std = !opts.cpp_std.empty() ? opts.cpp_std : "C++11";
+ std::transform(cpp_std.begin(), cpp_std.end(), cpp_std.begin(), CharToUpper);
+ if (cpp_std == "C++0X") {
+ opts.g_cpp_std = cpp::CPP_STD_X0;
+ opts.g_only_fixed_enums = false;
+ } else if (cpp_std == "C++11") {
+ // Use the standard C++11 code generator.
+ opts.g_cpp_std = cpp::CPP_STD_11;
+ opts.g_only_fixed_enums = true;
+ } else if (cpp_std == "C++17") {
+ opts.g_cpp_std = cpp::CPP_STD_17;
+ // With c++17 generate strong enums only.
+ opts.scoped_enums = true;
+ // By default, prefixed_enums==true, reset it.
+ opts.prefixed_enums = false;
+ } else {
+ LogCompilerError("Unknown value of the '--cpp-std' switch: " +
+ opts.cpp_std);
+ return false;
+ }
+ // The opts.scoped_enums has priority.
+ opts.g_only_fixed_enums |= opts.scoped_enums;
+
+ if (opts.cpp_static_reflection && opts.g_cpp_std < cpp::CPP_STD_17) {
+ LogCompilerError(
+ "--cpp-static-reflection requires using --cpp-std at \"C++17\" or "
+ "higher.");
+ return false;
+ }
+
+ cpp::CppGenerator generator(parser, path, file_name, opts);
+ return generator.generate();
+}
+
+std::string CPPMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ const auto filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ cpp::CppGenerator geneartor(parser, path, file_name, parser.opts);
+ const auto included_files = parser.GetIncludedFilesRecursive(file_name);
+ std::string make_rule =
+ geneartor.GeneratedFileName(path, filebase, parser.opts) + ": ";
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_cpp_yandex_maps_iter.cpp b/contrib/libs/flatbuffers/src/idl_gen_cpp_yandex_maps_iter.cpp
new file mode 100644
index 0000000000..1ba2ef3e4a
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_cpp_yandex_maps_iter.cpp
@@ -0,0 +1,731 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/flatc.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+// Pedantic warning free version of toupper().
+inline char ToUpper(char c) {
+ return static_cast<char>(::toupper(static_cast<unsigned char>(c)));
+}
+
+static std::string GeneratedIterFileName(const std::string &path,
+ const std::string &file_name) {
+ return path + file_name + ".iter.fbs.h";
+}
+
+namespace cpp_yandex_maps_iter {
+class CppIterGenerator : public BaseGenerator {
+ public:
+ CppIterGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "::", "h"),
+ cur_name_space_(nullptr) {
+ static const char *const keywords[] = {
+ "alignas",
+ "alignof",
+ "and",
+ "and_eq",
+ "asm",
+ "atomic_cancel",
+ "atomic_commit",
+ "atomic_noexcept",
+ "auto",
+ "bitand",
+ "bitor",
+ "bool",
+ "break",
+ "case",
+ "catch",
+ "char",
+ "char16_t",
+ "char32_t",
+ "class",
+ "compl",
+ "concept",
+ "const",
+ "constexpr",
+ "const_cast",
+ "continue",
+ "co_await",
+ "co_return",
+ "co_yield",
+ "decltype",
+ "default",
+ "delete",
+ "do",
+ "double",
+ "dynamic_cast",
+ "else",
+ "enum",
+ "explicit",
+ "export",
+ "extern",
+ "false",
+ "float",
+ "for",
+ "friend",
+ "goto",
+ "if",
+ "import",
+ "inline",
+ "int",
+ "long",
+ "module",
+ "mutable",
+ "namespace",
+ "new",
+ "noexcept",
+ "not",
+ "not_eq",
+ "nullptr",
+ "operator",
+ "or",
+ "or_eq",
+ "private",
+ "protected",
+ "public",
+ "register",
+ "reinterpret_cast",
+ "requires",
+ "return",
+ "short",
+ "signed",
+ "sizeof",
+ "static",
+ "static_assert",
+ "static_cast",
+ "struct",
+ "switch",
+ "synchronized",
+ "template",
+ "this",
+ "thread_local",
+ "throw",
+ "true",
+ "try",
+ "typedef",
+ "typeid",
+ "typename",
+ "union",
+ "unsigned",
+ "using",
+ "virtual",
+ "void",
+ "volatile",
+ "wchar_t",
+ "while",
+ "xor",
+ "xor_eq",
+ nullptr,
+ };
+ for (auto kw = keywords; *kw; kw++) keywords_.insert(*kw);
+ }
+
+ std::string GenIncludeGuard() const {
+ // Generate include guard.
+ std::string guard = file_name_;
+ // Remove any non-alpha-numeric characters that may appear in a filename.
+ struct IsAlnum {
+ bool operator()(char c) const { return !isalnum(c); }
+ };
+ guard.erase(std::remove_if(guard.begin(), guard.end(), IsAlnum()),
+ guard.end());
+ guard = "FLATBUFFERS_GENERATED_" + guard;
+ guard += "_";
+ // For further uniqueness, also add the namespace.
+ auto name_space = parser_.current_namespace_;
+ for (auto it = name_space->components.begin();
+ it != name_space->components.end(); ++it) {
+ guard += *it + "_";
+ }
+ guard += "ITER_";
+ guard += "H_";
+ std::transform(guard.begin(), guard.end(), guard.begin(), ToUpper);
+ return guard;
+ }
+
+ void GenIncludeDependencies() {
+ int num_includes = 0;
+ for (auto it = parser_.native_included_files_.begin();
+ it != parser_.native_included_files_.end(); ++it) {
+ code_ += "#include \"" + *it + "\"";
+ num_includes++;
+ }
+ for (auto it = parser_.included_files_.begin();
+ it != parser_.included_files_.end(); ++it) {
+ if (it->second.empty()) continue;
+ auto noext = flatbuffers::StripExtension(it->second);
+ auto basename = flatbuffers::StripPath(noext);
+
+ code_ += "#include \"" + parser_.opts.include_prefix +
+ (parser_.opts.keep_include_path ? noext : basename) +
+ ".iter.fbs.h\"";
+ num_includes++;
+ }
+ if (num_includes) code_ += "";
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+
+ std::string Name(const Definition &def) const {
+ return EscapeKeyword(def.name);
+ }
+
+ std::string Name(const EnumVal &ev) const { return EscapeKeyword(ev.name); }
+
+ // Iterate through all definitions we haven't generate code for (enums,
+ // structs, and tables) and output them to a single file.
+ bool generate() {
+ code_.Clear();
+ code_ += "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ const auto include_guard = GenIncludeGuard();
+ code_ += "#ifndef " + include_guard;
+ code_ += "#define " + include_guard;
+ code_ += "";
+
+ if (parser_.opts.gen_nullable) {
+ code_ += "#pragma clang system_header\n\n";
+ }
+
+ code_ += "#include \"" + file_name_ + ".fbs.h\"";
+ code_ += "#include \"contrib/libs/flatbuffers/include/flatbuffers/flatbuffers_iter.h\"";
+ code_ += "";
+
+ if (parser_.opts.include_dependence_headers) { GenIncludeDependencies(); }
+
+ FLATBUFFERS_ASSERT(!cur_name_space_);
+
+ // Generate forward declarations for all structs/tables, since they may
+ // have circular references.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.generated && !struct_def.fixed) {
+ SetNameSpace(struct_def.defined_namespace);
+ code_ += "template <typename Iter>";
+ code_ += "struct " + Name(struct_def) + ";";
+ code_ += "";
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.fixed && !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenTable(struct_def);
+ }
+ }
+
+ // Generate convenient global helper functions:
+ if (parser_.root_struct_def_ && !parser_.root_struct_def_->fixed) {
+ auto &struct_def = *parser_.root_struct_def_;
+ SetNameSpace(struct_def.defined_namespace);
+ auto name = Name(struct_def);
+ auto qualified_name = cur_name_space_->GetFullyQualifiedName(name);
+ auto cpp_name = TranslateNameSpace(qualified_name, true);
+ const auto cpp_non_iter_name = TranslateNameSpace(qualified_name);
+ const auto cpp_non_iter_getter = TranslateNameSpace(
+ parser_.namespaces_.back()->GetFullyQualifiedName("Get"+name));
+
+ code_.SetValue("STRUCT_NAME", name);
+ code_.SetValue("CPP_NAME", cpp_name);
+ code_.SetValue("CPP_NON_ITER_NAME", cpp_non_iter_name);
+ code_.SetValue("CPP_NON_ITER_GETTER", cpp_non_iter_getter);
+
+ // The root datatype accessor:
+ code_ += "template <typename Iter>";
+ code_ += "inline \\";
+ code_ += "std::optional<{{CPP_NAME}}<Iter>> Get{{STRUCT_NAME}}(const Iter& buf) {";
+ code_ += " return yandex::maps::flatbuffers_iter::GetRoot<{{CPP_NAME}}<Iter>, Iter>(buf);";
+ code_ += "}";
+ code_ += "";
+
+ // The non_iter datatype accessor:
+ code_ += "inline \\";
+ code_ += "const {{CPP_NON_ITER_NAME}} *Get{{STRUCT_NAME}}(const char *buf) {";
+ code_ += " return {{CPP_NON_ITER_GETTER}}(buf);";
+ code_ += "}";
+ code_ += "";
+
+ if (parser_.file_identifier_.length()) {
+ // Return the identifier
+ code_ += "inline const char *{{STRUCT_NAME}}Identifier() {";
+ code_ += " return \"" + parser_.file_identifier_ + "\";";
+ code_ += "}";
+ code_ += "";
+
+ // Check if a buffer has the identifier.
+ code_ += "template <typename Iter>";
+ code_ += "inline \\";
+ code_ += "bool {{STRUCT_NAME}}BufferHasIdentifier(const Iter& buf) {";
+ code_ += " return yandex::maps::flatbuffers_iter::BufferHasIdentifier(";
+ code_ += " buf, {{STRUCT_NAME}}Identifier());";
+ code_ += "}";
+ code_ += "";
+ }
+
+ // The root verifier.
+ if (parser_.file_identifier_.length()) {
+ code_.SetValue("ID", name + "Identifier()");
+ } else {
+ code_.SetValue("ID", "nullptr");
+ }
+
+ code_ += "template <typename Iter>";
+ code_ += "inline bool Verify{{STRUCT_NAME}}Buffer(";
+ code_ += " yandex::maps::flatbuffers_iter::Verifier<Iter> &verifier) {";
+ code_ += " return verifier.template VerifyBuffer<{{CPP_NAME}}<Iter>>({{ID}});";
+ code_ += "}";
+ code_ += "";
+
+ if (parser_.file_extension_.length()) {
+ // Return the extension
+ code_ += "inline const char *{{STRUCT_NAME}}Extension() {";
+ code_ += " return \"" + parser_.file_extension_ + "\";";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ if (cur_name_space_) SetNameSpace(nullptr);
+
+ // Close the include guard.
+ code_ += "#endif // " + include_guard;
+
+ const auto file_path = GeneratedIterFileName(path_, file_name_);
+ const auto final_code = code_.ToString();
+ return SaveFile(file_path.c_str(), final_code, false);
+ }
+
+ private:
+ CodeWriter code_;
+
+ std::unordered_set<std::string> keywords_;
+
+ // This tracks the current namespace so we can insert namespace declarations.
+ const Namespace *cur_name_space_;
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+// Ensure that a type is prefixed with its namespace whenever it is used
+// outside of its namespace.
+ std::string WrapInNameSpace(const Namespace *ns,
+ const std::string &name, bool needIter = false) const {
+ if (CurrentNameSpace() == ns) return name;
+ std::string qualified_name = qualifying_start_;
+ for (auto it = ns->components.begin(); it != ns->components.end(); ++it)
+ qualified_name += *it + qualifying_separator_;
+ if (needIter)
+ qualified_name += "iter" + qualifying_separator_;
+ return qualified_name + name;
+ }
+
+ std::string WrapInNameSpace(const Definition &def, bool needIter = false) const {
+ return WrapInNameSpace(def.defined_namespace, def.name, needIter);
+ }
+
+ // Translates a qualified name in flatbuffer text format to the same name in
+ // the equivalent C++ namespace.
+ static std::string TranslateNameSpace(const std::string &qualified_name, bool needIter = false) {
+ std::string cpp_qualified_name = qualified_name;
+ size_t start_pos = 0;
+ while ((start_pos = cpp_qualified_name.find(".", start_pos)) !=
+ std::string::npos) {
+ cpp_qualified_name.replace(start_pos, 1, "::");
+ }
+ if (needIter)
+ {
+ start_pos = cpp_qualified_name.rfind("::");
+ if (start_pos != std::string::npos)
+ cpp_qualified_name.replace(start_pos, 2, "::iter::");
+ }
+ return cpp_qualified_name;
+ }
+
+ void GenComment(const std::vector<std::string> &dc, const char *prefix = "") {
+ std::string text;
+ ::flatbuffers::GenComment(dc, &text, nullptr, prefix);
+ code_ += text + "\\";
+ }
+
+ // Return a C++ type from the table in idl.h
+ std::string GenTypeBasic(const Type &type, bool user_facing_type) const {
+ // clang-format off
+ static const char * const ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ #CTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ if (user_facing_type) {
+ if (type.enum_def) return WrapInNameSpace(*type.enum_def);
+ if (type.base_type == BASE_TYPE_BOOL) return "bool";
+ }
+ return ctypename[type.base_type];
+ }
+
+ // Return a C++ pointer type, specialized to the actual struct/table types,
+ // and vector element types.
+ std::string GenTypePointer(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: {
+ return "yandex::maps::flatbuffers_iter::String<Iter>";
+ }
+ case BASE_TYPE_VECTOR: {
+ const auto type_name = GenTypeWire(type.VectorType(), "", false);
+ return "yandex::maps::flatbuffers_iter::Vector<" + type_name + ", Iter>";
+ }
+ case BASE_TYPE_STRUCT: {
+ if (IsStruct(type))
+ return WrapInNameSpace(*type.struct_def, !type.struct_def->fixed);
+ return WrapInNameSpace(*type.struct_def, !type.struct_def->fixed) + "<Iter>";
+ }
+ case BASE_TYPE_UNION:
+ // fall through
+ default: { return "void"; }
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) specifically for
+ // building a flatbuffer.
+ std::string GenTypeWire(const Type &type, const char *postfix,
+ bool user_facing_type) const {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, user_facing_type) + postfix;
+ } else if (IsStruct(type)) {
+ return GenTypePointer(type);
+ } else {
+ return "yandex::maps::flatbuffers_iter::Offset<" + GenTypePointer(type) + ">" + postfix;
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) that reflects its
+ // serialized size.
+ std::string GenTypeSize(const Type &type) const {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, false);
+ } else if (IsStruct(type)) {
+ return GenTypePointer(type);
+ } else {
+ return "yandex::maps::flatbuffers_iter::uoffset_t";
+ }
+ }
+
+ // Return a C++ type for any type (scalar/pointer) specifically for
+ // using a flatbuffer.
+ std::string GenTypeGet(const Type &type, const char *afterbasic,
+ const char *beforeptr, const char *afterptr,
+ bool user_facing_type) {
+ if (IsScalar(type.base_type)) {
+ return GenTypeBasic(type, user_facing_type) + afterbasic;
+ } else {
+ return beforeptr + GenTypePointer(type) + afterptr;
+ }
+ }
+
+ // Generates a value with optionally a cast applied if the field has a
+ // different underlying type from its interface type (currently only the
+ // case for enums. "from" specify the direction, true meaning from the
+ // underlying type to the interface type.
+ std::string GenUnderlyingCast(const FieldDef &field, bool from,
+ const std::string &val) {
+ if (from && field.value.type.base_type == BASE_TYPE_BOOL) {
+ return val + " != 0";
+ } else if ((field.value.type.enum_def &&
+ IsScalar(field.value.type.base_type)) ||
+ field.value.type.base_type == BASE_TYPE_BOOL) {
+ return "static_cast<" + GenTypeBasic(field.value.type, from) + ">(" +
+ val + ")";
+ } else {
+ return val;
+ }
+ }
+
+ std::string GenFieldOffsetName(const FieldDef &field) {
+ std::string uname = Name(field);
+ std::transform(uname.begin(), uname.end(), uname.begin(), ToUpper);
+ return "VT_" + uname;
+ }
+
+ std::string GenDefaultConstant(const FieldDef &field) {
+ return field.value.type.base_type == BASE_TYPE_FLOAT
+ ? field.value.constant + "f"
+ : field.value.constant;
+ }
+
+ // Generate the code to call the appropriate Verify function(s) for a field.
+ void GenVerifyCall(const FieldDef &field, const char *prefix) {
+ code_.SetValue("PRE", prefix);
+ code_.SetValue("NAME", Name(field));
+ code_.SetValue("REQUIRED", field.IsRequired() ? "Required" : "");
+ code_.SetValue("SIZE", GenTypeSize(field.value.type));
+ code_.SetValue("OFFSET", GenFieldOffsetName(field));
+ code_ += "{{PRE}}this->template VerifyField{{REQUIRED}}<{{SIZE}}>(verifier, {{OFFSET}})\\";
+
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_UNION: {
+ code_.SetValue("ENUM_NAME", field.value.type.enum_def->name);
+ code_.SetValue("SUFFIX", UnionTypeFieldSuffix());
+ code_ +=
+ "{{PRE}}Verify{{ENUM_NAME}}(verifier, {{NAME}}(), "
+ "{{NAME}}{{SUFFIX}}())\\";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (!field.value.type.struct_def->fixed) {
+ code_ += "{{PRE}}verifier.VerifyTable({{NAME}}())\\";
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ code_ += "{{PRE}}verifier.Verify({{NAME}}())\\";
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ code_ += "{{PRE}}verifier.Verify({{NAME}}())\\";
+
+ switch (field.value.type.element) {
+ case BASE_TYPE_STRING: {
+ code_ += "{{PRE}}verifier.VerifyVectorOfStrings({{NAME}}())\\";
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (!field.value.type.struct_def->fixed) {
+ code_ += "{{PRE}}verifier.VerifyVectorOfTables({{NAME}}())\\";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ code_.SetValue("ENUM_NAME", field.value.type.enum_def->name);
+ code_ +=
+ "{{PRE}}Verify{{ENUM_NAME}}Vector(verifier, {{NAME}}(), "
+ "{{NAME}}_type())\\";
+ break;
+ }
+ default: break;
+ }
+ break;
+ }
+ default: { break; }
+ }
+ }
+
+ // Generate CompareWithValue method for a key field.
+ void GenKeyFieldMethods(const FieldDef &field) {
+ FLATBUFFERS_ASSERT(field.key);
+ const bool is_string = (field.value.type.base_type == BASE_TYPE_STRING);
+
+ code_ += " bool KeyCompareLessThan(const std::optional<{{STRUCT_NAME}}<Iter>>& o) const {";
+ if (is_string) {
+ // use operator< of flatbuffers::String
+ code_ += " return {{FIELD_NAME}}() < o->{{FIELD_NAME}}();";
+ } else {
+ code_ += " return {{FIELD_NAME}}() < o->{{FIELD_NAME}}();";
+ }
+ code_ += " }";
+
+ if (is_string) {
+ code_ += " int KeyCompareWithValue(const char *val) const {";
+ code_ += " return strcmp({{FIELD_NAME}}()->str().c_str(), val);";
+ code_ += " }";
+ } else {
+ FLATBUFFERS_ASSERT(IsScalar(field.value.type.base_type));
+ auto type = GenTypeBasic(field.value.type, false);
+ if (parser_.opts.scoped_enums && field.value.type.enum_def &&
+ IsScalar(field.value.type.base_type)) {
+ type = GenTypeGet(field.value.type, " ", "const ", " *", true);
+ }
+ // Returns {field<val: -1, field==val: 0, field>val: +1}.
+ code_.SetValue("KEY_TYPE", type);
+ code_ += " int KeyCompareWithValue({{KEY_TYPE}} val) const {";
+ code_ +=
+ " return static_cast<int>({{FIELD_NAME}}() > val) - "
+ "static_cast<int>({{FIELD_NAME}}() < val);";
+ code_ += " }";
+ }
+ }
+
+
+ // Generate an accessor struct, builder structs & function for a table.
+ void GenTable(const StructDef &struct_def) {
+ // Generate an accessor struct, with methods of the form:
+ // type name() const { return GetField<type>(offset, defaultval); }
+ GenComment(struct_def.doc_comment);
+
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_ += "template <typename Iter>";
+ code_ +=
+ "struct {{STRUCT_NAME}} FLATBUFFERS_FINAL_CLASS"
+ " : private yandex::maps::flatbuffers_iter::Table<Iter> {";
+
+ // Generate field id constants.
+ if (struct_def.fields.vec.size() > 0) {
+ // We need to add a trailing comma to all elements except the last one as
+ // older versions of gcc complain about this.
+ code_.SetValue("SEP", "");
+ code_ += " enum {";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+
+ code_.SetValue("OFFSET_NAME", GenFieldOffsetName(field));
+ code_.SetValue("OFFSET_VALUE", NumToString(field.value.offset));
+ code_ += "{{SEP}} {{OFFSET_NAME}} = {{OFFSET_VALUE}}\\";
+ code_.SetValue("SEP", ",\n");
+ }
+ code_ += "";
+ code_ += " };";
+ }
+
+ code_ += "";
+ code_ += " using yandex::maps::flatbuffers_iter::Table<Iter>::Table;";
+
+ // Generate the accessors.
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) {
+ // Deprecated fields won't be accessible.
+ continue;
+ }
+
+ const bool is_struct = IsStruct(field.value.type);
+ const bool is_scalar = IsScalar(field.value.type.base_type);
+ code_.SetValue("FIELD_NAME", Name(field));
+
+ // Call a different accessor for pointers, that indirects.
+ std::string accessor = "";
+ if (is_scalar) {
+ accessor = "this->template GetField<";
+ } else if (is_struct) {
+ accessor = "this->template GetStruct<";
+ } else {
+ accessor = "this->template GetPointer<";
+ }
+ auto offset_str = GenFieldOffsetName(field);
+ auto offset_type =
+ GenTypeGet(field.value.type, "", "", "", false);
+
+ auto call = accessor + offset_type + ">(" + offset_str;
+ // Default value as second arg for non-pointer types.
+ if (is_scalar) { call += ", " + GenDefaultConstant(field); }
+ call += ")";
+
+ GenComment(field.doc_comment, " ");
+ code_.SetValue("FIELD_TYPE",
+ GenTypeGet(field.value.type, " ", "std::optional<", "> ", true));
+ code_.SetValue("FIELD_VALUE", GenUnderlyingCast(field, true, call));
+
+ code_ += " {{FIELD_TYPE}}{{FIELD_NAME}}() const {";
+ code_ += " return {{FIELD_VALUE}};";
+ code_ += " }";
+
+ // Generate a comparison function for this field if it is a key.
+ if (field.key) {
+ GenKeyFieldMethods(field);
+ }
+ }
+
+ // Generate a verifier function that can check a buffer from an untrusted
+ // source will never cause reads outside the buffer.
+ code_ += " bool Verify(yandex::maps::flatbuffers_iter::Verifier<Iter> &verifier) const {";
+ code_ += " return this->VerifyTableStart(verifier)\\";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) { continue; }
+ GenVerifyCall(field, " &&\n ");
+ }
+
+ code_ += " &&\n verifier.EndTable();";
+ code_ += " }";
+
+ code_ += "};"; // End of table.
+ code_ += "";
+ }
+
+ // Set up the correct namespace. Only open a namespace if the existing one is
+ // different (closing/opening only what is necessary).
+ //
+ // The file must start and end with an empty (or null) namespace so that
+ // namespaces are properly opened and closed.
+ void SetNameSpace(const Namespace *ns) {
+ if (cur_name_space_ == ns) { return; }
+
+ // Compute the size of the longest common namespace prefix.
+ // If cur_name_space is A::B::C::D and ns is A::B::E::F::G,
+ // the common prefix is A::B:: and we have old_size = 4, new_size = 5
+ // and common_prefix_size = 2
+ size_t old_size = cur_name_space_ ? cur_name_space_->components.size() : 0;
+ size_t new_size = ns ? ns->components.size() : 0;
+
+ size_t common_prefix_size = 0;
+ while (common_prefix_size < old_size && common_prefix_size < new_size &&
+ ns->components[common_prefix_size] ==
+ cur_name_space_->components[common_prefix_size]) {
+ common_prefix_size++;
+ }
+
+ // Close cur_name_space in reverse order to reach the common prefix.
+ // In the previous example, D then C are closed.
+ if (old_size > 0)
+ code_ += "} // namespace iter";
+ for (size_t j = old_size; j > common_prefix_size; --j) {
+ code_ += "} // namespace " + cur_name_space_->components[j - 1];
+ }
+ if (old_size != common_prefix_size) { code_ += ""; }
+
+ // open namespace parts to reach the ns namespace
+ // in the previous example, E, then F, then G are opened
+ for (auto j = common_prefix_size; j != new_size; ++j) {
+ code_ += "namespace " + ns->components[j] + " {";
+ }
+ if (new_size > 0)
+ code_ += "namespace iter {";
+ if (new_size != common_prefix_size) { code_ += ""; }
+
+ cur_name_space_ = ns;
+ }
+};
+
+} // namespace cpp_yandex_maps_iter
+
+bool GenerateCPPYandexMapsIter(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ cpp_yandex_maps_iter::CppIterGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_csharp.cpp b/contrib/libs/flatbuffers/src/idl_gen_csharp.cpp
new file mode 100644
index 0000000000..681ab6d642
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_csharp.cpp
@@ -0,0 +1,2100 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+#if defined(FLATBUFFERS_CPP98_STL)
+# include <cctype>
+#endif // defined(FLATBUFFERS_CPP98_STL)
+
+namespace flatbuffers {
+
+static TypedFloatConstantGenerator CSharpFloatGen("Double.", "Single.", "NaN",
+ "PositiveInfinity",
+ "NegativeInfinity");
+static CommentConfig comment_config = {
+ nullptr,
+ "///",
+ nullptr,
+};
+
+namespace csharp {
+class CSharpGenerator : public BaseGenerator {
+ struct FieldArrayLength {
+ std::string name;
+ int length;
+ };
+
+ public:
+ CSharpGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", ".", "cs"),
+ cur_name_space_(nullptr) {}
+
+ CSharpGenerator &operator=(const CSharpGenerator &);
+
+ bool generate() {
+ std::string one_file_code;
+ cur_name_space_ = parser_.current_namespace_;
+
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ std::string enumcode;
+ auto &enum_def = **it;
+ if (!parser_.opts.one_file) cur_name_space_ = enum_def.defined_namespace;
+ GenEnum(enum_def, &enumcode, parser_.opts);
+ if (parser_.opts.one_file) {
+ one_file_code += enumcode;
+ } else {
+ if (!SaveType(enum_def.name, *enum_def.defined_namespace, enumcode,
+ false))
+ return false;
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ std::string declcode;
+ auto &struct_def = **it;
+ if (!parser_.opts.one_file)
+ cur_name_space_ = struct_def.defined_namespace;
+ GenStruct(struct_def, &declcode, parser_.opts);
+ if (parser_.opts.one_file) {
+ one_file_code += declcode;
+ } else {
+ if (!SaveType(struct_def.name, *struct_def.defined_namespace, declcode,
+ true))
+ return false;
+ }
+ }
+
+ if (parser_.opts.one_file) {
+ return SaveType(file_name_, *parser_.current_namespace_, one_file_code,
+ true);
+ }
+ return true;
+ }
+
+ // Save out the generated code for a single class while adding
+ // declaration boilerplate.
+ bool SaveType(const std::string &defname, const Namespace &ns,
+ const std::string &classcode, bool needs_includes) const {
+ if (!classcode.length()) return true;
+
+ std::string code =
+ "// <auto-generated>\n"
+ "// " +
+ std::string(FlatBuffersGeneratedWarning()) +
+ "\n"
+ "// </auto-generated>\n\n";
+
+ std::string namespace_name = FullNamespace(".", ns);
+ if (!namespace_name.empty()) {
+ code += "namespace " + namespace_name + "\n{\n\n";
+ }
+ if (needs_includes) {
+ code += "using global::System;\n";
+ code += "using global::System.Collections.Generic;\n";
+ code += "using global::FlatBuffers;\n\n";
+ }
+ code += classcode;
+ if (!namespace_name.empty()) { code += "\n}\n"; }
+ auto filename = NamespaceDir(ns) + defname + ".cs";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+ std::string GenTypeBasic(const Type &type, bool enableLangOverrides) const {
+ // clang-format off
+ static const char * const csharp_typename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, GTYPE, NTYPE, ...) \
+ #NTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+
+ if (enableLangOverrides) {
+ if (IsEnum(type)) return WrapInNameSpace(*type.enum_def);
+ if (type.base_type == BASE_TYPE_STRUCT) {
+ return "Offset<" + WrapInNameSpace(*type.struct_def) + ">";
+ }
+ }
+
+ return csharp_typename[type.base_type];
+ }
+
+ inline std::string GenTypeBasic(const Type &type) const {
+ return GenTypeBasic(type, true);
+ }
+
+ std::string GenTypePointer(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "string";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return WrapInNameSpace(*type.struct_def);
+ case BASE_TYPE_UNION: return "TTable";
+ default: return "Table";
+ }
+ }
+
+ std::string GenTypeGet(const Type &type) const {
+ return IsScalar(type.base_type)
+ ? GenTypeBasic(type)
+ : (IsArray(type) ? GenTypeGet(type.VectorType())
+ : GenTypePointer(type));
+ }
+
+ std::string GenOffsetType(const StructDef &struct_def) const {
+ return "Offset<" + WrapInNameSpace(struct_def) + ">";
+ }
+
+ std::string GenOffsetConstruct(const StructDef &struct_def,
+ const std::string &variable_name) const {
+ return "new Offset<" + WrapInNameSpace(struct_def) + ">(" + variable_name +
+ ")";
+ }
+
+ // Casts necessary to correctly read serialized data
+ std::string DestinationCast(const Type &type) const {
+ if (IsSeries(type)) {
+ return DestinationCast(type.VectorType());
+ } else {
+ if (IsEnum(type)) return "(" + WrapInNameSpace(*type.enum_def) + ")";
+ }
+ return "";
+ }
+
+ // Cast statements for mutator method parameters.
+ // In Java, parameters representing unsigned numbers need to be cast down to
+ // their respective type. For example, a long holding an unsigned int value
+ // would be cast down to int before being put onto the buffer. In C#, one cast
+ // directly cast an Enum to its underlying type, which is essential before
+ // putting it onto the buffer.
+ std::string SourceCast(const Type &type) const {
+ if (IsSeries(type)) {
+ return SourceCast(type.VectorType());
+ } else {
+ if (IsEnum(type)) return "(" + GenTypeBasic(type, false) + ")";
+ }
+ return "";
+ }
+
+ std::string SourceCastBasic(const Type &type) const {
+ return IsScalar(type.base_type) ? SourceCast(type) : "";
+ }
+
+ std::string GenEnumDefaultValue(const FieldDef &field) const {
+ auto &value = field.value;
+ FLATBUFFERS_ASSERT(value.type.enum_def);
+ auto &enum_def = *value.type.enum_def;
+ auto enum_val = enum_def.FindByValue(value.constant);
+ return enum_val ? (WrapInNameSpace(enum_def) + "." + enum_val->name)
+ : value.constant;
+ }
+
+ std::string GenDefaultValue(const FieldDef &field,
+ bool enableLangOverrides) const {
+ // If it is an optional scalar field, the default is null
+ if (field.IsScalarOptional()) { return "null"; }
+
+ auto &value = field.value;
+ if (enableLangOverrides) {
+ // handles both enum case and vector of enum case
+ if (value.type.enum_def != nullptr &&
+ value.type.base_type != BASE_TYPE_UNION) {
+ return GenEnumDefaultValue(field);
+ }
+ }
+
+ auto longSuffix = "";
+ switch (value.type.base_type) {
+ case BASE_TYPE_BOOL: return value.constant == "0" ? "false" : "true";
+ case BASE_TYPE_ULONG: return value.constant;
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_LONG: return value.constant + longSuffix;
+ default:
+ if (IsFloat(value.type.base_type))
+ return CSharpFloatGen.GenFloatConstant(field);
+ else
+ return value.constant;
+ }
+ }
+
+ std::string GenDefaultValue(const FieldDef &field) const {
+ return GenDefaultValue(field, true);
+ }
+
+ std::string GenDefaultValueBasic(const FieldDef &field,
+ bool enableLangOverrides) const {
+ auto &value = field.value;
+ if (!IsScalar(value.type.base_type)) {
+ if (enableLangOverrides) {
+ switch (value.type.base_type) {
+ case BASE_TYPE_STRING: return "default(StringOffset)";
+ case BASE_TYPE_STRUCT:
+ return "default(Offset<" + WrapInNameSpace(*value.type.struct_def) +
+ ">)";
+ case BASE_TYPE_VECTOR: return "default(VectorOffset)";
+ default: break;
+ }
+ }
+ return "0";
+ }
+ return GenDefaultValue(field, enableLangOverrides);
+ }
+
+ std::string GenDefaultValueBasic(const FieldDef &field) const {
+ return GenDefaultValueBasic(field, true);
+ }
+
+ void GenEnum(EnumDef &enum_def, std::string *code_ptr,
+ const IDLOptions &opts) const {
+ std::string &code = *code_ptr;
+ if (enum_def.generated) return;
+
+ // Generate enum definitions of the form:
+ // public static (final) int name = value;
+ // In Java, we use ints rather than the Enum feature, because we want them
+ // to map directly to how they're used in C/C++ and file formats.
+ // That, and Java Enums are expensive, and not universally liked.
+ GenComment(enum_def.doc_comment, code_ptr, &comment_config);
+
+ if (opts.cs_gen_json_serializer && opts.generate_object_based_api) {
+ code +=
+ "[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters."
+ "StringEnumConverter))]\n";
+ }
+ // In C# this indicates enumeration values can be treated as bit flags.
+ if (enum_def.attributes.Lookup("bit_flags")) {
+ code += "[System.FlagsAttribute]\n";
+ }
+ if (enum_def.attributes.Lookup("private")) {
+ code += "internal ";
+ } else {
+ code += "public ";
+ }
+ code += "enum " + enum_def.name;
+ code += " : " + GenTypeBasic(enum_def.underlying_type, false);
+ code += "\n{\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, &comment_config, " ");
+ code += " ";
+ code += ev.name + " = ";
+ code += enum_def.ToString(ev);
+ code += ",\n";
+ }
+ // Close the class
+ code += "};\n\n";
+
+ if (opts.generate_object_based_api) {
+ GenEnum_ObjectAPI(enum_def, code_ptr, opts);
+ }
+ }
+
+ bool HasUnionStringValue(const EnumDef &enum_def) const {
+ if (!enum_def.is_union) return false;
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &val = **it;
+ if (IsString(val.union_type)) { return true; }
+ }
+ return false;
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetter(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "__p.__string";
+ case BASE_TYPE_STRUCT: return "__p.__struct";
+ case BASE_TYPE_UNION: return "__p.__union";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ case BASE_TYPE_ARRAY: return GenGetter(type.VectorType());
+ default: {
+ std::string getter = "__p.bb.Get";
+ if (type.base_type == BASE_TYPE_BOOL) {
+ getter = "0!=" + getter;
+ } else if (GenTypeBasic(type, false) != "byte") {
+ getter += MakeCamel(GenTypeBasic(type, false));
+ }
+ return getter;
+ }
+ }
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetterForLookupByKey(flatbuffers::FieldDef *key_field,
+ const std::string &data_buffer,
+ const char *num = nullptr) const {
+ auto type = key_field->value.type;
+ auto dest_mask = "";
+ auto dest_cast = DestinationCast(type);
+ auto getter = data_buffer + ".Get";
+ if (GenTypeBasic(type, false) != "byte") {
+ getter += MakeCamel(GenTypeBasic(type, false));
+ }
+ getter = dest_cast + getter + "(" + GenOffsetGetter(key_field, num) + ")" +
+ dest_mask;
+ return getter;
+ }
+
+ // Direct mutation is only allowed for scalar fields.
+ // Hence a setter method will only be generated for such fields.
+ std::string GenSetter(const Type &type) const {
+ if (IsScalar(type.base_type)) {
+ std::string setter = "__p.bb.Put";
+ if (GenTypeBasic(type, false) != "byte" &&
+ type.base_type != BASE_TYPE_BOOL) {
+ setter += MakeCamel(GenTypeBasic(type, false));
+ }
+ return setter;
+ } else {
+ return "";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const Type &type) const {
+ return IsScalar(type.base_type) ? MakeCamel(GenTypeBasic(type, false))
+ : (IsStruct(type) ? "Struct" : "Offset");
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void GenStructArgs(const StructDef &struct_def, std::string *code_ptr,
+ const char *nameprefix, size_t array_count = 0) const {
+ std::string &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ const auto array_field = IsArray(field_type);
+ const auto &type = array_field ? field_type.VectorType() : field_type;
+ const auto array_cnt = array_field ? (array_count + 1) : array_count;
+ if (IsStruct(type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ GenStructArgs(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), array_cnt);
+ } else {
+ code += ", ";
+ code += GenTypeBasic(type);
+ if (field.IsScalarOptional()) { code += "?"; }
+ if (array_cnt > 0) {
+ code += "[";
+ for (size_t i = 1; i < array_cnt; i++) code += ",";
+ code += "]";
+ }
+ code += " ";
+ code += nameprefix;
+ code += MakeCamel(field.name, true);
+ }
+ }
+ }
+
+ // Recusively generate struct construction statements of the form:
+ // builder.putType(name);
+ // and insert manual padding.
+ void GenStructBody(const StructDef &struct_def, std::string *code_ptr,
+ const char *nameprefix, size_t index = 0,
+ bool in_array = false) const {
+ std::string &code = *code_ptr;
+ std::string indent((index + 1) * 2, ' ');
+ code += indent + " builder.Prep(";
+ code += NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ");\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ if (field.padding) {
+ code += indent + " builder.Pad(";
+ code += NumToString(field.padding) + ");\n";
+ }
+ if (IsStruct(field_type)) {
+ GenStructBody(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), index,
+ in_array);
+ } else {
+ const auto &type =
+ IsArray(field_type) ? field_type.VectorType() : field_type;
+ const auto index_var = "_idx" + NumToString(index);
+ if (IsArray(field_type)) {
+ code += indent + " for (int " + index_var + " = ";
+ code += NumToString(field_type.fixed_length);
+ code += "; " + index_var + " > 0; " + index_var + "--) {\n";
+ in_array = true;
+ }
+ if (IsStruct(type)) {
+ GenStructBody(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), index + 1,
+ in_array);
+ } else {
+ code += IsArray(field_type) ? " " : "";
+ code += indent + " builder.Put";
+ code += GenMethod(type) + "(";
+ code += SourceCast(type);
+ auto argname = nameprefix + MakeCamel(field.name, true);
+ code += argname;
+ size_t array_cnt = index + (IsArray(field_type) ? 1 : 0);
+ if (array_cnt > 0) {
+ code += "[";
+ for (size_t i = 0; in_array && i < array_cnt; i++) {
+ code += "_idx" + NumToString(i) + "-1";
+ if (i != (array_cnt - 1)) code += ",";
+ }
+ code += "]";
+ }
+ code += ");\n";
+ }
+ if (IsArray(field_type)) { code += indent + " }\n"; }
+ }
+ }
+ }
+ std::string GenOffsetGetter(flatbuffers::FieldDef *key_field,
+ const char *num = nullptr) const {
+ std::string key_offset =
+ "Table.__offset(" + NumToString(key_field->value.offset) + ", ";
+ if (num) {
+ key_offset += num;
+ key_offset += ".Value, builder.DataBuffer)";
+ } else {
+ key_offset += "bb.Length";
+ key_offset += " - tableOffset, bb)";
+ }
+ return key_offset;
+ }
+
+ std::string GenLookupKeyGetter(flatbuffers::FieldDef *key_field) const {
+ std::string key_getter = " ";
+ key_getter += "int tableOffset = Table.";
+ key_getter += "__indirect(vectorLocation + 4 * (start + middle)";
+ key_getter += ", bb);\n ";
+ if (IsString(key_field->value.type)) {
+ key_getter += "int comp = Table.";
+ key_getter += "CompareStrings(";
+ key_getter += GenOffsetGetter(key_field);
+ key_getter += ", byteKey, bb);\n";
+ } else {
+ auto get_val = GenGetterForLookupByKey(key_field, "bb");
+ key_getter += "int comp = " + get_val + ".CompareTo(key);\n";
+ }
+ return key_getter;
+ }
+
+ std::string GenKeyGetter(flatbuffers::FieldDef *key_field) const {
+ std::string key_getter = "";
+ auto data_buffer = "builder.DataBuffer";
+ if (IsString(key_field->value.type)) {
+ key_getter += "Table.CompareStrings(";
+ key_getter += GenOffsetGetter(key_field, "o1") + ", ";
+ key_getter += GenOffsetGetter(key_field, "o2") + ", " + data_buffer + ")";
+ } else {
+ auto field_getter = GenGetterForLookupByKey(key_field, data_buffer, "o1");
+ key_getter += field_getter;
+ field_getter = GenGetterForLookupByKey(key_field, data_buffer, "o2");
+ key_getter += ".CompareTo(" + field_getter + ")";
+ }
+ return key_getter;
+ }
+
+ void GenStruct(StructDef &struct_def, std::string *code_ptr,
+ const IDLOptions &opts) const {
+ if (struct_def.generated) return;
+ std::string &code = *code_ptr;
+
+ // Generate a struct accessor class, with methods of the form:
+ // public type name() { return bb.getType(i + offset); }
+ // or for tables of the form:
+ // public type name() {
+ // int o = __offset(offset); return o != 0 ? bb.getType(o + i) : default;
+ // }
+ GenComment(struct_def.doc_comment, code_ptr, &comment_config);
+ if (struct_def.attributes.Lookup("private")) {
+ code += "internal ";
+ } else {
+ code += "public ";
+ }
+ if (struct_def.attributes.Lookup("csharp_partial")) {
+ // generate a partial class for this C# struct/table
+ code += "partial ";
+ }
+ code += "struct " + struct_def.name;
+ code += " : IFlatbufferObject";
+ code += "\n{\n";
+ code += " private ";
+ code += struct_def.fixed ? "Struct" : "Table";
+ code += " __p;\n";
+
+ code += " public ByteBuffer ByteBuffer { get { return __p.bb; } }\n";
+
+ if (!struct_def.fixed) {
+ // Generate verson check method.
+ // Force compile time error if not using the same version runtime.
+ code += " public static void ValidateVersion() {";
+ code += " FlatBufferConstants.";
+ code += "FLATBUFFERS_2_0_0(); ";
+ code += "}\n";
+
+ // Generate a special accessor for the table that when used as the root
+ // of a FlatBuffer
+ std::string method_name = "GetRootAs" + struct_def.name;
+ std::string method_signature =
+ " public static " + struct_def.name + " " + method_name;
+
+ // create convenience method that doesn't require an existing object
+ code += method_signature + "(ByteBuffer _bb) ";
+ code += "{ return " + method_name + "(_bb, new " + struct_def.name +
+ "()); }\n";
+
+ // create method that allows object reuse
+ code +=
+ method_signature + "(ByteBuffer _bb, " + struct_def.name + " obj) { ";
+ code += "return (obj.__assign(_bb.GetInt(_bb.Position";
+ code += ") + _bb.Position";
+ code += ", _bb)); }\n";
+ if (parser_.root_struct_def_ == &struct_def) {
+ if (parser_.file_identifier_.length()) {
+ // Check if a buffer has the identifier.
+ code += " public static ";
+ code += "bool " + struct_def.name;
+ code += "BufferHasIdentifier(ByteBuffer _bb) { return ";
+ code += "Table.__has_identifier(_bb, \"";
+ code += parser_.file_identifier_;
+ code += "\"); }\n";
+ }
+ }
+ }
+ // Generate the __init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ code += " public void __init(int _i, ByteBuffer _bb) ";
+ code += "{ ";
+ code += "__p = new ";
+ code += struct_def.fixed ? "Struct" : "Table";
+ code += "(_i, _bb); ";
+ code += "}\n";
+ code +=
+ " public " + struct_def.name + " __assign(int _i, ByteBuffer _bb) ";
+ code += "{ __init(_i, _bb); return this; }\n\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ GenComment(field.doc_comment, code_ptr, &comment_config, " ");
+ std::string type_name = GenTypeGet(field.value.type);
+ std::string type_name_dest = GenTypeGet(field.value.type);
+ std::string conditional_cast = "";
+ std::string optional = "";
+ if (!struct_def.fixed &&
+ (field.value.type.base_type == BASE_TYPE_STRUCT ||
+ field.value.type.base_type == BASE_TYPE_UNION ||
+ (IsVector(field.value.type) &&
+ (field.value.type.element == BASE_TYPE_STRUCT ||
+ field.value.type.element == BASE_TYPE_UNION)))) {
+ optional = "?";
+ conditional_cast = "(" + type_name_dest + optional + ")";
+ }
+ if (field.IsScalarOptional()) { optional = "?"; }
+ std::string dest_mask = "";
+ std::string dest_cast = DestinationCast(field.value.type);
+ std::string src_cast = SourceCast(field.value.type);
+ std::string field_name_camel = MakeCamel(field.name, true);
+ std::string method_start =
+ " public " + type_name_dest + optional + " " + field_name_camel;
+ std::string obj = "(new " + type_name + "())";
+
+ // Most field accessors need to retrieve and test the field offset first,
+ // this is the prefix code for that:
+ auto offset_prefix =
+ IsArray(field.value.type)
+ ? " { return "
+ : (" { int o = __p.__offset(" + NumToString(field.value.offset) +
+ "); return o != 0 ? ");
+ // Generate the accessors that don't do object reuse.
+ if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ } else if (IsVector(field.value.type) &&
+ field.value.type.element == BASE_TYPE_STRUCT) {
+ } else if (field.value.type.base_type == BASE_TYPE_UNION ||
+ (IsVector(field.value.type) &&
+ field.value.type.VectorType().base_type == BASE_TYPE_UNION)) {
+ method_start += "<TTable>";
+ type_name = type_name_dest;
+ }
+ std::string getter = dest_cast + GenGetter(field.value.type);
+ code += method_start;
+ std::string default_cast = "";
+ // only create default casts for c# scalars or vectors of scalars
+ if ((IsScalar(field.value.type.base_type) ||
+ (IsVector(field.value.type) &&
+ IsScalar(field.value.type.element)))) {
+ // For scalars, default value will be returned by GetDefaultValue().
+ // If the scalar is an enum, GetDefaultValue() returns an actual c# enum
+ // that doesn't need to be casted. However, default values for enum
+ // elements of vectors are integer literals ("0") and are still casted
+ // for clarity.
+ // If the scalar is optional and enum, we still need the cast.
+ if ((field.value.type.enum_def == nullptr ||
+ IsVector(field.value.type)) ||
+ (IsEnum(field.value.type) && field.IsScalarOptional())) {
+ default_cast = "(" + type_name_dest + optional + ")";
+ }
+ }
+ std::string member_suffix = "; ";
+ if (IsScalar(field.value.type.base_type)) {
+ code += " { get";
+ member_suffix += "} ";
+ if (struct_def.fixed) {
+ code += " { return " + getter;
+ code += "(__p.bb_pos + ";
+ code += NumToString(field.value.offset) + ")";
+ code += dest_mask;
+ } else {
+ code += offset_prefix + getter;
+ code += "(o + __p.bb_pos)" + dest_mask;
+ code += " : " + default_cast;
+ code += GenDefaultValue(field);
+ }
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ code += " { get";
+ member_suffix += "} ";
+ if (struct_def.fixed) {
+ code += " { return " + obj + ".__assign(" + "__p.";
+ code += "bb_pos + " + NumToString(field.value.offset) + ", ";
+ code += "__p.bb)";
+ } else {
+ code += offset_prefix + conditional_cast;
+ code += obj + ".__assign(";
+ code += field.value.type.struct_def->fixed
+ ? "o + __p.bb_pos"
+ : "__p.__indirect(o + __p.bb_pos)";
+ code += ", __p.bb) : null";
+ }
+ break;
+ case BASE_TYPE_STRING:
+ code += " { get";
+ member_suffix += "} ";
+ code += offset_prefix + getter + "(o + " + "__p.";
+ code += "bb_pos) : null";
+ break;
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_UNION) {
+ conditional_cast = "(TTable?)";
+ getter += "<TTable>";
+ }
+ code += "(";
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ getter = obj + ".__assign";
+ } else if (vectortype.base_type == BASE_TYPE_UNION) {
+ }
+ code += "int j)";
+ const auto body = offset_prefix + conditional_cast + getter + "(";
+ if (vectortype.base_type == BASE_TYPE_UNION) {
+ code += " where TTable : struct, IFlatbufferObject" + body;
+ } else {
+ code += body;
+ }
+ std::string index = "__p.";
+ if (IsArray(field.value.type)) {
+ index += "bb_pos + " + NumToString(field.value.offset) + " + ";
+ } else {
+ index += "__vector(o) + ";
+ }
+ index += "j * " + NumToString(InlineSize(vectortype));
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += vectortype.struct_def->fixed
+ ? index
+ : "__p.__indirect(" + index + ")";
+ code += ", __p.bb";
+ } else {
+ code += index;
+ }
+ code += ")" + dest_mask;
+ if (!IsArray(field.value.type)) {
+ code += " : ";
+ code +=
+ field.value.type.element == BASE_TYPE_BOOL
+ ? "false"
+ : (IsScalar(field.value.type.element) ? default_cast + "0"
+ : "null");
+ }
+ if (vectortype.base_type == BASE_TYPE_UNION &&
+ HasUnionStringValue(*vectortype.enum_def)) {
+ code += member_suffix;
+ code += "}\n";
+ code += " public string " + MakeCamel(field.name, true) +
+ "AsString(int j)";
+ code += offset_prefix + GenGetter(Type(BASE_TYPE_STRING));
+ code += "(" + index + ") : null";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION:
+ code += "() where TTable : struct, IFlatbufferObject";
+ code += offset_prefix + "(TTable?)" + getter;
+ code += "<TTable>(o + __p.bb_pos) : null";
+ if (HasUnionStringValue(*field.value.type.enum_def)) {
+ code += member_suffix;
+ code += "}\n";
+ code += " public string " + MakeCamel(field.name, true) +
+ "AsString()";
+ code += offset_prefix + GenGetter(Type(BASE_TYPE_STRING));
+ code += "(o + __p.bb_pos) : null";
+ }
+ // As<> accesors for Unions
+ // Loop through all the possible union types and generate an As
+ // accessor that casts to the correct type.
+ for (auto uit = field.value.type.enum_def->Vals().begin();
+ uit != field.value.type.enum_def->Vals().end(); ++uit) {
+ auto val = *uit;
+ if (val->union_type.base_type == BASE_TYPE_NONE) { continue; }
+ auto union_field_type_name = GenTypeGet(val->union_type);
+ code += member_suffix + "}\n";
+ if (val->union_type.base_type == BASE_TYPE_STRUCT &&
+ val->union_type.struct_def->attributes.Lookup("private")) {
+ code += " internal ";
+ } else {
+ code += " public ";
+ }
+ code += union_field_type_name + " ";
+ code += field_name_camel + "As" + val->name + "() { return ";
+ code += field_name_camel;
+ if (IsString(val->union_type)) {
+ code += "AsString()";
+ } else {
+ code += "<" + union_field_type_name + ">().Value";
+ }
+ }
+ break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ code += member_suffix;
+ code += "}\n";
+ if (IsVector(field.value.type)) {
+ code += " public int " + MakeCamel(field.name, true);
+ code += "Length";
+ code += " { get";
+ code += offset_prefix;
+ code += "__p.__vector_len(o) : 0; ";
+ code += "} ";
+ code += "}\n";
+ // See if we should generate a by-key accessor.
+ if (field.value.type.element == BASE_TYPE_STRUCT &&
+ !field.value.type.struct_def->fixed) {
+ auto &sd = *field.value.type.struct_def;
+ auto &fields = sd.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &key_field = **kit;
+ if (key_field.key) {
+ auto qualified_name = WrapInNameSpace(sd);
+ code += " public " + qualified_name + "? ";
+ code += MakeCamel(field.name, true) + "ByKey(";
+ code += GenTypeGet(key_field.value.type) + " key)";
+ code += offset_prefix;
+ code += qualified_name + ".__lookup_by_key(";
+ code += "__p.__vector(o), key, ";
+ code += "__p.bb) : null; ";
+ code += "}\n";
+ break;
+ }
+ }
+ }
+ }
+ // Generate a ByteBuffer accessor for strings & vectors of scalars.
+ if ((IsVector(field.value.type) &&
+ IsScalar(field.value.type.VectorType().base_type)) ||
+ IsString(field.value.type)) {
+ code += "#if ENABLE_SPAN_T\n";
+ code += " public Span<" + GenTypeBasic(field.value.type.VectorType()) +
+ "> Get";
+ code += MakeCamel(field.name, true);
+ code += "Bytes() { return ";
+ code += "__p.__vector_as_span<" +
+ GenTypeBasic(field.value.type.VectorType()) + ">(";
+ code += NumToString(field.value.offset);
+ code +=
+ ", " + NumToString(SizeOf(field.value.type.VectorType().base_type));
+ code += "); }\n";
+ code += "#else\n";
+ code += " public ArraySegment<byte>? Get";
+ code += MakeCamel(field.name, true);
+ code += "Bytes() { return ";
+ code += "__p.__vector_as_arraysegment(";
+ code += NumToString(field.value.offset);
+ code += "); }\n";
+ code += "#endif\n";
+
+ // For direct blockcopying the data into a typed array
+ code += " public ";
+ code += GenTypeBasic(field.value.type.VectorType());
+ code += "[] Get";
+ code += MakeCamel(field.name, true);
+ code += "Array() { ";
+ if (IsEnum(field.value.type.VectorType())) {
+ // Since __vector_as_array does not work for enum types,
+ // fill array using an explicit loop.
+ code += "int o = __p.__offset(";
+ code += NumToString(field.value.offset);
+ code += "); if (o == 0) return null; int p = ";
+ code += "__p.__vector(o); int l = ";
+ code += "__p.__vector_len(o); ";
+ code += GenTypeBasic(field.value.type.VectorType());
+ code += "[] a = new ";
+ code += GenTypeBasic(field.value.type.VectorType());
+ code += "[l]; for (int i = 0; i < l; i++) { a[i] = " + getter;
+ code += "(p + i * ";
+ code += NumToString(InlineSize(field.value.type.VectorType()));
+ code += "); } return a;";
+ } else {
+ code += "return ";
+ code += "__p.__vector_as_array<";
+ code += GenTypeBasic(field.value.type.VectorType());
+ code += ">(";
+ code += NumToString(field.value.offset);
+ code += ");";
+ }
+ code += " }\n";
+ }
+ // generate object accessors if is nested_flatbuffer
+ if (field.nested_flatbuffer) {
+ auto nested_type_name = WrapInNameSpace(*field.nested_flatbuffer);
+ auto nested_method_name =
+ MakeCamel(field.name, true) + "As" + field.nested_flatbuffer->name;
+ auto get_nested_method_name = nested_method_name;
+ get_nested_method_name = "Get" + nested_method_name;
+ conditional_cast = "(" + nested_type_name + "?)";
+ obj = "(new " + nested_type_name + "())";
+ code += " public " + nested_type_name + "? ";
+ code += get_nested_method_name + "(";
+ code += ") { int o = __p.__offset(";
+ code += NumToString(field.value.offset) + "); ";
+ code += "return o != 0 ? " + conditional_cast + obj + ".__assign(";
+ code += "__p.";
+ code += "__indirect(__p.__vector(o)), ";
+ code += "__p.bb) : null; }\n";
+ }
+ // Generate mutators for scalar fields or vectors of scalars.
+ if (parser_.opts.mutable_buffer) {
+ auto is_series = (IsSeries(field.value.type));
+ const auto &underlying_type =
+ is_series ? field.value.type.VectorType() : field.value.type;
+ // Boolean parameters have to be explicitly converted to byte
+ // representation.
+ auto setter_parameter = underlying_type.base_type == BASE_TYPE_BOOL
+ ? "(byte)(" + field.name + " ? 1 : 0)"
+ : field.name;
+ auto mutator_prefix = MakeCamel("mutate", true);
+ // A vector mutator also needs the index of the vector element it should
+ // mutate.
+ auto mutator_params = (is_series ? "(int j, " : "(") +
+ GenTypeGet(underlying_type) + " " + field.name +
+ ") { ";
+ auto setter_index =
+ is_series
+ ? "__p." +
+ (IsArray(field.value.type)
+ ? "bb_pos + " + NumToString(field.value.offset)
+ : "__vector(o)") +
+ +" + j * " + NumToString(InlineSize(underlying_type))
+ : (struct_def.fixed
+ ? "__p.bb_pos + " + NumToString(field.value.offset)
+ : "o + __p.bb_pos");
+ if (IsScalar(underlying_type.base_type) && !IsUnion(field.value.type)) {
+ code += " public ";
+ code += struct_def.fixed ? "void " : "bool ";
+ code += mutator_prefix + MakeCamel(field.name, true);
+ code += mutator_params;
+ if (struct_def.fixed) {
+ code += GenSetter(underlying_type) + "(" + setter_index + ", ";
+ code += src_cast + setter_parameter + "); }\n";
+ } else {
+ code += "int o = __p.__offset(";
+ code += NumToString(field.value.offset) + ");";
+ code += " if (o != 0) { " + GenSetter(underlying_type);
+ code += "(" + setter_index + ", " + src_cast + setter_parameter +
+ "); return true; } else { return false; } }\n";
+ }
+ }
+ }
+ if (parser_.opts.java_primitive_has_method &&
+ IsScalar(field.value.type.base_type) && !struct_def.fixed) {
+ auto vt_offset_constant = " public static final int VT_" +
+ MakeScreamingCamel(field.name) + " = " +
+ NumToString(field.value.offset) + ";";
+
+ code += vt_offset_constant;
+ code += "\n";
+ }
+ }
+ code += "\n";
+ auto struct_has_create = false;
+ std::set<flatbuffers::FieldDef *> field_has_create_set;
+ flatbuffers::FieldDef *key_field = nullptr;
+ if (struct_def.fixed) {
+ struct_has_create = true;
+ // create a struct constructor function
+ code += " public static " + GenOffsetType(struct_def) + " ";
+ code += "Create";
+ code += struct_def.name + "(FlatBufferBuilder builder";
+ GenStructArgs(struct_def, code_ptr, "");
+ code += ") {\n";
+ GenStructBody(struct_def, code_ptr, "");
+ code += " return ";
+ code += GenOffsetConstruct(struct_def, "builder.Offset");
+ code += ";\n }\n";
+ } else {
+ // Generate a method that creates a table in one go. This is only possible
+ // when the table has no struct fields, since those have to be created
+ // inline, and there's no way to do so in Java.
+ bool has_no_struct_fields = true;
+ int num_fields = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (IsStruct(field.value.type)) {
+ has_no_struct_fields = false;
+ } else {
+ num_fields++;
+ }
+ }
+ // JVM specifications restrict default constructor params to be < 255.
+ // Longs and doubles take up 2 units, so we set the limit to be < 127.
+ if ((has_no_struct_fields || opts.generate_object_based_api) &&
+ num_fields && num_fields < 127) {
+ struct_has_create = true;
+ // Generate a table constructor of the form:
+ // public static int createName(FlatBufferBuilder builder, args...)
+ code += " public static " + GenOffsetType(struct_def) + " ";
+ code += "Create" + struct_def.name;
+ code += "(FlatBufferBuilder builder";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ code += ",\n ";
+ if (IsStruct(field.value.type) && opts.generate_object_based_api) {
+ code += WrapInNameSpace(
+ field.value.type.struct_def->defined_namespace,
+ GenTypeName_ObjectAPI(field.value.type.struct_def->name, opts));
+ code += " ";
+ code += field.name;
+ code += " = null";
+ } else {
+ code += GenTypeBasic(field.value.type);
+ if (field.IsScalarOptional()) { code += "?"; }
+ code += " ";
+ code += field.name;
+ if (!IsScalar(field.value.type.base_type)) code += "Offset";
+
+ code += " = ";
+ code += GenDefaultValueBasic(field);
+ }
+ }
+ code += ") {\n builder.";
+ code += "StartTable(";
+ code += NumToString(struct_def.fields.vec.size()) + ");\n";
+ for (size_t size = struct_def.sortbysize ? sizeof(largest_scalar_t) : 1;
+ size; size /= 2) {
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated &&
+ (!struct_def.sortbysize ||
+ size == SizeOf(field.value.type.base_type))) {
+ code += " " + struct_def.name + ".";
+ code += "Add";
+ code += MakeCamel(field.name) + "(builder, ";
+ if (IsStruct(field.value.type) &&
+ opts.generate_object_based_api) {
+ code += GenTypePointer(field.value.type) + ".Pack(builder, " +
+ field.name + ")";
+ } else {
+ code += field.name;
+ if (!IsScalar(field.value.type.base_type)) code += "Offset";
+ }
+
+ code += ");\n";
+ }
+ }
+ }
+ code += " return " + struct_def.name + ".";
+ code += "End" + struct_def.name;
+ code += "(builder);\n }\n\n";
+ }
+ // Generate a set of static methods that allow table construction,
+ // of the form:
+ // public static void addName(FlatBufferBuilder builder, short name)
+ // { builder.addShort(id, name, default); }
+ // Unlike the Create function, these always work.
+ code += " public static void Start";
+ code += struct_def.name;
+ code += "(FlatBufferBuilder builder) { builder.";
+ code += "StartTable(";
+ code += NumToString(struct_def.fields.vec.size()) + "); }\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.key) key_field = &field;
+ code += " public static void Add";
+ code += MakeCamel(field.name);
+ code += "(FlatBufferBuilder builder, ";
+ code += GenTypeBasic(field.value.type);
+ auto argname = MakeCamel(field.name, false);
+ if (!IsScalar(field.value.type.base_type)) argname += "Offset";
+ if (field.IsScalarOptional()) { code += "?"; }
+ code += " " + argname + ") { builder.Add";
+ code += GenMethod(field.value.type) + "(";
+ code += NumToString(it - struct_def.fields.vec.begin()) + ", ";
+ code += SourceCastBasic(field.value.type);
+ code += argname;
+ if (!IsScalar(field.value.type.base_type) &&
+ field.value.type.base_type != BASE_TYPE_UNION) {
+ code += ".Value";
+ }
+ if (!field.IsScalarOptional()) {
+ // When the scalar is optional, use the builder method that doesn't
+ // supply a default value. Otherwise, we to continue to use the
+ // default value method.
+ code += ", ";
+ code += GenDefaultValue(field, false);
+ }
+ code += "); }\n";
+ if (IsVector(field.value.type)) {
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ if (!IsStruct(vector_type)) {
+ field_has_create_set.insert(&field);
+ code += " public static VectorOffset ";
+ code += "Create";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, ";
+ code += GenTypeBasic(vector_type) + "[] data) ";
+ code += "{ builder.StartVector(";
+ code += NumToString(elem_size);
+ code += ", data.Length, ";
+ code += NumToString(alignment);
+ code += "); for (int i = data.";
+ code += "Length - 1; i >= 0; i--) builder.";
+ code += "Add";
+ code += GenMethod(vector_type);
+ code += "(";
+ code += SourceCastBasic(vector_type);
+ code += "data[i]";
+ if (vector_type.base_type == BASE_TYPE_STRUCT ||
+ IsString(vector_type))
+ code += ".Value";
+ code += "); return ";
+ code += "builder.EndVector(); }\n";
+
+ code += " public static VectorOffset ";
+ code += "Create";
+ code += MakeCamel(field.name);
+ code += "VectorBlock(FlatBufferBuilder builder, ";
+ code += GenTypeBasic(vector_type) + "[] data) ";
+ code += "{ builder.StartVector(";
+ code += NumToString(elem_size);
+ code += ", data.Length, ";
+ code += NumToString(alignment);
+ code += "); builder.Add(data); return builder.EndVector(); }\n";
+ }
+ // Generate a method to start a vector, data to be added manually
+ // after.
+ code += " public static void Start";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, int numElems) ";
+ code += "{ builder.StartVector(";
+ code += NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment);
+ code += "); }\n";
+ }
+ }
+ code += " public static " + GenOffsetType(struct_def) + " ";
+ code += "End" + struct_def.name;
+ code += "(FlatBufferBuilder builder) {\n int o = builder.";
+ code += "EndTable();\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code += " builder.Required(o, ";
+ code += NumToString(field.value.offset);
+ code += "); // " + field.name + "\n";
+ }
+ }
+ code += " return " + GenOffsetConstruct(struct_def, "o") + ";\n }\n";
+ if (parser_.root_struct_def_ == &struct_def) {
+ std::string size_prefix[] = { "", "SizePrefixed" };
+ for (int i = 0; i < 2; ++i) {
+ code += " public static void ";
+ code += "Finish" + size_prefix[i] + struct_def.name;
+ code +=
+ "Buffer(FlatBufferBuilder builder, " + GenOffsetType(struct_def);
+ code += " offset) {";
+ code += " builder.Finish" + size_prefix[i] + "(offset";
+ code += ".Value";
+
+ if (parser_.file_identifier_.length())
+ code += ", \"" + parser_.file_identifier_ + "\"";
+ code += "); }\n";
+ }
+ }
+ }
+ // Only generate key compare function for table,
+ // because `key_field` is not set for struct
+ if (struct_def.has_key && !struct_def.fixed) {
+ FLATBUFFERS_ASSERT(key_field);
+ code += "\n public static VectorOffset ";
+ code += "CreateSortedVectorOf" + struct_def.name;
+ code += "(FlatBufferBuilder builder, ";
+ code += "Offset<" + struct_def.name + ">";
+ code += "[] offsets) {\n";
+ code += " Array.Sort(offsets, (Offset<" + struct_def.name +
+ "> o1, Offset<" + struct_def.name + "> o2) => " +
+ GenKeyGetter(key_field);
+ code += ");\n";
+ code += " return builder.CreateVectorOfTables(offsets);\n }\n";
+
+ code += "\n public static " + struct_def.name + "?";
+ code += " __lookup_by_key(";
+ code += "int vectorLocation, ";
+ code += GenTypeGet(key_field->value.type);
+ code += " key, ByteBuffer bb) {\n";
+ if (IsString(key_field->value.type)) {
+ code += " byte[] byteKey = ";
+ code += "System.Text.Encoding.UTF8.GetBytes(key);\n";
+ }
+ code += " int span = ";
+ code += "bb.GetInt(vectorLocation - 4);\n";
+ code += " int start = 0;\n";
+ code += " while (span != 0) {\n";
+ code += " int middle = span / 2;\n";
+ code += GenLookupKeyGetter(key_field);
+ code += " if (comp > 0) {\n";
+ code += " span = middle;\n";
+ code += " } else if (comp < 0) {\n";
+ code += " middle++;\n";
+ code += " start += middle;\n";
+ code += " span -= middle;\n";
+ code += " } else {\n";
+ code += " return ";
+ code += "new " + struct_def.name + "()";
+ code += ".__assign(tableOffset, bb);\n";
+ code += " }\n }\n";
+ code += " return null;\n";
+ code += " }\n";
+ }
+
+ if (opts.generate_object_based_api) {
+ GenPackUnPack_ObjectAPI(struct_def, code_ptr, opts, struct_has_create,
+ field_has_create_set);
+ }
+ code += "};\n\n";
+
+ if (opts.generate_object_based_api) {
+ GenStruct_ObjectAPI(struct_def, code_ptr, opts);
+ }
+ }
+
+ void GenVectorAccessObject(StructDef &struct_def,
+ std::string *code_ptr) const {
+ auto &code = *code_ptr;
+ // Generate a vector of structs accessor class.
+ code += "\n";
+ code += " ";
+ if (!struct_def.attributes.Lookup("private")) code += "public ";
+ code += "static struct Vector : BaseVector\n{\n";
+
+ // Generate the __assign method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ std::string method_indent = " ";
+ code += method_indent + "public Vector ";
+ code += "__assign(int _vector, int _element_size, ByteBuffer _bb) { ";
+ code += "__reset(_vector, _element_size, _bb); return this; }\n\n";
+
+ auto type_name = struct_def.name;
+ auto method_start = method_indent + "public " + type_name + " Get";
+ // Generate the accessors that don't do object reuse.
+ code += method_start + "(int j) { return Get";
+ code += "(new " + type_name + "(), j); }\n";
+ code += method_start + "(" + type_name + " obj, int j) { ";
+ code += " return obj.__assign(";
+ code += struct_def.fixed ? "__p.__element(j)"
+ : "__p.__indirect(__p.__element(j), bb)";
+ code += ", __p.bb); }\n";
+ // See if we should generate a by-key accessor.
+ if (!struct_def.fixed) {
+ auto &fields = struct_def.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &key_field = **kit;
+ if (key_field.key) {
+ auto nullable_annotation =
+ parser_.opts.gen_nullable ? "@Nullable " : "";
+ code += method_indent + nullable_annotation;
+ code += "public " + type_name + "? ";
+ code += "GetByKey(";
+ code += GenTypeGet(key_field.value.type) + " key) { ";
+ code += " return __lookup_by_key(null, ";
+ code += "__p.__vector(), key, ";
+ code += "__p.bb); ";
+ code += "}\n";
+ code += method_indent + nullable_annotation;
+ code += "public " + type_name + "?" + " ";
+ code += "GetByKey(";
+ code += type_name + "? obj, ";
+ code += GenTypeGet(key_field.value.type) + " key) { ";
+ code += " return __lookup_by_key(obj, ";
+ code += "__p.__vector(), key, ";
+ code += "__p.bb); ";
+ code += "}\n";
+ break;
+ }
+ }
+ }
+ code += " }\n";
+ }
+
+ void GenEnum_ObjectAPI(EnumDef &enum_def, std::string *code_ptr,
+ const IDLOptions &opts) const {
+ auto &code = *code_ptr;
+ if (enum_def.generated) return;
+ if (!enum_def.is_union) return;
+ if (enum_def.attributes.Lookup("private")) {
+ code += "internal ";
+ } else {
+ code += "public ";
+ }
+ auto union_name = enum_def.name + "Union";
+ code += "class " + union_name + " {\n";
+ // Type
+ code += " public " + enum_def.name + " Type { get; set; }\n";
+ // Value
+ code += " public object Value { get; set; }\n";
+ code += "\n";
+ // Constructor
+ code += " public " + union_name + "() {\n";
+ code += " this.Type = " + enum_def.name + "." +
+ enum_def.Vals()[0]->name + ";\n";
+ code += " this.Value = null;\n";
+ code += " }\n\n";
+ // As<T>
+ code += " public T As<T>() where T : class { return this.Value as T; }\n";
+ // As
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) continue;
+ auto type_name = GenTypeGet_ObjectAPI(ev.union_type, opts);
+ if (ev.union_type.base_type == BASE_TYPE_STRUCT &&
+ ev.union_type.struct_def->attributes.Lookup("private")) {
+ code += " internal ";
+ } else {
+ code += " public ";
+ }
+ code += type_name + " As" + ev.name + "() { return this.As<" + type_name +
+ ">(); }\n";
+ }
+ code += "\n";
+ // Pack()
+ code += " public static int Pack(FlatBuffers.FlatBufferBuilder builder, " +
+ union_name + " _o) {\n";
+ code += " switch (_o.Type) {\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) {
+ code += " default: return 0;\n";
+ } else {
+ code += " case " + enum_def.name + "." + ev.name + ": return ";
+ if (IsString(ev.union_type)) {
+ code += "builder.CreateString(_o.As" + ev.name + "()).Value;\n";
+ } else {
+ code += GenTypeGet(ev.union_type) + ".Pack(builder, _o.As" + ev.name +
+ "()).Value;\n";
+ }
+ }
+ }
+ code += " }\n";
+ code += " }\n";
+ code += "}\n\n";
+ // JsonConverter
+ if (opts.cs_gen_json_serializer) {
+ if (enum_def.attributes.Lookup("private")) {
+ code += "internal ";
+ } else {
+ code += "public ";
+ }
+ code += "class " + union_name +
+ "_JsonConverter : Newtonsoft.Json.JsonConverter {\n";
+ code += " public override bool CanConvert(System.Type objectType) {\n";
+ code += " return objectType == typeof(" + union_name +
+ ") || objectType == typeof(System.Collections.Generic.List<" +
+ union_name + ">);\n";
+ code += " }\n";
+ code +=
+ " public override void WriteJson(Newtonsoft.Json.JsonWriter writer, "
+ "object value, "
+ "Newtonsoft.Json.JsonSerializer serializer) {\n";
+ code += " var _olist = value as System.Collections.Generic.List<" +
+ union_name + ">;\n";
+ code += " if (_olist != null) {\n";
+ code += " writer.WriteStartArray();\n";
+ code +=
+ " foreach (var _o in _olist) { this.WriteJson(writer, _o, "
+ "serializer); }\n";
+ code += " writer.WriteEndArray();\n";
+ code += " } else {\n";
+ code += " this.WriteJson(writer, value as " + union_name +
+ ", serializer);\n";
+ code += " }\n";
+ code += " }\n";
+ code += " public void WriteJson(Newtonsoft.Json.JsonWriter writer, " +
+ union_name +
+ " _o, "
+ "Newtonsoft.Json.JsonSerializer serializer) {\n";
+ code += " if (_o == null) return;\n";
+ code += " serializer.Serialize(writer, _o.Value);\n";
+ code += " }\n";
+ code +=
+ " public override object ReadJson(Newtonsoft.Json.JsonReader "
+ "reader, "
+ "System.Type objectType, "
+ "object existingValue, Newtonsoft.Json.JsonSerializer serializer) "
+ "{\n";
+ code +=
+ " var _olist = existingValue as System.Collections.Generic.List<" +
+ union_name + ">;\n";
+ code += " if (_olist != null) {\n";
+ code += " for (var _j = 0; _j < _olist.Count; ++_j) {\n";
+ code += " reader.Read();\n";
+ code +=
+ " _olist[_j] = this.ReadJson(reader, _olist[_j], "
+ "serializer);\n";
+ code += " }\n";
+ code += " reader.Read();\n";
+ code += " return _olist;\n";
+ code += " } else {\n";
+ code += " return this.ReadJson(reader, existingValue as " +
+ union_name + ", serializer);\n";
+ code += " }\n";
+ code += " }\n";
+ code += " public " + union_name +
+ " ReadJson(Newtonsoft.Json.JsonReader reader, " + union_name +
+ " _o, Newtonsoft.Json.JsonSerializer serializer) {\n";
+ code += " if (_o == null) return null;\n";
+ code += " switch (_o.Type) {\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ auto &ev = **it;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) {
+ code += " default: break;\n";
+ } else {
+ auto type_name = GenTypeGet_ObjectAPI(ev.union_type, opts);
+ code += " case " + enum_def.name + "." + ev.name +
+ ": _o.Value = serializer.Deserialize<" + type_name +
+ ">(reader); break;\n";
+ }
+ }
+ code += " }\n";
+ code += " return _o;\n";
+ code += " }\n";
+ code += "}\n\n";
+ }
+ }
+
+ std::string GenTypeName_ObjectAPI(const std::string &name,
+ const IDLOptions &opts) const {
+ return opts.object_prefix + name + opts.object_suffix;
+ }
+
+ void GenUnionUnPack_ObjectAPI(const EnumDef &enum_def, std::string *code_ptr,
+ const std::string &camel_name,
+ bool is_vector) const {
+ auto &code = *code_ptr;
+ std::string varialbe_name = "_o." + camel_name;
+ std::string type_suffix = "";
+ std::string func_suffix = "()";
+ std::string indent = " ";
+ if (is_vector) {
+ varialbe_name = "_o_" + camel_name;
+ type_suffix = "(_j)";
+ func_suffix = "(_j)";
+ indent = " ";
+ }
+ if (is_vector) {
+ code += indent + "var " + varialbe_name + " = new ";
+ } else {
+ code += indent + varialbe_name + " = new ";
+ }
+ code += WrapInNameSpace(enum_def) + "Union();\n";
+ code += indent + varialbe_name + ".Type = this." + camel_name + "Type" +
+ type_suffix + ";\n";
+ code +=
+ indent + "switch (this." + camel_name + "Type" + type_suffix + ") {\n";
+ for (auto eit = enum_def.Vals().begin(); eit != enum_def.Vals().end();
+ ++eit) {
+ auto &ev = **eit;
+ if (ev.union_type.base_type == BASE_TYPE_NONE) {
+ code += indent + " default: break;\n";
+ } else {
+ code += indent + " case " + WrapInNameSpace(enum_def) + "." + ev.name +
+ ":\n";
+ code += indent + " " + varialbe_name + ".Value = this." + camel_name;
+ if (IsString(ev.union_type)) {
+ code += "AsString" + func_suffix + ";\n";
+ } else {
+ code += "<" + GenTypeGet(ev.union_type) + ">" + func_suffix;
+ code += ".HasValue ? this." + camel_name;
+ code += "<" + GenTypeGet(ev.union_type) + ">" + func_suffix +
+ ".Value.UnPack() : null;\n";
+ }
+ code += indent + " break;\n";
+ }
+ }
+ code += indent + "}\n";
+ if (is_vector) {
+ code += indent + "_o." + camel_name + ".Add(" + varialbe_name + ");\n";
+ }
+ }
+
+ void GenPackUnPack_ObjectAPI(
+ StructDef &struct_def, std::string *code_ptr, const IDLOptions &opts,
+ bool struct_has_create,
+ const std::set<FieldDef *> &field_has_create) const {
+ auto &code = *code_ptr;
+ auto struct_name = GenTypeName_ObjectAPI(struct_def.name, opts);
+ // UnPack()
+ code += " public " + struct_name + " UnPack() {\n";
+ code += " var _o = new " + struct_name + "();\n";
+ code += " this.UnPackTo(_o);\n";
+ code += " return _o;\n";
+ code += " }\n";
+ // UnPackTo()
+ code += " public void UnPackTo(" + struct_name + " _o) {\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto camel_name = MakeCamel(field.name);
+ auto start = " _o." + camel_name + " = ";
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ auto fixed = struct_def.fixed && field.value.type.struct_def->fixed;
+ if (fixed) {
+ code += start + "this." + camel_name + ".UnPack();\n";
+ } else {
+ code += start + "this." + camel_name + ".HasValue ? this." +
+ camel_name + ".Value.UnPack() : null;\n";
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ auto type_name = GenTypeGet_ObjectAPI(field.value.type, opts);
+ auto length_str = NumToString(field.value.type.fixed_length);
+ auto unpack_method = field.value.type.struct_def == nullptr
+ ? ""
+ : field.value.type.struct_def->fixed
+ ? ".UnPack()"
+ : "?.UnPack()";
+ code += start + "new " + type_name.substr(0, type_name.length() - 1) +
+ length_str + "];\n";
+ code += " for (var _j = 0; _j < " + length_str + "; ++_j) { _o." +
+ camel_name + "[_j] = this." + camel_name + "(_j)" +
+ unpack_method + "; }\n";
+ break;
+ }
+ case BASE_TYPE_VECTOR:
+ if (field.value.type.element == BASE_TYPE_UNION) {
+ code += start + "new " +
+ GenTypeGet_ObjectAPI(field.value.type, opts) + "();\n";
+ code += " for (var _j = 0; _j < this." + camel_name +
+ "Length; ++_j) {\n";
+ GenUnionUnPack_ObjectAPI(*field.value.type.enum_def, code_ptr,
+ camel_name, true);
+ code += " }\n";
+ } else if (field.value.type.element != BASE_TYPE_UTYPE) {
+ auto fixed = field.value.type.struct_def == nullptr;
+ code += start + "new " +
+ GenTypeGet_ObjectAPI(field.value.type, opts) + "();\n";
+ code += " for (var _j = 0; _j < this." + camel_name +
+ "Length; ++_j) {";
+ code += "_o." + camel_name + ".Add(";
+ if (fixed) {
+ code += "this." + camel_name + "(_j)";
+ } else {
+ code += "this." + camel_name + "(_j).HasValue ? this." +
+ camel_name + "(_j).Value.UnPack() : null";
+ }
+ code += ");}\n";
+ }
+ break;
+ case BASE_TYPE_UTYPE: break;
+ case BASE_TYPE_UNION: {
+ GenUnionUnPack_ObjectAPI(*field.value.type.enum_def, code_ptr,
+ camel_name, false);
+ break;
+ }
+ default: {
+ code += start + "this." + camel_name + ";\n";
+ break;
+ }
+ }
+ }
+ code += " }\n";
+ // Pack()
+ code += " public static " + GenOffsetType(struct_def) +
+ " Pack(FlatBufferBuilder builder, " + struct_name + " _o) {\n";
+ code += " if (_o == null) return default(" + GenOffsetType(struct_def) +
+ ");\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto camel_name = MakeCamel(field.name);
+ // pre
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ if (!field.value.type.struct_def->fixed) {
+ code += " var _" + field.name + " = _o." + camel_name +
+ " == null ? default(" +
+ GenOffsetType(*field.value.type.struct_def) +
+ ") : " + GenTypeGet(field.value.type) +
+ ".Pack(builder, _o." + camel_name + ");\n";
+ } else if (struct_def.fixed && struct_has_create) {
+ std::vector<FieldArrayLength> array_lengths;
+ FieldArrayLength tmp_array_length = {
+ field.name,
+ field.value.type.fixed_length,
+ };
+ array_lengths.push_back(tmp_array_length);
+ GenStructPackDecl_ObjectAPI(*field.value.type.struct_def, code_ptr,
+ array_lengths);
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ std::string create_string =
+ field.shared ? "CreateSharedString" : "CreateString";
+ code += " var _" + field.name + " = _o." + camel_name +
+ " == null ? default(StringOffset) : "
+ "builder." +
+ create_string + "(_o." + camel_name + ");\n";
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ if (field_has_create.find(&field) != field_has_create.end()) {
+ auto property_name = camel_name;
+ auto gen_for_loop = true;
+ std::string array_name = "__" + field.name;
+ std::string array_type = "";
+ std::string to_array = "";
+ switch (field.value.type.element) {
+ case BASE_TYPE_STRING: {
+ std::string create_string =
+ field.shared ? "CreateSharedString" : "CreateString";
+ array_type = "StringOffset";
+ to_array += "builder." + create_string + "(_o." +
+ property_name + "[_j])";
+ break;
+ }
+ case BASE_TYPE_STRUCT:
+ array_type = "Offset<" + GenTypeGet(field.value.type) + ">";
+ to_array = GenTypeGet(field.value.type) + ".Pack(builder, _o." +
+ property_name + "[_j])";
+ break;
+ case BASE_TYPE_UTYPE:
+ property_name = camel_name.substr(0, camel_name.size() - 4);
+ array_type = WrapInNameSpace(*field.value.type.enum_def);
+ to_array = "_o." + property_name + "[_j].Type";
+ break;
+ case BASE_TYPE_UNION:
+ array_type = "int";
+ to_array = WrapInNameSpace(*field.value.type.enum_def) +
+ "Union.Pack(builder, _o." + property_name + "[_j])";
+ break;
+ default: gen_for_loop = false; break;
+ }
+ code += " var _" + field.name + " = default(VectorOffset);\n";
+ code += " if (_o." + property_name + " != null) {\n";
+ if (gen_for_loop) {
+ code += " var " + array_name + " = new " + array_type +
+ "[_o." + property_name + ".Count];\n";
+ code += " for (var _j = 0; _j < " + array_name +
+ ".Length; ++_j) { ";
+ code += array_name + "[_j] = " + to_array + "; }\n";
+ } else {
+ code += " var " + array_name + " = _o." + property_name +
+ ".ToArray();\n";
+ }
+ code += " _" + field.name + " = Create" + camel_name +
+ "Vector(builder, " + array_name + ");\n";
+ code += " }\n";
+ } else {
+ auto pack_method =
+ field.value.type.struct_def == nullptr
+ ? "builder.Add" + GenMethod(field.value.type.VectorType()) +
+ "(_o." + camel_name + "[_j]);"
+ : GenTypeGet(field.value.type) + ".Pack(builder, _o." +
+ camel_name + "[_j]);";
+ code += " var _" + field.name + " = default(VectorOffset);\n";
+ code += " if (_o." + camel_name + " != null) {\n";
+ code += " Start" + camel_name + "Vector(builder, _o." +
+ camel_name + ".Count);\n";
+ code += " for (var _j = _o." + camel_name +
+ ".Count - 1; _j >= 0; --_j) { " + pack_method + " }\n";
+ code += " _" + field.name + " = builder.EndVector();\n";
+ code += " }\n";
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ if (field.value.type.struct_def != nullptr) {
+ std::vector<FieldArrayLength> array_lengths;
+ FieldArrayLength tmp_array_length = {
+ field.name,
+ field.value.type.fixed_length,
+ };
+ array_lengths.push_back(tmp_array_length);
+ GenStructPackDecl_ObjectAPI(*field.value.type.struct_def, code_ptr,
+ array_lengths);
+ } else {
+ code += " var _" + field.name + " = _o." + camel_name + ";\n";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ code += " var _" + field.name + "_type = _o." + camel_name +
+ " == null ? " + WrapInNameSpace(*field.value.type.enum_def) +
+ ".NONE : " + "_o." + camel_name + ".Type;\n";
+ code +=
+ " var _" + field.name + " = _o." + camel_name +
+ " == null ? 0 : " + GenTypeGet_ObjectAPI(field.value.type, opts) +
+ ".Pack(builder, _o." + camel_name + ");\n";
+ break;
+ }
+ default: break;
+ }
+ }
+ if (struct_has_create) {
+ // Create
+ code += " return Create" + struct_def.name + "(\n";
+ code += " builder";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto camel_name = MakeCamel(field.name);
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ if (struct_def.fixed) {
+ GenStructPackCall_ObjectAPI(*field.value.type.struct_def,
+ code_ptr,
+ " _" + field.name + "_");
+ } else {
+ code += ",\n";
+ if (field.value.type.struct_def->fixed) {
+ if (opts.generate_object_based_api)
+ code += " _o." + camel_name;
+ else
+ code += " " + GenTypeGet(field.value.type) +
+ ".Pack(builder, _o." + camel_name + ")";
+ } else {
+ code += " _" + field.name;
+ }
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ if (field.value.type.struct_def != nullptr) {
+ GenStructPackCall_ObjectAPI(*field.value.type.struct_def,
+ code_ptr,
+ " _" + field.name + "_");
+ } else {
+ code += ",\n";
+ code += " _" + field.name;
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_UTYPE: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_STRING: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ code += ",\n";
+ code += " _" + field.name;
+ break;
+ }
+ default: // scalar
+ code += ",\n";
+ code += " _o." + camel_name;
+ break;
+ }
+ }
+ code += ");\n";
+ } else {
+ // Start, End
+ code += " Start" + struct_def.name + "(builder);\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto camel_name = MakeCamel(field.name);
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ if (field.value.type.struct_def->fixed) {
+ code += " Add" + camel_name + "(builder, " +
+ GenTypeGet(field.value.type) + ".Pack(builder, _o." +
+ camel_name + "));\n";
+ } else {
+ code +=
+ " Add" + camel_name + "(builder, _" + field.name + ");\n";
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ code +=
+ " Add" + camel_name + "(builder, _" + field.name + ");\n";
+ break;
+ }
+ case BASE_TYPE_UTYPE: break;
+ case BASE_TYPE_UNION: {
+ code += " Add" + camel_name + "Type(builder, _" + field.name +
+ "_type);\n";
+ code +=
+ " Add" + camel_name + "(builder, _" + field.name + ");\n";
+ break;
+ }
+ // scalar
+ default: {
+ code +=
+ " Add" + camel_name + "(builder, _o." + camel_name + ");\n";
+ break;
+ }
+ }
+ }
+ code += " return End" + struct_def.name + "(builder);\n";
+ }
+ code += " }\n";
+ }
+
+ void GenStructPackDecl_ObjectAPI(
+ const StructDef &struct_def, std::string *code_ptr,
+ std::vector<FieldArrayLength> &array_lengths) const {
+ auto &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ auto is_array = IsArray(field.value.type);
+ const auto &field_type =
+ is_array ? field.value.type.VectorType() : field.value.type;
+ FieldArrayLength tmp_array_length = {
+ field.name,
+ field_type.fixed_length,
+ };
+ array_lengths.push_back(tmp_array_length);
+ if (field_type.struct_def != nullptr) {
+ GenStructPackDecl_ObjectAPI(*field_type.struct_def, code_ptr,
+ array_lengths);
+ } else {
+ std::vector<FieldArrayLength> array_only_lengths;
+ for (size_t i = 0; i < array_lengths.size(); ++i) {
+ if (array_lengths[i].length > 0) {
+ array_only_lengths.push_back(array_lengths[i]);
+ }
+ }
+ std::string name;
+ for (size_t i = 0; i < array_lengths.size(); ++i) {
+ name += "_" + array_lengths[i].name;
+ }
+ code += " var " + name + " = ";
+ if (array_only_lengths.size() > 0) {
+ code += "new " + GenTypeBasic(field_type) + "[";
+ for (size_t i = 0; i < array_only_lengths.size(); ++i) {
+ if (i != 0) { code += ","; }
+ code += NumToString(array_only_lengths[i].length);
+ }
+ code += "];\n";
+ code += " ";
+ // initialize array
+ for (size_t i = 0; i < array_only_lengths.size(); ++i) {
+ auto idx = "idx" + NumToString(i);
+ code += "for (var " + idx + " = 0; " + idx + " < " +
+ NumToString(array_only_lengths[i].length) + "; ++" + idx +
+ ") {";
+ }
+ for (size_t i = 0; i < array_only_lengths.size(); ++i) {
+ auto idx = "idx" + NumToString(i);
+ if (i == 0) {
+ code += name + "[" + idx;
+ } else {
+ code += "," + idx;
+ }
+ }
+ code += "] = _o";
+ for (size_t i = 0, j = 0; i < array_lengths.size(); ++i) {
+ code += "." + MakeCamel(array_lengths[i].name);
+ if (array_lengths[i].length <= 0) continue;
+ code += "[idx" + NumToString(j++) + "]";
+ }
+ code += ";";
+ for (size_t i = 0; i < array_only_lengths.size(); ++i) {
+ code += "}";
+ }
+ } else {
+ code += "_o";
+ for (size_t i = 0; i < array_lengths.size(); ++i) {
+ code += "." + MakeCamel(array_lengths[i].name);
+ }
+ code += ";";
+ }
+ code += "\n";
+ }
+ array_lengths.pop_back();
+ }
+ }
+
+ void GenStructPackCall_ObjectAPI(const StructDef &struct_def,
+ std::string *code_ptr,
+ std::string prefix) const {
+ auto &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ if (field_type.struct_def != nullptr) {
+ GenStructPackCall_ObjectAPI(*field_type.struct_def, code_ptr,
+ prefix + field.name + "_");
+ } else {
+ code += ",\n";
+ code += prefix + field.name;
+ }
+ }
+ }
+
+ std::string GenTypeGet_ObjectAPI(flatbuffers::Type type,
+ const IDLOptions &opts) const {
+ auto type_name = GenTypeGet(type);
+ // Replace to ObjectBaseAPI Type Name
+ switch (type.base_type) {
+ case BASE_TYPE_STRUCT: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ if (type.struct_def != nullptr) {
+ auto type_name_length = type.struct_def->name.length();
+ auto new_type_name =
+ GenTypeName_ObjectAPI(type.struct_def->name, opts);
+ type_name.replace(type_name.length() - type_name_length,
+ type_name_length, new_type_name);
+ } else if (type.element == BASE_TYPE_UNION) {
+ type_name = WrapInNameSpace(*type.enum_def) + "Union";
+ }
+ break;
+ }
+
+ case BASE_TYPE_UNION: {
+ type_name = WrapInNameSpace(*type.enum_def) + "Union";
+ break;
+ }
+ default: break;
+ }
+
+ switch (type.base_type) {
+ case BASE_TYPE_ARRAY: {
+ type_name = type_name + "[]";
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ type_name = "List<" + type_name + ">";
+ break;
+ }
+ default: break;
+ }
+ return type_name;
+ }
+
+ void GenStruct_ObjectAPI(StructDef &struct_def, std::string *code_ptr,
+ const IDLOptions &opts) const {
+ auto &code = *code_ptr;
+ if (struct_def.attributes.Lookup("private")) {
+ code += "internal ";
+ } else {
+ code += "public ";
+ }
+ if (struct_def.attributes.Lookup("csharp_partial")) {
+ // generate a partial class for this C# struct/table
+ code += "partial ";
+ }
+ auto class_name = GenTypeName_ObjectAPI(struct_def.name, opts);
+ code += "class " + class_name;
+ code += "\n{\n";
+ // Generate Properties
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) continue;
+ if (field.value.type.element == BASE_TYPE_UTYPE) continue;
+ auto type_name = GenTypeGet_ObjectAPI(field.value.type, opts);
+ if (field.IsScalarOptional()) type_name += "?";
+ auto camel_name = MakeCamel(field.name, true);
+ if (opts.cs_gen_json_serializer) {
+ if (IsUnion(field.value.type)) {
+ auto utype_name = WrapInNameSpace(*field.value.type.enum_def);
+ code +=
+ " [Newtonsoft.Json.JsonProperty(\"" + field.name + "_type\")]\n";
+ if (IsVector(field.value.type)) {
+ code += " private " + utype_name + "[] " + camel_name + "Type {\n";
+ code += " get {\n";
+ code += " if (this." + camel_name + " == null) return null;\n";
+ code += " var _o = new " + utype_name + "[this." + camel_name +
+ ".Count];\n";
+ code +=
+ " for (var _j = 0; _j < _o.Length; ++_j) { _o[_j] = "
+ "this." +
+ camel_name + "[_j].Type; }\n";
+ code += " return _o;\n";
+ code += " }\n";
+ code += " set {\n";
+ code += " this." + camel_name + " = new List<" + utype_name +
+ "Union>();\n";
+ code += " for (var _j = 0; _j < value.Length; ++_j) {\n";
+ code += " var _o = new " + utype_name + "Union();\n";
+ code += " _o.Type = value[_j];\n";
+ code += " this." + camel_name + ".Add(_o);\n";
+ code += " }\n";
+ code += " }\n";
+ code += " }\n";
+ } else {
+ code += " private " + utype_name + " " + camel_name + "Type {\n";
+ code += " get {\n";
+ code += " return this." + camel_name + " != null ? this." +
+ camel_name + ".Type : " + utype_name + ".NONE;\n";
+ code += " }\n";
+ code += " set {\n";
+ code += " this." + camel_name + " = new " + utype_name +
+ "Union();\n";
+ code += " this." + camel_name + ".Type = value;\n";
+ code += " }\n";
+ code += " }\n";
+ }
+ }
+ code += " [Newtonsoft.Json.JsonProperty(\"" + field.name + "\")]\n";
+ if (IsUnion(field.value.type)) {
+ auto union_name =
+ (IsVector(field.value.type))
+ ? GenTypeGet_ObjectAPI(field.value.type.VectorType(), opts)
+ : type_name;
+ code += " [Newtonsoft.Json.JsonConverter(typeof(" + union_name +
+ "_JsonConverter))]\n";
+ }
+ if (field.attributes.Lookup("hash")) {
+ code += " [Newtonsoft.Json.JsonIgnore()]\n";
+ }
+ }
+ code += " public " + type_name + " " + camel_name + " { get; set; }\n";
+ }
+ // Generate Constructor
+ code += "\n";
+ code += " public " + class_name + "() {\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) continue;
+ if (field.value.type.element == BASE_TYPE_UTYPE) continue;
+ code += " this." + MakeCamel(field.name) + " = ";
+ auto type_name = GenTypeGet_ObjectAPI(field.value.type, opts);
+ if (IsScalar(field.value.type.base_type)) {
+ code += GenDefaultValue(field) + ";\n";
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ if (IsStruct(field.value.type)) {
+ code += "new " + type_name + "();\n";
+ } else {
+ code += "null;\n";
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ code += "new " + type_name.substr(0, type_name.length() - 1) +
+ NumToString(field.value.type.fixed_length) + "];\n";
+ break;
+ }
+ default: {
+ code += "null;\n";
+ break;
+ }
+ }
+ }
+ }
+ code += " }\n";
+ // Generate Serialization
+ if (opts.cs_gen_json_serializer &&
+ parser_.root_struct_def_ == &struct_def) {
+ code += "\n";
+ code += " public static " + class_name +
+ " DeserializeFromJson(string jsonText) {\n";
+ code += " return Newtonsoft.Json.JsonConvert.DeserializeObject<" +
+ class_name + ">(jsonText);\n";
+ code += " }\n";
+ code += " public string SerializeToJson() {\n";
+ code +=
+ " return Newtonsoft.Json.JsonConvert.SerializeObject(this, "
+ "Newtonsoft.Json.Formatting.Indented);\n";
+ code += " }\n";
+ }
+ if (parser_.root_struct_def_ == &struct_def) {
+ code += " public static " + class_name +
+ " DeserializeFromBinary(byte[] fbBuffer) {\n";
+ code += " return " + struct_def.name + ".GetRootAs" + struct_def.name +
+ "(new ByteBuffer(fbBuffer)).UnPack();\n";
+ code += " }\n";
+ code += " public byte[] SerializeToBinary() {\n";
+ code += " var fbb = new FlatBufferBuilder(0x10000);\n";
+ code += " " + struct_def.name + ".Finish" + struct_def.name +
+ "Buffer(fbb, " + struct_def.name + ".Pack(fbb, this));\n";
+ code += " return fbb.DataBuffer.ToSizedArray();\n";
+ code += " }\n";
+ }
+ code += "}\n\n";
+ }
+
+ // This tracks the current namespace used to determine if a type need to be
+ // prefixed by its namespace
+ const Namespace *cur_name_space_;
+};
+} // namespace csharp
+
+bool GenerateCSharp(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ csharp::CSharpGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_dart.cpp b/contrib/libs/flatbuffers/src/idl_gen_dart.cpp
new file mode 100644
index 0000000000..56c4a82555
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_dart.cpp
@@ -0,0 +1,955 @@
+/*
+ * Copyright 2018 Dan Field
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+#include <cassert>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+namespace dart {
+
+const std::string _kFb = "fb";
+// see https://www.dartlang.org/guides/language/language-tour#keywords
+// yeild*, async*, and sync* shouldn't be problems anyway but keeping them in
+static const char *keywords[] = {
+ "abstract", "deferred", "if", "super", "as", "do",
+ "implements", "switch", "assert", "dynamic", "import", "sync*",
+ "async", "else", "in", "this", "async*", "enum",
+ "is", "throw", "await", "export", "library", "true",
+ "break", "external", "new", "try", "case", "extends",
+ "null", "typedef", "catch", "factory", "operator", "var",
+ "class", "false", "part", "void", "const", "final",
+ "rethrow", "while", "continue", "finally", "return", "with",
+ "covariant", "for", "set", "yield", "default", "get",
+ "static", "yield*"
+};
+
+// Iterate through all definitions we haven't generate code for (enums, structs,
+// and tables) and output them to a single file.
+class DartGenerator : public BaseGenerator {
+ public:
+ typedef std::map<std::string, std::string> namespace_code_map;
+
+ DartGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", ".", "dart") {}
+ // Iterate through all definitions we haven't generate code for (enums,
+ // structs, and tables) and output them to a single file.
+ bool generate() {
+ std::string code;
+ namespace_code_map namespace_code;
+ GenerateEnums(&namespace_code);
+ GenerateStructs(&namespace_code);
+
+ for (auto kv = namespace_code.begin(); kv != namespace_code.end(); ++kv) {
+ code.clear();
+ code = code + "// " + FlatBuffersGeneratedWarning() + "\n";
+ code = code +
+ "// ignore_for_file: unused_import, unused_field, "
+ "unused_local_variable\n\n";
+
+ if (!kv->first.empty()) { code += "library " + kv->first + ";\n\n"; }
+
+ code += "import 'dart:typed_data' show Uint8List;\n";
+ code += "import 'package:flat_buffers/flat_buffers.dart' as " + _kFb +
+ ";\n\n";
+
+ for (auto kv2 = namespace_code.begin(); kv2 != namespace_code.end();
+ ++kv2) {
+ if (kv2->first != kv->first) {
+ code +=
+ "import '" +
+ GeneratedFileName(
+ "./",
+ file_name_ + (!kv2->first.empty() ? "_" + kv2->first : ""),
+ parser_.opts) +
+ "' as " + ImportAliasName(kv2->first) + ";\n";
+ }
+ }
+ code += "\n";
+ code += kv->second;
+
+ if (!SaveFile(
+ GeneratedFileName(
+ path_,
+ file_name_ + (!kv->first.empty() ? "_" + kv->first : ""),
+ parser_.opts)
+ .c_str(),
+ code, false)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private:
+ static std::string ImportAliasName(const std::string &ns) {
+ std::string ret;
+ ret.assign(ns);
+ size_t pos = ret.find('.');
+ while (pos != std::string::npos) {
+ ret.replace(pos, 1, "_");
+ pos = ret.find('.', pos + 1);
+ }
+
+ return ret;
+ }
+
+ static std::string BuildNamespaceName(const Namespace &ns) {
+ if (ns.components.empty()) { return ""; }
+ std::stringstream sstream;
+ std::copy(ns.components.begin(), ns.components.end() - 1,
+ std::ostream_iterator<std::string>(sstream, "."));
+
+ auto ret = sstream.str() + ns.components.back();
+ for (size_t i = 0; i < ret.size(); i++) {
+ auto lower = CharToLower(ret[i]);
+ if (lower != ret[i]) {
+ ret[i] = lower;
+ if (i != 0 && ret[i - 1] != '.') {
+ ret.insert(i, "_");
+ i++;
+ }
+ }
+ }
+ // std::transform(ret.begin(), ret.end(), ret.begin(), CharToLower);
+ return ret;
+ }
+
+ void GenIncludeDependencies(std::string *code,
+ const std::string &the_namespace) {
+ for (auto it = parser_.included_files_.begin();
+ it != parser_.included_files_.end(); ++it) {
+ if (it->second.empty()) continue;
+
+ auto noext = flatbuffers::StripExtension(it->second);
+ auto basename = flatbuffers::StripPath(noext);
+
+ *code +=
+ "import '" +
+ GeneratedFileName(
+ "", basename + (the_namespace == "" ? "" : "_" + the_namespace),
+ parser_.opts) +
+ "';\n";
+ }
+ }
+
+ static std::string EscapeKeyword(const std::string &name) {
+ for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
+ if (name == keywords[i]) { return MakeCamel(name + "_", false); }
+ }
+
+ return MakeCamel(name, false);
+ }
+
+ void GenerateEnums(namespace_code_map *namespace_code) {
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ GenEnum(enum_def, namespace_code); // enum_code_ptr);
+ }
+ }
+
+ void GenerateStructs(namespace_code_map *namespace_code) {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ GenStruct(struct_def, namespace_code);
+ }
+ }
+
+ // Generate a documentation comment, if available.
+ static void GenDocComment(const std::vector<std::string> &dc,
+ std::string *code_ptr,
+ const std::string &extra_lines,
+ const char *indent = nullptr) {
+ if (dc.empty() && extra_lines.empty()) {
+ // Don't output empty comment blocks with 0 lines of comment content.
+ return;
+ }
+
+ auto &code = *code_ptr;
+
+ for (auto it = dc.begin(); it != dc.end(); ++it) {
+ if (indent) code += indent;
+ code += "/// " + *it + "\n";
+ }
+ if (!extra_lines.empty()) {
+ if (!dc.empty()) {
+ if (indent) code += indent;
+ code += "///\n";
+ }
+ if (indent) code += indent;
+ std::string::size_type start = 0;
+ for (;;) {
+ auto end = extra_lines.find('\n', start);
+ if (end != std::string::npos) {
+ code += "/// " + extra_lines.substr(start, end - start) + "\n";
+ start = end + 1;
+ } else {
+ code += "/// " + extra_lines.substr(start) + "\n";
+ break;
+ }
+ }
+ }
+ }
+
+ static void GenDocComment(std::string *code_ptr,
+ const std::string &extra_lines) {
+ GenDocComment(std::vector<std::string>(), code_ptr, extra_lines);
+ }
+
+ // Generate an enum declaration and an enum string lookup table.
+ void GenEnum(EnumDef &enum_def, namespace_code_map *namespace_code) {
+ if (enum_def.generated) return;
+ auto ns = BuildNamespaceName(*enum_def.defined_namespace);
+ std::string code;
+ GenDocComment(enum_def.doc_comment, &code, "");
+
+ auto name = enum_def.is_union ? enum_def.name + "TypeId" : enum_def.name;
+ auto is_bit_flags = enum_def.attributes.Lookup("bit_flags");
+
+ code += "class " + name + " {\n";
+ code += " final int value;\n";
+ code += " const " + name + "._(this.value);\n\n";
+ code += " factory " + name + ".fromValue(int value) {\n";
+ code += " if (value == null) value = 0;\n";
+
+ code += " if (!values.containsKey(value)) {\n";
+ code +=
+ " throw new StateError('Invalid value $value for bit flag enum ";
+ code += name + "');\n";
+ code += " }\n";
+
+ code += " return values[value];\n";
+ code += " }\n\n";
+
+ // this is meaningless for bit_flags
+ // however, note that unlike "regular" dart enums this enum can still have
+ // holes.
+ if (!is_bit_flags) {
+ code += " static const int minValue = " +
+ enum_def.ToString(*enum_def.MinValue()) + ";\n";
+ code += " static const int maxValue = " +
+ enum_def.ToString(*enum_def.MaxValue()) + ";\n";
+ }
+
+ code +=
+ " static bool containsValue(int value) =>"
+ " values.containsKey(value);\n\n";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+
+ if (!ev.doc_comment.empty()) {
+ if (it != enum_def.Vals().begin()) { code += '\n'; }
+ GenDocComment(ev.doc_comment, &code, "", " ");
+ }
+ code += " static const " + name + " " + ev.name + " = ";
+ code += "const " + name + "._(" + enum_def.ToString(ev) + ");\n";
+ }
+
+ code += " static const Map<int," + name + "> values = {";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ code += enum_def.ToString(ev) + ": " + ev.name + ",";
+ }
+ code += "};\n\n";
+
+ code += " static const " + _kFb + ".Reader<" + name +
+ "> reader = const _" + name + "Reader();\n\n";
+ code += " @override\n";
+ code += " String toString() {\n";
+ code += " return '" + name + "{value: $value}';\n";
+ code += " }\n";
+ code += "}\n\n";
+
+ GenEnumReader(enum_def, name, &code);
+ (*namespace_code)[ns] += code;
+ }
+
+ void GenEnumReader(EnumDef &enum_def, const std::string &name,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ code += "class _" + name + "Reader extends " + _kFb + ".Reader<" + name +
+ "> {\n";
+ code += " const _" + name + "Reader();\n\n";
+ code += " @override\n";
+ code += " int get size => 1;\n\n";
+ code += " @override\n";
+ code +=
+ " " + name + " read(" + _kFb + ".BufferContext bc, int offset) =>\n";
+ code += " new " + name + ".fromValue(const " + _kFb + "." +
+ GenType(enum_def.underlying_type) + "Reader().read(bc, offset));\n";
+ code += "}\n\n";
+ }
+
+ static std::string GenType(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_BOOL: return "Bool";
+ case BASE_TYPE_CHAR: return "Int8";
+ case BASE_TYPE_UTYPE:
+ case BASE_TYPE_UCHAR: return "Uint8";
+ case BASE_TYPE_SHORT: return "Int16";
+ case BASE_TYPE_USHORT: return "Uint16";
+ case BASE_TYPE_INT: return "Int32";
+ case BASE_TYPE_UINT: return "Uint32";
+ case BASE_TYPE_LONG: return "Int64";
+ case BASE_TYPE_ULONG: return "Uint64";
+ case BASE_TYPE_FLOAT: return "Float32";
+ case BASE_TYPE_DOUBLE: return "Float64";
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_VECTOR: return GenType(type.VectorType());
+ case BASE_TYPE_STRUCT: return type.struct_def->name;
+ case BASE_TYPE_UNION: return type.enum_def->name + "TypeId";
+ default: return "Table";
+ }
+ }
+
+ std::string GenReaderTypeName(const Type &type, Namespace *current_namespace,
+ const FieldDef &def,
+ bool parent_is_vector = false) {
+ if (type.base_type == BASE_TYPE_BOOL) {
+ return "const " + _kFb + ".BoolReader()";
+ } else if (IsVector(type)) {
+ return "const " + _kFb + ".ListReader<" +
+ GenDartTypeName(type.VectorType(), current_namespace, def) + ">(" +
+ GenReaderTypeName(type.VectorType(), current_namespace, def,
+ true) +
+ ")";
+ } else if (IsString(type)) {
+ return "const " + _kFb + ".StringReader()";
+ }
+ if (IsScalar(type.base_type)) {
+ if (type.enum_def && parent_is_vector) {
+ return GenDartTypeName(type, current_namespace, def) + ".reader";
+ }
+ return "const " + _kFb + "." + GenType(type) + "Reader()";
+ } else {
+ return GenDartTypeName(type, current_namespace, def) + ".reader";
+ }
+ }
+
+ std::string GenDartTypeName(const Type &type, Namespace *current_namespace,
+ const FieldDef &def, bool addBuilder = false) {
+ if (type.enum_def) {
+ if (type.enum_def->is_union && type.base_type != BASE_TYPE_UNION) {
+ return type.enum_def->name + "TypeId";
+ } else if (type.enum_def->is_union) {
+ return "dynamic";
+ } else if (type.base_type != BASE_TYPE_VECTOR) {
+ return type.enum_def->name;
+ }
+ }
+
+ switch (type.base_type) {
+ case BASE_TYPE_BOOL: return "bool";
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG:
+ case BASE_TYPE_INT:
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_SHORT:
+ case BASE_TYPE_USHORT:
+ case BASE_TYPE_CHAR:
+ case BASE_TYPE_UCHAR: return "int";
+ case BASE_TYPE_FLOAT:
+ case BASE_TYPE_DOUBLE: return "double";
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_STRUCT:
+ return MaybeWrapNamespace(
+ type.struct_def->name + (addBuilder ? "ObjectBuilder" : ""),
+ current_namespace, def);
+ case BASE_TYPE_VECTOR:
+ return "List<" +
+ GenDartTypeName(type.VectorType(), current_namespace, def,
+ addBuilder) +
+ ">";
+ default: assert(0); return "dynamic";
+ }
+ }
+
+ static const std::string MaybeWrapNamespace(const std::string &type_name,
+ Namespace *current_ns,
+ const FieldDef &field) {
+ auto curr_ns_str = BuildNamespaceName(*current_ns);
+ std::string field_ns_str = "";
+ if (field.value.type.struct_def) {
+ field_ns_str +=
+ BuildNamespaceName(*field.value.type.struct_def->defined_namespace);
+ } else if (field.value.type.enum_def) {
+ field_ns_str +=
+ BuildNamespaceName(*field.value.type.enum_def->defined_namespace);
+ }
+
+ if (field_ns_str != "" && field_ns_str != curr_ns_str) {
+ return ImportAliasName(field_ns_str) + "." + type_name;
+ } else {
+ return type_name;
+ }
+ }
+
+ // Generate an accessor struct with constructor for a flatbuffers struct.
+ void GenStruct(const StructDef &struct_def,
+ namespace_code_map *namespace_code) {
+ if (struct_def.generated) return;
+
+ auto object_namespace = BuildNamespaceName(*struct_def.defined_namespace);
+ std::string code;
+
+ const auto &object_name = struct_def.name;
+
+ // Emit constructor
+
+ GenDocComment(struct_def.doc_comment, &code, "");
+
+ auto reader_name = "_" + object_name + "Reader";
+ auto builder_name = object_name + "Builder";
+ auto object_builder_name = object_name + "ObjectBuilder";
+
+ std::string reader_code, builder_code;
+
+ code += "class " + object_name + " {\n";
+
+ code += " " + object_name + "._(this._bc, this._bcOffset);\n";
+ if (!struct_def.fixed) {
+ code += " factory " + object_name + "(List<int> bytes) {\n";
+ code += " " + _kFb + ".BufferContext rootRef = new " + _kFb +
+ ".BufferContext.fromBytes(bytes);\n";
+ code += " return reader.read(rootRef, 0);\n";
+ code += " }\n";
+ }
+
+ code += "\n";
+ code += " static const " + _kFb + ".Reader<" + object_name +
+ "> reader = const " + reader_name + "();\n\n";
+
+ code += " final " + _kFb + ".BufferContext _bc;\n";
+ code += " final int _bcOffset;\n\n";
+
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto offset = static_cast<int>(it - struct_def.fields.vec.begin());
+ non_deprecated_fields.push_back(std::make_pair(offset, &field));
+ }
+
+ GenImplementationGetters(struct_def, non_deprecated_fields, &code);
+
+ code += "}\n\n";
+
+ GenReader(struct_def, &reader_name, &reader_code);
+ GenBuilder(struct_def, non_deprecated_fields, &builder_name, &builder_code);
+ GenObjectBuilder(struct_def, non_deprecated_fields, &object_builder_name,
+ &builder_code);
+
+ code += reader_code;
+ code += builder_code;
+
+ (*namespace_code)[object_namespace] += code;
+ }
+
+ std::string NamespaceAliasFromUnionType(Namespace *root_namespace,
+ const Type &type) {
+ const std::vector<std::string> qualified_name_parts =
+ type.struct_def->defined_namespace->components;
+ if (std::equal(root_namespace->components.begin(),
+ root_namespace->components.end(),
+ qualified_name_parts.begin())) {
+ return type.struct_def->name;
+ }
+
+ std::string ns;
+
+ for (auto it = qualified_name_parts.begin();
+ it != qualified_name_parts.end(); ++it) {
+ auto &part = *it;
+
+ for (size_t i = 0; i < part.length(); i++) {
+ if (i && !isdigit(part[i]) && part[i] == CharToUpper(part[i])) {
+ ns += "_";
+ ns += CharToLower(part[i]);
+ } else {
+ ns += CharToLower(part[i]);
+ }
+ }
+ if (it != qualified_name_parts.end() - 1) { ns += "_"; }
+ }
+
+ return ns + "." + type.struct_def->name;
+ }
+
+ void GenImplementationGetters(
+ const StructDef &struct_def,
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ std::string field_name = MakeCamel(field.name, false);
+ std::string type_name = GenDartTypeName(
+ field.value.type, struct_def.defined_namespace, field, false);
+
+ GenDocComment(field.doc_comment, &code, "", " ");
+
+ code += " " + type_name + " get " + field_name;
+ if (field.value.type.base_type == BASE_TYPE_UNION) {
+ code += " {\n";
+ code += " switch (" + field_name + "Type?.value) {\n";
+ auto &enum_def = *field.value.type.enum_def;
+ for (auto en_it = enum_def.Vals().begin() + 1;
+ en_it != enum_def.Vals().end(); ++en_it) {
+ auto &ev = **en_it;
+
+ auto enum_name = NamespaceAliasFromUnionType(
+ enum_def.defined_namespace, ev.union_type);
+ code += " case " + enum_def.ToString(ev) + ": return " +
+ enum_name + ".reader.vTableGet(_bc, _bcOffset, " +
+ NumToString(field.value.offset) + ", null);\n";
+ }
+ code += " default: return null;\n";
+ code += " }\n";
+ code += " }\n";
+ } else {
+ code += " => ";
+ if (field.value.type.enum_def &&
+ field.value.type.base_type != BASE_TYPE_VECTOR) {
+ code += "new " +
+ GenDartTypeName(field.value.type,
+ struct_def.defined_namespace, field) +
+ ".fromValue(";
+ }
+
+ code += GenReaderTypeName(field.value.type,
+ struct_def.defined_namespace, field);
+ if (struct_def.fixed) {
+ code +=
+ ".read(_bc, _bcOffset + " + NumToString(field.value.offset) + ")";
+ } else {
+ code += ".vTableGet(_bc, _bcOffset, " +
+ NumToString(field.value.offset) + ", ";
+ if (!field.value.constant.empty() && field.value.constant != "0") {
+ if (IsBool(field.value.type.base_type)) {
+ code += "true";
+ } else if (field.value.constant == "nan" ||
+ field.value.constant == "+nan" ||
+ field.value.constant == "-nan") {
+ code += "double.nan";
+ } else if (field.value.constant == "inf" ||
+ field.value.constant == "+inf") {
+ code += "double.infinity";
+ } else if (field.value.constant == "-inf") {
+ code += "double.negativeInfinity";
+ } else {
+ code += field.value.constant;
+ }
+ } else {
+ if (IsBool(field.value.type.base_type)) {
+ code += "false";
+ } else if (IsScalar(field.value.type.base_type)) {
+ code += "0";
+ } else {
+ code += "null";
+ }
+ }
+ code += ")";
+ }
+ if (field.value.type.enum_def &&
+ field.value.type.base_type != BASE_TYPE_VECTOR) {
+ code += ")";
+ }
+ code += ";\n";
+ }
+ }
+
+ code += "\n";
+
+ code += " @override\n";
+ code += " String toString() {\n";
+ code += " return '" + struct_def.name + "{";
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+ code +=
+ MakeCamel(field.name, false) + ": $" + MakeCamel(field.name, false);
+ if (it != non_deprecated_fields.end() - 1) { code += ", "; }
+ }
+ code += "}';\n";
+ code += " }\n";
+ }
+
+ void GenReader(const StructDef &struct_def, std::string *reader_name_ptr,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto &reader_name = *reader_name_ptr;
+ auto &impl_name = struct_def.name;
+
+ code += "class " + reader_name + " extends " + _kFb;
+ if (struct_def.fixed) {
+ code += ".StructReader<";
+ } else {
+ code += ".TableReader<";
+ }
+ code += impl_name + "> {\n";
+ code += " const " + reader_name + "();\n\n";
+
+ if (struct_def.fixed) {
+ code += " @override\n";
+ code += " int get size => " + NumToString(struct_def.bytesize) + ";\n\n";
+ }
+ code += " @override\n";
+ code += " " + impl_name +
+ " createObject(fb.BufferContext bc, int offset) => \n new " +
+ impl_name + "._(bc, offset);\n";
+ code += "}\n\n";
+ }
+
+ void GenBuilder(const StructDef &struct_def,
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *builder_name_ptr, std::string *code_ptr) {
+ if (non_deprecated_fields.size() == 0) { return; }
+ auto &code = *code_ptr;
+ auto &builder_name = *builder_name_ptr;
+
+ code += "class " + builder_name + " {\n";
+ code += " " + builder_name + "(this.fbBuilder) {\n";
+ code += " assert(fbBuilder != null);\n";
+ code += " }\n\n";
+ code += " final " + _kFb + ".Builder fbBuilder;\n\n";
+
+ if (struct_def.fixed) {
+ StructBuilderBody(struct_def, non_deprecated_fields, code_ptr);
+ } else {
+ TableBuilderBody(struct_def, non_deprecated_fields, code_ptr);
+ }
+
+ code += "}\n\n";
+ }
+
+ void StructBuilderBody(
+ const StructDef &struct_def,
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ code += " int finish(";
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ if (IsStruct(field.value.type)) {
+ code += "fb.StructBuilder";
+ } else {
+ code += GenDartTypeName(field.value.type, struct_def.defined_namespace,
+ field);
+ }
+ code += " " + field.name;
+ if (it != non_deprecated_fields.end() - 1) { code += ", "; }
+ }
+ code += ") {\n";
+
+ for (auto it = non_deprecated_fields.rbegin();
+ it != non_deprecated_fields.rend(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ if (field.padding) {
+ code += " fbBuilder.pad(" + NumToString(field.padding) + ");\n";
+ }
+
+ if (IsStruct(field.value.type)) {
+ code += " " + field.name + "();\n";
+ } else {
+ code += " fbBuilder.put" + GenType(field.value.type) + "(";
+ code += field.name;
+ if (field.value.type.enum_def) { code += "?.value"; }
+ code += ");\n";
+ }
+ }
+ code += " return fbBuilder.offset;\n";
+ code += " }\n\n";
+ }
+
+ void TableBuilderBody(
+ const StructDef &struct_def,
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ code += " void begin() {\n";
+ code += " fbBuilder.startTable();\n";
+ code += " }\n\n";
+
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+ auto offset = pair.first;
+
+ if (IsScalar(field.value.type.base_type)) {
+ code += " int add" + MakeCamel(field.name) + "(";
+ code += GenDartTypeName(field.value.type, struct_def.defined_namespace,
+ field);
+ code += " " + MakeCamel(field.name, false) + ") {\n";
+ code += " fbBuilder.add" + GenType(field.value.type) + "(" +
+ NumToString(offset) + ", ";
+ code += MakeCamel(field.name, false);
+ if (field.value.type.enum_def) { code += "?.value"; }
+ code += ");\n";
+ } else if (IsStruct(field.value.type)) {
+ code += " int add" + MakeCamel(field.name) + "(int offset) {\n";
+ code +=
+ " fbBuilder.addStruct(" + NumToString(offset) + ", offset);\n";
+ } else {
+ code += " int add" + MakeCamel(field.name) + "Offset(int offset) {\n";
+ code +=
+ " fbBuilder.addOffset(" + NumToString(offset) + ", offset);\n";
+ }
+ code += " return fbBuilder.offset;\n";
+ code += " }\n";
+ }
+
+ code += "\n";
+ code += " int finish() {\n";
+ code += " return fbBuilder.endTable();\n";
+ code += " }\n";
+ }
+
+ void GenObjectBuilder(
+ const StructDef &struct_def,
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *builder_name_ptr, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto &builder_name = *builder_name_ptr;
+
+ code += "class " + builder_name + " extends " + _kFb + ".ObjectBuilder {\n";
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ code += " final " +
+ GenDartTypeName(field.value.type, struct_def.defined_namespace,
+ field, true) +
+ " _" + MakeCamel(field.name, false) + ";\n";
+ }
+ code += "\n";
+ code += " " + builder_name + "(";
+
+ if (non_deprecated_fields.size() != 0) {
+ code += "{\n";
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ code += " " +
+ GenDartTypeName(field.value.type, struct_def.defined_namespace,
+ field, true) +
+ " " + MakeCamel(field.name, false) + ",\n";
+ }
+ code += " })\n";
+ code += " : ";
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ code += "_" + MakeCamel(field.name, false) + " = " +
+ MakeCamel(field.name, false);
+ if (it == non_deprecated_fields.end() - 1) {
+ code += ";\n\n";
+ } else {
+ code += ",\n ";
+ }
+ }
+ } else {
+ code += ");\n\n";
+ }
+
+ code += " /// Finish building, and store into the [fbBuilder].\n";
+ code += " @override\n";
+ code += " int finish(\n";
+ code += " " + _kFb + ".Builder fbBuilder) {\n";
+ code += " assert(fbBuilder != null);\n";
+
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ if (IsScalar(field.value.type.base_type) || IsStruct(field.value.type))
+ continue;
+
+ code += " final int " + MakeCamel(field.name, false) + "Offset";
+ if (IsVector(field.value.type)) {
+ code +=
+ " = _" + MakeCamel(field.name, false) + "?.isNotEmpty == true\n";
+ code += " ? fbBuilder.writeList";
+ switch (field.value.type.VectorType().base_type) {
+ case BASE_TYPE_STRING:
+ code += "(_" + MakeCamel(field.name, false) +
+ ".map((b) => fbBuilder.writeString(b)).toList())";
+ break;
+ case BASE_TYPE_STRUCT:
+ if (field.value.type.struct_def->fixed) {
+ code += "OfStructs(_" + MakeCamel(field.name, false) + ")";
+ } else {
+ code += "(_" + MakeCamel(field.name, false) +
+ ".map((b) => b.getOrCreateOffset(fbBuilder)).toList())";
+ }
+ break;
+ default:
+ code += GenType(field.value.type.VectorType()) + "(_" +
+ MakeCamel(field.name, false);
+ if (field.value.type.enum_def) { code += ".map((f) => f.value)"; }
+ code += ")";
+ }
+ code += "\n : null;\n";
+ } else if (IsString(field.value.type)) {
+ code += " = fbBuilder.writeString(_" + MakeCamel(field.name, false) +
+ ");\n";
+ } else {
+ code += " = _" + MakeCamel(field.name, false) +
+ "?.getOrCreateOffset(fbBuilder);\n";
+ }
+ }
+
+ code += "\n";
+ if (struct_def.fixed) {
+ StructObjectBuilderBody(non_deprecated_fields, code_ptr);
+ } else {
+ TableObjectBuilderBody(non_deprecated_fields, code_ptr);
+ }
+ code += " }\n\n";
+
+ code += " /// Convenience method to serialize to byte list.\n";
+ code += " @override\n";
+ code += " Uint8List toBytes([String fileIdentifier]) {\n";
+ code += " " + _kFb + ".Builder fbBuilder = new ";
+ code += _kFb + ".Builder();\n";
+ code += " int offset = finish(fbBuilder);\n";
+ code += " return fbBuilder.finish(offset, fileIdentifier);\n";
+ code += " }\n";
+ code += "}\n";
+ }
+
+ void StructObjectBuilderBody(
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *code_ptr, bool prependUnderscore = true) {
+ auto &code = *code_ptr;
+
+ for (auto it = non_deprecated_fields.rbegin();
+ it != non_deprecated_fields.rend(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+
+ if (field.padding) {
+ code += " fbBuilder.pad(" + NumToString(field.padding) + ");\n";
+ }
+
+ if (IsStruct(field.value.type)) {
+ code += " ";
+ if (prependUnderscore) { code += "_"; }
+ code += field.name + ".finish(fbBuilder);\n";
+ } else {
+ code += " fbBuilder.put" + GenType(field.value.type) + "(";
+ if (prependUnderscore) { code += "_"; }
+ code += field.name;
+ if (field.value.type.enum_def) { code += "?.value"; }
+ code += ");\n";
+ }
+ }
+
+ code += " return fbBuilder.offset;\n";
+ }
+
+ void TableObjectBuilderBody(
+ std::vector<std::pair<int, FieldDef *>> non_deprecated_fields,
+ std::string *code_ptr, bool prependUnderscore = true) {
+ std::string &code = *code_ptr;
+ code += " fbBuilder.startTable();\n";
+
+ for (auto it = non_deprecated_fields.begin();
+ it != non_deprecated_fields.end(); ++it) {
+ auto pair = *it;
+ auto &field = *pair.second;
+ auto offset = pair.first;
+
+ if (IsScalar(field.value.type.base_type)) {
+ code += " fbBuilder.add" + GenType(field.value.type) + "(" +
+ NumToString(offset) + ", ";
+ if (prependUnderscore) { code += "_"; }
+ code += MakeCamel(field.name, false);
+ if (field.value.type.enum_def) { code += "?.value"; }
+ code += ");\n";
+ } else if (IsStruct(field.value.type)) {
+ code += " if (";
+ if (prependUnderscore) { code += "_"; }
+ code += MakeCamel(field.name, false) + " != null) {\n";
+ code += " fbBuilder.addStruct(" + NumToString(offset) + ", ";
+ code += "_" + MakeCamel(field.name, false) + ".finish(fbBuilder));\n";
+ code += " }\n";
+ } else {
+ code +=
+ " if (" + MakeCamel(field.name, false) + "Offset != null) {\n";
+ code += " fbBuilder.addOffset(" + NumToString(offset) + ", " +
+ MakeCamel(field.name, false) + "Offset);\n";
+ code += " }\n";
+ }
+ }
+ code += " return fbBuilder.endTable();\n";
+ }
+};
+} // namespace dart
+
+bool GenerateDart(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ dart::DartGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+std::string DartMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ assert(parser.opts.lang <= IDLOptions::kMAX);
+
+ auto filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ dart::DartGenerator generator(parser, path, file_name);
+ auto make_rule =
+ generator.GeneratedFileName(path, file_name, parser.opts) + ": ";
+
+ auto included_files = parser.GetIncludedFilesRecursive(file_name);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_fbs.cpp b/contrib/libs/flatbuffers/src/idl_gen_fbs.cpp
new file mode 100644
index 0000000000..35c1a7d4f5
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_fbs.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+static std::string GenType(const Type &type, bool underlying = false) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRUCT:
+ return type.struct_def->defined_namespace->GetFullyQualifiedName(
+ type.struct_def->name);
+ case BASE_TYPE_VECTOR: return "[" + GenType(type.VectorType()) + "]";
+ default:
+ if (type.enum_def && !underlying) {
+ return type.enum_def->defined_namespace->GetFullyQualifiedName(
+ type.enum_def->name);
+ } else {
+ return kTypeNames[type.base_type];
+ }
+ }
+}
+
+static void GenNameSpace(const Namespace &name_space, std::string *_schema,
+ const Namespace **last_namespace) {
+ if (*last_namespace == &name_space) return;
+ *last_namespace = &name_space;
+ auto &schema = *_schema;
+ schema += "namespace ";
+ for (auto it = name_space.components.begin();
+ it != name_space.components.end(); ++it) {
+ if (it != name_space.components.begin()) schema += ".";
+ schema += *it;
+ }
+ schema += ";\n\n";
+}
+
+// Generate a flatbuffer schema from the Parser's internal representation.
+std::string GenerateFBS(const Parser &parser, const std::string &file_name) {
+ // Proto namespaces may clash with table names, escape the ones that were
+ // generated from a table:
+ for (auto it = parser.namespaces_.begin(); it != parser.namespaces_.end();
+ ++it) {
+ auto &ns = **it;
+ for (size_t i = 0; i < ns.from_table; i++) {
+ ns.components[ns.components.size() - 1 - i] += "_";
+ }
+
+ if (parser.opts.proto_mode && !parser.opts.proto_namespace_suffix.empty()) {
+ // Since we know that all these namespaces come from a .proto, and all are
+ // being converted, we can simply apply this suffix to all of them.
+ ns.components.insert(ns.components.end() - ns.from_table,
+ parser.opts.proto_namespace_suffix);
+ }
+ }
+
+ std::string schema;
+ schema += "// Generated from " + file_name + ".proto\n\n";
+ if (parser.opts.include_dependence_headers) {
+ // clang-format off
+ int num_includes = 0;
+ for (auto it = parser.included_files_.begin();
+ it != parser.included_files_.end(); ++it) {
+ if (it->second.empty())
+ continue;
+ std::string basename;
+ if(parser.opts.keep_include_path) {
+ basename = flatbuffers::StripExtension(it->second);
+ } else {
+ basename = flatbuffers::StripPath(
+ flatbuffers::StripExtension(it->second));
+ }
+ schema += "include \"" + basename + ".fbs\";\n";
+ num_includes++;
+ }
+ if (num_includes) schema += "\n";
+ // clang-format on
+ }
+ // Generate code for all the enum declarations.
+ const Namespace *last_namespace = nullptr;
+ for (auto enum_def_it = parser.enums_.vec.begin();
+ enum_def_it != parser.enums_.vec.end(); ++enum_def_it) {
+ EnumDef &enum_def = **enum_def_it;
+ if (parser.opts.include_dependence_headers && enum_def.generated) {
+ continue;
+ }
+ GenNameSpace(*enum_def.defined_namespace, &schema, &last_namespace);
+ GenComment(enum_def.doc_comment, &schema, nullptr);
+ if (enum_def.is_union)
+ schema += "union " + enum_def.name;
+ else
+ schema += "enum " + enum_def.name + " : ";
+ schema += GenType(enum_def.underlying_type, true) + " {\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, &schema, nullptr, " ");
+ if (enum_def.is_union)
+ schema += " " + GenType(ev.union_type) + ",\n";
+ else
+ schema += " " + ev.name + " = " + enum_def.ToString(ev) + ",\n";
+ }
+ schema += "}\n\n";
+ }
+ // Generate code for all structs/tables.
+ for (auto it = parser.structs_.vec.begin(); it != parser.structs_.vec.end();
+ ++it) {
+ StructDef &struct_def = **it;
+ if (parser.opts.include_dependence_headers && struct_def.generated) {
+ continue;
+ }
+ GenNameSpace(*struct_def.defined_namespace, &schema, &last_namespace);
+ GenComment(struct_def.doc_comment, &schema, nullptr);
+ schema += "table " + struct_def.name + " {\n";
+ for (auto field_it = struct_def.fields.vec.begin();
+ field_it != struct_def.fields.vec.end(); ++field_it) {
+ auto &field = **field_it;
+ if (field.value.type.base_type != BASE_TYPE_UTYPE) {
+ GenComment(field.doc_comment, &schema, nullptr, " ");
+ schema += " " + field.name + ":" + GenType(field.value.type);
+ if (field.value.constant != "0") schema += " = " + field.value.constant;
+ if (field.IsRequired()) schema += " (required)";
+ schema += ";\n";
+ }
+ }
+ schema += "}\n\n";
+ }
+ return schema;
+}
+
+bool GenerateFBS(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ return SaveFile((path + file_name + ".fbs").c_str(),
+ GenerateFBS(parser, file_name), false);
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_go.cpp b/contrib/libs/flatbuffers/src/idl_gen_go.cpp
new file mode 100644
index 0000000000..867f402322
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_go.cpp
@@ -0,0 +1,1374 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <sstream>
+#include <string>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+#ifdef _WIN32
+# include <direct.h>
+# define PATH_SEPARATOR "\\"
+# define mkdir(n, m) _mkdir(n)
+#else
+# include <sys/stat.h>
+# define PATH_SEPARATOR "/"
+#endif
+
+namespace flatbuffers {
+
+namespace go {
+
+// see https://golang.org/ref/spec#Keywords
+static const char *const g_golang_keywords[] = {
+ "break", "default", "func", "interface", "select", "case", "defer",
+ "go", "map", "struct", "chan", "else", "goto", "package",
+ "switch", "const", "fallthrough", "if", "range", "type", "continue",
+ "for", "import", "return", "var",
+};
+
+static std::string GoIdentity(const std::string &name) {
+ for (size_t i = 0;
+ i < sizeof(g_golang_keywords) / sizeof(g_golang_keywords[0]); i++) {
+ if (name == g_golang_keywords[i]) { return MakeCamel(name + "_", false); }
+ }
+
+ return MakeCamel(name, false);
+}
+
+class GoGenerator : public BaseGenerator {
+ public:
+ GoGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name, const std::string &go_namespace)
+ : BaseGenerator(parser, path, file_name, "" /* not used*/,
+ "" /* not used */, "go"),
+ cur_name_space_(nullptr) {
+ std::istringstream iss(go_namespace);
+ std::string component;
+ while (std::getline(iss, component, '.')) {
+ go_namespace_.components.push_back(component);
+ }
+ }
+
+ bool generate() {
+ std::string one_file_code;
+ bool needs_imports = false;
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ tracked_imported_namespaces_.clear();
+ needs_imports = false;
+ std::string enumcode;
+ GenEnum(**it, &enumcode);
+ if ((*it)->is_union && parser_.opts.generate_object_based_api) {
+ GenNativeUnion(**it, &enumcode);
+ GenNativeUnionPack(**it, &enumcode);
+ GenNativeUnionUnPack(**it, &enumcode);
+ needs_imports = true;
+ }
+ if (parser_.opts.one_file) {
+ one_file_code += enumcode;
+ } else {
+ if (!SaveType(**it, enumcode, needs_imports, true)) return false;
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ tracked_imported_namespaces_.clear();
+ std::string declcode;
+ GenStruct(**it, &declcode);
+ if (parser_.opts.one_file) {
+ one_file_code += declcode;
+ } else {
+ if (!SaveType(**it, declcode, true, false)) return false;
+ }
+ }
+
+ if (parser_.opts.one_file) {
+ std::string code = "";
+ const bool is_enum = !parser_.enums_.vec.empty();
+ BeginFile(LastNamespacePart(go_namespace_), true, is_enum, &code);
+ code += one_file_code;
+ const std::string filename =
+ GeneratedFileName(path_, file_name_, parser_.opts);
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ return true;
+ }
+
+ private:
+ Namespace go_namespace_;
+ Namespace *cur_name_space_;
+
+ struct NamespacePtrLess {
+ bool operator()(const Namespace *a, const Namespace *b) const {
+ return *a < *b;
+ }
+ };
+ std::set<const Namespace *, NamespacePtrLess> tracked_imported_namespaces_;
+
+ // Most field accessors need to retrieve and test the field offset first,
+ // this is the prefix code for that.
+ std::string OffsetPrefix(const FieldDef &field) {
+ return "{\n\to := flatbuffers.UOffsetT(rcv._tab.Offset(" +
+ NumToString(field.value.offset) + "))\n\tif o != 0 {\n";
+ }
+
+ // Begin a class declaration.
+ void BeginClass(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "type " + struct_def.name + " struct {\n\t";
+
+ // _ is reserved in flatbuffers field names, so no chance of name conflict:
+ code += "_tab ";
+ code += struct_def.fixed ? "flatbuffers.Struct" : "flatbuffers.Table";
+ code += "\n}\n\n";
+ }
+
+ // Construct the name of the type for this enum.
+ std::string GetEnumTypeName(const EnumDef &enum_def) {
+ return WrapInNameSpaceAndTrack(enum_def.defined_namespace,
+ GoIdentity(enum_def.name));
+ }
+
+ // Create a type for the enum values.
+ void GenEnumType(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "type " + GetEnumTypeName(enum_def) + " ";
+ code += GenTypeBasic(enum_def.underlying_type) + "\n\n";
+ }
+
+ // Begin enum code with a class declaration.
+ void BeginEnum(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "const (\n";
+ }
+
+ // A single enum member.
+ void EnumMember(const EnumDef &enum_def, const EnumVal &ev,
+ size_t max_name_length, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\t";
+ code += enum_def.name;
+ code += ev.name;
+ code += " ";
+ code += std::string(max_name_length - ev.name.length(), ' ');
+ code += GetEnumTypeName(enum_def);
+ code += " = ";
+ code += enum_def.ToString(ev) + "\n";
+ }
+
+ // End enum code.
+ void EndEnum(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += ")\n\n";
+ }
+
+ // Begin enum name map.
+ void BeginEnumNames(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "var EnumNames";
+ code += enum_def.name;
+ code += " = map[" + GetEnumTypeName(enum_def) + "]string{\n";
+ }
+
+ // A single enum name member.
+ void EnumNameMember(const EnumDef &enum_def, const EnumVal &ev,
+ size_t max_name_length, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\t";
+ code += enum_def.name;
+ code += ev.name;
+ code += ": ";
+ code += std::string(max_name_length - ev.name.length(), ' ');
+ code += "\"";
+ code += ev.name;
+ code += "\",\n";
+ }
+
+ // End enum name map.
+ void EndEnumNames(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "}\n\n";
+ }
+
+ // Generate String() method on enum type.
+ void EnumStringer(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func (v " + enum_def.name + ") String() string {\n";
+ code += "\tif s, ok := EnumNames" + enum_def.name + "[v]; ok {\n";
+ code += "\t\treturn s\n";
+ code += "\t}\n";
+ code += "\treturn \"" + enum_def.name;
+ code += "(\" + strconv.FormatInt(int64(v), 10) + \")\"\n";
+ code += "}\n\n";
+ }
+
+ // Begin enum value map.
+ void BeginEnumValues(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "var EnumValues";
+ code += enum_def.name;
+ code += " = map[string]" + GetEnumTypeName(enum_def) + "{\n";
+ }
+
+ // A single enum value member.
+ void EnumValueMember(const EnumDef &enum_def, const EnumVal &ev,
+ size_t max_name_length, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\t\"";
+ code += ev.name;
+ code += "\": ";
+ code += std::string(max_name_length - ev.name.length(), ' ');
+ code += enum_def.name;
+ code += ev.name;
+ code += ",\n";
+ }
+
+ // End enum value map.
+ void EndEnumValues(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "}\n\n";
+ }
+
+ // Initialize a new struct or table from existing data.
+ void NewRootTypeFromBuffer(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string size_prefix[] = { "", "SizePrefixed" };
+
+ for (int i = 0; i < 2; i++) {
+ code += "func Get" + size_prefix[i] + "RootAs";
+ code += struct_def.name;
+ code += "(buf []byte, offset flatbuffers.UOffsetT) ";
+ code += "*" + struct_def.name + "";
+ code += " {\n";
+ if (i == 0) {
+ code += "\tn := flatbuffers.GetUOffsetT(buf[offset:])\n";
+ } else {
+ code +=
+ "\tn := "
+ "flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])\n";
+ }
+ code += "\tx := &" + struct_def.name + "{}\n";
+ if (i == 0) {
+ code += "\tx.Init(buf, n+offset)\n";
+ } else {
+ code += "\tx.Init(buf, n+offset+flatbuffers.SizeUint32)\n";
+ }
+ code += "\treturn x\n";
+ code += "}\n\n";
+ }
+ }
+
+ // Initialize an existing object with other data, to avoid an allocation.
+ void InitializeExisting(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += " Init(buf []byte, i flatbuffers.UOffsetT) ";
+ code += "{\n";
+ code += "\trcv._tab.Bytes = buf\n";
+ code += "\trcv._tab.Pos = i\n";
+ code += "}\n\n";
+ }
+
+ // Implement the table accessor
+ void GenTableAccessor(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += " Table() flatbuffers.Table ";
+ code += "{\n";
+
+ if (struct_def.fixed) {
+ code += "\treturn rcv._tab.Table\n";
+ } else {
+ code += "\treturn rcv._tab\n";
+ }
+ code += "}\n\n";
+ }
+
+ // Get the length of a vector.
+ void GetVectorLen(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name) + "Length(";
+ code += ") int " + OffsetPrefix(field);
+ code += "\t\treturn rcv._tab.VectorLen(o)\n\t}\n";
+ code += "\treturn 0\n}\n\n";
+ }
+
+ // Get a [ubyte] vector as a byte slice.
+ void GetUByteSlice(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name) + "Bytes(";
+ code += ") []byte " + OffsetPrefix(field);
+ code += "\t\treturn rcv._tab.ByteVector(o + rcv._tab.Pos)\n\t}\n";
+ code += "\treturn nil\n}\n\n";
+ }
+
+ // Get the value of a struct's scalar.
+ void GetScalarFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "() " + TypeName(field) + " {\n";
+ code += "\treturn " +
+ CastToEnum(field.value.type,
+ getter + "(rcv._tab.Pos + flatbuffers.UOffsetT(" +
+ NumToString(field.value.offset) + "))");
+ code += "\n}\n";
+ }
+
+ // Get the value of a table's scalar.
+ void GetScalarFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "() " + TypeName(field) + " ";
+ code += OffsetPrefix(field) + "\t\treturn ";
+ code += CastToEnum(field.value.type, getter + "(o + rcv._tab.Pos)");
+ code += "\n\t}\n";
+ code += "\treturn " + GenConstant(field) + "\n";
+ code += "}\n\n";
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Struct.
+ void GetStructFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "(obj *" + TypeName(field);
+ code += ") *" + TypeName(field);
+ code += " {\n";
+ code += "\tif obj == nil {\n";
+ code += "\t\tobj = new(" + TypeName(field) + ")\n";
+ code += "\t}\n";
+ code += "\tobj.Init(rcv._tab.Bytes, rcv._tab.Pos+";
+ code += NumToString(field.value.offset) + ")";
+ code += "\n\treturn obj\n";
+ code += "}\n";
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Table.
+ void GetStructFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "(obj *";
+ code += TypeName(field);
+ code += ") *" + TypeName(field) + " " + OffsetPrefix(field);
+ if (field.value.type.struct_def->fixed) {
+ code += "\t\tx := o + rcv._tab.Pos\n";
+ } else {
+ code += "\t\tx := rcv._tab.Indirect(o + rcv._tab.Pos)\n";
+ }
+ code += "\t\tif obj == nil {\n";
+ code += "\t\t\tobj = new(" + TypeName(field) + ")\n";
+ code += "\t\t}\n";
+ code += "\t\tobj.Init(rcv._tab.Bytes, x)\n";
+ code += "\t\treturn obj\n\t}\n\treturn nil\n";
+ code += "}\n\n";
+ }
+
+ // Get the value of a string.
+ void GetStringField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "() " + TypeName(field) + " ";
+ code += OffsetPrefix(field) + "\t\treturn " + GenGetter(field.value.type);
+ code += "(o + rcv._tab.Pos)\n\t}\n\treturn nil\n";
+ code += "}\n\n";
+ }
+
+ // Get the value of a union from an object.
+ void GetUnionField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name) + "(";
+ code += "obj " + GenTypePointer(field.value.type) + ") bool ";
+ code += OffsetPrefix(field);
+ code += "\t\t" + GenGetter(field.value.type);
+ code += "(obj, o)\n\t\treturn true\n\t}\n";
+ code += "\treturn false\n";
+ code += "}\n\n";
+ }
+
+ // Get the value of a vector's struct member.
+ void GetMemberOfVectorOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "(obj *" + TypeName(field);
+ code += ", j int) bool " + OffsetPrefix(field);
+ code += "\t\tx := rcv._tab.Vector(o)\n";
+ code += "\t\tx += flatbuffers.UOffsetT(j) * ";
+ code += NumToString(InlineSize(vectortype)) + "\n";
+ if (!(vectortype.struct_def->fixed)) {
+ code += "\t\tx = rcv._tab.Indirect(x)\n";
+ }
+ code += "\t\tobj.Init(rcv._tab.Bytes, x)\n";
+ code += "\t\treturn true\n\t}\n";
+ code += "\treturn false\n";
+ code += "}\n\n";
+ }
+
+ // Get the value of a vector's non-struct member.
+ void GetMemberOfVectorOfNonStruct(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += " " + MakeCamel(field.name);
+ code += "(j int) " + TypeName(field) + " ";
+ code += OffsetPrefix(field);
+ code += "\t\ta := rcv._tab.Vector(o)\n";
+ code += "\t\treturn " +
+ CastToEnum(field.value.type,
+ GenGetter(field.value.type) +
+ "(a + flatbuffers.UOffsetT(j*" +
+ NumToString(InlineSize(vectortype)) + "))");
+ code += "\n\t}\n";
+ if (IsString(vectortype)) {
+ code += "\treturn nil\n";
+ } else if (vectortype.base_type == BASE_TYPE_BOOL) {
+ code += "\treturn false\n";
+ } else {
+ code += "\treturn 0\n";
+ }
+ code += "}\n\n";
+ }
+
+ // Begin the creator function signature.
+ void BeginBuilderArgs(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ if (code.substr(code.length() - 2) != "\n\n") {
+ // a previous mutate has not put an extra new line
+ code += "\n";
+ }
+ code += "func Create" + struct_def.name;
+ code += "(builder *flatbuffers.Builder";
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void StructBuilderArgs(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ StructBuilderArgs(*field.value.type.struct_def,
+ (nameprefix + (field.name + "_")).c_str(), code_ptr);
+ } else {
+ std::string &code = *code_ptr;
+ code += std::string(", ") + nameprefix;
+ code += GoIdentity(field.name);
+ code += " " + TypeName(field);
+ }
+ }
+ }
+
+ // End the creator function signature.
+ void EndBuilderArgs(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += ") flatbuffers.UOffsetT {\n";
+ }
+
+ // Recursively generate struct construction statements and instert manual
+ // padding.
+ void StructBuilderBody(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\tbuilder.Prep(" + NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ")\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (field.padding)
+ code += "\tbuilder.Pad(" + NumToString(field.padding) + ")\n";
+ if (IsStruct(field.value.type)) {
+ StructBuilderBody(*field.value.type.struct_def,
+ (nameprefix + (field.name + "_")).c_str(), code_ptr);
+ } else {
+ code += "\tbuilder.Prepend" + GenMethod(field) + "(";
+ code += CastToBaseType(field.value.type,
+ nameprefix + GoIdentity(field.name)) +
+ ")\n";
+ }
+ }
+ }
+
+ void EndBuilderBody(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\treturn builder.Offset()\n";
+ code += "}\n";
+ }
+
+ // Get the value of a table's starting offset.
+ void GetStartOfTable(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func " + struct_def.name + "Start";
+ code += "(builder *flatbuffers.Builder) {\n";
+ code += "\tbuilder.StartObject(";
+ code += NumToString(struct_def.fields.vec.size());
+ code += ")\n}\n";
+ }
+
+ // Set the value of a table's field.
+ void BuildFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ const size_t offset, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func " + struct_def.name + "Add" + MakeCamel(field.name);
+ code += "(builder *flatbuffers.Builder, ";
+ code += GoIdentity(field.name) + " ";
+ if (!IsScalar(field.value.type.base_type) && (!struct_def.fixed)) {
+ code += "flatbuffers.UOffsetT";
+ } else {
+ code += TypeName(field);
+ }
+ code += ") {\n";
+ code += "\tbuilder.Prepend";
+ code += GenMethod(field) + "Slot(";
+ code += NumToString(offset) + ", ";
+ if (!IsScalar(field.value.type.base_type) && (!struct_def.fixed)) {
+ code += "flatbuffers.UOffsetT";
+ code += "(";
+ code += GoIdentity(field.name) + ")";
+ } else {
+ code += CastToBaseType(field.value.type, GoIdentity(field.name));
+ }
+ code += ", " + GenConstant(field);
+ code += ")\n}\n";
+ }
+
+ // Set the value of one of the members of a table's vector.
+ void BuildVectorOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func " + struct_def.name + "Start";
+ code += MakeCamel(field.name);
+ code += "Vector(builder *flatbuffers.Builder, numElems int) ";
+ code += "flatbuffers.UOffsetT {\n\treturn builder.StartVector(";
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code += NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment);
+ code += ")\n}\n";
+ }
+
+ // Get the offset of the end of a table.
+ void GetEndOffsetOnTable(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func " + struct_def.name + "End";
+ code += "(builder *flatbuffers.Builder) flatbuffers.UOffsetT ";
+ code += "{\n\treturn builder.EndObject()\n}\n";
+ }
+
+ // Generate the receiver for function signatures.
+ void GenReceiver(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func (rcv *" + struct_def.name + ")";
+ }
+
+ // Generate a struct field getter, conditioned on its child type(s).
+ void GenStructAccessor(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, nullptr, "");
+ if (IsScalar(field.value.type.base_type)) {
+ if (struct_def.fixed) {
+ GetScalarFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetScalarFieldOfTable(struct_def, field, code_ptr);
+ }
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ GetStructFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetStructFieldOfTable(struct_def, field, code_ptr);
+ }
+ break;
+ case BASE_TYPE_STRING:
+ GetStringField(struct_def, field, code_ptr);
+ break;
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GetMemberOfVectorOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetMemberOfVectorOfNonStruct(struct_def, field, code_ptr);
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: GetUnionField(struct_def, field, code_ptr); break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ if (IsVector(field.value.type)) {
+ GetVectorLen(struct_def, field, code_ptr);
+ if (field.value.type.element == BASE_TYPE_UCHAR) {
+ GetUByteSlice(struct_def, field, code_ptr);
+ }
+ }
+ }
+
+ // Mutate the value of a struct's scalar.
+ void MutateScalarFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string type = MakeCamel(GenTypeBasic(field.value.type));
+ std::string setter = "rcv._tab.Mutate" + type;
+ GenReceiver(struct_def, code_ptr);
+ code += " Mutate" + MakeCamel(field.name);
+ code += "(n " + TypeName(field) + ") bool {\n\treturn " + setter;
+ code += "(rcv._tab.Pos+flatbuffers.UOffsetT(";
+ code += NumToString(field.value.offset) + "), ";
+ code += CastToBaseType(field.value.type, "n") + ")\n}\n\n";
+ }
+
+ // Mutate the value of a table's scalar.
+ void MutateScalarFieldOfTable(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string type = MakeCamel(GenTypeBasic(field.value.type));
+ std::string setter = "rcv._tab.Mutate" + type + "Slot";
+ GenReceiver(struct_def, code_ptr);
+ code += " Mutate" + MakeCamel(field.name);
+ code += "(n " + TypeName(field) + ") bool {\n\treturn ";
+ code += setter + "(" + NumToString(field.value.offset) + ", ";
+ code += CastToBaseType(field.value.type, "n") + ")\n";
+ code += "}\n\n";
+ }
+
+ // Mutate an element of a vector of scalars.
+ void MutateElementOfVectorOfNonStruct(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+ std::string type = MakeCamel(GenTypeBasic(vectortype));
+ std::string setter = "rcv._tab.Mutate" + type;
+ GenReceiver(struct_def, code_ptr);
+ code += " Mutate" + MakeCamel(field.name);
+ code += "(j int, n " + TypeName(field) + ") bool ";
+ code += OffsetPrefix(field);
+ code += "\t\ta := rcv._tab.Vector(o)\n";
+ code += "\t\treturn " + setter + "(";
+ code += "a+flatbuffers.UOffsetT(j*";
+ code += NumToString(InlineSize(vectortype)) + "), ";
+ code += CastToBaseType(vectortype, "n") + ")\n";
+ code += "\t}\n";
+ code += "\treturn false\n";
+ code += "}\n\n";
+ }
+
+ // Generate a struct field setter, conditioned on its child type(s).
+ void GenStructMutator(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, nullptr, "");
+ if (IsScalar(field.value.type.base_type)) {
+ if (struct_def.fixed) {
+ MutateScalarFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ MutateScalarFieldOfTable(struct_def, field, code_ptr);
+ }
+ } else if (IsVector(field.value.type)) {
+ if (IsScalar(field.value.type.element)) {
+ MutateElementOfVectorOfNonStruct(struct_def, field, code_ptr);
+ }
+ }
+ }
+
+ // Generate table constructors, conditioned on its members' types.
+ void GenTableBuilders(const StructDef &struct_def, std::string *code_ptr) {
+ GetStartOfTable(struct_def, code_ptr);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto offset = it - struct_def.fields.vec.begin();
+ BuildFieldOfTable(struct_def, field, offset, code_ptr);
+ if (IsVector(field.value.type)) {
+ BuildVectorOfTable(struct_def, field, code_ptr);
+ }
+ }
+
+ GetEndOffsetOnTable(struct_def, code_ptr);
+ }
+
+ // Generate struct or table methods.
+ void GenStruct(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+
+ cur_name_space_ = struct_def.defined_namespace;
+
+ GenComment(struct_def.doc_comment, code_ptr, nullptr);
+ if (parser_.opts.generate_object_based_api) {
+ GenNativeStruct(struct_def, code_ptr);
+ }
+ BeginClass(struct_def, code_ptr);
+ if (!struct_def.fixed) {
+ // Generate a special accessor for the table that has been declared as
+ // the root type.
+ NewRootTypeFromBuffer(struct_def, code_ptr);
+ }
+ // Generate the Init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ InitializeExisting(struct_def, code_ptr);
+ // Generate _tab accessor
+ GenTableAccessor(struct_def, code_ptr);
+
+ // Generate struct fields accessors
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ GenStructAccessor(struct_def, field, code_ptr);
+ GenStructMutator(struct_def, field, code_ptr);
+ }
+
+ // Generate builders
+ if (struct_def.fixed) {
+ // create a struct constructor function
+ GenStructBuilder(struct_def, code_ptr);
+ } else {
+ // Create a set of functions that allow table construction.
+ GenTableBuilders(struct_def, code_ptr);
+ }
+ }
+
+ void GenNativeStruct(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "type " + NativeName(struct_def) + " struct {\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.deprecated) continue;
+ if (IsScalar(field.value.type.base_type) &&
+ field.value.type.enum_def != nullptr &&
+ field.value.type.enum_def->is_union)
+ continue;
+ code += "\t" + MakeCamel(field.name) + " " +
+ NativeType(field.value.type) + "\n";
+ }
+ code += "}\n\n";
+
+ if (!struct_def.fixed) {
+ GenNativeTablePack(struct_def, code_ptr);
+ GenNativeTableUnPack(struct_def, code_ptr);
+ } else {
+ GenNativeStructPack(struct_def, code_ptr);
+ GenNativeStructUnPack(struct_def, code_ptr);
+ }
+ }
+
+ void GenNativeUnion(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "type " + NativeName(enum_def) + " struct {\n";
+ code += "\tType " + enum_def.name + "\n";
+ code += "\tValue interface{}\n";
+ code += "}\n\n";
+ }
+
+ void GenNativeUnionPack(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "func (t *" + NativeName(enum_def) +
+ ") Pack(builder *flatbuffers.Builder) flatbuffers.UOffsetT {\n";
+ code += "\tif t == nil {\n\t\treturn 0\n\t}\n";
+
+ code += "\tswitch t.Type {\n";
+ for (auto it2 = enum_def.Vals().begin(); it2 != enum_def.Vals().end();
+ ++it2) {
+ const EnumVal &ev = **it2;
+ if (ev.IsZero()) continue;
+ code += "\tcase " + enum_def.name + ev.name + ":\n";
+ code += "\t\treturn t.Value.(" + NativeType(ev.union_type) +
+ ").Pack(builder)\n";
+ }
+ code += "\t}\n";
+ code += "\treturn 0\n";
+ code += "}\n\n";
+ }
+
+ void GenNativeUnionUnPack(const EnumDef &enum_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "func (rcv " + enum_def.name +
+ ") UnPack(table flatbuffers.Table) *" + NativeName(enum_def) +
+ " {\n";
+ code += "\tswitch rcv {\n";
+
+ for (auto it2 = enum_def.Vals().begin(); it2 != enum_def.Vals().end();
+ ++it2) {
+ const EnumVal &ev = **it2;
+ if (ev.IsZero()) continue;
+ code += "\tcase " + enum_def.name + ev.name + ":\n";
+ code += "\t\tx := " + ev.union_type.struct_def->name + "{_tab: table}\n";
+
+ code += "\t\treturn &" +
+ WrapInNameSpaceAndTrack(enum_def.defined_namespace,
+ NativeName(enum_def)) +
+ "{ Type: " + enum_def.name + ev.name + ", Value: x.UnPack() }\n";
+ }
+ code += "\t}\n";
+ code += "\treturn nil\n";
+ code += "}\n\n";
+ }
+
+ void GenNativeTablePack(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "func (t *" + NativeName(struct_def) +
+ ") Pack(builder *flatbuffers.Builder) flatbuffers.UOffsetT {\n";
+ code += "\tif t == nil { return 0 }\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.deprecated) continue;
+ if (IsScalar(field.value.type.base_type)) continue;
+
+ std::string offset = MakeCamel(field.name, false) + "Offset";
+
+ if (IsString(field.value.type)) {
+ code += "\t" + offset + " := builder.CreateString(t." +
+ MakeCamel(field.name) + ")\n";
+ } else if (IsVector(field.value.type) &&
+ field.value.type.element == BASE_TYPE_UCHAR &&
+ field.value.type.enum_def == nullptr) {
+ code += "\t" + offset + " := flatbuffers.UOffsetT(0)\n";
+ code += "\tif t." + MakeCamel(field.name) + " != nil {\n";
+ code += "\t\t" + offset + " = builder.CreateByteString(t." +
+ MakeCamel(field.name) + ")\n";
+ code += "\t}\n";
+ } else if (IsVector(field.value.type)) {
+ code += "\t" + offset + " := flatbuffers.UOffsetT(0)\n";
+ code += "\tif t." + MakeCamel(field.name) + " != nil {\n";
+ std::string length = MakeCamel(field.name, false) + "Length";
+ std::string offsets = MakeCamel(field.name, false) + "Offsets";
+ code += "\t\t" + length + " := len(t." + MakeCamel(field.name) + ")\n";
+ if (field.value.type.element == BASE_TYPE_STRING) {
+ code += "\t\t" + offsets + " := make([]flatbuffers.UOffsetT, " +
+ length + ")\n";
+ code += "\t\tfor j := 0; j < " + length + "; j++ {\n";
+ code += "\t\t\t" + offsets + "[j] = builder.CreateString(t." +
+ MakeCamel(field.name) + "[j])\n";
+ code += "\t\t}\n";
+ } else if (field.value.type.element == BASE_TYPE_STRUCT &&
+ !field.value.type.struct_def->fixed) {
+ code += "\t\t" + offsets + " := make([]flatbuffers.UOffsetT, " +
+ length + ")\n";
+ code += "\t\tfor j := 0; j < " + length + "; j++ {\n";
+ code += "\t\t\t" + offsets + "[j] = t." + MakeCamel(field.name) +
+ "[j].Pack(builder)\n";
+ code += "\t\t}\n";
+ }
+ code += "\t\t" + struct_def.name + "Start" + MakeCamel(field.name) +
+ "Vector(builder, " + length + ")\n";
+ code += "\t\tfor j := " + length + " - 1; j >= 0; j-- {\n";
+ if (IsScalar(field.value.type.element)) {
+ code += "\t\t\tbuilder.Prepend" +
+ MakeCamel(GenTypeBasic(field.value.type.VectorType())) + "(" +
+ CastToBaseType(field.value.type.VectorType(),
+ "t." + MakeCamel(field.name) + "[j]") +
+ ")\n";
+ } else if (field.value.type.element == BASE_TYPE_STRUCT &&
+ field.value.type.struct_def->fixed) {
+ code += "\t\t\tt." + MakeCamel(field.name) + "[j].Pack(builder)\n";
+ } else {
+ code += "\t\t\tbuilder.PrependUOffsetT(" + offsets + "[j])\n";
+ }
+ code += "\t\t}\n";
+ code += "\t\t" + offset + " = builder.EndVector(" + length + ")\n";
+ code += "\t}\n";
+ } else if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ if (field.value.type.struct_def->fixed) continue;
+ code += "\t" + offset + " := t." + MakeCamel(field.name) +
+ ".Pack(builder)\n";
+ } else if (field.value.type.base_type == BASE_TYPE_UNION) {
+ code += "\t" + offset + " := t." + MakeCamel(field.name) +
+ ".Pack(builder)\n";
+ code += "\t\n";
+ } else {
+ FLATBUFFERS_ASSERT(0);
+ }
+ }
+ code += "\t" + struct_def.name + "Start(builder)\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.deprecated) continue;
+
+ std::string offset = MakeCamel(field.name, false) + "Offset";
+ if (IsScalar(field.value.type.base_type)) {
+ if (field.value.type.enum_def == nullptr ||
+ !field.value.type.enum_def->is_union) {
+ code += "\t" + struct_def.name + "Add" + MakeCamel(field.name) +
+ "(builder, t." + MakeCamel(field.name) + ")\n";
+ }
+ } else {
+ if (field.value.type.base_type == BASE_TYPE_STRUCT &&
+ field.value.type.struct_def->fixed) {
+ code += "\t" + offset + " := t." + MakeCamel(field.name) +
+ ".Pack(builder)\n";
+ } else if (field.value.type.enum_def != nullptr &&
+ field.value.type.enum_def->is_union) {
+ code += "\tif t." + MakeCamel(field.name) + " != nil {\n";
+ code += "\t\t" + struct_def.name + "Add" +
+ MakeCamel(field.name + UnionTypeFieldSuffix()) +
+ "(builder, t." + MakeCamel(field.name) + ".Type)\n";
+ code += "\t}\n";
+ }
+ code += "\t" + struct_def.name + "Add" + MakeCamel(field.name) +
+ "(builder, " + offset + ")\n";
+ }
+ }
+ code += "\treturn " + struct_def.name + "End(builder)\n";
+ code += "}\n\n";
+ }
+
+ void GenNativeTableUnPack(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "func (rcv *" + struct_def.name + ") UnPackTo(t *" +
+ NativeName(struct_def) + ") {\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.deprecated) continue;
+ std::string field_name_camel = MakeCamel(field.name);
+ std::string length = MakeCamel(field.name, false) + "Length";
+ if (IsScalar(field.value.type.base_type)) {
+ if (field.value.type.enum_def != nullptr &&
+ field.value.type.enum_def->is_union)
+ continue;
+ code +=
+ "\tt." + field_name_camel + " = rcv." + field_name_camel + "()\n";
+ } else if (IsString(field.value.type)) {
+ code += "\tt." + field_name_camel + " = string(rcv." +
+ field_name_camel + "())\n";
+ } else if (IsVector(field.value.type) &&
+ field.value.type.element == BASE_TYPE_UCHAR &&
+ field.value.type.enum_def == nullptr) {
+ code += "\tt." + field_name_camel + " = rcv." + field_name_camel +
+ "Bytes()\n";
+ } else if (IsVector(field.value.type)) {
+ code += "\t" + length + " := rcv." + field_name_camel + "Length()\n";
+ code += "\tt." + field_name_camel + " = make(" +
+ NativeType(field.value.type) + ", " + length + ")\n";
+ code += "\tfor j := 0; j < " + length + "; j++ {\n";
+ if (field.value.type.element == BASE_TYPE_STRUCT) {
+ code += "\t\tx := " +
+ WrapInNameSpaceAndTrack(*field.value.type.struct_def) +
+ "{}\n";
+ code += "\t\trcv." + field_name_camel + "(&x, j)\n";
+ }
+ code += "\t\tt." + field_name_camel + "[j] = ";
+ if (IsScalar(field.value.type.element)) {
+ code += "rcv." + field_name_camel + "(j)";
+ } else if (field.value.type.element == BASE_TYPE_STRING) {
+ code += "string(rcv." + field_name_camel + "(j))";
+ } else if (field.value.type.element == BASE_TYPE_STRUCT) {
+ code += "x.UnPack()";
+ } else {
+ // TODO(iceboy): Support vector of unions.
+ FLATBUFFERS_ASSERT(0);
+ }
+ code += "\n";
+ code += "\t}\n";
+ } else if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ code += "\tt." + field_name_camel + " = rcv." + field_name_camel +
+ "(nil).UnPack()\n";
+ } else if (field.value.type.base_type == BASE_TYPE_UNION) {
+ std::string field_table = MakeCamel(field.name, false) + "Table";
+ code += "\t" + field_table + " := flatbuffers.Table{}\n";
+ code +=
+ "\tif rcv." + MakeCamel(field.name) + "(&" + field_table + ") {\n";
+ code += "\t\tt." + field_name_camel + " = rcv." +
+ MakeCamel(field.name + UnionTypeFieldSuffix()) + "().UnPack(" +
+ field_table + ")\n";
+ code += "\t}\n";
+ } else {
+ FLATBUFFERS_ASSERT(0);
+ }
+ }
+ code += "}\n\n";
+
+ code += "func (rcv *" + struct_def.name + ") UnPack() *" +
+ NativeName(struct_def) + " {\n";
+ code += "\tif rcv == nil { return nil }\n";
+ code += "\tt := &" + NativeName(struct_def) + "{}\n";
+ code += "\trcv.UnPackTo(t)\n";
+ code += "\treturn t\n";
+ code += "}\n\n";
+ }
+
+ void GenNativeStructPack(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "func (t *" + NativeName(struct_def) +
+ ") Pack(builder *flatbuffers.Builder) flatbuffers.UOffsetT {\n";
+ code += "\tif t == nil { return 0 }\n";
+ code += "\treturn Create" + struct_def.name + "(builder";
+ StructPackArgs(struct_def, "", code_ptr);
+ code += ")\n";
+ code += "}\n";
+ }
+
+ void StructPackArgs(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ StructPackArgs(*field.value.type.struct_def,
+ (nameprefix + MakeCamel(field.name) + ".").c_str(),
+ code_ptr);
+ } else {
+ code += std::string(", t.") + nameprefix + MakeCamel(field.name);
+ }
+ }
+ }
+
+ void GenNativeStructUnPack(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "func (rcv *" + struct_def.name + ") UnPackTo(t *" +
+ NativeName(struct_def) + ") {\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const FieldDef &field = **it;
+ if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ code += "\tt." + MakeCamel(field.name) + " = rcv." +
+ MakeCamel(field.name) + "(nil).UnPack()\n";
+ } else {
+ code += "\tt." + MakeCamel(field.name) + " = rcv." +
+ MakeCamel(field.name) + "()\n";
+ }
+ }
+ code += "}\n\n";
+
+ code += "func (rcv *" + struct_def.name + ") UnPack() *" +
+ NativeName(struct_def) + " {\n";
+ code += "\tif rcv == nil { return nil }\n";
+ code += "\tt := &" + NativeName(struct_def) + "{}\n";
+ code += "\trcv.UnPackTo(t)\n";
+ code += "\treturn t\n";
+ code += "}\n\n";
+ }
+
+ // Generate enum declarations.
+ void GenEnum(const EnumDef &enum_def, std::string *code_ptr) {
+ if (enum_def.generated) return;
+
+ auto max_name_length = MaxNameLength(enum_def);
+ cur_name_space_ = enum_def.defined_namespace;
+
+ GenComment(enum_def.doc_comment, code_ptr, nullptr);
+ GenEnumType(enum_def, code_ptr);
+ BeginEnum(code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const EnumVal &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, nullptr, "\t");
+ EnumMember(enum_def, ev, max_name_length, code_ptr);
+ }
+ EndEnum(code_ptr);
+
+ BeginEnumNames(enum_def, code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const EnumVal &ev = **it;
+ EnumNameMember(enum_def, ev, max_name_length, code_ptr);
+ }
+ EndEnumNames(code_ptr);
+
+ BeginEnumValues(enum_def, code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ EnumValueMember(enum_def, ev, max_name_length, code_ptr);
+ }
+ EndEnumValues(code_ptr);
+
+ EnumStringer(enum_def, code_ptr);
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetter(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "rcv._tab.ByteVector";
+ case BASE_TYPE_UNION: return "rcv._tab.Union";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ default: return "rcv._tab.Get" + MakeCamel(GenTypeBasic(type));
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const FieldDef &field) {
+ return IsScalar(field.value.type.base_type)
+ ? MakeCamel(GenTypeBasic(field.value.type))
+ : (IsStruct(field.value.type) ? "Struct" : "UOffsetT");
+ }
+
+ std::string GenTypeBasic(const Type &type) {
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, GTYPE, ...) \
+ #GTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return ctypename[type.base_type];
+ }
+
+ std::string GenTypePointer(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "[]byte";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return WrapInNameSpaceAndTrack(*type.struct_def);
+ case BASE_TYPE_UNION:
+ // fall through
+ default: return "*flatbuffers.Table";
+ }
+ }
+
+ std::string GenTypeGet(const Type &type) {
+ if (type.enum_def != nullptr) { return GetEnumTypeName(*type.enum_def); }
+ return IsScalar(type.base_type) ? GenTypeBasic(type) : GenTypePointer(type);
+ }
+
+ std::string TypeName(const FieldDef &field) {
+ return GenTypeGet(field.value.type);
+ }
+
+ // If type is an enum, returns value with a cast to the enum type, otherwise
+ // returns value as-is.
+ std::string CastToEnum(const Type &type, std::string value) {
+ if (type.enum_def == nullptr) {
+ return value;
+ } else {
+ return GenTypeGet(type) + "(" + value + ")";
+ }
+ }
+
+ // If type is an enum, returns value with a cast to the enum base type,
+ // otherwise returns value as-is.
+ std::string CastToBaseType(const Type &type, std::string value) {
+ if (type.enum_def == nullptr) {
+ return value;
+ } else {
+ return GenTypeBasic(type) + "(" + value + ")";
+ }
+ }
+
+ std::string GenConstant(const FieldDef &field) {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_BOOL:
+ return field.value.constant == "0" ? "false" : "true";
+ default: return field.value.constant;
+ }
+ }
+
+ std::string NativeName(const StructDef &struct_def) {
+ return parser_.opts.object_prefix + struct_def.name +
+ parser_.opts.object_suffix;
+ }
+
+ std::string NativeName(const EnumDef &enum_def) {
+ return parser_.opts.object_prefix + enum_def.name +
+ parser_.opts.object_suffix;
+ }
+
+ std::string NativeType(const Type &type) {
+ if (IsScalar(type.base_type)) {
+ if (type.enum_def == nullptr) {
+ return GenTypeBasic(type);
+ } else {
+ return GetEnumTypeName(*type.enum_def);
+ }
+ } else if (IsString(type)) {
+ return "string";
+ } else if (IsVector(type)) {
+ return "[]" + NativeType(type.VectorType());
+ } else if (type.base_type == BASE_TYPE_STRUCT) {
+ return "*" + WrapInNameSpaceAndTrack(type.struct_def->defined_namespace,
+ NativeName(*type.struct_def));
+ } else if (type.base_type == BASE_TYPE_UNION) {
+ return "*" + WrapInNameSpaceAndTrack(type.enum_def->defined_namespace,
+ NativeName(*type.enum_def));
+ }
+ FLATBUFFERS_ASSERT(0);
+ return std::string();
+ }
+
+ // Create a struct with a builder and the struct's arguments.
+ void GenStructBuilder(const StructDef &struct_def, std::string *code_ptr) {
+ BeginBuilderArgs(struct_def, code_ptr);
+ StructBuilderArgs(struct_def, "", code_ptr);
+ EndBuilderArgs(code_ptr);
+
+ StructBuilderBody(struct_def, "", code_ptr);
+ EndBuilderBody(code_ptr);
+ }
+ // Begin by declaring namespace and imports.
+ void BeginFile(const std::string &name_space_name, const bool needs_imports,
+ const bool is_enum, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code = code +
+ "// Code generated by the FlatBuffers compiler. DO NOT EDIT.\n\n";
+ code += "package " + name_space_name + "\n\n";
+ if (needs_imports) {
+ code += "import (\n";
+ if (is_enum) { code += "\t\"strconv\"\n\n"; }
+ if (!parser_.opts.go_import.empty()) {
+ code += "\tflatbuffers \"" + parser_.opts.go_import + "\"\n";
+ } else {
+ code += "\tflatbuffers \"github.com/google/flatbuffers/go\"\n";
+ }
+ if (tracked_imported_namespaces_.size() > 0) {
+ code += "\n";
+ for (auto it = tracked_imported_namespaces_.begin();
+ it != tracked_imported_namespaces_.end(); ++it) {
+ code += "\t" + NamespaceImportName(*it) + " \"" +
+ NamespaceImportPath(*it) + "\"\n";
+ }
+ }
+ code += ")\n\n";
+ } else {
+ if (is_enum) { code += "import \"strconv\"\n\n"; }
+ }
+ }
+
+ // Save out the generated code for a Go Table type.
+ bool SaveType(const Definition &def, const std::string &classcode,
+ const bool needs_imports, const bool is_enum) {
+ if (!classcode.length()) return true;
+
+ Namespace &ns = go_namespace_.components.empty() ? *def.defined_namespace
+ : go_namespace_;
+ std::string code = "";
+ BeginFile(LastNamespacePart(ns), needs_imports, is_enum, &code);
+ code += classcode;
+ // Strip extra newlines at end of file to make it gofmt-clean.
+ while (code.length() > 2 && code.substr(code.length() - 2) == "\n\n") {
+ code.pop_back();
+ }
+ std::string filename = NamespaceDir(ns) + def.name + ".go";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ // Create the full name of the imported namespace (format: A__B__C).
+ std::string NamespaceImportName(const Namespace *ns) {
+ std::string s = "";
+ for (auto it = ns->components.begin(); it != ns->components.end(); ++it) {
+ if (s.size() == 0) {
+ s += *it;
+ } else {
+ s += "__" + *it;
+ }
+ }
+ return s;
+ }
+
+ // Create the full path for the imported namespace (format: A/B/C).
+ std::string NamespaceImportPath(const Namespace *ns) {
+ std::string s = "";
+ for (auto it = ns->components.begin(); it != ns->components.end(); ++it) {
+ if (s.size() == 0) {
+ s += *it;
+ } else {
+ s += "/" + *it;
+ }
+ }
+ return s;
+ }
+
+ // Ensure that a type is prefixed with its go package import name if it is
+ // used outside of its namespace.
+ std::string WrapInNameSpaceAndTrack(const Namespace *ns,
+ const std::string &name) {
+ if (CurrentNameSpace() == ns) return name;
+
+ tracked_imported_namespaces_.insert(ns);
+
+ std::string import_name = NamespaceImportName(ns);
+ return import_name + "." + name;
+ }
+
+ std::string WrapInNameSpaceAndTrack(const Definition &def) {
+ return WrapInNameSpaceAndTrack(def.defined_namespace, def.name);
+ }
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+ static size_t MaxNameLength(const EnumDef &enum_def) {
+ size_t max = 0;
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ max = std::max((*it)->name.length(), max);
+ }
+ return max;
+ }
+};
+} // namespace go
+
+bool GenerateGo(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ go::GoGenerator generator(parser, path, file_name, parser.opts.go_namespace);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_grpc.cpp b/contrib/libs/flatbuffers/src/idl_gen_grpc.cpp
new file mode 100644
index 0000000000..9aea745d4e
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_grpc.cpp
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+#include "src/compiler/cpp_generator.h"
+#include "src/compiler/go_generator.h"
+#include "src/compiler/java_generator.h"
+#include "src/compiler/python_generator.h"
+#include "src/compiler/swift_generator.h"
+#include "src/compiler/ts_generator.h"
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable : 4512) // C4512: 'class' : assignment operator could
+// not be generated
+#endif
+
+namespace flatbuffers {
+
+class FlatBufMethod : public grpc_generator::Method {
+ public:
+ enum Streaming { kNone, kClient, kServer, kBiDi };
+
+ FlatBufMethod(const RPCCall *method) : method_(method) {
+ streaming_ = kNone;
+ auto val = method_->attributes.Lookup("streaming");
+ if (val) {
+ if (val->constant == "client") streaming_ = kClient;
+ if (val->constant == "server") streaming_ = kServer;
+ if (val->constant == "bidi") streaming_ = kBiDi;
+ }
+ }
+
+ grpc::string GetLeadingComments(const grpc::string) const { return ""; }
+
+ grpc::string GetTrailingComments(const grpc::string) const { return ""; }
+
+ std::vector<grpc::string> GetAllComments() const {
+ return method_->doc_comment;
+ }
+
+ std::string name() const { return method_->name; }
+
+ // TODO: This method need to incorporate namespace for C++ side. Other
+ // language bindings simply don't use this method.
+ std::string GRPCType(const StructDef &sd) const {
+ return "flatbuffers::grpc::Message<" + sd.name + ">";
+ }
+
+ std::vector<std::string> get_input_namespace_parts() const {
+ return (*method_->request).defined_namespace->components;
+ }
+
+ std::string get_input_type_name() const { return (*method_->request).name; }
+
+ std::vector<std::string> get_output_namespace_parts() const {
+ return (*method_->response).defined_namespace->components;
+ }
+
+ std::string get_output_type_name() const { return (*method_->response).name; }
+
+ bool get_module_and_message_path_input(grpc::string * /*str*/,
+ grpc::string /*generator_file_name*/,
+ bool /*generate_in_pb2_grpc*/,
+ grpc::string /*import_prefix*/) const {
+ return true;
+ }
+
+ bool get_module_and_message_path_output(
+ grpc::string * /*str*/, grpc::string /*generator_file_name*/,
+ bool /*generate_in_pb2_grpc*/, grpc::string /*import_prefix*/) const {
+ return true;
+ }
+
+ std::string get_fb_builder() const { return "builder"; }
+
+ std::string input_type_name() const { return GRPCType(*method_->request); }
+
+ std::string output_type_name() const { return GRPCType(*method_->response); }
+
+ bool NoStreaming() const { return streaming_ == kNone; }
+
+ bool ClientStreaming() const { return streaming_ == kClient; }
+
+ bool ServerStreaming() const { return streaming_ == kServer; }
+
+ bool BidiStreaming() const { return streaming_ == kBiDi; }
+
+ private:
+ const RPCCall *method_;
+ Streaming streaming_;
+};
+
+class FlatBufService : public grpc_generator::Service {
+ public:
+ FlatBufService(const ServiceDef *service) : service_(service) {}
+
+ grpc::string GetLeadingComments(const grpc::string) const { return ""; }
+
+ grpc::string GetTrailingComments(const grpc::string) const { return ""; }
+
+ std::vector<grpc::string> GetAllComments() const {
+ return service_->doc_comment;
+ }
+
+ std::vector<grpc::string> namespace_parts() const {
+ return service_->defined_namespace->components;
+ }
+
+ std::string name() const { return service_->name; }
+ bool is_internal() const {
+ return service_->Definition::attributes.Lookup("private") ? true : false;
+ }
+
+ int method_count() const {
+ return static_cast<int>(service_->calls.vec.size());
+ }
+
+ std::unique_ptr<const grpc_generator::Method> method(int i) const {
+ return std::unique_ptr<const grpc_generator::Method>(
+ new FlatBufMethod(service_->calls.vec[i]));
+ }
+
+ private:
+ const ServiceDef *service_;
+};
+
+class FlatBufPrinter : public grpc_generator::Printer {
+ public:
+ FlatBufPrinter(std::string *str, const char indentation_type)
+ : str_(str),
+ escape_char_('$'),
+ indent_(0),
+ indentation_size_(2),
+ indentation_type_(indentation_type) {}
+
+ void Print(const std::map<std::string, std::string> &vars,
+ const char *string_template) {
+ std::string s = string_template;
+ // Replace any occurrences of strings in "vars" that are surrounded
+ // by the escape character by what they're mapped to.
+ size_t pos;
+ while ((pos = s.find(escape_char_)) != std::string::npos) {
+ // Found an escape char, must also find the closing one.
+ size_t pos2 = s.find(escape_char_, pos + 1);
+ // If placeholder not closed, ignore.
+ if (pos2 == std::string::npos) break;
+ auto it = vars.find(s.substr(pos + 1, pos2 - pos - 1));
+ // If unknown placeholder, ignore.
+ if (it == vars.end()) break;
+ // Subtitute placeholder.
+ s.replace(pos, pos2 - pos + 1, it->second);
+ }
+ Print(s.c_str());
+ }
+
+ void Print(const char *s) {
+ if (s == nullptr || *s == '\0') { return; }
+ // Add this string, but for each part separated by \n, add indentation.
+ for (;;) {
+ // Current indentation.
+ str_->insert(str_->end(), indent_ * indentation_size_, indentation_type_);
+ // See if this contains more than one line.
+ const char *lf = strchr(s, '\n');
+ if (lf) {
+ (*str_) += std::string(s, lf + 1);
+ s = lf + 1;
+ if (!*s) break; // Only continue if there's more lines.
+ } else {
+ (*str_) += s;
+ break;
+ }
+ }
+ }
+
+ void SetIndentationSize(const int size) {
+ FLATBUFFERS_ASSERT(str_->empty());
+ indentation_size_ = size;
+ }
+
+ void Indent() { indent_++; }
+
+ void Outdent() {
+ indent_--;
+ FLATBUFFERS_ASSERT(indent_ >= 0);
+ }
+
+ private:
+ std::string *str_;
+ char escape_char_;
+ int indent_;
+ int indentation_size_;
+ char indentation_type_;
+};
+
+class FlatBufFile : public grpc_generator::File {
+ public:
+ enum Language {
+ kLanguageGo,
+ kLanguageCpp,
+ kLanguageJava,
+ kLanguagePython,
+ kLanguageSwift,
+ kLanguageTS
+ };
+
+ FlatBufFile(const Parser &parser, const std::string &file_name,
+ Language language)
+ : parser_(parser), file_name_(file_name), language_(language) {}
+
+ FlatBufFile &operator=(const FlatBufFile &);
+
+ grpc::string GetLeadingComments(const grpc::string) const { return ""; }
+
+ grpc::string GetTrailingComments(const grpc::string) const { return ""; }
+
+ std::vector<grpc::string> GetAllComments() const {
+ return std::vector<grpc::string>();
+ }
+
+ std::string filename() const { return file_name_; }
+
+ std::string filename_without_ext() const {
+ return StripExtension(file_name_);
+ }
+
+ std::string message_header_ext() const { return "_generated.h"; }
+
+ std::string service_header_ext() const { return ".grpc.fb.h"; }
+
+ std::string package() const {
+ return parser_.current_namespace_->GetFullyQualifiedName("");
+ }
+
+ std::vector<std::string> package_parts() const {
+ return parser_.current_namespace_->components;
+ }
+
+ std::string additional_headers() const {
+ switch (language_) {
+ case kLanguageCpp: {
+ return "#include \"flatbuffers/grpc.h\"\n";
+ }
+ case kLanguageGo: {
+ return "import \"github.com/google/flatbuffers/go\"";
+ }
+ case kLanguageJava: {
+ return "import com.google.flatbuffers.grpc.FlatbuffersUtils;";
+ }
+ case kLanguagePython: {
+ return "";
+ }
+ case kLanguageSwift: {
+ return "";
+ }
+ case kLanguageTS: {
+ return "";
+ }
+ }
+ return "";
+ }
+
+ int service_count() const {
+ return static_cast<int>(parser_.services_.vec.size());
+ }
+
+ std::unique_ptr<const grpc_generator::Service> service(int i) const {
+ return std::unique_ptr<const grpc_generator::Service>(
+ new FlatBufService(parser_.services_.vec[i]));
+ }
+
+ std::unique_ptr<grpc_generator::Printer> CreatePrinter(
+ std::string *str, const char indentation_type = ' ') const {
+ return std::unique_ptr<grpc_generator::Printer>(
+ new FlatBufPrinter(str, indentation_type));
+ }
+
+ private:
+ const Parser &parser_;
+ const std::string &file_name_;
+ const Language language_;
+};
+
+class GoGRPCGenerator : public flatbuffers::BaseGenerator {
+ public:
+ GoGRPCGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "" /*Unused*/, "go"),
+ parser_(parser),
+ path_(path),
+ file_name_(file_name) {}
+
+ bool generate() {
+ FlatBufFile file(parser_, file_name_, FlatBufFile::kLanguageGo);
+ grpc_go_generator::Parameters p;
+ p.custom_method_io_type = "flatbuffers.Builder";
+ for (int i = 0; i < file.service_count(); i++) {
+ auto service = file.service(i);
+ const Definition *def = parser_.services_.vec[i];
+ p.package_name = LastNamespacePart(*(def->defined_namespace));
+ p.service_prefix =
+ def->defined_namespace->GetFullyQualifiedName(""); // file.package();
+ std::string output =
+ grpc_go_generator::GenerateServiceSource(&file, service.get(), &p);
+ std::string filename =
+ NamespaceDir(*def->defined_namespace) + def->name + "_grpc.go";
+ if (!flatbuffers::SaveFile(filename.c_str(), output, false)) return false;
+ }
+ return true;
+ }
+
+ protected:
+ const Parser &parser_;
+ const std::string &path_, &file_name_;
+};
+
+bool GenerateGoGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+ return GoGRPCGenerator(parser, path, file_name).generate();
+}
+
+bool GenerateCppGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+
+ grpc_cpp_generator::Parameters generator_parameters;
+ // TODO(wvo): make the other parameters in this struct configurable.
+ generator_parameters.use_system_headers = true;
+
+ FlatBufFile fbfile(parser, file_name, FlatBufFile::kLanguageCpp);
+
+ std::string header_code =
+ grpc_cpp_generator::GetHeaderPrologue(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetHeaderIncludes(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetHeaderServices(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetHeaderEpilogue(&fbfile, generator_parameters);
+
+ std::string source_code =
+ grpc_cpp_generator::GetSourcePrologue(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetSourceIncludes(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetSourceServices(&fbfile, generator_parameters) +
+ grpc_cpp_generator::GetSourceEpilogue(&fbfile, generator_parameters);
+
+ return flatbuffers::SaveFile((path + file_name + ".grpc.fb.h").c_str(),
+ header_code, false) &&
+ flatbuffers::SaveFile((path + file_name + ".grpc.fb.cc").c_str(),
+ source_code, false);
+}
+
+class JavaGRPCGenerator : public flatbuffers::BaseGenerator {
+ public:
+ JavaGRPCGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "." /*separator*/, "java") {}
+
+ bool generate() {
+ FlatBufFile file(parser_, file_name_, FlatBufFile::kLanguageJava);
+ grpc_java_generator::Parameters p;
+ for (int i = 0; i < file.service_count(); i++) {
+ auto service = file.service(i);
+ const Definition *def = parser_.services_.vec[i];
+ p.package_name =
+ def->defined_namespace->GetFullyQualifiedName(""); // file.package();
+ std::string output =
+ grpc_java_generator::GenerateServiceSource(&file, service.get(), &p);
+ std::string filename =
+ NamespaceDir(*def->defined_namespace) + def->name + "Grpc.java";
+ if (!flatbuffers::SaveFile(filename.c_str(), output, false)) return false;
+ }
+ return true;
+ }
+};
+
+bool GenerateJavaGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+ return JavaGRPCGenerator(parser, path, file_name).generate();
+}
+
+class PythonGRPCGenerator : public flatbuffers::BaseGenerator {
+ private:
+ CodeWriter code_;
+
+ public:
+ PythonGRPCGenerator(const Parser &parser, const std::string &filename)
+ : BaseGenerator(parser, "", filename, "", "" /*Unused*/, "swift") {}
+
+ bool generate() {
+ code_.Clear();
+ code_ +=
+ "# Generated by the gRPC Python protocol compiler plugin. "
+ "DO NOT EDIT!\n";
+ code_ += "import grpc\n";
+
+ FlatBufFile file(parser_, file_name_, FlatBufFile::kLanguagePython);
+
+ for (int i = 0; i < file.service_count(); i++) {
+ auto service = file.service(i);
+ code_ += grpc_python_generator::Generate(&file, service.get());
+ }
+ const auto final_code = code_.ToString();
+ const auto filename = GenerateFileName();
+ return SaveFile(filename.c_str(), final_code, false);
+ }
+
+ std::string GenerateFileName() {
+ std::string namespace_dir;
+ auto &namespaces = parser_.namespaces_.back()->components;
+ for (auto it = namespaces.begin(); it != namespaces.end(); ++it) {
+ if (it != namespaces.begin()) namespace_dir += kPathSeparator;
+ namespace_dir += *it;
+ }
+ std::string grpc_py_filename = namespace_dir;
+ if (!namespace_dir.empty()) grpc_py_filename += kPathSeparator;
+ return grpc_py_filename + file_name_ + "_grpc_fb.py";
+ }
+};
+
+bool GeneratePythonGRPC(const Parser &parser, const std::string & /*path*/,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+
+ return PythonGRPCGenerator(parser, file_name).generate();
+}
+
+class SwiftGRPCGenerator : public flatbuffers::BaseGenerator {
+ private:
+ CodeWriter code_;
+
+ public:
+ SwiftGRPCGenerator(const Parser &parser, const std::string &path,
+ const std::string &filename)
+ : BaseGenerator(parser, path, filename, "", "" /*Unused*/, "swift") {}
+
+ bool generate() {
+ code_.Clear();
+ code_ += "// Generated GRPC code for FlatBuffers swift!";
+ code_ += grpc_swift_generator::GenerateHeader();
+ FlatBufFile file(parser_, file_name_, FlatBufFile::kLanguageSwift);
+ for (int i = 0; i < file.service_count(); i++) {
+ auto service = file.service(i);
+ code_ += grpc_swift_generator::Generate(&file, service.get());
+ }
+ const auto final_code = code_.ToString();
+ const auto filename = GeneratedFileName(path_, file_name_);
+ return SaveFile(filename.c_str(), final_code, false);
+ }
+
+ static std::string GeneratedFileName(const std::string &path,
+ const std::string &file_name) {
+ return path + file_name + ".grpc.swift";
+ }
+};
+
+bool GenerateSwiftGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+ return SwiftGRPCGenerator(parser, path, file_name).generate();
+}
+
+class TSGRPCGenerator : public flatbuffers::BaseGenerator {
+ private:
+ CodeWriter code_;
+
+ public:
+ TSGRPCGenerator(const Parser &parser, const std::string &path,
+ const std::string &filename)
+ : BaseGenerator(parser, path, filename, "", "" /*Unused*/, "ts") {}
+
+ bool generate() {
+ code_.Clear();
+ FlatBufFile file(parser_, file_name_, FlatBufFile::kLanguageTS);
+
+ for (int i = 0; i < file.service_count(); i++) {
+ auto service = file.service(i);
+ code_ += grpc_ts_generator::Generate(&file, service.get(), file_name_);
+ const auto ts_name = GeneratedFileName(path_, file_name_);
+ if (!SaveFile(ts_name.c_str(), code_.ToString(), false)) return false;
+
+ code_.Clear();
+ code_ += grpc_ts_generator::GenerateInterface(&file, service.get(),
+ file_name_);
+ const auto ts_interface_name = GeneratedFileName(path_, file_name_, true);
+ if (!SaveFile(ts_interface_name.c_str(), code_.ToString(), false))
+ return false;
+ }
+ return true;
+ }
+
+ static std::string GeneratedFileName(const std::string &path,
+ const std::string &file_name,
+ const bool is_interface = false) {
+ if (is_interface) return path + file_name + "_grpc.d.ts";
+ return path + file_name + "_grpc.js";
+ }
+};
+
+bool GenerateTSGRPC(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ int nservices = 0;
+ for (auto it = parser.services_.vec.begin(); it != parser.services_.vec.end();
+ ++it) {
+ if (!(*it)->generated) nservices++;
+ }
+ if (!nservices) return true;
+ return TSGRPCGenerator(parser, path, file_name).generate();
+}
+
+} // namespace flatbuffers
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
diff --git a/contrib/libs/flatbuffers/src/idl_gen_java.cpp b/contrib/libs/flatbuffers/src/idl_gen_java.cpp
new file mode 100644
index 0000000000..cfd3a55cdb
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_java.cpp
@@ -0,0 +1,1244 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+#if defined(FLATBUFFERS_CPP98_STL)
+# include <cctype>
+#endif // defined(FLATBUFFERS_CPP98_STL)
+
+namespace flatbuffers {
+namespace java {
+
+static TypedFloatConstantGenerator JavaFloatGen("Double.", "Float.", "NaN",
+ "POSITIVE_INFINITY",
+ "NEGATIVE_INFINITY");
+
+static CommentConfig comment_config = {
+ "/**",
+ " *",
+ " */",
+};
+
+class JavaGenerator : public BaseGenerator {
+ public:
+ JavaGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", ".", "java"),
+ cur_name_space_(nullptr) {}
+
+ JavaGenerator &operator=(const JavaGenerator &);
+ bool generate() {
+ std::string one_file_code;
+ cur_name_space_ = parser_.current_namespace_;
+
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ std::string enumcode;
+ auto &enum_def = **it;
+ if (!parser_.opts.one_file) cur_name_space_ = enum_def.defined_namespace;
+ GenEnum(enum_def, &enumcode);
+ if (parser_.opts.one_file) {
+ one_file_code += enumcode;
+ } else {
+ if (!SaveType(enum_def.name, *enum_def.defined_namespace, enumcode,
+ /* needs_includes= */ false))
+ return false;
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ std::string declcode;
+ auto &struct_def = **it;
+ if (!parser_.opts.one_file)
+ cur_name_space_ = struct_def.defined_namespace;
+ GenStruct(struct_def, &declcode);
+ if (parser_.opts.one_file) {
+ one_file_code += declcode;
+ } else {
+ if (!SaveType(struct_def.name, *struct_def.defined_namespace, declcode,
+ /* needs_includes= */ true))
+ return false;
+ }
+ }
+
+ if (parser_.opts.one_file) {
+ return SaveType(file_name_, *parser_.current_namespace_, one_file_code,
+ /* needs_includes= */ true);
+ }
+ return true;
+ }
+
+ // Save out the generated code for a single class while adding
+ // declaration boilerplate.
+ bool SaveType(const std::string &defname, const Namespace &ns,
+ const std::string &classcode, bool needs_includes) const {
+ if (!classcode.length()) return true;
+
+ std::string code;
+ code = "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ std::string namespace_name = FullNamespace(".", ns);
+ if (!namespace_name.empty()) {
+ code += "package " + namespace_name + ";";
+ code += "\n\n";
+ }
+ if (needs_includes) {
+ code +=
+ "import java.nio.*;\nimport java.lang.*;\nimport "
+ "java.util.*;\nimport com.google.flatbuffers.*;\n";
+ if (parser_.opts.gen_nullable) {
+ code += "\nimport javax.annotation.Nullable;\n";
+ }
+ if (parser_.opts.java_checkerframework) {
+ code += "\nimport org.checkerframework.dataflow.qual.Pure;\n";
+ }
+ code += "\n";
+ }
+
+ code += classcode;
+ if (!namespace_name.empty()) code += "";
+ auto filename = NamespaceDir(ns) + defname + ".java";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+ std::string GenNullableAnnotation(const Type &t) const {
+ return parser_.opts.gen_nullable &&
+ !IsScalar(DestinationType(t, true).base_type) &&
+ t.base_type != BASE_TYPE_VECTOR
+ ? " @Nullable "
+ : "";
+ }
+
+ std::string GenPureAnnotation(const Type &t) const {
+ return parser_.opts.java_checkerframework &&
+ !IsScalar(DestinationType(t, true).base_type)
+ ? " @Pure "
+ : "";
+ }
+
+ std::string GenTypeBasic(const Type &type) const {
+ // clang-format off
+ static const char * const java_typename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, ...) \
+ #JTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return java_typename[type.base_type];
+ }
+
+ std::string GenTypePointer(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return WrapInNameSpace(*type.struct_def);
+ case BASE_TYPE_UNION: FLATBUFFERS_FALLTHROUGH(); // else fall thru
+ default: return "Table";
+ }
+ }
+
+ std::string GenTypeGet(const Type &type) const {
+ return IsScalar(type.base_type)
+ ? GenTypeBasic(type)
+ : (IsArray(type) ? GenTypeGet(type.VectorType())
+ : GenTypePointer(type));
+ }
+
+ // Find the destination type the user wants to receive the value in (e.g.
+ // one size higher signed types for unsigned serialized values in Java).
+ Type DestinationType(const Type &type, bool vectorelem) const {
+ switch (type.base_type) {
+ // We use int for both uchar/ushort, since that generally means less
+ // casting than using short for uchar.
+ case BASE_TYPE_UCHAR: return Type(BASE_TYPE_INT);
+ case BASE_TYPE_USHORT: return Type(BASE_TYPE_INT);
+ case BASE_TYPE_UINT: return Type(BASE_TYPE_LONG);
+ case BASE_TYPE_ARRAY:
+ case BASE_TYPE_VECTOR:
+ if (vectorelem) return DestinationType(type.VectorType(), vectorelem);
+ FLATBUFFERS_FALLTHROUGH(); // else fall thru
+ default: return type;
+ }
+ }
+
+ std::string GenOffsetType() const { return "int"; }
+
+ std::string GenOffsetConstruct(const std::string &variable_name) const {
+ return variable_name;
+ }
+
+ std::string GenVectorOffsetType() const { return "int"; }
+
+ // Generate destination type name
+ std::string GenTypeNameDest(const Type &type) const {
+ return GenTypeGet(DestinationType(type, true));
+ }
+
+ // Mask to turn serialized value into destination type value.
+ std::string DestinationMask(const Type &type, bool vectorelem) const {
+ switch (type.base_type) {
+ case BASE_TYPE_UCHAR: return " & 0xFF";
+ case BASE_TYPE_USHORT: return " & 0xFFFF";
+ case BASE_TYPE_UINT: return " & 0xFFFFFFFFL";
+ case BASE_TYPE_VECTOR:
+ if (vectorelem) return DestinationMask(type.VectorType(), vectorelem);
+ FLATBUFFERS_FALLTHROUGH(); // else fall thru
+ default: return "";
+ }
+ }
+
+ // Casts necessary to correctly read serialized data
+ std::string DestinationCast(const Type &type) const {
+ if (IsSeries(type)) {
+ return DestinationCast(type.VectorType());
+ } else {
+ // Cast necessary to correctly read serialized unsigned values.
+ if (type.base_type == BASE_TYPE_UINT) return "(long)";
+ }
+ return "";
+ }
+
+ // Cast statements for mutator method parameters.
+ // In Java, parameters representing unsigned numbers need to be cast down to
+ // their respective type. For example, a long holding an unsigned int value
+ // would be cast down to int before being put onto the buffer.
+ std::string SourceCast(const Type &type, bool castFromDest) const {
+ if (IsSeries(type)) {
+ return SourceCast(type.VectorType(), castFromDest);
+ } else {
+ if (castFromDest) {
+ if (type.base_type == BASE_TYPE_UINT)
+ return "(int)";
+ else if (type.base_type == BASE_TYPE_USHORT)
+ return "(short)";
+ else if (type.base_type == BASE_TYPE_UCHAR)
+ return "(byte)";
+ }
+ }
+ return "";
+ }
+
+ std::string SourceCast(const Type &type) const {
+ return SourceCast(type, true);
+ }
+
+ std::string SourceCastBasic(const Type &type, bool castFromDest) const {
+ return IsScalar(type.base_type) ? SourceCast(type, castFromDest) : "";
+ }
+
+ std::string SourceCastBasic(const Type &type) const {
+ return SourceCastBasic(type, true);
+ }
+
+ std::string GenEnumDefaultValue(const FieldDef &field) const {
+ auto &value = field.value;
+ FLATBUFFERS_ASSERT(value.type.enum_def);
+ auto &enum_def = *value.type.enum_def;
+ auto enum_val = enum_def.FindByValue(value.constant);
+ return enum_val ? (WrapInNameSpace(enum_def) + "." + enum_val->name)
+ : value.constant;
+ }
+
+ std::string GenDefaultValue(const FieldDef &field) const {
+ auto &value = field.value;
+ auto constant = field.IsScalarOptional() ? "0" : value.constant;
+ auto longSuffix = "L";
+ switch (value.type.base_type) {
+ case BASE_TYPE_BOOL: return constant == "0" ? "false" : "true";
+ case BASE_TYPE_ULONG: {
+ // Converts the ulong into its bits signed equivalent
+ uint64_t defaultValue = StringToUInt(constant.c_str());
+ return NumToString(static_cast<int64_t>(defaultValue)) + longSuffix;
+ }
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_LONG: return constant + longSuffix;
+ default:
+ if (IsFloat(value.type.base_type)) {
+ if (field.IsScalarOptional()) {
+ return value.type.base_type == BASE_TYPE_DOUBLE ? "0.0" : "0f";
+ }
+ return JavaFloatGen.GenFloatConstant(field);
+ } else {
+ return constant;
+ }
+ }
+ }
+
+ std::string GenDefaultValueBasic(const FieldDef &field) const {
+ auto &value = field.value;
+ if (!IsScalar(value.type.base_type)) { return "0"; }
+ return GenDefaultValue(field);
+ }
+
+ void GenEnum(EnumDef &enum_def, std::string *code_ptr) const {
+ std::string &code = *code_ptr;
+ if (enum_def.generated) return;
+
+ // Generate enum definitions of the form:
+ // public static (final) int name = value;
+ // In Java, we use ints rather than the Enum feature, because we want them
+ // to map directly to how they're used in C/C++ and file formats.
+ // That, and Java Enums are expensive, and not universally liked.
+ GenComment(enum_def.doc_comment, code_ptr, &comment_config);
+
+ if (enum_def.attributes.Lookup("private")) {
+ // For Java, we leave the enum unmarked to indicate package-private
+ } else {
+ code += "public ";
+ }
+ code += "final class " + enum_def.name;
+ code += " {\n";
+ code += " private " + enum_def.name + "() { }\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, &comment_config, " ");
+ code += " public static final ";
+ code += GenTypeBasic(DestinationType(enum_def.underlying_type, false));
+ code += " ";
+ code += ev.name + " = ";
+ code += enum_def.ToString(ev);
+ code += ";\n";
+ }
+
+ // Generate a generate string table for enum values.
+ // Problem is, if values are very sparse that could generate really big
+ // tables. Ideally in that case we generate a map lookup instead, but for
+ // the moment we simply don't output a table at all.
+ auto range = enum_def.Distance();
+ // Average distance between values above which we consider a table
+ // "too sparse". Change at will.
+ static const uint64_t kMaxSparseness = 5;
+ if (range / static_cast<uint64_t>(enum_def.size()) < kMaxSparseness) {
+ code += "\n public static final String";
+ code += "[] names = { ";
+ auto val = enum_def.Vals().front();
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ auto ev = *it;
+ for (auto k = enum_def.Distance(val, ev); k > 1; --k) code += "\"\", ";
+ val = ev;
+ code += "\"" + (*it)->name + "\", ";
+ }
+ code += "};\n\n";
+ code += " public static ";
+ code += "String";
+ code += " " + MakeCamel("name", false);
+ code += "(int e) { return names[e";
+ if (enum_def.MinValue()->IsNonZero())
+ code += " - " + enum_def.MinValue()->name;
+ code += "]; }\n";
+ }
+
+ // Close the class
+ code += "}\n\n";
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetter(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "__string";
+ case BASE_TYPE_STRUCT: return "__struct";
+ case BASE_TYPE_UNION: return "__union";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ case BASE_TYPE_ARRAY: return GenGetter(type.VectorType());
+ default: {
+ std::string getter = "bb.get";
+ if (type.base_type == BASE_TYPE_BOOL) {
+ getter = "0!=" + getter;
+ } else if (GenTypeBasic(type) != "byte") {
+ getter += MakeCamel(GenTypeBasic(type));
+ }
+ return getter;
+ }
+ }
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetterForLookupByKey(flatbuffers::FieldDef *key_field,
+ const std::string &data_buffer,
+ const char *num = nullptr) const {
+ auto type = key_field->value.type;
+ auto dest_mask = DestinationMask(type, true);
+ auto dest_cast = DestinationCast(type);
+ auto getter = data_buffer + ".get";
+ if (GenTypeBasic(type) != "byte") {
+ getter += MakeCamel(GenTypeBasic(type));
+ }
+ getter = dest_cast + getter + "(" + GenOffsetGetter(key_field, num) + ")" +
+ dest_mask;
+ return getter;
+ }
+
+ // Direct mutation is only allowed for scalar fields.
+ // Hence a setter method will only be generated for such fields.
+ std::string GenSetter(const Type &type) const {
+ if (IsScalar(type.base_type)) {
+ std::string setter = "bb.put";
+ if (GenTypeBasic(type) != "byte" && type.base_type != BASE_TYPE_BOOL) {
+ setter += MakeCamel(GenTypeBasic(type));
+ }
+ return setter;
+ } else {
+ return "";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const Type &type) const {
+ return IsScalar(type.base_type) ? MakeCamel(GenTypeBasic(type))
+ : (IsStruct(type) ? "Struct" : "Offset");
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void GenStructArgs(const StructDef &struct_def, std::string *code_ptr,
+ const char *nameprefix, size_t array_count = 0) const {
+ std::string &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ const auto array_field = IsArray(field_type);
+ const auto &type = array_field ? field_type.VectorType()
+ : DestinationType(field_type, false);
+ const auto array_cnt = array_field ? (array_count + 1) : array_count;
+ if (IsStruct(type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ GenStructArgs(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), array_cnt);
+ } else {
+ code += ", ";
+ code += GenTypeBasic(type);
+ for (size_t i = 0; i < array_cnt; i++) code += "[]";
+ code += " ";
+ code += nameprefix;
+ code += MakeCamel(field.name, false);
+ }
+ }
+ }
+
+ // Recusively generate struct construction statements of the form:
+ // builder.putType(name);
+ // and insert manual padding.
+ void GenStructBody(const StructDef &struct_def, std::string *code_ptr,
+ const char *nameprefix, size_t index = 0,
+ bool in_array = false) const {
+ std::string &code = *code_ptr;
+ std::string indent((index + 1) * 2, ' ');
+ code += indent + " builder.prep(";
+ code += NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ");\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ if (field.padding) {
+ code += indent + " builder.pad(";
+ code += NumToString(field.padding) + ");\n";
+ }
+ if (IsStruct(field_type)) {
+ GenStructBody(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), index,
+ in_array);
+ } else {
+ const auto &type =
+ IsArray(field_type) ? field_type.VectorType() : field_type;
+ const auto index_var = "_idx" + NumToString(index);
+ if (IsArray(field_type)) {
+ code += indent + " for (int " + index_var + " = ";
+ code += NumToString(field_type.fixed_length);
+ code += "; " + index_var + " > 0; " + index_var + "--) {\n";
+ in_array = true;
+ }
+ if (IsStruct(type)) {
+ GenStructBody(*field_type.struct_def, code_ptr,
+ (nameprefix + (field.name + "_")).c_str(), index + 1,
+ in_array);
+ } else {
+ code += IsArray(field_type) ? " " : "";
+ code += indent + " builder.put";
+ code += GenMethod(type) + "(";
+ code += SourceCast(type);
+ auto argname = nameprefix + MakeCamel(field.name, false);
+ code += argname;
+ size_t array_cnt = index + (IsArray(field_type) ? 1 : 0);
+ for (size_t i = 0; in_array && i < array_cnt; i++) {
+ code += "[_idx" + NumToString(i) + "-1]";
+ }
+ code += ");\n";
+ }
+ if (IsArray(field_type)) { code += indent + " }\n"; }
+ }
+ }
+ }
+
+ std::string GenByteBufferLength(const char *bb_name) const {
+ std::string bb_len = bb_name;
+ bb_len += ".capacity()";
+ return bb_len;
+ }
+
+ std::string GenOffsetGetter(flatbuffers::FieldDef *key_field,
+ const char *num = nullptr) const {
+ std::string key_offset = "";
+ key_offset += "__offset(" + NumToString(key_field->value.offset) + ", ";
+ if (num) {
+ key_offset += num;
+ key_offset += ", _bb)";
+ } else {
+ key_offset += GenByteBufferLength("bb");
+ key_offset += " - tableOffset, bb)";
+ }
+ return key_offset;
+ }
+
+ std::string GenLookupKeyGetter(flatbuffers::FieldDef *key_field) const {
+ std::string key_getter = " ";
+ key_getter += "int tableOffset = ";
+ key_getter += "__indirect(vectorLocation + 4 * (start + middle)";
+ key_getter += ", bb);\n ";
+ if (IsString(key_field->value.type)) {
+ key_getter += "int comp = ";
+ key_getter += "compareStrings(";
+ key_getter += GenOffsetGetter(key_field);
+ key_getter += ", byteKey, bb);\n";
+ } else {
+ auto get_val = GenGetterForLookupByKey(key_field, "bb");
+ key_getter += GenTypeNameDest(key_field->value.type) + " val = ";
+ key_getter += get_val + ";\n";
+ key_getter += " int comp = val > key ? 1 : val < key ? -1 : 0;\n";
+ }
+ return key_getter;
+ }
+
+ std::string GenKeyGetter(flatbuffers::FieldDef *key_field) const {
+ std::string key_getter = "";
+ auto data_buffer = "_bb";
+ if (IsString(key_field->value.type)) {
+ key_getter += " return ";
+ key_getter += "";
+ key_getter += "compareStrings(";
+ key_getter += GenOffsetGetter(key_field, "o1") + ", ";
+ key_getter += GenOffsetGetter(key_field, "o2") + ", " + data_buffer + ")";
+ key_getter += ";";
+ } else {
+ auto field_getter = GenGetterForLookupByKey(key_field, data_buffer, "o1");
+ key_getter +=
+ "\n " + GenTypeNameDest(key_field->value.type) + " val_1 = ";
+ key_getter +=
+ field_getter + ";\n " + GenTypeNameDest(key_field->value.type);
+ key_getter += " val_2 = ";
+ field_getter = GenGetterForLookupByKey(key_field, data_buffer, "o2");
+ key_getter += field_getter + ";\n";
+ key_getter += " return val_1 > val_2 ? 1 : val_1 < val_2 ? -1 : 0;\n ";
+ }
+ return key_getter;
+ }
+
+ void GenStruct(StructDef &struct_def, std::string *code_ptr) const {
+ if (struct_def.generated) return;
+ std::string &code = *code_ptr;
+
+ // Generate a struct accessor class, with methods of the form:
+ // public type name() { return bb.getType(i + offset); }
+ // or for tables of the form:
+ // public type name() {
+ // int o = __offset(offset); return o != 0 ? bb.getType(o + i) : default;
+ // }
+ GenComment(struct_def.doc_comment, code_ptr, &comment_config);
+
+ if (parser_.opts.gen_generated) {
+ code += "@javax.annotation.Generated(value=\"flatc\")\n";
+ }
+ code += "@SuppressWarnings(\"unused\")\n";
+ if (struct_def.attributes.Lookup("private")) {
+ // For Java, we leave the struct unmarked to indicate package-private
+ } else {
+ code += "public ";
+ }
+ code += "final class " + struct_def.name;
+ code += " extends ";
+ code += struct_def.fixed ? "Struct" : "Table";
+ code += " {\n";
+
+ if (!struct_def.fixed) {
+ // Generate verson check method.
+ // Force compile time error if not using the same version runtime.
+ code += " public static void ValidateVersion() {";
+ code += " Constants.";
+ code += "FLATBUFFERS_2_0_0(); ";
+ code += "}\n";
+
+ // Generate a special accessor for the table that when used as the root
+ // of a FlatBuffer
+ std::string method_name = "getRootAs" + struct_def.name;
+ std::string method_signature =
+ " public static " + struct_def.name + " " + method_name;
+
+ // create convenience method that doesn't require an existing object
+ code += method_signature + "(ByteBuffer _bb) ";
+ code += "{ return " + method_name + "(_bb, new " + struct_def.name +
+ "()); }\n";
+
+ // create method that allows object reuse
+ code +=
+ method_signature + "(ByteBuffer _bb, " + struct_def.name + " obj) { ";
+ code += "_bb.order(ByteOrder.LITTLE_ENDIAN); ";
+ code += "return (obj.__assign(_bb.getInt(_bb.";
+ code += "position()";
+ code += ") + _bb.";
+ code += "position()";
+ code += ", _bb)); }\n";
+ if (parser_.root_struct_def_ == &struct_def) {
+ if (parser_.file_identifier_.length()) {
+ // Check if a buffer has the identifier.
+ code += " public static ";
+ code += "boolean " + struct_def.name;
+ code += "BufferHasIdentifier(ByteBuffer _bb) { return ";
+ code += "__has_identifier(_bb, \"";
+ code += parser_.file_identifier_;
+ code += "\"); }\n";
+ }
+ }
+ }
+ // Generate the __init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ code += " public void __init(int _i, ByteBuffer _bb) ";
+ code += "{ ";
+ code += "__reset(_i, _bb); ";
+ code += "}\n";
+ code +=
+ " public " + struct_def.name + " __assign(int _i, ByteBuffer _bb) ";
+ code += "{ __init(_i, _bb); return this; }\n\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ GenComment(field.doc_comment, code_ptr, &comment_config, " ");
+ std::string type_name = GenTypeGet(field.value.type);
+ std::string type_name_dest = GenTypeNameDest(field.value.type);
+ std::string conditional_cast = "";
+ std::string optional = "";
+ std::string dest_mask = DestinationMask(field.value.type, true);
+ std::string dest_cast = DestinationCast(field.value.type);
+ std::string src_cast = SourceCast(field.value.type);
+ std::string method_start =
+ " public " +
+ (field.IsRequired() ? "" : GenNullableAnnotation(field.value.type)) +
+ GenPureAnnotation(field.value.type) + type_name_dest + optional +
+ " " + MakeCamel(field.name, false);
+ std::string obj = "obj";
+
+ // Most field accessors need to retrieve and test the field offset first,
+ // this is the prefix code for that:
+ auto offset_prefix =
+ IsArray(field.value.type)
+ ? " { return "
+ : (" { int o = __offset(" + NumToString(field.value.offset) +
+ "); return o != 0 ? ");
+ // Generate the accessors that don't do object reuse.
+ if (field.value.type.base_type == BASE_TYPE_STRUCT) {
+ // Calls the accessor that takes an accessor object with a new object.
+ code += method_start + "() { return ";
+ code += MakeCamel(field.name, false);
+ code += "(new ";
+ code += type_name + "()); }\n";
+ } else if (IsVector(field.value.type) &&
+ field.value.type.element == BASE_TYPE_STRUCT) {
+ // Accessors for vectors of structs also take accessor objects, this
+ // generates a variant without that argument.
+ code += method_start + "(int j) { return ";
+ code += MakeCamel(field.name, false);
+ code += "(new " + type_name + "(), j); }\n";
+ }
+
+ if (field.IsScalarOptional()) { code += GenOptionalScalarCheck(field); }
+ std::string getter = dest_cast + GenGetter(field.value.type);
+ code += method_start;
+ std::string default_cast = "";
+ std::string member_suffix = "; ";
+ if (IsScalar(field.value.type.base_type)) {
+ code += "()";
+ member_suffix += "";
+ if (struct_def.fixed) {
+ code += " { return " + getter;
+ code += "(bb_pos + ";
+ code += NumToString(field.value.offset) + ")";
+ code += dest_mask;
+ } else {
+ code += offset_prefix + getter;
+ code += "(o + bb_pos)" + dest_mask;
+ code += " : " + default_cast;
+ code += GenDefaultValue(field);
+ }
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ code += "(" + type_name + " obj)";
+ if (struct_def.fixed) {
+ code += " { return " + obj + ".__assign(";
+ code += "bb_pos + " + NumToString(field.value.offset) + ", ";
+ code += "bb)";
+ } else {
+ code += offset_prefix + conditional_cast;
+ code += obj + ".__assign(";
+ code += field.value.type.struct_def->fixed
+ ? "o + bb_pos"
+ : "__indirect(o + bb_pos)";
+ code += ", bb) : null";
+ }
+ break;
+ case BASE_TYPE_STRING:
+ code += "()";
+ member_suffix += "";
+ code += offset_prefix + getter + "(o + ";
+ code += "bb_pos) : null";
+ break;
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ code += "(";
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += type_name + " obj, ";
+ getter = obj + ".__assign";
+ } else if (vectortype.base_type == BASE_TYPE_UNION) {
+ code += type_name + " obj, ";
+ }
+ code += "int j)";
+ const auto body = offset_prefix + conditional_cast + getter + "(";
+ if (vectortype.base_type == BASE_TYPE_UNION) {
+ code += body + "obj, ";
+ } else {
+ code += body;
+ }
+ std::string index;
+ if (IsArray(field.value.type)) {
+ index += "bb_pos + " + NumToString(field.value.offset) + " + ";
+ } else {
+ index += "__vector(o) + ";
+ }
+ index += "j * " + NumToString(InlineSize(vectortype));
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += vectortype.struct_def->fixed
+ ? index
+ : "__indirect(" + index + ")";
+ code += ", bb";
+ } else {
+ code += index;
+ }
+ code += ")" + dest_mask;
+ if (!IsArray(field.value.type)) {
+ code += " : ";
+ code +=
+ field.value.type.element == BASE_TYPE_BOOL
+ ? "false"
+ : (IsScalar(field.value.type.element) ? default_cast + "0"
+ : "null");
+ }
+
+ break;
+ }
+ case BASE_TYPE_UNION:
+ code += "(" + type_name + " obj)" + offset_prefix + getter;
+ code += "(obj, o + bb_pos) : null";
+ break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ code += member_suffix;
+ code += "}\n";
+ if (IsVector(field.value.type)) {
+ code += " public int " + MakeCamel(field.name, false);
+ code += "Length";
+ code += "()";
+ code += offset_prefix;
+ code += "__vector_len(o) : 0; ";
+ code += "";
+ code += "}\n";
+ // See if we should generate a by-key accessor.
+ if (field.value.type.element == BASE_TYPE_STRUCT &&
+ !field.value.type.struct_def->fixed) {
+ auto &sd = *field.value.type.struct_def;
+ auto &fields = sd.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &key_field = **kit;
+ if (key_field.key) {
+ auto qualified_name = WrapInNameSpace(sd);
+ code += " public " + qualified_name + " ";
+ code += MakeCamel(field.name, false) + "ByKey(";
+ code += GenTypeNameDest(key_field.value.type) + " key)";
+ code += offset_prefix;
+ code += qualified_name + ".__lookup_by_key(";
+ code += "null, ";
+ code += "__vector(o), key, ";
+ code += "bb) : null; ";
+ code += "}\n";
+ code += " public " + qualified_name + " ";
+ code += MakeCamel(field.name, false) + "ByKey(";
+ code += qualified_name + " obj, ";
+ code += GenTypeNameDest(key_field.value.type) + " key)";
+ code += offset_prefix;
+ code += qualified_name + ".__lookup_by_key(obj, ";
+ code += "__vector(o), key, ";
+ code += "bb) : null; ";
+ code += "}\n";
+ break;
+ }
+ }
+ }
+ }
+ // Generate the accessors for vector of structs with vector access object
+ if (IsVector(field.value.type)) {
+ std::string vector_type_name;
+ const auto &element_base_type = field.value.type.VectorType().base_type;
+ if (IsScalar(element_base_type)) {
+ vector_type_name = MakeCamel(type_name, true) + "Vector";
+ } else if (element_base_type == BASE_TYPE_STRING) {
+ vector_type_name = "StringVector";
+ } else if (element_base_type == BASE_TYPE_UNION) {
+ vector_type_name = "UnionVector";
+ } else {
+ vector_type_name = type_name + ".Vector";
+ }
+ auto vector_method_start = GenNullableAnnotation(field.value.type) +
+ " public " + vector_type_name + optional +
+ " " + MakeCamel(field.name, false) +
+ "Vector";
+ code += vector_method_start + "() { return ";
+ code += MakeCamel(field.name, false) + "Vector";
+ code += "(new " + vector_type_name + "()); }\n";
+ code += vector_method_start + "(" + vector_type_name + " obj)";
+ code += offset_prefix + conditional_cast + obj + ".__assign(";
+ code += "__vector(o), ";
+ if (!IsScalar(element_base_type)) {
+ auto vectortype = field.value.type.VectorType();
+ code += NumToString(InlineSize(vectortype)) + ", ";
+ }
+ code += "bb) : null" + member_suffix + "}\n";
+ }
+ // Generate a ByteBuffer accessor for strings & vectors of scalars.
+ if ((IsVector(field.value.type) &&
+ IsScalar(field.value.type.VectorType().base_type)) ||
+ IsString(field.value.type)) {
+ code += " public ByteBuffer ";
+ code += MakeCamel(field.name, false);
+ code += "AsByteBuffer() { return ";
+ code += "__vector_as_bytebuffer(";
+ code += NumToString(field.value.offset) + ", ";
+ code += NumToString(IsString(field.value.type)
+ ? 1
+ : InlineSize(field.value.type.VectorType()));
+ code += "); }\n";
+ code += " public ByteBuffer ";
+ code += MakeCamel(field.name, false);
+ code += "InByteBuffer(ByteBuffer _bb) { return ";
+ code += "__vector_in_bytebuffer(_bb, ";
+ code += NumToString(field.value.offset) + ", ";
+ code += NumToString(IsString(field.value.type)
+ ? 1
+ : InlineSize(field.value.type.VectorType()));
+ code += "); }\n";
+ }
+ // generate object accessors if is nested_flatbuffer
+ if (field.nested_flatbuffer) {
+ auto nested_type_name = WrapInNameSpace(*field.nested_flatbuffer);
+ auto nested_method_name =
+ MakeCamel(field.name, false) + "As" + field.nested_flatbuffer->name;
+ auto get_nested_method_name = nested_method_name;
+ code += " public " + nested_type_name + " ";
+ code += nested_method_name + "() { return ";
+ code +=
+ get_nested_method_name + "(new " + nested_type_name + "()); }\n";
+ code += " public " + nested_type_name + " ";
+ code += get_nested_method_name + "(";
+ code += nested_type_name + " obj";
+ code += ") { int o = __offset(";
+ code += NumToString(field.value.offset) + "); ";
+ code += "return o != 0 ? " + conditional_cast + obj + ".__assign(";
+ code += "";
+ code += "__indirect(__vector(o)), ";
+ code += "bb) : null; }\n";
+ }
+ // Generate mutators for scalar fields or vectors of scalars.
+ if (parser_.opts.mutable_buffer) {
+ auto is_series = (IsSeries(field.value.type));
+ const auto &underlying_type =
+ is_series ? field.value.type.VectorType() : field.value.type;
+ // Boolean parameters have to be explicitly converted to byte
+ // representation.
+ auto setter_parameter = underlying_type.base_type == BASE_TYPE_BOOL
+ ? "(byte)(" + field.name + " ? 1 : 0)"
+ : field.name;
+ auto mutator_prefix = MakeCamel("mutate", false);
+ // A vector mutator also needs the index of the vector element it should
+ // mutate.
+ auto mutator_params = (is_series ? "(int j, " : "(") +
+ GenTypeNameDest(underlying_type) + " " +
+ field.name + ") { ";
+ auto setter_index =
+ is_series
+ ? (IsArray(field.value.type)
+ ? "bb_pos + " + NumToString(field.value.offset)
+ : "__vector(o)") +
+ +" + j * " + NumToString(InlineSize(underlying_type))
+ : (struct_def.fixed
+ ? "bb_pos + " + NumToString(field.value.offset)
+ : "o + bb_pos");
+ if (IsScalar(underlying_type.base_type) && !IsUnion(field.value.type)) {
+ code += " public ";
+ code += struct_def.fixed ? "void " : "boolean ";
+ code += mutator_prefix + MakeCamel(field.name, true);
+ code += mutator_params;
+ if (struct_def.fixed) {
+ code += GenSetter(underlying_type) + "(" + setter_index + ", ";
+ code += src_cast + setter_parameter + "); }\n";
+ } else {
+ code += "int o = __offset(";
+ code += NumToString(field.value.offset) + ");";
+ code += " if (o != 0) { " + GenSetter(underlying_type);
+ code += "(" + setter_index + ", " + src_cast + setter_parameter +
+ "); return true; } else { return false; } }\n";
+ }
+ }
+ }
+ if (parser_.opts.java_primitive_has_method &&
+ IsScalar(field.value.type.base_type) && !struct_def.fixed) {
+ auto vt_offset_constant = " public static final int VT_" +
+ MakeScreamingCamel(field.name) + " = " +
+ NumToString(field.value.offset) + ";";
+
+ code += vt_offset_constant;
+ code += "\n";
+ }
+ }
+ code += "\n";
+ flatbuffers::FieldDef *key_field = nullptr;
+ if (struct_def.fixed) {
+ // create a struct constructor function
+ code += " public static " + GenOffsetType() + " ";
+ code += "create";
+ code += struct_def.name + "(FlatBufferBuilder builder";
+ GenStructArgs(struct_def, code_ptr, "");
+ code += ") {\n";
+ GenStructBody(struct_def, code_ptr, "");
+ code += " return ";
+ code += GenOffsetConstruct("builder." + std::string("offset()"));
+ code += ";\n }\n";
+ } else {
+ // Generate a method that creates a table in one go. This is only possible
+ // when the table has no struct fields, since those have to be created
+ // inline, and there's no way to do so in Java.
+ bool has_no_struct_fields = true;
+ int num_fields = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (IsStruct(field.value.type)) {
+ has_no_struct_fields = false;
+ } else {
+ num_fields++;
+ }
+ }
+ // JVM specifications restrict default constructor params to be < 255.
+ // Longs and doubles take up 2 units, so we set the limit to be < 127.
+ if (has_no_struct_fields && num_fields && num_fields < 127) {
+ // Generate a table constructor of the form:
+ // public static int createName(FlatBufferBuilder builder, args...)
+ code += " public static " + GenOffsetType() + " ";
+ code += "create" + struct_def.name;
+ code += "(FlatBufferBuilder builder";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ code += ",\n ";
+ code += GenTypeBasic(DestinationType(field.value.type, false));
+ code += " ";
+ code += field.name;
+ if (!IsScalar(field.value.type.base_type)) code += "Offset";
+ }
+ code += ") {\n builder.";
+ code += "startTable(";
+ code += NumToString(struct_def.fields.vec.size()) + ");\n";
+ for (size_t size = struct_def.sortbysize ? sizeof(largest_scalar_t) : 1;
+ size; size /= 2) {
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated &&
+ (!struct_def.sortbysize ||
+ size == SizeOf(field.value.type.base_type))) {
+ code += " " + struct_def.name + ".";
+ code += "add";
+ code += MakeCamel(field.name) + "(builder, " + field.name;
+ if (!IsScalar(field.value.type.base_type)) code += "Offset";
+ code += ");\n";
+ }
+ }
+ }
+ code += " return " + struct_def.name + ".";
+ code += "end" + struct_def.name;
+ code += "(builder);\n }\n\n";
+ }
+ // Generate a set of static methods that allow table construction,
+ // of the form:
+ // public static void addName(FlatBufferBuilder builder, short name)
+ // { builder.addShort(id, name, default); }
+ // Unlike the Create function, these always work.
+ code += " public static void start";
+ code += struct_def.name;
+ code += "(FlatBufferBuilder builder) { builder.";
+ code += "startTable(";
+ code += NumToString(struct_def.fields.vec.size()) + "); }\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.key) key_field = &field;
+ code += " public static void add";
+ code += MakeCamel(field.name);
+ code += "(FlatBufferBuilder builder, ";
+ code += GenTypeBasic(DestinationType(field.value.type, false));
+ auto argname = MakeCamel(field.name, false);
+ if (!IsScalar(field.value.type.base_type)) argname += "Offset";
+ code += " " + argname + ") { builder.add";
+ code += GenMethod(field.value.type) + "(";
+ code += NumToString(it - struct_def.fields.vec.begin()) + ", ";
+ code += SourceCastBasic(field.value.type);
+ code += argname;
+ code += ", ";
+ code += SourceCastBasic(field.value.type);
+ code += GenDefaultValue(field);
+ code += "); }\n";
+ if (IsVector(field.value.type)) {
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ if (!IsStruct(vector_type)) {
+ // generate a method to create a vector from a java array.
+ if ((vector_type.base_type == BASE_TYPE_CHAR ||
+ vector_type.base_type == BASE_TYPE_UCHAR)) {
+ // Handle byte[] and ByteBuffers separately for Java
+ code += " public static " + GenVectorOffsetType() + " ";
+ code += "create";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, byte[] data) ";
+ code += "{ return builder.createByteVector(data); }\n";
+
+ code += " public static " + GenVectorOffsetType() + " ";
+ code += "create";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, ByteBuffer data) ";
+ code += "{ return builder.createByteVector(data); }\n";
+ } else {
+ code += " public static " + GenVectorOffsetType() + " ";
+ code += "create";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, ";
+ code += GenTypeBasic(vector_type) + "[] data) ";
+ code += "{ builder.startVector(";
+ code += NumToString(elem_size);
+ code += ", data.length, ";
+ code += NumToString(alignment);
+ code += "); for (int i = data.";
+ code += "length - 1; i >= 0; i--) builder.";
+ code += "add";
+ code += GenMethod(vector_type);
+ code += "(";
+ code += SourceCastBasic(vector_type, false);
+ code += "data[i]";
+ code += "); return ";
+ code += "builder.endVector(); }\n";
+ }
+ }
+ // Generate a method to start a vector, data to be added manually
+ // after.
+ code += " public static void start";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder builder, int numElems) ";
+ code += "{ builder.startVector(";
+ code += NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment);
+ code += "); }\n";
+ }
+ }
+ code += " public static " + GenOffsetType() + " ";
+ code += "end" + struct_def.name;
+ code += "(FlatBufferBuilder builder) {\n int o = builder.";
+ code += "endTable();\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code += " builder.required(o, ";
+ code += NumToString(field.value.offset);
+ code += "); // " + field.name + "\n";
+ }
+ }
+ code += " return " + GenOffsetConstruct("o") + ";\n }\n";
+ if (parser_.root_struct_def_ == &struct_def) {
+ std::string size_prefix[] = { "", "SizePrefixed" };
+ for (int i = 0; i < 2; ++i) {
+ code += " public static void ";
+ code += "finish" + size_prefix[i] + struct_def.name;
+ code += "Buffer(FlatBufferBuilder builder, " + GenOffsetType();
+ code += " offset) {";
+ code += " builder.finish" + size_prefix[i] + "(offset";
+
+ if (parser_.file_identifier_.length())
+ code += ", \"" + parser_.file_identifier_ + "\"";
+ code += "); }\n";
+ }
+ }
+ }
+ // Only generate key compare function for table,
+ // because `key_field` is not set for struct
+ if (struct_def.has_key && !struct_def.fixed) {
+ FLATBUFFERS_ASSERT(key_field);
+ code += "\n @Override\n protected int keysCompare(";
+ code += "Integer o1, Integer o2, ByteBuffer _bb) {";
+ code += GenKeyGetter(key_field);
+ code += " }\n";
+
+ code += "\n public static " + struct_def.name;
+ code += " __lookup_by_key(";
+ code += struct_def.name + " obj, ";
+ code += "int vectorLocation, ";
+ code += GenTypeNameDest(key_field->value.type);
+ code += " key, ByteBuffer bb) {\n";
+ if (IsString(key_field->value.type)) {
+ code += " byte[] byteKey = ";
+ code += "key.getBytes(java.nio.charset.StandardCharsets.UTF_8);\n";
+ }
+ code += " int span = ";
+ code += "bb.getInt(vectorLocation - 4);\n";
+ code += " int start = 0;\n";
+ code += " while (span != 0) {\n";
+ code += " int middle = span / 2;\n";
+ code += GenLookupKeyGetter(key_field);
+ code += " if (comp > 0) {\n";
+ code += " span = middle;\n";
+ code += " } else if (comp < 0) {\n";
+ code += " middle++;\n";
+ code += " start += middle;\n";
+ code += " span -= middle;\n";
+ code += " } else {\n";
+ code += " return ";
+ code += "(obj == null ? new " + struct_def.name + "() : obj)";
+ code += ".__assign(tableOffset, bb);\n";
+ code += " }\n }\n";
+ code += " return null;\n";
+ code += " }\n";
+ }
+ GenVectorAccessObject(struct_def, code_ptr);
+ code += "}";
+ code += "\n\n";
+ }
+
+ std::string GenOptionalScalarCheck(FieldDef &field) const {
+ if (!field.IsScalarOptional()) return "";
+ return " public boolean has" + MakeCamel(field.name, true) +
+ "() { return 0 != __offset(" + NumToString(field.value.offset) +
+ "); }\n";
+ }
+
+ void GenVectorAccessObject(StructDef &struct_def,
+ std::string *code_ptr) const {
+ auto &code = *code_ptr;
+ // Generate a vector of structs accessor class.
+ code += "\n";
+ code += " ";
+ if (!struct_def.attributes.Lookup("private")) code += "public ";
+ code += "static ";
+ code += "final ";
+ code += "class Vector extends ";
+ code += "BaseVector {\n";
+
+ // Generate the __assign method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ std::string method_indent = " ";
+ code += method_indent + "public Vector ";
+ code += "__assign(int _vector, int _element_size, ByteBuffer _bb) { ";
+ code += "__reset(_vector, _element_size, _bb); return this; }\n\n";
+
+ auto type_name = struct_def.name;
+ auto method_start = method_indent + "public " + type_name + " get";
+ // Generate the accessors that don't do object reuse.
+ code += method_start + "(int j) { return get";
+ code += "(new " + type_name + "(), j); }\n";
+ code += method_start + "(" + type_name + " obj, int j) { ";
+ code += " return obj.__assign(";
+ std::string index = "__element(j)";
+ code += struct_def.fixed ? index : "__indirect(" + index + ", bb)";
+ code += ", bb); }\n";
+ // See if we should generate a by-key accessor.
+ if (!struct_def.fixed) {
+ auto &fields = struct_def.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &key_field = **kit;
+ if (key_field.key) {
+ auto nullable_annotation =
+ parser_.opts.gen_nullable ? "@Nullable " : "";
+ code += method_indent + nullable_annotation;
+ code += "public " + type_name + " ";
+ code += "getByKey(";
+ code += GenTypeNameDest(key_field.value.type) + " key) { ";
+ code += " return __lookup_by_key(null, ";
+ code += "__vector(), key, ";
+ code += "bb); ";
+ code += "}\n";
+ code += method_indent + nullable_annotation;
+ code += "public " + type_name + " ";
+ code += "getByKey(";
+ code += type_name + " obj, ";
+ code += GenTypeNameDest(key_field.value.type) + " key) { ";
+ code += " return __lookup_by_key(obj, ";
+ code += "__vector(), key, ";
+ code += "bb); ";
+ code += "}\n";
+ break;
+ }
+ }
+ }
+ code += " }\n";
+ }
+
+ // This tracks the current namespace used to determine if a type need to be
+ // prefixed by its namespace
+ const Namespace *cur_name_space_;
+};
+} // namespace java
+
+bool GenerateJava(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ java::JavaGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_json_schema.cpp b/contrib/libs/flatbuffers/src/idl_gen_json_schema.cpp
new file mode 100644
index 0000000000..d58bb84976
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_json_schema.cpp
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <iostream>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+namespace jsons {
+
+template<class T> std::string GenFullName(const T *enum_def) {
+ std::string full_name;
+ const auto &name_spaces = enum_def->defined_namespace->components;
+ for (auto ns = name_spaces.cbegin(); ns != name_spaces.cend(); ++ns) {
+ full_name.append(*ns + "_");
+ }
+ full_name.append(enum_def->name);
+ return full_name;
+}
+
+template<class T> std::string GenTypeRef(const T *enum_def) {
+ return "\"$ref\" : \"#/definitions/" + GenFullName(enum_def) + "\"";
+}
+
+std::string GenType(const std::string &name) {
+ return "\"type\" : \"" + name + "\"";
+}
+
+std::string GenType(BaseType type) {
+ switch (type) {
+ case BASE_TYPE_BOOL: return "\"type\" : \"boolean\"";
+ case BASE_TYPE_CHAR:
+ return "\"type\" : \"integer\", \"minimum\" : " +
+ NumToString(std::numeric_limits<int8_t>::min()) +
+ ", \"maximum\" : " +
+ NumToString(std::numeric_limits<int8_t>::max());
+ case BASE_TYPE_UCHAR:
+ return "\"type\" : \"integer\", \"minimum\" : 0, \"maximum\" :" +
+ NumToString(std::numeric_limits<uint8_t>::max());
+ case BASE_TYPE_SHORT:
+ return "\"type\" : \"integer\", \"minimum\" : " +
+ NumToString(std::numeric_limits<int16_t>::min()) +
+ ", \"maximum\" : " +
+ NumToString(std::numeric_limits<int16_t>::max());
+ case BASE_TYPE_USHORT:
+ return "\"type\" : \"integer\", \"minimum\" : 0, \"maximum\" : " +
+ NumToString(std::numeric_limits<uint16_t>::max());
+ case BASE_TYPE_INT:
+ return "\"type\" : \"integer\", \"minimum\" : " +
+ NumToString(std::numeric_limits<int32_t>::min()) +
+ ", \"maximum\" : " +
+ NumToString(std::numeric_limits<int32_t>::max());
+ case BASE_TYPE_UINT:
+ return "\"type\" : \"integer\", \"minimum\" : 0, \"maximum\" : " +
+ NumToString(std::numeric_limits<uint32_t>::max());
+ case BASE_TYPE_LONG:
+ return "\"type\" : \"integer\", \"minimum\" : " +
+ NumToString(std::numeric_limits<int64_t>::min()) +
+ ", \"maximum\" : " +
+ NumToString(std::numeric_limits<int64_t>::max());
+ case BASE_TYPE_ULONG:
+ return "\"type\" : \"integer\", \"minimum\" : 0, \"maximum\" : " +
+ NumToString(std::numeric_limits<uint64_t>::max());
+ case BASE_TYPE_FLOAT:
+ case BASE_TYPE_DOUBLE: return "\"type\" : \"number\"";
+ case BASE_TYPE_STRING: return "\"type\" : \"string\"";
+ default: return "";
+ }
+}
+
+std::string GenBaseType(const Type &type) {
+ if (type.struct_def != nullptr) { return GenTypeRef(type.struct_def); }
+ if (type.enum_def != nullptr) { return GenTypeRef(type.enum_def); }
+ return GenType(type.base_type);
+}
+
+std::string GenArrayType(const Type &type) {
+ std::string element_type;
+ if (type.struct_def != nullptr) {
+ element_type = GenTypeRef(type.struct_def);
+ } else if (type.enum_def != nullptr) {
+ element_type = GenTypeRef(type.enum_def);
+ } else {
+ element_type = GenType(type.element);
+ }
+
+ return "\"type\" : \"array\", \"items\" : {" + element_type + "}";
+}
+
+std::string GenType(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: {
+ return GenArrayType(type);
+ }
+ case BASE_TYPE_STRUCT: {
+ return GenTypeRef(type.struct_def);
+ }
+ case BASE_TYPE_UNION: {
+ std::string union_type_string("\"anyOf\": [");
+ const auto &union_types = type.enum_def->Vals();
+ for (auto ut = union_types.cbegin(); ut < union_types.cend(); ++ut) {
+ const auto &union_type = *ut;
+ if (union_type->union_type.base_type == BASE_TYPE_NONE) { continue; }
+ if (union_type->union_type.base_type == BASE_TYPE_STRUCT) {
+ union_type_string.append(
+ "{ " + GenTypeRef(union_type->union_type.struct_def) + " }");
+ }
+ if (union_type != *type.enum_def->Vals().rbegin()) {
+ union_type_string.append(",");
+ }
+ }
+ union_type_string.append("]");
+ return union_type_string;
+ }
+ case BASE_TYPE_UTYPE: return GenTypeRef(type.enum_def);
+ default: {
+ return GenBaseType(type);
+ }
+ }
+}
+
+class JsonSchemaGenerator : public BaseGenerator {
+ private:
+ std::string code_;
+
+ public:
+ JsonSchemaGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "", "json") {}
+
+ explicit JsonSchemaGenerator(const BaseGenerator &base_generator)
+ : BaseGenerator(base_generator) {}
+
+ std::string GeneratedFileName(const std::string &path,
+ const std::string &file_name,
+ const IDLOptions &options /* unused */) const {
+ (void)options;
+ return path + file_name + ".schema.json";
+ }
+
+ // If indentation is less than 0, that indicates we don't want any newlines
+ // either.
+ std::string NewLine() const {
+ return parser_.opts.indent_step >= 0 ? "\n" : "";
+ }
+
+ std::string Indent(int indent) const {
+ const auto num_spaces = indent * std::max(parser_.opts.indent_step, 0);
+ return std::string(num_spaces, ' ');
+ }
+
+ bool generate() {
+ code_ = "";
+ if (parser_.root_struct_def_ == nullptr) { return false; }
+ code_ += "{" + NewLine();
+ code_ += Indent(1) +
+ "\"$schema\": \"https://json-schema.org/draft/2019-09/schema\"," +
+ NewLine();
+ code_ += Indent(1) + "\"definitions\": {" + NewLine();
+ for (auto e = parser_.enums_.vec.cbegin(); e != parser_.enums_.vec.cend();
+ ++e) {
+ code_ += Indent(2) + "\"" + GenFullName(*e) + "\" : {" + NewLine();
+ code_ += Indent(3) + GenType("string") + "," + NewLine();
+ auto enumdef(Indent(3) + "\"enum\": [");
+ for (auto enum_value = (*e)->Vals().begin();
+ enum_value != (*e)->Vals().end(); ++enum_value) {
+ enumdef.append("\"" + (*enum_value)->name + "\"");
+ if (*enum_value != (*e)->Vals().back()) { enumdef.append(", "); }
+ }
+ enumdef.append("]");
+ code_ += enumdef + NewLine();
+ code_ += Indent(2) + "}," + NewLine(); // close type
+ }
+ for (auto s = parser_.structs_.vec.cbegin();
+ s != parser_.structs_.vec.cend(); ++s) {
+ const auto &structure = *s;
+ code_ += Indent(2) + "\"" + GenFullName(structure) + "\" : {" + NewLine();
+ code_ += Indent(3) + GenType("object") + "," + NewLine();
+ std::string comment;
+ const auto &comment_lines = structure->doc_comment;
+ for (auto comment_line = comment_lines.cbegin();
+ comment_line != comment_lines.cend(); ++comment_line) {
+ comment.append(*comment_line);
+ }
+ if (!comment.empty()) {
+ std::string description;
+ if (!EscapeString(comment.c_str(), comment.length(), &description, true,
+ true)) {
+ return false;
+ }
+ code_ +=
+ Indent(3) + "\"description\" : " + description + "," + NewLine();
+ }
+ code_ += Indent(3) + "\"properties\" : {" + NewLine();
+
+ const auto &properties = structure->fields.vec;
+ for (auto prop = properties.cbegin(); prop != properties.cend(); ++prop) {
+ const auto &property = *prop;
+ std::string arrayInfo = "";
+ if (IsArray(property->value.type)) {
+ arrayInfo = "," + NewLine() + Indent(8) + "\"minItems\": " +
+ NumToString(property->value.type.fixed_length) + "," +
+ NewLine() + Indent(8) + "\"maxItems\": " +
+ NumToString(property->value.type.fixed_length);
+ }
+ std::string deprecated_info = "";
+ if (property->deprecated) {
+ deprecated_info =
+ "," + NewLine() + Indent(8) + "\"deprecated\" : true,";
+ }
+ std::string typeLine = Indent(4) + "\"" + property->name + "\"";
+ typeLine += " : {" + NewLine() + Indent(8);
+ typeLine += GenType(property->value.type);
+ typeLine += arrayInfo;
+ typeLine += deprecated_info;
+ typeLine += NewLine() + Indent(7) + "}";
+ if (property != properties.back()) { typeLine.append(","); }
+ code_ += typeLine + NewLine();
+ }
+ code_ += Indent(3) + "}," + NewLine(); // close properties
+
+ std::vector<FieldDef *> requiredProperties;
+ std::copy_if(properties.begin(), properties.end(),
+ back_inserter(requiredProperties),
+ [](FieldDef const *prop) { return prop->IsRequired(); });
+ if (!requiredProperties.empty()) {
+ auto required_string(Indent(3) + "\"required\" : [");
+ for (auto req_prop = requiredProperties.cbegin();
+ req_prop != requiredProperties.cend(); ++req_prop) {
+ required_string.append("\"" + (*req_prop)->name + "\"");
+ if (*req_prop != requiredProperties.back()) {
+ required_string.append(", ");
+ }
+ }
+ required_string.append("],");
+ code_ += required_string + NewLine();
+ }
+ code_ += Indent(3) + "\"additionalProperties\" : false" + NewLine();
+ auto closeType(Indent(2) + "}");
+ if (*s != parser_.structs_.vec.back()) { closeType.append(","); }
+ code_ += closeType + NewLine(); // close type
+ }
+ code_ += Indent(1) + "}," + NewLine(); // close definitions
+
+ // mark root type
+ code_ += Indent(1) + "\"$ref\" : \"#/definitions/" +
+ GenFullName(parser_.root_struct_def_) + "\"" + NewLine();
+
+ code_ += "}" + NewLine(); // close schema root
+ return true;
+ }
+
+ bool save() const {
+ const auto file_path = GeneratedFileName(path_, file_name_, parser_.opts);
+ return SaveFile(file_path.c_str(), code_, false);
+ }
+
+ const std::string getJson() { return code_; }
+};
+} // namespace jsons
+
+bool GenerateJsonSchema(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ jsons::JsonSchemaGenerator generator(parser, path, file_name);
+ if (!generator.generate()) { return false; }
+ return generator.save();
+}
+
+bool GenerateJsonSchema(const Parser &parser, std::string *json) {
+ jsons::JsonSchemaGenerator generator(parser, "", "");
+ if (!generator.generate()) { return false; }
+ *json = generator.getJson();
+ return true;
+}
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_kotlin.cpp b/contrib/libs/flatbuffers/src/idl_gen_kotlin.cpp
new file mode 100644
index 0000000000..fb4ce87a67
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_kotlin.cpp
@@ -0,0 +1,1527 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <functional>
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+#if defined(FLATBUFFERS_CPP98_STL)
+# include <cctype>
+#endif // defined(FLATBUFFERS_CPP98_STL)
+
+namespace flatbuffers {
+
+namespace kotlin {
+
+typedef std::map<std::string, std::pair<std::string, std::string> > FbbParamMap;
+static TypedFloatConstantGenerator KotlinFloatGen("Double.", "Float.", "NaN",
+ "POSITIVE_INFINITY",
+ "NEGATIVE_INFINITY");
+
+static const CommentConfig comment_config = { "/**", " *", " */" };
+static const std::string ident_pad = " ";
+static const char *keywords[] = {
+ "package", "as", "typealias", "class", "this", "super",
+ "val", "var", "fun", "for", "null", "true",
+ "false", "is", "in", "throw", "return", "break",
+ "continue", "object", "if", "try", "else", "while",
+ "do", "when", "interface", "typeof", "Any", "Character"
+};
+
+// Escape Keywords
+static std::string Esc(const std::string &name) {
+ for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
+ if (name == keywords[i]) { return MakeCamel(name + "_", false); }
+ }
+
+ return MakeCamel(name, false);
+}
+
+class KotlinGenerator : public BaseGenerator {
+ public:
+ KotlinGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", ".", "kt"),
+ cur_name_space_(nullptr) {}
+
+ KotlinGenerator &operator=(const KotlinGenerator &);
+ bool generate() FLATBUFFERS_OVERRIDE {
+ std::string one_file_code;
+
+ cur_name_space_ = parser_.current_namespace_;
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ CodeWriter enumWriter(ident_pad);
+ auto &enum_def = **it;
+ if (!parser_.opts.one_file) cur_name_space_ = enum_def.defined_namespace;
+ GenEnum(enum_def, enumWriter);
+ if (parser_.opts.one_file) {
+ one_file_code += enumWriter.ToString();
+ } else {
+ if (!SaveType(enum_def.name, *enum_def.defined_namespace,
+ enumWriter.ToString(), false))
+ return false;
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ CodeWriter structWriter(ident_pad);
+ auto &struct_def = **it;
+ if (!parser_.opts.one_file)
+ cur_name_space_ = struct_def.defined_namespace;
+ GenStruct(struct_def, structWriter, parser_.opts);
+ if (parser_.opts.one_file) {
+ one_file_code += structWriter.ToString();
+ } else {
+ if (!SaveType(struct_def.name, *struct_def.defined_namespace,
+ structWriter.ToString(), true))
+ return false;
+ }
+ }
+
+ if (parser_.opts.one_file) {
+ return SaveType(file_name_, *parser_.current_namespace_, one_file_code,
+ true);
+ }
+ return true;
+ }
+
+ // Save out the generated code for a single class while adding
+ // declaration boilerplate.
+ bool SaveType(const std::string &defname, const Namespace &ns,
+ const std::string &classcode, bool needs_includes) const {
+ if (!classcode.length()) return true;
+
+ std::string code =
+ "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ std::string namespace_name = FullNamespace(".", ns);
+ if (!namespace_name.empty()) {
+ code += "package " + namespace_name;
+ code += "\n\n";
+ }
+ if (needs_includes) {
+ code += "import java.nio.*\n";
+ code += "import kotlin.math.sign\n";
+ code += "import com.google.flatbuffers.*\n\n";
+ }
+ code += classcode;
+ auto filename = NamespaceDir(ns) + defname + ".kt";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ const Namespace *CurrentNameSpace() const FLATBUFFERS_OVERRIDE {
+ return cur_name_space_;
+ }
+
+ static bool IsEnum(const Type &type) {
+ return type.enum_def != nullptr && IsInteger(type.base_type);
+ }
+
+ static std::string GenTypeBasic(const BaseType &type) {
+ // clang-format off
+ static const char * const kotlin_typename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, RTYPE, KTYPE, ...) \
+ #KTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return kotlin_typename[type];
+ }
+
+ std::string GenTypePointer(const Type &type) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return WrapInNameSpace(*type.struct_def);
+ default: return "Table";
+ }
+ }
+
+ // with the addition of optional scalar types,
+ // we are adding the nullable '?' operator to return type of a field.
+ std::string GetterReturnType(const FieldDef &field) const {
+ auto base_type = field.value.type.base_type;
+
+ auto r_type = GenTypeGet(field.value.type);
+ if (field.IsScalarOptional() ||
+ // string, structs and unions
+ (base_type == BASE_TYPE_STRING || base_type == BASE_TYPE_STRUCT ||
+ base_type == BASE_TYPE_UNION) ||
+ // vector of anything not scalar
+ (base_type == BASE_TYPE_VECTOR &&
+ !IsScalar(field.value.type.VectorType().base_type))) {
+ r_type += "?";
+ }
+ return r_type;
+ }
+
+ std::string GenTypeGet(const Type &type) const {
+ return IsScalar(type.base_type) ? GenTypeBasic(type.base_type)
+ : GenTypePointer(type);
+ }
+
+ std::string GenEnumDefaultValue(const FieldDef &field) const {
+ auto &value = field.value;
+ FLATBUFFERS_ASSERT(value.type.enum_def);
+ auto &enum_def = *value.type.enum_def;
+ auto enum_val = enum_def.FindByValue(value.constant);
+ return enum_val ? (WrapInNameSpace(enum_def) + "." + enum_val->name)
+ : value.constant;
+ }
+
+ // Generate default values to compare against a default value when
+ // `force_defaults` is `false`.
+ // Main differences are:
+ // - Floats are upcasted to doubles
+ // - Unsigned are casted to signed
+ std::string GenFBBDefaultValue(const FieldDef &field) const {
+ if (field.IsScalarOptional()) {
+ // although default value is null, java API forces us to present a real
+ // default value for scalars, while adding a field to the buffer. This is
+ // not a problem because the default can be representing just by not
+ // calling builder.addMyField()
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_DOUBLE:
+ case BASE_TYPE_FLOAT: return "0.0";
+ case BASE_TYPE_BOOL: return "false";
+ default: return "0";
+ }
+ }
+ auto out = GenDefaultValue(field, true);
+ // All FlatBufferBuilder default floating point values are doubles
+ if (field.value.type.base_type == BASE_TYPE_FLOAT) {
+ if (out.find("Float") != std::string::npos) {
+ out.replace(0, 5, "Double");
+ }
+ }
+ // Guarantee all values are doubles
+ if (out.back() == 'f') out.pop_back();
+ return out;
+ }
+
+ // FlatBufferBuilder only store signed types, so this function
+ // returns a cast for unsigned values
+ std::string GenFBBValueCast(const FieldDef &field) const {
+ if (IsUnsigned(field.value.type.base_type)) {
+ return CastToSigned(field.value.type);
+ }
+ return "";
+ }
+
+ std::string GenDefaultValue(const FieldDef &field,
+ bool force_signed = false) const {
+ auto &value = field.value;
+ auto base_type = field.value.type.base_type;
+
+ if (field.IsScalarOptional()) { return "null"; }
+ if (IsFloat(base_type)) {
+ auto val = KotlinFloatGen.GenFloatConstant(field);
+ if (base_type == BASE_TYPE_DOUBLE && val.back() == 'f') {
+ val.pop_back();
+ }
+ return val;
+ }
+
+ if (base_type == BASE_TYPE_BOOL) {
+ return value.constant == "0" ? "false" : "true";
+ }
+
+ std::string suffix = "";
+
+ if (base_type == BASE_TYPE_LONG || !force_signed) {
+ suffix = LiteralSuffix(base_type);
+ }
+ return value.constant + suffix;
+ }
+
+ void GenEnum(EnumDef &enum_def, CodeWriter &writer) const {
+ if (enum_def.generated) return;
+
+ GenerateComment(enum_def.doc_comment, writer, &comment_config);
+
+ writer += "@Suppress(\"unused\")";
+ writer += "@ExperimentalUnsignedTypes";
+ writer += "class " + Esc(enum_def.name) + " private constructor() {";
+ writer.IncrementIdentLevel();
+
+ GenerateCompanionObject(writer, [&]() {
+ // Write all properties
+ auto vals = enum_def.Vals();
+ for (auto it = vals.begin(); it != vals.end(); ++it) {
+ auto &ev = **it;
+ auto field_type = GenTypeBasic(enum_def.underlying_type.base_type);
+ auto val = enum_def.ToString(ev);
+ auto suffix = LiteralSuffix(enum_def.underlying_type.base_type);
+ writer.SetValue("name", Esc(ev.name));
+ writer.SetValue("type", field_type);
+ writer.SetValue("val", val + suffix);
+ GenerateComment(ev.doc_comment, writer, &comment_config);
+ writer += "const val {{name}}: {{type}} = {{val}}";
+ }
+
+ // Generate a generate string table for enum values.
+ // Problem is, if values are very sparse that could generate really
+ // big tables. Ideally in that case we generate a map lookup
+ // instead, but for the moment we simply don't output a table at all.
+ auto range = enum_def.Distance();
+ // Average distance between values above which we consider a table
+ // "too sparse". Change at will.
+ static const uint64_t kMaxSparseness = 5;
+ if (range / static_cast<uint64_t>(enum_def.size()) < kMaxSparseness) {
+ GeneratePropertyOneLine(writer, "names", "Array<String>", [&]() {
+ writer += "arrayOf(\\";
+ auto val = enum_def.Vals().front();
+ for (auto it = vals.begin(); it != vals.end(); ++it) {
+ auto ev = *it;
+ for (auto k = enum_def.Distance(val, ev); k > 1; --k)
+ writer += "\"\", \\";
+ val = ev;
+ writer += "\"" + (*it)->name + "\"\\";
+ if (it + 1 != vals.end()) { writer += ", \\"; }
+ }
+ writer += ")";
+ });
+ GenerateFunOneLine(
+ writer, "name", "e: Int", "String",
+ [&]() {
+ writer += "names[e\\";
+ if (enum_def.MinValue()->IsNonZero())
+ writer += " - " + enum_def.MinValue()->name + ".toInt()\\";
+ writer += "]";
+ },
+ parser_.opts.gen_jvmstatic);
+ }
+ });
+ writer.DecrementIdentLevel();
+ writer += "}";
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string ByteBufferGetter(const Type &type,
+ std::string bb_var_name) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "__string";
+ case BASE_TYPE_STRUCT: return "__struct";
+ case BASE_TYPE_UNION: return "__union";
+ case BASE_TYPE_VECTOR:
+ return ByteBufferGetter(type.VectorType(), bb_var_name);
+ case BASE_TYPE_INT:
+ case BASE_TYPE_UINT: return bb_var_name + ".getInt";
+ case BASE_TYPE_SHORT:
+ case BASE_TYPE_USHORT: return bb_var_name + ".getShort";
+ case BASE_TYPE_ULONG:
+ case BASE_TYPE_LONG: return bb_var_name + ".getLong";
+ case BASE_TYPE_FLOAT: return bb_var_name + ".getFloat";
+ case BASE_TYPE_DOUBLE: return bb_var_name + ".getDouble";
+ case BASE_TYPE_CHAR:
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_NONE:
+ case BASE_TYPE_UTYPE: return bb_var_name + ".get";
+ case BASE_TYPE_BOOL: return "0.toByte() != " + bb_var_name + ".get";
+ default:
+ return bb_var_name + ".get" + MakeCamel(GenTypeBasic(type.base_type));
+ }
+ }
+
+ std::string ByteBufferSetter(const Type &type) const {
+ if (IsScalar(type.base_type)) {
+ switch (type.base_type) {
+ case BASE_TYPE_INT:
+ case BASE_TYPE_UINT: return "bb.putInt";
+ case BASE_TYPE_SHORT:
+ case BASE_TYPE_USHORT: return "bb.putShort";
+ case BASE_TYPE_ULONG:
+ case BASE_TYPE_LONG: return "bb.putLong";
+ case BASE_TYPE_FLOAT: return "bb.putFloat";
+ case BASE_TYPE_DOUBLE: return "bb.putDouble";
+ case BASE_TYPE_CHAR:
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_BOOL:
+ case BASE_TYPE_NONE:
+ case BASE_TYPE_UTYPE: return "bb.put";
+ default: return "bb.put" + MakeCamel(GenTypeBasic(type.base_type));
+ }
+ }
+ return "";
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenLookupByKey(flatbuffers::FieldDef *key_field,
+ const std::string &bb_var_name,
+ const char *num = nullptr) const {
+ auto type = key_field->value.type;
+ return ByteBufferGetter(type, bb_var_name) + "(" +
+ GenOffsetGetter(key_field, num) + ")";
+ }
+
+ // Returns the method name for use with add/put calls.
+ static std::string GenMethod(const Type &type) {
+ return IsScalar(type.base_type) ? ToSignedType(type)
+ : (IsStruct(type) ? "Struct" : "Offset");
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ static void GenStructArgs(const StructDef &struct_def, CodeWriter &writer,
+ const char *nameprefix) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure
+ // names don't clash, and to make it obvious these arguments are
+ // constructing a nested struct, prefix the name with the field
+ // name.
+ GenStructArgs(*field.value.type.struct_def, writer,
+ (nameprefix + (field.name + "_")).c_str());
+ } else {
+ writer += std::string(", ") + nameprefix + "\\";
+ writer += MakeCamel(field.name) + ": \\";
+ writer += GenTypeBasic(field.value.type.base_type) + "\\";
+ }
+ }
+ }
+
+ // Recusively generate struct construction statements of the form:
+ // builder.putType(name);
+ // and insert manual padding.
+ static void GenStructBody(const StructDef &struct_def, CodeWriter &writer,
+ const char *nameprefix) {
+ writer.SetValue("align", NumToString(struct_def.minalign));
+ writer.SetValue("size", NumToString(struct_def.bytesize));
+ writer += "builder.prep({{align}}, {{size}})";
+ auto fields_vec = struct_def.fields.vec;
+ for (auto it = fields_vec.rbegin(); it != fields_vec.rend(); ++it) {
+ auto &field = **it;
+
+ if (field.padding) {
+ writer.SetValue("pad", NumToString(field.padding));
+ writer += "builder.pad({{pad}})";
+ }
+ if (IsStruct(field.value.type)) {
+ GenStructBody(*field.value.type.struct_def, writer,
+ (nameprefix + (field.name + "_")).c_str());
+ } else {
+ writer.SetValue("type", GenMethod(field.value.type));
+ writer.SetValue("argname", nameprefix + MakeCamel(field.name, false));
+ writer.SetValue("cast", CastToSigned(field.value.type));
+ writer += "builder.put{{type}}({{argname}}{{cast}})";
+ }
+ }
+ }
+
+ std::string GenByteBufferLength(const char *bb_name) const {
+ std::string bb_len = bb_name;
+ bb_len += ".capacity()";
+ return bb_len;
+ }
+
+ std::string GenOffsetGetter(flatbuffers::FieldDef *key_field,
+ const char *num = nullptr) const {
+ std::string key_offset =
+ "__offset(" + NumToString(key_field->value.offset) + ", ";
+ if (num) {
+ key_offset += num;
+ key_offset += ", _bb)";
+ } else {
+ key_offset += GenByteBufferLength("bb");
+ key_offset += " - tableOffset, bb)";
+ }
+ return key_offset;
+ }
+
+ void GenStruct(StructDef &struct_def, CodeWriter &writer,
+ IDLOptions options) const {
+ if (struct_def.generated) return;
+
+ GenerateComment(struct_def.doc_comment, writer, &comment_config);
+ auto fixed = struct_def.fixed;
+
+ writer.SetValue("struct_name", Esc(struct_def.name));
+ writer.SetValue("superclass", fixed ? "Struct" : "Table");
+
+ writer += "@Suppress(\"unused\")";
+ writer += "@ExperimentalUnsignedTypes";
+ writer += "class {{struct_name}} : {{superclass}}() {\n";
+
+ writer.IncrementIdentLevel();
+
+ {
+ // Generate the __init() method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ GenerateFun(writer, "__init", "_i: Int, _bb: ByteBuffer", "",
+ [&]() { writer += "__reset(_i, _bb)"; });
+
+ // Generate assign method
+ GenerateFun(writer, "__assign", "_i: Int, _bb: ByteBuffer",
+ Esc(struct_def.name), [&]() {
+ writer += "__init(_i, _bb)";
+ writer += "return this";
+ });
+
+ // Generate all getters
+ GenerateStructGetters(struct_def, writer);
+
+ // Generate Static Fields
+ GenerateCompanionObject(writer, [&]() {
+ if (!struct_def.fixed) {
+ FieldDef *key_field = nullptr;
+
+ // Generate verson check method.
+ // Force compile time error if not using the same version
+ // runtime.
+ GenerateFunOneLine(
+ writer, "validateVersion", "", "",
+ [&]() { writer += "Constants.FLATBUFFERS_2_0_0()"; },
+ options.gen_jvmstatic);
+
+ GenerateGetRootAsAccessors(Esc(struct_def.name), writer, options);
+ GenerateBufferHasIdentifier(struct_def, writer, options);
+ GenerateTableCreator(struct_def, writer, options);
+
+ GenerateStartStructMethod(struct_def, writer, options);
+
+ // Static Add for fields
+ auto fields = struct_def.fields.vec;
+ int field_pos = -1;
+ for (auto it = fields.begin(); it != fields.end(); ++it) {
+ auto &field = **it;
+ field_pos++;
+ if (field.deprecated) continue;
+ if (field.key) key_field = &field;
+ GenerateAddField(NumToString(field_pos), field, writer, options);
+
+ if (IsVector(field.value.type)) {
+ auto vector_type = field.value.type.VectorType();
+ if (!IsStruct(vector_type)) {
+ GenerateCreateVectorField(field, writer, options);
+ }
+ GenerateStartVectorField(field, writer, options);
+ }
+ }
+
+ GenerateEndStructMethod(struct_def, writer, options);
+ auto file_identifier = parser_.file_identifier_;
+ if (parser_.root_struct_def_ == &struct_def) {
+ GenerateFinishStructBuffer(struct_def, file_identifier, writer,
+ options);
+ GenerateFinishSizePrefixed(struct_def, file_identifier, writer,
+ options);
+ }
+
+ if (struct_def.has_key) {
+ GenerateLookupByKey(key_field, struct_def, writer, options);
+ }
+ } else {
+ GenerateStaticConstructor(struct_def, writer, options);
+ }
+ });
+ }
+
+ // class closing
+ writer.DecrementIdentLevel();
+ writer += "}";
+ }
+
+ // TODO: move key_field to reference instead of pointer
+ void GenerateLookupByKey(FieldDef *key_field, StructDef &struct_def,
+ CodeWriter &writer, const IDLOptions options) const {
+ std::stringstream params;
+ params << "obj: " << Esc(struct_def.name) << "?"
+ << ", ";
+ params << "vectorLocation: Int, ";
+ params << "key: " << GenTypeGet(key_field->value.type) << ", ";
+ params << "bb: ByteBuffer";
+
+ auto statements = [&]() {
+ auto base_type = key_field->value.type.base_type;
+ writer.SetValue("struct_name", Esc(struct_def.name));
+ if (base_type == BASE_TYPE_STRING) {
+ writer +=
+ "val byteKey = key."
+ "toByteArray(java.nio.charset.StandardCharsets.UTF_8)";
+ }
+ writer += "var span = bb.getInt(vectorLocation - 4)";
+ writer += "var start = 0";
+ writer += "while (span != 0) {";
+ writer.IncrementIdentLevel();
+ writer += "var middle = span / 2";
+ writer +=
+ "val tableOffset = __indirect(vector"
+ "Location + 4 * (start + middle), bb)";
+ if (IsString(key_field->value.type)) {
+ writer += "val comp = compareStrings(\\";
+ writer += GenOffsetGetter(key_field) + "\\";
+ writer += ", byteKey, bb)";
+ } else {
+ auto cast = CastToUsigned(key_field->value.type);
+ auto get_val = GenLookupByKey(key_field, "bb");
+ writer += "val value = " + get_val + cast;
+ writer += "val comp = value.compareTo(key)";
+ }
+ writer += "when {";
+ writer.IncrementIdentLevel();
+ writer += "comp > 0 -> span = middle";
+ writer += "comp < 0 -> {";
+ writer.IncrementIdentLevel();
+ writer += "middle++";
+ writer += "start += middle";
+ writer += "span -= middle";
+ writer.DecrementIdentLevel();
+ writer += "}"; // end comp < 0
+ writer += "else -> {";
+ writer.IncrementIdentLevel();
+ writer += "return (obj ?: {{struct_name}}()).__assign(tableOffset, bb)";
+ writer.DecrementIdentLevel();
+ writer += "}"; // end else
+ writer.DecrementIdentLevel();
+ writer += "}"; // end when
+ writer.DecrementIdentLevel();
+ writer += "}"; // end while
+ writer += "return null";
+ };
+ GenerateFun(writer, "__lookup_by_key", params.str(),
+ Esc(struct_def.name) + "?", statements, options.gen_jvmstatic);
+ }
+
+ void GenerateFinishSizePrefixed(StructDef &struct_def,
+ const std::string &identifier,
+ CodeWriter &writer,
+ const IDLOptions options) const {
+ auto id = identifier.length() > 0 ? ", \"" + identifier + "\"" : "";
+ auto params = "builder: FlatBufferBuilder, offset: Int";
+ auto method_name = "finishSizePrefixed" + Esc(struct_def.name) + "Buffer";
+ GenerateFunOneLine(
+ writer, method_name, params, "",
+ [&]() { writer += "builder.finishSizePrefixed(offset" + id + ")"; },
+ options.gen_jvmstatic);
+ }
+ void GenerateFinishStructBuffer(StructDef &struct_def,
+ const std::string &identifier,
+ CodeWriter &writer,
+ const IDLOptions options) const {
+ auto id = identifier.length() > 0 ? ", \"" + identifier + "\"" : "";
+ auto params = "builder: FlatBufferBuilder, offset: Int";
+ auto method_name = "finish" + Esc(struct_def.name) + "Buffer";
+ GenerateFunOneLine(
+ writer, method_name, params, "",
+ [&]() { writer += "builder.finish(offset" + id + ")"; },
+ options.gen_jvmstatic);
+ }
+
+ void GenerateEndStructMethod(StructDef &struct_def, CodeWriter &writer,
+ const IDLOptions options) const {
+ // Generate end{{TableName}}(builder: FlatBufferBuilder) method
+ auto name = "end" + Esc(struct_def.name);
+ auto params = "builder: FlatBufferBuilder";
+ auto returns = "Int";
+ auto field_vec = struct_def.fields.vec;
+
+ GenerateFun(
+ writer, name, params, returns,
+ [&]() {
+ writer += "val o = builder.endTable()";
+ writer.IncrementIdentLevel();
+ for (auto it = field_vec.begin(); it != field_vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated || !field.IsRequired()) { continue; }
+ writer.SetValue("offset", NumToString(field.value.offset));
+ writer += "builder.required(o, {{offset}})";
+ }
+ writer.DecrementIdentLevel();
+ writer += "return o";
+ },
+ options.gen_jvmstatic);
+ }
+
+ // Generate a method to create a vector from a Kotlin array.
+ void GenerateCreateVectorField(FieldDef &field, CodeWriter &writer,
+ const IDLOptions options) const {
+ auto vector_type = field.value.type.VectorType();
+ auto method_name = "create" + MakeCamel(Esc(field.name)) + "Vector";
+ auto params = "builder: FlatBufferBuilder, data: " +
+ GenTypeBasic(vector_type.base_type) + "Array";
+ writer.SetValue("size", NumToString(InlineSize(vector_type)));
+ writer.SetValue("align", NumToString(InlineAlignment(vector_type)));
+ writer.SetValue("root", GenMethod(vector_type));
+ writer.SetValue("cast", CastToSigned(vector_type));
+
+ GenerateFun(
+ writer, method_name, params, "Int",
+ [&]() {
+ writer += "builder.startVector({{size}}, data.size, {{align}})";
+ writer += "for (i in data.size - 1 downTo 0) {";
+ writer.IncrementIdentLevel();
+ writer += "builder.add{{root}}(data[i]{{cast}})";
+ writer.DecrementIdentLevel();
+ writer += "}";
+ writer += "return builder.endVector()";
+ },
+ options.gen_jvmstatic);
+ }
+
+ void GenerateStartVectorField(FieldDef &field, CodeWriter &writer,
+ const IDLOptions options) const {
+ // Generate a method to start a vector, data to be added manually
+ // after.
+ auto vector_type = field.value.type.VectorType();
+ auto params = "builder: FlatBufferBuilder, numElems: Int";
+ writer.SetValue("size", NumToString(InlineSize(vector_type)));
+ writer.SetValue("align", NumToString(InlineAlignment(vector_type)));
+
+ GenerateFunOneLine(
+ writer, "start" + MakeCamel(Esc(field.name) + "Vector", true), params,
+ "",
+ [&]() {
+ writer += "builder.startVector({{size}}, numElems, {{align}})";
+ },
+ options.gen_jvmstatic);
+ }
+
+ void GenerateAddField(std::string field_pos, FieldDef &field,
+ CodeWriter &writer, const IDLOptions options) const {
+ auto field_type = GenTypeBasic(field.value.type.base_type);
+ auto secondArg = MakeCamel(Esc(field.name), false) + ": " + field_type;
+
+ GenerateFunOneLine(
+ writer, "add" + MakeCamel(Esc(field.name), true),
+ "builder: FlatBufferBuilder, " + secondArg, "",
+ [&]() {
+ auto method = GenMethod(field.value.type);
+ writer.SetValue("field_name", MakeCamel(Esc(field.name), false));
+ writer.SetValue("method_name", method);
+ writer.SetValue("pos", field_pos);
+ writer.SetValue("default", GenFBBDefaultValue(field));
+ writer.SetValue("cast", GenFBBValueCast(field));
+
+ writer += "builder.add{{method_name}}({{pos}}, \\";
+ writer += "{{field_name}}{{cast}}, {{default}})";
+ },
+ options.gen_jvmstatic);
+ }
+
+ static std::string ToSignedType(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_UINT: return GenTypeBasic(BASE_TYPE_INT);
+ case BASE_TYPE_ULONG: return GenTypeBasic(BASE_TYPE_LONG);
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_NONE:
+ case BASE_TYPE_UTYPE: return GenTypeBasic(BASE_TYPE_CHAR);
+ case BASE_TYPE_USHORT: return GenTypeBasic(BASE_TYPE_SHORT);
+ case BASE_TYPE_VECTOR: return ToSignedType(type.VectorType());
+ default: return GenTypeBasic(type.base_type);
+ }
+ }
+
+ static std::string FlexBufferBuilderCast(const std::string &method,
+ FieldDef &field, bool isFirst) {
+ auto field_type = GenTypeBasic(field.value.type.base_type);
+ std::string to_type;
+ if (method == "Boolean")
+ to_type = "Boolean";
+ else if (method == "Long")
+ to_type = "Long";
+ else if (method == "Int" || method == "Offset" || method == "Struct")
+ to_type = "Int";
+ else if (method == "Byte" || method.empty())
+ to_type = isFirst ? "Byte" : "Int";
+ else if (method == "Short")
+ to_type = isFirst ? "Short" : "Int";
+ else if (method == "Double")
+ to_type = "Double";
+ else if (method == "Float")
+ to_type = isFirst ? "Float" : "Double";
+ else if (method == "UByte")
+
+ if (field_type != to_type) return ".to" + to_type + "()";
+ return "";
+ }
+
+ // fun startMonster(builder: FlatBufferBuilder) = builder.startTable(11)
+ void GenerateStartStructMethod(StructDef &struct_def, CodeWriter &code,
+ const IDLOptions options) const {
+ GenerateFunOneLine(
+ code, "start" + Esc(struct_def.name), "builder: FlatBufferBuilder", "",
+ [&]() {
+ code += "builder.startTable(" +
+ NumToString(struct_def.fields.vec.size()) + ")";
+ },
+ options.gen_jvmstatic);
+ }
+
+ void GenerateTableCreator(StructDef &struct_def, CodeWriter &writer,
+ const IDLOptions options) const {
+ // Generate a method that creates a table in one go. This is only possible
+ // when the table has no struct fields, since those have to be created
+ // inline, and there's no way to do so in Java.
+ bool has_no_struct_fields = true;
+ int num_fields = 0;
+ auto fields_vec = struct_def.fields.vec;
+
+ for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (IsStruct(field.value.type)) {
+ has_no_struct_fields = false;
+ } else {
+ num_fields++;
+ }
+ }
+ // JVM specifications restrict default constructor params to be < 255.
+ // Longs and doubles take up 2 units, so we set the limit to be < 127.
+ if (has_no_struct_fields && num_fields && num_fields < 127) {
+ // Generate a table constructor of the form:
+ // public static int createName(FlatBufferBuilder builder, args...)
+
+ auto name = "create" + Esc(struct_def.name);
+ std::stringstream params;
+ params << "builder: FlatBufferBuilder";
+ for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ params << ", " << MakeCamel(Esc(field.name), false);
+ if (!IsScalar(field.value.type.base_type)) {
+ params << "Offset: ";
+ } else {
+ params << ": ";
+ }
+ auto optional = field.IsScalarOptional() ? "?" : "";
+ params << GenTypeBasic(field.value.type.base_type) << optional;
+ }
+
+ GenerateFun(
+ writer, name, params.str(), "Int",
+ [&]() {
+ writer.SetValue("vec_size", NumToString(fields_vec.size()));
+
+ writer += "builder.startTable({{vec_size}})";
+
+ auto sortbysize = struct_def.sortbysize;
+ auto largest = sortbysize ? sizeof(largest_scalar_t) : 1;
+ for (size_t size = largest; size; size /= 2) {
+ for (auto it = fields_vec.rbegin(); it != fields_vec.rend();
+ ++it) {
+ auto &field = **it;
+ auto base_type_size = SizeOf(field.value.type.base_type);
+ if (!field.deprecated &&
+ (!sortbysize || size == base_type_size)) {
+ writer.SetValue("camel_field_name",
+ MakeCamel(Esc(field.name), true));
+ writer.SetValue("field_name",
+ MakeCamel(Esc(field.name), false));
+
+ // we wrap on null check for scalar optionals
+ writer += field.IsScalarOptional()
+ ? "{{field_name}}?.run { \\"
+ : "\\";
+
+ writer += "add{{camel_field_name}}(builder, {{field_name}}\\";
+ if (!IsScalar(field.value.type.base_type)) {
+ writer += "Offset\\";
+ }
+ // we wrap on null check for scalar optionals
+ writer += field.IsScalarOptional() ? ") }" : ")";
+ }
+ }
+ }
+ writer += "return end{{struct_name}}(builder)";
+ },
+ options.gen_jvmstatic);
+ }
+ }
+ void GenerateBufferHasIdentifier(StructDef &struct_def, CodeWriter &writer,
+ IDLOptions options) const {
+ auto file_identifier = parser_.file_identifier_;
+ // Check if a buffer has the identifier.
+ if (parser_.root_struct_def_ != &struct_def || !file_identifier.length())
+ return;
+ auto name = MakeCamel(Esc(struct_def.name), false);
+ GenerateFunOneLine(
+ writer, name + "BufferHasIdentifier", "_bb: ByteBuffer", "Boolean",
+ [&]() {
+ writer += "__has_identifier(_bb, \"" + file_identifier + "\")";
+ },
+ options.gen_jvmstatic);
+ }
+
+ void GenerateStructGetters(StructDef &struct_def, CodeWriter &writer) const {
+ auto fields_vec = struct_def.fields.vec;
+ FieldDef *key_field = nullptr;
+ for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.key) key_field = &field;
+
+ GenerateComment(field.doc_comment, writer, &comment_config);
+
+ auto field_name = MakeCamel(Esc(field.name), false);
+ auto field_type = GenTypeGet(field.value.type);
+ auto field_default_value = GenDefaultValue(field);
+ auto return_type = GetterReturnType(field);
+ auto bbgetter = ByteBufferGetter(field.value.type, "bb");
+ auto ucast = CastToUsigned(field);
+ auto offset_val = NumToString(field.value.offset);
+ auto offset_prefix =
+ "val o = __offset(" + offset_val + "); return o != 0 ? ";
+ auto value_base_type = field.value.type.base_type;
+ // Most field accessors need to retrieve and test the field offset
+ // first, this is the offset value for that:
+ writer.SetValue("offset", NumToString(field.value.offset));
+ writer.SetValue("return_type", return_type);
+ writer.SetValue("field_type", field_type);
+ writer.SetValue("field_name", field_name);
+ writer.SetValue("field_default", field_default_value);
+ writer.SetValue("bbgetter", bbgetter);
+ writer.SetValue("ucast", ucast);
+
+ // Generate the accessors that don't do object reuse.
+ if (value_base_type == BASE_TYPE_STRUCT) {
+ // Calls the accessor that takes an accessor object with a
+ // new object.
+ // val pos
+ // get() = pos(Vec3())
+ GenerateGetterOneLine(writer, field_name, return_type, [&]() {
+ writer += "{{field_name}}({{field_type}}())";
+ });
+ } else if (value_base_type == BASE_TYPE_VECTOR &&
+ field.value.type.element == BASE_TYPE_STRUCT) {
+ // Accessors for vectors of structs also take accessor objects,
+ // this generates a variant without that argument.
+ // ex: fun weapons(j: Int) = weapons(Weapon(), j)
+ GenerateFunOneLine(writer, field_name, "j: Int", return_type, [&]() {
+ writer += "{{field_name}}({{field_type}}(), j)";
+ });
+ }
+
+ if (IsScalar(value_base_type)) {
+ if (struct_def.fixed) {
+ GenerateGetterOneLine(writer, field_name, return_type, [&]() {
+ writer += "{{bbgetter}}(bb_pos + {{offset}}){{ucast}}";
+ });
+ } else {
+ GenerateGetter(writer, field_name, return_type, [&]() {
+ writer += "val o = __offset({{offset}})";
+ writer +=
+ "return if(o != 0) {{bbgetter}}"
+ "(o + bb_pos){{ucast}} else "
+ "{{field_default}}";
+ });
+ }
+ } else {
+ switch (value_base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ // create getter with object reuse
+ // ex:
+ // fun pos(obj: Vec3) : Vec3? = obj.__assign(bb_pos + 4, bb)
+ // ? adds nullability annotation
+ GenerateFunOneLine(
+ writer, field_name, "obj: " + field_type, return_type,
+ [&]() { writer += "obj.__assign(bb_pos + {{offset}}, bb)"; });
+ } else {
+ // create getter with object reuse
+ // ex:
+ // fun pos(obj: Vec3) : Vec3? {
+ // val o = __offset(4)
+ // return if(o != 0) {
+ // obj.__assign(o + bb_pos, bb)
+ // else {
+ // null
+ // }
+ // }
+ // ? adds nullability annotation
+ GenerateFun(
+ writer, field_name, "obj: " + field_type, return_type, [&]() {
+ auto fixed = field.value.type.struct_def->fixed;
+
+ writer.SetValue("seek", Indirect("o + bb_pos", fixed));
+ OffsetWrapper(
+ writer, offset_val,
+ [&]() { writer += "obj.__assign({{seek}}, bb)"; },
+ [&]() { writer += "null"; });
+ });
+ }
+ break;
+ case BASE_TYPE_STRING:
+ // create string getter
+ // e.g.
+ // val Name : String?
+ // get() = {
+ // val o = __offset(10)
+ // return if (o != 0) __string(o + bb_pos) else null
+ // }
+ // ? adds nullability annotation
+ GenerateGetter(writer, field_name, return_type, [&]() {
+ writer += "val o = __offset({{offset}})";
+ writer += "return if (o != 0) __string(o + bb_pos) else null";
+ });
+ break;
+ case BASE_TYPE_VECTOR: {
+ // e.g.
+ // fun inventory(j: Int) : UByte {
+ // val o = __offset(14)
+ // return if (o != 0) {
+ // bb.get(__vector(o) + j * 1).toUByte()
+ // } else {
+ // 0
+ // }
+ // }
+
+ auto vectortype = field.value.type.VectorType();
+ std::string params = "j: Int";
+
+ if (vectortype.base_type == BASE_TYPE_STRUCT ||
+ vectortype.base_type == BASE_TYPE_UNION) {
+ params = "obj: " + field_type + ", j: Int";
+ }
+
+ GenerateFun(writer, field_name, params, return_type, [&]() {
+ auto inline_size = NumToString(InlineSize(vectortype));
+ auto index = "__vector(o) + j * " + inline_size;
+ auto not_found = NotFoundReturn(field.value.type.element);
+ auto found = "";
+ writer.SetValue("index", index);
+ switch (vectortype.base_type) {
+ case BASE_TYPE_STRUCT: {
+ bool fixed = vectortype.struct_def->fixed;
+ writer.SetValue("index", Indirect(index, fixed));
+ found = "obj.__assign({{index}}, bb)";
+ break;
+ }
+ case BASE_TYPE_UNION:
+ found = "{{bbgetter}}(obj, {{index}}){{ucast}}";
+ break;
+ default: found = "{{bbgetter}}({{index}}){{ucast}}";
+ }
+ OffsetWrapper(
+ writer, offset_val, [&]() { writer += found; },
+ [&]() { writer += not_found; });
+ });
+ break;
+ }
+ case BASE_TYPE_UNION:
+ GenerateFun(
+ writer, field_name, "obj: " + field_type, return_type, [&]() {
+ writer += OffsetWrapperOneLine(
+ offset_val, bbgetter + "(obj, o + bb_pos)", "null");
+ });
+ break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+
+ if (value_base_type == BASE_TYPE_VECTOR) {
+ // Generate Lenght functions for vectors
+ GenerateGetter(writer, field_name + "Length", "Int", [&]() {
+ writer += OffsetWrapperOneLine(offset_val, "__vector_len(o)", "0");
+ });
+
+ // See if we should generate a by-key accessor.
+ if (field.value.type.element == BASE_TYPE_STRUCT &&
+ !field.value.type.struct_def->fixed) {
+ auto &sd = *field.value.type.struct_def;
+ auto &fields = sd.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &kfield = **kit;
+ if (kfield.key) {
+ auto qualified_name = WrapInNameSpace(sd);
+ auto name = MakeCamel(Esc(field.name), false) + "ByKey";
+ auto params = "key: " + GenTypeGet(kfield.value.type);
+ auto rtype = qualified_name + "?";
+ GenerateFun(writer, name, params, rtype, [&]() {
+ OffsetWrapper(
+ writer, offset_val,
+ [&]() {
+ writer += qualified_name +
+ ".__lookup_by_key(null, __vector(o), key, bb)";
+ },
+ [&]() { writer += "null"; });
+ });
+
+ auto param2 = "obj: " + qualified_name +
+ ", key: " + GenTypeGet(kfield.value.type);
+ GenerateFun(writer, name, param2, rtype, [&]() {
+ OffsetWrapper(
+ writer, offset_val,
+ [&]() {
+ writer += qualified_name +
+ ".__lookup_by_key(obj, __vector(o), key, bb)";
+ },
+ [&]() { writer += "null"; });
+ });
+
+ break;
+ }
+ }
+ }
+ }
+
+ if ((value_base_type == BASE_TYPE_VECTOR &&
+ IsScalar(field.value.type.VectorType().base_type)) ||
+ value_base_type == BASE_TYPE_STRING) {
+ auto end_idx =
+ NumToString(value_base_type == BASE_TYPE_STRING
+ ? 1
+ : InlineSize(field.value.type.VectorType()));
+ // Generate a ByteBuffer accessor for strings & vectors of scalars.
+ // e.g.
+ // val inventoryByteBuffer: ByteBuffer
+ // get = __vector_as_bytebuffer(14, 1)
+
+ GenerateGetterOneLine(
+ writer, field_name + "AsByteBuffer", "ByteBuffer", [&]() {
+ writer.SetValue("end", end_idx);
+ writer += "__vector_as_bytebuffer({{offset}}, {{end}})";
+ });
+
+ // Generate a ByteBuffer accessor for strings & vectors of scalars.
+ // e.g.
+ // fun inventoryInByteBuffer(_bb: Bytebuffer):
+ // ByteBuffer = __vector_as_bytebuffer(_bb, 14, 1)
+ GenerateFunOneLine(
+ writer, field_name + "InByteBuffer", "_bb: ByteBuffer",
+ "ByteBuffer", [&]() {
+ writer.SetValue("end", end_idx);
+ writer += "__vector_in_bytebuffer(_bb, {{offset}}, {{end}})";
+ });
+ }
+
+ // generate object accessors if is nested_flatbuffer
+ // fun testnestedflatbufferAsMonster() : Monster?
+ //{ return testnestedflatbufferAsMonster(new Monster()); }
+
+ if (field.nested_flatbuffer) {
+ auto nested_type_name = WrapInNameSpace(*field.nested_flatbuffer);
+ auto nested_method_name =
+ field_name + "As" + field.nested_flatbuffer->name;
+
+ GenerateGetterOneLine(
+ writer, nested_method_name, nested_type_name + "?", [&]() {
+ writer += nested_method_name + "(" + nested_type_name + "())";
+ });
+
+ GenerateFun(writer, nested_method_name, "obj: " + nested_type_name,
+ nested_type_name + "?", [&]() {
+ OffsetWrapper(
+ writer, offset_val,
+ [&]() {
+ writer +=
+ "obj.__assign(__indirect(__vector(o)), bb)";
+ },
+ [&]() { writer += "null"; });
+ });
+ }
+
+ // Generate mutators for scalar fields or vectors of scalars.
+ if (parser_.opts.mutable_buffer) {
+ auto value_type = field.value.type;
+ auto underlying_type = value_base_type == BASE_TYPE_VECTOR
+ ? value_type.VectorType()
+ : value_type;
+ auto name = "mutate" + MakeCamel(Esc(field.name), true);
+ auto size = NumToString(InlineSize(underlying_type));
+ auto params = Esc(field.name) + ": " + GenTypeGet(underlying_type);
+ // A vector mutator also needs the index of the vector element it should
+ // mutate.
+ if (value_base_type == BASE_TYPE_VECTOR) params.insert(0, "j: Int, ");
+
+ // Boolean parameters have to be explicitly converted to byte
+ // representation.
+ auto setter_parameter =
+ underlying_type.base_type == BASE_TYPE_BOOL
+ ? "(if(" + Esc(field.name) + ") 1 else 0).toByte()"
+ : Esc(field.name);
+
+ auto setter_index =
+ value_base_type == BASE_TYPE_VECTOR
+ ? "__vector(o) + j * " + size
+ : (struct_def.fixed ? "bb_pos + " + offset_val : "o + bb_pos");
+ if (IsScalar(value_base_type) ||
+ (value_base_type == BASE_TYPE_VECTOR &&
+ IsScalar(value_type.VectorType().base_type))) {
+ auto statements = [&]() {
+ writer.SetValue("bbsetter", ByteBufferSetter(underlying_type));
+ writer.SetValue("index", setter_index);
+ writer.SetValue("params", setter_parameter);
+ writer.SetValue("cast", CastToSigned(field));
+ if (struct_def.fixed) {
+ writer += "{{bbsetter}}({{index}}, {{params}}{{cast}})";
+ } else {
+ OffsetWrapper(
+ writer, offset_val,
+ [&]() {
+ writer += "{{bbsetter}}({{index}}, {{params}}{{cast}})";
+ writer += "true";
+ },
+ [&]() { writer += "false"; });
+ }
+ };
+
+ if (struct_def.fixed) {
+ GenerateFunOneLine(writer, name, params, "ByteBuffer", statements);
+ } else {
+ GenerateFun(writer, name, params, "Boolean", statements);
+ }
+ }
+ }
+ }
+ if (struct_def.has_key && !struct_def.fixed) {
+ // Key Comparison method
+ GenerateOverrideFun(
+ writer, "keysCompare", "o1: Int, o2: Int, _bb: ByteBuffer", "Int",
+ [&]() {
+ if (IsString(key_field->value.type)) {
+ writer.SetValue("offset", NumToString(key_field->value.offset));
+ writer +=
+ " return compareStrings(__offset({{offset}}, o1, "
+ "_bb), __offset({{offset}}, o2, _bb), _bb)";
+
+ } else {
+ auto getter1 = GenLookupByKey(key_field, "_bb", "o1");
+ auto getter2 = GenLookupByKey(key_field, "_bb", "o2");
+ writer += "val val_1 = " + getter1;
+ writer += "val val_2 = " + getter2;
+ writer += "return (val_1 - val_2).sign";
+ }
+ });
+ }
+ }
+
+ static std::string CastToUsigned(const FieldDef &field) {
+ return CastToUsigned(field.value.type);
+ }
+
+ static std::string CastToUsigned(const Type type) {
+ switch (type.base_type) {
+ case BASE_TYPE_UINT: return ".toUInt()";
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_UTYPE: return ".toUByte()";
+ case BASE_TYPE_USHORT: return ".toUShort()";
+ case BASE_TYPE_ULONG: return ".toULong()";
+ case BASE_TYPE_VECTOR: return CastToUsigned(type.VectorType());
+ default: return "";
+ }
+ }
+
+ static std::string CastToSigned(const FieldDef &field) {
+ return CastToSigned(field.value.type);
+ }
+
+ static std::string CastToSigned(const Type type) {
+ switch (type.base_type) {
+ case BASE_TYPE_UINT: return ".toInt()";
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_UTYPE: return ".toByte()";
+ case BASE_TYPE_USHORT: return ".toShort()";
+ case BASE_TYPE_ULONG: return ".toLong()";
+ case BASE_TYPE_VECTOR: return CastToSigned(type.VectorType());
+ default: return "";
+ }
+ }
+
+ static std::string LiteralSuffix(const BaseType type) {
+ switch (type) {
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_UTYPE:
+ case BASE_TYPE_USHORT: return "u";
+ case BASE_TYPE_ULONG: return "UL";
+ case BASE_TYPE_LONG: return "L";
+ default: return "";
+ }
+ }
+
+ void GenerateCompanionObject(CodeWriter &code,
+ const std::function<void()> &callback) const {
+ code += "companion object {";
+ code.IncrementIdentLevel();
+ callback();
+ code.DecrementIdentLevel();
+ code += "}";
+ }
+
+ // Generate a documentation comment, if available.
+ void GenerateComment(const std::vector<std::string> &dc, CodeWriter &writer,
+ const CommentConfig *config) const {
+ if (dc.begin() == dc.end()) {
+ // Don't output empty comment blocks with 0 lines of comment content.
+ return;
+ }
+
+ if (config != nullptr && config->first_line != nullptr) {
+ writer += std::string(config->first_line);
+ }
+ std::string line_prefix =
+ ((config != nullptr && config->content_line_prefix != nullptr)
+ ? config->content_line_prefix
+ : "///");
+ for (auto it = dc.begin(); it != dc.end(); ++it) {
+ writer += line_prefix + *it;
+ }
+ if (config != nullptr && config->last_line != nullptr) {
+ writer += std::string(config->last_line);
+ }
+ }
+
+ static void GenerateGetRootAsAccessors(const std::string &struct_name,
+ CodeWriter &writer,
+ IDLOptions options) {
+ // Generate a special accessor for the table that when used as the root
+ // ex: fun getRootAsMonster(_bb: ByteBuffer): Monster {...}
+ writer.SetValue("gr_name", struct_name);
+ writer.SetValue("gr_method", "getRootAs" + struct_name);
+
+ // create convenience method that doesn't require an existing object
+ GenerateJvmStaticAnnotation(writer, options.gen_jvmstatic);
+ writer += "fun {{gr_method}}(_bb: ByteBuffer): {{gr_name}} = \\";
+ writer += "{{gr_method}}(_bb, {{gr_name}}())";
+
+ // create method that allows object reuse
+ // ex: fun Monster getRootAsMonster(_bb: ByteBuffer, obj: Monster) {...}
+ GenerateJvmStaticAnnotation(writer, options.gen_jvmstatic);
+ writer +=
+ "fun {{gr_method}}"
+ "(_bb: ByteBuffer, obj: {{gr_name}}): {{gr_name}} {";
+ writer.IncrementIdentLevel();
+ writer += "_bb.order(ByteOrder.LITTLE_ENDIAN)";
+ writer +=
+ "return (obj.__assign(_bb.getInt(_bb.position())"
+ " + _bb.position(), _bb))";
+ writer.DecrementIdentLevel();
+ writer += "}";
+ }
+
+ static void GenerateStaticConstructor(const StructDef &struct_def,
+ CodeWriter &code,
+ const IDLOptions options) {
+ // create a struct constructor function
+ auto params = StructConstructorParams(struct_def);
+ GenerateFun(
+ code, "create" + Esc(struct_def.name), params, "Int",
+ [&]() {
+ GenStructBody(struct_def, code, "");
+ code += "return builder.offset()";
+ },
+ options.gen_jvmstatic);
+ }
+
+ static std::string StructConstructorParams(const StructDef &struct_def,
+ const std::string &prefix = "") {
+ // builder: FlatBufferBuilder
+ std::stringstream out;
+ auto field_vec = struct_def.fields.vec;
+ if (prefix.empty()) { out << "builder: FlatBufferBuilder"; }
+ for (auto it = field_vec.begin(); it != field_vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure
+ // names don't clash, and to make it obvious these arguments are
+ // constructing a nested struct, prefix the name with the field
+ // name.
+ out << StructConstructorParams(*field.value.type.struct_def,
+ prefix + (Esc(field.name) + "_"));
+ } else {
+ out << ", " << prefix << MakeCamel(Esc(field.name), false) << ": "
+ << GenTypeBasic(field.value.type.base_type);
+ }
+ }
+ return out.str();
+ }
+
+ static void GeneratePropertyOneLine(CodeWriter &writer,
+ const std::string &name,
+ const std::string &type,
+ const std::function<void()> &body) {
+ // Generates Kotlin getter for properties
+ // e.g.:
+ // val prop: Mytype = x
+ writer.SetValue("_name", name);
+ writer.SetValue("_type", type);
+ writer += "val {{_name}} : {{_type}} = \\";
+ body();
+ }
+ static void GenerateGetterOneLine(CodeWriter &writer, const std::string &name,
+ const std::string &type,
+ const std::function<void()> &body) {
+ // Generates Kotlin getter for properties
+ // e.g.:
+ // val prop: Mytype get() = x
+ writer.SetValue("_name", name);
+ writer.SetValue("_type", type);
+ writer += "val {{_name}} : {{_type}} get() = \\";
+ body();
+ }
+
+ static void GenerateGetter(CodeWriter &writer, const std::string &name,
+ const std::string &type,
+ const std::function<void()> &body) {
+ // Generates Kotlin getter for properties
+ // e.g.:
+ // val prop: Mytype
+ // get() = {
+ // return x
+ // }
+ writer.SetValue("name", name);
+ writer.SetValue("type", type);
+ writer += "val {{name}} : {{type}}";
+ writer.IncrementIdentLevel();
+ writer += "get() {";
+ writer.IncrementIdentLevel();
+ body();
+ writer.DecrementIdentLevel();
+ writer += "}";
+ writer.DecrementIdentLevel();
+ }
+
+ static void GenerateFun(CodeWriter &writer, const std::string &name,
+ const std::string &params,
+ const std::string &returnType,
+ const std::function<void()> &body,
+ bool gen_jvmstatic = false) {
+ // Generates Kotlin function
+ // e.g.:
+ // fun path(j: Int): Vec3 {
+ // return path(Vec3(), j)
+ // }
+ auto noreturn = returnType.empty();
+ writer.SetValue("name", name);
+ writer.SetValue("params", params);
+ writer.SetValue("return_type", noreturn ? "" : ": " + returnType);
+ GenerateJvmStaticAnnotation(writer, gen_jvmstatic);
+ writer += "fun {{name}}({{params}}) {{return_type}} {";
+ writer.IncrementIdentLevel();
+ body();
+ writer.DecrementIdentLevel();
+ writer += "}";
+ }
+
+ static void GenerateFunOneLine(CodeWriter &writer, const std::string &name,
+ const std::string &params,
+ const std::string &returnType,
+ const std::function<void()> &body,
+ bool gen_jvmstatic = false) {
+ // Generates Kotlin function
+ // e.g.:
+ // fun path(j: Int): Vec3 = return path(Vec3(), j)
+ writer.SetValue("name", name);
+ writer.SetValue("params", params);
+ writer.SetValue("return_type_p",
+ returnType.empty() ? "" : " : " + returnType);
+ GenerateJvmStaticAnnotation(writer, gen_jvmstatic);
+ writer += "fun {{name}}({{params}}){{return_type_p}} = \\";
+ body();
+ }
+
+ static void GenerateOverrideFun(CodeWriter &writer, const std::string &name,
+ const std::string &params,
+ const std::string &returnType,
+ const std::function<void()> &body) {
+ // Generates Kotlin function
+ // e.g.:
+ // override fun path(j: Int): Vec3 = return path(Vec3(), j)
+ writer += "override \\";
+ GenerateFun(writer, name, params, returnType, body);
+ }
+
+ static void GenerateOverrideFunOneLine(CodeWriter &writer,
+ const std::string &name,
+ const std::string &params,
+ const std::string &returnType,
+ const std::string &statement) {
+ // Generates Kotlin function
+ // e.g.:
+ // override fun path(j: Int): Vec3 = return path(Vec3(), j)
+ writer.SetValue("name", name);
+ writer.SetValue("params", params);
+ writer.SetValue("return_type",
+ returnType.empty() ? "" : " : " + returnType);
+ writer += "override fun {{name}}({{params}}){{return_type}} = \\";
+ writer += statement;
+ }
+
+ static std::string OffsetWrapperOneLine(const std::string &offset,
+ const std::string &found,
+ const std::string &not_found) {
+ return "val o = __offset(" + offset + "); return if (o != 0) " + found +
+ " else " + not_found;
+ }
+
+ static void OffsetWrapper(CodeWriter &code, const std::string &offset,
+ const std::function<void()> &found,
+ const std::function<void()> &not_found) {
+ code += "val o = __offset(" + offset + ")";
+ code += "return if (o != 0) {";
+ code.IncrementIdentLevel();
+ found();
+ code.DecrementIdentLevel();
+ code += "} else {";
+ code.IncrementIdentLevel();
+ not_found();
+ code.DecrementIdentLevel();
+ code += "}";
+ }
+
+ static std::string Indirect(const std::string &index, bool fixed) {
+ // We apply __indirect() and struct is not fixed.
+ if (!fixed) return "__indirect(" + index + ")";
+ return index;
+ }
+
+ static std::string NotFoundReturn(BaseType el) {
+ switch (el) {
+ case BASE_TYPE_FLOAT: return "0.0f";
+ case BASE_TYPE_DOUBLE: return "0.0";
+ case BASE_TYPE_BOOL: return "false";
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_INT:
+ case BASE_TYPE_CHAR:
+ case BASE_TYPE_SHORT: return "0";
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_UCHAR:
+ case BASE_TYPE_USHORT:
+ case BASE_TYPE_UTYPE: return "0u";
+ case BASE_TYPE_ULONG: return "0uL";
+ default: return "null";
+ }
+ }
+
+ // Prepend @JvmStatic to methods in companion object.
+ static void GenerateJvmStaticAnnotation(CodeWriter &code,
+ bool gen_jvmstatic) {
+ if (gen_jvmstatic) { code += "@JvmStatic"; }
+ }
+
+ // This tracks the current namespace used to determine if a type need to be
+ // prefixed by its namespace
+ const Namespace *cur_name_space_;
+};
+} // namespace kotlin
+
+bool GenerateKotlin(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ kotlin::KotlinGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_lobster.cpp b/contrib/libs/flatbuffers/src/idl_gen_lobster.cpp
new file mode 100644
index 0000000000..6fdd6dc26a
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_lobster.cpp
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+namespace lobster {
+
+class LobsterGenerator : public BaseGenerator {
+ public:
+ LobsterGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "" /* not used */, "_",
+ "lobster") {
+ static const char *const keywords[] = {
+ "nil", "true", "false", "return", "struct", "class",
+ "import", "int", "float", "string", "any", "def",
+ "is", "from", "program", "private", "coroutine", "resource",
+ "enum", "typeof", "var", "let", "pakfile", "switch",
+ "case", "default", "namespace", "not", "and", "or",
+ "bool",
+ };
+ keywords_.insert(std::begin(keywords), std::end(keywords));
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+
+ std::string NormalizedName(const Definition &definition) const {
+ return EscapeKeyword(definition.name);
+ }
+
+ std::string NormalizedName(const EnumVal &ev) const {
+ return EscapeKeyword(ev.name);
+ }
+
+ std::string NamespacedName(const Definition &def) {
+ return WrapInNameSpace(def.defined_namespace, NormalizedName(def));
+ }
+
+ std::string GenTypeName(const Type &type) {
+ auto bits = NumToString(SizeOf(type.base_type) * 8);
+ if (IsInteger(type.base_type)) return "int" + bits;
+ if (IsFloat(type.base_type)) return "float" + bits;
+ if (IsString(type)) return "string";
+ if (type.base_type == BASE_TYPE_STRUCT) return "table";
+ return "none";
+ }
+
+ std::string LobsterType(const Type &type) {
+ if (IsFloat(type.base_type)) return "float";
+ if (IsScalar(type.base_type) && type.enum_def)
+ return NormalizedName(*type.enum_def);
+ if (!IsScalar(type.base_type)) return "flatbuffers_offset";
+ return "int";
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const Type &type) {
+ return IsScalar(type.base_type)
+ ? MakeCamel(GenTypeBasic(type))
+ : (IsStruct(type) ? "Struct" : "UOffsetTRelative");
+ }
+
+ // This uses Python names for now..
+ std::string GenTypeBasic(const Type &type) {
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, ...) \
+ #PTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return ctypename[type.base_type];
+ }
+
+ // Generate a struct field, conditioned on its child type(s).
+ void GenStructAccessor(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, nullptr, " ");
+ std::string &code = *code_ptr;
+ auto offsets = NumToString(field.value.offset);
+ auto def = " def " + NormalizedName(field);
+ if (IsScalar(field.value.type.base_type)) {
+ std::string acc;
+ if (struct_def.fixed) {
+ acc = "buf_.read_" + GenTypeName(field.value.type) + "_le(pos_ + " +
+ offsets + ")";
+
+ } else {
+ auto defval = field.IsOptional() ? "0" : field.value.constant;
+ acc = "buf_.flatbuffers_field_" + GenTypeName(field.value.type) +
+ "(pos_, " + offsets + ", " + defval + ")";
+ }
+ if (field.value.type.enum_def)
+ acc = NormalizedName(*field.value.type.enum_def) + "(" + acc + ")";
+ if (field.IsOptional())
+ acc += ", buf_.flatbuffers_field_present(pos_, " + offsets + ")";
+ code += def + "():\n return " + acc + "\n";
+ return;
+ }
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ auto name = NamespacedName(*field.value.type.struct_def);
+ code += def + "():\n ";
+ if (struct_def.fixed) {
+ code += "return " + name + "{ buf_, pos_ + " + offsets + " }\n";
+ } else {
+ code += std::string("let o = buf_.flatbuffers_field_") +
+ (field.value.type.struct_def->fixed ? "struct" : "table") +
+ "(pos_, " + offsets + ")\n return if o: " + name +
+ " { buf_, o } else: nil\n";
+ }
+ break;
+ }
+ case BASE_TYPE_STRING:
+ code += def +
+ "():\n return buf_.flatbuffers_field_string(pos_, " +
+ offsets + ")\n";
+ break;
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ code += def + "(i:int):\n return ";
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ auto start = "buf_.flatbuffers_field_vector(pos_, " + offsets +
+ ") + i * " + NumToString(InlineSize(vectortype));
+ if (!(vectortype.struct_def->fixed)) {
+ start = "buf_.flatbuffers_indirect(" + start + ")";
+ }
+ code += NamespacedName(*field.value.type.struct_def) + " { buf_, " +
+ start + " }\n";
+ } else {
+ if (IsString(vectortype))
+ code += "buf_.flatbuffers_string";
+ else
+ code += "buf_.read_" + GenTypeName(vectortype) + "_le";
+ code += "(buf_.flatbuffers_field_vector(pos_, " + offsets +
+ ") + i * " + NumToString(InlineSize(vectortype)) + ")\n";
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ for (auto it = field.value.type.enum_def->Vals().begin();
+ it != field.value.type.enum_def->Vals().end(); ++it) {
+ auto &ev = **it;
+ if (ev.IsNonZero()) {
+ code += def + "_as_" + ev.name + "():\n return " +
+ NamespacedName(*ev.union_type.struct_def) +
+ " { buf_, buf_.flatbuffers_field_table(pos_, " + offsets +
+ ") }\n";
+ }
+ }
+ break;
+ }
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ if (IsVector(field.value.type)) {
+ code += def +
+ "_length():\n return "
+ "buf_.flatbuffers_field_vector_len(pos_, " +
+ offsets + ")\n";
+ }
+ }
+
+ // Generate table constructors, conditioned on its members' types.
+ void GenTableBuilders(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "struct " + NormalizedName(struct_def) +
+ "Builder:\n b_:flatbuffers_builder\n";
+ code += " def start():\n b_.StartObject(" +
+ NumToString(struct_def.fields.vec.size()) +
+ ")\n return this\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto offset = it - struct_def.fields.vec.begin();
+ code += " def add_" + NormalizedName(field) + "(" +
+ NormalizedName(field) + ":" + LobsterType(field.value.type) +
+ "):\n b_.Prepend" + GenMethod(field.value.type) + "Slot(" +
+ NumToString(offset) + ", " + NormalizedName(field);
+ if (IsScalar(field.value.type.base_type) && !field.IsOptional())
+ code += ", " + field.value.constant;
+ code += ")\n return this\n";
+ }
+ code += " def end():\n return b_.EndObject()\n\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (IsVector(field.value.type)) {
+ code += "def " + NormalizedName(struct_def) + "Start" +
+ MakeCamel(NormalizedName(field)) +
+ "Vector(b_:flatbuffers_builder, n_:int):\n b_.StartVector(";
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code +=
+ NumToString(elem_size) + ", n_, " + NumToString(alignment) + ")\n";
+ if (vector_type.base_type != BASE_TYPE_STRUCT ||
+ !vector_type.struct_def->fixed) {
+ code += "def " + NormalizedName(struct_def) + "Create" +
+ MakeCamel(NormalizedName(field)) +
+ "Vector(b_:flatbuffers_builder, v_:[" +
+ LobsterType(vector_type) + "]):\n b_.StartVector(" +
+ NumToString(elem_size) + ", v_.length, " +
+ NumToString(alignment) + ")\n reverse(v_) e_: b_.Prepend" +
+ GenMethod(vector_type) +
+ "(e_)\n return b_.EndVector(v_.length)\n";
+ }
+ code += "\n";
+ }
+ }
+ }
+
+ void GenStructPreDecl(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+ std::string &code = *code_ptr;
+ CheckNameSpace(struct_def, &code);
+ code += "class " + NormalizedName(struct_def) + "\n\n";
+ }
+
+ // Generate struct or table methods.
+ void GenStruct(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+ std::string &code = *code_ptr;
+ CheckNameSpace(struct_def, &code);
+ GenComment(struct_def.doc_comment, code_ptr, nullptr, "");
+ code += "class " + NormalizedName(struct_def) + " : flatbuffers_handle\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ GenStructAccessor(struct_def, field, code_ptr);
+ }
+ code += "\n";
+ if (!struct_def.fixed) {
+ // Generate a special accessor for the table that has been declared as
+ // the root type.
+ code += "def GetRootAs" + NormalizedName(struct_def) +
+ "(buf:string): return " + NormalizedName(struct_def) +
+ " { buf, buf.flatbuffers_indirect(0) }\n\n";
+ }
+ if (struct_def.fixed) {
+ // create a struct constructor function
+ GenStructBuilder(struct_def, code_ptr);
+ } else {
+ // Create a set of functions that allow table construction.
+ GenTableBuilders(struct_def, code_ptr);
+ }
+ }
+
+ // Generate enum declarations.
+ void GenEnum(const EnumDef &enum_def, std::string *code_ptr) {
+ if (enum_def.generated) return;
+ std::string &code = *code_ptr;
+ CheckNameSpace(enum_def, &code);
+ GenComment(enum_def.doc_comment, code_ptr, nullptr, "");
+ code += "enum " + NormalizedName(enum_def) + ":\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, nullptr, " ");
+ code += " " + enum_def.name + "_" + NormalizedName(ev) + " = " +
+ enum_def.ToString(ev) + "\n";
+ }
+ code += "\n";
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void StructBuilderArgs(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ StructBuilderArgs(*field.value.type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(),
+ code_ptr);
+ } else {
+ std::string &code = *code_ptr;
+ code += ", " + (nameprefix + NormalizedName(field)) + ":" +
+ LobsterType(field.value.type);
+ }
+ }
+ }
+
+ // Recursively generate struct construction statements and instert manual
+ // padding.
+ void StructBuilderBody(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += " b_.Prep(" + NumToString(struct_def.minalign) + ", " +
+ NumToString(struct_def.bytesize) + ")\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (field.padding)
+ code += " b_.Pad(" + NumToString(field.padding) + ")\n";
+ if (IsStruct(field.value.type)) {
+ StructBuilderBody(*field.value.type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(),
+ code_ptr);
+ } else {
+ code += " b_.Prepend" + GenMethod(field.value.type) + "(" +
+ nameprefix + NormalizedName(field) + ")\n";
+ }
+ }
+ }
+
+ // Create a struct with a builder and the struct's arguments.
+ void GenStructBuilder(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code +=
+ "def Create" + NormalizedName(struct_def) + "(b_:flatbuffers_builder";
+ StructBuilderArgs(struct_def, "", code_ptr);
+ code += "):\n";
+ StructBuilderBody(struct_def, "", code_ptr);
+ code += " return b_.Offset()\n\n";
+ }
+
+ void CheckNameSpace(const Definition &def, std::string *code_ptr) {
+ auto ns = GetNameSpace(def);
+ if (ns == current_namespace_) return;
+ current_namespace_ = ns;
+ std::string &code = *code_ptr;
+ code += "namespace " + ns + "\n\n";
+ }
+
+ bool generate() {
+ std::string code;
+ code += std::string("// ") + FlatBuffersGeneratedWarning() +
+ "\nimport flatbuffers\n\n";
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ GenEnum(enum_def, &code);
+ }
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ GenStructPreDecl(struct_def, &code);
+ }
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ GenStruct(struct_def, &code);
+ }
+ return SaveFile(GeneratedFileName(path_, file_name_, parser_.opts).c_str(),
+ code, false);
+ }
+
+ private:
+ std::unordered_set<std::string> keywords_;
+ std::string current_namespace_;
+};
+
+} // namespace lobster
+
+bool GenerateLobster(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ lobster::LobsterGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_lua.cpp b/contrib/libs/flatbuffers/src/idl_gen_lua.cpp
new file mode 100644
index 0000000000..9efc435e24
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_lua.cpp
@@ -0,0 +1,745 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <string>
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+namespace lua {
+
+// Hardcode spaces per indentation.
+const CommentConfig def_comment = { nullptr, "--", nullptr };
+const char *Indent = " ";
+const char *Comment = "-- ";
+const char *End = "end\n";
+const char *EndFunc = "end\n";
+const char *SelfData = "self.view";
+const char *SelfDataPos = "self.view.pos";
+const char *SelfDataBytes = "self.view.bytes";
+
+class LuaGenerator : public BaseGenerator {
+ public:
+ LuaGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "" /* not used */,
+ "" /* not used */, "lua") {
+ static const char *const keywords[] = {
+ "and", "break", "do", "else", "elseif", "end", "false", "for",
+ "function", "goto", "if", "in", "local", "nil", "not", "or",
+ "repeat", "return", "then", "true", "until", "while"
+ };
+ keywords_.insert(std::begin(keywords), std::end(keywords));
+ }
+
+ // Most field accessors need to retrieve and test the field offset first,
+ // this is the prefix code for that.
+ std::string OffsetPrefix(const FieldDef &field) {
+ return std::string(Indent) + "local o = " + SelfData + ":Offset(" +
+ NumToString(field.value.offset) + ")\n" + Indent +
+ "if o ~= 0 then\n";
+ }
+
+ // Begin a class declaration.
+ void BeginClass(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "local " + NormalizedName(struct_def) + " = {} -- the module\n";
+ code += "local " + NormalizedMetaName(struct_def) +
+ " = {} -- the class metatable\n";
+ code += "\n";
+ }
+
+ // Begin enum code with a class declaration.
+ void BeginEnum(const std::string &class_name, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "local " + class_name + " = {\n";
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : "_" + name;
+ }
+
+ std::string NormalizedName(const Definition &definition) const {
+ return EscapeKeyword(definition.name);
+ }
+
+ std::string NormalizedName(const EnumVal &ev) const {
+ return EscapeKeyword(ev.name);
+ }
+
+ std::string NormalizedMetaName(const Definition &definition) const {
+ return EscapeKeyword(definition.name) + "_mt";
+ }
+
+ // A single enum member.
+ void EnumMember(const EnumDef &enum_def, const EnumVal &ev,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += std::string(Indent) + NormalizedName(ev) + " = " +
+ enum_def.ToString(ev) + ",\n";
+ }
+
+ // End enum code.
+ void EndEnum(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "}\n";
+ }
+
+ void GenerateNewObjectPrototype(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "function " + NormalizedName(struct_def) + ".New()\n";
+ code += std::string(Indent) + "local o = {}\n";
+ code += std::string(Indent) +
+ "setmetatable(o, {__index = " + NormalizedMetaName(struct_def) +
+ "})\n";
+ code += std::string(Indent) + "return o\n";
+ code += EndFunc;
+ }
+
+ // Initialize a new struct or table from existing data.
+ void NewRootTypeFromBuffer(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "function " + NormalizedName(struct_def) + ".GetRootAs" +
+ NormalizedName(struct_def) + "(buf, offset)\n";
+ code += std::string(Indent) + "if type(buf) == \"string\" then\n";
+ code += std::string(Indent) + Indent +
+ "buf = flatbuffers.binaryArray.New(buf)\n";
+ code += std::string(Indent) + "end\n";
+ code += std::string(Indent) +
+ "local n = flatbuffers.N.UOffsetT:Unpack(buf, offset)\n";
+ code += std::string(Indent) + "local o = " + NormalizedName(struct_def) +
+ ".New()\n";
+ code += std::string(Indent) + "o:Init(buf, n + offset)\n";
+ code += std::string(Indent) + "return o\n";
+ code += EndFunc;
+ }
+
+ // Initialize an existing object with other data, to avoid an allocation.
+ void InitializeExisting(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += "Init(buf, pos)\n";
+ code +=
+ std::string(Indent) + SelfData + " = flatbuffers.view.New(buf, pos)\n";
+ code += EndFunc;
+ }
+
+ // Get the length of a vector.
+ void GetVectorLen(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "Length()\n";
+ code += OffsetPrefix(field);
+ code +=
+ std::string(Indent) + Indent + "return " + SelfData + ":VectorLen(o)\n";
+ code += std::string(Indent) + End;
+ code += std::string(Indent) + "return 0\n";
+ code += EndFunc;
+ }
+
+ // Get the value of a struct's scalar.
+ void GetScalarFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "()\n";
+ code += std::string(Indent) + "return " + getter;
+ code += std::string(SelfDataPos) + " + " + NumToString(field.value.offset) +
+ ")\n";
+ code += EndFunc;
+ }
+
+ // Get the value of a table's scalar.
+ void GetScalarFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "()\n";
+ code += OffsetPrefix(field);
+ getter += std::string("o + ") + SelfDataPos + ")";
+ auto is_bool = field.value.type.base_type == BASE_TYPE_BOOL;
+ if (is_bool) { getter = "(" + getter + " ~= 0)"; }
+ code += std::string(Indent) + Indent + "return " + getter + "\n";
+ code += std::string(Indent) + End;
+ std::string default_value;
+ if (is_bool) {
+ default_value = field.value.constant == "0" ? "false" : "true";
+ } else {
+ default_value = field.value.constant;
+ }
+ code += std::string(Indent) + "return " + default_value + "\n";
+ code += EndFunc;
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Struct.
+ void GetStructFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(obj)\n";
+ code += std::string(Indent) + "obj:Init(" + SelfDataBytes + ", " +
+ SelfDataPos + " + ";
+ code += NumToString(field.value.offset) + ")\n";
+ code += std::string(Indent) + "return obj\n";
+ code += EndFunc;
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Table.
+ void GetStructFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "()\n";
+ code += OffsetPrefix(field);
+ if (field.value.type.struct_def->fixed) {
+ code +=
+ std::string(Indent) + Indent + "local x = o + " + SelfDataPos + "\n";
+ } else {
+ code += std::string(Indent) + Indent + "local x = " + SelfData +
+ ":Indirect(o + " + SelfDataPos + ")\n";
+ }
+ code += std::string(Indent) + Indent + "local obj = require('" +
+ TypeNameWithNamespace(field) + "').New()\n";
+ code +=
+ std::string(Indent) + Indent + "obj:Init(" + SelfDataBytes + ", x)\n";
+ code += std::string(Indent) + Indent + "return obj\n";
+ code += std::string(Indent) + End;
+ code += EndFunc;
+ }
+
+ // Get the value of a string.
+ void GetStringField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "()\n";
+ code += OffsetPrefix(field);
+ code +=
+ std::string(Indent) + Indent + "return " + GenGetter(field.value.type);
+ code += std::string("o + ") + SelfDataPos + ")\n";
+ code += std::string(Indent) + End;
+ code += EndFunc;
+ }
+
+ // Get the value of a union from an object.
+ void GetUnionField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "()\n";
+ code += OffsetPrefix(field);
+
+ // TODO(rw): this works and is not the good way to it:
+ // bool is_native_table = TypeName(field) == "*flatbuffers.Table";
+ // if (is_native_table) {
+ // code += std::string(Indent) + Indent + "from flatbuffers.table import
+ // Table\n";
+ //} else {
+ // code += std::string(Indent) + Indent +
+ // code += "from ." + TypeName(field) + " import " + TypeName(field) +
+ // "\n";
+ //}
+ code +=
+ std::string(Indent) + Indent +
+ "local obj = "
+ "flatbuffers.view.New(require('flatbuffers.binaryarray').New(0), 0)\n";
+ code += std::string(Indent) + Indent + GenGetter(field.value.type) +
+ "obj, o)\n";
+ code += std::string(Indent) + Indent + "return obj\n";
+ code += std::string(Indent) + End;
+ code += EndFunc;
+ }
+
+ // Get the value of a vector's struct member.
+ void GetMemberOfVectorOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(j)\n";
+ code += OffsetPrefix(field);
+ code +=
+ std::string(Indent) + Indent + "local x = " + SelfData + ":Vector(o)\n";
+ code += std::string(Indent) + Indent + "x = x + ((j-1) * ";
+ code += NumToString(InlineSize(vectortype)) + ")\n";
+ if (!(vectortype.struct_def->fixed)) {
+ code +=
+ std::string(Indent) + Indent + "x = " + SelfData + ":Indirect(x)\n";
+ }
+ code += std::string(Indent) + Indent + "local obj = require('" +
+ TypeNameWithNamespace(field) + "').New()\n";
+ code +=
+ std::string(Indent) + Indent + "obj:Init(" + SelfDataBytes + ", x)\n";
+ code += std::string(Indent) + Indent + "return obj\n";
+ code += std::string(Indent) + End;
+ code += EndFunc;
+ }
+
+ // Get the value of a vector's non-struct member. Uses a named return
+ // argument to conveniently set the zero value for the result.
+ void GetMemberOfVectorOfNonStruct(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(j)\n";
+ code += OffsetPrefix(field);
+ code +=
+ std::string(Indent) + Indent + "local a = " + SelfData + ":Vector(o)\n";
+ code += std::string(Indent) + Indent;
+ code += "return " + GenGetter(field.value.type);
+ code += "a + ((j-1) * ";
+ code += NumToString(InlineSize(vectortype)) + "))\n";
+ code += std::string(Indent) + End;
+ if (IsString(vectortype)) {
+ code += std::string(Indent) + "return ''\n";
+ } else {
+ code += std::string(Indent) + "return 0\n";
+ }
+ code += EndFunc;
+ }
+
+ // Access a byte/ubyte vector as a string
+ void AccessByteVectorAsString(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "AsString(start, stop)\n";
+ code += std::string(Indent) + "return " + SelfData + ":VectorAsString(" +
+ NumToString(field.value.offset) + ", start, stop)\n";
+ code += EndFunc;
+ }
+
+ // Begin the creator function signature.
+ void BeginBuilderArgs(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += "function " + NormalizedName(struct_def) + ".Create" +
+ NormalizedName(struct_def);
+ code += "(builder";
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void StructBuilderArgs(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ StructBuilderArgs(*field.value.type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(),
+ code_ptr);
+ } else {
+ std::string &code = *code_ptr;
+ code += std::string(", ") + nameprefix;
+ code += MakeCamel(NormalizedName(field), false);
+ }
+ }
+ }
+
+ // End the creator function signature.
+ void EndBuilderArgs(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += ")\n";
+ }
+
+ // Recursively generate struct construction statements and instert manual
+ // padding.
+ void StructBuilderBody(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += std::string(Indent) + "builder:Prep(" +
+ NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ")\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (field.padding)
+ code += std::string(Indent) + "builder:Pad(" +
+ NumToString(field.padding) + ")\n";
+ if (IsStruct(field.value.type)) {
+ StructBuilderBody(*field.value.type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(),
+ code_ptr);
+ } else {
+ code +=
+ std::string(Indent) + "builder:Prepend" + GenMethod(field) + "(";
+ code += nameprefix + MakeCamel(NormalizedName(field), false) + ")\n";
+ }
+ }
+ }
+
+ void EndBuilderBody(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += std::string(Indent) + "return builder:Offset()\n";
+ code += EndFunc;
+ }
+
+ // Get the value of a table's starting offset.
+ void GetStartOfTable(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "function " + NormalizedName(struct_def) + ".Start";
+ code += "(builder) ";
+ code += "builder:StartObject(";
+ code += NumToString(struct_def.fields.vec.size());
+ code += ") end\n";
+ }
+
+ // Set the value of a table's field.
+ void BuildFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ const size_t offset, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "function " + NormalizedName(struct_def) + ".Add" +
+ MakeCamel(NormalizedName(field));
+ code += "(builder, ";
+ code += MakeCamel(NormalizedName(field), false);
+ code += ") ";
+ code += "builder:Prepend";
+ code += GenMethod(field) + "Slot(";
+ code += NumToString(offset) + ", ";
+ // todo: i don't need to cast in Lua, but am I missing something?
+ // if (!IsScalar(field.value.type.base_type) && (!struct_def.fixed)) {
+ // code += "flatbuffers.N.UOffsetTFlags.py_type";
+ // code += "(";
+ // code += MakeCamel(NormalizedName(field), false) + ")";
+ // } else {
+ code += MakeCamel(NormalizedName(field), false);
+ // }
+ code += ", " + field.value.constant;
+ code += ") end\n";
+ }
+
+ // Set the value of one of the members of a table's vector.
+ void BuildVectorOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "function " + NormalizedName(struct_def) + ".Start";
+ code += MakeCamel(NormalizedName(field));
+ code += "Vector(builder, numElems) return builder:StartVector(";
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code += NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment);
+ code += ") end\n";
+ }
+
+ // Get the offset of the end of a table.
+ void GetEndOffsetOnTable(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "function " + NormalizedName(struct_def) + ".End";
+ code += "(builder) ";
+ code += "return builder:EndObject() end\n";
+ }
+
+ // Generate the receiver for function signatures.
+ void GenReceiver(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "function " + NormalizedMetaName(struct_def) + ":";
+ }
+
+ // Generate a struct field, conditioned on its child type(s).
+ void GenStructAccessor(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, &def_comment);
+ if (IsScalar(field.value.type.base_type)) {
+ if (struct_def.fixed) {
+ GetScalarFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetScalarFieldOfTable(struct_def, field, code_ptr);
+ }
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ GetStructFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetStructFieldOfTable(struct_def, field, code_ptr);
+ }
+ break;
+ case BASE_TYPE_STRING:
+ GetStringField(struct_def, field, code_ptr);
+ break;
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GetMemberOfVectorOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetMemberOfVectorOfNonStruct(struct_def, field, code_ptr);
+ if (vectortype.base_type == BASE_TYPE_CHAR ||
+ vectortype.base_type == BASE_TYPE_UCHAR) {
+ AccessByteVectorAsString(struct_def, field, code_ptr);
+ }
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: GetUnionField(struct_def, field, code_ptr); break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ if (IsVector(field.value.type)) {
+ GetVectorLen(struct_def, field, code_ptr);
+ }
+ }
+
+ // Generate table constructors, conditioned on its members' types.
+ void GenTableBuilders(const StructDef &struct_def, std::string *code_ptr) {
+ GetStartOfTable(struct_def, code_ptr);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto offset = it - struct_def.fields.vec.begin();
+ BuildFieldOfTable(struct_def, field, offset, code_ptr);
+ if (IsVector(field.value.type)) {
+ BuildVectorOfTable(struct_def, field, code_ptr);
+ }
+ }
+
+ GetEndOffsetOnTable(struct_def, code_ptr);
+ }
+
+ // Generate struct or table methods.
+ void GenStruct(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+
+ GenComment(struct_def.doc_comment, code_ptr, &def_comment);
+ BeginClass(struct_def, code_ptr);
+
+ GenerateNewObjectPrototype(struct_def, code_ptr);
+
+ if (!struct_def.fixed) {
+ // Generate a special accessor for the table that has been declared as
+ // the root type.
+ NewRootTypeFromBuffer(struct_def, code_ptr);
+ }
+
+ // Generate the Init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ InitializeExisting(struct_def, code_ptr);
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ GenStructAccessor(struct_def, field, code_ptr);
+ }
+
+ if (struct_def.fixed) {
+ // create a struct constructor function
+ GenStructBuilder(struct_def, code_ptr);
+ } else {
+ // Create a set of functions that allow table construction.
+ GenTableBuilders(struct_def, code_ptr);
+ }
+ }
+
+ // Generate enum declarations.
+ void GenEnum(const EnumDef &enum_def, std::string *code_ptr) {
+ if (enum_def.generated) return;
+
+ GenComment(enum_def.doc_comment, code_ptr, &def_comment);
+ BeginEnum(NormalizedName(enum_def), code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, &def_comment, Indent);
+ EnumMember(enum_def, ev, code_ptr);
+ }
+ EndEnum(code_ptr);
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetter(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return std::string(SelfData) + ":String(";
+ case BASE_TYPE_UNION: return std::string(SelfData) + ":Union(";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ default:
+ return std::string(SelfData) + ":Get(flatbuffers.N." +
+ MakeCamel(GenTypeGet(type)) + ", ";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const FieldDef &field) {
+ return IsScalar(field.value.type.base_type)
+ ? MakeCamel(GenTypeBasic(field.value.type))
+ : (IsStruct(field.value.type) ? "Struct" : "UOffsetTRelative");
+ }
+
+ std::string GenTypeBasic(const Type &type) {
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, ...) \
+ #PTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return ctypename[type.base_type];
+ }
+
+ std::string GenTypePointer(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "string";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return type.struct_def->name;
+ case BASE_TYPE_UNION:
+ // fall through
+ default: return "*flatbuffers.Table";
+ }
+ }
+
+ std::string GenTypeGet(const Type &type) {
+ return IsScalar(type.base_type) ? GenTypeBasic(type) : GenTypePointer(type);
+ }
+
+ std::string GetNamespace(const Type &type) {
+ return type.struct_def->defined_namespace->GetFullyQualifiedName(
+ type.struct_def->name);
+ }
+
+ std::string TypeName(const FieldDef &field) {
+ return GenTypeGet(field.value.type);
+ }
+
+ std::string TypeNameWithNamespace(const FieldDef &field) {
+ return GetNamespace(field.value.type);
+ }
+
+ // Create a struct with a builder and the struct's arguments.
+ void GenStructBuilder(const StructDef &struct_def, std::string *code_ptr) {
+ BeginBuilderArgs(struct_def, code_ptr);
+ StructBuilderArgs(struct_def, "", code_ptr);
+ EndBuilderArgs(code_ptr);
+
+ StructBuilderBody(struct_def, "", code_ptr);
+ EndBuilderBody(code_ptr);
+ }
+
+ bool generate() {
+ if (!generateEnums()) return false;
+ if (!generateStructs()) return false;
+ return true;
+ }
+
+ private:
+ bool generateEnums() {
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ std::string enumcode;
+ GenEnum(enum_def, &enumcode);
+ if (!SaveType(enum_def, enumcode, false)) return false;
+ }
+ return true;
+ }
+
+ bool generateStructs() {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ std::string declcode;
+ GenStruct(struct_def, &declcode);
+ if (!SaveType(struct_def, declcode, true)) return false;
+ }
+ return true;
+ }
+
+ // Begin by declaring namespace and imports.
+ void BeginFile(const std::string &name_space_name, const bool needs_imports,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += std::string(Comment) + FlatBuffersGeneratedWarning() + "\n\n";
+ code += std::string(Comment) + "namespace: " + name_space_name + "\n\n";
+ if (needs_imports) {
+ code += "local flatbuffers = require('flatbuffers')\n\n";
+ }
+ }
+
+ // Save out the generated code for a Lua Table type.
+ bool SaveType(const Definition &def, const std::string &classcode,
+ bool needs_imports) {
+ if (!classcode.length()) return true;
+
+ std::string namespace_dir = path_;
+ auto &namespaces = def.defined_namespace->components;
+ for (auto it = namespaces.begin(); it != namespaces.end(); ++it) {
+ if (it != namespaces.begin()) namespace_dir += kPathSeparator;
+ namespace_dir += *it;
+ // std::string init_py_filename = namespace_dir + "/__init__.py";
+ // SaveFile(init_py_filename.c_str(), "", false);
+ }
+
+ std::string code = "";
+ BeginFile(LastNamespacePart(*def.defined_namespace), needs_imports, &code);
+ code += classcode;
+ code += "\n";
+ code +=
+ "return " + NormalizedName(def) + " " + Comment + "return the module";
+ std::string filename =
+ NamespaceDir(*def.defined_namespace) + NormalizedName(def) + ".lua";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ private:
+ std::unordered_set<std::string> keywords_;
+};
+
+} // namespace lua
+
+bool GenerateLua(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ lua::LuaGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_php.cpp b/contrib/libs/flatbuffers/src/idl_gen_php.cpp
new file mode 100644
index 0000000000..dd3ed68189
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_php.cpp
@@ -0,0 +1,939 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <string>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+namespace php {
+// Hardcode spaces per indentation.
+const std::string Indent = " ";
+class PhpGenerator : public BaseGenerator {
+ public:
+ PhpGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "\\", "\\", "php") {}
+ bool generate() {
+ if (!GenerateEnums()) return false;
+ if (!GenerateStructs()) return false;
+ return true;
+ }
+
+ private:
+ bool GenerateEnums() {
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ std::string enumcode;
+ GenEnum(enum_def, &enumcode);
+ if (!SaveType(enum_def, enumcode, false)) return false;
+ }
+ return true;
+ }
+
+ bool GenerateStructs() {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ std::string declcode;
+ GenStruct(struct_def, &declcode);
+ if (!SaveType(struct_def, declcode, true)) return false;
+ }
+ return true;
+ }
+
+ // Begin by declaring namespace and imports.
+ void BeginFile(const std::string &name_space_name, const bool needs_imports,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "<?php\n";
+ code = code + "// " + FlatBuffersGeneratedWarning() + "\n\n";
+
+ if (!name_space_name.empty()) {
+ code += "namespace " + name_space_name + ";\n\n";
+ }
+
+ if (needs_imports) {
+ code += "use \\Google\\FlatBuffers\\Struct;\n";
+ code += "use \\Google\\FlatBuffers\\Table;\n";
+ code += "use \\Google\\FlatBuffers\\ByteBuffer;\n";
+ code += "use \\Google\\FlatBuffers\\FlatBufferBuilder;\n";
+ code += "\n";
+ }
+ }
+
+ // Save out the generated code for a Php Table type.
+ bool SaveType(const Definition &def, const std::string &classcode,
+ bool needs_imports) {
+ if (!classcode.length()) return true;
+
+ std::string code = "";
+ BeginFile(FullNamespace("\\", *def.defined_namespace), needs_imports,
+ &code);
+ code += classcode;
+
+ std::string filename =
+ NamespaceDir(*def.defined_namespace) + def.name + ".php";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ // Begin a class declaration.
+ static void BeginClass(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ if (struct_def.fixed) {
+ code += "class " + struct_def.name + " extends Struct\n";
+ } else {
+ code += "class " + struct_def.name + " extends Table\n";
+ }
+ code += "{\n";
+ }
+
+ static void EndClass(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "}\n";
+ }
+
+ // Begin enum code with a class declaration.
+ static void BeginEnum(const std::string &class_name, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "class " + class_name + "\n{\n";
+ }
+
+ // A single enum member.
+ static void EnumMember(const EnumDef &enum_def, const EnumVal &ev,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += Indent + "const ";
+ code += ev.name;
+ code += " = ";
+ code += enum_def.ToString(ev) + ";\n";
+ }
+
+ // End enum code.
+ static void EndEnum(std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "}\n";
+ }
+
+ // Initialize a new struct or table from existing data.
+ static void NewRootTypeFromBuffer(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param ByteBuffer $bb\n";
+ code += Indent + " * @return " + struct_def.name + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function getRootAs";
+ code += struct_def.name;
+ code += "(ByteBuffer $bb)\n";
+ code += Indent + "{\n";
+
+ code += Indent + Indent + "$obj = new " + struct_def.name + "();\n";
+ code += Indent + Indent;
+ code += "return ($obj->init($bb->getInt($bb->getPosition())";
+ code += " + $bb->getPosition(), $bb));\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Initialize an existing object with other data, to avoid an allocation.
+ static void InitializeExisting(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param int $_i offset\n";
+ code += Indent + " * @param ByteBuffer $_bb\n";
+ code += Indent + " * @return " + struct_def.name + "\n";
+ code += Indent + " **/\n";
+ code += Indent + "public function init($_i, ByteBuffer $_bb)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$this->bb_pos = $_i;\n";
+ code += Indent + Indent + "$this->bb = $_bb;\n";
+ code += Indent + Indent + "return $this;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the length of a vector.
+ static void GetVectorLen(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return int\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name) + "Length()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(";
+ code += NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent;
+ code += "return $o != 0 ? $this->__vector_len($o) : 0;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get a [ubyte] vector as a byte array.
+ static void GetUByte(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return string\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name) + "Bytes()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "return $this->__vector_as_bytes(";
+ code += NumToString(field.value.offset) + ");\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a struct's scalar.
+ static void GetScalarFieldOfStruct(const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return ";
+ code += GenTypeGet(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function " + getter;
+ code += MakeCamel(field.name) + "()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "return ";
+
+ code += "$this->bb->get";
+ code += MakeCamel(GenTypeGet(field.value.type));
+ code += "($this->bb_pos + ";
+ code += NumToString(field.value.offset) + ")";
+ code += ";\n";
+
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a table's scalar.
+ void GetScalarFieldOfTable(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return " + GenTypeGet(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n" + Indent + Indent +
+ "return $o != 0 ? ";
+ code += "$this->bb->get";
+ code += MakeCamel(GenTypeGet(field.value.type)) + "($o + $this->bb_pos)";
+ code += " : " + GenDefaultValue(field.value) + ";\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Struct.
+ void GetStructFieldOfStruct(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return " + GenTypeGet(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name) + "()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$obj = new ";
+ code += GenTypeGet(field.value.type) + "();\n";
+ code += Indent + Indent + "$obj->init($this->bb_pos + ";
+ code += NumToString(field.value.offset) + ", $this->bb);";
+ code += "\n" + Indent + Indent + "return $obj;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Table.
+ void GetStructFieldOfTable(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$obj = new ";
+ code += MakeCamel(GenTypeGet(field.value.type)) + "();\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent;
+ code += "return $o != 0 ? $obj->init(";
+ if (field.value.type.struct_def->fixed) {
+ code += "$o + $this->bb_pos, $this->bb) : ";
+ } else {
+ code += "$this->__indirect($o + $this->bb_pos), $this->bb) : ";
+ }
+ code += GenDefaultValue(field.value) + ";\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a string.
+ void GetStringField(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent;
+ code += "return $o != 0 ? $this->__string($o + $this->bb_pos) : ";
+ code += GenDefaultValue(field.value) + ";\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a union from an object.
+ void GetUnionField(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return" + GenTypeBasic(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name) + "($obj)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent;
+ code += "return $o != 0 ? $this->__union($obj, $o) : null;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a vector's struct member.
+ void GetMemberOfVectorOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ code += Indent + "/**\n";
+ code += Indent + " * @return" + GenTypeBasic(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "($j)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent + "$obj = new ";
+ code += MakeCamel(GenTypeGet(field.value.type)) + "();\n";
+
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ code += Indent + Indent;
+ code += "return $o != 0 ? $obj->init($this->bb_pos +" +
+ NumToString(field.value.offset) + ", $this->bb) : null;\n";
+ } else {
+ code += Indent + Indent + "return $o != 0 ? $obj->init(";
+ code += field.value.type.struct_def->fixed
+ ? "$o + $this->bb_pos"
+ : "$this->__indirect($o + $this->bb_pos)";
+ code += ", $this->bb) : null;\n";
+ }
+ break;
+ case BASE_TYPE_STRING:
+ code += "// base_type_string\n";
+ // TODO(chobie): do we need this?
+ break;
+ case BASE_TYPE_VECTOR:
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += Indent + Indent + "return $o != 0 ? $obj->init(";
+ if (vectortype.struct_def->fixed) {
+ code += "$this->__vector($o) + $j *";
+ code += NumToString(InlineSize(vectortype));
+ } else {
+ code += "$this->__indirect($this->__vector($o) + $j * ";
+ code += NumToString(InlineSize(vectortype)) + ")";
+ }
+ code += ", $this->bb) : null;\n";
+ }
+ break;
+ case BASE_TYPE_UNION:
+ code += Indent + Indent + "return $o != 0 ? $this->";
+ code += GenGetter(field.value.type) + "($obj, $o); null;\n";
+ break;
+ default: break;
+ }
+
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a vector's non-struct member. Uses a named return
+ // argument to conveniently set the zero value for the result.
+ void GetMemberOfVectorOfNonStruct(const FieldDef &field,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param int offset\n";
+ code += Indent + " * @return " + GenTypeGet(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "($j)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+
+ if (IsString(field.value.type.VectorType())) {
+ code += Indent + Indent;
+ code += "return $o != 0 ? $this->__string($this->__vector($o) + $j * ";
+ code += NumToString(InlineSize(vectortype)) + ") : ";
+ code += GenDefaultValue(field.value) + ";\n";
+ } else {
+ code += Indent + Indent + "return $o != 0 ? $this->bb->get";
+ code += MakeCamel(GenTypeGet(field.value.type));
+ code += "($this->__vector($o) + $j * ";
+ code += NumToString(InlineSize(vectortype)) + ") : ";
+ code += GenDefaultValue(field.value) + ";\n";
+ }
+ code += Indent + "}\n\n";
+ }
+
+ // Get the value of a vector's union member. Uses a named return
+ // argument to conveniently set the zero value for the result.
+ void GetMemberOfVectorOfUnion(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param int offset\n";
+ code += Indent + " * @return " + GenTypeGet(field.value.type) + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public function get";
+ code += MakeCamel(field.name);
+ code += "($j, $obj)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $this->__offset(" +
+ NumToString(field.value.offset) + ");\n";
+ code += Indent + Indent + "return $o != 0 ? ";
+ code += "$this->__union($obj, $this->__vector($o) + $j * ";
+ code += NumToString(InlineSize(vectortype)) + " - $this->bb_pos) : null;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ static void StructBuilderArgs(const StructDef &struct_def,
+ const char *nameprefix, std::string *code_ptr) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious
+ // these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ StructBuilderArgs(*field.value.type.struct_def,
+ (nameprefix + (field.name + "_")).c_str(), code_ptr);
+ } else {
+ std::string &code = *code_ptr;
+ code += std::string(", $") + nameprefix;
+ code += MakeCamel(field.name, false);
+ }
+ }
+ }
+
+ // Recursively generate struct construction statements and instert manual
+ // padding.
+ static void StructBuilderBody(const StructDef &struct_def,
+ const char *nameprefix, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += Indent + Indent + "$builder->prep(";
+ code += NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ");\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (field.padding) {
+ code += Indent + Indent + "$builder->pad(";
+ code += NumToString(field.padding) + ");\n";
+ }
+ if (IsStruct(field.value.type)) {
+ StructBuilderBody(*field.value.type.struct_def,
+ (nameprefix + (field.name + "_")).c_str(), code_ptr);
+ } else {
+ code += Indent + Indent + "$builder->put" + GenMethod(field) + "($";
+ code += nameprefix + MakeCamel(field.name, false) + ");\n";
+ }
+ }
+ }
+
+ // Get the value of a table's starting offset.
+ static void GetStartOfTable(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @return void\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function start" + struct_def.name;
+ code += "(FlatBufferBuilder $builder)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->StartObject(";
+ code += NumToString(struct_def.fields.vec.size());
+ code += ");\n";
+ code += Indent + "}\n\n";
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @return " + struct_def.name + "\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function create" + struct_def.name;
+ code += "(FlatBufferBuilder $builder, ";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+
+ if (field.deprecated) continue;
+ code += "$" + field.name;
+ if (it != struct_def.fields.vec.begin()) { code += ", "; }
+ }
+ code += ")\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->startObject(";
+ code += NumToString(struct_def.fields.vec.size());
+ code += ");\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ code += Indent + Indent + "self::add";
+ code += MakeCamel(field.name) + "($builder, $" + field.name + ");\n";
+ }
+
+ code += Indent + Indent + "$o = $builder->endObject();\n";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code += Indent + Indent + "$builder->required($o, ";
+ code += NumToString(field.value.offset);
+ code += "); // " + field.name + "\n";
+ }
+ }
+ code += Indent + Indent + "return $o;\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Set the value of a table's field.
+ static void BuildFieldOfTable(const FieldDef &field, const size_t offset,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @param " + GenTypeBasic(field.value.type) + "\n";
+ code += Indent + " * @return void\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function ";
+ code += "add" + MakeCamel(field.name);
+ code += "(FlatBufferBuilder $builder, ";
+ code += "$" + MakeCamel(field.name, false);
+ code += ")\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->add";
+ code += GenMethod(field) + "X(";
+ code += NumToString(offset) + ", ";
+
+ code += "$" + MakeCamel(field.name, false);
+ code += ", ";
+
+ if (field.value.type.base_type == BASE_TYPE_BOOL) {
+ code += "false";
+ } else {
+ code += field.value.constant;
+ }
+ code += ");\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Set the value of one of the members of a table's vector.
+ static void BuildVectorOfTable(const FieldDef &field, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @param array offset array\n";
+ code += Indent + " * @return int vector offset\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function create";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder $builder, array $data)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->startVector(";
+ code += NumToString(elem_size);
+ code += ", count($data), " + NumToString(alignment);
+ code += ");\n";
+ code += Indent + Indent;
+ code += "for ($i = count($data) - 1; $i >= 0; $i--) {\n";
+ if (IsScalar(field.value.type.VectorType().base_type)) {
+ code += Indent + Indent + Indent;
+ code += "$builder->put";
+ code += MakeCamel(GenTypeBasic(field.value.type.VectorType()));
+ code += "($data[$i]);\n";
+ } else {
+ code += Indent + Indent + Indent;
+ code += "$builder->putOffset($data[$i]);\n";
+ }
+ code += Indent + Indent + "}\n";
+ code += Indent + Indent + "return $builder->endVector();\n";
+ code += Indent + "}\n\n";
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @param int $numElems\n";
+ code += Indent + " * @return void\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function start";
+ code += MakeCamel(field.name);
+ code += "Vector(FlatBufferBuilder $builder, $numElems)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->startVector(";
+ code += NumToString(elem_size);
+ code += ", $numElems, " + NumToString(alignment);
+ code += ");\n";
+ code += Indent + "}\n\n";
+ }
+
+ // Get the offset of the end of a table.
+ void GetEndOffsetOnTable(const StructDef &struct_def, std::string *code_ptr) {
+ std::string &code = *code_ptr;
+
+ code += Indent + "/**\n";
+ code += Indent + " * @param FlatBufferBuilder $builder\n";
+ code += Indent + " * @return int table offset\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function end" + struct_def.name;
+ code += "(FlatBufferBuilder $builder)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$o = $builder->endObject();\n";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code += Indent + Indent + "$builder->required($o, ";
+ code += NumToString(field.value.offset);
+ code += "); // " + field.name + "\n";
+ }
+ }
+ code += Indent + Indent + "return $o;\n";
+ code += Indent + "}\n";
+
+ if (parser_.root_struct_def_ == &struct_def) {
+ code += "\n";
+ code += Indent + "public static function finish";
+ code += struct_def.name;
+ code += "Buffer(FlatBufferBuilder $builder, $offset)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->finish($offset";
+
+ if (parser_.file_identifier_.length())
+ code += ", \"" + parser_.file_identifier_ + "\"";
+ code += ");\n";
+ code += Indent + "}\n";
+ }
+ }
+
+ // Generate a struct field, conditioned on its child type(s).
+ void GenStructAccessor(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, nullptr, Indent.c_str());
+
+ if (IsScalar(field.value.type.base_type)) {
+ if (struct_def.fixed) {
+ GetScalarFieldOfStruct(field, code_ptr);
+ } else {
+ GetScalarFieldOfTable(field, code_ptr);
+ }
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ GetStructFieldOfStruct(field, code_ptr);
+ } else {
+ GetStructFieldOfTable(field, code_ptr);
+ }
+ break;
+ case BASE_TYPE_STRING: GetStringField(field, code_ptr); break;
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_UNION) {
+ GetMemberOfVectorOfUnion(field, code_ptr);
+ } else if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GetMemberOfVectorOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetMemberOfVectorOfNonStruct(field, code_ptr);
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: GetUnionField(field, code_ptr); break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ if (IsVector(field.value.type)) {
+ GetVectorLen(field, code_ptr);
+ if (field.value.type.element == BASE_TYPE_UCHAR) {
+ GetUByte(field, code_ptr);
+ }
+ }
+ }
+
+ // Generate table constructors, conditioned on its members' types.
+ void GenTableBuilders(const StructDef &struct_def, std::string *code_ptr) {
+ GetStartOfTable(struct_def, code_ptr);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto offset = it - struct_def.fields.vec.begin();
+ if (field.value.type.base_type == BASE_TYPE_UNION) {
+ std::string &code = *code_ptr;
+ code += Indent + "public static function add";
+ code += MakeCamel(field.name);
+ code += "(FlatBufferBuilder $builder, $offset)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "$builder->addOffsetX(";
+ code += NumToString(offset) + ", $offset, 0);\n";
+ code += Indent + "}\n\n";
+ } else {
+ BuildFieldOfTable(field, offset, code_ptr);
+ }
+ if (IsVector(field.value.type)) { BuildVectorOfTable(field, code_ptr); }
+ }
+
+ GetEndOffsetOnTable(struct_def, code_ptr);
+ }
+
+ // Generate struct or table methods.
+ void GenStruct(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+
+ GenComment(struct_def.doc_comment, code_ptr, nullptr);
+ BeginClass(struct_def, code_ptr);
+
+ if (!struct_def.fixed) {
+ // Generate a special accessor for the table that has been declared as
+ // the root type.
+ NewRootTypeFromBuffer(struct_def, code_ptr);
+ }
+
+ std::string &code = *code_ptr;
+ if (!struct_def.fixed) {
+ if (parser_.file_identifier_.length()) {
+ // Return the identifier
+ code += Indent + "public static function " + struct_def.name;
+ code += "Identifier()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "return \"";
+ code += parser_.file_identifier_ + "\";\n";
+ code += Indent + "}\n\n";
+
+ // Check if a buffer has the identifier.
+ code += Indent + "public static function " + struct_def.name;
+ code += "BufferHasIdentifier(ByteBuffer $buf)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "return self::";
+ code += "__has_identifier($buf, self::";
+ code += struct_def.name + "Identifier());\n";
+ code += Indent + "}\n\n";
+ }
+
+ if (parser_.file_extension_.length()) {
+ // Return the extension
+ code += Indent + "public static function " + struct_def.name;
+ code += "Extension()\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "return \"" + parser_.file_extension_;
+ code += "\";\n";
+ code += Indent + "}\n\n";
+ }
+ }
+
+ // Generate the Init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ InitializeExisting(struct_def, code_ptr);
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ GenStructAccessor(struct_def, field, code_ptr);
+ }
+
+ if (struct_def.fixed) {
+ // create a struct constructor function
+ GenStructBuilder(struct_def, code_ptr);
+ } else {
+ // Create a set of functions that allow table construction.
+ GenTableBuilders(struct_def, code_ptr);
+ }
+ EndClass(code_ptr);
+ }
+
+ // Generate enum declarations.
+ static void GenEnum(const EnumDef &enum_def, std::string *code_ptr) {
+ if (enum_def.generated) return;
+
+ GenComment(enum_def.doc_comment, code_ptr, nullptr);
+ BeginEnum(enum_def.name, code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, nullptr, Indent.c_str());
+ EnumMember(enum_def, ev, code_ptr);
+ }
+
+ std::string &code = *code_ptr;
+ code += "\n";
+ code += Indent + "private static $names = array(\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ code += Indent + Indent + enum_def.name + "::" + ev.name + "=>" + "\"" +
+ ev.name + "\",\n";
+ }
+
+ code += Indent + ");\n\n";
+ code += Indent + "public static function Name($e)\n";
+ code += Indent + "{\n";
+ code += Indent + Indent + "if (!isset(self::$names[$e])) {\n";
+ code += Indent + Indent + Indent + "throw new \\Exception();\n";
+ code += Indent + Indent + "}\n";
+ code += Indent + Indent + "return self::$names[$e];\n";
+ code += Indent + "}\n";
+ EndEnum(code_ptr);
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ static std::string GenGetter(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "__string";
+ case BASE_TYPE_STRUCT: return "__struct";
+ case BASE_TYPE_UNION: return "__union";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ default: return "Get";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ static std::string GenMethod(const FieldDef &field) {
+ return IsScalar(field.value.type.base_type)
+ ? MakeCamel(GenTypeBasic(field.value.type))
+ : (IsStruct(field.value.type) ? "Struct" : "Offset");
+ }
+
+ static std::string GenTypeBasic(const Type &type) {
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, ...) \
+ #NTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return ctypename[type.base_type];
+ }
+
+ std::string GenDefaultValue(const Value &value) {
+ if (value.type.enum_def) {
+ if (auto val = value.type.enum_def->FindByValue(value.constant)) {
+ return WrapInNameSpace(*value.type.enum_def) + "::" + val->name;
+ }
+ }
+
+ switch (value.type.base_type) {
+ case BASE_TYPE_BOOL: return value.constant == "0" ? "false" : "true";
+
+ case BASE_TYPE_STRING: return "null";
+
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG:
+ if (value.constant != "0") {
+ int64_t constant = StringToInt(value.constant.c_str());
+ return NumToString(constant);
+ }
+ return "0";
+
+ default: return value.constant;
+ }
+ }
+
+ static std::string GenTypePointer(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "string";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return type.struct_def->name;
+ case BASE_TYPE_UNION:
+ // fall through
+ default: return "Table";
+ }
+ }
+
+ static std::string GenTypeGet(const Type &type) {
+ return IsScalar(type.base_type) ? GenTypeBasic(type) : GenTypePointer(type);
+ }
+
+ // Create a struct with a builder and the struct's arguments.
+ static void GenStructBuilder(const StructDef &struct_def,
+ std::string *code_ptr) {
+ std::string &code = *code_ptr;
+ code += "\n";
+ code += Indent + "/**\n";
+ code += Indent + " * @return int offset\n";
+ code += Indent + " */\n";
+ code += Indent + "public static function create" + struct_def.name;
+ code += "(FlatBufferBuilder $builder";
+ StructBuilderArgs(struct_def, "", code_ptr);
+ code += ")\n";
+ code += Indent + "{\n";
+
+ StructBuilderBody(struct_def, "", code_ptr);
+
+ code += Indent + Indent + "return $builder->offset();\n";
+ code += Indent + "}\n";
+ }
+};
+} // namespace php
+
+bool GeneratePhp(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ php::PhpGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_python.cpp b/contrib/libs/flatbuffers/src/idl_gen_python.cpp
new file mode 100644
index 0000000000..b3f394ebf9
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_python.cpp
@@ -0,0 +1,1782 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include <cctype>
+#include <set>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+namespace python {
+
+// Hardcode spaces per indentation.
+const CommentConfig def_comment = { nullptr, "#", nullptr };
+const std::string Indent = " ";
+
+class PythonGenerator : public BaseGenerator {
+ public:
+ PythonGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "" /* not used */,
+ "" /* not used */, "py"),
+ float_const_gen_("float('nan')", "float('inf')", "float('-inf')") {
+ static const char *const keywords[] = {
+ "False", "None", "True", "and", "as", "assert", "break",
+ "class", "continue", "def", "del", "elif", "else", "except",
+ "finally", "for", "from", "global", "if", "import", "in",
+ "is", "lambda", "nonlocal", "not", "or", "pass", "raise",
+ "return", "try", "while", "with", "yield"
+ };
+ keywords_.insert(std::begin(keywords), std::end(keywords));
+ }
+
+ // Most field accessors need to retrieve and test the field offset first,
+ // this is the prefix code for that.
+ std::string OffsetPrefix(const FieldDef &field) {
+ return "\n" + Indent + Indent +
+ "o = flatbuffers.number_types.UOffsetTFlags.py_type" +
+ "(self._tab.Offset(" + NumToString(field.value.offset) + "))\n" +
+ Indent + Indent + "if o != 0:\n";
+ }
+
+ // Begin a class declaration.
+ void BeginClass(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "class " + NormalizedName(struct_def) + "(object):\n";
+ code += Indent + "__slots__ = ['_tab']";
+ code += "\n\n";
+ }
+
+ // Begin enum code with a class declaration.
+ void BeginEnum(const std::string &class_name, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "class " + class_name + "(object):\n";
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+
+ std::string NormalizedName(const Definition &definition) const {
+ return EscapeKeyword(definition.name);
+ }
+
+ std::string NormalizedName(const EnumVal &ev) const {
+ return EscapeKeyword(ev.name);
+ }
+
+ // Converts the name of a definition into upper Camel format.
+ std::string MakeUpperCamel(const Definition &definition) const {
+ return MakeCamel(NormalizedName(definition), true);
+ }
+
+ // Converts the name of a definition into lower Camel format.
+ std::string MakeLowerCamel(const Definition &definition) const {
+ auto name = MakeCamel(NormalizedName(definition), false);
+ name[0] = CharToLower(name[0]);
+ return name;
+ }
+
+ // Starts a new line and then indents.
+ std::string GenIndents(int num) {
+ return "\n" + std::string(num * Indent.length(), ' ');
+ }
+
+ // A single enum member.
+ void EnumMember(const EnumDef &enum_def, const EnumVal &ev,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += Indent;
+ code += NormalizedName(ev);
+ code += " = ";
+ code += enum_def.ToString(ev) + "\n";
+ }
+
+ // End enum code.
+ void EndEnum(std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "\n";
+ }
+
+ // Initialize a new struct or table from existing data.
+ void NewRootTypeFromBuffer(const StructDef &struct_def,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ code += Indent + "@classmethod\n";
+ code += Indent + "def GetRootAs";
+ code += "(cls, buf, offset=0):";
+ code += "\n";
+ code += Indent + Indent;
+ code += "n = flatbuffers.encode.Get";
+ code += "(flatbuffers.packer.uoffset, buf, offset)\n";
+ code += Indent + Indent + "x = " + NormalizedName(struct_def) + "()\n";
+ code += Indent + Indent + "x.Init(buf, n + offset)\n";
+ code += Indent + Indent + "return x\n";
+ code += "\n";
+
+ // Add an alias with the old name
+ code += Indent + "@classmethod\n";
+ code += Indent + "def GetRootAs";
+ code += NormalizedName(struct_def);
+ code += "(cls, buf, offset=0):\n";
+ code += Indent + Indent + "\"\"\"This method is deprecated. Please switch to GetRootAs.\"\"\"\n";
+ code += Indent + Indent + "return cls.GetRootAs(buf, offset)\n";
+ }
+
+ // Initialize an existing object with other data, to avoid an allocation.
+ void InitializeExisting(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += "Init(self, buf, pos):\n";
+ code += Indent + Indent + "self._tab = flatbuffers.table.Table(buf, pos)\n";
+ code += "\n";
+ }
+
+ // Get the length of a vector.
+ void GetVectorLen(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "Length(self";
+ code += "):" + OffsetPrefix(field);
+ code += Indent + Indent + Indent + "return self._tab.VectorLen(o)\n";
+ code += Indent + Indent + "return 0\n\n";
+ }
+
+ // Determines whether a vector is none or not.
+ void GetVectorIsNone(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "IsNone(self";
+ code += "):";
+ code += GenIndents(2) +
+ "o = flatbuffers.number_types.UOffsetTFlags.py_type" +
+ "(self._tab.Offset(" + NumToString(field.value.offset) + "))";
+ code += GenIndents(2) + "return o == 0";
+ code += "\n\n";
+ }
+
+ // Get the value of a struct's scalar.
+ void GetScalarFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self): return " + getter;
+ code += "self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(";
+ code += NumToString(field.value.offset) + "))\n";
+ }
+
+ // Get the value of a table's scalar.
+ void GetScalarFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ std::string getter = GenGetter(field.value.type);
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self):";
+ code += OffsetPrefix(field);
+ getter += "o + self._tab.Pos)";
+ auto is_bool = IsBool(field.value.type.base_type);
+ if (is_bool) { getter = "bool(" + getter + ")"; }
+ code += Indent + Indent + Indent + "return " + getter + "\n";
+ std::string default_value;
+ if (is_bool) {
+ default_value = field.value.constant == "0" ? "False" : "True";
+ } else {
+ default_value = IsFloat(field.value.type.base_type)
+ ? float_const_gen_.GenFloatConstant(field)
+ : field.value.constant;
+ }
+ code += Indent + Indent + "return " + default_value + "\n\n";
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Struct.
+ void GetStructFieldOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self, obj):\n";
+ code += Indent + Indent + "obj.Init(self._tab.Bytes, self._tab.Pos + ";
+ code += NumToString(field.value.offset) + ")";
+ code += "\n" + Indent + Indent + "return obj\n\n";
+ }
+
+ // Get the value of a fixed size array.
+ void GetArrayOfStruct(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ const auto vec_type = field.value.type.VectorType();
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ if (IsStruct(vec_type)) {
+ code += "(self, obj, i):\n";
+ code += Indent + Indent + "obj.Init(self._tab.Bytes, self._tab.Pos + ";
+ code += NumToString(field.value.offset) + " + i * ";
+ code += NumToString(InlineSize(vec_type));
+ code += ")\n" + Indent + Indent + "return obj\n\n";
+ } else {
+ auto getter = GenGetter(vec_type);
+ code += "(self): return [" + getter;
+ code += "self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(";
+ code += NumToString(field.value.offset) + " + i * ";
+ code += NumToString(InlineSize(vec_type));
+ code += ")) for i in range(";
+ code += NumToString(field.value.type.fixed_length) + ")]\n";
+ }
+ }
+
+ // Get a struct by initializing an existing struct.
+ // Specific to Table.
+ void GetStructFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self):";
+ code += OffsetPrefix(field);
+ if (field.value.type.struct_def->fixed) {
+ code += Indent + Indent + Indent + "x = o + self._tab.Pos\n";
+ } else {
+ code += Indent + Indent + Indent;
+ code += "x = self._tab.Indirect(o + self._tab.Pos)\n";
+ }
+ if (parser_.opts.include_dependence_headers) {
+ code += Indent + Indent + Indent;
+ code += "from " + GenPackageReference(field.value.type) + " import " +
+ TypeName(field) + "\n";
+ }
+ code += Indent + Indent + Indent + "obj = " + TypeName(field) + "()\n";
+ code += Indent + Indent + Indent + "obj.Init(self._tab.Bytes, x)\n";
+ code += Indent + Indent + Indent + "return obj\n";
+ code += Indent + Indent + "return None\n\n";
+ }
+
+ // Get the value of a string.
+ void GetStringField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self):";
+ code += OffsetPrefix(field);
+ code += Indent + Indent + Indent + "return " + GenGetter(field.value.type);
+ code += "o + self._tab.Pos)\n";
+ code += Indent + Indent + "return None\n\n";
+ }
+
+ // Get the value of a union from an object.
+ void GetUnionField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "(self):";
+ code += OffsetPrefix(field);
+
+ // TODO(rw): this works and is not the good way to it:
+ bool is_native_table = TypeName(field) == "*flatbuffers.Table";
+ if (is_native_table) {
+ code +=
+ Indent + Indent + Indent + "from flatbuffers.table import Table\n";
+ } else if (parser_.opts.include_dependence_headers) {
+ code += Indent + Indent + Indent;
+ code += "from " + GenPackageReference(field.value.type) + " import " +
+ TypeName(field) + "\n";
+ }
+ code += Indent + Indent + Indent + "obj = Table(bytearray(), 0)\n";
+ code += Indent + Indent + Indent + GenGetter(field.value.type);
+ code += "obj, o)\n" + Indent + Indent + Indent + "return obj\n";
+ code += Indent + Indent + "return None\n\n";
+ }
+
+ // Generate the package reference when importing a struct or enum from its
+ // module.
+ std::string GenPackageReference(const Type &type) {
+ Namespace *namespaces;
+ if (type.struct_def) {
+ namespaces = type.struct_def->defined_namespace;
+ } else if (type.enum_def) {
+ namespaces = type.enum_def->defined_namespace;
+ } else {
+ return "." + GenTypeGet(type);
+ }
+
+ return namespaces->GetFullyQualifiedName(GenTypeGet(type));
+ }
+
+ // Get the value of a vector's struct member.
+ void GetMemberOfVectorOfStruct(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self, j):" + OffsetPrefix(field);
+ code += Indent + Indent + Indent + "x = self._tab.Vector(o)\n";
+ code += Indent + Indent + Indent;
+ code += "x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * ";
+ code += NumToString(InlineSize(vectortype)) + "\n";
+ if (!(vectortype.struct_def->fixed)) {
+ code += Indent + Indent + Indent + "x = self._tab.Indirect(x)\n";
+ }
+ if (parser_.opts.include_dependence_headers) {
+ code += Indent + Indent + Indent;
+ code += "from " + GenPackageReference(field.value.type) + " import " +
+ TypeName(field) + "\n";
+ }
+ code += Indent + Indent + Indent + "obj = " + TypeName(field) + "()\n";
+ code += Indent + Indent + Indent + "obj.Init(self._tab.Bytes, x)\n";
+ code += Indent + Indent + Indent + "return obj\n";
+ code += Indent + Indent + "return None\n\n";
+ }
+
+ // Get the value of a vector's non-struct member. Uses a named return
+ // argument to conveniently set the zero value for the result.
+ void GetMemberOfVectorOfNonStruct(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field));
+ code += "(self, j):";
+ code += OffsetPrefix(field);
+ code += Indent + Indent + Indent + "a = self._tab.Vector(o)\n";
+ code += Indent + Indent + Indent;
+ code += "return " + GenGetter(field.value.type);
+ code += "a + flatbuffers.number_types.UOffsetTFlags.py_type(j * ";
+ code += NumToString(InlineSize(vectortype)) + "))\n";
+ if (IsString(vectortype)) {
+ code += Indent + Indent + "return \"\"\n";
+ } else {
+ code += Indent + Indent + "return 0\n";
+ }
+ code += "\n";
+ }
+
+ // Returns a non-struct vector as a numpy array. Much faster
+ // than iterating over the vector element by element.
+ void GetVectorOfNonStructAsNumpy(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto vectortype = field.value.type.VectorType();
+
+ // Currently, we only support accessing as numpy array if
+ // the vector type is a scalar.
+ if (!(IsScalar(vectortype.base_type))) { return; }
+
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "AsNumpy(self):";
+ code += OffsetPrefix(field);
+
+ code += Indent + Indent + Indent;
+ code += "return ";
+ code += "self._tab.GetVectorAsNumpy(flatbuffers.number_types.";
+ code += MakeCamel(GenTypeGet(field.value.type));
+ code += "Flags, o)\n";
+
+ if (IsString(vectortype)) {
+ code += Indent + Indent + "return \"\"\n";
+ } else {
+ code += Indent + Indent + "return 0\n";
+ }
+ code += "\n";
+ }
+
+ // Returns a nested flatbuffer as itself.
+ void GetVectorAsNestedFlatbuffer(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr) {
+ auto nested = field.attributes.Lookup("nested_flatbuffer");
+ if (!nested) { return; } // There is no nested flatbuffer.
+
+ std::string unqualified_name = nested->constant;
+ std::string qualified_name = nested->constant;
+ auto nested_root = parser_.LookupStruct(nested->constant);
+ if (nested_root == nullptr) {
+ qualified_name =
+ parser_.current_namespace_->GetFullyQualifiedName(nested->constant);
+ nested_root = parser_.LookupStruct(qualified_name);
+ }
+ FLATBUFFERS_ASSERT(nested_root); // Guaranteed to exist by parser.
+ (void)nested_root;
+
+ auto &code = *code_ptr;
+ GenReceiver(struct_def, code_ptr);
+ code += MakeCamel(NormalizedName(field)) + "NestedRoot(self):";
+
+ code += OffsetPrefix(field);
+
+ code += Indent + Indent + Indent;
+ code += "from " + qualified_name + " import " + unqualified_name + "\n";
+ code += Indent + Indent + Indent + "return " + unqualified_name;
+ code += ".GetRootAs";
+ code += "(self._tab.Bytes, self._tab.Vector(o))\n";
+ code += Indent + Indent + "return 0\n";
+ code += "\n";
+ }
+
+ // Begin the creator function signature.
+ void BeginBuilderArgs(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+
+ code += "\n";
+ code += "def Create" + NormalizedName(struct_def);
+ code += "(builder";
+ }
+
+ // Recursively generate arguments for a constructor, to deal with nested
+ // structs.
+ void StructBuilderArgs(const StructDef &struct_def,
+ const std::string nameprefix,
+ const std::string namesuffix, bool has_field_name,
+ const std::string fieldname_suffix,
+ std::string *code_ptr) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ const auto &type =
+ IsArray(field_type) ? field_type.VectorType() : field_type;
+ if (IsStruct(type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ auto subprefix = nameprefix;
+ if (has_field_name) {
+ subprefix += NormalizedName(field) + fieldname_suffix;
+ }
+ StructBuilderArgs(*field.value.type.struct_def, subprefix, namesuffix,
+ has_field_name, fieldname_suffix, code_ptr);
+ } else {
+ auto &code = *code_ptr;
+ code += std::string(", ") + nameprefix;
+ if (has_field_name) { code += MakeCamel(NormalizedName(field), false); }
+ code += namesuffix;
+ }
+ }
+ }
+
+ // End the creator function signature.
+ void EndBuilderArgs(std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "):\n";
+ }
+
+ // Recursively generate struct construction statements and instert manual
+ // padding.
+ void StructBuilderBody(const StructDef &struct_def, const char *nameprefix,
+ std::string *code_ptr, size_t index = 0,
+ bool in_array = false) {
+ auto &code = *code_ptr;
+ std::string indent(index * 4, ' ');
+ code +=
+ indent + " builder.Prep(" + NumToString(struct_def.minalign) + ", ";
+ code += NumToString(struct_def.bytesize) + ")\n";
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ const auto &field_type = field.value.type;
+ const auto &type =
+ IsArray(field_type) ? field_type.VectorType() : field_type;
+ if (field.padding)
+ code +=
+ indent + " builder.Pad(" + NumToString(field.padding) + ")\n";
+ if (IsStruct(field_type)) {
+ StructBuilderBody(*field_type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(),
+ code_ptr, index, in_array);
+ } else {
+ const auto index_var = "_idx" + NumToString(index);
+ if (IsArray(field_type)) {
+ code += indent + " for " + index_var + " in range(";
+ code += NumToString(field_type.fixed_length);
+ code += " , 0, -1):\n";
+ in_array = true;
+ }
+ if (IsStruct(type)) {
+ StructBuilderBody(
+ *field_type.struct_def,
+ (nameprefix + (NormalizedName(field) + "_")).c_str(), code_ptr,
+ index + 1, in_array);
+ } else {
+ code += IsArray(field_type) ? " " : "";
+ code += indent + " builder.Prepend" + GenMethod(field) + "(";
+ code += nameprefix + MakeCamel(NormalizedName(field), false);
+ size_t array_cnt = index + (IsArray(field_type) ? 1 : 0);
+ for (size_t i = 0; in_array && i < array_cnt; i++) {
+ code += "[_idx" + NumToString(i) + "-1]";
+ }
+ code += ")\n";
+ }
+ }
+ }
+ }
+
+ void EndBuilderBody(std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += " return builder.Offset()\n";
+ }
+
+ // Get the value of a table's starting offset.
+ void GetStartOfTable(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "def Start(builder): ";
+ code += "builder.StartObject(";
+ code += NumToString(struct_def.fields.vec.size());
+ code += ")\n";
+
+ // Add alias with the old name.
+ code += "def " + NormalizedName(struct_def) + "Start(builder):\n";
+ code += Indent + "\"\"\"This method is deprecated. Please switch to Start.\"\"\"\n";
+ code += Indent + "return Start(builder)\n";
+ }
+
+ // Set the value of a table's field.
+ void BuildFieldOfTable(const StructDef &struct_def, const FieldDef &field,
+ const size_t offset, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "def Add" + MakeCamel(NormalizedName(field));
+ code += "(builder, ";
+ code += MakeCamel(NormalizedName(field), false);
+ code += "): ";
+ code += "builder.Prepend";
+ code += GenMethod(field) + "Slot(";
+ code += NumToString(offset) + ", ";
+ if (!IsScalar(field.value.type.base_type) && (!struct_def.fixed)) {
+ code += "flatbuffers.number_types.UOffsetTFlags.py_type";
+ code += "(";
+ code += MakeCamel(NormalizedName(field), false) + ")";
+ } else {
+ code += MakeCamel(NormalizedName(field), false);
+ }
+ code += ", ";
+ code += IsFloat(field.value.type.base_type)
+ ? float_const_gen_.GenFloatConstant(field)
+ : field.value.constant;
+ code += ")\n";
+
+ // Add alias with the old name.
+ code += "def " + NormalizedName(struct_def) + "Add" + MakeCamel(NormalizedName(field));
+ code += "(builder, ";
+ code += MakeCamel(NormalizedName(field), false);
+ code += "):\n";
+ code += Indent + "\"\"\"This method is deprecated. Please switch to Add";
+ code += MakeCamel(NormalizedName(field)) + ".\"\"\"\n";
+ code += Indent + "return Add" + MakeCamel(NormalizedName(field));
+ code += "(builder, ";
+ code += MakeCamel(NormalizedName(field), false);
+ code += ")\n";
+
+ // Add alias with the old name.
+ }
+
+ // Set the value of one of the members of a table's vector.
+ void BuildVectorOfTable(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "def Start";
+ code += MakeCamel(NormalizedName(field));
+ code += "Vector(builder, numElems): return builder.StartVector(";
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code += NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment);
+ code += ")\n";
+
+ // Add alias with the old name.
+ code += "def " + NormalizedName(struct_def) + "Start";
+ code += MakeCamel(NormalizedName(field));
+ code += "Vector(builder, numElems):\n";
+ code += Indent + "\"\"\"This method is deprecated. Please switch to Start.\"\"\"\n";
+ code += Indent + "return Start";
+ code += MakeCamel(NormalizedName(field));
+ code += "Vector(builder, numElems)\n";
+ }
+
+ // Set the value of one of the members of a table's vector and fills in the
+ // elements from a bytearray. This is for simplifying the use of nested
+ // flatbuffers.
+ void BuildVectorOfTableFromBytes(const FieldDef &field, std::string *code_ptr) {
+ auto nested = field.attributes.Lookup("nested_flatbuffer");
+ if (!nested) { return; } // There is no nested flatbuffer.
+
+ std::string unqualified_name = nested->constant;
+ std::string qualified_name = nested->constant;
+ auto nested_root = parser_.LookupStruct(nested->constant);
+ if (nested_root == nullptr) {
+ qualified_name =
+ parser_.current_namespace_->GetFullyQualifiedName(nested->constant);
+ nested_root = parser_.LookupStruct(qualified_name);
+ }
+ FLATBUFFERS_ASSERT(nested_root); // Guaranteed to exist by parser.
+ (void)nested_root;
+
+ auto &code = *code_ptr;
+ code += "def MakeVectorFromBytes(builder, bytes):\n";
+ code += Indent + "builder.StartVector(";
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+ code += NumToString(elem_size);
+ code += ", len(bytes), " + NumToString(alignment);
+ code += ")\n";
+ code += Indent + "builder.head = builder.head - len(bytes)\n";
+ code += Indent + "builder.Bytes[builder.head : builder.head + len(bytes)]";
+ code += " = bytes\n";
+ code += Indent + "return builder.EndVector()\n";
+
+ // Add alias with the old name.
+ code += "def Make" + MakeCamel(NormalizedName(field));
+ code += "VectorFromBytes(builder, bytes):\n";
+ code += Indent + "builder.StartVector(";
+ code += NumToString(elem_size);
+ code += ", len(bytes), " + NumToString(alignment);
+ code += ")\n";
+ code += Indent + "builder.head = builder.head - len(bytes)\n";
+ code += Indent + "builder.Bytes[builder.head : builder.head + len(bytes)]";
+ code += " = bytes\n";
+ code += Indent + "return builder.EndVector()\n";
+ }
+
+ // Get the offset of the end of a table.
+ void GetEndOffsetOnTable(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "def End(builder): return builder.EndObject()\n";
+
+ // Add alias with the old name.
+ code += "def " + NormalizedName(struct_def) + "End(builder):\n";
+ code += Indent + "\"\"\"This method is deprecated. Please switch to End.\"\"\"\n";
+ code += Indent + "return End(builder)";
+ }
+
+ // Generate the receiver for function signatures.
+ void GenReceiver(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += Indent + "# " + NormalizedName(struct_def) + "\n";
+ code += Indent + "def ";
+ }
+
+ // Generate a struct field, conditioned on its child type(s).
+ void GenStructAccessor(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ GenComment(field.doc_comment, code_ptr, &def_comment, Indent.c_str());
+ if (IsScalar(field.value.type.base_type)) {
+ if (struct_def.fixed) {
+ GetScalarFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetScalarFieldOfTable(struct_def, field, code_ptr);
+ }
+ } else if (IsArray(field.value.type)) {
+ GetArrayOfStruct(struct_def, field, code_ptr);
+ } else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ if (struct_def.fixed) {
+ GetStructFieldOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetStructFieldOfTable(struct_def, field, code_ptr);
+ }
+ break;
+ case BASE_TYPE_STRING:
+ GetStringField(struct_def, field, code_ptr);
+ break;
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GetMemberOfVectorOfStruct(struct_def, field, code_ptr);
+ } else {
+ GetMemberOfVectorOfNonStruct(struct_def, field, code_ptr);
+ GetVectorOfNonStructAsNumpy(struct_def, field, code_ptr);
+ GetVectorAsNestedFlatbuffer(struct_def, field, code_ptr);
+ }
+ break;
+ }
+ case BASE_TYPE_UNION: GetUnionField(struct_def, field, code_ptr); break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ if (IsVector(field.value.type) || IsArray(field.value.type)) {
+ GetVectorLen(struct_def, field, code_ptr);
+ GetVectorIsNone(struct_def, field, code_ptr);
+ }
+ }
+
+ // Generate struct sizeof.
+ void GenStructSizeOf(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += Indent + "@classmethod\n";
+ code += Indent + "def SizeOf(cls):\n";
+ code +=
+ Indent + Indent + "return " + NumToString(struct_def.bytesize) + "\n";
+ code += "\n";
+ }
+
+ // Generate table constructors, conditioned on its members' types.
+ void GenTableBuilders(const StructDef &struct_def, std::string *code_ptr) {
+ GetStartOfTable(struct_def, code_ptr);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto offset = it - struct_def.fields.vec.begin();
+ BuildFieldOfTable(struct_def, field, offset, code_ptr);
+ if (IsVector(field.value.type)) {
+ BuildVectorOfTable(struct_def, field, code_ptr);
+ BuildVectorOfTableFromBytes(field, code_ptr);
+ }
+ }
+
+ GetEndOffsetOnTable(struct_def, code_ptr);
+ }
+
+ // Generate function to check for proper file identifier
+ void GenHasFileIdentifier(const StructDef &struct_def,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ std::string escapedID;
+ // In the event any of file_identifier characters are special(NULL, \, etc),
+ // problems occur. To prevent this, convert all chars to their hex-escaped
+ // equivalent.
+ for (auto it = parser_.file_identifier_.begin();
+ it != parser_.file_identifier_.end(); ++it) {
+ escapedID += "\\x" + IntToStringHex(*it, 2);
+ }
+
+ code += Indent + "@classmethod\n";
+ code += Indent + "def " + NormalizedName(struct_def);
+ code += "BufferHasIdentifier(cls, buf, offset, size_prefixed=False):";
+ code += "\n";
+ code += Indent + Indent;
+ code += "return flatbuffers.util.BufferHasIdentifier(buf, offset, b\"";
+ code += escapedID;
+ code += "\", size_prefixed=size_prefixed)\n";
+ code += "\n";
+ }
+
+ // Generates struct or table methods.
+ void GenStruct(const StructDef &struct_def, std::string *code_ptr) {
+ if (struct_def.generated) return;
+
+ GenComment(struct_def.doc_comment, code_ptr, &def_comment);
+ BeginClass(struct_def, code_ptr);
+ if (!struct_def.fixed) {
+ // Generate a special accessor for the table that has been declared as
+ // the root type.
+ NewRootTypeFromBuffer(struct_def, code_ptr);
+ if (parser_.file_identifier_.length()) {
+ // Generate a special function to test file_identifier
+ GenHasFileIdentifier(struct_def, code_ptr);
+ }
+ } else {
+ // Generates the SizeOf method for all structs.
+ GenStructSizeOf(struct_def, code_ptr);
+ }
+ // Generates the Init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ InitializeExisting(struct_def, code_ptr);
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ GenStructAccessor(struct_def, field, code_ptr);
+ }
+
+ if (struct_def.fixed) {
+ // creates a struct constructor function
+ GenStructBuilder(struct_def, code_ptr);
+ } else {
+ // Creates a set of functions that allow table construction.
+ GenTableBuilders(struct_def, code_ptr);
+ }
+ }
+
+ void GenReceiverForObjectAPI(const StructDef &struct_def,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += GenIndents(1) + "# " + NormalizedName(struct_def) + "T";
+ code += GenIndents(1) + "def ";
+ }
+
+ void BeginClassForObjectAPI(const StructDef &struct_def,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code += "\n";
+ code += "class " + NormalizedName(struct_def) + "T(object):";
+ code += "\n";
+ }
+
+ // Gets the accoresponding python builtin type of a BaseType for scalars and
+ // string.
+ std::string GetBasePythonTypeForScalarAndString(const BaseType &base_type) {
+ if (IsBool(base_type)) {
+ return "bool";
+ } else if (IsFloat(base_type)) {
+ return "float";
+ } else if (IsInteger(base_type)) {
+ return "int";
+ } else if (base_type == BASE_TYPE_STRING) {
+ return "str";
+ } else {
+ FLATBUFFERS_ASSERT(false && "base_type is not a scalar or string type.");
+ return "";
+ }
+ }
+
+ std::string GetDefaultValue(const FieldDef &field) {
+ BaseType base_type = field.value.type.base_type;
+ if (IsBool(base_type)) {
+ return field.value.constant == "0" ? "False" : "True";
+ } else if (IsFloat(base_type)) {
+ return float_const_gen_.GenFloatConstant(field);
+ } else if (IsInteger(base_type)) {
+ return field.value.constant;
+ } else {
+ // For string, struct, and table.
+ return "None";
+ }
+ }
+
+ void GenUnionInit(const FieldDef &field, std::string *field_types_ptr,
+ std::set<std::string> *import_list,
+ std::set<std::string> *import_typing_list) {
+ // Gets all possible types in the union.
+ import_typing_list->insert("Union");
+ auto &field_types = *field_types_ptr;
+ field_types = "Union[";
+
+ std::string separator_string = ", ";
+ auto enum_def = field.value.type.enum_def;
+ for (auto it = enum_def->Vals().begin(); it != enum_def->Vals().end();
+ ++it) {
+ auto &ev = **it;
+ // Union only supports string and table.
+ std::string field_type;
+ switch (ev.union_type.base_type) {
+ case BASE_TYPE_STRUCT:
+ field_type = GenTypeGet(ev.union_type) + "T";
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference = GenPackageReference(ev.union_type);
+ field_type = package_reference + "." + field_type;
+ import_list->insert("import " + package_reference);
+ }
+ break;
+ case BASE_TYPE_STRING: field_type += "str"; break;
+ case BASE_TYPE_NONE: field_type += "None"; break;
+ default: break;
+ }
+ field_types += field_type + separator_string;
+ }
+
+ // Removes the last separator_string.
+ field_types.erase(field_types.length() - separator_string.size());
+ field_types += "]";
+
+ // Gets the import lists for the union.
+ if (parser_.opts.include_dependence_headers) {
+ // The package reference is generated based on enum_def, instead
+ // of struct_def in field.type. That's why GenPackageReference() is
+ // not used.
+ Namespace *namespaces = field.value.type.enum_def->defined_namespace;
+ auto package_reference = namespaces->GetFullyQualifiedName(
+ MakeUpperCamel(*(field.value.type.enum_def)));
+ auto union_name = MakeUpperCamel(*(field.value.type.enum_def));
+ import_list->insert("import " + package_reference);
+ }
+ }
+
+ void GenStructInit(const FieldDef &field, std::string *field_type_ptr,
+ std::set<std::string> *import_list,
+ std::set<std::string> *import_typing_list) {
+ import_typing_list->insert("Optional");
+ auto &field_type = *field_type_ptr;
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference = GenPackageReference(field.value.type);
+ field_type = package_reference + "." + TypeName(field) + "T]";
+ import_list->insert("import " + package_reference);
+ } else {
+ field_type = TypeName(field) + "T]";
+ }
+ field_type = "Optional[" + field_type;
+ }
+
+ void GenVectorInit(const FieldDef &field, std::string *field_type_ptr,
+ std::set<std::string> *import_list,
+ std::set<std::string> *import_typing_list) {
+ import_typing_list->insert("List");
+ auto &field_type = *field_type_ptr;
+ auto base_type = field.value.type.VectorType().base_type;
+ if (base_type == BASE_TYPE_STRUCT) {
+ field_type = GenTypeGet(field.value.type.VectorType()) + "T]";
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference =
+ GenPackageReference(field.value.type.VectorType());
+ field_type = package_reference + "." +
+ GenTypeGet(field.value.type.VectorType()) + "T]";
+ import_list->insert("import " + package_reference);
+ }
+ field_type = "List[" + field_type;
+ } else {
+ field_type =
+ "List[" + GetBasePythonTypeForScalarAndString(base_type) + "]";
+ }
+ }
+
+ void GenInitialize(const StructDef &struct_def, std::string *code_ptr,
+ std::set<std::string> *import_list) {
+ std::string code;
+ std::set<std::string> import_typing_list;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ // Determines field type, default value, and typing imports.
+ auto base_type = field.value.type.base_type;
+ std::string field_type;
+ switch (base_type) {
+ case BASE_TYPE_UNION: {
+ GenUnionInit(field, &field_type, import_list, &import_typing_list);
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ GenStructInit(field, &field_type, import_list, &import_typing_list);
+ break;
+ }
+ case BASE_TYPE_VECTOR:
+ case BASE_TYPE_ARRAY: {
+ GenVectorInit(field, &field_type, import_list, &import_typing_list);
+ break;
+ }
+ default:
+ // Scalar or sting fields.
+ field_type = GetBasePythonTypeForScalarAndString(base_type);
+ break;
+ }
+
+ auto default_value = GetDefaultValue(field);
+ // Wrties the init statement.
+ auto field_instance_name = MakeLowerCamel(field);
+ code += GenIndents(2) + "self." + field_instance_name + " = " +
+ default_value + " # type: " + field_type;
+ }
+
+ // Writes __init__ method.
+ auto &code_base = *code_ptr;
+ GenReceiverForObjectAPI(struct_def, code_ptr);
+ code_base += "__init__(self):";
+ if (code.empty()) {
+ code_base += GenIndents(2) + "pass";
+ } else {
+ code_base += code;
+ }
+ code_base += "\n";
+
+ // Merges the typing imports into import_list.
+ if (!import_typing_list.empty()) {
+ // Adds the try statement.
+ std::string typing_imports = "try:";
+ typing_imports += GenIndents(1) + "from typing import ";
+ std::string separator_string = ", ";
+ for (auto it = import_typing_list.begin(); it != import_typing_list.end();
+ ++it) {
+ const std::string &im = *it;
+ typing_imports += im + separator_string;
+ }
+ // Removes the last separator_string.
+ typing_imports.erase(typing_imports.length() - separator_string.size());
+
+ // Adds the except statement.
+ typing_imports += "\n";
+ typing_imports += "except:";
+ typing_imports += GenIndents(1) + "pass";
+ import_list->insert(typing_imports);
+ }
+
+ // Removes the import of the struct itself, if applied.
+ auto package_reference =
+ struct_def.defined_namespace->GetFullyQualifiedName(
+ MakeUpperCamel(struct_def));
+ auto struct_import = "import " + package_reference;
+ import_list->erase(struct_import);
+ }
+
+ void InitializeFromBuf(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto instance_name = MakeLowerCamel(struct_def);
+ auto struct_name = NormalizedName(struct_def);
+
+ code += GenIndents(1) + "@classmethod";
+ code += GenIndents(1) + "def InitFromBuf(cls, buf, pos):";
+ code += GenIndents(2) + instance_name + " = " + struct_name + "()";
+ code += GenIndents(2) + instance_name + ".Init(buf, pos)";
+ code += GenIndents(2) + "return cls.InitFromObj(" + instance_name + ")";
+ code += "\n";
+ }
+
+ void InitializeFromObjForObject(const StructDef &struct_def,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto instance_name = MakeLowerCamel(struct_def);
+ auto struct_name = NormalizedName(struct_def);
+
+ code += GenIndents(1) + "@classmethod";
+ code += GenIndents(1) + "def InitFromObj(cls, " + instance_name + "):";
+ code += GenIndents(2) + "x = " + struct_name + "T()";
+ code += GenIndents(2) + "x._UnPack(" + instance_name + ")";
+ code += GenIndents(2) + "return x";
+ code += "\n";
+ }
+
+ void GenUnPackForStruct(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto field_type = TypeName(field);
+
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference = GenPackageReference(field.value.type);
+ field_type = package_reference + "." + TypeName(field);
+ }
+
+ code += GenIndents(2) + "if " + struct_instance_name + "." +
+ field_accessor_name + "(";
+ // if field is a struct, we need to create an instance for it first.
+ if (struct_def.fixed && field.value.type.base_type == BASE_TYPE_STRUCT) {
+ code += field_type + "()";
+ }
+ code += ") is not None:";
+ code += GenIndents(3) + "self." + field_instance_name + " = " + field_type +
+ "T.InitFromObj(" + struct_instance_name + "." +
+ field_accessor_name + "(";
+ // A struct's accessor requires a struct buf instance.
+ if (struct_def.fixed && field.value.type.base_type == BASE_TYPE_STRUCT) {
+ code += field_type + "()";
+ }
+ code += "))";
+ }
+
+ void GenUnPackForUnion(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+ auto union_name = MakeUpperCamel(*(field.value.type.enum_def));
+
+ if (parser_.opts.include_dependence_headers) {
+ Namespace *namespaces = field.value.type.enum_def->defined_namespace;
+ auto package_reference = namespaces->GetFullyQualifiedName(
+ MakeUpperCamel(*(field.value.type.enum_def)));
+ union_name = package_reference + "." + union_name;
+ }
+ code += GenIndents(2) + "self." + field_instance_name + " = " + union_name +
+ "Creator(" + "self." + field_instance_name + "Type, " +
+ struct_instance_name + "." + field_accessor_name + "())";
+ }
+
+ void GenUnPackForStructVector(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+
+ code += GenIndents(2) + "if not " + struct_instance_name + "." +
+ field_accessor_name + "IsNone():";
+ code += GenIndents(3) + "self." + field_instance_name + " = []";
+ code += GenIndents(3) + "for i in range(" + struct_instance_name + "." +
+ field_accessor_name + "Length()):";
+
+ auto field_type_name = TypeName(field);
+ auto one_instance = field_type_name + "_";
+ one_instance[0] = CharToLower(one_instance[0]);
+
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference = GenPackageReference(field.value.type);
+ field_type_name = package_reference + "." + TypeName(field);
+ }
+
+ code += GenIndents(4) + "if " + struct_instance_name + "." +
+ field_accessor_name + "(i) is None:";
+ code += GenIndents(5) + "self." + field_instance_name + ".append(None)";
+ code += GenIndents(4) + "else:";
+ code += GenIndents(5) + one_instance + " = " + field_type_name +
+ "T.InitFromObj(" + struct_instance_name + "." +
+ field_accessor_name + "(i))";
+ code += GenIndents(5) + "self." + field_instance_name + ".append(" +
+ one_instance + ")";
+ }
+
+ void GenUnpackforScalarVectorHelper(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr, int indents) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+
+ code += GenIndents(indents) + "self." + field_instance_name + " = []";
+ code += GenIndents(indents) + "for i in range(" + struct_instance_name +
+ "." + field_accessor_name + "Length()):";
+ code += GenIndents(indents + 1) + "self." + field_instance_name +
+ ".append(" + struct_instance_name + "." + field_accessor_name +
+ "(i))";
+ }
+
+ void GenUnPackForScalarVector(const StructDef &struct_def,
+ const FieldDef &field, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+
+ code += GenIndents(2) + "if not " + struct_instance_name + "." +
+ field_accessor_name + "IsNone():";
+
+ // String does not have the AsNumpy method.
+ if (!(IsScalar(field.value.type.VectorType().base_type))) {
+ GenUnpackforScalarVectorHelper(struct_def, field, code_ptr, 3);
+ return;
+ }
+
+ code += GenIndents(3) + "if np is None:";
+ GenUnpackforScalarVectorHelper(struct_def, field, code_ptr, 4);
+
+ // If numpy exists, use the AsNumpy method to optimize the unpack speed.
+ code += GenIndents(3) + "else:";
+ code += GenIndents(4) + "self." + field_instance_name + " = " +
+ struct_instance_name + "." + field_accessor_name + "AsNumpy()";
+ }
+
+ void GenUnPackForScalar(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+
+ code += GenIndents(2) + "self." + field_instance_name + " = " +
+ struct_instance_name + "." + field_accessor_name + "()";
+ }
+
+ // Generates the UnPack method for the object class.
+ void GenUnPack(const StructDef &struct_def, std::string *code_ptr) {
+ std::string code;
+ // Items that needs to be imported. No duplicate modules will be imported.
+ std::set<std::string> import_list;
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto field_type = TypeName(field);
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ GenUnPackForStruct(struct_def, field, &code);
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ GenUnPackForUnion(struct_def, field, &code);
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GenUnPackForStructVector(struct_def, field, &code);
+ } else {
+ GenUnPackForScalarVector(struct_def, field, &code);
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ GenUnPackForScalarVector(struct_def, field, &code);
+ break;
+ }
+ default: GenUnPackForScalar(struct_def, field, &code);
+ }
+ }
+
+ // Writes import statements and code into the generated file.
+ auto &code_base = *code_ptr;
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+ auto struct_name = MakeUpperCamel(struct_def);
+
+ GenReceiverForObjectAPI(struct_def, code_ptr);
+ code_base += "_UnPack(self, " + struct_instance_name + "):";
+ code_base += GenIndents(2) + "if " + struct_instance_name + " is None:";
+ code_base += GenIndents(3) + "return";
+
+ // Write the import statements.
+ for (std::set<std::string>::iterator it = import_list.begin();
+ it != import_list.end(); ++it) {
+ code_base += GenIndents(2) + *it;
+ }
+
+ // Write the code.
+ code_base += code;
+ code_base += "\n";
+ }
+
+ void GenPackForStruct(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto struct_name = MakeUpperCamel(struct_def);
+
+ GenReceiverForObjectAPI(struct_def, code_ptr);
+ code += "Pack(self, builder):";
+ code += GenIndents(2) + "return Create" + struct_name + "(builder";
+
+ StructBuilderArgs(struct_def,
+ /* nameprefix = */ "self.",
+ /* namesuffix = */ "",
+ /* has_field_name = */ true,
+ /* fieldname_suffix = */ ".", code_ptr);
+ code += ")\n";
+ }
+
+ void GenPackForStructVectorField(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_prefix_ptr,
+ std::string *code_ptr) {
+ auto &code_prefix = *code_prefix_ptr;
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto struct_name = NormalizedName(struct_def);
+ auto field_accessor_name = MakeUpperCamel(field);
+
+ // Creates the field.
+ code_prefix +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ if (field.value.type.struct_def->fixed) {
+ code_prefix += GenIndents(3) + "Start" +
+ field_accessor_name + "Vector(builder, len(self." +
+ field_instance_name + "))";
+ code_prefix += GenIndents(3) + "for i in reversed(range(len(self." +
+ field_instance_name + "))):";
+ code_prefix +=
+ GenIndents(4) + "self." + field_instance_name + "[i].Pack(builder)";
+ code_prefix +=
+ GenIndents(3) + field_instance_name + " = builder.EndVector()";
+ } else {
+ // If the vector is a struct vector, we need to first build accessor for
+ // each struct element.
+ code_prefix += GenIndents(3) + field_instance_name + "list = []";
+ code_prefix += GenIndents(3);
+ code_prefix += "for i in range(len(self." + field_instance_name + ")):";
+ code_prefix += GenIndents(4) + field_instance_name + "list.append(self." +
+ field_instance_name + "[i].Pack(builder))";
+
+ code_prefix += GenIndents(3) + "Start" +
+ field_accessor_name + "Vector(builder, len(self." +
+ field_instance_name + "))";
+ code_prefix += GenIndents(3) + "for i in reversed(range(len(self." +
+ field_instance_name + "))):";
+ code_prefix += GenIndents(4) + "builder.PrependUOffsetTRelative" + "(" +
+ field_instance_name + "list[i])";
+ code_prefix +=
+ GenIndents(3) + field_instance_name + " = builder.EndVector()";
+ }
+
+ // Adds the field into the struct.
+ code += GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code += GenIndents(3) + "Add" + field_accessor_name +
+ "(builder, " + field_instance_name + ")";
+ }
+
+ void GenPackForScalarVectorFieldHelper(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_ptr, int indents) {
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_name = NormalizedName(struct_def);
+ auto vectortype = field.value.type.VectorType();
+
+ code += GenIndents(indents) + "Start" + field_accessor_name +
+ "Vector(builder, len(self." + field_instance_name + "))";
+ code += GenIndents(indents) + "for i in reversed(range(len(self." +
+ field_instance_name + "))):";
+ code += GenIndents(indents + 1) + "builder.Prepend";
+
+ std::string type_name;
+ switch (vectortype.base_type) {
+ case BASE_TYPE_BOOL: type_name = "Bool"; break;
+ case BASE_TYPE_CHAR: type_name = "Byte"; break;
+ case BASE_TYPE_UCHAR: type_name = "Uint8"; break;
+ case BASE_TYPE_SHORT: type_name = "Int16"; break;
+ case BASE_TYPE_USHORT: type_name = "Uint16"; break;
+ case BASE_TYPE_INT: type_name = "Int32"; break;
+ case BASE_TYPE_UINT: type_name = "Uint32"; break;
+ case BASE_TYPE_LONG: type_name = "Int64"; break;
+ case BASE_TYPE_ULONG: type_name = "Uint64"; break;
+ case BASE_TYPE_FLOAT: type_name = "Float32"; break;
+ case BASE_TYPE_DOUBLE: type_name = "Float64"; break;
+ case BASE_TYPE_STRING: type_name = "UOffsetTRelative"; break;
+ default: type_name = "VOffsetT"; break;
+ }
+ code += type_name;
+ }
+
+ void GenPackForScalarVectorField(const StructDef &struct_def,
+ const FieldDef &field,
+ std::string *code_prefix_ptr,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto &code_prefix = *code_prefix_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_name = NormalizedName(struct_def);
+
+ // Adds the field into the struct.
+ code += GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code += GenIndents(3) + "Add" + field_accessor_name +
+ "(builder, " + field_instance_name + ")";
+
+ // Creates the field.
+ code_prefix +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ // If the vector is a string vector, we need to first build accessor for
+ // each string element. And this generated code, needs to be
+ // placed ahead of code_prefix.
+ auto vectortype = field.value.type.VectorType();
+ if (IsString(vectortype)) {
+ code_prefix += GenIndents(3) + MakeLowerCamel(field) + "list = []";
+ code_prefix += GenIndents(3) + "for i in range(len(self." +
+ field_instance_name + ")):";
+ code_prefix += GenIndents(4) + MakeLowerCamel(field) +
+ "list.append(builder.CreateString(self." +
+ field_instance_name + "[i]))";
+ GenPackForScalarVectorFieldHelper(struct_def, field, code_prefix_ptr, 3);
+ code_prefix += "(" + MakeLowerCamel(field) + "list[i])";
+ code_prefix +=
+ GenIndents(3) + field_instance_name + " = builder.EndVector()";
+ return;
+ }
+
+ code_prefix += GenIndents(3) + "if np is not None and type(self." +
+ field_instance_name + ") is np.ndarray:";
+ code_prefix += GenIndents(4) + field_instance_name +
+ " = builder.CreateNumpyVector(self." + field_instance_name +
+ ")";
+ code_prefix += GenIndents(3) + "else:";
+ GenPackForScalarVectorFieldHelper(struct_def, field, code_prefix_ptr, 4);
+ code_prefix += "(self." + field_instance_name + "[i])";
+ code_prefix +=
+ GenIndents(4) + field_instance_name + " = builder.EndVector()";
+ }
+
+ void GenPackForStructField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_prefix_ptr,
+ std::string *code_ptr) {
+ auto &code_prefix = *code_prefix_ptr;
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_name = NormalizedName(struct_def);
+
+ if (field.value.type.struct_def->fixed) {
+ // Pure struct fields need to be created along with their parent
+ // structs.
+ code +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code += GenIndents(3) + field_instance_name + " = self." +
+ field_instance_name + ".Pack(builder)";
+ } else {
+ // Tables need to be created before their parent structs are created.
+ code_prefix +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code_prefix += GenIndents(3) + field_instance_name + " = self." +
+ field_instance_name + ".Pack(builder)";
+ code +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ }
+
+ code += GenIndents(3) + "Add" + field_accessor_name +
+ "(builder, " + field_instance_name + ")";
+ }
+
+ void GenPackForUnionField(const StructDef &struct_def, const FieldDef &field,
+ std::string *code_prefix_ptr,
+ std::string *code_ptr) {
+ auto &code_prefix = *code_prefix_ptr;
+ auto &code = *code_ptr;
+ auto field_instance_name = MakeLowerCamel(field);
+
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto struct_name = NormalizedName(struct_def);
+
+ // TODO(luwa): TypeT should be moved under the None check as well.
+ code_prefix +=
+ GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code_prefix += GenIndents(3) + field_instance_name + " = self." +
+ field_instance_name + ".Pack(builder)";
+ code += GenIndents(2) + "if self." + field_instance_name + " is not None:";
+ code += GenIndents(3) + "Add" + field_accessor_name +
+ "(builder, " + field_instance_name + ")";
+ }
+
+ void GenPackForTable(const StructDef &struct_def, std::string *code_ptr) {
+ auto &code_base = *code_ptr;
+ std::string code, code_prefix;
+ auto struct_instance_name = MakeLowerCamel(struct_def);
+ auto struct_name = NormalizedName(struct_def);
+
+ GenReceiverForObjectAPI(struct_def, code_ptr);
+ code_base += "Pack(self, builder):";
+ code += GenIndents(2) + "Start(builder)";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto field_accessor_name = MakeUpperCamel(field);
+ auto field_instance_name = MakeLowerCamel(field);
+
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ GenPackForStructField(struct_def, field, &code_prefix, &code);
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ GenPackForUnionField(struct_def, field, &code_prefix, &code);
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ GenPackForStructVectorField(struct_def, field, &code_prefix, &code);
+ } else {
+ GenPackForScalarVectorField(struct_def, field, &code_prefix, &code);
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ GenPackForScalarVectorField(struct_def, field, &code_prefix, &code);
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ code_prefix += GenIndents(2) + "if self." + field_instance_name +
+ " is not None:";
+ code_prefix += GenIndents(3) + field_instance_name +
+ " = builder.CreateString(self." + field_instance_name +
+ ")";
+ code += GenIndents(2) + "if self." + field_instance_name +
+ " is not None:";
+ code += GenIndents(3) + "Add" + field_accessor_name +
+ "(builder, " + field_instance_name + ")";
+ break;
+ }
+ default:
+ // Generates code for scalar values. If the value equals to the
+ // default value, builder will automatically ignore it. So we don't
+ // need to check the value ahead.
+ code += GenIndents(2) + "Add" + field_accessor_name +
+ "(builder, self." + field_instance_name + ")";
+ break;
+ }
+ }
+
+ code += GenIndents(2) + struct_instance_name + " = " + "End(builder)";
+ code += GenIndents(2) + "return " + struct_instance_name;
+
+ code_base += code_prefix + code;
+ code_base += "\n";
+ }
+
+ void GenStructForObjectAPI(const StructDef &struct_def,
+ std::string *code_ptr) {
+ if (struct_def.generated) return;
+
+ std::set<std::string> import_list;
+ std::string code;
+
+ // Creates an object class for a struct or a table
+ BeginClassForObjectAPI(struct_def, &code);
+
+ GenInitialize(struct_def, &code, &import_list);
+
+ InitializeFromBuf(struct_def, &code);
+
+ InitializeFromObjForObject(struct_def, &code);
+
+ GenUnPack(struct_def, &code);
+
+ if (struct_def.fixed) {
+ GenPackForStruct(struct_def, &code);
+ } else {
+ GenPackForTable(struct_def, &code);
+ }
+
+ // Adds the imports at top.
+ auto &code_base = *code_ptr;
+ code_base += "\n";
+ for (auto it = import_list.begin(); it != import_list.end(); it++) {
+ auto im = *it;
+ code_base += im + "\n";
+ }
+ code_base += code;
+ }
+
+ void GenUnionCreatorForStruct(const EnumDef &enum_def, const EnumVal &ev,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto union_name = NormalizedName(enum_def);
+ auto field_name = NormalizedName(ev);
+ auto field_type = GenTypeGet(ev.union_type) + "T";
+
+ code += GenIndents(1) + "if unionType == " + union_name + "()." +
+ field_name + ":";
+ if (parser_.opts.include_dependence_headers) {
+ auto package_reference = GenPackageReference(ev.union_type);
+ code += GenIndents(2) + "import " + package_reference;
+ field_type = package_reference + "." + field_type;
+ }
+ code += GenIndents(2) + "return " + field_type +
+ ".InitFromBuf(table.Bytes, table.Pos)";
+ }
+
+ void GenUnionCreatorForString(const EnumDef &enum_def, const EnumVal &ev,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto union_name = NormalizedName(enum_def);
+ auto field_name = NormalizedName(ev);
+
+ code += GenIndents(1) + "if unionType == " + union_name + "()." +
+ field_name + ":";
+ code += GenIndents(2) + "tab = Table(table.Bytes, table.Pos)";
+ code += GenIndents(2) + "union = tab.String(table.Pos)";
+ code += GenIndents(2) + "return union";
+ }
+
+ // Creates an union object based on union type.
+ void GenUnionCreator(const EnumDef &enum_def, std::string *code_ptr) {
+ auto &code = *code_ptr;
+ auto union_name = MakeUpperCamel(enum_def);
+
+ code += "\n";
+ code += "def " + union_name + "Creator(unionType, table):";
+ code += GenIndents(1) + "from flatbuffers.table import Table";
+ code += GenIndents(1) + "if not isinstance(table, Table):";
+ code += GenIndents(2) + "return None";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ // Union only supports string and table.
+ switch (ev.union_type.base_type) {
+ case BASE_TYPE_STRUCT:
+ GenUnionCreatorForStruct(enum_def, ev, &code);
+ break;
+ case BASE_TYPE_STRING:
+ GenUnionCreatorForString(enum_def, ev, &code);
+ break;
+ default: break;
+ }
+ }
+ code += GenIndents(1) + "return None";
+ code += "\n";
+ }
+
+ // Generate enum declarations.
+ void GenEnum(const EnumDef &enum_def, std::string *code_ptr) {
+ if (enum_def.generated) return;
+
+ GenComment(enum_def.doc_comment, code_ptr, &def_comment);
+ BeginEnum(NormalizedName(enum_def), code_ptr);
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ GenComment(ev.doc_comment, code_ptr, &def_comment, Indent.c_str());
+ EnumMember(enum_def, ev, code_ptr);
+ }
+ EndEnum(code_ptr);
+ }
+
+ // Returns the function name that is able to read a value of the given type.
+ std::string GenGetter(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "self._tab.String(";
+ case BASE_TYPE_UNION: return "self._tab.Union(";
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType());
+ default:
+ return "self._tab.Get(flatbuffers.number_types." +
+ MakeCamel(GenTypeGet(type)) + "Flags, ";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ std::string GenMethod(const FieldDef &field) {
+ return (IsScalar(field.value.type.base_type) || IsArray(field.value.type))
+ ? MakeCamel(GenTypeBasic(field.value.type))
+ : (IsStruct(field.value.type) ? "Struct" : "UOffsetTRelative");
+ }
+
+ std::string GenTypeBasic(const Type &type) {
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, ...) \
+ #PTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ return ctypename[IsArray(type) ? type.VectorType().base_type
+ : type.base_type];
+ }
+
+ std::string GenTypePointer(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "string";
+ case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType());
+ case BASE_TYPE_STRUCT: return type.struct_def->name;
+ case BASE_TYPE_UNION:
+ // fall through
+ default: return "*flatbuffers.Table";
+ }
+ }
+
+ std::string GenTypeGet(const Type &type) {
+ return IsScalar(type.base_type) ? GenTypeBasic(type) : GenTypePointer(type);
+ }
+
+ std::string TypeName(const FieldDef &field) {
+ return GenTypeGet(field.value.type);
+ }
+
+ // Create a struct with a builder and the struct's arguments.
+ void GenStructBuilder(const StructDef &struct_def, std::string *code_ptr) {
+ BeginBuilderArgs(struct_def, code_ptr);
+ StructBuilderArgs(struct_def,
+ /* nameprefix = */ "",
+ /* namesuffix = */ "",
+ /* has_field_name = */ true,
+ /* fieldname_suffix = */ "_", code_ptr);
+ EndBuilderArgs(code_ptr);
+
+ StructBuilderBody(struct_def, "", code_ptr);
+ EndBuilderBody(code_ptr);
+ }
+
+ bool generate() {
+ if (!generateEnums()) return false;
+ if (!generateStructs()) return false;
+ return true;
+ }
+
+ private:
+ bool generateEnums() {
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ auto &enum_def = **it;
+ std::string enumcode;
+ GenEnum(enum_def, &enumcode);
+ if (parser_.opts.generate_object_based_api & enum_def.is_union) {
+ GenUnionCreator(enum_def, &enumcode);
+ }
+ if (!SaveType(enum_def, enumcode, false)) return false;
+ }
+ return true;
+ }
+
+ bool generateStructs() {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ auto &struct_def = **it;
+ std::string declcode;
+ GenStruct(struct_def, &declcode);
+ if (parser_.opts.generate_object_based_api) {
+ GenStructForObjectAPI(struct_def, &declcode);
+ }
+ if (!SaveType(struct_def, declcode, true)) return false;
+ }
+ return true;
+ }
+
+ // Begin by declaring namespace and imports.
+ void BeginFile(const std::string &name_space_name, const bool needs_imports,
+ std::string *code_ptr) {
+ auto &code = *code_ptr;
+ code = code + "# " + FlatBuffersGeneratedWarning() + "\n\n";
+ code += "# namespace: " + name_space_name + "\n\n";
+ if (needs_imports) {
+ code += "import flatbuffers\n";
+ code += "from flatbuffers.compat import import_numpy\n";
+ code += "np = import_numpy()\n\n";
+ }
+ }
+
+ // Save out the generated code for a Python Table type.
+ bool SaveType(const Definition &def, const std::string &classcode,
+ bool needs_imports) {
+ if (!classcode.length()) return true;
+
+ std::string namespace_dir = path_;
+ auto &namespaces = def.defined_namespace->components;
+ for (auto it = namespaces.begin(); it != namespaces.end(); ++it) {
+ if (it != namespaces.begin()) namespace_dir += kPathSeparator;
+ namespace_dir += *it;
+ std::string init_py_filename = namespace_dir + "/__init__.py";
+ SaveFile(init_py_filename.c_str(), "", false);
+ }
+
+ std::string code = "";
+ BeginFile(LastNamespacePart(*def.defined_namespace), needs_imports, &code);
+ code += classcode;
+ std::string filename =
+ NamespaceDir(*def.defined_namespace) + NormalizedName(def) + ".py";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ private:
+ std::unordered_set<std::string> keywords_;
+ const SimpleFloatConstantGenerator float_const_gen_;
+};
+
+} // namespace python
+
+bool GeneratePython(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ python::PythonGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_rust.cpp b/contrib/libs/flatbuffers/src/idl_gen_rust.cpp
new file mode 100644
index 0000000000..455780cd94
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_rust.cpp
@@ -0,0 +1,2817 @@
+/*
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+// Convert a camelCaseIdentifier or CamelCaseIdentifier to a
+// snake_case_identifier.
+std::string MakeSnakeCase(const std::string &in) {
+ std::string s;
+ for (size_t i = 0; i < in.length(); i++) {
+ if (i == 0) {
+ s += CharToLower(in[0]);
+ } else if (in[i] == '_') {
+ s += '_';
+ } else if (!islower(in[i])) {
+ // Prevent duplicate underscores for Upper_Snake_Case strings
+ // and UPPERCASE strings.
+ if (islower(in[i - 1])) { s += '_'; }
+ s += CharToLower(in[i]);
+ } else {
+ s += in[i];
+ }
+ }
+ return s;
+}
+
+// Convert a string to all uppercase.
+std::string MakeUpper(const std::string &in) {
+ std::string s;
+ for (size_t i = 0; i < in.length(); i++) { s += CharToUpper(in[i]); }
+ return s;
+}
+
+// Encapsulate all logical field types in this enum. This allows us to write
+// field logic based on type switches, instead of branches on the properties
+// set on the Type.
+// TODO(rw): for backwards compatibility, we can't use a strict `enum class`
+// declaration here. could we use the `-Wswitch-enum` warning to
+// achieve the same effect?
+enum FullType {
+ ftInteger = 0,
+ ftFloat = 1,
+ ftBool = 2,
+
+ ftStruct = 3,
+ ftTable = 4,
+
+ ftEnumKey = 5,
+ ftUnionKey = 6,
+
+ ftUnionValue = 7,
+
+ // TODO(rw): bytestring?
+ ftString = 8,
+
+ ftVectorOfInteger = 9,
+ ftVectorOfFloat = 10,
+ ftVectorOfBool = 11,
+ ftVectorOfEnumKey = 12,
+ ftVectorOfStruct = 13,
+ ftVectorOfTable = 14,
+ ftVectorOfString = 15,
+ ftVectorOfUnionValue = 16,
+
+ ftArrayOfBuiltin = 17,
+ ftArrayOfEnum = 18,
+ ftArrayOfStruct = 19,
+};
+
+// Convert a Type to a FullType (exhaustive).
+FullType GetFullType(const Type &type) {
+ // N.B. The order of these conditionals matters for some types.
+
+ if (IsString(type)) {
+ return ftString;
+ } else if (type.base_type == BASE_TYPE_STRUCT) {
+ if (type.struct_def->fixed) {
+ return ftStruct;
+ } else {
+ return ftTable;
+ }
+ } else if (IsVector(type)) {
+ switch (GetFullType(type.VectorType())) {
+ case ftInteger: {
+ return ftVectorOfInteger;
+ }
+ case ftFloat: {
+ return ftVectorOfFloat;
+ }
+ case ftBool: {
+ return ftVectorOfBool;
+ }
+ case ftStruct: {
+ return ftVectorOfStruct;
+ }
+ case ftTable: {
+ return ftVectorOfTable;
+ }
+ case ftString: {
+ return ftVectorOfString;
+ }
+ case ftEnumKey: {
+ return ftVectorOfEnumKey;
+ }
+ case ftUnionKey:
+ case ftUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions are unsupported");
+ break;
+ }
+ default: {
+ FLATBUFFERS_ASSERT(false && "vector of vectors are unsupported");
+ }
+ }
+ } else if (IsArray(type)) {
+ switch (GetFullType(type.VectorType())) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool: {
+ return ftArrayOfBuiltin;
+ }
+ case ftStruct: {
+ return ftArrayOfStruct;
+ }
+ case ftEnumKey: {
+ return ftArrayOfEnum;
+ }
+ default: {
+ FLATBUFFERS_ASSERT(false && "Unsupported type for fixed array");
+ }
+ }
+ } else if (type.enum_def != nullptr) {
+ if (type.enum_def->is_union) {
+ if (type.base_type == BASE_TYPE_UNION) {
+ return ftUnionValue;
+ } else if (IsInteger(type.base_type)) {
+ return ftUnionKey;
+ } else {
+ FLATBUFFERS_ASSERT(false && "unknown union field type");
+ }
+ } else {
+ return ftEnumKey;
+ }
+ } else if (IsScalar(type.base_type)) {
+ if (IsBool(type.base_type)) {
+ return ftBool;
+ } else if (IsInteger(type.base_type)) {
+ return ftInteger;
+ } else if (IsFloat(type.base_type)) {
+ return ftFloat;
+ } else {
+ FLATBUFFERS_ASSERT(false && "unknown number type");
+ }
+ }
+
+ FLATBUFFERS_ASSERT(false && "completely unknown type");
+
+ // this is only to satisfy the compiler's return analysis.
+ return ftBool;
+}
+
+// If the second parameter is false then wrap the first with Option<...>
+std::string WrapInOptionIfNotRequired(std::string s, bool required) {
+ if (required) {
+ return s;
+ } else {
+ return "Option<" + s + ">";
+ }
+}
+
+// If the second parameter is false then add .unwrap()
+std::string AddUnwrapIfRequired(std::string s, bool required) {
+ if (required) {
+ return s + ".unwrap()";
+ } else {
+ return s;
+ }
+}
+
+bool IsBitFlagsEnum(const EnumDef &enum_def) {
+ return enum_def.attributes.Lookup("bit_flags") != nullptr;
+}
+bool IsBitFlagsEnum(const FieldDef &field) {
+ EnumDef *ed = field.value.type.enum_def;
+ return ed && IsBitFlagsEnum(*ed);
+}
+
+// TableArgs make required non-scalars "Option<_>".
+// TODO(cneo): Rework how we do defaults and stuff.
+bool IsOptionalToBuilder(const FieldDef &field) {
+ return field.IsOptional() || !IsScalar(field.value.type.base_type);
+}
+
+namespace rust {
+
+class RustGenerator : public BaseGenerator {
+ public:
+ RustGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "::", "rs"),
+ cur_name_space_(nullptr) {
+ const char *keywords[] = {
+ // clang-format off
+ // list taken from:
+ // https://doc.rust-lang.org/book/second-edition/appendix-01-keywords.html
+ //
+ // we write keywords one per line so that we can easily compare them with
+ // changes to that webpage in the future.
+
+ // currently-used keywords
+ "as",
+ "break",
+ "const",
+ "continue",
+ "crate",
+ "else",
+ "enum",
+ "extern",
+ "false",
+ "fn",
+ "for",
+ "if",
+ "impl",
+ "in",
+ "let",
+ "loop",
+ "match",
+ "mod",
+ "move",
+ "mut",
+ "pub",
+ "ref",
+ "return",
+ "Self",
+ "self",
+ "static",
+ "struct",
+ "super",
+ "trait",
+ "true",
+ "type",
+ "unsafe",
+ "use",
+ "where",
+ "while",
+
+ // future possible keywords
+ "abstract",
+ "alignof",
+ "become",
+ "box",
+ "do",
+ "final",
+ "macro",
+ "offsetof",
+ "override",
+ "priv",
+ "proc",
+ "pure",
+ "sizeof",
+ "typeof",
+ "unsized",
+ "virtual",
+ "yield",
+
+ // other rust terms we should not use
+ "std",
+ "usize",
+ "isize",
+ "u8",
+ "i8",
+ "u16",
+ "i16",
+ "u32",
+ "i32",
+ "u64",
+ "i64",
+ "u128",
+ "i128",
+ "f32",
+ "f64",
+
+ // These are terms the code generator can implement on types.
+ //
+ // In Rust, the trait resolution rules (as described at
+ // https://github.com/rust-lang/rust/issues/26007) mean that, as long
+ // as we impl table accessors as inherent methods, we'll never create
+ // conflicts with these keywords. However, that's a fairly nuanced
+ // implementation detail, and how we implement methods could change in
+ // the future. as a result, we proactively block these out as reserved
+ // words.
+ "follow",
+ "push",
+ "size",
+ "alignment",
+ "to_little_endian",
+ "from_little_endian",
+ nullptr,
+
+ // used by Enum constants
+ "ENUM_MAX",
+ "ENUM_MIN",
+ "ENUM_VALUES",
+ };
+ for (auto kw = keywords; *kw; kw++) keywords_.insert(*kw);
+ }
+
+ // Iterate through all definitions we haven't generated code for (enums,
+ // structs, and tables) and output them to a single file.
+ bool generate() {
+ code_.Clear();
+ code_ += "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ assert(!cur_name_space_);
+
+ // Generate imports for the global scope in case no namespace is used
+ // in the schema file.
+ GenNamespaceImports(0);
+ code_ += "";
+
+ // Generate all code in their namespaces, once, because Rust does not
+ // permit re-opening modules.
+ //
+ // TODO(rw): Use a set data structure to reduce namespace evaluations from
+ // O(n**2) to O(n).
+ for (auto ns_it = parser_.namespaces_.begin();
+ ns_it != parser_.namespaces_.end(); ++ns_it) {
+ const auto &ns = *ns_it;
+
+ // Generate code for all the enum declarations.
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ const auto &enum_def = **it;
+ if (enum_def.defined_namespace == ns && !enum_def.generated) {
+ SetNameSpace(enum_def.defined_namespace);
+ GenEnum(enum_def);
+ }
+ }
+
+ // Generate code for all structs.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (struct_def.defined_namespace == ns && struct_def.fixed &&
+ !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenStruct(struct_def);
+ }
+ }
+
+ // Generate code for all tables.
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (struct_def.defined_namespace == ns && !struct_def.fixed &&
+ !struct_def.generated) {
+ SetNameSpace(struct_def.defined_namespace);
+ GenTable(struct_def);
+ if (parser_.opts.generate_object_based_api) {
+ GenTableObject(struct_def);
+ }
+ }
+ }
+
+ // Generate global helper functions.
+ if (parser_.root_struct_def_) {
+ auto &struct_def = *parser_.root_struct_def_;
+ if (struct_def.defined_namespace != ns) { continue; }
+ SetNameSpace(struct_def.defined_namespace);
+ GenRootTableFuncs(struct_def);
+ }
+ }
+ if (cur_name_space_) SetNameSpace(nullptr);
+
+ const auto file_path = GeneratedFileName(path_, file_name_, parser_.opts);
+ const auto final_code = code_.ToString();
+ return SaveFile(file_path.c_str(), final_code, false);
+ }
+
+ private:
+ CodeWriter code_;
+
+ std::set<std::string> keywords_;
+
+ // This tracks the current namespace so we can insert namespace declarations.
+ const Namespace *cur_name_space_;
+
+ const Namespace *CurrentNameSpace() const { return cur_name_space_; }
+
+ // Determine if a Type needs a lifetime template parameter when used in the
+ // Rust builder args.
+ bool TableBuilderTypeNeedsLifetime(const Type &type) const {
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool:
+ case ftEnumKey:
+ case ftUnionKey:
+ case ftUnionValue: {
+ return false;
+ }
+ default: {
+ return true;
+ }
+ }
+ }
+
+ // Determine if a table args rust type needs a lifetime template parameter.
+ bool TableBuilderArgsNeedsLifetime(const StructDef &struct_def) const {
+ FLATBUFFERS_ASSERT(!struct_def.fixed);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) { continue; }
+
+ if (TableBuilderTypeNeedsLifetime(field.value.type)) { return true; }
+ }
+
+ return false;
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+ std::string NamespacedNativeName(const Definition &def) {
+ return WrapInNameSpace(def.defined_namespace, NativeName(def));
+ }
+
+ std::string NativeName(const Definition &def) {
+ return parser_.opts.object_prefix + Name(def) + parser_.opts.object_suffix;
+ }
+
+ std::string Name(const Definition &def) const {
+ return EscapeKeyword(def.name);
+ }
+
+ std::string Name(const EnumVal &ev) const { return EscapeKeyword(ev.name); }
+
+ std::string WrapInNameSpace(const Definition &def) const {
+ return WrapInNameSpace(def.defined_namespace, Name(def));
+ }
+ std::string WrapInNameSpace(const Namespace *ns,
+ const std::string &name) const {
+ if (CurrentNameSpace() == ns) return name;
+ std::string prefix = GetRelativeNamespaceTraversal(CurrentNameSpace(), ns);
+ return prefix + name;
+ }
+
+ // Determine the namespace traversal needed from the Rust crate root.
+ // This may be useful in the future for referring to included files, but is
+ // currently unused.
+ std::string GetAbsoluteNamespaceTraversal(const Namespace *dst) const {
+ std::stringstream stream;
+
+ stream << "::";
+ for (auto d = dst->components.begin(); d != dst->components.end(); ++d) {
+ stream << MakeSnakeCase(*d) + "::";
+ }
+ return stream.str();
+ }
+
+ // Determine the relative namespace traversal needed to reference one
+ // namespace from another namespace. This is useful because it does not force
+ // the user to have a particular file layout. (If we output absolute
+ // namespace paths, that may require users to organize their Rust crates in a
+ // particular way.)
+ std::string GetRelativeNamespaceTraversal(const Namespace *src,
+ const Namespace *dst) const {
+ // calculate the path needed to reference dst from src.
+ // example: f(A::B::C, A::B::C) -> (none)
+ // example: f(A::B::C, A::B) -> super::
+ // example: f(A::B::C, A::B::D) -> super::D
+ // example: f(A::B::C, A) -> super::super::
+ // example: f(A::B::C, D) -> super::super::super::D
+ // example: f(A::B::C, D::E) -> super::super::super::D::E
+ // example: f(A, D::E) -> super::D::E
+ // does not include leaf object (typically a struct type).
+
+ size_t i = 0;
+ std::stringstream stream;
+
+ auto s = src->components.begin();
+ auto d = dst->components.begin();
+ for (;;) {
+ if (s == src->components.end()) { break; }
+ if (d == dst->components.end()) { break; }
+ if (*s != *d) { break; }
+ ++s;
+ ++d;
+ ++i;
+ }
+
+ for (; s != src->components.end(); ++s) { stream << "super::"; }
+ for (; d != dst->components.end(); ++d) {
+ stream << MakeSnakeCase(*d) + "::";
+ }
+ return stream.str();
+ }
+
+ // Generate a comment from the schema.
+ void GenComment(const std::vector<std::string> &dc, const char *prefix = "") {
+ std::string text;
+ ::flatbuffers::GenComment(dc, &text, nullptr, prefix);
+ code_ += text + "\\";
+ }
+
+ // Return a Rust type from the table in idl.h.
+ std::string GetTypeBasic(const Type &type) const {
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool:
+ case ftEnumKey:
+ case ftUnionKey: {
+ break;
+ }
+ default: {
+ FLATBUFFERS_ASSERT(false && "incorrect type given");
+ }
+ }
+
+ // clang-format off
+ static const char * const ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, \
+ RTYPE, ...) \
+ #RTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+
+ if (type.enum_def) { return WrapInNameSpace(*type.enum_def); }
+ return ctypename[type.base_type];
+ }
+
+ // Look up the native type for an enum. This will always be an integer like
+ // u8, i32, etc.
+ std::string GetEnumTypeForDecl(const Type &type) {
+ const auto ft = GetFullType(type);
+ if (!(ft == ftEnumKey || ft == ftUnionKey)) {
+ FLATBUFFERS_ASSERT(false && "precondition failed in GetEnumTypeForDecl");
+ }
+
+ // clang-format off
+ static const char *ctypename[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, \
+ RTYPE, ...) \
+ #RTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+
+ // Enums can be bools, but their Rust representation must be a u8, as used
+ // in the repr attribute (#[repr(bool)] is an invalid attribute).
+ if (type.base_type == BASE_TYPE_BOOL) return "u8";
+ return ctypename[type.base_type];
+ }
+
+ // Return a Rust type for any type (scalar, table, struct) specifically for
+ // using a FlatBuffer.
+ std::string GetTypeGet(const Type &type) const {
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool:
+ case ftEnumKey:
+ case ftUnionKey: {
+ return GetTypeBasic(type);
+ }
+ case ftArrayOfBuiltin:
+ case ftArrayOfEnum:
+ case ftArrayOfStruct: {
+ return "[" + GetTypeGet(type.VectorType()) + "; " +
+ NumToString(type.fixed_length) + "]";
+ }
+ case ftTable: {
+ return WrapInNameSpace(type.struct_def->defined_namespace,
+ type.struct_def->name) +
+ "<'a>";
+ }
+ default: {
+ return WrapInNameSpace(type.struct_def->defined_namespace,
+ type.struct_def->name);
+ }
+ }
+ }
+
+ std::string GetEnumValue(const EnumDef &enum_def,
+ const EnumVal &enum_val) const {
+ return Name(enum_def) + "::" + Name(enum_val);
+ }
+
+ // 1 suffix since old C++ can't figure out the overload.
+ void ForAllEnumValues1(const EnumDef &enum_def,
+ std::function<void(const EnumVal &)> cb) {
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const auto &ev = **it;
+ code_.SetValue("VARIANT", Name(ev));
+ code_.SetValue("VALUE", enum_def.ToString(ev));
+ cb(ev);
+ }
+ }
+ void ForAllEnumValues(const EnumDef &enum_def, std::function<void()> cb) {
+ std::function<void(const EnumVal &)> wrapped = [&](const EnumVal &unused) {
+ (void)unused;
+ cb();
+ };
+ ForAllEnumValues1(enum_def, wrapped);
+ }
+ // Generate an enum declaration,
+ // an enum string lookup table,
+ // an enum match function,
+ // and an enum array of values
+ void GenEnum(const EnumDef &enum_def) {
+ code_.SetValue("ENUM_NAME", Name(enum_def));
+ code_.SetValue("BASE_TYPE", GetEnumTypeForDecl(enum_def.underlying_type));
+ code_.SetValue("ENUM_NAME_SNAKE", MakeSnakeCase(Name(enum_def)));
+ code_.SetValue("ENUM_NAME_CAPS", MakeUpper(MakeSnakeCase(Name(enum_def))));
+ const EnumVal *minv = enum_def.MinValue();
+ const EnumVal *maxv = enum_def.MaxValue();
+ FLATBUFFERS_ASSERT(minv && maxv);
+ code_.SetValue("ENUM_MIN_BASE_VALUE", enum_def.ToString(*minv));
+ code_.SetValue("ENUM_MAX_BASE_VALUE", enum_def.ToString(*maxv));
+
+ if (IsBitFlagsEnum(enum_def)) {
+ // Defer to the convenient and canonical bitflags crate. We declare it in
+ // a module to #allow camel case constants in a smaller scope. This
+ // matches Flatbuffers c-modeled enums where variants are associated
+ // constants but in camel case.
+ code_ += "#[allow(non_upper_case_globals)]";
+ code_ += "mod bitflags_{{ENUM_NAME_SNAKE}} {";
+ code_ += " flatbuffers::bitflags::bitflags! {";
+ GenComment(enum_def.doc_comment, " ");
+ code_ += " #[derive(Default)]";
+ code_ += " pub struct {{ENUM_NAME}}: {{BASE_TYPE}} {";
+ ForAllEnumValues1(enum_def, [&](const EnumVal &ev) {
+ this->GenComment(ev.doc_comment, " ");
+ code_ += " const {{VARIANT}} = {{VALUE}};";
+ });
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+ code_ += "pub use self::bitflags_{{ENUM_NAME_SNAKE}}::{{ENUM_NAME}};";
+ code_ += "";
+
+ code_.SetValue("FROM_BASE", "unsafe { Self::from_bits_unchecked(b) }");
+ code_.SetValue("INTO_BASE", "self.bits()");
+ } else {
+ // Normal, c-modelled enums.
+ // Deprecated associated constants;
+ const std::string deprecation_warning =
+ "#[deprecated(since = \"2.0.0\", note = \"Use associated constants"
+ " instead. This will no longer be generated in 2021.\")]";
+ code_ += deprecation_warning;
+ code_ +=
+ "pub const ENUM_MIN_{{ENUM_NAME_CAPS}}: {{BASE_TYPE}}"
+ " = {{ENUM_MIN_BASE_VALUE}};";
+ code_ += deprecation_warning;
+ code_ +=
+ "pub const ENUM_MAX_{{ENUM_NAME_CAPS}}: {{BASE_TYPE}}"
+ " = {{ENUM_MAX_BASE_VALUE}};";
+ auto num_fields = NumToString(enum_def.size());
+ code_ += deprecation_warning;
+ code_ += "#[allow(non_camel_case_types)]";
+ code_ += "pub const ENUM_VALUES_{{ENUM_NAME_CAPS}}: [{{ENUM_NAME}}; " +
+ num_fields + "] = [";
+ ForAllEnumValues1(enum_def, [&](const EnumVal &ev) {
+ code_ += " " + GetEnumValue(enum_def, ev) + ",";
+ });
+ code_ += "];";
+ code_ += "";
+
+ GenComment(enum_def.doc_comment);
+ // Derive Default to be 0. flatc enforces this when the enum
+ // is put into a struct, though this isn't documented behavior, it is
+ // needed to derive defaults in struct objects.
+ code_ +=
+ "#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, "
+ "Default)]";
+ code_ += "#[repr(transparent)]";
+ code_ += "pub struct {{ENUM_NAME}}(pub {{BASE_TYPE}});";
+ code_ += "#[allow(non_upper_case_globals)]";
+ code_ += "impl {{ENUM_NAME}} {";
+ ForAllEnumValues1(enum_def, [&](const EnumVal &ev) {
+ this->GenComment(ev.doc_comment, " ");
+ code_ += " pub const {{VARIANT}}: Self = Self({{VALUE}});";
+ });
+ code_ += "";
+ // Generate Associated constants
+ code_ += " pub const ENUM_MIN: {{BASE_TYPE}} = {{ENUM_MIN_BASE_VALUE}};";
+ code_ += " pub const ENUM_MAX: {{BASE_TYPE}} = {{ENUM_MAX_BASE_VALUE}};";
+ code_ += " pub const ENUM_VALUES: &'static [Self] = &[";
+ ForAllEnumValues(enum_def, [&]() { code_ += " Self::{{VARIANT}},"; });
+ code_ += " ];";
+ code_ += " /// Returns the variant's name or \"\" if unknown.";
+ code_ += " pub fn variant_name(self) -> Option<&'static str> {";
+ code_ += " match self {";
+ ForAllEnumValues(enum_def, [&]() {
+ code_ += " Self::{{VARIANT}} => Some(\"{{VARIANT}}\"),";
+ });
+ code_ += " _ => None,";
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+
+ // Generate Debug. Unknown variants are printed like "<UNKNOWN 42>".
+ code_ += "impl std::fmt::Debug for {{ENUM_NAME}} {";
+ code_ +=
+ " fn fmt(&self, f: &mut std::fmt::Formatter) ->"
+ " std::fmt::Result {";
+ code_ += " if let Some(name) = self.variant_name() {";
+ code_ += " f.write_str(name)";
+ code_ += " } else {";
+ code_ += " f.write_fmt(format_args!(\"<UNKNOWN {:?}>\", self.0))";
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+
+ code_.SetValue("FROM_BASE", "Self(b)");
+ code_.SetValue("INTO_BASE", "self.0");
+ }
+
+ // Generate Follow and Push so we can serialize and stuff.
+ code_ += "impl<'a> flatbuffers::Follow<'a> for {{ENUM_NAME}} {";
+ code_ += " type Inner = Self;";
+ code_ += " #[inline]";
+ code_ += " fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {";
+ code_ += " let b = unsafe {";
+ code_ += " flatbuffers::read_scalar_at::<{{BASE_TYPE}}>(buf, loc)";
+ code_ += " };";
+ code_ += " {{FROM_BASE}}";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+ code_ += "impl flatbuffers::Push for {{ENUM_NAME}} {";
+ code_ += " type Output = {{ENUM_NAME}};";
+ code_ += " #[inline]";
+ code_ += " fn push(&self, dst: &mut [u8], _rest: &[u8]) {";
+ code_ +=
+ " unsafe { flatbuffers::emplace_scalar::<{{BASE_TYPE}}>"
+ "(dst, {{INTO_BASE}}); }";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+ code_ += "impl flatbuffers::EndianScalar for {{ENUM_NAME}} {";
+ code_ += " #[inline]";
+ code_ += " fn to_little_endian(self) -> Self {";
+ code_ += " let b = {{BASE_TYPE}}::to_le({{INTO_BASE}});";
+ code_ += " {{FROM_BASE}}";
+ code_ += " }";
+ code_ += " #[inline]";
+ code_ += " #[allow(clippy::wrong_self_convention)]";
+ code_ += " fn from_little_endian(self) -> Self {";
+ code_ += " let b = {{BASE_TYPE}}::from_le({{INTO_BASE}});";
+ code_ += " {{FROM_BASE}}";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ // Generate verifier - deferring to the base type.
+ code_ += "impl<'a> flatbuffers::Verifiable for {{ENUM_NAME}} {";
+ code_ += " #[inline]";
+ code_ += " fn run_verifier(";
+ code_ += " v: &mut flatbuffers::Verifier, pos: usize";
+ code_ += " ) -> Result<(), flatbuffers::InvalidFlatbuffer> {";
+ code_ += " use self::flatbuffers::Verifiable;";
+ code_ += " {{BASE_TYPE}}::run_verifier(v, pos)";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+ // Enums are basically integers.
+ code_ += "impl flatbuffers::SimpleToVerifyInSlice for {{ENUM_NAME}} {}";
+
+ if (enum_def.is_union) {
+ // Generate typesafe offset(s) for unions
+ code_.SetValue("NAME", Name(enum_def));
+ code_.SetValue("UNION_OFFSET_NAME", Name(enum_def) + "UnionTableOffset");
+ code_ += "pub struct {{UNION_OFFSET_NAME}} {}";
+ code_ += "";
+ if (parser_.opts.generate_object_based_api) { GenUnionObject(enum_def); }
+ }
+ }
+
+ // CASPER: dedup Object versions from non object versions.
+ void ForAllUnionObjectVariantsBesidesNone(const EnumDef &enum_def,
+ std::function<void()> cb) {
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &enum_val = **it;
+ if (enum_val.union_type.base_type == BASE_TYPE_NONE) continue;
+ code_.SetValue("VARIANT_NAME", Name(enum_val));
+ code_.SetValue("NATIVE_VARIANT", MakeCamel(Name(enum_val)));
+ code_.SetValue("U_ELEMENT_NAME", MakeSnakeCase(Name(enum_val)));
+ code_.SetValue("U_ELEMENT_TABLE_TYPE",
+ NamespacedNativeName(*enum_val.union_type.struct_def));
+ cb();
+ }
+ }
+ void GenUnionObject(const EnumDef &enum_def) {
+ code_.SetValue("ENUM_NAME", Name(enum_def));
+ code_.SetValue("ENUM_NAME_SNAKE", MakeSnakeCase(Name(enum_def)));
+ code_.SetValue("NATIVE_NAME", NativeName(enum_def));
+
+ // Generate native union.
+ code_ += "#[non_exhaustive]";
+ code_ += "#[derive(Debug, Clone, PartialEq)]";
+ code_ += "pub enum {{NATIVE_NAME}} {";
+ code_ += " NONE,";
+ ForAllUnionObjectVariantsBesidesNone(enum_def, [&] {
+ code_ += " {{NATIVE_VARIANT}}(Box<{{U_ELEMENT_TABLE_TYPE}}>),";
+ });
+ code_ += "}";
+ // Generate Default (NONE).
+ code_ += "impl Default for {{NATIVE_NAME}} {";
+ code_ += " fn default() -> Self {";
+ code_ += " Self::NONE";
+ code_ += " }";
+ code_ += "}";
+
+ // Generate native union methods.
+ code_ += "impl {{NATIVE_NAME}} {";
+
+ // Get flatbuffers union key.
+ // CASPER: add docstrings?
+ code_ += " pub fn {{ENUM_NAME_SNAKE}}_type(&self) -> {{ENUM_NAME}} {";
+ code_ += " match self {";
+ code_ += " Self::NONE => {{ENUM_NAME}}::NONE,";
+ ForAllUnionObjectVariantsBesidesNone(enum_def, [&] {
+ code_ +=
+ " Self::{{NATIVE_VARIANT}}(_) => {{ENUM_NAME}}::"
+ "{{VARIANT_NAME}},";
+ });
+ code_ += " }";
+ code_ += " }";
+ // Pack flatbuffers union value
+ code_ +=
+ " pub fn pack(&self, fbb: &mut flatbuffers::FlatBufferBuilder)"
+ " -> Option<flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>>"
+ " {";
+ code_ += " match self {";
+ code_ += " Self::NONE => None,";
+ ForAllUnionObjectVariantsBesidesNone(enum_def, [&] {
+ code_ +=
+ " Self::{{NATIVE_VARIANT}}(v) => "
+ "Some(v.pack(fbb).as_union_value()),";
+ });
+ code_ += " }";
+ code_ += " }";
+
+ // Generate some accessors;
+ ForAllUnionObjectVariantsBesidesNone(enum_def, [&] {
+ // Move accessor.
+ code_ +=
+ " /// If the union variant matches, return the owned "
+ "{{U_ELEMENT_TABLE_TYPE}}, setting the union to NONE.";
+ code_ +=
+ " pub fn take_{{U_ELEMENT_NAME}}(&mut self) -> "
+ "Option<Box<{{U_ELEMENT_TABLE_TYPE}}>> {";
+ code_ += " if let Self::{{NATIVE_VARIANT}}(_) = self {";
+ code_ += " let v = std::mem::replace(self, Self::NONE);";
+ code_ += " if let Self::{{NATIVE_VARIANT}}(w) = v {";
+ code_ += " Some(w)";
+ code_ += " } else {";
+ code_ += " unreachable!()";
+ code_ += " }";
+ code_ += " } else {";
+ code_ += " None";
+ code_ += " }";
+ code_ += " }";
+ // Immutable reference accessor.
+ code_ +=
+ " /// If the union variant matches, return a reference to the "
+ "{{U_ELEMENT_TABLE_TYPE}}.";
+ code_ +=
+ " pub fn as_{{U_ELEMENT_NAME}}(&self) -> "
+ "Option<&{{U_ELEMENT_TABLE_TYPE}}> {";
+ code_ +=
+ " if let Self::{{NATIVE_VARIANT}}(v) = self "
+ "{ Some(v.as_ref()) } else { None }";
+ code_ += " }";
+ // Mutable reference accessor.
+ code_ +=
+ " /// If the union variant matches, return a mutable reference"
+ " to the {{U_ELEMENT_TABLE_TYPE}}.";
+ code_ +=
+ " pub fn as_{{U_ELEMENT_NAME}}_mut(&mut self) -> "
+ "Option<&mut {{U_ELEMENT_TABLE_TYPE}}> {";
+ code_ +=
+ " if let Self::{{NATIVE_VARIANT}}(v) = self "
+ "{ Some(v.as_mut()) } else { None }";
+ code_ += " }";
+ });
+ code_ += "}"; // End union methods impl.
+ }
+
+ std::string GetFieldOffsetName(const FieldDef &field) {
+ return "VT_" + MakeUpper(Name(field));
+ }
+
+ enum DefaultContext { kBuilder, kAccessor, kObject };
+ std::string GetDefaultValue(const FieldDef &field,
+ const DefaultContext context) {
+ if (context == kBuilder) {
+ // Builders and Args structs model nonscalars "optional" even if they're
+ // required or have defaults according to the schema. I guess its because
+ // WIPOffset is not nullable.
+ if (!IsScalar(field.value.type.base_type) || field.IsOptional()) {
+ return "None";
+ }
+ } else {
+ // This for defaults in objects.
+ // Unions have a NONE variant instead of using Rust's None.
+ if (field.IsOptional() && !IsUnion(field.value.type)) { return "None"; }
+ }
+ switch (GetFullType(field.value.type)) {
+ case ftInteger:
+ case ftFloat: {
+ return field.value.constant;
+ }
+ case ftBool: {
+ return field.value.constant == "0" ? "false" : "true";
+ }
+ case ftUnionKey:
+ case ftEnumKey: {
+ auto ev = field.value.type.enum_def->FindByValue(field.value.constant);
+ if (!ev) return "Default::default()"; // Bitflags enum.
+ return WrapInNameSpace(field.value.type.enum_def->defined_namespace,
+ GetEnumValue(*field.value.type.enum_def, *ev));
+ }
+ case ftUnionValue: {
+ return ObjectFieldType(field, true) + "::NONE";
+ }
+ case ftString: {
+ // Required fields do not have defaults defined by the schema, but we
+ // need one for Rust's Default trait so we use empty string. The usual
+ // value of field.value.constant is `0`, which is non-sensical except
+ // maybe to c++ (nullptr == 0).
+ // TODO: Escape strings?
+ const std::string defval =
+ field.IsRequired() ? "\"\"" : "\"" + field.value.constant + "\"";
+ if (context == kObject) return defval + ".to_string()";
+ if (context == kAccessor) return "&" + defval;
+ FLATBUFFERS_ASSERT("Unreachable.");
+ return "INVALID_CODE_GENERATION";
+ }
+
+ case ftArrayOfStruct:
+ case ftArrayOfEnum:
+ case ftArrayOfBuiltin:
+ case ftVectorOfBool:
+ case ftVectorOfFloat:
+ case ftVectorOfInteger:
+ case ftVectorOfString:
+ case ftVectorOfStruct:
+ case ftVectorOfTable:
+ case ftVectorOfEnumKey:
+ case ftVectorOfUnionValue:
+ case ftStruct:
+ case ftTable: {
+ // We only support empty vectors which matches the defaults for
+ // &[T] and Vec<T> anyway.
+ //
+ // For required structs and tables fields, we defer to their object API
+ // defaults. This works so long as there's nothing recursive happening,
+ // but `table Infinity { i: Infinity (required); }` does compile.
+ return "Default::default()";
+ }
+ }
+ FLATBUFFERS_ASSERT("Unreachable.");
+ return "INVALID_CODE_GENERATION";
+ }
+
+ // Create the return type for fields in the *BuilderArgs structs that are
+ // used to create Tables.
+ //
+ // Note: we could make all inputs to the BuilderArgs be an Option, as well
+ // as all outputs. But, the UX of Flatbuffers is that the user doesn't get to
+ // know if the value is default or not, because there are three ways to
+ // return a default value:
+ // 1) return a stored value that happens to be the default,
+ // 2) return a hardcoded value because the relevant vtable field is not in
+ // the vtable, or
+ // 3) return a hardcoded value because the vtable field value is set to zero.
+ std::string TableBuilderArgsDefnType(const FieldDef &field,
+ const std::string &lifetime) {
+ const Type &type = field.value.type;
+ auto WrapOption = [&](std::string s) {
+ return IsOptionalToBuilder(field) ? "Option<" + s + ">" : s;
+ };
+ auto WrapVector = [&](std::string ty) {
+ return WrapOption("flatbuffers::WIPOffset<flatbuffers::Vector<" +
+ lifetime + ", " + ty + ">>");
+ };
+ auto WrapUOffsetsVector = [&](std::string ty) {
+ return WrapVector("flatbuffers::ForwardsUOffset<" + ty + ">");
+ };
+
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool: {
+ return WrapOption(GetTypeBasic(type));
+ }
+ case ftStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption("&" + lifetime + " " + typname);
+ }
+ case ftTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption("flatbuffers::WIPOffset<" + typname + "<" + lifetime +
+ ">>");
+ }
+ case ftString: {
+ return WrapOption("flatbuffers::WIPOffset<&" + lifetime + " str>");
+ }
+ case ftEnumKey:
+ case ftUnionKey: {
+ return WrapOption(WrapInNameSpace(*type.enum_def));
+ }
+ case ftUnionValue: {
+ return "Option<flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>>";
+ }
+
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ return WrapVector(typname);
+ }
+ case ftVectorOfEnumKey: {
+ const auto typname = WrapInNameSpace(*type.enum_def);
+ return WrapVector(typname);
+ }
+ case ftVectorOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapVector(typname);
+ }
+ case ftVectorOfTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapUOffsetsVector(typname + "<" + lifetime + ">");
+ }
+ case ftVectorOfString: {
+ return WrapUOffsetsVector("&" + lifetime + " str");
+ }
+ case ftVectorOfUnionValue: {
+ return WrapUOffsetsVector("flatbuffers::Table<" + lifetime + ">");
+ }
+ case ftArrayOfEnum:
+ case ftArrayOfStruct:
+ case ftArrayOfBuiltin: {
+ FLATBUFFERS_ASSERT(false && "arrays are not supported within tables");
+ return "ARRAYS_NOT_SUPPORTED_IN_TABLES";
+ }
+ }
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+
+ std::string ObjectFieldType(const FieldDef &field, bool in_a_table) {
+ const Type &type = field.value.type;
+ std::string ty;
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftBool:
+ case ftFloat: {
+ ty = GetTypeBasic(type);
+ break;
+ }
+ case ftString: {
+ ty = "String";
+ break;
+ }
+ case ftStruct: {
+ ty = NamespacedNativeName(*type.struct_def);
+ break;
+ }
+ case ftTable: {
+ // Since Tables can contain themselves, Box is required to avoid
+ // infinite types.
+ ty = "Box<" + NamespacedNativeName(*type.struct_def) + ">";
+ break;
+ }
+ case ftUnionKey: {
+ // There is no native "UnionKey", natively, unions are rust enums with
+ // newtype-struct-variants.
+ return "INVALID_CODE_GENERATION";
+ }
+ case ftUnionValue: {
+ ty = NamespacedNativeName(*type.enum_def);
+ break;
+ }
+ case ftEnumKey: {
+ ty = WrapInNameSpace(*type.enum_def);
+ break;
+ }
+ // Vectors are in tables and are optional
+ case ftVectorOfEnumKey: {
+ ty = "Vec<" + WrapInNameSpace(*type.VectorType().enum_def) + ">";
+ break;
+ }
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ ty = "Vec<" + GetTypeBasic(type.VectorType()) + ">";
+ break;
+ }
+ case ftVectorOfString: {
+ ty = "Vec<String>";
+ break;
+ }
+ case ftVectorOfTable:
+ case ftVectorOfStruct: {
+ ty = NamespacedNativeName(*type.VectorType().struct_def);
+ ty = "Vec<" + ty + ">";
+ break;
+ }
+ case ftVectorOfUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions are not yet supported");
+ return "INVALID_CODE_GENERATION"; // OH NO!
+ }
+ case ftArrayOfEnum: {
+ ty = "[" + WrapInNameSpace(*type.VectorType().enum_def) + "; " +
+ NumToString(type.fixed_length) + "]";
+ break;
+ }
+ case ftArrayOfStruct: {
+ ty = "[" + NamespacedNativeName(*type.VectorType().struct_def) + "; " +
+ NumToString(type.fixed_length) + "]";
+ break;
+ }
+ case ftArrayOfBuiltin: {
+ ty = "[" + GetTypeBasic(type.VectorType()) + "; " +
+ NumToString(type.fixed_length) + "]";
+ break;
+ }
+ }
+ if (in_a_table && !IsUnion(type) && field.IsOptional()) {
+ return "Option<" + ty + ">";
+ } else {
+ return ty;
+ }
+ }
+
+ std::string TableBuilderArgsAddFuncType(const FieldDef &field,
+ const std::string &lifetime) {
+ const Type &type = field.value.type;
+
+ switch (GetFullType(field.value.type)) {
+ case ftVectorOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime + ", " +
+ typname + ">>";
+ }
+ case ftVectorOfTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime +
+ ", flatbuffers::ForwardsUOffset<" + typname + "<" + lifetime +
+ ">>>>";
+ }
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime + ", " +
+ typname + ">>";
+ }
+ case ftVectorOfString: {
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime +
+ ", flatbuffers::ForwardsUOffset<&" + lifetime + " str>>>";
+ }
+ case ftVectorOfEnumKey: {
+ const auto typname = WrapInNameSpace(*type.enum_def);
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime + ", " +
+ typname + ">>";
+ }
+ case ftVectorOfUnionValue: {
+ return "flatbuffers::WIPOffset<flatbuffers::Vector<" + lifetime +
+ ", flatbuffers::ForwardsUOffset<flatbuffers::Table<" + lifetime +
+ ">>>";
+ }
+ case ftEnumKey:
+ case ftUnionKey: {
+ const auto typname = WrapInNameSpace(*type.enum_def);
+ return typname;
+ }
+ case ftStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "&" + typname + "";
+ }
+ case ftTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "flatbuffers::WIPOffset<" + typname + "<" + lifetime + ">>";
+ }
+ case ftInteger:
+ case ftBool:
+ case ftFloat: {
+ return GetTypeBasic(type);
+ }
+ case ftString: {
+ return "flatbuffers::WIPOffset<&" + lifetime + " str>";
+ }
+ case ftUnionValue: {
+ return "flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>";
+ }
+ case ftArrayOfBuiltin: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ return "flatbuffers::Array<" + lifetime + ", " + typname + ", " +
+ NumToString(type.fixed_length) + ">";
+ }
+ case ftArrayOfEnum: {
+ const auto typname = WrapInNameSpace(*type.enum_def);
+ return "flatbuffers::Array<" + lifetime + ", " + typname + ", " +
+ NumToString(type.fixed_length) + ">";
+ }
+ case ftArrayOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "flatbuffers::Array<" + lifetime + ", " + typname + ", " +
+ NumToString(type.fixed_length) + ">";
+ }
+ }
+
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+
+ std::string TableBuilderArgsAddFuncBody(const FieldDef &field) {
+ const Type &type = field.value.type;
+
+ switch (GetFullType(field.value.type)) {
+ case ftInteger:
+ case ftBool:
+ case ftFloat: {
+ const auto typname = GetTypeBasic(field.value.type);
+ return (field.IsOptional() ? "self.fbb_.push_slot_always::<"
+ : "self.fbb_.push_slot::<") +
+ typname + ">";
+ }
+ case ftEnumKey:
+ case ftUnionKey: {
+ const auto underlying_typname = GetTypeBasic(type);
+ return (field.IsOptional() ? "self.fbb_.push_slot_always::<"
+ : "self.fbb_.push_slot::<") +
+ underlying_typname + ">";
+ }
+
+ case ftStruct: {
+ const std::string typname = WrapInNameSpace(*type.struct_def);
+ return "self.fbb_.push_slot_always::<&" + typname + ">";
+ }
+ case ftTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return "self.fbb_.push_slot_always::<flatbuffers::WIPOffset<" +
+ typname + ">>";
+ }
+
+ case ftUnionValue:
+ case ftString:
+ case ftVectorOfInteger:
+ case ftVectorOfFloat:
+ case ftVectorOfBool:
+ case ftVectorOfEnumKey:
+ case ftVectorOfStruct:
+ case ftVectorOfTable:
+ case ftVectorOfString:
+ case ftVectorOfUnionValue: {
+ return "self.fbb_.push_slot_always::<flatbuffers::WIPOffset<_>>";
+ }
+ case ftArrayOfEnum:
+ case ftArrayOfStruct:
+ case ftArrayOfBuiltin: {
+ FLATBUFFERS_ASSERT(false && "arrays are not supported within tables");
+ return "ARRAYS_NOT_SUPPORTED_IN_TABLES";
+ }
+ }
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+
+ std::string GenTableAccessorFuncReturnType(const FieldDef &field,
+ const std::string &lifetime) {
+ const Type &type = field.value.type;
+ const auto WrapOption = [&](std::string s) {
+ return field.IsOptional() ? "Option<" + s + ">" : s;
+ };
+
+ switch (GetFullType(field.value.type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool: {
+ return WrapOption(GetTypeBasic(type));
+ }
+ case ftStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption("&" + lifetime + " " + typname);
+ }
+ case ftTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption(typname + "<" + lifetime + ">");
+ }
+ case ftEnumKey:
+ case ftUnionKey: {
+ return WrapOption(WrapInNameSpace(*type.enum_def));
+ }
+
+ case ftUnionValue: {
+ return WrapOption("flatbuffers::Table<" + lifetime + ">");
+ }
+ case ftString: {
+ return WrapOption("&" + lifetime + " str");
+ }
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ const auto vector_type =
+ IsOneByte(type.VectorType().base_type)
+ ? "&" + lifetime + " [" + typname + "]"
+ : "flatbuffers::Vector<" + lifetime + ", " + typname + ">";
+ return WrapOption(vector_type);
+ }
+ case ftVectorOfEnumKey: {
+ const auto typname = WrapInNameSpace(*type.enum_def);
+ return WrapOption("flatbuffers::Vector<" + lifetime + ", " + typname +
+ ">");
+ }
+ case ftVectorOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption("&" + lifetime + " [" + typname + "]");
+ }
+ case ftVectorOfTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapOption("flatbuffers::Vector<" + lifetime +
+ ", flatbuffers::ForwardsUOffset<" + typname + "<" +
+ lifetime + ">>>");
+ }
+ case ftVectorOfString: {
+ return WrapOption("flatbuffers::Vector<" + lifetime +
+ ", flatbuffers::ForwardsUOffset<&" + lifetime +
+ " str>>");
+ }
+ case ftVectorOfUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions are not yet supported");
+ // TODO(rw): when we do support these, we should consider using the
+ // Into trait to convert tables to typesafe union values.
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+ case ftArrayOfEnum:
+ case ftArrayOfStruct:
+ case ftArrayOfBuiltin: {
+ FLATBUFFERS_ASSERT(false && "arrays are not supported within tables");
+ return "ARRAYS_NOT_SUPPORTED_IN_TABLES";
+ }
+ }
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+
+ std::string FollowType(const Type &type, const std::string &lifetime) {
+ // IsVector... This can be made iterative?
+
+ const auto WrapForwardsUOffset = [](std::string ty) -> std::string {
+ return "flatbuffers::ForwardsUOffset<" + ty + ">";
+ };
+ const auto WrapVector = [&](std::string ty) -> std::string {
+ return "flatbuffers::Vector<" + lifetime + ", " + ty + ">";
+ };
+ const auto WrapArray = [&](std::string ty, uint16_t length) -> std::string {
+ return "flatbuffers::Array<" + lifetime + ", " + ty + ", " +
+ NumToString(length) + ">";
+ };
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftFloat:
+ case ftBool: {
+ return GetTypeBasic(type);
+ }
+ case ftStruct: {
+ return WrapInNameSpace(*type.struct_def);
+ }
+ case ftUnionKey:
+ case ftEnumKey: {
+ return WrapInNameSpace(*type.enum_def);
+ }
+ case ftTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapForwardsUOffset(typname);
+ }
+ case ftUnionValue: {
+ return WrapForwardsUOffset("flatbuffers::Table<" + lifetime + ">");
+ }
+ case ftString: {
+ return WrapForwardsUOffset("&str");
+ }
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ return WrapForwardsUOffset(WrapVector(typname));
+ }
+ case ftVectorOfEnumKey: {
+ const auto typname = WrapInNameSpace(*type.VectorType().enum_def);
+ return WrapForwardsUOffset(WrapVector(typname));
+ }
+ case ftVectorOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapForwardsUOffset(WrapVector(typname));
+ }
+ case ftVectorOfTable: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapForwardsUOffset(WrapVector(WrapForwardsUOffset(typname)));
+ }
+ case ftVectorOfString: {
+ return WrapForwardsUOffset(
+ WrapVector(WrapForwardsUOffset("&" + lifetime + " str")));
+ }
+ case ftVectorOfUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions are not yet supported");
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+ case ftArrayOfEnum: {
+ const auto typname = WrapInNameSpace(*type.VectorType().enum_def);
+ return WrapArray(typname, type.fixed_length);
+ }
+ case ftArrayOfStruct: {
+ const auto typname = WrapInNameSpace(*type.struct_def);
+ return WrapArray(typname, type.fixed_length);
+ }
+ case ftArrayOfBuiltin: {
+ const auto typname = GetTypeBasic(type.VectorType());
+ return WrapArray(typname, type.fixed_length);
+ }
+ }
+ return "INVALID_CODE_GENERATION"; // for return analysis
+ }
+
+ std::string GenTableAccessorFuncBody(const FieldDef &field,
+ const std::string &lifetime) {
+ const std::string vt_offset = GetFieldOffsetName(field);
+ const std::string typname = FollowType(field.value.type, lifetime);
+ // Default-y fields (scalars so far) are neither optional nor required.
+ const std::string default_value =
+ !(field.IsOptional() || field.IsRequired())
+ ? "Some(" + GetDefaultValue(field, kAccessor) + ")"
+ : "None";
+ const std::string unwrap = field.IsOptional() ? "" : ".unwrap()";
+
+ const auto t = GetFullType(field.value.type);
+
+ // TODO(caspern): Shouldn't 1byte VectorOfEnumKey be slice too?
+ const std::string safe_slice =
+ (t == ftVectorOfStruct ||
+ ((t == ftVectorOfBool || t == ftVectorOfFloat ||
+ t == ftVectorOfInteger) &&
+ IsOneByte(field.value.type.VectorType().base_type)))
+ ? ".map(|v| v.safe_slice())"
+ : "";
+
+ return "self._tab.get::<" + typname + ">({{STRUCT_NAME}}::" + vt_offset +
+ ", " + default_value + ")" + safe_slice + unwrap;
+ }
+
+ // Generates a fully-qualified name getter for use with --gen-name-strings
+ void GenFullyQualifiedNameGetter(const StructDef &struct_def,
+ const std::string &name) {
+ code_ += " pub const fn get_fully_qualified_name() -> &'static str {";
+ code_ += " \"" +
+ struct_def.defined_namespace->GetFullyQualifiedName(name) + "\"";
+ code_ += " }";
+ code_ += "";
+ }
+
+ void ForAllUnionVariantsBesidesNone(
+ const EnumDef &def, std::function<void(const EnumVal &ev)> cb) {
+ FLATBUFFERS_ASSERT(def.is_union);
+
+ for (auto it = def.Vals().begin(); it != def.Vals().end(); ++it) {
+ const EnumVal &ev = **it;
+ // TODO(cneo): Can variants be deprecated, should we skip them?
+ if (ev.union_type.base_type == BASE_TYPE_NONE) { continue; }
+ code_.SetValue(
+ "U_ELEMENT_ENUM_TYPE",
+ WrapInNameSpace(def.defined_namespace, GetEnumValue(def, ev)));
+ code_.SetValue(
+ "U_ELEMENT_TABLE_TYPE",
+ WrapInNameSpace(ev.union_type.struct_def->defined_namespace,
+ ev.union_type.struct_def->name));
+ code_.SetValue("U_ELEMENT_NAME", MakeSnakeCase(Name(ev)));
+ cb(ev);
+ }
+ }
+
+ void ForAllTableFields(const StructDef &struct_def,
+ std::function<void(const FieldDef &)> cb,
+ bool reversed = false) {
+ // TODO(cneo): Remove `reversed` overload. It's only here to minimize the
+ // diff when refactoring to the `ForAllX` helper functions.
+ auto go = [&](const FieldDef &field) {
+ if (field.deprecated) return;
+ code_.SetValue("OFFSET_NAME", GetFieldOffsetName(field));
+ code_.SetValue("OFFSET_VALUE", NumToString(field.value.offset));
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("BLDR_DEF_VAL", GetDefaultValue(field, kBuilder));
+ cb(field);
+ };
+ const auto &fields = struct_def.fields.vec;
+ if (reversed) {
+ for (auto it = fields.rbegin(); it != fields.rend(); ++it) go(**it);
+ } else {
+ for (auto it = fields.begin(); it != fields.end(); ++it) go(**it);
+ }
+ }
+ // Generate an accessor struct, builder struct, and create function for a
+ // table.
+ void GenTable(const StructDef &struct_def) {
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_.SetValue("OFFSET_TYPELABEL", Name(struct_def) + "Offset");
+ code_.SetValue("STRUCT_NAME_SNAKECASE", MakeSnakeCase(Name(struct_def)));
+
+ // Generate an offset type, the base type, the Follow impl, and the
+ // init_from_table impl.
+ code_ += "pub enum {{OFFSET_TYPELABEL}} {}";
+ code_ += "#[derive(Copy, Clone, PartialEq)]";
+ code_ += "";
+
+ GenComment(struct_def.doc_comment);
+
+ code_ += "pub struct {{STRUCT_NAME}}<'a> {";
+ code_ += " pub _tab: flatbuffers::Table<'a>,";
+ code_ += "}";
+ code_ += "";
+ code_ += "impl<'a> flatbuffers::Follow<'a> for {{STRUCT_NAME}}<'a> {";
+ code_ += " type Inner = {{STRUCT_NAME}}<'a>;";
+ code_ += " #[inline]";
+ code_ += " fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {";
+ code_ += " Self { _tab: flatbuffers::Table { buf, loc } }";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+ code_ += "impl<'a> {{STRUCT_NAME}}<'a> {";
+
+ if (parser_.opts.generate_name_strings) {
+ GenFullyQualifiedNameGetter(struct_def, struct_def.name);
+ }
+
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn init_from_table(table: flatbuffers::Table<'a>) -> "
+ "Self {";
+ code_ += " {{STRUCT_NAME}} { _tab: table }";
+ code_ += " }";
+
+ // Generate a convenient create* function that uses the above builder
+ // to create a table in one function call.
+ code_.SetValue("MAYBE_US", struct_def.fields.vec.size() == 0 ? "_" : "");
+ code_.SetValue("MAYBE_LT",
+ TableBuilderArgsNeedsLifetime(struct_def) ? "<'args>" : "");
+ code_ += " #[allow(unused_mut)]";
+ code_ += " pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>(";
+ code_ +=
+ " _fbb: "
+ "&'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>,";
+ code_ +=
+ " {{MAYBE_US}}args: &'args {{STRUCT_NAME}}Args{{MAYBE_LT}})"
+ " -> flatbuffers::WIPOffset<{{STRUCT_NAME}}<'bldr>> {";
+
+ code_ += " let mut builder = {{STRUCT_NAME}}Builder::new(_fbb);";
+ for (size_t size = struct_def.sortbysize ? sizeof(largest_scalar_t) : 1;
+ size; size /= 2) {
+ ForAllTableFields(
+ struct_def,
+ [&](const FieldDef &field) {
+ if (struct_def.sortbysize &&
+ size != SizeOf(field.value.type.base_type))
+ return;
+ if (IsOptionalToBuilder(field)) {
+ code_ +=
+ " if let Some(x) = args.{{FIELD_NAME}} "
+ "{ builder.add_{{FIELD_NAME}}(x); }";
+ } else {
+ code_ += " builder.add_{{FIELD_NAME}}(args.{{FIELD_NAME}});";
+ }
+ },
+ /*reverse=*/true);
+ }
+ code_ += " builder.finish()";
+ code_ += " }";
+ code_ += "";
+ // Generate Object API Packer function.
+ if (parser_.opts.generate_object_based_api) {
+ // TODO(cneo): Replace more for loops with ForAllX stuff.
+ // TODO(cneo): Manage indentation with IncrementIdentLevel?
+ code_.SetValue("OBJECT_NAME", NativeName(struct_def));
+ code_ += " pub fn unpack(&self) -> {{OBJECT_NAME}} {";
+ ForAllObjectTableFields(struct_def, [&](const FieldDef &field) {
+ const Type &type = field.value.type;
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftBool:
+ case ftFloat:
+ case ftEnumKey: {
+ code_ += " let {{FIELD_NAME}} = self.{{FIELD_NAME}}();";
+ return;
+ }
+ case ftUnionKey: return;
+ case ftUnionValue: {
+ const auto &enum_def = *type.enum_def;
+ code_.SetValue("ENUM_NAME", WrapInNameSpace(enum_def));
+ code_.SetValue("NATIVE_ENUM_NAME", NamespacedNativeName(enum_def));
+ code_ +=
+ " let {{FIELD_NAME}} = match "
+ "self.{{FIELD_NAME}}_type() {";
+ code_ +=
+ " {{ENUM_NAME}}::NONE =>"
+ " {{NATIVE_ENUM_NAME}}::NONE,";
+ ForAllUnionObjectVariantsBesidesNone(enum_def, [&] {
+ code_ +=
+ " {{ENUM_NAME}}::{{VARIANT_NAME}} => "
+ "{{NATIVE_ENUM_NAME}}::{{NATIVE_VARIANT}}(Box::new(";
+ code_ +=
+ " self.{{FIELD_NAME}}_as_"
+ "{{U_ELEMENT_NAME}}()";
+ code_ +=
+ " .expect(\"Invalid union table, "
+ "expected `{{ENUM_NAME}}::{{VARIANT_NAME}}`.\")";
+ code_ += " .unpack()";
+ code_ += " )),";
+ });
+ // Maybe we shouldn't throw away unknown discriminants?
+ code_ += " _ => {{NATIVE_ENUM_NAME}}::NONE,";
+ code_ += " };";
+ return;
+ }
+ // The rest of the types need special handling based on if the field
+ // is optional or not.
+ case ftString: {
+ code_.SetValue("EXPR", "x.to_string()");
+ break;
+ }
+ case ftStruct: {
+ code_.SetValue("EXPR", "x.unpack()");
+ break;
+ }
+ case ftTable: {
+ code_.SetValue("EXPR", "Box::new(x.unpack())");
+ break;
+ }
+ case ftVectorOfInteger:
+ case ftVectorOfBool: {
+ if (IsOneByte(type.VectorType().base_type)) {
+ // 1 byte stuff is viewed w/ slice instead of flatbuffer::Vector
+ // and thus needs to be cloned out of the slice.
+ code_.SetValue("EXPR", "x.to_vec()");
+ break;
+ }
+ code_.SetValue("EXPR", "x.into_iter().collect()");
+ break;
+ }
+ case ftVectorOfFloat:
+ case ftVectorOfEnumKey: {
+ code_.SetValue("EXPR", "x.into_iter().collect()");
+ break;
+ }
+ case ftVectorOfString: {
+ code_.SetValue("EXPR", "x.iter().map(|s| s.to_string()).collect()");
+ break;
+ }
+ case ftVectorOfStruct:
+ case ftVectorOfTable: {
+ code_.SetValue("EXPR", "x.iter().map(|t| t.unpack()).collect()");
+ break;
+ }
+ case ftVectorOfUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions not yet supported");
+ return;
+ }
+ case ftArrayOfEnum:
+ case ftArrayOfStruct:
+ case ftArrayOfBuiltin: {
+ FLATBUFFERS_ASSERT(false &&
+ "arrays are not supported within tables");
+ return;
+ }
+ }
+ if (field.IsOptional()) {
+ code_ += " let {{FIELD_NAME}} = self.{{FIELD_NAME}}().map(|x| {";
+ code_ += " {{EXPR}}";
+ code_ += " });";
+ } else {
+ code_ += " let {{FIELD_NAME}} = {";
+ code_ += " let x = self.{{FIELD_NAME}}();";
+ code_ += " {{EXPR}}";
+ code_ += " };";
+ }
+ });
+ code_ += " {{OBJECT_NAME}} {";
+ ForAllObjectTableFields(struct_def, [&](const FieldDef &field) {
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) return;
+ code_ += " {{FIELD_NAME}},";
+ });
+ code_ += " }";
+ code_ += " }";
+ }
+
+ // Generate field id constants.
+ ForAllTableFields(struct_def, [&](const FieldDef &unused) {
+ (void)unused;
+ code_ +=
+ " pub const {{OFFSET_NAME}}: flatbuffers::VOffsetT = "
+ "{{OFFSET_VALUE}};";
+ });
+ if (struct_def.fields.vec.size() > 0) code_ += "";
+
+ // Generate the accessors. Each has one of two forms:
+ //
+ // If a value can be None:
+ // pub fn name(&'a self) -> Option<user_facing_type> {
+ // self._tab.get::<internal_type>(offset, defaultval)
+ // }
+ //
+ // If a value is always Some:
+ // pub fn name(&'a self) -> user_facing_type {
+ // self._tab.get::<internal_type>(offset, defaultval).unwrap()
+ // }
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ code_.SetValue("RETURN_TYPE",
+ GenTableAccessorFuncReturnType(field, "'a"));
+
+ this->GenComment(field.doc_comment, " ");
+ code_ += " #[inline]";
+ code_ += " pub fn {{FIELD_NAME}}(&self) -> {{RETURN_TYPE}} {";
+ code_ += " " + GenTableAccessorFuncBody(field, "'a");
+ code_ += " }";
+
+ // Generate a comparison function for this field if it is a key.
+ if (field.key) { GenKeyFieldMethods(field); }
+
+ // Generate a nested flatbuffer field, if applicable.
+ auto nested = field.attributes.Lookup("nested_flatbuffer");
+ if (nested) {
+ std::string qualified_name = nested->constant;
+ auto nested_root = parser_.LookupStruct(nested->constant);
+ if (nested_root == nullptr) {
+ qualified_name = parser_.current_namespace_->GetFullyQualifiedName(
+ nested->constant);
+ nested_root = parser_.LookupStruct(qualified_name);
+ }
+ FLATBUFFERS_ASSERT(nested_root); // Guaranteed to exist by parser.
+
+ code_.SetValue("NESTED", WrapInNameSpace(*nested_root));
+ code_ += " pub fn {{FIELD_NAME}}_nested_flatbuffer(&'a self) -> \\";
+ if (field.IsRequired()) {
+ code_ += "{{NESTED}}<'a> {";
+ code_ += " let data = self.{{FIELD_NAME}}();";
+ code_ += " use flatbuffers::Follow;";
+ code_ +=
+ " <flatbuffers::ForwardsUOffset<{{NESTED}}<'a>>>"
+ "::follow(data, 0)";
+ } else {
+ code_ += "Option<{{NESTED}}<'a>> {";
+ code_ += " self.{{FIELD_NAME}}().map(|data| {";
+ code_ += " use flatbuffers::Follow;";
+ code_ +=
+ " <flatbuffers::ForwardsUOffset<{{NESTED}}<'a>>>"
+ "::follow(data, 0)";
+ code_ += " })";
+ }
+ code_ += " }";
+ }
+ });
+
+ // Explicit specializations for union accessors
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ if (field.value.type.base_type != BASE_TYPE_UNION) return;
+ code_.SetValue("FIELD_TYPE_FIELD_NAME", field.name);
+ ForAllUnionVariantsBesidesNone(
+ *field.value.type.enum_def, [&](const EnumVal &unused) {
+ (void)unused;
+ code_ += " #[inline]";
+ code_ += " #[allow(non_snake_case)]";
+ code_ +=
+ " pub fn {{FIELD_NAME}}_as_{{U_ELEMENT_NAME}}(&self) -> "
+ "Option<{{U_ELEMENT_TABLE_TYPE}}<'a>> {";
+ // If the user defined schemas name a field that clashes with a
+ // language reserved word, flatc will try to escape the field name
+ // by appending an underscore. This works well for most cases,
+ // except one. When generating union accessors (and referring to
+ // them internally within the code generated here), an extra
+ // underscore will be appended to the name, causing build failures.
+ //
+ // This only happens when unions have members that overlap with
+ // language reserved words.
+ //
+ // To avoid this problem the type field name is used unescaped here:
+ code_ +=
+ " if self.{{FIELD_TYPE_FIELD_NAME}}_type() == "
+ "{{U_ELEMENT_ENUM_TYPE}} {";
+
+ // The following logic is not tested in the integration test,
+ // as of April 10, 2020
+ if (field.IsRequired()) {
+ code_ += " let u = self.{{FIELD_NAME}}();";
+ code_ +=
+ " Some({{U_ELEMENT_TABLE_TYPE}}::init_from_table(u))";
+ } else {
+ code_ +=
+ " self.{{FIELD_NAME}}().map("
+ "{{U_ELEMENT_TABLE_TYPE}}::init_from_table)";
+ }
+ code_ += " } else {";
+ code_ += " None";
+ code_ += " }";
+ code_ += " }";
+ code_ += "";
+ });
+ });
+ code_ += "}"; // End of table impl.
+ code_ += "";
+
+ // Generate Verifier;
+ code_ += "impl flatbuffers::Verifiable for {{STRUCT_NAME}}<'_> {";
+ code_ += " #[inline]";
+ code_ += " fn run_verifier(";
+ code_ += " v: &mut flatbuffers::Verifier, pos: usize";
+ code_ += " ) -> Result<(), flatbuffers::InvalidFlatbuffer> {";
+ code_ += " use self::flatbuffers::Verifiable;";
+ code_ += " v.visit_table(pos)?\\";
+ // Escape newline and insert it onthe next line so we can end the builder
+ // with a nice semicolon.
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ if (GetFullType(field.value.type) == ftUnionKey) return;
+
+ code_.SetValue("IS_REQ", field.IsRequired() ? "true" : "false");
+ if (GetFullType(field.value.type) != ftUnionValue) {
+ // All types besides unions.
+ code_.SetValue("TY", FollowType(field.value.type, "'_"));
+ code_ +=
+ "\n .visit_field::<{{TY}}>(&\"{{FIELD_NAME}}\", "
+ "Self::{{OFFSET_NAME}}, {{IS_REQ}})?\\";
+ return;
+ }
+ // Unions.
+ EnumDef &union_def = *field.value.type.enum_def;
+ code_.SetValue("UNION_TYPE", WrapInNameSpace(union_def));
+ code_ +=
+ "\n .visit_union::<{{UNION_TYPE}}, _>("
+ "&\"{{FIELD_NAME}}_type\", Self::{{OFFSET_NAME}}_TYPE, "
+ "&\"{{FIELD_NAME}}\", Self::{{OFFSET_NAME}}, {{IS_REQ}}, "
+ "|key, v, pos| {";
+ code_ += " match key {";
+ ForAllUnionVariantsBesidesNone(union_def, [&](const EnumVal &unused) {
+ (void)unused;
+ code_ +=
+ " {{U_ELEMENT_ENUM_TYPE}} => v.verify_union_variant::"
+ "<flatbuffers::ForwardsUOffset<{{U_ELEMENT_TABLE_TYPE}}>>("
+ "\"{{U_ELEMENT_ENUM_TYPE}}\", pos),";
+ });
+ code_ += " _ => Ok(()),";
+ code_ += " }";
+ code_ += " })?\\";
+ });
+ code_ += "\n .finish();";
+ code_ += " Ok(())";
+ code_ += " }";
+ code_ += "}";
+
+ // Generate an args struct:
+ code_.SetValue("MAYBE_LT",
+ TableBuilderArgsNeedsLifetime(struct_def) ? "<'a>" : "");
+ code_ += "pub struct {{STRUCT_NAME}}Args{{MAYBE_LT}} {";
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ code_.SetValue("PARAM_TYPE", TableBuilderArgsDefnType(field, "'a"));
+ code_ += " pub {{FIELD_NAME}}: {{PARAM_TYPE}},";
+ });
+ code_ += "}";
+
+ // Generate an impl of Default for the *Args type:
+ code_ += "impl<'a> Default for {{STRUCT_NAME}}Args{{MAYBE_LT}} {";
+ code_ += " #[inline]";
+ code_ += " fn default() -> Self {";
+ code_ += " {{STRUCT_NAME}}Args {";
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ code_ += " {{FIELD_NAME}}: {{BLDR_DEF_VAL}},\\";
+ code_ += field.IsRequired() ? " // required field" : "";
+ });
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+
+ // Generate a builder struct:
+ code_ += "pub struct {{STRUCT_NAME}}Builder<'a: 'b, 'b> {";
+ code_ += " fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>,";
+ code_ +=
+ " start_: flatbuffers::WIPOffset<"
+ "flatbuffers::TableUnfinishedWIPOffset>,";
+ code_ += "}";
+
+ // Generate builder functions:
+ code_ += "impl<'a: 'b, 'b> {{STRUCT_NAME}}Builder<'a, 'b> {";
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ const bool is_scalar = IsScalar(field.value.type.base_type);
+ std::string offset = GetFieldOffsetName(field);
+ // Generate functions to add data, which take one of two forms.
+ //
+ // If a value has a default:
+ // fn add_x(x_: type) {
+ // fbb_.push_slot::<type>(offset, x_, Some(default));
+ // }
+ //
+ // If a value does not have a default:
+ // fn add_x(x_: type) {
+ // fbb_.push_slot_always::<type>(offset, x_);
+ // }
+ code_.SetValue("FIELD_OFFSET", Name(struct_def) + "::" + offset);
+ code_.SetValue("FIELD_TYPE", TableBuilderArgsAddFuncType(field, "'b "));
+ code_.SetValue("FUNC_BODY", TableBuilderArgsAddFuncBody(field));
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn add_{{FIELD_NAME}}(&mut self, {{FIELD_NAME}}: "
+ "{{FIELD_TYPE}}) {";
+ if (is_scalar && !field.IsOptional()) {
+ code_ +=
+ " {{FUNC_BODY}}({{FIELD_OFFSET}}, {{FIELD_NAME}}, "
+ "{{BLDR_DEF_VAL}});";
+ } else {
+ code_ += " {{FUNC_BODY}}({{FIELD_OFFSET}}, {{FIELD_NAME}});";
+ }
+ code_ += " }";
+ });
+
+ // Struct initializer (all fields required);
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> "
+ "{{STRUCT_NAME}}Builder<'a, 'b> {";
+ code_.SetValue("NUM_FIELDS", NumToString(struct_def.fields.vec.size()));
+ code_ += " let start = _fbb.start_table();";
+ code_ += " {{STRUCT_NAME}}Builder {";
+ code_ += " fbb_: _fbb,";
+ code_ += " start_: start,";
+ code_ += " }";
+ code_ += " }";
+
+ // finish() function.
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn finish(self) -> "
+ "flatbuffers::WIPOffset<{{STRUCT_NAME}}<'a>> {";
+ code_ += " let o = self.fbb_.end_table(self.start_);";
+
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ if (!field.IsRequired()) return;
+ code_ +=
+ " self.fbb_.required(o, {{STRUCT_NAME}}::{{OFFSET_NAME}},"
+ "\"{{FIELD_NAME}}\");";
+ });
+ code_ += " flatbuffers::WIPOffset::new(o.value())";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "impl std::fmt::Debug for {{STRUCT_NAME}}<'_> {";
+ code_ +=
+ " fn fmt(&self, f: &mut std::fmt::Formatter<'_>"
+ ") -> std::fmt::Result {";
+ code_ += " let mut ds = f.debug_struct(\"{{STRUCT_NAME}}\");";
+ ForAllTableFields(struct_def, [&](const FieldDef &field) {
+ if (GetFullType(field.value.type) == ftUnionValue) {
+ // Generate a match statement to handle unions properly.
+ code_.SetValue("KEY_TYPE", GenTableAccessorFuncReturnType(field, ""));
+ code_.SetValue("FIELD_TYPE_FIELD_NAME", field.name);
+ code_.SetValue("UNION_ERR",
+ "&\"InvalidFlatbuffer: Union discriminant"
+ " does not match value.\"");
+
+ code_ += " match self.{{FIELD_NAME}}_type() {";
+ ForAllUnionVariantsBesidesNone(
+ *field.value.type.enum_def, [&](const EnumVal &unused) {
+ (void)unused;
+ code_ += " {{U_ELEMENT_ENUM_TYPE}} => {";
+ code_ +=
+ " if let Some(x) = "
+ "self.{{FIELD_TYPE_FIELD_NAME}}_as_"
+ "{{U_ELEMENT_NAME}}() {";
+ code_ += " ds.field(\"{{FIELD_NAME}}\", &x)";
+ code_ += " } else {";
+ code_ +=
+ " ds.field(\"{{FIELD_NAME}}\", {{UNION_ERR}})";
+ code_ += " }";
+ code_ += " },";
+ });
+ code_ += " _ => {";
+ code_ += " let x: Option<()> = None;";
+ code_ += " ds.field(\"{{FIELD_NAME}}\", &x)";
+ code_ += " },";
+ code_ += " };";
+ } else {
+ // Most fields.
+ code_ += " ds.field(\"{{FIELD_NAME}}\", &self.{{FIELD_NAME}}());";
+ }
+ });
+ code_ += " ds.finish()";
+ code_ += " }";
+ code_ += "}";
+ }
+
+ void GenTableObject(const StructDef &table) {
+ code_.SetValue("OBJECT_NAME", NativeName(table));
+ code_.SetValue("STRUCT_NAME", Name(table));
+
+ // Generate the native object.
+ code_ += "#[non_exhaustive]";
+ code_ += "#[derive(Debug, Clone, PartialEq)]";
+ code_ += "pub struct {{OBJECT_NAME}} {";
+ ForAllObjectTableFields(table, [&](const FieldDef &field) {
+ // Union objects combine both the union discriminant and value, so we
+ // skip making a field for the discriminant.
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) return;
+ code_ += " pub {{FIELD_NAME}}: {{FIELD_OBJECT_TYPE}},";
+ });
+ code_ += "}";
+
+ code_ += "impl Default for {{OBJECT_NAME}} {";
+ code_ += " fn default() -> Self {";
+ code_ += " Self {";
+ ForAllObjectTableFields(table, [&](const FieldDef &field) {
+ if (field.value.type.base_type == BASE_TYPE_UTYPE) return;
+ std::string default_value = GetDefaultValue(field, kObject);
+ code_ += " {{FIELD_NAME}}: " + default_value + ",";
+ });
+ code_ += " }";
+ code_ += " }";
+ code_ += "}";
+
+ // TODO(cneo): Generate defaults for Native tables. However, since structs
+ // may be required, they, and therefore enums need defaults.
+
+ // Generate pack function.
+ code_ += "impl {{OBJECT_NAME}} {";
+ code_ += " pub fn pack<'b>(";
+ code_ += " &self,";
+ code_ += " _fbb: &mut flatbuffers::FlatBufferBuilder<'b>";
+ code_ += " ) -> flatbuffers::WIPOffset<{{STRUCT_NAME}}<'b>> {";
+ // First we generate variables for each field and then later assemble them
+ // using "StructArgs" to more easily manage ownership of the builder.
+ ForAllObjectTableFields(table, [&](const FieldDef &field) {
+ const Type &type = field.value.type;
+ switch (GetFullType(type)) {
+ case ftInteger:
+ case ftBool:
+ case ftFloat:
+ case ftEnumKey: {
+ code_ += " let {{FIELD_NAME}} = self.{{FIELD_NAME}};";
+ return;
+ }
+ case ftUnionKey: return; // Generate union type with union value.
+ case ftUnionValue: {
+ code_.SetValue("SNAKE_CASE_ENUM_NAME",
+ MakeSnakeCase(Name(*field.value.type.enum_def)));
+ code_ +=
+ " let {{FIELD_NAME}}_type = "
+ "self.{{FIELD_NAME}}.{{SNAKE_CASE_ENUM_NAME}}_type();";
+ code_ += " let {{FIELD_NAME}} = self.{{FIELD_NAME}}.pack(_fbb);";
+ return;
+ }
+ // The rest of the types require special casing around optionalness
+ // due to "required" annotation.
+ case ftString: {
+ MapNativeTableField(field, "_fbb.create_string(x)");
+ return;
+ }
+ case ftStruct: {
+ // Hold the struct in a variable so we can reference it.
+ if (field.IsRequired()) {
+ code_ +=
+ " let {{FIELD_NAME}}_tmp = "
+ "Some(self.{{FIELD_NAME}}.pack());";
+ } else {
+ code_ +=
+ " let {{FIELD_NAME}}_tmp = self.{{FIELD_NAME}}"
+ ".as_ref().map(|x| x.pack());";
+ }
+ code_ += " let {{FIELD_NAME}} = {{FIELD_NAME}}_tmp.as_ref();";
+
+ return;
+ }
+ case ftTable: {
+ MapNativeTableField(field, "x.pack(_fbb)");
+ return;
+ }
+ case ftVectorOfEnumKey:
+ case ftVectorOfInteger:
+ case ftVectorOfBool:
+ case ftVectorOfFloat: {
+ MapNativeTableField(field, "_fbb.create_vector(x)");
+ return;
+ }
+ case ftVectorOfStruct: {
+ MapNativeTableField(
+ field,
+ "let w: Vec<_> = x.iter().map(|t| t.pack()).collect();"
+ "_fbb.create_vector(&w)");
+ return;
+ }
+ case ftVectorOfString: {
+ // TODO(cneo): create_vector* should be more generic to avoid
+ // allocations.
+
+ MapNativeTableField(
+ field,
+ "let w: Vec<_> = x.iter().map(|s| s.as_ref()).collect();"
+ "_fbb.create_vector_of_strings(&w)");
+ return;
+ }
+ case ftVectorOfTable: {
+ MapNativeTableField(
+ field,
+ "let w: Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect();"
+ "_fbb.create_vector(&w)");
+ return;
+ }
+ case ftVectorOfUnionValue: {
+ FLATBUFFERS_ASSERT(false && "vectors of unions not yet supported");
+ return;
+ }
+ case ftArrayOfEnum:
+ case ftArrayOfStruct:
+ case ftArrayOfBuiltin: {
+ FLATBUFFERS_ASSERT(false && "arrays are not supported within tables");
+ return;
+ }
+ }
+ });
+ code_ += " {{STRUCT_NAME}}::create(_fbb, &{{STRUCT_NAME}}Args{";
+ ForAllObjectTableFields(table, [&](const FieldDef &field) {
+ (void)field; // Unused.
+ code_ += " {{FIELD_NAME}},";
+ });
+ code_ += " })";
+ code_ += " }";
+ code_ += "}";
+ }
+ void ForAllObjectTableFields(const StructDef &table,
+ std::function<void(const FieldDef &)> cb) {
+ const std::vector<FieldDef *> &v = table.fields.vec;
+ for (auto it = v.begin(); it != v.end(); it++) {
+ const FieldDef &field = **it;
+ if (field.deprecated) continue;
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_OBJECT_TYPE", ObjectFieldType(field, true));
+ cb(field);
+ }
+ }
+ void MapNativeTableField(const FieldDef &field, const std::string &expr) {
+ if (field.IsOptional()) {
+ code_ += " let {{FIELD_NAME}} = self.{{FIELD_NAME}}.as_ref().map(|x|{";
+ code_ += " " + expr;
+ code_ += " });";
+ } else {
+ // For some reason Args has optional types for required fields.
+ // TODO(cneo): Fix this... but its a breaking change?
+ code_ += " let {{FIELD_NAME}} = Some({";
+ code_ += " let x = &self.{{FIELD_NAME}};";
+ code_ += " " + expr;
+ code_ += " });";
+ }
+ }
+
+ // Generate functions to compare tables and structs by key. This function
+ // must only be called if the field key is defined.
+ void GenKeyFieldMethods(const FieldDef &field) {
+ FLATBUFFERS_ASSERT(field.key);
+
+ code_.SetValue("KEY_TYPE", GenTableAccessorFuncReturnType(field, ""));
+
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn key_compare_less_than(&self, o: &{{STRUCT_NAME}}) -> "
+ " bool {";
+ code_ += " self.{{FIELD_NAME}}() < o.{{FIELD_NAME}}()";
+ code_ += " }";
+ code_ += "";
+ code_ += " #[inline]";
+ code_ +=
+ " pub fn key_compare_with_value(&self, val: {{KEY_TYPE}}) -> "
+ " ::std::cmp::Ordering {";
+ code_ += " let key = self.{{FIELD_NAME}}();";
+ code_ += " key.cmp(&val)";
+ code_ += " }";
+ }
+
+ // Generate functions for accessing the root table object. This function
+ // must only be called if the root table is defined.
+ void GenRootTableFuncs(const StructDef &struct_def) {
+ FLATBUFFERS_ASSERT(parser_.root_struct_def_ && "root table not defined");
+ auto name = Name(struct_def);
+
+ code_.SetValue("STRUCT_NAME", name);
+ code_.SetValue("STRUCT_NAME_SNAKECASE", MakeSnakeCase(name));
+ code_.SetValue("STRUCT_NAME_CAPS", MakeUpper(MakeSnakeCase(name)));
+
+ // The root datatype accessors:
+ code_ += "#[inline]";
+ code_ +=
+ "#[deprecated(since=\"2.0.0\", "
+ "note=\"Deprecated in favor of `root_as...` methods.\")]";
+ code_ +=
+ "pub fn get_root_as_{{STRUCT_NAME_SNAKECASE}}<'a>(buf: &'a [u8])"
+ " -> {{STRUCT_NAME}}<'a> {";
+ code_ +=
+ " unsafe { flatbuffers::root_unchecked::<{{STRUCT_NAME}}"
+ "<'a>>(buf) }";
+ code_ += "}";
+ code_ += "";
+
+ code_ += "#[inline]";
+ code_ +=
+ "#[deprecated(since=\"2.0.0\", "
+ "note=\"Deprecated in favor of `root_as...` methods.\")]";
+ code_ +=
+ "pub fn get_size_prefixed_root_as_{{STRUCT_NAME_SNAKECASE}}"
+ "<'a>(buf: &'a [u8]) -> {{STRUCT_NAME}}<'a> {";
+ code_ +=
+ " unsafe { flatbuffers::size_prefixed_root_unchecked::<{{STRUCT_NAME}}"
+ "<'a>>(buf) }";
+ code_ += "}";
+ code_ += "";
+ // Default verifier root fns.
+ code_ += "#[inline]";
+ code_ += "/// Verifies that a buffer of bytes contains a `{{STRUCT_NAME}}`";
+ code_ += "/// and returns it.";
+ code_ += "/// Note that verification is still experimental and may not";
+ code_ += "/// catch every error, or be maximally performant. For the";
+ code_ += "/// previous, unchecked, behavior use";
+ code_ += "/// `root_as_{{STRUCT_NAME_SNAKECASE}}_unchecked`.";
+ code_ +=
+ "pub fn root_as_{{STRUCT_NAME_SNAKECASE}}(buf: &[u8]) "
+ "-> Result<{{STRUCT_NAME}}, flatbuffers::InvalidFlatbuffer> {";
+ code_ += " flatbuffers::root::<{{STRUCT_NAME}}>(buf)";
+ code_ += "}";
+ code_ += "#[inline]";
+ code_ += "/// Verifies that a buffer of bytes contains a size prefixed";
+ code_ += "/// `{{STRUCT_NAME}}` and returns it.";
+ code_ += "/// Note that verification is still experimental and may not";
+ code_ += "/// catch every error, or be maximally performant. For the";
+ code_ += "/// previous, unchecked, behavior use";
+ code_ += "/// `size_prefixed_root_as_{{STRUCT_NAME_SNAKECASE}}_unchecked`.";
+ code_ +=
+ "pub fn size_prefixed_root_as_{{STRUCT_NAME_SNAKECASE}}"
+ "(buf: &[u8]) -> Result<{{STRUCT_NAME}}, "
+ "flatbuffers::InvalidFlatbuffer> {";
+ code_ += " flatbuffers::size_prefixed_root::<{{STRUCT_NAME}}>(buf)";
+ code_ += "}";
+ // Verifier with options root fns.
+ code_ += "#[inline]";
+ code_ += "/// Verifies, with the given options, that a buffer of bytes";
+ code_ += "/// contains a `{{STRUCT_NAME}}` and returns it.";
+ code_ += "/// Note that verification is still experimental and may not";
+ code_ += "/// catch every error, or be maximally performant. For the";
+ code_ += "/// previous, unchecked, behavior use";
+ code_ += "/// `root_as_{{STRUCT_NAME_SNAKECASE}}_unchecked`.";
+ code_ += "pub fn root_as_{{STRUCT_NAME_SNAKECASE}}_with_opts<'b, 'o>(";
+ code_ += " opts: &'o flatbuffers::VerifierOptions,";
+ code_ += " buf: &'b [u8],";
+ code_ +=
+ ") -> Result<{{STRUCT_NAME}}<'b>, flatbuffers::InvalidFlatbuffer>"
+ " {";
+ code_ += " flatbuffers::root_with_opts::<{{STRUCT_NAME}}<'b>>(opts, buf)";
+ code_ += "}";
+ code_ += "#[inline]";
+ code_ += "/// Verifies, with the given verifier options, that a buffer of";
+ code_ += "/// bytes contains a size prefixed `{{STRUCT_NAME}}` and returns";
+ code_ += "/// it. Note that verification is still experimental and may not";
+ code_ += "/// catch every error, or be maximally performant. For the";
+ code_ += "/// previous, unchecked, behavior use";
+ code_ += "/// `root_as_{{STRUCT_NAME_SNAKECASE}}_unchecked`.";
+ code_ +=
+ "pub fn size_prefixed_root_as_{{STRUCT_NAME_SNAKECASE}}_with_opts"
+ "<'b, 'o>(";
+ code_ += " opts: &'o flatbuffers::VerifierOptions,";
+ code_ += " buf: &'b [u8],";
+ code_ +=
+ ") -> Result<{{STRUCT_NAME}}<'b>, flatbuffers::InvalidFlatbuffer>"
+ " {";
+ code_ +=
+ " flatbuffers::size_prefixed_root_with_opts::<{{STRUCT_NAME}}"
+ "<'b>>(opts, buf)";
+ code_ += "}";
+ // Unchecked root fns.
+ code_ += "#[inline]";
+ code_ +=
+ "/// Assumes, without verification, that a buffer of bytes "
+ "contains a {{STRUCT_NAME}} and returns it.";
+ code_ += "/// # Safety";
+ code_ +=
+ "/// Callers must trust the given bytes do indeed contain a valid"
+ " `{{STRUCT_NAME}}`.";
+ code_ +=
+ "pub unsafe fn root_as_{{STRUCT_NAME_SNAKECASE}}_unchecked"
+ "(buf: &[u8]) -> {{STRUCT_NAME}} {";
+ code_ += " flatbuffers::root_unchecked::<{{STRUCT_NAME}}>(buf)";
+ code_ += "}";
+ code_ += "#[inline]";
+ code_ +=
+ "/// Assumes, without verification, that a buffer of bytes "
+ "contains a size prefixed {{STRUCT_NAME}} and returns it.";
+ code_ += "/// # Safety";
+ code_ +=
+ "/// Callers must trust the given bytes do indeed contain a valid"
+ " size prefixed `{{STRUCT_NAME}}`.";
+ code_ +=
+ "pub unsafe fn size_prefixed_root_as_{{STRUCT_NAME_SNAKECASE}}"
+ "_unchecked(buf: &[u8]) -> {{STRUCT_NAME}} {";
+ code_ +=
+ " flatbuffers::size_prefixed_root_unchecked::<{{STRUCT_NAME}}>"
+ "(buf)";
+ code_ += "}";
+
+ if (parser_.file_identifier_.length()) {
+ // Declare the identifier
+ // (no lifetime needed as constants have static lifetimes by default)
+ code_ += "pub const {{STRUCT_NAME_CAPS}}_IDENTIFIER: &str\\";
+ code_ += " = \"" + parser_.file_identifier_ + "\";";
+ code_ += "";
+
+ // Check if a buffer has the identifier.
+ code_ += "#[inline]";
+ code_ += "pub fn {{STRUCT_NAME_SNAKECASE}}_buffer_has_identifier\\";
+ code_ += "(buf: &[u8]) -> bool {";
+ code_ += " flatbuffers::buffer_has_identifier(buf, \\";
+ code_ += "{{STRUCT_NAME_CAPS}}_IDENTIFIER, false)";
+ code_ += "}";
+ code_ += "";
+ code_ += "#[inline]";
+ code_ += "pub fn {{STRUCT_NAME_SNAKECASE}}_size_prefixed\\";
+ code_ += "_buffer_has_identifier(buf: &[u8]) -> bool {";
+ code_ += " flatbuffers::buffer_has_identifier(buf, \\";
+ code_ += "{{STRUCT_NAME_CAPS}}_IDENTIFIER, true)";
+ code_ += "}";
+ code_ += "";
+ }
+
+ if (parser_.file_extension_.length()) {
+ // Return the extension
+ code_ += "pub const {{STRUCT_NAME_CAPS}}_EXTENSION: &str = \\";
+ code_ += "\"" + parser_.file_extension_ + "\";";
+ code_ += "";
+ }
+
+ // Finish a buffer with a given root object:
+ code_.SetValue("OFFSET_TYPELABEL", Name(struct_def) + "Offset");
+ code_ += "#[inline]";
+ code_ += "pub fn finish_{{STRUCT_NAME_SNAKECASE}}_buffer<'a, 'b>(";
+ code_ += " fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>,";
+ code_ += " root: flatbuffers::WIPOffset<{{STRUCT_NAME}}<'a>>) {";
+ if (parser_.file_identifier_.length()) {
+ code_ += " fbb.finish(root, Some({{STRUCT_NAME_CAPS}}_IDENTIFIER));";
+ } else {
+ code_ += " fbb.finish(root, None);";
+ }
+ code_ += "}";
+ code_ += "";
+ code_ += "#[inline]";
+ code_ +=
+ "pub fn finish_size_prefixed_{{STRUCT_NAME_SNAKECASE}}_buffer"
+ "<'a, 'b>("
+ "fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, "
+ "root: flatbuffers::WIPOffset<{{STRUCT_NAME}}<'a>>) {";
+ if (parser_.file_identifier_.length()) {
+ code_ +=
+ " fbb.finish_size_prefixed(root, "
+ "Some({{STRUCT_NAME_CAPS}}_IDENTIFIER));";
+ } else {
+ code_ += " fbb.finish_size_prefixed(root, None);";
+ }
+ code_ += "}";
+ }
+
+ static void GenPadding(
+ const FieldDef &field, std::string *code_ptr, int *id,
+ const std::function<void(int bits, std::string *code_ptr, int *id)> &f) {
+ if (field.padding) {
+ for (int i = 0; i < 4; i++) {
+ if (static_cast<int>(field.padding) & (1 << i)) {
+ f((1 << i) * 8, code_ptr, id);
+ }
+ }
+ assert(!(field.padding & ~0xF));
+ }
+ }
+
+ static void PaddingDefinition(int bits, std::string *code_ptr, int *id) {
+ *code_ptr +=
+ " padding" + NumToString((*id)++) + "__: u" + NumToString(bits) + ",";
+ }
+
+ static void PaddingInitializer(int bits, std::string *code_ptr, int *id) {
+ (void)bits;
+ *code_ptr += "padding" + NumToString((*id)++) + "__: 0,";
+ }
+
+ void ForAllStructFields(const StructDef &struct_def,
+ std::function<void(const FieldDef &field)> cb) {
+ size_t offset_to_field = 0;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ code_.SetValue("FIELD_TYPE", GetTypeGet(field.value.type));
+ code_.SetValue("FIELD_OBJECT_TYPE", ObjectFieldType(field, false));
+ code_.SetValue("FIELD_NAME", Name(field));
+ code_.SetValue("FIELD_OFFSET", NumToString(offset_to_field));
+ code_.SetValue(
+ "REF",
+ IsStruct(field.value.type) || IsArray(field.value.type) ? "&" : "");
+ cb(field);
+ const size_t size = InlineSize(field.value.type);
+ offset_to_field += size + field.padding;
+ }
+ }
+ // Generate an accessor struct with constructor for a flatbuffers struct.
+ void GenStruct(const StructDef &struct_def) {
+ // Generates manual padding and alignment.
+ // Variables are private because they contain little endian data on all
+ // platforms.
+ GenComment(struct_def.doc_comment);
+ code_.SetValue("ALIGN", NumToString(struct_def.minalign));
+ code_.SetValue("STRUCT_NAME", Name(struct_def));
+ code_.SetValue("STRUCT_SIZE", NumToString(struct_def.bytesize));
+
+ // We represent Flatbuffers-structs in Rust-u8-arrays since the data may be
+ // of the wrong endianness and alignment 1.
+ //
+ // PartialEq is useful to derive because we can correctly compare structs
+ // for equality by just comparing their underlying byte data. This doesn't
+ // hold for PartialOrd/Ord.
+ code_ += "// struct {{STRUCT_NAME}}, aligned to {{ALIGN}}";
+ code_ += "#[repr(transparent)]";
+ code_ += "#[derive(Clone, Copy, PartialEq)]";
+ code_ += "pub struct {{STRUCT_NAME}}(pub [u8; {{STRUCT_SIZE}}]);";
+ code_ += "impl Default for {{STRUCT_NAME}} { ";
+ code_ += " fn default() -> Self { ";
+ code_ += " Self([0; {{STRUCT_SIZE}}])";
+ code_ += " }";
+ code_ += "}";
+
+ // Debug for structs.
+ code_ += "impl std::fmt::Debug for {{STRUCT_NAME}} {";
+ code_ +=
+ " fn fmt(&self, f: &mut std::fmt::Formatter"
+ ") -> std::fmt::Result {";
+ code_ += " f.debug_struct(\"{{STRUCT_NAME}}\")";
+ ForAllStructFields(struct_def, [&](const FieldDef &unused) {
+ (void)unused;
+ code_ += " .field(\"{{FIELD_NAME}}\", &self.{{FIELD_NAME}}())";
+ });
+ code_ += " .finish()";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ // Generate impls for SafeSliceAccess (because all structs are endian-safe),
+ // Follow for the value type, Follow for the reference type, Push for the
+ // value type, and Push for the reference type.
+ code_ += "impl flatbuffers::SimpleToVerifyInSlice for {{STRUCT_NAME}} {}";
+ code_ += "impl flatbuffers::SafeSliceAccess for {{STRUCT_NAME}} {}";
+ code_ += "impl<'a> flatbuffers::Follow<'a> for {{STRUCT_NAME}} {";
+ code_ += " type Inner = &'a {{STRUCT_NAME}};";
+ code_ += " #[inline]";
+ code_ += " fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {";
+ code_ += " <&'a {{STRUCT_NAME}}>::follow(buf, loc)";
+ code_ += " }";
+ code_ += "}";
+ code_ += "impl<'a> flatbuffers::Follow<'a> for &'a {{STRUCT_NAME}} {";
+ code_ += " type Inner = &'a {{STRUCT_NAME}};";
+ code_ += " #[inline]";
+ code_ += " fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {";
+ code_ += " flatbuffers::follow_cast_ref::<{{STRUCT_NAME}}>(buf, loc)";
+ code_ += " }";
+ code_ += "}";
+ code_ += "impl<'b> flatbuffers::Push for {{STRUCT_NAME}} {";
+ code_ += " type Output = {{STRUCT_NAME}};";
+ code_ += " #[inline]";
+ code_ += " fn push(&self, dst: &mut [u8], _rest: &[u8]) {";
+ code_ += " let src = unsafe {";
+ code_ +=
+ " ::std::slice::from_raw_parts("
+ "self as *const {{STRUCT_NAME}} as *const u8, Self::size())";
+ code_ += " };";
+ code_ += " dst.copy_from_slice(src);";
+ code_ += " }";
+ code_ += "}";
+ code_ += "impl<'b> flatbuffers::Push for &'b {{STRUCT_NAME}} {";
+ code_ += " type Output = {{STRUCT_NAME}};";
+ code_ += "";
+ code_ += " #[inline]";
+ code_ += " fn push(&self, dst: &mut [u8], _rest: &[u8]) {";
+ code_ += " let src = unsafe {";
+ code_ +=
+ " ::std::slice::from_raw_parts("
+ "*self as *const {{STRUCT_NAME}} as *const u8, Self::size())";
+ code_ += " };";
+ code_ += " dst.copy_from_slice(src);";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+
+ // Generate verifier: Structs are simple so presence and alignment are
+ // all that need to be checked.
+ code_ += "impl<'a> flatbuffers::Verifiable for {{STRUCT_NAME}} {";
+ code_ += " #[inline]";
+ code_ += " fn run_verifier(";
+ code_ += " v: &mut flatbuffers::Verifier, pos: usize";
+ code_ += " ) -> Result<(), flatbuffers::InvalidFlatbuffer> {";
+ code_ += " use self::flatbuffers::Verifiable;";
+ code_ += " v.in_buffer::<Self>(pos)";
+ code_ += " }";
+ code_ += "}";
+
+ // Generate a constructor that takes all fields as arguments.
+ code_ += "impl<'a> {{STRUCT_NAME}} {";
+ code_ += " #[allow(clippy::too_many_arguments)]";
+ code_ += " pub fn new(";
+ ForAllStructFields(struct_def, [&](const FieldDef &unused) {
+ (void)unused;
+ code_ += " {{FIELD_NAME}}: {{REF}}{{FIELD_TYPE}},";
+ });
+ code_ += " ) -> Self {";
+ code_ += " let mut s = Self([0; {{STRUCT_SIZE}}]);";
+ ForAllStructFields(struct_def, [&](const FieldDef &unused) {
+ (void)unused;
+ code_ += " s.set_{{FIELD_NAME}}({{REF}}{{FIELD_NAME}});";
+ });
+ code_ += " s";
+ code_ += " }";
+ code_ += "";
+
+ if (parser_.opts.generate_name_strings) {
+ GenFullyQualifiedNameGetter(struct_def, struct_def.name);
+ }
+
+ // Generate accessor methods for the struct.
+ ForAllStructFields(struct_def, [&](const FieldDef &field) {
+ this->GenComment(field.doc_comment, " ");
+ // Getter.
+ if (IsStruct(field.value.type)) {
+ code_ += " pub fn {{FIELD_NAME}}(&self) -> &{{FIELD_TYPE}} {";
+ code_ +=
+ " unsafe {"
+ " &*(self.0[{{FIELD_OFFSET}}..].as_ptr() as *const"
+ " {{FIELD_TYPE}}) }";
+ } else if (IsArray(field.value.type)) {
+ code_.SetValue("ARRAY_SIZE",
+ NumToString(field.value.type.fixed_length));
+ code_.SetValue("ARRAY_ITEM", GetTypeGet(field.value.type.VectorType()));
+ code_ +=
+ " pub fn {{FIELD_NAME}}(&'a self) -> "
+ "flatbuffers::Array<'a, {{ARRAY_ITEM}}, {{ARRAY_SIZE}}> {";
+ code_ += " flatbuffers::Array::follow(&self.0, {{FIELD_OFFSET}})";
+ } else {
+ code_ += " pub fn {{FIELD_NAME}}(&self) -> {{FIELD_TYPE}} {";
+ code_ +=
+ " let mut mem = core::mem::MaybeUninit::"
+ "<{{FIELD_TYPE}}>::uninit();";
+ code_ += " unsafe {";
+ code_ += " core::ptr::copy_nonoverlapping(";
+ code_ += " self.0[{{FIELD_OFFSET}}..].as_ptr(),";
+ code_ += " mem.as_mut_ptr() as *mut u8,";
+ code_ += " core::mem::size_of::<{{FIELD_TYPE}}>(),";
+ code_ += " );";
+ code_ += " mem.assume_init()";
+ code_ += " }.from_little_endian()";
+ }
+ code_ += " }\n";
+ // Setter.
+ if (IsStruct(field.value.type)) {
+ code_.SetValue("FIELD_SIZE", NumToString(InlineSize(field.value.type)));
+ code_ += " pub fn set_{{FIELD_NAME}}(&mut self, x: &{{FIELD_TYPE}}) {";
+ code_ +=
+ " self.0[{{FIELD_OFFSET}}..{{FIELD_OFFSET}}+{{FIELD_SIZE}}]"
+ ".copy_from_slice(&x.0)";
+ } else if (IsArray(field.value.type)) {
+ if (GetFullType(field.value.type) == ftArrayOfBuiltin) {
+ code_.SetValue("ARRAY_ITEM",
+ GetTypeGet(field.value.type.VectorType()));
+ code_.SetValue(
+ "ARRAY_ITEM_SIZE",
+ NumToString(InlineSize(field.value.type.VectorType())));
+ code_ +=
+ " pub fn set_{{FIELD_NAME}}(&mut self, items: &{{FIELD_TYPE}}) "
+ "{";
+ code_ +=
+ " flatbuffers::emplace_scalar_array(&mut self.0, "
+ "{{FIELD_OFFSET}}, items);";
+ } else {
+ code_.SetValue("FIELD_SIZE",
+ NumToString(InlineSize(field.value.type)));
+ code_ +=
+ " pub fn set_{{FIELD_NAME}}(&mut self, x: &{{FIELD_TYPE}}) {";
+ code_ += " unsafe {";
+ code_ += " std::ptr::copy(";
+ code_ += " x.as_ptr() as *const u8,";
+ code_ += " self.0.as_mut_ptr().add({{FIELD_OFFSET}}),";
+ code_ += " {{FIELD_SIZE}},";
+ code_ += " );";
+ code_ += " }";
+ }
+ } else {
+ code_ += " pub fn set_{{FIELD_NAME}}(&mut self, x: {{FIELD_TYPE}}) {";
+ code_ += " let x_le = x.to_little_endian();";
+ code_ += " unsafe {";
+ code_ += " core::ptr::copy_nonoverlapping(";
+ code_ += " &x_le as *const {{FIELD_TYPE}} as *const u8,";
+ code_ += " self.0[{{FIELD_OFFSET}}..].as_mut_ptr(),";
+ code_ += " core::mem::size_of::<{{FIELD_TYPE}}>(),";
+ code_ += " );";
+ code_ += " }";
+ }
+ code_ += " }\n";
+
+ // Generate a comparison function for this field if it is a key.
+ if (field.key) { GenKeyFieldMethods(field); }
+ });
+
+ // Generate Object API unpack method.
+ if (parser_.opts.generate_object_based_api) {
+ code_.SetValue("NATIVE_STRUCT_NAME", NativeName(struct_def));
+ code_ += " pub fn unpack(&self) -> {{NATIVE_STRUCT_NAME}} {";
+ code_ += " {{NATIVE_STRUCT_NAME}} {";
+ ForAllStructFields(struct_def, [&](const FieldDef &field) {
+ if (IsArray(field.value.type)) {
+ if (GetFullType(field.value.type) == ftArrayOfStruct) {
+ code_ +=
+ " {{FIELD_NAME}}: { let {{FIELD_NAME}} = "
+ "self.{{FIELD_NAME}}(); flatbuffers::array_init(|i| "
+ "{{FIELD_NAME}}.get(i).unpack()) },";
+ } else {
+ code_ += " {{FIELD_NAME}}: self.{{FIELD_NAME}}().into(),";
+ }
+ } else {
+ std::string unpack = IsStruct(field.value.type) ? ".unpack()" : "";
+ code_ += " {{FIELD_NAME}}: self.{{FIELD_NAME}}()" + unpack + ",";
+ }
+ });
+ code_ += " }";
+ code_ += " }";
+ }
+
+ code_ += "}"; // End impl Struct methods.
+ code_ += "";
+
+ // Generate Struct Object.
+ if (parser_.opts.generate_object_based_api) {
+ // Struct declaration
+ code_ += "#[derive(Debug, Clone, PartialEq, Default)]";
+ code_ += "pub struct {{NATIVE_STRUCT_NAME}} {";
+ ForAllStructFields(struct_def, [&](const FieldDef &field) {
+ (void)field; // unused.
+ code_ += " pub {{FIELD_NAME}}: {{FIELD_OBJECT_TYPE}},";
+ });
+ code_ += "}";
+ // The `pack` method that turns the native struct into its Flatbuffers
+ // counterpart.
+ code_ += "impl {{NATIVE_STRUCT_NAME}} {";
+ code_ += " pub fn pack(&self) -> {{STRUCT_NAME}} {";
+ code_ += " {{STRUCT_NAME}}::new(";
+ ForAllStructFields(struct_def, [&](const FieldDef &field) {
+ if (IsStruct(field.value.type)) {
+ code_ += " &self.{{FIELD_NAME}}.pack(),";
+ } else if (IsArray(field.value.type)) {
+ if (GetFullType(field.value.type) == ftArrayOfStruct) {
+ code_ +=
+ " &flatbuffers::array_init(|i| "
+ "self.{{FIELD_NAME}}[i].pack()),";
+ } else {
+ code_ += " &self.{{FIELD_NAME}},";
+ }
+ } else {
+ code_ += " self.{{FIELD_NAME}},";
+ }
+ });
+ code_ += " )";
+ code_ += " }";
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ void GenNamespaceImports(const int white_spaces) {
+ // DO not use global attributes (i.e. #![...]) since it interferes
+ // with users who include! generated files.
+ // See: https://github.com/google/flatbuffers/issues/6261
+ std::string indent = std::string(white_spaces, ' ');
+ code_ += "";
+ if (!parser_.opts.generate_all) {
+ for (auto it = parser_.included_files_.begin();
+ it != parser_.included_files_.end(); ++it) {
+ if (it->second.empty()) continue;
+ auto noext = flatbuffers::StripExtension(it->second);
+ auto basename = flatbuffers::StripPath(noext);
+
+ if (parser_.opts.include_prefix.empty()) {
+ code_ += indent + "use crate::" + basename +
+ parser_.opts.filename_suffix + "::*;";
+ } else {
+ auto prefix = parser_.opts.include_prefix;
+ prefix.pop_back();
+
+ code_ += indent + "use crate::" + prefix + "::" + basename +
+ parser_.opts.filename_suffix + "::*;";
+ }
+ }
+ }
+ code_ += indent + "use std::mem;";
+ code_ += indent + "use std::cmp::Ordering;";
+ code_ += "";
+ code_ += indent + "extern crate flatbuffers;";
+ code_ += indent + "use self::flatbuffers::{EndianScalar, Follow};";
+ }
+
+ // Set up the correct namespace. This opens a namespace if the current
+ // namespace is different from the target namespace. This function
+ // closes and opens the namespaces only as necessary.
+ //
+ // The file must start and end with an empty (or null) namespace so that
+ // namespaces are properly opened and closed.
+ void SetNameSpace(const Namespace *ns) {
+ if (cur_name_space_ == ns) { return; }
+
+ // Compute the size of the longest common namespace prefix.
+ // If cur_name_space is A::B::C::D and ns is A::B::E::F::G,
+ // the common prefix is A::B:: and we have old_size = 4, new_size = 5
+ // and common_prefix_size = 2
+ size_t old_size = cur_name_space_ ? cur_name_space_->components.size() : 0;
+ size_t new_size = ns ? ns->components.size() : 0;
+
+ size_t common_prefix_size = 0;
+ while (common_prefix_size < old_size && common_prefix_size < new_size &&
+ ns->components[common_prefix_size] ==
+ cur_name_space_->components[common_prefix_size]) {
+ common_prefix_size++;
+ }
+
+ // Close cur_name_space in reverse order to reach the common prefix.
+ // In the previous example, D then C are closed.
+ for (size_t j = old_size; j > common_prefix_size; --j) {
+ code_ += "} // pub mod " + cur_name_space_->components[j - 1];
+ }
+ if (old_size != common_prefix_size) { code_ += ""; }
+
+ // open namespace parts to reach the ns namespace
+ // in the previous example, E, then F, then G are opened
+ for (auto j = common_prefix_size; j != new_size; ++j) {
+ code_ += "#[allow(unused_imports, dead_code)]";
+ code_ += "pub mod " + MakeSnakeCase(ns->components[j]) + " {";
+ // Generate local namespace imports.
+ GenNamespaceImports(2);
+ }
+ if (new_size != common_prefix_size) { code_ += ""; }
+
+ cur_name_space_ = ns;
+ }
+};
+
+} // namespace rust
+
+bool GenerateRust(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ rust::RustGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+std::string RustMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ std::string filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ rust::RustGenerator generator(parser, path, file_name);
+ std::string make_rule =
+ generator.GeneratedFileName(path, filebase, parser.opts) + ": ";
+
+ auto included_files = parser.GetIncludedFilesRecursive(file_name);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
+
+// TODO(rw): Generated code should import other generated files.
+// TODO(rw): Generated code should refer to namespaces in included files in a
+// way that makes them referrable.
+// TODO(rw): Generated code should indent according to nesting level.
+// TODO(rw): Generated code should generate endian-safe Debug impls.
+// TODO(rw): Generated code could use a Rust-only enum type to access unions,
+// instead of making the user use _type() to manually switch.
+// TODO(maxburke): There should be test schemas added that use language
+// keywords as fields of structs, tables, unions, enums, to make sure
+// that internal code generated references escaped names correctly.
+// TODO(maxburke): We should see if there is a more flexible way of resolving
+// module paths for use declarations. Right now if schemas refer to
+// other flatbuffer files, the include paths in emitted Rust bindings
+// are crate-relative which may undesirable.
diff --git a/contrib/libs/flatbuffers/src/idl_gen_swift.cpp b/contrib/libs/flatbuffers/src/idl_gen_swift.cpp
new file mode 100644
index 0000000000..3fffd39455
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_swift.cpp
@@ -0,0 +1,1575 @@
+/*
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cctype>
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+namespace swift {
+
+inline std::string GenIndirect(const std::string &reading) {
+ return "{{ACCESS}}.indirect(" + reading + ")";
+}
+
+inline std::string GenArrayMainBody(const std::string &optional) {
+ return "{{ACCESS_TYPE}} func {{VALUENAME}}(at index: Int32) -> "
+ "{{VALUETYPE}}" +
+ optional + " { ";
+}
+
+class SwiftGenerator : public BaseGenerator {
+ private:
+ CodeWriter code_;
+ std::unordered_set<std::string> keywords_;
+ int namespace_depth;
+
+ public:
+ SwiftGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", "_", "swift") {
+ namespace_depth = 0;
+ code_.SetPadding(" ");
+ static const char *const keywords[] = {
+ "associatedtype",
+ "class",
+ "deinit",
+ "enum",
+ "extension",
+ "fileprivate",
+ "func",
+ "import",
+ "init",
+ "inout",
+ "internal",
+ "let",
+ "open",
+ "operator",
+ "private",
+ "protocol",
+ "public",
+ "rethrows",
+ "static",
+ "struct",
+ "subscript",
+ "typealias",
+ "var",
+ "break",
+ "case",
+ "continue",
+ "default",
+ "defer",
+ "do",
+ "else",
+ "fallthrough",
+ "for",
+ "guard",
+ "if",
+ "in",
+ "repeat",
+ "return",
+ "switch",
+ "where",
+ "while",
+ "Any",
+ "catch",
+ "false",
+ "is",
+ "nil",
+ "super",
+ "self",
+ "Self",
+ "throw",
+ "throws",
+ "true",
+ "try",
+ "associativity",
+ "convenience",
+ "dynamic",
+ "didSet",
+ "final",
+ "get",
+ "infix",
+ "indirect",
+ "lazy",
+ "left",
+ "mutating",
+ "none",
+ "nonmutating",
+ "optional",
+ "override",
+ "postfix",
+ "precedence",
+ "prefix",
+ "Protocol",
+ "required",
+ "right",
+ "set",
+ "Type",
+ "unowned",
+ "weak",
+ "willSet",
+ "Void",
+ nullptr,
+ };
+ for (auto kw = keywords; *kw; kw++) keywords_.insert(*kw);
+ }
+
+ bool generate() {
+ code_.Clear();
+ code_.SetValue("ACCESS", "_accessor");
+ code_.SetValue("TABLEOFFSET", "VTOFFSET");
+ code_ += "// " + std::string(FlatBuffersGeneratedWarning());
+ code_ += "// swiftlint:disable all";
+ code_ += "// swiftformat:disable all\n";
+ code_ += "import FlatBuffers\n";
+ // Generate code for all the enum declarations.
+
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ const auto &enum_def = **it;
+ if (!enum_def.generated) { GenEnum(enum_def); }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (struct_def.fixed && !struct_def.generated) {
+ GenStructReader(struct_def);
+ GenMutableStructReader(struct_def);
+ }
+ }
+
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ const auto &struct_def = **it;
+ if (!struct_def.fixed && !struct_def.generated) {
+ GenTable(struct_def);
+ if (parser_.opts.generate_object_based_api) {
+ GenObjectAPI(struct_def);
+ }
+ }
+ }
+
+ const auto filename = GeneratedFileName(path_, file_name_, parser_.opts);
+ const auto final_code = code_.ToString();
+ return SaveFile(filename.c_str(), final_code, false);
+ }
+
+ void mark(const std::string &str) {
+ code_.SetValue("MARKVALUE", str);
+ code_ += "\n// MARK: - {{MARKVALUE}}\n";
+ }
+
+ // MARK: - Generating structs
+
+ // Generates the reader for swift
+ void GenStructReader(const StructDef &struct_def) {
+ auto is_private_access = struct_def.attributes.Lookup("private");
+ code_.SetValue("ACCESS_TYPE", is_private_access ? "internal" : "public");
+ GenComment(struct_def.doc_comment);
+ code_.SetValue("STRUCTNAME", NameWrappedInNameSpace(struct_def));
+ code_ += "{{ACCESS_TYPE}} struct {{STRUCTNAME}}: NativeStruct\\";
+ if (parser_.opts.generate_object_based_api) code_ += ", NativeObject\\";
+ code_ += " {";
+ code_ += "";
+ Indent();
+ code_ += ValidateFunc();
+ code_ += "";
+ int padding_id = 0;
+ std::string constructor = "";
+ std::vector<std::string> base_constructor;
+ std::vector<std::string> main_constructor;
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ if (!constructor.empty()) constructor += ", ";
+
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ if (IsEnum(field.value.type)) {
+ code_.SetValue("BASEVALUE", GenTypeBasic(field.value.type, false));
+ }
+ code_.SetValue("VALUETYPE", type);
+ GenComment(field.doc_comment);
+ std::string valueType =
+ IsEnum(field.value.type) ? "{{BASEVALUE}}" : "{{VALUETYPE}}";
+ code_ += "private var _{{VALUENAME}}: " + valueType;
+ auto accessing_value = IsEnum(field.value.type) ? ".value" : "";
+ auto base_value =
+ IsStruct(field.value.type) ? (type + "()") : field.value.constant;
+
+ main_constructor.push_back("_" + name + " = " + name + accessing_value);
+ base_constructor.push_back("_" + name + " = " + base_value);
+
+ if (field.padding) { GenPadding(field, &padding_id); }
+ constructor += name + ": " + type;
+ }
+ code_ += "";
+ BuildObjectConstructor(main_constructor, constructor);
+ BuildObjectConstructor(base_constructor, "");
+
+ if (parser_.opts.generate_object_based_api)
+ GenerateObjectAPIStructConstructor(struct_def);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ code_.SetValue("VALUETYPE", type);
+ GenComment(field.doc_comment);
+ if (!IsEnum(field.value.type)) {
+ code_ += GenReaderMainBody() + "_{{VALUENAME}} }";
+ } else if (IsEnum(field.value.type)) {
+ code_ +=
+ GenReaderMainBody() + "{{VALUETYPE}}(rawValue: _{{VALUENAME}})! }";
+ }
+ }
+ Outdent();
+ code_ += "}\n";
+ }
+
+ void GenMutableStructReader(const StructDef &struct_def) {
+ GenObjectHeader(struct_def);
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto offset = NumToString(field.value.offset);
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ if (IsEnum(field.value.type)) {
+ code_.SetValue("BASEVALUE", GenTypeBasic(field.value.type, false));
+ }
+ code_.SetValue("VALUETYPE", type);
+ code_.SetValue("OFFSET", offset);
+ if (IsScalar(field.value.type.base_type) && !IsEnum(field.value.type)) {
+ code_ +=
+ GenReaderMainBody() + "return " + GenReader("VALUETYPE") + " }";
+ } else if (IsEnum(field.value.type)) {
+ code_.SetValue("BASEVALUE", GenTypeBasic(field.value.type, false));
+ code_ += GenReaderMainBody() + "return " +
+ GenEnumConstructor("{{OFFSET}}") + "?? " +
+ GenEnumDefaultValue(field) + " }";
+ } else if (IsStruct(field.value.type)) {
+ code_.SetValue("VALUETYPE", GenType(field.value.type) + Mutable());
+ code_ += GenReaderMainBody() + "return " +
+ GenConstructor("{{ACCESS}}.postion + {{OFFSET}}");
+ }
+ if (parser_.opts.mutable_buffer && !IsStruct(field.value.type))
+ code_ += GenMutate("{{OFFSET}}", "", IsEnum(field.value.type));
+ }
+
+ if (parser_.opts.generate_object_based_api) {
+ GenerateObjectAPIExtensionHeader(NameWrappedInNameSpace(struct_def));
+ code_ += "return builder.create(struct: obj)";
+ Outdent();
+ code_ += "}";
+ }
+ Outdent();
+ code_ += "}\n";
+ }
+
+ // Generates the create function for swift
+ void GenStructWriter(const StructDef &struct_def) {
+ auto is_private_access = struct_def.attributes.Lookup("private");
+ code_.SetValue("ACCESS_TYPE", is_private_access ? "internal" : "public");
+ code_.SetValue("STRUCTNAME", NameWrappedInNameSpace(struct_def));
+ code_.SetValue("SHORT_STRUCTNAME", Name(struct_def));
+ code_ += "extension {{STRUCTNAME}} {";
+ Indent();
+ code_ += "@discardableResult";
+ code_ +=
+ "{{ACCESS_TYPE}} static func create{{SHORT_STRUCTNAME}}(builder: inout "
+ "FlatBufferBuilder, \\";
+ std::string func_header = "";
+ GenerateStructArgs(struct_def, &func_header, "", "");
+ code_ += func_header.substr(0, func_header.size() - 2) + "\\";
+ code_ += ") -> Offset {";
+ Indent();
+ code_ +=
+ "builder.createStructOf(size: {{STRUCTNAME}}.size, alignment: "
+ "{{STRUCTNAME}}.alignment)";
+ code_ += "return builder.endStruct()";
+ Outdent();
+ code_ += "}\n";
+ Outdent();
+ code_ += "}\n";
+ }
+
+ void GenerateStructArgs(const StructDef &struct_def, std::string *code_ptr,
+ const std::string &nameprefix,
+ const std::string &object_name,
+ const std::string &obj_api_named = "",
+ bool is_obj_api = false) {
+ auto &code = *code_ptr;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ const auto &field_type = field.value.type;
+ if (IsStruct(field.value.type)) {
+ GenerateStructArgs(
+ *field_type.struct_def, code_ptr, (nameprefix + field.name),
+ (object_name + "." + field.name), obj_api_named, is_obj_api);
+ } else {
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ if (!is_obj_api) {
+ code += nameprefix + name + ": " + type;
+ if (!IsEnum(field.value.type)) {
+ code += " = ";
+ auto is_bool = IsBool(field.value.type.base_type);
+ auto constant =
+ is_bool ? ("0" == field.value.constant ? "false" : "true")
+ : field.value.constant;
+ code += constant;
+ }
+ code += ", ";
+ continue;
+ }
+ code +=
+ nameprefix + name + ": " + obj_api_named + object_name + "." + name;
+ code += ", ";
+ }
+ }
+ }
+
+ // MARK: - Table Generator
+
+ // Generates the reader for swift
+ void GenTable(const StructDef &struct_def) {
+ auto is_private_access = struct_def.attributes.Lookup("private");
+ code_.SetValue("ACCESS_TYPE", is_private_access ? "internal" : "public");
+
+ GenObjectHeader(struct_def);
+ GenTableAccessors(struct_def);
+ GenTableReader(struct_def);
+ GenTableWriter(struct_def);
+ if (parser_.opts.generate_object_based_api)
+ GenerateObjectAPITableExtension(struct_def);
+ Outdent();
+ code_ += "}\n";
+ }
+
+ // Generates the reader for swift
+ void GenTableAccessors(const StructDef &struct_def) {
+ // Generate field id constants.
+ if (struct_def.fields.vec.size() > 0) {
+ code_ += "private enum {{TABLEOFFSET}}: VOffset {";
+ Indent();
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) { continue; }
+ code_.SetValue("OFFSET_NAME", Name(field));
+ code_.SetValue("OFFSET_VALUE", NumToString(field.value.offset));
+ code_ += "case {{OFFSET_NAME}} = {{OFFSET_VALUE}}";
+ }
+ code_ += "var v: Int32 { Int32(self.rawValue) }";
+ code_ += "var p: VOffset { self.rawValue }";
+ Outdent();
+ code_ += "}";
+ code_ += "";
+ }
+ }
+
+ void GenObjectHeader(const StructDef &struct_def) {
+ GenComment(struct_def.doc_comment);
+
+ code_.SetValue("SHORT_STRUCTNAME", Name(struct_def));
+ code_.SetValue("STRUCTNAME", NameWrappedInNameSpace(struct_def));
+ code_.SetValue("OBJECTTYPE", struct_def.fixed ? "Struct" : "Table");
+ code_.SetValue("MUTABLE", struct_def.fixed ? Mutable() : "");
+ code_ +=
+ "{{ACCESS_TYPE}} struct {{STRUCTNAME}}{{MUTABLE}}: FlatBufferObject\\";
+ if (!struct_def.fixed && parser_.opts.generate_object_based_api)
+ code_ += ", ObjectAPIPacker\\";
+ code_ += " {\n";
+ Indent();
+ code_ += ValidateFunc();
+ code_ +=
+ "{{ACCESS_TYPE}} var __buffer: ByteBuffer! { return {{ACCESS}}.bb }";
+ code_ += "private var {{ACCESS}}: {{OBJECTTYPE}}\n";
+ if (!struct_def.fixed) {
+ if (parser_.file_identifier_.length()) {
+ code_.SetValue("FILENAME", parser_.file_identifier_);
+ code_ +=
+ "{{ACCESS_TYPE}} static func finish(_ fbb: inout "
+ "FlatBufferBuilder, end: "
+ "Offset, prefix: Bool = false) { fbb.finish(offset: end, "
+ "fileId: "
+ "\"{{FILENAME}}\", addPrefix: prefix) }";
+ }
+ code_ +=
+ "{{ACCESS_TYPE}} static func getRootAs{{SHORT_STRUCTNAME}}(bb: "
+ "ByteBuffer) -> "
+ "{{STRUCTNAME}} { return {{STRUCTNAME}}(Table(bb: bb, position: "
+ "Int32(bb.read(def: UOffset.self, position: bb.reader)) + "
+ "Int32(bb.reader))) }\n";
+ code_ += "private init(_ t: Table) { {{ACCESS}} = t }";
+ }
+ code_ +=
+ "{{ACCESS_TYPE}} init(_ bb: ByteBuffer, o: Int32) { {{ACCESS}} = "
+ "{{OBJECTTYPE}}(bb: "
+ "bb, position: o) }";
+ code_ += "";
+ }
+
+ void GenTableWriter(const StructDef &struct_def) {
+ flatbuffers::FieldDef *key_field = nullptr;
+ std::vector<std::string> require_fields;
+ std::vector<std::string> create_func_body;
+ std::vector<std::string> create_func_header;
+ auto should_generate_create = struct_def.fields.vec.size() != 0;
+
+ code_.SetValue("NUMBEROFFIELDS", NumToString(struct_def.fields.vec.size()));
+ code_ +=
+ "{{ACCESS_TYPE}} static func start{{SHORT_STRUCTNAME}}(_ fbb: inout "
+ "FlatBufferBuilder) -> "
+ "UOffset { fbb.startTable(with: {{NUMBEROFFIELDS}}) }";
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ if (field.key) key_field = &field;
+ if (field.IsRequired())
+ require_fields.push_back(NumToString(field.value.offset));
+
+ GenTableWriterFields(field, &create_func_body, &create_func_header);
+ }
+ code_ +=
+ "{{ACCESS_TYPE}} static func end{{SHORT_STRUCTNAME}}(_ fbb: inout "
+ "FlatBufferBuilder, "
+ "start: "
+ "UOffset) -> Offset { let end = Offset(offset: "
+ "fbb.endTable(at: start))\\";
+ if (require_fields.capacity() != 0) {
+ std::string fields = "";
+ for (auto it = require_fields.begin(); it != require_fields.end(); ++it)
+ fields += *it + ", ";
+ code_.SetValue("FIELDS", fields.substr(0, fields.size() - 2));
+ code_ += "; fbb.require(table: end, fields: [{{FIELDS}}])\\";
+ }
+ code_ += "; return end }";
+
+ if (should_generate_create) {
+ code_ += "{{ACCESS_TYPE}} static func create{{SHORT_STRUCTNAME}}(";
+ Indent();
+ code_ += "_ fbb: inout FlatBufferBuilder,";
+ for (auto it = create_func_header.begin(); it < create_func_header.end();
+ ++it) {
+ code_ += *it + "\\";
+ if (it < create_func_header.end() - 1) code_ += ",";
+ }
+ code_ += "";
+ Outdent();
+ code_ += ") -> Offset {";
+ Indent();
+ code_ += "let __start = {{STRUCTNAME}}.start{{SHORT_STRUCTNAME}}(&fbb)";
+ for (auto it = create_func_body.begin(); it < create_func_body.end();
+ ++it) {
+ code_ += *it;
+ }
+ code_ +=
+ "return {{STRUCTNAME}}.end{{SHORT_STRUCTNAME}}(&fbb, start: __start)";
+ Outdent();
+ code_ += "}";
+ }
+
+ std::string spacing = "";
+
+ if (key_field != nullptr && !struct_def.fixed && struct_def.has_key) {
+ code_.SetValue("VALUENAME", NameWrappedInNameSpace(struct_def));
+ code_.SetValue("SHORT_VALUENAME", Name(struct_def));
+ code_.SetValue("VOFFSET", NumToString(key_field->value.offset));
+
+ code_ +=
+ "{{ACCESS_TYPE}} static func "
+ "sortVectorOf{{SHORT_VALUENAME}}(offsets:[Offset], "
+ "_ fbb: inout FlatBufferBuilder) -> Offset {";
+ Indent();
+ code_ += spacing + "var off = offsets";
+ code_ +=
+ spacing +
+ "off.sort { Table.compare(Table.offset(Int32($1.o), vOffset: "
+ "{{VOFFSET}}, fbb: fbb.buffer), Table.offset(Int32($0.o), vOffset: "
+ "{{VOFFSET}}, fbb: fbb.buffer), fbb: fbb.buffer) < 0 } ";
+ code_ += spacing + "return fbb.createVector(ofOffsets: off)";
+ Outdent();
+ code_ += "}";
+ GenLookup(*key_field);
+ }
+ }
+
+ void GenTableWriterFields(const FieldDef &field,
+ std::vector<std::string> *create_body,
+ std::vector<std::string> *create_header) {
+ std::string builder_string = ", _ fbb: inout FlatBufferBuilder) { ";
+ auto &create_func_body = *create_body;
+ auto &create_func_header = *create_header;
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ auto opt_scalar =
+ field.IsOptional() && IsScalar(field.value.type.base_type);
+ auto nullable_type = opt_scalar ? type + "?" : type;
+ code_.SetValue("VALUENAME", name);
+ code_.SetValue("VALUETYPE", nullable_type);
+ code_.SetValue("OFFSET", name);
+ code_.SetValue("CONSTANT", field.value.constant);
+ std::string check_if_vector =
+ (IsVector(field.value.type) || IsArray(field.value.type)) ? "VectorOf("
+ : "(";
+ auto body = "add" + check_if_vector + name + ": ";
+ code_ += "{{ACCESS_TYPE}} static func " + body + "\\";
+
+ create_func_body.push_back("{{STRUCTNAME}}." + body + name + ", &fbb)");
+
+ if (IsScalar(field.value.type.base_type) &&
+ !IsBool(field.value.type.base_type)) {
+ std::string is_enum = IsEnum(field.value.type) ? ".rawValue" : "";
+ std::string optional_enum =
+ IsEnum(field.value.type) ? ("?" + is_enum) : "";
+ code_ +=
+ "{{VALUETYPE}}" + builder_string + "fbb.add(element: {{VALUENAME}}\\";
+
+ code_ += field.IsOptional() ? (optional_enum + "\\")
+ : (is_enum + ", def: {{CONSTANT}}\\");
+
+ code_ += ", at: {{TABLEOFFSET}}.{{OFFSET}}.p) }";
+
+ auto default_value =
+ IsEnum(field.value.type)
+ ? (field.IsOptional() ? "nil" : GenEnumDefaultValue(field))
+ : field.value.constant;
+ create_func_header.push_back(
+ "" + name + ": " + nullable_type + " = " +
+ (field.IsOptional() ? "nil" : default_value));
+ return;
+ }
+
+ if (IsBool(field.value.type.base_type)) {
+ std::string default_value =
+ "0" == field.value.constant ? "false" : "true";
+
+ code_.SetValue("CONSTANT", default_value);
+ code_.SetValue("VALUETYPE", field.IsOptional() ? "Bool?" : "Bool");
+ code_ += "{{VALUETYPE}}" + builder_string +
+ "fbb.add(element: {{VALUENAME}},\\";
+ code_ += field.IsOptional() ? "\\" : " def: {{CONSTANT}},";
+ code_ += " at: {{TABLEOFFSET}}.{{OFFSET}}.p) }";
+ create_func_header.push_back(
+ name + ": " + nullable_type + " = " +
+ (field.IsOptional() ? "nil" : default_value));
+ return;
+ }
+
+ if (IsStruct(field.value.type)) {
+ auto create_struct =
+ "guard let {{VALUENAME}} = {{VALUENAME}} else { return };"
+ " fbb.create(struct: {{VALUENAME}}, position: "
+ "{{TABLEOFFSET}}.{{OFFSET}}.p) }";
+ code_ += type + "?" + builder_string + create_struct;
+ /// Optional hard coded since structs are always optional
+ create_func_header.push_back(name + ": " + type + "? = nil");
+ return;
+ }
+
+ auto camel_case_name =
+ MakeCamel(name, false) +
+ (IsVector(field.value.type) || IsArray(field.value.type)
+ ? "VectorOffset"
+ : "Offset");
+ create_func_header.push_back(camel_case_name + " " + name + ": " +
+ "Offset = Offset()");
+ auto reader_type =
+ IsStruct(field.value.type) && field.value.type.struct_def->fixed
+ ? "structOffset: {{TABLEOFFSET}}.{{OFFSET}}.p) }"
+ : "offset: {{VALUENAME}}, at: {{TABLEOFFSET}}.{{OFFSET}}.p) }";
+ code_ += "Offset" + builder_string + "fbb.add(" + reader_type;
+
+ auto vectortype = field.value.type.VectorType();
+
+ if ((vectortype.base_type == BASE_TYPE_STRUCT &&
+ field.value.type.struct_def->fixed) &&
+ (IsVector(field.value.type) || IsArray(field.value.type))) {
+ auto field_name = NameWrappedInNameSpace(*vectortype.struct_def);
+ code_ += "public static func startVectorOf" + MakeCamel(name, true) +
+ "(_ size: Int, in builder: inout "
+ "FlatBufferBuilder) {";
+ Indent();
+ code_ += "builder.startVector(size * MemoryLayout<" + field_name +
+ ">.size, elementSize: MemoryLayout<" + field_name +
+ ">.alignment)";
+ Outdent();
+ code_ += "}";
+ }
+ }
+
+ void GenTableReader(const StructDef &struct_def) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ GenTableReaderFields(field);
+ }
+ }
+
+ void GenTableReaderFields(const FieldDef &field) {
+ auto offset = NumToString(field.value.offset);
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ code_.SetValue("VALUETYPE", type);
+ code_.SetValue("OFFSET", name);
+ code_.SetValue("CONSTANT", field.value.constant);
+ std::string def_Val = field.IsDefault() ? "{{CONSTANT}}" : "nil";
+ std::string optional = field.IsOptional() ? "?" : "";
+ auto const_string = "return o == 0 ? " + def_Val + " : ";
+ GenComment(field.doc_comment);
+ if (IsScalar(field.value.type.base_type) && !IsEnum(field.value.type) &&
+ !IsBool(field.value.type.base_type)) {
+ code_ += GenReaderMainBody(optional) + GenOffset() + const_string +
+ GenReader("VALUETYPE", "o") + " }";
+ if (parser_.opts.mutable_buffer) code_ += GenMutate("o", GenOffset());
+ return;
+ }
+
+ if (IsBool(field.value.type.base_type)) {
+ std::string default_value =
+ "0" == field.value.constant ? "false" : "true";
+ code_.SetValue("CONSTANT", default_value);
+ code_.SetValue("VALUETYPE", "Bool");
+ code_ += GenReaderMainBody(optional) + "\\";
+ code_.SetValue("VALUETYPE", "Byte");
+ code_ += GenOffset() + "return o == 0 ? {{CONSTANT}} : 0 != " +
+ GenReader("VALUETYPE", "o") + " }";
+ if (parser_.opts.mutable_buffer) code_ += GenMutate("o", GenOffset());
+ return;
+ }
+
+ if (IsEnum(field.value.type)) {
+ auto default_value =
+ field.IsOptional() ? "nil" : GenEnumDefaultValue(field);
+ code_.SetValue("BASEVALUE", GenTypeBasic(field.value.type, false));
+ code_ += GenReaderMainBody(optional) + "\\";
+ code_ += GenOffset() + "return o == 0 ? " + default_value + " : " +
+ GenEnumConstructor("o") + "?? " + default_value + " }";
+ if (parser_.opts.mutable_buffer && !IsUnion(field.value.type))
+ code_ += GenMutate("o", GenOffset(), true);
+ return;
+ }
+
+ std::string is_required = field.IsRequired() ? "!" : "?";
+ auto required_reader = field.IsRequired() ? "return " : const_string;
+
+ if (IsStruct(field.value.type) && field.value.type.struct_def->fixed) {
+ code_.SetValue("VALUETYPE", GenType(field.value.type));
+ code_.SetValue("CONSTANT", "nil");
+ code_ += GenReaderMainBody(is_required) + GenOffset() + required_reader +
+ "{{ACCESS}}.readBuffer(of: {{VALUETYPE}}.self, at: o) }";
+ code_.SetValue("VALUENAME", "mutable" + MakeCamel(name));
+ code_.SetValue("VALUETYPE", GenType(field.value.type) + Mutable());
+ code_.SetValue("CONSTANT", "nil");
+ code_ += GenReaderMainBody(is_required) + GenOffset() + required_reader +
+ GenConstructor("o + {{ACCESS}}.postion");
+ return;
+ }
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT:
+ code_.SetValue("VALUETYPE", GenType(field.value.type));
+ code_.SetValue("CONSTANT", "nil");
+ code_ += GenReaderMainBody(is_required) + GenOffset() +
+ required_reader +
+ GenConstructor(GenIndirect("o + {{ACCESS}}.postion"));
+ break;
+
+ case BASE_TYPE_STRING: {
+ auto default_string = "\"" + field.value.constant + "\"";
+ code_.SetValue("VALUETYPE", GenType(field.value.type));
+ code_.SetValue("CONSTANT", field.IsDefault() ? default_string : "nil");
+ code_ += GenReaderMainBody(is_required) + GenOffset() +
+ required_reader + "{{ACCESS}}.string(at: o) }";
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}SegmentArray: [UInt8]" +
+ is_required +
+ " { return "
+ "{{ACCESS}}.getVector(at: {{TABLEOFFSET}}.{{OFFSET}}.v) }";
+ break;
+ }
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case BASE_TYPE_VECTOR: GenTableReaderVectorFields(field); break;
+ case BASE_TYPE_UNION:
+ code_.SetValue("CONSTANT", "nil");
+ code_ +=
+ "{{ACCESS_TYPE}} func {{VALUENAME}}<T: "
+ "FlatbuffersInitializable>(type: "
+ "T.Type) -> T" +
+ is_required + " { " + GenOffset() + required_reader +
+ "{{ACCESS}}.union(o) }";
+ break;
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+
+ void GenTableReaderVectorFields(const FieldDef &field) {
+ std::string const_string = "return o == 0 ? {{CONSTANT}} : ";
+ auto vectortype = field.value.type.VectorType();
+ code_.SetValue("SIZE", NumToString(InlineSize(vectortype)));
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}Count: Int32 { " + GenOffset() +
+ "return o == 0 ? 0 : {{ACCESS}}.vector(count: o) }";
+ code_.SetValue("CONSTANT",
+ IsScalar(vectortype.base_type) == true ? "0" : "nil");
+ auto nullable = IsScalar(vectortype.base_type) == true ? "" : "?";
+ nullable = IsEnum(vectortype) == true ? "?" : nullable;
+
+ if (vectortype.base_type != BASE_TYPE_UNION) {
+ code_ += GenArrayMainBody(nullable) + GenOffset() + "\\";
+ } else {
+ code_ +=
+ "{{ACCESS_TYPE}} func {{VALUENAME}}<T: FlatbuffersInitializable>(at "
+ "index: "
+ "Int32, type: T.Type) -> T? { " +
+ GenOffset() + "\\";
+ }
+
+ if (IsBool(vectortype.base_type)) {
+ code_.SetValue("CONSTANT", field.value.offset == 0 ? "false" : "true");
+ code_.SetValue("VALUETYPE", "Bool");
+ }
+
+ if (!IsEnum(vectortype)) code_ += const_string + "\\";
+
+ if (IsScalar(vectortype.base_type) && !IsEnum(vectortype) &&
+ !IsBool(field.value.type.base_type)) {
+ code_ +=
+ "{{ACCESS}}.directRead(of: {{VALUETYPE}}.self, offset: "
+ "{{ACCESS}}.vector(at: o) + index * {{SIZE}}) }";
+ code_ +=
+ "{{ACCESS_TYPE}} var {{VALUENAME}}: [{{VALUETYPE}}] { return "
+ "{{ACCESS}}.getVector(at: {{TABLEOFFSET}}.{{OFFSET}}.v) ?? [] }";
+ if (parser_.opts.mutable_buffer) code_ += GenMutateArray();
+ return;
+ }
+
+ if (vectortype.base_type == BASE_TYPE_STRUCT &&
+ field.value.type.struct_def->fixed) {
+ code_ +=
+ "{{ACCESS}}.directRead(of: {{VALUETYPE}}.self, offset: "
+ "{{ACCESS}}.vector(at: o) + index * {{SIZE}}) }";
+ code_.SetValue("VALUENAME", "mutable" + MakeCamel(Name(field)));
+ code_.SetValue("VALUETYPE", GenType(field.value.type) + Mutable());
+ code_ += GenArrayMainBody(nullable) + GenOffset() + const_string +
+ GenConstructor("{{ACCESS}}.vector(at: o) + index * {{SIZE}}");
+
+ return;
+ }
+
+ if (IsString(vectortype)) {
+ code_ +=
+ "{{ACCESS}}.directString(at: {{ACCESS}}.vector(at: o) + "
+ "index * {{SIZE}}) }";
+ return;
+ }
+
+ if (IsEnum(vectortype)) {
+ code_.SetValue("BASEVALUE", GenTypeBasic(vectortype, false));
+ code_ += "return o == 0 ? {{VALUETYPE}}" + GenEnumDefaultValue(field) +
+ " : {{VALUETYPE}}(rawValue: {{ACCESS}}.directRead(of: "
+ "{{BASEVALUE}}.self, offset: {{ACCESS}}.vector(at: o) + "
+ "index * {{SIZE}})) }";
+ return;
+ }
+ if (vectortype.base_type == BASE_TYPE_UNION) {
+ code_ +=
+ "{{ACCESS}}.directUnion({{ACCESS}}.vector(at: o) + "
+ "index * {{SIZE}}) }";
+ return;
+ }
+
+ if (vectortype.base_type == BASE_TYPE_STRUCT &&
+ !field.value.type.struct_def->fixed) {
+ code_ += GenConstructor(
+ "{{ACCESS}}.indirect({{ACCESS}}.vector(at: o) + index * "
+ "{{SIZE}})");
+ auto &sd = *field.value.type.struct_def;
+ auto &fields = sd.fields.vec;
+ for (auto kit = fields.begin(); kit != fields.end(); ++kit) {
+ auto &key_field = **kit;
+ if (key_field.key) {
+ GenByKeyFunctions(key_field);
+ break;
+ }
+ }
+ }
+ }
+
+ void GenByKeyFunctions(const FieldDef &key_field) {
+ code_.SetValue("TYPE", GenType(key_field.value.type));
+ code_ +=
+ "{{ACCESS_TYPE}} func {{VALUENAME}}By(key: {{TYPE}}) -> {{VALUETYPE}}? "
+ "{ \\";
+ code_ += GenOffset() +
+ "return o == 0 ? nil : {{VALUETYPE}}.lookupByKey(vector: "
+ "{{ACCESS}}.vector(at: o), key: key, fbb: {{ACCESS}}.bb) }";
+ }
+
+ void GenEnum(const EnumDef &enum_def) {
+ if (enum_def.generated) return;
+ auto is_private_access = enum_def.attributes.Lookup("private");
+ code_.SetValue("ACCESS_TYPE", is_private_access ? "internal" : "public");
+ code_.SetValue("ENUM_NAME", NameWrappedInNameSpace(enum_def));
+ code_.SetValue("BASE_TYPE", GenTypeBasic(enum_def.underlying_type, false));
+ GenComment(enum_def.doc_comment);
+ code_ += "{{ACCESS_TYPE}} enum {{ENUM_NAME}}: {{BASE_TYPE}}, Enum {";
+ Indent();
+ code_ += "{{ACCESS_TYPE}} typealias T = {{BASE_TYPE}}";
+ code_ +=
+ "{{ACCESS_TYPE}} static var byteSize: Int { return "
+ "MemoryLayout<{{BASE_TYPE}}>.size "
+ "}";
+ code_ +=
+ "{{ACCESS_TYPE}} var value: {{BASE_TYPE}} { return self.rawValue }";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ const auto &ev = **it;
+ auto name = Name(ev);
+ code_.SetValue("KEY", name);
+ code_.SetValue("VALUE", enum_def.ToString(ev));
+ GenComment(ev.doc_comment);
+ code_ += "case {{KEY}} = {{VALUE}}";
+ }
+ code_ += "\n";
+ AddMinOrMaxEnumValue(Name(*enum_def.MaxValue()), "max");
+ AddMinOrMaxEnumValue(Name(*enum_def.MinValue()), "min");
+ Outdent();
+ code_ += "}\n";
+ if (parser_.opts.generate_object_based_api && enum_def.is_union) {
+ code_ += "{{ACCESS_TYPE}} struct {{ENUM_NAME}}Union {";
+ Indent();
+ code_ += "{{ACCESS_TYPE}} var type: {{ENUM_NAME}}";
+ code_ += "{{ACCESS_TYPE}} var value: NativeObject?";
+ code_ +=
+ "{{ACCESS_TYPE}} init(_ v: NativeObject?, type: {{ENUM_NAME}}) {";
+ Indent();
+ code_ += "self.type = type";
+ code_ += "self.value = v";
+ Outdent();
+ code_ += "}";
+ code_ +=
+ "{{ACCESS_TYPE}} func pack(builder: inout FlatBufferBuilder) -> "
+ "Offset {";
+ Indent();
+ BuildUnionEnumSwitchCaseWritter(enum_def);
+ Outdent();
+ code_ += "}";
+ Outdent();
+ code_ += "}";
+ }
+ }
+
+ // MARK: - Object API
+
+ void GenerateObjectAPIExtensionHeader(std::string name) {
+ code_ += "\n";
+ code_ += "{{ACCESS_TYPE}} mutating func unpack() -> " + name + " {";
+ Indent();
+ code_ += "return " + name + "(&self)";
+ Outdent();
+ code_ += "}";
+ code_ +=
+ "{{ACCESS_TYPE}} static func pack(_ builder: inout FlatBufferBuilder, "
+ "obj: "
+ "inout " +
+ name + "?) -> Offset {";
+ Indent();
+ code_ += "guard var obj = obj else { return Offset() }";
+ code_ += "return pack(&builder, obj: &obj)";
+ Outdent();
+ code_ += "}";
+ code_ += "";
+ code_ +=
+ "{{ACCESS_TYPE}} static func pack(_ builder: inout FlatBufferBuilder, "
+ "obj: "
+ "inout " +
+ name + ") -> Offset {";
+ Indent();
+ }
+
+ void GenerateObjectAPIStructConstructor(const StructDef &struct_def) {
+ code_ +=
+ "{{ACCESS_TYPE}} init(_ _t: inout {{STRUCTNAME}}" + Mutable() + ") {";
+ Indent();
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ if (IsStruct(field.value.type)) {
+ code_ += "var _v{{VALUENAME}} = _t.{{VALUENAME}}";
+ code_ += "_{{VALUENAME}} = _v{{VALUENAME}}.unpack()";
+ continue;
+ }
+ std::string is_enum = IsEnum(field.value.type) ? ".value" : "";
+ code_ += "_{{VALUENAME}} = _t.{{VALUENAME}}" + is_enum;
+ }
+ Outdent();
+ code_ += "}\n";
+ }
+
+ void GenObjectAPI(const StructDef &struct_def) {
+ code_ += "{{ACCESS_TYPE}} class " + ObjectAPIName("{{STRUCTNAME}}") +
+ ": NativeObject {\n";
+ std::vector<std::string> buffer_constructor;
+ std::vector<std::string> base_constructor;
+ Indent();
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ BuildObjectAPIConstructorBody(field, struct_def.fixed, buffer_constructor,
+ base_constructor);
+ }
+ code_ += "";
+ BuildObjectConstructor(buffer_constructor,
+ "_ _t: inout " + NameWrappedInNameSpace(struct_def));
+ BuildObjectConstructor(base_constructor);
+ if (!struct_def.fixed)
+ code_ +=
+ "{{ACCESS_TYPE}} func serialize() -> ByteBuffer { return "
+ "serialize(type: "
+ "{{STRUCTNAME}}.self) }\n";
+ Outdent();
+ code_ += "}";
+ }
+
+ void GenerateObjectAPITableExtension(const StructDef &struct_def) {
+ GenerateObjectAPIExtensionHeader(ObjectAPIName("{{STRUCTNAME}}"));
+ std::vector<std::string> unpack_body;
+ std::string builder = ", &builder)";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ std::string check_if_vector =
+ (IsVector(field.value.type) || IsArray(field.value.type))
+ ? "VectorOf("
+ : "(";
+ std::string body = "add" + check_if_vector + name + ": ";
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH();
+ case BASE_TYPE_VECTOR: {
+ GenerateVectorObjectAPITableExtension(field, name, type);
+ unpack_body.push_back("{{STRUCTNAME}}." + body + "__" + name +
+ builder);
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ code_ += "let __" + name + " = obj." + name +
+ "?.pack(builder: &builder) ?? Offset()";
+ unpack_body.push_back("if let o = obj." + name + "?.type {");
+ unpack_body.push_back(" {{STRUCTNAME}}.add(" + name + "Type: o" +
+ builder);
+ unpack_body.push_back(" {{STRUCTNAME}}." + body + "__" + name +
+ builder);
+ unpack_body.push_back("}\n");
+ break;
+ }
+ case BASE_TYPE_STRUCT: {
+ if (field.value.type.struct_def &&
+ field.value.type.struct_def->fixed) {
+ // This is a Struct (IsStruct), not a table. We create
+ // a native swift object in this case.
+ std::string code;
+ GenerateStructArgs(*field.value.type.struct_def, &code, "", "",
+ "$0", true);
+ code = code.substr(0, code.size() - 2);
+ unpack_body.push_back("{{STRUCTNAME}}." + body + "obj." + name +
+ builder);
+ } else {
+ code_ += "let __" + name + " = " + type +
+ ".pack(&builder, obj: &obj." + name + ")";
+ unpack_body.push_back("{{STRUCTNAME}}." + body + "__" + name +
+ builder);
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ unpack_body.push_back("{{STRUCTNAME}}." + body + "__" + name +
+ builder);
+ if (field.IsRequired()) {
+ code_ +=
+ "let __" + name + " = builder.create(string: obj." + name + ")";
+ } else {
+ BuildingOptionalObjects(name, "builder.create(string: s)");
+ }
+ break;
+ }
+ case BASE_TYPE_UTYPE: break;
+ default:
+ unpack_body.push_back("{{STRUCTNAME}}." + body + "obj." + name +
+ builder);
+ }
+ }
+ code_ += "let __root = {{STRUCTNAME}}.start{{SHORT_STRUCTNAME}}(&builder)";
+ for (auto it = unpack_body.begin(); it < unpack_body.end(); it++)
+ code_ += *it;
+ code_ +=
+ "return {{STRUCTNAME}}.end{{SHORT_STRUCTNAME}}(&builder, start: "
+ "__root)";
+ Outdent();
+ code_ += "}";
+ }
+
+ void GenerateVectorObjectAPITableExtension(const FieldDef &field,
+ const std::string &name,
+ const std::string &type) {
+ auto vectortype = field.value.type.VectorType();
+ switch (vectortype.base_type) {
+ case BASE_TYPE_UNION: {
+ code_ += "var __" + name + "__: [Offset] = []";
+ code_ += "for i in obj." + name + " {";
+ Indent();
+ code_ += "guard let off = i?.pack(builder: &builder) else { continue }";
+ code_ += "__" + name + "__.append(off)";
+ Outdent();
+ code_ += "}";
+ code_ += "let __" + name + " = builder.createVector(ofOffsets: __" +
+ name + "__)";
+ code_ += "let __" + name + "Type = builder.createVector(obj." + name +
+ ".compactMap { $0?.type })";
+ break;
+ }
+ case BASE_TYPE_UTYPE: break;
+ case BASE_TYPE_STRUCT: {
+ if (field.value.type.struct_def &&
+ !field.value.type.struct_def->fixed) {
+ code_ += "var __" + name + "__: [Offset] = []";
+ code_ += "for var i in obj." + name + " {";
+ Indent();
+ code_ +=
+ "__" + name + "__.append(" + type + ".pack(&builder, obj: &i))";
+ Outdent();
+ code_ += "}";
+ code_ += "let __" + name + " = builder.createVector(ofOffsets: __" +
+ name + "__)";
+ } else {
+ code_ += "{{STRUCTNAME}}.startVectorOf" + MakeCamel(name, true) +
+ "(obj." + name + ".count, in: &builder)";
+ std::string code;
+ GenerateStructArgs(*field.value.type.struct_def, &code, "", "", "_o",
+ true);
+ code = code.substr(0, code.size() - 2);
+ code_ += "for i in obj." + name + " {";
+ Indent();
+ code_ += "guard let _o = i else { continue }";
+ code_ += "builder.create(struct: _o)";
+ Outdent();
+ code_ += "}";
+ code_ += "let __" + name + " = builder.endVector(len: obj." + name +
+ ".count)";
+ }
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ code_ += "let __" + name + " = builder.createVector(ofStrings: obj." +
+ name + ".compactMap({ $0 }) )";
+ break;
+ }
+ default: {
+ code_ += "let __" + name + " = builder.createVector(obj." + name + ")";
+ break;
+ }
+ }
+ }
+
+ void BuildingOptionalObjects(const std::string &name,
+ const std::string &body_front) {
+ code_ += "let __" + name + ": Offset";
+ code_ += "if let s = obj." + name + " {";
+ Indent();
+ code_ += "__" + name + " = " + body_front;
+ Outdent();
+ code_ += "} else {";
+ Indent();
+ code_ += "__" + name + " = Offset()";
+ Outdent();
+ code_ += "}";
+ code_ += "";
+ }
+
+ void BuildObjectConstructor(const std::vector<std::string> &body,
+ const std::string &header = "") {
+ code_.SetValue("HEADER", header);
+ code_ += "{{ACCESS_TYPE}} init({{HEADER}}) {";
+ Indent();
+ for (auto it = body.begin(); it < body.end(); ++it) code_ += *it;
+ Outdent();
+ code_ += "}\n";
+ }
+
+ void BuildObjectAPIConstructorBody(
+ const FieldDef &field, bool is_fixed,
+ std::vector<std::string> &buffer_constructor,
+ std::vector<std::string> &base_constructor) {
+ auto name = Name(field);
+ auto type = GenType(field.value.type);
+ code_.SetValue("VALUENAME", name);
+ code_.SetValue("VALUETYPE", type);
+ std::string is_required = field.IsRequired() ? "" : "?";
+
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ type = GenType(field.value.type, true);
+ code_.SetValue("VALUETYPE", type);
+ auto optional =
+ (field.value.type.struct_def && field.value.type.struct_def->fixed);
+ std::string question_mark =
+ (field.IsRequired() || (optional && is_fixed) ? "" : "?");
+
+ code_ +=
+ "{{ACCESS_TYPE}} var {{VALUENAME}}: {{VALUETYPE}}" + question_mark;
+ base_constructor.push_back("" + name + " = " + type + "()");
+
+ if (field.value.type.struct_def->fixed) {
+ buffer_constructor.push_back("" + name + " = _t." + name);
+ } else {
+ buffer_constructor.push_back("var __" + name + " = _t." + name);
+ buffer_constructor.push_back(
+ "" + name + " = __" + name +
+ (field.IsRequired() ? "!" : question_mark) + ".unpack()");
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH();
+ case BASE_TYPE_VECTOR: {
+ BuildObjectAPIConstructorBodyVectors(field, name, buffer_constructor,
+ base_constructor, " ");
+ break;
+ }
+ case BASE_TYPE_STRING: {
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: String" + is_required;
+ buffer_constructor.push_back(name + " = _t." + name);
+
+ if (field.IsRequired()) {
+ std::string default_value =
+ field.IsDefault() ? field.value.constant : "";
+ base_constructor.push_back(name + " = \"" + default_value + "\"");
+ break;
+ }
+ if (field.IsDefault() && !field.IsRequired()) {
+ std::string value = field.IsDefault() ? field.value.constant : "nil";
+ base_constructor.push_back(name + " = \"" + value + "\"");
+ }
+ break;
+ }
+ case BASE_TYPE_UTYPE: break;
+ case BASE_TYPE_UNION: {
+ BuildUnionEnumSwitchCase(*field.value.type.enum_def, name,
+ buffer_constructor);
+ break;
+ }
+ default: {
+ buffer_constructor.push_back(name + " = _t." + name);
+ std::string nullable = field.IsOptional() ? "?" : "";
+ if (IsScalar(field.value.type.base_type) &&
+ !IsBool(field.value.type.base_type) && !IsEnum(field.value.type)) {
+ code_ +=
+ "{{ACCESS_TYPE}} var {{VALUENAME}}: {{VALUETYPE}}" + nullable;
+ if (!field.IsOptional())
+ base_constructor.push_back(name + " = " + field.value.constant);
+ break;
+ }
+
+ if (IsEnum(field.value.type)) {
+ auto default_value = IsEnum(field.value.type)
+ ? GenEnumDefaultValue(field)
+ : field.value.constant;
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: {{VALUETYPE}}";
+ base_constructor.push_back(name + " = " + default_value);
+ break;
+ }
+
+ if (IsBool(field.value.type.base_type)) {
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: Bool" + nullable;
+ std::string default_value =
+ "0" == field.value.constant ? "false" : "true";
+ if (!field.IsOptional())
+ base_constructor.push_back(name + " = " + default_value);
+ }
+ }
+ }
+ }
+
+ void BuildObjectAPIConstructorBodyVectors(
+ const FieldDef &field, const std::string &name,
+ std::vector<std::string> &buffer_constructor,
+ std::vector<std::string> &base_constructor,
+ const std::string &indentation) {
+ auto vectortype = field.value.type.VectorType();
+
+ if (vectortype.base_type != BASE_TYPE_UTYPE) {
+ buffer_constructor.push_back(name + " = []");
+ buffer_constructor.push_back("for index in 0..<_t." + name + "Count {");
+ base_constructor.push_back(name + " = []");
+ }
+
+ switch (vectortype.base_type) {
+ case BASE_TYPE_STRUCT: {
+ code_.SetValue("VALUETYPE", GenType(vectortype, true));
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: [{{VALUETYPE}}?]";
+ if (!vectortype.struct_def->fixed) {
+ buffer_constructor.push_back(indentation + "var __v_ = _t." + name +
+ "(at: index)");
+ buffer_constructor.push_back(indentation + name +
+ ".append(__v_?.unpack())");
+ } else {
+ buffer_constructor.push_back(indentation + name + ".append(_t." +
+ name + "(at: index))");
+ }
+ break;
+ }
+ case BASE_TYPE_ARRAY: FLATBUFFERS_FALLTHROUGH();
+ case BASE_TYPE_VECTOR: {
+ break;
+ }
+ case BASE_TYPE_UNION: {
+ BuildUnionEnumSwitchCase(*field.value.type.enum_def, name,
+ buffer_constructor, indentation, true);
+ break;
+ }
+ case BASE_TYPE_UTYPE: break;
+ default: {
+ code_.SetValue(
+ "VALUETYPE",
+ (IsString(vectortype) ? "String?" : GenType(vectortype)));
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: [{{VALUETYPE}}]";
+
+ if (IsEnum(vectortype) && vectortype.base_type != BASE_TYPE_UNION) {
+ auto default_value = IsEnum(field.value.type)
+ ? GenEnumDefaultValue(field)
+ : field.value.constant;
+ buffer_constructor.push_back(indentation + name + ".append(_t." +
+ name + "(at: index)!)");
+ break;
+ }
+ buffer_constructor.push_back(indentation + name + ".append(_t." + name +
+ "(at: index))");
+ break;
+ }
+ }
+ if (vectortype.base_type != BASE_TYPE_UTYPE)
+ buffer_constructor.push_back("}");
+ }
+
+ void BuildUnionEnumSwitchCaseWritter(const EnumDef &ev) {
+ auto field_name = Name(ev);
+ code_.SetValue("VALUETYPE", field_name);
+ code_ += "switch type {";
+ for (auto it = ev.Vals().begin(); it < ev.Vals().end(); ++it) {
+ auto field = **it;
+ auto ev_name = Name(field);
+ auto type = GenType(field.union_type);
+ auto is_struct = IsStruct(field.union_type) ? type + Mutable() : type;
+ if (field.union_type.base_type == BASE_TYPE_NONE) { continue; }
+ code_ += "case ." + ev_name + ":";
+ Indent();
+ code_ += "var __obj = value as? " + GenType(field.union_type, true);
+ code_ += "return " + is_struct + ".pack(&builder, obj: &__obj)";
+ Outdent();
+ }
+ code_ += "default: return Offset()";
+ code_ += "}";
+ }
+
+ void BuildUnionEnumSwitchCase(const EnumDef &ev, const std::string &name,
+ std::vector<std::string> &buffer_constructor,
+ const std::string &indentation = "",
+ const bool is_vector = false) {
+ auto field_name = NameWrappedInNameSpace(ev);
+ code_.SetValue("VALUETYPE", field_name);
+ code_ += "{{ACCESS_TYPE}} var {{VALUENAME}}: \\";
+ code_ += is_vector ? "[{{VALUETYPE}}Union?]" : "{{VALUETYPE}}Union?";
+
+ auto vector_reader = is_vector ? "(at: index" : "";
+ buffer_constructor.push_back(indentation + "switch _t." + name + "Type" +
+ vector_reader + (is_vector ? ")" : "") + " {");
+
+ for (auto it = ev.Vals().begin(); it < ev.Vals().end(); ++it) {
+ auto field = **it;
+ auto ev_name = Name(field);
+ if (field.union_type.base_type == BASE_TYPE_NONE) { continue; }
+ auto type = IsStruct(field.union_type)
+ ? GenType(field.union_type) + Mutable()
+ : GenType(field.union_type);
+ buffer_constructor.push_back(indentation + "case ." + ev_name + ":");
+ buffer_constructor.push_back(
+ indentation + " var _v = _t." + name + (is_vector ? "" : "(") +
+ vector_reader + (is_vector ? ", " : "") + "type: " + type + ".self)");
+ auto constructor =
+ field_name + "Union(_v?.unpack(), type: ." + ev_name + ")";
+ buffer_constructor.push_back(
+ indentation + " " + name +
+ (is_vector ? ".append(" + constructor + ")" : " = " + constructor));
+ }
+ buffer_constructor.push_back(indentation + "default: break");
+ buffer_constructor.push_back(indentation + "}");
+ }
+
+ void AddMinOrMaxEnumValue(const std::string &str, const std::string &type) {
+ auto current_value = str;
+ code_.SetValue(type, current_value);
+ code_ += "{{ACCESS_TYPE}} static var " + type +
+ ": {{ENUM_NAME}} { return .{{" + type + "}} }";
+ }
+
+ void GenLookup(const FieldDef &key_field) {
+ code_.SetValue("OFFSET", NumToString(key_field.value.offset));
+ std::string offset_reader =
+ "Table.offset(Int32(fbb.capacity) - tableOffset, vOffset: {{OFFSET}}, "
+ "fbb: fbb)";
+
+ code_.SetValue("TYPE", GenType(key_field.value.type));
+ code_ +=
+ "fileprivate static func lookupByKey(vector: Int32, key: {{TYPE}}, "
+ "fbb: "
+ "ByteBuffer) -> {{VALUENAME}}? {";
+ Indent();
+ if (IsString(key_field.value.type))
+ code_ += "let key = key.utf8.map { $0 }";
+ code_ += "var span = fbb.read(def: Int32.self, position: Int(vector - 4))";
+ code_ += "var start: Int32 = 0";
+ code_ += "while span != 0 {";
+ Indent();
+ code_ += "var middle = span / 2";
+ code_ +=
+ "let tableOffset = Table.indirect(vector + 4 * (start + middle), fbb)";
+ if (IsString(key_field.value.type)) {
+ code_ += "let comp = Table.compare(" + offset_reader + ", key, fbb: fbb)";
+ } else {
+ code_ += "let comp = fbb.read(def: {{TYPE}}.self, position: Int(" +
+ offset_reader + "))";
+ }
+
+ code_ += "if comp > 0 {";
+ Indent();
+ code_ += "span = middle";
+ Outdent();
+ code_ += "} else if comp < 0 {";
+ Indent();
+ code_ += "middle += 1";
+ code_ += "start += middle";
+ code_ += "span -= middle";
+ Outdent();
+ code_ += "} else {";
+ Indent();
+ code_ += "return {{VALUENAME}}(fbb, o: tableOffset)";
+ Outdent();
+ code_ += "}";
+ Outdent();
+ code_ += "}";
+ code_ += "return nil";
+ Outdent();
+ code_ += "}";
+ }
+
+ inline void GenPadding(const FieldDef &field, int *id) {
+ if (field.padding) {
+ for (int i = 0; i < 4; i++) {
+ if (static_cast<int>(field.padding) & (1 << i)) {
+ auto bits = (1 << i) * 8;
+ code_ += "private let padding" + NumToString((*id)++) + "__: UInt" +
+ NumToString(bits) + " = 0";
+ }
+ }
+ FLATBUFFERS_ASSERT(!(field.padding & ~0xF));
+ }
+ }
+
+ void GenComment(const std::vector<std::string> &dc) {
+ if (dc.begin() == dc.end()) {
+ // Don't output empty comment blocks with 0 lines of comment content.
+ return;
+ }
+ for (auto it = dc.begin(); it != dc.end(); ++it) { code_ += "/// " + *it; }
+ }
+
+ std::string GenOffset() {
+ return "let o = {{ACCESS}}.offset({{TABLEOFFSET}}.{{OFFSET}}.v); ";
+ }
+
+ std::string GenReaderMainBody(const std::string &optional = "") {
+ return "{{ACCESS_TYPE}} var {{VALUENAME}}: {{VALUETYPE}}" + optional +
+ " { ";
+ }
+
+ std::string GenReader(const std::string &type,
+ const std::string &at = "{{OFFSET}}") {
+ return "{{ACCESS}}.readBuffer(of: {{" + type + "}}.self, at: " + at + ")";
+ }
+
+ std::string GenConstructor(const std::string &offset) {
+ return "{{VALUETYPE}}({{ACCESS}}.bb, o: " + offset + ") }";
+ }
+
+ std::string GenMutate(const std::string &offset,
+ const std::string &get_offset, bool isRaw = false) {
+ return "@discardableResult {{ACCESS_TYPE}} func mutate({{VALUENAME}}: "
+ "{{VALUETYPE}}) -> Bool {" +
+ get_offset + " return {{ACCESS}}.mutate({{VALUENAME}}" +
+ (isRaw ? ".rawValue" : "") + ", index: " + offset + ") }";
+ }
+
+ std::string GenMutateArray() {
+ return "{{ACCESS_TYPE}} func mutate({{VALUENAME}}: {{VALUETYPE}}, at "
+ "index: "
+ "Int32) -> Bool { " +
+ GenOffset() +
+ "return {{ACCESS}}.directMutate({{VALUENAME}}, index: "
+ "{{ACCESS}}.vector(at: o) + index * {{SIZE}}) }";
+ }
+
+ std::string GenEnumDefaultValue(const FieldDef &field) {
+ auto &value = field.value;
+ FLATBUFFERS_ASSERT(value.type.enum_def);
+ auto &enum_def = *value.type.enum_def;
+ // Vector of enum defaults are always "[]" which never works.
+ const std::string constant = IsVector(value.type) ? "0" : value.constant;
+ auto enum_val = enum_def.FindByValue(constant);
+ std::string name;
+ if (enum_val) {
+ name = Name(*enum_val);
+ } else {
+ const auto &ev = **enum_def.Vals().begin();
+ name = Name(ev);
+ }
+ return "." + name;
+ }
+
+ std::string GenEnumConstructor(const std::string &at) {
+ return "{{VALUETYPE}}(rawValue: " + GenReader("BASEVALUE", at) + ") ";
+ }
+
+ std::string ValidateFunc() {
+ return "static func validateVersion() { FlatBuffersVersion_2_0_0() }";
+ }
+
+ std::string GenType(const Type &type,
+ const bool should_consider_suffix = false) const {
+ return IsScalar(type.base_type)
+ ? GenTypeBasic(type)
+ : (IsArray(type) ? GenType(type.VectorType())
+ : GenTypePointer(type, should_consider_suffix));
+ }
+
+ std::string GenTypePointer(const Type &type,
+ const bool should_consider_suffix) const {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_VECTOR: return GenType(type.VectorType());
+ case BASE_TYPE_STRUCT: {
+ auto &struct_ = *type.struct_def;
+ if (should_consider_suffix && !struct_.fixed) {
+ return WrapInNameSpace(struct_.defined_namespace,
+ ObjectAPIName(Name(struct_)));
+ }
+ return WrapInNameSpace(struct_.defined_namespace, Name(struct_));
+ }
+ case BASE_TYPE_UNION:
+ default: return "FlatbuffersInitializable";
+ }
+ }
+
+ std::string GenTypeBasic(const Type &type) const {
+ return GenTypeBasic(type, true);
+ }
+
+ std::string ObjectAPIName(const std::string &name) const {
+ return parser_.opts.object_prefix + name + parser_.opts.object_suffix;
+ }
+
+ void Indent() { code_.IncrementIdentLevel(); }
+
+ void Outdent() { code_.DecrementIdentLevel(); }
+
+ std::string NameWrappedInNameSpace(const EnumDef &enum_def) const {
+ return WrapInNameSpace(enum_def.defined_namespace, Name(enum_def));
+ }
+
+ std::string NameWrappedInNameSpace(const StructDef &struct_def) const {
+ return WrapInNameSpace(struct_def.defined_namespace, Name(struct_def));
+ }
+
+ std::string GenTypeBasic(const Type &type, bool can_override) const {
+ // clang-format off
+ static const char * const swift_type[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, \
+ CTYPE, JTYPE, GTYPE, NTYPE, PTYPE, RTYPE, KTYPE, STYPE) \
+ #STYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ // clang-format on
+ if (can_override) {
+ if (type.enum_def) return NameWrappedInNameSpace(*type.enum_def);
+ if (type.base_type == BASE_TYPE_BOOL) return "Bool";
+ }
+ return swift_type[static_cast<int>(type.base_type)];
+ }
+
+ std::string EscapeKeyword(const std::string &name) const {
+ return keywords_.find(name) == keywords_.end() ? name : name + "_";
+ }
+
+ std::string Mutable() const { return "_Mutable"; }
+
+ std::string Name(const EnumVal &ev) const {
+ auto name = ev.name;
+ if (isupper(name.front())) {
+ std::transform(name.begin(), name.end(), name.begin(), CharToLower);
+ }
+ return EscapeKeyword(MakeCamel(name, false));
+ }
+
+ std::string Name(const Definition &def) const {
+ return EscapeKeyword(MakeCamel(def.name, false));
+ }
+};
+} // namespace swift
+bool GenerateSwift(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ swift::SwiftGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_text.cpp b/contrib/libs/flatbuffers/src/idl_gen_text.cpp
new file mode 100644
index 0000000000..903c41ecdb
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_text.cpp
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/flexbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+struct PrintScalarTag {};
+struct PrintPointerTag {};
+template<typename T> struct PrintTag { typedef PrintScalarTag type; };
+template<> struct PrintTag<const void *> { typedef PrintPointerTag type; };
+
+struct JsonPrinter {
+ // If indentation is less than 0, that indicates we don't want any newlines
+ // either.
+ void AddNewLine() {
+ if (opts.indent_step >= 0) text += '\n';
+ }
+
+ void AddIndent(int ident) { text.append(ident, ' '); }
+
+ int Indent() const { return std::max(opts.indent_step, 0); }
+
+ // Output an identifier with or without quotes depending on strictness.
+ void OutputIdentifier(const std::string &name) {
+ if (opts.strict_json) text += '\"';
+ text += name;
+ if (opts.strict_json) text += '\"';
+ }
+
+ // Print (and its template specialization below for pointers) generate text
+ // for a single FlatBuffer value into JSON format.
+ // The general case for scalars:
+ template<typename T>
+ bool PrintScalar(T val, const Type &type, int /*indent*/) {
+ if (IsBool(type.base_type)) {
+ text += val != 0 ? "true" : "false";
+ return true; // done
+ }
+
+ if (opts.output_enum_identifiers && type.enum_def) {
+ const auto &enum_def = *type.enum_def;
+ if (auto ev = enum_def.ReverseLookup(static_cast<int64_t>(val))) {
+ text += '\"';
+ text += ev->name;
+ text += '\"';
+ return true; // done
+ } else if (val && enum_def.attributes.Lookup("bit_flags")) {
+ const auto entry_len = text.length();
+ const auto u64 = static_cast<uint64_t>(val);
+ uint64_t mask = 0;
+ text += '\"';
+ for (auto it = enum_def.Vals().begin(), e = enum_def.Vals().end();
+ it != e; ++it) {
+ auto f = (*it)->GetAsUInt64();
+ if (f & u64) {
+ mask |= f;
+ text += (*it)->name;
+ text += ' ';
+ }
+ }
+ // Don't slice if (u64 != mask)
+ if (mask && (u64 == mask)) {
+ text[text.length() - 1] = '\"';
+ return true; // done
+ }
+ text.resize(entry_len); // restore
+ }
+ // print as numeric value
+ }
+
+ text += NumToString(val);
+ return true;
+ }
+
+ void AddComma() {
+ if (!opts.protobuf_ascii_alike) text += ',';
+ }
+
+ // Print a vector or an array of JSON values, comma seperated, wrapped in
+ // "[]".
+ template<typename Container>
+ bool PrintContainer(PrintScalarTag, const Container &c, size_t size,
+ const Type &type, int indent, const uint8_t *) {
+ const auto elem_indent = indent + Indent();
+ text += '[';
+ AddNewLine();
+ for (uoffset_t i = 0; i < size; i++) {
+ if (i) {
+ AddComma();
+ AddNewLine();
+ }
+ AddIndent(elem_indent);
+ if (!PrintScalar(c[i], type, elem_indent)) { return false; }
+ }
+ AddNewLine();
+ AddIndent(indent);
+ text += ']';
+ return true;
+ }
+
+ // Print a vector or an array of JSON values, comma seperated, wrapped in
+ // "[]".
+ template<typename Container>
+ bool PrintContainer(PrintPointerTag, const Container &c, size_t size,
+ const Type &type, int indent, const uint8_t *prev_val) {
+ const auto is_struct = IsStruct(type);
+ const auto elem_indent = indent + Indent();
+ text += '[';
+ AddNewLine();
+ for (uoffset_t i = 0; i < size; i++) {
+ if (i) {
+ AddComma();
+ AddNewLine();
+ }
+ AddIndent(elem_indent);
+ auto ptr = is_struct ? reinterpret_cast<const void *>(
+ c.Data() + type.struct_def->bytesize * i)
+ : c[i];
+ if (!PrintOffset(ptr, type, elem_indent, prev_val,
+ static_cast<soffset_t>(i))) {
+ return false;
+ }
+ }
+ AddNewLine();
+ AddIndent(indent);
+ text += ']';
+ return true;
+ }
+
+ template<typename T>
+ bool PrintVector(const void *val, const Type &type, int indent,
+ const uint8_t *prev_val) {
+ typedef Vector<T> Container;
+ typedef typename PrintTag<typename Container::return_type>::type tag;
+ auto &vec = *reinterpret_cast<const Container *>(val);
+ return PrintContainer<Container>(tag(), vec, vec.size(), type, indent,
+ prev_val);
+ }
+
+ // Print an array a sequence of JSON values, comma separated, wrapped in "[]".
+ template<typename T>
+ bool PrintArray(const void *val, size_t size, const Type &type, int indent) {
+ typedef Array<T, 0xFFFF> Container;
+ typedef typename PrintTag<typename Container::return_type>::type tag;
+ auto &arr = *reinterpret_cast<const Container *>(val);
+ return PrintContainer<Container>(tag(), arr, size, type, indent, nullptr);
+ }
+
+ bool PrintOffset(const void *val, const Type &type, int indent,
+ const uint8_t *prev_val, soffset_t vector_index) {
+ switch (type.base_type) {
+ case BASE_TYPE_UNION: {
+ // If this assert hits, you have an corrupt buffer, a union type field
+ // was not present or was out of range.
+ FLATBUFFERS_ASSERT(prev_val);
+ auto union_type_byte = *prev_val; // Always a uint8_t.
+ if (vector_index >= 0) {
+ auto type_vec = reinterpret_cast<const Vector<uint8_t> *>(
+ prev_val + ReadScalar<uoffset_t>(prev_val));
+ union_type_byte = type_vec->Get(static_cast<uoffset_t>(vector_index));
+ }
+ auto enum_val = type.enum_def->ReverseLookup(union_type_byte, true);
+ if (enum_val) {
+ return PrintOffset(val, enum_val->union_type, indent, nullptr, -1);
+ } else {
+ return false;
+ }
+ }
+ case BASE_TYPE_STRUCT:
+ return GenStruct(*type.struct_def, reinterpret_cast<const Table *>(val),
+ indent);
+ case BASE_TYPE_STRING: {
+ auto s = reinterpret_cast<const String *>(val);
+ return EscapeString(s->c_str(), s->size(), &text, opts.allow_non_utf8,
+ opts.natural_utf8);
+ }
+ case BASE_TYPE_VECTOR: {
+ const auto vec_type = type.VectorType();
+ // Call PrintVector above specifically for each element type:
+ // clang-format off
+ switch (vec_type.base_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ if (!PrintVector<CTYPE>( \
+ val, vec_type, indent, prev_val)) { \
+ return false; \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ }
+ // clang-format on
+ return true;
+ }
+ case BASE_TYPE_ARRAY: {
+ const auto vec_type = type.VectorType();
+ // Call PrintArray above specifically for each element type:
+ // clang-format off
+ switch (vec_type.base_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ if (!PrintArray<CTYPE>( \
+ val, type.fixed_length, vec_type, indent)) { \
+ return false; \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+ // Arrays of scalars or structs are only possible.
+ FLATBUFFERS_GEN_TYPES_POINTER(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ case BASE_TYPE_ARRAY: FLATBUFFERS_ASSERT(0);
+ }
+ // clang-format on
+ return true;
+ }
+ default: FLATBUFFERS_ASSERT(0); return false;
+ }
+ }
+
+ template<typename T> static T GetFieldDefault(const FieldDef &fd) {
+ T val;
+ auto check = StringToNumber(fd.value.constant.c_str(), &val);
+ (void)check;
+ FLATBUFFERS_ASSERT(check);
+ return val;
+ }
+
+ // Generate text for a scalar field.
+ template<typename T>
+ bool GenField(const FieldDef &fd, const Table *table, bool fixed,
+ int indent) {
+ return PrintScalar(
+ fixed ? reinterpret_cast<const Struct *>(table)->GetField<T>(
+ fd.value.offset)
+ : table->GetField<T>(fd.value.offset, GetFieldDefault<T>(fd)),
+ fd.value.type, indent);
+ }
+
+ // Generate text for non-scalar field.
+ bool GenFieldOffset(const FieldDef &fd, const Table *table, bool fixed,
+ int indent, const uint8_t *prev_val) {
+ const void *val = nullptr;
+ if (fixed) {
+ // The only non-scalar fields in structs are structs or arrays.
+ FLATBUFFERS_ASSERT(IsStruct(fd.value.type) || IsArray(fd.value.type));
+ val = reinterpret_cast<const Struct *>(table)->GetStruct<const void *>(
+ fd.value.offset);
+ } else if (fd.flexbuffer) {
+ auto vec = table->GetPointer<const Vector<uint8_t> *>(fd.value.offset);
+ auto root = flexbuffers::GetRoot(vec->data(), vec->size());
+ root.ToString(true, opts.strict_json, text);
+ return true;
+ } else if (fd.nested_flatbuffer) {
+ auto vec = table->GetPointer<const Vector<uint8_t> *>(fd.value.offset);
+ auto root = GetRoot<Table>(vec->data());
+ return GenStruct(*fd.nested_flatbuffer, root, indent);
+ } else {
+ val = IsStruct(fd.value.type)
+ ? table->GetStruct<const void *>(fd.value.offset)
+ : table->GetPointer<const void *>(fd.value.offset);
+ }
+ return PrintOffset(val, fd.value.type, indent, prev_val, -1);
+ }
+
+ // Generate text for a struct or table, values separated by commas, indented,
+ // and bracketed by "{}"
+ bool GenStruct(const StructDef &struct_def, const Table *table, int indent) {
+ text += '{';
+ int fieldout = 0;
+ const uint8_t *prev_val = nullptr;
+ const auto elem_indent = indent + Indent();
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ FieldDef &fd = **it;
+ auto is_present = struct_def.fixed || table->CheckField(fd.value.offset);
+ auto output_anyway = (opts.output_default_scalars_in_json || fd.key) &&
+ IsScalar(fd.value.type.base_type) && !fd.deprecated;
+ if (is_present || output_anyway) {
+ if (fieldout++) { AddComma(); }
+ AddNewLine();
+ AddIndent(elem_indent);
+ OutputIdentifier(fd.name);
+ if (!opts.protobuf_ascii_alike ||
+ (fd.value.type.base_type != BASE_TYPE_STRUCT &&
+ fd.value.type.base_type != BASE_TYPE_VECTOR))
+ text += ':';
+ text += ' ';
+ // clang-format off
+ switch (fd.value.type.base_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ if (!GenField<CTYPE>(fd, table, struct_def.fixed, elem_indent)) { \
+ return false; \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ // Generate drop-thru case statements for all pointer types:
+ #define FLATBUFFERS_TD(ENUM, ...) \
+ case BASE_TYPE_ ## ENUM:
+ FLATBUFFERS_GEN_TYPES_POINTER(FLATBUFFERS_TD)
+ FLATBUFFERS_GEN_TYPE_ARRAY(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ if (!GenFieldOffset(fd, table, struct_def.fixed, elem_indent, prev_val)) {
+ return false;
+ }
+ break;
+ }
+ // clang-format on
+ // Track prev val for use with union types.
+ if (struct_def.fixed) {
+ prev_val = reinterpret_cast<const uint8_t *>(table) + fd.value.offset;
+ } else {
+ prev_val = table->GetAddressOf(fd.value.offset);
+ }
+ }
+ }
+ AddNewLine();
+ AddIndent(indent);
+ text += '}';
+ return true;
+ }
+
+ JsonPrinter(const Parser &parser, std::string &dest)
+ : opts(parser.opts), text(dest) {
+ text.reserve(1024); // Reduce amount of inevitable reallocs.
+ }
+
+ const IDLOptions &opts;
+ std::string &text;
+};
+
+static bool GenerateTextImpl(const Parser &parser, const Table *table,
+ const StructDef &struct_def, std::string *_text) {
+ JsonPrinter printer(parser, *_text);
+ if (!printer.GenStruct(struct_def, table, 0)) { return false; }
+ printer.AddNewLine();
+ return true;
+}
+
+// Generate a text representation of a flatbuffer in JSON format.
+bool GenerateTextFromTable(const Parser &parser, const void *table,
+ const std::string &table_name, std::string *_text) {
+ auto struct_def = parser.LookupStruct(table_name);
+ if (struct_def == nullptr) { return false; }
+ auto root = static_cast<const Table *>(table);
+ return GenerateTextImpl(parser, root, *struct_def, _text);
+}
+
+// Generate a text representation of a flatbuffer in JSON format.
+bool GenerateText(const Parser &parser, const void *flatbuffer,
+ std::string *_text) {
+ FLATBUFFERS_ASSERT(parser.root_struct_def_); // call SetRootType()
+ auto root = parser.opts.size_prefixed ? GetSizePrefixedRoot<Table>(flatbuffer)
+ : GetRoot<Table>(flatbuffer);
+ return GenerateTextImpl(parser, root, *parser.root_struct_def_, _text);
+}
+
+static std::string TextFileName(const std::string &path,
+ const std::string &file_name) {
+ return path + file_name + ".json";
+}
+
+bool GenerateTextFile(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ if (parser.opts.use_flexbuffers) {
+ std::string json;
+ parser.flex_root_.ToString(true, parser.opts.strict_json, json);
+ return flatbuffers::SaveFile(TextFileName(path, file_name).c_str(),
+ json.c_str(), json.size(), true);
+ }
+ if (!parser.builder_.GetSize() || !parser.root_struct_def_) return true;
+ std::string text;
+ if (!GenerateText(parser, parser.builder_.GetBufferPointer(), &text)) {
+ return false;
+ }
+ return flatbuffers::SaveFile(TextFileName(path, file_name).c_str(), text,
+ false);
+}
+
+std::string TextMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ if (!parser.builder_.GetSize() || !parser.root_struct_def_) return "";
+ std::string filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ std::string make_rule = TextFileName(path, filebase) + ": " + file_name;
+ auto included_files =
+ parser.GetIncludedFilesRecursive(parser.root_struct_def_->file);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_gen_ts.cpp b/contrib/libs/flatbuffers/src/idl_gen_ts.cpp
new file mode 100644
index 0000000000..53e088fe13
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_gen_ts.cpp
@@ -0,0 +1,1583 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// independent from idl_parser, since this code is not needed for most clients
+#include <algorithm>
+#include <cassert>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "flatbuffers/code_generators.h"
+#include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+struct ImportDefinition {
+ std::string name;
+ std::string statement;
+ const Definition *dependent;
+ const Definition *dependency;
+};
+
+enum AnnotationType { kParam = 0, kType = 1, kReturns = 2 };
+
+namespace ts {
+// Iterate through all definitions we haven't generate code for (enums, structs,
+// and tables) and output them to a single file.
+class TsGenerator : public BaseGenerator {
+ public:
+ typedef std::map<std::string, ImportDefinition> import_set;
+
+ TsGenerator(const Parser &parser, const std::string &path,
+ const std::string &file_name)
+ : BaseGenerator(parser, path, file_name, "", ".", "ts") {}
+ bool generate() {
+ generateEnums();
+ generateStructs();
+ return true;
+ }
+
+ // Save out the generated code for a single class while adding
+ // declaration boilerplate.
+ bool SaveType(const Definition &definition, const std::string &classcode,
+ import_set &imports, import_set &bare_imports) const {
+ if (!classcode.length()) return true;
+
+ std::string code =
+ "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n";
+
+ for (auto it = bare_imports.begin(); it != bare_imports.end(); it++)
+ code += it->second.statement + "\n";
+ if (!bare_imports.empty()) code += "\n";
+
+ for (auto it = imports.begin(); it != imports.end(); it++)
+ if (it->second.dependency != &definition) // do not import itself
+ code += it->second.statement + "\n";
+ if (!imports.empty()) code += "\n\n";
+
+ code += classcode;
+ auto filename = NamespaceDir(*definition.defined_namespace, true) +
+ ToDasherizedCase(definition.name) + ".ts";
+ return SaveFile(filename.c_str(), code, false);
+ }
+
+ private:
+ // Generate code for all enums.
+ void generateEnums() {
+ for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end();
+ ++it) {
+ import_set bare_imports;
+ import_set imports;
+ std::string enumcode;
+ auto &enum_def = **it;
+ GenEnum(enum_def, &enumcode, imports, false);
+ GenEnum(enum_def, &enumcode, imports, true);
+ SaveType(enum_def, enumcode, imports, bare_imports);
+ }
+ }
+
+ // Generate code for all structs.
+ void generateStructs() {
+ for (auto it = parser_.structs_.vec.begin();
+ it != parser_.structs_.vec.end(); ++it) {
+ import_set bare_imports;
+ import_set imports;
+ AddImport(bare_imports, "* as flatbuffers", "flatbuffers");
+ auto &struct_def = **it;
+ std::string declcode;
+ GenStruct(parser_, struct_def, &declcode, imports);
+ SaveType(struct_def, declcode, imports, bare_imports);
+ }
+ }
+
+ // Generate a documentation comment, if available.
+ static void GenDocComment(const std::vector<std::string> &dc,
+ std::string *code_ptr,
+ const char *indent = nullptr) {
+ if (dc.empty()) {
+ // Don't output empty comment blocks with 0 lines of comment content.
+ return;
+ }
+
+ std::string &code = *code_ptr;
+ if (indent) code += indent;
+ code += "/**\n";
+ for (auto it = dc.begin(); it != dc.end(); ++it) {
+ if (indent) code += indent;
+ code += " *" + *it + "\n";
+ }
+ if (indent) code += indent;
+ code += " */\n";
+ }
+
+ static void GenDocComment(std::string *code_ptr) {
+ GenDocComment(std::vector<std::string>(), code_ptr);
+ }
+
+ // Generate an enum declaration and an enum string lookup table.
+ void GenEnum(EnumDef &enum_def, std::string *code_ptr, import_set &imports,
+ bool reverse) {
+ if (enum_def.generated) return;
+ if (reverse) return; // FIXME.
+ std::string &code = *code_ptr;
+ GenDocComment(enum_def.doc_comment, code_ptr);
+ std::string ns = GetNameSpace(enum_def);
+ std::string enum_def_name = enum_def.name + (reverse ? "Name" : "");
+ code += "export enum " + enum_def.name + "{\n";
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end(); ++it) {
+ auto &ev = **it;
+ if (!ev.doc_comment.empty()) {
+ if (it != enum_def.Vals().begin()) { code += '\n'; }
+ GenDocComment(ev.doc_comment, code_ptr, " ");
+ }
+
+ // Generate mapping between EnumName: EnumValue(int)
+ if (reverse) {
+ code += " '" + enum_def.ToString(ev) + "'";
+ code += " = ";
+ code += "'" + ev.name + "'";
+ } else {
+ code += " " + ev.name;
+ code += " = ";
+ code += enum_def.ToString(ev);
+ }
+
+ code += (it + 1) != enum_def.Vals().end() ? ",\n" : "\n";
+ }
+ code += "}";
+
+ if (enum_def.is_union) {
+ code += GenUnionConvFunc(enum_def.underlying_type, imports);
+ }
+
+ code += "\n\n";
+ }
+
+ static std::string GenType(const Type &type) {
+ switch (type.base_type) {
+ case BASE_TYPE_BOOL:
+ case BASE_TYPE_CHAR: return "Int8";
+ case BASE_TYPE_UTYPE:
+ case BASE_TYPE_UCHAR: return "Uint8";
+ case BASE_TYPE_SHORT: return "Int16";
+ case BASE_TYPE_USHORT: return "Uint16";
+ case BASE_TYPE_INT: return "Int32";
+ case BASE_TYPE_UINT: return "Uint32";
+ case BASE_TYPE_LONG: return "Int64";
+ case BASE_TYPE_ULONG: return "Uint64";
+ case BASE_TYPE_FLOAT: return "Float32";
+ case BASE_TYPE_DOUBLE: return "Float64";
+ case BASE_TYPE_STRING: return "String";
+ case BASE_TYPE_VECTOR: return GenType(type.VectorType());
+ case BASE_TYPE_STRUCT: return type.struct_def->name;
+ default: return "flatbuffers.Table";
+ }
+ }
+
+ std::string GenGetter(const Type &type, const std::string &arguments) {
+ switch (type.base_type) {
+ case BASE_TYPE_STRING: return GenBBAccess() + ".__string" + arguments;
+ case BASE_TYPE_STRUCT: return GenBBAccess() + ".__struct" + arguments;
+ case BASE_TYPE_UNION:
+ if (!UnionHasStringType(*type.enum_def)) {
+ return GenBBAccess() + ".__union" + arguments;
+ }
+ return GenBBAccess() + ".__union_with_string" + arguments;
+ case BASE_TYPE_VECTOR: return GenGetter(type.VectorType(), arguments);
+ default: {
+ auto getter =
+ GenBBAccess() + ".read" + MakeCamel(GenType(type)) + arguments;
+ if (type.base_type == BASE_TYPE_BOOL) { getter = "!!" + getter; }
+ return getter;
+ }
+ }
+ }
+
+ std::string GenBBAccess() const { return "this.bb!"; }
+
+ std::string GenDefaultValue(const FieldDef &field, const std::string &context,
+ import_set &imports) {
+ if (field.IsScalarOptional()) { return "null"; }
+
+ const auto &value = field.value;
+ if (value.type.enum_def && value.type.base_type != BASE_TYPE_UNION &&
+ value.type.base_type != BASE_TYPE_VECTOR) {
+ if (auto val = value.type.enum_def->FindByValue(value.constant)) {
+ return AddImport(imports, *value.type.enum_def, *value.type.enum_def) +
+ "." + val->name;
+ } else {
+ return value.constant;
+ }
+ }
+
+ switch (value.type.base_type) {
+ case BASE_TYPE_BOOL: return value.constant == "0" ? "false" : "true";
+
+ case BASE_TYPE_STRING:
+ case BASE_TYPE_UNION:
+ case BASE_TYPE_STRUCT: {
+ return "null";
+ }
+
+ case BASE_TYPE_VECTOR: return "[]";
+
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG: {
+ int64_t constant = StringToInt(value.constant.c_str());
+ std::string createLong = context + ".createLong";
+ return createLong + "(" + NumToString(static_cast<int32_t>(constant)) +
+ ", " + NumToString(static_cast<int32_t>(constant >> 32)) + ")";
+ }
+
+ default: return value.constant;
+ }
+ }
+
+ std::string GenTypeName(import_set &imports, const Definition &owner,
+ const Type &type, bool input,
+ bool allowNull = false) {
+ if (!input) {
+ if (IsString(type) || type.base_type == BASE_TYPE_STRUCT) {
+ std::string name;
+ if (IsString(type)) {
+ name = "string|Uint8Array";
+ } else {
+ name = AddImport(imports, owner, *type.struct_def);
+ }
+ return allowNull ? (name + "|null") : name;
+ }
+ }
+
+ switch (type.base_type) {
+ case BASE_TYPE_BOOL: return allowNull ? "boolean|null" : "boolean";
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG:
+ return allowNull ? "flatbuffers.Long|null" : "flatbuffers.Long";
+ default:
+ if (IsScalar(type.base_type)) {
+ if (type.enum_def) {
+ const auto enum_name = AddImport(imports, owner, *type.enum_def);
+ return allowNull ? (enum_name + "|null") : enum_name;
+ }
+ return allowNull ? "number|null" : "number";
+ }
+ return "flatbuffers.Offset";
+ }
+ }
+
+ // Returns the method name for use with add/put calls.
+ static std::string GenWriteMethod(const Type &type) {
+ // Forward to signed versions since unsigned versions don't exist
+ switch (type.base_type) {
+ case BASE_TYPE_UTYPE:
+ case BASE_TYPE_UCHAR: return GenWriteMethod(Type(BASE_TYPE_CHAR));
+ case BASE_TYPE_USHORT: return GenWriteMethod(Type(BASE_TYPE_SHORT));
+ case BASE_TYPE_UINT: return GenWriteMethod(Type(BASE_TYPE_INT));
+ case BASE_TYPE_ULONG: return GenWriteMethod(Type(BASE_TYPE_LONG));
+ default: break;
+ }
+
+ return IsScalar(type.base_type) ? MakeCamel(GenType(type))
+ : (IsStruct(type) ? "Struct" : "Offset");
+ }
+
+ template<typename T> static std::string MaybeAdd(T value) {
+ return value != 0 ? " + " + NumToString(value) : "";
+ }
+
+ template<typename T> static std::string MaybeScale(T value) {
+ return value != 1 ? " * " + NumToString(value) : "";
+ }
+
+ void GenStructArgs(import_set &imports, const StructDef &struct_def,
+ std::string *arguments, const std::string &nameprefix) {
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ GenStructArgs(imports, *field.value.type.struct_def, arguments,
+ nameprefix + field.name + "_");
+ } else {
+ *arguments +=
+ ", " + nameprefix + field.name + ": " +
+ GenTypeName(imports, field, field.value.type, true, field.IsOptional());
+ }
+ }
+ }
+
+ static void GenStructBody(const StructDef &struct_def, std::string *body,
+ const std::string &nameprefix) {
+ *body += " builder.prep(";
+ *body += NumToString(struct_def.minalign) + ", ";
+ *body += NumToString(struct_def.bytesize) + ");\n";
+
+ for (auto it = struct_def.fields.vec.rbegin();
+ it != struct_def.fields.vec.rend(); ++it) {
+ auto &field = **it;
+ if (field.padding) {
+ *body += " builder.pad(" + NumToString(field.padding) + ");\n";
+ }
+ if (IsStruct(field.value.type)) {
+ // Generate arguments for a struct inside a struct. To ensure names
+ // don't clash, and to make it obvious these arguments are constructing
+ // a nested struct, prefix the name with the field name.
+ GenStructBody(*field.value.type.struct_def, body,
+ nameprefix + field.name + "_");
+ } else {
+ *body += " builder.write" + GenWriteMethod(field.value.type) + "(";
+ if (field.value.type.base_type == BASE_TYPE_BOOL) { *body += "+"; }
+ *body += nameprefix + field.name + ");\n";
+ }
+ }
+ }
+
+ std::string GenerateNewExpression(const std::string &object_name) {
+ return "new " + object_name + "()";
+ }
+
+ void GenerateRootAccessor(StructDef &struct_def, std::string *code_ptr,
+ std::string &code, const std::string &object_name,
+ bool size_prefixed) {
+ if (!struct_def.fixed) {
+ GenDocComment(code_ptr);
+ std::string sizePrefixed("SizePrefixed");
+ code += "static get" + (size_prefixed ? sizePrefixed : "") + "Root" +
+ GetPrefixedName(struct_def, "As");
+ code += "(bb:flatbuffers.ByteBuffer, obj?:" + object_name +
+ "):" + object_name + " {\n";
+ if (size_prefixed) {
+ code +=
+ " bb.setPosition(bb.position() + "
+ "flatbuffers.SIZE_PREFIX_LENGTH);\n";
+ }
+ code += " return (obj || " + GenerateNewExpression(object_name);
+ code += ").__init(bb.readInt32(bb.position()) + bb.position(), bb);\n";
+ code += "}\n\n";
+ }
+ }
+
+ void GenerateFinisher(StructDef &struct_def, std::string *code_ptr,
+ std::string &code, bool size_prefixed) {
+ if (parser_.root_struct_def_ == &struct_def) {
+ std::string sizePrefixed("SizePrefixed");
+ GenDocComment(code_ptr);
+
+ code += "static finish" + (size_prefixed ? sizePrefixed : "") +
+ GetPrefixedName(struct_def) + "Buffer";
+ code += "(builder:flatbuffers.Builder, offset:flatbuffers.Offset) {\n";
+ code += " builder.finish(offset";
+ if (!parser_.file_identifier_.empty()) {
+ code += ", '" + parser_.file_identifier_ + "'";
+ }
+ if (size_prefixed) {
+ if (parser_.file_identifier_.empty()) { code += ", undefined"; }
+ code += ", true";
+ }
+ code += ");\n";
+ code += "}\n\n";
+ }
+ }
+
+ static std::string GetObjApiClassName(const StructDef &sd,
+ const IDLOptions &opts) {
+ return GetObjApiClassName(sd.name, opts);
+ }
+
+ static std::string GetObjApiClassName(const std::string &name,
+ const IDLOptions &opts) {
+ return opts.object_prefix + name + opts.object_suffix;
+ }
+
+ bool UnionHasStringType(const EnumDef &union_enum) {
+ return std::any_of(union_enum.Vals().begin(), union_enum.Vals().end(),
+ [](const EnumVal *ev) {
+ return !ev->IsZero() && IsString(ev->union_type);
+ });
+ }
+
+ std::string GenUnionGenericTypeTS(const EnumDef &union_enum) {
+ // TODO: make it work without any
+ // return std::string("T") + (UnionHasStringType(union_enum) ? "|string" :
+ // "");
+ return std::string("any") +
+ (UnionHasStringType(union_enum) ? "|string" : "");
+ }
+
+ std::string GenUnionTypeTS(const EnumDef &union_enum, import_set &imports) {
+ std::string ret;
+ std::set<std::string> type_list;
+
+ for (auto it = union_enum.Vals().begin(); it != union_enum.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ std::string type = "";
+ if (IsString(ev.union_type)) {
+ type = "string"; // no need to wrap string type in namespace
+ } else if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ type = AddImport(imports, union_enum, *ev.union_type.struct_def);
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ type_list.insert(type);
+ }
+
+ for (auto it = type_list.begin(); it != type_list.end(); ++it) {
+ ret += *it + ((std::next(it) == type_list.end()) ? "" : "|");
+ }
+
+ return ret;
+ }
+
+ std::string AddImport(import_set &imports, const Definition &dependent,
+ const StructDef &dependency) {
+ std::string ns;
+ const auto &depc_comps = dependency.defined_namespace->components;
+ for (auto it = depc_comps.begin(); it != depc_comps.end(); it++) ns += *it;
+ std::string unique_name = ns + dependency.name;
+ std::string import_name = dependency.name;
+ std::string long_import_name;
+ if (imports.find(unique_name) != imports.end())
+ return imports.find(unique_name)->second.name;
+ for (auto it = imports.begin(); it != imports.end(); it++) {
+ if (it->second.name == import_name) {
+ long_import_name = ns + import_name;
+ break;
+ }
+ }
+ std::string import_statement;
+ import_statement += "import { ";
+ if (long_import_name.empty()) {
+ import_statement += import_name;
+ if (parser_.opts.generate_object_based_api)
+ import_statement += ", " + import_name + "T";
+ } else {
+ import_statement += dependency.name + " as " + long_import_name;
+ if (parser_.opts.generate_object_based_api)
+ import_statement +=
+ ", " + dependency.name + "T as " + long_import_name + "T";
+ }
+ import_statement += " } from '";
+ std::string file_name;
+ const auto &dep_comps = dependent.defined_namespace->components;
+ for (size_t i = 0; i < dep_comps.size(); i++)
+ file_name += i == 0 ? ".." : (kPathSeparator + std::string(".."));
+ if (dep_comps.size() == 0) file_name += ".";
+ for (auto it = depc_comps.begin(); it != depc_comps.end(); it++)
+ file_name += kPathSeparator + ToDasherizedCase(*it);
+ file_name += kPathSeparator + ToDasherizedCase(dependency.name);
+ import_statement += file_name + "';";
+ ImportDefinition import;
+ import.name = long_import_name.empty() ? import_name : long_import_name;
+ import.statement = import_statement;
+ import.dependency = &dependency;
+ import.dependent = &dependent;
+ imports.insert(std::make_pair(unique_name, import));
+ return import.name;
+ }
+
+ // TODO: largely (but not identical) duplicated code from above couln't find a
+ // good way to refactor
+ std::string AddImport(import_set &imports, const Definition &dependent,
+ const EnumDef &dependency) {
+ std::string ns;
+ const auto &depc_comps = dependency.defined_namespace->components;
+ for (auto it = depc_comps.begin(); it != depc_comps.end(); it++) ns += *it;
+ std::string unique_name = ns + dependency.name;
+ std::string import_name = dependency.name;
+ std::string long_import_name;
+ if (imports.find(unique_name) != imports.end())
+ return imports.find(unique_name)->second.name;
+ for (auto it = imports.begin(); it != imports.end(); it++) {
+ if (it->second.name == import_name) {
+ long_import_name = ns + import_name;
+ break;
+ }
+ }
+ std::string import_statement;
+ import_statement += "import { ";
+ if (long_import_name.empty())
+ import_statement += import_name;
+ else
+ import_statement += dependency.name + " as " + long_import_name;
+ if (dependency.is_union) {
+ import_statement += ", unionTo" + import_name;
+ import_statement += ", unionListTo" + import_name;
+ }
+ import_statement += " } from '";
+ std::string file_name;
+ const auto &dep_comps = dependent.defined_namespace->components;
+ for (size_t i = 0; i < dep_comps.size(); i++)
+ file_name += i == 0 ? ".." : (kPathSeparator + std::string(".."));
+ if (dep_comps.size() == 0) file_name += ".";
+ for (auto it = depc_comps.begin(); it != depc_comps.end(); it++)
+ file_name += kPathSeparator + ToDasherizedCase(*it);
+ file_name += kPathSeparator + ToDasherizedCase(dependency.name);
+ import_statement += file_name + "';";
+ ImportDefinition import;
+ import.name = long_import_name.empty() ? import_name : long_import_name;
+ import.statement = import_statement;
+ import.dependency = &dependency;
+ import.dependent = &dependent;
+ imports.insert(std::make_pair(unique_name, import));
+ return import.name;
+ }
+
+ void AddImport(import_set &imports, std::string import_name,
+ std::string fileName) {
+ ImportDefinition import;
+ import.name = import_name;
+ import.statement = "import " + import_name + " from '" + fileName + "';";
+ imports.insert(std::make_pair(import_name, import));
+ }
+
+ // Generate a TS union type based on a union's enum
+ std::string GenObjApiUnionTypeTS(import_set &imports, const IDLOptions &opts,
+ const EnumDef &union_enum) {
+ std::string ret = "";
+ std::set<std::string> type_list;
+
+ for (auto it = union_enum.Vals().begin(); it != union_enum.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ std::string type = "";
+ if (IsString(ev.union_type)) {
+ type = "string"; // no need to wrap string type in namespace
+ } else if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ type = GetObjApiClassName(
+ AddImport(imports, union_enum, *ev.union_type.struct_def), opts);
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ type_list.insert(type);
+ }
+
+ size_t totalPrinted = 0;
+ for (auto it = type_list.begin(); it != type_list.end(); ++it) {
+ ++totalPrinted;
+ ret += *it + ((totalPrinted == type_list.size()) ? "" : "|");
+ }
+
+ return ret;
+ }
+
+ std::string GenUnionConvFuncName(const EnumDef &enum_def) {
+ return "unionTo" + enum_def.name;
+ }
+
+ std::string GenUnionListConvFuncName(const EnumDef &enum_def) {
+ return "unionListTo" + enum_def.name;
+ }
+
+ std::string GenUnionConvFunc(const Type &union_type, import_set &imports) {
+ if (union_type.enum_def) {
+ const auto &enum_def = *union_type.enum_def;
+
+ const auto valid_union_type = GenUnionTypeTS(enum_def, imports);
+ const auto valid_union_type_with_null = valid_union_type + "|null";
+
+ auto ret = "\n\nexport function " + GenUnionConvFuncName(enum_def) +
+ "(\n type: " + enum_def.name +
+ ",\n accessor: (obj:" + valid_union_type + ") => " +
+ valid_union_type_with_null +
+ "\n): " + valid_union_type_with_null + " {\n";
+
+ const auto enum_type = AddImport(imports, enum_def, enum_def);
+
+ const auto union_enum_loop = [&](const std::string &accessor_str) {
+ ret += " switch(" + enum_type + "[type]) {\n";
+ ret += " case 'NONE': return null; \n";
+
+ for (auto it = enum_def.Vals().begin(); it != enum_def.Vals().end();
+ ++it) {
+ const auto &ev = **it;
+ if (ev.IsZero()) { continue; }
+
+ ret += " case '" + ev.name + "': ";
+
+ if (IsString(ev.union_type)) {
+ ret += "return " + accessor_str + "'') as string;";
+ } else if (ev.union_type.base_type == BASE_TYPE_STRUCT) {
+ const auto type =
+ AddImport(imports, enum_def, *ev.union_type.struct_def);
+ ret += "return " + accessor_str + "new " + type + "())! as " +
+ type + ";";
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ ret += "\n";
+ }
+
+ ret += " default: return null;\n";
+ ret += " }\n";
+ };
+
+ union_enum_loop("accessor(");
+ ret += "}";
+
+ ret += "\n\nexport function " + GenUnionListConvFuncName(enum_def) +
+ "(\n type: " + enum_def.name +
+ ", \n accessor: (index: number, obj:" + valid_union_type +
+ ") => " + valid_union_type_with_null +
+ ", \n index: number\n): " + valid_union_type_with_null + " {\n";
+ union_enum_loop("accessor(index, ");
+ ret += "}";
+
+ return ret;
+ }
+ FLATBUFFERS_ASSERT(0);
+ return "";
+ }
+
+ // Used for generating a short function that returns the correct class
+ // based on union enum type. Assume the context is inside the non object api
+ // type
+ std::string GenUnionValTS(import_set &imports, const std::string &field_name,
+ const Type &union_type,
+ const bool is_array = false) {
+ if (union_type.enum_def) {
+ const auto &enum_def = *union_type.enum_def;
+ const auto enum_type = AddImport(imports, enum_def, enum_def);
+ const std::string union_accessor = "this." + field_name;
+
+ const auto union_has_string = UnionHasStringType(enum_def);
+ const auto field_binded_method = "this." + field_name + ".bind(this)";
+
+ std::string ret;
+
+ if (!is_array) {
+ const auto conversion_function = GenUnionConvFuncName(enum_def);
+ const auto target_enum = "this." + field_name + "Type()";
+
+ ret = "(() => {\n";
+ ret += " let temp = " + conversion_function + "(" + target_enum +
+ ", " + field_binded_method + ");\n";
+ ret += " if(temp === null) { return null; }\n";
+ ret += union_has_string
+ ? " if(typeof temp === 'string') { return temp; }\n"
+ : "";
+ ret += " return temp.unpack()\n";
+ ret += " })()";
+ } else {
+ const auto conversion_function = GenUnionListConvFuncName(enum_def);
+ const auto target_enum_accesor = "this." + field_name + "Type";
+ const auto target_enum_length = target_enum_accesor + "Length()";
+
+ ret = "(() => {\n";
+ ret += " let ret = [];\n";
+ ret += " for(let targetEnumIndex = 0; targetEnumIndex < " +
+ target_enum_length +
+ "; "
+ "++targetEnumIndex) {\n";
+ ret += " let targetEnum = " + target_enum_accesor +
+ "(targetEnumIndex);\n";
+ ret += " if(targetEnum === null || " + enum_type +
+ "[targetEnum!] === 'NONE') { "
+ "continue; }\n\n";
+ ret += " let temp = " + conversion_function + "(targetEnum, " +
+ field_binded_method + ", targetEnumIndex);\n";
+ ret += " if(temp === null) { continue; }\n";
+ ret += union_has_string ? " if(typeof temp === 'string') { "
+ "ret.push(temp); continue; }\n"
+ : "";
+ ret += " ret.push(temp.unpack());\n";
+ ret += " }\n";
+ ret += " return ret;\n";
+ ret += " })()";
+ }
+
+ return ret;
+ }
+
+ FLATBUFFERS_ASSERT(0);
+ return "";
+ }
+
+ static std::string GenNullCheckConditional(
+ const std::string &nullCheckVar, const std::string &trueVal,
+ const std::string &falseVal = "null") {
+ return "(" + nullCheckVar + " !== null ? " + trueVal + " : " + falseVal +
+ ")";
+ }
+
+ std::string GenStructMemberValueTS(const StructDef &struct_def,
+ const std::string &prefix,
+ const std::string &delimiter,
+ const bool nullCheck = true) {
+ std::string ret;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+
+ const auto curr_member_accessor =
+ prefix + "." + MakeCamel(field.name, false);
+ if (IsStruct(field.value.type)) {
+ ret += GenStructMemberValueTS(*field.value.type.struct_def,
+ curr_member_accessor, delimiter);
+ } else {
+ if (nullCheck) {
+ ret +=
+ "(" + prefix + " === null ? 0 : " + curr_member_accessor + "!)";
+ } else {
+ ret += curr_member_accessor;
+ }
+ }
+
+ if (std::next(it) != struct_def.fields.vec.end()) { ret += delimiter; }
+ }
+
+ return ret;
+ }
+
+ void GenObjApi(const Parser &parser, StructDef &struct_def,
+ std::string &obj_api_unpack_func, std::string &obj_api_class,
+ import_set &imports) {
+ const auto class_name = GetObjApiClassName(struct_def, parser.opts);
+
+ std::string unpack_func = "\nunpack(): " + class_name +
+ " {\n return new " + class_name + "(" +
+ (struct_def.fields.vec.empty() ? "" : "\n");
+ std::string unpack_to_func = "\nunpackTo(_o: " + class_name + "): void {" +
+ +(struct_def.fields.vec.empty() ? "" : "\n");
+
+ std::string constructor_func = "constructor(";
+ constructor_func += (struct_def.fields.vec.empty() ? "" : "\n");
+
+ const auto has_create =
+ struct_def.fixed || CanCreateFactoryMethod(struct_def);
+
+ std::string pack_func_prototype =
+ "\npack(builder:flatbuffers.Builder): flatbuffers.Offset {\n";
+
+ std::string pack_func_offset_decl;
+ std::string pack_func_create_call;
+
+ const auto struct_name = AddImport(imports, struct_def, struct_def);
+
+ if (has_create) {
+ pack_func_create_call = " return " + struct_name + ".create" +
+ GetPrefixedName(struct_def) + "(builder" +
+ (struct_def.fields.vec.empty() ? "" : ",\n ");
+ } else {
+ pack_func_create_call = " " + struct_name + ".start" +
+ GetPrefixedName(struct_def) + "(builder);\n";
+ }
+
+ if (struct_def.fixed) {
+ // when packing struct, nested struct's members instead of the struct's
+ // offset are used
+ pack_func_create_call +=
+ GenStructMemberValueTS(struct_def, "this", ",\n ", false) + "\n ";
+ }
+
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+
+ const auto field_name = MakeCamel(field.name, false);
+ const std::string field_binded_method =
+ "this." + field_name + ".bind(this)";
+
+ std::string field_val;
+ std::string field_type;
+ // a string that declares a variable containing the
+ // offset for things that can't be generated inline
+ // empty otw
+ std::string field_offset_decl;
+ // a string that contains values for things that can be created inline or
+ // the variable name from field_offset_decl
+ std::string field_offset_val;
+ const auto field_default_val =
+ GenDefaultValue(field, "flatbuffers", imports);
+
+ // Emit a scalar field
+ const auto is_string = IsString(field.value.type);
+ if (IsScalar(field.value.type.base_type) || is_string) {
+ const auto has_null_default = is_string || HasNullDefault(field);
+
+ field_type += GenTypeName(imports, field, field.value.type, false,
+ has_null_default);
+ field_val = "this." + field_name + "()";
+
+ if (field.value.type.base_type != BASE_TYPE_STRING) {
+ field_offset_val = "this." + field_name;
+ } else {
+ field_offset_decl = GenNullCheckConditional(
+ "this." + field_name,
+ "builder.createString(this." + field_name + "!)", "0");
+ }
+ }
+
+ // Emit an object field
+ else {
+ auto is_vector = false;
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ const auto &sd = *field.value.type.struct_def;
+ field_type += GetObjApiClassName(sd, parser.opts);
+
+ const std::string field_accessor = "this." + field_name + "()";
+ field_val = GenNullCheckConditional(field_accessor,
+ field_accessor + "!.unpack()");
+ auto packing = GenNullCheckConditional(
+ "this." + field_name, "this." + field_name + "!.pack(builder)",
+ "0");
+
+ if (sd.fixed) {
+ field_offset_val = std::move(packing);
+ } else {
+ field_offset_decl = std::move(packing);
+ }
+
+ break;
+ }
+
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ auto vectortypename =
+ GenTypeName(imports, struct_def, vectortype, false);
+ is_vector = true;
+
+ field_type = "(";
+
+ switch (vectortype.base_type) {
+ case BASE_TYPE_STRUCT: {
+ const auto &sd = *field.value.type.struct_def;
+ field_type += GetObjApiClassName(sd, parser.opts);
+ field_type += ")[]";
+
+ field_val = GenBBAccess() + ".createObjList(" +
+ field_binded_method + ", this." + field_name +
+ "Length())";
+
+ if (sd.fixed) {
+ field_offset_decl =
+ "builder.createStructOffsetList(this." + field_name +
+ ", " + AddImport(imports, struct_def, struct_def) +
+ ".start" + MakeCamel(field_name) + "Vector)";
+ } else {
+ field_offset_decl =
+ AddImport(imports, struct_def, struct_def) + ".create" +
+ MakeCamel(field_name) +
+ "Vector(builder, builder.createObjectOffsetList(" +
+ "this." + field_name + "))";
+ }
+
+ break;
+ }
+
+ case BASE_TYPE_STRING: {
+ field_type += "string)[]";
+ field_val = GenBBAccess() + ".createScalarList(" +
+ field_binded_method + ", this." + field_name +
+ "Length())";
+ field_offset_decl =
+ AddImport(imports, struct_def, struct_def) + ".create" +
+ MakeCamel(field_name) +
+ "Vector(builder, builder.createObjectOffsetList(" +
+ "this." + field_name + "))";
+ break;
+ }
+
+ case BASE_TYPE_UNION: {
+ field_type += GenObjApiUnionTypeTS(imports, parser.opts,
+ *(vectortype.enum_def));
+ field_type += ")[]";
+ field_val =
+ GenUnionValTS(imports, field_name, vectortype, true);
+
+ field_offset_decl =
+ AddImport(imports, struct_def, struct_def) + ".create" +
+ MakeCamel(field_name) +
+ "Vector(builder, builder.createObjectOffsetList(" +
+ "this." + field_name + "))";
+
+ break;
+ }
+ default: {
+ if (vectortype.enum_def) {
+ field_type += GenTypeName(imports, struct_def, vectortype,
+ false, HasNullDefault(field));
+ } else {
+ field_type += vectortypename;
+ }
+ field_type += ")[]";
+ field_val = GenBBAccess() + ".createScalarList(" +
+ field_binded_method + ", this." + field_name +
+ "Length())";
+
+ field_offset_decl = AddImport(imports, struct_def, struct_def) +
+ ".create" + MakeCamel(field_name) +
+ "Vector(builder, this." + field_name + ")";
+
+ break;
+ }
+ }
+
+ break;
+ }
+
+ case BASE_TYPE_UNION: {
+ field_type += GenObjApiUnionTypeTS(imports, parser.opts,
+ *(field.value.type.enum_def));
+
+ field_val = GenUnionValTS(imports, field_name, field.value.type);
+ field_offset_decl =
+ "builder.createObjectOffset(this." + field_name + ")";
+ break;
+ }
+
+ default: FLATBUFFERS_ASSERT(0); break;
+ }
+
+ // length 0 vector is simply empty instead of null
+ field_type += is_vector ? "" : "|null";
+ }
+
+ if (!field_offset_decl.empty()) {
+ field_offset_decl =
+ " const " + field_name + " = " + field_offset_decl + ";";
+ }
+ if (field_offset_val.empty()) { field_offset_val = field_name; }
+
+ unpack_func += " " + field_val;
+ unpack_to_func += " _o." + field_name + " = " + field_val + ";";
+
+ constructor_func += " public " + field_name + ": " + field_type + " = " +
+ field_default_val;
+
+ if (!struct_def.fixed) {
+ if (!field_offset_decl.empty()) {
+ pack_func_offset_decl += field_offset_decl + "\n";
+ }
+
+ if (has_create) {
+ pack_func_create_call += field_offset_val;
+ } else {
+ pack_func_create_call += " " + struct_name + ".add" +
+ MakeCamel(field.name) + "(builder, " +
+ field_offset_val + ");\n";
+ }
+ }
+
+ if (std::next(it) != struct_def.fields.vec.end()) {
+ constructor_func += ",\n";
+
+ if (!struct_def.fixed && has_create) {
+ pack_func_create_call += ",\n ";
+ }
+
+ unpack_func += ",\n";
+ unpack_to_func += "\n";
+ } else {
+ constructor_func += "\n";
+ if (!struct_def.fixed) {
+ pack_func_offset_decl += (pack_func_offset_decl.empty() ? "" : "\n");
+ pack_func_create_call += "\n ";
+ }
+
+ unpack_func += "\n ";
+ unpack_to_func += "\n";
+ }
+ }
+
+ constructor_func += "){}\n\n";
+
+ if (has_create) {
+ pack_func_create_call += ");";
+ } else {
+ pack_func_create_call += "return " + struct_name + ".end" +
+ GetPrefixedName(struct_def) + "(builder);";
+ }
+
+ obj_api_class = "\nexport class " +
+ GetObjApiClassName(struct_def, parser.opts) + " {\n";
+
+ obj_api_class += constructor_func;
+ obj_api_class += pack_func_prototype + pack_func_offset_decl +
+ pack_func_create_call + "\n}";
+
+ obj_api_class += "\n}\n";
+
+ unpack_func += ");\n}";
+ unpack_to_func += "}\n";
+
+ obj_api_unpack_func = unpack_func + "\n\n" + unpack_to_func;
+ }
+
+ static bool CanCreateFactoryMethod(const StructDef &struct_def) {
+ // to preserve backwards compatibility, we allow the first field to be a
+ // struct
+ return struct_def.fields.vec.size() < 2 ||
+ std::all_of(std::begin(struct_def.fields.vec) + 1,
+ std::end(struct_def.fields.vec),
+ [](const FieldDef *f) -> bool {
+ FLATBUFFERS_ASSERT(f != nullptr);
+ return f->value.type.base_type != BASE_TYPE_STRUCT;
+ });
+ }
+
+ // Generate an accessor struct with constructor for a flatbuffers struct.
+ void GenStruct(const Parser &parser, StructDef &struct_def,
+ std::string *code_ptr, import_set &imports) {
+ if (struct_def.generated) return;
+ std::string &code = *code_ptr;
+
+ std::string object_name;
+ std::string object_namespace = GetNameSpace(struct_def);
+
+ // Emit constructor
+ object_name = struct_def.name;
+ GenDocComment(struct_def.doc_comment, code_ptr);
+ code += "export class " + struct_def.name;
+ code += " {\n";
+ code += " bb: flatbuffers.ByteBuffer|null = null;\n";
+ code += " bb_pos = 0;\n";
+
+ // Generate the __init method that sets the field in a pre-existing
+ // accessor object. This is to allow object reuse.
+ code +=
+ "__init(i:number, bb:flatbuffers.ByteBuffer):" + object_name + " {\n";
+ code += " this.bb_pos = i;\n";
+ code += " this.bb = bb;\n";
+ code += " return this;\n";
+ code += "}\n\n";
+
+ // Generate special accessors for the table that when used as the root of a
+ // FlatBuffer
+ GenerateRootAccessor(struct_def, code_ptr, code, object_name, false);
+ GenerateRootAccessor(struct_def, code_ptr, code, object_name, true);
+
+ // Generate the identifier check method
+ if (!struct_def.fixed && parser_.root_struct_def_ == &struct_def &&
+ !parser_.file_identifier_.empty()) {
+ GenDocComment(code_ptr);
+ code +=
+ "static bufferHasIdentifier(bb:flatbuffers.ByteBuffer):boolean "
+ "{\n";
+ code += " return bb.__has_identifier('" + parser_.file_identifier_;
+ code += "');\n}\n\n";
+ }
+
+ // Emit field accessors
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ auto offset_prefix =
+ " const offset = " + GenBBAccess() + ".__offset(this.bb_pos, " +
+ NumToString(field.value.offset) + ");\n return offset ? ";
+
+ // Emit a scalar field
+ const auto is_string = IsString(field.value.type);
+ if (IsScalar(field.value.type.base_type) || is_string) {
+ const auto has_null_default = is_string || HasNullDefault(field);
+
+ GenDocComment(field.doc_comment, code_ptr);
+ std::string prefix = MakeCamel(field.name, false) + "(";
+ if (is_string) {
+ code += prefix + "):string|null\n";
+ code +=
+ prefix + "optionalEncoding:flatbuffers.Encoding" + "):" +
+ GenTypeName(imports, struct_def, field.value.type, false, true) +
+ "\n";
+ code += prefix + "optionalEncoding?:any";
+ } else {
+ code += prefix;
+ }
+ if (field.value.type.enum_def) {
+ code += "):" +
+ GenTypeName(imports, struct_def, field.value.type, false,
+ field.IsOptional()) +
+ " {\n";
+ } else {
+ code += "):" +
+ GenTypeName(imports, struct_def, field.value.type, false,
+ has_null_default) +
+ " {\n";
+ }
+
+ if (struct_def.fixed) {
+ code +=
+ " return " +
+ GenGetter(field.value.type,
+ "(this.bb_pos" + MaybeAdd(field.value.offset) + ")") +
+ ";\n";
+ } else {
+ std::string index = "this.bb_pos + offset";
+ if (is_string) { index += ", optionalEncoding"; }
+ code += offset_prefix +
+ GenGetter(field.value.type, "(" + index + ")") + " : " +
+ GenDefaultValue(field, GenBBAccess(), imports);
+ code += ";\n";
+ }
+ }
+
+ // Emit an object field
+ else {
+ switch (field.value.type.base_type) {
+ case BASE_TYPE_STRUCT: {
+ const auto type =
+ AddImport(imports, struct_def, *field.value.type.struct_def);
+ GenDocComment(field.doc_comment, code_ptr);
+ code += MakeCamel(field.name, false);
+ code += "(obj?:" + type + "):" + type + "|null {\n";
+
+ if (struct_def.fixed) {
+ code += " return (obj || " + GenerateNewExpression(type);
+ code += ").__init(this.bb_pos";
+ code +=
+ MaybeAdd(field.value.offset) + ", " + GenBBAccess() + ");\n";
+ } else {
+ code += offset_prefix + "(obj || " + GenerateNewExpression(type) +
+ ").__init(";
+ code += field.value.type.struct_def->fixed
+ ? "this.bb_pos + offset"
+ : GenBBAccess() + ".__indirect(this.bb_pos + offset)";
+ code += ", " + GenBBAccess() + ") : null;\n";
+ }
+
+ break;
+ }
+
+ case BASE_TYPE_VECTOR: {
+ auto vectortype = field.value.type.VectorType();
+ auto vectortypename =
+ GenTypeName(imports, struct_def, vectortype, false);
+ auto inline_size = InlineSize(vectortype);
+ auto index = GenBBAccess() +
+ ".__vector(this.bb_pos + offset) + index" +
+ MaybeScale(inline_size);
+ std::string ret_type;
+ bool is_union = false;
+ switch (vectortype.base_type) {
+ case BASE_TYPE_STRUCT: ret_type = vectortypename; break;
+ case BASE_TYPE_STRING: ret_type = vectortypename; break;
+ case BASE_TYPE_UNION:
+ ret_type = "?flatbuffers.Table";
+ is_union = true;
+ break;
+ default: ret_type = vectortypename;
+ }
+ GenDocComment(field.doc_comment, code_ptr);
+ std::string prefix = MakeCamel(field.name, false);
+ // TODO: make it work without any
+ // if (is_union) { prefix += "<T extends flatbuffers.Table>"; }
+ if (is_union) { prefix += ""; }
+ prefix += "(index: number";
+ if (is_union) {
+ const auto union_type =
+ GenUnionGenericTypeTS(*(field.value.type.enum_def));
+
+ vectortypename = union_type;
+ code += prefix + ", obj:" + union_type;
+ } else if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += prefix + ", obj?:" + vectortypename;
+ } else if (IsString(vectortype)) {
+ code += prefix + "):string\n";
+ code += prefix + ",optionalEncoding:flatbuffers.Encoding" +
+ "):" + vectortypename + "\n";
+ code += prefix + ",optionalEncoding?:any";
+ } else {
+ code += prefix;
+ }
+ code += "):" + vectortypename + "|null {\n";
+
+ if (vectortype.base_type == BASE_TYPE_STRUCT) {
+ code += offset_prefix + "(obj || " +
+ GenerateNewExpression(vectortypename);
+ code += ").__init(";
+ code += vectortype.struct_def->fixed
+ ? index
+ : GenBBAccess() + ".__indirect(" + index + ")";
+ code += ", " + GenBBAccess() + ")";
+ } else {
+ if (is_union) {
+ index = "obj, " + index;
+ } else if (IsString(vectortype)) {
+ index += ", optionalEncoding";
+ }
+ code += offset_prefix + GenGetter(vectortype, "(" + index + ")");
+ }
+ code += " : ";
+ if (field.value.type.element == BASE_TYPE_BOOL) {
+ code += "false";
+ } else if (field.value.type.element == BASE_TYPE_LONG ||
+ field.value.type.element == BASE_TYPE_ULONG) {
+ code += GenBBAccess() + ".createLong(0, 0)";
+ } else if (IsScalar(field.value.type.element)) {
+ if (field.value.type.enum_def) {
+ code += field.value.constant;
+ } else {
+ code += "0";
+ }
+ } else {
+ code += "null";
+ }
+ code += ";\n";
+ break;
+ }
+
+ case BASE_TYPE_UNION: {
+ GenDocComment(field.doc_comment, code_ptr);
+ code += MakeCamel(field.name, false);
+
+ const auto &union_enum = *(field.value.type.enum_def);
+ const auto union_type = GenUnionGenericTypeTS(union_enum);
+ code += "<T extends flatbuffers.Table>(obj:" + union_type +
+ "):" + union_type +
+ "|null "
+ "{\n";
+
+ code += offset_prefix +
+ GenGetter(field.value.type, "(obj, this.bb_pos + offset)") +
+ " : null;\n";
+ break;
+ }
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ }
+ code += "}\n\n";
+
+ // Adds the mutable scalar value to the output
+ if (IsScalar(field.value.type.base_type) && parser.opts.mutable_buffer &&
+ !IsUnion(field.value.type)) {
+ std::string type =
+ GenTypeName(imports, struct_def, field.value.type, true);
+
+ code += "mutate_" + field.name + "(value:" + type + "):boolean {\n";
+
+ if (struct_def.fixed) {
+ code += " " + GenBBAccess() + ".write" +
+ MakeCamel(GenType(field.value.type)) + "(this.bb_pos + " +
+ NumToString(field.value.offset) + ", ";
+ } else {
+ code += " const offset = " + GenBBAccess() +
+ ".__offset(this.bb_pos, " + NumToString(field.value.offset) +
+ ");\n\n";
+ code += " if (offset === 0) {\n";
+ code += " return false;\n";
+ code += " }\n\n";
+
+ // special case for bools, which are treated as uint8
+ code += " " + GenBBAccess() + ".write" +
+ MakeCamel(GenType(field.value.type)) +
+ "(this.bb_pos + offset, ";
+ if (field.value.type.base_type == BASE_TYPE_BOOL) { code += "+"; }
+ }
+
+ code += "value);\n";
+ code += " return true;\n";
+ code += "}\n\n";
+ }
+
+ // Emit vector helpers
+ if (IsVector(field.value.type)) {
+ // Emit a length helper
+ GenDocComment(code_ptr);
+ code += MakeCamel(field.name, false);
+ code += "Length():number {\n" + offset_prefix;
+
+ code +=
+ GenBBAccess() + ".__vector_len(this.bb_pos + offset) : 0;\n}\n\n";
+
+ // For scalar types, emit a typed array helper
+ auto vectorType = field.value.type.VectorType();
+ if (IsScalar(vectorType.base_type) && !IsLong(vectorType.base_type)) {
+ GenDocComment(code_ptr);
+
+ code += MakeCamel(field.name, false);
+ code += "Array():" + GenType(vectorType) + "Array|null {\n" +
+ offset_prefix;
+
+ code += "new " + GenType(vectorType) + "Array(" + GenBBAccess() +
+ ".bytes().buffer, " + GenBBAccess() +
+ ".bytes().byteOffset + " + GenBBAccess() +
+ ".__vector(this.bb_pos + offset), " + GenBBAccess() +
+ ".__vector_len(this.bb_pos + offset)) : null;\n}\n\n";
+ }
+ }
+ }
+
+ // Emit the fully qualified name
+ if (parser_.opts.generate_name_strings) {
+ GenDocComment(code_ptr);
+ code += "static getFullyQualifiedName():string {\n";
+ code += " return '" + WrapInNameSpace(struct_def) + "';\n";
+ code += "}\n\n";
+ }
+
+ // Emit the size of the struct.
+ if (struct_def.fixed) {
+ GenDocComment(code_ptr);
+ code += "static sizeOf():number {\n";
+ code += " return " + NumToString(struct_def.bytesize) + ";\n";
+ code += "}\n\n";
+ }
+
+ // Emit a factory constructor
+ if (struct_def.fixed) {
+ std::string arguments;
+ GenStructArgs(imports, struct_def, &arguments, "");
+ GenDocComment(code_ptr);
+
+ code += "static create" + GetPrefixedName(struct_def) +
+ "(builder:flatbuffers.Builder";
+ code += arguments + "):flatbuffers.Offset {\n";
+
+ GenStructBody(struct_def, &code, "");
+ code += " return builder.offset();\n}\n\n";
+ } else {
+ // Generate a method to start building a new object
+ GenDocComment(code_ptr);
+
+ code += "static start" + GetPrefixedName(struct_def) +
+ "(builder:flatbuffers.Builder) {\n";
+
+ code += " builder.startObject(" +
+ NumToString(struct_def.fields.vec.size()) + ");\n";
+ code += "}\n\n";
+
+ // Generate a set of static methods that allow table construction
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (field.deprecated) continue;
+ const auto argname = GetArgName(field);
+
+ // Generate the field insertion method
+ GenDocComment(code_ptr);
+ code += "static add" + MakeCamel(field.name);
+ code += "(builder:flatbuffers.Builder, " + argname + ":" +
+ GetArgType(imports, struct_def, field, false) + ") {\n";
+ code += " builder.addField" + GenWriteMethod(field.value.type) + "(";
+ code += NumToString(it - struct_def.fields.vec.begin()) + ", ";
+ if (field.value.type.base_type == BASE_TYPE_BOOL) { code += "+"; }
+ code += argname + ", ";
+ if (!IsScalar(field.value.type.base_type)) {
+ code += "0";
+ } else if (HasNullDefault(field)) {
+ if (IsLong(field.value.type.base_type)) {
+ code += "builder.createLong(0, 0)";
+ } else {
+ code += "0";
+ }
+ } else {
+ if (field.value.type.base_type == BASE_TYPE_BOOL) { code += "+"; }
+ code += GenDefaultValue(field, "builder", imports);
+ }
+ code += ");\n}\n\n";
+
+ if (IsVector(field.value.type)) {
+ auto vector_type = field.value.type.VectorType();
+ auto alignment = InlineAlignment(vector_type);
+ auto elem_size = InlineSize(vector_type);
+
+ // Generate a method to create a vector from a JavaScript array
+ if (!IsStruct(vector_type)) {
+ GenDocComment(code_ptr);
+
+ const std::string sig_begin =
+ "static create" + MakeCamel(field.name) +
+ "Vector(builder:flatbuffers.Builder, data:";
+ const std::string sig_end = "):flatbuffers.Offset";
+ std::string type =
+ GenTypeName(imports, struct_def, vector_type, true) + "[]";
+ if (type == "number[]") {
+ const auto &array_type = GenType(vector_type);
+ // the old type should be deprecated in the future
+ std::string type_old = "number[]|Uint8Array";
+ std::string type_new = "number[]|" + array_type + "Array";
+ if (type_old == type_new) {
+ type = type_new;
+ } else {
+ // add function overloads
+ code += sig_begin + type_new + sig_end + ";\n";
+ code +=
+ "/**\n * @deprecated This Uint8Array overload will "
+ "be removed in the future.\n */\n";
+ code += sig_begin + type_old + sig_end + ";\n";
+ type = type_new + "|Uint8Array";
+ }
+ }
+ code += sig_begin + type + sig_end + " {\n";
+ code += " builder.startVector(" + NumToString(elem_size);
+ code += ", data.length, " + NumToString(alignment) + ");\n";
+ code += " for (let i = data.length - 1; i >= 0; i--) {\n";
+ code += " builder.add" + GenWriteMethod(vector_type) + "(";
+ if (vector_type.base_type == BASE_TYPE_BOOL) { code += "+"; }
+ code += "data[i]!);\n";
+ code += " }\n";
+ code += " return builder.endVector();\n";
+ code += "}\n\n";
+ }
+
+ // Generate a method to start a vector, data to be added manually
+ // after
+ GenDocComment(code_ptr);
+
+ code += "static start" + MakeCamel(field.name);
+ code += "Vector(builder:flatbuffers.Builder, numElems:number) {\n";
+ code += " builder.startVector(" + NumToString(elem_size);
+ code += ", numElems, " + NumToString(alignment) + ");\n";
+ code += "}\n\n";
+ }
+ }
+
+ // Generate a method to stop building a new object
+ GenDocComment(code_ptr);
+
+ code += "static end" + GetPrefixedName(struct_def);
+ code += "(builder:flatbuffers.Builder):flatbuffers.Offset {\n";
+
+ code += " const offset = builder.endObject();\n";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ auto &field = **it;
+ if (!field.deprecated && field.IsRequired()) {
+ code += " builder.requiredField(offset, ";
+ code += NumToString(field.value.offset);
+ code += ") // " + field.name + "\n";
+ }
+ }
+ code += " return offset;\n";
+ code += "}\n\n";
+
+ // Generate the methods to complete buffer construction
+ GenerateFinisher(struct_def, code_ptr, code, false);
+ GenerateFinisher(struct_def, code_ptr, code, true);
+
+ // Generate a convenient CreateX function
+ if (CanCreateFactoryMethod(struct_def)) {
+ code += "static create" + GetPrefixedName(struct_def);
+ code += "(builder:flatbuffers.Builder";
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) continue;
+ code += ", " + GetArgName(field) + ":" +
+ GetArgType(imports, struct_def, field, true);
+ }
+
+ code += "):flatbuffers.Offset {\n";
+ code += " " + struct_def.name + ".start" +
+ GetPrefixedName(struct_def) + "(builder);\n";
+
+ std::string methodPrefix = struct_def.name;
+ for (auto it = struct_def.fields.vec.begin();
+ it != struct_def.fields.vec.end(); ++it) {
+ const auto &field = **it;
+ if (field.deprecated) continue;
+
+ const auto arg_name = GetArgName(field);
+
+ if (field.IsScalarOptional()) {
+ code += " if (" + arg_name + " !== null)\n ";
+ }
+
+ code += " " + methodPrefix + ".add" + MakeCamel(field.name) + "(";
+ code += "builder, " + arg_name + ");\n";
+ }
+
+ code += " return " + methodPrefix + ".end" +
+ GetPrefixedName(struct_def) + "(builder);\n";
+ code += "}\n";
+ }
+ }
+
+ if (!struct_def.fixed && parser_.services_.vec.size() != 0) {
+ auto name = GetPrefixedName(struct_def, "");
+ code += "\n";
+ code += "serialize():Uint8Array {\n";
+ code += " return this.bb!.bytes();\n";
+ code += "}\n";
+
+ code += "\n";
+ code += "static deserialize(buffer: Uint8Array):" + name + " {\n";
+ code += " return " + AddImport(imports, struct_def, struct_def) +
+ ".getRootAs" + name + "(new flatbuffers.ByteBuffer(buffer))\n";
+ code += "}\n";
+ }
+
+ if (parser_.opts.generate_object_based_api) {
+ std::string obj_api_class;
+ std::string obj_api_unpack_func;
+ GenObjApi(parser_, struct_def, obj_api_unpack_func, obj_api_class,
+ imports);
+
+ code += obj_api_unpack_func + "}\n" + obj_api_class;
+ } else {
+ code += "}\n";
+ }
+ }
+
+ static bool HasNullDefault(const FieldDef &field) {
+ return field.IsOptional() && field.value.constant == "null";
+ }
+
+ std::string GetArgType(import_set &imports, const Definition &owner,
+ const FieldDef &field, bool allowNull) {
+ return GenTypeName(imports, owner, field.value.type, true,
+ allowNull && field.IsOptional());
+ }
+
+ static std::string GetArgName(const FieldDef &field) {
+ auto argname = MakeCamel(field.name, false);
+ if (!IsScalar(field.value.type.base_type)) { argname += "Offset"; }
+
+ return argname;
+ }
+
+ std::string GetPrefixedName(const StructDef &struct_def,
+ const char *prefix = "") {
+ return prefix + struct_def.name;
+ }
+}; // namespace ts
+} // namespace ts
+
+bool GenerateTS(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ ts::TsGenerator generator(parser, path, file_name);
+ return generator.generate();
+}
+
+std::string TSMakeRule(const Parser &parser, const std::string &path,
+ const std::string &file_name) {
+ FLATBUFFERS_ASSERT(parser.opts.lang <= IDLOptions::kMAX);
+
+ std::string filebase =
+ flatbuffers::StripPath(flatbuffers::StripExtension(file_name));
+ ts::TsGenerator generator(parser, path, file_name);
+ std::string make_rule =
+ generator.GeneratedFileName(path, filebase, parser.opts) + ": ";
+
+ auto included_files = parser.GetIncludedFilesRecursive(file_name);
+ for (auto it = included_files.begin(); it != included_files.end(); ++it) {
+ make_rule += " " + *it;
+ }
+ return make_rule;
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/idl_parser.cpp b/contrib/libs/flatbuffers/src/idl_parser.cpp
new file mode 100644
index 0000000000..ad642d79a9
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/idl_parser.cpp
@@ -0,0 +1,3986 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+#include <cmath>
+#include <list>
+#include <string>
+#include <utility>
+
+#include "flatbuffers/idl.h"
+#include "flatbuffers/util.h"
+
+namespace flatbuffers {
+
+// Reflects the version at the compiling time of binary(lib/dll/so).
+const char *FLATBUFFERS_VERSION() {
+ // clang-format off
+ return
+ FLATBUFFERS_STRING(FLATBUFFERS_VERSION_MAJOR) "."
+ FLATBUFFERS_STRING(FLATBUFFERS_VERSION_MINOR) "."
+ FLATBUFFERS_STRING(FLATBUFFERS_VERSION_REVISION);
+ // clang-format on
+}
+
+const double kPi = 3.14159265358979323846;
+
+// clang-format off
+const char *const kTypeNames[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, ...) \
+ IDLTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ nullptr
+};
+
+const char kTypeSizes[] = {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ sizeof(CTYPE),
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+};
+// clang-format on
+
+// The enums in the reflection schema should match the ones we use internally.
+// Compare the last element to check if these go out of sync.
+static_assert(BASE_TYPE_UNION == static_cast<BaseType>(reflection::Union),
+ "enums don't match");
+
+// Any parsing calls have to be wrapped in this macro, which automates
+// handling of recursive error checking a bit. It will check the received
+// CheckedError object, and return straight away on error.
+#define ECHECK(call) \
+ { \
+ auto ce = (call); \
+ if (ce.Check()) return ce; \
+ }
+
+// These two functions are called hundreds of times below, so define a short
+// form:
+#define NEXT() ECHECK(Next())
+#define EXPECT(tok) ECHECK(Expect(tok))
+
+static bool ValidateUTF8(const std::string &str) {
+ const char *s = &str[0];
+ const char *const sEnd = s + str.length();
+ while (s < sEnd) {
+ if (FromUTF8(&s) < 0) { return false; }
+ }
+ return true;
+}
+
+static bool IsLowerSnakeCase(const std::string &str) {
+ for (size_t i = 0; i < str.length(); i++) {
+ char c = str[i];
+ if (!check_ascii_range(c, 'a', 'z') && !is_digit(c) && c != '_') {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Convert an underscore_based_identifier in to camelCase.
+// Also uppercases the first character if first is true.
+std::string MakeCamel(const std::string &in, bool first) {
+ std::string s;
+ for (size_t i = 0; i < in.length(); i++) {
+ if (!i && first)
+ s += CharToUpper(in[0]);
+ else if (in[i] == '_' && i + 1 < in.length())
+ s += CharToUpper(in[++i]);
+ else
+ s += in[i];
+ }
+ return s;
+}
+
+// Convert an underscore_based_identifier in to screaming snake case.
+std::string MakeScreamingCamel(const std::string &in) {
+ std::string s;
+ for (size_t i = 0; i < in.length(); i++) {
+ if (in[i] != '_')
+ s += CharToUpper(in[i]);
+ else
+ s += in[i];
+ }
+ return s;
+}
+
+void DeserializeDoc(std::vector<std::string> &doc,
+ const Vector<Offset<String>> *documentation) {
+ if (documentation == nullptr) return;
+ for (uoffset_t index = 0; index < documentation->size(); index++)
+ doc.push_back(documentation->Get(index)->str());
+}
+
+void Parser::Message(const std::string &msg) {
+ if (!error_.empty()) error_ += "\n"; // log all warnings and errors
+ error_ += file_being_parsed_.length() ? AbsolutePath(file_being_parsed_) : "";
+ // clang-format off
+
+ #ifdef _WIN32 // MSVC alike
+ error_ +=
+ "(" + NumToString(line_) + ", " + NumToString(CursorPosition()) + ")";
+ #else // gcc alike
+ if (file_being_parsed_.length()) error_ += ":";
+ error_ += NumToString(line_) + ": " + NumToString(CursorPosition());
+ #endif
+ // clang-format on
+ error_ += ": " + msg;
+}
+
+void Parser::Warning(const std::string &msg) {
+ if (!opts.no_warnings) Message("warning: " + msg);
+}
+
+CheckedError Parser::Error(const std::string &msg) {
+ Message("error: " + msg);
+ return CheckedError(true);
+}
+
+inline CheckedError NoError() { return CheckedError(false); }
+
+CheckedError Parser::RecurseError() {
+ return Error("maximum parsing depth " + NumToString(parse_depth_counter_) +
+ " reached");
+}
+
+class Parser::ParseDepthGuard {
+ public:
+ explicit ParseDepthGuard(Parser *parser_not_null)
+ : parser_(*parser_not_null), caller_depth_(parser_.parse_depth_counter_) {
+ FLATBUFFERS_ASSERT(caller_depth_ <= (FLATBUFFERS_MAX_PARSING_DEPTH) &&
+ "Check() must be called to prevent stack overflow");
+ parser_.parse_depth_counter_ += 1;
+ }
+
+ ~ParseDepthGuard() { parser_.parse_depth_counter_ -= 1; }
+
+ CheckedError Check() {
+ return caller_depth_ >= (FLATBUFFERS_MAX_PARSING_DEPTH)
+ ? parser_.RecurseError()
+ : CheckedError(false);
+ }
+
+ FLATBUFFERS_DELETE_FUNC(ParseDepthGuard(const ParseDepthGuard &));
+ FLATBUFFERS_DELETE_FUNC(ParseDepthGuard &operator=(const ParseDepthGuard &));
+
+ private:
+ Parser &parser_;
+ const int caller_depth_;
+};
+
+template<typename T> std::string TypeToIntervalString() {
+ return "[" + NumToString((flatbuffers::numeric_limits<T>::lowest)()) + "; " +
+ NumToString((flatbuffers::numeric_limits<T>::max)()) + "]";
+}
+
+// atot: template version of atoi/atof: convert a string to an instance of T.
+template<typename T>
+bool atot_scalar(const char *s, T *val, bool_constant<false>) {
+ return StringToNumber(s, val);
+}
+
+template<typename T>
+bool atot_scalar(const char *s, T *val, bool_constant<true>) {
+ // Normalize NaN parsed from fbs or json to unsigned NaN.
+ if (false == StringToNumber(s, val)) return false;
+ *val = (*val != *val) ? std::fabs(*val) : *val;
+ return true;
+}
+
+template<typename T> CheckedError atot(const char *s, Parser &parser, T *val) {
+ auto done = atot_scalar(s, val, bool_constant<is_floating_point<T>::value>());
+ if (done) return NoError();
+ if (0 == *val)
+ return parser.Error("invalid number: \"" + std::string(s) + "\"");
+ else
+ return parser.Error("invalid number: \"" + std::string(s) + "\"" +
+ ", constant does not fit " + TypeToIntervalString<T>());
+}
+template<>
+inline CheckedError atot<Offset<void>>(const char *s, Parser &parser,
+ Offset<void> *val) {
+ (void)parser;
+ *val = Offset<void>(atoi(s));
+ return NoError();
+}
+
+std::string Namespace::GetFullyQualifiedName(const std::string &name,
+ size_t max_components) const {
+ // Early exit if we don't have a defined namespace.
+ if (components.empty() || !max_components) { return name; }
+ std::string stream_str;
+ for (size_t i = 0; i < std::min(components.size(), max_components); i++) {
+ stream_str += components[i];
+ stream_str += '.';
+ }
+ if (!stream_str.empty()) stream_str.pop_back();
+ if (name.length()) {
+ stream_str += '.';
+ stream_str += name;
+ }
+ return stream_str;
+}
+
+template<typename T>
+T *LookupTableByName(const SymbolTable<T> &table, const std::string &name,
+ const Namespace &current_namespace, size_t skip_top) {
+ const auto &components = current_namespace.components;
+ if (table.dict.empty()) return nullptr;
+ if (components.size() < skip_top) return nullptr;
+ const auto N = components.size() - skip_top;
+ std::string full_name;
+ for (size_t i = 0; i < N; i++) {
+ full_name += components[i];
+ full_name += '.';
+ }
+ for (size_t i = N; i > 0; i--) {
+ full_name += name;
+ auto obj = table.Lookup(full_name);
+ if (obj) return obj;
+ auto len = full_name.size() - components[i - 1].size() - 1 - name.size();
+ full_name.resize(len);
+ }
+ FLATBUFFERS_ASSERT(full_name.empty());
+ return table.Lookup(name); // lookup in global namespace
+}
+
+// Declare tokens we'll use. Single character tokens are represented by their
+// ascii character code (e.g. '{'), others above 256.
+// clang-format off
+#define FLATBUFFERS_GEN_TOKENS(TD) \
+ TD(Eof, 256, "end of file") \
+ TD(StringConstant, 257, "string constant") \
+ TD(IntegerConstant, 258, "integer constant") \
+ TD(FloatConstant, 259, "float constant") \
+ TD(Identifier, 260, "identifier")
+#ifdef __GNUC__
+__extension__ // Stop GCC complaining about trailing comma with -Wpendantic.
+#endif
+enum {
+ #define FLATBUFFERS_TOKEN(NAME, VALUE, STRING) kToken ## NAME = VALUE,
+ FLATBUFFERS_GEN_TOKENS(FLATBUFFERS_TOKEN)
+ #undef FLATBUFFERS_TOKEN
+};
+
+static std::string TokenToString(int t) {
+ static const char * const tokens[] = {
+ #define FLATBUFFERS_TOKEN(NAME, VALUE, STRING) STRING,
+ FLATBUFFERS_GEN_TOKENS(FLATBUFFERS_TOKEN)
+ #undef FLATBUFFERS_TOKEN
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, ...) \
+ IDLTYPE,
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ };
+ if (t < 256) { // A single ascii char token.
+ std::string s;
+ s.append(1, static_cast<char>(t));
+ return s;
+ } else { // Other tokens.
+ return tokens[t - 256];
+ }
+}
+// clang-format on
+
+std::string Parser::TokenToStringId(int t) const {
+ return t == kTokenIdentifier ? attribute_ : TokenToString(t);
+}
+
+// Parses exactly nibbles worth of hex digits into a number, or error.
+CheckedError Parser::ParseHexNum(int nibbles, uint64_t *val) {
+ FLATBUFFERS_ASSERT(nibbles > 0);
+ for (int i = 0; i < nibbles; i++)
+ if (!is_xdigit(cursor_[i]))
+ return Error("escape code must be followed by " + NumToString(nibbles) +
+ " hex digits");
+ std::string target(cursor_, cursor_ + nibbles);
+ *val = StringToUInt(target.c_str(), 16);
+ cursor_ += nibbles;
+ return NoError();
+}
+
+CheckedError Parser::SkipByteOrderMark() {
+ if (static_cast<unsigned char>(*cursor_) != 0xef) return NoError();
+ cursor_++;
+ if (static_cast<unsigned char>(*cursor_) != 0xbb)
+ return Error("invalid utf-8 byte order mark");
+ cursor_++;
+ if (static_cast<unsigned char>(*cursor_) != 0xbf)
+ return Error("invalid utf-8 byte order mark");
+ cursor_++;
+ return NoError();
+}
+
+static inline bool IsIdentifierStart(char c) {
+ return is_alpha(c) || (c == '_');
+}
+
+CheckedError Parser::Next() {
+ doc_comment_.clear();
+ bool seen_newline = cursor_ == source_;
+ attribute_.clear();
+ attr_is_trivial_ascii_string_ = true;
+ for (;;) {
+ char c = *cursor_++;
+ token_ = c;
+ switch (c) {
+ case '\0':
+ cursor_--;
+ token_ = kTokenEof;
+ return NoError();
+ case ' ':
+ case '\r':
+ case '\t': break;
+ case '\n':
+ MarkNewLine();
+ seen_newline = true;
+ break;
+ case '{':
+ case '}':
+ case '(':
+ case ')':
+ case '[':
+ case ']':
+ case ',':
+ case ':':
+ case ';':
+ case '=': return NoError();
+ case '\"':
+ case '\'': {
+ int unicode_high_surrogate = -1;
+
+ while (*cursor_ != c) {
+ if (*cursor_ < ' ' && static_cast<signed char>(*cursor_) >= 0)
+ return Error("illegal character in string constant");
+ if (*cursor_ == '\\') {
+ attr_is_trivial_ascii_string_ = false; // has escape sequence
+ cursor_++;
+ if (unicode_high_surrogate != -1 && *cursor_ != 'u') {
+ return Error(
+ "illegal Unicode sequence (unpaired high surrogate)");
+ }
+ switch (*cursor_) {
+ case 'n':
+ attribute_ += '\n';
+ cursor_++;
+ break;
+ case 't':
+ attribute_ += '\t';
+ cursor_++;
+ break;
+ case 'r':
+ attribute_ += '\r';
+ cursor_++;
+ break;
+ case 'b':
+ attribute_ += '\b';
+ cursor_++;
+ break;
+ case 'f':
+ attribute_ += '\f';
+ cursor_++;
+ break;
+ case '\"':
+ attribute_ += '\"';
+ cursor_++;
+ break;
+ case '\'':
+ attribute_ += '\'';
+ cursor_++;
+ break;
+ case '\\':
+ attribute_ += '\\';
+ cursor_++;
+ break;
+ case '/':
+ attribute_ += '/';
+ cursor_++;
+ break;
+ case 'x': { // Not in the JSON standard
+ cursor_++;
+ uint64_t val;
+ ECHECK(ParseHexNum(2, &val));
+ attribute_ += static_cast<char>(val);
+ break;
+ }
+ case 'u': {
+ cursor_++;
+ uint64_t val;
+ ECHECK(ParseHexNum(4, &val));
+ if (val >= 0xD800 && val <= 0xDBFF) {
+ if (unicode_high_surrogate != -1) {
+ return Error(
+ "illegal Unicode sequence (multiple high surrogates)");
+ } else {
+ unicode_high_surrogate = static_cast<int>(val);
+ }
+ } else if (val >= 0xDC00 && val <= 0xDFFF) {
+ if (unicode_high_surrogate == -1) {
+ return Error(
+ "illegal Unicode sequence (unpaired low surrogate)");
+ } else {
+ int code_point = 0x10000 +
+ ((unicode_high_surrogate & 0x03FF) << 10) +
+ (val & 0x03FF);
+ ToUTF8(code_point, &attribute_);
+ unicode_high_surrogate = -1;
+ }
+ } else {
+ if (unicode_high_surrogate != -1) {
+ return Error(
+ "illegal Unicode sequence (unpaired high surrogate)");
+ }
+ ToUTF8(static_cast<int>(val), &attribute_);
+ }
+ break;
+ }
+ default: return Error("unknown escape code in string constant");
+ }
+ } else { // printable chars + UTF-8 bytes
+ if (unicode_high_surrogate != -1) {
+ return Error(
+ "illegal Unicode sequence (unpaired high surrogate)");
+ }
+ // reset if non-printable
+ attr_is_trivial_ascii_string_ &=
+ check_ascii_range(*cursor_, ' ', '~');
+
+ attribute_ += *cursor_++;
+ }
+ }
+ if (unicode_high_surrogate != -1) {
+ return Error("illegal Unicode sequence (unpaired high surrogate)");
+ }
+ cursor_++;
+ if (!attr_is_trivial_ascii_string_ && !opts.allow_non_utf8 &&
+ !ValidateUTF8(attribute_)) {
+ return Error("illegal UTF-8 sequence");
+ }
+ token_ = kTokenStringConstant;
+ return NoError();
+ }
+ case '/':
+ if (*cursor_ == '/') {
+ const char *start = ++cursor_;
+ while (*cursor_ && *cursor_ != '\n' && *cursor_ != '\r') cursor_++;
+ if (*start == '/') { // documentation comment
+ if (!seen_newline)
+ return Error(
+ "a documentation comment should be on a line on its own");
+ doc_comment_.push_back(std::string(start + 1, cursor_));
+ }
+ break;
+ } else if (*cursor_ == '*') {
+ cursor_++;
+ // TODO: make nested.
+ while (*cursor_ != '*' || cursor_[1] != '/') {
+ if (*cursor_ == '\n') MarkNewLine();
+ if (!*cursor_) return Error("end of file in comment");
+ cursor_++;
+ }
+ cursor_ += 2;
+ break;
+ }
+ FLATBUFFERS_FALLTHROUGH(); // else fall thru
+ default:
+ if (IsIdentifierStart(c)) {
+ // Collect all chars of an identifier:
+ const char *start = cursor_ - 1;
+ while (IsIdentifierStart(*cursor_) || is_digit(*cursor_)) cursor_++;
+ attribute_.append(start, cursor_);
+ token_ = kTokenIdentifier;
+ return NoError();
+ }
+
+ const auto has_sign = (c == '+') || (c == '-');
+ if (has_sign && IsIdentifierStart(*cursor_)) {
+ // '-'/'+' and following identifier - it could be a predefined
+ // constant. Return the sign in token_, see ParseSingleValue.
+ return NoError();
+ }
+
+ auto dot_lvl =
+ (c == '.') ? 0 : 1; // dot_lvl==0 <=> exactly one '.' seen
+ if (!dot_lvl && !is_digit(*cursor_)) return NoError(); // enum?
+ // Parser accepts hexadecimal-floating-literal (see C++ 5.13.4).
+ if (is_digit(c) || has_sign || !dot_lvl) {
+ const auto start = cursor_ - 1;
+ auto start_digits = !is_digit(c) ? cursor_ : cursor_ - 1;
+ if (!is_digit(c) && is_digit(*cursor_)) {
+ start_digits = cursor_; // see digit in cursor_ position
+ c = *cursor_++;
+ }
+ // hex-float can't begind with '.'
+ auto use_hex = dot_lvl && (c == '0') && is_alpha_char(*cursor_, 'X');
+ if (use_hex) start_digits = ++cursor_; // '0x' is the prefix, skip it
+ // Read an integer number or mantisa of float-point number.
+ do {
+ if (use_hex) {
+ while (is_xdigit(*cursor_)) cursor_++;
+ } else {
+ while (is_digit(*cursor_)) cursor_++;
+ }
+ } while ((*cursor_ == '.') && (++cursor_) && (--dot_lvl >= 0));
+ // Exponent of float-point number.
+ if ((dot_lvl >= 0) && (cursor_ > start_digits)) {
+ // The exponent suffix of hexadecimal float number is mandatory.
+ if (use_hex && !dot_lvl) start_digits = cursor_;
+ if ((use_hex && is_alpha_char(*cursor_, 'P')) ||
+ is_alpha_char(*cursor_, 'E')) {
+ dot_lvl = 0; // Emulate dot to signal about float-point number.
+ cursor_++;
+ if (*cursor_ == '+' || *cursor_ == '-') cursor_++;
+ start_digits = cursor_; // the exponent-part has to have digits
+ // Exponent is decimal integer number
+ while (is_digit(*cursor_)) cursor_++;
+ if (*cursor_ == '.') {
+ cursor_++; // If see a dot treat it as part of invalid number.
+ dot_lvl = -1; // Fall thru to Error().
+ }
+ }
+ }
+ // Finalize.
+ if ((dot_lvl >= 0) && (cursor_ > start_digits)) {
+ attribute_.append(start, cursor_);
+ token_ = dot_lvl ? kTokenIntegerConstant : kTokenFloatConstant;
+ return NoError();
+ } else {
+ return Error("invalid number: " + std::string(start, cursor_));
+ }
+ }
+ std::string ch;
+ ch = c;
+ if (false == check_ascii_range(c, ' ', '~'))
+ ch = "code: " + NumToString(c);
+ return Error("illegal character: " + ch);
+ }
+ }
+}
+
+// Check if a given token is next.
+bool Parser::Is(int t) const { return t == token_; }
+
+bool Parser::IsIdent(const char *id) const {
+ return token_ == kTokenIdentifier && attribute_ == id;
+}
+
+// Expect a given token to be next, consume it, or error if not present.
+CheckedError Parser::Expect(int t) {
+ if (t != token_) {
+ return Error("expecting: " + TokenToString(t) +
+ " instead got: " + TokenToStringId(token_));
+ }
+ NEXT();
+ return NoError();
+}
+
+CheckedError Parser::ParseNamespacing(std::string *id, std::string *last) {
+ while (Is('.')) {
+ NEXT();
+ *id += ".";
+ *id += attribute_;
+ if (last) *last = attribute_;
+ EXPECT(kTokenIdentifier);
+ }
+ return NoError();
+}
+
+EnumDef *Parser::LookupEnum(const std::string &id) {
+ // Search thru parent namespaces.
+ return LookupTableByName(enums_, id, *current_namespace_, 0);
+}
+
+StructDef *Parser::LookupStruct(const std::string &id) const {
+ auto sd = structs_.Lookup(id);
+ if (sd) sd->refcount++;
+ return sd;
+}
+
+StructDef *Parser::LookupStructThruParentNamespaces(
+ const std::string &id) const {
+ auto sd = LookupTableByName(structs_, id, *current_namespace_, 1);
+ if (sd) sd->refcount++;
+ return sd;
+}
+
+CheckedError Parser::ParseTypeIdent(Type &type) {
+ std::string id = attribute_;
+ EXPECT(kTokenIdentifier);
+ ECHECK(ParseNamespacing(&id, nullptr));
+ auto enum_def = LookupEnum(id);
+ if (enum_def) {
+ type = enum_def->underlying_type;
+ if (enum_def->is_union) type.base_type = BASE_TYPE_UNION;
+ } else {
+ type.base_type = BASE_TYPE_STRUCT;
+ type.struct_def = LookupCreateStruct(id);
+ }
+ return NoError();
+}
+
+// Parse any IDL type.
+CheckedError Parser::ParseType(Type &type) {
+ if (token_ == kTokenIdentifier) {
+ if (IsIdent("bool")) {
+ type.base_type = BASE_TYPE_BOOL;
+ NEXT();
+ } else if (IsIdent("byte") || IsIdent("int8")) {
+ type.base_type = BASE_TYPE_CHAR;
+ NEXT();
+ } else if (IsIdent("ubyte") || IsIdent("uint8")) {
+ type.base_type = BASE_TYPE_UCHAR;
+ NEXT();
+ } else if (IsIdent("short") || IsIdent("int16")) {
+ type.base_type = BASE_TYPE_SHORT;
+ NEXT();
+ } else if (IsIdent("ushort") || IsIdent("uint16")) {
+ type.base_type = BASE_TYPE_USHORT;
+ NEXT();
+ } else if (IsIdent("int") || IsIdent("int32")) {
+ type.base_type = BASE_TYPE_INT;
+ NEXT();
+ } else if (IsIdent("uint") || IsIdent("uint32")) {
+ type.base_type = BASE_TYPE_UINT;
+ NEXT();
+ } else if (IsIdent("long") || IsIdent("int64")) {
+ type.base_type = BASE_TYPE_LONG;
+ NEXT();
+ } else if (IsIdent("ulong") || IsIdent("uint64")) {
+ type.base_type = BASE_TYPE_ULONG;
+ NEXT();
+ } else if (IsIdent("float") || IsIdent("float32")) {
+ type.base_type = BASE_TYPE_FLOAT;
+ NEXT();
+ } else if (IsIdent("double") || IsIdent("float64")) {
+ type.base_type = BASE_TYPE_DOUBLE;
+ NEXT();
+ } else if (IsIdent("string")) {
+ type.base_type = BASE_TYPE_STRING;
+ NEXT();
+ } else {
+ ECHECK(ParseTypeIdent(type));
+ }
+ } else if (token_ == '[') {
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+ NEXT();
+ Type subtype;
+ ECHECK(ParseType(subtype));
+ if (IsSeries(subtype)) {
+ // We could support this, but it will complicate things, and it's
+ // easier to work around with a struct around the inner vector.
+ return Error("nested vector types not supported (wrap in table first)");
+ }
+ if (token_ == ':') {
+ NEXT();
+ if (token_ != kTokenIntegerConstant) {
+ return Error("length of fixed-length array must be an integer value");
+ }
+ uint16_t fixed_length = 0;
+ bool check = StringToNumber(attribute_.c_str(), &fixed_length);
+ if (!check || fixed_length < 1) {
+ return Error(
+ "length of fixed-length array must be positive and fit to "
+ "uint16_t type");
+ }
+ type = Type(BASE_TYPE_ARRAY, subtype.struct_def, subtype.enum_def,
+ fixed_length);
+ NEXT();
+ } else {
+ type = Type(BASE_TYPE_VECTOR, subtype.struct_def, subtype.enum_def);
+ }
+ type.element = subtype.base_type;
+ EXPECT(']');
+ } else {
+ return Error("illegal type syntax");
+ }
+ return NoError();
+}
+
+CheckedError Parser::AddField(StructDef &struct_def, const std::string &name,
+ const Type &type, FieldDef **dest) {
+ auto &field = *new FieldDef();
+ field.value.offset =
+ FieldIndexToOffset(static_cast<voffset_t>(struct_def.fields.vec.size()));
+ field.name = name;
+ field.file = struct_def.file;
+ field.value.type = type;
+ if (struct_def.fixed) { // statically compute the field offset
+ auto size = InlineSize(type);
+ auto alignment = InlineAlignment(type);
+ // structs_ need to have a predictable format, so we need to align to
+ // the largest scalar
+ struct_def.minalign = std::max(struct_def.minalign, alignment);
+ struct_def.PadLastField(alignment);
+ field.value.offset = static_cast<voffset_t>(struct_def.bytesize);
+ struct_def.bytesize += size;
+ }
+ if (struct_def.fields.Add(name, &field))
+ return Error("field already exists: " + name);
+ *dest = &field;
+ return NoError();
+}
+
+CheckedError Parser::ParseField(StructDef &struct_def) {
+ std::string name = attribute_;
+
+ if (LookupCreateStruct(name, false, false))
+ return Error("field name can not be the same as table/struct name");
+
+ if (!IsLowerSnakeCase(name)) {
+ Warning("field names should be lowercase snake_case, got: " + name);
+ }
+
+ std::vector<std::string> dc = doc_comment_;
+ EXPECT(kTokenIdentifier);
+ EXPECT(':');
+ Type type;
+ ECHECK(ParseType(type));
+
+ if (struct_def.fixed) {
+ auto valid = IsScalar(type.base_type) || IsStruct(type);
+ if (!valid && IsArray(type)) {
+ const auto &elem_type = type.VectorType();
+ valid |= IsScalar(elem_type.base_type) || IsStruct(elem_type);
+ }
+ if (!valid)
+ return Error("structs may contain only scalar or struct fields");
+ }
+
+ if (!struct_def.fixed && IsArray(type))
+ return Error("fixed-length array in table must be wrapped in struct");
+
+ if (IsArray(type)) {
+ advanced_features_ |= reflection::AdvancedArrayFeatures;
+ if (!SupportsAdvancedArrayFeatures()) {
+ return Error(
+ "Arrays are not yet supported in all "
+ "the specified programming languages.");
+ }
+ }
+
+ FieldDef *typefield = nullptr;
+ if (type.base_type == BASE_TYPE_UNION) {
+ // For union fields, add a second auto-generated field to hold the type,
+ // with a special suffix.
+ ECHECK(AddField(struct_def, name + UnionTypeFieldSuffix(),
+ type.enum_def->underlying_type, &typefield));
+ } else if (IsVector(type) && type.element == BASE_TYPE_UNION) {
+ advanced_features_ |= reflection::AdvancedUnionFeatures;
+ // Only cpp, js and ts supports the union vector feature so far.
+ if (!SupportsAdvancedUnionFeatures()) {
+ return Error(
+ "Vectors of unions are not yet supported in at least one of "
+ "the specified programming languages.");
+ }
+ // For vector of union fields, add a second auto-generated vector field to
+ // hold the types, with a special suffix.
+ Type union_vector(BASE_TYPE_VECTOR, nullptr, type.enum_def);
+ union_vector.element = BASE_TYPE_UTYPE;
+ ECHECK(AddField(struct_def, name + UnionTypeFieldSuffix(), union_vector,
+ &typefield));
+ }
+
+ FieldDef *field;
+ ECHECK(AddField(struct_def, name, type, &field));
+
+ if (token_ == '=') {
+ NEXT();
+ ECHECK(ParseSingleValue(&field->name, field->value, true));
+ if (IsStruct(type) || (struct_def.fixed && field->value.constant != "0"))
+ return Error(
+ "default values are not supported for struct fields, table fields, "
+ "or in structs.");
+ if (IsString(type) || IsVector(type)) {
+ advanced_features_ |= reflection::DefaultVectorsAndStrings;
+ if (field->value.constant != "0" && field->value.constant != "null" &&
+ !SupportsDefaultVectorsAndStrings()) {
+ return Error(
+ "Default values for strings and vectors are not supported in one "
+ "of the specified programming languages");
+ }
+ }
+
+ if (IsVector(type) && field->value.constant != "0" &&
+ field->value.constant != "[]") {
+ return Error("The only supported default for vectors is `[]`.");
+ }
+ }
+
+ // Append .0 if the value has not it (skip hex and scientific floats).
+ // This suffix needed for generated C++ code.
+ if (IsFloat(type.base_type)) {
+ auto &text = field->value.constant;
+ FLATBUFFERS_ASSERT(false == text.empty());
+ auto s = text.c_str();
+ while (*s == ' ') s++;
+ if (*s == '-' || *s == '+') s++;
+ // 1) A float constants (nan, inf, pi, etc) is a kind of identifier.
+ // 2) A float number needn't ".0" at the end if it has exponent.
+ if ((false == IsIdentifierStart(*s)) &&
+ (std::string::npos == field->value.constant.find_first_of(".eEpP"))) {
+ field->value.constant += ".0";
+ }
+ }
+
+ field->doc_comment = dc;
+ ECHECK(ParseMetaData(&field->attributes));
+ field->deprecated = field->attributes.Lookup("deprecated") != nullptr;
+ auto hash_name = field->attributes.Lookup("hash");
+ if (hash_name) {
+ switch ((IsVector(type)) ? type.element : type.base_type) {
+ case BASE_TYPE_SHORT:
+ case BASE_TYPE_USHORT: {
+ if (FindHashFunction16(hash_name->constant.c_str()) == nullptr)
+ return Error("Unknown hashing algorithm for 16 bit types: " +
+ hash_name->constant);
+ break;
+ }
+ case BASE_TYPE_INT:
+ case BASE_TYPE_UINT: {
+ if (FindHashFunction32(hash_name->constant.c_str()) == nullptr)
+ return Error("Unknown hashing algorithm for 32 bit types: " +
+ hash_name->constant);
+ break;
+ }
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG: {
+ if (FindHashFunction64(hash_name->constant.c_str()) == nullptr)
+ return Error("Unknown hashing algorithm for 64 bit types: " +
+ hash_name->constant);
+ break;
+ }
+ default:
+ return Error(
+ "only short, ushort, int, uint, long and ulong data types support "
+ "hashing.");
+ }
+ }
+
+ // For historical convenience reasons, string keys are assumed required.
+ // Scalars are kDefault unless otherwise specified.
+ // Nonscalars are kOptional unless required;
+ field->key = field->attributes.Lookup("key") != nullptr;
+ const bool required = field->attributes.Lookup("required") != nullptr ||
+ (IsString(type) && field->key);
+ const bool default_str_or_vec =
+ ((IsString(type) || IsVector(type)) && field->value.constant != "0");
+ const bool optional = IsScalar(type.base_type)
+ ? (field->value.constant == "null")
+ : !(required || default_str_or_vec);
+ if (required && optional) {
+ return Error("Fields cannot be both optional and required.");
+ }
+ field->presence = FieldDef::MakeFieldPresence(optional, required);
+
+ if (required && (struct_def.fixed || IsScalar(type.base_type))) {
+ return Error("only non-scalar fields in tables may be 'required'");
+ }
+ if (field->key) {
+ if (struct_def.has_key) return Error("only one field may be set as 'key'");
+ struct_def.has_key = true;
+ if (!IsScalar(type.base_type) && !IsString(type)) {
+ return Error("'key' field must be string or scalar type");
+ }
+ }
+
+ if (field->IsScalarOptional()) {
+ advanced_features_ |= reflection::OptionalScalars;
+ if (type.enum_def && type.enum_def->Lookup("null")) {
+ FLATBUFFERS_ASSERT(IsInteger(type.base_type));
+ return Error(
+ "the default 'null' is reserved for declaring optional scalar "
+ "fields, it conflicts with declaration of enum '" +
+ type.enum_def->name + "'.");
+ }
+ if (field->attributes.Lookup("key")) {
+ return Error(
+ "only a non-optional scalar field can be used as a 'key' field");
+ }
+ if (!SupportsOptionalScalars()) {
+ return Error(
+ "Optional scalars are not yet supported in at least one the of "
+ "the specified programming languages.");
+ }
+ }
+
+ if (type.enum_def) {
+ // Verify the enum's type and default value.
+ const std::string &constant = field->value.constant;
+ if (type.base_type == BASE_TYPE_UNION) {
+ if (constant != "0") { return Error("Union defaults must be NONE"); }
+ } else if (IsVector(type)) {
+ if (constant != "0" && constant != "[]") {
+ return Error("Vector defaults may only be `[]`.");
+ }
+ } else if (IsArray(type)) {
+ if (constant != "0") {
+ return Error("Array defaults are not supported yet.");
+ }
+ } else {
+ if (!IsInteger(type.base_type)) {
+ return Error("Enums must have integer base types");
+ }
+ // Optional and bitflags enums may have default constants that are not
+ // their specified variants.
+ if (!field->IsOptional() &&
+ type.enum_def->attributes.Lookup("bit_flags") == nullptr) {
+ if (type.enum_def->FindByValue(constant) == nullptr) {
+ return Error("default value of `" + constant + "` for " + "field `" +
+ name + "` is not part of enum `" + type.enum_def->name +
+ "`.");
+ }
+ }
+ }
+ }
+
+ if (field->deprecated && struct_def.fixed)
+ return Error("can't deprecate fields in a struct");
+
+ auto cpp_type = field->attributes.Lookup("cpp_type");
+ if (cpp_type) {
+ if (!hash_name)
+ return Error("cpp_type can only be used with a hashed field");
+ /// forcing cpp_ptr_type to 'naked' if unset
+ auto cpp_ptr_type = field->attributes.Lookup("cpp_ptr_type");
+ if (!cpp_ptr_type) {
+ auto val = new Value();
+ val->type = cpp_type->type;
+ val->constant = "naked";
+ field->attributes.Add("cpp_ptr_type", val);
+ }
+ }
+
+ field->shared = field->attributes.Lookup("shared") != nullptr;
+ if (field->shared && field->value.type.base_type != BASE_TYPE_STRING)
+ return Error("shared can only be defined on strings");
+
+ auto field_native_custom_alloc =
+ field->attributes.Lookup("native_custom_alloc");
+ if (field_native_custom_alloc)
+ return Error(
+ "native_custom_alloc can only be used with a table or struct "
+ "definition");
+
+ field->native_inline = field->attributes.Lookup("native_inline") != nullptr;
+ if (field->native_inline && !IsStruct(field->value.type))
+ return Error("native_inline can only be defined on structs");
+
+ auto nested = field->attributes.Lookup("nested_flatbuffer");
+ if (nested) {
+ if (nested->type.base_type != BASE_TYPE_STRING)
+ return Error(
+ "nested_flatbuffer attribute must be a string (the root type)");
+ if (type.base_type != BASE_TYPE_VECTOR || type.element != BASE_TYPE_UCHAR)
+ return Error(
+ "nested_flatbuffer attribute may only apply to a vector of ubyte");
+ // This will cause an error if the root type of the nested flatbuffer
+ // wasn't defined elsewhere.
+ field->nested_flatbuffer = LookupCreateStruct(nested->constant);
+ }
+
+ if (field->attributes.Lookup("flexbuffer")) {
+ field->flexbuffer = true;
+ uses_flexbuffers_ = true;
+ if (type.base_type != BASE_TYPE_VECTOR || type.element != BASE_TYPE_UCHAR)
+ return Error("flexbuffer attribute may only apply to a vector of ubyte");
+ }
+
+ if (typefield) {
+ if (!IsScalar(typefield->value.type.base_type)) {
+ // this is a union vector field
+ typefield->presence = field->presence;
+ }
+ // If this field is a union, and it has a manually assigned id,
+ // the automatically added type field should have an id as well (of N - 1).
+ auto attr = field->attributes.Lookup("id");
+ if (attr) {
+ const auto &id_str = attr->constant;
+ voffset_t id = 0;
+ const auto done = !atot(id_str.c_str(), *this, &id).Check();
+ if (done && id > 0) {
+ auto val = new Value();
+ val->type = attr->type;
+ val->constant = NumToString(id - 1);
+ typefield->attributes.Add("id", val);
+ } else {
+ return Error(
+ "a union type effectively adds two fields with non-negative ids, "
+ "its id must be that of the second field (the first field is "
+ "the type field and not explicitly declared in the schema);\n"
+ "field: " +
+ field->name + ", id: " + id_str);
+ }
+ }
+ // if this field is a union that is deprecated,
+ // the automatically added type field should be deprecated as well
+ if (field->deprecated) { typefield->deprecated = true; }
+ }
+
+ EXPECT(';');
+ return NoError();
+}
+
+CheckedError Parser::ParseString(Value &val, bool use_string_pooling) {
+ auto s = attribute_;
+ EXPECT(kTokenStringConstant);
+ if (use_string_pooling) {
+ val.constant = NumToString(builder_.CreateSharedString(s).o);
+ } else {
+ val.constant = NumToString(builder_.CreateString(s).o);
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseComma() {
+ if (!opts.protobuf_ascii_alike) EXPECT(',');
+ return NoError();
+}
+
+CheckedError Parser::ParseAnyValue(Value &val, FieldDef *field,
+ size_t parent_fieldn,
+ const StructDef *parent_struct_def,
+ uoffset_t count, bool inside_vector) {
+ switch (val.type.base_type) {
+ case BASE_TYPE_UNION: {
+ FLATBUFFERS_ASSERT(field);
+ std::string constant;
+ Vector<uint8_t> *vector_of_union_types = nullptr;
+ // Find corresponding type field we may have already parsed.
+ for (auto elem = field_stack_.rbegin() + count;
+ elem != field_stack_.rbegin() + parent_fieldn + count; ++elem) {
+ auto &type = elem->second->value.type;
+ if (type.enum_def == val.type.enum_def) {
+ if (inside_vector) {
+ if (IsVector(type) && type.element == BASE_TYPE_UTYPE) {
+ // Vector of union type field.
+ uoffset_t offset;
+ ECHECK(atot(elem->first.constant.c_str(), *this, &offset));
+ vector_of_union_types = reinterpret_cast<Vector<uint8_t> *>(
+ builder_.GetCurrentBufferPointer() + builder_.GetSize() -
+ offset);
+ break;
+ }
+ } else {
+ if (type.base_type == BASE_TYPE_UTYPE) {
+ // Union type field.
+ constant = elem->first.constant;
+ break;
+ }
+ }
+ }
+ }
+ if (constant.empty() && !inside_vector) {
+ // We haven't seen the type field yet. Sadly a lot of JSON writers
+ // output these in alphabetical order, meaning it comes after this
+ // value. So we scan past the value to find it, then come back here.
+ // We currently don't do this for vectors of unions because the
+ // scanning/serialization logic would get very complicated.
+ auto type_name = field->name + UnionTypeFieldSuffix();
+ FLATBUFFERS_ASSERT(parent_struct_def);
+ auto type_field = parent_struct_def->fields.Lookup(type_name);
+ FLATBUFFERS_ASSERT(type_field); // Guaranteed by ParseField().
+ // Remember where we are in the source file, so we can come back here.
+ auto backup = *static_cast<ParserState *>(this);
+ ECHECK(SkipAnyJsonValue()); // The table.
+ ECHECK(ParseComma());
+ auto next_name = attribute_;
+ if (Is(kTokenStringConstant)) {
+ NEXT();
+ } else {
+ EXPECT(kTokenIdentifier);
+ }
+ if (next_name == type_name) {
+ EXPECT(':');
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+ Value type_val = type_field->value;
+ ECHECK(ParseAnyValue(type_val, type_field, 0, nullptr, 0));
+ constant = type_val.constant;
+ // Got the information we needed, now rewind:
+ *static_cast<ParserState *>(this) = backup;
+ }
+ }
+ if (constant.empty() && !vector_of_union_types) {
+ return Error("missing type field for this union value: " + field->name);
+ }
+ uint8_t enum_idx;
+ if (vector_of_union_types) {
+ enum_idx = vector_of_union_types->Get(count);
+ } else {
+ ECHECK(atot(constant.c_str(), *this, &enum_idx));
+ }
+ auto enum_val = val.type.enum_def->ReverseLookup(enum_idx, true);
+ if (!enum_val) return Error("illegal type id for: " + field->name);
+ if (enum_val->union_type.base_type == BASE_TYPE_STRUCT) {
+ ECHECK(ParseTable(*enum_val->union_type.struct_def, &val.constant,
+ nullptr));
+ if (enum_val->union_type.struct_def->fixed) {
+ // All BASE_TYPE_UNION values are offsets, so turn this into one.
+ SerializeStruct(*enum_val->union_type.struct_def, val);
+ builder_.ClearOffsets();
+ val.constant = NumToString(builder_.GetSize());
+ }
+ } else if (IsString(enum_val->union_type)) {
+ ECHECK(ParseString(val, field->shared));
+ } else {
+ FLATBUFFERS_ASSERT(false);
+ }
+ break;
+ }
+ case BASE_TYPE_STRUCT:
+ ECHECK(ParseTable(*val.type.struct_def, &val.constant, nullptr));
+ break;
+ case BASE_TYPE_STRING: {
+ ECHECK(ParseString(val, field->shared));
+ break;
+ }
+ case BASE_TYPE_VECTOR: {
+ uoffset_t off;
+ ECHECK(ParseVector(val.type.VectorType(), &off, field, parent_fieldn));
+ val.constant = NumToString(off);
+ break;
+ }
+ case BASE_TYPE_ARRAY: {
+ ECHECK(ParseArray(val));
+ break;
+ }
+ case BASE_TYPE_INT:
+ case BASE_TYPE_UINT:
+ case BASE_TYPE_LONG:
+ case BASE_TYPE_ULONG: {
+ if (field && field->attributes.Lookup("hash") &&
+ (token_ == kTokenIdentifier || token_ == kTokenStringConstant)) {
+ ECHECK(ParseHash(val, field));
+ } else {
+ ECHECK(ParseSingleValue(field ? &field->name : nullptr, val, false));
+ }
+ break;
+ }
+ default:
+ ECHECK(ParseSingleValue(field ? &field->name : nullptr, val, false));
+ break;
+ }
+ return NoError();
+}
+
+void Parser::SerializeStruct(const StructDef &struct_def, const Value &val) {
+ SerializeStruct(builder_, struct_def, val);
+}
+
+void Parser::SerializeStruct(FlatBufferBuilder &builder,
+ const StructDef &struct_def, const Value &val) {
+ FLATBUFFERS_ASSERT(val.constant.length() == struct_def.bytesize);
+ builder.Align(struct_def.minalign);
+ builder.PushBytes(reinterpret_cast<const uint8_t *>(val.constant.c_str()),
+ struct_def.bytesize);
+ builder.AddStructOffset(val.offset, builder.GetSize());
+}
+
+template<typename F>
+CheckedError Parser::ParseTableDelimiters(size_t &fieldn,
+ const StructDef *struct_def, F body) {
+ // We allow tables both as JSON object{ .. } with field names
+ // or vector[..] with all fields in order
+ char terminator = '}';
+ bool is_nested_vector = struct_def && Is('[');
+ if (is_nested_vector) {
+ NEXT();
+ terminator = ']';
+ } else {
+ EXPECT('{');
+ }
+ for (;;) {
+ if ((!opts.strict_json || !fieldn) && Is(terminator)) break;
+ std::string name;
+ if (is_nested_vector) {
+ if (fieldn >= struct_def->fields.vec.size()) {
+ return Error("too many unnamed fields in nested array");
+ }
+ name = struct_def->fields.vec[fieldn]->name;
+ } else {
+ name = attribute_;
+ if (Is(kTokenStringConstant)) {
+ NEXT();
+ } else {
+ EXPECT(opts.strict_json ? kTokenStringConstant : kTokenIdentifier);
+ }
+ if (!opts.protobuf_ascii_alike || !(Is('{') || Is('['))) EXPECT(':');
+ }
+ ECHECK(body(name, fieldn, struct_def));
+ if (Is(terminator)) break;
+ ECHECK(ParseComma());
+ }
+ NEXT();
+ if (is_nested_vector && fieldn != struct_def->fields.vec.size()) {
+ return Error("wrong number of unnamed fields in table vector");
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseTable(const StructDef &struct_def, std::string *value,
+ uoffset_t *ovalue) {
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+
+ size_t fieldn_outer = 0;
+ auto err = ParseTableDelimiters(
+ fieldn_outer, &struct_def,
+ [&](const std::string &name, size_t &fieldn,
+ const StructDef *struct_def_inner) -> CheckedError {
+ if (name == "$schema") {
+ ECHECK(Expect(kTokenStringConstant));
+ return NoError();
+ }
+ auto field = struct_def_inner->fields.Lookup(name);
+ if (!field) {
+ if (!opts.skip_unexpected_fields_in_json) {
+ return Error("unknown field: " + name);
+ } else {
+ ECHECK(SkipAnyJsonValue());
+ }
+ } else {
+ if (IsIdent("null") && !IsScalar(field->value.type.base_type)) {
+ ECHECK(Next()); // Ignore this field.
+ } else {
+ Value val = field->value;
+ if (field->flexbuffer) {
+ flexbuffers::Builder builder(1024,
+ flexbuffers::BUILDER_FLAG_SHARE_ALL);
+ ECHECK(ParseFlexBufferValue(&builder));
+ builder.Finish();
+ // Force alignment for nested flexbuffer
+ builder_.ForceVectorAlignment(builder.GetSize(), sizeof(uint8_t),
+ sizeof(largest_scalar_t));
+ auto off = builder_.CreateVector(builder.GetBuffer());
+ val.constant = NumToString(off.o);
+ } else if (field->nested_flatbuffer) {
+ ECHECK(
+ ParseNestedFlatbuffer(val, field, fieldn, struct_def_inner));
+ } else {
+ ECHECK(ParseAnyValue(val, field, fieldn, struct_def_inner, 0));
+ }
+ // Hardcoded insertion-sort with error-check.
+ // If fields are specified in order, then this loop exits
+ // immediately.
+ auto elem = field_stack_.rbegin();
+ for (; elem != field_stack_.rbegin() + fieldn; ++elem) {
+ auto existing_field = elem->second;
+ if (existing_field == field)
+ return Error("field set more than once: " + field->name);
+ if (existing_field->value.offset < field->value.offset) break;
+ }
+ // Note: elem points to before the insertion point, thus .base()
+ // points to the correct spot.
+ field_stack_.insert(elem.base(), std::make_pair(val, field));
+ fieldn++;
+ }
+ }
+ return NoError();
+ });
+ ECHECK(err);
+
+ // Check if all required fields are parsed.
+ for (auto field_it = struct_def.fields.vec.begin();
+ field_it != struct_def.fields.vec.end(); ++field_it) {
+ auto required_field = *field_it;
+ if (!required_field->IsRequired()) { continue; }
+ bool found = false;
+ for (auto pf_it = field_stack_.end() - fieldn_outer;
+ pf_it != field_stack_.end(); ++pf_it) {
+ auto parsed_field = pf_it->second;
+ if (parsed_field == required_field) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return Error("required field is missing: " + required_field->name +
+ " in " + struct_def.name);
+ }
+ }
+
+ if (struct_def.fixed && fieldn_outer != struct_def.fields.vec.size())
+ return Error("struct: wrong number of initializers: " + struct_def.name);
+
+ auto start = struct_def.fixed ? builder_.StartStruct(struct_def.minalign)
+ : builder_.StartTable();
+
+ for (size_t size = struct_def.sortbysize ? sizeof(largest_scalar_t) : 1; size;
+ size /= 2) {
+ // Go through elements in reverse, since we're building the data backwards.
+ for (auto it = field_stack_.rbegin();
+ it != field_stack_.rbegin() + fieldn_outer; ++it) {
+ auto &field_value = it->first;
+ auto field = it->second;
+ if (!struct_def.sortbysize ||
+ size == SizeOf(field_value.type.base_type)) {
+ switch (field_value.type.base_type) {
+ // clang-format off
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ builder_.Pad(field->padding); \
+ if (struct_def.fixed) { \
+ CTYPE val; \
+ ECHECK(atot(field_value.constant.c_str(), *this, &val)); \
+ builder_.PushElement(val); \
+ } else { \
+ CTYPE val, valdef; \
+ ECHECK(atot(field_value.constant.c_str(), *this, &val)); \
+ ECHECK(atot(field->value.constant.c_str(), *this, &valdef)); \
+ builder_.AddElement(field_value.offset, val, valdef); \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ builder_.Pad(field->padding); \
+ if (IsStruct(field->value.type)) { \
+ SerializeStruct(*field->value.type.struct_def, field_value); \
+ } else { \
+ CTYPE val; \
+ ECHECK(atot(field_value.constant.c_str(), *this, &val)); \
+ builder_.AddOffset(field_value.offset, val); \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES_POINTER(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ case BASE_TYPE_ARRAY:
+ builder_.Pad(field->padding);
+ builder_.PushBytes(
+ reinterpret_cast<const uint8_t*>(field_value.constant.c_str()),
+ InlineSize(field_value.type));
+ break;
+ // clang-format on
+ }
+ }
+ }
+ }
+ for (size_t i = 0; i < fieldn_outer; i++) field_stack_.pop_back();
+
+ if (struct_def.fixed) {
+ builder_.ClearOffsets();
+ builder_.EndStruct();
+ FLATBUFFERS_ASSERT(value);
+ // Temporarily store this struct in the value string, since it is to
+ // be serialized in-place elsewhere.
+ value->assign(
+ reinterpret_cast<const char *>(builder_.GetCurrentBufferPointer()),
+ struct_def.bytesize);
+ builder_.PopBytes(struct_def.bytesize);
+ FLATBUFFERS_ASSERT(!ovalue);
+ } else {
+ auto val = builder_.EndTable(start);
+ if (ovalue) *ovalue = val;
+ if (value) *value = NumToString(val);
+ }
+ return NoError();
+}
+
+template<typename F>
+CheckedError Parser::ParseVectorDelimiters(uoffset_t &count, F body) {
+ EXPECT('[');
+ for (;;) {
+ if ((!opts.strict_json || !count) && Is(']')) break;
+ ECHECK(body(count));
+ count++;
+ if (Is(']')) break;
+ ECHECK(ParseComma());
+ }
+ NEXT();
+ return NoError();
+}
+
+static bool CompareSerializedScalars(const uint8_t *a, const uint8_t *b,
+ const FieldDef &key) {
+ switch (key.value.type.base_type) {
+#define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_##ENUM: { \
+ CTYPE def = static_cast<CTYPE>(0); \
+ if (!a || !b) { StringToNumber(key.value.constant.c_str(), &def); } \
+ const auto av = a ? ReadScalar<CTYPE>(a) : def; \
+ const auto bv = b ? ReadScalar<CTYPE>(b) : def; \
+ return av < bv; \
+ }
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+#undef FLATBUFFERS_TD
+ default: {
+ FLATBUFFERS_ASSERT(false && "scalar type expected");
+ return false;
+ }
+ }
+}
+
+static bool CompareTablesByScalarKey(const Offset<Table> *_a,
+ const Offset<Table> *_b,
+ const FieldDef &key) {
+ const voffset_t offset = key.value.offset;
+ // Indirect offset pointer to table pointer.
+ auto a = reinterpret_cast<const uint8_t *>(_a) + ReadScalar<uoffset_t>(_a);
+ auto b = reinterpret_cast<const uint8_t *>(_b) + ReadScalar<uoffset_t>(_b);
+ // Fetch field address from table.
+ a = reinterpret_cast<const Table *>(a)->GetAddressOf(offset);
+ b = reinterpret_cast<const Table *>(b)->GetAddressOf(offset);
+ return CompareSerializedScalars(a, b, key);
+}
+
+static bool CompareTablesByStringKey(const Offset<Table> *_a,
+ const Offset<Table> *_b,
+ const FieldDef &key) {
+ const voffset_t offset = key.value.offset;
+ // Indirect offset pointer to table pointer.
+ auto a = reinterpret_cast<const uint8_t *>(_a) + ReadScalar<uoffset_t>(_a);
+ auto b = reinterpret_cast<const uint8_t *>(_b) + ReadScalar<uoffset_t>(_b);
+ // Fetch field address from table.
+ a = reinterpret_cast<const Table *>(a)->GetAddressOf(offset);
+ b = reinterpret_cast<const Table *>(b)->GetAddressOf(offset);
+ if (a && b) {
+ // Indirect offset pointer to string pointer.
+ a += ReadScalar<uoffset_t>(a);
+ b += ReadScalar<uoffset_t>(b);
+ return *reinterpret_cast<const String *>(a) <
+ *reinterpret_cast<const String *>(b);
+ } else {
+ return a ? true : false;
+ }
+}
+
+static void SwapSerializedTables(Offset<Table> *a, Offset<Table> *b) {
+ // These are serialized offsets, so are relative where they are
+ // stored in memory, so compute the distance between these pointers:
+ ptrdiff_t diff = (b - a) * sizeof(Offset<Table>);
+ FLATBUFFERS_ASSERT(diff >= 0); // Guaranteed by SimpleQsort.
+ auto udiff = static_cast<uoffset_t>(diff);
+ a->o = EndianScalar(ReadScalar<uoffset_t>(a) - udiff);
+ b->o = EndianScalar(ReadScalar<uoffset_t>(b) + udiff);
+ std::swap(*a, *b);
+}
+
+// See below for why we need our own sort :(
+template<typename T, typename F, typename S>
+void SimpleQsort(T *begin, T *end, size_t width, F comparator, S swapper) {
+ if (end - begin <= static_cast<ptrdiff_t>(width)) return;
+ auto l = begin + width;
+ auto r = end;
+ while (l < r) {
+ if (comparator(begin, l)) {
+ r -= width;
+ swapper(l, r);
+ } else {
+ l += width;
+ }
+ }
+ l -= width;
+ swapper(begin, l);
+ SimpleQsort(begin, l, width, comparator, swapper);
+ SimpleQsort(r, end, width, comparator, swapper);
+}
+
+CheckedError Parser::ParseAlignAttribute(const std::string &align_constant,
+ size_t min_align, size_t *align) {
+ // Use uint8_t to avoid problems with size_t==`unsigned long` on LP64.
+ uint8_t align_value;
+ if (StringToNumber(align_constant.c_str(), &align_value) &&
+ VerifyAlignmentRequirements(static_cast<size_t>(align_value),
+ min_align)) {
+ *align = align_value;
+ return NoError();
+ }
+ return Error("unexpected force_align value '" + align_constant +
+ "', alignment must be a power of two integer ranging from the "
+ "type\'s natural alignment " +
+ NumToString(min_align) + " to " +
+ NumToString(FLATBUFFERS_MAX_ALIGNMENT));
+}
+
+CheckedError Parser::ParseVector(const Type &type, uoffset_t *ovalue,
+ FieldDef *field, size_t fieldn) {
+ uoffset_t count = 0;
+ auto err = ParseVectorDelimiters(count, [&](uoffset_t &) -> CheckedError {
+ Value val;
+ val.type = type;
+ ECHECK(ParseAnyValue(val, field, fieldn, nullptr, count, true));
+ field_stack_.push_back(std::make_pair(val, nullptr));
+ return NoError();
+ });
+ ECHECK(err);
+
+ const size_t len = count * InlineSize(type) / InlineAlignment(type);
+ const size_t elemsize = InlineAlignment(type);
+ const auto force_align = field->attributes.Lookup("force_align");
+ if (force_align) {
+ size_t align;
+ ECHECK(ParseAlignAttribute(force_align->constant, 1, &align));
+ if (align > 1) { builder_.ForceVectorAlignment(len, elemsize, align); }
+ }
+
+ builder_.StartVector(len, elemsize);
+ for (uoffset_t i = 0; i < count; i++) {
+ // start at the back, since we're building the data backwards.
+ auto &val = field_stack_.back().first;
+ switch (val.type.base_type) {
+ // clang-format off
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE,...) \
+ case BASE_TYPE_ ## ENUM: \
+ if (IsStruct(val.type)) SerializeStruct(*val.type.struct_def, val); \
+ else { \
+ CTYPE elem; \
+ ECHECK(atot(val.constant.c_str(), *this, &elem)); \
+ builder_.PushElement(elem); \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ // clang-format on
+ }
+ field_stack_.pop_back();
+ }
+
+ builder_.ClearOffsets();
+ *ovalue = builder_.EndVector(count);
+
+ if (type.base_type == BASE_TYPE_STRUCT && type.struct_def->has_key) {
+ // We should sort this vector. Find the key first.
+ const FieldDef *key = nullptr;
+ for (auto it = type.struct_def->fields.vec.begin();
+ it != type.struct_def->fields.vec.end(); ++it) {
+ if ((*it)->key) {
+ key = (*it);
+ break;
+ }
+ }
+ FLATBUFFERS_ASSERT(key);
+ // Now sort it.
+ // We can't use std::sort because for structs the size is not known at
+ // compile time, and for tables our iterators dereference offsets, so can't
+ // be used to swap elements.
+ // And we can't use C qsort either, since that would force use to use
+ // globals, making parsing thread-unsafe.
+ // So for now, we use SimpleQsort above.
+ // TODO: replace with something better, preferably not recursive.
+
+ if (type.struct_def->fixed) {
+ const voffset_t offset = key->value.offset;
+ const size_t struct_size = type.struct_def->bytesize;
+ auto v =
+ reinterpret_cast<VectorOfAny *>(builder_.GetCurrentBufferPointer());
+ SimpleQsort<uint8_t>(
+ v->Data(), v->Data() + v->size() * type.struct_def->bytesize,
+ type.struct_def->bytesize,
+ [offset, key](const uint8_t *a, const uint8_t *b) -> bool {
+ return CompareSerializedScalars(a + offset, b + offset, *key);
+ },
+ [struct_size](uint8_t *a, uint8_t *b) {
+ // FIXME: faster?
+ for (size_t i = 0; i < struct_size; i++) { std::swap(a[i], b[i]); }
+ });
+ } else {
+ auto v = reinterpret_cast<Vector<Offset<Table>> *>(
+ builder_.GetCurrentBufferPointer());
+ // Here also can't use std::sort. We do have an iterator type for it,
+ // but it is non-standard as it will dereference the offsets, and thus
+ // can't be used to swap elements.
+ if (key->value.type.base_type == BASE_TYPE_STRING) {
+ SimpleQsort<Offset<Table>>(
+ v->data(), v->data() + v->size(), 1,
+ [key](const Offset<Table> *_a, const Offset<Table> *_b) -> bool {
+ return CompareTablesByStringKey(_a, _b, *key);
+ },
+ SwapSerializedTables);
+ } else {
+ SimpleQsort<Offset<Table>>(
+ v->data(), v->data() + v->size(), 1,
+ [key](const Offset<Table> *_a, const Offset<Table> *_b) -> bool {
+ return CompareTablesByScalarKey(_a, _b, *key);
+ },
+ SwapSerializedTables);
+ }
+ }
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseArray(Value &array) {
+ std::vector<Value> stack;
+ FlatBufferBuilder builder;
+ const auto &type = array.type.VectorType();
+ auto length = array.type.fixed_length;
+ uoffset_t count = 0;
+ auto err = ParseVectorDelimiters(count, [&](uoffset_t &) -> CheckedError {
+ vector_emplace_back(&stack, Value());
+ auto &val = stack.back();
+ val.type = type;
+ if (IsStruct(type)) {
+ ECHECK(ParseTable(*val.type.struct_def, &val.constant, nullptr));
+ } else {
+ ECHECK(ParseSingleValue(nullptr, val, false));
+ }
+ return NoError();
+ });
+ ECHECK(err);
+ if (length != count) return Error("Fixed-length array size is incorrect.");
+
+ for (auto it = stack.rbegin(); it != stack.rend(); ++it) {
+ auto &val = *it;
+ // clang-format off
+ switch (val.type.base_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: \
+ if (IsStruct(val.type)) { \
+ SerializeStruct(builder, *val.type.struct_def, val); \
+ } else { \
+ CTYPE elem; \
+ ECHECK(atot(val.constant.c_str(), *this, &elem)); \
+ builder.PushElement(elem); \
+ } \
+ break;
+ FLATBUFFERS_GEN_TYPES(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ // clang-format on
+ }
+
+ array.constant.assign(
+ reinterpret_cast<const char *>(builder.GetCurrentBufferPointer()),
+ InlineSize(array.type));
+ return NoError();
+}
+
+CheckedError Parser::ParseNestedFlatbuffer(Value &val, FieldDef *field,
+ size_t fieldn,
+ const StructDef *parent_struct_def) {
+ if (token_ == '[') { // backwards compat for 'legacy' ubyte buffers
+ ECHECK(ParseAnyValue(val, field, fieldn, parent_struct_def, 0));
+ } else {
+ auto cursor_at_value_begin = cursor_;
+ ECHECK(SkipAnyJsonValue());
+ std::string substring(cursor_at_value_begin - 1, cursor_ - 1);
+
+ // Create and initialize new parser
+ Parser nested_parser;
+ FLATBUFFERS_ASSERT(field->nested_flatbuffer);
+ nested_parser.root_struct_def_ = field->nested_flatbuffer;
+ nested_parser.enums_ = enums_;
+ nested_parser.opts = opts;
+ nested_parser.uses_flexbuffers_ = uses_flexbuffers_;
+ nested_parser.parse_depth_counter_ = parse_depth_counter_;
+ // Parse JSON substring into new flatbuffer builder using nested_parser
+ bool ok = nested_parser.Parse(substring.c_str(), nullptr, nullptr);
+
+ // Clean nested_parser to avoid deleting the elements in
+ // the SymbolTables on destruction
+ nested_parser.enums_.dict.clear();
+ nested_parser.enums_.vec.clear();
+
+ if (!ok) { ECHECK(Error(nested_parser.error_)); }
+ // Force alignment for nested flatbuffer
+ builder_.ForceVectorAlignment(
+ nested_parser.builder_.GetSize(), sizeof(uint8_t),
+ nested_parser.builder_.GetBufferMinAlignment());
+
+ auto off = builder_.CreateVector(nested_parser.builder_.GetBufferPointer(),
+ nested_parser.builder_.GetSize());
+ val.constant = NumToString(off.o);
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseMetaData(SymbolTable<Value> *attributes) {
+ if (Is('(')) {
+ NEXT();
+ for (;;) {
+ auto name = attribute_;
+ if (false == (Is(kTokenIdentifier) || Is(kTokenStringConstant)))
+ return Error("attribute name must be either identifier or string: " +
+ name);
+ if (known_attributes_.find(name) == known_attributes_.end())
+ return Error("user define attributes must be declared before use: " +
+ name);
+ NEXT();
+ auto e = new Value();
+ if (attributes->Add(name, e)) Warning("attribute already found: " + name);
+ if (Is(':')) {
+ NEXT();
+ ECHECK(ParseSingleValue(&name, *e, true));
+ }
+ if (Is(')')) {
+ NEXT();
+ break;
+ }
+ EXPECT(',');
+ }
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseEnumFromString(const Type &type,
+ std::string *result) {
+ const auto base_type =
+ type.enum_def ? type.enum_def->underlying_type.base_type : type.base_type;
+ if (!IsInteger(base_type)) return Error("not a valid value for this field");
+ uint64_t u64 = 0;
+ for (size_t pos = 0; pos != std::string::npos;) {
+ const auto delim = attribute_.find_first_of(' ', pos);
+ const auto last = (std::string::npos == delim);
+ auto word = attribute_.substr(pos, !last ? delim - pos : std::string::npos);
+ pos = !last ? delim + 1 : std::string::npos;
+ const EnumVal *ev = nullptr;
+ if (type.enum_def) {
+ ev = type.enum_def->Lookup(word);
+ } else {
+ auto dot = word.find_first_of('.');
+ if (std::string::npos == dot)
+ return Error("enum values need to be qualified by an enum type");
+ auto enum_def_str = word.substr(0, dot);
+ const auto enum_def = LookupEnum(enum_def_str);
+ if (!enum_def) return Error("unknown enum: " + enum_def_str);
+ auto enum_val_str = word.substr(dot + 1);
+ ev = enum_def->Lookup(enum_val_str);
+ }
+ if (!ev) return Error("unknown enum value: " + word);
+ u64 |= ev->GetAsUInt64();
+ }
+ *result = IsUnsigned(base_type) ? NumToString(u64)
+ : NumToString(static_cast<int64_t>(u64));
+ return NoError();
+}
+
+CheckedError Parser::ParseHash(Value &e, FieldDef *field) {
+ FLATBUFFERS_ASSERT(field);
+ Value *hash_name = field->attributes.Lookup("hash");
+ switch (e.type.base_type) {
+ case BASE_TYPE_SHORT: {
+ auto hash = FindHashFunction16(hash_name->constant.c_str());
+ int16_t hashed_value = static_cast<int16_t>(hash(attribute_.c_str()));
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ case BASE_TYPE_USHORT: {
+ auto hash = FindHashFunction16(hash_name->constant.c_str());
+ uint16_t hashed_value = hash(attribute_.c_str());
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ case BASE_TYPE_INT: {
+ auto hash = FindHashFunction32(hash_name->constant.c_str());
+ int32_t hashed_value = static_cast<int32_t>(hash(attribute_.c_str()));
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ case BASE_TYPE_UINT: {
+ auto hash = FindHashFunction32(hash_name->constant.c_str());
+ uint32_t hashed_value = hash(attribute_.c_str());
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ case BASE_TYPE_LONG: {
+ auto hash = FindHashFunction64(hash_name->constant.c_str());
+ int64_t hashed_value = static_cast<int64_t>(hash(attribute_.c_str()));
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ case BASE_TYPE_ULONG: {
+ auto hash = FindHashFunction64(hash_name->constant.c_str());
+ uint64_t hashed_value = hash(attribute_.c_str());
+ e.constant = NumToString(hashed_value);
+ break;
+ }
+ default: FLATBUFFERS_ASSERT(0);
+ }
+ NEXT();
+ return NoError();
+}
+
+CheckedError Parser::TokenError() {
+ return Error("cannot parse value starting with: " + TokenToStringId(token_));
+}
+
+// Re-pack helper (ParseSingleValue) to normalize defaults of scalars.
+template<typename T> inline void SingleValueRepack(Value &e, T val) {
+ // Remove leading zeros.
+ if (IsInteger(e.type.base_type)) { e.constant = NumToString(val); }
+}
+#if defined(FLATBUFFERS_HAS_NEW_STRTOD) && (FLATBUFFERS_HAS_NEW_STRTOD > 0)
+// Normalize defaults NaN to unsigned quiet-NaN(0) if value was parsed from
+// hex-float literal.
+static inline void SingleValueRepack(Value &e, float val) {
+ if (val != val) e.constant = "nan";
+}
+static inline void SingleValueRepack(Value &e, double val) {
+ if (val != val) e.constant = "nan";
+}
+#endif
+
+CheckedError Parser::ParseFunction(const std::string *name, Value &e) {
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+
+ // Copy name, attribute will be changed on NEXT().
+ const auto functionname = attribute_;
+ if (!IsFloat(e.type.base_type)) {
+ return Error(functionname + ": type of argument mismatch, expecting: " +
+ kTypeNames[BASE_TYPE_DOUBLE] +
+ ", found: " + kTypeNames[e.type.base_type] +
+ ", name: " + (name ? *name : "") + ", value: " + e.constant);
+ }
+ NEXT();
+ EXPECT('(');
+ ECHECK(ParseSingleValue(name, e, false));
+ EXPECT(')');
+ // calculate with double precision
+ double x, y = 0.0;
+ ECHECK(atot(e.constant.c_str(), *this, &x));
+ // clang-format off
+ auto func_match = false;
+ #define FLATBUFFERS_FN_DOUBLE(name, op) \
+ if (!func_match && functionname == name) { y = op; func_match = true; }
+ FLATBUFFERS_FN_DOUBLE("deg", x / kPi * 180);
+ FLATBUFFERS_FN_DOUBLE("rad", x * kPi / 180);
+ FLATBUFFERS_FN_DOUBLE("sin", sin(x));
+ FLATBUFFERS_FN_DOUBLE("cos", cos(x));
+ FLATBUFFERS_FN_DOUBLE("tan", tan(x));
+ FLATBUFFERS_FN_DOUBLE("asin", asin(x));
+ FLATBUFFERS_FN_DOUBLE("acos", acos(x));
+ FLATBUFFERS_FN_DOUBLE("atan", atan(x));
+ // TODO(wvo): add more useful conversion functions here.
+ #undef FLATBUFFERS_FN_DOUBLE
+ // clang-format on
+ if (true != func_match) {
+ return Error(std::string("Unknown conversion function: ") + functionname +
+ ", field name: " + (name ? *name : "") +
+ ", value: " + e.constant);
+ }
+ e.constant = NumToString(y);
+ return NoError();
+}
+
+CheckedError Parser::TryTypedValue(const std::string *name, int dtoken,
+ bool check, Value &e, BaseType req,
+ bool *destmatch) {
+ FLATBUFFERS_ASSERT(*destmatch == false && dtoken == token_);
+ *destmatch = true;
+ e.constant = attribute_;
+ // Check token match
+ if (!check) {
+ if (e.type.base_type == BASE_TYPE_NONE) {
+ e.type.base_type = req;
+ } else {
+ return Error(std::string("type mismatch: expecting: ") +
+ kTypeNames[e.type.base_type] +
+ ", found: " + kTypeNames[req] +
+ ", name: " + (name ? *name : "") + ", value: " + e.constant);
+ }
+ }
+ // The exponent suffix of hexadecimal float-point number is mandatory.
+ // A hex-integer constant is forbidden as an initializer of float number.
+ if ((kTokenFloatConstant != dtoken) && IsFloat(e.type.base_type)) {
+ const auto &s = e.constant;
+ const auto k = s.find_first_of("0123456789.");
+ if ((std::string::npos != k) && (s.length() > (k + 1)) &&
+ (s[k] == '0' && is_alpha_char(s[k + 1], 'X')) &&
+ (std::string::npos == s.find_first_of("pP", k + 2))) {
+ return Error(
+ "invalid number, the exponent suffix of hexadecimal "
+ "floating-point literals is mandatory: \"" +
+ s + "\"");
+ }
+ }
+ NEXT();
+ return NoError();
+}
+
+CheckedError Parser::ParseSingleValue(const std::string *name, Value &e,
+ bool check_now) {
+ if (token_ == '+' || token_ == '-') {
+ const char sign = static_cast<char>(token_);
+ // Get an indentifier: NAN, INF, or function name like cos/sin/deg.
+ NEXT();
+ if (token_ != kTokenIdentifier) return Error("constant name expected");
+ attribute_.insert(size_t(0), size_t(1), sign);
+ }
+
+ const auto in_type = e.type.base_type;
+ const auto is_tok_ident = (token_ == kTokenIdentifier);
+ const auto is_tok_string = (token_ == kTokenStringConstant);
+
+ // First see if this could be a conversion function.
+ if (is_tok_ident && *cursor_ == '(') { return ParseFunction(name, e); }
+
+ // clang-format off
+ auto match = false;
+
+ #define IF_ECHECK_(force, dtoken, check, req) \
+ if (!match && ((dtoken) == token_) && ((check) || IsConstTrue(force))) \
+ ECHECK(TryTypedValue(name, dtoken, check, e, req, &match))
+ #define TRY_ECHECK(dtoken, check, req) IF_ECHECK_(false, dtoken, check, req)
+ #define FORCE_ECHECK(dtoken, check, req) IF_ECHECK_(true, dtoken, check, req)
+ // clang-format on
+
+ if (is_tok_ident || is_tok_string) {
+ const auto kTokenStringOrIdent = token_;
+ // The string type is a most probable type, check it first.
+ TRY_ECHECK(kTokenStringConstant, in_type == BASE_TYPE_STRING,
+ BASE_TYPE_STRING);
+
+ // avoid escaped and non-ascii in the string
+ if (!match && is_tok_string && IsScalar(in_type) &&
+ !attr_is_trivial_ascii_string_) {
+ return Error(
+ std::string("type mismatch or invalid value, an initializer of "
+ "non-string field must be trivial ASCII string: type: ") +
+ kTypeNames[in_type] + ", name: " + (name ? *name : "") +
+ ", value: " + attribute_);
+ }
+
+ // A boolean as true/false. Boolean as Integer check below.
+ if (!match && IsBool(in_type)) {
+ auto is_true = attribute_ == "true";
+ if (is_true || attribute_ == "false") {
+ attribute_ = is_true ? "1" : "0";
+ // accepts both kTokenStringConstant and kTokenIdentifier
+ TRY_ECHECK(kTokenStringOrIdent, IsBool(in_type), BASE_TYPE_BOOL);
+ }
+ }
+ // Check for optional scalars.
+ if (!match && IsScalar(in_type) && attribute_ == "null") {
+ e.constant = "null";
+ NEXT();
+ match = true;
+ }
+ // Check if this could be a string/identifier enum value.
+ // Enum can have only true integer base type.
+ if (!match && IsInteger(in_type) && !IsBool(in_type) &&
+ IsIdentifierStart(*attribute_.c_str())) {
+ ECHECK(ParseEnumFromString(e.type, &e.constant));
+ NEXT();
+ match = true;
+ }
+ // Parse a float/integer number from the string.
+ // A "scalar-in-string" value needs extra checks.
+ if (!match && is_tok_string && IsScalar(in_type)) {
+ // Strip trailing whitespaces from attribute_.
+ auto last_non_ws = attribute_.find_last_not_of(' ');
+ if (std::string::npos != last_non_ws) attribute_.resize(last_non_ws + 1);
+ if (IsFloat(e.type.base_type)) {
+ // The functions strtod() and strtof() accept both 'nan' and
+ // 'nan(number)' literals. While 'nan(number)' is rejected by the parser
+ // as an unsupported function if is_tok_ident is true.
+ if (attribute_.find_last_of(')') != std::string::npos) {
+ return Error("invalid number: " + attribute_);
+ }
+ }
+ }
+ // Float numbers or nan, inf, pi, etc.
+ TRY_ECHECK(kTokenStringOrIdent, IsFloat(in_type), BASE_TYPE_FLOAT);
+ // An integer constant in string.
+ TRY_ECHECK(kTokenStringOrIdent, IsInteger(in_type), BASE_TYPE_INT);
+ // Unknown tokens will be interpreted as string type.
+ // An attribute value may be a scalar or string constant.
+ FORCE_ECHECK(kTokenStringConstant, in_type == BASE_TYPE_STRING,
+ BASE_TYPE_STRING);
+ } else {
+ // Try a float number.
+ TRY_ECHECK(kTokenFloatConstant, IsFloat(in_type), BASE_TYPE_FLOAT);
+ // Integer token can init any scalar (integer of float).
+ FORCE_ECHECK(kTokenIntegerConstant, IsScalar(in_type), BASE_TYPE_INT);
+ }
+ // Match empty vectors for default-empty-vectors.
+ if (!match && IsVector(e.type) && token_ == '[') {
+ NEXT();
+ if (token_ != ']') { return Error("Expected `]` in vector default"); }
+ NEXT();
+ match = true;
+ e.constant = "[]";
+ }
+
+#undef FORCE_ECHECK
+#undef TRY_ECHECK
+#undef IF_ECHECK_
+
+ if (!match) {
+ std::string msg;
+ msg += "Cannot assign token starting with '" + TokenToStringId(token_) +
+ "' to value of <" + std::string(kTypeNames[in_type]) + "> type.";
+ return Error(msg);
+ }
+ const auto match_type = e.type.base_type; // may differ from in_type
+ // The check_now flag must be true when parse a fbs-schema.
+ // This flag forces to check default scalar values or metadata of field.
+ // For JSON parser the flag should be false.
+ // If it is set for JSON each value will be checked twice (see ParseTable).
+ // Special case 'null' since atot can't handle that.
+ if (check_now && IsScalar(match_type) && e.constant != "null") {
+ // clang-format off
+ switch (match_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_ ## ENUM: {\
+ CTYPE val; \
+ ECHECK(atot(e.constant.c_str(), *this, &val)); \
+ SingleValueRepack(e, val); \
+ break; }
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ default: break;
+ }
+ // clang-format on
+ }
+ return NoError();
+}
+
+StructDef *Parser::LookupCreateStruct(const std::string &name,
+ bool create_if_new, bool definition) {
+ std::string qualified_name = current_namespace_->GetFullyQualifiedName(name);
+ // See if it exists pre-declared by an unqualified use.
+ auto struct_def = LookupStruct(name);
+ if (struct_def && struct_def->predecl) {
+ if (definition) {
+ // Make sure it has the current namespace, and is registered under its
+ // qualified name.
+ struct_def->defined_namespace = current_namespace_;
+ structs_.Move(name, qualified_name);
+ }
+ return struct_def;
+ }
+ // See if it exists pre-declared by an qualified use.
+ struct_def = LookupStruct(qualified_name);
+ if (struct_def && struct_def->predecl) {
+ if (definition) {
+ // Make sure it has the current namespace.
+ struct_def->defined_namespace = current_namespace_;
+ }
+ return struct_def;
+ }
+ if (!definition && !struct_def) {
+ struct_def = LookupStructThruParentNamespaces(name);
+ }
+ if (!struct_def && create_if_new) {
+ struct_def = new StructDef();
+ if (definition) {
+ structs_.Add(qualified_name, struct_def);
+ struct_def->name = name;
+ struct_def->defined_namespace = current_namespace_;
+ } else {
+ // Not a definition.
+ // Rather than failing, we create a "pre declared" StructDef, due to
+ // circular references, and check for errors at the end of parsing.
+ // It is defined in the current namespace, as the best guess what the
+ // final namespace will be.
+ structs_.Add(name, struct_def);
+ struct_def->name = name;
+ struct_def->defined_namespace = current_namespace_;
+ struct_def->original_location.reset(
+ new std::string(file_being_parsed_ + ":" + NumToString(line_)));
+ }
+ }
+ return struct_def;
+}
+
+const EnumVal *EnumDef::MinValue() const {
+ return vals.vec.empty() ? nullptr : vals.vec.front();
+}
+const EnumVal *EnumDef::MaxValue() const {
+ return vals.vec.empty() ? nullptr : vals.vec.back();
+}
+
+template<typename T> static uint64_t EnumDistanceImpl(T e1, T e2) {
+ if (e1 < e2) { std::swap(e1, e2); } // use std for scalars
+ // Signed overflow may occur, use unsigned calculation.
+ // The unsigned overflow is well-defined by C++ standard (modulo 2^n).
+ return static_cast<uint64_t>(e1) - static_cast<uint64_t>(e2);
+}
+
+uint64_t EnumDef::Distance(const EnumVal *v1, const EnumVal *v2) const {
+ return IsUInt64() ? EnumDistanceImpl(v1->GetAsUInt64(), v2->GetAsUInt64())
+ : EnumDistanceImpl(v1->GetAsInt64(), v2->GetAsInt64());
+}
+
+std::string EnumDef::AllFlags() const {
+ FLATBUFFERS_ASSERT(attributes.Lookup("bit_flags"));
+ uint64_t u64 = 0;
+ for (auto it = Vals().begin(); it != Vals().end(); ++it) {
+ u64 |= (*it)->GetAsUInt64();
+ }
+ return IsUInt64() ? NumToString(u64) : NumToString(static_cast<int64_t>(u64));
+}
+
+EnumVal *EnumDef::ReverseLookup(int64_t enum_idx,
+ bool skip_union_default) const {
+ auto skip_first = static_cast<int>(is_union && skip_union_default);
+ for (auto it = Vals().begin() + skip_first; it != Vals().end(); ++it) {
+ if ((*it)->GetAsInt64() == enum_idx) { return *it; }
+ }
+ return nullptr;
+}
+
+EnumVal *EnumDef::FindByValue(const std::string &constant) const {
+ int64_t i64;
+ auto done = false;
+ if (IsUInt64()) {
+ uint64_t u64; // avoid reinterpret_cast of pointers
+ done = StringToNumber(constant.c_str(), &u64);
+ i64 = static_cast<int64_t>(u64);
+ } else {
+ done = StringToNumber(constant.c_str(), &i64);
+ }
+ FLATBUFFERS_ASSERT(done);
+ if (!done) return nullptr;
+ return ReverseLookup(i64, false);
+}
+
+void EnumDef::SortByValue() {
+ auto &v = vals.vec;
+ if (IsUInt64())
+ std::sort(v.begin(), v.end(), [](const EnumVal *e1, const EnumVal *e2) {
+ return e1->GetAsUInt64() < e2->GetAsUInt64();
+ });
+ else
+ std::sort(v.begin(), v.end(), [](const EnumVal *e1, const EnumVal *e2) {
+ return e1->GetAsInt64() < e2->GetAsInt64();
+ });
+}
+
+void EnumDef::RemoveDuplicates() {
+ // This method depends form SymbolTable implementation!
+ // 1) vals.vec - owner (raw pointer)
+ // 2) vals.dict - access map
+ auto first = vals.vec.begin();
+ auto last = vals.vec.end();
+ if (first == last) return;
+ auto result = first;
+ while (++first != last) {
+ if ((*result)->value != (*first)->value) {
+ *(++result) = *first;
+ } else {
+ auto ev = *first;
+ for (auto it = vals.dict.begin(); it != vals.dict.end(); ++it) {
+ if (it->second == ev) it->second = *result; // reassign
+ }
+ delete ev; // delete enum value
+ *first = nullptr;
+ }
+ }
+ vals.vec.erase(++result, last);
+}
+
+template<typename T> void EnumDef::ChangeEnumValue(EnumVal *ev, T new_value) {
+ ev->value = static_cast<int64_t>(new_value);
+}
+
+namespace EnumHelper {
+template<BaseType E> struct EnumValType { typedef int64_t type; };
+template<> struct EnumValType<BASE_TYPE_ULONG> { typedef uint64_t type; };
+} // namespace EnumHelper
+
+struct EnumValBuilder {
+ EnumVal *CreateEnumerator(const std::string &ev_name) {
+ FLATBUFFERS_ASSERT(!temp);
+ auto first = enum_def.vals.vec.empty();
+ user_value = first;
+ temp = new EnumVal(ev_name, first ? 0 : enum_def.vals.vec.back()->value);
+ return temp;
+ }
+
+ EnumVal *CreateEnumerator(const std::string &ev_name, int64_t val) {
+ FLATBUFFERS_ASSERT(!temp);
+ user_value = true;
+ temp = new EnumVal(ev_name, val);
+ return temp;
+ }
+
+ FLATBUFFERS_CHECKED_ERROR AcceptEnumerator(const std::string &name) {
+ FLATBUFFERS_ASSERT(temp);
+ ECHECK(ValidateValue(&temp->value, false == user_value));
+ FLATBUFFERS_ASSERT((temp->union_type.enum_def == nullptr) ||
+ (temp->union_type.enum_def == &enum_def));
+ auto not_unique = enum_def.vals.Add(name, temp);
+ temp = nullptr;
+ if (not_unique) return parser.Error("enum value already exists: " + name);
+ return NoError();
+ }
+
+ FLATBUFFERS_CHECKED_ERROR AcceptEnumerator() {
+ return AcceptEnumerator(temp->name);
+ }
+
+ FLATBUFFERS_CHECKED_ERROR AssignEnumeratorValue(const std::string &value) {
+ user_value = true;
+ auto fit = false;
+ if (enum_def.IsUInt64()) {
+ uint64_t u64;
+ fit = StringToNumber(value.c_str(), &u64);
+ temp->value = static_cast<int64_t>(u64); // well-defined since C++20.
+ } else {
+ int64_t i64;
+ fit = StringToNumber(value.c_str(), &i64);
+ temp->value = i64;
+ }
+ if (!fit) return parser.Error("enum value does not fit, \"" + value + "\"");
+ return NoError();
+ }
+
+ template<BaseType E, typename CTYPE>
+ inline FLATBUFFERS_CHECKED_ERROR ValidateImpl(int64_t *ev, int m) {
+ typedef typename EnumHelper::EnumValType<E>::type T; // int64_t or uint64_t
+ static_assert(sizeof(T) == sizeof(int64_t), "invalid EnumValType");
+ const auto v = static_cast<T>(*ev);
+ auto up = static_cast<T>((flatbuffers::numeric_limits<CTYPE>::max)());
+ auto dn = static_cast<T>((flatbuffers::numeric_limits<CTYPE>::lowest)());
+ if (v < dn || v > (up - m)) {
+ return parser.Error("enum value does not fit, \"" + NumToString(v) +
+ (m ? " + 1\"" : "\"") + " out of " +
+ TypeToIntervalString<CTYPE>());
+ }
+ *ev = static_cast<int64_t>(v + m); // well-defined since C++20.
+ return NoError();
+ }
+
+ FLATBUFFERS_CHECKED_ERROR ValidateValue(int64_t *ev, bool next) {
+ // clang-format off
+ switch (enum_def.underlying_type.base_type) {
+ #define FLATBUFFERS_TD(ENUM, IDLTYPE, CTYPE, ...) \
+ case BASE_TYPE_##ENUM: { \
+ if (!IsInteger(BASE_TYPE_##ENUM)) break; \
+ return ValidateImpl<BASE_TYPE_##ENUM, CTYPE>(ev, next ? 1 : 0); \
+ }
+ FLATBUFFERS_GEN_TYPES_SCALAR(FLATBUFFERS_TD)
+ #undef FLATBUFFERS_TD
+ default: break;
+ }
+ // clang-format on
+ return parser.Error("fatal: invalid enum underlying type");
+ }
+
+ EnumValBuilder(Parser &_parser, EnumDef &_enum_def)
+ : parser(_parser),
+ enum_def(_enum_def),
+ temp(nullptr),
+ user_value(false) {}
+
+ ~EnumValBuilder() { delete temp; }
+
+ Parser &parser;
+ EnumDef &enum_def;
+ EnumVal *temp;
+ bool user_value;
+};
+
+CheckedError Parser::ParseEnum(const bool is_union, EnumDef **dest) {
+ std::vector<std::string> enum_comment = doc_comment_;
+ NEXT();
+ std::string enum_name = attribute_;
+ EXPECT(kTokenIdentifier);
+ EnumDef *enum_def;
+ ECHECK(StartEnum(enum_name, is_union, &enum_def));
+ enum_def->doc_comment = enum_comment;
+ if (!is_union && !opts.proto_mode) {
+ // Give specialized error message, since this type spec used to
+ // be optional in the first FlatBuffers release.
+ if (!Is(':')) {
+ return Error(
+ "must specify the underlying integer type for this"
+ " enum (e.g. \': short\', which was the default).");
+ } else {
+ NEXT();
+ }
+ // Specify the integer type underlying this enum.
+ ECHECK(ParseType(enum_def->underlying_type));
+ if (!IsInteger(enum_def->underlying_type.base_type) ||
+ IsBool(enum_def->underlying_type.base_type))
+ return Error("underlying enum type must be integral");
+ // Make this type refer back to the enum it was derived from.
+ enum_def->underlying_type.enum_def = enum_def;
+ }
+ ECHECK(ParseMetaData(&enum_def->attributes));
+ const auto underlying_type = enum_def->underlying_type.base_type;
+ if (enum_def->attributes.Lookup("bit_flags") &&
+ !IsUnsigned(underlying_type)) {
+ // todo: Convert to the Error in the future?
+ Warning("underlying type of bit_flags enum must be unsigned");
+ }
+ EnumValBuilder evb(*this, *enum_def);
+ EXPECT('{');
+ // A lot of code generatos expect that an enum is not-empty.
+ if ((is_union || Is('}')) && !opts.proto_mode) {
+ evb.CreateEnumerator("NONE");
+ ECHECK(evb.AcceptEnumerator());
+ }
+ std::set<std::pair<BaseType, StructDef *>> union_types;
+ while (!Is('}')) {
+ if (opts.proto_mode && attribute_ == "option") {
+ ECHECK(ParseProtoOption());
+ } else {
+ auto &ev = *evb.CreateEnumerator(attribute_);
+ auto full_name = ev.name;
+ ev.doc_comment = doc_comment_;
+ EXPECT(kTokenIdentifier);
+ if (is_union) {
+ ECHECK(ParseNamespacing(&full_name, &ev.name));
+ if (opts.union_value_namespacing) {
+ // Since we can't namespace the actual enum identifiers, turn
+ // namespace parts into part of the identifier.
+ ev.name = full_name;
+ std::replace(ev.name.begin(), ev.name.end(), '.', '_');
+ }
+ if (Is(':')) {
+ NEXT();
+ ECHECK(ParseType(ev.union_type));
+ if (ev.union_type.base_type != BASE_TYPE_STRUCT &&
+ ev.union_type.base_type != BASE_TYPE_STRING)
+ return Error("union value type may only be table/struct/string");
+ } else {
+ ev.union_type = Type(BASE_TYPE_STRUCT, LookupCreateStruct(full_name));
+ }
+ if (!enum_def->uses_multiple_type_instances) {
+ auto ins = union_types.insert(std::make_pair(
+ ev.union_type.base_type, ev.union_type.struct_def));
+ enum_def->uses_multiple_type_instances = (false == ins.second);
+ }
+ }
+
+ if (Is('=')) {
+ NEXT();
+ ECHECK(evb.AssignEnumeratorValue(attribute_));
+ EXPECT(kTokenIntegerConstant);
+ }
+
+ ECHECK(evb.AcceptEnumerator());
+
+ if (opts.proto_mode && Is('[')) {
+ NEXT();
+ // ignore attributes on enums.
+ while (token_ != ']') NEXT();
+ NEXT();
+ }
+ }
+ if (!Is(opts.proto_mode ? ';' : ',')) break;
+ NEXT();
+ }
+ EXPECT('}');
+
+ // At this point, the enum can be empty if input is invalid proto-file.
+ if (!enum_def->size())
+ return Error("incomplete enum declaration, values not found");
+
+ if (enum_def->attributes.Lookup("bit_flags")) {
+ const auto base_width = static_cast<uint64_t>(8 * SizeOf(underlying_type));
+ for (auto it = enum_def->Vals().begin(); it != enum_def->Vals().end();
+ ++it) {
+ auto ev = *it;
+ const auto u = ev->GetAsUInt64();
+ // Stop manipulations with the sign.
+ if (!IsUnsigned(underlying_type) && u == (base_width - 1))
+ return Error("underlying type of bit_flags enum must be unsigned");
+ if (u >= base_width)
+ return Error("bit flag out of range of underlying integral type");
+ enum_def->ChangeEnumValue(ev, 1ULL << u);
+ }
+ }
+
+ enum_def->SortByValue(); // Must be sorted to use MinValue/MaxValue.
+
+ // Ensure enum value uniqueness.
+ auto prev_it = enum_def->Vals().begin();
+ for (auto it = prev_it + 1; it != enum_def->Vals().end(); ++it) {
+ auto prev_ev = *prev_it;
+ auto ev = *it;
+ if (prev_ev->GetAsUInt64() == ev->GetAsUInt64())
+ return Error("all enum values must be unique: " + prev_ev->name +
+ " and " + ev->name + " are both " +
+ NumToString(ev->GetAsInt64()));
+ }
+
+ if (dest) *dest = enum_def;
+ types_.Add(current_namespace_->GetFullyQualifiedName(enum_def->name),
+ new Type(BASE_TYPE_UNION, nullptr, enum_def));
+ return NoError();
+}
+
+CheckedError Parser::StartStruct(const std::string &name, StructDef **dest) {
+ auto &struct_def = *LookupCreateStruct(name, true, true);
+ if (!struct_def.predecl) return Error("datatype already exists: " + name);
+ struct_def.predecl = false;
+ struct_def.name = name;
+ struct_def.file = file_being_parsed_;
+ // Move this struct to the back of the vector just in case it was predeclared,
+ // to preserve declaration order.
+ *std::remove(structs_.vec.begin(), structs_.vec.end(), &struct_def) =
+ &struct_def;
+ *dest = &struct_def;
+ return NoError();
+}
+
+CheckedError Parser::CheckClash(std::vector<FieldDef *> &fields,
+ StructDef *struct_def, const char *suffix,
+ BaseType basetype) {
+ auto len = strlen(suffix);
+ for (auto it = fields.begin(); it != fields.end(); ++it) {
+ auto &fname = (*it)->name;
+ if (fname.length() > len &&
+ fname.compare(fname.length() - len, len, suffix) == 0 &&
+ (*it)->value.type.base_type != BASE_TYPE_UTYPE) {
+ auto field =
+ struct_def->fields.Lookup(fname.substr(0, fname.length() - len));
+ if (field && field->value.type.base_type == basetype)
+ return Error("Field " + fname +
+ " would clash with generated functions for field " +
+ field->name);
+ }
+ }
+ return NoError();
+}
+
+bool Parser::SupportsOptionalScalars(const flatbuffers::IDLOptions &opts) {
+ static FLATBUFFERS_CONSTEXPR unsigned long supported_langs =
+ IDLOptions::kRust | IDLOptions::kSwift | IDLOptions::kLobster |
+ IDLOptions::kKotlin | IDLOptions::kCpp | IDLOptions::kJava |
+ IDLOptions::kCSharp | IDLOptions::kTs | IDLOptions::kBinary;
+ unsigned long langs = opts.lang_to_generate;
+ return (langs > 0 && langs < IDLOptions::kMAX) && !(langs & ~supported_langs);
+}
+bool Parser::SupportsOptionalScalars() const {
+ // Check in general if a language isn't specified.
+ return opts.lang_to_generate == 0 || SupportsOptionalScalars(opts);
+}
+
+bool Parser::SupportsDefaultVectorsAndStrings() const {
+ static FLATBUFFERS_CONSTEXPR unsigned long supported_langs =
+ IDLOptions::kRust | IDLOptions::kSwift;
+ return !(opts.lang_to_generate & ~supported_langs);
+}
+
+bool Parser::SupportsAdvancedUnionFeatures() const {
+ return opts.lang_to_generate != 0 &&
+ (opts.lang_to_generate &
+ ~(IDLOptions::kCpp | IDLOptions::kTs | IDLOptions::kPhp |
+ IDLOptions::kJava | IDLOptions::kCSharp | IDLOptions::kKotlin |
+ IDLOptions::kBinary | IDLOptions::kSwift)) == 0;
+}
+
+bool Parser::SupportsAdvancedArrayFeatures() const {
+ return (opts.lang_to_generate &
+ ~(IDLOptions::kCpp | IDLOptions::kPython | IDLOptions::kJava |
+ IDLOptions::kCSharp | IDLOptions::kJsonSchema | IDLOptions::kJson |
+ IDLOptions::kBinary | IDLOptions::kRust)) == 0;
+}
+
+Namespace *Parser::UniqueNamespace(Namespace *ns) {
+ for (auto it = namespaces_.begin(); it != namespaces_.end(); ++it) {
+ if (ns->components == (*it)->components) {
+ delete ns;
+ return *it;
+ }
+ }
+ namespaces_.push_back(ns);
+ return ns;
+}
+
+std::string Parser::UnqualifiedName(const std::string &full_qualified_name) {
+ Namespace *ns = new Namespace();
+
+ std::size_t current, previous = 0;
+ current = full_qualified_name.find('.');
+ while (current != std::string::npos) {
+ ns->components.push_back(
+ full_qualified_name.substr(previous, current - previous));
+ previous = current + 1;
+ current = full_qualified_name.find('.', previous);
+ }
+ current_namespace_ = UniqueNamespace(ns);
+ return full_qualified_name.substr(previous, current - previous);
+}
+
+static bool compareFieldDefs(const FieldDef *a, const FieldDef *b) {
+ auto a_id = atoi(a->attributes.Lookup("id")->constant.c_str());
+ auto b_id = atoi(b->attributes.Lookup("id")->constant.c_str());
+ return a_id < b_id;
+}
+
+CheckedError Parser::ParseDecl() {
+ std::vector<std::string> dc = doc_comment_;
+ bool fixed = IsIdent("struct");
+ if (!fixed && !IsIdent("table")) return Error("declaration expected");
+ NEXT();
+ std::string name = attribute_;
+ EXPECT(kTokenIdentifier);
+ StructDef *struct_def;
+ ECHECK(StartStruct(name, &struct_def));
+ struct_def->doc_comment = dc;
+ struct_def->fixed = fixed;
+ ECHECK(ParseMetaData(&struct_def->attributes));
+ struct_def->sortbysize =
+ struct_def->attributes.Lookup("original_order") == nullptr && !fixed;
+ EXPECT('{');
+ while (token_ != '}') ECHECK(ParseField(*struct_def));
+ if (fixed) {
+ const auto force_align = struct_def->attributes.Lookup("force_align");
+ if (force_align) {
+ size_t align;
+ ECHECK(ParseAlignAttribute(force_align->constant, struct_def->minalign,
+ &align));
+ struct_def->minalign = align;
+ }
+ if (!struct_def->bytesize) return Error("size 0 structs not allowed");
+ }
+ struct_def->PadLastField(struct_def->minalign);
+ // Check if this is a table that has manual id assignments
+ auto &fields = struct_def->fields.vec;
+ if (!fixed && fields.size()) {
+ size_t num_id_fields = 0;
+ for (auto it = fields.begin(); it != fields.end(); ++it) {
+ if ((*it)->attributes.Lookup("id")) num_id_fields++;
+ }
+ // If any fields have ids..
+ if (num_id_fields || opts.require_explicit_ids) {
+ // Then all fields must have them.
+ if (num_id_fields != fields.size()) {
+ if (opts.require_explicit_ids) {
+ return Error(
+ "all fields must have an 'id' attribute when "
+ "--require-explicit-ids is used");
+ } else {
+ return Error(
+ "either all fields or no fields must have an 'id' attribute");
+ }
+ }
+ // Simply sort by id, then the fields are the same as if no ids had
+ // been specified.
+ std::sort(fields.begin(), fields.end(), compareFieldDefs);
+ // Verify we have a contiguous set, and reassign vtable offsets.
+ FLATBUFFERS_ASSERT(fields.size() <=
+ flatbuffers::numeric_limits<voffset_t>::max());
+ for (voffset_t i = 0; i < static_cast<voffset_t>(fields.size()); i++) {
+ auto &field = *fields[i];
+ const auto &id_str = field.attributes.Lookup("id")->constant;
+ // Metadata values have a dynamic type, they can be `float`, 'int', or
+ // 'string`.
+ // The FieldIndexToOffset(i) expects the voffset_t so `id` is limited by
+ // this type.
+ voffset_t id = 0;
+ const auto done = !atot(id_str.c_str(), *this, &id).Check();
+ if (!done)
+ return Error("field id\'s must be non-negative number, field: " +
+ field.name + ", id: " + id_str);
+ if (i != id)
+ return Error("field id\'s must be consecutive from 0, id " +
+ NumToString(i) + " missing or set twice, field: " +
+ field.name + ", id: " + id_str);
+ field.value.offset = FieldIndexToOffset(i);
+ }
+ }
+ }
+
+ ECHECK(
+ CheckClash(fields, struct_def, UnionTypeFieldSuffix(), BASE_TYPE_UNION));
+ ECHECK(CheckClash(fields, struct_def, "Type", BASE_TYPE_UNION));
+ ECHECK(CheckClash(fields, struct_def, "_length", BASE_TYPE_VECTOR));
+ ECHECK(CheckClash(fields, struct_def, "Length", BASE_TYPE_VECTOR));
+ ECHECK(CheckClash(fields, struct_def, "_byte_vector", BASE_TYPE_STRING));
+ ECHECK(CheckClash(fields, struct_def, "ByteVector", BASE_TYPE_STRING));
+ EXPECT('}');
+ types_.Add(current_namespace_->GetFullyQualifiedName(struct_def->name),
+ new Type(BASE_TYPE_STRUCT, struct_def, nullptr));
+ return NoError();
+}
+
+CheckedError Parser::ParseService() {
+ std::vector<std::string> service_comment = doc_comment_;
+ NEXT();
+ auto service_name = attribute_;
+ EXPECT(kTokenIdentifier);
+ auto &service_def = *new ServiceDef();
+ service_def.name = service_name;
+ service_def.file = file_being_parsed_;
+ service_def.doc_comment = service_comment;
+ service_def.defined_namespace = current_namespace_;
+ if (services_.Add(current_namespace_->GetFullyQualifiedName(service_name),
+ &service_def))
+ return Error("service already exists: " + service_name);
+ ECHECK(ParseMetaData(&service_def.attributes));
+ EXPECT('{');
+ do {
+ std::vector<std::string> doc_comment = doc_comment_;
+ auto rpc_name = attribute_;
+ EXPECT(kTokenIdentifier);
+ EXPECT('(');
+ Type reqtype, resptype;
+ ECHECK(ParseTypeIdent(reqtype));
+ EXPECT(')');
+ EXPECT(':');
+ ECHECK(ParseTypeIdent(resptype));
+ if (reqtype.base_type != BASE_TYPE_STRUCT || reqtype.struct_def->fixed ||
+ resptype.base_type != BASE_TYPE_STRUCT || resptype.struct_def->fixed)
+ return Error("rpc request and response types must be tables");
+ auto &rpc = *new RPCCall();
+ rpc.name = rpc_name;
+ rpc.request = reqtype.struct_def;
+ rpc.response = resptype.struct_def;
+ rpc.doc_comment = doc_comment;
+ if (service_def.calls.Add(rpc_name, &rpc))
+ return Error("rpc already exists: " + rpc_name);
+ ECHECK(ParseMetaData(&rpc.attributes));
+ EXPECT(';');
+ } while (token_ != '}');
+ NEXT();
+ return NoError();
+}
+
+bool Parser::SetRootType(const char *name) {
+ root_struct_def_ = LookupStruct(name);
+ if (!root_struct_def_)
+ root_struct_def_ =
+ LookupStruct(current_namespace_->GetFullyQualifiedName(name));
+ return root_struct_def_ != nullptr;
+}
+
+void Parser::MarkGenerated() {
+ // This function marks all existing definitions as having already
+ // been generated, which signals no code for included files should be
+ // generated.
+ for (auto it = enums_.vec.begin(); it != enums_.vec.end(); ++it) {
+ (*it)->generated = true;
+ }
+ for (auto it = structs_.vec.begin(); it != structs_.vec.end(); ++it) {
+ if (!(*it)->predecl) { (*it)->generated = true; }
+ }
+ for (auto it = services_.vec.begin(); it != services_.vec.end(); ++it) {
+ (*it)->generated = true;
+ }
+}
+
+CheckedError Parser::ParseNamespace() {
+ NEXT();
+ auto ns = new Namespace();
+ namespaces_.push_back(ns); // Store it here to not leak upon error.
+ if (token_ != ';') {
+ for (;;) {
+ ns->components.push_back(attribute_);
+ EXPECT(kTokenIdentifier);
+ if (Is('.')) NEXT() else break;
+ }
+ }
+ namespaces_.pop_back();
+ current_namespace_ = UniqueNamespace(ns);
+ EXPECT(';');
+ return NoError();
+}
+
+// Best effort parsing of .proto declarations, with the aim to turn them
+// in the closest corresponding FlatBuffer equivalent.
+// We parse everything as identifiers instead of keywords, since we don't
+// want protobuf keywords to become invalid identifiers in FlatBuffers.
+CheckedError Parser::ParseProtoDecl() {
+ bool isextend = IsIdent("extend");
+ if (IsIdent("package")) {
+ // These are identical in syntax to FlatBuffer's namespace decl.
+ ECHECK(ParseNamespace());
+ } else if (IsIdent("message") || isextend) {
+ std::vector<std::string> struct_comment = doc_comment_;
+ NEXT();
+ StructDef *struct_def = nullptr;
+ Namespace *parent_namespace = nullptr;
+ if (isextend) {
+ if (Is('.')) NEXT(); // qualified names may start with a . ?
+ auto id = attribute_;
+ EXPECT(kTokenIdentifier);
+ ECHECK(ParseNamespacing(&id, nullptr));
+ struct_def = LookupCreateStruct(id, false);
+ if (!struct_def)
+ return Error("cannot extend unknown message type: " + id);
+ } else {
+ std::string name = attribute_;
+ EXPECT(kTokenIdentifier);
+ ECHECK(StartStruct(name, &struct_def));
+ // Since message definitions can be nested, we create a new namespace.
+ auto ns = new Namespace();
+ // Copy of current namespace.
+ *ns = *current_namespace_;
+ // But with current message name.
+ ns->components.push_back(name);
+ ns->from_table++;
+ parent_namespace = current_namespace_;
+ current_namespace_ = UniqueNamespace(ns);
+ }
+ struct_def->doc_comment = struct_comment;
+ ECHECK(ParseProtoFields(struct_def, isextend, false));
+ if (!isextend) { current_namespace_ = parent_namespace; }
+ if (Is(';')) NEXT();
+ } else if (IsIdent("enum")) {
+ // These are almost the same, just with different terminator:
+ EnumDef *enum_def;
+ ECHECK(ParseEnum(false, &enum_def));
+ if (Is(';')) NEXT();
+ // Temp: remove any duplicates, as .fbs files can't handle them.
+ enum_def->RemoveDuplicates();
+ } else if (IsIdent("syntax")) { // Skip these.
+ NEXT();
+ EXPECT('=');
+ EXPECT(kTokenStringConstant);
+ EXPECT(';');
+ } else if (IsIdent("option")) { // Skip these.
+ ECHECK(ParseProtoOption());
+ EXPECT(';');
+ } else if (IsIdent("service")) { // Skip these.
+ NEXT();
+ EXPECT(kTokenIdentifier);
+ ECHECK(ParseProtoCurliesOrIdent());
+ } else {
+ return Error("don\'t know how to parse .proto declaration starting with " +
+ TokenToStringId(token_));
+ }
+ return NoError();
+}
+
+CheckedError Parser::StartEnum(const std::string &enum_name, bool is_union,
+ EnumDef **dest) {
+ auto &enum_def = *new EnumDef();
+ enum_def.name = enum_name;
+ enum_def.file = file_being_parsed_;
+ enum_def.doc_comment = doc_comment_;
+ enum_def.is_union = is_union;
+ enum_def.defined_namespace = current_namespace_;
+ if (enums_.Add(current_namespace_->GetFullyQualifiedName(enum_name),
+ &enum_def))
+ return Error("enum already exists: " + enum_name);
+ enum_def.underlying_type.base_type =
+ is_union ? BASE_TYPE_UTYPE : BASE_TYPE_INT;
+ enum_def.underlying_type.enum_def = &enum_def;
+ if (dest) *dest = &enum_def;
+ return NoError();
+}
+
+CheckedError Parser::ParseProtoFields(StructDef *struct_def, bool isextend,
+ bool inside_oneof) {
+ EXPECT('{');
+ while (token_ != '}') {
+ if (IsIdent("message") || IsIdent("extend") || IsIdent("enum")) {
+ // Nested declarations.
+ ECHECK(ParseProtoDecl());
+ } else if (IsIdent("extensions")) { // Skip these.
+ NEXT();
+ EXPECT(kTokenIntegerConstant);
+ if (Is(kTokenIdentifier)) {
+ NEXT(); // to
+ NEXT(); // num
+ }
+ EXPECT(';');
+ } else if (IsIdent("option")) { // Skip these.
+ ECHECK(ParseProtoOption());
+ EXPECT(';');
+ } else if (IsIdent("reserved")) { // Skip these.
+ NEXT();
+ while (!Is(';')) { NEXT(); } // A variety of formats, just skip.
+ NEXT();
+ } else {
+ std::vector<std::string> field_comment = doc_comment_;
+ // Parse the qualifier.
+ bool required = false;
+ bool repeated = false;
+ bool oneof = false;
+ if (!inside_oneof) {
+ if (IsIdent("optional")) {
+ // This is the default.
+ NEXT();
+ } else if (IsIdent("required")) {
+ required = true;
+ NEXT();
+ } else if (IsIdent("repeated")) {
+ repeated = true;
+ NEXT();
+ } else if (IsIdent("oneof")) {
+ oneof = true;
+ NEXT();
+ } else {
+ // can't error, proto3 allows decls without any of the above.
+ }
+ }
+ StructDef *anonymous_struct = nullptr;
+ EnumDef *oneof_union = nullptr;
+ Type type;
+ if (IsIdent("group") || oneof) {
+ if (!oneof) NEXT();
+ if (oneof && opts.proto_oneof_union) {
+ auto name = MakeCamel(attribute_, true) + "Union";
+ ECHECK(StartEnum(name, true, &oneof_union));
+ type = Type(BASE_TYPE_UNION, nullptr, oneof_union);
+ } else {
+ auto name = "Anonymous" + NumToString(anonymous_counter_++);
+ ECHECK(StartStruct(name, &anonymous_struct));
+ type = Type(BASE_TYPE_STRUCT, anonymous_struct);
+ }
+ } else {
+ ECHECK(ParseTypeFromProtoType(&type));
+ }
+ // Repeated elements get mapped to a vector.
+ if (repeated) {
+ type.element = type.base_type;
+ type.base_type = BASE_TYPE_VECTOR;
+ if (type.element == BASE_TYPE_VECTOR) {
+ // We have a vector or vectors, which FlatBuffers doesn't support.
+ // For now make it a vector of string (since the source is likely
+ // "repeated bytes").
+ // TODO(wvo): A better solution would be to wrap this in a table.
+ type.element = BASE_TYPE_STRING;
+ }
+ }
+ std::string name = attribute_;
+ EXPECT(kTokenIdentifier);
+ if (!oneof) {
+ // Parse the field id. Since we're just translating schemas, not
+ // any kind of binary compatibility, we can safely ignore these, and
+ // assign our own.
+ EXPECT('=');
+ EXPECT(kTokenIntegerConstant);
+ }
+ FieldDef *field = nullptr;
+ if (isextend) {
+ // We allow a field to be re-defined when extending.
+ // TODO: are there situations where that is problematic?
+ field = struct_def->fields.Lookup(name);
+ }
+ if (!field) ECHECK(AddField(*struct_def, name, type, &field));
+ field->doc_comment = field_comment;
+ if (!IsScalar(type.base_type) && required) {
+ field->presence = FieldDef::kRequired;
+ }
+ // See if there's a default specified.
+ if (Is('[')) {
+ NEXT();
+ for (;;) {
+ auto key = attribute_;
+ ECHECK(ParseProtoKey());
+ EXPECT('=');
+ auto val = attribute_;
+ ECHECK(ParseProtoCurliesOrIdent());
+ if (key == "default") {
+ // Temp: skip non-numeric and non-boolean defaults (enums).
+ auto numeric = strpbrk(val.c_str(), "0123456789-+.");
+ if (IsScalar(type.base_type) && numeric == val.c_str()) {
+ field->value.constant = val;
+ } else if (val == "true") {
+ field->value.constant = val;
+ } // "false" is default, no need to handle explicitly.
+ } else if (key == "deprecated") {
+ field->deprecated = val == "true";
+ }
+ if (!Is(',')) break;
+ NEXT();
+ }
+ EXPECT(']');
+ }
+ if (anonymous_struct) {
+ ECHECK(ParseProtoFields(anonymous_struct, false, oneof));
+ if (Is(';')) NEXT();
+ } else if (oneof_union) {
+ // Parse into a temporary StructDef, then transfer fields into an
+ // EnumDef describing the oneof as a union.
+ StructDef oneof_struct;
+ ECHECK(ParseProtoFields(&oneof_struct, false, oneof));
+ if (Is(';')) NEXT();
+ for (auto field_it = oneof_struct.fields.vec.begin();
+ field_it != oneof_struct.fields.vec.end(); ++field_it) {
+ const auto &oneof_field = **field_it;
+ const auto &oneof_type = oneof_field.value.type;
+ if (oneof_type.base_type != BASE_TYPE_STRUCT ||
+ !oneof_type.struct_def || oneof_type.struct_def->fixed)
+ return Error("oneof '" + name +
+ "' cannot be mapped to a union because member '" +
+ oneof_field.name + "' is not a table type.");
+ EnumValBuilder evb(*this, *oneof_union);
+ auto ev = evb.CreateEnumerator(oneof_type.struct_def->name);
+ ev->union_type = oneof_type;
+ ev->doc_comment = oneof_field.doc_comment;
+ ECHECK(evb.AcceptEnumerator(oneof_field.name));
+ }
+ } else {
+ EXPECT(';');
+ }
+ }
+ }
+ NEXT();
+ return NoError();
+}
+
+CheckedError Parser::ParseProtoKey() {
+ if (token_ == '(') {
+ NEXT();
+ // Skip "(a.b)" style custom attributes.
+ while (token_ == '.' || token_ == kTokenIdentifier) NEXT();
+ EXPECT(')');
+ while (Is('.')) {
+ NEXT();
+ EXPECT(kTokenIdentifier);
+ }
+ } else {
+ EXPECT(kTokenIdentifier);
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseProtoCurliesOrIdent() {
+ if (Is('{')) {
+ NEXT();
+ for (int nesting = 1; nesting;) {
+ if (token_ == '{')
+ nesting++;
+ else if (token_ == '}')
+ nesting--;
+ NEXT();
+ }
+ } else {
+ NEXT(); // Any single token.
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseProtoOption() {
+ NEXT();
+ ECHECK(ParseProtoKey());
+ EXPECT('=');
+ ECHECK(ParseProtoCurliesOrIdent());
+ return NoError();
+}
+
+// Parse a protobuf type, and map it to the corresponding FlatBuffer one.
+CheckedError Parser::ParseTypeFromProtoType(Type *type) {
+ struct type_lookup {
+ const char *proto_type;
+ BaseType fb_type, element;
+ };
+ static type_lookup lookup[] = {
+ { "float", BASE_TYPE_FLOAT, BASE_TYPE_NONE },
+ { "double", BASE_TYPE_DOUBLE, BASE_TYPE_NONE },
+ { "int32", BASE_TYPE_INT, BASE_TYPE_NONE },
+ { "int64", BASE_TYPE_LONG, BASE_TYPE_NONE },
+ { "uint32", BASE_TYPE_UINT, BASE_TYPE_NONE },
+ { "uint64", BASE_TYPE_ULONG, BASE_TYPE_NONE },
+ { "sint32", BASE_TYPE_INT, BASE_TYPE_NONE },
+ { "sint64", BASE_TYPE_LONG, BASE_TYPE_NONE },
+ { "fixed32", BASE_TYPE_UINT, BASE_TYPE_NONE },
+ { "fixed64", BASE_TYPE_ULONG, BASE_TYPE_NONE },
+ { "sfixed32", BASE_TYPE_INT, BASE_TYPE_NONE },
+ { "sfixed64", BASE_TYPE_LONG, BASE_TYPE_NONE },
+ { "bool", BASE_TYPE_BOOL, BASE_TYPE_NONE },
+ { "string", BASE_TYPE_STRING, BASE_TYPE_NONE },
+ { "bytes", BASE_TYPE_VECTOR, BASE_TYPE_UCHAR },
+ { nullptr, BASE_TYPE_NONE, BASE_TYPE_NONE }
+ };
+ for (auto tl = lookup; tl->proto_type; tl++) {
+ if (attribute_ == tl->proto_type) {
+ type->base_type = tl->fb_type;
+ type->element = tl->element;
+ NEXT();
+ return NoError();
+ }
+ }
+ if (Is('.')) NEXT(); // qualified names may start with a . ?
+ ECHECK(ParseTypeIdent(*type));
+ return NoError();
+}
+
+CheckedError Parser::SkipAnyJsonValue() {
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+
+ switch (token_) {
+ case '{': {
+ size_t fieldn_outer = 0;
+ return ParseTableDelimiters(fieldn_outer, nullptr,
+ [&](const std::string &, size_t &fieldn,
+ const StructDef *) -> CheckedError {
+ ECHECK(SkipAnyJsonValue());
+ fieldn++;
+ return NoError();
+ });
+ }
+ case '[': {
+ uoffset_t count = 0;
+ return ParseVectorDelimiters(count, [&](uoffset_t &) -> CheckedError {
+ return SkipAnyJsonValue();
+ });
+ }
+ case kTokenStringConstant:
+ case kTokenIntegerConstant:
+ case kTokenFloatConstant: NEXT(); break;
+ default:
+ if (IsIdent("true") || IsIdent("false") || IsIdent("null")) {
+ NEXT();
+ } else
+ return TokenError();
+ }
+ return NoError();
+}
+
+CheckedError Parser::ParseFlexBufferNumericConstant(
+ flexbuffers::Builder *builder) {
+ double d;
+ if (!StringToNumber(attribute_.c_str(), &d))
+ return Error("unexpected floating-point constant: " + attribute_);
+ builder->Double(d);
+ return NoError();
+}
+
+CheckedError Parser::ParseFlexBufferValue(flexbuffers::Builder *builder) {
+ ParseDepthGuard depth_guard(this);
+ ECHECK(depth_guard.Check());
+
+ switch (token_) {
+ case '{': {
+ auto start = builder->StartMap();
+ size_t fieldn_outer = 0;
+ auto err =
+ ParseTableDelimiters(fieldn_outer, nullptr,
+ [&](const std::string &name, size_t &fieldn,
+ const StructDef *) -> CheckedError {
+ builder->Key(name);
+ ECHECK(ParseFlexBufferValue(builder));
+ fieldn++;
+ return NoError();
+ });
+ ECHECK(err);
+ builder->EndMap(start);
+ if (builder->HasDuplicateKeys())
+ return Error("FlexBuffers map has duplicate keys");
+ break;
+ }
+ case '[': {
+ auto start = builder->StartVector();
+ uoffset_t count = 0;
+ ECHECK(ParseVectorDelimiters(count, [&](uoffset_t &) -> CheckedError {
+ return ParseFlexBufferValue(builder);
+ }));
+ builder->EndVector(start, false, false);
+ break;
+ }
+ case kTokenStringConstant:
+ builder->String(attribute_);
+ EXPECT(kTokenStringConstant);
+ break;
+ case kTokenIntegerConstant:
+ builder->Int(StringToInt(attribute_.c_str()));
+ EXPECT(kTokenIntegerConstant);
+ break;
+ case kTokenFloatConstant: {
+ double d;
+ StringToNumber(attribute_.c_str(), &d);
+ builder->Double(d);
+ EXPECT(kTokenFloatConstant);
+ break;
+ }
+ case '-':
+ case '+': {
+ // `[-+]?(nan|inf|infinity)`, see ParseSingleValue().
+ const auto sign = static_cast<char>(token_);
+ NEXT();
+ if (token_ != kTokenIdentifier)
+ return Error("floating-point constant expected");
+ attribute_.insert(size_t(0), size_t(1), sign);
+ ECHECK(ParseFlexBufferNumericConstant(builder));
+ NEXT();
+ break;
+ }
+ default:
+ if (IsIdent("true")) {
+ builder->Bool(true);
+ NEXT();
+ } else if (IsIdent("false")) {
+ builder->Bool(false);
+ NEXT();
+ } else if (IsIdent("null")) {
+ builder->Null();
+ NEXT();
+ } else if (IsIdent("inf") || IsIdent("infinity") || IsIdent("nan")) {
+ ECHECK(ParseFlexBufferNumericConstant(builder));
+ NEXT();
+ } else
+ return TokenError();
+ }
+ return NoError();
+}
+
+bool Parser::ParseFlexBuffer(const char *source, const char *source_filename,
+ flexbuffers::Builder *builder) {
+ const auto initial_depth = parse_depth_counter_;
+ (void)initial_depth;
+ auto ok = !StartParseFile(source, source_filename).Check() &&
+ !ParseFlexBufferValue(builder).Check();
+ if (ok) builder->Finish();
+ FLATBUFFERS_ASSERT(initial_depth == parse_depth_counter_);
+ return ok;
+}
+
+bool Parser::Parse(const char *source, const char **include_paths,
+ const char *source_filename) {
+ const auto initial_depth = parse_depth_counter_;
+ (void)initial_depth;
+ bool r;
+
+ if (opts.use_flexbuffers) {
+ r = ParseFlexBuffer(source, source_filename, &flex_builder_);
+ } else {
+ r = !ParseRoot(source, include_paths, source_filename).Check();
+ }
+ FLATBUFFERS_ASSERT(initial_depth == parse_depth_counter_);
+ return r;
+}
+
+bool Parser::ParseJson(const char *json, const char *json_filename) {
+ const auto initial_depth = parse_depth_counter_;
+ (void)initial_depth;
+ builder_.Clear();
+ const auto done =
+ !StartParseFile(json, json_filename).Check() && !DoParseJson().Check();
+ FLATBUFFERS_ASSERT(initial_depth == parse_depth_counter_);
+ return done;
+}
+
+CheckedError Parser::StartParseFile(const char *source,
+ const char *source_filename) {
+ file_being_parsed_ = source_filename ? source_filename : "";
+ source_ = source;
+ ResetState(source_);
+ error_.clear();
+ ECHECK(SkipByteOrderMark());
+ NEXT();
+ if (Is(kTokenEof)) return Error("input file is empty");
+ return NoError();
+}
+
+CheckedError Parser::ParseRoot(const char *source, const char **include_paths,
+ const char *source_filename) {
+ ECHECK(DoParse(source, include_paths, source_filename, nullptr));
+
+ // Check that all types were defined.
+ for (auto it = structs_.vec.begin(); it != structs_.vec.end();) {
+ auto &struct_def = **it;
+ if (struct_def.predecl) {
+ if (opts.proto_mode) {
+ // Protos allow enums to be used before declaration, so check if that
+ // is the case here.
+ EnumDef *enum_def = nullptr;
+ for (size_t components =
+ struct_def.defined_namespace->components.size() + 1;
+ components && !enum_def; components--) {
+ auto qualified_name =
+ struct_def.defined_namespace->GetFullyQualifiedName(
+ struct_def.name, components - 1);
+ enum_def = LookupEnum(qualified_name);
+ }
+ if (enum_def) {
+ // This is pretty slow, but a simple solution for now.
+ auto initial_count = struct_def.refcount;
+ for (auto struct_it = structs_.vec.begin();
+ struct_it != structs_.vec.end(); ++struct_it) {
+ auto &sd = **struct_it;
+ for (auto field_it = sd.fields.vec.begin();
+ field_it != sd.fields.vec.end(); ++field_it) {
+ auto &field = **field_it;
+ if (field.value.type.struct_def == &struct_def) {
+ field.value.type.struct_def = nullptr;
+ field.value.type.enum_def = enum_def;
+ auto &bt = IsVector(field.value.type)
+ ? field.value.type.element
+ : field.value.type.base_type;
+ FLATBUFFERS_ASSERT(bt == BASE_TYPE_STRUCT);
+ bt = enum_def->underlying_type.base_type;
+ struct_def.refcount--;
+ enum_def->refcount++;
+ }
+ }
+ }
+ if (struct_def.refcount)
+ return Error("internal: " + NumToString(struct_def.refcount) + "/" +
+ NumToString(initial_count) +
+ " use(s) of pre-declaration enum not accounted for: " +
+ enum_def->name);
+ structs_.dict.erase(structs_.dict.find(struct_def.name));
+ it = structs_.vec.erase(it);
+ delete &struct_def;
+ continue; // Skip error.
+ }
+ }
+ auto err = "type referenced but not defined (check namespace): " +
+ struct_def.name;
+ if (struct_def.original_location)
+ err += ", originally at: " + *struct_def.original_location;
+ return Error(err);
+ }
+ ++it;
+ }
+
+ // This check has to happen here and not earlier, because only now do we
+ // know for sure what the type of these are.
+ for (auto it = enums_.vec.begin(); it != enums_.vec.end(); ++it) {
+ auto &enum_def = **it;
+ if (enum_def.is_union) {
+ for (auto val_it = enum_def.Vals().begin();
+ val_it != enum_def.Vals().end(); ++val_it) {
+ auto &val = **val_it;
+ if (!SupportsAdvancedUnionFeatures() &&
+ (IsStruct(val.union_type) || IsString(val.union_type)))
+ return Error(
+ "only tables can be union elements in the generated language: " +
+ val.name);
+ }
+ }
+ }
+ // Parse JSON object only if the scheme has been parsed.
+ if (token_ == '{') { ECHECK(DoParseJson()); }
+ EXPECT(kTokenEof);
+ return NoError();
+}
+
+// Generate a unique hash for a file based on its name and contents (if any).
+static uint64_t HashFile(const char *source_filename, const char *source) {
+ uint64_t hash = 0;
+
+ if (source_filename)
+ hash = HashFnv1a<uint64_t>(StripPath(source_filename).c_str());
+
+ if (source && *source) hash ^= HashFnv1a<uint64_t>(source);
+
+ return hash;
+}
+
+CheckedError Parser::DoParse(const char *source, const char **include_paths,
+ const char *source_filename,
+ const char *include_filename) {
+ uint64_t source_hash = 0;
+ if (source_filename) {
+ // If the file is in-memory, don't include its contents in the hash as we
+ // won't be able to load them later.
+ if (FileExists(source_filename))
+ source_hash = HashFile(source_filename, source);
+ else
+ source_hash = HashFile(source_filename, nullptr);
+
+ if (included_files_.find(source_hash) == included_files_.end()) {
+ included_files_[source_hash] = include_filename ? include_filename : "";
+ files_included_per_file_[source_filename] = std::set<std::string>();
+ } else {
+ return NoError();
+ }
+ }
+ if (!include_paths) {
+ static const char *current_directory[] = { "", nullptr };
+ include_paths = current_directory;
+ }
+ field_stack_.clear();
+ builder_.Clear();
+ // Start with a blank namespace just in case this file doesn't have one.
+ current_namespace_ = empty_namespace_;
+
+ ECHECK(StartParseFile(source, source_filename));
+
+ // Includes must come before type declarations:
+ for (;;) {
+ // Parse pre-include proto statements if any:
+ if (opts.proto_mode && (attribute_ == "option" || attribute_ == "syntax" ||
+ attribute_ == "package")) {
+ ECHECK(ParseProtoDecl());
+ } else if (IsIdent("native_include")) {
+ NEXT();
+ vector_emplace_back(&native_included_files_, attribute_);
+ EXPECT(kTokenStringConstant);
+ EXPECT(';');
+ } else if (IsIdent("include") || (opts.proto_mode && IsIdent("import"))) {
+ NEXT();
+ if (opts.proto_mode && attribute_ == "public") NEXT();
+ auto name = flatbuffers::PosixPath(attribute_.c_str());
+ EXPECT(kTokenStringConstant);
+ // Look for the file relative to the directory of the current file.
+ std::string filepath;
+ if (source_filename) {
+ auto source_file_directory =
+ flatbuffers::StripFileName(source_filename);
+ filepath = flatbuffers::ConCatPathFileName(source_file_directory, name);
+ }
+ if (filepath.empty() || !FileExists(filepath.c_str())) {
+ // Look for the file in include_paths.
+ for (auto paths = include_paths; paths && *paths; paths++) {
+ filepath = flatbuffers::ConCatPathFileName(*paths, name);
+ if (FileExists(filepath.c_str())) break;
+ }
+ }
+ if (filepath.empty())
+ return Error("unable to locate include file: " + name);
+ if (source_filename)
+ files_included_per_file_[source_filename].insert(filepath);
+
+ std::string contents;
+ bool file_loaded = LoadFile(filepath.c_str(), true, &contents);
+ if (included_files_.find(HashFile(filepath.c_str(), contents.c_str())) ==
+ included_files_.end()) {
+ // We found an include file that we have not parsed yet.
+ // Parse it.
+ if (!file_loaded) return Error("unable to load include file: " + name);
+ ECHECK(DoParse(contents.c_str(), include_paths, filepath.c_str(),
+ name.c_str()));
+ // We generally do not want to output code for any included files:
+ if (!opts.generate_all) MarkGenerated();
+ // Reset these just in case the included file had them, and the
+ // parent doesn't.
+ root_struct_def_ = nullptr;
+ file_identifier_.clear();
+ file_extension_.clear();
+ // This is the easiest way to continue this file after an include:
+ // instead of saving and restoring all the state, we simply start the
+ // file anew. This will cause it to encounter the same include
+ // statement again, but this time it will skip it, because it was
+ // entered into included_files_.
+ // This is recursive, but only go as deep as the number of include
+ // statements.
+ included_files_.erase(source_hash);
+ return DoParse(source, include_paths, source_filename,
+ include_filename);
+ }
+ EXPECT(';');
+ } else {
+ break;
+ }
+ }
+ // Now parse all other kinds of declarations:
+ while (token_ != kTokenEof) {
+ if (opts.proto_mode) {
+ ECHECK(ParseProtoDecl());
+ } else if (IsIdent("namespace")) {
+ ECHECK(ParseNamespace());
+ } else if (token_ == '{') {
+ return NoError();
+ } else if (IsIdent("enum")) {
+ ECHECK(ParseEnum(false, nullptr));
+ } else if (IsIdent("union")) {
+ ECHECK(ParseEnum(true, nullptr));
+ } else if (IsIdent("root_type")) {
+ NEXT();
+ auto root_type = attribute_;
+ EXPECT(kTokenIdentifier);
+ ECHECK(ParseNamespacing(&root_type, nullptr));
+ if (opts.root_type.empty()) {
+ if (!SetRootType(root_type.c_str()))
+ return Error("unknown root type: " + root_type);
+ if (root_struct_def_->fixed) return Error("root type must be a table");
+ }
+ EXPECT(';');
+ } else if (IsIdent("file_identifier")) {
+ NEXT();
+ file_identifier_ = attribute_;
+ EXPECT(kTokenStringConstant);
+ if (file_identifier_.length() != FlatBufferBuilder::kFileIdentifierLength)
+ return Error("file_identifier must be exactly " +
+ NumToString(FlatBufferBuilder::kFileIdentifierLength) +
+ " characters");
+ EXPECT(';');
+ } else if (IsIdent("file_extension")) {
+ NEXT();
+ file_extension_ = attribute_;
+ EXPECT(kTokenStringConstant);
+ EXPECT(';');
+ } else if (IsIdent("include")) {
+ return Error("includes must come before declarations");
+ } else if (IsIdent("attribute")) {
+ NEXT();
+ auto name = attribute_;
+ if (Is(kTokenIdentifier)) {
+ NEXT();
+ } else {
+ EXPECT(kTokenStringConstant);
+ }
+ EXPECT(';');
+ known_attributes_[name] = false;
+ } else if (IsIdent("rpc_service")) {
+ ECHECK(ParseService());
+ } else {
+ ECHECK(ParseDecl());
+ }
+ }
+ return NoError();
+}
+
+CheckedError Parser::DoParseJson() {
+ if (token_ != '{') {
+ EXPECT('{');
+ } else {
+ if (!root_struct_def_) return Error("no root type set to parse json with");
+ if (builder_.GetSize()) {
+ return Error("cannot have more than one json object in a file");
+ }
+ uoffset_t toff;
+ ECHECK(ParseTable(*root_struct_def_, nullptr, &toff));
+ if (opts.size_prefixed) {
+ builder_.FinishSizePrefixed(
+ Offset<Table>(toff),
+ file_identifier_.length() ? file_identifier_.c_str() : nullptr);
+ } else {
+ builder_.Finish(Offset<Table>(toff), file_identifier_.length()
+ ? file_identifier_.c_str()
+ : nullptr);
+ }
+ }
+ // Check that JSON file doesn't contain more objects or IDL directives.
+ // Comments after JSON are allowed.
+ EXPECT(kTokenEof);
+ return NoError();
+}
+
+std::set<std::string> Parser::GetIncludedFilesRecursive(
+ const std::string &file_name) const {
+ std::set<std::string> included_files;
+ std::list<std::string> to_process;
+
+ if (file_name.empty()) return included_files;
+ to_process.push_back(file_name);
+
+ while (!to_process.empty()) {
+ std::string current = to_process.front();
+ to_process.pop_front();
+ included_files.insert(current);
+
+ // Workaround the lack of const accessor in C++98 maps.
+ auto &new_files =
+ (*const_cast<std::map<std::string, std::set<std::string>> *>(
+ &files_included_per_file_))[current];
+ for (auto it = new_files.begin(); it != new_files.end(); ++it) {
+ if (included_files.find(*it) == included_files.end())
+ to_process.push_back(*it);
+ }
+ }
+
+ return included_files;
+}
+
+// Schema serialization functionality:
+
+template<typename T> bool compareName(const T *a, const T *b) {
+ return a->defined_namespace->GetFullyQualifiedName(a->name) <
+ b->defined_namespace->GetFullyQualifiedName(b->name);
+}
+
+template<typename T> void AssignIndices(const std::vector<T *> &defvec) {
+ // Pre-sort these vectors, such that we can set the correct indices for them.
+ auto vec = defvec;
+ std::sort(vec.begin(), vec.end(), compareName<T>);
+ for (int i = 0; i < static_cast<int>(vec.size()); i++) vec[i]->index = i;
+}
+
+void Parser::Serialize() {
+ builder_.Clear();
+ AssignIndices(structs_.vec);
+ AssignIndices(enums_.vec);
+ std::vector<Offset<reflection::Object>> object_offsets;
+ for (auto it = structs_.vec.begin(); it != structs_.vec.end(); ++it) {
+ auto offset = (*it)->Serialize(&builder_, *this);
+ object_offsets.push_back(offset);
+ (*it)->serialized_location = offset.o;
+ }
+ std::vector<Offset<reflection::Enum>> enum_offsets;
+ for (auto it = enums_.vec.begin(); it != enums_.vec.end(); ++it) {
+ auto offset = (*it)->Serialize(&builder_, *this);
+ enum_offsets.push_back(offset);
+ (*it)->serialized_location = offset.o;
+ }
+ std::vector<Offset<reflection::Service>> service_offsets;
+ for (auto it = services_.vec.begin(); it != services_.vec.end(); ++it) {
+ auto offset = (*it)->Serialize(&builder_, *this);
+ service_offsets.push_back(offset);
+ (*it)->serialized_location = offset.o;
+ }
+ auto objs__ = builder_.CreateVectorOfSortedTables(&object_offsets);
+ auto enum__ = builder_.CreateVectorOfSortedTables(&enum_offsets);
+ auto fiid__ = builder_.CreateString(file_identifier_);
+ auto fext__ = builder_.CreateString(file_extension_);
+ auto serv__ = builder_.CreateVectorOfSortedTables(&service_offsets);
+ auto schema_offset = reflection::CreateSchema(
+ builder_, objs__, enum__, fiid__, fext__,
+ (root_struct_def_ ? root_struct_def_->serialized_location : 0), serv__,
+ static_cast<reflection::AdvancedFeatures>(advanced_features_));
+ if (opts.size_prefixed) {
+ builder_.FinishSizePrefixed(schema_offset, reflection::SchemaIdentifier());
+ } else {
+ builder_.Finish(schema_offset, reflection::SchemaIdentifier());
+ }
+}
+
+static Namespace *GetNamespace(
+ const std::string &qualified_name, std::vector<Namespace *> &namespaces,
+ std::map<std::string, Namespace *> &namespaces_index) {
+ size_t dot = qualified_name.find_last_of('.');
+ std::string namespace_name = (dot != std::string::npos)
+ ? std::string(qualified_name.c_str(), dot)
+ : "";
+ Namespace *&ns = namespaces_index[namespace_name];
+
+ if (!ns) {
+ ns = new Namespace();
+ namespaces.push_back(ns);
+
+ size_t pos = 0;
+
+ for (;;) {
+ dot = qualified_name.find('.', pos);
+ if (dot == std::string::npos) { break; }
+ ns->components.push_back(qualified_name.substr(pos, dot - pos));
+ pos = dot + 1;
+ }
+ }
+
+ return ns;
+}
+
+Offset<reflection::Object> StructDef::Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ std::vector<Offset<reflection::Field>> field_offsets;
+ for (auto it = fields.vec.begin(); it != fields.vec.end(); ++it) {
+ field_offsets.push_back((*it)->Serialize(
+ builder, static_cast<uint16_t>(it - fields.vec.begin()), parser));
+ }
+ auto qualified_name = defined_namespace->GetFullyQualifiedName(name);
+ auto name__ = builder->CreateString(qualified_name);
+ auto flds__ = builder->CreateVectorOfSortedTables(&field_offsets);
+ auto attr__ = SerializeAttributes(builder, parser);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ return reflection::CreateObject(*builder, name__, flds__, fixed,
+ static_cast<int>(minalign),
+ static_cast<int>(bytesize), attr__, docs__);
+}
+
+bool StructDef::Deserialize(Parser &parser, const reflection::Object *object) {
+ if (!DeserializeAttributes(parser, object->attributes())) return false;
+ DeserializeDoc(doc_comment, object->documentation());
+ name = parser.UnqualifiedName(object->name()->str());
+ predecl = false;
+ sortbysize = attributes.Lookup("original_order") == nullptr && !fixed;
+ const auto &of = *(object->fields());
+ auto indexes = std::vector<uoffset_t>(of.size());
+ for (uoffset_t i = 0; i < of.size(); i++) indexes[of.Get(i)->id()] = i;
+ size_t tmp_struct_size = 0;
+ for (size_t i = 0; i < indexes.size(); i++) {
+ auto field = of.Get(indexes[i]);
+ auto field_def = new FieldDef();
+ if (!field_def->Deserialize(parser, field) ||
+ fields.Add(field_def->name, field_def)) {
+ delete field_def;
+ return false;
+ }
+ if (fixed) {
+ // Recompute padding since that's currently not serialized.
+ auto size = InlineSize(field_def->value.type);
+ auto next_field =
+ i + 1 < indexes.size() ? of.Get(indexes[i + 1]) : nullptr;
+ tmp_struct_size += size;
+ field_def->padding =
+ next_field ? (next_field->offset() - field_def->value.offset) - size
+ : PaddingBytes(tmp_struct_size, minalign);
+ tmp_struct_size += field_def->padding;
+ }
+ }
+ FLATBUFFERS_ASSERT(static_cast<int>(tmp_struct_size) == object->bytesize());
+ return true;
+}
+
+Offset<reflection::Field> FieldDef::Serialize(FlatBufferBuilder *builder,
+ uint16_t id,
+ const Parser &parser) const {
+ auto name__ = builder->CreateString(name);
+ auto type__ = value.type.Serialize(builder);
+ auto attr__ = SerializeAttributes(builder, parser);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ double d;
+ StringToNumber(value.constant.c_str(), &d);
+ return reflection::CreateField(
+ *builder, name__, type__, id, value.offset,
+ // Is uint64>max(int64) tested?
+ IsInteger(value.type.base_type) ? StringToInt(value.constant.c_str()) : 0,
+ // result may be platform-dependent if underlying is float (not double)
+ IsFloat(value.type.base_type) ? d : 0.0, deprecated, IsRequired(), key,
+ attr__, docs__, IsOptional());
+ // TODO: value.constant is almost always "0", we could save quite a bit of
+ // space by sharing it. Same for common values of value.type.
+}
+
+bool FieldDef::Deserialize(Parser &parser, const reflection::Field *field) {
+ name = field->name()->str();
+ defined_namespace = parser.current_namespace_;
+ if (!value.type.Deserialize(parser, field->type())) return false;
+ value.offset = field->offset();
+ if (IsInteger(value.type.base_type)) {
+ value.constant = NumToString(field->default_integer());
+ } else if (IsFloat(value.type.base_type)) {
+ value.constant = FloatToString(field->default_real(), 16);
+ }
+ presence = FieldDef::MakeFieldPresence(field->optional(), field->required());
+ key = field->key();
+ if (!DeserializeAttributes(parser, field->attributes())) return false;
+ // TODO: this should probably be handled by a separate attribute
+ if (attributes.Lookup("flexbuffer")) {
+ flexbuffer = true;
+ parser.uses_flexbuffers_ = true;
+ if (value.type.base_type != BASE_TYPE_VECTOR ||
+ value.type.element != BASE_TYPE_UCHAR)
+ return false;
+ }
+ if (auto nested = attributes.Lookup("nested_flatbuffer")) {
+ auto nested_qualified_name =
+ parser.current_namespace_->GetFullyQualifiedName(nested->constant);
+ nested_flatbuffer = parser.LookupStruct(nested_qualified_name);
+ if (!nested_flatbuffer) return false;
+ }
+ shared = attributes.Lookup("shared") != nullptr;
+ DeserializeDoc(doc_comment, field->documentation());
+ return true;
+}
+
+Offset<reflection::RPCCall> RPCCall::Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ auto name__ = builder->CreateString(name);
+ auto attr__ = SerializeAttributes(builder, parser);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ return reflection::CreateRPCCall(
+ *builder, name__, request->serialized_location,
+ response->serialized_location, attr__, docs__);
+}
+
+bool RPCCall::Deserialize(Parser &parser, const reflection::RPCCall *call) {
+ name = call->name()->str();
+ if (!DeserializeAttributes(parser, call->attributes())) return false;
+ DeserializeDoc(doc_comment, call->documentation());
+ request = parser.structs_.Lookup(call->request()->name()->str());
+ response = parser.structs_.Lookup(call->response()->name()->str());
+ if (!request || !response) { return false; }
+ return true;
+}
+
+Offset<reflection::Service> ServiceDef::Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ std::vector<Offset<reflection::RPCCall>> servicecall_offsets;
+ for (auto it = calls.vec.begin(); it != calls.vec.end(); ++it) {
+ servicecall_offsets.push_back((*it)->Serialize(builder, parser));
+ }
+ auto qualified_name = defined_namespace->GetFullyQualifiedName(name);
+ auto name__ = builder->CreateString(qualified_name);
+ auto call__ = builder->CreateVector(servicecall_offsets);
+ auto attr__ = SerializeAttributes(builder, parser);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ return reflection::CreateService(*builder, name__, call__, attr__, docs__);
+}
+
+bool ServiceDef::Deserialize(Parser &parser,
+ const reflection::Service *service) {
+ name = parser.UnqualifiedName(service->name()->str());
+ if (service->calls()) {
+ for (uoffset_t i = 0; i < service->calls()->size(); ++i) {
+ auto call = new RPCCall();
+ if (!call->Deserialize(parser, service->calls()->Get(i)) ||
+ calls.Add(call->name, call)) {
+ delete call;
+ return false;
+ }
+ }
+ }
+ if (!DeserializeAttributes(parser, service->attributes())) return false;
+ DeserializeDoc(doc_comment, service->documentation());
+ return true;
+}
+
+Offset<reflection::Enum> EnumDef::Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ std::vector<Offset<reflection::EnumVal>> enumval_offsets;
+ for (auto it = vals.vec.begin(); it != vals.vec.end(); ++it) {
+ enumval_offsets.push_back((*it)->Serialize(builder, parser));
+ }
+ auto qualified_name = defined_namespace->GetFullyQualifiedName(name);
+ auto name__ = builder->CreateString(qualified_name);
+ auto vals__ = builder->CreateVector(enumval_offsets);
+ auto type__ = underlying_type.Serialize(builder);
+ auto attr__ = SerializeAttributes(builder, parser);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ return reflection::CreateEnum(*builder, name__, vals__, is_union, type__,
+ attr__, docs__);
+}
+
+bool EnumDef::Deserialize(Parser &parser, const reflection::Enum *_enum) {
+ name = parser.UnqualifiedName(_enum->name()->str());
+ for (uoffset_t i = 0; i < _enum->values()->size(); ++i) {
+ auto val = new EnumVal();
+ if (!val->Deserialize(parser, _enum->values()->Get(i)) ||
+ vals.Add(val->name, val)) {
+ delete val;
+ return false;
+ }
+ }
+ is_union = _enum->is_union();
+ if (!underlying_type.Deserialize(parser, _enum->underlying_type())) {
+ return false;
+ }
+ if (!DeserializeAttributes(parser, _enum->attributes())) return false;
+ DeserializeDoc(doc_comment, _enum->documentation());
+ return true;
+}
+
+Offset<reflection::EnumVal> EnumVal::Serialize(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ auto name__ = builder->CreateString(name);
+ auto type__ = union_type.Serialize(builder);
+ auto docs__ = parser.opts.binary_schema_comments
+ ? builder->CreateVectorOfStrings(doc_comment)
+ : 0;
+ return reflection::CreateEnumVal(
+ *builder, name__, value,
+ union_type.struct_def ? union_type.struct_def->serialized_location : 0,
+ type__, docs__);
+}
+
+bool EnumVal::Deserialize(const Parser &parser,
+ const reflection::EnumVal *val) {
+ name = val->name()->str();
+ value = val->value();
+ if (!union_type.Deserialize(parser, val->union_type())) return false;
+ DeserializeDoc(doc_comment, val->documentation());
+ return true;
+}
+
+Offset<reflection::Type> Type::Serialize(FlatBufferBuilder *builder) const {
+ return reflection::CreateType(
+ *builder, static_cast<reflection::BaseType>(base_type),
+ static_cast<reflection::BaseType>(element),
+ struct_def ? struct_def->index : (enum_def ? enum_def->index : -1),
+ fixed_length);
+}
+
+bool Type::Deserialize(const Parser &parser, const reflection::Type *type) {
+ if (type == nullptr) return true;
+ base_type = static_cast<BaseType>(type->base_type());
+ element = static_cast<BaseType>(type->element());
+ fixed_length = type->fixed_length();
+ if (type->index() >= 0) {
+ bool is_series = type->base_type() == reflection::Vector ||
+ type->base_type() == reflection::Array;
+ if (type->base_type() == reflection::Obj ||
+ (is_series && type->element() == reflection::Obj)) {
+ if (static_cast<size_t>(type->index()) < parser.structs_.vec.size()) {
+ struct_def = parser.structs_.vec[type->index()];
+ struct_def->refcount++;
+ } else {
+ return false;
+ }
+ } else {
+ if (static_cast<size_t>(type->index()) < parser.enums_.vec.size()) {
+ enum_def = parser.enums_.vec[type->index()];
+ } else {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+flatbuffers::Offset<
+ flatbuffers::Vector<flatbuffers::Offset<reflection::KeyValue>>>
+Definition::SerializeAttributes(FlatBufferBuilder *builder,
+ const Parser &parser) const {
+ std::vector<flatbuffers::Offset<reflection::KeyValue>> attrs;
+ for (auto kv = attributes.dict.begin(); kv != attributes.dict.end(); ++kv) {
+ auto it = parser.known_attributes_.find(kv->first);
+ FLATBUFFERS_ASSERT(it != parser.known_attributes_.end());
+ if (parser.opts.binary_schema_builtins || !it->second) {
+ auto key = builder->CreateString(kv->first);
+ auto val = builder->CreateString(kv->second->constant);
+ attrs.push_back(reflection::CreateKeyValue(*builder, key, val));
+ }
+ }
+ if (attrs.size()) {
+ return builder->CreateVectorOfSortedTables(&attrs);
+ } else {
+ return 0;
+ }
+}
+
+bool Definition::DeserializeAttributes(
+ Parser &parser, const Vector<Offset<reflection::KeyValue>> *attrs) {
+ if (attrs == nullptr) return true;
+ for (uoffset_t i = 0; i < attrs->size(); ++i) {
+ auto kv = attrs->Get(i);
+ auto value = new Value();
+ if (kv->value()) { value->constant = kv->value()->str(); }
+ if (attributes.Add(kv->key()->str(), value)) {
+ delete value;
+ return false;
+ }
+ parser.known_attributes_[kv->key()->str()];
+ }
+ return true;
+}
+
+/************************************************************************/
+/* DESERIALIZATION */
+/************************************************************************/
+bool Parser::Deserialize(const uint8_t *buf, const size_t size) {
+ flatbuffers::Verifier verifier(reinterpret_cast<const uint8_t *>(buf), size);
+ bool size_prefixed = false;
+ if (!reflection::SchemaBufferHasIdentifier(buf)) {
+ if (!flatbuffers::BufferHasIdentifier(buf, reflection::SchemaIdentifier(),
+ true))
+ return false;
+ else
+ size_prefixed = true;
+ }
+ auto verify_fn = size_prefixed ? &reflection::VerifySizePrefixedSchemaBuffer
+ : &reflection::VerifySchemaBuffer;
+ if (!verify_fn(verifier)) { return false; }
+ auto schema = size_prefixed ? reflection::GetSizePrefixedSchema(buf)
+ : reflection::GetSchema(buf);
+ return Deserialize(schema);
+}
+
+bool Parser::Deserialize(const reflection::Schema *schema) {
+ file_identifier_ = schema->file_ident() ? schema->file_ident()->str() : "";
+ file_extension_ = schema->file_ext() ? schema->file_ext()->str() : "";
+ std::map<std::string, Namespace *> namespaces_index;
+
+ // Create defs without deserializing so references from fields to structs and
+ // enums can be resolved.
+ for (auto it = schema->objects()->begin(); it != schema->objects()->end();
+ ++it) {
+ auto struct_def = new StructDef();
+ struct_def->bytesize = it->bytesize();
+ struct_def->fixed = it->is_struct();
+ struct_def->minalign = it->minalign();
+ if (structs_.Add(it->name()->str(), struct_def)) {
+ delete struct_def;
+ return false;
+ }
+ auto type = new Type(BASE_TYPE_STRUCT, struct_def, nullptr);
+ if (types_.Add(it->name()->str(), type)) {
+ delete type;
+ return false;
+ }
+ }
+ for (auto it = schema->enums()->begin(); it != schema->enums()->end(); ++it) {
+ auto enum_def = new EnumDef();
+ if (enums_.Add(it->name()->str(), enum_def)) {
+ delete enum_def;
+ return false;
+ }
+ auto type = new Type(BASE_TYPE_UNION, nullptr, enum_def);
+ if (types_.Add(it->name()->str(), type)) {
+ delete type;
+ return false;
+ }
+ }
+
+ // Now fields can refer to structs and enums by index.
+ for (auto it = schema->objects()->begin(); it != schema->objects()->end();
+ ++it) {
+ std::string qualified_name = it->name()->str();
+ auto struct_def = structs_.Lookup(qualified_name);
+ struct_def->defined_namespace =
+ GetNamespace(qualified_name, namespaces_, namespaces_index);
+ if (!struct_def->Deserialize(*this, *it)) { return false; }
+ if (schema->root_table() == *it) { root_struct_def_ = struct_def; }
+ }
+ for (auto it = schema->enums()->begin(); it != schema->enums()->end(); ++it) {
+ std::string qualified_name = it->name()->str();
+ auto enum_def = enums_.Lookup(qualified_name);
+ enum_def->defined_namespace =
+ GetNamespace(qualified_name, namespaces_, namespaces_index);
+ if (!enum_def->Deserialize(*this, *it)) { return false; }
+ }
+
+ if (schema->services()) {
+ for (auto it = schema->services()->begin(); it != schema->services()->end();
+ ++it) {
+ std::string qualified_name = it->name()->str();
+ auto service_def = new ServiceDef();
+ service_def->defined_namespace =
+ GetNamespace(qualified_name, namespaces_, namespaces_index);
+ if (!service_def->Deserialize(*this, *it) ||
+ services_.Add(qualified_name, service_def)) {
+ delete service_def;
+ return false;
+ }
+ }
+ }
+ advanced_features_ = schema->advanced_features();
+ return true;
+}
+
+std::string Parser::ConformTo(const Parser &base) {
+ for (auto sit = structs_.vec.begin(); sit != structs_.vec.end(); ++sit) {
+ auto &struct_def = **sit;
+ auto qualified_name =
+ struct_def.defined_namespace->GetFullyQualifiedName(struct_def.name);
+ auto struct_def_base = base.LookupStruct(qualified_name);
+ if (!struct_def_base) continue;
+ for (auto fit = struct_def.fields.vec.begin();
+ fit != struct_def.fields.vec.end(); ++fit) {
+ auto &field = **fit;
+ auto field_base = struct_def_base->fields.Lookup(field.name);
+ if (field_base) {
+ if (field.value.offset != field_base->value.offset)
+ return "offsets differ for field: " + field.name;
+ if (field.value.constant != field_base->value.constant)
+ return "defaults differ for field: " + field.name;
+ if (!EqualByName(field.value.type, field_base->value.type))
+ return "types differ for field: " + field.name;
+ } else {
+ // Doesn't have to exist, deleting fields is fine.
+ // But we should check if there is a field that has the same offset
+ // but is incompatible (in the case of field renaming).
+ for (auto fbit = struct_def_base->fields.vec.begin();
+ fbit != struct_def_base->fields.vec.end(); ++fbit) {
+ field_base = *fbit;
+ if (field.value.offset == field_base->value.offset) {
+ if (!EqualByName(field.value.type, field_base->value.type))
+ return "field renamed to different type: " + field.name;
+ break;
+ }
+ }
+ }
+ }
+ }
+ for (auto eit = enums_.vec.begin(); eit != enums_.vec.end(); ++eit) {
+ auto &enum_def = **eit;
+ auto qualified_name =
+ enum_def.defined_namespace->GetFullyQualifiedName(enum_def.name);
+ auto enum_def_base = base.enums_.Lookup(qualified_name);
+ if (!enum_def_base) continue;
+ for (auto evit = enum_def.Vals().begin(); evit != enum_def.Vals().end();
+ ++evit) {
+ auto &enum_val = **evit;
+ auto enum_val_base = enum_def_base->Lookup(enum_val.name);
+ if (enum_val_base) {
+ if (enum_val != *enum_val_base)
+ return "values differ for enum: " + enum_val.name;
+ }
+ }
+ }
+ return "";
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/reflection.cpp b/contrib/libs/flatbuffers/src/reflection.cpp
new file mode 100644
index 0000000000..2dedcb4f18
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/reflection.cpp
@@ -0,0 +1,713 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "flatbuffers/reflection.h"
+
+#include "flatbuffers/util.h"
+
+// Helper functionality for reflection.
+
+namespace flatbuffers {
+
+int64_t GetAnyValueI(reflection::BaseType type, const uint8_t *data) {
+// clang-format off
+ #define FLATBUFFERS_GET(T) static_cast<int64_t>(ReadScalar<T>(data))
+ switch (type) {
+ case reflection::UType:
+ case reflection::Bool:
+ case reflection::UByte: return FLATBUFFERS_GET(uint8_t);
+ case reflection::Byte: return FLATBUFFERS_GET(int8_t);
+ case reflection::Short: return FLATBUFFERS_GET(int16_t);
+ case reflection::UShort: return FLATBUFFERS_GET(uint16_t);
+ case reflection::Int: return FLATBUFFERS_GET(int32_t);
+ case reflection::UInt: return FLATBUFFERS_GET(uint32_t);
+ case reflection::Long: return FLATBUFFERS_GET(int64_t);
+ case reflection::ULong: return FLATBUFFERS_GET(uint64_t);
+ case reflection::Float: return FLATBUFFERS_GET(float);
+ case reflection::Double: return FLATBUFFERS_GET(double);
+ case reflection::String: {
+ auto s = reinterpret_cast<const String *>(ReadScalar<uoffset_t>(data) +
+ data);
+ return s ? StringToInt(s->c_str()) : 0;
+ }
+ default: return 0; // Tables & vectors do not make sense.
+ }
+ #undef FLATBUFFERS_GET
+ // clang-format on
+}
+
+double GetAnyValueF(reflection::BaseType type, const uint8_t *data) {
+ switch (type) {
+ case reflection::Float: return static_cast<double>(ReadScalar<float>(data));
+ case reflection::Double: return ReadScalar<double>(data);
+ case reflection::String: {
+ auto s =
+ reinterpret_cast<const String *>(ReadScalar<uoffset_t>(data) + data);
+ if (s) {
+ double d;
+ StringToNumber(s->c_str(), &d);
+ return d;
+ } else {
+ return 0.0;
+ }
+ }
+ default: return static_cast<double>(GetAnyValueI(type, data));
+ }
+}
+
+std::string GetAnyValueS(reflection::BaseType type, const uint8_t *data,
+ const reflection::Schema *schema, int type_index) {
+ switch (type) {
+ case reflection::Float:
+ case reflection::Double: return NumToString(GetAnyValueF(type, data));
+ case reflection::String: {
+ auto s =
+ reinterpret_cast<const String *>(ReadScalar<uoffset_t>(data) + data);
+ return s ? s->c_str() : "";
+ }
+ case reflection::Obj:
+ if (schema) {
+ // Convert the table to a string. This is mostly for debugging purposes,
+ // and does NOT promise to be JSON compliant.
+ // Also prefixes the type.
+ auto &objectdef = *schema->objects()->Get(type_index);
+ auto s = objectdef.name()->str();
+ if (objectdef.is_struct()) {
+ s += "(struct)"; // TODO: implement this as well.
+ } else {
+ auto table_field = reinterpret_cast<const Table *>(
+ ReadScalar<uoffset_t>(data) + data);
+ s += " { ";
+ auto fielddefs = objectdef.fields();
+ for (auto it = fielddefs->begin(); it != fielddefs->end(); ++it) {
+ auto &fielddef = **it;
+ if (!table_field->CheckField(fielddef.offset())) continue;
+ auto val = GetAnyFieldS(*table_field, fielddef, schema);
+ if (fielddef.type()->base_type() == reflection::String) {
+ std::string esc;
+ flatbuffers::EscapeString(val.c_str(), val.length(), &esc, true,
+ false);
+ val = esc;
+ }
+ s += fielddef.name()->str();
+ s += ": ";
+ s += val;
+ s += ", ";
+ }
+ s += "}";
+ }
+ return s;
+ } else {
+ return "(table)";
+ }
+ case reflection::Vector:
+ return "[(elements)]"; // TODO: implement this as well.
+ case reflection::Union: return "(union)"; // TODO: implement this as well.
+ default: return NumToString(GetAnyValueI(type, data));
+ }
+}
+
+void SetAnyValueI(reflection::BaseType type, uint8_t *data, int64_t val) {
+// clang-format off
+ #define FLATBUFFERS_SET(T) WriteScalar(data, static_cast<T>(val))
+ switch (type) {
+ case reflection::UType:
+ case reflection::Bool:
+ case reflection::UByte: FLATBUFFERS_SET(uint8_t ); break;
+ case reflection::Byte: FLATBUFFERS_SET(int8_t ); break;
+ case reflection::Short: FLATBUFFERS_SET(int16_t ); break;
+ case reflection::UShort: FLATBUFFERS_SET(uint16_t); break;
+ case reflection::Int: FLATBUFFERS_SET(int32_t ); break;
+ case reflection::UInt: FLATBUFFERS_SET(uint32_t); break;
+ case reflection::Long: FLATBUFFERS_SET(int64_t ); break;
+ case reflection::ULong: FLATBUFFERS_SET(uint64_t); break;
+ case reflection::Float: FLATBUFFERS_SET(float ); break;
+ case reflection::Double: FLATBUFFERS_SET(double ); break;
+ // TODO: support strings
+ default: break;
+ }
+ #undef FLATBUFFERS_SET
+ // clang-format on
+}
+
+void SetAnyValueF(reflection::BaseType type, uint8_t *data, double val) {
+ switch (type) {
+ case reflection::Float: WriteScalar(data, static_cast<float>(val)); break;
+ case reflection::Double: WriteScalar(data, val); break;
+ // TODO: support strings.
+ default: SetAnyValueI(type, data, static_cast<int64_t>(val)); break;
+ }
+}
+
+void SetAnyValueS(reflection::BaseType type, uint8_t *data, const char *val) {
+ switch (type) {
+ case reflection::Float:
+ case reflection::Double: {
+ double d;
+ StringToNumber(val, &d);
+ SetAnyValueF(type, data, d);
+ break;
+ }
+ // TODO: support strings.
+ default: SetAnyValueI(type, data, StringToInt(val)); break;
+ }
+}
+
+// Resize a FlatBuffer in-place by iterating through all offsets in the buffer
+// and adjusting them by "delta" if they straddle the start offset.
+// Once that is done, bytes can now be inserted/deleted safely.
+// "delta" may be negative (shrinking).
+// Unless "delta" is a multiple of the largest alignment, you'll create a small
+// amount of garbage space in the buffer (usually 0..7 bytes).
+// If your FlatBuffer's root table is not the schema's root table, you should
+// pass in your root_table type as well.
+class ResizeContext {
+ public:
+ ResizeContext(const reflection::Schema &schema, uoffset_t start, int delta,
+ std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table = nullptr)
+ : schema_(schema),
+ startptr_(vector_data(*flatbuf) + start),
+ delta_(delta),
+ buf_(*flatbuf),
+ dag_check_(flatbuf->size() / sizeof(uoffset_t), false) {
+ auto mask = static_cast<int>(sizeof(largest_scalar_t) - 1);
+ delta_ = (delta_ + mask) & ~mask;
+ if (!delta_) return; // We can't shrink by less than largest_scalar_t.
+ // Now change all the offsets by delta_.
+ auto root = GetAnyRoot(vector_data(buf_));
+ Straddle<uoffset_t, 1>(vector_data(buf_), root, vector_data(buf_));
+ ResizeTable(root_table ? *root_table : *schema.root_table(), root);
+ // We can now add or remove bytes at start.
+ if (delta_ > 0)
+ buf_.insert(buf_.begin() + start, delta_, 0);
+ else
+ buf_.erase(buf_.begin() + start + delta_, buf_.begin() + start);
+ }
+
+ // Check if the range between first (lower address) and second straddles
+ // the insertion point. If it does, change the offset at offsetloc (of
+ // type T, with direction D).
+ template<typename T, int D>
+ void Straddle(const void *first, const void *second, void *offsetloc) {
+ if (first <= startptr_ && second >= startptr_) {
+ WriteScalar<T>(offsetloc, ReadScalar<T>(offsetloc) + delta_ * D);
+ DagCheck(offsetloc) = true;
+ }
+ }
+
+ // This returns a boolean that records if the corresponding offset location
+ // has been modified already. If so, we can't even read the corresponding
+ // offset, since it is pointing to a location that is illegal until the
+ // resize actually happens.
+ // This must be checked for every offset, since we can't know which offsets
+ // will straddle and which won't.
+ uint8_t &DagCheck(const void *offsetloc) {
+ auto dag_idx = reinterpret_cast<const uoffset_t *>(offsetloc) -
+ reinterpret_cast<const uoffset_t *>(vector_data(buf_));
+ return dag_check_[dag_idx];
+ }
+
+ void ResizeTable(const reflection::Object &objectdef, Table *table) {
+ if (DagCheck(table)) return; // Table already visited.
+ auto vtable = table->GetVTable();
+ // Early out: since all fields inside the table must point forwards in
+ // memory, if the insertion point is before the table we can stop here.
+ auto tableloc = reinterpret_cast<uint8_t *>(table);
+ if (startptr_ <= tableloc) {
+ // Check if insertion point is between the table and a vtable that
+ // precedes it. This can't happen in current construction code, but check
+ // just in case we ever change the way flatbuffers are built.
+ Straddle<soffset_t, -1>(vtable, table, table);
+ } else {
+ // Check each field.
+ auto fielddefs = objectdef.fields();
+ for (auto it = fielddefs->begin(); it != fielddefs->end(); ++it) {
+ auto &fielddef = **it;
+ auto base_type = fielddef.type()->base_type();
+ // Ignore scalars.
+ if (base_type <= reflection::Double) continue;
+ // Ignore fields that are not stored.
+ auto offset = table->GetOptionalFieldOffset(fielddef.offset());
+ if (!offset) continue;
+ // Ignore structs.
+ auto subobjectdef =
+ base_type == reflection::Obj
+ ? schema_.objects()->Get(fielddef.type()->index())
+ : nullptr;
+ if (subobjectdef && subobjectdef->is_struct()) continue;
+ // Get this fields' offset, and read it if safe.
+ auto offsetloc = tableloc + offset;
+ if (DagCheck(offsetloc)) continue; // This offset already visited.
+ auto ref = offsetloc + ReadScalar<uoffset_t>(offsetloc);
+ Straddle<uoffset_t, 1>(offsetloc, ref, offsetloc);
+ // Recurse.
+ switch (base_type) {
+ case reflection::Obj: {
+ ResizeTable(*subobjectdef, reinterpret_cast<Table *>(ref));
+ break;
+ }
+ case reflection::Vector: {
+ auto elem_type = fielddef.type()->element();
+ if (elem_type != reflection::Obj && elem_type != reflection::String)
+ break;
+ auto vec = reinterpret_cast<Vector<uoffset_t> *>(ref);
+ auto elemobjectdef =
+ elem_type == reflection::Obj
+ ? schema_.objects()->Get(fielddef.type()->index())
+ : nullptr;
+ if (elemobjectdef && elemobjectdef->is_struct()) break;
+ for (uoffset_t i = 0; i < vec->size(); i++) {
+ auto loc = vec->Data() + i * sizeof(uoffset_t);
+ if (DagCheck(loc)) continue; // This offset already visited.
+ auto dest = loc + vec->Get(i);
+ Straddle<uoffset_t, 1>(loc, dest, loc);
+ if (elemobjectdef)
+ ResizeTable(*elemobjectdef, reinterpret_cast<Table *>(dest));
+ }
+ break;
+ }
+ case reflection::Union: {
+ ResizeTable(GetUnionType(schema_, objectdef, fielddef, *table),
+ reinterpret_cast<Table *>(ref));
+ break;
+ }
+ case reflection::String: break;
+ default: FLATBUFFERS_ASSERT(false);
+ }
+ }
+ // Check if the vtable offset points beyond the insertion point.
+ // Must do this last, since GetOptionalFieldOffset above still reads
+ // this value.
+ Straddle<soffset_t, -1>(table, vtable, table);
+ }
+ }
+
+ private:
+ const reflection::Schema &schema_;
+ uint8_t *startptr_;
+ int delta_;
+ std::vector<uint8_t> &buf_;
+ std::vector<uint8_t> dag_check_;
+};
+
+void SetString(const reflection::Schema &schema, const std::string &val,
+ const String *str, std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table) {
+ auto delta = static_cast<int>(val.size()) - static_cast<int>(str->size());
+ auto str_start = static_cast<uoffset_t>(
+ reinterpret_cast<const uint8_t *>(str) - vector_data(*flatbuf));
+ auto start = str_start + static_cast<uoffset_t>(sizeof(uoffset_t));
+ if (delta) {
+ // Clear the old string, since we don't want parts of it remaining.
+ memset(vector_data(*flatbuf) + start, 0, str->size());
+ // Different size, we must expand (or contract).
+ ResizeContext(schema, start, delta, flatbuf, root_table);
+ // Set the new length.
+ WriteScalar(vector_data(*flatbuf) + str_start,
+ static_cast<uoffset_t>(val.size()));
+ }
+ // Copy new data. Safe because we created the right amount of space.
+ memcpy(vector_data(*flatbuf) + start, val.c_str(), val.size() + 1);
+}
+
+uint8_t *ResizeAnyVector(const reflection::Schema &schema, uoffset_t newsize,
+ const VectorOfAny *vec, uoffset_t num_elems,
+ uoffset_t elem_size, std::vector<uint8_t> *flatbuf,
+ const reflection::Object *root_table) {
+ auto delta_elem = static_cast<int>(newsize) - static_cast<int>(num_elems);
+ auto delta_bytes = delta_elem * static_cast<int>(elem_size);
+ auto vec_start =
+ reinterpret_cast<const uint8_t *>(vec) - vector_data(*flatbuf);
+ auto start = static_cast<uoffset_t>(vec_start + sizeof(uoffset_t) +
+ elem_size * num_elems);
+ if (delta_bytes) {
+ if (delta_elem < 0) {
+ // Clear elements we're throwing away, since some might remain in the
+ // buffer.
+ auto size_clear = -delta_elem * elem_size;
+ memset(vector_data(*flatbuf) + start - size_clear, 0, size_clear);
+ }
+ ResizeContext(schema, start, delta_bytes, flatbuf, root_table);
+ WriteScalar(vector_data(*flatbuf) + vec_start, newsize); // Length field.
+ // Set new elements to 0.. this can be overwritten by the caller.
+ if (delta_elem > 0) {
+ memset(vector_data(*flatbuf) + start, 0, delta_elem * elem_size);
+ }
+ }
+ return vector_data(*flatbuf) + start;
+}
+
+const uint8_t *AddFlatBuffer(std::vector<uint8_t> &flatbuf,
+ const uint8_t *newbuf, size_t newlen) {
+ // Align to sizeof(uoffset_t) past sizeof(largest_scalar_t) since we're
+ // going to chop off the root offset.
+ while ((flatbuf.size() & (sizeof(uoffset_t) - 1)) ||
+ !(flatbuf.size() & (sizeof(largest_scalar_t) - 1))) {
+ flatbuf.push_back(0);
+ }
+ auto insertion_point = static_cast<uoffset_t>(flatbuf.size());
+ // Insert the entire FlatBuffer minus the root pointer.
+ flatbuf.insert(flatbuf.end(), newbuf + sizeof(uoffset_t), newbuf + newlen);
+ auto root_offset = ReadScalar<uoffset_t>(newbuf) - sizeof(uoffset_t);
+ return vector_data(flatbuf) + insertion_point + root_offset;
+}
+
+void CopyInline(FlatBufferBuilder &fbb, const reflection::Field &fielddef,
+ const Table &table, size_t align, size_t size) {
+ fbb.Align(align);
+ fbb.PushBytes(table.GetStruct<const uint8_t *>(fielddef.offset()), size);
+ fbb.TrackField(fielddef.offset(), fbb.GetSize());
+}
+
+Offset<const Table *> CopyTable(FlatBufferBuilder &fbb,
+ const reflection::Schema &schema,
+ const reflection::Object &objectdef,
+ const Table &table, bool use_string_pooling) {
+ // Before we can construct the table, we have to first generate any
+ // subobjects, and collect their offsets.
+ std::vector<uoffset_t> offsets;
+ auto fielddefs = objectdef.fields();
+ for (auto it = fielddefs->begin(); it != fielddefs->end(); ++it) {
+ auto &fielddef = **it;
+ // Skip if field is not present in the source.
+ if (!table.CheckField(fielddef.offset())) continue;
+ uoffset_t offset = 0;
+ switch (fielddef.type()->base_type()) {
+ case reflection::String: {
+ offset = use_string_pooling
+ ? fbb.CreateSharedString(GetFieldS(table, fielddef)).o
+ : fbb.CreateString(GetFieldS(table, fielddef)).o;
+ break;
+ }
+ case reflection::Obj: {
+ auto &subobjectdef = *schema.objects()->Get(fielddef.type()->index());
+ if (!subobjectdef.is_struct()) {
+ offset = CopyTable(fbb, schema, subobjectdef,
+ *GetFieldT(table, fielddef), use_string_pooling)
+ .o;
+ }
+ break;
+ }
+ case reflection::Union: {
+ auto &subobjectdef = GetUnionType(schema, objectdef, fielddef, table);
+ offset = CopyTable(fbb, schema, subobjectdef,
+ *GetFieldT(table, fielddef), use_string_pooling)
+ .o;
+ break;
+ }
+ case reflection::Vector: {
+ auto vec =
+ table.GetPointer<const Vector<Offset<Table>> *>(fielddef.offset());
+ auto element_base_type = fielddef.type()->element();
+ auto elemobjectdef =
+ element_base_type == reflection::Obj
+ ? schema.objects()->Get(fielddef.type()->index())
+ : nullptr;
+ switch (element_base_type) {
+ case reflection::String: {
+ std::vector<Offset<const String *>> elements(vec->size());
+ auto vec_s = reinterpret_cast<const Vector<Offset<String>> *>(vec);
+ for (uoffset_t i = 0; i < vec_s->size(); i++) {
+ elements[i] = use_string_pooling
+ ? fbb.CreateSharedString(vec_s->Get(i)).o
+ : fbb.CreateString(vec_s->Get(i)).o;
+ }
+ offset = fbb.CreateVector(elements).o;
+ break;
+ }
+ case reflection::Obj: {
+ if (!elemobjectdef->is_struct()) {
+ std::vector<Offset<const Table *>> elements(vec->size());
+ for (uoffset_t i = 0; i < vec->size(); i++) {
+ elements[i] = CopyTable(fbb, schema, *elemobjectdef,
+ *vec->Get(i), use_string_pooling);
+ }
+ offset = fbb.CreateVector(elements).o;
+ break;
+ }
+ }
+ FLATBUFFERS_FALLTHROUGH(); // fall thru
+ default: { // Scalars and structs.
+ auto element_size = GetTypeSize(element_base_type);
+ if (elemobjectdef && elemobjectdef->is_struct())
+ element_size = elemobjectdef->bytesize();
+ fbb.StartVector(vec->size(), element_size);
+ fbb.PushBytes(vec->Data(), element_size * vec->size());
+ offset = fbb.EndVector(vec->size());
+ break;
+ }
+ }
+ break;
+ }
+ default: // Scalars.
+ break;
+ }
+ if (offset) { offsets.push_back(offset); }
+ }
+ // Now we can build the actual table from either offsets or scalar data.
+ auto start = objectdef.is_struct() ? fbb.StartStruct(objectdef.minalign())
+ : fbb.StartTable();
+ size_t offset_idx = 0;
+ for (auto it = fielddefs->begin(); it != fielddefs->end(); ++it) {
+ auto &fielddef = **it;
+ if (!table.CheckField(fielddef.offset())) continue;
+ auto base_type = fielddef.type()->base_type();
+ switch (base_type) {
+ case reflection::Obj: {
+ auto &subobjectdef = *schema.objects()->Get(fielddef.type()->index());
+ if (subobjectdef.is_struct()) {
+ CopyInline(fbb, fielddef, table, subobjectdef.minalign(),
+ subobjectdef.bytesize());
+ break;
+ }
+ }
+ FLATBUFFERS_FALLTHROUGH(); // fall thru
+ case reflection::Union:
+ case reflection::String:
+ case reflection::Vector:
+ fbb.AddOffset(fielddef.offset(), Offset<void>(offsets[offset_idx++]));
+ break;
+ default: { // Scalars.
+ auto size = GetTypeSize(base_type);
+ CopyInline(fbb, fielddef, table, size, size);
+ break;
+ }
+ }
+ }
+ FLATBUFFERS_ASSERT(offset_idx == offsets.size());
+ if (objectdef.is_struct()) {
+ fbb.ClearOffsets();
+ return fbb.EndStruct();
+ } else {
+ return fbb.EndTable(start);
+ }
+}
+
+bool VerifyStruct(flatbuffers::Verifier &v,
+ const flatbuffers::Table &parent_table,
+ voffset_t field_offset, const reflection::Object &obj,
+ bool required) {
+ auto offset = parent_table.GetOptionalFieldOffset(field_offset);
+ if (required && !offset) { return false; }
+
+ return !offset || v.Verify(reinterpret_cast<const uint8_t *>(&parent_table),
+ offset, obj.bytesize());
+}
+
+bool VerifyVectorOfStructs(flatbuffers::Verifier &v,
+ const flatbuffers::Table &parent_table,
+ voffset_t field_offset,
+ const reflection::Object &obj, bool required) {
+ auto p = parent_table.GetPointer<const uint8_t *>(field_offset);
+ if (required && !p) { return false; }
+
+ return !p || v.VerifyVectorOrString(p, obj.bytesize());
+}
+
+// forward declare to resolve cyclic deps between VerifyObject and VerifyVector
+bool VerifyObject(flatbuffers::Verifier &v, const reflection::Schema &schema,
+ const reflection::Object &obj,
+ const flatbuffers::Table *table, bool required);
+
+bool VerifyUnion(flatbuffers::Verifier &v, const reflection::Schema &schema,
+ uint8_t utype, const uint8_t *elem,
+ const reflection::Field &union_field) {
+ if (!utype) return true; // Not present.
+ auto fb_enum = schema.enums()->Get(union_field.type()->index());
+ if (utype >= fb_enum->values()->size()) return false;
+ auto elem_type = fb_enum->values()->Get(utype)->union_type();
+ switch (elem_type->base_type()) {
+ case reflection::Obj: {
+ auto elem_obj = schema.objects()->Get(elem_type->index());
+ if (elem_obj->is_struct()) {
+ return v.VerifyFromPointer(elem, elem_obj->bytesize());
+ } else {
+ return VerifyObject(v, schema, *elem_obj,
+ reinterpret_cast<const flatbuffers::Table *>(elem),
+ true);
+ }
+ }
+ case reflection::String:
+ return v.VerifyString(
+ reinterpret_cast<const flatbuffers::String *>(elem));
+ default: return false;
+ }
+}
+
+bool VerifyVector(flatbuffers::Verifier &v, const reflection::Schema &schema,
+ const flatbuffers::Table &table,
+ const reflection::Field &vec_field) {
+ FLATBUFFERS_ASSERT(vec_field.type()->base_type() == reflection::Vector);
+ if (!table.VerifyField<uoffset_t>(v, vec_field.offset())) return false;
+
+ switch (vec_field.type()->element()) {
+ case reflection::UType:
+ return v.VerifyVector(flatbuffers::GetFieldV<uint8_t>(table, vec_field));
+ case reflection::Bool:
+ case reflection::Byte:
+ case reflection::UByte:
+ return v.VerifyVector(flatbuffers::GetFieldV<int8_t>(table, vec_field));
+ case reflection::Short:
+ case reflection::UShort:
+ return v.VerifyVector(flatbuffers::GetFieldV<int16_t>(table, vec_field));
+ case reflection::Int:
+ case reflection::UInt:
+ return v.VerifyVector(flatbuffers::GetFieldV<int32_t>(table, vec_field));
+ case reflection::Long:
+ case reflection::ULong:
+ return v.VerifyVector(flatbuffers::GetFieldV<int64_t>(table, vec_field));
+ case reflection::Float:
+ return v.VerifyVector(flatbuffers::GetFieldV<float>(table, vec_field));
+ case reflection::Double:
+ return v.VerifyVector(flatbuffers::GetFieldV<double>(table, vec_field));
+ case reflection::String: {
+ auto vec_string =
+ flatbuffers::GetFieldV<flatbuffers::Offset<flatbuffers::String>>(
+ table, vec_field);
+ if (v.VerifyVector(vec_string) && v.VerifyVectorOfStrings(vec_string)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ case reflection::Obj: {
+ auto obj = schema.objects()->Get(vec_field.type()->index());
+ if (obj->is_struct()) {
+ return VerifyVectorOfStructs(v, table, vec_field.offset(), *obj,
+ vec_field.required());
+ } else {
+ auto vec =
+ flatbuffers::GetFieldV<flatbuffers::Offset<flatbuffers::Table>>(
+ table, vec_field);
+ if (!v.VerifyVector(vec)) return false;
+ if (!vec) return true;
+ for (uoffset_t j = 0; j < vec->size(); j++) {
+ if (!VerifyObject(v, schema, *obj, vec->Get(j), true)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ case reflection::Union: {
+ auto vec = flatbuffers::GetFieldV<flatbuffers::Offset<uint8_t>>(
+ table, vec_field);
+ if (!v.VerifyVector(vec)) return false;
+ if (!vec) return true;
+ auto type_vec = table.GetPointer<Vector<uint8_t> *>(vec_field.offset() -
+ sizeof(voffset_t));
+ if (!v.VerifyVector(type_vec)) return false;
+ for (uoffset_t j = 0; j < vec->size(); j++) {
+ // get union type from the prev field
+ auto utype = type_vec->Get(j);
+ auto elem = vec->Get(j);
+ if (!VerifyUnion(v, schema, utype, elem, vec_field)) return false;
+ }
+ return true;
+ }
+ case reflection::Vector:
+ case reflection::None:
+ default: FLATBUFFERS_ASSERT(false); return false;
+ }
+}
+
+bool VerifyObject(flatbuffers::Verifier &v, const reflection::Schema &schema,
+ const reflection::Object &obj,
+ const flatbuffers::Table *table, bool required) {
+ if (!table) return !required;
+ if (!table->VerifyTableStart(v)) return false;
+ for (uoffset_t i = 0; i < obj.fields()->size(); i++) {
+ auto field_def = obj.fields()->Get(i);
+ switch (field_def->type()->base_type()) {
+ case reflection::None: FLATBUFFERS_ASSERT(false); break;
+ case reflection::UType:
+ if (!table->VerifyField<uint8_t>(v, field_def->offset())) return false;
+ break;
+ case reflection::Bool:
+ case reflection::Byte:
+ case reflection::UByte:
+ if (!table->VerifyField<int8_t>(v, field_def->offset())) return false;
+ break;
+ case reflection::Short:
+ case reflection::UShort:
+ if (!table->VerifyField<int16_t>(v, field_def->offset())) return false;
+ break;
+ case reflection::Int:
+ case reflection::UInt:
+ if (!table->VerifyField<int32_t>(v, field_def->offset())) return false;
+ break;
+ case reflection::Long:
+ case reflection::ULong:
+ if (!table->VerifyField<int64_t>(v, field_def->offset())) return false;
+ break;
+ case reflection::Float:
+ if (!table->VerifyField<float>(v, field_def->offset())) return false;
+ break;
+ case reflection::Double:
+ if (!table->VerifyField<double>(v, field_def->offset())) return false;
+ break;
+ case reflection::String:
+ if (!table->VerifyField<uoffset_t>(v, field_def->offset()) ||
+ !v.VerifyString(flatbuffers::GetFieldS(*table, *field_def))) {
+ return false;
+ }
+ break;
+ case reflection::Vector:
+ if (!VerifyVector(v, schema, *table, *field_def)) return false;
+ break;
+ case reflection::Obj: {
+ auto child_obj = schema.objects()->Get(field_def->type()->index());
+ if (child_obj->is_struct()) {
+ if (!VerifyStruct(v, *table, field_def->offset(), *child_obj,
+ field_def->required())) {
+ return false;
+ }
+ } else {
+ if (!VerifyObject(v, schema, *child_obj,
+ flatbuffers::GetFieldT(*table, *field_def),
+ field_def->required())) {
+ return false;
+ }
+ }
+ break;
+ }
+ case reflection::Union: {
+ // get union type from the prev field
+ voffset_t utype_offset = field_def->offset() - sizeof(voffset_t);
+ auto utype = table->GetField<uint8_t>(utype_offset, 0);
+ auto uval = reinterpret_cast<const uint8_t *>(
+ flatbuffers::GetFieldT(*table, *field_def));
+ if (!VerifyUnion(v, schema, utype, uval, *field_def)) { return false; }
+ break;
+ }
+ default: FLATBUFFERS_ASSERT(false); break;
+ }
+ }
+
+ if (!v.EndTable()) return false;
+
+ return true;
+}
+
+bool Verify(const reflection::Schema &schema, const reflection::Object &root,
+ const uint8_t *buf, size_t length, uoffset_t max_depth /*= 64*/,
+ uoffset_t max_tables /*= 1000000*/) {
+ Verifier v(buf, length, max_depth, max_tables);
+ return VerifyObject(v, schema, root, flatbuffers::GetAnyRoot(buf), true);
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/src/util.cpp b/contrib/libs/flatbuffers/src/util.cpp
new file mode 100644
index 0000000000..3670a01939
--- /dev/null
+++ b/contrib/libs/flatbuffers/src/util.cpp
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// clang-format off
+// Dont't remove `format off`, it prevent reordering of win-includes.
+
+#if defined(__MINGW32__) || defined(__MINGW64__) || defined(__CYGWIN__) || \
+ defined(__QNXNTO__)
+# define _POSIX_C_SOURCE 200809L
+# define _XOPEN_SOURCE 700L
+#endif
+
+#ifdef _WIN32
+# ifndef WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN
+# endif
+# ifndef NOMINMAX
+# define NOMINMAX
+# endif
+# ifdef _MSC_VER
+# include <crtdbg.h>
+# endif
+# include <windows.h> // Must be included before <direct.h>
+# include <direct.h>
+# include <winbase.h>
+# undef interface // This is also important because of reasons
+#endif
+// clang-format on
+
+#include "flatbuffers/base.h"
+#include "flatbuffers/util.h"
+
+#include <sys/stat.h>
+#include <clocale>
+#include <cstdlib>
+#include <fstream>
+
+namespace flatbuffers {
+
+bool FileExistsRaw(const char *name) {
+ std::ifstream ifs(name);
+ return ifs.good();
+}
+
+bool LoadFileRaw(const char *name, bool binary, std::string *buf) {
+ if (DirExists(name)) return false;
+ std::ifstream ifs(name, binary ? std::ifstream::binary : std::ifstream::in);
+ if (!ifs.is_open()) return false;
+ if (binary) {
+ // The fastest way to read a file into a string.
+ ifs.seekg(0, std::ios::end);
+ auto size = ifs.tellg();
+ (*buf).resize(static_cast<size_t>(size));
+ ifs.seekg(0, std::ios::beg);
+ ifs.read(&(*buf)[0], (*buf).size());
+ } else {
+ // This is slower, but works correctly on all platforms for text files.
+ std::ostringstream oss;
+ oss << ifs.rdbuf();
+ *buf = oss.str();
+ }
+ return !ifs.bad();
+}
+
+static LoadFileFunction g_load_file_function = LoadFileRaw;
+static FileExistsFunction g_file_exists_function = FileExistsRaw;
+
+bool LoadFile(const char *name, bool binary, std::string *buf) {
+ FLATBUFFERS_ASSERT(g_load_file_function);
+ return g_load_file_function(name, binary, buf);
+}
+
+bool FileExists(const char *name) {
+ FLATBUFFERS_ASSERT(g_file_exists_function);
+ return g_file_exists_function(name);
+}
+
+bool DirExists(const char *name) {
+ // clang-format off
+
+ #ifdef _WIN32
+ #define flatbuffers_stat _stat
+ #define FLATBUFFERS_S_IFDIR _S_IFDIR
+ #else
+ #define flatbuffers_stat stat
+ #define FLATBUFFERS_S_IFDIR S_IFDIR
+ #endif
+ // clang-format on
+ struct flatbuffers_stat file_info;
+ if (flatbuffers_stat(name, &file_info) != 0) return false;
+ return (file_info.st_mode & FLATBUFFERS_S_IFDIR) != 0;
+}
+
+LoadFileFunction SetLoadFileFunction(LoadFileFunction load_file_function) {
+ LoadFileFunction previous_function = g_load_file_function;
+ g_load_file_function = load_file_function ? load_file_function : LoadFileRaw;
+ return previous_function;
+}
+
+FileExistsFunction SetFileExistsFunction(
+ FileExistsFunction file_exists_function) {
+ FileExistsFunction previous_function = g_file_exists_function;
+ g_file_exists_function =
+ file_exists_function ? file_exists_function : FileExistsRaw;
+ return previous_function;
+}
+
+bool SaveFile(const char *name, const char *buf, size_t len, bool binary) {
+ std::ofstream ofs(name, binary ? std::ofstream::binary : std::ofstream::out);
+ if (!ofs.is_open()) return false;
+ ofs.write(buf, len);
+ return !ofs.bad();
+}
+
+// We internally store paths in posix format ('/'). Paths supplied
+// by the user should go through PosixPath to ensure correct behavior
+// on Windows when paths are string-compared.
+
+static const char kPathSeparatorWindows = '\\';
+static const char *PathSeparatorSet = "\\/"; // Intentionally no ':'
+
+std::string StripExtension(const std::string &filepath) {
+ size_t i = filepath.find_last_of('.');
+ return i != std::string::npos ? filepath.substr(0, i) : filepath;
+}
+
+std::string GetExtension(const std::string &filepath) {
+ size_t i = filepath.find_last_of('.');
+ return i != std::string::npos ? filepath.substr(i + 1) : "";
+}
+
+std::string StripPath(const std::string &filepath) {
+ size_t i = filepath.find_last_of(PathSeparatorSet);
+ return i != std::string::npos ? filepath.substr(i + 1) : filepath;
+}
+
+std::string StripFileName(const std::string &filepath) {
+ size_t i = filepath.find_last_of(PathSeparatorSet);
+ return i != std::string::npos ? filepath.substr(0, i) : "";
+}
+
+std::string ConCatPathFileName(const std::string &path,
+ const std::string &filename) {
+ std::string filepath = path;
+ if (filepath.length()) {
+ char &filepath_last_character = string_back(filepath);
+ if (filepath_last_character == kPathSeparatorWindows) {
+ filepath_last_character = kPathSeparator;
+ } else if (filepath_last_character != kPathSeparator) {
+ filepath += kPathSeparator;
+ }
+ }
+ filepath += filename;
+ // Ignore './' at the start of filepath.
+ if (filepath[0] == '.' && filepath[1] == kPathSeparator) {
+ filepath.erase(0, 2);
+ }
+ return filepath;
+}
+
+std::string PosixPath(const char *path) {
+ std::string p = path;
+ std::replace(p.begin(), p.end(), '\\', '/');
+ return p;
+}
+
+void EnsureDirExists(const std::string &filepath) {
+ auto parent = StripFileName(filepath);
+ if (parent.length()) EnsureDirExists(parent);
+ // clang-format off
+
+ #ifdef _WIN32
+ (void)_mkdir(filepath.c_str());
+ #else
+ mkdir(filepath.c_str(), S_IRWXU|S_IRGRP|S_IXGRP);
+ #endif
+ // clang-format on
+}
+
+std::string AbsolutePath(const std::string &filepath) {
+ // clang-format off
+
+ #ifdef FLATBUFFERS_NO_ABSOLUTE_PATH_RESOLUTION
+ return filepath;
+ #else
+ #ifdef _WIN32
+ char abs_path[MAX_PATH];
+ return GetFullPathNameA(filepath.c_str(), MAX_PATH, abs_path, nullptr)
+ #else
+ char *abs_path_temp = realpath(filepath.c_str(), nullptr);
+ bool success = abs_path_temp != nullptr;
+ std::string abs_path;
+ if(success) {
+ abs_path = abs_path_temp;
+ free(abs_path_temp);
+ }
+ return success
+ #endif
+ ? abs_path
+ : filepath;
+ #endif // FLATBUFFERS_NO_ABSOLUTE_PATH_RESOLUTION
+ // clang-format on
+}
+
+// Locale-independent code.
+#if defined(FLATBUFFERS_LOCALE_INDEPENDENT) && \
+ (FLATBUFFERS_LOCALE_INDEPENDENT > 0)
+
+// clang-format off
+// Allocate locale instance at startup of application.
+ClassicLocale ClassicLocale::instance_;
+
+#ifdef _MSC_VER
+ ClassicLocale::ClassicLocale()
+ : locale_(_create_locale(LC_ALL, "C")) {}
+ ClassicLocale::~ClassicLocale() { _free_locale(locale_); }
+#else
+ ClassicLocale::ClassicLocale()
+ : locale_(newlocale(LC_ALL, "C", nullptr)) {}
+ ClassicLocale::~ClassicLocale() { freelocale(locale_); }
+#endif
+// clang-format on
+
+#endif // !FLATBUFFERS_LOCALE_INDEPENDENT
+
+std::string RemoveStringQuotes(const std::string &s) {
+ auto ch = *s.c_str();
+ return ((s.size() >= 2) && (ch == '\"' || ch == '\'') &&
+ (ch == string_back(s)))
+ ? s.substr(1, s.length() - 2)
+ : s;
+}
+
+bool SetGlobalTestLocale(const char *locale_name, std::string *_value) {
+ const auto the_locale = setlocale(LC_ALL, locale_name);
+ if (!the_locale) return false;
+ if (_value) *_value = std::string(the_locale);
+ return true;
+}
+
+bool ReadEnvironmentVariable(const char *var_name, std::string *_value) {
+#ifdef _MSC_VER
+ __pragma(warning(disable : 4996)); // _CRT_SECURE_NO_WARNINGS
+#endif
+ auto env_str = std::getenv(var_name);
+ if (!env_str) return false;
+ if (_value) *_value = std::string(env_str);
+ return true;
+}
+
+void SetupDefaultCRTReportMode() {
+ // clang-format off
+
+ #ifdef _MSC_VER
+ // By default, send all reports to STDOUT to prevent CI hangs.
+ // Enable assert report box [Abort|Retry|Ignore] if a debugger is present.
+ const int dbg_mode = (_CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG) |
+ (IsDebuggerPresent() ? _CRTDBG_MODE_WNDW : 0);
+ (void)dbg_mode; // release mode fix
+ // CrtDebug reports to _CRT_WARN channel.
+ _CrtSetReportMode(_CRT_WARN, dbg_mode);
+ _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);
+ // The assert from <assert.h> reports to _CRT_ERROR channel
+ _CrtSetReportMode(_CRT_ERROR, dbg_mode);
+ _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDOUT);
+ // Internal CRT assert channel?
+ _CrtSetReportMode(_CRT_ASSERT, dbg_mode);
+ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT);
+ #endif
+
+ // clang-format on
+}
+
+} // namespace flatbuffers
diff --git a/contrib/libs/flatbuffers/ya.make b/contrib/libs/flatbuffers/ya.make
new file mode 100644
index 0000000000..0fa3e01129
--- /dev/null
+++ b/contrib/libs/flatbuffers/ya.make
@@ -0,0 +1,40 @@
+# Generated by devtools/yamaker from nixpkgs 22.05.
+
+LIBRARY()
+
+VERSION(2.0.0)
+
+ORIGINAL_SOURCE(https://github.com/google/flatbuffers/archive/v2.0.0.tar.gz)
+
+LICENSE(
+ Apache-2.0 AND
+ BSD-3-Clause
+)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+ADDINCL(
+ contrib/libs/flatbuffers/include
+)
+
+NO_COMPILER_WARNINGS()
+
+NO_UTIL()
+
+CFLAGS(
+ -DFLATBUFFERS_LOCALE_INDEPENDENT=1
+)
+
+SRCS(
+ src/idl_gen_text.cpp
+ src/idl_parser.cpp
+ src/reflection.cpp
+ src/util.cpp
+)
+
+END()
+
+RECURSE(
+ flatc
+ samples
+)
diff --git a/contrib/libs/isa-l/erasure_code/ec_base.c b/contrib/libs/isa-l/erasure_code/ec_base.c
new file mode 100644
index 0000000000..9a8fbc759e
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ec_base.c
@@ -0,0 +1,371 @@
+/**********************************************************************
+ Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+#include <limits.h>
+#include <string.h> // for memset
+#include "erasure_code.h"
+#include "ec_base.h" // for GF tables
+
+void ec_init_tables(int k, int rows, unsigned char *a, unsigned char *g_tbls)
+{
+ int i, j;
+
+ for (i = 0; i < rows; i++) {
+ for (j = 0; j < k; j++) {
+ gf_vect_mul_init(*a++, g_tbls);
+ g_tbls += 32;
+ }
+ }
+}
+
+unsigned char gf_mul_erasure(unsigned char a, unsigned char b)
+{
+#ifndef GF_LARGE_TABLES
+ int i;
+
+ if ((a == 0) || (b == 0))
+ return 0;
+
+ return gff_base[(i = gflog_base[a] + gflog_base[b]) > 254 ? i - 255 : i];
+#else
+ return gf_mul_table_base[b * 256 + a];
+#endif
+}
+
+unsigned char gf_inv(unsigned char a)
+{
+#ifndef GF_LARGE_TABLES
+ if (a == 0)
+ return 0;
+
+ return gff_base[255 - gflog_base[a]];
+#else
+ return gf_inv_table_base[a];
+#endif
+}
+
+void gf_gen_rs_matrix(unsigned char *a, int m, int k)
+{
+ int i, j;
+ unsigned char p, gen = 1;
+
+ memset(a, 0, k * m);
+ for (i = 0; i < k; i++)
+ a[k * i + i] = 1;
+
+ for (i = k; i < m; i++) {
+ p = 1;
+ for (j = 0; j < k; j++) {
+ a[k * i + j] = p;
+ p = gf_mul_erasure(p, gen);
+ }
+ gen = gf_mul_erasure(gen, 2);
+ }
+}
+
+void gf_gen_cauchy1_matrix(unsigned char *a, int m, int k)
+{
+ int i, j;
+ unsigned char *p;
+
+ // Identity matrix in high position
+ memset(a, 0, k * m);
+ for (i = 0; i < k; i++)
+ a[k * i + i] = 1;
+
+ // For the rest choose 1/(i + j) | i != j
+ p = &a[k * k];
+ for (i = k; i < m; i++)
+ for (j = 0; j < k; j++)
+ *p++ = gf_inv(i ^ j);
+
+}
+
+int gf_invert_matrix(unsigned char *in_mat, unsigned char *out_mat, const int n)
+{
+ int i, j, k;
+ unsigned char temp;
+
+ // Set out_mat[] to the identity matrix
+ for (i = 0; i < n * n; i++) // memset(out_mat, 0, n*n)
+ out_mat[i] = 0;
+
+ for (i = 0; i < n; i++)
+ out_mat[i * n + i] = 1;
+
+ // Inverse
+ for (i = 0; i < n; i++) {
+ // Check for 0 in pivot element
+ if (in_mat[i * n + i] == 0) {
+ // Find a row with non-zero in current column and swap
+ for (j = i + 1; j < n; j++)
+ if (in_mat[j * n + i])
+ break;
+
+ if (j == n) // Couldn't find means it's singular
+ return -1;
+
+ for (k = 0; k < n; k++) { // Swap rows i,j
+ temp = in_mat[i * n + k];
+ in_mat[i * n + k] = in_mat[j * n + k];
+ in_mat[j * n + k] = temp;
+
+ temp = out_mat[i * n + k];
+ out_mat[i * n + k] = out_mat[j * n + k];
+ out_mat[j * n + k] = temp;
+ }
+ }
+
+ temp = gf_inv(in_mat[i * n + i]); // 1/pivot
+ for (j = 0; j < n; j++) { // Scale row i by 1/pivot
+ in_mat[i * n + j] = gf_mul_erasure(in_mat[i * n + j], temp);
+ out_mat[i * n + j] = gf_mul_erasure(out_mat[i * n + j], temp);
+ }
+
+ for (j = 0; j < n; j++) {
+ if (j == i)
+ continue;
+
+ temp = in_mat[j * n + i];
+ for (k = 0; k < n; k++) {
+ out_mat[j * n + k] ^= gf_mul_erasure(temp, out_mat[i * n + k]);
+ in_mat[j * n + k] ^= gf_mul_erasure(temp, in_mat[i * n + k]);
+ }
+ }
+ }
+ return 0;
+}
+
+// Calculates const table gftbl in GF(2^8) from single input A
+// gftbl(A) = {A{00}, A{01}, A{02}, ... , A{0f} }, {A{00}, A{10}, A{20}, ... , A{f0} }
+
+void gf_vect_mul_init(unsigned char c, unsigned char *tbl)
+{
+ unsigned char c2 = (c << 1) ^ ((c & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ unsigned char c4 = (c2 << 1) ^ ((c2 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ unsigned char c8 = (c4 << 1) ^ ((c4 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+
+#if __WORDSIZE == 64 || _WIN64 || __x86_64__
+ unsigned long long v1, v2, v4, v8, *t;
+ unsigned long long v10, v20, v40, v80;
+ unsigned char c17, c18, c20, c24;
+
+ t = (unsigned long long *)tbl;
+
+ v1 = c * 0x0100010001000100ull;
+ v2 = c2 * 0x0101000001010000ull;
+ v4 = c4 * 0x0101010100000000ull;
+ v8 = c8 * 0x0101010101010101ull;
+
+ v4 = v1 ^ v2 ^ v4;
+ t[0] = v4;
+ t[1] = v8 ^ v4;
+
+ c17 = (c8 << 1) ^ ((c8 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c18 = (c17 << 1) ^ ((c17 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c20 = (c18 << 1) ^ ((c18 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c24 = (c20 << 1) ^ ((c20 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+
+ v10 = c17 * 0x0100010001000100ull;
+ v20 = c18 * 0x0101000001010000ull;
+ v40 = c20 * 0x0101010100000000ull;
+ v80 = c24 * 0x0101010101010101ull;
+
+ v40 = v10 ^ v20 ^ v40;
+ t[2] = v40;
+ t[3] = v80 ^ v40;
+
+#else // 32-bit or other
+ unsigned char c3, c5, c6, c7, c9, c10, c11, c12, c13, c14, c15;
+ unsigned char c17, c18, c19, c20, c21, c22, c23, c24, c25, c26, c27, c28, c29, c30,
+ c31;
+
+ c3 = c2 ^ c;
+ c5 = c4 ^ c;
+ c6 = c4 ^ c2;
+ c7 = c4 ^ c3;
+
+ c9 = c8 ^ c;
+ c10 = c8 ^ c2;
+ c11 = c8 ^ c3;
+ c12 = c8 ^ c4;
+ c13 = c8 ^ c5;
+ c14 = c8 ^ c6;
+ c15 = c8 ^ c7;
+
+ tbl[0] = 0;
+ tbl[1] = c;
+ tbl[2] = c2;
+ tbl[3] = c3;
+ tbl[4] = c4;
+ tbl[5] = c5;
+ tbl[6] = c6;
+ tbl[7] = c7;
+ tbl[8] = c8;
+ tbl[9] = c9;
+ tbl[10] = c10;
+ tbl[11] = c11;
+ tbl[12] = c12;
+ tbl[13] = c13;
+ tbl[14] = c14;
+ tbl[15] = c15;
+
+ c17 = (c8 << 1) ^ ((c8 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c18 = (c17 << 1) ^ ((c17 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c19 = c18 ^ c17;
+ c20 = (c18 << 1) ^ ((c18 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c21 = c20 ^ c17;
+ c22 = c20 ^ c18;
+ c23 = c20 ^ c19;
+ c24 = (c20 << 1) ^ ((c20 & 0x80) ? 0x1d : 0); //Mult by GF{2}
+ c25 = c24 ^ c17;
+ c26 = c24 ^ c18;
+ c27 = c24 ^ c19;
+ c28 = c24 ^ c20;
+ c29 = c24 ^ c21;
+ c30 = c24 ^ c22;
+ c31 = c24 ^ c23;
+
+ tbl[16] = 0;
+ tbl[17] = c17;
+ tbl[18] = c18;
+ tbl[19] = c19;
+ tbl[20] = c20;
+ tbl[21] = c21;
+ tbl[22] = c22;
+ tbl[23] = c23;
+ tbl[24] = c24;
+ tbl[25] = c25;
+ tbl[26] = c26;
+ tbl[27] = c27;
+ tbl[28] = c28;
+ tbl[29] = c29;
+ tbl[30] = c30;
+ tbl[31] = c31;
+
+#endif //__WORDSIZE == 64 || _WIN64 || __x86_64__
+}
+
+void gf_vect_dot_prod_base(int len, int vlen, unsigned char *v,
+ unsigned char **src, unsigned char *dest)
+{
+ int i, j;
+ unsigned char s;
+ for (i = 0; i < len; i++) {
+ s = 0;
+ for (j = 0; j < vlen; j++)
+ s ^= gf_mul_erasure(src[j][i], v[j * 32 + 1]);
+
+ dest[i] = s;
+ }
+}
+
+void gf_vect_mad_base(int len, int vec, int vec_i,
+ unsigned char *v, unsigned char *src, unsigned char *dest)
+{
+ int i;
+ unsigned char s;
+ for (i = 0; i < len; i++) {
+ s = dest[i];
+ s ^= gf_mul_erasure(src[i], v[vec_i * 32 + 1]);
+ dest[i] = s;
+ }
+}
+
+void ec_encode_data_base(int len, int srcs, int dests, unsigned char *v,
+ unsigned char **src, unsigned char **dest)
+{
+ int i, j, l;
+ unsigned char s;
+
+ for (l = 0; l < dests; l++) {
+ for (i = 0; i < len; i++) {
+ s = 0;
+ for (j = 0; j < srcs; j++)
+ s ^= gf_mul_erasure(src[j][i], v[j * 32 + l * srcs * 32 + 1]);
+
+ dest[l][i] = s;
+ }
+ }
+}
+
+void ec_encode_data_update_base(int len, int k, int rows, int vec_i, unsigned char *v,
+ unsigned char *data, unsigned char **dest)
+{
+ int i, l;
+ unsigned char s;
+
+ for (l = 0; l < rows; l++) {
+ for (i = 0; i < len; i++) {
+ s = dest[l][i];
+ s ^= gf_mul_erasure(data[i], v[vec_i * 32 + l * k * 32 + 1]);
+
+ dest[l][i] = s;
+ }
+ }
+}
+
+void gf_vect_mul_base(int len, unsigned char *a, unsigned char *src, unsigned char *dest)
+{
+ //2nd element of table array is ref value used to fill it in
+ unsigned char c = a[1];
+ while (len-- > 0)
+ *dest++ = gf_mul_erasure(c, *src++);
+}
+
+struct slver {
+ unsigned short snum;
+ unsigned char ver;
+ unsigned char core;
+};
+
+// Version info
+struct slver gf_vect_mul_init_slver_00020035;
+struct slver gf_vect_mul_init_slver = { 0x0035, 0x02, 0x00 };
+
+struct slver ec_encode_data_base_slver_00010135;
+struct slver ec_encode_data_base_slver = { 0x0135, 0x01, 0x00 };
+
+struct slver gf_vect_mul_base_slver_00010136;
+struct slver gf_vect_mul_base_slver = { 0x0136, 0x01, 0x00 };
+
+struct slver gf_vect_dot_prod_base_slver_00010137;
+struct slver gf_vect_dot_prod_base_slver = { 0x0137, 0x01, 0x00 };
+
+struct slver gf_mul_slver_00000214;
+struct slver gf_mul_slver = { 0x0214, 0x00, 0x00 };
+
+struct slver gf_invert_matrix_slver_00000215;
+struct slver gf_invert_matrix_slver = { 0x0215, 0x00, 0x00 };
+
+struct slver gf_gen_rs_matrix_slver_00000216;
+struct slver gf_gen_rs_matrix_slver = { 0x0216, 0x00, 0x00 };
+
+struct slver gf_gen_cauchy1_matrix_slver_00000217;
+struct slver gf_gen_cauchy1_matrix_slver = { 0x0217, 0x00, 0x00 };
diff --git a/contrib/libs/isa-l/erasure_code/ec_base.h b/contrib/libs/isa-l/erasure_code/ec_base.h
new file mode 100644
index 0000000000..070b276652
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ec_base.h
@@ -0,0 +1,6680 @@
+/**********************************************************************
+ Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+#ifndef _EC_BASE_H_
+#define _EC_BASE_H_
+
+// Global GF(256) tables
+#ifndef GF_LARGE_TABLES
+static const unsigned char gff_base[] = {
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a,
+ 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, 0x4c, 0x98, 0x2d, 0x5a,
+ 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30,
+ 0x60, 0xc0, 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35,
+ 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, 0x46, 0x8c,
+ 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2,
+ 0xb9, 0x6f, 0xde, 0xa1, 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f,
+ 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
+ 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1,
+ 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, 0xd9, 0xaf, 0x43, 0x86,
+ 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd,
+ 0x67, 0xce, 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93,
+ 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc, 0x85, 0x17,
+ 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42,
+ 0x84, 0x15, 0x2a, 0x54, 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4,
+ 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
+ 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5,
+ 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, 0xe3, 0xdb, 0xab, 0x4b,
+ 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57,
+ 0xae, 0x41, 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e,
+ 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, 0x51, 0xa2,
+ 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56,
+ 0xac, 0x45, 0x8a, 0x09, 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a,
+ 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
+ 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36,
+ 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x01
+};
+
+static const unsigned char gflog_base[] = {
+ 0x00, 0xff, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf,
+ 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, 0x04, 0x64, 0xe0, 0x0e,
+ 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08,
+ 0x4c, 0x71, 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21,
+ 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, 0x1d, 0xb5,
+ 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78,
+ 0x4d, 0xe4, 0x72, 0xa6, 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd,
+ 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
+ 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2,
+ 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, 0x1e, 0x42, 0xb6, 0xa3,
+ 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85,
+ 0xba, 0x3d, 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b,
+ 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57, 0x07, 0x70,
+ 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed,
+ 0x31, 0xc5, 0xfe, 0x18, 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8,
+ 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
+ 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87,
+ 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, 0xf2, 0x56, 0xd3, 0xab,
+ 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d,
+ 0x41, 0xa2, 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76,
+ 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, 0x6c, 0xa1,
+ 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1,
+ 0xbb, 0xcc, 0x3e, 0x5a, 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9,
+ 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
+ 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6,
+ 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
+};
+#else
+static const unsigned char gf_mul_table_base[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
+ 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21,
+ 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+ 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35,
+ 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
+ 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53,
+ 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d,
+ 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
+ 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71,
+ 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b,
+ 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85,
+ 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
+ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
+ 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3,
+ 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad,
+ 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
+ 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1,
+ 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb,
+ 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5,
+ 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
+ 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
+ 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3,
+ 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd,
+ 0xfe, 0xff, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e,
+ 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22,
+ 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 0x30, 0x32, 0x34, 0x36,
+ 0x38, 0x3a, 0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4a,
+ 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e,
+ 0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72,
+ 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82, 0x84, 0x86,
+ 0x88, 0x8a, 0x8c, 0x8e, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a,
+ 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6, 0xa8, 0xaa, 0xac, 0xae,
+ 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe, 0xc0, 0xc2,
+ 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6,
+ 0xd8, 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea,
+ 0xec, 0xee, 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe,
+ 0x1d, 0x1f, 0x19, 0x1b, 0x15, 0x17, 0x11, 0x13, 0x0d, 0x0f,
+ 0x09, 0x0b, 0x05, 0x07, 0x01, 0x03, 0x3d, 0x3f, 0x39, 0x3b,
+ 0x35, 0x37, 0x31, 0x33, 0x2d, 0x2f, 0x29, 0x2b, 0x25, 0x27,
+ 0x21, 0x23, 0x5d, 0x5f, 0x59, 0x5b, 0x55, 0x57, 0x51, 0x53,
+ 0x4d, 0x4f, 0x49, 0x4b, 0x45, 0x47, 0x41, 0x43, 0x7d, 0x7f,
+ 0x79, 0x7b, 0x75, 0x77, 0x71, 0x73, 0x6d, 0x6f, 0x69, 0x6b,
+ 0x65, 0x67, 0x61, 0x63, 0x9d, 0x9f, 0x99, 0x9b, 0x95, 0x97,
+ 0x91, 0x93, 0x8d, 0x8f, 0x89, 0x8b, 0x85, 0x87, 0x81, 0x83,
+ 0xbd, 0xbf, 0xb9, 0xbb, 0xb5, 0xb7, 0xb1, 0xb3, 0xad, 0xaf,
+ 0xa9, 0xab, 0xa5, 0xa7, 0xa1, 0xa3, 0xdd, 0xdf, 0xd9, 0xdb,
+ 0xd5, 0xd7, 0xd1, 0xd3, 0xcd, 0xcf, 0xc9, 0xcb, 0xc5, 0xc7,
+ 0xc1, 0xc3, 0xfd, 0xff, 0xf9, 0xfb, 0xf5, 0xf7, 0xf1, 0xf3,
+ 0xed, 0xef, 0xe9, 0xeb, 0xe5, 0xe7, 0xe1, 0xe3, 0x00, 0x03,
+ 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d,
+ 0x14, 0x17, 0x12, 0x11, 0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f,
+ 0x3a, 0x39, 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21,
+ 0x60, 0x63, 0x66, 0x65, 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b,
+ 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71, 0x50, 0x53, 0x56, 0x55,
+ 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d, 0x44, 0x47,
+ 0x42, 0x41, 0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9,
+ 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1, 0xf0, 0xf3,
+ 0xf6, 0xf5, 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed,
+ 0xe4, 0xe7, 0xe2, 0xe1, 0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf,
+ 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd, 0xb4, 0xb7, 0xb2, 0xb1,
+ 0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99, 0x88, 0x8b,
+ 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81, 0x9d, 0x9e, 0x9b, 0x98,
+ 0x91, 0x92, 0x97, 0x94, 0x85, 0x86, 0x83, 0x80, 0x89, 0x8a,
+ 0x8f, 0x8c, 0xad, 0xae, 0xab, 0xa8, 0xa1, 0xa2, 0xa7, 0xa4,
+ 0xb5, 0xb6, 0xb3, 0xb0, 0xb9, 0xba, 0xbf, 0xbc, 0xfd, 0xfe,
+ 0xfb, 0xf8, 0xf1, 0xf2, 0xf7, 0xf4, 0xe5, 0xe6, 0xe3, 0xe0,
+ 0xe9, 0xea, 0xef, 0xec, 0xcd, 0xce, 0xcb, 0xc8, 0xc1, 0xc2,
+ 0xc7, 0xc4, 0xd5, 0xd6, 0xd3, 0xd0, 0xd9, 0xda, 0xdf, 0xdc,
+ 0x5d, 0x5e, 0x5b, 0x58, 0x51, 0x52, 0x57, 0x54, 0x45, 0x46,
+ 0x43, 0x40, 0x49, 0x4a, 0x4f, 0x4c, 0x6d, 0x6e, 0x6b, 0x68,
+ 0x61, 0x62, 0x67, 0x64, 0x75, 0x76, 0x73, 0x70, 0x79, 0x7a,
+ 0x7f, 0x7c, 0x3d, 0x3e, 0x3b, 0x38, 0x31, 0x32, 0x37, 0x34,
+ 0x25, 0x26, 0x23, 0x20, 0x29, 0x2a, 0x2f, 0x2c, 0x0d, 0x0e,
+ 0x0b, 0x08, 0x01, 0x02, 0x07, 0x04, 0x15, 0x16, 0x13, 0x10,
+ 0x19, 0x1a, 0x1f, 0x1c, 0x00, 0x04, 0x08, 0x0c, 0x10, 0x14,
+ 0x18, 0x1c, 0x20, 0x24, 0x28, 0x2c, 0x30, 0x34, 0x38, 0x3c,
+ 0x40, 0x44, 0x48, 0x4c, 0x50, 0x54, 0x58, 0x5c, 0x60, 0x64,
+ 0x68, 0x6c, 0x70, 0x74, 0x78, 0x7c, 0x80, 0x84, 0x88, 0x8c,
+ 0x90, 0x94, 0x98, 0x9c, 0xa0, 0xa4, 0xa8, 0xac, 0xb0, 0xb4,
+ 0xb8, 0xbc, 0xc0, 0xc4, 0xc8, 0xcc, 0xd0, 0xd4, 0xd8, 0xdc,
+ 0xe0, 0xe4, 0xe8, 0xec, 0xf0, 0xf4, 0xf8, 0xfc, 0x1d, 0x19,
+ 0x15, 0x11, 0x0d, 0x09, 0x05, 0x01, 0x3d, 0x39, 0x35, 0x31,
+ 0x2d, 0x29, 0x25, 0x21, 0x5d, 0x59, 0x55, 0x51, 0x4d, 0x49,
+ 0x45, 0x41, 0x7d, 0x79, 0x75, 0x71, 0x6d, 0x69, 0x65, 0x61,
+ 0x9d, 0x99, 0x95, 0x91, 0x8d, 0x89, 0x85, 0x81, 0xbd, 0xb9,
+ 0xb5, 0xb1, 0xad, 0xa9, 0xa5, 0xa1, 0xdd, 0xd9, 0xd5, 0xd1,
+ 0xcd, 0xc9, 0xc5, 0xc1, 0xfd, 0xf9, 0xf5, 0xf1, 0xed, 0xe9,
+ 0xe5, 0xe1, 0x3a, 0x3e, 0x32, 0x36, 0x2a, 0x2e, 0x22, 0x26,
+ 0x1a, 0x1e, 0x12, 0x16, 0x0a, 0x0e, 0x02, 0x06, 0x7a, 0x7e,
+ 0x72, 0x76, 0x6a, 0x6e, 0x62, 0x66, 0x5a, 0x5e, 0x52, 0x56,
+ 0x4a, 0x4e, 0x42, 0x46, 0xba, 0xbe, 0xb2, 0xb6, 0xaa, 0xae,
+ 0xa2, 0xa6, 0x9a, 0x9e, 0x92, 0x96, 0x8a, 0x8e, 0x82, 0x86,
+ 0xfa, 0xfe, 0xf2, 0xf6, 0xea, 0xee, 0xe2, 0xe6, 0xda, 0xde,
+ 0xd2, 0xd6, 0xca, 0xce, 0xc2, 0xc6, 0x27, 0x23, 0x2f, 0x2b,
+ 0x37, 0x33, 0x3f, 0x3b, 0x07, 0x03, 0x0f, 0x0b, 0x17, 0x13,
+ 0x1f, 0x1b, 0x67, 0x63, 0x6f, 0x6b, 0x77, 0x73, 0x7f, 0x7b,
+ 0x47, 0x43, 0x4f, 0x4b, 0x57, 0x53, 0x5f, 0x5b, 0xa7, 0xa3,
+ 0xaf, 0xab, 0xb7, 0xb3, 0xbf, 0xbb, 0x87, 0x83, 0x8f, 0x8b,
+ 0x97, 0x93, 0x9f, 0x9b, 0xe7, 0xe3, 0xef, 0xeb, 0xf7, 0xf3,
+ 0xff, 0xfb, 0xc7, 0xc3, 0xcf, 0xcb, 0xd7, 0xd3, 0xdf, 0xdb,
+ 0x00, 0x05, 0x0a, 0x0f, 0x14, 0x11, 0x1e, 0x1b, 0x28, 0x2d,
+ 0x22, 0x27, 0x3c, 0x39, 0x36, 0x33, 0x50, 0x55, 0x5a, 0x5f,
+ 0x44, 0x41, 0x4e, 0x4b, 0x78, 0x7d, 0x72, 0x77, 0x6c, 0x69,
+ 0x66, 0x63, 0xa0, 0xa5, 0xaa, 0xaf, 0xb4, 0xb1, 0xbe, 0xbb,
+ 0x88, 0x8d, 0x82, 0x87, 0x9c, 0x99, 0x96, 0x93, 0xf0, 0xf5,
+ 0xfa, 0xff, 0xe4, 0xe1, 0xee, 0xeb, 0xd8, 0xdd, 0xd2, 0xd7,
+ 0xcc, 0xc9, 0xc6, 0xc3, 0x5d, 0x58, 0x57, 0x52, 0x49, 0x4c,
+ 0x43, 0x46, 0x75, 0x70, 0x7f, 0x7a, 0x61, 0x64, 0x6b, 0x6e,
+ 0x0d, 0x08, 0x07, 0x02, 0x19, 0x1c, 0x13, 0x16, 0x25, 0x20,
+ 0x2f, 0x2a, 0x31, 0x34, 0x3b, 0x3e, 0xfd, 0xf8, 0xf7, 0xf2,
+ 0xe9, 0xec, 0xe3, 0xe6, 0xd5, 0xd0, 0xdf, 0xda, 0xc1, 0xc4,
+ 0xcb, 0xce, 0xad, 0xa8, 0xa7, 0xa2, 0xb9, 0xbc, 0xb3, 0xb6,
+ 0x85, 0x80, 0x8f, 0x8a, 0x91, 0x94, 0x9b, 0x9e, 0xba, 0xbf,
+ 0xb0, 0xb5, 0xae, 0xab, 0xa4, 0xa1, 0x92, 0x97, 0x98, 0x9d,
+ 0x86, 0x83, 0x8c, 0x89, 0xea, 0xef, 0xe0, 0xe5, 0xfe, 0xfb,
+ 0xf4, 0xf1, 0xc2, 0xc7, 0xc8, 0xcd, 0xd6, 0xd3, 0xdc, 0xd9,
+ 0x1a, 0x1f, 0x10, 0x15, 0x0e, 0x0b, 0x04, 0x01, 0x32, 0x37,
+ 0x38, 0x3d, 0x26, 0x23, 0x2c, 0x29, 0x4a, 0x4f, 0x40, 0x45,
+ 0x5e, 0x5b, 0x54, 0x51, 0x62, 0x67, 0x68, 0x6d, 0x76, 0x73,
+ 0x7c, 0x79, 0xe7, 0xe2, 0xed, 0xe8, 0xf3, 0xf6, 0xf9, 0xfc,
+ 0xcf, 0xca, 0xc5, 0xc0, 0xdb, 0xde, 0xd1, 0xd4, 0xb7, 0xb2,
+ 0xbd, 0xb8, 0xa3, 0xa6, 0xa9, 0xac, 0x9f, 0x9a, 0x95, 0x90,
+ 0x8b, 0x8e, 0x81, 0x84, 0x47, 0x42, 0x4d, 0x48, 0x53, 0x56,
+ 0x59, 0x5c, 0x6f, 0x6a, 0x65, 0x60, 0x7b, 0x7e, 0x71, 0x74,
+ 0x17, 0x12, 0x1d, 0x18, 0x03, 0x06, 0x09, 0x0c, 0x3f, 0x3a,
+ 0x35, 0x30, 0x2b, 0x2e, 0x21, 0x24, 0x00, 0x06, 0x0c, 0x0a,
+ 0x18, 0x1e, 0x14, 0x12, 0x30, 0x36, 0x3c, 0x3a, 0x28, 0x2e,
+ 0x24, 0x22, 0x60, 0x66, 0x6c, 0x6a, 0x78, 0x7e, 0x74, 0x72,
+ 0x50, 0x56, 0x5c, 0x5a, 0x48, 0x4e, 0x44, 0x42, 0xc0, 0xc6,
+ 0xcc, 0xca, 0xd8, 0xde, 0xd4, 0xd2, 0xf0, 0xf6, 0xfc, 0xfa,
+ 0xe8, 0xee, 0xe4, 0xe2, 0xa0, 0xa6, 0xac, 0xaa, 0xb8, 0xbe,
+ 0xb4, 0xb2, 0x90, 0x96, 0x9c, 0x9a, 0x88, 0x8e, 0x84, 0x82,
+ 0x9d, 0x9b, 0x91, 0x97, 0x85, 0x83, 0x89, 0x8f, 0xad, 0xab,
+ 0xa1, 0xa7, 0xb5, 0xb3, 0xb9, 0xbf, 0xfd, 0xfb, 0xf1, 0xf7,
+ 0xe5, 0xe3, 0xe9, 0xef, 0xcd, 0xcb, 0xc1, 0xc7, 0xd5, 0xd3,
+ 0xd9, 0xdf, 0x5d, 0x5b, 0x51, 0x57, 0x45, 0x43, 0x49, 0x4f,
+ 0x6d, 0x6b, 0x61, 0x67, 0x75, 0x73, 0x79, 0x7f, 0x3d, 0x3b,
+ 0x31, 0x37, 0x25, 0x23, 0x29, 0x2f, 0x0d, 0x0b, 0x01, 0x07,
+ 0x15, 0x13, 0x19, 0x1f, 0x27, 0x21, 0x2b, 0x2d, 0x3f, 0x39,
+ 0x33, 0x35, 0x17, 0x11, 0x1b, 0x1d, 0x0f, 0x09, 0x03, 0x05,
+ 0x47, 0x41, 0x4b, 0x4d, 0x5f, 0x59, 0x53, 0x55, 0x77, 0x71,
+ 0x7b, 0x7d, 0x6f, 0x69, 0x63, 0x65, 0xe7, 0xe1, 0xeb, 0xed,
+ 0xff, 0xf9, 0xf3, 0xf5, 0xd7, 0xd1, 0xdb, 0xdd, 0xcf, 0xc9,
+ 0xc3, 0xc5, 0x87, 0x81, 0x8b, 0x8d, 0x9f, 0x99, 0x93, 0x95,
+ 0xb7, 0xb1, 0xbb, 0xbd, 0xaf, 0xa9, 0xa3, 0xa5, 0xba, 0xbc,
+ 0xb6, 0xb0, 0xa2, 0xa4, 0xae, 0xa8, 0x8a, 0x8c, 0x86, 0x80,
+ 0x92, 0x94, 0x9e, 0x98, 0xda, 0xdc, 0xd6, 0xd0, 0xc2, 0xc4,
+ 0xce, 0xc8, 0xea, 0xec, 0xe6, 0xe0, 0xf2, 0xf4, 0xfe, 0xf8,
+ 0x7a, 0x7c, 0x76, 0x70, 0x62, 0x64, 0x6e, 0x68, 0x4a, 0x4c,
+ 0x46, 0x40, 0x52, 0x54, 0x5e, 0x58, 0x1a, 0x1c, 0x16, 0x10,
+ 0x02, 0x04, 0x0e, 0x08, 0x2a, 0x2c, 0x26, 0x20, 0x32, 0x34,
+ 0x3e, 0x38, 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15,
+ 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77,
+ 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, 0x48, 0x4f, 0x46, 0x41,
+ 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb,
+ 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
+ 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf,
+ 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, 0xdd, 0xda, 0xd3, 0xd4,
+ 0xc1, 0xc6, 0xcf, 0xc8, 0xe5, 0xe2, 0xeb, 0xec, 0xf9, 0xfe,
+ 0xf7, 0xf0, 0xad, 0xaa, 0xa3, 0xa4, 0xb1, 0xb6, 0xbf, 0xb8,
+ 0x95, 0x92, 0x9b, 0x9c, 0x89, 0x8e, 0x87, 0x80, 0x3d, 0x3a,
+ 0x33, 0x34, 0x21, 0x26, 0x2f, 0x28, 0x05, 0x02, 0x0b, 0x0c,
+ 0x19, 0x1e, 0x17, 0x10, 0x4d, 0x4a, 0x43, 0x44, 0x51, 0x56,
+ 0x5f, 0x58, 0x75, 0x72, 0x7b, 0x7c, 0x69, 0x6e, 0x67, 0x60,
+ 0xa7, 0xa0, 0xa9, 0xae, 0xbb, 0xbc, 0xb5, 0xb2, 0x9f, 0x98,
+ 0x91, 0x96, 0x83, 0x84, 0x8d, 0x8a, 0xd7, 0xd0, 0xd9, 0xde,
+ 0xcb, 0xcc, 0xc5, 0xc2, 0xef, 0xe8, 0xe1, 0xe6, 0xf3, 0xf4,
+ 0xfd, 0xfa, 0x47, 0x40, 0x49, 0x4e, 0x5b, 0x5c, 0x55, 0x52,
+ 0x7f, 0x78, 0x71, 0x76, 0x63, 0x64, 0x6d, 0x6a, 0x37, 0x30,
+ 0x39, 0x3e, 0x2b, 0x2c, 0x25, 0x22, 0x0f, 0x08, 0x01, 0x06,
+ 0x13, 0x14, 0x1d, 0x1a, 0x7a, 0x7d, 0x74, 0x73, 0x66, 0x61,
+ 0x68, 0x6f, 0x42, 0x45, 0x4c, 0x4b, 0x5e, 0x59, 0x50, 0x57,
+ 0x0a, 0x0d, 0x04, 0x03, 0x16, 0x11, 0x18, 0x1f, 0x32, 0x35,
+ 0x3c, 0x3b, 0x2e, 0x29, 0x20, 0x27, 0x9a, 0x9d, 0x94, 0x93,
+ 0x86, 0x81, 0x88, 0x8f, 0xa2, 0xa5, 0xac, 0xab, 0xbe, 0xb9,
+ 0xb0, 0xb7, 0xea, 0xed, 0xe4, 0xe3, 0xf6, 0xf1, 0xf8, 0xff,
+ 0xd2, 0xd5, 0xdc, 0xdb, 0xce, 0xc9, 0xc0, 0xc7, 0x00, 0x08,
+ 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x48, 0x50, 0x58,
+ 0x60, 0x68, 0x70, 0x78, 0x80, 0x88, 0x90, 0x98, 0xa0, 0xa8,
+ 0xb0, 0xb8, 0xc0, 0xc8, 0xd0, 0xd8, 0xe0, 0xe8, 0xf0, 0xf8,
+ 0x1d, 0x15, 0x0d, 0x05, 0x3d, 0x35, 0x2d, 0x25, 0x5d, 0x55,
+ 0x4d, 0x45, 0x7d, 0x75, 0x6d, 0x65, 0x9d, 0x95, 0x8d, 0x85,
+ 0xbd, 0xb5, 0xad, 0xa5, 0xdd, 0xd5, 0xcd, 0xc5, 0xfd, 0xf5,
+ 0xed, 0xe5, 0x3a, 0x32, 0x2a, 0x22, 0x1a, 0x12, 0x0a, 0x02,
+ 0x7a, 0x72, 0x6a, 0x62, 0x5a, 0x52, 0x4a, 0x42, 0xba, 0xb2,
+ 0xaa, 0xa2, 0x9a, 0x92, 0x8a, 0x82, 0xfa, 0xf2, 0xea, 0xe2,
+ 0xda, 0xd2, 0xca, 0xc2, 0x27, 0x2f, 0x37, 0x3f, 0x07, 0x0f,
+ 0x17, 0x1f, 0x67, 0x6f, 0x77, 0x7f, 0x47, 0x4f, 0x57, 0x5f,
+ 0xa7, 0xaf, 0xb7, 0xbf, 0x87, 0x8f, 0x97, 0x9f, 0xe7, 0xef,
+ 0xf7, 0xff, 0xc7, 0xcf, 0xd7, 0xdf, 0x74, 0x7c, 0x64, 0x6c,
+ 0x54, 0x5c, 0x44, 0x4c, 0x34, 0x3c, 0x24, 0x2c, 0x14, 0x1c,
+ 0x04, 0x0c, 0xf4, 0xfc, 0xe4, 0xec, 0xd4, 0xdc, 0xc4, 0xcc,
+ 0xb4, 0xbc, 0xa4, 0xac, 0x94, 0x9c, 0x84, 0x8c, 0x69, 0x61,
+ 0x79, 0x71, 0x49, 0x41, 0x59, 0x51, 0x29, 0x21, 0x39, 0x31,
+ 0x09, 0x01, 0x19, 0x11, 0xe9, 0xe1, 0xf9, 0xf1, 0xc9, 0xc1,
+ 0xd9, 0xd1, 0xa9, 0xa1, 0xb9, 0xb1, 0x89, 0x81, 0x99, 0x91,
+ 0x4e, 0x46, 0x5e, 0x56, 0x6e, 0x66, 0x7e, 0x76, 0x0e, 0x06,
+ 0x1e, 0x16, 0x2e, 0x26, 0x3e, 0x36, 0xce, 0xc6, 0xde, 0xd6,
+ 0xee, 0xe6, 0xfe, 0xf6, 0x8e, 0x86, 0x9e, 0x96, 0xae, 0xa6,
+ 0xbe, 0xb6, 0x53, 0x5b, 0x43, 0x4b, 0x73, 0x7b, 0x63, 0x6b,
+ 0x13, 0x1b, 0x03, 0x0b, 0x33, 0x3b, 0x23, 0x2b, 0xd3, 0xdb,
+ 0xc3, 0xcb, 0xf3, 0xfb, 0xe3, 0xeb, 0x93, 0x9b, 0x83, 0x8b,
+ 0xb3, 0xbb, 0xa3, 0xab, 0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d,
+ 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77,
+ 0x90, 0x99, 0x82, 0x8b, 0xb4, 0xbd, 0xa6, 0xaf, 0xd8, 0xd1,
+ 0xca, 0xc3, 0xfc, 0xf5, 0xee, 0xe7, 0x3d, 0x34, 0x2f, 0x26,
+ 0x19, 0x10, 0x0b, 0x02, 0x75, 0x7c, 0x67, 0x6e, 0x51, 0x58,
+ 0x43, 0x4a, 0xad, 0xa4, 0xbf, 0xb6, 0x89, 0x80, 0x9b, 0x92,
+ 0xe5, 0xec, 0xf7, 0xfe, 0xc1, 0xc8, 0xd3, 0xda, 0x7a, 0x73,
+ 0x68, 0x61, 0x5e, 0x57, 0x4c, 0x45, 0x32, 0x3b, 0x20, 0x29,
+ 0x16, 0x1f, 0x04, 0x0d, 0xea, 0xe3, 0xf8, 0xf1, 0xce, 0xc7,
+ 0xdc, 0xd5, 0xa2, 0xab, 0xb0, 0xb9, 0x86, 0x8f, 0x94, 0x9d,
+ 0x47, 0x4e, 0x55, 0x5c, 0x63, 0x6a, 0x71, 0x78, 0x0f, 0x06,
+ 0x1d, 0x14, 0x2b, 0x22, 0x39, 0x30, 0xd7, 0xde, 0xc5, 0xcc,
+ 0xf3, 0xfa, 0xe1, 0xe8, 0x9f, 0x96, 0x8d, 0x84, 0xbb, 0xb2,
+ 0xa9, 0xa0, 0xf4, 0xfd, 0xe6, 0xef, 0xd0, 0xd9, 0xc2, 0xcb,
+ 0xbc, 0xb5, 0xae, 0xa7, 0x98, 0x91, 0x8a, 0x83, 0x64, 0x6d,
+ 0x76, 0x7f, 0x40, 0x49, 0x52, 0x5b, 0x2c, 0x25, 0x3e, 0x37,
+ 0x08, 0x01, 0x1a, 0x13, 0xc9, 0xc0, 0xdb, 0xd2, 0xed, 0xe4,
+ 0xff, 0xf6, 0x81, 0x88, 0x93, 0x9a, 0xa5, 0xac, 0xb7, 0xbe,
+ 0x59, 0x50, 0x4b, 0x42, 0x7d, 0x74, 0x6f, 0x66, 0x11, 0x18,
+ 0x03, 0x0a, 0x35, 0x3c, 0x27, 0x2e, 0x8e, 0x87, 0x9c, 0x95,
+ 0xaa, 0xa3, 0xb8, 0xb1, 0xc6, 0xcf, 0xd4, 0xdd, 0xe2, 0xeb,
+ 0xf0, 0xf9, 0x1e, 0x17, 0x0c, 0x05, 0x3a, 0x33, 0x28, 0x21,
+ 0x56, 0x5f, 0x44, 0x4d, 0x72, 0x7b, 0x60, 0x69, 0xb3, 0xba,
+ 0xa1, 0xa8, 0x97, 0x9e, 0x85, 0x8c, 0xfb, 0xf2, 0xe9, 0xe0,
+ 0xdf, 0xd6, 0xcd, 0xc4, 0x23, 0x2a, 0x31, 0x38, 0x07, 0x0e,
+ 0x15, 0x1c, 0x6b, 0x62, 0x79, 0x70, 0x4f, 0x46, 0x5d, 0x54,
+ 0x00, 0x0a, 0x14, 0x1e, 0x28, 0x22, 0x3c, 0x36, 0x50, 0x5a,
+ 0x44, 0x4e, 0x78, 0x72, 0x6c, 0x66, 0xa0, 0xaa, 0xb4, 0xbe,
+ 0x88, 0x82, 0x9c, 0x96, 0xf0, 0xfa, 0xe4, 0xee, 0xd8, 0xd2,
+ 0xcc, 0xc6, 0x5d, 0x57, 0x49, 0x43, 0x75, 0x7f, 0x61, 0x6b,
+ 0x0d, 0x07, 0x19, 0x13, 0x25, 0x2f, 0x31, 0x3b, 0xfd, 0xf7,
+ 0xe9, 0xe3, 0xd5, 0xdf, 0xc1, 0xcb, 0xad, 0xa7, 0xb9, 0xb3,
+ 0x85, 0x8f, 0x91, 0x9b, 0xba, 0xb0, 0xae, 0xa4, 0x92, 0x98,
+ 0x86, 0x8c, 0xea, 0xe0, 0xfe, 0xf4, 0xc2, 0xc8, 0xd6, 0xdc,
+ 0x1a, 0x10, 0x0e, 0x04, 0x32, 0x38, 0x26, 0x2c, 0x4a, 0x40,
+ 0x5e, 0x54, 0x62, 0x68, 0x76, 0x7c, 0xe7, 0xed, 0xf3, 0xf9,
+ 0xcf, 0xc5, 0xdb, 0xd1, 0xb7, 0xbd, 0xa3, 0xa9, 0x9f, 0x95,
+ 0x8b, 0x81, 0x47, 0x4d, 0x53, 0x59, 0x6f, 0x65, 0x7b, 0x71,
+ 0x17, 0x1d, 0x03, 0x09, 0x3f, 0x35, 0x2b, 0x21, 0x69, 0x63,
+ 0x7d, 0x77, 0x41, 0x4b, 0x55, 0x5f, 0x39, 0x33, 0x2d, 0x27,
+ 0x11, 0x1b, 0x05, 0x0f, 0xc9, 0xc3, 0xdd, 0xd7, 0xe1, 0xeb,
+ 0xf5, 0xff, 0x99, 0x93, 0x8d, 0x87, 0xb1, 0xbb, 0xa5, 0xaf,
+ 0x34, 0x3e, 0x20, 0x2a, 0x1c, 0x16, 0x08, 0x02, 0x64, 0x6e,
+ 0x70, 0x7a, 0x4c, 0x46, 0x58, 0x52, 0x94, 0x9e, 0x80, 0x8a,
+ 0xbc, 0xb6, 0xa8, 0xa2, 0xc4, 0xce, 0xd0, 0xda, 0xec, 0xe6,
+ 0xf8, 0xf2, 0xd3, 0xd9, 0xc7, 0xcd, 0xfb, 0xf1, 0xef, 0xe5,
+ 0x83, 0x89, 0x97, 0x9d, 0xab, 0xa1, 0xbf, 0xb5, 0x73, 0x79,
+ 0x67, 0x6d, 0x5b, 0x51, 0x4f, 0x45, 0x23, 0x29, 0x37, 0x3d,
+ 0x0b, 0x01, 0x1f, 0x15, 0x8e, 0x84, 0x9a, 0x90, 0xa6, 0xac,
+ 0xb2, 0xb8, 0xde, 0xd4, 0xca, 0xc0, 0xf6, 0xfc, 0xe2, 0xe8,
+ 0x2e, 0x24, 0x3a, 0x30, 0x06, 0x0c, 0x12, 0x18, 0x7e, 0x74,
+ 0x6a, 0x60, 0x56, 0x5c, 0x42, 0x48, 0x00, 0x0b, 0x16, 0x1d,
+ 0x2c, 0x27, 0x3a, 0x31, 0x58, 0x53, 0x4e, 0x45, 0x74, 0x7f,
+ 0x62, 0x69, 0xb0, 0xbb, 0xa6, 0xad, 0x9c, 0x97, 0x8a, 0x81,
+ 0xe8, 0xe3, 0xfe, 0xf5, 0xc4, 0xcf, 0xd2, 0xd9, 0x7d, 0x76,
+ 0x6b, 0x60, 0x51, 0x5a, 0x47, 0x4c, 0x25, 0x2e, 0x33, 0x38,
+ 0x09, 0x02, 0x1f, 0x14, 0xcd, 0xc6, 0xdb, 0xd0, 0xe1, 0xea,
+ 0xf7, 0xfc, 0x95, 0x9e, 0x83, 0x88, 0xb9, 0xb2, 0xaf, 0xa4,
+ 0xfa, 0xf1, 0xec, 0xe7, 0xd6, 0xdd, 0xc0, 0xcb, 0xa2, 0xa9,
+ 0xb4, 0xbf, 0x8e, 0x85, 0x98, 0x93, 0x4a, 0x41, 0x5c, 0x57,
+ 0x66, 0x6d, 0x70, 0x7b, 0x12, 0x19, 0x04, 0x0f, 0x3e, 0x35,
+ 0x28, 0x23, 0x87, 0x8c, 0x91, 0x9a, 0xab, 0xa0, 0xbd, 0xb6,
+ 0xdf, 0xd4, 0xc9, 0xc2, 0xf3, 0xf8, 0xe5, 0xee, 0x37, 0x3c,
+ 0x21, 0x2a, 0x1b, 0x10, 0x0d, 0x06, 0x6f, 0x64, 0x79, 0x72,
+ 0x43, 0x48, 0x55, 0x5e, 0xe9, 0xe2, 0xff, 0xf4, 0xc5, 0xce,
+ 0xd3, 0xd8, 0xb1, 0xba, 0xa7, 0xac, 0x9d, 0x96, 0x8b, 0x80,
+ 0x59, 0x52, 0x4f, 0x44, 0x75, 0x7e, 0x63, 0x68, 0x01, 0x0a,
+ 0x17, 0x1c, 0x2d, 0x26, 0x3b, 0x30, 0x94, 0x9f, 0x82, 0x89,
+ 0xb8, 0xb3, 0xae, 0xa5, 0xcc, 0xc7, 0xda, 0xd1, 0xe0, 0xeb,
+ 0xf6, 0xfd, 0x24, 0x2f, 0x32, 0x39, 0x08, 0x03, 0x1e, 0x15,
+ 0x7c, 0x77, 0x6a, 0x61, 0x50, 0x5b, 0x46, 0x4d, 0x13, 0x18,
+ 0x05, 0x0e, 0x3f, 0x34, 0x29, 0x22, 0x4b, 0x40, 0x5d, 0x56,
+ 0x67, 0x6c, 0x71, 0x7a, 0xa3, 0xa8, 0xb5, 0xbe, 0x8f, 0x84,
+ 0x99, 0x92, 0xfb, 0xf0, 0xed, 0xe6, 0xd7, 0xdc, 0xc1, 0xca,
+ 0x6e, 0x65, 0x78, 0x73, 0x42, 0x49, 0x54, 0x5f, 0x36, 0x3d,
+ 0x20, 0x2b, 0x1a, 0x11, 0x0c, 0x07, 0xde, 0xd5, 0xc8, 0xc3,
+ 0xf2, 0xf9, 0xe4, 0xef, 0x86, 0x8d, 0x90, 0x9b, 0xaa, 0xa1,
+ 0xbc, 0xb7, 0x00, 0x0c, 0x18, 0x14, 0x30, 0x3c, 0x28, 0x24,
+ 0x60, 0x6c, 0x78, 0x74, 0x50, 0x5c, 0x48, 0x44, 0xc0, 0xcc,
+ 0xd8, 0xd4, 0xf0, 0xfc, 0xe8, 0xe4, 0xa0, 0xac, 0xb8, 0xb4,
+ 0x90, 0x9c, 0x88, 0x84, 0x9d, 0x91, 0x85, 0x89, 0xad, 0xa1,
+ 0xb5, 0xb9, 0xfd, 0xf1, 0xe5, 0xe9, 0xcd, 0xc1, 0xd5, 0xd9,
+ 0x5d, 0x51, 0x45, 0x49, 0x6d, 0x61, 0x75, 0x79, 0x3d, 0x31,
+ 0x25, 0x29, 0x0d, 0x01, 0x15, 0x19, 0x27, 0x2b, 0x3f, 0x33,
+ 0x17, 0x1b, 0x0f, 0x03, 0x47, 0x4b, 0x5f, 0x53, 0x77, 0x7b,
+ 0x6f, 0x63, 0xe7, 0xeb, 0xff, 0xf3, 0xd7, 0xdb, 0xcf, 0xc3,
+ 0x87, 0x8b, 0x9f, 0x93, 0xb7, 0xbb, 0xaf, 0xa3, 0xba, 0xb6,
+ 0xa2, 0xae, 0x8a, 0x86, 0x92, 0x9e, 0xda, 0xd6, 0xc2, 0xce,
+ 0xea, 0xe6, 0xf2, 0xfe, 0x7a, 0x76, 0x62, 0x6e, 0x4a, 0x46,
+ 0x52, 0x5e, 0x1a, 0x16, 0x02, 0x0e, 0x2a, 0x26, 0x32, 0x3e,
+ 0x4e, 0x42, 0x56, 0x5a, 0x7e, 0x72, 0x66, 0x6a, 0x2e, 0x22,
+ 0x36, 0x3a, 0x1e, 0x12, 0x06, 0x0a, 0x8e, 0x82, 0x96, 0x9a,
+ 0xbe, 0xb2, 0xa6, 0xaa, 0xee, 0xe2, 0xf6, 0xfa, 0xde, 0xd2,
+ 0xc6, 0xca, 0xd3, 0xdf, 0xcb, 0xc7, 0xe3, 0xef, 0xfb, 0xf7,
+ 0xb3, 0xbf, 0xab, 0xa7, 0x83, 0x8f, 0x9b, 0x97, 0x13, 0x1f,
+ 0x0b, 0x07, 0x23, 0x2f, 0x3b, 0x37, 0x73, 0x7f, 0x6b, 0x67,
+ 0x43, 0x4f, 0x5b, 0x57, 0x69, 0x65, 0x71, 0x7d, 0x59, 0x55,
+ 0x41, 0x4d, 0x09, 0x05, 0x11, 0x1d, 0x39, 0x35, 0x21, 0x2d,
+ 0xa9, 0xa5, 0xb1, 0xbd, 0x99, 0x95, 0x81, 0x8d, 0xc9, 0xc5,
+ 0xd1, 0xdd, 0xf9, 0xf5, 0xe1, 0xed, 0xf4, 0xf8, 0xec, 0xe0,
+ 0xc4, 0xc8, 0xdc, 0xd0, 0x94, 0x98, 0x8c, 0x80, 0xa4, 0xa8,
+ 0xbc, 0xb0, 0x34, 0x38, 0x2c, 0x20, 0x04, 0x08, 0x1c, 0x10,
+ 0x54, 0x58, 0x4c, 0x40, 0x64, 0x68, 0x7c, 0x70, 0x00, 0x0d,
+ 0x1a, 0x17, 0x34, 0x39, 0x2e, 0x23, 0x68, 0x65, 0x72, 0x7f,
+ 0x5c, 0x51, 0x46, 0x4b, 0xd0, 0xdd, 0xca, 0xc7, 0xe4, 0xe9,
+ 0xfe, 0xf3, 0xb8, 0xb5, 0xa2, 0xaf, 0x8c, 0x81, 0x96, 0x9b,
+ 0xbd, 0xb0, 0xa7, 0xaa, 0x89, 0x84, 0x93, 0x9e, 0xd5, 0xd8,
+ 0xcf, 0xc2, 0xe1, 0xec, 0xfb, 0xf6, 0x6d, 0x60, 0x77, 0x7a,
+ 0x59, 0x54, 0x43, 0x4e, 0x05, 0x08, 0x1f, 0x12, 0x31, 0x3c,
+ 0x2b, 0x26, 0x67, 0x6a, 0x7d, 0x70, 0x53, 0x5e, 0x49, 0x44,
+ 0x0f, 0x02, 0x15, 0x18, 0x3b, 0x36, 0x21, 0x2c, 0xb7, 0xba,
+ 0xad, 0xa0, 0x83, 0x8e, 0x99, 0x94, 0xdf, 0xd2, 0xc5, 0xc8,
+ 0xeb, 0xe6, 0xf1, 0xfc, 0xda, 0xd7, 0xc0, 0xcd, 0xee, 0xe3,
+ 0xf4, 0xf9, 0xb2, 0xbf, 0xa8, 0xa5, 0x86, 0x8b, 0x9c, 0x91,
+ 0x0a, 0x07, 0x10, 0x1d, 0x3e, 0x33, 0x24, 0x29, 0x62, 0x6f,
+ 0x78, 0x75, 0x56, 0x5b, 0x4c, 0x41, 0xce, 0xc3, 0xd4, 0xd9,
+ 0xfa, 0xf7, 0xe0, 0xed, 0xa6, 0xab, 0xbc, 0xb1, 0x92, 0x9f,
+ 0x88, 0x85, 0x1e, 0x13, 0x04, 0x09, 0x2a, 0x27, 0x30, 0x3d,
+ 0x76, 0x7b, 0x6c, 0x61, 0x42, 0x4f, 0x58, 0x55, 0x73, 0x7e,
+ 0x69, 0x64, 0x47, 0x4a, 0x5d, 0x50, 0x1b, 0x16, 0x01, 0x0c,
+ 0x2f, 0x22, 0x35, 0x38, 0xa3, 0xae, 0xb9, 0xb4, 0x97, 0x9a,
+ 0x8d, 0x80, 0xcb, 0xc6, 0xd1, 0xdc, 0xff, 0xf2, 0xe5, 0xe8,
+ 0xa9, 0xa4, 0xb3, 0xbe, 0x9d, 0x90, 0x87, 0x8a, 0xc1, 0xcc,
+ 0xdb, 0xd6, 0xf5, 0xf8, 0xef, 0xe2, 0x79, 0x74, 0x63, 0x6e,
+ 0x4d, 0x40, 0x57, 0x5a, 0x11, 0x1c, 0x0b, 0x06, 0x25, 0x28,
+ 0x3f, 0x32, 0x14, 0x19, 0x0e, 0x03, 0x20, 0x2d, 0x3a, 0x37,
+ 0x7c, 0x71, 0x66, 0x6b, 0x48, 0x45, 0x52, 0x5f, 0xc4, 0xc9,
+ 0xde, 0xd3, 0xf0, 0xfd, 0xea, 0xe7, 0xac, 0xa1, 0xb6, 0xbb,
+ 0x98, 0x95, 0x82, 0x8f, 0x00, 0x0e, 0x1c, 0x12, 0x38, 0x36,
+ 0x24, 0x2a, 0x70, 0x7e, 0x6c, 0x62, 0x48, 0x46, 0x54, 0x5a,
+ 0xe0, 0xee, 0xfc, 0xf2, 0xd8, 0xd6, 0xc4, 0xca, 0x90, 0x9e,
+ 0x8c, 0x82, 0xa8, 0xa6, 0xb4, 0xba, 0xdd, 0xd3, 0xc1, 0xcf,
+ 0xe5, 0xeb, 0xf9, 0xf7, 0xad, 0xa3, 0xb1, 0xbf, 0x95, 0x9b,
+ 0x89, 0x87, 0x3d, 0x33, 0x21, 0x2f, 0x05, 0x0b, 0x19, 0x17,
+ 0x4d, 0x43, 0x51, 0x5f, 0x75, 0x7b, 0x69, 0x67, 0xa7, 0xa9,
+ 0xbb, 0xb5, 0x9f, 0x91, 0x83, 0x8d, 0xd7, 0xd9, 0xcb, 0xc5,
+ 0xef, 0xe1, 0xf3, 0xfd, 0x47, 0x49, 0x5b, 0x55, 0x7f, 0x71,
+ 0x63, 0x6d, 0x37, 0x39, 0x2b, 0x25, 0x0f, 0x01, 0x13, 0x1d,
+ 0x7a, 0x74, 0x66, 0x68, 0x42, 0x4c, 0x5e, 0x50, 0x0a, 0x04,
+ 0x16, 0x18, 0x32, 0x3c, 0x2e, 0x20, 0x9a, 0x94, 0x86, 0x88,
+ 0xa2, 0xac, 0xbe, 0xb0, 0xea, 0xe4, 0xf6, 0xf8, 0xd2, 0xdc,
+ 0xce, 0xc0, 0x53, 0x5d, 0x4f, 0x41, 0x6b, 0x65, 0x77, 0x79,
+ 0x23, 0x2d, 0x3f, 0x31, 0x1b, 0x15, 0x07, 0x09, 0xb3, 0xbd,
+ 0xaf, 0xa1, 0x8b, 0x85, 0x97, 0x99, 0xc3, 0xcd, 0xdf, 0xd1,
+ 0xfb, 0xf5, 0xe7, 0xe9, 0x8e, 0x80, 0x92, 0x9c, 0xb6, 0xb8,
+ 0xaa, 0xa4, 0xfe, 0xf0, 0xe2, 0xec, 0xc6, 0xc8, 0xda, 0xd4,
+ 0x6e, 0x60, 0x72, 0x7c, 0x56, 0x58, 0x4a, 0x44, 0x1e, 0x10,
+ 0x02, 0x0c, 0x26, 0x28, 0x3a, 0x34, 0xf4, 0xfa, 0xe8, 0xe6,
+ 0xcc, 0xc2, 0xd0, 0xde, 0x84, 0x8a, 0x98, 0x96, 0xbc, 0xb2,
+ 0xa0, 0xae, 0x14, 0x1a, 0x08, 0x06, 0x2c, 0x22, 0x30, 0x3e,
+ 0x64, 0x6a, 0x78, 0x76, 0x5c, 0x52, 0x40, 0x4e, 0x29, 0x27,
+ 0x35, 0x3b, 0x11, 0x1f, 0x0d, 0x03, 0x59, 0x57, 0x45, 0x4b,
+ 0x61, 0x6f, 0x7d, 0x73, 0xc9, 0xc7, 0xd5, 0xdb, 0xf1, 0xff,
+ 0xed, 0xe3, 0xb9, 0xb7, 0xa5, 0xab, 0x81, 0x8f, 0x9d, 0x93,
+ 0x00, 0x0f, 0x1e, 0x11, 0x3c, 0x33, 0x22, 0x2d, 0x78, 0x77,
+ 0x66, 0x69, 0x44, 0x4b, 0x5a, 0x55, 0xf0, 0xff, 0xee, 0xe1,
+ 0xcc, 0xc3, 0xd2, 0xdd, 0x88, 0x87, 0x96, 0x99, 0xb4, 0xbb,
+ 0xaa, 0xa5, 0xfd, 0xf2, 0xe3, 0xec, 0xc1, 0xce, 0xdf, 0xd0,
+ 0x85, 0x8a, 0x9b, 0x94, 0xb9, 0xb6, 0xa7, 0xa8, 0x0d, 0x02,
+ 0x13, 0x1c, 0x31, 0x3e, 0x2f, 0x20, 0x75, 0x7a, 0x6b, 0x64,
+ 0x49, 0x46, 0x57, 0x58, 0xe7, 0xe8, 0xf9, 0xf6, 0xdb, 0xd4,
+ 0xc5, 0xca, 0x9f, 0x90, 0x81, 0x8e, 0xa3, 0xac, 0xbd, 0xb2,
+ 0x17, 0x18, 0x09, 0x06, 0x2b, 0x24, 0x35, 0x3a, 0x6f, 0x60,
+ 0x71, 0x7e, 0x53, 0x5c, 0x4d, 0x42, 0x1a, 0x15, 0x04, 0x0b,
+ 0x26, 0x29, 0x38, 0x37, 0x62, 0x6d, 0x7c, 0x73, 0x5e, 0x51,
+ 0x40, 0x4f, 0xea, 0xe5, 0xf4, 0xfb, 0xd6, 0xd9, 0xc8, 0xc7,
+ 0x92, 0x9d, 0x8c, 0x83, 0xae, 0xa1, 0xb0, 0xbf, 0xd3, 0xdc,
+ 0xcd, 0xc2, 0xef, 0xe0, 0xf1, 0xfe, 0xab, 0xa4, 0xb5, 0xba,
+ 0x97, 0x98, 0x89, 0x86, 0x23, 0x2c, 0x3d, 0x32, 0x1f, 0x10,
+ 0x01, 0x0e, 0x5b, 0x54, 0x45, 0x4a, 0x67, 0x68, 0x79, 0x76,
+ 0x2e, 0x21, 0x30, 0x3f, 0x12, 0x1d, 0x0c, 0x03, 0x56, 0x59,
+ 0x48, 0x47, 0x6a, 0x65, 0x74, 0x7b, 0xde, 0xd1, 0xc0, 0xcf,
+ 0xe2, 0xed, 0xfc, 0xf3, 0xa6, 0xa9, 0xb8, 0xb7, 0x9a, 0x95,
+ 0x84, 0x8b, 0x34, 0x3b, 0x2a, 0x25, 0x08, 0x07, 0x16, 0x19,
+ 0x4c, 0x43, 0x52, 0x5d, 0x70, 0x7f, 0x6e, 0x61, 0xc4, 0xcb,
+ 0xda, 0xd5, 0xf8, 0xf7, 0xe6, 0xe9, 0xbc, 0xb3, 0xa2, 0xad,
+ 0x80, 0x8f, 0x9e, 0x91, 0xc9, 0xc6, 0xd7, 0xd8, 0xf5, 0xfa,
+ 0xeb, 0xe4, 0xb1, 0xbe, 0xaf, 0xa0, 0x8d, 0x82, 0x93, 0x9c,
+ 0x39, 0x36, 0x27, 0x28, 0x05, 0x0a, 0x1b, 0x14, 0x41, 0x4e,
+ 0x5f, 0x50, 0x7d, 0x72, 0x63, 0x6c, 0x00, 0x10, 0x20, 0x30,
+ 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0,
+ 0xe0, 0xf0, 0x1d, 0x0d, 0x3d, 0x2d, 0x5d, 0x4d, 0x7d, 0x6d,
+ 0x9d, 0x8d, 0xbd, 0xad, 0xdd, 0xcd, 0xfd, 0xed, 0x3a, 0x2a,
+ 0x1a, 0x0a, 0x7a, 0x6a, 0x5a, 0x4a, 0xba, 0xaa, 0x9a, 0x8a,
+ 0xfa, 0xea, 0xda, 0xca, 0x27, 0x37, 0x07, 0x17, 0x67, 0x77,
+ 0x47, 0x57, 0xa7, 0xb7, 0x87, 0x97, 0xe7, 0xf7, 0xc7, 0xd7,
+ 0x74, 0x64, 0x54, 0x44, 0x34, 0x24, 0x14, 0x04, 0xf4, 0xe4,
+ 0xd4, 0xc4, 0xb4, 0xa4, 0x94, 0x84, 0x69, 0x79, 0x49, 0x59,
+ 0x29, 0x39, 0x09, 0x19, 0xe9, 0xf9, 0xc9, 0xd9, 0xa9, 0xb9,
+ 0x89, 0x99, 0x4e, 0x5e, 0x6e, 0x7e, 0x0e, 0x1e, 0x2e, 0x3e,
+ 0xce, 0xde, 0xee, 0xfe, 0x8e, 0x9e, 0xae, 0xbe, 0x53, 0x43,
+ 0x73, 0x63, 0x13, 0x03, 0x33, 0x23, 0xd3, 0xc3, 0xf3, 0xe3,
+ 0x93, 0x83, 0xb3, 0xa3, 0xe8, 0xf8, 0xc8, 0xd8, 0xa8, 0xb8,
+ 0x88, 0x98, 0x68, 0x78, 0x48, 0x58, 0x28, 0x38, 0x08, 0x18,
+ 0xf5, 0xe5, 0xd5, 0xc5, 0xb5, 0xa5, 0x95, 0x85, 0x75, 0x65,
+ 0x55, 0x45, 0x35, 0x25, 0x15, 0x05, 0xd2, 0xc2, 0xf2, 0xe2,
+ 0x92, 0x82, 0xb2, 0xa2, 0x52, 0x42, 0x72, 0x62, 0x12, 0x02,
+ 0x32, 0x22, 0xcf, 0xdf, 0xef, 0xff, 0x8f, 0x9f, 0xaf, 0xbf,
+ 0x4f, 0x5f, 0x6f, 0x7f, 0x0f, 0x1f, 0x2f, 0x3f, 0x9c, 0x8c,
+ 0xbc, 0xac, 0xdc, 0xcc, 0xfc, 0xec, 0x1c, 0x0c, 0x3c, 0x2c,
+ 0x5c, 0x4c, 0x7c, 0x6c, 0x81, 0x91, 0xa1, 0xb1, 0xc1, 0xd1,
+ 0xe1, 0xf1, 0x01, 0x11, 0x21, 0x31, 0x41, 0x51, 0x61, 0x71,
+ 0xa6, 0xb6, 0x86, 0x96, 0xe6, 0xf6, 0xc6, 0xd6, 0x26, 0x36,
+ 0x06, 0x16, 0x66, 0x76, 0x46, 0x56, 0xbb, 0xab, 0x9b, 0x8b,
+ 0xfb, 0xeb, 0xdb, 0xcb, 0x3b, 0x2b, 0x1b, 0x0b, 0x7b, 0x6b,
+ 0x5b, 0x4b, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+ 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x0d, 0x1c,
+ 0x2f, 0x3e, 0x49, 0x58, 0x6b, 0x7a, 0x85, 0x94, 0xa7, 0xb6,
+ 0xc1, 0xd0, 0xe3, 0xf2, 0x1a, 0x0b, 0x38, 0x29, 0x5e, 0x4f,
+ 0x7c, 0x6d, 0x92, 0x83, 0xb0, 0xa1, 0xd6, 0xc7, 0xf4, 0xe5,
+ 0x17, 0x06, 0x35, 0x24, 0x53, 0x42, 0x71, 0x60, 0x9f, 0x8e,
+ 0xbd, 0xac, 0xdb, 0xca, 0xf9, 0xe8, 0x34, 0x25, 0x16, 0x07,
+ 0x70, 0x61, 0x52, 0x43, 0xbc, 0xad, 0x9e, 0x8f, 0xf8, 0xe9,
+ 0xda, 0xcb, 0x39, 0x28, 0x1b, 0x0a, 0x7d, 0x6c, 0x5f, 0x4e,
+ 0xb1, 0xa0, 0x93, 0x82, 0xf5, 0xe4, 0xd7, 0xc6, 0x2e, 0x3f,
+ 0x0c, 0x1d, 0x6a, 0x7b, 0x48, 0x59, 0xa6, 0xb7, 0x84, 0x95,
+ 0xe2, 0xf3, 0xc0, 0xd1, 0x23, 0x32, 0x01, 0x10, 0x67, 0x76,
+ 0x45, 0x54, 0xab, 0xba, 0x89, 0x98, 0xef, 0xfe, 0xcd, 0xdc,
+ 0x68, 0x79, 0x4a, 0x5b, 0x2c, 0x3d, 0x0e, 0x1f, 0xe0, 0xf1,
+ 0xc2, 0xd3, 0xa4, 0xb5, 0x86, 0x97, 0x65, 0x74, 0x47, 0x56,
+ 0x21, 0x30, 0x03, 0x12, 0xed, 0xfc, 0xcf, 0xde, 0xa9, 0xb8,
+ 0x8b, 0x9a, 0x72, 0x63, 0x50, 0x41, 0x36, 0x27, 0x14, 0x05,
+ 0xfa, 0xeb, 0xd8, 0xc9, 0xbe, 0xaf, 0x9c, 0x8d, 0x7f, 0x6e,
+ 0x5d, 0x4c, 0x3b, 0x2a, 0x19, 0x08, 0xf7, 0xe6, 0xd5, 0xc4,
+ 0xb3, 0xa2, 0x91, 0x80, 0x5c, 0x4d, 0x7e, 0x6f, 0x18, 0x09,
+ 0x3a, 0x2b, 0xd4, 0xc5, 0xf6, 0xe7, 0x90, 0x81, 0xb2, 0xa3,
+ 0x51, 0x40, 0x73, 0x62, 0x15, 0x04, 0x37, 0x26, 0xd9, 0xc8,
+ 0xfb, 0xea, 0x9d, 0x8c, 0xbf, 0xae, 0x46, 0x57, 0x64, 0x75,
+ 0x02, 0x13, 0x20, 0x31, 0xce, 0xdf, 0xec, 0xfd, 0x8a, 0x9b,
+ 0xa8, 0xb9, 0x4b, 0x5a, 0x69, 0x78, 0x0f, 0x1e, 0x2d, 0x3c,
+ 0xc3, 0xd2, 0xe1, 0xf0, 0x87, 0x96, 0xa5, 0xb4, 0x00, 0x12,
+ 0x24, 0x36, 0x48, 0x5a, 0x6c, 0x7e, 0x90, 0x82, 0xb4, 0xa6,
+ 0xd8, 0xca, 0xfc, 0xee, 0x3d, 0x2f, 0x19, 0x0b, 0x75, 0x67,
+ 0x51, 0x43, 0xad, 0xbf, 0x89, 0x9b, 0xe5, 0xf7, 0xc1, 0xd3,
+ 0x7a, 0x68, 0x5e, 0x4c, 0x32, 0x20, 0x16, 0x04, 0xea, 0xf8,
+ 0xce, 0xdc, 0xa2, 0xb0, 0x86, 0x94, 0x47, 0x55, 0x63, 0x71,
+ 0x0f, 0x1d, 0x2b, 0x39, 0xd7, 0xc5, 0xf3, 0xe1, 0x9f, 0x8d,
+ 0xbb, 0xa9, 0xf4, 0xe6, 0xd0, 0xc2, 0xbc, 0xae, 0x98, 0x8a,
+ 0x64, 0x76, 0x40, 0x52, 0x2c, 0x3e, 0x08, 0x1a, 0xc9, 0xdb,
+ 0xed, 0xff, 0x81, 0x93, 0xa5, 0xb7, 0x59, 0x4b, 0x7d, 0x6f,
+ 0x11, 0x03, 0x35, 0x27, 0x8e, 0x9c, 0xaa, 0xb8, 0xc6, 0xd4,
+ 0xe2, 0xf0, 0x1e, 0x0c, 0x3a, 0x28, 0x56, 0x44, 0x72, 0x60,
+ 0xb3, 0xa1, 0x97, 0x85, 0xfb, 0xe9, 0xdf, 0xcd, 0x23, 0x31,
+ 0x07, 0x15, 0x6b, 0x79, 0x4f, 0x5d, 0xf5, 0xe7, 0xd1, 0xc3,
+ 0xbd, 0xaf, 0x99, 0x8b, 0x65, 0x77, 0x41, 0x53, 0x2d, 0x3f,
+ 0x09, 0x1b, 0xc8, 0xda, 0xec, 0xfe, 0x80, 0x92, 0xa4, 0xb6,
+ 0x58, 0x4a, 0x7c, 0x6e, 0x10, 0x02, 0x34, 0x26, 0x8f, 0x9d,
+ 0xab, 0xb9, 0xc7, 0xd5, 0xe3, 0xf1, 0x1f, 0x0d, 0x3b, 0x29,
+ 0x57, 0x45, 0x73, 0x61, 0xb2, 0xa0, 0x96, 0x84, 0xfa, 0xe8,
+ 0xde, 0xcc, 0x22, 0x30, 0x06, 0x14, 0x6a, 0x78, 0x4e, 0x5c,
+ 0x01, 0x13, 0x25, 0x37, 0x49, 0x5b, 0x6d, 0x7f, 0x91, 0x83,
+ 0xb5, 0xa7, 0xd9, 0xcb, 0xfd, 0xef, 0x3c, 0x2e, 0x18, 0x0a,
+ 0x74, 0x66, 0x50, 0x42, 0xac, 0xbe, 0x88, 0x9a, 0xe4, 0xf6,
+ 0xc0, 0xd2, 0x7b, 0x69, 0x5f, 0x4d, 0x33, 0x21, 0x17, 0x05,
+ 0xeb, 0xf9, 0xcf, 0xdd, 0xa3, 0xb1, 0x87, 0x95, 0x46, 0x54,
+ 0x62, 0x70, 0x0e, 0x1c, 0x2a, 0x38, 0xd6, 0xc4, 0xf2, 0xe0,
+ 0x9e, 0x8c, 0xba, 0xa8, 0x00, 0x13, 0x26, 0x35, 0x4c, 0x5f,
+ 0x6a, 0x79, 0x98, 0x8b, 0xbe, 0xad, 0xd4, 0xc7, 0xf2, 0xe1,
+ 0x2d, 0x3e, 0x0b, 0x18, 0x61, 0x72, 0x47, 0x54, 0xb5, 0xa6,
+ 0x93, 0x80, 0xf9, 0xea, 0xdf, 0xcc, 0x5a, 0x49, 0x7c, 0x6f,
+ 0x16, 0x05, 0x30, 0x23, 0xc2, 0xd1, 0xe4, 0xf7, 0x8e, 0x9d,
+ 0xa8, 0xbb, 0x77, 0x64, 0x51, 0x42, 0x3b, 0x28, 0x1d, 0x0e,
+ 0xef, 0xfc, 0xc9, 0xda, 0xa3, 0xb0, 0x85, 0x96, 0xb4, 0xa7,
+ 0x92, 0x81, 0xf8, 0xeb, 0xde, 0xcd, 0x2c, 0x3f, 0x0a, 0x19,
+ 0x60, 0x73, 0x46, 0x55, 0x99, 0x8a, 0xbf, 0xac, 0xd5, 0xc6,
+ 0xf3, 0xe0, 0x01, 0x12, 0x27, 0x34, 0x4d, 0x5e, 0x6b, 0x78,
+ 0xee, 0xfd, 0xc8, 0xdb, 0xa2, 0xb1, 0x84, 0x97, 0x76, 0x65,
+ 0x50, 0x43, 0x3a, 0x29, 0x1c, 0x0f, 0xc3, 0xd0, 0xe5, 0xf6,
+ 0x8f, 0x9c, 0xa9, 0xba, 0x5b, 0x48, 0x7d, 0x6e, 0x17, 0x04,
+ 0x31, 0x22, 0x75, 0x66, 0x53, 0x40, 0x39, 0x2a, 0x1f, 0x0c,
+ 0xed, 0xfe, 0xcb, 0xd8, 0xa1, 0xb2, 0x87, 0x94, 0x58, 0x4b,
+ 0x7e, 0x6d, 0x14, 0x07, 0x32, 0x21, 0xc0, 0xd3, 0xe6, 0xf5,
+ 0x8c, 0x9f, 0xaa, 0xb9, 0x2f, 0x3c, 0x09, 0x1a, 0x63, 0x70,
+ 0x45, 0x56, 0xb7, 0xa4, 0x91, 0x82, 0xfb, 0xe8, 0xdd, 0xce,
+ 0x02, 0x11, 0x24, 0x37, 0x4e, 0x5d, 0x68, 0x7b, 0x9a, 0x89,
+ 0xbc, 0xaf, 0xd6, 0xc5, 0xf0, 0xe3, 0xc1, 0xd2, 0xe7, 0xf4,
+ 0x8d, 0x9e, 0xab, 0xb8, 0x59, 0x4a, 0x7f, 0x6c, 0x15, 0x06,
+ 0x33, 0x20, 0xec, 0xff, 0xca, 0xd9, 0xa0, 0xb3, 0x86, 0x95,
+ 0x74, 0x67, 0x52, 0x41, 0x38, 0x2b, 0x1e, 0x0d, 0x9b, 0x88,
+ 0xbd, 0xae, 0xd7, 0xc4, 0xf1, 0xe2, 0x03, 0x10, 0x25, 0x36,
+ 0x4f, 0x5c, 0x69, 0x7a, 0xb6, 0xa5, 0x90, 0x83, 0xfa, 0xe9,
+ 0xdc, 0xcf, 0x2e, 0x3d, 0x08, 0x1b, 0x62, 0x71, 0x44, 0x57,
+ 0x00, 0x14, 0x28, 0x3c, 0x50, 0x44, 0x78, 0x6c, 0xa0, 0xb4,
+ 0x88, 0x9c, 0xf0, 0xe4, 0xd8, 0xcc, 0x5d, 0x49, 0x75, 0x61,
+ 0x0d, 0x19, 0x25, 0x31, 0xfd, 0xe9, 0xd5, 0xc1, 0xad, 0xb9,
+ 0x85, 0x91, 0xba, 0xae, 0x92, 0x86, 0xea, 0xfe, 0xc2, 0xd6,
+ 0x1a, 0x0e, 0x32, 0x26, 0x4a, 0x5e, 0x62, 0x76, 0xe7, 0xf3,
+ 0xcf, 0xdb, 0xb7, 0xa3, 0x9f, 0x8b, 0x47, 0x53, 0x6f, 0x7b,
+ 0x17, 0x03, 0x3f, 0x2b, 0x69, 0x7d, 0x41, 0x55, 0x39, 0x2d,
+ 0x11, 0x05, 0xc9, 0xdd, 0xe1, 0xf5, 0x99, 0x8d, 0xb1, 0xa5,
+ 0x34, 0x20, 0x1c, 0x08, 0x64, 0x70, 0x4c, 0x58, 0x94, 0x80,
+ 0xbc, 0xa8, 0xc4, 0xd0, 0xec, 0xf8, 0xd3, 0xc7, 0xfb, 0xef,
+ 0x83, 0x97, 0xab, 0xbf, 0x73, 0x67, 0x5b, 0x4f, 0x23, 0x37,
+ 0x0b, 0x1f, 0x8e, 0x9a, 0xa6, 0xb2, 0xde, 0xca, 0xf6, 0xe2,
+ 0x2e, 0x3a, 0x06, 0x12, 0x7e, 0x6a, 0x56, 0x42, 0xd2, 0xc6,
+ 0xfa, 0xee, 0x82, 0x96, 0xaa, 0xbe, 0x72, 0x66, 0x5a, 0x4e,
+ 0x22, 0x36, 0x0a, 0x1e, 0x8f, 0x9b, 0xa7, 0xb3, 0xdf, 0xcb,
+ 0xf7, 0xe3, 0x2f, 0x3b, 0x07, 0x13, 0x7f, 0x6b, 0x57, 0x43,
+ 0x68, 0x7c, 0x40, 0x54, 0x38, 0x2c, 0x10, 0x04, 0xc8, 0xdc,
+ 0xe0, 0xf4, 0x98, 0x8c, 0xb0, 0xa4, 0x35, 0x21, 0x1d, 0x09,
+ 0x65, 0x71, 0x4d, 0x59, 0x95, 0x81, 0xbd, 0xa9, 0xc5, 0xd1,
+ 0xed, 0xf9, 0xbb, 0xaf, 0x93, 0x87, 0xeb, 0xff, 0xc3, 0xd7,
+ 0x1b, 0x0f, 0x33, 0x27, 0x4b, 0x5f, 0x63, 0x77, 0xe6, 0xf2,
+ 0xce, 0xda, 0xb6, 0xa2, 0x9e, 0x8a, 0x46, 0x52, 0x6e, 0x7a,
+ 0x16, 0x02, 0x3e, 0x2a, 0x01, 0x15, 0x29, 0x3d, 0x51, 0x45,
+ 0x79, 0x6d, 0xa1, 0xb5, 0x89, 0x9d, 0xf1, 0xe5, 0xd9, 0xcd,
+ 0x5c, 0x48, 0x74, 0x60, 0x0c, 0x18, 0x24, 0x30, 0xfc, 0xe8,
+ 0xd4, 0xc0, 0xac, 0xb8, 0x84, 0x90, 0x00, 0x15, 0x2a, 0x3f,
+ 0x54, 0x41, 0x7e, 0x6b, 0xa8, 0xbd, 0x82, 0x97, 0xfc, 0xe9,
+ 0xd6, 0xc3, 0x4d, 0x58, 0x67, 0x72, 0x19, 0x0c, 0x33, 0x26,
+ 0xe5, 0xf0, 0xcf, 0xda, 0xb1, 0xa4, 0x9b, 0x8e, 0x9a, 0x8f,
+ 0xb0, 0xa5, 0xce, 0xdb, 0xe4, 0xf1, 0x32, 0x27, 0x18, 0x0d,
+ 0x66, 0x73, 0x4c, 0x59, 0xd7, 0xc2, 0xfd, 0xe8, 0x83, 0x96,
+ 0xa9, 0xbc, 0x7f, 0x6a, 0x55, 0x40, 0x2b, 0x3e, 0x01, 0x14,
+ 0x29, 0x3c, 0x03, 0x16, 0x7d, 0x68, 0x57, 0x42, 0x81, 0x94,
+ 0xab, 0xbe, 0xd5, 0xc0, 0xff, 0xea, 0x64, 0x71, 0x4e, 0x5b,
+ 0x30, 0x25, 0x1a, 0x0f, 0xcc, 0xd9, 0xe6, 0xf3, 0x98, 0x8d,
+ 0xb2, 0xa7, 0xb3, 0xa6, 0x99, 0x8c, 0xe7, 0xf2, 0xcd, 0xd8,
+ 0x1b, 0x0e, 0x31, 0x24, 0x4f, 0x5a, 0x65, 0x70, 0xfe, 0xeb,
+ 0xd4, 0xc1, 0xaa, 0xbf, 0x80, 0x95, 0x56, 0x43, 0x7c, 0x69,
+ 0x02, 0x17, 0x28, 0x3d, 0x52, 0x47, 0x78, 0x6d, 0x06, 0x13,
+ 0x2c, 0x39, 0xfa, 0xef, 0xd0, 0xc5, 0xae, 0xbb, 0x84, 0x91,
+ 0x1f, 0x0a, 0x35, 0x20, 0x4b, 0x5e, 0x61, 0x74, 0xb7, 0xa2,
+ 0x9d, 0x88, 0xe3, 0xf6, 0xc9, 0xdc, 0xc8, 0xdd, 0xe2, 0xf7,
+ 0x9c, 0x89, 0xb6, 0xa3, 0x60, 0x75, 0x4a, 0x5f, 0x34, 0x21,
+ 0x1e, 0x0b, 0x85, 0x90, 0xaf, 0xba, 0xd1, 0xc4, 0xfb, 0xee,
+ 0x2d, 0x38, 0x07, 0x12, 0x79, 0x6c, 0x53, 0x46, 0x7b, 0x6e,
+ 0x51, 0x44, 0x2f, 0x3a, 0x05, 0x10, 0xd3, 0xc6, 0xf9, 0xec,
+ 0x87, 0x92, 0xad, 0xb8, 0x36, 0x23, 0x1c, 0x09, 0x62, 0x77,
+ 0x48, 0x5d, 0x9e, 0x8b, 0xb4, 0xa1, 0xca, 0xdf, 0xe0, 0xf5,
+ 0xe1, 0xf4, 0xcb, 0xde, 0xb5, 0xa0, 0x9f, 0x8a, 0x49, 0x5c,
+ 0x63, 0x76, 0x1d, 0x08, 0x37, 0x22, 0xac, 0xb9, 0x86, 0x93,
+ 0xf8, 0xed, 0xd2, 0xc7, 0x04, 0x11, 0x2e, 0x3b, 0x50, 0x45,
+ 0x7a, 0x6f, 0x00, 0x16, 0x2c, 0x3a, 0x58, 0x4e, 0x74, 0x62,
+ 0xb0, 0xa6, 0x9c, 0x8a, 0xe8, 0xfe, 0xc4, 0xd2, 0x7d, 0x6b,
+ 0x51, 0x47, 0x25, 0x33, 0x09, 0x1f, 0xcd, 0xdb, 0xe1, 0xf7,
+ 0x95, 0x83, 0xb9, 0xaf, 0xfa, 0xec, 0xd6, 0xc0, 0xa2, 0xb4,
+ 0x8e, 0x98, 0x4a, 0x5c, 0x66, 0x70, 0x12, 0x04, 0x3e, 0x28,
+ 0x87, 0x91, 0xab, 0xbd, 0xdf, 0xc9, 0xf3, 0xe5, 0x37, 0x21,
+ 0x1b, 0x0d, 0x6f, 0x79, 0x43, 0x55, 0xe9, 0xff, 0xc5, 0xd3,
+ 0xb1, 0xa7, 0x9d, 0x8b, 0x59, 0x4f, 0x75, 0x63, 0x01, 0x17,
+ 0x2d, 0x3b, 0x94, 0x82, 0xb8, 0xae, 0xcc, 0xda, 0xe0, 0xf6,
+ 0x24, 0x32, 0x08, 0x1e, 0x7c, 0x6a, 0x50, 0x46, 0x13, 0x05,
+ 0x3f, 0x29, 0x4b, 0x5d, 0x67, 0x71, 0xa3, 0xb5, 0x8f, 0x99,
+ 0xfb, 0xed, 0xd7, 0xc1, 0x6e, 0x78, 0x42, 0x54, 0x36, 0x20,
+ 0x1a, 0x0c, 0xde, 0xc8, 0xf2, 0xe4, 0x86, 0x90, 0xaa, 0xbc,
+ 0xcf, 0xd9, 0xe3, 0xf5, 0x97, 0x81, 0xbb, 0xad, 0x7f, 0x69,
+ 0x53, 0x45, 0x27, 0x31, 0x0b, 0x1d, 0xb2, 0xa4, 0x9e, 0x88,
+ 0xea, 0xfc, 0xc6, 0xd0, 0x02, 0x14, 0x2e, 0x38, 0x5a, 0x4c,
+ 0x76, 0x60, 0x35, 0x23, 0x19, 0x0f, 0x6d, 0x7b, 0x41, 0x57,
+ 0x85, 0x93, 0xa9, 0xbf, 0xdd, 0xcb, 0xf1, 0xe7, 0x48, 0x5e,
+ 0x64, 0x72, 0x10, 0x06, 0x3c, 0x2a, 0xf8, 0xee, 0xd4, 0xc2,
+ 0xa0, 0xb6, 0x8c, 0x9a, 0x26, 0x30, 0x0a, 0x1c, 0x7e, 0x68,
+ 0x52, 0x44, 0x96, 0x80, 0xba, 0xac, 0xce, 0xd8, 0xe2, 0xf4,
+ 0x5b, 0x4d, 0x77, 0x61, 0x03, 0x15, 0x2f, 0x39, 0xeb, 0xfd,
+ 0xc7, 0xd1, 0xb3, 0xa5, 0x9f, 0x89, 0xdc, 0xca, 0xf0, 0xe6,
+ 0x84, 0x92, 0xa8, 0xbe, 0x6c, 0x7a, 0x40, 0x56, 0x34, 0x22,
+ 0x18, 0x0e, 0xa1, 0xb7, 0x8d, 0x9b, 0xf9, 0xef, 0xd5, 0xc3,
+ 0x11, 0x07, 0x3d, 0x2b, 0x49, 0x5f, 0x65, 0x73, 0x00, 0x17,
+ 0x2e, 0x39, 0x5c, 0x4b, 0x72, 0x65, 0xb8, 0xaf, 0x96, 0x81,
+ 0xe4, 0xf3, 0xca, 0xdd, 0x6d, 0x7a, 0x43, 0x54, 0x31, 0x26,
+ 0x1f, 0x08, 0xd5, 0xc2, 0xfb, 0xec, 0x89, 0x9e, 0xa7, 0xb0,
+ 0xda, 0xcd, 0xf4, 0xe3, 0x86, 0x91, 0xa8, 0xbf, 0x62, 0x75,
+ 0x4c, 0x5b, 0x3e, 0x29, 0x10, 0x07, 0xb7, 0xa0, 0x99, 0x8e,
+ 0xeb, 0xfc, 0xc5, 0xd2, 0x0f, 0x18, 0x21, 0x36, 0x53, 0x44,
+ 0x7d, 0x6a, 0xa9, 0xbe, 0x87, 0x90, 0xf5, 0xe2, 0xdb, 0xcc,
+ 0x11, 0x06, 0x3f, 0x28, 0x4d, 0x5a, 0x63, 0x74, 0xc4, 0xd3,
+ 0xea, 0xfd, 0x98, 0x8f, 0xb6, 0xa1, 0x7c, 0x6b, 0x52, 0x45,
+ 0x20, 0x37, 0x0e, 0x19, 0x73, 0x64, 0x5d, 0x4a, 0x2f, 0x38,
+ 0x01, 0x16, 0xcb, 0xdc, 0xe5, 0xf2, 0x97, 0x80, 0xb9, 0xae,
+ 0x1e, 0x09, 0x30, 0x27, 0x42, 0x55, 0x6c, 0x7b, 0xa6, 0xb1,
+ 0x88, 0x9f, 0xfa, 0xed, 0xd4, 0xc3, 0x4f, 0x58, 0x61, 0x76,
+ 0x13, 0x04, 0x3d, 0x2a, 0xf7, 0xe0, 0xd9, 0xce, 0xab, 0xbc,
+ 0x85, 0x92, 0x22, 0x35, 0x0c, 0x1b, 0x7e, 0x69, 0x50, 0x47,
+ 0x9a, 0x8d, 0xb4, 0xa3, 0xc6, 0xd1, 0xe8, 0xff, 0x95, 0x82,
+ 0xbb, 0xac, 0xc9, 0xde, 0xe7, 0xf0, 0x2d, 0x3a, 0x03, 0x14,
+ 0x71, 0x66, 0x5f, 0x48, 0xf8, 0xef, 0xd6, 0xc1, 0xa4, 0xb3,
+ 0x8a, 0x9d, 0x40, 0x57, 0x6e, 0x79, 0x1c, 0x0b, 0x32, 0x25,
+ 0xe6, 0xf1, 0xc8, 0xdf, 0xba, 0xad, 0x94, 0x83, 0x5e, 0x49,
+ 0x70, 0x67, 0x02, 0x15, 0x2c, 0x3b, 0x8b, 0x9c, 0xa5, 0xb2,
+ 0xd7, 0xc0, 0xf9, 0xee, 0x33, 0x24, 0x1d, 0x0a, 0x6f, 0x78,
+ 0x41, 0x56, 0x3c, 0x2b, 0x12, 0x05, 0x60, 0x77, 0x4e, 0x59,
+ 0x84, 0x93, 0xaa, 0xbd, 0xd8, 0xcf, 0xf6, 0xe1, 0x51, 0x46,
+ 0x7f, 0x68, 0x0d, 0x1a, 0x23, 0x34, 0xe9, 0xfe, 0xc7, 0xd0,
+ 0xb5, 0xa2, 0x9b, 0x8c, 0x00, 0x18, 0x30, 0x28, 0x60, 0x78,
+ 0x50, 0x48, 0xc0, 0xd8, 0xf0, 0xe8, 0xa0, 0xb8, 0x90, 0x88,
+ 0x9d, 0x85, 0xad, 0xb5, 0xfd, 0xe5, 0xcd, 0xd5, 0x5d, 0x45,
+ 0x6d, 0x75, 0x3d, 0x25, 0x0d, 0x15, 0x27, 0x3f, 0x17, 0x0f,
+ 0x47, 0x5f, 0x77, 0x6f, 0xe7, 0xff, 0xd7, 0xcf, 0x87, 0x9f,
+ 0xb7, 0xaf, 0xba, 0xa2, 0x8a, 0x92, 0xda, 0xc2, 0xea, 0xf2,
+ 0x7a, 0x62, 0x4a, 0x52, 0x1a, 0x02, 0x2a, 0x32, 0x4e, 0x56,
+ 0x7e, 0x66, 0x2e, 0x36, 0x1e, 0x06, 0x8e, 0x96, 0xbe, 0xa6,
+ 0xee, 0xf6, 0xde, 0xc6, 0xd3, 0xcb, 0xe3, 0xfb, 0xb3, 0xab,
+ 0x83, 0x9b, 0x13, 0x0b, 0x23, 0x3b, 0x73, 0x6b, 0x43, 0x5b,
+ 0x69, 0x71, 0x59, 0x41, 0x09, 0x11, 0x39, 0x21, 0xa9, 0xb1,
+ 0x99, 0x81, 0xc9, 0xd1, 0xf9, 0xe1, 0xf4, 0xec, 0xc4, 0xdc,
+ 0x94, 0x8c, 0xa4, 0xbc, 0x34, 0x2c, 0x04, 0x1c, 0x54, 0x4c,
+ 0x64, 0x7c, 0x9c, 0x84, 0xac, 0xb4, 0xfc, 0xe4, 0xcc, 0xd4,
+ 0x5c, 0x44, 0x6c, 0x74, 0x3c, 0x24, 0x0c, 0x14, 0x01, 0x19,
+ 0x31, 0x29, 0x61, 0x79, 0x51, 0x49, 0xc1, 0xd9, 0xf1, 0xe9,
+ 0xa1, 0xb9, 0x91, 0x89, 0xbb, 0xa3, 0x8b, 0x93, 0xdb, 0xc3,
+ 0xeb, 0xf3, 0x7b, 0x63, 0x4b, 0x53, 0x1b, 0x03, 0x2b, 0x33,
+ 0x26, 0x3e, 0x16, 0x0e, 0x46, 0x5e, 0x76, 0x6e, 0xe6, 0xfe,
+ 0xd6, 0xce, 0x86, 0x9e, 0xb6, 0xae, 0xd2, 0xca, 0xe2, 0xfa,
+ 0xb2, 0xaa, 0x82, 0x9a, 0x12, 0x0a, 0x22, 0x3a, 0x72, 0x6a,
+ 0x42, 0x5a, 0x4f, 0x57, 0x7f, 0x67, 0x2f, 0x37, 0x1f, 0x07,
+ 0x8f, 0x97, 0xbf, 0xa7, 0xef, 0xf7, 0xdf, 0xc7, 0xf5, 0xed,
+ 0xc5, 0xdd, 0x95, 0x8d, 0xa5, 0xbd, 0x35, 0x2d, 0x05, 0x1d,
+ 0x55, 0x4d, 0x65, 0x7d, 0x68, 0x70, 0x58, 0x40, 0x08, 0x10,
+ 0x38, 0x20, 0xa8, 0xb0, 0x98, 0x80, 0xc8, 0xd0, 0xf8, 0xe0,
+ 0x00, 0x19, 0x32, 0x2b, 0x64, 0x7d, 0x56, 0x4f, 0xc8, 0xd1,
+ 0xfa, 0xe3, 0xac, 0xb5, 0x9e, 0x87, 0x8d, 0x94, 0xbf, 0xa6,
+ 0xe9, 0xf0, 0xdb, 0xc2, 0x45, 0x5c, 0x77, 0x6e, 0x21, 0x38,
+ 0x13, 0x0a, 0x07, 0x1e, 0x35, 0x2c, 0x63, 0x7a, 0x51, 0x48,
+ 0xcf, 0xd6, 0xfd, 0xe4, 0xab, 0xb2, 0x99, 0x80, 0x8a, 0x93,
+ 0xb8, 0xa1, 0xee, 0xf7, 0xdc, 0xc5, 0x42, 0x5b, 0x70, 0x69,
+ 0x26, 0x3f, 0x14, 0x0d, 0x0e, 0x17, 0x3c, 0x25, 0x6a, 0x73,
+ 0x58, 0x41, 0xc6, 0xdf, 0xf4, 0xed, 0xa2, 0xbb, 0x90, 0x89,
+ 0x83, 0x9a, 0xb1, 0xa8, 0xe7, 0xfe, 0xd5, 0xcc, 0x4b, 0x52,
+ 0x79, 0x60, 0x2f, 0x36, 0x1d, 0x04, 0x09, 0x10, 0x3b, 0x22,
+ 0x6d, 0x74, 0x5f, 0x46, 0xc1, 0xd8, 0xf3, 0xea, 0xa5, 0xbc,
+ 0x97, 0x8e, 0x84, 0x9d, 0xb6, 0xaf, 0xe0, 0xf9, 0xd2, 0xcb,
+ 0x4c, 0x55, 0x7e, 0x67, 0x28, 0x31, 0x1a, 0x03, 0x1c, 0x05,
+ 0x2e, 0x37, 0x78, 0x61, 0x4a, 0x53, 0xd4, 0xcd, 0xe6, 0xff,
+ 0xb0, 0xa9, 0x82, 0x9b, 0x91, 0x88, 0xa3, 0xba, 0xf5, 0xec,
+ 0xc7, 0xde, 0x59, 0x40, 0x6b, 0x72, 0x3d, 0x24, 0x0f, 0x16,
+ 0x1b, 0x02, 0x29, 0x30, 0x7f, 0x66, 0x4d, 0x54, 0xd3, 0xca,
+ 0xe1, 0xf8, 0xb7, 0xae, 0x85, 0x9c, 0x96, 0x8f, 0xa4, 0xbd,
+ 0xf2, 0xeb, 0xc0, 0xd9, 0x5e, 0x47, 0x6c, 0x75, 0x3a, 0x23,
+ 0x08, 0x11, 0x12, 0x0b, 0x20, 0x39, 0x76, 0x6f, 0x44, 0x5d,
+ 0xda, 0xc3, 0xe8, 0xf1, 0xbe, 0xa7, 0x8c, 0x95, 0x9f, 0x86,
+ 0xad, 0xb4, 0xfb, 0xe2, 0xc9, 0xd0, 0x57, 0x4e, 0x65, 0x7c,
+ 0x33, 0x2a, 0x01, 0x18, 0x15, 0x0c, 0x27, 0x3e, 0x71, 0x68,
+ 0x43, 0x5a, 0xdd, 0xc4, 0xef, 0xf6, 0xb9, 0xa0, 0x8b, 0x92,
+ 0x98, 0x81, 0xaa, 0xb3, 0xfc, 0xe5, 0xce, 0xd7, 0x50, 0x49,
+ 0x62, 0x7b, 0x34, 0x2d, 0x06, 0x1f, 0x00, 0x1a, 0x34, 0x2e,
+ 0x68, 0x72, 0x5c, 0x46, 0xd0, 0xca, 0xe4, 0xfe, 0xb8, 0xa2,
+ 0x8c, 0x96, 0xbd, 0xa7, 0x89, 0x93, 0xd5, 0xcf, 0xe1, 0xfb,
+ 0x6d, 0x77, 0x59, 0x43, 0x05, 0x1f, 0x31, 0x2b, 0x67, 0x7d,
+ 0x53, 0x49, 0x0f, 0x15, 0x3b, 0x21, 0xb7, 0xad, 0x83, 0x99,
+ 0xdf, 0xc5, 0xeb, 0xf1, 0xda, 0xc0, 0xee, 0xf4, 0xb2, 0xa8,
+ 0x86, 0x9c, 0x0a, 0x10, 0x3e, 0x24, 0x62, 0x78, 0x56, 0x4c,
+ 0xce, 0xd4, 0xfa, 0xe0, 0xa6, 0xbc, 0x92, 0x88, 0x1e, 0x04,
+ 0x2a, 0x30, 0x76, 0x6c, 0x42, 0x58, 0x73, 0x69, 0x47, 0x5d,
+ 0x1b, 0x01, 0x2f, 0x35, 0xa3, 0xb9, 0x97, 0x8d, 0xcb, 0xd1,
+ 0xff, 0xe5, 0xa9, 0xb3, 0x9d, 0x87, 0xc1, 0xdb, 0xf5, 0xef,
+ 0x79, 0x63, 0x4d, 0x57, 0x11, 0x0b, 0x25, 0x3f, 0x14, 0x0e,
+ 0x20, 0x3a, 0x7c, 0x66, 0x48, 0x52, 0xc4, 0xde, 0xf0, 0xea,
+ 0xac, 0xb6, 0x98, 0x82, 0x81, 0x9b, 0xb5, 0xaf, 0xe9, 0xf3,
+ 0xdd, 0xc7, 0x51, 0x4b, 0x65, 0x7f, 0x39, 0x23, 0x0d, 0x17,
+ 0x3c, 0x26, 0x08, 0x12, 0x54, 0x4e, 0x60, 0x7a, 0xec, 0xf6,
+ 0xd8, 0xc2, 0x84, 0x9e, 0xb0, 0xaa, 0xe6, 0xfc, 0xd2, 0xc8,
+ 0x8e, 0x94, 0xba, 0xa0, 0x36, 0x2c, 0x02, 0x18, 0x5e, 0x44,
+ 0x6a, 0x70, 0x5b, 0x41, 0x6f, 0x75, 0x33, 0x29, 0x07, 0x1d,
+ 0x8b, 0x91, 0xbf, 0xa5, 0xe3, 0xf9, 0xd7, 0xcd, 0x4f, 0x55,
+ 0x7b, 0x61, 0x27, 0x3d, 0x13, 0x09, 0x9f, 0x85, 0xab, 0xb1,
+ 0xf7, 0xed, 0xc3, 0xd9, 0xf2, 0xe8, 0xc6, 0xdc, 0x9a, 0x80,
+ 0xae, 0xb4, 0x22, 0x38, 0x16, 0x0c, 0x4a, 0x50, 0x7e, 0x64,
+ 0x28, 0x32, 0x1c, 0x06, 0x40, 0x5a, 0x74, 0x6e, 0xf8, 0xe2,
+ 0xcc, 0xd6, 0x90, 0x8a, 0xa4, 0xbe, 0x95, 0x8f, 0xa1, 0xbb,
+ 0xfd, 0xe7, 0xc9, 0xd3, 0x45, 0x5f, 0x71, 0x6b, 0x2d, 0x37,
+ 0x19, 0x03, 0x00, 0x1b, 0x36, 0x2d, 0x6c, 0x77, 0x5a, 0x41,
+ 0xd8, 0xc3, 0xee, 0xf5, 0xb4, 0xaf, 0x82, 0x99, 0xad, 0xb6,
+ 0x9b, 0x80, 0xc1, 0xda, 0xf7, 0xec, 0x75, 0x6e, 0x43, 0x58,
+ 0x19, 0x02, 0x2f, 0x34, 0x47, 0x5c, 0x71, 0x6a, 0x2b, 0x30,
+ 0x1d, 0x06, 0x9f, 0x84, 0xa9, 0xb2, 0xf3, 0xe8, 0xc5, 0xde,
+ 0xea, 0xf1, 0xdc, 0xc7, 0x86, 0x9d, 0xb0, 0xab, 0x32, 0x29,
+ 0x04, 0x1f, 0x5e, 0x45, 0x68, 0x73, 0x8e, 0x95, 0xb8, 0xa3,
+ 0xe2, 0xf9, 0xd4, 0xcf, 0x56, 0x4d, 0x60, 0x7b, 0x3a, 0x21,
+ 0x0c, 0x17, 0x23, 0x38, 0x15, 0x0e, 0x4f, 0x54, 0x79, 0x62,
+ 0xfb, 0xe0, 0xcd, 0xd6, 0x97, 0x8c, 0xa1, 0xba, 0xc9, 0xd2,
+ 0xff, 0xe4, 0xa5, 0xbe, 0x93, 0x88, 0x11, 0x0a, 0x27, 0x3c,
+ 0x7d, 0x66, 0x4b, 0x50, 0x64, 0x7f, 0x52, 0x49, 0x08, 0x13,
+ 0x3e, 0x25, 0xbc, 0xa7, 0x8a, 0x91, 0xd0, 0xcb, 0xe6, 0xfd,
+ 0x01, 0x1a, 0x37, 0x2c, 0x6d, 0x76, 0x5b, 0x40, 0xd9, 0xc2,
+ 0xef, 0xf4, 0xb5, 0xae, 0x83, 0x98, 0xac, 0xb7, 0x9a, 0x81,
+ 0xc0, 0xdb, 0xf6, 0xed, 0x74, 0x6f, 0x42, 0x59, 0x18, 0x03,
+ 0x2e, 0x35, 0x46, 0x5d, 0x70, 0x6b, 0x2a, 0x31, 0x1c, 0x07,
+ 0x9e, 0x85, 0xa8, 0xb3, 0xf2, 0xe9, 0xc4, 0xdf, 0xeb, 0xf0,
+ 0xdd, 0xc6, 0x87, 0x9c, 0xb1, 0xaa, 0x33, 0x28, 0x05, 0x1e,
+ 0x5f, 0x44, 0x69, 0x72, 0x8f, 0x94, 0xb9, 0xa2, 0xe3, 0xf8,
+ 0xd5, 0xce, 0x57, 0x4c, 0x61, 0x7a, 0x3b, 0x20, 0x0d, 0x16,
+ 0x22, 0x39, 0x14, 0x0f, 0x4e, 0x55, 0x78, 0x63, 0xfa, 0xe1,
+ 0xcc, 0xd7, 0x96, 0x8d, 0xa0, 0xbb, 0xc8, 0xd3, 0xfe, 0xe5,
+ 0xa4, 0xbf, 0x92, 0x89, 0x10, 0x0b, 0x26, 0x3d, 0x7c, 0x67,
+ 0x4a, 0x51, 0x65, 0x7e, 0x53, 0x48, 0x09, 0x12, 0x3f, 0x24,
+ 0xbd, 0xa6, 0x8b, 0x90, 0xd1, 0xca, 0xe7, 0xfc, 0x00, 0x1c,
+ 0x38, 0x24, 0x70, 0x6c, 0x48, 0x54, 0xe0, 0xfc, 0xd8, 0xc4,
+ 0x90, 0x8c, 0xa8, 0xb4, 0xdd, 0xc1, 0xe5, 0xf9, 0xad, 0xb1,
+ 0x95, 0x89, 0x3d, 0x21, 0x05, 0x19, 0x4d, 0x51, 0x75, 0x69,
+ 0xa7, 0xbb, 0x9f, 0x83, 0xd7, 0xcb, 0xef, 0xf3, 0x47, 0x5b,
+ 0x7f, 0x63, 0x37, 0x2b, 0x0f, 0x13, 0x7a, 0x66, 0x42, 0x5e,
+ 0x0a, 0x16, 0x32, 0x2e, 0x9a, 0x86, 0xa2, 0xbe, 0xea, 0xf6,
+ 0xd2, 0xce, 0x53, 0x4f, 0x6b, 0x77, 0x23, 0x3f, 0x1b, 0x07,
+ 0xb3, 0xaf, 0x8b, 0x97, 0xc3, 0xdf, 0xfb, 0xe7, 0x8e, 0x92,
+ 0xb6, 0xaa, 0xfe, 0xe2, 0xc6, 0xda, 0x6e, 0x72, 0x56, 0x4a,
+ 0x1e, 0x02, 0x26, 0x3a, 0xf4, 0xe8, 0xcc, 0xd0, 0x84, 0x98,
+ 0xbc, 0xa0, 0x14, 0x08, 0x2c, 0x30, 0x64, 0x78, 0x5c, 0x40,
+ 0x29, 0x35, 0x11, 0x0d, 0x59, 0x45, 0x61, 0x7d, 0xc9, 0xd5,
+ 0xf1, 0xed, 0xb9, 0xa5, 0x81, 0x9d, 0xa6, 0xba, 0x9e, 0x82,
+ 0xd6, 0xca, 0xee, 0xf2, 0x46, 0x5a, 0x7e, 0x62, 0x36, 0x2a,
+ 0x0e, 0x12, 0x7b, 0x67, 0x43, 0x5f, 0x0b, 0x17, 0x33, 0x2f,
+ 0x9b, 0x87, 0xa3, 0xbf, 0xeb, 0xf7, 0xd3, 0xcf, 0x01, 0x1d,
+ 0x39, 0x25, 0x71, 0x6d, 0x49, 0x55, 0xe1, 0xfd, 0xd9, 0xc5,
+ 0x91, 0x8d, 0xa9, 0xb5, 0xdc, 0xc0, 0xe4, 0xf8, 0xac, 0xb0,
+ 0x94, 0x88, 0x3c, 0x20, 0x04, 0x18, 0x4c, 0x50, 0x74, 0x68,
+ 0xf5, 0xe9, 0xcd, 0xd1, 0x85, 0x99, 0xbd, 0xa1, 0x15, 0x09,
+ 0x2d, 0x31, 0x65, 0x79, 0x5d, 0x41, 0x28, 0x34, 0x10, 0x0c,
+ 0x58, 0x44, 0x60, 0x7c, 0xc8, 0xd4, 0xf0, 0xec, 0xb8, 0xa4,
+ 0x80, 0x9c, 0x52, 0x4e, 0x6a, 0x76, 0x22, 0x3e, 0x1a, 0x06,
+ 0xb2, 0xae, 0x8a, 0x96, 0xc2, 0xde, 0xfa, 0xe6, 0x8f, 0x93,
+ 0xb7, 0xab, 0xff, 0xe3, 0xc7, 0xdb, 0x6f, 0x73, 0x57, 0x4b,
+ 0x1f, 0x03, 0x27, 0x3b, 0x00, 0x1d, 0x3a, 0x27, 0x74, 0x69,
+ 0x4e, 0x53, 0xe8, 0xf5, 0xd2, 0xcf, 0x9c, 0x81, 0xa6, 0xbb,
+ 0xcd, 0xd0, 0xf7, 0xea, 0xb9, 0xa4, 0x83, 0x9e, 0x25, 0x38,
+ 0x1f, 0x02, 0x51, 0x4c, 0x6b, 0x76, 0x87, 0x9a, 0xbd, 0xa0,
+ 0xf3, 0xee, 0xc9, 0xd4, 0x6f, 0x72, 0x55, 0x48, 0x1b, 0x06,
+ 0x21, 0x3c, 0x4a, 0x57, 0x70, 0x6d, 0x3e, 0x23, 0x04, 0x19,
+ 0xa2, 0xbf, 0x98, 0x85, 0xd6, 0xcb, 0xec, 0xf1, 0x13, 0x0e,
+ 0x29, 0x34, 0x67, 0x7a, 0x5d, 0x40, 0xfb, 0xe6, 0xc1, 0xdc,
+ 0x8f, 0x92, 0xb5, 0xa8, 0xde, 0xc3, 0xe4, 0xf9, 0xaa, 0xb7,
+ 0x90, 0x8d, 0x36, 0x2b, 0x0c, 0x11, 0x42, 0x5f, 0x78, 0x65,
+ 0x94, 0x89, 0xae, 0xb3, 0xe0, 0xfd, 0xda, 0xc7, 0x7c, 0x61,
+ 0x46, 0x5b, 0x08, 0x15, 0x32, 0x2f, 0x59, 0x44, 0x63, 0x7e,
+ 0x2d, 0x30, 0x17, 0x0a, 0xb1, 0xac, 0x8b, 0x96, 0xc5, 0xd8,
+ 0xff, 0xe2, 0x26, 0x3b, 0x1c, 0x01, 0x52, 0x4f, 0x68, 0x75,
+ 0xce, 0xd3, 0xf4, 0xe9, 0xba, 0xa7, 0x80, 0x9d, 0xeb, 0xf6,
+ 0xd1, 0xcc, 0x9f, 0x82, 0xa5, 0xb8, 0x03, 0x1e, 0x39, 0x24,
+ 0x77, 0x6a, 0x4d, 0x50, 0xa1, 0xbc, 0x9b, 0x86, 0xd5, 0xc8,
+ 0xef, 0xf2, 0x49, 0x54, 0x73, 0x6e, 0x3d, 0x20, 0x07, 0x1a,
+ 0x6c, 0x71, 0x56, 0x4b, 0x18, 0x05, 0x22, 0x3f, 0x84, 0x99,
+ 0xbe, 0xa3, 0xf0, 0xed, 0xca, 0xd7, 0x35, 0x28, 0x0f, 0x12,
+ 0x41, 0x5c, 0x7b, 0x66, 0xdd, 0xc0, 0xe7, 0xfa, 0xa9, 0xb4,
+ 0x93, 0x8e, 0xf8, 0xe5, 0xc2, 0xdf, 0x8c, 0x91, 0xb6, 0xab,
+ 0x10, 0x0d, 0x2a, 0x37, 0x64, 0x79, 0x5e, 0x43, 0xb2, 0xaf,
+ 0x88, 0x95, 0xc6, 0xdb, 0xfc, 0xe1, 0x5a, 0x47, 0x60, 0x7d,
+ 0x2e, 0x33, 0x14, 0x09, 0x7f, 0x62, 0x45, 0x58, 0x0b, 0x16,
+ 0x31, 0x2c, 0x97, 0x8a, 0xad, 0xb0, 0xe3, 0xfe, 0xd9, 0xc4,
+ 0x00, 0x1e, 0x3c, 0x22, 0x78, 0x66, 0x44, 0x5a, 0xf0, 0xee,
+ 0xcc, 0xd2, 0x88, 0x96, 0xb4, 0xaa, 0xfd, 0xe3, 0xc1, 0xdf,
+ 0x85, 0x9b, 0xb9, 0xa7, 0x0d, 0x13, 0x31, 0x2f, 0x75, 0x6b,
+ 0x49, 0x57, 0xe7, 0xf9, 0xdb, 0xc5, 0x9f, 0x81, 0xa3, 0xbd,
+ 0x17, 0x09, 0x2b, 0x35, 0x6f, 0x71, 0x53, 0x4d, 0x1a, 0x04,
+ 0x26, 0x38, 0x62, 0x7c, 0x5e, 0x40, 0xea, 0xf4, 0xd6, 0xc8,
+ 0x92, 0x8c, 0xae, 0xb0, 0xd3, 0xcd, 0xef, 0xf1, 0xab, 0xb5,
+ 0x97, 0x89, 0x23, 0x3d, 0x1f, 0x01, 0x5b, 0x45, 0x67, 0x79,
+ 0x2e, 0x30, 0x12, 0x0c, 0x56, 0x48, 0x6a, 0x74, 0xde, 0xc0,
+ 0xe2, 0xfc, 0xa6, 0xb8, 0x9a, 0x84, 0x34, 0x2a, 0x08, 0x16,
+ 0x4c, 0x52, 0x70, 0x6e, 0xc4, 0xda, 0xf8, 0xe6, 0xbc, 0xa2,
+ 0x80, 0x9e, 0xc9, 0xd7, 0xf5, 0xeb, 0xb1, 0xaf, 0x8d, 0x93,
+ 0x39, 0x27, 0x05, 0x1b, 0x41, 0x5f, 0x7d, 0x63, 0xbb, 0xa5,
+ 0x87, 0x99, 0xc3, 0xdd, 0xff, 0xe1, 0x4b, 0x55, 0x77, 0x69,
+ 0x33, 0x2d, 0x0f, 0x11, 0x46, 0x58, 0x7a, 0x64, 0x3e, 0x20,
+ 0x02, 0x1c, 0xb6, 0xa8, 0x8a, 0x94, 0xce, 0xd0, 0xf2, 0xec,
+ 0x5c, 0x42, 0x60, 0x7e, 0x24, 0x3a, 0x18, 0x06, 0xac, 0xb2,
+ 0x90, 0x8e, 0xd4, 0xca, 0xe8, 0xf6, 0xa1, 0xbf, 0x9d, 0x83,
+ 0xd9, 0xc7, 0xe5, 0xfb, 0x51, 0x4f, 0x6d, 0x73, 0x29, 0x37,
+ 0x15, 0x0b, 0x68, 0x76, 0x54, 0x4a, 0x10, 0x0e, 0x2c, 0x32,
+ 0x98, 0x86, 0xa4, 0xba, 0xe0, 0xfe, 0xdc, 0xc2, 0x95, 0x8b,
+ 0xa9, 0xb7, 0xed, 0xf3, 0xd1, 0xcf, 0x65, 0x7b, 0x59, 0x47,
+ 0x1d, 0x03, 0x21, 0x3f, 0x8f, 0x91, 0xb3, 0xad, 0xf7, 0xe9,
+ 0xcb, 0xd5, 0x7f, 0x61, 0x43, 0x5d, 0x07, 0x19, 0x3b, 0x25,
+ 0x72, 0x6c, 0x4e, 0x50, 0x0a, 0x14, 0x36, 0x28, 0x82, 0x9c,
+ 0xbe, 0xa0, 0xfa, 0xe4, 0xc6, 0xd8, 0x00, 0x1f, 0x3e, 0x21,
+ 0x7c, 0x63, 0x42, 0x5d, 0xf8, 0xe7, 0xc6, 0xd9, 0x84, 0x9b,
+ 0xba, 0xa5, 0xed, 0xf2, 0xd3, 0xcc, 0x91, 0x8e, 0xaf, 0xb0,
+ 0x15, 0x0a, 0x2b, 0x34, 0x69, 0x76, 0x57, 0x48, 0xc7, 0xd8,
+ 0xf9, 0xe6, 0xbb, 0xa4, 0x85, 0x9a, 0x3f, 0x20, 0x01, 0x1e,
+ 0x43, 0x5c, 0x7d, 0x62, 0x2a, 0x35, 0x14, 0x0b, 0x56, 0x49,
+ 0x68, 0x77, 0xd2, 0xcd, 0xec, 0xf3, 0xae, 0xb1, 0x90, 0x8f,
+ 0x93, 0x8c, 0xad, 0xb2, 0xef, 0xf0, 0xd1, 0xce, 0x6b, 0x74,
+ 0x55, 0x4a, 0x17, 0x08, 0x29, 0x36, 0x7e, 0x61, 0x40, 0x5f,
+ 0x02, 0x1d, 0x3c, 0x23, 0x86, 0x99, 0xb8, 0xa7, 0xfa, 0xe5,
+ 0xc4, 0xdb, 0x54, 0x4b, 0x6a, 0x75, 0x28, 0x37, 0x16, 0x09,
+ 0xac, 0xb3, 0x92, 0x8d, 0xd0, 0xcf, 0xee, 0xf1, 0xb9, 0xa6,
+ 0x87, 0x98, 0xc5, 0xda, 0xfb, 0xe4, 0x41, 0x5e, 0x7f, 0x60,
+ 0x3d, 0x22, 0x03, 0x1c, 0x3b, 0x24, 0x05, 0x1a, 0x47, 0x58,
+ 0x79, 0x66, 0xc3, 0xdc, 0xfd, 0xe2, 0xbf, 0xa0, 0x81, 0x9e,
+ 0xd6, 0xc9, 0xe8, 0xf7, 0xaa, 0xb5, 0x94, 0x8b, 0x2e, 0x31,
+ 0x10, 0x0f, 0x52, 0x4d, 0x6c, 0x73, 0xfc, 0xe3, 0xc2, 0xdd,
+ 0x80, 0x9f, 0xbe, 0xa1, 0x04, 0x1b, 0x3a, 0x25, 0x78, 0x67,
+ 0x46, 0x59, 0x11, 0x0e, 0x2f, 0x30, 0x6d, 0x72, 0x53, 0x4c,
+ 0xe9, 0xf6, 0xd7, 0xc8, 0x95, 0x8a, 0xab, 0xb4, 0xa8, 0xb7,
+ 0x96, 0x89, 0xd4, 0xcb, 0xea, 0xf5, 0x50, 0x4f, 0x6e, 0x71,
+ 0x2c, 0x33, 0x12, 0x0d, 0x45, 0x5a, 0x7b, 0x64, 0x39, 0x26,
+ 0x07, 0x18, 0xbd, 0xa2, 0x83, 0x9c, 0xc1, 0xde, 0xff, 0xe0,
+ 0x6f, 0x70, 0x51, 0x4e, 0x13, 0x0c, 0x2d, 0x32, 0x97, 0x88,
+ 0xa9, 0xb6, 0xeb, 0xf4, 0xd5, 0xca, 0x82, 0x9d, 0xbc, 0xa3,
+ 0xfe, 0xe1, 0xc0, 0xdf, 0x7a, 0x65, 0x44, 0x5b, 0x06, 0x19,
+ 0x38, 0x27, 0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0,
+ 0x1d, 0x3d, 0x5d, 0x7d, 0x9d, 0xbd, 0xdd, 0xfd, 0x3a, 0x1a,
+ 0x7a, 0x5a, 0xba, 0x9a, 0xfa, 0xda, 0x27, 0x07, 0x67, 0x47,
+ 0xa7, 0x87, 0xe7, 0xc7, 0x74, 0x54, 0x34, 0x14, 0xf4, 0xd4,
+ 0xb4, 0x94, 0x69, 0x49, 0x29, 0x09, 0xe9, 0xc9, 0xa9, 0x89,
+ 0x4e, 0x6e, 0x0e, 0x2e, 0xce, 0xee, 0x8e, 0xae, 0x53, 0x73,
+ 0x13, 0x33, 0xd3, 0xf3, 0x93, 0xb3, 0xe8, 0xc8, 0xa8, 0x88,
+ 0x68, 0x48, 0x28, 0x08, 0xf5, 0xd5, 0xb5, 0x95, 0x75, 0x55,
+ 0x35, 0x15, 0xd2, 0xf2, 0x92, 0xb2, 0x52, 0x72, 0x12, 0x32,
+ 0xcf, 0xef, 0x8f, 0xaf, 0x4f, 0x6f, 0x0f, 0x2f, 0x9c, 0xbc,
+ 0xdc, 0xfc, 0x1c, 0x3c, 0x5c, 0x7c, 0x81, 0xa1, 0xc1, 0xe1,
+ 0x01, 0x21, 0x41, 0x61, 0xa6, 0x86, 0xe6, 0xc6, 0x26, 0x06,
+ 0x66, 0x46, 0xbb, 0x9b, 0xfb, 0xdb, 0x3b, 0x1b, 0x7b, 0x5b,
+ 0xcd, 0xed, 0x8d, 0xad, 0x4d, 0x6d, 0x0d, 0x2d, 0xd0, 0xf0,
+ 0x90, 0xb0, 0x50, 0x70, 0x10, 0x30, 0xf7, 0xd7, 0xb7, 0x97,
+ 0x77, 0x57, 0x37, 0x17, 0xea, 0xca, 0xaa, 0x8a, 0x6a, 0x4a,
+ 0x2a, 0x0a, 0xb9, 0x99, 0xf9, 0xd9, 0x39, 0x19, 0x79, 0x59,
+ 0xa4, 0x84, 0xe4, 0xc4, 0x24, 0x04, 0x64, 0x44, 0x83, 0xa3,
+ 0xc3, 0xe3, 0x03, 0x23, 0x43, 0x63, 0x9e, 0xbe, 0xde, 0xfe,
+ 0x1e, 0x3e, 0x5e, 0x7e, 0x25, 0x05, 0x65, 0x45, 0xa5, 0x85,
+ 0xe5, 0xc5, 0x38, 0x18, 0x78, 0x58, 0xb8, 0x98, 0xf8, 0xd8,
+ 0x1f, 0x3f, 0x5f, 0x7f, 0x9f, 0xbf, 0xdf, 0xff, 0x02, 0x22,
+ 0x42, 0x62, 0x82, 0xa2, 0xc2, 0xe2, 0x51, 0x71, 0x11, 0x31,
+ 0xd1, 0xf1, 0x91, 0xb1, 0x4c, 0x6c, 0x0c, 0x2c, 0xcc, 0xec,
+ 0x8c, 0xac, 0x6b, 0x4b, 0x2b, 0x0b, 0xeb, 0xcb, 0xab, 0x8b,
+ 0x76, 0x56, 0x36, 0x16, 0xf6, 0xd6, 0xb6, 0x96, 0x00, 0x21,
+ 0x42, 0x63, 0x84, 0xa5, 0xc6, 0xe7, 0x15, 0x34, 0x57, 0x76,
+ 0x91, 0xb0, 0xd3, 0xf2, 0x2a, 0x0b, 0x68, 0x49, 0xae, 0x8f,
+ 0xec, 0xcd, 0x3f, 0x1e, 0x7d, 0x5c, 0xbb, 0x9a, 0xf9, 0xd8,
+ 0x54, 0x75, 0x16, 0x37, 0xd0, 0xf1, 0x92, 0xb3, 0x41, 0x60,
+ 0x03, 0x22, 0xc5, 0xe4, 0x87, 0xa6, 0x7e, 0x5f, 0x3c, 0x1d,
+ 0xfa, 0xdb, 0xb8, 0x99, 0x6b, 0x4a, 0x29, 0x08, 0xef, 0xce,
+ 0xad, 0x8c, 0xa8, 0x89, 0xea, 0xcb, 0x2c, 0x0d, 0x6e, 0x4f,
+ 0xbd, 0x9c, 0xff, 0xde, 0x39, 0x18, 0x7b, 0x5a, 0x82, 0xa3,
+ 0xc0, 0xe1, 0x06, 0x27, 0x44, 0x65, 0x97, 0xb6, 0xd5, 0xf4,
+ 0x13, 0x32, 0x51, 0x70, 0xfc, 0xdd, 0xbe, 0x9f, 0x78, 0x59,
+ 0x3a, 0x1b, 0xe9, 0xc8, 0xab, 0x8a, 0x6d, 0x4c, 0x2f, 0x0e,
+ 0xd6, 0xf7, 0x94, 0xb5, 0x52, 0x73, 0x10, 0x31, 0xc3, 0xe2,
+ 0x81, 0xa0, 0x47, 0x66, 0x05, 0x24, 0x4d, 0x6c, 0x0f, 0x2e,
+ 0xc9, 0xe8, 0x8b, 0xaa, 0x58, 0x79, 0x1a, 0x3b, 0xdc, 0xfd,
+ 0x9e, 0xbf, 0x67, 0x46, 0x25, 0x04, 0xe3, 0xc2, 0xa1, 0x80,
+ 0x72, 0x53, 0x30, 0x11, 0xf6, 0xd7, 0xb4, 0x95, 0x19, 0x38,
+ 0x5b, 0x7a, 0x9d, 0xbc, 0xdf, 0xfe, 0x0c, 0x2d, 0x4e, 0x6f,
+ 0x88, 0xa9, 0xca, 0xeb, 0x33, 0x12, 0x71, 0x50, 0xb7, 0x96,
+ 0xf5, 0xd4, 0x26, 0x07, 0x64, 0x45, 0xa2, 0x83, 0xe0, 0xc1,
+ 0xe5, 0xc4, 0xa7, 0x86, 0x61, 0x40, 0x23, 0x02, 0xf0, 0xd1,
+ 0xb2, 0x93, 0x74, 0x55, 0x36, 0x17, 0xcf, 0xee, 0x8d, 0xac,
+ 0x4b, 0x6a, 0x09, 0x28, 0xda, 0xfb, 0x98, 0xb9, 0x5e, 0x7f,
+ 0x1c, 0x3d, 0xb1, 0x90, 0xf3, 0xd2, 0x35, 0x14, 0x77, 0x56,
+ 0xa4, 0x85, 0xe6, 0xc7, 0x20, 0x01, 0x62, 0x43, 0x9b, 0xba,
+ 0xd9, 0xf8, 0x1f, 0x3e, 0x5d, 0x7c, 0x8e, 0xaf, 0xcc, 0xed,
+ 0x0a, 0x2b, 0x48, 0x69, 0x00, 0x22, 0x44, 0x66, 0x88, 0xaa,
+ 0xcc, 0xee, 0x0d, 0x2f, 0x49, 0x6b, 0x85, 0xa7, 0xc1, 0xe3,
+ 0x1a, 0x38, 0x5e, 0x7c, 0x92, 0xb0, 0xd6, 0xf4, 0x17, 0x35,
+ 0x53, 0x71, 0x9f, 0xbd, 0xdb, 0xf9, 0x34, 0x16, 0x70, 0x52,
+ 0xbc, 0x9e, 0xf8, 0xda, 0x39, 0x1b, 0x7d, 0x5f, 0xb1, 0x93,
+ 0xf5, 0xd7, 0x2e, 0x0c, 0x6a, 0x48, 0xa6, 0x84, 0xe2, 0xc0,
+ 0x23, 0x01, 0x67, 0x45, 0xab, 0x89, 0xef, 0xcd, 0x68, 0x4a,
+ 0x2c, 0x0e, 0xe0, 0xc2, 0xa4, 0x86, 0x65, 0x47, 0x21, 0x03,
+ 0xed, 0xcf, 0xa9, 0x8b, 0x72, 0x50, 0x36, 0x14, 0xfa, 0xd8,
+ 0xbe, 0x9c, 0x7f, 0x5d, 0x3b, 0x19, 0xf7, 0xd5, 0xb3, 0x91,
+ 0x5c, 0x7e, 0x18, 0x3a, 0xd4, 0xf6, 0x90, 0xb2, 0x51, 0x73,
+ 0x15, 0x37, 0xd9, 0xfb, 0x9d, 0xbf, 0x46, 0x64, 0x02, 0x20,
+ 0xce, 0xec, 0x8a, 0xa8, 0x4b, 0x69, 0x0f, 0x2d, 0xc3, 0xe1,
+ 0x87, 0xa5, 0xd0, 0xf2, 0x94, 0xb6, 0x58, 0x7a, 0x1c, 0x3e,
+ 0xdd, 0xff, 0x99, 0xbb, 0x55, 0x77, 0x11, 0x33, 0xca, 0xe8,
+ 0x8e, 0xac, 0x42, 0x60, 0x06, 0x24, 0xc7, 0xe5, 0x83, 0xa1,
+ 0x4f, 0x6d, 0x0b, 0x29, 0xe4, 0xc6, 0xa0, 0x82, 0x6c, 0x4e,
+ 0x28, 0x0a, 0xe9, 0xcb, 0xad, 0x8f, 0x61, 0x43, 0x25, 0x07,
+ 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0xf3, 0xd1,
+ 0xb7, 0x95, 0x7b, 0x59, 0x3f, 0x1d, 0xb8, 0x9a, 0xfc, 0xde,
+ 0x30, 0x12, 0x74, 0x56, 0xb5, 0x97, 0xf1, 0xd3, 0x3d, 0x1f,
+ 0x79, 0x5b, 0xa2, 0x80, 0xe6, 0xc4, 0x2a, 0x08, 0x6e, 0x4c,
+ 0xaf, 0x8d, 0xeb, 0xc9, 0x27, 0x05, 0x63, 0x41, 0x8c, 0xae,
+ 0xc8, 0xea, 0x04, 0x26, 0x40, 0x62, 0x81, 0xa3, 0xc5, 0xe7,
+ 0x09, 0x2b, 0x4d, 0x6f, 0x96, 0xb4, 0xd2, 0xf0, 0x1e, 0x3c,
+ 0x5a, 0x78, 0x9b, 0xb9, 0xdf, 0xfd, 0x13, 0x31, 0x57, 0x75,
+ 0x00, 0x23, 0x46, 0x65, 0x8c, 0xaf, 0xca, 0xe9, 0x05, 0x26,
+ 0x43, 0x60, 0x89, 0xaa, 0xcf, 0xec, 0x0a, 0x29, 0x4c, 0x6f,
+ 0x86, 0xa5, 0xc0, 0xe3, 0x0f, 0x2c, 0x49, 0x6a, 0x83, 0xa0,
+ 0xc5, 0xe6, 0x14, 0x37, 0x52, 0x71, 0x98, 0xbb, 0xde, 0xfd,
+ 0x11, 0x32, 0x57, 0x74, 0x9d, 0xbe, 0xdb, 0xf8, 0x1e, 0x3d,
+ 0x58, 0x7b, 0x92, 0xb1, 0xd4, 0xf7, 0x1b, 0x38, 0x5d, 0x7e,
+ 0x97, 0xb4, 0xd1, 0xf2, 0x28, 0x0b, 0x6e, 0x4d, 0xa4, 0x87,
+ 0xe2, 0xc1, 0x2d, 0x0e, 0x6b, 0x48, 0xa1, 0x82, 0xe7, 0xc4,
+ 0x22, 0x01, 0x64, 0x47, 0xae, 0x8d, 0xe8, 0xcb, 0x27, 0x04,
+ 0x61, 0x42, 0xab, 0x88, 0xed, 0xce, 0x3c, 0x1f, 0x7a, 0x59,
+ 0xb0, 0x93, 0xf6, 0xd5, 0x39, 0x1a, 0x7f, 0x5c, 0xb5, 0x96,
+ 0xf3, 0xd0, 0x36, 0x15, 0x70, 0x53, 0xba, 0x99, 0xfc, 0xdf,
+ 0x33, 0x10, 0x75, 0x56, 0xbf, 0x9c, 0xf9, 0xda, 0x50, 0x73,
+ 0x16, 0x35, 0xdc, 0xff, 0x9a, 0xb9, 0x55, 0x76, 0x13, 0x30,
+ 0xd9, 0xfa, 0x9f, 0xbc, 0x5a, 0x79, 0x1c, 0x3f, 0xd6, 0xf5,
+ 0x90, 0xb3, 0x5f, 0x7c, 0x19, 0x3a, 0xd3, 0xf0, 0x95, 0xb6,
+ 0x44, 0x67, 0x02, 0x21, 0xc8, 0xeb, 0x8e, 0xad, 0x41, 0x62,
+ 0x07, 0x24, 0xcd, 0xee, 0x8b, 0xa8, 0x4e, 0x6d, 0x08, 0x2b,
+ 0xc2, 0xe1, 0x84, 0xa7, 0x4b, 0x68, 0x0d, 0x2e, 0xc7, 0xe4,
+ 0x81, 0xa2, 0x78, 0x5b, 0x3e, 0x1d, 0xf4, 0xd7, 0xb2, 0x91,
+ 0x7d, 0x5e, 0x3b, 0x18, 0xf1, 0xd2, 0xb7, 0x94, 0x72, 0x51,
+ 0x34, 0x17, 0xfe, 0xdd, 0xb8, 0x9b, 0x77, 0x54, 0x31, 0x12,
+ 0xfb, 0xd8, 0xbd, 0x9e, 0x6c, 0x4f, 0x2a, 0x09, 0xe0, 0xc3,
+ 0xa6, 0x85, 0x69, 0x4a, 0x2f, 0x0c, 0xe5, 0xc6, 0xa3, 0x80,
+ 0x66, 0x45, 0x20, 0x03, 0xea, 0xc9, 0xac, 0x8f, 0x63, 0x40,
+ 0x25, 0x06, 0xef, 0xcc, 0xa9, 0x8a, 0x00, 0x24, 0x48, 0x6c,
+ 0x90, 0xb4, 0xd8, 0xfc, 0x3d, 0x19, 0x75, 0x51, 0xad, 0x89,
+ 0xe5, 0xc1, 0x7a, 0x5e, 0x32, 0x16, 0xea, 0xce, 0xa2, 0x86,
+ 0x47, 0x63, 0x0f, 0x2b, 0xd7, 0xf3, 0x9f, 0xbb, 0xf4, 0xd0,
+ 0xbc, 0x98, 0x64, 0x40, 0x2c, 0x08, 0xc9, 0xed, 0x81, 0xa5,
+ 0x59, 0x7d, 0x11, 0x35, 0x8e, 0xaa, 0xc6, 0xe2, 0x1e, 0x3a,
+ 0x56, 0x72, 0xb3, 0x97, 0xfb, 0xdf, 0x23, 0x07, 0x6b, 0x4f,
+ 0xf5, 0xd1, 0xbd, 0x99, 0x65, 0x41, 0x2d, 0x09, 0xc8, 0xec,
+ 0x80, 0xa4, 0x58, 0x7c, 0x10, 0x34, 0x8f, 0xab, 0xc7, 0xe3,
+ 0x1f, 0x3b, 0x57, 0x73, 0xb2, 0x96, 0xfa, 0xde, 0x22, 0x06,
+ 0x6a, 0x4e, 0x01, 0x25, 0x49, 0x6d, 0x91, 0xb5, 0xd9, 0xfd,
+ 0x3c, 0x18, 0x74, 0x50, 0xac, 0x88, 0xe4, 0xc0, 0x7b, 0x5f,
+ 0x33, 0x17, 0xeb, 0xcf, 0xa3, 0x87, 0x46, 0x62, 0x0e, 0x2a,
+ 0xd6, 0xf2, 0x9e, 0xba, 0xf7, 0xd3, 0xbf, 0x9b, 0x67, 0x43,
+ 0x2f, 0x0b, 0xca, 0xee, 0x82, 0xa6, 0x5a, 0x7e, 0x12, 0x36,
+ 0x8d, 0xa9, 0xc5, 0xe1, 0x1d, 0x39, 0x55, 0x71, 0xb0, 0x94,
+ 0xf8, 0xdc, 0x20, 0x04, 0x68, 0x4c, 0x03, 0x27, 0x4b, 0x6f,
+ 0x93, 0xb7, 0xdb, 0xff, 0x3e, 0x1a, 0x76, 0x52, 0xae, 0x8a,
+ 0xe6, 0xc2, 0x79, 0x5d, 0x31, 0x15, 0xe9, 0xcd, 0xa1, 0x85,
+ 0x44, 0x60, 0x0c, 0x28, 0xd4, 0xf0, 0x9c, 0xb8, 0x02, 0x26,
+ 0x4a, 0x6e, 0x92, 0xb6, 0xda, 0xfe, 0x3f, 0x1b, 0x77, 0x53,
+ 0xaf, 0x8b, 0xe7, 0xc3, 0x78, 0x5c, 0x30, 0x14, 0xe8, 0xcc,
+ 0xa0, 0x84, 0x45, 0x61, 0x0d, 0x29, 0xd5, 0xf1, 0x9d, 0xb9,
+ 0xf6, 0xd2, 0xbe, 0x9a, 0x66, 0x42, 0x2e, 0x0a, 0xcb, 0xef,
+ 0x83, 0xa7, 0x5b, 0x7f, 0x13, 0x37, 0x8c, 0xa8, 0xc4, 0xe0,
+ 0x1c, 0x38, 0x54, 0x70, 0xb1, 0x95, 0xf9, 0xdd, 0x21, 0x05,
+ 0x69, 0x4d, 0x00, 0x25, 0x4a, 0x6f, 0x94, 0xb1, 0xde, 0xfb,
+ 0x35, 0x10, 0x7f, 0x5a, 0xa1, 0x84, 0xeb, 0xce, 0x6a, 0x4f,
+ 0x20, 0x05, 0xfe, 0xdb, 0xb4, 0x91, 0x5f, 0x7a, 0x15, 0x30,
+ 0xcb, 0xee, 0x81, 0xa4, 0xd4, 0xf1, 0x9e, 0xbb, 0x40, 0x65,
+ 0x0a, 0x2f, 0xe1, 0xc4, 0xab, 0x8e, 0x75, 0x50, 0x3f, 0x1a,
+ 0xbe, 0x9b, 0xf4, 0xd1, 0x2a, 0x0f, 0x60, 0x45, 0x8b, 0xae,
+ 0xc1, 0xe4, 0x1f, 0x3a, 0x55, 0x70, 0xb5, 0x90, 0xff, 0xda,
+ 0x21, 0x04, 0x6b, 0x4e, 0x80, 0xa5, 0xca, 0xef, 0x14, 0x31,
+ 0x5e, 0x7b, 0xdf, 0xfa, 0x95, 0xb0, 0x4b, 0x6e, 0x01, 0x24,
+ 0xea, 0xcf, 0xa0, 0x85, 0x7e, 0x5b, 0x34, 0x11, 0x61, 0x44,
+ 0x2b, 0x0e, 0xf5, 0xd0, 0xbf, 0x9a, 0x54, 0x71, 0x1e, 0x3b,
+ 0xc0, 0xe5, 0x8a, 0xaf, 0x0b, 0x2e, 0x41, 0x64, 0x9f, 0xba,
+ 0xd5, 0xf0, 0x3e, 0x1b, 0x74, 0x51, 0xaa, 0x8f, 0xe0, 0xc5,
+ 0x77, 0x52, 0x3d, 0x18, 0xe3, 0xc6, 0xa9, 0x8c, 0x42, 0x67,
+ 0x08, 0x2d, 0xd6, 0xf3, 0x9c, 0xb9, 0x1d, 0x38, 0x57, 0x72,
+ 0x89, 0xac, 0xc3, 0xe6, 0x28, 0x0d, 0x62, 0x47, 0xbc, 0x99,
+ 0xf6, 0xd3, 0xa3, 0x86, 0xe9, 0xcc, 0x37, 0x12, 0x7d, 0x58,
+ 0x96, 0xb3, 0xdc, 0xf9, 0x02, 0x27, 0x48, 0x6d, 0xc9, 0xec,
+ 0x83, 0xa6, 0x5d, 0x78, 0x17, 0x32, 0xfc, 0xd9, 0xb6, 0x93,
+ 0x68, 0x4d, 0x22, 0x07, 0xc2, 0xe7, 0x88, 0xad, 0x56, 0x73,
+ 0x1c, 0x39, 0xf7, 0xd2, 0xbd, 0x98, 0x63, 0x46, 0x29, 0x0c,
+ 0xa8, 0x8d, 0xe2, 0xc7, 0x3c, 0x19, 0x76, 0x53, 0x9d, 0xb8,
+ 0xd7, 0xf2, 0x09, 0x2c, 0x43, 0x66, 0x16, 0x33, 0x5c, 0x79,
+ 0x82, 0xa7, 0xc8, 0xed, 0x23, 0x06, 0x69, 0x4c, 0xb7, 0x92,
+ 0xfd, 0xd8, 0x7c, 0x59, 0x36, 0x13, 0xe8, 0xcd, 0xa2, 0x87,
+ 0x49, 0x6c, 0x03, 0x26, 0xdd, 0xf8, 0x97, 0xb2, 0x00, 0x26,
+ 0x4c, 0x6a, 0x98, 0xbe, 0xd4, 0xf2, 0x2d, 0x0b, 0x61, 0x47,
+ 0xb5, 0x93, 0xf9, 0xdf, 0x5a, 0x7c, 0x16, 0x30, 0xc2, 0xe4,
+ 0x8e, 0xa8, 0x77, 0x51, 0x3b, 0x1d, 0xef, 0xc9, 0xa3, 0x85,
+ 0xb4, 0x92, 0xf8, 0xde, 0x2c, 0x0a, 0x60, 0x46, 0x99, 0xbf,
+ 0xd5, 0xf3, 0x01, 0x27, 0x4d, 0x6b, 0xee, 0xc8, 0xa2, 0x84,
+ 0x76, 0x50, 0x3a, 0x1c, 0xc3, 0xe5, 0x8f, 0xa9, 0x5b, 0x7d,
+ 0x17, 0x31, 0x75, 0x53, 0x39, 0x1f, 0xed, 0xcb, 0xa1, 0x87,
+ 0x58, 0x7e, 0x14, 0x32, 0xc0, 0xe6, 0x8c, 0xaa, 0x2f, 0x09,
+ 0x63, 0x45, 0xb7, 0x91, 0xfb, 0xdd, 0x02, 0x24, 0x4e, 0x68,
+ 0x9a, 0xbc, 0xd6, 0xf0, 0xc1, 0xe7, 0x8d, 0xab, 0x59, 0x7f,
+ 0x15, 0x33, 0xec, 0xca, 0xa0, 0x86, 0x74, 0x52, 0x38, 0x1e,
+ 0x9b, 0xbd, 0xd7, 0xf1, 0x03, 0x25, 0x4f, 0x69, 0xb6, 0x90,
+ 0xfa, 0xdc, 0x2e, 0x08, 0x62, 0x44, 0xea, 0xcc, 0xa6, 0x80,
+ 0x72, 0x54, 0x3e, 0x18, 0xc7, 0xe1, 0x8b, 0xad, 0x5f, 0x79,
+ 0x13, 0x35, 0xb0, 0x96, 0xfc, 0xda, 0x28, 0x0e, 0x64, 0x42,
+ 0x9d, 0xbb, 0xd1, 0xf7, 0x05, 0x23, 0x49, 0x6f, 0x5e, 0x78,
+ 0x12, 0x34, 0xc6, 0xe0, 0x8a, 0xac, 0x73, 0x55, 0x3f, 0x19,
+ 0xeb, 0xcd, 0xa7, 0x81, 0x04, 0x22, 0x48, 0x6e, 0x9c, 0xba,
+ 0xd0, 0xf6, 0x29, 0x0f, 0x65, 0x43, 0xb1, 0x97, 0xfd, 0xdb,
+ 0x9f, 0xb9, 0xd3, 0xf5, 0x07, 0x21, 0x4b, 0x6d, 0xb2, 0x94,
+ 0xfe, 0xd8, 0x2a, 0x0c, 0x66, 0x40, 0xc5, 0xe3, 0x89, 0xaf,
+ 0x5d, 0x7b, 0x11, 0x37, 0xe8, 0xce, 0xa4, 0x82, 0x70, 0x56,
+ 0x3c, 0x1a, 0x2b, 0x0d, 0x67, 0x41, 0xb3, 0x95, 0xff, 0xd9,
+ 0x06, 0x20, 0x4a, 0x6c, 0x9e, 0xb8, 0xd2, 0xf4, 0x71, 0x57,
+ 0x3d, 0x1b, 0xe9, 0xcf, 0xa5, 0x83, 0x5c, 0x7a, 0x10, 0x36,
+ 0xc4, 0xe2, 0x88, 0xae, 0x00, 0x27, 0x4e, 0x69, 0x9c, 0xbb,
+ 0xd2, 0xf5, 0x25, 0x02, 0x6b, 0x4c, 0xb9, 0x9e, 0xf7, 0xd0,
+ 0x4a, 0x6d, 0x04, 0x23, 0xd6, 0xf1, 0x98, 0xbf, 0x6f, 0x48,
+ 0x21, 0x06, 0xf3, 0xd4, 0xbd, 0x9a, 0x94, 0xb3, 0xda, 0xfd,
+ 0x08, 0x2f, 0x46, 0x61, 0xb1, 0x96, 0xff, 0xd8, 0x2d, 0x0a,
+ 0x63, 0x44, 0xde, 0xf9, 0x90, 0xb7, 0x42, 0x65, 0x0c, 0x2b,
+ 0xfb, 0xdc, 0xb5, 0x92, 0x67, 0x40, 0x29, 0x0e, 0x35, 0x12,
+ 0x7b, 0x5c, 0xa9, 0x8e, 0xe7, 0xc0, 0x10, 0x37, 0x5e, 0x79,
+ 0x8c, 0xab, 0xc2, 0xe5, 0x7f, 0x58, 0x31, 0x16, 0xe3, 0xc4,
+ 0xad, 0x8a, 0x5a, 0x7d, 0x14, 0x33, 0xc6, 0xe1, 0x88, 0xaf,
+ 0xa1, 0x86, 0xef, 0xc8, 0x3d, 0x1a, 0x73, 0x54, 0x84, 0xa3,
+ 0xca, 0xed, 0x18, 0x3f, 0x56, 0x71, 0xeb, 0xcc, 0xa5, 0x82,
+ 0x77, 0x50, 0x39, 0x1e, 0xce, 0xe9, 0x80, 0xa7, 0x52, 0x75,
+ 0x1c, 0x3b, 0x6a, 0x4d, 0x24, 0x03, 0xf6, 0xd1, 0xb8, 0x9f,
+ 0x4f, 0x68, 0x01, 0x26, 0xd3, 0xf4, 0x9d, 0xba, 0x20, 0x07,
+ 0x6e, 0x49, 0xbc, 0x9b, 0xf2, 0xd5, 0x05, 0x22, 0x4b, 0x6c,
+ 0x99, 0xbe, 0xd7, 0xf0, 0xfe, 0xd9, 0xb0, 0x97, 0x62, 0x45,
+ 0x2c, 0x0b, 0xdb, 0xfc, 0x95, 0xb2, 0x47, 0x60, 0x09, 0x2e,
+ 0xb4, 0x93, 0xfa, 0xdd, 0x28, 0x0f, 0x66, 0x41, 0x91, 0xb6,
+ 0xdf, 0xf8, 0x0d, 0x2a, 0x43, 0x64, 0x5f, 0x78, 0x11, 0x36,
+ 0xc3, 0xe4, 0x8d, 0xaa, 0x7a, 0x5d, 0x34, 0x13, 0xe6, 0xc1,
+ 0xa8, 0x8f, 0x15, 0x32, 0x5b, 0x7c, 0x89, 0xae, 0xc7, 0xe0,
+ 0x30, 0x17, 0x7e, 0x59, 0xac, 0x8b, 0xe2, 0xc5, 0xcb, 0xec,
+ 0x85, 0xa2, 0x57, 0x70, 0x19, 0x3e, 0xee, 0xc9, 0xa0, 0x87,
+ 0x72, 0x55, 0x3c, 0x1b, 0x81, 0xa6, 0xcf, 0xe8, 0x1d, 0x3a,
+ 0x53, 0x74, 0xa4, 0x83, 0xea, 0xcd, 0x38, 0x1f, 0x76, 0x51,
+ 0x00, 0x28, 0x50, 0x78, 0xa0, 0x88, 0xf0, 0xd8, 0x5d, 0x75,
+ 0x0d, 0x25, 0xfd, 0xd5, 0xad, 0x85, 0xba, 0x92, 0xea, 0xc2,
+ 0x1a, 0x32, 0x4a, 0x62, 0xe7, 0xcf, 0xb7, 0x9f, 0x47, 0x6f,
+ 0x17, 0x3f, 0x69, 0x41, 0x39, 0x11, 0xc9, 0xe1, 0x99, 0xb1,
+ 0x34, 0x1c, 0x64, 0x4c, 0x94, 0xbc, 0xc4, 0xec, 0xd3, 0xfb,
+ 0x83, 0xab, 0x73, 0x5b, 0x23, 0x0b, 0x8e, 0xa6, 0xde, 0xf6,
+ 0x2e, 0x06, 0x7e, 0x56, 0xd2, 0xfa, 0x82, 0xaa, 0x72, 0x5a,
+ 0x22, 0x0a, 0x8f, 0xa7, 0xdf, 0xf7, 0x2f, 0x07, 0x7f, 0x57,
+ 0x68, 0x40, 0x38, 0x10, 0xc8, 0xe0, 0x98, 0xb0, 0x35, 0x1d,
+ 0x65, 0x4d, 0x95, 0xbd, 0xc5, 0xed, 0xbb, 0x93, 0xeb, 0xc3,
+ 0x1b, 0x33, 0x4b, 0x63, 0xe6, 0xce, 0xb6, 0x9e, 0x46, 0x6e,
+ 0x16, 0x3e, 0x01, 0x29, 0x51, 0x79, 0xa1, 0x89, 0xf1, 0xd9,
+ 0x5c, 0x74, 0x0c, 0x24, 0xfc, 0xd4, 0xac, 0x84, 0xb9, 0x91,
+ 0xe9, 0xc1, 0x19, 0x31, 0x49, 0x61, 0xe4, 0xcc, 0xb4, 0x9c,
+ 0x44, 0x6c, 0x14, 0x3c, 0x03, 0x2b, 0x53, 0x7b, 0xa3, 0x8b,
+ 0xf3, 0xdb, 0x5e, 0x76, 0x0e, 0x26, 0xfe, 0xd6, 0xae, 0x86,
+ 0xd0, 0xf8, 0x80, 0xa8, 0x70, 0x58, 0x20, 0x08, 0x8d, 0xa5,
+ 0xdd, 0xf5, 0x2d, 0x05, 0x7d, 0x55, 0x6a, 0x42, 0x3a, 0x12,
+ 0xca, 0xe2, 0x9a, 0xb2, 0x37, 0x1f, 0x67, 0x4f, 0x97, 0xbf,
+ 0xc7, 0xef, 0x6b, 0x43, 0x3b, 0x13, 0xcb, 0xe3, 0x9b, 0xb3,
+ 0x36, 0x1e, 0x66, 0x4e, 0x96, 0xbe, 0xc6, 0xee, 0xd1, 0xf9,
+ 0x81, 0xa9, 0x71, 0x59, 0x21, 0x09, 0x8c, 0xa4, 0xdc, 0xf4,
+ 0x2c, 0x04, 0x7c, 0x54, 0x02, 0x2a, 0x52, 0x7a, 0xa2, 0x8a,
+ 0xf2, 0xda, 0x5f, 0x77, 0x0f, 0x27, 0xff, 0xd7, 0xaf, 0x87,
+ 0xb8, 0x90, 0xe8, 0xc0, 0x18, 0x30, 0x48, 0x60, 0xe5, 0xcd,
+ 0xb5, 0x9d, 0x45, 0x6d, 0x15, 0x3d, 0x00, 0x29, 0x52, 0x7b,
+ 0xa4, 0x8d, 0xf6, 0xdf, 0x55, 0x7c, 0x07, 0x2e, 0xf1, 0xd8,
+ 0xa3, 0x8a, 0xaa, 0x83, 0xf8, 0xd1, 0x0e, 0x27, 0x5c, 0x75,
+ 0xff, 0xd6, 0xad, 0x84, 0x5b, 0x72, 0x09, 0x20, 0x49, 0x60,
+ 0x1b, 0x32, 0xed, 0xc4, 0xbf, 0x96, 0x1c, 0x35, 0x4e, 0x67,
+ 0xb8, 0x91, 0xea, 0xc3, 0xe3, 0xca, 0xb1, 0x98, 0x47, 0x6e,
+ 0x15, 0x3c, 0xb6, 0x9f, 0xe4, 0xcd, 0x12, 0x3b, 0x40, 0x69,
+ 0x92, 0xbb, 0xc0, 0xe9, 0x36, 0x1f, 0x64, 0x4d, 0xc7, 0xee,
+ 0x95, 0xbc, 0x63, 0x4a, 0x31, 0x18, 0x38, 0x11, 0x6a, 0x43,
+ 0x9c, 0xb5, 0xce, 0xe7, 0x6d, 0x44, 0x3f, 0x16, 0xc9, 0xe0,
+ 0x9b, 0xb2, 0xdb, 0xf2, 0x89, 0xa0, 0x7f, 0x56, 0x2d, 0x04,
+ 0x8e, 0xa7, 0xdc, 0xf5, 0x2a, 0x03, 0x78, 0x51, 0x71, 0x58,
+ 0x23, 0x0a, 0xd5, 0xfc, 0x87, 0xae, 0x24, 0x0d, 0x76, 0x5f,
+ 0x80, 0xa9, 0xd2, 0xfb, 0x39, 0x10, 0x6b, 0x42, 0x9d, 0xb4,
+ 0xcf, 0xe6, 0x6c, 0x45, 0x3e, 0x17, 0xc8, 0xe1, 0x9a, 0xb3,
+ 0x93, 0xba, 0xc1, 0xe8, 0x37, 0x1e, 0x65, 0x4c, 0xc6, 0xef,
+ 0x94, 0xbd, 0x62, 0x4b, 0x30, 0x19, 0x70, 0x59, 0x22, 0x0b,
+ 0xd4, 0xfd, 0x86, 0xaf, 0x25, 0x0c, 0x77, 0x5e, 0x81, 0xa8,
+ 0xd3, 0xfa, 0xda, 0xf3, 0x88, 0xa1, 0x7e, 0x57, 0x2c, 0x05,
+ 0x8f, 0xa6, 0xdd, 0xf4, 0x2b, 0x02, 0x79, 0x50, 0xab, 0x82,
+ 0xf9, 0xd0, 0x0f, 0x26, 0x5d, 0x74, 0xfe, 0xd7, 0xac, 0x85,
+ 0x5a, 0x73, 0x08, 0x21, 0x01, 0x28, 0x53, 0x7a, 0xa5, 0x8c,
+ 0xf7, 0xde, 0x54, 0x7d, 0x06, 0x2f, 0xf0, 0xd9, 0xa2, 0x8b,
+ 0xe2, 0xcb, 0xb0, 0x99, 0x46, 0x6f, 0x14, 0x3d, 0xb7, 0x9e,
+ 0xe5, 0xcc, 0x13, 0x3a, 0x41, 0x68, 0x48, 0x61, 0x1a, 0x33,
+ 0xec, 0xc5, 0xbe, 0x97, 0x1d, 0x34, 0x4f, 0x66, 0xb9, 0x90,
+ 0xeb, 0xc2, 0x00, 0x2a, 0x54, 0x7e, 0xa8, 0x82, 0xfc, 0xd6,
+ 0x4d, 0x67, 0x19, 0x33, 0xe5, 0xcf, 0xb1, 0x9b, 0x9a, 0xb0,
+ 0xce, 0xe4, 0x32, 0x18, 0x66, 0x4c, 0xd7, 0xfd, 0x83, 0xa9,
+ 0x7f, 0x55, 0x2b, 0x01, 0x29, 0x03, 0x7d, 0x57, 0x81, 0xab,
+ 0xd5, 0xff, 0x64, 0x4e, 0x30, 0x1a, 0xcc, 0xe6, 0x98, 0xb2,
+ 0xb3, 0x99, 0xe7, 0xcd, 0x1b, 0x31, 0x4f, 0x65, 0xfe, 0xd4,
+ 0xaa, 0x80, 0x56, 0x7c, 0x02, 0x28, 0x52, 0x78, 0x06, 0x2c,
+ 0xfa, 0xd0, 0xae, 0x84, 0x1f, 0x35, 0x4b, 0x61, 0xb7, 0x9d,
+ 0xe3, 0xc9, 0xc8, 0xe2, 0x9c, 0xb6, 0x60, 0x4a, 0x34, 0x1e,
+ 0x85, 0xaf, 0xd1, 0xfb, 0x2d, 0x07, 0x79, 0x53, 0x7b, 0x51,
+ 0x2f, 0x05, 0xd3, 0xf9, 0x87, 0xad, 0x36, 0x1c, 0x62, 0x48,
+ 0x9e, 0xb4, 0xca, 0xe0, 0xe1, 0xcb, 0xb5, 0x9f, 0x49, 0x63,
+ 0x1d, 0x37, 0xac, 0x86, 0xf8, 0xd2, 0x04, 0x2e, 0x50, 0x7a,
+ 0xa4, 0x8e, 0xf0, 0xda, 0x0c, 0x26, 0x58, 0x72, 0xe9, 0xc3,
+ 0xbd, 0x97, 0x41, 0x6b, 0x15, 0x3f, 0x3e, 0x14, 0x6a, 0x40,
+ 0x96, 0xbc, 0xc2, 0xe8, 0x73, 0x59, 0x27, 0x0d, 0xdb, 0xf1,
+ 0x8f, 0xa5, 0x8d, 0xa7, 0xd9, 0xf3, 0x25, 0x0f, 0x71, 0x5b,
+ 0xc0, 0xea, 0x94, 0xbe, 0x68, 0x42, 0x3c, 0x16, 0x17, 0x3d,
+ 0x43, 0x69, 0xbf, 0x95, 0xeb, 0xc1, 0x5a, 0x70, 0x0e, 0x24,
+ 0xf2, 0xd8, 0xa6, 0x8c, 0xf6, 0xdc, 0xa2, 0x88, 0x5e, 0x74,
+ 0x0a, 0x20, 0xbb, 0x91, 0xef, 0xc5, 0x13, 0x39, 0x47, 0x6d,
+ 0x6c, 0x46, 0x38, 0x12, 0xc4, 0xee, 0x90, 0xba, 0x21, 0x0b,
+ 0x75, 0x5f, 0x89, 0xa3, 0xdd, 0xf7, 0xdf, 0xf5, 0x8b, 0xa1,
+ 0x77, 0x5d, 0x23, 0x09, 0x92, 0xb8, 0xc6, 0xec, 0x3a, 0x10,
+ 0x6e, 0x44, 0x45, 0x6f, 0x11, 0x3b, 0xed, 0xc7, 0xb9, 0x93,
+ 0x08, 0x22, 0x5c, 0x76, 0xa0, 0x8a, 0xf4, 0xde, 0x00, 0x2b,
+ 0x56, 0x7d, 0xac, 0x87, 0xfa, 0xd1, 0x45, 0x6e, 0x13, 0x38,
+ 0xe9, 0xc2, 0xbf, 0x94, 0x8a, 0xa1, 0xdc, 0xf7, 0x26, 0x0d,
+ 0x70, 0x5b, 0xcf, 0xe4, 0x99, 0xb2, 0x63, 0x48, 0x35, 0x1e,
+ 0x09, 0x22, 0x5f, 0x74, 0xa5, 0x8e, 0xf3, 0xd8, 0x4c, 0x67,
+ 0x1a, 0x31, 0xe0, 0xcb, 0xb6, 0x9d, 0x83, 0xa8, 0xd5, 0xfe,
+ 0x2f, 0x04, 0x79, 0x52, 0xc6, 0xed, 0x90, 0xbb, 0x6a, 0x41,
+ 0x3c, 0x17, 0x12, 0x39, 0x44, 0x6f, 0xbe, 0x95, 0xe8, 0xc3,
+ 0x57, 0x7c, 0x01, 0x2a, 0xfb, 0xd0, 0xad, 0x86, 0x98, 0xb3,
+ 0xce, 0xe5, 0x34, 0x1f, 0x62, 0x49, 0xdd, 0xf6, 0x8b, 0xa0,
+ 0x71, 0x5a, 0x27, 0x0c, 0x1b, 0x30, 0x4d, 0x66, 0xb7, 0x9c,
+ 0xe1, 0xca, 0x5e, 0x75, 0x08, 0x23, 0xf2, 0xd9, 0xa4, 0x8f,
+ 0x91, 0xba, 0xc7, 0xec, 0x3d, 0x16, 0x6b, 0x40, 0xd4, 0xff,
+ 0x82, 0xa9, 0x78, 0x53, 0x2e, 0x05, 0x24, 0x0f, 0x72, 0x59,
+ 0x88, 0xa3, 0xde, 0xf5, 0x61, 0x4a, 0x37, 0x1c, 0xcd, 0xe6,
+ 0x9b, 0xb0, 0xae, 0x85, 0xf8, 0xd3, 0x02, 0x29, 0x54, 0x7f,
+ 0xeb, 0xc0, 0xbd, 0x96, 0x47, 0x6c, 0x11, 0x3a, 0x2d, 0x06,
+ 0x7b, 0x50, 0x81, 0xaa, 0xd7, 0xfc, 0x68, 0x43, 0x3e, 0x15,
+ 0xc4, 0xef, 0x92, 0xb9, 0xa7, 0x8c, 0xf1, 0xda, 0x0b, 0x20,
+ 0x5d, 0x76, 0xe2, 0xc9, 0xb4, 0x9f, 0x4e, 0x65, 0x18, 0x33,
+ 0x36, 0x1d, 0x60, 0x4b, 0x9a, 0xb1, 0xcc, 0xe7, 0x73, 0x58,
+ 0x25, 0x0e, 0xdf, 0xf4, 0x89, 0xa2, 0xbc, 0x97, 0xea, 0xc1,
+ 0x10, 0x3b, 0x46, 0x6d, 0xf9, 0xd2, 0xaf, 0x84, 0x55, 0x7e,
+ 0x03, 0x28, 0x3f, 0x14, 0x69, 0x42, 0x93, 0xb8, 0xc5, 0xee,
+ 0x7a, 0x51, 0x2c, 0x07, 0xd6, 0xfd, 0x80, 0xab, 0xb5, 0x9e,
+ 0xe3, 0xc8, 0x19, 0x32, 0x4f, 0x64, 0xf0, 0xdb, 0xa6, 0x8d,
+ 0x5c, 0x77, 0x0a, 0x21, 0x00, 0x2c, 0x58, 0x74, 0xb0, 0x9c,
+ 0xe8, 0xc4, 0x7d, 0x51, 0x25, 0x09, 0xcd, 0xe1, 0x95, 0xb9,
+ 0xfa, 0xd6, 0xa2, 0x8e, 0x4a, 0x66, 0x12, 0x3e, 0x87, 0xab,
+ 0xdf, 0xf3, 0x37, 0x1b, 0x6f, 0x43, 0xe9, 0xc5, 0xb1, 0x9d,
+ 0x59, 0x75, 0x01, 0x2d, 0x94, 0xb8, 0xcc, 0xe0, 0x24, 0x08,
+ 0x7c, 0x50, 0x13, 0x3f, 0x4b, 0x67, 0xa3, 0x8f, 0xfb, 0xd7,
+ 0x6e, 0x42, 0x36, 0x1a, 0xde, 0xf2, 0x86, 0xaa, 0xcf, 0xe3,
+ 0x97, 0xbb, 0x7f, 0x53, 0x27, 0x0b, 0xb2, 0x9e, 0xea, 0xc6,
+ 0x02, 0x2e, 0x5a, 0x76, 0x35, 0x19, 0x6d, 0x41, 0x85, 0xa9,
+ 0xdd, 0xf1, 0x48, 0x64, 0x10, 0x3c, 0xf8, 0xd4, 0xa0, 0x8c,
+ 0x26, 0x0a, 0x7e, 0x52, 0x96, 0xba, 0xce, 0xe2, 0x5b, 0x77,
+ 0x03, 0x2f, 0xeb, 0xc7, 0xb3, 0x9f, 0xdc, 0xf0, 0x84, 0xa8,
+ 0x6c, 0x40, 0x34, 0x18, 0xa1, 0x8d, 0xf9, 0xd5, 0x11, 0x3d,
+ 0x49, 0x65, 0x83, 0xaf, 0xdb, 0xf7, 0x33, 0x1f, 0x6b, 0x47,
+ 0xfe, 0xd2, 0xa6, 0x8a, 0x4e, 0x62, 0x16, 0x3a, 0x79, 0x55,
+ 0x21, 0x0d, 0xc9, 0xe5, 0x91, 0xbd, 0x04, 0x28, 0x5c, 0x70,
+ 0xb4, 0x98, 0xec, 0xc0, 0x6a, 0x46, 0x32, 0x1e, 0xda, 0xf6,
+ 0x82, 0xae, 0x17, 0x3b, 0x4f, 0x63, 0xa7, 0x8b, 0xff, 0xd3,
+ 0x90, 0xbc, 0xc8, 0xe4, 0x20, 0x0c, 0x78, 0x54, 0xed, 0xc1,
+ 0xb5, 0x99, 0x5d, 0x71, 0x05, 0x29, 0x4c, 0x60, 0x14, 0x38,
+ 0xfc, 0xd0, 0xa4, 0x88, 0x31, 0x1d, 0x69, 0x45, 0x81, 0xad,
+ 0xd9, 0xf5, 0xb6, 0x9a, 0xee, 0xc2, 0x06, 0x2a, 0x5e, 0x72,
+ 0xcb, 0xe7, 0x93, 0xbf, 0x7b, 0x57, 0x23, 0x0f, 0xa5, 0x89,
+ 0xfd, 0xd1, 0x15, 0x39, 0x4d, 0x61, 0xd8, 0xf4, 0x80, 0xac,
+ 0x68, 0x44, 0x30, 0x1c, 0x5f, 0x73, 0x07, 0x2b, 0xef, 0xc3,
+ 0xb7, 0x9b, 0x22, 0x0e, 0x7a, 0x56, 0x92, 0xbe, 0xca, 0xe6,
+ 0x00, 0x2d, 0x5a, 0x77, 0xb4, 0x99, 0xee, 0xc3, 0x75, 0x58,
+ 0x2f, 0x02, 0xc1, 0xec, 0x9b, 0xb6, 0xea, 0xc7, 0xb0, 0x9d,
+ 0x5e, 0x73, 0x04, 0x29, 0x9f, 0xb2, 0xc5, 0xe8, 0x2b, 0x06,
+ 0x71, 0x5c, 0xc9, 0xe4, 0x93, 0xbe, 0x7d, 0x50, 0x27, 0x0a,
+ 0xbc, 0x91, 0xe6, 0xcb, 0x08, 0x25, 0x52, 0x7f, 0x23, 0x0e,
+ 0x79, 0x54, 0x97, 0xba, 0xcd, 0xe0, 0x56, 0x7b, 0x0c, 0x21,
+ 0xe2, 0xcf, 0xb8, 0x95, 0x8f, 0xa2, 0xd5, 0xf8, 0x3b, 0x16,
+ 0x61, 0x4c, 0xfa, 0xd7, 0xa0, 0x8d, 0x4e, 0x63, 0x14, 0x39,
+ 0x65, 0x48, 0x3f, 0x12, 0xd1, 0xfc, 0x8b, 0xa6, 0x10, 0x3d,
+ 0x4a, 0x67, 0xa4, 0x89, 0xfe, 0xd3, 0x46, 0x6b, 0x1c, 0x31,
+ 0xf2, 0xdf, 0xa8, 0x85, 0x33, 0x1e, 0x69, 0x44, 0x87, 0xaa,
+ 0xdd, 0xf0, 0xac, 0x81, 0xf6, 0xdb, 0x18, 0x35, 0x42, 0x6f,
+ 0xd9, 0xf4, 0x83, 0xae, 0x6d, 0x40, 0x37, 0x1a, 0x03, 0x2e,
+ 0x59, 0x74, 0xb7, 0x9a, 0xed, 0xc0, 0x76, 0x5b, 0x2c, 0x01,
+ 0xc2, 0xef, 0x98, 0xb5, 0xe9, 0xc4, 0xb3, 0x9e, 0x5d, 0x70,
+ 0x07, 0x2a, 0x9c, 0xb1, 0xc6, 0xeb, 0x28, 0x05, 0x72, 0x5f,
+ 0xca, 0xe7, 0x90, 0xbd, 0x7e, 0x53, 0x24, 0x09, 0xbf, 0x92,
+ 0xe5, 0xc8, 0x0b, 0x26, 0x51, 0x7c, 0x20, 0x0d, 0x7a, 0x57,
+ 0x94, 0xb9, 0xce, 0xe3, 0x55, 0x78, 0x0f, 0x22, 0xe1, 0xcc,
+ 0xbb, 0x96, 0x8c, 0xa1, 0xd6, 0xfb, 0x38, 0x15, 0x62, 0x4f,
+ 0xf9, 0xd4, 0xa3, 0x8e, 0x4d, 0x60, 0x17, 0x3a, 0x66, 0x4b,
+ 0x3c, 0x11, 0xd2, 0xff, 0x88, 0xa5, 0x13, 0x3e, 0x49, 0x64,
+ 0xa7, 0x8a, 0xfd, 0xd0, 0x45, 0x68, 0x1f, 0x32, 0xf1, 0xdc,
+ 0xab, 0x86, 0x30, 0x1d, 0x6a, 0x47, 0x84, 0xa9, 0xde, 0xf3,
+ 0xaf, 0x82, 0xf5, 0xd8, 0x1b, 0x36, 0x41, 0x6c, 0xda, 0xf7,
+ 0x80, 0xad, 0x6e, 0x43, 0x34, 0x19, 0x00, 0x2e, 0x5c, 0x72,
+ 0xb8, 0x96, 0xe4, 0xca, 0x6d, 0x43, 0x31, 0x1f, 0xd5, 0xfb,
+ 0x89, 0xa7, 0xda, 0xf4, 0x86, 0xa8, 0x62, 0x4c, 0x3e, 0x10,
+ 0xb7, 0x99, 0xeb, 0xc5, 0x0f, 0x21, 0x53, 0x7d, 0xa9, 0x87,
+ 0xf5, 0xdb, 0x11, 0x3f, 0x4d, 0x63, 0xc4, 0xea, 0x98, 0xb6,
+ 0x7c, 0x52, 0x20, 0x0e, 0x73, 0x5d, 0x2f, 0x01, 0xcb, 0xe5,
+ 0x97, 0xb9, 0x1e, 0x30, 0x42, 0x6c, 0xa6, 0x88, 0xfa, 0xd4,
+ 0x4f, 0x61, 0x13, 0x3d, 0xf7, 0xd9, 0xab, 0x85, 0x22, 0x0c,
+ 0x7e, 0x50, 0x9a, 0xb4, 0xc6, 0xe8, 0x95, 0xbb, 0xc9, 0xe7,
+ 0x2d, 0x03, 0x71, 0x5f, 0xf8, 0xd6, 0xa4, 0x8a, 0x40, 0x6e,
+ 0x1c, 0x32, 0xe6, 0xc8, 0xba, 0x94, 0x5e, 0x70, 0x02, 0x2c,
+ 0x8b, 0xa5, 0xd7, 0xf9, 0x33, 0x1d, 0x6f, 0x41, 0x3c, 0x12,
+ 0x60, 0x4e, 0x84, 0xaa, 0xd8, 0xf6, 0x51, 0x7f, 0x0d, 0x23,
+ 0xe9, 0xc7, 0xb5, 0x9b, 0x9e, 0xb0, 0xc2, 0xec, 0x26, 0x08,
+ 0x7a, 0x54, 0xf3, 0xdd, 0xaf, 0x81, 0x4b, 0x65, 0x17, 0x39,
+ 0x44, 0x6a, 0x18, 0x36, 0xfc, 0xd2, 0xa0, 0x8e, 0x29, 0x07,
+ 0x75, 0x5b, 0x91, 0xbf, 0xcd, 0xe3, 0x37, 0x19, 0x6b, 0x45,
+ 0x8f, 0xa1, 0xd3, 0xfd, 0x5a, 0x74, 0x06, 0x28, 0xe2, 0xcc,
+ 0xbe, 0x90, 0xed, 0xc3, 0xb1, 0x9f, 0x55, 0x7b, 0x09, 0x27,
+ 0x80, 0xae, 0xdc, 0xf2, 0x38, 0x16, 0x64, 0x4a, 0xd1, 0xff,
+ 0x8d, 0xa3, 0x69, 0x47, 0x35, 0x1b, 0xbc, 0x92, 0xe0, 0xce,
+ 0x04, 0x2a, 0x58, 0x76, 0x0b, 0x25, 0x57, 0x79, 0xb3, 0x9d,
+ 0xef, 0xc1, 0x66, 0x48, 0x3a, 0x14, 0xde, 0xf0, 0x82, 0xac,
+ 0x78, 0x56, 0x24, 0x0a, 0xc0, 0xee, 0x9c, 0xb2, 0x15, 0x3b,
+ 0x49, 0x67, 0xad, 0x83, 0xf1, 0xdf, 0xa2, 0x8c, 0xfe, 0xd0,
+ 0x1a, 0x34, 0x46, 0x68, 0xcf, 0xe1, 0x93, 0xbd, 0x77, 0x59,
+ 0x2b, 0x05, 0x00, 0x2f, 0x5e, 0x71, 0xbc, 0x93, 0xe2, 0xcd,
+ 0x65, 0x4a, 0x3b, 0x14, 0xd9, 0xf6, 0x87, 0xa8, 0xca, 0xe5,
+ 0x94, 0xbb, 0x76, 0x59, 0x28, 0x07, 0xaf, 0x80, 0xf1, 0xde,
+ 0x13, 0x3c, 0x4d, 0x62, 0x89, 0xa6, 0xd7, 0xf8, 0x35, 0x1a,
+ 0x6b, 0x44, 0xec, 0xc3, 0xb2, 0x9d, 0x50, 0x7f, 0x0e, 0x21,
+ 0x43, 0x6c, 0x1d, 0x32, 0xff, 0xd0, 0xa1, 0x8e, 0x26, 0x09,
+ 0x78, 0x57, 0x9a, 0xb5, 0xc4, 0xeb, 0x0f, 0x20, 0x51, 0x7e,
+ 0xb3, 0x9c, 0xed, 0xc2, 0x6a, 0x45, 0x34, 0x1b, 0xd6, 0xf9,
+ 0x88, 0xa7, 0xc5, 0xea, 0x9b, 0xb4, 0x79, 0x56, 0x27, 0x08,
+ 0xa0, 0x8f, 0xfe, 0xd1, 0x1c, 0x33, 0x42, 0x6d, 0x86, 0xa9,
+ 0xd8, 0xf7, 0x3a, 0x15, 0x64, 0x4b, 0xe3, 0xcc, 0xbd, 0x92,
+ 0x5f, 0x70, 0x01, 0x2e, 0x4c, 0x63, 0x12, 0x3d, 0xf0, 0xdf,
+ 0xae, 0x81, 0x29, 0x06, 0x77, 0x58, 0x95, 0xba, 0xcb, 0xe4,
+ 0x1e, 0x31, 0x40, 0x6f, 0xa2, 0x8d, 0xfc, 0xd3, 0x7b, 0x54,
+ 0x25, 0x0a, 0xc7, 0xe8, 0x99, 0xb6, 0xd4, 0xfb, 0x8a, 0xa5,
+ 0x68, 0x47, 0x36, 0x19, 0xb1, 0x9e, 0xef, 0xc0, 0x0d, 0x22,
+ 0x53, 0x7c, 0x97, 0xb8, 0xc9, 0xe6, 0x2b, 0x04, 0x75, 0x5a,
+ 0xf2, 0xdd, 0xac, 0x83, 0x4e, 0x61, 0x10, 0x3f, 0x5d, 0x72,
+ 0x03, 0x2c, 0xe1, 0xce, 0xbf, 0x90, 0x38, 0x17, 0x66, 0x49,
+ 0x84, 0xab, 0xda, 0xf5, 0x11, 0x3e, 0x4f, 0x60, 0xad, 0x82,
+ 0xf3, 0xdc, 0x74, 0x5b, 0x2a, 0x05, 0xc8, 0xe7, 0x96, 0xb9,
+ 0xdb, 0xf4, 0x85, 0xaa, 0x67, 0x48, 0x39, 0x16, 0xbe, 0x91,
+ 0xe0, 0xcf, 0x02, 0x2d, 0x5c, 0x73, 0x98, 0xb7, 0xc6, 0xe9,
+ 0x24, 0x0b, 0x7a, 0x55, 0xfd, 0xd2, 0xa3, 0x8c, 0x41, 0x6e,
+ 0x1f, 0x30, 0x52, 0x7d, 0x0c, 0x23, 0xee, 0xc1, 0xb0, 0x9f,
+ 0x37, 0x18, 0x69, 0x46, 0x8b, 0xa4, 0xd5, 0xfa, 0x00, 0x30,
+ 0x60, 0x50, 0xc0, 0xf0, 0xa0, 0x90, 0x9d, 0xad, 0xfd, 0xcd,
+ 0x5d, 0x6d, 0x3d, 0x0d, 0x27, 0x17, 0x47, 0x77, 0xe7, 0xd7,
+ 0x87, 0xb7, 0xba, 0x8a, 0xda, 0xea, 0x7a, 0x4a, 0x1a, 0x2a,
+ 0x4e, 0x7e, 0x2e, 0x1e, 0x8e, 0xbe, 0xee, 0xde, 0xd3, 0xe3,
+ 0xb3, 0x83, 0x13, 0x23, 0x73, 0x43, 0x69, 0x59, 0x09, 0x39,
+ 0xa9, 0x99, 0xc9, 0xf9, 0xf4, 0xc4, 0x94, 0xa4, 0x34, 0x04,
+ 0x54, 0x64, 0x9c, 0xac, 0xfc, 0xcc, 0x5c, 0x6c, 0x3c, 0x0c,
+ 0x01, 0x31, 0x61, 0x51, 0xc1, 0xf1, 0xa1, 0x91, 0xbb, 0x8b,
+ 0xdb, 0xeb, 0x7b, 0x4b, 0x1b, 0x2b, 0x26, 0x16, 0x46, 0x76,
+ 0xe6, 0xd6, 0x86, 0xb6, 0xd2, 0xe2, 0xb2, 0x82, 0x12, 0x22,
+ 0x72, 0x42, 0x4f, 0x7f, 0x2f, 0x1f, 0x8f, 0xbf, 0xef, 0xdf,
+ 0xf5, 0xc5, 0x95, 0xa5, 0x35, 0x05, 0x55, 0x65, 0x68, 0x58,
+ 0x08, 0x38, 0xa8, 0x98, 0xc8, 0xf8, 0x25, 0x15, 0x45, 0x75,
+ 0xe5, 0xd5, 0x85, 0xb5, 0xb8, 0x88, 0xd8, 0xe8, 0x78, 0x48,
+ 0x18, 0x28, 0x02, 0x32, 0x62, 0x52, 0xc2, 0xf2, 0xa2, 0x92,
+ 0x9f, 0xaf, 0xff, 0xcf, 0x5f, 0x6f, 0x3f, 0x0f, 0x6b, 0x5b,
+ 0x0b, 0x3b, 0xab, 0x9b, 0xcb, 0xfb, 0xf6, 0xc6, 0x96, 0xa6,
+ 0x36, 0x06, 0x56, 0x66, 0x4c, 0x7c, 0x2c, 0x1c, 0x8c, 0xbc,
+ 0xec, 0xdc, 0xd1, 0xe1, 0xb1, 0x81, 0x11, 0x21, 0x71, 0x41,
+ 0xb9, 0x89, 0xd9, 0xe9, 0x79, 0x49, 0x19, 0x29, 0x24, 0x14,
+ 0x44, 0x74, 0xe4, 0xd4, 0x84, 0xb4, 0x9e, 0xae, 0xfe, 0xce,
+ 0x5e, 0x6e, 0x3e, 0x0e, 0x03, 0x33, 0x63, 0x53, 0xc3, 0xf3,
+ 0xa3, 0x93, 0xf7, 0xc7, 0x97, 0xa7, 0x37, 0x07, 0x57, 0x67,
+ 0x6a, 0x5a, 0x0a, 0x3a, 0xaa, 0x9a, 0xca, 0xfa, 0xd0, 0xe0,
+ 0xb0, 0x80, 0x10, 0x20, 0x70, 0x40, 0x4d, 0x7d, 0x2d, 0x1d,
+ 0x8d, 0xbd, 0xed, 0xdd, 0x00, 0x31, 0x62, 0x53, 0xc4, 0xf5,
+ 0xa6, 0x97, 0x95, 0xa4, 0xf7, 0xc6, 0x51, 0x60, 0x33, 0x02,
+ 0x37, 0x06, 0x55, 0x64, 0xf3, 0xc2, 0x91, 0xa0, 0xa2, 0x93,
+ 0xc0, 0xf1, 0x66, 0x57, 0x04, 0x35, 0x6e, 0x5f, 0x0c, 0x3d,
+ 0xaa, 0x9b, 0xc8, 0xf9, 0xfb, 0xca, 0x99, 0xa8, 0x3f, 0x0e,
+ 0x5d, 0x6c, 0x59, 0x68, 0x3b, 0x0a, 0x9d, 0xac, 0xff, 0xce,
+ 0xcc, 0xfd, 0xae, 0x9f, 0x08, 0x39, 0x6a, 0x5b, 0xdc, 0xed,
+ 0xbe, 0x8f, 0x18, 0x29, 0x7a, 0x4b, 0x49, 0x78, 0x2b, 0x1a,
+ 0x8d, 0xbc, 0xef, 0xde, 0xeb, 0xda, 0x89, 0xb8, 0x2f, 0x1e,
+ 0x4d, 0x7c, 0x7e, 0x4f, 0x1c, 0x2d, 0xba, 0x8b, 0xd8, 0xe9,
+ 0xb2, 0x83, 0xd0, 0xe1, 0x76, 0x47, 0x14, 0x25, 0x27, 0x16,
+ 0x45, 0x74, 0xe3, 0xd2, 0x81, 0xb0, 0x85, 0xb4, 0xe7, 0xd6,
+ 0x41, 0x70, 0x23, 0x12, 0x10, 0x21, 0x72, 0x43, 0xd4, 0xe5,
+ 0xb6, 0x87, 0xa5, 0x94, 0xc7, 0xf6, 0x61, 0x50, 0x03, 0x32,
+ 0x30, 0x01, 0x52, 0x63, 0xf4, 0xc5, 0x96, 0xa7, 0x92, 0xa3,
+ 0xf0, 0xc1, 0x56, 0x67, 0x34, 0x05, 0x07, 0x36, 0x65, 0x54,
+ 0xc3, 0xf2, 0xa1, 0x90, 0xcb, 0xfa, 0xa9, 0x98, 0x0f, 0x3e,
+ 0x6d, 0x5c, 0x5e, 0x6f, 0x3c, 0x0d, 0x9a, 0xab, 0xf8, 0xc9,
+ 0xfc, 0xcd, 0x9e, 0xaf, 0x38, 0x09, 0x5a, 0x6b, 0x69, 0x58,
+ 0x0b, 0x3a, 0xad, 0x9c, 0xcf, 0xfe, 0x79, 0x48, 0x1b, 0x2a,
+ 0xbd, 0x8c, 0xdf, 0xee, 0xec, 0xdd, 0x8e, 0xbf, 0x28, 0x19,
+ 0x4a, 0x7b, 0x4e, 0x7f, 0x2c, 0x1d, 0x8a, 0xbb, 0xe8, 0xd9,
+ 0xdb, 0xea, 0xb9, 0x88, 0x1f, 0x2e, 0x7d, 0x4c, 0x17, 0x26,
+ 0x75, 0x44, 0xd3, 0xe2, 0xb1, 0x80, 0x82, 0xb3, 0xe0, 0xd1,
+ 0x46, 0x77, 0x24, 0x15, 0x20, 0x11, 0x42, 0x73, 0xe4, 0xd5,
+ 0x86, 0xb7, 0xb5, 0x84, 0xd7, 0xe6, 0x71, 0x40, 0x13, 0x22,
+ 0x00, 0x32, 0x64, 0x56, 0xc8, 0xfa, 0xac, 0x9e, 0x8d, 0xbf,
+ 0xe9, 0xdb, 0x45, 0x77, 0x21, 0x13, 0x07, 0x35, 0x63, 0x51,
+ 0xcf, 0xfd, 0xab, 0x99, 0x8a, 0xb8, 0xee, 0xdc, 0x42, 0x70,
+ 0x26, 0x14, 0x0e, 0x3c, 0x6a, 0x58, 0xc6, 0xf4, 0xa2, 0x90,
+ 0x83, 0xb1, 0xe7, 0xd5, 0x4b, 0x79, 0x2f, 0x1d, 0x09, 0x3b,
+ 0x6d, 0x5f, 0xc1, 0xf3, 0xa5, 0x97, 0x84, 0xb6, 0xe0, 0xd2,
+ 0x4c, 0x7e, 0x28, 0x1a, 0x1c, 0x2e, 0x78, 0x4a, 0xd4, 0xe6,
+ 0xb0, 0x82, 0x91, 0xa3, 0xf5, 0xc7, 0x59, 0x6b, 0x3d, 0x0f,
+ 0x1b, 0x29, 0x7f, 0x4d, 0xd3, 0xe1, 0xb7, 0x85, 0x96, 0xa4,
+ 0xf2, 0xc0, 0x5e, 0x6c, 0x3a, 0x08, 0x12, 0x20, 0x76, 0x44,
+ 0xda, 0xe8, 0xbe, 0x8c, 0x9f, 0xad, 0xfb, 0xc9, 0x57, 0x65,
+ 0x33, 0x01, 0x15, 0x27, 0x71, 0x43, 0xdd, 0xef, 0xb9, 0x8b,
+ 0x98, 0xaa, 0xfc, 0xce, 0x50, 0x62, 0x34, 0x06, 0x38, 0x0a,
+ 0x5c, 0x6e, 0xf0, 0xc2, 0x94, 0xa6, 0xb5, 0x87, 0xd1, 0xe3,
+ 0x7d, 0x4f, 0x19, 0x2b, 0x3f, 0x0d, 0x5b, 0x69, 0xf7, 0xc5,
+ 0x93, 0xa1, 0xb2, 0x80, 0xd6, 0xe4, 0x7a, 0x48, 0x1e, 0x2c,
+ 0x36, 0x04, 0x52, 0x60, 0xfe, 0xcc, 0x9a, 0xa8, 0xbb, 0x89,
+ 0xdf, 0xed, 0x73, 0x41, 0x17, 0x25, 0x31, 0x03, 0x55, 0x67,
+ 0xf9, 0xcb, 0x9d, 0xaf, 0xbc, 0x8e, 0xd8, 0xea, 0x74, 0x46,
+ 0x10, 0x22, 0x24, 0x16, 0x40, 0x72, 0xec, 0xde, 0x88, 0xba,
+ 0xa9, 0x9b, 0xcd, 0xff, 0x61, 0x53, 0x05, 0x37, 0x23, 0x11,
+ 0x47, 0x75, 0xeb, 0xd9, 0x8f, 0xbd, 0xae, 0x9c, 0xca, 0xf8,
+ 0x66, 0x54, 0x02, 0x30, 0x2a, 0x18, 0x4e, 0x7c, 0xe2, 0xd0,
+ 0x86, 0xb4, 0xa7, 0x95, 0xc3, 0xf1, 0x6f, 0x5d, 0x0b, 0x39,
+ 0x2d, 0x1f, 0x49, 0x7b, 0xe5, 0xd7, 0x81, 0xb3, 0xa0, 0x92,
+ 0xc4, 0xf6, 0x68, 0x5a, 0x0c, 0x3e, 0x00, 0x33, 0x66, 0x55,
+ 0xcc, 0xff, 0xaa, 0x99, 0x85, 0xb6, 0xe3, 0xd0, 0x49, 0x7a,
+ 0x2f, 0x1c, 0x17, 0x24, 0x71, 0x42, 0xdb, 0xe8, 0xbd, 0x8e,
+ 0x92, 0xa1, 0xf4, 0xc7, 0x5e, 0x6d, 0x38, 0x0b, 0x2e, 0x1d,
+ 0x48, 0x7b, 0xe2, 0xd1, 0x84, 0xb7, 0xab, 0x98, 0xcd, 0xfe,
+ 0x67, 0x54, 0x01, 0x32, 0x39, 0x0a, 0x5f, 0x6c, 0xf5, 0xc6,
+ 0x93, 0xa0, 0xbc, 0x8f, 0xda, 0xe9, 0x70, 0x43, 0x16, 0x25,
+ 0x5c, 0x6f, 0x3a, 0x09, 0x90, 0xa3, 0xf6, 0xc5, 0xd9, 0xea,
+ 0xbf, 0x8c, 0x15, 0x26, 0x73, 0x40, 0x4b, 0x78, 0x2d, 0x1e,
+ 0x87, 0xb4, 0xe1, 0xd2, 0xce, 0xfd, 0xa8, 0x9b, 0x02, 0x31,
+ 0x64, 0x57, 0x72, 0x41, 0x14, 0x27, 0xbe, 0x8d, 0xd8, 0xeb,
+ 0xf7, 0xc4, 0x91, 0xa2, 0x3b, 0x08, 0x5d, 0x6e, 0x65, 0x56,
+ 0x03, 0x30, 0xa9, 0x9a, 0xcf, 0xfc, 0xe0, 0xd3, 0x86, 0xb5,
+ 0x2c, 0x1f, 0x4a, 0x79, 0xb8, 0x8b, 0xde, 0xed, 0x74, 0x47,
+ 0x12, 0x21, 0x3d, 0x0e, 0x5b, 0x68, 0xf1, 0xc2, 0x97, 0xa4,
+ 0xaf, 0x9c, 0xc9, 0xfa, 0x63, 0x50, 0x05, 0x36, 0x2a, 0x19,
+ 0x4c, 0x7f, 0xe6, 0xd5, 0x80, 0xb3, 0x96, 0xa5, 0xf0, 0xc3,
+ 0x5a, 0x69, 0x3c, 0x0f, 0x13, 0x20, 0x75, 0x46, 0xdf, 0xec,
+ 0xb9, 0x8a, 0x81, 0xb2, 0xe7, 0xd4, 0x4d, 0x7e, 0x2b, 0x18,
+ 0x04, 0x37, 0x62, 0x51, 0xc8, 0xfb, 0xae, 0x9d, 0xe4, 0xd7,
+ 0x82, 0xb1, 0x28, 0x1b, 0x4e, 0x7d, 0x61, 0x52, 0x07, 0x34,
+ 0xad, 0x9e, 0xcb, 0xf8, 0xf3, 0xc0, 0x95, 0xa6, 0x3f, 0x0c,
+ 0x59, 0x6a, 0x76, 0x45, 0x10, 0x23, 0xba, 0x89, 0xdc, 0xef,
+ 0xca, 0xf9, 0xac, 0x9f, 0x06, 0x35, 0x60, 0x53, 0x4f, 0x7c,
+ 0x29, 0x1a, 0x83, 0xb0, 0xe5, 0xd6, 0xdd, 0xee, 0xbb, 0x88,
+ 0x11, 0x22, 0x77, 0x44, 0x58, 0x6b, 0x3e, 0x0d, 0x94, 0xa7,
+ 0xf2, 0xc1, 0x00, 0x34, 0x68, 0x5c, 0xd0, 0xe4, 0xb8, 0x8c,
+ 0xbd, 0x89, 0xd5, 0xe1, 0x6d, 0x59, 0x05, 0x31, 0x67, 0x53,
+ 0x0f, 0x3b, 0xb7, 0x83, 0xdf, 0xeb, 0xda, 0xee, 0xb2, 0x86,
+ 0x0a, 0x3e, 0x62, 0x56, 0xce, 0xfa, 0xa6, 0x92, 0x1e, 0x2a,
+ 0x76, 0x42, 0x73, 0x47, 0x1b, 0x2f, 0xa3, 0x97, 0xcb, 0xff,
+ 0xa9, 0x9d, 0xc1, 0xf5, 0x79, 0x4d, 0x11, 0x25, 0x14, 0x20,
+ 0x7c, 0x48, 0xc4, 0xf0, 0xac, 0x98, 0x81, 0xb5, 0xe9, 0xdd,
+ 0x51, 0x65, 0x39, 0x0d, 0x3c, 0x08, 0x54, 0x60, 0xec, 0xd8,
+ 0x84, 0xb0, 0xe6, 0xd2, 0x8e, 0xba, 0x36, 0x02, 0x5e, 0x6a,
+ 0x5b, 0x6f, 0x33, 0x07, 0x8b, 0xbf, 0xe3, 0xd7, 0x4f, 0x7b,
+ 0x27, 0x13, 0x9f, 0xab, 0xf7, 0xc3, 0xf2, 0xc6, 0x9a, 0xae,
+ 0x22, 0x16, 0x4a, 0x7e, 0x28, 0x1c, 0x40, 0x74, 0xf8, 0xcc,
+ 0x90, 0xa4, 0x95, 0xa1, 0xfd, 0xc9, 0x45, 0x71, 0x2d, 0x19,
+ 0x1f, 0x2b, 0x77, 0x43, 0xcf, 0xfb, 0xa7, 0x93, 0xa2, 0x96,
+ 0xca, 0xfe, 0x72, 0x46, 0x1a, 0x2e, 0x78, 0x4c, 0x10, 0x24,
+ 0xa8, 0x9c, 0xc0, 0xf4, 0xc5, 0xf1, 0xad, 0x99, 0x15, 0x21,
+ 0x7d, 0x49, 0xd1, 0xe5, 0xb9, 0x8d, 0x01, 0x35, 0x69, 0x5d,
+ 0x6c, 0x58, 0x04, 0x30, 0xbc, 0x88, 0xd4, 0xe0, 0xb6, 0x82,
+ 0xde, 0xea, 0x66, 0x52, 0x0e, 0x3a, 0x0b, 0x3f, 0x63, 0x57,
+ 0xdb, 0xef, 0xb3, 0x87, 0x9e, 0xaa, 0xf6, 0xc2, 0x4e, 0x7a,
+ 0x26, 0x12, 0x23, 0x17, 0x4b, 0x7f, 0xf3, 0xc7, 0x9b, 0xaf,
+ 0xf9, 0xcd, 0x91, 0xa5, 0x29, 0x1d, 0x41, 0x75, 0x44, 0x70,
+ 0x2c, 0x18, 0x94, 0xa0, 0xfc, 0xc8, 0x50, 0x64, 0x38, 0x0c,
+ 0x80, 0xb4, 0xe8, 0xdc, 0xed, 0xd9, 0x85, 0xb1, 0x3d, 0x09,
+ 0x55, 0x61, 0x37, 0x03, 0x5f, 0x6b, 0xe7, 0xd3, 0x8f, 0xbb,
+ 0x8a, 0xbe, 0xe2, 0xd6, 0x5a, 0x6e, 0x32, 0x06, 0x00, 0x35,
+ 0x6a, 0x5f, 0xd4, 0xe1, 0xbe, 0x8b, 0xb5, 0x80, 0xdf, 0xea,
+ 0x61, 0x54, 0x0b, 0x3e, 0x77, 0x42, 0x1d, 0x28, 0xa3, 0x96,
+ 0xc9, 0xfc, 0xc2, 0xf7, 0xa8, 0x9d, 0x16, 0x23, 0x7c, 0x49,
+ 0xee, 0xdb, 0x84, 0xb1, 0x3a, 0x0f, 0x50, 0x65, 0x5b, 0x6e,
+ 0x31, 0x04, 0x8f, 0xba, 0xe5, 0xd0, 0x99, 0xac, 0xf3, 0xc6,
+ 0x4d, 0x78, 0x27, 0x12, 0x2c, 0x19, 0x46, 0x73, 0xf8, 0xcd,
+ 0x92, 0xa7, 0xc1, 0xf4, 0xab, 0x9e, 0x15, 0x20, 0x7f, 0x4a,
+ 0x74, 0x41, 0x1e, 0x2b, 0xa0, 0x95, 0xca, 0xff, 0xb6, 0x83,
+ 0xdc, 0xe9, 0x62, 0x57, 0x08, 0x3d, 0x03, 0x36, 0x69, 0x5c,
+ 0xd7, 0xe2, 0xbd, 0x88, 0x2f, 0x1a, 0x45, 0x70, 0xfb, 0xce,
+ 0x91, 0xa4, 0x9a, 0xaf, 0xf0, 0xc5, 0x4e, 0x7b, 0x24, 0x11,
+ 0x58, 0x6d, 0x32, 0x07, 0x8c, 0xb9, 0xe6, 0xd3, 0xed, 0xd8,
+ 0x87, 0xb2, 0x39, 0x0c, 0x53, 0x66, 0x9f, 0xaa, 0xf5, 0xc0,
+ 0x4b, 0x7e, 0x21, 0x14, 0x2a, 0x1f, 0x40, 0x75, 0xfe, 0xcb,
+ 0x94, 0xa1, 0xe8, 0xdd, 0x82, 0xb7, 0x3c, 0x09, 0x56, 0x63,
+ 0x5d, 0x68, 0x37, 0x02, 0x89, 0xbc, 0xe3, 0xd6, 0x71, 0x44,
+ 0x1b, 0x2e, 0xa5, 0x90, 0xcf, 0xfa, 0xc4, 0xf1, 0xae, 0x9b,
+ 0x10, 0x25, 0x7a, 0x4f, 0x06, 0x33, 0x6c, 0x59, 0xd2, 0xe7,
+ 0xb8, 0x8d, 0xb3, 0x86, 0xd9, 0xec, 0x67, 0x52, 0x0d, 0x38,
+ 0x5e, 0x6b, 0x34, 0x01, 0x8a, 0xbf, 0xe0, 0xd5, 0xeb, 0xde,
+ 0x81, 0xb4, 0x3f, 0x0a, 0x55, 0x60, 0x29, 0x1c, 0x43, 0x76,
+ 0xfd, 0xc8, 0x97, 0xa2, 0x9c, 0xa9, 0xf6, 0xc3, 0x48, 0x7d,
+ 0x22, 0x17, 0xb0, 0x85, 0xda, 0xef, 0x64, 0x51, 0x0e, 0x3b,
+ 0x05, 0x30, 0x6f, 0x5a, 0xd1, 0xe4, 0xbb, 0x8e, 0xc7, 0xf2,
+ 0xad, 0x98, 0x13, 0x26, 0x79, 0x4c, 0x72, 0x47, 0x18, 0x2d,
+ 0xa6, 0x93, 0xcc, 0xf9, 0x00, 0x36, 0x6c, 0x5a, 0xd8, 0xee,
+ 0xb4, 0x82, 0xad, 0x9b, 0xc1, 0xf7, 0x75, 0x43, 0x19, 0x2f,
+ 0x47, 0x71, 0x2b, 0x1d, 0x9f, 0xa9, 0xf3, 0xc5, 0xea, 0xdc,
+ 0x86, 0xb0, 0x32, 0x04, 0x5e, 0x68, 0x8e, 0xb8, 0xe2, 0xd4,
+ 0x56, 0x60, 0x3a, 0x0c, 0x23, 0x15, 0x4f, 0x79, 0xfb, 0xcd,
+ 0x97, 0xa1, 0xc9, 0xff, 0xa5, 0x93, 0x11, 0x27, 0x7d, 0x4b,
+ 0x64, 0x52, 0x08, 0x3e, 0xbc, 0x8a, 0xd0, 0xe6, 0x01, 0x37,
+ 0x6d, 0x5b, 0xd9, 0xef, 0xb5, 0x83, 0xac, 0x9a, 0xc0, 0xf6,
+ 0x74, 0x42, 0x18, 0x2e, 0x46, 0x70, 0x2a, 0x1c, 0x9e, 0xa8,
+ 0xf2, 0xc4, 0xeb, 0xdd, 0x87, 0xb1, 0x33, 0x05, 0x5f, 0x69,
+ 0x8f, 0xb9, 0xe3, 0xd5, 0x57, 0x61, 0x3b, 0x0d, 0x22, 0x14,
+ 0x4e, 0x78, 0xfa, 0xcc, 0x96, 0xa0, 0xc8, 0xfe, 0xa4, 0x92,
+ 0x10, 0x26, 0x7c, 0x4a, 0x65, 0x53, 0x09, 0x3f, 0xbd, 0x8b,
+ 0xd1, 0xe7, 0x02, 0x34, 0x6e, 0x58, 0xda, 0xec, 0xb6, 0x80,
+ 0xaf, 0x99, 0xc3, 0xf5, 0x77, 0x41, 0x1b, 0x2d, 0x45, 0x73,
+ 0x29, 0x1f, 0x9d, 0xab, 0xf1, 0xc7, 0xe8, 0xde, 0x84, 0xb2,
+ 0x30, 0x06, 0x5c, 0x6a, 0x8c, 0xba, 0xe0, 0xd6, 0x54, 0x62,
+ 0x38, 0x0e, 0x21, 0x17, 0x4d, 0x7b, 0xf9, 0xcf, 0x95, 0xa3,
+ 0xcb, 0xfd, 0xa7, 0x91, 0x13, 0x25, 0x7f, 0x49, 0x66, 0x50,
+ 0x0a, 0x3c, 0xbe, 0x88, 0xd2, 0xe4, 0x03, 0x35, 0x6f, 0x59,
+ 0xdb, 0xed, 0xb7, 0x81, 0xae, 0x98, 0xc2, 0xf4, 0x76, 0x40,
+ 0x1a, 0x2c, 0x44, 0x72, 0x28, 0x1e, 0x9c, 0xaa, 0xf0, 0xc6,
+ 0xe9, 0xdf, 0x85, 0xb3, 0x31, 0x07, 0x5d, 0x6b, 0x8d, 0xbb,
+ 0xe1, 0xd7, 0x55, 0x63, 0x39, 0x0f, 0x20, 0x16, 0x4c, 0x7a,
+ 0xf8, 0xce, 0x94, 0xa2, 0xca, 0xfc, 0xa6, 0x90, 0x12, 0x24,
+ 0x7e, 0x48, 0x67, 0x51, 0x0b, 0x3d, 0xbf, 0x89, 0xd3, 0xe5,
+ 0x00, 0x37, 0x6e, 0x59, 0xdc, 0xeb, 0xb2, 0x85, 0xa5, 0x92,
+ 0xcb, 0xfc, 0x79, 0x4e, 0x17, 0x20, 0x57, 0x60, 0x39, 0x0e,
+ 0x8b, 0xbc, 0xe5, 0xd2, 0xf2, 0xc5, 0x9c, 0xab, 0x2e, 0x19,
+ 0x40, 0x77, 0xae, 0x99, 0xc0, 0xf7, 0x72, 0x45, 0x1c, 0x2b,
+ 0x0b, 0x3c, 0x65, 0x52, 0xd7, 0xe0, 0xb9, 0x8e, 0xf9, 0xce,
+ 0x97, 0xa0, 0x25, 0x12, 0x4b, 0x7c, 0x5c, 0x6b, 0x32, 0x05,
+ 0x80, 0xb7, 0xee, 0xd9, 0x41, 0x76, 0x2f, 0x18, 0x9d, 0xaa,
+ 0xf3, 0xc4, 0xe4, 0xd3, 0x8a, 0xbd, 0x38, 0x0f, 0x56, 0x61,
+ 0x16, 0x21, 0x78, 0x4f, 0xca, 0xfd, 0xa4, 0x93, 0xb3, 0x84,
+ 0xdd, 0xea, 0x6f, 0x58, 0x01, 0x36, 0xef, 0xd8, 0x81, 0xb6,
+ 0x33, 0x04, 0x5d, 0x6a, 0x4a, 0x7d, 0x24, 0x13, 0x96, 0xa1,
+ 0xf8, 0xcf, 0xb8, 0x8f, 0xd6, 0xe1, 0x64, 0x53, 0x0a, 0x3d,
+ 0x1d, 0x2a, 0x73, 0x44, 0xc1, 0xf6, 0xaf, 0x98, 0x82, 0xb5,
+ 0xec, 0xdb, 0x5e, 0x69, 0x30, 0x07, 0x27, 0x10, 0x49, 0x7e,
+ 0xfb, 0xcc, 0x95, 0xa2, 0xd5, 0xe2, 0xbb, 0x8c, 0x09, 0x3e,
+ 0x67, 0x50, 0x70, 0x47, 0x1e, 0x29, 0xac, 0x9b, 0xc2, 0xf5,
+ 0x2c, 0x1b, 0x42, 0x75, 0xf0, 0xc7, 0x9e, 0xa9, 0x89, 0xbe,
+ 0xe7, 0xd0, 0x55, 0x62, 0x3b, 0x0c, 0x7b, 0x4c, 0x15, 0x22,
+ 0xa7, 0x90, 0xc9, 0xfe, 0xde, 0xe9, 0xb0, 0x87, 0x02, 0x35,
+ 0x6c, 0x5b, 0xc3, 0xf4, 0xad, 0x9a, 0x1f, 0x28, 0x71, 0x46,
+ 0x66, 0x51, 0x08, 0x3f, 0xba, 0x8d, 0xd4, 0xe3, 0x94, 0xa3,
+ 0xfa, 0xcd, 0x48, 0x7f, 0x26, 0x11, 0x31, 0x06, 0x5f, 0x68,
+ 0xed, 0xda, 0x83, 0xb4, 0x6d, 0x5a, 0x03, 0x34, 0xb1, 0x86,
+ 0xdf, 0xe8, 0xc8, 0xff, 0xa6, 0x91, 0x14, 0x23, 0x7a, 0x4d,
+ 0x3a, 0x0d, 0x54, 0x63, 0xe6, 0xd1, 0x88, 0xbf, 0x9f, 0xa8,
+ 0xf1, 0xc6, 0x43, 0x74, 0x2d, 0x1a, 0x00, 0x38, 0x70, 0x48,
+ 0xe0, 0xd8, 0x90, 0xa8, 0xdd, 0xe5, 0xad, 0x95, 0x3d, 0x05,
+ 0x4d, 0x75, 0xa7, 0x9f, 0xd7, 0xef, 0x47, 0x7f, 0x37, 0x0f,
+ 0x7a, 0x42, 0x0a, 0x32, 0x9a, 0xa2, 0xea, 0xd2, 0x53, 0x6b,
+ 0x23, 0x1b, 0xb3, 0x8b, 0xc3, 0xfb, 0x8e, 0xb6, 0xfe, 0xc6,
+ 0x6e, 0x56, 0x1e, 0x26, 0xf4, 0xcc, 0x84, 0xbc, 0x14, 0x2c,
+ 0x64, 0x5c, 0x29, 0x11, 0x59, 0x61, 0xc9, 0xf1, 0xb9, 0x81,
+ 0xa6, 0x9e, 0xd6, 0xee, 0x46, 0x7e, 0x36, 0x0e, 0x7b, 0x43,
+ 0x0b, 0x33, 0x9b, 0xa3, 0xeb, 0xd3, 0x01, 0x39, 0x71, 0x49,
+ 0xe1, 0xd9, 0x91, 0xa9, 0xdc, 0xe4, 0xac, 0x94, 0x3c, 0x04,
+ 0x4c, 0x74, 0xf5, 0xcd, 0x85, 0xbd, 0x15, 0x2d, 0x65, 0x5d,
+ 0x28, 0x10, 0x58, 0x60, 0xc8, 0xf0, 0xb8, 0x80, 0x52, 0x6a,
+ 0x22, 0x1a, 0xb2, 0x8a, 0xc2, 0xfa, 0x8f, 0xb7, 0xff, 0xc7,
+ 0x6f, 0x57, 0x1f, 0x27, 0x51, 0x69, 0x21, 0x19, 0xb1, 0x89,
+ 0xc1, 0xf9, 0x8c, 0xb4, 0xfc, 0xc4, 0x6c, 0x54, 0x1c, 0x24,
+ 0xf6, 0xce, 0x86, 0xbe, 0x16, 0x2e, 0x66, 0x5e, 0x2b, 0x13,
+ 0x5b, 0x63, 0xcb, 0xf3, 0xbb, 0x83, 0x02, 0x3a, 0x72, 0x4a,
+ 0xe2, 0xda, 0x92, 0xaa, 0xdf, 0xe7, 0xaf, 0x97, 0x3f, 0x07,
+ 0x4f, 0x77, 0xa5, 0x9d, 0xd5, 0xed, 0x45, 0x7d, 0x35, 0x0d,
+ 0x78, 0x40, 0x08, 0x30, 0x98, 0xa0, 0xe8, 0xd0, 0xf7, 0xcf,
+ 0x87, 0xbf, 0x17, 0x2f, 0x67, 0x5f, 0x2a, 0x12, 0x5a, 0x62,
+ 0xca, 0xf2, 0xba, 0x82, 0x50, 0x68, 0x20, 0x18, 0xb0, 0x88,
+ 0xc0, 0xf8, 0x8d, 0xb5, 0xfd, 0xc5, 0x6d, 0x55, 0x1d, 0x25,
+ 0xa4, 0x9c, 0xd4, 0xec, 0x44, 0x7c, 0x34, 0x0c, 0x79, 0x41,
+ 0x09, 0x31, 0x99, 0xa1, 0xe9, 0xd1, 0x03, 0x3b, 0x73, 0x4b,
+ 0xe3, 0xdb, 0x93, 0xab, 0xde, 0xe6, 0xae, 0x96, 0x3e, 0x06,
+ 0x4e, 0x76, 0x00, 0x39, 0x72, 0x4b, 0xe4, 0xdd, 0x96, 0xaf,
+ 0xd5, 0xec, 0xa7, 0x9e, 0x31, 0x08, 0x43, 0x7a, 0xb7, 0x8e,
+ 0xc5, 0xfc, 0x53, 0x6a, 0x21, 0x18, 0x62, 0x5b, 0x10, 0x29,
+ 0x86, 0xbf, 0xf4, 0xcd, 0x73, 0x4a, 0x01, 0x38, 0x97, 0xae,
+ 0xe5, 0xdc, 0xa6, 0x9f, 0xd4, 0xed, 0x42, 0x7b, 0x30, 0x09,
+ 0xc4, 0xfd, 0xb6, 0x8f, 0x20, 0x19, 0x52, 0x6b, 0x11, 0x28,
+ 0x63, 0x5a, 0xf5, 0xcc, 0x87, 0xbe, 0xe6, 0xdf, 0x94, 0xad,
+ 0x02, 0x3b, 0x70, 0x49, 0x33, 0x0a, 0x41, 0x78, 0xd7, 0xee,
+ 0xa5, 0x9c, 0x51, 0x68, 0x23, 0x1a, 0xb5, 0x8c, 0xc7, 0xfe,
+ 0x84, 0xbd, 0xf6, 0xcf, 0x60, 0x59, 0x12, 0x2b, 0x95, 0xac,
+ 0xe7, 0xde, 0x71, 0x48, 0x03, 0x3a, 0x40, 0x79, 0x32, 0x0b,
+ 0xa4, 0x9d, 0xd6, 0xef, 0x22, 0x1b, 0x50, 0x69, 0xc6, 0xff,
+ 0xb4, 0x8d, 0xf7, 0xce, 0x85, 0xbc, 0x13, 0x2a, 0x61, 0x58,
+ 0xd1, 0xe8, 0xa3, 0x9a, 0x35, 0x0c, 0x47, 0x7e, 0x04, 0x3d,
+ 0x76, 0x4f, 0xe0, 0xd9, 0x92, 0xab, 0x66, 0x5f, 0x14, 0x2d,
+ 0x82, 0xbb, 0xf0, 0xc9, 0xb3, 0x8a, 0xc1, 0xf8, 0x57, 0x6e,
+ 0x25, 0x1c, 0xa2, 0x9b, 0xd0, 0xe9, 0x46, 0x7f, 0x34, 0x0d,
+ 0x77, 0x4e, 0x05, 0x3c, 0x93, 0xaa, 0xe1, 0xd8, 0x15, 0x2c,
+ 0x67, 0x5e, 0xf1, 0xc8, 0x83, 0xba, 0xc0, 0xf9, 0xb2, 0x8b,
+ 0x24, 0x1d, 0x56, 0x6f, 0x37, 0x0e, 0x45, 0x7c, 0xd3, 0xea,
+ 0xa1, 0x98, 0xe2, 0xdb, 0x90, 0xa9, 0x06, 0x3f, 0x74, 0x4d,
+ 0x80, 0xb9, 0xf2, 0xcb, 0x64, 0x5d, 0x16, 0x2f, 0x55, 0x6c,
+ 0x27, 0x1e, 0xb1, 0x88, 0xc3, 0xfa, 0x44, 0x7d, 0x36, 0x0f,
+ 0xa0, 0x99, 0xd2, 0xeb, 0x91, 0xa8, 0xe3, 0xda, 0x75, 0x4c,
+ 0x07, 0x3e, 0xf3, 0xca, 0x81, 0xb8, 0x17, 0x2e, 0x65, 0x5c,
+ 0x26, 0x1f, 0x54, 0x6d, 0xc2, 0xfb, 0xb0, 0x89, 0x00, 0x3a,
+ 0x74, 0x4e, 0xe8, 0xd2, 0x9c, 0xa6, 0xcd, 0xf7, 0xb9, 0x83,
+ 0x25, 0x1f, 0x51, 0x6b, 0x87, 0xbd, 0xf3, 0xc9, 0x6f, 0x55,
+ 0x1b, 0x21, 0x4a, 0x70, 0x3e, 0x04, 0xa2, 0x98, 0xd6, 0xec,
+ 0x13, 0x29, 0x67, 0x5d, 0xfb, 0xc1, 0x8f, 0xb5, 0xde, 0xe4,
+ 0xaa, 0x90, 0x36, 0x0c, 0x42, 0x78, 0x94, 0xae, 0xe0, 0xda,
+ 0x7c, 0x46, 0x08, 0x32, 0x59, 0x63, 0x2d, 0x17, 0xb1, 0x8b,
+ 0xc5, 0xff, 0x26, 0x1c, 0x52, 0x68, 0xce, 0xf4, 0xba, 0x80,
+ 0xeb, 0xd1, 0x9f, 0xa5, 0x03, 0x39, 0x77, 0x4d, 0xa1, 0x9b,
+ 0xd5, 0xef, 0x49, 0x73, 0x3d, 0x07, 0x6c, 0x56, 0x18, 0x22,
+ 0x84, 0xbe, 0xf0, 0xca, 0x35, 0x0f, 0x41, 0x7b, 0xdd, 0xe7,
+ 0xa9, 0x93, 0xf8, 0xc2, 0x8c, 0xb6, 0x10, 0x2a, 0x64, 0x5e,
+ 0xb2, 0x88, 0xc6, 0xfc, 0x5a, 0x60, 0x2e, 0x14, 0x7f, 0x45,
+ 0x0b, 0x31, 0x97, 0xad, 0xe3, 0xd9, 0x4c, 0x76, 0x38, 0x02,
+ 0xa4, 0x9e, 0xd0, 0xea, 0x81, 0xbb, 0xf5, 0xcf, 0x69, 0x53,
+ 0x1d, 0x27, 0xcb, 0xf1, 0xbf, 0x85, 0x23, 0x19, 0x57, 0x6d,
+ 0x06, 0x3c, 0x72, 0x48, 0xee, 0xd4, 0x9a, 0xa0, 0x5f, 0x65,
+ 0x2b, 0x11, 0xb7, 0x8d, 0xc3, 0xf9, 0x92, 0xa8, 0xe6, 0xdc,
+ 0x7a, 0x40, 0x0e, 0x34, 0xd8, 0xe2, 0xac, 0x96, 0x30, 0x0a,
+ 0x44, 0x7e, 0x15, 0x2f, 0x61, 0x5b, 0xfd, 0xc7, 0x89, 0xb3,
+ 0x6a, 0x50, 0x1e, 0x24, 0x82, 0xb8, 0xf6, 0xcc, 0xa7, 0x9d,
+ 0xd3, 0xe9, 0x4f, 0x75, 0x3b, 0x01, 0xed, 0xd7, 0x99, 0xa3,
+ 0x05, 0x3f, 0x71, 0x4b, 0x20, 0x1a, 0x54, 0x6e, 0xc8, 0xf2,
+ 0xbc, 0x86, 0x79, 0x43, 0x0d, 0x37, 0x91, 0xab, 0xe5, 0xdf,
+ 0xb4, 0x8e, 0xc0, 0xfa, 0x5c, 0x66, 0x28, 0x12, 0xfe, 0xc4,
+ 0x8a, 0xb0, 0x16, 0x2c, 0x62, 0x58, 0x33, 0x09, 0x47, 0x7d,
+ 0xdb, 0xe1, 0xaf, 0x95, 0x00, 0x3b, 0x76, 0x4d, 0xec, 0xd7,
+ 0x9a, 0xa1, 0xc5, 0xfe, 0xb3, 0x88, 0x29, 0x12, 0x5f, 0x64,
+ 0x97, 0xac, 0xe1, 0xda, 0x7b, 0x40, 0x0d, 0x36, 0x52, 0x69,
+ 0x24, 0x1f, 0xbe, 0x85, 0xc8, 0xf3, 0x33, 0x08, 0x45, 0x7e,
+ 0xdf, 0xe4, 0xa9, 0x92, 0xf6, 0xcd, 0x80, 0xbb, 0x1a, 0x21,
+ 0x6c, 0x57, 0xa4, 0x9f, 0xd2, 0xe9, 0x48, 0x73, 0x3e, 0x05,
+ 0x61, 0x5a, 0x17, 0x2c, 0x8d, 0xb6, 0xfb, 0xc0, 0x66, 0x5d,
+ 0x10, 0x2b, 0x8a, 0xb1, 0xfc, 0xc7, 0xa3, 0x98, 0xd5, 0xee,
+ 0x4f, 0x74, 0x39, 0x02, 0xf1, 0xca, 0x87, 0xbc, 0x1d, 0x26,
+ 0x6b, 0x50, 0x34, 0x0f, 0x42, 0x79, 0xd8, 0xe3, 0xae, 0x95,
+ 0x55, 0x6e, 0x23, 0x18, 0xb9, 0x82, 0xcf, 0xf4, 0x90, 0xab,
+ 0xe6, 0xdd, 0x7c, 0x47, 0x0a, 0x31, 0xc2, 0xf9, 0xb4, 0x8f,
+ 0x2e, 0x15, 0x58, 0x63, 0x07, 0x3c, 0x71, 0x4a, 0xeb, 0xd0,
+ 0x9d, 0xa6, 0xcc, 0xf7, 0xba, 0x81, 0x20, 0x1b, 0x56, 0x6d,
+ 0x09, 0x32, 0x7f, 0x44, 0xe5, 0xde, 0x93, 0xa8, 0x5b, 0x60,
+ 0x2d, 0x16, 0xb7, 0x8c, 0xc1, 0xfa, 0x9e, 0xa5, 0xe8, 0xd3,
+ 0x72, 0x49, 0x04, 0x3f, 0xff, 0xc4, 0x89, 0xb2, 0x13, 0x28,
+ 0x65, 0x5e, 0x3a, 0x01, 0x4c, 0x77, 0xd6, 0xed, 0xa0, 0x9b,
+ 0x68, 0x53, 0x1e, 0x25, 0x84, 0xbf, 0xf2, 0xc9, 0xad, 0x96,
+ 0xdb, 0xe0, 0x41, 0x7a, 0x37, 0x0c, 0xaa, 0x91, 0xdc, 0xe7,
+ 0x46, 0x7d, 0x30, 0x0b, 0x6f, 0x54, 0x19, 0x22, 0x83, 0xb8,
+ 0xf5, 0xce, 0x3d, 0x06, 0x4b, 0x70, 0xd1, 0xea, 0xa7, 0x9c,
+ 0xf8, 0xc3, 0x8e, 0xb5, 0x14, 0x2f, 0x62, 0x59, 0x99, 0xa2,
+ 0xef, 0xd4, 0x75, 0x4e, 0x03, 0x38, 0x5c, 0x67, 0x2a, 0x11,
+ 0xb0, 0x8b, 0xc6, 0xfd, 0x0e, 0x35, 0x78, 0x43, 0xe2, 0xd9,
+ 0x94, 0xaf, 0xcb, 0xf0, 0xbd, 0x86, 0x27, 0x1c, 0x51, 0x6a,
+ 0x00, 0x3c, 0x78, 0x44, 0xf0, 0xcc, 0x88, 0xb4, 0xfd, 0xc1,
+ 0x85, 0xb9, 0x0d, 0x31, 0x75, 0x49, 0xe7, 0xdb, 0x9f, 0xa3,
+ 0x17, 0x2b, 0x6f, 0x53, 0x1a, 0x26, 0x62, 0x5e, 0xea, 0xd6,
+ 0x92, 0xae, 0xd3, 0xef, 0xab, 0x97, 0x23, 0x1f, 0x5b, 0x67,
+ 0x2e, 0x12, 0x56, 0x6a, 0xde, 0xe2, 0xa6, 0x9a, 0x34, 0x08,
+ 0x4c, 0x70, 0xc4, 0xf8, 0xbc, 0x80, 0xc9, 0xf5, 0xb1, 0x8d,
+ 0x39, 0x05, 0x41, 0x7d, 0xbb, 0x87, 0xc3, 0xff, 0x4b, 0x77,
+ 0x33, 0x0f, 0x46, 0x7a, 0x3e, 0x02, 0xb6, 0x8a, 0xce, 0xf2,
+ 0x5c, 0x60, 0x24, 0x18, 0xac, 0x90, 0xd4, 0xe8, 0xa1, 0x9d,
+ 0xd9, 0xe5, 0x51, 0x6d, 0x29, 0x15, 0x68, 0x54, 0x10, 0x2c,
+ 0x98, 0xa4, 0xe0, 0xdc, 0x95, 0xa9, 0xed, 0xd1, 0x65, 0x59,
+ 0x1d, 0x21, 0x8f, 0xb3, 0xf7, 0xcb, 0x7f, 0x43, 0x07, 0x3b,
+ 0x72, 0x4e, 0x0a, 0x36, 0x82, 0xbe, 0xfa, 0xc6, 0x6b, 0x57,
+ 0x13, 0x2f, 0x9b, 0xa7, 0xe3, 0xdf, 0x96, 0xaa, 0xee, 0xd2,
+ 0x66, 0x5a, 0x1e, 0x22, 0x8c, 0xb0, 0xf4, 0xc8, 0x7c, 0x40,
+ 0x04, 0x38, 0x71, 0x4d, 0x09, 0x35, 0x81, 0xbd, 0xf9, 0xc5,
+ 0xb8, 0x84, 0xc0, 0xfc, 0x48, 0x74, 0x30, 0x0c, 0x45, 0x79,
+ 0x3d, 0x01, 0xb5, 0x89, 0xcd, 0xf1, 0x5f, 0x63, 0x27, 0x1b,
+ 0xaf, 0x93, 0xd7, 0xeb, 0xa2, 0x9e, 0xda, 0xe6, 0x52, 0x6e,
+ 0x2a, 0x16, 0xd0, 0xec, 0xa8, 0x94, 0x20, 0x1c, 0x58, 0x64,
+ 0x2d, 0x11, 0x55, 0x69, 0xdd, 0xe1, 0xa5, 0x99, 0x37, 0x0b,
+ 0x4f, 0x73, 0xc7, 0xfb, 0xbf, 0x83, 0xca, 0xf6, 0xb2, 0x8e,
+ 0x3a, 0x06, 0x42, 0x7e, 0x03, 0x3f, 0x7b, 0x47, 0xf3, 0xcf,
+ 0x8b, 0xb7, 0xfe, 0xc2, 0x86, 0xba, 0x0e, 0x32, 0x76, 0x4a,
+ 0xe4, 0xd8, 0x9c, 0xa0, 0x14, 0x28, 0x6c, 0x50, 0x19, 0x25,
+ 0x61, 0x5d, 0xe9, 0xd5, 0x91, 0xad, 0x00, 0x3d, 0x7a, 0x47,
+ 0xf4, 0xc9, 0x8e, 0xb3, 0xf5, 0xc8, 0x8f, 0xb2, 0x01, 0x3c,
+ 0x7b, 0x46, 0xf7, 0xca, 0x8d, 0xb0, 0x03, 0x3e, 0x79, 0x44,
+ 0x02, 0x3f, 0x78, 0x45, 0xf6, 0xcb, 0x8c, 0xb1, 0xf3, 0xce,
+ 0x89, 0xb4, 0x07, 0x3a, 0x7d, 0x40, 0x06, 0x3b, 0x7c, 0x41,
+ 0xf2, 0xcf, 0x88, 0xb5, 0x04, 0x39, 0x7e, 0x43, 0xf0, 0xcd,
+ 0x8a, 0xb7, 0xf1, 0xcc, 0x8b, 0xb6, 0x05, 0x38, 0x7f, 0x42,
+ 0xfb, 0xc6, 0x81, 0xbc, 0x0f, 0x32, 0x75, 0x48, 0x0e, 0x33,
+ 0x74, 0x49, 0xfa, 0xc7, 0x80, 0xbd, 0x0c, 0x31, 0x76, 0x4b,
+ 0xf8, 0xc5, 0x82, 0xbf, 0xf9, 0xc4, 0x83, 0xbe, 0x0d, 0x30,
+ 0x77, 0x4a, 0x08, 0x35, 0x72, 0x4f, 0xfc, 0xc1, 0x86, 0xbb,
+ 0xfd, 0xc0, 0x87, 0xba, 0x09, 0x34, 0x73, 0x4e, 0xff, 0xc2,
+ 0x85, 0xb8, 0x0b, 0x36, 0x71, 0x4c, 0x0a, 0x37, 0x70, 0x4d,
+ 0xfe, 0xc3, 0x84, 0xb9, 0xeb, 0xd6, 0x91, 0xac, 0x1f, 0x22,
+ 0x65, 0x58, 0x1e, 0x23, 0x64, 0x59, 0xea, 0xd7, 0x90, 0xad,
+ 0x1c, 0x21, 0x66, 0x5b, 0xe8, 0xd5, 0x92, 0xaf, 0xe9, 0xd4,
+ 0x93, 0xae, 0x1d, 0x20, 0x67, 0x5a, 0x18, 0x25, 0x62, 0x5f,
+ 0xec, 0xd1, 0x96, 0xab, 0xed, 0xd0, 0x97, 0xaa, 0x19, 0x24,
+ 0x63, 0x5e, 0xef, 0xd2, 0x95, 0xa8, 0x1b, 0x26, 0x61, 0x5c,
+ 0x1a, 0x27, 0x60, 0x5d, 0xee, 0xd3, 0x94, 0xa9, 0x10, 0x2d,
+ 0x6a, 0x57, 0xe4, 0xd9, 0x9e, 0xa3, 0xe5, 0xd8, 0x9f, 0xa2,
+ 0x11, 0x2c, 0x6b, 0x56, 0xe7, 0xda, 0x9d, 0xa0, 0x13, 0x2e,
+ 0x69, 0x54, 0x12, 0x2f, 0x68, 0x55, 0xe6, 0xdb, 0x9c, 0xa1,
+ 0xe3, 0xde, 0x99, 0xa4, 0x17, 0x2a, 0x6d, 0x50, 0x16, 0x2b,
+ 0x6c, 0x51, 0xe2, 0xdf, 0x98, 0xa5, 0x14, 0x29, 0x6e, 0x53,
+ 0xe0, 0xdd, 0x9a, 0xa7, 0xe1, 0xdc, 0x9b, 0xa6, 0x15, 0x28,
+ 0x6f, 0x52, 0x00, 0x3e, 0x7c, 0x42, 0xf8, 0xc6, 0x84, 0xba,
+ 0xed, 0xd3, 0x91, 0xaf, 0x15, 0x2b, 0x69, 0x57, 0xc7, 0xf9,
+ 0xbb, 0x85, 0x3f, 0x01, 0x43, 0x7d, 0x2a, 0x14, 0x56, 0x68,
+ 0xd2, 0xec, 0xae, 0x90, 0x93, 0xad, 0xef, 0xd1, 0x6b, 0x55,
+ 0x17, 0x29, 0x7e, 0x40, 0x02, 0x3c, 0x86, 0xb8, 0xfa, 0xc4,
+ 0x54, 0x6a, 0x28, 0x16, 0xac, 0x92, 0xd0, 0xee, 0xb9, 0x87,
+ 0xc5, 0xfb, 0x41, 0x7f, 0x3d, 0x03, 0x3b, 0x05, 0x47, 0x79,
+ 0xc3, 0xfd, 0xbf, 0x81, 0xd6, 0xe8, 0xaa, 0x94, 0x2e, 0x10,
+ 0x52, 0x6c, 0xfc, 0xc2, 0x80, 0xbe, 0x04, 0x3a, 0x78, 0x46,
+ 0x11, 0x2f, 0x6d, 0x53, 0xe9, 0xd7, 0x95, 0xab, 0xa8, 0x96,
+ 0xd4, 0xea, 0x50, 0x6e, 0x2c, 0x12, 0x45, 0x7b, 0x39, 0x07,
+ 0xbd, 0x83, 0xc1, 0xff, 0x6f, 0x51, 0x13, 0x2d, 0x97, 0xa9,
+ 0xeb, 0xd5, 0x82, 0xbc, 0xfe, 0xc0, 0x7a, 0x44, 0x06, 0x38,
+ 0x76, 0x48, 0x0a, 0x34, 0x8e, 0xb0, 0xf2, 0xcc, 0x9b, 0xa5,
+ 0xe7, 0xd9, 0x63, 0x5d, 0x1f, 0x21, 0xb1, 0x8f, 0xcd, 0xf3,
+ 0x49, 0x77, 0x35, 0x0b, 0x5c, 0x62, 0x20, 0x1e, 0xa4, 0x9a,
+ 0xd8, 0xe6, 0xe5, 0xdb, 0x99, 0xa7, 0x1d, 0x23, 0x61, 0x5f,
+ 0x08, 0x36, 0x74, 0x4a, 0xf0, 0xce, 0x8c, 0xb2, 0x22, 0x1c,
+ 0x5e, 0x60, 0xda, 0xe4, 0xa6, 0x98, 0xcf, 0xf1, 0xb3, 0x8d,
+ 0x37, 0x09, 0x4b, 0x75, 0x4d, 0x73, 0x31, 0x0f, 0xb5, 0x8b,
+ 0xc9, 0xf7, 0xa0, 0x9e, 0xdc, 0xe2, 0x58, 0x66, 0x24, 0x1a,
+ 0x8a, 0xb4, 0xf6, 0xc8, 0x72, 0x4c, 0x0e, 0x30, 0x67, 0x59,
+ 0x1b, 0x25, 0x9f, 0xa1, 0xe3, 0xdd, 0xde, 0xe0, 0xa2, 0x9c,
+ 0x26, 0x18, 0x5a, 0x64, 0x33, 0x0d, 0x4f, 0x71, 0xcb, 0xf5,
+ 0xb7, 0x89, 0x19, 0x27, 0x65, 0x5b, 0xe1, 0xdf, 0x9d, 0xa3,
+ 0xf4, 0xca, 0x88, 0xb6, 0x0c, 0x32, 0x70, 0x4e, 0x00, 0x3f,
+ 0x7e, 0x41, 0xfc, 0xc3, 0x82, 0xbd, 0xe5, 0xda, 0x9b, 0xa4,
+ 0x19, 0x26, 0x67, 0x58, 0xd7, 0xe8, 0xa9, 0x96, 0x2b, 0x14,
+ 0x55, 0x6a, 0x32, 0x0d, 0x4c, 0x73, 0xce, 0xf1, 0xb0, 0x8f,
+ 0xb3, 0x8c, 0xcd, 0xf2, 0x4f, 0x70, 0x31, 0x0e, 0x56, 0x69,
+ 0x28, 0x17, 0xaa, 0x95, 0xd4, 0xeb, 0x64, 0x5b, 0x1a, 0x25,
+ 0x98, 0xa7, 0xe6, 0xd9, 0x81, 0xbe, 0xff, 0xc0, 0x7d, 0x42,
+ 0x03, 0x3c, 0x7b, 0x44, 0x05, 0x3a, 0x87, 0xb8, 0xf9, 0xc6,
+ 0x9e, 0xa1, 0xe0, 0xdf, 0x62, 0x5d, 0x1c, 0x23, 0xac, 0x93,
+ 0xd2, 0xed, 0x50, 0x6f, 0x2e, 0x11, 0x49, 0x76, 0x37, 0x08,
+ 0xb5, 0x8a, 0xcb, 0xf4, 0xc8, 0xf7, 0xb6, 0x89, 0x34, 0x0b,
+ 0x4a, 0x75, 0x2d, 0x12, 0x53, 0x6c, 0xd1, 0xee, 0xaf, 0x90,
+ 0x1f, 0x20, 0x61, 0x5e, 0xe3, 0xdc, 0x9d, 0xa2, 0xfa, 0xc5,
+ 0x84, 0xbb, 0x06, 0x39, 0x78, 0x47, 0xf6, 0xc9, 0x88, 0xb7,
+ 0x0a, 0x35, 0x74, 0x4b, 0x13, 0x2c, 0x6d, 0x52, 0xef, 0xd0,
+ 0x91, 0xae, 0x21, 0x1e, 0x5f, 0x60, 0xdd, 0xe2, 0xa3, 0x9c,
+ 0xc4, 0xfb, 0xba, 0x85, 0x38, 0x07, 0x46, 0x79, 0x45, 0x7a,
+ 0x3b, 0x04, 0xb9, 0x86, 0xc7, 0xf8, 0xa0, 0x9f, 0xde, 0xe1,
+ 0x5c, 0x63, 0x22, 0x1d, 0x92, 0xad, 0xec, 0xd3, 0x6e, 0x51,
+ 0x10, 0x2f, 0x77, 0x48, 0x09, 0x36, 0x8b, 0xb4, 0xf5, 0xca,
+ 0x8d, 0xb2, 0xf3, 0xcc, 0x71, 0x4e, 0x0f, 0x30, 0x68, 0x57,
+ 0x16, 0x29, 0x94, 0xab, 0xea, 0xd5, 0x5a, 0x65, 0x24, 0x1b,
+ 0xa6, 0x99, 0xd8, 0xe7, 0xbf, 0x80, 0xc1, 0xfe, 0x43, 0x7c,
+ 0x3d, 0x02, 0x3e, 0x01, 0x40, 0x7f, 0xc2, 0xfd, 0xbc, 0x83,
+ 0xdb, 0xe4, 0xa5, 0x9a, 0x27, 0x18, 0x59, 0x66, 0xe9, 0xd6,
+ 0x97, 0xa8, 0x15, 0x2a, 0x6b, 0x54, 0x0c, 0x33, 0x72, 0x4d,
+ 0xf0, 0xcf, 0x8e, 0xb1, 0x00, 0x40, 0x80, 0xc0, 0x1d, 0x5d,
+ 0x9d, 0xdd, 0x3a, 0x7a, 0xba, 0xfa, 0x27, 0x67, 0xa7, 0xe7,
+ 0x74, 0x34, 0xf4, 0xb4, 0x69, 0x29, 0xe9, 0xa9, 0x4e, 0x0e,
+ 0xce, 0x8e, 0x53, 0x13, 0xd3, 0x93, 0xe8, 0xa8, 0x68, 0x28,
+ 0xf5, 0xb5, 0x75, 0x35, 0xd2, 0x92, 0x52, 0x12, 0xcf, 0x8f,
+ 0x4f, 0x0f, 0x9c, 0xdc, 0x1c, 0x5c, 0x81, 0xc1, 0x01, 0x41,
+ 0xa6, 0xe6, 0x26, 0x66, 0xbb, 0xfb, 0x3b, 0x7b, 0xcd, 0x8d,
+ 0x4d, 0x0d, 0xd0, 0x90, 0x50, 0x10, 0xf7, 0xb7, 0x77, 0x37,
+ 0xea, 0xaa, 0x6a, 0x2a, 0xb9, 0xf9, 0x39, 0x79, 0xa4, 0xe4,
+ 0x24, 0x64, 0x83, 0xc3, 0x03, 0x43, 0x9e, 0xde, 0x1e, 0x5e,
+ 0x25, 0x65, 0xa5, 0xe5, 0x38, 0x78, 0xb8, 0xf8, 0x1f, 0x5f,
+ 0x9f, 0xdf, 0x02, 0x42, 0x82, 0xc2, 0x51, 0x11, 0xd1, 0x91,
+ 0x4c, 0x0c, 0xcc, 0x8c, 0x6b, 0x2b, 0xeb, 0xab, 0x76, 0x36,
+ 0xf6, 0xb6, 0x87, 0xc7, 0x07, 0x47, 0x9a, 0xda, 0x1a, 0x5a,
+ 0xbd, 0xfd, 0x3d, 0x7d, 0xa0, 0xe0, 0x20, 0x60, 0xf3, 0xb3,
+ 0x73, 0x33, 0xee, 0xae, 0x6e, 0x2e, 0xc9, 0x89, 0x49, 0x09,
+ 0xd4, 0x94, 0x54, 0x14, 0x6f, 0x2f, 0xef, 0xaf, 0x72, 0x32,
+ 0xf2, 0xb2, 0x55, 0x15, 0xd5, 0x95, 0x48, 0x08, 0xc8, 0x88,
+ 0x1b, 0x5b, 0x9b, 0xdb, 0x06, 0x46, 0x86, 0xc6, 0x21, 0x61,
+ 0xa1, 0xe1, 0x3c, 0x7c, 0xbc, 0xfc, 0x4a, 0x0a, 0xca, 0x8a,
+ 0x57, 0x17, 0xd7, 0x97, 0x70, 0x30, 0xf0, 0xb0, 0x6d, 0x2d,
+ 0xed, 0xad, 0x3e, 0x7e, 0xbe, 0xfe, 0x23, 0x63, 0xa3, 0xe3,
+ 0x04, 0x44, 0x84, 0xc4, 0x19, 0x59, 0x99, 0xd9, 0xa2, 0xe2,
+ 0x22, 0x62, 0xbf, 0xff, 0x3f, 0x7f, 0x98, 0xd8, 0x18, 0x58,
+ 0x85, 0xc5, 0x05, 0x45, 0xd6, 0x96, 0x56, 0x16, 0xcb, 0x8b,
+ 0x4b, 0x0b, 0xec, 0xac, 0x6c, 0x2c, 0xf1, 0xb1, 0x71, 0x31,
+ 0x00, 0x41, 0x82, 0xc3, 0x19, 0x58, 0x9b, 0xda, 0x32, 0x73,
+ 0xb0, 0xf1, 0x2b, 0x6a, 0xa9, 0xe8, 0x64, 0x25, 0xe6, 0xa7,
+ 0x7d, 0x3c, 0xff, 0xbe, 0x56, 0x17, 0xd4, 0x95, 0x4f, 0x0e,
+ 0xcd, 0x8c, 0xc8, 0x89, 0x4a, 0x0b, 0xd1, 0x90, 0x53, 0x12,
+ 0xfa, 0xbb, 0x78, 0x39, 0xe3, 0xa2, 0x61, 0x20, 0xac, 0xed,
+ 0x2e, 0x6f, 0xb5, 0xf4, 0x37, 0x76, 0x9e, 0xdf, 0x1c, 0x5d,
+ 0x87, 0xc6, 0x05, 0x44, 0x8d, 0xcc, 0x0f, 0x4e, 0x94, 0xd5,
+ 0x16, 0x57, 0xbf, 0xfe, 0x3d, 0x7c, 0xa6, 0xe7, 0x24, 0x65,
+ 0xe9, 0xa8, 0x6b, 0x2a, 0xf0, 0xb1, 0x72, 0x33, 0xdb, 0x9a,
+ 0x59, 0x18, 0xc2, 0x83, 0x40, 0x01, 0x45, 0x04, 0xc7, 0x86,
+ 0x5c, 0x1d, 0xde, 0x9f, 0x77, 0x36, 0xf5, 0xb4, 0x6e, 0x2f,
+ 0xec, 0xad, 0x21, 0x60, 0xa3, 0xe2, 0x38, 0x79, 0xba, 0xfb,
+ 0x13, 0x52, 0x91, 0xd0, 0x0a, 0x4b, 0x88, 0xc9, 0x07, 0x46,
+ 0x85, 0xc4, 0x1e, 0x5f, 0x9c, 0xdd, 0x35, 0x74, 0xb7, 0xf6,
+ 0x2c, 0x6d, 0xae, 0xef, 0x63, 0x22, 0xe1, 0xa0, 0x7a, 0x3b,
+ 0xf8, 0xb9, 0x51, 0x10, 0xd3, 0x92, 0x48, 0x09, 0xca, 0x8b,
+ 0xcf, 0x8e, 0x4d, 0x0c, 0xd6, 0x97, 0x54, 0x15, 0xfd, 0xbc,
+ 0x7f, 0x3e, 0xe4, 0xa5, 0x66, 0x27, 0xab, 0xea, 0x29, 0x68,
+ 0xb2, 0xf3, 0x30, 0x71, 0x99, 0xd8, 0x1b, 0x5a, 0x80, 0xc1,
+ 0x02, 0x43, 0x8a, 0xcb, 0x08, 0x49, 0x93, 0xd2, 0x11, 0x50,
+ 0xb8, 0xf9, 0x3a, 0x7b, 0xa1, 0xe0, 0x23, 0x62, 0xee, 0xaf,
+ 0x6c, 0x2d, 0xf7, 0xb6, 0x75, 0x34, 0xdc, 0x9d, 0x5e, 0x1f,
+ 0xc5, 0x84, 0x47, 0x06, 0x42, 0x03, 0xc0, 0x81, 0x5b, 0x1a,
+ 0xd9, 0x98, 0x70, 0x31, 0xf2, 0xb3, 0x69, 0x28, 0xeb, 0xaa,
+ 0x26, 0x67, 0xa4, 0xe5, 0x3f, 0x7e, 0xbd, 0xfc, 0x14, 0x55,
+ 0x96, 0xd7, 0x0d, 0x4c, 0x8f, 0xce, 0x00, 0x42, 0x84, 0xc6,
+ 0x15, 0x57, 0x91, 0xd3, 0x2a, 0x68, 0xae, 0xec, 0x3f, 0x7d,
+ 0xbb, 0xf9, 0x54, 0x16, 0xd0, 0x92, 0x41, 0x03, 0xc5, 0x87,
+ 0x7e, 0x3c, 0xfa, 0xb8, 0x6b, 0x29, 0xef, 0xad, 0xa8, 0xea,
+ 0x2c, 0x6e, 0xbd, 0xff, 0x39, 0x7b, 0x82, 0xc0, 0x06, 0x44,
+ 0x97, 0xd5, 0x13, 0x51, 0xfc, 0xbe, 0x78, 0x3a, 0xe9, 0xab,
+ 0x6d, 0x2f, 0xd6, 0x94, 0x52, 0x10, 0xc3, 0x81, 0x47, 0x05,
+ 0x4d, 0x0f, 0xc9, 0x8b, 0x58, 0x1a, 0xdc, 0x9e, 0x67, 0x25,
+ 0xe3, 0xa1, 0x72, 0x30, 0xf6, 0xb4, 0x19, 0x5b, 0x9d, 0xdf,
+ 0x0c, 0x4e, 0x88, 0xca, 0x33, 0x71, 0xb7, 0xf5, 0x26, 0x64,
+ 0xa2, 0xe0, 0xe5, 0xa7, 0x61, 0x23, 0xf0, 0xb2, 0x74, 0x36,
+ 0xcf, 0x8d, 0x4b, 0x09, 0xda, 0x98, 0x5e, 0x1c, 0xb1, 0xf3,
+ 0x35, 0x77, 0xa4, 0xe6, 0x20, 0x62, 0x9b, 0xd9, 0x1f, 0x5d,
+ 0x8e, 0xcc, 0x0a, 0x48, 0x9a, 0xd8, 0x1e, 0x5c, 0x8f, 0xcd,
+ 0x0b, 0x49, 0xb0, 0xf2, 0x34, 0x76, 0xa5, 0xe7, 0x21, 0x63,
+ 0xce, 0x8c, 0x4a, 0x08, 0xdb, 0x99, 0x5f, 0x1d, 0xe4, 0xa6,
+ 0x60, 0x22, 0xf1, 0xb3, 0x75, 0x37, 0x32, 0x70, 0xb6, 0xf4,
+ 0x27, 0x65, 0xa3, 0xe1, 0x18, 0x5a, 0x9c, 0xde, 0x0d, 0x4f,
+ 0x89, 0xcb, 0x66, 0x24, 0xe2, 0xa0, 0x73, 0x31, 0xf7, 0xb5,
+ 0x4c, 0x0e, 0xc8, 0x8a, 0x59, 0x1b, 0xdd, 0x9f, 0xd7, 0x95,
+ 0x53, 0x11, 0xc2, 0x80, 0x46, 0x04, 0xfd, 0xbf, 0x79, 0x3b,
+ 0xe8, 0xaa, 0x6c, 0x2e, 0x83, 0xc1, 0x07, 0x45, 0x96, 0xd4,
+ 0x12, 0x50, 0xa9, 0xeb, 0x2d, 0x6f, 0xbc, 0xfe, 0x38, 0x7a,
+ 0x7f, 0x3d, 0xfb, 0xb9, 0x6a, 0x28, 0xee, 0xac, 0x55, 0x17,
+ 0xd1, 0x93, 0x40, 0x02, 0xc4, 0x86, 0x2b, 0x69, 0xaf, 0xed,
+ 0x3e, 0x7c, 0xba, 0xf8, 0x01, 0x43, 0x85, 0xc7, 0x14, 0x56,
+ 0x90, 0xd2, 0x00, 0x43, 0x86, 0xc5, 0x11, 0x52, 0x97, 0xd4,
+ 0x22, 0x61, 0xa4, 0xe7, 0x33, 0x70, 0xb5, 0xf6, 0x44, 0x07,
+ 0xc2, 0x81, 0x55, 0x16, 0xd3, 0x90, 0x66, 0x25, 0xe0, 0xa3,
+ 0x77, 0x34, 0xf1, 0xb2, 0x88, 0xcb, 0x0e, 0x4d, 0x99, 0xda,
+ 0x1f, 0x5c, 0xaa, 0xe9, 0x2c, 0x6f, 0xbb, 0xf8, 0x3d, 0x7e,
+ 0xcc, 0x8f, 0x4a, 0x09, 0xdd, 0x9e, 0x5b, 0x18, 0xee, 0xad,
+ 0x68, 0x2b, 0xff, 0xbc, 0x79, 0x3a, 0x0d, 0x4e, 0x8b, 0xc8,
+ 0x1c, 0x5f, 0x9a, 0xd9, 0x2f, 0x6c, 0xa9, 0xea, 0x3e, 0x7d,
+ 0xb8, 0xfb, 0x49, 0x0a, 0xcf, 0x8c, 0x58, 0x1b, 0xde, 0x9d,
+ 0x6b, 0x28, 0xed, 0xae, 0x7a, 0x39, 0xfc, 0xbf, 0x85, 0xc6,
+ 0x03, 0x40, 0x94, 0xd7, 0x12, 0x51, 0xa7, 0xe4, 0x21, 0x62,
+ 0xb6, 0xf5, 0x30, 0x73, 0xc1, 0x82, 0x47, 0x04, 0xd0, 0x93,
+ 0x56, 0x15, 0xe3, 0xa0, 0x65, 0x26, 0xf2, 0xb1, 0x74, 0x37,
+ 0x1a, 0x59, 0x9c, 0xdf, 0x0b, 0x48, 0x8d, 0xce, 0x38, 0x7b,
+ 0xbe, 0xfd, 0x29, 0x6a, 0xaf, 0xec, 0x5e, 0x1d, 0xd8, 0x9b,
+ 0x4f, 0x0c, 0xc9, 0x8a, 0x7c, 0x3f, 0xfa, 0xb9, 0x6d, 0x2e,
+ 0xeb, 0xa8, 0x92, 0xd1, 0x14, 0x57, 0x83, 0xc0, 0x05, 0x46,
+ 0xb0, 0xf3, 0x36, 0x75, 0xa1, 0xe2, 0x27, 0x64, 0xd6, 0x95,
+ 0x50, 0x13, 0xc7, 0x84, 0x41, 0x02, 0xf4, 0xb7, 0x72, 0x31,
+ 0xe5, 0xa6, 0x63, 0x20, 0x17, 0x54, 0x91, 0xd2, 0x06, 0x45,
+ 0x80, 0xc3, 0x35, 0x76, 0xb3, 0xf0, 0x24, 0x67, 0xa2, 0xe1,
+ 0x53, 0x10, 0xd5, 0x96, 0x42, 0x01, 0xc4, 0x87, 0x71, 0x32,
+ 0xf7, 0xb4, 0x60, 0x23, 0xe6, 0xa5, 0x9f, 0xdc, 0x19, 0x5a,
+ 0x8e, 0xcd, 0x08, 0x4b, 0xbd, 0xfe, 0x3b, 0x78, 0xac, 0xef,
+ 0x2a, 0x69, 0xdb, 0x98, 0x5d, 0x1e, 0xca, 0x89, 0x4c, 0x0f,
+ 0xf9, 0xba, 0x7f, 0x3c, 0xe8, 0xab, 0x6e, 0x2d, 0x00, 0x44,
+ 0x88, 0xcc, 0x0d, 0x49, 0x85, 0xc1, 0x1a, 0x5e, 0x92, 0xd6,
+ 0x17, 0x53, 0x9f, 0xdb, 0x34, 0x70, 0xbc, 0xf8, 0x39, 0x7d,
+ 0xb1, 0xf5, 0x2e, 0x6a, 0xa6, 0xe2, 0x23, 0x67, 0xab, 0xef,
+ 0x68, 0x2c, 0xe0, 0xa4, 0x65, 0x21, 0xed, 0xa9, 0x72, 0x36,
+ 0xfa, 0xbe, 0x7f, 0x3b, 0xf7, 0xb3, 0x5c, 0x18, 0xd4, 0x90,
+ 0x51, 0x15, 0xd9, 0x9d, 0x46, 0x02, 0xce, 0x8a, 0x4b, 0x0f,
+ 0xc3, 0x87, 0xd0, 0x94, 0x58, 0x1c, 0xdd, 0x99, 0x55, 0x11,
+ 0xca, 0x8e, 0x42, 0x06, 0xc7, 0x83, 0x4f, 0x0b, 0xe4, 0xa0,
+ 0x6c, 0x28, 0xe9, 0xad, 0x61, 0x25, 0xfe, 0xba, 0x76, 0x32,
+ 0xf3, 0xb7, 0x7b, 0x3f, 0xb8, 0xfc, 0x30, 0x74, 0xb5, 0xf1,
+ 0x3d, 0x79, 0xa2, 0xe6, 0x2a, 0x6e, 0xaf, 0xeb, 0x27, 0x63,
+ 0x8c, 0xc8, 0x04, 0x40, 0x81, 0xc5, 0x09, 0x4d, 0x96, 0xd2,
+ 0x1e, 0x5a, 0x9b, 0xdf, 0x13, 0x57, 0xbd, 0xf9, 0x35, 0x71,
+ 0xb0, 0xf4, 0x38, 0x7c, 0xa7, 0xe3, 0x2f, 0x6b, 0xaa, 0xee,
+ 0x22, 0x66, 0x89, 0xcd, 0x01, 0x45, 0x84, 0xc0, 0x0c, 0x48,
+ 0x93, 0xd7, 0x1b, 0x5f, 0x9e, 0xda, 0x16, 0x52, 0xd5, 0x91,
+ 0x5d, 0x19, 0xd8, 0x9c, 0x50, 0x14, 0xcf, 0x8b, 0x47, 0x03,
+ 0xc2, 0x86, 0x4a, 0x0e, 0xe1, 0xa5, 0x69, 0x2d, 0xec, 0xa8,
+ 0x64, 0x20, 0xfb, 0xbf, 0x73, 0x37, 0xf6, 0xb2, 0x7e, 0x3a,
+ 0x6d, 0x29, 0xe5, 0xa1, 0x60, 0x24, 0xe8, 0xac, 0x77, 0x33,
+ 0xff, 0xbb, 0x7a, 0x3e, 0xf2, 0xb6, 0x59, 0x1d, 0xd1, 0x95,
+ 0x54, 0x10, 0xdc, 0x98, 0x43, 0x07, 0xcb, 0x8f, 0x4e, 0x0a,
+ 0xc6, 0x82, 0x05, 0x41, 0x8d, 0xc9, 0x08, 0x4c, 0x80, 0xc4,
+ 0x1f, 0x5b, 0x97, 0xd3, 0x12, 0x56, 0x9a, 0xde, 0x31, 0x75,
+ 0xb9, 0xfd, 0x3c, 0x78, 0xb4, 0xf0, 0x2b, 0x6f, 0xa3, 0xe7,
+ 0x26, 0x62, 0xae, 0xea, 0x00, 0x45, 0x8a, 0xcf, 0x09, 0x4c,
+ 0x83, 0xc6, 0x12, 0x57, 0x98, 0xdd, 0x1b, 0x5e, 0x91, 0xd4,
+ 0x24, 0x61, 0xae, 0xeb, 0x2d, 0x68, 0xa7, 0xe2, 0x36, 0x73,
+ 0xbc, 0xf9, 0x3f, 0x7a, 0xb5, 0xf0, 0x48, 0x0d, 0xc2, 0x87,
+ 0x41, 0x04, 0xcb, 0x8e, 0x5a, 0x1f, 0xd0, 0x95, 0x53, 0x16,
+ 0xd9, 0x9c, 0x6c, 0x29, 0xe6, 0xa3, 0x65, 0x20, 0xef, 0xaa,
+ 0x7e, 0x3b, 0xf4, 0xb1, 0x77, 0x32, 0xfd, 0xb8, 0x90, 0xd5,
+ 0x1a, 0x5f, 0x99, 0xdc, 0x13, 0x56, 0x82, 0xc7, 0x08, 0x4d,
+ 0x8b, 0xce, 0x01, 0x44, 0xb4, 0xf1, 0x3e, 0x7b, 0xbd, 0xf8,
+ 0x37, 0x72, 0xa6, 0xe3, 0x2c, 0x69, 0xaf, 0xea, 0x25, 0x60,
+ 0xd8, 0x9d, 0x52, 0x17, 0xd1, 0x94, 0x5b, 0x1e, 0xca, 0x8f,
+ 0x40, 0x05, 0xc3, 0x86, 0x49, 0x0c, 0xfc, 0xb9, 0x76, 0x33,
+ 0xf5, 0xb0, 0x7f, 0x3a, 0xee, 0xab, 0x64, 0x21, 0xe7, 0xa2,
+ 0x6d, 0x28, 0x3d, 0x78, 0xb7, 0xf2, 0x34, 0x71, 0xbe, 0xfb,
+ 0x2f, 0x6a, 0xa5, 0xe0, 0x26, 0x63, 0xac, 0xe9, 0x19, 0x5c,
+ 0x93, 0xd6, 0x10, 0x55, 0x9a, 0xdf, 0x0b, 0x4e, 0x81, 0xc4,
+ 0x02, 0x47, 0x88, 0xcd, 0x75, 0x30, 0xff, 0xba, 0x7c, 0x39,
+ 0xf6, 0xb3, 0x67, 0x22, 0xed, 0xa8, 0x6e, 0x2b, 0xe4, 0xa1,
+ 0x51, 0x14, 0xdb, 0x9e, 0x58, 0x1d, 0xd2, 0x97, 0x43, 0x06,
+ 0xc9, 0x8c, 0x4a, 0x0f, 0xc0, 0x85, 0xad, 0xe8, 0x27, 0x62,
+ 0xa4, 0xe1, 0x2e, 0x6b, 0xbf, 0xfa, 0x35, 0x70, 0xb6, 0xf3,
+ 0x3c, 0x79, 0x89, 0xcc, 0x03, 0x46, 0x80, 0xc5, 0x0a, 0x4f,
+ 0x9b, 0xde, 0x11, 0x54, 0x92, 0xd7, 0x18, 0x5d, 0xe5, 0xa0,
+ 0x6f, 0x2a, 0xec, 0xa9, 0x66, 0x23, 0xf7, 0xb2, 0x7d, 0x38,
+ 0xfe, 0xbb, 0x74, 0x31, 0xc1, 0x84, 0x4b, 0x0e, 0xc8, 0x8d,
+ 0x42, 0x07, 0xd3, 0x96, 0x59, 0x1c, 0xda, 0x9f, 0x50, 0x15,
+ 0x00, 0x46, 0x8c, 0xca, 0x05, 0x43, 0x89, 0xcf, 0x0a, 0x4c,
+ 0x86, 0xc0, 0x0f, 0x49, 0x83, 0xc5, 0x14, 0x52, 0x98, 0xde,
+ 0x11, 0x57, 0x9d, 0xdb, 0x1e, 0x58, 0x92, 0xd4, 0x1b, 0x5d,
+ 0x97, 0xd1, 0x28, 0x6e, 0xa4, 0xe2, 0x2d, 0x6b, 0xa1, 0xe7,
+ 0x22, 0x64, 0xae, 0xe8, 0x27, 0x61, 0xab, 0xed, 0x3c, 0x7a,
+ 0xb0, 0xf6, 0x39, 0x7f, 0xb5, 0xf3, 0x36, 0x70, 0xba, 0xfc,
+ 0x33, 0x75, 0xbf, 0xf9, 0x50, 0x16, 0xdc, 0x9a, 0x55, 0x13,
+ 0xd9, 0x9f, 0x5a, 0x1c, 0xd6, 0x90, 0x5f, 0x19, 0xd3, 0x95,
+ 0x44, 0x02, 0xc8, 0x8e, 0x41, 0x07, 0xcd, 0x8b, 0x4e, 0x08,
+ 0xc2, 0x84, 0x4b, 0x0d, 0xc7, 0x81, 0x78, 0x3e, 0xf4, 0xb2,
+ 0x7d, 0x3b, 0xf1, 0xb7, 0x72, 0x34, 0xfe, 0xb8, 0x77, 0x31,
+ 0xfb, 0xbd, 0x6c, 0x2a, 0xe0, 0xa6, 0x69, 0x2f, 0xe5, 0xa3,
+ 0x66, 0x20, 0xea, 0xac, 0x63, 0x25, 0xef, 0xa9, 0xa0, 0xe6,
+ 0x2c, 0x6a, 0xa5, 0xe3, 0x29, 0x6f, 0xaa, 0xec, 0x26, 0x60,
+ 0xaf, 0xe9, 0x23, 0x65, 0xb4, 0xf2, 0x38, 0x7e, 0xb1, 0xf7,
+ 0x3d, 0x7b, 0xbe, 0xf8, 0x32, 0x74, 0xbb, 0xfd, 0x37, 0x71,
+ 0x88, 0xce, 0x04, 0x42, 0x8d, 0xcb, 0x01, 0x47, 0x82, 0xc4,
+ 0x0e, 0x48, 0x87, 0xc1, 0x0b, 0x4d, 0x9c, 0xda, 0x10, 0x56,
+ 0x99, 0xdf, 0x15, 0x53, 0x96, 0xd0, 0x1a, 0x5c, 0x93, 0xd5,
+ 0x1f, 0x59, 0xf0, 0xb6, 0x7c, 0x3a, 0xf5, 0xb3, 0x79, 0x3f,
+ 0xfa, 0xbc, 0x76, 0x30, 0xff, 0xb9, 0x73, 0x35, 0xe4, 0xa2,
+ 0x68, 0x2e, 0xe1, 0xa7, 0x6d, 0x2b, 0xee, 0xa8, 0x62, 0x24,
+ 0xeb, 0xad, 0x67, 0x21, 0xd8, 0x9e, 0x54, 0x12, 0xdd, 0x9b,
+ 0x51, 0x17, 0xd2, 0x94, 0x5e, 0x18, 0xd7, 0x91, 0x5b, 0x1d,
+ 0xcc, 0x8a, 0x40, 0x06, 0xc9, 0x8f, 0x45, 0x03, 0xc6, 0x80,
+ 0x4a, 0x0c, 0xc3, 0x85, 0x4f, 0x09, 0x00, 0x47, 0x8e, 0xc9,
+ 0x01, 0x46, 0x8f, 0xc8, 0x02, 0x45, 0x8c, 0xcb, 0x03, 0x44,
+ 0x8d, 0xca, 0x04, 0x43, 0x8a, 0xcd, 0x05, 0x42, 0x8b, 0xcc,
+ 0x06, 0x41, 0x88, 0xcf, 0x07, 0x40, 0x89, 0xce, 0x08, 0x4f,
+ 0x86, 0xc1, 0x09, 0x4e, 0x87, 0xc0, 0x0a, 0x4d, 0x84, 0xc3,
+ 0x0b, 0x4c, 0x85, 0xc2, 0x0c, 0x4b, 0x82, 0xc5, 0x0d, 0x4a,
+ 0x83, 0xc4, 0x0e, 0x49, 0x80, 0xc7, 0x0f, 0x48, 0x81, 0xc6,
+ 0x10, 0x57, 0x9e, 0xd9, 0x11, 0x56, 0x9f, 0xd8, 0x12, 0x55,
+ 0x9c, 0xdb, 0x13, 0x54, 0x9d, 0xda, 0x14, 0x53, 0x9a, 0xdd,
+ 0x15, 0x52, 0x9b, 0xdc, 0x16, 0x51, 0x98, 0xdf, 0x17, 0x50,
+ 0x99, 0xde, 0x18, 0x5f, 0x96, 0xd1, 0x19, 0x5e, 0x97, 0xd0,
+ 0x1a, 0x5d, 0x94, 0xd3, 0x1b, 0x5c, 0x95, 0xd2, 0x1c, 0x5b,
+ 0x92, 0xd5, 0x1d, 0x5a, 0x93, 0xd4, 0x1e, 0x59, 0x90, 0xd7,
+ 0x1f, 0x58, 0x91, 0xd6, 0x20, 0x67, 0xae, 0xe9, 0x21, 0x66,
+ 0xaf, 0xe8, 0x22, 0x65, 0xac, 0xeb, 0x23, 0x64, 0xad, 0xea,
+ 0x24, 0x63, 0xaa, 0xed, 0x25, 0x62, 0xab, 0xec, 0x26, 0x61,
+ 0xa8, 0xef, 0x27, 0x60, 0xa9, 0xee, 0x28, 0x6f, 0xa6, 0xe1,
+ 0x29, 0x6e, 0xa7, 0xe0, 0x2a, 0x6d, 0xa4, 0xe3, 0x2b, 0x6c,
+ 0xa5, 0xe2, 0x2c, 0x6b, 0xa2, 0xe5, 0x2d, 0x6a, 0xa3, 0xe4,
+ 0x2e, 0x69, 0xa0, 0xe7, 0x2f, 0x68, 0xa1, 0xe6, 0x30, 0x77,
+ 0xbe, 0xf9, 0x31, 0x76, 0xbf, 0xf8, 0x32, 0x75, 0xbc, 0xfb,
+ 0x33, 0x74, 0xbd, 0xfa, 0x34, 0x73, 0xba, 0xfd, 0x35, 0x72,
+ 0xbb, 0xfc, 0x36, 0x71, 0xb8, 0xff, 0x37, 0x70, 0xb9, 0xfe,
+ 0x38, 0x7f, 0xb6, 0xf1, 0x39, 0x7e, 0xb7, 0xf0, 0x3a, 0x7d,
+ 0xb4, 0xf3, 0x3b, 0x7c, 0xb5, 0xf2, 0x3c, 0x7b, 0xb2, 0xf5,
+ 0x3d, 0x7a, 0xb3, 0xf4, 0x3e, 0x79, 0xb0, 0xf7, 0x3f, 0x78,
+ 0xb1, 0xf6, 0x00, 0x48, 0x90, 0xd8, 0x3d, 0x75, 0xad, 0xe5,
+ 0x7a, 0x32, 0xea, 0xa2, 0x47, 0x0f, 0xd7, 0x9f, 0xf4, 0xbc,
+ 0x64, 0x2c, 0xc9, 0x81, 0x59, 0x11, 0x8e, 0xc6, 0x1e, 0x56,
+ 0xb3, 0xfb, 0x23, 0x6b, 0xf5, 0xbd, 0x65, 0x2d, 0xc8, 0x80,
+ 0x58, 0x10, 0x8f, 0xc7, 0x1f, 0x57, 0xb2, 0xfa, 0x22, 0x6a,
+ 0x01, 0x49, 0x91, 0xd9, 0x3c, 0x74, 0xac, 0xe4, 0x7b, 0x33,
+ 0xeb, 0xa3, 0x46, 0x0e, 0xd6, 0x9e, 0xf7, 0xbf, 0x67, 0x2f,
+ 0xca, 0x82, 0x5a, 0x12, 0x8d, 0xc5, 0x1d, 0x55, 0xb0, 0xf8,
+ 0x20, 0x68, 0x03, 0x4b, 0x93, 0xdb, 0x3e, 0x76, 0xae, 0xe6,
+ 0x79, 0x31, 0xe9, 0xa1, 0x44, 0x0c, 0xd4, 0x9c, 0x02, 0x4a,
+ 0x92, 0xda, 0x3f, 0x77, 0xaf, 0xe7, 0x78, 0x30, 0xe8, 0xa0,
+ 0x45, 0x0d, 0xd5, 0x9d, 0xf6, 0xbe, 0x66, 0x2e, 0xcb, 0x83,
+ 0x5b, 0x13, 0x8c, 0xc4, 0x1c, 0x54, 0xb1, 0xf9, 0x21, 0x69,
+ 0xf3, 0xbb, 0x63, 0x2b, 0xce, 0x86, 0x5e, 0x16, 0x89, 0xc1,
+ 0x19, 0x51, 0xb4, 0xfc, 0x24, 0x6c, 0x07, 0x4f, 0x97, 0xdf,
+ 0x3a, 0x72, 0xaa, 0xe2, 0x7d, 0x35, 0xed, 0xa5, 0x40, 0x08,
+ 0xd0, 0x98, 0x06, 0x4e, 0x96, 0xde, 0x3b, 0x73, 0xab, 0xe3,
+ 0x7c, 0x34, 0xec, 0xa4, 0x41, 0x09, 0xd1, 0x99, 0xf2, 0xba,
+ 0x62, 0x2a, 0xcf, 0x87, 0x5f, 0x17, 0x88, 0xc0, 0x18, 0x50,
+ 0xb5, 0xfd, 0x25, 0x6d, 0x04, 0x4c, 0x94, 0xdc, 0x39, 0x71,
+ 0xa9, 0xe1, 0x7e, 0x36, 0xee, 0xa6, 0x43, 0x0b, 0xd3, 0x9b,
+ 0xf0, 0xb8, 0x60, 0x28, 0xcd, 0x85, 0x5d, 0x15, 0x8a, 0xc2,
+ 0x1a, 0x52, 0xb7, 0xff, 0x27, 0x6f, 0xf1, 0xb9, 0x61, 0x29,
+ 0xcc, 0x84, 0x5c, 0x14, 0x8b, 0xc3, 0x1b, 0x53, 0xb6, 0xfe,
+ 0x26, 0x6e, 0x05, 0x4d, 0x95, 0xdd, 0x38, 0x70, 0xa8, 0xe0,
+ 0x7f, 0x37, 0xef, 0xa7, 0x42, 0x0a, 0xd2, 0x9a, 0x00, 0x49,
+ 0x92, 0xdb, 0x39, 0x70, 0xab, 0xe2, 0x72, 0x3b, 0xe0, 0xa9,
+ 0x4b, 0x02, 0xd9, 0x90, 0xe4, 0xad, 0x76, 0x3f, 0xdd, 0x94,
+ 0x4f, 0x06, 0x96, 0xdf, 0x04, 0x4d, 0xaf, 0xe6, 0x3d, 0x74,
+ 0xd5, 0x9c, 0x47, 0x0e, 0xec, 0xa5, 0x7e, 0x37, 0xa7, 0xee,
+ 0x35, 0x7c, 0x9e, 0xd7, 0x0c, 0x45, 0x31, 0x78, 0xa3, 0xea,
+ 0x08, 0x41, 0x9a, 0xd3, 0x43, 0x0a, 0xd1, 0x98, 0x7a, 0x33,
+ 0xe8, 0xa1, 0xb7, 0xfe, 0x25, 0x6c, 0x8e, 0xc7, 0x1c, 0x55,
+ 0xc5, 0x8c, 0x57, 0x1e, 0xfc, 0xb5, 0x6e, 0x27, 0x53, 0x1a,
+ 0xc1, 0x88, 0x6a, 0x23, 0xf8, 0xb1, 0x21, 0x68, 0xb3, 0xfa,
+ 0x18, 0x51, 0x8a, 0xc3, 0x62, 0x2b, 0xf0, 0xb9, 0x5b, 0x12,
+ 0xc9, 0x80, 0x10, 0x59, 0x82, 0xcb, 0x29, 0x60, 0xbb, 0xf2,
+ 0x86, 0xcf, 0x14, 0x5d, 0xbf, 0xf6, 0x2d, 0x64, 0xf4, 0xbd,
+ 0x66, 0x2f, 0xcd, 0x84, 0x5f, 0x16, 0x73, 0x3a, 0xe1, 0xa8,
+ 0x4a, 0x03, 0xd8, 0x91, 0x01, 0x48, 0x93, 0xda, 0x38, 0x71,
+ 0xaa, 0xe3, 0x97, 0xde, 0x05, 0x4c, 0xae, 0xe7, 0x3c, 0x75,
+ 0xe5, 0xac, 0x77, 0x3e, 0xdc, 0x95, 0x4e, 0x07, 0xa6, 0xef,
+ 0x34, 0x7d, 0x9f, 0xd6, 0x0d, 0x44, 0xd4, 0x9d, 0x46, 0x0f,
+ 0xed, 0xa4, 0x7f, 0x36, 0x42, 0x0b, 0xd0, 0x99, 0x7b, 0x32,
+ 0xe9, 0xa0, 0x30, 0x79, 0xa2, 0xeb, 0x09, 0x40, 0x9b, 0xd2,
+ 0xc4, 0x8d, 0x56, 0x1f, 0xfd, 0xb4, 0x6f, 0x26, 0xb6, 0xff,
+ 0x24, 0x6d, 0x8f, 0xc6, 0x1d, 0x54, 0x20, 0x69, 0xb2, 0xfb,
+ 0x19, 0x50, 0x8b, 0xc2, 0x52, 0x1b, 0xc0, 0x89, 0x6b, 0x22,
+ 0xf9, 0xb0, 0x11, 0x58, 0x83, 0xca, 0x28, 0x61, 0xba, 0xf3,
+ 0x63, 0x2a, 0xf1, 0xb8, 0x5a, 0x13, 0xc8, 0x81, 0xf5, 0xbc,
+ 0x67, 0x2e, 0xcc, 0x85, 0x5e, 0x17, 0x87, 0xce, 0x15, 0x5c,
+ 0xbe, 0xf7, 0x2c, 0x65, 0x00, 0x4a, 0x94, 0xde, 0x35, 0x7f,
+ 0xa1, 0xeb, 0x6a, 0x20, 0xfe, 0xb4, 0x5f, 0x15, 0xcb, 0x81,
+ 0xd4, 0x9e, 0x40, 0x0a, 0xe1, 0xab, 0x75, 0x3f, 0xbe, 0xf4,
+ 0x2a, 0x60, 0x8b, 0xc1, 0x1f, 0x55, 0xb5, 0xff, 0x21, 0x6b,
+ 0x80, 0xca, 0x14, 0x5e, 0xdf, 0x95, 0x4b, 0x01, 0xea, 0xa0,
+ 0x7e, 0x34, 0x61, 0x2b, 0xf5, 0xbf, 0x54, 0x1e, 0xc0, 0x8a,
+ 0x0b, 0x41, 0x9f, 0xd5, 0x3e, 0x74, 0xaa, 0xe0, 0x77, 0x3d,
+ 0xe3, 0xa9, 0x42, 0x08, 0xd6, 0x9c, 0x1d, 0x57, 0x89, 0xc3,
+ 0x28, 0x62, 0xbc, 0xf6, 0xa3, 0xe9, 0x37, 0x7d, 0x96, 0xdc,
+ 0x02, 0x48, 0xc9, 0x83, 0x5d, 0x17, 0xfc, 0xb6, 0x68, 0x22,
+ 0xc2, 0x88, 0x56, 0x1c, 0xf7, 0xbd, 0x63, 0x29, 0xa8, 0xe2,
+ 0x3c, 0x76, 0x9d, 0xd7, 0x09, 0x43, 0x16, 0x5c, 0x82, 0xc8,
+ 0x23, 0x69, 0xb7, 0xfd, 0x7c, 0x36, 0xe8, 0xa2, 0x49, 0x03,
+ 0xdd, 0x97, 0xee, 0xa4, 0x7a, 0x30, 0xdb, 0x91, 0x4f, 0x05,
+ 0x84, 0xce, 0x10, 0x5a, 0xb1, 0xfb, 0x25, 0x6f, 0x3a, 0x70,
+ 0xae, 0xe4, 0x0f, 0x45, 0x9b, 0xd1, 0x50, 0x1a, 0xc4, 0x8e,
+ 0x65, 0x2f, 0xf1, 0xbb, 0x5b, 0x11, 0xcf, 0x85, 0x6e, 0x24,
+ 0xfa, 0xb0, 0x31, 0x7b, 0xa5, 0xef, 0x04, 0x4e, 0x90, 0xda,
+ 0x8f, 0xc5, 0x1b, 0x51, 0xba, 0xf0, 0x2e, 0x64, 0xe5, 0xaf,
+ 0x71, 0x3b, 0xd0, 0x9a, 0x44, 0x0e, 0x99, 0xd3, 0x0d, 0x47,
+ 0xac, 0xe6, 0x38, 0x72, 0xf3, 0xb9, 0x67, 0x2d, 0xc6, 0x8c,
+ 0x52, 0x18, 0x4d, 0x07, 0xd9, 0x93, 0x78, 0x32, 0xec, 0xa6,
+ 0x27, 0x6d, 0xb3, 0xf9, 0x12, 0x58, 0x86, 0xcc, 0x2c, 0x66,
+ 0xb8, 0xf2, 0x19, 0x53, 0x8d, 0xc7, 0x46, 0x0c, 0xd2, 0x98,
+ 0x73, 0x39, 0xe7, 0xad, 0xf8, 0xb2, 0x6c, 0x26, 0xcd, 0x87,
+ 0x59, 0x13, 0x92, 0xd8, 0x06, 0x4c, 0xa7, 0xed, 0x33, 0x79,
+ 0x00, 0x4b, 0x96, 0xdd, 0x31, 0x7a, 0xa7, 0xec, 0x62, 0x29,
+ 0xf4, 0xbf, 0x53, 0x18, 0xc5, 0x8e, 0xc4, 0x8f, 0x52, 0x19,
+ 0xf5, 0xbe, 0x63, 0x28, 0xa6, 0xed, 0x30, 0x7b, 0x97, 0xdc,
+ 0x01, 0x4a, 0x95, 0xde, 0x03, 0x48, 0xa4, 0xef, 0x32, 0x79,
+ 0xf7, 0xbc, 0x61, 0x2a, 0xc6, 0x8d, 0x50, 0x1b, 0x51, 0x1a,
+ 0xc7, 0x8c, 0x60, 0x2b, 0xf6, 0xbd, 0x33, 0x78, 0xa5, 0xee,
+ 0x02, 0x49, 0x94, 0xdf, 0x37, 0x7c, 0xa1, 0xea, 0x06, 0x4d,
+ 0x90, 0xdb, 0x55, 0x1e, 0xc3, 0x88, 0x64, 0x2f, 0xf2, 0xb9,
+ 0xf3, 0xb8, 0x65, 0x2e, 0xc2, 0x89, 0x54, 0x1f, 0x91, 0xda,
+ 0x07, 0x4c, 0xa0, 0xeb, 0x36, 0x7d, 0xa2, 0xe9, 0x34, 0x7f,
+ 0x93, 0xd8, 0x05, 0x4e, 0xc0, 0x8b, 0x56, 0x1d, 0xf1, 0xba,
+ 0x67, 0x2c, 0x66, 0x2d, 0xf0, 0xbb, 0x57, 0x1c, 0xc1, 0x8a,
+ 0x04, 0x4f, 0x92, 0xd9, 0x35, 0x7e, 0xa3, 0xe8, 0x6e, 0x25,
+ 0xf8, 0xb3, 0x5f, 0x14, 0xc9, 0x82, 0x0c, 0x47, 0x9a, 0xd1,
+ 0x3d, 0x76, 0xab, 0xe0, 0xaa, 0xe1, 0x3c, 0x77, 0x9b, 0xd0,
+ 0x0d, 0x46, 0xc8, 0x83, 0x5e, 0x15, 0xf9, 0xb2, 0x6f, 0x24,
+ 0xfb, 0xb0, 0x6d, 0x26, 0xca, 0x81, 0x5c, 0x17, 0x99, 0xd2,
+ 0x0f, 0x44, 0xa8, 0xe3, 0x3e, 0x75, 0x3f, 0x74, 0xa9, 0xe2,
+ 0x0e, 0x45, 0x98, 0xd3, 0x5d, 0x16, 0xcb, 0x80, 0x6c, 0x27,
+ 0xfa, 0xb1, 0x59, 0x12, 0xcf, 0x84, 0x68, 0x23, 0xfe, 0xb5,
+ 0x3b, 0x70, 0xad, 0xe6, 0x0a, 0x41, 0x9c, 0xd7, 0x9d, 0xd6,
+ 0x0b, 0x40, 0xac, 0xe7, 0x3a, 0x71, 0xff, 0xb4, 0x69, 0x22,
+ 0xce, 0x85, 0x58, 0x13, 0xcc, 0x87, 0x5a, 0x11, 0xfd, 0xb6,
+ 0x6b, 0x20, 0xae, 0xe5, 0x38, 0x73, 0x9f, 0xd4, 0x09, 0x42,
+ 0x08, 0x43, 0x9e, 0xd5, 0x39, 0x72, 0xaf, 0xe4, 0x6a, 0x21,
+ 0xfc, 0xb7, 0x5b, 0x10, 0xcd, 0x86, 0x00, 0x4c, 0x98, 0xd4,
+ 0x2d, 0x61, 0xb5, 0xf9, 0x5a, 0x16, 0xc2, 0x8e, 0x77, 0x3b,
+ 0xef, 0xa3, 0xb4, 0xf8, 0x2c, 0x60, 0x99, 0xd5, 0x01, 0x4d,
+ 0xee, 0xa2, 0x76, 0x3a, 0xc3, 0x8f, 0x5b, 0x17, 0x75, 0x39,
+ 0xed, 0xa1, 0x58, 0x14, 0xc0, 0x8c, 0x2f, 0x63, 0xb7, 0xfb,
+ 0x02, 0x4e, 0x9a, 0xd6, 0xc1, 0x8d, 0x59, 0x15, 0xec, 0xa0,
+ 0x74, 0x38, 0x9b, 0xd7, 0x03, 0x4f, 0xb6, 0xfa, 0x2e, 0x62,
+ 0xea, 0xa6, 0x72, 0x3e, 0xc7, 0x8b, 0x5f, 0x13, 0xb0, 0xfc,
+ 0x28, 0x64, 0x9d, 0xd1, 0x05, 0x49, 0x5e, 0x12, 0xc6, 0x8a,
+ 0x73, 0x3f, 0xeb, 0xa7, 0x04, 0x48, 0x9c, 0xd0, 0x29, 0x65,
+ 0xb1, 0xfd, 0x9f, 0xd3, 0x07, 0x4b, 0xb2, 0xfe, 0x2a, 0x66,
+ 0xc5, 0x89, 0x5d, 0x11, 0xe8, 0xa4, 0x70, 0x3c, 0x2b, 0x67,
+ 0xb3, 0xff, 0x06, 0x4a, 0x9e, 0xd2, 0x71, 0x3d, 0xe9, 0xa5,
+ 0x5c, 0x10, 0xc4, 0x88, 0xc9, 0x85, 0x51, 0x1d, 0xe4, 0xa8,
+ 0x7c, 0x30, 0x93, 0xdf, 0x0b, 0x47, 0xbe, 0xf2, 0x26, 0x6a,
+ 0x7d, 0x31, 0xe5, 0xa9, 0x50, 0x1c, 0xc8, 0x84, 0x27, 0x6b,
+ 0xbf, 0xf3, 0x0a, 0x46, 0x92, 0xde, 0xbc, 0xf0, 0x24, 0x68,
+ 0x91, 0xdd, 0x09, 0x45, 0xe6, 0xaa, 0x7e, 0x32, 0xcb, 0x87,
+ 0x53, 0x1f, 0x08, 0x44, 0x90, 0xdc, 0x25, 0x69, 0xbd, 0xf1,
+ 0x52, 0x1e, 0xca, 0x86, 0x7f, 0x33, 0xe7, 0xab, 0x23, 0x6f,
+ 0xbb, 0xf7, 0x0e, 0x42, 0x96, 0xda, 0x79, 0x35, 0xe1, 0xad,
+ 0x54, 0x18, 0xcc, 0x80, 0x97, 0xdb, 0x0f, 0x43, 0xba, 0xf6,
+ 0x22, 0x6e, 0xcd, 0x81, 0x55, 0x19, 0xe0, 0xac, 0x78, 0x34,
+ 0x56, 0x1a, 0xce, 0x82, 0x7b, 0x37, 0xe3, 0xaf, 0x0c, 0x40,
+ 0x94, 0xd8, 0x21, 0x6d, 0xb9, 0xf5, 0xe2, 0xae, 0x7a, 0x36,
+ 0xcf, 0x83, 0x57, 0x1b, 0xb8, 0xf4, 0x20, 0x6c, 0x95, 0xd9,
+ 0x0d, 0x41, 0x00, 0x4d, 0x9a, 0xd7, 0x29, 0x64, 0xb3, 0xfe,
+ 0x52, 0x1f, 0xc8, 0x85, 0x7b, 0x36, 0xe1, 0xac, 0xa4, 0xe9,
+ 0x3e, 0x73, 0x8d, 0xc0, 0x17, 0x5a, 0xf6, 0xbb, 0x6c, 0x21,
+ 0xdf, 0x92, 0x45, 0x08, 0x55, 0x18, 0xcf, 0x82, 0x7c, 0x31,
+ 0xe6, 0xab, 0x07, 0x4a, 0x9d, 0xd0, 0x2e, 0x63, 0xb4, 0xf9,
+ 0xf1, 0xbc, 0x6b, 0x26, 0xd8, 0x95, 0x42, 0x0f, 0xa3, 0xee,
+ 0x39, 0x74, 0x8a, 0xc7, 0x10, 0x5d, 0xaa, 0xe7, 0x30, 0x7d,
+ 0x83, 0xce, 0x19, 0x54, 0xf8, 0xb5, 0x62, 0x2f, 0xd1, 0x9c,
+ 0x4b, 0x06, 0x0e, 0x43, 0x94, 0xd9, 0x27, 0x6a, 0xbd, 0xf0,
+ 0x5c, 0x11, 0xc6, 0x8b, 0x75, 0x38, 0xef, 0xa2, 0xff, 0xb2,
+ 0x65, 0x28, 0xd6, 0x9b, 0x4c, 0x01, 0xad, 0xe0, 0x37, 0x7a,
+ 0x84, 0xc9, 0x1e, 0x53, 0x5b, 0x16, 0xc1, 0x8c, 0x72, 0x3f,
+ 0xe8, 0xa5, 0x09, 0x44, 0x93, 0xde, 0x20, 0x6d, 0xba, 0xf7,
+ 0x49, 0x04, 0xd3, 0x9e, 0x60, 0x2d, 0xfa, 0xb7, 0x1b, 0x56,
+ 0x81, 0xcc, 0x32, 0x7f, 0xa8, 0xe5, 0xed, 0xa0, 0x77, 0x3a,
+ 0xc4, 0x89, 0x5e, 0x13, 0xbf, 0xf2, 0x25, 0x68, 0x96, 0xdb,
+ 0x0c, 0x41, 0x1c, 0x51, 0x86, 0xcb, 0x35, 0x78, 0xaf, 0xe2,
+ 0x4e, 0x03, 0xd4, 0x99, 0x67, 0x2a, 0xfd, 0xb0, 0xb8, 0xf5,
+ 0x22, 0x6f, 0x91, 0xdc, 0x0b, 0x46, 0xea, 0xa7, 0x70, 0x3d,
+ 0xc3, 0x8e, 0x59, 0x14, 0xe3, 0xae, 0x79, 0x34, 0xca, 0x87,
+ 0x50, 0x1d, 0xb1, 0xfc, 0x2b, 0x66, 0x98, 0xd5, 0x02, 0x4f,
+ 0x47, 0x0a, 0xdd, 0x90, 0x6e, 0x23, 0xf4, 0xb9, 0x15, 0x58,
+ 0x8f, 0xc2, 0x3c, 0x71, 0xa6, 0xeb, 0xb6, 0xfb, 0x2c, 0x61,
+ 0x9f, 0xd2, 0x05, 0x48, 0xe4, 0xa9, 0x7e, 0x33, 0xcd, 0x80,
+ 0x57, 0x1a, 0x12, 0x5f, 0x88, 0xc5, 0x3b, 0x76, 0xa1, 0xec,
+ 0x40, 0x0d, 0xda, 0x97, 0x69, 0x24, 0xf3, 0xbe, 0x00, 0x4e,
+ 0x9c, 0xd2, 0x25, 0x6b, 0xb9, 0xf7, 0x4a, 0x04, 0xd6, 0x98,
+ 0x6f, 0x21, 0xf3, 0xbd, 0x94, 0xda, 0x08, 0x46, 0xb1, 0xff,
+ 0x2d, 0x63, 0xde, 0x90, 0x42, 0x0c, 0xfb, 0xb5, 0x67, 0x29,
+ 0x35, 0x7b, 0xa9, 0xe7, 0x10, 0x5e, 0x8c, 0xc2, 0x7f, 0x31,
+ 0xe3, 0xad, 0x5a, 0x14, 0xc6, 0x88, 0xa1, 0xef, 0x3d, 0x73,
+ 0x84, 0xca, 0x18, 0x56, 0xeb, 0xa5, 0x77, 0x39, 0xce, 0x80,
+ 0x52, 0x1c, 0x6a, 0x24, 0xf6, 0xb8, 0x4f, 0x01, 0xd3, 0x9d,
+ 0x20, 0x6e, 0xbc, 0xf2, 0x05, 0x4b, 0x99, 0xd7, 0xfe, 0xb0,
+ 0x62, 0x2c, 0xdb, 0x95, 0x47, 0x09, 0xb4, 0xfa, 0x28, 0x66,
+ 0x91, 0xdf, 0x0d, 0x43, 0x5f, 0x11, 0xc3, 0x8d, 0x7a, 0x34,
+ 0xe6, 0xa8, 0x15, 0x5b, 0x89, 0xc7, 0x30, 0x7e, 0xac, 0xe2,
+ 0xcb, 0x85, 0x57, 0x19, 0xee, 0xa0, 0x72, 0x3c, 0x81, 0xcf,
+ 0x1d, 0x53, 0xa4, 0xea, 0x38, 0x76, 0xd4, 0x9a, 0x48, 0x06,
+ 0xf1, 0xbf, 0x6d, 0x23, 0x9e, 0xd0, 0x02, 0x4c, 0xbb, 0xf5,
+ 0x27, 0x69, 0x40, 0x0e, 0xdc, 0x92, 0x65, 0x2b, 0xf9, 0xb7,
+ 0x0a, 0x44, 0x96, 0xd8, 0x2f, 0x61, 0xb3, 0xfd, 0xe1, 0xaf,
+ 0x7d, 0x33, 0xc4, 0x8a, 0x58, 0x16, 0xab, 0xe5, 0x37, 0x79,
+ 0x8e, 0xc0, 0x12, 0x5c, 0x75, 0x3b, 0xe9, 0xa7, 0x50, 0x1e,
+ 0xcc, 0x82, 0x3f, 0x71, 0xa3, 0xed, 0x1a, 0x54, 0x86, 0xc8,
+ 0xbe, 0xf0, 0x22, 0x6c, 0x9b, 0xd5, 0x07, 0x49, 0xf4, 0xba,
+ 0x68, 0x26, 0xd1, 0x9f, 0x4d, 0x03, 0x2a, 0x64, 0xb6, 0xf8,
+ 0x0f, 0x41, 0x93, 0xdd, 0x60, 0x2e, 0xfc, 0xb2, 0x45, 0x0b,
+ 0xd9, 0x97, 0x8b, 0xc5, 0x17, 0x59, 0xae, 0xe0, 0x32, 0x7c,
+ 0xc1, 0x8f, 0x5d, 0x13, 0xe4, 0xaa, 0x78, 0x36, 0x1f, 0x51,
+ 0x83, 0xcd, 0x3a, 0x74, 0xa6, 0xe8, 0x55, 0x1b, 0xc9, 0x87,
+ 0x70, 0x3e, 0xec, 0xa2, 0x00, 0x4f, 0x9e, 0xd1, 0x21, 0x6e,
+ 0xbf, 0xf0, 0x42, 0x0d, 0xdc, 0x93, 0x63, 0x2c, 0xfd, 0xb2,
+ 0x84, 0xcb, 0x1a, 0x55, 0xa5, 0xea, 0x3b, 0x74, 0xc6, 0x89,
+ 0x58, 0x17, 0xe7, 0xa8, 0x79, 0x36, 0x15, 0x5a, 0x8b, 0xc4,
+ 0x34, 0x7b, 0xaa, 0xe5, 0x57, 0x18, 0xc9, 0x86, 0x76, 0x39,
+ 0xe8, 0xa7, 0x91, 0xde, 0x0f, 0x40, 0xb0, 0xff, 0x2e, 0x61,
+ 0xd3, 0x9c, 0x4d, 0x02, 0xf2, 0xbd, 0x6c, 0x23, 0x2a, 0x65,
+ 0xb4, 0xfb, 0x0b, 0x44, 0x95, 0xda, 0x68, 0x27, 0xf6, 0xb9,
+ 0x49, 0x06, 0xd7, 0x98, 0xae, 0xe1, 0x30, 0x7f, 0x8f, 0xc0,
+ 0x11, 0x5e, 0xec, 0xa3, 0x72, 0x3d, 0xcd, 0x82, 0x53, 0x1c,
+ 0x3f, 0x70, 0xa1, 0xee, 0x1e, 0x51, 0x80, 0xcf, 0x7d, 0x32,
+ 0xe3, 0xac, 0x5c, 0x13, 0xc2, 0x8d, 0xbb, 0xf4, 0x25, 0x6a,
+ 0x9a, 0xd5, 0x04, 0x4b, 0xf9, 0xb6, 0x67, 0x28, 0xd8, 0x97,
+ 0x46, 0x09, 0x54, 0x1b, 0xca, 0x85, 0x75, 0x3a, 0xeb, 0xa4,
+ 0x16, 0x59, 0x88, 0xc7, 0x37, 0x78, 0xa9, 0xe6, 0xd0, 0x9f,
+ 0x4e, 0x01, 0xf1, 0xbe, 0x6f, 0x20, 0x92, 0xdd, 0x0c, 0x43,
+ 0xb3, 0xfc, 0x2d, 0x62, 0x41, 0x0e, 0xdf, 0x90, 0x60, 0x2f,
+ 0xfe, 0xb1, 0x03, 0x4c, 0x9d, 0xd2, 0x22, 0x6d, 0xbc, 0xf3,
+ 0xc5, 0x8a, 0x5b, 0x14, 0xe4, 0xab, 0x7a, 0x35, 0x87, 0xc8,
+ 0x19, 0x56, 0xa6, 0xe9, 0x38, 0x77, 0x7e, 0x31, 0xe0, 0xaf,
+ 0x5f, 0x10, 0xc1, 0x8e, 0x3c, 0x73, 0xa2, 0xed, 0x1d, 0x52,
+ 0x83, 0xcc, 0xfa, 0xb5, 0x64, 0x2b, 0xdb, 0x94, 0x45, 0x0a,
+ 0xb8, 0xf7, 0x26, 0x69, 0x99, 0xd6, 0x07, 0x48, 0x6b, 0x24,
+ 0xf5, 0xba, 0x4a, 0x05, 0xd4, 0x9b, 0x29, 0x66, 0xb7, 0xf8,
+ 0x08, 0x47, 0x96, 0xd9, 0xef, 0xa0, 0x71, 0x3e, 0xce, 0x81,
+ 0x50, 0x1f, 0xad, 0xe2, 0x33, 0x7c, 0x8c, 0xc3, 0x12, 0x5d,
+ 0x00, 0x50, 0xa0, 0xf0, 0x5d, 0x0d, 0xfd, 0xad, 0xba, 0xea,
+ 0x1a, 0x4a, 0xe7, 0xb7, 0x47, 0x17, 0x69, 0x39, 0xc9, 0x99,
+ 0x34, 0x64, 0x94, 0xc4, 0xd3, 0x83, 0x73, 0x23, 0x8e, 0xde,
+ 0x2e, 0x7e, 0xd2, 0x82, 0x72, 0x22, 0x8f, 0xdf, 0x2f, 0x7f,
+ 0x68, 0x38, 0xc8, 0x98, 0x35, 0x65, 0x95, 0xc5, 0xbb, 0xeb,
+ 0x1b, 0x4b, 0xe6, 0xb6, 0x46, 0x16, 0x01, 0x51, 0xa1, 0xf1,
+ 0x5c, 0x0c, 0xfc, 0xac, 0xb9, 0xe9, 0x19, 0x49, 0xe4, 0xb4,
+ 0x44, 0x14, 0x03, 0x53, 0xa3, 0xf3, 0x5e, 0x0e, 0xfe, 0xae,
+ 0xd0, 0x80, 0x70, 0x20, 0x8d, 0xdd, 0x2d, 0x7d, 0x6a, 0x3a,
+ 0xca, 0x9a, 0x37, 0x67, 0x97, 0xc7, 0x6b, 0x3b, 0xcb, 0x9b,
+ 0x36, 0x66, 0x96, 0xc6, 0xd1, 0x81, 0x71, 0x21, 0x8c, 0xdc,
+ 0x2c, 0x7c, 0x02, 0x52, 0xa2, 0xf2, 0x5f, 0x0f, 0xff, 0xaf,
+ 0xb8, 0xe8, 0x18, 0x48, 0xe5, 0xb5, 0x45, 0x15, 0x6f, 0x3f,
+ 0xcf, 0x9f, 0x32, 0x62, 0x92, 0xc2, 0xd5, 0x85, 0x75, 0x25,
+ 0x88, 0xd8, 0x28, 0x78, 0x06, 0x56, 0xa6, 0xf6, 0x5b, 0x0b,
+ 0xfb, 0xab, 0xbc, 0xec, 0x1c, 0x4c, 0xe1, 0xb1, 0x41, 0x11,
+ 0xbd, 0xed, 0x1d, 0x4d, 0xe0, 0xb0, 0x40, 0x10, 0x07, 0x57,
+ 0xa7, 0xf7, 0x5a, 0x0a, 0xfa, 0xaa, 0xd4, 0x84, 0x74, 0x24,
+ 0x89, 0xd9, 0x29, 0x79, 0x6e, 0x3e, 0xce, 0x9e, 0x33, 0x63,
+ 0x93, 0xc3, 0xd6, 0x86, 0x76, 0x26, 0x8b, 0xdb, 0x2b, 0x7b,
+ 0x6c, 0x3c, 0xcc, 0x9c, 0x31, 0x61, 0x91, 0xc1, 0xbf, 0xef,
+ 0x1f, 0x4f, 0xe2, 0xb2, 0x42, 0x12, 0x05, 0x55, 0xa5, 0xf5,
+ 0x58, 0x08, 0xf8, 0xa8, 0x04, 0x54, 0xa4, 0xf4, 0x59, 0x09,
+ 0xf9, 0xa9, 0xbe, 0xee, 0x1e, 0x4e, 0xe3, 0xb3, 0x43, 0x13,
+ 0x6d, 0x3d, 0xcd, 0x9d, 0x30, 0x60, 0x90, 0xc0, 0xd7, 0x87,
+ 0x77, 0x27, 0x8a, 0xda, 0x2a, 0x7a, 0x00, 0x51, 0xa2, 0xf3,
+ 0x59, 0x08, 0xfb, 0xaa, 0xb2, 0xe3, 0x10, 0x41, 0xeb, 0xba,
+ 0x49, 0x18, 0x79, 0x28, 0xdb, 0x8a, 0x20, 0x71, 0x82, 0xd3,
+ 0xcb, 0x9a, 0x69, 0x38, 0x92, 0xc3, 0x30, 0x61, 0xf2, 0xa3,
+ 0x50, 0x01, 0xab, 0xfa, 0x09, 0x58, 0x40, 0x11, 0xe2, 0xb3,
+ 0x19, 0x48, 0xbb, 0xea, 0x8b, 0xda, 0x29, 0x78, 0xd2, 0x83,
+ 0x70, 0x21, 0x39, 0x68, 0x9b, 0xca, 0x60, 0x31, 0xc2, 0x93,
+ 0xf9, 0xa8, 0x5b, 0x0a, 0xa0, 0xf1, 0x02, 0x53, 0x4b, 0x1a,
+ 0xe9, 0xb8, 0x12, 0x43, 0xb0, 0xe1, 0x80, 0xd1, 0x22, 0x73,
+ 0xd9, 0x88, 0x7b, 0x2a, 0x32, 0x63, 0x90, 0xc1, 0x6b, 0x3a,
+ 0xc9, 0x98, 0x0b, 0x5a, 0xa9, 0xf8, 0x52, 0x03, 0xf0, 0xa1,
+ 0xb9, 0xe8, 0x1b, 0x4a, 0xe0, 0xb1, 0x42, 0x13, 0x72, 0x23,
+ 0xd0, 0x81, 0x2b, 0x7a, 0x89, 0xd8, 0xc0, 0x91, 0x62, 0x33,
+ 0x99, 0xc8, 0x3b, 0x6a, 0xef, 0xbe, 0x4d, 0x1c, 0xb6, 0xe7,
+ 0x14, 0x45, 0x5d, 0x0c, 0xff, 0xae, 0x04, 0x55, 0xa6, 0xf7,
+ 0x96, 0xc7, 0x34, 0x65, 0xcf, 0x9e, 0x6d, 0x3c, 0x24, 0x75,
+ 0x86, 0xd7, 0x7d, 0x2c, 0xdf, 0x8e, 0x1d, 0x4c, 0xbf, 0xee,
+ 0x44, 0x15, 0xe6, 0xb7, 0xaf, 0xfe, 0x0d, 0x5c, 0xf6, 0xa7,
+ 0x54, 0x05, 0x64, 0x35, 0xc6, 0x97, 0x3d, 0x6c, 0x9f, 0xce,
+ 0xd6, 0x87, 0x74, 0x25, 0x8f, 0xde, 0x2d, 0x7c, 0x16, 0x47,
+ 0xb4, 0xe5, 0x4f, 0x1e, 0xed, 0xbc, 0xa4, 0xf5, 0x06, 0x57,
+ 0xfd, 0xac, 0x5f, 0x0e, 0x6f, 0x3e, 0xcd, 0x9c, 0x36, 0x67,
+ 0x94, 0xc5, 0xdd, 0x8c, 0x7f, 0x2e, 0x84, 0xd5, 0x26, 0x77,
+ 0xe4, 0xb5, 0x46, 0x17, 0xbd, 0xec, 0x1f, 0x4e, 0x56, 0x07,
+ 0xf4, 0xa5, 0x0f, 0x5e, 0xad, 0xfc, 0x9d, 0xcc, 0x3f, 0x6e,
+ 0xc4, 0x95, 0x66, 0x37, 0x2f, 0x7e, 0x8d, 0xdc, 0x76, 0x27,
+ 0xd4, 0x85, 0x00, 0x52, 0xa4, 0xf6, 0x55, 0x07, 0xf1, 0xa3,
+ 0xaa, 0xf8, 0x0e, 0x5c, 0xff, 0xad, 0x5b, 0x09, 0x49, 0x1b,
+ 0xed, 0xbf, 0x1c, 0x4e, 0xb8, 0xea, 0xe3, 0xb1, 0x47, 0x15,
+ 0xb6, 0xe4, 0x12, 0x40, 0x92, 0xc0, 0x36, 0x64, 0xc7, 0x95,
+ 0x63, 0x31, 0x38, 0x6a, 0x9c, 0xce, 0x6d, 0x3f, 0xc9, 0x9b,
+ 0xdb, 0x89, 0x7f, 0x2d, 0x8e, 0xdc, 0x2a, 0x78, 0x71, 0x23,
+ 0xd5, 0x87, 0x24, 0x76, 0x80, 0xd2, 0x39, 0x6b, 0x9d, 0xcf,
+ 0x6c, 0x3e, 0xc8, 0x9a, 0x93, 0xc1, 0x37, 0x65, 0xc6, 0x94,
+ 0x62, 0x30, 0x70, 0x22, 0xd4, 0x86, 0x25, 0x77, 0x81, 0xd3,
+ 0xda, 0x88, 0x7e, 0x2c, 0x8f, 0xdd, 0x2b, 0x79, 0xab, 0xf9,
+ 0x0f, 0x5d, 0xfe, 0xac, 0x5a, 0x08, 0x01, 0x53, 0xa5, 0xf7,
+ 0x54, 0x06, 0xf0, 0xa2, 0xe2, 0xb0, 0x46, 0x14, 0xb7, 0xe5,
+ 0x13, 0x41, 0x48, 0x1a, 0xec, 0xbe, 0x1d, 0x4f, 0xb9, 0xeb,
+ 0x72, 0x20, 0xd6, 0x84, 0x27, 0x75, 0x83, 0xd1, 0xd8, 0x8a,
+ 0x7c, 0x2e, 0x8d, 0xdf, 0x29, 0x7b, 0x3b, 0x69, 0x9f, 0xcd,
+ 0x6e, 0x3c, 0xca, 0x98, 0x91, 0xc3, 0x35, 0x67, 0xc4, 0x96,
+ 0x60, 0x32, 0xe0, 0xb2, 0x44, 0x16, 0xb5, 0xe7, 0x11, 0x43,
+ 0x4a, 0x18, 0xee, 0xbc, 0x1f, 0x4d, 0xbb, 0xe9, 0xa9, 0xfb,
+ 0x0d, 0x5f, 0xfc, 0xae, 0x58, 0x0a, 0x03, 0x51, 0xa7, 0xf5,
+ 0x56, 0x04, 0xf2, 0xa0, 0x4b, 0x19, 0xef, 0xbd, 0x1e, 0x4c,
+ 0xba, 0xe8, 0xe1, 0xb3, 0x45, 0x17, 0xb4, 0xe6, 0x10, 0x42,
+ 0x02, 0x50, 0xa6, 0xf4, 0x57, 0x05, 0xf3, 0xa1, 0xa8, 0xfa,
+ 0x0c, 0x5e, 0xfd, 0xaf, 0x59, 0x0b, 0xd9, 0x8b, 0x7d, 0x2f,
+ 0x8c, 0xde, 0x28, 0x7a, 0x73, 0x21, 0xd7, 0x85, 0x26, 0x74,
+ 0x82, 0xd0, 0x90, 0xc2, 0x34, 0x66, 0xc5, 0x97, 0x61, 0x33,
+ 0x3a, 0x68, 0x9e, 0xcc, 0x6f, 0x3d, 0xcb, 0x99, 0x00, 0x53,
+ 0xa6, 0xf5, 0x51, 0x02, 0xf7, 0xa4, 0xa2, 0xf1, 0x04, 0x57,
+ 0xf3, 0xa0, 0x55, 0x06, 0x59, 0x0a, 0xff, 0xac, 0x08, 0x5b,
+ 0xae, 0xfd, 0xfb, 0xa8, 0x5d, 0x0e, 0xaa, 0xf9, 0x0c, 0x5f,
+ 0xb2, 0xe1, 0x14, 0x47, 0xe3, 0xb0, 0x45, 0x16, 0x10, 0x43,
+ 0xb6, 0xe5, 0x41, 0x12, 0xe7, 0xb4, 0xeb, 0xb8, 0x4d, 0x1e,
+ 0xba, 0xe9, 0x1c, 0x4f, 0x49, 0x1a, 0xef, 0xbc, 0x18, 0x4b,
+ 0xbe, 0xed, 0x79, 0x2a, 0xdf, 0x8c, 0x28, 0x7b, 0x8e, 0xdd,
+ 0xdb, 0x88, 0x7d, 0x2e, 0x8a, 0xd9, 0x2c, 0x7f, 0x20, 0x73,
+ 0x86, 0xd5, 0x71, 0x22, 0xd7, 0x84, 0x82, 0xd1, 0x24, 0x77,
+ 0xd3, 0x80, 0x75, 0x26, 0xcb, 0x98, 0x6d, 0x3e, 0x9a, 0xc9,
+ 0x3c, 0x6f, 0x69, 0x3a, 0xcf, 0x9c, 0x38, 0x6b, 0x9e, 0xcd,
+ 0x92, 0xc1, 0x34, 0x67, 0xc3, 0x90, 0x65, 0x36, 0x30, 0x63,
+ 0x96, 0xc5, 0x61, 0x32, 0xc7, 0x94, 0xf2, 0xa1, 0x54, 0x07,
+ 0xa3, 0xf0, 0x05, 0x56, 0x50, 0x03, 0xf6, 0xa5, 0x01, 0x52,
+ 0xa7, 0xf4, 0xab, 0xf8, 0x0d, 0x5e, 0xfa, 0xa9, 0x5c, 0x0f,
+ 0x09, 0x5a, 0xaf, 0xfc, 0x58, 0x0b, 0xfe, 0xad, 0x40, 0x13,
+ 0xe6, 0xb5, 0x11, 0x42, 0xb7, 0xe4, 0xe2, 0xb1, 0x44, 0x17,
+ 0xb3, 0xe0, 0x15, 0x46, 0x19, 0x4a, 0xbf, 0xec, 0x48, 0x1b,
+ 0xee, 0xbd, 0xbb, 0xe8, 0x1d, 0x4e, 0xea, 0xb9, 0x4c, 0x1f,
+ 0x8b, 0xd8, 0x2d, 0x7e, 0xda, 0x89, 0x7c, 0x2f, 0x29, 0x7a,
+ 0x8f, 0xdc, 0x78, 0x2b, 0xde, 0x8d, 0xd2, 0x81, 0x74, 0x27,
+ 0x83, 0xd0, 0x25, 0x76, 0x70, 0x23, 0xd6, 0x85, 0x21, 0x72,
+ 0x87, 0xd4, 0x39, 0x6a, 0x9f, 0xcc, 0x68, 0x3b, 0xce, 0x9d,
+ 0x9b, 0xc8, 0x3d, 0x6e, 0xca, 0x99, 0x6c, 0x3f, 0x60, 0x33,
+ 0xc6, 0x95, 0x31, 0x62, 0x97, 0xc4, 0xc2, 0x91, 0x64, 0x37,
+ 0x93, 0xc0, 0x35, 0x66, 0x00, 0x54, 0xa8, 0xfc, 0x4d, 0x19,
+ 0xe5, 0xb1, 0x9a, 0xce, 0x32, 0x66, 0xd7, 0x83, 0x7f, 0x2b,
+ 0x29, 0x7d, 0x81, 0xd5, 0x64, 0x30, 0xcc, 0x98, 0xb3, 0xe7,
+ 0x1b, 0x4f, 0xfe, 0xaa, 0x56, 0x02, 0x52, 0x06, 0xfa, 0xae,
+ 0x1f, 0x4b, 0xb7, 0xe3, 0xc8, 0x9c, 0x60, 0x34, 0x85, 0xd1,
+ 0x2d, 0x79, 0x7b, 0x2f, 0xd3, 0x87, 0x36, 0x62, 0x9e, 0xca,
+ 0xe1, 0xb5, 0x49, 0x1d, 0xac, 0xf8, 0x04, 0x50, 0xa4, 0xf0,
+ 0x0c, 0x58, 0xe9, 0xbd, 0x41, 0x15, 0x3e, 0x6a, 0x96, 0xc2,
+ 0x73, 0x27, 0xdb, 0x8f, 0x8d, 0xd9, 0x25, 0x71, 0xc0, 0x94,
+ 0x68, 0x3c, 0x17, 0x43, 0xbf, 0xeb, 0x5a, 0x0e, 0xf2, 0xa6,
+ 0xf6, 0xa2, 0x5e, 0x0a, 0xbb, 0xef, 0x13, 0x47, 0x6c, 0x38,
+ 0xc4, 0x90, 0x21, 0x75, 0x89, 0xdd, 0xdf, 0x8b, 0x77, 0x23,
+ 0x92, 0xc6, 0x3a, 0x6e, 0x45, 0x11, 0xed, 0xb9, 0x08, 0x5c,
+ 0xa0, 0xf4, 0x55, 0x01, 0xfd, 0xa9, 0x18, 0x4c, 0xb0, 0xe4,
+ 0xcf, 0x9b, 0x67, 0x33, 0x82, 0xd6, 0x2a, 0x7e, 0x7c, 0x28,
+ 0xd4, 0x80, 0x31, 0x65, 0x99, 0xcd, 0xe6, 0xb2, 0x4e, 0x1a,
+ 0xab, 0xff, 0x03, 0x57, 0x07, 0x53, 0xaf, 0xfb, 0x4a, 0x1e,
+ 0xe2, 0xb6, 0x9d, 0xc9, 0x35, 0x61, 0xd0, 0x84, 0x78, 0x2c,
+ 0x2e, 0x7a, 0x86, 0xd2, 0x63, 0x37, 0xcb, 0x9f, 0xb4, 0xe0,
+ 0x1c, 0x48, 0xf9, 0xad, 0x51, 0x05, 0xf1, 0xa5, 0x59, 0x0d,
+ 0xbc, 0xe8, 0x14, 0x40, 0x6b, 0x3f, 0xc3, 0x97, 0x26, 0x72,
+ 0x8e, 0xda, 0xd8, 0x8c, 0x70, 0x24, 0x95, 0xc1, 0x3d, 0x69,
+ 0x42, 0x16, 0xea, 0xbe, 0x0f, 0x5b, 0xa7, 0xf3, 0xa3, 0xf7,
+ 0x0b, 0x5f, 0xee, 0xba, 0x46, 0x12, 0x39, 0x6d, 0x91, 0xc5,
+ 0x74, 0x20, 0xdc, 0x88, 0x8a, 0xde, 0x22, 0x76, 0xc7, 0x93,
+ 0x6f, 0x3b, 0x10, 0x44, 0xb8, 0xec, 0x5d, 0x09, 0xf5, 0xa1,
+ 0x00, 0x55, 0xaa, 0xff, 0x49, 0x1c, 0xe3, 0xb6, 0x92, 0xc7,
+ 0x38, 0x6d, 0xdb, 0x8e, 0x71, 0x24, 0x39, 0x6c, 0x93, 0xc6,
+ 0x70, 0x25, 0xda, 0x8f, 0xab, 0xfe, 0x01, 0x54, 0xe2, 0xb7,
+ 0x48, 0x1d, 0x72, 0x27, 0xd8, 0x8d, 0x3b, 0x6e, 0x91, 0xc4,
+ 0xe0, 0xb5, 0x4a, 0x1f, 0xa9, 0xfc, 0x03, 0x56, 0x4b, 0x1e,
+ 0xe1, 0xb4, 0x02, 0x57, 0xa8, 0xfd, 0xd9, 0x8c, 0x73, 0x26,
+ 0x90, 0xc5, 0x3a, 0x6f, 0xe4, 0xb1, 0x4e, 0x1b, 0xad, 0xf8,
+ 0x07, 0x52, 0x76, 0x23, 0xdc, 0x89, 0x3f, 0x6a, 0x95, 0xc0,
+ 0xdd, 0x88, 0x77, 0x22, 0x94, 0xc1, 0x3e, 0x6b, 0x4f, 0x1a,
+ 0xe5, 0xb0, 0x06, 0x53, 0xac, 0xf9, 0x96, 0xc3, 0x3c, 0x69,
+ 0xdf, 0x8a, 0x75, 0x20, 0x04, 0x51, 0xae, 0xfb, 0x4d, 0x18,
+ 0xe7, 0xb2, 0xaf, 0xfa, 0x05, 0x50, 0xe6, 0xb3, 0x4c, 0x19,
+ 0x3d, 0x68, 0x97, 0xc2, 0x74, 0x21, 0xde, 0x8b, 0xd5, 0x80,
+ 0x7f, 0x2a, 0x9c, 0xc9, 0x36, 0x63, 0x47, 0x12, 0xed, 0xb8,
+ 0x0e, 0x5b, 0xa4, 0xf1, 0xec, 0xb9, 0x46, 0x13, 0xa5, 0xf0,
+ 0x0f, 0x5a, 0x7e, 0x2b, 0xd4, 0x81, 0x37, 0x62, 0x9d, 0xc8,
+ 0xa7, 0xf2, 0x0d, 0x58, 0xee, 0xbb, 0x44, 0x11, 0x35, 0x60,
+ 0x9f, 0xca, 0x7c, 0x29, 0xd6, 0x83, 0x9e, 0xcb, 0x34, 0x61,
+ 0xd7, 0x82, 0x7d, 0x28, 0x0c, 0x59, 0xa6, 0xf3, 0x45, 0x10,
+ 0xef, 0xba, 0x31, 0x64, 0x9b, 0xce, 0x78, 0x2d, 0xd2, 0x87,
+ 0xa3, 0xf6, 0x09, 0x5c, 0xea, 0xbf, 0x40, 0x15, 0x08, 0x5d,
+ 0xa2, 0xf7, 0x41, 0x14, 0xeb, 0xbe, 0x9a, 0xcf, 0x30, 0x65,
+ 0xd3, 0x86, 0x79, 0x2c, 0x43, 0x16, 0xe9, 0xbc, 0x0a, 0x5f,
+ 0xa0, 0xf5, 0xd1, 0x84, 0x7b, 0x2e, 0x98, 0xcd, 0x32, 0x67,
+ 0x7a, 0x2f, 0xd0, 0x85, 0x33, 0x66, 0x99, 0xcc, 0xe8, 0xbd,
+ 0x42, 0x17, 0xa1, 0xf4, 0x0b, 0x5e, 0x00, 0x56, 0xac, 0xfa,
+ 0x45, 0x13, 0xe9, 0xbf, 0x8a, 0xdc, 0x26, 0x70, 0xcf, 0x99,
+ 0x63, 0x35, 0x09, 0x5f, 0xa5, 0xf3, 0x4c, 0x1a, 0xe0, 0xb6,
+ 0x83, 0xd5, 0x2f, 0x79, 0xc6, 0x90, 0x6a, 0x3c, 0x12, 0x44,
+ 0xbe, 0xe8, 0x57, 0x01, 0xfb, 0xad, 0x98, 0xce, 0x34, 0x62,
+ 0xdd, 0x8b, 0x71, 0x27, 0x1b, 0x4d, 0xb7, 0xe1, 0x5e, 0x08,
+ 0xf2, 0xa4, 0x91, 0xc7, 0x3d, 0x6b, 0xd4, 0x82, 0x78, 0x2e,
+ 0x24, 0x72, 0x88, 0xde, 0x61, 0x37, 0xcd, 0x9b, 0xae, 0xf8,
+ 0x02, 0x54, 0xeb, 0xbd, 0x47, 0x11, 0x2d, 0x7b, 0x81, 0xd7,
+ 0x68, 0x3e, 0xc4, 0x92, 0xa7, 0xf1, 0x0b, 0x5d, 0xe2, 0xb4,
+ 0x4e, 0x18, 0x36, 0x60, 0x9a, 0xcc, 0x73, 0x25, 0xdf, 0x89,
+ 0xbc, 0xea, 0x10, 0x46, 0xf9, 0xaf, 0x55, 0x03, 0x3f, 0x69,
+ 0x93, 0xc5, 0x7a, 0x2c, 0xd6, 0x80, 0xb5, 0xe3, 0x19, 0x4f,
+ 0xf0, 0xa6, 0x5c, 0x0a, 0x48, 0x1e, 0xe4, 0xb2, 0x0d, 0x5b,
+ 0xa1, 0xf7, 0xc2, 0x94, 0x6e, 0x38, 0x87, 0xd1, 0x2b, 0x7d,
+ 0x41, 0x17, 0xed, 0xbb, 0x04, 0x52, 0xa8, 0xfe, 0xcb, 0x9d,
+ 0x67, 0x31, 0x8e, 0xd8, 0x22, 0x74, 0x5a, 0x0c, 0xf6, 0xa0,
+ 0x1f, 0x49, 0xb3, 0xe5, 0xd0, 0x86, 0x7c, 0x2a, 0x95, 0xc3,
+ 0x39, 0x6f, 0x53, 0x05, 0xff, 0xa9, 0x16, 0x40, 0xba, 0xec,
+ 0xd9, 0x8f, 0x75, 0x23, 0x9c, 0xca, 0x30, 0x66, 0x6c, 0x3a,
+ 0xc0, 0x96, 0x29, 0x7f, 0x85, 0xd3, 0xe6, 0xb0, 0x4a, 0x1c,
+ 0xa3, 0xf5, 0x0f, 0x59, 0x65, 0x33, 0xc9, 0x9f, 0x20, 0x76,
+ 0x8c, 0xda, 0xef, 0xb9, 0x43, 0x15, 0xaa, 0xfc, 0x06, 0x50,
+ 0x7e, 0x28, 0xd2, 0x84, 0x3b, 0x6d, 0x97, 0xc1, 0xf4, 0xa2,
+ 0x58, 0x0e, 0xb1, 0xe7, 0x1d, 0x4b, 0x77, 0x21, 0xdb, 0x8d,
+ 0x32, 0x64, 0x9e, 0xc8, 0xfd, 0xab, 0x51, 0x07, 0xb8, 0xee,
+ 0x14, 0x42, 0x00, 0x57, 0xae, 0xf9, 0x41, 0x16, 0xef, 0xb8,
+ 0x82, 0xd5, 0x2c, 0x7b, 0xc3, 0x94, 0x6d, 0x3a, 0x19, 0x4e,
+ 0xb7, 0xe0, 0x58, 0x0f, 0xf6, 0xa1, 0x9b, 0xcc, 0x35, 0x62,
+ 0xda, 0x8d, 0x74, 0x23, 0x32, 0x65, 0x9c, 0xcb, 0x73, 0x24,
+ 0xdd, 0x8a, 0xb0, 0xe7, 0x1e, 0x49, 0xf1, 0xa6, 0x5f, 0x08,
+ 0x2b, 0x7c, 0x85, 0xd2, 0x6a, 0x3d, 0xc4, 0x93, 0xa9, 0xfe,
+ 0x07, 0x50, 0xe8, 0xbf, 0x46, 0x11, 0x64, 0x33, 0xca, 0x9d,
+ 0x25, 0x72, 0x8b, 0xdc, 0xe6, 0xb1, 0x48, 0x1f, 0xa7, 0xf0,
+ 0x09, 0x5e, 0x7d, 0x2a, 0xd3, 0x84, 0x3c, 0x6b, 0x92, 0xc5,
+ 0xff, 0xa8, 0x51, 0x06, 0xbe, 0xe9, 0x10, 0x47, 0x56, 0x01,
+ 0xf8, 0xaf, 0x17, 0x40, 0xb9, 0xee, 0xd4, 0x83, 0x7a, 0x2d,
+ 0x95, 0xc2, 0x3b, 0x6c, 0x4f, 0x18, 0xe1, 0xb6, 0x0e, 0x59,
+ 0xa0, 0xf7, 0xcd, 0x9a, 0x63, 0x34, 0x8c, 0xdb, 0x22, 0x75,
+ 0xc8, 0x9f, 0x66, 0x31, 0x89, 0xde, 0x27, 0x70, 0x4a, 0x1d,
+ 0xe4, 0xb3, 0x0b, 0x5c, 0xa5, 0xf2, 0xd1, 0x86, 0x7f, 0x28,
+ 0x90, 0xc7, 0x3e, 0x69, 0x53, 0x04, 0xfd, 0xaa, 0x12, 0x45,
+ 0xbc, 0xeb, 0xfa, 0xad, 0x54, 0x03, 0xbb, 0xec, 0x15, 0x42,
+ 0x78, 0x2f, 0xd6, 0x81, 0x39, 0x6e, 0x97, 0xc0, 0xe3, 0xb4,
+ 0x4d, 0x1a, 0xa2, 0xf5, 0x0c, 0x5b, 0x61, 0x36, 0xcf, 0x98,
+ 0x20, 0x77, 0x8e, 0xd9, 0xac, 0xfb, 0x02, 0x55, 0xed, 0xba,
+ 0x43, 0x14, 0x2e, 0x79, 0x80, 0xd7, 0x6f, 0x38, 0xc1, 0x96,
+ 0xb5, 0xe2, 0x1b, 0x4c, 0xf4, 0xa3, 0x5a, 0x0d, 0x37, 0x60,
+ 0x99, 0xce, 0x76, 0x21, 0xd8, 0x8f, 0x9e, 0xc9, 0x30, 0x67,
+ 0xdf, 0x88, 0x71, 0x26, 0x1c, 0x4b, 0xb2, 0xe5, 0x5d, 0x0a,
+ 0xf3, 0xa4, 0x87, 0xd0, 0x29, 0x7e, 0xc6, 0x91, 0x68, 0x3f,
+ 0x05, 0x52, 0xab, 0xfc, 0x44, 0x13, 0xea, 0xbd, 0x00, 0x58,
+ 0xb0, 0xe8, 0x7d, 0x25, 0xcd, 0x95, 0xfa, 0xa2, 0x4a, 0x12,
+ 0x87, 0xdf, 0x37, 0x6f, 0xe9, 0xb1, 0x59, 0x01, 0x94, 0xcc,
+ 0x24, 0x7c, 0x13, 0x4b, 0xa3, 0xfb, 0x6e, 0x36, 0xde, 0x86,
+ 0xcf, 0x97, 0x7f, 0x27, 0xb2, 0xea, 0x02, 0x5a, 0x35, 0x6d,
+ 0x85, 0xdd, 0x48, 0x10, 0xf8, 0xa0, 0x26, 0x7e, 0x96, 0xce,
+ 0x5b, 0x03, 0xeb, 0xb3, 0xdc, 0x84, 0x6c, 0x34, 0xa1, 0xf9,
+ 0x11, 0x49, 0x83, 0xdb, 0x33, 0x6b, 0xfe, 0xa6, 0x4e, 0x16,
+ 0x79, 0x21, 0xc9, 0x91, 0x04, 0x5c, 0xb4, 0xec, 0x6a, 0x32,
+ 0xda, 0x82, 0x17, 0x4f, 0xa7, 0xff, 0x90, 0xc8, 0x20, 0x78,
+ 0xed, 0xb5, 0x5d, 0x05, 0x4c, 0x14, 0xfc, 0xa4, 0x31, 0x69,
+ 0x81, 0xd9, 0xb6, 0xee, 0x06, 0x5e, 0xcb, 0x93, 0x7b, 0x23,
+ 0xa5, 0xfd, 0x15, 0x4d, 0xd8, 0x80, 0x68, 0x30, 0x5f, 0x07,
+ 0xef, 0xb7, 0x22, 0x7a, 0x92, 0xca, 0x1b, 0x43, 0xab, 0xf3,
+ 0x66, 0x3e, 0xd6, 0x8e, 0xe1, 0xb9, 0x51, 0x09, 0x9c, 0xc4,
+ 0x2c, 0x74, 0xf2, 0xaa, 0x42, 0x1a, 0x8f, 0xd7, 0x3f, 0x67,
+ 0x08, 0x50, 0xb8, 0xe0, 0x75, 0x2d, 0xc5, 0x9d, 0xd4, 0x8c,
+ 0x64, 0x3c, 0xa9, 0xf1, 0x19, 0x41, 0x2e, 0x76, 0x9e, 0xc6,
+ 0x53, 0x0b, 0xe3, 0xbb, 0x3d, 0x65, 0x8d, 0xd5, 0x40, 0x18,
+ 0xf0, 0xa8, 0xc7, 0x9f, 0x77, 0x2f, 0xba, 0xe2, 0x0a, 0x52,
+ 0x98, 0xc0, 0x28, 0x70, 0xe5, 0xbd, 0x55, 0x0d, 0x62, 0x3a,
+ 0xd2, 0x8a, 0x1f, 0x47, 0xaf, 0xf7, 0x71, 0x29, 0xc1, 0x99,
+ 0x0c, 0x54, 0xbc, 0xe4, 0x8b, 0xd3, 0x3b, 0x63, 0xf6, 0xae,
+ 0x46, 0x1e, 0x57, 0x0f, 0xe7, 0xbf, 0x2a, 0x72, 0x9a, 0xc2,
+ 0xad, 0xf5, 0x1d, 0x45, 0xd0, 0x88, 0x60, 0x38, 0xbe, 0xe6,
+ 0x0e, 0x56, 0xc3, 0x9b, 0x73, 0x2b, 0x44, 0x1c, 0xf4, 0xac,
+ 0x39, 0x61, 0x89, 0xd1, 0x00, 0x59, 0xb2, 0xeb, 0x79, 0x20,
+ 0xcb, 0x92, 0xf2, 0xab, 0x40, 0x19, 0x8b, 0xd2, 0x39, 0x60,
+ 0xf9, 0xa0, 0x4b, 0x12, 0x80, 0xd9, 0x32, 0x6b, 0x0b, 0x52,
+ 0xb9, 0xe0, 0x72, 0x2b, 0xc0, 0x99, 0xef, 0xb6, 0x5d, 0x04,
+ 0x96, 0xcf, 0x24, 0x7d, 0x1d, 0x44, 0xaf, 0xf6, 0x64, 0x3d,
+ 0xd6, 0x8f, 0x16, 0x4f, 0xa4, 0xfd, 0x6f, 0x36, 0xdd, 0x84,
+ 0xe4, 0xbd, 0x56, 0x0f, 0x9d, 0xc4, 0x2f, 0x76, 0xc3, 0x9a,
+ 0x71, 0x28, 0xba, 0xe3, 0x08, 0x51, 0x31, 0x68, 0x83, 0xda,
+ 0x48, 0x11, 0xfa, 0xa3, 0x3a, 0x63, 0x88, 0xd1, 0x43, 0x1a,
+ 0xf1, 0xa8, 0xc8, 0x91, 0x7a, 0x23, 0xb1, 0xe8, 0x03, 0x5a,
+ 0x2c, 0x75, 0x9e, 0xc7, 0x55, 0x0c, 0xe7, 0xbe, 0xde, 0x87,
+ 0x6c, 0x35, 0xa7, 0xfe, 0x15, 0x4c, 0xd5, 0x8c, 0x67, 0x3e,
+ 0xac, 0xf5, 0x1e, 0x47, 0x27, 0x7e, 0x95, 0xcc, 0x5e, 0x07,
+ 0xec, 0xb5, 0x9b, 0xc2, 0x29, 0x70, 0xe2, 0xbb, 0x50, 0x09,
+ 0x69, 0x30, 0xdb, 0x82, 0x10, 0x49, 0xa2, 0xfb, 0x62, 0x3b,
+ 0xd0, 0x89, 0x1b, 0x42, 0xa9, 0xf0, 0x90, 0xc9, 0x22, 0x7b,
+ 0xe9, 0xb0, 0x5b, 0x02, 0x74, 0x2d, 0xc6, 0x9f, 0x0d, 0x54,
+ 0xbf, 0xe6, 0x86, 0xdf, 0x34, 0x6d, 0xff, 0xa6, 0x4d, 0x14,
+ 0x8d, 0xd4, 0x3f, 0x66, 0xf4, 0xad, 0x46, 0x1f, 0x7f, 0x26,
+ 0xcd, 0x94, 0x06, 0x5f, 0xb4, 0xed, 0x58, 0x01, 0xea, 0xb3,
+ 0x21, 0x78, 0x93, 0xca, 0xaa, 0xf3, 0x18, 0x41, 0xd3, 0x8a,
+ 0x61, 0x38, 0xa1, 0xf8, 0x13, 0x4a, 0xd8, 0x81, 0x6a, 0x33,
+ 0x53, 0x0a, 0xe1, 0xb8, 0x2a, 0x73, 0x98, 0xc1, 0xb7, 0xee,
+ 0x05, 0x5c, 0xce, 0x97, 0x7c, 0x25, 0x45, 0x1c, 0xf7, 0xae,
+ 0x3c, 0x65, 0x8e, 0xd7, 0x4e, 0x17, 0xfc, 0xa5, 0x37, 0x6e,
+ 0x85, 0xdc, 0xbc, 0xe5, 0x0e, 0x57, 0xc5, 0x9c, 0x77, 0x2e,
+ 0x00, 0x5a, 0xb4, 0xee, 0x75, 0x2f, 0xc1, 0x9b, 0xea, 0xb0,
+ 0x5e, 0x04, 0x9f, 0xc5, 0x2b, 0x71, 0xc9, 0x93, 0x7d, 0x27,
+ 0xbc, 0xe6, 0x08, 0x52, 0x23, 0x79, 0x97, 0xcd, 0x56, 0x0c,
+ 0xe2, 0xb8, 0x8f, 0xd5, 0x3b, 0x61, 0xfa, 0xa0, 0x4e, 0x14,
+ 0x65, 0x3f, 0xd1, 0x8b, 0x10, 0x4a, 0xa4, 0xfe, 0x46, 0x1c,
+ 0xf2, 0xa8, 0x33, 0x69, 0x87, 0xdd, 0xac, 0xf6, 0x18, 0x42,
+ 0xd9, 0x83, 0x6d, 0x37, 0x03, 0x59, 0xb7, 0xed, 0x76, 0x2c,
+ 0xc2, 0x98, 0xe9, 0xb3, 0x5d, 0x07, 0x9c, 0xc6, 0x28, 0x72,
+ 0xca, 0x90, 0x7e, 0x24, 0xbf, 0xe5, 0x0b, 0x51, 0x20, 0x7a,
+ 0x94, 0xce, 0x55, 0x0f, 0xe1, 0xbb, 0x8c, 0xd6, 0x38, 0x62,
+ 0xf9, 0xa3, 0x4d, 0x17, 0x66, 0x3c, 0xd2, 0x88, 0x13, 0x49,
+ 0xa7, 0xfd, 0x45, 0x1f, 0xf1, 0xab, 0x30, 0x6a, 0x84, 0xde,
+ 0xaf, 0xf5, 0x1b, 0x41, 0xda, 0x80, 0x6e, 0x34, 0x06, 0x5c,
+ 0xb2, 0xe8, 0x73, 0x29, 0xc7, 0x9d, 0xec, 0xb6, 0x58, 0x02,
+ 0x99, 0xc3, 0x2d, 0x77, 0xcf, 0x95, 0x7b, 0x21, 0xba, 0xe0,
+ 0x0e, 0x54, 0x25, 0x7f, 0x91, 0xcb, 0x50, 0x0a, 0xe4, 0xbe,
+ 0x89, 0xd3, 0x3d, 0x67, 0xfc, 0xa6, 0x48, 0x12, 0x63, 0x39,
+ 0xd7, 0x8d, 0x16, 0x4c, 0xa2, 0xf8, 0x40, 0x1a, 0xf4, 0xae,
+ 0x35, 0x6f, 0x81, 0xdb, 0xaa, 0xf0, 0x1e, 0x44, 0xdf, 0x85,
+ 0x6b, 0x31, 0x05, 0x5f, 0xb1, 0xeb, 0x70, 0x2a, 0xc4, 0x9e,
+ 0xef, 0xb5, 0x5b, 0x01, 0x9a, 0xc0, 0x2e, 0x74, 0xcc, 0x96,
+ 0x78, 0x22, 0xb9, 0xe3, 0x0d, 0x57, 0x26, 0x7c, 0x92, 0xc8,
+ 0x53, 0x09, 0xe7, 0xbd, 0x8a, 0xd0, 0x3e, 0x64, 0xff, 0xa5,
+ 0x4b, 0x11, 0x60, 0x3a, 0xd4, 0x8e, 0x15, 0x4f, 0xa1, 0xfb,
+ 0x43, 0x19, 0xf7, 0xad, 0x36, 0x6c, 0x82, 0xd8, 0xa9, 0xf3,
+ 0x1d, 0x47, 0xdc, 0x86, 0x68, 0x32, 0x00, 0x5b, 0xb6, 0xed,
+ 0x71, 0x2a, 0xc7, 0x9c, 0xe2, 0xb9, 0x54, 0x0f, 0x93, 0xc8,
+ 0x25, 0x7e, 0xd9, 0x82, 0x6f, 0x34, 0xa8, 0xf3, 0x1e, 0x45,
+ 0x3b, 0x60, 0x8d, 0xd6, 0x4a, 0x11, 0xfc, 0xa7, 0xaf, 0xf4,
+ 0x19, 0x42, 0xde, 0x85, 0x68, 0x33, 0x4d, 0x16, 0xfb, 0xa0,
+ 0x3c, 0x67, 0x8a, 0xd1, 0x76, 0x2d, 0xc0, 0x9b, 0x07, 0x5c,
+ 0xb1, 0xea, 0x94, 0xcf, 0x22, 0x79, 0xe5, 0xbe, 0x53, 0x08,
+ 0x43, 0x18, 0xf5, 0xae, 0x32, 0x69, 0x84, 0xdf, 0xa1, 0xfa,
+ 0x17, 0x4c, 0xd0, 0x8b, 0x66, 0x3d, 0x9a, 0xc1, 0x2c, 0x77,
+ 0xeb, 0xb0, 0x5d, 0x06, 0x78, 0x23, 0xce, 0x95, 0x09, 0x52,
+ 0xbf, 0xe4, 0xec, 0xb7, 0x5a, 0x01, 0x9d, 0xc6, 0x2b, 0x70,
+ 0x0e, 0x55, 0xb8, 0xe3, 0x7f, 0x24, 0xc9, 0x92, 0x35, 0x6e,
+ 0x83, 0xd8, 0x44, 0x1f, 0xf2, 0xa9, 0xd7, 0x8c, 0x61, 0x3a,
+ 0xa6, 0xfd, 0x10, 0x4b, 0x86, 0xdd, 0x30, 0x6b, 0xf7, 0xac,
+ 0x41, 0x1a, 0x64, 0x3f, 0xd2, 0x89, 0x15, 0x4e, 0xa3, 0xf8,
+ 0x5f, 0x04, 0xe9, 0xb2, 0x2e, 0x75, 0x98, 0xc3, 0xbd, 0xe6,
+ 0x0b, 0x50, 0xcc, 0x97, 0x7a, 0x21, 0x29, 0x72, 0x9f, 0xc4,
+ 0x58, 0x03, 0xee, 0xb5, 0xcb, 0x90, 0x7d, 0x26, 0xba, 0xe1,
+ 0x0c, 0x57, 0xf0, 0xab, 0x46, 0x1d, 0x81, 0xda, 0x37, 0x6c,
+ 0x12, 0x49, 0xa4, 0xff, 0x63, 0x38, 0xd5, 0x8e, 0xc5, 0x9e,
+ 0x73, 0x28, 0xb4, 0xef, 0x02, 0x59, 0x27, 0x7c, 0x91, 0xca,
+ 0x56, 0x0d, 0xe0, 0xbb, 0x1c, 0x47, 0xaa, 0xf1, 0x6d, 0x36,
+ 0xdb, 0x80, 0xfe, 0xa5, 0x48, 0x13, 0x8f, 0xd4, 0x39, 0x62,
+ 0x6a, 0x31, 0xdc, 0x87, 0x1b, 0x40, 0xad, 0xf6, 0x88, 0xd3,
+ 0x3e, 0x65, 0xf9, 0xa2, 0x4f, 0x14, 0xb3, 0xe8, 0x05, 0x5e,
+ 0xc2, 0x99, 0x74, 0x2f, 0x51, 0x0a, 0xe7, 0xbc, 0x20, 0x7b,
+ 0x96, 0xcd, 0x00, 0x5c, 0xb8, 0xe4, 0x6d, 0x31, 0xd5, 0x89,
+ 0xda, 0x86, 0x62, 0x3e, 0xb7, 0xeb, 0x0f, 0x53, 0xa9, 0xf5,
+ 0x11, 0x4d, 0xc4, 0x98, 0x7c, 0x20, 0x73, 0x2f, 0xcb, 0x97,
+ 0x1e, 0x42, 0xa6, 0xfa, 0x4f, 0x13, 0xf7, 0xab, 0x22, 0x7e,
+ 0x9a, 0xc6, 0x95, 0xc9, 0x2d, 0x71, 0xf8, 0xa4, 0x40, 0x1c,
+ 0xe6, 0xba, 0x5e, 0x02, 0x8b, 0xd7, 0x33, 0x6f, 0x3c, 0x60,
+ 0x84, 0xd8, 0x51, 0x0d, 0xe9, 0xb5, 0x9e, 0xc2, 0x26, 0x7a,
+ 0xf3, 0xaf, 0x4b, 0x17, 0x44, 0x18, 0xfc, 0xa0, 0x29, 0x75,
+ 0x91, 0xcd, 0x37, 0x6b, 0x8f, 0xd3, 0x5a, 0x06, 0xe2, 0xbe,
+ 0xed, 0xb1, 0x55, 0x09, 0x80, 0xdc, 0x38, 0x64, 0xd1, 0x8d,
+ 0x69, 0x35, 0xbc, 0xe0, 0x04, 0x58, 0x0b, 0x57, 0xb3, 0xef,
+ 0x66, 0x3a, 0xde, 0x82, 0x78, 0x24, 0xc0, 0x9c, 0x15, 0x49,
+ 0xad, 0xf1, 0xa2, 0xfe, 0x1a, 0x46, 0xcf, 0x93, 0x77, 0x2b,
+ 0x21, 0x7d, 0x99, 0xc5, 0x4c, 0x10, 0xf4, 0xa8, 0xfb, 0xa7,
+ 0x43, 0x1f, 0x96, 0xca, 0x2e, 0x72, 0x88, 0xd4, 0x30, 0x6c,
+ 0xe5, 0xb9, 0x5d, 0x01, 0x52, 0x0e, 0xea, 0xb6, 0x3f, 0x63,
+ 0x87, 0xdb, 0x6e, 0x32, 0xd6, 0x8a, 0x03, 0x5f, 0xbb, 0xe7,
+ 0xb4, 0xe8, 0x0c, 0x50, 0xd9, 0x85, 0x61, 0x3d, 0xc7, 0x9b,
+ 0x7f, 0x23, 0xaa, 0xf6, 0x12, 0x4e, 0x1d, 0x41, 0xa5, 0xf9,
+ 0x70, 0x2c, 0xc8, 0x94, 0xbf, 0xe3, 0x07, 0x5b, 0xd2, 0x8e,
+ 0x6a, 0x36, 0x65, 0x39, 0xdd, 0x81, 0x08, 0x54, 0xb0, 0xec,
+ 0x16, 0x4a, 0xae, 0xf2, 0x7b, 0x27, 0xc3, 0x9f, 0xcc, 0x90,
+ 0x74, 0x28, 0xa1, 0xfd, 0x19, 0x45, 0xf0, 0xac, 0x48, 0x14,
+ 0x9d, 0xc1, 0x25, 0x79, 0x2a, 0x76, 0x92, 0xce, 0x47, 0x1b,
+ 0xff, 0xa3, 0x59, 0x05, 0xe1, 0xbd, 0x34, 0x68, 0x8c, 0xd0,
+ 0x83, 0xdf, 0x3b, 0x67, 0xee, 0xb2, 0x56, 0x0a, 0x00, 0x5d,
+ 0xba, 0xe7, 0x69, 0x34, 0xd3, 0x8e, 0xd2, 0x8f, 0x68, 0x35,
+ 0xbb, 0xe6, 0x01, 0x5c, 0xb9, 0xe4, 0x03, 0x5e, 0xd0, 0x8d,
+ 0x6a, 0x37, 0x6b, 0x36, 0xd1, 0x8c, 0x02, 0x5f, 0xb8, 0xe5,
+ 0x6f, 0x32, 0xd5, 0x88, 0x06, 0x5b, 0xbc, 0xe1, 0xbd, 0xe0,
+ 0x07, 0x5a, 0xd4, 0x89, 0x6e, 0x33, 0xd6, 0x8b, 0x6c, 0x31,
+ 0xbf, 0xe2, 0x05, 0x58, 0x04, 0x59, 0xbe, 0xe3, 0x6d, 0x30,
+ 0xd7, 0x8a, 0xde, 0x83, 0x64, 0x39, 0xb7, 0xea, 0x0d, 0x50,
+ 0x0c, 0x51, 0xb6, 0xeb, 0x65, 0x38, 0xdf, 0x82, 0x67, 0x3a,
+ 0xdd, 0x80, 0x0e, 0x53, 0xb4, 0xe9, 0xb5, 0xe8, 0x0f, 0x52,
+ 0xdc, 0x81, 0x66, 0x3b, 0xb1, 0xec, 0x0b, 0x56, 0xd8, 0x85,
+ 0x62, 0x3f, 0x63, 0x3e, 0xd9, 0x84, 0x0a, 0x57, 0xb0, 0xed,
+ 0x08, 0x55, 0xb2, 0xef, 0x61, 0x3c, 0xdb, 0x86, 0xda, 0x87,
+ 0x60, 0x3d, 0xb3, 0xee, 0x09, 0x54, 0xa1, 0xfc, 0x1b, 0x46,
+ 0xc8, 0x95, 0x72, 0x2f, 0x73, 0x2e, 0xc9, 0x94, 0x1a, 0x47,
+ 0xa0, 0xfd, 0x18, 0x45, 0xa2, 0xff, 0x71, 0x2c, 0xcb, 0x96,
+ 0xca, 0x97, 0x70, 0x2d, 0xa3, 0xfe, 0x19, 0x44, 0xce, 0x93,
+ 0x74, 0x29, 0xa7, 0xfa, 0x1d, 0x40, 0x1c, 0x41, 0xa6, 0xfb,
+ 0x75, 0x28, 0xcf, 0x92, 0x77, 0x2a, 0xcd, 0x90, 0x1e, 0x43,
+ 0xa4, 0xf9, 0xa5, 0xf8, 0x1f, 0x42, 0xcc, 0x91, 0x76, 0x2b,
+ 0x7f, 0x22, 0xc5, 0x98, 0x16, 0x4b, 0xac, 0xf1, 0xad, 0xf0,
+ 0x17, 0x4a, 0xc4, 0x99, 0x7e, 0x23, 0xc6, 0x9b, 0x7c, 0x21,
+ 0xaf, 0xf2, 0x15, 0x48, 0x14, 0x49, 0xae, 0xf3, 0x7d, 0x20,
+ 0xc7, 0x9a, 0x10, 0x4d, 0xaa, 0xf7, 0x79, 0x24, 0xc3, 0x9e,
+ 0xc2, 0x9f, 0x78, 0x25, 0xab, 0xf6, 0x11, 0x4c, 0xa9, 0xf4,
+ 0x13, 0x4e, 0xc0, 0x9d, 0x7a, 0x27, 0x7b, 0x26, 0xc1, 0x9c,
+ 0x12, 0x4f, 0xa8, 0xf5, 0x00, 0x5e, 0xbc, 0xe2, 0x65, 0x3b,
+ 0xd9, 0x87, 0xca, 0x94, 0x76, 0x28, 0xaf, 0xf1, 0x13, 0x4d,
+ 0x89, 0xd7, 0x35, 0x6b, 0xec, 0xb2, 0x50, 0x0e, 0x43, 0x1d,
+ 0xff, 0xa1, 0x26, 0x78, 0x9a, 0xc4, 0x0f, 0x51, 0xb3, 0xed,
+ 0x6a, 0x34, 0xd6, 0x88, 0xc5, 0x9b, 0x79, 0x27, 0xa0, 0xfe,
+ 0x1c, 0x42, 0x86, 0xd8, 0x3a, 0x64, 0xe3, 0xbd, 0x5f, 0x01,
+ 0x4c, 0x12, 0xf0, 0xae, 0x29, 0x77, 0x95, 0xcb, 0x1e, 0x40,
+ 0xa2, 0xfc, 0x7b, 0x25, 0xc7, 0x99, 0xd4, 0x8a, 0x68, 0x36,
+ 0xb1, 0xef, 0x0d, 0x53, 0x97, 0xc9, 0x2b, 0x75, 0xf2, 0xac,
+ 0x4e, 0x10, 0x5d, 0x03, 0xe1, 0xbf, 0x38, 0x66, 0x84, 0xda,
+ 0x11, 0x4f, 0xad, 0xf3, 0x74, 0x2a, 0xc8, 0x96, 0xdb, 0x85,
+ 0x67, 0x39, 0xbe, 0xe0, 0x02, 0x5c, 0x98, 0xc6, 0x24, 0x7a,
+ 0xfd, 0xa3, 0x41, 0x1f, 0x52, 0x0c, 0xee, 0xb0, 0x37, 0x69,
+ 0x8b, 0xd5, 0x3c, 0x62, 0x80, 0xde, 0x59, 0x07, 0xe5, 0xbb,
+ 0xf6, 0xa8, 0x4a, 0x14, 0x93, 0xcd, 0x2f, 0x71, 0xb5, 0xeb,
+ 0x09, 0x57, 0xd0, 0x8e, 0x6c, 0x32, 0x7f, 0x21, 0xc3, 0x9d,
+ 0x1a, 0x44, 0xa6, 0xf8, 0x33, 0x6d, 0x8f, 0xd1, 0x56, 0x08,
+ 0xea, 0xb4, 0xf9, 0xa7, 0x45, 0x1b, 0x9c, 0xc2, 0x20, 0x7e,
+ 0xba, 0xe4, 0x06, 0x58, 0xdf, 0x81, 0x63, 0x3d, 0x70, 0x2e,
+ 0xcc, 0x92, 0x15, 0x4b, 0xa9, 0xf7, 0x22, 0x7c, 0x9e, 0xc0,
+ 0x47, 0x19, 0xfb, 0xa5, 0xe8, 0xb6, 0x54, 0x0a, 0x8d, 0xd3,
+ 0x31, 0x6f, 0xab, 0xf5, 0x17, 0x49, 0xce, 0x90, 0x72, 0x2c,
+ 0x61, 0x3f, 0xdd, 0x83, 0x04, 0x5a, 0xb8, 0xe6, 0x2d, 0x73,
+ 0x91, 0xcf, 0x48, 0x16, 0xf4, 0xaa, 0xe7, 0xb9, 0x5b, 0x05,
+ 0x82, 0xdc, 0x3e, 0x60, 0xa4, 0xfa, 0x18, 0x46, 0xc1, 0x9f,
+ 0x7d, 0x23, 0x6e, 0x30, 0xd2, 0x8c, 0x0b, 0x55, 0xb7, 0xe9,
+ 0x00, 0x5f, 0xbe, 0xe1, 0x61, 0x3e, 0xdf, 0x80, 0xc2, 0x9d,
+ 0x7c, 0x23, 0xa3, 0xfc, 0x1d, 0x42, 0x99, 0xc6, 0x27, 0x78,
+ 0xf8, 0xa7, 0x46, 0x19, 0x5b, 0x04, 0xe5, 0xba, 0x3a, 0x65,
+ 0x84, 0xdb, 0x2f, 0x70, 0x91, 0xce, 0x4e, 0x11, 0xf0, 0xaf,
+ 0xed, 0xb2, 0x53, 0x0c, 0x8c, 0xd3, 0x32, 0x6d, 0xb6, 0xe9,
+ 0x08, 0x57, 0xd7, 0x88, 0x69, 0x36, 0x74, 0x2b, 0xca, 0x95,
+ 0x15, 0x4a, 0xab, 0xf4, 0x5e, 0x01, 0xe0, 0xbf, 0x3f, 0x60,
+ 0x81, 0xde, 0x9c, 0xc3, 0x22, 0x7d, 0xfd, 0xa2, 0x43, 0x1c,
+ 0xc7, 0x98, 0x79, 0x26, 0xa6, 0xf9, 0x18, 0x47, 0x05, 0x5a,
+ 0xbb, 0xe4, 0x64, 0x3b, 0xda, 0x85, 0x71, 0x2e, 0xcf, 0x90,
+ 0x10, 0x4f, 0xae, 0xf1, 0xb3, 0xec, 0x0d, 0x52, 0xd2, 0x8d,
+ 0x6c, 0x33, 0xe8, 0xb7, 0x56, 0x09, 0x89, 0xd6, 0x37, 0x68,
+ 0x2a, 0x75, 0x94, 0xcb, 0x4b, 0x14, 0xf5, 0xaa, 0xbc, 0xe3,
+ 0x02, 0x5d, 0xdd, 0x82, 0x63, 0x3c, 0x7e, 0x21, 0xc0, 0x9f,
+ 0x1f, 0x40, 0xa1, 0xfe, 0x25, 0x7a, 0x9b, 0xc4, 0x44, 0x1b,
+ 0xfa, 0xa5, 0xe7, 0xb8, 0x59, 0x06, 0x86, 0xd9, 0x38, 0x67,
+ 0x93, 0xcc, 0x2d, 0x72, 0xf2, 0xad, 0x4c, 0x13, 0x51, 0x0e,
+ 0xef, 0xb0, 0x30, 0x6f, 0x8e, 0xd1, 0x0a, 0x55, 0xb4, 0xeb,
+ 0x6b, 0x34, 0xd5, 0x8a, 0xc8, 0x97, 0x76, 0x29, 0xa9, 0xf6,
+ 0x17, 0x48, 0xe2, 0xbd, 0x5c, 0x03, 0x83, 0xdc, 0x3d, 0x62,
+ 0x20, 0x7f, 0x9e, 0xc1, 0x41, 0x1e, 0xff, 0xa0, 0x7b, 0x24,
+ 0xc5, 0x9a, 0x1a, 0x45, 0xa4, 0xfb, 0xb9, 0xe6, 0x07, 0x58,
+ 0xd8, 0x87, 0x66, 0x39, 0xcd, 0x92, 0x73, 0x2c, 0xac, 0xf3,
+ 0x12, 0x4d, 0x0f, 0x50, 0xb1, 0xee, 0x6e, 0x31, 0xd0, 0x8f,
+ 0x54, 0x0b, 0xea, 0xb5, 0x35, 0x6a, 0x8b, 0xd4, 0x96, 0xc9,
+ 0x28, 0x77, 0xf7, 0xa8, 0x49, 0x16, 0x00, 0x60, 0xc0, 0xa0,
+ 0x9d, 0xfd, 0x5d, 0x3d, 0x27, 0x47, 0xe7, 0x87, 0xba, 0xda,
+ 0x7a, 0x1a, 0x4e, 0x2e, 0x8e, 0xee, 0xd3, 0xb3, 0x13, 0x73,
+ 0x69, 0x09, 0xa9, 0xc9, 0xf4, 0x94, 0x34, 0x54, 0x9c, 0xfc,
+ 0x5c, 0x3c, 0x01, 0x61, 0xc1, 0xa1, 0xbb, 0xdb, 0x7b, 0x1b,
+ 0x26, 0x46, 0xe6, 0x86, 0xd2, 0xb2, 0x12, 0x72, 0x4f, 0x2f,
+ 0x8f, 0xef, 0xf5, 0x95, 0x35, 0x55, 0x68, 0x08, 0xa8, 0xc8,
+ 0x25, 0x45, 0xe5, 0x85, 0xb8, 0xd8, 0x78, 0x18, 0x02, 0x62,
+ 0xc2, 0xa2, 0x9f, 0xff, 0x5f, 0x3f, 0x6b, 0x0b, 0xab, 0xcb,
+ 0xf6, 0x96, 0x36, 0x56, 0x4c, 0x2c, 0x8c, 0xec, 0xd1, 0xb1,
+ 0x11, 0x71, 0xb9, 0xd9, 0x79, 0x19, 0x24, 0x44, 0xe4, 0x84,
+ 0x9e, 0xfe, 0x5e, 0x3e, 0x03, 0x63, 0xc3, 0xa3, 0xf7, 0x97,
+ 0x37, 0x57, 0x6a, 0x0a, 0xaa, 0xca, 0xd0, 0xb0, 0x10, 0x70,
+ 0x4d, 0x2d, 0x8d, 0xed, 0x4a, 0x2a, 0x8a, 0xea, 0xd7, 0xb7,
+ 0x17, 0x77, 0x6d, 0x0d, 0xad, 0xcd, 0xf0, 0x90, 0x30, 0x50,
+ 0x04, 0x64, 0xc4, 0xa4, 0x99, 0xf9, 0x59, 0x39, 0x23, 0x43,
+ 0xe3, 0x83, 0xbe, 0xde, 0x7e, 0x1e, 0xd6, 0xb6, 0x16, 0x76,
+ 0x4b, 0x2b, 0x8b, 0xeb, 0xf1, 0x91, 0x31, 0x51, 0x6c, 0x0c,
+ 0xac, 0xcc, 0x98, 0xf8, 0x58, 0x38, 0x05, 0x65, 0xc5, 0xa5,
+ 0xbf, 0xdf, 0x7f, 0x1f, 0x22, 0x42, 0xe2, 0x82, 0x6f, 0x0f,
+ 0xaf, 0xcf, 0xf2, 0x92, 0x32, 0x52, 0x48, 0x28, 0x88, 0xe8,
+ 0xd5, 0xb5, 0x15, 0x75, 0x21, 0x41, 0xe1, 0x81, 0xbc, 0xdc,
+ 0x7c, 0x1c, 0x06, 0x66, 0xc6, 0xa6, 0x9b, 0xfb, 0x5b, 0x3b,
+ 0xf3, 0x93, 0x33, 0x53, 0x6e, 0x0e, 0xae, 0xce, 0xd4, 0xb4,
+ 0x14, 0x74, 0x49, 0x29, 0x89, 0xe9, 0xbd, 0xdd, 0x7d, 0x1d,
+ 0x20, 0x40, 0xe0, 0x80, 0x9a, 0xfa, 0x5a, 0x3a, 0x07, 0x67,
+ 0xc7, 0xa7, 0x00, 0x61, 0xc2, 0xa3, 0x99, 0xf8, 0x5b, 0x3a,
+ 0x2f, 0x4e, 0xed, 0x8c, 0xb6, 0xd7, 0x74, 0x15, 0x5e, 0x3f,
+ 0x9c, 0xfd, 0xc7, 0xa6, 0x05, 0x64, 0x71, 0x10, 0xb3, 0xd2,
+ 0xe8, 0x89, 0x2a, 0x4b, 0xbc, 0xdd, 0x7e, 0x1f, 0x25, 0x44,
+ 0xe7, 0x86, 0x93, 0xf2, 0x51, 0x30, 0x0a, 0x6b, 0xc8, 0xa9,
+ 0xe2, 0x83, 0x20, 0x41, 0x7b, 0x1a, 0xb9, 0xd8, 0xcd, 0xac,
+ 0x0f, 0x6e, 0x54, 0x35, 0x96, 0xf7, 0x65, 0x04, 0xa7, 0xc6,
+ 0xfc, 0x9d, 0x3e, 0x5f, 0x4a, 0x2b, 0x88, 0xe9, 0xd3, 0xb2,
+ 0x11, 0x70, 0x3b, 0x5a, 0xf9, 0x98, 0xa2, 0xc3, 0x60, 0x01,
+ 0x14, 0x75, 0xd6, 0xb7, 0x8d, 0xec, 0x4f, 0x2e, 0xd9, 0xb8,
+ 0x1b, 0x7a, 0x40, 0x21, 0x82, 0xe3, 0xf6, 0x97, 0x34, 0x55,
+ 0x6f, 0x0e, 0xad, 0xcc, 0x87, 0xe6, 0x45, 0x24, 0x1e, 0x7f,
+ 0xdc, 0xbd, 0xa8, 0xc9, 0x6a, 0x0b, 0x31, 0x50, 0xf3, 0x92,
+ 0xca, 0xab, 0x08, 0x69, 0x53, 0x32, 0x91, 0xf0, 0xe5, 0x84,
+ 0x27, 0x46, 0x7c, 0x1d, 0xbe, 0xdf, 0x94, 0xf5, 0x56, 0x37,
+ 0x0d, 0x6c, 0xcf, 0xae, 0xbb, 0xda, 0x79, 0x18, 0x22, 0x43,
+ 0xe0, 0x81, 0x76, 0x17, 0xb4, 0xd5, 0xef, 0x8e, 0x2d, 0x4c,
+ 0x59, 0x38, 0x9b, 0xfa, 0xc0, 0xa1, 0x02, 0x63, 0x28, 0x49,
+ 0xea, 0x8b, 0xb1, 0xd0, 0x73, 0x12, 0x07, 0x66, 0xc5, 0xa4,
+ 0x9e, 0xff, 0x5c, 0x3d, 0xaf, 0xce, 0x6d, 0x0c, 0x36, 0x57,
+ 0xf4, 0x95, 0x80, 0xe1, 0x42, 0x23, 0x19, 0x78, 0xdb, 0xba,
+ 0xf1, 0x90, 0x33, 0x52, 0x68, 0x09, 0xaa, 0xcb, 0xde, 0xbf,
+ 0x1c, 0x7d, 0x47, 0x26, 0x85, 0xe4, 0x13, 0x72, 0xd1, 0xb0,
+ 0x8a, 0xeb, 0x48, 0x29, 0x3c, 0x5d, 0xfe, 0x9f, 0xa5, 0xc4,
+ 0x67, 0x06, 0x4d, 0x2c, 0x8f, 0xee, 0xd4, 0xb5, 0x16, 0x77,
+ 0x62, 0x03, 0xa0, 0xc1, 0xfb, 0x9a, 0x39, 0x58, 0x00, 0x62,
+ 0xc4, 0xa6, 0x95, 0xf7, 0x51, 0x33, 0x37, 0x55, 0xf3, 0x91,
+ 0xa2, 0xc0, 0x66, 0x04, 0x6e, 0x0c, 0xaa, 0xc8, 0xfb, 0x99,
+ 0x3f, 0x5d, 0x59, 0x3b, 0x9d, 0xff, 0xcc, 0xae, 0x08, 0x6a,
+ 0xdc, 0xbe, 0x18, 0x7a, 0x49, 0x2b, 0x8d, 0xef, 0xeb, 0x89,
+ 0x2f, 0x4d, 0x7e, 0x1c, 0xba, 0xd8, 0xb2, 0xd0, 0x76, 0x14,
+ 0x27, 0x45, 0xe3, 0x81, 0x85, 0xe7, 0x41, 0x23, 0x10, 0x72,
+ 0xd4, 0xb6, 0xa5, 0xc7, 0x61, 0x03, 0x30, 0x52, 0xf4, 0x96,
+ 0x92, 0xf0, 0x56, 0x34, 0x07, 0x65, 0xc3, 0xa1, 0xcb, 0xa9,
+ 0x0f, 0x6d, 0x5e, 0x3c, 0x9a, 0xf8, 0xfc, 0x9e, 0x38, 0x5a,
+ 0x69, 0x0b, 0xad, 0xcf, 0x79, 0x1b, 0xbd, 0xdf, 0xec, 0x8e,
+ 0x28, 0x4a, 0x4e, 0x2c, 0x8a, 0xe8, 0xdb, 0xb9, 0x1f, 0x7d,
+ 0x17, 0x75, 0xd3, 0xb1, 0x82, 0xe0, 0x46, 0x24, 0x20, 0x42,
+ 0xe4, 0x86, 0xb5, 0xd7, 0x71, 0x13, 0x57, 0x35, 0x93, 0xf1,
+ 0xc2, 0xa0, 0x06, 0x64, 0x60, 0x02, 0xa4, 0xc6, 0xf5, 0x97,
+ 0x31, 0x53, 0x39, 0x5b, 0xfd, 0x9f, 0xac, 0xce, 0x68, 0x0a,
+ 0x0e, 0x6c, 0xca, 0xa8, 0x9b, 0xf9, 0x5f, 0x3d, 0x8b, 0xe9,
+ 0x4f, 0x2d, 0x1e, 0x7c, 0xda, 0xb8, 0xbc, 0xde, 0x78, 0x1a,
+ 0x29, 0x4b, 0xed, 0x8f, 0xe5, 0x87, 0x21, 0x43, 0x70, 0x12,
+ 0xb4, 0xd6, 0xd2, 0xb0, 0x16, 0x74, 0x47, 0x25, 0x83, 0xe1,
+ 0xf2, 0x90, 0x36, 0x54, 0x67, 0x05, 0xa3, 0xc1, 0xc5, 0xa7,
+ 0x01, 0x63, 0x50, 0x32, 0x94, 0xf6, 0x9c, 0xfe, 0x58, 0x3a,
+ 0x09, 0x6b, 0xcd, 0xaf, 0xab, 0xc9, 0x6f, 0x0d, 0x3e, 0x5c,
+ 0xfa, 0x98, 0x2e, 0x4c, 0xea, 0x88, 0xbb, 0xd9, 0x7f, 0x1d,
+ 0x19, 0x7b, 0xdd, 0xbf, 0x8c, 0xee, 0x48, 0x2a, 0x40, 0x22,
+ 0x84, 0xe6, 0xd5, 0xb7, 0x11, 0x73, 0x77, 0x15, 0xb3, 0xd1,
+ 0xe2, 0x80, 0x26, 0x44, 0x00, 0x63, 0xc6, 0xa5, 0x91, 0xf2,
+ 0x57, 0x34, 0x3f, 0x5c, 0xf9, 0x9a, 0xae, 0xcd, 0x68, 0x0b,
+ 0x7e, 0x1d, 0xb8, 0xdb, 0xef, 0x8c, 0x29, 0x4a, 0x41, 0x22,
+ 0x87, 0xe4, 0xd0, 0xb3, 0x16, 0x75, 0xfc, 0x9f, 0x3a, 0x59,
+ 0x6d, 0x0e, 0xab, 0xc8, 0xc3, 0xa0, 0x05, 0x66, 0x52, 0x31,
+ 0x94, 0xf7, 0x82, 0xe1, 0x44, 0x27, 0x13, 0x70, 0xd5, 0xb6,
+ 0xbd, 0xde, 0x7b, 0x18, 0x2c, 0x4f, 0xea, 0x89, 0xe5, 0x86,
+ 0x23, 0x40, 0x74, 0x17, 0xb2, 0xd1, 0xda, 0xb9, 0x1c, 0x7f,
+ 0x4b, 0x28, 0x8d, 0xee, 0x9b, 0xf8, 0x5d, 0x3e, 0x0a, 0x69,
+ 0xcc, 0xaf, 0xa4, 0xc7, 0x62, 0x01, 0x35, 0x56, 0xf3, 0x90,
+ 0x19, 0x7a, 0xdf, 0xbc, 0x88, 0xeb, 0x4e, 0x2d, 0x26, 0x45,
+ 0xe0, 0x83, 0xb7, 0xd4, 0x71, 0x12, 0x67, 0x04, 0xa1, 0xc2,
+ 0xf6, 0x95, 0x30, 0x53, 0x58, 0x3b, 0x9e, 0xfd, 0xc9, 0xaa,
+ 0x0f, 0x6c, 0xd7, 0xb4, 0x11, 0x72, 0x46, 0x25, 0x80, 0xe3,
+ 0xe8, 0x8b, 0x2e, 0x4d, 0x79, 0x1a, 0xbf, 0xdc, 0xa9, 0xca,
+ 0x6f, 0x0c, 0x38, 0x5b, 0xfe, 0x9d, 0x96, 0xf5, 0x50, 0x33,
+ 0x07, 0x64, 0xc1, 0xa2, 0x2b, 0x48, 0xed, 0x8e, 0xba, 0xd9,
+ 0x7c, 0x1f, 0x14, 0x77, 0xd2, 0xb1, 0x85, 0xe6, 0x43, 0x20,
+ 0x55, 0x36, 0x93, 0xf0, 0xc4, 0xa7, 0x02, 0x61, 0x6a, 0x09,
+ 0xac, 0xcf, 0xfb, 0x98, 0x3d, 0x5e, 0x32, 0x51, 0xf4, 0x97,
+ 0xa3, 0xc0, 0x65, 0x06, 0x0d, 0x6e, 0xcb, 0xa8, 0x9c, 0xff,
+ 0x5a, 0x39, 0x4c, 0x2f, 0x8a, 0xe9, 0xdd, 0xbe, 0x1b, 0x78,
+ 0x73, 0x10, 0xb5, 0xd6, 0xe2, 0x81, 0x24, 0x47, 0xce, 0xad,
+ 0x08, 0x6b, 0x5f, 0x3c, 0x99, 0xfa, 0xf1, 0x92, 0x37, 0x54,
+ 0x60, 0x03, 0xa6, 0xc5, 0xb0, 0xd3, 0x76, 0x15, 0x21, 0x42,
+ 0xe7, 0x84, 0x8f, 0xec, 0x49, 0x2a, 0x1e, 0x7d, 0xd8, 0xbb,
+ 0x00, 0x64, 0xc8, 0xac, 0x8d, 0xe9, 0x45, 0x21, 0x07, 0x63,
+ 0xcf, 0xab, 0x8a, 0xee, 0x42, 0x26, 0x0e, 0x6a, 0xc6, 0xa2,
+ 0x83, 0xe7, 0x4b, 0x2f, 0x09, 0x6d, 0xc1, 0xa5, 0x84, 0xe0,
+ 0x4c, 0x28, 0x1c, 0x78, 0xd4, 0xb0, 0x91, 0xf5, 0x59, 0x3d,
+ 0x1b, 0x7f, 0xd3, 0xb7, 0x96, 0xf2, 0x5e, 0x3a, 0x12, 0x76,
+ 0xda, 0xbe, 0x9f, 0xfb, 0x57, 0x33, 0x15, 0x71, 0xdd, 0xb9,
+ 0x98, 0xfc, 0x50, 0x34, 0x38, 0x5c, 0xf0, 0x94, 0xb5, 0xd1,
+ 0x7d, 0x19, 0x3f, 0x5b, 0xf7, 0x93, 0xb2, 0xd6, 0x7a, 0x1e,
+ 0x36, 0x52, 0xfe, 0x9a, 0xbb, 0xdf, 0x73, 0x17, 0x31, 0x55,
+ 0xf9, 0x9d, 0xbc, 0xd8, 0x74, 0x10, 0x24, 0x40, 0xec, 0x88,
+ 0xa9, 0xcd, 0x61, 0x05, 0x23, 0x47, 0xeb, 0x8f, 0xae, 0xca,
+ 0x66, 0x02, 0x2a, 0x4e, 0xe2, 0x86, 0xa7, 0xc3, 0x6f, 0x0b,
+ 0x2d, 0x49, 0xe5, 0x81, 0xa0, 0xc4, 0x68, 0x0c, 0x70, 0x14,
+ 0xb8, 0xdc, 0xfd, 0x99, 0x35, 0x51, 0x77, 0x13, 0xbf, 0xdb,
+ 0xfa, 0x9e, 0x32, 0x56, 0x7e, 0x1a, 0xb6, 0xd2, 0xf3, 0x97,
+ 0x3b, 0x5f, 0x79, 0x1d, 0xb1, 0xd5, 0xf4, 0x90, 0x3c, 0x58,
+ 0x6c, 0x08, 0xa4, 0xc0, 0xe1, 0x85, 0x29, 0x4d, 0x6b, 0x0f,
+ 0xa3, 0xc7, 0xe6, 0x82, 0x2e, 0x4a, 0x62, 0x06, 0xaa, 0xce,
+ 0xef, 0x8b, 0x27, 0x43, 0x65, 0x01, 0xad, 0xc9, 0xe8, 0x8c,
+ 0x20, 0x44, 0x48, 0x2c, 0x80, 0xe4, 0xc5, 0xa1, 0x0d, 0x69,
+ 0x4f, 0x2b, 0x87, 0xe3, 0xc2, 0xa6, 0x0a, 0x6e, 0x46, 0x22,
+ 0x8e, 0xea, 0xcb, 0xaf, 0x03, 0x67, 0x41, 0x25, 0x89, 0xed,
+ 0xcc, 0xa8, 0x04, 0x60, 0x54, 0x30, 0x9c, 0xf8, 0xd9, 0xbd,
+ 0x11, 0x75, 0x53, 0x37, 0x9b, 0xff, 0xde, 0xba, 0x16, 0x72,
+ 0x5a, 0x3e, 0x92, 0xf6, 0xd7, 0xb3, 0x1f, 0x7b, 0x5d, 0x39,
+ 0x95, 0xf1, 0xd0, 0xb4, 0x18, 0x7c, 0x00, 0x65, 0xca, 0xaf,
+ 0x89, 0xec, 0x43, 0x26, 0x0f, 0x6a, 0xc5, 0xa0, 0x86, 0xe3,
+ 0x4c, 0x29, 0x1e, 0x7b, 0xd4, 0xb1, 0x97, 0xf2, 0x5d, 0x38,
+ 0x11, 0x74, 0xdb, 0xbe, 0x98, 0xfd, 0x52, 0x37, 0x3c, 0x59,
+ 0xf6, 0x93, 0xb5, 0xd0, 0x7f, 0x1a, 0x33, 0x56, 0xf9, 0x9c,
+ 0xba, 0xdf, 0x70, 0x15, 0x22, 0x47, 0xe8, 0x8d, 0xab, 0xce,
+ 0x61, 0x04, 0x2d, 0x48, 0xe7, 0x82, 0xa4, 0xc1, 0x6e, 0x0b,
+ 0x78, 0x1d, 0xb2, 0xd7, 0xf1, 0x94, 0x3b, 0x5e, 0x77, 0x12,
+ 0xbd, 0xd8, 0xfe, 0x9b, 0x34, 0x51, 0x66, 0x03, 0xac, 0xc9,
+ 0xef, 0x8a, 0x25, 0x40, 0x69, 0x0c, 0xa3, 0xc6, 0xe0, 0x85,
+ 0x2a, 0x4f, 0x44, 0x21, 0x8e, 0xeb, 0xcd, 0xa8, 0x07, 0x62,
+ 0x4b, 0x2e, 0x81, 0xe4, 0xc2, 0xa7, 0x08, 0x6d, 0x5a, 0x3f,
+ 0x90, 0xf5, 0xd3, 0xb6, 0x19, 0x7c, 0x55, 0x30, 0x9f, 0xfa,
+ 0xdc, 0xb9, 0x16, 0x73, 0xf0, 0x95, 0x3a, 0x5f, 0x79, 0x1c,
+ 0xb3, 0xd6, 0xff, 0x9a, 0x35, 0x50, 0x76, 0x13, 0xbc, 0xd9,
+ 0xee, 0x8b, 0x24, 0x41, 0x67, 0x02, 0xad, 0xc8, 0xe1, 0x84,
+ 0x2b, 0x4e, 0x68, 0x0d, 0xa2, 0xc7, 0xcc, 0xa9, 0x06, 0x63,
+ 0x45, 0x20, 0x8f, 0xea, 0xc3, 0xa6, 0x09, 0x6c, 0x4a, 0x2f,
+ 0x80, 0xe5, 0xd2, 0xb7, 0x18, 0x7d, 0x5b, 0x3e, 0x91, 0xf4,
+ 0xdd, 0xb8, 0x17, 0x72, 0x54, 0x31, 0x9e, 0xfb, 0x88, 0xed,
+ 0x42, 0x27, 0x01, 0x64, 0xcb, 0xae, 0x87, 0xe2, 0x4d, 0x28,
+ 0x0e, 0x6b, 0xc4, 0xa1, 0x96, 0xf3, 0x5c, 0x39, 0x1f, 0x7a,
+ 0xd5, 0xb0, 0x99, 0xfc, 0x53, 0x36, 0x10, 0x75, 0xda, 0xbf,
+ 0xb4, 0xd1, 0x7e, 0x1b, 0x3d, 0x58, 0xf7, 0x92, 0xbb, 0xde,
+ 0x71, 0x14, 0x32, 0x57, 0xf8, 0x9d, 0xaa, 0xcf, 0x60, 0x05,
+ 0x23, 0x46, 0xe9, 0x8c, 0xa5, 0xc0, 0x6f, 0x0a, 0x2c, 0x49,
+ 0xe6, 0x83, 0x00, 0x66, 0xcc, 0xaa, 0x85, 0xe3, 0x49, 0x2f,
+ 0x17, 0x71, 0xdb, 0xbd, 0x92, 0xf4, 0x5e, 0x38, 0x2e, 0x48,
+ 0xe2, 0x84, 0xab, 0xcd, 0x67, 0x01, 0x39, 0x5f, 0xf5, 0x93,
+ 0xbc, 0xda, 0x70, 0x16, 0x5c, 0x3a, 0x90, 0xf6, 0xd9, 0xbf,
+ 0x15, 0x73, 0x4b, 0x2d, 0x87, 0xe1, 0xce, 0xa8, 0x02, 0x64,
+ 0x72, 0x14, 0xbe, 0xd8, 0xf7, 0x91, 0x3b, 0x5d, 0x65, 0x03,
+ 0xa9, 0xcf, 0xe0, 0x86, 0x2c, 0x4a, 0xb8, 0xde, 0x74, 0x12,
+ 0x3d, 0x5b, 0xf1, 0x97, 0xaf, 0xc9, 0x63, 0x05, 0x2a, 0x4c,
+ 0xe6, 0x80, 0x96, 0xf0, 0x5a, 0x3c, 0x13, 0x75, 0xdf, 0xb9,
+ 0x81, 0xe7, 0x4d, 0x2b, 0x04, 0x62, 0xc8, 0xae, 0xe4, 0x82,
+ 0x28, 0x4e, 0x61, 0x07, 0xad, 0xcb, 0xf3, 0x95, 0x3f, 0x59,
+ 0x76, 0x10, 0xba, 0xdc, 0xca, 0xac, 0x06, 0x60, 0x4f, 0x29,
+ 0x83, 0xe5, 0xdd, 0xbb, 0x11, 0x77, 0x58, 0x3e, 0x94, 0xf2,
+ 0x6d, 0x0b, 0xa1, 0xc7, 0xe8, 0x8e, 0x24, 0x42, 0x7a, 0x1c,
+ 0xb6, 0xd0, 0xff, 0x99, 0x33, 0x55, 0x43, 0x25, 0x8f, 0xe9,
+ 0xc6, 0xa0, 0x0a, 0x6c, 0x54, 0x32, 0x98, 0xfe, 0xd1, 0xb7,
+ 0x1d, 0x7b, 0x31, 0x57, 0xfd, 0x9b, 0xb4, 0xd2, 0x78, 0x1e,
+ 0x26, 0x40, 0xea, 0x8c, 0xa3, 0xc5, 0x6f, 0x09, 0x1f, 0x79,
+ 0xd3, 0xb5, 0x9a, 0xfc, 0x56, 0x30, 0x08, 0x6e, 0xc4, 0xa2,
+ 0x8d, 0xeb, 0x41, 0x27, 0xd5, 0xb3, 0x19, 0x7f, 0x50, 0x36,
+ 0x9c, 0xfa, 0xc2, 0xa4, 0x0e, 0x68, 0x47, 0x21, 0x8b, 0xed,
+ 0xfb, 0x9d, 0x37, 0x51, 0x7e, 0x18, 0xb2, 0xd4, 0xec, 0x8a,
+ 0x20, 0x46, 0x69, 0x0f, 0xa5, 0xc3, 0x89, 0xef, 0x45, 0x23,
+ 0x0c, 0x6a, 0xc0, 0xa6, 0x9e, 0xf8, 0x52, 0x34, 0x1b, 0x7d,
+ 0xd7, 0xb1, 0xa7, 0xc1, 0x6b, 0x0d, 0x22, 0x44, 0xee, 0x88,
+ 0xb0, 0xd6, 0x7c, 0x1a, 0x35, 0x53, 0xf9, 0x9f, 0x00, 0x67,
+ 0xce, 0xa9, 0x81, 0xe6, 0x4f, 0x28, 0x1f, 0x78, 0xd1, 0xb6,
+ 0x9e, 0xf9, 0x50, 0x37, 0x3e, 0x59, 0xf0, 0x97, 0xbf, 0xd8,
+ 0x71, 0x16, 0x21, 0x46, 0xef, 0x88, 0xa0, 0xc7, 0x6e, 0x09,
+ 0x7c, 0x1b, 0xb2, 0xd5, 0xfd, 0x9a, 0x33, 0x54, 0x63, 0x04,
+ 0xad, 0xca, 0xe2, 0x85, 0x2c, 0x4b, 0x42, 0x25, 0x8c, 0xeb,
+ 0xc3, 0xa4, 0x0d, 0x6a, 0x5d, 0x3a, 0x93, 0xf4, 0xdc, 0xbb,
+ 0x12, 0x75, 0xf8, 0x9f, 0x36, 0x51, 0x79, 0x1e, 0xb7, 0xd0,
+ 0xe7, 0x80, 0x29, 0x4e, 0x66, 0x01, 0xa8, 0xcf, 0xc6, 0xa1,
+ 0x08, 0x6f, 0x47, 0x20, 0x89, 0xee, 0xd9, 0xbe, 0x17, 0x70,
+ 0x58, 0x3f, 0x96, 0xf1, 0x84, 0xe3, 0x4a, 0x2d, 0x05, 0x62,
+ 0xcb, 0xac, 0x9b, 0xfc, 0x55, 0x32, 0x1a, 0x7d, 0xd4, 0xb3,
+ 0xba, 0xdd, 0x74, 0x13, 0x3b, 0x5c, 0xf5, 0x92, 0xa5, 0xc2,
+ 0x6b, 0x0c, 0x24, 0x43, 0xea, 0x8d, 0xed, 0x8a, 0x23, 0x44,
+ 0x6c, 0x0b, 0xa2, 0xc5, 0xf2, 0x95, 0x3c, 0x5b, 0x73, 0x14,
+ 0xbd, 0xda, 0xd3, 0xb4, 0x1d, 0x7a, 0x52, 0x35, 0x9c, 0xfb,
+ 0xcc, 0xab, 0x02, 0x65, 0x4d, 0x2a, 0x83, 0xe4, 0x91, 0xf6,
+ 0x5f, 0x38, 0x10, 0x77, 0xde, 0xb9, 0x8e, 0xe9, 0x40, 0x27,
+ 0x0f, 0x68, 0xc1, 0xa6, 0xaf, 0xc8, 0x61, 0x06, 0x2e, 0x49,
+ 0xe0, 0x87, 0xb0, 0xd7, 0x7e, 0x19, 0x31, 0x56, 0xff, 0x98,
+ 0x15, 0x72, 0xdb, 0xbc, 0x94, 0xf3, 0x5a, 0x3d, 0x0a, 0x6d,
+ 0xc4, 0xa3, 0x8b, 0xec, 0x45, 0x22, 0x2b, 0x4c, 0xe5, 0x82,
+ 0xaa, 0xcd, 0x64, 0x03, 0x34, 0x53, 0xfa, 0x9d, 0xb5, 0xd2,
+ 0x7b, 0x1c, 0x69, 0x0e, 0xa7, 0xc0, 0xe8, 0x8f, 0x26, 0x41,
+ 0x76, 0x11, 0xb8, 0xdf, 0xf7, 0x90, 0x39, 0x5e, 0x57, 0x30,
+ 0x99, 0xfe, 0xd6, 0xb1, 0x18, 0x7f, 0x48, 0x2f, 0x86, 0xe1,
+ 0xc9, 0xae, 0x07, 0x60, 0x00, 0x68, 0xd0, 0xb8, 0xbd, 0xd5,
+ 0x6d, 0x05, 0x67, 0x0f, 0xb7, 0xdf, 0xda, 0xb2, 0x0a, 0x62,
+ 0xce, 0xa6, 0x1e, 0x76, 0x73, 0x1b, 0xa3, 0xcb, 0xa9, 0xc1,
+ 0x79, 0x11, 0x14, 0x7c, 0xc4, 0xac, 0x81, 0xe9, 0x51, 0x39,
+ 0x3c, 0x54, 0xec, 0x84, 0xe6, 0x8e, 0x36, 0x5e, 0x5b, 0x33,
+ 0x8b, 0xe3, 0x4f, 0x27, 0x9f, 0xf7, 0xf2, 0x9a, 0x22, 0x4a,
+ 0x28, 0x40, 0xf8, 0x90, 0x95, 0xfd, 0x45, 0x2d, 0x1f, 0x77,
+ 0xcf, 0xa7, 0xa2, 0xca, 0x72, 0x1a, 0x78, 0x10, 0xa8, 0xc0,
+ 0xc5, 0xad, 0x15, 0x7d, 0xd1, 0xb9, 0x01, 0x69, 0x6c, 0x04,
+ 0xbc, 0xd4, 0xb6, 0xde, 0x66, 0x0e, 0x0b, 0x63, 0xdb, 0xb3,
+ 0x9e, 0xf6, 0x4e, 0x26, 0x23, 0x4b, 0xf3, 0x9b, 0xf9, 0x91,
+ 0x29, 0x41, 0x44, 0x2c, 0x94, 0xfc, 0x50, 0x38, 0x80, 0xe8,
+ 0xed, 0x85, 0x3d, 0x55, 0x37, 0x5f, 0xe7, 0x8f, 0x8a, 0xe2,
+ 0x5a, 0x32, 0x3e, 0x56, 0xee, 0x86, 0x83, 0xeb, 0x53, 0x3b,
+ 0x59, 0x31, 0x89, 0xe1, 0xe4, 0x8c, 0x34, 0x5c, 0xf0, 0x98,
+ 0x20, 0x48, 0x4d, 0x25, 0x9d, 0xf5, 0x97, 0xff, 0x47, 0x2f,
+ 0x2a, 0x42, 0xfa, 0x92, 0xbf, 0xd7, 0x6f, 0x07, 0x02, 0x6a,
+ 0xd2, 0xba, 0xd8, 0xb0, 0x08, 0x60, 0x65, 0x0d, 0xb5, 0xdd,
+ 0x71, 0x19, 0xa1, 0xc9, 0xcc, 0xa4, 0x1c, 0x74, 0x16, 0x7e,
+ 0xc6, 0xae, 0xab, 0xc3, 0x7b, 0x13, 0x21, 0x49, 0xf1, 0x99,
+ 0x9c, 0xf4, 0x4c, 0x24, 0x46, 0x2e, 0x96, 0xfe, 0xfb, 0x93,
+ 0x2b, 0x43, 0xef, 0x87, 0x3f, 0x57, 0x52, 0x3a, 0x82, 0xea,
+ 0x88, 0xe0, 0x58, 0x30, 0x35, 0x5d, 0xe5, 0x8d, 0xa0, 0xc8,
+ 0x70, 0x18, 0x1d, 0x75, 0xcd, 0xa5, 0xc7, 0xaf, 0x17, 0x7f,
+ 0x7a, 0x12, 0xaa, 0xc2, 0x6e, 0x06, 0xbe, 0xd6, 0xd3, 0xbb,
+ 0x03, 0x6b, 0x09, 0x61, 0xd9, 0xb1, 0xb4, 0xdc, 0x64, 0x0c,
+ 0x00, 0x69, 0xd2, 0xbb, 0xb9, 0xd0, 0x6b, 0x02, 0x6f, 0x06,
+ 0xbd, 0xd4, 0xd6, 0xbf, 0x04, 0x6d, 0xde, 0xb7, 0x0c, 0x65,
+ 0x67, 0x0e, 0xb5, 0xdc, 0xb1, 0xd8, 0x63, 0x0a, 0x08, 0x61,
+ 0xda, 0xb3, 0xa1, 0xc8, 0x73, 0x1a, 0x18, 0x71, 0xca, 0xa3,
+ 0xce, 0xa7, 0x1c, 0x75, 0x77, 0x1e, 0xa5, 0xcc, 0x7f, 0x16,
+ 0xad, 0xc4, 0xc6, 0xaf, 0x14, 0x7d, 0x10, 0x79, 0xc2, 0xab,
+ 0xa9, 0xc0, 0x7b, 0x12, 0x5f, 0x36, 0x8d, 0xe4, 0xe6, 0x8f,
+ 0x34, 0x5d, 0x30, 0x59, 0xe2, 0x8b, 0x89, 0xe0, 0x5b, 0x32,
+ 0x81, 0xe8, 0x53, 0x3a, 0x38, 0x51, 0xea, 0x83, 0xee, 0x87,
+ 0x3c, 0x55, 0x57, 0x3e, 0x85, 0xec, 0xfe, 0x97, 0x2c, 0x45,
+ 0x47, 0x2e, 0x95, 0xfc, 0x91, 0xf8, 0x43, 0x2a, 0x28, 0x41,
+ 0xfa, 0x93, 0x20, 0x49, 0xf2, 0x9b, 0x99, 0xf0, 0x4b, 0x22,
+ 0x4f, 0x26, 0x9d, 0xf4, 0xf6, 0x9f, 0x24, 0x4d, 0xbe, 0xd7,
+ 0x6c, 0x05, 0x07, 0x6e, 0xd5, 0xbc, 0xd1, 0xb8, 0x03, 0x6a,
+ 0x68, 0x01, 0xba, 0xd3, 0x60, 0x09, 0xb2, 0xdb, 0xd9, 0xb0,
+ 0x0b, 0x62, 0x0f, 0x66, 0xdd, 0xb4, 0xb6, 0xdf, 0x64, 0x0d,
+ 0x1f, 0x76, 0xcd, 0xa4, 0xa6, 0xcf, 0x74, 0x1d, 0x70, 0x19,
+ 0xa2, 0xcb, 0xc9, 0xa0, 0x1b, 0x72, 0xc1, 0xa8, 0x13, 0x7a,
+ 0x78, 0x11, 0xaa, 0xc3, 0xae, 0xc7, 0x7c, 0x15, 0x17, 0x7e,
+ 0xc5, 0xac, 0xe1, 0x88, 0x33, 0x5a, 0x58, 0x31, 0x8a, 0xe3,
+ 0x8e, 0xe7, 0x5c, 0x35, 0x37, 0x5e, 0xe5, 0x8c, 0x3f, 0x56,
+ 0xed, 0x84, 0x86, 0xef, 0x54, 0x3d, 0x50, 0x39, 0x82, 0xeb,
+ 0xe9, 0x80, 0x3b, 0x52, 0x40, 0x29, 0x92, 0xfb, 0xf9, 0x90,
+ 0x2b, 0x42, 0x2f, 0x46, 0xfd, 0x94, 0x96, 0xff, 0x44, 0x2d,
+ 0x9e, 0xf7, 0x4c, 0x25, 0x27, 0x4e, 0xf5, 0x9c, 0xf1, 0x98,
+ 0x23, 0x4a, 0x48, 0x21, 0x9a, 0xf3, 0x00, 0x6a, 0xd4, 0xbe,
+ 0xb5, 0xdf, 0x61, 0x0b, 0x77, 0x1d, 0xa3, 0xc9, 0xc2, 0xa8,
+ 0x16, 0x7c, 0xee, 0x84, 0x3a, 0x50, 0x5b, 0x31, 0x8f, 0xe5,
+ 0x99, 0xf3, 0x4d, 0x27, 0x2c, 0x46, 0xf8, 0x92, 0xc1, 0xab,
+ 0x15, 0x7f, 0x74, 0x1e, 0xa0, 0xca, 0xb6, 0xdc, 0x62, 0x08,
+ 0x03, 0x69, 0xd7, 0xbd, 0x2f, 0x45, 0xfb, 0x91, 0x9a, 0xf0,
+ 0x4e, 0x24, 0x58, 0x32, 0x8c, 0xe6, 0xed, 0x87, 0x39, 0x53,
+ 0x9f, 0xf5, 0x4b, 0x21, 0x2a, 0x40, 0xfe, 0x94, 0xe8, 0x82,
+ 0x3c, 0x56, 0x5d, 0x37, 0x89, 0xe3, 0x71, 0x1b, 0xa5, 0xcf,
+ 0xc4, 0xae, 0x10, 0x7a, 0x06, 0x6c, 0xd2, 0xb8, 0xb3, 0xd9,
+ 0x67, 0x0d, 0x5e, 0x34, 0x8a, 0xe0, 0xeb, 0x81, 0x3f, 0x55,
+ 0x29, 0x43, 0xfd, 0x97, 0x9c, 0xf6, 0x48, 0x22, 0xb0, 0xda,
+ 0x64, 0x0e, 0x05, 0x6f, 0xd1, 0xbb, 0xc7, 0xad, 0x13, 0x79,
+ 0x72, 0x18, 0xa6, 0xcc, 0x23, 0x49, 0xf7, 0x9d, 0x96, 0xfc,
+ 0x42, 0x28, 0x54, 0x3e, 0x80, 0xea, 0xe1, 0x8b, 0x35, 0x5f,
+ 0xcd, 0xa7, 0x19, 0x73, 0x78, 0x12, 0xac, 0xc6, 0xba, 0xd0,
+ 0x6e, 0x04, 0x0f, 0x65, 0xdb, 0xb1, 0xe2, 0x88, 0x36, 0x5c,
+ 0x57, 0x3d, 0x83, 0xe9, 0x95, 0xff, 0x41, 0x2b, 0x20, 0x4a,
+ 0xf4, 0x9e, 0x0c, 0x66, 0xd8, 0xb2, 0xb9, 0xd3, 0x6d, 0x07,
+ 0x7b, 0x11, 0xaf, 0xc5, 0xce, 0xa4, 0x1a, 0x70, 0xbc, 0xd6,
+ 0x68, 0x02, 0x09, 0x63, 0xdd, 0xb7, 0xcb, 0xa1, 0x1f, 0x75,
+ 0x7e, 0x14, 0xaa, 0xc0, 0x52, 0x38, 0x86, 0xec, 0xe7, 0x8d,
+ 0x33, 0x59, 0x25, 0x4f, 0xf1, 0x9b, 0x90, 0xfa, 0x44, 0x2e,
+ 0x7d, 0x17, 0xa9, 0xc3, 0xc8, 0xa2, 0x1c, 0x76, 0x0a, 0x60,
+ 0xde, 0xb4, 0xbf, 0xd5, 0x6b, 0x01, 0x93, 0xf9, 0x47, 0x2d,
+ 0x26, 0x4c, 0xf2, 0x98, 0xe4, 0x8e, 0x30, 0x5a, 0x51, 0x3b,
+ 0x85, 0xef, 0x00, 0x6b, 0xd6, 0xbd, 0xb1, 0xda, 0x67, 0x0c,
+ 0x7f, 0x14, 0xa9, 0xc2, 0xce, 0xa5, 0x18, 0x73, 0xfe, 0x95,
+ 0x28, 0x43, 0x4f, 0x24, 0x99, 0xf2, 0x81, 0xea, 0x57, 0x3c,
+ 0x30, 0x5b, 0xe6, 0x8d, 0xe1, 0x8a, 0x37, 0x5c, 0x50, 0x3b,
+ 0x86, 0xed, 0x9e, 0xf5, 0x48, 0x23, 0x2f, 0x44, 0xf9, 0x92,
+ 0x1f, 0x74, 0xc9, 0xa2, 0xae, 0xc5, 0x78, 0x13, 0x60, 0x0b,
+ 0xb6, 0xdd, 0xd1, 0xba, 0x07, 0x6c, 0xdf, 0xb4, 0x09, 0x62,
+ 0x6e, 0x05, 0xb8, 0xd3, 0xa0, 0xcb, 0x76, 0x1d, 0x11, 0x7a,
+ 0xc7, 0xac, 0x21, 0x4a, 0xf7, 0x9c, 0x90, 0xfb, 0x46, 0x2d,
+ 0x5e, 0x35, 0x88, 0xe3, 0xef, 0x84, 0x39, 0x52, 0x3e, 0x55,
+ 0xe8, 0x83, 0x8f, 0xe4, 0x59, 0x32, 0x41, 0x2a, 0x97, 0xfc,
+ 0xf0, 0x9b, 0x26, 0x4d, 0xc0, 0xab, 0x16, 0x7d, 0x71, 0x1a,
+ 0xa7, 0xcc, 0xbf, 0xd4, 0x69, 0x02, 0x0e, 0x65, 0xd8, 0xb3,
+ 0xa3, 0xc8, 0x75, 0x1e, 0x12, 0x79, 0xc4, 0xaf, 0xdc, 0xb7,
+ 0x0a, 0x61, 0x6d, 0x06, 0xbb, 0xd0, 0x5d, 0x36, 0x8b, 0xe0,
+ 0xec, 0x87, 0x3a, 0x51, 0x22, 0x49, 0xf4, 0x9f, 0x93, 0xf8,
+ 0x45, 0x2e, 0x42, 0x29, 0x94, 0xff, 0xf3, 0x98, 0x25, 0x4e,
+ 0x3d, 0x56, 0xeb, 0x80, 0x8c, 0xe7, 0x5a, 0x31, 0xbc, 0xd7,
+ 0x6a, 0x01, 0x0d, 0x66, 0xdb, 0xb0, 0xc3, 0xa8, 0x15, 0x7e,
+ 0x72, 0x19, 0xa4, 0xcf, 0x7c, 0x17, 0xaa, 0xc1, 0xcd, 0xa6,
+ 0x1b, 0x70, 0x03, 0x68, 0xd5, 0xbe, 0xb2, 0xd9, 0x64, 0x0f,
+ 0x82, 0xe9, 0x54, 0x3f, 0x33, 0x58, 0xe5, 0x8e, 0xfd, 0x96,
+ 0x2b, 0x40, 0x4c, 0x27, 0x9a, 0xf1, 0x9d, 0xf6, 0x4b, 0x20,
+ 0x2c, 0x47, 0xfa, 0x91, 0xe2, 0x89, 0x34, 0x5f, 0x53, 0x38,
+ 0x85, 0xee, 0x63, 0x08, 0xb5, 0xde, 0xd2, 0xb9, 0x04, 0x6f,
+ 0x1c, 0x77, 0xca, 0xa1, 0xad, 0xc6, 0x7b, 0x10, 0x00, 0x6c,
+ 0xd8, 0xb4, 0xad, 0xc1, 0x75, 0x19, 0x47, 0x2b, 0x9f, 0xf3,
+ 0xea, 0x86, 0x32, 0x5e, 0x8e, 0xe2, 0x56, 0x3a, 0x23, 0x4f,
+ 0xfb, 0x97, 0xc9, 0xa5, 0x11, 0x7d, 0x64, 0x08, 0xbc, 0xd0,
+ 0x01, 0x6d, 0xd9, 0xb5, 0xac, 0xc0, 0x74, 0x18, 0x46, 0x2a,
+ 0x9e, 0xf2, 0xeb, 0x87, 0x33, 0x5f, 0x8f, 0xe3, 0x57, 0x3b,
+ 0x22, 0x4e, 0xfa, 0x96, 0xc8, 0xa4, 0x10, 0x7c, 0x65, 0x09,
+ 0xbd, 0xd1, 0x02, 0x6e, 0xda, 0xb6, 0xaf, 0xc3, 0x77, 0x1b,
+ 0x45, 0x29, 0x9d, 0xf1, 0xe8, 0x84, 0x30, 0x5c, 0x8c, 0xe0,
+ 0x54, 0x38, 0x21, 0x4d, 0xf9, 0x95, 0xcb, 0xa7, 0x13, 0x7f,
+ 0x66, 0x0a, 0xbe, 0xd2, 0x03, 0x6f, 0xdb, 0xb7, 0xae, 0xc2,
+ 0x76, 0x1a, 0x44, 0x28, 0x9c, 0xf0, 0xe9, 0x85, 0x31, 0x5d,
+ 0x8d, 0xe1, 0x55, 0x39, 0x20, 0x4c, 0xf8, 0x94, 0xca, 0xa6,
+ 0x12, 0x7e, 0x67, 0x0b, 0xbf, 0xd3, 0x04, 0x68, 0xdc, 0xb0,
+ 0xa9, 0xc5, 0x71, 0x1d, 0x43, 0x2f, 0x9b, 0xf7, 0xee, 0x82,
+ 0x36, 0x5a, 0x8a, 0xe6, 0x52, 0x3e, 0x27, 0x4b, 0xff, 0x93,
+ 0xcd, 0xa1, 0x15, 0x79, 0x60, 0x0c, 0xb8, 0xd4, 0x05, 0x69,
+ 0xdd, 0xb1, 0xa8, 0xc4, 0x70, 0x1c, 0x42, 0x2e, 0x9a, 0xf6,
+ 0xef, 0x83, 0x37, 0x5b, 0x8b, 0xe7, 0x53, 0x3f, 0x26, 0x4a,
+ 0xfe, 0x92, 0xcc, 0xa0, 0x14, 0x78, 0x61, 0x0d, 0xb9, 0xd5,
+ 0x06, 0x6a, 0xde, 0xb2, 0xab, 0xc7, 0x73, 0x1f, 0x41, 0x2d,
+ 0x99, 0xf5, 0xec, 0x80, 0x34, 0x58, 0x88, 0xe4, 0x50, 0x3c,
+ 0x25, 0x49, 0xfd, 0x91, 0xcf, 0xa3, 0x17, 0x7b, 0x62, 0x0e,
+ 0xba, 0xd6, 0x07, 0x6b, 0xdf, 0xb3, 0xaa, 0xc6, 0x72, 0x1e,
+ 0x40, 0x2c, 0x98, 0xf4, 0xed, 0x81, 0x35, 0x59, 0x89, 0xe5,
+ 0x51, 0x3d, 0x24, 0x48, 0xfc, 0x90, 0xce, 0xa2, 0x16, 0x7a,
+ 0x63, 0x0f, 0xbb, 0xd7, 0x00, 0x6d, 0xda, 0xb7, 0xa9, 0xc4,
+ 0x73, 0x1e, 0x4f, 0x22, 0x95, 0xf8, 0xe6, 0x8b, 0x3c, 0x51,
+ 0x9e, 0xf3, 0x44, 0x29, 0x37, 0x5a, 0xed, 0x80, 0xd1, 0xbc,
+ 0x0b, 0x66, 0x78, 0x15, 0xa2, 0xcf, 0x21, 0x4c, 0xfb, 0x96,
+ 0x88, 0xe5, 0x52, 0x3f, 0x6e, 0x03, 0xb4, 0xd9, 0xc7, 0xaa,
+ 0x1d, 0x70, 0xbf, 0xd2, 0x65, 0x08, 0x16, 0x7b, 0xcc, 0xa1,
+ 0xf0, 0x9d, 0x2a, 0x47, 0x59, 0x34, 0x83, 0xee, 0x42, 0x2f,
+ 0x98, 0xf5, 0xeb, 0x86, 0x31, 0x5c, 0x0d, 0x60, 0xd7, 0xba,
+ 0xa4, 0xc9, 0x7e, 0x13, 0xdc, 0xb1, 0x06, 0x6b, 0x75, 0x18,
+ 0xaf, 0xc2, 0x93, 0xfe, 0x49, 0x24, 0x3a, 0x57, 0xe0, 0x8d,
+ 0x63, 0x0e, 0xb9, 0xd4, 0xca, 0xa7, 0x10, 0x7d, 0x2c, 0x41,
+ 0xf6, 0x9b, 0x85, 0xe8, 0x5f, 0x32, 0xfd, 0x90, 0x27, 0x4a,
+ 0x54, 0x39, 0x8e, 0xe3, 0xb2, 0xdf, 0x68, 0x05, 0x1b, 0x76,
+ 0xc1, 0xac, 0x84, 0xe9, 0x5e, 0x33, 0x2d, 0x40, 0xf7, 0x9a,
+ 0xcb, 0xa6, 0x11, 0x7c, 0x62, 0x0f, 0xb8, 0xd5, 0x1a, 0x77,
+ 0xc0, 0xad, 0xb3, 0xde, 0x69, 0x04, 0x55, 0x38, 0x8f, 0xe2,
+ 0xfc, 0x91, 0x26, 0x4b, 0xa5, 0xc8, 0x7f, 0x12, 0x0c, 0x61,
+ 0xd6, 0xbb, 0xea, 0x87, 0x30, 0x5d, 0x43, 0x2e, 0x99, 0xf4,
+ 0x3b, 0x56, 0xe1, 0x8c, 0x92, 0xff, 0x48, 0x25, 0x74, 0x19,
+ 0xae, 0xc3, 0xdd, 0xb0, 0x07, 0x6a, 0xc6, 0xab, 0x1c, 0x71,
+ 0x6f, 0x02, 0xb5, 0xd8, 0x89, 0xe4, 0x53, 0x3e, 0x20, 0x4d,
+ 0xfa, 0x97, 0x58, 0x35, 0x82, 0xef, 0xf1, 0x9c, 0x2b, 0x46,
+ 0x17, 0x7a, 0xcd, 0xa0, 0xbe, 0xd3, 0x64, 0x09, 0xe7, 0x8a,
+ 0x3d, 0x50, 0x4e, 0x23, 0x94, 0xf9, 0xa8, 0xc5, 0x72, 0x1f,
+ 0x01, 0x6c, 0xdb, 0xb6, 0x79, 0x14, 0xa3, 0xce, 0xd0, 0xbd,
+ 0x0a, 0x67, 0x36, 0x5b, 0xec, 0x81, 0x9f, 0xf2, 0x45, 0x28,
+ 0x00, 0x6e, 0xdc, 0xb2, 0xa5, 0xcb, 0x79, 0x17, 0x57, 0x39,
+ 0x8b, 0xe5, 0xf2, 0x9c, 0x2e, 0x40, 0xae, 0xc0, 0x72, 0x1c,
+ 0x0b, 0x65, 0xd7, 0xb9, 0xf9, 0x97, 0x25, 0x4b, 0x5c, 0x32,
+ 0x80, 0xee, 0x41, 0x2f, 0x9d, 0xf3, 0xe4, 0x8a, 0x38, 0x56,
+ 0x16, 0x78, 0xca, 0xa4, 0xb3, 0xdd, 0x6f, 0x01, 0xef, 0x81,
+ 0x33, 0x5d, 0x4a, 0x24, 0x96, 0xf8, 0xb8, 0xd6, 0x64, 0x0a,
+ 0x1d, 0x73, 0xc1, 0xaf, 0x82, 0xec, 0x5e, 0x30, 0x27, 0x49,
+ 0xfb, 0x95, 0xd5, 0xbb, 0x09, 0x67, 0x70, 0x1e, 0xac, 0xc2,
+ 0x2c, 0x42, 0xf0, 0x9e, 0x89, 0xe7, 0x55, 0x3b, 0x7b, 0x15,
+ 0xa7, 0xc9, 0xde, 0xb0, 0x02, 0x6c, 0xc3, 0xad, 0x1f, 0x71,
+ 0x66, 0x08, 0xba, 0xd4, 0x94, 0xfa, 0x48, 0x26, 0x31, 0x5f,
+ 0xed, 0x83, 0x6d, 0x03, 0xb1, 0xdf, 0xc8, 0xa6, 0x14, 0x7a,
+ 0x3a, 0x54, 0xe6, 0x88, 0x9f, 0xf1, 0x43, 0x2d, 0x19, 0x77,
+ 0xc5, 0xab, 0xbc, 0xd2, 0x60, 0x0e, 0x4e, 0x20, 0x92, 0xfc,
+ 0xeb, 0x85, 0x37, 0x59, 0xb7, 0xd9, 0x6b, 0x05, 0x12, 0x7c,
+ 0xce, 0xa0, 0xe0, 0x8e, 0x3c, 0x52, 0x45, 0x2b, 0x99, 0xf7,
+ 0x58, 0x36, 0x84, 0xea, 0xfd, 0x93, 0x21, 0x4f, 0x0f, 0x61,
+ 0xd3, 0xbd, 0xaa, 0xc4, 0x76, 0x18, 0xf6, 0x98, 0x2a, 0x44,
+ 0x53, 0x3d, 0x8f, 0xe1, 0xa1, 0xcf, 0x7d, 0x13, 0x04, 0x6a,
+ 0xd8, 0xb6, 0x9b, 0xf5, 0x47, 0x29, 0x3e, 0x50, 0xe2, 0x8c,
+ 0xcc, 0xa2, 0x10, 0x7e, 0x69, 0x07, 0xb5, 0xdb, 0x35, 0x5b,
+ 0xe9, 0x87, 0x90, 0xfe, 0x4c, 0x22, 0x62, 0x0c, 0xbe, 0xd0,
+ 0xc7, 0xa9, 0x1b, 0x75, 0xda, 0xb4, 0x06, 0x68, 0x7f, 0x11,
+ 0xa3, 0xcd, 0x8d, 0xe3, 0x51, 0x3f, 0x28, 0x46, 0xf4, 0x9a,
+ 0x74, 0x1a, 0xa8, 0xc6, 0xd1, 0xbf, 0x0d, 0x63, 0x23, 0x4d,
+ 0xff, 0x91, 0x86, 0xe8, 0x5a, 0x34, 0x00, 0x6f, 0xde, 0xb1,
+ 0xa1, 0xce, 0x7f, 0x10, 0x5f, 0x30, 0x81, 0xee, 0xfe, 0x91,
+ 0x20, 0x4f, 0xbe, 0xd1, 0x60, 0x0f, 0x1f, 0x70, 0xc1, 0xae,
+ 0xe1, 0x8e, 0x3f, 0x50, 0x40, 0x2f, 0x9e, 0xf1, 0x61, 0x0e,
+ 0xbf, 0xd0, 0xc0, 0xaf, 0x1e, 0x71, 0x3e, 0x51, 0xe0, 0x8f,
+ 0x9f, 0xf0, 0x41, 0x2e, 0xdf, 0xb0, 0x01, 0x6e, 0x7e, 0x11,
+ 0xa0, 0xcf, 0x80, 0xef, 0x5e, 0x31, 0x21, 0x4e, 0xff, 0x90,
+ 0xc2, 0xad, 0x1c, 0x73, 0x63, 0x0c, 0xbd, 0xd2, 0x9d, 0xf2,
+ 0x43, 0x2c, 0x3c, 0x53, 0xe2, 0x8d, 0x7c, 0x13, 0xa2, 0xcd,
+ 0xdd, 0xb2, 0x03, 0x6c, 0x23, 0x4c, 0xfd, 0x92, 0x82, 0xed,
+ 0x5c, 0x33, 0xa3, 0xcc, 0x7d, 0x12, 0x02, 0x6d, 0xdc, 0xb3,
+ 0xfc, 0x93, 0x22, 0x4d, 0x5d, 0x32, 0x83, 0xec, 0x1d, 0x72,
+ 0xc3, 0xac, 0xbc, 0xd3, 0x62, 0x0d, 0x42, 0x2d, 0x9c, 0xf3,
+ 0xe3, 0x8c, 0x3d, 0x52, 0x99, 0xf6, 0x47, 0x28, 0x38, 0x57,
+ 0xe6, 0x89, 0xc6, 0xa9, 0x18, 0x77, 0x67, 0x08, 0xb9, 0xd6,
+ 0x27, 0x48, 0xf9, 0x96, 0x86, 0xe9, 0x58, 0x37, 0x78, 0x17,
+ 0xa6, 0xc9, 0xd9, 0xb6, 0x07, 0x68, 0xf8, 0x97, 0x26, 0x49,
+ 0x59, 0x36, 0x87, 0xe8, 0xa7, 0xc8, 0x79, 0x16, 0x06, 0x69,
+ 0xd8, 0xb7, 0x46, 0x29, 0x98, 0xf7, 0xe7, 0x88, 0x39, 0x56,
+ 0x19, 0x76, 0xc7, 0xa8, 0xb8, 0xd7, 0x66, 0x09, 0x5b, 0x34,
+ 0x85, 0xea, 0xfa, 0x95, 0x24, 0x4b, 0x04, 0x6b, 0xda, 0xb5,
+ 0xa5, 0xca, 0x7b, 0x14, 0xe5, 0x8a, 0x3b, 0x54, 0x44, 0x2b,
+ 0x9a, 0xf5, 0xba, 0xd5, 0x64, 0x0b, 0x1b, 0x74, 0xc5, 0xaa,
+ 0x3a, 0x55, 0xe4, 0x8b, 0x9b, 0xf4, 0x45, 0x2a, 0x65, 0x0a,
+ 0xbb, 0xd4, 0xc4, 0xab, 0x1a, 0x75, 0x84, 0xeb, 0x5a, 0x35,
+ 0x25, 0x4a, 0xfb, 0x94, 0xdb, 0xb4, 0x05, 0x6a, 0x7a, 0x15,
+ 0xa4, 0xcb, 0x00, 0x70, 0xe0, 0x90, 0xdd, 0xad, 0x3d, 0x4d,
+ 0xa7, 0xd7, 0x47, 0x37, 0x7a, 0x0a, 0x9a, 0xea, 0x53, 0x23,
+ 0xb3, 0xc3, 0x8e, 0xfe, 0x6e, 0x1e, 0xf4, 0x84, 0x14, 0x64,
+ 0x29, 0x59, 0xc9, 0xb9, 0xa6, 0xd6, 0x46, 0x36, 0x7b, 0x0b,
+ 0x9b, 0xeb, 0x01, 0x71, 0xe1, 0x91, 0xdc, 0xac, 0x3c, 0x4c,
+ 0xf5, 0x85, 0x15, 0x65, 0x28, 0x58, 0xc8, 0xb8, 0x52, 0x22,
+ 0xb2, 0xc2, 0x8f, 0xff, 0x6f, 0x1f, 0x51, 0x21, 0xb1, 0xc1,
+ 0x8c, 0xfc, 0x6c, 0x1c, 0xf6, 0x86, 0x16, 0x66, 0x2b, 0x5b,
+ 0xcb, 0xbb, 0x02, 0x72, 0xe2, 0x92, 0xdf, 0xaf, 0x3f, 0x4f,
+ 0xa5, 0xd5, 0x45, 0x35, 0x78, 0x08, 0x98, 0xe8, 0xf7, 0x87,
+ 0x17, 0x67, 0x2a, 0x5a, 0xca, 0xba, 0x50, 0x20, 0xb0, 0xc0,
+ 0x8d, 0xfd, 0x6d, 0x1d, 0xa4, 0xd4, 0x44, 0x34, 0x79, 0x09,
+ 0x99, 0xe9, 0x03, 0x73, 0xe3, 0x93, 0xde, 0xae, 0x3e, 0x4e,
+ 0xa2, 0xd2, 0x42, 0x32, 0x7f, 0x0f, 0x9f, 0xef, 0x05, 0x75,
+ 0xe5, 0x95, 0xd8, 0xa8, 0x38, 0x48, 0xf1, 0x81, 0x11, 0x61,
+ 0x2c, 0x5c, 0xcc, 0xbc, 0x56, 0x26, 0xb6, 0xc6, 0x8b, 0xfb,
+ 0x6b, 0x1b, 0x04, 0x74, 0xe4, 0x94, 0xd9, 0xa9, 0x39, 0x49,
+ 0xa3, 0xd3, 0x43, 0x33, 0x7e, 0x0e, 0x9e, 0xee, 0x57, 0x27,
+ 0xb7, 0xc7, 0x8a, 0xfa, 0x6a, 0x1a, 0xf0, 0x80, 0x10, 0x60,
+ 0x2d, 0x5d, 0xcd, 0xbd, 0xf3, 0x83, 0x13, 0x63, 0x2e, 0x5e,
+ 0xce, 0xbe, 0x54, 0x24, 0xb4, 0xc4, 0x89, 0xf9, 0x69, 0x19,
+ 0xa0, 0xd0, 0x40, 0x30, 0x7d, 0x0d, 0x9d, 0xed, 0x07, 0x77,
+ 0xe7, 0x97, 0xda, 0xaa, 0x3a, 0x4a, 0x55, 0x25, 0xb5, 0xc5,
+ 0x88, 0xf8, 0x68, 0x18, 0xf2, 0x82, 0x12, 0x62, 0x2f, 0x5f,
+ 0xcf, 0xbf, 0x06, 0x76, 0xe6, 0x96, 0xdb, 0xab, 0x3b, 0x4b,
+ 0xa1, 0xd1, 0x41, 0x31, 0x7c, 0x0c, 0x9c, 0xec, 0x00, 0x71,
+ 0xe2, 0x93, 0xd9, 0xa8, 0x3b, 0x4a, 0xaf, 0xde, 0x4d, 0x3c,
+ 0x76, 0x07, 0x94, 0xe5, 0x43, 0x32, 0xa1, 0xd0, 0x9a, 0xeb,
+ 0x78, 0x09, 0xec, 0x9d, 0x0e, 0x7f, 0x35, 0x44, 0xd7, 0xa6,
+ 0x86, 0xf7, 0x64, 0x15, 0x5f, 0x2e, 0xbd, 0xcc, 0x29, 0x58,
+ 0xcb, 0xba, 0xf0, 0x81, 0x12, 0x63, 0xc5, 0xb4, 0x27, 0x56,
+ 0x1c, 0x6d, 0xfe, 0x8f, 0x6a, 0x1b, 0x88, 0xf9, 0xb3, 0xc2,
+ 0x51, 0x20, 0x11, 0x60, 0xf3, 0x82, 0xc8, 0xb9, 0x2a, 0x5b,
+ 0xbe, 0xcf, 0x5c, 0x2d, 0x67, 0x16, 0x85, 0xf4, 0x52, 0x23,
+ 0xb0, 0xc1, 0x8b, 0xfa, 0x69, 0x18, 0xfd, 0x8c, 0x1f, 0x6e,
+ 0x24, 0x55, 0xc6, 0xb7, 0x97, 0xe6, 0x75, 0x04, 0x4e, 0x3f,
+ 0xac, 0xdd, 0x38, 0x49, 0xda, 0xab, 0xe1, 0x90, 0x03, 0x72,
+ 0xd4, 0xa5, 0x36, 0x47, 0x0d, 0x7c, 0xef, 0x9e, 0x7b, 0x0a,
+ 0x99, 0xe8, 0xa2, 0xd3, 0x40, 0x31, 0x22, 0x53, 0xc0, 0xb1,
+ 0xfb, 0x8a, 0x19, 0x68, 0x8d, 0xfc, 0x6f, 0x1e, 0x54, 0x25,
+ 0xb6, 0xc7, 0x61, 0x10, 0x83, 0xf2, 0xb8, 0xc9, 0x5a, 0x2b,
+ 0xce, 0xbf, 0x2c, 0x5d, 0x17, 0x66, 0xf5, 0x84, 0xa4, 0xd5,
+ 0x46, 0x37, 0x7d, 0x0c, 0x9f, 0xee, 0x0b, 0x7a, 0xe9, 0x98,
+ 0xd2, 0xa3, 0x30, 0x41, 0xe7, 0x96, 0x05, 0x74, 0x3e, 0x4f,
+ 0xdc, 0xad, 0x48, 0x39, 0xaa, 0xdb, 0x91, 0xe0, 0x73, 0x02,
+ 0x33, 0x42, 0xd1, 0xa0, 0xea, 0x9b, 0x08, 0x79, 0x9c, 0xed,
+ 0x7e, 0x0f, 0x45, 0x34, 0xa7, 0xd6, 0x70, 0x01, 0x92, 0xe3,
+ 0xa9, 0xd8, 0x4b, 0x3a, 0xdf, 0xae, 0x3d, 0x4c, 0x06, 0x77,
+ 0xe4, 0x95, 0xb5, 0xc4, 0x57, 0x26, 0x6c, 0x1d, 0x8e, 0xff,
+ 0x1a, 0x6b, 0xf8, 0x89, 0xc3, 0xb2, 0x21, 0x50, 0xf6, 0x87,
+ 0x14, 0x65, 0x2f, 0x5e, 0xcd, 0xbc, 0x59, 0x28, 0xbb, 0xca,
+ 0x80, 0xf1, 0x62, 0x13, 0x00, 0x72, 0xe4, 0x96, 0xd5, 0xa7,
+ 0x31, 0x43, 0xb7, 0xc5, 0x53, 0x21, 0x62, 0x10, 0x86, 0xf4,
+ 0x73, 0x01, 0x97, 0xe5, 0xa6, 0xd4, 0x42, 0x30, 0xc4, 0xb6,
+ 0x20, 0x52, 0x11, 0x63, 0xf5, 0x87, 0xe6, 0x94, 0x02, 0x70,
+ 0x33, 0x41, 0xd7, 0xa5, 0x51, 0x23, 0xb5, 0xc7, 0x84, 0xf6,
+ 0x60, 0x12, 0x95, 0xe7, 0x71, 0x03, 0x40, 0x32, 0xa4, 0xd6,
+ 0x22, 0x50, 0xc6, 0xb4, 0xf7, 0x85, 0x13, 0x61, 0xd1, 0xa3,
+ 0x35, 0x47, 0x04, 0x76, 0xe0, 0x92, 0x66, 0x14, 0x82, 0xf0,
+ 0xb3, 0xc1, 0x57, 0x25, 0xa2, 0xd0, 0x46, 0x34, 0x77, 0x05,
+ 0x93, 0xe1, 0x15, 0x67, 0xf1, 0x83, 0xc0, 0xb2, 0x24, 0x56,
+ 0x37, 0x45, 0xd3, 0xa1, 0xe2, 0x90, 0x06, 0x74, 0x80, 0xf2,
+ 0x64, 0x16, 0x55, 0x27, 0xb1, 0xc3, 0x44, 0x36, 0xa0, 0xd2,
+ 0x91, 0xe3, 0x75, 0x07, 0xf3, 0x81, 0x17, 0x65, 0x26, 0x54,
+ 0xc2, 0xb0, 0xbf, 0xcd, 0x5b, 0x29, 0x6a, 0x18, 0x8e, 0xfc,
+ 0x08, 0x7a, 0xec, 0x9e, 0xdd, 0xaf, 0x39, 0x4b, 0xcc, 0xbe,
+ 0x28, 0x5a, 0x19, 0x6b, 0xfd, 0x8f, 0x7b, 0x09, 0x9f, 0xed,
+ 0xae, 0xdc, 0x4a, 0x38, 0x59, 0x2b, 0xbd, 0xcf, 0x8c, 0xfe,
+ 0x68, 0x1a, 0xee, 0x9c, 0x0a, 0x78, 0x3b, 0x49, 0xdf, 0xad,
+ 0x2a, 0x58, 0xce, 0xbc, 0xff, 0x8d, 0x1b, 0x69, 0x9d, 0xef,
+ 0x79, 0x0b, 0x48, 0x3a, 0xac, 0xde, 0x6e, 0x1c, 0x8a, 0xf8,
+ 0xbb, 0xc9, 0x5f, 0x2d, 0xd9, 0xab, 0x3d, 0x4f, 0x0c, 0x7e,
+ 0xe8, 0x9a, 0x1d, 0x6f, 0xf9, 0x8b, 0xc8, 0xba, 0x2c, 0x5e,
+ 0xaa, 0xd8, 0x4e, 0x3c, 0x7f, 0x0d, 0x9b, 0xe9, 0x88, 0xfa,
+ 0x6c, 0x1e, 0x5d, 0x2f, 0xb9, 0xcb, 0x3f, 0x4d, 0xdb, 0xa9,
+ 0xea, 0x98, 0x0e, 0x7c, 0xfb, 0x89, 0x1f, 0x6d, 0x2e, 0x5c,
+ 0xca, 0xb8, 0x4c, 0x3e, 0xa8, 0xda, 0x99, 0xeb, 0x7d, 0x0f,
+ 0x00, 0x73, 0xe6, 0x95, 0xd1, 0xa2, 0x37, 0x44, 0xbf, 0xcc,
+ 0x59, 0x2a, 0x6e, 0x1d, 0x88, 0xfb, 0x63, 0x10, 0x85, 0xf6,
+ 0xb2, 0xc1, 0x54, 0x27, 0xdc, 0xaf, 0x3a, 0x49, 0x0d, 0x7e,
+ 0xeb, 0x98, 0xc6, 0xb5, 0x20, 0x53, 0x17, 0x64, 0xf1, 0x82,
+ 0x79, 0x0a, 0x9f, 0xec, 0xa8, 0xdb, 0x4e, 0x3d, 0xa5, 0xd6,
+ 0x43, 0x30, 0x74, 0x07, 0x92, 0xe1, 0x1a, 0x69, 0xfc, 0x8f,
+ 0xcb, 0xb8, 0x2d, 0x5e, 0x91, 0xe2, 0x77, 0x04, 0x40, 0x33,
+ 0xa6, 0xd5, 0x2e, 0x5d, 0xc8, 0xbb, 0xff, 0x8c, 0x19, 0x6a,
+ 0xf2, 0x81, 0x14, 0x67, 0x23, 0x50, 0xc5, 0xb6, 0x4d, 0x3e,
+ 0xab, 0xd8, 0x9c, 0xef, 0x7a, 0x09, 0x57, 0x24, 0xb1, 0xc2,
+ 0x86, 0xf5, 0x60, 0x13, 0xe8, 0x9b, 0x0e, 0x7d, 0x39, 0x4a,
+ 0xdf, 0xac, 0x34, 0x47, 0xd2, 0xa1, 0xe5, 0x96, 0x03, 0x70,
+ 0x8b, 0xf8, 0x6d, 0x1e, 0x5a, 0x29, 0xbc, 0xcf, 0x3f, 0x4c,
+ 0xd9, 0xaa, 0xee, 0x9d, 0x08, 0x7b, 0x80, 0xf3, 0x66, 0x15,
+ 0x51, 0x22, 0xb7, 0xc4, 0x5c, 0x2f, 0xba, 0xc9, 0x8d, 0xfe,
+ 0x6b, 0x18, 0xe3, 0x90, 0x05, 0x76, 0x32, 0x41, 0xd4, 0xa7,
+ 0xf9, 0x8a, 0x1f, 0x6c, 0x28, 0x5b, 0xce, 0xbd, 0x46, 0x35,
+ 0xa0, 0xd3, 0x97, 0xe4, 0x71, 0x02, 0x9a, 0xe9, 0x7c, 0x0f,
+ 0x4b, 0x38, 0xad, 0xde, 0x25, 0x56, 0xc3, 0xb0, 0xf4, 0x87,
+ 0x12, 0x61, 0xae, 0xdd, 0x48, 0x3b, 0x7f, 0x0c, 0x99, 0xea,
+ 0x11, 0x62, 0xf7, 0x84, 0xc0, 0xb3, 0x26, 0x55, 0xcd, 0xbe,
+ 0x2b, 0x58, 0x1c, 0x6f, 0xfa, 0x89, 0x72, 0x01, 0x94, 0xe7,
+ 0xa3, 0xd0, 0x45, 0x36, 0x68, 0x1b, 0x8e, 0xfd, 0xb9, 0xca,
+ 0x5f, 0x2c, 0xd7, 0xa4, 0x31, 0x42, 0x06, 0x75, 0xe0, 0x93,
+ 0x0b, 0x78, 0xed, 0x9e, 0xda, 0xa9, 0x3c, 0x4f, 0xb4, 0xc7,
+ 0x52, 0x21, 0x65, 0x16, 0x83, 0xf0, 0x00, 0x74, 0xe8, 0x9c,
+ 0xcd, 0xb9, 0x25, 0x51, 0x87, 0xf3, 0x6f, 0x1b, 0x4a, 0x3e,
+ 0xa2, 0xd6, 0x13, 0x67, 0xfb, 0x8f, 0xde, 0xaa, 0x36, 0x42,
+ 0x94, 0xe0, 0x7c, 0x08, 0x59, 0x2d, 0xb1, 0xc5, 0x26, 0x52,
+ 0xce, 0xba, 0xeb, 0x9f, 0x03, 0x77, 0xa1, 0xd5, 0x49, 0x3d,
+ 0x6c, 0x18, 0x84, 0xf0, 0x35, 0x41, 0xdd, 0xa9, 0xf8, 0x8c,
+ 0x10, 0x64, 0xb2, 0xc6, 0x5a, 0x2e, 0x7f, 0x0b, 0x97, 0xe3,
+ 0x4c, 0x38, 0xa4, 0xd0, 0x81, 0xf5, 0x69, 0x1d, 0xcb, 0xbf,
+ 0x23, 0x57, 0x06, 0x72, 0xee, 0x9a, 0x5f, 0x2b, 0xb7, 0xc3,
+ 0x92, 0xe6, 0x7a, 0x0e, 0xd8, 0xac, 0x30, 0x44, 0x15, 0x61,
+ 0xfd, 0x89, 0x6a, 0x1e, 0x82, 0xf6, 0xa7, 0xd3, 0x4f, 0x3b,
+ 0xed, 0x99, 0x05, 0x71, 0x20, 0x54, 0xc8, 0xbc, 0x79, 0x0d,
+ 0x91, 0xe5, 0xb4, 0xc0, 0x5c, 0x28, 0xfe, 0x8a, 0x16, 0x62,
+ 0x33, 0x47, 0xdb, 0xaf, 0x98, 0xec, 0x70, 0x04, 0x55, 0x21,
+ 0xbd, 0xc9, 0x1f, 0x6b, 0xf7, 0x83, 0xd2, 0xa6, 0x3a, 0x4e,
+ 0x8b, 0xff, 0x63, 0x17, 0x46, 0x32, 0xae, 0xda, 0x0c, 0x78,
+ 0xe4, 0x90, 0xc1, 0xb5, 0x29, 0x5d, 0xbe, 0xca, 0x56, 0x22,
+ 0x73, 0x07, 0x9b, 0xef, 0x39, 0x4d, 0xd1, 0xa5, 0xf4, 0x80,
+ 0x1c, 0x68, 0xad, 0xd9, 0x45, 0x31, 0x60, 0x14, 0x88, 0xfc,
+ 0x2a, 0x5e, 0xc2, 0xb6, 0xe7, 0x93, 0x0f, 0x7b, 0xd4, 0xa0,
+ 0x3c, 0x48, 0x19, 0x6d, 0xf1, 0x85, 0x53, 0x27, 0xbb, 0xcf,
+ 0x9e, 0xea, 0x76, 0x02, 0xc7, 0xb3, 0x2f, 0x5b, 0x0a, 0x7e,
+ 0xe2, 0x96, 0x40, 0x34, 0xa8, 0xdc, 0x8d, 0xf9, 0x65, 0x11,
+ 0xf2, 0x86, 0x1a, 0x6e, 0x3f, 0x4b, 0xd7, 0xa3, 0x75, 0x01,
+ 0x9d, 0xe9, 0xb8, 0xcc, 0x50, 0x24, 0xe1, 0x95, 0x09, 0x7d,
+ 0x2c, 0x58, 0xc4, 0xb0, 0x66, 0x12, 0x8e, 0xfa, 0xab, 0xdf,
+ 0x43, 0x37, 0x00, 0x75, 0xea, 0x9f, 0xc9, 0xbc, 0x23, 0x56,
+ 0x8f, 0xfa, 0x65, 0x10, 0x46, 0x33, 0xac, 0xd9, 0x03, 0x76,
+ 0xe9, 0x9c, 0xca, 0xbf, 0x20, 0x55, 0x8c, 0xf9, 0x66, 0x13,
+ 0x45, 0x30, 0xaf, 0xda, 0x06, 0x73, 0xec, 0x99, 0xcf, 0xba,
+ 0x25, 0x50, 0x89, 0xfc, 0x63, 0x16, 0x40, 0x35, 0xaa, 0xdf,
+ 0x05, 0x70, 0xef, 0x9a, 0xcc, 0xb9, 0x26, 0x53, 0x8a, 0xff,
+ 0x60, 0x15, 0x43, 0x36, 0xa9, 0xdc, 0x0c, 0x79, 0xe6, 0x93,
+ 0xc5, 0xb0, 0x2f, 0x5a, 0x83, 0xf6, 0x69, 0x1c, 0x4a, 0x3f,
+ 0xa0, 0xd5, 0x0f, 0x7a, 0xe5, 0x90, 0xc6, 0xb3, 0x2c, 0x59,
+ 0x80, 0xf5, 0x6a, 0x1f, 0x49, 0x3c, 0xa3, 0xd6, 0x0a, 0x7f,
+ 0xe0, 0x95, 0xc3, 0xb6, 0x29, 0x5c, 0x85, 0xf0, 0x6f, 0x1a,
+ 0x4c, 0x39, 0xa6, 0xd3, 0x09, 0x7c, 0xe3, 0x96, 0xc0, 0xb5,
+ 0x2a, 0x5f, 0x86, 0xf3, 0x6c, 0x19, 0x4f, 0x3a, 0xa5, 0xd0,
+ 0x18, 0x6d, 0xf2, 0x87, 0xd1, 0xa4, 0x3b, 0x4e, 0x97, 0xe2,
+ 0x7d, 0x08, 0x5e, 0x2b, 0xb4, 0xc1, 0x1b, 0x6e, 0xf1, 0x84,
+ 0xd2, 0xa7, 0x38, 0x4d, 0x94, 0xe1, 0x7e, 0x0b, 0x5d, 0x28,
+ 0xb7, 0xc2, 0x1e, 0x6b, 0xf4, 0x81, 0xd7, 0xa2, 0x3d, 0x48,
+ 0x91, 0xe4, 0x7b, 0x0e, 0x58, 0x2d, 0xb2, 0xc7, 0x1d, 0x68,
+ 0xf7, 0x82, 0xd4, 0xa1, 0x3e, 0x4b, 0x92, 0xe7, 0x78, 0x0d,
+ 0x5b, 0x2e, 0xb1, 0xc4, 0x14, 0x61, 0xfe, 0x8b, 0xdd, 0xa8,
+ 0x37, 0x42, 0x9b, 0xee, 0x71, 0x04, 0x52, 0x27, 0xb8, 0xcd,
+ 0x17, 0x62, 0xfd, 0x88, 0xde, 0xab, 0x34, 0x41, 0x98, 0xed,
+ 0x72, 0x07, 0x51, 0x24, 0xbb, 0xce, 0x12, 0x67, 0xf8, 0x8d,
+ 0xdb, 0xae, 0x31, 0x44, 0x9d, 0xe8, 0x77, 0x02, 0x54, 0x21,
+ 0xbe, 0xcb, 0x11, 0x64, 0xfb, 0x8e, 0xd8, 0xad, 0x32, 0x47,
+ 0x9e, 0xeb, 0x74, 0x01, 0x57, 0x22, 0xbd, 0xc8, 0x00, 0x76,
+ 0xec, 0x9a, 0xc5, 0xb3, 0x29, 0x5f, 0x97, 0xe1, 0x7b, 0x0d,
+ 0x52, 0x24, 0xbe, 0xc8, 0x33, 0x45, 0xdf, 0xa9, 0xf6, 0x80,
+ 0x1a, 0x6c, 0xa4, 0xd2, 0x48, 0x3e, 0x61, 0x17, 0x8d, 0xfb,
+ 0x66, 0x10, 0x8a, 0xfc, 0xa3, 0xd5, 0x4f, 0x39, 0xf1, 0x87,
+ 0x1d, 0x6b, 0x34, 0x42, 0xd8, 0xae, 0x55, 0x23, 0xb9, 0xcf,
+ 0x90, 0xe6, 0x7c, 0x0a, 0xc2, 0xb4, 0x2e, 0x58, 0x07, 0x71,
+ 0xeb, 0x9d, 0xcc, 0xba, 0x20, 0x56, 0x09, 0x7f, 0xe5, 0x93,
+ 0x5b, 0x2d, 0xb7, 0xc1, 0x9e, 0xe8, 0x72, 0x04, 0xff, 0x89,
+ 0x13, 0x65, 0x3a, 0x4c, 0xd6, 0xa0, 0x68, 0x1e, 0x84, 0xf2,
+ 0xad, 0xdb, 0x41, 0x37, 0xaa, 0xdc, 0x46, 0x30, 0x6f, 0x19,
+ 0x83, 0xf5, 0x3d, 0x4b, 0xd1, 0xa7, 0xf8, 0x8e, 0x14, 0x62,
+ 0x99, 0xef, 0x75, 0x03, 0x5c, 0x2a, 0xb0, 0xc6, 0x0e, 0x78,
+ 0xe2, 0x94, 0xcb, 0xbd, 0x27, 0x51, 0x85, 0xf3, 0x69, 0x1f,
+ 0x40, 0x36, 0xac, 0xda, 0x12, 0x64, 0xfe, 0x88, 0xd7, 0xa1,
+ 0x3b, 0x4d, 0xb6, 0xc0, 0x5a, 0x2c, 0x73, 0x05, 0x9f, 0xe9,
+ 0x21, 0x57, 0xcd, 0xbb, 0xe4, 0x92, 0x08, 0x7e, 0xe3, 0x95,
+ 0x0f, 0x79, 0x26, 0x50, 0xca, 0xbc, 0x74, 0x02, 0x98, 0xee,
+ 0xb1, 0xc7, 0x5d, 0x2b, 0xd0, 0xa6, 0x3c, 0x4a, 0x15, 0x63,
+ 0xf9, 0x8f, 0x47, 0x31, 0xab, 0xdd, 0x82, 0xf4, 0x6e, 0x18,
+ 0x49, 0x3f, 0xa5, 0xd3, 0x8c, 0xfa, 0x60, 0x16, 0xde, 0xa8,
+ 0x32, 0x44, 0x1b, 0x6d, 0xf7, 0x81, 0x7a, 0x0c, 0x96, 0xe0,
+ 0xbf, 0xc9, 0x53, 0x25, 0xed, 0x9b, 0x01, 0x77, 0x28, 0x5e,
+ 0xc4, 0xb2, 0x2f, 0x59, 0xc3, 0xb5, 0xea, 0x9c, 0x06, 0x70,
+ 0xb8, 0xce, 0x54, 0x22, 0x7d, 0x0b, 0x91, 0xe7, 0x1c, 0x6a,
+ 0xf0, 0x86, 0xd9, 0xaf, 0x35, 0x43, 0x8b, 0xfd, 0x67, 0x11,
+ 0x4e, 0x38, 0xa2, 0xd4, 0x00, 0x77, 0xee, 0x99, 0xc1, 0xb6,
+ 0x2f, 0x58, 0x9f, 0xe8, 0x71, 0x06, 0x5e, 0x29, 0xb0, 0xc7,
+ 0x23, 0x54, 0xcd, 0xba, 0xe2, 0x95, 0x0c, 0x7b, 0xbc, 0xcb,
+ 0x52, 0x25, 0x7d, 0x0a, 0x93, 0xe4, 0x46, 0x31, 0xa8, 0xdf,
+ 0x87, 0xf0, 0x69, 0x1e, 0xd9, 0xae, 0x37, 0x40, 0x18, 0x6f,
+ 0xf6, 0x81, 0x65, 0x12, 0x8b, 0xfc, 0xa4, 0xd3, 0x4a, 0x3d,
+ 0xfa, 0x8d, 0x14, 0x63, 0x3b, 0x4c, 0xd5, 0xa2, 0x8c, 0xfb,
+ 0x62, 0x15, 0x4d, 0x3a, 0xa3, 0xd4, 0x13, 0x64, 0xfd, 0x8a,
+ 0xd2, 0xa5, 0x3c, 0x4b, 0xaf, 0xd8, 0x41, 0x36, 0x6e, 0x19,
+ 0x80, 0xf7, 0x30, 0x47, 0xde, 0xa9, 0xf1, 0x86, 0x1f, 0x68,
+ 0xca, 0xbd, 0x24, 0x53, 0x0b, 0x7c, 0xe5, 0x92, 0x55, 0x22,
+ 0xbb, 0xcc, 0x94, 0xe3, 0x7a, 0x0d, 0xe9, 0x9e, 0x07, 0x70,
+ 0x28, 0x5f, 0xc6, 0xb1, 0x76, 0x01, 0x98, 0xef, 0xb7, 0xc0,
+ 0x59, 0x2e, 0x05, 0x72, 0xeb, 0x9c, 0xc4, 0xb3, 0x2a, 0x5d,
+ 0x9a, 0xed, 0x74, 0x03, 0x5b, 0x2c, 0xb5, 0xc2, 0x26, 0x51,
+ 0xc8, 0xbf, 0xe7, 0x90, 0x09, 0x7e, 0xb9, 0xce, 0x57, 0x20,
+ 0x78, 0x0f, 0x96, 0xe1, 0x43, 0x34, 0xad, 0xda, 0x82, 0xf5,
+ 0x6c, 0x1b, 0xdc, 0xab, 0x32, 0x45, 0x1d, 0x6a, 0xf3, 0x84,
+ 0x60, 0x17, 0x8e, 0xf9, 0xa1, 0xd6, 0x4f, 0x38, 0xff, 0x88,
+ 0x11, 0x66, 0x3e, 0x49, 0xd0, 0xa7, 0x89, 0xfe, 0x67, 0x10,
+ 0x48, 0x3f, 0xa6, 0xd1, 0x16, 0x61, 0xf8, 0x8f, 0xd7, 0xa0,
+ 0x39, 0x4e, 0xaa, 0xdd, 0x44, 0x33, 0x6b, 0x1c, 0x85, 0xf2,
+ 0x35, 0x42, 0xdb, 0xac, 0xf4, 0x83, 0x1a, 0x6d, 0xcf, 0xb8,
+ 0x21, 0x56, 0x0e, 0x79, 0xe0, 0x97, 0x50, 0x27, 0xbe, 0xc9,
+ 0x91, 0xe6, 0x7f, 0x08, 0xec, 0x9b, 0x02, 0x75, 0x2d, 0x5a,
+ 0xc3, 0xb4, 0x73, 0x04, 0x9d, 0xea, 0xb2, 0xc5, 0x5c, 0x2b,
+ 0x00, 0x78, 0xf0, 0x88, 0xfd, 0x85, 0x0d, 0x75, 0xe7, 0x9f,
+ 0x17, 0x6f, 0x1a, 0x62, 0xea, 0x92, 0xd3, 0xab, 0x23, 0x5b,
+ 0x2e, 0x56, 0xde, 0xa6, 0x34, 0x4c, 0xc4, 0xbc, 0xc9, 0xb1,
+ 0x39, 0x41, 0xbb, 0xc3, 0x4b, 0x33, 0x46, 0x3e, 0xb6, 0xce,
+ 0x5c, 0x24, 0xac, 0xd4, 0xa1, 0xd9, 0x51, 0x29, 0x68, 0x10,
+ 0x98, 0xe0, 0x95, 0xed, 0x65, 0x1d, 0x8f, 0xf7, 0x7f, 0x07,
+ 0x72, 0x0a, 0x82, 0xfa, 0x6b, 0x13, 0x9b, 0xe3, 0x96, 0xee,
+ 0x66, 0x1e, 0x8c, 0xf4, 0x7c, 0x04, 0x71, 0x09, 0x81, 0xf9,
+ 0xb8, 0xc0, 0x48, 0x30, 0x45, 0x3d, 0xb5, 0xcd, 0x5f, 0x27,
+ 0xaf, 0xd7, 0xa2, 0xda, 0x52, 0x2a, 0xd0, 0xa8, 0x20, 0x58,
+ 0x2d, 0x55, 0xdd, 0xa5, 0x37, 0x4f, 0xc7, 0xbf, 0xca, 0xb2,
+ 0x3a, 0x42, 0x03, 0x7b, 0xf3, 0x8b, 0xfe, 0x86, 0x0e, 0x76,
+ 0xe4, 0x9c, 0x14, 0x6c, 0x19, 0x61, 0xe9, 0x91, 0xd6, 0xae,
+ 0x26, 0x5e, 0x2b, 0x53, 0xdb, 0xa3, 0x31, 0x49, 0xc1, 0xb9,
+ 0xcc, 0xb4, 0x3c, 0x44, 0x05, 0x7d, 0xf5, 0x8d, 0xf8, 0x80,
+ 0x08, 0x70, 0xe2, 0x9a, 0x12, 0x6a, 0x1f, 0x67, 0xef, 0x97,
+ 0x6d, 0x15, 0x9d, 0xe5, 0x90, 0xe8, 0x60, 0x18, 0x8a, 0xf2,
+ 0x7a, 0x02, 0x77, 0x0f, 0x87, 0xff, 0xbe, 0xc6, 0x4e, 0x36,
+ 0x43, 0x3b, 0xb3, 0xcb, 0x59, 0x21, 0xa9, 0xd1, 0xa4, 0xdc,
+ 0x54, 0x2c, 0xbd, 0xc5, 0x4d, 0x35, 0x40, 0x38, 0xb0, 0xc8,
+ 0x5a, 0x22, 0xaa, 0xd2, 0xa7, 0xdf, 0x57, 0x2f, 0x6e, 0x16,
+ 0x9e, 0xe6, 0x93, 0xeb, 0x63, 0x1b, 0x89, 0xf1, 0x79, 0x01,
+ 0x74, 0x0c, 0x84, 0xfc, 0x06, 0x7e, 0xf6, 0x8e, 0xfb, 0x83,
+ 0x0b, 0x73, 0xe1, 0x99, 0x11, 0x69, 0x1c, 0x64, 0xec, 0x94,
+ 0xd5, 0xad, 0x25, 0x5d, 0x28, 0x50, 0xd8, 0xa0, 0x32, 0x4a,
+ 0xc2, 0xba, 0xcf, 0xb7, 0x3f, 0x47, 0x00, 0x79, 0xf2, 0x8b,
+ 0xf9, 0x80, 0x0b, 0x72, 0xef, 0x96, 0x1d, 0x64, 0x16, 0x6f,
+ 0xe4, 0x9d, 0xc3, 0xba, 0x31, 0x48, 0x3a, 0x43, 0xc8, 0xb1,
+ 0x2c, 0x55, 0xde, 0xa7, 0xd5, 0xac, 0x27, 0x5e, 0x9b, 0xe2,
+ 0x69, 0x10, 0x62, 0x1b, 0x90, 0xe9, 0x74, 0x0d, 0x86, 0xff,
+ 0x8d, 0xf4, 0x7f, 0x06, 0x58, 0x21, 0xaa, 0xd3, 0xa1, 0xd8,
+ 0x53, 0x2a, 0xb7, 0xce, 0x45, 0x3c, 0x4e, 0x37, 0xbc, 0xc5,
+ 0x2b, 0x52, 0xd9, 0xa0, 0xd2, 0xab, 0x20, 0x59, 0xc4, 0xbd,
+ 0x36, 0x4f, 0x3d, 0x44, 0xcf, 0xb6, 0xe8, 0x91, 0x1a, 0x63,
+ 0x11, 0x68, 0xe3, 0x9a, 0x07, 0x7e, 0xf5, 0x8c, 0xfe, 0x87,
+ 0x0c, 0x75, 0xb0, 0xc9, 0x42, 0x3b, 0x49, 0x30, 0xbb, 0xc2,
+ 0x5f, 0x26, 0xad, 0xd4, 0xa6, 0xdf, 0x54, 0x2d, 0x73, 0x0a,
+ 0x81, 0xf8, 0x8a, 0xf3, 0x78, 0x01, 0x9c, 0xe5, 0x6e, 0x17,
+ 0x65, 0x1c, 0x97, 0xee, 0x56, 0x2f, 0xa4, 0xdd, 0xaf, 0xd6,
+ 0x5d, 0x24, 0xb9, 0xc0, 0x4b, 0x32, 0x40, 0x39, 0xb2, 0xcb,
+ 0x95, 0xec, 0x67, 0x1e, 0x6c, 0x15, 0x9e, 0xe7, 0x7a, 0x03,
+ 0x88, 0xf1, 0x83, 0xfa, 0x71, 0x08, 0xcd, 0xb4, 0x3f, 0x46,
+ 0x34, 0x4d, 0xc6, 0xbf, 0x22, 0x5b, 0xd0, 0xa9, 0xdb, 0xa2,
+ 0x29, 0x50, 0x0e, 0x77, 0xfc, 0x85, 0xf7, 0x8e, 0x05, 0x7c,
+ 0xe1, 0x98, 0x13, 0x6a, 0x18, 0x61, 0xea, 0x93, 0x7d, 0x04,
+ 0x8f, 0xf6, 0x84, 0xfd, 0x76, 0x0f, 0x92, 0xeb, 0x60, 0x19,
+ 0x6b, 0x12, 0x99, 0xe0, 0xbe, 0xc7, 0x4c, 0x35, 0x47, 0x3e,
+ 0xb5, 0xcc, 0x51, 0x28, 0xa3, 0xda, 0xa8, 0xd1, 0x5a, 0x23,
+ 0xe6, 0x9f, 0x14, 0x6d, 0x1f, 0x66, 0xed, 0x94, 0x09, 0x70,
+ 0xfb, 0x82, 0xf0, 0x89, 0x02, 0x7b, 0x25, 0x5c, 0xd7, 0xae,
+ 0xdc, 0xa5, 0x2e, 0x57, 0xca, 0xb3, 0x38, 0x41, 0x33, 0x4a,
+ 0xc1, 0xb8, 0x00, 0x7a, 0xf4, 0x8e, 0xf5, 0x8f, 0x01, 0x7b,
+ 0xf7, 0x8d, 0x03, 0x79, 0x02, 0x78, 0xf6, 0x8c, 0xf3, 0x89,
+ 0x07, 0x7d, 0x06, 0x7c, 0xf2, 0x88, 0x04, 0x7e, 0xf0, 0x8a,
+ 0xf1, 0x8b, 0x05, 0x7f, 0xfb, 0x81, 0x0f, 0x75, 0x0e, 0x74,
+ 0xfa, 0x80, 0x0c, 0x76, 0xf8, 0x82, 0xf9, 0x83, 0x0d, 0x77,
+ 0x08, 0x72, 0xfc, 0x86, 0xfd, 0x87, 0x09, 0x73, 0xff, 0x85,
+ 0x0b, 0x71, 0x0a, 0x70, 0xfe, 0x84, 0xeb, 0x91, 0x1f, 0x65,
+ 0x1e, 0x64, 0xea, 0x90, 0x1c, 0x66, 0xe8, 0x92, 0xe9, 0x93,
+ 0x1d, 0x67, 0x18, 0x62, 0xec, 0x96, 0xed, 0x97, 0x19, 0x63,
+ 0xef, 0x95, 0x1b, 0x61, 0x1a, 0x60, 0xee, 0x94, 0x10, 0x6a,
+ 0xe4, 0x9e, 0xe5, 0x9f, 0x11, 0x6b, 0xe7, 0x9d, 0x13, 0x69,
+ 0x12, 0x68, 0xe6, 0x9c, 0xe3, 0x99, 0x17, 0x6d, 0x16, 0x6c,
+ 0xe2, 0x98, 0x14, 0x6e, 0xe0, 0x9a, 0xe1, 0x9b, 0x15, 0x6f,
+ 0xcb, 0xb1, 0x3f, 0x45, 0x3e, 0x44, 0xca, 0xb0, 0x3c, 0x46,
+ 0xc8, 0xb2, 0xc9, 0xb3, 0x3d, 0x47, 0x38, 0x42, 0xcc, 0xb6,
+ 0xcd, 0xb7, 0x39, 0x43, 0xcf, 0xb5, 0x3b, 0x41, 0x3a, 0x40,
+ 0xce, 0xb4, 0x30, 0x4a, 0xc4, 0xbe, 0xc5, 0xbf, 0x31, 0x4b,
+ 0xc7, 0xbd, 0x33, 0x49, 0x32, 0x48, 0xc6, 0xbc, 0xc3, 0xb9,
+ 0x37, 0x4d, 0x36, 0x4c, 0xc2, 0xb8, 0x34, 0x4e, 0xc0, 0xba,
+ 0xc1, 0xbb, 0x35, 0x4f, 0x20, 0x5a, 0xd4, 0xae, 0xd5, 0xaf,
+ 0x21, 0x5b, 0xd7, 0xad, 0x23, 0x59, 0x22, 0x58, 0xd6, 0xac,
+ 0xd3, 0xa9, 0x27, 0x5d, 0x26, 0x5c, 0xd2, 0xa8, 0x24, 0x5e,
+ 0xd0, 0xaa, 0xd1, 0xab, 0x25, 0x5f, 0xdb, 0xa1, 0x2f, 0x55,
+ 0x2e, 0x54, 0xda, 0xa0, 0x2c, 0x56, 0xd8, 0xa2, 0xd9, 0xa3,
+ 0x2d, 0x57, 0x28, 0x52, 0xdc, 0xa6, 0xdd, 0xa7, 0x29, 0x53,
+ 0xdf, 0xa5, 0x2b, 0x51, 0x2a, 0x50, 0xde, 0xa4, 0x00, 0x7b,
+ 0xf6, 0x8d, 0xf1, 0x8a, 0x07, 0x7c, 0xff, 0x84, 0x09, 0x72,
+ 0x0e, 0x75, 0xf8, 0x83, 0xe3, 0x98, 0x15, 0x6e, 0x12, 0x69,
+ 0xe4, 0x9f, 0x1c, 0x67, 0xea, 0x91, 0xed, 0x96, 0x1b, 0x60,
+ 0xdb, 0xa0, 0x2d, 0x56, 0x2a, 0x51, 0xdc, 0xa7, 0x24, 0x5f,
+ 0xd2, 0xa9, 0xd5, 0xae, 0x23, 0x58, 0x38, 0x43, 0xce, 0xb5,
+ 0xc9, 0xb2, 0x3f, 0x44, 0xc7, 0xbc, 0x31, 0x4a, 0x36, 0x4d,
+ 0xc0, 0xbb, 0xab, 0xd0, 0x5d, 0x26, 0x5a, 0x21, 0xac, 0xd7,
+ 0x54, 0x2f, 0xa2, 0xd9, 0xa5, 0xde, 0x53, 0x28, 0x48, 0x33,
+ 0xbe, 0xc5, 0xb9, 0xc2, 0x4f, 0x34, 0xb7, 0xcc, 0x41, 0x3a,
+ 0x46, 0x3d, 0xb0, 0xcb, 0x70, 0x0b, 0x86, 0xfd, 0x81, 0xfa,
+ 0x77, 0x0c, 0x8f, 0xf4, 0x79, 0x02, 0x7e, 0x05, 0x88, 0xf3,
+ 0x93, 0xe8, 0x65, 0x1e, 0x62, 0x19, 0x94, 0xef, 0x6c, 0x17,
+ 0x9a, 0xe1, 0x9d, 0xe6, 0x6b, 0x10, 0x4b, 0x30, 0xbd, 0xc6,
+ 0xba, 0xc1, 0x4c, 0x37, 0xb4, 0xcf, 0x42, 0x39, 0x45, 0x3e,
+ 0xb3, 0xc8, 0xa8, 0xd3, 0x5e, 0x25, 0x59, 0x22, 0xaf, 0xd4,
+ 0x57, 0x2c, 0xa1, 0xda, 0xa6, 0xdd, 0x50, 0x2b, 0x90, 0xeb,
+ 0x66, 0x1d, 0x61, 0x1a, 0x97, 0xec, 0x6f, 0x14, 0x99, 0xe2,
+ 0x9e, 0xe5, 0x68, 0x13, 0x73, 0x08, 0x85, 0xfe, 0x82, 0xf9,
+ 0x74, 0x0f, 0x8c, 0xf7, 0x7a, 0x01, 0x7d, 0x06, 0x8b, 0xf0,
+ 0xe0, 0x9b, 0x16, 0x6d, 0x11, 0x6a, 0xe7, 0x9c, 0x1f, 0x64,
+ 0xe9, 0x92, 0xee, 0x95, 0x18, 0x63, 0x03, 0x78, 0xf5, 0x8e,
+ 0xf2, 0x89, 0x04, 0x7f, 0xfc, 0x87, 0x0a, 0x71, 0x0d, 0x76,
+ 0xfb, 0x80, 0x3b, 0x40, 0xcd, 0xb6, 0xca, 0xb1, 0x3c, 0x47,
+ 0xc4, 0xbf, 0x32, 0x49, 0x35, 0x4e, 0xc3, 0xb8, 0xd8, 0xa3,
+ 0x2e, 0x55, 0x29, 0x52, 0xdf, 0xa4, 0x27, 0x5c, 0xd1, 0xaa,
+ 0xd6, 0xad, 0x20, 0x5b, 0x00, 0x7c, 0xf8, 0x84, 0xed, 0x91,
+ 0x15, 0x69, 0xc7, 0xbb, 0x3f, 0x43, 0x2a, 0x56, 0xd2, 0xae,
+ 0x93, 0xef, 0x6b, 0x17, 0x7e, 0x02, 0x86, 0xfa, 0x54, 0x28,
+ 0xac, 0xd0, 0xb9, 0xc5, 0x41, 0x3d, 0x3b, 0x47, 0xc3, 0xbf,
+ 0xd6, 0xaa, 0x2e, 0x52, 0xfc, 0x80, 0x04, 0x78, 0x11, 0x6d,
+ 0xe9, 0x95, 0xa8, 0xd4, 0x50, 0x2c, 0x45, 0x39, 0xbd, 0xc1,
+ 0x6f, 0x13, 0x97, 0xeb, 0x82, 0xfe, 0x7a, 0x06, 0x76, 0x0a,
+ 0x8e, 0xf2, 0x9b, 0xe7, 0x63, 0x1f, 0xb1, 0xcd, 0x49, 0x35,
+ 0x5c, 0x20, 0xa4, 0xd8, 0xe5, 0x99, 0x1d, 0x61, 0x08, 0x74,
+ 0xf0, 0x8c, 0x22, 0x5e, 0xda, 0xa6, 0xcf, 0xb3, 0x37, 0x4b,
+ 0x4d, 0x31, 0xb5, 0xc9, 0xa0, 0xdc, 0x58, 0x24, 0x8a, 0xf6,
+ 0x72, 0x0e, 0x67, 0x1b, 0x9f, 0xe3, 0xde, 0xa2, 0x26, 0x5a,
+ 0x33, 0x4f, 0xcb, 0xb7, 0x19, 0x65, 0xe1, 0x9d, 0xf4, 0x88,
+ 0x0c, 0x70, 0xec, 0x90, 0x14, 0x68, 0x01, 0x7d, 0xf9, 0x85,
+ 0x2b, 0x57, 0xd3, 0xaf, 0xc6, 0xba, 0x3e, 0x42, 0x7f, 0x03,
+ 0x87, 0xfb, 0x92, 0xee, 0x6a, 0x16, 0xb8, 0xc4, 0x40, 0x3c,
+ 0x55, 0x29, 0xad, 0xd1, 0xd7, 0xab, 0x2f, 0x53, 0x3a, 0x46,
+ 0xc2, 0xbe, 0x10, 0x6c, 0xe8, 0x94, 0xfd, 0x81, 0x05, 0x79,
+ 0x44, 0x38, 0xbc, 0xc0, 0xa9, 0xd5, 0x51, 0x2d, 0x83, 0xff,
+ 0x7b, 0x07, 0x6e, 0x12, 0x96, 0xea, 0x9a, 0xe6, 0x62, 0x1e,
+ 0x77, 0x0b, 0x8f, 0xf3, 0x5d, 0x21, 0xa5, 0xd9, 0xb0, 0xcc,
+ 0x48, 0x34, 0x09, 0x75, 0xf1, 0x8d, 0xe4, 0x98, 0x1c, 0x60,
+ 0xce, 0xb2, 0x36, 0x4a, 0x23, 0x5f, 0xdb, 0xa7, 0xa1, 0xdd,
+ 0x59, 0x25, 0x4c, 0x30, 0xb4, 0xc8, 0x66, 0x1a, 0x9e, 0xe2,
+ 0x8b, 0xf7, 0x73, 0x0f, 0x32, 0x4e, 0xca, 0xb6, 0xdf, 0xa3,
+ 0x27, 0x5b, 0xf5, 0x89, 0x0d, 0x71, 0x18, 0x64, 0xe0, 0x9c,
+ 0x00, 0x7d, 0xfa, 0x87, 0xe9, 0x94, 0x13, 0x6e, 0xcf, 0xb2,
+ 0x35, 0x48, 0x26, 0x5b, 0xdc, 0xa1, 0x83, 0xfe, 0x79, 0x04,
+ 0x6a, 0x17, 0x90, 0xed, 0x4c, 0x31, 0xb6, 0xcb, 0xa5, 0xd8,
+ 0x5f, 0x22, 0x1b, 0x66, 0xe1, 0x9c, 0xf2, 0x8f, 0x08, 0x75,
+ 0xd4, 0xa9, 0x2e, 0x53, 0x3d, 0x40, 0xc7, 0xba, 0x98, 0xe5,
+ 0x62, 0x1f, 0x71, 0x0c, 0x8b, 0xf6, 0x57, 0x2a, 0xad, 0xd0,
+ 0xbe, 0xc3, 0x44, 0x39, 0x36, 0x4b, 0xcc, 0xb1, 0xdf, 0xa2,
+ 0x25, 0x58, 0xf9, 0x84, 0x03, 0x7e, 0x10, 0x6d, 0xea, 0x97,
+ 0xb5, 0xc8, 0x4f, 0x32, 0x5c, 0x21, 0xa6, 0xdb, 0x7a, 0x07,
+ 0x80, 0xfd, 0x93, 0xee, 0x69, 0x14, 0x2d, 0x50, 0xd7, 0xaa,
+ 0xc4, 0xb9, 0x3e, 0x43, 0xe2, 0x9f, 0x18, 0x65, 0x0b, 0x76,
+ 0xf1, 0x8c, 0xae, 0xd3, 0x54, 0x29, 0x47, 0x3a, 0xbd, 0xc0,
+ 0x61, 0x1c, 0x9b, 0xe6, 0x88, 0xf5, 0x72, 0x0f, 0x6c, 0x11,
+ 0x96, 0xeb, 0x85, 0xf8, 0x7f, 0x02, 0xa3, 0xde, 0x59, 0x24,
+ 0x4a, 0x37, 0xb0, 0xcd, 0xef, 0x92, 0x15, 0x68, 0x06, 0x7b,
+ 0xfc, 0x81, 0x20, 0x5d, 0xda, 0xa7, 0xc9, 0xb4, 0x33, 0x4e,
+ 0x77, 0x0a, 0x8d, 0xf0, 0x9e, 0xe3, 0x64, 0x19, 0xb8, 0xc5,
+ 0x42, 0x3f, 0x51, 0x2c, 0xab, 0xd6, 0xf4, 0x89, 0x0e, 0x73,
+ 0x1d, 0x60, 0xe7, 0x9a, 0x3b, 0x46, 0xc1, 0xbc, 0xd2, 0xaf,
+ 0x28, 0x55, 0x5a, 0x27, 0xa0, 0xdd, 0xb3, 0xce, 0x49, 0x34,
+ 0x95, 0xe8, 0x6f, 0x12, 0x7c, 0x01, 0x86, 0xfb, 0xd9, 0xa4,
+ 0x23, 0x5e, 0x30, 0x4d, 0xca, 0xb7, 0x16, 0x6b, 0xec, 0x91,
+ 0xff, 0x82, 0x05, 0x78, 0x41, 0x3c, 0xbb, 0xc6, 0xa8, 0xd5,
+ 0x52, 0x2f, 0x8e, 0xf3, 0x74, 0x09, 0x67, 0x1a, 0x9d, 0xe0,
+ 0xc2, 0xbf, 0x38, 0x45, 0x2b, 0x56, 0xd1, 0xac, 0x0d, 0x70,
+ 0xf7, 0x8a, 0xe4, 0x99, 0x1e, 0x63, 0x00, 0x7e, 0xfc, 0x82,
+ 0xe5, 0x9b, 0x19, 0x67, 0xd7, 0xa9, 0x2b, 0x55, 0x32, 0x4c,
+ 0xce, 0xb0, 0xb3, 0xcd, 0x4f, 0x31, 0x56, 0x28, 0xaa, 0xd4,
+ 0x64, 0x1a, 0x98, 0xe6, 0x81, 0xff, 0x7d, 0x03, 0x7b, 0x05,
+ 0x87, 0xf9, 0x9e, 0xe0, 0x62, 0x1c, 0xac, 0xd2, 0x50, 0x2e,
+ 0x49, 0x37, 0xb5, 0xcb, 0xc8, 0xb6, 0x34, 0x4a, 0x2d, 0x53,
+ 0xd1, 0xaf, 0x1f, 0x61, 0xe3, 0x9d, 0xfa, 0x84, 0x06, 0x78,
+ 0xf6, 0x88, 0x0a, 0x74, 0x13, 0x6d, 0xef, 0x91, 0x21, 0x5f,
+ 0xdd, 0xa3, 0xc4, 0xba, 0x38, 0x46, 0x45, 0x3b, 0xb9, 0xc7,
+ 0xa0, 0xde, 0x5c, 0x22, 0x92, 0xec, 0x6e, 0x10, 0x77, 0x09,
+ 0x8b, 0xf5, 0x8d, 0xf3, 0x71, 0x0f, 0x68, 0x16, 0x94, 0xea,
+ 0x5a, 0x24, 0xa6, 0xd8, 0xbf, 0xc1, 0x43, 0x3d, 0x3e, 0x40,
+ 0xc2, 0xbc, 0xdb, 0xa5, 0x27, 0x59, 0xe9, 0x97, 0x15, 0x6b,
+ 0x0c, 0x72, 0xf0, 0x8e, 0xf1, 0x8f, 0x0d, 0x73, 0x14, 0x6a,
+ 0xe8, 0x96, 0x26, 0x58, 0xda, 0xa4, 0xc3, 0xbd, 0x3f, 0x41,
+ 0x42, 0x3c, 0xbe, 0xc0, 0xa7, 0xd9, 0x5b, 0x25, 0x95, 0xeb,
+ 0x69, 0x17, 0x70, 0x0e, 0x8c, 0xf2, 0x8a, 0xf4, 0x76, 0x08,
+ 0x6f, 0x11, 0x93, 0xed, 0x5d, 0x23, 0xa1, 0xdf, 0xb8, 0xc6,
+ 0x44, 0x3a, 0x39, 0x47, 0xc5, 0xbb, 0xdc, 0xa2, 0x20, 0x5e,
+ 0xee, 0x90, 0x12, 0x6c, 0x0b, 0x75, 0xf7, 0x89, 0x07, 0x79,
+ 0xfb, 0x85, 0xe2, 0x9c, 0x1e, 0x60, 0xd0, 0xae, 0x2c, 0x52,
+ 0x35, 0x4b, 0xc9, 0xb7, 0xb4, 0xca, 0x48, 0x36, 0x51, 0x2f,
+ 0xad, 0xd3, 0x63, 0x1d, 0x9f, 0xe1, 0x86, 0xf8, 0x7a, 0x04,
+ 0x7c, 0x02, 0x80, 0xfe, 0x99, 0xe7, 0x65, 0x1b, 0xab, 0xd5,
+ 0x57, 0x29, 0x4e, 0x30, 0xb2, 0xcc, 0xcf, 0xb1, 0x33, 0x4d,
+ 0x2a, 0x54, 0xd6, 0xa8, 0x18, 0x66, 0xe4, 0x9a, 0xfd, 0x83,
+ 0x01, 0x7f, 0x00, 0x7f, 0xfe, 0x81, 0xe1, 0x9e, 0x1f, 0x60,
+ 0xdf, 0xa0, 0x21, 0x5e, 0x3e, 0x41, 0xc0, 0xbf, 0xa3, 0xdc,
+ 0x5d, 0x22, 0x42, 0x3d, 0xbc, 0xc3, 0x7c, 0x03, 0x82, 0xfd,
+ 0x9d, 0xe2, 0x63, 0x1c, 0x5b, 0x24, 0xa5, 0xda, 0xba, 0xc5,
+ 0x44, 0x3b, 0x84, 0xfb, 0x7a, 0x05, 0x65, 0x1a, 0x9b, 0xe4,
+ 0xf8, 0x87, 0x06, 0x79, 0x19, 0x66, 0xe7, 0x98, 0x27, 0x58,
+ 0xd9, 0xa6, 0xc6, 0xb9, 0x38, 0x47, 0xb6, 0xc9, 0x48, 0x37,
+ 0x57, 0x28, 0xa9, 0xd6, 0x69, 0x16, 0x97, 0xe8, 0x88, 0xf7,
+ 0x76, 0x09, 0x15, 0x6a, 0xeb, 0x94, 0xf4, 0x8b, 0x0a, 0x75,
+ 0xca, 0xb5, 0x34, 0x4b, 0x2b, 0x54, 0xd5, 0xaa, 0xed, 0x92,
+ 0x13, 0x6c, 0x0c, 0x73, 0xf2, 0x8d, 0x32, 0x4d, 0xcc, 0xb3,
+ 0xd3, 0xac, 0x2d, 0x52, 0x4e, 0x31, 0xb0, 0xcf, 0xaf, 0xd0,
+ 0x51, 0x2e, 0x91, 0xee, 0x6f, 0x10, 0x70, 0x0f, 0x8e, 0xf1,
+ 0x71, 0x0e, 0x8f, 0xf0, 0x90, 0xef, 0x6e, 0x11, 0xae, 0xd1,
+ 0x50, 0x2f, 0x4f, 0x30, 0xb1, 0xce, 0xd2, 0xad, 0x2c, 0x53,
+ 0x33, 0x4c, 0xcd, 0xb2, 0x0d, 0x72, 0xf3, 0x8c, 0xec, 0x93,
+ 0x12, 0x6d, 0x2a, 0x55, 0xd4, 0xab, 0xcb, 0xb4, 0x35, 0x4a,
+ 0xf5, 0x8a, 0x0b, 0x74, 0x14, 0x6b, 0xea, 0x95, 0x89, 0xf6,
+ 0x77, 0x08, 0x68, 0x17, 0x96, 0xe9, 0x56, 0x29, 0xa8, 0xd7,
+ 0xb7, 0xc8, 0x49, 0x36, 0xc7, 0xb8, 0x39, 0x46, 0x26, 0x59,
+ 0xd8, 0xa7, 0x18, 0x67, 0xe6, 0x99, 0xf9, 0x86, 0x07, 0x78,
+ 0x64, 0x1b, 0x9a, 0xe5, 0x85, 0xfa, 0x7b, 0x04, 0xbb, 0xc4,
+ 0x45, 0x3a, 0x5a, 0x25, 0xa4, 0xdb, 0x9c, 0xe3, 0x62, 0x1d,
+ 0x7d, 0x02, 0x83, 0xfc, 0x43, 0x3c, 0xbd, 0xc2, 0xa2, 0xdd,
+ 0x5c, 0x23, 0x3f, 0x40, 0xc1, 0xbe, 0xde, 0xa1, 0x20, 0x5f,
+ 0xe0, 0x9f, 0x1e, 0x61, 0x01, 0x7e, 0xff, 0x80, 0x00, 0x80,
+ 0x1d, 0x9d, 0x3a, 0xba, 0x27, 0xa7, 0x74, 0xf4, 0x69, 0xe9,
+ 0x4e, 0xce, 0x53, 0xd3, 0xe8, 0x68, 0xf5, 0x75, 0xd2, 0x52,
+ 0xcf, 0x4f, 0x9c, 0x1c, 0x81, 0x01, 0xa6, 0x26, 0xbb, 0x3b,
+ 0xcd, 0x4d, 0xd0, 0x50, 0xf7, 0x77, 0xea, 0x6a, 0xb9, 0x39,
+ 0xa4, 0x24, 0x83, 0x03, 0x9e, 0x1e, 0x25, 0xa5, 0x38, 0xb8,
+ 0x1f, 0x9f, 0x02, 0x82, 0x51, 0xd1, 0x4c, 0xcc, 0x6b, 0xeb,
+ 0x76, 0xf6, 0x87, 0x07, 0x9a, 0x1a, 0xbd, 0x3d, 0xa0, 0x20,
+ 0xf3, 0x73, 0xee, 0x6e, 0xc9, 0x49, 0xd4, 0x54, 0x6f, 0xef,
+ 0x72, 0xf2, 0x55, 0xd5, 0x48, 0xc8, 0x1b, 0x9b, 0x06, 0x86,
+ 0x21, 0xa1, 0x3c, 0xbc, 0x4a, 0xca, 0x57, 0xd7, 0x70, 0xf0,
+ 0x6d, 0xed, 0x3e, 0xbe, 0x23, 0xa3, 0x04, 0x84, 0x19, 0x99,
+ 0xa2, 0x22, 0xbf, 0x3f, 0x98, 0x18, 0x85, 0x05, 0xd6, 0x56,
+ 0xcb, 0x4b, 0xec, 0x6c, 0xf1, 0x71, 0x13, 0x93, 0x0e, 0x8e,
+ 0x29, 0xa9, 0x34, 0xb4, 0x67, 0xe7, 0x7a, 0xfa, 0x5d, 0xdd,
+ 0x40, 0xc0, 0xfb, 0x7b, 0xe6, 0x66, 0xc1, 0x41, 0xdc, 0x5c,
+ 0x8f, 0x0f, 0x92, 0x12, 0xb5, 0x35, 0xa8, 0x28, 0xde, 0x5e,
+ 0xc3, 0x43, 0xe4, 0x64, 0xf9, 0x79, 0xaa, 0x2a, 0xb7, 0x37,
+ 0x90, 0x10, 0x8d, 0x0d, 0x36, 0xb6, 0x2b, 0xab, 0x0c, 0x8c,
+ 0x11, 0x91, 0x42, 0xc2, 0x5f, 0xdf, 0x78, 0xf8, 0x65, 0xe5,
+ 0x94, 0x14, 0x89, 0x09, 0xae, 0x2e, 0xb3, 0x33, 0xe0, 0x60,
+ 0xfd, 0x7d, 0xda, 0x5a, 0xc7, 0x47, 0x7c, 0xfc, 0x61, 0xe1,
+ 0x46, 0xc6, 0x5b, 0xdb, 0x08, 0x88, 0x15, 0x95, 0x32, 0xb2,
+ 0x2f, 0xaf, 0x59, 0xd9, 0x44, 0xc4, 0x63, 0xe3, 0x7e, 0xfe,
+ 0x2d, 0xad, 0x30, 0xb0, 0x17, 0x97, 0x0a, 0x8a, 0xb1, 0x31,
+ 0xac, 0x2c, 0x8b, 0x0b, 0x96, 0x16, 0xc5, 0x45, 0xd8, 0x58,
+ 0xff, 0x7f, 0xe2, 0x62, 0x00, 0x81, 0x1f, 0x9e, 0x3e, 0xbf,
+ 0x21, 0xa0, 0x7c, 0xfd, 0x63, 0xe2, 0x42, 0xc3, 0x5d, 0xdc,
+ 0xf8, 0x79, 0xe7, 0x66, 0xc6, 0x47, 0xd9, 0x58, 0x84, 0x05,
+ 0x9b, 0x1a, 0xba, 0x3b, 0xa5, 0x24, 0xed, 0x6c, 0xf2, 0x73,
+ 0xd3, 0x52, 0xcc, 0x4d, 0x91, 0x10, 0x8e, 0x0f, 0xaf, 0x2e,
+ 0xb0, 0x31, 0x15, 0x94, 0x0a, 0x8b, 0x2b, 0xaa, 0x34, 0xb5,
+ 0x69, 0xe8, 0x76, 0xf7, 0x57, 0xd6, 0x48, 0xc9, 0xc7, 0x46,
+ 0xd8, 0x59, 0xf9, 0x78, 0xe6, 0x67, 0xbb, 0x3a, 0xa4, 0x25,
+ 0x85, 0x04, 0x9a, 0x1b, 0x3f, 0xbe, 0x20, 0xa1, 0x01, 0x80,
+ 0x1e, 0x9f, 0x43, 0xc2, 0x5c, 0xdd, 0x7d, 0xfc, 0x62, 0xe3,
+ 0x2a, 0xab, 0x35, 0xb4, 0x14, 0x95, 0x0b, 0x8a, 0x56, 0xd7,
+ 0x49, 0xc8, 0x68, 0xe9, 0x77, 0xf6, 0xd2, 0x53, 0xcd, 0x4c,
+ 0xec, 0x6d, 0xf3, 0x72, 0xae, 0x2f, 0xb1, 0x30, 0x90, 0x11,
+ 0x8f, 0x0e, 0x93, 0x12, 0x8c, 0x0d, 0xad, 0x2c, 0xb2, 0x33,
+ 0xef, 0x6e, 0xf0, 0x71, 0xd1, 0x50, 0xce, 0x4f, 0x6b, 0xea,
+ 0x74, 0xf5, 0x55, 0xd4, 0x4a, 0xcb, 0x17, 0x96, 0x08, 0x89,
+ 0x29, 0xa8, 0x36, 0xb7, 0x7e, 0xff, 0x61, 0xe0, 0x40, 0xc1,
+ 0x5f, 0xde, 0x02, 0x83, 0x1d, 0x9c, 0x3c, 0xbd, 0x23, 0xa2,
+ 0x86, 0x07, 0x99, 0x18, 0xb8, 0x39, 0xa7, 0x26, 0xfa, 0x7b,
+ 0xe5, 0x64, 0xc4, 0x45, 0xdb, 0x5a, 0x54, 0xd5, 0x4b, 0xca,
+ 0x6a, 0xeb, 0x75, 0xf4, 0x28, 0xa9, 0x37, 0xb6, 0x16, 0x97,
+ 0x09, 0x88, 0xac, 0x2d, 0xb3, 0x32, 0x92, 0x13, 0x8d, 0x0c,
+ 0xd0, 0x51, 0xcf, 0x4e, 0xee, 0x6f, 0xf1, 0x70, 0xb9, 0x38,
+ 0xa6, 0x27, 0x87, 0x06, 0x98, 0x19, 0xc5, 0x44, 0xda, 0x5b,
+ 0xfb, 0x7a, 0xe4, 0x65, 0x41, 0xc0, 0x5e, 0xdf, 0x7f, 0xfe,
+ 0x60, 0xe1, 0x3d, 0xbc, 0x22, 0xa3, 0x03, 0x82, 0x1c, 0x9d,
+ 0x00, 0x82, 0x19, 0x9b, 0x32, 0xb0, 0x2b, 0xa9, 0x64, 0xe6,
+ 0x7d, 0xff, 0x56, 0xd4, 0x4f, 0xcd, 0xc8, 0x4a, 0xd1, 0x53,
+ 0xfa, 0x78, 0xe3, 0x61, 0xac, 0x2e, 0xb5, 0x37, 0x9e, 0x1c,
+ 0x87, 0x05, 0x8d, 0x0f, 0x94, 0x16, 0xbf, 0x3d, 0xa6, 0x24,
+ 0xe9, 0x6b, 0xf0, 0x72, 0xdb, 0x59, 0xc2, 0x40, 0x45, 0xc7,
+ 0x5c, 0xde, 0x77, 0xf5, 0x6e, 0xec, 0x21, 0xa3, 0x38, 0xba,
+ 0x13, 0x91, 0x0a, 0x88, 0x07, 0x85, 0x1e, 0x9c, 0x35, 0xb7,
+ 0x2c, 0xae, 0x63, 0xe1, 0x7a, 0xf8, 0x51, 0xd3, 0x48, 0xca,
+ 0xcf, 0x4d, 0xd6, 0x54, 0xfd, 0x7f, 0xe4, 0x66, 0xab, 0x29,
+ 0xb2, 0x30, 0x99, 0x1b, 0x80, 0x02, 0x8a, 0x08, 0x93, 0x11,
+ 0xb8, 0x3a, 0xa1, 0x23, 0xee, 0x6c, 0xf7, 0x75, 0xdc, 0x5e,
+ 0xc5, 0x47, 0x42, 0xc0, 0x5b, 0xd9, 0x70, 0xf2, 0x69, 0xeb,
+ 0x26, 0xa4, 0x3f, 0xbd, 0x14, 0x96, 0x0d, 0x8f, 0x0e, 0x8c,
+ 0x17, 0x95, 0x3c, 0xbe, 0x25, 0xa7, 0x6a, 0xe8, 0x73, 0xf1,
+ 0x58, 0xda, 0x41, 0xc3, 0xc6, 0x44, 0xdf, 0x5d, 0xf4, 0x76,
+ 0xed, 0x6f, 0xa2, 0x20, 0xbb, 0x39, 0x90, 0x12, 0x89, 0x0b,
+ 0x83, 0x01, 0x9a, 0x18, 0xb1, 0x33, 0xa8, 0x2a, 0xe7, 0x65,
+ 0xfe, 0x7c, 0xd5, 0x57, 0xcc, 0x4e, 0x4b, 0xc9, 0x52, 0xd0,
+ 0x79, 0xfb, 0x60, 0xe2, 0x2f, 0xad, 0x36, 0xb4, 0x1d, 0x9f,
+ 0x04, 0x86, 0x09, 0x8b, 0x10, 0x92, 0x3b, 0xb9, 0x22, 0xa0,
+ 0x6d, 0xef, 0x74, 0xf6, 0x5f, 0xdd, 0x46, 0xc4, 0xc1, 0x43,
+ 0xd8, 0x5a, 0xf3, 0x71, 0xea, 0x68, 0xa5, 0x27, 0xbc, 0x3e,
+ 0x97, 0x15, 0x8e, 0x0c, 0x84, 0x06, 0x9d, 0x1f, 0xb6, 0x34,
+ 0xaf, 0x2d, 0xe0, 0x62, 0xf9, 0x7b, 0xd2, 0x50, 0xcb, 0x49,
+ 0x4c, 0xce, 0x55, 0xd7, 0x7e, 0xfc, 0x67, 0xe5, 0x28, 0xaa,
+ 0x31, 0xb3, 0x1a, 0x98, 0x03, 0x81, 0x00, 0x83, 0x1b, 0x98,
+ 0x36, 0xb5, 0x2d, 0xae, 0x6c, 0xef, 0x77, 0xf4, 0x5a, 0xd9,
+ 0x41, 0xc2, 0xd8, 0x5b, 0xc3, 0x40, 0xee, 0x6d, 0xf5, 0x76,
+ 0xb4, 0x37, 0xaf, 0x2c, 0x82, 0x01, 0x99, 0x1a, 0xad, 0x2e,
+ 0xb6, 0x35, 0x9b, 0x18, 0x80, 0x03, 0xc1, 0x42, 0xda, 0x59,
+ 0xf7, 0x74, 0xec, 0x6f, 0x75, 0xf6, 0x6e, 0xed, 0x43, 0xc0,
+ 0x58, 0xdb, 0x19, 0x9a, 0x02, 0x81, 0x2f, 0xac, 0x34, 0xb7,
+ 0x47, 0xc4, 0x5c, 0xdf, 0x71, 0xf2, 0x6a, 0xe9, 0x2b, 0xa8,
+ 0x30, 0xb3, 0x1d, 0x9e, 0x06, 0x85, 0x9f, 0x1c, 0x84, 0x07,
+ 0xa9, 0x2a, 0xb2, 0x31, 0xf3, 0x70, 0xe8, 0x6b, 0xc5, 0x46,
+ 0xde, 0x5d, 0xea, 0x69, 0xf1, 0x72, 0xdc, 0x5f, 0xc7, 0x44,
+ 0x86, 0x05, 0x9d, 0x1e, 0xb0, 0x33, 0xab, 0x28, 0x32, 0xb1,
+ 0x29, 0xaa, 0x04, 0x87, 0x1f, 0x9c, 0x5e, 0xdd, 0x45, 0xc6,
+ 0x68, 0xeb, 0x73, 0xf0, 0x8e, 0x0d, 0x95, 0x16, 0xb8, 0x3b,
+ 0xa3, 0x20, 0xe2, 0x61, 0xf9, 0x7a, 0xd4, 0x57, 0xcf, 0x4c,
+ 0x56, 0xd5, 0x4d, 0xce, 0x60, 0xe3, 0x7b, 0xf8, 0x3a, 0xb9,
+ 0x21, 0xa2, 0x0c, 0x8f, 0x17, 0x94, 0x23, 0xa0, 0x38, 0xbb,
+ 0x15, 0x96, 0x0e, 0x8d, 0x4f, 0xcc, 0x54, 0xd7, 0x79, 0xfa,
+ 0x62, 0xe1, 0xfb, 0x78, 0xe0, 0x63, 0xcd, 0x4e, 0xd6, 0x55,
+ 0x97, 0x14, 0x8c, 0x0f, 0xa1, 0x22, 0xba, 0x39, 0xc9, 0x4a,
+ 0xd2, 0x51, 0xff, 0x7c, 0xe4, 0x67, 0xa5, 0x26, 0xbe, 0x3d,
+ 0x93, 0x10, 0x88, 0x0b, 0x11, 0x92, 0x0a, 0x89, 0x27, 0xa4,
+ 0x3c, 0xbf, 0x7d, 0xfe, 0x66, 0xe5, 0x4b, 0xc8, 0x50, 0xd3,
+ 0x64, 0xe7, 0x7f, 0xfc, 0x52, 0xd1, 0x49, 0xca, 0x08, 0x8b,
+ 0x13, 0x90, 0x3e, 0xbd, 0x25, 0xa6, 0xbc, 0x3f, 0xa7, 0x24,
+ 0x8a, 0x09, 0x91, 0x12, 0xd0, 0x53, 0xcb, 0x48, 0xe6, 0x65,
+ 0xfd, 0x7e, 0x00, 0x84, 0x15, 0x91, 0x2a, 0xae, 0x3f, 0xbb,
+ 0x54, 0xd0, 0x41, 0xc5, 0x7e, 0xfa, 0x6b, 0xef, 0xa8, 0x2c,
+ 0xbd, 0x39, 0x82, 0x06, 0x97, 0x13, 0xfc, 0x78, 0xe9, 0x6d,
+ 0xd6, 0x52, 0xc3, 0x47, 0x4d, 0xc9, 0x58, 0xdc, 0x67, 0xe3,
+ 0x72, 0xf6, 0x19, 0x9d, 0x0c, 0x88, 0x33, 0xb7, 0x26, 0xa2,
+ 0xe5, 0x61, 0xf0, 0x74, 0xcf, 0x4b, 0xda, 0x5e, 0xb1, 0x35,
+ 0xa4, 0x20, 0x9b, 0x1f, 0x8e, 0x0a, 0x9a, 0x1e, 0x8f, 0x0b,
+ 0xb0, 0x34, 0xa5, 0x21, 0xce, 0x4a, 0xdb, 0x5f, 0xe4, 0x60,
+ 0xf1, 0x75, 0x32, 0xb6, 0x27, 0xa3, 0x18, 0x9c, 0x0d, 0x89,
+ 0x66, 0xe2, 0x73, 0xf7, 0x4c, 0xc8, 0x59, 0xdd, 0xd7, 0x53,
+ 0xc2, 0x46, 0xfd, 0x79, 0xe8, 0x6c, 0x83, 0x07, 0x96, 0x12,
+ 0xa9, 0x2d, 0xbc, 0x38, 0x7f, 0xfb, 0x6a, 0xee, 0x55, 0xd1,
+ 0x40, 0xc4, 0x2b, 0xaf, 0x3e, 0xba, 0x01, 0x85, 0x14, 0x90,
+ 0x29, 0xad, 0x3c, 0xb8, 0x03, 0x87, 0x16, 0x92, 0x7d, 0xf9,
+ 0x68, 0xec, 0x57, 0xd3, 0x42, 0xc6, 0x81, 0x05, 0x94, 0x10,
+ 0xab, 0x2f, 0xbe, 0x3a, 0xd5, 0x51, 0xc0, 0x44, 0xff, 0x7b,
+ 0xea, 0x6e, 0x64, 0xe0, 0x71, 0xf5, 0x4e, 0xca, 0x5b, 0xdf,
+ 0x30, 0xb4, 0x25, 0xa1, 0x1a, 0x9e, 0x0f, 0x8b, 0xcc, 0x48,
+ 0xd9, 0x5d, 0xe6, 0x62, 0xf3, 0x77, 0x98, 0x1c, 0x8d, 0x09,
+ 0xb2, 0x36, 0xa7, 0x23, 0xb3, 0x37, 0xa6, 0x22, 0x99, 0x1d,
+ 0x8c, 0x08, 0xe7, 0x63, 0xf2, 0x76, 0xcd, 0x49, 0xd8, 0x5c,
+ 0x1b, 0x9f, 0x0e, 0x8a, 0x31, 0xb5, 0x24, 0xa0, 0x4f, 0xcb,
+ 0x5a, 0xde, 0x65, 0xe1, 0x70, 0xf4, 0xfe, 0x7a, 0xeb, 0x6f,
+ 0xd4, 0x50, 0xc1, 0x45, 0xaa, 0x2e, 0xbf, 0x3b, 0x80, 0x04,
+ 0x95, 0x11, 0x56, 0xd2, 0x43, 0xc7, 0x7c, 0xf8, 0x69, 0xed,
+ 0x02, 0x86, 0x17, 0x93, 0x28, 0xac, 0x3d, 0xb9, 0x00, 0x85,
+ 0x17, 0x92, 0x2e, 0xab, 0x39, 0xbc, 0x5c, 0xd9, 0x4b, 0xce,
+ 0x72, 0xf7, 0x65, 0xe0, 0xb8, 0x3d, 0xaf, 0x2a, 0x96, 0x13,
+ 0x81, 0x04, 0xe4, 0x61, 0xf3, 0x76, 0xca, 0x4f, 0xdd, 0x58,
+ 0x6d, 0xe8, 0x7a, 0xff, 0x43, 0xc6, 0x54, 0xd1, 0x31, 0xb4,
+ 0x26, 0xa3, 0x1f, 0x9a, 0x08, 0x8d, 0xd5, 0x50, 0xc2, 0x47,
+ 0xfb, 0x7e, 0xec, 0x69, 0x89, 0x0c, 0x9e, 0x1b, 0xa7, 0x22,
+ 0xb0, 0x35, 0xda, 0x5f, 0xcd, 0x48, 0xf4, 0x71, 0xe3, 0x66,
+ 0x86, 0x03, 0x91, 0x14, 0xa8, 0x2d, 0xbf, 0x3a, 0x62, 0xe7,
+ 0x75, 0xf0, 0x4c, 0xc9, 0x5b, 0xde, 0x3e, 0xbb, 0x29, 0xac,
+ 0x10, 0x95, 0x07, 0x82, 0xb7, 0x32, 0xa0, 0x25, 0x99, 0x1c,
+ 0x8e, 0x0b, 0xeb, 0x6e, 0xfc, 0x79, 0xc5, 0x40, 0xd2, 0x57,
+ 0x0f, 0x8a, 0x18, 0x9d, 0x21, 0xa4, 0x36, 0xb3, 0x53, 0xd6,
+ 0x44, 0xc1, 0x7d, 0xf8, 0x6a, 0xef, 0xa9, 0x2c, 0xbe, 0x3b,
+ 0x87, 0x02, 0x90, 0x15, 0xf5, 0x70, 0xe2, 0x67, 0xdb, 0x5e,
+ 0xcc, 0x49, 0x11, 0x94, 0x06, 0x83, 0x3f, 0xba, 0x28, 0xad,
+ 0x4d, 0xc8, 0x5a, 0xdf, 0x63, 0xe6, 0x74, 0xf1, 0xc4, 0x41,
+ 0xd3, 0x56, 0xea, 0x6f, 0xfd, 0x78, 0x98, 0x1d, 0x8f, 0x0a,
+ 0xb6, 0x33, 0xa1, 0x24, 0x7c, 0xf9, 0x6b, 0xee, 0x52, 0xd7,
+ 0x45, 0xc0, 0x20, 0xa5, 0x37, 0xb2, 0x0e, 0x8b, 0x19, 0x9c,
+ 0x73, 0xf6, 0x64, 0xe1, 0x5d, 0xd8, 0x4a, 0xcf, 0x2f, 0xaa,
+ 0x38, 0xbd, 0x01, 0x84, 0x16, 0x93, 0xcb, 0x4e, 0xdc, 0x59,
+ 0xe5, 0x60, 0xf2, 0x77, 0x97, 0x12, 0x80, 0x05, 0xb9, 0x3c,
+ 0xae, 0x2b, 0x1e, 0x9b, 0x09, 0x8c, 0x30, 0xb5, 0x27, 0xa2,
+ 0x42, 0xc7, 0x55, 0xd0, 0x6c, 0xe9, 0x7b, 0xfe, 0xa6, 0x23,
+ 0xb1, 0x34, 0x88, 0x0d, 0x9f, 0x1a, 0xfa, 0x7f, 0xed, 0x68,
+ 0xd4, 0x51, 0xc3, 0x46, 0x00, 0x86, 0x11, 0x97, 0x22, 0xa4,
+ 0x33, 0xb5, 0x44, 0xc2, 0x55, 0xd3, 0x66, 0xe0, 0x77, 0xf1,
+ 0x88, 0x0e, 0x99, 0x1f, 0xaa, 0x2c, 0xbb, 0x3d, 0xcc, 0x4a,
+ 0xdd, 0x5b, 0xee, 0x68, 0xff, 0x79, 0x0d, 0x8b, 0x1c, 0x9a,
+ 0x2f, 0xa9, 0x3e, 0xb8, 0x49, 0xcf, 0x58, 0xde, 0x6b, 0xed,
+ 0x7a, 0xfc, 0x85, 0x03, 0x94, 0x12, 0xa7, 0x21, 0xb6, 0x30,
+ 0xc1, 0x47, 0xd0, 0x56, 0xe3, 0x65, 0xf2, 0x74, 0x1a, 0x9c,
+ 0x0b, 0x8d, 0x38, 0xbe, 0x29, 0xaf, 0x5e, 0xd8, 0x4f, 0xc9,
+ 0x7c, 0xfa, 0x6d, 0xeb, 0x92, 0x14, 0x83, 0x05, 0xb0, 0x36,
+ 0xa1, 0x27, 0xd6, 0x50, 0xc7, 0x41, 0xf4, 0x72, 0xe5, 0x63,
+ 0x17, 0x91, 0x06, 0x80, 0x35, 0xb3, 0x24, 0xa2, 0x53, 0xd5,
+ 0x42, 0xc4, 0x71, 0xf7, 0x60, 0xe6, 0x9f, 0x19, 0x8e, 0x08,
+ 0xbd, 0x3b, 0xac, 0x2a, 0xdb, 0x5d, 0xca, 0x4c, 0xf9, 0x7f,
+ 0xe8, 0x6e, 0x34, 0xb2, 0x25, 0xa3, 0x16, 0x90, 0x07, 0x81,
+ 0x70, 0xf6, 0x61, 0xe7, 0x52, 0xd4, 0x43, 0xc5, 0xbc, 0x3a,
+ 0xad, 0x2b, 0x9e, 0x18, 0x8f, 0x09, 0xf8, 0x7e, 0xe9, 0x6f,
+ 0xda, 0x5c, 0xcb, 0x4d, 0x39, 0xbf, 0x28, 0xae, 0x1b, 0x9d,
+ 0x0a, 0x8c, 0x7d, 0xfb, 0x6c, 0xea, 0x5f, 0xd9, 0x4e, 0xc8,
+ 0xb1, 0x37, 0xa0, 0x26, 0x93, 0x15, 0x82, 0x04, 0xf5, 0x73,
+ 0xe4, 0x62, 0xd7, 0x51, 0xc6, 0x40, 0x2e, 0xa8, 0x3f, 0xb9,
+ 0x0c, 0x8a, 0x1d, 0x9b, 0x6a, 0xec, 0x7b, 0xfd, 0x48, 0xce,
+ 0x59, 0xdf, 0xa6, 0x20, 0xb7, 0x31, 0x84, 0x02, 0x95, 0x13,
+ 0xe2, 0x64, 0xf3, 0x75, 0xc0, 0x46, 0xd1, 0x57, 0x23, 0xa5,
+ 0x32, 0xb4, 0x01, 0x87, 0x10, 0x96, 0x67, 0xe1, 0x76, 0xf0,
+ 0x45, 0xc3, 0x54, 0xd2, 0xab, 0x2d, 0xba, 0x3c, 0x89, 0x0f,
+ 0x98, 0x1e, 0xef, 0x69, 0xfe, 0x78, 0xcd, 0x4b, 0xdc, 0x5a,
+ 0x00, 0x87, 0x13, 0x94, 0x26, 0xa1, 0x35, 0xb2, 0x4c, 0xcb,
+ 0x5f, 0xd8, 0x6a, 0xed, 0x79, 0xfe, 0x98, 0x1f, 0x8b, 0x0c,
+ 0xbe, 0x39, 0xad, 0x2a, 0xd4, 0x53, 0xc7, 0x40, 0xf2, 0x75,
+ 0xe1, 0x66, 0x2d, 0xaa, 0x3e, 0xb9, 0x0b, 0x8c, 0x18, 0x9f,
+ 0x61, 0xe6, 0x72, 0xf5, 0x47, 0xc0, 0x54, 0xd3, 0xb5, 0x32,
+ 0xa6, 0x21, 0x93, 0x14, 0x80, 0x07, 0xf9, 0x7e, 0xea, 0x6d,
+ 0xdf, 0x58, 0xcc, 0x4b, 0x5a, 0xdd, 0x49, 0xce, 0x7c, 0xfb,
+ 0x6f, 0xe8, 0x16, 0x91, 0x05, 0x82, 0x30, 0xb7, 0x23, 0xa4,
+ 0xc2, 0x45, 0xd1, 0x56, 0xe4, 0x63, 0xf7, 0x70, 0x8e, 0x09,
+ 0x9d, 0x1a, 0xa8, 0x2f, 0xbb, 0x3c, 0x77, 0xf0, 0x64, 0xe3,
+ 0x51, 0xd6, 0x42, 0xc5, 0x3b, 0xbc, 0x28, 0xaf, 0x1d, 0x9a,
+ 0x0e, 0x89, 0xef, 0x68, 0xfc, 0x7b, 0xc9, 0x4e, 0xda, 0x5d,
+ 0xa3, 0x24, 0xb0, 0x37, 0x85, 0x02, 0x96, 0x11, 0xb4, 0x33,
+ 0xa7, 0x20, 0x92, 0x15, 0x81, 0x06, 0xf8, 0x7f, 0xeb, 0x6c,
+ 0xde, 0x59, 0xcd, 0x4a, 0x2c, 0xab, 0x3f, 0xb8, 0x0a, 0x8d,
+ 0x19, 0x9e, 0x60, 0xe7, 0x73, 0xf4, 0x46, 0xc1, 0x55, 0xd2,
+ 0x99, 0x1e, 0x8a, 0x0d, 0xbf, 0x38, 0xac, 0x2b, 0xd5, 0x52,
+ 0xc6, 0x41, 0xf3, 0x74, 0xe0, 0x67, 0x01, 0x86, 0x12, 0x95,
+ 0x27, 0xa0, 0x34, 0xb3, 0x4d, 0xca, 0x5e, 0xd9, 0x6b, 0xec,
+ 0x78, 0xff, 0xee, 0x69, 0xfd, 0x7a, 0xc8, 0x4f, 0xdb, 0x5c,
+ 0xa2, 0x25, 0xb1, 0x36, 0x84, 0x03, 0x97, 0x10, 0x76, 0xf1,
+ 0x65, 0xe2, 0x50, 0xd7, 0x43, 0xc4, 0x3a, 0xbd, 0x29, 0xae,
+ 0x1c, 0x9b, 0x0f, 0x88, 0xc3, 0x44, 0xd0, 0x57, 0xe5, 0x62,
+ 0xf6, 0x71, 0x8f, 0x08, 0x9c, 0x1b, 0xa9, 0x2e, 0xba, 0x3d,
+ 0x5b, 0xdc, 0x48, 0xcf, 0x7d, 0xfa, 0x6e, 0xe9, 0x17, 0x90,
+ 0x04, 0x83, 0x31, 0xb6, 0x22, 0xa5, 0x00, 0x88, 0x0d, 0x85,
+ 0x1a, 0x92, 0x17, 0x9f, 0x34, 0xbc, 0x39, 0xb1, 0x2e, 0xa6,
+ 0x23, 0xab, 0x68, 0xe0, 0x65, 0xed, 0x72, 0xfa, 0x7f, 0xf7,
+ 0x5c, 0xd4, 0x51, 0xd9, 0x46, 0xce, 0x4b, 0xc3, 0xd0, 0x58,
+ 0xdd, 0x55, 0xca, 0x42, 0xc7, 0x4f, 0xe4, 0x6c, 0xe9, 0x61,
+ 0xfe, 0x76, 0xf3, 0x7b, 0xb8, 0x30, 0xb5, 0x3d, 0xa2, 0x2a,
+ 0xaf, 0x27, 0x8c, 0x04, 0x81, 0x09, 0x96, 0x1e, 0x9b, 0x13,
+ 0xbd, 0x35, 0xb0, 0x38, 0xa7, 0x2f, 0xaa, 0x22, 0x89, 0x01,
+ 0x84, 0x0c, 0x93, 0x1b, 0x9e, 0x16, 0xd5, 0x5d, 0xd8, 0x50,
+ 0xcf, 0x47, 0xc2, 0x4a, 0xe1, 0x69, 0xec, 0x64, 0xfb, 0x73,
+ 0xf6, 0x7e, 0x6d, 0xe5, 0x60, 0xe8, 0x77, 0xff, 0x7a, 0xf2,
+ 0x59, 0xd1, 0x54, 0xdc, 0x43, 0xcb, 0x4e, 0xc6, 0x05, 0x8d,
+ 0x08, 0x80, 0x1f, 0x97, 0x12, 0x9a, 0x31, 0xb9, 0x3c, 0xb4,
+ 0x2b, 0xa3, 0x26, 0xae, 0x67, 0xef, 0x6a, 0xe2, 0x7d, 0xf5,
+ 0x70, 0xf8, 0x53, 0xdb, 0x5e, 0xd6, 0x49, 0xc1, 0x44, 0xcc,
+ 0x0f, 0x87, 0x02, 0x8a, 0x15, 0x9d, 0x18, 0x90, 0x3b, 0xb3,
+ 0x36, 0xbe, 0x21, 0xa9, 0x2c, 0xa4, 0xb7, 0x3f, 0xba, 0x32,
+ 0xad, 0x25, 0xa0, 0x28, 0x83, 0x0b, 0x8e, 0x06, 0x99, 0x11,
+ 0x94, 0x1c, 0xdf, 0x57, 0xd2, 0x5a, 0xc5, 0x4d, 0xc8, 0x40,
+ 0xeb, 0x63, 0xe6, 0x6e, 0xf1, 0x79, 0xfc, 0x74, 0xda, 0x52,
+ 0xd7, 0x5f, 0xc0, 0x48, 0xcd, 0x45, 0xee, 0x66, 0xe3, 0x6b,
+ 0xf4, 0x7c, 0xf9, 0x71, 0xb2, 0x3a, 0xbf, 0x37, 0xa8, 0x20,
+ 0xa5, 0x2d, 0x86, 0x0e, 0x8b, 0x03, 0x9c, 0x14, 0x91, 0x19,
+ 0x0a, 0x82, 0x07, 0x8f, 0x10, 0x98, 0x1d, 0x95, 0x3e, 0xb6,
+ 0x33, 0xbb, 0x24, 0xac, 0x29, 0xa1, 0x62, 0xea, 0x6f, 0xe7,
+ 0x78, 0xf0, 0x75, 0xfd, 0x56, 0xde, 0x5b, 0xd3, 0x4c, 0xc4,
+ 0x41, 0xc9, 0x00, 0x89, 0x0f, 0x86, 0x1e, 0x97, 0x11, 0x98,
+ 0x3c, 0xb5, 0x33, 0xba, 0x22, 0xab, 0x2d, 0xa4, 0x78, 0xf1,
+ 0x77, 0xfe, 0x66, 0xef, 0x69, 0xe0, 0x44, 0xcd, 0x4b, 0xc2,
+ 0x5a, 0xd3, 0x55, 0xdc, 0xf0, 0x79, 0xff, 0x76, 0xee, 0x67,
+ 0xe1, 0x68, 0xcc, 0x45, 0xc3, 0x4a, 0xd2, 0x5b, 0xdd, 0x54,
+ 0x88, 0x01, 0x87, 0x0e, 0x96, 0x1f, 0x99, 0x10, 0xb4, 0x3d,
+ 0xbb, 0x32, 0xaa, 0x23, 0xa5, 0x2c, 0xfd, 0x74, 0xf2, 0x7b,
+ 0xe3, 0x6a, 0xec, 0x65, 0xc1, 0x48, 0xce, 0x47, 0xdf, 0x56,
+ 0xd0, 0x59, 0x85, 0x0c, 0x8a, 0x03, 0x9b, 0x12, 0x94, 0x1d,
+ 0xb9, 0x30, 0xb6, 0x3f, 0xa7, 0x2e, 0xa8, 0x21, 0x0d, 0x84,
+ 0x02, 0x8b, 0x13, 0x9a, 0x1c, 0x95, 0x31, 0xb8, 0x3e, 0xb7,
+ 0x2f, 0xa6, 0x20, 0xa9, 0x75, 0xfc, 0x7a, 0xf3, 0x6b, 0xe2,
+ 0x64, 0xed, 0x49, 0xc0, 0x46, 0xcf, 0x57, 0xde, 0x58, 0xd1,
+ 0xe7, 0x6e, 0xe8, 0x61, 0xf9, 0x70, 0xf6, 0x7f, 0xdb, 0x52,
+ 0xd4, 0x5d, 0xc5, 0x4c, 0xca, 0x43, 0x9f, 0x16, 0x90, 0x19,
+ 0x81, 0x08, 0x8e, 0x07, 0xa3, 0x2a, 0xac, 0x25, 0xbd, 0x34,
+ 0xb2, 0x3b, 0x17, 0x9e, 0x18, 0x91, 0x09, 0x80, 0x06, 0x8f,
+ 0x2b, 0xa2, 0x24, 0xad, 0x35, 0xbc, 0x3a, 0xb3, 0x6f, 0xe6,
+ 0x60, 0xe9, 0x71, 0xf8, 0x7e, 0xf7, 0x53, 0xda, 0x5c, 0xd5,
+ 0x4d, 0xc4, 0x42, 0xcb, 0x1a, 0x93, 0x15, 0x9c, 0x04, 0x8d,
+ 0x0b, 0x82, 0x26, 0xaf, 0x29, 0xa0, 0x38, 0xb1, 0x37, 0xbe,
+ 0x62, 0xeb, 0x6d, 0xe4, 0x7c, 0xf5, 0x73, 0xfa, 0x5e, 0xd7,
+ 0x51, 0xd8, 0x40, 0xc9, 0x4f, 0xc6, 0xea, 0x63, 0xe5, 0x6c,
+ 0xf4, 0x7d, 0xfb, 0x72, 0xd6, 0x5f, 0xd9, 0x50, 0xc8, 0x41,
+ 0xc7, 0x4e, 0x92, 0x1b, 0x9d, 0x14, 0x8c, 0x05, 0x83, 0x0a,
+ 0xae, 0x27, 0xa1, 0x28, 0xb0, 0x39, 0xbf, 0x36, 0x00, 0x8a,
+ 0x09, 0x83, 0x12, 0x98, 0x1b, 0x91, 0x24, 0xae, 0x2d, 0xa7,
+ 0x36, 0xbc, 0x3f, 0xb5, 0x48, 0xc2, 0x41, 0xcb, 0x5a, 0xd0,
+ 0x53, 0xd9, 0x6c, 0xe6, 0x65, 0xef, 0x7e, 0xf4, 0x77, 0xfd,
+ 0x90, 0x1a, 0x99, 0x13, 0x82, 0x08, 0x8b, 0x01, 0xb4, 0x3e,
+ 0xbd, 0x37, 0xa6, 0x2c, 0xaf, 0x25, 0xd8, 0x52, 0xd1, 0x5b,
+ 0xca, 0x40, 0xc3, 0x49, 0xfc, 0x76, 0xf5, 0x7f, 0xee, 0x64,
+ 0xe7, 0x6d, 0x3d, 0xb7, 0x34, 0xbe, 0x2f, 0xa5, 0x26, 0xac,
+ 0x19, 0x93, 0x10, 0x9a, 0x0b, 0x81, 0x02, 0x88, 0x75, 0xff,
+ 0x7c, 0xf6, 0x67, 0xed, 0x6e, 0xe4, 0x51, 0xdb, 0x58, 0xd2,
+ 0x43, 0xc9, 0x4a, 0xc0, 0xad, 0x27, 0xa4, 0x2e, 0xbf, 0x35,
+ 0xb6, 0x3c, 0x89, 0x03, 0x80, 0x0a, 0x9b, 0x11, 0x92, 0x18,
+ 0xe5, 0x6f, 0xec, 0x66, 0xf7, 0x7d, 0xfe, 0x74, 0xc1, 0x4b,
+ 0xc8, 0x42, 0xd3, 0x59, 0xda, 0x50, 0x7a, 0xf0, 0x73, 0xf9,
+ 0x68, 0xe2, 0x61, 0xeb, 0x5e, 0xd4, 0x57, 0xdd, 0x4c, 0xc6,
+ 0x45, 0xcf, 0x32, 0xb8, 0x3b, 0xb1, 0x20, 0xaa, 0x29, 0xa3,
+ 0x16, 0x9c, 0x1f, 0x95, 0x04, 0x8e, 0x0d, 0x87, 0xea, 0x60,
+ 0xe3, 0x69, 0xf8, 0x72, 0xf1, 0x7b, 0xce, 0x44, 0xc7, 0x4d,
+ 0xdc, 0x56, 0xd5, 0x5f, 0xa2, 0x28, 0xab, 0x21, 0xb0, 0x3a,
+ 0xb9, 0x33, 0x86, 0x0c, 0x8f, 0x05, 0x94, 0x1e, 0x9d, 0x17,
+ 0x47, 0xcd, 0x4e, 0xc4, 0x55, 0xdf, 0x5c, 0xd6, 0x63, 0xe9,
+ 0x6a, 0xe0, 0x71, 0xfb, 0x78, 0xf2, 0x0f, 0x85, 0x06, 0x8c,
+ 0x1d, 0x97, 0x14, 0x9e, 0x2b, 0xa1, 0x22, 0xa8, 0x39, 0xb3,
+ 0x30, 0xba, 0xd7, 0x5d, 0xde, 0x54, 0xc5, 0x4f, 0xcc, 0x46,
+ 0xf3, 0x79, 0xfa, 0x70, 0xe1, 0x6b, 0xe8, 0x62, 0x9f, 0x15,
+ 0x96, 0x1c, 0x8d, 0x07, 0x84, 0x0e, 0xbb, 0x31, 0xb2, 0x38,
+ 0xa9, 0x23, 0xa0, 0x2a, 0x00, 0x8b, 0x0b, 0x80, 0x16, 0x9d,
+ 0x1d, 0x96, 0x2c, 0xa7, 0x27, 0xac, 0x3a, 0xb1, 0x31, 0xba,
+ 0x58, 0xd3, 0x53, 0xd8, 0x4e, 0xc5, 0x45, 0xce, 0x74, 0xff,
+ 0x7f, 0xf4, 0x62, 0xe9, 0x69, 0xe2, 0xb0, 0x3b, 0xbb, 0x30,
+ 0xa6, 0x2d, 0xad, 0x26, 0x9c, 0x17, 0x97, 0x1c, 0x8a, 0x01,
+ 0x81, 0x0a, 0xe8, 0x63, 0xe3, 0x68, 0xfe, 0x75, 0xf5, 0x7e,
+ 0xc4, 0x4f, 0xcf, 0x44, 0xd2, 0x59, 0xd9, 0x52, 0x7d, 0xf6,
+ 0x76, 0xfd, 0x6b, 0xe0, 0x60, 0xeb, 0x51, 0xda, 0x5a, 0xd1,
+ 0x47, 0xcc, 0x4c, 0xc7, 0x25, 0xae, 0x2e, 0xa5, 0x33, 0xb8,
+ 0x38, 0xb3, 0x09, 0x82, 0x02, 0x89, 0x1f, 0x94, 0x14, 0x9f,
+ 0xcd, 0x46, 0xc6, 0x4d, 0xdb, 0x50, 0xd0, 0x5b, 0xe1, 0x6a,
+ 0xea, 0x61, 0xf7, 0x7c, 0xfc, 0x77, 0x95, 0x1e, 0x9e, 0x15,
+ 0x83, 0x08, 0x88, 0x03, 0xb9, 0x32, 0xb2, 0x39, 0xaf, 0x24,
+ 0xa4, 0x2f, 0xfa, 0x71, 0xf1, 0x7a, 0xec, 0x67, 0xe7, 0x6c,
+ 0xd6, 0x5d, 0xdd, 0x56, 0xc0, 0x4b, 0xcb, 0x40, 0xa2, 0x29,
+ 0xa9, 0x22, 0xb4, 0x3f, 0xbf, 0x34, 0x8e, 0x05, 0x85, 0x0e,
+ 0x98, 0x13, 0x93, 0x18, 0x4a, 0xc1, 0x41, 0xca, 0x5c, 0xd7,
+ 0x57, 0xdc, 0x66, 0xed, 0x6d, 0xe6, 0x70, 0xfb, 0x7b, 0xf0,
+ 0x12, 0x99, 0x19, 0x92, 0x04, 0x8f, 0x0f, 0x84, 0x3e, 0xb5,
+ 0x35, 0xbe, 0x28, 0xa3, 0x23, 0xa8, 0x87, 0x0c, 0x8c, 0x07,
+ 0x91, 0x1a, 0x9a, 0x11, 0xab, 0x20, 0xa0, 0x2b, 0xbd, 0x36,
+ 0xb6, 0x3d, 0xdf, 0x54, 0xd4, 0x5f, 0xc9, 0x42, 0xc2, 0x49,
+ 0xf3, 0x78, 0xf8, 0x73, 0xe5, 0x6e, 0xee, 0x65, 0x37, 0xbc,
+ 0x3c, 0xb7, 0x21, 0xaa, 0x2a, 0xa1, 0x1b, 0x90, 0x10, 0x9b,
+ 0x0d, 0x86, 0x06, 0x8d, 0x6f, 0xe4, 0x64, 0xef, 0x79, 0xf2,
+ 0x72, 0xf9, 0x43, 0xc8, 0x48, 0xc3, 0x55, 0xde, 0x5e, 0xd5,
+ 0x00, 0x8c, 0x05, 0x89, 0x0a, 0x86, 0x0f, 0x83, 0x14, 0x98,
+ 0x11, 0x9d, 0x1e, 0x92, 0x1b, 0x97, 0x28, 0xa4, 0x2d, 0xa1,
+ 0x22, 0xae, 0x27, 0xab, 0x3c, 0xb0, 0x39, 0xb5, 0x36, 0xba,
+ 0x33, 0xbf, 0x50, 0xdc, 0x55, 0xd9, 0x5a, 0xd6, 0x5f, 0xd3,
+ 0x44, 0xc8, 0x41, 0xcd, 0x4e, 0xc2, 0x4b, 0xc7, 0x78, 0xf4,
+ 0x7d, 0xf1, 0x72, 0xfe, 0x77, 0xfb, 0x6c, 0xe0, 0x69, 0xe5,
+ 0x66, 0xea, 0x63, 0xef, 0xa0, 0x2c, 0xa5, 0x29, 0xaa, 0x26,
+ 0xaf, 0x23, 0xb4, 0x38, 0xb1, 0x3d, 0xbe, 0x32, 0xbb, 0x37,
+ 0x88, 0x04, 0x8d, 0x01, 0x82, 0x0e, 0x87, 0x0b, 0x9c, 0x10,
+ 0x99, 0x15, 0x96, 0x1a, 0x93, 0x1f, 0xf0, 0x7c, 0xf5, 0x79,
+ 0xfa, 0x76, 0xff, 0x73, 0xe4, 0x68, 0xe1, 0x6d, 0xee, 0x62,
+ 0xeb, 0x67, 0xd8, 0x54, 0xdd, 0x51, 0xd2, 0x5e, 0xd7, 0x5b,
+ 0xcc, 0x40, 0xc9, 0x45, 0xc6, 0x4a, 0xc3, 0x4f, 0x5d, 0xd1,
+ 0x58, 0xd4, 0x57, 0xdb, 0x52, 0xde, 0x49, 0xc5, 0x4c, 0xc0,
+ 0x43, 0xcf, 0x46, 0xca, 0x75, 0xf9, 0x70, 0xfc, 0x7f, 0xf3,
+ 0x7a, 0xf6, 0x61, 0xed, 0x64, 0xe8, 0x6b, 0xe7, 0x6e, 0xe2,
+ 0x0d, 0x81, 0x08, 0x84, 0x07, 0x8b, 0x02, 0x8e, 0x19, 0x95,
+ 0x1c, 0x90, 0x13, 0x9f, 0x16, 0x9a, 0x25, 0xa9, 0x20, 0xac,
+ 0x2f, 0xa3, 0x2a, 0xa6, 0x31, 0xbd, 0x34, 0xb8, 0x3b, 0xb7,
+ 0x3e, 0xb2, 0xfd, 0x71, 0xf8, 0x74, 0xf7, 0x7b, 0xf2, 0x7e,
+ 0xe9, 0x65, 0xec, 0x60, 0xe3, 0x6f, 0xe6, 0x6a, 0xd5, 0x59,
+ 0xd0, 0x5c, 0xdf, 0x53, 0xda, 0x56, 0xc1, 0x4d, 0xc4, 0x48,
+ 0xcb, 0x47, 0xce, 0x42, 0xad, 0x21, 0xa8, 0x24, 0xa7, 0x2b,
+ 0xa2, 0x2e, 0xb9, 0x35, 0xbc, 0x30, 0xb3, 0x3f, 0xb6, 0x3a,
+ 0x85, 0x09, 0x80, 0x0c, 0x8f, 0x03, 0x8a, 0x06, 0x91, 0x1d,
+ 0x94, 0x18, 0x9b, 0x17, 0x9e, 0x12, 0x00, 0x8d, 0x07, 0x8a,
+ 0x0e, 0x83, 0x09, 0x84, 0x1c, 0x91, 0x1b, 0x96, 0x12, 0x9f,
+ 0x15, 0x98, 0x38, 0xb5, 0x3f, 0xb2, 0x36, 0xbb, 0x31, 0xbc,
+ 0x24, 0xa9, 0x23, 0xae, 0x2a, 0xa7, 0x2d, 0xa0, 0x70, 0xfd,
+ 0x77, 0xfa, 0x7e, 0xf3, 0x79, 0xf4, 0x6c, 0xe1, 0x6b, 0xe6,
+ 0x62, 0xef, 0x65, 0xe8, 0x48, 0xc5, 0x4f, 0xc2, 0x46, 0xcb,
+ 0x41, 0xcc, 0x54, 0xd9, 0x53, 0xde, 0x5a, 0xd7, 0x5d, 0xd0,
+ 0xe0, 0x6d, 0xe7, 0x6a, 0xee, 0x63, 0xe9, 0x64, 0xfc, 0x71,
+ 0xfb, 0x76, 0xf2, 0x7f, 0xf5, 0x78, 0xd8, 0x55, 0xdf, 0x52,
+ 0xd6, 0x5b, 0xd1, 0x5c, 0xc4, 0x49, 0xc3, 0x4e, 0xca, 0x47,
+ 0xcd, 0x40, 0x90, 0x1d, 0x97, 0x1a, 0x9e, 0x13, 0x99, 0x14,
+ 0x8c, 0x01, 0x8b, 0x06, 0x82, 0x0f, 0x85, 0x08, 0xa8, 0x25,
+ 0xaf, 0x22, 0xa6, 0x2b, 0xa1, 0x2c, 0xb4, 0x39, 0xb3, 0x3e,
+ 0xba, 0x37, 0xbd, 0x30, 0xdd, 0x50, 0xda, 0x57, 0xd3, 0x5e,
+ 0xd4, 0x59, 0xc1, 0x4c, 0xc6, 0x4b, 0xcf, 0x42, 0xc8, 0x45,
+ 0xe5, 0x68, 0xe2, 0x6f, 0xeb, 0x66, 0xec, 0x61, 0xf9, 0x74,
+ 0xfe, 0x73, 0xf7, 0x7a, 0xf0, 0x7d, 0xad, 0x20, 0xaa, 0x27,
+ 0xa3, 0x2e, 0xa4, 0x29, 0xb1, 0x3c, 0xb6, 0x3b, 0xbf, 0x32,
+ 0xb8, 0x35, 0x95, 0x18, 0x92, 0x1f, 0x9b, 0x16, 0x9c, 0x11,
+ 0x89, 0x04, 0x8e, 0x03, 0x87, 0x0a, 0x80, 0x0d, 0x3d, 0xb0,
+ 0x3a, 0xb7, 0x33, 0xbe, 0x34, 0xb9, 0x21, 0xac, 0x26, 0xab,
+ 0x2f, 0xa2, 0x28, 0xa5, 0x05, 0x88, 0x02, 0x8f, 0x0b, 0x86,
+ 0x0c, 0x81, 0x19, 0x94, 0x1e, 0x93, 0x17, 0x9a, 0x10, 0x9d,
+ 0x4d, 0xc0, 0x4a, 0xc7, 0x43, 0xce, 0x44, 0xc9, 0x51, 0xdc,
+ 0x56, 0xdb, 0x5f, 0xd2, 0x58, 0xd5, 0x75, 0xf8, 0x72, 0xff,
+ 0x7b, 0xf6, 0x7c, 0xf1, 0x69, 0xe4, 0x6e, 0xe3, 0x67, 0xea,
+ 0x60, 0xed, 0x00, 0x8e, 0x01, 0x8f, 0x02, 0x8c, 0x03, 0x8d,
+ 0x04, 0x8a, 0x05, 0x8b, 0x06, 0x88, 0x07, 0x89, 0x08, 0x86,
+ 0x09, 0x87, 0x0a, 0x84, 0x0b, 0x85, 0x0c, 0x82, 0x0d, 0x83,
+ 0x0e, 0x80, 0x0f, 0x81, 0x10, 0x9e, 0x11, 0x9f, 0x12, 0x9c,
+ 0x13, 0x9d, 0x14, 0x9a, 0x15, 0x9b, 0x16, 0x98, 0x17, 0x99,
+ 0x18, 0x96, 0x19, 0x97, 0x1a, 0x94, 0x1b, 0x95, 0x1c, 0x92,
+ 0x1d, 0x93, 0x1e, 0x90, 0x1f, 0x91, 0x20, 0xae, 0x21, 0xaf,
+ 0x22, 0xac, 0x23, 0xad, 0x24, 0xaa, 0x25, 0xab, 0x26, 0xa8,
+ 0x27, 0xa9, 0x28, 0xa6, 0x29, 0xa7, 0x2a, 0xa4, 0x2b, 0xa5,
+ 0x2c, 0xa2, 0x2d, 0xa3, 0x2e, 0xa0, 0x2f, 0xa1, 0x30, 0xbe,
+ 0x31, 0xbf, 0x32, 0xbc, 0x33, 0xbd, 0x34, 0xba, 0x35, 0xbb,
+ 0x36, 0xb8, 0x37, 0xb9, 0x38, 0xb6, 0x39, 0xb7, 0x3a, 0xb4,
+ 0x3b, 0xb5, 0x3c, 0xb2, 0x3d, 0xb3, 0x3e, 0xb0, 0x3f, 0xb1,
+ 0x40, 0xce, 0x41, 0xcf, 0x42, 0xcc, 0x43, 0xcd, 0x44, 0xca,
+ 0x45, 0xcb, 0x46, 0xc8, 0x47, 0xc9, 0x48, 0xc6, 0x49, 0xc7,
+ 0x4a, 0xc4, 0x4b, 0xc5, 0x4c, 0xc2, 0x4d, 0xc3, 0x4e, 0xc0,
+ 0x4f, 0xc1, 0x50, 0xde, 0x51, 0xdf, 0x52, 0xdc, 0x53, 0xdd,
+ 0x54, 0xda, 0x55, 0xdb, 0x56, 0xd8, 0x57, 0xd9, 0x58, 0xd6,
+ 0x59, 0xd7, 0x5a, 0xd4, 0x5b, 0xd5, 0x5c, 0xd2, 0x5d, 0xd3,
+ 0x5e, 0xd0, 0x5f, 0xd1, 0x60, 0xee, 0x61, 0xef, 0x62, 0xec,
+ 0x63, 0xed, 0x64, 0xea, 0x65, 0xeb, 0x66, 0xe8, 0x67, 0xe9,
+ 0x68, 0xe6, 0x69, 0xe7, 0x6a, 0xe4, 0x6b, 0xe5, 0x6c, 0xe2,
+ 0x6d, 0xe3, 0x6e, 0xe0, 0x6f, 0xe1, 0x70, 0xfe, 0x71, 0xff,
+ 0x72, 0xfc, 0x73, 0xfd, 0x74, 0xfa, 0x75, 0xfb, 0x76, 0xf8,
+ 0x77, 0xf9, 0x78, 0xf6, 0x79, 0xf7, 0x7a, 0xf4, 0x7b, 0xf5,
+ 0x7c, 0xf2, 0x7d, 0xf3, 0x7e, 0xf0, 0x7f, 0xf1, 0x00, 0x8f,
+ 0x03, 0x8c, 0x06, 0x89, 0x05, 0x8a, 0x0c, 0x83, 0x0f, 0x80,
+ 0x0a, 0x85, 0x09, 0x86, 0x18, 0x97, 0x1b, 0x94, 0x1e, 0x91,
+ 0x1d, 0x92, 0x14, 0x9b, 0x17, 0x98, 0x12, 0x9d, 0x11, 0x9e,
+ 0x30, 0xbf, 0x33, 0xbc, 0x36, 0xb9, 0x35, 0xba, 0x3c, 0xb3,
+ 0x3f, 0xb0, 0x3a, 0xb5, 0x39, 0xb6, 0x28, 0xa7, 0x2b, 0xa4,
+ 0x2e, 0xa1, 0x2d, 0xa2, 0x24, 0xab, 0x27, 0xa8, 0x22, 0xad,
+ 0x21, 0xae, 0x60, 0xef, 0x63, 0xec, 0x66, 0xe9, 0x65, 0xea,
+ 0x6c, 0xe3, 0x6f, 0xe0, 0x6a, 0xe5, 0x69, 0xe6, 0x78, 0xf7,
+ 0x7b, 0xf4, 0x7e, 0xf1, 0x7d, 0xf2, 0x74, 0xfb, 0x77, 0xf8,
+ 0x72, 0xfd, 0x71, 0xfe, 0x50, 0xdf, 0x53, 0xdc, 0x56, 0xd9,
+ 0x55, 0xda, 0x5c, 0xd3, 0x5f, 0xd0, 0x5a, 0xd5, 0x59, 0xd6,
+ 0x48, 0xc7, 0x4b, 0xc4, 0x4e, 0xc1, 0x4d, 0xc2, 0x44, 0xcb,
+ 0x47, 0xc8, 0x42, 0xcd, 0x41, 0xce, 0xc0, 0x4f, 0xc3, 0x4c,
+ 0xc6, 0x49, 0xc5, 0x4a, 0xcc, 0x43, 0xcf, 0x40, 0xca, 0x45,
+ 0xc9, 0x46, 0xd8, 0x57, 0xdb, 0x54, 0xde, 0x51, 0xdd, 0x52,
+ 0xd4, 0x5b, 0xd7, 0x58, 0xd2, 0x5d, 0xd1, 0x5e, 0xf0, 0x7f,
+ 0xf3, 0x7c, 0xf6, 0x79, 0xf5, 0x7a, 0xfc, 0x73, 0xff, 0x70,
+ 0xfa, 0x75, 0xf9, 0x76, 0xe8, 0x67, 0xeb, 0x64, 0xee, 0x61,
+ 0xed, 0x62, 0xe4, 0x6b, 0xe7, 0x68, 0xe2, 0x6d, 0xe1, 0x6e,
+ 0xa0, 0x2f, 0xa3, 0x2c, 0xa6, 0x29, 0xa5, 0x2a, 0xac, 0x23,
+ 0xaf, 0x20, 0xaa, 0x25, 0xa9, 0x26, 0xb8, 0x37, 0xbb, 0x34,
+ 0xbe, 0x31, 0xbd, 0x32, 0xb4, 0x3b, 0xb7, 0x38, 0xb2, 0x3d,
+ 0xb1, 0x3e, 0x90, 0x1f, 0x93, 0x1c, 0x96, 0x19, 0x95, 0x1a,
+ 0x9c, 0x13, 0x9f, 0x10, 0x9a, 0x15, 0x99, 0x16, 0x88, 0x07,
+ 0x8b, 0x04, 0x8e, 0x01, 0x8d, 0x02, 0x84, 0x0b, 0x87, 0x08,
+ 0x82, 0x0d, 0x81, 0x0e, 0x00, 0x90, 0x3d, 0xad, 0x7a, 0xea,
+ 0x47, 0xd7, 0xf4, 0x64, 0xc9, 0x59, 0x8e, 0x1e, 0xb3, 0x23,
+ 0xf5, 0x65, 0xc8, 0x58, 0x8f, 0x1f, 0xb2, 0x22, 0x01, 0x91,
+ 0x3c, 0xac, 0x7b, 0xeb, 0x46, 0xd6, 0xf7, 0x67, 0xca, 0x5a,
+ 0x8d, 0x1d, 0xb0, 0x20, 0x03, 0x93, 0x3e, 0xae, 0x79, 0xe9,
+ 0x44, 0xd4, 0x02, 0x92, 0x3f, 0xaf, 0x78, 0xe8, 0x45, 0xd5,
+ 0xf6, 0x66, 0xcb, 0x5b, 0x8c, 0x1c, 0xb1, 0x21, 0xf3, 0x63,
+ 0xce, 0x5e, 0x89, 0x19, 0xb4, 0x24, 0x07, 0x97, 0x3a, 0xaa,
+ 0x7d, 0xed, 0x40, 0xd0, 0x06, 0x96, 0x3b, 0xab, 0x7c, 0xec,
+ 0x41, 0xd1, 0xf2, 0x62, 0xcf, 0x5f, 0x88, 0x18, 0xb5, 0x25,
+ 0x04, 0x94, 0x39, 0xa9, 0x7e, 0xee, 0x43, 0xd3, 0xf0, 0x60,
+ 0xcd, 0x5d, 0x8a, 0x1a, 0xb7, 0x27, 0xf1, 0x61, 0xcc, 0x5c,
+ 0x8b, 0x1b, 0xb6, 0x26, 0x05, 0x95, 0x38, 0xa8, 0x7f, 0xef,
+ 0x42, 0xd2, 0xfb, 0x6b, 0xc6, 0x56, 0x81, 0x11, 0xbc, 0x2c,
+ 0x0f, 0x9f, 0x32, 0xa2, 0x75, 0xe5, 0x48, 0xd8, 0x0e, 0x9e,
+ 0x33, 0xa3, 0x74, 0xe4, 0x49, 0xd9, 0xfa, 0x6a, 0xc7, 0x57,
+ 0x80, 0x10, 0xbd, 0x2d, 0x0c, 0x9c, 0x31, 0xa1, 0x76, 0xe6,
+ 0x4b, 0xdb, 0xf8, 0x68, 0xc5, 0x55, 0x82, 0x12, 0xbf, 0x2f,
+ 0xf9, 0x69, 0xc4, 0x54, 0x83, 0x13, 0xbe, 0x2e, 0x0d, 0x9d,
+ 0x30, 0xa0, 0x77, 0xe7, 0x4a, 0xda, 0x08, 0x98, 0x35, 0xa5,
+ 0x72, 0xe2, 0x4f, 0xdf, 0xfc, 0x6c, 0xc1, 0x51, 0x86, 0x16,
+ 0xbb, 0x2b, 0xfd, 0x6d, 0xc0, 0x50, 0x87, 0x17, 0xba, 0x2a,
+ 0x09, 0x99, 0x34, 0xa4, 0x73, 0xe3, 0x4e, 0xde, 0xff, 0x6f,
+ 0xc2, 0x52, 0x85, 0x15, 0xb8, 0x28, 0x0b, 0x9b, 0x36, 0xa6,
+ 0x71, 0xe1, 0x4c, 0xdc, 0x0a, 0x9a, 0x37, 0xa7, 0x70, 0xe0,
+ 0x4d, 0xdd, 0xfe, 0x6e, 0xc3, 0x53, 0x84, 0x14, 0xb9, 0x29,
+ 0x00, 0x91, 0x3f, 0xae, 0x7e, 0xef, 0x41, 0xd0, 0xfc, 0x6d,
+ 0xc3, 0x52, 0x82, 0x13, 0xbd, 0x2c, 0xe5, 0x74, 0xda, 0x4b,
+ 0x9b, 0x0a, 0xa4, 0x35, 0x19, 0x88, 0x26, 0xb7, 0x67, 0xf6,
+ 0x58, 0xc9, 0xd7, 0x46, 0xe8, 0x79, 0xa9, 0x38, 0x96, 0x07,
+ 0x2b, 0xba, 0x14, 0x85, 0x55, 0xc4, 0x6a, 0xfb, 0x32, 0xa3,
+ 0x0d, 0x9c, 0x4c, 0xdd, 0x73, 0xe2, 0xce, 0x5f, 0xf1, 0x60,
+ 0xb0, 0x21, 0x8f, 0x1e, 0xb3, 0x22, 0x8c, 0x1d, 0xcd, 0x5c,
+ 0xf2, 0x63, 0x4f, 0xde, 0x70, 0xe1, 0x31, 0xa0, 0x0e, 0x9f,
+ 0x56, 0xc7, 0x69, 0xf8, 0x28, 0xb9, 0x17, 0x86, 0xaa, 0x3b,
+ 0x95, 0x04, 0xd4, 0x45, 0xeb, 0x7a, 0x64, 0xf5, 0x5b, 0xca,
+ 0x1a, 0x8b, 0x25, 0xb4, 0x98, 0x09, 0xa7, 0x36, 0xe6, 0x77,
+ 0xd9, 0x48, 0x81, 0x10, 0xbe, 0x2f, 0xff, 0x6e, 0xc0, 0x51,
+ 0x7d, 0xec, 0x42, 0xd3, 0x03, 0x92, 0x3c, 0xad, 0x7b, 0xea,
+ 0x44, 0xd5, 0x05, 0x94, 0x3a, 0xab, 0x87, 0x16, 0xb8, 0x29,
+ 0xf9, 0x68, 0xc6, 0x57, 0x9e, 0x0f, 0xa1, 0x30, 0xe0, 0x71,
+ 0xdf, 0x4e, 0x62, 0xf3, 0x5d, 0xcc, 0x1c, 0x8d, 0x23, 0xb2,
+ 0xac, 0x3d, 0x93, 0x02, 0xd2, 0x43, 0xed, 0x7c, 0x50, 0xc1,
+ 0x6f, 0xfe, 0x2e, 0xbf, 0x11, 0x80, 0x49, 0xd8, 0x76, 0xe7,
+ 0x37, 0xa6, 0x08, 0x99, 0xb5, 0x24, 0x8a, 0x1b, 0xcb, 0x5a,
+ 0xf4, 0x65, 0xc8, 0x59, 0xf7, 0x66, 0xb6, 0x27, 0x89, 0x18,
+ 0x34, 0xa5, 0x0b, 0x9a, 0x4a, 0xdb, 0x75, 0xe4, 0x2d, 0xbc,
+ 0x12, 0x83, 0x53, 0xc2, 0x6c, 0xfd, 0xd1, 0x40, 0xee, 0x7f,
+ 0xaf, 0x3e, 0x90, 0x01, 0x1f, 0x8e, 0x20, 0xb1, 0x61, 0xf0,
+ 0x5e, 0xcf, 0xe3, 0x72, 0xdc, 0x4d, 0x9d, 0x0c, 0xa2, 0x33,
+ 0xfa, 0x6b, 0xc5, 0x54, 0x84, 0x15, 0xbb, 0x2a, 0x06, 0x97,
+ 0x39, 0xa8, 0x78, 0xe9, 0x47, 0xd6, 0x00, 0x92, 0x39, 0xab,
+ 0x72, 0xe0, 0x4b, 0xd9, 0xe4, 0x76, 0xdd, 0x4f, 0x96, 0x04,
+ 0xaf, 0x3d, 0xd5, 0x47, 0xec, 0x7e, 0xa7, 0x35, 0x9e, 0x0c,
+ 0x31, 0xa3, 0x08, 0x9a, 0x43, 0xd1, 0x7a, 0xe8, 0xb7, 0x25,
+ 0x8e, 0x1c, 0xc5, 0x57, 0xfc, 0x6e, 0x53, 0xc1, 0x6a, 0xf8,
+ 0x21, 0xb3, 0x18, 0x8a, 0x62, 0xf0, 0x5b, 0xc9, 0x10, 0x82,
+ 0x29, 0xbb, 0x86, 0x14, 0xbf, 0x2d, 0xf4, 0x66, 0xcd, 0x5f,
+ 0x73, 0xe1, 0x4a, 0xd8, 0x01, 0x93, 0x38, 0xaa, 0x97, 0x05,
+ 0xae, 0x3c, 0xe5, 0x77, 0xdc, 0x4e, 0xa6, 0x34, 0x9f, 0x0d,
+ 0xd4, 0x46, 0xed, 0x7f, 0x42, 0xd0, 0x7b, 0xe9, 0x30, 0xa2,
+ 0x09, 0x9b, 0xc4, 0x56, 0xfd, 0x6f, 0xb6, 0x24, 0x8f, 0x1d,
+ 0x20, 0xb2, 0x19, 0x8b, 0x52, 0xc0, 0x6b, 0xf9, 0x11, 0x83,
+ 0x28, 0xba, 0x63, 0xf1, 0x5a, 0xc8, 0xf5, 0x67, 0xcc, 0x5e,
+ 0x87, 0x15, 0xbe, 0x2c, 0xe6, 0x74, 0xdf, 0x4d, 0x94, 0x06,
+ 0xad, 0x3f, 0x02, 0x90, 0x3b, 0xa9, 0x70, 0xe2, 0x49, 0xdb,
+ 0x33, 0xa1, 0x0a, 0x98, 0x41, 0xd3, 0x78, 0xea, 0xd7, 0x45,
+ 0xee, 0x7c, 0xa5, 0x37, 0x9c, 0x0e, 0x51, 0xc3, 0x68, 0xfa,
+ 0x23, 0xb1, 0x1a, 0x88, 0xb5, 0x27, 0x8c, 0x1e, 0xc7, 0x55,
+ 0xfe, 0x6c, 0x84, 0x16, 0xbd, 0x2f, 0xf6, 0x64, 0xcf, 0x5d,
+ 0x60, 0xf2, 0x59, 0xcb, 0x12, 0x80, 0x2b, 0xb9, 0x95, 0x07,
+ 0xac, 0x3e, 0xe7, 0x75, 0xde, 0x4c, 0x71, 0xe3, 0x48, 0xda,
+ 0x03, 0x91, 0x3a, 0xa8, 0x40, 0xd2, 0x79, 0xeb, 0x32, 0xa0,
+ 0x0b, 0x99, 0xa4, 0x36, 0x9d, 0x0f, 0xd6, 0x44, 0xef, 0x7d,
+ 0x22, 0xb0, 0x1b, 0x89, 0x50, 0xc2, 0x69, 0xfb, 0xc6, 0x54,
+ 0xff, 0x6d, 0xb4, 0x26, 0x8d, 0x1f, 0xf7, 0x65, 0xce, 0x5c,
+ 0x85, 0x17, 0xbc, 0x2e, 0x13, 0x81, 0x2a, 0xb8, 0x61, 0xf3,
+ 0x58, 0xca, 0x00, 0x93, 0x3b, 0xa8, 0x76, 0xe5, 0x4d, 0xde,
+ 0xec, 0x7f, 0xd7, 0x44, 0x9a, 0x09, 0xa1, 0x32, 0xc5, 0x56,
+ 0xfe, 0x6d, 0xb3, 0x20, 0x88, 0x1b, 0x29, 0xba, 0x12, 0x81,
+ 0x5f, 0xcc, 0x64, 0xf7, 0x97, 0x04, 0xac, 0x3f, 0xe1, 0x72,
+ 0xda, 0x49, 0x7b, 0xe8, 0x40, 0xd3, 0x0d, 0x9e, 0x36, 0xa5,
+ 0x52, 0xc1, 0x69, 0xfa, 0x24, 0xb7, 0x1f, 0x8c, 0xbe, 0x2d,
+ 0x85, 0x16, 0xc8, 0x5b, 0xf3, 0x60, 0x33, 0xa0, 0x08, 0x9b,
+ 0x45, 0xd6, 0x7e, 0xed, 0xdf, 0x4c, 0xe4, 0x77, 0xa9, 0x3a,
+ 0x92, 0x01, 0xf6, 0x65, 0xcd, 0x5e, 0x80, 0x13, 0xbb, 0x28,
+ 0x1a, 0x89, 0x21, 0xb2, 0x6c, 0xff, 0x57, 0xc4, 0xa4, 0x37,
+ 0x9f, 0x0c, 0xd2, 0x41, 0xe9, 0x7a, 0x48, 0xdb, 0x73, 0xe0,
+ 0x3e, 0xad, 0x05, 0x96, 0x61, 0xf2, 0x5a, 0xc9, 0x17, 0x84,
+ 0x2c, 0xbf, 0x8d, 0x1e, 0xb6, 0x25, 0xfb, 0x68, 0xc0, 0x53,
+ 0x66, 0xf5, 0x5d, 0xce, 0x10, 0x83, 0x2b, 0xb8, 0x8a, 0x19,
+ 0xb1, 0x22, 0xfc, 0x6f, 0xc7, 0x54, 0xa3, 0x30, 0x98, 0x0b,
+ 0xd5, 0x46, 0xee, 0x7d, 0x4f, 0xdc, 0x74, 0xe7, 0x39, 0xaa,
+ 0x02, 0x91, 0xf1, 0x62, 0xca, 0x59, 0x87, 0x14, 0xbc, 0x2f,
+ 0x1d, 0x8e, 0x26, 0xb5, 0x6b, 0xf8, 0x50, 0xc3, 0x34, 0xa7,
+ 0x0f, 0x9c, 0x42, 0xd1, 0x79, 0xea, 0xd8, 0x4b, 0xe3, 0x70,
+ 0xae, 0x3d, 0x95, 0x06, 0x55, 0xc6, 0x6e, 0xfd, 0x23, 0xb0,
+ 0x18, 0x8b, 0xb9, 0x2a, 0x82, 0x11, 0xcf, 0x5c, 0xf4, 0x67,
+ 0x90, 0x03, 0xab, 0x38, 0xe6, 0x75, 0xdd, 0x4e, 0x7c, 0xef,
+ 0x47, 0xd4, 0x0a, 0x99, 0x31, 0xa2, 0xc2, 0x51, 0xf9, 0x6a,
+ 0xb4, 0x27, 0x8f, 0x1c, 0x2e, 0xbd, 0x15, 0x86, 0x58, 0xcb,
+ 0x63, 0xf0, 0x07, 0x94, 0x3c, 0xaf, 0x71, 0xe2, 0x4a, 0xd9,
+ 0xeb, 0x78, 0xd0, 0x43, 0x9d, 0x0e, 0xa6, 0x35, 0x00, 0x94,
+ 0x35, 0xa1, 0x6a, 0xfe, 0x5f, 0xcb, 0xd4, 0x40, 0xe1, 0x75,
+ 0xbe, 0x2a, 0x8b, 0x1f, 0xb5, 0x21, 0x80, 0x14, 0xdf, 0x4b,
+ 0xea, 0x7e, 0x61, 0xf5, 0x54, 0xc0, 0x0b, 0x9f, 0x3e, 0xaa,
+ 0x77, 0xe3, 0x42, 0xd6, 0x1d, 0x89, 0x28, 0xbc, 0xa3, 0x37,
+ 0x96, 0x02, 0xc9, 0x5d, 0xfc, 0x68, 0xc2, 0x56, 0xf7, 0x63,
+ 0xa8, 0x3c, 0x9d, 0x09, 0x16, 0x82, 0x23, 0xb7, 0x7c, 0xe8,
+ 0x49, 0xdd, 0xee, 0x7a, 0xdb, 0x4f, 0x84, 0x10, 0xb1, 0x25,
+ 0x3a, 0xae, 0x0f, 0x9b, 0x50, 0xc4, 0x65, 0xf1, 0x5b, 0xcf,
+ 0x6e, 0xfa, 0x31, 0xa5, 0x04, 0x90, 0x8f, 0x1b, 0xba, 0x2e,
+ 0xe5, 0x71, 0xd0, 0x44, 0x99, 0x0d, 0xac, 0x38, 0xf3, 0x67,
+ 0xc6, 0x52, 0x4d, 0xd9, 0x78, 0xec, 0x27, 0xb3, 0x12, 0x86,
+ 0x2c, 0xb8, 0x19, 0x8d, 0x46, 0xd2, 0x73, 0xe7, 0xf8, 0x6c,
+ 0xcd, 0x59, 0x92, 0x06, 0xa7, 0x33, 0xc1, 0x55, 0xf4, 0x60,
+ 0xab, 0x3f, 0x9e, 0x0a, 0x15, 0x81, 0x20, 0xb4, 0x7f, 0xeb,
+ 0x4a, 0xde, 0x74, 0xe0, 0x41, 0xd5, 0x1e, 0x8a, 0x2b, 0xbf,
+ 0xa0, 0x34, 0x95, 0x01, 0xca, 0x5e, 0xff, 0x6b, 0xb6, 0x22,
+ 0x83, 0x17, 0xdc, 0x48, 0xe9, 0x7d, 0x62, 0xf6, 0x57, 0xc3,
+ 0x08, 0x9c, 0x3d, 0xa9, 0x03, 0x97, 0x36, 0xa2, 0x69, 0xfd,
+ 0x5c, 0xc8, 0xd7, 0x43, 0xe2, 0x76, 0xbd, 0x29, 0x88, 0x1c,
+ 0x2f, 0xbb, 0x1a, 0x8e, 0x45, 0xd1, 0x70, 0xe4, 0xfb, 0x6f,
+ 0xce, 0x5a, 0x91, 0x05, 0xa4, 0x30, 0x9a, 0x0e, 0xaf, 0x3b,
+ 0xf0, 0x64, 0xc5, 0x51, 0x4e, 0xda, 0x7b, 0xef, 0x24, 0xb0,
+ 0x11, 0x85, 0x58, 0xcc, 0x6d, 0xf9, 0x32, 0xa6, 0x07, 0x93,
+ 0x8c, 0x18, 0xb9, 0x2d, 0xe6, 0x72, 0xd3, 0x47, 0xed, 0x79,
+ 0xd8, 0x4c, 0x87, 0x13, 0xb2, 0x26, 0x39, 0xad, 0x0c, 0x98,
+ 0x53, 0xc7, 0x66, 0xf2, 0x00, 0x95, 0x37, 0xa2, 0x6e, 0xfb,
+ 0x59, 0xcc, 0xdc, 0x49, 0xeb, 0x7e, 0xb2, 0x27, 0x85, 0x10,
+ 0xa5, 0x30, 0x92, 0x07, 0xcb, 0x5e, 0xfc, 0x69, 0x79, 0xec,
+ 0x4e, 0xdb, 0x17, 0x82, 0x20, 0xb5, 0x57, 0xc2, 0x60, 0xf5,
+ 0x39, 0xac, 0x0e, 0x9b, 0x8b, 0x1e, 0xbc, 0x29, 0xe5, 0x70,
+ 0xd2, 0x47, 0xf2, 0x67, 0xc5, 0x50, 0x9c, 0x09, 0xab, 0x3e,
+ 0x2e, 0xbb, 0x19, 0x8c, 0x40, 0xd5, 0x77, 0xe2, 0xae, 0x3b,
+ 0x99, 0x0c, 0xc0, 0x55, 0xf7, 0x62, 0x72, 0xe7, 0x45, 0xd0,
+ 0x1c, 0x89, 0x2b, 0xbe, 0x0b, 0x9e, 0x3c, 0xa9, 0x65, 0xf0,
+ 0x52, 0xc7, 0xd7, 0x42, 0xe0, 0x75, 0xb9, 0x2c, 0x8e, 0x1b,
+ 0xf9, 0x6c, 0xce, 0x5b, 0x97, 0x02, 0xa0, 0x35, 0x25, 0xb0,
+ 0x12, 0x87, 0x4b, 0xde, 0x7c, 0xe9, 0x5c, 0xc9, 0x6b, 0xfe,
+ 0x32, 0xa7, 0x05, 0x90, 0x80, 0x15, 0xb7, 0x22, 0xee, 0x7b,
+ 0xd9, 0x4c, 0x41, 0xd4, 0x76, 0xe3, 0x2f, 0xba, 0x18, 0x8d,
+ 0x9d, 0x08, 0xaa, 0x3f, 0xf3, 0x66, 0xc4, 0x51, 0xe4, 0x71,
+ 0xd3, 0x46, 0x8a, 0x1f, 0xbd, 0x28, 0x38, 0xad, 0x0f, 0x9a,
+ 0x56, 0xc3, 0x61, 0xf4, 0x16, 0x83, 0x21, 0xb4, 0x78, 0xed,
+ 0x4f, 0xda, 0xca, 0x5f, 0xfd, 0x68, 0xa4, 0x31, 0x93, 0x06,
+ 0xb3, 0x26, 0x84, 0x11, 0xdd, 0x48, 0xea, 0x7f, 0x6f, 0xfa,
+ 0x58, 0xcd, 0x01, 0x94, 0x36, 0xa3, 0xef, 0x7a, 0xd8, 0x4d,
+ 0x81, 0x14, 0xb6, 0x23, 0x33, 0xa6, 0x04, 0x91, 0x5d, 0xc8,
+ 0x6a, 0xff, 0x4a, 0xdf, 0x7d, 0xe8, 0x24, 0xb1, 0x13, 0x86,
+ 0x96, 0x03, 0xa1, 0x34, 0xf8, 0x6d, 0xcf, 0x5a, 0xb8, 0x2d,
+ 0x8f, 0x1a, 0xd6, 0x43, 0xe1, 0x74, 0x64, 0xf1, 0x53, 0xc6,
+ 0x0a, 0x9f, 0x3d, 0xa8, 0x1d, 0x88, 0x2a, 0xbf, 0x73, 0xe6,
+ 0x44, 0xd1, 0xc1, 0x54, 0xf6, 0x63, 0xaf, 0x3a, 0x98, 0x0d,
+ 0x00, 0x96, 0x31, 0xa7, 0x62, 0xf4, 0x53, 0xc5, 0xc4, 0x52,
+ 0xf5, 0x63, 0xa6, 0x30, 0x97, 0x01, 0x95, 0x03, 0xa4, 0x32,
+ 0xf7, 0x61, 0xc6, 0x50, 0x51, 0xc7, 0x60, 0xf6, 0x33, 0xa5,
+ 0x02, 0x94, 0x37, 0xa1, 0x06, 0x90, 0x55, 0xc3, 0x64, 0xf2,
+ 0xf3, 0x65, 0xc2, 0x54, 0x91, 0x07, 0xa0, 0x36, 0xa2, 0x34,
+ 0x93, 0x05, 0xc0, 0x56, 0xf1, 0x67, 0x66, 0xf0, 0x57, 0xc1,
+ 0x04, 0x92, 0x35, 0xa3, 0x6e, 0xf8, 0x5f, 0xc9, 0x0c, 0x9a,
+ 0x3d, 0xab, 0xaa, 0x3c, 0x9b, 0x0d, 0xc8, 0x5e, 0xf9, 0x6f,
+ 0xfb, 0x6d, 0xca, 0x5c, 0x99, 0x0f, 0xa8, 0x3e, 0x3f, 0xa9,
+ 0x0e, 0x98, 0x5d, 0xcb, 0x6c, 0xfa, 0x59, 0xcf, 0x68, 0xfe,
+ 0x3b, 0xad, 0x0a, 0x9c, 0x9d, 0x0b, 0xac, 0x3a, 0xff, 0x69,
+ 0xce, 0x58, 0xcc, 0x5a, 0xfd, 0x6b, 0xae, 0x38, 0x9f, 0x09,
+ 0x08, 0x9e, 0x39, 0xaf, 0x6a, 0xfc, 0x5b, 0xcd, 0xdc, 0x4a,
+ 0xed, 0x7b, 0xbe, 0x28, 0x8f, 0x19, 0x18, 0x8e, 0x29, 0xbf,
+ 0x7a, 0xec, 0x4b, 0xdd, 0x49, 0xdf, 0x78, 0xee, 0x2b, 0xbd,
+ 0x1a, 0x8c, 0x8d, 0x1b, 0xbc, 0x2a, 0xef, 0x79, 0xde, 0x48,
+ 0xeb, 0x7d, 0xda, 0x4c, 0x89, 0x1f, 0xb8, 0x2e, 0x2f, 0xb9,
+ 0x1e, 0x88, 0x4d, 0xdb, 0x7c, 0xea, 0x7e, 0xe8, 0x4f, 0xd9,
+ 0x1c, 0x8a, 0x2d, 0xbb, 0xba, 0x2c, 0x8b, 0x1d, 0xd8, 0x4e,
+ 0xe9, 0x7f, 0xb2, 0x24, 0x83, 0x15, 0xd0, 0x46, 0xe1, 0x77,
+ 0x76, 0xe0, 0x47, 0xd1, 0x14, 0x82, 0x25, 0xb3, 0x27, 0xb1,
+ 0x16, 0x80, 0x45, 0xd3, 0x74, 0xe2, 0xe3, 0x75, 0xd2, 0x44,
+ 0x81, 0x17, 0xb0, 0x26, 0x85, 0x13, 0xb4, 0x22, 0xe7, 0x71,
+ 0xd6, 0x40, 0x41, 0xd7, 0x70, 0xe6, 0x23, 0xb5, 0x12, 0x84,
+ 0x10, 0x86, 0x21, 0xb7, 0x72, 0xe4, 0x43, 0xd5, 0xd4, 0x42,
+ 0xe5, 0x73, 0xb6, 0x20, 0x87, 0x11, 0x00, 0x97, 0x33, 0xa4,
+ 0x66, 0xf1, 0x55, 0xc2, 0xcc, 0x5b, 0xff, 0x68, 0xaa, 0x3d,
+ 0x99, 0x0e, 0x85, 0x12, 0xb6, 0x21, 0xe3, 0x74, 0xd0, 0x47,
+ 0x49, 0xde, 0x7a, 0xed, 0x2f, 0xb8, 0x1c, 0x8b, 0x17, 0x80,
+ 0x24, 0xb3, 0x71, 0xe6, 0x42, 0xd5, 0xdb, 0x4c, 0xe8, 0x7f,
+ 0xbd, 0x2a, 0x8e, 0x19, 0x92, 0x05, 0xa1, 0x36, 0xf4, 0x63,
+ 0xc7, 0x50, 0x5e, 0xc9, 0x6d, 0xfa, 0x38, 0xaf, 0x0b, 0x9c,
+ 0x2e, 0xb9, 0x1d, 0x8a, 0x48, 0xdf, 0x7b, 0xec, 0xe2, 0x75,
+ 0xd1, 0x46, 0x84, 0x13, 0xb7, 0x20, 0xab, 0x3c, 0x98, 0x0f,
+ 0xcd, 0x5a, 0xfe, 0x69, 0x67, 0xf0, 0x54, 0xc3, 0x01, 0x96,
+ 0x32, 0xa5, 0x39, 0xae, 0x0a, 0x9d, 0x5f, 0xc8, 0x6c, 0xfb,
+ 0xf5, 0x62, 0xc6, 0x51, 0x93, 0x04, 0xa0, 0x37, 0xbc, 0x2b,
+ 0x8f, 0x18, 0xda, 0x4d, 0xe9, 0x7e, 0x70, 0xe7, 0x43, 0xd4,
+ 0x16, 0x81, 0x25, 0xb2, 0x5c, 0xcb, 0x6f, 0xf8, 0x3a, 0xad,
+ 0x09, 0x9e, 0x90, 0x07, 0xa3, 0x34, 0xf6, 0x61, 0xc5, 0x52,
+ 0xd9, 0x4e, 0xea, 0x7d, 0xbf, 0x28, 0x8c, 0x1b, 0x15, 0x82,
+ 0x26, 0xb1, 0x73, 0xe4, 0x40, 0xd7, 0x4b, 0xdc, 0x78, 0xef,
+ 0x2d, 0xba, 0x1e, 0x89, 0x87, 0x10, 0xb4, 0x23, 0xe1, 0x76,
+ 0xd2, 0x45, 0xce, 0x59, 0xfd, 0x6a, 0xa8, 0x3f, 0x9b, 0x0c,
+ 0x02, 0x95, 0x31, 0xa6, 0x64, 0xf3, 0x57, 0xc0, 0x72, 0xe5,
+ 0x41, 0xd6, 0x14, 0x83, 0x27, 0xb0, 0xbe, 0x29, 0x8d, 0x1a,
+ 0xd8, 0x4f, 0xeb, 0x7c, 0xf7, 0x60, 0xc4, 0x53, 0x91, 0x06,
+ 0xa2, 0x35, 0x3b, 0xac, 0x08, 0x9f, 0x5d, 0xca, 0x6e, 0xf9,
+ 0x65, 0xf2, 0x56, 0xc1, 0x03, 0x94, 0x30, 0xa7, 0xa9, 0x3e,
+ 0x9a, 0x0d, 0xcf, 0x58, 0xfc, 0x6b, 0xe0, 0x77, 0xd3, 0x44,
+ 0x86, 0x11, 0xb5, 0x22, 0x2c, 0xbb, 0x1f, 0x88, 0x4a, 0xdd,
+ 0x79, 0xee, 0x00, 0x98, 0x2d, 0xb5, 0x5a, 0xc2, 0x77, 0xef,
+ 0xb4, 0x2c, 0x99, 0x01, 0xee, 0x76, 0xc3, 0x5b, 0x75, 0xed,
+ 0x58, 0xc0, 0x2f, 0xb7, 0x02, 0x9a, 0xc1, 0x59, 0xec, 0x74,
+ 0x9b, 0x03, 0xb6, 0x2e, 0xea, 0x72, 0xc7, 0x5f, 0xb0, 0x28,
+ 0x9d, 0x05, 0x5e, 0xc6, 0x73, 0xeb, 0x04, 0x9c, 0x29, 0xb1,
+ 0x9f, 0x07, 0xb2, 0x2a, 0xc5, 0x5d, 0xe8, 0x70, 0x2b, 0xb3,
+ 0x06, 0x9e, 0x71, 0xe9, 0x5c, 0xc4, 0xc9, 0x51, 0xe4, 0x7c,
+ 0x93, 0x0b, 0xbe, 0x26, 0x7d, 0xe5, 0x50, 0xc8, 0x27, 0xbf,
+ 0x0a, 0x92, 0xbc, 0x24, 0x91, 0x09, 0xe6, 0x7e, 0xcb, 0x53,
+ 0x08, 0x90, 0x25, 0xbd, 0x52, 0xca, 0x7f, 0xe7, 0x23, 0xbb,
+ 0x0e, 0x96, 0x79, 0xe1, 0x54, 0xcc, 0x97, 0x0f, 0xba, 0x22,
+ 0xcd, 0x55, 0xe0, 0x78, 0x56, 0xce, 0x7b, 0xe3, 0x0c, 0x94,
+ 0x21, 0xb9, 0xe2, 0x7a, 0xcf, 0x57, 0xb8, 0x20, 0x95, 0x0d,
+ 0x8f, 0x17, 0xa2, 0x3a, 0xd5, 0x4d, 0xf8, 0x60, 0x3b, 0xa3,
+ 0x16, 0x8e, 0x61, 0xf9, 0x4c, 0xd4, 0xfa, 0x62, 0xd7, 0x4f,
+ 0xa0, 0x38, 0x8d, 0x15, 0x4e, 0xd6, 0x63, 0xfb, 0x14, 0x8c,
+ 0x39, 0xa1, 0x65, 0xfd, 0x48, 0xd0, 0x3f, 0xa7, 0x12, 0x8a,
+ 0xd1, 0x49, 0xfc, 0x64, 0x8b, 0x13, 0xa6, 0x3e, 0x10, 0x88,
+ 0x3d, 0xa5, 0x4a, 0xd2, 0x67, 0xff, 0xa4, 0x3c, 0x89, 0x11,
+ 0xfe, 0x66, 0xd3, 0x4b, 0x46, 0xde, 0x6b, 0xf3, 0x1c, 0x84,
+ 0x31, 0xa9, 0xf2, 0x6a, 0xdf, 0x47, 0xa8, 0x30, 0x85, 0x1d,
+ 0x33, 0xab, 0x1e, 0x86, 0x69, 0xf1, 0x44, 0xdc, 0x87, 0x1f,
+ 0xaa, 0x32, 0xdd, 0x45, 0xf0, 0x68, 0xac, 0x34, 0x81, 0x19,
+ 0xf6, 0x6e, 0xdb, 0x43, 0x18, 0x80, 0x35, 0xad, 0x42, 0xda,
+ 0x6f, 0xf7, 0xd9, 0x41, 0xf4, 0x6c, 0x83, 0x1b, 0xae, 0x36,
+ 0x6d, 0xf5, 0x40, 0xd8, 0x37, 0xaf, 0x1a, 0x82, 0x00, 0x99,
+ 0x2f, 0xb6, 0x5e, 0xc7, 0x71, 0xe8, 0xbc, 0x25, 0x93, 0x0a,
+ 0xe2, 0x7b, 0xcd, 0x54, 0x65, 0xfc, 0x4a, 0xd3, 0x3b, 0xa2,
+ 0x14, 0x8d, 0xd9, 0x40, 0xf6, 0x6f, 0x87, 0x1e, 0xa8, 0x31,
+ 0xca, 0x53, 0xe5, 0x7c, 0x94, 0x0d, 0xbb, 0x22, 0x76, 0xef,
+ 0x59, 0xc0, 0x28, 0xb1, 0x07, 0x9e, 0xaf, 0x36, 0x80, 0x19,
+ 0xf1, 0x68, 0xde, 0x47, 0x13, 0x8a, 0x3c, 0xa5, 0x4d, 0xd4,
+ 0x62, 0xfb, 0x89, 0x10, 0xa6, 0x3f, 0xd7, 0x4e, 0xf8, 0x61,
+ 0x35, 0xac, 0x1a, 0x83, 0x6b, 0xf2, 0x44, 0xdd, 0xec, 0x75,
+ 0xc3, 0x5a, 0xb2, 0x2b, 0x9d, 0x04, 0x50, 0xc9, 0x7f, 0xe6,
+ 0x0e, 0x97, 0x21, 0xb8, 0x43, 0xda, 0x6c, 0xf5, 0x1d, 0x84,
+ 0x32, 0xab, 0xff, 0x66, 0xd0, 0x49, 0xa1, 0x38, 0x8e, 0x17,
+ 0x26, 0xbf, 0x09, 0x90, 0x78, 0xe1, 0x57, 0xce, 0x9a, 0x03,
+ 0xb5, 0x2c, 0xc4, 0x5d, 0xeb, 0x72, 0x0f, 0x96, 0x20, 0xb9,
+ 0x51, 0xc8, 0x7e, 0xe7, 0xb3, 0x2a, 0x9c, 0x05, 0xed, 0x74,
+ 0xc2, 0x5b, 0x6a, 0xf3, 0x45, 0xdc, 0x34, 0xad, 0x1b, 0x82,
+ 0xd6, 0x4f, 0xf9, 0x60, 0x88, 0x11, 0xa7, 0x3e, 0xc5, 0x5c,
+ 0xea, 0x73, 0x9b, 0x02, 0xb4, 0x2d, 0x79, 0xe0, 0x56, 0xcf,
+ 0x27, 0xbe, 0x08, 0x91, 0xa0, 0x39, 0x8f, 0x16, 0xfe, 0x67,
+ 0xd1, 0x48, 0x1c, 0x85, 0x33, 0xaa, 0x42, 0xdb, 0x6d, 0xf4,
+ 0x86, 0x1f, 0xa9, 0x30, 0xd8, 0x41, 0xf7, 0x6e, 0x3a, 0xa3,
+ 0x15, 0x8c, 0x64, 0xfd, 0x4b, 0xd2, 0xe3, 0x7a, 0xcc, 0x55,
+ 0xbd, 0x24, 0x92, 0x0b, 0x5f, 0xc6, 0x70, 0xe9, 0x01, 0x98,
+ 0x2e, 0xb7, 0x4c, 0xd5, 0x63, 0xfa, 0x12, 0x8b, 0x3d, 0xa4,
+ 0xf0, 0x69, 0xdf, 0x46, 0xae, 0x37, 0x81, 0x18, 0x29, 0xb0,
+ 0x06, 0x9f, 0x77, 0xee, 0x58, 0xc1, 0x95, 0x0c, 0xba, 0x23,
+ 0xcb, 0x52, 0xe4, 0x7d, 0x00, 0x9a, 0x29, 0xb3, 0x52, 0xc8,
+ 0x7b, 0xe1, 0xa4, 0x3e, 0x8d, 0x17, 0xf6, 0x6c, 0xdf, 0x45,
+ 0x55, 0xcf, 0x7c, 0xe6, 0x07, 0x9d, 0x2e, 0xb4, 0xf1, 0x6b,
+ 0xd8, 0x42, 0xa3, 0x39, 0x8a, 0x10, 0xaa, 0x30, 0x83, 0x19,
+ 0xf8, 0x62, 0xd1, 0x4b, 0x0e, 0x94, 0x27, 0xbd, 0x5c, 0xc6,
+ 0x75, 0xef, 0xff, 0x65, 0xd6, 0x4c, 0xad, 0x37, 0x84, 0x1e,
+ 0x5b, 0xc1, 0x72, 0xe8, 0x09, 0x93, 0x20, 0xba, 0x49, 0xd3,
+ 0x60, 0xfa, 0x1b, 0x81, 0x32, 0xa8, 0xed, 0x77, 0xc4, 0x5e,
+ 0xbf, 0x25, 0x96, 0x0c, 0x1c, 0x86, 0x35, 0xaf, 0x4e, 0xd4,
+ 0x67, 0xfd, 0xb8, 0x22, 0x91, 0x0b, 0xea, 0x70, 0xc3, 0x59,
+ 0xe3, 0x79, 0xca, 0x50, 0xb1, 0x2b, 0x98, 0x02, 0x47, 0xdd,
+ 0x6e, 0xf4, 0x15, 0x8f, 0x3c, 0xa6, 0xb6, 0x2c, 0x9f, 0x05,
+ 0xe4, 0x7e, 0xcd, 0x57, 0x12, 0x88, 0x3b, 0xa1, 0x40, 0xda,
+ 0x69, 0xf3, 0x92, 0x08, 0xbb, 0x21, 0xc0, 0x5a, 0xe9, 0x73,
+ 0x36, 0xac, 0x1f, 0x85, 0x64, 0xfe, 0x4d, 0xd7, 0xc7, 0x5d,
+ 0xee, 0x74, 0x95, 0x0f, 0xbc, 0x26, 0x63, 0xf9, 0x4a, 0xd0,
+ 0x31, 0xab, 0x18, 0x82, 0x38, 0xa2, 0x11, 0x8b, 0x6a, 0xf0,
+ 0x43, 0xd9, 0x9c, 0x06, 0xb5, 0x2f, 0xce, 0x54, 0xe7, 0x7d,
+ 0x6d, 0xf7, 0x44, 0xde, 0x3f, 0xa5, 0x16, 0x8c, 0xc9, 0x53,
+ 0xe0, 0x7a, 0x9b, 0x01, 0xb2, 0x28, 0xdb, 0x41, 0xf2, 0x68,
+ 0x89, 0x13, 0xa0, 0x3a, 0x7f, 0xe5, 0x56, 0xcc, 0x2d, 0xb7,
+ 0x04, 0x9e, 0x8e, 0x14, 0xa7, 0x3d, 0xdc, 0x46, 0xf5, 0x6f,
+ 0x2a, 0xb0, 0x03, 0x99, 0x78, 0xe2, 0x51, 0xcb, 0x71, 0xeb,
+ 0x58, 0xc2, 0x23, 0xb9, 0x0a, 0x90, 0xd5, 0x4f, 0xfc, 0x66,
+ 0x87, 0x1d, 0xae, 0x34, 0x24, 0xbe, 0x0d, 0x97, 0x76, 0xec,
+ 0x5f, 0xc5, 0x80, 0x1a, 0xa9, 0x33, 0xd2, 0x48, 0xfb, 0x61,
+ 0x00, 0x9b, 0x2b, 0xb0, 0x56, 0xcd, 0x7d, 0xe6, 0xac, 0x37,
+ 0x87, 0x1c, 0xfa, 0x61, 0xd1, 0x4a, 0x45, 0xde, 0x6e, 0xf5,
+ 0x13, 0x88, 0x38, 0xa3, 0xe9, 0x72, 0xc2, 0x59, 0xbf, 0x24,
+ 0x94, 0x0f, 0x8a, 0x11, 0xa1, 0x3a, 0xdc, 0x47, 0xf7, 0x6c,
+ 0x26, 0xbd, 0x0d, 0x96, 0x70, 0xeb, 0x5b, 0xc0, 0xcf, 0x54,
+ 0xe4, 0x7f, 0x99, 0x02, 0xb2, 0x29, 0x63, 0xf8, 0x48, 0xd3,
+ 0x35, 0xae, 0x1e, 0x85, 0x09, 0x92, 0x22, 0xb9, 0x5f, 0xc4,
+ 0x74, 0xef, 0xa5, 0x3e, 0x8e, 0x15, 0xf3, 0x68, 0xd8, 0x43,
+ 0x4c, 0xd7, 0x67, 0xfc, 0x1a, 0x81, 0x31, 0xaa, 0xe0, 0x7b,
+ 0xcb, 0x50, 0xb6, 0x2d, 0x9d, 0x06, 0x83, 0x18, 0xa8, 0x33,
+ 0xd5, 0x4e, 0xfe, 0x65, 0x2f, 0xb4, 0x04, 0x9f, 0x79, 0xe2,
+ 0x52, 0xc9, 0xc6, 0x5d, 0xed, 0x76, 0x90, 0x0b, 0xbb, 0x20,
+ 0x6a, 0xf1, 0x41, 0xda, 0x3c, 0xa7, 0x17, 0x8c, 0x12, 0x89,
+ 0x39, 0xa2, 0x44, 0xdf, 0x6f, 0xf4, 0xbe, 0x25, 0x95, 0x0e,
+ 0xe8, 0x73, 0xc3, 0x58, 0x57, 0xcc, 0x7c, 0xe7, 0x01, 0x9a,
+ 0x2a, 0xb1, 0xfb, 0x60, 0xd0, 0x4b, 0xad, 0x36, 0x86, 0x1d,
+ 0x98, 0x03, 0xb3, 0x28, 0xce, 0x55, 0xe5, 0x7e, 0x34, 0xaf,
+ 0x1f, 0x84, 0x62, 0xf9, 0x49, 0xd2, 0xdd, 0x46, 0xf6, 0x6d,
+ 0x8b, 0x10, 0xa0, 0x3b, 0x71, 0xea, 0x5a, 0xc1, 0x27, 0xbc,
+ 0x0c, 0x97, 0x1b, 0x80, 0x30, 0xab, 0x4d, 0xd6, 0x66, 0xfd,
+ 0xb7, 0x2c, 0x9c, 0x07, 0xe1, 0x7a, 0xca, 0x51, 0x5e, 0xc5,
+ 0x75, 0xee, 0x08, 0x93, 0x23, 0xb8, 0xf2, 0x69, 0xd9, 0x42,
+ 0xa4, 0x3f, 0x8f, 0x14, 0x91, 0x0a, 0xba, 0x21, 0xc7, 0x5c,
+ 0xec, 0x77, 0x3d, 0xa6, 0x16, 0x8d, 0x6b, 0xf0, 0x40, 0xdb,
+ 0xd4, 0x4f, 0xff, 0x64, 0x82, 0x19, 0xa9, 0x32, 0x78, 0xe3,
+ 0x53, 0xc8, 0x2e, 0xb5, 0x05, 0x9e, 0x00, 0x9c, 0x25, 0xb9,
+ 0x4a, 0xd6, 0x6f, 0xf3, 0x94, 0x08, 0xb1, 0x2d, 0xde, 0x42,
+ 0xfb, 0x67, 0x35, 0xa9, 0x10, 0x8c, 0x7f, 0xe3, 0x5a, 0xc6,
+ 0xa1, 0x3d, 0x84, 0x18, 0xeb, 0x77, 0xce, 0x52, 0x6a, 0xf6,
+ 0x4f, 0xd3, 0x20, 0xbc, 0x05, 0x99, 0xfe, 0x62, 0xdb, 0x47,
+ 0xb4, 0x28, 0x91, 0x0d, 0x5f, 0xc3, 0x7a, 0xe6, 0x15, 0x89,
+ 0x30, 0xac, 0xcb, 0x57, 0xee, 0x72, 0x81, 0x1d, 0xa4, 0x38,
+ 0xd4, 0x48, 0xf1, 0x6d, 0x9e, 0x02, 0xbb, 0x27, 0x40, 0xdc,
+ 0x65, 0xf9, 0x0a, 0x96, 0x2f, 0xb3, 0xe1, 0x7d, 0xc4, 0x58,
+ 0xab, 0x37, 0x8e, 0x12, 0x75, 0xe9, 0x50, 0xcc, 0x3f, 0xa3,
+ 0x1a, 0x86, 0xbe, 0x22, 0x9b, 0x07, 0xf4, 0x68, 0xd1, 0x4d,
+ 0x2a, 0xb6, 0x0f, 0x93, 0x60, 0xfc, 0x45, 0xd9, 0x8b, 0x17,
+ 0xae, 0x32, 0xc1, 0x5d, 0xe4, 0x78, 0x1f, 0x83, 0x3a, 0xa6,
+ 0x55, 0xc9, 0x70, 0xec, 0xb5, 0x29, 0x90, 0x0c, 0xff, 0x63,
+ 0xda, 0x46, 0x21, 0xbd, 0x04, 0x98, 0x6b, 0xf7, 0x4e, 0xd2,
+ 0x80, 0x1c, 0xa5, 0x39, 0xca, 0x56, 0xef, 0x73, 0x14, 0x88,
+ 0x31, 0xad, 0x5e, 0xc2, 0x7b, 0xe7, 0xdf, 0x43, 0xfa, 0x66,
+ 0x95, 0x09, 0xb0, 0x2c, 0x4b, 0xd7, 0x6e, 0xf2, 0x01, 0x9d,
+ 0x24, 0xb8, 0xea, 0x76, 0xcf, 0x53, 0xa0, 0x3c, 0x85, 0x19,
+ 0x7e, 0xe2, 0x5b, 0xc7, 0x34, 0xa8, 0x11, 0x8d, 0x61, 0xfd,
+ 0x44, 0xd8, 0x2b, 0xb7, 0x0e, 0x92, 0xf5, 0x69, 0xd0, 0x4c,
+ 0xbf, 0x23, 0x9a, 0x06, 0x54, 0xc8, 0x71, 0xed, 0x1e, 0x82,
+ 0x3b, 0xa7, 0xc0, 0x5c, 0xe5, 0x79, 0x8a, 0x16, 0xaf, 0x33,
+ 0x0b, 0x97, 0x2e, 0xb2, 0x41, 0xdd, 0x64, 0xf8, 0x9f, 0x03,
+ 0xba, 0x26, 0xd5, 0x49, 0xf0, 0x6c, 0x3e, 0xa2, 0x1b, 0x87,
+ 0x74, 0xe8, 0x51, 0xcd, 0xaa, 0x36, 0x8f, 0x13, 0xe0, 0x7c,
+ 0xc5, 0x59, 0x00, 0x9d, 0x27, 0xba, 0x4e, 0xd3, 0x69, 0xf4,
+ 0x9c, 0x01, 0xbb, 0x26, 0xd2, 0x4f, 0xf5, 0x68, 0x25, 0xb8,
+ 0x02, 0x9f, 0x6b, 0xf6, 0x4c, 0xd1, 0xb9, 0x24, 0x9e, 0x03,
+ 0xf7, 0x6a, 0xd0, 0x4d, 0x4a, 0xd7, 0x6d, 0xf0, 0x04, 0x99,
+ 0x23, 0xbe, 0xd6, 0x4b, 0xf1, 0x6c, 0x98, 0x05, 0xbf, 0x22,
+ 0x6f, 0xf2, 0x48, 0xd5, 0x21, 0xbc, 0x06, 0x9b, 0xf3, 0x6e,
+ 0xd4, 0x49, 0xbd, 0x20, 0x9a, 0x07, 0x94, 0x09, 0xb3, 0x2e,
+ 0xda, 0x47, 0xfd, 0x60, 0x08, 0x95, 0x2f, 0xb2, 0x46, 0xdb,
+ 0x61, 0xfc, 0xb1, 0x2c, 0x96, 0x0b, 0xff, 0x62, 0xd8, 0x45,
+ 0x2d, 0xb0, 0x0a, 0x97, 0x63, 0xfe, 0x44, 0xd9, 0xde, 0x43,
+ 0xf9, 0x64, 0x90, 0x0d, 0xb7, 0x2a, 0x42, 0xdf, 0x65, 0xf8,
+ 0x0c, 0x91, 0x2b, 0xb6, 0xfb, 0x66, 0xdc, 0x41, 0xb5, 0x28,
+ 0x92, 0x0f, 0x67, 0xfa, 0x40, 0xdd, 0x29, 0xb4, 0x0e, 0x93,
+ 0x35, 0xa8, 0x12, 0x8f, 0x7b, 0xe6, 0x5c, 0xc1, 0xa9, 0x34,
+ 0x8e, 0x13, 0xe7, 0x7a, 0xc0, 0x5d, 0x10, 0x8d, 0x37, 0xaa,
+ 0x5e, 0xc3, 0x79, 0xe4, 0x8c, 0x11, 0xab, 0x36, 0xc2, 0x5f,
+ 0xe5, 0x78, 0x7f, 0xe2, 0x58, 0xc5, 0x31, 0xac, 0x16, 0x8b,
+ 0xe3, 0x7e, 0xc4, 0x59, 0xad, 0x30, 0x8a, 0x17, 0x5a, 0xc7,
+ 0x7d, 0xe0, 0x14, 0x89, 0x33, 0xae, 0xc6, 0x5b, 0xe1, 0x7c,
+ 0x88, 0x15, 0xaf, 0x32, 0xa1, 0x3c, 0x86, 0x1b, 0xef, 0x72,
+ 0xc8, 0x55, 0x3d, 0xa0, 0x1a, 0x87, 0x73, 0xee, 0x54, 0xc9,
+ 0x84, 0x19, 0xa3, 0x3e, 0xca, 0x57, 0xed, 0x70, 0x18, 0x85,
+ 0x3f, 0xa2, 0x56, 0xcb, 0x71, 0xec, 0xeb, 0x76, 0xcc, 0x51,
+ 0xa5, 0x38, 0x82, 0x1f, 0x77, 0xea, 0x50, 0xcd, 0x39, 0xa4,
+ 0x1e, 0x83, 0xce, 0x53, 0xe9, 0x74, 0x80, 0x1d, 0xa7, 0x3a,
+ 0x52, 0xcf, 0x75, 0xe8, 0x1c, 0x81, 0x3b, 0xa6, 0x00, 0x9e,
+ 0x21, 0xbf, 0x42, 0xdc, 0x63, 0xfd, 0x84, 0x1a, 0xa5, 0x3b,
+ 0xc6, 0x58, 0xe7, 0x79, 0x15, 0x8b, 0x34, 0xaa, 0x57, 0xc9,
+ 0x76, 0xe8, 0x91, 0x0f, 0xb0, 0x2e, 0xd3, 0x4d, 0xf2, 0x6c,
+ 0x2a, 0xb4, 0x0b, 0x95, 0x68, 0xf6, 0x49, 0xd7, 0xae, 0x30,
+ 0x8f, 0x11, 0xec, 0x72, 0xcd, 0x53, 0x3f, 0xa1, 0x1e, 0x80,
+ 0x7d, 0xe3, 0x5c, 0xc2, 0xbb, 0x25, 0x9a, 0x04, 0xf9, 0x67,
+ 0xd8, 0x46, 0x54, 0xca, 0x75, 0xeb, 0x16, 0x88, 0x37, 0xa9,
+ 0xd0, 0x4e, 0xf1, 0x6f, 0x92, 0x0c, 0xb3, 0x2d, 0x41, 0xdf,
+ 0x60, 0xfe, 0x03, 0x9d, 0x22, 0xbc, 0xc5, 0x5b, 0xe4, 0x7a,
+ 0x87, 0x19, 0xa6, 0x38, 0x7e, 0xe0, 0x5f, 0xc1, 0x3c, 0xa2,
+ 0x1d, 0x83, 0xfa, 0x64, 0xdb, 0x45, 0xb8, 0x26, 0x99, 0x07,
+ 0x6b, 0xf5, 0x4a, 0xd4, 0x29, 0xb7, 0x08, 0x96, 0xef, 0x71,
+ 0xce, 0x50, 0xad, 0x33, 0x8c, 0x12, 0xa8, 0x36, 0x89, 0x17,
+ 0xea, 0x74, 0xcb, 0x55, 0x2c, 0xb2, 0x0d, 0x93, 0x6e, 0xf0,
+ 0x4f, 0xd1, 0xbd, 0x23, 0x9c, 0x02, 0xff, 0x61, 0xde, 0x40,
+ 0x39, 0xa7, 0x18, 0x86, 0x7b, 0xe5, 0x5a, 0xc4, 0x82, 0x1c,
+ 0xa3, 0x3d, 0xc0, 0x5e, 0xe1, 0x7f, 0x06, 0x98, 0x27, 0xb9,
+ 0x44, 0xda, 0x65, 0xfb, 0x97, 0x09, 0xb6, 0x28, 0xd5, 0x4b,
+ 0xf4, 0x6a, 0x13, 0x8d, 0x32, 0xac, 0x51, 0xcf, 0x70, 0xee,
+ 0xfc, 0x62, 0xdd, 0x43, 0xbe, 0x20, 0x9f, 0x01, 0x78, 0xe6,
+ 0x59, 0xc7, 0x3a, 0xa4, 0x1b, 0x85, 0xe9, 0x77, 0xc8, 0x56,
+ 0xab, 0x35, 0x8a, 0x14, 0x6d, 0xf3, 0x4c, 0xd2, 0x2f, 0xb1,
+ 0x0e, 0x90, 0xd6, 0x48, 0xf7, 0x69, 0x94, 0x0a, 0xb5, 0x2b,
+ 0x52, 0xcc, 0x73, 0xed, 0x10, 0x8e, 0x31, 0xaf, 0xc3, 0x5d,
+ 0xe2, 0x7c, 0x81, 0x1f, 0xa0, 0x3e, 0x47, 0xd9, 0x66, 0xf8,
+ 0x05, 0x9b, 0x24, 0xba, 0x00, 0x9f, 0x23, 0xbc, 0x46, 0xd9,
+ 0x65, 0xfa, 0x8c, 0x13, 0xaf, 0x30, 0xca, 0x55, 0xe9, 0x76,
+ 0x05, 0x9a, 0x26, 0xb9, 0x43, 0xdc, 0x60, 0xff, 0x89, 0x16,
+ 0xaa, 0x35, 0xcf, 0x50, 0xec, 0x73, 0x0a, 0x95, 0x29, 0xb6,
+ 0x4c, 0xd3, 0x6f, 0xf0, 0x86, 0x19, 0xa5, 0x3a, 0xc0, 0x5f,
+ 0xe3, 0x7c, 0x0f, 0x90, 0x2c, 0xb3, 0x49, 0xd6, 0x6a, 0xf5,
+ 0x83, 0x1c, 0xa0, 0x3f, 0xc5, 0x5a, 0xe6, 0x79, 0x14, 0x8b,
+ 0x37, 0xa8, 0x52, 0xcd, 0x71, 0xee, 0x98, 0x07, 0xbb, 0x24,
+ 0xde, 0x41, 0xfd, 0x62, 0x11, 0x8e, 0x32, 0xad, 0x57, 0xc8,
+ 0x74, 0xeb, 0x9d, 0x02, 0xbe, 0x21, 0xdb, 0x44, 0xf8, 0x67,
+ 0x1e, 0x81, 0x3d, 0xa2, 0x58, 0xc7, 0x7b, 0xe4, 0x92, 0x0d,
+ 0xb1, 0x2e, 0xd4, 0x4b, 0xf7, 0x68, 0x1b, 0x84, 0x38, 0xa7,
+ 0x5d, 0xc2, 0x7e, 0xe1, 0x97, 0x08, 0xb4, 0x2b, 0xd1, 0x4e,
+ 0xf2, 0x6d, 0x28, 0xb7, 0x0b, 0x94, 0x6e, 0xf1, 0x4d, 0xd2,
+ 0xa4, 0x3b, 0x87, 0x18, 0xe2, 0x7d, 0xc1, 0x5e, 0x2d, 0xb2,
+ 0x0e, 0x91, 0x6b, 0xf4, 0x48, 0xd7, 0xa1, 0x3e, 0x82, 0x1d,
+ 0xe7, 0x78, 0xc4, 0x5b, 0x22, 0xbd, 0x01, 0x9e, 0x64, 0xfb,
+ 0x47, 0xd8, 0xae, 0x31, 0x8d, 0x12, 0xe8, 0x77, 0xcb, 0x54,
+ 0x27, 0xb8, 0x04, 0x9b, 0x61, 0xfe, 0x42, 0xdd, 0xab, 0x34,
+ 0x88, 0x17, 0xed, 0x72, 0xce, 0x51, 0x3c, 0xa3, 0x1f, 0x80,
+ 0x7a, 0xe5, 0x59, 0xc6, 0xb0, 0x2f, 0x93, 0x0c, 0xf6, 0x69,
+ 0xd5, 0x4a, 0x39, 0xa6, 0x1a, 0x85, 0x7f, 0xe0, 0x5c, 0xc3,
+ 0xb5, 0x2a, 0x96, 0x09, 0xf3, 0x6c, 0xd0, 0x4f, 0x36, 0xa9,
+ 0x15, 0x8a, 0x70, 0xef, 0x53, 0xcc, 0xba, 0x25, 0x99, 0x06,
+ 0xfc, 0x63, 0xdf, 0x40, 0x33, 0xac, 0x10, 0x8f, 0x75, 0xea,
+ 0x56, 0xc9, 0xbf, 0x20, 0x9c, 0x03, 0xf9, 0x66, 0xda, 0x45,
+ 0x00, 0xa0, 0x5d, 0xfd, 0xba, 0x1a, 0xe7, 0x47, 0x69, 0xc9,
+ 0x34, 0x94, 0xd3, 0x73, 0x8e, 0x2e, 0xd2, 0x72, 0x8f, 0x2f,
+ 0x68, 0xc8, 0x35, 0x95, 0xbb, 0x1b, 0xe6, 0x46, 0x01, 0xa1,
+ 0x5c, 0xfc, 0xb9, 0x19, 0xe4, 0x44, 0x03, 0xa3, 0x5e, 0xfe,
+ 0xd0, 0x70, 0x8d, 0x2d, 0x6a, 0xca, 0x37, 0x97, 0x6b, 0xcb,
+ 0x36, 0x96, 0xd1, 0x71, 0x8c, 0x2c, 0x02, 0xa2, 0x5f, 0xff,
+ 0xb8, 0x18, 0xe5, 0x45, 0x6f, 0xcf, 0x32, 0x92, 0xd5, 0x75,
+ 0x88, 0x28, 0x06, 0xa6, 0x5b, 0xfb, 0xbc, 0x1c, 0xe1, 0x41,
+ 0xbd, 0x1d, 0xe0, 0x40, 0x07, 0xa7, 0x5a, 0xfa, 0xd4, 0x74,
+ 0x89, 0x29, 0x6e, 0xce, 0x33, 0x93, 0xd6, 0x76, 0x8b, 0x2b,
+ 0x6c, 0xcc, 0x31, 0x91, 0xbf, 0x1f, 0xe2, 0x42, 0x05, 0xa5,
+ 0x58, 0xf8, 0x04, 0xa4, 0x59, 0xf9, 0xbe, 0x1e, 0xe3, 0x43,
+ 0x6d, 0xcd, 0x30, 0x90, 0xd7, 0x77, 0x8a, 0x2a, 0xde, 0x7e,
+ 0x83, 0x23, 0x64, 0xc4, 0x39, 0x99, 0xb7, 0x17, 0xea, 0x4a,
+ 0x0d, 0xad, 0x50, 0xf0, 0x0c, 0xac, 0x51, 0xf1, 0xb6, 0x16,
+ 0xeb, 0x4b, 0x65, 0xc5, 0x38, 0x98, 0xdf, 0x7f, 0x82, 0x22,
+ 0x67, 0xc7, 0x3a, 0x9a, 0xdd, 0x7d, 0x80, 0x20, 0x0e, 0xae,
+ 0x53, 0xf3, 0xb4, 0x14, 0xe9, 0x49, 0xb5, 0x15, 0xe8, 0x48,
+ 0x0f, 0xaf, 0x52, 0xf2, 0xdc, 0x7c, 0x81, 0x21, 0x66, 0xc6,
+ 0x3b, 0x9b, 0xb1, 0x11, 0xec, 0x4c, 0x0b, 0xab, 0x56, 0xf6,
+ 0xd8, 0x78, 0x85, 0x25, 0x62, 0xc2, 0x3f, 0x9f, 0x63, 0xc3,
+ 0x3e, 0x9e, 0xd9, 0x79, 0x84, 0x24, 0x0a, 0xaa, 0x57, 0xf7,
+ 0xb0, 0x10, 0xed, 0x4d, 0x08, 0xa8, 0x55, 0xf5, 0xb2, 0x12,
+ 0xef, 0x4f, 0x61, 0xc1, 0x3c, 0x9c, 0xdb, 0x7b, 0x86, 0x26,
+ 0xda, 0x7a, 0x87, 0x27, 0x60, 0xc0, 0x3d, 0x9d, 0xb3, 0x13,
+ 0xee, 0x4e, 0x09, 0xa9, 0x54, 0xf4, 0x00, 0xa1, 0x5f, 0xfe,
+ 0xbe, 0x1f, 0xe1, 0x40, 0x61, 0xc0, 0x3e, 0x9f, 0xdf, 0x7e,
+ 0x80, 0x21, 0xc2, 0x63, 0x9d, 0x3c, 0x7c, 0xdd, 0x23, 0x82,
+ 0xa3, 0x02, 0xfc, 0x5d, 0x1d, 0xbc, 0x42, 0xe3, 0x99, 0x38,
+ 0xc6, 0x67, 0x27, 0x86, 0x78, 0xd9, 0xf8, 0x59, 0xa7, 0x06,
+ 0x46, 0xe7, 0x19, 0xb8, 0x5b, 0xfa, 0x04, 0xa5, 0xe5, 0x44,
+ 0xba, 0x1b, 0x3a, 0x9b, 0x65, 0xc4, 0x84, 0x25, 0xdb, 0x7a,
+ 0x2f, 0x8e, 0x70, 0xd1, 0x91, 0x30, 0xce, 0x6f, 0x4e, 0xef,
+ 0x11, 0xb0, 0xf0, 0x51, 0xaf, 0x0e, 0xed, 0x4c, 0xb2, 0x13,
+ 0x53, 0xf2, 0x0c, 0xad, 0x8c, 0x2d, 0xd3, 0x72, 0x32, 0x93,
+ 0x6d, 0xcc, 0xb6, 0x17, 0xe9, 0x48, 0x08, 0xa9, 0x57, 0xf6,
+ 0xd7, 0x76, 0x88, 0x29, 0x69, 0xc8, 0x36, 0x97, 0x74, 0xd5,
+ 0x2b, 0x8a, 0xca, 0x6b, 0x95, 0x34, 0x15, 0xb4, 0x4a, 0xeb,
+ 0xab, 0x0a, 0xf4, 0x55, 0x5e, 0xff, 0x01, 0xa0, 0xe0, 0x41,
+ 0xbf, 0x1e, 0x3f, 0x9e, 0x60, 0xc1, 0x81, 0x20, 0xde, 0x7f,
+ 0x9c, 0x3d, 0xc3, 0x62, 0x22, 0x83, 0x7d, 0xdc, 0xfd, 0x5c,
+ 0xa2, 0x03, 0x43, 0xe2, 0x1c, 0xbd, 0xc7, 0x66, 0x98, 0x39,
+ 0x79, 0xd8, 0x26, 0x87, 0xa6, 0x07, 0xf9, 0x58, 0x18, 0xb9,
+ 0x47, 0xe6, 0x05, 0xa4, 0x5a, 0xfb, 0xbb, 0x1a, 0xe4, 0x45,
+ 0x64, 0xc5, 0x3b, 0x9a, 0xda, 0x7b, 0x85, 0x24, 0x71, 0xd0,
+ 0x2e, 0x8f, 0xcf, 0x6e, 0x90, 0x31, 0x10, 0xb1, 0x4f, 0xee,
+ 0xae, 0x0f, 0xf1, 0x50, 0xb3, 0x12, 0xec, 0x4d, 0x0d, 0xac,
+ 0x52, 0xf3, 0xd2, 0x73, 0x8d, 0x2c, 0x6c, 0xcd, 0x33, 0x92,
+ 0xe8, 0x49, 0xb7, 0x16, 0x56, 0xf7, 0x09, 0xa8, 0x89, 0x28,
+ 0xd6, 0x77, 0x37, 0x96, 0x68, 0xc9, 0x2a, 0x8b, 0x75, 0xd4,
+ 0x94, 0x35, 0xcb, 0x6a, 0x4b, 0xea, 0x14, 0xb5, 0xf5, 0x54,
+ 0xaa, 0x0b, 0x00, 0xa2, 0x59, 0xfb, 0xb2, 0x10, 0xeb, 0x49,
+ 0x79, 0xdb, 0x20, 0x82, 0xcb, 0x69, 0x92, 0x30, 0xf2, 0x50,
+ 0xab, 0x09, 0x40, 0xe2, 0x19, 0xbb, 0x8b, 0x29, 0xd2, 0x70,
+ 0x39, 0x9b, 0x60, 0xc2, 0xf9, 0x5b, 0xa0, 0x02, 0x4b, 0xe9,
+ 0x12, 0xb0, 0x80, 0x22, 0xd9, 0x7b, 0x32, 0x90, 0x6b, 0xc9,
+ 0x0b, 0xa9, 0x52, 0xf0, 0xb9, 0x1b, 0xe0, 0x42, 0x72, 0xd0,
+ 0x2b, 0x89, 0xc0, 0x62, 0x99, 0x3b, 0xef, 0x4d, 0xb6, 0x14,
+ 0x5d, 0xff, 0x04, 0xa6, 0x96, 0x34, 0xcf, 0x6d, 0x24, 0x86,
+ 0x7d, 0xdf, 0x1d, 0xbf, 0x44, 0xe6, 0xaf, 0x0d, 0xf6, 0x54,
+ 0x64, 0xc6, 0x3d, 0x9f, 0xd6, 0x74, 0x8f, 0x2d, 0x16, 0xb4,
+ 0x4f, 0xed, 0xa4, 0x06, 0xfd, 0x5f, 0x6f, 0xcd, 0x36, 0x94,
+ 0xdd, 0x7f, 0x84, 0x26, 0xe4, 0x46, 0xbd, 0x1f, 0x56, 0xf4,
+ 0x0f, 0xad, 0x9d, 0x3f, 0xc4, 0x66, 0x2f, 0x8d, 0x76, 0xd4,
+ 0xc3, 0x61, 0x9a, 0x38, 0x71, 0xd3, 0x28, 0x8a, 0xba, 0x18,
+ 0xe3, 0x41, 0x08, 0xaa, 0x51, 0xf3, 0x31, 0x93, 0x68, 0xca,
+ 0x83, 0x21, 0xda, 0x78, 0x48, 0xea, 0x11, 0xb3, 0xfa, 0x58,
+ 0xa3, 0x01, 0x3a, 0x98, 0x63, 0xc1, 0x88, 0x2a, 0xd1, 0x73,
+ 0x43, 0xe1, 0x1a, 0xb8, 0xf1, 0x53, 0xa8, 0x0a, 0xc8, 0x6a,
+ 0x91, 0x33, 0x7a, 0xd8, 0x23, 0x81, 0xb1, 0x13, 0xe8, 0x4a,
+ 0x03, 0xa1, 0x5a, 0xf8, 0x2c, 0x8e, 0x75, 0xd7, 0x9e, 0x3c,
+ 0xc7, 0x65, 0x55, 0xf7, 0x0c, 0xae, 0xe7, 0x45, 0xbe, 0x1c,
+ 0xde, 0x7c, 0x87, 0x25, 0x6c, 0xce, 0x35, 0x97, 0xa7, 0x05,
+ 0xfe, 0x5c, 0x15, 0xb7, 0x4c, 0xee, 0xd5, 0x77, 0x8c, 0x2e,
+ 0x67, 0xc5, 0x3e, 0x9c, 0xac, 0x0e, 0xf5, 0x57, 0x1e, 0xbc,
+ 0x47, 0xe5, 0x27, 0x85, 0x7e, 0xdc, 0x95, 0x37, 0xcc, 0x6e,
+ 0x5e, 0xfc, 0x07, 0xa5, 0xec, 0x4e, 0xb5, 0x17, 0x00, 0xa3,
+ 0x5b, 0xf8, 0xb6, 0x15, 0xed, 0x4e, 0x71, 0xd2, 0x2a, 0x89,
+ 0xc7, 0x64, 0x9c, 0x3f, 0xe2, 0x41, 0xb9, 0x1a, 0x54, 0xf7,
+ 0x0f, 0xac, 0x93, 0x30, 0xc8, 0x6b, 0x25, 0x86, 0x7e, 0xdd,
+ 0xd9, 0x7a, 0x82, 0x21, 0x6f, 0xcc, 0x34, 0x97, 0xa8, 0x0b,
+ 0xf3, 0x50, 0x1e, 0xbd, 0x45, 0xe6, 0x3b, 0x98, 0x60, 0xc3,
+ 0x8d, 0x2e, 0xd6, 0x75, 0x4a, 0xe9, 0x11, 0xb2, 0xfc, 0x5f,
+ 0xa7, 0x04, 0xaf, 0x0c, 0xf4, 0x57, 0x19, 0xba, 0x42, 0xe1,
+ 0xde, 0x7d, 0x85, 0x26, 0x68, 0xcb, 0x33, 0x90, 0x4d, 0xee,
+ 0x16, 0xb5, 0xfb, 0x58, 0xa0, 0x03, 0x3c, 0x9f, 0x67, 0xc4,
+ 0x8a, 0x29, 0xd1, 0x72, 0x76, 0xd5, 0x2d, 0x8e, 0xc0, 0x63,
+ 0x9b, 0x38, 0x07, 0xa4, 0x5c, 0xff, 0xb1, 0x12, 0xea, 0x49,
+ 0x94, 0x37, 0xcf, 0x6c, 0x22, 0x81, 0x79, 0xda, 0xe5, 0x46,
+ 0xbe, 0x1d, 0x53, 0xf0, 0x08, 0xab, 0x43, 0xe0, 0x18, 0xbb,
+ 0xf5, 0x56, 0xae, 0x0d, 0x32, 0x91, 0x69, 0xca, 0x84, 0x27,
+ 0xdf, 0x7c, 0xa1, 0x02, 0xfa, 0x59, 0x17, 0xb4, 0x4c, 0xef,
+ 0xd0, 0x73, 0x8b, 0x28, 0x66, 0xc5, 0x3d, 0x9e, 0x9a, 0x39,
+ 0xc1, 0x62, 0x2c, 0x8f, 0x77, 0xd4, 0xeb, 0x48, 0xb0, 0x13,
+ 0x5d, 0xfe, 0x06, 0xa5, 0x78, 0xdb, 0x23, 0x80, 0xce, 0x6d,
+ 0x95, 0x36, 0x09, 0xaa, 0x52, 0xf1, 0xbf, 0x1c, 0xe4, 0x47,
+ 0xec, 0x4f, 0xb7, 0x14, 0x5a, 0xf9, 0x01, 0xa2, 0x9d, 0x3e,
+ 0xc6, 0x65, 0x2b, 0x88, 0x70, 0xd3, 0x0e, 0xad, 0x55, 0xf6,
+ 0xb8, 0x1b, 0xe3, 0x40, 0x7f, 0xdc, 0x24, 0x87, 0xc9, 0x6a,
+ 0x92, 0x31, 0x35, 0x96, 0x6e, 0xcd, 0x83, 0x20, 0xd8, 0x7b,
+ 0x44, 0xe7, 0x1f, 0xbc, 0xf2, 0x51, 0xa9, 0x0a, 0xd7, 0x74,
+ 0x8c, 0x2f, 0x61, 0xc2, 0x3a, 0x99, 0xa6, 0x05, 0xfd, 0x5e,
+ 0x10, 0xb3, 0x4b, 0xe8, 0x00, 0xa4, 0x55, 0xf1, 0xaa, 0x0e,
+ 0xff, 0x5b, 0x49, 0xed, 0x1c, 0xb8, 0xe3, 0x47, 0xb6, 0x12,
+ 0x92, 0x36, 0xc7, 0x63, 0x38, 0x9c, 0x6d, 0xc9, 0xdb, 0x7f,
+ 0x8e, 0x2a, 0x71, 0xd5, 0x24, 0x80, 0x39, 0x9d, 0x6c, 0xc8,
+ 0x93, 0x37, 0xc6, 0x62, 0x70, 0xd4, 0x25, 0x81, 0xda, 0x7e,
+ 0x8f, 0x2b, 0xab, 0x0f, 0xfe, 0x5a, 0x01, 0xa5, 0x54, 0xf0,
+ 0xe2, 0x46, 0xb7, 0x13, 0x48, 0xec, 0x1d, 0xb9, 0x72, 0xd6,
+ 0x27, 0x83, 0xd8, 0x7c, 0x8d, 0x29, 0x3b, 0x9f, 0x6e, 0xca,
+ 0x91, 0x35, 0xc4, 0x60, 0xe0, 0x44, 0xb5, 0x11, 0x4a, 0xee,
+ 0x1f, 0xbb, 0xa9, 0x0d, 0xfc, 0x58, 0x03, 0xa7, 0x56, 0xf2,
+ 0x4b, 0xef, 0x1e, 0xba, 0xe1, 0x45, 0xb4, 0x10, 0x02, 0xa6,
+ 0x57, 0xf3, 0xa8, 0x0c, 0xfd, 0x59, 0xd9, 0x7d, 0x8c, 0x28,
+ 0x73, 0xd7, 0x26, 0x82, 0x90, 0x34, 0xc5, 0x61, 0x3a, 0x9e,
+ 0x6f, 0xcb, 0xe4, 0x40, 0xb1, 0x15, 0x4e, 0xea, 0x1b, 0xbf,
+ 0xad, 0x09, 0xf8, 0x5c, 0x07, 0xa3, 0x52, 0xf6, 0x76, 0xd2,
+ 0x23, 0x87, 0xdc, 0x78, 0x89, 0x2d, 0x3f, 0x9b, 0x6a, 0xce,
+ 0x95, 0x31, 0xc0, 0x64, 0xdd, 0x79, 0x88, 0x2c, 0x77, 0xd3,
+ 0x22, 0x86, 0x94, 0x30, 0xc1, 0x65, 0x3e, 0x9a, 0x6b, 0xcf,
+ 0x4f, 0xeb, 0x1a, 0xbe, 0xe5, 0x41, 0xb0, 0x14, 0x06, 0xa2,
+ 0x53, 0xf7, 0xac, 0x08, 0xf9, 0x5d, 0x96, 0x32, 0xc3, 0x67,
+ 0x3c, 0x98, 0x69, 0xcd, 0xdf, 0x7b, 0x8a, 0x2e, 0x75, 0xd1,
+ 0x20, 0x84, 0x04, 0xa0, 0x51, 0xf5, 0xae, 0x0a, 0xfb, 0x5f,
+ 0x4d, 0xe9, 0x18, 0xbc, 0xe7, 0x43, 0xb2, 0x16, 0xaf, 0x0b,
+ 0xfa, 0x5e, 0x05, 0xa1, 0x50, 0xf4, 0xe6, 0x42, 0xb3, 0x17,
+ 0x4c, 0xe8, 0x19, 0xbd, 0x3d, 0x99, 0x68, 0xcc, 0x97, 0x33,
+ 0xc2, 0x66, 0x74, 0xd0, 0x21, 0x85, 0xde, 0x7a, 0x8b, 0x2f,
+ 0x00, 0xa5, 0x57, 0xf2, 0xae, 0x0b, 0xf9, 0x5c, 0x41, 0xe4,
+ 0x16, 0xb3, 0xef, 0x4a, 0xb8, 0x1d, 0x82, 0x27, 0xd5, 0x70,
+ 0x2c, 0x89, 0x7b, 0xde, 0xc3, 0x66, 0x94, 0x31, 0x6d, 0xc8,
+ 0x3a, 0x9f, 0x19, 0xbc, 0x4e, 0xeb, 0xb7, 0x12, 0xe0, 0x45,
+ 0x58, 0xfd, 0x0f, 0xaa, 0xf6, 0x53, 0xa1, 0x04, 0x9b, 0x3e,
+ 0xcc, 0x69, 0x35, 0x90, 0x62, 0xc7, 0xda, 0x7f, 0x8d, 0x28,
+ 0x74, 0xd1, 0x23, 0x86, 0x32, 0x97, 0x65, 0xc0, 0x9c, 0x39,
+ 0xcb, 0x6e, 0x73, 0xd6, 0x24, 0x81, 0xdd, 0x78, 0x8a, 0x2f,
+ 0xb0, 0x15, 0xe7, 0x42, 0x1e, 0xbb, 0x49, 0xec, 0xf1, 0x54,
+ 0xa6, 0x03, 0x5f, 0xfa, 0x08, 0xad, 0x2b, 0x8e, 0x7c, 0xd9,
+ 0x85, 0x20, 0xd2, 0x77, 0x6a, 0xcf, 0x3d, 0x98, 0xc4, 0x61,
+ 0x93, 0x36, 0xa9, 0x0c, 0xfe, 0x5b, 0x07, 0xa2, 0x50, 0xf5,
+ 0xe8, 0x4d, 0xbf, 0x1a, 0x46, 0xe3, 0x11, 0xb4, 0x64, 0xc1,
+ 0x33, 0x96, 0xca, 0x6f, 0x9d, 0x38, 0x25, 0x80, 0x72, 0xd7,
+ 0x8b, 0x2e, 0xdc, 0x79, 0xe6, 0x43, 0xb1, 0x14, 0x48, 0xed,
+ 0x1f, 0xba, 0xa7, 0x02, 0xf0, 0x55, 0x09, 0xac, 0x5e, 0xfb,
+ 0x7d, 0xd8, 0x2a, 0x8f, 0xd3, 0x76, 0x84, 0x21, 0x3c, 0x99,
+ 0x6b, 0xce, 0x92, 0x37, 0xc5, 0x60, 0xff, 0x5a, 0xa8, 0x0d,
+ 0x51, 0xf4, 0x06, 0xa3, 0xbe, 0x1b, 0xe9, 0x4c, 0x10, 0xb5,
+ 0x47, 0xe2, 0x56, 0xf3, 0x01, 0xa4, 0xf8, 0x5d, 0xaf, 0x0a,
+ 0x17, 0xb2, 0x40, 0xe5, 0xb9, 0x1c, 0xee, 0x4b, 0xd4, 0x71,
+ 0x83, 0x26, 0x7a, 0xdf, 0x2d, 0x88, 0x95, 0x30, 0xc2, 0x67,
+ 0x3b, 0x9e, 0x6c, 0xc9, 0x4f, 0xea, 0x18, 0xbd, 0xe1, 0x44,
+ 0xb6, 0x13, 0x0e, 0xab, 0x59, 0xfc, 0xa0, 0x05, 0xf7, 0x52,
+ 0xcd, 0x68, 0x9a, 0x3f, 0x63, 0xc6, 0x34, 0x91, 0x8c, 0x29,
+ 0xdb, 0x7e, 0x22, 0x87, 0x75, 0xd0, 0x00, 0xa6, 0x51, 0xf7,
+ 0xa2, 0x04, 0xf3, 0x55, 0x59, 0xff, 0x08, 0xae, 0xfb, 0x5d,
+ 0xaa, 0x0c, 0xb2, 0x14, 0xe3, 0x45, 0x10, 0xb6, 0x41, 0xe7,
+ 0xeb, 0x4d, 0xba, 0x1c, 0x49, 0xef, 0x18, 0xbe, 0x79, 0xdf,
+ 0x28, 0x8e, 0xdb, 0x7d, 0x8a, 0x2c, 0x20, 0x86, 0x71, 0xd7,
+ 0x82, 0x24, 0xd3, 0x75, 0xcb, 0x6d, 0x9a, 0x3c, 0x69, 0xcf,
+ 0x38, 0x9e, 0x92, 0x34, 0xc3, 0x65, 0x30, 0x96, 0x61, 0xc7,
+ 0xf2, 0x54, 0xa3, 0x05, 0x50, 0xf6, 0x01, 0xa7, 0xab, 0x0d,
+ 0xfa, 0x5c, 0x09, 0xaf, 0x58, 0xfe, 0x40, 0xe6, 0x11, 0xb7,
+ 0xe2, 0x44, 0xb3, 0x15, 0x19, 0xbf, 0x48, 0xee, 0xbb, 0x1d,
+ 0xea, 0x4c, 0x8b, 0x2d, 0xda, 0x7c, 0x29, 0x8f, 0x78, 0xde,
+ 0xd2, 0x74, 0x83, 0x25, 0x70, 0xd6, 0x21, 0x87, 0x39, 0x9f,
+ 0x68, 0xce, 0x9b, 0x3d, 0xca, 0x6c, 0x60, 0xc6, 0x31, 0x97,
+ 0xc2, 0x64, 0x93, 0x35, 0xf9, 0x5f, 0xa8, 0x0e, 0x5b, 0xfd,
+ 0x0a, 0xac, 0xa0, 0x06, 0xf1, 0x57, 0x02, 0xa4, 0x53, 0xf5,
+ 0x4b, 0xed, 0x1a, 0xbc, 0xe9, 0x4f, 0xb8, 0x1e, 0x12, 0xb4,
+ 0x43, 0xe5, 0xb0, 0x16, 0xe1, 0x47, 0x80, 0x26, 0xd1, 0x77,
+ 0x22, 0x84, 0x73, 0xd5, 0xd9, 0x7f, 0x88, 0x2e, 0x7b, 0xdd,
+ 0x2a, 0x8c, 0x32, 0x94, 0x63, 0xc5, 0x90, 0x36, 0xc1, 0x67,
+ 0x6b, 0xcd, 0x3a, 0x9c, 0xc9, 0x6f, 0x98, 0x3e, 0x0b, 0xad,
+ 0x5a, 0xfc, 0xa9, 0x0f, 0xf8, 0x5e, 0x52, 0xf4, 0x03, 0xa5,
+ 0xf0, 0x56, 0xa1, 0x07, 0xb9, 0x1f, 0xe8, 0x4e, 0x1b, 0xbd,
+ 0x4a, 0xec, 0xe0, 0x46, 0xb1, 0x17, 0x42, 0xe4, 0x13, 0xb5,
+ 0x72, 0xd4, 0x23, 0x85, 0xd0, 0x76, 0x81, 0x27, 0x2b, 0x8d,
+ 0x7a, 0xdc, 0x89, 0x2f, 0xd8, 0x7e, 0xc0, 0x66, 0x91, 0x37,
+ 0x62, 0xc4, 0x33, 0x95, 0x99, 0x3f, 0xc8, 0x6e, 0x3b, 0x9d,
+ 0x6a, 0xcc, 0x00, 0xa7, 0x53, 0xf4, 0xa6, 0x01, 0xf5, 0x52,
+ 0x51, 0xf6, 0x02, 0xa5, 0xf7, 0x50, 0xa4, 0x03, 0xa2, 0x05,
+ 0xf1, 0x56, 0x04, 0xa3, 0x57, 0xf0, 0xf3, 0x54, 0xa0, 0x07,
+ 0x55, 0xf2, 0x06, 0xa1, 0x59, 0xfe, 0x0a, 0xad, 0xff, 0x58,
+ 0xac, 0x0b, 0x08, 0xaf, 0x5b, 0xfc, 0xae, 0x09, 0xfd, 0x5a,
+ 0xfb, 0x5c, 0xa8, 0x0f, 0x5d, 0xfa, 0x0e, 0xa9, 0xaa, 0x0d,
+ 0xf9, 0x5e, 0x0c, 0xab, 0x5f, 0xf8, 0xb2, 0x15, 0xe1, 0x46,
+ 0x14, 0xb3, 0x47, 0xe0, 0xe3, 0x44, 0xb0, 0x17, 0x45, 0xe2,
+ 0x16, 0xb1, 0x10, 0xb7, 0x43, 0xe4, 0xb6, 0x11, 0xe5, 0x42,
+ 0x41, 0xe6, 0x12, 0xb5, 0xe7, 0x40, 0xb4, 0x13, 0xeb, 0x4c,
+ 0xb8, 0x1f, 0x4d, 0xea, 0x1e, 0xb9, 0xba, 0x1d, 0xe9, 0x4e,
+ 0x1c, 0xbb, 0x4f, 0xe8, 0x49, 0xee, 0x1a, 0xbd, 0xef, 0x48,
+ 0xbc, 0x1b, 0x18, 0xbf, 0x4b, 0xec, 0xbe, 0x19, 0xed, 0x4a,
+ 0x79, 0xde, 0x2a, 0x8d, 0xdf, 0x78, 0x8c, 0x2b, 0x28, 0x8f,
+ 0x7b, 0xdc, 0x8e, 0x29, 0xdd, 0x7a, 0xdb, 0x7c, 0x88, 0x2f,
+ 0x7d, 0xda, 0x2e, 0x89, 0x8a, 0x2d, 0xd9, 0x7e, 0x2c, 0x8b,
+ 0x7f, 0xd8, 0x20, 0x87, 0x73, 0xd4, 0x86, 0x21, 0xd5, 0x72,
+ 0x71, 0xd6, 0x22, 0x85, 0xd7, 0x70, 0x84, 0x23, 0x82, 0x25,
+ 0xd1, 0x76, 0x24, 0x83, 0x77, 0xd0, 0xd3, 0x74, 0x80, 0x27,
+ 0x75, 0xd2, 0x26, 0x81, 0xcb, 0x6c, 0x98, 0x3f, 0x6d, 0xca,
+ 0x3e, 0x99, 0x9a, 0x3d, 0xc9, 0x6e, 0x3c, 0x9b, 0x6f, 0xc8,
+ 0x69, 0xce, 0x3a, 0x9d, 0xcf, 0x68, 0x9c, 0x3b, 0x38, 0x9f,
+ 0x6b, 0xcc, 0x9e, 0x39, 0xcd, 0x6a, 0x92, 0x35, 0xc1, 0x66,
+ 0x34, 0x93, 0x67, 0xc0, 0xc3, 0x64, 0x90, 0x37, 0x65, 0xc2,
+ 0x36, 0x91, 0x30, 0x97, 0x63, 0xc4, 0x96, 0x31, 0xc5, 0x62,
+ 0x61, 0xc6, 0x32, 0x95, 0xc7, 0x60, 0x94, 0x33, 0x00, 0xa8,
+ 0x4d, 0xe5, 0x9a, 0x32, 0xd7, 0x7f, 0x29, 0x81, 0x64, 0xcc,
+ 0xb3, 0x1b, 0xfe, 0x56, 0x52, 0xfa, 0x1f, 0xb7, 0xc8, 0x60,
+ 0x85, 0x2d, 0x7b, 0xd3, 0x36, 0x9e, 0xe1, 0x49, 0xac, 0x04,
+ 0xa4, 0x0c, 0xe9, 0x41, 0x3e, 0x96, 0x73, 0xdb, 0x8d, 0x25,
+ 0xc0, 0x68, 0x17, 0xbf, 0x5a, 0xf2, 0xf6, 0x5e, 0xbb, 0x13,
+ 0x6c, 0xc4, 0x21, 0x89, 0xdf, 0x77, 0x92, 0x3a, 0x45, 0xed,
+ 0x08, 0xa0, 0x55, 0xfd, 0x18, 0xb0, 0xcf, 0x67, 0x82, 0x2a,
+ 0x7c, 0xd4, 0x31, 0x99, 0xe6, 0x4e, 0xab, 0x03, 0x07, 0xaf,
+ 0x4a, 0xe2, 0x9d, 0x35, 0xd0, 0x78, 0x2e, 0x86, 0x63, 0xcb,
+ 0xb4, 0x1c, 0xf9, 0x51, 0xf1, 0x59, 0xbc, 0x14, 0x6b, 0xc3,
+ 0x26, 0x8e, 0xd8, 0x70, 0x95, 0x3d, 0x42, 0xea, 0x0f, 0xa7,
+ 0xa3, 0x0b, 0xee, 0x46, 0x39, 0x91, 0x74, 0xdc, 0x8a, 0x22,
+ 0xc7, 0x6f, 0x10, 0xb8, 0x5d, 0xf5, 0xaa, 0x02, 0xe7, 0x4f,
+ 0x30, 0x98, 0x7d, 0xd5, 0x83, 0x2b, 0xce, 0x66, 0x19, 0xb1,
+ 0x54, 0xfc, 0xf8, 0x50, 0xb5, 0x1d, 0x62, 0xca, 0x2f, 0x87,
+ 0xd1, 0x79, 0x9c, 0x34, 0x4b, 0xe3, 0x06, 0xae, 0x0e, 0xa6,
+ 0x43, 0xeb, 0x94, 0x3c, 0xd9, 0x71, 0x27, 0x8f, 0x6a, 0xc2,
+ 0xbd, 0x15, 0xf0, 0x58, 0x5c, 0xf4, 0x11, 0xb9, 0xc6, 0x6e,
+ 0x8b, 0x23, 0x75, 0xdd, 0x38, 0x90, 0xef, 0x47, 0xa2, 0x0a,
+ 0xff, 0x57, 0xb2, 0x1a, 0x65, 0xcd, 0x28, 0x80, 0xd6, 0x7e,
+ 0x9b, 0x33, 0x4c, 0xe4, 0x01, 0xa9, 0xad, 0x05, 0xe0, 0x48,
+ 0x37, 0x9f, 0x7a, 0xd2, 0x84, 0x2c, 0xc9, 0x61, 0x1e, 0xb6,
+ 0x53, 0xfb, 0x5b, 0xf3, 0x16, 0xbe, 0xc1, 0x69, 0x8c, 0x24,
+ 0x72, 0xda, 0x3f, 0x97, 0xe8, 0x40, 0xa5, 0x0d, 0x09, 0xa1,
+ 0x44, 0xec, 0x93, 0x3b, 0xde, 0x76, 0x20, 0x88, 0x6d, 0xc5,
+ 0xba, 0x12, 0xf7, 0x5f, 0x00, 0xa9, 0x4f, 0xe6, 0x9e, 0x37,
+ 0xd1, 0x78, 0x21, 0x88, 0x6e, 0xc7, 0xbf, 0x16, 0xf0, 0x59,
+ 0x42, 0xeb, 0x0d, 0xa4, 0xdc, 0x75, 0x93, 0x3a, 0x63, 0xca,
+ 0x2c, 0x85, 0xfd, 0x54, 0xb2, 0x1b, 0x84, 0x2d, 0xcb, 0x62,
+ 0x1a, 0xb3, 0x55, 0xfc, 0xa5, 0x0c, 0xea, 0x43, 0x3b, 0x92,
+ 0x74, 0xdd, 0xc6, 0x6f, 0x89, 0x20, 0x58, 0xf1, 0x17, 0xbe,
+ 0xe7, 0x4e, 0xa8, 0x01, 0x79, 0xd0, 0x36, 0x9f, 0x15, 0xbc,
+ 0x5a, 0xf3, 0x8b, 0x22, 0xc4, 0x6d, 0x34, 0x9d, 0x7b, 0xd2,
+ 0xaa, 0x03, 0xe5, 0x4c, 0x57, 0xfe, 0x18, 0xb1, 0xc9, 0x60,
+ 0x86, 0x2f, 0x76, 0xdf, 0x39, 0x90, 0xe8, 0x41, 0xa7, 0x0e,
+ 0x91, 0x38, 0xde, 0x77, 0x0f, 0xa6, 0x40, 0xe9, 0xb0, 0x19,
+ 0xff, 0x56, 0x2e, 0x87, 0x61, 0xc8, 0xd3, 0x7a, 0x9c, 0x35,
+ 0x4d, 0xe4, 0x02, 0xab, 0xf2, 0x5b, 0xbd, 0x14, 0x6c, 0xc5,
+ 0x23, 0x8a, 0x2a, 0x83, 0x65, 0xcc, 0xb4, 0x1d, 0xfb, 0x52,
+ 0x0b, 0xa2, 0x44, 0xed, 0x95, 0x3c, 0xda, 0x73, 0x68, 0xc1,
+ 0x27, 0x8e, 0xf6, 0x5f, 0xb9, 0x10, 0x49, 0xe0, 0x06, 0xaf,
+ 0xd7, 0x7e, 0x98, 0x31, 0xae, 0x07, 0xe1, 0x48, 0x30, 0x99,
+ 0x7f, 0xd6, 0x8f, 0x26, 0xc0, 0x69, 0x11, 0xb8, 0x5e, 0xf7,
+ 0xec, 0x45, 0xa3, 0x0a, 0x72, 0xdb, 0x3d, 0x94, 0xcd, 0x64,
+ 0x82, 0x2b, 0x53, 0xfa, 0x1c, 0xb5, 0x3f, 0x96, 0x70, 0xd9,
+ 0xa1, 0x08, 0xee, 0x47, 0x1e, 0xb7, 0x51, 0xf8, 0x80, 0x29,
+ 0xcf, 0x66, 0x7d, 0xd4, 0x32, 0x9b, 0xe3, 0x4a, 0xac, 0x05,
+ 0x5c, 0xf5, 0x13, 0xba, 0xc2, 0x6b, 0x8d, 0x24, 0xbb, 0x12,
+ 0xf4, 0x5d, 0x25, 0x8c, 0x6a, 0xc3, 0x9a, 0x33, 0xd5, 0x7c,
+ 0x04, 0xad, 0x4b, 0xe2, 0xf9, 0x50, 0xb6, 0x1f, 0x67, 0xce,
+ 0x28, 0x81, 0xd8, 0x71, 0x97, 0x3e, 0x46, 0xef, 0x09, 0xa0,
+ 0x00, 0xaa, 0x49, 0xe3, 0x92, 0x38, 0xdb, 0x71, 0x39, 0x93,
+ 0x70, 0xda, 0xab, 0x01, 0xe2, 0x48, 0x72, 0xd8, 0x3b, 0x91,
+ 0xe0, 0x4a, 0xa9, 0x03, 0x4b, 0xe1, 0x02, 0xa8, 0xd9, 0x73,
+ 0x90, 0x3a, 0xe4, 0x4e, 0xad, 0x07, 0x76, 0xdc, 0x3f, 0x95,
+ 0xdd, 0x77, 0x94, 0x3e, 0x4f, 0xe5, 0x06, 0xac, 0x96, 0x3c,
+ 0xdf, 0x75, 0x04, 0xae, 0x4d, 0xe7, 0xaf, 0x05, 0xe6, 0x4c,
+ 0x3d, 0x97, 0x74, 0xde, 0xd5, 0x7f, 0x9c, 0x36, 0x47, 0xed,
+ 0x0e, 0xa4, 0xec, 0x46, 0xa5, 0x0f, 0x7e, 0xd4, 0x37, 0x9d,
+ 0xa7, 0x0d, 0xee, 0x44, 0x35, 0x9f, 0x7c, 0xd6, 0x9e, 0x34,
+ 0xd7, 0x7d, 0x0c, 0xa6, 0x45, 0xef, 0x31, 0x9b, 0x78, 0xd2,
+ 0xa3, 0x09, 0xea, 0x40, 0x08, 0xa2, 0x41, 0xeb, 0x9a, 0x30,
+ 0xd3, 0x79, 0x43, 0xe9, 0x0a, 0xa0, 0xd1, 0x7b, 0x98, 0x32,
+ 0x7a, 0xd0, 0x33, 0x99, 0xe8, 0x42, 0xa1, 0x0b, 0xb7, 0x1d,
+ 0xfe, 0x54, 0x25, 0x8f, 0x6c, 0xc6, 0x8e, 0x24, 0xc7, 0x6d,
+ 0x1c, 0xb6, 0x55, 0xff, 0xc5, 0x6f, 0x8c, 0x26, 0x57, 0xfd,
+ 0x1e, 0xb4, 0xfc, 0x56, 0xb5, 0x1f, 0x6e, 0xc4, 0x27, 0x8d,
+ 0x53, 0xf9, 0x1a, 0xb0, 0xc1, 0x6b, 0x88, 0x22, 0x6a, 0xc0,
+ 0x23, 0x89, 0xf8, 0x52, 0xb1, 0x1b, 0x21, 0x8b, 0x68, 0xc2,
+ 0xb3, 0x19, 0xfa, 0x50, 0x18, 0xb2, 0x51, 0xfb, 0x8a, 0x20,
+ 0xc3, 0x69, 0x62, 0xc8, 0x2b, 0x81, 0xf0, 0x5a, 0xb9, 0x13,
+ 0x5b, 0xf1, 0x12, 0xb8, 0xc9, 0x63, 0x80, 0x2a, 0x10, 0xba,
+ 0x59, 0xf3, 0x82, 0x28, 0xcb, 0x61, 0x29, 0x83, 0x60, 0xca,
+ 0xbb, 0x11, 0xf2, 0x58, 0x86, 0x2c, 0xcf, 0x65, 0x14, 0xbe,
+ 0x5d, 0xf7, 0xbf, 0x15, 0xf6, 0x5c, 0x2d, 0x87, 0x64, 0xce,
+ 0xf4, 0x5e, 0xbd, 0x17, 0x66, 0xcc, 0x2f, 0x85, 0xcd, 0x67,
+ 0x84, 0x2e, 0x5f, 0xf5, 0x16, 0xbc, 0x00, 0xab, 0x4b, 0xe0,
+ 0x96, 0x3d, 0xdd, 0x76, 0x31, 0x9a, 0x7a, 0xd1, 0xa7, 0x0c,
+ 0xec, 0x47, 0x62, 0xc9, 0x29, 0x82, 0xf4, 0x5f, 0xbf, 0x14,
+ 0x53, 0xf8, 0x18, 0xb3, 0xc5, 0x6e, 0x8e, 0x25, 0xc4, 0x6f,
+ 0x8f, 0x24, 0x52, 0xf9, 0x19, 0xb2, 0xf5, 0x5e, 0xbe, 0x15,
+ 0x63, 0xc8, 0x28, 0x83, 0xa6, 0x0d, 0xed, 0x46, 0x30, 0x9b,
+ 0x7b, 0xd0, 0x97, 0x3c, 0xdc, 0x77, 0x01, 0xaa, 0x4a, 0xe1,
+ 0x95, 0x3e, 0xde, 0x75, 0x03, 0xa8, 0x48, 0xe3, 0xa4, 0x0f,
+ 0xef, 0x44, 0x32, 0x99, 0x79, 0xd2, 0xf7, 0x5c, 0xbc, 0x17,
+ 0x61, 0xca, 0x2a, 0x81, 0xc6, 0x6d, 0x8d, 0x26, 0x50, 0xfb,
+ 0x1b, 0xb0, 0x51, 0xfa, 0x1a, 0xb1, 0xc7, 0x6c, 0x8c, 0x27,
+ 0x60, 0xcb, 0x2b, 0x80, 0xf6, 0x5d, 0xbd, 0x16, 0x33, 0x98,
+ 0x78, 0xd3, 0xa5, 0x0e, 0xee, 0x45, 0x02, 0xa9, 0x49, 0xe2,
+ 0x94, 0x3f, 0xdf, 0x74, 0x37, 0x9c, 0x7c, 0xd7, 0xa1, 0x0a,
+ 0xea, 0x41, 0x06, 0xad, 0x4d, 0xe6, 0x90, 0x3b, 0xdb, 0x70,
+ 0x55, 0xfe, 0x1e, 0xb5, 0xc3, 0x68, 0x88, 0x23, 0x64, 0xcf,
+ 0x2f, 0x84, 0xf2, 0x59, 0xb9, 0x12, 0xf3, 0x58, 0xb8, 0x13,
+ 0x65, 0xce, 0x2e, 0x85, 0xc2, 0x69, 0x89, 0x22, 0x54, 0xff,
+ 0x1f, 0xb4, 0x91, 0x3a, 0xda, 0x71, 0x07, 0xac, 0x4c, 0xe7,
+ 0xa0, 0x0b, 0xeb, 0x40, 0x36, 0x9d, 0x7d, 0xd6, 0xa2, 0x09,
+ 0xe9, 0x42, 0x34, 0x9f, 0x7f, 0xd4, 0x93, 0x38, 0xd8, 0x73,
+ 0x05, 0xae, 0x4e, 0xe5, 0xc0, 0x6b, 0x8b, 0x20, 0x56, 0xfd,
+ 0x1d, 0xb6, 0xf1, 0x5a, 0xba, 0x11, 0x67, 0xcc, 0x2c, 0x87,
+ 0x66, 0xcd, 0x2d, 0x86, 0xf0, 0x5b, 0xbb, 0x10, 0x57, 0xfc,
+ 0x1c, 0xb7, 0xc1, 0x6a, 0x8a, 0x21, 0x04, 0xaf, 0x4f, 0xe4,
+ 0x92, 0x39, 0xd9, 0x72, 0x35, 0x9e, 0x7e, 0xd5, 0xa3, 0x08,
+ 0xe8, 0x43, 0x00, 0xac, 0x45, 0xe9, 0x8a, 0x26, 0xcf, 0x63,
+ 0x09, 0xa5, 0x4c, 0xe0, 0x83, 0x2f, 0xc6, 0x6a, 0x12, 0xbe,
+ 0x57, 0xfb, 0x98, 0x34, 0xdd, 0x71, 0x1b, 0xb7, 0x5e, 0xf2,
+ 0x91, 0x3d, 0xd4, 0x78, 0x24, 0x88, 0x61, 0xcd, 0xae, 0x02,
+ 0xeb, 0x47, 0x2d, 0x81, 0x68, 0xc4, 0xa7, 0x0b, 0xe2, 0x4e,
+ 0x36, 0x9a, 0x73, 0xdf, 0xbc, 0x10, 0xf9, 0x55, 0x3f, 0x93,
+ 0x7a, 0xd6, 0xb5, 0x19, 0xf0, 0x5c, 0x48, 0xe4, 0x0d, 0xa1,
+ 0xc2, 0x6e, 0x87, 0x2b, 0x41, 0xed, 0x04, 0xa8, 0xcb, 0x67,
+ 0x8e, 0x22, 0x5a, 0xf6, 0x1f, 0xb3, 0xd0, 0x7c, 0x95, 0x39,
+ 0x53, 0xff, 0x16, 0xba, 0xd9, 0x75, 0x9c, 0x30, 0x6c, 0xc0,
+ 0x29, 0x85, 0xe6, 0x4a, 0xa3, 0x0f, 0x65, 0xc9, 0x20, 0x8c,
+ 0xef, 0x43, 0xaa, 0x06, 0x7e, 0xd2, 0x3b, 0x97, 0xf4, 0x58,
+ 0xb1, 0x1d, 0x77, 0xdb, 0x32, 0x9e, 0xfd, 0x51, 0xb8, 0x14,
+ 0x90, 0x3c, 0xd5, 0x79, 0x1a, 0xb6, 0x5f, 0xf3, 0x99, 0x35,
+ 0xdc, 0x70, 0x13, 0xbf, 0x56, 0xfa, 0x82, 0x2e, 0xc7, 0x6b,
+ 0x08, 0xa4, 0x4d, 0xe1, 0x8b, 0x27, 0xce, 0x62, 0x01, 0xad,
+ 0x44, 0xe8, 0xb4, 0x18, 0xf1, 0x5d, 0x3e, 0x92, 0x7b, 0xd7,
+ 0xbd, 0x11, 0xf8, 0x54, 0x37, 0x9b, 0x72, 0xde, 0xa6, 0x0a,
+ 0xe3, 0x4f, 0x2c, 0x80, 0x69, 0xc5, 0xaf, 0x03, 0xea, 0x46,
+ 0x25, 0x89, 0x60, 0xcc, 0xd8, 0x74, 0x9d, 0x31, 0x52, 0xfe,
+ 0x17, 0xbb, 0xd1, 0x7d, 0x94, 0x38, 0x5b, 0xf7, 0x1e, 0xb2,
+ 0xca, 0x66, 0x8f, 0x23, 0x40, 0xec, 0x05, 0xa9, 0xc3, 0x6f,
+ 0x86, 0x2a, 0x49, 0xe5, 0x0c, 0xa0, 0xfc, 0x50, 0xb9, 0x15,
+ 0x76, 0xda, 0x33, 0x9f, 0xf5, 0x59, 0xb0, 0x1c, 0x7f, 0xd3,
+ 0x3a, 0x96, 0xee, 0x42, 0xab, 0x07, 0x64, 0xc8, 0x21, 0x8d,
+ 0xe7, 0x4b, 0xa2, 0x0e, 0x6d, 0xc1, 0x28, 0x84, 0x00, 0xad,
+ 0x47, 0xea, 0x8e, 0x23, 0xc9, 0x64, 0x01, 0xac, 0x46, 0xeb,
+ 0x8f, 0x22, 0xc8, 0x65, 0x02, 0xaf, 0x45, 0xe8, 0x8c, 0x21,
+ 0xcb, 0x66, 0x03, 0xae, 0x44, 0xe9, 0x8d, 0x20, 0xca, 0x67,
+ 0x04, 0xa9, 0x43, 0xee, 0x8a, 0x27, 0xcd, 0x60, 0x05, 0xa8,
+ 0x42, 0xef, 0x8b, 0x26, 0xcc, 0x61, 0x06, 0xab, 0x41, 0xec,
+ 0x88, 0x25, 0xcf, 0x62, 0x07, 0xaa, 0x40, 0xed, 0x89, 0x24,
+ 0xce, 0x63, 0x08, 0xa5, 0x4f, 0xe2, 0x86, 0x2b, 0xc1, 0x6c,
+ 0x09, 0xa4, 0x4e, 0xe3, 0x87, 0x2a, 0xc0, 0x6d, 0x0a, 0xa7,
+ 0x4d, 0xe0, 0x84, 0x29, 0xc3, 0x6e, 0x0b, 0xa6, 0x4c, 0xe1,
+ 0x85, 0x28, 0xc2, 0x6f, 0x0c, 0xa1, 0x4b, 0xe6, 0x82, 0x2f,
+ 0xc5, 0x68, 0x0d, 0xa0, 0x4a, 0xe7, 0x83, 0x2e, 0xc4, 0x69,
+ 0x0e, 0xa3, 0x49, 0xe4, 0x80, 0x2d, 0xc7, 0x6a, 0x0f, 0xa2,
+ 0x48, 0xe5, 0x81, 0x2c, 0xc6, 0x6b, 0x10, 0xbd, 0x57, 0xfa,
+ 0x9e, 0x33, 0xd9, 0x74, 0x11, 0xbc, 0x56, 0xfb, 0x9f, 0x32,
+ 0xd8, 0x75, 0x12, 0xbf, 0x55, 0xf8, 0x9c, 0x31, 0xdb, 0x76,
+ 0x13, 0xbe, 0x54, 0xf9, 0x9d, 0x30, 0xda, 0x77, 0x14, 0xb9,
+ 0x53, 0xfe, 0x9a, 0x37, 0xdd, 0x70, 0x15, 0xb8, 0x52, 0xff,
+ 0x9b, 0x36, 0xdc, 0x71, 0x16, 0xbb, 0x51, 0xfc, 0x98, 0x35,
+ 0xdf, 0x72, 0x17, 0xba, 0x50, 0xfd, 0x99, 0x34, 0xde, 0x73,
+ 0x18, 0xb5, 0x5f, 0xf2, 0x96, 0x3b, 0xd1, 0x7c, 0x19, 0xb4,
+ 0x5e, 0xf3, 0x97, 0x3a, 0xd0, 0x7d, 0x1a, 0xb7, 0x5d, 0xf0,
+ 0x94, 0x39, 0xd3, 0x7e, 0x1b, 0xb6, 0x5c, 0xf1, 0x95, 0x38,
+ 0xd2, 0x7f, 0x1c, 0xb1, 0x5b, 0xf6, 0x92, 0x3f, 0xd5, 0x78,
+ 0x1d, 0xb0, 0x5a, 0xf7, 0x93, 0x3e, 0xd4, 0x79, 0x1e, 0xb3,
+ 0x59, 0xf4, 0x90, 0x3d, 0xd7, 0x7a, 0x1f, 0xb2, 0x58, 0xf5,
+ 0x91, 0x3c, 0xd6, 0x7b, 0x00, 0xae, 0x41, 0xef, 0x82, 0x2c,
+ 0xc3, 0x6d, 0x19, 0xb7, 0x58, 0xf6, 0x9b, 0x35, 0xda, 0x74,
+ 0x32, 0x9c, 0x73, 0xdd, 0xb0, 0x1e, 0xf1, 0x5f, 0x2b, 0x85,
+ 0x6a, 0xc4, 0xa9, 0x07, 0xe8, 0x46, 0x64, 0xca, 0x25, 0x8b,
+ 0xe6, 0x48, 0xa7, 0x09, 0x7d, 0xd3, 0x3c, 0x92, 0xff, 0x51,
+ 0xbe, 0x10, 0x56, 0xf8, 0x17, 0xb9, 0xd4, 0x7a, 0x95, 0x3b,
+ 0x4f, 0xe1, 0x0e, 0xa0, 0xcd, 0x63, 0x8c, 0x22, 0xc8, 0x66,
+ 0x89, 0x27, 0x4a, 0xe4, 0x0b, 0xa5, 0xd1, 0x7f, 0x90, 0x3e,
+ 0x53, 0xfd, 0x12, 0xbc, 0xfa, 0x54, 0xbb, 0x15, 0x78, 0xd6,
+ 0x39, 0x97, 0xe3, 0x4d, 0xa2, 0x0c, 0x61, 0xcf, 0x20, 0x8e,
+ 0xac, 0x02, 0xed, 0x43, 0x2e, 0x80, 0x6f, 0xc1, 0xb5, 0x1b,
+ 0xf4, 0x5a, 0x37, 0x99, 0x76, 0xd8, 0x9e, 0x30, 0xdf, 0x71,
+ 0x1c, 0xb2, 0x5d, 0xf3, 0x87, 0x29, 0xc6, 0x68, 0x05, 0xab,
+ 0x44, 0xea, 0x8d, 0x23, 0xcc, 0x62, 0x0f, 0xa1, 0x4e, 0xe0,
+ 0x94, 0x3a, 0xd5, 0x7b, 0x16, 0xb8, 0x57, 0xf9, 0xbf, 0x11,
+ 0xfe, 0x50, 0x3d, 0x93, 0x7c, 0xd2, 0xa6, 0x08, 0xe7, 0x49,
+ 0x24, 0x8a, 0x65, 0xcb, 0xe9, 0x47, 0xa8, 0x06, 0x6b, 0xc5,
+ 0x2a, 0x84, 0xf0, 0x5e, 0xb1, 0x1f, 0x72, 0xdc, 0x33, 0x9d,
+ 0xdb, 0x75, 0x9a, 0x34, 0x59, 0xf7, 0x18, 0xb6, 0xc2, 0x6c,
+ 0x83, 0x2d, 0x40, 0xee, 0x01, 0xaf, 0x45, 0xeb, 0x04, 0xaa,
+ 0xc7, 0x69, 0x86, 0x28, 0x5c, 0xf2, 0x1d, 0xb3, 0xde, 0x70,
+ 0x9f, 0x31, 0x77, 0xd9, 0x36, 0x98, 0xf5, 0x5b, 0xb4, 0x1a,
+ 0x6e, 0xc0, 0x2f, 0x81, 0xec, 0x42, 0xad, 0x03, 0x21, 0x8f,
+ 0x60, 0xce, 0xa3, 0x0d, 0xe2, 0x4c, 0x38, 0x96, 0x79, 0xd7,
+ 0xba, 0x14, 0xfb, 0x55, 0x13, 0xbd, 0x52, 0xfc, 0x91, 0x3f,
+ 0xd0, 0x7e, 0x0a, 0xa4, 0x4b, 0xe5, 0x88, 0x26, 0xc9, 0x67,
+ 0x00, 0xaf, 0x43, 0xec, 0x86, 0x29, 0xc5, 0x6a, 0x11, 0xbe,
+ 0x52, 0xfd, 0x97, 0x38, 0xd4, 0x7b, 0x22, 0x8d, 0x61, 0xce,
+ 0xa4, 0x0b, 0xe7, 0x48, 0x33, 0x9c, 0x70, 0xdf, 0xb5, 0x1a,
+ 0xf6, 0x59, 0x44, 0xeb, 0x07, 0xa8, 0xc2, 0x6d, 0x81, 0x2e,
+ 0x55, 0xfa, 0x16, 0xb9, 0xd3, 0x7c, 0x90, 0x3f, 0x66, 0xc9,
+ 0x25, 0x8a, 0xe0, 0x4f, 0xa3, 0x0c, 0x77, 0xd8, 0x34, 0x9b,
+ 0xf1, 0x5e, 0xb2, 0x1d, 0x88, 0x27, 0xcb, 0x64, 0x0e, 0xa1,
+ 0x4d, 0xe2, 0x99, 0x36, 0xda, 0x75, 0x1f, 0xb0, 0x5c, 0xf3,
+ 0xaa, 0x05, 0xe9, 0x46, 0x2c, 0x83, 0x6f, 0xc0, 0xbb, 0x14,
+ 0xf8, 0x57, 0x3d, 0x92, 0x7e, 0xd1, 0xcc, 0x63, 0x8f, 0x20,
+ 0x4a, 0xe5, 0x09, 0xa6, 0xdd, 0x72, 0x9e, 0x31, 0x5b, 0xf4,
+ 0x18, 0xb7, 0xee, 0x41, 0xad, 0x02, 0x68, 0xc7, 0x2b, 0x84,
+ 0xff, 0x50, 0xbc, 0x13, 0x79, 0xd6, 0x3a, 0x95, 0x0d, 0xa2,
+ 0x4e, 0xe1, 0x8b, 0x24, 0xc8, 0x67, 0x1c, 0xb3, 0x5f, 0xf0,
+ 0x9a, 0x35, 0xd9, 0x76, 0x2f, 0x80, 0x6c, 0xc3, 0xa9, 0x06,
+ 0xea, 0x45, 0x3e, 0x91, 0x7d, 0xd2, 0xb8, 0x17, 0xfb, 0x54,
+ 0x49, 0xe6, 0x0a, 0xa5, 0xcf, 0x60, 0x8c, 0x23, 0x58, 0xf7,
+ 0x1b, 0xb4, 0xde, 0x71, 0x9d, 0x32, 0x6b, 0xc4, 0x28, 0x87,
+ 0xed, 0x42, 0xae, 0x01, 0x7a, 0xd5, 0x39, 0x96, 0xfc, 0x53,
+ 0xbf, 0x10, 0x85, 0x2a, 0xc6, 0x69, 0x03, 0xac, 0x40, 0xef,
+ 0x94, 0x3b, 0xd7, 0x78, 0x12, 0xbd, 0x51, 0xfe, 0xa7, 0x08,
+ 0xe4, 0x4b, 0x21, 0x8e, 0x62, 0xcd, 0xb6, 0x19, 0xf5, 0x5a,
+ 0x30, 0x9f, 0x73, 0xdc, 0xc1, 0x6e, 0x82, 0x2d, 0x47, 0xe8,
+ 0x04, 0xab, 0xd0, 0x7f, 0x93, 0x3c, 0x56, 0xf9, 0x15, 0xba,
+ 0xe3, 0x4c, 0xa0, 0x0f, 0x65, 0xca, 0x26, 0x89, 0xf2, 0x5d,
+ 0xb1, 0x1e, 0x74, 0xdb, 0x37, 0x98, 0x00, 0xb0, 0x7d, 0xcd,
+ 0xfa, 0x4a, 0x87, 0x37, 0xe9, 0x59, 0x94, 0x24, 0x13, 0xa3,
+ 0x6e, 0xde, 0xcf, 0x7f, 0xb2, 0x02, 0x35, 0x85, 0x48, 0xf8,
+ 0x26, 0x96, 0x5b, 0xeb, 0xdc, 0x6c, 0xa1, 0x11, 0x83, 0x33,
+ 0xfe, 0x4e, 0x79, 0xc9, 0x04, 0xb4, 0x6a, 0xda, 0x17, 0xa7,
+ 0x90, 0x20, 0xed, 0x5d, 0x4c, 0xfc, 0x31, 0x81, 0xb6, 0x06,
+ 0xcb, 0x7b, 0xa5, 0x15, 0xd8, 0x68, 0x5f, 0xef, 0x22, 0x92,
+ 0x1b, 0xab, 0x66, 0xd6, 0xe1, 0x51, 0x9c, 0x2c, 0xf2, 0x42,
+ 0x8f, 0x3f, 0x08, 0xb8, 0x75, 0xc5, 0xd4, 0x64, 0xa9, 0x19,
+ 0x2e, 0x9e, 0x53, 0xe3, 0x3d, 0x8d, 0x40, 0xf0, 0xc7, 0x77,
+ 0xba, 0x0a, 0x98, 0x28, 0xe5, 0x55, 0x62, 0xd2, 0x1f, 0xaf,
+ 0x71, 0xc1, 0x0c, 0xbc, 0x8b, 0x3b, 0xf6, 0x46, 0x57, 0xe7,
+ 0x2a, 0x9a, 0xad, 0x1d, 0xd0, 0x60, 0xbe, 0x0e, 0xc3, 0x73,
+ 0x44, 0xf4, 0x39, 0x89, 0x36, 0x86, 0x4b, 0xfb, 0xcc, 0x7c,
+ 0xb1, 0x01, 0xdf, 0x6f, 0xa2, 0x12, 0x25, 0x95, 0x58, 0xe8,
+ 0xf9, 0x49, 0x84, 0x34, 0x03, 0xb3, 0x7e, 0xce, 0x10, 0xa0,
+ 0x6d, 0xdd, 0xea, 0x5a, 0x97, 0x27, 0xb5, 0x05, 0xc8, 0x78,
+ 0x4f, 0xff, 0x32, 0x82, 0x5c, 0xec, 0x21, 0x91, 0xa6, 0x16,
+ 0xdb, 0x6b, 0x7a, 0xca, 0x07, 0xb7, 0x80, 0x30, 0xfd, 0x4d,
+ 0x93, 0x23, 0xee, 0x5e, 0x69, 0xd9, 0x14, 0xa4, 0x2d, 0x9d,
+ 0x50, 0xe0, 0xd7, 0x67, 0xaa, 0x1a, 0xc4, 0x74, 0xb9, 0x09,
+ 0x3e, 0x8e, 0x43, 0xf3, 0xe2, 0x52, 0x9f, 0x2f, 0x18, 0xa8,
+ 0x65, 0xd5, 0x0b, 0xbb, 0x76, 0xc6, 0xf1, 0x41, 0x8c, 0x3c,
+ 0xae, 0x1e, 0xd3, 0x63, 0x54, 0xe4, 0x29, 0x99, 0x47, 0xf7,
+ 0x3a, 0x8a, 0xbd, 0x0d, 0xc0, 0x70, 0x61, 0xd1, 0x1c, 0xac,
+ 0x9b, 0x2b, 0xe6, 0x56, 0x88, 0x38, 0xf5, 0x45, 0x72, 0xc2,
+ 0x0f, 0xbf, 0x00, 0xb1, 0x7f, 0xce, 0xfe, 0x4f, 0x81, 0x30,
+ 0xe1, 0x50, 0x9e, 0x2f, 0x1f, 0xae, 0x60, 0xd1, 0xdf, 0x6e,
+ 0xa0, 0x11, 0x21, 0x90, 0x5e, 0xef, 0x3e, 0x8f, 0x41, 0xf0,
+ 0xc0, 0x71, 0xbf, 0x0e, 0xa3, 0x12, 0xdc, 0x6d, 0x5d, 0xec,
+ 0x22, 0x93, 0x42, 0xf3, 0x3d, 0x8c, 0xbc, 0x0d, 0xc3, 0x72,
+ 0x7c, 0xcd, 0x03, 0xb2, 0x82, 0x33, 0xfd, 0x4c, 0x9d, 0x2c,
+ 0xe2, 0x53, 0x63, 0xd2, 0x1c, 0xad, 0x5b, 0xea, 0x24, 0x95,
+ 0xa5, 0x14, 0xda, 0x6b, 0xba, 0x0b, 0xc5, 0x74, 0x44, 0xf5,
+ 0x3b, 0x8a, 0x84, 0x35, 0xfb, 0x4a, 0x7a, 0xcb, 0x05, 0xb4,
+ 0x65, 0xd4, 0x1a, 0xab, 0x9b, 0x2a, 0xe4, 0x55, 0xf8, 0x49,
+ 0x87, 0x36, 0x06, 0xb7, 0x79, 0xc8, 0x19, 0xa8, 0x66, 0xd7,
+ 0xe7, 0x56, 0x98, 0x29, 0x27, 0x96, 0x58, 0xe9, 0xd9, 0x68,
+ 0xa6, 0x17, 0xc6, 0x77, 0xb9, 0x08, 0x38, 0x89, 0x47, 0xf6,
+ 0xb6, 0x07, 0xc9, 0x78, 0x48, 0xf9, 0x37, 0x86, 0x57, 0xe6,
+ 0x28, 0x99, 0xa9, 0x18, 0xd6, 0x67, 0x69, 0xd8, 0x16, 0xa7,
+ 0x97, 0x26, 0xe8, 0x59, 0x88, 0x39, 0xf7, 0x46, 0x76, 0xc7,
+ 0x09, 0xb8, 0x15, 0xa4, 0x6a, 0xdb, 0xeb, 0x5a, 0x94, 0x25,
+ 0xf4, 0x45, 0x8b, 0x3a, 0x0a, 0xbb, 0x75, 0xc4, 0xca, 0x7b,
+ 0xb5, 0x04, 0x34, 0x85, 0x4b, 0xfa, 0x2b, 0x9a, 0x54, 0xe5,
+ 0xd5, 0x64, 0xaa, 0x1b, 0xed, 0x5c, 0x92, 0x23, 0x13, 0xa2,
+ 0x6c, 0xdd, 0x0c, 0xbd, 0x73, 0xc2, 0xf2, 0x43, 0x8d, 0x3c,
+ 0x32, 0x83, 0x4d, 0xfc, 0xcc, 0x7d, 0xb3, 0x02, 0xd3, 0x62,
+ 0xac, 0x1d, 0x2d, 0x9c, 0x52, 0xe3, 0x4e, 0xff, 0x31, 0x80,
+ 0xb0, 0x01, 0xcf, 0x7e, 0xaf, 0x1e, 0xd0, 0x61, 0x51, 0xe0,
+ 0x2e, 0x9f, 0x91, 0x20, 0xee, 0x5f, 0x6f, 0xde, 0x10, 0xa1,
+ 0x70, 0xc1, 0x0f, 0xbe, 0x8e, 0x3f, 0xf1, 0x40, 0x00, 0xb2,
+ 0x79, 0xcb, 0xf2, 0x40, 0x8b, 0x39, 0xf9, 0x4b, 0x80, 0x32,
+ 0x0b, 0xb9, 0x72, 0xc0, 0xef, 0x5d, 0x96, 0x24, 0x1d, 0xaf,
+ 0x64, 0xd6, 0x16, 0xa4, 0x6f, 0xdd, 0xe4, 0x56, 0x9d, 0x2f,
+ 0xc3, 0x71, 0xba, 0x08, 0x31, 0x83, 0x48, 0xfa, 0x3a, 0x88,
+ 0x43, 0xf1, 0xc8, 0x7a, 0xb1, 0x03, 0x2c, 0x9e, 0x55, 0xe7,
+ 0xde, 0x6c, 0xa7, 0x15, 0xd5, 0x67, 0xac, 0x1e, 0x27, 0x95,
+ 0x5e, 0xec, 0x9b, 0x29, 0xe2, 0x50, 0x69, 0xdb, 0x10, 0xa2,
+ 0x62, 0xd0, 0x1b, 0xa9, 0x90, 0x22, 0xe9, 0x5b, 0x74, 0xc6,
+ 0x0d, 0xbf, 0x86, 0x34, 0xff, 0x4d, 0x8d, 0x3f, 0xf4, 0x46,
+ 0x7f, 0xcd, 0x06, 0xb4, 0x58, 0xea, 0x21, 0x93, 0xaa, 0x18,
+ 0xd3, 0x61, 0xa1, 0x13, 0xd8, 0x6a, 0x53, 0xe1, 0x2a, 0x98,
+ 0xb7, 0x05, 0xce, 0x7c, 0x45, 0xf7, 0x3c, 0x8e, 0x4e, 0xfc,
+ 0x37, 0x85, 0xbc, 0x0e, 0xc5, 0x77, 0x2b, 0x99, 0x52, 0xe0,
+ 0xd9, 0x6b, 0xa0, 0x12, 0xd2, 0x60, 0xab, 0x19, 0x20, 0x92,
+ 0x59, 0xeb, 0xc4, 0x76, 0xbd, 0x0f, 0x36, 0x84, 0x4f, 0xfd,
+ 0x3d, 0x8f, 0x44, 0xf6, 0xcf, 0x7d, 0xb6, 0x04, 0xe8, 0x5a,
+ 0x91, 0x23, 0x1a, 0xa8, 0x63, 0xd1, 0x11, 0xa3, 0x68, 0xda,
+ 0xe3, 0x51, 0x9a, 0x28, 0x07, 0xb5, 0x7e, 0xcc, 0xf5, 0x47,
+ 0x8c, 0x3e, 0xfe, 0x4c, 0x87, 0x35, 0x0c, 0xbe, 0x75, 0xc7,
+ 0xb0, 0x02, 0xc9, 0x7b, 0x42, 0xf0, 0x3b, 0x89, 0x49, 0xfb,
+ 0x30, 0x82, 0xbb, 0x09, 0xc2, 0x70, 0x5f, 0xed, 0x26, 0x94,
+ 0xad, 0x1f, 0xd4, 0x66, 0xa6, 0x14, 0xdf, 0x6d, 0x54, 0xe6,
+ 0x2d, 0x9f, 0x73, 0xc1, 0x0a, 0xb8, 0x81, 0x33, 0xf8, 0x4a,
+ 0x8a, 0x38, 0xf3, 0x41, 0x78, 0xca, 0x01, 0xb3, 0x9c, 0x2e,
+ 0xe5, 0x57, 0x6e, 0xdc, 0x17, 0xa5, 0x65, 0xd7, 0x1c, 0xae,
+ 0x97, 0x25, 0xee, 0x5c, 0x00, 0xb3, 0x7b, 0xc8, 0xf6, 0x45,
+ 0x8d, 0x3e, 0xf1, 0x42, 0x8a, 0x39, 0x07, 0xb4, 0x7c, 0xcf,
+ 0xff, 0x4c, 0x84, 0x37, 0x09, 0xba, 0x72, 0xc1, 0x0e, 0xbd,
+ 0x75, 0xc6, 0xf8, 0x4b, 0x83, 0x30, 0xe3, 0x50, 0x98, 0x2b,
+ 0x15, 0xa6, 0x6e, 0xdd, 0x12, 0xa1, 0x69, 0xda, 0xe4, 0x57,
+ 0x9f, 0x2c, 0x1c, 0xaf, 0x67, 0xd4, 0xea, 0x59, 0x91, 0x22,
+ 0xed, 0x5e, 0x96, 0x25, 0x1b, 0xa8, 0x60, 0xd3, 0xdb, 0x68,
+ 0xa0, 0x13, 0x2d, 0x9e, 0x56, 0xe5, 0x2a, 0x99, 0x51, 0xe2,
+ 0xdc, 0x6f, 0xa7, 0x14, 0x24, 0x97, 0x5f, 0xec, 0xd2, 0x61,
+ 0xa9, 0x1a, 0xd5, 0x66, 0xae, 0x1d, 0x23, 0x90, 0x58, 0xeb,
+ 0x38, 0x8b, 0x43, 0xf0, 0xce, 0x7d, 0xb5, 0x06, 0xc9, 0x7a,
+ 0xb2, 0x01, 0x3f, 0x8c, 0x44, 0xf7, 0xc7, 0x74, 0xbc, 0x0f,
+ 0x31, 0x82, 0x4a, 0xf9, 0x36, 0x85, 0x4d, 0xfe, 0xc0, 0x73,
+ 0xbb, 0x08, 0xab, 0x18, 0xd0, 0x63, 0x5d, 0xee, 0x26, 0x95,
+ 0x5a, 0xe9, 0x21, 0x92, 0xac, 0x1f, 0xd7, 0x64, 0x54, 0xe7,
+ 0x2f, 0x9c, 0xa2, 0x11, 0xd9, 0x6a, 0xa5, 0x16, 0xde, 0x6d,
+ 0x53, 0xe0, 0x28, 0x9b, 0x48, 0xfb, 0x33, 0x80, 0xbe, 0x0d,
+ 0xc5, 0x76, 0xb9, 0x0a, 0xc2, 0x71, 0x4f, 0xfc, 0x34, 0x87,
+ 0xb7, 0x04, 0xcc, 0x7f, 0x41, 0xf2, 0x3a, 0x89, 0x46, 0xf5,
+ 0x3d, 0x8e, 0xb0, 0x03, 0xcb, 0x78, 0x70, 0xc3, 0x0b, 0xb8,
+ 0x86, 0x35, 0xfd, 0x4e, 0x81, 0x32, 0xfa, 0x49, 0x77, 0xc4,
+ 0x0c, 0xbf, 0x8f, 0x3c, 0xf4, 0x47, 0x79, 0xca, 0x02, 0xb1,
+ 0x7e, 0xcd, 0x05, 0xb6, 0x88, 0x3b, 0xf3, 0x40, 0x93, 0x20,
+ 0xe8, 0x5b, 0x65, 0xd6, 0x1e, 0xad, 0x62, 0xd1, 0x19, 0xaa,
+ 0x94, 0x27, 0xef, 0x5c, 0x6c, 0xdf, 0x17, 0xa4, 0x9a, 0x29,
+ 0xe1, 0x52, 0x9d, 0x2e, 0xe6, 0x55, 0x6b, 0xd8, 0x10, 0xa3,
+ 0x00, 0xb4, 0x75, 0xc1, 0xea, 0x5e, 0x9f, 0x2b, 0xc9, 0x7d,
+ 0xbc, 0x08, 0x23, 0x97, 0x56, 0xe2, 0x8f, 0x3b, 0xfa, 0x4e,
+ 0x65, 0xd1, 0x10, 0xa4, 0x46, 0xf2, 0x33, 0x87, 0xac, 0x18,
+ 0xd9, 0x6d, 0x03, 0xb7, 0x76, 0xc2, 0xe9, 0x5d, 0x9c, 0x28,
+ 0xca, 0x7e, 0xbf, 0x0b, 0x20, 0x94, 0x55, 0xe1, 0x8c, 0x38,
+ 0xf9, 0x4d, 0x66, 0xd2, 0x13, 0xa7, 0x45, 0xf1, 0x30, 0x84,
+ 0xaf, 0x1b, 0xda, 0x6e, 0x06, 0xb2, 0x73, 0xc7, 0xec, 0x58,
+ 0x99, 0x2d, 0xcf, 0x7b, 0xba, 0x0e, 0x25, 0x91, 0x50, 0xe4,
+ 0x89, 0x3d, 0xfc, 0x48, 0x63, 0xd7, 0x16, 0xa2, 0x40, 0xf4,
+ 0x35, 0x81, 0xaa, 0x1e, 0xdf, 0x6b, 0x05, 0xb1, 0x70, 0xc4,
+ 0xef, 0x5b, 0x9a, 0x2e, 0xcc, 0x78, 0xb9, 0x0d, 0x26, 0x92,
+ 0x53, 0xe7, 0x8a, 0x3e, 0xff, 0x4b, 0x60, 0xd4, 0x15, 0xa1,
+ 0x43, 0xf7, 0x36, 0x82, 0xa9, 0x1d, 0xdc, 0x68, 0x0c, 0xb8,
+ 0x79, 0xcd, 0xe6, 0x52, 0x93, 0x27, 0xc5, 0x71, 0xb0, 0x04,
+ 0x2f, 0x9b, 0x5a, 0xee, 0x83, 0x37, 0xf6, 0x42, 0x69, 0xdd,
+ 0x1c, 0xa8, 0x4a, 0xfe, 0x3f, 0x8b, 0xa0, 0x14, 0xd5, 0x61,
+ 0x0f, 0xbb, 0x7a, 0xce, 0xe5, 0x51, 0x90, 0x24, 0xc6, 0x72,
+ 0xb3, 0x07, 0x2c, 0x98, 0x59, 0xed, 0x80, 0x34, 0xf5, 0x41,
+ 0x6a, 0xde, 0x1f, 0xab, 0x49, 0xfd, 0x3c, 0x88, 0xa3, 0x17,
+ 0xd6, 0x62, 0x0a, 0xbe, 0x7f, 0xcb, 0xe0, 0x54, 0x95, 0x21,
+ 0xc3, 0x77, 0xb6, 0x02, 0x29, 0x9d, 0x5c, 0xe8, 0x85, 0x31,
+ 0xf0, 0x44, 0x6f, 0xdb, 0x1a, 0xae, 0x4c, 0xf8, 0x39, 0x8d,
+ 0xa6, 0x12, 0xd3, 0x67, 0x09, 0xbd, 0x7c, 0xc8, 0xe3, 0x57,
+ 0x96, 0x22, 0xc0, 0x74, 0xb5, 0x01, 0x2a, 0x9e, 0x5f, 0xeb,
+ 0x86, 0x32, 0xf3, 0x47, 0x6c, 0xd8, 0x19, 0xad, 0x4f, 0xfb,
+ 0x3a, 0x8e, 0xa5, 0x11, 0xd0, 0x64, 0x00, 0xb5, 0x77, 0xc2,
+ 0xee, 0x5b, 0x99, 0x2c, 0xc1, 0x74, 0xb6, 0x03, 0x2f, 0x9a,
+ 0x58, 0xed, 0x9f, 0x2a, 0xe8, 0x5d, 0x71, 0xc4, 0x06, 0xb3,
+ 0x5e, 0xeb, 0x29, 0x9c, 0xb0, 0x05, 0xc7, 0x72, 0x23, 0x96,
+ 0x54, 0xe1, 0xcd, 0x78, 0xba, 0x0f, 0xe2, 0x57, 0x95, 0x20,
+ 0x0c, 0xb9, 0x7b, 0xce, 0xbc, 0x09, 0xcb, 0x7e, 0x52, 0xe7,
+ 0x25, 0x90, 0x7d, 0xc8, 0x0a, 0xbf, 0x93, 0x26, 0xe4, 0x51,
+ 0x46, 0xf3, 0x31, 0x84, 0xa8, 0x1d, 0xdf, 0x6a, 0x87, 0x32,
+ 0xf0, 0x45, 0x69, 0xdc, 0x1e, 0xab, 0xd9, 0x6c, 0xae, 0x1b,
+ 0x37, 0x82, 0x40, 0xf5, 0x18, 0xad, 0x6f, 0xda, 0xf6, 0x43,
+ 0x81, 0x34, 0x65, 0xd0, 0x12, 0xa7, 0x8b, 0x3e, 0xfc, 0x49,
+ 0xa4, 0x11, 0xd3, 0x66, 0x4a, 0xff, 0x3d, 0x88, 0xfa, 0x4f,
+ 0x8d, 0x38, 0x14, 0xa1, 0x63, 0xd6, 0x3b, 0x8e, 0x4c, 0xf9,
+ 0xd5, 0x60, 0xa2, 0x17, 0x8c, 0x39, 0xfb, 0x4e, 0x62, 0xd7,
+ 0x15, 0xa0, 0x4d, 0xf8, 0x3a, 0x8f, 0xa3, 0x16, 0xd4, 0x61,
+ 0x13, 0xa6, 0x64, 0xd1, 0xfd, 0x48, 0x8a, 0x3f, 0xd2, 0x67,
+ 0xa5, 0x10, 0x3c, 0x89, 0x4b, 0xfe, 0xaf, 0x1a, 0xd8, 0x6d,
+ 0x41, 0xf4, 0x36, 0x83, 0x6e, 0xdb, 0x19, 0xac, 0x80, 0x35,
+ 0xf7, 0x42, 0x30, 0x85, 0x47, 0xf2, 0xde, 0x6b, 0xa9, 0x1c,
+ 0xf1, 0x44, 0x86, 0x33, 0x1f, 0xaa, 0x68, 0xdd, 0xca, 0x7f,
+ 0xbd, 0x08, 0x24, 0x91, 0x53, 0xe6, 0x0b, 0xbe, 0x7c, 0xc9,
+ 0xe5, 0x50, 0x92, 0x27, 0x55, 0xe0, 0x22, 0x97, 0xbb, 0x0e,
+ 0xcc, 0x79, 0x94, 0x21, 0xe3, 0x56, 0x7a, 0xcf, 0x0d, 0xb8,
+ 0xe9, 0x5c, 0x9e, 0x2b, 0x07, 0xb2, 0x70, 0xc5, 0x28, 0x9d,
+ 0x5f, 0xea, 0xc6, 0x73, 0xb1, 0x04, 0x76, 0xc3, 0x01, 0xb4,
+ 0x98, 0x2d, 0xef, 0x5a, 0xb7, 0x02, 0xc0, 0x75, 0x59, 0xec,
+ 0x2e, 0x9b, 0x00, 0xb6, 0x71, 0xc7, 0xe2, 0x54, 0x93, 0x25,
+ 0xd9, 0x6f, 0xa8, 0x1e, 0x3b, 0x8d, 0x4a, 0xfc, 0xaf, 0x19,
+ 0xde, 0x68, 0x4d, 0xfb, 0x3c, 0x8a, 0x76, 0xc0, 0x07, 0xb1,
+ 0x94, 0x22, 0xe5, 0x53, 0x43, 0xf5, 0x32, 0x84, 0xa1, 0x17,
+ 0xd0, 0x66, 0x9a, 0x2c, 0xeb, 0x5d, 0x78, 0xce, 0x09, 0xbf,
+ 0xec, 0x5a, 0x9d, 0x2b, 0x0e, 0xb8, 0x7f, 0xc9, 0x35, 0x83,
+ 0x44, 0xf2, 0xd7, 0x61, 0xa6, 0x10, 0x86, 0x30, 0xf7, 0x41,
+ 0x64, 0xd2, 0x15, 0xa3, 0x5f, 0xe9, 0x2e, 0x98, 0xbd, 0x0b,
+ 0xcc, 0x7a, 0x29, 0x9f, 0x58, 0xee, 0xcb, 0x7d, 0xba, 0x0c,
+ 0xf0, 0x46, 0x81, 0x37, 0x12, 0xa4, 0x63, 0xd5, 0xc5, 0x73,
+ 0xb4, 0x02, 0x27, 0x91, 0x56, 0xe0, 0x1c, 0xaa, 0x6d, 0xdb,
+ 0xfe, 0x48, 0x8f, 0x39, 0x6a, 0xdc, 0x1b, 0xad, 0x88, 0x3e,
+ 0xf9, 0x4f, 0xb3, 0x05, 0xc2, 0x74, 0x51, 0xe7, 0x20, 0x96,
+ 0x11, 0xa7, 0x60, 0xd6, 0xf3, 0x45, 0x82, 0x34, 0xc8, 0x7e,
+ 0xb9, 0x0f, 0x2a, 0x9c, 0x5b, 0xed, 0xbe, 0x08, 0xcf, 0x79,
+ 0x5c, 0xea, 0x2d, 0x9b, 0x67, 0xd1, 0x16, 0xa0, 0x85, 0x33,
+ 0xf4, 0x42, 0x52, 0xe4, 0x23, 0x95, 0xb0, 0x06, 0xc1, 0x77,
+ 0x8b, 0x3d, 0xfa, 0x4c, 0x69, 0xdf, 0x18, 0xae, 0xfd, 0x4b,
+ 0x8c, 0x3a, 0x1f, 0xa9, 0x6e, 0xd8, 0x24, 0x92, 0x55, 0xe3,
+ 0xc6, 0x70, 0xb7, 0x01, 0x97, 0x21, 0xe6, 0x50, 0x75, 0xc3,
+ 0x04, 0xb2, 0x4e, 0xf8, 0x3f, 0x89, 0xac, 0x1a, 0xdd, 0x6b,
+ 0x38, 0x8e, 0x49, 0xff, 0xda, 0x6c, 0xab, 0x1d, 0xe1, 0x57,
+ 0x90, 0x26, 0x03, 0xb5, 0x72, 0xc4, 0xd4, 0x62, 0xa5, 0x13,
+ 0x36, 0x80, 0x47, 0xf1, 0x0d, 0xbb, 0x7c, 0xca, 0xef, 0x59,
+ 0x9e, 0x28, 0x7b, 0xcd, 0x0a, 0xbc, 0x99, 0x2f, 0xe8, 0x5e,
+ 0xa2, 0x14, 0xd3, 0x65, 0x40, 0xf6, 0x31, 0x87, 0x00, 0xb7,
+ 0x73, 0xc4, 0xe6, 0x51, 0x95, 0x22, 0xd1, 0x66, 0xa2, 0x15,
+ 0x37, 0x80, 0x44, 0xf3, 0xbf, 0x08, 0xcc, 0x7b, 0x59, 0xee,
+ 0x2a, 0x9d, 0x6e, 0xd9, 0x1d, 0xaa, 0x88, 0x3f, 0xfb, 0x4c,
+ 0x63, 0xd4, 0x10, 0xa7, 0x85, 0x32, 0xf6, 0x41, 0xb2, 0x05,
+ 0xc1, 0x76, 0x54, 0xe3, 0x27, 0x90, 0xdc, 0x6b, 0xaf, 0x18,
+ 0x3a, 0x8d, 0x49, 0xfe, 0x0d, 0xba, 0x7e, 0xc9, 0xeb, 0x5c,
+ 0x98, 0x2f, 0xc6, 0x71, 0xb5, 0x02, 0x20, 0x97, 0x53, 0xe4,
+ 0x17, 0xa0, 0x64, 0xd3, 0xf1, 0x46, 0x82, 0x35, 0x79, 0xce,
+ 0x0a, 0xbd, 0x9f, 0x28, 0xec, 0x5b, 0xa8, 0x1f, 0xdb, 0x6c,
+ 0x4e, 0xf9, 0x3d, 0x8a, 0xa5, 0x12, 0xd6, 0x61, 0x43, 0xf4,
+ 0x30, 0x87, 0x74, 0xc3, 0x07, 0xb0, 0x92, 0x25, 0xe1, 0x56,
+ 0x1a, 0xad, 0x69, 0xde, 0xfc, 0x4b, 0x8f, 0x38, 0xcb, 0x7c,
+ 0xb8, 0x0f, 0x2d, 0x9a, 0x5e, 0xe9, 0x91, 0x26, 0xe2, 0x55,
+ 0x77, 0xc0, 0x04, 0xb3, 0x40, 0xf7, 0x33, 0x84, 0xa6, 0x11,
+ 0xd5, 0x62, 0x2e, 0x99, 0x5d, 0xea, 0xc8, 0x7f, 0xbb, 0x0c,
+ 0xff, 0x48, 0x8c, 0x3b, 0x19, 0xae, 0x6a, 0xdd, 0xf2, 0x45,
+ 0x81, 0x36, 0x14, 0xa3, 0x67, 0xd0, 0x23, 0x94, 0x50, 0xe7,
+ 0xc5, 0x72, 0xb6, 0x01, 0x4d, 0xfa, 0x3e, 0x89, 0xab, 0x1c,
+ 0xd8, 0x6f, 0x9c, 0x2b, 0xef, 0x58, 0x7a, 0xcd, 0x09, 0xbe,
+ 0x57, 0xe0, 0x24, 0x93, 0xb1, 0x06, 0xc2, 0x75, 0x86, 0x31,
+ 0xf5, 0x42, 0x60, 0xd7, 0x13, 0xa4, 0xe8, 0x5f, 0x9b, 0x2c,
+ 0x0e, 0xb9, 0x7d, 0xca, 0x39, 0x8e, 0x4a, 0xfd, 0xdf, 0x68,
+ 0xac, 0x1b, 0x34, 0x83, 0x47, 0xf0, 0xd2, 0x65, 0xa1, 0x16,
+ 0xe5, 0x52, 0x96, 0x21, 0x03, 0xb4, 0x70, 0xc7, 0x8b, 0x3c,
+ 0xf8, 0x4f, 0x6d, 0xda, 0x1e, 0xa9, 0x5a, 0xed, 0x29, 0x9e,
+ 0xbc, 0x0b, 0xcf, 0x78, 0x00, 0xb8, 0x6d, 0xd5, 0xda, 0x62,
+ 0xb7, 0x0f, 0xa9, 0x11, 0xc4, 0x7c, 0x73, 0xcb, 0x1e, 0xa6,
+ 0x4f, 0xf7, 0x22, 0x9a, 0x95, 0x2d, 0xf8, 0x40, 0xe6, 0x5e,
+ 0x8b, 0x33, 0x3c, 0x84, 0x51, 0xe9, 0x9e, 0x26, 0xf3, 0x4b,
+ 0x44, 0xfc, 0x29, 0x91, 0x37, 0x8f, 0x5a, 0xe2, 0xed, 0x55,
+ 0x80, 0x38, 0xd1, 0x69, 0xbc, 0x04, 0x0b, 0xb3, 0x66, 0xde,
+ 0x78, 0xc0, 0x15, 0xad, 0xa2, 0x1a, 0xcf, 0x77, 0x21, 0x99,
+ 0x4c, 0xf4, 0xfb, 0x43, 0x96, 0x2e, 0x88, 0x30, 0xe5, 0x5d,
+ 0x52, 0xea, 0x3f, 0x87, 0x6e, 0xd6, 0x03, 0xbb, 0xb4, 0x0c,
+ 0xd9, 0x61, 0xc7, 0x7f, 0xaa, 0x12, 0x1d, 0xa5, 0x70, 0xc8,
+ 0xbf, 0x07, 0xd2, 0x6a, 0x65, 0xdd, 0x08, 0xb0, 0x16, 0xae,
+ 0x7b, 0xc3, 0xcc, 0x74, 0xa1, 0x19, 0xf0, 0x48, 0x9d, 0x25,
+ 0x2a, 0x92, 0x47, 0xff, 0x59, 0xe1, 0x34, 0x8c, 0x83, 0x3b,
+ 0xee, 0x56, 0x42, 0xfa, 0x2f, 0x97, 0x98, 0x20, 0xf5, 0x4d,
+ 0xeb, 0x53, 0x86, 0x3e, 0x31, 0x89, 0x5c, 0xe4, 0x0d, 0xb5,
+ 0x60, 0xd8, 0xd7, 0x6f, 0xba, 0x02, 0xa4, 0x1c, 0xc9, 0x71,
+ 0x7e, 0xc6, 0x13, 0xab, 0xdc, 0x64, 0xb1, 0x09, 0x06, 0xbe,
+ 0x6b, 0xd3, 0x75, 0xcd, 0x18, 0xa0, 0xaf, 0x17, 0xc2, 0x7a,
+ 0x93, 0x2b, 0xfe, 0x46, 0x49, 0xf1, 0x24, 0x9c, 0x3a, 0x82,
+ 0x57, 0xef, 0xe0, 0x58, 0x8d, 0x35, 0x63, 0xdb, 0x0e, 0xb6,
+ 0xb9, 0x01, 0xd4, 0x6c, 0xca, 0x72, 0xa7, 0x1f, 0x10, 0xa8,
+ 0x7d, 0xc5, 0x2c, 0x94, 0x41, 0xf9, 0xf6, 0x4e, 0x9b, 0x23,
+ 0x85, 0x3d, 0xe8, 0x50, 0x5f, 0xe7, 0x32, 0x8a, 0xfd, 0x45,
+ 0x90, 0x28, 0x27, 0x9f, 0x4a, 0xf2, 0x54, 0xec, 0x39, 0x81,
+ 0x8e, 0x36, 0xe3, 0x5b, 0xb2, 0x0a, 0xdf, 0x67, 0x68, 0xd0,
+ 0x05, 0xbd, 0x1b, 0xa3, 0x76, 0xce, 0xc1, 0x79, 0xac, 0x14,
+ 0x00, 0xb9, 0x6f, 0xd6, 0xde, 0x67, 0xb1, 0x08, 0xa1, 0x18,
+ 0xce, 0x77, 0x7f, 0xc6, 0x10, 0xa9, 0x5f, 0xe6, 0x30, 0x89,
+ 0x81, 0x38, 0xee, 0x57, 0xfe, 0x47, 0x91, 0x28, 0x20, 0x99,
+ 0x4f, 0xf6, 0xbe, 0x07, 0xd1, 0x68, 0x60, 0xd9, 0x0f, 0xb6,
+ 0x1f, 0xa6, 0x70, 0xc9, 0xc1, 0x78, 0xae, 0x17, 0xe1, 0x58,
+ 0x8e, 0x37, 0x3f, 0x86, 0x50, 0xe9, 0x40, 0xf9, 0x2f, 0x96,
+ 0x9e, 0x27, 0xf1, 0x48, 0x61, 0xd8, 0x0e, 0xb7, 0xbf, 0x06,
+ 0xd0, 0x69, 0xc0, 0x79, 0xaf, 0x16, 0x1e, 0xa7, 0x71, 0xc8,
+ 0x3e, 0x87, 0x51, 0xe8, 0xe0, 0x59, 0x8f, 0x36, 0x9f, 0x26,
+ 0xf0, 0x49, 0x41, 0xf8, 0x2e, 0x97, 0xdf, 0x66, 0xb0, 0x09,
+ 0x01, 0xb8, 0x6e, 0xd7, 0x7e, 0xc7, 0x11, 0xa8, 0xa0, 0x19,
+ 0xcf, 0x76, 0x80, 0x39, 0xef, 0x56, 0x5e, 0xe7, 0x31, 0x88,
+ 0x21, 0x98, 0x4e, 0xf7, 0xff, 0x46, 0x90, 0x29, 0xc2, 0x7b,
+ 0xad, 0x14, 0x1c, 0xa5, 0x73, 0xca, 0x63, 0xda, 0x0c, 0xb5,
+ 0xbd, 0x04, 0xd2, 0x6b, 0x9d, 0x24, 0xf2, 0x4b, 0x43, 0xfa,
+ 0x2c, 0x95, 0x3c, 0x85, 0x53, 0xea, 0xe2, 0x5b, 0x8d, 0x34,
+ 0x7c, 0xc5, 0x13, 0xaa, 0xa2, 0x1b, 0xcd, 0x74, 0xdd, 0x64,
+ 0xb2, 0x0b, 0x03, 0xba, 0x6c, 0xd5, 0x23, 0x9a, 0x4c, 0xf5,
+ 0xfd, 0x44, 0x92, 0x2b, 0x82, 0x3b, 0xed, 0x54, 0x5c, 0xe5,
+ 0x33, 0x8a, 0xa3, 0x1a, 0xcc, 0x75, 0x7d, 0xc4, 0x12, 0xab,
+ 0x02, 0xbb, 0x6d, 0xd4, 0xdc, 0x65, 0xb3, 0x0a, 0xfc, 0x45,
+ 0x93, 0x2a, 0x22, 0x9b, 0x4d, 0xf4, 0x5d, 0xe4, 0x32, 0x8b,
+ 0x83, 0x3a, 0xec, 0x55, 0x1d, 0xa4, 0x72, 0xcb, 0xc3, 0x7a,
+ 0xac, 0x15, 0xbc, 0x05, 0xd3, 0x6a, 0x62, 0xdb, 0x0d, 0xb4,
+ 0x42, 0xfb, 0x2d, 0x94, 0x9c, 0x25, 0xf3, 0x4a, 0xe3, 0x5a,
+ 0x8c, 0x35, 0x3d, 0x84, 0x52, 0xeb, 0x00, 0xba, 0x69, 0xd3,
+ 0xd2, 0x68, 0xbb, 0x01, 0xb9, 0x03, 0xd0, 0x6a, 0x6b, 0xd1,
+ 0x02, 0xb8, 0x6f, 0xd5, 0x06, 0xbc, 0xbd, 0x07, 0xd4, 0x6e,
+ 0xd6, 0x6c, 0xbf, 0x05, 0x04, 0xbe, 0x6d, 0xd7, 0xde, 0x64,
+ 0xb7, 0x0d, 0x0c, 0xb6, 0x65, 0xdf, 0x67, 0xdd, 0x0e, 0xb4,
+ 0xb5, 0x0f, 0xdc, 0x66, 0xb1, 0x0b, 0xd8, 0x62, 0x63, 0xd9,
+ 0x0a, 0xb0, 0x08, 0xb2, 0x61, 0xdb, 0xda, 0x60, 0xb3, 0x09,
+ 0xa1, 0x1b, 0xc8, 0x72, 0x73, 0xc9, 0x1a, 0xa0, 0x18, 0xa2,
+ 0x71, 0xcb, 0xca, 0x70, 0xa3, 0x19, 0xce, 0x74, 0xa7, 0x1d,
+ 0x1c, 0xa6, 0x75, 0xcf, 0x77, 0xcd, 0x1e, 0xa4, 0xa5, 0x1f,
+ 0xcc, 0x76, 0x7f, 0xc5, 0x16, 0xac, 0xad, 0x17, 0xc4, 0x7e,
+ 0xc6, 0x7c, 0xaf, 0x15, 0x14, 0xae, 0x7d, 0xc7, 0x10, 0xaa,
+ 0x79, 0xc3, 0xc2, 0x78, 0xab, 0x11, 0xa9, 0x13, 0xc0, 0x7a,
+ 0x7b, 0xc1, 0x12, 0xa8, 0x5f, 0xe5, 0x36, 0x8c, 0x8d, 0x37,
+ 0xe4, 0x5e, 0xe6, 0x5c, 0x8f, 0x35, 0x34, 0x8e, 0x5d, 0xe7,
+ 0x30, 0x8a, 0x59, 0xe3, 0xe2, 0x58, 0x8b, 0x31, 0x89, 0x33,
+ 0xe0, 0x5a, 0x5b, 0xe1, 0x32, 0x88, 0x81, 0x3b, 0xe8, 0x52,
+ 0x53, 0xe9, 0x3a, 0x80, 0x38, 0x82, 0x51, 0xeb, 0xea, 0x50,
+ 0x83, 0x39, 0xee, 0x54, 0x87, 0x3d, 0x3c, 0x86, 0x55, 0xef,
+ 0x57, 0xed, 0x3e, 0x84, 0x85, 0x3f, 0xec, 0x56, 0xfe, 0x44,
+ 0x97, 0x2d, 0x2c, 0x96, 0x45, 0xff, 0x47, 0xfd, 0x2e, 0x94,
+ 0x95, 0x2f, 0xfc, 0x46, 0x91, 0x2b, 0xf8, 0x42, 0x43, 0xf9,
+ 0x2a, 0x90, 0x28, 0x92, 0x41, 0xfb, 0xfa, 0x40, 0x93, 0x29,
+ 0x20, 0x9a, 0x49, 0xf3, 0xf2, 0x48, 0x9b, 0x21, 0x99, 0x23,
+ 0xf0, 0x4a, 0x4b, 0xf1, 0x22, 0x98, 0x4f, 0xf5, 0x26, 0x9c,
+ 0x9d, 0x27, 0xf4, 0x4e, 0xf6, 0x4c, 0x9f, 0x25, 0x24, 0x9e,
+ 0x4d, 0xf7, 0x00, 0xbb, 0x6b, 0xd0, 0xd6, 0x6d, 0xbd, 0x06,
+ 0xb1, 0x0a, 0xda, 0x61, 0x67, 0xdc, 0x0c, 0xb7, 0x7f, 0xc4,
+ 0x14, 0xaf, 0xa9, 0x12, 0xc2, 0x79, 0xce, 0x75, 0xa5, 0x1e,
+ 0x18, 0xa3, 0x73, 0xc8, 0xfe, 0x45, 0x95, 0x2e, 0x28, 0x93,
+ 0x43, 0xf8, 0x4f, 0xf4, 0x24, 0x9f, 0x99, 0x22, 0xf2, 0x49,
+ 0x81, 0x3a, 0xea, 0x51, 0x57, 0xec, 0x3c, 0x87, 0x30, 0x8b,
+ 0x5b, 0xe0, 0xe6, 0x5d, 0x8d, 0x36, 0xe1, 0x5a, 0x8a, 0x31,
+ 0x37, 0x8c, 0x5c, 0xe7, 0x50, 0xeb, 0x3b, 0x80, 0x86, 0x3d,
+ 0xed, 0x56, 0x9e, 0x25, 0xf5, 0x4e, 0x48, 0xf3, 0x23, 0x98,
+ 0x2f, 0x94, 0x44, 0xff, 0xf9, 0x42, 0x92, 0x29, 0x1f, 0xa4,
+ 0x74, 0xcf, 0xc9, 0x72, 0xa2, 0x19, 0xae, 0x15, 0xc5, 0x7e,
+ 0x78, 0xc3, 0x13, 0xa8, 0x60, 0xdb, 0x0b, 0xb0, 0xb6, 0x0d,
+ 0xdd, 0x66, 0xd1, 0x6a, 0xba, 0x01, 0x07, 0xbc, 0x6c, 0xd7,
+ 0xdf, 0x64, 0xb4, 0x0f, 0x09, 0xb2, 0x62, 0xd9, 0x6e, 0xd5,
+ 0x05, 0xbe, 0xb8, 0x03, 0xd3, 0x68, 0xa0, 0x1b, 0xcb, 0x70,
+ 0x76, 0xcd, 0x1d, 0xa6, 0x11, 0xaa, 0x7a, 0xc1, 0xc7, 0x7c,
+ 0xac, 0x17, 0x21, 0x9a, 0x4a, 0xf1, 0xf7, 0x4c, 0x9c, 0x27,
+ 0x90, 0x2b, 0xfb, 0x40, 0x46, 0xfd, 0x2d, 0x96, 0x5e, 0xe5,
+ 0x35, 0x8e, 0x88, 0x33, 0xe3, 0x58, 0xef, 0x54, 0x84, 0x3f,
+ 0x39, 0x82, 0x52, 0xe9, 0x3e, 0x85, 0x55, 0xee, 0xe8, 0x53,
+ 0x83, 0x38, 0x8f, 0x34, 0xe4, 0x5f, 0x59, 0xe2, 0x32, 0x89,
+ 0x41, 0xfa, 0x2a, 0x91, 0x97, 0x2c, 0xfc, 0x47, 0xf0, 0x4b,
+ 0x9b, 0x20, 0x26, 0x9d, 0x4d, 0xf6, 0xc0, 0x7b, 0xab, 0x10,
+ 0x16, 0xad, 0x7d, 0xc6, 0x71, 0xca, 0x1a, 0xa1, 0xa7, 0x1c,
+ 0xcc, 0x77, 0xbf, 0x04, 0xd4, 0x6f, 0x69, 0xd2, 0x02, 0xb9,
+ 0x0e, 0xb5, 0x65, 0xde, 0xd8, 0x63, 0xb3, 0x08, 0x00, 0xbc,
+ 0x65, 0xd9, 0xca, 0x76, 0xaf, 0x13, 0x89, 0x35, 0xec, 0x50,
+ 0x43, 0xff, 0x26, 0x9a, 0x0f, 0xb3, 0x6a, 0xd6, 0xc5, 0x79,
+ 0xa0, 0x1c, 0x86, 0x3a, 0xe3, 0x5f, 0x4c, 0xf0, 0x29, 0x95,
+ 0x1e, 0xa2, 0x7b, 0xc7, 0xd4, 0x68, 0xb1, 0x0d, 0x97, 0x2b,
+ 0xf2, 0x4e, 0x5d, 0xe1, 0x38, 0x84, 0x11, 0xad, 0x74, 0xc8,
+ 0xdb, 0x67, 0xbe, 0x02, 0x98, 0x24, 0xfd, 0x41, 0x52, 0xee,
+ 0x37, 0x8b, 0x3c, 0x80, 0x59, 0xe5, 0xf6, 0x4a, 0x93, 0x2f,
+ 0xb5, 0x09, 0xd0, 0x6c, 0x7f, 0xc3, 0x1a, 0xa6, 0x33, 0x8f,
+ 0x56, 0xea, 0xf9, 0x45, 0x9c, 0x20, 0xba, 0x06, 0xdf, 0x63,
+ 0x70, 0xcc, 0x15, 0xa9, 0x22, 0x9e, 0x47, 0xfb, 0xe8, 0x54,
+ 0x8d, 0x31, 0xab, 0x17, 0xce, 0x72, 0x61, 0xdd, 0x04, 0xb8,
+ 0x2d, 0x91, 0x48, 0xf4, 0xe7, 0x5b, 0x82, 0x3e, 0xa4, 0x18,
+ 0xc1, 0x7d, 0x6e, 0xd2, 0x0b, 0xb7, 0x78, 0xc4, 0x1d, 0xa1,
+ 0xb2, 0x0e, 0xd7, 0x6b, 0xf1, 0x4d, 0x94, 0x28, 0x3b, 0x87,
+ 0x5e, 0xe2, 0x77, 0xcb, 0x12, 0xae, 0xbd, 0x01, 0xd8, 0x64,
+ 0xfe, 0x42, 0x9b, 0x27, 0x34, 0x88, 0x51, 0xed, 0x66, 0xda,
+ 0x03, 0xbf, 0xac, 0x10, 0xc9, 0x75, 0xef, 0x53, 0x8a, 0x36,
+ 0x25, 0x99, 0x40, 0xfc, 0x69, 0xd5, 0x0c, 0xb0, 0xa3, 0x1f,
+ 0xc6, 0x7a, 0xe0, 0x5c, 0x85, 0x39, 0x2a, 0x96, 0x4f, 0xf3,
+ 0x44, 0xf8, 0x21, 0x9d, 0x8e, 0x32, 0xeb, 0x57, 0xcd, 0x71,
+ 0xa8, 0x14, 0x07, 0xbb, 0x62, 0xde, 0x4b, 0xf7, 0x2e, 0x92,
+ 0x81, 0x3d, 0xe4, 0x58, 0xc2, 0x7e, 0xa7, 0x1b, 0x08, 0xb4,
+ 0x6d, 0xd1, 0x5a, 0xe6, 0x3f, 0x83, 0x90, 0x2c, 0xf5, 0x49,
+ 0xd3, 0x6f, 0xb6, 0x0a, 0x19, 0xa5, 0x7c, 0xc0, 0x55, 0xe9,
+ 0x30, 0x8c, 0x9f, 0x23, 0xfa, 0x46, 0xdc, 0x60, 0xb9, 0x05,
+ 0x16, 0xaa, 0x73, 0xcf, 0x00, 0xbd, 0x67, 0xda, 0xce, 0x73,
+ 0xa9, 0x14, 0x81, 0x3c, 0xe6, 0x5b, 0x4f, 0xf2, 0x28, 0x95,
+ 0x1f, 0xa2, 0x78, 0xc5, 0xd1, 0x6c, 0xb6, 0x0b, 0x9e, 0x23,
+ 0xf9, 0x44, 0x50, 0xed, 0x37, 0x8a, 0x3e, 0x83, 0x59, 0xe4,
+ 0xf0, 0x4d, 0x97, 0x2a, 0xbf, 0x02, 0xd8, 0x65, 0x71, 0xcc,
+ 0x16, 0xab, 0x21, 0x9c, 0x46, 0xfb, 0xef, 0x52, 0x88, 0x35,
+ 0xa0, 0x1d, 0xc7, 0x7a, 0x6e, 0xd3, 0x09, 0xb4, 0x7c, 0xc1,
+ 0x1b, 0xa6, 0xb2, 0x0f, 0xd5, 0x68, 0xfd, 0x40, 0x9a, 0x27,
+ 0x33, 0x8e, 0x54, 0xe9, 0x63, 0xde, 0x04, 0xb9, 0xad, 0x10,
+ 0xca, 0x77, 0xe2, 0x5f, 0x85, 0x38, 0x2c, 0x91, 0x4b, 0xf6,
+ 0x42, 0xff, 0x25, 0x98, 0x8c, 0x31, 0xeb, 0x56, 0xc3, 0x7e,
+ 0xa4, 0x19, 0x0d, 0xb0, 0x6a, 0xd7, 0x5d, 0xe0, 0x3a, 0x87,
+ 0x93, 0x2e, 0xf4, 0x49, 0xdc, 0x61, 0xbb, 0x06, 0x12, 0xaf,
+ 0x75, 0xc8, 0xf8, 0x45, 0x9f, 0x22, 0x36, 0x8b, 0x51, 0xec,
+ 0x79, 0xc4, 0x1e, 0xa3, 0xb7, 0x0a, 0xd0, 0x6d, 0xe7, 0x5a,
+ 0x80, 0x3d, 0x29, 0x94, 0x4e, 0xf3, 0x66, 0xdb, 0x01, 0xbc,
+ 0xa8, 0x15, 0xcf, 0x72, 0xc6, 0x7b, 0xa1, 0x1c, 0x08, 0xb5,
+ 0x6f, 0xd2, 0x47, 0xfa, 0x20, 0x9d, 0x89, 0x34, 0xee, 0x53,
+ 0xd9, 0x64, 0xbe, 0x03, 0x17, 0xaa, 0x70, 0xcd, 0x58, 0xe5,
+ 0x3f, 0x82, 0x96, 0x2b, 0xf1, 0x4c, 0x84, 0x39, 0xe3, 0x5e,
+ 0x4a, 0xf7, 0x2d, 0x90, 0x05, 0xb8, 0x62, 0xdf, 0xcb, 0x76,
+ 0xac, 0x11, 0x9b, 0x26, 0xfc, 0x41, 0x55, 0xe8, 0x32, 0x8f,
+ 0x1a, 0xa7, 0x7d, 0xc0, 0xd4, 0x69, 0xb3, 0x0e, 0xba, 0x07,
+ 0xdd, 0x60, 0x74, 0xc9, 0x13, 0xae, 0x3b, 0x86, 0x5c, 0xe1,
+ 0xf5, 0x48, 0x92, 0x2f, 0xa5, 0x18, 0xc2, 0x7f, 0x6b, 0xd6,
+ 0x0c, 0xb1, 0x24, 0x99, 0x43, 0xfe, 0xea, 0x57, 0x8d, 0x30,
+ 0x00, 0xbe, 0x61, 0xdf, 0xc2, 0x7c, 0xa3, 0x1d, 0x99, 0x27,
+ 0xf8, 0x46, 0x5b, 0xe5, 0x3a, 0x84, 0x2f, 0x91, 0x4e, 0xf0,
+ 0xed, 0x53, 0x8c, 0x32, 0xb6, 0x08, 0xd7, 0x69, 0x74, 0xca,
+ 0x15, 0xab, 0x5e, 0xe0, 0x3f, 0x81, 0x9c, 0x22, 0xfd, 0x43,
+ 0xc7, 0x79, 0xa6, 0x18, 0x05, 0xbb, 0x64, 0xda, 0x71, 0xcf,
+ 0x10, 0xae, 0xb3, 0x0d, 0xd2, 0x6c, 0xe8, 0x56, 0x89, 0x37,
+ 0x2a, 0x94, 0x4b, 0xf5, 0xbc, 0x02, 0xdd, 0x63, 0x7e, 0xc0,
+ 0x1f, 0xa1, 0x25, 0x9b, 0x44, 0xfa, 0xe7, 0x59, 0x86, 0x38,
+ 0x93, 0x2d, 0xf2, 0x4c, 0x51, 0xef, 0x30, 0x8e, 0x0a, 0xb4,
+ 0x6b, 0xd5, 0xc8, 0x76, 0xa9, 0x17, 0xe2, 0x5c, 0x83, 0x3d,
+ 0x20, 0x9e, 0x41, 0xff, 0x7b, 0xc5, 0x1a, 0xa4, 0xb9, 0x07,
+ 0xd8, 0x66, 0xcd, 0x73, 0xac, 0x12, 0x0f, 0xb1, 0x6e, 0xd0,
+ 0x54, 0xea, 0x35, 0x8b, 0x96, 0x28, 0xf7, 0x49, 0x65, 0xdb,
+ 0x04, 0xba, 0xa7, 0x19, 0xc6, 0x78, 0xfc, 0x42, 0x9d, 0x23,
+ 0x3e, 0x80, 0x5f, 0xe1, 0x4a, 0xf4, 0x2b, 0x95, 0x88, 0x36,
+ 0xe9, 0x57, 0xd3, 0x6d, 0xb2, 0x0c, 0x11, 0xaf, 0x70, 0xce,
+ 0x3b, 0x85, 0x5a, 0xe4, 0xf9, 0x47, 0x98, 0x26, 0xa2, 0x1c,
+ 0xc3, 0x7d, 0x60, 0xde, 0x01, 0xbf, 0x14, 0xaa, 0x75, 0xcb,
+ 0xd6, 0x68, 0xb7, 0x09, 0x8d, 0x33, 0xec, 0x52, 0x4f, 0xf1,
+ 0x2e, 0x90, 0xd9, 0x67, 0xb8, 0x06, 0x1b, 0xa5, 0x7a, 0xc4,
+ 0x40, 0xfe, 0x21, 0x9f, 0x82, 0x3c, 0xe3, 0x5d, 0xf6, 0x48,
+ 0x97, 0x29, 0x34, 0x8a, 0x55, 0xeb, 0x6f, 0xd1, 0x0e, 0xb0,
+ 0xad, 0x13, 0xcc, 0x72, 0x87, 0x39, 0xe6, 0x58, 0x45, 0xfb,
+ 0x24, 0x9a, 0x1e, 0xa0, 0x7f, 0xc1, 0xdc, 0x62, 0xbd, 0x03,
+ 0xa8, 0x16, 0xc9, 0x77, 0x6a, 0xd4, 0x0b, 0xb5, 0x31, 0x8f,
+ 0x50, 0xee, 0xf3, 0x4d, 0x92, 0x2c, 0x00, 0xbf, 0x63, 0xdc,
+ 0xc6, 0x79, 0xa5, 0x1a, 0x91, 0x2e, 0xf2, 0x4d, 0x57, 0xe8,
+ 0x34, 0x8b, 0x3f, 0x80, 0x5c, 0xe3, 0xf9, 0x46, 0x9a, 0x25,
+ 0xae, 0x11, 0xcd, 0x72, 0x68, 0xd7, 0x0b, 0xb4, 0x7e, 0xc1,
+ 0x1d, 0xa2, 0xb8, 0x07, 0xdb, 0x64, 0xef, 0x50, 0x8c, 0x33,
+ 0x29, 0x96, 0x4a, 0xf5, 0x41, 0xfe, 0x22, 0x9d, 0x87, 0x38,
+ 0xe4, 0x5b, 0xd0, 0x6f, 0xb3, 0x0c, 0x16, 0xa9, 0x75, 0xca,
+ 0xfc, 0x43, 0x9f, 0x20, 0x3a, 0x85, 0x59, 0xe6, 0x6d, 0xd2,
+ 0x0e, 0xb1, 0xab, 0x14, 0xc8, 0x77, 0xc3, 0x7c, 0xa0, 0x1f,
+ 0x05, 0xba, 0x66, 0xd9, 0x52, 0xed, 0x31, 0x8e, 0x94, 0x2b,
+ 0xf7, 0x48, 0x82, 0x3d, 0xe1, 0x5e, 0x44, 0xfb, 0x27, 0x98,
+ 0x13, 0xac, 0x70, 0xcf, 0xd5, 0x6a, 0xb6, 0x09, 0xbd, 0x02,
+ 0xde, 0x61, 0x7b, 0xc4, 0x18, 0xa7, 0x2c, 0x93, 0x4f, 0xf0,
+ 0xea, 0x55, 0x89, 0x36, 0xe5, 0x5a, 0x86, 0x39, 0x23, 0x9c,
+ 0x40, 0xff, 0x74, 0xcb, 0x17, 0xa8, 0xb2, 0x0d, 0xd1, 0x6e,
+ 0xda, 0x65, 0xb9, 0x06, 0x1c, 0xa3, 0x7f, 0xc0, 0x4b, 0xf4,
+ 0x28, 0x97, 0x8d, 0x32, 0xee, 0x51, 0x9b, 0x24, 0xf8, 0x47,
+ 0x5d, 0xe2, 0x3e, 0x81, 0x0a, 0xb5, 0x69, 0xd6, 0xcc, 0x73,
+ 0xaf, 0x10, 0xa4, 0x1b, 0xc7, 0x78, 0x62, 0xdd, 0x01, 0xbe,
+ 0x35, 0x8a, 0x56, 0xe9, 0xf3, 0x4c, 0x90, 0x2f, 0x19, 0xa6,
+ 0x7a, 0xc5, 0xdf, 0x60, 0xbc, 0x03, 0x88, 0x37, 0xeb, 0x54,
+ 0x4e, 0xf1, 0x2d, 0x92, 0x26, 0x99, 0x45, 0xfa, 0xe0, 0x5f,
+ 0x83, 0x3c, 0xb7, 0x08, 0xd4, 0x6b, 0x71, 0xce, 0x12, 0xad,
+ 0x67, 0xd8, 0x04, 0xbb, 0xa1, 0x1e, 0xc2, 0x7d, 0xf6, 0x49,
+ 0x95, 0x2a, 0x30, 0x8f, 0x53, 0xec, 0x58, 0xe7, 0x3b, 0x84,
+ 0x9e, 0x21, 0xfd, 0x42, 0xc9, 0x76, 0xaa, 0x15, 0x0f, 0xb0,
+ 0x6c, 0xd3, 0x00, 0xc0, 0x9d, 0x5d, 0x27, 0xe7, 0xba, 0x7a,
+ 0x4e, 0x8e, 0xd3, 0x13, 0x69, 0xa9, 0xf4, 0x34, 0x9c, 0x5c,
+ 0x01, 0xc1, 0xbb, 0x7b, 0x26, 0xe6, 0xd2, 0x12, 0x4f, 0x8f,
+ 0xf5, 0x35, 0x68, 0xa8, 0x25, 0xe5, 0xb8, 0x78, 0x02, 0xc2,
+ 0x9f, 0x5f, 0x6b, 0xab, 0xf6, 0x36, 0x4c, 0x8c, 0xd1, 0x11,
+ 0xb9, 0x79, 0x24, 0xe4, 0x9e, 0x5e, 0x03, 0xc3, 0xf7, 0x37,
+ 0x6a, 0xaa, 0xd0, 0x10, 0x4d, 0x8d, 0x4a, 0x8a, 0xd7, 0x17,
+ 0x6d, 0xad, 0xf0, 0x30, 0x04, 0xc4, 0x99, 0x59, 0x23, 0xe3,
+ 0xbe, 0x7e, 0xd6, 0x16, 0x4b, 0x8b, 0xf1, 0x31, 0x6c, 0xac,
+ 0x98, 0x58, 0x05, 0xc5, 0xbf, 0x7f, 0x22, 0xe2, 0x6f, 0xaf,
+ 0xf2, 0x32, 0x48, 0x88, 0xd5, 0x15, 0x21, 0xe1, 0xbc, 0x7c,
+ 0x06, 0xc6, 0x9b, 0x5b, 0xf3, 0x33, 0x6e, 0xae, 0xd4, 0x14,
+ 0x49, 0x89, 0xbd, 0x7d, 0x20, 0xe0, 0x9a, 0x5a, 0x07, 0xc7,
+ 0x94, 0x54, 0x09, 0xc9, 0xb3, 0x73, 0x2e, 0xee, 0xda, 0x1a,
+ 0x47, 0x87, 0xfd, 0x3d, 0x60, 0xa0, 0x08, 0xc8, 0x95, 0x55,
+ 0x2f, 0xef, 0xb2, 0x72, 0x46, 0x86, 0xdb, 0x1b, 0x61, 0xa1,
+ 0xfc, 0x3c, 0xb1, 0x71, 0x2c, 0xec, 0x96, 0x56, 0x0b, 0xcb,
+ 0xff, 0x3f, 0x62, 0xa2, 0xd8, 0x18, 0x45, 0x85, 0x2d, 0xed,
+ 0xb0, 0x70, 0x0a, 0xca, 0x97, 0x57, 0x63, 0xa3, 0xfe, 0x3e,
+ 0x44, 0x84, 0xd9, 0x19, 0xde, 0x1e, 0x43, 0x83, 0xf9, 0x39,
+ 0x64, 0xa4, 0x90, 0x50, 0x0d, 0xcd, 0xb7, 0x77, 0x2a, 0xea,
+ 0x42, 0x82, 0xdf, 0x1f, 0x65, 0xa5, 0xf8, 0x38, 0x0c, 0xcc,
+ 0x91, 0x51, 0x2b, 0xeb, 0xb6, 0x76, 0xfb, 0x3b, 0x66, 0xa6,
+ 0xdc, 0x1c, 0x41, 0x81, 0xb5, 0x75, 0x28, 0xe8, 0x92, 0x52,
+ 0x0f, 0xcf, 0x67, 0xa7, 0xfa, 0x3a, 0x40, 0x80, 0xdd, 0x1d,
+ 0x29, 0xe9, 0xb4, 0x74, 0x0e, 0xce, 0x93, 0x53, 0x00, 0xc1,
+ 0x9f, 0x5e, 0x23, 0xe2, 0xbc, 0x7d, 0x46, 0x87, 0xd9, 0x18,
+ 0x65, 0xa4, 0xfa, 0x3b, 0x8c, 0x4d, 0x13, 0xd2, 0xaf, 0x6e,
+ 0x30, 0xf1, 0xca, 0x0b, 0x55, 0x94, 0xe9, 0x28, 0x76, 0xb7,
+ 0x05, 0xc4, 0x9a, 0x5b, 0x26, 0xe7, 0xb9, 0x78, 0x43, 0x82,
+ 0xdc, 0x1d, 0x60, 0xa1, 0xff, 0x3e, 0x89, 0x48, 0x16, 0xd7,
+ 0xaa, 0x6b, 0x35, 0xf4, 0xcf, 0x0e, 0x50, 0x91, 0xec, 0x2d,
+ 0x73, 0xb2, 0x0a, 0xcb, 0x95, 0x54, 0x29, 0xe8, 0xb6, 0x77,
+ 0x4c, 0x8d, 0xd3, 0x12, 0x6f, 0xae, 0xf0, 0x31, 0x86, 0x47,
+ 0x19, 0xd8, 0xa5, 0x64, 0x3a, 0xfb, 0xc0, 0x01, 0x5f, 0x9e,
+ 0xe3, 0x22, 0x7c, 0xbd, 0x0f, 0xce, 0x90, 0x51, 0x2c, 0xed,
+ 0xb3, 0x72, 0x49, 0x88, 0xd6, 0x17, 0x6a, 0xab, 0xf5, 0x34,
+ 0x83, 0x42, 0x1c, 0xdd, 0xa0, 0x61, 0x3f, 0xfe, 0xc5, 0x04,
+ 0x5a, 0x9b, 0xe6, 0x27, 0x79, 0xb8, 0x14, 0xd5, 0x8b, 0x4a,
+ 0x37, 0xf6, 0xa8, 0x69, 0x52, 0x93, 0xcd, 0x0c, 0x71, 0xb0,
+ 0xee, 0x2f, 0x98, 0x59, 0x07, 0xc6, 0xbb, 0x7a, 0x24, 0xe5,
+ 0xde, 0x1f, 0x41, 0x80, 0xfd, 0x3c, 0x62, 0xa3, 0x11, 0xd0,
+ 0x8e, 0x4f, 0x32, 0xf3, 0xad, 0x6c, 0x57, 0x96, 0xc8, 0x09,
+ 0x74, 0xb5, 0xeb, 0x2a, 0x9d, 0x5c, 0x02, 0xc3, 0xbe, 0x7f,
+ 0x21, 0xe0, 0xdb, 0x1a, 0x44, 0x85, 0xf8, 0x39, 0x67, 0xa6,
+ 0x1e, 0xdf, 0x81, 0x40, 0x3d, 0xfc, 0xa2, 0x63, 0x58, 0x99,
+ 0xc7, 0x06, 0x7b, 0xba, 0xe4, 0x25, 0x92, 0x53, 0x0d, 0xcc,
+ 0xb1, 0x70, 0x2e, 0xef, 0xd4, 0x15, 0x4b, 0x8a, 0xf7, 0x36,
+ 0x68, 0xa9, 0x1b, 0xda, 0x84, 0x45, 0x38, 0xf9, 0xa7, 0x66,
+ 0x5d, 0x9c, 0xc2, 0x03, 0x7e, 0xbf, 0xe1, 0x20, 0x97, 0x56,
+ 0x08, 0xc9, 0xb4, 0x75, 0x2b, 0xea, 0xd1, 0x10, 0x4e, 0x8f,
+ 0xf2, 0x33, 0x6d, 0xac, 0x00, 0xc2, 0x99, 0x5b, 0x2f, 0xed,
+ 0xb6, 0x74, 0x5e, 0x9c, 0xc7, 0x05, 0x71, 0xb3, 0xe8, 0x2a,
+ 0xbc, 0x7e, 0x25, 0xe7, 0x93, 0x51, 0x0a, 0xc8, 0xe2, 0x20,
+ 0x7b, 0xb9, 0xcd, 0x0f, 0x54, 0x96, 0x65, 0xa7, 0xfc, 0x3e,
+ 0x4a, 0x88, 0xd3, 0x11, 0x3b, 0xf9, 0xa2, 0x60, 0x14, 0xd6,
+ 0x8d, 0x4f, 0xd9, 0x1b, 0x40, 0x82, 0xf6, 0x34, 0x6f, 0xad,
+ 0x87, 0x45, 0x1e, 0xdc, 0xa8, 0x6a, 0x31, 0xf3, 0xca, 0x08,
+ 0x53, 0x91, 0xe5, 0x27, 0x7c, 0xbe, 0x94, 0x56, 0x0d, 0xcf,
+ 0xbb, 0x79, 0x22, 0xe0, 0x76, 0xb4, 0xef, 0x2d, 0x59, 0x9b,
+ 0xc0, 0x02, 0x28, 0xea, 0xb1, 0x73, 0x07, 0xc5, 0x9e, 0x5c,
+ 0xaf, 0x6d, 0x36, 0xf4, 0x80, 0x42, 0x19, 0xdb, 0xf1, 0x33,
+ 0x68, 0xaa, 0xde, 0x1c, 0x47, 0x85, 0x13, 0xd1, 0x8a, 0x48,
+ 0x3c, 0xfe, 0xa5, 0x67, 0x4d, 0x8f, 0xd4, 0x16, 0x62, 0xa0,
+ 0xfb, 0x39, 0x89, 0x4b, 0x10, 0xd2, 0xa6, 0x64, 0x3f, 0xfd,
+ 0xd7, 0x15, 0x4e, 0x8c, 0xf8, 0x3a, 0x61, 0xa3, 0x35, 0xf7,
+ 0xac, 0x6e, 0x1a, 0xd8, 0x83, 0x41, 0x6b, 0xa9, 0xf2, 0x30,
+ 0x44, 0x86, 0xdd, 0x1f, 0xec, 0x2e, 0x75, 0xb7, 0xc3, 0x01,
+ 0x5a, 0x98, 0xb2, 0x70, 0x2b, 0xe9, 0x9d, 0x5f, 0x04, 0xc6,
+ 0x50, 0x92, 0xc9, 0x0b, 0x7f, 0xbd, 0xe6, 0x24, 0x0e, 0xcc,
+ 0x97, 0x55, 0x21, 0xe3, 0xb8, 0x7a, 0x43, 0x81, 0xda, 0x18,
+ 0x6c, 0xae, 0xf5, 0x37, 0x1d, 0xdf, 0x84, 0x46, 0x32, 0xf0,
+ 0xab, 0x69, 0xff, 0x3d, 0x66, 0xa4, 0xd0, 0x12, 0x49, 0x8b,
+ 0xa1, 0x63, 0x38, 0xfa, 0x8e, 0x4c, 0x17, 0xd5, 0x26, 0xe4,
+ 0xbf, 0x7d, 0x09, 0xcb, 0x90, 0x52, 0x78, 0xba, 0xe1, 0x23,
+ 0x57, 0x95, 0xce, 0x0c, 0x9a, 0x58, 0x03, 0xc1, 0xb5, 0x77,
+ 0x2c, 0xee, 0xc4, 0x06, 0x5d, 0x9f, 0xeb, 0x29, 0x72, 0xb0,
+ 0x00, 0xc3, 0x9b, 0x58, 0x2b, 0xe8, 0xb0, 0x73, 0x56, 0x95,
+ 0xcd, 0x0e, 0x7d, 0xbe, 0xe6, 0x25, 0xac, 0x6f, 0x37, 0xf4,
+ 0x87, 0x44, 0x1c, 0xdf, 0xfa, 0x39, 0x61, 0xa2, 0xd1, 0x12,
+ 0x4a, 0x89, 0x45, 0x86, 0xde, 0x1d, 0x6e, 0xad, 0xf5, 0x36,
+ 0x13, 0xd0, 0x88, 0x4b, 0x38, 0xfb, 0xa3, 0x60, 0xe9, 0x2a,
+ 0x72, 0xb1, 0xc2, 0x01, 0x59, 0x9a, 0xbf, 0x7c, 0x24, 0xe7,
+ 0x94, 0x57, 0x0f, 0xcc, 0x8a, 0x49, 0x11, 0xd2, 0xa1, 0x62,
+ 0x3a, 0xf9, 0xdc, 0x1f, 0x47, 0x84, 0xf7, 0x34, 0x6c, 0xaf,
+ 0x26, 0xe5, 0xbd, 0x7e, 0x0d, 0xce, 0x96, 0x55, 0x70, 0xb3,
+ 0xeb, 0x28, 0x5b, 0x98, 0xc0, 0x03, 0xcf, 0x0c, 0x54, 0x97,
+ 0xe4, 0x27, 0x7f, 0xbc, 0x99, 0x5a, 0x02, 0xc1, 0xb2, 0x71,
+ 0x29, 0xea, 0x63, 0xa0, 0xf8, 0x3b, 0x48, 0x8b, 0xd3, 0x10,
+ 0x35, 0xf6, 0xae, 0x6d, 0x1e, 0xdd, 0x85, 0x46, 0x09, 0xca,
+ 0x92, 0x51, 0x22, 0xe1, 0xb9, 0x7a, 0x5f, 0x9c, 0xc4, 0x07,
+ 0x74, 0xb7, 0xef, 0x2c, 0xa5, 0x66, 0x3e, 0xfd, 0x8e, 0x4d,
+ 0x15, 0xd6, 0xf3, 0x30, 0x68, 0xab, 0xd8, 0x1b, 0x43, 0x80,
+ 0x4c, 0x8f, 0xd7, 0x14, 0x67, 0xa4, 0xfc, 0x3f, 0x1a, 0xd9,
+ 0x81, 0x42, 0x31, 0xf2, 0xaa, 0x69, 0xe0, 0x23, 0x7b, 0xb8,
+ 0xcb, 0x08, 0x50, 0x93, 0xb6, 0x75, 0x2d, 0xee, 0x9d, 0x5e,
+ 0x06, 0xc5, 0x83, 0x40, 0x18, 0xdb, 0xa8, 0x6b, 0x33, 0xf0,
+ 0xd5, 0x16, 0x4e, 0x8d, 0xfe, 0x3d, 0x65, 0xa6, 0x2f, 0xec,
+ 0xb4, 0x77, 0x04, 0xc7, 0x9f, 0x5c, 0x79, 0xba, 0xe2, 0x21,
+ 0x52, 0x91, 0xc9, 0x0a, 0xc6, 0x05, 0x5d, 0x9e, 0xed, 0x2e,
+ 0x76, 0xb5, 0x90, 0x53, 0x0b, 0xc8, 0xbb, 0x78, 0x20, 0xe3,
+ 0x6a, 0xa9, 0xf1, 0x32, 0x41, 0x82, 0xda, 0x19, 0x3c, 0xff,
+ 0xa7, 0x64, 0x17, 0xd4, 0x8c, 0x4f, 0x00, 0xc4, 0x95, 0x51,
+ 0x37, 0xf3, 0xa2, 0x66, 0x6e, 0xaa, 0xfb, 0x3f, 0x59, 0x9d,
+ 0xcc, 0x08, 0xdc, 0x18, 0x49, 0x8d, 0xeb, 0x2f, 0x7e, 0xba,
+ 0xb2, 0x76, 0x27, 0xe3, 0x85, 0x41, 0x10, 0xd4, 0xa5, 0x61,
+ 0x30, 0xf4, 0x92, 0x56, 0x07, 0xc3, 0xcb, 0x0f, 0x5e, 0x9a,
+ 0xfc, 0x38, 0x69, 0xad, 0x79, 0xbd, 0xec, 0x28, 0x4e, 0x8a,
+ 0xdb, 0x1f, 0x17, 0xd3, 0x82, 0x46, 0x20, 0xe4, 0xb5, 0x71,
+ 0x57, 0x93, 0xc2, 0x06, 0x60, 0xa4, 0xf5, 0x31, 0x39, 0xfd,
+ 0xac, 0x68, 0x0e, 0xca, 0x9b, 0x5f, 0x8b, 0x4f, 0x1e, 0xda,
+ 0xbc, 0x78, 0x29, 0xed, 0xe5, 0x21, 0x70, 0xb4, 0xd2, 0x16,
+ 0x47, 0x83, 0xf2, 0x36, 0x67, 0xa3, 0xc5, 0x01, 0x50, 0x94,
+ 0x9c, 0x58, 0x09, 0xcd, 0xab, 0x6f, 0x3e, 0xfa, 0x2e, 0xea,
+ 0xbb, 0x7f, 0x19, 0xdd, 0x8c, 0x48, 0x40, 0x84, 0xd5, 0x11,
+ 0x77, 0xb3, 0xe2, 0x26, 0xae, 0x6a, 0x3b, 0xff, 0x99, 0x5d,
+ 0x0c, 0xc8, 0xc0, 0x04, 0x55, 0x91, 0xf7, 0x33, 0x62, 0xa6,
+ 0x72, 0xb6, 0xe7, 0x23, 0x45, 0x81, 0xd0, 0x14, 0x1c, 0xd8,
+ 0x89, 0x4d, 0x2b, 0xef, 0xbe, 0x7a, 0x0b, 0xcf, 0x9e, 0x5a,
+ 0x3c, 0xf8, 0xa9, 0x6d, 0x65, 0xa1, 0xf0, 0x34, 0x52, 0x96,
+ 0xc7, 0x03, 0xd7, 0x13, 0x42, 0x86, 0xe0, 0x24, 0x75, 0xb1,
+ 0xb9, 0x7d, 0x2c, 0xe8, 0x8e, 0x4a, 0x1b, 0xdf, 0xf9, 0x3d,
+ 0x6c, 0xa8, 0xce, 0x0a, 0x5b, 0x9f, 0x97, 0x53, 0x02, 0xc6,
+ 0xa0, 0x64, 0x35, 0xf1, 0x25, 0xe1, 0xb0, 0x74, 0x12, 0xd6,
+ 0x87, 0x43, 0x4b, 0x8f, 0xde, 0x1a, 0x7c, 0xb8, 0xe9, 0x2d,
+ 0x5c, 0x98, 0xc9, 0x0d, 0x6b, 0xaf, 0xfe, 0x3a, 0x32, 0xf6,
+ 0xa7, 0x63, 0x05, 0xc1, 0x90, 0x54, 0x80, 0x44, 0x15, 0xd1,
+ 0xb7, 0x73, 0x22, 0xe6, 0xee, 0x2a, 0x7b, 0xbf, 0xd9, 0x1d,
+ 0x4c, 0x88, 0x00, 0xc5, 0x97, 0x52, 0x33, 0xf6, 0xa4, 0x61,
+ 0x66, 0xa3, 0xf1, 0x34, 0x55, 0x90, 0xc2, 0x07, 0xcc, 0x09,
+ 0x5b, 0x9e, 0xff, 0x3a, 0x68, 0xad, 0xaa, 0x6f, 0x3d, 0xf8,
+ 0x99, 0x5c, 0x0e, 0xcb, 0x85, 0x40, 0x12, 0xd7, 0xb6, 0x73,
+ 0x21, 0xe4, 0xe3, 0x26, 0x74, 0xb1, 0xd0, 0x15, 0x47, 0x82,
+ 0x49, 0x8c, 0xde, 0x1b, 0x7a, 0xbf, 0xed, 0x28, 0x2f, 0xea,
+ 0xb8, 0x7d, 0x1c, 0xd9, 0x8b, 0x4e, 0x17, 0xd2, 0x80, 0x45,
+ 0x24, 0xe1, 0xb3, 0x76, 0x71, 0xb4, 0xe6, 0x23, 0x42, 0x87,
+ 0xd5, 0x10, 0xdb, 0x1e, 0x4c, 0x89, 0xe8, 0x2d, 0x7f, 0xba,
+ 0xbd, 0x78, 0x2a, 0xef, 0x8e, 0x4b, 0x19, 0xdc, 0x92, 0x57,
+ 0x05, 0xc0, 0xa1, 0x64, 0x36, 0xf3, 0xf4, 0x31, 0x63, 0xa6,
+ 0xc7, 0x02, 0x50, 0x95, 0x5e, 0x9b, 0xc9, 0x0c, 0x6d, 0xa8,
+ 0xfa, 0x3f, 0x38, 0xfd, 0xaf, 0x6a, 0x0b, 0xce, 0x9c, 0x59,
+ 0x2e, 0xeb, 0xb9, 0x7c, 0x1d, 0xd8, 0x8a, 0x4f, 0x48, 0x8d,
+ 0xdf, 0x1a, 0x7b, 0xbe, 0xec, 0x29, 0xe2, 0x27, 0x75, 0xb0,
+ 0xd1, 0x14, 0x46, 0x83, 0x84, 0x41, 0x13, 0xd6, 0xb7, 0x72,
+ 0x20, 0xe5, 0xab, 0x6e, 0x3c, 0xf9, 0x98, 0x5d, 0x0f, 0xca,
+ 0xcd, 0x08, 0x5a, 0x9f, 0xfe, 0x3b, 0x69, 0xac, 0x67, 0xa2,
+ 0xf0, 0x35, 0x54, 0x91, 0xc3, 0x06, 0x01, 0xc4, 0x96, 0x53,
+ 0x32, 0xf7, 0xa5, 0x60, 0x39, 0xfc, 0xae, 0x6b, 0x0a, 0xcf,
+ 0x9d, 0x58, 0x5f, 0x9a, 0xc8, 0x0d, 0x6c, 0xa9, 0xfb, 0x3e,
+ 0xf5, 0x30, 0x62, 0xa7, 0xc6, 0x03, 0x51, 0x94, 0x93, 0x56,
+ 0x04, 0xc1, 0xa0, 0x65, 0x37, 0xf2, 0xbc, 0x79, 0x2b, 0xee,
+ 0x8f, 0x4a, 0x18, 0xdd, 0xda, 0x1f, 0x4d, 0x88, 0xe9, 0x2c,
+ 0x7e, 0xbb, 0x70, 0xb5, 0xe7, 0x22, 0x43, 0x86, 0xd4, 0x11,
+ 0x16, 0xd3, 0x81, 0x44, 0x25, 0xe0, 0xb2, 0x77, 0x00, 0xc6,
+ 0x91, 0x57, 0x3f, 0xf9, 0xae, 0x68, 0x7e, 0xb8, 0xef, 0x29,
+ 0x41, 0x87, 0xd0, 0x16, 0xfc, 0x3a, 0x6d, 0xab, 0xc3, 0x05,
+ 0x52, 0x94, 0x82, 0x44, 0x13, 0xd5, 0xbd, 0x7b, 0x2c, 0xea,
+ 0xe5, 0x23, 0x74, 0xb2, 0xda, 0x1c, 0x4b, 0x8d, 0x9b, 0x5d,
+ 0x0a, 0xcc, 0xa4, 0x62, 0x35, 0xf3, 0x19, 0xdf, 0x88, 0x4e,
+ 0x26, 0xe0, 0xb7, 0x71, 0x67, 0xa1, 0xf6, 0x30, 0x58, 0x9e,
+ 0xc9, 0x0f, 0xd7, 0x11, 0x46, 0x80, 0xe8, 0x2e, 0x79, 0xbf,
+ 0xa9, 0x6f, 0x38, 0xfe, 0x96, 0x50, 0x07, 0xc1, 0x2b, 0xed,
+ 0xba, 0x7c, 0x14, 0xd2, 0x85, 0x43, 0x55, 0x93, 0xc4, 0x02,
+ 0x6a, 0xac, 0xfb, 0x3d, 0x32, 0xf4, 0xa3, 0x65, 0x0d, 0xcb,
+ 0x9c, 0x5a, 0x4c, 0x8a, 0xdd, 0x1b, 0x73, 0xb5, 0xe2, 0x24,
+ 0xce, 0x08, 0x5f, 0x99, 0xf1, 0x37, 0x60, 0xa6, 0xb0, 0x76,
+ 0x21, 0xe7, 0x8f, 0x49, 0x1e, 0xd8, 0xb3, 0x75, 0x22, 0xe4,
+ 0x8c, 0x4a, 0x1d, 0xdb, 0xcd, 0x0b, 0x5c, 0x9a, 0xf2, 0x34,
+ 0x63, 0xa5, 0x4f, 0x89, 0xde, 0x18, 0x70, 0xb6, 0xe1, 0x27,
+ 0x31, 0xf7, 0xa0, 0x66, 0x0e, 0xc8, 0x9f, 0x59, 0x56, 0x90,
+ 0xc7, 0x01, 0x69, 0xaf, 0xf8, 0x3e, 0x28, 0xee, 0xb9, 0x7f,
+ 0x17, 0xd1, 0x86, 0x40, 0xaa, 0x6c, 0x3b, 0xfd, 0x95, 0x53,
+ 0x04, 0xc2, 0xd4, 0x12, 0x45, 0x83, 0xeb, 0x2d, 0x7a, 0xbc,
+ 0x64, 0xa2, 0xf5, 0x33, 0x5b, 0x9d, 0xca, 0x0c, 0x1a, 0xdc,
+ 0x8b, 0x4d, 0x25, 0xe3, 0xb4, 0x72, 0x98, 0x5e, 0x09, 0xcf,
+ 0xa7, 0x61, 0x36, 0xf0, 0xe6, 0x20, 0x77, 0xb1, 0xd9, 0x1f,
+ 0x48, 0x8e, 0x81, 0x47, 0x10, 0xd6, 0xbe, 0x78, 0x2f, 0xe9,
+ 0xff, 0x39, 0x6e, 0xa8, 0xc0, 0x06, 0x51, 0x97, 0x7d, 0xbb,
+ 0xec, 0x2a, 0x42, 0x84, 0xd3, 0x15, 0x03, 0xc5, 0x92, 0x54,
+ 0x3c, 0xfa, 0xad, 0x6b, 0x00, 0xc7, 0x93, 0x54, 0x3b, 0xfc,
+ 0xa8, 0x6f, 0x76, 0xb1, 0xe5, 0x22, 0x4d, 0x8a, 0xde, 0x19,
+ 0xec, 0x2b, 0x7f, 0xb8, 0xd7, 0x10, 0x44, 0x83, 0x9a, 0x5d,
+ 0x09, 0xce, 0xa1, 0x66, 0x32, 0xf5, 0xc5, 0x02, 0x56, 0x91,
+ 0xfe, 0x39, 0x6d, 0xaa, 0xb3, 0x74, 0x20, 0xe7, 0x88, 0x4f,
+ 0x1b, 0xdc, 0x29, 0xee, 0xba, 0x7d, 0x12, 0xd5, 0x81, 0x46,
+ 0x5f, 0x98, 0xcc, 0x0b, 0x64, 0xa3, 0xf7, 0x30, 0x97, 0x50,
+ 0x04, 0xc3, 0xac, 0x6b, 0x3f, 0xf8, 0xe1, 0x26, 0x72, 0xb5,
+ 0xda, 0x1d, 0x49, 0x8e, 0x7b, 0xbc, 0xe8, 0x2f, 0x40, 0x87,
+ 0xd3, 0x14, 0x0d, 0xca, 0x9e, 0x59, 0x36, 0xf1, 0xa5, 0x62,
+ 0x52, 0x95, 0xc1, 0x06, 0x69, 0xae, 0xfa, 0x3d, 0x24, 0xe3,
+ 0xb7, 0x70, 0x1f, 0xd8, 0x8c, 0x4b, 0xbe, 0x79, 0x2d, 0xea,
+ 0x85, 0x42, 0x16, 0xd1, 0xc8, 0x0f, 0x5b, 0x9c, 0xf3, 0x34,
+ 0x60, 0xa7, 0x33, 0xf4, 0xa0, 0x67, 0x08, 0xcf, 0x9b, 0x5c,
+ 0x45, 0x82, 0xd6, 0x11, 0x7e, 0xb9, 0xed, 0x2a, 0xdf, 0x18,
+ 0x4c, 0x8b, 0xe4, 0x23, 0x77, 0xb0, 0xa9, 0x6e, 0x3a, 0xfd,
+ 0x92, 0x55, 0x01, 0xc6, 0xf6, 0x31, 0x65, 0xa2, 0xcd, 0x0a,
+ 0x5e, 0x99, 0x80, 0x47, 0x13, 0xd4, 0xbb, 0x7c, 0x28, 0xef,
+ 0x1a, 0xdd, 0x89, 0x4e, 0x21, 0xe6, 0xb2, 0x75, 0x6c, 0xab,
+ 0xff, 0x38, 0x57, 0x90, 0xc4, 0x03, 0xa4, 0x63, 0x37, 0xf0,
+ 0x9f, 0x58, 0x0c, 0xcb, 0xd2, 0x15, 0x41, 0x86, 0xe9, 0x2e,
+ 0x7a, 0xbd, 0x48, 0x8f, 0xdb, 0x1c, 0x73, 0xb4, 0xe0, 0x27,
+ 0x3e, 0xf9, 0xad, 0x6a, 0x05, 0xc2, 0x96, 0x51, 0x61, 0xa6,
+ 0xf2, 0x35, 0x5a, 0x9d, 0xc9, 0x0e, 0x17, 0xd0, 0x84, 0x43,
+ 0x2c, 0xeb, 0xbf, 0x78, 0x8d, 0x4a, 0x1e, 0xd9, 0xb6, 0x71,
+ 0x25, 0xe2, 0xfb, 0x3c, 0x68, 0xaf, 0xc0, 0x07, 0x53, 0x94,
+ 0x00, 0xc8, 0x8d, 0x45, 0x07, 0xcf, 0x8a, 0x42, 0x0e, 0xc6,
+ 0x83, 0x4b, 0x09, 0xc1, 0x84, 0x4c, 0x1c, 0xd4, 0x91, 0x59,
+ 0x1b, 0xd3, 0x96, 0x5e, 0x12, 0xda, 0x9f, 0x57, 0x15, 0xdd,
+ 0x98, 0x50, 0x38, 0xf0, 0xb5, 0x7d, 0x3f, 0xf7, 0xb2, 0x7a,
+ 0x36, 0xfe, 0xbb, 0x73, 0x31, 0xf9, 0xbc, 0x74, 0x24, 0xec,
+ 0xa9, 0x61, 0x23, 0xeb, 0xae, 0x66, 0x2a, 0xe2, 0xa7, 0x6f,
+ 0x2d, 0xe5, 0xa0, 0x68, 0x70, 0xb8, 0xfd, 0x35, 0x77, 0xbf,
+ 0xfa, 0x32, 0x7e, 0xb6, 0xf3, 0x3b, 0x79, 0xb1, 0xf4, 0x3c,
+ 0x6c, 0xa4, 0xe1, 0x29, 0x6b, 0xa3, 0xe6, 0x2e, 0x62, 0xaa,
+ 0xef, 0x27, 0x65, 0xad, 0xe8, 0x20, 0x48, 0x80, 0xc5, 0x0d,
+ 0x4f, 0x87, 0xc2, 0x0a, 0x46, 0x8e, 0xcb, 0x03, 0x41, 0x89,
+ 0xcc, 0x04, 0x54, 0x9c, 0xd9, 0x11, 0x53, 0x9b, 0xde, 0x16,
+ 0x5a, 0x92, 0xd7, 0x1f, 0x5d, 0x95, 0xd0, 0x18, 0xe0, 0x28,
+ 0x6d, 0xa5, 0xe7, 0x2f, 0x6a, 0xa2, 0xee, 0x26, 0x63, 0xab,
+ 0xe9, 0x21, 0x64, 0xac, 0xfc, 0x34, 0x71, 0xb9, 0xfb, 0x33,
+ 0x76, 0xbe, 0xf2, 0x3a, 0x7f, 0xb7, 0xf5, 0x3d, 0x78, 0xb0,
+ 0xd8, 0x10, 0x55, 0x9d, 0xdf, 0x17, 0x52, 0x9a, 0xd6, 0x1e,
+ 0x5b, 0x93, 0xd1, 0x19, 0x5c, 0x94, 0xc4, 0x0c, 0x49, 0x81,
+ 0xc3, 0x0b, 0x4e, 0x86, 0xca, 0x02, 0x47, 0x8f, 0xcd, 0x05,
+ 0x40, 0x88, 0x90, 0x58, 0x1d, 0xd5, 0x97, 0x5f, 0x1a, 0xd2,
+ 0x9e, 0x56, 0x13, 0xdb, 0x99, 0x51, 0x14, 0xdc, 0x8c, 0x44,
+ 0x01, 0xc9, 0x8b, 0x43, 0x06, 0xce, 0x82, 0x4a, 0x0f, 0xc7,
+ 0x85, 0x4d, 0x08, 0xc0, 0xa8, 0x60, 0x25, 0xed, 0xaf, 0x67,
+ 0x22, 0xea, 0xa6, 0x6e, 0x2b, 0xe3, 0xa1, 0x69, 0x2c, 0xe4,
+ 0xb4, 0x7c, 0x39, 0xf1, 0xb3, 0x7b, 0x3e, 0xf6, 0xba, 0x72,
+ 0x37, 0xff, 0xbd, 0x75, 0x30, 0xf8, 0x00, 0xc9, 0x8f, 0x46,
+ 0x03, 0xca, 0x8c, 0x45, 0x06, 0xcf, 0x89, 0x40, 0x05, 0xcc,
+ 0x8a, 0x43, 0x0c, 0xc5, 0x83, 0x4a, 0x0f, 0xc6, 0x80, 0x49,
+ 0x0a, 0xc3, 0x85, 0x4c, 0x09, 0xc0, 0x86, 0x4f, 0x18, 0xd1,
+ 0x97, 0x5e, 0x1b, 0xd2, 0x94, 0x5d, 0x1e, 0xd7, 0x91, 0x58,
+ 0x1d, 0xd4, 0x92, 0x5b, 0x14, 0xdd, 0x9b, 0x52, 0x17, 0xde,
+ 0x98, 0x51, 0x12, 0xdb, 0x9d, 0x54, 0x11, 0xd8, 0x9e, 0x57,
+ 0x30, 0xf9, 0xbf, 0x76, 0x33, 0xfa, 0xbc, 0x75, 0x36, 0xff,
+ 0xb9, 0x70, 0x35, 0xfc, 0xba, 0x73, 0x3c, 0xf5, 0xb3, 0x7a,
+ 0x3f, 0xf6, 0xb0, 0x79, 0x3a, 0xf3, 0xb5, 0x7c, 0x39, 0xf0,
+ 0xb6, 0x7f, 0x28, 0xe1, 0xa7, 0x6e, 0x2b, 0xe2, 0xa4, 0x6d,
+ 0x2e, 0xe7, 0xa1, 0x68, 0x2d, 0xe4, 0xa2, 0x6b, 0x24, 0xed,
+ 0xab, 0x62, 0x27, 0xee, 0xa8, 0x61, 0x22, 0xeb, 0xad, 0x64,
+ 0x21, 0xe8, 0xae, 0x67, 0x60, 0xa9, 0xef, 0x26, 0x63, 0xaa,
+ 0xec, 0x25, 0x66, 0xaf, 0xe9, 0x20, 0x65, 0xac, 0xea, 0x23,
+ 0x6c, 0xa5, 0xe3, 0x2a, 0x6f, 0xa6, 0xe0, 0x29, 0x6a, 0xa3,
+ 0xe5, 0x2c, 0x69, 0xa0, 0xe6, 0x2f, 0x78, 0xb1, 0xf7, 0x3e,
+ 0x7b, 0xb2, 0xf4, 0x3d, 0x7e, 0xb7, 0xf1, 0x38, 0x7d, 0xb4,
+ 0xf2, 0x3b, 0x74, 0xbd, 0xfb, 0x32, 0x77, 0xbe, 0xf8, 0x31,
+ 0x72, 0xbb, 0xfd, 0x34, 0x71, 0xb8, 0xfe, 0x37, 0x50, 0x99,
+ 0xdf, 0x16, 0x53, 0x9a, 0xdc, 0x15, 0x56, 0x9f, 0xd9, 0x10,
+ 0x55, 0x9c, 0xda, 0x13, 0x5c, 0x95, 0xd3, 0x1a, 0x5f, 0x96,
+ 0xd0, 0x19, 0x5a, 0x93, 0xd5, 0x1c, 0x59, 0x90, 0xd6, 0x1f,
+ 0x48, 0x81, 0xc7, 0x0e, 0x4b, 0x82, 0xc4, 0x0d, 0x4e, 0x87,
+ 0xc1, 0x08, 0x4d, 0x84, 0xc2, 0x0b, 0x44, 0x8d, 0xcb, 0x02,
+ 0x47, 0x8e, 0xc8, 0x01, 0x42, 0x8b, 0xcd, 0x04, 0x41, 0x88,
+ 0xce, 0x07, 0x00, 0xca, 0x89, 0x43, 0x0f, 0xc5, 0x86, 0x4c,
+ 0x1e, 0xd4, 0x97, 0x5d, 0x11, 0xdb, 0x98, 0x52, 0x3c, 0xf6,
+ 0xb5, 0x7f, 0x33, 0xf9, 0xba, 0x70, 0x22, 0xe8, 0xab, 0x61,
+ 0x2d, 0xe7, 0xa4, 0x6e, 0x78, 0xb2, 0xf1, 0x3b, 0x77, 0xbd,
+ 0xfe, 0x34, 0x66, 0xac, 0xef, 0x25, 0x69, 0xa3, 0xe0, 0x2a,
+ 0x44, 0x8e, 0xcd, 0x07, 0x4b, 0x81, 0xc2, 0x08, 0x5a, 0x90,
+ 0xd3, 0x19, 0x55, 0x9f, 0xdc, 0x16, 0xf0, 0x3a, 0x79, 0xb3,
+ 0xff, 0x35, 0x76, 0xbc, 0xee, 0x24, 0x67, 0xad, 0xe1, 0x2b,
+ 0x68, 0xa2, 0xcc, 0x06, 0x45, 0x8f, 0xc3, 0x09, 0x4a, 0x80,
+ 0xd2, 0x18, 0x5b, 0x91, 0xdd, 0x17, 0x54, 0x9e, 0x88, 0x42,
+ 0x01, 0xcb, 0x87, 0x4d, 0x0e, 0xc4, 0x96, 0x5c, 0x1f, 0xd5,
+ 0x99, 0x53, 0x10, 0xda, 0xb4, 0x7e, 0x3d, 0xf7, 0xbb, 0x71,
+ 0x32, 0xf8, 0xaa, 0x60, 0x23, 0xe9, 0xa5, 0x6f, 0x2c, 0xe6,
+ 0xfd, 0x37, 0x74, 0xbe, 0xf2, 0x38, 0x7b, 0xb1, 0xe3, 0x29,
+ 0x6a, 0xa0, 0xec, 0x26, 0x65, 0xaf, 0xc1, 0x0b, 0x48, 0x82,
+ 0xce, 0x04, 0x47, 0x8d, 0xdf, 0x15, 0x56, 0x9c, 0xd0, 0x1a,
+ 0x59, 0x93, 0x85, 0x4f, 0x0c, 0xc6, 0x8a, 0x40, 0x03, 0xc9,
+ 0x9b, 0x51, 0x12, 0xd8, 0x94, 0x5e, 0x1d, 0xd7, 0xb9, 0x73,
+ 0x30, 0xfa, 0xb6, 0x7c, 0x3f, 0xf5, 0xa7, 0x6d, 0x2e, 0xe4,
+ 0xa8, 0x62, 0x21, 0xeb, 0x0d, 0xc7, 0x84, 0x4e, 0x02, 0xc8,
+ 0x8b, 0x41, 0x13, 0xd9, 0x9a, 0x50, 0x1c, 0xd6, 0x95, 0x5f,
+ 0x31, 0xfb, 0xb8, 0x72, 0x3e, 0xf4, 0xb7, 0x7d, 0x2f, 0xe5,
+ 0xa6, 0x6c, 0x20, 0xea, 0xa9, 0x63, 0x75, 0xbf, 0xfc, 0x36,
+ 0x7a, 0xb0, 0xf3, 0x39, 0x6b, 0xa1, 0xe2, 0x28, 0x64, 0xae,
+ 0xed, 0x27, 0x49, 0x83, 0xc0, 0x0a, 0x46, 0x8c, 0xcf, 0x05,
+ 0x57, 0x9d, 0xde, 0x14, 0x58, 0x92, 0xd1, 0x1b, 0x00, 0xcb,
+ 0x8b, 0x40, 0x0b, 0xc0, 0x80, 0x4b, 0x16, 0xdd, 0x9d, 0x56,
+ 0x1d, 0xd6, 0x96, 0x5d, 0x2c, 0xe7, 0xa7, 0x6c, 0x27, 0xec,
+ 0xac, 0x67, 0x3a, 0xf1, 0xb1, 0x7a, 0x31, 0xfa, 0xba, 0x71,
+ 0x58, 0x93, 0xd3, 0x18, 0x53, 0x98, 0xd8, 0x13, 0x4e, 0x85,
+ 0xc5, 0x0e, 0x45, 0x8e, 0xce, 0x05, 0x74, 0xbf, 0xff, 0x34,
+ 0x7f, 0xb4, 0xf4, 0x3f, 0x62, 0xa9, 0xe9, 0x22, 0x69, 0xa2,
+ 0xe2, 0x29, 0xb0, 0x7b, 0x3b, 0xf0, 0xbb, 0x70, 0x30, 0xfb,
+ 0xa6, 0x6d, 0x2d, 0xe6, 0xad, 0x66, 0x26, 0xed, 0x9c, 0x57,
+ 0x17, 0xdc, 0x97, 0x5c, 0x1c, 0xd7, 0x8a, 0x41, 0x01, 0xca,
+ 0x81, 0x4a, 0x0a, 0xc1, 0xe8, 0x23, 0x63, 0xa8, 0xe3, 0x28,
+ 0x68, 0xa3, 0xfe, 0x35, 0x75, 0xbe, 0xf5, 0x3e, 0x7e, 0xb5,
+ 0xc4, 0x0f, 0x4f, 0x84, 0xcf, 0x04, 0x44, 0x8f, 0xd2, 0x19,
+ 0x59, 0x92, 0xd9, 0x12, 0x52, 0x99, 0x7d, 0xb6, 0xf6, 0x3d,
+ 0x76, 0xbd, 0xfd, 0x36, 0x6b, 0xa0, 0xe0, 0x2b, 0x60, 0xab,
+ 0xeb, 0x20, 0x51, 0x9a, 0xda, 0x11, 0x5a, 0x91, 0xd1, 0x1a,
+ 0x47, 0x8c, 0xcc, 0x07, 0x4c, 0x87, 0xc7, 0x0c, 0x25, 0xee,
+ 0xae, 0x65, 0x2e, 0xe5, 0xa5, 0x6e, 0x33, 0xf8, 0xb8, 0x73,
+ 0x38, 0xf3, 0xb3, 0x78, 0x09, 0xc2, 0x82, 0x49, 0x02, 0xc9,
+ 0x89, 0x42, 0x1f, 0xd4, 0x94, 0x5f, 0x14, 0xdf, 0x9f, 0x54,
+ 0xcd, 0x06, 0x46, 0x8d, 0xc6, 0x0d, 0x4d, 0x86, 0xdb, 0x10,
+ 0x50, 0x9b, 0xd0, 0x1b, 0x5b, 0x90, 0xe1, 0x2a, 0x6a, 0xa1,
+ 0xea, 0x21, 0x61, 0xaa, 0xf7, 0x3c, 0x7c, 0xb7, 0xfc, 0x37,
+ 0x77, 0xbc, 0x95, 0x5e, 0x1e, 0xd5, 0x9e, 0x55, 0x15, 0xde,
+ 0x83, 0x48, 0x08, 0xc3, 0x88, 0x43, 0x03, 0xc8, 0xb9, 0x72,
+ 0x32, 0xf9, 0xb2, 0x79, 0x39, 0xf2, 0xaf, 0x64, 0x24, 0xef,
+ 0xa4, 0x6f, 0x2f, 0xe4, 0x00, 0xcc, 0x85, 0x49, 0x17, 0xdb,
+ 0x92, 0x5e, 0x2e, 0xe2, 0xab, 0x67, 0x39, 0xf5, 0xbc, 0x70,
+ 0x5c, 0x90, 0xd9, 0x15, 0x4b, 0x87, 0xce, 0x02, 0x72, 0xbe,
+ 0xf7, 0x3b, 0x65, 0xa9, 0xe0, 0x2c, 0xb8, 0x74, 0x3d, 0xf1,
+ 0xaf, 0x63, 0x2a, 0xe6, 0x96, 0x5a, 0x13, 0xdf, 0x81, 0x4d,
+ 0x04, 0xc8, 0xe4, 0x28, 0x61, 0xad, 0xf3, 0x3f, 0x76, 0xba,
+ 0xca, 0x06, 0x4f, 0x83, 0xdd, 0x11, 0x58, 0x94, 0x6d, 0xa1,
+ 0xe8, 0x24, 0x7a, 0xb6, 0xff, 0x33, 0x43, 0x8f, 0xc6, 0x0a,
+ 0x54, 0x98, 0xd1, 0x1d, 0x31, 0xfd, 0xb4, 0x78, 0x26, 0xea,
+ 0xa3, 0x6f, 0x1f, 0xd3, 0x9a, 0x56, 0x08, 0xc4, 0x8d, 0x41,
+ 0xd5, 0x19, 0x50, 0x9c, 0xc2, 0x0e, 0x47, 0x8b, 0xfb, 0x37,
+ 0x7e, 0xb2, 0xec, 0x20, 0x69, 0xa5, 0x89, 0x45, 0x0c, 0xc0,
+ 0x9e, 0x52, 0x1b, 0xd7, 0xa7, 0x6b, 0x22, 0xee, 0xb0, 0x7c,
+ 0x35, 0xf9, 0xda, 0x16, 0x5f, 0x93, 0xcd, 0x01, 0x48, 0x84,
+ 0xf4, 0x38, 0x71, 0xbd, 0xe3, 0x2f, 0x66, 0xaa, 0x86, 0x4a,
+ 0x03, 0xcf, 0x91, 0x5d, 0x14, 0xd8, 0xa8, 0x64, 0x2d, 0xe1,
+ 0xbf, 0x73, 0x3a, 0xf6, 0x62, 0xae, 0xe7, 0x2b, 0x75, 0xb9,
+ 0xf0, 0x3c, 0x4c, 0x80, 0xc9, 0x05, 0x5b, 0x97, 0xde, 0x12,
+ 0x3e, 0xf2, 0xbb, 0x77, 0x29, 0xe5, 0xac, 0x60, 0x10, 0xdc,
+ 0x95, 0x59, 0x07, 0xcb, 0x82, 0x4e, 0xb7, 0x7b, 0x32, 0xfe,
+ 0xa0, 0x6c, 0x25, 0xe9, 0x99, 0x55, 0x1c, 0xd0, 0x8e, 0x42,
+ 0x0b, 0xc7, 0xeb, 0x27, 0x6e, 0xa2, 0xfc, 0x30, 0x79, 0xb5,
+ 0xc5, 0x09, 0x40, 0x8c, 0xd2, 0x1e, 0x57, 0x9b, 0x0f, 0xc3,
+ 0x8a, 0x46, 0x18, 0xd4, 0x9d, 0x51, 0x21, 0xed, 0xa4, 0x68,
+ 0x36, 0xfa, 0xb3, 0x7f, 0x53, 0x9f, 0xd6, 0x1a, 0x44, 0x88,
+ 0xc1, 0x0d, 0x7d, 0xb1, 0xf8, 0x34, 0x6a, 0xa6, 0xef, 0x23,
+ 0x00, 0xcd, 0x87, 0x4a, 0x13, 0xde, 0x94, 0x59, 0x26, 0xeb,
+ 0xa1, 0x6c, 0x35, 0xf8, 0xb2, 0x7f, 0x4c, 0x81, 0xcb, 0x06,
+ 0x5f, 0x92, 0xd8, 0x15, 0x6a, 0xa7, 0xed, 0x20, 0x79, 0xb4,
+ 0xfe, 0x33, 0x98, 0x55, 0x1f, 0xd2, 0x8b, 0x46, 0x0c, 0xc1,
+ 0xbe, 0x73, 0x39, 0xf4, 0xad, 0x60, 0x2a, 0xe7, 0xd4, 0x19,
+ 0x53, 0x9e, 0xc7, 0x0a, 0x40, 0x8d, 0xf2, 0x3f, 0x75, 0xb8,
+ 0xe1, 0x2c, 0x66, 0xab, 0x2d, 0xe0, 0xaa, 0x67, 0x3e, 0xf3,
+ 0xb9, 0x74, 0x0b, 0xc6, 0x8c, 0x41, 0x18, 0xd5, 0x9f, 0x52,
+ 0x61, 0xac, 0xe6, 0x2b, 0x72, 0xbf, 0xf5, 0x38, 0x47, 0x8a,
+ 0xc0, 0x0d, 0x54, 0x99, 0xd3, 0x1e, 0xb5, 0x78, 0x32, 0xff,
+ 0xa6, 0x6b, 0x21, 0xec, 0x93, 0x5e, 0x14, 0xd9, 0x80, 0x4d,
+ 0x07, 0xca, 0xf9, 0x34, 0x7e, 0xb3, 0xea, 0x27, 0x6d, 0xa0,
+ 0xdf, 0x12, 0x58, 0x95, 0xcc, 0x01, 0x4b, 0x86, 0x5a, 0x97,
+ 0xdd, 0x10, 0x49, 0x84, 0xce, 0x03, 0x7c, 0xb1, 0xfb, 0x36,
+ 0x6f, 0xa2, 0xe8, 0x25, 0x16, 0xdb, 0x91, 0x5c, 0x05, 0xc8,
+ 0x82, 0x4f, 0x30, 0xfd, 0xb7, 0x7a, 0x23, 0xee, 0xa4, 0x69,
+ 0xc2, 0x0f, 0x45, 0x88, 0xd1, 0x1c, 0x56, 0x9b, 0xe4, 0x29,
+ 0x63, 0xae, 0xf7, 0x3a, 0x70, 0xbd, 0x8e, 0x43, 0x09, 0xc4,
+ 0x9d, 0x50, 0x1a, 0xd7, 0xa8, 0x65, 0x2f, 0xe2, 0xbb, 0x76,
+ 0x3c, 0xf1, 0x77, 0xba, 0xf0, 0x3d, 0x64, 0xa9, 0xe3, 0x2e,
+ 0x51, 0x9c, 0xd6, 0x1b, 0x42, 0x8f, 0xc5, 0x08, 0x3b, 0xf6,
+ 0xbc, 0x71, 0x28, 0xe5, 0xaf, 0x62, 0x1d, 0xd0, 0x9a, 0x57,
+ 0x0e, 0xc3, 0x89, 0x44, 0xef, 0x22, 0x68, 0xa5, 0xfc, 0x31,
+ 0x7b, 0xb6, 0xc9, 0x04, 0x4e, 0x83, 0xda, 0x17, 0x5d, 0x90,
+ 0xa3, 0x6e, 0x24, 0xe9, 0xb0, 0x7d, 0x37, 0xfa, 0x85, 0x48,
+ 0x02, 0xcf, 0x96, 0x5b, 0x11, 0xdc, 0x00, 0xce, 0x81, 0x4f,
+ 0x1f, 0xd1, 0x9e, 0x50, 0x3e, 0xf0, 0xbf, 0x71, 0x21, 0xef,
+ 0xa0, 0x6e, 0x7c, 0xb2, 0xfd, 0x33, 0x63, 0xad, 0xe2, 0x2c,
+ 0x42, 0x8c, 0xc3, 0x0d, 0x5d, 0x93, 0xdc, 0x12, 0xf8, 0x36,
+ 0x79, 0xb7, 0xe7, 0x29, 0x66, 0xa8, 0xc6, 0x08, 0x47, 0x89,
+ 0xd9, 0x17, 0x58, 0x96, 0x84, 0x4a, 0x05, 0xcb, 0x9b, 0x55,
+ 0x1a, 0xd4, 0xba, 0x74, 0x3b, 0xf5, 0xa5, 0x6b, 0x24, 0xea,
+ 0xed, 0x23, 0x6c, 0xa2, 0xf2, 0x3c, 0x73, 0xbd, 0xd3, 0x1d,
+ 0x52, 0x9c, 0xcc, 0x02, 0x4d, 0x83, 0x91, 0x5f, 0x10, 0xde,
+ 0x8e, 0x40, 0x0f, 0xc1, 0xaf, 0x61, 0x2e, 0xe0, 0xb0, 0x7e,
+ 0x31, 0xff, 0x15, 0xdb, 0x94, 0x5a, 0x0a, 0xc4, 0x8b, 0x45,
+ 0x2b, 0xe5, 0xaa, 0x64, 0x34, 0xfa, 0xb5, 0x7b, 0x69, 0xa7,
+ 0xe8, 0x26, 0x76, 0xb8, 0xf7, 0x39, 0x57, 0x99, 0xd6, 0x18,
+ 0x48, 0x86, 0xc9, 0x07, 0xc7, 0x09, 0x46, 0x88, 0xd8, 0x16,
+ 0x59, 0x97, 0xf9, 0x37, 0x78, 0xb6, 0xe6, 0x28, 0x67, 0xa9,
+ 0xbb, 0x75, 0x3a, 0xf4, 0xa4, 0x6a, 0x25, 0xeb, 0x85, 0x4b,
+ 0x04, 0xca, 0x9a, 0x54, 0x1b, 0xd5, 0x3f, 0xf1, 0xbe, 0x70,
+ 0x20, 0xee, 0xa1, 0x6f, 0x01, 0xcf, 0x80, 0x4e, 0x1e, 0xd0,
+ 0x9f, 0x51, 0x43, 0x8d, 0xc2, 0x0c, 0x5c, 0x92, 0xdd, 0x13,
+ 0x7d, 0xb3, 0xfc, 0x32, 0x62, 0xac, 0xe3, 0x2d, 0x2a, 0xe4,
+ 0xab, 0x65, 0x35, 0xfb, 0xb4, 0x7a, 0x14, 0xda, 0x95, 0x5b,
+ 0x0b, 0xc5, 0x8a, 0x44, 0x56, 0x98, 0xd7, 0x19, 0x49, 0x87,
+ 0xc8, 0x06, 0x68, 0xa6, 0xe9, 0x27, 0x77, 0xb9, 0xf6, 0x38,
+ 0xd2, 0x1c, 0x53, 0x9d, 0xcd, 0x03, 0x4c, 0x82, 0xec, 0x22,
+ 0x6d, 0xa3, 0xf3, 0x3d, 0x72, 0xbc, 0xae, 0x60, 0x2f, 0xe1,
+ 0xb1, 0x7f, 0x30, 0xfe, 0x90, 0x5e, 0x11, 0xdf, 0x8f, 0x41,
+ 0x0e, 0xc0, 0x00, 0xcf, 0x83, 0x4c, 0x1b, 0xd4, 0x98, 0x57,
+ 0x36, 0xf9, 0xb5, 0x7a, 0x2d, 0xe2, 0xae, 0x61, 0x6c, 0xa3,
+ 0xef, 0x20, 0x77, 0xb8, 0xf4, 0x3b, 0x5a, 0x95, 0xd9, 0x16,
+ 0x41, 0x8e, 0xc2, 0x0d, 0xd8, 0x17, 0x5b, 0x94, 0xc3, 0x0c,
+ 0x40, 0x8f, 0xee, 0x21, 0x6d, 0xa2, 0xf5, 0x3a, 0x76, 0xb9,
+ 0xb4, 0x7b, 0x37, 0xf8, 0xaf, 0x60, 0x2c, 0xe3, 0x82, 0x4d,
+ 0x01, 0xce, 0x99, 0x56, 0x1a, 0xd5, 0xad, 0x62, 0x2e, 0xe1,
+ 0xb6, 0x79, 0x35, 0xfa, 0x9b, 0x54, 0x18, 0xd7, 0x80, 0x4f,
+ 0x03, 0xcc, 0xc1, 0x0e, 0x42, 0x8d, 0xda, 0x15, 0x59, 0x96,
+ 0xf7, 0x38, 0x74, 0xbb, 0xec, 0x23, 0x6f, 0xa0, 0x75, 0xba,
+ 0xf6, 0x39, 0x6e, 0xa1, 0xed, 0x22, 0x43, 0x8c, 0xc0, 0x0f,
+ 0x58, 0x97, 0xdb, 0x14, 0x19, 0xd6, 0x9a, 0x55, 0x02, 0xcd,
+ 0x81, 0x4e, 0x2f, 0xe0, 0xac, 0x63, 0x34, 0xfb, 0xb7, 0x78,
+ 0x47, 0x88, 0xc4, 0x0b, 0x5c, 0x93, 0xdf, 0x10, 0x71, 0xbe,
+ 0xf2, 0x3d, 0x6a, 0xa5, 0xe9, 0x26, 0x2b, 0xe4, 0xa8, 0x67,
+ 0x30, 0xff, 0xb3, 0x7c, 0x1d, 0xd2, 0x9e, 0x51, 0x06, 0xc9,
+ 0x85, 0x4a, 0x9f, 0x50, 0x1c, 0xd3, 0x84, 0x4b, 0x07, 0xc8,
+ 0xa9, 0x66, 0x2a, 0xe5, 0xb2, 0x7d, 0x31, 0xfe, 0xf3, 0x3c,
+ 0x70, 0xbf, 0xe8, 0x27, 0x6b, 0xa4, 0xc5, 0x0a, 0x46, 0x89,
+ 0xde, 0x11, 0x5d, 0x92, 0xea, 0x25, 0x69, 0xa6, 0xf1, 0x3e,
+ 0x72, 0xbd, 0xdc, 0x13, 0x5f, 0x90, 0xc7, 0x08, 0x44, 0x8b,
+ 0x86, 0x49, 0x05, 0xca, 0x9d, 0x52, 0x1e, 0xd1, 0xb0, 0x7f,
+ 0x33, 0xfc, 0xab, 0x64, 0x28, 0xe7, 0x32, 0xfd, 0xb1, 0x7e,
+ 0x29, 0xe6, 0xaa, 0x65, 0x04, 0xcb, 0x87, 0x48, 0x1f, 0xd0,
+ 0x9c, 0x53, 0x5e, 0x91, 0xdd, 0x12, 0x45, 0x8a, 0xc6, 0x09,
+ 0x68, 0xa7, 0xeb, 0x24, 0x73, 0xbc, 0xf0, 0x3f, 0x00, 0xd0,
+ 0xbd, 0x6d, 0x67, 0xb7, 0xda, 0x0a, 0xce, 0x1e, 0x73, 0xa3,
+ 0xa9, 0x79, 0x14, 0xc4, 0x81, 0x51, 0x3c, 0xec, 0xe6, 0x36,
+ 0x5b, 0x8b, 0x4f, 0x9f, 0xf2, 0x22, 0x28, 0xf8, 0x95, 0x45,
+ 0x1f, 0xcf, 0xa2, 0x72, 0x78, 0xa8, 0xc5, 0x15, 0xd1, 0x01,
+ 0x6c, 0xbc, 0xb6, 0x66, 0x0b, 0xdb, 0x9e, 0x4e, 0x23, 0xf3,
+ 0xf9, 0x29, 0x44, 0x94, 0x50, 0x80, 0xed, 0x3d, 0x37, 0xe7,
+ 0x8a, 0x5a, 0x3e, 0xee, 0x83, 0x53, 0x59, 0x89, 0xe4, 0x34,
+ 0xf0, 0x20, 0x4d, 0x9d, 0x97, 0x47, 0x2a, 0xfa, 0xbf, 0x6f,
+ 0x02, 0xd2, 0xd8, 0x08, 0x65, 0xb5, 0x71, 0xa1, 0xcc, 0x1c,
+ 0x16, 0xc6, 0xab, 0x7b, 0x21, 0xf1, 0x9c, 0x4c, 0x46, 0x96,
+ 0xfb, 0x2b, 0xef, 0x3f, 0x52, 0x82, 0x88, 0x58, 0x35, 0xe5,
+ 0xa0, 0x70, 0x1d, 0xcd, 0xc7, 0x17, 0x7a, 0xaa, 0x6e, 0xbe,
+ 0xd3, 0x03, 0x09, 0xd9, 0xb4, 0x64, 0x7c, 0xac, 0xc1, 0x11,
+ 0x1b, 0xcb, 0xa6, 0x76, 0xb2, 0x62, 0x0f, 0xdf, 0xd5, 0x05,
+ 0x68, 0xb8, 0xfd, 0x2d, 0x40, 0x90, 0x9a, 0x4a, 0x27, 0xf7,
+ 0x33, 0xe3, 0x8e, 0x5e, 0x54, 0x84, 0xe9, 0x39, 0x63, 0xb3,
+ 0xde, 0x0e, 0x04, 0xd4, 0xb9, 0x69, 0xad, 0x7d, 0x10, 0xc0,
+ 0xca, 0x1a, 0x77, 0xa7, 0xe2, 0x32, 0x5f, 0x8f, 0x85, 0x55,
+ 0x38, 0xe8, 0x2c, 0xfc, 0x91, 0x41, 0x4b, 0x9b, 0xf6, 0x26,
+ 0x42, 0x92, 0xff, 0x2f, 0x25, 0xf5, 0x98, 0x48, 0x8c, 0x5c,
+ 0x31, 0xe1, 0xeb, 0x3b, 0x56, 0x86, 0xc3, 0x13, 0x7e, 0xae,
+ 0xa4, 0x74, 0x19, 0xc9, 0x0d, 0xdd, 0xb0, 0x60, 0x6a, 0xba,
+ 0xd7, 0x07, 0x5d, 0x8d, 0xe0, 0x30, 0x3a, 0xea, 0x87, 0x57,
+ 0x93, 0x43, 0x2e, 0xfe, 0xf4, 0x24, 0x49, 0x99, 0xdc, 0x0c,
+ 0x61, 0xb1, 0xbb, 0x6b, 0x06, 0xd6, 0x12, 0xc2, 0xaf, 0x7f,
+ 0x75, 0xa5, 0xc8, 0x18, 0x00, 0xd1, 0xbf, 0x6e, 0x63, 0xb2,
+ 0xdc, 0x0d, 0xc6, 0x17, 0x79, 0xa8, 0xa5, 0x74, 0x1a, 0xcb,
+ 0x91, 0x40, 0x2e, 0xff, 0xf2, 0x23, 0x4d, 0x9c, 0x57, 0x86,
+ 0xe8, 0x39, 0x34, 0xe5, 0x8b, 0x5a, 0x3f, 0xee, 0x80, 0x51,
+ 0x5c, 0x8d, 0xe3, 0x32, 0xf9, 0x28, 0x46, 0x97, 0x9a, 0x4b,
+ 0x25, 0xf4, 0xae, 0x7f, 0x11, 0xc0, 0xcd, 0x1c, 0x72, 0xa3,
+ 0x68, 0xb9, 0xd7, 0x06, 0x0b, 0xda, 0xb4, 0x65, 0x7e, 0xaf,
+ 0xc1, 0x10, 0x1d, 0xcc, 0xa2, 0x73, 0xb8, 0x69, 0x07, 0xd6,
+ 0xdb, 0x0a, 0x64, 0xb5, 0xef, 0x3e, 0x50, 0x81, 0x8c, 0x5d,
+ 0x33, 0xe2, 0x29, 0xf8, 0x96, 0x47, 0x4a, 0x9b, 0xf5, 0x24,
+ 0x41, 0x90, 0xfe, 0x2f, 0x22, 0xf3, 0x9d, 0x4c, 0x87, 0x56,
+ 0x38, 0xe9, 0xe4, 0x35, 0x5b, 0x8a, 0xd0, 0x01, 0x6f, 0xbe,
+ 0xb3, 0x62, 0x0c, 0xdd, 0x16, 0xc7, 0xa9, 0x78, 0x75, 0xa4,
+ 0xca, 0x1b, 0xfc, 0x2d, 0x43, 0x92, 0x9f, 0x4e, 0x20, 0xf1,
+ 0x3a, 0xeb, 0x85, 0x54, 0x59, 0x88, 0xe6, 0x37, 0x6d, 0xbc,
+ 0xd2, 0x03, 0x0e, 0xdf, 0xb1, 0x60, 0xab, 0x7a, 0x14, 0xc5,
+ 0xc8, 0x19, 0x77, 0xa6, 0xc3, 0x12, 0x7c, 0xad, 0xa0, 0x71,
+ 0x1f, 0xce, 0x05, 0xd4, 0xba, 0x6b, 0x66, 0xb7, 0xd9, 0x08,
+ 0x52, 0x83, 0xed, 0x3c, 0x31, 0xe0, 0x8e, 0x5f, 0x94, 0x45,
+ 0x2b, 0xfa, 0xf7, 0x26, 0x48, 0x99, 0x82, 0x53, 0x3d, 0xec,
+ 0xe1, 0x30, 0x5e, 0x8f, 0x44, 0x95, 0xfb, 0x2a, 0x27, 0xf6,
+ 0x98, 0x49, 0x13, 0xc2, 0xac, 0x7d, 0x70, 0xa1, 0xcf, 0x1e,
+ 0xd5, 0x04, 0x6a, 0xbb, 0xb6, 0x67, 0x09, 0xd8, 0xbd, 0x6c,
+ 0x02, 0xd3, 0xde, 0x0f, 0x61, 0xb0, 0x7b, 0xaa, 0xc4, 0x15,
+ 0x18, 0xc9, 0xa7, 0x76, 0x2c, 0xfd, 0x93, 0x42, 0x4f, 0x9e,
+ 0xf0, 0x21, 0xea, 0x3b, 0x55, 0x84, 0x89, 0x58, 0x36, 0xe7,
+ 0x00, 0xd2, 0xb9, 0x6b, 0x6f, 0xbd, 0xd6, 0x04, 0xde, 0x0c,
+ 0x67, 0xb5, 0xb1, 0x63, 0x08, 0xda, 0xa1, 0x73, 0x18, 0xca,
+ 0xce, 0x1c, 0x77, 0xa5, 0x7f, 0xad, 0xc6, 0x14, 0x10, 0xc2,
+ 0xa9, 0x7b, 0x5f, 0x8d, 0xe6, 0x34, 0x30, 0xe2, 0x89, 0x5b,
+ 0x81, 0x53, 0x38, 0xea, 0xee, 0x3c, 0x57, 0x85, 0xfe, 0x2c,
+ 0x47, 0x95, 0x91, 0x43, 0x28, 0xfa, 0x20, 0xf2, 0x99, 0x4b,
+ 0x4f, 0x9d, 0xf6, 0x24, 0xbe, 0x6c, 0x07, 0xd5, 0xd1, 0x03,
+ 0x68, 0xba, 0x60, 0xb2, 0xd9, 0x0b, 0x0f, 0xdd, 0xb6, 0x64,
+ 0x1f, 0xcd, 0xa6, 0x74, 0x70, 0xa2, 0xc9, 0x1b, 0xc1, 0x13,
+ 0x78, 0xaa, 0xae, 0x7c, 0x17, 0xc5, 0xe1, 0x33, 0x58, 0x8a,
+ 0x8e, 0x5c, 0x37, 0xe5, 0x3f, 0xed, 0x86, 0x54, 0x50, 0x82,
+ 0xe9, 0x3b, 0x40, 0x92, 0xf9, 0x2b, 0x2f, 0xfd, 0x96, 0x44,
+ 0x9e, 0x4c, 0x27, 0xf5, 0xf1, 0x23, 0x48, 0x9a, 0x61, 0xb3,
+ 0xd8, 0x0a, 0x0e, 0xdc, 0xb7, 0x65, 0xbf, 0x6d, 0x06, 0xd4,
+ 0xd0, 0x02, 0x69, 0xbb, 0xc0, 0x12, 0x79, 0xab, 0xaf, 0x7d,
+ 0x16, 0xc4, 0x1e, 0xcc, 0xa7, 0x75, 0x71, 0xa3, 0xc8, 0x1a,
+ 0x3e, 0xec, 0x87, 0x55, 0x51, 0x83, 0xe8, 0x3a, 0xe0, 0x32,
+ 0x59, 0x8b, 0x8f, 0x5d, 0x36, 0xe4, 0x9f, 0x4d, 0x26, 0xf4,
+ 0xf0, 0x22, 0x49, 0x9b, 0x41, 0x93, 0xf8, 0x2a, 0x2e, 0xfc,
+ 0x97, 0x45, 0xdf, 0x0d, 0x66, 0xb4, 0xb0, 0x62, 0x09, 0xdb,
+ 0x01, 0xd3, 0xb8, 0x6a, 0x6e, 0xbc, 0xd7, 0x05, 0x7e, 0xac,
+ 0xc7, 0x15, 0x11, 0xc3, 0xa8, 0x7a, 0xa0, 0x72, 0x19, 0xcb,
+ 0xcf, 0x1d, 0x76, 0xa4, 0x80, 0x52, 0x39, 0xeb, 0xef, 0x3d,
+ 0x56, 0x84, 0x5e, 0x8c, 0xe7, 0x35, 0x31, 0xe3, 0x88, 0x5a,
+ 0x21, 0xf3, 0x98, 0x4a, 0x4e, 0x9c, 0xf7, 0x25, 0xff, 0x2d,
+ 0x46, 0x94, 0x90, 0x42, 0x29, 0xfb, 0x00, 0xd3, 0xbb, 0x68,
+ 0x6b, 0xb8, 0xd0, 0x03, 0xd6, 0x05, 0x6d, 0xbe, 0xbd, 0x6e,
+ 0x06, 0xd5, 0xb1, 0x62, 0x0a, 0xd9, 0xda, 0x09, 0x61, 0xb2,
+ 0x67, 0xb4, 0xdc, 0x0f, 0x0c, 0xdf, 0xb7, 0x64, 0x7f, 0xac,
+ 0xc4, 0x17, 0x14, 0xc7, 0xaf, 0x7c, 0xa9, 0x7a, 0x12, 0xc1,
+ 0xc2, 0x11, 0x79, 0xaa, 0xce, 0x1d, 0x75, 0xa6, 0xa5, 0x76,
+ 0x1e, 0xcd, 0x18, 0xcb, 0xa3, 0x70, 0x73, 0xa0, 0xc8, 0x1b,
+ 0xfe, 0x2d, 0x45, 0x96, 0x95, 0x46, 0x2e, 0xfd, 0x28, 0xfb,
+ 0x93, 0x40, 0x43, 0x90, 0xf8, 0x2b, 0x4f, 0x9c, 0xf4, 0x27,
+ 0x24, 0xf7, 0x9f, 0x4c, 0x99, 0x4a, 0x22, 0xf1, 0xf2, 0x21,
+ 0x49, 0x9a, 0x81, 0x52, 0x3a, 0xe9, 0xea, 0x39, 0x51, 0x82,
+ 0x57, 0x84, 0xec, 0x3f, 0x3c, 0xef, 0x87, 0x54, 0x30, 0xe3,
+ 0x8b, 0x58, 0x5b, 0x88, 0xe0, 0x33, 0xe6, 0x35, 0x5d, 0x8e,
+ 0x8d, 0x5e, 0x36, 0xe5, 0xe1, 0x32, 0x5a, 0x89, 0x8a, 0x59,
+ 0x31, 0xe2, 0x37, 0xe4, 0x8c, 0x5f, 0x5c, 0x8f, 0xe7, 0x34,
+ 0x50, 0x83, 0xeb, 0x38, 0x3b, 0xe8, 0x80, 0x53, 0x86, 0x55,
+ 0x3d, 0xee, 0xed, 0x3e, 0x56, 0x85, 0x9e, 0x4d, 0x25, 0xf6,
+ 0xf5, 0x26, 0x4e, 0x9d, 0x48, 0x9b, 0xf3, 0x20, 0x23, 0xf0,
+ 0x98, 0x4b, 0x2f, 0xfc, 0x94, 0x47, 0x44, 0x97, 0xff, 0x2c,
+ 0xf9, 0x2a, 0x42, 0x91, 0x92, 0x41, 0x29, 0xfa, 0x1f, 0xcc,
+ 0xa4, 0x77, 0x74, 0xa7, 0xcf, 0x1c, 0xc9, 0x1a, 0x72, 0xa1,
+ 0xa2, 0x71, 0x19, 0xca, 0xae, 0x7d, 0x15, 0xc6, 0xc5, 0x16,
+ 0x7e, 0xad, 0x78, 0xab, 0xc3, 0x10, 0x13, 0xc0, 0xa8, 0x7b,
+ 0x60, 0xb3, 0xdb, 0x08, 0x0b, 0xd8, 0xb0, 0x63, 0xb6, 0x65,
+ 0x0d, 0xde, 0xdd, 0x0e, 0x66, 0xb5, 0xd1, 0x02, 0x6a, 0xb9,
+ 0xba, 0x69, 0x01, 0xd2, 0x07, 0xd4, 0xbc, 0x6f, 0x6c, 0xbf,
+ 0xd7, 0x04, 0x00, 0xd4, 0xb5, 0x61, 0x77, 0xa3, 0xc2, 0x16,
+ 0xee, 0x3a, 0x5b, 0x8f, 0x99, 0x4d, 0x2c, 0xf8, 0xc1, 0x15,
+ 0x74, 0xa0, 0xb6, 0x62, 0x03, 0xd7, 0x2f, 0xfb, 0x9a, 0x4e,
+ 0x58, 0x8c, 0xed, 0x39, 0x9f, 0x4b, 0x2a, 0xfe, 0xe8, 0x3c,
+ 0x5d, 0x89, 0x71, 0xa5, 0xc4, 0x10, 0x06, 0xd2, 0xb3, 0x67,
+ 0x5e, 0x8a, 0xeb, 0x3f, 0x29, 0xfd, 0x9c, 0x48, 0xb0, 0x64,
+ 0x05, 0xd1, 0xc7, 0x13, 0x72, 0xa6, 0x23, 0xf7, 0x96, 0x42,
+ 0x54, 0x80, 0xe1, 0x35, 0xcd, 0x19, 0x78, 0xac, 0xba, 0x6e,
+ 0x0f, 0xdb, 0xe2, 0x36, 0x57, 0x83, 0x95, 0x41, 0x20, 0xf4,
+ 0x0c, 0xd8, 0xb9, 0x6d, 0x7b, 0xaf, 0xce, 0x1a, 0xbc, 0x68,
+ 0x09, 0xdd, 0xcb, 0x1f, 0x7e, 0xaa, 0x52, 0x86, 0xe7, 0x33,
+ 0x25, 0xf1, 0x90, 0x44, 0x7d, 0xa9, 0xc8, 0x1c, 0x0a, 0xde,
+ 0xbf, 0x6b, 0x93, 0x47, 0x26, 0xf2, 0xe4, 0x30, 0x51, 0x85,
+ 0x46, 0x92, 0xf3, 0x27, 0x31, 0xe5, 0x84, 0x50, 0xa8, 0x7c,
+ 0x1d, 0xc9, 0xdf, 0x0b, 0x6a, 0xbe, 0x87, 0x53, 0x32, 0xe6,
+ 0xf0, 0x24, 0x45, 0x91, 0x69, 0xbd, 0xdc, 0x08, 0x1e, 0xca,
+ 0xab, 0x7f, 0xd9, 0x0d, 0x6c, 0xb8, 0xae, 0x7a, 0x1b, 0xcf,
+ 0x37, 0xe3, 0x82, 0x56, 0x40, 0x94, 0xf5, 0x21, 0x18, 0xcc,
+ 0xad, 0x79, 0x6f, 0xbb, 0xda, 0x0e, 0xf6, 0x22, 0x43, 0x97,
+ 0x81, 0x55, 0x34, 0xe0, 0x65, 0xb1, 0xd0, 0x04, 0x12, 0xc6,
+ 0xa7, 0x73, 0x8b, 0x5f, 0x3e, 0xea, 0xfc, 0x28, 0x49, 0x9d,
+ 0xa4, 0x70, 0x11, 0xc5, 0xd3, 0x07, 0x66, 0xb2, 0x4a, 0x9e,
+ 0xff, 0x2b, 0x3d, 0xe9, 0x88, 0x5c, 0xfa, 0x2e, 0x4f, 0x9b,
+ 0x8d, 0x59, 0x38, 0xec, 0x14, 0xc0, 0xa1, 0x75, 0x63, 0xb7,
+ 0xd6, 0x02, 0x3b, 0xef, 0x8e, 0x5a, 0x4c, 0x98, 0xf9, 0x2d,
+ 0xd5, 0x01, 0x60, 0xb4, 0xa2, 0x76, 0x17, 0xc3, 0x00, 0xd5,
+ 0xb7, 0x62, 0x73, 0xa6, 0xc4, 0x11, 0xe6, 0x33, 0x51, 0x84,
+ 0x95, 0x40, 0x22, 0xf7, 0xd1, 0x04, 0x66, 0xb3, 0xa2, 0x77,
+ 0x15, 0xc0, 0x37, 0xe2, 0x80, 0x55, 0x44, 0x91, 0xf3, 0x26,
+ 0xbf, 0x6a, 0x08, 0xdd, 0xcc, 0x19, 0x7b, 0xae, 0x59, 0x8c,
+ 0xee, 0x3b, 0x2a, 0xff, 0x9d, 0x48, 0x6e, 0xbb, 0xd9, 0x0c,
+ 0x1d, 0xc8, 0xaa, 0x7f, 0x88, 0x5d, 0x3f, 0xea, 0xfb, 0x2e,
+ 0x4c, 0x99, 0x63, 0xb6, 0xd4, 0x01, 0x10, 0xc5, 0xa7, 0x72,
+ 0x85, 0x50, 0x32, 0xe7, 0xf6, 0x23, 0x41, 0x94, 0xb2, 0x67,
+ 0x05, 0xd0, 0xc1, 0x14, 0x76, 0xa3, 0x54, 0x81, 0xe3, 0x36,
+ 0x27, 0xf2, 0x90, 0x45, 0xdc, 0x09, 0x6b, 0xbe, 0xaf, 0x7a,
+ 0x18, 0xcd, 0x3a, 0xef, 0x8d, 0x58, 0x49, 0x9c, 0xfe, 0x2b,
+ 0x0d, 0xd8, 0xba, 0x6f, 0x7e, 0xab, 0xc9, 0x1c, 0xeb, 0x3e,
+ 0x5c, 0x89, 0x98, 0x4d, 0x2f, 0xfa, 0xc6, 0x13, 0x71, 0xa4,
+ 0xb5, 0x60, 0x02, 0xd7, 0x20, 0xf5, 0x97, 0x42, 0x53, 0x86,
+ 0xe4, 0x31, 0x17, 0xc2, 0xa0, 0x75, 0x64, 0xb1, 0xd3, 0x06,
+ 0xf1, 0x24, 0x46, 0x93, 0x82, 0x57, 0x35, 0xe0, 0x79, 0xac,
+ 0xce, 0x1b, 0x0a, 0xdf, 0xbd, 0x68, 0x9f, 0x4a, 0x28, 0xfd,
+ 0xec, 0x39, 0x5b, 0x8e, 0xa8, 0x7d, 0x1f, 0xca, 0xdb, 0x0e,
+ 0x6c, 0xb9, 0x4e, 0x9b, 0xf9, 0x2c, 0x3d, 0xe8, 0x8a, 0x5f,
+ 0xa5, 0x70, 0x12, 0xc7, 0xd6, 0x03, 0x61, 0xb4, 0x43, 0x96,
+ 0xf4, 0x21, 0x30, 0xe5, 0x87, 0x52, 0x74, 0xa1, 0xc3, 0x16,
+ 0x07, 0xd2, 0xb0, 0x65, 0x92, 0x47, 0x25, 0xf0, 0xe1, 0x34,
+ 0x56, 0x83, 0x1a, 0xcf, 0xad, 0x78, 0x69, 0xbc, 0xde, 0x0b,
+ 0xfc, 0x29, 0x4b, 0x9e, 0x8f, 0x5a, 0x38, 0xed, 0xcb, 0x1e,
+ 0x7c, 0xa9, 0xb8, 0x6d, 0x0f, 0xda, 0x2d, 0xf8, 0x9a, 0x4f,
+ 0x5e, 0x8b, 0xe9, 0x3c, 0x00, 0xd6, 0xb1, 0x67, 0x7f, 0xa9,
+ 0xce, 0x18, 0xfe, 0x28, 0x4f, 0x99, 0x81, 0x57, 0x30, 0xe6,
+ 0xe1, 0x37, 0x50, 0x86, 0x9e, 0x48, 0x2f, 0xf9, 0x1f, 0xc9,
+ 0xae, 0x78, 0x60, 0xb6, 0xd1, 0x07, 0xdf, 0x09, 0x6e, 0xb8,
+ 0xa0, 0x76, 0x11, 0xc7, 0x21, 0xf7, 0x90, 0x46, 0x5e, 0x88,
+ 0xef, 0x39, 0x3e, 0xe8, 0x8f, 0x59, 0x41, 0x97, 0xf0, 0x26,
+ 0xc0, 0x16, 0x71, 0xa7, 0xbf, 0x69, 0x0e, 0xd8, 0xa3, 0x75,
+ 0x12, 0xc4, 0xdc, 0x0a, 0x6d, 0xbb, 0x5d, 0x8b, 0xec, 0x3a,
+ 0x22, 0xf4, 0x93, 0x45, 0x42, 0x94, 0xf3, 0x25, 0x3d, 0xeb,
+ 0x8c, 0x5a, 0xbc, 0x6a, 0x0d, 0xdb, 0xc3, 0x15, 0x72, 0xa4,
+ 0x7c, 0xaa, 0xcd, 0x1b, 0x03, 0xd5, 0xb2, 0x64, 0x82, 0x54,
+ 0x33, 0xe5, 0xfd, 0x2b, 0x4c, 0x9a, 0x9d, 0x4b, 0x2c, 0xfa,
+ 0xe2, 0x34, 0x53, 0x85, 0x63, 0xb5, 0xd2, 0x04, 0x1c, 0xca,
+ 0xad, 0x7b, 0x5b, 0x8d, 0xea, 0x3c, 0x24, 0xf2, 0x95, 0x43,
+ 0xa5, 0x73, 0x14, 0xc2, 0xda, 0x0c, 0x6b, 0xbd, 0xba, 0x6c,
+ 0x0b, 0xdd, 0xc5, 0x13, 0x74, 0xa2, 0x44, 0x92, 0xf5, 0x23,
+ 0x3b, 0xed, 0x8a, 0x5c, 0x84, 0x52, 0x35, 0xe3, 0xfb, 0x2d,
+ 0x4a, 0x9c, 0x7a, 0xac, 0xcb, 0x1d, 0x05, 0xd3, 0xb4, 0x62,
+ 0x65, 0xb3, 0xd4, 0x02, 0x1a, 0xcc, 0xab, 0x7d, 0x9b, 0x4d,
+ 0x2a, 0xfc, 0xe4, 0x32, 0x55, 0x83, 0xf8, 0x2e, 0x49, 0x9f,
+ 0x87, 0x51, 0x36, 0xe0, 0x06, 0xd0, 0xb7, 0x61, 0x79, 0xaf,
+ 0xc8, 0x1e, 0x19, 0xcf, 0xa8, 0x7e, 0x66, 0xb0, 0xd7, 0x01,
+ 0xe7, 0x31, 0x56, 0x80, 0x98, 0x4e, 0x29, 0xff, 0x27, 0xf1,
+ 0x96, 0x40, 0x58, 0x8e, 0xe9, 0x3f, 0xd9, 0x0f, 0x68, 0xbe,
+ 0xa6, 0x70, 0x17, 0xc1, 0xc6, 0x10, 0x77, 0xa1, 0xb9, 0x6f,
+ 0x08, 0xde, 0x38, 0xee, 0x89, 0x5f, 0x47, 0x91, 0xf6, 0x20,
+ 0x00, 0xd7, 0xb3, 0x64, 0x7b, 0xac, 0xc8, 0x1f, 0xf6, 0x21,
+ 0x45, 0x92, 0x8d, 0x5a, 0x3e, 0xe9, 0xf1, 0x26, 0x42, 0x95,
+ 0x8a, 0x5d, 0x39, 0xee, 0x07, 0xd0, 0xb4, 0x63, 0x7c, 0xab,
+ 0xcf, 0x18, 0xff, 0x28, 0x4c, 0x9b, 0x84, 0x53, 0x37, 0xe0,
+ 0x09, 0xde, 0xba, 0x6d, 0x72, 0xa5, 0xc1, 0x16, 0x0e, 0xd9,
+ 0xbd, 0x6a, 0x75, 0xa2, 0xc6, 0x11, 0xf8, 0x2f, 0x4b, 0x9c,
+ 0x83, 0x54, 0x30, 0xe7, 0xe3, 0x34, 0x50, 0x87, 0x98, 0x4f,
+ 0x2b, 0xfc, 0x15, 0xc2, 0xa6, 0x71, 0x6e, 0xb9, 0xdd, 0x0a,
+ 0x12, 0xc5, 0xa1, 0x76, 0x69, 0xbe, 0xda, 0x0d, 0xe4, 0x33,
+ 0x57, 0x80, 0x9f, 0x48, 0x2c, 0xfb, 0x1c, 0xcb, 0xaf, 0x78,
+ 0x67, 0xb0, 0xd4, 0x03, 0xea, 0x3d, 0x59, 0x8e, 0x91, 0x46,
+ 0x22, 0xf5, 0xed, 0x3a, 0x5e, 0x89, 0x96, 0x41, 0x25, 0xf2,
+ 0x1b, 0xcc, 0xa8, 0x7f, 0x60, 0xb7, 0xd3, 0x04, 0xdb, 0x0c,
+ 0x68, 0xbf, 0xa0, 0x77, 0x13, 0xc4, 0x2d, 0xfa, 0x9e, 0x49,
+ 0x56, 0x81, 0xe5, 0x32, 0x2a, 0xfd, 0x99, 0x4e, 0x51, 0x86,
+ 0xe2, 0x35, 0xdc, 0x0b, 0x6f, 0xb8, 0xa7, 0x70, 0x14, 0xc3,
+ 0x24, 0xf3, 0x97, 0x40, 0x5f, 0x88, 0xec, 0x3b, 0xd2, 0x05,
+ 0x61, 0xb6, 0xa9, 0x7e, 0x1a, 0xcd, 0xd5, 0x02, 0x66, 0xb1,
+ 0xae, 0x79, 0x1d, 0xca, 0x23, 0xf4, 0x90, 0x47, 0x58, 0x8f,
+ 0xeb, 0x3c, 0x38, 0xef, 0x8b, 0x5c, 0x43, 0x94, 0xf0, 0x27,
+ 0xce, 0x19, 0x7d, 0xaa, 0xb5, 0x62, 0x06, 0xd1, 0xc9, 0x1e,
+ 0x7a, 0xad, 0xb2, 0x65, 0x01, 0xd6, 0x3f, 0xe8, 0x8c, 0x5b,
+ 0x44, 0x93, 0xf7, 0x20, 0xc7, 0x10, 0x74, 0xa3, 0xbc, 0x6b,
+ 0x0f, 0xd8, 0x31, 0xe6, 0x82, 0x55, 0x4a, 0x9d, 0xf9, 0x2e,
+ 0x36, 0xe1, 0x85, 0x52, 0x4d, 0x9a, 0xfe, 0x29, 0xc0, 0x17,
+ 0x73, 0xa4, 0xbb, 0x6c, 0x08, 0xdf, 0x00, 0xd8, 0xad, 0x75,
+ 0x47, 0x9f, 0xea, 0x32, 0x8e, 0x56, 0x23, 0xfb, 0xc9, 0x11,
+ 0x64, 0xbc, 0x01, 0xd9, 0xac, 0x74, 0x46, 0x9e, 0xeb, 0x33,
+ 0x8f, 0x57, 0x22, 0xfa, 0xc8, 0x10, 0x65, 0xbd, 0x02, 0xda,
+ 0xaf, 0x77, 0x45, 0x9d, 0xe8, 0x30, 0x8c, 0x54, 0x21, 0xf9,
+ 0xcb, 0x13, 0x66, 0xbe, 0x03, 0xdb, 0xae, 0x76, 0x44, 0x9c,
+ 0xe9, 0x31, 0x8d, 0x55, 0x20, 0xf8, 0xca, 0x12, 0x67, 0xbf,
+ 0x04, 0xdc, 0xa9, 0x71, 0x43, 0x9b, 0xee, 0x36, 0x8a, 0x52,
+ 0x27, 0xff, 0xcd, 0x15, 0x60, 0xb8, 0x05, 0xdd, 0xa8, 0x70,
+ 0x42, 0x9a, 0xef, 0x37, 0x8b, 0x53, 0x26, 0xfe, 0xcc, 0x14,
+ 0x61, 0xb9, 0x06, 0xde, 0xab, 0x73, 0x41, 0x99, 0xec, 0x34,
+ 0x88, 0x50, 0x25, 0xfd, 0xcf, 0x17, 0x62, 0xba, 0x07, 0xdf,
+ 0xaa, 0x72, 0x40, 0x98, 0xed, 0x35, 0x89, 0x51, 0x24, 0xfc,
+ 0xce, 0x16, 0x63, 0xbb, 0x08, 0xd0, 0xa5, 0x7d, 0x4f, 0x97,
+ 0xe2, 0x3a, 0x86, 0x5e, 0x2b, 0xf3, 0xc1, 0x19, 0x6c, 0xb4,
+ 0x09, 0xd1, 0xa4, 0x7c, 0x4e, 0x96, 0xe3, 0x3b, 0x87, 0x5f,
+ 0x2a, 0xf2, 0xc0, 0x18, 0x6d, 0xb5, 0x0a, 0xd2, 0xa7, 0x7f,
+ 0x4d, 0x95, 0xe0, 0x38, 0x84, 0x5c, 0x29, 0xf1, 0xc3, 0x1b,
+ 0x6e, 0xb6, 0x0b, 0xd3, 0xa6, 0x7e, 0x4c, 0x94, 0xe1, 0x39,
+ 0x85, 0x5d, 0x28, 0xf0, 0xc2, 0x1a, 0x6f, 0xb7, 0x0c, 0xd4,
+ 0xa1, 0x79, 0x4b, 0x93, 0xe6, 0x3e, 0x82, 0x5a, 0x2f, 0xf7,
+ 0xc5, 0x1d, 0x68, 0xb0, 0x0d, 0xd5, 0xa0, 0x78, 0x4a, 0x92,
+ 0xe7, 0x3f, 0x83, 0x5b, 0x2e, 0xf6, 0xc4, 0x1c, 0x69, 0xb1,
+ 0x0e, 0xd6, 0xa3, 0x7b, 0x49, 0x91, 0xe4, 0x3c, 0x80, 0x58,
+ 0x2d, 0xf5, 0xc7, 0x1f, 0x6a, 0xb2, 0x0f, 0xd7, 0xa2, 0x7a,
+ 0x48, 0x90, 0xe5, 0x3d, 0x81, 0x59, 0x2c, 0xf4, 0xc6, 0x1e,
+ 0x6b, 0xb3, 0x00, 0xd9, 0xaf, 0x76, 0x43, 0x9a, 0xec, 0x35,
+ 0x86, 0x5f, 0x29, 0xf0, 0xc5, 0x1c, 0x6a, 0xb3, 0x11, 0xc8,
+ 0xbe, 0x67, 0x52, 0x8b, 0xfd, 0x24, 0x97, 0x4e, 0x38, 0xe1,
+ 0xd4, 0x0d, 0x7b, 0xa2, 0x22, 0xfb, 0x8d, 0x54, 0x61, 0xb8,
+ 0xce, 0x17, 0xa4, 0x7d, 0x0b, 0xd2, 0xe7, 0x3e, 0x48, 0x91,
+ 0x33, 0xea, 0x9c, 0x45, 0x70, 0xa9, 0xdf, 0x06, 0xb5, 0x6c,
+ 0x1a, 0xc3, 0xf6, 0x2f, 0x59, 0x80, 0x44, 0x9d, 0xeb, 0x32,
+ 0x07, 0xde, 0xa8, 0x71, 0xc2, 0x1b, 0x6d, 0xb4, 0x81, 0x58,
+ 0x2e, 0xf7, 0x55, 0x8c, 0xfa, 0x23, 0x16, 0xcf, 0xb9, 0x60,
+ 0xd3, 0x0a, 0x7c, 0xa5, 0x90, 0x49, 0x3f, 0xe6, 0x66, 0xbf,
+ 0xc9, 0x10, 0x25, 0xfc, 0x8a, 0x53, 0xe0, 0x39, 0x4f, 0x96,
+ 0xa3, 0x7a, 0x0c, 0xd5, 0x77, 0xae, 0xd8, 0x01, 0x34, 0xed,
+ 0x9b, 0x42, 0xf1, 0x28, 0x5e, 0x87, 0xb2, 0x6b, 0x1d, 0xc4,
+ 0x88, 0x51, 0x27, 0xfe, 0xcb, 0x12, 0x64, 0xbd, 0x0e, 0xd7,
+ 0xa1, 0x78, 0x4d, 0x94, 0xe2, 0x3b, 0x99, 0x40, 0x36, 0xef,
+ 0xda, 0x03, 0x75, 0xac, 0x1f, 0xc6, 0xb0, 0x69, 0x5c, 0x85,
+ 0xf3, 0x2a, 0xaa, 0x73, 0x05, 0xdc, 0xe9, 0x30, 0x46, 0x9f,
+ 0x2c, 0xf5, 0x83, 0x5a, 0x6f, 0xb6, 0xc0, 0x19, 0xbb, 0x62,
+ 0x14, 0xcd, 0xf8, 0x21, 0x57, 0x8e, 0x3d, 0xe4, 0x92, 0x4b,
+ 0x7e, 0xa7, 0xd1, 0x08, 0xcc, 0x15, 0x63, 0xba, 0x8f, 0x56,
+ 0x20, 0xf9, 0x4a, 0x93, 0xe5, 0x3c, 0x09, 0xd0, 0xa6, 0x7f,
+ 0xdd, 0x04, 0x72, 0xab, 0x9e, 0x47, 0x31, 0xe8, 0x5b, 0x82,
+ 0xf4, 0x2d, 0x18, 0xc1, 0xb7, 0x6e, 0xee, 0x37, 0x41, 0x98,
+ 0xad, 0x74, 0x02, 0xdb, 0x68, 0xb1, 0xc7, 0x1e, 0x2b, 0xf2,
+ 0x84, 0x5d, 0xff, 0x26, 0x50, 0x89, 0xbc, 0x65, 0x13, 0xca,
+ 0x79, 0xa0, 0xd6, 0x0f, 0x3a, 0xe3, 0x95, 0x4c, 0x00, 0xda,
+ 0xa9, 0x73, 0x4f, 0x95, 0xe6, 0x3c, 0x9e, 0x44, 0x37, 0xed,
+ 0xd1, 0x0b, 0x78, 0xa2, 0x21, 0xfb, 0x88, 0x52, 0x6e, 0xb4,
+ 0xc7, 0x1d, 0xbf, 0x65, 0x16, 0xcc, 0xf0, 0x2a, 0x59, 0x83,
+ 0x42, 0x98, 0xeb, 0x31, 0x0d, 0xd7, 0xa4, 0x7e, 0xdc, 0x06,
+ 0x75, 0xaf, 0x93, 0x49, 0x3a, 0xe0, 0x63, 0xb9, 0xca, 0x10,
+ 0x2c, 0xf6, 0x85, 0x5f, 0xfd, 0x27, 0x54, 0x8e, 0xb2, 0x68,
+ 0x1b, 0xc1, 0x84, 0x5e, 0x2d, 0xf7, 0xcb, 0x11, 0x62, 0xb8,
+ 0x1a, 0xc0, 0xb3, 0x69, 0x55, 0x8f, 0xfc, 0x26, 0xa5, 0x7f,
+ 0x0c, 0xd6, 0xea, 0x30, 0x43, 0x99, 0x3b, 0xe1, 0x92, 0x48,
+ 0x74, 0xae, 0xdd, 0x07, 0xc6, 0x1c, 0x6f, 0xb5, 0x89, 0x53,
+ 0x20, 0xfa, 0x58, 0x82, 0xf1, 0x2b, 0x17, 0xcd, 0xbe, 0x64,
+ 0xe7, 0x3d, 0x4e, 0x94, 0xa8, 0x72, 0x01, 0xdb, 0x79, 0xa3,
+ 0xd0, 0x0a, 0x36, 0xec, 0x9f, 0x45, 0x15, 0xcf, 0xbc, 0x66,
+ 0x5a, 0x80, 0xf3, 0x29, 0x8b, 0x51, 0x22, 0xf8, 0xc4, 0x1e,
+ 0x6d, 0xb7, 0x34, 0xee, 0x9d, 0x47, 0x7b, 0xa1, 0xd2, 0x08,
+ 0xaa, 0x70, 0x03, 0xd9, 0xe5, 0x3f, 0x4c, 0x96, 0x57, 0x8d,
+ 0xfe, 0x24, 0x18, 0xc2, 0xb1, 0x6b, 0xc9, 0x13, 0x60, 0xba,
+ 0x86, 0x5c, 0x2f, 0xf5, 0x76, 0xac, 0xdf, 0x05, 0x39, 0xe3,
+ 0x90, 0x4a, 0xe8, 0x32, 0x41, 0x9b, 0xa7, 0x7d, 0x0e, 0xd4,
+ 0x91, 0x4b, 0x38, 0xe2, 0xde, 0x04, 0x77, 0xad, 0x0f, 0xd5,
+ 0xa6, 0x7c, 0x40, 0x9a, 0xe9, 0x33, 0xb0, 0x6a, 0x19, 0xc3,
+ 0xff, 0x25, 0x56, 0x8c, 0x2e, 0xf4, 0x87, 0x5d, 0x61, 0xbb,
+ 0xc8, 0x12, 0xd3, 0x09, 0x7a, 0xa0, 0x9c, 0x46, 0x35, 0xef,
+ 0x4d, 0x97, 0xe4, 0x3e, 0x02, 0xd8, 0xab, 0x71, 0xf2, 0x28,
+ 0x5b, 0x81, 0xbd, 0x67, 0x14, 0xce, 0x6c, 0xb6, 0xc5, 0x1f,
+ 0x23, 0xf9, 0x8a, 0x50, 0x00, 0xdb, 0xab, 0x70, 0x4b, 0x90,
+ 0xe0, 0x3b, 0x96, 0x4d, 0x3d, 0xe6, 0xdd, 0x06, 0x76, 0xad,
+ 0x31, 0xea, 0x9a, 0x41, 0x7a, 0xa1, 0xd1, 0x0a, 0xa7, 0x7c,
+ 0x0c, 0xd7, 0xec, 0x37, 0x47, 0x9c, 0x62, 0xb9, 0xc9, 0x12,
+ 0x29, 0xf2, 0x82, 0x59, 0xf4, 0x2f, 0x5f, 0x84, 0xbf, 0x64,
+ 0x14, 0xcf, 0x53, 0x88, 0xf8, 0x23, 0x18, 0xc3, 0xb3, 0x68,
+ 0xc5, 0x1e, 0x6e, 0xb5, 0x8e, 0x55, 0x25, 0xfe, 0xc4, 0x1f,
+ 0x6f, 0xb4, 0x8f, 0x54, 0x24, 0xff, 0x52, 0x89, 0xf9, 0x22,
+ 0x19, 0xc2, 0xb2, 0x69, 0xf5, 0x2e, 0x5e, 0x85, 0xbe, 0x65,
+ 0x15, 0xce, 0x63, 0xb8, 0xc8, 0x13, 0x28, 0xf3, 0x83, 0x58,
+ 0xa6, 0x7d, 0x0d, 0xd6, 0xed, 0x36, 0x46, 0x9d, 0x30, 0xeb,
+ 0x9b, 0x40, 0x7b, 0xa0, 0xd0, 0x0b, 0x97, 0x4c, 0x3c, 0xe7,
+ 0xdc, 0x07, 0x77, 0xac, 0x01, 0xda, 0xaa, 0x71, 0x4a, 0x91,
+ 0xe1, 0x3a, 0x95, 0x4e, 0x3e, 0xe5, 0xde, 0x05, 0x75, 0xae,
+ 0x03, 0xd8, 0xa8, 0x73, 0x48, 0x93, 0xe3, 0x38, 0xa4, 0x7f,
+ 0x0f, 0xd4, 0xef, 0x34, 0x44, 0x9f, 0x32, 0xe9, 0x99, 0x42,
+ 0x79, 0xa2, 0xd2, 0x09, 0xf7, 0x2c, 0x5c, 0x87, 0xbc, 0x67,
+ 0x17, 0xcc, 0x61, 0xba, 0xca, 0x11, 0x2a, 0xf1, 0x81, 0x5a,
+ 0xc6, 0x1d, 0x6d, 0xb6, 0x8d, 0x56, 0x26, 0xfd, 0x50, 0x8b,
+ 0xfb, 0x20, 0x1b, 0xc0, 0xb0, 0x6b, 0x51, 0x8a, 0xfa, 0x21,
+ 0x1a, 0xc1, 0xb1, 0x6a, 0xc7, 0x1c, 0x6c, 0xb7, 0x8c, 0x57,
+ 0x27, 0xfc, 0x60, 0xbb, 0xcb, 0x10, 0x2b, 0xf0, 0x80, 0x5b,
+ 0xf6, 0x2d, 0x5d, 0x86, 0xbd, 0x66, 0x16, 0xcd, 0x33, 0xe8,
+ 0x98, 0x43, 0x78, 0xa3, 0xd3, 0x08, 0xa5, 0x7e, 0x0e, 0xd5,
+ 0xee, 0x35, 0x45, 0x9e, 0x02, 0xd9, 0xa9, 0x72, 0x49, 0x92,
+ 0xe2, 0x39, 0x94, 0x4f, 0x3f, 0xe4, 0xdf, 0x04, 0x74, 0xaf,
+ 0x00, 0xdc, 0xa5, 0x79, 0x57, 0x8b, 0xf2, 0x2e, 0xae, 0x72,
+ 0x0b, 0xd7, 0xf9, 0x25, 0x5c, 0x80, 0x41, 0x9d, 0xe4, 0x38,
+ 0x16, 0xca, 0xb3, 0x6f, 0xef, 0x33, 0x4a, 0x96, 0xb8, 0x64,
+ 0x1d, 0xc1, 0x82, 0x5e, 0x27, 0xfb, 0xd5, 0x09, 0x70, 0xac,
+ 0x2c, 0xf0, 0x89, 0x55, 0x7b, 0xa7, 0xde, 0x02, 0xc3, 0x1f,
+ 0x66, 0xba, 0x94, 0x48, 0x31, 0xed, 0x6d, 0xb1, 0xc8, 0x14,
+ 0x3a, 0xe6, 0x9f, 0x43, 0x19, 0xc5, 0xbc, 0x60, 0x4e, 0x92,
+ 0xeb, 0x37, 0xb7, 0x6b, 0x12, 0xce, 0xe0, 0x3c, 0x45, 0x99,
+ 0x58, 0x84, 0xfd, 0x21, 0x0f, 0xd3, 0xaa, 0x76, 0xf6, 0x2a,
+ 0x53, 0x8f, 0xa1, 0x7d, 0x04, 0xd8, 0x9b, 0x47, 0x3e, 0xe2,
+ 0xcc, 0x10, 0x69, 0xb5, 0x35, 0xe9, 0x90, 0x4c, 0x62, 0xbe,
+ 0xc7, 0x1b, 0xda, 0x06, 0x7f, 0xa3, 0x8d, 0x51, 0x28, 0xf4,
+ 0x74, 0xa8, 0xd1, 0x0d, 0x23, 0xff, 0x86, 0x5a, 0x32, 0xee,
+ 0x97, 0x4b, 0x65, 0xb9, 0xc0, 0x1c, 0x9c, 0x40, 0x39, 0xe5,
+ 0xcb, 0x17, 0x6e, 0xb2, 0x73, 0xaf, 0xd6, 0x0a, 0x24, 0xf8,
+ 0x81, 0x5d, 0xdd, 0x01, 0x78, 0xa4, 0x8a, 0x56, 0x2f, 0xf3,
+ 0xb0, 0x6c, 0x15, 0xc9, 0xe7, 0x3b, 0x42, 0x9e, 0x1e, 0xc2,
+ 0xbb, 0x67, 0x49, 0x95, 0xec, 0x30, 0xf1, 0x2d, 0x54, 0x88,
+ 0xa6, 0x7a, 0x03, 0xdf, 0x5f, 0x83, 0xfa, 0x26, 0x08, 0xd4,
+ 0xad, 0x71, 0x2b, 0xf7, 0x8e, 0x52, 0x7c, 0xa0, 0xd9, 0x05,
+ 0x85, 0x59, 0x20, 0xfc, 0xd2, 0x0e, 0x77, 0xab, 0x6a, 0xb6,
+ 0xcf, 0x13, 0x3d, 0xe1, 0x98, 0x44, 0xc4, 0x18, 0x61, 0xbd,
+ 0x93, 0x4f, 0x36, 0xea, 0xa9, 0x75, 0x0c, 0xd0, 0xfe, 0x22,
+ 0x5b, 0x87, 0x07, 0xdb, 0xa2, 0x7e, 0x50, 0x8c, 0xf5, 0x29,
+ 0xe8, 0x34, 0x4d, 0x91, 0xbf, 0x63, 0x1a, 0xc6, 0x46, 0x9a,
+ 0xe3, 0x3f, 0x11, 0xcd, 0xb4, 0x68, 0x00, 0xdd, 0xa7, 0x7a,
+ 0x53, 0x8e, 0xf4, 0x29, 0xa6, 0x7b, 0x01, 0xdc, 0xf5, 0x28,
+ 0x52, 0x8f, 0x51, 0x8c, 0xf6, 0x2b, 0x02, 0xdf, 0xa5, 0x78,
+ 0xf7, 0x2a, 0x50, 0x8d, 0xa4, 0x79, 0x03, 0xde, 0xa2, 0x7f,
+ 0x05, 0xd8, 0xf1, 0x2c, 0x56, 0x8b, 0x04, 0xd9, 0xa3, 0x7e,
+ 0x57, 0x8a, 0xf0, 0x2d, 0xf3, 0x2e, 0x54, 0x89, 0xa0, 0x7d,
+ 0x07, 0xda, 0x55, 0x88, 0xf2, 0x2f, 0x06, 0xdb, 0xa1, 0x7c,
+ 0x59, 0x84, 0xfe, 0x23, 0x0a, 0xd7, 0xad, 0x70, 0xff, 0x22,
+ 0x58, 0x85, 0xac, 0x71, 0x0b, 0xd6, 0x08, 0xd5, 0xaf, 0x72,
+ 0x5b, 0x86, 0xfc, 0x21, 0xae, 0x73, 0x09, 0xd4, 0xfd, 0x20,
+ 0x5a, 0x87, 0xfb, 0x26, 0x5c, 0x81, 0xa8, 0x75, 0x0f, 0xd2,
+ 0x5d, 0x80, 0xfa, 0x27, 0x0e, 0xd3, 0xa9, 0x74, 0xaa, 0x77,
+ 0x0d, 0xd0, 0xf9, 0x24, 0x5e, 0x83, 0x0c, 0xd1, 0xab, 0x76,
+ 0x5f, 0x82, 0xf8, 0x25, 0xb2, 0x6f, 0x15, 0xc8, 0xe1, 0x3c,
+ 0x46, 0x9b, 0x14, 0xc9, 0xb3, 0x6e, 0x47, 0x9a, 0xe0, 0x3d,
+ 0xe3, 0x3e, 0x44, 0x99, 0xb0, 0x6d, 0x17, 0xca, 0x45, 0x98,
+ 0xe2, 0x3f, 0x16, 0xcb, 0xb1, 0x6c, 0x10, 0xcd, 0xb7, 0x6a,
+ 0x43, 0x9e, 0xe4, 0x39, 0xb6, 0x6b, 0x11, 0xcc, 0xe5, 0x38,
+ 0x42, 0x9f, 0x41, 0x9c, 0xe6, 0x3b, 0x12, 0xcf, 0xb5, 0x68,
+ 0xe7, 0x3a, 0x40, 0x9d, 0xb4, 0x69, 0x13, 0xce, 0xeb, 0x36,
+ 0x4c, 0x91, 0xb8, 0x65, 0x1f, 0xc2, 0x4d, 0x90, 0xea, 0x37,
+ 0x1e, 0xc3, 0xb9, 0x64, 0xba, 0x67, 0x1d, 0xc0, 0xe9, 0x34,
+ 0x4e, 0x93, 0x1c, 0xc1, 0xbb, 0x66, 0x4f, 0x92, 0xe8, 0x35,
+ 0x49, 0x94, 0xee, 0x33, 0x1a, 0xc7, 0xbd, 0x60, 0xef, 0x32,
+ 0x48, 0x95, 0xbc, 0x61, 0x1b, 0xc6, 0x18, 0xc5, 0xbf, 0x62,
+ 0x4b, 0x96, 0xec, 0x31, 0xbe, 0x63, 0x19, 0xc4, 0xed, 0x30,
+ 0x4a, 0x97, 0x00, 0xde, 0xa1, 0x7f, 0x5f, 0x81, 0xfe, 0x20,
+ 0xbe, 0x60, 0x1f, 0xc1, 0xe1, 0x3f, 0x40, 0x9e, 0x61, 0xbf,
+ 0xc0, 0x1e, 0x3e, 0xe0, 0x9f, 0x41, 0xdf, 0x01, 0x7e, 0xa0,
+ 0x80, 0x5e, 0x21, 0xff, 0xc2, 0x1c, 0x63, 0xbd, 0x9d, 0x43,
+ 0x3c, 0xe2, 0x7c, 0xa2, 0xdd, 0x03, 0x23, 0xfd, 0x82, 0x5c,
+ 0xa3, 0x7d, 0x02, 0xdc, 0xfc, 0x22, 0x5d, 0x83, 0x1d, 0xc3,
+ 0xbc, 0x62, 0x42, 0x9c, 0xe3, 0x3d, 0x99, 0x47, 0x38, 0xe6,
+ 0xc6, 0x18, 0x67, 0xb9, 0x27, 0xf9, 0x86, 0x58, 0x78, 0xa6,
+ 0xd9, 0x07, 0xf8, 0x26, 0x59, 0x87, 0xa7, 0x79, 0x06, 0xd8,
+ 0x46, 0x98, 0xe7, 0x39, 0x19, 0xc7, 0xb8, 0x66, 0x5b, 0x85,
+ 0xfa, 0x24, 0x04, 0xda, 0xa5, 0x7b, 0xe5, 0x3b, 0x44, 0x9a,
+ 0xba, 0x64, 0x1b, 0xc5, 0x3a, 0xe4, 0x9b, 0x45, 0x65, 0xbb,
+ 0xc4, 0x1a, 0x84, 0x5a, 0x25, 0xfb, 0xdb, 0x05, 0x7a, 0xa4,
+ 0x2f, 0xf1, 0x8e, 0x50, 0x70, 0xae, 0xd1, 0x0f, 0x91, 0x4f,
+ 0x30, 0xee, 0xce, 0x10, 0x6f, 0xb1, 0x4e, 0x90, 0xef, 0x31,
+ 0x11, 0xcf, 0xb0, 0x6e, 0xf0, 0x2e, 0x51, 0x8f, 0xaf, 0x71,
+ 0x0e, 0xd0, 0xed, 0x33, 0x4c, 0x92, 0xb2, 0x6c, 0x13, 0xcd,
+ 0x53, 0x8d, 0xf2, 0x2c, 0x0c, 0xd2, 0xad, 0x73, 0x8c, 0x52,
+ 0x2d, 0xf3, 0xd3, 0x0d, 0x72, 0xac, 0x32, 0xec, 0x93, 0x4d,
+ 0x6d, 0xb3, 0xcc, 0x12, 0xb6, 0x68, 0x17, 0xc9, 0xe9, 0x37,
+ 0x48, 0x96, 0x08, 0xd6, 0xa9, 0x77, 0x57, 0x89, 0xf6, 0x28,
+ 0xd7, 0x09, 0x76, 0xa8, 0x88, 0x56, 0x29, 0xf7, 0x69, 0xb7,
+ 0xc8, 0x16, 0x36, 0xe8, 0x97, 0x49, 0x74, 0xaa, 0xd5, 0x0b,
+ 0x2b, 0xf5, 0x8a, 0x54, 0xca, 0x14, 0x6b, 0xb5, 0x95, 0x4b,
+ 0x34, 0xea, 0x15, 0xcb, 0xb4, 0x6a, 0x4a, 0x94, 0xeb, 0x35,
+ 0xab, 0x75, 0x0a, 0xd4, 0xf4, 0x2a, 0x55, 0x8b, 0x00, 0xdf,
+ 0xa3, 0x7c, 0x5b, 0x84, 0xf8, 0x27, 0xb6, 0x69, 0x15, 0xca,
+ 0xed, 0x32, 0x4e, 0x91, 0x71, 0xae, 0xd2, 0x0d, 0x2a, 0xf5,
+ 0x89, 0x56, 0xc7, 0x18, 0x64, 0xbb, 0x9c, 0x43, 0x3f, 0xe0,
+ 0xe2, 0x3d, 0x41, 0x9e, 0xb9, 0x66, 0x1a, 0xc5, 0x54, 0x8b,
+ 0xf7, 0x28, 0x0f, 0xd0, 0xac, 0x73, 0x93, 0x4c, 0x30, 0xef,
+ 0xc8, 0x17, 0x6b, 0xb4, 0x25, 0xfa, 0x86, 0x59, 0x7e, 0xa1,
+ 0xdd, 0x02, 0xd9, 0x06, 0x7a, 0xa5, 0x82, 0x5d, 0x21, 0xfe,
+ 0x6f, 0xb0, 0xcc, 0x13, 0x34, 0xeb, 0x97, 0x48, 0xa8, 0x77,
+ 0x0b, 0xd4, 0xf3, 0x2c, 0x50, 0x8f, 0x1e, 0xc1, 0xbd, 0x62,
+ 0x45, 0x9a, 0xe6, 0x39, 0x3b, 0xe4, 0x98, 0x47, 0x60, 0xbf,
+ 0xc3, 0x1c, 0x8d, 0x52, 0x2e, 0xf1, 0xd6, 0x09, 0x75, 0xaa,
+ 0x4a, 0x95, 0xe9, 0x36, 0x11, 0xce, 0xb2, 0x6d, 0xfc, 0x23,
+ 0x5f, 0x80, 0xa7, 0x78, 0x04, 0xdb, 0xaf, 0x70, 0x0c, 0xd3,
+ 0xf4, 0x2b, 0x57, 0x88, 0x19, 0xc6, 0xba, 0x65, 0x42, 0x9d,
+ 0xe1, 0x3e, 0xde, 0x01, 0x7d, 0xa2, 0x85, 0x5a, 0x26, 0xf9,
+ 0x68, 0xb7, 0xcb, 0x14, 0x33, 0xec, 0x90, 0x4f, 0x4d, 0x92,
+ 0xee, 0x31, 0x16, 0xc9, 0xb5, 0x6a, 0xfb, 0x24, 0x58, 0x87,
+ 0xa0, 0x7f, 0x03, 0xdc, 0x3c, 0xe3, 0x9f, 0x40, 0x67, 0xb8,
+ 0xc4, 0x1b, 0x8a, 0x55, 0x29, 0xf6, 0xd1, 0x0e, 0x72, 0xad,
+ 0x76, 0xa9, 0xd5, 0x0a, 0x2d, 0xf2, 0x8e, 0x51, 0xc0, 0x1f,
+ 0x63, 0xbc, 0x9b, 0x44, 0x38, 0xe7, 0x07, 0xd8, 0xa4, 0x7b,
+ 0x5c, 0x83, 0xff, 0x20, 0xb1, 0x6e, 0x12, 0xcd, 0xea, 0x35,
+ 0x49, 0x96, 0x94, 0x4b, 0x37, 0xe8, 0xcf, 0x10, 0x6c, 0xb3,
+ 0x22, 0xfd, 0x81, 0x5e, 0x79, 0xa6, 0xda, 0x05, 0xe5, 0x3a,
+ 0x46, 0x99, 0xbe, 0x61, 0x1d, 0xc2, 0x53, 0x8c, 0xf0, 0x2f,
+ 0x08, 0xd7, 0xab, 0x74, 0x00, 0xe0, 0xdd, 0x3d, 0xa7, 0x47,
+ 0x7a, 0x9a, 0x53, 0xb3, 0x8e, 0x6e, 0xf4, 0x14, 0x29, 0xc9,
+ 0xa6, 0x46, 0x7b, 0x9b, 0x01, 0xe1, 0xdc, 0x3c, 0xf5, 0x15,
+ 0x28, 0xc8, 0x52, 0xb2, 0x8f, 0x6f, 0x51, 0xb1, 0x8c, 0x6c,
+ 0xf6, 0x16, 0x2b, 0xcb, 0x02, 0xe2, 0xdf, 0x3f, 0xa5, 0x45,
+ 0x78, 0x98, 0xf7, 0x17, 0x2a, 0xca, 0x50, 0xb0, 0x8d, 0x6d,
+ 0xa4, 0x44, 0x79, 0x99, 0x03, 0xe3, 0xde, 0x3e, 0xa2, 0x42,
+ 0x7f, 0x9f, 0x05, 0xe5, 0xd8, 0x38, 0xf1, 0x11, 0x2c, 0xcc,
+ 0x56, 0xb6, 0x8b, 0x6b, 0x04, 0xe4, 0xd9, 0x39, 0xa3, 0x43,
+ 0x7e, 0x9e, 0x57, 0xb7, 0x8a, 0x6a, 0xf0, 0x10, 0x2d, 0xcd,
+ 0xf3, 0x13, 0x2e, 0xce, 0x54, 0xb4, 0x89, 0x69, 0xa0, 0x40,
+ 0x7d, 0x9d, 0x07, 0xe7, 0xda, 0x3a, 0x55, 0xb5, 0x88, 0x68,
+ 0xf2, 0x12, 0x2f, 0xcf, 0x06, 0xe6, 0xdb, 0x3b, 0xa1, 0x41,
+ 0x7c, 0x9c, 0x59, 0xb9, 0x84, 0x64, 0xfe, 0x1e, 0x23, 0xc3,
+ 0x0a, 0xea, 0xd7, 0x37, 0xad, 0x4d, 0x70, 0x90, 0xff, 0x1f,
+ 0x22, 0xc2, 0x58, 0xb8, 0x85, 0x65, 0xac, 0x4c, 0x71, 0x91,
+ 0x0b, 0xeb, 0xd6, 0x36, 0x08, 0xe8, 0xd5, 0x35, 0xaf, 0x4f,
+ 0x72, 0x92, 0x5b, 0xbb, 0x86, 0x66, 0xfc, 0x1c, 0x21, 0xc1,
+ 0xae, 0x4e, 0x73, 0x93, 0x09, 0xe9, 0xd4, 0x34, 0xfd, 0x1d,
+ 0x20, 0xc0, 0x5a, 0xba, 0x87, 0x67, 0xfb, 0x1b, 0x26, 0xc6,
+ 0x5c, 0xbc, 0x81, 0x61, 0xa8, 0x48, 0x75, 0x95, 0x0f, 0xef,
+ 0xd2, 0x32, 0x5d, 0xbd, 0x80, 0x60, 0xfa, 0x1a, 0x27, 0xc7,
+ 0x0e, 0xee, 0xd3, 0x33, 0xa9, 0x49, 0x74, 0x94, 0xaa, 0x4a,
+ 0x77, 0x97, 0x0d, 0xed, 0xd0, 0x30, 0xf9, 0x19, 0x24, 0xc4,
+ 0x5e, 0xbe, 0x83, 0x63, 0x0c, 0xec, 0xd1, 0x31, 0xab, 0x4b,
+ 0x76, 0x96, 0x5f, 0xbf, 0x82, 0x62, 0xf8, 0x18, 0x25, 0xc5,
+ 0x00, 0xe1, 0xdf, 0x3e, 0xa3, 0x42, 0x7c, 0x9d, 0x5b, 0xba,
+ 0x84, 0x65, 0xf8, 0x19, 0x27, 0xc6, 0xb6, 0x57, 0x69, 0x88,
+ 0x15, 0xf4, 0xca, 0x2b, 0xed, 0x0c, 0x32, 0xd3, 0x4e, 0xaf,
+ 0x91, 0x70, 0x71, 0x90, 0xae, 0x4f, 0xd2, 0x33, 0x0d, 0xec,
+ 0x2a, 0xcb, 0xf5, 0x14, 0x89, 0x68, 0x56, 0xb7, 0xc7, 0x26,
+ 0x18, 0xf9, 0x64, 0x85, 0xbb, 0x5a, 0x9c, 0x7d, 0x43, 0xa2,
+ 0x3f, 0xde, 0xe0, 0x01, 0xe2, 0x03, 0x3d, 0xdc, 0x41, 0xa0,
+ 0x9e, 0x7f, 0xb9, 0x58, 0x66, 0x87, 0x1a, 0xfb, 0xc5, 0x24,
+ 0x54, 0xb5, 0x8b, 0x6a, 0xf7, 0x16, 0x28, 0xc9, 0x0f, 0xee,
+ 0xd0, 0x31, 0xac, 0x4d, 0x73, 0x92, 0x93, 0x72, 0x4c, 0xad,
+ 0x30, 0xd1, 0xef, 0x0e, 0xc8, 0x29, 0x17, 0xf6, 0x6b, 0x8a,
+ 0xb4, 0x55, 0x25, 0xc4, 0xfa, 0x1b, 0x86, 0x67, 0x59, 0xb8,
+ 0x7e, 0x9f, 0xa1, 0x40, 0xdd, 0x3c, 0x02, 0xe3, 0xd9, 0x38,
+ 0x06, 0xe7, 0x7a, 0x9b, 0xa5, 0x44, 0x82, 0x63, 0x5d, 0xbc,
+ 0x21, 0xc0, 0xfe, 0x1f, 0x6f, 0x8e, 0xb0, 0x51, 0xcc, 0x2d,
+ 0x13, 0xf2, 0x34, 0xd5, 0xeb, 0x0a, 0x97, 0x76, 0x48, 0xa9,
+ 0xa8, 0x49, 0x77, 0x96, 0x0b, 0xea, 0xd4, 0x35, 0xf3, 0x12,
+ 0x2c, 0xcd, 0x50, 0xb1, 0x8f, 0x6e, 0x1e, 0xff, 0xc1, 0x20,
+ 0xbd, 0x5c, 0x62, 0x83, 0x45, 0xa4, 0x9a, 0x7b, 0xe6, 0x07,
+ 0x39, 0xd8, 0x3b, 0xda, 0xe4, 0x05, 0x98, 0x79, 0x47, 0xa6,
+ 0x60, 0x81, 0xbf, 0x5e, 0xc3, 0x22, 0x1c, 0xfd, 0x8d, 0x6c,
+ 0x52, 0xb3, 0x2e, 0xcf, 0xf1, 0x10, 0xd6, 0x37, 0x09, 0xe8,
+ 0x75, 0x94, 0xaa, 0x4b, 0x4a, 0xab, 0x95, 0x74, 0xe9, 0x08,
+ 0x36, 0xd7, 0x11, 0xf0, 0xce, 0x2f, 0xb2, 0x53, 0x6d, 0x8c,
+ 0xfc, 0x1d, 0x23, 0xc2, 0x5f, 0xbe, 0x80, 0x61, 0xa7, 0x46,
+ 0x78, 0x99, 0x04, 0xe5, 0xdb, 0x3a, 0x00, 0xe2, 0xd9, 0x3b,
+ 0xaf, 0x4d, 0x76, 0x94, 0x43, 0xa1, 0x9a, 0x78, 0xec, 0x0e,
+ 0x35, 0xd7, 0x86, 0x64, 0x5f, 0xbd, 0x29, 0xcb, 0xf0, 0x12,
+ 0xc5, 0x27, 0x1c, 0xfe, 0x6a, 0x88, 0xb3, 0x51, 0x11, 0xf3,
+ 0xc8, 0x2a, 0xbe, 0x5c, 0x67, 0x85, 0x52, 0xb0, 0x8b, 0x69,
+ 0xfd, 0x1f, 0x24, 0xc6, 0x97, 0x75, 0x4e, 0xac, 0x38, 0xda,
+ 0xe1, 0x03, 0xd4, 0x36, 0x0d, 0xef, 0x7b, 0x99, 0xa2, 0x40,
+ 0x22, 0xc0, 0xfb, 0x19, 0x8d, 0x6f, 0x54, 0xb6, 0x61, 0x83,
+ 0xb8, 0x5a, 0xce, 0x2c, 0x17, 0xf5, 0xa4, 0x46, 0x7d, 0x9f,
+ 0x0b, 0xe9, 0xd2, 0x30, 0xe7, 0x05, 0x3e, 0xdc, 0x48, 0xaa,
+ 0x91, 0x73, 0x33, 0xd1, 0xea, 0x08, 0x9c, 0x7e, 0x45, 0xa7,
+ 0x70, 0x92, 0xa9, 0x4b, 0xdf, 0x3d, 0x06, 0xe4, 0xb5, 0x57,
+ 0x6c, 0x8e, 0x1a, 0xf8, 0xc3, 0x21, 0xf6, 0x14, 0x2f, 0xcd,
+ 0x59, 0xbb, 0x80, 0x62, 0x44, 0xa6, 0x9d, 0x7f, 0xeb, 0x09,
+ 0x32, 0xd0, 0x07, 0xe5, 0xde, 0x3c, 0xa8, 0x4a, 0x71, 0x93,
+ 0xc2, 0x20, 0x1b, 0xf9, 0x6d, 0x8f, 0xb4, 0x56, 0x81, 0x63,
+ 0x58, 0xba, 0x2e, 0xcc, 0xf7, 0x15, 0x55, 0xb7, 0x8c, 0x6e,
+ 0xfa, 0x18, 0x23, 0xc1, 0x16, 0xf4, 0xcf, 0x2d, 0xb9, 0x5b,
+ 0x60, 0x82, 0xd3, 0x31, 0x0a, 0xe8, 0x7c, 0x9e, 0xa5, 0x47,
+ 0x90, 0x72, 0x49, 0xab, 0x3f, 0xdd, 0xe6, 0x04, 0x66, 0x84,
+ 0xbf, 0x5d, 0xc9, 0x2b, 0x10, 0xf2, 0x25, 0xc7, 0xfc, 0x1e,
+ 0x8a, 0x68, 0x53, 0xb1, 0xe0, 0x02, 0x39, 0xdb, 0x4f, 0xad,
+ 0x96, 0x74, 0xa3, 0x41, 0x7a, 0x98, 0x0c, 0xee, 0xd5, 0x37,
+ 0x77, 0x95, 0xae, 0x4c, 0xd8, 0x3a, 0x01, 0xe3, 0x34, 0xd6,
+ 0xed, 0x0f, 0x9b, 0x79, 0x42, 0xa0, 0xf1, 0x13, 0x28, 0xca,
+ 0x5e, 0xbc, 0x87, 0x65, 0xb2, 0x50, 0x6b, 0x89, 0x1d, 0xff,
+ 0xc4, 0x26, 0x00, 0xe3, 0xdb, 0x38, 0xab, 0x48, 0x70, 0x93,
+ 0x4b, 0xa8, 0x90, 0x73, 0xe0, 0x03, 0x3b, 0xd8, 0x96, 0x75,
+ 0x4d, 0xae, 0x3d, 0xde, 0xe6, 0x05, 0xdd, 0x3e, 0x06, 0xe5,
+ 0x76, 0x95, 0xad, 0x4e, 0x31, 0xd2, 0xea, 0x09, 0x9a, 0x79,
+ 0x41, 0xa2, 0x7a, 0x99, 0xa1, 0x42, 0xd1, 0x32, 0x0a, 0xe9,
+ 0xa7, 0x44, 0x7c, 0x9f, 0x0c, 0xef, 0xd7, 0x34, 0xec, 0x0f,
+ 0x37, 0xd4, 0x47, 0xa4, 0x9c, 0x7f, 0x62, 0x81, 0xb9, 0x5a,
+ 0xc9, 0x2a, 0x12, 0xf1, 0x29, 0xca, 0xf2, 0x11, 0x82, 0x61,
+ 0x59, 0xba, 0xf4, 0x17, 0x2f, 0xcc, 0x5f, 0xbc, 0x84, 0x67,
+ 0xbf, 0x5c, 0x64, 0x87, 0x14, 0xf7, 0xcf, 0x2c, 0x53, 0xb0,
+ 0x88, 0x6b, 0xf8, 0x1b, 0x23, 0xc0, 0x18, 0xfb, 0xc3, 0x20,
+ 0xb3, 0x50, 0x68, 0x8b, 0xc5, 0x26, 0x1e, 0xfd, 0x6e, 0x8d,
+ 0xb5, 0x56, 0x8e, 0x6d, 0x55, 0xb6, 0x25, 0xc6, 0xfe, 0x1d,
+ 0xc4, 0x27, 0x1f, 0xfc, 0x6f, 0x8c, 0xb4, 0x57, 0x8f, 0x6c,
+ 0x54, 0xb7, 0x24, 0xc7, 0xff, 0x1c, 0x52, 0xb1, 0x89, 0x6a,
+ 0xf9, 0x1a, 0x22, 0xc1, 0x19, 0xfa, 0xc2, 0x21, 0xb2, 0x51,
+ 0x69, 0x8a, 0xf5, 0x16, 0x2e, 0xcd, 0x5e, 0xbd, 0x85, 0x66,
+ 0xbe, 0x5d, 0x65, 0x86, 0x15, 0xf6, 0xce, 0x2d, 0x63, 0x80,
+ 0xb8, 0x5b, 0xc8, 0x2b, 0x13, 0xf0, 0x28, 0xcb, 0xf3, 0x10,
+ 0x83, 0x60, 0x58, 0xbb, 0xa6, 0x45, 0x7d, 0x9e, 0x0d, 0xee,
+ 0xd6, 0x35, 0xed, 0x0e, 0x36, 0xd5, 0x46, 0xa5, 0x9d, 0x7e,
+ 0x30, 0xd3, 0xeb, 0x08, 0x9b, 0x78, 0x40, 0xa3, 0x7b, 0x98,
+ 0xa0, 0x43, 0xd0, 0x33, 0x0b, 0xe8, 0x97, 0x74, 0x4c, 0xaf,
+ 0x3c, 0xdf, 0xe7, 0x04, 0xdc, 0x3f, 0x07, 0xe4, 0x77, 0x94,
+ 0xac, 0x4f, 0x01, 0xe2, 0xda, 0x39, 0xaa, 0x49, 0x71, 0x92,
+ 0x4a, 0xa9, 0x91, 0x72, 0xe1, 0x02, 0x3a, 0xd9, 0x00, 0xe4,
+ 0xd5, 0x31, 0xb7, 0x53, 0x62, 0x86, 0x73, 0x97, 0xa6, 0x42,
+ 0xc4, 0x20, 0x11, 0xf5, 0xe6, 0x02, 0x33, 0xd7, 0x51, 0xb5,
+ 0x84, 0x60, 0x95, 0x71, 0x40, 0xa4, 0x22, 0xc6, 0xf7, 0x13,
+ 0xd1, 0x35, 0x04, 0xe0, 0x66, 0x82, 0xb3, 0x57, 0xa2, 0x46,
+ 0x77, 0x93, 0x15, 0xf1, 0xc0, 0x24, 0x37, 0xd3, 0xe2, 0x06,
+ 0x80, 0x64, 0x55, 0xb1, 0x44, 0xa0, 0x91, 0x75, 0xf3, 0x17,
+ 0x26, 0xc2, 0xbf, 0x5b, 0x6a, 0x8e, 0x08, 0xec, 0xdd, 0x39,
+ 0xcc, 0x28, 0x19, 0xfd, 0x7b, 0x9f, 0xae, 0x4a, 0x59, 0xbd,
+ 0x8c, 0x68, 0xee, 0x0a, 0x3b, 0xdf, 0x2a, 0xce, 0xff, 0x1b,
+ 0x9d, 0x79, 0x48, 0xac, 0x6e, 0x8a, 0xbb, 0x5f, 0xd9, 0x3d,
+ 0x0c, 0xe8, 0x1d, 0xf9, 0xc8, 0x2c, 0xaa, 0x4e, 0x7f, 0x9b,
+ 0x88, 0x6c, 0x5d, 0xb9, 0x3f, 0xdb, 0xea, 0x0e, 0xfb, 0x1f,
+ 0x2e, 0xca, 0x4c, 0xa8, 0x99, 0x7d, 0x63, 0x87, 0xb6, 0x52,
+ 0xd4, 0x30, 0x01, 0xe5, 0x10, 0xf4, 0xc5, 0x21, 0xa7, 0x43,
+ 0x72, 0x96, 0x85, 0x61, 0x50, 0xb4, 0x32, 0xd6, 0xe7, 0x03,
+ 0xf6, 0x12, 0x23, 0xc7, 0x41, 0xa5, 0x94, 0x70, 0xb2, 0x56,
+ 0x67, 0x83, 0x05, 0xe1, 0xd0, 0x34, 0xc1, 0x25, 0x14, 0xf0,
+ 0x76, 0x92, 0xa3, 0x47, 0x54, 0xb0, 0x81, 0x65, 0xe3, 0x07,
+ 0x36, 0xd2, 0x27, 0xc3, 0xf2, 0x16, 0x90, 0x74, 0x45, 0xa1,
+ 0xdc, 0x38, 0x09, 0xed, 0x6b, 0x8f, 0xbe, 0x5a, 0xaf, 0x4b,
+ 0x7a, 0x9e, 0x18, 0xfc, 0xcd, 0x29, 0x3a, 0xde, 0xef, 0x0b,
+ 0x8d, 0x69, 0x58, 0xbc, 0x49, 0xad, 0x9c, 0x78, 0xfe, 0x1a,
+ 0x2b, 0xcf, 0x0d, 0xe9, 0xd8, 0x3c, 0xba, 0x5e, 0x6f, 0x8b,
+ 0x7e, 0x9a, 0xab, 0x4f, 0xc9, 0x2d, 0x1c, 0xf8, 0xeb, 0x0f,
+ 0x3e, 0xda, 0x5c, 0xb8, 0x89, 0x6d, 0x98, 0x7c, 0x4d, 0xa9,
+ 0x2f, 0xcb, 0xfa, 0x1e, 0x00, 0xe5, 0xd7, 0x32, 0xb3, 0x56,
+ 0x64, 0x81, 0x7b, 0x9e, 0xac, 0x49, 0xc8, 0x2d, 0x1f, 0xfa,
+ 0xf6, 0x13, 0x21, 0xc4, 0x45, 0xa0, 0x92, 0x77, 0x8d, 0x68,
+ 0x5a, 0xbf, 0x3e, 0xdb, 0xe9, 0x0c, 0xf1, 0x14, 0x26, 0xc3,
+ 0x42, 0xa7, 0x95, 0x70, 0x8a, 0x6f, 0x5d, 0xb8, 0x39, 0xdc,
+ 0xee, 0x0b, 0x07, 0xe2, 0xd0, 0x35, 0xb4, 0x51, 0x63, 0x86,
+ 0x7c, 0x99, 0xab, 0x4e, 0xcf, 0x2a, 0x18, 0xfd, 0xff, 0x1a,
+ 0x28, 0xcd, 0x4c, 0xa9, 0x9b, 0x7e, 0x84, 0x61, 0x53, 0xb6,
+ 0x37, 0xd2, 0xe0, 0x05, 0x09, 0xec, 0xde, 0x3b, 0xba, 0x5f,
+ 0x6d, 0x88, 0x72, 0x97, 0xa5, 0x40, 0xc1, 0x24, 0x16, 0xf3,
+ 0x0e, 0xeb, 0xd9, 0x3c, 0xbd, 0x58, 0x6a, 0x8f, 0x75, 0x90,
+ 0xa2, 0x47, 0xc6, 0x23, 0x11, 0xf4, 0xf8, 0x1d, 0x2f, 0xca,
+ 0x4b, 0xae, 0x9c, 0x79, 0x83, 0x66, 0x54, 0xb1, 0x30, 0xd5,
+ 0xe7, 0x02, 0xe3, 0x06, 0x34, 0xd1, 0x50, 0xb5, 0x87, 0x62,
+ 0x98, 0x7d, 0x4f, 0xaa, 0x2b, 0xce, 0xfc, 0x19, 0x15, 0xf0,
+ 0xc2, 0x27, 0xa6, 0x43, 0x71, 0x94, 0x6e, 0x8b, 0xb9, 0x5c,
+ 0xdd, 0x38, 0x0a, 0xef, 0x12, 0xf7, 0xc5, 0x20, 0xa1, 0x44,
+ 0x76, 0x93, 0x69, 0x8c, 0xbe, 0x5b, 0xda, 0x3f, 0x0d, 0xe8,
+ 0xe4, 0x01, 0x33, 0xd6, 0x57, 0xb2, 0x80, 0x65, 0x9f, 0x7a,
+ 0x48, 0xad, 0x2c, 0xc9, 0xfb, 0x1e, 0x1c, 0xf9, 0xcb, 0x2e,
+ 0xaf, 0x4a, 0x78, 0x9d, 0x67, 0x82, 0xb0, 0x55, 0xd4, 0x31,
+ 0x03, 0xe6, 0xea, 0x0f, 0x3d, 0xd8, 0x59, 0xbc, 0x8e, 0x6b,
+ 0x91, 0x74, 0x46, 0xa3, 0x22, 0xc7, 0xf5, 0x10, 0xed, 0x08,
+ 0x3a, 0xdf, 0x5e, 0xbb, 0x89, 0x6c, 0x96, 0x73, 0x41, 0xa4,
+ 0x25, 0xc0, 0xf2, 0x17, 0x1b, 0xfe, 0xcc, 0x29, 0xa8, 0x4d,
+ 0x7f, 0x9a, 0x60, 0x85, 0xb7, 0x52, 0xd3, 0x36, 0x04, 0xe1,
+ 0x00, 0xe6, 0xd1, 0x37, 0xbf, 0x59, 0x6e, 0x88, 0x63, 0x85,
+ 0xb2, 0x54, 0xdc, 0x3a, 0x0d, 0xeb, 0xc6, 0x20, 0x17, 0xf1,
+ 0x79, 0x9f, 0xa8, 0x4e, 0xa5, 0x43, 0x74, 0x92, 0x1a, 0xfc,
+ 0xcb, 0x2d, 0x91, 0x77, 0x40, 0xa6, 0x2e, 0xc8, 0xff, 0x19,
+ 0xf2, 0x14, 0x23, 0xc5, 0x4d, 0xab, 0x9c, 0x7a, 0x57, 0xb1,
+ 0x86, 0x60, 0xe8, 0x0e, 0x39, 0xdf, 0x34, 0xd2, 0xe5, 0x03,
+ 0x8b, 0x6d, 0x5a, 0xbc, 0x3f, 0xd9, 0xee, 0x08, 0x80, 0x66,
+ 0x51, 0xb7, 0x5c, 0xba, 0x8d, 0x6b, 0xe3, 0x05, 0x32, 0xd4,
+ 0xf9, 0x1f, 0x28, 0xce, 0x46, 0xa0, 0x97, 0x71, 0x9a, 0x7c,
+ 0x4b, 0xad, 0x25, 0xc3, 0xf4, 0x12, 0xae, 0x48, 0x7f, 0x99,
+ 0x11, 0xf7, 0xc0, 0x26, 0xcd, 0x2b, 0x1c, 0xfa, 0x72, 0x94,
+ 0xa3, 0x45, 0x68, 0x8e, 0xb9, 0x5f, 0xd7, 0x31, 0x06, 0xe0,
+ 0x0b, 0xed, 0xda, 0x3c, 0xb4, 0x52, 0x65, 0x83, 0x7e, 0x98,
+ 0xaf, 0x49, 0xc1, 0x27, 0x10, 0xf6, 0x1d, 0xfb, 0xcc, 0x2a,
+ 0xa2, 0x44, 0x73, 0x95, 0xb8, 0x5e, 0x69, 0x8f, 0x07, 0xe1,
+ 0xd6, 0x30, 0xdb, 0x3d, 0x0a, 0xec, 0x64, 0x82, 0xb5, 0x53,
+ 0xef, 0x09, 0x3e, 0xd8, 0x50, 0xb6, 0x81, 0x67, 0x8c, 0x6a,
+ 0x5d, 0xbb, 0x33, 0xd5, 0xe2, 0x04, 0x29, 0xcf, 0xf8, 0x1e,
+ 0x96, 0x70, 0x47, 0xa1, 0x4a, 0xac, 0x9b, 0x7d, 0xf5, 0x13,
+ 0x24, 0xc2, 0x41, 0xa7, 0x90, 0x76, 0xfe, 0x18, 0x2f, 0xc9,
+ 0x22, 0xc4, 0xf3, 0x15, 0x9d, 0x7b, 0x4c, 0xaa, 0x87, 0x61,
+ 0x56, 0xb0, 0x38, 0xde, 0xe9, 0x0f, 0xe4, 0x02, 0x35, 0xd3,
+ 0x5b, 0xbd, 0x8a, 0x6c, 0xd0, 0x36, 0x01, 0xe7, 0x6f, 0x89,
+ 0xbe, 0x58, 0xb3, 0x55, 0x62, 0x84, 0x0c, 0xea, 0xdd, 0x3b,
+ 0x16, 0xf0, 0xc7, 0x21, 0xa9, 0x4f, 0x78, 0x9e, 0x75, 0x93,
+ 0xa4, 0x42, 0xca, 0x2c, 0x1b, 0xfd, 0x00, 0xe7, 0xd3, 0x34,
+ 0xbb, 0x5c, 0x68, 0x8f, 0x6b, 0x8c, 0xb8, 0x5f, 0xd0, 0x37,
+ 0x03, 0xe4, 0xd6, 0x31, 0x05, 0xe2, 0x6d, 0x8a, 0xbe, 0x59,
+ 0xbd, 0x5a, 0x6e, 0x89, 0x06, 0xe1, 0xd5, 0x32, 0xb1, 0x56,
+ 0x62, 0x85, 0x0a, 0xed, 0xd9, 0x3e, 0xda, 0x3d, 0x09, 0xee,
+ 0x61, 0x86, 0xb2, 0x55, 0x67, 0x80, 0xb4, 0x53, 0xdc, 0x3b,
+ 0x0f, 0xe8, 0x0c, 0xeb, 0xdf, 0x38, 0xb7, 0x50, 0x64, 0x83,
+ 0x7f, 0x98, 0xac, 0x4b, 0xc4, 0x23, 0x17, 0xf0, 0x14, 0xf3,
+ 0xc7, 0x20, 0xaf, 0x48, 0x7c, 0x9b, 0xa9, 0x4e, 0x7a, 0x9d,
+ 0x12, 0xf5, 0xc1, 0x26, 0xc2, 0x25, 0x11, 0xf6, 0x79, 0x9e,
+ 0xaa, 0x4d, 0xce, 0x29, 0x1d, 0xfa, 0x75, 0x92, 0xa6, 0x41,
+ 0xa5, 0x42, 0x76, 0x91, 0x1e, 0xf9, 0xcd, 0x2a, 0x18, 0xff,
+ 0xcb, 0x2c, 0xa3, 0x44, 0x70, 0x97, 0x73, 0x94, 0xa0, 0x47,
+ 0xc8, 0x2f, 0x1b, 0xfc, 0xfe, 0x19, 0x2d, 0xca, 0x45, 0xa2,
+ 0x96, 0x71, 0x95, 0x72, 0x46, 0xa1, 0x2e, 0xc9, 0xfd, 0x1a,
+ 0x28, 0xcf, 0xfb, 0x1c, 0x93, 0x74, 0x40, 0xa7, 0x43, 0xa4,
+ 0x90, 0x77, 0xf8, 0x1f, 0x2b, 0xcc, 0x4f, 0xa8, 0x9c, 0x7b,
+ 0xf4, 0x13, 0x27, 0xc0, 0x24, 0xc3, 0xf7, 0x10, 0x9f, 0x78,
+ 0x4c, 0xab, 0x99, 0x7e, 0x4a, 0xad, 0x22, 0xc5, 0xf1, 0x16,
+ 0xf2, 0x15, 0x21, 0xc6, 0x49, 0xae, 0x9a, 0x7d, 0x81, 0x66,
+ 0x52, 0xb5, 0x3a, 0xdd, 0xe9, 0x0e, 0xea, 0x0d, 0x39, 0xde,
+ 0x51, 0xb6, 0x82, 0x65, 0x57, 0xb0, 0x84, 0x63, 0xec, 0x0b,
+ 0x3f, 0xd8, 0x3c, 0xdb, 0xef, 0x08, 0x87, 0x60, 0x54, 0xb3,
+ 0x30, 0xd7, 0xe3, 0x04, 0x8b, 0x6c, 0x58, 0xbf, 0x5b, 0xbc,
+ 0x88, 0x6f, 0xe0, 0x07, 0x33, 0xd4, 0xe6, 0x01, 0x35, 0xd2,
+ 0x5d, 0xba, 0x8e, 0x69, 0x8d, 0x6a, 0x5e, 0xb9, 0x36, 0xd1,
+ 0xe5, 0x02, 0x00, 0xe8, 0xcd, 0x25, 0x87, 0x6f, 0x4a, 0xa2,
+ 0x13, 0xfb, 0xde, 0x36, 0x94, 0x7c, 0x59, 0xb1, 0x26, 0xce,
+ 0xeb, 0x03, 0xa1, 0x49, 0x6c, 0x84, 0x35, 0xdd, 0xf8, 0x10,
+ 0xb2, 0x5a, 0x7f, 0x97, 0x4c, 0xa4, 0x81, 0x69, 0xcb, 0x23,
+ 0x06, 0xee, 0x5f, 0xb7, 0x92, 0x7a, 0xd8, 0x30, 0x15, 0xfd,
+ 0x6a, 0x82, 0xa7, 0x4f, 0xed, 0x05, 0x20, 0xc8, 0x79, 0x91,
+ 0xb4, 0x5c, 0xfe, 0x16, 0x33, 0xdb, 0x98, 0x70, 0x55, 0xbd,
+ 0x1f, 0xf7, 0xd2, 0x3a, 0x8b, 0x63, 0x46, 0xae, 0x0c, 0xe4,
+ 0xc1, 0x29, 0xbe, 0x56, 0x73, 0x9b, 0x39, 0xd1, 0xf4, 0x1c,
+ 0xad, 0x45, 0x60, 0x88, 0x2a, 0xc2, 0xe7, 0x0f, 0xd4, 0x3c,
+ 0x19, 0xf1, 0x53, 0xbb, 0x9e, 0x76, 0xc7, 0x2f, 0x0a, 0xe2,
+ 0x40, 0xa8, 0x8d, 0x65, 0xf2, 0x1a, 0x3f, 0xd7, 0x75, 0x9d,
+ 0xb8, 0x50, 0xe1, 0x09, 0x2c, 0xc4, 0x66, 0x8e, 0xab, 0x43,
+ 0x2d, 0xc5, 0xe0, 0x08, 0xaa, 0x42, 0x67, 0x8f, 0x3e, 0xd6,
+ 0xf3, 0x1b, 0xb9, 0x51, 0x74, 0x9c, 0x0b, 0xe3, 0xc6, 0x2e,
+ 0x8c, 0x64, 0x41, 0xa9, 0x18, 0xf0, 0xd5, 0x3d, 0x9f, 0x77,
+ 0x52, 0xba, 0x61, 0x89, 0xac, 0x44, 0xe6, 0x0e, 0x2b, 0xc3,
+ 0x72, 0x9a, 0xbf, 0x57, 0xf5, 0x1d, 0x38, 0xd0, 0x47, 0xaf,
+ 0x8a, 0x62, 0xc0, 0x28, 0x0d, 0xe5, 0x54, 0xbc, 0x99, 0x71,
+ 0xd3, 0x3b, 0x1e, 0xf6, 0xb5, 0x5d, 0x78, 0x90, 0x32, 0xda,
+ 0xff, 0x17, 0xa6, 0x4e, 0x6b, 0x83, 0x21, 0xc9, 0xec, 0x04,
+ 0x93, 0x7b, 0x5e, 0xb6, 0x14, 0xfc, 0xd9, 0x31, 0x80, 0x68,
+ 0x4d, 0xa5, 0x07, 0xef, 0xca, 0x22, 0xf9, 0x11, 0x34, 0xdc,
+ 0x7e, 0x96, 0xb3, 0x5b, 0xea, 0x02, 0x27, 0xcf, 0x6d, 0x85,
+ 0xa0, 0x48, 0xdf, 0x37, 0x12, 0xfa, 0x58, 0xb0, 0x95, 0x7d,
+ 0xcc, 0x24, 0x01, 0xe9, 0x4b, 0xa3, 0x86, 0x6e, 0x00, 0xe9,
+ 0xcf, 0x26, 0x83, 0x6a, 0x4c, 0xa5, 0x1b, 0xf2, 0xd4, 0x3d,
+ 0x98, 0x71, 0x57, 0xbe, 0x36, 0xdf, 0xf9, 0x10, 0xb5, 0x5c,
+ 0x7a, 0x93, 0x2d, 0xc4, 0xe2, 0x0b, 0xae, 0x47, 0x61, 0x88,
+ 0x6c, 0x85, 0xa3, 0x4a, 0xef, 0x06, 0x20, 0xc9, 0x77, 0x9e,
+ 0xb8, 0x51, 0xf4, 0x1d, 0x3b, 0xd2, 0x5a, 0xb3, 0x95, 0x7c,
+ 0xd9, 0x30, 0x16, 0xff, 0x41, 0xa8, 0x8e, 0x67, 0xc2, 0x2b,
+ 0x0d, 0xe4, 0xd8, 0x31, 0x17, 0xfe, 0x5b, 0xb2, 0x94, 0x7d,
+ 0xc3, 0x2a, 0x0c, 0xe5, 0x40, 0xa9, 0x8f, 0x66, 0xee, 0x07,
+ 0x21, 0xc8, 0x6d, 0x84, 0xa2, 0x4b, 0xf5, 0x1c, 0x3a, 0xd3,
+ 0x76, 0x9f, 0xb9, 0x50, 0xb4, 0x5d, 0x7b, 0x92, 0x37, 0xde,
+ 0xf8, 0x11, 0xaf, 0x46, 0x60, 0x89, 0x2c, 0xc5, 0xe3, 0x0a,
+ 0x82, 0x6b, 0x4d, 0xa4, 0x01, 0xe8, 0xce, 0x27, 0x99, 0x70,
+ 0x56, 0xbf, 0x1a, 0xf3, 0xd5, 0x3c, 0xad, 0x44, 0x62, 0x8b,
+ 0x2e, 0xc7, 0xe1, 0x08, 0xb6, 0x5f, 0x79, 0x90, 0x35, 0xdc,
+ 0xfa, 0x13, 0x9b, 0x72, 0x54, 0xbd, 0x18, 0xf1, 0xd7, 0x3e,
+ 0x80, 0x69, 0x4f, 0xa6, 0x03, 0xea, 0xcc, 0x25, 0xc1, 0x28,
+ 0x0e, 0xe7, 0x42, 0xab, 0x8d, 0x64, 0xda, 0x33, 0x15, 0xfc,
+ 0x59, 0xb0, 0x96, 0x7f, 0xf7, 0x1e, 0x38, 0xd1, 0x74, 0x9d,
+ 0xbb, 0x52, 0xec, 0x05, 0x23, 0xca, 0x6f, 0x86, 0xa0, 0x49,
+ 0x75, 0x9c, 0xba, 0x53, 0xf6, 0x1f, 0x39, 0xd0, 0x6e, 0x87,
+ 0xa1, 0x48, 0xed, 0x04, 0x22, 0xcb, 0x43, 0xaa, 0x8c, 0x65,
+ 0xc0, 0x29, 0x0f, 0xe6, 0x58, 0xb1, 0x97, 0x7e, 0xdb, 0x32,
+ 0x14, 0xfd, 0x19, 0xf0, 0xd6, 0x3f, 0x9a, 0x73, 0x55, 0xbc,
+ 0x02, 0xeb, 0xcd, 0x24, 0x81, 0x68, 0x4e, 0xa7, 0x2f, 0xc6,
+ 0xe0, 0x09, 0xac, 0x45, 0x63, 0x8a, 0x34, 0xdd, 0xfb, 0x12,
+ 0xb7, 0x5e, 0x78, 0x91, 0x00, 0xea, 0xc9, 0x23, 0x8f, 0x65,
+ 0x46, 0xac, 0x03, 0xe9, 0xca, 0x20, 0x8c, 0x66, 0x45, 0xaf,
+ 0x06, 0xec, 0xcf, 0x25, 0x89, 0x63, 0x40, 0xaa, 0x05, 0xef,
+ 0xcc, 0x26, 0x8a, 0x60, 0x43, 0xa9, 0x0c, 0xe6, 0xc5, 0x2f,
+ 0x83, 0x69, 0x4a, 0xa0, 0x0f, 0xe5, 0xc6, 0x2c, 0x80, 0x6a,
+ 0x49, 0xa3, 0x0a, 0xe0, 0xc3, 0x29, 0x85, 0x6f, 0x4c, 0xa6,
+ 0x09, 0xe3, 0xc0, 0x2a, 0x86, 0x6c, 0x4f, 0xa5, 0x18, 0xf2,
+ 0xd1, 0x3b, 0x97, 0x7d, 0x5e, 0xb4, 0x1b, 0xf1, 0xd2, 0x38,
+ 0x94, 0x7e, 0x5d, 0xb7, 0x1e, 0xf4, 0xd7, 0x3d, 0x91, 0x7b,
+ 0x58, 0xb2, 0x1d, 0xf7, 0xd4, 0x3e, 0x92, 0x78, 0x5b, 0xb1,
+ 0x14, 0xfe, 0xdd, 0x37, 0x9b, 0x71, 0x52, 0xb8, 0x17, 0xfd,
+ 0xde, 0x34, 0x98, 0x72, 0x51, 0xbb, 0x12, 0xf8, 0xdb, 0x31,
+ 0x9d, 0x77, 0x54, 0xbe, 0x11, 0xfb, 0xd8, 0x32, 0x9e, 0x74,
+ 0x57, 0xbd, 0x30, 0xda, 0xf9, 0x13, 0xbf, 0x55, 0x76, 0x9c,
+ 0x33, 0xd9, 0xfa, 0x10, 0xbc, 0x56, 0x75, 0x9f, 0x36, 0xdc,
+ 0xff, 0x15, 0xb9, 0x53, 0x70, 0x9a, 0x35, 0xdf, 0xfc, 0x16,
+ 0xba, 0x50, 0x73, 0x99, 0x3c, 0xd6, 0xf5, 0x1f, 0xb3, 0x59,
+ 0x7a, 0x90, 0x3f, 0xd5, 0xf6, 0x1c, 0xb0, 0x5a, 0x79, 0x93,
+ 0x3a, 0xd0, 0xf3, 0x19, 0xb5, 0x5f, 0x7c, 0x96, 0x39, 0xd3,
+ 0xf0, 0x1a, 0xb6, 0x5c, 0x7f, 0x95, 0x28, 0xc2, 0xe1, 0x0b,
+ 0xa7, 0x4d, 0x6e, 0x84, 0x2b, 0xc1, 0xe2, 0x08, 0xa4, 0x4e,
+ 0x6d, 0x87, 0x2e, 0xc4, 0xe7, 0x0d, 0xa1, 0x4b, 0x68, 0x82,
+ 0x2d, 0xc7, 0xe4, 0x0e, 0xa2, 0x48, 0x6b, 0x81, 0x24, 0xce,
+ 0xed, 0x07, 0xab, 0x41, 0x62, 0x88, 0x27, 0xcd, 0xee, 0x04,
+ 0xa8, 0x42, 0x61, 0x8b, 0x22, 0xc8, 0xeb, 0x01, 0xad, 0x47,
+ 0x64, 0x8e, 0x21, 0xcb, 0xe8, 0x02, 0xae, 0x44, 0x67, 0x8d,
+ 0x00, 0xeb, 0xcb, 0x20, 0x8b, 0x60, 0x40, 0xab, 0x0b, 0xe0,
+ 0xc0, 0x2b, 0x80, 0x6b, 0x4b, 0xa0, 0x16, 0xfd, 0xdd, 0x36,
+ 0x9d, 0x76, 0x56, 0xbd, 0x1d, 0xf6, 0xd6, 0x3d, 0x96, 0x7d,
+ 0x5d, 0xb6, 0x2c, 0xc7, 0xe7, 0x0c, 0xa7, 0x4c, 0x6c, 0x87,
+ 0x27, 0xcc, 0xec, 0x07, 0xac, 0x47, 0x67, 0x8c, 0x3a, 0xd1,
+ 0xf1, 0x1a, 0xb1, 0x5a, 0x7a, 0x91, 0x31, 0xda, 0xfa, 0x11,
+ 0xba, 0x51, 0x71, 0x9a, 0x58, 0xb3, 0x93, 0x78, 0xd3, 0x38,
+ 0x18, 0xf3, 0x53, 0xb8, 0x98, 0x73, 0xd8, 0x33, 0x13, 0xf8,
+ 0x4e, 0xa5, 0x85, 0x6e, 0xc5, 0x2e, 0x0e, 0xe5, 0x45, 0xae,
+ 0x8e, 0x65, 0xce, 0x25, 0x05, 0xee, 0x74, 0x9f, 0xbf, 0x54,
+ 0xff, 0x14, 0x34, 0xdf, 0x7f, 0x94, 0xb4, 0x5f, 0xf4, 0x1f,
+ 0x3f, 0xd4, 0x62, 0x89, 0xa9, 0x42, 0xe9, 0x02, 0x22, 0xc9,
+ 0x69, 0x82, 0xa2, 0x49, 0xe2, 0x09, 0x29, 0xc2, 0xb0, 0x5b,
+ 0x7b, 0x90, 0x3b, 0xd0, 0xf0, 0x1b, 0xbb, 0x50, 0x70, 0x9b,
+ 0x30, 0xdb, 0xfb, 0x10, 0xa6, 0x4d, 0x6d, 0x86, 0x2d, 0xc6,
+ 0xe6, 0x0d, 0xad, 0x46, 0x66, 0x8d, 0x26, 0xcd, 0xed, 0x06,
+ 0x9c, 0x77, 0x57, 0xbc, 0x17, 0xfc, 0xdc, 0x37, 0x97, 0x7c,
+ 0x5c, 0xb7, 0x1c, 0xf7, 0xd7, 0x3c, 0x8a, 0x61, 0x41, 0xaa,
+ 0x01, 0xea, 0xca, 0x21, 0x81, 0x6a, 0x4a, 0xa1, 0x0a, 0xe1,
+ 0xc1, 0x2a, 0xe8, 0x03, 0x23, 0xc8, 0x63, 0x88, 0xa8, 0x43,
+ 0xe3, 0x08, 0x28, 0xc3, 0x68, 0x83, 0xa3, 0x48, 0xfe, 0x15,
+ 0x35, 0xde, 0x75, 0x9e, 0xbe, 0x55, 0xf5, 0x1e, 0x3e, 0xd5,
+ 0x7e, 0x95, 0xb5, 0x5e, 0xc4, 0x2f, 0x0f, 0xe4, 0x4f, 0xa4,
+ 0x84, 0x6f, 0xcf, 0x24, 0x04, 0xef, 0x44, 0xaf, 0x8f, 0x64,
+ 0xd2, 0x39, 0x19, 0xf2, 0x59, 0xb2, 0x92, 0x79, 0xd9, 0x32,
+ 0x12, 0xf9, 0x52, 0xb9, 0x99, 0x72, 0x00, 0xec, 0xc5, 0x29,
+ 0x97, 0x7b, 0x52, 0xbe, 0x33, 0xdf, 0xf6, 0x1a, 0xa4, 0x48,
+ 0x61, 0x8d, 0x66, 0x8a, 0xa3, 0x4f, 0xf1, 0x1d, 0x34, 0xd8,
+ 0x55, 0xb9, 0x90, 0x7c, 0xc2, 0x2e, 0x07, 0xeb, 0xcc, 0x20,
+ 0x09, 0xe5, 0x5b, 0xb7, 0x9e, 0x72, 0xff, 0x13, 0x3a, 0xd6,
+ 0x68, 0x84, 0xad, 0x41, 0xaa, 0x46, 0x6f, 0x83, 0x3d, 0xd1,
+ 0xf8, 0x14, 0x99, 0x75, 0x5c, 0xb0, 0x0e, 0xe2, 0xcb, 0x27,
+ 0x85, 0x69, 0x40, 0xac, 0x12, 0xfe, 0xd7, 0x3b, 0xb6, 0x5a,
+ 0x73, 0x9f, 0x21, 0xcd, 0xe4, 0x08, 0xe3, 0x0f, 0x26, 0xca,
+ 0x74, 0x98, 0xb1, 0x5d, 0xd0, 0x3c, 0x15, 0xf9, 0x47, 0xab,
+ 0x82, 0x6e, 0x49, 0xa5, 0x8c, 0x60, 0xde, 0x32, 0x1b, 0xf7,
+ 0x7a, 0x96, 0xbf, 0x53, 0xed, 0x01, 0x28, 0xc4, 0x2f, 0xc3,
+ 0xea, 0x06, 0xb8, 0x54, 0x7d, 0x91, 0x1c, 0xf0, 0xd9, 0x35,
+ 0x8b, 0x67, 0x4e, 0xa2, 0x17, 0xfb, 0xd2, 0x3e, 0x80, 0x6c,
+ 0x45, 0xa9, 0x24, 0xc8, 0xe1, 0x0d, 0xb3, 0x5f, 0x76, 0x9a,
+ 0x71, 0x9d, 0xb4, 0x58, 0xe6, 0x0a, 0x23, 0xcf, 0x42, 0xae,
+ 0x87, 0x6b, 0xd5, 0x39, 0x10, 0xfc, 0xdb, 0x37, 0x1e, 0xf2,
+ 0x4c, 0xa0, 0x89, 0x65, 0xe8, 0x04, 0x2d, 0xc1, 0x7f, 0x93,
+ 0xba, 0x56, 0xbd, 0x51, 0x78, 0x94, 0x2a, 0xc6, 0xef, 0x03,
+ 0x8e, 0x62, 0x4b, 0xa7, 0x19, 0xf5, 0xdc, 0x30, 0x92, 0x7e,
+ 0x57, 0xbb, 0x05, 0xe9, 0xc0, 0x2c, 0xa1, 0x4d, 0x64, 0x88,
+ 0x36, 0xda, 0xf3, 0x1f, 0xf4, 0x18, 0x31, 0xdd, 0x63, 0x8f,
+ 0xa6, 0x4a, 0xc7, 0x2b, 0x02, 0xee, 0x50, 0xbc, 0x95, 0x79,
+ 0x5e, 0xb2, 0x9b, 0x77, 0xc9, 0x25, 0x0c, 0xe0, 0x6d, 0x81,
+ 0xa8, 0x44, 0xfa, 0x16, 0x3f, 0xd3, 0x38, 0xd4, 0xfd, 0x11,
+ 0xaf, 0x43, 0x6a, 0x86, 0x0b, 0xe7, 0xce, 0x22, 0x9c, 0x70,
+ 0x59, 0xb5, 0x00, 0xed, 0xc7, 0x2a, 0x93, 0x7e, 0x54, 0xb9,
+ 0x3b, 0xd6, 0xfc, 0x11, 0xa8, 0x45, 0x6f, 0x82, 0x76, 0x9b,
+ 0xb1, 0x5c, 0xe5, 0x08, 0x22, 0xcf, 0x4d, 0xa0, 0x8a, 0x67,
+ 0xde, 0x33, 0x19, 0xf4, 0xec, 0x01, 0x2b, 0xc6, 0x7f, 0x92,
+ 0xb8, 0x55, 0xd7, 0x3a, 0x10, 0xfd, 0x44, 0xa9, 0x83, 0x6e,
+ 0x9a, 0x77, 0x5d, 0xb0, 0x09, 0xe4, 0xce, 0x23, 0xa1, 0x4c,
+ 0x66, 0x8b, 0x32, 0xdf, 0xf5, 0x18, 0xc5, 0x28, 0x02, 0xef,
+ 0x56, 0xbb, 0x91, 0x7c, 0xfe, 0x13, 0x39, 0xd4, 0x6d, 0x80,
+ 0xaa, 0x47, 0xb3, 0x5e, 0x74, 0x99, 0x20, 0xcd, 0xe7, 0x0a,
+ 0x88, 0x65, 0x4f, 0xa2, 0x1b, 0xf6, 0xdc, 0x31, 0x29, 0xc4,
+ 0xee, 0x03, 0xba, 0x57, 0x7d, 0x90, 0x12, 0xff, 0xd5, 0x38,
+ 0x81, 0x6c, 0x46, 0xab, 0x5f, 0xb2, 0x98, 0x75, 0xcc, 0x21,
+ 0x0b, 0xe6, 0x64, 0x89, 0xa3, 0x4e, 0xf7, 0x1a, 0x30, 0xdd,
+ 0x97, 0x7a, 0x50, 0xbd, 0x04, 0xe9, 0xc3, 0x2e, 0xac, 0x41,
+ 0x6b, 0x86, 0x3f, 0xd2, 0xf8, 0x15, 0xe1, 0x0c, 0x26, 0xcb,
+ 0x72, 0x9f, 0xb5, 0x58, 0xda, 0x37, 0x1d, 0xf0, 0x49, 0xa4,
+ 0x8e, 0x63, 0x7b, 0x96, 0xbc, 0x51, 0xe8, 0x05, 0x2f, 0xc2,
+ 0x40, 0xad, 0x87, 0x6a, 0xd3, 0x3e, 0x14, 0xf9, 0x0d, 0xe0,
+ 0xca, 0x27, 0x9e, 0x73, 0x59, 0xb4, 0x36, 0xdb, 0xf1, 0x1c,
+ 0xa5, 0x48, 0x62, 0x8f, 0x52, 0xbf, 0x95, 0x78, 0xc1, 0x2c,
+ 0x06, 0xeb, 0x69, 0x84, 0xae, 0x43, 0xfa, 0x17, 0x3d, 0xd0,
+ 0x24, 0xc9, 0xe3, 0x0e, 0xb7, 0x5a, 0x70, 0x9d, 0x1f, 0xf2,
+ 0xd8, 0x35, 0x8c, 0x61, 0x4b, 0xa6, 0xbe, 0x53, 0x79, 0x94,
+ 0x2d, 0xc0, 0xea, 0x07, 0x85, 0x68, 0x42, 0xaf, 0x16, 0xfb,
+ 0xd1, 0x3c, 0xc8, 0x25, 0x0f, 0xe2, 0x5b, 0xb6, 0x9c, 0x71,
+ 0xf3, 0x1e, 0x34, 0xd9, 0x60, 0x8d, 0xa7, 0x4a, 0x00, 0xee,
+ 0xc1, 0x2f, 0x9f, 0x71, 0x5e, 0xb0, 0x23, 0xcd, 0xe2, 0x0c,
+ 0xbc, 0x52, 0x7d, 0x93, 0x46, 0xa8, 0x87, 0x69, 0xd9, 0x37,
+ 0x18, 0xf6, 0x65, 0x8b, 0xa4, 0x4a, 0xfa, 0x14, 0x3b, 0xd5,
+ 0x8c, 0x62, 0x4d, 0xa3, 0x13, 0xfd, 0xd2, 0x3c, 0xaf, 0x41,
+ 0x6e, 0x80, 0x30, 0xde, 0xf1, 0x1f, 0xca, 0x24, 0x0b, 0xe5,
+ 0x55, 0xbb, 0x94, 0x7a, 0xe9, 0x07, 0x28, 0xc6, 0x76, 0x98,
+ 0xb7, 0x59, 0x05, 0xeb, 0xc4, 0x2a, 0x9a, 0x74, 0x5b, 0xb5,
+ 0x26, 0xc8, 0xe7, 0x09, 0xb9, 0x57, 0x78, 0x96, 0x43, 0xad,
+ 0x82, 0x6c, 0xdc, 0x32, 0x1d, 0xf3, 0x60, 0x8e, 0xa1, 0x4f,
+ 0xff, 0x11, 0x3e, 0xd0, 0x89, 0x67, 0x48, 0xa6, 0x16, 0xf8,
+ 0xd7, 0x39, 0xaa, 0x44, 0x6b, 0x85, 0x35, 0xdb, 0xf4, 0x1a,
+ 0xcf, 0x21, 0x0e, 0xe0, 0x50, 0xbe, 0x91, 0x7f, 0xec, 0x02,
+ 0x2d, 0xc3, 0x73, 0x9d, 0xb2, 0x5c, 0x0a, 0xe4, 0xcb, 0x25,
+ 0x95, 0x7b, 0x54, 0xba, 0x29, 0xc7, 0xe8, 0x06, 0xb6, 0x58,
+ 0x77, 0x99, 0x4c, 0xa2, 0x8d, 0x63, 0xd3, 0x3d, 0x12, 0xfc,
+ 0x6f, 0x81, 0xae, 0x40, 0xf0, 0x1e, 0x31, 0xdf, 0x86, 0x68,
+ 0x47, 0xa9, 0x19, 0xf7, 0xd8, 0x36, 0xa5, 0x4b, 0x64, 0x8a,
+ 0x3a, 0xd4, 0xfb, 0x15, 0xc0, 0x2e, 0x01, 0xef, 0x5f, 0xb1,
+ 0x9e, 0x70, 0xe3, 0x0d, 0x22, 0xcc, 0x7c, 0x92, 0xbd, 0x53,
+ 0x0f, 0xe1, 0xce, 0x20, 0x90, 0x7e, 0x51, 0xbf, 0x2c, 0xc2,
+ 0xed, 0x03, 0xb3, 0x5d, 0x72, 0x9c, 0x49, 0xa7, 0x88, 0x66,
+ 0xd6, 0x38, 0x17, 0xf9, 0x6a, 0x84, 0xab, 0x45, 0xf5, 0x1b,
+ 0x34, 0xda, 0x83, 0x6d, 0x42, 0xac, 0x1c, 0xf2, 0xdd, 0x33,
+ 0xa0, 0x4e, 0x61, 0x8f, 0x3f, 0xd1, 0xfe, 0x10, 0xc5, 0x2b,
+ 0x04, 0xea, 0x5a, 0xb4, 0x9b, 0x75, 0xe6, 0x08, 0x27, 0xc9,
+ 0x79, 0x97, 0xb8, 0x56, 0x00, 0xef, 0xc3, 0x2c, 0x9b, 0x74,
+ 0x58, 0xb7, 0x2b, 0xc4, 0xe8, 0x07, 0xb0, 0x5f, 0x73, 0x9c,
+ 0x56, 0xb9, 0x95, 0x7a, 0xcd, 0x22, 0x0e, 0xe1, 0x7d, 0x92,
+ 0xbe, 0x51, 0xe6, 0x09, 0x25, 0xca, 0xac, 0x43, 0x6f, 0x80,
+ 0x37, 0xd8, 0xf4, 0x1b, 0x87, 0x68, 0x44, 0xab, 0x1c, 0xf3,
+ 0xdf, 0x30, 0xfa, 0x15, 0x39, 0xd6, 0x61, 0x8e, 0xa2, 0x4d,
+ 0xd1, 0x3e, 0x12, 0xfd, 0x4a, 0xa5, 0x89, 0x66, 0x45, 0xaa,
+ 0x86, 0x69, 0xde, 0x31, 0x1d, 0xf2, 0x6e, 0x81, 0xad, 0x42,
+ 0xf5, 0x1a, 0x36, 0xd9, 0x13, 0xfc, 0xd0, 0x3f, 0x88, 0x67,
+ 0x4b, 0xa4, 0x38, 0xd7, 0xfb, 0x14, 0xa3, 0x4c, 0x60, 0x8f,
+ 0xe9, 0x06, 0x2a, 0xc5, 0x72, 0x9d, 0xb1, 0x5e, 0xc2, 0x2d,
+ 0x01, 0xee, 0x59, 0xb6, 0x9a, 0x75, 0xbf, 0x50, 0x7c, 0x93,
+ 0x24, 0xcb, 0xe7, 0x08, 0x94, 0x7b, 0x57, 0xb8, 0x0f, 0xe0,
+ 0xcc, 0x23, 0x8a, 0x65, 0x49, 0xa6, 0x11, 0xfe, 0xd2, 0x3d,
+ 0xa1, 0x4e, 0x62, 0x8d, 0x3a, 0xd5, 0xf9, 0x16, 0xdc, 0x33,
+ 0x1f, 0xf0, 0x47, 0xa8, 0x84, 0x6b, 0xf7, 0x18, 0x34, 0xdb,
+ 0x6c, 0x83, 0xaf, 0x40, 0x26, 0xc9, 0xe5, 0x0a, 0xbd, 0x52,
+ 0x7e, 0x91, 0x0d, 0xe2, 0xce, 0x21, 0x96, 0x79, 0x55, 0xba,
+ 0x70, 0x9f, 0xb3, 0x5c, 0xeb, 0x04, 0x28, 0xc7, 0x5b, 0xb4,
+ 0x98, 0x77, 0xc0, 0x2f, 0x03, 0xec, 0xcf, 0x20, 0x0c, 0xe3,
+ 0x54, 0xbb, 0x97, 0x78, 0xe4, 0x0b, 0x27, 0xc8, 0x7f, 0x90,
+ 0xbc, 0x53, 0x99, 0x76, 0x5a, 0xb5, 0x02, 0xed, 0xc1, 0x2e,
+ 0xb2, 0x5d, 0x71, 0x9e, 0x29, 0xc6, 0xea, 0x05, 0x63, 0x8c,
+ 0xa0, 0x4f, 0xf8, 0x17, 0x3b, 0xd4, 0x48, 0xa7, 0x8b, 0x64,
+ 0xd3, 0x3c, 0x10, 0xff, 0x35, 0xda, 0xf6, 0x19, 0xae, 0x41,
+ 0x6d, 0x82, 0x1e, 0xf1, 0xdd, 0x32, 0x85, 0x6a, 0x46, 0xa9,
+ 0x00, 0xf0, 0xfd, 0x0d, 0xe7, 0x17, 0x1a, 0xea, 0xd3, 0x23,
+ 0x2e, 0xde, 0x34, 0xc4, 0xc9, 0x39, 0xbb, 0x4b, 0x46, 0xb6,
+ 0x5c, 0xac, 0xa1, 0x51, 0x68, 0x98, 0x95, 0x65, 0x8f, 0x7f,
+ 0x72, 0x82, 0x6b, 0x9b, 0x96, 0x66, 0x8c, 0x7c, 0x71, 0x81,
+ 0xb8, 0x48, 0x45, 0xb5, 0x5f, 0xaf, 0xa2, 0x52, 0xd0, 0x20,
+ 0x2d, 0xdd, 0x37, 0xc7, 0xca, 0x3a, 0x03, 0xf3, 0xfe, 0x0e,
+ 0xe4, 0x14, 0x19, 0xe9, 0xd6, 0x26, 0x2b, 0xdb, 0x31, 0xc1,
+ 0xcc, 0x3c, 0x05, 0xf5, 0xf8, 0x08, 0xe2, 0x12, 0x1f, 0xef,
+ 0x6d, 0x9d, 0x90, 0x60, 0x8a, 0x7a, 0x77, 0x87, 0xbe, 0x4e,
+ 0x43, 0xb3, 0x59, 0xa9, 0xa4, 0x54, 0xbd, 0x4d, 0x40, 0xb0,
+ 0x5a, 0xaa, 0xa7, 0x57, 0x6e, 0x9e, 0x93, 0x63, 0x89, 0x79,
+ 0x74, 0x84, 0x06, 0xf6, 0xfb, 0x0b, 0xe1, 0x11, 0x1c, 0xec,
+ 0xd5, 0x25, 0x28, 0xd8, 0x32, 0xc2, 0xcf, 0x3f, 0xb1, 0x41,
+ 0x4c, 0xbc, 0x56, 0xa6, 0xab, 0x5b, 0x62, 0x92, 0x9f, 0x6f,
+ 0x85, 0x75, 0x78, 0x88, 0x0a, 0xfa, 0xf7, 0x07, 0xed, 0x1d,
+ 0x10, 0xe0, 0xd9, 0x29, 0x24, 0xd4, 0x3e, 0xce, 0xc3, 0x33,
+ 0xda, 0x2a, 0x27, 0xd7, 0x3d, 0xcd, 0xc0, 0x30, 0x09, 0xf9,
+ 0xf4, 0x04, 0xee, 0x1e, 0x13, 0xe3, 0x61, 0x91, 0x9c, 0x6c,
+ 0x86, 0x76, 0x7b, 0x8b, 0xb2, 0x42, 0x4f, 0xbf, 0x55, 0xa5,
+ 0xa8, 0x58, 0x67, 0x97, 0x9a, 0x6a, 0x80, 0x70, 0x7d, 0x8d,
+ 0xb4, 0x44, 0x49, 0xb9, 0x53, 0xa3, 0xae, 0x5e, 0xdc, 0x2c,
+ 0x21, 0xd1, 0x3b, 0xcb, 0xc6, 0x36, 0x0f, 0xff, 0xf2, 0x02,
+ 0xe8, 0x18, 0x15, 0xe5, 0x0c, 0xfc, 0xf1, 0x01, 0xeb, 0x1b,
+ 0x16, 0xe6, 0xdf, 0x2f, 0x22, 0xd2, 0x38, 0xc8, 0xc5, 0x35,
+ 0xb7, 0x47, 0x4a, 0xba, 0x50, 0xa0, 0xad, 0x5d, 0x64, 0x94,
+ 0x99, 0x69, 0x83, 0x73, 0x7e, 0x8e, 0x00, 0xf1, 0xff, 0x0e,
+ 0xe3, 0x12, 0x1c, 0xed, 0xdb, 0x2a, 0x24, 0xd5, 0x38, 0xc9,
+ 0xc7, 0x36, 0xab, 0x5a, 0x54, 0xa5, 0x48, 0xb9, 0xb7, 0x46,
+ 0x70, 0x81, 0x8f, 0x7e, 0x93, 0x62, 0x6c, 0x9d, 0x4b, 0xba,
+ 0xb4, 0x45, 0xa8, 0x59, 0x57, 0xa6, 0x90, 0x61, 0x6f, 0x9e,
+ 0x73, 0x82, 0x8c, 0x7d, 0xe0, 0x11, 0x1f, 0xee, 0x03, 0xf2,
+ 0xfc, 0x0d, 0x3b, 0xca, 0xc4, 0x35, 0xd8, 0x29, 0x27, 0xd6,
+ 0x96, 0x67, 0x69, 0x98, 0x75, 0x84, 0x8a, 0x7b, 0x4d, 0xbc,
+ 0xb2, 0x43, 0xae, 0x5f, 0x51, 0xa0, 0x3d, 0xcc, 0xc2, 0x33,
+ 0xde, 0x2f, 0x21, 0xd0, 0xe6, 0x17, 0x19, 0xe8, 0x05, 0xf4,
+ 0xfa, 0x0b, 0xdd, 0x2c, 0x22, 0xd3, 0x3e, 0xcf, 0xc1, 0x30,
+ 0x06, 0xf7, 0xf9, 0x08, 0xe5, 0x14, 0x1a, 0xeb, 0x76, 0x87,
+ 0x89, 0x78, 0x95, 0x64, 0x6a, 0x9b, 0xad, 0x5c, 0x52, 0xa3,
+ 0x4e, 0xbf, 0xb1, 0x40, 0x31, 0xc0, 0xce, 0x3f, 0xd2, 0x23,
+ 0x2d, 0xdc, 0xea, 0x1b, 0x15, 0xe4, 0x09, 0xf8, 0xf6, 0x07,
+ 0x9a, 0x6b, 0x65, 0x94, 0x79, 0x88, 0x86, 0x77, 0x41, 0xb0,
+ 0xbe, 0x4f, 0xa2, 0x53, 0x5d, 0xac, 0x7a, 0x8b, 0x85, 0x74,
+ 0x99, 0x68, 0x66, 0x97, 0xa1, 0x50, 0x5e, 0xaf, 0x42, 0xb3,
+ 0xbd, 0x4c, 0xd1, 0x20, 0x2e, 0xdf, 0x32, 0xc3, 0xcd, 0x3c,
+ 0x0a, 0xfb, 0xf5, 0x04, 0xe9, 0x18, 0x16, 0xe7, 0xa7, 0x56,
+ 0x58, 0xa9, 0x44, 0xb5, 0xbb, 0x4a, 0x7c, 0x8d, 0x83, 0x72,
+ 0x9f, 0x6e, 0x60, 0x91, 0x0c, 0xfd, 0xf3, 0x02, 0xef, 0x1e,
+ 0x10, 0xe1, 0xd7, 0x26, 0x28, 0xd9, 0x34, 0xc5, 0xcb, 0x3a,
+ 0xec, 0x1d, 0x13, 0xe2, 0x0f, 0xfe, 0xf0, 0x01, 0x37, 0xc6,
+ 0xc8, 0x39, 0xd4, 0x25, 0x2b, 0xda, 0x47, 0xb6, 0xb8, 0x49,
+ 0xa4, 0x55, 0x5b, 0xaa, 0x9c, 0x6d, 0x63, 0x92, 0x7f, 0x8e,
+ 0x80, 0x71, 0x00, 0xf2, 0xf9, 0x0b, 0xef, 0x1d, 0x16, 0xe4,
+ 0xc3, 0x31, 0x3a, 0xc8, 0x2c, 0xde, 0xd5, 0x27, 0x9b, 0x69,
+ 0x62, 0x90, 0x74, 0x86, 0x8d, 0x7f, 0x58, 0xaa, 0xa1, 0x53,
+ 0xb7, 0x45, 0x4e, 0xbc, 0x2b, 0xd9, 0xd2, 0x20, 0xc4, 0x36,
+ 0x3d, 0xcf, 0xe8, 0x1a, 0x11, 0xe3, 0x07, 0xf5, 0xfe, 0x0c,
+ 0xb0, 0x42, 0x49, 0xbb, 0x5f, 0xad, 0xa6, 0x54, 0x73, 0x81,
+ 0x8a, 0x78, 0x9c, 0x6e, 0x65, 0x97, 0x56, 0xa4, 0xaf, 0x5d,
+ 0xb9, 0x4b, 0x40, 0xb2, 0x95, 0x67, 0x6c, 0x9e, 0x7a, 0x88,
+ 0x83, 0x71, 0xcd, 0x3f, 0x34, 0xc6, 0x22, 0xd0, 0xdb, 0x29,
+ 0x0e, 0xfc, 0xf7, 0x05, 0xe1, 0x13, 0x18, 0xea, 0x7d, 0x8f,
+ 0x84, 0x76, 0x92, 0x60, 0x6b, 0x99, 0xbe, 0x4c, 0x47, 0xb5,
+ 0x51, 0xa3, 0xa8, 0x5a, 0xe6, 0x14, 0x1f, 0xed, 0x09, 0xfb,
+ 0xf0, 0x02, 0x25, 0xd7, 0xdc, 0x2e, 0xca, 0x38, 0x33, 0xc1,
+ 0xac, 0x5e, 0x55, 0xa7, 0x43, 0xb1, 0xba, 0x48, 0x6f, 0x9d,
+ 0x96, 0x64, 0x80, 0x72, 0x79, 0x8b, 0x37, 0xc5, 0xce, 0x3c,
+ 0xd8, 0x2a, 0x21, 0xd3, 0xf4, 0x06, 0x0d, 0xff, 0x1b, 0xe9,
+ 0xe2, 0x10, 0x87, 0x75, 0x7e, 0x8c, 0x68, 0x9a, 0x91, 0x63,
+ 0x44, 0xb6, 0xbd, 0x4f, 0xab, 0x59, 0x52, 0xa0, 0x1c, 0xee,
+ 0xe5, 0x17, 0xf3, 0x01, 0x0a, 0xf8, 0xdf, 0x2d, 0x26, 0xd4,
+ 0x30, 0xc2, 0xc9, 0x3b, 0xfa, 0x08, 0x03, 0xf1, 0x15, 0xe7,
+ 0xec, 0x1e, 0x39, 0xcb, 0xc0, 0x32, 0xd6, 0x24, 0x2f, 0xdd,
+ 0x61, 0x93, 0x98, 0x6a, 0x8e, 0x7c, 0x77, 0x85, 0xa2, 0x50,
+ 0x5b, 0xa9, 0x4d, 0xbf, 0xb4, 0x46, 0xd1, 0x23, 0x28, 0xda,
+ 0x3e, 0xcc, 0xc7, 0x35, 0x12, 0xe0, 0xeb, 0x19, 0xfd, 0x0f,
+ 0x04, 0xf6, 0x4a, 0xb8, 0xb3, 0x41, 0xa5, 0x57, 0x5c, 0xae,
+ 0x89, 0x7b, 0x70, 0x82, 0x66, 0x94, 0x9f, 0x6d, 0x00, 0xf3,
+ 0xfb, 0x08, 0xeb, 0x18, 0x10, 0xe3, 0xcb, 0x38, 0x30, 0xc3,
+ 0x20, 0xd3, 0xdb, 0x28, 0x8b, 0x78, 0x70, 0x83, 0x60, 0x93,
+ 0x9b, 0x68, 0x40, 0xb3, 0xbb, 0x48, 0xab, 0x58, 0x50, 0xa3,
+ 0x0b, 0xf8, 0xf0, 0x03, 0xe0, 0x13, 0x1b, 0xe8, 0xc0, 0x33,
+ 0x3b, 0xc8, 0x2b, 0xd8, 0xd0, 0x23, 0x80, 0x73, 0x7b, 0x88,
+ 0x6b, 0x98, 0x90, 0x63, 0x4b, 0xb8, 0xb0, 0x43, 0xa0, 0x53,
+ 0x5b, 0xa8, 0x16, 0xe5, 0xed, 0x1e, 0xfd, 0x0e, 0x06, 0xf5,
+ 0xdd, 0x2e, 0x26, 0xd5, 0x36, 0xc5, 0xcd, 0x3e, 0x9d, 0x6e,
+ 0x66, 0x95, 0x76, 0x85, 0x8d, 0x7e, 0x56, 0xa5, 0xad, 0x5e,
+ 0xbd, 0x4e, 0x46, 0xb5, 0x1d, 0xee, 0xe6, 0x15, 0xf6, 0x05,
+ 0x0d, 0xfe, 0xd6, 0x25, 0x2d, 0xde, 0x3d, 0xce, 0xc6, 0x35,
+ 0x96, 0x65, 0x6d, 0x9e, 0x7d, 0x8e, 0x86, 0x75, 0x5d, 0xae,
+ 0xa6, 0x55, 0xb6, 0x45, 0x4d, 0xbe, 0x2c, 0xdf, 0xd7, 0x24,
+ 0xc7, 0x34, 0x3c, 0xcf, 0xe7, 0x14, 0x1c, 0xef, 0x0c, 0xff,
+ 0xf7, 0x04, 0xa7, 0x54, 0x5c, 0xaf, 0x4c, 0xbf, 0xb7, 0x44,
+ 0x6c, 0x9f, 0x97, 0x64, 0x87, 0x74, 0x7c, 0x8f, 0x27, 0xd4,
+ 0xdc, 0x2f, 0xcc, 0x3f, 0x37, 0xc4, 0xec, 0x1f, 0x17, 0xe4,
+ 0x07, 0xf4, 0xfc, 0x0f, 0xac, 0x5f, 0x57, 0xa4, 0x47, 0xb4,
+ 0xbc, 0x4f, 0x67, 0x94, 0x9c, 0x6f, 0x8c, 0x7f, 0x77, 0x84,
+ 0x3a, 0xc9, 0xc1, 0x32, 0xd1, 0x22, 0x2a, 0xd9, 0xf1, 0x02,
+ 0x0a, 0xf9, 0x1a, 0xe9, 0xe1, 0x12, 0xb1, 0x42, 0x4a, 0xb9,
+ 0x5a, 0xa9, 0xa1, 0x52, 0x7a, 0x89, 0x81, 0x72, 0x91, 0x62,
+ 0x6a, 0x99, 0x31, 0xc2, 0xca, 0x39, 0xda, 0x29, 0x21, 0xd2,
+ 0xfa, 0x09, 0x01, 0xf2, 0x11, 0xe2, 0xea, 0x19, 0xba, 0x49,
+ 0x41, 0xb2, 0x51, 0xa2, 0xaa, 0x59, 0x71, 0x82, 0x8a, 0x79,
+ 0x9a, 0x69, 0x61, 0x92, 0x00, 0xf4, 0xf5, 0x01, 0xf7, 0x03,
+ 0x02, 0xf6, 0xf3, 0x07, 0x06, 0xf2, 0x04, 0xf0, 0xf1, 0x05,
+ 0xfb, 0x0f, 0x0e, 0xfa, 0x0c, 0xf8, 0xf9, 0x0d, 0x08, 0xfc,
+ 0xfd, 0x09, 0xff, 0x0b, 0x0a, 0xfe, 0xeb, 0x1f, 0x1e, 0xea,
+ 0x1c, 0xe8, 0xe9, 0x1d, 0x18, 0xec, 0xed, 0x19, 0xef, 0x1b,
+ 0x1a, 0xee, 0x10, 0xe4, 0xe5, 0x11, 0xe7, 0x13, 0x12, 0xe6,
+ 0xe3, 0x17, 0x16, 0xe2, 0x14, 0xe0, 0xe1, 0x15, 0xcb, 0x3f,
+ 0x3e, 0xca, 0x3c, 0xc8, 0xc9, 0x3d, 0x38, 0xcc, 0xcd, 0x39,
+ 0xcf, 0x3b, 0x3a, 0xce, 0x30, 0xc4, 0xc5, 0x31, 0xc7, 0x33,
+ 0x32, 0xc6, 0xc3, 0x37, 0x36, 0xc2, 0x34, 0xc0, 0xc1, 0x35,
+ 0x20, 0xd4, 0xd5, 0x21, 0xd7, 0x23, 0x22, 0xd6, 0xd3, 0x27,
+ 0x26, 0xd2, 0x24, 0xd0, 0xd1, 0x25, 0xdb, 0x2f, 0x2e, 0xda,
+ 0x2c, 0xd8, 0xd9, 0x2d, 0x28, 0xdc, 0xdd, 0x29, 0xdf, 0x2b,
+ 0x2a, 0xde, 0x8b, 0x7f, 0x7e, 0x8a, 0x7c, 0x88, 0x89, 0x7d,
+ 0x78, 0x8c, 0x8d, 0x79, 0x8f, 0x7b, 0x7a, 0x8e, 0x70, 0x84,
+ 0x85, 0x71, 0x87, 0x73, 0x72, 0x86, 0x83, 0x77, 0x76, 0x82,
+ 0x74, 0x80, 0x81, 0x75, 0x60, 0x94, 0x95, 0x61, 0x97, 0x63,
+ 0x62, 0x96, 0x93, 0x67, 0x66, 0x92, 0x64, 0x90, 0x91, 0x65,
+ 0x9b, 0x6f, 0x6e, 0x9a, 0x6c, 0x98, 0x99, 0x6d, 0x68, 0x9c,
+ 0x9d, 0x69, 0x9f, 0x6b, 0x6a, 0x9e, 0x40, 0xb4, 0xb5, 0x41,
+ 0xb7, 0x43, 0x42, 0xb6, 0xb3, 0x47, 0x46, 0xb2, 0x44, 0xb0,
+ 0xb1, 0x45, 0xbb, 0x4f, 0x4e, 0xba, 0x4c, 0xb8, 0xb9, 0x4d,
+ 0x48, 0xbc, 0xbd, 0x49, 0xbf, 0x4b, 0x4a, 0xbe, 0xab, 0x5f,
+ 0x5e, 0xaa, 0x5c, 0xa8, 0xa9, 0x5d, 0x58, 0xac, 0xad, 0x59,
+ 0xaf, 0x5b, 0x5a, 0xae, 0x50, 0xa4, 0xa5, 0x51, 0xa7, 0x53,
+ 0x52, 0xa6, 0xa3, 0x57, 0x56, 0xa2, 0x54, 0xa0, 0xa1, 0x55,
+ 0x00, 0xf5, 0xf7, 0x02, 0xf3, 0x06, 0x04, 0xf1, 0xfb, 0x0e,
+ 0x0c, 0xf9, 0x08, 0xfd, 0xff, 0x0a, 0xeb, 0x1e, 0x1c, 0xe9,
+ 0x18, 0xed, 0xef, 0x1a, 0x10, 0xe5, 0xe7, 0x12, 0xe3, 0x16,
+ 0x14, 0xe1, 0xcb, 0x3e, 0x3c, 0xc9, 0x38, 0xcd, 0xcf, 0x3a,
+ 0x30, 0xc5, 0xc7, 0x32, 0xc3, 0x36, 0x34, 0xc1, 0x20, 0xd5,
+ 0xd7, 0x22, 0xd3, 0x26, 0x24, 0xd1, 0xdb, 0x2e, 0x2c, 0xd9,
+ 0x28, 0xdd, 0xdf, 0x2a, 0x8b, 0x7e, 0x7c, 0x89, 0x78, 0x8d,
+ 0x8f, 0x7a, 0x70, 0x85, 0x87, 0x72, 0x83, 0x76, 0x74, 0x81,
+ 0x60, 0x95, 0x97, 0x62, 0x93, 0x66, 0x64, 0x91, 0x9b, 0x6e,
+ 0x6c, 0x99, 0x68, 0x9d, 0x9f, 0x6a, 0x40, 0xb5, 0xb7, 0x42,
+ 0xb3, 0x46, 0x44, 0xb1, 0xbb, 0x4e, 0x4c, 0xb9, 0x48, 0xbd,
+ 0xbf, 0x4a, 0xab, 0x5e, 0x5c, 0xa9, 0x58, 0xad, 0xaf, 0x5a,
+ 0x50, 0xa5, 0xa7, 0x52, 0xa3, 0x56, 0x54, 0xa1, 0x0b, 0xfe,
+ 0xfc, 0x09, 0xf8, 0x0d, 0x0f, 0xfa, 0xf0, 0x05, 0x07, 0xf2,
+ 0x03, 0xf6, 0xf4, 0x01, 0xe0, 0x15, 0x17, 0xe2, 0x13, 0xe6,
+ 0xe4, 0x11, 0x1b, 0xee, 0xec, 0x19, 0xe8, 0x1d, 0x1f, 0xea,
+ 0xc0, 0x35, 0x37, 0xc2, 0x33, 0xc6, 0xc4, 0x31, 0x3b, 0xce,
+ 0xcc, 0x39, 0xc8, 0x3d, 0x3f, 0xca, 0x2b, 0xde, 0xdc, 0x29,
+ 0xd8, 0x2d, 0x2f, 0xda, 0xd0, 0x25, 0x27, 0xd2, 0x23, 0xd6,
+ 0xd4, 0x21, 0x80, 0x75, 0x77, 0x82, 0x73, 0x86, 0x84, 0x71,
+ 0x7b, 0x8e, 0x8c, 0x79, 0x88, 0x7d, 0x7f, 0x8a, 0x6b, 0x9e,
+ 0x9c, 0x69, 0x98, 0x6d, 0x6f, 0x9a, 0x90, 0x65, 0x67, 0x92,
+ 0x63, 0x96, 0x94, 0x61, 0x4b, 0xbe, 0xbc, 0x49, 0xb8, 0x4d,
+ 0x4f, 0xba, 0xb0, 0x45, 0x47, 0xb2, 0x43, 0xb6, 0xb4, 0x41,
+ 0xa0, 0x55, 0x57, 0xa2, 0x53, 0xa6, 0xa4, 0x51, 0x5b, 0xae,
+ 0xac, 0x59, 0xa8, 0x5d, 0x5f, 0xaa, 0x00, 0xf6, 0xf1, 0x07,
+ 0xff, 0x09, 0x0e, 0xf8, 0xe3, 0x15, 0x12, 0xe4, 0x1c, 0xea,
+ 0xed, 0x1b, 0xdb, 0x2d, 0x2a, 0xdc, 0x24, 0xd2, 0xd5, 0x23,
+ 0x38, 0xce, 0xc9, 0x3f, 0xc7, 0x31, 0x36, 0xc0, 0xab, 0x5d,
+ 0x5a, 0xac, 0x54, 0xa2, 0xa5, 0x53, 0x48, 0xbe, 0xb9, 0x4f,
+ 0xb7, 0x41, 0x46, 0xb0, 0x70, 0x86, 0x81, 0x77, 0x8f, 0x79,
+ 0x7e, 0x88, 0x93, 0x65, 0x62, 0x94, 0x6c, 0x9a, 0x9d, 0x6b,
+ 0x4b, 0xbd, 0xba, 0x4c, 0xb4, 0x42, 0x45, 0xb3, 0xa8, 0x5e,
+ 0x59, 0xaf, 0x57, 0xa1, 0xa6, 0x50, 0x90, 0x66, 0x61, 0x97,
+ 0x6f, 0x99, 0x9e, 0x68, 0x73, 0x85, 0x82, 0x74, 0x8c, 0x7a,
+ 0x7d, 0x8b, 0xe0, 0x16, 0x11, 0xe7, 0x1f, 0xe9, 0xee, 0x18,
+ 0x03, 0xf5, 0xf2, 0x04, 0xfc, 0x0a, 0x0d, 0xfb, 0x3b, 0xcd,
+ 0xca, 0x3c, 0xc4, 0x32, 0x35, 0xc3, 0xd8, 0x2e, 0x29, 0xdf,
+ 0x27, 0xd1, 0xd6, 0x20, 0x96, 0x60, 0x67, 0x91, 0x69, 0x9f,
+ 0x98, 0x6e, 0x75, 0x83, 0x84, 0x72, 0x8a, 0x7c, 0x7b, 0x8d,
+ 0x4d, 0xbb, 0xbc, 0x4a, 0xb2, 0x44, 0x43, 0xb5, 0xae, 0x58,
+ 0x5f, 0xa9, 0x51, 0xa7, 0xa0, 0x56, 0x3d, 0xcb, 0xcc, 0x3a,
+ 0xc2, 0x34, 0x33, 0xc5, 0xde, 0x28, 0x2f, 0xd9, 0x21, 0xd7,
+ 0xd0, 0x26, 0xe6, 0x10, 0x17, 0xe1, 0x19, 0xef, 0xe8, 0x1e,
+ 0x05, 0xf3, 0xf4, 0x02, 0xfa, 0x0c, 0x0b, 0xfd, 0xdd, 0x2b,
+ 0x2c, 0xda, 0x22, 0xd4, 0xd3, 0x25, 0x3e, 0xc8, 0xcf, 0x39,
+ 0xc1, 0x37, 0x30, 0xc6, 0x06, 0xf0, 0xf7, 0x01, 0xf9, 0x0f,
+ 0x08, 0xfe, 0xe5, 0x13, 0x14, 0xe2, 0x1a, 0xec, 0xeb, 0x1d,
+ 0x76, 0x80, 0x87, 0x71, 0x89, 0x7f, 0x78, 0x8e, 0x95, 0x63,
+ 0x64, 0x92, 0x6a, 0x9c, 0x9b, 0x6d, 0xad, 0x5b, 0x5c, 0xaa,
+ 0x52, 0xa4, 0xa3, 0x55, 0x4e, 0xb8, 0xbf, 0x49, 0xb1, 0x47,
+ 0x40, 0xb6, 0x00, 0xf7, 0xf3, 0x04, 0xfb, 0x0c, 0x08, 0xff,
+ 0xeb, 0x1c, 0x18, 0xef, 0x10, 0xe7, 0xe3, 0x14, 0xcb, 0x3c,
+ 0x38, 0xcf, 0x30, 0xc7, 0xc3, 0x34, 0x20, 0xd7, 0xd3, 0x24,
+ 0xdb, 0x2c, 0x28, 0xdf, 0x8b, 0x7c, 0x78, 0x8f, 0x70, 0x87,
+ 0x83, 0x74, 0x60, 0x97, 0x93, 0x64, 0x9b, 0x6c, 0x68, 0x9f,
+ 0x40, 0xb7, 0xb3, 0x44, 0xbb, 0x4c, 0x48, 0xbf, 0xab, 0x5c,
+ 0x58, 0xaf, 0x50, 0xa7, 0xa3, 0x54, 0x0b, 0xfc, 0xf8, 0x0f,
+ 0xf0, 0x07, 0x03, 0xf4, 0xe0, 0x17, 0x13, 0xe4, 0x1b, 0xec,
+ 0xe8, 0x1f, 0xc0, 0x37, 0x33, 0xc4, 0x3b, 0xcc, 0xc8, 0x3f,
+ 0x2b, 0xdc, 0xd8, 0x2f, 0xd0, 0x27, 0x23, 0xd4, 0x80, 0x77,
+ 0x73, 0x84, 0x7b, 0x8c, 0x88, 0x7f, 0x6b, 0x9c, 0x98, 0x6f,
+ 0x90, 0x67, 0x63, 0x94, 0x4b, 0xbc, 0xb8, 0x4f, 0xb0, 0x47,
+ 0x43, 0xb4, 0xa0, 0x57, 0x53, 0xa4, 0x5b, 0xac, 0xa8, 0x5f,
+ 0x16, 0xe1, 0xe5, 0x12, 0xed, 0x1a, 0x1e, 0xe9, 0xfd, 0x0a,
+ 0x0e, 0xf9, 0x06, 0xf1, 0xf5, 0x02, 0xdd, 0x2a, 0x2e, 0xd9,
+ 0x26, 0xd1, 0xd5, 0x22, 0x36, 0xc1, 0xc5, 0x32, 0xcd, 0x3a,
+ 0x3e, 0xc9, 0x9d, 0x6a, 0x6e, 0x99, 0x66, 0x91, 0x95, 0x62,
+ 0x76, 0x81, 0x85, 0x72, 0x8d, 0x7a, 0x7e, 0x89, 0x56, 0xa1,
+ 0xa5, 0x52, 0xad, 0x5a, 0x5e, 0xa9, 0xbd, 0x4a, 0x4e, 0xb9,
+ 0x46, 0xb1, 0xb5, 0x42, 0x1d, 0xea, 0xee, 0x19, 0xe6, 0x11,
+ 0x15, 0xe2, 0xf6, 0x01, 0x05, 0xf2, 0x0d, 0xfa, 0xfe, 0x09,
+ 0xd6, 0x21, 0x25, 0xd2, 0x2d, 0xda, 0xde, 0x29, 0x3d, 0xca,
+ 0xce, 0x39, 0xc6, 0x31, 0x35, 0xc2, 0x96, 0x61, 0x65, 0x92,
+ 0x6d, 0x9a, 0x9e, 0x69, 0x7d, 0x8a, 0x8e, 0x79, 0x86, 0x71,
+ 0x75, 0x82, 0x5d, 0xaa, 0xae, 0x59, 0xa6, 0x51, 0x55, 0xa2,
+ 0xb6, 0x41, 0x45, 0xb2, 0x4d, 0xba, 0xbe, 0x49, 0x00, 0xf8,
+ 0xed, 0x15, 0xc7, 0x3f, 0x2a, 0xd2, 0x93, 0x6b, 0x7e, 0x86,
+ 0x54, 0xac, 0xb9, 0x41, 0x3b, 0xc3, 0xd6, 0x2e, 0xfc, 0x04,
+ 0x11, 0xe9, 0xa8, 0x50, 0x45, 0xbd, 0x6f, 0x97, 0x82, 0x7a,
+ 0x76, 0x8e, 0x9b, 0x63, 0xb1, 0x49, 0x5c, 0xa4, 0xe5, 0x1d,
+ 0x08, 0xf0, 0x22, 0xda, 0xcf, 0x37, 0x4d, 0xb5, 0xa0, 0x58,
+ 0x8a, 0x72, 0x67, 0x9f, 0xde, 0x26, 0x33, 0xcb, 0x19, 0xe1,
+ 0xf4, 0x0c, 0xec, 0x14, 0x01, 0xf9, 0x2b, 0xd3, 0xc6, 0x3e,
+ 0x7f, 0x87, 0x92, 0x6a, 0xb8, 0x40, 0x55, 0xad, 0xd7, 0x2f,
+ 0x3a, 0xc2, 0x10, 0xe8, 0xfd, 0x05, 0x44, 0xbc, 0xa9, 0x51,
+ 0x83, 0x7b, 0x6e, 0x96, 0x9a, 0x62, 0x77, 0x8f, 0x5d, 0xa5,
+ 0xb0, 0x48, 0x09, 0xf1, 0xe4, 0x1c, 0xce, 0x36, 0x23, 0xdb,
+ 0xa1, 0x59, 0x4c, 0xb4, 0x66, 0x9e, 0x8b, 0x73, 0x32, 0xca,
+ 0xdf, 0x27, 0xf5, 0x0d, 0x18, 0xe0, 0xc5, 0x3d, 0x28, 0xd0,
+ 0x02, 0xfa, 0xef, 0x17, 0x56, 0xae, 0xbb, 0x43, 0x91, 0x69,
+ 0x7c, 0x84, 0xfe, 0x06, 0x13, 0xeb, 0x39, 0xc1, 0xd4, 0x2c,
+ 0x6d, 0x95, 0x80, 0x78, 0xaa, 0x52, 0x47, 0xbf, 0xb3, 0x4b,
+ 0x5e, 0xa6, 0x74, 0x8c, 0x99, 0x61, 0x20, 0xd8, 0xcd, 0x35,
+ 0xe7, 0x1f, 0x0a, 0xf2, 0x88, 0x70, 0x65, 0x9d, 0x4f, 0xb7,
+ 0xa2, 0x5a, 0x1b, 0xe3, 0xf6, 0x0e, 0xdc, 0x24, 0x31, 0xc9,
+ 0x29, 0xd1, 0xc4, 0x3c, 0xee, 0x16, 0x03, 0xfb, 0xba, 0x42,
+ 0x57, 0xaf, 0x7d, 0x85, 0x90, 0x68, 0x12, 0xea, 0xff, 0x07,
+ 0xd5, 0x2d, 0x38, 0xc0, 0x81, 0x79, 0x6c, 0x94, 0x46, 0xbe,
+ 0xab, 0x53, 0x5f, 0xa7, 0xb2, 0x4a, 0x98, 0x60, 0x75, 0x8d,
+ 0xcc, 0x34, 0x21, 0xd9, 0x0b, 0xf3, 0xe6, 0x1e, 0x64, 0x9c,
+ 0x89, 0x71, 0xa3, 0x5b, 0x4e, 0xb6, 0xf7, 0x0f, 0x1a, 0xe2,
+ 0x30, 0xc8, 0xdd, 0x25, 0x00, 0xf9, 0xef, 0x16, 0xc3, 0x3a,
+ 0x2c, 0xd5, 0x9b, 0x62, 0x74, 0x8d, 0x58, 0xa1, 0xb7, 0x4e,
+ 0x2b, 0xd2, 0xc4, 0x3d, 0xe8, 0x11, 0x07, 0xfe, 0xb0, 0x49,
+ 0x5f, 0xa6, 0x73, 0x8a, 0x9c, 0x65, 0x56, 0xaf, 0xb9, 0x40,
+ 0x95, 0x6c, 0x7a, 0x83, 0xcd, 0x34, 0x22, 0xdb, 0x0e, 0xf7,
+ 0xe1, 0x18, 0x7d, 0x84, 0x92, 0x6b, 0xbe, 0x47, 0x51, 0xa8,
+ 0xe6, 0x1f, 0x09, 0xf0, 0x25, 0xdc, 0xca, 0x33, 0xac, 0x55,
+ 0x43, 0xba, 0x6f, 0x96, 0x80, 0x79, 0x37, 0xce, 0xd8, 0x21,
+ 0xf4, 0x0d, 0x1b, 0xe2, 0x87, 0x7e, 0x68, 0x91, 0x44, 0xbd,
+ 0xab, 0x52, 0x1c, 0xe5, 0xf3, 0x0a, 0xdf, 0x26, 0x30, 0xc9,
+ 0xfa, 0x03, 0x15, 0xec, 0x39, 0xc0, 0xd6, 0x2f, 0x61, 0x98,
+ 0x8e, 0x77, 0xa2, 0x5b, 0x4d, 0xb4, 0xd1, 0x28, 0x3e, 0xc7,
+ 0x12, 0xeb, 0xfd, 0x04, 0x4a, 0xb3, 0xa5, 0x5c, 0x89, 0x70,
+ 0x66, 0x9f, 0x45, 0xbc, 0xaa, 0x53, 0x86, 0x7f, 0x69, 0x90,
+ 0xde, 0x27, 0x31, 0xc8, 0x1d, 0xe4, 0xf2, 0x0b, 0x6e, 0x97,
+ 0x81, 0x78, 0xad, 0x54, 0x42, 0xbb, 0xf5, 0x0c, 0x1a, 0xe3,
+ 0x36, 0xcf, 0xd9, 0x20, 0x13, 0xea, 0xfc, 0x05, 0xd0, 0x29,
+ 0x3f, 0xc6, 0x88, 0x71, 0x67, 0x9e, 0x4b, 0xb2, 0xa4, 0x5d,
+ 0x38, 0xc1, 0xd7, 0x2e, 0xfb, 0x02, 0x14, 0xed, 0xa3, 0x5a,
+ 0x4c, 0xb5, 0x60, 0x99, 0x8f, 0x76, 0xe9, 0x10, 0x06, 0xff,
+ 0x2a, 0xd3, 0xc5, 0x3c, 0x72, 0x8b, 0x9d, 0x64, 0xb1, 0x48,
+ 0x5e, 0xa7, 0xc2, 0x3b, 0x2d, 0xd4, 0x01, 0xf8, 0xee, 0x17,
+ 0x59, 0xa0, 0xb6, 0x4f, 0x9a, 0x63, 0x75, 0x8c, 0xbf, 0x46,
+ 0x50, 0xa9, 0x7c, 0x85, 0x93, 0x6a, 0x24, 0xdd, 0xcb, 0x32,
+ 0xe7, 0x1e, 0x08, 0xf1, 0x94, 0x6d, 0x7b, 0x82, 0x57, 0xae,
+ 0xb8, 0x41, 0x0f, 0xf6, 0xe0, 0x19, 0xcc, 0x35, 0x23, 0xda,
+ 0x00, 0xfa, 0xe9, 0x13, 0xcf, 0x35, 0x26, 0xdc, 0x83, 0x79,
+ 0x6a, 0x90, 0x4c, 0xb6, 0xa5, 0x5f, 0x1b, 0xe1, 0xf2, 0x08,
+ 0xd4, 0x2e, 0x3d, 0xc7, 0x98, 0x62, 0x71, 0x8b, 0x57, 0xad,
+ 0xbe, 0x44, 0x36, 0xcc, 0xdf, 0x25, 0xf9, 0x03, 0x10, 0xea,
+ 0xb5, 0x4f, 0x5c, 0xa6, 0x7a, 0x80, 0x93, 0x69, 0x2d, 0xd7,
+ 0xc4, 0x3e, 0xe2, 0x18, 0x0b, 0xf1, 0xae, 0x54, 0x47, 0xbd,
+ 0x61, 0x9b, 0x88, 0x72, 0x6c, 0x96, 0x85, 0x7f, 0xa3, 0x59,
+ 0x4a, 0xb0, 0xef, 0x15, 0x06, 0xfc, 0x20, 0xda, 0xc9, 0x33,
+ 0x77, 0x8d, 0x9e, 0x64, 0xb8, 0x42, 0x51, 0xab, 0xf4, 0x0e,
+ 0x1d, 0xe7, 0x3b, 0xc1, 0xd2, 0x28, 0x5a, 0xa0, 0xb3, 0x49,
+ 0x95, 0x6f, 0x7c, 0x86, 0xd9, 0x23, 0x30, 0xca, 0x16, 0xec,
+ 0xff, 0x05, 0x41, 0xbb, 0xa8, 0x52, 0x8e, 0x74, 0x67, 0x9d,
+ 0xc2, 0x38, 0x2b, 0xd1, 0x0d, 0xf7, 0xe4, 0x1e, 0xd8, 0x22,
+ 0x31, 0xcb, 0x17, 0xed, 0xfe, 0x04, 0x5b, 0xa1, 0xb2, 0x48,
+ 0x94, 0x6e, 0x7d, 0x87, 0xc3, 0x39, 0x2a, 0xd0, 0x0c, 0xf6,
+ 0xe5, 0x1f, 0x40, 0xba, 0xa9, 0x53, 0x8f, 0x75, 0x66, 0x9c,
+ 0xee, 0x14, 0x07, 0xfd, 0x21, 0xdb, 0xc8, 0x32, 0x6d, 0x97,
+ 0x84, 0x7e, 0xa2, 0x58, 0x4b, 0xb1, 0xf5, 0x0f, 0x1c, 0xe6,
+ 0x3a, 0xc0, 0xd3, 0x29, 0x76, 0x8c, 0x9f, 0x65, 0xb9, 0x43,
+ 0x50, 0xaa, 0xb4, 0x4e, 0x5d, 0xa7, 0x7b, 0x81, 0x92, 0x68,
+ 0x37, 0xcd, 0xde, 0x24, 0xf8, 0x02, 0x11, 0xeb, 0xaf, 0x55,
+ 0x46, 0xbc, 0x60, 0x9a, 0x89, 0x73, 0x2c, 0xd6, 0xc5, 0x3f,
+ 0xe3, 0x19, 0x0a, 0xf0, 0x82, 0x78, 0x6b, 0x91, 0x4d, 0xb7,
+ 0xa4, 0x5e, 0x01, 0xfb, 0xe8, 0x12, 0xce, 0x34, 0x27, 0xdd,
+ 0x99, 0x63, 0x70, 0x8a, 0x56, 0xac, 0xbf, 0x45, 0x1a, 0xe0,
+ 0xf3, 0x09, 0xd5, 0x2f, 0x3c, 0xc6, 0x00, 0xfb, 0xeb, 0x10,
+ 0xcb, 0x30, 0x20, 0xdb, 0x8b, 0x70, 0x60, 0x9b, 0x40, 0xbb,
+ 0xab, 0x50, 0x0b, 0xf0, 0xe0, 0x1b, 0xc0, 0x3b, 0x2b, 0xd0,
+ 0x80, 0x7b, 0x6b, 0x90, 0x4b, 0xb0, 0xa0, 0x5b, 0x16, 0xed,
+ 0xfd, 0x06, 0xdd, 0x26, 0x36, 0xcd, 0x9d, 0x66, 0x76, 0x8d,
+ 0x56, 0xad, 0xbd, 0x46, 0x1d, 0xe6, 0xf6, 0x0d, 0xd6, 0x2d,
+ 0x3d, 0xc6, 0x96, 0x6d, 0x7d, 0x86, 0x5d, 0xa6, 0xb6, 0x4d,
+ 0x2c, 0xd7, 0xc7, 0x3c, 0xe7, 0x1c, 0x0c, 0xf7, 0xa7, 0x5c,
+ 0x4c, 0xb7, 0x6c, 0x97, 0x87, 0x7c, 0x27, 0xdc, 0xcc, 0x37,
+ 0xec, 0x17, 0x07, 0xfc, 0xac, 0x57, 0x47, 0xbc, 0x67, 0x9c,
+ 0x8c, 0x77, 0x3a, 0xc1, 0xd1, 0x2a, 0xf1, 0x0a, 0x1a, 0xe1,
+ 0xb1, 0x4a, 0x5a, 0xa1, 0x7a, 0x81, 0x91, 0x6a, 0x31, 0xca,
+ 0xda, 0x21, 0xfa, 0x01, 0x11, 0xea, 0xba, 0x41, 0x51, 0xaa,
+ 0x71, 0x8a, 0x9a, 0x61, 0x58, 0xa3, 0xb3, 0x48, 0x93, 0x68,
+ 0x78, 0x83, 0xd3, 0x28, 0x38, 0xc3, 0x18, 0xe3, 0xf3, 0x08,
+ 0x53, 0xa8, 0xb8, 0x43, 0x98, 0x63, 0x73, 0x88, 0xd8, 0x23,
+ 0x33, 0xc8, 0x13, 0xe8, 0xf8, 0x03, 0x4e, 0xb5, 0xa5, 0x5e,
+ 0x85, 0x7e, 0x6e, 0x95, 0xc5, 0x3e, 0x2e, 0xd5, 0x0e, 0xf5,
+ 0xe5, 0x1e, 0x45, 0xbe, 0xae, 0x55, 0x8e, 0x75, 0x65, 0x9e,
+ 0xce, 0x35, 0x25, 0xde, 0x05, 0xfe, 0xee, 0x15, 0x74, 0x8f,
+ 0x9f, 0x64, 0xbf, 0x44, 0x54, 0xaf, 0xff, 0x04, 0x14, 0xef,
+ 0x34, 0xcf, 0xdf, 0x24, 0x7f, 0x84, 0x94, 0x6f, 0xb4, 0x4f,
+ 0x5f, 0xa4, 0xf4, 0x0f, 0x1f, 0xe4, 0x3f, 0xc4, 0xd4, 0x2f,
+ 0x62, 0x99, 0x89, 0x72, 0xa9, 0x52, 0x42, 0xb9, 0xe9, 0x12,
+ 0x02, 0xf9, 0x22, 0xd9, 0xc9, 0x32, 0x69, 0x92, 0x82, 0x79,
+ 0xa2, 0x59, 0x49, 0xb2, 0xe2, 0x19, 0x09, 0xf2, 0x29, 0xd2,
+ 0xc2, 0x39, 0x00, 0xfc, 0xe5, 0x19, 0xd7, 0x2b, 0x32, 0xce,
+ 0xb3, 0x4f, 0x56, 0xaa, 0x64, 0x98, 0x81, 0x7d, 0x7b, 0x87,
+ 0x9e, 0x62, 0xac, 0x50, 0x49, 0xb5, 0xc8, 0x34, 0x2d, 0xd1,
+ 0x1f, 0xe3, 0xfa, 0x06, 0xf6, 0x0a, 0x13, 0xef, 0x21, 0xdd,
+ 0xc4, 0x38, 0x45, 0xb9, 0xa0, 0x5c, 0x92, 0x6e, 0x77, 0x8b,
+ 0x8d, 0x71, 0x68, 0x94, 0x5a, 0xa6, 0xbf, 0x43, 0x3e, 0xc2,
+ 0xdb, 0x27, 0xe9, 0x15, 0x0c, 0xf0, 0xf1, 0x0d, 0x14, 0xe8,
+ 0x26, 0xda, 0xc3, 0x3f, 0x42, 0xbe, 0xa7, 0x5b, 0x95, 0x69,
+ 0x70, 0x8c, 0x8a, 0x76, 0x6f, 0x93, 0x5d, 0xa1, 0xb8, 0x44,
+ 0x39, 0xc5, 0xdc, 0x20, 0xee, 0x12, 0x0b, 0xf7, 0x07, 0xfb,
+ 0xe2, 0x1e, 0xd0, 0x2c, 0x35, 0xc9, 0xb4, 0x48, 0x51, 0xad,
+ 0x63, 0x9f, 0x86, 0x7a, 0x7c, 0x80, 0x99, 0x65, 0xab, 0x57,
+ 0x4e, 0xb2, 0xcf, 0x33, 0x2a, 0xd6, 0x18, 0xe4, 0xfd, 0x01,
+ 0xff, 0x03, 0x1a, 0xe6, 0x28, 0xd4, 0xcd, 0x31, 0x4c, 0xb0,
+ 0xa9, 0x55, 0x9b, 0x67, 0x7e, 0x82, 0x84, 0x78, 0x61, 0x9d,
+ 0x53, 0xaf, 0xb6, 0x4a, 0x37, 0xcb, 0xd2, 0x2e, 0xe0, 0x1c,
+ 0x05, 0xf9, 0x09, 0xf5, 0xec, 0x10, 0xde, 0x22, 0x3b, 0xc7,
+ 0xba, 0x46, 0x5f, 0xa3, 0x6d, 0x91, 0x88, 0x74, 0x72, 0x8e,
+ 0x97, 0x6b, 0xa5, 0x59, 0x40, 0xbc, 0xc1, 0x3d, 0x24, 0xd8,
+ 0x16, 0xea, 0xf3, 0x0f, 0x0e, 0xf2, 0xeb, 0x17, 0xd9, 0x25,
+ 0x3c, 0xc0, 0xbd, 0x41, 0x58, 0xa4, 0x6a, 0x96, 0x8f, 0x73,
+ 0x75, 0x89, 0x90, 0x6c, 0xa2, 0x5e, 0x47, 0xbb, 0xc6, 0x3a,
+ 0x23, 0xdf, 0x11, 0xed, 0xf4, 0x08, 0xf8, 0x04, 0x1d, 0xe1,
+ 0x2f, 0xd3, 0xca, 0x36, 0x4b, 0xb7, 0xae, 0x52, 0x9c, 0x60,
+ 0x79, 0x85, 0x83, 0x7f, 0x66, 0x9a, 0x54, 0xa8, 0xb1, 0x4d,
+ 0x30, 0xcc, 0xd5, 0x29, 0xe7, 0x1b, 0x02, 0xfe, 0x00, 0xfd,
+ 0xe7, 0x1a, 0xd3, 0x2e, 0x34, 0xc9, 0xbb, 0x46, 0x5c, 0xa1,
+ 0x68, 0x95, 0x8f, 0x72, 0x6b, 0x96, 0x8c, 0x71, 0xb8, 0x45,
+ 0x5f, 0xa2, 0xd0, 0x2d, 0x37, 0xca, 0x03, 0xfe, 0xe4, 0x19,
+ 0xd6, 0x2b, 0x31, 0xcc, 0x05, 0xf8, 0xe2, 0x1f, 0x6d, 0x90,
+ 0x8a, 0x77, 0xbe, 0x43, 0x59, 0xa4, 0xbd, 0x40, 0x5a, 0xa7,
+ 0x6e, 0x93, 0x89, 0x74, 0x06, 0xfb, 0xe1, 0x1c, 0xd5, 0x28,
+ 0x32, 0xcf, 0xb1, 0x4c, 0x56, 0xab, 0x62, 0x9f, 0x85, 0x78,
+ 0x0a, 0xf7, 0xed, 0x10, 0xd9, 0x24, 0x3e, 0xc3, 0xda, 0x27,
+ 0x3d, 0xc0, 0x09, 0xf4, 0xee, 0x13, 0x61, 0x9c, 0x86, 0x7b,
+ 0xb2, 0x4f, 0x55, 0xa8, 0x67, 0x9a, 0x80, 0x7d, 0xb4, 0x49,
+ 0x53, 0xae, 0xdc, 0x21, 0x3b, 0xc6, 0x0f, 0xf2, 0xe8, 0x15,
+ 0x0c, 0xf1, 0xeb, 0x16, 0xdf, 0x22, 0x38, 0xc5, 0xb7, 0x4a,
+ 0x50, 0xad, 0x64, 0x99, 0x83, 0x7e, 0x7f, 0x82, 0x98, 0x65,
+ 0xac, 0x51, 0x4b, 0xb6, 0xc4, 0x39, 0x23, 0xde, 0x17, 0xea,
+ 0xf0, 0x0d, 0x14, 0xe9, 0xf3, 0x0e, 0xc7, 0x3a, 0x20, 0xdd,
+ 0xaf, 0x52, 0x48, 0xb5, 0x7c, 0x81, 0x9b, 0x66, 0xa9, 0x54,
+ 0x4e, 0xb3, 0x7a, 0x87, 0x9d, 0x60, 0x12, 0xef, 0xf5, 0x08,
+ 0xc1, 0x3c, 0x26, 0xdb, 0xc2, 0x3f, 0x25, 0xd8, 0x11, 0xec,
+ 0xf6, 0x0b, 0x79, 0x84, 0x9e, 0x63, 0xaa, 0x57, 0x4d, 0xb0,
+ 0xce, 0x33, 0x29, 0xd4, 0x1d, 0xe0, 0xfa, 0x07, 0x75, 0x88,
+ 0x92, 0x6f, 0xa6, 0x5b, 0x41, 0xbc, 0xa5, 0x58, 0x42, 0xbf,
+ 0x76, 0x8b, 0x91, 0x6c, 0x1e, 0xe3, 0xf9, 0x04, 0xcd, 0x30,
+ 0x2a, 0xd7, 0x18, 0xe5, 0xff, 0x02, 0xcb, 0x36, 0x2c, 0xd1,
+ 0xa3, 0x5e, 0x44, 0xb9, 0x70, 0x8d, 0x97, 0x6a, 0x73, 0x8e,
+ 0x94, 0x69, 0xa0, 0x5d, 0x47, 0xba, 0xc8, 0x35, 0x2f, 0xd2,
+ 0x1b, 0xe6, 0xfc, 0x01, 0x00, 0xfe, 0xe1, 0x1f, 0xdf, 0x21,
+ 0x3e, 0xc0, 0xa3, 0x5d, 0x42, 0xbc, 0x7c, 0x82, 0x9d, 0x63,
+ 0x5b, 0xa5, 0xba, 0x44, 0x84, 0x7a, 0x65, 0x9b, 0xf8, 0x06,
+ 0x19, 0xe7, 0x27, 0xd9, 0xc6, 0x38, 0xb6, 0x48, 0x57, 0xa9,
+ 0x69, 0x97, 0x88, 0x76, 0x15, 0xeb, 0xf4, 0x0a, 0xca, 0x34,
+ 0x2b, 0xd5, 0xed, 0x13, 0x0c, 0xf2, 0x32, 0xcc, 0xd3, 0x2d,
+ 0x4e, 0xb0, 0xaf, 0x51, 0x91, 0x6f, 0x70, 0x8e, 0x71, 0x8f,
+ 0x90, 0x6e, 0xae, 0x50, 0x4f, 0xb1, 0xd2, 0x2c, 0x33, 0xcd,
+ 0x0d, 0xf3, 0xec, 0x12, 0x2a, 0xd4, 0xcb, 0x35, 0xf5, 0x0b,
+ 0x14, 0xea, 0x89, 0x77, 0x68, 0x96, 0x56, 0xa8, 0xb7, 0x49,
+ 0xc7, 0x39, 0x26, 0xd8, 0x18, 0xe6, 0xf9, 0x07, 0x64, 0x9a,
+ 0x85, 0x7b, 0xbb, 0x45, 0x5a, 0xa4, 0x9c, 0x62, 0x7d, 0x83,
+ 0x43, 0xbd, 0xa2, 0x5c, 0x3f, 0xc1, 0xde, 0x20, 0xe0, 0x1e,
+ 0x01, 0xff, 0xe2, 0x1c, 0x03, 0xfd, 0x3d, 0xc3, 0xdc, 0x22,
+ 0x41, 0xbf, 0xa0, 0x5e, 0x9e, 0x60, 0x7f, 0x81, 0xb9, 0x47,
+ 0x58, 0xa6, 0x66, 0x98, 0x87, 0x79, 0x1a, 0xe4, 0xfb, 0x05,
+ 0xc5, 0x3b, 0x24, 0xda, 0x54, 0xaa, 0xb5, 0x4b, 0x8b, 0x75,
+ 0x6a, 0x94, 0xf7, 0x09, 0x16, 0xe8, 0x28, 0xd6, 0xc9, 0x37,
+ 0x0f, 0xf1, 0xee, 0x10, 0xd0, 0x2e, 0x31, 0xcf, 0xac, 0x52,
+ 0x4d, 0xb3, 0x73, 0x8d, 0x92, 0x6c, 0x93, 0x6d, 0x72, 0x8c,
+ 0x4c, 0xb2, 0xad, 0x53, 0x30, 0xce, 0xd1, 0x2f, 0xef, 0x11,
+ 0x0e, 0xf0, 0xc8, 0x36, 0x29, 0xd7, 0x17, 0xe9, 0xf6, 0x08,
+ 0x6b, 0x95, 0x8a, 0x74, 0xb4, 0x4a, 0x55, 0xab, 0x25, 0xdb,
+ 0xc4, 0x3a, 0xfa, 0x04, 0x1b, 0xe5, 0x86, 0x78, 0x67, 0x99,
+ 0x59, 0xa7, 0xb8, 0x46, 0x7e, 0x80, 0x9f, 0x61, 0xa1, 0x5f,
+ 0x40, 0xbe, 0xdd, 0x23, 0x3c, 0xc2, 0x02, 0xfc, 0xe3, 0x1d,
+ 0x00, 0xff, 0xe3, 0x1c, 0xdb, 0x24, 0x38, 0xc7, 0xab, 0x54,
+ 0x48, 0xb7, 0x70, 0x8f, 0x93, 0x6c, 0x4b, 0xb4, 0xa8, 0x57,
+ 0x90, 0x6f, 0x73, 0x8c, 0xe0, 0x1f, 0x03, 0xfc, 0x3b, 0xc4,
+ 0xd8, 0x27, 0x96, 0x69, 0x75, 0x8a, 0x4d, 0xb2, 0xae, 0x51,
+ 0x3d, 0xc2, 0xde, 0x21, 0xe6, 0x19, 0x05, 0xfa, 0xdd, 0x22,
+ 0x3e, 0xc1, 0x06, 0xf9, 0xe5, 0x1a, 0x76, 0x89, 0x95, 0x6a,
+ 0xad, 0x52, 0x4e, 0xb1, 0x31, 0xce, 0xd2, 0x2d, 0xea, 0x15,
+ 0x09, 0xf6, 0x9a, 0x65, 0x79, 0x86, 0x41, 0xbe, 0xa2, 0x5d,
+ 0x7a, 0x85, 0x99, 0x66, 0xa1, 0x5e, 0x42, 0xbd, 0xd1, 0x2e,
+ 0x32, 0xcd, 0x0a, 0xf5, 0xe9, 0x16, 0xa7, 0x58, 0x44, 0xbb,
+ 0x7c, 0x83, 0x9f, 0x60, 0x0c, 0xf3, 0xef, 0x10, 0xd7, 0x28,
+ 0x34, 0xcb, 0xec, 0x13, 0x0f, 0xf0, 0x37, 0xc8, 0xd4, 0x2b,
+ 0x47, 0xb8, 0xa4, 0x5b, 0x9c, 0x63, 0x7f, 0x80, 0x62, 0x9d,
+ 0x81, 0x7e, 0xb9, 0x46, 0x5a, 0xa5, 0xc9, 0x36, 0x2a, 0xd5,
+ 0x12, 0xed, 0xf1, 0x0e, 0x29, 0xd6, 0xca, 0x35, 0xf2, 0x0d,
+ 0x11, 0xee, 0x82, 0x7d, 0x61, 0x9e, 0x59, 0xa6, 0xba, 0x45,
+ 0xf4, 0x0b, 0x17, 0xe8, 0x2f, 0xd0, 0xcc, 0x33, 0x5f, 0xa0,
+ 0xbc, 0x43, 0x84, 0x7b, 0x67, 0x98, 0xbf, 0x40, 0x5c, 0xa3,
+ 0x64, 0x9b, 0x87, 0x78, 0x14, 0xeb, 0xf7, 0x08, 0xcf, 0x30,
+ 0x2c, 0xd3, 0x53, 0xac, 0xb0, 0x4f, 0x88, 0x77, 0x6b, 0x94,
+ 0xf8, 0x07, 0x1b, 0xe4, 0x23, 0xdc, 0xc0, 0x3f, 0x18, 0xe7,
+ 0xfb, 0x04, 0xc3, 0x3c, 0x20, 0xdf, 0xb3, 0x4c, 0x50, 0xaf,
+ 0x68, 0x97, 0x8b, 0x74, 0xc5, 0x3a, 0x26, 0xd9, 0x1e, 0xe1,
+ 0xfd, 0x02, 0x6e, 0x91, 0x8d, 0x72, 0xb5, 0x4a, 0x56, 0xa9,
+ 0x8e, 0x71, 0x6d, 0x92, 0x55, 0xaa, 0xb6, 0x49, 0x25, 0xda,
+ 0xc6, 0x39, 0xfe, 0x01, 0x1d, 0xe2
+};
+
+static const unsigned char gf_inv_table_base[] = {
+ 0x00, 0x01, 0x8e, 0xf4, 0x47, 0xa7, 0x7a, 0xba, 0xad, 0x9d,
+ 0xdd, 0x98, 0x3d, 0xaa, 0x5d, 0x96, 0xd8, 0x72, 0xc0, 0x58,
+ 0xe0, 0x3e, 0x4c, 0x66, 0x90, 0xde, 0x55, 0x80, 0xa0, 0x83,
+ 0x4b, 0x2a, 0x6c, 0xed, 0x39, 0x51, 0x60, 0x56, 0x2c, 0x8a,
+ 0x70, 0xd0, 0x1f, 0x4a, 0x26, 0x8b, 0x33, 0x6e, 0x48, 0x89,
+ 0x6f, 0x2e, 0xa4, 0xc3, 0x40, 0x5e, 0x50, 0x22, 0xcf, 0xa9,
+ 0xab, 0x0c, 0x15, 0xe1, 0x36, 0x5f, 0xf8, 0xd5, 0x92, 0x4e,
+ 0xa6, 0x04, 0x30, 0x88, 0x2b, 0x1e, 0x16, 0x67, 0x45, 0x93,
+ 0x38, 0x23, 0x68, 0x8c, 0x81, 0x1a, 0x25, 0x61, 0x13, 0xc1,
+ 0xcb, 0x63, 0x97, 0x0e, 0x37, 0x41, 0x24, 0x57, 0xca, 0x5b,
+ 0xb9, 0xc4, 0x17, 0x4d, 0x52, 0x8d, 0xef, 0xb3, 0x20, 0xec,
+ 0x2f, 0x32, 0x28, 0xd1, 0x11, 0xd9, 0xe9, 0xfb, 0xda, 0x79,
+ 0xdb, 0x77, 0x06, 0xbb, 0x84, 0xcd, 0xfe, 0xfc, 0x1b, 0x54,
+ 0xa1, 0x1d, 0x7c, 0xcc, 0xe4, 0xb0, 0x49, 0x31, 0x27, 0x2d,
+ 0x53, 0x69, 0x02, 0xf5, 0x18, 0xdf, 0x44, 0x4f, 0x9b, 0xbc,
+ 0x0f, 0x5c, 0x0b, 0xdc, 0xbd, 0x94, 0xac, 0x09, 0xc7, 0xa2,
+ 0x1c, 0x82, 0x9f, 0xc6, 0x34, 0xc2, 0x46, 0x05, 0xce, 0x3b,
+ 0x0d, 0x3c, 0x9c, 0x08, 0xbe, 0xb7, 0x87, 0xe5, 0xee, 0x6b,
+ 0xeb, 0xf2, 0xbf, 0xaf, 0xc5, 0x64, 0x07, 0x7b, 0x95, 0x9a,
+ 0xae, 0xb6, 0x12, 0x59, 0xa5, 0x35, 0x65, 0xb8, 0xa3, 0x9e,
+ 0xd2, 0xf7, 0x62, 0x5a, 0x85, 0x7d, 0xa8, 0x3a, 0x29, 0x71,
+ 0xc8, 0xf6, 0xf9, 0x43, 0xd7, 0xd6, 0x10, 0x73, 0x76, 0x78,
+ 0x99, 0x0a, 0x19, 0x91, 0x14, 0x3f, 0xe6, 0xf0, 0x86, 0xb1,
+ 0xe2, 0xf1, 0xfa, 0x74, 0xf3, 0xb4, 0x6d, 0x21, 0xb2, 0x6a,
+ 0xe3, 0xe7, 0xb5, 0xea, 0x03, 0x8f, 0xd3, 0xc9, 0x42, 0xd4,
+ 0xe8, 0x75, 0x7f, 0xff, 0x7e, 0xfd
+};
+#endif // GF_LARGE_TABLES
+
+#endif //_EC_BASE_H_
diff --git a/contrib/libs/isa-l/erasure_code/ec_highlevel_func.c b/contrib/libs/isa-l/erasure_code/ec_highlevel_func.c
new file mode 100644
index 0000000000..c57d460a61
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ec_highlevel_func.c
@@ -0,0 +1,336 @@
+/**********************************************************************
+ Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+#include <limits.h>
+#include "erasure_code.h"
+
+#if __x86_64__ || __i386__ || _M_X64 || _M_IX86
+void ec_encode_data_sse(int len, int k, int rows, unsigned char *g_tbls, unsigned char **data,
+ unsigned char **coding)
+{
+
+ if (len < 16) {
+ ec_encode_data_base(len, k, rows, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows >= 4) {
+ gf_4vect_dot_prod_sse(len, k, g_tbls, data, coding);
+ g_tbls += 4 * k * 32;
+ coding += 4;
+ rows -= 4;
+ }
+ switch (rows) {
+ case 3:
+ gf_3vect_dot_prod_sse(len, k, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_dot_prod_sse(len, k, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_dot_prod_sse(len, k, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+void ec_encode_data_avx(int len, int k, int rows, unsigned char *g_tbls, unsigned char **data,
+ unsigned char **coding)
+{
+ if (len < 16) {
+ ec_encode_data_base(len, k, rows, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows >= 4) {
+ gf_4vect_dot_prod_avx(len, k, g_tbls, data, coding);
+ g_tbls += 4 * k * 32;
+ coding += 4;
+ rows -= 4;
+ }
+ switch (rows) {
+ case 3:
+ gf_3vect_dot_prod_avx(len, k, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_dot_prod_avx(len, k, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_dot_prod_avx(len, k, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+void ec_encode_data_avx2(int len, int k, int rows, unsigned char *g_tbls, unsigned char **data,
+ unsigned char **coding)
+{
+
+ if (len < 32) {
+ ec_encode_data_base(len, k, rows, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows >= 4) {
+ gf_4vect_dot_prod_avx2(len, k, g_tbls, data, coding);
+ g_tbls += 4 * k * 32;
+ coding += 4;
+ rows -= 4;
+ }
+ switch (rows) {
+ case 3:
+ gf_3vect_dot_prod_avx2(len, k, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_dot_prod_avx2(len, k, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_dot_prod_avx2(len, k, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+#ifdef HAVE_AS_KNOWS_AVX512
+
+extern int gf_vect_dot_prod_avx512(int len, int k, unsigned char *g_tbls, unsigned char **data,
+ unsigned char *dest);
+extern int gf_2vect_dot_prod_avx512(int len, int k, unsigned char *g_tbls,
+ unsigned char **data, unsigned char **coding);
+extern int gf_3vect_dot_prod_avx512(int len, int k, unsigned char *g_tbls,
+ unsigned char **data, unsigned char **coding);
+extern int gf_4vect_dot_prod_avx512(int len, int k, unsigned char *g_tbls,
+ unsigned char **data, unsigned char **coding);
+extern void gf_vect_mad_avx512(int len, int vec, int vec_i, unsigned char *gftbls,
+ unsigned char *src, unsigned char *dest);
+extern void gf_2vect_mad_avx512(int len, int vec, int vec_i, unsigned char *gftbls,
+ unsigned char *src, unsigned char **dest);
+extern void gf_3vect_mad_avx512(int len, int vec, int vec_i, unsigned char *gftbls,
+ unsigned char *src, unsigned char **dest);
+extern void gf_4vect_mad_avx512(int len, int vec, int vec_i, unsigned char *gftbls,
+ unsigned char *src, unsigned char **dest);
+
+void ec_encode_data_avx512(int len, int k, int rows, unsigned char *g_tbls,
+ unsigned char **data, unsigned char **coding)
+{
+
+ if (len < 64) {
+ ec_encode_data_base(len, k, rows, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows >= 4) {
+ gf_4vect_dot_prod_avx512(len, k, g_tbls, data, coding);
+ g_tbls += 4 * k * 32;
+ coding += 4;
+ rows -= 4;
+ }
+ switch (rows) {
+ case 3:
+ gf_3vect_dot_prod_avx512(len, k, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_dot_prod_avx512(len, k, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_dot_prod_avx512(len, k, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+}
+
+void ec_encode_data_update_avx512(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding)
+{
+ if (len < 64) {
+ ec_encode_data_update_base(len, k, rows, vec_i, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows >= 4) {
+ gf_4vect_mad_avx512(len, k, vec_i, g_tbls, data, coding);
+ g_tbls += 4 * k * 32;
+ coding += 4;
+ rows -= 4;
+ }
+ switch (rows) {
+ case 3:
+ gf_3vect_mad_avx512(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_mad_avx512(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_mad_avx512(len, k, vec_i, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+}
+
+#endif // HAVE_AS_KNOWS_AVX512
+
+#if __WORDSIZE == 64 || _WIN64 || __x86_64__
+
+void ec_encode_data_update_sse(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding)
+{
+ if (len < 16) {
+ ec_encode_data_update_base(len, k, rows, vec_i, g_tbls, data, coding);
+ return;
+ }
+
+ while (rows > 6) {
+ gf_6vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ g_tbls += 6 * k * 32;
+ coding += 6;
+ rows -= 6;
+ }
+ switch (rows) {
+ case 6:
+ gf_6vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 5:
+ gf_5vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 4:
+ gf_4vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 3:
+ gf_3vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_mad_sse(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_mad_sse(len, k, vec_i, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+void ec_encode_data_update_avx(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding)
+{
+ if (len < 16) {
+ ec_encode_data_update_base(len, k, rows, vec_i, g_tbls, data, coding);
+ return;
+ }
+ while (rows > 6) {
+ gf_6vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ g_tbls += 6 * k * 32;
+ coding += 6;
+ rows -= 6;
+ }
+ switch (rows) {
+ case 6:
+ gf_6vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 5:
+ gf_5vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 4:
+ gf_4vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 3:
+ gf_3vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_mad_avx(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_mad_avx(len, k, vec_i, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+void ec_encode_data_update_avx2(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding)
+{
+ if (len < 32) {
+ ec_encode_data_update_base(len, k, rows, vec_i, g_tbls, data, coding);
+ return;
+ }
+ while (rows > 6) {
+ gf_6vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ g_tbls += 6 * k * 32;
+ coding += 6;
+ rows -= 6;
+ }
+ switch (rows) {
+ case 6:
+ gf_6vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 5:
+ gf_5vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 4:
+ gf_4vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 3:
+ gf_3vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 2:
+ gf_2vect_mad_avx2(len, k, vec_i, g_tbls, data, coding);
+ break;
+ case 1:
+ gf_vect_mad_avx2(len, k, vec_i, g_tbls, data, *coding);
+ break;
+ case 0:
+ break;
+ }
+
+}
+
+#endif //__WORDSIZE == 64 || _WIN64 || __x86_64__
+#endif //__x86_64__ || __i386__ || _M_X64 || _M_IX86
+
+struct slver {
+ unsigned short snum;
+ unsigned char ver;
+ unsigned char core;
+};
+
+// Version info
+struct slver ec_init_tables_slver_00010068;
+struct slver ec_init_tables_slver = { 0x0068, 0x01, 0x00 };
+
+struct slver ec_encode_data_sse_slver_00020069;
+struct slver ec_encode_data_sse_slver = { 0x0069, 0x02, 0x00 };
diff --git a/contrib/libs/isa-l/erasure_code/ec_multibinary.asm b/contrib/libs/isa-l/erasure_code/ec_multibinary.asm
new file mode 100644
index 0000000000..a07f45d6f8
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ec_multibinary.asm
@@ -0,0 +1,95 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+%include "reg_sizes.asm"
+%include "multibinary.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf32
+ [bits 32]
+%else
+ default rel
+ [bits 64]
+
+ extern ec_encode_data_update_sse
+ extern ec_encode_data_update_avx
+ extern ec_encode_data_update_avx2
+%ifdef HAVE_AS_KNOWS_AVX512
+ extern ec_encode_data_avx512
+ extern gf_vect_dot_prod_avx512
+ extern ec_encode_data_update_avx512
+ extern gf_vect_mad_avx512
+%endif
+ extern gf_vect_mul_sse
+ extern gf_vect_mul_avx
+
+ extern gf_vect_mad_sse
+ extern gf_vect_mad_avx
+ extern gf_vect_mad_avx2
+%endif
+
+extern gf_vect_mul_base
+extern ec_encode_data_base
+extern ec_encode_data_update_base
+extern gf_vect_dot_prod_base
+extern gf_vect_mad_base
+
+extern gf_vect_dot_prod_sse
+extern gf_vect_dot_prod_avx
+extern gf_vect_dot_prod_avx2
+extern ec_encode_data_sse
+extern ec_encode_data_avx
+extern ec_encode_data_avx2
+
+mbin_interface ec_encode_data
+mbin_interface gf_vect_dot_prod
+mbin_interface gf_vect_mul
+mbin_interface ec_encode_data_update
+mbin_interface gf_vect_mad
+
+%ifidn __OUTPUT_FORMAT__, elf32
+ mbin_dispatch_init5 ec_encode_data, ec_encode_data_base, ec_encode_data_sse, ec_encode_data_avx, ec_encode_data_avx2
+ mbin_dispatch_init5 gf_vect_dot_prod, gf_vect_dot_prod_base, gf_vect_dot_prod_sse, gf_vect_dot_prod_avx, gf_vect_dot_prod_avx2
+ mbin_dispatch_init2 gf_vect_mul, gf_vect_mul_base
+ mbin_dispatch_init2 ec_encode_data_update, ec_encode_data_update_base
+ mbin_dispatch_init2 gf_vect_mad, gf_vect_mad_base
+%else
+
+ mbin_dispatch_init5 gf_vect_mul, gf_vect_mul_base, gf_vect_mul_sse, gf_vect_mul_avx, gf_vect_mul_avx
+ mbin_dispatch_init6 ec_encode_data, ec_encode_data_base, ec_encode_data_sse, ec_encode_data_avx, ec_encode_data_avx2, ec_encode_data_avx512
+ mbin_dispatch_init6 ec_encode_data_update, ec_encode_data_update_base, ec_encode_data_update_sse, ec_encode_data_update_avx, ec_encode_data_update_avx2, ec_encode_data_update_avx512
+ mbin_dispatch_init6 gf_vect_mad, gf_vect_mad_base, gf_vect_mad_sse, gf_vect_mad_avx, gf_vect_mad_avx2, gf_vect_mad_avx512
+ mbin_dispatch_init6 gf_vect_dot_prod, gf_vect_dot_prod_base, gf_vect_dot_prod_sse, gf_vect_dot_prod_avx, gf_vect_dot_prod_avx2, gf_vect_dot_prod_avx512
+%endif
+
+;;; func core, ver, snum
+slversion ec_encode_data, 00, 06, 0133
+slversion gf_vect_mul, 00, 05, 0134
+slversion ec_encode_data_update, 00, 05, 0212
+slversion gf_vect_dot_prod, 00, 05, 0138
+slversion gf_vect_mad, 00, 04, 0213
diff --git a/contrib/libs/isa-l/erasure_code/ec_multibinary_darwin.asm b/contrib/libs/isa-l/erasure_code/ec_multibinary_darwin.asm
new file mode 100644
index 0000000000..8c2537f562
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ec_multibinary_darwin.asm
@@ -0,0 +1,96 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+%include "reg_sizes.asm"
+%include "multibinary.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf32
+ [bits 32]
+%else
+ default rel
+ [bits 64]
+
+ extern _ec_encode_data_update_sse
+ extern _ec_encode_data_update_avx
+ extern _ec_encode_data_update_avx2
+%ifdef HAVE_AS_KNOWS_AVX512
+ extern _ec_encode_data_avx512
+ extern _gf_vect_dot_prod_avx512
+ extern _ec_encode_data_update_avx512
+ extern _gf_vect_mad_avx512
+%endif
+ extern _gf_vect_mul_sse
+ extern _gf_vect_mul_avx
+
+ extern _gf_vect_mad_sse
+ extern _gf_vect_mad_avx
+ extern _gf_vect_mad_avx2
+%endif
+
+extern _gf_vect_mul_base
+extern _ec_encode_data_base
+extern _ec_encode_data_update_base
+extern _gf_vect_dot_prod_base
+extern _gf_vect_mad_base
+
+extern _gf_vect_dot_prod_sse
+extern _gf_vect_dot_prod_avx
+extern _gf_vect_dot_prod_avx2
+extern _ec_encode_data_sse
+extern _ec_encode_data_avx
+extern _ec_encode_data_avx2
+
+mbin_interface _ec_encode_data
+mbin_interface _gf_vect_dot_prod
+mbin_interface _gf_vect_mul
+mbin_interface _ec_encode_data_update
+mbin_interface _gf_vect_mad
+
+%ifidn __OUTPUT_FORMAT__, elf32
+ mbin_dispatch_init5 _ec_encode_data, _ec_encode_data_base, _ec_encode_data_sse, _ec_encode_data_avx, _ec_encode_data_avx2
+ mbin_dispatch_init5 _gf_vect_dot_prod, _gf_vect_dot_prod_base, _gf_vect_dot_prod_sse, _gf_vect_dot_prod_avx, _gf_vect_dot_prod_avx2
+ mbin_dispatch_init2 _gf_vect_mul, _gf_vect_mul_base
+ mbin_dispatch_init2 _ec_encode_data_update, _ec_encode_data_update_base
+ mbin_dispatch_init2 _gf_vect_mad, _gf_vect_mad_base
+%else
+
+ mbin_dispatch_init5 _gf_vect_mul, _gf_vect_mul_base, _gf_vect_mul_sse, _gf_vect_mul_avx, _gf_vect_mul_avx
+ mbin_dispatch_init6 _ec_encode_data, _ec_encode_data_base, _ec_encode_data_sse, _ec_encode_data_avx, _ec_encode_data_avx2, _ec_encode_data_avx512
+ mbin_dispatch_init6 _ec_encode_data_update, _ec_encode_data_update_base, _ec_encode_data_update_sse, _ec_encode_data_update_avx, _ec_encode_data_update_avx2, _ec_encode_data_update_avx512
+ mbin_dispatch_init6 _gf_vect_mad, _gf_vect_mad_base, _gf_vect_mad_sse, _gf_vect_mad_avx, _gf_vect_mad_avx2, _gf_vect_mad_avx512
+ mbin_dispatch_init6 _gf_vect_dot_prod, _gf_vect_dot_prod_base, _gf_vect_dot_prod_sse, _gf_vect_dot_prod_avx, _gf_vect_dot_prod_avx2, _gf_vect_dot_prod_avx512
+%endif
+
+
+;;; func core, ver, snum
+slversion ec_encode_data, 00, 06, 0133
+slversion gf_vect_mul, 00, 05, 0134
+slversion ec_encode_data_update, 00, 05, 0212
+slversion gf_vect_dot_prod, 00, 05, 0138
+slversion gf_vect_mad, 00, 04, 0213
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..6233d42e5d
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx.asm
@@ -0,0 +1,341 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_dot_prod_avx(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r9
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 3*16 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_reg r12, 3*16 + 0*8
+ save_reg r13, 3*16 + 1*8
+ save_reg r14, 3*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r13, [rsp + 3*16 + 1*8]
+ mov r14, [rsp + 3*16 + 2*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp4 trans2
+ %define tmp4_m var(0)
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*1 ;1 local variable
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*1 ;1 local variable
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define dest2 tmp4
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm8
+ %define xgft1_lo xmm7
+ %define xgft1_hi xmm6
+ %define xgft2_lo xmm5
+ %define xgft2_hi xmm4
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+%else ;32-bit code
+ %define xmask0f xmm4
+ %define xgft1_lo xmm7
+ %define xgft1_hi xmm6
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+%endif
+
+align 16
+global gf_2vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %ifidn PS,8 ; 64-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_2vect_dot_prod_avx, 02, 05, 0191
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..53052d56e0
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx2.asm
@@ -0,0 +1,360 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r9
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 3*16 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ save_reg r12, 3*16 + 0*8
+ save_reg r13, 3*16 + 1*8
+ save_reg r14, 3*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r13, [rsp + 3*16 + 1*8]
+ mov r14, [rsp + 3*16 + 2*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp.w edx
+ %define tmp.b dl
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp4 trans2
+ %define tmp4_m var(0)
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*1 ;1 local variable
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*1 ;1 local variable
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define dest2 tmp4
+%define pos return
+
+%ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp4_m
+%endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f ymm8
+ %define xmask0fx xmm8
+ %define xgft1_lo ymm7
+ %define xgft1_hi ymm6
+ %define xgft2_lo ymm5
+ %define xgft2_hi ymm4
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+%else ;32-bit code
+ %define xmask0f ymm7
+ %define xmask0fx xmm7
+ %define xgft1_lo ymm5
+ %define xgft1_hi ymm4
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+
+%endif
+
+align 16
+global gf_2vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 32
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop32:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft1_lo, xgft1_lo, xgft1_lo, 0x00 ; swapped to lo | lo
+ %ifidn PS,8 ; 64-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add tmp, 32
+ add vec_i, PS
+ %else
+ XLDR x0, [ptr+pos] ;Get next source vector
+ %endif
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+
+ SLDR len, len_m
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_2vect_dot_prod_avx2, 04, 05, 0196
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx512.asm
new file mode 100644
index 0000000000..0fe2f434a1
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_avx512.asm
@@ -0,0 +1,250 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_dot_prod_avx512(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 5*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%define xmask0f zmm8
+%define xgft1_lo zmm7
+%define xgft1_loy ymm7
+%define xgft1_hi zmm6
+%define xgft2_lo zmm5
+%define xgft2_loy ymm5
+%define xgft2_hi zmm4
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xp1 zmm2
+%define xp2 zmm3
+
+default rel
+[bits 64]
+
+section .text
+
+align 16
+global gf_2vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_dot_prod_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_dot_prod_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+.loop64:
+ vpxorq xp1, xp1, xp1
+ vpxorq xp2, xp2, xp2
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vmovdqu8 xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu8 xgft2_loy, [tmp+vec*(32/PS)] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ add tmp, 32
+
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+
+ vpshufb xgft1_hi, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxorq xp1, xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft2_hi, xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxorq xp2, xp2, xgft2_hi ;xp2 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_2vect_dot_prod_avx512
+no_gf_2vect_dot_prod_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..ad61093471
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_dot_prod_sse.asm
@@ -0,0 +1,343 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_dot_prod_sse(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r9
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 3*16 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_reg r12, 3*16 + 0*8
+ save_reg r13, 3*16 + 1*8
+ save_reg r14, 3*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm8, [rsp + 2*16]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r13, [rsp + 3*16 + 1*8]
+ mov r14, [rsp + 3*16 + 2*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp4 trans2
+ %define tmp4_m var(0)
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*1 ;1 local variable
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*1 ;1 local variable
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define dest2 tmp4
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm8
+ %define xgft1_lo xmm7
+ %define xgft1_hi xmm6
+ %define xgft2_lo xmm5
+ %define xgft2_hi xmm4
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+%else ;32-bit code
+ %define xmask0f xmm4
+ %define xgft1_lo xmm7
+ %define xgft1_hi xmm6
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+%endif
+
+align 16
+global gf_2vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ pxor xp1, xp1
+ pxor xp2, xp2
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %ifidn PS,8 ;64-bit code
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ;32-bit code
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp2, xgft2_hi ;xp2 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_2vect_dot_prod_sse, 00, 04, 0062
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx.asm
new file mode 100644
index 0000000000..2d51dad33f
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx.asm
@@ -0,0 +1,240 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*9 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r15, 9*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r15, [rsp + 9*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_2vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp2
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm14
+%define xgft1_lo xmm13
+%define xgft1_hi xmm12
+%define xgft2_lo xmm11
+%define xgft2_hi xmm10
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xd1 xmm6
+%define xd2 xmm7
+%define xtmpd1 xmm8
+%define xtmpd2 xmm9
+
+
+align 16
+global gf_2vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_mad_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xgft2_hi, [tmp+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+ XLDR xtmpd1, [dest1+len] ;backup the last 16 bytes in dest
+ XLDR xtmpd2, [dest2+len] ;backup the last 16 bytes in dest
+
+.loop16:
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+.loop16_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph1, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ vpshufb xtmph2, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ vmovdqa xd1, xtmpd1 ;Restore xd1
+ vmovdqa xd2, xtmpd2 ;Restore xd2
+ jmp .loop16_overlap ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_2vect_mad_avx, 02, 01, 0204
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx2.asm
new file mode 100644
index 0000000000..2b0fd8ea2d
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx2.asm
@@ -0,0 +1,251 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*9 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ vmovdqa [rsp+16*3],xmm9
+ vmovdqa [rsp+16*4],xmm10
+ vmovdqa [rsp+16*5],xmm11
+ vmovdqa [rsp+16*6],xmm12
+ vmovdqa [rsp+16*7],xmm13
+ vmovdqa [rsp+16*8],xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r15, 9*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ vmovdqa xmm9, [rsp+16*3]
+ vmovdqa xmm10, [rsp+16*4]
+ vmovdqa xmm11, [rsp+16*5]
+ vmovdqa xmm12, [rsp+16*6]
+ vmovdqa xmm13, [rsp+16*7]
+ vmovdqa xmm14, [rsp+16*8]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r15, [rsp + 9*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_2vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp2
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm14
+%define xmask0fx xmm14
+%define xgft1_lo ymm13
+%define xgft1_hi ymm12
+%define xgft2_lo ymm11
+%define xgft2_hi ymm10
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmph1 ymm2
+%define xtmpl1 ymm3
+%define xtmph2 ymm4
+%define xtmpl2 ymm5
+%define xd1 ymm6
+%define xd2 ymm7
+%define xtmpd1 ymm8
+%define xtmpd2 ymm9
+
+align 16
+global gf_2vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft1_lo, xgft1_lo, xgft1_lo, 0x00 ; swapped to lo | lo
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest1, [dest1]
+
+ XLDR xtmpd1, [dest1+len] ;backup the last 16 bytes in dest
+ XLDR xtmpd2, [dest2+len] ;backup the last 16 bytes in dest
+
+.loop32:
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+.loop32_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph1, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ vpshufb xtmph2, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-32
+ vmovdqa xd1, xtmpd1 ;Restore xd1
+ vmovdqa xd2, xtmpd2 ;Restore xd2
+ jmp .loop32_overlap ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_2vect_mad_avx2, 04, 01, 0205
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx512.asm
new file mode 100644
index 0000000000..acb67e4334
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_avx512.asm
@@ -0,0 +1,235 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_mad_avx512(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define stack_size 16*9 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ vmovdqa [rsp+16*3],xmm9
+ vmovdqa [rsp+16*4],xmm10
+ vmovdqa [rsp+16*5],xmm11
+ vmovdqa [rsp+16*6],xmm12
+ vmovdqa [rsp+16*7],xmm13
+ vmovdqa [rsp+16*8],xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r15, 9*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ vmovdqa xmm9, [rsp+16*3]
+ vmovdqa xmm10, [rsp+16*4]
+ vmovdqa xmm11, [rsp+16*5]
+ vmovdqa xmm12, [rsp+16*6]
+ vmovdqa xmm13, [rsp+16*7]
+ vmovdqa xmm14, [rsp+16*8]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r15, [rsp + 9*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+
+%define PS 8
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+%define dest2 tmp2
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+[bits 64]
+section .text
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xtmph1 zmm2
+%define xtmpl1 zmm3
+%define xtmph2 zmm4
+%define xtmpl2 zmm5
+%define xd1 zmm6
+%define xd2 zmm7
+%define xtmpd1 zmm8
+%define xtmpd2 zmm9
+%define xgft1_hi zmm10
+%define xgft1_lo zmm11
+%define xgft1_loy ymm11
+%define xgft2_hi zmm12
+%define xgft2_lo zmm13
+%define xgft2_loy ymm13
+%define xmask0f zmm14
+
+align 16
+global gf_2vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_mad_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_mad_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu xgft2_loy, [tmp+vec] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest1, [dest1]
+ mov tmp, -1
+ kmovq k1, tmp
+
+.loop64:
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph1 {k1}{z}, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1 {k1}{z}, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxorq xd1, xd1, xtmph1 ;xd1 += partial
+
+ vpshufb xtmph2 {k1}{z}, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2 {k1}{z}, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxorq xd2, xd2, xtmph2 ;xd2 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, (1 << 63)
+ lea tmp, [len + 64 - 1]
+ and tmp, 63
+ sarx pos, pos, tmp
+ kmovq k1, pos
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_2vect_mad_avx512
+no_gf_2vect_mad_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_2vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_sse.asm
new file mode 100644
index 0000000000..5bf380df14
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_2vect_mad_sse.asm
@@ -0,0 +1,244 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_2vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*9 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r15, 9*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r15, [rsp + 9*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_2vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp2
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm14
+%define xgft1_lo xmm13
+%define xgft1_hi xmm12
+%define xgft2_lo xmm11
+%define xgft2_hi xmm10
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xd1 xmm6
+%define xd2 xmm7
+%define xtmpd1 xmm8
+%define xtmpd2 xmm9
+
+
+align 16
+global gf_2vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_2vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_2vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_2vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ movdqu xgft1_lo,[tmp] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xgft2_hi, [tmp+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+ XLDR xtmpd1, [dest1+len] ;backup the last 16 bytes in dest
+ XLDR xtmpd2, [dest2+len] ;backup the last 16 bytes in dest
+
+.loop16:
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+.loop16_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+ movdqa xtmph1, xgft1_hi ;Reload const array registers
+ movdqa xtmpl1, xgft1_lo
+ movdqa xtmph2, xgft2_hi ;Reload const array registers
+ movdqa xtmpl2, xgft2_lo
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ XSTR [dest1+pos], xd1 ;Store result
+ XSTR [dest2+pos], xd2 ;Store result
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ movdqa xd1, xtmpd1 ;Restore xd1
+ movdqa xd2, xtmpd2 ;Restore xd2
+ jmp .loop16_overlap ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+ dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_2vect_mad_sse, 00, 01, 0203
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..a2619507b7
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx.asm
@@ -0,0 +1,382 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_dot_prod_avx(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 6*16 + 5*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_reg r12, 6*16 + 0*8
+ save_reg r13, 6*16 + 1*8
+ save_reg r14, 6*16 + 2*8
+ save_reg r15, 6*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ mov r12, [rsp + 6*16 + 0*8]
+ mov r13, [rsp + 6*16 + 1*8]
+ mov r14, [rsp + 6*16 + 2*8]
+ mov r15, [rsp + 6*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*2 ;2 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*2 ;2 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm11
+ %define xgft1_lo xmm10
+ %define xgft1_hi xmm9
+ %define xgft2_lo xmm8
+ %define xgft2_hi xmm7
+ %define xgft3_lo xmm6
+ %define xgft3_hi xmm5
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+%else
+ %define xmask0f xmm7
+ %define xgft1_lo xmm6
+ %define xgft1_hi xmm5
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+%endif
+
+align 16
+global gf_3vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %ifidn PS,8 ; 64-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vec*(64/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ sal vec, 1
+ vmovdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vec*(32/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ sar vec, 1
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_3vect_dot_prod_avx, 02, 05, 0192
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..26b6b82e21
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx2.asm
@@ -0,0 +1,402 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 6*16 + 5*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ save_reg r12, 6*16 + 0*8
+ save_reg r13, 6*16 + 1*8
+ save_reg r14, 6*16 + 2*8
+ save_reg r15, 6*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ mov r12, [rsp + 6*16 + 0*8]
+ mov r13, [rsp + 6*16 + 1*8]
+ mov r14, [rsp + 6*16 + 2*8]
+ mov r15, [rsp + 6*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp.w edx
+ %define tmp.b dl
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*2 ;2 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*2 ;2 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define pos return
+
+%ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+%endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f ymm11
+ %define xmask0fx xmm11
+ %define xgft1_lo ymm10
+ %define xgft1_hi ymm9
+ %define xgft2_lo ymm8
+ %define xgft2_hi ymm7
+ %define xgft3_lo ymm6
+ %define xgft3_hi ymm5
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+ %define xp3 ymm4
+%else
+ %define xmask0f ymm7
+ %define xmask0fx xmm7
+ %define xgft1_lo ymm6
+ %define xgft1_hi ymm5
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+ %define xp3 ymm4
+
+%endif
+
+align 16
+global gf_3vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 32
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop32:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft1_lo, xgft1_lo, xgft1_lo, 0x00 ; swapped to lo | lo
+ %ifidn PS,8 ; 64-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+
+ vmovdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft3_lo, xgft3_lo, xgft3_lo, 0x00 ; swapped to lo | lo
+
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ sal vec, 1
+ vmovdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft3_lo, xgft3_lo, xgft3_lo, 0x00 ; swapped to lo | lo
+ sar vec, 1
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+
+ SLDR len, len_m
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_3vect_dot_prod_avx2, 04, 05, 0197
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx512.asm
new file mode 100644
index 0000000000..16a90eb2af
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_avx512.asm
@@ -0,0 +1,275 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_dot_prod_avx512(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 5*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%define xmask0f zmm11
+%define xgft1_lo zmm10
+%define xgft1_loy ymm10
+%define xgft1_hi zmm9
+%define xgft2_lo zmm8
+%define xgft2_loy ymm8
+%define xgft2_hi zmm7
+%define xgft3_lo zmm6
+%define xgft3_loy ymm6
+%define xgft3_hi zmm5
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xp1 zmm2
+%define xp2 zmm3
+%define xp3 zmm4
+
+default rel
+[bits 64]
+
+section .text
+
+align 16
+global gf_3vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_dot_prod_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_dot_prod_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest2, [dest1+PS]
+ mov dest3, [dest1+2*PS]
+ mov dest1, [dest1]
+
+.loop64:
+ vpxorq xp1, xp1, xp1
+ vpxorq xp2, xp2, xp2
+ vpxorq xp3, xp3, xp3
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vmovdqu8 xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu8 xgft2_loy, [tmp+vec*(32/PS)] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ vmovdqu8 xgft3_loy, [tmp+vec*(64/PS)] ;Load array Cx{00}..{0f}, Cx{00}..{f0}
+ add tmp, 32
+
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+
+ vpshufb xgft1_hi, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxorq xp1, xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft2_hi, xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxorq xp2, xp2, xgft2_hi ;xp2 += partial
+
+ vshufi64x2 xgft3_hi, xgft3_lo, xgft3_lo, 0x55
+ vshufi64x2 xgft3_lo, xgft3_lo, xgft3_lo, 0x00
+
+ vpshufb xgft3_hi, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft3_hi, xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxorq xp3, xp3, xgft3_hi ;xp3 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [dest3+pos], xp3
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_3vect_dot_prod_avx512
+no_gf_3vect_dot_prod_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..582fac8481
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_dot_prod_sse.asm
@@ -0,0 +1,383 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_dot_prod_sse(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 6*16 + 5*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_reg r12, 6*16 + 0*8
+ save_reg r13, 6*16 + 1*8
+ save_reg r14, 6*16 + 2*8
+ save_reg r15, 6*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm8, [rsp + 2*16]
+ movdqa xmm9, [rsp + 3*16]
+ movdqa xmm10, [rsp + 4*16]
+ movdqa xmm11, [rsp + 5*16]
+ mov r12, [rsp + 6*16 + 0*8]
+ mov r13, [rsp + 6*16 + 1*8]
+ mov r14, [rsp + 6*16 + 2*8]
+ mov r15, [rsp + 6*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*2 ;2 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*2 ;2 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm11
+ %define xgft1_lo xmm2
+ %define xgft1_hi xmm3
+ %define xgft2_lo xmm4
+ %define xgft2_hi xmm7
+ %define xgft3_lo xmm6
+ %define xgft3_hi xmm5
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm10
+ %define xp2 xmm9
+ %define xp3 xmm8
+%else
+ %define xmask0f xmm7
+ %define xgft1_lo xmm6
+ %define xgft1_hi xmm5
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+%endif
+
+align 16
+global gf_3vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ pxor xp1, xp1
+ pxor xp2, xp2
+ pxor xp3, xp3
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %ifidn PS,8 ;64-bit code
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ movdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vec*(64/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ;32-bit code
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ %endif
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ;32-bit code
+ sal vec, 1
+ movdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vec*(32/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ sar vec, 1
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pxor xp3, xgft3_hi ;xp3 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_3vect_dot_prod_sse, 00, 06, 0063
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx.asm
new file mode 100644
index 0000000000..7cf630558c
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx.asm
@@ -0,0 +1,292 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ vmovdqa [rsp+16*3],xmm9
+ vmovdqa [rsp+16*4],xmm10
+ vmovdqa [rsp+16*5],xmm11
+ vmovdqa [rsp+16*6],xmm12
+ vmovdqa [rsp+16*7],xmm13
+ vmovdqa [rsp+16*8],xmm14
+ vmovdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ vmovdqa xmm9, [rsp+16*3]
+ vmovdqa xmm10, [rsp+16*4]
+ vmovdqa xmm11, [rsp+16*5]
+ vmovdqa xmm12, [rsp+16*6]
+ vmovdqa xmm13, [rsp+16*7]
+ vmovdqa xmm14, [rsp+16*8]
+ vmovdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_3vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm14
+%define xgft1_hi xmm13
+%define xgft2_lo xmm12
+%define xgft2_hi xmm11
+%define xgft3_lo xmm10
+%define xgft3_hi xmm9
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xd1 xmm8
+%define xd2 xtmpl1
+%define xd3 xtmph1
+
+align 16
+global gf_3vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_mad_avx)
+%endif
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xgft2_hi, [tmp+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xgft3_hi, [tmp+2*vec+16]; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ XLDR xd2, [dest2+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+pos] ;reuse xtmph1. Get next dest vector
+
+ ; dest2
+ vpshufb xtmph2, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xd3, xtmph3 ;xd3 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp, len ;Overlapped offset length-16
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+tmp] ;reuse xtmph1. Get next dest vector
+
+ sub len, pos
+
+ movdqa xtmph3, [constip16] ;Load const of i + 16
+ vpinsrb xtmpl3, xtmpl3, len.w, 15
+ vpshufb xtmpl3, xtmpl3, xmask0f ;Broadcast len to all bytes
+ vpcmpgtb xtmpl3, xtmpl3, xtmph3
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xgft1_hi, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpand xgft1_hi, xgft1_hi, xtmpl3
+ vpxor xd1, xd1, xgft1_hi
+
+ ; dest2
+ vpshufb xgft2_hi, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpand xgft2_hi, xgft2_hi, xtmpl3
+ vpxor xd2, xd2, xgft2_hi
+
+ ; dest3
+ vpshufb xgft3_hi, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpand xgft3_hi, xgft3_hi, xtmpl3
+ vpxor xd3, xd3, xgft3_hi
+
+ XSTR [dest1+tmp], xd1
+ XSTR [dest2+tmp], xd2
+ XSTR [dest3+tmp], xd3
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_3vect_mad_avx, 02, 01, 0207
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx2.asm
new file mode 100644
index 0000000000..c218b4db28
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx2.asm
@@ -0,0 +1,322 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ vmovdqa [rsp+16*3],xmm9
+ vmovdqa [rsp+16*4],xmm10
+ vmovdqa [rsp+16*5],xmm11
+ vmovdqa [rsp+16*6],xmm12
+ vmovdqa [rsp+16*7],xmm13
+ vmovdqa [rsp+16*8],xmm14
+ vmovdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ vmovdqa xmm9, [rsp+16*3]
+ vmovdqa xmm10, [rsp+16*4]
+ vmovdqa xmm11, [rsp+16*5]
+ vmovdqa xmm12, [rsp+16*6]
+ vmovdqa xmm13, [rsp+16*7]
+ vmovdqa xmm14, [rsp+16*8]
+ vmovdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_3vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft1_hi ymm13
+%define xgft2_lo ymm12
+%define xgft3_lo ymm11
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmph1 ymm2
+%define xtmpl1 ymm3
+%define xtmph2 ymm4
+%define xtmpl2 ymm5
+%define xtmpl2x xmm5
+%define xtmph3 ymm6
+%define xtmpl3 ymm7
+%define xtmpl3x xmm7
+%define xd1 ymm8
+%define xd2 ymm9
+%define xd3 ymm10
+
+align 16
+global gf_3vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft1_lo, xgft1_lo, xgft1_lo, 0x00 ; swapped to lo | lo
+
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xtmpl2, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+
+ vperm2i128 xtmph3, xgft3_lo, xgft3_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xtmpl3, xgft3_lo, xgft3_lo, 0x00 ; swapped to lo | lo
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xtmph3 ;xd3 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan32:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp.b, 0x1f
+ vpinsrb xtmpl2x, xtmpl2x, tmp.w, 0
+ vpbroadcastb xtmpl2, xtmpl2x ;Construct mask 0x1f1f1f...
+
+ mov tmp, len ;Overlapped offset length-32
+
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xd3, [dest3+tmp] ;Get next dest vector
+
+ sub len, pos
+
+ vmovdqa xtmph3, [constip32] ;Load const of i + 32
+ vpinsrb xtmpl3x, xtmpl3x, len.w, 15
+ vinserti128 xtmpl3, xtmpl3, xtmpl3x, 1 ;swapped to xtmpl3x | xtmpl3x
+ vpshufb xtmpl3, xtmpl3, xtmpl2 ;Broadcast len to all bytes. xtmpl2=0x1f1f1f...
+ vpcmpgtb xtmpl3, xtmpl3, xtmph3
+
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft2_lo, xgft2_lo, xgft2_lo, 0x00 ; swapped to lo | lo
+
+ vperm2i128 xtmph3, xgft3_lo, xgft3_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft3_lo, xgft3_lo, xgft3_lo, 0x00 ; swapped to lo | lo
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmpl3
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xgft2_lo ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmpl3
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3, xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xgft3_lo ;GF add high and low partials
+ vpand xtmph3, xtmph3, xtmpl3
+ vpxor xd3, xd3, xtmph3 ;xd3 += partial
+
+ XSTR [dest1+tmp], xd1
+ XSTR [dest2+tmp], xd2
+ XSTR [dest3+tmp], xd3
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 32
+constip32:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+ dq 0xe8e9eaebecedeeef, 0xe0e1e2e3e4e5e6e7
+
+;;; func core, ver, snum
+slversion gf_3vect_mad_avx2, 04, 01, 0208
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx512.asm
new file mode 100644
index 0000000000..53b3eb5afa
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_avx512.asm
@@ -0,0 +1,252 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_mad_avx512(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ vmovdqa [rsp+16*3],xmm9
+ vmovdqa [rsp+16*4],xmm10
+ vmovdqa [rsp+16*5],xmm11
+ vmovdqa [rsp+16*6],xmm12
+ vmovdqa [rsp+16*7],xmm13
+ vmovdqa [rsp+16*8],xmm14
+ vmovdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ vmovdqa xmm9, [rsp+16*3]
+ vmovdqa xmm10, [rsp+16*4]
+ vmovdqa xmm11, [rsp+16*5]
+ vmovdqa xmm12, [rsp+16*6]
+ vmovdqa xmm13, [rsp+16*7]
+ vmovdqa xmm14, [rsp+16*8]
+ vmovdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define PS 8
+%define len arg0
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define dest2 mul_array
+%define dest3 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+[bits 64]
+section .text
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xtmph1 zmm2
+%define xtmpl1 zmm3
+%define xtmph2 zmm4
+%define xtmpl2 zmm5
+%define xtmph3 zmm6
+%define xtmpl3 zmm7
+%define xgft1_hi zmm8
+%define xgft1_lo zmm9
+%define xgft1_loy ymm9
+%define xgft2_hi zmm10
+%define xgft2_lo zmm11
+%define xgft2_loy ymm11
+%define xgft3_hi zmm12
+%define xgft3_lo zmm13
+%define xgft3_loy ymm13
+%define xd1 zmm14
+%define xd2 zmm15
+%define xd3 zmm16
+%define xmask0f zmm17
+
+align 16
+global gf_3vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_mad_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_mad_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu xgft2_loy, [tmp+vec] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ vmovdqu xgft3_loy, [tmp+2*vec] ;Load array Cx{00}..{0f}, Cx{00}..{f0}
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+ vshufi64x2 xgft3_hi, xgft3_lo, xgft3_lo, 0x55
+ vshufi64x2 xgft3_lo, xgft3_lo, xgft3_lo, 0x00
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec_i
+ mov dest1, [dest1]
+ mov tmp, -1
+ kmovq k1, tmp
+
+.loop64:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1 {k1}{z}, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1 {k1}{z}, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxorq xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2 {k1}{z}, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2 {k1}{z}, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxorq xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3 {k1}{z}, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3 {k1}{z}, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxorq xd3, xd3, xtmph3 ;xd2 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, (1 << 63)
+ lea tmp, [len + 64 - 1]
+ and tmp, 63
+ sarx pos, pos, tmp
+ kmovq k1, pos
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_3vect_mad_avx512
+no_gf_3vect_mad_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_3vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_sse.asm
new file mode 100644
index 0000000000..d6dbe8f200
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_3vect_mad_sse.asm
@@ -0,0 +1,303 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_3vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_3vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm14
+%define xgft1_hi xmm13
+%define xgft2_lo xmm12
+%define xgft2_hi xmm11
+%define xgft3_lo xmm10
+%define xgft3_hi xmm9
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xd1 xmm8
+%define xd2 xtmpl1
+%define xd3 xtmph1
+
+align 16
+global gf_3vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_3vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_3vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_3vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5
+ lea tmp, [mul_array + vec_i]
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xgft2_hi, [tmp+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xgft3_hi, [tmp+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+ movdqa xtmph1, xgft1_hi ;Reload const array registers
+ movdqa xtmpl1, xgft1_lo
+ movdqa xtmph2, xgft2_hi ;Reload const array registers
+ movdqa xtmpl2, xgft2_lo
+ movdqa xtmph3, xgft3_hi ;Reload const array registers
+ movdqa xtmpl3, xgft3_lo
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ XLDR xd2, [dest2+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+pos] ;reuse xtmph1. Get next dest vector
+
+ ; dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ ; dest3
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pxor xd3, xtmph3
+
+ XSTR [dest1+pos], xd1 ;Store result
+ XSTR [dest2+pos], xd2 ;Store result
+ XSTR [dest3+pos], xd3 ;Store result
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp, len ;Overlapped offset length-16
+
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+tmp] ;reuse xtmph1. Get next dest vector
+
+ sub len, pos
+
+ movdqa xtmph3, [constip16] ;Load const of i + 16
+ pinsrb xtmpl3, len.w, 15
+ pshufb xtmpl3, xmask0f ;Broadcast len to all bytes
+ pcmpgtb xtmpl3, xtmph3
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pand xgft1_hi, xtmpl3
+ pxor xd1, xgft1_hi
+
+ ; dest2
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pand xgft2_hi, xtmpl3
+ pxor xd2, xgft2_hi
+
+ ; dest3
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pand xgft3_hi, xtmpl3
+ pxor xd3, xgft3_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result
+ XSTR [dest2+tmp], xd2 ;Store result
+ XSTR [dest3+tmp], xd3 ;Store result
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+ dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_3vect_mad_sse, 00, 01, 0206
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..30f1e81f6b
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx.asm
@@ -0,0 +1,446 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_dot_prod_avx(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ save_reg rdi, 9*16 + 4*8
+ save_reg rsi, 9*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ mov rdi, [rsp + 9*16 + 4*8]
+ mov rsi, [rsp + 9*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; var2
+;;; var3
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define tmp5 trans2
+ %define tmp5_m var(2)
+ %define tmp6 trans2
+ %define tmp6_m var(3)
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*4 ;4 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*4 ;4 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define dest4 tmp5
+%define vskip3 tmp6
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+ %define dest4_m tmp5_m
+ %define vskip3_m tmp6_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm14
+ %define xgft1_lo xmm13
+ %define xgft1_hi xmm12
+ %define xgft2_lo xmm11
+ %define xgft2_hi xmm10
+ %define xgft3_lo xmm9
+ %define xgft3_hi xmm8
+ %define xgft4_lo xmm7
+ %define xgft4_hi xmm6
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+ %define xp4 xmm5
+%else
+ %define xmm_trans xmm7 ;reuse xmask0f and xgft1_lo
+ %define xmask0f xmm_trans
+ %define xgft1_lo xmm_trans
+ %define xgft1_hi xmm6
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+ %define xgft4_lo xgft1_lo
+ %define xgft4_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+ %define xp4 xmm5
+%endif
+align 16
+global gf_4vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip3, vec
+ imul vskip3, 96
+ SSTR vskip3_m, vskip3
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest4, [dest1+3*PS]
+ SSTR dest4_m, dest4
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ %ifidn PS,8 ;64-bit code
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vec*(64/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ vmovdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add tmp, 32
+ add vec_i, PS
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ %else ;32-bit code
+ XLDR x0, [ptr+pos] ;Get next source vector
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %endif
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ;32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ;32-bit code
+ sal vec, 1
+ vmovdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vec*(32/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ sar vec, 1
+ %endif
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ %ifidn PS,4 ;32-bit code
+ SLDR vskip3, vskip3_m
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ vmovdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ vpshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpxor xp4, xgft4_hi ;xp4 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+ SLDR dest4, dest4_m
+ XSTR [dest4+pos], xp4
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_4vect_dot_prod_avx, 02, 05, 0193
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..efe2f76de9
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx2.asm
@@ -0,0 +1,466 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ save_reg rdi, 9*16 + 4*8
+ save_reg rsi, 9*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ mov rdi, [rsp + 9*16 + 4*8]
+ mov rsi, [rsp + 9*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; var2
+;;; var3
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp.w edx
+ %define tmp.b dl
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define tmp5 trans2
+ %define tmp5_m var(2)
+ %define tmp6 trans2
+ %define tmp6_m var(3)
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*4 ;4 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*4 ;4 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define dest4 tmp5
+%define vskip3 tmp6
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+ %define dest4_m tmp5_m
+ %define vskip3_m tmp6_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f ymm14
+ %define xmask0fx xmm14
+ %define xgft1_lo ymm13
+ %define xgft1_hi ymm12
+ %define xgft2_lo ymm11
+ %define xgft2_hi ymm10
+ %define xgft3_lo ymm9
+ %define xgft3_hi ymm8
+ %define xgft4_lo ymm7
+ %define xgft4_hi ymm6
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+ %define xp3 ymm4
+ %define xp4 ymm5
+%else
+ %define ymm_trans ymm7 ;reuse xmask0f and xgft1_hi
+ %define xmask0f ymm_trans
+ %define xmask0fx xmm7
+ %define xgft1_lo ymm6
+ %define xgft1_hi ymm_trans
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+ %define xgft4_lo xgft1_lo
+ %define xgft4_hi xgft1_hi
+
+ %define x0 ymm0
+ %define xtmpa ymm1
+ %define xp1 ymm2
+ %define xp2 ymm3
+ %define xp3 ymm4
+ %define xp4 ymm5
+%endif
+
+align 16
+global gf_4vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 32
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+ mov vskip3, vec
+ imul vskip3, 96
+ SSTR vskip3_m, vskip3
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest4, [dest1+3*PS]
+ SSTR dest4_m, dest4
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop32:
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ add vec_i, PS
+ %ifidn PS,8 ;64-bit code
+ vpand xgft4_lo, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xgft4_lo, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xgft4_lo, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft4_hi, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ add tmp, 32
+ %else ;32-bit code
+ mov cl, 0x0f ;use ecx as a temp variable
+ vpinsrb xmask0fx, xmask0fx, ecx, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ vpand xgft4_lo, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xgft4_lo, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xgft4_lo, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ %endif
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ vmovdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ %endif
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ sal vec, 1
+ vmovdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ sar vec, 1
+ %endif
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ %ifidn PS,4 ; 32-bit code
+ SLDR vskip3, vskip3_m
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " DX{00}, Dx{10}, ..., Dx{f0}
+ vperm2i128 xgft4_hi, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ add tmp, 32
+ %endif
+ vpshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpxor xp4, xgft4_hi ;xp4 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+ SLDR dest4, dest4_m
+ XSTR [dest4+pos], xp4
+
+ SLDR len, len_m
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-32
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_4vect_dot_prod_avx2, 04, 05, 0198
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx512.asm
new file mode 100644
index 0000000000..c810008c85
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_avx512.asm
@@ -0,0 +1,306 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_dot_prod_avx512(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ save_reg rdi, 9*16 + 4*8
+ save_reg rsi, 9*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ mov rdi, [rsp + 9*16 + 4*8]
+ mov rsi, [rsp + 9*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define dest4 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%define xmask0f zmm14
+%define xgft1_lo zmm13
+%define xgft1_loy ymm13
+%define xgft1_hi zmm12
+%define xgft2_lo zmm11
+%define xgft2_loy ymm11
+%define xgft2_hi zmm10
+%define xgft3_lo zmm9
+%define xgft3_loy ymm9
+%define xgft3_hi zmm8
+%define xgft4_lo zmm7
+%define xgft4_loy ymm7
+%define xgft4_hi zmm6
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xp1 zmm2
+%define xp2 zmm3
+%define xp3 zmm4
+%define xp4 zmm5
+
+default rel
+[bits 64]
+
+section .text
+
+align 16
+global gf_4vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_dot_prod_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_dot_prod_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest2, [dest1+PS]
+ mov dest3, [dest1+2*PS]
+ mov dest4, [dest1+3*PS]
+ mov dest1, [dest1]
+
+.loop64:
+ vpxorq xp1, xp1, xp1
+ vpxorq xp2, xp2, xp2
+ vpxorq xp3, xp3, xp3
+ vpxorq xp4, xp4, xp4
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vmovdqu8 xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu8 xgft2_loy, [tmp+vec*(32/PS)] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ vmovdqu8 xgft3_loy, [tmp+vec*(64/PS)] ;Load array Cx{00}..{0f}, Cx{00}..{f0}
+ vmovdqu8 xgft4_loy, [tmp+vskip3] ;Load array Dx{00}..{0f}, Dx{00}..{f0}
+ add tmp, 32
+
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+
+ vpshufb xgft1_hi, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxorq xp1, xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft2_hi, xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxorq xp2, xp2, xgft2_hi ;xp2 += partial
+
+ vshufi64x2 xgft3_hi, xgft3_lo, xgft3_lo, 0x55
+ vshufi64x2 xgft3_lo, xgft3_lo, xgft3_lo, 0x00
+ vshufi64x2 xgft4_hi, xgft4_lo, xgft4_lo, 0x55
+ vshufi64x2 xgft4_lo, xgft4_lo, xgft4_lo, 0x00
+
+ vpshufb xgft3_hi, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft3_hi, xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxorq xp3, xp3, xgft3_hi ;xp3 += partial
+
+ vpshufb xgft4_hi, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft4_hi, xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpxorq xp4, xp4, xgft4_hi ;xp4 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [dest3+pos], xp3
+ XSTR [dest4+pos], xp4
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_4vect_dot_prod_avx512
+no_gf_4vect_dot_prod_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..8a486bf7b5
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_dot_prod_sse.asm
@@ -0,0 +1,448 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_dot_prod_sse(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 9*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_reg r12, 9*16 + 0*8
+ save_reg r13, 9*16 + 1*8
+ save_reg r14, 9*16 + 2*8
+ save_reg r15, 9*16 + 3*8
+ save_reg rdi, 9*16 + 4*8
+ save_reg rsi, 9*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm8, [rsp + 2*16]
+ movdqa xmm9, [rsp + 3*16]
+ movdqa xmm10, [rsp + 4*16]
+ movdqa xmm11, [rsp + 5*16]
+ movdqa xmm12, [rsp + 6*16]
+ movdqa xmm13, [rsp + 7*16]
+ movdqa xmm14, [rsp + 8*16]
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r13, [rsp + 9*16 + 1*8]
+ mov r14, [rsp + 9*16 + 2*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ mov rdi, [rsp + 9*16 + 4*8]
+ mov rsi, [rsp + 9*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; var0
+;;; var1
+;;; var2
+;;; var3
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+ %define var(x) [ebp - PS - PS*x]
+
+ %define trans ecx
+ %define trans2 esi
+ %define arg0 trans ;trans and trans2 are for the variables in stack
+ %define arg0_m arg(0)
+ %define arg1 ebx
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 trans
+ %define arg3_m arg(3)
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define arg5 trans2
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 trans2
+ %define tmp3_m var(0)
+ %define tmp4 trans2
+ %define tmp4_m var(1)
+ %define tmp5 trans2
+ %define tmp5_m var(2)
+ %define tmp6 trans2
+ %define tmp6_m var(3)
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ sub esp, PS*4 ;4 local variables
+ push esi
+ push edi
+ push ebx
+ mov arg1, arg(1)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ add esp, PS*4 ;4 local variables
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest2 tmp3
+%define dest3 tmp4
+%define dest4 tmp5
+%define vskip3 tmp6
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define len_m arg0_m
+ %define src_m arg3_m
+ %define dest1_m arg4_m
+ %define dest2_m tmp3_m
+ %define dest3_m tmp4_m
+ %define dest4_m tmp5_m
+ %define vskip3_m tmp6_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+
+section .text
+
+%ifidn PS,8 ;64-bit code
+ %define xmask0f xmm14
+ %define xgft1_lo xmm2
+ %define xgft1_hi xmm3
+ %define xgft2_lo xmm11
+ %define xgft2_hi xmm4
+ %define xgft3_lo xmm9
+ %define xgft3_hi xmm5
+ %define xgft4_lo xmm7
+ %define xgft4_hi xmm6
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm8
+ %define xp2 xmm10
+ %define xp3 xmm12
+ %define xp4 xmm13
+%else
+ %define xmm_trans xmm7 ;reuse xmask0f and xgft1_lo
+ %define xmask0f xmm_trans
+ %define xgft1_lo xmm_trans
+ %define xgft1_hi xmm6
+ %define xgft2_lo xgft1_lo
+ %define xgft2_hi xgft1_hi
+ %define xgft3_lo xgft1_lo
+ %define xgft3_hi xgft1_hi
+ %define xgft4_lo xgft1_lo
+ %define xgft4_hi xgft1_hi
+
+ %define x0 xmm0
+ %define xtmpa xmm1
+ %define xp1 xmm2
+ %define xp2 xmm3
+ %define xp3 xmm4
+ %define xp4 xmm5
+%endif
+align 16
+global gf_4vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip3, vec
+ imul vskip3, 96
+ SSTR vskip3_m, vskip3
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ SLDR dest1, dest1_m
+ mov dest2, [dest1+PS]
+ SSTR dest2_m, dest2
+ mov dest3, [dest1+2*PS]
+ SSTR dest3_m, dest3
+ mov dest4, [dest1+3*PS]
+ SSTR dest4_m, dest4
+ mov dest1, [dest1]
+ SSTR dest1_m, dest1
+
+.loop16:
+ pxor xp1, xp1
+ pxor xp2, xp2
+ pxor xp3, xp3
+ pxor xp4, xp4
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ SLDR src, src_m
+ mov ptr, [src+vec_i]
+
+ %ifidn PS,8 ;64-bit code
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ movdqu xgft3_lo, [tmp+vec*(64/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vec*(64/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ movdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ movdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add tmp, 32
+ add vec_i, PS
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+ %else ;32-bit code
+ XLDR x0, [ptr+pos] ;Get next source vector
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ %endif
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp1, xgft1_hi ;xp1 += partial
+
+ %ifidn PS,4 ;32-bit code
+ movdqu xgft2_lo, [tmp+vec*(32/PS)] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vec*(32/PS)+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ %endif
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp2, xgft2_hi ;xp2 += partial
+
+ %ifidn PS,4 ;32-bit code
+ sal vec, 1
+ movdqu xgft3_lo, [tmp+vec*(32/PS)] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vec*(32/PS)+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ sar vec, 1
+ %endif
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pxor xp3, xgft3_hi ;xp3 += partial
+
+ %ifidn PS,4 ;32-bit code
+ SLDR vskip3, vskip3_m
+ movdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ movdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+ add tmp, 32
+ add vec_i, PS
+ %endif
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pxor xp4, xgft4_hi ;xp4 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest1, dest1_m
+ SLDR dest2, dest2_m
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ SLDR dest3, dest3_m
+ XSTR [dest3+pos], xp3
+ SLDR dest4, dest4_m
+ XSTR [dest4+pos], xp4
+
+ SLDR len, len_m
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_4vect_dot_prod_sse, 00, 06, 0064
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx.asm
new file mode 100644
index 0000000000..2d351663c3
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx.asm
@@ -0,0 +1,341 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r15, 10*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r15, [rsp + 10*16 + 2*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_4vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 tmp2
+%define dest4 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft3_hi xmm14
+%define xgft4_hi xmm13
+%define xgft4_lo xmm12
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xtmph4 xmm8
+%define xtmpl4 xmm9
+%define xd1 xmm10
+%define xd2 xmm11
+%define xd3 xtmph1
+%define xd4 xtmpl1
+
+align 16
+global gf_4vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_mad_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ mov tmp, vec
+
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+
+ sal tmp, 6 ;Multiply by 64
+ vmovdqu xgft3_hi, [tmp3+tmp+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ sal vec, 5 ;Multiply by 32
+ add tmp, vec
+ vmovdqu xgft4_lo, [tmp3+tmp] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+ vmovdqu xgft4_hi, [tmp3+tmp+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS]
+ mov dest4, [dest1+3*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+ vmovdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xtmpl1, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1
+
+ XLDR xd3, [dest3+pos] ;Reuse xtmph1, Get next dest vector
+ XLDR xd4, [dest4+pos] ;Reuse xtmpl1, Get next dest vector
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2
+
+ ; dest3
+ vpshufb xtmph3, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xd3, xtmph3
+
+ ; dest4
+ vpshufb xtmph4, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl4, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph4, xtmph4, xtmpl4 ;GF add high and low partials
+ vpxor xd4, xd4, xtmph4
+
+ XSTR [dest1+pos], xd1 ;Store result
+ XSTR [dest2+pos], xd2 ;Store result
+ XSTR [dest3+pos], xd3 ;Store result
+ XSTR [dest4+pos], xd4 ;Store result
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+
+ mov tmp, len ;Overlapped offset length-16
+
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ vmovdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xtmph4, [dest3+tmp] ;Get next dest vector
+
+ sub len, pos
+
+ vmovdqa xtmpl4, [constip16] ;Load const of i + 16
+ vpinsrb xtmph3, xtmph3, len.w, 15
+ vpshufb xtmph3, xtmph3, xmask0f ;Broadcast len to all bytes
+ vpcmpgtb xtmph3, xtmph3, xtmpl4
+
+ XLDR xtmpl4, [dest4+tmp] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xtmpl1, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmph3
+ vpxor xd1, xd1, xtmph1
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmph3
+ vpxor xd2, xd2, xtmph2
+
+ ; dest3
+ vpshufb xgft3_hi, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_hi, xtmpl3 ;GF add high and low partials
+ vpand xgft3_hi, xgft3_hi, xtmph3
+ vpxor xtmph4, xtmph4, xgft3_hi
+
+ ; dest4
+ vpshufb xgft4_hi, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpand xgft4_hi, xgft4_hi, xtmph3
+ vpxor xtmpl4, xtmpl4, xgft4_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result
+ XSTR [dest2+tmp], xd2 ;Store result
+ XSTR [dest3+tmp], xtmph4 ;Store result
+ XSTR [dest4+tmp], xtmpl4 ;Store result
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_4vect_mad_avx, 02, 01, 020a
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx2.asm
new file mode 100644
index 0000000000..9ec431ff27
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx2.asm
@@ -0,0 +1,347 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+
+;;; gf_4vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 vec
+%define dest4 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft2_lo ymm13
+%define xgft3_lo ymm12
+%define xgft4_lo ymm11
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmpl ymm2
+%define xtmplx xmm2
+%define xtmph1 ymm3
+%define xtmph1x xmm3
+%define xtmph2 ymm4
+%define xtmph3 ymm5
+%define xtmph4 ymm6
+%define xd1 ymm7
+%define xd2 ymm8
+%define xd3 ymm9
+%define xd4 ymm10
+
+align 16
+global gf_4vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5 ;Multiply by 32
+ lea tmp, [mul_array + vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ add tmp, vec
+ vmovdqu xgft4_lo, [tmp+2*vec] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+ ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec
+ mov dest4, [dest1+3*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+ XLDR xd4, [dest4+pos] ;reuse xtmpl1. Get next dest vector
+
+ vpand xtmpl, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vperm2i128 xtmpa, xtmpl, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmpl, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vperm2i128 xtmph1, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph3, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph4, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3, xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl ;GF add high and low partials
+ vpxor xd3, xd3, xtmph3 ;xd3 += partial
+
+ ; dest4
+ vpshufb xtmph4, xtmph4, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph4, xtmph4, xtmpl ;GF add high and low partials
+ vpxor xd4, xd4, xtmph4 ;xd4 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+ XSTR [dest4+pos], xd4
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan32:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp.b, 0x1f
+ vpinsrb xtmph1x, xtmph1x, tmp.w, 0
+ vpbroadcastb xtmph1, xtmph1x ;Construct mask 0x1f1f1f...
+
+ mov tmp, len ;Overlapped offset length-32
+
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xd3, [dest3+tmp] ;Get next dest vector
+ XLDR xd4, [dest4+tmp] ;Get next dest vector
+
+ sub len, pos
+
+ vmovdqa xtmph2, [constip32] ;Load const of i + 32
+ vpinsrb xtmplx, xtmplx, len.w, 15
+ vinserti128 xtmpl, xtmpl, xtmplx, 1 ;swapped to xtmplx | xtmplx
+ vpshufb xtmpl, xtmpl, xtmph1 ;Broadcast len to all bytes. xtmph1=0x1f1f1f...
+ vpcmpgtb xtmpl, xtmpl, xtmph2
+
+ vpand xtmph1, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vperm2i128 xtmpa, xtmph1, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmph1, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vperm2i128 xtmph1, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph3, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph4, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xgft1_lo ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmpl
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xgft2_lo ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmpl
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3, xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xgft3_lo ;GF add high and low partials
+ vpand xtmph3, xtmph3, xtmpl
+ vpxor xd3, xd3, xtmph3 ;xd3 += partial
+
+ ; dest4
+ vpshufb xtmph4, xtmph4, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph4, xtmph4, xgft4_lo ;GF add high and low partials
+ vpand xtmph4, xtmph4, xtmpl
+ vpxor xd4, xd4, xtmph4 ;xd4 += partial
+
+ XSTR [dest1+tmp], xd1
+ XSTR [dest2+tmp], xd2
+ XSTR [dest3+tmp], xd3
+ XSTR [dest4+tmp], xd4
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+align 32
+constip32:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+ dq 0xe8e9eaebecedeeef, 0xe0e1e2e3e4e5e6e7
+
+;;; func core, ver, snum
+slversion gf_4vect_mad_avx2, 04, 01, 020b
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx512.asm
new file mode 100644
index 0000000000..7a5866fdf0
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_avx512.asm
@@ -0,0 +1,272 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_mad_avx512(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+%endif
+
+%define PS 8
+%define len arg0
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define dest2 mul_array
+%define dest3 vec
+%define dest4 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+[bits 64]
+section .text
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xtmpl1 zmm2
+%define xtmph1 zmm3
+%define xtmph2 zmm4
+%define xtmph3 zmm5
+%define xtmph4 zmm6
+%define xgft1_hi zmm7
+%define xgft1_lo zmm8
+%define xgft1_loy ymm8
+%define xgft2_hi zmm9
+%define xgft2_lo zmm10
+%define xgft2_loy ymm10
+%define xgft3_hi zmm11
+%define xgft3_lo zmm12
+%define xgft3_loy ymm12
+%define xgft4_hi zmm13
+%define xgft4_lo zmm14
+%define xgft4_loy ymm14
+%define xd1 zmm15
+%define xd2 zmm16
+%define xd3 zmm17
+%define xd4 zmm18
+%define xmask0f zmm19
+%define xtmpl2 zmm20
+%define xtmpl3 zmm21
+%define xtmpl4 zmm22
+%define xtmpl5 zmm23
+
+align 16
+global gf_4vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_mad_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_mad_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5 ;Multiply by 32
+ lea tmp, [mul_array + vec_i]
+ vmovdqu xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ vmovdqu xgft2_loy, [tmp+vec] ;Load array Bx{00}..{0f}, Bx{00}..{f0}
+ vmovdqu xgft3_loy, [tmp+2*vec] ;Load array Cx{00}..{0f}, Cx{00}..{f0}
+ add tmp, vec
+ vmovdqu xgft4_loy, [tmp+2*vec] ;Load array Dx{00}..{0f}, Dx{00}..{f0}
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+ vshufi64x2 xgft2_hi, xgft2_lo, xgft2_lo, 0x55
+ vshufi64x2 xgft2_lo, xgft2_lo, xgft2_lo, 0x00
+ vshufi64x2 xgft3_hi, xgft3_lo, xgft3_lo, 0x55
+ vshufi64x2 xgft3_lo, xgft3_lo, xgft3_lo, 0x00
+ vshufi64x2 xgft4_hi, xgft4_lo, xgft4_lo, 0x55
+ vshufi64x2 xgft4_lo, xgft4_lo, xgft4_lo, 0x00
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS] ; reuse vec
+ mov dest4, [dest1+3*PS] ; reuse vec_i
+ mov dest1, [dest1]
+ mov tmp, -1
+ kmovq k1, tmp
+
+.loop64:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+ XLDR xd4, [dest4+pos] ;reuse xtmpl1. Get next dest vector
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1 {k1}{z}, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1 {k1}{z}, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxorq xd1, xd1, xtmph1 ;xd1 += partial
+
+ ; dest2
+ vpshufb xtmph2 {k1}{z}, xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2 {k1}{z}, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxorq xd2, xd2, xtmph2 ;xd2 += partial
+
+ ; dest3
+ vpshufb xtmph3 {k1}{z}, xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3 {k1}{z}, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxorq xd3, xd3, xtmph3 ;xd2 += partial
+
+ ; dest4
+ vpshufb xtmph4 {k1}{z}, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl4 {k1}{z}, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph4, xtmph4, xtmpl4 ;GF add high and low partials
+ vpxorq xd4, xd4, xtmph4 ;xd2 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+ XSTR [dest4+pos], xd4
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, (1 << 63)
+ lea tmp, [len + 64 - 1]
+ and tmp, 63
+ sarx pos, pos, tmp
+ kmovq k1, pos
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_4vect_mad_avx512
+no_gf_4vect_mad_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_4vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_sse.asm
new file mode 100644
index 0000000000..32b6cda183
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_4vect_mad_sse.asm
@@ -0,0 +1,347 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_4vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r15, 10*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r15, [rsp + 10*16 + 2*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_4vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 tmp2
+%define dest4 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft3_hi xmm14
+%define xgft4_hi xmm13
+%define xgft4_lo xmm12
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xtmph4 xmm8
+%define xtmpl4 xmm9
+%define xd1 xmm10
+%define xd2 xmm11
+%define xd3 xtmph1
+%define xd4 xtmpl1
+
+align 16
+global gf_4vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_4vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_4vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_4vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov tmp, vec
+
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+
+ sal tmp, 6 ;Multiply by 64
+
+ movdqu xgft3_hi, [tmp3+tmp+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ sal vec, 5 ;Multiply by 32
+ add tmp, vec
+ movdqu xgft4_lo, [tmp3+tmp] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+ movdqu xgft4_hi, [tmp3+tmp+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+
+ mov dest2, [dest1+PS] ; reuse mul_array
+ mov dest3, [dest1+2*PS]
+ mov dest4, [dest1+3*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+ movdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+
+ movdqa xtmph3, xgft3_hi
+ movdqa xtmpl4, xgft4_lo
+ movdqa xtmph4, xgft4_hi
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ XLDR xd3, [dest3+pos] ;Reuse xtmph1, Get next dest vector
+ XLDR xd4, [dest4+pos] ;Reuse xtmpl1, Get next dest vector
+
+ ; dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ ; dest3
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pxor xd3, xtmph3
+
+ ; dest4
+ pshufb xtmph4, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl4, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph4, xtmpl4 ;GF add high and low partials
+ pxor xd4, xtmph4
+
+ XSTR [dest1+pos], xd1 ;Store result
+ XSTR [dest2+pos], xd2 ;Store result
+ XSTR [dest3+pos], xd3 ;Store result
+ XSTR [dest4+pos], xd4 ;Store result
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp, len ;Overlapped offset length-16
+
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ movdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xtmph4, [dest3+tmp] ;Reuse xtmph1. Get next dest vector
+
+ sub len, pos
+
+ movdqa xtmpl4, [constip16] ;Load const of i + 16
+ pinsrb xtmph3, len.w, 15
+ pshufb xtmph3, xmask0f ;Broadcast len to all bytes
+ pcmpgtb xtmph3, xtmpl4
+
+ XLDR xtmpl4, [dest4+tmp] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pand xtmph1, xtmph3
+ pxor xd1, xtmph1
+
+ ; dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pand xtmph2, xtmph3
+ pxor xd2, xtmph2
+
+ ; dest3
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xtmpl3 ;GF add high and low partials
+ pand xgft3_hi, xtmph3
+ pxor xtmph4, xgft3_hi
+
+ ; dest4
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pand xgft4_hi, xtmph3
+ pxor xtmpl4, xgft4_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result
+ XSTR [dest2+tmp], xd2 ;Store result
+ XSTR [dest3+tmp], xtmph4 ;Store result
+ XSTR [dest4+tmp], xtmpl4 ;Store result
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+ dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_4vect_mad_sse, 00, 01, 0209
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..1d8cccf70b
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx.asm
@@ -0,0 +1,308 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_dot_prod_avx(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_xmm128 xmm15, 9*16
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ vmovdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm14
+%define xgft1_hi xmm13
+%define xgft2_lo xmm12
+%define xgft2_hi xmm11
+%define xgft3_lo xmm10
+%define xgft3_hi xmm9
+%define xgft4_lo xmm8
+%define xgft4_hi xmm7
+
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp1 xmm2
+%define xp2 xmm3
+%define xp3 xmm4
+%define xp4 xmm5
+%define xp5 xmm6
+
+align 16
+global gf_5vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop16:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ vpxor xp5, xp5
+
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ add vec_i, PS
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vskip1*1+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vskip1*2+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ vmovdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ vmovdqu xgft1_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ vmovdqu xgft1_hi, [tmp+vskip1*4+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ add tmp, 32
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ vpshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpxor xp4, xgft4_hi ;xp4 += partial
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp5, xgft1_hi ;xp5 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_5vect_dot_prod_avx, 02, 04, 0194
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..0cdfee906e
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_avx2.asm
@@ -0,0 +1,320 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ vmovdqa [rsp + 9*16], xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ vmovdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft1_hi ymm13
+%define xgft2_lo ymm12
+%define xgft2_hi ymm11
+%define xgft3_lo ymm10
+%define xgft3_hi ymm9
+%define xgft4_lo ymm8
+%define xgft4_hi ymm7
+
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xp1 ymm2
+%define xp2 ymm3
+%define xp3 ymm4
+%define xp4 ymm5
+%define xp5 ymm6
+
+align 16
+global gf_5vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop32:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ vpxor xp5, xp5
+
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpand xgft4_lo, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xgft4_lo, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xgft4_lo, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft4_hi, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ vmovdqu xgft1_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ add tmp, 32
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+ vpshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpxor xp4, xgft4_hi ;xp4 += partial
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp5, xgft1_hi ;xp5 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_5vect_dot_prod_avx2, 04, 04, 0199
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..577875dbb4
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_dot_prod_sse.asm
@@ -0,0 +1,309 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_dot_prod_sse(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_xmm128 xmm15, 9*16
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm8, [rsp + 2*16]
+ movdqa xmm9, [rsp + 3*16]
+ movdqa xmm10, [rsp + 4*16]
+ movdqa xmm11, [rsp + 5*16]
+ movdqa xmm12, [rsp + 6*16]
+ movdqa xmm13, [rsp + 7*16]
+ movdqa xmm14, [rsp + 8*16]
+ movdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm2
+%define xgft1_hi xmm3
+%define xgft2_lo xmm4
+%define xgft2_hi xmm5
+%define xgft3_lo xmm10
+%define xgft3_hi xmm6
+%define xgft4_lo xmm8
+%define xgft4_hi xmm7
+
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp1 xmm9
+%define xp2 xmm11
+%define xp3 xmm12
+%define xp4 xmm13
+%define xp5 xmm14
+
+align 16
+global gf_5vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop16:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ pxor xp1, xp1
+ pxor xp2, xp2
+ pxor xp3, xp3
+ pxor xp4, xp4
+ pxor xp5, xp5
+
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ add vec_i, PS
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ movdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vskip1*1+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ movdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vskip1*2+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ movdqu xgft4_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ movdqu xgft4_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp1, xgft1_hi ;xp1 += partial
+
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp2, xgft2_hi ;xp2 += partial
+
+ movdqu xgft1_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ movdqu xgft1_hi, [tmp+vskip1*4+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ add tmp, 32
+
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pxor xp3, xgft3_hi ;xp3 += partial
+
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pxor xp4, xgft4_hi ;xp4 += partial
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp5, xgft1_hi ;xp5 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_5vect_dot_prod_sse, 00, 05, 0065
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx.asm
new file mode 100644
index 0000000000..8f38a415a1
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx.asm
@@ -0,0 +1,370 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13
+ %define tmp4 r14
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 5*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12
+ %define tmp4 r13
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_5vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp4
+%define dest3 mul_array
+%define dest4 tmp2
+%define dest5 vec_i
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft5_hi xmm14
+%define xgft4_lo xmm13
+%define xgft4_hi xmm12
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xtmph5 xmm8
+%define xtmpl5 xmm9
+%define xd1 xmm10
+%define xd2 xmm11
+%define xd3 xtmpl1
+%define xd4 xtmph1
+%define xd5 xtmpl2
+
+
+align 16
+global gf_5vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_mad_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov tmp, vec
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+ sal tmp, 6 ;Multiply by 64
+ vmovdqu xgft5_hi, [tmp3+2*tmp+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ sal vec, 5 ;Multiply by 32
+ add tmp, vec
+ vmovdqu xgft4_hi, [tmp3+tmp+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+ vmovdqu xgft4_lo, [tmp3+tmp] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+
+ mov dest3, [dest1+2*PS] ; reuse mul_array
+ mov dest4, [dest1+3*PS]
+ mov dest5, [dest1+4*PS] ; reuse vec_i
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vmovdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ vmovdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xtmpl5, [tmp3+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xtmpl1, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1
+
+ XLDR xd3, [dest3+pos] ;Reuse xtmpl1, Get next dest vector
+ XLDR xd4, [dest4+pos] ;Reuse xtmph1, Get next dest vector
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2
+
+ XLDR xd5, [dest5+pos] ;Reuse xtmpl2. Get next dest vector
+
+ ; dest3
+ vpshufb xtmph3, xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xd3, xtmph3
+
+ ; dest4
+ vpshufb xtmph2, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl3 ;GF add high and low partials
+ vpxor xd4, xd4, xtmph2
+
+ ; dest5
+ vpshufb xtmph5, xgft5_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl5, xtmpl5, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph5, xtmph5, xtmpl5 ;GF add high and low partials
+ vpxor xd5, xd5, xtmph5
+
+ XSTR [dest1+pos], xd1 ;Store result into dest1
+ XSTR [dest2+pos], xd2 ;Store result into dest2
+ XSTR [dest3+pos], xd3 ;Store result into dest3
+ XSTR [dest4+pos], xd4 ;Store result into dest4
+ XSTR [dest5+pos], xd5 ;Store result into dest5
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp, len ;Overlapped offset length-16
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ sub len, pos
+
+ vmovdqa xtmph1, [constip16] ;Load const of i + 16
+ vpinsrb xtmph5, len.w, 15
+ vpshufb xtmph5, xmask0f ;Broadcast len to all bytes
+ vpcmpgtb xtmph5, xtmph5, xtmph1
+
+ vmovdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ vmovdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xtmpl5, [tmp3+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xtmpl1, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmph5
+ vpxor xd1, xd1, xtmph1
+
+ XLDR xd3, [dest3+tmp] ;Reuse xtmpl1, Get next dest vector
+ XLDR xd4, [dest4+tmp] ;Reuse xtmph1, Get next dest vector
+
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmph5
+ vpxor xd2, xd2, xtmph2
+
+ XLDR xd5, [dest5+tmp] ;Reuse xtmpl2. Get next dest vector
+
+ ; dest3
+ vpshufb xtmph3, xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpand xtmph3, xtmph3, xtmph5
+ vpxor xd3, xd3, xtmph3
+
+ ; dest4
+ vpshufb xgft4_hi, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpand xgft4_hi, xgft4_hi, xtmph5
+ vpxor xd4, xd4, xgft4_hi
+
+ ; dest5
+ vpshufb xgft5_hi, xgft5_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl5, xtmpl5, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft5_hi, xgft5_hi, xtmpl5 ;GF add high and low partials
+ vpand xgft5_hi, xgft5_hi, xtmph5
+ vpxor xd5, xd5, xgft5_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result into dest1
+ XSTR [dest2+tmp], xd2 ;Store result into dest2
+ XSTR [dest3+tmp], xd3 ;Store result into dest3
+ XSTR [dest4+tmp], xd4 ;Store result into dest4
+ XSTR [dest5+tmp], xd5 ;Store result into dest5
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_5vect_mad_avx, 02, 01, 020d
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx2.asm
new file mode 100644
index 0000000000..9029f9287e
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_avx2.asm
@@ -0,0 +1,368 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r15, 10*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r15, [rsp + 10*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_5vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp2
+%define dest3 mul_array
+%define dest4 vec
+%define dest5 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft2_lo ymm13
+%define xgft3_lo ymm12
+%define xgft4_lo ymm11
+%define xgft5_lo ymm10
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmpl ymm2
+%define xtmplx xmm2
+%define xtmph1 ymm3
+%define xtmph1x xmm3
+%define xtmph2 ymm4
+%define xd1 ymm5
+%define xd2 ymm6
+%define xd3 ymm7
+%define xd4 ymm8
+%define xd5 ymm9
+
+align 16
+global gf_5vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5 ;Multiply by 32
+ lea tmp, [mul_array + vec_i]
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft5_lo, [tmp+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ add tmp, vec
+ vmovdqu xgft4_lo, [tmp+2*vec] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ mov dest3, [dest1+2*PS] ; reuse mul_array
+ mov dest4, [dest1+3*PS] ; reuse vec
+ mov dest5, [dest1+4*PS] ; reuse vec_i
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+.loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+ XLDR xd4, [dest4+pos] ;Get next dest vector
+ XLDR xd5, [dest5+pos] ;Get next dest vector
+
+ vpand xtmpl, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xtmpl, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmpl, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vperm2i128 xtmph1, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ vperm2i128 xtmph1, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ vperm2i128 xtmph2, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ ; dest3
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl ;GF add high and low partials
+ vpxor xd3, xd3, xtmph1 ;xd3 += partial
+
+ vperm2i128 xtmph1, xgft5_lo, xgft5_lo, 0x01 ; swapped to hi | lo
+ ; dest4
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl ;GF add high and low partials
+ vpxor xd4, xd4, xtmph2 ;xd4 += partial
+
+ ; dest5
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl ;GF add high and low partials
+ vpxor xd5, xd5, xtmph1 ;xd5 += partial
+
+ XSTR [dest1+pos], xd1
+ XSTR [dest2+pos], xd2
+ XSTR [dest3+pos], xd3
+ XSTR [dest4+pos], xd4
+ XSTR [dest5+pos], xd5
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan32:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp.b, 0x1f
+ vpinsrb xtmph1x, xtmph1x, tmp.w, 0
+ vpbroadcastb xtmph1, xtmph1x ;Construct mask 0x1f1f1f...
+
+ mov tmp, len ;Overlapped offset length-32
+
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xd3, [dest3+tmp] ;Get next dest vector
+ XLDR xd4, [dest4+tmp] ;Get next dest vector
+ XLDR xd5, [dest5+tmp] ;Get next dest vector
+
+ sub len, pos
+
+ vmovdqa xtmph2, [constip32] ;Load const of i + 32
+ vpinsrb xtmplx, xtmplx, len.w, 15
+ vinserti128 xtmpl, xtmpl, xtmplx, 1 ;swapped to xtmplx | xtmplx
+ vpshufb xtmpl, xtmpl, xtmph1 ;Broadcast len to all bytes. xtmph1=0x1f1f1f...
+ vpcmpgtb xtmpl, xtmpl, xtmph2
+
+ vpand xtmph1, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xtmph1, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmph1, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vperm2i128 xtmph1, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xtmph2, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+
+ ; dest1
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xgft1_lo ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmpl
+ vpxor xd1, xd1, xtmph1 ;xd1 += partial
+
+ vperm2i128 xtmph1, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ ; dest2
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xgft2_lo ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmpl
+ vpxor xd2, xd2, xtmph2 ;xd2 += partial
+
+ vperm2i128 xtmph2, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ ; dest3
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xgft3_lo ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmpl
+ vpxor xd3, xd3, xtmph1 ;xd3 += partial
+
+ vperm2i128 xtmph1, xgft5_lo, xgft5_lo, 0x01 ; swapped to hi | lo
+ ; dest4
+ vpshufb xtmph2, xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xgft4_lo ;GF add high and low partials
+ vpand xtmph2, xtmph2, xtmpl
+ vpxor xd4, xd4, xtmph2 ;xd4 += partial
+
+ ; dest5
+ vpshufb xtmph1, xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xgft5_lo, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xgft5_lo ;GF add high and low partials
+ vpand xtmph1, xtmph1, xtmpl
+ vpxor xd5, xd5, xtmph1 ;xd5 += partial
+
+ XSTR [dest1+tmp], xd1
+ XSTR [dest2+tmp], xd2
+ XSTR [dest3+tmp], xd3
+ XSTR [dest4+tmp], xd4
+ XSTR [dest5+tmp], xd5
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+align 32
+constip32:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+ dq 0xe8e9eaebecedeeef, 0xe0e1e2e3e4e5e6e7
+
+;;; func core, ver, snum
+slversion gf_5vect_mad_avx2, 04, 01, 020e
diff --git a/contrib/libs/isa-l/erasure_code/gf_5vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_sse.asm
new file mode 100644
index 0000000000..15c96bf4dc
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_5vect_mad_sse.asm
@@ -0,0 +1,378 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_5vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13
+ %define tmp4 r14
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 5*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12
+ %define tmp4 r13
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_5vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp4
+%define dest3 mul_array
+%define dest4 tmp2
+%define dest5 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft5_hi xmm14
+%define xgft4_lo xmm13
+%define xgft4_hi xmm12
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xtmph5 xmm8
+%define xtmpl5 xmm9
+%define xd1 xmm10
+%define xd2 xmm11
+%define xd3 xtmpl1
+%define xd4 xtmph1
+%define xd5 xtmpl2
+
+
+align 16
+global gf_5vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_5vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_5vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_5vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov tmp, vec
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+ sal tmp, 6 ;Multiply by 64
+ movdqu xgft5_hi, [tmp3+2*tmp+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ sal vec, 5 ;Multiply by 32
+ add tmp, vec
+ movdqu xgft4_hi, [tmp3+tmp+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+ movdqu xgft4_lo, [tmp3+tmp] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+
+ mov dest3, [dest1+2*PS] ; reuse mul_array
+ mov dest4, [dest1+3*PS]
+ mov dest5, [dest1+4*PS] ; reuse vec_i
+ mov dest2, [dest1+PS]
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ movdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ movdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xtmpl5, [tmp3+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ movdqa xtmph5, xgft5_hi ;Reload const array registers
+
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ XLDR xd3, [dest3+pos] ;Reuse xtmpl1, Get next dest vector
+ XLDR xd4, [dest4+pos] ;Reuse xtmph1. Get next dest vector
+
+ ; dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ XLDR xd5, [dest5+pos] ;Reuse xtmpl2. Get next dest vector
+
+ ; dest3
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pxor xd3, xtmph3
+
+ movdqa xtmph2, xgft4_hi ;Reload const array registers
+ movdqa xtmpl3, xgft4_lo ;Reload const array registers
+
+ ; dest5
+ pshufb xtmph5, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl5, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph5, xtmpl5 ;GF add high and low partials
+ pxor xd5, xtmph5
+
+ ; dest4
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl3 ;GF add high and low partials
+ pxor xd4, xtmph2
+
+ XSTR [dest1+pos], xd1 ;Store result into dest1
+ XSTR [dest2+pos], xd2 ;Store result into dest2
+ XSTR [dest3+pos], xd3 ;Store result into dest3
+ XSTR [dest4+pos], xd4 ;Store result into dest4
+ XSTR [dest5+pos], xd5 ;Store result into dest5
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp, len ;Overlapped offset length-16
+ XLDR x0, [src+tmp] ;Get next source vector
+
+ sub len, pos
+
+ movdqa xtmpl1, [constip16] ;Load const of i + 16
+ pinsrb xtmph5, len.w, 15
+ pshufb xtmph5, xmask0f ;Broadcast len to all bytes
+ pcmpgtb xtmph5, xtmpl1
+
+ movdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ movdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xtmpl5, [tmp3+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ; dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pand xtmph1, xtmph5
+ pxor xd1, xtmph1
+
+ XLDR xd3, [dest3+tmp] ;Reuse xtmpl1, Get next dest vector
+ XLDR xd4, [dest4+tmp] ;Reuse xtmph1. Get next dest vector
+
+ ; dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pand xtmph2, xtmph5
+ pxor xd2, xtmph2
+
+ XLDR xd5, [dest5+tmp] ;Reuse xtmpl2. Get next dest vector
+
+ ; dest3
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pand xtmph3, xtmph5
+ pxor xd3, xtmph3
+
+ ; dest4
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pand xgft4_hi, xtmph5
+ pxor xd4, xgft4_hi
+
+ ; dest5
+ pshufb xgft5_hi, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl5, xtmpa ;Lookup mul table of low nibble
+ pxor xgft5_hi, xtmpl5 ;GF add high and low partials
+ pand xgft5_hi, xtmph5
+ pxor xd5, xgft5_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result into dest1
+ XSTR [dest2+tmp], xd2 ;Store result into dest2
+ XSTR [dest3+tmp], xd3 ;Store result into dest3
+ XSTR [dest4+tmp], xd4 ;Store result into dest4
+ XSTR [dest5+tmp], xd5 ;Store result into dest5
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+ dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_5vect_mad_sse, 00, 01, 020c
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..f12798edec
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx.asm
@@ -0,0 +1,320 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_dot_prod_avx(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_xmm128 xmm15, 9*16
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ vmovdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm14
+%define xgft1_hi xmm13
+%define xgft2_lo xmm12
+%define xgft2_hi xmm11
+%define xgft3_lo xmm10
+%define xgft3_hi xmm9
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp1 xmm2
+%define xp2 xmm3
+%define xp3 xmm4
+%define xp4 xmm5
+%define xp5 xmm6
+%define xp6 xmm7
+
+align 16
+global gf_6vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop16:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ vpxor xp5, xp5
+ vpxor xp6, xp6
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ add vec_i, PS
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ vmovdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ vmovdqu xgft2_hi, [tmp+vskip1*1+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft3_hi, [tmp+vskip1*2+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ lea ptr, [vskip1 + vskip1*4] ;ptr = vskip5
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+
+ vmovdqu xgft1_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ vmovdqu xgft1_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ vmovdqu xgft2_hi, [tmp+vskip1*4+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ vmovdqu xgft3_lo, [tmp+ptr] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ vmovdqu xgft3_hi, [tmp+ptr+16] ; " Fx{00}, Fx{10}, ..., Fx{f0}
+ add tmp, 32
+
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp4, xgft1_hi ;xp4 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp5, xgft2_hi ;xp5 += partial
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp6, xgft3_hi ;xp6 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ mov tmp, [dest+5*PS]
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+ XSTR [tmp+pos], xp6
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_6vect_dot_prod_avx, 02, 04, 0195
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..d5b2543225
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_avx2.asm
@@ -0,0 +1,331 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ vmovdqa [rsp + 0*16], xmm6
+ vmovdqa [rsp + 1*16], xmm7
+ vmovdqa [rsp + 2*16], xmm8
+ vmovdqa [rsp + 3*16], xmm9
+ vmovdqa [rsp + 4*16], xmm10
+ vmovdqa [rsp + 5*16], xmm11
+ vmovdqa [rsp + 6*16], xmm12
+ vmovdqa [rsp + 7*16], xmm13
+ vmovdqa [rsp + 8*16], xmm14
+ vmovdqa [rsp + 9*16], xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm8, [rsp + 2*16]
+ vmovdqa xmm9, [rsp + 3*16]
+ vmovdqa xmm10, [rsp + 4*16]
+ vmovdqa xmm11, [rsp + 5*16]
+ vmovdqa xmm12, [rsp + 6*16]
+ vmovdqa xmm13, [rsp + 7*16]
+ vmovdqa xmm14, [rsp + 8*16]
+ vmovdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft1_hi ymm13
+%define xgft2_lo ymm12
+%define xgft2_hi ymm11
+%define xgft3_lo ymm10
+%define xgft3_hi ymm9
+%define x0 ymm0
+%define xtmpa ymm1
+%define xp1 ymm2
+%define xp2 ymm3
+%define xp3 ymm4
+%define xp4 ymm5
+%define xp5 ymm6
+%define xp6 ymm7
+
+align 16
+global gf_6vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop32:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ vpxor xp1, xp1
+ vpxor xp2, xp2
+ vpxor xp3, xp3
+ vpxor xp4, xp4
+ vpxor xp5, xp5
+ vpxor xp6, xp6
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpand xgft3_lo, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xgft3_lo, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xgft3_lo, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ lea ptr, [vskip1 + vskip1*4] ;ptr = vskip5
+
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp1, xgft1_hi ;xp1 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp2, xgft2_hi ;xp2 += partial
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp3, xgft3_hi ;xp3 += partial
+
+
+ vmovdqu xgft1_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " Dx{00}, Dx{10}, ..., Dx{f0}
+ vmovdqu xgft2_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ vmovdqu xgft3_lo, [tmp+ptr] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ ; " Fx{00}, Fx{10}, ..., Fx{f0}
+ add tmp, 32
+ vperm2i128 xgft1_hi, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft2_hi, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vperm2i128 xgft3_hi, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+
+ vpshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxor xp4, xgft1_hi ;xp4 += partial
+
+ vpshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ vpxor xp5, xgft2_hi ;xp5 += partial
+
+ vpshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ vpxor xp6, xgft3_hi ;xp6 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ mov tmp, [dest+5*PS]
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+ XSTR [tmp+pos], xp6
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_6vect_dot_prod_avx2, 04, 04, 019a
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..5dea0be18e
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_dot_prod_sse.asm
@@ -0,0 +1,320 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_dot_prod_sse(len, vec, *g_tbls, **buffs, **dests);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r12 ; must be saved and restored
+ %define tmp5 r14 ; must be saved and restored
+ %define tmp6 r15 ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ push r15
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13 ; must be saved and restored
+ %define tmp4 r14 ; must be saved and restored
+ %define tmp5 rdi ; must be saved and restored
+ %define tmp6 rsi ; must be saved and restored
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 10*16 + 7*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm8, 2*16
+ save_xmm128 xmm9, 3*16
+ save_xmm128 xmm10, 4*16
+ save_xmm128 xmm11, 5*16
+ save_xmm128 xmm12, 6*16
+ save_xmm128 xmm13, 7*16
+ save_xmm128 xmm14, 8*16
+ save_xmm128 xmm15, 9*16
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ save_reg rsi, 10*16 + 5*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm8, [rsp + 2*16]
+ movdqa xmm9, [rsp + 3*16]
+ movdqa xmm10, [rsp + 4*16]
+ movdqa xmm11, [rsp + 5*16]
+ movdqa xmm12, [rsp + 6*16]
+ movdqa xmm13, [rsp + 7*16]
+ movdqa xmm14, [rsp + 8*16]
+ movdqa xmm15, [rsp + 9*16]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ mov rsi, [rsp + 10*16 + 5*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+%define ptr arg5
+%define vec_i tmp2
+%define dest1 tmp3
+%define dest2 tmp4
+%define vskip1 tmp5
+%define vskip3 tmp6
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft1_lo xmm2
+%define xgft1_hi xmm3
+%define xgft2_lo xmm4
+%define xgft2_hi xmm5
+%define xgft3_lo xmm6
+%define xgft3_hi xmm7
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp1 xmm8
+%define xp2 xmm9
+%define xp3 xmm10
+%define xp4 xmm11
+%define xp5 xmm12
+%define xp6 xmm13
+
+align 16
+global gf_6vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov vskip1, vec
+ imul vskip1, 32
+ mov vskip3, vec
+ imul vskip3, 96
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ mov dest1, [dest]
+ mov dest2, [dest+PS]
+
+
+.loop16:
+ mov tmp, mul_array
+ xor vec_i, vec_i
+ pxor xp1, xp1
+ pxor xp2, xp2
+ pxor xp3, xp3
+ pxor xp4, xp4
+ pxor xp5, xp5
+ pxor xp6, xp6
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ add vec_i, PS
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ movdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ movdqu xgft1_hi, [tmp+16] ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ movdqu xgft2_lo, [tmp+vskip1*1] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ movdqu xgft2_hi, [tmp+vskip1*1+16] ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ movdqu xgft3_lo, [tmp+vskip1*2] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft3_hi, [tmp+vskip1*2+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ lea ptr, [vskip1 + vskip1*4] ;ptr = vskip5
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp1, xgft1_hi ;xp1 += partial
+
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp2, xgft2_hi ;xp2 += partial
+
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pxor xp3, xgft3_hi ;xp3 += partial
+
+
+ movdqu xgft1_lo, [tmp+vskip3] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ movdqu xgft1_hi, [tmp+vskip3+16] ; " Dx{00}, Dx{10}, ..., Dx{f0}
+ movdqu xgft2_lo, [tmp+vskip1*4] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ movdqu xgft2_hi, [tmp+vskip1*4+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ movdqu xgft3_lo, [tmp+ptr] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ movdqu xgft3_hi, [tmp+ptr+16] ; " Fx{00}, Fx{10}, ..., Fx{f0}
+ add tmp, 32
+
+
+ pshufb xgft1_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft1_hi, xgft1_lo ;GF add high and low partials
+ pxor xp4, xgft1_hi ;xp4 += partial
+
+ pshufb xgft2_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft2_hi, xgft2_lo ;GF add high and low partials
+ pxor xp5, xgft2_hi ;xp5 += partial
+
+ pshufb xgft3_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft3_hi, xgft3_lo ;GF add high and low partials
+ pxor xp6, xgft3_hi ;xp6 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+
+ mov tmp, [dest+2*PS]
+ mov ptr, [dest+3*PS]
+ mov vec_i, [dest+4*PS]
+
+ XSTR [dest1+pos], xp1
+ XSTR [dest2+pos], xp2
+ XSTR [tmp+pos], xp3
+ mov tmp, [dest+5*PS]
+ XSTR [ptr+pos], xp4
+ XSTR [vec_i+pos], xp5
+ XSTR [tmp+pos], xp6
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_6vect_dot_prod_sse, 00, 05, 0066
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx.asm
new file mode 100644
index 0000000000..c9d7e57472
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx.asm
@@ -0,0 +1,399 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r13
+ %define tmp4 r14
+ %define tmp5 rdi
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 5*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r12
+ %define tmp4 r13
+ %define tmp5 r14
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_6vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp4
+%define dest3 tmp2
+%define dest4 mul_array
+%define dest5 tmp5
+%define dest6 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft4_lo xmm14
+%define xgft4_hi xmm13
+%define xgft5_lo xmm12
+%define xgft5_hi xmm11
+%define xgft6_lo xmm10
+%define xgft6_hi xmm9
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xd1 xmm8
+%define xd2 xtmpl1
+%define xd3 xtmph1
+
+
+align 16
+global gf_6vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_mad_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ mov tmp, vec
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+ sal tmp, 6 ;Multiply by 64
+
+ sal vec, 5 ;Multiply by 32
+ lea vec_i, [tmp + vec] ;vec_i = vec*96
+ lea mul_array, [tmp + vec_i] ;mul_array = vec*160
+
+ vmovdqu xgft5_lo, [tmp3+2*tmp] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ vmovdqu xgft5_hi, [tmp3+2*tmp+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ vmovdqu xgft4_lo, [tmp3+vec_i] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+ vmovdqu xgft4_hi, [tmp3+vec_i+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+ vmovdqu xgft6_lo, [tmp3+mul_array] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ vmovdqu xgft6_hi, [tmp3+mul_array+16] ; " Fx{00}, Fx{10}, ..., Fx{f0}
+
+ mov dest2, [dest1+PS]
+ mov dest3, [dest1+2*PS]
+ mov dest4, [dest1+3*PS] ; reuse mul_array
+ mov dest5, [dest1+4*PS]
+ mov dest6, [dest1+5*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vmovdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+
+ ;dest1
+ vpshufb xtmph1, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xtmph1
+
+ XLDR xd2, [dest2+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+pos] ;reuse xtmph1. Get next dest vector
+
+ ;dest2
+ vpshufb xtmph2, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xtmph2
+
+ ;dest3
+ vpshufb xtmph3, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xtmph3
+
+ XSTR [dest1+pos], xd1 ;Store result into dest1
+ XSTR [dest2+pos], xd2 ;Store result into dest2
+ XSTR [dest3+pos], xd3 ;Store result into dest3
+
+ ;dest4
+ XLDR xd1, [dest4+pos] ;Get next dest vector
+ vpshufb xtmph1, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl1, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph1, xtmph1, xtmpl1 ;GF add high and low partials
+ vpxor xd1, xd1, xtmph1
+
+ XLDR xd2, [dest5+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest6+pos] ;reuse xtmph1. Get next dest vector
+
+ ;dest5
+ vpshufb xtmph2, xgft5_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl2, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph2, xtmph2, xtmpl2 ;GF add high and low partials
+ vpxor xd2, xd2, xtmph2
+
+ ;dest6
+ vpshufb xtmph3, xgft6_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl3, xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph3, xtmph3, xtmpl3 ;GF add high and low partials
+ vpxor xd3, xd3, xtmph3
+
+ XSTR [dest4+pos], xd1 ;Store result into dest4
+ XSTR [dest5+pos], xd2 ;Store result into dest5
+ XSTR [dest6+pos], xd3 ;Store result into dest6
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ ;; Overlapped offset length-16
+ mov tmp, len ;Backup len as len=rdi
+
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest4+tmp] ;Get next dest vector
+ XLDR xd2, [dest5+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest6+tmp] ;reuse xtmph1. Get next dest vector
+
+ sub len, pos
+
+ vmovdqa xtmph3, [constip16] ;Load const of i + 16
+ vpinsrb xtmpl3, len.w, 15
+ vpshufb xtmpl3, xmask0f ;Broadcast len to all bytes
+ vpcmpgtb xtmpl3, xtmpl3, xtmph3
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ ;dest4
+ vpshufb xgft4_hi, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpand xgft4_hi, xgft4_hi, xtmpl3
+ vpxor xd1, xd1, xgft4_hi
+
+ ;dest5
+ vpshufb xgft5_hi, xgft5_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft5_lo, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft5_hi, xgft5_hi, xgft5_lo ;GF add high and low partials
+ vpand xgft5_hi, xgft5_hi, xtmpl3
+ vpxor xd2, xd2, xgft5_hi
+
+ ;dest6
+ vpshufb xgft6_hi, xgft6_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft6_lo, xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft6_hi, xgft6_hi, xgft6_lo ;GF add high and low partials
+ vpand xgft6_hi, xgft6_hi, xtmpl3
+ vpxor xd3, xd3, xgft6_hi
+
+ XSTR [dest4+tmp], xd1 ;Store result into dest4
+ XSTR [dest5+tmp], xd2 ;Store result into dest5
+ XSTR [dest6+tmp], xd3 ;Store result into dest6
+
+ vmovdqu xgft4_lo, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ vmovdqu xgft4_hi, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ vmovdqu xgft5_lo, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ vmovdqu xgft5_hi, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ vmovdqu xgft6_lo, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xgft6_hi, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+tmp] ;reuse xtmph1. Get next dest3 vector
+
+ ;dest1
+ vpshufb xgft4_hi, xgft4_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft4_hi, xgft4_hi, xgft4_lo ;GF add high and low partials
+ vpand xgft4_hi, xgft4_hi, xtmpl3
+ vpxor xd1, xd1, xgft4_hi
+
+ ;dest2
+ vpshufb xgft5_hi, xgft5_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft5_lo, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft5_hi, xgft5_hi, xgft5_lo ;GF add high and low partials
+ vpand xgft5_hi, xgft5_hi, xtmpl3
+ vpxor xd2, xd2, xgft5_hi
+
+ ;dest3
+ vpshufb xgft6_hi, xgft6_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft6_lo, xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft6_hi, xgft6_hi, xgft6_lo ;GF add high and low partials
+ vpand xgft6_hi, xgft6_hi, xtmpl3
+ vpxor xd3, xd3, xgft6_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result into dest1
+ XSTR [dest2+tmp], xd2 ;Store result into dest2
+ XSTR [dest3+tmp], xd3 ;Store result into dest3
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_6vect_mad_avx, 02, 01, 0210
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx2.asm
new file mode 100644
index 0000000000..2b6babcba5
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_avx2.asm
@@ -0,0 +1,405 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r13
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r15, 10*16 + 2*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r15, [rsp + 10*16 + 2*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r12
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_6vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 tmp3
+%define dest3 tmp2
+%define dest4 mul_array
+%define dest5 vec
+%define dest6 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm15
+%define xmask0fx xmm15
+%define xgft1_lo ymm14
+%define xgft2_lo ymm13
+%define xgft3_lo ymm12
+%define xgft4_lo ymm11
+%define xgft5_lo ymm10
+%define xgft6_lo ymm9
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmpl ymm2
+%define xtmplx xmm2
+%define xtmph ymm3
+%define xtmphx xmm3
+%define xd1 ymm4
+%define xd2 ymm5
+%define xd3 ymm6
+%define xd4 ymm7
+%define xd5 ymm8
+%define xd6 xd1
+
+align 16
+global gf_6vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ sal vec, 5 ;Multiply by 32
+ lea tmp, [mul_array + vec_i]
+ mov vec_i, vec
+ mov mul_array, vec
+ sal vec_i, 1
+ sal mul_array, 1
+ add vec_i, vec ;vec_i=vec*96
+ add mul_array, vec_i ;vec_i=vec*160
+
+ vmovdqu xgft1_lo, [tmp] ;Load array Ax{00}, Ax{01}, ..., Ax{0f}
+ ; " Ax{00}, Ax{10}, ..., Ax{f0}
+ vmovdqu xgft2_lo, [tmp+vec] ;Load array Bx{00}, Bx{01}, ..., Bx{0f}
+ ; " Bx{00}, Bx{10}, ..., Bx{f0}
+ vmovdqu xgft3_lo, [tmp+2*vec] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ vmovdqu xgft4_lo, [tmp+vec_i] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ ; " Fx{00}, Fx{10}, ..., Fx{f0}
+ vmovdqu xgft5_lo, [tmp+4*vec] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ vmovdqu xgft6_lo, [tmp+mul_array] ;Load array Dx{00}, Dx{01}, ..., Dx{0f}
+ ; " Dx{00}, Dx{10}, ..., Dx{f0}
+
+ mov dest2, [dest1+PS] ; reuse tmp3
+ mov dest3, [dest1+2*PS] ; reuse tmp2
+ mov dest4, [dest1+3*PS] ; reuse mul_array
+ mov dest5, [dest1+4*PS] ; reuse vec
+ mov dest6, [dest1+5*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+ XLDR xd2, [dest2+pos] ;Get next dest vector
+ XLDR xd3, [dest3+pos] ;Get next dest vector
+ XLDR xd4, [dest4+pos] ;Get next dest vector
+ XLDR xd5, [dest5+pos] ;Get next dest vector
+
+ vpand xtmpl, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xtmpl, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmpl, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ ;dest1
+ vperm2i128 xtmph, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd1, xd1, xtmph ;xd1 += partial
+
+ XSTR [dest1+pos], xd1 ;Store result into dest1
+
+ ;dest2
+ vperm2i128 xtmph, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd2, xd2, xtmph ;xd2 += partial
+
+ ;dest3
+ vperm2i128 xtmph, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd3, xd3, xtmph ;xd3 += partial
+
+ XLDR xd6, [dest6+pos] ;reuse xd1. Get next dest vector
+
+ ;dest4
+ vperm2i128 xtmph, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd4, xd4, xtmph ;xd4 += partial
+
+ ;dest5
+ vperm2i128 xtmph, xgft5_lo, xgft5_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd5, xd5, xtmph ;xd5 += partial
+
+ ;dest6
+ vperm2i128 xtmph, xgft6_lo, xgft6_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd6, xd6, xtmph ;xd6 += partial
+
+ XSTR [dest2+pos], xd2 ;Store result into dest2
+ XSTR [dest3+pos], xd3 ;Store result into dest3
+ XSTR [dest4+pos], xd4 ;Store result into dest4
+ XSTR [dest5+pos], xd5 ;Store result into dest5
+ XSTR [dest6+pos], xd6 ;Store result into dest6
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan32:
+ ;; Tail len
+ ;; Do one more overlap pass
+ mov tmp.b, 0x1f
+ vpinsrb xtmphx, xtmphx, tmp.w, 0
+ vpbroadcastb xtmph, xtmphx ;Construct mask 0x1f1f1f...
+
+ mov tmp, len ;Overlapped offset length-32
+
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;Get next dest vector
+ XLDR xd3, [dest3+tmp] ;Get next dest vector
+ XLDR xd4, [dest4+tmp] ;Get next dest vector
+ XLDR xd5, [dest5+tmp] ;Get next dest vector
+
+ sub len, pos
+
+ vpinsrb xtmplx, xtmplx, len.w, 15
+ vinserti128 xtmpl, xtmpl, xtmplx, 1 ;swapped to xtmplx | xtmplx
+ vpshufb xtmpl, xtmpl, xtmph ;Broadcast len to all bytes. xtmph=0x1f1f1f...
+ vpcmpgtb xtmpl, xtmpl, [constip32]
+
+ vpand xtmph, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vperm2i128 xtmpa, xtmph, x0, 0x30 ;swap xtmpa from 1lo|2lo to 1lo|2hi
+ vperm2i128 x0, xtmph, x0, 0x12 ;swap x0 from 1hi|2hi to 1hi|2lo
+
+ ;dest1
+ vperm2i128 xtmph, xgft1_lo, xgft1_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft1_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd1, xd1, xtmph ;xd1 += partial
+
+ XSTR [dest1+tmp], xd1 ;Store result into dest1
+
+ ;dest2
+ vperm2i128 xtmph, xgft2_lo, xgft2_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft2_lo, xgft2_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft2_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd2, xd2, xtmph ;xd2 += partial
+
+ ;dest3
+ vperm2i128 xtmph, xgft3_lo, xgft3_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft3_lo, xgft3_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft3_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd3, xd3, xtmph ;xd3 += partial
+
+ XLDR xd6, [dest6+tmp] ;reuse xd1. Get next dest vector
+
+ ;dest4
+ vperm2i128 xtmph, xgft4_lo, xgft4_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft4_lo, xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft4_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd4, xd4, xtmph ;xd4 += partial
+
+ ;dest5
+ vperm2i128 xtmph, xgft5_lo, xgft5_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft5_lo, xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft5_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd5, xd5, xtmph ;xd5 += partial
+
+ ;dest6
+ vperm2i128 xtmph, xgft6_lo, xgft6_lo, 0x01 ; swapped to hi | lo
+ vpshufb xtmph, xtmph, x0 ;Lookup mul table of high nibble
+ vpshufb xgft6_lo, xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xgft6_lo ;GF add high and low partials
+ vpand xtmph, xtmph, xtmpl
+ vpxor xd6, xd6, xtmph ;xd6 += partial
+
+ XSTR [dest2+tmp], xd2 ;Store result into dest2
+ XSTR [dest3+tmp], xd3 ;Store result into dest3
+ XSTR [dest4+tmp], xd4 ;Store result into dest4
+ XSTR [dest5+tmp], xd5 ;Store result into dest5
+ XSTR [dest6+tmp], xd6 ;Store result into dest6
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+align 32
+constip32:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+ dq 0xe8e9eaebecedeeef, 0xe0e1e2e3e4e5e6e7
+
+;;; func core, ver, snum
+slversion gf_6vect_mad_avx2, 04, 01, 0211
diff --git a/contrib/libs/isa-l/erasure_code/gf_6vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_sse.asm
new file mode 100644
index 0000000000..8e0fc0e0a4
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_6vect_mad_sse.asm
@@ -0,0 +1,411 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_6vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%define PS 8
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp2 r10
+ %define tmp3 r13
+ %define tmp4 r14
+ %define tmp5 rdi
+ %define return rax
+ %define return.w eax
+ %define stack_size 16*10 + 5*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ movdqa [rsp+16*3],xmm9
+ movdqa [rsp+16*4],xmm10
+ movdqa [rsp+16*5],xmm11
+ movdqa [rsp+16*6],xmm12
+ movdqa [rsp+16*7],xmm13
+ movdqa [rsp+16*8],xmm14
+ movdqa [rsp+16*9],xmm15
+ save_reg r12, 10*16 + 0*8
+ save_reg r13, 10*16 + 1*8
+ save_reg r14, 10*16 + 2*8
+ save_reg r15, 10*16 + 3*8
+ save_reg rdi, 10*16 + 4*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ movdqa xmm9, [rsp+16*3]
+ movdqa xmm10, [rsp+16*4]
+ movdqa xmm11, [rsp+16*5]
+ movdqa xmm12, [rsp+16*6]
+ movdqa xmm13, [rsp+16*7]
+ movdqa xmm14, [rsp+16*8]
+ movdqa xmm15, [rsp+16*9]
+ mov r12, [rsp + 10*16 + 0*8]
+ mov r13, [rsp + 10*16 + 1*8]
+ mov r14, [rsp + 10*16 + 2*8]
+ mov r15, [rsp + 10*16 + 3*8]
+ mov rdi, [rsp + 10*16 + 4*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp2 r10
+ %define tmp3 r12
+ %define tmp4 r13
+ %define tmp5 r14
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %macro FUNC_SAVE 0
+ push r12
+ push r13
+ push r14
+ %endmacro
+ %macro FUNC_RESTORE 0
+ pop r14
+ pop r13
+ pop r12
+ %endmacro
+%endif
+
+;;; gf_6vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest1 arg5
+%define pos return
+%define pos.w return.w
+
+%define dest2 mul_array
+%define dest3 tmp2
+%define dest4 tmp4
+%define dest5 tmp5
+%define dest6 vec_i
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft4_lo xmm14
+%define xgft4_hi xmm13
+%define xgft5_lo xmm12
+%define xgft5_hi xmm11
+%define xgft6_lo xmm10
+%define xgft6_hi xmm9
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph1 xmm2
+%define xtmpl1 xmm3
+%define xtmph2 xmm4
+%define xtmpl2 xmm5
+%define xtmph3 xmm6
+%define xtmpl3 xmm7
+%define xd1 xmm8
+%define xd2 xtmpl1
+%define xd3 xtmph1
+
+
+align 16
+global gf_6vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_6vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_6vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_6vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ mov tmp, vec
+ sal vec_i, 5 ;Multiply by 32
+ lea tmp3, [mul_array + vec_i]
+ sal tmp, 6 ;Multiply by 64
+
+ sal vec, 5 ;Multiply by 32
+ lea vec_i, [tmp + vec] ;vec_i = 96
+ lea mul_array, [tmp + vec_i] ;mul_array = 160
+
+ movdqu xgft5_lo, [tmp3+2*tmp] ;Load array Ex{00}, Ex{01}, ..., Ex{0f}
+ movdqu xgft5_hi, [tmp3+2*tmp+16] ; " Ex{00}, Ex{10}, ..., Ex{f0}
+ movdqu xgft4_lo, [tmp3+vec_i] ;Load array Dx{00}, Dx{01}, Dx{02}, ...
+ movdqu xgft4_hi, [tmp3+vec_i+16] ; " Dx{00}, Dx{10}, Dx{20}, ... , Dx{f0}
+ movdqu xgft6_lo, [tmp3+mul_array] ;Load array Fx{00}, Fx{01}, ..., Fx{0f}
+ movdqu xgft6_hi, [tmp3+mul_array+16] ; " Fx{00}, Fx{10}, ..., Fx{f0}
+
+ mov dest2, [dest1+PS]
+ mov dest3, [dest1+2*PS]
+ mov dest4, [dest1+3*PS] ; reuse mul_array
+ mov dest5, [dest1+4*PS]
+ mov dest6, [dest1+5*PS] ; reuse vec_i
+ mov dest1, [dest1]
+
+.loop16:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ movdqu xtmpl1, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xtmph1, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xtmpl2, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xtmph2, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xtmpl3, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xtmph3, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ XLDR xd1, [dest1+pos] ;Get next dest vector
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ;dest1
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ XLDR xd2, [dest2+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+pos] ;reuse xtmph1. Get next dest3 vector
+
+ ;dest2
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ ;dest3
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pxor xd3, xtmph3
+
+ XSTR [dest1+pos], xd1 ;Store result into dest1
+ XSTR [dest2+pos], xd2 ;Store result into dest2
+ XSTR [dest3+pos], xd3 ;Store result into dest3
+
+ movdqa xtmph1, xgft4_hi ;Reload const array registers
+ movdqa xtmpl1, xgft4_lo ;Reload const array registers
+ movdqa xtmph2, xgft5_hi ;Reload const array registers
+ movdqa xtmpl2, xgft5_lo ;Reload const array registers
+ movdqa xtmph3, xgft6_hi ;Reload const array registers
+ movdqa xtmpl3, xgft6_lo ;Reload const array registers
+
+ ;dest4
+ XLDR xd1, [dest4+pos] ;Get next dest vector
+ pshufb xtmph1, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl1, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph1, xtmpl1 ;GF add high and low partials
+ pxor xd1, xtmph1
+
+ XLDR xd2, [dest5+pos] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest6+pos] ;reuse xtmph1. Get next dest vector
+
+ ;dest5
+ pshufb xtmph2, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl2, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph2, xtmpl2 ;GF add high and low partials
+ pxor xd2, xtmph2
+
+ ;dest6
+ pshufb xtmph3, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl3, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph3, xtmpl3 ;GF add high and low partials
+ pxor xd3, xtmph3
+
+ XSTR [dest4+pos], xd1 ;Store result into dest4
+ XSTR [dest5+pos], xd2 ;Store result into dest5
+ XSTR [dest6+pos], xd3 ;Store result into dest6
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+.lessthan16:
+ ;; Tail len
+ ;; Do one more overlap pass
+ ;; Overlapped offset length-16
+ mov tmp, len ;Backup len as len=rdi
+
+ XLDR x0, [src+tmp] ;Get next source vector
+ XLDR xd1, [dest4+tmp] ;Get next dest vector
+ XLDR xd2, [dest5+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest6+tmp] ;reuse xtmph1. Get next dest vector
+
+ sub len, pos
+
+ movdqa xtmph3, [constip16] ;Load const of i + 16
+ pinsrb xtmpl3, len.w, 15
+ pshufb xtmpl3, xmask0f ;Broadcast len to all bytes
+ pcmpgtb xtmpl3, xtmph3
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ ;dest4
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pand xgft4_hi, xtmpl3
+ pxor xd1, xgft4_hi
+
+ ;dest5
+ pshufb xgft5_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft5_hi, xgft5_lo ;GF add high and low partials
+ pand xgft5_hi, xtmpl3
+ pxor xd2, xgft5_hi
+
+ ;dest6
+ pshufb xgft6_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft6_hi, xgft6_lo ;GF add high and low partials
+ pand xgft6_hi, xtmpl3
+ pxor xd3, xgft6_hi
+
+ XSTR [dest4+tmp], xd1 ;Store result into dest4
+ XSTR [dest5+tmp], xd2 ;Store result into dest5
+ XSTR [dest6+tmp], xd3 ;Store result into dest6
+
+ movdqu xgft4_lo, [tmp3] ;Load array Ax{00}, Ax{01}, Ax{02}, ...
+ movdqu xgft4_hi, [tmp3+16] ; " Ax{00}, Ax{10}, Ax{20}, ... , Ax{f0}
+ movdqu xgft5_lo, [tmp3+vec] ;Load array Bx{00}, Bx{01}, Bx{02}, ...
+ movdqu xgft5_hi, [tmp3+vec+16] ; " Bx{00}, Bx{10}, Bx{20}, ... , Bx{f0}
+ movdqu xgft6_lo, [tmp3+2*vec] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xgft6_hi, [tmp3+2*vec+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ XLDR xd1, [dest1+tmp] ;Get next dest vector
+ XLDR xd2, [dest2+tmp] ;reuse xtmpl1. Get next dest vector
+ XLDR xd3, [dest3+tmp] ;reuse xtmph1. Get next dest3 vector
+
+ ;dest1
+ pshufb xgft4_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft4_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft4_hi, xgft4_lo ;GF add high and low partials
+ pand xgft4_hi, xtmpl3
+ pxor xd1, xgft4_hi
+
+ ;dest2
+ pshufb xgft5_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft5_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft5_hi, xgft5_lo ;GF add high and low partials
+ pand xgft5_hi, xtmpl3
+ pxor xd2, xgft5_hi
+
+ ;dest3
+ pshufb xgft6_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft6_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft6_hi, xgft6_lo ;GF add high and low partials
+ pand xgft6_hi, xtmpl3
+ pxor xd3, xgft6_hi
+
+ XSTR [dest1+tmp], xd1 ;Store result into dest1
+ XSTR [dest2+tmp], xd2 ;Store result into dest2
+ XSTR [dest3+tmp], xd3 ;Store result into dest3
+
+.return_pass:
+ FUNC_RESTORE
+ mov return, 0
+ ret
+
+.return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+constip16:
+ dq 0xf8f9fafbfcfdfeff, 0xf0f1f2f3f4f5f6f7
+
+;;; func core, ver, snum
+slversion gf_6vect_mad_sse, 00, 01, 020f
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx.asm b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx.asm
new file mode 100644
index 0000000000..dc1eebb972
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx.asm
@@ -0,0 +1,276 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_dot_prod_avx(len, vec, *g_tbls, **buffs, *dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r9
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved and loaded
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 rdi ; must be saved and loaded
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define frame_size 2*8
+ %define arg(x) [rsp + frame_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ rex_push_reg r12
+ push_reg rdi
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop rdi
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+
+ %define trans ecx ;trans is for the variables in stack
+ %define arg0 trans
+ %define arg0_m arg(0)
+ %define arg1 trans
+ %define arg1_m arg(1)
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 ebx
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 esi
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ push esi
+ push edi
+ push ebx
+ mov arg3, arg(3)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ mov esp, ebp
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define vec_m arg1_m
+ %define len_m arg0_m
+ %define dest_m arg4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ; 64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%define xmask0f xmm5
+%define xgft_lo xmm4
+%define xgft_hi xmm3
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp xmm2
+
+align 16
+global gf_vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_dot_prod_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_dot_prod_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_dot_prod_avx)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+.loop16:
+ vpxor xp, xp
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+
+ mov ptr, [src+vec_i*PS]
+ vmovdqu xgft_lo, [tmp] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ vmovdqu xgft_hi, [tmp+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ add tmp, 32
+ add vec_i, 1
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft_hi, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft_lo, xgft_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft_hi, xgft_hi, xgft_lo ;GF add high and low partials
+ vpxor xp, xp, xgft_hi ;xp += partial
+
+ SLDR vec, vec_m
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest, dest_m
+ XSTR [dest+pos], xp
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ SLDR len, len_m
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_dot_prod_avx, 02, 05, 0061
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx2.asm
new file mode 100644
index 0000000000..986fd28a4e
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx2.asm
@@ -0,0 +1,285 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_dot_prod_avx2(len, vec, *g_tbls, **buffs, *dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 r9
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved and loaded
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define tmp2 r10
+ %define tmp3 rdi ; must be saved and loaded
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define frame_size 2*8
+ %define arg(x) [rsp + frame_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ rex_push_reg r12
+ push_reg rdi
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop rdi
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+
+ %define trans ecx ;trans is for the variables in stack
+ %define arg0 trans
+ %define arg0_m arg(0)
+ %define arg1 trans
+ %define arg1_m arg(1)
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 ebx
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp.w edx
+ %define tmp.b dl
+ %define tmp2 edi
+ %define tmp3 esi
+ %define return eax
+ %macro SLDR 2 ;stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ push esi
+ push edi
+ push ebx
+ mov arg3, arg(3)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ mov esp, ebp
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define pos return
+
+%ifidn PS,4 ;32-bit code
+ %define vec_m arg1_m
+ %define len_m arg0_m
+ %define dest_m arg4_m
+%endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%define xmask0f ymm3
+%define xmask0fx xmm3
+%define xgft_lo ymm4
+%define xgft_hi ymm5
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xp ymm2
+
+align 16
+global gf_vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_dot_prod_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_dot_prod_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_dot_prod_avx2)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 32
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+.loop32:
+ vpxor xp, xp
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+
+ mov ptr, [src+vec_i*PS]
+
+ vmovdqu xgft_lo, [tmp] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ vperm2i128 xgft_hi, xgft_lo, xgft_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft_lo, xgft_lo, xgft_lo, 0x00 ; swapped to lo | lo
+
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ add tmp, 32
+ add vec_i, 1
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xgft_hi, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft_lo, xgft_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xgft_hi, xgft_hi, xgft_lo ;GF add high and low partials
+ vpxor xp, xp, xgft_hi ;xp += partial
+
+ SLDR vec, vec_m
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest, dest_m
+ XSTR [dest+pos], xp
+
+ add pos, 32 ;Loop on 32 bytes at a time
+ SLDR len, len_m
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-32
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_vect_dot_prod_avx2, 04, 05, 0190
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx512.asm
new file mode 100644
index 0000000000..405c1e48e2
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_avx512.asm
@@ -0,0 +1,245 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_dot_prod_avx512(len, vec, *g_tbls, **buffs, *dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved, loaded and restored
+ %define arg5 r15 ; must be saved and restored
+ %define tmp r11
+ %define tmp2 r10
+ %define return rax
+ %define PS 8
+ %define LOG_PS 3
+ %define stack_size 0*16 + 3*8 ; must be an odd multiple of 8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_reg r12, 9*16 + 0*8
+ save_reg r15, 9*16 + 3*8
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ mov r12, [rsp + 9*16 + 0*8]
+ mov r15, [rsp + 9*16 + 3*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest1 arg4
+%define ptr arg5
+%define vec_i tmp2
+%define pos return
+
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+%define xmask0f zmm5
+%define xgft1_lo zmm4
+%define xgft1_loy ymm4
+%define xgft1_hi zmm3
+%define x0 zmm0
+%define xgft1_loy ymm4
+%define x0y ymm0
+%define xtmpa zmm1
+%define xp1 zmm2
+%define xp1y ymm2
+
+default rel
+[bits 64]
+section .text
+
+align 16
+global gf_vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_dot_prod_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_dot_prod_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_dot_prod_avx512)
+%endif
+
+ FUNC_SAVE
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec, LOG_PS ;vec *= PS. Make vec_i count by PS
+ sub len, 64
+ jl .len_lt_64
+
+.loop64:
+ vpxorq xp1, xp1, xp1
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+ mov ptr, [src+vec_i]
+ XLDR x0, [ptr+pos] ;Get next source vector
+ add vec_i, PS
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vmovdqu8 xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ add tmp, 32
+
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x55
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x00
+
+ vpshufb xgft1_hi, xgft1_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xgft1_lo, xgft1_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxorq xp1, xp1, xgft1_hi ;xp1 += partial
+
+ cmp vec_i, vec
+ jl .next_vect
+
+ XSTR [dest1+pos], xp1
+
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+
+.len_lt_64: ; 32-byte version
+ add len, 32
+ jl .return_fail
+
+.loop32:
+ vpxorq xp1, xp1, xp1
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect2:
+ mov ptr, [src+vec_i]
+ XLDR x0y, [ptr+pos] ;Get next source vector 32B
+ add vec_i, PS
+ vpsraw xtmpa, x0, 4 ;Shift to put high nibble into bits 4-0
+ vshufi64x2 x0, x0, xtmpa, 0x44 ;put x0 = xl:xh
+ vpandq x0, x0, xmask0f ;Mask bits 4-0
+ vmovdqu8 xgft1_loy, [tmp] ;Load array Ax{00}..{0f}, Ax{00}..{f0}
+ add tmp, 32
+ vshufi64x2 xgft1_lo, xgft1_lo, xgft1_lo, 0x50 ;=AlAh:AlAh
+ vpshufb xgft1_lo, xgft1_lo, x0 ;Lookup mul table
+ vshufi64x2 xgft1_hi, xgft1_lo, xgft1_lo, 0x0e ;=xh:
+ vpxorq xgft1_hi, xgft1_hi, xgft1_lo ;GF add high and low partials
+ vpxorq xp1, xp1, xgft1_hi ;xp1 += partial
+ cmp vec_i, vec
+ jl .next_vect2
+
+ XSTR [dest1+pos], xp1y
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-32
+ jmp .loop32 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_vect_dot_prod_avx512
+no_gf_vect_dot_prod_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_sse.asm b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_sse.asm
new file mode 100644
index 0000000000..67f4a1a329
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_dot_prod_sse.asm
@@ -0,0 +1,276 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_dot_prod_sse(len, vec, *g_tbls, **buffs, *dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 r9
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+
+ %define arg4 r12 ; must be saved and loaded
+ %define tmp r11
+ %define tmp2 r10
+ %define tmp3 rdi ; must be saved and loaded
+ %define return rax
+ %macro SLDR 2
+ %endmacro
+ %define SSTR SLDR
+ %define PS 8
+ %define frame_size 2*8
+ %define arg(x) [rsp + frame_size + PS + PS*x]
+
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ rex_push_reg r12
+ push_reg rdi
+ end_prolog
+ mov arg4, arg(4)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop rdi
+ pop r12
+ %endmacro
+%endif
+
+%ifidn __OUTPUT_FORMAT__, elf32
+
+;;;================== High Address;
+;;; arg4
+;;; arg3
+;;; arg2
+;;; arg1
+;;; arg0
+;;; return
+;;;<================= esp of caller
+;;; ebp
+;;;<================= ebp = esp
+;;; esi
+;;; edi
+;;; ebx
+;;;<================= esp of callee
+;;;
+;;;================== Low Address;
+
+ %define PS 4
+ %define LOG_PS 2
+ %define func(x) x:
+ %define arg(x) [ebp + PS*2 + PS*x]
+
+ %define trans ecx ;trans is for the variables in stack
+ %define arg0 trans
+ %define arg0_m arg(0)
+ %define arg1 trans
+ %define arg1_m arg(1)
+ %define arg2 arg2_m
+ %define arg2_m arg(2)
+ %define arg3 ebx
+ %define arg4 trans
+ %define arg4_m arg(4)
+ %define tmp edx
+ %define tmp2 edi
+ %define tmp3 esi
+ %define return eax
+ %macro SLDR 2 ;; stack load/restore
+ mov %1, %2
+ %endmacro
+ %define SSTR SLDR
+
+ %macro FUNC_SAVE 0
+ push ebp
+ mov ebp, esp
+ push esi
+ push edi
+ push ebx
+ mov arg3, arg(3)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ pop ebx
+ pop edi
+ pop esi
+ mov esp, ebp
+ pop ebp
+ %endmacro
+
+%endif ; output formats
+
+%define len arg0
+%define vec arg1
+%define mul_array arg2
+%define src arg3
+%define dest arg4
+
+%define vec_i tmp2
+%define ptr tmp3
+%define pos return
+
+ %ifidn PS,4 ;32-bit code
+ %define vec_m arg1_m
+ %define len_m arg0_m
+ %define dest_m arg4_m
+ %endif
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+%ifidn PS,8 ;64-bit code
+ default rel
+ [bits 64]
+%endif
+
+section .text
+
+%define xmask0f xmm5
+%define xgft_lo xmm4
+%define xgft_hi xmm3
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xp xmm2
+
+align 16
+global gf_vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_dot_prod_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_dot_prod_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_dot_prod_sse)
+%endif
+
+ FUNC_SAVE
+ SLDR len, len_m
+ sub len, 16
+ SSTR len_m, len
+ jl .return_fail
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+.loop16:
+ pxor xp, xp
+ mov tmp, mul_array
+ xor vec_i, vec_i
+
+.next_vect:
+
+ mov ptr, [src+vec_i*PS]
+ movdqu xgft_lo, [tmp] ;Load array Cx{00}, Cx{01}, ..., Cx{0f}
+ movdqu xgft_hi, [tmp+16] ; " Cx{00}, Cx{10}, ..., Cx{f0}
+ XLDR x0, [ptr+pos] ;Get next source vector
+
+ add tmp, 32
+ add vec_i, 1
+
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+
+ pshufb xgft_hi, x0 ;Lookup mul table of high nibble
+ pshufb xgft_lo, xtmpa ;Lookup mul table of low nibble
+ pxor xgft_hi, xgft_lo ;GF add high and low partials
+ pxor xp, xgft_hi ;xp += partial
+
+ SLDR vec, vec_m
+ cmp vec_i, vec
+ jl .next_vect
+
+ SLDR dest, dest_m
+ XSTR [dest+pos], xp
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ SLDR len, len_m
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ jmp .loop16 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_dot_prod_sse, 00, 05, 0060
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx.asm
new file mode 100644
index 0000000000..1a252c474f
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx.asm
@@ -0,0 +1,201 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mad_avx(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+ %define PS 8
+ %define stack_size 16*3 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ save_reg r12, 3*16 + 0*8
+ save_reg r15, 3*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r15, [rsp + 3*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_vect_mad_avx(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest arg5
+%define pos return
+%define pos.w return.w
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm8
+%define xgft_lo xmm7
+%define xgft_hi xmm6
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph xmm2
+%define xtmpl xmm3
+%define xd xmm4
+%define xtmpd xmm5
+
+align 16
+global gf_vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mad_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mad_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mad_avx)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+
+ xor pos, pos
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+
+ sal vec_i, 5 ;Multiply by 32
+ vmovdqu xgft_lo, [vec_i+mul_array] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xgft_hi, [vec_i+mul_array+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+
+ XLDR xtmpd, [dest+len] ;backup the last 16 bytes in dest
+
+.loop16:
+ XLDR xd, [dest+pos] ;Get next dest vector
+.loop16_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd, xd, xtmph ;xd += partial
+
+ XSTR [dest+pos], xd
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ vmovdqa xd, xtmpd ;Restore xd
+ jmp .loop16_overlap ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_mad_avx, 02, 01, 0201
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx2.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx2.asm
new file mode 100644
index 0000000000..9b24c6e62a
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx2.asm
@@ -0,0 +1,208 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mad_avx2(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12 ; must be saved and loaded
+ %define arg5 r15
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+ %define PS 8
+ %define stack_size 16*3 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ save_reg r12, 3*16 + 0*8
+ save_reg r15, 3*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r15, [rsp + 3*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+
+ %define tmp r11
+ %define tmp.w r11d
+ %define tmp.b r11b
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+
+;;; gf_vect_mad_avx2(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest arg5
+%define pos return
+%define pos.w return.w
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu
+ %define XSTR vmovdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f ymm8
+%define xmask0fx xmm8
+%define xgft_lo ymm7
+%define xgft_hi ymm6
+
+%define x0 ymm0
+%define xtmpa ymm1
+%define xtmph ymm2
+%define xtmpl ymm3
+%define xd ymm4
+%define xtmpd ymm5
+
+align 16
+global gf_vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mad_avx2)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mad_avx2:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mad_avx2)
+%endif
+
+ FUNC_SAVE
+ sub len, 32
+ jl .return_fail
+ xor pos, pos
+ mov tmp.b, 0x0f
+ vpinsrb xmask0fx, xmask0fx, tmp.w, 0
+ vpbroadcastb xmask0f, xmask0fx ;Construct mask 0x0f0f0f...
+
+ sal vec_i, 5 ;Multiply by 32
+ vmovdqu xgft_lo, [vec_i+mul_array] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+ vperm2i128 xgft_hi, xgft_lo, xgft_lo, 0x11 ; swapped to hi | hi
+ vperm2i128 xgft_lo, xgft_lo, xgft_lo, 0x00 ; swapped to lo | lo
+
+ XLDR xtmpd, [dest+len] ;backup the last 32 bytes in dest
+
+.loop32:
+ XLDR xd, [dest+pos] ;Get next dest vector
+.loop32_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpand xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl, xgft_lo, xtmpa ;Lookup mul table of low nibble
+ vpxor xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxor xd, xd, xtmph ;xd += partial
+
+ XSTR [dest+pos], xd
+ add pos, 32 ;Loop on 32 bytes at a time
+ cmp pos, len
+ jle .loop32
+
+ lea tmp, [len + 32]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-32
+ vmovdqa xd, xtmpd ;Restore xd
+ jmp .loop32_overlap ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+;;; func core, ver, snum
+slversion gf_vect_mad_avx2, 04, 01, 0202
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx512.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx512.asm
new file mode 100644
index 0000000000..adc2acf3e8
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mad_avx512.asm
@@ -0,0 +1,198 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mad_avx512(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifdef HAVE_AS_KNOWS_AVX512
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12 ; must be saved and loaded
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define PS 8
+ %define stack_size 16*3 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+ %macro FUNC_SAVE 0
+ sub rsp, stack_size
+ vmovdqa [rsp+16*0],xmm6
+ vmovdqa [rsp+16*1],xmm7
+ vmovdqa [rsp+16*2],xmm8
+ save_reg r12, 3*16 + 0*8
+ save_reg r15, 3*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp+16*0]
+ vmovdqa xmm7, [rsp+16*1]
+ vmovdqa xmm8, [rsp+16*2]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r15, [rsp + 3*16 + 1*8]
+ add rsp, stack_size
+ %endmacro
+%endif
+
+;;; gf_vect_mad_avx512(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest arg5
+%define pos return
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR vmovdqu8
+ %define XSTR vmovdqu8
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+ %else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+ %endif
+%endif
+
+
+default rel
+
+[bits 64]
+section .text
+
+%define x0 zmm0
+%define xtmpa zmm1
+%define xtmph zmm2
+%define xtmpl zmm3
+%define xd zmm4
+%define xtmpd zmm5
+%define xgft_hi zmm6
+%define xgft_lo zmm7
+%define xgft_loy ymm7
+%define xmask0f zmm8
+
+align 16
+global gf_vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mad_avx512)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mad_avx512:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mad_avx512)
+%endif
+
+ FUNC_SAVE
+ sub len, 64
+ jl .return_fail
+ xor pos, pos
+ mov tmp, 0x0f
+ vpbroadcastb xmask0f, tmp ;Construct mask 0x0f0f0f...
+ sal vec_i, 5 ;Multiply by 32
+ vmovdqu8 xgft_loy, [vec_i+mul_array] ;Load array Cx{00}..{0f}, Cx{00}..{f0}
+ vshufi64x2 xgft_hi, xgft_lo, xgft_lo, 0x55
+ vshufi64x2 xgft_lo, xgft_lo, xgft_lo, 0x00
+ mov tmp, -1
+ kmovq k1, tmp
+
+.loop64:
+ XLDR xd, [dest+pos] ;Get next dest vector
+ XLDR x0, [src+pos] ;Get next source vector
+
+ vpandq xtmpa, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpandq x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+
+ vpshufb xtmph {k1}{z}, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmpl {k1}{z}, xgft_lo, xtmpa ;Lookup mul table of low nibble
+ vpxorq xtmph, xtmph, xtmpl ;GF add high and low partials
+ vpxorq xd, xd, xtmph ;xd += partial
+
+ XSTR [dest+pos], xd
+ add pos, 64 ;Loop on 64 bytes at a time
+ cmp pos, len
+ jle .loop64
+
+ lea tmp, [len + 64]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, (1 << 63)
+ lea tmp, [len + 64 - 1]
+ and tmp, 63
+ sarx pos, pos, tmp
+ kmovq k1, pos
+ mov pos, len ;Overlapped offset length-64
+ jmp .loop64 ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+%else
+%ifidn __OUTPUT_FORMAT__, win64
+global no_gf_vect_mad_avx512
+no_gf_vect_mad_avx512:
+%endif
+%endif ; ifdef HAVE_AS_KNOWS_AVX512
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mad_sse.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mad_sse.asm
new file mode 100644
index 0000000000..ea48612324
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mad_sse.asm
@@ -0,0 +1,202 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mad_sse(len, vec, vec_i, mul_array, src, dest);
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg0.w ecx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define arg4 r12
+ %define arg5 r15
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+ %define PS 8
+ %define stack_size 16*3 + 3*8
+ %define arg(x) [rsp + stack_size + PS + PS*x]
+ %define func(x) proc_frame x
+
+%macro FUNC_SAVE 0
+ sub rsp, stack_size
+ movdqa [rsp+16*0],xmm6
+ movdqa [rsp+16*1],xmm7
+ movdqa [rsp+16*2],xmm8
+ save_reg r12, 3*16 + 0*8
+ save_reg r15, 3*16 + 1*8
+ end_prolog
+ mov arg4, arg(4)
+ mov arg5, arg(5)
+%endmacro
+
+%macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp+16*0]
+ movdqa xmm7, [rsp+16*1]
+ movdqa xmm8, [rsp+16*2]
+ mov r12, [rsp + 3*16 + 0*8]
+ mov r15, [rsp + 3*16 + 1*8]
+ add rsp, stack_size
+%endmacro
+
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg0.w edi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define return.w eax
+
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+%endif
+
+;;; gf_vect_mad_sse(len, vec, vec_i, mul_array, src, dest)
+%define len arg0
+%define len.w arg0.w
+%define vec arg1
+%define vec_i arg2
+%define mul_array arg3
+%define src arg4
+%define dest arg5
+%define pos return
+%define pos.w return.w
+
+%ifndef EC_ALIGNED_ADDR
+;;; Use Un-aligned load/store
+ %define XLDR movdqu
+ %define XSTR movdqu
+%else
+;;; Use Non-temporal load/stor
+ %ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+ %else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+ %endif
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm8
+%define xgft_lo xmm7
+%define xgft_hi xmm6
+
+%define x0 xmm0
+%define xtmpa xmm1
+%define xtmph xmm2
+%define xtmpl xmm3
+%define xd xmm4
+%define xtmpd xmm5
+
+
+align 16
+global gf_vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mad_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mad_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mad_sse)
+%endif
+
+ FUNC_SAVE
+ sub len, 16
+ jl .return_fail
+
+ xor pos, pos
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ sal vec_i, 5 ;Multiply by 32
+ movdqu xgft_lo, [vec_i+mul_array] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xgft_hi, [vec_i+mul_array+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+
+ XLDR xtmpd, [dest+len] ;backup the last 16 bytes in dest
+
+.loop16:
+ XLDR xd, [dest+pos] ;Get next dest vector
+.loop16_overlap:
+ XLDR x0, [src+pos] ;Get next source vector
+ movdqa xtmph, xgft_hi ;Reload const array registers
+ movdqa xtmpl, xgft_lo
+ movdqa xtmpa, x0 ;Keep unshifted copy of src
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand xtmpa, xmask0f ;Mask low src nibble in bits 4-0
+ pshufb xtmph, x0 ;Lookup mul table of high nibble
+ pshufb xtmpl, xtmpa ;Lookup mul table of low nibble
+ pxor xtmph, xtmpl ;GF add high and low partials
+
+ pxor xd, xtmph
+ XSTR [dest+pos], xd ;Store result
+
+ add pos, 16 ;Loop on 16 bytes at a time
+ cmp pos, len
+ jle .loop16
+
+ lea tmp, [len + 16]
+ cmp pos, tmp
+ je .return_pass
+
+ ;; Tail len
+ mov pos, len ;Overlapped offset length-16
+ movdqa xd, xtmpd ;Restore xd
+ jmp .loop16_overlap ;Do one more overlap pass
+
+.return_pass:
+ mov return, 0
+ FUNC_RESTORE
+ ret
+
+.return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f: dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_mad_sse, 00, 01, 0200
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mul_avx.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mul_avx.asm
new file mode 100644
index 0000000000..86121b298a
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mul_avx.asm
@@ -0,0 +1,168 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mul_avx(len, mul_array, src, dest)
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+
+%elifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define return rax
+ %define stack_size 5*16 + 8 ; must be an odd multiple of 8
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm13, 2*16
+ save_xmm128 xmm14, 3*16
+ save_xmm128 xmm15, 4*16
+ end_prolog
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ vmovdqa xmm6, [rsp + 0*16]
+ vmovdqa xmm7, [rsp + 1*16]
+ vmovdqa xmm13, [rsp + 2*16]
+ vmovdqa xmm14, [rsp + 3*16]
+ vmovdqa xmm15, [rsp + 4*16]
+ add rsp, stack_size
+ %endmacro
+
+%endif
+
+
+%define len arg0
+%define mul_array arg1
+%define src arg2
+%define dest arg3
+%define pos return
+
+
+;;; Use Non-temporal load/stor
+%ifdef NO_NT_LDST
+ %define XLDR vmovdqa
+ %define XSTR vmovdqa
+%else
+ %define XLDR vmovntdqa
+ %define XSTR vmovntdq
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft_lo xmm14
+%define xgft_hi xmm13
+
+%define x0 xmm0
+%define xtmp1a xmm1
+%define xtmp1b xmm2
+%define xtmp1c xmm3
+%define x1 xmm4
+%define xtmp2a xmm5
+%define xtmp2b xmm6
+%define xtmp2c xmm7
+
+align 16
+global gf_vect_mul_avx:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mul_avx)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mul_avx:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mul_avx)
+%endif
+ FUNC_SAVE
+ mov pos, 0
+ vmovdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ vmovdqu xgft_lo, [mul_array] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ vmovdqu xgft_hi, [mul_array+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+
+loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR x1, [src+pos+16] ;Get next source vector + 16B ahead
+ add pos, 32 ;Loop on 16 bytes at a time
+ cmp pos, len
+ vpand xtmp1a, x0, xmask0f ;Mask low src nibble in bits 4-0
+ vpand xtmp2a, x1, xmask0f
+ vpsraw x0, x0, 4 ;Shift to put high nibble into bits 4-0
+ vpsraw x1, x1, 4
+ vpand x0, x0, xmask0f ;Mask high src nibble in bits 4-0
+ vpand x1, x1, xmask0f
+ vpshufb xtmp1b, xgft_hi, x0 ;Lookup mul table of high nibble
+ vpshufb xtmp1c, xgft_lo, xtmp1a ;Lookup mul table of low nibble
+ vpshufb xtmp2b, xgft_hi, x1 ;Lookup mul table of high nibble
+ vpshufb xtmp2c, xgft_lo, xtmp2a ;Lookup mul table of low nibble
+ vpxor xtmp1b, xtmp1b, xtmp1c ;GF add high and low partials
+ vpxor xtmp2b, xtmp2b, xtmp2c
+ XSTR [dest+pos-32], xtmp1b ;Store result
+ XSTR [dest+pos-16], xtmp2b ;Store +16B result
+ jl loop32
+
+
+return_pass:
+ FUNC_RESTORE
+ sub pos, len
+ ret
+
+return_fail:
+ FUNC_RESTORE
+ mov return, 1
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+
+mask0f:
+dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_mul_avx, 01, 03, 0036
diff --git a/contrib/libs/isa-l/erasure_code/gf_vect_mul_sse.asm b/contrib/libs/isa-l/erasure_code/gf_vect_mul_sse.asm
new file mode 100644
index 0000000000..01a3269d65
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/gf_vect_mul_sse.asm
@@ -0,0 +1,175 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;
+;;; gf_vect_mul_sse(len, mul_array, src, dest)
+;;;
+
+%include "reg_sizes.asm"
+
+%ifidn __OUTPUT_FORMAT__, elf64
+ %define arg0 rdi
+ %define arg1 rsi
+ %define arg2 rdx
+ %define arg3 rcx
+ %define arg4 r8
+ %define arg5 r9
+ %define tmp r11
+ %define return rax
+ %define func(x) x:
+ %define FUNC_SAVE
+ %define FUNC_RESTORE
+
+%elifidn __OUTPUT_FORMAT__, win64
+ %define arg0 rcx
+ %define arg1 rdx
+ %define arg2 r8
+ %define arg3 r9
+ %define return rax
+ %define stack_size 5*16 + 8 ; must be an odd multiple of 8
+ %define func(x) proc_frame x
+ %macro FUNC_SAVE 0
+ alloc_stack stack_size
+ save_xmm128 xmm6, 0*16
+ save_xmm128 xmm7, 1*16
+ save_xmm128 xmm13, 2*16
+ save_xmm128 xmm14, 3*16
+ save_xmm128 xmm15, 4*16
+ end_prolog
+ %endmacro
+
+ %macro FUNC_RESTORE 0
+ movdqa xmm6, [rsp + 0*16]
+ movdqa xmm7, [rsp + 1*16]
+ movdqa xmm13, [rsp + 2*16]
+ movdqa xmm14, [rsp + 3*16]
+ movdqa xmm15, [rsp + 4*16]
+ add rsp, stack_size
+ %endmacro
+
+%endif
+
+
+%define len arg0
+%define mul_array arg1
+%define src arg2
+%define dest arg3
+%define pos return
+
+
+;;; Use Non-temporal load/stor
+%ifdef NO_NT_LDST
+ %define XLDR movdqa
+ %define XSTR movdqa
+%else
+ %define XLDR movntdqa
+ %define XSTR movntdq
+%endif
+
+default rel
+
+[bits 64]
+section .text
+
+%define xmask0f xmm15
+%define xgft_lo xmm14
+%define xgft_hi xmm13
+
+%define x0 xmm0
+%define xtmp1a xmm1
+%define xtmp1b xmm2
+%define xtmp1c xmm3
+%define x1 xmm4
+%define xtmp2a xmm5
+%define xtmp2b xmm6
+%define xtmp2c xmm7
+
+
+align 16
+global gf_vect_mul_sse:ISAL_SYM_TYPE_FUNCTION
+func(gf_vect_mul_sse)
+%ifidn __OUTPUT_FORMAT__, macho64
+global _gf_vect_mul_sse:ISAL_SYM_TYPE_FUNCTION
+func(_gf_vect_mul_sse)
+%endif
+
+ FUNC_SAVE
+ mov pos, 0
+ movdqa xmask0f, [mask0f] ;Load mask of lower nibble in each byte
+ movdqu xgft_lo, [mul_array] ;Load array Cx{00}, Cx{01}, Cx{02}, ...
+ movdqu xgft_hi, [mul_array+16] ; " Cx{00}, Cx{10}, Cx{20}, ... , Cx{f0}
+
+loop32:
+ XLDR x0, [src+pos] ;Get next source vector
+ XLDR x1, [src+pos+16] ;Get next source vector + 16B ahead
+ movdqa xtmp1b, xgft_hi ;Reload const array registers
+ movdqa xtmp1c, xgft_lo
+ movdqa xtmp2b, xgft_hi
+ movdqa xtmp2c, xgft_lo
+ movdqa xtmp1a, x0 ;Keep unshifted copy of src
+ movdqa xtmp2a, x1
+ psraw x0, 4 ;Shift to put high nibble into bits 4-0
+ psraw x1, 4
+ pand xtmp1a, xmask0f ;Mask low src nibble in bits 4-0
+ pand xtmp2a, xmask0f
+ pand x0, xmask0f ;Mask high src nibble in bits 4-0
+ pand x1, xmask0f
+ pshufb xtmp1b, x0 ;Lookup mul table of high nibble
+ pshufb xtmp1c, xtmp1a ;Lookup mul table of low nibble
+ pshufb xtmp2b, x1
+ pshufb xtmp2c, xtmp2a
+ pxor xtmp1b, xtmp1c ;GF add high and low partials
+ pxor xtmp2b, xtmp2c
+ XSTR [dest+pos], xtmp1b ;Store result
+ XSTR [dest+pos+16], xtmp2b ;Store +16B result
+ add pos, 32 ;Loop on 32 bytes at at time
+ cmp pos, len
+ jl loop32
+
+
+return_pass:
+ sub pos, len
+ FUNC_RESTORE
+ ret
+
+return_fail:
+ mov return, 1
+ FUNC_RESTORE
+ ret
+
+endproc_frame
+
+section .data
+
+align 16
+mask0f:
+dq 0x0f0f0f0f0f0f0f0f, 0x0f0f0f0f0f0f0f0f
+
+;;; func core, ver, snum
+slversion gf_vect_mul_sse, 00, 03, 0034
diff --git a/contrib/libs/isa-l/erasure_code/ya.make b/contrib/libs/isa-l/erasure_code/ya.make
new file mode 100644
index 0000000000..2326f814bb
--- /dev/null
+++ b/contrib/libs/isa-l/erasure_code/ya.make
@@ -0,0 +1,81 @@
+LIBRARY(isa-l_ec)
+
+LICENSE(BSD-3-Clause)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+VERSION(2.28)
+
+NO_UTIL()
+
+NO_COMPILER_WARNINGS()
+
+ADDINCL(
+ contrib/libs/isa-l/include
+ FOR
+ asm
+ contrib/libs/isa-l/include
+)
+
+IF (OS_DARWIN)
+ SRCS(
+ ec_multibinary_darwin.asm
+ )
+ELSE()
+ SRCS(
+ ec_multibinary.asm
+ )
+ENDIF()
+
+SRCS(
+ ec_base.c
+ ec_highlevel_func.c
+ gf_vect_mul_sse.asm
+ gf_vect_mul_avx.asm
+ gf_vect_dot_prod_sse.asm
+ gf_vect_dot_prod_avx.asm
+ gf_vect_dot_prod_avx2.asm
+ gf_vect_dot_prod_avx512.asm
+ gf_2vect_dot_prod_sse.asm
+ gf_2vect_dot_prod_avx.asm
+ gf_2vect_dot_prod_avx2.asm
+ gf_2vect_dot_prod_avx512.asm
+ gf_3vect_dot_prod_sse.asm
+ gf_3vect_dot_prod_avx.asm
+ gf_3vect_dot_prod_avx2.asm
+ gf_3vect_dot_prod_avx512.asm
+ gf_4vect_dot_prod_sse.asm
+ gf_4vect_dot_prod_avx.asm
+ gf_4vect_dot_prod_avx2.asm
+ gf_4vect_dot_prod_avx512.asm
+ gf_5vect_dot_prod_sse.asm
+ gf_5vect_dot_prod_avx.asm
+ gf_5vect_dot_prod_avx2.asm
+ gf_6vect_dot_prod_sse.asm
+ gf_6vect_dot_prod_avx.asm
+ gf_6vect_dot_prod_avx2.asm
+ gf_vect_mad_sse.asm
+ gf_vect_mad_avx.asm
+ gf_vect_mad_avx2.asm
+ gf_vect_mad_avx512.asm
+ gf_2vect_mad_sse.asm
+ gf_2vect_mad_avx.asm
+ gf_2vect_mad_avx2.asm
+ gf_2vect_mad_avx512.asm
+ gf_3vect_mad_sse.asm
+ gf_3vect_mad_avx.asm
+ gf_3vect_mad_avx2.asm
+ gf_3vect_mad_avx512.asm
+ gf_4vect_mad_sse.asm
+ gf_4vect_mad_avx.asm
+ gf_4vect_mad_avx2.asm
+ gf_4vect_mad_avx512.asm
+ gf_5vect_mad_sse.asm
+ gf_5vect_mad_avx.asm
+ gf_5vect_mad_avx2.asm
+ gf_6vect_mad_sse.asm
+ gf_6vect_mad_avx.asm
+ gf_6vect_mad_avx2.asm
+)
+
+END()
diff --git a/contrib/libs/isa-l/include/erasure_code.h b/contrib/libs/isa-l/include/erasure_code.h
new file mode 100644
index 0000000000..04fdfb1bc2
--- /dev/null
+++ b/contrib/libs/isa-l/include/erasure_code.h
@@ -0,0 +1,944 @@
+/**********************************************************************
+ Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+
+#ifndef _ERASURE_CODE_H_
+#define _ERASURE_CODE_H_
+
+/**
+ * @file erasure_code.h
+ * @brief Interface to functions supporting erasure code encode and decode.
+ *
+ * This file defines the interface to optimized functions used in erasure
+ * codes. Encode and decode of erasures in GF(2^8) are made by calculating the
+ * dot product of the symbols (bytes in GF(2^8)) across a set of buffers and a
+ * set of coefficients. Values for the coefficients are determined by the type
+ * of erasure code. Using a general dot product means that any sequence of
+ * coefficients may be used including erasure codes based on random
+ * coefficients.
+ * Multiple versions of dot product are supplied to calculate 1-6 output
+ * vectors in one pass.
+ * Base GF multiply and divide functions can be sped up by defining
+ * GF_LARGE_TABLES at the expense of memory size.
+ *
+ */
+
+#include "gf_vect_mul.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Initialize tables for fast Erasure Code encode and decode.
+ *
+ * Generates the expanded tables needed for fast encode or decode for erasure
+ * codes on blocks of data. 32bytes is generated for each input coefficient.
+ *
+ * @param k The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param rows The number of output vectors to concurrently encode/decode.
+ * @param a Pointer to sets of arrays of input coefficients used to encode
+ * or decode data.
+ * @param gftbls Pointer to start of space for concatenated output tables
+ * generated from input coefficients. Must be of size 32*k*rows.
+ * @returns none
+ */
+
+void ec_init_tables(int k, int rows, unsigned char* a, unsigned char* gftbls);
+
+/**
+ * @brief Generate or decode erasure codes on blocks of data, runs appropriate version.
+ *
+ * Given a list of source data blocks, generate one or multiple blocks of
+ * encoded data as specified by a matrix of GF(2^8) coefficients. When given a
+ * suitable set of coefficients, this function will perform the fast generation
+ * or decoding of Reed-Solomon type erasure codes.
+ *
+ * This function determines what instruction sets are enabled and
+ * selects the appropriate version at runtime.
+ *
+ * @param len Length of each block of data (vector) of source or dest data.
+ * @param k The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param rows The number of output vectors to concurrently encode/decode.
+ * @param gftbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*k*rows
+ * @param data Array of pointers to source input buffers.
+ * @param coding Array of pointers to coded output buffers.
+ * @returns none
+ */
+
+void ec_encode_data(int len, int k, int rows, unsigned char *gftbls, unsigned char **data,
+ unsigned char **coding);
+
+/**
+ * @brief Generate or decode erasure codes on blocks of data, runs baseline version.
+ *
+ * Baseline version of ec_encode_data() with same parameters.
+ */
+void ec_encode_data_base(int len, int srcs, int dests, unsigned char *v, unsigned char **src,
+ unsigned char **dest);
+
+/**
+ * @brief Generate update for encode or decode of erasure codes from single source, runs appropriate version.
+ *
+ * Given one source data block, update one or multiple blocks of encoded data as
+ * specified by a matrix of GF(2^8) coefficients. When given a suitable set of
+ * coefficients, this function will perform the fast generation or decoding of
+ * Reed-Solomon type erasure codes from one input source at a time.
+ *
+ * This function determines what instruction sets are enabled and selects the
+ * appropriate version at runtime.
+ *
+ * @param len Length of each block of data (vector) of source or dest data.
+ * @param k The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param rows The number of output vectors to concurrently encode/decode.
+ * @param vec_i The vector index corresponding to the single input source.
+ * @param g_tbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*k*rows
+ * @param data Pointer to single input source used to update output parity.
+ * @param coding Array of pointers to coded output buffers.
+ * @returns none
+ */
+void ec_encode_data_update(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding);
+
+/**
+ * @brief Generate update for encode or decode of erasure codes from single source.
+ *
+ * Baseline version of ec_encode_data_update().
+ */
+
+void ec_encode_data_update_base(int len, int k, int rows, int vec_i, unsigned char *v,
+ unsigned char *data, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product, runs baseline version.
+ *
+ * Does a GF(2^8) dot product across each byte of the input array and a constant
+ * set of coefficients to produce each byte of the output. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 32*vlen byte constant array based on the input coefficients.
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 32*vlen byte array of pre-calculated constants based
+ * on the array of input coefficients. Only elements 32*CONST*j + 1
+ * of this array are used, where j = (0, 1, 2...) and CONST is the
+ * number of elements in the array of input coefficients. The
+ * elements used correspond to the original input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+
+void gf_vect_dot_prod_base(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector dot product, runs appropriate version.
+ *
+ * Does a GF(2^8) dot product across each byte of the input array and a constant
+ * set of coefficients to produce each byte of the output. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 32*vlen byte constant array based on the input coefficients.
+ *
+ * This function determines what instruction sets are enabled and
+ * selects the appropriate version at runtime.
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 32*vlen byte array of pre-calculated constants based
+ * on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+void gf_vect_dot_prod(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector multiply accumulate, runs appropriate version.
+ *
+ * Does a GF(2^8) multiply across each byte of input source with expanded
+ * constant and add to destination array. Can be used for erasure coding encode
+ * and decode update when only one source is available at a time. Function
+ * requires pre-calculation of a 32*vec byte constant array based on the input
+ * coefficients.
+ *
+ * This function determines what instruction sets are enabled and selects the
+ * appropriate version at runtime.
+ *
+ * @param len Length of each vector in bytes. Must be >= 64.
+ * @param vec The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param vec_i The vector index corresponding to the single input source.
+ * @param gftbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*vec.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+void gf_vect_mad(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector multiply accumulate, baseline version.
+ *
+ * Baseline version of gf_vect_mad() with same parameters.
+ */
+
+void gf_vect_mad_base(int len, int vec, int vec_i, unsigned char *v, unsigned char *src,
+ unsigned char *dest);
+
+// x86 only
+#if defined(__i386__) || defined(__x86_64__)
+
+/**
+ * @brief Generate or decode erasure codes on blocks of data.
+ *
+ * Arch specific version of ec_encode_data() with same parameters.
+ * @requires SSE4.1
+ */
+void ec_encode_data_sse(int len, int k, int rows, unsigned char *gftbls, unsigned char **data,
+ unsigned char **coding);
+
+/**
+ * @brief Generate or decode erasure codes on blocks of data.
+ *
+ * Arch specific version of ec_encode_data() with same parameters.
+ * @requires AVX
+ */
+void ec_encode_data_avx(int len, int k, int rows, unsigned char *gftbls, unsigned char **data,
+ unsigned char **coding);
+
+/**
+ * @brief Generate or decode erasure codes on blocks of data.
+ *
+ * Arch specific version of ec_encode_data() with same parameters.
+ * @requires AVX2
+ */
+void ec_encode_data_avx2(int len, int k, int rows, unsigned char *gftbls, unsigned char **data,
+ unsigned char **coding);
+
+/**
+ * @brief Generate update for encode or decode of erasure codes from single source.
+ *
+ * Arch specific version of ec_encode_data_update() with same parameters.
+ * @requires SSE4.1
+ */
+
+void ec_encode_data_update_sse(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding);
+
+/**
+ * @brief Generate update for encode or decode of erasure codes from single source.
+ *
+ * Arch specific version of ec_encode_data_update() with same parameters.
+ * @requires AVX
+ */
+
+void ec_encode_data_update_avx(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding);
+
+/**
+ * @brief Generate update for encode or decode of erasure codes from single source.
+ *
+ * Arch specific version of ec_encode_data_update() with same parameters.
+ * @requires AVX2
+ */
+
+void ec_encode_data_update_avx2(int len, int k, int rows, int vec_i, unsigned char *g_tbls,
+ unsigned char *data, unsigned char **coding);
+
+/**
+ * @brief GF(2^8) vector dot product.
+ *
+ * Does a GF(2^8) dot product across each byte of the input array and a constant
+ * set of coefficients to produce each byte of the output. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 32*vlen byte constant array based on the input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 32*vlen byte array of pre-calculated constants based
+ * on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+void gf_vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector dot product.
+ *
+ * Does a GF(2^8) dot product across each byte of the input array and a constant
+ * set of coefficients to produce each byte of the output. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 32*vlen byte constant array based on the input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 32*vlen byte array of pre-calculated constants based
+ * on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+void gf_vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector dot product.
+ *
+ * Does a GF(2^8) dot product across each byte of the input array and a constant
+ * set of coefficients to produce each byte of the output. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 32*vlen byte constant array based on the input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 32*vlen byte array of pre-calculated constants based
+ * on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Pointer to destination data array.
+ * @returns none
+ */
+
+void gf_vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector dot product with two outputs.
+ *
+ * Vector dot product optimized to calculate two outputs at a time. Does two
+ * GF(2^8) dot products across each byte of the input array and two constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 2*32*vlen byte constant array based on the two sets of input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 2*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_2vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with two outputs.
+ *
+ * Vector dot product optimized to calculate two outputs at a time. Does two
+ * GF(2^8) dot products across each byte of the input array and two constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 2*32*vlen byte constant array based on the two sets of input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 2*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_2vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with two outputs.
+ *
+ * Vector dot product optimized to calculate two outputs at a time. Does two
+ * GF(2^8) dot products across each byte of the input array and two constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 2*32*vlen byte constant array based on the two sets of input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 2*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_2vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with three outputs.
+ *
+ * Vector dot product optimized to calculate three outputs at a time. Does three
+ * GF(2^8) dot products across each byte of the input array and three constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 3*32*vlen byte constant array based on the three sets of input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 3*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_3vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with three outputs.
+ *
+ * Vector dot product optimized to calculate three outputs at a time. Does three
+ * GF(2^8) dot products across each byte of the input array and three constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 3*32*vlen byte constant array based on the three sets of input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 3*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_3vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with three outputs.
+ *
+ * Vector dot product optimized to calculate three outputs at a time. Does three
+ * GF(2^8) dot products across each byte of the input array and three constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 3*32*vlen byte constant array based on the three sets of input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 3*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_3vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with four outputs.
+ *
+ * Vector dot product optimized to calculate four outputs at a time. Does four
+ * GF(2^8) dot products across each byte of the input array and four constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 4*32*vlen byte constant array based on the four sets of input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 4*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_4vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with four outputs.
+ *
+ * Vector dot product optimized to calculate four outputs at a time. Does four
+ * GF(2^8) dot products across each byte of the input array and four constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 4*32*vlen byte constant array based on the four sets of input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 4*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_4vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with four outputs.
+ *
+ * Vector dot product optimized to calculate four outputs at a time. Does four
+ * GF(2^8) dot products across each byte of the input array and four constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 4*32*vlen byte constant array based on the four sets of input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 4*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_4vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with five outputs.
+ *
+ * Vector dot product optimized to calculate five outputs at a time. Does five
+ * GF(2^8) dot products across each byte of the input array and five constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 5*32*vlen byte constant array based on the five sets of input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 5*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_5vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with five outputs.
+ *
+ * Vector dot product optimized to calculate five outputs at a time. Does five
+ * GF(2^8) dot products across each byte of the input array and five constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 5*32*vlen byte constant array based on the five sets of input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 5*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_5vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with five outputs.
+ *
+ * Vector dot product optimized to calculate five outputs at a time. Does five
+ * GF(2^8) dot products across each byte of the input array and five constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 5*32*vlen byte constant array based on the five sets of input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 5*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_5vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with six outputs.
+ *
+ * Vector dot product optimized to calculate six outputs at a time. Does six
+ * GF(2^8) dot products across each byte of the input array and six constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 6*32*vlen byte constant array based on the six sets of input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 6*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_6vect_dot_prod_sse(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with six outputs.
+ *
+ * Vector dot product optimized to calculate six outputs at a time. Does six
+ * GF(2^8) dot products across each byte of the input array and six constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 6*32*vlen byte constant array based on the six sets of input coefficients.
+ * @requires AVX
+ *
+ * @param len Length of each vector in bytes. Must be >= 16.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 6*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_6vect_dot_prod_avx(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector dot product with six outputs.
+ *
+ * Vector dot product optimized to calculate six outputs at a time. Does six
+ * GF(2^8) dot products across each byte of the input array and six constant
+ * sets of coefficients to produce each byte of the outputs. Can be used for
+ * erasure coding encode and decode. Function requires pre-calculation of a
+ * 6*32*vlen byte constant array based on the six sets of input coefficients.
+ * @requires AVX2
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vlen Number of vector sources.
+ * @param gftbls Pointer to 6*32*vlen byte array of pre-calculated constants
+ * based on the array of input coefficients.
+ * @param src Array of pointers to source inputs.
+ * @param dest Array of pointers to destination data buffers.
+ * @returns none
+ */
+
+void gf_6vect_dot_prod_avx2(int len, int vlen, unsigned char *gftbls,
+ unsigned char **src, unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply accumulate, arch specific version.
+ *
+ * Arch specific version of gf_vect_mad() with same parameters.
+ * @requires SSE4.1
+ */
+
+void gf_vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char *dest);
+/**
+ * @brief GF(2^8) vector multiply accumulate, arch specific version.
+ *
+ * Arch specific version of gf_vect_mad() with same parameters.
+ * @requires AVX
+ */
+
+void gf_vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char *dest);
+
+/**
+ * @brief GF(2^8) vector multiply accumulate, arch specific version.
+ *
+ * Arch specific version of gf_vect_mad() with same parameters.
+ * @requires AVX2
+ */
+
+void gf_vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char *dest);
+
+
+/**
+ * @brief GF(2^8) vector multiply with 2 accumulate. SSE version.
+ *
+ * Does a GF(2^8) multiply across each byte of input source with expanded
+ * constants and add to destination arrays. Can be used for erasure coding
+ * encode and decode update when only one source is available at a
+ * time. Function requires pre-calculation of a 32*vec byte constant array based
+ * on the input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vec The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param vec_i The vector index corresponding to the single input source.
+ * @param gftbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*vec.
+ * @param src Pointer to source input array.
+ * @param dest Array of pointers to destination input/outputs.
+ * @returns none
+ */
+
+void gf_2vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 2 accumulate. AVX version of gf_2vect_mad_sse().
+ * @requires AVX
+ */
+void gf_2vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+/**
+ * @brief GF(2^8) vector multiply with 2 accumulate. AVX2 version of gf_2vect_mad_sse().
+ * @requires AVX2
+ */
+void gf_2vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 3 accumulate. SSE version.
+ *
+ * Does a GF(2^8) multiply across each byte of input source with expanded
+ * constants and add to destination arrays. Can be used for erasure coding
+ * encode and decode update when only one source is available at a
+ * time. Function requires pre-calculation of a 32*vec byte constant array based
+ * on the input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vec The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param vec_i The vector index corresponding to the single input source.
+ * @param gftbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*vec.
+ * @param src Pointer to source input array.
+ * @param dest Array of pointers to destination input/outputs.
+ * @returns none
+ */
+
+void gf_3vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 3 accumulate. AVX version of gf_3vect_mad_sse().
+ * @requires AVX
+ */
+void gf_3vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 3 accumulate. AVX2 version of gf_3vect_mad_sse().
+ * @requires AVX2
+ */
+void gf_3vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 4 accumulate. SSE version.
+ *
+ * Does a GF(2^8) multiply across each byte of input source with expanded
+ * constants and add to destination arrays. Can be used for erasure coding
+ * encode and decode update when only one source is available at a
+ * time. Function requires pre-calculation of a 32*vec byte constant array based
+ * on the input coefficients.
+ * @requires SSE4.1
+ *
+ * @param len Length of each vector in bytes. Must be >= 32.
+ * @param vec The number of vector sources or rows in the generator matrix
+ * for coding.
+ * @param vec_i The vector index corresponding to the single input source.
+ * @param gftbls Pointer to array of input tables generated from coding
+ * coefficients in ec_init_tables(). Must be of size 32*vec.
+ * @param src Pointer to source input array.
+ * @param dest Array of pointers to destination input/outputs.
+ * @returns none
+ */
+
+void gf_4vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 4 accumulate. AVX version of gf_4vect_mad_sse().
+ * @requires AVX
+ */
+void gf_4vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+/**
+ * @brief GF(2^8) vector multiply with 4 accumulate. AVX2 version of gf_4vect_mad_sse().
+ * @requires AVX2
+ */
+void gf_4vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 5 accumulate. SSE version.
+ * @requires SSE4.1
+ */
+void gf_5vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 5 accumulate. AVX version.
+ * @requires AVX
+ */
+void gf_5vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+/**
+ * @brief GF(2^8) vector multiply with 5 accumulate. AVX2 version.
+ * @requires AVX2
+ */
+void gf_5vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 6 accumulate. SSE version.
+ * @requires SSE4.1
+ */
+void gf_6vect_mad_sse(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+/**
+ * @brief GF(2^8) vector multiply with 6 accumulate. AVX version.
+ * @requires AVX
+ */
+void gf_6vect_mad_avx(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+/**
+ * @brief GF(2^8) vector multiply with 6 accumulate. AVX2 version.
+ * @requires AVX2
+ */
+void gf_6vect_mad_avx2(int len, int vec, int vec_i, unsigned char *gftbls, unsigned char *src,
+ unsigned char **dest);
+
+#endif
+
+/**********************************************************************
+ * The remaining are lib support functions used in GF(2^8) operations.
+ */
+
+/**
+ * @brief Single element GF(2^8) multiply.
+ *
+ * @param a Multiplicand a
+ * @param b Multiplicand b
+ * @returns Product of a and b in GF(2^8)
+ */
+
+unsigned char gf_mul_erasure(unsigned char a, unsigned char b);
+
+/**
+ * @brief Single element GF(2^8) inverse.
+ *
+ * @param a Input element
+ * @returns Field element b such that a x b = {1}
+ */
+
+unsigned char gf_inv(unsigned char a);
+
+/**
+ * @brief Generate a matrix of coefficients to be used for encoding.
+ *
+ * Vandermonde matrix example of encoding coefficients where high portion of
+ * matrix is identity matrix I and lower portion is constructed as 2^{i*(j-k+1)}
+ * i:{0,k-1} j:{k,m-1}. Commonly used method for choosing coefficients in
+ * erasure encoding but does not guarantee invertable for every sub matrix. For
+ * large pairs of m and k it is possible to find cases where the decode matrix
+ * chosen from sources and parity is not invertable. Users may want to adjust
+ * for certain pairs m and k. If m and k satisfy one of the following
+ * inequalities, no adjustment is required:
+ *
+ * - k <= 3
+ * - k = 4, m <= 25
+ * - k = 5, m <= 10
+ * - k <= 21, m-k = 4
+ * - m - k <= 3.
+ *
+ * @param a [m x k] array to hold coefficients
+ * @param m number of rows in matrix corresponding to srcs + parity.
+ * @param k number of columns in matrix corresponding to srcs.
+ * @returns none
+ */
+
+void gf_gen_rs_matrix(unsigned char *a, int m, int k);
+
+/**
+ * @brief Generate a Cauchy matrix of coefficients to be used for encoding.
+ *
+ * Cauchy matrix example of encoding coefficients where high portion of matrix
+ * is identity matrix I and lower portion is constructed as 1/(i + j) | i != j,
+ * i:{0,k-1} j:{k,m-1}. Any sub-matrix of a Cauchy matrix should be invertable.
+ *
+ * @param a [m x k] array to hold coefficients
+ * @param m number of rows in matrix corresponding to srcs + parity.
+ * @param k number of columns in matrix corresponding to srcs.
+ * @returns none
+ */
+
+void gf_gen_cauchy1_matrix(unsigned char *a, int m, int k);
+
+/**
+ * @brief Invert a matrix in GF(2^8)
+ *
+ * @param in input matrix
+ * @param out output matrix such that [in] x [out] = [I] - identity matrix
+ * @param n size of matrix [nxn]
+ * @returns 0 successful, other fail on singular input matrix
+ */
+
+int gf_invert_matrix(unsigned char *in, unsigned char *out, const int n);
+
+
+/*************************************************************/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //_ERASURE_CODE_H_
diff --git a/contrib/libs/isa-l/include/gf_vect_mul.h b/contrib/libs/isa-l/include/gf_vect_mul.h
new file mode 100644
index 0000000000..70a0ab2ed3
--- /dev/null
+++ b/contrib/libs/isa-l/include/gf_vect_mul.h
@@ -0,0 +1,152 @@
+/**********************************************************************
+ Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+
+#ifndef _GF_VECT_MUL_H
+#define _GF_VECT_MUL_H
+
+/**
+ * @file gf_vect_mul.h
+ * @brief Interface to functions for vector (block) multiplication in GF(2^8).
+ *
+ * This file defines the interface to routines used in fast RAID rebuild and
+ * erasure codes.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// x86 only
+#if defined(__i386__) || defined(__x86_64__)
+
+ /**
+ * @brief GF(2^8) vector multiply by constant.
+ *
+ * Does a GF(2^8) vector multiply b = Ca where a and b are arrays and C
+ * is a single field element in GF(2^8). Can be used for RAID6 rebuild
+ * and partial write functions. Function requires pre-calculation of a
+ * 32-element constant array based on constant C. gftbl(C) = {C{00},
+ * C{01}, C{02}, ... , C{0f} }, {C{00}, C{10}, C{20}, ... , C{f0} }. Len
+ * and src must be aligned to 32B.
+ * @requires SSE4.1
+ *
+ * @param len Length of vector in bytes. Must be aligned to 32B.
+ * @param gftbl Pointer to 32-byte array of pre-calculated constants based on C.
+ * @param src Pointer to src data array. Must be aligned to 32B.
+ * @param dest Pointer to destination data array. Must be aligned to 32B.
+ * @returns 0 pass, other fail
+ */
+
+int gf_vect_mul_sse(int len, unsigned char *gftbl, void *src, void *dest);
+
+
+ /**
+ * @brief GF(2^8) vector multiply by constant.
+ *
+ * Does a GF(2^8) vector multiply b = Ca where a and b are arrays and C
+ * is a single field element in GF(2^8). Can be used for RAID6 rebuild
+ * and partial write functions. Function requires pre-calculation of a
+ * 32-element constant array based on constant C. gftbl(C) = {C{00},
+ * C{01}, C{02}, ... , C{0f} }, {C{00}, C{10}, C{20}, ... , C{f0} }. Len
+ * and src must be aligned to 32B.
+ * @requires AVX
+ *
+ * @param len Length of vector in bytes. Must be aligned to 32B.
+ * @param gftbl Pointer to 32-byte array of pre-calculated constants based on C.
+ * @param src Pointer to src data array. Must be aligned to 32B.
+ * @param dest Pointer to destination data array. Must be aligned to 32B.
+ * @returns 0 pass, other fail
+ */
+
+int gf_vect_mul_avx(int len, unsigned char *gftbl, void *src, void *dest);
+
+#endif
+
+/**
+ * @brief GF(2^8) vector multiply by constant, runs appropriate version.
+ *
+ * Does a GF(2^8) vector multiply b = Ca where a and b are arrays and C
+ * is a single field element in GF(2^8). Can be used for RAID6 rebuild
+ * and partial write functions. Function requires pre-calculation of a
+ * 32-element constant array based on constant C. gftbl(C) = {C{00},
+ * C{01}, C{02}, ... , C{0f} }, {C{00}, C{10}, C{20}, ... , C{f0} }.
+ * Len and src must be aligned to 32B.
+ *
+ * This function determines what instruction sets are enabled
+ * and selects the appropriate version at runtime.
+ *
+ * @param len Length of vector in bytes. Must be aligned to 32B.
+ * @param gftbl Pointer to 32-byte array of pre-calculated constants based on C.
+ * @param src Pointer to src data array. Must be aligned to 32B.
+ * @param dest Pointer to destination data array. Must be aligned to 32B.
+ * @returns 0 pass, other fail
+ */
+
+int gf_vect_mul(int len, unsigned char *gftbl, void *src, void *dest);
+
+
+/**
+ * @brief Initialize 32-byte constant array for GF(2^8) vector multiply
+ *
+ * Calculates array {C{00}, C{01}, C{02}, ... , C{0f} }, {C{00}, C{10},
+ * C{20}, ... , C{f0} } as required by other fast vector multiply
+ * functions.
+ * @param c Constant input.
+ * @param gftbl Table output.
+ */
+
+void gf_vect_mul_init(unsigned char c, unsigned char* gftbl);
+
+
+/**
+ * @brief GF(2^8) vector multiply by constant, runs baseline version.
+ *
+ * Does a GF(2^8) vector multiply b = Ca where a and b are arrays and C
+ * is a single field element in GF(2^8). Can be used for RAID6 rebuild
+ * and partial write functions. Function requires pre-calculation of a
+ * 32-element constant array based on constant C. gftbl(C) = {C{00},
+ * C{01}, C{02}, ... , C{0f} }, {C{00}, C{10}, C{20}, ... , C{f0} }. Len
+ * and src must be aligned to 32B.
+ *
+ * @param len Length of vector in bytes. Must be aligned to 32B.
+ * @param a Pointer to 32-byte array of pre-calculated constants based on C.
+ * only use 2nd element is used.
+ * @param src Pointer to src data array. Must be aligned to 32B.
+ * @param dest Pointer to destination data array. Must be aligned to 32B.
+ */
+
+void gf_vect_mul_base(int len, unsigned char *a, unsigned char *src,
+ unsigned char *dest);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //_GF_VECT_MUL_H
diff --git a/contrib/libs/isa-l/include/multibinary.asm b/contrib/libs/isa-l/include/multibinary.asm
new file mode 100644
index 0000000000..2cad1c51be
--- /dev/null
+++ b/contrib/libs/isa-l/include/multibinary.asm
@@ -0,0 +1,399 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+%ifndef _MULTIBINARY_ASM_
+%define _MULTIBINARY_ASM_
+
+%ifidn __OUTPUT_FORMAT__, elf32
+ %define mbin_def_ptr dd
+ %define mbin_ptr_sz dword
+ %define mbin_rdi edi
+ %define mbin_rsi esi
+ %define mbin_rax eax
+ %define mbin_rbx ebx
+ %define mbin_rcx ecx
+ %define mbin_rdx edx
+%else
+ %define mbin_def_ptr dq
+ %define mbin_ptr_sz qword
+ %define mbin_rdi rdi
+ %define mbin_rsi rsi
+ %define mbin_rax rax
+ %define mbin_rbx rbx
+ %define mbin_rcx rcx
+ %define mbin_rdx rdx
+%endif
+
+%ifndef AS_FEATURE_LEVEL
+%define AS_FEATURE_LEVEL 4
+%endif
+
+;;;;
+; multibinary macro:
+; creates the visable entry point that uses HW optimized call pointer
+; creates the init of the HW optimized call pointer
+;;;;
+%macro mbin_interface 1
+ ;;;;
+ ; *_dispatched is defaulted to *_mbinit and replaced on first call.
+ ; Therefore, *_dispatch_init is only executed on first call.
+ ;;;;
+ section .data
+ %1_dispatched:
+ mbin_def_ptr %1_mbinit
+
+ section .text
+ global %1:ISAL_SYM_TYPE_FUNCTION
+ %1_mbinit:
+ ;;; only called the first time to setup hardware match
+ call %1_dispatch_init
+ ;;; falls thru to execute the hw optimized code
+ %1:
+ jmp mbin_ptr_sz [%1_dispatched]
+%endmacro
+
+;;;;;
+; mbin_dispatch_init parameters
+; Use this function when SSE/00/01 is a minimum requirement
+; 1-> function name
+; 2-> SSE/00/01 optimized function used as base
+; 3-> AVX or AVX/02 opt func
+; 4-> AVX2 or AVX/04 opt func
+;;;;;
+%macro mbin_dispatch_init 4
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ push mbin_rax
+ push mbin_rbx
+ push mbin_rcx
+ push mbin_rdx
+ lea mbin_rsi, [%2 WRT_OPT] ; Default to SSE 00/01
+
+ mov eax, 1
+ cpuid
+ and ecx, (FLAG_CPUID1_ECX_AVX | FLAG_CPUID1_ECX_OSXSAVE)
+ cmp ecx, (FLAG_CPUID1_ECX_AVX | FLAG_CPUID1_ECX_OSXSAVE)
+ lea mbin_rbx, [%3 WRT_OPT] ; AVX (gen2) opt func
+ jne _%1_init_done ; AVX is not available so end
+ mov mbin_rsi, mbin_rbx
+
+ ;; Try for AVX2
+ xor ecx, ecx
+ mov eax, 7
+ cpuid
+ test ebx, FLAG_CPUID7_EBX_AVX2
+ lea mbin_rbx, [%4 WRT_OPT] ; AVX (gen4) opt func
+ cmovne mbin_rsi, mbin_rbx
+
+ ;; Does it have xmm and ymm support
+ xor ecx, ecx
+ xgetbv
+ and eax, FLAG_XGETBV_EAX_XMM_YMM
+ cmp eax, FLAG_XGETBV_EAX_XMM_YMM
+ je _%1_init_done
+ lea mbin_rsi, [%2 WRT_OPT]
+
+ _%1_init_done:
+ pop mbin_rdx
+ pop mbin_rcx
+ pop mbin_rbx
+ pop mbin_rax
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+
+;;;;;
+; mbin_dispatch_init2 parameters
+; Cases where only base functions are available
+; 1-> function name
+; 2-> base function
+;;;;;
+%macro mbin_dispatch_init2 2
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ lea mbin_rsi, [%2 WRT_OPT] ; Default
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+
+;;;;;
+; mbin_dispatch_init_clmul 3 parameters
+; Use this case for CRC which needs both SSE4_1 and CLMUL
+; 1-> function name
+; 2-> base function
+; 3-> SSE4_1 and CLMUL optimized function
+;;;;;
+%macro mbin_dispatch_init_clmul 3
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ push mbin_rax
+ push mbin_rbx
+ push mbin_rcx
+ push mbin_rdx
+ lea mbin_rsi, [%2 WRT_OPT] ; Default - use base function
+
+ mov eax, 1
+ cpuid
+ lea mbin_rbx, [%3 WRT_OPT] ; SSE opt func
+
+ ; Test for SSE4.2
+ test ecx, FLAG_CPUID1_ECX_SSE4_1
+ jz _%1_init_done
+ test ecx, FLAG_CPUID1_ECX_CLMUL
+ cmovne mbin_rsi, mbin_rbx
+ _%1_init_done:
+ pop mbin_rdx
+ pop mbin_rcx
+ pop mbin_rbx
+ pop mbin_rax
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+
+;;;;;
+; mbin_dispatch_init5 parameters
+; 1-> function name
+; 2-> base function
+; 3-> SSE4_2 or 00/01 optimized function
+; 4-> AVX/02 opt func
+; 5-> AVX2/04 opt func
+;;;;;
+%macro mbin_dispatch_init5 5
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ push mbin_rax
+ push mbin_rbx
+ push mbin_rcx
+ push mbin_rdx
+ lea mbin_rsi, [%2 WRT_OPT] ; Default - use base function
+
+ mov eax, 1
+ cpuid
+ ; Test for SSE4.2
+ test ecx, FLAG_CPUID1_ECX_SSE4_2
+ lea mbin_rbx, [%3 WRT_OPT] ; SSE opt func
+ cmovne mbin_rsi, mbin_rbx
+
+ and ecx, (FLAG_CPUID1_ECX_AVX | FLAG_CPUID1_ECX_OSXSAVE)
+ cmp ecx, (FLAG_CPUID1_ECX_AVX | FLAG_CPUID1_ECX_OSXSAVE)
+ lea mbin_rbx, [%4 WRT_OPT] ; AVX (gen2) opt func
+ jne _%1_init_done ; AVX is not available so end
+ mov mbin_rsi, mbin_rbx
+
+ ;; Try for AVX2
+ xor ecx, ecx
+ mov eax, 7
+ cpuid
+ test ebx, FLAG_CPUID7_EBX_AVX2
+ lea mbin_rbx, [%5 WRT_OPT] ; AVX (gen4) opt func
+ cmovne mbin_rsi, mbin_rbx
+
+ ;; Does it have xmm and ymm support
+ xor ecx, ecx
+ xgetbv
+ and eax, FLAG_XGETBV_EAX_XMM_YMM
+ cmp eax, FLAG_XGETBV_EAX_XMM_YMM
+ je _%1_init_done
+ lea mbin_rsi, [%3 WRT_OPT]
+
+ _%1_init_done:
+ pop mbin_rdx
+ pop mbin_rcx
+ pop mbin_rbx
+ pop mbin_rax
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+
+%if AS_FEATURE_LEVEL >= 6
+;;;;;
+; mbin_dispatch_init6 parameters
+; 1-> function name
+; 2-> base function
+; 3-> SSE4_2 or 00/01 optimized function
+; 4-> AVX/02 opt func
+; 5-> AVX2/04 opt func
+; 6-> AVX512/06 opt func
+;;;;;
+%macro mbin_dispatch_init6 6
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ push mbin_rax
+ push mbin_rbx
+ push mbin_rcx
+ push mbin_rdx
+ push mbin_rdi
+ lea mbin_rsi, [%2 WRT_OPT] ; Default - use base function
+
+ mov eax, 1
+ cpuid
+ mov ebx, ecx ; save cpuid1.ecx
+ test ecx, FLAG_CPUID1_ECX_SSE4_2
+ je _%1_init_done ; Use base function if no SSE4_2
+ lea mbin_rsi, [%3 WRT_OPT] ; SSE possible so use 00/01 opt
+
+ ;; Test for XMM_YMM support/AVX
+ test ecx, FLAG_CPUID1_ECX_OSXSAVE
+ je _%1_init_done
+ xor ecx, ecx
+ xgetbv ; xcr -> edx:eax
+ mov edi, eax ; save xgetvb.eax
+
+ and eax, FLAG_XGETBV_EAX_XMM_YMM
+ cmp eax, FLAG_XGETBV_EAX_XMM_YMM
+ jne _%1_init_done
+ test ebx, FLAG_CPUID1_ECX_AVX
+ je _%1_init_done
+ lea mbin_rsi, [%4 WRT_OPT] ; AVX/02 opt
+
+ ;; Test for AVX2
+ xor ecx, ecx
+ mov eax, 7
+ cpuid
+ test ebx, FLAG_CPUID7_EBX_AVX2
+ je _%1_init_done ; No AVX2 possible
+ lea mbin_rsi, [%5 WRT_OPT] ; AVX2/04 opt func
+
+ ;; Test for AVX512
+ and edi, FLAG_XGETBV_EAX_ZMM_OPM
+ cmp edi, FLAG_XGETBV_EAX_ZMM_OPM
+ jne _%1_init_done ; No AVX512 possible
+ and ebx, FLAGS_CPUID7_EBX_AVX512_G1
+ cmp ebx, FLAGS_CPUID7_EBX_AVX512_G1
+ lea mbin_rbx, [%6 WRT_OPT] ; AVX512/06 opt
+ cmove mbin_rsi, mbin_rbx
+
+ _%1_init_done:
+ pop mbin_rdi
+ pop mbin_rdx
+ pop mbin_rcx
+ pop mbin_rbx
+ pop mbin_rax
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+
+%else
+%macro mbin_dispatch_init6 6
+ mbin_dispatch_init5 %1, %2, %3, %4, %5
+%endmacro
+%endif
+
+%if AS_FEATURE_LEVEL >= 10
+;;;;;
+; mbin_dispatch_init7 parameters
+; 1-> function name
+; 2-> base function
+; 3-> SSE4_2 or 00/01 optimized function
+; 4-> AVX/02 opt func
+; 5-> AVX2/04 opt func
+; 6-> AVX512/06 opt func
+; 7-> AVX512 Update/10 opt func
+;;;;;
+%macro mbin_dispatch_init7 7
+ section .text
+ %1_dispatch_init:
+ push mbin_rsi
+ push mbin_rax
+ push mbin_rbx
+ push mbin_rcx
+ push mbin_rdx
+ push mbin_rdi
+ lea mbin_rsi, [%2 WRT_OPT] ; Default - use base function
+
+ mov eax, 1
+ cpuid
+ mov ebx, ecx ; save cpuid1.ecx
+ test ecx, FLAG_CPUID1_ECX_SSE4_2
+ je _%1_init_done ; Use base function if no SSE4_2
+ lea mbin_rsi, [%3 WRT_OPT] ; SSE possible so use 00/01 opt
+
+ ;; Test for XMM_YMM support/AVX
+ test ecx, FLAG_CPUID1_ECX_OSXSAVE
+ je _%1_init_done
+ xor ecx, ecx
+ xgetbv ; xcr -> edx:eax
+ mov edi, eax ; save xgetvb.eax
+
+ and eax, FLAG_XGETBV_EAX_XMM_YMM
+ cmp eax, FLAG_XGETBV_EAX_XMM_YMM
+ jne _%1_init_done
+ test ebx, FLAG_CPUID1_ECX_AVX
+ je _%1_init_done
+ lea mbin_rsi, [%4 WRT_OPT] ; AVX/02 opt
+
+ ;; Test for AVX2
+ xor ecx, ecx
+ mov eax, 7
+ cpuid
+ test ebx, FLAG_CPUID7_EBX_AVX2
+ je _%1_init_done ; No AVX2 possible
+ lea mbin_rsi, [%5 WRT_OPT] ; AVX2/04 opt func
+
+ ;; Test for AVX512
+ and edi, FLAG_XGETBV_EAX_ZMM_OPM
+ cmp edi, FLAG_XGETBV_EAX_ZMM_OPM
+ jne _%1_init_done ; No AVX512 possible
+ and ebx, FLAGS_CPUID7_EBX_AVX512_G1
+ cmp ebx, FLAGS_CPUID7_EBX_AVX512_G1
+ lea mbin_rbx, [%6 WRT_OPT] ; AVX512/06 opt
+ cmove mbin_rsi, mbin_rbx
+
+ and ecx, FLAGS_CPUID7_ECX_AVX512_G2
+ cmp ecx, FLAGS_CPUID7_ECX_AVX512_G2
+ lea mbin_rbx, [%7 WRT_OPT] ; AVX512/06 opt
+ cmove mbin_rsi, mbin_rbx
+
+ _%1_init_done:
+ pop mbin_rdi
+ pop mbin_rdx
+ pop mbin_rcx
+ pop mbin_rbx
+ pop mbin_rax
+ mov [%1_dispatched], mbin_rsi
+ pop mbin_rsi
+ ret
+%endmacro
+%else
+%macro mbin_dispatch_init7 7
+ mbin_dispatch_init6 %1, %2, %3, %4, %5, %6
+%endmacro
+%endif
+
+%endif ; ifndef _MULTIBINARY_ASM_
diff --git a/contrib/libs/isa-l/include/reg_sizes.asm b/contrib/libs/isa-l/include/reg_sizes.asm
new file mode 100644
index 0000000000..fec6a8aafb
--- /dev/null
+++ b/contrib/libs/isa-l/include/reg_sizes.asm
@@ -0,0 +1,248 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+%ifndef _REG_SIZES_ASM_
+%define _REG_SIZES_ASM_
+
+%ifdef __NASM_VER__
+%ifidn __OUTPUT_FORMAT__, win64
+%error nasm not supported in windows
+%else
+%define endproc_frame
+%endif
+%endif
+
+%ifndef AS_FEATURE_LEVEL
+%define AS_FEATURE_LEVEL 4
+%endif
+
+%define EFLAGS_HAS_CPUID (1<<21)
+%define FLAG_CPUID1_ECX_CLMUL (1<<1)
+%define FLAG_CPUID1_EDX_SSE2 (1<<26)
+%define FLAG_CPUID1_ECX_SSE3 (1)
+%define FLAG_CPUID1_ECX_SSE4_1 (1<<19)
+%define FLAG_CPUID1_ECX_SSE4_2 (1<<20)
+%define FLAG_CPUID1_ECX_POPCNT (1<<23)
+%define FLAG_CPUID1_ECX_AESNI (1<<25)
+%define FLAG_CPUID1_ECX_OSXSAVE (1<<27)
+%define FLAG_CPUID1_ECX_AVX (1<<28)
+%define FLAG_CPUID1_EBX_AVX2 (1<<5)
+
+%define FLAG_CPUID7_EBX_AVX2 (1<<5)
+%define FLAG_CPUID7_EBX_AVX512F (1<<16)
+%define FLAG_CPUID7_EBX_AVX512DQ (1<<17)
+%define FLAG_CPUID7_EBX_AVX512IFMA (1<<21)
+%define FLAG_CPUID7_EBX_AVX512PF (1<<26)
+%define FLAG_CPUID7_EBX_AVX512ER (1<<27)
+%define FLAG_CPUID7_EBX_AVX512CD (1<<28)
+%define FLAG_CPUID7_EBX_AVX512BW (1<<30)
+%define FLAG_CPUID7_EBX_AVX512VL (1<<31)
+
+%define FLAG_CPUID7_ECX_AVX512VBMI (1<<1)
+%define FLAG_CPUID7_ECX_AVX512VBMI2 (1 << 6)
+%define FLAG_CPUID7_ECX_GFNI (1 << 8)
+%define FLAG_CPUID7_ECX_VAES (1 << 9)
+%define FLAG_CPUID7_ECX_VPCLMULQDQ (1 << 10)
+%define FLAG_CPUID7_ECX_VNNI (1 << 11)
+%define FLAG_CPUID7_ECX_BITALG (1 << 12)
+%define FLAG_CPUID7_ECX_VPOPCNTDQ (1 << 14)
+
+%define FLAGS_CPUID7_EBX_AVX512_G1 (FLAG_CPUID7_EBX_AVX512F | FLAG_CPUID7_EBX_AVX512VL | FLAG_CPUID7_EBX_AVX512BW | FLAG_CPUID7_EBX_AVX512CD | FLAG_CPUID7_EBX_AVX512DQ)
+%define FLAGS_CPUID7_ECX_AVX512_G2 (FLAG_CPUID7_ECX_AVX512VBMI2 | FLAG_CPUID7_ECX_GFNI | FLAG_CPUID7_ECX_VAES | FLAG_CPUID7_ECX_VPCLMULQDQ | FLAG_CPUID7_ECX_VNNI | FLAG_CPUID7_ECX_BITALG | FLAG_CPUID7_ECX_VPOPCNTDQ)
+
+%define FLAG_XGETBV_EAX_XMM (1<<1)
+%define FLAG_XGETBV_EAX_YMM (1<<2)
+%define FLAG_XGETBV_EAX_XMM_YMM 0x6
+%define FLAG_XGETBV_EAX_ZMM_OPM 0xe0
+
+%define FLAG_CPUID1_EAX_AVOTON 0x000406d0
+%define FLAG_CPUID1_EAX_STEP_MASK 0xfffffff0
+
+; define d and w variants for registers
+
+%define raxd eax
+%define raxw ax
+%define raxb al
+
+%define rbxd ebx
+%define rbxw bx
+%define rbxb bl
+
+%define rcxd ecx
+%define rcxw cx
+%define rcxb cl
+
+%define rdxd edx
+%define rdxw dx
+%define rdxb dl
+
+%define rsid esi
+%define rsiw si
+%define rsib sil
+
+%define rdid edi
+%define rdiw di
+%define rdib dil
+
+%define rbpd ebp
+%define rbpw bp
+%define rbpb bpl
+
+%define ymm0x xmm0
+%define ymm1x xmm1
+%define ymm2x xmm2
+%define ymm3x xmm3
+%define ymm4x xmm4
+%define ymm5x xmm5
+%define ymm6x xmm6
+%define ymm7x xmm7
+%define ymm8x xmm8
+%define ymm9x xmm9
+%define ymm10x xmm10
+%define ymm11x xmm11
+%define ymm12x xmm12
+%define ymm13x xmm13
+%define ymm14x xmm14
+%define ymm15x xmm15
+
+%define zmm0x xmm0
+%define zmm1x xmm1
+%define zmm2x xmm2
+%define zmm3x xmm3
+%define zmm4x xmm4
+%define zmm5x xmm5
+%define zmm6x xmm6
+%define zmm7x xmm7
+%define zmm8x xmm8
+%define zmm9x xmm9
+%define zmm10x xmm10
+%define zmm11x xmm11
+%define zmm12x xmm12
+%define zmm13x xmm13
+%define zmm14x xmm14
+%define zmm15x xmm15
+%define zmm16x xmm16
+%define zmm17x xmm17
+%define zmm18x xmm18
+%define zmm19x xmm19
+%define zmm20x xmm20
+%define zmm21x xmm21
+%define zmm22x xmm22
+%define zmm23x xmm23
+%define zmm24x xmm24
+%define zmm25x xmm25
+%define zmm26x xmm26
+%define zmm27x xmm27
+%define zmm28x xmm28
+%define zmm29x xmm29
+%define zmm30x xmm30
+%define zmm31x xmm31
+
+%define zmm0y ymm0
+%define zmm1y ymm1
+%define zmm2y ymm2
+%define zmm3y ymm3
+%define zmm4y ymm4
+%define zmm5y ymm5
+%define zmm6y ymm6
+%define zmm7y ymm7
+%define zmm8y ymm8
+%define zmm9y ymm9
+%define zmm10y ymm10
+%define zmm11y ymm11
+%define zmm12y ymm12
+%define zmm13y ymm13
+%define zmm14y ymm14
+%define zmm15y ymm15
+%define zmm16y ymm16
+%define zmm17y ymm17
+%define zmm18y ymm18
+%define zmm19y ymm19
+%define zmm20y ymm20
+%define zmm21y ymm21
+%define zmm22y ymm22
+%define zmm23y ymm23
+%define zmm24y ymm24
+%define zmm25y ymm25
+%define zmm26y ymm26
+%define zmm27y ymm27
+%define zmm28y ymm28
+%define zmm29y ymm29
+%define zmm30y ymm30
+%define zmm31y ymm31
+
+%define DWORD(reg) reg %+ d
+%define WORD(reg) reg %+ w
+%define BYTE(reg) reg %+ b
+
+%define XWORD(reg) reg %+ x
+
+%ifidn __OUTPUT_FORMAT__,elf32
+section .note.GNU-stack noalloc noexec nowrite progbits
+section .text
+%endif
+%ifidn __OUTPUT_FORMAT__,elf64
+section .note.GNU-stack noalloc noexec nowrite progbits
+section .text
+%endif
+
+%ifdef REL_TEXT
+ %define WRT_OPT
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define WRT_OPT wrt ..plt
+%else
+ %define WRT_OPT
+%endif
+
+%ifidn __OUTPUT_FORMAT__, macho64
+ %define elf64 macho64
+ mac_equ equ 1
+ %ifdef __NASM_VER__
+ %define ISAL_SYM_TYPE_FUNCTION
+ %define ISAL_SYM_TYPE_DATA_INTERNAL
+ %else
+ %define ISAL_SYM_TYPE_FUNCTION function
+ %define ISAL_SYM_TYPE_DATA_INTERNAL data internal
+ %endif
+%else
+ %define ISAL_SYM_TYPE_FUNCTION function
+ %define ISAL_SYM_TYPE_DATA_INTERNAL data internal
+%endif
+
+%macro slversion 4
+ section .text
+ global %1_slver_%2%3%4
+ global %1_slver
+ %1_slver:
+ %1_slver_%2%3%4:
+ dw 0x%4
+ db 0x%3, 0x%2
+%endmacro
+
+%endif ; ifndef _REG_SIZES_ASM_
diff --git a/contrib/libs/linux-headers/linux/bpf_common.h b/contrib/libs/linux-headers/linux/bpf_common.h
new file mode 100644
index 0000000000..f0fe139497
--- /dev/null
+++ b/contrib/libs/linux-headers/linux/bpf_common.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef __LINUX_BPF_COMMON_H__
+#define __LINUX_BPF_COMMON_H__
+
+/* Instruction classes */
+#define BPF_CLASS(code) ((code) & 0x07)
+#define BPF_LD 0x00
+#define BPF_LDX 0x01
+#define BPF_ST 0x02
+#define BPF_STX 0x03
+#define BPF_ALU 0x04
+#define BPF_JMP 0x05
+#define BPF_RET 0x06
+#define BPF_MISC 0x07
+
+/* ld/ldx fields */
+#define BPF_SIZE(code) ((code) & 0x18)
+#define BPF_W 0x00 /* 32-bit */
+#define BPF_H 0x08 /* 16-bit */
+#define BPF_B 0x10 /* 8-bit */
+/* eBPF BPF_DW 0x18 64-bit */
+#define BPF_MODE(code) ((code) & 0xe0)
+#define BPF_IMM 0x00
+#define BPF_ABS 0x20
+#define BPF_IND 0x40
+#define BPF_MEM 0x60
+#define BPF_LEN 0x80
+#define BPF_MSH 0xa0
+
+/* alu/jmp fields */
+#define BPF_OP(code) ((code) & 0xf0)
+#define BPF_ADD 0x00
+#define BPF_SUB 0x10
+#define BPF_MUL 0x20
+#define BPF_DIV 0x30
+#define BPF_OR 0x40
+#define BPF_AND 0x50
+#define BPF_LSH 0x60
+#define BPF_RSH 0x70
+#define BPF_NEG 0x80
+#define BPF_MOD 0x90
+#define BPF_XOR 0xa0
+
+#define BPF_JA 0x00
+#define BPF_JEQ 0x10
+#define BPF_JGT 0x20
+#define BPF_JGE 0x30
+#define BPF_JSET 0x40
+#define BPF_SRC(code) ((code) & 0x08)
+#define BPF_K 0x00
+#define BPF_X 0x08
+
+#ifndef BPF_MAXINSNS
+#define BPF_MAXINSNS 4096
+#endif
+
+#endif /* __LINUX_BPF_COMMON_H__ */
diff --git a/contrib/libs/linux-headers/linux/filter.h b/contrib/libs/linux-headers/linux/filter.h
new file mode 100644
index 0000000000..eaef459e7b
--- /dev/null
+++ b/contrib/libs/linux-headers/linux/filter.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Linux Socket Filter Data Structures
+ */
+
+#ifndef __LINUX_FILTER_H__
+#define __LINUX_FILTER_H__
+
+
+#include <linux/types.h>
+#include <linux/bpf_common.h>
+
+/*
+ * Current version of the filter code architecture.
+ */
+#define BPF_MAJOR_VERSION 1
+#define BPF_MINOR_VERSION 1
+
+/*
+ * Try and keep these values and structures similar to BSD, especially
+ * the BPF code definitions which need to match so you can share filters
+ */
+
+struct sock_filter { /* Filter block */
+ __u16 code; /* Actual filter code */
+ __u8 jt; /* Jump true */
+ __u8 jf; /* Jump false */
+ __u32 k; /* Generic multiuse field */
+};
+
+struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
+ unsigned short len; /* Number of filter blocks */
+ struct sock_filter *filter;
+};
+
+/* ret - BPF_K and BPF_X also apply */
+#define BPF_RVAL(code) ((code) & 0x18)
+#define BPF_A 0x10
+
+/* misc */
+#define BPF_MISCOP(code) ((code) & 0xf8)
+#define BPF_TAX 0x00
+#define BPF_TXA 0x80
+
+/*
+ * Macros for filter block array initializers.
+ */
+#ifndef BPF_STMT
+#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k }
+#endif
+#ifndef BPF_JUMP
+#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k }
+#endif
+
+/*
+ * Number of scratch memory words for: BPF_ST and BPF_STX
+ */
+#define BPF_MEMWORDS 16
+
+/* RATIONALE. Negative offsets are invalid in BPF.
+ We use them to reference ancillary data.
+ Unlike introduction new instructions, it does not break
+ existing compilers/optimizers.
+ */
+#define SKF_AD_OFF (-0x1000)
+#define SKF_AD_PROTOCOL 0
+#define SKF_AD_PKTTYPE 4
+#define SKF_AD_IFINDEX 8
+#define SKF_AD_NLATTR 12
+#define SKF_AD_NLATTR_NEST 16
+#define SKF_AD_MARK 20
+#define SKF_AD_QUEUE 24
+#define SKF_AD_HATYPE 28
+#define SKF_AD_RXHASH 32
+#define SKF_AD_CPU 36
+#define SKF_AD_ALU_XOR_X 40
+#define SKF_AD_VLAN_TAG 44
+#define SKF_AD_VLAN_TAG_PRESENT 48
+#define SKF_AD_PAY_OFFSET 52
+#define SKF_AD_RANDOM 56
+#define SKF_AD_VLAN_TPID 60
+#define SKF_AD_MAX 64
+
+#define SKF_NET_OFF (-0x100000)
+#define SKF_LL_OFF (-0x200000)
+
+#define BPF_NET_OFF SKF_NET_OFF
+#define BPF_LL_OFF SKF_LL_OFF
+
+#endif /* __LINUX_FILTER_H__ */
diff --git a/contrib/libs/linux-headers/linux/hw_breakpoint.h b/contrib/libs/linux-headers/linux/hw_breakpoint.h
new file mode 100644
index 0000000000..769a38bb70
--- /dev/null
+++ b/contrib/libs/linux-headers/linux/hw_breakpoint.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_HW_BREAKPOINT_H
+#define _LINUX_HW_BREAKPOINT_H
+
+enum {
+ HW_BREAKPOINT_LEN_1 = 1,
+ HW_BREAKPOINT_LEN_2 = 2,
+ HW_BREAKPOINT_LEN_3 = 3,
+ HW_BREAKPOINT_LEN_4 = 4,
+ HW_BREAKPOINT_LEN_5 = 5,
+ HW_BREAKPOINT_LEN_6 = 6,
+ HW_BREAKPOINT_LEN_7 = 7,
+ HW_BREAKPOINT_LEN_8 = 8,
+};
+
+enum {
+ HW_BREAKPOINT_EMPTY = 0,
+ HW_BREAKPOINT_R = 1,
+ HW_BREAKPOINT_W = 2,
+ HW_BREAKPOINT_RW = HW_BREAKPOINT_R | HW_BREAKPOINT_W,
+ HW_BREAKPOINT_X = 4,
+ HW_BREAKPOINT_INVALID = HW_BREAKPOINT_RW | HW_BREAKPOINT_X,
+};
+
+enum bp_type_idx {
+ TYPE_INST = 0,
+#ifdef CONFIG_HAVE_MIXED_BREAKPOINTS_REGS
+ TYPE_DATA = 0,
+#else
+ TYPE_DATA = 1,
+#endif
+ TYPE_MAX
+};
+
+#endif /* _LINUX_HW_BREAKPOINT_H */
diff --git a/contrib/libs/pfr/LICENSE_1_0.txt b/contrib/libs/pfr/LICENSE_1_0.txt
new file mode 100644
index 0000000000..36b7cd93cd
--- /dev/null
+++ b/contrib/libs/pfr/LICENSE_1_0.txt
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/contrib/libs/pfr/README.md b/contrib/libs/pfr/README.md
new file mode 100644
index 0000000000..0a83f4b983
--- /dev/null
+++ b/contrib/libs/pfr/README.md
@@ -0,0 +1,104 @@
+# [PFR](https://apolukhin.github.io/pfr_non_boost/)
+
+This is a C++14 library for very basic reflection that gives you access to structure elements by index and provides other `std::tuple` like methods for user defined types without any macro or boilerplate code.
+
+[Boost.PFR](https://boost.org/libs/pfr) is a part of the [Boost C++ Libraries](https://github.com/boostorg). However, Boost.PFR is a header only library that does not depend on Boost. You can just copy the content of the "include" folder from the github into your project, and the library will work fine.
+
+For a version of the library without `boost::` namespace see [PFR](https://github.com/apolukhin/pfr_non_boost).
+
+### Test results
+
+Branches | Build | Tests coverage | More info
+----------------|-------------- | -------------- |-----------
+Develop: | [![CI](https://github.com/boostorg/pfr/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/boostorg/pfr/actions/workflows/ci.yml) [![Build status](https://ci.appveyor.com/api/projects/status/0mavmnkdmltcdmqa/branch/develop?svg=true)](https://ci.appveyor.com/project/apolukhin/pfr/branch/develop) | [![Coverage Status](https://coveralls.io/repos/github/apolukhin/magic_get/badge.png?branch=develop)](https://coveralls.io/github/apolukhin/magic_get?branch=develop) | [details...](https://www.boost.org/development/tests/develop/developer/pfr.html)
+Master: | [![CI](https://github.com/boostorg/pfr/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/boostorg/pfr/actions/workflows/ci.yml) [![Build status](https://ci.appveyor.com/api/projects/status/0mavmnkdmltcdmqa/branch/master?svg=true)](https://ci.appveyor.com/project/apolukhin/pfr/branch/master) | [![Coverage Status](https://coveralls.io/repos/github/apolukhin/magic_get/badge.png?branch=master)](https://coveralls.io/github/apolukhin/magic_get?branch=master) | [details...](https://www.boost.org/development/tests/master/developer/pfr.html)
+
+[Latest developer documentation](https://www.boost.org/doc/libs/develop/doc/html/boost_pfr.html)
+
+### Motivating Example #0
+```c++
+#include <iostream>
+#include <fstream>
+#include <string>
+
+#include "pfr.hpp"
+
+struct some_person {
+ std::string name;
+ unsigned birth_year;
+};
+
+int main(int argc, const char* argv[]) {
+ some_person val{"Edgar Allan Poe", 1809};
+
+ std::cout << pfr::get<0>(val) // No macro!
+ << " was born in " << pfr::get<1>(val); // Works with any aggregate initializables!
+
+ if (argc > 1) {
+ std::ofstream ofs(argv[1]);
+ ofs << pfr::io(val); // File now contains: {"Edgar Allan Poe", 1809}
+ }
+}
+```
+Outputs:
+```
+Edgar Allan Poe was born in 1809
+```
+
+
+### Motivating Example #1
+```c++
+#include <iostream>
+#include "pfr/precise.hpp"
+
+struct my_struct { // no ostream operator defined!
+ int i;
+ char c;
+ double d;
+};
+
+int main() {
+ my_struct s{100, 'H', 3.141593};
+ std::cout << "my_struct has " << pfr::tuple_size<my_struct>::value
+ << " fields: " << pfr::io(s) << "\n";
+}
+
+```
+
+Outputs:
+```
+my_struct has 3 fields: {100, H, 3.14159}
+```
+
+### Motivating Example #2
+
+```c++
+#include <iostream>
+#include "pfr/precise.hpp"
+
+struct my_struct { // no ostream operator defined!
+ std::string s;
+ int i;
+};
+
+int main() {
+ my_struct s{{"Das ist fantastisch!"}, 100};
+ std::cout << "my_struct has " << pfr::tuple_size<my_struct>::value
+ << " fields: " << pfr::io(s) << "\n";
+}
+
+```
+
+Outputs:
+```
+my_struct has 2 fields: {"Das ist fantastisch!", 100}
+```
+
+
+### Requirements and Limitations
+
+[See docs](https://www.boost.org/doc/libs/develop/doc/html/boost_pfr.html).
+
+### License
+
+Distributed under the [Boost Software License, Version 1.0](https://boost.org/LICENSE_1_0.txt).
diff --git a/contrib/libs/pfr/include/pfr/detail/config.hpp b/contrib/libs/pfr/include/pfr/detail/config.hpp
new file mode 100644
index 0000000000..ec2143d9ba
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/config.hpp
@@ -0,0 +1,91 @@
+// Copyright (c) 2016-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_CONFIG_HPP
+#define PFR_DETAIL_CONFIG_HPP
+#pragma once
+
+#include <type_traits> // to get non standard platform macro definitions (__GLIBCXX__ for example)
+
+// Reminder:
+// * MSVC++ 14.2 _MSC_VER == 1927 <- Loophole is known to work (Visual Studio ????)
+// * MSVC++ 14.1 _MSC_VER == 1916 <- Loophole is known to NOT work (Visual Studio 2017)
+// * MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015)
+// * MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013)
+
+#if defined(_MSC_VER)
+# if !defined(_MSVC_LANG) || _MSC_VER <= 1900
+# error Boost.PFR library requires more modern MSVC compiler.
+# endif
+#elif __cplusplus < 201402L
+# error Boost.PFR library requires at least C++14.
+#endif
+
+#ifndef PFR_USE_LOOPHOLE
+# if defined(_MSC_VER)
+# if _MSC_VER >= 1927
+# define PFR_USE_LOOPHOLE 1
+# else
+# define PFR_USE_LOOPHOLE 0
+# endif
+# elif defined(__clang_major__) && __clang_major__ >= 8
+# define PFR_USE_LOOPHOLE 0
+# else
+# define PFR_USE_LOOPHOLE 1
+# endif
+#endif
+
+#ifndef PFR_USE_CPP17
+# ifdef __cpp_structured_bindings
+# define PFR_USE_CPP17 1
+# elif defined(_MSVC_LANG)
+# if _MSVC_LANG >= 201703L
+# define PFR_USE_CPP17 1
+# else
+# define PFR_USE_CPP17 0
+# endif
+# else
+# define PFR_USE_CPP17 0
+# endif
+#endif
+
+#if (!PFR_USE_CPP17 && !PFR_USE_LOOPHOLE)
+# if (defined(_MSC_VER) && _MSC_VER < 1916) ///< in Visual Studio 2017 v15.9 PFR library with classic engine normally works
+# error Boost.PFR requires /std:c++latest or /std:c++17 flags on your compiler.
+# endif
+#endif
+
+#ifndef PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE
+// Assume that libstdc++ since GCC-7.3 does not have linear instantiation depth in std::make_integral_sequence
+# if defined( __GLIBCXX__) && __GLIBCXX__ >= 20180101
+# define PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE 1
+# elif defined(_MSC_VER)
+# define PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE 1
+//# elif other known working lib
+# else
+# define PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE 0
+# endif
+#endif
+
+#ifndef PFR_HAS_GUARANTEED_COPY_ELISION
+# if defined(__cpp_guaranteed_copy_elision) && (!defined(_MSC_VER) || _MSC_VER > 1928)
+# define PFR_HAS_GUARANTEED_COPY_ELISION 1
+# else
+# define PFR_HAS_GUARANTEED_COPY_ELISION 0
+# endif
+#endif
+
+#if defined(__has_cpp_attribute)
+# if __has_cpp_attribute(maybe_unused)
+# define PFR_MAYBE_UNUSED [[maybe_unused]]
+# endif
+#endif
+
+#ifndef PFR_MAYBE_UNUSED
+# define PFR_MAYBE_UNUSED
+#endif
+
+
+#endif // PFR_DETAIL_CONFIG_HPP
diff --git a/contrib/libs/pfr/include/pfr/detail/fields_count.hpp b/contrib/libs/pfr/include/pfr/detail/fields_count.hpp
new file mode 100644
index 0000000000..d4ad82353a
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/fields_count.hpp
@@ -0,0 +1,330 @@
+// Copyright (c) 2016-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_FIELDS_COUNT_HPP
+#define PFR_DETAIL_FIELDS_COUNT_HPP
+#pragma once
+
+#include <pfr/detail/config.hpp>
+#include <pfr/detail/make_integer_sequence.hpp>
+#include <pfr/detail/size_t_.hpp>
+#include <pfr/detail/unsafe_declval.hpp>
+
+#include <climits> // CHAR_BIT
+#include <type_traits>
+#include <utility> // metaprogramming stuff
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wmissing-braces"
+# pragma clang diagnostic ignored "-Wundefined-inline"
+# pragma clang diagnostic ignored "-Wundefined-internal"
+# pragma clang diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+namespace pfr { namespace detail {
+
+///////////////////// Structure that can be converted to reference to anything
+struct ubiq_lref_constructor {
+ std::size_t ignore;
+ template <class Type> constexpr operator Type&() const && noexcept { // tweak for template_unconstrained.cpp like cases
+ return detail::unsafe_declval<Type&>();
+ }
+
+ template <class Type> constexpr operator Type&() const & noexcept { // tweak for optional_chrono.cpp like cases
+ return detail::unsafe_declval<Type&>();
+ }
+};
+
+///////////////////// Structure that can be converted to rvalue reference to anything
+struct ubiq_rref_constructor {
+ std::size_t ignore;
+ template <class Type> /*constexpr*/ operator Type() const && noexcept { // Allows initialization of rvalue reference fields and move-only types
+ return detail::unsafe_declval<Type>();
+ }
+};
+
+
+#ifndef __cpp_lib_is_aggregate
+///////////////////// Hand-made is_aggregate_initializable_n<T> trait
+
+// Structure that can be converted to reference to anything except reference to T
+template <class T, bool IsCopyConstructible>
+struct ubiq_constructor_except {
+ std::size_t ignore;
+ template <class Type> constexpr operator std::enable_if_t<!std::is_same<T, Type>::value, Type&> () const noexcept; // Undefined
+};
+
+template <class T>
+struct ubiq_constructor_except<T, false> {
+ std::size_t ignore;
+ template <class Type> constexpr operator std::enable_if_t<!std::is_same<T, Type>::value, Type&&> () const noexcept; // Undefined
+};
+
+
+// `std::is_constructible<T, ubiq_constructor_except<T>>` consumes a lot of time, so we made a separate lazy trait for it.
+template <std::size_t N, class T> struct is_single_field_and_aggregate_initializable: std::false_type {};
+template <class T> struct is_single_field_and_aggregate_initializable<1, T>: std::integral_constant<
+ bool, !std::is_constructible<T, ubiq_constructor_except<T, std::is_copy_constructible<T>::value>>::value
+> {};
+
+// Hand-made is_aggregate<T> trait:
+// Before C++20 aggregates could be constructed from `decltype(ubiq_?ref_constructor{I})...` but type traits report that
+// there's no constructor from `decltype(ubiq_?ref_constructor{I})...`
+// Special case for N == 1: `std::is_constructible<T, ubiq_?ref_constructor>` returns true if N == 1 and T is copy/move constructible.
+template <class T, std::size_t N>
+struct is_aggregate_initializable_n {
+ template <std::size_t ...I>
+ static constexpr bool is_not_constructible_n(std::index_sequence<I...>) noexcept {
+ return (!std::is_constructible<T, decltype(ubiq_lref_constructor{I})...>::value && !std::is_constructible<T, decltype(ubiq_rref_constructor{I})...>::value)
+ || is_single_field_and_aggregate_initializable<N, T>::value
+ ;
+ }
+
+ static constexpr bool value =
+ std::is_empty<T>::value
+ || std::is_array<T>::value
+ || std::is_fundamental<T>::value
+ || is_not_constructible_n(detail::make_index_sequence<N>{})
+ ;
+};
+
+#endif // #ifndef __cpp_lib_is_aggregate
+
+///////////////////// Detect aggregates with inheritance
+template <class Derived, class U>
+constexpr bool static_assert_non_inherited() noexcept {
+ static_assert(
+ !std::is_base_of<U, Derived>::value,
+ "====================> Boost.PFR: Boost.PFR: Inherited types are not supported."
+ );
+ return true;
+}
+
+template <class Derived>
+struct ubiq_lref_base_asserting {
+ template <class Type> constexpr operator Type&() const && // tweak for template_unconstrained.cpp like cases
+ noexcept(detail::static_assert_non_inherited<Derived, Type>()) // force the computation of assert function
+ {
+ return detail::unsafe_declval<Type&>();
+ }
+
+ template <class Type> constexpr operator Type&() const & // tweak for optional_chrono.cpp like cases
+ noexcept(detail::static_assert_non_inherited<Derived, Type>()) // force the computation of assert function
+ {
+ return detail::unsafe_declval<Type&>();
+ }
+};
+
+template <class Derived>
+struct ubiq_rref_base_asserting {
+ template <class Type> /*constexpr*/ operator Type() const && // Allows initialization of rvalue reference fields and move-only types
+ noexcept(detail::static_assert_non_inherited<Derived, Type>()) // force the computation of assert function
+ {
+ return detail::unsafe_declval<Type>();
+ }
+};
+
+template <class T, std::size_t I0, std::size_t... I, class /*Enable*/ = typename std::enable_if<std::is_copy_constructible<T>::value>::type>
+constexpr auto assert_first_not_base(std::index_sequence<I0, I...>) noexcept
+ -> typename std::add_pointer<decltype(T{ ubiq_lref_base_asserting<T>{}, ubiq_lref_constructor{I}... })>::type
+{
+ return nullptr;
+}
+
+template <class T, std::size_t I0, std::size_t... I, class /*Enable*/ = typename std::enable_if<!std::is_copy_constructible<T>::value>::type>
+constexpr auto assert_first_not_base(std::index_sequence<I0, I...>) noexcept
+ -> typename std::add_pointer<decltype(T{ ubiq_rref_base_asserting<T>{}, ubiq_rref_constructor{I}... })>::type
+{
+ return nullptr;
+}
+
+template <class T>
+constexpr void* assert_first_not_base(std::index_sequence<>) noexcept
+{
+ return nullptr;
+}
+
+///////////////////// Helper for SFINAE on fields count
+template <class T, std::size_t... I, class /*Enable*/ = typename std::enable_if<std::is_copy_constructible<T>::value>::type>
+constexpr auto enable_if_constructible_helper(std::index_sequence<I...>) noexcept
+ -> typename std::add_pointer<decltype(T{ ubiq_lref_constructor{I}... })>::type;
+
+template <class T, std::size_t... I, class /*Enable*/ = typename std::enable_if<!std::is_copy_constructible<T>::value>::type>
+constexpr auto enable_if_constructible_helper(std::index_sequence<I...>) noexcept
+ -> typename std::add_pointer<decltype(T{ ubiq_rref_constructor{I}... })>::type;
+
+template <class T, std::size_t N, class /*Enable*/ = decltype( enable_if_constructible_helper<T>(detail::make_index_sequence<N>()) ) >
+using enable_if_constructible_helper_t = std::size_t;
+
+///////////////////// Helpers for range size detection
+template <std::size_t Begin, std::size_t Last>
+using is_one_element_range = std::integral_constant<bool, Begin == Last>;
+
+using multi_element_range = std::false_type;
+using one_element_range = std::true_type;
+
+///////////////////// Non greedy fields count search. Templates instantiation depth is log(sizeof(T)), templates instantiation count is log(sizeof(T)).
+template <class T, std::size_t Begin, std::size_t Middle>
+constexpr std::size_t detect_fields_count(detail::one_element_range, long) noexcept {
+ static_assert(
+ Begin == Middle,
+ "====================> Boost.PFR: Internal logic error."
+ );
+ return Begin;
+}
+
+template <class T, std::size_t Begin, std::size_t Middle>
+constexpr std::size_t detect_fields_count(detail::multi_element_range, int) noexcept;
+
+template <class T, std::size_t Begin, std::size_t Middle>
+constexpr auto detect_fields_count(detail::multi_element_range, long) noexcept
+ -> detail::enable_if_constructible_helper_t<T, Middle>
+{
+ constexpr std::size_t next_v = Middle + (Middle - Begin + 1) / 2;
+ return detail::detect_fields_count<T, Middle, next_v>(detail::is_one_element_range<Middle, next_v>{}, 1L);
+}
+
+template <class T, std::size_t Begin, std::size_t Middle>
+constexpr std::size_t detect_fields_count(detail::multi_element_range, int) noexcept {
+ constexpr std::size_t next_v = Begin + (Middle - Begin) / 2;
+ return detail::detect_fields_count<T, Begin, next_v>(detail::is_one_element_range<Begin, next_v>{}, 1L);
+}
+
+///////////////////// Greedy search. Templates instantiation depth is log(sizeof(T)), templates instantiation count is log(sizeof(T))*T in worst case.
+template <class T, std::size_t N>
+constexpr auto detect_fields_count_greedy_remember(long) noexcept
+ -> detail::enable_if_constructible_helper_t<T, N>
+{
+ return N;
+}
+
+template <class T, std::size_t N>
+constexpr std::size_t detect_fields_count_greedy_remember(int) noexcept {
+ return 0;
+}
+
+template <class T, std::size_t Begin, std::size_t Last>
+constexpr std::size_t detect_fields_count_greedy(detail::one_element_range) noexcept {
+ static_assert(
+ Begin == Last,
+ "====================> Boost.PFR: Internal logic error."
+ );
+ return detail::detect_fields_count_greedy_remember<T, Begin>(1L);
+}
+
+template <class T, std::size_t Begin, std::size_t Last>
+constexpr std::size_t detect_fields_count_greedy(detail::multi_element_range) noexcept {
+ constexpr std::size_t middle = Begin + (Last - Begin) / 2;
+ constexpr std::size_t fields_count_big_range = detail::detect_fields_count_greedy<T, middle + 1, Last>(
+ detail::is_one_element_range<middle + 1, Last>{}
+ );
+
+ constexpr std::size_t small_range_begin = (fields_count_big_range ? 0 : Begin);
+ constexpr std::size_t small_range_last = (fields_count_big_range ? 0 : middle);
+ constexpr std::size_t fields_count_small_range = detail::detect_fields_count_greedy<T, small_range_begin, small_range_last>(
+ detail::is_one_element_range<small_range_begin, small_range_last>{}
+ );
+ return fields_count_big_range ? fields_count_big_range : fields_count_small_range;
+}
+
+///////////////////// Choosing between array size, greedy and non greedy search.
+template <class T, std::size_t N>
+constexpr auto detect_fields_count_dispatch(size_t_<N>, long, long) noexcept
+ -> typename std::enable_if<std::is_array<T>::value, std::size_t>::type
+{
+ return sizeof(T) / sizeof(typename std::remove_all_extents<T>::type);
+}
+
+template <class T, std::size_t N>
+constexpr auto detect_fields_count_dispatch(size_t_<N>, long, int) noexcept
+ -> decltype(sizeof(T{}))
+{
+ constexpr std::size_t middle = N / 2 + 1;
+ return detail::detect_fields_count<T, 0, middle>(detail::multi_element_range{}, 1L);
+}
+
+template <class T, std::size_t N>
+constexpr std::size_t detect_fields_count_dispatch(size_t_<N>, int, int) noexcept {
+ // T is not default aggregate initialzable. It means that at least one of the members is not default constructible,
+ // so we have to check all the aggregate initializations for T up to N parameters and return the bigest succeeded
+ // (we can not use binary search for detecting fields count).
+ return detail::detect_fields_count_greedy<T, 0, N>(detail::multi_element_range{});
+}
+
+///////////////////// Returns fields count
+template <class T>
+constexpr std::size_t fields_count() noexcept {
+ using type = std::remove_cv_t<T>;
+
+ static_assert(
+ !std::is_reference<type>::value,
+ "====================> Boost.PFR: Attempt to get fields count on a reference. This is not allowed because that could hide an issue and different library users expect different behavior in that case."
+ );
+
+#if !PFR_HAS_GUARANTEED_COPY_ELISION
+ static_assert(
+ std::is_copy_constructible<std::remove_all_extents_t<type>>::value || (
+ std::is_move_constructible<std::remove_all_extents_t<type>>::value
+ && std::is_move_assignable<std::remove_all_extents_t<type>>::value
+ ),
+ "====================> Boost.PFR: Type and each field in the type must be copy constructible (or move constructible and move assignable)."
+ );
+#endif // #if !PFR_HAS_GUARANTEED_COPY_ELISION
+
+ static_assert(
+ !std::is_polymorphic<type>::value,
+ "====================> Boost.PFR: Type must have no virtual function, because otherwise it is not aggregate initializable."
+ );
+
+#ifdef __cpp_lib_is_aggregate
+ static_assert(
+ std::is_aggregate<type>::value // Does not return `true` for built-in types.
+ || std::is_scalar<type>::value,
+ "====================> Boost.PFR: Type must be aggregate initializable."
+ );
+#endif
+
+// Can't use the following. See the non_std_layout.cpp test.
+//#if !PFR_USE_CPP17
+// static_assert(
+// std::is_standard_layout<type>::value, // Does not return `true` for structs that have non standard layout members.
+// "Type must be aggregate initializable."
+// );
+//#endif
+
+#if defined(_MSC_VER) && (_MSC_VER <= 1920)
+ // Workaround for msvc compilers. Versions <= 1920 have a limit of max 1024 elements in template parameter pack
+ constexpr std::size_t max_fields_count = (sizeof(type) * CHAR_BIT >= 1024 ? 1024 : sizeof(type) * CHAR_BIT);
+#else
+ constexpr std::size_t max_fields_count = (sizeof(type) * CHAR_BIT); // We multiply by CHAR_BIT because the type may have bitfields in T
+#endif
+
+ constexpr std::size_t result = detail::detect_fields_count_dispatch<type>(size_t_<max_fields_count>{}, 1L, 1L);
+
+ detail::assert_first_not_base<type>(detail::make_index_sequence<result>{});
+
+#ifndef __cpp_lib_is_aggregate
+ static_assert(
+ is_aggregate_initializable_n<type, result>::value,
+ "====================> Boost.PFR: Types with user specified constructors (non-aggregate initializable types) are not supported."
+ );
+#endif
+
+ static_assert(
+ result != 0 || std::is_empty<type>::value || std::is_fundamental<type>::value || std::is_reference<type>::value,
+ "====================> Boost.PFR: If there's no other failed static asserts then something went wrong. Please report this issue to the github along with the structure you're reflecting."
+ );
+
+ return result;
+}
+
+}} // namespace pfr::detail
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#endif // PFR_DETAIL_FIELDS_COUNT_HPP
diff --git a/contrib/libs/pfr/include/pfr/detail/make_integer_sequence.hpp b/contrib/libs/pfr/include/pfr/detail/make_integer_sequence.hpp
new file mode 100644
index 0000000000..b1af047d2f
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/make_integer_sequence.hpp
@@ -0,0 +1,90 @@
+// Copyright (c) 2018 Sergei Fedorov
+// Copyright (c) 2019-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_MAKE_INTEGER_SEQUENCE_HPP
+#define PFR_DETAIL_MAKE_INTEGER_SEQUENCE_HPP
+#pragma once
+
+#include <pfr/detail/config.hpp>
+
+#include <type_traits>
+#include <utility>
+#include <cstddef>
+
+namespace pfr { namespace detail {
+
+#if PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE == 0
+
+#ifdef __has_builtin
+# if __has_builtin(__make_integer_seq)
+# define PFR_USE_MAKE_INTEGER_SEQ_BUILTIN
+# endif
+#endif
+
+#ifdef PFR_USE_MAKE_INTEGER_SEQ_BUILTIN
+
+using std::integer_sequence;
+
+// Clang unable to use namespace qualified std::integer_sequence in __make_integer_seq.
+template <typename T, T N>
+using make_integer_sequence = __make_integer_seq<integer_sequence, T, N>;
+
+#undef PFR_USE_MAKE_INTEGER_SEQ_BUILTIN
+
+#else
+
+template <typename T, typename U>
+struct join_sequences;
+
+template <typename T, T... A, T... B>
+struct join_sequences<std::integer_sequence<T, A...>, std::integer_sequence<T, B...>> {
+ using type = std::integer_sequence<T, A..., B...>;
+};
+
+template <typename T, T Min, T Max>
+struct build_sequence_impl {
+ static_assert(Min < Max, "Start of range must be less than its end");
+ static constexpr T size = Max - Min;
+ using type = typename join_sequences<
+ typename build_sequence_impl<T, Min, Min + size / 2>::type,
+ typename build_sequence_impl<T, Min + size / 2 + 1, Max>::type
+ >::type;
+};
+
+template <typename T, T V>
+struct build_sequence_impl<T, V, V> {
+ using type = std::integer_sequence<T, V>;
+};
+
+template <typename T, std::size_t N>
+struct make_integer_sequence_impl : build_sequence_impl<T, 0, N - 1> {};
+
+template <typename T>
+struct make_integer_sequence_impl<T, 0> {
+ using type = std::integer_sequence<T>;
+};
+
+template <typename T, T N>
+using make_integer_sequence = typename make_integer_sequence_impl<T, N>::type;
+
+#endif // !defined PFR_USE_MAKE_INTEGER_SEQ_BUILTIN
+#else // PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE == 1
+
+template <typename T, T N>
+using make_integer_sequence = std::make_integer_sequence<T, N>;
+
+#endif // PFR_USE_STD_MAKE_INTEGRAL_SEQUENCE == 1
+
+template <std::size_t N>
+using make_index_sequence = make_integer_sequence<std::size_t, N>;
+
+template <typename... T>
+using index_sequence_for = make_index_sequence<sizeof...(T)>;
+
+}} // namespace pfr::detail
+
+#endif
+
diff --git a/contrib/libs/pfr/include/pfr/detail/sequence_tuple.hpp b/contrib/libs/pfr/include/pfr/detail/sequence_tuple.hpp
new file mode 100644
index 0000000000..3ccd130914
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/sequence_tuple.hpp
@@ -0,0 +1,127 @@
+// Copyright (c) 2016-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_SEQUENCE_TUPLE_HPP
+#define PFR_DETAIL_SEQUENCE_TUPLE_HPP
+#pragma once
+
+#include <pfr/detail/config.hpp>
+#include <pfr/detail/make_integer_sequence.hpp>
+
+#include <utility> // metaprogramming stuff
+#include <cstddef> // std::size_t
+
+///////////////////// Tuple that holds its values in the supplied order
+namespace pfr { namespace detail { namespace sequence_tuple {
+
+template <std::size_t N, class T>
+struct base_from_member {
+ T value;
+};
+
+template <class I, class ...Tail>
+struct tuple_base;
+
+
+
+template <std::size_t... I, class ...Tail>
+struct tuple_base< std::index_sequence<I...>, Tail... >
+ : base_from_member<I , Tail>...
+{
+ static constexpr std::size_t size_v = sizeof...(I);
+
+ // We do not use `noexcept` in the following functions, because if user forget to put one then clang will issue an error:
+ // "error: exception specification of explicitly defaulted default constructor does not match the calculated one".
+ constexpr tuple_base() = default;
+ constexpr tuple_base(tuple_base&&) = default;
+ constexpr tuple_base(const tuple_base&) = default;
+
+ constexpr tuple_base(Tail... v) noexcept
+ : base_from_member<I, Tail>{ v }...
+ {}
+};
+
+template <>
+struct tuple_base<std::index_sequence<> > {
+ static constexpr std::size_t size_v = 0;
+};
+
+template <std::size_t N, class T>
+constexpr T& get_impl(base_from_member<N, T>& t) noexcept {
+ return t.value;
+}
+
+template <std::size_t N, class T>
+constexpr const T& get_impl(const base_from_member<N, T>& t) noexcept {
+ return t.value;
+}
+
+template <std::size_t N, class T>
+constexpr volatile T& get_impl(volatile base_from_member<N, T>& t) noexcept {
+ return t.value;
+}
+
+template <std::size_t N, class T>
+constexpr const volatile T& get_impl(const volatile base_from_member<N, T>& t) noexcept {
+ return t.value;
+}
+
+template <std::size_t N, class T>
+constexpr T&& get_impl(base_from_member<N, T>&& t) noexcept {
+ return std::forward<T>(t.value);
+}
+
+
+template <class ...Values>
+struct tuple: tuple_base<
+ detail::index_sequence_for<Values...>,
+ Values...>
+{
+ using tuple_base<
+ detail::index_sequence_for<Values...>,
+ Values...
+ >::tuple_base;
+};
+
+
+template <std::size_t N, class ...T>
+constexpr decltype(auto) get(tuple<T...>& t) noexcept {
+ static_assert(N < tuple<T...>::size_v, "====================> Boost.PFR: Tuple index out of bounds");
+ return sequence_tuple::get_impl<N>(t);
+}
+
+template <std::size_t N, class ...T>
+constexpr decltype(auto) get(const tuple<T...>& t) noexcept {
+ static_assert(N < tuple<T...>::size_v, "====================> Boost.PFR: Tuple index out of bounds");
+ return sequence_tuple::get_impl<N>(t);
+}
+
+template <std::size_t N, class ...T>
+constexpr decltype(auto) get(const volatile tuple<T...>& t) noexcept {
+ static_assert(N < tuple<T...>::size_v, "====================> Boost.PFR: Tuple index out of bounds");
+ return sequence_tuple::get_impl<N>(t);
+}
+
+template <std::size_t N, class ...T>
+constexpr decltype(auto) get(volatile tuple<T...>& t) noexcept {
+ static_assert(N < tuple<T...>::size_v, "====================> Boost.PFR: Tuple index out of bounds");
+ return sequence_tuple::get_impl<N>(t);
+}
+
+template <std::size_t N, class ...T>
+constexpr decltype(auto) get(tuple<T...>&& t) noexcept {
+ static_assert(N < tuple<T...>::size_v, "====================> Boost.PFR: Tuple index out of bounds");
+ return sequence_tuple::get_impl<N>(std::move(t));
+}
+
+template <std::size_t I, class T>
+using tuple_element = std::remove_reference< decltype(
+ ::pfr::detail::sequence_tuple::get<I>( std::declval<T>() )
+ ) >;
+
+
+}}} // namespace pfr::detail::sequence_tuple
+
+#endif // PFR_CORE_HPP
diff --git a/contrib/libs/pfr/include/pfr/detail/size_t_.hpp b/contrib/libs/pfr/include/pfr/detail/size_t_.hpp
new file mode 100644
index 0000000000..da54096b0e
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/size_t_.hpp
@@ -0,0 +1,18 @@
+// Copyright (c) 2016-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_SIZE_T_HPP
+#define PFR_DETAIL_SIZE_T_HPP
+#pragma once
+
+namespace pfr { namespace detail {
+
+///////////////////// General utility stuff
+template <std::size_t Index>
+using size_t_ = std::integral_constant<std::size_t, Index >;
+
+}} // namespace pfr::detail
+
+#endif // PFR_DETAIL_SIZE_T_HPP
diff --git a/contrib/libs/pfr/include/pfr/detail/unsafe_declval.hpp b/contrib/libs/pfr/include/pfr/detail/unsafe_declval.hpp
new file mode 100644
index 0000000000..3742abcd2a
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/detail/unsafe_declval.hpp
@@ -0,0 +1,36 @@
+// Copyright (c) 2019-2021 Antony Polukhin.
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef PFR_DETAIL_UNSAFE_DECLVAL_HPP
+#define PFR_DETAIL_UNSAFE_DECLVAL_HPP
+
+#include <pfr/detail/config.hpp>
+
+#include <type_traits>
+
+namespace pfr { namespace detail {
+
+// This function serves as a link-time assert. If linker requires it, then
+// `unsafe_declval()` is used at runtime.
+void report_if_you_see_link_error_with_this_function() noexcept;
+
+// For returning non default constructible types. Do NOT use at runtime!
+//
+// GCCs std::declval may not be used in potentionally evaluated contexts,
+// so we reinvent it.
+template <class T>
+constexpr T unsafe_declval() noexcept {
+ report_if_you_see_link_error_with_this_function();
+
+ typename std::remove_reference<T>::type* ptr = 0;
+ ptr += 42; // suppresses 'null pointer dereference' warnings
+ return static_cast<T>(*ptr);
+}
+
+}} // namespace pfr::detail
+
+
+#endif // PFR_DETAIL_UNSAFE_DECLVAL_HPP
+
diff --git a/contrib/libs/pfr/include/pfr/tuple_size.hpp b/contrib/libs/pfr/include/pfr/tuple_size.hpp
new file mode 100644
index 0000000000..7ad8f08993
--- /dev/null
+++ b/contrib/libs/pfr/include/pfr/tuple_size.hpp
@@ -0,0 +1,48 @@
+// Copyright (c) 2016-2021 Antony Polukhin
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+
+#ifndef PFR_TUPLE_SIZE_HPP
+#define PFR_TUPLE_SIZE_HPP
+#pragma once
+
+#include <pfr/detail/config.hpp>
+
+#include <type_traits>
+#include <utility> // metaprogramming stuff
+
+#include <pfr/detail/sequence_tuple.hpp>
+#include <pfr/detail/fields_count.hpp>
+
+/// \file pfr/tuple_size.hpp
+/// Contains tuple-like interfaces to get fields count \forcedlink{tuple_size}, \forcedlink{tuple_size_v}.
+///
+/// \b Synopsis:
+namespace pfr {
+
+/// Has a static const member variable `value` that contains fields count in a T.
+/// Works for any T that supports aggregate initialization.
+///
+/// \b Example:
+/// \code
+/// std::array<int, pfr::tuple_size<my_structure>::value > a;
+/// \endcode
+template <class T>
+using tuple_size = detail::size_t_< pfr::detail::fields_count<T>() >;
+
+
+/// `tuple_size_v` is a template variable that contains fields count in a T and
+/// works for any T that supports aggregate initialization.
+///
+/// \b Example:
+/// \code
+/// std::array<int, pfr::tuple_size_v<my_structure> > a;
+/// \endcode
+template <class T>
+constexpr std::size_t tuple_size_v = tuple_size<T>::value;
+
+} // namespace pfr
+
+#endif // PFR_TUPLE_SIZE_HPP
diff --git a/contrib/libs/pfr/ya.make b/contrib/libs/pfr/ya.make
new file mode 100644
index 0000000000..cdf2bfde6c
--- /dev/null
+++ b/contrib/libs/pfr/ya.make
@@ -0,0 +1,20 @@
+# Generated by devtools/yamaker from nixpkgs 22.05.
+
+LIBRARY()
+
+LICENSE(
+ BSL-1.0 AND
+ Public-Domain
+)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+VERSION(2.0.3)
+
+ORIGINAL_SOURCE(https://github.com/apolukhin/pfr_non_boost/archive/2.0.3.tar.gz)
+
+ADDINCL(
+ GLOBAL contrib/libs/pfr/include
+)
+
+END()
diff --git a/contrib/libs/yajl/api/yajl_common.h b/contrib/libs/yajl/api/yajl_common.h
new file mode 100644
index 0000000000..2507576a1a
--- /dev/null
+++ b/contrib/libs/yajl/api/yajl_common.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_COMMON_H__
+#define __YAJL_COMMON_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define YAJL_MAX_DEPTH 1024
+
+/* msft dll export gunk. To build a DLL on windows, you
+ * must define WIN32, YAJL_SHARED, and YAJL_BUILD. To use a shared
+ * DLL, you must define YAJL_SHARED and WIN32 */
+#if (defined(_WIN32) || defined(WIN32)) && defined(YAJL_SHARED)
+# ifdef YAJL_BUILD
+# define YAJL_API __declspec(dllexport)
+# else
+# define YAJL_API __declspec(dllimport)
+# endif
+#else
+# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
+# define YAJL_API __attribute__ ((visibility("default")))
+# else
+# define YAJL_API
+# endif
+#endif
+
+/** pointer to a malloc function, supporting client overriding memory
+ * allocation routines */
+typedef void * (*yajl_malloc_func)(void *ctx, size_t sz);
+
+/** pointer to a free function, supporting client overriding memory
+ * allocation routines */
+typedef void (*yajl_free_func)(void *ctx, void * ptr);
+
+/** pointer to a realloc function which can resize an allocation. */
+typedef void * (*yajl_realloc_func)(void *ctx, void * ptr, size_t sz);
+
+/** A structure which can be passed to yajl_*_alloc routines to allow the
+ * client to specify memory allocation functions to be used. */
+typedef struct
+{
+ /** pointer to a function that can allocate uninitialized memory */
+ yajl_malloc_func malloc;
+ /** pointer to a function that can resize memory allocations */
+ yajl_realloc_func realloc;
+ /** pointer to a function that can free memory allocated using
+ * reallocFunction or mallocFunction */
+ yajl_free_func free;
+ /** a context pointer that will be passed to above allocation routines */
+ void * ctx;
+} yajl_alloc_funcs;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/contrib/libs/yajl/api/yajl_gen.h b/contrib/libs/yajl/api/yajl_gen.h
new file mode 100644
index 0000000000..fb1409df70
--- /dev/null
+++ b/contrib/libs/yajl/api/yajl_gen.h
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_gen.h
+ * Interface to YAJL's JSON generation facilities.
+ */
+
+#include "yajl_common.h"
+
+#ifndef __YAJL_GEN_H__
+#define __YAJL_GEN_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+ /** generator status codes */
+ typedef enum {
+ /** no error */
+ yajl_gen_status_ok = 0,
+ /** at a point where a map key is generated, a function other than
+ * yajl_gen_string was called */
+ yajl_gen_keys_must_be_strings,
+ /** YAJL's maximum generation depth was exceeded. see
+ * YAJL_MAX_DEPTH */
+ yajl_max_depth_exceeded,
+ /** A generator function (yajl_gen_XXX) was called while in an error
+ * state */
+ yajl_gen_in_error_state,
+ /** A complete JSON document has been generated */
+ yajl_gen_generation_complete,
+ /** yajl_gen_double was passed an invalid floating point value
+ * (infinity or NaN). */
+ yajl_gen_invalid_number,
+ /** A print callback was passed in, so there is no internal
+ * buffer to get from */
+ yajl_gen_no_buf,
+ /** returned from yajl_gen_string() when the yajl_gen_validate_utf8
+ * option is enabled and an invalid was passed by client code.
+ */
+ yajl_gen_invalid_string
+ } yajl_gen_status;
+
+ /** an opaque handle to a generator */
+ typedef struct yajl_gen_t * yajl_gen;
+
+ /** a callback used for "printing" the results. */
+ typedef void (*yajl_print_t)(void * ctx,
+ const char * str,
+ size_t len);
+
+ /** configuration parameters for the parser, these may be passed to
+ * yajl_gen_config() along with option specific argument(s). In general,
+ * all configuration parameters default to *off*. */
+ typedef enum {
+ /** generate indented (beautiful) output */
+ yajl_gen_beautify = 0x01,
+ /**
+ * Set an indent string which is used when yajl_gen_beautify
+ * is enabled. Maybe something like \\t or some number of
+ * spaces. The default is four spaces ' '.
+ */
+ yajl_gen_indent_string = 0x02,
+ /**
+ * Set a function and context argument that should be used to
+ * output generated json. the function should conform to the
+ * yajl_print_t prototype while the context argument is a
+ * void * of your choosing.
+ *
+ * example:
+ * yajl_gen_config(g, yajl_gen_print_callback, myFunc, myVoidPtr);
+ */
+ yajl_gen_print_callback = 0x04,
+ /**
+ * Normally the generator does not validate that strings you
+ * pass to it via yajl_gen_string() are valid UTF8. Enabling
+ * this option will cause it to do so.
+ */
+ yajl_gen_validate_utf8 = 0x08,
+ /**
+ * the forward solidus (slash or '/' in human) is not required to be
+ * escaped in json text. By default, YAJL will not escape it in the
+ * iterest of saving bytes. Setting this flag will cause YAJL to
+ * always escape '/' in generated JSON strings.
+ */
+ yajl_gen_escape_solidus = 0x10,
+ /**
+ * Disable yandex double format
+ */
+ yajl_gen_disable_yandex_double_format = 0x20,
+ /**
+ * do not print final newline '\n' in case of indented (beautiful) output
+ */
+ yajl_gen_skip_final_newline = 0x40,
+ /**
+ * enable printing infinity value
+ */
+ yajl_gen_support_infinity = 0x80
+ } yajl_gen_option;
+
+ /** allow the modification of generator options subsequent to handle
+ * allocation (via yajl_alloc)
+ * \returns zero in case of errors, non-zero otherwise
+ */
+ YAJL_API int yajl_gen_config(yajl_gen g, yajl_gen_option opt, ...);
+
+ /** allocate a generator handle
+ * \param allocFuncs an optional pointer to a structure which allows
+ * the client to overide the memory allocation
+ * used by yajl. May be NULL, in which case
+ * malloc/free/realloc will be used.
+ *
+ * \returns an allocated handle on success, NULL on failure (bad params)
+ */
+ YAJL_API yajl_gen yajl_gen_alloc(const yajl_alloc_funcs * allocFuncs);
+
+ /** free a generator handle */
+ YAJL_API void yajl_gen_free(yajl_gen handle);
+
+ YAJL_API yajl_gen_status yajl_gen_integer(yajl_gen hand, long long int number);
+ YAJL_API yajl_gen_status yajl_gen_uinteger(yajl_gen hand, long long unsigned number);
+ /** generate a floating point number. number may not be infinity or
+ * NaN, as these have no representation in JSON. In these cases the
+ * generator will return 'yajl_gen_invalid_number' */
+ YAJL_API yajl_gen_status yajl_gen_double(yajl_gen hand, double number);
+ YAJL_API yajl_gen_status yajl_gen_number(yajl_gen hand,
+ const char * num,
+ size_t len);
+ YAJL_API yajl_gen_status yajl_gen_string(yajl_gen hand,
+ const unsigned char * str,
+ size_t len);
+ YAJL_API yajl_gen_status yajl_gen_null(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_bool(yajl_gen hand, int boolean);
+ YAJL_API yajl_gen_status yajl_gen_map_open(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_map_close(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_array_open(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_array_close(yajl_gen hand);
+
+ /** access the null terminated generator buffer. If incrementally
+ * outputing JSON, one should call yajl_gen_clear to clear the
+ * buffer. This allows stream generation. */
+ YAJL_API yajl_gen_status yajl_gen_get_buf(yajl_gen hand,
+ const unsigned char ** buf,
+ size_t * len);
+
+ /** clear yajl's output buffer, but maintain all internal generation
+ * state. This function will not "reset" the generator state, and is
+ * intended to enable incremental JSON outputing. */
+ YAJL_API void yajl_gen_clear(yajl_gen hand);
+
+ /** Reset the generator state. Allows a client to generate multiple
+ * json entities in a stream. The "sep" string will be inserted to
+ * separate the previously generated entity from the current,
+ * NULL means *no separation* of entites (clients beware, generating
+ * multiple JSON numbers without a separator, for instance, will result in ambiguous output)
+ *
+ * Note: this call will not clear yajl's output buffer. This
+ * may be accomplished explicitly by calling yajl_gen_clear() */
+ YAJL_API void yajl_gen_reset(yajl_gen hand, const char * sep);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/contrib/libs/yajl/api/yajl_parse.h b/contrib/libs/yajl/api/yajl_parse.h
new file mode 100644
index 0000000000..36a89a52a4
--- /dev/null
+++ b/contrib/libs/yajl/api/yajl_parse.h
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_parse.h
+ * Interface to YAJL's JSON stream parsing facilities.
+ */
+
+#include "yajl_common.h"
+
+#ifndef __YAJL_PARSE_H__
+#define __YAJL_PARSE_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+ /** error codes returned from this interface */
+ typedef enum {
+ /** no error was encountered */
+ yajl_status_ok,
+ /** a client callback returned zero, stopping the parse */
+ yajl_status_client_canceled,
+ /** An error occured during the parse. Call yajl_get_error for
+ * more information about the encountered error */
+ yajl_status_error
+ } yajl_status;
+
+ /** attain a human readable, english, string for an error */
+ YAJL_API const char * yajl_status_to_string(yajl_status code);
+
+ /** an opaque handle to a parser */
+ typedef struct yajl_handle_t * yajl_handle;
+
+ /** yajl is an event driven parser. this means as json elements are
+ * parsed, you are called back to do something with the data. The
+ * functions in this table indicate the various events for which
+ * you will be called back. Each callback accepts a "context"
+ * pointer, this is a void * that is passed into the yajl_parse
+ * function which the client code may use to pass around context.
+ *
+ * All callbacks return an integer. If non-zero, the parse will
+ * continue. If zero, the parse will be canceled and
+ * yajl_status_client_canceled will be returned from the parse.
+ *
+ * \attention {
+ * A note about the handling of numbers:
+ *
+ * yajl will only convert numbers that can be represented in a
+ * double or a 64 bit (long long) int. All other numbers will
+ * be passed to the client in string form using the yajl_number
+ * callback. Furthermore, if yajl_number is not NULL, it will
+ * always be used to return numbers, that is yajl_integer,
+ * yajl_unsigned_integer and yajl_double will be ignored.
+ * If yajl_number is NULL but one of yajl_integer or
+ * yajl_double are defined, parsing of a number larger than
+ * is representable in a double or 64 bit integer will result
+ * in a parse error.
+ * }
+ */
+ typedef struct {
+ int (* yajl_null)(void * ctx);
+ int (* yajl_boolean)(void * ctx, int boolVal);
+ int (* yajl_integer)(void * ctx, long long integerVal);
+ int (* yajl_unsigned_integer)(void * ctx, unsigned long long integerVal);
+ int (* yajl_double)(void * ctx, double doubleVal);
+ /** A callback which passes the string representation of the number
+ * back to the client. Will be used for all numbers when present */
+ int (* yajl_number)(void * ctx, const char * numberVal,
+ size_t numberLen);
+
+ /** strings are returned as pointers into the JSON text when,
+ * possible, as a result, they are _not_ null padded */
+ int (* yajl_string)(void * ctx, const unsigned char * stringVal,
+ size_t stringLen);
+
+ int (* yajl_start_map)(void * ctx);
+ int (* yajl_map_key)(void * ctx, const unsigned char * key,
+ size_t stringLen);
+ int (* yajl_end_map)(void * ctx);
+
+ int (* yajl_start_array)(void * ctx);
+ int (* yajl_end_array)(void * ctx);
+ } yajl_callbacks;
+
+ /** allocate a parser handle
+ * \param callbacks a yajl callbacks structure specifying the
+ * functions to call when different JSON entities
+ * are encountered in the input text. May be NULL,
+ * which is only useful for validation.
+ * \param afs memory allocation functions, may be NULL for to use
+ * C runtime library routines (malloc and friends)
+ * \param ctx a context pointer that will be passed to callbacks.
+ */
+ YAJL_API yajl_handle yajl_alloc(const yajl_callbacks * callbacks,
+ yajl_alloc_funcs * afs,
+ void * ctx);
+
+
+ /** configuration parameters for the parser, these may be passed to
+ * yajl_config() along with option specific argument(s). In general,
+ * all configuration parameters default to *off*. */
+ typedef enum {
+ /** Ignore javascript style comments present in
+ * JSON input. Non-standard, but rather fun
+ * arguments: toggled off with integer zero, on otherwise.
+ *
+ * example:
+ * yajl_config(h, yajl_allow_comments, 1); // turn comment support on
+ */
+ yajl_allow_comments = 0x01,
+ /**
+ * When set the parser will verify that all strings in JSON input are
+ * valid UTF8 and will emit a parse error if this is not so. When set,
+ * this option makes parsing slightly more expensive (~7% depending
+ * on processor and compiler in use)
+ *
+ * example:
+ * yajl_config(h, yajl_dont_validate_strings, 1); // disable utf8 checking
+ */
+ yajl_dont_validate_strings = 0x02,
+ /**
+ * By default, upon calls to yajl_complete_parse(), yajl will
+ * ensure the entire input text was consumed and will raise an error
+ * otherwise. Enabling this flag will cause yajl to disable this
+ * check. This can be useful when parsing json out of a that contains more
+ * than a single JSON document.
+ */
+ yajl_allow_trailing_garbage = 0x04,
+ /**
+ * Allow multiple values to be parsed by a single handle. The
+ * entire text must be valid JSON, and values can be seperated
+ * by any kind of whitespace. This flag will change the
+ * behavior of the parser, and cause it continue parsing after
+ * a value is parsed, rather than transitioning into a
+ * complete state. This option can be useful when parsing multiple
+ * values from an input stream.
+ */
+ yajl_allow_multiple_values = 0x08,
+ /**
+ * When yajl_complete_parse() is called the parser will
+ * check that the top level value was completely consumed. I.E.,
+ * if called whilst in the middle of parsing a value
+ * yajl will enter an error state (premature EOF). Setting this
+ * flag suppresses that check and the corresponding error.
+ */
+ yajl_allow_partial_values = 0x10
+ } yajl_option;
+
+ /** allow the modification of parser options subsequent to handle
+ * allocation (via yajl_alloc)
+ * \returns zero in case of errors, non-zero otherwise
+ */
+ YAJL_API int yajl_config(yajl_handle h, yajl_option opt, ...);
+
+ /** set limit for total size of internal buffers */
+ YAJL_API void yajl_set_memory_limit(yajl_handle h, unsigned long limit);
+
+ /** free a parser handle */
+ YAJL_API void yajl_free(yajl_handle handle);
+
+ /** Parse some json!
+ * \param hand - a handle to the json parser allocated with yajl_alloc
+ * \param jsonText - a pointer to the UTF8 json text to be parsed
+ * \param jsonTextLength - the length, in bytes, of input text
+ */
+ YAJL_API yajl_status yajl_parse(yajl_handle hand,
+ const unsigned char * jsonText,
+ size_t jsonTextLength);
+
+ /** Parse any remaining buffered json.
+ * Since yajl is a stream-based parser, without an explicit end of
+ * input, yajl sometimes can't decide if content at the end of the
+ * stream is valid or not. For example, if "1" has been fed in,
+ * yajl can't know whether another digit is next or some character
+ * that would terminate the integer token.
+ *
+ * \param hand - a handle to the json parser allocated with yajl_alloc
+ */
+ YAJL_API yajl_status yajl_complete_parse(yajl_handle hand);
+
+ /** get an error string describing the state of the
+ * parse.
+ *
+ * If verbose is non-zero, the message will include the JSON
+ * text where the error occured, along with an arrow pointing to
+ * the specific char.
+ *
+ * \returns A dynamically allocated string will be returned which should
+ * be freed with yajl_free_error
+ */
+ YAJL_API unsigned char * yajl_get_error(yajl_handle hand, int verbose,
+ const unsigned char * jsonText,
+ size_t jsonTextLength);
+
+ /**
+ * get the amount of data consumed from the last chunk passed to YAJL.
+ *
+ * In the case of a successful parse this can help you understand if
+ * the entire buffer was consumed (which will allow you to handle
+ * "junk at end of input").
+ *
+ * In the event an error is encountered during parsing, this function
+ * affords the client a way to get the offset into the most recent
+ * chunk where the error occured. 0 will be returned if no error
+ * was encountered.
+ */
+ YAJL_API size_t yajl_get_bytes_consumed(yajl_handle hand);
+
+ /** free an error returned from yajl_get_error */
+ YAJL_API void yajl_free_error(yajl_handle hand, unsigned char * str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/contrib/libs/yajl/api/yajl_tree.h b/contrib/libs/yajl/api/yajl_tree.h
new file mode 100644
index 0000000000..2ec36fd576
--- /dev/null
+++ b/contrib/libs/yajl/api/yajl_tree.h
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2010-2011 Florian Forster <ff at octo.it>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_tree.h
+ *
+ * Parses JSON data and returns the data in tree form.
+ *
+ * \author Florian Forster
+ * \date August 2010
+ *
+ * This interface makes quick parsing and extraction of
+ * smallish JSON docs trivial:
+ *
+ * \include example/parse_config.c
+ */
+
+#ifndef YAJL_TREE_H
+#define YAJL_TREE_H 1
+
+#include "yajl_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** possible data types that a yajl_val_s can hold */
+typedef enum {
+ yajl_t_string = 1,
+ yajl_t_number = 2,
+ yajl_t_object = 3,
+ yajl_t_array = 4,
+ yajl_t_true = 5,
+ yajl_t_false = 6,
+ yajl_t_null = 7,
+ /** The any type isn't valid for yajl_val_s.type, but can be
+ * used as an argument to routines like yajl_tree_get().
+ */
+ yajl_t_any = 8
+} yajl_type;
+
+#define YAJL_NUMBER_INT_VALID 0x01
+#define YAJL_NUMBER_DOUBLE_VALID 0x02
+#define YAJL_NUMBER_UINT_VALID 0x04
+
+/** A pointer to a node in the parse tree */
+typedef struct yajl_val_s * yajl_val;
+
+/**
+ * A JSON value representation capable of holding one of the seven
+ * types above. For "string", "number", "object", and "array"
+ * additional data is available in the union. The "YAJL_IS_*"
+ * and "YAJL_GET_*" macros below allow type checking and convenient
+ * value extraction.
+ */
+struct yajl_val_s
+{
+ /** Type of the value contained. Use the "YAJL_IS_*" macros to check for a
+ * specific type. */
+ yajl_type type;
+ /** Type-specific data. You may use the "YAJL_GET_*" macros to access these
+ * members. */
+ union
+ {
+ char * string;
+ struct {
+ long long i; /*< integer value, if representable. */
+ unsigned long long ui; /*< unsigned integer value, if representable. */
+ double d; /*< double value, if representable. */
+ char *r; /*< unparsed number in string form. */
+ /** Signals whether the \em i and \em d members are
+ * valid. See \c YAJL_NUMBER_INT_VALID and
+ * \c YAJL_NUMBER_DOUBLE_VALID. */
+ unsigned int flags;
+ } number;
+ struct {
+ const char **keys; /*< Array of keys */
+ yajl_val *values; /*< Array of values. */
+ size_t len; /*< Number of key-value-pairs. */
+ } object;
+ struct {
+ yajl_val *values; /*< Array of elements. */
+ size_t len; /*< Number of elements. */
+ } array;
+ } u;
+};
+
+/**
+ * Parse a string.
+ *
+ * Parses an null-terminated string containing JSON data and returns a pointer
+ * to the top-level value (root of the parse tree).
+ *
+ * \param input Pointer to a null-terminated utf8 string containing
+ * JSON data.
+ * \param error_buffer Pointer to a buffer in which an error message will
+ * be stored if \em yajl_tree_parse fails, or
+ * \c NULL. The buffer will be initialized before
+ * parsing, so its content will be destroyed even if
+ * \em yajl_tree_parse succeeds.
+ * \param error_buffer_size Size of the memory area pointed to by
+ * \em error_buffer_size. If \em error_buffer_size is
+ * \c NULL, this argument is ignored.
+ *
+ * \returns Pointer to the top-level value or \c NULL on error. The memory
+ * pointed to must be freed using \em yajl_tree_free. In case of an error, a
+ * null terminated message describing the error in more detail is stored in
+ * \em error_buffer if it is not \c NULL.
+ */
+YAJL_API yajl_val yajl_tree_parse (const char *input,
+ char *error_buffer, size_t error_buffer_size);
+
+
+/**
+ * Free a parse tree returned by "yajl_tree_parse".
+ *
+ * \param v Pointer to a JSON value returned by "yajl_tree_parse". Passing NULL
+ * is valid and results in a no-op.
+ */
+YAJL_API void yajl_tree_free (yajl_val v);
+
+/**
+ * Access a nested value inside a tree.
+ *
+ * \param parent the node under which you'd like to extract values.
+ * \param path A null terminated array of strings, each the name of an object key
+ * \param type the yajl_type of the object you seek, or yajl_t_any if any will do.
+ *
+ * \returns a pointer to the found value, or NULL if we came up empty.
+ *
+ * Future Ideas: it'd be nice to move path to a string and implement support for
+ * a teeny tiny micro language here, so you can extract array elements, do things
+ * like .first and .last, even .length. Inspiration from JSONPath and css selectors?
+ * No it wouldn't be fast, but that's not what this API is about.
+ */
+YAJL_API yajl_val yajl_tree_get(yajl_val parent, const char ** path, yajl_type type);
+
+/* Various convenience macros to check the type of a `yajl_val` */
+#define YAJL_IS_STRING(v) (((v) != NULL) && ((v)->type == yajl_t_string))
+#define YAJL_IS_NUMBER(v) (((v) != NULL) && ((v)->type == yajl_t_number))
+#define YAJL_IS_INTEGER(v) (YAJL_IS_NUMBER(v) && ((v)->u.number.flags & YAJL_NUMBER_INT_VALID))
+#define YAJL_IS_UINTEGER(v) (YAJL_IS_NUMBER(v) && ((v)->u.number.flags & YAJL_NUMBER_UINT_VALID))
+#define YAJL_IS_DOUBLE(v) (YAJL_IS_NUMBER(v) && ((v)->u.number.flags & YAJL_NUMBER_DOUBLE_VALID))
+#define YAJL_IS_OBJECT(v) (((v) != NULL) && ((v)->type == yajl_t_object))
+#define YAJL_IS_ARRAY(v) (((v) != NULL) && ((v)->type == yajl_t_array ))
+#define YAJL_IS_TRUE(v) (((v) != NULL) && ((v)->type == yajl_t_true ))
+#define YAJL_IS_FALSE(v) (((v) != NULL) && ((v)->type == yajl_t_false ))
+#define YAJL_IS_NULL(v) (((v) != NULL) && ((v)->type == yajl_t_null ))
+
+/** Given a yajl_val_string return a ptr to the bare string it contains,
+ * or NULL if the value is not a string. */
+#define YAJL_GET_STRING(v) (YAJL_IS_STRING(v) ? (v)->u.string : NULL)
+
+/** Get the string representation of a number. You should check type first,
+ * perhaps using YAJL_IS_NUMBER */
+#define YAJL_GET_NUMBER(v) ((v)->u.number.r)
+
+/** Get the double representation of a number. You should check type first,
+ * perhaps using YAJL_IS_DOUBLE */
+#define YAJL_GET_DOUBLE(v) ((v)->u.number.d)
+
+/** Get the 64bit (long long) integer representation of a number. You should
+ * check type first, perhaps using YAJL_IS_INTEGER */
+#define YAJL_GET_INTEGER(v) ((v)->u.number.i)
+
+/** Get the 64bit (unsigned long long) unsigned integer representation of a number. You should
+ * check type first, perhaps using YAJL_IS_UINTEGER */
+#define YAJL_GET_UINTEGER(v) ((v)->u.number.ui)
+
+/** Get a pointer to a yajl_val_object or NULL if the value is not an object. */
+#define YAJL_GET_OBJECT(v) (YAJL_IS_OBJECT(v) ? &(v)->u.object : NULL)
+
+/** Get a pointer to a yajl_val_array or NULL if the value is not an object. */
+#define YAJL_GET_ARRAY(v) (YAJL_IS_ARRAY(v) ? &(v)->u.array : NULL)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YAJL_TREE_H */
diff --git a/contrib/libs/yajl/api/yajl_version.h b/contrib/libs/yajl/api/yajl_version.h
new file mode 100644
index 0000000000..adc0ad8225
--- /dev/null
+++ b/contrib/libs/yajl/api/yajl_version.h
@@ -0,0 +1,23 @@
+#ifndef YAJL_VERSION_H_
+#define YAJL_VERSION_H_
+
+#include "yajl_common.h"
+
+#define YAJL_MAJOR 2
+#define YAJL_MINOR 1
+#define YAJL_MICRO 1
+
+#define YAJL_VERSION ((YAJL_MAJOR * 10000) + (YAJL_MINOR * 100) + YAJL_MICRO)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern int YAJL_API yajl_version(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YAJL_VERSION_H_ */
+
diff --git a/contrib/libs/yajl/ya.make b/contrib/libs/yajl/ya.make
new file mode 100644
index 0000000000..2b201502a9
--- /dev/null
+++ b/contrib/libs/yajl/ya.make
@@ -0,0 +1,24 @@
+LIBRARY()
+
+LICENSE(ISC)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+VERSION(2.1.1)
+
+NO_COMPILER_WARNINGS()
+
+SRCS(
+ yajl.c
+ yajl_buf.c
+ yajl_gen.c
+ yajl_parser.c
+ yajl_version.c
+ yajl_alloc.c
+ yajl_encode.c
+ yajl_lex.c
+ yajl_tree.c
+ yajl_parser.cpp
+)
+
+END()
diff --git a/contrib/libs/yajl/yajl.c b/contrib/libs/yajl/yajl.c
new file mode 100644
index 0000000000..0a97f6ef00
--- /dev/null
+++ b/contrib/libs/yajl/yajl.c
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_parse.h"
+#include "yajl_lex.h"
+#include "yajl_parser.h"
+#include "yajl_alloc.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+const char *
+yajl_status_to_string(yajl_status stat)
+{
+ const char * statStr = "unknown";
+ switch (stat) {
+ case yajl_status_ok:
+ statStr = "ok, no error";
+ break;
+ case yajl_status_client_canceled:
+ statStr = "client canceled parse";
+ break;
+ case yajl_status_error:
+ statStr = "parse error";
+ break;
+ }
+ return statStr;
+}
+
+yajl_handle
+yajl_alloc(const yajl_callbacks * callbacks,
+ yajl_alloc_funcs * afs,
+ void * ctx)
+{
+ yajl_handle hand = NULL;
+ yajl_alloc_funcs afsBuffer;
+
+ /* first order of business is to set up memory allocation routines */
+ if (afs != NULL) {
+ if (afs->malloc == NULL || afs->realloc == NULL || afs->free == NULL)
+ {
+ return NULL;
+ }
+ } else {
+ yajl_set_default_alloc_funcs(&afsBuffer);
+ afs = &afsBuffer;
+ }
+
+ hand = (yajl_handle) YA_MALLOC(afs, sizeof(struct yajl_handle_t));
+ if (hand == NULL) {
+ return NULL;
+ }
+
+ /* copy in pointers to allocation routines */
+ memcpy((void *) &(hand->alloc), (void *) afs, sizeof(yajl_alloc_funcs));
+
+ hand->callbacks = callbacks;
+ hand->ctx = ctx;
+ hand->lexer = NULL;
+ hand->bytesConsumed = 0;
+ hand->decodeBuf = yajl_buf_alloc(&(hand->alloc));
+ hand->flags = 0;
+ hand->memoryLimit = 0;
+ yajl_bs_init(hand->stateStack, &(hand->alloc));
+ yajl_bs_push(hand->stateStack, yajl_state_start);
+
+ return hand;
+}
+
+int
+yajl_config(yajl_handle h, yajl_option opt, ...)
+{
+ int rv = 1;
+ va_list ap;
+ va_start(ap, opt);
+
+ switch(opt) {
+ case yajl_allow_comments:
+ case yajl_dont_validate_strings:
+ case yajl_allow_trailing_garbage:
+ case yajl_allow_multiple_values:
+ case yajl_allow_partial_values:
+ if (va_arg(ap, int)) h->flags |= opt;
+ else h->flags &= ~opt;
+ break;
+ default:
+ rv = 0;
+ }
+ va_end(ap);
+
+ return rv;
+}
+
+/** set limit for total size of internal buffers */
+void yajl_set_memory_limit(yajl_handle h, unsigned long limit)
+{
+ h->memoryLimit = limit;
+}
+
+void
+yajl_free(yajl_handle handle)
+{
+ yajl_bs_free(handle->stateStack);
+ yajl_buf_free(handle->decodeBuf);
+ if (handle->lexer) {
+ yajl_lex_free(handle->lexer);
+ handle->lexer = NULL;
+ }
+ YA_FREE(&(handle->alloc), handle);
+}
+
+yajl_status
+yajl_parse(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen)
+{
+ yajl_status status;
+
+ /* lazy allocation of the lexer */
+ if (hand->lexer == NULL) {
+ hand->lexer = yajl_lex_alloc(&(hand->alloc),
+ hand->flags & yajl_allow_comments,
+ !(hand->flags & yajl_dont_validate_strings));
+ }
+
+ status = yajl_do_parse(hand, jsonText, jsonTextLen);
+
+ if (status == yajl_status_ok &&
+ hand->memoryLimit != 0 &&
+ yajl_buf_capacity(hand->decodeBuf) + yajl_lex_buf_capacity(hand->lexer) > hand->memoryLimit)
+ {
+ hand->parseError = "Out of memory (this is typically caused by an inefficient representation of strings in JSON)";
+ status = yajl_status_error;
+ yajl_bs_push(hand->stateStack, status);
+ }
+
+ return status;
+}
+
+
+yajl_status
+yajl_complete_parse(yajl_handle hand)
+{
+ /* The lexer is lazy allocated in the first call to parse. if parse is
+ * never called, then no data was provided to parse at all. This is a
+ * "premature EOF" error unless yajl_allow_partial_values is specified.
+ * allocating the lexer now is the simplest possible way to handle this
+ * case while preserving all the other semantics of the parser
+ * (multiple values, partial values, etc). */
+ if (hand->lexer == NULL) {
+ hand->lexer = yajl_lex_alloc(&(hand->alloc),
+ hand->flags & yajl_allow_comments,
+ !(hand->flags & yajl_dont_validate_strings));
+ }
+
+ return yajl_do_finish(hand);
+}
+
+unsigned char *
+yajl_get_error(yajl_handle hand, int verbose,
+ const unsigned char * jsonText, size_t jsonTextLen)
+{
+ return yajl_render_error_string(hand, jsonText, jsonTextLen, verbose);
+}
+
+size_t
+yajl_get_bytes_consumed(yajl_handle hand)
+{
+ if (!hand) return 0;
+ else return hand->bytesConsumed;
+}
+
+
+void
+yajl_free_error(yajl_handle hand, unsigned char * str)
+{
+ /* use memory allocation functions if set */
+ YA_FREE(&(hand->alloc), str);
+}
+
+/* XXX: add utility routines to parse from file */
diff --git a/contrib/libs/yajl/yajl_alloc.c b/contrib/libs/yajl/yajl_alloc.c
new file mode 100644
index 0000000000..96ad1d3304
--- /dev/null
+++ b/contrib/libs/yajl/yajl_alloc.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_alloc.h
+ * default memory allocation routines for yajl which use malloc/realloc and
+ * free
+ */
+
+#include "yajl_alloc.h"
+#include <stdlib.h>
+
+static void * yajl_internal_malloc(void *ctx, size_t sz)
+{
+ (void)ctx;
+ return malloc(sz);
+}
+
+static void * yajl_internal_realloc(void *ctx, void * previous,
+ size_t sz)
+{
+ (void)ctx;
+ return realloc(previous, sz);
+}
+
+static void yajl_internal_free(void *ctx, void * ptr)
+{
+ (void)ctx;
+ free(ptr);
+}
+
+void yajl_set_default_alloc_funcs(yajl_alloc_funcs * yaf)
+{
+ yaf->malloc = yajl_internal_malloc;
+ yaf->free = yajl_internal_free;
+ yaf->realloc = yajl_internal_realloc;
+ yaf->ctx = NULL;
+}
+
diff --git a/contrib/libs/yajl/yajl_alloc.h b/contrib/libs/yajl/yajl_alloc.h
new file mode 100644
index 0000000000..203c2f97bd
--- /dev/null
+++ b/contrib/libs/yajl/yajl_alloc.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_alloc.h
+ * default memory allocation routines for yajl which use malloc/realloc and
+ * free
+ */
+
+#ifndef __YAJL_ALLOC_H__
+#define __YAJL_ALLOC_H__
+
+#include "api/yajl_common.h"
+
+#define YA_MALLOC(afs, sz) (afs)->malloc((afs)->ctx, (sz))
+#define YA_FREE(afs, ptr) (afs)->free((afs)->ctx, (ptr))
+#define YA_REALLOC(afs, ptr, sz) (afs)->realloc((afs)->ctx, (ptr), (sz))
+
+void yajl_set_default_alloc_funcs(yajl_alloc_funcs * yaf);
+
+#endif
diff --git a/contrib/libs/yajl/yajl_buf.c b/contrib/libs/yajl/yajl_buf.c
new file mode 100644
index 0000000000..2f055a6e6a
--- /dev/null
+++ b/contrib/libs/yajl/yajl_buf.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_buf.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define YAJL_BUF_INIT_SIZE 2048
+
+struct yajl_buf_t {
+ size_t len;
+ size_t used;
+ unsigned char * data;
+ yajl_alloc_funcs * alloc;
+};
+
+static
+void yajl_buf_ensure_available(yajl_buf buf, size_t want)
+{
+ size_t need;
+
+ assert(buf != NULL);
+
+ /* first call */
+ if (buf->data == NULL) {
+ buf->len = YAJL_BUF_INIT_SIZE;
+ buf->data = (unsigned char *) YA_MALLOC(buf->alloc, buf->len);
+ buf->data[0] = 0;
+ }
+
+ need = buf->len;
+
+ while (want >= (need - buf->used)) need <<= 1;
+
+ if (need != buf->len) {
+ buf->data = (unsigned char *) YA_REALLOC(buf->alloc, buf->data, need);
+ buf->len = need;
+ }
+}
+
+yajl_buf yajl_buf_alloc(yajl_alloc_funcs * alloc)
+{
+ yajl_buf b = YA_MALLOC(alloc, sizeof(struct yajl_buf_t));
+ memset((void *) b, 0, sizeof(struct yajl_buf_t));
+ b->alloc = alloc;
+ return b;
+}
+
+void yajl_buf_free(yajl_buf buf)
+{
+ assert(buf != NULL);
+ if (buf->data) YA_FREE(buf->alloc, buf->data);
+ YA_FREE(buf->alloc, buf);
+}
+
+void yajl_buf_append(yajl_buf buf, const void * data, size_t len)
+{
+ yajl_buf_ensure_available(buf, len);
+ if (len > 0) {
+ assert(data != NULL);
+ memcpy(buf->data + buf->used, data, len);
+ buf->used += len;
+ buf->data[buf->used] = 0;
+ }
+}
+
+void yajl_buf_clear(yajl_buf buf)
+{
+ buf->used = 0;
+ if (buf->data) buf->data[buf->used] = 0;
+}
+
+const unsigned char * yajl_buf_data(yajl_buf buf)
+{
+ return buf->data;
+}
+
+size_t yajl_buf_len(yajl_buf buf)
+{
+ return buf->used;
+}
+
+size_t yajl_buf_capacity(yajl_buf buf)
+{
+ return buf->len;
+}
+
+void
+yajl_buf_truncate(yajl_buf buf, size_t len)
+{
+ assert(len <= buf->used);
+ buf->used = len;
+}
diff --git a/contrib/libs/yajl/yajl_buf.h b/contrib/libs/yajl/yajl_buf.h
new file mode 100644
index 0000000000..b855a08b8b
--- /dev/null
+++ b/contrib/libs/yajl/yajl_buf.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_BUF_H__
+#define __YAJL_BUF_H__
+
+#include "api/yajl_common.h"
+#include "yajl_alloc.h"
+
+/*
+ * Implementation/performance notes. If this were moved to a header
+ * only implementation using #define's where possible we might be
+ * able to sqeeze a little performance out of the guy by killing function
+ * call overhead. YMMV.
+ */
+
+/**
+ * yajl_buf is a buffer with exponential growth. the buffer ensures that
+ * you are always null padded.
+ */
+typedef struct yajl_buf_t * yajl_buf;
+
+/* allocate a new buffer */
+yajl_buf yajl_buf_alloc(yajl_alloc_funcs * alloc);
+
+/* free the buffer */
+void yajl_buf_free(yajl_buf buf);
+
+/* append a number of bytes to the buffer */
+void yajl_buf_append(yajl_buf buf, const void * data, size_t len);
+
+/* empty the buffer */
+void yajl_buf_clear(yajl_buf buf);
+
+/* get a pointer to the beginning of the buffer */
+const unsigned char * yajl_buf_data(yajl_buf buf);
+
+/* get the length of the buffer */
+size_t yajl_buf_len(yajl_buf buf);
+
+/* get the allocated size of the buffer */
+size_t yajl_buf_capacity(yajl_buf buf);
+
+/* truncate the buffer */
+void yajl_buf_truncate(yajl_buf buf, size_t len);
+
+#endif
diff --git a/contrib/libs/yajl/yajl_bytestack.h b/contrib/libs/yajl/yajl_bytestack.h
new file mode 100644
index 0000000000..9ea7d151e3
--- /dev/null
+++ b/contrib/libs/yajl/yajl_bytestack.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * A header only implementation of a simple stack of bytes, used in YAJL
+ * to maintain parse state.
+ */
+
+#ifndef __YAJL_BYTESTACK_H__
+#define __YAJL_BYTESTACK_H__
+
+#include "api/yajl_common.h"
+
+#define YAJL_BS_INC 128
+
+typedef struct yajl_bytestack_t
+{
+ unsigned char * stack;
+ size_t size;
+ size_t used;
+ yajl_alloc_funcs * yaf;
+} yajl_bytestack;
+
+/* initialize a bytestack */
+#define yajl_bs_init(obs, _yaf) { \
+ (obs).stack = NULL; \
+ (obs).size = 0; \
+ (obs).used = 0; \
+ (obs).yaf = (_yaf); \
+ } \
+
+
+/* initialize a bytestack */
+#define yajl_bs_free(obs) \
+ if ((obs).stack) (obs).yaf->free((obs).yaf->ctx, (obs).stack);
+
+#define yajl_bs_current(obs) \
+ (assert((obs).used > 0), (obs).stack[(obs).used - 1])
+
+#define yajl_bs_push(obs, byte) { \
+ if (((obs).size - (obs).used) == 0) { \
+ (obs).size += YAJL_BS_INC; \
+ (obs).stack = (obs).yaf->realloc((obs).yaf->ctx,\
+ (void *) (obs).stack, (obs).size);\
+ } \
+ (obs).stack[((obs).used)++] = (byte); \
+}
+
+/* removes the top item of the stack, returns nothing */
+#define yajl_bs_pop(obs) { ((obs).used)--; }
+
+#define yajl_bs_set(obs, byte) \
+ (obs).stack[((obs).used) - 1] = (byte);
+
+
+#endif
diff --git a/contrib/libs/yajl/yajl_encode.c b/contrib/libs/yajl/yajl_encode.c
new file mode 100644
index 0000000000..fd08258188
--- /dev/null
+++ b/contrib/libs/yajl/yajl_encode.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_encode.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static void CharToHex(unsigned char c, char * hexBuf)
+{
+ const char * hexchar = "0123456789ABCDEF";
+ hexBuf[0] = hexchar[c >> 4];
+ hexBuf[1] = hexchar[c & 0x0F];
+}
+
+void
+yajl_string_encode(const yajl_print_t print,
+ void * ctx,
+ const unsigned char * str,
+ size_t len,
+ int escape_solidus)
+{
+ size_t beg = 0;
+ size_t end = 0;
+ char hexBuf[7];
+ hexBuf[0] = '\\'; hexBuf[1] = 'u'; hexBuf[2] = '0'; hexBuf[3] = '0';
+ hexBuf[6] = 0;
+
+ while (end < len) {
+ const char * escaped = NULL;
+ switch (str[end]) {
+ case '\r': escaped = "\\r"; break;
+ case '\n': escaped = "\\n"; break;
+ case '\\': escaped = "\\\\"; break;
+ /* it is not required to escape a solidus in JSON:
+ * read sec. 2.5: http://www.ietf.org/rfc/rfc4627.txt
+ * specifically, this production from the grammar:
+ * unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
+ */
+ case '/': if (escape_solidus) escaped = "\\/"; break;
+ case '"': escaped = "\\\""; break;
+ case '\f': escaped = "\\f"; break;
+ case '\b': escaped = "\\b"; break;
+ case '\t': escaped = "\\t"; break;
+ default:
+ if ((unsigned char) str[end] < 32) {
+ CharToHex(str[end], hexBuf + 4);
+ escaped = hexBuf;
+ }
+ break;
+ }
+ if (escaped != NULL) {
+ print(ctx, (const char *) (str + beg), end - beg);
+ print(ctx, escaped, (unsigned int)strlen(escaped));
+ beg = ++end;
+ } else {
+ ++end;
+ }
+ }
+ print(ctx, (const char *) (str + beg), end - beg);
+}
+
+static void hexToDigit(unsigned int * val, const unsigned char * hex)
+{
+ unsigned int i;
+ for (i=0;i<4;i++) {
+ unsigned char c = hex[i];
+ if (c >= 'A') c = (c & ~0x20) - 7;
+ c -= '0';
+ assert(!(c & 0xF0));
+ *val = (*val << 4) | c;
+ }
+}
+
+static void Utf32toUtf8(unsigned int codepoint, char * utf8Buf)
+{
+ if (codepoint < 0x80) {
+ utf8Buf[0] = (char) codepoint;
+ utf8Buf[1] = 0;
+ } else if (codepoint < 0x0800) {
+ utf8Buf[0] = (char) ((codepoint >> 6) | 0xC0);
+ utf8Buf[1] = (char) ((codepoint & 0x3F) | 0x80);
+ utf8Buf[2] = 0;
+ } else if (codepoint < 0x10000) {
+ utf8Buf[0] = (char) ((codepoint >> 12) | 0xE0);
+ utf8Buf[1] = (char) (((codepoint >> 6) & 0x3F) | 0x80);
+ utf8Buf[2] = (char) ((codepoint & 0x3F) | 0x80);
+ utf8Buf[3] = 0;
+ } else if (codepoint < 0x200000) {
+ utf8Buf[0] =(char)((codepoint >> 18) | 0xF0);
+ utf8Buf[1] =(char)(((codepoint >> 12) & 0x3F) | 0x80);
+ utf8Buf[2] =(char)(((codepoint >> 6) & 0x3F) | 0x80);
+ utf8Buf[3] =(char)((codepoint & 0x3F) | 0x80);
+ utf8Buf[4] = 0;
+ } else {
+ utf8Buf[0] = '?';
+ utf8Buf[1] = 0;
+ }
+}
+
+void yajl_string_decode(yajl_buf buf, const unsigned char * str,
+ size_t len)
+{
+ size_t beg = 0;
+ size_t end = 0;
+
+ while (end < len) {
+ if (str[end] == '\\') {
+ char utf8Buf[5];
+ const char * unescaped = "?";
+ yajl_buf_append(buf, str + beg, end - beg);
+ switch (str[++end]) {
+ case 'r': unescaped = "\r"; break;
+ case 'n': unescaped = "\n"; break;
+ case '\\': unescaped = "\\"; break;
+ case '/': unescaped = "/"; break;
+ case '"': unescaped = "\""; break;
+ case 'f': unescaped = "\f"; break;
+ case 'b': unescaped = "\b"; break;
+ case 't': unescaped = "\t"; break;
+ case 'u': {
+ unsigned int codepoint = 0;
+ hexToDigit(&codepoint, str + ++end);
+ end+=3;
+ /* check if this is a surrogate */
+ if ((codepoint & 0xFC00) == 0xD800) {
+ end++;
+ if (str[end] == '\\' && str[end + 1] == 'u') {
+ unsigned int surrogate = 0;
+ hexToDigit(&surrogate, str + end + 2);
+ codepoint =
+ (((codepoint & 0x3F) << 10) |
+ ((((codepoint >> 6) & 0xF) + 1) << 16) |
+ (surrogate & 0x3FF));
+ end += 5;
+ } else {
+ unescaped = "?";
+ break;
+ }
+ }
+
+ Utf32toUtf8(codepoint, utf8Buf);
+ unescaped = utf8Buf;
+
+ if (codepoint == 0) {
+ yajl_buf_append(buf, unescaped, 1);
+ beg = ++end;
+ continue;
+ }
+
+ break;
+ }
+ default:
+ assert("this should never happen" == NULL);
+ }
+ yajl_buf_append(buf, unescaped, (unsigned int)strlen(unescaped));
+ beg = ++end;
+ } else {
+ end++;
+ }
+ }
+ yajl_buf_append(buf, str + beg, end - beg);
+}
+
+#define ADV_PTR s++; if (!(len--)) return 0;
+
+int yajl_string_validate_utf8(const unsigned char * s, size_t len)
+{
+ if (!len) return 1;
+ if (!s) return 0;
+
+ while (len--) {
+ /* single byte */
+ if (*s <= 0x7f) {
+ /* noop */
+ }
+ /* two byte */
+ else if ((*s >> 5) == 0x6) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ }
+ /* three byte */
+ else if ((*s >> 4) == 0x0e) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ }
+ /* four byte */
+ else if ((*s >> 3) == 0x1e) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ } else {
+ return 0;
+ }
+
+ s++;
+ }
+
+ return 1;
+}
diff --git a/contrib/libs/yajl/yajl_encode.h b/contrib/libs/yajl/yajl_encode.h
new file mode 100644
index 0000000000..853a1a701c
--- /dev/null
+++ b/contrib/libs/yajl/yajl_encode.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_ENCODE_H__
+#define __YAJL_ENCODE_H__
+
+#include "yajl_buf.h"
+#include "api/yajl_gen.h"
+
+void yajl_string_encode(const yajl_print_t printer,
+ void * ctx,
+ const unsigned char * str,
+ size_t length,
+ int escape_solidus);
+
+void yajl_string_decode(yajl_buf buf, const unsigned char * str,
+ size_t length);
+
+int yajl_string_validate_utf8(const unsigned char * s, size_t len);
+
+#endif
diff --git a/contrib/libs/yajl/yajl_gen.c b/contrib/libs/yajl/yajl_gen.c
new file mode 100644
index 0000000000..5b1dd93246
--- /dev/null
+++ b/contrib/libs/yajl/yajl_gen.c
@@ -0,0 +1,392 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_gen.h"
+#include "yajl_buf.h"
+#include "yajl_encode.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdarg.h>
+
+typedef enum {
+ yajl_gen_start,
+ yajl_gen_map_start,
+ yajl_gen_map_key,
+ yajl_gen_map_val,
+ yajl_gen_array_start,
+ yajl_gen_in_array,
+ yajl_gen_complete,
+ yajl_gen_error
+} yajl_gen_state;
+
+struct yajl_gen_t
+{
+ unsigned int flags;
+ unsigned int depth;
+ const char * indentString;
+ yajl_gen_state state[YAJL_MAX_DEPTH];
+ yajl_print_t print;
+ void * ctx; /* yajl_buf */
+ /* memory allocation routines */
+ yajl_alloc_funcs alloc;
+};
+
+int
+yajl_gen_config(yajl_gen g, yajl_gen_option opt, ...)
+{
+ int rv = 1;
+ va_list ap;
+ va_start(ap, opt);
+
+ switch(opt) {
+ case yajl_gen_beautify:
+ case yajl_gen_validate_utf8:
+ case yajl_gen_escape_solidus:
+ case yajl_gen_disable_yandex_double_format:
+ case yajl_gen_skip_final_newline:
+ case yajl_gen_support_infinity:
+ if (va_arg(ap, int)) g->flags |= opt;
+ else g->flags &= ~opt;
+ break;
+ case yajl_gen_indent_string: {
+ const char *indent = va_arg(ap, const char *);
+ g->indentString = indent;
+ for (; *indent; indent++) {
+ if (*indent != '\n'
+ && *indent != '\v'
+ && *indent != '\f'
+ && *indent != '\t'
+ && *indent != '\r'
+ && *indent != ' ')
+ {
+ g->indentString = NULL;
+ rv = 0;
+ }
+ }
+ break;
+ }
+ case yajl_gen_print_callback:
+ yajl_buf_free(g->ctx);
+ g->print = va_arg(ap, const yajl_print_t);
+ g->ctx = va_arg(ap, void *);
+ break;
+ default:
+ rv = 0;
+ }
+
+ va_end(ap);
+
+ return rv;
+}
+
+
+
+yajl_gen
+yajl_gen_alloc(const yajl_alloc_funcs * afs)
+{
+ yajl_gen g = NULL;
+ yajl_alloc_funcs afsBuffer;
+
+ /* first order of business is to set up memory allocation routines */
+ if (afs != NULL) {
+ if (afs->malloc == NULL || afs->realloc == NULL || afs->free == NULL)
+ {
+ return NULL;
+ }
+ } else {
+ yajl_set_default_alloc_funcs(&afsBuffer);
+ afs = &afsBuffer;
+ }
+
+ g = (yajl_gen) YA_MALLOC(afs, sizeof(struct yajl_gen_t));
+ if (!g) return NULL;
+
+ memset((void *) g, 0, sizeof(struct yajl_gen_t));
+ /* copy in pointers to allocation routines */
+ memcpy((void *) &(g->alloc), (void *) afs, sizeof(yajl_alloc_funcs));
+
+ g->print = (yajl_print_t)&yajl_buf_append;
+ g->ctx = yajl_buf_alloc(&(g->alloc));
+ g->indentString = " ";
+
+ return g;
+}
+
+void
+yajl_gen_reset(yajl_gen g, const char * sep)
+{
+ g->depth = 0;
+ memset((void *) &(g->state), 0, sizeof(g->state));
+ if (sep != NULL) g->print(g->ctx, sep, strlen(sep));
+}
+
+void
+yajl_gen_free(yajl_gen g)
+{
+ if (g->print == (yajl_print_t)&yajl_buf_append) yajl_buf_free((yajl_buf)g->ctx);
+ YA_FREE(&(g->alloc), g);
+}
+
+#define INSERT_SEP \
+ if (g->state[g->depth] == yajl_gen_map_key || \
+ g->state[g->depth] == yajl_gen_in_array) { \
+ g->print(g->ctx, ",", 1); \
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1); \
+ } else if (g->state[g->depth] == yajl_gen_map_val) { \
+ g->print(g->ctx, ":", 1); \
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, " ", 1); \
+ }
+
+#define INSERT_WHITESPACE \
+ if ((g->flags & yajl_gen_beautify)) { \
+ if (g->state[g->depth] != yajl_gen_map_val) { \
+ unsigned int _i; \
+ for (_i=0;_i<g->depth;_i++) \
+ g->print(g->ctx, \
+ g->indentString, \
+ (unsigned int)strlen(g->indentString)); \
+ } \
+ }
+
+#define ENSURE_NOT_KEY \
+ if (g->state[g->depth] == yajl_gen_map_key || \
+ g->state[g->depth] == yajl_gen_map_start) { \
+ return yajl_gen_keys_must_be_strings; \
+ } \
+
+/* check that we're not complete, or in error state. in a valid state
+ * to be generating */
+#define ENSURE_VALID_STATE \
+ if (g->state[g->depth] == yajl_gen_error) { \
+ return yajl_gen_in_error_state;\
+ } else if (g->state[g->depth] == yajl_gen_complete) { \
+ return yajl_gen_generation_complete; \
+ }
+
+#define INCREMENT_DEPTH \
+ if (++(g->depth) >= YAJL_MAX_DEPTH) return yajl_max_depth_exceeded;
+
+#define DECREMENT_DEPTH \
+ if (--(g->depth) >= YAJL_MAX_DEPTH) return yajl_gen_generation_complete;
+
+#define APPENDED_ATOM \
+ switch (g->state[g->depth]) { \
+ case yajl_gen_start: \
+ g->state[g->depth] = yajl_gen_complete; \
+ break; \
+ case yajl_gen_map_start: \
+ case yajl_gen_map_key: \
+ g->state[g->depth] = yajl_gen_map_val; \
+ break; \
+ case yajl_gen_array_start: \
+ g->state[g->depth] = yajl_gen_in_array; \
+ break; \
+ case yajl_gen_map_val: \
+ g->state[g->depth] = yajl_gen_map_key; \
+ break; \
+ default: \
+ break; \
+ } \
+
+#define FINAL_NEWLINE \
+ if ((g->flags & yajl_gen_beautify) && \
+ !(g->flags & yajl_gen_skip_final_newline) && \
+ g->state[g->depth] == yajl_gen_complete) \
+ { \
+ g->print(g->ctx, "\n", 1); \
+ } \
+
+yajl_gen_status
+yajl_gen_integer(yajl_gen g, long long int number)
+{
+ char i[32];
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ sprintf(i, "%lld", number);
+ g->print(g->ctx, i, (unsigned int)strlen(i));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_uinteger(yajl_gen g, long long unsigned number)
+{
+ char i[32];
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ sprintf(i, "%llu", number);
+ g->print(g->ctx, i, (unsigned int)strlen(i));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+#if defined(_WIN32) || defined(WIN32)
+#include <float.h>
+#define isnan _isnan
+#define isinf !_finite
+#endif
+
+extern void FormatDoubleYandex(char* buf, size_t len, double val);
+
+yajl_gen_status
+yajl_gen_double(yajl_gen g, double number)
+{
+ char i[32];
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY;
+ if (isnan(number) ||
+ (isinf(number) && !(g->flags & yajl_gen_support_infinity)))
+ return yajl_gen_invalid_number;
+
+ INSERT_SEP; INSERT_WHITESPACE;
+
+ if (g->flags & yajl_gen_disable_yandex_double_format) {
+ sprintf(i, "%.20g", number);
+ if (strspn(i, "0123456789-") == strlen(i)) {
+ strcat(i, ".0");
+ }
+ } else {
+ FormatDoubleYandex(i, sizeof(i) - 1, number);
+ }
+
+ g->print(g->ctx, i, (unsigned int)strlen(i));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_number(yajl_gen g, const char * s, size_t l)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, s, l);
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_string(yajl_gen g, const unsigned char * str,
+ size_t len)
+{
+ // if validation is enabled, check that the string is valid utf8
+ // XXX: This checking could be done a little faster, in the same pass as
+ // the string encoding
+ if (g->flags & yajl_gen_validate_utf8) {
+ if (!yajl_string_validate_utf8(str, len)) {
+ return yajl_gen_invalid_string;
+ }
+ }
+ ENSURE_VALID_STATE; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, "\"", 1);
+ yajl_string_encode(g->print, g->ctx, str, len, g->flags & yajl_gen_escape_solidus);
+ g->print(g->ctx, "\"", 1);
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_null(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, "null", strlen("null"));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_bool(yajl_gen g, int boolean)
+{
+ const char * val = boolean ? "true" : "false";
+
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, val, (unsigned int)strlen(val));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_map_open(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ INCREMENT_DEPTH;
+
+ g->state[g->depth] = yajl_gen_map_start;
+ g->print(g->ctx, "{", 1);
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_map_close(yajl_gen g)
+{
+ ENSURE_VALID_STATE;
+ DECREMENT_DEPTH;
+
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ APPENDED_ATOM;
+ INSERT_WHITESPACE;
+ g->print(g->ctx, "}", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_array_open(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ INCREMENT_DEPTH;
+ g->state[g->depth] = yajl_gen_array_start;
+ g->print(g->ctx, "[", 1);
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_array_close(yajl_gen g)
+{
+ ENSURE_VALID_STATE;
+ DECREMENT_DEPTH;
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ APPENDED_ATOM;
+ INSERT_WHITESPACE;
+ g->print(g->ctx, "]", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_get_buf(yajl_gen g, const unsigned char ** buf,
+ size_t * len)
+{
+ if (g->print != (yajl_print_t)&yajl_buf_append) return yajl_gen_no_buf;
+ *buf = yajl_buf_data((yajl_buf)g->ctx);
+ *len = yajl_buf_len((yajl_buf)g->ctx);
+ return yajl_gen_status_ok;
+}
+
+void
+yajl_gen_clear(yajl_gen g)
+{
+ if (g->print == (yajl_print_t)&yajl_buf_append) yajl_buf_clear((yajl_buf)g->ctx);
+}
diff --git a/contrib/libs/yajl/yajl_lex.c b/contrib/libs/yajl/yajl_lex.c
new file mode 100644
index 0000000000..70555c5ea3
--- /dev/null
+++ b/contrib/libs/yajl/yajl_lex.c
@@ -0,0 +1,766 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_lex.h"
+#include "yajl_buf.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+#ifdef YAJL_LEXER_DEBUG
+static const char *
+tokToStr(yajl_tok tok)
+{
+ switch (tok) {
+ case yajl_tok_bool: return "bool";
+ case yajl_tok_colon: return "colon";
+ case yajl_tok_comma: return "comma";
+ case yajl_tok_eof: return "eof";
+ case yajl_tok_error: return "error";
+ case yajl_tok_left_brace: return "brace";
+ case yajl_tok_left_bracket: return "bracket";
+ case yajl_tok_null: return "null";
+ case yajl_tok_inf: return "infinity";
+ case yajl_tok_minus_inf: return "-infinity";
+ case yajl_tok_integer: return "integer";
+ case yajl_tok_double: return "double";
+ case yajl_tok_right_brace: return "brace";
+ case yajl_tok_right_bracket: return "bracket";
+ case yajl_tok_string: return "string";
+ case yajl_tok_string_with_escapes: return "string_with_escapes";
+ }
+ return "unknown";
+}
+#endif
+
+/* Impact of the stream parsing feature on the lexer:
+ *
+ * YAJL support stream parsing. That is, the ability to parse the first
+ * bits of a chunk of JSON before the last bits are available (still on
+ * the network or disk). This makes the lexer more complex. The
+ * responsibility of the lexer is to handle transparently the case where
+ * a chunk boundary falls in the middle of a token. This is
+ * accomplished is via a buffer and a character reading abstraction.
+ *
+ * Overview of implementation
+ *
+ * When we lex to end of input string before end of token is hit, we
+ * copy all of the input text composing the token into our lexBuf.
+ *
+ * Every time we read a character, we do so through the readChar function.
+ * readChar's responsibility is to handle pulling all chars from the buffer
+ * before pulling chars from input text
+ */
+
+struct yajl_lexer_t {
+ /* the overal line and char offset into the data */
+ size_t lineOff;
+ size_t charOff;
+
+ /* error */
+ yajl_lex_error error;
+
+ /* a input buffer to handle the case where a token is spread over
+ * multiple chunks */
+ yajl_buf buf;
+
+ /* in the case where we have data in the lexBuf, bufOff holds
+ * the current offset into the lexBuf. */
+ size_t bufOff;
+
+ /* are we using the lex buf? */
+ unsigned int bufInUse;
+
+ /* shall we allow comments? */
+ unsigned int allowComments;
+
+ /* shall we validate utf8 inside strings? */
+ unsigned int validateUTF8;
+
+ yajl_alloc_funcs * alloc;
+};
+
+#define readChar(lxr, txt, off) \
+ (((lxr)->bufInUse && yajl_buf_len((lxr)->buf) && lxr->bufOff < yajl_buf_len((lxr)->buf)) ? \
+ (*((const unsigned char *) yajl_buf_data((lxr)->buf) + ((lxr)->bufOff)++)) : \
+ ((txt)[(*(off))++]))
+
+#define unreadChar(lxr, off) ((*(off) > 0) ? (*(off))-- : ((lxr)->bufOff--))
+
+yajl_lexer
+yajl_lex_alloc(yajl_alloc_funcs * alloc,
+ unsigned int allowComments,
+ unsigned int validateUTF8)
+{
+ yajl_lexer lxr = (yajl_lexer) YA_MALLOC(alloc, sizeof(struct yajl_lexer_t));
+ memset((void *) lxr, 0, sizeof(struct yajl_lexer_t));
+ lxr->buf = yajl_buf_alloc(alloc);
+ lxr->allowComments = allowComments;
+ lxr->validateUTF8 = validateUTF8;
+ lxr->alloc = alloc;
+ return lxr;
+}
+
+void
+yajl_lex_free(yajl_lexer lxr)
+{
+ yajl_buf_free(lxr->buf);
+ YA_FREE(lxr->alloc, lxr);
+ return;
+}
+
+/* a lookup table which lets us quickly determine three things:
+ * VEC - valid escaped control char
+ * note. the solidus '/' may be escaped or not.
+ * IJC - invalid json char
+ * VHC - valid hex char
+ * NFP - needs further processing (from a string scanning perspective)
+ * NUC - needs utf8 checking when enabled (from a string scanning perspective)
+ */
+#define VEC 0x01
+#define IJC 0x02
+#define VHC 0x04
+#define NFP 0x08
+#define NUC 0x10
+
+static const char charLookupTable[256] =
+{
+/*00*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*08*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*10*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*18*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+
+/*20*/ 0 , 0 , NFP|VEC|IJC, 0 , 0 , 0 , 0 , 0 ,
+/*28*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , VEC ,
+/*30*/ VHC , VHC , VHC , VHC , VHC , VHC , VHC , VHC ,
+/*38*/ VHC , VHC , 0 , 0 , 0 , 0 , 0 , 0 ,
+
+/*40*/ 0 , VHC , VHC , VHC , VHC , VHC , VHC , 0 ,
+/*48*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+/*50*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+/*58*/ 0 , 0 , 0 , 0 , NFP|VEC|IJC, 0 , 0 , 0 ,
+
+/*60*/ 0 , VHC , VEC|VHC, VHC , VHC , VHC , VEC|VHC, 0 ,
+/*68*/ 0 , 0 , 0 , 0 , 0 , 0 , VEC , 0 ,
+/*70*/ 0 , 0 , VEC , 0 , VEC , 0 , 0 , 0 ,
+/*78*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC
+};
+
+/** process a variable length utf8 encoded codepoint.
+ *
+ * returns:
+ * yajl_tok_string - if valid utf8 char was parsed and offset was
+ * advanced
+ * yajl_tok_eof - if end of input was hit before validation could
+ * complete
+ * yajl_tok_error - if invalid utf8 was encountered
+ *
+ * NOTE: on error the offset will point to the first char of the
+ * invalid utf8 */
+#define UTF8_CHECK_EOF if (*offset >= jsonTextLen) { return yajl_tok_eof; }
+
+static yajl_tok
+yajl_lex_utf8_char(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ unsigned char curChar)
+{
+ if (curChar <= 0x7f) {
+ /* single byte */
+ return yajl_tok_string;
+ } else if ((curChar >> 5) == 0x6) {
+ /* two byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ } else if ((curChar >> 4) == 0x0e) {
+ /* three byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ }
+ } else if ((curChar >> 3) == 0x1e) {
+ /* four byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ }
+ }
+ }
+
+ return yajl_tok_error;
+}
+
+/* lex a string. input is the lexer, pointer to beginning of
+ * json text, and start of string (offset).
+ * a token is returned which has the following meanings:
+ * yajl_tok_string: lex of string was successful. offset points to
+ * terminating '"'.
+ * yajl_tok_eof: end of text was encountered before we could complete
+ * the lex.
+ * yajl_tok_error: embedded in the string were unallowable chars. offset
+ * points to the offending char
+ */
+#define STR_CHECK_EOF \
+if (*offset >= jsonTextLen) { \
+ tok = yajl_tok_eof; \
+ goto finish_string_lex; \
+}
+
+/** scan a string for interesting characters that might need further
+ * review. return the number of chars that are uninteresting and can
+ * be skipped.
+ * (lth) hi world, any thoughts on how to make this routine faster? */
+static size_t
+yajl_string_scan(const unsigned char * buf, size_t len, int utf8check)
+{
+ unsigned char mask = IJC|NFP|(utf8check ? NUC : 0);
+ size_t skip = 0;
+ while (skip < len && !(charLookupTable[*buf] & mask))
+ {
+ skip++;
+ buf++;
+ }
+ return skip;
+}
+
+static yajl_tok
+yajl_lex_string(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ yajl_tok tok = yajl_tok_error;
+ int hasEscapes = 0;
+
+ for (;;) {
+ unsigned char curChar;
+
+ /* now jump into a faster scanning routine to skip as much
+ * of the buffers as possible */
+ {
+ const unsigned char * p;
+ size_t len;
+
+ if ((lexer->bufInUse && yajl_buf_len(lexer->buf) &&
+ lexer->bufOff < yajl_buf_len(lexer->buf)))
+ {
+ p = ((const unsigned char *) yajl_buf_data(lexer->buf) +
+ (lexer->bufOff));
+ len = yajl_buf_len(lexer->buf) - lexer->bufOff;
+ lexer->bufOff += yajl_string_scan(p, len, lexer->validateUTF8);
+ }
+ else if (*offset < jsonTextLen)
+ {
+ p = jsonText + *offset;
+ len = jsonTextLen - *offset;
+ *offset += yajl_string_scan(p, len, lexer->validateUTF8);
+ }
+ }
+
+ STR_CHECK_EOF;
+
+ curChar = readChar(lexer, jsonText, offset);
+
+ /* quote terminates */
+ if (curChar == '"') {
+ tok = yajl_tok_string;
+ break;
+ }
+ /* backslash escapes a set of control chars, */
+ else if (curChar == '\\') {
+ hasEscapes = 1;
+ STR_CHECK_EOF;
+
+ /* special case \u */
+ curChar = readChar(lexer, jsonText, offset);
+ if (curChar == 'u') {
+ unsigned int i = 0;
+
+ for (i=0;i<4;i++) {
+ STR_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if (!(charLookupTable[curChar] & VHC)) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_hex_char;
+ goto finish_string_lex;
+ }
+ }
+ } else if (!(charLookupTable[curChar] & VEC)) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_escaped_char;
+ goto finish_string_lex;
+ }
+ }
+ /* when not validating UTF8 it's a simple table lookup to determine
+ * if the present character is invalid */
+ else if(charLookupTable[curChar] & IJC) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_json_char;
+ goto finish_string_lex;
+ }
+ /* when in validate UTF8 mode we need to do some extra work */
+ else if (lexer->validateUTF8) {
+ yajl_tok t = yajl_lex_utf8_char(lexer, jsonText, jsonTextLen,
+ offset, curChar);
+
+ if (t == yajl_tok_eof) {
+ tok = yajl_tok_eof;
+ goto finish_string_lex;
+ } else if (t == yajl_tok_error) {
+ lexer->error = yajl_lex_string_invalid_utf8;
+ goto finish_string_lex;
+ }
+ }
+ /* accept it, and move on */
+ }
+ finish_string_lex:
+ /* tell our buddy, the parser, wether he needs to process this string
+ * again */
+ if (hasEscapes && tok == yajl_tok_string) {
+ tok = yajl_tok_string_with_escapes;
+ }
+
+ return tok;
+}
+
+#define RETURN_IF_EOF if (*offset >= jsonTextLen) return yajl_tok_eof;
+
+static yajl_tok
+yajl_lex_number(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ /** XXX: numbers are the only entities in json that we must lex
+ * _beyond_ in order to know that they are complete. There
+ * is an ambiguous case for integers at EOF. */
+
+ unsigned char c;
+
+ yajl_tok tok = yajl_tok_integer;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* optional leading minus */
+ char minus = 0;
+ if (c == '-') {
+ minus = 1;
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ /* a single zero, or a series of integers */
+ if (c == '0') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } else if (c >= '1' && c <= '9') {
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c >= '0' && c <= '9');
+ } else if (c == 'i') {
+ if (readChar(lexer, jsonText, offset) != 'n') {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_invalid_infinity;
+ return yajl_tok_error;
+ }
+ if (readChar(lexer, jsonText, offset) != 'f') {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_invalid_infinity;
+ return yajl_tok_error;
+ }
+ if (minus) {
+ return yajl_tok_minus_inf;
+ } else {
+ return yajl_tok_inf;
+ }
+ } else {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_minus;
+ return yajl_tok_error;
+ }
+
+ /* optional fraction (indicates this is floating point) */
+ if (c == '.') {
+ int numRd = 0;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ while (c >= '0' && c <= '9') {
+ numRd++;
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ if (!numRd) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_decimal;
+ return yajl_tok_error;
+ }
+ tok = yajl_tok_double;
+ }
+
+ /* optional exponent (indicates this is floating point) */
+ if (c == 'e' || c == 'E') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* optional sign */
+ if (c == '+' || c == '-') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ if (c >= '0' && c <= '9') {
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c >= '0' && c <= '9');
+ } else {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_exponent;
+ return yajl_tok_error;
+ }
+ tok = yajl_tok_double;
+ }
+
+ /* we always go "one too far" */
+ unreadChar(lexer, offset);
+
+ return tok;
+}
+
+static yajl_tok
+yajl_lex_comment(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ unsigned char c;
+
+ yajl_tok tok = yajl_tok_comment;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* either slash or star expected */
+ if (c == '/') {
+ /* now we throw away until end of line */
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c != '\n');
+ } else if (c == '*') {
+ /* now we throw away until end of comment */
+ for (;;) {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ if (c == '*') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ if (c == '/') {
+ break;
+ } else {
+ unreadChar(lexer, offset);
+ }
+ }
+ }
+ } else {
+ lexer->error = yajl_lex_invalid_char;
+ tok = yajl_tok_error;
+ }
+
+ return tok;
+}
+
+#define MATCH(want_value, target_token) \
+ const char * want = want_value; \
+ do { \
+ if (*offset >= jsonTextLen) { \
+ tok = yajl_tok_eof; \
+ goto lexed; \
+ } \
+ c = readChar(lexer, jsonText, offset); \
+ if (c != *want) { \
+ unreadChar(lexer, offset); \
+ lexer->error = yajl_lex_invalid_string; \
+ tok = yajl_tok_error; \
+ goto lexed; \
+ } \
+ } while (*(++want)); \
+ tok = target_token; \
+ goto lexed;
+
+yajl_tok
+yajl_lex_lex(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ const unsigned char ** outBuf, size_t * outLen)
+{
+ yajl_tok tok = yajl_tok_error;
+ unsigned char c;
+ size_t startOffset = *offset;
+
+ *outBuf = NULL;
+ *outLen = 0;
+
+ for (;;) {
+ assert(*offset <= jsonTextLen);
+
+ if (*offset >= jsonTextLen) {
+ tok = yajl_tok_eof;
+ goto lexed;
+ }
+
+ c = readChar(lexer, jsonText, offset);
+
+ switch (c) {
+ case '{':
+ tok = yajl_tok_left_bracket;
+ goto lexed;
+ case '}':
+ tok = yajl_tok_right_bracket;
+ goto lexed;
+ case '[':
+ tok = yajl_tok_left_brace;
+ goto lexed;
+ case ']':
+ tok = yajl_tok_right_brace;
+ goto lexed;
+ case ',':
+ tok = yajl_tok_comma;
+ goto lexed;
+ case ':':
+ tok = yajl_tok_colon;
+ goto lexed;
+ case '\t': case '\n': case '\v': case '\f': case '\r': case ' ':
+ startOffset++;
+ break;
+ case 't': {
+ MATCH("rue", yajl_tok_bool);
+ }
+ case 'f': {
+ MATCH("alse", yajl_tok_bool);
+ }
+ case 'n': {
+ MATCH("ull", yajl_tok_null);
+ }
+ case '"': {
+ tok = yajl_lex_string(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ goto lexed;
+ }
+ case '-':
+ case 'i':
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9': {
+ /* integer parsing wants to start from the beginning */
+ unreadChar(lexer, offset);
+ tok = yajl_lex_number(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ goto lexed;
+ }
+ case '/':
+ /* hey, look, a probable comment! If comments are disabled
+ * it's an error. */
+ if (!lexer->allowComments) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_unallowed_comment;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ /* if comments are enabled, then we should try to lex
+ * the thing. possible outcomes are
+ * - successful lex (tok_comment, which means continue),
+ * - malformed comment opening (slash not followed by
+ * '*' or '/') (tok_error)
+ * - eof hit. (tok_eof) */
+ tok = yajl_lex_comment(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ if (tok == yajl_tok_comment) {
+ /* "error" is silly, but that's the initial
+ * state of tok. guilty until proven innocent. */
+ tok = yajl_tok_error;
+ yajl_buf_clear(lexer->buf);
+ lexer->bufInUse = 0;
+ startOffset = *offset;
+ break;
+ }
+ /* hit error or eof, bail */
+ goto lexed;
+ default:
+ lexer->error = yajl_lex_invalid_char;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ }
+
+
+ lexed:
+ /* need to append to buffer if the buffer is in use or
+ * if it's an EOF token */
+ if (tok == yajl_tok_eof || lexer->bufInUse) {
+ if (!lexer->bufInUse) yajl_buf_clear(lexer->buf);
+ lexer->bufInUse = 1;
+ yajl_buf_append(lexer->buf, jsonText + startOffset, *offset - startOffset);
+ lexer->bufOff = 0;
+
+ if (tok != yajl_tok_eof) {
+ *outBuf = yajl_buf_data(lexer->buf);
+ *outLen = yajl_buf_len(lexer->buf);
+ lexer->bufInUse = 0;
+ }
+ } else if (tok != yajl_tok_error) {
+ *outBuf = jsonText + startOffset;
+ *outLen = *offset - startOffset;
+ }
+
+ /* special case for strings. skip the quotes. */
+ if (tok == yajl_tok_string || tok == yajl_tok_string_with_escapes)
+ {
+ assert(*outLen >= 2);
+ (*outBuf)++;
+ *outLen -= 2;
+ }
+
+
+#ifdef YAJL_LEXER_DEBUG
+ if (tok == yajl_tok_error) {
+ printf("lexical error: %s\n",
+ yajl_lex_error_to_string(yajl_lex_get_error(lexer)));
+ } else if (tok == yajl_tok_eof) {
+ printf("EOF hit\n");
+ } else {
+ printf("lexed %s: '", tokToStr(tok));
+ fwrite(*outBuf, 1, *outLen, stdout);
+ printf("'\n");
+ }
+#endif
+
+ return tok;
+}
+
+const char *
+yajl_lex_error_to_string(yajl_lex_error error)
+{
+ switch (error) {
+ case yajl_lex_e_ok:
+ return "ok, no error";
+ case yajl_lex_string_invalid_utf8:
+ return "invalid bytes in UTF8 string.";
+ case yajl_lex_string_invalid_escaped_char:
+ return "inside a string, '\\' occurs before a character "
+ "which it may not.";
+ case yajl_lex_string_invalid_json_char:
+ return "invalid character inside string.";
+ case yajl_lex_string_invalid_hex_char:
+ return "invalid (non-hex) character occurs after '\\u' inside "
+ "string.";
+ case yajl_lex_invalid_char:
+ return "invalid char in json text.";
+ case yajl_lex_invalid_string:
+ return "invalid string in json text.";
+ case yajl_lex_missing_integer_after_exponent:
+ return "malformed number, a digit is required after the exponent.";
+ case yajl_lex_missing_integer_after_decimal:
+ return "malformed number, a digit is required after the "
+ "decimal point.";
+ case yajl_lex_missing_integer_after_minus:
+ return "malformed number, a digit is required after the "
+ "minus sign.";
+ case yajl_lex_invalid_infinity:
+ return "malformed number, a token inf required for number starting "
+ "from 'i'";
+ case yajl_lex_unallowed_comment:
+ return "probable comment found in input text, comments are "
+ "not enabled.";
+ }
+ return "unknown error code";
+}
+
+
+/** allows access to more specific information about the lexical
+ * error when yajl_lex_lex returns yajl_tok_error. */
+yajl_lex_error
+yajl_lex_get_error(yajl_lexer lexer)
+{
+ if (lexer == NULL) return (yajl_lex_error) -1;
+ return lexer->error;
+}
+
+size_t yajl_lex_current_line(yajl_lexer lexer)
+{
+ return lexer->lineOff;
+}
+
+size_t yajl_lex_current_char(yajl_lexer lexer)
+{
+ return lexer->charOff;
+}
+
+yajl_tok yajl_lex_peek(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t offset)
+{
+ const unsigned char * outBuf;
+ size_t outLen;
+ size_t bufLen = yajl_buf_len(lexer->buf);
+ size_t bufOff = lexer->bufOff;
+ unsigned int bufInUse = lexer->bufInUse;
+ yajl_tok tok;
+
+ tok = yajl_lex_lex(lexer, jsonText, jsonTextLen, &offset,
+ &outBuf, &outLen);
+
+ lexer->bufOff = bufOff;
+ lexer->bufInUse = bufInUse;
+ yajl_buf_truncate(lexer->buf, bufLen);
+
+ return tok;
+}
+
+size_t yajl_lex_buf_capacity(yajl_lexer lexer)
+{
+ return yajl_buf_capacity(lexer->buf);
+}
diff --git a/contrib/libs/yajl/yajl_lex.h b/contrib/libs/yajl/yajl_lex.h
new file mode 100644
index 0000000000..309c29c54c
--- /dev/null
+++ b/contrib/libs/yajl/yajl_lex.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_LEX_H__
+#define __YAJL_LEX_H__
+
+#include "api/yajl_common.h"
+
+typedef enum {
+ yajl_tok_bool,
+ yajl_tok_colon,
+ yajl_tok_comma,
+ yajl_tok_eof,
+ yajl_tok_error,
+ yajl_tok_left_brace,
+ yajl_tok_left_bracket,
+ yajl_tok_null,
+ yajl_tok_inf,
+ yajl_tok_minus_inf,
+ yajl_tok_right_brace,
+ yajl_tok_right_bracket,
+
+ /* we differentiate between integers and doubles to allow the
+ * parser to interpret the number without re-scanning */
+ yajl_tok_integer,
+ yajl_tok_double,
+
+ /* we differentiate between strings which require further processing,
+ * and strings that do not */
+ yajl_tok_string,
+ yajl_tok_string_with_escapes,
+
+ /* comment tokens are not currently returned to the parser, ever */
+ yajl_tok_comment
+} yajl_tok;
+
+typedef struct yajl_lexer_t * yajl_lexer;
+
+yajl_lexer yajl_lex_alloc(yajl_alloc_funcs * alloc,
+ unsigned int allowComments,
+ unsigned int validateUTF8);
+
+void yajl_lex_free(yajl_lexer lexer);
+
+/**
+ * run/continue a lex. "offset" is an input/output parameter.
+ * It should be initialized to zero for a
+ * new chunk of target text, and upon subsetquent calls with the same
+ * target text should passed with the value of the previous invocation.
+ *
+ * the client may be interested in the value of offset when an error is
+ * returned from the lexer. This allows the client to render useful
+ * error messages.
+ *
+ * When you pass the next chunk of data, context should be reinitialized
+ * to zero.
+ *
+ * Finally, the output buffer is usually just a pointer into the jsonText,
+ * however in cases where the entity being lexed spans multiple chunks,
+ * the lexer will buffer the entity and the data returned will be
+ * a pointer into that buffer.
+ *
+ * This behavior is abstracted from client code except for the performance
+ * implications which require that the client choose a reasonable chunk
+ * size to get adequate performance.
+ */
+yajl_tok yajl_lex_lex(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ const unsigned char ** outBuf, size_t * outLen);
+
+/** have a peek at the next token, but don't move the lexer forward */
+yajl_tok yajl_lex_peek(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t offset);
+
+
+typedef enum {
+ yajl_lex_e_ok = 0,
+ yajl_lex_string_invalid_utf8,
+ yajl_lex_string_invalid_escaped_char,
+ yajl_lex_string_invalid_json_char,
+ yajl_lex_string_invalid_hex_char,
+ yajl_lex_invalid_char,
+ yajl_lex_invalid_string,
+ yajl_lex_missing_integer_after_decimal,
+ yajl_lex_missing_integer_after_exponent,
+ yajl_lex_missing_integer_after_minus,
+ yajl_lex_invalid_infinity,
+ yajl_lex_unallowed_comment
+} yajl_lex_error;
+
+const char * yajl_lex_error_to_string(yajl_lex_error error);
+
+/** allows access to more specific information about the lexical
+ * error when yajl_lex_lex returns yajl_tok_error. */
+yajl_lex_error yajl_lex_get_error(yajl_lexer lexer);
+
+/** get the current offset into the most recently lexed json string. */
+size_t yajl_lex_current_offset(yajl_lexer lexer);
+
+/** get the number of lines lexed by this lexer instance */
+size_t yajl_lex_current_line(yajl_lexer lexer);
+
+/** get the number of chars lexed by this lexer instance since the last
+ * \n or \r */
+size_t yajl_lex_current_char(yajl_lexer lexer);
+
+/** get size of allocated buffer */
+size_t yajl_lex_buf_capacity(yajl_lexer lexer);
+
+#endif
diff --git a/contrib/libs/yajl/yajl_parser.c b/contrib/libs/yajl/yajl_parser.c
new file mode 100644
index 0000000000..d1758d67c9
--- /dev/null
+++ b/contrib/libs/yajl/yajl_parser.c
@@ -0,0 +1,498 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_parse.h"
+#include "yajl_lex.h"
+#include "yajl_parser.h"
+#include "yajl_encode.h"
+#include "yajl_bytestack.h"
+
+#include <stdlib.h>
+#include <limits.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <math.h>
+
+
+unsigned char *
+yajl_render_error_string(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen, int verbose)
+{
+ size_t offset = hand->bytesConsumed;
+ unsigned char * str;
+ const char * errorType = NULL;
+ const char * errorText = NULL;
+ char text[72];
+ const char * arrow = " (right here) ------^\n";
+
+ if (yajl_bs_current(hand->stateStack) == yajl_state_parse_error) {
+ errorType = "parse";
+ errorText = hand->parseError;
+ } else if (yajl_bs_current(hand->stateStack) == yajl_state_lexical_error) {
+ errorType = "lexical";
+ errorText = yajl_lex_error_to_string(yajl_lex_get_error(hand->lexer));
+ } else {
+ errorType = "unknown";
+ }
+
+ {
+ size_t memneeded = 0;
+ memneeded += strlen(errorType);
+ memneeded += strlen(" error");
+ if (errorText != NULL) {
+ memneeded += strlen(": ");
+ memneeded += strlen(errorText);
+ }
+ str = (unsigned char *) YA_MALLOC(&(hand->alloc), memneeded + 2);
+ if (!str) return NULL;
+ str[0] = 0;
+ strcat((char *) str, errorType);
+ strcat((char *) str, " error");
+ if (errorText != NULL) {
+ strcat((char *) str, ": ");
+ strcat((char *) str, errorText);
+ }
+ strcat((char *) str, "\n");
+ }
+
+ /* now we append as many spaces as needed to make sure the error
+ * falls at char 41, if verbose was specified */
+ if (verbose) {
+ size_t start, end, i;
+ size_t spacesNeeded;
+
+ spacesNeeded = (offset < 30 ? 40 - offset : 10);
+ start = (offset >= 30 ? offset - 30 : 0);
+ end = (offset + 30 > jsonTextLen ? jsonTextLen : offset + 30);
+
+ for (i=0;i<spacesNeeded;i++) text[i] = ' ';
+
+ for (;start < end;start++, i++) {
+ if (jsonText[start] != '\n' && jsonText[start] != '\r')
+ {
+ text[i] = jsonText[start];
+ }
+ else
+ {
+ text[i] = ' ';
+ }
+ }
+ assert(i <= 71);
+ text[i++] = '\n';
+ text[i] = 0;
+ {
+ char * newStr = (char *)
+ YA_MALLOC(&(hand->alloc), (unsigned int)(strlen((char *) str) +
+ strlen((char *) text) +
+ strlen(arrow) + 1));
+ if (newStr) {
+ newStr[0] = 0;
+ strcat((char *) newStr, (char *) str);
+ strcat((char *) newStr, text);
+ strcat((char *) newStr, arrow);
+ }
+ YA_FREE(&(hand->alloc), str);
+ str = (unsigned char *) newStr;
+ }
+ }
+ return str;
+}
+
+/* check for client cancelation */
+#define _CC_CHK(x) \
+ if (!(x)) { \
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error); \
+ hand->parseError = \
+ "client cancelled parse via callback return value"; \
+ return yajl_status_client_canceled; \
+ }
+
+
+yajl_status
+yajl_do_finish(yajl_handle hand)
+{
+ yajl_status stat;
+ stat = yajl_do_parse(hand,(const unsigned char *) " ",1);
+
+ if (stat != yajl_status_ok) return stat;
+
+ switch(yajl_bs_current(hand->stateStack))
+ {
+ case yajl_state_parse_error:
+ case yajl_state_lexical_error:
+ return yajl_status_error;
+ case yajl_state_got_value:
+ case yajl_state_parse_complete:
+ return yajl_status_ok;
+ default:
+ if (!(hand->flags & yajl_allow_partial_values))
+ {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "premature EOF";
+ return yajl_status_error;
+ }
+ return yajl_status_ok;
+ }
+}
+
+yajl_status
+yajl_do_parse(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen)
+{
+ yajl_tok tok;
+ const unsigned char * buf;
+ size_t bufLen;
+ size_t * offset = &(hand->bytesConsumed);
+
+ *offset = 0;
+
+ around_again:
+ switch (yajl_bs_current(hand->stateStack)) {
+ case yajl_state_parse_complete:
+ if (hand->flags & yajl_allow_multiple_values) {
+ yajl_bs_set(hand->stateStack, yajl_state_got_value);
+ goto around_again;
+ }
+ if (!(hand->flags & yajl_allow_trailing_garbage)) {
+ if (*offset != jsonTextLen) {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ if (tok != yajl_tok_eof) {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "trailing garbage";
+ }
+ goto around_again;
+ }
+ }
+ return yajl_status_ok;
+ case yajl_state_lexical_error:
+ case yajl_state_parse_error:
+ return yajl_status_error;
+ case yajl_state_start:
+ case yajl_state_got_value:
+ case yajl_state_map_need_val:
+ case yajl_state_array_need_val:
+ case yajl_state_array_start: {
+ /* for arrays and maps, we advance the state for this
+ * depth, then push the state of the next depth.
+ * If an error occurs during the parsing of the nesting
+ * enitity, the state at this level will not matter.
+ * a state that needs pushing will be anything other
+ * than state_start */
+
+ yajl_state stateToPush = yajl_state_start;
+
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+
+ switch (tok) {
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ case yajl_tok_string:
+ if (hand->callbacks && hand->callbacks->yajl_string) {
+ _CC_CHK(hand->callbacks->yajl_string(hand->ctx,
+ buf, bufLen));
+ }
+ break;
+ case yajl_tok_string_with_escapes:
+ if (hand->callbacks && hand->callbacks->yajl_string) {
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_string_decode(hand->decodeBuf, buf, bufLen);
+ _CC_CHK(hand->callbacks->yajl_string(
+ hand->ctx, yajl_buf_data(hand->decodeBuf),
+ yajl_buf_len(hand->decodeBuf)));
+ }
+ break;
+ case yajl_tok_bool:
+ if (hand->callbacks && hand->callbacks->yajl_boolean) {
+ _CC_CHK(hand->callbacks->yajl_boolean(hand->ctx,
+ *buf == 't'));
+ }
+ break;
+ case yajl_tok_null:
+ if (hand->callbacks && hand->callbacks->yajl_null) {
+ _CC_CHK(hand->callbacks->yajl_null(hand->ctx));
+ }
+ break;
+ case yajl_tok_inf:
+ if (hand->callbacks && hand->callbacks->yajl_double) {
+ _CC_CHK(hand->callbacks->yajl_double(hand->ctx, INFINITY));
+ }
+ break;
+ case yajl_tok_minus_inf:
+ if (hand->callbacks && hand->callbacks->yajl_double) {
+ _CC_CHK(hand->callbacks->yajl_double(hand->ctx, -INFINITY));
+ }
+ break;
+ case yajl_tok_left_bracket:
+ if (hand->callbacks && hand->callbacks->yajl_start_map) {
+ _CC_CHK(hand->callbacks->yajl_start_map(hand->ctx));
+ }
+ stateToPush = yajl_state_map_start;
+ break;
+ case yajl_tok_left_brace:
+ if (hand->callbacks && hand->callbacks->yajl_start_array) {
+ _CC_CHK(hand->callbacks->yajl_start_array(hand->ctx));
+ }
+ stateToPush = yajl_state_array_start;
+ break;
+ case yajl_tok_integer:
+ if (hand->callbacks) {
+ if (hand->callbacks->yajl_number) {
+ _CC_CHK(hand->callbacks->yajl_number(
+ hand->ctx,(const char *) buf, bufLen));
+ } else if (hand->callbacks->yajl_integer || hand->callbacks->yajl_unsigned_integer) {
+ // NB: We want to use errno for checking parsing correctness.
+ errno = 0;
+ long long int i = 0;
+ unsigned long long int ui = 0;
+ if (*buf == '-') {
+ i = yajl_parse_integer(buf, bufLen);
+ if (errno == 0) {
+ if (hand->callbacks->yajl_integer) {
+ _CC_CHK(hand->callbacks->yajl_integer(hand->ctx,
+ i));
+ }
+ }
+ } else {
+ ui = yajl_parse_unsigned_integer(buf, bufLen);
+ if (errno == 0) {
+ if (ui <= LLONG_MAX) {
+ if (hand->callbacks->yajl_integer) {
+ _CC_CHK(hand->callbacks->yajl_integer(hand->ctx,
+ ui));
+ }
+ } else {
+ if (hand->callbacks->yajl_unsigned_integer) {
+ _CC_CHK(hand->callbacks->yajl_unsigned_integer(hand->ctx,
+ ui));
+ }
+ }
+ }
+ }
+ if (errno == ERANGE) {
+ yajl_bs_set(hand->stateStack,
+ yajl_state_parse_error);
+ hand->parseError = "integer overflow" ;
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ }
+ }
+ break;
+ case yajl_tok_double:
+ if (hand->callbacks) {
+ if (hand->callbacks->yajl_number) {
+ _CC_CHK(hand->callbacks->yajl_number(
+ hand->ctx, (const char *) buf, bufLen));
+ } else if (hand->callbacks->yajl_double) {
+ double d = 0.0;
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_buf_append(hand->decodeBuf, buf, bufLen);
+ buf = yajl_buf_data(hand->decodeBuf);
+ errno = 0;
+ d = strtod((char *) buf, NULL);
+ if ((d == HUGE_VAL || d == -HUGE_VAL) &&
+ errno == ERANGE)
+ {
+ yajl_bs_set(hand->stateStack,
+ yajl_state_parse_error);
+ hand->parseError = "numeric (floating point) "
+ "overflow";
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ _CC_CHK(hand->callbacks->yajl_double(hand->ctx,
+ d));
+ }
+ }
+ break;
+ case yajl_tok_right_brace: {
+ if (yajl_bs_current(hand->stateStack) ==
+ yajl_state_array_start)
+ {
+ if (hand->callbacks &&
+ hand->callbacks->yajl_end_array)
+ {
+ _CC_CHK(hand->callbacks->yajl_end_array(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ }
+ /* intentional fall-through */
+ }
+ case yajl_tok_colon:
+ case yajl_tok_comma:
+ case yajl_tok_right_bracket:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "unallowed token at this point in JSON text";
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "invalid token, internal error";
+ goto around_again;
+ }
+ /* got a value. transition depends on the state we're in. */
+ {
+ yajl_state s = yajl_bs_current(hand->stateStack);
+ if (s == yajl_state_start || s == yajl_state_got_value) {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_complete);
+ } else if (s == yajl_state_map_need_val) {
+ yajl_bs_set(hand->stateStack, yajl_state_map_got_val);
+ } else {
+ yajl_bs_set(hand->stateStack, yajl_state_array_got_val);
+ }
+ }
+ if (stateToPush != yajl_state_start) {
+ yajl_bs_push(hand->stateStack, stateToPush);
+ }
+
+ goto around_again;
+ }
+ case yajl_state_map_start:
+ case yajl_state_map_need_key: {
+ /* only difference between these two states is that in
+ * start '}' is valid, whereas in need_key, we've parsed
+ * a comma, and a string key _must_ follow */
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ case yajl_tok_string_with_escapes:
+ if (hand->callbacks && hand->callbacks->yajl_map_key) {
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_string_decode(hand->decodeBuf, buf, bufLen);
+ buf = yajl_buf_data(hand->decodeBuf);
+ bufLen = yajl_buf_len(hand->decodeBuf);
+ }
+ /* intentional fall-through */
+ case yajl_tok_string:
+ if (hand->callbacks && hand->callbacks->yajl_map_key) {
+ _CC_CHK(hand->callbacks->yajl_map_key(hand->ctx, buf,
+ bufLen));
+ }
+ yajl_bs_set(hand->stateStack, yajl_state_map_sep);
+ goto around_again;
+ case yajl_tok_right_bracket:
+ if (yajl_bs_current(hand->stateStack) ==
+ yajl_state_map_start)
+ {
+ if (hand->callbacks && hand->callbacks->yajl_end_map) {
+ _CC_CHK(hand->callbacks->yajl_end_map(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ }
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "invalid object key (must be a string)";
+ goto around_again;
+ }
+ }
+ case yajl_state_map_sep: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_colon:
+ yajl_bs_set(hand->stateStack, yajl_state_map_need_val);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "object key and value must "
+ "be separated by a colon (':')";
+ goto around_again;
+ }
+ }
+ case yajl_state_map_got_val: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_right_bracket:
+ if (hand->callbacks && hand->callbacks->yajl_end_map) {
+ _CC_CHK(hand->callbacks->yajl_end_map(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ case yajl_tok_comma:
+ yajl_bs_set(hand->stateStack, yajl_state_map_need_key);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "after key and value, inside map, "
+ "I expect ',' or '}'";
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ }
+ case yajl_state_array_got_val: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_right_brace:
+ if (hand->callbacks && hand->callbacks->yajl_end_array) {
+ _CC_CHK(hand->callbacks->yajl_end_array(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ case yajl_tok_comma:
+ yajl_bs_set(hand->stateStack, yajl_state_array_need_val);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "after array element, I expect ',' or ']'";
+ goto around_again;
+ }
+ }
+ }
+
+ abort();
+ return yajl_status_error;
+}
+
diff --git a/contrib/libs/yajl/yajl_parser.cpp b/contrib/libs/yajl/yajl_parser.cpp
new file mode 100644
index 0000000000..fc60530682
--- /dev/null
+++ b/contrib/libs/yajl/yajl_parser.cpp
@@ -0,0 +1,29 @@
+#include "yajl_parser.h"
+
+#include <errno.h>
+
+#include <util/string/cast.h>
+
+long long
+yajl_parse_integer(const unsigned char *number, unsigned int length) {
+ try {
+ return FromString<long long>((const char*)number, length);
+ } catch (const yexception& ex) {
+ errno = ERANGE;
+ return (*number == '-') ? LLONG_MIN : LLONG_MAX;
+ }
+}
+
+unsigned long long
+yajl_parse_unsigned_integer(const unsigned char *number, unsigned int length) {
+ try {
+ return FromString<unsigned long long>((const char*)number, length);
+ } catch (const yexception& ex) {
+ errno = ERANGE;
+ return (*number == '-') ? 0ull : ULLONG_MAX;
+ }
+}
+
+extern "C" void FormatDoubleYandex(char* buf, size_t len, double val) {
+ buf[ToString(val, buf, len)] = 0;
+}
diff --git a/contrib/libs/yajl/yajl_parser.h b/contrib/libs/yajl/yajl_parser.h
new file mode 100644
index 0000000000..6ecc0f3a5c
--- /dev/null
+++ b/contrib/libs/yajl/yajl_parser.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2007-2011, Lloyd Hilaiel <lloyd@hilaiel.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_PARSER_H__
+#define __YAJL_PARSER_H__
+
+#include "api/yajl_parse.h"
+#include "yajl_bytestack.h"
+#include "yajl_buf.h"
+#include "yajl_lex.h"
+
+
+typedef enum {
+ yajl_state_start = 0,
+ yajl_state_parse_complete,
+ yajl_state_parse_error,
+ yajl_state_lexical_error,
+ yajl_state_map_start,
+ yajl_state_map_sep,
+ yajl_state_map_need_val,
+ yajl_state_map_got_val,
+ yajl_state_map_need_key,
+ yajl_state_array_start,
+ yajl_state_array_got_val,
+ yajl_state_array_need_val,
+ yajl_state_got_value,
+} yajl_state;
+
+struct yajl_handle_t {
+ const yajl_callbacks * callbacks;
+ void * ctx;
+ yajl_lexer lexer;
+ const char * parseError;
+ /* the number of bytes consumed from the last client buffer,
+ * in the case of an error this will be an error offset, in the
+ * case of an error this can be used as the error offset */
+ size_t bytesConsumed;
+ /* temporary storage for decoded strings */
+ yajl_buf decodeBuf;
+ /* a stack of states. access with yajl_state_XXX routines */
+ yajl_bytestack stateStack;
+ /* memory allocation routines */
+ yajl_alloc_funcs alloc;
+ /* bitfield */
+ unsigned int flags;
+ /* memory limit */
+ unsigned long memoryLimit;
+};
+
+yajl_status
+yajl_do_parse(yajl_handle handle, const unsigned char * jsonText,
+ size_t jsonTextLen);
+
+yajl_status
+yajl_do_finish(yajl_handle handle);
+
+unsigned char *
+yajl_render_error_string(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen, int verbose);
+
+/* A little built in integer parsing routine with the same semantics as strtol
+ * that's unaffected by LOCALE. */
+#ifdef __cplusplus
+extern "C"
+#endif
+long long
+yajl_parse_integer(const unsigned char *number, unsigned int length);
+
+#ifdef __cplusplus
+extern "C"
+#endif
+unsigned long long
+yajl_parse_unsigned_integer(const unsigned char *number, unsigned int length);
+
+
+#endif
diff --git a/contrib/libs/yajl/yajl_tree.c b/contrib/libs/yajl/yajl_tree.c
new file mode 100644
index 0000000000..c5874e660e
--- /dev/null
+++ b/contrib/libs/yajl/yajl_tree.c
@@ -0,0 +1,510 @@
+/*
+ * Copyright (c) 2010-2011 Florian Forster <ff at octo.it>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "api/yajl_tree.h"
+#include "api/yajl_parse.h"
+
+#include "yajl_parser.h"
+
+#ifdef WIN32
+#define snprintf sprintf_s
+#endif
+
+#define STATUS_CONTINUE 1
+#define STATUS_ABORT 0
+
+struct stack_elem_s;
+typedef struct stack_elem_s stack_elem_t;
+struct stack_elem_s
+{
+ char * key;
+ yajl_val value;
+ stack_elem_t *next;
+};
+
+struct context_s
+{
+ stack_elem_t *stack;
+ yajl_val root;
+ char *errbuf;
+ size_t errbuf_size;
+};
+typedef struct context_s context_t;
+
+#define RETURN_ERROR(ctx,retval,...) { \
+ if ((ctx)->errbuf != NULL) \
+ snprintf ((ctx)->errbuf, (ctx)->errbuf_size, __VA_ARGS__); \
+ return (retval); \
+ }
+
+static yajl_val value_alloc (yajl_type type)
+{
+ yajl_val v;
+
+ v = malloc (sizeof (*v));
+ if (v == NULL) return (NULL);
+ memset (v, 0, sizeof (*v));
+ v->type = type;
+
+ return (v);
+}
+
+static void yajl_object_free (yajl_val v)
+{
+ size_t i;
+
+ if (!YAJL_IS_OBJECT(v)) return;
+
+ for (i = 0; i < v->u.object.len; i++)
+ {
+ free((char *) v->u.object.keys[i]);
+ v->u.object.keys[i] = NULL;
+ yajl_tree_free (v->u.object.values[i]);
+ v->u.object.values[i] = NULL;
+ }
+
+ free((void*) v->u.object.keys);
+ free(v->u.object.values);
+ free(v);
+}
+
+static void yajl_array_free (yajl_val v)
+{
+ size_t i;
+
+ if (!YAJL_IS_ARRAY(v)) return;
+
+ for (i = 0; i < v->u.array.len; i++)
+ {
+ yajl_tree_free (v->u.array.values[i]);
+ v->u.array.values[i] = NULL;
+ }
+
+ free(v->u.array.values);
+ free(v);
+}
+
+/*
+ * Parsing nested objects and arrays is implemented using a stack. When a new
+ * object or array starts (a curly or a square opening bracket is read), an
+ * appropriate value is pushed on the stack. When the end of the object is
+ * reached (an appropriate closing bracket has been read), the value is popped
+ * off the stack and added to the enclosing object using "context_add_value".
+ */
+static int context_push(context_t *ctx, yajl_val v)
+{
+ stack_elem_t *stack;
+
+ stack = malloc (sizeof (*stack));
+ if (stack == NULL)
+ RETURN_ERROR (ctx, ENOMEM, "Out of memory");
+ memset (stack, 0, sizeof (*stack));
+
+ assert ((ctx->stack == NULL)
+ || YAJL_IS_OBJECT (v)
+ || YAJL_IS_ARRAY (v));
+
+ stack->value = v;
+ stack->next = ctx->stack;
+ ctx->stack = stack;
+
+ return (0);
+}
+
+static yajl_val context_pop(context_t *ctx)
+{
+ stack_elem_t *stack;
+ yajl_val v;
+
+ if (ctx->stack == NULL)
+ RETURN_ERROR (ctx, NULL, "context_pop: "
+ "Bottom of stack reached prematurely");
+
+ stack = ctx->stack;
+ ctx->stack = stack->next;
+
+ v = stack->value;
+
+ free (stack);
+
+ return (v);
+}
+
+static int object_add_keyval(context_t *ctx,
+ yajl_val obj, char *key, yajl_val value)
+{
+ const char **tmpk;
+ yajl_val *tmpv;
+
+ /* We're checking for NULL in "context_add_value" or its callers. */
+ assert (ctx != NULL);
+ assert (obj != NULL);
+ assert (key != NULL);
+ assert (value != NULL);
+
+ /* We're assuring that "obj" is an object in "context_add_value". */
+ assert(YAJL_IS_OBJECT(obj));
+
+ tmpk = realloc((void *) obj->u.object.keys, sizeof(*(obj->u.object.keys)) * (obj->u.object.len + 1));
+ if (tmpk == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ obj->u.object.keys = tmpk;
+
+ tmpv = realloc(obj->u.object.values, sizeof (*obj->u.object.values) * (obj->u.object.len + 1));
+ if (tmpv == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ obj->u.object.values = tmpv;
+
+ obj->u.object.keys[obj->u.object.len] = key;
+ obj->u.object.values[obj->u.object.len] = value;
+ obj->u.object.len++;
+
+ return (0);
+}
+
+static int array_add_value (context_t *ctx,
+ yajl_val array, yajl_val value)
+{
+ yajl_val *tmp;
+
+ /* We're checking for NULL pointers in "context_add_value" or its
+ * callers. */
+ assert (ctx != NULL);
+ assert (array != NULL);
+ assert (value != NULL);
+
+ /* "context_add_value" will only call us with array values. */
+ assert(YAJL_IS_ARRAY(array));
+
+ tmp = realloc(array->u.array.values,
+ sizeof(*(array->u.array.values)) * (array->u.array.len + 1));
+ if (tmp == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ array->u.array.values = tmp;
+ array->u.array.values[array->u.array.len] = value;
+ array->u.array.len++;
+
+ return 0;
+}
+
+/*
+ * Add a value to the value on top of the stack or the "root" member in the
+ * context if the end of the parsing process is reached.
+ */
+static int context_add_value (context_t *ctx, yajl_val v)
+{
+ /* We're checking for NULL values in all the calling functions. */
+ assert (ctx != NULL);
+ assert (v != NULL);
+
+ /*
+ * There are three valid states in which this function may be called:
+ * - There is no value on the stack => This is the only value. This is the
+ * last step done when parsing a document. We assign the value to the
+ * "root" member and return.
+ * - The value on the stack is an object. In this case store the key on the
+ * stack or, if the key has already been read, add key and value to the
+ * object.
+ * - The value on the stack is an array. In this case simply add the value
+ * and return.
+ */
+ if (ctx->stack == NULL)
+ {
+ assert (ctx->root == NULL);
+ ctx->root = v;
+ return (0);
+ }
+ else if (YAJL_IS_OBJECT (ctx->stack->value))
+ {
+ if (ctx->stack->key == NULL)
+ {
+ if (!YAJL_IS_STRING (v))
+ RETURN_ERROR (ctx, EINVAL, "context_add_value: "
+ "Object key is not a string (%#04x)",
+ v->type);
+
+ ctx->stack->key = v->u.string;
+ v->u.string = NULL;
+ free(v);
+ return (0);
+ }
+ else /* if (ctx->key != NULL) */
+ {
+ char * key;
+
+ key = ctx->stack->key;
+ ctx->stack->key = NULL;
+ return (object_add_keyval (ctx, ctx->stack->value, key, v));
+ }
+ }
+ else if (YAJL_IS_ARRAY (ctx->stack->value))
+ {
+ return (array_add_value (ctx, ctx->stack->value, v));
+ }
+ else
+ {
+ RETURN_ERROR (ctx, EINVAL, "context_add_value: Cannot add value to "
+ "a value of type %#04x (not a composite type)",
+ ctx->stack->value->type);
+ }
+}
+
+static int handle_string (void *ctx,
+ const unsigned char *string, size_t string_length)
+{
+ yajl_val v;
+
+ v = value_alloc (yajl_t_string);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.string = malloc (string_length + 1);
+ if (v->u.string == NULL)
+ {
+ free (v);
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+ }
+ memcpy(v->u.string, string, string_length);
+ v->u.string[string_length] = 0;
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_number (void *ctx, const char *string, size_t string_length)
+{
+ yajl_val v;
+ char *endptr;
+
+ v = value_alloc(yajl_t_number);
+ if (v == NULL)
+ RETURN_ERROR((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.number.r = malloc(string_length + 1);
+ if (v->u.number.r == NULL)
+ {
+ free(v);
+ RETURN_ERROR((context_t *) ctx, STATUS_ABORT, "Out of memory");
+ }
+ memcpy(v->u.number.r, string, string_length);
+ v->u.number.r[string_length] = 0;
+
+ v->u.number.flags = 0;
+
+ endptr = NULL;
+ errno = 0;
+ v->u.number.i = yajl_parse_integer((const unsigned char *) v->u.number.r,
+ strlen(v->u.number.r));
+ if ((errno == 0) && (endptr != NULL) && (*endptr == 0))
+ v->u.number.flags |= YAJL_NUMBER_INT_VALID;
+
+ endptr = NULL;
+ errno = 0;
+ v->u.number.ui = yajl_parse_unsigned_integer((const unsigned char *) v->u.number.r,
+ strlen(v->u.number.r));
+
+ if ((errno == 0) && (endptr != NULL) && (*endptr == 0))
+ v->u.number.flags |= YAJL_NUMBER_UINT_VALID;
+
+ endptr = NULL;
+ errno = 0;
+ v->u.number.d = strtod(v->u.number.r, &endptr);
+ if ((errno == 0) && (endptr != NULL) && (*endptr == 0))
+ v->u.number.flags |= YAJL_NUMBER_DOUBLE_VALID;
+
+ return ((context_add_value(ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_start_map (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc(yajl_t_object);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.object.keys = NULL;
+ v->u.object.values = NULL;
+ v->u.object.len = 0;
+
+ return ((context_push (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_end_map (void *ctx)
+{
+ yajl_val v;
+
+ v = context_pop (ctx);
+ if (v == NULL)
+ return (STATUS_ABORT);
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_start_array (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc(yajl_t_array);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.array.values = NULL;
+ v->u.array.len = 0;
+
+ return ((context_push (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_end_array (void *ctx)
+{
+ yajl_val v;
+
+ v = context_pop (ctx);
+ if (v == NULL)
+ return (STATUS_ABORT);
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_boolean (void *ctx, int boolean_value)
+{
+ yajl_val v;
+
+ v = value_alloc (boolean_value ? yajl_t_true : yajl_t_false);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_null (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc (yajl_t_null);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+/*
+ * Public functions
+ */
+yajl_val yajl_tree_parse (const char *input,
+ char *error_buffer, size_t error_buffer_size)
+{
+ static const yajl_callbacks callbacks =
+ {
+ /* null = */ handle_null,
+ /* boolean = */ handle_boolean,
+ /* integer = */ NULL,
+ /* unsigned integer = */ NULL,
+ /* double = */ NULL,
+ /* number = */ handle_number,
+ /* string = */ handle_string,
+ /* start map = */ handle_start_map,
+ /* map key = */ handle_string,
+ /* end map = */ handle_end_map,
+ /* start array = */ handle_start_array,
+ /* end array = */ handle_end_array
+ };
+
+ yajl_handle handle;
+ yajl_status status;
+ context_t ctx = { NULL, NULL, NULL, 0 };
+
+ ctx.errbuf = error_buffer;
+ ctx.errbuf_size = error_buffer_size;
+
+ if (error_buffer != NULL)
+ memset (error_buffer, 0, error_buffer_size);
+
+ handle = yajl_alloc (&callbacks, NULL, &ctx);
+ yajl_config(handle, yajl_allow_comments, 1);
+
+ status = yajl_parse(handle,
+ (unsigned char *) input,
+ strlen (input));
+ status = yajl_complete_parse (handle);
+ if (status != yajl_status_ok) {
+ if (error_buffer != NULL && error_buffer_size > 0) {
+ snprintf(
+ error_buffer, error_buffer_size, "%s",
+ (char *) yajl_get_error(handle, 1,
+ (const unsigned char *) input,
+ strlen(input)));
+ }
+ yajl_free (handle);
+ return NULL;
+ }
+
+ yajl_free (handle);
+ return (ctx.root);
+}
+
+yajl_val yajl_tree_get(yajl_val n, const char ** path, yajl_type type)
+{
+ if (!path) return NULL;
+ while (n && *path) {
+ unsigned int i;
+
+ if (n->type != yajl_t_object) return NULL;
+ for (i = 0; i < n->u.object.len; i++) {
+ if (!strcmp(*path, n->u.object.keys[i])) {
+ n = n->u.object.values[i];
+ break;
+ }
+ }
+ if (i == n->u.object.len) return NULL;
+ path++;
+ }
+ if (n && type != yajl_t_any && type != n->type) n = NULL;
+ return n;
+}
+
+void yajl_tree_free (yajl_val v)
+{
+ if (v == NULL) return;
+
+ if (YAJL_IS_STRING(v))
+ {
+ free(v->u.string);
+ free(v);
+ }
+ else if (YAJL_IS_NUMBER(v))
+ {
+ free(v->u.number.r);
+ free(v);
+ }
+ else if (YAJL_GET_OBJECT(v))
+ {
+ yajl_object_free(v);
+ }
+ else if (YAJL_GET_ARRAY(v))
+ {
+ yajl_array_free(v);
+ }
+ else /* if (yajl_t_true or yajl_t_false or yajl_t_null) */
+ {
+ free(v);
+ }
+}
diff --git a/contrib/libs/yajl/yajl_version.c b/contrib/libs/yajl/yajl_version.c
new file mode 100644
index 0000000000..618ac1b77e
--- /dev/null
+++ b/contrib/libs/yajl/yajl_version.c
@@ -0,0 +1,7 @@
+#include "api/yajl_version.h"
+
+int yajl_version(void)
+{
+ return YAJL_VERSION;
+}
+
diff --git a/contrib/restricted/http-parser/AUTHORS b/contrib/restricted/http-parser/AUTHORS
new file mode 100644
index 0000000000..5323b685ca
--- /dev/null
+++ b/contrib/restricted/http-parser/AUTHORS
@@ -0,0 +1,68 @@
+# Authors ordered by first contribution.
+Ryan Dahl <ry@tinyclouds.org>
+Jeremy Hinegardner <jeremy@hinegardner.org>
+Sergey Shepelev <temotor@gmail.com>
+Joe Damato <ice799@gmail.com>
+tomika <tomika_nospam@freemail.hu>
+Phoenix Sol <phoenix@burninglabs.com>
+Cliff Frey <cliff@meraki.com>
+Ewen Cheslack-Postava <ewencp@cs.stanford.edu>
+Santiago Gala <sgala@apache.org>
+Tim Becker <tim.becker@syngenio.de>
+Jeff Terrace <jterrace@gmail.com>
+Ben Noordhuis <info@bnoordhuis.nl>
+Nathan Rajlich <nathan@tootallnate.net>
+Mark Nottingham <mnot@mnot.net>
+Aman Gupta <aman@tmm1.net>
+Tim Becker <tim.becker@kuriositaet.de>
+Sean Cunningham <sean.cunningham@mandiant.com>
+Peter Griess <pg@std.in>
+Salman Haq <salman.haq@asti-usa.com>
+Cliff Frey <clifffrey@gmail.com>
+Jon Kolb <jon@b0g.us>
+Fouad Mardini <f.mardini@gmail.com>
+Paul Querna <pquerna@apache.org>
+Felix Geisendörfer <felix@debuggable.com>
+koichik <koichik@improvement.jp>
+Andre Caron <andre.l.caron@gmail.com>
+Ivo Raisr <ivosh@ivosh.net>
+James McLaughlin <jamie@lacewing-project.org>
+David Gwynne <loki@animata.net>
+Thomas LE ROUX <thomas@november-eleven.fr>
+Randy Rizun <rrizun@ortivawireless.com>
+Andre Louis Caron <andre.louis.caron@usherbrooke.ca>
+Simon Zimmermann <simonz05@gmail.com>
+Erik Dubbelboer <erik@dubbelboer.com>
+Martell Malone <martellmalone@gmail.com>
+Bertrand Paquet <bpaquet@octo.com>
+BogDan Vatra <bogdan@kde.org>
+Peter Faiman <peter@thepicard.org>
+Corey Richardson <corey@octayn.net>
+Tóth Tamás <tomika_nospam@freemail.hu>
+Cam Swords <cam.swords@gmail.com>
+Chris Dickinson <christopher.s.dickinson@gmail.com>
+Uli Köhler <ukoehler@btronik.de>
+Charlie Somerville <charlie@charliesomerville.com>
+Patrik Stutz <patrik.stutz@gmail.com>
+Fedor Indutny <fedor.indutny@gmail.com>
+runner <runner.mei@gmail.com>
+Alexis Campailla <alexis@janeasystems.com>
+David Wragg <david@wragg.org>
+Vinnie Falco <vinnie.falco@gmail.com>
+Alex Butum <alexbutum@linux.com>
+Rex Feng <rexfeng@gmail.com>
+Alex Kocharin <alex@kocharin.ru>
+Mark Koopman <markmontymark@yahoo.com>
+Helge Heß <me@helgehess.eu>
+Alexis La Goutte <alexis.lagoutte@gmail.com>
+George Miroshnykov <george.miroshnykov@gmail.com>
+Maciej Małecki <me@mmalecki.com>
+Marc O'Morain <github.com@marcomorain.com>
+Jeff Pinner <jpinner@twitter.com>
+Timothy J Fontaine <tjfontaine@gmail.com>
+Akagi201 <akagi201@gmail.com>
+Romain Giraud <giraud.romain@gmail.com>
+Jay Satiro <raysatiro@yahoo.com>
+Arne Steen <Arne.Steen@gmx.de>
+Kjell Schubert <kjell.schubert@gmail.com>
+Olivier Mengué <dolmen@cpan.org>
diff --git a/contrib/restricted/http-parser/LICENSE-MIT b/contrib/restricted/http-parser/LICENSE-MIT
new file mode 100644
index 0000000000..1ec0ab4e17
--- /dev/null
+++ b/contrib/restricted/http-parser/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright Joyent, Inc. and other Node contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/contrib/restricted/http-parser/README.md b/contrib/restricted/http-parser/README.md
new file mode 100644
index 0000000000..b265d71715
--- /dev/null
+++ b/contrib/restricted/http-parser/README.md
@@ -0,0 +1,246 @@
+HTTP Parser
+===========
+
+[![Build Status](https://api.travis-ci.org/nodejs/http-parser.svg?branch=master)](https://travis-ci.org/nodejs/http-parser)
+
+This is a parser for HTTP messages written in C. It parses both requests and
+responses. The parser is designed to be used in performance HTTP
+applications. It does not make any syscalls nor allocations, it does not
+buffer data, it can be interrupted at anytime. Depending on your
+architecture, it only requires about 40 bytes of data per message
+stream (in a web server that is per connection).
+
+Features:
+
+ * No dependencies
+ * Handles persistent streams (keep-alive).
+ * Decodes chunked encoding.
+ * Upgrade support
+ * Defends against buffer overflow attacks.
+
+The parser extracts the following information from HTTP messages:
+
+ * Header fields and values
+ * Content-Length
+ * Request method
+ * Response status code
+ * Transfer-Encoding
+ * HTTP version
+ * Request URL
+ * Message body
+
+
+Usage
+-----
+
+One `http_parser` object is used per TCP connection. Initialize the struct
+using `http_parser_init()` and set the callbacks. That might look something
+like this for a request parser:
+```c
+http_parser_settings settings;
+settings.on_url = my_url_callback;
+settings.on_header_field = my_header_field_callback;
+/* ... */
+
+http_parser *parser = malloc(sizeof(http_parser));
+http_parser_init(parser, HTTP_REQUEST);
+parser->data = my_socket;
+```
+
+When data is received on the socket execute the parser and check for errors.
+
+```c
+size_t len = 80*1024, nparsed;
+char buf[len];
+ssize_t recved;
+
+recved = recv(fd, buf, len, 0);
+
+if (recved < 0) {
+ /* Handle error. */
+}
+
+/* Start up / continue the parser.
+ * Note we pass recved==0 to signal that EOF has been received.
+ */
+nparsed = http_parser_execute(parser, &settings, buf, recved);
+
+if (parser->upgrade) {
+ /* handle new protocol */
+} else if (nparsed != recved) {
+ /* Handle error. Usually just close the connection. */
+}
+```
+
+`http_parser` needs to know where the end of the stream is. For example, sometimes
+servers send responses without Content-Length and expect the client to
+consume input (for the body) until EOF. To tell `http_parser` about EOF, give
+`0` as the fourth parameter to `http_parser_execute()`. Callbacks and errors
+can still be encountered during an EOF, so one must still be prepared
+to receive them.
+
+Scalar valued message information such as `status_code`, `method`, and the
+HTTP version are stored in the parser structure. This data is only
+temporally stored in `http_parser` and gets reset on each new message. If
+this information is needed later, copy it out of the structure during the
+`headers_complete` callback.
+
+The parser decodes the transfer-encoding for both requests and responses
+transparently. That is, a chunked encoding is decoded before being sent to
+the on_body callback.
+
+
+The Special Problem of Upgrade
+------------------------------
+
+`http_parser` supports upgrading the connection to a different protocol. An
+increasingly common example of this is the WebSocket protocol which sends
+a request like
+
+ GET /demo HTTP/1.1
+ Upgrade: WebSocket
+ Connection: Upgrade
+ Host: example.com
+ Origin: http://example.com
+ WebSocket-Protocol: sample
+
+followed by non-HTTP data.
+
+(See [RFC6455](https://tools.ietf.org/html/rfc6455) for more information the
+WebSocket protocol.)
+
+To support this, the parser will treat this as a normal HTTP message without a
+body, issuing both on_headers_complete and on_message_complete callbacks. However
+http_parser_execute() will stop parsing at the end of the headers and return.
+
+The user is expected to check if `parser->upgrade` has been set to 1 after
+`http_parser_execute()` returns. Non-HTTP data begins at the buffer supplied
+offset by the return value of `http_parser_execute()`.
+
+
+Callbacks
+---------
+
+During the `http_parser_execute()` call, the callbacks set in
+`http_parser_settings` will be executed. The parser maintains state and
+never looks behind, so buffering the data is not necessary. If you need to
+save certain data for later usage, you can do that from the callbacks.
+
+There are two types of callbacks:
+
+* notification `typedef int (*http_cb) (http_parser*);`
+ Callbacks: on_message_begin, on_headers_complete, on_message_complete.
+* data `typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);`
+ Callbacks: (requests only) on_url,
+ (common) on_header_field, on_header_value, on_body;
+
+Callbacks must return 0 on success. Returning a non-zero value indicates
+error to the parser, making it exit immediately.
+
+For cases where it is necessary to pass local information to/from a callback,
+the `http_parser` object's `data` field can be used.
+An example of such a case is when using threads to handle a socket connection,
+parse a request, and then give a response over that socket. By instantiation
+of a thread-local struct containing relevant data (e.g. accepted socket,
+allocated memory for callbacks to write into, etc), a parser's callbacks are
+able to communicate data between the scope of the thread and the scope of the
+callback in a threadsafe manner. This allows `http_parser` to be used in
+multi-threaded contexts.
+
+Example:
+```c
+ typedef struct {
+ socket_t sock;
+ void* buffer;
+ int buf_len;
+ } custom_data_t;
+
+
+int my_url_callback(http_parser* parser, const char *at, size_t length) {
+ /* access to thread local custom_data_t struct.
+ Use this access save parsed data for later use into thread local
+ buffer, or communicate over socket
+ */
+ parser->data;
+ ...
+ return 0;
+}
+
+...
+
+void http_parser_thread(socket_t sock) {
+ int nparsed = 0;
+ /* allocate memory for user data */
+ custom_data_t *my_data = malloc(sizeof(custom_data_t));
+
+ /* some information for use by callbacks.
+ * achieves thread -> callback information flow */
+ my_data->sock = sock;
+
+ /* instantiate a thread-local parser */
+ http_parser *parser = malloc(sizeof(http_parser));
+ http_parser_init(parser, HTTP_REQUEST); /* initialise parser */
+ /* this custom data reference is accessible through the reference to the
+ parser supplied to callback functions */
+ parser->data = my_data;
+
+ http_parser_settings settings; /* set up callbacks */
+ settings.on_url = my_url_callback;
+
+ /* execute parser */
+ nparsed = http_parser_execute(parser, &settings, buf, recved);
+
+ ...
+ /* parsed information copied from callback.
+ can now perform action on data copied into thread-local memory from callbacks.
+ achieves callback -> thread information flow */
+ my_data->buffer;
+ ...
+}
+
+```
+
+In case you parse HTTP message in chunks (i.e. `read()` request line
+from socket, parse, read half headers, parse, etc) your data callbacks
+may be called more than once. `http_parser` guarantees that data pointer is only
+valid for the lifetime of callback. You can also `read()` into a heap allocated
+buffer to avoid copying memory around if this fits your application.
+
+Reading headers may be a tricky task if you read/parse headers partially.
+Basically, you need to remember whether last header callback was field or value
+and apply the following logic:
+
+ (on_header_field and on_header_value shortened to on_h_*)
+ ------------------------ ------------ --------------------------------------------
+ | State (prev. callback) | Callback | Description/action |
+ ------------------------ ------------ --------------------------------------------
+ | nothing (first call) | on_h_field | Allocate new buffer and copy callback data |
+ | | | into it |
+ ------------------------ ------------ --------------------------------------------
+ | value | on_h_field | New header started. |
+ | | | Copy current name,value buffers to headers |
+ | | | list and allocate new buffer for new name |
+ ------------------------ ------------ --------------------------------------------
+ | field | on_h_field | Previous name continues. Reallocate name |
+ | | | buffer and append callback data to it |
+ ------------------------ ------------ --------------------------------------------
+ | field | on_h_value | Value for current header started. Allocate |
+ | | | new buffer and copy callback data to it |
+ ------------------------ ------------ --------------------------------------------
+ | value | on_h_value | Value continues. Reallocate value buffer |
+ | | | and append callback data to it |
+ ------------------------ ------------ --------------------------------------------
+
+
+Parsing URLs
+------------
+
+A simplistic zero-copy URL parser is provided as `http_parser_parse_url()`.
+Users of this library may wish to use it to parse URLs constructed from
+consecutive `on_url` callbacks.
+
+See examples of reading in headers:
+
+* [partial example](http://gist.github.com/155877) in C
+* [from http-parser tests](http://github.com/joyent/http-parser/blob/37a0ff8/test.c#L403) in C
+* [from Node library](http://github.com/joyent/node/blob/842eaf4/src/http.js#L284) in Javascript
diff --git a/contrib/restricted/http-parser/http_parser.c b/contrib/restricted/http-parser/http_parser.c
new file mode 100644
index 0000000000..95ff42f783
--- /dev/null
+++ b/contrib/restricted/http-parser/http_parser.c
@@ -0,0 +1,2568 @@
+/* Copyright Joyent, Inc. and other Node contributors.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+#include "http_parser.h"
+#include <assert.h>
+#include <stddef.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+
+static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE;
+
+#ifndef ULLONG_MAX
+# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */
+#endif
+
+#ifndef MIN
+# define MIN(a,b) ((a) < (b) ? (a) : (b))
+#endif
+
+#ifndef ARRAY_SIZE
+# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+#endif
+
+#ifndef BIT_AT
+# define BIT_AT(a, i) \
+ (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \
+ (1 << ((unsigned int) (i) & 7))))
+#endif
+
+#ifndef ELEM_AT
+# define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v))
+#endif
+
+#define SET_ERRNO(e) \
+do { \
+ parser->nread = nread; \
+ parser->http_errno = (e); \
+} while(0)
+
+#define CURRENT_STATE() p_state
+#define UPDATE_STATE(V) p_state = (enum state) (V);
+#define RETURN(V) \
+do { \
+ parser->nread = nread; \
+ parser->state = CURRENT_STATE(); \
+ return (V); \
+} while (0);
+#define REEXECUTE() \
+ goto reexecute; \
+
+
+#ifdef __GNUC__
+# define LIKELY(X) __builtin_expect(!!(X), 1)
+# define UNLIKELY(X) __builtin_expect(!!(X), 0)
+#else
+# define LIKELY(X) (X)
+# define UNLIKELY(X) (X)
+#endif
+
+
+/* Run the notify callback FOR, returning ER if it fails */
+#define CALLBACK_NOTIFY_(FOR, ER) \
+do { \
+ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \
+ \
+ if (LIKELY(settings->on_##FOR)) { \
+ parser->state = CURRENT_STATE(); \
+ if (UNLIKELY(0 != settings->on_##FOR(parser))) { \
+ SET_ERRNO(HPE_CB_##FOR); \
+ } \
+ UPDATE_STATE(parser->state); \
+ \
+ /* We either errored above or got paused; get out */ \
+ if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \
+ return (ER); \
+ } \
+ } \
+} while (0)
+
+/* Run the notify callback FOR and consume the current byte */
+#define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1)
+
+/* Run the notify callback FOR and don't consume the current byte */
+#define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data)
+
+/* Run data callback FOR with LEN bytes, returning ER if it fails */
+#define CALLBACK_DATA_(FOR, LEN, ER) \
+do { \
+ assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \
+ \
+ if (FOR##_mark) { \
+ if (LIKELY(settings->on_##FOR)) { \
+ parser->state = CURRENT_STATE(); \
+ if (UNLIKELY(0 != \
+ settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \
+ SET_ERRNO(HPE_CB_##FOR); \
+ } \
+ UPDATE_STATE(parser->state); \
+ \
+ /* We either errored above or got paused; get out */ \
+ if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \
+ return (ER); \
+ } \
+ } \
+ FOR##_mark = NULL; \
+ } \
+} while (0)
+
+/* Run the data callback FOR and consume the current byte */
+#define CALLBACK_DATA(FOR) \
+ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1)
+
+/* Run the data callback FOR and don't consume the current byte */
+#define CALLBACK_DATA_NOADVANCE(FOR) \
+ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data)
+
+/* Set the mark FOR; non-destructive if mark is already set */
+#define MARK(FOR) \
+do { \
+ if (!FOR##_mark) { \
+ FOR##_mark = p; \
+ } \
+} while (0)
+
+/* Don't allow the total size of the HTTP headers (including the status
+ * line) to exceed max_header_size. This check is here to protect
+ * embedders against denial-of-service attacks where the attacker feeds
+ * us a never-ending header that the embedder keeps buffering.
+ *
+ * This check is arguably the responsibility of embedders but we're doing
+ * it on the embedder's behalf because most won't bother and this way we
+ * make the web a little safer. max_header_size is still far bigger
+ * than any reasonable request or response so this should never affect
+ * day-to-day operation.
+ */
+#define COUNT_HEADER_SIZE(V) \
+do { \
+ nread += (uint32_t)(V); \
+ if (UNLIKELY(nread > max_header_size)) { \
+ SET_ERRNO(HPE_HEADER_OVERFLOW); \
+ goto error; \
+ } \
+} while (0)
+
+
+#define PROXY_CONNECTION "proxy-connection"
+#define CONNECTION "connection"
+#define CONTENT_LENGTH "content-length"
+#define TRANSFER_ENCODING "transfer-encoding"
+#define UPGRADE "upgrade"
+#define CHUNKED "chunked"
+#define KEEP_ALIVE "keep-alive"
+#define CLOSE "close"
+
+
+static const char *method_strings[] =
+ {
+#define XX(num, name, string) #string,
+ HTTP_METHOD_MAP(XX)
+#undef XX
+ };
+
+
+/* Tokens as defined by rfc 2616. Also lowercases them.
+ * token = 1*<any CHAR except CTLs or separators>
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ */
+static const char tokens[256] = {
+/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */
+ 0, 0, 0, 0, 0, 0, 0, 0,
+/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */
+ 0, 0, 0, 0, 0, 0, 0, 0,
+/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */
+ 0, 0, 0, 0, 0, 0, 0, 0,
+/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */
+ 0, 0, 0, 0, 0, 0, 0, 0,
+/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */
+ ' ', '!', 0, '#', '$', '%', '&', '\'',
+/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */
+ 0, 0, '*', '+', 0, '-', '.', 0,
+/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */
+ '0', '1', '2', '3', '4', '5', '6', '7',
+/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */
+ '8', '9', 0, 0, 0, 0, 0, 0,
+/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */
+ 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */
+ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
+/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */
+ 'x', 'y', 'z', 0, 0, 0, '^', '_',
+/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */
+ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */
+ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
+/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */
+ 'x', 'y', 'z', 0, '|', 0, '~', 0 };
+
+
+static const int8_t unhex[256] =
+ {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1
+ ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+ };
+
+
+#if HTTP_PARSER_STRICT
+# define T(v) 0
+#else
+# define T(v) v
+#endif
+
+
+static const uint8_t normal_url_char[32] = {
+/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */
+ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0,
+/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */
+ 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0,
+/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */
+ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0,
+/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */
+ 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0,
+/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */
+ 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128,
+/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0,
+/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128,
+/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */
+ 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, };
+
+#undef T
+
+enum state
+ { s_dead = 1 /* important that this is > 0 */
+
+ , s_start_req_or_res
+ , s_res_or_resp_H
+ , s_start_res
+ , s_res_H
+ , s_res_HT
+ , s_res_HTT
+ , s_res_HTTP
+ , s_res_http_major
+ , s_res_http_dot
+ , s_res_http_minor
+ , s_res_http_end
+ , s_res_first_status_code
+ , s_res_status_code
+ , s_res_status_start
+ , s_res_status
+ , s_res_line_almost_done
+
+ , s_start_req
+
+ , s_req_method
+ , s_req_spaces_before_url
+ , s_req_schema
+ , s_req_schema_slash
+ , s_req_schema_slash_slash
+ , s_req_server_start
+ , s_req_server
+ , s_req_server_with_at
+ , s_req_path
+ , s_req_query_string_start
+ , s_req_query_string
+ , s_req_fragment_start
+ , s_req_fragment
+ , s_req_http_start
+ , s_req_http_H
+ , s_req_http_HT
+ , s_req_http_HTT
+ , s_req_http_HTTP
+ , s_req_http_I
+ , s_req_http_IC
+ , s_req_http_major
+ , s_req_http_dot
+ , s_req_http_minor
+ , s_req_http_end
+ , s_req_line_almost_done
+
+ , s_header_field_start
+ , s_header_field
+ , s_header_value_discard_ws
+ , s_header_value_discard_ws_almost_done
+ , s_header_value_discard_lws
+ , s_header_value_start
+ , s_header_value
+ , s_header_value_lws
+
+ , s_header_almost_done
+
+ , s_chunk_size_start
+ , s_chunk_size
+ , s_chunk_parameters
+ , s_chunk_size_almost_done
+
+ , s_headers_almost_done
+ , s_headers_done
+
+ /* Important: 's_headers_done' must be the last 'header' state. All
+ * states beyond this must be 'body' states. It is used for overflow
+ * checking. See the PARSING_HEADER() macro.
+ */
+
+ , s_chunk_data
+ , s_chunk_data_almost_done
+ , s_chunk_data_done
+
+ , s_body_identity
+ , s_body_identity_eof
+
+ , s_message_done
+ };
+
+
+#define PARSING_HEADER(state) (state <= s_headers_done)
+
+
+enum header_states
+ { h_general = 0
+ , h_C
+ , h_CO
+ , h_CON
+
+ , h_matching_connection
+ , h_matching_proxy_connection
+ , h_matching_content_length
+ , h_matching_transfer_encoding
+ , h_matching_upgrade
+
+ , h_connection
+ , h_content_length
+ , h_content_length_num
+ , h_content_length_ws
+ , h_transfer_encoding
+ , h_upgrade
+
+ , h_matching_transfer_encoding_token_start
+ , h_matching_transfer_encoding_chunked
+ , h_matching_transfer_encoding_token
+
+ , h_matching_connection_token_start
+ , h_matching_connection_keep_alive
+ , h_matching_connection_close
+ , h_matching_connection_upgrade
+ , h_matching_connection_token
+
+ , h_transfer_encoding_chunked
+ , h_connection_keep_alive
+ , h_connection_close
+ , h_connection_upgrade
+ };
+
+enum http_host_state
+ {
+ s_http_host_dead = 1
+ , s_http_userinfo_start
+ , s_http_userinfo
+ , s_http_host_start
+ , s_http_host_v6_start
+ , s_http_host
+ , s_http_host_v6
+ , s_http_host_v6_end
+ , s_http_host_v6_zone_start
+ , s_http_host_v6_zone
+ , s_http_host_port_start
+ , s_http_host_port
+};
+
+/* Macros for character classes; depends on strict-mode */
+#define CR '\r'
+#define LF '\n'
+#define LOWER(c) (unsigned char)(c | 0x20)
+#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z')
+#define IS_NUM(c) ((c) >= '0' && (c) <= '9')
+#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c))
+#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f'))
+#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \
+ (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \
+ (c) == ')')
+#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \
+ (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \
+ (c) == '$' || (c) == ',')
+
+#define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c])
+
+#if HTTP_PARSER_STRICT
+#define TOKEN(c) STRICT_TOKEN(c)
+#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c))
+#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-')
+#else
+#define TOKEN(c) tokens[(unsigned char)c]
+#define IS_URL_CHAR(c) \
+ (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80))
+#define IS_HOST_CHAR(c) \
+ (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_')
+#endif
+
+/**
+ * Verify that a char is a valid visible (printable) US-ASCII
+ * character or %x80-FF
+ **/
+#define IS_HEADER_CHAR(ch) \
+ (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127))
+
+#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res)
+
+
+#if HTTP_PARSER_STRICT
+# define STRICT_CHECK(cond) \
+do { \
+ if (cond) { \
+ SET_ERRNO(HPE_STRICT); \
+ goto error; \
+ } \
+} while (0)
+# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead)
+#else
+# define STRICT_CHECK(cond)
+# define NEW_MESSAGE() start_state
+#endif
+
+
+/* Map errno values to strings for human-readable output */
+#define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s },
+static struct {
+ const char *name;
+ const char *description;
+} http_strerror_tab[] = {
+ HTTP_ERRNO_MAP(HTTP_STRERROR_GEN)
+};
+#undef HTTP_STRERROR_GEN
+
+int http_message_needs_eof(const http_parser *parser);
+
+/* Our URL parser.
+ *
+ * This is designed to be shared by http_parser_execute() for URL validation,
+ * hence it has a state transition + byte-for-byte interface. In addition, it
+ * is meant to be embedded in http_parser_parse_url(), which does the dirty
+ * work of turning state transitions URL components for its API.
+ *
+ * This function should only be invoked with non-space characters. It is
+ * assumed that the caller cares about (and can detect) the transition between
+ * URL and non-URL states by looking for these.
+ */
+static enum state
+parse_url_char(enum state s, const char ch)
+{
+ if (ch == ' ' || ch == '\r' || ch == '\n') {
+ return s_dead;
+ }
+
+#if HTTP_PARSER_STRICT
+ if (ch == '\t' || ch == '\f') {
+ return s_dead;
+ }
+#endif
+
+ switch (s) {
+ case s_req_spaces_before_url:
+ /* Proxied requests are followed by scheme of an absolute URI (alpha).
+ * All methods except CONNECT are followed by '/' or '*'.
+ */
+
+ if (ch == '/' || ch == '*') {
+ return s_req_path;
+ }
+
+ if (IS_ALPHA(ch)) {
+ return s_req_schema;
+ }
+
+ break;
+
+ case s_req_schema:
+ if (IS_ALPHA(ch)) {
+ return s;
+ }
+
+ if (ch == ':') {
+ return s_req_schema_slash;
+ }
+
+ break;
+
+ case s_req_schema_slash:
+ if (ch == '/') {
+ return s_req_schema_slash_slash;
+ }
+
+ break;
+
+ case s_req_schema_slash_slash:
+ if (ch == '/') {
+ return s_req_server_start;
+ }
+
+ break;
+
+ case s_req_server_with_at:
+ if (ch == '@') {
+ return s_dead;
+ }
+
+ /* fall through */
+ case s_req_server_start:
+ case s_req_server:
+ if (ch == '/') {
+ return s_req_path;
+ }
+
+ if (ch == '?') {
+ return s_req_query_string_start;
+ }
+
+ if (ch == '@') {
+ return s_req_server_with_at;
+ }
+
+ if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') {
+ return s_req_server;
+ }
+
+ break;
+
+ case s_req_path:
+ if (IS_URL_CHAR(ch)) {
+ return s;
+ }
+
+ switch (ch) {
+ case '?':
+ return s_req_query_string_start;
+
+ case '#':
+ return s_req_fragment_start;
+ }
+
+ break;
+
+ case s_req_query_string_start:
+ case s_req_query_string:
+ if (IS_URL_CHAR(ch)) {
+ return s_req_query_string;
+ }
+
+ switch (ch) {
+ case '?':
+ /* allow extra '?' in query string */
+ return s_req_query_string;
+
+ case '#':
+ return s_req_fragment_start;
+ }
+
+ break;
+
+ case s_req_fragment_start:
+ if (IS_URL_CHAR(ch)) {
+ return s_req_fragment;
+ }
+
+ switch (ch) {
+ case '?':
+ return s_req_fragment;
+
+ case '#':
+ return s;
+ }
+
+ break;
+
+ case s_req_fragment:
+ if (IS_URL_CHAR(ch)) {
+ return s;
+ }
+
+ switch (ch) {
+ case '?':
+ case '#':
+ return s;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ /* We should never fall out of the switch above unless there's an error */
+ return s_dead;
+}
+
+size_t http_parser_execute (http_parser *parser,
+ const http_parser_settings *settings,
+ const char *data,
+ size_t len)
+{
+ char c, ch;
+ int8_t unhex_val;
+ const char *p = data;
+ const char *header_field_mark = 0;
+ const char *header_value_mark = 0;
+ const char *url_mark = 0;
+ const char *body_mark = 0;
+ const char *status_mark = 0;
+ enum state p_state = (enum state) parser->state;
+ const unsigned int lenient = parser->lenient_http_headers;
+ uint32_t nread = parser->nread;
+
+ /* We're in an error state. Don't bother doing anything. */
+ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) {
+ return 0;
+ }
+
+ if (len == 0) {
+ switch (CURRENT_STATE()) {
+ case s_body_identity_eof:
+ /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if
+ * we got paused.
+ */
+ CALLBACK_NOTIFY_NOADVANCE(message_complete);
+ return 0;
+
+ case s_dead:
+ case s_start_req_or_res:
+ case s_start_res:
+ case s_start_req:
+ return 0;
+
+ default:
+ SET_ERRNO(HPE_INVALID_EOF_STATE);
+ return 1;
+ }
+ }
+
+
+ if (CURRENT_STATE() == s_header_field)
+ header_field_mark = data;
+ if (CURRENT_STATE() == s_header_value)
+ header_value_mark = data;
+ switch (CURRENT_STATE()) {
+ case s_req_path:
+ case s_req_schema:
+ case s_req_schema_slash:
+ case s_req_schema_slash_slash:
+ case s_req_server_start:
+ case s_req_server:
+ case s_req_server_with_at:
+ case s_req_query_string_start:
+ case s_req_query_string:
+ case s_req_fragment_start:
+ case s_req_fragment:
+ url_mark = data;
+ break;
+ case s_res_status:
+ status_mark = data;
+ break;
+ default:
+ break;
+ }
+
+ for (p=data; p != data + len; p++) {
+ ch = *p;
+
+ if (PARSING_HEADER(CURRENT_STATE()))
+ COUNT_HEADER_SIZE(1);
+
+reexecute:
+ switch (CURRENT_STATE()) {
+
+ case s_dead:
+ /* this state is used after a 'Connection: close' message
+ * the parser will error out if it reads another message
+ */
+ if (LIKELY(ch == CR || ch == LF))
+ break;
+
+ SET_ERRNO(HPE_CLOSED_CONNECTION);
+ goto error;
+
+ case s_start_req_or_res:
+ {
+ if (ch == CR || ch == LF)
+ break;
+ parser->flags = 0;
+ parser->extra_flags = 0;
+ parser->content_length = ULLONG_MAX;
+
+ if (ch == 'H') {
+ UPDATE_STATE(s_res_or_resp_H);
+
+ CALLBACK_NOTIFY(message_begin);
+ } else {
+ parser->type = HTTP_REQUEST;
+ UPDATE_STATE(s_start_req);
+ REEXECUTE();
+ }
+
+ break;
+ }
+
+ case s_res_or_resp_H:
+ if (ch == 'T') {
+ parser->type = HTTP_RESPONSE;
+ UPDATE_STATE(s_res_HT);
+ } else {
+ if (UNLIKELY(ch != 'E')) {
+ SET_ERRNO(HPE_INVALID_CONSTANT);
+ goto error;
+ }
+
+ parser->type = HTTP_REQUEST;
+ parser->method = HTTP_HEAD;
+ parser->index = 2;
+ UPDATE_STATE(s_req_method);
+ }
+ break;
+
+ case s_start_res:
+ {
+ if (ch == CR || ch == LF)
+ break;
+ parser->flags = 0;
+ parser->extra_flags = 0;
+ parser->content_length = ULLONG_MAX;
+
+ if (ch == 'H') {
+ UPDATE_STATE(s_res_H);
+ } else {
+ SET_ERRNO(HPE_INVALID_CONSTANT);
+ goto error;
+ }
+
+ CALLBACK_NOTIFY(message_begin);
+ break;
+ }
+
+ case s_res_H:
+ STRICT_CHECK(ch != 'T');
+ UPDATE_STATE(s_res_HT);
+ break;
+
+ case s_res_HT:
+ STRICT_CHECK(ch != 'T');
+ UPDATE_STATE(s_res_HTT);
+ break;
+
+ case s_res_HTT:
+ STRICT_CHECK(ch != 'P');
+ UPDATE_STATE(s_res_HTTP);
+ break;
+
+ case s_res_HTTP:
+ STRICT_CHECK(ch != '/');
+ UPDATE_STATE(s_res_http_major);
+ break;
+
+ case s_res_http_major:
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ parser->http_major = ch - '0';
+ UPDATE_STATE(s_res_http_dot);
+ break;
+
+ case s_res_http_dot:
+ {
+ if (UNLIKELY(ch != '.')) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ UPDATE_STATE(s_res_http_minor);
+ break;
+ }
+
+ case s_res_http_minor:
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ parser->http_minor = ch - '0';
+ UPDATE_STATE(s_res_http_end);
+ break;
+
+ case s_res_http_end:
+ {
+ if (UNLIKELY(ch != ' ')) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ UPDATE_STATE(s_res_first_status_code);
+ break;
+ }
+
+ case s_res_first_status_code:
+ {
+ if (!IS_NUM(ch)) {
+ if (ch == ' ') {
+ break;
+ }
+
+ SET_ERRNO(HPE_INVALID_STATUS);
+ goto error;
+ }
+ parser->status_code = ch - '0';
+ UPDATE_STATE(s_res_status_code);
+ break;
+ }
+
+ case s_res_status_code:
+ {
+ if (!IS_NUM(ch)) {
+ switch (ch) {
+ case ' ':
+ UPDATE_STATE(s_res_status_start);
+ break;
+ case CR:
+ case LF:
+ UPDATE_STATE(s_res_status_start);
+ REEXECUTE();
+ break;
+ default:
+ SET_ERRNO(HPE_INVALID_STATUS);
+ goto error;
+ }
+ break;
+ }
+
+ parser->status_code *= 10;
+ parser->status_code += ch - '0';
+
+ if (UNLIKELY(parser->status_code > 999)) {
+ SET_ERRNO(HPE_INVALID_STATUS);
+ goto error;
+ }
+
+ break;
+ }
+
+ case s_res_status_start:
+ {
+ MARK(status);
+ UPDATE_STATE(s_res_status);
+ parser->index = 0;
+
+ if (ch == CR || ch == LF)
+ REEXECUTE();
+
+ break;
+ }
+
+ case s_res_status:
+ if (ch == CR) {
+ UPDATE_STATE(s_res_line_almost_done);
+ CALLBACK_DATA(status);
+ break;
+ }
+
+ if (ch == LF) {
+ UPDATE_STATE(s_header_field_start);
+ CALLBACK_DATA(status);
+ break;
+ }
+
+ break;
+
+ case s_res_line_almost_done:
+ STRICT_CHECK(ch != LF);
+ UPDATE_STATE(s_header_field_start);
+ break;
+
+ case s_start_req:
+ {
+ if (ch == CR || ch == LF)
+ break;
+ parser->flags = 0;
+ parser->extra_flags = 0;
+ parser->content_length = ULLONG_MAX;
+
+ if (UNLIKELY(!IS_ALPHA(ch))) {
+ SET_ERRNO(HPE_INVALID_METHOD);
+ goto error;
+ }
+
+ parser->method = (enum http_method) 0;
+ parser->index = 1;
+ switch (ch) {
+ case 'A': parser->method = HTTP_ACL; break;
+ case 'B': parser->method = HTTP_BIND; break;
+ case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break;
+ case 'D': parser->method = HTTP_DELETE; break;
+ case 'G': parser->method = HTTP_GET; break;
+ case 'H': parser->method = HTTP_HEAD; break;
+ case 'L': parser->method = HTTP_LOCK; /* or LINK */ break;
+ case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break;
+ case 'N': parser->method = HTTP_NOTIFY; break;
+ case 'O': parser->method = HTTP_OPTIONS; break;
+ case 'P': parser->method = HTTP_POST;
+ /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */
+ break;
+ case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break;
+ case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break;
+ case 'T': parser->method = HTTP_TRACE; break;
+ case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break;
+ default:
+ SET_ERRNO(HPE_INVALID_METHOD);
+ goto error;
+ }
+ UPDATE_STATE(s_req_method);
+
+ CALLBACK_NOTIFY(message_begin);
+
+ break;
+ }
+
+ case s_req_method:
+ {
+ const char *matcher;
+ if (UNLIKELY(ch == '\0')) {
+ SET_ERRNO(HPE_INVALID_METHOD);
+ goto error;
+ }
+
+ matcher = method_strings[parser->method];
+ if (ch == ' ' && matcher[parser->index] == '\0') {
+ UPDATE_STATE(s_req_spaces_before_url);
+ } else if (ch == matcher[parser->index]) {
+ ; /* nada */
+ } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') {
+
+ switch (parser->method << 16 | parser->index << 8 | ch) {
+#define XX(meth, pos, ch, new_meth) \
+ case (HTTP_##meth << 16 | pos << 8 | ch): \
+ parser->method = HTTP_##new_meth; break;
+
+ XX(POST, 1, 'U', PUT)
+ XX(POST, 1, 'A', PATCH)
+ XX(POST, 1, 'R', PROPFIND)
+ XX(PUT, 2, 'R', PURGE)
+ XX(CONNECT, 1, 'H', CHECKOUT)
+ XX(CONNECT, 2, 'P', COPY)
+ XX(MKCOL, 1, 'O', MOVE)
+ XX(MKCOL, 1, 'E', MERGE)
+ XX(MKCOL, 1, '-', MSEARCH)
+ XX(MKCOL, 2, 'A', MKACTIVITY)
+ XX(MKCOL, 3, 'A', MKCALENDAR)
+ XX(SUBSCRIBE, 1, 'E', SEARCH)
+ XX(SUBSCRIBE, 1, 'O', SOURCE)
+ XX(REPORT, 2, 'B', REBIND)
+ XX(PROPFIND, 4, 'P', PROPPATCH)
+ XX(LOCK, 1, 'I', LINK)
+ XX(UNLOCK, 2, 'S', UNSUBSCRIBE)
+ XX(UNLOCK, 2, 'B', UNBIND)
+ XX(UNLOCK, 3, 'I', UNLINK)
+#undef XX
+ default:
+ SET_ERRNO(HPE_INVALID_METHOD);
+ goto error;
+ }
+ } else {
+ SET_ERRNO(HPE_INVALID_METHOD);
+ goto error;
+ }
+
+ ++parser->index;
+ break;
+ }
+
+ case s_req_spaces_before_url:
+ {
+ if (ch == ' ') break;
+
+ MARK(url);
+ if (parser->method == HTTP_CONNECT) {
+ UPDATE_STATE(s_req_server_start);
+ }
+
+ UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch));
+ if (UNLIKELY(CURRENT_STATE() == s_dead)) {
+ SET_ERRNO(HPE_INVALID_URL);
+ goto error;
+ }
+
+ break;
+ }
+
+ case s_req_schema:
+ case s_req_schema_slash:
+ case s_req_schema_slash_slash:
+ case s_req_server_start:
+ {
+ switch (ch) {
+ /* No whitespace allowed here */
+ case ' ':
+ case CR:
+ case LF:
+ SET_ERRNO(HPE_INVALID_URL);
+ goto error;
+ default:
+ UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch));
+ if (UNLIKELY(CURRENT_STATE() == s_dead)) {
+ SET_ERRNO(HPE_INVALID_URL);
+ goto error;
+ }
+ }
+
+ break;
+ }
+
+ case s_req_server:
+ case s_req_server_with_at:
+ case s_req_path:
+ case s_req_query_string_start:
+ case s_req_query_string:
+ case s_req_fragment_start:
+ case s_req_fragment:
+ {
+ switch (ch) {
+ case ' ':
+ UPDATE_STATE(s_req_http_start);
+ CALLBACK_DATA(url);
+ break;
+ case CR:
+ case LF:
+ parser->http_major = 0;
+ parser->http_minor = 9;
+ UPDATE_STATE((ch == CR) ?
+ s_req_line_almost_done :
+ s_header_field_start);
+ CALLBACK_DATA(url);
+ break;
+ default:
+ UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch));
+ if (UNLIKELY(CURRENT_STATE() == s_dead)) {
+ SET_ERRNO(HPE_INVALID_URL);
+ goto error;
+ }
+ }
+ break;
+ }
+
+ case s_req_http_start:
+ switch (ch) {
+ case ' ':
+ break;
+ case 'H':
+ UPDATE_STATE(s_req_http_H);
+ break;
+ case 'I':
+ if (parser->method == HTTP_SOURCE) {
+ UPDATE_STATE(s_req_http_I);
+ break;
+ }
+ /* fall through */
+ default:
+ SET_ERRNO(HPE_INVALID_CONSTANT);
+ goto error;
+ }
+ break;
+
+ case s_req_http_H:
+ STRICT_CHECK(ch != 'T');
+ UPDATE_STATE(s_req_http_HT);
+ break;
+
+ case s_req_http_HT:
+ STRICT_CHECK(ch != 'T');
+ UPDATE_STATE(s_req_http_HTT);
+ break;
+
+ case s_req_http_HTT:
+ STRICT_CHECK(ch != 'P');
+ UPDATE_STATE(s_req_http_HTTP);
+ break;
+
+ case s_req_http_I:
+ STRICT_CHECK(ch != 'C');
+ UPDATE_STATE(s_req_http_IC);
+ break;
+
+ case s_req_http_IC:
+ STRICT_CHECK(ch != 'E');
+ UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */
+ break;
+
+ case s_req_http_HTTP:
+ STRICT_CHECK(ch != '/');
+ UPDATE_STATE(s_req_http_major);
+ break;
+
+ case s_req_http_major:
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ parser->http_major = ch - '0';
+ UPDATE_STATE(s_req_http_dot);
+ break;
+
+ case s_req_http_dot:
+ {
+ if (UNLIKELY(ch != '.')) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ UPDATE_STATE(s_req_http_minor);
+ break;
+ }
+
+ case s_req_http_minor:
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ }
+
+ parser->http_minor = ch - '0';
+ UPDATE_STATE(s_req_http_end);
+ break;
+
+ case s_req_http_end:
+ {
+ if (ch == CR) {
+ UPDATE_STATE(s_req_line_almost_done);
+ break;
+ }
+
+ if (ch == LF) {
+ UPDATE_STATE(s_header_field_start);
+ break;
+ }
+
+ SET_ERRNO(HPE_INVALID_VERSION);
+ goto error;
+ break;
+ }
+
+ /* end of request line */
+ case s_req_line_almost_done:
+ {
+ if (UNLIKELY(ch != LF)) {
+ SET_ERRNO(HPE_LF_EXPECTED);
+ goto error;
+ }
+
+ UPDATE_STATE(s_header_field_start);
+ break;
+ }
+
+ case s_header_field_start:
+ {
+ if (ch == CR) {
+ UPDATE_STATE(s_headers_almost_done);
+ break;
+ }
+
+ if (ch == LF) {
+ /* they might be just sending \n instead of \r\n so this would be
+ * the second \n to denote the end of headers*/
+ UPDATE_STATE(s_headers_almost_done);
+ REEXECUTE();
+ }
+
+ c = TOKEN(ch);
+
+ if (UNLIKELY(!c)) {
+ SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+ goto error;
+ }
+
+ MARK(header_field);
+
+ parser->index = 0;
+ UPDATE_STATE(s_header_field);
+
+ switch (c) {
+ case 'c':
+ parser->header_state = h_C;
+ break;
+
+ case 'p':
+ parser->header_state = h_matching_proxy_connection;
+ break;
+
+ case 't':
+ parser->header_state = h_matching_transfer_encoding;
+ break;
+
+ case 'u':
+ parser->header_state = h_matching_upgrade;
+ break;
+
+ default:
+ parser->header_state = h_general;
+ break;
+ }
+ break;
+ }
+
+ case s_header_field:
+ {
+ const char* start = p;
+ for (; p != data + len; p++) {
+ ch = *p;
+ c = TOKEN(ch);
+
+ if (!c)
+ break;
+
+ switch (parser->header_state) {
+ case h_general: {
+ size_t left = data + len - p;
+ const char* pe = p + MIN(left, max_header_size);
+ while (p+1 < pe && TOKEN(p[1])) {
+ p++;
+ }
+ break;
+ }
+
+ case h_C:
+ parser->index++;
+ parser->header_state = (c == 'o' ? h_CO : h_general);
+ break;
+
+ case h_CO:
+ parser->index++;
+ parser->header_state = (c == 'n' ? h_CON : h_general);
+ break;
+
+ case h_CON:
+ parser->index++;
+ switch (c) {
+ case 'n':
+ parser->header_state = h_matching_connection;
+ break;
+ case 't':
+ parser->header_state = h_matching_content_length;
+ break;
+ default:
+ parser->header_state = h_general;
+ break;
+ }
+ break;
+
+ /* connection */
+
+ case h_matching_connection:
+ parser->index++;
+ if (parser->index > sizeof(CONNECTION)-1
+ || c != CONNECTION[parser->index]) {
+ parser->header_state = h_general;
+ } else if (parser->index == sizeof(CONNECTION)-2) {
+ parser->header_state = h_connection;
+ }
+ break;
+
+ /* proxy-connection */
+
+ case h_matching_proxy_connection:
+ parser->index++;
+ if (parser->index > sizeof(PROXY_CONNECTION)-1
+ || c != PROXY_CONNECTION[parser->index]) {
+ parser->header_state = h_general;
+ } else if (parser->index == sizeof(PROXY_CONNECTION)-2) {
+ parser->header_state = h_connection;
+ }
+ break;
+
+ /* content-length */
+
+ case h_matching_content_length:
+ parser->index++;
+ if (parser->index > sizeof(CONTENT_LENGTH)-1
+ || c != CONTENT_LENGTH[parser->index]) {
+ parser->header_state = h_general;
+ } else if (parser->index == sizeof(CONTENT_LENGTH)-2) {
+ parser->header_state = h_content_length;
+ }
+ break;
+
+ /* transfer-encoding */
+
+ case h_matching_transfer_encoding:
+ parser->index++;
+ if (parser->index > sizeof(TRANSFER_ENCODING)-1
+ || c != TRANSFER_ENCODING[parser->index]) {
+ parser->header_state = h_general;
+ } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) {
+ parser->header_state = h_transfer_encoding;
+ parser->extra_flags |= F_TRANSFER_ENCODING >> 8;
+ }
+ break;
+
+ /* upgrade */
+
+ case h_matching_upgrade:
+ parser->index++;
+ if (parser->index > sizeof(UPGRADE)-1
+ || c != UPGRADE[parser->index]) {
+ parser->header_state = h_general;
+ } else if (parser->index == sizeof(UPGRADE)-2) {
+ parser->header_state = h_upgrade;
+ }
+ break;
+
+ case h_connection:
+ case h_content_length:
+ case h_transfer_encoding:
+ case h_upgrade:
+ if (ch != ' ') parser->header_state = h_general;
+ break;
+
+ default:
+ assert(0 && "Unknown header_state");
+ break;
+ }
+ }
+
+ if (p == data + len) {
+ --p;
+ COUNT_HEADER_SIZE(p - start);
+ break;
+ }
+
+ COUNT_HEADER_SIZE(p - start);
+
+ if (ch == ':') {
+ UPDATE_STATE(s_header_value_discard_ws);
+ CALLBACK_DATA(header_field);
+ break;
+ }
+
+ SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+ goto error;
+ }
+
+ case s_header_value_discard_ws:
+ if (ch == ' ' || ch == '\t') break;
+
+ if (ch == CR) {
+ UPDATE_STATE(s_header_value_discard_ws_almost_done);
+ break;
+ }
+
+ if (ch == LF) {
+ UPDATE_STATE(s_header_value_discard_lws);
+ break;
+ }
+
+ /* fall through */
+
+ case s_header_value_start:
+ {
+ MARK(header_value);
+
+ UPDATE_STATE(s_header_value);
+ parser->index = 0;
+
+ c = LOWER(ch);
+
+ switch (parser->header_state) {
+ case h_upgrade:
+ parser->flags |= F_UPGRADE;
+ parser->header_state = h_general;
+ break;
+
+ case h_transfer_encoding:
+ /* looking for 'Transfer-Encoding: chunked' */
+ if ('c' == c) {
+ parser->header_state = h_matching_transfer_encoding_chunked;
+ } else {
+ parser->header_state = h_matching_transfer_encoding_token;
+ }
+ break;
+
+ /* Multi-value `Transfer-Encoding` header */
+ case h_matching_transfer_encoding_token_start:
+ break;
+
+ case h_content_length:
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ goto error;
+ }
+
+ if (parser->flags & F_CONTENTLENGTH) {
+ SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
+ goto error;
+ }
+
+ parser->flags |= F_CONTENTLENGTH;
+ parser->content_length = ch - '0';
+ parser->header_state = h_content_length_num;
+ break;
+
+ /* when obsolete line folding is encountered for content length
+ * continue to the s_header_value state */
+ case h_content_length_ws:
+ break;
+
+ case h_connection:
+ /* looking for 'Connection: keep-alive' */
+ if (c == 'k') {
+ parser->header_state = h_matching_connection_keep_alive;
+ /* looking for 'Connection: close' */
+ } else if (c == 'c') {
+ parser->header_state = h_matching_connection_close;
+ } else if (c == 'u') {
+ parser->header_state = h_matching_connection_upgrade;
+ } else {
+ parser->header_state = h_matching_connection_token;
+ }
+ break;
+
+ /* Multi-value `Connection` header */
+ case h_matching_connection_token_start:
+ break;
+
+ default:
+ parser->header_state = h_general;
+ break;
+ }
+ break;
+ }
+
+ case s_header_value:
+ {
+ const char* start = p;
+ enum header_states h_state = (enum header_states) parser->header_state;
+ for (; p != data + len; p++) {
+ ch = *p;
+ if (ch == CR) {
+ UPDATE_STATE(s_header_almost_done);
+ parser->header_state = h_state;
+ CALLBACK_DATA(header_value);
+ break;
+ }
+
+ if (ch == LF) {
+ UPDATE_STATE(s_header_almost_done);
+ COUNT_HEADER_SIZE(p - start);
+ parser->header_state = h_state;
+ CALLBACK_DATA_NOADVANCE(header_value);
+ REEXECUTE();
+ }
+
+ if (!lenient && !IS_HEADER_CHAR(ch)) {
+ SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+ goto error;
+ }
+
+ c = LOWER(ch);
+
+ switch (h_state) {
+ case h_general:
+ {
+ size_t left = data + len - p;
+ const char* pe = p + MIN(left, max_header_size);
+
+ for (; p != pe; p++) {
+ ch = *p;
+ if (ch == CR || ch == LF) {
+ --p;
+ break;
+ }
+ if (!lenient && !IS_HEADER_CHAR(ch)) {
+ SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
+ goto error;
+ }
+ }
+ if (p == data + len)
+ --p;
+ break;
+ }
+
+ case h_connection:
+ case h_transfer_encoding:
+ assert(0 && "Shouldn't get here.");
+ break;
+
+ case h_content_length:
+ if (ch == ' ') break;
+ h_state = h_content_length_num;
+ /* fall through */
+
+ case h_content_length_num:
+ {
+ uint64_t t;
+
+ if (ch == ' ') {
+ h_state = h_content_length_ws;
+ break;
+ }
+
+ if (UNLIKELY(!IS_NUM(ch))) {
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ parser->header_state = h_state;
+ goto error;
+ }
+
+ t = parser->content_length;
+ t *= 10;
+ t += ch - '0';
+
+ /* Overflow? Test against a conservative limit for simplicity. */
+ if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) {
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ parser->header_state = h_state;
+ goto error;
+ }
+
+ parser->content_length = t;
+ break;
+ }
+
+ case h_content_length_ws:
+ if (ch == ' ') break;
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ parser->header_state = h_state;
+ goto error;
+
+ /* Transfer-Encoding: chunked */
+ case h_matching_transfer_encoding_token_start:
+ /* looking for 'Transfer-Encoding: chunked' */
+ if ('c' == c) {
+ h_state = h_matching_transfer_encoding_chunked;
+ } else if (STRICT_TOKEN(c)) {
+ /* TODO(indutny): similar code below does this, but why?
+ * At the very least it seems to be inconsistent given that
+ * h_matching_transfer_encoding_token does not check for
+ * `STRICT_TOKEN`
+ */
+ h_state = h_matching_transfer_encoding_token;
+ } else if (c == ' ' || c == '\t') {
+ /* Skip lws */
+ } else {
+ h_state = h_general;
+ }
+ break;
+
+ case h_matching_transfer_encoding_chunked:
+ parser->index++;
+ if (parser->index > sizeof(CHUNKED)-1
+ || c != CHUNKED[parser->index]) {
+ h_state = h_matching_transfer_encoding_token;
+ } else if (parser->index == sizeof(CHUNKED)-2) {
+ h_state = h_transfer_encoding_chunked;
+ }
+ break;
+
+ case h_matching_transfer_encoding_token:
+ if (ch == ',') {
+ h_state = h_matching_transfer_encoding_token_start;
+ parser->index = 0;
+ }
+ break;
+
+ case h_matching_connection_token_start:
+ /* looking for 'Connection: keep-alive' */
+ if (c == 'k') {
+ h_state = h_matching_connection_keep_alive;
+ /* looking for 'Connection: close' */
+ } else if (c == 'c') {
+ h_state = h_matching_connection_close;
+ } else if (c == 'u') {
+ h_state = h_matching_connection_upgrade;
+ } else if (STRICT_TOKEN(c)) {
+ h_state = h_matching_connection_token;
+ } else if (c == ' ' || c == '\t') {
+ /* Skip lws */
+ } else {
+ h_state = h_general;
+ }
+ break;
+
+ /* looking for 'Connection: keep-alive' */
+ case h_matching_connection_keep_alive:
+ parser->index++;
+ if (parser->index > sizeof(KEEP_ALIVE)-1
+ || c != KEEP_ALIVE[parser->index]) {
+ h_state = h_matching_connection_token;
+ } else if (parser->index == sizeof(KEEP_ALIVE)-2) {
+ h_state = h_connection_keep_alive;
+ }
+ break;
+
+ /* looking for 'Connection: close' */
+ case h_matching_connection_close:
+ parser->index++;
+ if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) {
+ h_state = h_matching_connection_token;
+ } else if (parser->index == sizeof(CLOSE)-2) {
+ h_state = h_connection_close;
+ }
+ break;
+
+ /* looking for 'Connection: upgrade' */
+ case h_matching_connection_upgrade:
+ parser->index++;
+ if (parser->index > sizeof(UPGRADE) - 1 ||
+ c != UPGRADE[parser->index]) {
+ h_state = h_matching_connection_token;
+ } else if (parser->index == sizeof(UPGRADE)-2) {
+ h_state = h_connection_upgrade;
+ }
+ break;
+
+ case h_matching_connection_token:
+ if (ch == ',') {
+ h_state = h_matching_connection_token_start;
+ parser->index = 0;
+ }
+ break;
+
+ case h_transfer_encoding_chunked:
+ if (ch != ' ') h_state = h_matching_transfer_encoding_token;
+ break;
+
+ case h_connection_keep_alive:
+ case h_connection_close:
+ case h_connection_upgrade:
+ if (ch == ',') {
+ if (h_state == h_connection_keep_alive) {
+ parser->flags |= F_CONNECTION_KEEP_ALIVE;
+ } else if (h_state == h_connection_close) {
+ parser->flags |= F_CONNECTION_CLOSE;
+ } else if (h_state == h_connection_upgrade) {
+ parser->flags |= F_CONNECTION_UPGRADE;
+ }
+ h_state = h_matching_connection_token_start;
+ parser->index = 0;
+ } else if (ch != ' ') {
+ h_state = h_matching_connection_token;
+ }
+ break;
+
+ default:
+ UPDATE_STATE(s_header_value);
+ h_state = h_general;
+ break;
+ }
+ }
+ parser->header_state = h_state;
+
+ if (p == data + len)
+ --p;
+
+ COUNT_HEADER_SIZE(p - start);
+ break;
+ }
+
+ case s_header_almost_done:
+ {
+ if (UNLIKELY(ch != LF)) {
+ SET_ERRNO(HPE_LF_EXPECTED);
+ goto error;
+ }
+
+ UPDATE_STATE(s_header_value_lws);
+ break;
+ }
+
+ case s_header_value_lws:
+ {
+ if (ch == ' ' || ch == '\t') {
+ if (parser->header_state == h_content_length_num) {
+ /* treat obsolete line folding as space */
+ parser->header_state = h_content_length_ws;
+ }
+ UPDATE_STATE(s_header_value_start);
+ REEXECUTE();
+ }
+
+ /* finished the header */
+ switch (parser->header_state) {
+ case h_connection_keep_alive:
+ parser->flags |= F_CONNECTION_KEEP_ALIVE;
+ break;
+ case h_connection_close:
+ parser->flags |= F_CONNECTION_CLOSE;
+ break;
+ case h_transfer_encoding_chunked:
+ parser->flags |= F_CHUNKED;
+ break;
+ case h_connection_upgrade:
+ parser->flags |= F_CONNECTION_UPGRADE;
+ break;
+ default:
+ break;
+ }
+
+ UPDATE_STATE(s_header_field_start);
+ REEXECUTE();
+ }
+
+ case s_header_value_discard_ws_almost_done:
+ {
+ STRICT_CHECK(ch != LF);
+ UPDATE_STATE(s_header_value_discard_lws);
+ break;
+ }
+
+ case s_header_value_discard_lws:
+ {
+ if (ch == ' ' || ch == '\t') {
+ UPDATE_STATE(s_header_value_discard_ws);
+ break;
+ } else {
+ switch (parser->header_state) {
+ case h_connection_keep_alive:
+ parser->flags |= F_CONNECTION_KEEP_ALIVE;
+ break;
+ case h_connection_close:
+ parser->flags |= F_CONNECTION_CLOSE;
+ break;
+ case h_connection_upgrade:
+ parser->flags |= F_CONNECTION_UPGRADE;
+ break;
+ case h_transfer_encoding_chunked:
+ parser->flags |= F_CHUNKED;
+ break;
+ case h_content_length:
+ /* do not allow empty content length */
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ goto error;
+ break;
+ default:
+ break;
+ }
+
+ /* header value was empty */
+ MARK(header_value);
+ UPDATE_STATE(s_header_field_start);
+ CALLBACK_DATA_NOADVANCE(header_value);
+ REEXECUTE();
+ }
+ }
+
+ case s_headers_almost_done:
+ {
+ STRICT_CHECK(ch != LF);
+
+ if (parser->flags & F_TRAILING) {
+ /* End of a chunked request */
+ UPDATE_STATE(s_message_done);
+ CALLBACK_NOTIFY_NOADVANCE(chunk_complete);
+ REEXECUTE();
+ }
+
+ /* Cannot us transfer-encoding and a content-length header together
+ per the HTTP specification. (RFC 7230 Section 3.3.3) */
+ if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) &&
+ (parser->flags & F_CONTENTLENGTH)) {
+ /* Allow it for lenient parsing as long as `Transfer-Encoding` is
+ * not `chunked`
+ */
+ if (!lenient || (parser->flags & F_CHUNKED)) {
+ SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
+ goto error;
+ }
+ }
+
+ UPDATE_STATE(s_headers_done);
+
+ /* Set this here so that on_headers_complete() callbacks can see it */
+ if ((parser->flags & F_UPGRADE) &&
+ (parser->flags & F_CONNECTION_UPGRADE)) {
+ /* For responses, "Upgrade: foo" and "Connection: upgrade" are
+ * mandatory only when it is a 101 Switching Protocols response,
+ * otherwise it is purely informational, to announce support.
+ */
+ parser->upgrade =
+ (parser->type == HTTP_REQUEST || parser->status_code == 101);
+ } else {
+ parser->upgrade = (parser->method == HTTP_CONNECT);
+ }
+
+ /* Here we call the headers_complete callback. This is somewhat
+ * different than other callbacks because if the user returns 1, we
+ * will interpret that as saying that this message has no body. This
+ * is needed for the annoying case of recieving a response to a HEAD
+ * request.
+ *
+ * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so
+ * we have to simulate it by handling a change in errno below.
+ */
+ if (settings->on_headers_complete) {
+ switch (settings->on_headers_complete(parser)) {
+ case 0:
+ break;
+
+ case 2:
+ parser->upgrade = 1;
+
+ /* fall through */
+ case 1:
+ parser->flags |= F_SKIPBODY;
+ break;
+
+ default:
+ SET_ERRNO(HPE_CB_headers_complete);
+ RETURN(p - data); /* Error */
+ }
+ }
+
+ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) {
+ RETURN(p - data);
+ }
+
+ REEXECUTE();
+ }
+
+ case s_headers_done:
+ {
+ int hasBody;
+ STRICT_CHECK(ch != LF);
+
+ parser->nread = 0;
+ nread = 0;
+
+ hasBody = parser->flags & F_CHUNKED ||
+ (parser->content_length > 0 && parser->content_length != ULLONG_MAX);
+ if (parser->upgrade && (parser->method == HTTP_CONNECT ||
+ (parser->flags & F_SKIPBODY) || !hasBody)) {
+ /* Exit, the rest of the message is in a different protocol. */
+ UPDATE_STATE(NEW_MESSAGE());
+ CALLBACK_NOTIFY(message_complete);
+ RETURN((p - data) + 1);
+ }
+
+ if (parser->flags & F_SKIPBODY) {
+ UPDATE_STATE(NEW_MESSAGE());
+ CALLBACK_NOTIFY(message_complete);
+ } else if (parser->flags & F_CHUNKED) {
+ /* chunked encoding - ignore Content-Length header,
+ * prepare for a chunk */
+ UPDATE_STATE(s_chunk_size_start);
+ } else if (parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) {
+ if (parser->type == HTTP_REQUEST && !lenient) {
+ /* RFC 7230 3.3.3 */
+
+ /* If a Transfer-Encoding header field
+ * is present in a request and the chunked transfer coding is not
+ * the final encoding, the message body length cannot be determined
+ * reliably; the server MUST respond with the 400 (Bad Request)
+ * status code and then close the connection.
+ */
+ SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING);
+ RETURN(p - data); /* Error */
+ } else {
+ /* RFC 7230 3.3.3 */
+
+ /* If a Transfer-Encoding header field is present in a response and
+ * the chunked transfer coding is not the final encoding, the
+ * message body length is determined by reading the connection until
+ * it is closed by the server.
+ */
+ UPDATE_STATE(s_body_identity_eof);
+ }
+ } else {
+ if (parser->content_length == 0) {
+ /* Content-Length header given but zero: Content-Length: 0\r\n */
+ UPDATE_STATE(NEW_MESSAGE());
+ CALLBACK_NOTIFY(message_complete);
+ } else if (parser->content_length != ULLONG_MAX) {
+ /* Content-Length header given and non-zero */
+ UPDATE_STATE(s_body_identity);
+ } else {
+ if (!http_message_needs_eof(parser)) {
+ /* Assume content-length 0 - read the next */
+ UPDATE_STATE(NEW_MESSAGE());
+ CALLBACK_NOTIFY(message_complete);
+ } else {
+ /* Read body until EOF */
+ UPDATE_STATE(s_body_identity_eof);
+ }
+ }
+ }
+
+ break;
+ }
+
+ case s_body_identity:
+ {
+ uint64_t to_read = MIN(parser->content_length,
+ (uint64_t) ((data + len) - p));
+
+ assert(parser->content_length != 0
+ && parser->content_length != ULLONG_MAX);
+
+ /* The difference between advancing content_length and p is because
+ * the latter will automaticaly advance on the next loop iteration.
+ * Further, if content_length ends up at 0, we want to see the last
+ * byte again for our message complete callback.
+ */
+ MARK(body);
+ parser->content_length -= to_read;
+ p += to_read - 1;
+
+ if (parser->content_length == 0) {
+ UPDATE_STATE(s_message_done);
+
+ /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte.
+ *
+ * The alternative to doing this is to wait for the next byte to
+ * trigger the data callback, just as in every other case. The
+ * problem with this is that this makes it difficult for the test
+ * harness to distinguish between complete-on-EOF and
+ * complete-on-length. It's not clear that this distinction is
+ * important for applications, but let's keep it for now.
+ */
+ CALLBACK_DATA_(body, p - body_mark + 1, p - data);
+ REEXECUTE();
+ }
+
+ break;
+ }
+
+ /* read until EOF */
+ case s_body_identity_eof:
+ MARK(body);
+ p = data + len - 1;
+
+ break;
+
+ case s_message_done:
+ UPDATE_STATE(NEW_MESSAGE());
+ CALLBACK_NOTIFY(message_complete);
+ if (parser->upgrade) {
+ /* Exit, the rest of the message is in a different protocol. */
+ RETURN((p - data) + 1);
+ }
+ break;
+
+ case s_chunk_size_start:
+ {
+ assert(nread == 1);
+ assert(parser->flags & F_CHUNKED);
+
+ unhex_val = unhex[(unsigned char)ch];
+ if (UNLIKELY(unhex_val == -1)) {
+ SET_ERRNO(HPE_INVALID_CHUNK_SIZE);
+ goto error;
+ }
+
+ parser->content_length = unhex_val;
+ UPDATE_STATE(s_chunk_size);
+ break;
+ }
+
+ case s_chunk_size:
+ {
+ uint64_t t;
+
+ assert(parser->flags & F_CHUNKED);
+
+ if (ch == CR) {
+ UPDATE_STATE(s_chunk_size_almost_done);
+ break;
+ }
+
+ unhex_val = unhex[(unsigned char)ch];
+
+ if (unhex_val == -1) {
+ if (ch == ';' || ch == ' ') {
+ UPDATE_STATE(s_chunk_parameters);
+ break;
+ }
+
+ SET_ERRNO(HPE_INVALID_CHUNK_SIZE);
+ goto error;
+ }
+
+ t = parser->content_length;
+ t *= 16;
+ t += unhex_val;
+
+ /* Overflow? Test against a conservative limit for simplicity. */
+ if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) {
+ SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
+ goto error;
+ }
+
+ parser->content_length = t;
+ break;
+ }
+
+ case s_chunk_parameters:
+ {
+ assert(parser->flags & F_CHUNKED);
+ /* just ignore this shit. TODO check for overflow */
+ if (ch == CR) {
+ UPDATE_STATE(s_chunk_size_almost_done);
+ break;
+ }
+ break;
+ }
+
+ case s_chunk_size_almost_done:
+ {
+ assert(parser->flags & F_CHUNKED);
+ STRICT_CHECK(ch != LF);
+
+ parser->nread = 0;
+ nread = 0;
+
+ if (parser->content_length == 0) {
+ parser->flags |= F_TRAILING;
+ UPDATE_STATE(s_header_field_start);
+ } else {
+ UPDATE_STATE(s_chunk_data);
+ }
+ CALLBACK_NOTIFY(chunk_header);
+ break;
+ }
+
+ case s_chunk_data:
+ {
+ uint64_t to_read = MIN(parser->content_length,
+ (uint64_t) ((data + len) - p));
+
+ assert(parser->flags & F_CHUNKED);
+ assert(parser->content_length != 0
+ && parser->content_length != ULLONG_MAX);
+
+ /* See the explanation in s_body_identity for why the content
+ * length and data pointers are managed this way.
+ */
+ MARK(body);
+ parser->content_length -= to_read;
+ p += to_read - 1;
+
+ if (parser->content_length == 0) {
+ UPDATE_STATE(s_chunk_data_almost_done);
+ }
+
+ break;
+ }
+
+ case s_chunk_data_almost_done:
+ assert(parser->flags & F_CHUNKED);
+ assert(parser->content_length == 0);
+ STRICT_CHECK(ch != CR);
+ UPDATE_STATE(s_chunk_data_done);
+ CALLBACK_DATA(body);
+ break;
+
+ case s_chunk_data_done:
+ assert(parser->flags & F_CHUNKED);
+ STRICT_CHECK(ch != LF);
+ parser->nread = 0;
+ nread = 0;
+ UPDATE_STATE(s_chunk_size_start);
+ CALLBACK_NOTIFY(chunk_complete);
+ break;
+
+ default:
+ assert(0 && "unhandled state");
+ SET_ERRNO(HPE_INVALID_INTERNAL_STATE);
+ goto error;
+ }
+ }
+
+ /* Run callbacks for any marks that we have leftover after we ran out of
+ * bytes. There should be at most one of these set, so it's OK to invoke
+ * them in series (unset marks will not result in callbacks).
+ *
+ * We use the NOADVANCE() variety of callbacks here because 'p' has already
+ * overflowed 'data' and this allows us to correct for the off-by-one that
+ * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p'
+ * value that's in-bounds).
+ */
+
+ assert(((header_field_mark ? 1 : 0) +
+ (header_value_mark ? 1 : 0) +
+ (url_mark ? 1 : 0) +
+ (body_mark ? 1 : 0) +
+ (status_mark ? 1 : 0)) <= 1);
+
+ CALLBACK_DATA_NOADVANCE(header_field);
+ CALLBACK_DATA_NOADVANCE(header_value);
+ CALLBACK_DATA_NOADVANCE(url);
+ CALLBACK_DATA_NOADVANCE(body);
+ CALLBACK_DATA_NOADVANCE(status);
+
+ RETURN(len);
+
+error:
+ if (HTTP_PARSER_ERRNO(parser) == HPE_OK) {
+ SET_ERRNO(HPE_UNKNOWN);
+ }
+
+ RETURN(p - data);
+}
+
+
+/* Does the parser need to see an EOF to find the end of the message? */
+int
+http_message_needs_eof (const http_parser *parser)
+{
+ if (parser->type == HTTP_REQUEST) {
+ return 0;
+ }
+
+ /* See RFC 2616 section 4.4 */
+ if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */
+ parser->status_code == 204 || /* No Content */
+ parser->status_code == 304 || /* Not Modified */
+ parser->flags & F_SKIPBODY) { /* response to a HEAD request */
+ return 0;
+ }
+
+ /* RFC 7230 3.3.3, see `s_headers_almost_done` */
+ if ((parser->extra_flags & (F_TRANSFER_ENCODING >> 8)) &&
+ (parser->flags & F_CHUNKED) == 0) {
+ return 1;
+ }
+
+ if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) {
+ return 0;
+ }
+
+ return 1;
+}
+
+
+int
+http_should_keep_alive (const http_parser *parser)
+{
+ if (parser->http_major > 0 && parser->http_minor > 0) {
+ /* HTTP/1.1 */
+ if (parser->flags & F_CONNECTION_CLOSE) {
+ return 0;
+ }
+ } else {
+ /* HTTP/1.0 or earlier */
+ if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) {
+ return 0;
+ }
+ }
+
+ return !http_message_needs_eof(parser);
+}
+
+
+const char *
+http_method_str (enum http_method m)
+{
+ return ELEM_AT(method_strings, m, "<unknown>");
+}
+
+const char *
+http_status_str (enum http_status s)
+{
+ switch (s) {
+#define XX(num, name, string) case HTTP_STATUS_##name: return #string;
+ HTTP_STATUS_MAP(XX)
+#undef XX
+ default: return "<unknown>";
+ }
+}
+
+void
+http_parser_init (http_parser *parser, enum http_parser_type t)
+{
+ void *data = parser->data; /* preserve application data */
+ memset(parser, 0, sizeof(*parser));
+ parser->data = data;
+ parser->type = t;
+ parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res));
+ parser->http_errno = HPE_OK;
+}
+
+void
+http_parser_settings_init(http_parser_settings *settings)
+{
+ memset(settings, 0, sizeof(*settings));
+}
+
+const char *
+http_errno_name(enum http_errno err) {
+ assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab));
+ return http_strerror_tab[err].name;
+}
+
+const char *
+http_errno_description(enum http_errno err) {
+ assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab));
+ return http_strerror_tab[err].description;
+}
+
+static enum http_host_state
+http_parse_host_char(enum http_host_state s, const char ch) {
+ switch(s) {
+ case s_http_userinfo:
+ case s_http_userinfo_start:
+ if (ch == '@') {
+ return s_http_host_start;
+ }
+
+ if (IS_USERINFO_CHAR(ch)) {
+ return s_http_userinfo;
+ }
+ break;
+
+ case s_http_host_start:
+ if (ch == '[') {
+ return s_http_host_v6_start;
+ }
+
+ if (IS_HOST_CHAR(ch)) {
+ return s_http_host;
+ }
+
+ break;
+
+ case s_http_host:
+ if (IS_HOST_CHAR(ch)) {
+ return s_http_host;
+ }
+
+ /* fall through */
+ case s_http_host_v6_end:
+ if (ch == ':') {
+ return s_http_host_port_start;
+ }
+
+ break;
+
+ case s_http_host_v6:
+ if (ch == ']') {
+ return s_http_host_v6_end;
+ }
+
+ /* fall through */
+ case s_http_host_v6_start:
+ if (IS_HEX(ch) || ch == ':' || ch == '.') {
+ return s_http_host_v6;
+ }
+
+ if (s == s_http_host_v6 && ch == '%') {
+ return s_http_host_v6_zone_start;
+ }
+ break;
+
+ case s_http_host_v6_zone:
+ if (ch == ']') {
+ return s_http_host_v6_end;
+ }
+
+ /* fall through */
+ case s_http_host_v6_zone_start:
+ /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */
+ if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' ||
+ ch == '~') {
+ return s_http_host_v6_zone;
+ }
+ break;
+
+ case s_http_host_port:
+ case s_http_host_port_start:
+ if (IS_NUM(ch)) {
+ return s_http_host_port;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ return s_http_host_dead;
+}
+
+static int
+http_parse_host(const char * buf, struct http_parser_url *u, int found_at) {
+ enum http_host_state s;
+
+ const char *p;
+ size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len;
+
+ assert(u->field_set & (1 << UF_HOST));
+
+ u->field_data[UF_HOST].len = 0;
+
+ s = found_at ? s_http_userinfo_start : s_http_host_start;
+
+ for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) {
+ enum http_host_state new_s = http_parse_host_char(s, *p);
+
+ if (new_s == s_http_host_dead) {
+ return 1;
+ }
+
+ switch(new_s) {
+ case s_http_host:
+ if (s != s_http_host) {
+ u->field_data[UF_HOST].off = (uint16_t)(p - buf);
+ }
+ u->field_data[UF_HOST].len++;
+ break;
+
+ case s_http_host_v6:
+ if (s != s_http_host_v6) {
+ u->field_data[UF_HOST].off = (uint16_t)(p - buf);
+ }
+ u->field_data[UF_HOST].len++;
+ break;
+
+ case s_http_host_v6_zone_start:
+ case s_http_host_v6_zone:
+ u->field_data[UF_HOST].len++;
+ break;
+
+ case s_http_host_port:
+ if (s != s_http_host_port) {
+ u->field_data[UF_PORT].off = (uint16_t)(p - buf);
+ u->field_data[UF_PORT].len = 0;
+ u->field_set |= (1 << UF_PORT);
+ }
+ u->field_data[UF_PORT].len++;
+ break;
+
+ case s_http_userinfo:
+ if (s != s_http_userinfo) {
+ u->field_data[UF_USERINFO].off = (uint16_t)(p - buf);
+ u->field_data[UF_USERINFO].len = 0;
+ u->field_set |= (1 << UF_USERINFO);
+ }
+ u->field_data[UF_USERINFO].len++;
+ break;
+
+ default:
+ break;
+ }
+ s = new_s;
+ }
+
+ /* Make sure we don't end somewhere unexpected */
+ switch (s) {
+ case s_http_host_start:
+ case s_http_host_v6_start:
+ case s_http_host_v6:
+ case s_http_host_v6_zone_start:
+ case s_http_host_v6_zone:
+ case s_http_host_port_start:
+ case s_http_userinfo:
+ case s_http_userinfo_start:
+ return 1;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+void
+http_parser_url_init(struct http_parser_url *u) {
+ memset(u, 0, sizeof(*u));
+}
+
+int
+http_parser_parse_url(const char *buf, size_t buflen, int is_connect,
+ struct http_parser_url *u)
+{
+ enum state s;
+ const char *p;
+ enum http_parser_url_fields uf, old_uf;
+ int found_at = 0;
+
+ if (buflen == 0) {
+ return 1;
+ }
+
+ u->port = u->field_set = 0;
+ s = is_connect ? s_req_server_start : s_req_spaces_before_url;
+ old_uf = UF_MAX;
+
+ for (p = buf; p < buf + buflen; p++) {
+ s = parse_url_char(s, *p);
+
+ /* Figure out the next field that we're operating on */
+ switch (s) {
+ case s_dead:
+ return 1;
+
+ /* Skip delimeters */
+ case s_req_schema_slash:
+ case s_req_schema_slash_slash:
+ case s_req_server_start:
+ case s_req_query_string_start:
+ case s_req_fragment_start:
+ continue;
+
+ case s_req_schema:
+ uf = UF_SCHEMA;
+ break;
+
+ case s_req_server_with_at:
+ found_at = 1;
+
+ /* fall through */
+ case s_req_server:
+ uf = UF_HOST;
+ break;
+
+ case s_req_path:
+ uf = UF_PATH;
+ break;
+
+ case s_req_query_string:
+ uf = UF_QUERY;
+ break;
+
+ case s_req_fragment:
+ uf = UF_FRAGMENT;
+ break;
+
+ default:
+ assert(!"Unexpected state");
+ return 1;
+ }
+
+ /* Nothing's changed; soldier on */
+ if (uf == old_uf) {
+ u->field_data[uf].len++;
+ continue;
+ }
+
+ u->field_data[uf].off = (uint16_t)(p - buf);
+ u->field_data[uf].len = 1;
+
+ u->field_set |= (1 << uf);
+ old_uf = uf;
+ }
+
+ /* host must be present if there is a schema */
+ /* parsing http:///toto will fail */
+ if ((u->field_set & (1 << UF_SCHEMA)) &&
+ (u->field_set & (1 << UF_HOST)) == 0) {
+ return 1;
+ }
+
+ if (u->field_set & (1 << UF_HOST)) {
+ if (http_parse_host(buf, u, found_at) != 0) {
+ return 1;
+ }
+ }
+
+ /* CONNECT requests can only contain "hostname:port" */
+ if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) {
+ return 1;
+ }
+
+ if (u->field_set & (1 << UF_PORT)) {
+ uint16_t off;
+ uint16_t len;
+ const char* p;
+ const char* end;
+ unsigned long v;
+
+ off = u->field_data[UF_PORT].off;
+ len = u->field_data[UF_PORT].len;
+ end = buf + off + len;
+
+ /* NOTE: The characters are already validated and are in the [0-9] range */
+ assert(off + len <= buflen && "Port number overflow");
+ v = 0;
+ for (p = buf + off; p < end; p++) {
+ v *= 10;
+ v += *p - '0';
+
+ /* Ports have a max value of 2^16 */
+ if (v > 0xffff) {
+ return 1;
+ }
+ }
+
+ u->port = (uint16_t) v;
+ }
+
+ return 0;
+}
+
+void
+http_parser_pause(http_parser *parser, int paused) {
+ /* Users should only be pausing/unpausing a parser that is not in an error
+ * state. In non-debug builds, there's not much that we can do about this
+ * other than ignore it.
+ */
+ if (HTTP_PARSER_ERRNO(parser) == HPE_OK ||
+ HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) {
+ uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */
+ SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK);
+ } else {
+ assert(0 && "Attempting to pause parser in error state");
+ }
+}
+
+int
+http_body_is_final(const struct http_parser *parser) {
+ return parser->state == s_message_done;
+}
+
+unsigned long
+http_parser_version(void) {
+ return HTTP_PARSER_VERSION_MAJOR * 0x10000 |
+ HTTP_PARSER_VERSION_MINOR * 0x00100 |
+ HTTP_PARSER_VERSION_PATCH * 0x00001;
+}
+
+void
+http_parser_set_max_header_size(uint32_t size) {
+ max_header_size = size;
+}
diff --git a/contrib/restricted/http-parser/http_parser.h b/contrib/restricted/http-parser/http_parser.h
new file mode 100644
index 0000000000..262a0394c8
--- /dev/null
+++ b/contrib/restricted/http-parser/http_parser.h
@@ -0,0 +1,443 @@
+/* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+#ifndef http_parser_h
+#define http_parser_h
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Also update SONAME in the Makefile whenever you change these. */
+#define HTTP_PARSER_VERSION_MAJOR 2
+#define HTTP_PARSER_VERSION_MINOR 9
+#define HTTP_PARSER_VERSION_PATCH 4
+
+#include <stddef.h>
+#if defined(_WIN32) && !defined(__MINGW32__) && \
+ (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__)
+#include <BaseTsd.h>
+typedef __int8 int8_t;
+typedef unsigned __int8 uint8_t;
+typedef __int16 int16_t;
+typedef unsigned __int16 uint16_t;
+typedef __int32 int32_t;
+typedef unsigned __int32 uint32_t;
+typedef __int64 int64_t;
+typedef unsigned __int64 uint64_t;
+#else
+#include <stdint.h>
+#endif
+
+/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run
+ * faster
+ */
+#ifndef HTTP_PARSER_STRICT
+# define HTTP_PARSER_STRICT 1
+#endif
+
+/* Maximium header size allowed. If the macro is not defined
+ * before including this header then the default is used. To
+ * change the maximum header size, define the macro in the build
+ * environment (e.g. -DHTTP_MAX_HEADER_SIZE=<value>). To remove
+ * the effective limit on the size of the header, define the macro
+ * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff)
+ */
+#ifndef HTTP_MAX_HEADER_SIZE
+# define HTTP_MAX_HEADER_SIZE (80*1024)
+#endif
+
+typedef struct http_parser http_parser;
+typedef struct http_parser_settings http_parser_settings;
+
+
+/* Callbacks should return non-zero to indicate an error. The parser will
+ * then halt execution.
+ *
+ * The one exception is on_headers_complete. In a HTTP_RESPONSE parser
+ * returning '1' from on_headers_complete will tell the parser that it
+ * should not expect a body. This is used when receiving a response to a
+ * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding:
+ * chunked' headers that indicate the presence of a body.
+ *
+ * Returning `2` from on_headers_complete will tell parser that it should not
+ * expect neither a body nor any futher responses on this connection. This is
+ * useful for handling responses to a CONNECT request which may not contain
+ * `Upgrade` or `Connection: upgrade` headers.
+ *
+ * http_data_cb does not return data chunks. It will be called arbitrarily
+ * many times for each string. E.G. you might get 10 callbacks for "on_url"
+ * each providing just a few characters more data.
+ */
+typedef int (*http_data_cb) (http_parser*, const char *at, size_t length);
+typedef int (*http_cb) (http_parser*);
+
+
+/* Status Codes */
+#define HTTP_STATUS_MAP(XX) \
+ XX(100, CONTINUE, Continue) \
+ XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \
+ XX(102, PROCESSING, Processing) \
+ XX(200, OK, OK) \
+ XX(201, CREATED, Created) \
+ XX(202, ACCEPTED, Accepted) \
+ XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \
+ XX(204, NO_CONTENT, No Content) \
+ XX(205, RESET_CONTENT, Reset Content) \
+ XX(206, PARTIAL_CONTENT, Partial Content) \
+ XX(207, MULTI_STATUS, Multi-Status) \
+ XX(208, ALREADY_REPORTED, Already Reported) \
+ XX(226, IM_USED, IM Used) \
+ XX(300, MULTIPLE_CHOICES, Multiple Choices) \
+ XX(301, MOVED_PERMANENTLY, Moved Permanently) \
+ XX(302, FOUND, Found) \
+ XX(303, SEE_OTHER, See Other) \
+ XX(304, NOT_MODIFIED, Not Modified) \
+ XX(305, USE_PROXY, Use Proxy) \
+ XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \
+ XX(308, PERMANENT_REDIRECT, Permanent Redirect) \
+ XX(400, BAD_REQUEST, Bad Request) \
+ XX(401, UNAUTHORIZED, Unauthorized) \
+ XX(402, PAYMENT_REQUIRED, Payment Required) \
+ XX(403, FORBIDDEN, Forbidden) \
+ XX(404, NOT_FOUND, Not Found) \
+ XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \
+ XX(406, NOT_ACCEPTABLE, Not Acceptable) \
+ XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \
+ XX(408, REQUEST_TIMEOUT, Request Timeout) \
+ XX(409, CONFLICT, Conflict) \
+ XX(410, GONE, Gone) \
+ XX(411, LENGTH_REQUIRED, Length Required) \
+ XX(412, PRECONDITION_FAILED, Precondition Failed) \
+ XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \
+ XX(414, URI_TOO_LONG, URI Too Long) \
+ XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \
+ XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \
+ XX(417, EXPECTATION_FAILED, Expectation Failed) \
+ XX(421, MISDIRECTED_REQUEST, Misdirected Request) \
+ XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \
+ XX(423, LOCKED, Locked) \
+ XX(424, FAILED_DEPENDENCY, Failed Dependency) \
+ XX(426, UPGRADE_REQUIRED, Upgrade Required) \
+ XX(428, PRECONDITION_REQUIRED, Precondition Required) \
+ XX(429, TOO_MANY_REQUESTS, Too Many Requests) \
+ XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \
+ XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \
+ XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \
+ XX(501, NOT_IMPLEMENTED, Not Implemented) \
+ XX(502, BAD_GATEWAY, Bad Gateway) \
+ XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \
+ XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \
+ XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \
+ XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \
+ XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \
+ XX(508, LOOP_DETECTED, Loop Detected) \
+ XX(510, NOT_EXTENDED, Not Extended) \
+ XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \
+
+enum http_status
+ {
+#define XX(num, name, string) HTTP_STATUS_##name = num,
+ HTTP_STATUS_MAP(XX)
+#undef XX
+ };
+
+
+/* Request Methods */
+#define HTTP_METHOD_MAP(XX) \
+ XX(0, DELETE, DELETE) \
+ XX(1, GET, GET) \
+ XX(2, HEAD, HEAD) \
+ XX(3, POST, POST) \
+ XX(4, PUT, PUT) \
+ /* pathological */ \
+ XX(5, CONNECT, CONNECT) \
+ XX(6, OPTIONS, OPTIONS) \
+ XX(7, TRACE, TRACE) \
+ /* WebDAV */ \
+ XX(8, COPY, COPY) \
+ XX(9, LOCK, LOCK) \
+ XX(10, MKCOL, MKCOL) \
+ XX(11, MOVE, MOVE) \
+ XX(12, PROPFIND, PROPFIND) \
+ XX(13, PROPPATCH, PROPPATCH) \
+ XX(14, SEARCH, SEARCH) \
+ XX(15, UNLOCK, UNLOCK) \
+ XX(16, BIND, BIND) \
+ XX(17, REBIND, REBIND) \
+ XX(18, UNBIND, UNBIND) \
+ XX(19, ACL, ACL) \
+ /* subversion */ \
+ XX(20, REPORT, REPORT) \
+ XX(21, MKACTIVITY, MKACTIVITY) \
+ XX(22, CHECKOUT, CHECKOUT) \
+ XX(23, MERGE, MERGE) \
+ /* upnp */ \
+ XX(24, MSEARCH, M-SEARCH) \
+ XX(25, NOTIFY, NOTIFY) \
+ XX(26, SUBSCRIBE, SUBSCRIBE) \
+ XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \
+ /* RFC-5789 */ \
+ XX(28, PATCH, PATCH) \
+ XX(29, PURGE, PURGE) \
+ /* CalDAV */ \
+ XX(30, MKCALENDAR, MKCALENDAR) \
+ /* RFC-2068, section 19.6.1.2 */ \
+ XX(31, LINK, LINK) \
+ XX(32, UNLINK, UNLINK) \
+ /* icecast */ \
+ XX(33, SOURCE, SOURCE) \
+
+enum http_method
+ {
+#define XX(num, name, string) HTTP_##name = num,
+ HTTP_METHOD_MAP(XX)
+#undef XX
+ };
+
+
+enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH };
+
+
+/* Flag values for http_parser.flags field */
+enum flags
+ { F_CHUNKED = 1 << 0
+ , F_CONNECTION_KEEP_ALIVE = 1 << 1
+ , F_CONNECTION_CLOSE = 1 << 2
+ , F_CONNECTION_UPGRADE = 1 << 3
+ , F_TRAILING = 1 << 4
+ , F_UPGRADE = 1 << 5
+ , F_SKIPBODY = 1 << 6
+ , F_CONTENTLENGTH = 1 << 7
+ , F_TRANSFER_ENCODING = 1 << 8 /* Never set in http_parser.flags */
+ };
+
+
+/* Map for errno-related constants
+ *
+ * The provided argument should be a macro that takes 2 arguments.
+ */
+#define HTTP_ERRNO_MAP(XX) \
+ /* No error */ \
+ XX(OK, "success") \
+ \
+ /* Callback-related errors */ \
+ XX(CB_message_begin, "the on_message_begin callback failed") \
+ XX(CB_url, "the on_url callback failed") \
+ XX(CB_header_field, "the on_header_field callback failed") \
+ XX(CB_header_value, "the on_header_value callback failed") \
+ XX(CB_headers_complete, "the on_headers_complete callback failed") \
+ XX(CB_body, "the on_body callback failed") \
+ XX(CB_message_complete, "the on_message_complete callback failed") \
+ XX(CB_status, "the on_status callback failed") \
+ XX(CB_chunk_header, "the on_chunk_header callback failed") \
+ XX(CB_chunk_complete, "the on_chunk_complete callback failed") \
+ \
+ /* Parsing-related errors */ \
+ XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \
+ XX(HEADER_OVERFLOW, \
+ "too many header bytes seen; overflow detected") \
+ XX(CLOSED_CONNECTION, \
+ "data received after completed connection: close message") \
+ XX(INVALID_VERSION, "invalid HTTP version") \
+ XX(INVALID_STATUS, "invalid HTTP status code") \
+ XX(INVALID_METHOD, "invalid HTTP method") \
+ XX(INVALID_URL, "invalid URL") \
+ XX(INVALID_HOST, "invalid host") \
+ XX(INVALID_PORT, "invalid port") \
+ XX(INVALID_PATH, "invalid path") \
+ XX(INVALID_QUERY_STRING, "invalid query string") \
+ XX(INVALID_FRAGMENT, "invalid fragment") \
+ XX(LF_EXPECTED, "LF character expected") \
+ XX(INVALID_HEADER_TOKEN, "invalid character in header") \
+ XX(INVALID_CONTENT_LENGTH, \
+ "invalid character in content-length header") \
+ XX(UNEXPECTED_CONTENT_LENGTH, \
+ "unexpected content-length header") \
+ XX(INVALID_CHUNK_SIZE, \
+ "invalid character in chunk size header") \
+ XX(INVALID_CONSTANT, "invalid constant string") \
+ XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
+ XX(STRICT, "strict mode assertion failed") \
+ XX(PAUSED, "parser is paused") \
+ XX(UNKNOWN, "an unknown error occurred") \
+ XX(INVALID_TRANSFER_ENCODING, \
+ "request has invalid transfer-encoding") \
+
+
+/* Define HPE_* values for each errno value above */
+#define HTTP_ERRNO_GEN(n, s) HPE_##n,
+enum http_errno {
+ HTTP_ERRNO_MAP(HTTP_ERRNO_GEN)
+};
+#undef HTTP_ERRNO_GEN
+
+
+/* Get an http_errno value from an http_parser */
+#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno)
+
+
+struct http_parser {
+ /** PRIVATE **/
+ unsigned int type : 2; /* enum http_parser_type */
+ unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
+ unsigned int state : 7; /* enum state from http_parser.c */
+ unsigned int header_state : 7; /* enum header_state from http_parser.c */
+ unsigned int index : 5; /* index into current matcher */
+ unsigned int extra_flags : 2;
+ unsigned int lenient_http_headers : 1;
+
+ uint32_t nread; /* # bytes read in various scenarios */
+ uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
+
+ /** READ-ONLY **/
+ unsigned short http_major;
+ unsigned short http_minor;
+ unsigned int status_code : 16; /* responses only */
+ unsigned int method : 8; /* requests only */
+ unsigned int http_errno : 7;
+
+ /* 1 = Upgrade header was present and the parser has exited because of that.
+ * 0 = No upgrade header present.
+ * Should be checked when http_parser_execute() returns in addition to
+ * error checking.
+ */
+ unsigned int upgrade : 1;
+
+ /** PUBLIC **/
+ void *data; /* A pointer to get hook to the "connection" or "socket" object */
+};
+
+
+struct http_parser_settings {
+ http_cb on_message_begin;
+ http_data_cb on_url;
+ http_data_cb on_status;
+ http_data_cb on_header_field;
+ http_data_cb on_header_value;
+ http_cb on_headers_complete;
+ http_data_cb on_body;
+ http_cb on_message_complete;
+ /* When on_chunk_header is called, the current chunk length is stored
+ * in parser->content_length.
+ */
+ http_cb on_chunk_header;
+ http_cb on_chunk_complete;
+};
+
+
+enum http_parser_url_fields
+ { UF_SCHEMA = 0
+ , UF_HOST = 1
+ , UF_PORT = 2
+ , UF_PATH = 3
+ , UF_QUERY = 4
+ , UF_FRAGMENT = 5
+ , UF_USERINFO = 6
+ , UF_MAX = 7
+ };
+
+
+/* Result structure for http_parser_parse_url().
+ *
+ * Callers should index into field_data[] with UF_* values iff field_set
+ * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and
+ * because we probably have padding left over), we convert any port to
+ * a uint16_t.
+ */
+struct http_parser_url {
+ uint16_t field_set; /* Bitmask of (1 << UF_*) values */
+ uint16_t port; /* Converted UF_PORT string */
+
+ struct {
+ uint32_t off; /* Offset into buffer in which field starts */
+ uint32_t len; /* Length of run in buffer */
+ } field_data[UF_MAX];
+};
+
+
+/* Returns the library version. Bits 16-23 contain the major version number,
+ * bits 8-15 the minor version number and bits 0-7 the patch level.
+ * Usage example:
+ *
+ * unsigned long version = http_parser_version();
+ * unsigned major = (version >> 16) & 255;
+ * unsigned minor = (version >> 8) & 255;
+ * unsigned patch = version & 255;
+ * printf("http_parser v%u.%u.%u\n", major, minor, patch);
+ */
+unsigned long http_parser_version(void);
+
+void http_parser_init(http_parser *parser, enum http_parser_type type);
+
+
+/* Initialize http_parser_settings members to 0
+ */
+void http_parser_settings_init(http_parser_settings *settings);
+
+
+/* Executes the parser. Returns number of parsed bytes. Sets
+ * `parser->http_errno` on error. */
+size_t http_parser_execute(http_parser *parser,
+ const http_parser_settings *settings,
+ const char *data,
+ size_t len);
+
+
+/* If http_should_keep_alive() in the on_headers_complete or
+ * on_message_complete callback returns 0, then this should be
+ * the last message on the connection.
+ * If you are the server, respond with the "Connection: close" header.
+ * If you are the client, close the connection.
+ */
+int http_should_keep_alive(const http_parser *parser);
+
+/* Returns a string version of the HTTP method. */
+const char *http_method_str(enum http_method m);
+
+/* Returns a string version of the HTTP status code. */
+const char *http_status_str(enum http_status s);
+
+/* Return a string name of the given error */
+const char *http_errno_name(enum http_errno err);
+
+/* Return a string description of the given error */
+const char *http_errno_description(enum http_errno err);
+
+/* Initialize all http_parser_url members to 0 */
+void http_parser_url_init(struct http_parser_url *u);
+
+/* Parse a URL; return nonzero on failure */
+int http_parser_parse_url(const char *buf, size_t buflen,
+ int is_connect,
+ struct http_parser_url *u);
+
+/* Pause or un-pause the parser; a nonzero value pauses */
+void http_parser_pause(http_parser *parser, int paused);
+
+/* Checks if this is the final chunk of the body. */
+int http_body_is_final(const http_parser *parser);
+
+/* Change the maximum header size provided at compile time. */
+void http_parser_set_max_header_size(uint32_t size);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/contrib/restricted/http-parser/ya.make b/contrib/restricted/http-parser/ya.make
new file mode 100644
index 0000000000..e7b705f984
--- /dev/null
+++ b/contrib/restricted/http-parser/ya.make
@@ -0,0 +1,30 @@
+# Generated by devtools/yamaker from nixpkgs 22.11.
+
+LIBRARY()
+
+LICENSE(MIT)
+
+LICENSE_TEXTS(.yandex_meta/licenses.list.txt)
+
+VERSION(2.9.4)
+
+ORIGINAL_SOURCE(https://github.com/nodejs/http-parser/archive/v2.9.4.tar.gz)
+
+ADDINCL(
+ contrib/restricted/http-parser
+)
+
+NO_COMPILER_WARNINGS()
+
+NO_RUNTIME()
+
+CFLAGS(
+ -DHTTP_MAX_HEADER_SIZE=0x7fffffff
+ -DHTTP_PARSER_STRICT=0
+)
+
+SRCS(
+ http_parser.c
+)
+
+END()
diff --git a/contrib/tools/flatc/bin/ya.make b/contrib/tools/flatc/bin/ya.make
new file mode 100644
index 0000000000..f94d2e6aa2
--- /dev/null
+++ b/contrib/tools/flatc/bin/ya.make
@@ -0,0 +1,23 @@
+PROGRAM(flatc)
+
+NO_UTIL()
+
+ADDINCL(
+ contrib/libs/flatbuffers/include
+)
+
+PEERDIR(
+ contrib/libs/flatbuffers/flatc
+)
+
+SRCDIR(
+ contrib/libs/flatbuffers/src
+)
+
+SRCS(
+ flatc_main.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/build/prebuilt/contrib/tools/flatc/ya.make.induced_deps)
+
+END()
diff --git a/contrib/tools/flatc/ya.make b/contrib/tools/flatc/ya.make
new file mode 100644
index 0000000000..93e5104a93
--- /dev/null
+++ b/contrib/tools/flatc/ya.make
@@ -0,0 +1,11 @@
+IF (USE_PREBUILT_TOOLS)
+ INCLUDE(${ARCADIA_ROOT}/build/prebuilt/contrib/tools/flatc/ya.make.prebuilt)
+ENDIF()
+
+IF (NOT PREBUILT)
+ INCLUDE(${ARCADIA_ROOT}/contrib/tools/flatc/bin/ya.make)
+ENDIF()
+
+RECURSE(
+ bin
+)
diff --git a/library/cpp/containers/concurrent_hash/concurrent_hash.h b/library/cpp/containers/concurrent_hash/concurrent_hash.h
new file mode 100644
index 0000000000..f15a1c3d6e
--- /dev/null
+++ b/library/cpp/containers/concurrent_hash/concurrent_hash.h
@@ -0,0 +1,128 @@
+#pragma once
+
+#include <util/generic/hash.h>
+#include <util/system/spinlock.h>
+
+#include <array>
+
+template <typename K, typename V, size_t BucketCount = 64, typename L = TAdaptiveLock>
+class TConcurrentHashMap {
+public:
+ using TActualMap = THashMap<K, V>;
+ using TLock = L;
+
+ struct TBucket {
+ friend class TConcurrentHashMap;
+
+ private:
+ TActualMap Map;
+ mutable TLock Mutex;
+
+ public:
+ TLock& GetMutex() const {
+ return Mutex;
+ }
+
+ TActualMap& GetMap() {
+ return Map;
+ }
+ const TActualMap& GetMap() const {
+ return Map;
+ }
+
+ const V& GetUnsafe(const K& key) const {
+ typename TActualMap::const_iterator it = Map.find(key);
+ Y_VERIFY(it != Map.end(), "not found by key");
+ return it->second;
+ }
+
+ V& GetUnsafe(const K& key) {
+ typename TActualMap::iterator it = Map.find(key);
+ Y_VERIFY(it != Map.end(), "not found by key");
+ return it->second;
+ }
+
+ V RemoveUnsafe(const K& key) {
+ typename TActualMap::iterator it = Map.find(key);
+ Y_VERIFY(it != Map.end(), "removing non-existent key");
+ V r = std::move(it->second);
+ Map.erase(it);
+ return r;
+ }
+
+ bool HasUnsafe(const K& key) const {
+ typename TActualMap::const_iterator it = Map.find(key);
+ return (it != Map.end());
+ }
+ };
+
+ std::array<TBucket, BucketCount> Buckets;
+
+public:
+ TBucket& GetBucketForKey(const K& key) {
+ return Buckets[THash<K>()(key) % BucketCount];
+ }
+
+ const TBucket& GetBucketForKey(const K& key) const {
+ return Buckets[THash<K>()(key) % BucketCount];
+ }
+
+ void Insert(const K& key, const V& value) {
+ TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ bucket.Map[key] = value;
+ }
+
+ void InsertUnique(const K& key, const V& value) {
+ TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ if (!bucket.Map.insert(std::make_pair(key, value)).second) {
+ Y_FAIL("non-unique key");
+ }
+ }
+
+ V& InsertIfAbsent(const K& key, const V& value) {
+ TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ return bucket.Map.insert(std::make_pair(key, value)).first->second;
+ }
+
+ template <typename Callable>
+ V& InsertIfAbsentWithInit(const K& key, Callable initFunc) {
+ TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ if (bucket.HasUnsafe(key)) {
+ return bucket.GetUnsafe(key);
+ }
+
+ return bucket.Map.insert(std::make_pair(key, initFunc())).first->second;
+ }
+
+ V Get(const K& key) const {
+ const TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ return bucket.GetUnsafe(key);
+ }
+
+ bool Get(const K& key, V& result) const {
+ const TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ if (bucket.HasUnsafe(key)) {
+ result = bucket.GetUnsafe(key);
+ return true;
+ }
+ return false;
+ }
+
+ V Remove(const K& key) {
+ TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ return bucket.RemoveUnsafe(key);
+ }
+
+ bool Has(const K& key) const {
+ const TBucket& bucket = GetBucketForKey(key);
+ TGuard<TLock> guard(bucket.Mutex);
+ return bucket.HasUnsafe(key);
+ }
+};
diff --git a/library/cpp/disjoint_sets/disjoint_sets.cpp b/library/cpp/disjoint_sets/disjoint_sets.cpp
new file mode 100644
index 0000000000..5720e6c41a
--- /dev/null
+++ b/library/cpp/disjoint_sets/disjoint_sets.cpp
@@ -0,0 +1 @@
+#include "disjoint_sets.h"
diff --git a/library/cpp/disjoint_sets/disjoint_sets.h b/library/cpp/disjoint_sets/disjoint_sets.h
new file mode 100644
index 0000000000..5768886704
--- /dev/null
+++ b/library/cpp/disjoint_sets/disjoint_sets.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <util/system/yassert.h>
+#include <util/generic/vector.h>
+
+// Implementation of disjoint-set data structure with union by rank and path compression.
+// See http://en.wikipedia.org/wiki/Disjoint-set_data_structure
+class TDisjointSets {
+public:
+ using TElement = size_t;
+
+private:
+ mutable TVector<TElement> Parents;
+ TVector<size_t> Ranks;
+ TVector<size_t> Sizes;
+ size_t NumberOfSets;
+
+public:
+ TDisjointSets(size_t setCount)
+ : Parents(setCount)
+ , Ranks(setCount, 0)
+ , Sizes(setCount, 1)
+ , NumberOfSets(setCount)
+ {
+ for (size_t i = 0; i < setCount; ++i)
+ Parents[i] = i;
+ }
+
+ TElement CanonicSetElement(TElement item) const {
+ if (Parents[item] != item)
+ Parents[item] = CanonicSetElement(Parents[item]);
+ return Parents[item];
+ }
+
+ size_t SizeOfSet(TElement item) const {
+ return Sizes[CanonicSetElement(item)];
+ }
+
+ size_t InitialSetCount() const {
+ return Parents.size();
+ }
+
+ size_t SetCount() const {
+ return NumberOfSets;
+ }
+
+ void UnionSets(TElement item1, TElement item2) {
+ TElement canonic1 = CanonicSetElement(item1);
+ TElement canonic2 = CanonicSetElement(item2);
+ if (canonic1 == canonic2)
+ return;
+
+ --NumberOfSets;
+ if (Ranks[canonic1] < Ranks[canonic2]) {
+ Parents[canonic1] = canonic2;
+ Sizes[canonic2] += Sizes[canonic1];
+ } else {
+ Parents[canonic2] = canonic1;
+ Sizes[canonic1] += Sizes[canonic2];
+ Ranks[canonic2] += Ranks[canonic1] == Ranks[canonic2] ? 1 : 0;
+ }
+ }
+
+ void Expand(size_t setCount) {
+ if (setCount < Parents.size()) {
+ return;
+ }
+
+ size_t prevSize = Parents.size();
+ Parents.resize(setCount);
+ Ranks.resize(setCount);
+ Sizes.resize(setCount);
+ NumberOfSets += setCount - prevSize;
+
+ for (size_t i = prevSize; i < setCount; ++i) {
+ Parents[i] = i;
+ Ranks[i] = 0;
+ Sizes[i] = 1;
+ }
+ }
+};
diff --git a/library/cpp/disjoint_sets/ya.make b/library/cpp/disjoint_sets/ya.make
new file mode 100644
index 0000000000..2104298baa
--- /dev/null
+++ b/library/cpp/disjoint_sets/ya.make
@@ -0,0 +1,7 @@
+LIBRARY()
+
+SRCS(
+ disjoint_sets.cpp
+)
+
+END()
diff --git a/library/cpp/dwarf_backtrace/backtrace.cpp b/library/cpp/dwarf_backtrace/backtrace.cpp
new file mode 100644
index 0000000000..a955d07249
--- /dev/null
+++ b/library/cpp/dwarf_backtrace/backtrace.cpp
@@ -0,0 +1,62 @@
+#include "backtrace.h"
+
+#include <contrib/libs/backtrace/backtrace.h>
+
+#include <util/generic/yexception.h>
+#include <util/system/type_name.h>
+#include <util/system/execpath.h>
+
+namespace NDwarf {
+ namespace {
+ struct TContext {
+ TCallback& Callback;
+ int Counter = 0;
+ TMaybe<TError> Error;
+ };
+
+ void HandleLibBacktraceError(void* data, const char* msg, int errnum) {
+ auto* context = reinterpret_cast<TContext*>(data);
+ context->Error = TError{.Code = errnum, .Message=msg};
+ }
+
+ int HandleLibBacktraceFrame(void* data, uintptr_t pc, const char* filename, int lineno, const char* function) {
+ auto* context = reinterpret_cast<TContext*>(data);
+ TLineInfo lineInfo{
+ .FileName = filename != nullptr ? filename : "???",
+ .Line = lineno,
+ .Col = 0, // libbacktrace doesn't provide column numbers, so fill this field with a dummy value.
+ .FunctionName = function != nullptr ? CppDemangle(function) : "???",
+ .Address = pc,
+ .Index = context->Counter++,
+ };
+ return static_cast<int>(context->Callback(lineInfo));
+ }
+ }
+
+ TMaybe<TError> ResolveBacktrace(TArrayRef<const void* const> backtrace, TCallback callback) {
+ TContext context{.Callback = callback};
+ // Intentionally never freed (see https://a.yandex-team.ru/arc/trunk/arcadia/contrib/libs/backtrace/backtrace.h?rev=6789902#L80).
+ static auto* state = backtrace_create_state(
+ GetPersistentExecPath().c_str(),
+ 1 /* threaded */,
+ HandleLibBacktraceError,
+ &context /* data for the error callback */
+ );
+ if (nullptr == state) {
+ static const auto initError = context.Error;
+ return initError;
+ }
+ for (const void* address : backtrace) {
+ int status = backtrace_pcinfo(
+ state,
+ reinterpret_cast<uintptr_t>(address) - 1, // last byte of the call instruction
+ HandleLibBacktraceFrame,
+ HandleLibBacktraceError,
+ &context /* data for both callbacks */);
+ if (0 != status) {
+ break;
+ }
+ }
+ return context.Error;
+ }
+}
diff --git a/library/cpp/dwarf_backtrace/backtrace.h b/library/cpp/dwarf_backtrace/backtrace.h
new file mode 100644
index 0000000000..62ec36dba5
--- /dev/null
+++ b/library/cpp/dwarf_backtrace/backtrace.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <util/generic/array_ref.h>
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+#include <functional>
+
+namespace NDwarf {
+ struct TLineInfo {
+ TString FileName;
+ int Line;
+ int Col;
+ TString FunctionName;
+ uintptr_t Address;
+ int Index;
+ };
+
+ struct TError {
+ int Code;
+ TString Message;
+ };
+
+ enum class EResolving {
+ Continue = 0,
+ Break = 1,
+ };
+
+ using TCallback = std::function<EResolving(const TLineInfo&)>;
+
+ // Resolves backtrace addresses and calls the callback for all line infos of inlined functions there.
+ // Stops execution if the callback returns `EResolving::Break`.
+ [[nodiscard]] TMaybe<TError> ResolveBacktrace(TArrayRef<const void* const> backtrace, TCallback callback);
+
+}
diff --git a/library/cpp/dwarf_backtrace/ya.make b/library/cpp/dwarf_backtrace/ya.make
new file mode 100644
index 0000000000..e955f9aa92
--- /dev/null
+++ b/library/cpp/dwarf_backtrace/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+NO_WSHADOW()
+
+SRCS(
+ backtrace.cpp
+)
+
+PEERDIR(
+ contrib/libs/backtrace
+)
+
+END()
+
+IF (NOT OS_WINDOWS)
+ RECURSE_FOR_TESTS(
+ ut
+ )
+ENDIF()
diff --git a/library/cpp/erasure/README.md b/library/cpp/erasure/README.md
new file mode 100644
index 0000000000..7723bf7c23
--- /dev/null
+++ b/library/cpp/erasure/README.md
@@ -0,0 +1,9 @@
+# Erasure based codecs for arbitrary data
+
+C++ wrapper for LRC and Reed-Solomon erasure codecs.
+There are two backends for LRC: Jerasure(http://jerasure.org) and ISA-L(https://github.com/intel/isa-l). ISA-L is much faster - it condsiders different instrucion sets to optimize speed of encode and decode. The only limitations now are if you don't have SSE4.2 instruction set (then base variant is as slow as Jerasure) or if you run it on aarch64 architecture (however, 2.29 version will be going to support fast implementation). However, we still have to keep Jerasure because it is incompatible due to some optimization in it which affect data layout in coded blocks.
+Also see https://wiki.yandex-team.ru/yt/userdoc/erasure/, https://wiki.yandex-team.ru/ignatijjkolesnichenko/yt/erasure/.
+
+It is possible to use codecs LRC 2k-2-2 and Reed Solomon n-k for any data stream. All you need is to provide CodecTraits (see `codecs_ut.cpp` for examples). Note that ISA-L only supports WordSize equal to 8. If you use Jerasure codecs with bigger WordSize than `MaxWordSize` in `public.h`, codec is not guaranteed to be thread-safe.
+
+You can use interface in `codec.h` or use the exact codec from `lrc_isa.h`, `lrc_jerasure.h` and `reed_solomon.h` if you like.
diff --git a/library/cpp/erasure/codec.cpp b/library/cpp/erasure/codec.cpp
new file mode 100644
index 0000000000..5fbcd720b2
--- /dev/null
+++ b/library/cpp/erasure/codec.cpp
@@ -0,0 +1 @@
+#include "codec.h"
diff --git a/library/cpp/erasure/codec.h b/library/cpp/erasure/codec.h
new file mode 100644
index 0000000000..c03d25e9c8
--- /dev/null
+++ b/library/cpp/erasure/codec.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "public.h"
+
+#include <optional>
+#include <vector>
+
+namespace NErasure {
+
+//! Describes a generic way to generate parity blocks from data blocks and
+//! to recover (repair) missing blocks.
+/*!
+ * Given N data blocks (numbered from 0 to N - 1) one can call #Encode to generate
+ * another M parity blocks (numbered from N to N + M - 1).
+ *
+ * If some of the resulting N + M blocks ever become missing one can attempt to
+ * repair the missing blocks by calling #Decode.
+ *
+ * Here N and M are fixed (codec-specific) parameters.
+ * Call #GetDataPartCount and #GetParityPartCount to figure out the
+ * the values for N and M, respectively.
+ *
+ */
+template <class TBlobType>
+struct ICodec {
+ //! Computes a sequence of parity blocks for given data blocks.
+ /*!
+ * The size of #blocks must be equal to #GetDataPartCount.
+ * The size of the returned array is equal to #GetParityPartCount.
+ */
+ virtual std::vector<TBlobType> Encode(const std::vector<TBlobType>& blocks) const = 0;
+
+ //! Decodes (repairs) missing blocks.
+ /*!
+ * #erasedIndices must contain the set of erased blocks indices.
+ * #blocks must contain known blocks (in the order specified by #GetRepairIndices).
+ * \returns The repaired blocks.
+ */
+ virtual std::vector<TBlobType> Decode(
+ const std::vector<TBlobType>& blocks,
+ const TPartIndexList& erasedIndices) const = 0;
+
+ //! Given a set of missing block indices, returns |true| if missing blocks can be repaired.
+ //! Due to performance reasons the elements of #erasedIndices must unique and sorted.
+ virtual bool CanRepair(const TPartIndexList& erasedIndices) const = 0;
+
+ //! Rapid version that works with set instead of list.
+ virtual bool CanRepair(const TPartIndexSet& erasedIndices) const = 0;
+
+ //! Given a set of missing block indices, checks if missing blocks can be repaired.
+ /*!
+ * \returns
+ * If repair is not possible, returns |std::nullopt|.
+ * Otherwise returns the indices of blocks (both data and parity) to be passed to #Decode
+ * (in this very order). Not all known blocks may be needed for repair.
+ */
+ virtual std::optional<TPartIndexList> GetRepairIndices(const TPartIndexList& erasedIndices) const = 0;
+
+ //! Returns the number of data blocks this codec can handle.
+ virtual int GetDataPartCount() const = 0;
+
+ //! Returns the number of parity blocks this codec can handle.
+ virtual int GetParityPartCount() const = 0;
+
+ //! Returns the maximum number of blocks that can always be repaired when missing.
+ virtual int GetGuaranteedRepairablePartCount() const = 0;
+
+ //! Every block passed to this codec must have size divisible by the result of #GetWordSize.
+ virtual int GetWordSize() const = 0;
+
+ // Extension methods
+
+ //! Returns the sum of #GetDataPartCount and #GetParityPartCount.
+ int GetTotalPartCount() const {
+ return GetDataPartCount() + GetParityPartCount();
+ }
+};
+
+} // namespace NErasure
+
diff --git a/library/cpp/erasure/helpers.cpp b/library/cpp/erasure/helpers.cpp
new file mode 100644
index 0000000000..74edeca52c
--- /dev/null
+++ b/library/cpp/erasure/helpers.cpp
@@ -0,0 +1,80 @@
+#include "helpers.h"
+
+#include <algorithm>
+#include <iterator>
+
+namespace NErasure {
+
+TPartIndexList MakeSegment(int begin, int end) {
+ TPartIndexList result(end - begin);
+ for (int i = begin; i < end; ++i) {
+ result[i - begin] = i;
+ }
+ return result;
+}
+
+TPartIndexList MakeSingleton(int elem) {
+ TPartIndexList result;
+ result.push_back(elem);
+ return result;
+}
+
+TPartIndexList Difference(int begin, int end, const TPartIndexList& subtrahend) {
+ size_t pos = 0;
+ TPartIndexList result;
+ for (int i = begin; i < end; ++i) {
+ while (pos < subtrahend.size() && subtrahend[pos] < i) {
+ pos += 1;
+ }
+ if (pos == subtrahend.size() || subtrahend[pos] != i) {
+ result.push_back(i);
+ }
+ }
+ return result;
+}
+
+TPartIndexList Difference(const TPartIndexList& first, const TPartIndexList& second) {
+ TPartIndexList result;
+ std::set_difference(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result));
+ return result;
+}
+
+TPartIndexList Difference(const TPartIndexList& set, int subtrahend) {
+ return Difference(set, MakeSingleton(subtrahend));
+}
+
+TPartIndexList Intersection(const TPartIndexList& first, const TPartIndexList& second) {
+ TPartIndexList result;
+ std::set_intersection(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result));
+ return result;
+}
+
+TPartIndexList Union(const TPartIndexList& first, const TPartIndexList& second) {
+ TPartIndexList result;
+ std::set_union(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result));
+ return result;
+}
+
+bool Contains(const TPartIndexList& set, int elem) {
+ return std::binary_search(set.begin(), set.end(), elem);
+}
+
+TPartIndexList UniqueSortedIndices(const TPartIndexList& indices) {
+ TPartIndexList copy = indices;
+ std::sort(copy.begin(), copy.end());
+ copy.erase(std::unique(copy.begin(), copy.end()), copy.end());
+ return copy;
+}
+
+TPartIndexList ExtractRows(const TPartIndexList& matrix, int width, const TPartIndexList& rows) {
+ Y_ASSERT(matrix.size() % width == 0);
+ TPartIndexList result(width * rows.size());
+ for (size_t i = 0; i < rows.size(); ++i) {
+ auto start = matrix.begin() + rows[i] * width;
+ std::copy(start, start + width, result.begin() + i * width);
+ }
+ return result;
+}
+
+} // namespace NErasure
+
diff --git a/library/cpp/erasure/helpers.h b/library/cpp/erasure/helpers.h
new file mode 100644
index 0000000000..741186322a
--- /dev/null
+++ b/library/cpp/erasure/helpers.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "public.h"
+
+namespace NErasure {
+
+// All vectors here are assumed to be sorted.
+
+TPartIndexList MakeSegment(int begin, int end);
+
+TPartIndexList MakeSingleton(int elem);
+
+TPartIndexList Difference(int begin, int end, const TPartIndexList& subtrahend);
+
+TPartIndexList Difference(const TPartIndexList& first, const TPartIndexList& second);
+
+TPartIndexList Difference(const TPartIndexList& first, int elem);
+
+TPartIndexList Intersection(const TPartIndexList& first, const TPartIndexList& second);
+
+TPartIndexList Union(const TPartIndexList& first, const TPartIndexList& second);
+
+bool Contains(const TPartIndexList& set, int elem);
+
+TPartIndexList UniqueSortedIndices(const TPartIndexList& indices);
+
+TPartIndexList ExtractRows(const TPartIndexList& matrix, int width, const TPartIndexList& rows);
+
+} // namespace NErasure
+
diff --git a/library/cpp/erasure/isa_erasure.cpp b/library/cpp/erasure/isa_erasure.cpp
new file mode 100644
index 0000000000..0b0199934e
--- /dev/null
+++ b/library/cpp/erasure/isa_erasure.cpp
@@ -0,0 +1 @@
+#include "isa_erasure.h"
diff --git a/library/cpp/erasure/isa_erasure.h b/library/cpp/erasure/isa_erasure.h
new file mode 100644
index 0000000000..a7df61307f
--- /dev/null
+++ b/library/cpp/erasure/isa_erasure.h
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "public.h"
+
+#include "helpers.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/generic/array_ref.h>
+#include <util/generic/ptr.h>
+#include <util/generic/singleton.h>
+
+#include <vector>
+
+extern "C" {
+ #include <contrib/libs/isa-l/include/erasure_code.h>
+}
+
+namespace NErasure {
+
+template <class TBlobType>
+static inline unsigned char* ConstCast(typename TBlobType::const_iterator blobIter) {
+ return const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(blobIter));
+}
+
+template <int DataPartCount, int ParityPartCount, class TCodecTraits, class TBlobType = typename TCodecTraits::TBlobType, class TMutableBlobType = typename TCodecTraits::TMutableBlobType>
+std::vector<TBlobType> ISAErasureEncode(
+ const std::vector<unsigned char>& encodeGFTables,
+ const std::vector<TBlobType>& dataBlocks)
+{
+ YT_VERIFY(dataBlocks.size() == DataPartCount);
+
+ size_t blockLength = dataBlocks.front().Size();
+ for (size_t i = 1; i < dataBlocks.size(); ++i) {
+ YT_VERIFY(dataBlocks[i].Size() == blockLength);
+ }
+
+ std::vector<unsigned char*> dataPointers;
+ for (const auto& block : dataBlocks) {
+ dataPointers.emplace_back(ConstCast<TBlobType>(block.Begin()));
+ }
+
+ std::vector<TMutableBlobType> parities(ParityPartCount);
+ std::vector<unsigned char*> parityPointers(ParityPartCount);
+ for (size_t i = 0; i < ParityPartCount; ++i) {
+ parities[i] = TCodecTraits::AllocateBlob(blockLength);
+ parityPointers[i] = ConstCast<TBlobType>(parities[i].Begin());
+ memset(parityPointers[i], 0, blockLength);
+ }
+
+ ec_encode_data(
+ blockLength,
+ DataPartCount,
+ ParityPartCount,
+ const_cast<unsigned char*>(encodeGFTables.data()),
+ dataPointers.data(),
+ parityPointers.data());
+
+ return std::vector<TBlobType>(parities.begin(), parities.end());
+}
+
+template <int DataPartCount, int ParityPartCount, class TCodecTraits, class TBlobType = typename TCodecTraits::TBlobType, class TMutableBlobType = typename TCodecTraits::TMutableBlobType>
+std::vector<TBlobType> ISAErasureDecode(
+ const std::vector<TBlobType>& dataBlocks,
+ const TPartIndexList& erasedIndices,
+ TConstArrayRef<TPartIndexList> groups,
+ const std::vector<unsigned char>& fullGeneratorMatrix)
+{
+ YT_VERIFY(dataBlocks.size() >= DataPartCount);
+ YT_VERIFY(erasedIndices.size() <= ParityPartCount);
+
+ size_t blockLength = dataBlocks.front().Size();
+ for (size_t i = 1; i < dataBlocks.size(); ++i) {
+ YT_VERIFY(dataBlocks[i].Size() == blockLength);
+ }
+
+ std::vector<unsigned char> partialGeneratorMatrix(DataPartCount * DataPartCount, 0);
+
+ std::vector<unsigned char*> recoveryBlocks;
+ for (size_t i = 0; i < DataPartCount; ++i) {
+ recoveryBlocks.emplace_back(ConstCast<TBlobType>(dataBlocks[i].Begin()));
+ }
+
+ // Groups check is specific for LRC.
+ std::vector<int> isGroupHealthy(2, 1);
+ for (size_t i = 0; i < 2; ++i) {
+ for (const auto& index : erasedIndices) {
+ if (!groups.empty() && Contains(groups[0], index)) {
+ isGroupHealthy[0] = 0;
+ } else if (!groups.empty() && Contains(groups[1], index)) {
+ isGroupHealthy[1] = 0;
+ }
+ }
+ }
+
+ // When a group is healthy we cannot use its local parity, thus skip it using gap.
+ size_t gap = 0;
+ size_t decodeMatrixIndex = 0;
+ size_t erasedBlockIndex = 0;
+ while (decodeMatrixIndex < DataPartCount) {
+ size_t globalIndex = decodeMatrixIndex + erasedBlockIndex + gap;
+
+ if (erasedBlockIndex < erasedIndices.size() &&
+ globalIndex == static_cast<size_t>(erasedIndices[erasedBlockIndex]))
+ {
+ ++erasedBlockIndex;
+ continue;
+ }
+
+ if (!groups.empty() && globalIndex >= DataPartCount && globalIndex < DataPartCount + 2) {
+ if (Contains(groups[0], globalIndex) && isGroupHealthy[0]) {
+ ++gap;
+ continue;
+ }
+ if (Contains(groups[1], globalIndex) && isGroupHealthy[1]) {
+ ++gap;
+ continue;
+ }
+ }
+
+ memcpy(&partialGeneratorMatrix[decodeMatrixIndex * DataPartCount], &fullGeneratorMatrix[globalIndex * DataPartCount], DataPartCount);
+ ++decodeMatrixIndex;
+ }
+
+ std::vector<unsigned char> invertedGeneratorMatrix(DataPartCount * DataPartCount, 0);
+ int res = gf_invert_matrix(partialGeneratorMatrix.data(), invertedGeneratorMatrix.data(), DataPartCount);
+ YT_VERIFY(res == 0);
+
+ std::vector<unsigned char> decodeMatrix(DataPartCount * (DataPartCount + ParityPartCount), 0);
+
+ //! Some magical code from library example.
+ for (size_t i = 0; i < erasedIndices.size(); ++i) {
+ if (erasedIndices[i] < DataPartCount) {
+ memcpy(&decodeMatrix[i * DataPartCount], &invertedGeneratorMatrix[erasedIndices[i] * DataPartCount], DataPartCount);
+ } else {
+ for (int k = 0; k < DataPartCount; ++k) {
+ int val = 0;
+ for (int j = 0; j < DataPartCount; ++j) {
+ val ^= gf_mul_erasure(invertedGeneratorMatrix[j * DataPartCount + k], fullGeneratorMatrix[DataPartCount * erasedIndices[i] + j]);
+ }
+
+ decodeMatrix[DataPartCount * i + k] = val;
+ }
+ }
+ }
+
+ std::vector<unsigned char> decodeGFTables(DataPartCount * erasedIndices.size() * 32);
+ ec_init_tables(DataPartCount, erasedIndices.size(), decodeMatrix.data(), decodeGFTables.data());
+
+ std::vector<TMutableBlobType> recoveredParts;
+ std::vector<unsigned char*> recoveredPartsPointers;
+ for (size_t i = 0; i < erasedIndices.size(); ++i) {
+ recoveredParts.emplace_back(TCodecTraits::AllocateBlob(blockLength));
+ recoveredPartsPointers.emplace_back(ConstCast<TBlobType>(recoveredParts.back().Begin()));
+ memset(recoveredPartsPointers.back(), 0, blockLength);
+ }
+
+ ec_encode_data(
+ blockLength,
+ DataPartCount,
+ erasedIndices.size(),
+ decodeGFTables.data(),
+ recoveryBlocks.data(),
+ recoveredPartsPointers.data());
+
+ return std::vector<TBlobType>(recoveredParts.begin(), recoveredParts.end());
+}
+
+} // namespace NErasure
+
diff --git a/library/cpp/erasure/lrc.cpp b/library/cpp/erasure/lrc.cpp
new file mode 100644
index 0000000000..8c6a347091
--- /dev/null
+++ b/library/cpp/erasure/lrc.cpp
@@ -0,0 +1 @@
+#include "lrc.h"
diff --git a/library/cpp/erasure/lrc.h b/library/cpp/erasure/lrc.h
new file mode 100644
index 0000000000..15185a47f4
--- /dev/null
+++ b/library/cpp/erasure/lrc.h
@@ -0,0 +1,326 @@
+#pragma once
+
+#include "helpers.h"
+
+#include <library/cpp/sse/sse.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/generic/array_ref.h>
+
+#include <algorithm>
+#include <optional>
+
+namespace NErasure {
+
+template <class TCodecTraits, class TBlobType = typename TCodecTraits::TBlobType>
+static inline TBlobType Xor(const std::vector<TBlobType>& refs) {
+ using TBufferType = typename TCodecTraits::TBufferType;
+ size_t size = refs.front().Size();
+ TBufferType result = TCodecTraits::AllocateBuffer(size); // this also fills the buffer with zeros
+ for (const TBlobType& ref : refs) {
+ const char* data = reinterpret_cast<const char*>(ref.Begin());
+ size_t pos = 0;
+#ifdef ARCADIA_SSE
+ for (; pos + sizeof(__m128i) <= size; pos += sizeof(__m128i)) {
+ __m128i* dst = reinterpret_cast<__m128i*>(result.Begin() + pos);
+ const __m128i* src = reinterpret_cast<const __m128i*>(data + pos);
+ _mm_storeu_si128(dst, _mm_xor_si128(_mm_loadu_si128(src), _mm_loadu_si128(dst)));
+ }
+#endif
+ for (; pos < size; ++pos) {
+ *(result.Begin() + pos) ^= data[pos];
+ }
+ }
+ return TCodecTraits::FromBufferToBlob(std::move(result));
+}
+
+//! Locally Reconstructable Codes
+/*!
+ * See https://www.usenix.org/conference/usenixfederatedconferencesweek/erasure-coding-windows-azure-storage
+ * for more details.
+ */
+template <int DataPartCount, int ParityPartCount, int WordSize, class TCodecTraits>
+class TLrcCodecBase
+ : public ICodec<typename TCodecTraits::TBlobType>
+{
+ static_assert(DataPartCount % 2 == 0, "Data part count must be even.");
+ static_assert(ParityPartCount == 4, "Now we only support n-2-2 scheme for LRC codec");
+ static_assert(1 + DataPartCount / 2 < (1 << (WordSize / 2)), "Data part count should be enough small to construct proper matrix.");
+public:
+ //! Main blob for storing data.
+ using TBlobType = typename TCodecTraits::TBlobType;
+ //! Main mutable blob for decoding data.
+ using TMutableBlobType = typename TCodecTraits::TMutableBlobType;
+
+ static constexpr ui64 RequiredDataAlignment = alignof(ui64);
+
+ TLrcCodecBase() {
+ Groups_[0] = MakeSegment(0, DataPartCount / 2);
+ // Xor.
+ Groups_[0].push_back(DataPartCount);
+
+ Groups_[1] = MakeSegment(DataPartCount / 2, DataPartCount);
+ // Xor.
+ Groups_[1].push_back(DataPartCount + 1);
+
+ constexpr int totalPartCount = DataPartCount + ParityPartCount;
+ if constexpr (totalPartCount <= BitmaskOptimizationThreshold) {
+ CanRepair_.resize(1 << totalPartCount);
+ for (int mask = 0; mask < (1 << totalPartCount); ++mask) {
+ TPartIndexList erasedIndices;
+ for (size_t i = 0; i < totalPartCount; ++i) {
+ if ((mask & (1 << i)) == 0) {
+ erasedIndices.push_back(i);
+ }
+ }
+ CanRepair_[mask] = CalculateCanRepair(erasedIndices);
+ }
+ }
+ }
+
+ /*! Note that if you want to restore any internal data, blocks offsets must by WordSize * sizeof(long) aligned.
+ * Though it is possible to restore unaligned data if no more than one index in each Group is failed. See unittests for this case.
+ */
+ std::vector<TBlobType> Decode(
+ const std::vector<TBlobType>& blocks,
+ const TPartIndexList& erasedIndices) const override
+ {
+ if (erasedIndices.empty()) {
+ return std::vector<TBlobType>();
+ }
+
+ size_t blockLength = blocks.front().Size();
+ for (size_t i = 1; i < blocks.size(); ++i) {
+ YT_VERIFY(blocks[i].Size() == blockLength);
+ }
+
+ TPartIndexList indices = UniqueSortedIndices(erasedIndices);
+
+ // We can restore one block by xor.
+ if (indices.size() == 1) {
+ int index = erasedIndices.front();
+ for (size_t i = 0; i < 2; ++i) {
+ if (Contains(Groups_[i], index)) {
+ return std::vector<TBlobType>(1, Xor<TCodecTraits>(blocks));
+ }
+ }
+ }
+
+ TPartIndexList recoveryIndices = GetRepairIndices(indices).value();
+ // We can restore two blocks from different groups using xor.
+ if (indices.size() == 2 &&
+ indices.back() < DataPartCount + 2 &&
+ recoveryIndices.back() < DataPartCount + 2)
+ {
+ std::vector<TBlobType> result;
+ for (int index : indices) {
+ for (size_t groupIndex = 0; groupIndex < 2; ++groupIndex) {
+ if (!Contains(Groups_[groupIndex], index)) {
+ continue;
+ }
+
+ std::vector<TBlobType> correspondingBlocks;
+ for (int pos : Groups_[groupIndex]) {
+ for (size_t i = 0; i < blocks.size(); ++i) {
+ if (recoveryIndices[i] != pos) {
+ continue;
+ }
+ correspondingBlocks.push_back(blocks[i]);
+ }
+ }
+
+ result.push_back(Xor<TCodecTraits>(correspondingBlocks));
+ }
+ }
+ return result;
+ }
+
+ return FallbackToCodecDecode(blocks, std::move(indices));
+ }
+
+ bool CanRepair(const TPartIndexList& erasedIndices) const final {
+ constexpr int totalPartCount = DataPartCount + ParityPartCount;
+ if constexpr (totalPartCount <= BitmaskOptimizationThreshold) {
+ int mask = (1 << (totalPartCount)) - 1;
+ for (int index : erasedIndices) {
+ mask -= (1 << index);
+ }
+ return CanRepair_[mask];
+ } else {
+ return CalculateCanRepair(erasedIndices);
+ }
+ }
+
+ bool CanRepair(const TPartIndexSet& erasedIndicesMask) const final {
+ constexpr int totalPartCount = DataPartCount + ParityPartCount;
+ if constexpr (totalPartCount <= BitmaskOptimizationThreshold) {
+ TPartIndexSet mask = erasedIndicesMask;
+ return CanRepair_[mask.flip().to_ulong()];
+ } else {
+ TPartIndexList erasedIndices;
+ for (size_t i = 0; i < erasedIndicesMask.size(); ++i) {
+ if (erasedIndicesMask[i]) {
+ erasedIndices.push_back(i);
+ }
+ }
+ return CalculateCanRepair(erasedIndices);
+ }
+ }
+
+ std::optional<TPartIndexList> GetRepairIndices(const TPartIndexList& erasedIndices) const final {
+ if (erasedIndices.empty()) {
+ return TPartIndexList();
+ }
+
+ TPartIndexList indices = UniqueSortedIndices(erasedIndices);
+
+ if (indices.size() > ParityPartCount) {
+ return std::nullopt;
+ }
+
+ // One erasure from data or xor blocks.
+ if (indices.size() == 1) {
+ int index = indices.front();
+ for (size_t i = 0; i < 2; ++i) {
+ if (Contains(Groups_[i], index)) {
+ return Difference(Groups_[i], index);
+ }
+ }
+ }
+
+ // Null if we have 4 erasures in one group.
+ if (indices.size() == ParityPartCount) {
+ bool intersectsAny = true;
+ for (size_t i = 0; i < 2; ++i) {
+ if (Intersection(indices, Groups_[i]).empty()) {
+ intersectsAny = false;
+ }
+ }
+ if (!intersectsAny) {
+ return std::nullopt;
+ }
+ }
+
+ // Calculate coverage of each group.
+ int groupCoverage[2] = {};
+ for (int index : indices) {
+ for (size_t i = 0; i < 2; ++i) {
+ if (Contains(Groups_[i], index)) {
+ ++groupCoverage[i];
+ }
+ }
+ }
+
+ // Two erasures, one in each group.
+ if (indices.size() == 2 && groupCoverage[0] == 1 && groupCoverage[1] == 1) {
+ return Difference(Union(Groups_[0], Groups_[1]), indices);
+ }
+
+ // Erasures in only parity blocks.
+ if (indices.front() >= DataPartCount) {
+ return MakeSegment(0, DataPartCount);
+ }
+
+ // Remove unnecessary xor parities.
+ TPartIndexList result = Difference(0, DataPartCount + ParityPartCount, indices);
+ for (size_t i = 0; i < 2; ++i) {
+ if (groupCoverage[i] == 0 && indices.size() <= 3) {
+ result = Difference(result, DataPartCount + i);
+ }
+ }
+ return result;
+ }
+
+ int GetDataPartCount() const override {
+ return DataPartCount;
+ }
+
+ int GetParityPartCount() const override {
+ return ParityPartCount;
+ }
+
+ int GetGuaranteedRepairablePartCount() const override {
+ return ParityPartCount - 1;
+ }
+
+ int GetWordSize() const override {
+ return WordSize * sizeof(long);
+ }
+
+ virtual ~TLrcCodecBase() = default;
+
+protected:
+ // Indices of data blocks and corresponding xor (we have two xor parities).
+ TConstArrayRef<TPartIndexList> GetXorGroups() const {
+ return Groups_;
+ }
+
+ virtual std::vector<TBlobType> FallbackToCodecDecode(
+ const std::vector<TBlobType>& /* blocks */,
+ TPartIndexList /* erasedIndices */) const = 0;
+
+ template <typename T>
+ void InitializeGeneratorMatrix(T* generatorMatrix, const std::function<T(T)>& GFSquare) {
+ for (int row = 0; row < ParityPartCount; ++row) {
+ for (int column = 0; column < DataPartCount; ++column) {
+ int index = row * DataPartCount + column;
+
+ bool isFirstHalf = column < DataPartCount / 2;
+ if (row == 0) generatorMatrix[index] = isFirstHalf ? 1 : 0;
+ if (row == 1) generatorMatrix[index] = isFirstHalf ? 0 : 1;
+
+ // Let alpha_i be coefficient of first half and beta_i of the second half.
+ // Then matrix is non-singular iff:
+ // a) alpha_i, beta_j != 0
+ // b) alpha_i != beta_j
+ // c) alpha_i + alpha_k != beta_j + beta_l
+ // for any i, j, k, l.
+ if (row == 2) {
+ int shift = isFirstHalf ? 1 : (1 << (WordSize / 2));
+ int relativeColumn = isFirstHalf ? column : (column - (DataPartCount / 2));
+ generatorMatrix[index] = shift * (1 + relativeColumn);
+ }
+
+ // The last row is the square of the row before last.
+ if (row == 3) {
+ auto prev = generatorMatrix[index - DataPartCount];
+ generatorMatrix[index] = GFSquare(prev);
+ }
+ }
+ }
+ }
+
+private:
+ bool CalculateCanRepair(const TPartIndexList& erasedIndices) const {
+ TPartIndexList indices = UniqueSortedIndices(erasedIndices);
+ if (indices.size() > ParityPartCount) {
+ return false;
+ }
+
+ if (indices.size() == 1) {
+ int index = indices.front();
+ for (size_t i = 0; i < 2; ++i) {
+ if (Contains(Groups_[i], index)) {
+ return true;
+ }
+ }
+ }
+
+ // If 4 indices miss in one block we cannot recover.
+ if (indices.size() == ParityPartCount) {
+ for (size_t i = 0; i < 2; ++i) {
+ if (Intersection(indices, Groups_[i]).empty()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ TPartIndexList Groups_[2];
+ std::vector<bool> CanRepair_;
+};
+
+} // namespace NErasure
diff --git a/library/cpp/erasure/lrc_isa.cpp b/library/cpp/erasure/lrc_isa.cpp
new file mode 100644
index 0000000000..2068f840c8
--- /dev/null
+++ b/library/cpp/erasure/lrc_isa.cpp
@@ -0,0 +1 @@
+#include "lrc_isa.h"
diff --git a/library/cpp/erasure/lrc_isa.h b/library/cpp/erasure/lrc_isa.h
new file mode 100644
index 0000000000..800dc3c5ca
--- /dev/null
+++ b/library/cpp/erasure/lrc_isa.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "lrc.h"
+#include "helpers.h"
+
+#include "isa_erasure.h"
+
+extern "C" {
+ #include <contrib/libs/isa-l/include/erasure_code.h>
+}
+
+#include <library/cpp/sse/sse.h>
+
+#include <util/generic/array_ref.h>
+
+#include <optional>
+#include <vector>
+
+namespace NErasure {
+
+template <int DataPartCount, int ParityPartCount, int WordSize, class TCodecTraits>
+class TLrcIsa
+ : public TLrcCodecBase<DataPartCount, ParityPartCount, WordSize, TCodecTraits>
+{
+ static_assert(WordSize == 8, "ISA-l erasure codes support computations only in GF(2^8)");
+public:
+ //! Main blob for storing data.
+ using TBlobType = typename TCodecTraits::TBlobType;
+ //! Main mutable blob for decoding data.
+ using TMutableBlobType = typename TCodecTraits::TMutableBlobType;
+
+ static constexpr ui64 RequiredDataAlignment = alignof(ui64);
+
+ TLrcIsa()
+ : TLrcCodecBase<DataPartCount, ParityPartCount, WordSize, TCodecTraits>()
+ {
+ EncodeGFTables_.resize(DataPartCount * ParityPartCount * 32, 0);
+ GeneratorMatrix_.resize((DataPartCount + ParityPartCount) * DataPartCount, 0);
+
+ for (int row = 0; row < DataPartCount; ++row) {
+ GeneratorMatrix_[row * DataPartCount + row] = 1;
+ }
+ this->template InitializeGeneratorMatrix<typename decltype(GeneratorMatrix_)::value_type>(
+ &GeneratorMatrix_[DataPartCount * DataPartCount],
+ std::bind(&gf_mul_erasure, std::placeholders::_1, std::placeholders::_1));
+
+ ec_init_tables(
+ DataPartCount,
+ ParityPartCount,
+ &GeneratorMatrix_.data()[DataPartCount * DataPartCount],
+ EncodeGFTables_.data());
+ }
+
+ std::vector<TBlobType> Encode(const std::vector<TBlobType>& blocks) const override {
+ return ISAErasureEncode<DataPartCount, ParityPartCount, TCodecTraits, TBlobType, TMutableBlobType>(EncodeGFTables_, blocks);
+ }
+
+ virtual ~TLrcIsa() = default;
+
+private:
+ std::vector<TBlobType> FallbackToCodecDecode(
+ const std::vector<TBlobType>& blocks,
+ TPartIndexList erasedIndices) const override
+ {
+ return ISAErasureDecode<DataPartCount, ParityPartCount, TCodecTraits, TBlobType, TMutableBlobType>(
+ blocks,
+ std::move(erasedIndices),
+ this->GetXorGroups(),
+ GeneratorMatrix_);
+ }
+
+ std::vector<unsigned char> GeneratorMatrix_;
+ std::vector<unsigned char> EncodeGFTables_;
+};
+
+} // NErasure
+
diff --git a/library/cpp/erasure/public.cpp b/library/cpp/erasure/public.cpp
new file mode 100644
index 0000000000..4df9bcaa18
--- /dev/null
+++ b/library/cpp/erasure/public.cpp
@@ -0,0 +1 @@
+#include "public.h"
diff --git a/library/cpp/erasure/public.h b/library/cpp/erasure/public.h
new file mode 100644
index 0000000000..d5cf01297b
--- /dev/null
+++ b/library/cpp/erasure/public.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <util/generic/buffer.h>
+#include <util/generic/yexception.h>
+#include <util/memory/blob.h>
+#include <util/string/cast.h>
+#include <util/system/src_root.h>
+
+#include <vector>
+
+#include <bitset>
+
+namespace NErasure {
+
+//! The maximum total number of blocks our erasure codec can handle.
+static constexpr int MaxTotalPartCount = 16;
+
+//! Max word size to use. `w` in jerasure notation
+static constexpr int MaxWordSize = 8;
+
+//! Max threshold to generate bitmask for CanRepair indices for LRC encoding.
+static constexpr int BitmaskOptimizationThreshold = 22;
+
+//! A vector type for holding block indexes.
+using TPartIndexList = std::vector<int>;
+
+//! Each bit corresponds to a possible block index.
+using TPartIndexSet = std::bitset<MaxTotalPartCount>;
+
+template <class TBlobType>
+struct ICodec;
+
+struct TDefaultCodecTraits {
+ using TBlobType = TBlob;
+ using TMutableBlobType = TBlob;
+ using TBufferType = TBuffer;
+
+ static inline TMutableBlobType AllocateBlob(size_t size) {
+ TBufferType buffer(size);
+ buffer.Resize(size);
+ // The buffer is cleared after this call so no use after free.
+ return TBlob::FromBuffer(buffer);
+ }
+
+ // AllocateBuffer must fill the memory with 0.
+ static inline TBufferType AllocateBuffer(size_t size) {
+ TBufferType buffer(size);
+ buffer.Fill(0, size);
+ return buffer;
+ }
+
+ static inline TBlobType FromBufferToBlob(TBufferType&& blob) {
+ return TBlobType::FromBuffer(blob);
+ }
+};
+
+} // namespace NErasure
diff --git a/library/cpp/erasure/reed_solomon.cpp b/library/cpp/erasure/reed_solomon.cpp
new file mode 100644
index 0000000000..68aa2acbfb
--- /dev/null
+++ b/library/cpp/erasure/reed_solomon.cpp
@@ -0,0 +1 @@
+#include "reed_solomon.h"
diff --git a/library/cpp/erasure/reed_solomon.h b/library/cpp/erasure/reed_solomon.h
new file mode 100644
index 0000000000..a2e268e81d
--- /dev/null
+++ b/library/cpp/erasure/reed_solomon.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "helpers.h"
+
+#include <algorithm>
+#include <optional>
+
+namespace NErasure {
+
+template <int DataPartCount, int ParityPartCount, int WordSize, class TCodecTraits>
+class TReedSolomonBase
+ : public ICodec<typename TCodecTraits::TBlobType>
+{
+public:
+ static constexpr ui64 RequiredDataAlignment = alignof(ui64);
+
+ bool CanRepair(const TPartIndexList& erasedIndices) const final {
+ return erasedIndices.size() <= ParityPartCount;
+ }
+
+ bool CanRepair(const TPartIndexSet& erasedIndices) const final {
+ return erasedIndices.count() <= static_cast<size_t>(ParityPartCount);
+ }
+
+ std::optional<TPartIndexList> GetRepairIndices(const TPartIndexList& erasedIndices) const final {
+ if (erasedIndices.empty()) {
+ return TPartIndexList();
+ }
+
+ TPartIndexList indices = erasedIndices;
+ std::sort(indices.begin(), indices.end());
+ indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
+
+ if (indices.size() > static_cast<size_t>(ParityPartCount)) {
+ return std::nullopt;
+ }
+
+ return Difference(0, DataPartCount + ParityPartCount, indices);
+ }
+
+ int GetDataPartCount() const final {
+ return DataPartCount;
+ }
+
+ int GetParityPartCount() const final {
+ return ParityPartCount;
+ }
+
+ int GetGuaranteedRepairablePartCount() const final {
+ return ParityPartCount;
+ }
+
+ int GetWordSize() const final {
+ return WordSize * sizeof(long);
+ }
+
+ virtual ~TReedSolomonBase() = default;
+};
+
+} // namespace NErasure
diff --git a/library/cpp/erasure/reed_solomon_isa.cpp b/library/cpp/erasure/reed_solomon_isa.cpp
new file mode 100644
index 0000000000..eaffde54b5
--- /dev/null
+++ b/library/cpp/erasure/reed_solomon_isa.cpp
@@ -0,0 +1 @@
+#include "reed_solomon_isa.h"
diff --git a/library/cpp/erasure/reed_solomon_isa.h b/library/cpp/erasure/reed_solomon_isa.h
new file mode 100644
index 0000000000..73a3cd630f
--- /dev/null
+++ b/library/cpp/erasure/reed_solomon_isa.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "isa_erasure.h"
+#include "reed_solomon.h"
+
+extern "C" {
+ #include <contrib/libs/isa-l/include/erasure_code.h>
+}
+
+#include <util/generic/array_ref.h>
+
+#include <array>
+
+namespace NErasure {
+
+template <int DataPartCount, int ParityPartCount, int WordSize, class TCodecTraits>
+class TReedSolomonIsa
+ : public TReedSolomonBase<DataPartCount, ParityPartCount, WordSize, TCodecTraits>
+{
+ static_assert(WordSize == 8, "ISA-l erasure codes support computations only in GF(2^8)");
+public:
+ //! Main blob for storing data.
+ using TBlobType = typename TCodecTraits::TBlobType;
+ //! Main mutable blob for decoding data.
+ using TMutableBlobType = typename TCodecTraits::TMutableBlobType;
+
+ TReedSolomonIsa() {
+ EncodeGFTables_.resize(DataPartCount * ParityPartCount * 32, 0);
+ GeneratorMatrix_.resize((DataPartCount + ParityPartCount) * DataPartCount, 0);
+
+ gf_gen_rs_matrix(
+ GeneratorMatrix_.data(),
+ DataPartCount + ParityPartCount,
+ DataPartCount);
+
+ ec_init_tables(
+ DataPartCount,
+ ParityPartCount,
+ &GeneratorMatrix_.data()[DataPartCount * DataPartCount],
+ EncodeGFTables_.data());
+ }
+
+ virtual std::vector<TBlobType> Encode(const std::vector<TBlobType>& blocks) const override {
+ return ISAErasureEncode<DataPartCount, ParityPartCount, TCodecTraits, TBlobType, TMutableBlobType>(EncodeGFTables_, blocks);
+ }
+
+ virtual std::vector<TBlobType> Decode(
+ const std::vector<TBlobType>& blocks,
+ const TPartIndexList& erasedIndices) const override
+ {
+ if (erasedIndices.empty()) {
+ return std::vector<TBlobType>();
+ }
+
+ return ISAErasureDecode<DataPartCount, ParityPartCount, TCodecTraits, TBlobType, TMutableBlobType>(
+ blocks,
+ erasedIndices,
+ TConstArrayRef<TPartIndexList>(),
+ GeneratorMatrix_);
+ }
+
+ virtual ~TReedSolomonIsa() = default;
+
+private:
+ std::vector<unsigned char> GeneratorMatrix_;
+ std::vector<unsigned char> EncodeGFTables_;
+};
+
+} // namespace NErasure
diff --git a/library/cpp/erasure/ya.make b/library/cpp/erasure/ya.make
new file mode 100644
index 0000000000..bde816b4d8
--- /dev/null
+++ b/library/cpp/erasure/ya.make
@@ -0,0 +1,35 @@
+LIBRARY()
+
+SRCS(
+ public.cpp
+ codec.cpp
+ helpers.cpp
+
+ isa_erasure.cpp
+
+ reed_solomon.cpp
+ reed_solomon_isa.cpp
+
+ lrc.cpp
+ lrc_isa.cpp
+)
+
+PEERDIR(
+ contrib/libs/isa-l/erasure_code
+ library/cpp/sse
+ library/cpp/yt/assert
+)
+
+IF (NOT OPENSOURCE)
+ SRCS(
+ jerasure.cpp
+ reed_solomon_jerasure.cpp
+ lrc_jerasure.cpp
+ )
+
+ PEERDIR(contrib/libs/jerasure)
+ENDIF()
+
+GENERATE_ENUM_SERIALIZATION(public.h)
+
+END()
diff --git a/library/cpp/skiff/public.h b/library/cpp/skiff/public.h
new file mode 100644
index 0000000000..d67c6f26ee
--- /dev/null
+++ b/library/cpp/skiff/public.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <vector>
+#include <memory>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class EWireType
+{
+ Nothing /* "nothing" */,
+ Int8 /* "int8" */,
+ Int16 /* "int16" */,
+ Int32 /* "int32" */,
+ Int64 /* "int64" */,
+ Int128 /* "int128" */,
+ Uint8 /* "uint8" */,
+ Uint16 /* "uint16" */,
+ Uint32 /* "uint32" */,
+ Uint64 /* "uint64" */,
+ Uint128 /* "uint128" */,
+ Double /* "double" */,
+ Boolean /* "boolean" */,
+ String32 /* "string32" */,
+ Yson32 /* "yson32" */,
+
+ Tuple /* "tuple" */,
+ Variant8 /* "variant8" */,
+ Variant16 /* "variant16" */,
+ RepeatedVariant8 /* "repeated_variant8" */,
+ RepeatedVariant16 /* "repeated_variant16" */,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffSchema;
+using TSkiffSchemaPtr = std::shared_ptr<TSkiffSchema>;
+
+using TSkiffSchemaList = std::vector<TSkiffSchemaPtr>;
+
+class TSimpleTypeSchema;
+using TSimpleTypeSchemaPtr = std::shared_ptr<TSimpleTypeSchema>;
+
+class TSkiffValidator;
+
+class TUncheckedSkiffParser;
+class TCheckedSkiffParser;
+
+class TUncheckedSkiffWriter;
+class TCheckedSkiffWriter;
+
+#ifdef DEBUG
+using TCheckedInDebugSkiffParser = TCheckedSkiffParser;
+using TCheckedInDebugSkiffWriter = TCheckedSkiffWriter;
+#else
+using TCheckedInDebugSkiffParser = TUncheckedSkiffParser;
+using TCheckedInDebugSkiffWriter = TUncheckedSkiffWriter;
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/skiff-inl.h b/library/cpp/skiff/skiff-inl.h
new file mode 100644
index 0000000000..a3f68a9374
--- /dev/null
+++ b/library/cpp/skiff/skiff-inl.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#ifndef SKIFF_H
+#error "Direct inclusion of this file is not allowed, include skiff.h"
+// For the sake of sane code completion.
+#include "skiff.h"
+#endif
+#undef SKIFF_H
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EWireType wireType>
+constexpr auto TUnderlyingIntegerType<wireType>::F() {
+ if constexpr (wireType == EWireType::Int8) {
+ return i8{};
+ } else if constexpr (wireType == EWireType::Int16) {
+ return i16{};
+ } else if constexpr (wireType == EWireType::Int32) {
+ return i32{};
+ } else if constexpr (wireType == EWireType::Int64) {
+ return i64{};
+ } else if constexpr (wireType == EWireType::Uint8) {
+ return ui8{};
+ } else if constexpr (wireType == EWireType::Uint16) {
+ return ui16{};
+ } else if constexpr (wireType == EWireType::Uint32) {
+ return ui32{};
+ } else if constexpr (wireType == EWireType::Uint64) {
+ return ui64{};
+ } else {
+ static_assert(wireType == EWireType::Int8, "expected integer wire type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/skiff.cpp b/library/cpp/skiff/skiff.cpp
new file mode 100644
index 0000000000..cbdbdfe364
--- /dev/null
+++ b/library/cpp/skiff/skiff.cpp
@@ -0,0 +1,591 @@
+#include "skiff.h"
+
+#include "skiff_validator.h"
+
+#include <util/stream/buffered.h>
+#include <util/system/byteorder.h>
+#include <util/system/unaligned_mem.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(TInt128 lhs, TInt128 rhs)
+{
+ return lhs.Low == rhs.Low && lhs.High == rhs.High;
+}
+
+bool operator!=(TInt128 lhs, TInt128 rhs)
+{
+ return !(lhs == rhs);
+}
+
+bool operator==(TUint128 lhs, TUint128 rhs)
+{
+ return lhs.Low == rhs.Low && lhs.High == rhs.High;
+}
+
+bool operator!=(TUint128 lhs, TUint128 rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUncheckedSkiffParser::TUncheckedSkiffParser(IZeroCopyInput* underlying)
+ : Underlying_(underlying)
+ , Buffer_(512 * 1024)
+{ }
+
+TUncheckedSkiffParser::TUncheckedSkiffParser(const std::shared_ptr<TSkiffSchema>& /*schema*/, IZeroCopyInput* underlying)
+ : TUncheckedSkiffParser(underlying)
+{ }
+
+i8 TUncheckedSkiffParser::ParseInt8()
+{
+ return ParseSimple<i8>();
+}
+
+i16 TUncheckedSkiffParser::ParseInt16()
+{
+ return ParseSimple<i16>();
+}
+
+i32 TUncheckedSkiffParser::ParseInt32()
+{
+ return ParseSimple<i32>();
+}
+
+i64 TUncheckedSkiffParser::ParseInt64()
+{
+ return ParseSimple<i64>();
+}
+
+ui8 TUncheckedSkiffParser::ParseUint8()
+{
+ return ParseSimple<ui8>();
+}
+
+ui16 TUncheckedSkiffParser::ParseUint16()
+{
+ return ParseSimple<ui16>();
+}
+
+ui32 TUncheckedSkiffParser::ParseUint32()
+{
+ return ParseSimple<ui32>();
+}
+
+ui64 TUncheckedSkiffParser::ParseUint64()
+{
+ return ParseSimple<ui64>();
+}
+
+TInt128 TUncheckedSkiffParser::ParseInt128()
+{
+ auto low = ParseSimple<ui64>();
+ auto high = ParseSimple<i64>();
+ return {low, high};
+}
+
+TUint128 TUncheckedSkiffParser::ParseUint128()
+{
+ auto low = ParseSimple<ui64>();
+ auto high = ParseSimple<ui64>();
+ return {low, high};
+}
+
+double TUncheckedSkiffParser::ParseDouble()
+{
+ return ParseSimple<double>();
+}
+
+bool TUncheckedSkiffParser::ParseBoolean()
+{
+ ui8 result = ParseSimple<ui8>();
+ if (result > 1) {
+ ythrow TSkiffException() << "Invalid boolean value \"" << result << "\"";
+ }
+ return result;
+}
+
+TStringBuf TUncheckedSkiffParser::ParseString32()
+{
+ ui32 len = ParseSimple<ui32>();
+ const void* data = GetData(len);
+ return TStringBuf(static_cast<const char*>(data), len);
+}
+
+TStringBuf TUncheckedSkiffParser::ParseYson32()
+{
+ return ParseString32();
+}
+
+ui8 TUncheckedSkiffParser::ParseVariant8Tag()
+{
+ return ParseSimple<ui8>();
+}
+
+ui16 TUncheckedSkiffParser::ParseVariant16Tag()
+{
+ return ParseSimple<ui16>();
+}
+
+template <typename T>
+T TUncheckedSkiffParser::ParseSimple()
+{
+ return ReadUnaligned<T>(GetData(sizeof(T)));
+}
+
+const void* TUncheckedSkiffParser::GetData(size_t size)
+{
+ if (RemainingBytes() >= size) {
+ const void* result = Position_;
+ Advance(size);
+ return result;
+ }
+
+ return GetDataViaBuffer(size);
+}
+
+const void* TUncheckedSkiffParser::GetDataViaBuffer(size_t size)
+{
+ Buffer_.Clear();
+ Buffer_.Reserve(size);
+ while (Buffer_.Size() < size) {
+ size_t toCopy = Min(size - Buffer_.Size(), RemainingBytes());
+ Buffer_.Append(Position_, toCopy);
+ Advance(toCopy);
+
+ if (RemainingBytes() == 0) {
+ RefillBuffer();
+ if (Exhausted_ && Buffer_.Size() < size) {
+ ythrow TSkiffException() << "Premature end of stream while parsing Skiff";
+ }
+ }
+ }
+ return Buffer_.Data();
+}
+
+size_t TUncheckedSkiffParser::RemainingBytes() const
+{
+ Y_ASSERT(End_ >= Position_);
+ return End_ - Position_;
+}
+
+void TUncheckedSkiffParser::Advance(size_t size)
+{
+ Y_ASSERT(size <= RemainingBytes());
+ Position_ += size;
+ ReadBytesCount_ += size;
+}
+
+void TUncheckedSkiffParser::RefillBuffer()
+{
+ size_t bufferSize = Underlying_->Next(&Position_);
+ End_ = Position_ + bufferSize;
+ if (bufferSize == 0) {
+ Exhausted_ = true;
+ }
+}
+
+bool TUncheckedSkiffParser::HasMoreData()
+{
+ if (RemainingBytes() == 0 && !Exhausted_) {
+ RefillBuffer();
+ }
+ return !(RemainingBytes() == 0 && Exhausted_);
+}
+
+void TUncheckedSkiffParser::ValidateFinished()
+{ }
+
+ui64 TUncheckedSkiffParser::GetReadBytesCount() const
+{
+ return ReadBytesCount_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCheckedSkiffParser::TCheckedSkiffParser(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyInput* stream)
+ : Parser_(stream)
+ , Validator_(std::make_unique<TSkiffValidator>(schema))
+{ }
+
+TCheckedSkiffParser::~TCheckedSkiffParser() = default;
+
+i8 TCheckedSkiffParser::ParseInt8()
+{
+ Validator_->OnSimpleType(EWireType::Int8);
+ return Parser_.ParseInt8();
+}
+
+i16 TCheckedSkiffParser::ParseInt16()
+{
+ Validator_->OnSimpleType(EWireType::Int16);
+ return Parser_.ParseInt16();
+}
+
+i32 TCheckedSkiffParser::ParseInt32()
+{
+ Validator_->OnSimpleType(EWireType::Int32);
+ return Parser_.ParseInt32();
+}
+
+i64 TCheckedSkiffParser::ParseInt64()
+{
+ Validator_->OnSimpleType(EWireType::Int64);
+ return Parser_.ParseInt64();
+}
+
+ui8 TCheckedSkiffParser::ParseUint8()
+{
+ Validator_->OnSimpleType(EWireType::Uint8);
+ return Parser_.ParseUint8();
+}
+
+ui16 TCheckedSkiffParser::ParseUint16()
+{
+ Validator_->OnSimpleType(EWireType::Uint16);
+ return Parser_.ParseUint16();
+}
+
+ui32 TCheckedSkiffParser::ParseUint32()
+{
+ Validator_->OnSimpleType(EWireType::Uint32);
+ return Parser_.ParseUint32();
+}
+
+ui64 TCheckedSkiffParser::ParseUint64()
+{
+ Validator_->OnSimpleType(EWireType::Uint64);
+ return Parser_.ParseUint64();
+}
+
+TInt128 TCheckedSkiffParser::ParseInt128()
+{
+ Validator_->OnSimpleType(EWireType::Int128);
+ return Parser_.ParseInt128();
+}
+
+TUint128 TCheckedSkiffParser::ParseUint128()
+{
+ Validator_->OnSimpleType(EWireType::Uint128);
+ return Parser_.ParseUint128();
+}
+
+double TCheckedSkiffParser::ParseDouble()
+{
+ Validator_->OnSimpleType(EWireType::Double);
+ return Parser_.ParseDouble();
+}
+
+bool TCheckedSkiffParser::ParseBoolean()
+{
+ Validator_->OnSimpleType(EWireType::Boolean);
+ return Parser_.ParseBoolean();
+}
+
+TStringBuf TCheckedSkiffParser::ParseString32()
+{
+ Validator_->OnSimpleType(EWireType::String32);
+ return Parser_.ParseString32();
+}
+
+TStringBuf TCheckedSkiffParser::ParseYson32()
+{
+ Validator_->OnSimpleType(EWireType::Yson32);
+ return Parser_.ParseYson32();
+}
+
+ui8 TCheckedSkiffParser::ParseVariant8Tag()
+{
+ Validator_->BeforeVariant8Tag();
+ auto result = Parser_.ParseVariant8Tag();
+ Validator_->OnVariant8Tag(result);
+ return result;
+}
+
+ui16 TCheckedSkiffParser::ParseVariant16Tag()
+{
+ Validator_->BeforeVariant16Tag();
+ auto result = Parser_.ParseVariant16Tag();
+ Validator_->OnVariant16Tag(result);
+ return result;
+}
+
+bool TCheckedSkiffParser::HasMoreData()
+{
+ return Parser_.HasMoreData();
+}
+
+void TCheckedSkiffParser::ValidateFinished()
+{
+ Validator_->ValidateFinished();
+ Parser_.ValidateFinished();
+}
+
+ui64 TCheckedSkiffParser::GetReadBytesCount() const
+{
+ return Parser_.GetReadBytesCount();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUncheckedSkiffWriter::TUncheckedSkiffWriter(IZeroCopyOutput* underlying)
+ : Underlying_(underlying)
+{ }
+
+TUncheckedSkiffWriter::TUncheckedSkiffWriter(IOutputStream* underlying)
+ : BufferedOutput_(MakeHolder<TBufferedOutput>(underlying))
+ , Underlying_(BufferedOutput_.Get())
+{ }
+
+TUncheckedSkiffWriter::TUncheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& /*schema*/, IZeroCopyOutput* underlying)
+ : TUncheckedSkiffWriter(underlying)
+{ }
+
+TUncheckedSkiffWriter::TUncheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& /*schema*/, IOutputStream* underlying)
+ : TUncheckedSkiffWriter(underlying)
+{ }
+
+TUncheckedSkiffWriter::~TUncheckedSkiffWriter()
+{
+ try {
+ Flush();
+ } catch (...) {
+ }
+}
+
+void TUncheckedSkiffWriter::WriteInt8(i8 value)
+{
+ WriteSimple<i8>(value);
+}
+
+void TUncheckedSkiffWriter::WriteInt16(i16 value)
+{
+ WriteSimple<i16>(value);
+}
+
+void TUncheckedSkiffWriter::WriteInt32(i32 value)
+{
+ WriteSimple<i32>(value);
+}
+
+void TUncheckedSkiffWriter::WriteInt64(i64 value)
+{
+ WriteSimple<i64>(value);
+}
+
+void TUncheckedSkiffWriter::WriteInt128(TInt128 value)
+{
+ WriteSimple<ui64>(value.Low);
+ WriteSimple<i64>(value.High);
+}
+
+void TUncheckedSkiffWriter::WriteUint128(TUint128 value)
+{
+ WriteSimple<ui64>(value.Low);
+ WriteSimple<ui64>(value.High);
+}
+
+void TUncheckedSkiffWriter::WriteUint8(ui8 value)
+{
+ WriteSimple<ui8>(value);
+}
+
+void TUncheckedSkiffWriter::WriteUint16(ui16 value)
+{
+ WriteSimple<ui16>(value);
+}
+
+void TUncheckedSkiffWriter::WriteUint32(ui32 value)
+{
+ WriteSimple<ui32>(value);
+}
+
+void TUncheckedSkiffWriter::WriteUint64(ui64 value)
+{
+ WriteSimple<ui64>(value);
+}
+
+void TUncheckedSkiffWriter::WriteDouble(double value)
+{
+ return WriteSimple<double>(value);
+}
+
+void TUncheckedSkiffWriter::WriteBoolean(bool value)
+{
+ return WriteSimple<ui8>(value ? 1 : 0);
+}
+
+void TUncheckedSkiffWriter::WriteString32(TStringBuf value)
+{
+ WriteSimple<ui32>(value.size());
+ Underlying_.Write(value.data(), value.size());
+}
+
+void TUncheckedSkiffWriter::WriteYson32(TStringBuf value)
+{
+ WriteSimple<ui32>(value.size());
+ Underlying_.Write(value.data(), value.size());
+}
+
+void TUncheckedSkiffWriter::WriteVariant8Tag(ui8 tag)
+{
+ WriteSimple<ui8>(tag);
+}
+
+void TUncheckedSkiffWriter::WriteVariant16Tag(ui16 tag)
+{
+ WriteSimple<ui16>(tag);
+}
+
+void TUncheckedSkiffWriter::Flush()
+{
+ Underlying_.UndoRemaining();
+ if (BufferedOutput_) {
+ BufferedOutput_->Flush();
+ }
+}
+
+template <typename T>
+Y_FORCE_INLINE void TUncheckedSkiffWriter::WriteSimple(T value)
+{
+ if constexpr (std::is_integral_v<T>) {
+ value = HostToLittle(value);
+ Underlying_.Write(&value, sizeof(T));
+ } else {
+ Underlying_.Write(&value, sizeof(T));
+ }
+}
+
+void TUncheckedSkiffWriter::Finish()
+{
+ Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCheckedSkiffWriter::TCheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyOutput* underlying)
+ : Writer_(underlying)
+ , Validator_(std::make_unique<TSkiffValidator>(schema))
+{ }
+
+TCheckedSkiffWriter::TCheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IOutputStream* underlying)
+ : Writer_(underlying)
+ , Validator_(std::make_unique<TSkiffValidator>(schema))
+{ }
+
+TCheckedSkiffWriter::~TCheckedSkiffWriter() = default;
+
+void TCheckedSkiffWriter::WriteDouble(double value)
+{
+ Validator_->OnSimpleType(EWireType::Double);
+ Writer_.WriteDouble(value);
+}
+
+void TCheckedSkiffWriter::WriteBoolean(bool value)
+{
+ Validator_->OnSimpleType(EWireType::Boolean);
+ Writer_.WriteBoolean(value);
+}
+
+void TCheckedSkiffWriter::WriteInt8(i8 value)
+{
+ Validator_->OnSimpleType(EWireType::Int8);
+ Writer_.WriteInt8(value);
+}
+
+void TCheckedSkiffWriter::WriteInt16(i16 value)
+{
+ Validator_->OnSimpleType(EWireType::Int16);
+ Writer_.WriteInt16(value);
+}
+
+void TCheckedSkiffWriter::WriteInt32(i32 value)
+{
+ Validator_->OnSimpleType(EWireType::Int32);
+ Writer_.WriteInt32(value);
+}
+
+void TCheckedSkiffWriter::WriteInt64(i64 value)
+{
+ Validator_->OnSimpleType(EWireType::Int64);
+ Writer_.WriteInt64(value);
+}
+
+void TCheckedSkiffWriter::WriteUint8(ui8 value)
+{
+ Validator_->OnSimpleType(EWireType::Uint8);
+ Writer_.WriteUint8(value);
+}
+
+void TCheckedSkiffWriter::WriteUint16(ui16 value)
+{
+ Validator_->OnSimpleType(EWireType::Uint16);
+ Writer_.WriteUint16(value);
+}
+
+void TCheckedSkiffWriter::WriteUint32(ui32 value)
+{
+ Validator_->OnSimpleType(EWireType::Uint32);
+ Writer_.WriteUint32(value);
+}
+
+void TCheckedSkiffWriter::WriteUint64(ui64 value)
+{
+ Validator_->OnSimpleType(EWireType::Uint64);
+ Writer_.WriteUint64(value);
+}
+
+void TCheckedSkiffWriter::WriteInt128(TInt128 value)
+{
+ Validator_->OnSimpleType(EWireType::Int128);
+ Writer_.WriteInt128(value);
+}
+
+void TCheckedSkiffWriter::WriteUint128(TUint128 value)
+{
+ Validator_->OnSimpleType(EWireType::Uint128);
+ Writer_.WriteUint128(value);
+}
+
+void TCheckedSkiffWriter::WriteString32(TStringBuf value)
+{
+ Validator_->OnSimpleType(EWireType::String32);
+ Writer_.WriteString32(value);
+}
+
+void TCheckedSkiffWriter::WriteYson32(TStringBuf value)
+{
+ Validator_->OnSimpleType(EWireType::Yson32);
+ Writer_.WriteYson32(value);
+}
+
+void TCheckedSkiffWriter::WriteVariant8Tag(ui8 tag)
+{
+ Validator_->OnVariant8Tag(tag);
+ Writer_.WriteVariant8Tag(tag);
+}
+
+void TCheckedSkiffWriter::WriteVariant16Tag(ui16 tag)
+{
+ Validator_->OnVariant16Tag(tag);
+ Writer_.WriteVariant16Tag(tag);
+}
+
+void TCheckedSkiffWriter::Flush()
+{
+ Writer_.Flush();
+}
+
+void TCheckedSkiffWriter::Finish()
+{
+ Validator_->ValidateFinished();
+ Writer_.Finish();
+}
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/skiff.h b/library/cpp/skiff/skiff.h
new file mode 100644
index 0000000000..183c112700
--- /dev/null
+++ b/library/cpp/skiff/skiff.h
@@ -0,0 +1,259 @@
+#pragma once
+
+#include "public.h"
+
+#include "zerocopy_output_writer.h"
+
+#include <util/generic/buffer.h>
+#include <util/generic/yexception.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffException
+ : public yexception
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+constexpr T EndOfSequenceTag()
+{
+ static_assert(std::is_integral<T>::value && std::is_unsigned<T>::value, "T must be unsigned integer");
+ return T(-1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TInt128
+{
+ ui64 Low = 0;
+ i64 High = 0;
+};
+
+struct TUint128
+{
+ ui64 Low = 0;
+ ui64 High = 0;
+};
+
+bool operator==(TInt128 lhs, TInt128 rhs);
+bool operator!=(TInt128 lhs, TInt128 rhs);
+
+bool operator==(TUint128 lhs, TUint128 rhs);
+bool operator!=(TUint128 lhs, TUint128 rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUncheckedSkiffParser
+{
+public:
+ explicit TUncheckedSkiffParser(IZeroCopyInput* stream);
+ TUncheckedSkiffParser(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyInput* stream);
+
+ i8 ParseInt8();
+ i16 ParseInt16();
+ i32 ParseInt32();
+ i64 ParseInt64();
+
+ ui8 ParseUint8();
+ ui16 ParseUint16();
+ ui32 ParseUint32();
+ ui64 ParseUint64();
+
+ TInt128 ParseInt128();
+ TUint128 ParseUint128();
+
+ double ParseDouble();
+
+ bool ParseBoolean();
+
+ TStringBuf ParseString32();
+
+ TStringBuf ParseYson32();
+
+ ui8 ParseVariant8Tag();
+ ui16 ParseVariant16Tag();
+
+ bool HasMoreData();
+
+ void ValidateFinished();
+
+ ui64 GetReadBytesCount() const;
+
+private:
+ const void* GetData(size_t size);
+ const void* GetDataViaBuffer(size_t size);
+
+ size_t RemainingBytes() const;
+ void Advance(size_t size);
+ void RefillBuffer();
+
+ template <typename T>
+ T ParseSimple();
+
+private:
+ IZeroCopyInput* const Underlying_;
+
+ TBuffer Buffer_;
+ ui64 ReadBytesCount_ = 0;
+ char* Position_ = nullptr;
+ char* End_ = nullptr;
+ bool Exhausted_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCheckedSkiffParser
+{
+public:
+ TCheckedSkiffParser(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyInput* stream);
+ ~TCheckedSkiffParser();
+
+ i8 ParseInt8();
+ i16 ParseInt16();
+ i32 ParseInt32();
+ i64 ParseInt64();
+
+ ui8 ParseUint8();
+ ui16 ParseUint16();
+ ui32 ParseUint32();
+ ui64 ParseUint64();
+
+ TInt128 ParseInt128();
+ TUint128 ParseUint128();
+
+ double ParseDouble();
+
+ bool ParseBoolean();
+
+ TStringBuf ParseString32();
+
+ TStringBuf ParseYson32();
+
+ ui8 ParseVariant8Tag();
+ ui16 ParseVariant16Tag();
+
+ bool HasMoreData();
+
+ void ValidateFinished();
+
+ ui64 GetReadBytesCount() const;
+
+private:
+ TUncheckedSkiffParser Parser_;
+ std::unique_ptr<TSkiffValidator> Validator_;
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TUncheckedSkiffWriter
+{
+public:
+ explicit TUncheckedSkiffWriter(IZeroCopyOutput* underlying);
+ explicit TUncheckedSkiffWriter(IOutputStream* underlying);
+ TUncheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyOutput* underlying);
+ TUncheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IOutputStream* underlying);
+
+ ~TUncheckedSkiffWriter();
+
+ void WriteDouble(double value);
+ void WriteBoolean(bool value);
+
+ void WriteInt8(i8 value);
+ void WriteInt16(i16 value);
+ void WriteInt32(i32 value);
+ void WriteInt64(i64 value);
+
+ void WriteUint8(ui8 value);
+ void WriteUint16(ui16 value);
+ void WriteUint32(ui32 value);
+ void WriteUint64(ui64 value);
+
+ void WriteInt128(TInt128 value);
+ void WriteUint128(TUint128 value);
+
+ void WriteString32(TStringBuf value);
+
+ void WriteYson32(TStringBuf value);
+
+ void WriteVariant8Tag(ui8 tag);
+ void WriteVariant16Tag(ui16 tag);
+
+ void Flush();
+ void Finish();
+
+private:
+
+ template <typename T>
+ void WriteSimple(T data);
+
+private:
+ THolder<TBufferedOutput> BufferedOutput_;
+ TZeroCopyOutputStreamWriter Underlying_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCheckedSkiffWriter
+{
+public:
+ TCheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IZeroCopyOutput* underlying);
+ TCheckedSkiffWriter(const std::shared_ptr<TSkiffSchema>& schema, IOutputStream* underlying);
+
+ ~TCheckedSkiffWriter();
+
+ void WriteInt8(i8 value);
+ void WriteInt16(i16 value);
+ void WriteInt32(i32 value);
+ void WriteInt64(i64 value);
+
+ void WriteUint8(ui8 value);
+ void WriteUint16(ui16 value);
+ void WriteUint32(ui32 value);
+ void WriteUint64(ui64 value);
+
+ void WriteDouble(double value);
+ void WriteBoolean(bool value);
+
+ void WriteInt128(TInt128 value);
+ void WriteUint128(TUint128 value);
+
+ void WriteString32(TStringBuf value);
+
+ void WriteYson32(TStringBuf value);
+
+ void WriteVariant8Tag(ui8 tag);
+ void WriteVariant16Tag(ui16 tag);
+
+ void Flush();
+ void Finish();
+
+private:
+ TUncheckedSkiffWriter Writer_;
+ std::unique_ptr<TSkiffValidator> Validator_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EWireType wireType>
+class TUnderlyingIntegerType {
+private:
+ TUnderlyingIntegerType() = default;
+ static constexpr auto F();
+
+public:
+ using TValue = decltype(TUnderlyingIntegerType::F());
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
+
+#define SKIFF_H
+#include "skiff-inl.h"
+#undef SKIFF_H
diff --git a/library/cpp/skiff/skiff_schema-inl.h b/library/cpp/skiff/skiff_schema-inl.h
new file mode 100644
index 0000000000..d66325b222
--- /dev/null
+++ b/library/cpp/skiff/skiff_schema-inl.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#ifndef SKIFF_SCHEMA_H
+#error "Direct inclusion of this file is not allowed, include skiff_schema.h"
+// For the sake of sane code completion.
+#include "skiff_schema.h"
+#endif
+#undef SKIFF_SCHEMA_H
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool IsSimpleType(EWireType type)
+{
+ switch (type) {
+ case EWireType::Int8:
+ case EWireType::Int16:
+ case EWireType::Int32:
+ case EWireType::Int64:
+ case EWireType::Int128:
+
+ case EWireType::Uint8:
+ case EWireType::Uint16:
+ case EWireType::Uint32:
+ case EWireType::Uint64:
+ case EWireType::Uint128:
+
+ case EWireType::Double:
+ case EWireType::Boolean:
+ case EWireType::String32:
+ case EWireType::Yson32:
+ case EWireType::Nothing:
+ return true;
+ case EWireType::Tuple:
+ case EWireType::Variant8:
+ case EWireType::Variant16:
+ case EWireType::RepeatedVariant8:
+ case EWireType::RepeatedVariant16:
+ return false;
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EWireType WireType>
+TComplexSchema<WireType>::TComplexSchema(TSkiffSchemaList elements)
+ : TSkiffSchema(WireType)
+ , Elements_(std::move(elements))
+{ }
+
+template <EWireType WireType>
+const TSkiffSchemaList& TComplexSchema<WireType>::GetChildren() const
+{
+ return Elements_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/skiff_schema.cpp b/library/cpp/skiff/skiff_schema.cpp
new file mode 100644
index 0000000000..c762896ad0
--- /dev/null
+++ b/library/cpp/skiff/skiff_schema.cpp
@@ -0,0 +1,164 @@
+#include "skiff_schema.h"
+
+#include "skiff.h"
+
+#include <util/generic/hash.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TSkiffSchema& lhs, const TSkiffSchema& rhs)
+{
+ if (lhs.GetWireType() != rhs.GetWireType() || lhs.GetName() != rhs.GetName()) {
+ return false;
+ }
+ const auto& lhsChildren = lhs.GetChildren();
+ const auto& rhsChildren = rhs.GetChildren();
+ return std::equal(
+ std::begin(lhsChildren),
+ std::end(lhsChildren),
+ std::begin(rhsChildren),
+ std::end(rhsChildren),
+ TSkiffSchemaPtrEqual());
+}
+
+bool operator!=(const TSkiffSchema& lhs, const TSkiffSchema& rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintShortDebugString(const std::shared_ptr<const TSkiffSchema>& schema, IOutputStream* out)
+{
+ (*out) << ToString(schema->GetWireType());
+ if (!IsSimpleType(schema->GetWireType())) {
+ auto children = schema->GetChildren();
+ if (!children.empty()) {
+ (*out) << '<';
+ for (const auto& child : children) {
+ PrintShortDebugString(child, out);
+ (*out) << ';';
+ }
+ (*out) << '>';
+ }
+ }
+}
+
+TString GetShortDebugString(const std::shared_ptr<const TSkiffSchema>& schema)
+{
+ TStringStream out;
+ PrintShortDebugString(schema, &out);
+ return out.Str();
+}
+
+std::shared_ptr<TSimpleTypeSchema> CreateSimpleTypeSchema(EWireType type)
+{
+ return std::make_shared<TSimpleTypeSchema>(type);
+}
+
+static void VerifyNonemptyChildren(const TSkiffSchemaList& children, EWireType wireType)
+{
+ if (children.empty()) {
+ ythrow TSkiffException() << "\"" << ToString(wireType) << "\" must have at least one child";
+ }
+}
+
+std::shared_ptr<TTupleSchema> CreateTupleSchema(TSkiffSchemaList children)
+{
+ return std::make_shared<TTupleSchema>(std::move(children));
+}
+
+std::shared_ptr<TVariant8Schema> CreateVariant8Schema(TSkiffSchemaList children)
+{
+ VerifyNonemptyChildren(children, EWireType::Variant8);
+ return std::make_shared<TVariant8Schema>(std::move(children));
+}
+
+std::shared_ptr<TVariant16Schema> CreateVariant16Schema(TSkiffSchemaList children)
+{
+ VerifyNonemptyChildren(children, EWireType::Variant16);
+ return std::make_shared<TVariant16Schema>(std::move(children));
+}
+
+std::shared_ptr<TRepeatedVariant8Schema> CreateRepeatedVariant8Schema(TSkiffSchemaList children)
+{
+ VerifyNonemptyChildren(children, EWireType::RepeatedVariant8);
+ return std::make_shared<TRepeatedVariant8Schema>(std::move(children));
+}
+
+std::shared_ptr<TRepeatedVariant16Schema> CreateRepeatedVariant16Schema(TSkiffSchemaList children)
+{
+ VerifyNonemptyChildren(children, EWireType::RepeatedVariant16);
+ return std::make_shared<TRepeatedVariant16Schema>(std::move(children));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSkiffSchema::TSkiffSchema(EWireType type)
+ : Type_(type)
+{ }
+
+EWireType TSkiffSchema::GetWireType() const
+{
+ return Type_;
+}
+
+std::shared_ptr<TSkiffSchema> TSkiffSchema::SetName(TString name)
+{
+ Name_ = std::move(name);
+ return shared_from_this();
+}
+
+const TString& TSkiffSchema::GetName() const
+{
+ return Name_;
+}
+
+const TSkiffSchemaList& TSkiffSchema::GetChildren() const
+{
+ static const TSkiffSchemaList children;
+ return children;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSimpleTypeSchema::TSimpleTypeSchema(EWireType type)
+ : TSkiffSchema(type)
+{
+ Y_VERIFY(IsSimpleType(type));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TSkiffSchemaPtrHasher::operator()(const std::shared_ptr<TSkiffSchema>& schema) const
+{
+ return THash<NSkiff::TSkiffSchema>()(*schema);
+}
+
+size_t TSkiffSchemaPtrEqual::operator()(
+ const std::shared_ptr<TSkiffSchema>& lhs,
+ const std::shared_ptr<TSkiffSchema>& rhs) const
+{
+ return *lhs == *rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t THash<NSkiff::TSkiffSchema>::operator()(const NSkiff::TSkiffSchema &schema) const
+{
+ auto hash = CombineHashes(
+ THash<TString>()(schema.GetName()),
+ static_cast<size_t>(schema.GetWireType()));
+ for (const auto& child : schema.GetChildren()) {
+ hash = CombineHashes(hash, (*this)(*child));
+ }
+ return hash;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/skiff/skiff_schema.h b/library/cpp/skiff/skiff_schema.h
new file mode 100644
index 0000000000..8952a84bac
--- /dev/null
+++ b/library/cpp/skiff/skiff_schema.h
@@ -0,0 +1,121 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/string.h>
+#include <util/string/cast.h>
+
+#include <vector>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EWireType WireType>
+class TComplexSchema;
+
+using TTupleSchema = TComplexSchema<EWireType::Tuple>;
+using TVariant8Schema = TComplexSchema<EWireType::Variant8>;
+using TVariant16Schema = TComplexSchema<EWireType::Variant16>;
+using TRepeatedVariant8Schema = TComplexSchema<EWireType::RepeatedVariant8>;
+using TRepeatedVariant16Schema = TComplexSchema<EWireType::RepeatedVariant16>;
+
+using TTupleSchemaPtr = std::shared_ptr<TTupleSchema>;
+using TVariant8SchemaPtr = std::shared_ptr<TVariant8Schema>;
+using TVariant16SchemaPtr = std::shared_ptr<TVariant16Schema>;
+using TRepeatedVariant8SchemaPtr = std::shared_ptr<TRepeatedVariant8Schema>;
+using TRepeatedVariant16SchemaPtr = std::shared_ptr<TRepeatedVariant16Schema>;
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffSchema
+ : public std::enable_shared_from_this<TSkiffSchema>
+{
+public:
+ virtual ~TSkiffSchema() = default;
+
+ EWireType GetWireType() const;
+ std::shared_ptr<TSkiffSchema> SetName(TString name);
+ const TString& GetName() const;
+
+ virtual const TSkiffSchemaList& GetChildren() const;
+
+protected:
+ explicit TSkiffSchema(EWireType type);
+
+private:
+ const EWireType Type_;
+ TString Name_;
+};
+
+bool operator==(const TSkiffSchema& lhs, const TSkiffSchema& rhs);
+bool operator!=(const TSkiffSchema& lhs, const TSkiffSchema& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleTypeSchema
+ : public TSkiffSchema
+{
+public:
+ explicit TSimpleTypeSchema(EWireType type);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EWireType WireType>
+class TComplexSchema
+ : public TSkiffSchema
+{
+public:
+ explicit TComplexSchema(TSkiffSchemaList elements);
+
+ virtual const TSkiffSchemaList& GetChildren() const override;
+
+private:
+ const TSkiffSchemaList Elements_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSimpleType(EWireType type);
+TString GetShortDebugString(const std::shared_ptr<const TSkiffSchema>& schema);
+void PrintShortDebugString(const std::shared_ptr<const TSkiffSchema>& schema, IOutputStream* out);
+
+std::shared_ptr<TSimpleTypeSchema> CreateSimpleTypeSchema(EWireType type);
+std::shared_ptr<TTupleSchema> CreateTupleSchema(TSkiffSchemaList children);
+std::shared_ptr<TVariant8Schema> CreateVariant8Schema(TSkiffSchemaList children);
+std::shared_ptr<TVariant16Schema> CreateVariant16Schema(TSkiffSchemaList children);
+std::shared_ptr<TRepeatedVariant8Schema> CreateRepeatedVariant8Schema(TSkiffSchemaList children);
+std::shared_ptr<TRepeatedVariant16Schema> CreateRepeatedVariant16Schema(TSkiffSchemaList children);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSkiffSchemaPtrHasher
+{
+ size_t operator()(const std::shared_ptr<TSkiffSchema>& schema) const;
+};
+
+struct TSkiffSchemaPtrEqual
+{
+ size_t operator()(
+ const std::shared_ptr<TSkiffSchema>& lhs,
+ const std::shared_ptr<TSkiffSchema>& rhs) const;
+};
+
+} // namespace NSkiff
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+struct THash<NSkiff::TSkiffSchema>
+{
+ size_t operator()(const NSkiff::TSkiffSchema& schema) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define SKIFF_SCHEMA_H
+#include "skiff_schema-inl.h"
+#undef SKIFF_SCHEMA_H
diff --git a/library/cpp/skiff/skiff_validator.cpp b/library/cpp/skiff/skiff_validator.cpp
new file mode 100644
index 0000000000..1b1b98d5a6
--- /dev/null
+++ b/library/cpp/skiff/skiff_validator.cpp
@@ -0,0 +1,396 @@
+#include "skiff.h"
+#include "skiff_validator.h"
+
+#include <vector>
+#include <stack>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IValidatorNode;
+
+using TValidatorNodeList = std::vector<std::shared_ptr<IValidatorNode>>;
+using TSkiffSchemaList = std::vector<std::shared_ptr<TSkiffSchema>>;
+
+static std::shared_ptr<IValidatorNode> CreateUsageValidatorNode(const std::shared_ptr<TSkiffSchema>& skiffSchema);
+static TValidatorNodeList CreateUsageValidatorNodeList(const TSkiffSchemaList& skiffSchemaList);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+inline void ThrowUnexpectedParseWrite(T wireType)
+{
+ ythrow TSkiffException() << "Unexpected parse/write of \"" << ::ToString(wireType) << "\" token";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IValidatorNode
+{
+ virtual ~IValidatorNode() = default;
+
+ virtual void OnBegin(TValidatorNodeStack* /*validatorNodeStack*/)
+ { }
+
+ virtual void OnChildDone(TValidatorNodeStack* /*validatorNodeStack*/)
+ {
+ Y_FAIL();
+ }
+
+ virtual void OnSimpleType(TValidatorNodeStack* /*validatorNodeStack*/, EWireType wireType)
+ {
+ ThrowUnexpectedParseWrite(wireType);
+ }
+
+ virtual void BeforeVariant8Tag()
+ {
+ ThrowUnexpectedParseWrite(EWireType::Variant8);
+ }
+
+ virtual void OnVariant8Tag(TValidatorNodeStack* /*validatorNodeStack*/, ui8 /*tag*/)
+ {
+ IValidatorNode::BeforeVariant8Tag();
+ }
+
+ virtual void BeforeVariant16Tag()
+ {
+ ThrowUnexpectedParseWrite(EWireType::Variant16);
+ }
+
+ virtual void OnVariant16Tag(TValidatorNodeStack* /*validatorNodeStack*/, ui16 /*tag*/)
+ {
+ IValidatorNode::BeforeVariant16Tag();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TValidatorNodeStack
+{
+public:
+ explicit TValidatorNodeStack(std::shared_ptr<IValidatorNode> validator)
+ : RootValidator_(std::move(validator))
+ { }
+
+ void PushValidator(IValidatorNode* validator)
+ {
+ ValidatorStack_.push(validator);
+ validator->OnBegin(this);
+ }
+
+ void PopValidator()
+ {
+ Y_VERIFY(!ValidatorStack_.empty());
+ ValidatorStack_.pop();
+ if (!ValidatorStack_.empty()) {
+ ValidatorStack_.top()->OnChildDone(this);
+ }
+ }
+
+ void PushRootIfRequired()
+ {
+ if (ValidatorStack_.empty()) {
+ PushValidator(RootValidator_.get());
+ }
+ }
+
+ IValidatorNode* Top() const
+ {
+ Y_VERIFY(!ValidatorStack_.empty());
+ return ValidatorStack_.top();
+ }
+
+ bool IsFinished() const
+ {
+ return ValidatorStack_.empty();
+ }
+
+private:
+ const std::shared_ptr<IValidatorNode> RootValidator_;
+ std::stack<IValidatorNode*> ValidatorStack_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNothingTypeValidator
+ : public IValidatorNode
+{
+public:
+ void OnBegin(TValidatorNodeStack* validatorNodeStack) override
+ {
+ validatorNodeStack->PopValidator();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleTypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TSimpleTypeUsageValidator(EWireType type)
+ : Type_(type)
+ { }
+
+ void OnSimpleType(TValidatorNodeStack* validatorNodeStack, EWireType type) override
+ {
+ if (type != Type_) {
+ ThrowUnexpectedParseWrite(type);
+ }
+ validatorNodeStack->PopValidator();
+ }
+
+private:
+ const EWireType Type_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TTag>
+void ValidateVariantTag(TValidatorNodeStack* validatorNodeStack, TTag tag, const TValidatorNodeList& children)
+{
+ if (tag == EndOfSequenceTag<TTag>()) {
+ // Root validator is pushed into the stack before variant tag
+ // if the stack is empty.
+ validatorNodeStack->PopValidator();
+ } else if (tag >= children.size()) {
+ ythrow TSkiffException() << "Variant tag \"" << tag << "\" "
+ << "exceeds number of children \"" << children.size();
+ } else {
+ validatorNodeStack->PushValidator(children[tag].get());
+ }
+}
+
+class TVariant8TypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TVariant8TypeUsageValidator(TValidatorNodeList children)
+ : Children_(std::move(children))
+ { }
+
+ void BeforeVariant8Tag() override
+ { }
+
+ void OnVariant8Tag(TValidatorNodeStack* validatorNodeStack, ui8 tag) override
+ {
+ ValidateVariantTag(validatorNodeStack, tag, Children_);
+ }
+
+ void OnChildDone(TValidatorNodeStack* validatorNodeStack) override
+ {
+ validatorNodeStack->PopValidator();
+ }
+
+private:
+ const TValidatorNodeList Children_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVariant16TypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TVariant16TypeUsageValidator(TValidatorNodeList children)
+ : Children_(std::move(children))
+ { }
+
+ void BeforeVariant16Tag() override
+ { }
+
+ void OnVariant16Tag(TValidatorNodeStack* validatorNodeStack, ui16 tag) override
+ {
+ ValidateVariantTag(validatorNodeStack, tag, Children_);
+ }
+
+ void OnChildDone(TValidatorNodeStack* validatorNodeStack) override
+ {
+ validatorNodeStack->PopValidator();
+ }
+
+private:
+ const TValidatorNodeList Children_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRepeatedVariant8TypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TRepeatedVariant8TypeUsageValidator(TValidatorNodeList children)
+ : Children_(std::move(children))
+ { }
+
+ void BeforeVariant8Tag() override
+ { }
+
+ void OnVariant8Tag(TValidatorNodeStack* validatorNodeStack, ui8 tag) override
+ {
+ ValidateVariantTag(validatorNodeStack, tag, Children_);
+ }
+
+ void OnChildDone(TValidatorNodeStack* /*validatorNodeStack*/) override
+ { }
+
+private:
+ const TValidatorNodeList Children_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRepeatedVariant16TypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TRepeatedVariant16TypeUsageValidator(TValidatorNodeList children)
+ : Children_(std::move(children))
+ { }
+
+ void BeforeVariant16Tag() override
+ { }
+
+ void OnVariant16Tag(TValidatorNodeStack* validatorNodeStack, ui16 tag) override
+ {
+ ValidateVariantTag(validatorNodeStack, tag, Children_);
+ }
+
+ void OnChildDone(TValidatorNodeStack* /*validatorNodeStack*/) override
+ { }
+
+private:
+ const TValidatorNodeList Children_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTupleTypeUsageValidator
+ : public IValidatorNode
+{
+public:
+ explicit TTupleTypeUsageValidator(TValidatorNodeList children)
+ : Children_(std::move(children))
+ { }
+
+ void OnBegin(TValidatorNodeStack* validatorNodeStack) override
+ {
+ Position_ = 0;
+ if (!Children_.empty()) {
+ validatorNodeStack->PushValidator(Children_[0].get());
+ }
+ }
+
+ void OnChildDone(TValidatorNodeStack* validatorNodeStack) override
+ {
+ Position_++;
+ if (Position_ < Children_.size()) {
+ validatorNodeStack->PushValidator(Children_[Position_].get());
+ } else {
+ validatorNodeStack->PopValidator();
+ }
+ }
+
+private:
+ const TValidatorNodeList Children_;
+ ui32 Position_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSkiffValidator::TSkiffValidator(std::shared_ptr<TSkiffSchema> skiffSchema)
+ : Context_(std::make_unique<TValidatorNodeStack>(CreateUsageValidatorNode(std::move(skiffSchema))))
+{ }
+
+TSkiffValidator::~TSkiffValidator()
+{ }
+
+void TSkiffValidator::BeforeVariant8Tag()
+{
+ Context_->PushRootIfRequired();
+ Context_->Top()->BeforeVariant8Tag();
+}
+
+void TSkiffValidator::OnVariant8Tag(ui8 tag)
+{
+ Context_->PushRootIfRequired();
+ Context_->Top()->OnVariant8Tag(Context_.get(), tag);
+}
+
+void TSkiffValidator::BeforeVariant16Tag()
+{
+ Context_->PushRootIfRequired();
+ Context_->Top()->BeforeVariant16Tag();
+}
+
+void TSkiffValidator::OnVariant16Tag(ui16 tag)
+{
+ Context_->PushRootIfRequired();
+ Context_->Top()->OnVariant16Tag(Context_.get(), tag);
+}
+
+void TSkiffValidator::OnSimpleType(EWireType value)
+{
+ Context_->PushRootIfRequired();
+ Context_->Top()->OnSimpleType(Context_.get(), value);
+}
+
+void TSkiffValidator::ValidateFinished()
+{
+ if (!Context_->IsFinished()) {
+ ythrow TSkiffException() << "Parse/write is not finished";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TValidatorNodeList CreateUsageValidatorNodeList(const TSkiffSchemaList& skiffSchemaList)
+{
+ TValidatorNodeList result;
+ result.reserve(skiffSchemaList.size());
+ for (const auto& skiffSchema : skiffSchemaList) {
+ result.push_back(CreateUsageValidatorNode(skiffSchema));
+ }
+ return result;
+}
+
+std::shared_ptr<IValidatorNode> CreateUsageValidatorNode(const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ switch (skiffSchema->GetWireType()) {
+ case EWireType::Int8:
+ case EWireType::Int16:
+ case EWireType::Int32:
+ case EWireType::Int64:
+ case EWireType::Int128:
+
+ case EWireType::Uint8:
+ case EWireType::Uint16:
+ case EWireType::Uint32:
+ case EWireType::Uint64:
+ case EWireType::Uint128:
+
+ case EWireType::Double:
+ case EWireType::Boolean:
+ case EWireType::String32:
+ case EWireType::Yson32:
+ return std::make_shared<TSimpleTypeUsageValidator>(skiffSchema->GetWireType());
+ case EWireType::Nothing:
+ return std::make_shared<TNothingTypeValidator>();
+ case EWireType::Tuple:
+ return std::make_shared<TTupleTypeUsageValidator>(CreateUsageValidatorNodeList(skiffSchema->GetChildren()));
+ case EWireType::Variant8:
+ return std::make_shared<TVariant8TypeUsageValidator>(CreateUsageValidatorNodeList(skiffSchema->GetChildren()));
+ case EWireType::Variant16:
+ return std::make_shared<TVariant16TypeUsageValidator>(CreateUsageValidatorNodeList(skiffSchema->GetChildren()));
+ case EWireType::RepeatedVariant8:
+ return std::make_shared<TRepeatedVariant8TypeUsageValidator>(CreateUsageValidatorNodeList(skiffSchema->GetChildren()));
+ case EWireType::RepeatedVariant16:
+ return std::make_shared<TRepeatedVariant16TypeUsageValidator>(CreateUsageValidatorNodeList(skiffSchema->GetChildren()));
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/skiff_validator.h b/library/cpp/skiff/skiff_validator.h
new file mode 100644
index 0000000000..522cc74db6
--- /dev/null
+++ b/library/cpp/skiff/skiff_validator.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "public.h"
+
+#include "skiff_schema.h"
+
+#include <util/string/cast.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TValidatorNodeStack;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffValidator
+{
+public:
+ explicit TSkiffValidator(std::shared_ptr<TSkiffSchema> skiffSchema);
+ ~TSkiffValidator();
+
+ void BeforeVariant8Tag();
+ void OnVariant8Tag(ui8 tag);
+
+ void BeforeVariant16Tag();
+ void OnVariant16Tag(ui16 tag);
+
+ void OnSimpleType(EWireType value);
+
+ void ValidateFinished();
+
+private:
+ const std::unique_ptr<TValidatorNodeStack> Context_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/unittests/skiff_schema_ut.cpp b/library/cpp/skiff/unittests/skiff_schema_ut.cpp
new file mode 100644
index 0000000000..c20a560dfc
--- /dev/null
+++ b/library/cpp/skiff/unittests/skiff_schema_ut.cpp
@@ -0,0 +1,148 @@
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/stream/buffer.h>
+
+using namespace NSkiff;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<>
+void Out<TSkiffSchema>(IOutputStream& s, const TSkiffSchema& schema)
+{
+ s << "TSkiffSchema:" << GetShortDebugString(schema.shared_from_this());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_UNIT_TEST_SUITE(TSkiffSchemaTestSuite) {
+ Y_UNIT_TEST(TestIntEqual)
+ {
+ std::shared_ptr<TSkiffSchema> schema1 = CreateSimpleTypeSchema(EWireType::Uint64);
+ schema1->SetName("schema");
+
+ std::shared_ptr<TSkiffSchema> schema2 = CreateSimpleTypeSchema(EWireType::Uint64);
+ schema2->SetName("schema");
+
+ UNIT_ASSERT_VALUES_EQUAL(*schema1, *schema2);
+ }
+
+ Y_UNIT_TEST(TestTupleEqual)
+ {
+ std::shared_ptr<TSkiffSchema> schema1 = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+
+ std::shared_ptr<TSkiffSchema> schema2 = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+
+ Cerr << *schema1 << Endl;
+
+ schema1->SetName("schema");
+ UNIT_ASSERT_VALUES_UNEQUAL(*schema1, *schema2);
+
+ schema2->SetName("schema");
+ UNIT_ASSERT_VALUES_EQUAL(*schema1, *schema2);
+ }
+
+ Y_UNIT_TEST(TestHashes)
+ {
+ TSet<size_t> hashes;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint64);
+ schema->SetName("schema");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema));
+
+ schema = CreateSimpleTypeSchema(EWireType::Uint64);
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema));
+
+ auto schema2 = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+ schema2->SetName("s");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema2));
+
+ schema2->SetName("s0");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema2));
+
+ schema2->SetName("s");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema2));
+
+ auto schema3 = CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ schema2,
+ });
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema3));
+
+ schema3->SetName("kek");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema3));
+
+ auto schema4 = CreateRepeatedVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ schema2,
+ });
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema4));
+
+ schema4->SetName("kek");
+ hashes.insert(THash<NSkiff::TSkiffSchema>()(*schema4));
+
+ UNIT_ASSERT_VALUES_EQUAL(hashes.size(), 8);
+ }
+
+ Y_UNIT_TEST(TestDifferent)
+ {
+ TVector<std::shared_ptr<TSkiffSchema>> schemas;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint64);
+ schema->SetName("schema");
+ schemas.push_back(schema);
+ schemas.push_back(CreateSimpleTypeSchema(EWireType::Uint64));
+
+ auto schema2 = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+ schema2->SetName("s");
+ schemas.push_back(schema2);
+
+ auto schema3 = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+ schema3->SetName("s0");
+ schemas.push_back(schema3);
+
+ auto schema4 = CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ schema2,
+ });
+ schemas.push_back(schema4);
+
+ auto schema5 = CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ schema2,
+ });
+ schema5->SetName("kek");
+ schemas.push_back(schema5);
+
+ auto schema6 = CreateRepeatedVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ schema2,
+ });
+ schemas.push_back(schema6);
+
+ for (size_t i = 0; i < schemas.size(); ++i) {
+ for (size_t j = i + 1; j < schemas.size(); ++j) {
+ UNIT_ASSERT_VALUES_UNEQUAL(*schemas[i], *schemas[j]);
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/skiff/unittests/skiff_ut.cpp b/library/cpp/skiff/unittests/skiff_ut.cpp
new file mode 100644
index 0000000000..5e4c709611
--- /dev/null
+++ b/library/cpp/skiff/unittests/skiff_ut.cpp
@@ -0,0 +1,627 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <util/stream/buffer.h>
+#include <util/string/hex.h>
+
+using namespace NSkiff;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TString HexEncode(const TBuffer& buffer)
+{
+ auto result = HexEncode(buffer.Data(), buffer.Size());
+ result.to_lower();
+ return result;
+}
+
+Y_UNIT_TEST_SUITE(Skiff)
+{
+ Y_UNIT_TEST(TestInt8)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int8);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteInt8(42);
+ tokenWriter.WriteInt8(-42);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "2a"
+ "d6");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt8(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt8(), -42);
+ }
+
+ Y_UNIT_TEST(TestInt16)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int16);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteInt16(0x1234);
+ tokenWriter.WriteInt16(-0x1234);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "3412"
+ "cced");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt16(), 0x1234);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt16(), -0x1234);
+ }
+
+ Y_UNIT_TEST(TestInt32)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int32);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteInt32(0x12345678);
+ tokenWriter.WriteInt32(-0x12345678);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "78563412"
+ "88a9cbed");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt32(), 0x12345678);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt32(), -0x12345678);
+ }
+
+ Y_UNIT_TEST(TestInt64)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int64);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteInt64(-42);
+ tokenWriter.WriteInt64(100500);
+ tokenWriter.WriteInt64(-0x123456789abcdef0);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "d6ffffffffffffff"
+ "9488010000000000"
+ "1021436587a9cbed");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), -42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), 100500);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), -0x123456789abcdef0);
+ }
+
+ Y_UNIT_TEST(TestUint8)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint8);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteUint8(42);
+ tokenWriter.WriteUint8(200);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "2a"
+ "c8");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint8(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint8(), 200);
+ }
+
+ Y_UNIT_TEST(TestUint16)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint16);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteUint16(0x1234);
+ tokenWriter.WriteUint16(0xfedc);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "3412"
+ "dcfe");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint16(), 0x1234);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint16(), 0xfedc);
+ }
+
+ Y_UNIT_TEST(TestUint32)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint32);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteUint32(0x12345678);
+ tokenWriter.WriteUint32(0x87654321);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "78563412"
+ "21436587");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint32(), 0x12345678);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint32(), 0x87654321);
+ }
+
+
+ Y_UNIT_TEST(TestUint64)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint64);
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteUint64(42);
+ tokenWriter.WriteUint64(100500);
+ tokenWriter.WriteUint64(0x123456789abcdef0);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "2a00000000000000"
+ "9488010000000000"
+ "f0debc9a78563412");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 100500);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 0x123456789abcdef0);
+ }
+
+ Y_UNIT_TEST(TestInt128)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int128);
+
+ const TInt128 val1 = {0x1924cd4aeb9ced82, 0x0885e83f456d6a7e};
+ const TInt128 val2 = {0xe9ba36585eccae1a, -0x7854b6f9ce448be9};
+
+ TCheckedSkiffWriter writer(schema, &bufferStream);
+ writer.WriteInt128(val1);
+ writer.WriteInt128(val2);
+ writer.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "82ed9ceb4acd2419" "7e6a6d453fe88508"
+ "1aaecc5e5836bae9" "1774bb310649ab87");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_EQUAL(parser.ParseInt128(), val1);
+ UNIT_ASSERT_EQUAL(parser.ParseInt128(), val2);
+ }
+
+ Y_UNIT_TEST(TestUint128)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Uint128);
+
+ const auto val1 = TUint128{0x1924cd4aeb9ced82, 0x0885e83f456d6a7e};
+ const auto val2 = TUint128{0xe9ba36585eccae1a, 0x8854b6f9ce448be9};
+
+ TCheckedSkiffWriter writer(schema, &bufferStream);
+ writer.WriteUint128(val1);
+ writer.WriteUint128(val2);
+ writer.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "82ed9ceb4acd2419" "7e6a6d453fe88508"
+ "1aaecc5e5836bae9" "e98b44cef9b65488");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_EQUAL(parser.ParseUint128(), val1);
+ UNIT_ASSERT_EQUAL(parser.ParseUint128(), val2);
+ }
+
+ Y_UNIT_TEST(TestBoolean)
+ {
+ auto schema = CreateSimpleTypeSchema(EWireType::Boolean);
+
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteBoolean(true);
+ tokenWriter.WriteBoolean(false);
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseBoolean(), true);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseBoolean(), false);
+
+ {
+ TBufferStream bufferStream;
+ bufferStream.Write('\x02');
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_EXCEPTION(parser.ParseBoolean(), std::exception);
+ }
+ }
+
+ Y_UNIT_TEST(TestVariant8)
+ {
+ auto schema = CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ });
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ UNIT_ASSERT_EXCEPTION(tokenWriter.WriteUint64(42), std::exception);
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteVariant8Tag(0);
+ UNIT_ASSERT_EXCEPTION(tokenWriter.WriteUint64(42), std::exception);
+ }
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteVariant8Tag(1);
+ UNIT_ASSERT_EXCEPTION(tokenWriter.WriteInt64(42), std::exception);
+ }
+
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteVariant8Tag(0);
+ tokenWriter.WriteVariant8Tag(1);
+ tokenWriter.WriteUint64(42);
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 42);
+
+ parser.ValidateFinished();
+ }
+
+ Y_UNIT_TEST(TestTuple)
+ {
+
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::String32),
+ });
+
+ {
+ TBufferStream bufferStream;
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteInt64(42);
+ tokenWriter.WriteString32("foobar");
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseString32(), "foobar");
+ parser.ValidateFinished();
+ }
+ }
+
+ Y_UNIT_TEST(TestString)
+ {
+
+ auto schema = CreateSimpleTypeSchema(EWireType::String32);
+
+ {
+ TBufferStream bufferStream;
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteString32("foo");
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseString32(), "foo");
+
+ parser.ValidateFinished();
+ }
+
+ {
+ TBufferStream bufferStream;
+
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+ tokenWriter.WriteString32("foo");
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_EXCEPTION(parser.ParseInt64(), std::exception);
+ }
+ }
+
+ Y_UNIT_TEST(TestRepeatedVariant8)
+ {
+
+ auto schema = CreateRepeatedVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ });
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ // row 0
+ tokenWriter.WriteVariant8Tag(0);
+ tokenWriter.WriteInt64(-8);
+
+ // row 2
+ tokenWriter.WriteVariant8Tag(1);
+ tokenWriter.WriteUint64(42);
+
+ // end
+ tokenWriter.WriteVariant8Tag(EndOfSequenceTag<ui8>());
+
+ tokenWriter.Finish();
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ // row 1
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), -8);
+
+ // row 2
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 42);
+
+ // end
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), EndOfSequenceTag<ui8>());
+
+ parser.ValidateFinished();
+ }
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ UNIT_ASSERT_EXCEPTION(parser.ParseInt64(), std::exception);
+ }
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ parser.ParseVariant8Tag();
+ UNIT_ASSERT_EXCEPTION(parser.ParseUint64(), std::exception);
+ }
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ parser.ParseVariant8Tag();
+ parser.ParseInt64();
+
+ UNIT_ASSERT_EXCEPTION(parser.ValidateFinished(), std::exception);
+ }
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ tokenWriter.WriteVariant8Tag(0);
+ UNIT_ASSERT_EXCEPTION(tokenWriter.WriteUint64(5), std::exception);
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ tokenWriter.WriteVariant8Tag(1);
+ tokenWriter.WriteUint64(5);
+
+ UNIT_ASSERT_EXCEPTION(tokenWriter.Finish(), std::exception);
+ }
+ }
+
+ Y_UNIT_TEST(TestRepeatedVariant16)
+ {
+
+ auto schema = CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ });
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ // row 0
+ tokenWriter.WriteVariant16Tag(0);
+ tokenWriter.WriteInt64(-8);
+
+ // row 2
+ tokenWriter.WriteVariant16Tag(1);
+ tokenWriter.WriteUint64(42);
+
+ // end
+ tokenWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ tokenWriter.Finish();
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+
+ // row 1
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), -8);
+
+ // row 2
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 42);
+
+ // end
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ parser.ValidateFinished();
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ tokenWriter.WriteVariant16Tag(0);
+ UNIT_ASSERT_EXCEPTION(tokenWriter.WriteUint64(5), std::exception);
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ tokenWriter.WriteVariant16Tag(1);
+ tokenWriter.WriteUint64(5);
+
+ UNIT_ASSERT_EXCEPTION(tokenWriter.Finish(), std::exception);
+ }
+
+ {
+ TBufferStream bufferStream;
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ // row 0
+ tokenWriter.WriteVariant16Tag(0);
+ tokenWriter.WriteInt64(-8);
+
+ // row 2
+ tokenWriter.WriteVariant16Tag(1);
+ tokenWriter.WriteUint64(42);
+
+ // end
+ tokenWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ tokenWriter.Finish();
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ UNIT_ASSERT_EXCEPTION(parser.ParseInt64(), std::exception);
+ }
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ parser.ParseVariant16Tag();
+ UNIT_ASSERT_EXCEPTION(parser.ParseUint64(), std::exception);
+ }
+
+ {
+ TBufferInput input(bufferStream.Buffer());
+ TCheckedSkiffParser parser(schema, &input);
+
+ parser.ParseVariant16Tag();
+ parser.ParseInt64();
+
+ UNIT_ASSERT_EXCEPTION(parser.ValidateFinished(), std::exception);
+ }
+ }
+ }
+
+ Y_UNIT_TEST(TestStruct)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateRepeatedVariant16Schema(
+ {
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64)
+ }),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ })
+ }
+ );
+
+ {
+ TCheckedSkiffWriter tokenWriter(schema, &bufferStream);
+
+ // row 0
+ tokenWriter.WriteVariant16Tag(0);
+
+ // row 1
+ tokenWriter.WriteVariant16Tag(1);
+ tokenWriter.WriteVariant8Tag(0);
+ tokenWriter.WriteUint64(1);
+
+ // row 2
+ tokenWriter.WriteVariant16Tag(1);
+ tokenWriter.WriteVariant8Tag(1);
+ tokenWriter.WriteInt64(2);
+ tokenWriter.WriteUint64(3);
+
+ // end
+ tokenWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ tokenWriter.Finish();
+ }
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+
+ // row 0
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), 0);
+
+ // row 1
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 1);
+
+ // row 2
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant8Tag(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt64(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseUint64(), 3);
+
+ // end
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ parser.ValidateFinished();
+ }
+
+ Y_UNIT_TEST(TestSimpleOutputStream)
+ {
+ TBufferStream bufferStream;
+
+ auto schema = CreateSimpleTypeSchema(EWireType::Int8);
+
+ TCheckedSkiffWriter tokenWriter(schema, static_cast<IOutputStream*>(&bufferStream));
+ tokenWriter.WriteInt8(42);
+ tokenWriter.WriteInt8(-42);
+ tokenWriter.Finish();
+
+ UNIT_ASSERT_VALUES_EQUAL(HexEncode(bufferStream.Buffer()),
+ "2a"
+ "d6");
+
+ TCheckedSkiffParser parser(schema, &bufferStream);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt8(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(parser.ParseInt8(), -42);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/skiff/unittests/ya.make b/library/cpp/skiff/unittests/ya.make
new file mode 100644
index 0000000000..d67ca8c618
--- /dev/null
+++ b/library/cpp/skiff/unittests/ya.make
@@ -0,0 +1,12 @@
+UNITTEST()
+
+SRCS(
+ skiff_ut.cpp
+ skiff_schema_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/skiff
+)
+
+END()
diff --git a/library/cpp/skiff/ya.make b/library/cpp/skiff/ya.make
new file mode 100644
index 0000000000..ff3eb55c9f
--- /dev/null
+++ b/library/cpp/skiff/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ skiff.cpp
+ skiff_schema.cpp
+ skiff_validator.cpp
+ zerocopy_output_writer.cpp
+)
+
+GENERATE_ENUM_SERIALIZATION(public.h)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests
+)
diff --git a/library/cpp/skiff/zerocopy_output_writer-inl.h b/library/cpp/skiff/zerocopy_output_writer-inl.h
new file mode 100644
index 0000000000..6bd067c9fa
--- /dev/null
+++ b/library/cpp/skiff/zerocopy_output_writer-inl.h
@@ -0,0 +1,51 @@
+#pragma once
+#ifndef ZEROCOPY_OUTPUT_WRITER_INL_H_
+#error "Direct inclusion of this file is not allowed, include zerocopy_output_writer.h"
+// For the sake of sane code completion.
+#include "zerocopy_output_writer.h"
+#endif
+
+#include <util/system/yassert.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+char* TZeroCopyOutputStreamWriter::Current() const
+{
+ return Current_;
+}
+
+ui64 TZeroCopyOutputStreamWriter::RemainingBytes() const
+{
+ return RemainingBytes_;
+}
+
+void TZeroCopyOutputStreamWriter::Advance(size_t bytes)
+{
+ Y_VERIFY(bytes <= RemainingBytes_);
+ Current_ += bytes;
+ RemainingBytes_ -= bytes;
+}
+
+void TZeroCopyOutputStreamWriter::Write(const void* buffer, size_t length)
+{
+ if (length > RemainingBytes_) {
+ UndoRemaining();
+ Output_->Write(buffer, length);
+ TotalWrittenBlockSize_ += length;
+ ObtainNextBlock();
+ } else {
+ memcpy(Current_, buffer, length);
+ Advance(length);
+ }
+}
+
+ui64 TZeroCopyOutputStreamWriter::GetTotalWrittenSize() const
+{
+ return TotalWrittenBlockSize_ - RemainingBytes_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/zerocopy_output_writer.cpp b/library/cpp/skiff/zerocopy_output_writer.cpp
new file mode 100644
index 0000000000..49492b55a4
--- /dev/null
+++ b/library/cpp/skiff/zerocopy_output_writer.cpp
@@ -0,0 +1,38 @@
+#include "zerocopy_output_writer.h"
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TZeroCopyOutputStreamWriter::TZeroCopyOutputStreamWriter(IZeroCopyOutput* output)
+ : Output_(output)
+{
+ ObtainNextBlock();
+}
+
+TZeroCopyOutputStreamWriter::~TZeroCopyOutputStreamWriter()
+{
+ if (RemainingBytes_ > 0) {
+ UndoRemaining();
+ }
+}
+
+void TZeroCopyOutputStreamWriter::ObtainNextBlock()
+{
+ if (RemainingBytes_ > 0) {
+ UndoRemaining();
+ }
+ RemainingBytes_ = Output_->Next(&Current_);
+ TotalWrittenBlockSize_ += RemainingBytes_;
+}
+
+void TZeroCopyOutputStreamWriter::UndoRemaining()
+{
+ Output_->Undo(RemainingBytes_);
+ TotalWrittenBlockSize_ -= RemainingBytes_;
+ RemainingBytes_ = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
diff --git a/library/cpp/skiff/zerocopy_output_writer.h b/library/cpp/skiff/zerocopy_output_writer.h
new file mode 100644
index 0000000000..b0bccc5a63
--- /dev/null
+++ b/library/cpp/skiff/zerocopy_output_writer.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <util/stream/zerocopy_output.h>
+
+namespace NSkiff {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Simple wrapper around
+class TZeroCopyOutputStreamWriter
+ : private TNonCopyable
+{
+public:
+ explicit TZeroCopyOutputStreamWriter(IZeroCopyOutput* output);
+
+ ~TZeroCopyOutputStreamWriter();
+
+ Y_FORCE_INLINE char* Current() const;
+ Y_FORCE_INLINE ui64 RemainingBytes() const;
+ Y_FORCE_INLINE void Advance(size_t bytes);
+ void UndoRemaining();
+ Y_FORCE_INLINE void Write(const void* buffer, size_t length);
+ Y_FORCE_INLINE ui64 GetTotalWrittenSize() const;
+
+private:
+ void ObtainNextBlock();
+
+private:
+ IZeroCopyOutput* Output_;
+ char* Current_ = nullptr;
+ ui64 RemainingBytes_ = 0;
+ ui64 TotalWrittenBlockSize_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSkiff
+
+#define ZEROCOPY_OUTPUT_WRITER_INL_H_
+#include "zerocopy_output_writer-inl.h"
+#undef ZEROCOPY_OUTPUT_WRITER_INL_H_
diff --git a/library/cpp/testing/gtest/friend.h b/library/cpp/testing/gtest/friend.h
new file mode 100644
index 0000000000..551a218be0
--- /dev/null
+++ b/library/cpp/testing/gtest/friend.h
@@ -0,0 +1,5 @@
+#pragma once
+
+// Using absolute path to gtest headers in order to allow using friend.h without PEERDIRing gtest.
+#include <contrib/restricted/googletest/googletest/include/gtest/gtest_prod.h>
+
diff --git a/library/cpp/threading/blocking_queue/blocking_queue.cpp b/library/cpp/threading/blocking_queue/blocking_queue.cpp
new file mode 100644
index 0000000000..db199c80be
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/blocking_queue.cpp
@@ -0,0 +1,3 @@
+#include "blocking_queue.h"
+
+// just check compilability
diff --git a/library/cpp/threading/blocking_queue/blocking_queue.h b/library/cpp/threading/blocking_queue/blocking_queue.h
new file mode 100644
index 0000000000..48d3762f68
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/blocking_queue.h
@@ -0,0 +1,158 @@
+#pragma once
+
+#include <util/generic/deque.h>
+#include <util/generic/maybe.h>
+#include <util/generic/yexception.h>
+#include <util/system/condvar.h>
+#include <util/system/guard.h>
+#include <util/system/mutex.h>
+
+#include <utility>
+
+namespace NThreading {
+ ///
+ /// TBlockingQueue is a queue of elements of limited or unlimited size.
+ /// Queue provides Push and Pop operations that block if operation can't be executed
+ /// (queue is empty or maximum size is reached).
+ ///
+ /// Queue can be stopped, in that case all blocked operation will return `Nothing` / false.
+ ///
+ /// All operations are thread safe.
+ ///
+ ///
+ /// Example of usage:
+ /// TBlockingQueue<int> queue;
+ ///
+ /// ...
+ ///
+ /// // thread 1
+ /// queue.Push(42);
+ /// queue.Push(100500);
+ ///
+ /// ...
+ ///
+ /// // thread 2
+ /// while (TMaybe<int> number = queue.Pop()) {
+ /// ProcessNumber(number.GetRef());
+ /// }
+ template <class TElement>
+ class TBlockingQueue {
+ public:
+ ///
+ /// Creates blocking queue with given maxSize
+ /// if maxSize == 0 then queue is unlimited
+ TBlockingQueue(size_t maxSize)
+ : MaxSize(maxSize == 0 ? Max<size_t>() : maxSize)
+ , Stopped(false)
+ {
+ }
+
+ ///
+ /// Blocks until queue has some elements or queue is stopped or deadline is reached.
+ /// Returns `Nothing` if queue is stopped or deadline is reached.
+ /// Returns element otherwise.
+ TMaybe<TElement> Pop(TInstant deadline = TInstant::Max()) {
+ TGuard<TMutex> g(Lock);
+
+ const auto canPop = [this]() { return CanPop(); };
+ if (!CanPopCV.WaitD(Lock, deadline, canPop)) {
+ return Nothing();
+ }
+
+ if (Stopped && Queue.empty()) {
+ return Nothing();
+ }
+ TElement e = std::move(Queue.front());
+ Queue.pop_front();
+ CanPushCV.Signal();
+ return std::move(e);
+ }
+
+ TMaybe<TElement> Pop(TDuration duration) {
+ return Pop(TInstant::Now() + duration);
+ }
+
+ ///
+ /// Blocks until queue has space for new elements or queue is stopped or deadline is reached.
+ /// Returns false exception if queue is stopped and push failed or deadline is reached.
+ /// Pushes element to queue and returns true otherwise.
+ bool Push(const TElement& e, TInstant deadline = TInstant::Max()) {
+ return PushRef(e, deadline);
+ }
+
+ bool Push(TElement&& e, TInstant deadline = TInstant::Max()) {
+ return PushRef(std::move(e), deadline);
+ }
+
+ bool Push(const TElement& e, TDuration duration) {
+ return Push(e, TInstant::Now() + duration);
+ }
+
+ bool Push(TElement&& e, TDuration duration) {
+ return Push(std::move(e), TInstant::Now() + duration);
+ }
+
+ ///
+ /// Stops the queue, all blocked operations will be aborted.
+ void Stop() {
+ TGuard<TMutex> g(Lock);
+ Stopped = true;
+ CanPopCV.BroadCast();
+ CanPushCV.BroadCast();
+ }
+
+ ///
+ /// Checks whether queue is empty.
+ bool Empty() const {
+ TGuard<TMutex> g(Lock);
+ return Queue.empty();
+ }
+
+ ///
+ /// Returns size of the queue.
+ size_t Size() const {
+ TGuard<TMutex> g(Lock);
+ return Queue.size();
+ }
+
+ ///
+ /// Checks whether queue is stopped.
+ bool IsStopped() const {
+ TGuard<TMutex> g(Lock);
+ return Stopped;
+ }
+
+ private:
+ bool CanPush() const {
+ return Queue.size() < MaxSize || Stopped;
+ }
+
+ bool CanPop() const {
+ return !Queue.empty() || Stopped;
+ }
+
+ template <typename Ref>
+ bool PushRef(Ref e, TInstant deadline) {
+ TGuard<TMutex> g(Lock);
+ const auto canPush = [this]() { return CanPush(); };
+ if (!CanPushCV.WaitD(Lock, deadline, canPush)) {
+ return false;
+ }
+ if (Stopped) {
+ return false;
+ }
+ Queue.push_back(std::forward<TElement>(e));
+ CanPopCV.Signal();
+ return true;
+ }
+
+ private:
+ TMutex Lock;
+ TCondVar CanPopCV;
+ TCondVar CanPushCV;
+ TDeque<TElement> Queue;
+ size_t MaxSize;
+ bool Stopped;
+ };
+
+}
diff --git a/library/cpp/threading/blocking_queue/blocking_queue_ut.cpp b/library/cpp/threading/blocking_queue/blocking_queue_ut.cpp
new file mode 100644
index 0000000000..26e125279d
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/blocking_queue_ut.cpp
@@ -0,0 +1,211 @@
+#include "blocking_queue.h"
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/string/builder.h>
+#include <util/system/thread.h>
+
+namespace {
+ class TFunctionThread: public ISimpleThread {
+ public:
+ using TFunc = std::function<void()>;
+
+ private:
+ TFunc Func;
+
+ public:
+ TFunctionThread(const TFunc& func)
+ : Func(func)
+ {
+ }
+
+ void* ThreadProc() noexcept override {
+ Func();
+ return nullptr;
+ }
+ };
+
+}
+
+IOutputStream& operator<<(IOutputStream& o, const TMaybe<int>& val) {
+ if (val) {
+ o << "TMaybe<int>(" << val.GetRef() << ')';
+ } else {
+ o << "TMaybe<int>()";
+ }
+ return o;
+}
+
+Y_UNIT_TEST_SUITE(BlockingQueueTest) {
+ Y_UNIT_TEST(SimplePushPopTest) {
+ const size_t limit = 100;
+
+ NThreading::TBlockingQueue<int> queue(100);
+
+ for (int i = 0; i != limit; ++i) {
+ queue.Push(i);
+ }
+
+ for (int i = 0; i != limit; ++i) {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), i);
+ }
+
+ UNIT_ASSERT(queue.Empty());
+ }
+
+ Y_UNIT_TEST(SimpleStopTest) {
+ const size_t limit = 100;
+
+ NThreading::TBlockingQueue<int> queue(100);
+
+ for (int i = 0; i != limit; ++i) {
+ queue.Push(i);
+ }
+ queue.Stop();
+
+ bool ok = queue.Push(100500);
+ UNIT_ASSERT_VALUES_EQUAL(ok, false);
+
+ for (int i = 0; i != limit; ++i) {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), i);
+ }
+
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), TMaybe<int>());
+ }
+
+ Y_UNIT_TEST(BigPushPop) {
+ const int limit = 100000;
+
+ NThreading::TBlockingQueue<int> queue(10);
+
+ TFunctionThread pusher([&] {
+ for (int i = 0; i != limit; ++i) {
+ if (!queue.Push(i)) {
+ break;
+ }
+ }
+ });
+
+ pusher.Start();
+
+ try {
+ for (int i = 0; i != limit; ++i) {
+ size_t size = queue.Size();
+ UNIT_ASSERT_C(size <= 10, (TStringBuilder() << "Size exceeds 10: " << size).data());
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), i);
+ }
+ } catch (...) {
+ // gracefull shutdown of pusher thread if assertion fails
+ queue.Stop();
+ throw;
+ }
+
+ pusher.Join();
+ }
+
+ Y_UNIT_TEST(StopWhenMultiplePoppers) {
+ NThreading::TBlockingQueue<int> queue(10);
+ TFunctionThread popper1([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), TMaybe<int>());
+ });
+ TFunctionThread popper2([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Pop(), TMaybe<int>());
+ });
+ popper1.Start();
+ popper2.Start();
+
+ queue.Stop();
+
+ popper1.Join();
+ popper2.Join();
+ }
+
+ Y_UNIT_TEST(StopWhenMultiplePushers) {
+ NThreading::TBlockingQueue<int> queue(1);
+ queue.Push(1);
+ TFunctionThread pusher1([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Push(2), false);
+ });
+ TFunctionThread pusher2([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue.Push(2), false);
+ });
+ pusher1.Start();
+ pusher2.Start();
+
+ queue.Stop();
+
+ pusher1.Join();
+ pusher2.Join();
+ }
+
+ Y_UNIT_TEST(InterruptPopByDeadline) {
+ NThreading::TBlockingQueue<int> queue1(10);
+ NThreading::TBlockingQueue<int> queue2(10);
+
+ const auto popper1DeadLine = TInstant::Now();
+ const auto popper2DeadLine = TInstant::Now() + TDuration::Seconds(2);
+
+ TFunctionThread popper1([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue1.Pop(popper1DeadLine), TMaybe<int>());
+ UNIT_ASSERT_VALUES_EQUAL(queue1.IsStopped(), false);
+ });
+
+ TFunctionThread popper2([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue2.Pop(popper2DeadLine), 2);
+ UNIT_ASSERT_VALUES_EQUAL(queue2.IsStopped(), false);
+ });
+
+ popper1.Start();
+ popper2.Start();
+
+ Sleep(TDuration::Seconds(1));
+
+ queue1.Push(1);
+ queue2.Push(2);
+
+ Sleep(TDuration::Seconds(1));
+
+ queue1.Stop();
+ queue2.Stop();
+
+ popper1.Join();
+ popper2.Join();
+ }
+
+ Y_UNIT_TEST(InterruptPushByDeadline) {
+ NThreading::TBlockingQueue<int> queue1(1);
+ NThreading::TBlockingQueue<int> queue2(1);
+
+ queue1.Push(0);
+ queue2.Push(0);
+
+ const auto pusher1DeadLine = TInstant::Now();
+ const auto pusher2DeadLine = TInstant::Now() + TDuration::Seconds(2);
+
+ TFunctionThread pusher1([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue1.Push(1, pusher1DeadLine), false);
+ UNIT_ASSERT_VALUES_EQUAL(queue1.IsStopped(), false);
+ });
+
+ TFunctionThread pusher2([&] {
+ UNIT_ASSERT_VALUES_EQUAL(queue2.Push(2, pusher2DeadLine), true);
+ UNIT_ASSERT_VALUES_EQUAL(queue2.IsStopped(), false);
+ });
+
+ pusher1.Start();
+ pusher2.Start();
+
+ Sleep(TDuration::Seconds(1));
+
+ queue1.Pop();
+ queue2.Pop();
+
+ Sleep(TDuration::Seconds(1));
+
+ queue1.Stop();
+ queue2.Stop();
+
+ pusher1.Join();
+ pusher2.Join();
+ }
+}
diff --git a/library/cpp/threading/blocking_queue/ut/ya.make b/library/cpp/threading/blocking_queue/ut/ya.make
new file mode 100644
index 0000000000..50f220d552
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/ut/ya.make
@@ -0,0 +1,7 @@
+UNITTEST_FOR(library/cpp/threading/blocking_queue)
+
+SRCS(
+ blocking_queue_ut.cpp
+)
+
+END()
diff --git a/library/cpp/threading/blocking_queue/ya.make b/library/cpp/threading/blocking_queue/ya.make
new file mode 100644
index 0000000000..ce1104c1c9
--- /dev/null
+++ b/library/cpp/threading/blocking_queue/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+SRCS(
+ blocking_queue.cpp
+)
+
+END()
+
+RECURSE_FOR_TESTS(ut)
diff --git a/library/cpp/threading/cron/cron.cpp b/library/cpp/threading/cron/cron.cpp
new file mode 100644
index 0000000000..e7c1c59735
--- /dev/null
+++ b/library/cpp/threading/cron/cron.cpp
@@ -0,0 +1,69 @@
+#include "cron.h"
+
+#include <library/cpp/deprecated/atomic/atomic.h>
+
+#include <util/system/thread.h>
+#include <util/system/event.h>
+
+using namespace NCron;
+
+namespace {
+ struct TPeriodicHandle: public IHandle {
+ inline TPeriodicHandle(TJob job, TDuration interval, const TString& threadName)
+ : Job(job)
+ , Interval(interval)
+ , Done(false)
+ {
+ TThread::TParams params(DoRun, this);
+ if (!threadName.empty()) {
+ params.SetName(threadName);
+ }
+ Thread = MakeHolder<TThread>(params);
+ Thread->Start();
+ }
+
+ static inline void* DoRun(void* data) noexcept {
+ ((TPeriodicHandle*)data)->Run();
+
+ return nullptr;
+ }
+
+ inline void Run() noexcept {
+ while (true) {
+ Job();
+
+ Event.WaitT(Interval);
+
+ if (AtomicGet(Done)) {
+ return;
+ }
+ }
+ }
+
+ ~TPeriodicHandle() override {
+ AtomicSet(Done, true);
+ Event.Signal();
+ Thread->Join();
+ }
+
+ TJob Job;
+ TDuration Interval;
+ TManualEvent Event;
+ TAtomic Done;
+ THolder<TThread> Thread;
+ };
+}
+
+IHandlePtr NCron::StartPeriodicJob(TJob job) {
+ return NCron::StartPeriodicJob(job, TDuration::Seconds(0), "");
+}
+
+IHandlePtr NCron::StartPeriodicJob(TJob job, TDuration interval) {
+ return NCron::StartPeriodicJob(job, interval, "");
+}
+
+IHandlePtr NCron::StartPeriodicJob(TJob job, TDuration interval, const TString& threadName) {
+ return new TPeriodicHandle(job, interval, threadName);
+}
+
+IHandle::~IHandle() = default;
diff --git a/library/cpp/threading/cron/cron.h b/library/cpp/threading/cron/cron.h
new file mode 100644
index 0000000000..77fa40c5e2
--- /dev/null
+++ b/library/cpp/threading/cron/cron.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <util/generic/ptr.h>
+#include <util/generic/function.h>
+#include <util/datetime/base.h>
+
+namespace NCron {
+ struct IHandle {
+ virtual ~IHandle();
+ };
+
+ using TJob = std::function<void()>;
+ using IHandlePtr = TAutoPtr<IHandle>;
+
+ IHandlePtr StartPeriodicJob(TJob job);
+ IHandlePtr StartPeriodicJob(TJob job, TDuration interval);
+ IHandlePtr StartPeriodicJob(TJob job, TDuration interval, const TString& threadName);
+}
diff --git a/library/cpp/threading/cron/ya.make b/library/cpp/threading/cron/ya.make
new file mode 100644
index 0000000000..ead272e837
--- /dev/null
+++ b/library/cpp/threading/cron/ya.make
@@ -0,0 +1,11 @@
+LIBRARY()
+
+SRCS(
+ cron.cpp
+)
+
+PEERDIR(
+ library/cpp/deprecated/atomic
+)
+
+END()
diff --git a/library/cpp/type_info/Readme.md b/library/cpp/type_info/Readme.md
new file mode 100644
index 0000000000..e4501c8c11
--- /dev/null
+++ b/library/cpp/type_info/Readme.md
@@ -0,0 +1,9 @@
+# TI — unified in-memory representation for Common Yandex Typesystem
+
+Common Yandex Typesystem is a type system used across data storage and processing technologies developed by Yandex. A list of systems that support it includes YT, YDB, RTMR, YQL and others.
+
+The Type Info library provides classes that represent types from Common Yandex Typesystem. It allows constructing in-memory representations of types, inspecting said representations, combining them to derive new types, as well as serializing and deserializing them.
+
+For those familiar with Protobuf, Type Info implements the concept of message descriptors for Yandex type system.
+
+Here you can find open issues: https://st.yandex-team.ru/YT/order:updated:false/filter?tags=type_info&resolution=empty() \ No newline at end of file
diff --git a/library/cpp/type_info/builder.cpp b/library/cpp/type_info/builder.cpp
new file mode 100644
index 0000000000..008e0af754
--- /dev/null
+++ b/library/cpp/type_info/builder.cpp
@@ -0,0 +1,458 @@
+#include "builder.h"
+
+#include "type_factory.h"
+
+namespace NTi {
+ TStructBuilderRaw::TStructBuilderRaw(IPoolTypeFactory& factory) noexcept
+ : Factory_(&factory)
+ {
+ }
+
+ TStructBuilderRaw::TStructBuilderRaw(IPoolTypeFactory& factory, TStructTypePtr prototype) noexcept
+ : TStructBuilderRaw(factory, prototype.Get())
+ {
+ }
+
+ TStructBuilderRaw::TStructBuilderRaw(IPoolTypeFactory& factory, const TStructType* prototype) noexcept
+ : TStructBuilderRaw(factory)
+ {
+ Members_.reserve(prototype->GetMembers().size());
+
+ if (prototype->GetFactory() == Factory_) {
+ // members are in the same factory -- can reuse them.
+ for (auto member : prototype->GetMembers()) {
+ Members_.push_back(member);
+ }
+ } else {
+ // members are in a different factory -- should copy them.
+ for (auto member : prototype->GetMembers()) {
+ AddMember(member.GetName(), member.GetTypeRaw());
+ }
+ }
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::SetName(TMaybe<TStringBuf> name) & noexcept {
+ Name_ = Factory_->AllocateStringMaybe(name);
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::SetName(TMaybe<TStringBuf> name) && noexcept {
+ return std::move(SetName(name));
+ }
+
+ bool TStructBuilderRaw::HasName() const noexcept {
+ return Name_.Defined();
+ }
+
+ TMaybe<TStringBuf> TStructBuilderRaw::GetName() const noexcept {
+ return Name_;
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::Reserve(size_t size) & noexcept {
+ Members_.reserve(size);
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::Reserve(size_t size) && noexcept {
+ return std::move(Reserve(size));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMember(TStringBuf name, TTypePtr type) & noexcept {
+ return AddMember(name, type.Get());
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMember(TStringBuf name, TTypePtr type) && noexcept {
+ return std::move(AddMember(name, type));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMember(TStringBuf name, const TType* type) & noexcept {
+ Members_.emplace_back(Factory_->AllocateString(name), Factory_->Own(type));
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMember(TStringBuf name, const TType* type) && noexcept {
+ return std::move(AddMember(name, type));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMemberName(TStringBuf name) & noexcept {
+ PendingMemberName_ = Factory_->AllocateString(name);
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMemberName(TStringBuf name) && noexcept {
+ return std::move(AddMemberName(name));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::DiscardMemberName() & noexcept {
+ PendingMemberName_.Clear();
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::DiscardMemberName() && noexcept {
+ return std::move(DiscardMemberName());
+ }
+
+ bool TStructBuilderRaw::HasMemberName() const noexcept {
+ return PendingMemberName_.Defined();
+ }
+
+ TMaybe<TStringBuf> TStructBuilderRaw::GetMemberName() const noexcept {
+ return PendingMemberName_;
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMemberType(TTypePtr type) & noexcept {
+ return AddMemberType(type.Get());
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMemberType(TTypePtr type) && noexcept {
+ return std::move(AddMemberType(type));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMemberType(const TType* type) & noexcept {
+ PendingMemberType_ = Factory_->Own(type);
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMemberType(const TType* type) && noexcept {
+ return std::move(AddMemberType(type));
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::DiscardMemberType() & noexcept {
+ PendingMemberType_.Clear();
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::DiscardMemberType() && noexcept {
+ return std::move(DiscardMemberType());
+ }
+
+ bool TStructBuilderRaw::HasMemberType() const noexcept {
+ return PendingMemberType_.Defined();
+ }
+
+ TMaybe<const TType*> TStructBuilderRaw::GetMemberType() const noexcept {
+ return PendingMemberType_;
+ }
+
+ bool TStructBuilderRaw::CanAddMember() const noexcept {
+ return HasMemberName() && HasMemberType();
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::AddMember() & noexcept {
+ Y_VERIFY(CanAddMember());
+ Members_.emplace_back(*PendingMemberName_, *PendingMemberType_);
+ DiscardMember();
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::AddMember() && noexcept {
+ return std::move(AddMember());
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::DiscardMember() & noexcept {
+ DiscardMemberName();
+ DiscardMemberType();
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::DiscardMember() && noexcept {
+ return std::move(DiscardMember());
+ }
+
+ TStructType::TMembers TStructBuilderRaw::GetMembers() const noexcept {
+ return Members_;
+ }
+
+ TStructBuilderRaw& TStructBuilderRaw::Reset() & noexcept {
+ Name_ = {};
+ Members_.clear();
+ return *this;
+ }
+
+ TStructBuilderRaw TStructBuilderRaw::Reset() && noexcept {
+ return std::move(Reset());
+ }
+
+ TStructTypePtr TStructBuilderRaw::Build() {
+ return BuildRaw()->AsPtr();
+ }
+
+ const TStructType* TStructBuilderRaw::BuildRaw() {
+ return DoBuildRaw(Name_);
+ }
+
+ TVariantTypePtr TStructBuilderRaw::BuildVariant() {
+ return BuildVariantRaw()->AsPtr();
+ }
+
+ const TVariantType* TStructBuilderRaw::BuildVariantRaw() {
+ return Factory_->New<TVariantType>(Nothing(), Name_, DoBuildRaw(Nothing()));
+ }
+
+ const TStructType* TStructBuilderRaw::DoBuildRaw(TMaybe<TStringBuf> name) {
+ auto members = Factory_->NewArray<TStructType::TMember>(Members_.size(), [this](TStructType::TMember* member, size_t i) {
+ new (member) TStructType::TMember(Members_[i]);
+ });
+
+ auto sortedMembersArray = Factory_->AllocateArrayFor<size_t>(Members_.size());
+ auto sortedMembers = TArrayRef(sortedMembersArray, members.size());
+ TStructType::MakeSortedMembers(members, sortedMembers);
+
+ return Factory_->New<TStructType>(Nothing(), name, members, sortedMembers);
+ }
+
+ TTupleBuilderRaw::TTupleBuilderRaw(IPoolTypeFactory& factory) noexcept
+ : Factory_(&factory)
+ {
+ }
+
+ TTupleBuilderRaw::TTupleBuilderRaw(IPoolTypeFactory& factory, TTupleTypePtr prototype) noexcept
+ : TTupleBuilderRaw(factory, prototype.Get())
+ {
+ }
+
+ TTupleBuilderRaw::TTupleBuilderRaw(IPoolTypeFactory& factory, const TTupleType* prototype) noexcept
+ : TTupleBuilderRaw(factory)
+ {
+ Elements_.reserve(prototype->GetElements().size());
+
+ if (prototype->GetFactory() == Factory_) {
+ // elements are in the same factory -- can reuse them
+ for (auto element : prototype->GetElements()) {
+ Elements_.push_back(element);
+ }
+ } else {
+ // items are in a different factory -- should copy them
+ for (auto element : prototype->GetElements()) {
+ AddElement(element.GetTypeRaw());
+ }
+ }
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::SetName(TMaybe<TStringBuf> name) & noexcept {
+ Name_ = Factory_->AllocateStringMaybe(name);
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::SetName(TMaybe<TStringBuf> name) && noexcept {
+ return std::move(SetName(name));
+ }
+
+ bool TTupleBuilderRaw::HasName() const noexcept {
+ return Name_.Defined();
+ }
+
+ TMaybe<TStringBuf> TTupleBuilderRaw::GetName() const noexcept {
+ return Name_;
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::Reserve(size_t size) & noexcept {
+ Elements_.reserve(size);
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::Reserve(size_t size) && noexcept {
+ return std::move(Reserve(size));
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::AddElement(TTypePtr type) & noexcept {
+ return AddElement(type.Get());
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::AddElement(TTypePtr type) && noexcept {
+ return std::move(AddElement(type));
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::AddElement(const TType* type) & noexcept {
+ Elements_.emplace_back(Factory_->Own(type));
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::AddElement(const TType* type) && noexcept {
+ return std::move(AddElement(type));
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::AddElementType(TTypePtr type) & noexcept {
+ return AddElementType(type.Get());
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::AddElementType(TTypePtr type) && noexcept {
+ return std::move(AddElementType(type));
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::AddElementType(const TType* type) & noexcept {
+ PendingElementType_ = Factory_->Own(type);
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::AddElementType(const TType* type) && noexcept {
+ return std::move(AddElementType(type));
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::DiscardElementType() & noexcept {
+ PendingElementType_.Clear();
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::DiscardElementType() && noexcept {
+ return std::move(DiscardElementType());
+ }
+
+ bool TTupleBuilderRaw::HasElementType() const noexcept {
+ return PendingElementType_.Defined();
+ }
+
+ TMaybe<const TType*> TTupleBuilderRaw::GetElementType() const noexcept {
+ return PendingElementType_;
+ }
+
+ bool TTupleBuilderRaw::CanAddElement() const noexcept {
+ return HasElementType();
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::AddElement() & noexcept {
+ Y_VERIFY(CanAddElement());
+ Elements_.emplace_back(*PendingElementType_);
+ DiscardElement();
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::AddElement() && noexcept {
+ return std::move(AddElement());
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::DiscardElement() & noexcept {
+ DiscardElementType();
+ return *this;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::DiscardElement() && noexcept {
+ return std::move(DiscardElement());
+ }
+
+ TTupleBuilderRaw& TTupleBuilderRaw::Reset() & noexcept {
+ Name_ = {};
+ Elements_.clear();
+ return *this;
+ }
+
+ TTupleType::TElements TTupleBuilderRaw::GetElements() const noexcept {
+ return Elements_;
+ }
+
+ TTupleBuilderRaw TTupleBuilderRaw::Reset() && noexcept {
+ return std::move(Reset());
+ }
+
+ TTupleTypePtr TTupleBuilderRaw::Build() {
+ return BuildRaw()->AsPtr();
+ }
+
+ const TTupleType* TTupleBuilderRaw::BuildRaw() {
+ return DoBuildRaw(Name_);
+ }
+
+ TVariantTypePtr TTupleBuilderRaw::BuildVariant() {
+ return BuildVariantRaw()->AsPtr();
+ }
+
+ const TVariantType* TTupleBuilderRaw::BuildVariantRaw() {
+ return Factory_->New<TVariantType>(Nothing(), Name_, DoBuildRaw(Nothing()));
+ }
+
+ const TTupleType* TTupleBuilderRaw::DoBuildRaw(TMaybe<TStringBuf> name) {
+ auto items = Factory_->NewArray<TTupleType::TElement>(Elements_.size(), [this](TTupleType::TElement* element, size_t i) {
+ new (element) TTupleType::TElement(Elements_[i]);
+ });
+
+ return Factory_->New<TTupleType>(Nothing(), name, items);
+ }
+
+ TTaggedBuilderRaw::TTaggedBuilderRaw(IPoolTypeFactory& factory) noexcept
+ : Factory_(&factory)
+ {
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::SetTag(TStringBuf tag) & noexcept {
+ Tag_ = Factory_->AllocateString(tag);
+ return *this;
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::SetTag(TStringBuf tag) && noexcept {
+ return std::move(SetTag(tag));
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::DiscardTag() & noexcept {
+ Tag_.Clear();
+ return *this;
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::DiscardTag() && noexcept {
+ return std::move(DiscardTag());
+ }
+
+ bool TTaggedBuilderRaw::HasTag() const noexcept {
+ return Tag_.Defined();
+ }
+
+ TMaybe<TStringBuf> TTaggedBuilderRaw::GetTag() const noexcept {
+ return Tag_;
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::SetItem(TTypePtr type) & noexcept {
+ return SetItem(type.Get());
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::SetItem(TTypePtr type) && noexcept {
+ return std::move(SetItem(std::move(type)));
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::SetItem(const TType* type) & noexcept {
+ Item_ = Factory_->Own(type);
+ return *this;
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::SetItem(const TType* type) && noexcept {
+ return std::move(SetItem(type));
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::DiscardItem() & noexcept {
+ Item_.Clear();
+ return *this;
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::DiscardItem() && noexcept {
+ return std::move(DiscardItem());
+ }
+
+ bool TTaggedBuilderRaw::HasItem() const noexcept {
+ return Item_.Defined();
+ }
+
+ TMaybe<const TType*> TTaggedBuilderRaw::GetItem() const noexcept {
+ return Item_;
+ }
+
+ bool TTaggedBuilderRaw::CanBuild() const noexcept {
+ return HasTag() && HasItem();
+ }
+
+ TTaggedBuilderRaw& TTaggedBuilderRaw::Reset() & noexcept {
+ DiscardTag();
+ DiscardItem();
+ return *this;
+ }
+
+ TTaggedBuilderRaw TTaggedBuilderRaw::Reset() && noexcept {
+ return std::move(Reset());
+ }
+
+ TTaggedTypePtr TTaggedBuilderRaw::Build() {
+ return BuildRaw()->AsPtr();
+ }
+
+ const TTaggedType* TTaggedBuilderRaw::BuildRaw() {
+ Y_VERIFY(CanBuild());
+ return Factory_->New<TTaggedType>(Nothing(), *Item_, *Tag_);
+ }
+}
diff --git a/library/cpp/type_info/builder.h b/library/cpp/type_info/builder.h
new file mode 100644
index 0000000000..faae45ab51
--- /dev/null
+++ b/library/cpp/type_info/builder.h
@@ -0,0 +1,346 @@
+#pragma once
+
+//! @file builder.h
+//!
+//! Builders help with creating complex types piece-by-piece.
+
+#include <util/generic/vector.h>
+
+#include "type.h"
+
+#include "fwd.h"
+
+namespace NTi {
+ /// An interface for building structs using the raw interface (via memory-pool-based factory).
+ ///
+ /// This interface allows allocating structs piece-by-piece. You can feed it struct items, one-by-one, and they'll
+ /// be copied into the memory pool before the struct is created. This way, you don't have to allocate heap memory
+ /// to temporarily store item names or types.
+ ///
+ /// Note: this builder doesn't own the underlying factory.
+ class TStructBuilderRaw {
+ public:
+ /// Create a new builder with the given factory.
+ TStructBuilderRaw(IPoolTypeFactory& factory) noexcept;
+
+ /// Create a new builder with the given factory and add fields from the given struct.
+ /// Note that struct name is not copied to the builder.
+ TStructBuilderRaw(IPoolTypeFactory& factory, TStructTypePtr prototype) noexcept;
+ TStructBuilderRaw(IPoolTypeFactory& factory, const TStructType* prototype) noexcept;
+
+ /// Set a new struct name.
+ ///
+ /// Note that the name is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite name from the first call, but will not remove it from the memory pool.
+ TStructBuilderRaw& SetName(TMaybe<TStringBuf> name) & noexcept;
+ TStructBuilderRaw SetName(TMaybe<TStringBuf> name) && noexcept;
+
+ /// Check if there's a struct name set.
+ bool HasName() const noexcept;
+
+ /// Get a struct name.
+ ///
+ /// Name was copied to the factory's memory pool and will live as long as the factory lives.
+ TMaybe<TStringBuf> GetName() const noexcept;
+
+ /// Reserve some place in the underlying vector that collects struct items.
+ TStructBuilderRaw& Reserve(size_t size) & noexcept;
+ TStructBuilderRaw Reserve(size_t size) && noexcept;
+
+ /// Append a new struct member.
+ TStructBuilderRaw& AddMember(TStringBuf name, TTypePtr type) & noexcept;
+ TStructBuilderRaw AddMember(TStringBuf name, TTypePtr type) && noexcept;
+ TStructBuilderRaw& AddMember(TStringBuf name, const TType* type) & noexcept;
+ TStructBuilderRaw AddMember(TStringBuf name, const TType* type) && noexcept;
+
+ /// Partial member creation interface.
+ ///
+ /// This interface allows building individual struct items piece-by-piece. You can pass member's name
+ /// and copy it to the pool without passing its type. For example:
+ ///
+ /// ```
+ /// auto builder = TStructBuilderRaw(factory);
+ /// builder.AddMemberName("name"); // add name for a new member.
+ /// builder.AddMemberType(type); // add type for a new member.
+ /// builder.AddMember(); // use added name and type to construct and append an member.
+ /// ```
+ ///
+ /// This interface is useful when you can't store member name for a long time. For example, if you building
+ /// a parser, at some point have an member name, but you don't have an member type yet. Then you can add member name
+ /// when you have it, and add member type later.
+ ///
+ /// @{
+ //-
+ /// Set name for pending member.
+ ///
+ /// Note that the name is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite name from the first call, but will not remove it from the memory pool.
+ TStructBuilderRaw& AddMemberName(TStringBuf name) & noexcept;
+ TStructBuilderRaw AddMemberName(TStringBuf name) && noexcept;
+
+ /// Unset name for pending member.
+ ///
+ /// Note that member name is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TStructBuilderRaw& DiscardMemberName() & noexcept;
+ TStructBuilderRaw DiscardMemberName() && noexcept;
+
+ /// Check if there's a name set for pending member.
+ bool HasMemberName() const noexcept;
+
+ /// Get name for pending member.
+ ///
+ /// Name was copied to the factory's memory pool and will live as long as the factory lives.
+ TMaybe<TStringBuf> GetMemberName() const noexcept;
+
+ /// Set type for pending member.
+ ///
+ /// Note that the type is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite type from the first call, but will not remove it from the memory pool.
+ TStructBuilderRaw& AddMemberType(TTypePtr type) & noexcept;
+ TStructBuilderRaw AddMemberType(TTypePtr type) && noexcept;
+ TStructBuilderRaw& AddMemberType(const TType* type) & noexcept;
+ TStructBuilderRaw AddMemberType(const TType* type) && noexcept;
+
+ /// Unset type for pending member.
+ ///
+ /// Note that member type is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TStructBuilderRaw& DiscardMemberType() & noexcept;
+ TStructBuilderRaw DiscardMemberType() && noexcept;
+
+ /// Check if there's a type set for pending member.
+ bool HasMemberType() const noexcept;
+
+ /// Get type for pending member.
+ TMaybe<const TType*> GetMemberType() const noexcept;
+
+ /// Check if both name and type are set for pending member.
+ bool CanAddMember() const noexcept;
+
+ /// Use data added via `AddMemberName` and `AddMemberType` to construct a new struct member
+ /// and append it to this builder. This function panics if there's no name or no type set for pending member.
+ TStructBuilderRaw& AddMember() & noexcept;
+ TStructBuilderRaw AddMember() && noexcept;
+
+ /// Discard all data added via `AddMemberName` and `AddMemberType` functions.
+ ///
+ /// Note that member name and type are copied to the factory right away. If you discard them via this method,
+ /// they will not be removed from the memory pool.
+ TStructBuilderRaw& DiscardMember() & noexcept;
+ TStructBuilderRaw DiscardMember() && noexcept;
+
+ /// @}
+
+ /// Get immutable list of items that've been added to this builder.
+ /// The returned object is invalidated when this builder dies or when `AddMember` is called.
+ TStructType::TMembers GetMembers() const noexcept;
+
+ /// Reset struct name and items.
+ TStructBuilderRaw& Reset() & noexcept;
+ TStructBuilderRaw Reset() && noexcept;
+
+ /// Create a new struct type using name and items from this builder.
+ TStructTypePtr Build();
+
+ /// Like `Build`, but returns a raw pointer.
+ const TStructType* BuildRaw();
+
+ /// Create a new variant over struct using items from this builder.
+ TVariantTypePtr BuildVariant();
+
+ /// Like `BuildVariant`, but returns a raw pointer.
+ const TVariantType* BuildVariantRaw();
+
+ private:
+ const TStructType* DoBuildRaw(TMaybe<TStringBuf> name);
+
+ private:
+ IPoolTypeFactory* Factory_;
+ TMaybe<TStringBuf> Name_;
+ TVector<TStructType::TMember> Members_;
+ TMaybe<TStringBuf> PendingMemberName_;
+ TMaybe<const TType*> PendingMemberType_;
+ };
+
+ /// An interface for building tuples using the raw interface (via memory-pool-based factory).
+ ///
+ /// Note: this builder doesn't own the underlying factory.
+ class TTupleBuilderRaw {
+ public:
+ /// Create a new builder with the given factory.
+ TTupleBuilderRaw(IPoolTypeFactory& factory) noexcept;
+
+ /// Create a new builder with the given factory and add fields from the given tuple.
+ /// Note that tuple name is not copied to the builder.
+ TTupleBuilderRaw(IPoolTypeFactory& factory, TTupleTypePtr prototype) noexcept;
+ TTupleBuilderRaw(IPoolTypeFactory& factory, const TTupleType* prototype) noexcept;
+
+ /// Set a new tuple name.
+ /// Note that the name is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite name from the first call, but will not remove it from the memory pool.
+ TTupleBuilderRaw& SetName(TMaybe<TStringBuf> name) & noexcept;
+ TTupleBuilderRaw SetName(TMaybe<TStringBuf> name) && noexcept;
+
+ /// Check if there's a tuple name set.
+ bool HasName() const noexcept;
+
+ /// Get a tuple name.
+ ///
+ /// Name was copied to the factory's memory pool and will live as long as the factory lives.
+ TMaybe<TStringBuf> GetName() const noexcept;
+
+ /// Reserve some place in the underlying vector that collects tuple items.
+ TTupleBuilderRaw& Reserve(size_t size) & noexcept;
+ TTupleBuilderRaw Reserve(size_t size) && noexcept;
+
+ /// Append a new tuple item.
+ TTupleBuilderRaw& AddElement(TTypePtr type) & noexcept;
+ TTupleBuilderRaw AddElement(TTypePtr type) && noexcept;
+ TTupleBuilderRaw& AddElement(const TType* type) & noexcept;
+ TTupleBuilderRaw AddElement(const TType* type) && noexcept;
+
+ /// Partial item creation interface.
+ ///
+ /// This interface allows building individual tuple items piece-by-piece. It mirrors the same type of interface
+ /// in the struct builder.
+ ///
+ /// @{
+ //-
+ /// Set type for pending item.
+ ///
+ /// Note that the type is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite type from the first call, but will not remove it from the memory pool.
+ TTupleBuilderRaw& AddElementType(TTypePtr type) & noexcept;
+ TTupleBuilderRaw AddElementType(TTypePtr type) && noexcept;
+ TTupleBuilderRaw& AddElementType(const TType* type) & noexcept;
+ TTupleBuilderRaw AddElementType(const TType* type) && noexcept;
+
+ /// Unset type for pending item.
+ ///
+ /// Note that item type is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TTupleBuilderRaw& DiscardElementType() & noexcept;
+ TTupleBuilderRaw DiscardElementType() && noexcept;
+
+ /// Check if there's a type set for pending item.
+ bool HasElementType() const noexcept;
+
+ /// Get type for pending item.
+ TMaybe<const TType*> GetElementType() const noexcept;
+
+ /// Check if type is set for pending item.
+ bool CanAddElement() const noexcept;
+
+ /// Use data added via `AddElementType` to construct a new tuple item and append it to this builder.
+ /// This function panics if there's no type set for pending item.
+ TTupleBuilderRaw& AddElement() & noexcept;
+ TTupleBuilderRaw AddElement() && noexcept;
+
+ /// Discard all data added via `AddElementType` function.
+ ///
+ /// Note that item type is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TTupleBuilderRaw& DiscardElement() & noexcept;
+ TTupleBuilderRaw DiscardElement() && noexcept;
+
+ /// @}
+
+ /// Get immutable list of items that've been added to this builder.
+ /// The returned object is invalidated when this builder dies or when `AddElement` is called.
+ TTupleType::TElements GetElements() const noexcept;
+
+ /// Reset tuple name and items.
+ TTupleBuilderRaw& Reset() & noexcept;
+ TTupleBuilderRaw Reset() && noexcept;
+
+ /// Create a new tuple type using name and items from this builder.
+ TTupleTypePtr Build();
+
+ /// Like `Build`, but returns a raw pointer.
+ const TTupleType* BuildRaw();
+
+ /// Create a new variant over tuple using items from this builder.
+ TVariantTypePtr BuildVariant();
+
+ /// Like `BuildVariant`, but returns a raw pointer.
+ const TVariantType* BuildVariantRaw();
+
+ private:
+ const TTupleType* DoBuildRaw(TMaybe<TStringBuf> name);
+
+ private:
+ IPoolTypeFactory* Factory_;
+ TMaybe<TStringBuf> Name_;
+ TVector<TTupleType::TElement> Elements_;
+ TMaybe<const TType*> PendingElementType_;
+ };
+
+ /// An interface for building tagged types using the raw interface (via memory-pool-based factory).
+ ///
+ /// Note: this builder doesn't own the underlying factory.
+ class TTaggedBuilderRaw {
+ public:
+ /// Create a new builder with the given factory.
+ TTaggedBuilderRaw(IPoolTypeFactory& factory) noexcept;
+
+ /// Set a new tag.
+ ///
+ /// Note that the tag is copied to the factory right away. If you call this function twice, second call will
+ /// overwrite tag from the first call, but will not remove it from the memory pool.
+ TTaggedBuilderRaw& SetTag(TStringBuf tag) & noexcept;
+ TTaggedBuilderRaw SetTag(TStringBuf tag) && noexcept;
+
+ /// Unset tag.
+ ///
+ /// Note that tag is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TTaggedBuilderRaw& DiscardTag() & noexcept;
+ TTaggedBuilderRaw DiscardTag() && noexcept;
+
+ /// Check if a tag is set.
+ bool HasTag() const noexcept;
+
+ /// Get a tag.
+ ///
+ /// The tag was copied to the factory's memory pool and will live as long as the factory lives.
+ TMaybe<TStringBuf> GetTag() const noexcept;
+
+ /// Set type that's being tagged.
+ TTaggedBuilderRaw& SetItem(TTypePtr type) & noexcept;
+ TTaggedBuilderRaw SetItem(TTypePtr type) && noexcept;
+ TTaggedBuilderRaw& SetItem(const TType* type) & noexcept;
+ TTaggedBuilderRaw SetItem(const TType* type) && noexcept;
+
+ /// Unset item type.
+ ///
+ /// Note that item type is copied to the factory right away. If you discard it via this method,
+ /// it will not be removed from the memory pool.
+ TTaggedBuilderRaw& DiscardItem() & noexcept;
+ TTaggedBuilderRaw DiscardItem() && noexcept;
+
+ /// Check if there's an item type set.
+ bool HasItem() const noexcept;
+
+ /// Get item type.
+ TMaybe<const TType*> GetItem() const noexcept;
+
+ /// Check if there's both name and item type set.
+ bool CanBuild() const noexcept;
+
+ /// Discard both tag and item.
+ TTaggedBuilderRaw& Reset() & noexcept;
+ TTaggedBuilderRaw Reset() && noexcept;
+
+ /// Create a new tagged type using name and item from this builder.
+ TTaggedTypePtr Build();
+
+ /// Like `Build`, but returns a raw pointer.
+ const TTaggedType* BuildRaw();
+
+ private:
+ IPoolTypeFactory* Factory_;
+ TMaybe<TStringBuf> Tag_;
+ TMaybe<const TType*> Item_;
+ };
+}
diff --git a/library/cpp/type_info/error.cpp b/library/cpp/type_info/error.cpp
new file mode 100644
index 0000000000..fe9ecf1d9f
--- /dev/null
+++ b/library/cpp/type_info/error.cpp
@@ -0,0 +1 @@
+#include "error.h"
diff --git a/library/cpp/type_info/error.h b/library/cpp/type_info/error.h
new file mode 100644
index 0000000000..26fd8853f9
--- /dev/null
+++ b/library/cpp/type_info/error.h
@@ -0,0 +1,33 @@
+#pragma once
+
+//! @file error.h
+//!
+//! All error classes that one can encounter when working with type info library.
+
+#include <util/generic/yexception.h>
+
+namespace NTi {
+ /// Base class for all exceptions that arise when working with Type Info library.
+ class TException: public yexception {
+ };
+
+ /// Type Info API used in an unintended way.
+ class TApiException: public TException {
+ };
+
+ /// Attempting to create an illegal type.
+ ///
+ /// For example, this exception is raised when attempting to create a struct with non-unique item names.
+ class TIllegalTypeException: public TException {
+ };
+
+ /// Type deserializer got an invalid input.
+ ///
+ /// See `TType::Serialize()` and `TType::Deserialize()` for more info on type serialization/deserialization.
+ class TDeserializationException: public TException {
+ };
+
+ /// No such item in type.
+ class TItemNotFound: public TException {
+ };
+}
diff --git a/library/cpp/type_info/fwd.h b/library/cpp/type_info/fwd.h
new file mode 100644
index 0000000000..644932f677
--- /dev/null
+++ b/library/cpp/type_info/fwd.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include <util/generic/fwd.h>
+
+namespace NTi {
+ class ITypeFactoryInternal;
+
+ class TNamedTypeBuilderRaw;
+ class TStructBuilderRaw;
+ class TTupleBuilderRaw;
+ class TTaggedBuilderRaw;
+
+ class ITypeFactory;
+ using ITypeFactoryPtr = TIntrusivePtr<ITypeFactory>;
+
+ class IPoolTypeFactory;
+ using IPoolTypeFactoryPtr = TIntrusivePtr<IPoolTypeFactory>;
+
+ class TType;
+ using TTypePtr = TIntrusiveConstPtr<TType>;
+
+ class TVoidType;
+ using TVoidTypePtr = TIntrusiveConstPtr<TVoidType>;
+
+ class TNullType;
+ using TNullTypePtr = TIntrusiveConstPtr<TNullType>;
+
+ class TPrimitiveType;
+ using TPrimitiveTypePtr = TIntrusiveConstPtr<TPrimitiveType>;
+
+ class TBoolType;
+ using TBoolTypePtr = TIntrusiveConstPtr<TBoolType>;
+
+ class TInt8Type;
+ using TInt8TypePtr = TIntrusiveConstPtr<TInt8Type>;
+
+ class TInt16Type;
+ using TInt16TypePtr = TIntrusiveConstPtr<TInt16Type>;
+
+ class TInt32Type;
+ using TInt32TypePtr = TIntrusiveConstPtr<TInt32Type>;
+
+ class TInt64Type;
+ using TInt64TypePtr = TIntrusiveConstPtr<TInt64Type>;
+
+ class TUint8Type;
+ using TUint8TypePtr = TIntrusiveConstPtr<TUint8Type>;
+
+ class TUint16Type;
+ using TUint16TypePtr = TIntrusiveConstPtr<TUint16Type>;
+
+ class TUint32Type;
+ using TUint32TypePtr = TIntrusiveConstPtr<TUint32Type>;
+
+ class TUint64Type;
+ using TUint64TypePtr = TIntrusiveConstPtr<TUint64Type>;
+
+ class TFloatType;
+ using TFloatTypePtr = TIntrusiveConstPtr<TFloatType>;
+
+ class TDoubleType;
+ using TDoubleTypePtr = TIntrusiveConstPtr<TDoubleType>;
+
+ class TStringType;
+ using TStringTypePtr = TIntrusiveConstPtr<TStringType>;
+
+ class TUtf8Type;
+ using TUtf8TypePtr = TIntrusiveConstPtr<TUtf8Type>;
+
+ class TDateType;
+ using TDateTypePtr = TIntrusiveConstPtr<TDateType>;
+
+ class TDatetimeType;
+ using TDatetimeTypePtr = TIntrusiveConstPtr<TDatetimeType>;
+
+ class TTimestampType;
+ using TTimestampTypePtr = TIntrusiveConstPtr<TTimestampType>;
+
+ class TTzDateType;
+ using TTzDateTypePtr = TIntrusiveConstPtr<TTzDateType>;
+
+ class TTzDatetimeType;
+ using TTzDatetimeTypePtr = TIntrusiveConstPtr<TTzDatetimeType>;
+
+ class TTzTimestampType;
+ using TTzTimestampTypePtr = TIntrusiveConstPtr<TTzTimestampType>;
+
+ class TIntervalType;
+ using TIntervalTypePtr = TIntrusiveConstPtr<TIntervalType>;
+
+ class TDecimalType;
+ using TDecimalTypePtr = TIntrusiveConstPtr<TDecimalType>;
+
+ class TJsonType;
+ using TJsonTypePtr = TIntrusiveConstPtr<TJsonType>;
+
+ class TYsonType;
+ using TYsonTypePtr = TIntrusiveConstPtr<TYsonType>;
+
+ class TUuidType;
+ using TUuidTypePtr = TIntrusiveConstPtr<TUuidType>;
+
+ class TOptionalType;
+ using TOptionalTypePtr = TIntrusiveConstPtr<TOptionalType>;
+
+ class TListType;
+ using TListTypePtr = TIntrusiveConstPtr<TListType>;
+
+ class TDictType;
+ using TDictTypePtr = TIntrusiveConstPtr<TDictType>;
+
+ class TStructType;
+ using TStructTypePtr = TIntrusiveConstPtr<TStructType>;
+
+ class TTupleType;
+ using TTupleTypePtr = TIntrusiveConstPtr<TTupleType>;
+
+ class TVariantType;
+ using TVariantTypePtr = TIntrusiveConstPtr<TVariantType>;
+
+ class TTaggedType;
+ using TTaggedTypePtr = TIntrusiveConstPtr<TTaggedType>;
+}
diff --git a/library/cpp/type_info/test-data/bad-types.txt b/library/cpp/type_info/test-data/bad-types.txt
new file mode 100644
index 0000000000..c8f3120f63
--- /dev/null
+++ b/library/cpp/type_info/test-data/bad-types.txt
@@ -0,0 +1,229 @@
+#
+# The format of this file is described in README.txt
+#
+# Each test case contains 3 fields:
+# - Yson representation (that cannot be parsed to type).
+# - Expected error description.
+# - Path in yson that we expect to see in error.
+#
+# Suggested approach to writing test:
+# 1. Try to deserialize type from yson (1st field).
+# 2. Ensure that error is raised.
+# 3. (Optionally) ensure that error description matches (contains) 2nd field.
+# 4. (Optionally) ensure that error description contains path from 3rd field
+
+5 :: type must be either a string or a map :: ;;
+
+"" :: unknown type "" :: ;;
+
+"int" :: unknown type "int" :: ;;
+
+# Type names must be in lowercase.
+"Int32" :: unknown type "Int32" :: ;;
+
+{typename=int32} :: missing required key "type_name" :: ;;
+
+{type_name=5} :: "type_name" must contain a string :: ;;
+
+#
+# Decimal
+#
+
+{
+ type_name=decimal;
+ precision=3;
+} :: missing required key "scale" :: ;;
+
+{
+ type_name=decimal;
+ scale=3;
+} :: missing required key "precision" :: ;;
+
+#
+# Optional
+#
+
+{
+ type_name=optional;
+} :: missing required key "item" :: ;;
+
+{
+ item=int32;
+} :: missing required key "type_name" :: ;;
+
+#
+# List
+#
+
+{
+ type_name=list;
+} :: missing required key "item" :: ;;
+
+#
+# Struct
+#
+
+{
+ type_name=struct;
+} :: missing required key "members" :: ;;
+
+{
+ type_name=struct;
+ members=5;
+} :: "members" must contain a list :: ;;
+
+{
+ type_name=struct;
+ members=[
+ {name=foo; type=int32;};
+ 5;
+ ];
+} :: "members" must contain a list of maps :: ;;
+
+{
+ type_name=struct;
+ members=[
+ {type=int32;};
+ ];
+} :: missing required key "name" :: ;;
+
+{
+ type_name=struct;
+ members=[
+ {name=foo;};
+ ];
+} :: missing required key "type" :: ;;
+
+{
+ type_name=struct;
+ members=[
+ {name=foo; type=int32};
+ {name=bar; type=5};
+ ];
+} :: type must be either a string or a map :: /members/1/type ;;
+
+{
+ type_name=struct;
+ members=[
+ {name=4; type=int32};
+ ];
+} :: "name" must contain a string :: ;;
+
+{
+ type_name=struct;
+ elements=[
+ {name=4; type=int32};
+ ];
+} :: missing required key "members" :: ;;
+
+#
+# Tuple
+#
+
+{
+ type_name=tuple;
+} :: missing required key "elements" :: ;;
+
+{
+ type_name=tuple;
+ elements=5;
+} :: "elements" must contain a list :: ;;
+
+{
+ type_name=tuple;
+ elements=[
+ {type=int32;};
+ 5;
+ ];
+} :: "elements" must contain a list of maps :: ;;
+
+{
+ type_name=tuple;
+ elements=[
+ {};
+ ];
+} :: missing required key "type" :: /elements/0 ;;
+
+{
+ type_name=tuple;
+ elements=[
+ {type=5};
+ ];
+} :: type must be either a string or a map :: /elements/1/type ;;
+
+{
+ type_name=tuple;
+ members=[
+ {name=foo; type=int32};
+ ];
+} :: missing required key "elements" :: ;;
+
+#
+# Variant
+#
+
+# We don't specify exception message here because in C++ library that message is not good and we don't want to canonize it
+# Though fixing it is not that easy too.
+{
+ type_name=variant;
+ members=[
+ {name=foo; type=int32};
+ ];
+ elements=[
+ {type=int8};
+ ]
+} :: :: ;;
+
+{
+ type_name=variant;
+} :: missing both keys "members" and "elements" :: ;;
+
+#
+# Dict
+#
+{
+ type_name=dict;
+ key=string;
+} :: missing required key "value" :: ;;
+
+{
+ type_name=dict;
+ value=string;
+} :: missing required key "key" :: ;;
+
+{
+ type_name=dict;
+ value=string;
+} :: missing required key "key" :: ;;
+
+{
+ type_name=dict;
+ key=5;
+ value=string;
+} :: type must be either a string or a map :: /key ;;
+
+{
+ type_name=dict;
+ key=string;
+ value=5;
+} :: type must be either a string or a map :: /value ;;
+
+#
+# Tagged
+#
+
+{
+ type_name=tagged;
+ item=string;
+} :: missing required key "tag" :: ;;
+
+{
+ type_name=tagged;
+ tag=string;
+} :: missing required key "item" :: ;;
+
+{
+ type_name=tagged;
+ tag=5;
+ item=string;
+} :: "tag" must contain a string :: /tag ;;
diff --git a/library/cpp/type_info/test-data/good-types.txt b/library/cpp/type_info/test-data/good-types.txt
new file mode 100644
index 0000000000..cb082707b6
--- /dev/null
+++ b/library/cpp/type_info/test-data/good-types.txt
@@ -0,0 +1,478 @@
+#
+# The format of this file is described in README.txt
+#
+# Each test case contains 2 fields:
+# - Yson reporesentation of the type.
+# - String representation of the type.
+#
+# Suggested approach to writing test:
+# 1. Try to deserialize type from yson (1st field).
+# 2. Check that text representation of parsed type matches 2nd field.
+# 3. Serialize type to yson and deserialize it from it.
+# 4. Check that typef from 1. and 3. are equal.
+
+# Integer
+int8 :: Int8 ;;
+int16 :: Int16 ;;
+int32 :: Int32 ;;
+int64 :: Int64 ;;
+{type_name=int8} :: Int8 ;;
+{type_name=int16} :: Int16 ;;
+{type_name=int32} :: Int32 ;;
+{type_name=int64} :: Int64 ;;
+
+# Unsigned integer
+uint8 :: Uint8 ;;
+uint16 :: Uint16 ;;
+uint32 :: Uint32 ;;
+uint64 :: Uint64 ;;
+{type_name=uint8} :: Uint8 ;;
+{type_name=uint16} :: Uint16 ;;
+{type_name=uint32} :: Uint32 ;;
+{type_name=uint64} :: Uint64 ;;
+
+# Floating
+float :: Float ;;
+double :: Double ;;
+{type_name=float} :: Float ;;
+{type_name=double} :: Double ;;
+
+# Strings
+string :: String ;;
+utf8 :: Utf8 ;;
+{type_name=string} :: String ;;
+{type_name=utf8} :: Utf8 ;;
+
+# Time
+date :: Date ;;
+datetime :: Datetime ;;
+timestamp :: Timestamp ;;
+tz_date :: TzDate ;;
+tz_datetime :: TzDatetime ;;
+tz_timestamp :: TzTimestamp ;;
+interval :: Interval ;;
+{type_name=date} :: Date ;;
+{type_name=datetime} :: Datetime ;;
+{type_name=timestamp} :: Timestamp ;;
+{type_name=tz_date} :: TzDate ;;
+{type_name=tz_datetime} :: TzDatetime ;;
+{type_name=tz_timestamp} :: TzTimestamp ;;
+{type_name=interval} :: Interval ;;
+
+# Singular
+void :: Void ;;
+null :: Null ;;
+{type_name=void} :: Void ;;
+{type_name=null} :: Null ;;
+
+# UUID
+uuid :: Uuid ;;
+{type_name=uuid} :: Uuid ;;
+
+# Json / Yson
+yson :: Yson ;;
+json :: Json ;;
+{type_name=yson} :: Yson ;;
+{type_name=json} :: Json ;;
+
+{
+ type_name=string;
+ unknown_key=bar;
+} :: String ;;
+
+# Decimal
+{
+ type_name=decimal;
+ precision=3;
+ scale=2;
+} :: Decimal(3, 2) ;;
+
+{
+ type_name=decimal;
+ precision=3;
+ scale=2;
+ unknown_column=ha;
+} :: Decimal(3, 2) ;;
+
+#
+# Optional
+#
+{
+ type_name=optional;
+ item=string;
+} :: Optional<String> ;;
+
+{
+ type_name=optional;
+ item={
+ type_name=string;
+ }
+} :: Optional<String> ;;
+
+{
+ type_name=optional;
+ item={
+ type_name=optional;
+ item={
+ type_name=list;
+ item={
+ type_name=decimal;
+ precision=10;
+ scale=5;
+ }
+
+ }
+ }
+} :: Optional<Optional<List<Decimal(10, 5)>>> ;;
+
+{
+ type_name=optional;
+ item={
+ type_name=tagged;
+ item=int32;
+ tag="foo";
+ };
+} :: Optional<Tagged<Int32, 'foo'>> ;;
+
+{
+ type_name=optional;
+ item={
+ type_name=string;
+ };
+ unknown_column=ha;
+} :: Optional<String> ;;
+
+#
+# List
+#
+{
+ type_name=list;
+ item=string;
+} :: List<String> ;;
+
+{
+ type_name=list;
+ item={
+ type_name=string;
+ }
+} :: List<String> ;;
+
+{
+ type_name=list;
+ item={
+ type_name=list;
+ item={
+ type_name=optional;
+ item=string;
+ }
+ };
+} :: List<List<Optional<String>>> ;;
+
+{
+ type_name=list;
+ item={
+ type_name=tagged;
+ item=int32;
+ tag="foo";
+ };
+} :: List<Tagged<Int32, 'foo'>> ;;
+
+{
+ type_name=list;
+ item={
+ type_name=string;
+ };
+ unknown_column=ha;
+} :: List<String> ;;
+
+#
+# Dict
+#
+
+{
+ type_name=dict;
+ key=int32;
+ value=string;
+} :: Dict<Int32, String> ;;
+
+{
+ type_name=dict;
+ key={
+ type_name=optional;
+ item=string;
+ };
+ value={
+ type_name=list;
+ item={
+ type_name=int32;
+ }
+ };
+} :: Dict<Optional<String>, List<Int32>> ;;
+
+{
+ type_name=dict;
+ key={
+ type_name=tagged;
+ item=int32;
+ tag="foo";
+ };
+ value={
+ type_name=tagged;
+ item=string;
+ tag="bar";
+ };
+} :: Dict<Tagged<Int32, 'foo'>, Tagged<String, 'bar'>> ;;
+
+{
+ type_name=dict;
+ key=int32;
+ value=string;
+ unknown_column=ha;
+} :: Dict<Int32, String> ;;
+
+#
+# Struct
+#
+
+{
+ type_name=struct;
+ members=[];
+} :: Struct<> ;;
+
+{
+ type_name=struct;
+ members=[
+ {
+ name=foo;
+ type=int32;
+ };
+ {
+ name=bar;
+ type={
+ type_name=optional;
+ item=string;
+ };
+ };
+ ];
+} :: Struct<'foo': Int32, 'bar': Optional<String>> ;;
+
+{
+ type_name=struct;
+ members=[
+ {
+ name=foo;
+ type={
+ type_name=tagged;
+ item=string;
+ tag=foo;
+ };
+ };
+ ];
+} :: Struct<'foo': Tagged<String, 'foo'>> ;;
+
+{
+ type_name=struct;
+ unknown_column=ha;
+ members=[
+ {
+ unknown_column=ha;
+ name=foo;
+ type=int32;
+ };
+ {
+ name=bar;
+ type={
+ type_name=optional;
+ item=string;
+ };
+ };
+ ];
+} :: Struct<'foo': Int32, 'bar': Optional<String>> ;;
+
+#
+# Tuple
+#
+
+{
+ type_name=tuple;
+ elements=[];
+} :: Tuple<> ;;
+
+{
+ type_name=tuple;
+ elements=[
+ {
+ type=int32;
+ };
+ {
+ type={
+ type_name=optional;
+ item=string;
+ };
+ };
+ ];
+} :: Tuple<Int32, Optional<String>> ;;
+
+{
+ type_name=tuple;
+ elements=[
+ {
+ type={
+ type_name=tagged;
+ item=string;
+ tag=foo;
+ };
+ };
+ ];
+} :: Tuple<Tagged<String, 'foo'>> ;;
+
+{
+ type_name=tuple;
+ unknown_column=ha;
+ elements=[
+ {
+ type=int32;
+ unknown_column=ha;
+ };
+ {
+ type={
+ type_name=optional;
+ item=string;
+ };
+ };
+ ];
+} :: Tuple<Int32, Optional<String>> ;;
+
+#
+# Variant
+#
+
+{
+ type_name=variant;
+ elements=[
+ {
+ type=int32;
+ };
+ {
+ type={type_name=string};
+ };
+ ];
+} :: Variant<Int32, String> ;;
+
+{
+ type_name=variant;
+ members=[
+ {
+ name=foo;
+ type=int32;
+ };
+ {
+ name=bar;
+ type={
+ type_name=optional;
+ item=string;
+ };
+ };
+ ];
+} :: Variant<'foo': Int32, 'bar': Optional<String>> ;;
+
+{
+ type_name=variant;
+ elements=[
+ {
+ type={
+ type_name=tagged;
+ item=string;
+ tag=foo;
+ }
+ };
+ ];
+} :: Variant<Tagged<String, 'foo'>> ;;
+
+{
+ type_name=variant;
+ members=[
+ {
+ name=bar;
+ type={
+ type_name=tagged;
+ item=string;
+ tag=foo;
+ };
+ };
+ ];
+} :: Variant<'bar': Tagged<String, 'foo'>> ;;
+
+{
+ type_name=variant;
+ unknown_column=ha;
+ elements=[
+ {
+ type=int32;
+ unknown_column=ha;
+ };
+ {
+ type={
+ type_name=string;
+ unknown_column=ha;
+ };
+ unknown_column=ha;
+ };
+ ];
+} :: Variant<Int32, String> ;;
+
+{
+ type_name=variant;
+ unknown_column=ha;
+ members=[
+ {
+ unknown_column=ha;
+ name=foo;
+ type=int32;
+ };
+ {
+ name=bar;
+ type={
+ type_name=optional;
+ unknown_column=ha;
+ item=string;
+ };
+ };
+ ];
+} :: Variant<'foo': Int32, 'bar': Optional<String>> ;;
+
+#
+# Tagged
+#
+
+{
+ type_name=tagged;
+ item=string;
+ tag="image/png"
+} :: Tagged<String, 'image/png'> ;;
+
+{
+ type_name=tagged;
+ item={
+ type_name=optional;
+ item=string;
+ };
+ tag="image/png"
+} :: Tagged<Optional<String>, 'image/png'> ;;
+
+{
+ type_name=tagged;
+ item={
+ type_name=tagged;
+ item=string;
+ tag=foo;
+ };
+ tag=bar;
+} :: Tagged<Tagged<String, 'foo'>, 'bar'> ;;
+
+{
+ type_name=tagged;
+ item=string;
+ unknown_column=ha;
+ tag="image/png"
+} :: Tagged<String, 'image/png'> ;;
diff --git a/library/cpp/type_info/type.cpp b/library/cpp/type_info/type.cpp
new file mode 100644
index 0000000000..cee58a0a79
--- /dev/null
+++ b/library/cpp/type_info/type.cpp
@@ -0,0 +1,1662 @@
+#include "type.h"
+
+#include "type_factory.h"
+#include "type_equivalence.h"
+
+#include <util/generic/overloaded.h>
+
+#include <util/digest/murmur.h>
+#include <util/generic/hash_set.h>
+#include <util/string/escape.h>
+
+namespace {
+ inline ui64 Hash(NTi::ETypeName type) {
+ return IntHash(static_cast<ui64>(type));
+ }
+
+ inline ui64 Hash(ui64 value, ui64 seed) {
+ return MurmurHash(&value, sizeof(value), seed);
+ }
+
+ inline ui64 Hash(TStringBuf string, ui64 seed) {
+ seed = ::Hash(string.size(), seed);
+ return MurmurHash(string.data(), string.size(), seed);
+ }
+
+ inline ui64 Hash(TMaybe<TStringBuf> string, ui64 seed) {
+ if (string.Defined()) {
+ return MurmurHash(string->data(), string->size(), seed);
+ } else {
+ return seed;
+ }
+ }
+
+ TString Quote(TStringBuf s) {
+ TString result;
+ result.push_back('\'');
+ result += EscapeC(s);
+ result.push_back('\'');
+ return result;
+ }
+}
+
+namespace NTi {
+ TType::TType(TMaybe<ui64> hash, ETypeName typeName) noexcept
+ : TypeName_(typeName)
+ , HasHash_(hash.Defined())
+ , Hash_(hash.GetOrElse(0))
+ {
+ }
+
+ ui64 TType::CalculateHash() const noexcept {
+ return ::Hash(TypeName_);
+ }
+
+ TMaybe<ui64> TType::GetHashRaw() const noexcept {
+ if (HasHash_.load(std::memory_order_seq_cst)) {
+ return Hash_.load(std::memory_order_seq_cst);
+ } else {
+ return Nothing();
+ }
+ }
+
+ ui64 TType::GetHash() const {
+ if (HasHash_.load(std::memory_order_seq_cst)) {
+ return Hash_.load(std::memory_order_seq_cst);
+ } else {
+ ui64 hash = VisitRaw([](const auto* type) {
+ return type->CalculateHash();
+ });
+ Hash_.store(hash, std::memory_order_seq_cst);
+ HasHash_.store(true, std::memory_order_seq_cst);
+ return hash;
+ }
+ }
+
+ TTypePtr TType::StripTags() const noexcept {
+ return StripTagsRaw()->AsPtr();
+ }
+
+ const TType* TType::StripTagsRaw() const noexcept {
+ auto type = this;
+ while (type->IsTagged()) {
+ type = type->AsTaggedRaw()->GetItemTypeRaw();
+ }
+ return type;
+ }
+
+ TTypePtr TType::StripOptionals() const noexcept {
+ return StripOptionalsRaw()->AsPtr();
+ }
+
+ const TType* TType::StripOptionalsRaw() const noexcept {
+ auto type = this;
+ while (type->IsOptional()) {
+ type = type->AsOptionalRaw()->GetItemTypeRaw();
+ }
+ return type;
+ }
+
+ TTypePtr TType::StripTagsAndOptionals() const noexcept {
+ return StripTagsAndOptionalsRaw()->AsPtr();
+ }
+
+ const TType* TType::StripTagsAndOptionalsRaw() const noexcept {
+ auto type = this;
+ while (type->IsTagged() || type->IsOptional()) {
+ if (type->IsTagged()) {
+ type = type->AsTaggedRaw()->GetItemTypeRaw();
+ } else {
+ type = type->AsOptionalRaw()->GetItemTypeRaw();
+ }
+ }
+ return type;
+ }
+
+ const TType* TType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return VisitRaw([&factory](const auto* type) -> const NTi::TType* {
+ return type->Clone(factory);
+ });
+ }
+
+ void TType::Drop(ITypeFactoryInternal& factory) noexcept {
+ VisitRaw([&factory](const auto* type) {
+ using T = std::remove_const_t<std::remove_pointer_t<decltype(type)>>;
+ return const_cast<T*>(type)->Drop(factory);
+ });
+ }
+
+ ITypeFactoryInternal* TType::GetFactory() const noexcept {
+ size_t manager_or_rc = FactoryOrRc_.load(std::memory_order_relaxed);
+ if (IsRc(manager_or_rc)) {
+ return NPrivate::GetDefaultHeapFactory();
+ } else if (IsFactory(manager_or_rc)) {
+ return CastToFactory(manager_or_rc);
+ } else {
+ return nullptr;
+ }
+ }
+
+ void TType::SetFactory(ITypeFactoryInternal* factory) noexcept {
+ if (factory == NPrivate::GetDefaultHeapFactory()) {
+ FactoryOrRc_.store(0b1u, std::memory_order_release);
+ } else {
+ FactoryOrRc_.store(CastFromFactory(factory), std::memory_order_release);
+ }
+ }
+
+ ITypeFactoryInternal& TType::FactoryInternal(ITypeFactory& factory) noexcept {
+ return static_cast<ITypeFactoryInternal&>(factory);
+ }
+
+ template <bool RefFactory>
+ void TType::RefImpl() noexcept {
+ size_t factoryOrRc = FactoryOrRc_.load(std::memory_order_relaxed);
+ if (Y_LIKELY(IsRc(factoryOrRc))) {
+ FactoryOrRc_.fetch_add(0b10u, std::memory_order_acq_rel);
+ } else if (Y_LIKELY(IsFactory(factoryOrRc))) {
+ auto factory = CastToFactory(factoryOrRc);
+ if (RefFactory) {
+ factory->Ref();
+ }
+ factory->RefType(this);
+ }
+ }
+
+ template void TType::RefImpl<true>() noexcept;
+ template void TType::RefImpl<false>() noexcept;
+
+ template <bool UnRefFactory>
+ void TType::UnRefImpl() noexcept {
+ size_t factoryOrRc = FactoryOrRc_.load(std::memory_order_relaxed);
+ if (Y_LIKELY(IsRc(factoryOrRc))) {
+ size_t rc = FactoryOrRc_.fetch_sub(0b10u, std::memory_order_acq_rel);
+ if (rc == 0b11u) {
+ auto factory = NPrivate::GetDefaultHeapFactory();
+ Drop(*factory);
+ factory->Delete(this);
+ }
+ } else if (Y_LIKELY(IsFactory(factoryOrRc))) {
+ auto factory = CastToFactory(factoryOrRc);
+ factory->UnRefType(this);
+ if (UnRefFactory) {
+ factory->UnRef();
+ }
+ }
+ }
+
+ template void TType::UnRefImpl<true>() noexcept;
+ template void TType::UnRefImpl<false>() noexcept;
+
+ template <bool DecRefFactory>
+ void TType::DecRefImpl() noexcept {
+ size_t factoryOrRc = FactoryOrRc_.load(std::memory_order_relaxed);
+ if (Y_LIKELY(IsRc(factoryOrRc))) {
+ size_t rc = FactoryOrRc_.fetch_sub(2, std::memory_order_acq_rel);
+ if (rc == 2) {
+ Y_FAIL("DecRef isn't supposed to drop");
+ }
+ } else if (Y_LIKELY(IsFactory(factoryOrRc))) {
+ auto factory = CastToFactory(factoryOrRc);
+ factory->DecRefType(this);
+ if (DecRefFactory) {
+ factory->DecRef();
+ }
+ }
+ }
+
+ template void TType::DecRefImpl<true>() noexcept;
+ template void TType::DecRefImpl<false>() noexcept;
+
+ long TType::RefCountImpl() const noexcept {
+ size_t factoryOrRc = FactoryOrRc_.load(std::memory_order_relaxed);
+ if (Y_LIKELY(IsRc(factoryOrRc))) {
+ return factoryOrRc >> 1u;
+ } else if (Y_LIKELY(IsFactory(factoryOrRc))) {
+ return CastToFactory(factoryOrRc)->RefCountType(this);
+ } else {
+ return 0;
+ }
+ }
+
+ template <typename T, typename TCtor>
+ const T* TType::Cached(const T* type, ITypeFactoryInternal& factory, TCtor&& ctor) {
+ const TType* result = factory.LookupCache(type);
+
+ if (result == nullptr) {
+ result = std::forward<TCtor>(ctor)();
+ factory.SaveCache(result);
+ }
+
+ Y_VERIFY(result->GetTypeName() == type->GetTypeName());
+ Y_VERIFY_DEBUG(result->GetHash() == type->GetHash());
+ return static_cast<const T*>(result);
+ }
+
+ bool operator==(const TType& lhs, const TType& rhs) {
+ Y_VERIFY(&lhs);
+ Y_VERIFY(&rhs);
+ return NEq::TStrictlyEqual().IgnoreHash(&lhs, &rhs);
+ }
+
+ bool operator!=(const TType& lhs, const TType& rhs)
+ {
+ return !(lhs == rhs);
+ }
+
+ TVoidType::TVoidType()
+ : TType({}, ETypeName::Void)
+ {
+ }
+
+ TVoidTypePtr TVoidType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TVoidType* TVoidType::InstanceRaw() {
+ static auto singleton = TVoidType();
+ return &singleton;
+ }
+
+ const TVoidType* TVoidType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TVoidType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TNullType::TNullType()
+ : TType({}, ETypeName::Null)
+ {
+ }
+
+ TNullTypePtr TNullType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TNullType* TNullType::InstanceRaw() {
+ static auto singleton = TNullType();
+ return &singleton;
+ }
+
+ const TNullType* TNullType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TNullType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TPrimitiveType::TPrimitiveType(TMaybe<ui64> hash, EPrimitiveTypeName primitiveTypeName) noexcept
+ : TType(hash, ToTypeName(primitiveTypeName))
+ {
+ }
+
+ TBoolType::TBoolType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Bool)
+ {
+ }
+
+ TBoolTypePtr TBoolType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TBoolType* TBoolType::InstanceRaw() {
+ static auto singleton = TBoolType();
+ return &singleton;
+ }
+
+ const TBoolType* TBoolType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TBoolType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TInt8Type::TInt8Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Int8)
+ {
+ }
+
+ TInt8TypePtr TInt8Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TInt8Type* TInt8Type::InstanceRaw() {
+ static auto singleton = TInt8Type();
+ return &singleton;
+ }
+
+ const TInt8Type* TInt8Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TInt8Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TInt16Type::TInt16Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Int16)
+ {
+ }
+
+ TInt16TypePtr TInt16Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TInt16Type* TInt16Type::InstanceRaw() {
+ static auto singleton = TInt16Type();
+ return &singleton;
+ }
+
+ const TInt16Type* TInt16Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TInt16Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TInt32Type::TInt32Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Int32)
+ {
+ }
+
+ TInt32TypePtr TInt32Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TInt32Type* TInt32Type::InstanceRaw() {
+ static auto singleton = TInt32Type();
+ return &singleton;
+ }
+
+ const TInt32Type* TInt32Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TInt32Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TInt64Type::TInt64Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Int64)
+ {
+ }
+
+ TInt64TypePtr TInt64Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TInt64Type* TInt64Type::InstanceRaw() {
+ static auto singleton = TInt64Type();
+ return &singleton;
+ }
+
+ const TInt64Type* TInt64Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TInt64Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUint8Type::TUint8Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Uint8)
+ {
+ }
+
+ TUint8TypePtr TUint8Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUint8Type* TUint8Type::InstanceRaw() {
+ static auto singleton = TUint8Type();
+ return &singleton;
+ }
+
+ const TUint8Type* TUint8Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUint8Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUint16Type::TUint16Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Uint16)
+ {
+ }
+
+ TUint16TypePtr TUint16Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUint16Type* TUint16Type::InstanceRaw() {
+ static auto singleton = TUint16Type();
+ return &singleton;
+ }
+
+ const TUint16Type* TUint16Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUint16Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUint32Type::TUint32Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Uint32)
+ {
+ }
+
+ TUint32TypePtr TUint32Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUint32Type* TUint32Type::InstanceRaw() {
+ static auto singleton = TUint32Type();
+ return &singleton;
+ }
+
+ const TUint32Type* TUint32Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUint32Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUint64Type::TUint64Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Uint64)
+ {
+ }
+
+ TUint64TypePtr TUint64Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUint64Type* TUint64Type::InstanceRaw() {
+ static auto singleton = TUint64Type();
+ return &singleton;
+ }
+
+ const TUint64Type* TUint64Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUint64Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TFloatType::TFloatType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Float)
+ {
+ }
+
+ TFloatTypePtr TFloatType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TFloatType* TFloatType::InstanceRaw() {
+ static auto singleton = TFloatType();
+ return &singleton;
+ }
+
+ const TFloatType* TFloatType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TFloatType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TDoubleType::TDoubleType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Double)
+ {
+ }
+
+ TDoubleTypePtr TDoubleType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TDoubleType* TDoubleType::InstanceRaw() {
+ static auto singleton = TDoubleType();
+ return &singleton;
+ }
+
+ const TDoubleType* TDoubleType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TDoubleType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TStringType::TStringType()
+ : TPrimitiveType({}, EPrimitiveTypeName::String)
+ {
+ }
+
+ TStringTypePtr TStringType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TStringType* TStringType::InstanceRaw() {
+ static auto singleton = TStringType();
+ return &singleton;
+ }
+
+ const TStringType* TStringType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TStringType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUtf8Type::TUtf8Type()
+ : TPrimitiveType({}, EPrimitiveTypeName::Utf8)
+ {
+ }
+
+ TUtf8TypePtr TUtf8Type::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUtf8Type* TUtf8Type::InstanceRaw() {
+ static auto singleton = TUtf8Type();
+ return &singleton;
+ }
+
+ const TUtf8Type* TUtf8Type::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUtf8Type::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TDateType::TDateType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Date)
+ {
+ }
+
+ TDateTypePtr TDateType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TDateType* TDateType::InstanceRaw() {
+ static auto singleton = TDateType();
+ return &singleton;
+ }
+
+ const TDateType* TDateType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TDateType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TDatetimeType::TDatetimeType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Datetime)
+ {
+ }
+
+ TDatetimeTypePtr TDatetimeType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TDatetimeType* TDatetimeType::InstanceRaw() {
+ static auto singleton = TDatetimeType();
+ return &singleton;
+ }
+
+ const TDatetimeType* TDatetimeType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TDatetimeType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TTimestampType::TTimestampType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Timestamp)
+ {
+ }
+
+ TTimestampTypePtr TTimestampType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TTimestampType* TTimestampType::InstanceRaw() {
+ static auto singleton = TTimestampType();
+ return &singleton;
+ }
+
+ const TTimestampType* TTimestampType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TTimestampType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TTzDateType::TTzDateType()
+ : TPrimitiveType({}, EPrimitiveTypeName::TzDate)
+ {
+ }
+
+ TTzDateTypePtr TTzDateType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TTzDateType* TTzDateType::InstanceRaw() {
+ static auto singleton = TTzDateType();
+ return &singleton;
+ }
+
+ const TTzDateType* TTzDateType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TTzDateType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TTzDatetimeType::TTzDatetimeType()
+ : TPrimitiveType({}, EPrimitiveTypeName::TzDatetime)
+ {
+ }
+
+ TTzDatetimeTypePtr TTzDatetimeType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TTzDatetimeType* TTzDatetimeType::InstanceRaw() {
+ static auto singleton = TTzDatetimeType();
+ return &singleton;
+ }
+
+ const TTzDatetimeType* TTzDatetimeType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TTzDatetimeType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TTzTimestampType::TTzTimestampType()
+ : TPrimitiveType({}, EPrimitiveTypeName::TzTimestamp)
+ {
+ }
+
+ TTzTimestampTypePtr TTzTimestampType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TTzTimestampType* TTzTimestampType::InstanceRaw() {
+ static auto singleton = TTzTimestampType();
+ return &singleton;
+ }
+
+ const TTzTimestampType* TTzTimestampType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TTzTimestampType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TIntervalType::TIntervalType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Interval)
+ {
+ }
+
+ TIntervalTypePtr TIntervalType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TIntervalType* TIntervalType::InstanceRaw() {
+ static auto singleton = TIntervalType();
+ return &singleton;
+ }
+
+ const TIntervalType* TIntervalType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TIntervalType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TDecimalType::TDecimalType(TMaybe<ui64> hash, ui8 precision, ui8 scale) noexcept
+ : TPrimitiveType(hash, EPrimitiveTypeName::Decimal)
+ , Precision_(precision)
+ , Scale_(scale)
+ {
+ }
+
+ TDecimalTypePtr TDecimalType::Create(ITypeFactory& factory, ui8 precision, ui8 scale) {
+ return CreateRaw(factory, precision, scale)->AsPtr();
+ }
+
+ const TDecimalType* TDecimalType::CreateRaw(ITypeFactory& factory, ui8 precision, ui8 scale) {
+ Y_ENSURE_EX(
+ scale <= precision,
+ TIllegalTypeException() << "decimal scale " << (ui32)scale
+ << " should be no greater than decimal precision " << (ui32)precision);
+
+ return TDecimalType({}, precision, scale).Clone(FactoryInternal(factory));
+ }
+
+ ui64 TDecimalType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Precision_, hash);
+ hash = ::Hash(Scale_, hash);
+ return hash;
+ }
+
+ const TDecimalType* TDecimalType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TDecimalType* {
+ auto hash = GetHashRaw();
+ auto precision = Precision_;
+ auto scale = Scale_;
+ return factory.New<TDecimalType>(hash, precision, scale);
+ });
+ }
+
+ void TDecimalType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TJsonType::TJsonType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Json)
+ {
+ }
+
+ TJsonTypePtr TJsonType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TJsonType* TJsonType::InstanceRaw() {
+ static auto singleton = TJsonType();
+ return &singleton;
+ }
+
+ const TJsonType* TJsonType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TJsonType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TYsonType::TYsonType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Yson)
+ {
+ }
+
+ TYsonTypePtr TYsonType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TYsonType* TYsonType::InstanceRaw() {
+ static auto singleton = TYsonType();
+ return &singleton;
+ }
+
+ const TYsonType* TYsonType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TYsonType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TUuidType::TUuidType()
+ : TPrimitiveType({}, EPrimitiveTypeName::Uuid)
+ {
+ }
+
+ TUuidTypePtr TUuidType::Instance() {
+ return InstanceRaw()->AsPtr();
+ }
+
+ const TUuidType* TUuidType::InstanceRaw() {
+ static auto singleton = TUuidType();
+ return &singleton;
+ }
+
+ const TUuidType* TUuidType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ Y_UNUSED(factory);
+ return InstanceRaw();
+ }
+
+ void TUuidType::Drop(ITypeFactoryInternal& factory) noexcept {
+ Y_UNUSED(factory);
+ }
+
+ TOptionalType::TOptionalType(TMaybe<ui64> hash, const TType* item) noexcept
+ : TType(hash, ETypeName::Optional)
+ , Item_(item)
+ {
+ }
+
+ TOptionalTypePtr TOptionalType::Create(ITypeFactory& factory, TTypePtr item) {
+ return CreateRaw(factory, item.Get())->AsPtr();
+ }
+
+ const TOptionalType* TOptionalType::CreateRaw(ITypeFactory& factory, const TType* item) {
+ return TOptionalType({}, item).Clone(FactoryInternal(factory));
+ }
+
+ const TOptionalType* TOptionalType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TOptionalType* {
+ auto hash = GetHashRaw();
+ auto item = factory.Own(Item_);
+ return factory.New<TOptionalType>(hash, item);
+ });
+ }
+
+ void TOptionalType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.Disown(Item_);
+ }
+
+ ui64 TOptionalType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Item_->GetHash(), hash);
+ return hash;
+ }
+
+ TListType::TListType(TMaybe<ui64> hash, const TType* item) noexcept
+ : TType(hash, ETypeName::List)
+ , Item_(item)
+ {
+ }
+
+ TListTypePtr TListType::Create(ITypeFactory& factory, TTypePtr item) {
+ return CreateRaw(factory, item.Get())->AsPtr();
+ }
+
+ const TListType* TListType::CreateRaw(ITypeFactory& factory, const TType* item) {
+ return TListType({}, item).Clone(FactoryInternal(factory));
+ }
+
+ const TListType* TListType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TListType* {
+ auto hash = GetHashRaw();
+ auto item = factory.Own(Item_);
+ return factory.New<TListType>(hash, item);
+ });
+ }
+
+ void TListType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.Disown(Item_);
+ }
+
+ ui64 TListType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Item_->GetHash(), hash);
+ return hash;
+ }
+
+ TDictType::TDictType(TMaybe<ui64> hash, const TType* key, const TType* value) noexcept
+ : TType(hash, ETypeName::Dict)
+ , Key_(key)
+ , Value_(value)
+ {
+ }
+
+ TDictTypePtr TDictType::Create(ITypeFactory& factory, TTypePtr key, TTypePtr value) {
+ return CreateRaw(factory, key.Get(), value.Get())->AsPtr();
+ }
+
+ const TDictType* TDictType::CreateRaw(ITypeFactory& factory, const TType* key, const TType* value) {
+ return TDictType({}, key, value).Clone(FactoryInternal(factory));
+ }
+
+ const TDictType* TDictType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TDictType* {
+ auto hash = GetHashRaw();
+ auto key = factory.Own(Key_);
+ auto value = factory.Own(Value_);
+ return factory.New<TDictType>(hash, key, value);
+ });
+ }
+
+ void TDictType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.Disown(Key_);
+ factory.Disown(Value_);
+ }
+
+ ui64 TDictType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Key_->GetHash(), hash);
+ hash = ::Hash(Value_->GetHash(), hash);
+ return hash;
+ }
+
+ TStructType::TMember::TMember(TStringBuf name, const TType* type)
+ : Name_(name)
+ , Type_(type)
+ {
+ }
+
+ ui64 TStructType::TMember::Hash() const {
+ auto hash = 0x10000;
+ hash = ::Hash(Name_, hash);
+ hash = ::Hash(Type_->GetHash(), hash);
+ return hash;
+ }
+
+ TStructType::TOwnedMember::TOwnedMember(TString name, TTypePtr type)
+ : Name_(std::move(name))
+ , Type_(std::move(type))
+ {
+ }
+
+ TStructType::TOwnedMember::operator TStructType::TMember() const& {
+ return TStructType::TMember(Name_, Type_.Get());
+ }
+
+ TStructType::TStructType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, TMembers members, TConstArrayRef<size_t> sortedItems) noexcept
+ : TType(hash, ETypeName::Struct)
+ , Name_(name)
+ , Members_(members)
+ , SortedMembers_(sortedItems)
+ {
+ }
+
+ TStructTypePtr TStructType::Create(ITypeFactory& factory, TStructType::TOwnedMembers members) {
+ return Create(factory, Nothing(), members);
+ }
+
+ TStructTypePtr TStructType::Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TStructType::TOwnedMembers members) {
+ auto rawItems = TTempArray<TMember>(members.size());
+ for (size_t i = 0; i < members.size(); ++i) {
+ new (rawItems.Data() + i) TMember(members[i]);
+ }
+ return CreateRaw(factory, name, TArrayRef(rawItems.Data(), members.size()))->AsPtr();
+ }
+
+ const TStructType* TStructType::CreateRaw(ITypeFactory& factory, TStructType::TMembers members) {
+ return CreateRaw(factory, Nothing(), members);
+ }
+
+ const TStructType* TStructType::CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, TStructType::TMembers members) {
+ auto sortedMembersArray = TTempArray<size_t>(members.size());
+ auto sortedMembers = TArrayRef(sortedMembersArray.Data(), members.size());
+ MakeSortedMembers(members, sortedMembers);
+ return TStructType({}, name, members, sortedMembers).Clone(FactoryInternal(factory));
+ }
+
+ void TStructType::MakeSortedMembers(TStructType::TMembers members, TArrayRef<size_t> sortedItems) {
+ Y_VERIFY(members.size() == sortedItems.size());
+
+ for (size_t i = 0; i < members.size(); ++i) {
+ sortedItems[i] = i;
+ }
+
+ Sort(sortedItems.begin(), sortedItems.end(), [members](size_t lhs, size_t rhs) {
+ return members[lhs].GetName() < members[rhs].GetName();
+ });
+
+ for (size_t i = 1; i < members.size(); ++i) {
+ if (members[sortedItems[i - 1]].GetName() == members[sortedItems[i]].GetName()) {
+ ythrow TIllegalTypeException() << "duplicate struct item " << Quote(members[sortedItems[i]].GetName());
+ }
+ }
+ }
+
+ const TStructType* TStructType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TStructType* {
+ auto hash = GetHashRaw();
+ auto name = factory.AllocateStringMaybe(Name_);
+ auto members = factory.NewArray<TMember>(Members_.size(), [this, &factory](TMember* item, size_t i) {
+ auto name = factory.AllocateString(Members_[i].GetName());
+ auto type = factory.Own(Members_[i].GetTypeRaw());
+ new (item) TMember(name, type);
+ });
+ auto sortedItems = factory.AllocateArrayFor<size_t>(SortedMembers_.size());
+ Copy(SortedMembers_.begin(), SortedMembers_.end(), sortedItems);
+ return factory.New<TStructType>(hash, name, members, TArrayRef{sortedItems, SortedMembers_.size()});
+ });
+ }
+
+ void TStructType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.FreeStringMaybe(Name_);
+ factory.DeleteArray(Members_, [&factory](const TMember* item, size_t) {
+ factory.FreeString(item->GetName());
+ factory.Disown(item->GetTypeRaw());
+ });
+ factory.Free(const_cast<void*>(static_cast<const void*>(SortedMembers_.data())));
+ }
+
+ ui64 TStructType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Name_, hash);
+ hash = ::Hash(Members_.size(), hash);
+ for (auto& item : Members_) {
+ hash = ::Hash(item.Hash(), hash);
+ }
+ return hash;
+ }
+
+ bool TStructType::HasMember(TStringBuf name) const noexcept {
+ return GetMemberIndex(name) != -1;
+ }
+
+ const TStructType::TMember& TStructType::GetMember(TStringBuf name) const {
+ auto idx = GetMemberIndex(name);
+ if (idx == -1) {
+ ythrow TItemNotFound() << "no item named " << Quote(name);
+ } else {
+ return Members_[idx];
+ }
+ }
+
+ ssize_t TStructType::GetMemberIndex(TStringBuf name) const noexcept {
+ auto it = LowerBound(SortedMembers_.begin(), SortedMembers_.end(), name, [this](size_t i, TStringBuf name) {
+ return Members_[i].GetName() < name;
+ });
+
+ if (it == SortedMembers_.end() || Members_[*it].GetName() != name) {
+ return -1;
+ } else {
+ return *it;
+ }
+ }
+
+ TTupleType::TElement::TElement(const TType* type)
+ : Type_(type)
+ {
+ }
+
+ ui64 TTupleType::TElement::Hash() const {
+ auto hash = 0x10001;
+ hash = ::Hash(Type_->GetHash(), hash);
+ return hash;
+ }
+
+ TTupleType::TOwnedElement::TOwnedElement(TTypePtr type)
+ : Type_(std::move(type))
+ {
+ }
+
+ TTupleType::TOwnedElement::operator TTupleType::TElement() const& {
+ return TTupleType::TElement(Type_.Get());
+ }
+
+ TTupleType::TTupleType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, TElements elements) noexcept
+ : TType(hash, ETypeName::Tuple)
+ , Name_(name)
+ , Elements_(elements)
+ {
+ }
+
+ TTupleTypePtr TTupleType::Create(ITypeFactory& factory, TTupleType::TOwnedElements elements) {
+ return Create(factory, Nothing(), elements);
+ }
+
+ TTupleTypePtr TTupleType::Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TTupleType::TOwnedElements elements) {
+ auto rawItems = TTempArray<TElement>(elements.size());
+ for (size_t i = 0; i < elements.size(); ++i) {
+ new (rawItems.Data() + i) TElement(elements[i]);
+ }
+ return CreateRaw(factory, name, TArrayRef(rawItems.Data(), elements.size()))->AsPtr();
+ }
+
+ const TTupleType* TTupleType::CreateRaw(ITypeFactory& factory, TTupleType::TElements elements) {
+ return CreateRaw(factory, Nothing(), elements);
+ }
+
+ const TTupleType* TTupleType::CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, TTupleType::TElements elements) {
+ return TTupleType({}, name, elements).Clone(FactoryInternal(factory));
+ }
+
+ const TTupleType* TTupleType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TTupleType* {
+ auto hash = GetHashRaw();
+ auto name = factory.AllocateStringMaybe(Name_);
+ auto elements = factory.NewArray<TElement>(Elements_.size(), [this, &factory](TElement* item, size_t i) {
+ auto type = factory.Own(Elements_[i].GetTypeRaw());
+ new (item) TElement(type);
+ });
+ return factory.New<TTupleType>(hash, name, elements);
+ });
+ }
+
+ void TTupleType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.FreeStringMaybe(Name_);
+ factory.DeleteArray(Elements_, [&factory](const TElement* item, size_t) {
+ factory.Disown(item->GetTypeRaw());
+ });
+ }
+
+ ui64 TTupleType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Name_, hash);
+ hash = ::Hash(Elements_.size(), hash);
+ for (auto& item : Elements_) {
+ hash = ::Hash(item.Hash(), hash);
+ }
+ return hash;
+ }
+
+ TVariantType::TVariantType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, const TType* inner) noexcept
+ : TType(hash, ETypeName::Variant)
+ , Name_(name)
+ , Underlying_(inner)
+ {
+ }
+
+ TVariantTypePtr TVariantType::Create(ITypeFactory& factory, TTypePtr inner) {
+ return Create(factory, Nothing(), std::move(inner));
+ }
+
+ TVariantTypePtr TVariantType::Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TTypePtr inner) {
+ return CreateRaw(factory, name, inner.Get())->AsPtr();
+ }
+
+ const TVariantType* TVariantType::CreateRaw(ITypeFactory& factory, const TType* inner) {
+ return CreateRaw(factory, Nothing(), inner);
+ }
+
+ const TVariantType* TVariantType::CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, const TType* inner) {
+ inner->VisitRaw(TOverloaded{
+ [&](const TStructType* s) {
+ Y_ENSURE_EX(
+ !s->GetMembers().empty(),
+ TIllegalTypeException() << "variant should contain at least one alternative");
+ },
+ [&](const TTupleType* t) {
+ Y_ENSURE_EX(
+ !t->GetElements().empty(),
+ TIllegalTypeException() << "variant should contain at least one alternative");
+ },
+ [](const TType* t) {
+ ythrow TIllegalTypeException() << "variants can only contain structs and tuples, got "
+ << t->GetTypeName() << " instead";
+ }});
+
+ return TVariantType({}, name, inner).Clone(FactoryInternal(factory));
+ }
+
+ const TVariantType* TVariantType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TVariantType* {
+ auto hash = GetHashRaw();
+ auto name = factory.AllocateStringMaybe(Name_);
+ auto inner = factory.Own(Underlying_);
+ return factory.New<TVariantType>(hash, name, inner);
+ });
+ }
+
+ void TVariantType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.FreeStringMaybe(Name_);
+ factory.Disown(Underlying_);
+ }
+
+ ui64 TVariantType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Name_, hash);
+ hash = ::Hash(Underlying_->GetHash(), hash);
+ return hash;
+ }
+
+ TTaggedType::TTaggedType(TMaybe<ui64> hash, const TType* item, TStringBuf tag) noexcept
+ : TType(hash, ETypeName::Tagged)
+ , Item_(item)
+ , Tag_(tag)
+ {
+ }
+
+ TTaggedTypePtr TTaggedType::Create(ITypeFactory& factory, TTypePtr type, TStringBuf tag) {
+ return CreateRaw(factory, type.Get(), tag)->AsPtr();
+ }
+
+ const TTaggedType* TTaggedType::CreateRaw(ITypeFactory& factory, const TType* type, TStringBuf tag) {
+ return TTaggedType({}, type, tag).Clone(FactoryInternal(factory));
+ }
+
+ const TTaggedType* TTaggedType::Clone(ITypeFactoryInternal& factory) const noexcept {
+ return Cached(this, factory, [this, &factory]() -> const TTaggedType* {
+ auto hash = GetHashRaw();
+ auto item = factory.Own(Item_);
+ auto tag = factory.AllocateString(Tag_);
+ return factory.New<TTaggedType>(hash, item, tag);
+ });
+ }
+
+ void TTaggedType::Drop(ITypeFactoryInternal& factory) noexcept {
+ factory.FreeString(Tag_);
+ factory.Disown(Item_);
+ }
+
+ ui64 TTaggedType::CalculateHash() const noexcept {
+ auto hash = TType::CalculateHash();
+ hash = ::Hash(Tag_, hash);
+ hash = ::Hash(Item_->GetHash(), hash);
+ return hash;
+ }
+
+ TVoidTypePtr Void() {
+ return NPrivate::GetDefaultHeapFactory()->Void();
+ }
+
+ TNullTypePtr Null() {
+ return NPrivate::GetDefaultHeapFactory()->Null();
+ }
+
+ TBoolTypePtr Bool() {
+ return NPrivate::GetDefaultHeapFactory()->Bool();
+ }
+
+ TInt8TypePtr Int8() {
+ return NPrivate::GetDefaultHeapFactory()->Int8();
+ }
+
+ TInt16TypePtr Int16() {
+ return NPrivate::GetDefaultHeapFactory()->Int16();
+ }
+
+ TInt32TypePtr Int32() {
+ return NPrivate::GetDefaultHeapFactory()->Int32();
+ }
+
+ TInt64TypePtr Int64() {
+ return NPrivate::GetDefaultHeapFactory()->Int64();
+ }
+
+ TUint8TypePtr Uint8() {
+ return NPrivate::GetDefaultHeapFactory()->Uint8();
+ }
+
+ TUint16TypePtr Uint16() {
+ return NPrivate::GetDefaultHeapFactory()->Uint16();
+ }
+
+ TUint32TypePtr Uint32() {
+ return NPrivate::GetDefaultHeapFactory()->Uint32();
+ }
+
+ TUint64TypePtr Uint64() {
+ return NPrivate::GetDefaultHeapFactory()->Uint64();
+ }
+
+ TFloatTypePtr Float() {
+ return NPrivate::GetDefaultHeapFactory()->Float();
+ }
+
+ TDoubleTypePtr Double() {
+ return NPrivate::GetDefaultHeapFactory()->Double();
+ }
+
+ TStringTypePtr String() {
+ return NPrivate::GetDefaultHeapFactory()->String();
+ }
+
+ TUtf8TypePtr Utf8() {
+ return NPrivate::GetDefaultHeapFactory()->Utf8();
+ }
+
+ TDateTypePtr Date() {
+ return NPrivate::GetDefaultHeapFactory()->Date();
+ }
+
+ TDatetimeTypePtr Datetime() {
+ return NPrivate::GetDefaultHeapFactory()->Datetime();
+ }
+
+ TTimestampTypePtr Timestamp() {
+ return NPrivate::GetDefaultHeapFactory()->Timestamp();
+ }
+
+ TTzDateTypePtr TzDate() {
+ return NPrivate::GetDefaultHeapFactory()->TzDate();
+ }
+
+ TTzDatetimeTypePtr TzDatetime() {
+ return NPrivate::GetDefaultHeapFactory()->TzDatetime();
+ }
+
+ TTzTimestampTypePtr TzTimestamp() {
+ return NPrivate::GetDefaultHeapFactory()->TzTimestamp();
+ }
+
+ TIntervalTypePtr Interval() {
+ return NPrivate::GetDefaultHeapFactory()->Interval();
+ }
+
+ TDecimalTypePtr Decimal(ui8 precision, ui8 scale) {
+ return NPrivate::GetDefaultHeapFactory()->Decimal(precision, scale);
+ }
+
+ TJsonTypePtr Json() {
+ return NPrivate::GetDefaultHeapFactory()->Json();
+ }
+
+ TYsonTypePtr Yson() {
+ return NPrivate::GetDefaultHeapFactory()->Yson();
+ }
+
+ TUuidTypePtr Uuid() {
+ return NPrivate::GetDefaultHeapFactory()->Uuid();
+ }
+
+ TOptionalTypePtr Optional(TTypePtr item) {
+ return NPrivate::GetDefaultHeapFactory()->Optional(std::move(item));
+ }
+
+ TListTypePtr List(TTypePtr item) {
+ return NPrivate::GetDefaultHeapFactory()->List(std::move(item));
+ }
+
+ TDictTypePtr Dict(TTypePtr key, TTypePtr value) {
+ return NPrivate::GetDefaultHeapFactory()->Dict(std::move(key), std::move(value));
+ }
+
+ TStructTypePtr Struct(TStructType::TOwnedMembers members) {
+ return NPrivate::GetDefaultHeapFactory()->Struct(members);
+ }
+
+ TStructTypePtr Struct(TMaybe<TStringBuf> name, TStructType::TOwnedMembers members) {
+ return NPrivate::GetDefaultHeapFactory()->Struct(name, members);
+ }
+
+ TTupleTypePtr Tuple(TTupleType::TOwnedElements elements) {
+ return NPrivate::GetDefaultHeapFactory()->Tuple(elements);
+ }
+
+ TTupleTypePtr Tuple(TMaybe<TStringBuf> name, TTupleType::TOwnedElements elements) {
+ return NPrivate::GetDefaultHeapFactory()->Tuple(name, elements);
+ }
+
+ TVariantTypePtr Variant(TTypePtr underlying) {
+ return NPrivate::GetDefaultHeapFactory()->Variant(std::move(underlying));
+ }
+
+ TVariantTypePtr Variant(TMaybe<TStringBuf> name, TTypePtr underlying) {
+ return NPrivate::GetDefaultHeapFactory()->Variant(name, std::move(underlying));
+ }
+
+ TTaggedTypePtr Tagged(TTypePtr type, TStringBuf tag) {
+ return NPrivate::GetDefaultHeapFactory()->Tagged(std::move(type), tag);
+ }
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TType, o, v) {
+ v.VisitRaw([&o](const auto* v) { o << *v; });
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TPrimitiveType, o, v) {
+ v.VisitPrimitiveRaw([&o](const auto* v) { o << *v; });
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TVoidType, o, v) {
+ Y_UNUSED(v);
+ o << "Void";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TNullType, o, v) {
+ Y_UNUSED(v);
+ o << "Null";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TBoolType, o, v) {
+ Y_UNUSED(v);
+ o << "Bool";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TInt8Type, o, v) {
+ Y_UNUSED(v);
+ o << "Int8";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TInt16Type, o, v) {
+ Y_UNUSED(v);
+ o << "Int16";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TInt32Type, o, v) {
+ Y_UNUSED(v);
+ o << "Int32";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TInt64Type, o, v) {
+ Y_UNUSED(v);
+ o << "Int64";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUint8Type, o, v) {
+ Y_UNUSED(v);
+ o << "Uint8";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUint16Type, o, v) {
+ Y_UNUSED(v);
+ o << "Uint16";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUint32Type, o, v) {
+ Y_UNUSED(v);
+ o << "Uint32";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUint64Type, o, v) {
+ Y_UNUSED(v);
+ o << "Uint64";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TFloatType, o, v) {
+ Y_UNUSED(v);
+ o << "Float";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TDoubleType, o, v) {
+ Y_UNUSED(v);
+ o << "Double";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TStringType, o, v) {
+ Y_UNUSED(v);
+ o << "String";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUtf8Type, o, v) {
+ Y_UNUSED(v);
+ o << "Utf8";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TDateType, o, v) {
+ Y_UNUSED(v);
+ o << "Date";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TDatetimeType, o, v) {
+ Y_UNUSED(v);
+ o << "Datetime";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTimestampType, o, v) {
+ Y_UNUSED(v);
+ o << "Timestamp";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTzDateType, o, v) {
+ Y_UNUSED(v);
+ o << "TzDate";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTzDatetimeType, o, v) {
+ Y_UNUSED(v);
+ o << "TzDatetime";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTzTimestampType, o, v) {
+ Y_UNUSED(v);
+ o << "TzTimestamp";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TIntervalType, o, v) {
+ Y_UNUSED(v);
+ o << "Interval";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TDecimalType, o, v) {
+ o << "Decimal(" << (i32)v.GetPrecision() << ", " << (i32)v.GetScale() << ')';
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TJsonType, o, v) {
+ Y_UNUSED(v);
+ o << "Json";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TYsonType, o, v) {
+ Y_UNUSED(v);
+ o << "Yson";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TUuidType, o, v) {
+ Y_UNUSED(v);
+ o << "Uuid";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TOptionalType, o, v) {
+ o << "Optional<" << *v.GetItemTypeRaw() << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TListType, o, v) {
+ o << "List<" << *v.GetItemTypeRaw() << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TDictType, o, v) {
+ o << "Dict<" << *v.GetKeyTypeRaw() << ", " << *v.GetValueTypeRaw() << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TStructType, o, v) {
+ o << "Struct";
+
+ if (v.GetName().Defined()) {
+ o << "[" << Quote(*v.GetName()) << "]";
+ }
+
+ o << "<";
+ const char* sep = "";
+ for (auto& item : v.GetMembers()) {
+ o << sep << Quote(item.GetName()) << ": " << *item.GetTypeRaw();
+ sep = ", ";
+ }
+ o << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTupleType, o, v) {
+ o << "Tuple";
+
+ if (v.GetName().Defined()) {
+ o << "[" << Quote(*v.GetName()) << "]";
+ }
+
+ o << "<";
+ const char* sep = "";
+ for (auto& item : v.GetElements()) {
+ o << sep << *item.GetTypeRaw();
+ sep = ", ";
+ }
+ o << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TVariantType, o, v) {
+ o << "Variant";
+
+ if (v.GetName().Defined()) {
+ o << "[" << Quote(*v.GetName()) << "]";
+ }
+
+ o << "<";
+ v.VisitUnderlyingRaw(
+ TOverloaded{
+ [&o](const NTi::TStructType* s) {
+ const char* sep = "";
+ for (auto& item : s->GetMembers()) {
+ o << sep << Quote(item.GetName()) << ": " << *item.GetTypeRaw();
+ sep = ", ";
+ }
+ },
+ [&o](const NTi::TTupleType* t) {
+ const char* sep = "";
+ for (auto& item : t->GetElements()) {
+ o << sep << *item.GetTypeRaw();
+ sep = ", ";
+ }
+ }});
+ o << ">";
+}
+
+Y_DECLARE_OUT_SPEC(, NTi::TTaggedType, o, v) {
+ o << "Tagged<" << *v.GetItemTypeRaw() << ", " << Quote(v.GetTag()) << ">";
+}
+
+static_assert(std::is_trivially_destructible_v<NTi::TVoidType>);
+static_assert(std::is_trivially_destructible_v<NTi::TNullType>);
+static_assert(std::is_trivially_destructible_v<NTi::TPrimitiveType>);
+static_assert(std::is_trivially_destructible_v<NTi::TBoolType>);
+static_assert(std::is_trivially_destructible_v<NTi::TInt8Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TInt16Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TInt32Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TInt64Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TUint8Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TUint16Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TUint32Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TUint64Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TFloatType>);
+static_assert(std::is_trivially_destructible_v<NTi::TDoubleType>);
+static_assert(std::is_trivially_destructible_v<NTi::TStringType>);
+static_assert(std::is_trivially_destructible_v<NTi::TUtf8Type>);
+static_assert(std::is_trivially_destructible_v<NTi::TDateType>);
+static_assert(std::is_trivially_destructible_v<NTi::TDatetimeType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTimestampType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTzDateType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTzDatetimeType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTzTimestampType>);
+static_assert(std::is_trivially_destructible_v<NTi::TIntervalType>);
+static_assert(std::is_trivially_destructible_v<NTi::TDecimalType>);
+static_assert(std::is_trivially_destructible_v<NTi::TJsonType>);
+static_assert(std::is_trivially_destructible_v<NTi::TYsonType>);
+static_assert(std::is_trivially_destructible_v<NTi::TUuidType>);
+static_assert(std::is_trivially_destructible_v<NTi::TOptionalType>);
+static_assert(std::is_trivially_destructible_v<NTi::TListType>);
+static_assert(std::is_trivially_destructible_v<NTi::TDictType>);
+static_assert(std::is_trivially_destructible_v<NTi::TStructType>);
+static_assert(std::is_trivially_destructible_v<NTi::TStructType::TMember>);
+static_assert(std::is_trivially_destructible_v<NTi::TTupleType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTupleType::TElement>);
+static_assert(std::is_trivially_destructible_v<NTi::TVariantType>);
+static_assert(std::is_trivially_destructible_v<NTi::TTaggedType>);
diff --git a/library/cpp/type_info/type.h b/library/cpp/type_info/type.h
new file mode 100644
index 0000000000..1577f1cee3
--- /dev/null
+++ b/library/cpp/type_info/type.h
@@ -0,0 +1,2421 @@
+#pragma once
+
+//! @file type.h
+//!
+//! Hierarchy of classes that represent types.
+#include "fwd.h"
+
+#include "error.h"
+#include "type_list.h"
+
+#include <atomic>
+#include <util/generic/array_ref.h>
+#include <util/generic/maybe.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+
+namespace NTi {
+ /// Represents a single type.
+ ///
+ /// Create instances of types using type factory (see `NTi::ITypeFactory`).
+ ///
+ /// Introspect them using associated methods and functions that work with `ETypeName`.
+ ///
+ /// Pattern-match them using the `Visit` method.
+ ///
+ /// Serialize and deserialize them using functions from `NTi::NIo`.
+ class TType {
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+ template <typename T>
+ friend class ::TDefaultIntrusivePtrOps;
+
+ public:
+ TTypePtr AsPtr() const noexcept {
+ return const_cast<TType*>(this);
+ }
+
+ protected:
+ explicit TType(TMaybe<ui64> hash, ETypeName typeName) noexcept;
+
+ protected:
+ /// Calculate hash for this type. This function is lazily called by `GetHash`.
+ ///
+ /// Note: this function is not marked as `virtual` because we use our own dispatch via `Visit`.
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get hash of this type. Hashes follow the 'strict equivalence' relation (see `type_equivalence.h`).
+ ui64 GetHash() const;
+
+ /// Get hash of this type. If hash is not calculated, returns nothing.
+ TMaybe<ui64> GetHashRaw() const noexcept;
+
+ /// Get name of this type as a `NTi::ETypeName` enumerator.
+ ETypeName GetTypeName() const noexcept {
+ return TypeName_;
+ }
+
+ /// @name Simple type downcast functions
+ ///
+ /// Check if type is of the given subclass and convert between subclasses.
+ /// Conversions panic if downcasting into an incompatible type.
+ ///
+ /// @{
+ inline bool IsVoid() const noexcept;
+ inline TVoidTypePtr AsVoid() const noexcept;
+ inline const TVoidType* AsVoidRaw() const noexcept;
+
+ inline bool IsNull() const noexcept;
+ inline TNullTypePtr AsNull() const noexcept;
+ inline const TNullType* AsNullRaw() const noexcept;
+
+ inline bool IsPrimitive() const noexcept;
+ inline TPrimitiveTypePtr AsPrimitive() const noexcept;
+ inline const TPrimitiveType* AsPrimitiveRaw() const noexcept;
+
+ inline bool IsBool() const noexcept;
+ inline TBoolTypePtr AsBool() const noexcept;
+ inline const TBoolType* AsBoolRaw() const noexcept;
+
+ inline bool IsInt8() const noexcept;
+ inline TInt8TypePtr AsInt8() const noexcept;
+ inline const TInt8Type* AsInt8Raw() const noexcept;
+
+ inline bool IsInt16() const noexcept;
+ inline TInt16TypePtr AsInt16() const noexcept;
+ inline const TInt16Type* AsInt16Raw() const noexcept;
+
+ inline bool IsInt32() const noexcept;
+ inline TInt32TypePtr AsInt32() const noexcept;
+ inline const TInt32Type* AsInt32Raw() const noexcept;
+
+ inline bool IsInt64() const noexcept;
+ inline TInt64TypePtr AsInt64() const noexcept;
+ inline const TInt64Type* AsInt64Raw() const noexcept;
+
+ inline bool IsUint8() const noexcept;
+ inline TUint8TypePtr AsUint8() const noexcept;
+ inline const TUint8Type* AsUint8Raw() const noexcept;
+
+ inline bool IsUint16() const noexcept;
+ inline TUint16TypePtr AsUint16() const noexcept;
+ inline const TUint16Type* AsUint16Raw() const noexcept;
+
+ inline bool IsUint32() const noexcept;
+ inline TUint32TypePtr AsUint32() const noexcept;
+ inline const TUint32Type* AsUint32Raw() const noexcept;
+
+ inline bool IsUint64() const noexcept;
+ inline TUint64TypePtr AsUint64() const noexcept;
+ inline const TUint64Type* AsUint64Raw() const noexcept;
+
+ inline bool IsFloat() const noexcept;
+ inline TFloatTypePtr AsFloat() const noexcept;
+ inline const TFloatType* AsFloatRaw() const noexcept;
+
+ inline bool IsDouble() const noexcept;
+ inline TDoubleTypePtr AsDouble() const noexcept;
+ inline const TDoubleType* AsDoubleRaw() const noexcept;
+
+ inline bool IsString() const noexcept;
+ inline TStringTypePtr AsString() const noexcept;
+ inline const TStringType* AsStringRaw() const noexcept;
+
+ inline bool IsUtf8() const noexcept;
+ inline TUtf8TypePtr AsUtf8() const noexcept;
+ inline const TUtf8Type* AsUtf8Raw() const noexcept;
+
+ inline bool IsDate() const noexcept;
+ inline TDateTypePtr AsDate() const noexcept;
+ inline const TDateType* AsDateRaw() const noexcept;
+
+ inline bool IsDatetime() const noexcept;
+ inline TDatetimeTypePtr AsDatetime() const noexcept;
+ inline const TDatetimeType* AsDatetimeRaw() const noexcept;
+
+ inline bool IsTimestamp() const noexcept;
+ inline TTimestampTypePtr AsTimestamp() const noexcept;
+ inline const TTimestampType* AsTimestampRaw() const noexcept;
+
+ inline bool IsTzDate() const noexcept;
+ inline TTzDateTypePtr AsTzDate() const noexcept;
+ inline const TTzDateType* AsTzDateRaw() const noexcept;
+
+ inline bool IsTzDatetime() const noexcept;
+ inline TTzDatetimeTypePtr AsTzDatetime() const noexcept;
+ inline const TTzDatetimeType* AsTzDatetimeRaw() const noexcept;
+
+ inline bool IsTzTimestamp() const noexcept;
+ inline TTzTimestampTypePtr AsTzTimestamp() const noexcept;
+ inline const TTzTimestampType* AsTzTimestampRaw() const noexcept;
+
+ inline bool IsInterval() const noexcept;
+ inline TIntervalTypePtr AsInterval() const noexcept;
+ inline const TIntervalType* AsIntervalRaw() const noexcept;
+
+ inline bool IsDecimal() const noexcept;
+ inline TDecimalTypePtr AsDecimal() const noexcept;
+ inline const TDecimalType* AsDecimalRaw() const noexcept;
+
+ inline bool IsJson() const noexcept;
+ inline TJsonTypePtr AsJson() const noexcept;
+ inline const TJsonType* AsJsonRaw() const noexcept;
+
+ inline bool IsYson() const noexcept;
+ inline TYsonTypePtr AsYson() const noexcept;
+ inline const TYsonType* AsYsonRaw() const noexcept;
+
+ inline bool IsUuid() const noexcept;
+ inline TUuidTypePtr AsUuid() const noexcept;
+ inline const TUuidType* AsUuidRaw() const noexcept;
+
+ inline bool IsOptional() const noexcept;
+ inline TOptionalTypePtr AsOptional() const noexcept;
+ inline const TOptionalType* AsOptionalRaw() const noexcept;
+
+ inline bool IsList() const noexcept;
+ inline TListTypePtr AsList() const noexcept;
+ inline const TListType* AsListRaw() const noexcept;
+
+ inline bool IsDict() const noexcept;
+ inline TDictTypePtr AsDict() const noexcept;
+ inline const TDictType* AsDictRaw() const noexcept;
+
+ inline bool IsStruct() const noexcept;
+ inline TStructTypePtr AsStruct() const noexcept;
+ inline const TStructType* AsStructRaw() const noexcept;
+
+ inline bool IsTuple() const noexcept;
+ inline TTupleTypePtr AsTuple() const noexcept;
+ inline const TTupleType* AsTupleRaw() const noexcept;
+
+ inline bool IsVariant() const noexcept;
+ inline TVariantTypePtr AsVariant() const noexcept;
+ inline const TVariantType* AsVariantRaw() const noexcept;
+
+ inline bool IsTagged() const noexcept;
+ inline TTaggedTypePtr AsTagged() const noexcept;
+ inline const TTaggedType* AsTaggedRaw() const noexcept;
+
+ /// @}
+
+ /// Recursively descends to tagged types and returns first non-tagged type.
+ TTypePtr StripTags() const noexcept;
+
+ /// Like `StripTags`, but returns a raw pointer.
+ const TType* StripTagsRaw() const noexcept;
+
+ /// Recursively descends to optional types and returns first non-optional type.
+ TTypePtr StripOptionals() const noexcept;
+
+ /// Like `StripOptionals`, but returns a raw pointer.
+ const TType* StripOptionalsRaw() const noexcept;
+
+ /// Recursively descends to tagged and optional types and returns first non-tagged non-optional type.
+ TTypePtr StripTagsAndOptionals() const noexcept;
+
+ /// Like `StripTagsAndOptionals`, but returns a raw pointer.
+ const TType* StripTagsAndOptionalsRaw() const noexcept;
+
+ /// Cast this base class down to the most-derived class and pass it to the `visitor`.
+ ///
+ /// This function is used as a safer alternative to manually downcasting types via `IsType`/`AsType` calls.
+ /// It works like `std::visit` for types, except that it doesn't produce as much code bloat as `std::visit`
+ /// does, and can be optimized better. It casts an instance of `NTi::TType` down to the most-derived class,
+ /// and passes an intrusive pointer to that concrete type to the `visitor` functor. That is, `visitor` should
+ /// be a callable which can handle `NTi::TVoidTypePtr`, `NTi::TOptionalTypePtr`, etc.
+ ///
+ /// This function returns whatever the `visitor` returns.
+ ///
+ ///
+ /// # Example: visitor
+ ///
+ /// A simple visitor that returns name for a type would look like this:
+ ///
+ /// ```
+ /// struct TGetNameVisitor {
+ /// TString operator()(TVoidTypePtr) {
+ /// return "Void";
+ /// }
+ ///
+ /// TString operator()(TStringTypePtr) {
+ /// return "String";
+ /// }
+ ///
+ /// // ...
+ ///
+ /// TString operator()(TStructTypePtr type) {
+ /// return TString(type->GetName().GetOrElse("Struct"));;
+ /// }
+ ///
+ /// // ...
+ /// }
+ /// ```
+ ///
+ /// Now, we can use this visitor as following:
+ ///
+ /// ```
+ /// TString typeName = type->Visit(TGetNameVisitor());
+ /// ```
+ ///
+ ///
+ /// # Example: overloaded struct
+ ///
+ /// Writing a separate struct each time one needs a visitor is tedious. Thanks to C++17 magic, we may avoid it.
+ /// Using lambdas and `TOverloaded` from `library/cpp/overloaded` allows replacing separate struct with
+ /// a bunch of lambdas:
+ ///
+ /// ```
+ /// TString typeName = type->Visit(TOverloaded{
+ /// [](TVoidTypePtr) -> TString {
+ /// return "Void";
+ /// },
+ /// [](TStringTypePtr) -> TString {
+ /// return "String";
+ /// },
+ ///
+ /// // ...
+ ///
+ /// });
+ /// ```
+ ///
+ ///
+ /// # Example: handling all primitives at once
+ ///
+ /// Since all primitives derive from `TPrimitiveType`, they can be handled all at once,
+ /// by accepting `TPrimitiveTypePtr`:
+ ///
+ /// ```
+ /// TString typeName = type->Visit(TOverloaded{
+ /// // All primitive types are handled by this lambda.
+ /// [](TPrimitiveTypePtr) -> TString {
+ /// return "Primitive";
+ /// },
+ ///
+ /// // Special handler for string type. Strings are handled by this lambda
+ /// // because of how C++ prioritizes overloads.
+ /// [](TStringTypePtr) -> TString {
+ /// return "String";
+ /// },
+ ///
+ /// // ...
+ ///
+ /// });
+ /// ```
+ template <typename V>
+ inline decltype(auto) Visit(V&& visitor) const;
+
+ /// Like `Visit`, but passes const raw pointers to the visitor.
+ template <typename V>
+ inline decltype(auto) VisitRaw(V&& visitor) const;
+
+ /// @}
+
+ protected:
+ /// @name Internal interface for adoption semantics support
+ ///
+ /// Do not call these functions manually!
+ ///
+ /// See `type_factory.h`'s section on implementation details for more info.
+ ///
+ /// @{
+ //-
+ /// Create a new instance of the class using this instance as a prototype.
+ ///
+ /// This is a [virtual copy constructor]. Typical implementation does the following:
+ ///
+ /// 1. for nested types, if any, it calls the factory's `Own` function. The `Own` acquires internal
+ /// ownership over the nested types, thus guaranteeing that they'll outlive the object that've
+ /// owned them. Depending on the particular factory implementation, `Own` may either recursively deepcopy
+ /// the whole nested type, increment some reference counter, or do nothing;
+ /// 2. for other resources owned by this type (i.e. arrays, strings, etc.), it copies them into the given
+ /// factory by calling factory's `New` and `Allocate` functions;
+ /// 3. finally, it creates a new instance of the type by invoking its constructor via the factory's
+ /// `New` function.
+ ///
+ /// Note: there are no guarantees on the value stored in `FactoryOrRc_` during invocation of this function.
+ /// Specifically, creating types on the stack (`FactoryOrRc_` is `0` in this case) and moving them
+ /// into a factory is a valid technique used extensively throughout this library.
+ ///
+ /// See `type_factory.h`'s section on implementation details for more info.
+ ///
+ /// Note: this function is not marked as `virtual` because we use our own dispatch via `Visit`.
+ ///
+ /// [virtual move constructor]: https://isocpp.org/wiki/faq/virtual-functions#virtual-ctors
+ const TType* Clone(ITypeFactoryInternal& factory) const noexcept;
+
+ /// Release internal resources that were allocated in `Clone`.
+ ///
+ /// This function is the opposite of `Clone`. It releases all memory that was allocated within `Clone`,
+ /// and disowns nested types.
+ ///
+ /// This function is called by factories that perform active memory management, such as the default
+ /// heap factory. Typical implementation does the following:
+ ///
+ /// 1. for each `Clone`'s call to `Own` it calls `Disown`. The `Disown` releases internal ownership
+ /// over the nested types, thus allowing factory to free their memory. Depending
+ /// on the particular factory implementation, `Disown` may either decrement some reference counter,
+ /// call `Drop` and free the underlying memory, or do nothing;
+ /// 2. for each `Clone`'s call to `New` and `Allocate`, it calls `Delete` and `Free`;
+ /// 3. it should *not* call `Delete(this)` to mirror `Clone`'s final call to `New` (see the third bullet
+ /// in the `Clone`'s documentation). It is the factory's job to release memory under the type that's
+ /// being dropped.
+ ///
+ /// Note: there are no guarantees on whether this method will be called or not. For example, the default
+ /// heap factory will call it when some type's reference counter reaches zero. The default memory pool
+ /// factory will not call it.
+ ///
+ /// Note: this function is not marked as `virtual` because we use our own dispatch via `Visit`.
+ ///
+ /// See `type_factory.h`'s section on implementation details for more info.
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ /// Get factory that manages this instance.
+ ///
+ /// If this instance is refcounted, returns the default heap factory. If it is unmanaged, returns `nullptr`.
+ /// Otherwise, returns pointer to the instance's factory.
+ ///
+ /// Remember that factories are not thread safe, thus using factory from this method may not be safe.
+ ITypeFactoryInternal* GetFactory() const noexcept;
+
+ /// Mark this instance as managed by the given factory.
+ void SetFactory(ITypeFactoryInternal* factory) noexcept;
+
+ /// Get factory's internal interface.
+ static ITypeFactoryInternal& FactoryInternal(ITypeFactory& factory) noexcept;
+
+ /// @}
+
+ protected:
+ /// @name Internal interface for reference counting
+ ///
+ /// See `type_factory.h`'s section on implementation details for more info.
+ ///
+ /// @{
+ //-
+ /// Increase reference count of this type.
+ void RefSelf() noexcept {
+ RefImpl</* RefFactory = */ false>();
+ }
+
+ /// Increase reference count of this type and its factory.
+ void Ref() noexcept {
+ RefImpl</* RefFactory = */ true>();
+ }
+
+ /// Decrease reference count of this type.
+ void UnRefSelf() noexcept {
+ UnRefImpl</* UnRefFactory = */ false>();
+ }
+
+ /// Decrease reference count of this type and its factory.
+ void UnRef() noexcept {
+ UnRefImpl</* UnRefFactory = */ true>();
+ }
+
+ /// Decrease reference count of this type. Panic if any of it reaches zero.
+ void DecRefSelf() noexcept {
+ DecRefImpl</* DecRefFactory = */ false>();
+ }
+
+ /// Decrease reference count of type and its factory. Panic if any of it reaches zero.
+ void DecRef() noexcept {
+ DecRefImpl</* DecRefFactory = */ true>();
+ }
+
+ /// Get current reference count for this type.
+ long RefCount() const noexcept {
+ return RefCountImpl();
+ }
+
+ /// @}
+
+ private:
+ template <bool RefFactory>
+ void RefImpl() noexcept;
+
+ template <bool UnRefFactory>
+ void UnRefImpl() noexcept;
+
+ template <bool DecRefFactory>
+ void DecRefImpl() noexcept;
+
+ long RefCountImpl() const noexcept;
+
+ protected:
+ /// Helper for implementing `Clone` with caching.
+ template <typename T, typename TCtor>
+ static const T* Cached(const T* type, ITypeFactoryInternal& factory, TCtor&& ctor);
+
+ private:
+ /// Pointer to the type factory that've created this instance.
+ /// If this instance is static, this variable contains zero.
+ /// If this instance was created by the default heap factory, this variable is used as a reference counter.
+ std::atomic<size_t> FactoryOrRc_ = 0;
+
+ /// Name of this type. Can be used to check before downcast.
+ ETypeName TypeName_;
+
+ /// Hash is calculated lazily.
+ mutable std::atomic<bool> HasHash_;
+ mutable std::atomic<ui64> Hash_;
+
+ private:
+ static bool IsRc(size_t factoryOrRc) noexcept {
+ return factoryOrRc & 1u;
+ }
+ static bool IsFactory(size_t factoryOrRc) noexcept {
+ return factoryOrRc != 0 && !IsRc(factoryOrRc);
+ }
+ static size_t CastFromFactory(ITypeFactoryInternal* factory) noexcept {
+ return reinterpret_cast<size_t>(factory);
+ }
+ static ITypeFactoryInternal* CastToFactory(size_t factoryOrRc) noexcept {
+ return reinterpret_cast<ITypeFactoryInternal*>(factoryOrRc);
+ }
+ };
+
+ static_assert(sizeof(TType) == 24);
+
+ bool operator==(const TTypePtr& lhs, const TTypePtr& rhs) = delete;
+ bool operator!=(const TTypePtr& lhs, const TTypePtr& rhs) = delete;
+
+ /// @brief Check for strict equivalence of the types.
+ ///
+ /// @see NTi::NEq::TStrictlyEqual
+ ///
+ /// @{
+ bool operator==(const TType& lhs, const TType& rhs);
+ bool operator!=(const TType& lhs, const TType& rhs);
+ /// @}
+
+ /// A singular type. This type has only one value. When serialized, it takes no space because it carries no data.
+ ///
+ /// Historically, YQL's `Void` is what's known as unit type (see https://en.wikipedia.org/wiki/Unit_type),
+ /// i.e. a type with only one possible value. This is similar to Python's `NoneType` or Rust's `()`.
+ class TVoidType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TVoidTypePtr AsPtr() const noexcept {
+ return const_cast<TVoidType*>(this);
+ }
+
+ private:
+ explicit TVoidType();
+
+ public:
+ static TVoidTypePtr Instance();
+ static const TVoidType* InstanceRaw();
+
+ protected:
+ const TVoidType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// Create new `Void` type using the default heap factory.
+ TVoidTypePtr Void();
+
+ /// A singular type, value of an empty optional.
+ ///
+ /// This type is used by YQL for `NULL` literal. Use `TVoidType` unless you have reasons to use this one.
+ class TNullType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TNullTypePtr AsPtr() const noexcept {
+ return const_cast<TNullType*>(this);
+ }
+
+ private:
+ explicit TNullType();
+
+ public:
+ static TNullTypePtr Instance();
+ static const TNullType* InstanceRaw();
+
+ protected:
+ const TNullType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// Base class for all primitive types.
+ class TPrimitiveType: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TPrimitiveTypePtr AsPtr() const noexcept {
+ return const_cast<TPrimitiveType*>(this);
+ }
+
+ protected:
+ explicit TPrimitiveType(TMaybe<ui64> hash, EPrimitiveTypeName primitiveTypeName) noexcept;
+
+ public:
+ /// Get name of this primitive type as a `NTi::EPrimitiveTypeName` enumerator.
+ EPrimitiveTypeName GetPrimitiveTypeName() const noexcept {
+ return ToPrimitiveTypeName(GetTypeName());
+ }
+
+ /// Cast this scalar class down to the most-derived type and pass it to the `visitor`.
+ ///
+ /// This function works like `NTi::TType::Visit`, but only handles primitive types.
+ ///
+ ///
+ /// # Example:
+ ///
+ /// ```
+ /// auto name = scalar->VisitPrimitive(TOverloaded{
+ /// [](TBoolTypePtr t) { return "Bool" },
+ /// [](TStringTypePtr t) { return "String" },
+ /// // ...
+ /// });
+ /// ```
+ template <typename V>
+ inline decltype(auto) VisitPrimitive(V&& visitor) const;
+
+ /// Like `VisitPrimitive`, but passes raw pointers to the visitor.
+ template <typename V>
+ inline decltype(auto) VisitPrimitiveRaw(V&& visitor) const;
+ };
+
+ /// A logical type capable of holding one of the two values: true or false.
+ class TBoolType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TBoolTypePtr AsPtr() const noexcept {
+ return const_cast<TBoolType*>(this);
+ }
+
+ private:
+ explicit TBoolType();
+
+ public:
+ static TBoolTypePtr Instance();
+ static const TBoolType* InstanceRaw();
+
+ protected:
+ const TBoolType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A signed integer, one byte.
+ class TInt8Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TInt8TypePtr AsPtr() const noexcept {
+ return const_cast<TInt8Type*>(this);
+ }
+
+ private:
+ explicit TInt8Type();
+
+ public:
+ static TInt8TypePtr Instance();
+ static const TInt8Type* InstanceRaw();
+
+ protected:
+ const TInt8Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A signed integer, two bytes.
+ class TInt16Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TInt16TypePtr AsPtr() const noexcept {
+ return const_cast<TInt16Type*>(this);
+ }
+
+ private:
+ explicit TInt16Type();
+
+ public:
+ static TInt16TypePtr Instance();
+ static const TInt16Type* InstanceRaw();
+
+ protected:
+ const TInt16Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A signed integer, four bytes.
+ class TInt32Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TInt32TypePtr AsPtr() const noexcept {
+ return const_cast<TInt32Type*>(this);
+ }
+
+ private:
+ explicit TInt32Type();
+
+ public:
+ static TInt32TypePtr Instance();
+ static const TInt32Type* InstanceRaw();
+
+ protected:
+ const TInt32Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A signed integer, eight bytes.
+ class TInt64Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TInt64TypePtr AsPtr() const noexcept {
+ return const_cast<TInt64Type*>(this);
+ }
+
+ private:
+ explicit TInt64Type();
+
+ public:
+ static TInt64TypePtr Instance();
+ static const TInt64Type* InstanceRaw();
+
+ protected:
+ const TInt64Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An unsigned integer, one byte.
+ class TUint8Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUint8TypePtr AsPtr() const noexcept {
+ return const_cast<TUint8Type*>(this);
+ }
+
+ private:
+ explicit TUint8Type();
+
+ public:
+ static TUint8TypePtr Instance();
+ static const TUint8Type* InstanceRaw();
+
+ protected:
+ const TUint8Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An unsigned integer, two bytes.
+ class TUint16Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUint16TypePtr AsPtr() const noexcept {
+ return const_cast<TUint16Type*>(this);
+ }
+
+ private:
+ explicit TUint16Type();
+
+ public:
+ static TUint16TypePtr Instance();
+ static const TUint16Type* InstanceRaw();
+
+ protected:
+ const TUint16Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An unsigned integer, four bytes.
+ class TUint32Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUint32TypePtr AsPtr() const noexcept {
+ return const_cast<TUint32Type*>(this);
+ }
+
+ private:
+ explicit TUint32Type();
+
+ public:
+ static TUint32TypePtr Instance();
+ static const TUint32Type* InstanceRaw();
+
+ protected:
+ const TUint32Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An unsigned integer, eight bytes.
+ class TUint64Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUint64TypePtr AsPtr() const noexcept {
+ return const_cast<TUint64Type*>(this);
+ }
+
+ private:
+ explicit TUint64Type();
+
+ public:
+ static TUint64TypePtr Instance();
+ static const TUint64Type* InstanceRaw();
+
+ protected:
+ const TUint64Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A floating point number, four bytes.
+ class TFloatType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TFloatTypePtr AsPtr() const noexcept {
+ return const_cast<TFloatType*>(this);
+ }
+
+ private:
+ explicit TFloatType();
+
+ public:
+ static TFloatTypePtr Instance();
+ static const TFloatType* InstanceRaw();
+
+ protected:
+ const TFloatType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A floating point number, eight bytes.
+ class TDoubleType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TDoubleTypePtr AsPtr() const noexcept {
+ return const_cast<TDoubleType*>(this);
+ }
+
+ private:
+ explicit TDoubleType();
+
+ public:
+ static TDoubleTypePtr Instance();
+ static const TDoubleType* InstanceRaw();
+
+ protected:
+ const TDoubleType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A binary blob.
+ ///
+ /// This type can be used for any binary data. For text, consider using type `Utf8`.
+ class TStringType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TStringTypePtr AsPtr() const noexcept {
+ return const_cast<TStringType*>(this);
+ }
+
+ private:
+ explicit TStringType();
+
+ public:
+ static TStringTypePtr Instance();
+ static const TStringType* InstanceRaw();
+
+ protected:
+ const TStringType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A utf-8 encoded text.
+ class TUtf8Type final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUtf8TypePtr AsPtr() const noexcept {
+ return const_cast<TUtf8Type*>(this);
+ }
+
+ private:
+ explicit TUtf8Type();
+
+ public:
+ static TUtf8TypePtr Instance();
+ static const TUtf8Type* InstanceRaw();
+
+ protected:
+ const TUtf8Type* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An absolute point in time in range `[1970-01-01, 2106-01-01)`, precision up to days.
+ class TDateType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TDateTypePtr AsPtr() const noexcept {
+ return const_cast<TDateType*>(this);
+ }
+
+ private:
+ explicit TDateType();
+
+ public:
+ static TDateTypePtr Instance();
+ static const TDateType* InstanceRaw();
+
+ protected:
+ const TDateType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An absolute point in time in range `[1970-01-01, 2106-01-01)`, precision up to seconds.
+ class TDatetimeType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TDatetimeTypePtr AsPtr() const noexcept {
+ return const_cast<TDatetimeType*>(this);
+ }
+
+ private:
+ explicit TDatetimeType();
+
+ public:
+ static TDatetimeTypePtr Instance();
+ static const TDatetimeType* InstanceRaw();
+
+ protected:
+ const TDatetimeType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// An absolute point in time in range `[1970-01-01, 2106-01-01)`, precision up to microseconds.
+ class TTimestampType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TTimestampTypePtr AsPtr() const noexcept {
+ return const_cast<TTimestampType*>(this);
+ }
+
+ private:
+ explicit TTimestampType();
+
+ public:
+ static TTimestampTypePtr Instance();
+ static const TTimestampType* InstanceRaw();
+
+ protected:
+ const TTimestampType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// `TDateType` with additional timezone mark.
+ class TTzDateType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TTzDateTypePtr AsPtr() const noexcept {
+ return const_cast<TTzDateType*>(this);
+ }
+
+ private:
+ explicit TTzDateType();
+
+ public:
+ static TTzDateTypePtr Instance();
+ static const TTzDateType* InstanceRaw();
+
+ protected:
+ const TTzDateType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// `TDatetimeType` with additional timezone mark.
+ class TTzDatetimeType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TTzDatetimeTypePtr AsPtr() const noexcept {
+ return const_cast<TTzDatetimeType*>(this);
+ }
+
+ private:
+ explicit TTzDatetimeType();
+
+ public:
+ static TTzDatetimeTypePtr Instance();
+ static const TTzDatetimeType* InstanceRaw();
+
+ protected:
+ const TTzDatetimeType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// `TTimestampType` with additional timezone mark.
+ class TTzTimestampType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TTzTimestampTypePtr AsPtr() const noexcept {
+ return const_cast<TTzTimestampType*>(this);
+ }
+
+ private:
+ explicit TTzTimestampType();
+
+ public:
+ static TTzTimestampTypePtr Instance();
+ static const TTzTimestampType* InstanceRaw();
+
+ protected:
+ const TTzTimestampType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// Signed delta between two timestamps.
+ class TIntervalType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TIntervalTypePtr AsPtr() const noexcept {
+ return const_cast<TIntervalType*>(this);
+ }
+
+ private:
+ explicit TIntervalType();
+
+ public:
+ static TIntervalTypePtr Instance();
+ static const TIntervalType* InstanceRaw();
+
+ protected:
+ const TIntervalType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A 128-bit number with controlled exponent/significand size.
+ ///
+ /// Decimal is a type for extra precise calculations. Internally, it is represented by a float-like 128-bit number.
+ ///
+ /// Two parameters control number of decimal digits in the decimal value. `Precision` is the total number
+ /// of decimal digits. `Scale` is the number of digits after the decimal point.
+ class TDecimalType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TDecimalTypePtr AsPtr() const noexcept {
+ return const_cast<TDecimalType*>(this);
+ }
+
+ private:
+ explicit TDecimalType(TMaybe<ui64> hash, ui8 precision, ui8 scale) noexcept;
+ static TDecimalTypePtr Create(ITypeFactory& factory, ui8 precision, ui8 scale);
+ static const TDecimalType* CreateRaw(ITypeFactory& factory, ui8 precision, ui8 scale);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get total number of decimal digits.
+ ui8 GetPrecision() const noexcept {
+ return Precision_;
+ }
+
+ /// Get number of decimal digits after the decimal point.
+ ui8 GetScale() const noexcept {
+ return Scale_;
+ }
+
+ protected:
+ const TDecimalType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ ui8 Precision_;
+ ui8 Scale_;
+ };
+
+ /// A string with valid JSON.
+ class TJsonType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TJsonTypePtr AsPtr() const noexcept {
+ return const_cast<TJsonType*>(this);
+ }
+
+ private:
+ explicit TJsonType();
+
+ public:
+ static TJsonTypePtr Instance();
+ static const TJsonType* InstanceRaw();
+
+ protected:
+ const TJsonType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A string with valid YSON.
+ class TYsonType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TYsonTypePtr AsPtr() const noexcept {
+ return const_cast<TYsonType*>(this);
+ }
+
+ private:
+ explicit TYsonType();
+
+ public:
+ static TYsonTypePtr Instance();
+ static const TYsonType* InstanceRaw();
+
+ protected:
+ const TYsonType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// A string with valid UUID.
+ class TUuidType final: public TPrimitiveType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TUuidTypePtr AsPtr() const noexcept {
+ return const_cast<TUuidType*>(this);
+ }
+
+ private:
+ explicit TUuidType();
+
+ public:
+ static TUuidTypePtr Instance();
+ static const TUuidType* InstanceRaw();
+
+ protected:
+ const TUuidType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+ };
+
+ /// Object which can store a value or a singular `NULL` value.
+ ///
+ /// This type is used to encode a value or its absence.
+ class TOptionalType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TOptionalTypePtr AsPtr() const noexcept {
+ return const_cast<TOptionalType*>(this);
+ }
+
+ private:
+ explicit TOptionalType(TMaybe<ui64> hash, const TType* item) noexcept;
+ static TOptionalTypePtr Create(ITypeFactory& factory, TTypePtr item);
+ static const TOptionalType* CreateRaw(ITypeFactory& factory, const TType* item);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get underlying type.
+ TTypePtr GetItemType() const noexcept {
+ return GetItemTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetMemberType`, but returns a raw pointer.
+ const TType* GetItemTypeRaw() const noexcept {
+ return Item_;
+ }
+
+ protected:
+ const TOptionalType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ const TType* Item_;
+ };
+
+ /// A variable-size collection of homogeneous values.
+ class TListType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TListTypePtr AsPtr() const noexcept {
+ return const_cast<TListType*>(this);
+ }
+
+ private:
+ explicit TListType(TMaybe<ui64> hash, const TType* item) noexcept;
+ static TListTypePtr Create(ITypeFactory& factory, TTypePtr item);
+ static const TListType* CreateRaw(ITypeFactory& factory, const TType* item);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get underlying type.
+ TTypePtr GetItemType() const noexcept {
+ return GetItemTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetMemberType`, but returns a raw pointer.
+ const TType* GetItemTypeRaw() const noexcept {
+ return Item_;
+ }
+
+ protected:
+ const TListType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ const TType* Item_;
+ };
+
+ /// An associative key-value container.
+ ///
+ /// Values of this type are usually represented as hashmaps.
+ class TDictType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TDictTypePtr AsPtr() const noexcept {
+ return const_cast<TDictType*>(this);
+ }
+
+ private:
+ explicit TDictType(TMaybe<ui64> hash, const TType* key, const TType* value) noexcept;
+ static TDictTypePtr Create(ITypeFactory& factory, TTypePtr key, TTypePtr value);
+ static const TDictType* CreateRaw(ITypeFactory& factory, const TType* key, const TType* value);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get the key type.
+ TTypePtr GetKeyType() const noexcept {
+ return GetKeyTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetKeyType`, but returns a raw pointer.
+ const TType* GetKeyTypeRaw() const noexcept {
+ return Key_;
+ }
+
+ /// Get the value type.
+ TTypePtr GetValueType() const noexcept {
+ return GetValueTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetValueType`, but returns a raw pointer.
+ const TType* GetValueTypeRaw() const noexcept {
+ return Value_;
+ }
+
+ protected:
+ const TDictType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ const TType* Key_;
+ const TType* Value_;
+ };
+
+ /// A fixed-size collection of named heterogeneous values.
+ ///
+ /// Structs represent multiple values grouped into a single entity. Values stored in a struct are called items.
+ /// Each item have an associated type and a name.
+ ///
+ /// Struct type is represented by an array of item types and their names.
+ ///
+ /// Even though struct elements are accessed by their names, we use vector to represent struct type because order
+ /// of struct items matters. If affects memory layout of a struct, how struct is serialized and deserialized.
+ /// Items order is vital for struct versioning. New fields should always be added to the end of the struct.
+ /// This way older parsers can read values serialized by newer writers: they'll simply read known head of a struct
+ /// and skip tail that contains unknown fields.
+ ///
+ ///
+ /// # Struct names
+ ///
+ /// Each struct defined by YDL must have an associated name. This name is used to refer struct in code, to generate
+ /// code representing this struct in other programming languages, and to report errors. The struct's name is saved
+ /// in this field.
+ ///
+ /// Note that, even though YDL requires struct names, name field is optional because other systems might
+ /// use anonymous structs (or maybe they dont't use struct names at all).
+ ///
+ /// Note also that type aliases (especially `newtype`) use tags to name types, so primitives don't have name field.
+ class TStructType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+ friend class TStructBuilderRaw;
+
+ public:
+ TStructTypePtr AsPtr() const noexcept {
+ return const_cast<TStructType*>(this);
+ }
+
+ public:
+ /// A single struct element.
+ class TMember {
+ public:
+ TMember(TStringBuf name, const TType* type);
+
+ public:
+ /// Get name of this item.
+ TStringBuf GetName() const {
+ return Name_;
+ }
+
+ /// Get type of this item.
+ TTypePtr GetType() const {
+ return GetTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetType`, but returns a raw pointer.
+ const TType* GetTypeRaw() const {
+ return Type_;
+ }
+
+ /// Calculate this item's hash. Hashes follow the 'strict equivalence' (see type_equivalence.h).
+ ui64 Hash() const;
+
+ private:
+ TStringBuf Name_;
+ const TType* Type_;
+ };
+
+ using TMembers = TConstArrayRef<TMember>;
+
+ /// Like `TMember`, but owns its contents. Used in non-raw constructors to guarantee data validity.
+ class TOwnedMember {
+ public:
+ TOwnedMember(TString name, TTypePtr type);
+
+ public:
+ operator TMember() const&;
+
+ private:
+ TString Name_;
+ TTypePtr Type_;
+ };
+
+ using TOwnedMembers = TConstArrayRef<TOwnedMember>;
+
+ private:
+ explicit TStructType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, TMembers members, TConstArrayRef<size_t> sortedMembers) noexcept;
+ static TStructTypePtr Create(ITypeFactory& factory, TOwnedMembers members);
+ static TStructTypePtr Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TOwnedMembers members);
+ static const TStructType* CreateRaw(ITypeFactory& factory, TMembers members);
+ static const TStructType* CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, TMembers members);
+ static void MakeSortedMembers(TMembers members, TArrayRef<size_t> sortedItems);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get name of this struct.
+ TMaybe<TStringBuf> GetName() const noexcept {
+ return Name_;
+ }
+
+ /// Get description of structure members.
+ TMembers GetMembers() const noexcept {
+ return Members_;
+ }
+
+ /// Check if there is an item with the given name in this struct.
+ bool HasMember(TStringBuf name) const noexcept;
+
+ /// Lookup struct item by name. Throw an error if there is no such item.
+ const TMember& GetMember(TStringBuf name) const;
+
+ /// Lookup struct item by name, return its index or `-1`, if item was not found.
+ /// This function works in `O(log(n))` time, where `n` is number of struct members.
+ ssize_t GetMemberIndex(TStringBuf name) const noexcept;
+
+ protected:
+ const TStructType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ TMaybe<TStringBuf> Name_;
+ TMembers Members_;
+ TConstArrayRef<size_t> SortedMembers_;
+ };
+
+ /// A fixed-size collection of named heterogeneous values.
+ ///
+ /// Tuples, like structs, represent multiple values, also called items, grouped into a single entity. Unlike
+ /// structs, though, tuple items are unnamed. Instead of names, they are accessed by their indexes.
+ ///
+ /// For a particular tuple type, number of items, their order and types are fixed, they should be known before
+ /// creating instances of tuples and can't change over time.
+ ///
+ ///
+ /// # Tuple names
+ ///
+ /// YDL requires each tuple definition to have a name (see `TStructType`). The name might not be mandatory
+ /// in other systems, so name field is optional.
+ class TTupleType: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+ friend class TTupleBuilderRaw;
+
+ public:
+ TTupleTypePtr AsPtr() const noexcept {
+ return const_cast<TTupleType*>(this);
+ }
+
+ public:
+ /// A single tuple element.
+ class TElement {
+ public:
+ TElement(const TType* type);
+
+ public:
+ /// Get type of this item.
+ TTypePtr GetType() const {
+ return GetTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetType`, but returns a raw pointer.
+ const TType* GetTypeRaw() const {
+ return Type_;
+ }
+
+ /// Calculate this item's hash. Hashes follow the 'strict equivalence' (see type_equivalence.h).
+ ui64 Hash() const;
+
+ private:
+ const TType* Type_;
+ };
+
+ using TElements = TConstArrayRef<TElement>;
+
+ /// Like `TElement`, but owns its contents. Used in non-raw constructors to guarantee data validity.
+ class TOwnedElement {
+ public:
+ TOwnedElement(TTypePtr type);
+
+ public:
+ operator TElement() const&;
+
+ private:
+ TTypePtr Type_;
+ };
+
+ using TOwnedElements = TConstArrayRef<TOwnedElement>;
+
+ private:
+ explicit TTupleType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, TElements items) noexcept;
+ static TTupleTypePtr Create(ITypeFactory& factory, TOwnedElements items);
+ static TTupleTypePtr Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TOwnedElements items);
+ static const TTupleType* CreateRaw(ITypeFactory& factory, TElements items);
+ static const TTupleType* CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, TElements items);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get name of this type.
+ TMaybe<TStringBuf> GetName() const noexcept {
+ return Name_;
+ }
+
+ /// Get description of tuple items.
+ TElements GetElements() const noexcept {
+ return Elements_;
+ }
+
+ protected:
+ const TTupleType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ TMaybe<TStringBuf> Name_;
+ TElements Elements_;
+ };
+
+ /// A tagged union with named or unnamed alternatives (a.k.a. variant over struct or variant over tuple).
+ ///
+ /// Variants are used to store values of different types.
+ ///
+ /// For example, a variant over struct which holds an ip address could look like
+ /// `Ip = Variant<v4: IpV4, v6: IpV6>`, where `IpV4` and `IpV6` are some other types. Now, a value of type `Ip`
+ /// could store either a value of type `IpV4` or a value of type `IpV6`.
+ ///
+ /// Even though item types can be the same, each item represents a distinct state of a variant. For example,
+ /// a variant for a user identifier can look like `Uid = Variant<yuid: String, ip: String>`. This variant can
+ /// contain either user's yandexuid of user's ip. Despite both items are of the same type `String`, `Uid` which
+ /// contains a `yuid` and `Uid` which contains an `ip` are never equal.
+ /// That is, `Uid.yuid("000000") != Uid.ip("000000")`.
+ ///
+ /// Exactly like with structs or tuples, order of variant items matter. Indexes of variant items are used
+ /// instead of names when variant is serialized.
+ ///
+ ///
+ /// # Variant names
+ ///
+ /// YDL requires each variant definition to have a name (see `TStructType`). The name might not be mandatory
+ /// in other systems, so name field is optional.
+ class TVariantType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+ friend class TStructBuilderRaw;
+ friend class TTupleBuilderRaw;
+
+ public:
+ TVariantTypePtr AsPtr() const noexcept {
+ return const_cast<TVariantType*>(this);
+ }
+
+ private:
+ explicit TVariantType(TMaybe<ui64> hash, TMaybe<TStringBuf> name, const TType* inner) noexcept;
+ static TVariantTypePtr Create(ITypeFactory& factory, TTypePtr inner);
+ static TVariantTypePtr Create(ITypeFactory& factory, TMaybe<TStringBuf> name, TTypePtr inner);
+ static const TVariantType* CreateRaw(ITypeFactory& factory, const TType* inner);
+ static const TVariantType* CreateRaw(ITypeFactory& factory, TMaybe<TStringBuf> name, const TType* inner);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get name of this variant.
+ TMaybe<TStringBuf> GetName() const noexcept {
+ return Name_;
+ }
+
+ /// Get vector of variant items.
+ TTypePtr GetUnderlyingType() const noexcept {
+ return Underlying_->AsPtr();
+ }
+
+ /// Like `GetUnderlyingType`, but returns a raw pointer.
+ const TType* GetUnderlyingTypeRaw() const noexcept {
+ return Underlying_;
+ }
+
+ /// Check if this variant's inner type is a struct.
+ bool IsVariantOverStruct() const noexcept {
+ return GetUnderlyingTypeRaw()->GetTypeName() == ETypeName::Struct;
+ }
+
+ /// Check if this variant's inner type is a tuple.
+ bool IsVariantOverTuple() const noexcept {
+ return GetUnderlyingTypeRaw()->GetTypeName() == ETypeName::Tuple;
+ }
+
+ /// Visit inner type of this variant. This function works like `Visit`, but only casts inner type
+ /// to struct or tuple, so you don't need to handle other types in a visitor.
+ template <typename V>
+ inline decltype(auto) VisitUnderlying(V&& visitor) const;
+
+ /// Like `VisitUnderlying`, but passes const raw pointers to the visitor.
+ template <typename V>
+ inline decltype(auto) VisitUnderlyingRaw(V&& visitor) const;
+
+ protected:
+ const TVariantType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ TMaybe<TStringBuf> Name_;
+ const TType* Underlying_;
+ };
+
+ /// Named or tagged or user-defined type.
+ ///
+ /// Tags are used to create new types from existing ones by assigning them a tag, i.e. a name. They wrap other types
+ /// adding them additional semantics.
+ ///
+ /// On physical level, tags do not change types. Both `Tagged<Int32, 'GeoId'>` and `Int32` have exactly the same
+ /// representation when serialized.
+ ///
+ /// On logical level, tags change semantics of a type. This can affect how types are displayed, how they
+ /// are checked and converted, etc.
+ class TTaggedType final: public TType {
+ friend class TType;
+ friend class ITypeFactoryInternal;
+ friend class ITypeFactory;
+ friend class IPoolTypeFactory;
+
+ public:
+ TTaggedTypePtr AsPtr() const noexcept {
+ return const_cast<TTaggedType*>(this);
+ }
+
+ private:
+ explicit TTaggedType(TMaybe<ui64> hash, const TType* type, TStringBuf tag) noexcept;
+ static TTaggedTypePtr Create(ITypeFactory& factory, TTypePtr type, TStringBuf tag);
+ static const TTaggedType* CreateRaw(ITypeFactory& factory, const TType* type, TStringBuf tag);
+
+ protected:
+ ui64 CalculateHash() const noexcept;
+
+ public:
+ /// Get tag content, i.e. name of the new type.
+ TStringBuf GetTag() const noexcept {
+ return Tag_;
+ }
+
+ /// Get the wrapped type.
+ TTypePtr GetItemType() const noexcept {
+ return GetItemTypeRaw()->AsPtr();
+ }
+
+ /// Like `GetMemberType`, but returns a raw pointer.
+ const TType* GetItemTypeRaw() const noexcept {
+ return Item_;
+ }
+
+ protected:
+ const TTaggedType* Clone(ITypeFactoryInternal& factory) const noexcept;
+ void Drop(ITypeFactoryInternal& factory) noexcept;
+
+ private:
+ const TType* Item_;
+ TStringBuf Tag_;
+ };
+
+#ifdef __JETBRAINS_IDE__
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-static-cast-downcast"
+#endif
+
+ bool TType::IsPrimitive() const noexcept {
+ return ::NTi::IsPrimitive(TypeName_);
+ }
+
+ TPrimitiveTypePtr TType::AsPrimitive() const noexcept {
+ return AsPrimitiveRaw()->AsPtr();
+ }
+
+ const TPrimitiveType* TType::AsPrimitiveRaw() const noexcept {
+ Y_VERIFY(IsPrimitive());
+ return static_cast<const TPrimitiveType*>(this);
+ }
+
+ bool TType::IsVoid() const noexcept {
+ return TypeName_ == ETypeName::Void;
+ }
+
+ TVoidTypePtr TType::AsVoid() const noexcept {
+ return AsVoidRaw()->AsPtr();
+ }
+
+ const TVoidType* TType::AsVoidRaw() const noexcept {
+ Y_VERIFY(IsVoid());
+ return static_cast<const TVoidType*>(this);
+ }
+
+ bool TType::IsNull() const noexcept {
+ return TypeName_ == ETypeName::Null;
+ }
+
+ TNullTypePtr TType::AsNull() const noexcept {
+ return AsNullRaw()->AsPtr();
+ }
+
+ const TNullType* TType::AsNullRaw() const noexcept {
+ Y_VERIFY(IsNull());
+ return static_cast<const TNullType*>(this);
+ }
+
+ bool TType::IsBool() const noexcept {
+ return TypeName_ == ETypeName::Bool;
+ }
+
+ TBoolTypePtr TType::AsBool() const noexcept {
+ return AsBoolRaw()->AsPtr();
+ }
+
+ const TBoolType* TType::AsBoolRaw() const noexcept {
+ Y_VERIFY(IsBool());
+ return static_cast<const TBoolType*>(this);
+ }
+
+ bool TType::IsInt8() const noexcept {
+ return TypeName_ == ETypeName::Int8;
+ }
+
+ TInt8TypePtr TType::AsInt8() const noexcept {
+ return AsInt8Raw()->AsPtr();
+ }
+
+ const TInt8Type* TType::AsInt8Raw() const noexcept {
+ Y_VERIFY(IsInt8());
+ return static_cast<const TInt8Type*>(this);
+ }
+
+ bool TType::IsInt16() const noexcept {
+ return TypeName_ == ETypeName::Int16;
+ }
+
+ TInt16TypePtr TType::AsInt16() const noexcept {
+ return AsInt16Raw()->AsPtr();
+ }
+
+ const TInt16Type* TType::AsInt16Raw() const noexcept {
+ Y_VERIFY(IsInt16());
+ return static_cast<const TInt16Type*>(this);
+ }
+
+ bool TType::IsInt32() const noexcept {
+ return TypeName_ == ETypeName::Int32;
+ }
+
+ TInt32TypePtr TType::AsInt32() const noexcept {
+ return AsInt32Raw()->AsPtr();
+ }
+
+ const TInt32Type* TType::AsInt32Raw() const noexcept {
+ Y_VERIFY(IsInt32());
+ return static_cast<const TInt32Type*>(this);
+ }
+
+ bool TType::IsInt64() const noexcept {
+ return TypeName_ == ETypeName::Int64;
+ }
+
+ TInt64TypePtr TType::AsInt64() const noexcept {
+ return AsInt64Raw()->AsPtr();
+ }
+
+ const TInt64Type* TType::AsInt64Raw() const noexcept {
+ Y_VERIFY(IsInt64());
+ return static_cast<const TInt64Type*>(this);
+ }
+
+ bool TType::IsUint8() const noexcept {
+ return TypeName_ == ETypeName::Uint8;
+ }
+
+ TUint8TypePtr TType::AsUint8() const noexcept {
+ return AsUint8Raw()->AsPtr();
+ }
+
+ const TUint8Type* TType::AsUint8Raw() const noexcept {
+ Y_VERIFY(IsUint8());
+ return static_cast<const TUint8Type*>(this);
+ }
+
+ bool TType::IsUint16() const noexcept {
+ return TypeName_ == ETypeName::Uint16;
+ }
+
+ TUint16TypePtr TType::AsUint16() const noexcept {
+ return AsUint16Raw()->AsPtr();
+ }
+
+ const TUint16Type* TType::AsUint16Raw() const noexcept {
+ Y_VERIFY(IsUint16());
+ return static_cast<const TUint16Type*>(this);
+ }
+
+ bool TType::IsUint32() const noexcept {
+ return TypeName_ == ETypeName::Uint32;
+ }
+
+ TUint32TypePtr TType::AsUint32() const noexcept {
+ return AsUint32Raw()->AsPtr();
+ }
+
+ const TUint32Type* TType::AsUint32Raw() const noexcept {
+ Y_VERIFY(IsUint32());
+ return static_cast<const TUint32Type*>(this);
+ }
+
+ bool TType::IsUint64() const noexcept {
+ return TypeName_ == ETypeName::Uint64;
+ }
+
+ TUint64TypePtr TType::AsUint64() const noexcept {
+ return AsUint64Raw()->AsPtr();
+ }
+
+ const TUint64Type* TType::AsUint64Raw() const noexcept {
+ Y_VERIFY(IsUint64());
+ return static_cast<const TUint64Type*>(this);
+ }
+
+ bool TType::IsFloat() const noexcept {
+ return TypeName_ == ETypeName::Float;
+ }
+
+ TFloatTypePtr TType::AsFloat() const noexcept {
+ return AsFloatRaw()->AsPtr();
+ }
+
+ const TFloatType* TType::AsFloatRaw() const noexcept {
+ Y_VERIFY(IsFloat());
+ return static_cast<const TFloatType*>(this);
+ }
+
+ bool TType::IsDouble() const noexcept {
+ return TypeName_ == ETypeName::Double;
+ }
+
+ TDoubleTypePtr TType::AsDouble() const noexcept {
+ return AsDoubleRaw()->AsPtr();
+ }
+
+ const TDoubleType* TType::AsDoubleRaw() const noexcept {
+ Y_VERIFY(IsDouble());
+ return static_cast<const TDoubleType*>(this);
+ }
+
+ bool TType::IsString() const noexcept {
+ return TypeName_ == ETypeName::String;
+ }
+
+ TStringTypePtr TType::AsString() const noexcept {
+ return AsStringRaw()->AsPtr();
+ }
+
+ const TStringType* TType::AsStringRaw() const noexcept {
+ Y_VERIFY(IsString());
+ return static_cast<const TStringType*>(this);
+ }
+
+ bool TType::IsUtf8() const noexcept {
+ return TypeName_ == ETypeName::Utf8;
+ }
+
+ TUtf8TypePtr TType::AsUtf8() const noexcept {
+ return AsUtf8Raw()->AsPtr();
+ }
+
+ const TUtf8Type* TType::AsUtf8Raw() const noexcept {
+ Y_VERIFY(IsUtf8());
+ return static_cast<const TUtf8Type*>(this);
+ }
+
+ bool TType::IsDate() const noexcept {
+ return TypeName_ == ETypeName::Date;
+ }
+
+ TDateTypePtr TType::AsDate() const noexcept {
+ return AsDateRaw()->AsPtr();
+ }
+
+ const TDateType* TType::AsDateRaw() const noexcept {
+ Y_VERIFY(IsDate());
+ return static_cast<const TDateType*>(this);
+ }
+
+ bool TType::IsDatetime() const noexcept {
+ return TypeName_ == ETypeName::Datetime;
+ }
+
+ TDatetimeTypePtr TType::AsDatetime() const noexcept {
+ return AsDatetimeRaw()->AsPtr();
+ }
+
+ const TDatetimeType* TType::AsDatetimeRaw() const noexcept {
+ Y_VERIFY(IsDatetime());
+ return static_cast<const TDatetimeType*>(this);
+ }
+
+ bool TType::IsTimestamp() const noexcept {
+ return TypeName_ == ETypeName::Timestamp;
+ }
+
+ TTimestampTypePtr TType::AsTimestamp() const noexcept {
+ return AsTimestampRaw()->AsPtr();
+ }
+
+ const TTimestampType* TType::AsTimestampRaw() const noexcept {
+ Y_VERIFY(IsTimestamp());
+ return static_cast<const TTimestampType*>(this);
+ }
+
+ bool TType::IsTzDate() const noexcept {
+ return TypeName_ == ETypeName::TzDate;
+ }
+
+ TTzDateTypePtr TType::AsTzDate() const noexcept {
+ return AsTzDateRaw()->AsPtr();
+ }
+
+ const TTzDateType* TType::AsTzDateRaw() const noexcept {
+ Y_VERIFY(IsTzDate());
+ return static_cast<const TTzDateType*>(this);
+ }
+
+ bool TType::IsTzDatetime() const noexcept {
+ return TypeName_ == ETypeName::TzDatetime;
+ }
+
+ TTzDatetimeTypePtr TType::AsTzDatetime() const noexcept {
+ return AsTzDatetimeRaw()->AsPtr();
+ }
+
+ const TTzDatetimeType* TType::AsTzDatetimeRaw() const noexcept {
+ Y_VERIFY(IsTzDatetime());
+ return static_cast<const TTzDatetimeType*>(this);
+ }
+
+ bool TType::IsTzTimestamp() const noexcept {
+ return TypeName_ == ETypeName::TzTimestamp;
+ }
+
+ TTzTimestampTypePtr TType::AsTzTimestamp() const noexcept {
+ return AsTzTimestampRaw()->AsPtr();
+ }
+
+ const TTzTimestampType* TType::AsTzTimestampRaw() const noexcept {
+ Y_VERIFY(IsTzTimestamp());
+ return static_cast<const TTzTimestampType*>(this);
+ }
+
+ bool TType::IsInterval() const noexcept {
+ return TypeName_ == ETypeName::Interval;
+ }
+
+ TIntervalTypePtr TType::AsInterval() const noexcept {
+ return AsIntervalRaw()->AsPtr();
+ }
+
+ const TIntervalType* TType::AsIntervalRaw() const noexcept {
+ Y_VERIFY(IsInterval());
+ return static_cast<const TIntervalType*>(this);
+ }
+
+ bool TType::IsDecimal() const noexcept {
+ return TypeName_ == ETypeName::Decimal;
+ }
+
+ TDecimalTypePtr TType::AsDecimal() const noexcept {
+ return AsDecimalRaw()->AsPtr();
+ }
+
+ const TDecimalType* TType::AsDecimalRaw() const noexcept {
+ Y_VERIFY(IsDecimal());
+ return static_cast<const TDecimalType*>(this);
+ }
+
+ bool TType::IsJson() const noexcept {
+ return TypeName_ == ETypeName::Json;
+ }
+
+ TJsonTypePtr TType::AsJson() const noexcept {
+ return AsJsonRaw()->AsPtr();
+ }
+
+ const TJsonType* TType::AsJsonRaw() const noexcept {
+ Y_VERIFY(IsJson());
+ return static_cast<const TJsonType*>(this);
+ }
+
+ bool TType::IsYson() const noexcept {
+ return TypeName_ == ETypeName::Yson;
+ }
+
+ TYsonTypePtr TType::AsYson() const noexcept {
+ return AsYsonRaw()->AsPtr();
+ }
+
+ const TYsonType* TType::AsYsonRaw() const noexcept {
+ Y_VERIFY(IsYson());
+ return static_cast<const TYsonType*>(this);
+ }
+
+ bool TType::IsUuid() const noexcept {
+ return TypeName_ == ETypeName::Uuid;
+ }
+
+ TUuidTypePtr TType::AsUuid() const noexcept {
+ return AsUuidRaw()->AsPtr();
+ }
+
+ const TUuidType* TType::AsUuidRaw() const noexcept {
+ Y_VERIFY(IsUuid());
+ return static_cast<const TUuidType*>(this);
+ }
+
+ bool TType::IsOptional() const noexcept {
+ return TypeName_ == ETypeName::Optional;
+ }
+
+ TOptionalTypePtr TType::AsOptional() const noexcept {
+ return AsOptionalRaw()->AsPtr();
+ }
+
+ const TOptionalType* TType::AsOptionalRaw() const noexcept {
+ Y_VERIFY(IsOptional());
+ return static_cast<const TOptionalType*>(this);
+ }
+
+ bool TType::IsList() const noexcept {
+ return TypeName_ == ETypeName::List;
+ }
+
+ TListTypePtr TType::AsList() const noexcept {
+ return AsListRaw()->AsPtr();
+ }
+
+ const TListType* TType::AsListRaw() const noexcept {
+ Y_VERIFY(IsList());
+ return static_cast<const TListType*>(this);
+ }
+
+ bool TType::IsDict() const noexcept {
+ return TypeName_ == ETypeName::Dict;
+ }
+
+ TDictTypePtr TType::AsDict() const noexcept {
+ return AsDictRaw()->AsPtr();
+ }
+
+ const TDictType* TType::AsDictRaw() const noexcept {
+ Y_VERIFY(IsDict());
+ return static_cast<const TDictType*>(this);
+ }
+
+ bool TType::IsStruct() const noexcept {
+ return TypeName_ == ETypeName::Struct;
+ }
+
+ TStructTypePtr TType::AsStruct() const noexcept {
+ return AsStructRaw()->AsPtr();
+ }
+
+ const TStructType* TType::AsStructRaw() const noexcept {
+ Y_VERIFY(IsStruct());
+ return static_cast<const TStructType*>(this);
+ }
+
+ bool TType::IsTuple() const noexcept {
+ return TypeName_ == ETypeName::Tuple;
+ }
+
+ TTupleTypePtr TType::AsTuple() const noexcept {
+ return AsTupleRaw()->AsPtr();
+ }
+
+ const TTupleType* TType::AsTupleRaw() const noexcept {
+ Y_VERIFY(IsTuple());
+ return static_cast<const TTupleType*>(this);
+ }
+
+ bool TType::IsVariant() const noexcept {
+ return TypeName_ == ETypeName::Variant;
+ }
+
+ TVariantTypePtr TType::AsVariant() const noexcept {
+ return AsVariantRaw()->AsPtr();
+ }
+
+ const TVariantType* TType::AsVariantRaw() const noexcept {
+ Y_VERIFY(IsVariant());
+ return static_cast<const TVariantType*>(this);
+ }
+
+ bool TType::IsTagged() const noexcept {
+ return TypeName_ == ETypeName::Tagged;
+ }
+
+ TTaggedTypePtr TType::AsTagged() const noexcept {
+ return AsTaggedRaw()->AsPtr();
+ }
+
+ const TTaggedType* TType::AsTaggedRaw() const noexcept {
+ Y_VERIFY(IsTagged());
+ return static_cast<const TTaggedType*>(this);
+ }
+
+#ifdef __JETBRAINS_IDE__
+#pragma clang diagnostic pop
+#endif
+
+ template <typename V>
+ decltype(auto) TType::Visit(V&& visitor) const {
+ switch (TypeName_) {
+ case ETypeName::Bool:
+ return std::forward<V>(visitor)(this->AsBool());
+ case ETypeName::Int8:
+ return std::forward<V>(visitor)(this->AsInt8());
+ case ETypeName::Int16:
+ return std::forward<V>(visitor)(this->AsInt16());
+ case ETypeName::Int32:
+ return std::forward<V>(visitor)(this->AsInt32());
+ case ETypeName::Int64:
+ return std::forward<V>(visitor)(this->AsInt64());
+ case ETypeName::Uint8:
+ return std::forward<V>(visitor)(this->AsUint8());
+ case ETypeName::Uint16:
+ return std::forward<V>(visitor)(this->AsUint16());
+ case ETypeName::Uint32:
+ return std::forward<V>(visitor)(this->AsUint32());
+ case ETypeName::Uint64:
+ return std::forward<V>(visitor)(this->AsUint64());
+ case ETypeName::Float:
+ return std::forward<V>(visitor)(this->AsFloat());
+ case ETypeName::Double:
+ return std::forward<V>(visitor)(this->AsDouble());
+ case ETypeName::String:
+ return std::forward<V>(visitor)(this->AsString());
+ case ETypeName::Utf8:
+ return std::forward<V>(visitor)(this->AsUtf8());
+ case ETypeName::Date:
+ return std::forward<V>(visitor)(this->AsDate());
+ case ETypeName::Datetime:
+ return std::forward<V>(visitor)(this->AsDatetime());
+ case ETypeName::Timestamp:
+ return std::forward<V>(visitor)(this->AsTimestamp());
+ case ETypeName::TzDate:
+ return std::forward<V>(visitor)(this->AsTzDate());
+ case ETypeName::TzDatetime:
+ return std::forward<V>(visitor)(this->AsTzDatetime());
+ case ETypeName::TzTimestamp:
+ return std::forward<V>(visitor)(this->AsTzTimestamp());
+ case ETypeName::Interval:
+ return std::forward<V>(visitor)(this->AsInterval());
+ case ETypeName::Decimal:
+ return std::forward<V>(visitor)(this->AsDecimal());
+ case ETypeName::Json:
+ return std::forward<V>(visitor)(this->AsJson());
+ case ETypeName::Yson:
+ return std::forward<V>(visitor)(this->AsYson());
+ case ETypeName::Uuid:
+ return std::forward<V>(visitor)(this->AsUuid());
+ case ETypeName::Void:
+ return std::forward<V>(visitor)(this->AsVoid());
+ case ETypeName::Null:
+ return std::forward<V>(visitor)(this->AsNull());
+ case ETypeName::Optional:
+ return std::forward<V>(visitor)(this->AsOptional());
+ case ETypeName::List:
+ return std::forward<V>(visitor)(this->AsList());
+ case ETypeName::Dict:
+ return std::forward<V>(visitor)(this->AsDict());
+ case ETypeName::Struct:
+ return std::forward<V>(visitor)(this->AsStruct());
+ case ETypeName::Tuple:
+ return std::forward<V>(visitor)(this->AsTuple());
+ case ETypeName::Variant:
+ return std::forward<V>(visitor)(this->AsVariant());
+ case ETypeName::Tagged:
+ return std::forward<V>(visitor)(this->AsTagged());
+ }
+
+ Y_UNREACHABLE();
+ }
+
+ template <typename V>
+ decltype(auto) TType::VisitRaw(V&& visitor) const {
+ switch (TypeName_) {
+ case ETypeName::Bool:
+ return std::forward<V>(visitor)(this->AsBoolRaw());
+ case ETypeName::Int8:
+ return std::forward<V>(visitor)(this->AsInt8Raw());
+ case ETypeName::Int16:
+ return std::forward<V>(visitor)(this->AsInt16Raw());
+ case ETypeName::Int32:
+ return std::forward<V>(visitor)(this->AsInt32Raw());
+ case ETypeName::Int64:
+ return std::forward<V>(visitor)(this->AsInt64Raw());
+ case ETypeName::Uint8:
+ return std::forward<V>(visitor)(this->AsUint8Raw());
+ case ETypeName::Uint16:
+ return std::forward<V>(visitor)(this->AsUint16Raw());
+ case ETypeName::Uint32:
+ return std::forward<V>(visitor)(this->AsUint32Raw());
+ case ETypeName::Uint64:
+ return std::forward<V>(visitor)(this->AsUint64Raw());
+ case ETypeName::Float:
+ return std::forward<V>(visitor)(this->AsFloatRaw());
+ case ETypeName::Double:
+ return std::forward<V>(visitor)(this->AsDoubleRaw());
+ case ETypeName::String:
+ return std::forward<V>(visitor)(this->AsStringRaw());
+ case ETypeName::Utf8:
+ return std::forward<V>(visitor)(this->AsUtf8Raw());
+ case ETypeName::Date:
+ return std::forward<V>(visitor)(this->AsDateRaw());
+ case ETypeName::Datetime:
+ return std::forward<V>(visitor)(this->AsDatetimeRaw());
+ case ETypeName::Timestamp:
+ return std::forward<V>(visitor)(this->AsTimestampRaw());
+ case ETypeName::TzDate:
+ return std::forward<V>(visitor)(this->AsTzDateRaw());
+ case ETypeName::TzDatetime:
+ return std::forward<V>(visitor)(this->AsTzDatetimeRaw());
+ case ETypeName::TzTimestamp:
+ return std::forward<V>(visitor)(this->AsTzTimestampRaw());
+ case ETypeName::Interval:
+ return std::forward<V>(visitor)(this->AsIntervalRaw());
+ case ETypeName::Decimal:
+ return std::forward<V>(visitor)(this->AsDecimalRaw());
+ case ETypeName::Json:
+ return std::forward<V>(visitor)(this->AsJsonRaw());
+ case ETypeName::Yson:
+ return std::forward<V>(visitor)(this->AsYsonRaw());
+ case ETypeName::Uuid:
+ return std::forward<V>(visitor)(this->AsUuidRaw());
+ case ETypeName::Void:
+ return std::forward<V>(visitor)(this->AsVoidRaw());
+ case ETypeName::Null:
+ return std::forward<V>(visitor)(this->AsNullRaw());
+ case ETypeName::Optional:
+ return std::forward<V>(visitor)(this->AsOptionalRaw());
+ case ETypeName::List:
+ return std::forward<V>(visitor)(this->AsListRaw());
+ case ETypeName::Dict:
+ return std::forward<V>(visitor)(this->AsDictRaw());
+ case ETypeName::Struct:
+ return std::forward<V>(visitor)(this->AsStructRaw());
+ case ETypeName::Tuple:
+ return std::forward<V>(visitor)(this->AsTupleRaw());
+ case ETypeName::Variant:
+ return std::forward<V>(visitor)(this->AsVariantRaw());
+ case ETypeName::Tagged:
+ return std::forward<V>(visitor)(this->AsTaggedRaw());
+ }
+
+ Y_UNREACHABLE();
+ }
+
+ template <typename V>
+ decltype(auto) TPrimitiveType::VisitPrimitive(V&& visitor) const {
+ switch (GetPrimitiveTypeName()) {
+ case EPrimitiveTypeName::Bool:
+ return std::forward<V>(visitor)(this->AsBool());
+ case EPrimitiveTypeName::Int8:
+ return std::forward<V>(visitor)(this->AsInt8());
+ case EPrimitiveTypeName::Int16:
+ return std::forward<V>(visitor)(this->AsInt16());
+ case EPrimitiveTypeName::Int32:
+ return std::forward<V>(visitor)(this->AsInt32());
+ case EPrimitiveTypeName::Int64:
+ return std::forward<V>(visitor)(this->AsInt64());
+ case EPrimitiveTypeName::Uint8:
+ return std::forward<V>(visitor)(this->AsUint8());
+ case EPrimitiveTypeName::Uint16:
+ return std::forward<V>(visitor)(this->AsUint16());
+ case EPrimitiveTypeName::Uint32:
+ return std::forward<V>(visitor)(this->AsUint32());
+ case EPrimitiveTypeName::Uint64:
+ return std::forward<V>(visitor)(this->AsUint64());
+ case EPrimitiveTypeName::Float:
+ return std::forward<V>(visitor)(this->AsFloat());
+ case EPrimitiveTypeName::Double:
+ return std::forward<V>(visitor)(this->AsDouble());
+ case EPrimitiveTypeName::String:
+ return std::forward<V>(visitor)(this->AsString());
+ case EPrimitiveTypeName::Utf8:
+ return std::forward<V>(visitor)(this->AsUtf8());
+ case EPrimitiveTypeName::Date:
+ return std::forward<V>(visitor)(this->AsDate());
+ case EPrimitiveTypeName::Datetime:
+ return std::forward<V>(visitor)(this->AsDatetime());
+ case EPrimitiveTypeName::Timestamp:
+ return std::forward<V>(visitor)(this->AsTimestamp());
+ case EPrimitiveTypeName::TzDate:
+ return std::forward<V>(visitor)(this->AsTzDate());
+ case EPrimitiveTypeName::TzDatetime:
+ return std::forward<V>(visitor)(this->AsTzDatetime());
+ case EPrimitiveTypeName::TzTimestamp:
+ return std::forward<V>(visitor)(this->AsTzTimestamp());
+ case EPrimitiveTypeName::Interval:
+ return std::forward<V>(visitor)(this->AsInterval());
+ case EPrimitiveTypeName::Decimal:
+ return std::forward<V>(visitor)(this->AsDecimal());
+ case EPrimitiveTypeName::Json:
+ return std::forward<V>(visitor)(this->AsJson());
+ case EPrimitiveTypeName::Yson:
+ return std::forward<V>(visitor)(this->AsYson());
+ case EPrimitiveTypeName::Uuid:
+ return std::forward<V>(visitor)(this->AsUuid());
+ }
+
+ Y_UNREACHABLE();
+ }
+
+ template <typename V>
+ decltype(auto) TPrimitiveType::VisitPrimitiveRaw(V&& visitor) const {
+ switch (GetPrimitiveTypeName()) {
+ case EPrimitiveTypeName::Bool:
+ return std::forward<V>(visitor)(this->AsBoolRaw());
+ case EPrimitiveTypeName::Int8:
+ return std::forward<V>(visitor)(this->AsInt8Raw());
+ case EPrimitiveTypeName::Int16:
+ return std::forward<V>(visitor)(this->AsInt16Raw());
+ case EPrimitiveTypeName::Int32:
+ return std::forward<V>(visitor)(this->AsInt32Raw());
+ case EPrimitiveTypeName::Int64:
+ return std::forward<V>(visitor)(this->AsInt64Raw());
+ case EPrimitiveTypeName::Uint8:
+ return std::forward<V>(visitor)(this->AsUint8Raw());
+ case EPrimitiveTypeName::Uint16:
+ return std::forward<V>(visitor)(this->AsUint16Raw());
+ case EPrimitiveTypeName::Uint32:
+ return std::forward<V>(visitor)(this->AsUint32Raw());
+ case EPrimitiveTypeName::Uint64:
+ return std::forward<V>(visitor)(this->AsUint64Raw());
+ case EPrimitiveTypeName::Float:
+ return std::forward<V>(visitor)(this->AsFloatRaw());
+ case EPrimitiveTypeName::Double:
+ return std::forward<V>(visitor)(this->AsDoubleRaw());
+ case EPrimitiveTypeName::String:
+ return std::forward<V>(visitor)(this->AsStringRaw());
+ case EPrimitiveTypeName::Utf8:
+ return std::forward<V>(visitor)(this->AsUtf8Raw());
+ case EPrimitiveTypeName::Date:
+ return std::forward<V>(visitor)(this->AsDateRaw());
+ case EPrimitiveTypeName::Datetime:
+ return std::forward<V>(visitor)(this->AsDatetimeRaw());
+ case EPrimitiveTypeName::Timestamp:
+ return std::forward<V>(visitor)(this->AsTimestampRaw());
+ case EPrimitiveTypeName::TzDate:
+ return std::forward<V>(visitor)(this->AsTzDateRaw());
+ case EPrimitiveTypeName::TzDatetime:
+ return std::forward<V>(visitor)(this->AsTzDatetimeRaw());
+ case EPrimitiveTypeName::TzTimestamp:
+ return std::forward<V>(visitor)(this->AsTzTimestampRaw());
+ case EPrimitiveTypeName::Interval:
+ return std::forward<V>(visitor)(this->AsIntervalRaw());
+ case EPrimitiveTypeName::Decimal:
+ return std::forward<V>(visitor)(this->AsDecimalRaw());
+ case EPrimitiveTypeName::Json:
+ return std::forward<V>(visitor)(this->AsJsonRaw());
+ case EPrimitiveTypeName::Yson:
+ return std::forward<V>(visitor)(this->AsYsonRaw());
+ case EPrimitiveTypeName::Uuid:
+ return std::forward<V>(visitor)(this->AsUuidRaw());
+ }
+
+ Y_UNREACHABLE();
+ }
+
+ template <typename V>
+ decltype(auto) TVariantType::VisitUnderlying(V&& visitor) const {
+ switch (GetUnderlyingTypeRaw()->GetTypeName()) {
+ case ETypeName::Struct:
+ return std::forward<V>(visitor)(this->GetUnderlyingTypeRaw()->AsStruct());
+ case ETypeName::Tuple:
+ return std::forward<V>(visitor)(this->GetUnderlyingTypeRaw()->AsTuple());
+ default:
+ Y_UNREACHABLE();
+ }
+ }
+
+ template <typename V>
+ decltype(auto) TVariantType::VisitUnderlyingRaw(V&& visitor) const {
+ switch (GetUnderlyingTypeRaw()->GetTypeName()) {
+ case ETypeName::Struct:
+ return std::forward<V>(visitor)(this->GetUnderlyingTypeRaw()->AsStructRaw());
+ case ETypeName::Tuple:
+ return std::forward<V>(visitor)(this->GetUnderlyingTypeRaw()->AsTupleRaw());
+ default:
+ Y_UNREACHABLE();
+ }
+ }
+} // namespace NTi
diff --git a/library/cpp/type_info/type_complexity.cpp b/library/cpp/type_info/type_complexity.cpp
new file mode 100644
index 0000000000..073c24b909
--- /dev/null
+++ b/library/cpp/type_info/type_complexity.cpp
@@ -0,0 +1,78 @@
+#include "type_complexity.h"
+
+#include "type.h"
+
+
+namespace NTi {
+
+int ComputeTypeComplexity(const TTypePtr& type)
+{
+ return ComputeTypeComplexity(type.Get());
+}
+
+int ComputeTypeComplexity(const TType* type)
+{
+ switch (type->GetTypeName()) {
+ case ETypeName::Bool:
+ case ETypeName::Int8:
+ case ETypeName::Int16:
+ case ETypeName::Int32:
+ case ETypeName::Int64:
+ case ETypeName::Uint8:
+ case ETypeName::Uint16:
+ case ETypeName::Uint32:
+ case ETypeName::Uint64:
+ case ETypeName::Float:
+ case ETypeName::Double:
+ case ETypeName::String:
+ case ETypeName::Utf8:
+ case ETypeName::Date:
+ case ETypeName::Datetime:
+ case ETypeName::Timestamp:
+ case ETypeName::TzDate:
+ case ETypeName::TzDatetime:
+ case ETypeName::TzTimestamp:
+ case ETypeName::Interval:
+ case ETypeName::Decimal:
+ case ETypeName::Json:
+ case ETypeName::Yson:
+ case ETypeName::Uuid:
+ case ETypeName::Void:
+ case ETypeName::Null:
+ return 1;
+
+ case ETypeName::Optional:
+ return 1 + ComputeTypeComplexity(type->AsOptionalRaw()->GetItemTypeRaw());
+
+ case ETypeName::List:
+ return 1 + ComputeTypeComplexity(type->AsListRaw()->GetItemTypeRaw());
+
+ case ETypeName::Dict:
+ return 1 + ComputeTypeComplexity(type->AsDictRaw()->GetKeyTypeRaw())
+ + ComputeTypeComplexity(type->AsDictRaw()->GetValueTypeRaw());
+ case ETypeName::Struct: {
+ int result = 1;
+ for (const auto& member : type->AsStructRaw()->GetMembers()) {
+ result += ComputeTypeComplexity(member.GetTypeRaw());
+ }
+ return result;
+ }
+ case ETypeName::Tuple: {
+ int result = 1;
+ for (const auto& element : type->AsTupleRaw()->GetElements()) {
+ result += ComputeTypeComplexity(element.GetTypeRaw());
+ }
+ return result;
+ }
+ case ETypeName::Variant: {
+ return ComputeTypeComplexity(type->AsVariantRaw()->GetUnderlyingTypeRaw());
+ }
+ case ETypeName::Tagged: {
+ return 1 + ComputeTypeComplexity(type->AsTaggedRaw()->GetItemType());
+ }
+ }
+ Y_FAIL("internal error: unreachable code");
+}
+
+} // namespace NTi
+
diff --git a/library/cpp/type_info/type_complexity.h b/library/cpp/type_info/type_complexity.h
new file mode 100644
index 0000000000..fcf367eb81
--- /dev/null
+++ b/library/cpp/type_info/type_complexity.h
@@ -0,0 +1,18 @@
+#pragma once
+#include "fwd.h"
+
+namespace NTi {
+
+ ///
+ /// Compute type complexity.
+ /// Roughly speaking type complexity is a number of nodes in the schema tree.
+ /// Examples:
+ /// - Type complexity of simple or singular type (i.e. Int64, String, Null, Decimal) is 1.
+ /// - Type complexity of `Optional<Int64>` is 2
+ /// - Type complexity of `Struct<a:Int64,b:Optional<String>` is 4 (1 for Int64, 2 for Optional<String> and 1 for struct)
+ ///
+ /// Systems might impose restrictions on the type complexity.
+ int ComputeTypeComplexity(const TTypePtr& type);
+ int ComputeTypeComplexity(const TType* type);
+
+} // namespace NTi
diff --git a/library/cpp/type_info/type_constructors.h b/library/cpp/type_info/type_constructors.h
new file mode 100644
index 0000000000..439d5a1887
--- /dev/null
+++ b/library/cpp/type_info/type_constructors.h
@@ -0,0 +1,110 @@
+#pragma once
+
+//! @file type_constructors.h
+
+#include "type.h"
+
+namespace NTi {
+ /// Create new `Null` type using the default heap factory.
+ TNullTypePtr Null();
+
+ /// Create new `Bool` type using the default heap factory.
+ TBoolTypePtr Bool();
+
+ /// Create new `Int8` type using the default heap factory.
+ TInt8TypePtr Int8();
+
+ /// Create new `Int16` type using the default heap factory.
+ TInt16TypePtr Int16();
+
+ /// Create new `Int32` type using the default heap factory.
+ TInt32TypePtr Int32();
+
+ /// Create new `Int64` type using the default heap factory.
+ TInt64TypePtr Int64();
+
+ /// Create new `Uint8` type using the default heap factory.
+ TUint8TypePtr Uint8();
+
+ /// Create new `Uint16` type using the default heap factory.
+ TUint16TypePtr Uint16();
+
+ /// Create new `Uint32` type using the default heap factory.
+ TUint32TypePtr Uint32();
+
+ /// Create new `Uint64` type using the default heap factory.
+ TUint64TypePtr Uint64();
+
+ /// Create new `Float` type using the default heap factory.
+ TFloatTypePtr Float();
+
+ /// Create new `Double` type using the default heap factory.
+ TDoubleTypePtr Double();
+
+ /// Create new `String` type using the default heap factory.
+ TStringTypePtr String();
+
+ /// Create new `Utf8` type using the default heap factory.
+ TUtf8TypePtr Utf8();
+
+ /// Create new `Date` type using the default heap factory.
+ TDateTypePtr Date();
+
+ /// Create new `Datetime` type using the default heap factory.
+ TDatetimeTypePtr Datetime();
+
+ /// Create new `Timestamp` type using the default heap factory.
+ TTimestampTypePtr Timestamp();
+
+ /// Create new `TzDate` type using the default heap factory.
+ TTzDateTypePtr TzDate();
+
+ /// Create new `TzDatetime` type using the default heap factory.
+ TTzDatetimeTypePtr TzDatetime();
+
+ /// Create new `TzTimestamp` type using the default heap factory.
+ TTzTimestampTypePtr TzTimestamp();
+
+ /// Create new `Interval` type using the default heap factory.
+ TIntervalTypePtr Interval();
+
+ /// Create new `Decimal` type using the default heap factory.
+ TDecimalTypePtr Decimal(ui8 precision, ui8 scale);
+
+ /// Create new `Json` type using the default heap factory.
+ TJsonTypePtr Json();
+
+ /// Create new `Yson` type using the default heap factory.
+ TYsonTypePtr Yson();
+
+ /// Create new `Uuid` type using the default heap factory.
+ TUuidTypePtr Uuid();
+
+ /// Create new `Optional` type using the default heap factory.
+ TOptionalTypePtr Optional(TTypePtr item);
+
+ /// Create new `List` type using the default heap factory.
+ TListTypePtr List(TTypePtr item);
+
+ /// Create new `Dict` type using the default heap factory.
+ TDictTypePtr Dict(TTypePtr key, TTypePtr value);
+
+ /// Create new `Struct` type using the default heap factory.
+ TStructTypePtr Struct(TStructType::TOwnedMembers items);
+ /// Create new `Struct` type using the default heap factory.
+ TStructTypePtr Struct(TMaybe<TStringBuf> name, TStructType::TOwnedMembers items);
+
+ /// Create new `Tuple` type using the default heap factory.
+ TTupleTypePtr Tuple(TTupleType::TOwnedElements items);
+ /// Create new `Tuple` type using the default heap factory.
+ TTupleTypePtr Tuple(TMaybe<TStringBuf> name, TTupleType::TOwnedElements items);
+
+ /// Create new `Variant` type using the default heap factory.
+ TVariantTypePtr Variant(TTypePtr underlying);
+ /// Create new `Variant` type using the default heap factory.
+ TVariantTypePtr Variant(TMaybe<TStringBuf> name, TTypePtr underlying);
+
+ /// Create new `Tagged` type using the default heap factory.
+ TTaggedTypePtr Tagged(TTypePtr type, TStringBuf tag);
+
+} // namespace NTi
diff --git a/library/cpp/type_info/type_equivalence.cpp b/library/cpp/type_info/type_equivalence.cpp
new file mode 100644
index 0000000000..9f22f0308c
--- /dev/null
+++ b/library/cpp/type_info/type_equivalence.cpp
@@ -0,0 +1,286 @@
+#include "type_equivalence.h"
+
+#include <util/generic/overloaded.h>
+
+#include "type.h"
+
+namespace NTi::NEq {
+ namespace {
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TType* lhs, const TType* rhs);
+
+ // template <bool IgnoreHash>
+ // bool StrictlyEqual(const TDocumentation& lhs, const TDocumentation& rhs) {
+ // ...
+ // }
+
+ // template <bool IgnoreHash>
+ // bool StrictlyEqual(const TAnnotations& lhs, const TAnnotations& rhs) {
+ // ...
+ // }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TVoidType&, const TVoidType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TNullType&, const TNullType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TBoolType&, const TBoolType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TInt8Type&, const TInt8Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TInt16Type&, const TInt16Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TInt32Type&, const TInt32Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TInt64Type&, const TInt64Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUint8Type&, const TUint8Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUint16Type&, const TUint16Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUint32Type&, const TUint32Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUint64Type&, const TUint64Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TFloatType&, const TFloatType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TDoubleType&, const TDoubleType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TStringType&, const TStringType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUtf8Type&, const TUtf8Type&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TDateType&, const TDateType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TDatetimeType&, const TDatetimeType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTimestampType&, const TTimestampType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTzDateType&, const TTzDateType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTzDatetimeType&, const TTzDatetimeType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTzTimestampType&, const TTzTimestampType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TIntervalType&, const TIntervalType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TDecimalType& lhs, const TDecimalType& rhs) {
+ return lhs.GetPrecision() == rhs.GetPrecision() && lhs.GetScale() == rhs.GetScale();
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TJsonType&, const TJsonType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TYsonType&, const TYsonType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TUuidType&, const TUuidType&) {
+ return true;
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TOptionalType& lhs, const TOptionalType& rhs) {
+ return StrictlyEqual<IgnoreHash>(lhs.GetItemTypeRaw(), rhs.GetItemTypeRaw());
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TListType& lhs, const TListType& rhs) {
+ return StrictlyEqual<IgnoreHash>(lhs.GetItemTypeRaw(), rhs.GetItemTypeRaw());
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TDictType& lhs, const TDictType& rhs) {
+ return StrictlyEqual<IgnoreHash>(lhs.GetKeyTypeRaw(), rhs.GetKeyTypeRaw()) &&
+ StrictlyEqual<IgnoreHash>(lhs.GetValueTypeRaw(), rhs.GetValueTypeRaw());
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TStructType& lhs, const TStructType& rhs) {
+ if (lhs.GetName() != rhs.GetName()) {
+ return false;
+ }
+
+ return std::equal(
+ lhs.GetMembers().begin(), lhs.GetMembers().end(),
+ rhs.GetMembers().begin(), rhs.GetMembers().end(),
+ [](const TStructType::TMember& lhs, const TStructType::TMember& rhs) -> bool {
+ return lhs.GetName() == rhs.GetName() &&
+ StrictlyEqual<IgnoreHash>(lhs.GetTypeRaw(), rhs.GetTypeRaw());
+ // && StrictlyEqual(lhs.GetAnnotations(), rhs.GetAnnotations())
+ // && StrictlyEqual(lhs.GetDocumentation(), rhs.GetDocumentation());
+ });
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTupleType& lhs, const TTupleType& rhs) {
+ if (lhs.GetName() != rhs.GetName()) {
+ return false;
+ }
+
+ return std::equal(
+ lhs.GetElements().begin(), lhs.GetElements().end(),
+ rhs.GetElements().begin(), rhs.GetElements().end(),
+ [](const TTupleType::TElement& lhs, const TTupleType::TElement& rhs) -> bool {
+ return StrictlyEqual<IgnoreHash>(lhs.GetTypeRaw(), rhs.GetTypeRaw());
+ // && StrictlyEqual(lhs.GetAnnotations(), rhs.GetAnnotations())
+ // && StrictlyEqual(lhs.GetDocumentation(), rhs.GetDocumentation());
+ });
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TVariantType& lhs, const TVariantType& rhs) {
+ if (lhs.GetName() != rhs.GetName()) {
+ return false;
+ }
+
+ return StrictlyEqual<IgnoreHash>(lhs.GetUnderlyingTypeRaw(), rhs.GetUnderlyingTypeRaw());
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TTaggedType& lhs, const TTaggedType& rhs) {
+ return lhs.GetTag() == rhs.GetTag() &&
+ StrictlyEqual<IgnoreHash>(lhs.GetItemTypeRaw(), rhs.GetItemTypeRaw());
+ }
+
+ template <bool IgnoreHash>
+ bool StrictlyEqual(const TType* lhs, const TType* rhs) {
+ if (lhs == rhs) {
+ return true;
+ }
+
+ if (lhs == nullptr || rhs == nullptr) {
+ return false;
+ }
+
+ if (!IgnoreHash && lhs->GetHash() != rhs->GetHash()) {
+ return false;
+ }
+
+ if (lhs->GetTypeName() != rhs->GetTypeName()) {
+ return false;
+ }
+
+ // FIXME: update `TStrictlyEqual`'s docs to explicitly state that documentation and annotations
+ // must be the same for types to compare strictly equal.
+
+ // if (!StrictlyEqual(lhs->GetAnnotations(), rhs->GetAnnotations())) {
+ // return false;
+ // }
+
+ // if (!StrictlyEqual(lhs->GetDocumentation(), rhs->GetDocumentation())) {
+ // return false;
+ // }
+
+ return lhs->VisitRaw([&rhs](const auto* lhs) {
+ using TSameType = decltype(lhs);
+ return rhs->VisitRaw(TOverloaded{
+ [lhs](TSameType rhs) {
+ return StrictlyEqual<IgnoreHash>(*lhs, *rhs);
+ },
+ [](const auto* rhs) {
+ static_assert(!std::is_same_v<decltype(lhs), decltype(rhs)>);
+ return false;
+ }});
+ });
+ }
+ }
+
+ bool TStrictlyEqual::operator()(const TType* lhs, const TType* rhs) const {
+ return StrictlyEqual<false>(lhs, rhs);
+ }
+
+ bool TStrictlyEqual::operator()(TTypePtr lhs, TTypePtr rhs) const {
+ return operator()(lhs.Get(), rhs.Get());
+ }
+
+ bool TStrictlyEqual::IgnoreHash(const TType* lhs, const TType* rhs) const {
+ return StrictlyEqual<true>(lhs, rhs);
+ }
+
+ bool TStrictlyEqual::IgnoreHash(TTypePtr lhs, TTypePtr rhs) const {
+ return IgnoreHash(lhs.Get(), rhs.Get());
+ }
+
+ ui64 TStrictlyEqualHash::operator()(const TType* type) const {
+ if (type == nullptr) {
+ return 0;
+ }
+
+ return type->GetHash();
+ }
+
+ ui64 TStrictlyEqualHash::operator()(TTypePtr type) const {
+ return operator()(type.Get());
+ }
+}
diff --git a/library/cpp/type_info/type_equivalence.h b/library/cpp/type_info/type_equivalence.h
new file mode 100644
index 0000000000..70351dc689
--- /dev/null
+++ b/library/cpp/type_info/type_equivalence.h
@@ -0,0 +1,36 @@
+#pragma once
+
+//! @file type_equivalence.h
+//!
+//! Relations between types.
+//!
+//! Type info declares multiple ways to compare types. There's strict, nominal, structural and other equivalences,
+//! as well as subtyping. See corresponding functors for more info.
+//!
+//! At the moment, only strict equivalence is implemented because others are not yet standartized.
+
+#include <type_traits>
+
+#include "fwd.h"
+
+#include <util/system/types.h>
+
+namespace NTi::NEq {
+ /// Strict equivalence is the strongest form of type equivalence. If two types are strictly equal,
+ /// they're literally the same type for all intents and purposes. This includes struct, tuple, variant and enum
+ /// names, order of their items, names of their items, names of tagged types, and so on.
+ struct TStrictlyEqual {
+ bool operator()(const TType* lhs, const TType* rhs) const;
+ bool operator()(TTypePtr lhs, TTypePtr rhs) const;
+
+ /// Compare types without calculating and comparing their hashes first.
+ bool IgnoreHash(const TType* lhs, const TType* rhs) const;
+ bool IgnoreHash(TTypePtr lhs, TTypePtr rhs) const;
+ };
+
+ /// Hash that follows the strict equality rules (see `TStrictlyEqual`).
+ struct TStrictlyEqualHash {
+ ui64 operator()(const TType* type) const;
+ ui64 operator()(TTypePtr type) const;
+ };
+}
diff --git a/library/cpp/type_info/type_factory.cpp b/library/cpp/type_info/type_factory.cpp
new file mode 100644
index 0000000000..9d60307938
--- /dev/null
+++ b/library/cpp/type_info/type_factory.cpp
@@ -0,0 +1,495 @@
+#include "type_factory.h"
+
+#include "type.h"
+#include "type_equivalence.h"
+
+#include <util/memory/pool.h>
+#include <util/generic/hash_set.h>
+
+#include <cstdlib>
+
+namespace NTi {
+ TVoidTypePtr ITypeFactory::Void() {
+ return TVoidType::Instance();
+ }
+
+ const TVoidType* IPoolTypeFactory::VoidRaw() {
+ return TVoidType::InstanceRaw();
+ }
+
+ TNullTypePtr ITypeFactory::Null() {
+ return TNullType::Instance();
+ }
+
+ const TNullType* IPoolTypeFactory::NullRaw() {
+ return TNullType::InstanceRaw();
+ }
+
+ TBoolTypePtr ITypeFactory::Bool() {
+ return TBoolType::Instance();
+ }
+
+ const TBoolType* IPoolTypeFactory::BoolRaw() {
+ return TBoolType::InstanceRaw();
+ }
+
+ TInt8TypePtr ITypeFactory::Int8() {
+ return TInt8Type::Instance();
+ }
+
+ const TInt8Type* IPoolTypeFactory::Int8Raw() {
+ return TInt8Type::InstanceRaw();
+ }
+
+ TInt16TypePtr ITypeFactory::Int16() {
+ return TInt16Type::Instance();
+ }
+
+ const TInt16Type* IPoolTypeFactory::Int16Raw() {
+ return TInt16Type::InstanceRaw();
+ }
+
+ TInt32TypePtr ITypeFactory::Int32() {
+ return TInt32Type::Instance();
+ }
+
+ const TInt32Type* IPoolTypeFactory::Int32Raw() {
+ return TInt32Type::InstanceRaw();
+ }
+
+ TInt64TypePtr ITypeFactory::Int64() {
+ return TInt64Type::Instance();
+ }
+
+ const TInt64Type* IPoolTypeFactory::Int64Raw() {
+ return TInt64Type::InstanceRaw();
+ }
+
+ TUint8TypePtr ITypeFactory::Uint8() {
+ return TUint8Type::Instance();
+ }
+
+ const TUint8Type* IPoolTypeFactory::Uint8Raw() {
+ return TUint8Type::InstanceRaw();
+ }
+
+ TUint16TypePtr ITypeFactory::Uint16() {
+ return TUint16Type::Instance();
+ }
+
+ const TUint16Type* IPoolTypeFactory::Uint16Raw() {
+ return TUint16Type::InstanceRaw();
+ }
+
+ TUint32TypePtr ITypeFactory::Uint32() {
+ return TUint32Type::Instance();
+ }
+
+ const TUint32Type* IPoolTypeFactory::Uint32Raw() {
+ return TUint32Type::InstanceRaw();
+ }
+
+ TUint64TypePtr ITypeFactory::Uint64() {
+ return TUint64Type::Instance();
+ }
+
+ const TUint64Type* IPoolTypeFactory::Uint64Raw() {
+ return TUint64Type::InstanceRaw();
+ }
+
+ TFloatTypePtr ITypeFactory::Float() {
+ return TFloatType::Instance();
+ }
+
+ const TFloatType* IPoolTypeFactory::FloatRaw() {
+ return TFloatType::InstanceRaw();
+ }
+
+ TDoubleTypePtr ITypeFactory::Double() {
+ return TDoubleType::Instance();
+ }
+
+ const TDoubleType* IPoolTypeFactory::DoubleRaw() {
+ return TDoubleType::InstanceRaw();
+ }
+
+ TStringTypePtr ITypeFactory::String() {
+ return TStringType::Instance();
+ }
+
+ const TStringType* IPoolTypeFactory::StringRaw() {
+ return TStringType::InstanceRaw();
+ }
+
+ TUtf8TypePtr ITypeFactory::Utf8() {
+ return TUtf8Type::Instance();
+ }
+
+ const TUtf8Type* IPoolTypeFactory::Utf8Raw() {
+ return TUtf8Type::InstanceRaw();
+ }
+
+ TDateTypePtr ITypeFactory::Date() {
+ return TDateType::Instance();
+ }
+
+ const TDateType* IPoolTypeFactory::DateRaw() {
+ return TDateType::InstanceRaw();
+ }
+
+ TDatetimeTypePtr ITypeFactory::Datetime() {
+ return TDatetimeType::Instance();
+ }
+
+ const TDatetimeType* IPoolTypeFactory::DatetimeRaw() {
+ return TDatetimeType::InstanceRaw();
+ }
+
+ TTimestampTypePtr ITypeFactory::Timestamp() {
+ return TTimestampType::Instance();
+ }
+
+ const TTimestampType* IPoolTypeFactory::TimestampRaw() {
+ return TTimestampType::InstanceRaw();
+ }
+
+ TTzDateTypePtr ITypeFactory::TzDate() {
+ return TTzDateType::Instance();
+ }
+
+ const TTzDateType* IPoolTypeFactory::TzDateRaw() {
+ return TTzDateType::InstanceRaw();
+ }
+
+ TTzDatetimeTypePtr ITypeFactory::TzDatetime() {
+ return TTzDatetimeType::Instance();
+ }
+
+ const TTzDatetimeType* IPoolTypeFactory::TzDatetimeRaw() {
+ return TTzDatetimeType::InstanceRaw();
+ }
+
+ TTzTimestampTypePtr ITypeFactory::TzTimestamp() {
+ return TTzTimestampType::Instance();
+ }
+
+ const TTzTimestampType* IPoolTypeFactory::TzTimestampRaw() {
+ return TTzTimestampType::InstanceRaw();
+ }
+
+ TIntervalTypePtr ITypeFactory::Interval() {
+ return TIntervalType::Instance();
+ }
+
+ const TIntervalType* IPoolTypeFactory::IntervalRaw() {
+ return TIntervalType::InstanceRaw();
+ }
+
+ TDecimalTypePtr ITypeFactory::Decimal(ui8 precision, ui8 scale) {
+ return TDecimalType::Create(*this, precision, scale);
+ }
+
+ const TDecimalType* IPoolTypeFactory::DecimalRaw(ui8 precision, ui8 scale) {
+ return TDecimalType::CreateRaw(*this, precision, scale);
+ }
+
+ TJsonTypePtr ITypeFactory::Json() {
+ return TJsonType::Instance();
+ }
+
+ const TJsonType* IPoolTypeFactory::JsonRaw() {
+ return TJsonType::InstanceRaw();
+ }
+
+ TYsonTypePtr ITypeFactory::Yson() {
+ return TYsonType::Instance();
+ }
+
+ const TYsonType* IPoolTypeFactory::YsonRaw() {
+ return TYsonType::InstanceRaw();
+ }
+
+ TUuidTypePtr ITypeFactory::Uuid() {
+ return TUuidType::Instance();
+ }
+
+ const TUuidType* IPoolTypeFactory::UuidRaw() {
+ return TUuidType::InstanceRaw();
+ }
+
+ TOptionalTypePtr ITypeFactory::Optional(TTypePtr item) {
+ return TOptionalType::Create(*this, std::move(item));
+ }
+
+ const TOptionalType* IPoolTypeFactory::OptionalRaw(const TType* item) {
+ return TOptionalType::CreateRaw(*this, item);
+ }
+
+ TListTypePtr ITypeFactory::List(TTypePtr item) {
+ return TListType::Create(*this, std::move(item));
+ }
+
+ const TListType* IPoolTypeFactory::ListRaw(const TType* item) {
+ return TListType::CreateRaw(*this, item);
+ }
+
+ TDictTypePtr ITypeFactory::Dict(TTypePtr key, TTypePtr value) {
+ return TDictType::Create(*this, std::move(key), std::move(value));
+ }
+
+ const TDictType* IPoolTypeFactory::DictRaw(const TType* key, const TType* value) {
+ return TDictType::CreateRaw(*this, key, value);
+ }
+
+ TStructTypePtr ITypeFactory::Struct(TStructType::TOwnedMembers items) {
+ return TStructType::Create(*this, items);
+ }
+
+ TStructTypePtr ITypeFactory::Struct(TMaybe<TStringBuf> name, TStructType::TOwnedMembers items) {
+ return TStructType::Create(*this, name, items);
+ }
+
+ const TStructType* IPoolTypeFactory::StructRaw(TStructType::TMembers items) {
+ return TStructType::CreateRaw(*this, items);
+ }
+
+ const TStructType* IPoolTypeFactory::StructRaw(TMaybe<TStringBuf> name, TStructType::TMembers items) {
+ return TStructType::CreateRaw(*this, name, items);
+ }
+
+ TTupleTypePtr ITypeFactory::Tuple(TTupleType::TOwnedElements items) {
+ return TTupleType::Create(*this, items);
+ }
+
+ TTupleTypePtr ITypeFactory::Tuple(TMaybe<TStringBuf> name, TTupleType::TOwnedElements items) {
+ return TTupleType::Create(*this, name, items);
+ }
+
+ const TTupleType* IPoolTypeFactory::TupleRaw(TTupleType::TElements items) {
+ return TTupleType::CreateRaw(*this, items);
+ }
+
+ const TTupleType* IPoolTypeFactory::TupleRaw(TMaybe<TStringBuf> name, TTupleType::TElements items) {
+ return TTupleType::CreateRaw(*this, name, items);
+ }
+
+ TVariantTypePtr ITypeFactory::Variant(TTypePtr inner) {
+ return TVariantType::Create(*this, std::move(inner));
+ }
+
+ TVariantTypePtr ITypeFactory::Variant(TMaybe<TStringBuf> name, TTypePtr inner) {
+ return TVariantType::Create(*this, name, std::move(inner));
+ }
+
+ const TVariantType* IPoolTypeFactory::VariantRaw(const TType* inner) {
+ return TVariantType::CreateRaw(*this, inner);
+ }
+
+ const TVariantType* IPoolTypeFactory::VariantRaw(TMaybe<TStringBuf> name, const TType* inner) {
+ return TVariantType::CreateRaw(*this, name, inner);
+ }
+
+ TTaggedTypePtr ITypeFactory::Tagged(TTypePtr type, TStringBuf tag) {
+ return TTaggedType::Create(*this, std::move(type), tag);
+ }
+
+ const TTaggedType* IPoolTypeFactory::TaggedRaw(const TType* type, TStringBuf tag) {
+ return TTaggedType::CreateRaw(*this, type, tag);
+ }
+
+ namespace {
+ class TPoolFactory: public NTi::IPoolTypeFactory {
+ public:
+ TPoolFactory(size_t initial, TMemoryPool::IGrowPolicy* grow, IAllocator* alloc, TMemoryPool::TOptions options)
+ : Pool_(initial, grow, alloc, options)
+ {
+ }
+
+ public:
+ void* Allocate(size_t size, size_t align) noexcept override {
+ return Pool_.Allocate(size, align);
+ }
+
+ void Free(void* data) noexcept override {
+ Y_UNUSED(data);
+ }
+
+ protected:
+ const NTi::TType* LookupCache(const NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ return nullptr;
+ }
+
+ void SaveCache(const NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ }
+
+ void Ref() noexcept override {
+ Counter_.Inc();
+ }
+
+ void UnRef() noexcept override {
+ if (Counter_.Dec() == 0) {
+ delete this;
+ }
+ }
+
+ void DecRef() noexcept override {
+ if (Counter_.Dec() == 0) {
+ Y_FAIL("DecRef is not supposed to drop");
+ }
+ }
+
+ long RefCount() const noexcept override {
+ return Counter_.Val();
+ }
+
+ void RefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ }
+
+ void UnRefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ }
+
+ void DecRefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ }
+
+ long RefCountType(const NTi::TType* type) const noexcept override {
+ Y_UNUSED(type);
+ return RefCount();
+ }
+
+ public:
+ size_t Available() const noexcept override {
+ return Pool_.Available();
+ }
+
+ size_t MemoryAllocated() const noexcept override {
+ return Pool_.MemoryAllocated();
+ }
+
+ size_t MemoryWaste() const noexcept override {
+ return Pool_.MemoryWaste();
+ }
+
+ private:
+ TAtomicCounter Counter_;
+ TMemoryPool Pool_;
+ };
+
+ class TPoolFactoryDedup: public TPoolFactory {
+ public:
+ using TPoolFactory::TPoolFactory;
+
+ public:
+ const NTi::TType* LookupCache(const NTi::TType* type) noexcept override {
+ if (auto it = Cache_.find(type); it != Cache_.end()) {
+ return *it;
+ } else {
+ return nullptr;
+ }
+ }
+
+ void SaveCache(const NTi::TType* type) noexcept override {
+ Cache_.insert(type);
+ }
+
+ protected:
+ TStringBuf AllocateString(TStringBuf str) noexcept override {
+ if (str.empty()) {
+ return TStringBuf(); // `str` could still point somewhere whereas empty strbuf points to NULL
+ }
+
+ if (auto it = StringCache_.find(str); it != StringCache_.end()) {
+ return *it;
+ } else {
+ return *StringCache_.insert(it, ITypeFactoryInternal::AllocateString(str));
+ }
+ }
+
+ private:
+ THashSet<const NTi::TType*, NTi::NEq::TStrictlyEqualHash, NTi::NEq::TStrictlyEqual> Cache_;
+ THashSet<TStringBuf> StringCache_;
+ };
+
+ class THeapFactory: public NTi::ITypeFactory {
+ public:
+ void* Allocate(size_t size, size_t align) noexcept override {
+ Y_UNUSED(align);
+ return malloc(size);
+ }
+
+ void Free(void* data) noexcept override {
+ free(data);
+ }
+
+ protected:
+ const NTi::TType* LookupCache(const NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ return nullptr;
+ }
+
+ void SaveCache(const NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ }
+
+ void Ref() noexcept override {
+ // nothing
+ }
+
+ void UnRef() noexcept override {
+ // nothing
+ }
+
+ void DecRef() noexcept override {
+ // nothing
+ }
+
+ long RefCount() const noexcept override {
+ return 0;
+ }
+
+ void RefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ Y_FAIL("not supposed to be called");
+ }
+
+ void UnRefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ Y_FAIL("not supposed to be called");
+ }
+
+ void DecRefType(NTi::TType* type) noexcept override {
+ Y_UNUSED(type);
+ Y_FAIL("not supposed to be called");
+ }
+
+ long RefCountType(const NTi::TType* type) const noexcept override {
+ Y_UNUSED(type);
+ Y_FAIL("not supposed to be called");
+ }
+ };
+
+ THeapFactory HEAP_FACTORY;
+ }
+
+ IPoolTypeFactoryPtr PoolFactory(bool deduplicate, size_t initial, TMemoryPool::IGrowPolicy* grow, IAllocator* alloc, TMemoryPool::TOptions options) {
+ if (deduplicate) {
+ return new TPoolFactoryDedup(initial, grow, alloc, options);
+ } else {
+ return new TPoolFactory(initial, grow, alloc, options);
+ }
+ }
+
+ namespace NPrivate {
+ ITypeFactory* GetDefaultHeapFactory() {
+ return &HEAP_FACTORY;
+ }
+ }
+
+ ITypeFactoryPtr HeapFactory() {
+ return NPrivate::GetDefaultHeapFactory();
+ }
+}
diff --git a/library/cpp/type_info/type_factory.h b/library/cpp/type_info/type_factory.h
new file mode 100644
index 0000000000..df47c7082b
--- /dev/null
+++ b/library/cpp/type_info/type_factory.h
@@ -0,0 +1,906 @@
+#pragma once
+
+//! @file type_factory.h
+//!
+//! Type factory creates type instances and manages their lifetimes and destruction.
+//!
+//! Type info supports multiple ways of allocating type instances and managing their lifetimes:
+//!
+//! - [heap-based allocation] is the standard memory allocation method for C++. It offers great flexibility
+//! as it can allocate memory regions of almost arbitrary size and alignment, deallocate them, re-use them
+//! for new allocations or return the to the operating system. This flexibility comes with a price, though.
+//! Heap allocators usually require synchronisation (i.e. a mutex or a spinlock), and some memory overhead to track
+//! allocated regions. Also, they provide no guarantees on data locality and CPU cache friendliness whatsoever.
+//!
+//! When using a heap-based factory, each type instance have a separate reference counter. `TTypePtr`s will
+//! increment and decrement this counter. Whenever it reaches zero, they will destroy the type instance and free
+//! its memory. This is the standard reference counting technique, just like in `TRefCounted`.
+//!
+//! - [memory pool] is a data structure for faster memory allocation. It pre-allocates large chunks of memory and uses
+//! them to allocate smaller objects. This way, we don't have to `malloc` memory for each new object.
+//! Also, we can drop all objects at once by discarding all memory chunks.
+//!
+//! When using memory-pool-based factory, types don't have individual reference counters. Instead, they store
+//! a pointer to the factory that've created them. Whenever such type is referenced or unreferenced, the factory's
+//! own reference counter is incremented or decremented. Factory dies and releases all pool's memory when the last
+//! reference to a type in its pool dies.
+//!
+//! [heap-based allocation]: https://en.wikipedia.org/wiki/Memory_management#Dynamic_memory_allocation
+//! [Reference counting]: https://en.wikipedia.org/wiki/Reference_counting
+//! [memory pool]: https://en.wikipedia.org/wiki/Region-based_memory_management
+//!
+//! The rule of thumb is: if you have an intrusive pointer to a type, you can be sure it points to an alive object;
+//! if you have a raw pointer to a type, it's your responsibility to ensure its validity.
+//!
+//! Whenever you have a valid raw pointer to a type, you can promote it to an intrusive pointer by calling `AsPtr`.
+//! This is always safe because there's no way to create a type instance on the stack or in the static memory
+//! of a program.
+//!
+//!
+//! # Implementation details
+//!
+//! We're building a hierarchy of classes that work equally well with both heap-based allocation model
+//! and memory-pool-based allocation model. In order to understand how we do it, we should first understand how
+//! object ownership works in both models.
+//!
+//!
+//! ## Ownership schema
+//!
+//! Depending on the chosen factory implementation, ownership schema may vary.
+//!
+//! When using the heap-based factory, each type has its own reference counter. User own types, and types own
+//! their nested types. For `Optional<String>`, user owns the `Optional` type, and `Optional` type
+//! owns the `String` type:
+//!
+//! ```text
+//! Legend: A ───> B -- A owns B | A ─ ─> B -- A points to B
+//!
+//! User code: Objects:
+//! ┌─────────────┐ ┌────────────────┐
+//! │ IFactoryPtr │───────>│ Factory │
+//! └─────────────┘ └────────────────┘
+//! ┌─────────────┐ ┌────────────────┐
+//! │ TTypePtr │───────>│ Type: Optional │──┐
+//! └─────────────┘ └────────────────┘ │
+//! ┌──────────────────────┘
+//! │ ┌────────────────┐
+//! └─>│ Type: String │
+//! └────────────────┘
+//! ```
+//!
+//! When using a pool-based factory, all allocated types are owned by pool. So, in the `Optional<String>` example,
+//! user owns factory, factory owns the `Optional` type and the `String` type. All references between types
+//! are just borrows:
+//!
+//! ```text
+//! Legend: A ───> B -- A owns B | A ─ ─> B -- A points to B
+//!
+//! User code: Objects:
+//! ┌─────────────┐ ┌────────────────┐
+//! │ IFactoryPtr │─┌─────>│ Factory │──┐
+//! └─────────────┘ │ └────────────────┘ │
+//! ┌─────────────┐ │ ┌────────────────┐ │
+//! │ TTypePtr │─┘─ ─ ─>│ Type: Optional │<─┤
+//! └─────────────┘ └───────┬────────┘ │
+//! ┌ ─ ─ ─ ─ ─ ─┘ │
+//! ╎ ┌────────────────┐ │
+//! └─ ─>│ Type: String │<─┘
+//! └────────────────┘
+//! ```
+//!
+//! Notice how `TTypePtr` points to `Optional`, but doesn't own it. Instead, it owns the factory which,
+//! transitively, owns `Optional`. This way we can be sure that the factory will not be destroyed before
+//! all `IFactoryPtr`s and `TTypePtr`s die, thus guaranteeing that all `TTypePtr` always stay valid.
+//!
+//! With that in mind, we can derive two types of ownership here.
+//!
+//! Whenever `TTypePtr` owns some type, we call it 'external ownership' (because it's code that is external
+//! to this library owns something). In this situation, ref and unref procedures must increment type's own reference
+//! counter, if there is one, and also increment factory's reference counter. `NTi::TType::Ref` and `NTi::TType::Unref`
+//! do this by calling `NTi::ITypeFactoryInternal::Ref`, `NTi::ITypeFactoryInternal::RefType`,
+//! `NTi::ITypeFactoryInternal::UnRef`, `NTi::ITypeFactoryInternal::UnRefType`.
+//!
+//! Whenever one type owns another type, we call it 'internal ownership'. In this situation, ref and unref procedures
+//! must only increment and decrement type's own reference counter. They should *not* increment or decrement
+//! factory's reference counter. `NTi::TType::RefSelf` and `NTi::TType::UnrefSelf` do this by calling
+//! `NTi::ITypeFactoryInternal::RefType` and `NTi::ITypeFactoryInternal::UnRefType`, and not calling
+//! `NTi::ITypeFactoryInternal::Ref` and `NTi::ITypeFactoryInternal::UnRef`.
+//!
+//!
+//! ## Adoption semantics
+//!
+//! Let's see now what should happen when we create some complex type using more that one factory.
+//!
+//! Suppose we're creating a container type, such as `Optional`, using factory `a`. If its nested type is
+//! managed by the same factory, we have no issues at all:
+//!
+//! ```
+//! auto a = NTi::PoolFactory();
+//! auto string = a->String();
+//! auto optional = a->Optional(string);
+//! ```
+//!
+//! But what if the nested type is managed by some other factory `b`? Consider:
+//!
+//! ```
+//! auto b = NTi::PoolFactory();
+//! auto a = NTi::PoolFactory();
+//! auto string = b->String();
+//! auto optional = a->Optional(string);
+//! ```
+//!
+//! Well, we're in trouble. It's not enough for `optional` to just acquire internal ownership over `string`.
+//! Actually, `optional` has to own both `string` and its factory, `b`. Otherwise, if `b` dies, `string` will
+//! die with it, leaving `optional` with a dangling pointer.
+//!
+//! However, we can't have a type owning its factory. It will create a loop. The only safe solution is to copy
+//! the nested type from one factory to another. If we have some type managed by factory `a`, all nested types
+//! must also be managed by the same factory `a`.
+//!
+//! So, whenever we create a type, we must ensure that all its nested types are managed by the same factory.
+//! If they're not, we must deep-copy them to the current factory. This is called the 'adoption semantics'. That is,
+//! we adopt nested types into the current factory.
+//!
+//! In fact, adoption semantics is a bit more complicated that this. Some types are not managed by any factory.
+//! They have static storage duration and therefore need no management. So, if we see that there's no factory
+//! associated with some type, we don't perform deep-copy. Instead, we just return it as is.
+//!
+//!
+//! ## Overview
+//!
+//! So, now we're ready to put this puzzle together.
+//!
+//! When we create a new type instance, we do the following:
+//!
+//! 1: we adopt all nested types and acquire internal ownership over them. This is done by the `Own` function;
+//! 2: we allocate some memory for the type. This is done by `New`, `Allocate` and other functions;
+//! 3: we initialize all this memory;
+//! 4: finally, we acquire external ownership over the newly created type and return it to the user.
+//!
+//! Steps 1 through 3 are performed by the `NTi::TType::Clone` function. Step 4 is performed by the factory.
+//!
+//! When we adopt some type, we check if it's managed by any factory other that the current one. If needed,
+//! we deep-copy it by the `NTi::TType::Clone` function. That is, `Clone` copies type to a new factory and adopts
+//! nested types, adoption calls nested type's `Clone`, causing recursive deep-copying.
+//!
+//! When we release external or internal ownership over some type, factory might decide to destroy it (if, for
+//! example, this type's reference count reached zero). In this case, we need to release internal ownership
+//! over the nested types, and release all memory that was allocated in step 3.
+//! This is done by `NTi::TType::Drop` function.
+
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/generic/strbuf.h>
+#include <util/memory/pool.h>
+
+#include "type.h"
+
+#include "fwd.h"
+
+namespace NTi {
+ namespace NPrivate {
+ /// If `T` is derived from `NTi::TType` (ignoring cv-qualification), provides the member constant `value`
+ /// equal to `true`. Otherwise `value` is `false`.
+ template <typename T>
+ struct TIsType: public std::is_base_of<TType, T> {
+ };
+
+ /// Helper template variable for `TIsType`.
+ template <typename T>
+ inline constexpr const bool IsTypeV = TIsType<T>::value;
+
+ /// Statically assert that `T` is a managed object.
+ template <typename T>
+ void AssertIsType() {
+ static_assert(IsTypeV<T>, "object should be derived from 'NTi::TType'");
+ }
+ }
+
+ /// Internal interface for type factory.
+ ///
+ /// This interface is accessible from `TType` implementation, but not from outside.
+ class ITypeFactoryInternal {
+ friend class TType;
+ template <typename T>
+ friend class ::TDefaultIntrusivePtrOps;
+
+ public:
+ virtual ~ITypeFactoryInternal() = default;
+
+ public:
+ /// @name Object creation and lifetime management interface
+ ///
+ /// @{
+ //-
+ /// Create a new object of type `T` by allocating memory for it and constructing it with arguments `args`.
+ ///
+ /// If `T` is derived from `TType`, it will be linked to this factory via the `NTi::TType::SetFactory` method.
+ ///
+ /// This method does not acquire any ownership over the created object, not internal nor external. It just
+ /// allocates some memory and calls a constructor and that's it.
+ template <typename T, typename... Args>
+ inline T* New(Args&&... args) {
+ // Note: it's important to understand difference between `New` and `Create` - read the docs!
+
+ static_assert(std::is_trivially_destructible_v<T>, "can only create trivially destructible types");
+
+ auto value = new (AllocateFor<T>()) T(std::forward<Args>(args)...);
+
+ if constexpr (NPrivate::IsTypeV<T>) {
+ value->SetFactory(this);
+ }
+
+ return value;
+ }
+
+ /// Delete an object that was created via the `New` function.
+ ///
+ /// This function essentially just calls `Free`. Note that `New` can only create trivially destructible types,
+ /// thus `Delete` does not neet to call object's destructor.
+ template <typename T>
+ inline void Delete(T* obj) noexcept {
+ Free(obj);
+ }
+
+ /// Create a new array of `count` objects of type `T`.
+ ///
+ /// This function will allocate an array of `count` objects of type `T`. Then, for each object in the array,
+ /// it will call the `ctor(T* memory, size_t index)`, passing a pointer to the uninitialized object
+ /// and its index in the array. `ctor` should call placement new on the passed pointer.
+ ///
+ /// If `T` is derived from `TType`, it will be linked to this factory via the `NTi::TType::SetFactory` method.
+ ///
+ /// This method does not acquire any ownership over the created array elements, not internal nor external.
+ /// It just allocates some memory and calls a constructor and that's it.
+ ///
+ ///
+ /// # Examples
+ ///
+ /// Copy vector to factory:
+ ///
+ /// ```
+ /// TVector<T> source = ...;
+ /// TArrayRef<T> copied = NMem::NewArray<T>(factory, source.size(), [&source](T* ptr, size_t i) {
+ /// new (ptr) T(source[i]);
+ /// });
+ /// ```
+ template <typename T, typename TCtor>
+ inline TArrayRef<T> NewArray(size_t count, TCtor&& ctor) {
+ // Note: it's important to understand difference between `New` and `Create` - read the docs!
+
+ static_assert(std::is_trivially_destructible_v<T>, "can only create trivially destructible types");
+
+ auto items = AllocateArrayFor<T>(count);
+
+ for (size_t i = 0; i < count; ++i) {
+ auto value = items + i;
+ ctor(value, i);
+ if constexpr (NPrivate::IsTypeV<T>) {
+ value->SetFactory(this);
+ }
+ }
+
+ return TArrayRef(items, count);
+ }
+
+ /// Delete an object that was created via the `NewArray` function.
+ ///
+ /// This function runs `dtor` on each element of the array and then just calls `Free`. Note that `NewArray`
+ /// can only create arrays for trivially destructible types, thus `DeleteArray`
+ /// does not call object destructors.
+ template <typename T, typename TDtor>
+ inline void DeleteArray(TArrayRef<T> obj, TDtor&& dtor) noexcept {
+ for (size_t i = 0; i < obj.size(); ++i) {
+ auto value = obj.data() + i;
+ dtor(value, i);
+ }
+
+ Free(const_cast<void*>(static_cast<const void*>(obj.data())));
+ }
+
+ /// Adopt `type` into this factory.
+ ///
+ /// This function works like `AdoptRaw`, but it wraps its result to an intrusive pointer, thus acquiring
+ /// external ownership over it.
+ ///
+ /// It is used by external code to copy types between factories.
+ template <typename T>
+ inline TIntrusiveConstPtr<T> Adopt(TIntrusiveConstPtr<T> type) noexcept {
+ NPrivate::AssertIsType<T>();
+
+ return TIntrusiveConstPtr<T>(const_cast<T*>(AdoptRaw<T>(type.Get())));
+ }
+
+ /// Adopt `type` into this factory.
+ ///
+ /// This function is used to transfer types between factories. It has the following semantics:
+ ///
+ /// - if `type` is managed by this factory, this function returns its argument unchanged;
+ /// - if `type` is not managed by any factory, this function also returns its argument unchanged;
+ /// - if `type` is managed by another factory, this function deep-copies `type` into this factory and returns
+ /// a pointer to the new deep-copied value.
+ ///
+ /// This function uses `NTi::TType::Clone` to deep-copy types. It does not acquire any ownership over
+ /// the returned object, not internal nor external. If you need to adopt and acquire external ownership
+ /// (i.e. you're returning type to a user), use `Adopt`. If you need to adopt and acquire internal ownership
+ /// (i.e. you're writing `NTi::TType::Clone` implementation), use `Own`.
+ template <typename T>
+ inline const T* AdoptRaw(const T* type) noexcept {
+ NPrivate::AssertIsType<T>();
+
+ ITypeFactoryInternal* typeFactory = type->GetFactory();
+ if (typeFactory == nullptr || typeFactory == this) {
+ return type;
+ } else {
+ return type->Clone(*this);
+ }
+ }
+
+ /// Adopt some type and acquire internal ownership over it.
+ ///
+ /// This function works like `AdoptRaw`, but acquires internal ownership over the returned type.
+ ///
+ /// It is used by `NTi::TType::Clone` implementations to acquire ownership over the nested types.
+ template <typename T>
+ inline const T* Own(const T* type) noexcept {
+ NPrivate::AssertIsType<T>();
+
+ type = AdoptRaw(type);
+ const_cast<T*>(type)->RefSelf();
+ return type;
+ }
+
+ /// Release internal ownership over some type.
+ ///
+ /// This function is used by `NTi::TType::Drop` to release internal ownership over nested types.
+ template <typename T>
+ inline void Disown(const T* type) noexcept {
+ NPrivate::AssertIsType<T>();
+
+ const_cast<T*>(type)->UnRefSelf();
+ }
+
+ /// @}
+
+ public:
+ /// @name Memory management interface
+ ///
+ /// Functions for dealing with raw unmanaged memory.
+ ///
+ /// @{
+ //-
+ /// Allocate a new chunk of memory of size `size` aligned on `align`.
+ ///
+ /// The behaviour is undefined if `size` is zero, `align` is zero, is not a power of two,
+ /// or greater than `PLATFORM_DATA_ALIGN` (i.e., over-aligned).
+ ///
+ /// This function panics if memory can't be allocated.
+ ///
+ /// Returned memory may or may not be initialized.
+ virtual void* Allocate(size_t size, size_t align) noexcept = 0;
+
+ /// Allocate a chunk of memory suitable for storing an object of type `T`.
+ ///
+ /// This function panics if memory can't be allocated.
+ ///
+ /// Returned memory may or may not be initialized.
+ template <typename T>
+ T* AllocateFor() noexcept {
+ static_assert(alignof(T) <= PLATFORM_DATA_ALIGN, "over-aligned types are not supported");
+ return static_cast<T*>(Allocate(sizeof(T), alignof(T)));
+ }
+
+ /// Allocate a new chunk of memory suitable for storing `count` of objects of size `size` aligned on `align`.
+ ///
+ /// The behaviour is undefined if `count` is zero, `size` is zero, `align` is zero, is not a power of two,
+ /// or greater than `PLATFORM_DATA_ALIGN` (i.e., over-aligned).
+ ///
+ /// This function panics if memory can't be allocated.
+ ///
+ /// Returned memory may or may not be initialized.
+ void* AllocateArray(size_t count, size_t size, size_t align) noexcept {
+ return Allocate(size * count, align);
+ }
+
+ /// Allocate a chunk of memory suitable for storing an array of `count` objects of type `T`.
+ ///
+ /// This function panics if memory can't be allocated.
+ ///
+ /// Returned memory may or may not be initialized.
+ template <typename T>
+ T* AllocateArrayFor(size_t count) noexcept {
+ static_assert(alignof(T) <= PLATFORM_DATA_ALIGN, "over-aligned types are not supported");
+ return static_cast<T*>(AllocateArray(count, sizeof(T), alignof(T)));
+ }
+
+ /// Reclaim a chunk of memory memory that was allocated via one of the above `Allocate*` functions.
+ ///
+ /// This function is not suitable for freeing memory allocated via `AllocateString` or `AllocateStringMaybe`.
+ ///
+ /// Behaviour of this function varies between implementations. Specifically, in the memory-pool-based factory,
+ /// this function does nothing, while in heap-based factory, it calls `free`.
+ ///
+ /// Passing a null pointer here is ok and does nothing.
+ ///
+ /// The behaviour is undefined if the given pointer is not null and it wasn't obtained from a call
+ /// to the `Allocate` function or it was obtained from a call to the `Allocate` function
+ /// of a different factory.
+ virtual void Free(void* data) noexcept = 0;
+
+ /// Allocate memory for string `str` and copy `str` to the allocated memory.
+ ///
+ /// Note: the returned string is not guaranteed to be null-terminated.
+ ///
+ /// Note: some factories may cache results of this function to avoid repeated allocations. Read documentation
+ /// for specific factory for more info.
+ virtual TStringBuf AllocateString(TStringBuf str) noexcept {
+ return TStringBuf(MemCopy(AllocateArrayFor<char>(str.size()), str.data(), str.size()), str.size());
+ }
+
+ /// Reclaim a chunk of memory memory that was allocated via the `AllocateString` function.
+ virtual void FreeString(TStringBuf str) noexcept {
+ Free(const_cast<char*>(str.Data()));
+ }
+
+ /// Like `AllocateString`, but works with `TMaybe<TStringBuf>`.
+ ///
+ /// If the given `str` contains a value, pass it to `AllocateString`.
+ /// If it contains no value, return an empty `TMaybe`.
+ TMaybe<TStringBuf> AllocateStringMaybe(TMaybe<TStringBuf> str) noexcept {
+ if (str.Defined()) {
+ return AllocateString(str.GetRef());
+ } else {
+ return Nothing();
+ }
+ }
+
+ /// Reclaim a chunk of memory memory that was allocated via the `AllocateStringMaybe` function.
+ void FreeStringMaybe(TMaybe<TStringBuf> str) noexcept {
+ if (str.Defined()) {
+ FreeString(str.GetRef());
+ }
+ }
+
+ /// @}
+
+ public:
+ /// @name Type caching and deduplication interface
+ ///
+ /// @{
+ //-
+ /// Lookup the given type in the factory cache. Return cached version of the type or `nullptr`.
+ virtual const TType* LookupCache(const TType* type) noexcept = 0;
+
+ /// Save the given type to the factory cache. Type must be allocated via this factory.
+ virtual void SaveCache(const TType* type) noexcept = 0;
+
+ /// @}
+
+ public:
+ /// @name Factory reference counting interface
+ ///
+ /// @{
+ //-
+ /// Increase reference count of this factory.
+ virtual void Ref() noexcept = 0;
+
+ /// Decrease reference count of this factory.
+ /// If reference count reaches zero, drop this factory.
+ virtual void UnRef() noexcept = 0;
+
+ /// Decrease reference count of this factory.
+ /// If reference count reaches zero, panic.
+ virtual void DecRef() noexcept = 0;
+
+ /// Get current reference count of this factory.
+ virtual long RefCount() const noexcept = 0;
+
+ /// @}
+
+ public:
+ /// @name Type instance reference counting interface
+ ///
+ /// @{
+ //-
+ /// Increase reference count of some object managed by this factory.
+ virtual void RefType(TType*) noexcept = 0;
+
+ /// Decrease reference count of some object managed by this factory.
+ /// If reference count reaches zero, call `TType::Drop` and free object's memory, if needed.
+ virtual void UnRefType(TType*) noexcept = 0;
+
+ /// Decrease reference count of some object managed by this factory.
+ /// If reference count reaches zero, panic.
+ virtual void DecRefType(TType*) noexcept = 0;
+
+ /// Get current reference count of some object managed by this factory.
+ virtual long RefCountType(const TType*) const noexcept = 0;
+
+ /// @}
+ };
+
+ /// Base interface for type factory. There are functions to create type instances, and also a function to
+ /// adopt a type from one factory to another. See the file-level documentation for more info.
+ class ITypeFactory: protected ITypeFactoryInternal {
+ friend class TType;
+ template <typename T>
+ friend class ::TDefaultIntrusivePtrOps;
+
+ public:
+ /// Adopt type into this factory.
+ ///
+ /// This function is used to transfer types between factories. It has the following semantics:
+ ///
+ /// - if `type` is managed by this factory, this function returns its argument unchanged;
+ /// - if `type` is not managed by any factory, this function also returns its argument unchanged;
+ /// - if `type` is managed by another factory, this function deep-copies `type` into this factory and returns
+ /// a pointer to the new deep-copied value.
+ ///
+ /// This function should be used on API borders, whenever you receive ownership over some type that wasn't
+ /// created by you. For example, you may ensure that the type you got is allocated in heap, or you may want to
+ /// copy some type into a memory pool that you have control over.
+ template <typename T>
+ inline TIntrusiveConstPtr<T> Adopt(TIntrusiveConstPtr<T> value) noexcept {
+ return ITypeFactoryInternal::Adopt<T>(std::move(value));
+ }
+
+ public:
+ /// Create a new instance of a type using this factory.
+ /// @{
+ //-
+ /// Create a new `Void` type. See `NTi::TVoidType` for more info.
+ TVoidTypePtr Void();
+
+ /// Create a new `Null` type. See `NTi::TNullType` for more info.
+ TNullTypePtr Null();
+
+ /// Create a new `Bool` type. See `NTi::TBoolType` for more info.
+ TBoolTypePtr Bool();
+
+ /// Create a new `Int8` type. See `NTi::TInt8Type` for more info.
+ TInt8TypePtr Int8();
+
+ /// Create a new `Int16` type. See `NTi::TInt16Type` for more info.
+ TInt16TypePtr Int16();
+
+ /// Create a new `Int32` type. See `NTi::TInt32Type` for more info.
+ TInt32TypePtr Int32();
+
+ /// Create a new `Int64` type. See `NTi::TInt64Type` for more info.
+ TInt64TypePtr Int64();
+
+ /// Create a new `Uint8` type. See `NTi::TUint8Type` for more info.
+ TUint8TypePtr Uint8();
+
+ /// Create a new `Uint16` type. See `NTi::TUint16Type` for more info.
+ TUint16TypePtr Uint16();
+
+ /// Create a new `Uint32` type. See `NTi::TUint32Type` for more info.
+ TUint32TypePtr Uint32();
+
+ /// Create a new `Uint64` type. See `NTi::TUint64Type` for more info.
+ TUint64TypePtr Uint64();
+
+ /// Create a new `Float` type. See `NTi::TFloatType` for more info.
+ TFloatTypePtr Float();
+
+ /// Create a new `Double` type. See `NTi::TDoubleType` for more info.
+ TDoubleTypePtr Double();
+
+ /// Create a new `String` type. See `NTi::TStringType` for more info.
+ TStringTypePtr String();
+
+ /// Create a new `Utf8` type. See `NTi::TUtf8Type` for more info.
+ TUtf8TypePtr Utf8();
+
+ /// Create a new `Date` type. See `NTi::TDateType` for more info.
+ TDateTypePtr Date();
+
+ /// Create a new `Datetime` type. See `NTi::TDatetimeType` for more info.
+ TDatetimeTypePtr Datetime();
+
+ /// Create a new `Timestamp` type. See `NTi::TTimestampType` for more info.
+ TTimestampTypePtr Timestamp();
+
+ /// Create a new `TzDate` type. See `NTi::TTzDateType` for more info.
+ TTzDateTypePtr TzDate();
+
+ /// Create a new `TzDatetime` type. See `NTi::TTzDatetimeType` for more info.
+ TTzDatetimeTypePtr TzDatetime();
+
+ /// Create a new `TzTimestamp` type. See `NTi::TTzTimestampType` for more info.
+ TTzTimestampTypePtr TzTimestamp();
+
+ /// Create a new `Interval` type. See `NTi::TIntervalType` for more info.
+ TIntervalTypePtr Interval();
+
+ /// Create a new `Decimal` type. See `NTi::TDecimalType` for more info.
+ TDecimalTypePtr Decimal(ui8 precision, ui8 scale);
+
+ /// Create a new `Json` type. See `NTi::TJsonType` for more info.
+ TJsonTypePtr Json();
+
+ /// Create a new `Yson` type. See `NTi::TYsonType` for more info.
+ TYsonTypePtr Yson();
+
+ /// Create a new `Uuid` type. See `NTi::TUuidType` for more info.
+ TUuidTypePtr Uuid();
+
+ /// Create a new `Optional` type. See `NTi::TOptionalType` for more info.
+ /// If `item` is managed by some other factory, it will be deep-copied into this factory.
+ TOptionalTypePtr Optional(TTypePtr item);
+
+ /// Create a new `List` type. See `NTi::TListType` for more info.
+ /// If `item` is managed by some other factory, it will be deep-copied into this factory.
+ TListTypePtr List(TTypePtr item);
+
+ /// Create a new `Dict` type. See `NTi::TDictType` for more info.
+ /// If `key` or `value` are managed by some other factory, they will be deep-copied into this factory.
+ TDictTypePtr Dict(TTypePtr key, TTypePtr value);
+
+ /// Create a new `Struct` type. See `NTi::TStructType` for more info.
+ /// If item types are managed by some other factory, they will be deep-copied into this factory.
+ TStructTypePtr Struct(TStructType::TOwnedMembers items);
+ TStructTypePtr Struct(TMaybe<TStringBuf> name, TStructType::TOwnedMembers items);
+
+ /// Create a new `Tuple` type. See `NTi::TTupleType` for more info.
+ /// If item types are managed by some other factory, they will be deep-copied into this factory.
+ TTupleTypePtr Tuple(TTupleType::TOwnedElements items);
+ TTupleTypePtr Tuple(TMaybe<TStringBuf> name, TTupleType::TOwnedElements items);
+
+ /// Create a new `Variant` type. See `NTi::TVariantType` for more info.
+ /// If `inner` is managed by some other factory, it will be deep-copied into this factory.
+ TVariantTypePtr Variant(TTypePtr inner);
+ TVariantTypePtr Variant(TMaybe<TStringBuf> name, TTypePtr inner);
+
+ /// Create a new `Tagged` type. See `NTi::TTaggedType` for more info.
+ /// If `type` is managed by some other factory, it will be deep-copied into this factory.
+ TTaggedTypePtr Tagged(TTypePtr type, TStringBuf tag);
+
+ /// @}
+ };
+
+ /// Type factory over a memory pool.
+ ///
+ /// This interface provides some additional info related to the underlying memory pool state.
+ ///
+ /// Also, since all types that're created via this factory will live exactly as long as this factory lives,
+ /// one doesn't have to actually refcount them. Thus, it is safe to work with raw pointers instead of intrusive
+ /// pointers when using this kind of factory. So we provide the 'raw' interface that creates type instances
+ /// as usual, but doesn't increment any reference counters, thus saving some atomic operations. It still
+ /// adopts nested types into this factory, though.
+ class IPoolTypeFactory: public ITypeFactory {
+ friend class TNamedTypeBuilderRaw;
+ friend class TStructBuilderRaw;
+ friend class TTupleBuilderRaw;
+ friend class TTaggedBuilderRaw;
+
+ public:
+ /// Memory available in the current chunk.
+ ///
+ /// Allocating object of size greater than this number will cause allocation of a new chunk. When this happens,
+ /// available memory in the current chunk is lost, i.e. it'll never be used again.
+ virtual size_t Available() const noexcept = 0;
+
+ /// Number of bytes that're currently used by some object (i.e. they were allocated).
+ ///
+ /// Total memory consumed by arena is `MemoryAllocated() + MemoryWaste()`.
+ virtual size_t MemoryAllocated() const noexcept = 0;
+
+ /// Number of bytes that're not used by anyone.
+ ///
+ /// Total memory consumed by arena is `MemoryAllocated() + MemoryWaste()`.
+ ///
+ /// Memory that's lost and will not be reused is `MemoryWaste() - Available()`.
+ virtual size_t MemoryWaste() const noexcept = 0;
+
+ public:
+ /// Adopt type into this factory.
+ ///
+ /// This works like `ITypeFactory::Adopt`, but returns a raw pointer.
+ template <typename T>
+ inline const T* AdoptRaw(const T* value) noexcept {
+ return ITypeFactoryInternal::AdoptRaw<T>(value);
+ }
+
+ public:
+ /// Raw versions of type constructors. See the class-level documentation for more info.
+ ///
+ /// @{
+ //-
+ /// Create a new `Void` type. See `NTi::TVoidType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TVoidType* VoidRaw();
+
+ /// Create a new `Null` type. See `NTi::TNullType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TNullType* NullRaw();
+
+ /// Create a new `Bool` type. See `NTi::TBoolType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TBoolType* BoolRaw();
+
+ /// Create a new `Int8` type. See `NTi::TInt8Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TInt8Type* Int8Raw();
+
+ /// Create a new `Int16` type. See `NTi::TInt16Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TInt16Type* Int16Raw();
+
+ /// Create a new `Int32` type. See `NTi::TInt32Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TInt32Type* Int32Raw();
+
+ /// Create a new `Int64` type. See `NTi::TInt64Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TInt64Type* Int64Raw();
+
+ /// Create a new `Uint8` type. See `NTi::TUint8Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUint8Type* Uint8Raw();
+
+ /// Create a new `Uint16` type. See `NTi::TUint16Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUint16Type* Uint16Raw();
+
+ /// Create a new `Uint32` type. See `NTi::TUint32Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUint32Type* Uint32Raw();
+
+ /// Create a new `Uint64` type. See `NTi::TUint64Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUint64Type* Uint64Raw();
+
+ /// Create a new `Float` type. See `NTi::TFloatType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TFloatType* FloatRaw();
+
+ /// Create a new `Double` type. See `NTi::TDoubleType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TDoubleType* DoubleRaw();
+
+ /// Create a new `String` type. See `NTi::TStringType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TStringType* StringRaw();
+
+ /// Create a new `Utf8` type. See `NTi::TUtf8Type` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUtf8Type* Utf8Raw();
+
+ /// Create a new `Date` type. See `NTi::TDateType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TDateType* DateRaw();
+
+ /// Create a new `Datetime` type. See `NTi::TDatetimeType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TDatetimeType* DatetimeRaw();
+
+ /// Create a new `Timestamp` type. See `NTi::TTimestampType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTimestampType* TimestampRaw();
+
+ /// Create a new `TzDate` type. See `NTi::TTzDateType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTzDateType* TzDateRaw();
+
+ /// Create a new `TzDatetime` type. See `NTi::TTzDatetimeType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTzDatetimeType* TzDatetimeRaw();
+
+ /// Create a new `TzTimestamp` type. See `NTi::TTzTimestampType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTzTimestampType* TzTimestampRaw();
+
+ /// Create a new `Interval` type. See `NTi::TIntervalType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TIntervalType* IntervalRaw();
+
+ /// Create a new `Decimal` type. See `NTi::TDecimalType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TDecimalType* DecimalRaw(ui8 precision, ui8 scale);
+
+ /// Create a new `Json` type. See `NTi::TJsonType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TJsonType* JsonRaw();
+
+ /// Create a new `Yson` type. See `NTi::TYsonType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TYsonType* YsonRaw();
+
+ /// Create a new `Uuid` type. See `NTi::TUuidType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TUuidType* UuidRaw();
+
+ /// Create a new `Optional` type. See `NTi::TOptionalType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TOptionalType* OptionalRaw(const TType* item);
+
+ /// Create a new `List` type. See `NTi::TListType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TListType* ListRaw(const TType* item);
+
+ /// Create a new `Dict` type. See `NTi::TDictType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TDictType* DictRaw(const TType* key, const TType* value);
+
+ /// Create a new `Struct` type. See `NTi::TStructType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TStructType* StructRaw(TStructType::TMembers items);
+ const TStructType* StructRaw(TMaybe<TStringBuf> name, TStructType::TMembers items);
+
+ /// Create a new `Tuple` type. See `NTi::TTupleType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTupleType* TupleRaw(TTupleType::TElements items);
+ const TTupleType* TupleRaw(TMaybe<TStringBuf> name, TTupleType::TElements items);
+
+ /// Create a new `Variant` type. See `NTi::TVariantType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TVariantType* VariantRaw(const TType* inner);
+ const TVariantType* VariantRaw(TMaybe<TStringBuf> name, const TType* inner);
+
+ /// Create a new `Tagged` type. See `NTi::TTaggedType` for more info.
+ /// The returned object will live for as long as this factory lives.
+ const TTaggedType* TaggedRaw(const TType* type, TStringBuf tag);
+
+ /// @}
+ };
+
+ namespace NPrivate {
+ ITypeFactory* GetDefaultHeapFactory();
+ }
+
+ /// Create a heap-based factory.
+ ///
+ /// This factory uses heap to allocate type instances, and reference counting to track individual type lifetimes.
+ ///
+ /// Choose this factory if you need safety and flexibility.
+ ITypeFactoryPtr HeapFactory();
+
+ /// Create a memory-pool-based factory.
+ ///
+ /// This factory uses a memory pool to allocate type instances. All allocated memory will be released when
+ /// the factory dies.
+ ///
+ /// Choose this factory if you need speed and memory efficiency. You have to understand how memory pool works.
+ /// We advise testing your code with msan when using this factory.
+ ///
+ /// If in doubt, use a heap-based factory instead.
+ ///
+ /// @param deduplicate
+ /// if enabled, factory will cache all types and avoid allocations when creating a new type that is
+ /// strictly-equal (see `type_equivalence.h`) to some previously created type.
+ ///
+ /// For example:
+ ///
+ /// ```
+ /// auto a = factory.Optional(factory.String());
+ /// auto b = factory.Optional(factory.String());
+ /// Y_ASSERT(a == b);
+ /// ```
+ ///
+ /// If `deduplicate` is disabled, this assert will fail. If, however, `deduplicate` is enabled,
+ /// the assert will not fail because `a` and `b` will point to the same type instance.
+ ///
+ /// This behaviour slows down creation process, but can save some memory when working with complex types.
+ ///
+ /// @param initial
+ /// number of bytes in the first page of the memory pool.
+ /// By default, we pre-allocate `64` bytes.
+ ///
+ /// @param grow
+ /// policy for calculating size for a next pool page.
+ /// By default, next page is twice as big as the previous one.
+ ///
+ /// @param alloc
+ /// underlying allocator that'll be used to allocate pool pages.
+ ///
+ /// @param options
+ /// other memory pool options.
+ IPoolTypeFactoryPtr PoolFactory(
+ bool deduplicate = true,
+ size_t initial = 64,
+ TMemoryPool::IGrowPolicy* grow = TMemoryPool::TExpGrow::Instance(),
+ IAllocator* alloc = TDefaultAllocator::Instance(),
+ TMemoryPool::TOptions options = {});
+}
diff --git a/library/cpp/type_info/type_info.cpp b/library/cpp/type_info/type_info.cpp
new file mode 100644
index 0000000000..8c14b158d0
--- /dev/null
+++ b/library/cpp/type_info/type_info.cpp
@@ -0,0 +1 @@
+#include "type_info.h"
diff --git a/library/cpp/type_info/type_info.h b/library/cpp/type_info/type_info.h
new file mode 100644
index 0000000000..5c6e249c11
--- /dev/null
+++ b/library/cpp/type_info/type_info.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "builder.h"
+#include "error.h"
+#include "type.h"
+#include "type_constructors.h"
+#include "type_equivalence.h"
+#include "type_factory.h"
+#include "type_io.h"
+#include "type_list.h"
diff --git a/library/cpp/type_info/type_io.cpp b/library/cpp/type_info/type_io.cpp
new file mode 100644
index 0000000000..90b98c8baa
--- /dev/null
+++ b/library/cpp/type_info/type_io.cpp
@@ -0,0 +1,1186 @@
+#include "type_io.h"
+
+#include "builder.h"
+#include "type_constructors.h"
+#include "type_factory.h"
+
+#include <util/generic/overloaded.h>
+
+#include <library/cpp/yson_pull/read_ops.h>
+
+#include <util/stream/mem.h>
+#include <util/string/cast.h>
+#include <util/generic/vector.h>
+#include <util/generic/scope.h>
+
+namespace NTi::NIo {
+ namespace {
+ class TYsonDeserializer: private TNonCopyable {
+ public:
+ TYsonDeserializer(IPoolTypeFactory* factory, NYsonPull::TReader* reader)
+ : Factory_(factory)
+ , Reader_(reader)
+ {
+ }
+
+ public:
+ const TType* ReadType() {
+ if (++Depth_ > 100) {
+ ythrow TDeserializationException() << "types are nested too deep";
+ }
+
+ Y_DEFER {
+ Depth_--;
+ };
+
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() == NYsonPull::EEventType::BeginStream) {
+ event = Reader_->NextEvent();
+ }
+
+ if (event.Type() == NYsonPull::EEventType::EndStream) {
+ if (Depth_ == 1) {
+ return nullptr;
+ } else {
+ ythrow TDeserializationException() << "unexpected end of stream";
+ }
+ }
+
+ if (event.Type() == NYsonPull::EEventType::Scalar && event.AsScalar().Type() == NYsonPull::EScalarType::String) {
+ return ReadTypeFromData(TypeNameStringToEnum(event.AsString()), std::monostate{});
+ } else if (event.Type() == NYsonPull::EEventType::BeginMap) {
+ return ReadTypeFromMap();
+ } else {
+ ythrow TDeserializationException() << "type must be either a string or a map";
+ }
+ }
+
+ private:
+ struct TDictData {
+ const TType *Key, *Value;
+ };
+ struct TDecimalData {
+ ui8 Precision, Scale;
+ };
+ using TTypeData = std::variant<
+ std::monostate,
+ TDictData,
+ TDecimalData,
+ TStructBuilderRaw,
+ TTupleBuilderRaw,
+ TTaggedBuilderRaw>;
+
+ const TType* ReadTypeFromMap() {
+ TMaybe<ETypeName> typeName;
+ TTypeData data;
+
+ while (true) {
+ auto event = Reader_->NextEvent();
+ if (event.Type() == NYsonPull::EEventType::Key) {
+ auto mapKey = event.AsString();
+
+ if (mapKey == "type_name") {
+ if (typeName.Defined()) {
+ ythrow TDeserializationException() << R"(duplicate key R"(type_name"))";
+ } else {
+ typeName = TypeNameStringToEnum(ReadString(R"("type_name")"));
+ }
+ } else if (mapKey == "item") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TTaggedBuilderRaw(*Factory_);
+ }
+ if (std::holds_alternative<TTaggedBuilderRaw>(data)) {
+ auto& builder = std::get<TTaggedBuilderRaw>(data);
+ if (!builder.HasItem()) {
+ builder.SetItem(ReadType());
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "item")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "item")";
+ }
+ } else if (mapKey == "key") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TDictData{nullptr, nullptr};
+ }
+ if (std::holds_alternative<TDictData>(data)) {
+ auto& dictData = std::get<TDictData>(data);
+ if (dictData.Key == nullptr) {
+ dictData.Key = ReadType();
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "key")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "key")";
+ }
+ } else if (mapKey == "value") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TDictData{nullptr, nullptr};
+ }
+ if (std::holds_alternative<TDictData>(data)) {
+ auto& dictData = std::get<TDictData>(data);
+ if (dictData.Value == nullptr) {
+ dictData.Value = ReadType();
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "value")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "value")";
+ }
+ } else if (mapKey == "members") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TStructBuilderRaw(*Factory_);
+ }
+ if (std::holds_alternative<TStructBuilderRaw>(data)) {
+ ReadMembers(std::get<TStructBuilderRaw>(data));
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "members")";
+ }
+ } else if (mapKey == "elements") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TTupleBuilderRaw(*Factory_);
+ }
+ if (std::holds_alternative<TTupleBuilderRaw>(data)) {
+ ReadElements(std::get<TTupleBuilderRaw>(data));
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "elements")";
+ }
+ } else if (mapKey == "tag") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TTaggedBuilderRaw(*Factory_);
+ }
+ if (std::holds_alternative<TTaggedBuilderRaw>(data)) {
+ auto& builder = std::get<TTaggedBuilderRaw>(data);
+ if (!builder.HasTag()) {
+ builder.SetTag(ReadString(R"("tag")"));
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "tag")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "tag")";
+ }
+ } else if (mapKey == "precision") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TDecimalData{ReadSmallInt(R"("precision")"), 0};
+ } else if (std::holds_alternative<TDecimalData>(data)) {
+ auto& decimalData = std::get<TDecimalData>(data);
+ if (decimalData.Precision == 0) {
+ decimalData.Precision = ReadSmallInt(R"("precision")");
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "precision")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "precision")";
+ }
+ } else if (mapKey == "scale") {
+ if (std::holds_alternative<std::monostate>(data)) {
+ data = TDecimalData{0, ReadSmallInt(R"("scale")")};
+ } else if (std::holds_alternative<TDecimalData>(data)) {
+ auto& decimalData = std::get<TDecimalData>(data);
+ if (decimalData.Scale == 0) {
+ decimalData.Scale = ReadSmallInt(R"("scale")");
+ } else {
+ ythrow TDeserializationException() << R"(duplicate key "scale")";
+ }
+ } else {
+ ythrow TDeserializationException() << R"(unexpected key "scale")";
+ }
+ } else {
+ NYsonPull::NReadOps::SkipValue(*Reader_);
+ }
+ } else if (event.Type() == NYsonPull::EEventType::EndMap) {
+ if (!typeName.Defined()) {
+ ythrow TDeserializationException() << R"(missing required key "type_name")";
+ }
+
+ return ReadTypeFromData(*typeName, std::move(data));
+ } else {
+ ythrow TDeserializationException() << "unexpected event " << event.Type();
+ }
+ }
+ }
+
+ const TType* ReadTypeFromData(ETypeName typeName, TTypeData data) {
+ const TType* type;
+
+ switch (typeName) {
+ case ETypeName::Bool:
+ type = TBoolType::InstanceRaw();
+ break;
+ case ETypeName::Int8:
+ type = TInt8Type::InstanceRaw();
+ break;
+ case ETypeName::Int16:
+ type = TInt16Type::InstanceRaw();
+ break;
+ case ETypeName::Int32:
+ type = TInt32Type::InstanceRaw();
+ break;
+ case ETypeName::Int64:
+ type = TInt64Type::InstanceRaw();
+ break;
+ case ETypeName::Uint8:
+ type = TUint8Type::InstanceRaw();
+ break;
+ case ETypeName::Uint16:
+ type = TUint16Type::InstanceRaw();
+ break;
+ case ETypeName::Uint32:
+ type = TUint32Type::InstanceRaw();
+ break;
+ case ETypeName::Uint64:
+ type = TUint64Type::InstanceRaw();
+ break;
+ case ETypeName::Float:
+ type = TFloatType::InstanceRaw();
+ break;
+ case ETypeName::Double:
+ type = TDoubleType::InstanceRaw();
+ break;
+ case ETypeName::String:
+ type = TStringType::InstanceRaw();
+ break;
+ case ETypeName::Utf8:
+ type = TUtf8Type::InstanceRaw();
+ break;
+ case ETypeName::Date:
+ type = TDateType::InstanceRaw();
+ break;
+ case ETypeName::Datetime:
+ type = TDatetimeType::InstanceRaw();
+ break;
+ case ETypeName::Timestamp:
+ type = TTimestampType::InstanceRaw();
+ break;
+ case ETypeName::TzDate:
+ type = TTzDateType::InstanceRaw();
+ break;
+ case ETypeName::TzDatetime:
+ type = TTzDatetimeType::InstanceRaw();
+ break;
+ case ETypeName::TzTimestamp:
+ type = TTzTimestampType::InstanceRaw();
+ break;
+ case ETypeName::Interval:
+ type = TIntervalType::InstanceRaw();
+ break;
+ case ETypeName::Decimal: {
+ if (!std::holds_alternative<TDecimalData>(data)) {
+ ythrow TDeserializationException() << R"(missing required keys "precision" and "scale" for type Decimal)";
+ }
+
+ auto& decimalData = std::get<TDecimalData>(data);
+
+ if (decimalData.Precision == 0) {
+ ythrow TDeserializationException() << R"(missing required key "precision" for type Decimal)";
+ }
+
+ if (decimalData.Scale == 0) {
+ ythrow TDeserializationException() << R"(missing required key "scale" for type Decimal)";
+ }
+
+ return Factory_->DecimalRaw(decimalData.Precision, decimalData.Scale);
+ }
+ case ETypeName::Json:
+ type = TJsonType::InstanceRaw();
+ break;
+ case ETypeName::Yson:
+ type = TYsonType::InstanceRaw();
+ break;
+ case ETypeName::Uuid:
+ type = TUuidType::InstanceRaw();
+ break;
+ case ETypeName::Void:
+ type = TVoidType::InstanceRaw();
+ break;
+ case ETypeName::Null:
+ type = TNullType::InstanceRaw();
+ break;
+ case ETypeName::Optional: {
+ if (!std::holds_alternative<TTaggedBuilderRaw>(data)) {
+ ythrow TDeserializationException() << R"(missing required key "item" for type Optional)";
+ }
+
+ auto& builder = std::get<TTaggedBuilderRaw>(data);
+
+ if (!builder.HasItem()) {
+ ythrow TDeserializationException() << R"(missing required key "item" for type Optional)";
+ }
+
+ return Factory_->OptionalRaw(*builder.GetItem());
+ }
+ case ETypeName::List: {
+ if (!std::holds_alternative<TTaggedBuilderRaw>(data)) {
+ ythrow TDeserializationException() << R"(missing required key "item" for type List)";
+ }
+
+ auto& builder = std::get<TTaggedBuilderRaw>(data);
+
+ if (!builder.HasItem()) {
+ ythrow TDeserializationException() << R"(missing required key "item" for type List)";
+ }
+
+ return Factory_->ListRaw(*builder.GetItem());
+ }
+ case ETypeName::Dict: {
+ if (!std::holds_alternative<TDictData>(data)) {
+ ythrow TDeserializationException() << R"(missing required keys "key" and "value" for type Dict)";
+ }
+
+ auto& dictData = std::get<TDictData>(data);
+
+ if (dictData.Key == nullptr) {
+ ythrow TDeserializationException() << R"(missing required key "key" for type Dict)";
+ }
+
+ if (dictData.Value == nullptr) {
+ ythrow TDeserializationException() << R"(missing required key "value" for type Dict)";
+ }
+
+ return Factory_->DictRaw(dictData.Key, dictData.Value);
+ }
+ case ETypeName::Struct: {
+ if (!std::holds_alternative<TStructBuilderRaw>(data)) {
+ ythrow TDeserializationException() << R"(missing required key "members" for type Struct)";
+ }
+
+ return std::get<TStructBuilderRaw>(data).BuildRaw();
+ }
+ case ETypeName::Tuple: {
+ if (!std::holds_alternative<TTupleBuilderRaw>(data)) {
+ ythrow TDeserializationException() << R"(missing required key "elements" for type Tuple)";
+ }
+
+ return std::get<TTupleBuilderRaw>(data).BuildRaw();
+ }
+ case ETypeName::Variant: {
+ if (std::holds_alternative<TStructBuilderRaw>(data)) {
+ return std::get<TStructBuilderRaw>(data).BuildVariantRaw();
+ } else if (std::holds_alternative<TTupleBuilderRaw>(data)) {
+ return std::get<TTupleBuilderRaw>(data).BuildVariantRaw();
+ } else {
+ ythrow TDeserializationException() << R"(missing both keys "members" and "elements" for type Variant)";
+ }
+ }
+ case ETypeName::Tagged: {
+ if (!std::holds_alternative<TTaggedBuilderRaw>(data)) {
+ ythrow TDeserializationException() << R"(missing required keys "tag" and "item" for type Tagged)";
+ }
+
+ auto& builder = std::get<TTaggedBuilderRaw>(data);
+
+ if (!builder.HasItem()) {
+ ythrow TDeserializationException() << R"(missing required key "item" for type Tagged)";
+ }
+
+ if (!builder.HasTag()) {
+ ythrow TDeserializationException() << R"(missing required key "tag" for type Tagged)";
+ }
+
+ return builder.BuildRaw();
+ }
+ }
+
+ if (!std::holds_alternative<std::monostate>(data)) {
+ ythrow TDeserializationException() << "unexpected key for type " << typeName;
+ }
+
+ return type;
+ }
+
+ TStringBuf ReadString(TStringBuf what) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() != NYsonPull::EEventType::Scalar || event.AsScalar().Type() != NYsonPull::EScalarType::String) {
+ ythrow TDeserializationException() << what << " must contain a string";
+ }
+
+ return event.AsString();
+ }
+
+ ui8 ReadSmallInt(TStringBuf what) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() != NYsonPull::EEventType::Scalar || event.AsScalar().Type() != NYsonPull::EScalarType::Int64) {
+ ythrow TDeserializationException() << what << " must contain a signed integer";
+ }
+
+ auto result = event.AsScalar().AsInt64();
+
+ if (result <= 0) {
+ ythrow TDeserializationException() << what << " must be greater than zero";
+ }
+
+ if (result > Max<ui8>()) {
+ ythrow TDeserializationException() << what << " is too big";
+ }
+
+ return static_cast<ui8>(result);
+ }
+
+ void ReadMembers(TStructBuilderRaw& builder) {
+ if (Reader_->NextEvent().Type() != NYsonPull::EEventType::BeginList) {
+ ythrow TDeserializationException() << R"("members" must contain a list)";
+ }
+
+ while (true) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() == NYsonPull::EEventType::BeginMap) {
+ while (true) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() == NYsonPull::EEventType::Key) {
+ auto mapKey = event.AsString();
+ if (mapKey == "name") {
+ if (builder.HasMemberName()) {
+ ythrow TDeserializationException() << R"(duplicate key "name")";
+ }
+
+ builder.AddMemberName(ReadString(R"("name")"));
+ } else if (mapKey == "type") {
+ if (builder.HasMemberType()) {
+ ythrow TDeserializationException() << R"(duplicate key "type")";
+ }
+
+ builder.AddMemberType(ReadType());
+ } else {
+ NYsonPull::NReadOps::SkipValue(*Reader_);
+ }
+ } else if (event.Type() == NYsonPull::EEventType::EndMap) {
+ if (!builder.HasMemberName()) {
+ ythrow TDeserializationException() << R"(missing required key "name")";
+ }
+ if (!builder.HasMemberType()) {
+ ythrow TDeserializationException() << R"(missing required key "type")";
+ }
+
+ builder.AddMember();
+ break;
+ } else {
+ ythrow TDeserializationException() << "unexpected event " << event.Type();
+ }
+ }
+ } else if (event.Type() == NYsonPull::EEventType::EndList) {
+ break;
+ } else {
+ ythrow TDeserializationException() << R"("members" must contain a list of maps)";
+ }
+ }
+ }
+
+ void ReadElements(TTupleBuilderRaw& builder) {
+ if (Reader_->NextEvent().Type() != NYsonPull::EEventType::BeginList) {
+ ythrow TDeserializationException() << R"("elements" must contain a list)";
+ }
+
+ while (true) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() == NYsonPull::EEventType::BeginMap) {
+ while (true) {
+ auto event = Reader_->NextEvent();
+
+ if (event.Type() == NYsonPull::EEventType::Key) {
+ auto mapKey = event.AsString();
+ if (mapKey == "type") {
+ if (builder.HasElementType()) {
+ ythrow TDeserializationException() << R"(duplicate key "type")";
+ }
+
+ builder.AddElementType(ReadType());
+ } else {
+ NYsonPull::NReadOps::SkipValue(*Reader_);
+ }
+ } else if (event.Type() == NYsonPull::EEventType::EndMap) {
+ if (!builder.HasElementType()) {
+ ythrow TDeserializationException() << R"(missing required key "type")";
+ }
+
+ builder.AddElement();
+ break;
+ } else {
+ ythrow TDeserializationException() << "unexpected event " << event.Type();
+ }
+ }
+ } else if (event.Type() == NYsonPull::EEventType::EndList) {
+ break;
+ } else {
+ ythrow TDeserializationException() << R"("elements" must contain a list of maps)";
+ }
+ }
+ }
+
+ static ETypeName TypeNameStringToEnum(TStringBuf name) {
+ static const THashMap<TStringBuf, ETypeName> dispatch = {
+ {"void", ETypeName::Void},
+ {"null", ETypeName::Null},
+ {"bool", ETypeName::Bool},
+ {"int8", ETypeName::Int8},
+ {"int16", ETypeName::Int16},
+ {"int32", ETypeName::Int32},
+ {"int64", ETypeName::Int64},
+ {"uint8", ETypeName::Uint8},
+ {"uint16", ETypeName::Uint16},
+ {"uint32", ETypeName::Uint32},
+ {"uint64", ETypeName::Uint64},
+ {"float", ETypeName::Float},
+ {"double", ETypeName::Double},
+ {"string", ETypeName::String},
+ {"utf8", ETypeName::Utf8},
+ {"date", ETypeName::Date},
+ {"datetime", ETypeName::Datetime},
+ {"timestamp", ETypeName::Timestamp},
+ {"tz_date", ETypeName::TzDate},
+ {"tz_datetime", ETypeName::TzDatetime},
+ {"tz_timestamp", ETypeName::TzTimestamp},
+ {"interval", ETypeName::Interval},
+ {"json", ETypeName::Json},
+ {"yson", ETypeName::Yson},
+ {"uuid", ETypeName::Uuid},
+ {"decimal", ETypeName::Decimal},
+ {"optional", ETypeName::Optional},
+ {"list", ETypeName::List},
+ {"dict", ETypeName::Dict},
+ {"struct", ETypeName::Struct},
+ {"tuple", ETypeName::Tuple},
+ {"variant", ETypeName::Variant},
+ {"tagged", ETypeName::Tagged},
+ };
+
+ if (auto it = dispatch.find(name); it != dispatch.end()) {
+ return it->second;
+ } else {
+ ythrow TDeserializationException() << "unknown type " << TString{name}.Quote();
+ }
+ }
+
+ private:
+ IPoolTypeFactory* Factory_;
+ NYsonPull::TReader* Reader_;
+ size_t Depth_ = 0;
+ };
+ }
+
+ TTypePtr DeserializeYson(ITypeFactory& factory, NYsonPull::TReader& reader, bool deduplicate) {
+ auto pool = PoolFactory(deduplicate, 2048);
+ auto type = DeserializeYsonRaw(*pool, reader);
+ return factory.Adopt(type->AsPtr());
+ }
+
+ TTypePtr DeserializeYson(ITypeFactory& factory, TStringBuf data, bool deduplicate) {
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromMemory(data), NYsonPull::EStreamType::Node);
+ return DeserializeYson(factory, reader, deduplicate);
+ }
+
+ TTypePtr DeserializeYson(ITypeFactory& factory, IInputStream& input, bool deduplicate) {
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromInputStream(&input), NYsonPull::EStreamType::Node);
+ return DeserializeYson(factory, reader, deduplicate);
+ }
+
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, NYsonPull::TReader& reader) {
+ if (reader.LastEvent().Type() != NYsonPull::EEventType::BeginStream) {
+ ythrow TDeserializationException() << "stream contains extraneous data";
+ }
+
+ auto type = DeserializeYsonMultipleRaw(factory, reader);
+
+ if (type == nullptr) {
+ ythrow TDeserializationException() << "unexpected end of stream";
+ }
+
+ if (reader.NextEvent().Type() != NYsonPull::EEventType::EndStream) {
+ ythrow TDeserializationException() << "stream contains extraneous data";
+ }
+
+ return type;
+ }
+
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, TStringBuf data) {
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromMemory(data), NYsonPull::EStreamType::Node);
+ return DeserializeYsonRaw(factory, reader);
+ }
+
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, IInputStream& input) {
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromInputStream(&input), NYsonPull::EStreamType::Node);
+ return DeserializeYsonRaw(factory, reader);
+ }
+
+ const TType* DeserializeYsonMultipleRaw(IPoolTypeFactory& factory, NYsonPull::TReader& reader) {
+ return TYsonDeserializer(&factory, &reader).ReadType();
+ }
+
+ TString SerializeYson(const TType* type, bool humanReadable, bool includeTags) {
+ auto result = TString();
+ auto writer = humanReadable
+ ? NYsonPull::MakePrettyTextWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::Node)
+ : NYsonPull::MakeBinaryWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::Node);
+ SerializeYson(type, writer.GetConsumer(), includeTags);
+ return result;
+ }
+
+ void SerializeYson(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags) {
+ consumer.OnBeginStream();
+ SerializeYsonMultiple(type, consumer, includeTags);
+ consumer.OnEndStream();
+ }
+
+ void SerializeYson(const TType* type, IOutputStream& stream, bool humanReadable, bool includeTags) {
+ auto writer = humanReadable
+ ? NYsonPull::MakePrettyTextWriter(NYsonPull::NOutput::FromOutputStream(&stream), NYsonPull::EStreamType::Node)
+ : NYsonPull::MakeBinaryWriter(NYsonPull::NOutput::FromOutputStream(&stream), NYsonPull::EStreamType::Node);
+ SerializeYson(type, writer.GetConsumer(), includeTags);
+ }
+
+ void SerializeYsonMultiple(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags) {
+ type->VisitRaw(TOverloaded{
+ [&consumer](const TVoidType*) {
+ consumer.OnScalarString("void");
+ },
+ [&consumer](const TNullType*) {
+ consumer.OnScalarString("null");
+ },
+ [&consumer](const TBoolType*) {
+ consumer.OnScalarString("bool");
+ },
+ [&consumer](const TInt8Type*) {
+ consumer.OnScalarString("int8");
+ },
+ [&consumer](const TInt16Type*) {
+ consumer.OnScalarString("int16");
+ },
+ [&consumer](const TInt32Type*) {
+ consumer.OnScalarString("int32");
+ },
+ [&consumer](const TInt64Type*) {
+ consumer.OnScalarString("int64");
+ },
+ [&consumer](const TUint8Type*) {
+ consumer.OnScalarString("uint8");
+ },
+ [&consumer](const TUint16Type*) {
+ consumer.OnScalarString("uint16");
+ },
+ [&consumer](const TUint32Type*) {
+ consumer.OnScalarString("uint32");
+ },
+ [&consumer](const TUint64Type*) {
+ consumer.OnScalarString("uint64");
+ },
+ [&consumer](const TFloatType*) {
+ consumer.OnScalarString("float");
+ },
+ [&consumer](const TDoubleType*) {
+ consumer.OnScalarString("double");
+ },
+ [&consumer](const TStringType*) {
+ consumer.OnScalarString("string");
+ },
+ [&consumer](const TUtf8Type*) {
+ consumer.OnScalarString("utf8");
+ },
+ [&consumer](const TDateType*) {
+ consumer.OnScalarString("date");
+ },
+ [&consumer](const TDatetimeType*) {
+ consumer.OnScalarString("datetime");
+ },
+ [&consumer](const TTimestampType*) {
+ consumer.OnScalarString("timestamp");
+ },
+ [&consumer](const TTzDateType*) {
+ consumer.OnScalarString("tz_date");
+ },
+ [&consumer](const TTzDatetimeType*) {
+ consumer.OnScalarString("tz_datetime");
+ },
+ [&consumer](const TTzTimestampType*) {
+ consumer.OnScalarString("tz_timestamp");
+ },
+ [&consumer](const TIntervalType*) {
+ consumer.OnScalarString("interval");
+ },
+ [&consumer](const TJsonType*) {
+ consumer.OnScalarString("json");
+ },
+ [&consumer](const TYsonType*) {
+ consumer.OnScalarString("yson");
+ },
+ [&consumer](const TUuidType*) {
+ consumer.OnScalarString("uuid");
+ },
+ [&consumer](const TDecimalType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("decimal");
+
+ consumer.OnKey("precision");
+ consumer.OnScalarInt64(t->GetPrecision());
+
+ consumer.OnKey("scale");
+ consumer.OnScalarInt64(t->GetScale());
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TOptionalType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("optional");
+
+ consumer.OnKey("item");
+ SerializeYsonMultiple(t->GetItemTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TListType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("list");
+
+ consumer.OnKey("item");
+ SerializeYsonMultiple(t->GetItemTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TDictType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("dict");
+
+ consumer.OnKey("key");
+ SerializeYsonMultiple(t->GetKeyTypeRaw(), consumer, includeTags);
+
+ consumer.OnKey("value");
+ SerializeYsonMultiple(t->GetValueTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TStructType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("struct");
+
+ consumer.OnKey("members");
+ consumer.OnBeginList();
+ for (auto& item : t->GetMembers()) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("name");
+ consumer.OnScalarString(item.GetName());
+
+ consumer.OnKey("type");
+ SerializeYsonMultiple(item.GetTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ }
+ consumer.OnEndList();
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TTupleType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("tuple");
+
+ consumer.OnKey("elements");
+ consumer.OnBeginList();
+ for (auto& item : t->GetElements()) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type");
+ SerializeYsonMultiple(item.GetTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ }
+ consumer.OnEndList();
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TVariantType* t) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("variant");
+
+ t->VisitUnderlyingRaw(
+ TOverloaded{
+ [&consumer, includeTags](const TStructType* t) {
+ // Warning: we loose struct's name here.
+ // See https://ml.yandex-team.ru/thread/data-com-dev/171136785840079161/
+
+ consumer.OnKey("members");
+ consumer.OnBeginList();
+ for (auto& item : t->GetMembers()) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("name");
+ consumer.OnScalarString(item.GetName());
+
+ consumer.OnKey("type");
+ SerializeYsonMultiple(item.GetTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ }
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TTupleType* t) {
+ // Warning: we loose tuple's name here.
+ // See https://ml.yandex-team.ru/thread/data-com-dev/171136785840079161/
+
+ consumer.OnKey("elements");
+ consumer.OnBeginList();
+ for (auto& item : t->GetElements()) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type");
+ SerializeYsonMultiple(item.GetTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ }
+ consumer.OnEndList();
+ }});
+
+ consumer.OnEndMap();
+ },
+ [&consumer, includeTags](const TTaggedType* t) {
+ if (includeTags) {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("type_name");
+ consumer.OnScalarString("tagged");
+
+ consumer.OnKey("tag");
+ consumer.OnScalarString(t->GetTag());
+
+ consumer.OnKey("item");
+ SerializeYsonMultiple(t->GetItemTypeRaw(), consumer, includeTags);
+
+ consumer.OnEndMap();
+ } else {
+ SerializeYsonMultiple(t->GetItemTypeRaw(), consumer, includeTags);
+ }
+ },
+ });
+ }
+
+ namespace {
+ void WriteVoidType(NYsonPull::IConsumer& consumer) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("VoidType");
+ consumer.OnEndList();
+ }
+
+ void WriteNullType(NYsonPull::IConsumer& consumer) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("NullType");
+ consumer.OnEndList();
+ }
+
+ void WriteDataType(NYsonPull::IConsumer& consumer, EPrimitiveTypeName name) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("DataType");
+ consumer.OnScalarString(ToString(name));
+ consumer.OnEndList();
+ }
+ }
+
+ void AsYqlType(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags) {
+ type->VisitRaw(TOverloaded{
+ [&consumer](const TVoidType*) {
+ WriteVoidType(consumer);
+ },
+ [&consumer](const TNullType*) {
+ WriteNullType(consumer);
+ },
+ [&consumer](const TBoolType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Bool);
+ },
+ [&consumer](const TInt8Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Int8);
+ },
+ [&consumer](const TInt16Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Int16);
+ },
+ [&consumer](const TInt32Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Int32);
+ },
+ [&consumer](const TInt64Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Int64);
+ },
+ [&consumer](const TUint8Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Uint8);
+ },
+ [&consumer](const TUint16Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Uint16);
+ },
+ [&consumer](const TUint32Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Uint32);
+ },
+ [&consumer](const TUint64Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Uint64);
+ },
+ [&consumer](const TFloatType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Float);
+ },
+ [&consumer](const TDoubleType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Double);
+ },
+ [&consumer](const TStringType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::String);
+ },
+ [&consumer](const TUtf8Type*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Utf8);
+ },
+ [&consumer](const TDateType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Date);
+ },
+ [&consumer](const TDatetimeType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Datetime);
+ },
+ [&consumer](const TTimestampType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Timestamp);
+ },
+ [&consumer](const TTzDateType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::TzDate);
+ },
+ [&consumer](const TTzDatetimeType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::TzDatetime);
+ },
+ [&consumer](const TTzTimestampType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::TzTimestamp);
+ },
+ [&consumer](const TIntervalType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Interval);
+ },
+ [&consumer](const TJsonType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Json);
+ },
+ [&consumer](const TYsonType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Yson);
+ },
+ [&consumer](const TUuidType*) {
+ WriteDataType(consumer, EPrimitiveTypeName::Uuid);
+ },
+ [&consumer](const TDecimalType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("DataType");
+ consumer.OnScalarString("Decimal");
+ consumer.OnScalarString(ToString(t->GetPrecision()));
+ consumer.OnScalarString(ToString(t->GetScale()));
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TOptionalType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("OptionalType");
+ AsYqlType(t->GetItemTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TListType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("ListType");
+ AsYqlType(t->GetItemTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TDictType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("DictType");
+ AsYqlType(t->GetKeyTypeRaw(), consumer, includeTags);
+ AsYqlType(t->GetValueTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TStructType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("StructType");
+ {
+ consumer.OnBeginList();
+ for (auto& item : t->GetMembers()) {
+ consumer.OnBeginList();
+ consumer.OnScalarString(item.GetName());
+ AsYqlType(item.GetTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ }
+ consumer.OnEndList();
+ }
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TTupleType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("TupleType");
+ {
+ consumer.OnBeginList();
+ for (auto& item : t->GetElements()) {
+ AsYqlType(item.GetTypeRaw(), consumer, includeTags);
+ }
+ consumer.OnEndList();
+ }
+
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TVariantType* t) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("VariantType");
+ AsYqlType(t->GetUnderlyingTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ },
+ [&consumer, includeTags](const TTaggedType* t) {
+ if (includeTags) {
+ consumer.OnBeginList();
+ consumer.OnScalarString("TaggedType");
+ consumer.OnScalarString(t->GetTag());
+ AsYqlType(t->GetItemTypeRaw(), consumer, includeTags);
+ consumer.OnEndList();
+ } else {
+ AsYqlType(t->GetItemTypeRaw(), consumer, includeTags);
+ }
+ },
+ });
+ }
+
+ TString AsYqlType(const NTi::TType* type, bool includeTags) {
+ auto result = TString();
+ auto writer = NYsonPull::MakePrettyTextWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::Node);
+ writer.BeginStream();
+ AsYqlType(type, writer.GetConsumer(), includeTags);
+ writer.EndStream();
+ return result;
+ }
+
+ void AsYqlRowSpec(const TType* maybeTagged, NYsonPull::IConsumer& consumer, bool includeTags) {
+ auto* type = maybeTagged->StripTagsRaw();
+
+ if (!type->IsStruct()) {
+ ythrow TApiException() << "AsYqlRowSpec expected a struct type but got " << type->GetTypeName();
+ }
+
+ consumer.OnBeginMap();
+ consumer.OnKey("StrictSchema");
+ consumer.OnScalarBoolean(true);
+ consumer.OnKey("Type");
+ AsYqlType(type, consumer, includeTags);
+ consumer.OnEndMap();
+ }
+
+ TString AsYqlRowSpec(const NTi::TType* type, bool includeTags) {
+ auto result = TString();
+ auto writer = NYsonPull::MakePrettyTextWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::Node);
+ writer.BeginStream();
+ AsYqlRowSpec(type, writer.GetConsumer(), includeTags);
+ writer.EndStream();
+ return result;
+ }
+
+ void AsYtSchema(const TType* maybeTagged, NYsonPull::IConsumer& consumer, bool failOnEmptyStruct) {
+ auto* type = maybeTagged->StripTagsRaw();
+
+ if (!type->IsStruct()) {
+ ythrow TApiException() << "AsYtSchema expected a struct type but got " << type->GetTypeName();
+ }
+
+ auto* structType = type->AsStructRaw();
+
+ if (structType->GetMembers().empty()) {
+ if (failOnEmptyStruct) {
+ ythrow TApiException() << "AsYtSchema expected a non-empty struct";
+ }
+
+ AsYtSchema(Struct({{"_yql_fake_column", Optional(Bool())}}).Get(), consumer);
+ return;
+ }
+
+ consumer.OnBeginAttributes();
+
+ consumer.OnKey("strict");
+ consumer.OnScalarBoolean(true);
+
+ consumer.OnKey("unique_keys");
+ consumer.OnScalarBoolean(false);
+
+ consumer.OnEndAttributes();
+
+ consumer.OnBeginList();
+ for (auto& item : structType->GetMembers()) {
+ auto* itemType = item.GetTypeRaw()->StripTagsRaw();
+
+ bool required = true;
+
+ if (itemType->IsOptional()) {
+ // toplevel optionals make non-required columns
+ itemType = itemType->AsOptionalRaw()->GetItemTypeRaw();
+ required = false;
+ }
+
+ TStringBuf typeString = itemType->VisitRaw(TOverloaded{
+ [](const TVoidType*) -> TStringBuf { return "any"; },
+ [](const TNullType*) -> TStringBuf { return "any"; },
+ [](const TBoolType*) -> TStringBuf { return "boolean"; },
+ [](const TInt8Type*) -> TStringBuf { return "int8"; },
+ [](const TInt16Type*) -> TStringBuf { return "int16"; },
+ [](const TInt32Type*) -> TStringBuf { return "int32"; },
+ [](const TInt64Type*) -> TStringBuf { return "int64"; },
+ [](const TUint8Type*) -> TStringBuf { return "uint8"; },
+ [](const TUint16Type*) -> TStringBuf { return "uint16"; },
+ [](const TUint32Type*) -> TStringBuf { return "uint32"; },
+ [](const TUint64Type*) -> TStringBuf { return "uint64"; },
+ [](const TFloatType*) -> TStringBuf { return "double"; },
+ [](const TDoubleType*) -> TStringBuf { return "double"; },
+ [](const TStringType*) -> TStringBuf { return "string"; },
+ [](const TUtf8Type*) -> TStringBuf { return "utf8"; },
+ [](const TDateType*) -> TStringBuf { return "uint16"; },
+ [](const TDatetimeType*) -> TStringBuf { return "uint32"; },
+ [](const TTimestampType*) -> TStringBuf { return "uint64"; },
+ [](const TTzDateType*) -> TStringBuf { return "string"; },
+ [](const TTzDatetimeType*) -> TStringBuf { return "string"; },
+ [](const TTzTimestampType*) -> TStringBuf { return "string"; },
+ [](const TIntervalType*) -> TStringBuf { return "int64"; },
+ [](const TJsonType*) -> TStringBuf { return "string"; },
+ [](const TYsonType*) -> TStringBuf { return "any"; },
+ [](const TUuidType*) -> TStringBuf { return "string"; },
+ [](const TDecimalType*) -> TStringBuf { return "string"; },
+ [](const TOptionalType*) -> TStringBuf { return "any"; },
+ [](const TListType*) -> TStringBuf { return "any"; },
+ [](const TDictType*) -> TStringBuf { return "any"; },
+ [](const TStructType*) -> TStringBuf { return "any"; },
+ [](const TTupleType*) -> TStringBuf { return "any"; },
+ [](const TVariantType*) -> TStringBuf { return "any"; },
+ [](const TTaggedType*) -> TStringBuf { return "any"; },
+ });
+
+ if (typeString == "any") {
+ // columns of type `any` cannot be required
+ required = false;
+ }
+
+ {
+ consumer.OnBeginMap();
+
+ consumer.OnKey("name");
+ consumer.OnScalarString(item.GetName());
+
+ consumer.OnKey("required");
+ consumer.OnScalarBoolean(required);
+
+ consumer.OnKey("type");
+ consumer.OnScalarString(typeString);
+
+ consumer.OnEndMap();
+ }
+ }
+ consumer.OnEndList();
+ }
+
+ TString AsYtSchema(const NTi::TType* type, bool failOnEmptyStruct) {
+ auto result = TString();
+ auto writer = NYsonPull::MakePrettyTextWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::Node);
+ writer.BeginStream();
+ AsYtSchema(type, writer.GetConsumer(), failOnEmptyStruct);
+ writer.EndStream();
+ return result;
+ }
+}
diff --git a/library/cpp/type_info/type_io.h b/library/cpp/type_info/type_io.h
new file mode 100644
index 0000000000..e400414cff
--- /dev/null
+++ b/library/cpp/type_info/type_io.h
@@ -0,0 +1,115 @@
+#pragma once
+
+//! @file type_io.h
+//!
+//! Utilities for serializing and deserializing type instances.
+
+#include "type.h"
+
+#include <library/cpp/yson_pull/yson.h>
+
+namespace NTi::NIo {
+ /// Load type from a serialized representation.
+ ///
+ /// Serialization uses YSON (either binary or text). Contents are described in the [docs page].
+ ///
+ /// Throws `TTypeDeserializationException` if input is not valid.
+ ///
+ /// [docs page]: https://a.yandex-team.ru/arc/trunk/arcadia/library/cpp/type_info/docs/types_serialization.md
+ ///
+ /// @param factory factory that will be used to allocate new type. Technically, type will be deserialized into
+ /// a temporary pool factory and then adopted into a given one.
+ /// @param reader yson pull reader that'll be used to read types.
+ /// @param deduplicate use deduplication while creating new types. See `NTi::PoolFactory` function for more info.
+ /// @{
+ TTypePtr DeserializeYson(ITypeFactory& factory, NYsonPull::TReader& reader, bool deduplicate = true);
+ TTypePtr DeserializeYson(ITypeFactory& factory, TStringBuf data, bool deduplicate = true);
+ TTypePtr DeserializeYson(ITypeFactory& factory, IInputStream& input, bool deduplicate = true);
+ /// @}
+
+ /// Like `Deserialize`, but returns a raw pointer.
+ /// @{
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, NYsonPull::TReader& reader);
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, TStringBuf data);
+ const TType* DeserializeYsonRaw(IPoolTypeFactory& factory, IInputStream& input);
+ /// @}
+
+ /// Like `Deserialize`, but allows deserializing multiple types from the same reader.
+ ///
+ /// This function takes a reader created with `NYsonPull::EStreamType::ListFragment` mode. It reads a type,
+ /// but doesn't fails if there is no `BeginStream` event. If the reader is empty, it returns nullptr.
+ ///
+ /// This function mirrors `SerializeMultiple`. Call it multiple times on the same reader to read multiple types.
+ const TType* DeserializeYsonMultipleRaw(IPoolTypeFactory& factory, NYsonPull::TReader& reader);
+
+ /// Serialize this type info.
+ ///
+ /// Serialization uses YSON (either binary or text). Contents are described in the [RFC].
+ ///
+ /// [RFC]: https://a.yandex-team.ru/arc/trunk/arcadia/logfeller/mvp/docs/types_serialization.md
+ ///
+ /// @param humanReadable use pretty textual format instead of a binary one.
+ /// @param includeTags when disabled, tagged types will be removed from the result, only tagged type contents
+ /// will be dumped. For example, `Tagged<'Url', String>` will be rendered as just `String`.
+ /// This is useful if you only care about physical layout of a type and don't want to export
+ /// any semantical meaning.
+ /// @{
+ void SerializeYson(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags = true);
+ void SerializeYson(const TType* type, IOutputStream& stream, bool humanReadable = false, bool includeTags = true);
+ TString SerializeYson(const TType* type, bool humanReadable = false, bool includeTags = true);
+ /// @}
+
+ /// Like `Serialize`, but allows serializing multiple types into the same consumer.
+ ///
+ /// This function takes a consumer created with `NYsonPull::EStreamType::ListFragment` mode. It writes type,
+ /// but doesn't emit the `BeginStream` and `EndStream` commands.
+ ///
+ /// Call this function multiple times on the same consumer to write multiple types. Note that you must emit
+ /// the `BeginStream` and `EndStream` commands to the consumer yourself.
+ void SerializeYsonMultiple(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags = true);
+
+ /// Convert type to the lisp-like representation used in YQL row specs.
+ ///
+ /// TODO: move this code to yql/
+ ///
+ /// @param includeTags when disabled, tagged types will be removed from the result, only tagged type contents
+ /// will be dumped. For example, `Tagged<'Url', String>` will be rendered as just `String`.
+ /// This is useful if you only care about physical layout of a type and don't want to export
+ /// any semantic meaning.
+ /// @{
+ void AsYqlType(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags = true);
+ TString AsYqlType(const TType* type, bool includeTags = true);
+ /// @}
+
+ /// Generate a strict YQL row spec. Toplevel tags will be ignored.
+ ///
+ /// Throws `TApiException` if the type is not a (possibly tagged) struct.
+ ///
+ /// TODO: move this code to yql/
+ ///
+ /// @param type type that'll be converted to YQL row spec.
+ /// @param consumer yson pull consumer. Attention: `OnBeginStream` and `OnEndStream` should be emitted manually
+ /// before and after calling this function.
+ /// @param includeTags same as in `TType::AsYqlType`.
+ /// @{
+ void AsYqlRowSpec(const TType* type, NYsonPull::IConsumer& consumer, bool includeTags = true);
+ TString AsYqlRowSpec(const TType* type, bool includeTags = true);
+ /// @}
+
+ /// Generate a strict YT schema (only types are exported, no index/sorting information).
+ ///
+ /// Throws `TApiException` if the type is not a (possibly tagged) struct.
+ ///
+ /// The schema is generated according to the translation rules of YQL types, i.e. container types translate
+ /// to `Any`.
+ ///
+ /// TODO: move this code to mapreduce/yt/
+ ///
+ /// @param failOnEmptyStruct if true, will throw `TApiException` if called on a struct with no fields;
+ /// if false, will emit a strict YT schema with a single column `'_yql_fake_column'`
+ /// of type `Optional<Bool>` (this is how YQL handles empty tables).
+ /// @{
+ void AsYtSchema(const TType* type, NYsonPull::IConsumer& consumer, bool failOnEmptyStruct = true);
+ TString AsYtSchema(const TType* type, bool failOnEmptyStruct = true);
+ /// @}
+}
diff --git a/library/cpp/type_info/type_list.cpp b/library/cpp/type_info/type_list.cpp
new file mode 100644
index 0000000000..b741b2858b
--- /dev/null
+++ b/library/cpp/type_info/type_list.cpp
@@ -0,0 +1 @@
+#include "type_list.h"
diff --git a/library/cpp/type_info/type_list.h b/library/cpp/type_info/type_list.h
new file mode 100644
index 0000000000..c87c51918d
--- /dev/null
+++ b/library/cpp/type_info/type_list.h
@@ -0,0 +1,183 @@
+#pragma once
+
+//! @file type_list.h
+//!
+//! Enum with all type names that are included in the Common Type System.
+//!
+//!
+//! # Primitive and non-primitive types
+//!
+//! Some systems only work with primitive types, so we've split the enum in two: the first contains primitive types,
+//! and the second contains all types, including the primitive ones. This way systems that are only interested
+//! in primitives can stop handling containers and make all their switch-cases exhaustive.
+//!
+//! Consequently, the class hierarchy follows the same division: there is `NTi::TPrimitiveType`,
+//! from which all primitives are derived.
+//!
+//!
+//! # Enumerator values
+//!
+//! Enumerator values are implementation detail and should not be relied upon. In particular, use `NTi::ToTypeName`
+//! and `NTi::ToPrimitiveTypeName` to safely cast between `NTi::EPrimitiveTypeName` and `NTi::ETypeName`. Also, don't
+//! use enumerator values for serialization and deserialization — convert enumerator values to strings
+//! it you need persistence.
+
+#include <util/system/types.h>
+#include <util/generic/variant.h>
+
+namespace NTi {
+ /// Enum with names of all primitive types.
+ ///
+ /// See the file-level documentation.
+ enum class EPrimitiveTypeName : i32 {
+ Bool,
+
+ Int8,
+ Int16,
+ Int32,
+ Int64,
+ Uint8,
+ Uint16,
+ Uint32,
+ Uint64,
+
+ Float,
+ Double,
+
+ String,
+ Utf8,
+
+ Date,
+ Datetime,
+ Timestamp,
+ TzDate,
+ TzDatetime,
+ TzTimestamp,
+ Interval,
+
+ Decimal,
+ Json,
+ Yson,
+ Uuid,
+ };
+
+ /// Enum with names of all types, including primitives.
+ ///
+ /// See the file-level documentation.
+ enum class ETypeName : i32 {
+ //
+ // # Primitive types
+
+ Bool,
+
+ Int8,
+ Int16,
+ Int32,
+ Int64,
+ Uint8,
+ Uint16,
+ Uint32,
+ Uint64,
+
+ Float,
+ Double,
+
+ String,
+ Utf8,
+
+ Date,
+ Datetime,
+ Timestamp,
+ TzDate,
+ TzDatetime,
+ TzTimestamp,
+ Interval,
+
+ Decimal,
+ Json,
+ Yson,
+ Uuid,
+
+ FIRST_PRIMITIVE = Bool,
+ LAST_PRIMITIVE = Uuid,
+
+ //
+ // # Singular types
+
+ Void,
+ Null,
+
+ FIRST_SINGULAR = Void,
+ LAST_SINGULAR = Null,
+
+ //
+ // # Containers
+
+ Optional,
+ List,
+ Dict,
+ Struct,
+ Tuple,
+ Variant,
+ Tagged,
+
+ FIRST_CONTAINER = Optional,
+ LAST_CONTAINER = Tagged,
+ };
+
+ /// Return true if the given type is a primitive one.
+ ///
+ /// Primitive type is a type that have no type parameters and is not a singular one.
+ inline constexpr bool IsPrimitive(ETypeName typeName) {
+ return ETypeName::FIRST_PRIMITIVE <= typeName && typeName <= ETypeName::LAST_PRIMITIVE;
+ }
+
+ /// Return true if the given type is one of singular types.
+ ///
+ /// Singular type is a type that has only one instance and therefore carries no information,
+ /// i.e. occupy zero-length memory buffer.
+ inline constexpr bool IsSingular(ETypeName typeName) {
+ return ETypeName::FIRST_SINGULAR <= typeName && typeName <= ETypeName::LAST_SINGULAR;
+ }
+
+ /// Return true if the given type is one of containers.
+ ///
+ /// Container type is a type that has type parameters.
+ inline constexpr bool IsContainer(ETypeName typeName) {
+ return ETypeName::FIRST_CONTAINER <= typeName && typeName <= ETypeName::LAST_CONTAINER;
+ }
+
+ /// Return true if the given type has any type parameters.
+ inline constexpr bool HasTypeParameters(ETypeName typeName) {
+ return IsContainer(typeName);
+ }
+
+ /// Return true if the given type has any non-type parameters.
+ inline constexpr bool HasNonTypeParameters(ETypeName typeName) {
+ return typeName == ETypeName::Decimal;
+ }
+
+ /// Return true if the given type has any type or non-type parameters.
+ inline constexpr bool HasParameters(ETypeName typeName) {
+ return HasTypeParameters(typeName) || HasNonTypeParameters(typeName);
+ }
+
+ /// Safely cast `NTi::EPrimitiveTypeName` to `NTi::ETypeName`.
+ ///
+ /// Enumerator values should not relied upon, therefore users should not cast `NTi::EPrimitiveTypeName`
+ /// to `NTi::ETypeName` using `static_cast`.
+ inline constexpr ETypeName ToTypeName(EPrimitiveTypeName primitiveTypeName) {
+ // Note: there's a test in ut/type_list.cpp that checks this is a safe conversion
+ return static_cast<ETypeName>(primitiveTypeName);
+ }
+
+ /// Cast `NTi::ETypeName` to `NTi::EPrimitiveTypeName`, panic if the given type is not a primitive one.
+ ///
+ /// Enumerator values should not relied upon, therefore users should not cast `NTi::ETypeName`
+ /// to `NTi::EPrimitiveTypeName` using `static_cast`.
+ inline constexpr EPrimitiveTypeName ToPrimitiveTypeName(ETypeName typeName) {
+ Y_VERIFY(IsPrimitive(typeName));
+ // Note: there's a test in ut/type_list.cpp that checks this is a safe conversion
+ return static_cast<EPrimitiveTypeName>(typeName);
+ }
+}
diff --git a/library/cpp/type_info/ut/builder.cpp b/library/cpp/type_info/ut/builder.cpp
new file mode 100644
index 0000000000..43b7bb8c9b
--- /dev/null
+++ b/library/cpp/type_info/ut/builder.cpp
@@ -0,0 +1,125 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+class Builder: public NTesting::TTest {
+public:
+ void SetUp() override {
+ F = NTi::PoolFactory(false);
+ }
+
+ void TearDown() override {
+ F.Reset();
+ }
+
+ NTi::IPoolTypeFactoryPtr F;
+};
+
+TEST_F(Builder, TaggedBuilder) {
+ auto builder = NTi::TTaggedBuilderRaw(*F);
+
+ ASSERT_FALSE(builder.CanBuild());
+ ASSERT_FALSE(builder.HasTag());
+ ASSERT_FALSE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), Nothing());
+ UNIT_ASSERT_EQUAL(builder.GetItem(), Nothing());
+
+ {
+ auto tag = TString("Url");
+ builder.SetTag(tag);
+ }
+
+ ASSERT_FALSE(builder.CanBuild());
+ ASSERT_TRUE(builder.HasTag());
+ ASSERT_FALSE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), MakeMaybe<TStringBuf>("Url"));
+ UNIT_ASSERT_EQUAL(builder.GetItem(), Nothing());
+
+ {
+ auto ty = NTi::String();
+ builder.SetItem(ty);
+ }
+
+ ASSERT_TRUE(builder.CanBuild());
+ ASSERT_TRUE(builder.HasTag());
+ ASSERT_TRUE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), MakeMaybe<TStringBuf>("Url"));
+ UNIT_ASSERT_EQUAL(builder.GetItem(), MakeMaybe<const NTi::TType*>(NTi::TStringType::InstanceRaw()));
+
+ {
+ auto tagged = builder.Build();
+ ASSERT_STRICT_EQ(tagged, NTi::Tagged(NTi::String(), "Url"));
+ }
+
+ ASSERT_TRUE(builder.CanBuild());
+
+ builder.Reset();
+
+ ASSERT_FALSE(builder.CanBuild());
+ ASSERT_FALSE(builder.HasTag());
+ ASSERT_FALSE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), Nothing());
+ UNIT_ASSERT_EQUAL(builder.GetItem(), Nothing());
+
+ builder.SetTag("T");
+ builder.SetItem(NTi::String());
+
+ ASSERT_TRUE(builder.CanBuild());
+ ASSERT_TRUE(builder.HasTag());
+ ASSERT_TRUE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), MakeMaybe<TStringBuf>("T"));
+ UNIT_ASSERT_EQUAL(builder.GetItem(), MakeMaybe<const NTi::TType*>(NTi::TStringType::InstanceRaw()));
+
+ builder.DiscardItem();
+
+ ASSERT_FALSE(builder.CanBuild());
+ ASSERT_TRUE(builder.HasTag());
+ ASSERT_FALSE(builder.HasItem());
+
+ UNIT_ASSERT_EQUAL(builder.GetTag(), MakeMaybe<TStringBuf>("T"));
+ UNIT_ASSERT_EQUAL(builder.GetItem(), Nothing());
+
+ builder.DiscardTag();
+
+ builder.SetTag("T");
+ builder.SetItem(NTi::String());
+
+ ASSERT_TRUE(builder.CanBuild());
+ ASSERT_TRUE(builder.HasTag());
+ ASSERT_TRUE(builder.HasItem());
+}
+
+TEST_F(Builder, TaggedBuilderChaining) {
+ auto builder = NTi::TTaggedBuilderRaw(*F)
+ .SetTag("Uuu")
+ .SetItem(NTi::Optional(NTi::String()));
+
+ ASSERT_STRICT_EQ(builder.Build(), NTi::Tagged(NTi::Optional(NTi::String()), "Uuu"));
+
+ builder = std::move(builder)
+ .DiscardTag()
+ .SetTag("Urls");
+
+ ASSERT_STRICT_EQ(builder.Build(), NTi::Tagged(NTi::Optional(NTi::String()), "Urls"));
+
+ builder = std::move(builder)
+ .DiscardItem()
+ .SetItem(F->ListRaw(F->StringRaw()));
+
+ ASSERT_STRICT_EQ(builder.Build(), NTi::Tagged(NTi::List(NTi::String()), "Urls"));
+
+ auto type = std::move(builder)
+ .Reset()
+ .SetTag("Url")
+ .SetItem(F->StringRaw())
+ .Build();
+
+ ASSERT_STRICT_EQ(type, NTi::Tagged(NTi::String(), "Url"));
+}
diff --git a/library/cpp/type_info/ut/test_data.cpp b/library/cpp/type_info/ut/test_data.cpp
new file mode 100644
index 0000000000..36944e7bc4
--- /dev/null
+++ b/library/cpp/type_info/ut/test_data.cpp
@@ -0,0 +1,91 @@
+#include <library/cpp/testing/unittest/gtest.h>
+#include <library/cpp/resource/resource.h>
+
+#include <library/cpp/type_info/type_info.h>
+#include <library/cpp/type_info/type_io.h>
+
+#include <util/string/strip.h>
+#include <util/string/split.h>
+
+using namespace NTi;
+
+std::vector<std::vector<TString>> ParseData(TStringBuf data, int expectedFieldsCount) {
+ TString noComments;
+ {
+ TMemoryInput in(data);
+ TString line;
+ while (in.ReadLine(line)) {
+ if (StripString(line).StartsWith('#')) {
+ continue;
+ }
+ noComments += line;
+ }
+ }
+
+ std::vector<std::vector<TString>> result;
+ for (TStringBuf record : StringSplitter(noComments).SplitByString(";;")) {
+ record = StripString(record);
+ if (record.Empty()) {
+ continue;
+ }
+ std::vector<TString> fields;
+ for (TStringBuf field : StringSplitter(record).SplitByString("::")) {
+ fields.emplace_back(StripString(field));
+ }
+ if (static_cast<int>(fields.size()) != expectedFieldsCount) {
+ ythrow yexception() << "Unexpected field count expected: " << expectedFieldsCount << " actual: " << fields.size();
+ }
+ result.push_back(fields);
+ }
+ return result;
+}
+
+TEST(TestData, GoodTypes) {
+ auto records = ParseData(NResource::Find("/good"), 2);
+
+ for (const auto& record : records) {
+ const auto& typeYson = record.at(0);
+ const auto& typeText = record.at(1);
+ TString context = TStringBuilder()
+ << "text: " << typeText << Endl
+ << "yson: " << typeYson << Endl;
+ auto wrapError = [&] (const std::exception& ex) {
+ return yexception() << "Unexpected error: " << ex.what() << '\n' << context;
+ };
+
+ TTypePtr type;
+ try {
+ type = NIo::DeserializeYson(*HeapFactory(), typeYson);
+ } catch (const std::exception& ex) {
+ ythrow wrapError(ex);
+ }
+ UNIT_ASSERT_VALUES_EQUAL_C(ToString(*type), typeText, context);
+
+ TTypePtr type2;
+ try {
+ auto yson2 = NIo::SerializeYson(type.Get(), true);
+ type2 = NIo::DeserializeYson(*HeapFactory(), yson2);
+ } catch (const std::exception& ex) {
+ ythrow wrapError(ex);
+ }
+ UNIT_ASSERT_VALUES_EQUAL_C(*type, *type2, context);
+ }
+}
+
+TEST(TestData, BadTypes) {
+ auto records = ParseData(NResource::Find("/bad"), 3);
+
+ for (const auto& record : records) {
+ const auto& typeYson = record.at(0);
+ const auto& exceptionMessage = record.at(1);
+
+ TString context = TStringBuilder()
+ << "exception: " << exceptionMessage << Endl
+ << "yson: " << typeYson << Endl;
+ UNIT_ASSERT_EXCEPTION_CONTAINS_C(
+ NIo::DeserializeYson(*HeapFactory(), typeYson),
+ yexception,
+ exceptionMessage,
+ context);
+ }
+} \ No newline at end of file
diff --git a/library/cpp/type_info/ut/type_basics.cpp b/library/cpp/type_info/ut/type_basics.cpp
new file mode 100644
index 0000000000..83996d63d3
--- /dev/null
+++ b/library/cpp/type_info/ut/type_basics.cpp
@@ -0,0 +1,381 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeBasics, Void) {
+ auto t = f.Void();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Void);
+ ASSERT_TRUE(t->IsVoid());
+}
+
+TEST_TF(TypeBasics, Bool) {
+ auto t = f.Bool();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Bool);
+ ASSERT_TRUE(t->IsBool());
+}
+
+TEST_TF(TypeBasics, Int8) {
+ auto t = f.Int8();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int8);
+ ASSERT_TRUE(t->IsInt8());
+}
+
+TEST_TF(TypeBasics, Int16) {
+ auto t = f.Int16();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int16);
+ ASSERT_TRUE(t->IsInt16());
+}
+
+TEST_TF(TypeBasics, Int32) {
+ auto t = f.Int32();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int32);
+ ASSERT_TRUE(t->IsInt32());
+}
+
+TEST_TF(TypeBasics, Int64) {
+ auto t = f.Int64();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int64);
+ ASSERT_TRUE(t->IsInt64());
+}
+
+TEST_TF(TypeBasics, Uint8) {
+ auto t = f.Uint8();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint8);
+ ASSERT_TRUE(t->IsUint8());
+}
+
+TEST_TF(TypeBasics, Uint16) {
+ auto t = f.Uint16();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint16);
+ ASSERT_TRUE(t->IsUint16());
+}
+
+TEST_TF(TypeBasics, Uint32) {
+ auto t = f.Uint32();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint32);
+ ASSERT_TRUE(t->IsUint32());
+}
+
+TEST_TF(TypeBasics, Uint64) {
+ auto t = f.Uint64();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint64);
+ ASSERT_TRUE(t->IsUint64());
+}
+
+TEST_TF(TypeBasics, Float) {
+ auto t = f.Float();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Float);
+ ASSERT_TRUE(t->IsFloat());
+}
+
+TEST_TF(TypeBasics, Double) {
+ auto t = f.Double();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Double);
+ ASSERT_TRUE(t->IsDouble());
+}
+
+TEST_TF(TypeBasics, String) {
+ auto t = f.String();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::String);
+ ASSERT_TRUE(t->IsString());
+}
+
+TEST_TF(TypeBasics, Utf8) {
+ auto t = f.Utf8();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Utf8);
+ ASSERT_TRUE(t->IsUtf8());
+}
+
+TEST_TF(TypeBasics, Date) {
+ auto t = f.Date();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Date);
+ ASSERT_TRUE(t->IsDate());
+}
+
+TEST_TF(TypeBasics, Datetime) {
+ auto t = f.Datetime();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Datetime);
+ ASSERT_TRUE(t->IsDatetime());
+}
+
+TEST_TF(TypeBasics, Timestamp) {
+ auto t = f.Timestamp();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Timestamp);
+ ASSERT_TRUE(t->IsTimestamp());
+}
+
+TEST_TF(TypeBasics, TzDate) {
+ auto t = f.TzDate();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzDate);
+ ASSERT_TRUE(t->IsTzDate());
+}
+
+TEST_TF(TypeBasics, TzDatetime) {
+ auto t = f.TzDatetime();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzDatetime);
+ ASSERT_TRUE(t->IsTzDatetime());
+}
+
+TEST_TF(TypeBasics, TzTimestamp) {
+ auto t = f.TzTimestamp();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzTimestamp);
+ ASSERT_TRUE(t->IsTzTimestamp());
+}
+
+TEST_TF(TypeBasics, Interval) {
+ auto t = f.Interval();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Interval);
+ ASSERT_TRUE(t->IsInterval());
+}
+
+TEST_TF(TypeBasics, Decimal) {
+ auto t = f.Decimal(20, 10);
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Decimal);
+ ASSERT_TRUE(t->IsDecimal());
+ ASSERT_EQ(t->GetPrecision(), 20);
+ ASSERT_EQ(t->GetScale(), 10);
+}
+
+TEST_TF(TypeBasics, Json) {
+ auto t = f.Json();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Json);
+ ASSERT_TRUE(t->IsJson());
+}
+
+TEST_TF(TypeBasics, Yson) {
+ auto t = f.Yson();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Yson);
+ ASSERT_TRUE(t->IsYson());
+}
+
+TEST_TF(TypeBasics, Uuid) {
+ auto t = f.Uuid();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uuid);
+ ASSERT_TRUE(t->IsUuid());
+}
+
+TEST_TF(TypeBasics, Optional) {
+ auto t = f.Optional(f.Void());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Optional);
+ ASSERT_TRUE(t->IsOptional());
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
+
+TEST_TF(TypeBasics, List) {
+ auto t = f.List(f.Void());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::List);
+ ASSERT_TRUE(t->IsList());
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
+
+TEST_TF(TypeBasics, Dict) {
+ auto t = f.Dict(f.Void(), f.String());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Dict);
+ ASSERT_TRUE(t->IsDict());
+ ASSERT_TRUE(t->GetKeyType()->IsVoid());
+ ASSERT_EQ(t->GetKeyType().Get(), t->GetKeyTypeRaw());
+ ASSERT_TRUE(t->GetValueType()->IsString());
+ ASSERT_EQ(t->GetValueType().Get(), t->GetValueTypeRaw());
+}
+
+TEST_TF(TypeBasics, EmptyStruct) {
+ auto t = f.Struct({});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetMembers().size(), 0);
+}
+
+TEST_TF(TypeBasics, NamedEmptyStruct) {
+ auto t = f.Struct("S", {});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "S");
+ ASSERT_EQ(t->GetMembers().size(), 0);
+}
+
+TEST_TF(TypeBasics, Struct) {
+ auto t = f.Struct({{"a", f.Int64()}, {"b", f.Int8()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetMembers().size(), 2);
+ ASSERT_EQ(t->GetMembers()[0].GetName(), "a");
+ ASSERT_TRUE(t->GetMembers()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetMembers()[0].GetType().Get(), t->GetMembers()[0].GetTypeRaw());
+ ASSERT_EQ(t->GetMembers()[1].GetName(), "b");
+ ASSERT_TRUE(t->GetMembers()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetMembers()[1].GetType().Get(), t->GetMembers()[1].GetTypeRaw());
+}
+
+TEST_TF(TypeBasics, NamedStruct) {
+ auto t = f.Struct("S", {{"a", f.Int64()}, {"b", f.Int8()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "S");
+ ASSERT_EQ(t->GetMembers().size(), 2);
+ ASSERT_EQ(t->GetMembers()[0].GetName(), "a");
+ ASSERT_TRUE(t->GetMembers()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetMembers()[0].GetType().Get(), t->GetMembers()[0].GetTypeRaw());
+ ASSERT_EQ(t->GetMembers()[1].GetName(), "b");
+ ASSERT_TRUE(t->GetMembers()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetMembers()[1].GetType().Get(), t->GetMembers()[1].GetTypeRaw());
+}
+
+TEST_TF(TypeBasics, EmptyTuple) {
+ auto t = f.Tuple({});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetElements().size(), 0);
+}
+
+TEST_TF(TypeBasics, NamedEmptyTuple) {
+ auto t = f.Tuple("T", {});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "T");
+ ASSERT_EQ(t->GetElements().size(), 0);
+}
+
+TEST_TF(TypeBasics, Tuple) {
+ auto t = f.Tuple({{f.Int64()}, {f.Int8()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetElements().size(), 2);
+ ASSERT_TRUE(t->GetElements()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetElements()[0].GetType().Get(), t->GetElements()[0].GetTypeRaw());
+ ASSERT_TRUE(t->GetElements()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetElements()[1].GetType().Get(), t->GetElements()[1].GetTypeRaw());
+}
+
+TEST_TF(TypeBasics, NamedTuple) {
+ auto t = f.Tuple("T", {{f.Int64()}, {f.Int8()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "T");
+ ASSERT_EQ(t->GetElements().size(), 2);
+ ASSERT_TRUE(t->GetElements()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetElements()[0].GetType().Get(), t->GetElements()[0].GetTypeRaw());
+ ASSERT_TRUE(t->GetElements()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetElements()[1].GetType().Get(), t->GetElements()[1].GetTypeRaw());
+}
+
+TEST_TF(TypeBasics, VariantOverStruct) {
+ auto t = f.Variant(f.Struct("Inner", {{"x", f.Void()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_TRUE(t->IsVariantOverStruct());
+ ASSERT_FALSE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsStruct()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST_TF(TypeBasics, NamedVariantOverStruct) {
+ auto t = f.Variant("V", f.Struct("Inner", {{"x", f.Void()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "V");
+ ASSERT_TRUE(t->IsVariantOverStruct());
+ ASSERT_FALSE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsStruct()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST_TF(TypeBasics, VariantOverTuple) {
+ auto t = f.Variant(f.Tuple("Inner", {{f.Void()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_FALSE(t->IsVariantOverStruct());
+ ASSERT_TRUE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsTuple()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST_TF(TypeBasics, NamedVariantOverTuple) {
+ auto t = f.Variant("V", f.Tuple("Inner", {{f.Void()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "V");
+ ASSERT_FALSE(t->IsVariantOverStruct());
+ ASSERT_TRUE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsTuple()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST_TF(TypeBasics, Tagged) {
+ auto t = f.Tagged(f.Void(), "T");
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tagged);
+ ASSERT_TRUE(t->IsTagged());
+ ASSERT_EQ(t->GetTag(), "T");
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
+
+TEST_TF(TypeBasics, EmptyStructLookupByName) {
+ auto s = f.Struct({});
+
+ ASSERT_FALSE(s->HasMember("item"));
+ ASSERT_EQ(s->GetMemberIndex("item"), -1);
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ [=]() {
+ s->GetMember("item");
+ }(),
+ NTi::TItemNotFound, "no item named 'item'");
+}
+
+TEST_TF(TypeBasics, StructLookupByName) {
+ auto s = f.Struct({
+ {"a", f.Void()},
+ {"field2", f.String()},
+ {"field1", f.Utf8()},
+ {"", f.Null()},
+ });
+
+ ASSERT_TRUE(s->HasMember("a"));
+ ASSERT_EQ(s->GetMemberIndex("a"), 0);
+
+ ASSERT_TRUE(s->HasMember("field2"));
+ ASSERT_EQ(s->GetMemberIndex("field2"), 1);
+
+ ASSERT_TRUE(s->HasMember("field1"));
+ ASSERT_EQ(s->GetMemberIndex("field1"), 2);
+
+ ASSERT_TRUE(s->HasMember(""));
+ ASSERT_EQ(s->GetMemberIndex(""), 3);
+
+ ASSERT_FALSE(s->HasMember("b"));
+ ASSERT_EQ(s->GetMemberIndex("b"), -1);
+
+ ASSERT_EQ(s->GetMember("a").GetName(), "a");
+ ASSERT_TRUE(s->GetMember("a").GetTypeRaw()->IsVoid());
+
+ ASSERT_EQ(s->GetMember("field2").GetName(), "field2");
+ ASSERT_TRUE(s->GetMember("field2").GetTypeRaw()->IsString());
+
+ ASSERT_EQ(s->GetMember("field1").GetName(), "field1");
+ ASSERT_TRUE(s->GetMember("field1").GetTypeRaw()->IsUtf8());
+
+ ASSERT_EQ(s->GetMember("").GetName(), "");
+ ASSERT_TRUE(s->GetMember("").GetTypeRaw()->IsNull());
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ [=]() {
+ s->GetMember("b");
+ }(),
+ NTi::TItemNotFound, "no item named 'b'");
+}
diff --git a/library/cpp/type_info/ut/type_complexity_ut.cpp b/library/cpp/type_info/ut/type_complexity_ut.cpp
new file mode 100644
index 0000000000..3b4f6e5372
--- /dev/null
+++ b/library/cpp/type_info/ut/type_complexity_ut.cpp
@@ -0,0 +1,33 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_complexity.h>
+#include <library/cpp/type_info/type_constructors.h>
+
+using namespace NTi;
+
+TEST(TypeComplexity, Test)
+{
+ EXPECT_EQ(ComputeTypeComplexity(Int64()), 1);
+ EXPECT_EQ(ComputeTypeComplexity(String()), 1);
+ EXPECT_EQ(ComputeTypeComplexity(Null()), 1);
+ EXPECT_EQ(ComputeTypeComplexity(Decimal(4, 2)), 1);
+
+ EXPECT_EQ(ComputeTypeComplexity(Optional(Utf8())), 2);
+ EXPECT_EQ(ComputeTypeComplexity(List(Json())), 2);
+ EXPECT_EQ(ComputeTypeComplexity(Tagged(String(), "jpeg")), 2);
+ EXPECT_EQ(ComputeTypeComplexity(Dict(String(), Optional(Int64()))), 4);
+ EXPECT_EQ(ComputeTypeComplexity(Struct({
+ {"a", String()},
+ {"b", List(Optional(Int64()))},
+ })), 5);
+ EXPECT_EQ(ComputeTypeComplexity(Tuple({{Float()}, {Float()}})), 3);
+
+ EXPECT_EQ(ComputeTypeComplexity(Variant(Struct({
+ {"a", String()},
+ {"b", Int64()},
+ }))), 3);
+ EXPECT_EQ(ComputeTypeComplexity(Tuple({
+ {String()},
+ {Int64()},
+ })), 3);
+} \ No newline at end of file
diff --git a/library/cpp/type_info/ut/type_constraints.cpp b/library/cpp/type_info/ut/type_constraints.cpp
new file mode 100644
index 0000000000..0f56bd89b0
--- /dev/null
+++ b/library/cpp/type_info/ut/type_constraints.cpp
@@ -0,0 +1,53 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeConstraints, DecimalScale) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Decimal(20, 21);
+ }(),
+ NTi::TIllegalTypeException, "decimal scale 21 should be no greater than decimal precision 20");
+}
+
+TEST_TF(TypeConstraints, StructDuplicateItem) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Struct({{"a", f.Void()}, {"a", f.String()}});
+ }(),
+ NTi::TIllegalTypeException, "duplicate struct item 'a'");
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Struct({{"a", f.Void()}, {"b", f.Bool()}, {"a", f.String()}});
+ }(),
+ NTi::TIllegalTypeException, "duplicate struct item 'a'");
+}
+
+TEST_TF(TypeConstraints, StructEmpty) {
+ f.Struct({}); // empty structs are ok, this should not fail
+}
+
+TEST_TF(TypeConstraints, TupleEmpty) {
+ f.Tuple({}); // empty tuples are ok, this should not fail
+}
+
+TEST_TF(TypeConstraints, VariantStructEmpty) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Variant(f.Struct({}));
+ }(),
+ NTi::TIllegalTypeException, "variant should contain at least one alternative");
+}
+
+TEST_TF(TypeConstraints, VariantTupleEmpty) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Variant(f.Tuple({}));
+ }(),
+ NTi::TIllegalTypeException, "variant should contain at least one alternative");
+}
+
+TEST_TF(TypeConstraints, VariantWrongInnerType) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ f.Variant(f.String());
+ }(),
+ NTi::TIllegalTypeException, "variants can only contain structs and tuples, got String instead");
+}
diff --git a/library/cpp/type_info/ut/type_deserialize.cpp b/library/cpp/type_info/ut/type_deserialize.cpp
new file mode 100644
index 0000000000..9e93a26bee
--- /dev/null
+++ b/library/cpp/type_info/ut/type_deserialize.cpp
@@ -0,0 +1,528 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include <library/cpp/yson_pull/yson.h>
+
+#include "utils.h"
+
+TEST(TypeDeserialize, Void) {
+ ASSERT_DESERIALIZED_EQ(NTi::Void(), R"(void)");
+ ASSERT_DESERIALIZED_EQ(NTi::Void(), R"({type_name=void})");
+}
+
+TEST(TypeDeserialize, Null) {
+ ASSERT_DESERIALIZED_EQ(NTi::Null(), R"(null)");
+ ASSERT_DESERIALIZED_EQ(NTi::Null(), R"({type_name=null})");
+}
+
+TEST(TypeDeserialize, Bool) {
+ ASSERT_DESERIALIZED_EQ(NTi::Bool(), R"(bool)");
+ ASSERT_DESERIALIZED_EQ(NTi::Bool(), R"({type_name=bool})");
+}
+
+TEST(TypeDeserialize, Int8) {
+ ASSERT_DESERIALIZED_EQ(NTi::Int8(), R"(int8)");
+ ASSERT_DESERIALIZED_EQ(NTi::Int8(), R"({type_name=int8})");
+}
+
+TEST(TypeDeserialize, Int16) {
+ ASSERT_DESERIALIZED_EQ(NTi::Int16(), R"(int16)");
+ ASSERT_DESERIALIZED_EQ(NTi::Int16(), R"({type_name=int16})");
+}
+
+TEST(TypeDeserialize, Int32) {
+ ASSERT_DESERIALIZED_EQ(NTi::Int32(), R"(int32)");
+ ASSERT_DESERIALIZED_EQ(NTi::Int32(), R"({type_name=int32})");
+}
+
+TEST(TypeDeserialize, Int64) {
+ ASSERT_DESERIALIZED_EQ(NTi::Int64(), R"(int64)");
+ ASSERT_DESERIALIZED_EQ(NTi::Int64(), R"({type_name=int64})");
+}
+
+TEST(TypeDeserialize, Uint8) {
+ ASSERT_DESERIALIZED_EQ(NTi::Uint8(), R"(uint8)");
+ ASSERT_DESERIALIZED_EQ(NTi::Uint8(), R"({type_name=uint8})");
+}
+
+TEST(TypeDeserialize, Uint16) {
+ ASSERT_DESERIALIZED_EQ(NTi::Uint16(), R"(uint16)");
+ ASSERT_DESERIALIZED_EQ(NTi::Uint16(), R"({type_name=uint16})");
+}
+
+TEST(TypeDeserialize, Uint32) {
+ ASSERT_DESERIALIZED_EQ(NTi::Uint32(), R"(uint32)");
+ ASSERT_DESERIALIZED_EQ(NTi::Uint32(), R"({type_name=uint32})");
+}
+
+TEST(TypeDeserialize, Uint64) {
+ ASSERT_DESERIALIZED_EQ(NTi::Uint64(), R"(uint64)");
+ ASSERT_DESERIALIZED_EQ(NTi::Uint64(), R"({type_name=uint64})");
+}
+
+TEST(TypeDeserialize, Float) {
+ ASSERT_DESERIALIZED_EQ(NTi::Float(), R"(float)");
+ ASSERT_DESERIALIZED_EQ(NTi::Float(), R"({type_name=float})");
+}
+
+TEST(TypeDeserialize, Double) {
+ ASSERT_DESERIALIZED_EQ(NTi::Double(), R"(double)");
+ ASSERT_DESERIALIZED_EQ(NTi::Double(), R"({type_name=double})");
+}
+
+TEST(TypeDeserialize, String) {
+ ASSERT_DESERIALIZED_EQ(NTi::String(), R"(string)");
+ ASSERT_DESERIALIZED_EQ(NTi::String(), R"({type_name=string})");
+}
+
+TEST(TypeDeserialize, Utf8) {
+ ASSERT_DESERIALIZED_EQ(NTi::Utf8(), R"(utf8)");
+ ASSERT_DESERIALIZED_EQ(NTi::Utf8(), R"({type_name=utf8})");
+}
+
+TEST(TypeDeserialize, Date) {
+ ASSERT_DESERIALIZED_EQ(NTi::Date(), R"(date)");
+ ASSERT_DESERIALIZED_EQ(NTi::Date(), R"({type_name=date})");
+}
+
+TEST(TypeDeserialize, Datetime) {
+ ASSERT_DESERIALIZED_EQ(NTi::Datetime(), R"(datetime)");
+ ASSERT_DESERIALIZED_EQ(NTi::Datetime(), R"({type_name=datetime})");
+}
+
+TEST(TypeDeserialize, Timestamp) {
+ ASSERT_DESERIALIZED_EQ(NTi::Timestamp(), R"(timestamp)");
+ ASSERT_DESERIALIZED_EQ(NTi::Timestamp(), R"({type_name=timestamp})");
+}
+
+TEST(TypeDeserialize, TzDate) {
+ ASSERT_DESERIALIZED_EQ(NTi::TzDate(), R"(tz_date)");
+ ASSERT_DESERIALIZED_EQ(NTi::TzDate(), R"({type_name=tz_date})");
+}
+
+TEST(TypeDeserialize, TzDatetime) {
+ ASSERT_DESERIALIZED_EQ(NTi::TzDatetime(), R"(tz_datetime)");
+ ASSERT_DESERIALIZED_EQ(NTi::TzDatetime(), R"({type_name=tz_datetime})");
+}
+
+TEST(TypeDeserialize, TzTimestamp) {
+ ASSERT_DESERIALIZED_EQ(NTi::TzTimestamp(), R"(tz_timestamp)");
+ ASSERT_DESERIALIZED_EQ(NTi::TzTimestamp(), R"({type_name=tz_timestamp})");
+}
+
+TEST(TypeDeserialize, Interval) {
+ ASSERT_DESERIALIZED_EQ(NTi::Interval(), R"(interval)");
+ ASSERT_DESERIALIZED_EQ(NTi::Interval(), R"({type_name=interval})");
+}
+
+TEST(TypeDeserialize, Decimal) {
+ ASSERT_DESERIALIZED_EQ(NTi::Decimal(20, 10), R"({type_name=decimal; precision=20; scale=10})");
+ ASSERT_DESERIALIZED_EQ(NTi::Decimal(20, 10), R"({scale=10; type_name=decimal; precision=20})");
+ ASSERT_DESERIALIZED_EQ(NTi::Decimal(10, 10), R"({type_name=decimal; precision=10; scale=10})");
+}
+
+TEST(TypeDeserialize, DecimalMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({precision=20; scale=10})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=decimal; scale=10})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "precision")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=decimal; precision=20})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "scale")");
+}
+
+TEST(TypeDeserialize, Json) {
+ ASSERT_DESERIALIZED_EQ(NTi::Json(), R"(json)");
+ ASSERT_DESERIALIZED_EQ(NTi::Json(), R"({type_name=json})");
+}
+
+TEST(TypeDeserialize, Yson) {
+ ASSERT_DESERIALIZED_EQ(NTi::Yson(), R"(yson)");
+ ASSERT_DESERIALIZED_EQ(NTi::Yson(), R"({type_name=yson})");
+}
+
+TEST(TypeDeserialize, Uuid) {
+ ASSERT_DESERIALIZED_EQ(NTi::Uuid(), R"(uuid)");
+ ASSERT_DESERIALIZED_EQ(NTi::Uuid(), R"({type_name=uuid})");
+}
+
+TEST(TypeDeserialize, Optional) {
+ ASSERT_DESERIALIZED_EQ(NTi::Optional(NTi::Void()), R"({type_name=optional; item=void})");
+ ASSERT_DESERIALIZED_EQ(NTi::Optional(NTi::String()), R"({type_name=optional; item=string})");
+ ASSERT_DESERIALIZED_EQ(NTi::Optional(NTi::String()), R"({item=string; type_name=optional})");
+}
+
+TEST(TypeDeserialize, OptionalMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({item=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=optional})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "item")");
+}
+
+TEST(TypeDeserialize, List) {
+ ASSERT_DESERIALIZED_EQ(NTi::List(NTi::Void()), R"({type_name=list; item=void})");
+ ASSERT_DESERIALIZED_EQ(NTi::List(NTi::String()), R"({type_name=list; item=string})");
+ ASSERT_DESERIALIZED_EQ(NTi::List(NTi::String()), R"({item=string; type_name=list})");
+}
+
+TEST(TypeDeserialize, ListMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({item=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=list})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "item")");
+}
+
+TEST(TypeDeserialize, Dict) {
+ ASSERT_DESERIALIZED_EQ(NTi::Dict(NTi::Void(), NTi::Void()), R"({type_name=dict; key=void; value=void})");
+ ASSERT_DESERIALIZED_EQ(NTi::Dict(NTi::Int32(), NTi::String()), R"({type_name=dict; key=int32; value=string})");
+ ASSERT_DESERIALIZED_EQ(NTi::Dict(NTi::Int32(), NTi::String()), R"({key=int32; value=string; type_name=dict})");
+ ASSERT_DESERIALIZED_EQ(NTi::Dict(NTi::Int32(), NTi::String()), R"({value=string; key=int32; type_name=dict})");
+}
+
+TEST(TypeDeserialize, DictMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({key=string; value=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=dict})");
+ }(),
+ NTi::TDeserializationException, R"(missing required keys "key" and "value")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=dict; value=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "key")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=dict; key=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "value")");
+}
+
+TEST(TypeDeserialize, StructEmpty) {
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Struct({}),
+ R"({type_name=struct; members=[]})");
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Struct({}),
+ R"({members=[]; type_name=struct})");
+}
+
+TEST(TypeDeserialize, Struct) {
+ auto ty = NTi::Struct({{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}});
+
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=struct; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]; type_name=struct})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=struct; members=[{type=string; name=ItemB}; {name=ItemA; type={item=int64; type_name=list}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=struct; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]; name=#})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=struct; name=#; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeDeserialize, StructMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({members=[]})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=struct})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "members")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=struct; name=S})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "members")");
+}
+
+TEST(TypeDeserialize, TupleEmpty) {
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Tuple({}),
+ R"({type_name=tuple; elements=[]})");
+
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Tuple({}),
+ R"({elements=[]; type_name=tuple})");
+}
+
+TEST(TypeDeserialize, Tuple) {
+ auto ty = NTi::Tuple({{NTi::String()}, {NTi::List(NTi::Int64())}});
+
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=tuple; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({elements=[{type=string}; {type={item=int64; type_name=list}}]; type_name=tuple})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=tuple; name=#; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=tuple; elements=[{type=string}; {type={type_name=list; item=int64}}]; name=#})");
+}
+
+TEST(TypeDeserialize, TupleMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({elements=[]})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=tuple})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "elements")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=tuple; name=T})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "elements")");
+}
+
+TEST(TypeDeserialize, VariantStruct) {
+ auto ty = NTi::Variant(NTi::Struct({{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}}));
+
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]; type_name=variant})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; name=#; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]; name=#})");
+}
+
+TEST(TypeDeserialize, VariantTuple) {
+ auto ty = NTi::Variant(NTi::Tuple({{NTi::String()}, {NTi::List(NTi::Int64())}}));
+
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({elements=[{type=string}; {type={type_name=list; item=int64}}]; type_name=variant})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; name=#; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ ty,
+ R"({type_name=variant; elements=[{type=string}; {type={type_name=list; item=int64}}]; name=#})");
+}
+
+TEST(TypeDeserialize, VariantMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({elements=[]})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({name=X})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=variant})");
+ }(),
+ NTi::TDeserializationException, R"(missing both keys "members" and "elements")");
+}
+
+TEST(TypeDeserialize, Tagged) {
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Tagged(NTi::String(), "Url"),
+ R"({type_name=tagged; tag=Url; item=string})");
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Tagged(NTi::String(), "Url"),
+ R"({type_name=tagged; item=string; tag=Url})");
+ ASSERT_DESERIALIZED_EQ(
+ NTi::Tagged(NTi::String(), "Url"),
+ R"({item=string; tag=Url; type_name=tagged})");
+}
+
+TEST(TypeDeserialize, TaggedMissingTypeParameters) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({tag=T; item=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({tag=T})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=tagged; tag=T})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "item")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({type_name=tagged; item=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "tag")");
+}
+
+TEST(TypeDeserialize, ComplexTypeAsString) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(decimal)");
+ }(),
+ NTi::TDeserializationException, R"(missing required keys "precision" and "scale")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(optional)");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "item")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(list)");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "item")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(dict)");
+ }(),
+ NTi::TDeserializationException, R"(missing required keys "key" and "value")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(struct)");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "members")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(tuple)");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "elements")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(variant)");
+ }(),
+ NTi::TDeserializationException, R"(missing both keys "members" and "elements")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"(tagged)");
+ }(),
+ NTi::TDeserializationException, R"(missing required keys "tag" and "item")");
+}
+
+TEST(TypeDeserialize, MissingTypeName) {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({item=string})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({tag=Url})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(
+ []() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), R"({key=string; value=int32})");
+ }(),
+ NTi::TDeserializationException, R"(missing required key "type_name")");
+}
+
+TEST(TypeDeserialize, UnknownKeys) {
+ auto tupleType = NTi::Tuple({{NTi::String()}, {NTi::List(NTi::Int64())}});
+
+ ASSERT_DESERIALIZED_EQ(
+ tupleType,
+ R"({type_name=tuple; unknown_key=<>0; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+
+ ASSERT_DESERIALIZED_EQ(
+ tupleType,
+ R"({type_name=tuple; elements=[{unknown_key={foo=<>0}; type=string}; {type={type_name=list; item=int64}}]})");
+
+ auto structType = NTi::Struct({{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}});
+ ASSERT_DESERIALIZED_EQ(
+ structType,
+ R"({type_name=struct; unknown_key=[]; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+ ASSERT_DESERIALIZED_EQ(
+ structType,
+ R"({type_name=struct; members=[{name=ItemB; unknown_key=foo; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+
+ auto utf8Type = NTi::Utf8();
+ ASSERT_DESERIALIZED_EQ(
+ utf8Type,
+ R"({type_name=utf8; unknown_key=[];})");
+}
+
+
+TEST(TypeDeserialize, DeepType) {
+ auto ty = TStringBuilder();
+ for (size_t i = 0; i < 100; ++i)
+ ty << "{type_name=optional; item=";
+ ty << "string";
+ for (size_t i = 0; i < 100; ++i)
+ ty << "}";
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&ty]() {
+ NTi::NIo::DeserializeYson(*NTi::HeapFactory(), ty);
+ }(),
+ NTi::TDeserializationException, R"(too deep)");
+}
+
+TEST(TypeDeserialize, MultipleTypes) {
+ auto ty = "{type_name=optional; item=string}; {type_name=list; item=utf8}";
+
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromMemory(ty), NYsonPull::EStreamType::ListFragment);
+ auto factory = NTi::PoolFactory();
+
+ {
+ auto ty1 = NTi::Optional(NTi::String());
+ ASSERT_STRICT_EQ(NTi::NIo::DeserializeYsonMultipleRaw(*factory, reader), ty1.Get());
+ }
+
+ {
+ auto ty2 = NTi::List(NTi::Utf8());
+ ASSERT_STRICT_EQ(NTi::NIo::DeserializeYsonMultipleRaw(*factory, reader), ty2.Get());
+ }
+
+ ASSERT_EQ(NTi::NIo::DeserializeYsonMultipleRaw(*factory, reader), nullptr);
+}
diff --git a/library/cpp/type_info/ut/type_equivalence.cpp b/library/cpp/type_info/ut/type_equivalence.cpp
new file mode 100644
index 0000000000..ca697620ad
--- /dev/null
+++ b/library/cpp/type_info/ut/type_equivalence.cpp
@@ -0,0 +1,394 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeEquivalence, StrictEqSelf) {
+ ASSERT_STRICT_EQ(
+ f.Void(),
+ f.Void());
+ ASSERT_STRICT_EQ(
+ f.Bool(),
+ f.Bool());
+ ASSERT_STRICT_EQ(
+ f.Int8(),
+ f.Int8());
+ ASSERT_STRICT_EQ(
+ f.Int16(),
+ f.Int16());
+ ASSERT_STRICT_EQ(
+ f.Int32(),
+ f.Int32());
+ ASSERT_STRICT_EQ(
+ f.Int64(),
+ f.Int64());
+ ASSERT_STRICT_EQ(
+ f.Uint8(),
+ f.Uint8());
+ ASSERT_STRICT_EQ(
+ f.Uint16(),
+ f.Uint16());
+ ASSERT_STRICT_EQ(
+ f.Uint32(),
+ f.Uint32());
+ ASSERT_STRICT_EQ(
+ f.Uint64(),
+ f.Uint64());
+ ASSERT_STRICT_EQ(
+ f.Float(),
+ f.Float());
+ ASSERT_STRICT_EQ(
+ f.Double(),
+ f.Double());
+ ASSERT_STRICT_EQ(
+ f.String(),
+ f.String());
+ ASSERT_STRICT_EQ(
+ f.Utf8(),
+ f.Utf8());
+ ASSERT_STRICT_EQ(
+ f.Date(),
+ f.Date());
+ ASSERT_STRICT_EQ(
+ f.Datetime(),
+ f.Datetime());
+ ASSERT_STRICT_EQ(
+ f.Timestamp(),
+ f.Timestamp());
+ ASSERT_STRICT_EQ(
+ f.TzDate(),
+ f.TzDate());
+ ASSERT_STRICT_EQ(
+ f.TzDatetime(),
+ f.TzDatetime());
+ ASSERT_STRICT_EQ(
+ f.TzTimestamp(),
+ f.TzTimestamp());
+ ASSERT_STRICT_EQ(
+ f.Interval(),
+ f.Interval());
+ ASSERT_STRICT_EQ(
+ f.Decimal(20, 10),
+ f.Decimal(20, 10));
+ ASSERT_STRICT_EQ(
+ f.Json(),
+ f.Json());
+ ASSERT_STRICT_EQ(
+ f.Yson(),
+ f.Yson());
+ ASSERT_STRICT_EQ(
+ f.Uuid(),
+ f.Uuid());
+ ASSERT_STRICT_EQ(
+ f.Optional(f.Void()),
+ f.Optional(f.Void()));
+ ASSERT_STRICT_EQ(
+ f.List(f.Void()),
+ f.List(f.Void()));
+ ASSERT_STRICT_EQ(
+ f.Dict(f.Void(), f.String()),
+ f.Dict(f.Void(), f.String()));
+ ASSERT_STRICT_EQ(
+ f.Struct({}),
+ f.Struct({}));
+ ASSERT_STRICT_EQ(
+ f.Struct("S", {}),
+ f.Struct("S", {}));
+ ASSERT_STRICT_EQ(
+ f.Struct({{"a", f.Int64()}, {"b", f.Int8()}}),
+ f.Struct({{"a", f.Int64()}, {"b", f.Int8()}}));
+ ASSERT_STRICT_EQ(
+ f.Struct("S", {{"a", f.Int64()}, {"b", f.Int8()}}),
+ f.Struct("S", {{"a", f.Int64()}, {"b", f.Int8()}}));
+ ASSERT_STRICT_EQ(
+ f.Tuple({}),
+ f.Tuple({}));
+ ASSERT_STRICT_EQ(
+ f.Tuple("T", {}),
+ f.Tuple("T", {}));
+ ASSERT_STRICT_EQ(
+ f.Tuple({{f.Int64()}, {f.Int8()}}),
+ f.Tuple({{f.Int64()}, {f.Int8()}}));
+ ASSERT_STRICT_EQ(
+ f.Tuple("T", {{f.Int64()}, {f.Int8()}}),
+ f.Tuple("T", {{f.Int64()}, {f.Int8()}}));
+ ASSERT_STRICT_EQ(
+ f.Variant(f.Struct("Inner", {{"x", f.Void()}})),
+ f.Variant(f.Struct("Inner", {{"x", f.Void()}})));
+ ASSERT_STRICT_EQ(
+ f.Variant("V", f.Struct("Inner", {{"x", f.Void()}})),
+ f.Variant("V", f.Struct("Inner", {{"x", f.Void()}})));
+ ASSERT_STRICT_EQ(
+ f.Variant(f.Tuple("Inner", {{f.Void()}})),
+ f.Variant(f.Tuple("Inner", {{f.Void()}})));
+ ASSERT_STRICT_EQ(
+ f.Variant("V", f.Tuple("Inner", {{f.Void()}})),
+ f.Variant("V", f.Tuple("Inner", {{f.Void()}})));
+ ASSERT_STRICT_EQ(
+ f.Tagged(f.Void(), "T"),
+ f.Tagged(f.Void(), "T"));
+}
+
+TEST_TF(TypeEquivalence, StrictNeOtherType) {
+ ASSERT_STRICT_NE(
+ f.Void(),
+ f.Bool());
+ ASSERT_STRICT_NE(
+ f.Bool(),
+ f.Int8());
+ ASSERT_STRICT_NE(
+ f.Int8(),
+ f.Int16());
+ ASSERT_STRICT_NE(
+ f.Int16(),
+ f.Int32());
+ ASSERT_STRICT_NE(
+ f.Int32(),
+ f.Int64());
+ ASSERT_STRICT_NE(
+ f.Int64(),
+ f.Uint8());
+ ASSERT_STRICT_NE(
+ f.Uint8(),
+ f.Uint16());
+ ASSERT_STRICT_NE(
+ f.Uint16(),
+ f.Uint32());
+ ASSERT_STRICT_NE(
+ f.Uint32(),
+ f.Uint64());
+ ASSERT_STRICT_NE(
+ f.Uint64(),
+ f.Float());
+ ASSERT_STRICT_NE(
+ f.Float(),
+ f.Double());
+ ASSERT_STRICT_NE(
+ f.Double(),
+ f.String());
+ ASSERT_STRICT_NE(
+ f.String(),
+ f.Utf8());
+ ASSERT_STRICT_NE(
+ f.Utf8(),
+ f.Date());
+ ASSERT_STRICT_NE(
+ f.Date(),
+ f.Datetime());
+ ASSERT_STRICT_NE(
+ f.Datetime(),
+ f.Timestamp());
+ ASSERT_STRICT_NE(
+ f.Timestamp(),
+ f.TzDate());
+ ASSERT_STRICT_NE(
+ f.TzDate(),
+ f.TzDatetime());
+ ASSERT_STRICT_NE(
+ f.TzDatetime(),
+ f.TzTimestamp());
+ ASSERT_STRICT_NE(
+ f.TzTimestamp(),
+ f.Interval());
+ ASSERT_STRICT_NE(
+ f.Interval(),
+ f.Decimal(20, 10));
+ ASSERT_STRICT_NE(
+ f.Decimal(20, 10),
+ f.Json());
+ ASSERT_STRICT_NE(
+ f.Json(),
+ f.Yson());
+ ASSERT_STRICT_NE(
+ f.Yson(),
+ f.Uuid());
+ ASSERT_STRICT_NE(
+ f.Uuid(),
+ f.Optional(f.Void()));
+ ASSERT_STRICT_NE(
+ f.Optional(f.Void()),
+ f.List(f.Void()));
+ ASSERT_STRICT_NE(
+ f.List(f.Void()),
+ f.Dict(f.Void(), f.String()));
+ ASSERT_STRICT_NE(
+ f.Dict(f.Void(), f.String()),
+ f.Struct({}));
+ ASSERT_STRICT_NE(
+ f.Struct({}),
+ f.Struct("S", {}));
+ ASSERT_STRICT_NE(
+ f.Struct("S", {}),
+ f.Struct({{"a", f.Int64()}, {"b", f.Int8()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"a", f.Int64()}, {"b", f.Int8()}}),
+ f.Struct("S", {{"a", f.Int64()}, {"b", f.Int8()}}));
+ ASSERT_STRICT_NE(
+ f.Struct("S", {{"a", f.Int64()}, {"b", f.Int8()}}),
+ f.Tuple({}));
+ ASSERT_STRICT_NE(
+ f.Tuple({}),
+ f.Tuple("T", {}));
+ ASSERT_STRICT_NE(
+ f.Tuple("T", {}),
+ f.Tuple({{f.Int64()}, {f.Int8()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple({{f.Int64()}, {f.Int8()}}),
+ f.Tuple("T", {{f.Int64()}, {f.Int8()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple("T", {{f.Int64()}, {f.Int8()}}),
+ f.Variant(f.Struct("Inner", {{"x", f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant(f.Struct("Inner", {{"x", f.Void()}})),
+ f.Variant("V", f.Struct("Inner", {{"x", f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant("V", f.Struct("Inner", {{"x", f.Void()}})),
+ f.Variant(f.Tuple("Inner", {{f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant(f.Tuple("Inner", {{f.Void()}})),
+ f.Variant("V", f.Tuple("Inner", {{f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant("V", f.Tuple("Inner", {{f.Void()}})),
+ f.Tagged(f.Void(), "T"));
+ ASSERT_STRICT_NE(
+ f.Tagged(f.Void(), "T"),
+ f.Void());
+}
+
+TEST_TF(TypeEquivalence, StrictNeDecimal) {
+ ASSERT_STRICT_NE(
+ f.Decimal(20, 10),
+ f.Decimal(21, 10));
+ ASSERT_STRICT_NE(
+ f.Decimal(20, 10),
+ f.Decimal(20, 11));
+}
+
+TEST_TF(TypeEquivalence, StrictNeStruct) {
+ ASSERT_STRICT_NE(
+ f.Struct({}),
+ f.Struct("", {}));
+ ASSERT_STRICT_NE(
+ f.Struct("name", {}),
+ f.Struct("other name", {}));
+ ASSERT_STRICT_NE(
+ f.Struct({}),
+ f.Struct({{"x", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"x", f.Void()}}),
+ f.Struct({{"y", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"x", f.Void()}}),
+ f.Struct({{"x", f.String()}}));
+ ASSERT_STRICT_NE(
+ f.Struct("name", {}),
+ f.Struct("name", {{"x", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct("name", {{"x", f.Void()}}),
+ f.Struct("name", {{"y", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct("name", {{"x", f.Void()}}),
+ f.Struct("name", {{"x", f.String()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"x", f.Void()}, {"y", f.String()}}),
+ f.Struct({{"x", f.String()}, {"y", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"x", f.Void()}, {"y", f.Void()}}),
+ f.Struct({{"y", f.Void()}, {"x", f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Struct({{"x", f.Void()}, {"y", f.Void()}}),
+ f.Struct({{"x", f.Void()}, {"y", f.Void()}, {"z", f.Void()}}));
+}
+
+TEST_TF(TypeEquivalence, StrictNeTuple) {
+ ASSERT_STRICT_NE(
+ f.Tuple({}),
+ f.Tuple("", {}));
+ ASSERT_STRICT_NE(
+ f.Tuple("name", {}),
+ f.Tuple("other name", {}));
+ ASSERT_STRICT_NE(
+ f.Tuple({}),
+ f.Tuple({{f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple({{f.Void()}}),
+ f.Tuple({{f.String()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple("name", {}),
+ f.Tuple("name", {{f.Void()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple("name", {{f.Void()}}),
+ f.Tuple("name", {{f.String()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple({{f.String()}, {f.Void()}}),
+ f.Tuple({{f.Void()}, {f.String()}}));
+ ASSERT_STRICT_NE(
+ f.Tuple({{f.Void()}, {f.Void()}}),
+ f.Tuple({{f.Void()}, {f.Void()}, {f.Void()}}));
+}
+
+TEST_TF(TypeEquivalence, StrictNeVariant) {
+ ASSERT_STRICT_NE(
+ f.Variant(f.Tuple({{f.Void()}})),
+ f.Variant("", f.Tuple({{f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant("", f.Tuple({{f.Void()}})),
+ f.Variant("X", f.Tuple({{f.Void()}})));
+ ASSERT_STRICT_NE(
+ f.Variant(f.Tuple({{f.Void()}})),
+ f.Variant(f.Tuple({{f.String()}})));
+ ASSERT_STRICT_NE(
+ f.Variant("X", f.Tuple({{f.Utf8()}})),
+ f.Variant("X", f.Tuple({{f.String()}})));
+ ASSERT_STRICT_NE(
+ f.Variant(f.Tuple({{f.Utf8()}})),
+ f.Variant(f.Struct({{"_", f.Utf8()}})));
+ ASSERT_STRICT_NE(
+ f.Variant(f.Struct({{"item1", f.String()}})),
+ f.Variant(f.Struct({{"item2", f.String()}})));
+ ASSERT_STRICT_NE(
+ f.Variant("X", f.Struct({{"item2", f.String()}})),
+ f.Variant("X", f.Struct({{"item1", f.String()}})));
+}
+
+TEST_TF(TypeEquivalence, StrictNeTagged) {
+ ASSERT_STRICT_NE(
+ f.Tagged(f.String(), "Tag"),
+ f.Tagged(f.String(), "Other tag"));
+ ASSERT_STRICT_NE(
+ f.Tagged(f.String(), "Tag"),
+ f.Tagged(f.Utf8(), "Tag"));
+}
+
+TEST_TF(TypeEquivalence, StrictNeDeep) {
+ auto t1 = f.Struct({
+ {"i", f.Optional(f.String())},
+ {"don't", f.Utf8()},
+ {"have", f.Tuple("GeoCoordinates", {{f.Float()}, {f.Float()}})},
+ {"enough", f.List(f.Optional(f.String()))},
+ {"fantasy", f.Dict(f.String(), f.List(f.Utf8()))}, // < difference
+ {"to", f.Yson()},
+ {"think", f.Optional(f.Json())},
+ {"of", f.Optional(f.String())},
+ {"a", f.List(f.Tuple({{f.String()}, {f.Tuple("GeoCoordinates", {{f.Float()}, {f.Float()}})}}))},
+ {"meaningful", f.Optional(f.String())},
+ {"example", f.Dict(f.Optional(f.Decimal(10, 5)), f.List(f.Dict(f.Int32(), f.Float())))},
+ });
+
+ auto t2 = f.Struct({
+ {"i", f.Optional(f.String())},
+ {"don't", f.Utf8()},
+ {"have", f.Tuple("GeoCoordinates", {{f.Float()}, {f.Float()}})},
+ {"enough", f.List(f.Optional(f.String()))},
+ {"fantasy", f.Dict(f.String(), f.List(f.String()))}, // < difference
+ {"to", f.Yson()},
+ {"think", f.Optional(f.Json())},
+ {"of", f.Optional(f.String())},
+ {"a", f.List(f.Tuple({{f.String()}, {f.Tuple("GeoCoordinates", {{f.Float()}, {f.Float()}})}}))},
+ {"meaningful", f.Optional(f.String())},
+ {"example", f.Dict(f.Optional(f.Decimal(10, 5)), f.List(f.Dict(f.Int32(), f.Float())))},
+ });
+
+ ASSERT_STRICT_NE(t1, t2);
+}
diff --git a/library/cpp/type_info/ut/type_factory.cpp b/library/cpp/type_info/ut/type_factory.cpp
new file mode 100644
index 0000000000..0ab44129ad
--- /dev/null
+++ b/library/cpp/type_info/ut/type_factory.cpp
@@ -0,0 +1,121 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+TEST(TypeFactory, AdoptPoolToPool) {
+ auto f1 = NTi::PoolFactory();
+ auto f2 = NTi::PoolFactory();
+
+ auto t = f1->Optional(f1->List(f1->String()));
+ auto ta = f2->Adopt(t);
+
+ ASSERT_NE(t.Get(), ta.Get());
+ ASSERT_NE(t->GetItemTypeRaw(), ta->GetItemTypeRaw());
+ ASSERT_EQ(t->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw(), ta->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw());
+
+ f1 = nullptr;
+ f2 = nullptr;
+ t = nullptr;
+
+ // `ta` is still alive
+
+ ASSERT_TRUE(ta->IsOptional());
+ ASSERT_TRUE(ta->GetItemTypeRaw()->IsList());
+}
+
+TEST(TypeFactory, AdoptPoolToSamePool) {
+ auto f = NTi::PoolFactory();
+
+ auto t = f->Optional(f->List(f->String()));
+ auto ta = f->Adopt(t);
+
+ ASSERT_EQ(t.Get(), ta.Get());
+}
+
+TEST(TypeFactory, AdoptHeapToHeap) {
+ auto f = NTi::HeapFactory();
+
+ auto t = f->Optional(f->List(f->String()));
+ auto ta = f->Adopt(t);
+
+ ASSERT_EQ(t.Get(), ta.Get());
+}
+
+TEST(TypeFactory, AdoptHeapToPool) {
+ auto f1 = NTi::HeapFactory();
+ auto f2 = NTi::PoolFactory();
+
+ auto t = f1->Optional(f1->List(f1->String()));
+ auto ta = f2->Adopt(t);
+
+ ASSERT_NE(t.Get(), ta.Get());
+ ASSERT_NE(t->GetItemTypeRaw(), ta->GetItemTypeRaw());
+ ASSERT_EQ(t->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw(), ta->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw());
+
+ f1 = nullptr;
+ f2 = nullptr;
+ t = nullptr;
+
+ // `ta` is still alive
+
+ ASSERT_TRUE(ta->IsOptional());
+ ASSERT_TRUE(ta->GetItemTypeRaw()->IsList());
+}
+
+TEST(TypeFactory, AdoptPoolToHeap) {
+ auto f1 = NTi::PoolFactory();
+ auto f2 = NTi::HeapFactory();
+
+ auto t = f1->Optional(f1->List(f1->String()));
+ auto ta = f2->Adopt(t);
+
+ ASSERT_NE(t.Get(), ta.Get());
+ ASSERT_NE(t->GetItemTypeRaw(), ta->GetItemTypeRaw());
+ ASSERT_EQ(t->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw(), ta->GetItemTypeRaw()->AsListRaw()->GetItemTypeRaw());
+
+ f1 = nullptr;
+ f2 = nullptr;
+ t = nullptr;
+
+ // `ta` is still alive
+
+ ASSERT_TRUE(ta->IsOptional());
+ ASSERT_TRUE(ta->GetItemTypeRaw()->IsList());
+}
+
+TEST(TypeFactory, AdoptStaticToPool) {
+ auto f = NTi::PoolFactory();
+
+ auto t = NTi::Void();
+ auto ta = f->Adopt(t);
+
+ ASSERT_EQ(t.Get(), ta.Get());
+}
+
+TEST(TypeFactory, AdoptStaticToHeap) {
+ auto f = NTi::HeapFactory();
+
+ auto t = NTi::Void();
+ auto ta = f->Adopt(t);
+ ASSERT_EQ(t.Get(), ta.Get());
+}
+
+TEST(TypeFactory, Dedup) {
+ {
+ auto f = NTi::PoolFactory(/* deduplicate = */ false);
+
+ auto a = f->OptionalRaw(f->StringRaw());
+ auto b = f->OptionalRaw(f->StringRaw());
+
+ ASSERT_NE(a, b);
+ }
+
+ {
+ auto f = NTi::PoolFactory(/* deduplicate = */ true);
+
+ auto a = f->OptionalRaw(f->StringRaw());
+ auto b = f->OptionalRaw(f->StringRaw());
+
+ ASSERT_EQ(a, b);
+ }
+}
diff --git a/library/cpp/type_info/ut/type_factory_raw.cpp b/library/cpp/type_info/ut/type_factory_raw.cpp
new file mode 100644
index 0000000000..37f5d71aa7
--- /dev/null
+++ b/library/cpp/type_info/ut/type_factory_raw.cpp
@@ -0,0 +1,365 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+TEST(TypeFactoryRaw, Void) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->VoidRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Void);
+ ASSERT_TRUE(t->IsVoid());
+}
+
+TEST(TypeFactoryRaw, Bool) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->BoolRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Bool);
+ ASSERT_TRUE(t->IsBool());
+}
+
+TEST(TypeFactoryRaw, Int8) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Int8Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int8);
+ ASSERT_TRUE(t->IsInt8());
+}
+
+TEST(TypeFactoryRaw, Int16) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Int16Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int16);
+ ASSERT_TRUE(t->IsInt16());
+}
+
+TEST(TypeFactoryRaw, Int32) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Int32Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int32);
+ ASSERT_TRUE(t->IsInt32());
+}
+
+TEST(TypeFactoryRaw, Int64) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Int64Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Int64);
+ ASSERT_TRUE(t->IsInt64());
+}
+
+TEST(TypeFactoryRaw, Uint8) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Uint8Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint8);
+ ASSERT_TRUE(t->IsUint8());
+}
+
+TEST(TypeFactoryRaw, Uint16) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Uint16Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint16);
+ ASSERT_TRUE(t->IsUint16());
+}
+
+TEST(TypeFactoryRaw, Uint32) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Uint32Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint32);
+ ASSERT_TRUE(t->IsUint32());
+}
+
+TEST(TypeFactoryRaw, Uint64) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Uint64Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uint64);
+ ASSERT_TRUE(t->IsUint64());
+}
+
+TEST(TypeFactoryRaw, Float) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->FloatRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Float);
+ ASSERT_TRUE(t->IsFloat());
+}
+
+TEST(TypeFactoryRaw, Double) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->DoubleRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Double);
+ ASSERT_TRUE(t->IsDouble());
+}
+
+TEST(TypeFactoryRaw, String) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->StringRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::String);
+ ASSERT_TRUE(t->IsString());
+}
+
+TEST(TypeFactoryRaw, Utf8) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->Utf8Raw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Utf8);
+ ASSERT_TRUE(t->IsUtf8());
+}
+
+TEST(TypeFactoryRaw, Date) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->DateRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Date);
+ ASSERT_TRUE(t->IsDate());
+}
+
+TEST(TypeFactoryRaw, Datetime) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->DatetimeRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Datetime);
+ ASSERT_TRUE(t->IsDatetime());
+}
+
+TEST(TypeFactoryRaw, Timestamp) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TimestampRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Timestamp);
+ ASSERT_TRUE(t->IsTimestamp());
+}
+
+TEST(TypeFactoryRaw, TzDate) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TzDateRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzDate);
+ ASSERT_TRUE(t->IsTzDate());
+}
+
+TEST(TypeFactoryRaw, TzDatetime) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TzDatetimeRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzDatetime);
+ ASSERT_TRUE(t->IsTzDatetime());
+}
+
+TEST(TypeFactoryRaw, TzTimestamp) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TzTimestampRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::TzTimestamp);
+ ASSERT_TRUE(t->IsTzTimestamp());
+}
+
+TEST(TypeFactoryRaw, Interval) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->IntervalRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Interval);
+ ASSERT_TRUE(t->IsInterval());
+}
+
+TEST(TypeFactoryRaw, Decimal) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->DecimalRaw(20, 10);
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Decimal);
+ ASSERT_TRUE(t->IsDecimal());
+ ASSERT_EQ(t->GetPrecision(), 20);
+ ASSERT_EQ(t->GetScale(), 10);
+}
+
+TEST(TypeFactoryRaw, Json) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->JsonRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Json);
+ ASSERT_TRUE(t->IsJson());
+}
+
+TEST(TypeFactoryRaw, Yson) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->YsonRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Yson);
+ ASSERT_TRUE(t->IsYson());
+}
+
+TEST(TypeFactoryRaw, Uuid) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->UuidRaw();
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Uuid);
+ ASSERT_TRUE(t->IsUuid());
+}
+
+TEST(TypeFactoryRaw, Optional) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->OptionalRaw(f->VoidRaw());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Optional);
+ ASSERT_TRUE(t->IsOptional());
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
+
+TEST(TypeFactoryRaw, List) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->ListRaw(f->VoidRaw());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::List);
+ ASSERT_TRUE(t->IsList());
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
+
+TEST(TypeFactoryRaw, Dict) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->DictRaw(f->VoidRaw(), f->StringRaw());
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Dict);
+ ASSERT_TRUE(t->IsDict());
+ ASSERT_TRUE(t->GetKeyType()->IsVoid());
+ ASSERT_EQ(t->GetKeyType().Get(), t->GetKeyTypeRaw());
+ ASSERT_TRUE(t->GetValueType()->IsString());
+ ASSERT_EQ(t->GetValueType().Get(), t->GetValueTypeRaw());
+}
+
+TEST(TypeFactoryRaw, EmptyStruct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->StructRaw({});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetMembers().size(), 0);
+}
+
+TEST(TypeFactoryRaw, NamedEmptyStruct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->StructRaw("S", {});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "S");
+ ASSERT_EQ(t->GetMembers().size(), 0);
+}
+
+TEST(TypeFactoryRaw, Struct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->StructRaw({{"a", f->Int64Raw()}, {"b", f->Int8Raw()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetMembers().size(), 2);
+ ASSERT_EQ(t->GetMembers()[0].GetName(), "a");
+ ASSERT_TRUE(t->GetMembers()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetMembers()[0].GetType().Get(), t->GetMembers()[0].GetTypeRaw());
+ ASSERT_EQ(t->GetMembers()[1].GetName(), "b");
+ ASSERT_TRUE(t->GetMembers()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetMembers()[1].GetType().Get(), t->GetMembers()[1].GetTypeRaw());
+}
+
+TEST(TypeFactoryRaw, NamedStruct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->StructRaw("S", {{"a", f->Int64Raw()}, {"b", f->Int8Raw()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Struct);
+ ASSERT_TRUE(t->IsStruct());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "S");
+ ASSERT_EQ(t->GetMembers().size(), 2);
+ ASSERT_EQ(t->GetMembers()[0].GetName(), "a");
+ ASSERT_TRUE(t->GetMembers()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetMembers()[0].GetType().Get(), t->GetMembers()[0].GetTypeRaw());
+ ASSERT_EQ(t->GetMembers()[1].GetName(), "b");
+ ASSERT_TRUE(t->GetMembers()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetMembers()[1].GetType().Get(), t->GetMembers()[1].GetTypeRaw());
+}
+
+TEST(TypeFactoryRaw, EmptyTuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TupleRaw({});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetElements().size(), 0);
+}
+
+TEST(TypeFactoryRaw, NamedEmptyTuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TupleRaw("T", {});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "T");
+ ASSERT_EQ(t->GetElements().size(), 0);
+}
+
+TEST(TypeFactoryRaw, Tuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TupleRaw({{f->Int64Raw()}, {f->Int8Raw()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_EQ(t->GetElements().size(), 2);
+ ASSERT_TRUE(t->GetElements()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetElements()[0].GetType().Get(), t->GetElements()[0].GetTypeRaw());
+ ASSERT_TRUE(t->GetElements()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetElements()[1].GetType().Get(), t->GetElements()[1].GetTypeRaw());
+}
+
+TEST(TypeFactoryRaw, NamedTuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TupleRaw("T", {{f->Int64Raw()}, {f->Int8Raw()}});
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tuple);
+ ASSERT_TRUE(t->IsTuple());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "T");
+ ASSERT_EQ(t->GetElements().size(), 2);
+ ASSERT_TRUE(t->GetElements()[0].GetTypeRaw()->IsInt64());
+ ASSERT_EQ(t->GetElements()[0].GetType().Get(), t->GetElements()[0].GetTypeRaw());
+ ASSERT_TRUE(t->GetElements()[1].GetTypeRaw()->IsInt8());
+ ASSERT_EQ(t->GetElements()[1].GetType().Get(), t->GetElements()[1].GetTypeRaw());
+}
+
+TEST(TypeFactoryRaw, VariantOverStruct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->VariantRaw(f->StructRaw("Inner", {{"x", f->VoidRaw()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_TRUE(t->IsVariantOverStruct());
+ ASSERT_FALSE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsStruct()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST(TypeFactoryRaw, NamedVariantOverStruct) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->VariantRaw("V", f->StructRaw("Inner", {{"x", f->VoidRaw()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "V");
+ ASSERT_TRUE(t->IsVariantOverStruct());
+ ASSERT_FALSE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsStruct()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST(TypeFactoryRaw, VariantOverTuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->VariantRaw(f->TupleRaw("Inner", {{f->VoidRaw()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_FALSE(t->GetName().Defined());
+ ASSERT_FALSE(t->IsVariantOverStruct());
+ ASSERT_TRUE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsTuple()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST(TypeFactoryRaw, NamedVariantOverTuple) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->VariantRaw("V", f->TupleRaw("Inner", {{f->VoidRaw()}}));
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Variant);
+ ASSERT_TRUE(t->IsVariant());
+ ASSERT_TRUE(t->GetName().Defined());
+ ASSERT_EQ(t->GetName().GetRef(), "V");
+ ASSERT_FALSE(t->IsVariantOverStruct());
+ ASSERT_TRUE(t->IsVariantOverTuple());
+ ASSERT_EQ(t->GetUnderlyingType()->AsTuple()->GetName().GetRef(), "Inner");
+ ASSERT_EQ(t->GetUnderlyingType().Get(), t->GetUnderlyingTypeRaw());
+}
+
+TEST(TypeFactoryRaw, Tagged) {
+ auto f = NTi::PoolFactory(false);
+ auto t = f->TaggedRaw(f->VoidRaw(), "T");
+ ASSERT_EQ(t->GetTypeName(), NTi::ETypeName::Tagged);
+ ASSERT_TRUE(t->IsTagged());
+ ASSERT_EQ(t->GetTag(), "T");
+ ASSERT_TRUE(t->GetItemType()->IsVoid());
+ ASSERT_EQ(t->GetItemType().Get(), t->GetItemTypeRaw());
+}
diff --git a/library/cpp/type_info/ut/type_io.cpp b/library/cpp/type_info/ut/type_io.cpp
new file mode 100644
index 0000000000..4feb9b7d83
--- /dev/null
+++ b/library/cpp/type_info/ut/type_io.cpp
@@ -0,0 +1,535 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeIO, AsYqlType) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Void().Get()),
+ "[VoidType]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Null().Get()),
+ "[NullType]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Bool().Get()),
+ "[DataType; Bool]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Int8().Get()),
+ "[DataType; Int8]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Int16().Get()),
+ "[DataType; Int16]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Int32().Get()),
+ "[DataType; Int32]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Int64().Get()),
+ "[DataType; Int64]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Uint8().Get()),
+ "[DataType; Uint8]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Uint16().Get()),
+ "[DataType; Uint16]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Uint32().Get()),
+ "[DataType; Uint32]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Uint64().Get()),
+ "[DataType; Uint64]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Float().Get()),
+ "[DataType; Float]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Double().Get()),
+ "[DataType; Double]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.String().Get()),
+ "[DataType; String]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Utf8().Get()),
+ "[DataType; Utf8]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Date().Get()),
+ "[DataType; Date]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Datetime().Get()),
+ "[DataType; Datetime]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Timestamp().Get()),
+ "[DataType; Timestamp]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.TzDate().Get()),
+ "[DataType; TzDate]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.TzDatetime().Get()),
+ "[DataType; TzDatetime]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.TzTimestamp().Get()),
+ "[DataType; TzTimestamp]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Interval().Get()),
+ "[DataType; Interval]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Json().Get()),
+ "[DataType; Json]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Yson().Get()),
+ "[DataType; Yson]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Uuid().Get()),
+ "[DataType; Uuid]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Decimal(20, 10).Get()),
+ "[DataType; Decimal; \"20\"; \"10\"]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Decimal(35, 35).Get()),
+ "[DataType; Decimal; \"35\"; \"35\"]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(Optional(f.Bool()).Get()),
+ "[OptionalType; [DataType; Bool]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.List(f.Bool()).Get()),
+ "[ListType; [DataType; Bool]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Dict(f.Bool(), f.Int32()).Get()),
+ "[DictType; [DataType; Bool]; [DataType; Int32]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Struct({}).Get()),
+ "[StructType; []]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Struct({{"a", f.Bool()}}).Get()),
+ "[StructType; [[a; [DataType; Bool]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Struct({{"a", f.Yson()}, {"b", f.Bool()}}).Get()),
+ "[StructType; [[a; [DataType; Yson]]; [b; [DataType; Bool]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Struct({{"a", f.Int32()}, {"b", f.Int32()}, {"c", f.Int64()}}).Get()),
+ "[StructType; [[a; [DataType; Int32]]; [b; [DataType; Int32]]; [c; [DataType; Int64]]]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tuple({}).Get()),
+ "[TupleType; []]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tuple({{f.Bool()}}).Get()),
+ "[TupleType; [[DataType; Bool]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tuple({{f.Yson()}, {f.Bool()}}).Get()),
+ "[TupleType; [[DataType; Yson]; [DataType; Bool]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tuple({{f.Int32()}, {f.Int32()}, {f.Int64()}}).Get()),
+ "[TupleType; [[DataType; Int32]; [DataType; Int32]; [DataType; Int64]]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Struct({{"a", f.Bool()}})).Get()),
+ "[VariantType; [StructType; [[a; [DataType; Bool]]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Struct({{"a", f.Yson()}, {"b", f.Bool()}})).Get()),
+ "[VariantType; [StructType; [[a; [DataType; Yson]]; [b; [DataType; Bool]]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Struct({{"a", f.Int32()}, {"b", f.Int32()}, {"c", f.Int64()}})).Get()),
+ "[VariantType; [StructType; [[a; [DataType; Int32]]; [b; [DataType; Int32]]; [c; [DataType; Int64]]]]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Tuple({{f.Bool()}})).Get()),
+ "[VariantType; [TupleType; [[DataType; Bool]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Tuple({{f.Yson()}, {f.Bool()}})).Get()),
+ "[VariantType; [TupleType; [[DataType; Yson]; [DataType; Bool]]]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Variant(f.Tuple({{f.Int32()}, {f.Int32()}, {f.Int32()}})).Get()),
+ "[VariantType; [TupleType; [[DataType; Int32]; [DataType; Int32]; [DataType; Int32]]]]");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tagged(f.String(), "Url").Get()),
+ "[TaggedType; Url; [DataType; String]]");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlType(f.Tagged(f.String(), "Url").Get(), /* includeTags = */ false),
+ "[DataType; String]");
+}
+
+TEST_TF(TypeIO, AsYqlRowSpec) {
+ {
+ auto type = f.Struct({});
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; []]}");
+ }
+ {
+ auto type = f.Tagged(f.Struct({}), "Event");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; []]}");
+ }
+ {
+ auto type = f.Struct({{"x", f.Tagged(f.String(), "Url")}});
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; [[x; [TaggedType; Url; [DataType; String]]]]]}");
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get(), /* includeTags = */ false),
+ "{StrictSchema=%true; Type=[StructType; [[x; [DataType; String]]]]}");
+ }
+ {
+ auto type = f.Struct({{"a", f.Bool()}});
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; [[a; [DataType; Bool]]]]}");
+ }
+ {
+ auto type = f.Struct({{"a", f.Yson()}, {"b", f.Bool()}});
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; [[a; [DataType; Yson]]; [b; [DataType; Bool]]]]}");
+ }
+ {
+ auto type = f.Struct({{"a", f.Int32()}, {"b", f.Int32()}, {"c", f.Int32()}});
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYqlRowSpec(type.Get()),
+ "{StrictSchema=%true; Type=[StructType; [[a; [DataType; Int32]]; [b; [DataType; Int32]]; [c; [DataType; Int32]]]]}");
+ }
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ NTi::NIo::AsYqlRowSpec(f.Void().Get());
+ }(),
+ NTi::TApiException, "expected a struct type");
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ NTi::NIo::AsYqlRowSpec(Optional(f.Struct({})).Get());
+ }(),
+ NTi::TApiException, "expected a struct type");
+}
+
+TEST_TF(TypeIO, AsYtSchema) {
+ {
+ auto type = f.Struct({
+ {"void", f.Void()},
+ {"null", f.Null()},
+ {"bool", f.Bool()},
+ {"int8", f.Int8()},
+ {"int16", f.Int16()},
+ {"int32", f.Int32()},
+ {"int64", f.Int64()},
+ {"uint8", f.Uint8()},
+ {"uint16", f.Uint16()},
+ {"uint32", f.Uint32()},
+ {"uint64", f.Uint64()},
+ {"float", f.Float()},
+ {"double", f.Double()},
+ {"string", f.String()},
+ {"utf8", f.Utf8()},
+ {"date", f.Date()},
+ {"datetime", f.Datetime()},
+ {"timestamp", f.Timestamp()},
+ {"tzdate", f.TzDate()},
+ {"tzdatetime", f.TzDatetime()},
+ {"tztimestamp", f.TzTimestamp()},
+ {"interval", f.Interval()},
+ {"decimal", f.Decimal(20, 10)},
+ {"json", f.Json()},
+ {"yson", f.Yson()},
+ {"uuid", f.Uuid()},
+ {"list", f.List(f.Bool())},
+ {"dict", f.Dict(f.Bool(), f.Bool())},
+ {"struct", f.Struct({})},
+ {"tuple", f.Tuple({})},
+ {"variant", f.Variant(f.Struct({{"x", f.Bool()}}))},
+ });
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYtSchema(type.Get()),
+ R"(
+ <strict=%true; unique_keys=%false>
+ [
+ {name=void; required=%false; type=any };
+ {name=null; required=%false; type=any };
+ {name=bool; required=%true; type=boolean };
+ {name=int8; required=%true; type=int8 };
+ {name=int16; required=%true; type=int16 };
+ {name=int32; required=%true; type=int32 };
+ {name=int64; required=%true; type=int64 };
+ {name=uint8; required=%true; type=uint8 };
+ {name=uint16; required=%true; type=uint16 };
+ {name=uint32; required=%true; type=uint32 };
+ {name=uint64; required=%true; type=uint64 };
+ {name=float; required=%true; type=double };
+ {name=double; required=%true; type=double };
+ {name=string; required=%true; type=string };
+ {name=utf8; required=%true; type=utf8 };
+ {name=date; required=%true; type=uint16 };
+ {name=datetime; required=%true; type=uint32 };
+ {name=timestamp; required=%true; type=uint64 };
+ {name=tzdate; required=%true; type=string };
+ {name=tzdatetime; required=%true; type=string };
+ {name=tztimestamp; required=%true; type=string };
+ {name=interval; required=%true; type=int64 };
+ {name=decimal; required=%true; type=string };
+ {name=json; required=%true; type=string };
+ {name=yson; required=%false; type=any };
+ {name=uuid; required=%true; type=string };
+ {name=list; required=%false; type=any };
+ {name=dict; required=%false; type=any };
+ {name=struct; required=%false; type=any };
+ {name=tuple; required=%false; type=any };
+ {name=variant; required=%false; type=any };
+ ]
+ )");
+ }
+
+ {
+ auto type = f.Struct({
+ {"void", f.Tagged(f.Void(), "Tag")},
+ {"null", f.Tagged(f.Null(), "Tag")},
+ {"bool", f.Tagged(f.Bool(), "Tag")},
+ {"int8", f.Tagged(f.Int8(), "Tag")},
+ {"int16", f.Tagged(f.Int16(), "Tag")},
+ {"int32", f.Tagged(f.Int32(), "Tag")},
+ {"int64", f.Tagged(f.Int64(), "Tag")},
+ {"uint8", f.Tagged(f.Uint8(), "Tag")},
+ {"uint16", f.Tagged(f.Uint16(), "Tag")},
+ {"uint32", f.Tagged(f.Uint32(), "Tag")},
+ {"uint64", f.Tagged(f.Uint64(), "Tag")},
+ {"float", f.Tagged(f.Float(), "Tag")},
+ {"double", f.Tagged(f.Double(), "Tag")},
+ {"string", f.Tagged(f.String(), "Tag")},
+ {"utf8", f.Tagged(f.Utf8(), "Tag")},
+ {"date", f.Tagged(f.Date(), "Tag")},
+ {"datetime", f.Tagged(f.Datetime(), "Tag")},
+ {"timestamp", f.Tagged(f.Timestamp(), "Tag")},
+ {"tzdate", f.Tagged(f.TzDate(), "Tag")},
+ {"tzdatetime", f.Tagged(f.TzDatetime(), "Tag")},
+ {"tztimestamp", f.Tagged(f.TzTimestamp(), "Tag")},
+ {"interval", f.Tagged(f.Interval(), "Tag")},
+ {"decimal", f.Tagged(f.Decimal(20, 10), "Tag")},
+ {"json", f.Tagged(f.Json(), "Tag")},
+ {"yson", f.Tagged(f.Yson(), "Tag")},
+ {"uuid", f.Tagged(f.Uuid(), "Tag")},
+ {"list", f.Tagged(f.List(f.Bool()), "Tag")},
+ {"dict", f.Tagged(f.Dict(f.Bool(), f.Bool()), "Tag")},
+ {"struct", f.Tagged(f.Struct({}), "Tag")},
+ {"tuple", f.Tagged(f.Tuple({}), "Tag")},
+ {"variant", f.Tagged(f.Variant(f.Struct({{"x", f.Bool()}})), "Tag")},
+ });
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYtSchema(type.Get()),
+ R"(
+ <strict=%true; unique_keys=%false>
+ [
+ {name=void; required=%false; type=any };
+ {name=null; required=%false; type=any };
+ {name=bool; required=%true; type=boolean };
+ {name=int8; required=%true; type=int8 };
+ {name=int16; required=%true; type=int16 };
+ {name=int32; required=%true; type=int32 };
+ {name=int64; required=%true; type=int64 };
+ {name=uint8; required=%true; type=uint8 };
+ {name=uint16; required=%true; type=uint16 };
+ {name=uint32; required=%true; type=uint32 };
+ {name=uint64; required=%true; type=uint64 };
+ {name=float; required=%true; type=double };
+ {name=double; required=%true; type=double };
+ {name=string; required=%true; type=string };
+ {name=utf8; required=%true; type=utf8 };
+ {name=date; required=%true; type=uint16 };
+ {name=datetime; required=%true; type=uint32 };
+ {name=timestamp; required=%true; type=uint64 };
+ {name=tzdate; required=%true; type=string };
+ {name=tzdatetime; required=%true; type=string };
+ {name=tztimestamp; required=%true; type=string };
+ {name=interval; required=%true; type=int64 };
+ {name=decimal; required=%true; type=string };
+ {name=json; required=%true; type=string };
+ {name=yson; required=%false; type=any };
+ {name=uuid; required=%true; type=string };
+ {name=list; required=%false; type=any };
+ {name=dict; required=%false; type=any };
+ {name=struct; required=%false; type=any };
+ {name=tuple; required=%false; type=any };
+ {name=variant; required=%false; type=any };
+ ]
+ )");
+ }
+
+ {
+ auto type = f.Struct({
+ {"void", Optional(f.Void())},
+ {"null", Optional(f.Null())},
+ {"bool", Optional(f.Bool())},
+ {"int8", Optional(f.Int8())},
+ {"int16", Optional(f.Int16())},
+ {"int32", Optional(f.Int32())},
+ {"int64", Optional(f.Int64())},
+ {"uint8", Optional(f.Uint8())},
+ {"uint16", Optional(f.Uint16())},
+ {"uint32", Optional(f.Uint32())},
+ {"uint64", Optional(f.Uint64())},
+ {"float", Optional(f.Float())},
+ {"double", Optional(f.Double())},
+ {"string", Optional(f.String())},
+ {"utf8", Optional(f.Utf8())},
+ {"date", Optional(f.Date())},
+ {"datetime", Optional(f.Datetime())},
+ {"timestamp", Optional(f.Timestamp())},
+ {"tzdate", Optional(f.TzDate())},
+ {"tzdatetime", Optional(f.TzDatetime())},
+ {"tztimestamp", Optional(f.TzTimestamp())},
+ {"interval", Optional(f.Interval())},
+ {"decimal", Optional(f.Decimal(20, 10))},
+ {"json", Optional(f.Json())},
+ {"yson", Optional(f.Yson())},
+ {"uuid", Optional(f.Uuid())},
+ {"list", Optional(f.List(f.Bool()))},
+ {"dict", Optional(f.Dict(f.Bool(), f.Bool()))},
+ {"struct", Optional(f.Struct({}))},
+ {"tuple", Optional(f.Tuple({}))},
+ {"variant", Optional(f.Variant(f.Struct({{"x", f.Bool()}})))},
+ });
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYtSchema(type.Get()),
+ R"(
+ <strict=%true; unique_keys=%false>
+ [
+ {name=void; required=%false; type=any };
+ {name=null; required=%false; type=any };
+ {name=bool; required=%false; type=boolean };
+ {name=int8; required=%false; type=int8 };
+ {name=int16; required=%false; type=int16 };
+ {name=int32; required=%false; type=int32 };
+ {name=int64; required=%false; type=int64 };
+ {name=uint8; required=%false; type=uint8 };
+ {name=uint16; required=%false; type=uint16 };
+ {name=uint32; required=%false; type=uint32 };
+ {name=uint64; required=%false; type=uint64 };
+ {name=float; required=%false; type=double };
+ {name=double; required=%false; type=double };
+ {name=string; required=%false; type=string };
+ {name=utf8; required=%false; type=utf8 };
+ {name=date; required=%false; type=uint16 };
+ {name=datetime; required=%false; type=uint32 };
+ {name=timestamp; required=%false; type=uint64 };
+ {name=tzdate; required=%false; type=string };
+ {name=tzdatetime; required=%false; type=string };
+ {name=tztimestamp; required=%false; type=string };
+ {name=interval; required=%false; type=int64 };
+ {name=decimal; required=%false; type=string };
+ {name=json; required=%false; type=string };
+ {name=yson; required=%false; type=any };
+ {name=uuid; required=%false; type=string };
+ {name=list; required=%false; type=any };
+ {name=dict; required=%false; type=any };
+ {name=struct; required=%false; type=any };
+ {name=tuple; required=%false; type=any };
+ {name=variant; required=%false; type=any };
+ ]
+ )");
+ }
+
+ {
+ auto type = f.Struct({
+ {"void", Optional(Optional(f.Void()))},
+ {"null", Optional(Optional(f.Null()))},
+ {"bool", Optional(Optional(f.Bool()))},
+ {"int8", Optional(Optional(f.Int8()))},
+ {"int16", Optional(Optional(f.Int16()))},
+ {"int32", Optional(Optional(f.Int32()))},
+ {"int64", Optional(Optional(f.Int64()))},
+ {"uint8", Optional(Optional(f.Uint8()))},
+ {"uint16", Optional(Optional(f.Uint16()))},
+ {"uint32", Optional(Optional(f.Uint32()))},
+ {"uint64", Optional(Optional(f.Uint64()))},
+ {"float", Optional(Optional(f.Float()))},
+ {"double", Optional(Optional(f.Double()))},
+ {"string", Optional(Optional(f.String()))},
+ {"utf8", Optional(Optional(f.Utf8()))},
+ {"date", Optional(Optional(f.Date()))},
+ {"datetime", Optional(Optional(f.Datetime()))},
+ {"timestamp", Optional(Optional(f.Timestamp()))},
+ {"tzdate", Optional(Optional(f.TzDate()))},
+ {"tzdatetime", Optional(Optional(f.TzDatetime()))},
+ {"tztimestamp", Optional(Optional(f.TzTimestamp()))},
+ {"interval", Optional(Optional(f.Interval()))},
+ {"decimal", Optional(Optional(f.Decimal(20, 10)))},
+ {"json", Optional(Optional(f.Json()))},
+ {"yson", Optional(Optional(f.Yson()))},
+ {"uuid", Optional(Optional(f.Uuid()))},
+ {"list", Optional(Optional(f.List(f.Bool())))},
+ {"dict", Optional(Optional(f.Dict(f.Bool(), f.Bool())))},
+ {"struct", Optional(Optional(f.Struct({})))},
+ {"tuple", Optional(Optional(f.Tuple({})))},
+ {"variant", Optional(Optional(f.Variant(f.Struct({{"x", f.Bool()}}))))},
+ });
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYtSchema(type.Get()),
+ R"(
+ <strict=%true; unique_keys=%false>
+ [
+ {name=void; required=%false; type=any };
+ {name=null; required=%false; type=any };
+ {name=bool; required=%false; type=any };
+ {name=int8; required=%false; type=any };
+ {name=int16; required=%false; type=any };
+ {name=int32; required=%false; type=any };
+ {name=int64; required=%false; type=any };
+ {name=uint8; required=%false; type=any };
+ {name=uint16; required=%false; type=any };
+ {name=uint32; required=%false; type=any };
+ {name=uint64; required=%false; type=any };
+ {name=float; required=%false; type=any };
+ {name=double; required=%false; type=any };
+ {name=string; required=%false; type=any };
+ {name=utf8; required=%false; type=any };
+ {name=date; required=%false; type=any };
+ {name=datetime; required=%false; type=any };
+ {name=timestamp; required=%false; type=any };
+ {name=tzdate; required=%false; type=any };
+ {name=tzdatetime; required=%false; type=any };
+ {name=tztimestamp; required=%false; type=any };
+ {name=interval; required=%false; type=any };
+ {name=decimal; required=%false; type=any };
+ {name=json; required=%false; type=any };
+ {name=yson; required=%false; type=any };
+ {name=uuid; required=%false; type=any };
+ {name=list; required=%false; type=any };
+ {name=dict; required=%false; type=any };
+ {name=struct; required=%false; type=any };
+ {name=tuple; required=%false; type=any };
+ {name=variant; required=%false; type=any };
+ ]
+ )");
+ }
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ NTi::NIo::AsYtSchema(f.Void().Get());
+ }(),
+ NTi::TApiException, "expected a struct type");
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ NTi::NIo::AsYtSchema(Optional(f.Struct({})).Get());
+ }(),
+ NTi::TApiException, "expected a struct type");
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS([&f]() {
+ NTi::NIo::AsYtSchema(f.Struct({}).Get());
+ }(),
+ NTi::TApiException, "expected a non-empty struct");
+
+ ASSERT_YSON_EQ(
+ NTi::NIo::AsYtSchema(f.Struct({}).Get(), /* failOnEmptyf.Struct = */ false),
+ "<strict=%true; unique_keys=%false>[{name=_yql_fake_column; required=%false; type=boolean}]");
+}
diff --git a/library/cpp/type_info/ut/type_list.cpp b/library/cpp/type_info/ut/type_list.cpp
new file mode 100644
index 0000000000..56d9e16061
--- /dev/null
+++ b/library/cpp/type_info/ut/type_list.cpp
@@ -0,0 +1,64 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include <util/generic/serialized_enum.h>
+
+TEST(TypeList, PrimitiveTypeNameSequence) {
+ auto primitiveNames = GetEnumAllValues<NTi::EPrimitiveTypeName>();
+
+ ASSERT_GT(primitiveNames.size(), 0);
+
+ ASSERT_EQ(static_cast<i32>(primitiveNames[0]), 0);
+
+ for (size_t i = 0; i < primitiveNames.size() - 1; ++i) {
+ ASSERT_EQ(static_cast<i32>(primitiveNames[i]) + 1, static_cast<i32>(primitiveNames[i + 1]));
+ }
+}
+
+TEST(TypeList, PrimitiveTypeGroup) {
+ auto primitiveNames = GetEnumAllValues<NTi::EPrimitiveTypeName>();
+
+ for (auto typeName : primitiveNames) {
+ ASSERT_TRUE(NTi::IsPrimitive(static_cast<NTi::ETypeName>(typeName)));
+ }
+}
+
+TEST(TypeList, TypeNameInExactlyOneGroup) {
+ auto allNames = GetEnumAllValues<NTi::ETypeName>();
+
+ for (auto typeName : allNames) {
+ int groups = 0;
+ groups += NTi::IsPrimitive(typeName);
+ groups += NTi::IsSingular(typeName);
+ groups += NTi::IsContainer(typeName);
+ ASSERT_EQ(groups, 1);
+ }
+}
+
+TEST(TypeList, EnumCoherence) {
+ auto primitiveNames = GetEnumAllValues<NTi::EPrimitiveTypeName>();
+ auto allNames = GetEnumAllValues<NTi::ETypeName>();
+
+ ASSERT_GT(allNames.size(), primitiveNames.size());
+
+ size_t i = 0;
+
+ for (; i < primitiveNames.size(); ++i) {
+ ASSERT_EQ(static_cast<i32>(primitiveNames[i]), static_cast<i32>(allNames[i]));
+ ASSERT_EQ(ToString(primitiveNames[i]), ToString(allNames[i]));
+ ASSERT_TRUE(NTi::IsPrimitive(allNames[i]));
+ }
+
+ for (; i < allNames.size(); ++i) {
+ ASSERT_FALSE(NTi::IsPrimitive(allNames[i]));
+ }
+}
+
+TEST(TypeList, Cast) {
+ auto primitiveNames = GetEnumAllValues<NTi::EPrimitiveTypeName>();
+
+ for (auto typeName : primitiveNames) {
+ ASSERT_EQ(typeName, NTi::ToPrimitiveTypeName(NTi::ToTypeName(typeName)));
+ }
+}
diff --git a/library/cpp/type_info/ut/type_serialize.cpp b/library/cpp/type_info/ut/type_serialize.cpp
new file mode 100644
index 0000000000..0d7e184eca
--- /dev/null
+++ b/library/cpp/type_info/ut/type_serialize.cpp
@@ -0,0 +1,251 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST(TypeSerialize, Void) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Void().Get()), R"(void)");
+}
+
+TEST(TypeSerialize, Null) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Null().Get()), R"(null)");
+}
+
+TEST(TypeSerialize, Bool) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Bool().Get()), R"(bool)");
+}
+
+TEST(TypeSerialize, Int8) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Int8().Get()), R"(int8)");
+}
+
+TEST(TypeSerialize, Int16) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Int16().Get()), R"(int16)");
+}
+
+TEST(TypeSerialize, Int32) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Int32().Get()), R"(int32)");
+}
+
+TEST(TypeSerialize, Int64) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Int64().Get()), R"(int64)");
+}
+
+TEST(TypeSerialize, Uint8) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Uint8().Get()), R"(uint8)");
+}
+
+TEST(TypeSerialize, Uint16) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Uint16().Get()), R"(uint16)");
+}
+
+TEST(TypeSerialize, Uint32) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Uint32().Get()), R"(uint32)");
+}
+
+TEST(TypeSerialize, Uint64) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Uint64().Get()), R"(uint64)");
+}
+
+TEST(TypeSerialize, Float) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Float().Get()), R"(float)");
+}
+
+TEST(TypeSerialize, Double) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Double().Get()), R"(double)");
+}
+
+TEST(TypeSerialize, String) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::String().Get()), R"(string)");
+}
+
+TEST(TypeSerialize, Utf8) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Utf8().Get()), R"(utf8)");
+}
+
+TEST(TypeSerialize, Date) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Date().Get()), R"(date)");
+}
+
+TEST(TypeSerialize, Datetime) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Datetime().Get()), R"(datetime)");
+}
+
+TEST(TypeSerialize, Timestamp) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Timestamp().Get()), R"(timestamp)");
+}
+
+TEST(TypeSerialize, TzDate) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::TzDate().Get()), R"(tz_date)");
+}
+
+TEST(TypeSerialize, TzDatetime) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::TzDatetime().Get()), R"(tz_datetime)");
+}
+
+TEST(TypeSerialize, TzTimestamp) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::TzTimestamp().Get()), R"(tz_timestamp)");
+}
+
+TEST(TypeSerialize, Interval) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Interval().Get()), R"(interval)");
+}
+
+TEST(TypeSerialize, Decimal) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Decimal(20, 10).Get()), R"({type_name=decimal; precision=20; scale=10})");
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Decimal(10, 10).Get()), R"({type_name=decimal; precision=10; scale=10})");
+}
+
+TEST(TypeSerialize, Json) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Json().Get()), R"(json)");
+}
+
+TEST(TypeSerialize, Yson) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Yson().Get()), R"(yson)");
+}
+
+TEST(TypeSerialize, Uuid) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Uuid().Get()), R"(uuid)");
+}
+
+TEST(TypeSerialize, Optional) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Optional(NTi::Void()).Get()), R"({type_name=optional; item=void})");
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Optional(NTi::String()).Get()), R"({type_name=optional; item=string})");
+}
+
+TEST(TypeSerialize, List) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::List(NTi::Void()).Get()), R"({type_name=list; item=void})");
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::List(NTi::String()).Get()), R"({type_name=list; item=string})");
+}
+
+TEST(TypeSerialize, Dict) {
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Dict(NTi::Void(), NTi::Void()).Get()), R"({type_name=dict; key=void; value=void})");
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(NTi::Dict(NTi::Int32(), NTi::String()).Get()), R"({type_name=dict; key=int32; value=string})");
+}
+
+TEST(TypeSerialize, StructEmpty) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Struct({}).Get()),
+ R"({type_name=struct; members=[]})");
+}
+
+TEST(TypeSerialize, Struct) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Struct({{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}}).Get()),
+ R"({type_name=struct; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeSerialize, StructNamedEmpty) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Struct("S", {}).Get()),
+ R"({type_name=struct; members=[]})");
+}
+
+TEST(TypeSerialize, StructNamedEmptyNoNames) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Struct("S", {}).Get(),
+ /* binary = */ false,
+ /* includeTags = */ true),
+ R"({type_name=struct; members=[]})");
+}
+
+TEST(TypeSerialize, StructNamed) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Struct("S", {{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}}).Get()),
+ R"({type_name=struct; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeSerialize, TupleEmpty) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tuple({}).Get()),
+ R"({type_name=tuple; elements=[]})");
+}
+
+TEST(TypeSerialize, Tuple) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tuple({{NTi::String()}, {NTi::List(NTi::Int64())}}).Get()),
+ R"({type_name=tuple; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeSerialize, TupleNamedEmpty) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tuple("S", {}).Get()),
+ R"({type_name=tuple; elements=[]})");
+}
+
+TEST(TypeSerialize, TupleNamedEmptyNoNames) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tuple("S", {}).Get(),
+ /* binary = */ false),
+ R"({type_name=tuple; elements=[]})");
+}
+
+TEST(TypeSerialize, VariantStruct) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Variant(
+ NTi::Struct({{"ItemB", NTi::String()}, {"ItemA", NTi::List(NTi::Int64())}}))
+ .Get()),
+ R"({type_name=variant; members=[{name=ItemB; type=string}; {name=ItemA; type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeSerialize, VariantTuple) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Variant(
+ NTi::Tuple({
+ {NTi::String()},
+ {NTi::List(NTi::Int64())},
+ }))
+ .Get()),
+ R"({type_name=variant; elements=[{type=string}; {type={type_name=list; item=int64}}]})");
+}
+
+TEST(TypeSerialize, Tagged) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tagged(NTi::String(), "Url").Get()),
+ R"({type_name=tagged; tag=Url; item=string})");
+}
+
+TEST(TypeSerialize, TaggedNoTags) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tagged(NTi::String(), "Url").Get(),
+ /* binary = */ false,
+ /* includeTags = */ false),
+ R"(string)");
+}
+
+TEST(TypeSerialize, MultipleType) {
+ auto result = TString();
+
+ {
+ auto writer = NYsonPull::MakeTextWriter(NYsonPull::NOutput::FromString(&result), NYsonPull::EStreamType::ListFragment);
+
+ writer.BeginStream();
+ NTi::NIo::SerializeYsonMultiple(NTi::Optional(NTi::String()).Get(), writer.GetConsumer());
+ NTi::NIo::SerializeYsonMultiple(NTi::List(NTi::Utf8()).Get(), writer.GetConsumer());
+ writer.EndStream();
+ }
+
+ ASSERT_YSON_EQ("[" + result + "]", R"([{type_name=optional; item=string}; {type_name=list; item=utf8}])");
+}
+
+TEST(TypeSerialize, Binary) {
+ ASSERT_YSON_EQ(
+ NTi::NIo::SerializeYson(
+ NTi::Tagged(NTi::String(), "Url").Get(),
+ /* binary = */ true),
+ R"({type_name=tagged; tag=Url; item=string})");
+}
diff --git a/library/cpp/type_info/ut/type_show.cpp b/library/cpp/type_info/ut/type_show.cpp
new file mode 100644
index 0000000000..cde0d9371a
--- /dev/null
+++ b/library/cpp/type_info/ut/type_show.cpp
@@ -0,0 +1,199 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeShow, Void) {
+ ASSERT_EQ(ToString(*f.Void()), "Void");
+}
+
+TEST_TF(TypeShow, Null) {
+ ASSERT_EQ(ToString(*f.Null()), "Null");
+}
+
+TEST_TF(TypeShow, Bool) {
+ ASSERT_EQ(ToString(*f.Bool()), "Bool");
+}
+
+TEST_TF(TypeShow, Int8) {
+ ASSERT_EQ(ToString(*f.Int8()), "Int8");
+}
+
+TEST_TF(TypeShow, Int16) {
+ ASSERT_EQ(ToString(*f.Int16()), "Int16");
+}
+
+TEST_TF(TypeShow, Int32) {
+ ASSERT_EQ(ToString(*f.Int32()), "Int32");
+}
+
+TEST_TF(TypeShow, Int64) {
+ ASSERT_EQ(ToString(*f.Int64()), "Int64");
+}
+
+TEST_TF(TypeShow, Uint8) {
+ ASSERT_EQ(ToString(*f.Uint8()), "Uint8");
+}
+
+TEST_TF(TypeShow, Uint16) {
+ ASSERT_EQ(ToString(*f.Uint16()), "Uint16");
+}
+
+TEST_TF(TypeShow, Uint32) {
+ ASSERT_EQ(ToString(*f.Uint32()), "Uint32");
+}
+
+TEST_TF(TypeShow, Uint64) {
+ ASSERT_EQ(ToString(*f.Uint64()), "Uint64");
+}
+
+TEST_TF(TypeShow, Float) {
+ ASSERT_EQ(ToString(*f.Float()), "Float");
+}
+
+TEST_TF(TypeShow, Double) {
+ ASSERT_EQ(ToString(*f.Double()), "Double");
+}
+
+TEST_TF(TypeShow, String) {
+ ASSERT_EQ(ToString(*f.String()), "String");
+}
+
+TEST_TF(TypeShow, Utf8) {
+ ASSERT_EQ(ToString(*f.Utf8()), "Utf8");
+}
+
+TEST_TF(TypeShow, Date) {
+ ASSERT_EQ(ToString(*f.Date()), "Date");
+}
+
+TEST_TF(TypeShow, Datetime) {
+ ASSERT_EQ(ToString(*f.Datetime()), "Datetime");
+}
+
+TEST_TF(TypeShow, Timestamp) {
+ ASSERT_EQ(ToString(*f.Timestamp()), "Timestamp");
+}
+
+TEST_TF(TypeShow, TzDate) {
+ ASSERT_EQ(ToString(*f.TzDate()), "TzDate");
+}
+
+TEST_TF(TypeShow, TzDatetime) {
+ ASSERT_EQ(ToString(*f.TzDatetime()), "TzDatetime");
+}
+
+TEST_TF(TypeShow, TzTimestamp) {
+ ASSERT_EQ(ToString(*f.TzTimestamp()), "TzTimestamp");
+}
+
+TEST_TF(TypeShow, Interval) {
+ ASSERT_EQ(ToString(*f.Interval()), "Interval");
+}
+
+TEST_TF(TypeShow, Decimal) {
+ ASSERT_EQ(ToString(*f.Decimal(20, 10)), "Decimal(20, 10)");
+}
+
+TEST_TF(TypeShow, Json) {
+ ASSERT_EQ(ToString(*f.Json()), "Json");
+}
+
+TEST_TF(TypeShow, Yson) {
+ ASSERT_EQ(ToString(*f.Yson()), "Yson");
+}
+
+TEST_TF(TypeShow, Uuid) {
+ ASSERT_EQ(ToString(*f.Uuid()), "Uuid");
+}
+
+TEST_TF(TypeShow, Optional) {
+ ASSERT_EQ(ToString(*f.Optional(f.String())), "Optional<String>");
+}
+
+TEST_TF(TypeShow, List) {
+ ASSERT_EQ(ToString(*f.List(f.String())), "List<String>");
+}
+
+TEST_TF(TypeShow, Dict) {
+ ASSERT_EQ(ToString(*f.Dict(f.String(), f.Int32())), "Dict<String, Int32>");
+}
+
+TEST_TF(TypeShow, Struct) {
+ ASSERT_EQ(
+ ToString(*f.Struct({})),
+ "Struct<>");
+ ASSERT_EQ(
+ ToString(*f.Struct({{"x1", f.Void()}})),
+ "Struct<'x1': Void>");
+ ASSERT_EQ(
+ ToString(*f.Struct({{"x1", f.Void()}, {"x2", f.String()}})),
+ "Struct<'x1': Void, 'x2': String>");
+ ASSERT_EQ(
+ ToString(*f.Struct("Name", {})),
+ "Struct['Name']<>");
+ ASSERT_EQ(
+ ToString(*f.Struct("Name", {{"x1", f.Void()}})),
+ "Struct['Name']<'x1': Void>");
+ ASSERT_EQ(
+ ToString(*f.Struct("Name", {{"x1", f.Void()}, {"x2", f.String()}})),
+ "Struct['Name']<'x1': Void, 'x2': String>");
+}
+
+TEST_TF(TypeShow, Tuple) {
+ ASSERT_EQ(
+ ToString(*f.Tuple({})),
+ "Tuple<>");
+ ASSERT_EQ(
+ ToString(*f.Tuple({{f.Void()}})),
+ "Tuple<Void>");
+ ASSERT_EQ(
+ ToString(*f.Tuple({{f.Void()}, {f.String()}})),
+ "Tuple<Void, String>");
+ ASSERT_EQ(
+ ToString(*f.Tuple("Name", {})),
+ "Tuple['Name']<>");
+ ASSERT_EQ(
+ ToString(*f.Tuple("Name", {{f.Void()}})),
+ "Tuple['Name']<Void>");
+ ASSERT_EQ(
+ ToString(*f.Tuple("Name", {{f.Void()}, {f.String()}})),
+ "Tuple['Name']<Void, String>");
+}
+
+TEST_TF(TypeShow, VariantStruct) {
+ ASSERT_EQ(
+ ToString(*f.Variant(f.Struct({{"x1", f.Void()}}))),
+ "Variant<'x1': Void>");
+ ASSERT_EQ(
+ ToString(*f.Variant(f.Struct({{"x1", f.Void()}, {"x2", f.String()}}))),
+ "Variant<'x1': Void, 'x2': String>");
+ ASSERT_EQ(
+ ToString(*f.Variant("Name", f.Struct({{"x1", f.Void()}}))),
+ "Variant['Name']<'x1': Void>");
+ ASSERT_EQ(
+ ToString(*f.Variant("Name", f.Struct({{"x1", f.Void()}, {"x2", f.String()}}))),
+ "Variant['Name']<'x1': Void, 'x2': String>");
+}
+
+TEST_TF(TypeShow, VariantTuple) {
+ ASSERT_EQ(
+ ToString(*f.Variant(f.Tuple({{f.Void()}}))),
+ "Variant<Void>");
+ ASSERT_EQ(
+ ToString(*f.Variant(f.Tuple({{f.Void()}, {f.String()}}))),
+ "Variant<Void, String>");
+ ASSERT_EQ(
+ ToString(*f.Variant("Name", f.Tuple({{f.Void()}}))),
+ "Variant['Name']<Void>");
+ ASSERT_EQ(
+ ToString(*f.Variant("Name", f.Tuple({{f.Void()}, {f.String()}}))),
+ "Variant['Name']<Void, String>");
+}
+
+TEST_TF(TypeShow, Tagged) {
+ ASSERT_EQ(
+ ToString(*f.Tagged(f.Void(), "Tag")),
+ "Tagged<Void, 'Tag'>");
+}
diff --git a/library/cpp/type_info/ut/type_strip_tags.cpp b/library/cpp/type_info/ut/type_strip_tags.cpp
new file mode 100644
index 0000000000..b3ef4c0825
--- /dev/null
+++ b/library/cpp/type_info/ut/type_strip_tags.cpp
@@ -0,0 +1,31 @@
+#include <library/cpp/testing/unittest/gtest.h>
+
+#include <library/cpp/type_info/type_info.h>
+
+#include "utils.h"
+
+TEST_TF(TypeStripTags, StripTags) {
+ auto t = f.List(f.Tagged(f.Void(), "Tag"));
+
+ ASSERT_EQ(t->StripTags().Get(), t.Get());
+ ASSERT_EQ(f.Tagged(t, "Tag")->StripTags().Get(), t.Get());
+ ASSERT_EQ(f.Tagged(f.Tagged(t, "Tag"), "Tag2")->StripTags().Get(), t.Get());
+}
+
+TEST_TF(TypeStripTags, StripOptionals) {
+ auto t = f.Tagged(f.Optional(f.Void()), "Tag");
+
+ ASSERT_EQ(t->StripOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(t)->StripOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(f.Optional(t))->StripOptionals().Get(), t.Get());
+}
+
+TEST_TF(TypeStripTags, StripTagsAndOptionals) {
+ auto t = f.Void();
+
+ ASSERT_EQ(t->StripTagsAndOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(t)->StripTagsAndOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(f.Optional(t))->StripTagsAndOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(f.Tagged(t, "Tag"))->StripTagsAndOptionals().Get(), t.Get());
+ ASSERT_EQ(f.Optional(f.Optional(f.Optional(t)))->StripTagsAndOptionals().Get(), t.Get());
+}
diff --git a/library/cpp/type_info/ut/utils.h b/library/cpp/type_info/ut/utils.h
new file mode 100644
index 0000000000..b35316cb2f
--- /dev/null
+++ b/library/cpp/type_info/ut/utils.h
@@ -0,0 +1,73 @@
+#pragma once
+
+//! @file utils.h
+//!
+//! Infrastructure for running type info tests with different factories and settings.
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <library/cpp/yson/consumer.h>
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/node_builder.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/type_info/fwd.h>
+
+template <typename T>
+inline TString ToCanonicalYson(const T& value) {
+ return NYT::NodeToCanonicalYsonString(NYT::NodeFromYsonString(TStringBuf(value)));
+}
+
+/// Assert that two YSON strings are equal.
+#define ASSERT_YSON_EQ(L, R) ASSERT_EQ(ToCanonicalYson(L), ToCanonicalYson(R))
+
+/// Assert that two types are strictly equal.
+#define ASSERT_STRICT_EQ(a, b) \
+ do { \
+ auto lhs = (a); \
+ auto rhs = (b); \
+ ASSERT_TRUE(NTi::NEq::TStrictlyEqual()(lhs, rhs)); \
+ ASSERT_TRUE(NTi::NEq::TStrictlyEqual().IgnoreHash(lhs, rhs)); \
+ ASSERT_EQ(lhs->GetHash(), NTi::NEq::TStrictlyEqualHash()(lhs)); \
+ ASSERT_EQ(rhs->GetHash(), NTi::NEq::TStrictlyEqualHash()(rhs)); \
+ ASSERT_EQ(lhs->GetHash(), rhs->GetHash()); \
+ } while (false)
+
+/// Assert that two types are strictly unequal.
+///
+/// Note: we check that, if types are not equal, their hashes are also not equal.
+/// While this is not guaranteed, we haven't seen any collisions so far.
+/// If some collision happen, check if hashing isn't broken before removing the assert.
+#define ASSERT_STRICT_NE(a, b) \
+ do { \
+ auto lhs = (a); \
+ auto rhs = (b); \
+ ASSERT_FALSE(NTi::NEq::TStrictlyEqual()(lhs, rhs)); \
+ ASSERT_FALSE(NTi::NEq::TStrictlyEqual().IgnoreHash(lhs, rhs)); \
+ ASSERT_EQ(lhs->GetHash(), NTi::NEq::TStrictlyEqualHash()(lhs)); \
+ ASSERT_EQ(rhs->GetHash(), NTi::NEq::TStrictlyEqualHash()(rhs)); \
+ ASSERT_NE(lhs->GetHash(), rhs->GetHash()); \
+ } while (false)
+
+/// Assert that a type string is equal to the given type after deserialization.
+#define ASSERT_DESERIALIZED_EQ(canonicalType, serializedType) \
+ do { \
+ auto reader = NYsonPull::TReader(NYsonPull::NInput::FromMemory(serializedType), NYsonPull::EStreamType::Node); \
+ auto deserializedType = NTi::NIo::DeserializeYson(*NTi::HeapFactory(), reader); \
+ ASSERT_STRICT_EQ(deserializedType, canonicalType); \
+ ASSERT_YSON_EQ(NTi::NIo::SerializeYson(canonicalType.Get()), NTi::NIo::SerializeYson(deserializedType.Get())); \
+ } while (false)
+
+/// Test parametrized over different type factories.
+#define TEST_TF(N, NN) \
+ void Test##N##NN(NTi::ITypeFactory& f); \
+ TEST(N, NN##_Heap) { \
+ Test##N##NN(*NTi::HeapFactory()); \
+ } \
+ TEST(N, NN##_Pool) { \
+ Test##N##NN(*NTi::PoolFactory(false)); \
+ } \
+ TEST(N, NN##_PoolDedup) { \
+ Test##N##NN(*NTi::PoolFactory(true)); \
+ } \
+ void Test##N##NN(NTi::ITypeFactory& f)
diff --git a/library/cpp/type_info/ut/ya.make b/library/cpp/type_info/ut/ya.make
new file mode 100644
index 0000000000..dc27e2a7db
--- /dev/null
+++ b/library/cpp/type_info/ut/ya.make
@@ -0,0 +1,32 @@
+UNITTEST()
+
+SRCS(
+ builder.cpp
+ type_basics.cpp
+ type_complexity_ut.cpp
+ type_constraints.cpp
+ type_deserialize.cpp
+ type_equivalence.cpp
+ type_factory.cpp
+ type_factory_raw.cpp
+ type_io.cpp
+ type_list.cpp
+ type_serialize.cpp
+ type_show.cpp
+ type_strip_tags.cpp
+ test_data.cpp
+)
+
+PEERDIR(
+ library/cpp/type_info
+ library/cpp/yson
+ library/cpp/yson/node
+ library/cpp/resource
+)
+
+RESOURCE(
+ ${ARCADIA_ROOT}/library/cpp/type_info/test-data/good-types.txt /good
+ ${ARCADIA_ROOT}/library/cpp/type_info/test-data/bad-types.txt /bad
+)
+
+END()
diff --git a/library/cpp/type_info/ya.make b/library/cpp/type_info/ya.make
new file mode 100644
index 0000000000..555f0d1433
--- /dev/null
+++ b/library/cpp/type_info/ya.make
@@ -0,0 +1,26 @@
+LIBRARY()
+
+SRCS(
+ type_info.cpp
+
+ builder.cpp
+ error.cpp
+ type.cpp
+ type_complexity.cpp
+ type_equivalence.cpp
+ type_factory.cpp
+ type_io.cpp
+ type_list.cpp
+)
+
+GENERATE_ENUM_SERIALIZATION(
+ type_list.h
+)
+
+PEERDIR(
+ library/cpp/yson_pull
+)
+
+END()
+
+RECURSE_FOR_TESTS(ut)
diff --git a/library/cpp/yson_pull/bridge.h b/library/cpp/yson_pull/bridge.h
new file mode 100644
index 0000000000..ac767dcba0
--- /dev/null
+++ b/library/cpp/yson_pull/bridge.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "consumer.h"
+#include "event.h"
+#include "writer.h"
+
+namespace NYsonPull {
+ //! \brief Connect YSON stream producer and consumer.
+ //!
+ //! Useful for writing YSON stream filters.
+ //! \p Producer must have a \p next_event() method (like \p NYsonPull::reader).
+ //! \p Consumer must be like \p NYsonPull::consumer interface.
+ template <typename Producer, typename Consumer>
+ inline void Bridge(Producer&& producer, Consumer&& consumer) {
+ for (;;) {
+ auto& event = producer.NextEvent();
+ consumer.OnEvent(event);
+ if (event.Type() == EEventType::EndStream) {
+ break;
+ }
+ }
+ }
+
+ template <typename Producer>
+ inline void Bridge(Producer&& producer, TWriter& writer_) {
+ Bridge(std::forward<Producer>(producer), writer_.GetConsumer());
+ }
+
+ template <typename Producer>
+ inline void Bridge(Producer&& producer, TWriter&& writer_) {
+ Bridge(std::forward<Producer>(producer), writer_.GetConsumer());
+ }
+
+}
diff --git a/library/cpp/yson_pull/yson.h b/library/cpp/yson_pull/yson.h
new file mode 100644
index 0000000000..a77eaa5c94
--- /dev/null
+++ b/library/cpp/yson_pull/yson.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "bridge.h"
+#include "consumer.h"
+#include "event.h"
+#include "exceptions.h"
+#include "input.h"
+#include "output.h"
+#include "position_info.h"
+#include "range.h"
+#include "reader.h"
+#include "scalar.h"
+#include "stream_type.h"
+#include "writer.h"
diff --git a/library/cpp/yt/backtrace/backtrace-inl.h b/library/cpp/yt/backtrace/backtrace-inl.h
new file mode 100644
index 0000000000..b78eeffd75
--- /dev/null
+++ b/library/cpp/yt/backtrace/backtrace-inl.h
@@ -0,0 +1,36 @@
+#pragma once
+#ifndef BACKTRACE_INL_H_
+#error "Direct inclusion of this file is not allowed, include backtrace.h"
+// For the sake of sane code completion.
+#include "backtrace.h"
+#endif
+
+#include <util/system/compiler.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TCursor>
+Y_NO_INLINE TBacktrace GetBacktrace(
+ TCursor* cursor,
+ TBacktraceBuffer buffer,
+ int framesToSkip)
+{
+ // Account for the current frame.
+ ++framesToSkip;
+ size_t frameCount = 0;
+ while (frameCount < buffer.size() && !cursor->IsFinished()) {
+ if (framesToSkip > 0) {
+ --framesToSkip;
+ } else {
+ buffer[frameCount++] = cursor->GetCurrentIP();
+ }
+ cursor->MoveNext();
+ }
+ return {buffer.begin(), frameCount};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/backtrace.cpp b/library/cpp/yt/backtrace/backtrace.cpp
new file mode 100644
index 0000000000..153a0a5dd0
--- /dev/null
+++ b/library/cpp/yt/backtrace/backtrace.cpp
@@ -0,0 +1,18 @@
+#include "backtrace.h"
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString SymbolizeBacktrace(TBacktrace backtrace)
+{
+ TString result;
+ SymbolizeBacktrace(
+ backtrace,
+ [&] (TStringBuf str) { result += str; });
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/backtrace.h b/library/cpp/yt/backtrace/backtrace.h
new file mode 100644
index 0000000000..ea70d9558c
--- /dev/null
+++ b/library/cpp/yt/backtrace/backtrace.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <library/cpp/yt/memory/range.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TBacktrace = TRange<const void*>;
+using TBacktraceBuffer = TMutableRange<const void*>;
+
+//! Obtains a backtrace via a given cursor.
+/*!
+ * \param buffer is the buffer where the backtrace is written to
+ * \param framesToSkip is the number of top frames to skip
+ * \returns the portion of #buffer that has actually been filled
+ */
+template <class TCursor>
+TBacktrace GetBacktrace(
+ TCursor* cursor,
+ TBacktraceBuffer buffer,
+ int framesToSkip);
+
+//! Symbolizes a backtrace invoking a given callback for each frame.
+/*!
+ * \param backtrace Backtrace to symbolize
+ * \param frameCallback Callback to invoke per each frame
+ */
+void SymbolizeBacktrace(
+ TBacktrace backtrace,
+ const std::function<void(TStringBuf)>& frameCallback);
+
+//! Symbolizes a backtrace to a string.
+/*!
+ * \param backtrace Backtrace to symbolize
+ */
+TString SymbolizeBacktrace(TBacktrace backtrace);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
+
+#define BACKTRACE_INL_H_
+#include "backtrace-inl.h"
+#undef BACKTRACE_INL_H_
diff --git a/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.cpp b/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.cpp
new file mode 100644
index 0000000000..ea6e0bc08e
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.cpp
@@ -0,0 +1,22 @@
+#include "dummy_cursor.h"
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TDummyCursor::IsFinished() const
+{
+ return true;
+}
+
+const void* TDummyCursor::GetCurrentIP() const
+{
+ return nullptr;
+}
+
+void TDummyCursor::MoveNext()
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.h b/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.h
new file mode 100644
index 0000000000..b47d7d2aba
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.h
@@ -0,0 +1,17 @@
+#pragma once
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDummyCursor
+{
+public:
+ bool IsFinished() const;
+ const void* GetCurrentIP() const;
+ void MoveNext();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/dummy/ya.make b/library/cpp/yt/backtrace/cursors/dummy/ya.make
new file mode 100644
index 0000000000..49fd7be050
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/dummy/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ dummy_cursor.cpp
+)
+
+END()
diff --git a/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.cpp b/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.cpp
new file mode 100644
index 0000000000..290d30c3ce
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.cpp
@@ -0,0 +1,146 @@
+#include "frame_pointer_cursor.h"
+
+#include <util/generic/size_literals.h>
+
+#include <array>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFramePointerCursor::TFramePointerCursor(
+ TSafeMemoryReader* memoryReader,
+ const TFramePointerCursorContext& context)
+ : MemoryReader_(memoryReader)
+ , Rip_(reinterpret_cast<const void*>(context.Rip))
+ , Rbp_(reinterpret_cast<const void*>(context.Rbp))
+ , StartRsp_(reinterpret_cast<const void*>(context.Rsp))
+{ }
+
+bool TFramePointerCursor::IsFinished() const
+{
+ return Finished_;
+}
+
+const void* TFramePointerCursor::GetCurrentIP() const
+{
+ return Rip_;
+}
+
+void TFramePointerCursor::MoveNext()
+{
+ if (Finished_) {
+ return;
+ }
+
+ auto add = [] (auto ptr, auto delta) {
+ return reinterpret_cast<void*>(reinterpret_cast<intptr_t>(ptr) + delta);
+ };
+
+ auto checkPtr = [&] (auto ptr) {
+ ui8 data;
+ return MemoryReader_->Read(ptr, &data);
+ };
+
+ // We try unwinding stack manually by following frame pointers.
+ //
+ // We assume that stack does not span more than 4mb.
+
+ if (First_) {
+ First_ = false;
+
+ // For the first frame there are three special cases where naive
+ // unwinding would skip the caller frame.
+ //
+ // 1) Right after call instruction, rbp points to frame of a caller.
+ // 2) Right after "push rbp" instruction.
+ // 3) Right before ret instruction, rbp points to frame of a caller.
+ //
+ // We read current instruction and try to detect such cases.
+ //
+ // 55 push %rbp
+ // 48 89 e5 mov %rsp, %rbp
+ // c3 retq
+
+ std::array<ui8, 3> data;
+ if (!MemoryReader_->Read(Rip_, &data)) {
+ Finished_ = true;
+ return;
+ }
+
+ if (data[0] == 0xc3 || data[0] == 0x55) {
+ void* savedRip;
+ if (!MemoryReader_->Read(StartRsp_, &savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ // Avoid infinite loop.
+ if (Rip_ == savedRip) {
+ Finished_ = true;
+ return;
+ }
+
+ // Detect garbage pointer.
+ if (!checkPtr(savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ Rip_ = savedRip;
+ return;
+ }
+
+ if (data[0] == 0x48 && data[1] == 0x89 && data[2] == 0xe5) {
+ void* savedRip;
+ if (!MemoryReader_->Read(add(StartRsp_, 8), &savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ // Avoid infinite loop.
+ if (Rip_ == savedRip) {
+ Finished_ = true;
+ return;
+ }
+
+ // Detect garbage pointer.
+ if (!checkPtr(savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ Rip_ = savedRip;
+ return;
+ }
+ }
+
+ const void* savedRbp;
+ const void* savedRip;
+ if (!MemoryReader_->Read(Rbp_, &savedRbp) || !MemoryReader_->Read(add(Rbp_, 8), &savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ if (!checkPtr(savedRbp)) {
+ Finished_ = true;
+ return;
+ }
+
+ if (!checkPtr(savedRip)) {
+ Finished_ = true;
+ return;
+ }
+
+ if (savedRbp < StartRsp_ || savedRbp > add(StartRsp_, 4_MB)) {
+ Finished_ = true;
+ return;
+ }
+
+ Rip_ = savedRip;
+ Rbp_ = savedRbp;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h b/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h
new file mode 100644
index 0000000000..7a6eaf431b
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <library/cpp/yt/memory/safe_memory_reader.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFramePointerCursorContext
+{
+ ui64 Rip;
+ ui64 Rsp;
+ ui64 Rbp;
+};
+
+class TFramePointerCursor
+{
+public:
+ TFramePointerCursor(
+ TSafeMemoryReader* memoryReader,
+ const TFramePointerCursorContext& context);
+
+ bool IsFinished() const;
+ const void* GetCurrentIP() const;
+ void MoveNext();
+
+private:
+ TSafeMemoryReader* MemoryReader_;
+ bool Finished_ = false;
+ bool First_ = true;
+
+ const void* Rip_ = nullptr;
+ const void* Rbp_ = nullptr;
+ const void* StartRsp_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/frame_pointer/ya.make b/library/cpp/yt/backtrace/cursors/frame_pointer/ya.make
new file mode 100644
index 0000000000..cb85d70315
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/frame_pointer/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ frame_pointer_cursor.cpp
+)
+
+END()
diff --git a/library/cpp/yt/backtrace/cursors/interop/interop.cpp b/library/cpp/yt/backtrace/cursors/interop/interop.cpp
new file mode 100644
index 0000000000..b4e6cfbe6e
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/interop/interop.cpp
@@ -0,0 +1,102 @@
+#include "interop.h"
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFramePointerCursorContext FramePointerCursorContextFromUcontext(const ucontext_t& ucontext)
+{
+#if defined(_linux_)
+ return {
+ .Rip = static_cast<ui64>(ucontext.uc_mcontext.gregs[REG_RIP]),
+ .Rsp = static_cast<ui64>(ucontext.uc_mcontext.gregs[REG_RSP]),
+ .Rbp = static_cast<ui64>(ucontext.uc_mcontext.gregs[REG_RBP]),
+ };
+#elif defined(_darwin_)
+ return {
+ .Rip = static_cast<ui64>(ucontext.uc_mcontext->__ss.__rip),
+ .Rsp = static_cast<ui64>(ucontext.uc_mcontext->__ss.__rsp),
+ .Rbp = static_cast<ui64>(ucontext.uc_mcontext->__ss.__rbp),
+ };
+#else
+ #error Unsupported platform
+#endif
+}
+
+std::optional<unw_context_t> TrySynthesizeLibunwindContextFromMachineContext(
+ const TContMachineContext& machineContext)
+{
+ unw_context_t unwindContext;
+ if (unw_getcontext(&unwindContext) != 0) {
+ return {};
+ }
+
+ // Some dirty hacks follow.
+ struct TUnwindContextRegisters
+ {
+ ui64 Rax;
+ ui64 Rbx;
+ ui64 Rcx;
+ ui64 Rdx;
+ ui64 Rdi;
+ ui64 Rsi;
+ ui64 Rbp;
+ ui64 Rsp;
+ ui64 R8;
+ ui64 R9;
+ ui64 R10;
+ ui64 R11;
+ ui64 R12;
+ ui64 R13;
+ ui64 R14;
+ ui64 R15;
+ ui64 Rip;
+ ui64 Rflags;
+ ui64 CS;
+ ui64 FS;
+ ui64 GS;
+ };
+
+ struct TMachineContextRegisters
+ {
+ ui64 Rbx;
+ ui64 Rbp;
+ ui64 R12;
+ ui64 R13;
+ ui64 R14;
+ ui64 R15;
+ ui64 Rsp;
+ ui64 Rip;
+ };
+
+ static_assert(sizeof(TContMachineContext) >= sizeof(TMachineContextRegisters));
+ static_assert(sizeof(unw_context_t) >= sizeof(TUnwindContextRegisters));
+ const auto* machineContextRegisters = reinterpret_cast<const TMachineContextRegisters*>(&machineContext);
+ auto* unwindContextRegisters = reinterpret_cast<TUnwindContextRegisters*>(&unwindContext);
+ #define XX(register) unwindContextRegisters->register = machineContextRegisters->register;
+ XX(Rbx)
+ XX(Rbp)
+ XX(R12)
+ XX(R13)
+ XX(R14)
+ XX(R15)
+ XX(Rsp)
+ XX(Rip)
+ #undef XX
+ return unwindContext;
+}
+
+TFramePointerCursorContext FramePointerCursorContextFromLibunwindCursor(
+ const unw_cursor_t& cursor)
+{
+ TFramePointerCursorContext context{};
+ auto& mutableCursor = const_cast<unw_cursor_t&>(cursor);
+ YT_VERIFY(unw_get_reg(&mutableCursor, UNW_REG_IP, &context.Rip) == 0);
+ YT_VERIFY(unw_get_reg(&mutableCursor, UNW_X86_64_RSP, &context.Rsp) == 0);
+ YT_VERIFY(unw_get_reg(&mutableCursor, UNW_X86_64_RBP, &context.Rbp) == 0);
+ return context;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/interop/interop.h b/library/cpp/yt/backtrace/cursors/interop/interop.h
new file mode 100644
index 0000000000..62e7177107
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/interop/interop.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h>
+
+#include <contrib/libs/libunwind/include/libunwind.h>
+
+#include <util/system/context.h>
+
+#include <optional>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFramePointerCursorContext FramePointerCursorContextFromUcontext(const ucontext_t& ucontext);
+
+std::optional<unw_context_t> TrySynthesizeLibunwindContextFromMachineContext(
+ const TContMachineContext& machineContext);
+
+TFramePointerCursorContext FramePointerCursorContextFromLibunwindCursor(
+ const unw_cursor_t& uwCursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/interop/ya.make b/library/cpp/yt/backtrace/cursors/interop/ya.make
new file mode 100644
index 0000000000..6637f6a9b4
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/interop/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ interop.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/backtrace/cursors/frame_pointer
+ contrib/libs/libunwind
+)
+
+END()
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp b/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp
new file mode 100644
index 0000000000..f814753034
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.cpp
@@ -0,0 +1,70 @@
+#include "libunwind_cursor.h"
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLibunwindCursor::TLibunwindCursor()
+{
+ if (unw_getcontext(&Context_) != 0) {
+ Finished_ = true;
+ return;
+ }
+
+ Initialize();
+}
+
+TLibunwindCursor::TLibunwindCursor(const unw_context_t& context)
+ : Context_(context)
+{
+ Initialize();
+}
+
+void TLibunwindCursor::Initialize()
+{
+ if (unw_init_local(&Cursor_, &Context_) != 0) {
+ Finished_ = true;
+ return;
+ }
+
+ ReadCurrentIP();
+}
+
+bool TLibunwindCursor::IsFinished() const
+{
+ return Finished_;
+}
+
+const void* TLibunwindCursor::GetCurrentIP() const
+{
+ return CurrentIP_;
+}
+
+void TLibunwindCursor::MoveNext()
+{
+ if (Finished_) {
+ return;
+ }
+
+ if (unw_step(&Cursor_) <= 0) {
+ Finished_ = true;
+ return;
+ }
+
+ ReadCurrentIP();
+}
+
+void TLibunwindCursor::ReadCurrentIP()
+{
+ unw_word_t ip = 0;
+ if (unw_get_reg(&Cursor_, UNW_REG_IP, &ip) < 0) {
+ Finished_ = true;
+ return;
+ }
+
+ CurrentIP_ = reinterpret_cast<const void*>(ip);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.h b/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.h
new file mode 100644
index 0000000000..08b01d07ef
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <contrib/libs/libunwind/include/libunwind.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLibunwindCursor
+{
+public:
+ TLibunwindCursor();
+ explicit TLibunwindCursor(const unw_context_t& context);
+
+ bool IsFinished() const;
+ const void* GetCurrentIP() const;
+ void MoveNext();
+
+private:
+ unw_context_t Context_;
+ unw_cursor_t Cursor_;
+
+ bool Finished_ = false;
+
+ const void* CurrentIP_ = nullptr;
+
+ void Initialize();
+ void ReadCurrentIP();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/cursors/libunwind/ya.make b/library/cpp/yt/backtrace/cursors/libunwind/ya.make
new file mode 100644
index 0000000000..8f3a8c5284
--- /dev/null
+++ b/library/cpp/yt/backtrace/cursors/libunwind/ya.make
@@ -0,0 +1,13 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ libunwind_cursor.cpp
+)
+
+PEERDIR(
+ contrib/libs/libunwind
+)
+
+END()
diff --git a/library/cpp/yt/backtrace/symbolizers/dummy/dummy_symbolizer.cpp b/library/cpp/yt/backtrace/symbolizers/dummy/dummy_symbolizer.cpp
new file mode 100644
index 0000000000..19cb41e795
--- /dev/null
+++ b/library/cpp/yt/backtrace/symbolizers/dummy/dummy_symbolizer.cpp
@@ -0,0 +1,25 @@
+#include <library/cpp/yt/backtrace/backtrace.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SymbolizeBacktrace(
+ TBacktrace backtrace,
+ const std::function<void(TStringBuf)>& frameCallback)
+{
+ for (int index = 0; index < std::ssize(backtrace); ++index) {
+ TRawFormatter<1024> formatter;
+ formatter.AppendNumber(index + 1, 10, 2);
+ formatter.AppendString(". ");
+ formatter.AppendNumberAsHexWithPadding(reinterpret_cast<uintptr_t>(backtrace[index]), 12);
+ formatter.AppendString("\n");
+ frameCallback(formatter.GetBuffer());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/symbolizers/dwarf/dwarf_symbolizer.cpp b/library/cpp/yt/backtrace/symbolizers/dwarf/dwarf_symbolizer.cpp
new file mode 100644
index 0000000000..f5d02aaa33
--- /dev/null
+++ b/library/cpp/yt/backtrace/symbolizers/dwarf/dwarf_symbolizer.cpp
@@ -0,0 +1,64 @@
+#include <library/cpp/yt/backtrace/backtrace.h>
+
+#include <library/cpp/dwarf_backtrace/backtrace.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SymbolizeBacktrace(
+ TBacktrace backtrace,
+ const std::function<void(TStringBuf)>& frameCallback)
+{
+ auto error = NDwarf::ResolveBacktrace({backtrace.begin(), backtrace.size()}, [&] (const NDwarf::TLineInfo& info) {
+ TRawFormatter<1024> formatter;
+ formatter.AppendNumber(info.Index + 1, 10, 2);
+ formatter.AppendString(". ");
+ formatter.AppendString("0x");
+ const int width = (sizeof(void*) == 8 ? 12 : 8);
+ // 12 for x86_64 because higher bits are always zeroed.
+ formatter.AppendNumber(info.Address, 16, width, '0');
+ formatter.AppendString(" in ");
+ formatter.AppendString(info.FunctionName);
+ const int bytesToAppendEstimate = 4 + info.FileName.Size() + 1 + 4 /* who cares about line numbers > 9999 */ + 1;
+ if (formatter.GetBytesRemaining() < bytesToAppendEstimate) {
+ const int offset = formatter.GetBytesRemaining() - bytesToAppendEstimate;
+ if (formatter.GetBytesWritten() + offset >= 0) {
+ formatter.Advance(offset);
+ }
+ }
+ formatter.AppendString(" at ");
+ formatter.AppendString(info.FileName);
+ formatter.AppendChar(':');
+ formatter.AppendNumber(info.Line);
+ if (formatter.GetBytesRemaining() == 0) {
+ formatter.Revert(1);
+ }
+ formatter.AppendString("\n");
+ frameCallback(formatter.GetBuffer());
+ // Call the callback exactly `frameCount` times,
+ // even if there are inline functions and one frame resolved to several lines.
+ // It needs for case when caller uses `frameCount` less than 100 for pretty formatting.
+ if (info.Index + 1 == std::ssize(backtrace)) {
+ return NDwarf::EResolving::Break;
+ }
+ return NDwarf::EResolving::Continue;
+ });
+ if (error) {
+ TRawFormatter<1024> formatter;
+ formatter.AppendString("*** Error symbolizing backtrace via Dwarf\n");
+ formatter.AppendString("*** Code: ");
+ formatter.AppendNumber(error->Code);
+ formatter.AppendString("\n");
+ formatter.AppendString("*** Message: ");
+ formatter.AppendString(error->Message);
+ formatter.AppendString("\n");
+ frameCallback(formatter.GetBuffer());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/symbolizers/dwarf/ya.make b/library/cpp/yt/backtrace/symbolizers/dwarf/ya.make
new file mode 100644
index 0000000000..bffeb676d8
--- /dev/null
+++ b/library/cpp/yt/backtrace/symbolizers/dwarf/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+SRCS(
+ GLOBAL dwarf_symbolizer.cpp
+)
+
+PEERDIR(
+ library/cpp/dwarf_backtrace
+ library/cpp/yt/backtrace
+)
+
+END()
+
+IF (BUILD_TYPE == "DEBUG" OR BUILD_TYPE == "PROFILE")
+ RECURSE_FOR_TESTS(
+ unittests
+ )
+ENDIF()
diff --git a/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp b/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp
new file mode 100644
index 0000000000..37ebda8e48
--- /dev/null
+++ b/library/cpp/yt/backtrace/symbolizers/dynload/dynload_symbolizer.cpp
@@ -0,0 +1,113 @@
+#include <library/cpp/yt/backtrace/backtrace.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <util/system/compiler.h>
+
+#include <dlfcn.h>
+#include <cxxabi.h>
+
+namespace NYT::NBacktrace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+int GetSymbolInfo(const void* pc, char* buffer, int length)
+{
+ TBaseFormatter formatter(buffer, length);
+
+ // See http://www.codesourcery.com/cxx-abi/abi.html#mangling
+ // And, yes, dladdr() is not async signal safe. We can substitute it
+ // with hand-written symbolization code from google-glog in case of any trouble.
+ Dl_info info;
+ if (!dladdr(pc, &info)) {
+ return 0;
+ }
+
+ /*
+ * typedef struct {
+ * const char *dli_fname; // Pathname of shared object that
+ * // contains address
+ * void *dli_fbase; // Address at which shared object
+ * // is loaded
+ * const char *dli_sname; // Name of nearest symbol with address
+ * // lower than addr
+ * void *dli_saddr; // Exact address of symbol named
+ * // in dli_sname
+ * } Dl_info;
+ *
+ * If no symbol matching addr could be found, then dli_sname and dli_saddr are set to NULL.
+ */
+
+ if (info.dli_sname && info.dli_saddr) {
+ formatter.AppendString("<");
+ int demangleStatus = 0;
+
+ if (info.dli_sname[0] == '_' && info.dli_sname[1] == 'Z') {
+ // This is also not async signal safe.
+ // But (ta-dah!) we can replace it with symbolization code from google-glob.
+ char* demangledName = abi::__cxa_demangle(info.dli_sname, 0, 0, &demangleStatus);
+ if (demangleStatus == 0) {
+ formatter.AppendString(demangledName);
+ } else {
+ formatter.AppendString(info.dli_sname);
+ }
+ free(demangledName);
+ } else {
+ formatter.AppendString(info.dli_sname);
+ }
+ formatter.AppendString("+");
+ formatter.AppendNumber((char*)pc - (char*)info.dli_saddr);
+ formatter.AppendString(">");
+ formatter.AppendString(" ");
+ }
+
+ if (info.dli_fname && info.dli_fbase) {
+ formatter.AppendString("(");
+ formatter.AppendString(info.dli_fname);
+ formatter.AppendString("+");
+ formatter.AppendNumber((char*)pc - (char*)info.dli_fbase);
+ formatter.AppendString(")");
+ }
+ return formatter.GetBytesWritten();
+}
+
+void DumpStackFrameInfo(TBaseFormatter* formatter, const void* pc)
+{
+ formatter->AppendString("@ ");
+ const int width = (sizeof(void*) == 8 ? 12 : 8) + 2;
+ // +2 for "0x"; 12 for x86_64 because higher bits are always zeroed.
+ formatter->AppendNumberAsHexWithPadding(reinterpret_cast<uintptr_t>(pc), width);
+ formatter->AppendString(" ");
+ // Get the symbol from the previous address of PC,
+ // because PC may be in the next function.
+ formatter->Advance(GetSymbolInfo(
+ reinterpret_cast<const char*>(pc) - 1,
+ formatter->GetCursor(),
+ formatter->GetBytesRemaining()));
+ if (formatter->GetBytesRemaining() == 0) {
+ formatter->Revert(1);
+ }
+ formatter->AppendString("\n");
+}
+
+} // namespace
+
+Y_WEAK void SymbolizeBacktrace(
+ TBacktrace backtrace,
+ const std::function<void(TStringBuf)>& frameCallback)
+{
+ for (int i = 0; i < std::ssize(backtrace); ++i) {
+ TRawFormatter<1024> formatter;
+ formatter.Reset();
+ formatter.AppendNumber(i + 1, 10, 2);
+ formatter.AppendString(". ");
+ DumpStackFrameInfo(&formatter, backtrace[i]);
+ frameCallback(formatter.GetBuffer());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/unittests/backtrace_ut.cpp b/library/cpp/yt/backtrace/unittests/backtrace_ut.cpp
new file mode 100644
index 0000000000..5992b69277
--- /dev/null
+++ b/library/cpp/yt/backtrace/unittests/backtrace_ut.cpp
@@ -0,0 +1,61 @@
+#include <gtest/gtest.h>
+
+#include <gmock/gmock.h>
+
+#include <library/cpp/yt/memory/safe_memory_reader.h>
+
+#include <library/cpp/yt/backtrace/cursors/frame_pointer/frame_pointer_cursor.h>
+
+#include <library/cpp/yt/backtrace/cursors/interop/interop.h>
+
+#include <util/system/compiler.h>
+
+#include <contrib/libs/libunwind/include/libunwind.h>
+
+namespace NYT::NBacktrace {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <int Depth, class TFn>
+Y_NO_INLINE void RunInDeepStack(TFn cb)
+{
+ if constexpr (Depth == 0) {
+ cb();
+ } else {
+ std::vector<int> touchMem;
+ touchMem.push_back(0);
+
+ RunInDeepStack<Depth-1>(cb);
+
+ DoNotOptimizeAway(touchMem);
+ }
+}
+
+TEST(TFramePointerCursor, FramePointerCursor)
+{
+ std::vector<const void*> backtrace;
+ RunInDeepStack<64>([&] {
+ unw_context_t unwContext;
+ ASSERT_TRUE(unw_getcontext(&unwContext) == 0);
+
+ unw_cursor_t unwCursor;
+ ASSERT_TRUE(unw_init_local(&unwCursor, &unwContext) == 0);
+
+ TSafeMemoryReader reader;
+ auto fpCursorContext = NBacktrace::FramePointerCursorContextFromLibunwindCursor(unwCursor);
+ NBacktrace::TFramePointerCursor fpCursor(&reader, fpCursorContext);
+
+ while (!fpCursor.IsFinished()) {
+ backtrace.push_back(fpCursor.GetCurrentIP());
+ fpCursor.MoveNext();
+ }
+ });
+
+ ASSERT_THAT(backtrace, testing::SizeIs(testing::Ge(64u)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NBacktrace
diff --git a/library/cpp/yt/backtrace/unittests/ya.make b/library/cpp/yt/backtrace/unittests/ya.make
new file mode 100644
index 0000000000..89e55a95ef
--- /dev/null
+++ b/library/cpp/yt/backtrace/unittests/ya.make
@@ -0,0 +1,20 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ library/cpp/yt/backtrace
+ library/cpp/yt/backtrace/cursors/interop
+ library/cpp/yt/backtrace/cursors/frame_pointer
+ library/cpp/yt/backtrace/cursors/libunwind
+ library/cpp/yt/memory
+)
+
+IF (BUILD_TYPE == "DEBUG" OR BUILD_TYPE == "PROFILE")
+ SRCS(
+ backtrace_ut.cpp
+ )
+ENDIF()
+
+END()
diff --git a/library/cpp/yt/backtrace/ya.make b/library/cpp/yt/backtrace/ya.make
new file mode 100644
index 0000000000..d294082e06
--- /dev/null
+++ b/library/cpp/yt/backtrace/ya.make
@@ -0,0 +1,44 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ backtrace.cpp
+)
+
+IF (OS_WINDOWS)
+ SRCS(
+ symbolizers/dummy/dummy_symbolizer.cpp
+ )
+ELSE()
+ SRCS(
+ symbolizers/dynload/dynload_symbolizer.cpp
+ )
+ENDIF()
+
+PEERDIR(
+ library/cpp/yt/string
+)
+
+END()
+
+RECURSE(
+ cursors/dummy
+ cursors/frame_pointer
+)
+
+IF (NOT OS_WINDOWS)
+ RECURSE(
+ cursors/libunwind
+ )
+ENDIF()
+
+IF (OS_LINUX)
+ RECURSE(
+ symbolizers/dwarf
+ )
+
+ RECURSE_FOR_TESTS(
+ unittests
+ )
+ENDIF()
diff --git a/library/cpp/yt/containers/sharded_set-inl.h b/library/cpp/yt/containers/sharded_set-inl.h
new file mode 100644
index 0000000000..67d5be58c6
--- /dev/null
+++ b/library/cpp/yt/containers/sharded_set-inl.h
@@ -0,0 +1,217 @@
+#ifndef SHARDED_SET_INL_H_
+#error "Direct inclusion of this file is not allowed, include sharded_set.h"
+// For the sake of sane code completion.
+#include "sharded_set.h"
+#endif
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, int N, class F, class S>
+class TShardedSet<T, N, F, S>::const_iterator
+{
+private:
+ friend class TShardedSet<T, N, F, S>;
+
+ using TOwner = TShardedSet<T, N, F, S>;
+ using TShardIterator = typename S::const_iterator;
+
+ const TOwner* const Owner_;
+
+ int ShardIndex_;
+ TShardIterator ShardIterator_;
+
+ const_iterator(
+ const TOwner* owner,
+ int shardIndex,
+ TShardIterator shardIterator)
+ : Owner_(owner)
+ , ShardIndex_(shardIndex)
+ , ShardIterator_(shardIterator)
+ { }
+
+ bool IsValid() const
+ {
+ return ShardIterator_ != Owner_->Shards_[ShardIndex_].end();
+ }
+
+ void FastForward()
+ {
+ while (ShardIndex_ != N - 1 && !IsValid()) {
+ ++ShardIndex_;
+ ShardIterator_ = Owner_->Shards_[ShardIndex_].begin();
+ }
+ }
+
+public:
+ using difference_type = typename std::iterator_traits<TShardIterator>::difference_type;
+ using value_type = typename std::iterator_traits<TShardIterator>::value_type;
+ using pointer = typename std::iterator_traits<TShardIterator>::pointer;
+ using reference = typename std::iterator_traits<TShardIterator>::reference;
+ using iterator_category = std::forward_iterator_tag;
+
+ const_iterator& operator++()
+ {
+ ++ShardIterator_;
+ FastForward();
+
+ return *this;
+ }
+
+ const_iterator operator++(int)
+ {
+ auto result = *this;
+
+ ++ShardIterator_;
+ FastForward();
+
+ return result;
+ }
+
+ bool operator==(const const_iterator& rhs) const
+ {
+ return
+ ShardIndex_ == rhs.ShardIndex_ &&
+ ShardIterator_ == rhs.ShardIterator_;
+ }
+
+ bool operator!=(const const_iterator& rhs) const
+ {
+ return !(*this == rhs);
+ }
+
+ const T& operator*() const
+ {
+ return *ShardIterator_;
+ }
+
+ const T* operator->() const
+ {
+ return &operator*();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, int N, class F, class S>
+TShardedSet<T, N, F, S>::TShardedSet(F elementToShard)
+ : ElementToShard_(elementToShard)
+{ }
+
+template <class T, int N, class F, class S>
+bool TShardedSet<T, N, F, S>::empty() const
+{
+ return size() == 0;
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::size_type TShardedSet<T, N, F, S>::size() const
+{
+ size_type result = 0;
+ for (const auto& shard : Shards_) {
+ result += shard.size();
+ }
+
+ return result;
+}
+
+template <class T, int N, class F, class S>
+const T& TShardedSet<T, N, F, S>::front() const
+{
+ return *begin();
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::size_type TShardedSet<T, N, F, S>::count(const T& value) const
+{
+ return GetShard(value).count(value);
+}
+
+template <class T, int N, class F, class S>
+bool TShardedSet<T, N, F, S>::contains(const T& value) const
+{
+ return GetShard(value).contains(value);
+}
+
+template <class T, int N, class F, class S>
+std::pair<typename TShardedSet<T, N, F, S>::const_iterator, bool> TShardedSet<T, N, F, S>::insert(const T& value)
+{
+ auto shardIndex = ElementToShard_(value);
+ auto& shard = Shards_[shardIndex];
+ auto [shardIterator, inserted] = shard.insert(value);
+
+ const_iterator iterator(this, shardIndex, shardIterator);
+ return {iterator, inserted};
+}
+
+template <class T, int N, class F, class S>
+bool TShardedSet<T, N, F, S>::erase(const T& value)
+{
+ return GetShard(value).erase(value);
+}
+
+template <class T, int N, class F, class S>
+void TShardedSet<T, N, F, S>::clear()
+{
+ for (auto& shard : Shards_) {
+ shard.clear();
+ }
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::const_iterator TShardedSet<T, N, F, S>::begin() const
+{
+ const_iterator iterator(this, /*shardIndex*/ 0, /*shardIterator*/ Shards_[0].begin());
+ iterator.FastForward();
+
+ return iterator;
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::const_iterator TShardedSet<T, N, F, S>::cbegin() const
+{
+ return begin();
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::const_iterator TShardedSet<T, N, F, S>::end() const
+{
+ return const_iterator(this, /*shardIndex*/ N - 1, /*shardIterator*/ Shards_[N - 1].end());
+}
+
+template <class T, int N, class F, class S>
+typename TShardedSet<T, N, F, S>::const_iterator TShardedSet<T, N, F, S>::cend() const
+{
+ return end();
+}
+
+template <class T, int N, class F, class S>
+const S& TShardedSet<T, N, F, S>::Shard(int shardIndex) const
+{
+ return Shards_[shardIndex];
+}
+
+template <class T, int N, class F, class S>
+S& TShardedSet<T, N, F, S>::MutableShard(int shardIndex)
+{
+ return Shards_[shardIndex];
+}
+
+template <class T, int N, class F, class S>
+S& TShardedSet<T, N, F, S>::GetShard(const T& value)
+{
+ return Shards_[ElementToShard_(value)];
+}
+
+template <class T, int N, class F, class S>
+const S& TShardedSet<T, N, F, S>::GetShard(const T& value) const
+{
+ return Shards_[ElementToShard_(value)];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/containers/sharded_set.h b/library/cpp/yt/containers/sharded_set.h
new file mode 100644
index 0000000000..fa24893aa4
--- /dev/null
+++ b/library/cpp/yt/containers/sharded_set.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <util/generic/hash_set.h>
+
+#include <array>
+#include <cstddef>
+#include <utility>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A set that stores elements divided into fixed amount of shards.
+//! Provides access to whole set and particular shards.
+//! The interface is pretty minimalistic, feel free to extend it when needed.
+template <class T, int N, class F, class S = THashSet<T>>
+class TShardedSet
+{
+public:
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+
+ using value_type = T;
+
+ class const_iterator;
+
+ explicit TShardedSet(F elementToShard = F());
+
+ [[nodiscard]] bool empty() const;
+
+ size_type size() const;
+
+ const T& front() const;
+
+ size_type count(const T& value) const;
+
+ bool contains(const T& value) const;
+
+ std::pair<const_iterator, bool> insert(const T& value);
+
+ bool erase(const T& value);
+
+ void clear();
+
+ const_iterator begin() const;
+ const_iterator cbegin() const;
+
+ const_iterator end() const;
+ const_iterator cend() const;
+
+ const S& Shard(int shardIndex) const;
+ S& MutableShard(int shardIndex);
+
+private:
+ std::array<S, N> Shards_;
+
+ const F ElementToShard_;
+
+ S& GetShard(const T& value);
+ const S& GetShard(const T& value) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SHARDED_SET_INL_H_
+#include "sharded_set-inl.h"
+#undef SHARDED_SET_INL_H_
diff --git a/library/cpp/yt/containers/unittests/sharded_set_ut.cpp b/library/cpp/yt/containers/unittests/sharded_set_ut.cpp
new file mode 100644
index 0000000000..2c4f8c5935
--- /dev/null
+++ b/library/cpp/yt/containers/unittests/sharded_set_ut.cpp
@@ -0,0 +1,121 @@
+#include <library/cpp/yt/containers/sharded_set.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TIntToShard
+{
+ int operator()(int value) const
+ {
+ return value % 16;
+ }
+};
+
+using TSet = TShardedSet<int, 16, TIntToShard>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(CompactSetTest, Insert)
+{
+ TSet set;
+
+ for (int i = 0; i < 4; i++) {
+ set.insert(i);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ set.insert(i);
+ }
+
+ EXPECT_EQ(4u, set.size());
+
+ for (int i = 0; i < 4; i++)
+ EXPECT_EQ(1u, set.count(i));
+
+ EXPECT_EQ(0u, set.count(4));
+}
+
+TEST(CompactSetTest, Erase)
+{
+ TSet set;
+
+ for (int i = 0; i < 8; i++) {
+ set.insert(i);
+ }
+
+ EXPECT_EQ(8u, set.size());
+
+ // Remove elements one by one and check if all other elements are still there.
+ for (int i = 0; i < 8; i++) {
+ EXPECT_EQ(1u, set.count(i));
+ EXPECT_TRUE(set.erase(i));
+ EXPECT_EQ(0u, set.count(i));
+ EXPECT_EQ(8u - i - 1, set.size());
+ for (int j = i + 1; j < 8; j++) {
+ EXPECT_EQ(1u, set.count(j));
+ }
+ }
+
+ EXPECT_EQ(0u, set.count(8));
+}
+
+TEST(CompactSetTest, StressTest)
+{
+ TSet set;
+
+ constexpr int Iterations = 1'000'000;
+ constexpr int Values = 128;
+
+ THashSet<int> values;
+
+ auto checkEverything = [&] {
+ EXPECT_EQ(values.size(), set.size());
+ EXPECT_EQ(values.empty(), set.empty());
+ EXPECT_EQ(values, THashSet<int>(set.begin(), set.end()));
+
+ std::array<THashSet<int>, 16> shards;
+ for (int value : values) {
+ shards[value % 16].insert(value);
+ }
+ for (int shardIndex = 0; shardIndex < 16; ++shardIndex) {
+ EXPECT_EQ(shards[shardIndex], set.Shard(shardIndex));
+ }
+
+ for (int value = 0; value < Values; ++value) {
+ EXPECT_EQ(values.contains(value), set.contains(value));
+ EXPECT_EQ(values.count(value), set.count(value));
+ }
+ };
+
+ std::mt19937_64 rng(42);
+
+ for (int iteration = 0; iteration < Iterations; ++iteration) {
+ if (rng() % 100 == 0) {
+ set.clear();
+ values.clear();
+ checkEverything();
+ }
+
+ int value = rng() % Values;
+ if (rng() % 2 == 0) {
+ set.insert(value);
+ values.insert(value);
+ } else {
+ set.erase(value);
+ values.erase(value);
+ }
+
+ checkEverything();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/containers/unittests/ya.make b/library/cpp/yt/containers/unittests/ya.make
new file mode 100644
index 0000000000..3e7cfd4311
--- /dev/null
+++ b/library/cpp/yt/containers/unittests/ya.make
@@ -0,0 +1,15 @@
+GTEST(unittester-containers)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ sharded_set_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/containers
+
+ library/cpp/testing/gtest
+)
+
+END()
diff --git a/library/cpp/yt/cpu_clock/benchmark/benchmark.cpp b/library/cpp/yt/cpu_clock/benchmark/benchmark.cpp
new file mode 100644
index 0000000000..9d300b6726
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/benchmark/benchmark.cpp
@@ -0,0 +1,41 @@
+#include "benchmark/benchmark.h"
+#include <benchmark/benchmark.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void BM_GetCpuInstant(benchmark::State& state)
+{
+ for (auto _ : state) {
+ benchmark::DoNotOptimize(GetCpuInstant());
+ }
+}
+
+BENCHMARK(BM_GetCpuInstant);
+
+void BM_GetCpuApproximateInstant(benchmark::State& state)
+{
+ for (auto _ : state) {
+ benchmark::DoNotOptimize(GetApproximateCpuInstant());
+ }
+}
+
+BENCHMARK(BM_GetCpuApproximateInstant);
+
+void BM_InstantNow(benchmark::State& state)
+{
+ for (auto _ : state) {
+ benchmark::DoNotOptimize(TInstant::Now());
+ }
+}
+
+BENCHMARK(BM_InstantNow);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/cpu_clock/benchmark/ya.make b/library/cpp/yt/cpu_clock/benchmark/ya.make
new file mode 100644
index 0000000000..4550bf5934
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/benchmark/ya.make
@@ -0,0 +1,11 @@
+G_BENCHMARK()
+
+SRCS(
+ benchmark.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/cpu_clock
+)
+
+END()
diff --git a/library/cpp/yt/cpu_clock/unittests/clock_ut.cpp b/library/cpp/yt/cpu_clock/unittests/clock_ut.cpp
new file mode 100644
index 0000000000..bd9cb6d4be
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/unittests/clock_ut.cpp
@@ -0,0 +1,46 @@
+#include <gtest/gtest.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+i64 DiffMS(T a, T b)
+{
+ return a >= b
+ ? static_cast<i64>(a.MilliSeconds()) - static_cast<i64>(b.MilliSeconds())
+ : DiffMS(b, a);
+}
+
+TEST(TTimingTest, GetInstant)
+{
+ GetInstant();
+
+ EXPECT_LE(DiffMS(GetInstant(), TInstant::Now()), 10);
+}
+
+TEST(TTimingTest, InstantVSCpuInstant)
+{
+ auto instant1 = TInstant::Now();
+ auto cpuInstant = InstantToCpuInstant(instant1);
+ auto instant2 = CpuInstantToInstant(cpuInstant);
+ EXPECT_LE(DiffMS(instant1, instant2), 10);
+}
+
+TEST(TTimingTest, DurationVSCpuDuration)
+{
+ auto cpuInstant1 = GetCpuInstant();
+ constexpr auto duration1 = TDuration::MilliSeconds(100);
+ Sleep(duration1);
+ auto cpuInstant2 = GetCpuInstant();
+ auto duration2 = CpuDurationToDuration(cpuInstant2 - cpuInstant1);
+ EXPECT_LE(DiffMS(duration1, duration2), 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/cpu_clock/unittests/ya.make b/library/cpp/yt/cpu_clock/unittests/ya.make
new file mode 100644
index 0000000000..921087c295
--- /dev/null
+++ b/library/cpp/yt/cpu_clock/unittests/ya.make
@@ -0,0 +1,13 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ clock_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/cpu_clock
+)
+
+END()
diff --git a/library/cpp/yt/farmhash/farm_hash.h b/library/cpp/yt/farmhash/farm_hash.h
new file mode 100644
index 0000000000..fe4c8193a0
--- /dev/null
+++ b/library/cpp/yt/farmhash/farm_hash.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <contrib/libs/farmhash/farmhash.h>
+
+#include <util/system/types.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TFingerprint = ui64;
+
+static inline TFingerprint FarmHash(ui64 value)
+{
+ return ::util::Fingerprint(value);
+}
+
+static inline TFingerprint FarmHash(const void* buf, size_t len)
+{
+ return ::util::Hash64(static_cast<const char*>(buf), len);
+}
+
+static inline TFingerprint FarmHash(const void* buf, size_t len, ui64 seed)
+{
+ return ::util::Hash64WithSeed(static_cast<const char*>(buf), len, seed);
+}
+
+static inline TFingerprint FarmFingerprint(ui64 value)
+{
+ return ::util::Fingerprint(value);
+}
+
+static inline TFingerprint FarmFingerprint(const void* buf, size_t len)
+{
+ return ::util::Fingerprint64(static_cast<const char*>(buf), len);
+}
+
+static inline TFingerprint FarmFingerprint(TStringBuf buf)
+{
+ return FarmFingerprint(buf.Data(), buf.Size());
+}
+
+static inline TFingerprint FarmFingerprint(ui64 first, ui64 second)
+{
+ return ::util::Fingerprint(::util::Uint128(first, second));
+}
+
+// Forever-fixed Google FarmHash fingerprint.
+template <class T>
+TFingerprint FarmFingerprint(const T* begin, const T* end)
+{
+ ui64 result = 0xdeadc0de;
+ for (const auto* value = begin; value < end; ++value) {
+ result = FarmFingerprint(result, FarmFingerprint(*value));
+ }
+ return result ^ (end - begin);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/logging/logger-inl.h b/library/cpp/yt/logging/logger-inl.h
new file mode 100644
index 0000000000..6f489da82d
--- /dev/null
+++ b/library/cpp/yt/logging/logger-inl.h
@@ -0,0 +1,303 @@
+#ifndef LOGGER_INL_H_
+#error "Direct inclusion of this file is not allowed, include logger.h"
+// For the sake of sane code completion.
+#include "logger.h"
+#endif
+#undef LOGGER_INL_H_
+
+#include <library/cpp/yt/yson_string/convert.h>
+#include <library/cpp/yt/yson_string/string.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool TLogger::IsAnchorUpToDate(const TLoggingAnchor& position) const
+{
+ return
+ !Category_ ||
+ position.CurrentVersion == Category_->ActualVersion->load(std::memory_order::relaxed);
+}
+
+template <class... TArgs>
+void TLogger::AddTag(const char* format, TArgs&&... args)
+{
+ AddRawTag(Format(format, std::forward<TArgs>(args)...));
+}
+
+template <class TType>
+void TLogger::AddStructuredTag(TStringBuf key, TType value)
+{
+ StructuredTags_.emplace_back(key, NYson::ConvertToYsonString(value));
+}
+
+template <class... TArgs>
+TLogger TLogger::WithTag(const char* format, TArgs&&... args) const
+{
+ auto result = *this;
+ result.AddTag(format, std::forward<TArgs>(args)...);
+ return result;
+}
+
+template <class TType>
+TLogger TLogger::WithStructuredTag(TStringBuf key, TType value) const
+{
+ auto result = *this;
+ result.AddStructuredTag(key, value);
+ return result;
+}
+
+Y_FORCE_INLINE bool TLogger::IsLevelEnabled(ELogLevel level) const
+{
+ // This is the first check which is intended to be inlined next to
+ // logging invocation point. Check below is almost zero-cost due
+ // to branch prediction (which requires inlining for proper work).
+ if (level < MinLevel_) {
+ return false;
+ }
+
+ // Next check is heavier and requires full log manager definition which
+ // is undesirable in -inl.h header file. This is why we extract it
+ // to a separate method which is implemented in cpp file.
+ return IsLevelEnabledHeavy(level);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+struct TMessageStringBuilderContext
+{
+ TSharedMutableRef Chunk;
+};
+
+struct TMessageBufferTag
+{ };
+
+class TMessageStringBuilder
+ : public TStringBuilderBase
+{
+public:
+ TSharedRef Flush();
+
+ // For testing only.
+ static void DisablePerThreadCache();
+
+protected:
+ void DoReset() override;
+ void DoReserve(size_t newLength) override;
+
+private:
+ struct TPerThreadCache
+ {
+ ~TPerThreadCache();
+
+ TSharedMutableRef Chunk;
+ size_t ChunkOffset = 0;
+ };
+
+ TSharedMutableRef Buffer_;
+
+ static thread_local TPerThreadCache* Cache_;
+ static thread_local bool CacheDestroyed_;
+ static TPerThreadCache* GetCache();
+
+ static constexpr size_t ChunkSize = 128_KB - 64;
+};
+
+inline bool HasMessageTags(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger)
+{
+ if (logger.GetTag()) {
+ return true;
+ }
+ if (loggingContext.TraceLoggingTag) {
+ return true;
+ }
+ return false;
+}
+
+inline void AppendMessageTags(
+ TStringBuilderBase* builder,
+ const TLoggingContext& loggingContext,
+ const TLogger& logger)
+{
+ bool printComma = false;
+ if (const auto& loggerTag = logger.GetTag()) {
+ builder->AppendString(loggerTag);
+ printComma = true;
+ }
+ if (auto traceLoggingTag = loggingContext.TraceLoggingTag) {
+ if (printComma) {
+ builder->AppendString(TStringBuf(", "));
+ }
+ builder->AppendString(traceLoggingTag);
+ }
+}
+
+inline void AppendLogMessage(
+ TStringBuilderBase* builder,
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ TRef message)
+{
+ if (HasMessageTags(loggingContext, logger)) {
+ if (message.Size() >= 1 && message[message.Size() - 1] == ')') {
+ builder->AppendString(TStringBuf(message.Begin(), message.Size() - 1));
+ builder->AppendString(TStringBuf(", "));
+ } else {
+ builder->AppendString(TStringBuf(message.Begin(), message.Size()));
+ builder->AppendString(TStringBuf(" ("));
+ }
+ AppendMessageTags(builder, loggingContext, logger);
+ builder->AppendChar(')');
+ } else {
+ builder->AppendString(TStringBuf(message.Begin(), message.Size()));
+ }
+}
+
+template <class... TArgs>
+void AppendLogMessageWithFormat(
+ TStringBuilderBase* builder,
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ TStringBuf format,
+ TArgs&&... args)
+{
+ if (HasMessageTags(loggingContext, logger)) {
+ if (format.size() >= 2 && format[format.size() - 1] == ')') {
+ builder->AppendFormat(format.substr(0, format.size() - 1), std::forward<TArgs>(args)...);
+ builder->AppendString(TStringBuf(", "));
+ } else {
+ builder->AppendFormat(format, std::forward<TArgs>(args)...);
+ builder->AppendString(TStringBuf(" ("));
+ }
+ AppendMessageTags(builder, loggingContext, logger);
+ builder->AppendChar(')');
+ } else {
+ builder->AppendFormat(format, std::forward<TArgs>(args)...);
+ }
+}
+
+struct TLogMessage
+{
+ TSharedRef MessageRef;
+ TStringBuf Anchor;
+};
+
+template <size_t Length, class... TArgs>
+TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ const char (&format)[Length],
+ TArgs&&... args)
+{
+ TMessageStringBuilder builder;
+ AppendLogMessageWithFormat(&builder, loggingContext, logger, format, std::forward<TArgs>(args)...);
+ return {builder.Flush(), format};
+}
+
+template <class T>
+TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ const T& obj)
+{
+ TMessageStringBuilder builder;
+ FormatValue(&builder, obj, TStringBuf());
+ if (HasMessageTags(loggingContext, logger)) {
+ builder.AppendString(TStringBuf(" ("));
+ AppendMessageTags(&builder, loggingContext, logger);
+ builder.AppendChar(')');
+ }
+ return {builder.Flush(), TStringBuf()};
+}
+
+inline TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ TStringBuf message)
+{
+ TMessageStringBuilder builder;
+ builder.AppendString(message);
+ if (HasMessageTags(loggingContext, logger)) {
+ builder.AppendString(TStringBuf(" ("));
+ AppendMessageTags(&builder, loggingContext, logger);
+ builder.AppendChar(')');
+ }
+ return {builder.Flush(), message};
+}
+
+template <size_t Length>
+TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ const char (&message)[Length])
+{
+ return BuildLogMessage(
+ loggingContext,
+ logger,
+ TStringBuf(message));
+}
+
+inline TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ TSharedRef&& message)
+{
+ if (HasMessageTags(loggingContext, logger)) {
+ TMessageStringBuilder builder;
+ AppendLogMessage(&builder, loggingContext, logger, message);
+ return {builder.Flush(), TStringBuf()};
+ } else {
+ return {std::move(message), TStringBuf()};
+ }
+}
+
+inline TLogEvent CreateLogEvent(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ ELogLevel level)
+{
+ TLogEvent event;
+ event.Instant = loggingContext.Instant;
+ event.Category = logger.GetCategory();
+ event.Essential = logger.IsEssential();
+ event.Level = level;
+ event.ThreadId = loggingContext.ThreadId;
+ event.ThreadName = loggingContext.ThreadName;
+ event.FiberId = loggingContext.FiberId;
+ event.TraceId = loggingContext.TraceId;
+ event.RequestId = loggingContext.RequestId;
+ return event;
+}
+
+void OnCriticalLogEvent(
+ const TLogger& logger,
+ const TLogEvent& event);
+
+inline void LogEventImpl(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ ELogLevel level,
+ ::TSourceLocation sourceLocation,
+ TSharedRef message)
+{
+ auto event = CreateLogEvent(loggingContext, logger, level);
+ event.MessageKind = ELogMessageKind::Unstructured;
+ event.MessageRef = std::move(message);
+ event.Family = ELogFamily::PlainText;
+ event.SourceFile = sourceLocation.File;
+ event.SourceLine = sourceLocation.Line;
+ logger.Write(std::move(event));
+ if (Y_UNLIKELY(event.Level >= ELogLevel::Alert)) {
+ OnCriticalLogEvent(logger, event);
+ }
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/logger.cpp b/library/cpp/yt/logging/logger.cpp
new file mode 100644
index 0000000000..4ee5c1a01b
--- /dev/null
+++ b/library/cpp/yt/logging/logger.cpp
@@ -0,0 +1,289 @@
+#include "logger.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <library/cpp/yt/misc/thread_name.h>
+
+#include <util/system/compiler.h>
+#include <util/system/thread.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+void OnCriticalLogEvent(
+ const TLogger& logger,
+ const TLogEvent& event)
+{
+ if (event.Level == ELogLevel::Fatal ||
+ event.Level == ELogLevel::Alert && logger.GetAbortOnAlert())
+ {
+ fprintf(stderr, "*** Aborting on critical log event\n");
+ fwrite(event.MessageRef.begin(), 1, event.MessageRef.size(), stderr);
+ fprintf(stderr, "\n");
+ YT_ABORT();
+ }
+}
+
+TSharedRef TMessageStringBuilder::Flush()
+{
+ return Buffer_.Slice(0, GetLength());
+}
+
+void TMessageStringBuilder::DisablePerThreadCache()
+{
+ Cache_ = nullptr;
+ CacheDestroyed_ = true;
+}
+
+void TMessageStringBuilder::DoReset()
+{
+ Buffer_.Reset();
+}
+
+void TMessageStringBuilder::DoReserve(size_t newCapacity)
+{
+ auto oldLength = GetLength();
+ newCapacity = FastClp2(newCapacity);
+
+ auto newChunkSize = std::max(ChunkSize, newCapacity);
+ // Hold the old buffer until the data is copied.
+ auto oldBuffer = std::move(Buffer_);
+ auto* cache = GetCache();
+ if (Y_LIKELY(cache)) {
+ auto oldCapacity = End_ - Begin_;
+ auto deltaCapacity = newCapacity - oldCapacity;
+ if (End_ == cache->Chunk.Begin() + cache->ChunkOffset &&
+ cache->ChunkOffset + deltaCapacity <= cache->Chunk.Size())
+ {
+ // Resize inplace.
+ Buffer_ = cache->Chunk.Slice(cache->ChunkOffset - oldCapacity, cache->ChunkOffset + deltaCapacity);
+ cache->ChunkOffset += deltaCapacity;
+ End_ = Begin_ + newCapacity;
+ return;
+ }
+
+ if (Y_UNLIKELY(cache->ChunkOffset + newCapacity > cache->Chunk.Size())) {
+ cache->Chunk = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false});
+ cache->ChunkOffset = 0;
+ }
+
+ Buffer_ = cache->Chunk.Slice(cache->ChunkOffset, cache->ChunkOffset + newCapacity);
+ cache->ChunkOffset += newCapacity;
+ } else {
+ Buffer_ = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false});
+ newCapacity = newChunkSize;
+ }
+ if (oldLength > 0) {
+ ::memcpy(Buffer_.Begin(), Begin_, oldLength);
+ }
+ Begin_ = Buffer_.Begin();
+ End_ = Begin_ + newCapacity;
+}
+
+TMessageStringBuilder::TPerThreadCache* TMessageStringBuilder::GetCache()
+{
+ if (Y_LIKELY(Cache_)) {
+ return Cache_;
+ }
+ if (CacheDestroyed_) {
+ return nullptr;
+ }
+ static thread_local TPerThreadCache Cache;
+ Cache_ = &Cache;
+ return Cache_;
+}
+
+TMessageStringBuilder::TPerThreadCache::~TPerThreadCache()
+{
+ TMessageStringBuilder::DisablePerThreadCache();
+}
+
+thread_local TMessageStringBuilder::TPerThreadCache* TMessageStringBuilder::Cache_;
+thread_local bool TMessageStringBuilder::CacheDestroyed_;
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_WEAK TLoggingContext GetLoggingContext()
+{
+ return {
+ .Instant = GetCpuInstant(),
+ .ThreadId = TThread::CurrentThreadId(),
+ .ThreadName = GetCurrentThreadName(),
+ };
+}
+
+Y_WEAK ILogManager* GetDefaultLogManager()
+{
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+thread_local ELogLevel ThreadMinLogLevel = ELogLevel::Minimum;
+
+void SetThreadMinLogLevel(ELogLevel minLogLevel)
+{
+ ThreadMinLogLevel = minLogLevel;
+}
+
+ELogLevel GetThreadMinLogLevel()
+{
+ return ThreadMinLogLevel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLogger::TLogger(ILogManager* logManager, TStringBuf categoryName)
+ : LogManager_(logManager)
+ , Category_(LogManager_ ? LogManager_->GetCategory(categoryName) : nullptr)
+ , MinLevel_(LogManager_ ? LoggerDefaultMinLevel : NullLoggerMinLevel)
+{ }
+
+TLogger::TLogger(TStringBuf categoryName)
+ : TLogger(GetDefaultLogManager(), categoryName)
+{ }
+
+TLogger::operator bool() const
+{
+ return LogManager_;
+}
+
+const TLoggingCategory* TLogger::GetCategory() const
+{
+ return Category_;
+}
+
+bool TLogger::IsLevelEnabledHeavy(ELogLevel level) const
+{
+ // Note that we managed to reach this point, i.e. level >= MinLevel_,
+ // which implies that MinLevel_ != ELogLevel::Maximum, so this logger was not
+ // default constructed, thus it has non-trivial category.
+ YT_ASSERT(Category_);
+
+ if (Category_->CurrentVersion != Category_->ActualVersion->load(std::memory_order::relaxed)) {
+ LogManager_->UpdateCategory(const_cast<TLoggingCategory*>(Category_));
+ }
+
+ return
+ level >= Category_->MinPlainTextLevel &&
+ level >= ThreadMinLogLevel;
+}
+
+bool TLogger::GetAbortOnAlert() const
+{
+ return LogManager_->GetAbortOnAlert();
+}
+
+bool TLogger::IsEssential() const
+{
+ return Essential_;
+}
+
+void TLogger::UpdateAnchor(TLoggingAnchor* anchor) const
+{
+ LogManager_->UpdateAnchor(anchor);
+}
+
+void TLogger::RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation sourceLocation, TStringBuf message) const
+{
+ LogManager_->RegisterStaticAnchor(anchor, sourceLocation, message);
+}
+
+void TLogger::Write(TLogEvent&& event) const
+{
+ LogManager_->Enqueue(std::move(event));
+}
+
+void TLogger::AddRawTag(const TString& tag)
+{
+ if (!Tag_.empty()) {
+ Tag_ += ", ";
+ }
+ Tag_ += tag;
+}
+
+TLogger TLogger::WithRawTag(const TString& tag) const
+{
+ auto result = *this;
+ result.AddRawTag(tag);
+ return result;
+}
+
+TLogger TLogger::WithEssential(bool essential) const
+{
+ auto result = *this;
+ result.Essential_ = essential;
+ return result;
+}
+
+TLogger TLogger::WithStructuredValidator(TStructuredValidator validator) const
+{
+ auto result = *this;
+ result.StructuredValidators_.push_back(std::move(validator));
+ return result;
+}
+
+TLogger TLogger::WithMinLevel(ELogLevel minLevel) const
+{
+ auto result = *this;
+ if (result) {
+ result.MinLevel_ = minLevel;
+ }
+ return result;
+}
+
+const TString& TLogger::GetTag() const
+{
+ return Tag_;
+}
+
+const TLogger::TStructuredTags& TLogger::GetStructuredTags() const
+{
+ return StructuredTags_;
+}
+
+const TLogger::TStructuredValidators& TLogger::GetStructuredValidators() const
+{
+ return StructuredValidators_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void LogStructuredEvent(
+ const TLogger& logger,
+ NYson::TYsonString message,
+ ELogLevel level)
+{
+ YT_VERIFY(message.GetType() == NYson::EYsonType::MapFragment);
+
+ if (!logger.GetStructuredValidators().empty()) {
+ auto samplingRate = logger.GetCategory()->StructuredValidationSamplingRate.load();
+ auto p = RandomNumber<double>();
+ if (p < samplingRate) {
+ for (const auto& validator : logger.GetStructuredValidators()) {
+ validator(message);
+ }
+ }
+ }
+
+ auto loggingContext = GetLoggingContext();
+ auto event = NDetail::CreateLogEvent(
+ loggingContext,
+ logger,
+ level);
+ event.MessageKind = ELogMessageKind::Structured;
+ event.MessageRef = message.ToSharedRef();
+ event.Family = ELogFamily::Structured;
+ logger.Write(std::move(event));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/logger.h b/library/cpp/yt/logging/logger.h
new file mode 100644
index 0000000000..cdb5584d29
--- /dev/null
+++ b/library/cpp/yt/logging/logger.h
@@ -0,0 +1,351 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/string/format.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/cpu_clock/public.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+
+#include <library/cpp/yt/misc/guid.h>
+
+#include <library/cpp/yt/misc/thread_name.h>
+
+#include <library/cpp/yt/memory/leaky_singleton.h>
+
+#include <util/system/src_location.h>
+
+#include <util/generic/size_literals.h>
+
+#include <atomic>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr double DefaultStructuredValidationSamplingRate = 0.01;
+
+struct TLoggingCategory
+{
+ TString Name;
+ //! This value is used for early dropping of plaintext events in order
+ //! to reduce load on logging thread for events which are definitely going
+ //! to be dropped due to rule setup.
+ //! NB: this optimization is used only for plaintext events since structured
+ //! logging rate is negligible comparing to the plaintext logging rate.
+ std::atomic<ELogLevel> MinPlainTextLevel;
+ std::atomic<int> CurrentVersion;
+ std::atomic<int>* ActualVersion;
+ std::atomic<double> StructuredValidationSamplingRate = DefaultStructuredValidationSamplingRate;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLoggingAnchor
+{
+ std::atomic<bool> Registered = false;
+ ::TSourceLocation SourceLocation = {TStringBuf{}, 0};
+ TString AnchorMessage;
+ TLoggingAnchor* NextAnchor = nullptr;
+
+ std::atomic<int> CurrentVersion = 0;
+ std::atomic<bool> Enabled = false;
+
+ struct TCounter
+ {
+ std::atomic<i64> Current = 0;
+ i64 Previous = 0;
+ };
+
+ TCounter MessageCounter;
+ TCounter ByteCounter;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Declare some type aliases to avoid circular dependencies.
+using TThreadId = size_t;
+using TFiberId = size_t;
+using TTraceId = TGuid;
+using TRequestId = TGuid;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ELogMessageKind,
+ (Unstructured)
+ (Structured)
+);
+
+struct TLogEvent
+{
+ const TLoggingCategory* Category = nullptr;
+ ELogLevel Level = ELogLevel::Minimum;
+ ELogFamily Family = ELogFamily::PlainText;
+ bool Essential = false;
+
+ ELogMessageKind MessageKind = ELogMessageKind::Unstructured;
+ TSharedRef MessageRef;
+
+ TCpuInstant Instant = 0;
+
+ TThreadId ThreadId = {};
+ TThreadName ThreadName = {};
+
+ TFiberId FiberId = {};
+
+ TTraceId TraceId;
+ TRequestId RequestId;
+
+ TStringBuf SourceFile;
+ int SourceLine = -1;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogManager
+{
+ virtual ~ILogManager() = default;
+
+ virtual void RegisterStaticAnchor(
+ TLoggingAnchor* position,
+ ::TSourceLocation sourceLocation,
+ TStringBuf anchorMessage) = 0;
+ virtual void UpdateAnchor(TLoggingAnchor* position) = 0;
+
+ virtual void Enqueue(TLogEvent&& event) = 0;
+
+ virtual const TLoggingCategory* GetCategory(TStringBuf categoryName) = 0;
+ virtual void UpdateCategory(TLoggingCategory* category) = 0;
+
+ virtual bool GetAbortOnAlert() const = 0;
+};
+
+ILogManager* GetDefaultLogManager();
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLoggingContext
+{
+ TCpuInstant Instant;
+ TThreadId ThreadId;
+ TThreadName ThreadName;
+ TFiberId FiberId;
+ TTraceId TraceId;
+ TRequestId RequestId;
+ TStringBuf TraceLoggingTag;
+};
+
+TLoggingContext GetLoggingContext();
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Sets the minimum logging level for messages in current thread.
+// NB: In fiber environment, min log level is attached to a fiber,
+// so after context switch thread min log level might change.
+void SetThreadMinLogLevel(ELogLevel minLogLevel);
+ELogLevel GetThreadMinLogLevel();
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto NullLoggerMinLevel = ELogLevel::Maximum;
+
+// Min level for non-null logger depends on whether we are in debug or release build.
+// - For release mode default behavior is to omit trace logging,
+// this is done by setting logger min level to Debug by default.
+// - For debug mode logger min level is set to trace by default, so that trace logging is
+// allowed by logger, but still may be discarded by category min level.
+#ifdef NDEBUG
+static constexpr auto LoggerDefaultMinLevel = ELogLevel::Debug;
+#else
+static constexpr auto LoggerDefaultMinLevel = ELogLevel::Trace;
+#endif
+
+class TLogger
+{
+public:
+ using TStructuredValidator = std::function<void(const NYson::TYsonString&)>;
+ using TStructuredValidators = std::vector<TStructuredValidator>;
+
+ using TStructuredTag = std::pair<TString, NYson::TYsonString>;
+ // TODO(max42): switch to TCompactVector after YT-15430.
+ using TStructuredTags = std::vector<TStructuredTag>;
+
+ TLogger() = default;
+ TLogger(const TLogger& other) = default;
+ TLogger& operator=(const TLogger& other) = default;
+
+ TLogger(ILogManager* logManager, TStringBuf categoryName);
+ explicit TLogger(TStringBuf categoryName);
+
+ explicit operator bool() const;
+
+ const TLoggingCategory* GetCategory() const;
+
+ //! Validate that level is admitted by logger's own min level
+ //! and by category's min level.
+ bool IsLevelEnabled(ELogLevel level) const;
+
+ bool GetAbortOnAlert() const;
+
+ bool IsEssential() const;
+
+ bool IsAnchorUpToDate(const TLoggingAnchor& anchor) const;
+ void UpdateAnchor(TLoggingAnchor* anchor) const;
+ void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation sourceLocation, TStringBuf message) const;
+
+ void Write(TLogEvent&& event) const;
+
+ void AddRawTag(const TString& tag);
+ template <class... TArgs>
+ void AddTag(const char* format, TArgs&&... args);
+
+ template <class TType>
+ void AddStructuredTag(TStringBuf key, TType value);
+
+ TLogger WithRawTag(const TString& tag) const;
+ template <class... TArgs>
+ TLogger WithTag(const char* format, TArgs&&... args) const;
+
+ template <class TType>
+ TLogger WithStructuredTag(TStringBuf key, TType value) const;
+
+ TLogger WithStructuredValidator(TStructuredValidator validator) const;
+
+ TLogger WithMinLevel(ELogLevel minLevel) const;
+
+ TLogger WithEssential(bool essential = true) const;
+
+ const TString& GetTag() const;
+ const TStructuredTags& GetStructuredTags() const;
+
+ const TStructuredValidators& GetStructuredValidators() const;
+
+protected:
+ // These fields are set only during logger creation, so they are effectively const
+ // and accessing them is thread-safe.
+ ILogManager* LogManager_ = nullptr;
+ const TLoggingCategory* Category_ = nullptr;
+ bool Essential_ = false;
+ ELogLevel MinLevel_ = NullLoggerMinLevel;
+ TString Tag_;
+ TStructuredTags StructuredTags_;
+ TStructuredValidators StructuredValidators_;
+
+private:
+ //! This method checks level against category's min level.
+ //! Refer to comment in TLogger::IsLevelEnabled for more details.
+ bool IsLevelEnabledHeavy(ELogLevel level) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void LogStructuredEvent(
+ const TLogger& logger,
+ NYson::TYsonString message,
+ ELogLevel level);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef YT_ENABLE_TRACE_LOGGING
+#define YT_LOG_TRACE(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Trace, __VA_ARGS__)
+#define YT_LOG_TRACE_IF(condition, ...) if (condition) YT_LOG_TRACE(__VA_ARGS__)
+#define YT_LOG_TRACE_UNLESS(condition, ...) if (!(condition)) YT_LOG_TRACE(__VA_ARGS__)
+#else
+#define YT_LOG_UNUSED(...) if (true) { } else { YT_LOG_DEBUG(__VA_ARGS__); }
+#define YT_LOG_TRACE(...) YT_LOG_UNUSED(__VA_ARGS__)
+#define YT_LOG_TRACE_IF(condition, ...) YT_LOG_UNUSED(__VA_ARGS__)
+#define YT_LOG_TRACE_UNLESS(condition, ...) YT_LOG_UNUSED(__VA_ARGS__)
+#endif
+
+#define YT_LOG_DEBUG(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Debug, __VA_ARGS__)
+#define YT_LOG_DEBUG_IF(condition, ...) if (condition) YT_LOG_DEBUG(__VA_ARGS__)
+#define YT_LOG_DEBUG_UNLESS(condition, ...) if (!(condition)) YT_LOG_DEBUG(__VA_ARGS__)
+
+#define YT_LOG_INFO(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Info, __VA_ARGS__)
+#define YT_LOG_INFO_IF(condition, ...) if (condition) YT_LOG_INFO(__VA_ARGS__)
+#define YT_LOG_INFO_UNLESS(condition, ...) if (!(condition)) YT_LOG_INFO(__VA_ARGS__)
+
+#define YT_LOG_WARNING(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Warning, __VA_ARGS__)
+#define YT_LOG_WARNING_IF(condition, ...) if (condition) YT_LOG_WARNING(__VA_ARGS__)
+#define YT_LOG_WARNING_UNLESS(condition, ...) if (!(condition)) YT_LOG_WARNING(__VA_ARGS__)
+
+#define YT_LOG_ERROR(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Error, __VA_ARGS__)
+#define YT_LOG_ERROR_IF(condition, ...) if (condition) YT_LOG_ERROR(__VA_ARGS__)
+#define YT_LOG_ERROR_UNLESS(condition, ...) if (!(condition)) YT_LOG_ERROR(__VA_ARGS__)
+
+#define YT_LOG_ALERT(...) YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Alert, __VA_ARGS__);
+#define YT_LOG_ALERT_IF(condition, ...) if (condition) YT_LOG_ALERT(__VA_ARGS__)
+#define YT_LOG_ALERT_UNLESS(condition, ...) if (!(condition)) YT_LOG_ALERT(__VA_ARGS__)
+
+#define YT_LOG_FATAL(...) \
+ do { \
+ YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Fatal, __VA_ARGS__); \
+ Y_UNREACHABLE(); \
+ } while(false)
+#define YT_LOG_FATAL_IF(condition, ...) if (Y_UNLIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__)
+#define YT_LOG_FATAL_UNLESS(condition, ...) if (!Y_LIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__)
+
+#define YT_LOG_EVENT(logger, level, ...) \
+ YT_LOG_EVENT_WITH_ANCHOR(logger, level, nullptr, __VA_ARGS__)
+
+#define YT_LOG_EVENT_WITH_ANCHOR(logger, level, anchor, ...) \
+ do { \
+ const auto& logger__##__LINE__ = (logger); \
+ auto level__##__LINE__ = (level); \
+ \
+ if (!logger__##__LINE__.IsLevelEnabled(level__##__LINE__)) { \
+ break; \
+ } \
+ \
+ auto location__##__LINE__ = __LOCATION__; \
+ \
+ ::NYT::NLogging::TLoggingAnchor* anchor__##__LINE__ = (anchor); \
+ if (!anchor__##__LINE__) { \
+ static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> staticAnchor__##__LINE__; \
+ anchor__##__LINE__ = staticAnchor__##__LINE__.Get(); \
+ } \
+ \
+ bool anchorUpToDate__##__LINE__ = logger__##__LINE__.IsAnchorUpToDate(*anchor__##__LINE__); \
+ if (anchorUpToDate__##__LINE__ && !anchor__##__LINE__->Enabled.load(std::memory_order::relaxed)) { \
+ break; \
+ } \
+ \
+ auto loggingContext__##__LINE__ = ::NYT::NLogging::GetLoggingContext(); \
+ auto message__##__LINE__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__##__LINE__, logger__##__LINE__, __VA_ARGS__); \
+ \
+ if (!anchorUpToDate__##__LINE__) { \
+ logger__##__LINE__.RegisterStaticAnchor(anchor__##__LINE__, location__##__LINE__, message__##__LINE__.Anchor); \
+ logger__##__LINE__.UpdateAnchor(anchor__##__LINE__); \
+ } \
+ \
+ if (!anchor__##__LINE__->Enabled.load(std::memory_order::relaxed)) { \
+ break; \
+ } \
+ \
+ static thread_local i64 localByteCounter__##__LINE__; \
+ static thread_local ui8 localMessageCounter__##__LINE__; \
+ \
+ localByteCounter__##__LINE__ += message__##__LINE__.MessageRef.Size(); \
+ if (Y_UNLIKELY(++localMessageCounter__##__LINE__ == 0)) { \
+ anchor__##__LINE__->MessageCounter.Current += 256; \
+ anchor__##__LINE__->ByteCounter.Current += localByteCounter__##__LINE__; \
+ localByteCounter__##__LINE__ = 0; \
+ } \
+ \
+ ::NYT::NLogging::NDetail::LogEventImpl( \
+ loggingContext__##__LINE__, \
+ logger__##__LINE__, \
+ level__##__LINE__, \
+ location__##__LINE__, \
+ std::move(message__##__LINE__.MessageRef)); \
+ } while (false)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
+
+#define LOGGER_INL_H_
+#include "logger-inl.h"
+#undef LOGGER_INL_H_
diff --git a/library/cpp/yt/logging/public.h b/library/cpp/yt/logging/public.h
new file mode 100644
index 0000000000..1e2b59ca0d
--- /dev/null
+++ b/library/cpp/yt/logging/public.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Any change to this enum must be also propagated to FormatLevel.
+DEFINE_ENUM(ELogLevel,
+ (Minimum)
+ (Trace)
+ (Debug)
+ (Info)
+ (Warning)
+ (Error)
+ (Alert)
+ (Fatal)
+ (Maximum)
+);
+
+DEFINE_ENUM(ELogFamily,
+ (PlainText)
+ (Structured)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLoggingCategory;
+struct TLoggingAnchor;
+struct TLogEvent;
+struct TLoggingContext;
+
+class TLogger;
+struct ILogManager;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/logger_ut.cpp b/library/cpp/yt/logging/unittests/logger_ut.cpp
new file mode 100644
index 0000000000..7696ea4a83
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/logger_ut.cpp
@@ -0,0 +1,38 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+namespace NYT::NLogging {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLogger, NullByDefault)
+{
+ {
+ TLogger logger;
+ EXPECT_FALSE(logger);
+ EXPECT_FALSE(logger.IsLevelEnabled(ELogLevel::Fatal));
+ }
+ {
+ TLogger logger{"Category"};
+ EXPECT_FALSE(logger);
+ EXPECT_FALSE(logger.IsLevelEnabled(ELogLevel::Fatal));
+ }
+}
+
+TEST(TLogger, CopyOfNullLogger)
+{
+ TLogger nullLogger{/*logManager*/ nullptr, "Category"};
+ ASSERT_FALSE(nullLogger);
+
+ auto logger = nullLogger.WithMinLevel(ELogLevel::Debug);
+
+ EXPECT_FALSE(logger);
+ EXPECT_FALSE(logger.IsLevelEnabled(ELogLevel::Fatal));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/ya.make b/library/cpp/yt/logging/unittests/ya.make
new file mode 100644
index 0000000000..42268d3db2
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/ya.make
@@ -0,0 +1,18 @@
+GTEST(unittester-library-logging)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS)
+ ALLOCATOR(YT)
+ENDIF()
+
+SRCS(
+ logger_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ library/cpp/yt/logging
+)
+
+END()
diff --git a/library/cpp/yt/logging/ya.make b/library/cpp/yt/logging/ya.make
new file mode 100644
index 0000000000..cf629a24b6
--- /dev/null
+++ b/library/cpp/yt/logging/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ logger.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/memory
+ library/cpp/yt/misc
+ library/cpp/yt/yson_string
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests
+)
diff --git a/library/cpp/yt/memory/leaky_ref_counted_singleton-inl.h b/library/cpp/yt/memory/leaky_ref_counted_singleton-inl.h
new file mode 100644
index 0000000000..1fba63c427
--- /dev/null
+++ b/library/cpp/yt/memory/leaky_ref_counted_singleton-inl.h
@@ -0,0 +1,43 @@
+#ifndef LEAKY_REF_COUNTED_SINGLETON_INL_H_
+#error "Direct inclusion of this file is not allowed, include leaky_ref_counted_singleton.h"
+// For the sake of sane code completion.
+#include "leaky_ref_counted_singleton.h"
+#endif
+
+#include "new.h"
+
+#include <atomic>
+#include <mutex>
+
+#include <util/system/compiler.h>
+#include <util/system/sanitizers.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class... TArgs>
+TIntrusivePtr<T> LeakyRefCountedSingleton(TArgs&&... args)
+{
+ static std::atomic<T*> Ptr;
+ auto* ptr = Ptr.load(std::memory_order::acquire);
+ if (Y_LIKELY(ptr)) {
+ return ptr;
+ }
+
+ static std::once_flag Initialized;
+ std::call_once(Initialized, [&] {
+ auto ptr = New<T>(std::forward<TArgs>(args)...);
+ Ref(ptr.Get());
+ Ptr.store(ptr.Get());
+#if defined(_asan_enabled_)
+ NSan::MarkAsIntentionallyLeaked(ptr.Get());
+#endif
+ });
+
+ return Ptr.load();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/memory/leaky_ref_counted_singleton.h b/library/cpp/yt/memory/leaky_ref_counted_singleton.h
new file mode 100644
index 0000000000..d77c3c9829
--- /dev/null
+++ b/library/cpp/yt/memory/leaky_ref_counted_singleton.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "intrusive_ptr.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DECLARE_LEAKY_REF_COUNTED_SINGLETON_FRIEND() \
+ template <class T> \
+ friend struct ::NYT::TRefCountedWrapper;
+
+template <class T, class... TArgs>
+TIntrusivePtr<T> LeakyRefCountedSingleton(TArgs&&... args);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define LEAKY_REF_COUNTED_SINGLETON_INL_H_
+#include "leaky_ref_counted_singleton-inl.h"
+#undef LEAKY_REF_COUNTED_SINGLETON_INL_H_
diff --git a/library/cpp/yt/misc/arcadia_enum-inl.h b/library/cpp/yt/misc/arcadia_enum-inl.h
new file mode 100644
index 0000000000..17a10bb3b2
--- /dev/null
+++ b/library/cpp/yt/misc/arcadia_enum-inl.h
@@ -0,0 +1,49 @@
+#pragma once
+#ifndef ARCADIA_ENUM_INL_H_
+#error "Direct inclusion of this file is not allowed, include arcadia_enum.h"
+// For the sake of sane code completion.
+#include "arcadia_enum.h"
+#endif
+
+#include <util/system/type_name.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TArcadiaEnumTraitsImpl
+{
+ static constexpr bool IsBitEnum = false;
+ static constexpr bool IsStringSerializableEnum = false;
+
+ static TStringBuf GetTypeName()
+ {
+ static const auto Result = TypeName<T>();
+ return Result;
+ }
+
+ static std::optional<TStringBuf> FindLiteralByValue(T value)
+ {
+ auto names = GetEnumNames<T>();
+ auto it = names.find(value);
+ return it == names.end() ? std::nullopt : std::make_optional(TStringBuf(it->second));
+ }
+
+ static std::optional<T> FindValueByLiteral(TStringBuf literal)
+ {
+ static const auto LiteralToValue = [] {
+ THashMap<TString, T> result;
+ for (const auto& [value, name] : GetEnumNames<T>()) {
+ result.emplace(name, value);
+ }
+ return result;
+ }();
+ auto it = LiteralToValue.find(literal);
+ return it == LiteralToValue.end() ? std::nullopt : std::make_optional(it->second);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/library/cpp/yt/misc/arcadia_enum.h b/library/cpp/yt/misc/arcadia_enum.h
new file mode 100644
index 0000000000..85ad182a6c
--- /dev/null
+++ b/library/cpp/yt/misc/arcadia_enum.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <util/generic/serialized_enum.h>
+
+////////////////////////////////////////////////////////////////////////////////
+// TEnumTraits interop for Arcadia enums
+
+#define YT_DEFINE_ARCADIA_ENUM_TRAITS(enumType) \
+ [[maybe_unused]] inline ::NYT::NDetail::TArcadiaEnumTraitsImpl<enumType> GetEnumTraitsImpl(enumType) \
+ { \
+ return {}; \
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ARCADIA_ENUM_INL_H_
+#include "arcadia_enum-inl.h"
+#undef ARCADIA_ENUM_INL_H_
diff --git a/library/cpp/yt/misc/property.h b/library/cpp/yt/misc/property.h
new file mode 100644
index 0000000000..d5c2a26c7a
--- /dev/null
+++ b/library/cpp/yt/misc/property.h
@@ -0,0 +1,306 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Declares a trivial public read-write property that is passed by reference.
+#define DECLARE_BYREF_RW_PROPERTY(type, name) \
+public: \
+ type& name(); \
+ const type& name() const
+
+//! Defines a trivial public read-write property that is passed by reference.
+//! All arguments after name are used as default value (via braced-init-list).
+#define DEFINE_BYREF_RW_PROPERTY(type, name, ...) \
+protected: \
+ type name##_ { __VA_ARGS__ }; \
+ \
+public: \
+ Y_FORCE_INLINE type& name() \
+ { \
+ return name##_; \
+ } \
+ \
+ Y_FORCE_INLINE const type& name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+//! Defines a trivial public read-write property that is passed by reference
+//! and is not inline-initialized.
+#define DEFINE_BYREF_RW_PROPERTY_NO_INIT(type, name) \
+protected: \
+ type name##_; \
+ \
+public: \
+ Y_FORCE_INLINE type& name() \
+ { \
+ return name##_; \
+ } \
+ \
+ Y_FORCE_INLINE const type& name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+//! Forwards a trivial public read-write property that is passed by reference.
+#define DELEGATE_BYREF_RW_PROPERTY(declaringType, type, name, delegateTo) \
+ type& declaringType::name() \
+ { \
+ return (delegateTo).name(); \
+ } \
+ \
+ const type& declaringType::name() const \
+ { \
+ return (delegateTo).name(); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Declares a trivial public read-only property that is passed by reference.
+#define DECLARE_BYREF_RO_PROPERTY(type, name) \
+public: \
+ const type& name() const
+
+//! Defines a trivial public read-only property that is passed by reference.
+//! All arguments after name are used as default value (via braced-init-list).
+#define DEFINE_BYREF_RO_PROPERTY(type, name, ...) \
+protected: \
+ type name##_ { __VA_ARGS__ }; \
+ \
+public: \
+ Y_FORCE_INLINE const type& name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+//! Defines a trivial public read-only property that is passed by reference
+//! and is not inline-initialized.
+#define DEFINE_BYREF_RO_PROPERTY_NO_INIT(type, name) \
+protected: \
+ type name##_; \
+ \
+public: \
+ Y_FORCE_INLINE const type& name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+//! Forwards a trivial public read-only property that is passed by reference.
+#define DELEGATE_BYREF_RO_PROPERTY(declaringType, type, name, delegateTo) \
+ const type& declaringType::name() const \
+ { \
+ return (delegateTo).name(); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Declares a trivial public read-write property that is passed by value.
+#define DECLARE_BYVAL_RW_PROPERTY(type, name) \
+public: \
+ type Get##name() const; \
+ void Set##name(type value)
+
+//! Defines a trivial public read-write property that is passed by value.
+//! All arguments after name are used as default value (via braced-init-list).
+#define DEFINE_BYVAL_RW_PROPERTY(type, name, ...) \
+protected: \
+ type name##_ { __VA_ARGS__ }; \
+ \
+public: \
+ Y_FORCE_INLINE type Get##name() const \
+ { \
+ return name##_; \
+ } \
+ \
+ Y_FORCE_INLINE void Set##name(type value) \
+ { \
+ name##_ = value; \
+ } \
+ static_assert(true)
+
+
+//! Defines a trivial public read-write property that is passed by value.
+//! All arguments after name are used as default value (via braced-init-list).
+#define DEFINE_BYVAL_RW_PROPERTY_WITH_FLUENT_SETTER(declaringType, type, name, ...) \
+protected: \
+ type name##_ { __VA_ARGS__ }; \
+ \
+public: \
+ Y_FORCE_INLINE type Get##name() const \
+ { \
+ return name##_; \
+ } \
+ \
+ Y_FORCE_INLINE void Set##name(type value) &\
+ { \
+ name##_ = value; \
+ } \
+ \
+ Y_FORCE_INLINE declaringType&& Set##name(type value) &&\
+ { \
+ name##_ = value; \
+ return std::move(*this); \
+ } \
+ static_assert(true)
+
+//! Defines a trivial public read-write property that is passed by value
+//! and is not inline-initialized.
+#define DEFINE_BYVAL_RW_PROPERTY_NO_INIT(type, name, ...) \
+protected: \
+ type name##_; \
+ \
+public: \
+ Y_FORCE_INLINE type Get##name() const \
+ { \
+ return name##_; \
+ } \
+ \
+ Y_FORCE_INLINE void Set##name(type value) \
+ { \
+ name##_ = value; \
+ } \
+ static_assert(true)
+
+//! Forwards a trivial public read-write property that is passed by value.
+#define DELEGATE_BYVAL_RW_PROPERTY(declaringType, type, name, delegateTo) \
+ type declaringType::Get##name() const \
+ { \
+ return (delegateTo).Get##name(); \
+ } \
+ \
+ void declaringType::Set##name(type value) \
+ { \
+ (delegateTo).Set##name(value); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Declares a trivial public read-only property that is passed by value.
+#define DECLARE_BYVAL_RO_PROPERTY(type, name) \
+public: \
+ type Get##name() const
+
+//! Defines a trivial public read-only property that is passed by value.
+//! All arguments after name are used as default value (via braced-init-list).
+#define DEFINE_BYVAL_RO_PROPERTY(type, name, ...) \
+protected: \
+ type name##_ { __VA_ARGS__ }; \
+ \
+public: \
+ Y_FORCE_INLINE type Get##name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+
+//! Defines a trivial public read-only property that is passed by value
+//! and is not inline-initialized.
+#define DEFINE_BYVAL_RO_PROPERTY_NO_INIT(type, name) \
+protected: \
+ type name##_; \
+ \
+public: \
+ Y_FORCE_INLINE type Get##name() const \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+//! Forwards a trivial public read-only property that is passed by value.
+#define DELEGATE_BYVAL_RO_PROPERTY(declaringType, type, name, delegateTo) \
+ type declaringType::Get##name() \
+ { \
+ return (delegateTo).Get##name(); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Below are macro helpers for extra properties.
+//! Extra properties should be used for lazy memory allocation for properties that
+//! hold default values for the majority of objects.
+
+//! Initializes extra property holder if it is not initialized.
+#define INITIALIZE_EXTRA_PROPERTY_HOLDER(holder) \
+ if (!holder##_) { \
+ holder##_.reset(new decltype(holder##_)::element_type()); \
+ } \
+ static_assert(true)
+
+//! Declares an extra property holder. Holder contains extra properties values.
+//! Holder is not created until some property is set with a non-default value.
+//! If there is no holder property getter returns default value.
+#define DECLARE_EXTRA_PROPERTY_HOLDER(type, holder) \
+public: \
+ Y_FORCE_INLINE bool HasCustom##holder() const \
+ { \
+ return static_cast<bool>(holder##_); \
+ } \
+ Y_FORCE_INLINE const type* GetCustom##holder() const \
+ { \
+ return holder##_.get(); \
+ } \
+ Y_FORCE_INLINE type* GetCustom##holder() \
+ { \
+ return holder##_.get(); \
+ } \
+ Y_FORCE_INLINE void InitializeCustom##holder() \
+ { \
+ INITIALIZE_EXTRA_PROPERTY_HOLDER(holder); \
+ } \
+private: \
+ std::unique_ptr<type> holder##_; \
+ static const type Default##holder##_
+
+//! Defines a storage for extra properties default values.
+#define DEFINE_EXTRA_PROPERTY_HOLDER(class, type, holder) \
+ const type class::Default##holder##_
+
+//! Defines a public read-write extra property that is passed by value.
+#define DEFINE_BYVAL_RW_EXTRA_PROPERTY(holder, name) \
+public: \
+ Y_FORCE_INLINE decltype(holder##_->name) Get##name() const \
+ { \
+ if (!holder##_) { \
+ return Default##holder##_.name; \
+ } \
+ return holder##_->name; \
+ } \
+ Y_FORCE_INLINE void Set##name(decltype(holder##_->name) val) \
+ { \
+ if (!holder##_) { \
+ if (val == Default##holder##_.name) { \
+ return; \
+ } \
+ INITIALIZE_EXTRA_PROPERTY_HOLDER(holder); \
+ } \
+ holder##_->name = val; \
+ } \
+ static_assert(true)
+
+//! Defines a public read-write extra property that is passed by reference.
+#define DEFINE_BYREF_RW_EXTRA_PROPERTY(holder, name) \
+public: \
+ Y_FORCE_INLINE const decltype(holder##_->name)& name() const \
+ { \
+ if (!holder##_) { \
+ return Default##holder##_.name; \
+ } \
+ return holder##_->name; \
+ } \
+ Y_FORCE_INLINE decltype(holder##_->name)& Mutable##name() \
+ { \
+ INITIALIZE_EXTRA_PROPERTY_HOLDER(holder); \
+ return holder##_->name; \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/yt/string/raw_formatter.h b/library/cpp/yt/string/raw_formatter.h
new file mode 100644
index 0000000000..6956330883
--- /dev/null
+++ b/library/cpp/yt/string/raw_formatter.h
@@ -0,0 +1,212 @@
+#pragma once
+
+#include "guid.h"
+
+#include <algorithm>
+#include <array>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A dead-simple string formatter.
+/*!
+ * This formatter is intended to be as simple as possible and async signal safe.
+ * This is the reason we do not use printf(): it does not meet signal-safety
+ * requirements.
+ */
+
+class TBaseFormatter
+{
+public:
+ TBaseFormatter(char* buffer, int length)
+ : Begin_(buffer)
+ , Cursor_(buffer)
+ , End_(buffer + length)
+ { }
+
+ //! Returns an underlying cursor.
+ char* GetCursor()
+ {
+ return Cursor_;
+ }
+
+ //! Returns an pointer to the underlying buffer.
+ const char* GetData() const
+ {
+ return Begin_;
+ }
+
+ //! Returns the number of bytes written in the buffer.
+ int GetBytesWritten() const
+ {
+ return Cursor_ - Begin_;
+ }
+
+ //! Returns the number of bytes available in the buffer.
+ int GetBytesRemaining() const
+ {
+ return End_ - Cursor_;
+ }
+
+ //! Advances the internal cursor #count symbols forward (assuming the data is already present).
+ void Advance(int count)
+ {
+ Cursor_ += count;
+
+ if (Cursor_ > End_) {
+ Cursor_ = End_;
+ }
+ }
+
+ //! Drops trailing #count symbols (assuming these are present).
+ void Revert(int count)
+ {
+ Cursor_ -= count;
+ }
+
+ //! Appends the string and updates the internal cursor.
+ void AppendString(const char* string)
+ {
+ while (*string != '\0' && Cursor_ < End_) {
+ *Cursor_++ = *string++;
+ }
+ }
+
+ //! Appends the string and updates the internal cursor.
+ void AppendString(TStringBuf string)
+ {
+ size_t position = 0;
+ while (position < string.length() && Cursor_ < End_) {
+ *Cursor_++ = string[position++];
+ }
+ }
+
+ //! Appends a single character and updates the internal cursor.
+ void AppendChar(char ch)
+ {
+ if (Cursor_ < End_) {
+ *Cursor_++ = ch;
+ }
+ }
+
+ //! Formats |number| in base |radix| and updates the internal cursor.
+ void AppendNumber(uintptr_t number, int radix = 10, int width = 0, char ch = ' ')
+ {
+ int digits = 0;
+
+ if (radix == 16) {
+ // Optimize output of hex numbers.
+
+ uintptr_t reverse = 0;
+ int length = 0;
+ do {
+ reverse <<= 4;
+ reverse |= number & 0xf;
+ number >>= 4;
+ ++length;
+ } while (number > 0);
+
+ for (int index = 0; index < length && Cursor_ + digits < End_; ++index) {
+ unsigned int modulus = reverse & 0xf;
+ Cursor_[digits] = (modulus < 10 ? '0' + modulus : 'a' + modulus - 10);
+ ++digits;
+ reverse >>= 4;
+ }
+ } else {
+ while (Cursor_ + digits < End_) {
+ const int modulus = number % radix;
+ number /= radix;
+ Cursor_[digits] = (modulus < 10 ? '0' + modulus : 'a' + modulus - 10);
+ ++digits;
+ if (number == 0) {
+ break;
+ }
+ }
+
+ // Reverse the bytes written.
+ std::reverse(Cursor_, Cursor_ + digits);
+ }
+
+ if (digits < width) {
+ auto delta = width - digits;
+ std::copy(Cursor_, Cursor_ + digits, Cursor_ + delta);
+ std::fill(Cursor_, Cursor_ + delta, ch);
+ Cursor_ += width;
+ } else {
+ Cursor_ += digits;
+ }
+ }
+
+ //! Formats |number| as hexadecimal number and updates the internal cursor.
+ //! Padding will be added in front if needed.
+ void AppendNumberAsHexWithPadding(uintptr_t number, int width)
+ {
+ char* begin = Cursor_;
+ AppendString("0x");
+ AppendNumber(number, 16);
+ // Move to right and add padding in front if needed.
+ if (Cursor_ < begin + width) {
+ auto delta = begin + width - Cursor_;
+ std::copy(begin, Cursor_, begin + delta);
+ std::fill(begin, begin + delta, ' ');
+ Cursor_ = begin + width;
+ }
+ }
+
+ //! Formats |guid| and updates the internal cursor.
+ void AppendGuid(TGuid guid)
+ {
+ if (Y_LIKELY(End_ - Cursor_ >= MaxGuidStringSize)) {
+ // Fast path.
+ Cursor_ = WriteGuidToBuffer(Cursor_, guid);
+ } else {
+ // Slow path.
+ std::array<char, MaxGuidStringSize> buffer;
+ auto* end = WriteGuidToBuffer(buffer.data(), guid);
+ AppendString(TStringBuf(buffer.data(), end));
+ }
+ }
+
+ //! Resets the underlying cursor.
+ void Reset()
+ {
+ Cursor_ = Begin_;
+ }
+
+ TStringBuf GetBuffer() const
+ {
+ return {Begin_, Cursor_};
+ }
+
+private:
+ char* const Begin_;
+ char* Cursor_;
+ char* const End_;
+
+};
+
+template <size_t N>
+class TRawFormatter
+ : public TBaseFormatter
+{
+public:
+ TRawFormatter()
+ : TBaseFormatter(Buffer_, N)
+ { }
+
+ TRawFormatter(char* buffer, int length)
+ : TBaseFormatter(buffer, length)
+ { }
+
+private:
+ char Buffer_[N];
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/library/cpp/yt/threading/unittests/count_down_latch_ut.cpp b/library/cpp/yt/threading/unittests/count_down_latch_ut.cpp
new file mode 100644
index 0000000000..894bdab22a
--- /dev/null
+++ b/library/cpp/yt/threading/unittests/count_down_latch_ut.cpp
@@ -0,0 +1,78 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/threading/count_down_latch.h>
+
+#include <thread>
+
+namespace NYT::NThreading {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WaitForLatch(const TCountDownLatch& latch)
+{
+ latch.Wait();
+ EXPECT_EQ(0, latch.GetCount());
+}
+
+TEST(TCountDownLatch, TwoThreads)
+{
+ TCountDownLatch latch(2);
+
+ std::thread t1(std::bind(&WaitForLatch, std::cref(latch)));
+ std::thread t2(std::bind(&WaitForLatch, std::cref(latch)));
+
+ EXPECT_EQ(2, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(1, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(0, latch.GetCount());
+
+ t1.join();
+ t2.join();
+}
+
+TEST(TCountDownLatch, TwoThreadsPredecremented)
+{
+ TCountDownLatch latch(2);
+
+ EXPECT_EQ(2, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(1, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(0, latch.GetCount());
+
+ std::thread t1(std::bind(&WaitForLatch, std::cref(latch)));
+ std::thread t2(std::bind(&WaitForLatch, std::cref(latch)));
+
+ t1.join();
+ t2.join();
+}
+
+TEST(TCountDownLatch, TwoThreadsTwoLatches)
+{
+ TCountDownLatch first(1);
+ TCountDownLatch second(1);
+
+ std::thread t1([&] () {
+ first.Wait();
+ second.CountDown();
+ EXPECT_EQ(0, first.GetCount());
+ EXPECT_EQ(0, second.GetCount());
+ });
+
+ std::thread t2([&] () {
+ first.CountDown();
+ second.Wait();
+ EXPECT_EQ(0, first.GetCount());
+ EXPECT_EQ(0, second.GetCount());
+ });
+
+ t1.join();
+ t2.join();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NThreading
diff --git a/library/cpp/yt/threading/unittests/recursive_spin_lock_ut.cpp b/library/cpp/yt/threading/unittests/recursive_spin_lock_ut.cpp
new file mode 100644
index 0000000000..9c2d8f16cb
--- /dev/null
+++ b/library/cpp/yt/threading/unittests/recursive_spin_lock_ut.cpp
@@ -0,0 +1,88 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/threading/recursive_spin_lock.h>
+#include <library/cpp/yt/threading/event_count.h>
+
+#include <thread>
+
+namespace NYT::NThreading {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TRecursiveSpinLockTest, SingleThread)
+{
+ TRecursiveSpinLock lock;
+ EXPECT_FALSE(lock.IsLocked());
+ EXPECT_TRUE(lock.TryAcquire());
+ EXPECT_TRUE(lock.IsLocked());
+ EXPECT_TRUE(lock.TryAcquire());
+ EXPECT_TRUE(lock.IsLocked());
+ lock.Release();
+ EXPECT_TRUE(lock.IsLocked());
+ lock.Release();
+ EXPECT_FALSE(lock.IsLocked());
+ EXPECT_TRUE(lock.TryAcquire());
+ EXPECT_TRUE(lock.IsLocked());
+ lock.Release();
+ lock.Acquire();
+ lock.Release();
+}
+
+TEST(TRecursiveSpinLockTest, TwoThreads)
+{
+ TRecursiveSpinLock lock;
+ TEvent e1, e2, e3, e4, e5, e6, e7;
+
+ std::thread t1([&] {
+ e1.Wait();
+ EXPECT_TRUE(lock.IsLocked());
+ EXPECT_FALSE(lock.IsLockedByCurrentThread());
+ EXPECT_FALSE(lock.TryAcquire());
+ e2.NotifyOne();
+ e3.Wait();
+ EXPECT_TRUE(lock.IsLocked());
+ EXPECT_FALSE(lock.IsLockedByCurrentThread());
+ EXPECT_FALSE(lock.TryAcquire());
+ e4.NotifyOne();
+ e5.Wait();
+ EXPECT_FALSE(lock.IsLocked());
+ EXPECT_FALSE(lock.IsLockedByCurrentThread());
+ EXPECT_TRUE(lock.TryAcquire());
+ e6.NotifyOne();
+ e7.Wait();
+ lock.Release();
+ });
+
+ std::thread t2([&] {
+ EXPECT_FALSE(lock.IsLocked());
+ EXPECT_TRUE(lock.TryAcquire());
+ EXPECT_TRUE(lock.IsLockedByCurrentThread());
+ e1.NotifyOne();
+ e2.Wait();
+ EXPECT_TRUE(lock.TryAcquire());
+ EXPECT_TRUE(lock.IsLockedByCurrentThread());
+ e3.NotifyOne();
+ e4.Wait();
+ lock.Release();
+ lock.Release();
+ EXPECT_FALSE(lock.IsLocked());
+ e5.NotifyOne();
+ e6.Wait();
+ EXPECT_TRUE(lock.IsLocked());
+ EXPECT_FALSE(lock.IsLockedByCurrentThread());
+ e7.NotifyOne();
+ lock.Acquire();
+ lock.Acquire();
+ lock.Release();
+ lock.Release();
+ });
+
+ t1.join();
+ t2.join();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NThreading
diff --git a/library/cpp/yt/threading/unittests/spin_wait_ut.cpp b/library/cpp/yt/threading/unittests/spin_wait_ut.cpp
new file mode 100644
index 0000000000..8469634f34
--- /dev/null
+++ b/library/cpp/yt/threading/unittests/spin_wait_ut.cpp
@@ -0,0 +1,48 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/threading/spin_wait.h>
+#include <library/cpp/yt/threading/spin_wait_hook.h>
+
+#include <thread>
+#include <mutex>
+
+namespace NYT::NThreading {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool SpinWaitSlowPathHookInvoked;
+
+void SpinWaitSlowPathHook(
+ TCpuDuration cpuDelay,
+ const TSourceLocation& /*location*/,
+ ESpinLockActivityKind /*activityKind*/)
+{
+ SpinWaitSlowPathHookInvoked = true;
+ auto delay = CpuDurationToDuration(cpuDelay);
+ EXPECT_GE(delay, TDuration::Seconds(1));
+ EXPECT_LE(delay, TDuration::Seconds(5));
+}
+
+TEST(TSpinWaitTest, SlowPathHook)
+{
+ static std::once_flag registerFlag;
+ std::call_once(
+ registerFlag,
+ [] {
+ RegisterSpinWaitSlowPathHook(SpinWaitSlowPathHook);
+ });
+ SpinWaitSlowPathHookInvoked = false;
+ {
+ TSpinWait spinWait(__LOCATION__, ESpinLockActivityKind::ReadWrite);
+ for (int i = 0; i < 1'000'000; ++i) {
+ spinWait.Wait();
+ }
+ }
+ EXPECT_TRUE(SpinWaitSlowPathHookInvoked);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NThreading
diff --git a/library/cpp/yt/threading/unittests/ya.make b/library/cpp/yt/threading/unittests/ya.make
new file mode 100644
index 0000000000..ef9b5d2995
--- /dev/null
+++ b/library/cpp/yt/threading/unittests/ya.make
@@ -0,0 +1,17 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ count_down_latch_ut.cpp
+ recursive_spin_lock_ut.cpp
+ spin_wait_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/threading
+ library/cpp/testing/gtest
+)
+
+END()
diff --git a/library/cpp/yt/user_job_statistics/user_job_statistics.cpp b/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
new file mode 100644
index 0000000000..b7fd71503d
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/user_job_statistics.cpp
@@ -0,0 +1,133 @@
+#include "user_job_statistics.h"
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <util/stream/null.h>
+#include <util/string/builder.h>
+#include <util/system/mutex.h>
+#include <util/system/env.h>
+
+using namespace NYtTools;
+
+static TMutex GlobalStatsWritingMutex;
+
+#if defined(_unix_)
+const FHANDLE TUserJobStatsProxy::JobStatisticsHandle = 5;
+#elif defined(_win_)
+const FHANDLE TUserJobStatsProxy::JobStatisticsHandle = nullptr;
+#endif
+
+static IOutputStream* CorrectHandle(const FHANDLE h) {
+#if defined(_unix_)
+ if (fcntl(h, F_GETFD) == -1) {
+ return &Cerr;
+ }
+ return nullptr;
+#elif defined(_win_)
+ return &Cerr;
+#endif
+}
+
+static TString PrintNodeSimple(const NYT::TNode& n) {
+ return NYT::NodeToYsonString(n, NYson::EYsonFormat::Text);
+}
+
+void TUserJobStatsProxy::Init(IOutputStream * usingStream) {
+ if (usingStream == nullptr) {
+ usingStream = CorrectHandle(JobStatisticsHandle);
+ }
+
+ if(usingStream == nullptr && GetEnv("YT_JOB_ID").empty()) {
+ usingStream = &Cerr;
+ }
+
+
+ if (usingStream == nullptr) {
+ TFileHandle fixedDesrc(JobStatisticsHandle);
+ FetchedOut = MakeHolder<TFixedBufferFileOutput>(TFile(fixedDesrc.Duplicate()));
+ UsingStream = FetchedOut.Get();
+ fixedDesrc.Release();
+ } else {
+ UsingStream = usingStream;
+ }
+}
+
+void TUserJobStatsProxy::InitChecked(IOutputStream* def) {
+ IOutputStream* usingStream = CorrectHandle(JobStatisticsHandle);
+
+ if (usingStream == nullptr && !GetEnv("YT_JOB_ID").empty()) {
+ TFileHandle fixedDesrc(JobStatisticsHandle);
+ FetchedOut = MakeHolder<TFixedBufferFileOutput>(TFile(fixedDesrc.Duplicate()));
+ UsingStream = FetchedOut.Get();
+ fixedDesrc.Release();
+ } else {
+ UsingStream = def;
+ }
+}
+
+void TUserJobStatsProxy::InitIfNotInited(IOutputStream * usingStream) {
+ if (UsingStream == nullptr) {
+ Init(usingStream);
+ }
+}
+
+void TUserJobStatsProxy::CommitStats() {
+ if (Stats.empty()) {
+ return;
+ }
+
+ auto res = NYT::TNode::CreateMap();
+ for (auto& p : Stats) {
+ res[p.first] = p.second;
+ }
+ for (auto& p : TimeStats) {
+ res[p.first] = p.second.MilliSeconds();
+ }
+ with_lock(GlobalStatsWritingMutex) {
+ *UsingStream << PrintNodeSimple(res) << ";" << Endl;
+ }
+ Stats.clear();
+}
+
+
+TTimeStatHolder TUserJobStatsProxy::TimerStart(TString name, bool commitOnFinish) {
+ return THolder(new TTimeStat(this, name, commitOnFinish));
+}
+
+void TUserJobStatsProxy::WriteStat(TString name, i64 val) {
+ auto res = NYT::TNode {} (name, val);
+ with_lock(GlobalStatsWritingMutex) {
+ *UsingStream << PrintNodeSimple(res) << ";" << Endl;
+ }
+}
+
+void TUserJobStatsProxy::WriteStatNoFlush(TString name, i64 val) {
+ auto res = NYT::TNode {} (name, val);
+ with_lock(GlobalStatsWritingMutex) {
+ *UsingStream << (TStringBuilder{} << PrintNodeSimple(res) << ";\n");
+ }
+}
+
+TTimeStat::TTimeStat(TUserJobStatsProxy* parent, TString name, bool commit)
+ : Parent(parent)
+ , Name(name)
+ , Commit(commit) {}
+
+TTimeStat::~TTimeStat() {
+ Finish();
+}
+
+void TTimeStat::Cancel() {
+ Parent = nullptr;
+}
+
+void TTimeStat::Finish() {
+ if (!Parent) {
+ return;
+ }
+
+ if (Commit) {
+ Parent->WriteStatNoFlush(Name, Timer.Get().MilliSeconds());
+ } else {
+ Parent->TimeStats[Name] += Timer.Get();
+ }
+ Cancel();
+}
diff --git a/library/cpp/yt/user_job_statistics/user_job_statistics.h b/library/cpp/yt/user_job_statistics/user_job_statistics.h
new file mode 100644
index 0000000000..6939d20417
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/user_job_statistics.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <util/stream/file.h>
+#include <util/generic/hash.h>
+#include <util/datetime/cputimer.h>
+
+namespace NYtTools {
+ class TTimeStat;
+ using TTimeStatHolder = THolder<TTimeStat>;
+
+ class TUserJobStatsProxy {
+ public:
+ static const FHANDLE JobStatisticsHandle;
+ private:
+ THolder<IOutputStream> FetchedOut;
+ IOutputStream* UsingStream = &Cerr;
+ public:
+ // TODO: add inheritance
+ THashMap<TString, i64> Stats;//will be dumped in CommitStats or desctructor
+ THashMap<TString, TDuration> TimeStats;//will be dumped in CommitStats or desctructor
+
+ TUserJobStatsProxy() { Init(nullptr); }
+ ~TUserJobStatsProxy() {
+ CommitStats();
+ }
+ TUserJobStatsProxy (IOutputStream* usingStream) {Init(usingStream);}
+
+ void Init(IOutputStream* usingStream);
+ void InitChecked(IOutputStream* ifNotInJob);
+ void InitIfNotInited(IOutputStream* usingStream);
+ IOutputStream* GetStream() const { return UsingStream; }
+ void CommitStats();
+ void WriteStat(TString name, i64 val); //immidiatly wirtes stat
+ void WriteStatNoFlush(TString name, i64 val); //immidiatly wirtes stat but do not flush it
+
+ //@param name name of statistic to be written in millisecs from creation to destruction
+ //@param commitOnFinish if false: will update state/write on job finish; if true: write stat in destructor
+ TTimeStatHolder TimerStart(TString name, bool commitOnFinish = false);
+ };
+
+ class TTimeStat {
+ TUserJobStatsProxy* Parent;
+ TString Name;
+ bool Commit;
+
+ TTimeStat(TUserJobStatsProxy* parent, TString name, bool commit);
+ friend class TUserJobStatsProxy;
+
+ TSimpleTimer Timer;
+ public:
+ ~TTimeStat();
+ TDuration Get() const {
+ return Timer.Get();
+ }
+ void Cancel();
+ void Finish();
+ };
+}
diff --git a/library/cpp/yt/user_job_statistics/ya.make b/library/cpp/yt/user_job_statistics/ya.make
new file mode 100644
index 0000000000..7179660b31
--- /dev/null
+++ b/library/cpp/yt/user_job_statistics/ya.make
@@ -0,0 +1,11 @@
+LIBRARY()
+
+SRCS(
+ user_job_statistics.cpp
+)
+
+PEERDIR(
+ yt/cpp/mapreduce/common
+)
+
+END()
diff --git a/ydb/library/yql/core/extract_predicate/ut/extract_predicate_ut.cpp b/ydb/library/yql/core/extract_predicate/ut/extract_predicate_ut.cpp
new file mode 100644
index 0000000000..7951c2fb61
--- /dev/null
+++ b/ydb/library/yql/core/extract_predicate/ut/extract_predicate_ut.cpp
@@ -0,0 +1,858 @@
+#include "extract_predicate_impl.h"
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h>
+#include <ydb/library/yql/providers/config/yql_config_provider.h>
+#include <ydb/library/yql/providers/result/provider/yql_result_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/sql/settings/translation_settings.h>
+#include <ydb/library/yql/sql/sql.h>
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/facade/yql_facade.h>
+#include <ydb/library/yql/core/services/yql_transform_pipeline.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/string/cast.h>
+#include <util/system/user.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TYqlExtractPredicate) {
+
+ TExprNode::TPtr ParseAndOptimize(const TString& program, TExprContext& exprCtx, TTypeAnnotationContextPtr& typesCtx) {
+ NSQLTranslation::TTranslationSettings settings;
+ settings.ClusterMapping["plato"] = YtProviderName;
+ settings.SyntaxVersion = 1;
+
+ TAstParseResult astRes = SqlToYql(program, settings);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ typesCtx = MakeIntrusive<TTypeAnnotationContext>();
+ typesCtx->RandomProvider = CreateDeterministicRandomProvider(1);
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typesCtx.Get();
+
+ InitializeYtGateway(ytGateway, ytState);
+ typesCtx->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ typesCtx->AddDataSource(YtProviderName, CreateYtDataSource(ytState));
+
+ typesCtx->AddDataSource(ConfigProviderName, CreateConfigProvider(*typesCtx, nullptr, ""));
+
+ auto transformer = TTransformationPipeline(typesCtx)
+ .AddServiceTransformers()
+ .AddPreTypeAnnotation()
+ .AddExpressionEvaluation(*functionRegistry)
+ .AddIOAnnotation()
+ .AddTypeAnnotation()
+ .AddPostTypeAnnotation()
+ .AddOptimization()
+ .Build();
+
+ auto status = SyncTransform(*transformer, exprRoot, exprCtx);
+ if (status != IGraphTransformer::TStatus::Ok) {
+ auto issues = exprCtx.IssueManager.GetIssues();
+ issues.PrintTo(Cerr);
+ }
+ UNIT_ASSERT(status == IGraphTransformer::TStatus::Ok);
+
+ return exprRoot;
+ }
+
+ TString DumpNode(const TExprNode& node, TExprContext& exprCtx) {
+ auto ast = ConvertToAst(node, exprCtx, TExprAnnotationFlags::None, true);
+ return ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ }
+
+ TExprNode::TPtr LocateFilterLambda(const TExprNode::TPtr& root) {
+ TExprNode::TPtr filterLambda;
+ VisitExpr(root, [&filterLambda] (const TExprNode::TPtr& node) {
+ if (node->IsCallable({"FlatMap", "OrderedFlatMap"})) {
+ auto lambda = node->Child(1);
+ if (lambda->Tail().IsCallable("OptionalIf")) {
+ filterLambda = lambda;
+ }
+ }
+ return !filterLambda;
+ });
+
+ UNIT_ASSERT(filterLambda);
+ return filterLambda;
+ }
+
+ void CheckRanges(const TExprNode::TPtr& exprRoot, const TVector<TString>& keyColumns, TExprContext& exprCtx, TTypeAnnotationContext& typesCtx, const TString& expectedRanges,
+ const THashSet<TString>& expectedColumns, const TString& expectedComputeRanges = "", bool verbose = false)
+ {
+ if (verbose) {
+ Cerr << "Input: " << DumpNode(*exprRoot, exprCtx) << "\n";
+ }
+
+ TExprNode::TPtr filterLambda = LocateFilterLambda(exprRoot);
+
+ THashSet<TString> usedColumns;
+ using NDetail::TPredicateRangeExtractor;
+
+ THolder<TPredicateRangeExtractor> extractor{new TPredicateRangeExtractor};
+
+ UNIT_ASSERT(extractor->Prepare(filterLambda, *filterLambda->Head().Head().GetTypeAnn(), usedColumns, exprCtx, typesCtx));
+ auto range = extractor->GetPreparedRange();
+ UNIT_ASSERT(range);
+ auto rangeDump = DumpNode(*range, exprCtx);
+
+ TString computeRangeDump;
+ if (expectedComputeRanges) {
+ auto buildResult = extractor->BuildComputeNode(keyColumns, exprCtx);
+ if (buildResult.ComputeNode) {
+ computeRangeDump = DumpNode(*buildResult.ComputeNode, exprCtx);
+ }
+ }
+
+ if (verbose) {
+ Cerr << "Ranges: " << rangeDump << "\n";
+ Cerr << "ComputeRanges: " << computeRangeDump << "\n";
+ }
+
+ UNIT_ASSERT_NO_DIFF(expectedRanges, rangeDump);
+ UNIT_ASSERT(expectedColumns == usedColumns);
+ if (expectedComputeRanges) {
+ UNIT_ASSERT_NO_DIFF(expectedComputeRanges, computeRangeDump);
+ }
+ }
+
+ struct TRunSingleProgram {
+ TString Src;
+ TString TmpDir;
+ TString Parameters;
+ IOutputStream& Err;
+ TVector<TString> Res;
+ THashMap<TString, TString> Tables;
+
+ TRunSingleProgram(const TString& src, IOutputStream& err)
+ : Src(src)
+ , Err(err)
+ {
+ }
+
+ bool Run(
+ const NKikimr::NMiniKQL::IFunctionRegistry* funcReg
+ ) {
+ auto yqlNativeServices = NFile::TYtFileServices::Make(funcReg, Tables, {}, TmpDir);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+
+ TVector<TDataProviderInitializer> dataProvidersInit;
+ dataProvidersInit.push_back(GetYtNativeDataProviderInitializer(ytGateway));
+ TProgramFactory factory(true, funcReg, 0ULL, dataProvidersInit, "ut");
+
+ TProgramPtr program = factory.Create("-stdin-", Src);
+ program->ConfigureYsonResultFormat(NYson::EYsonFormat::Text);
+ if (!Parameters.empty()) {
+ program->SetParametersYson(Parameters);
+ }
+
+ if (!program->ParseYql() || !program->Compile(GetUsername())) {
+ program->PrintErrorsTo(Err);
+ return false;
+ }
+
+ TProgram::TStatus status = program->Run(GetUsername());
+ if (status == TProgram::TStatus::Error) {
+ program->PrintErrorsTo(Err);
+ }
+ Res = program->Results();
+ return status == TProgram::TStatus::Ok;
+ }
+
+ void AddResults(TVector<TString>& res) const {
+ res.insert(res.end(), Res.begin(), Res.end());
+ }
+
+ bool Finished() const {
+ return true;
+ }
+ };
+
+ template <typename TDriver>
+ TVector<TString> Run(TDriver& driver) {
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+
+ TVector<TString> res;
+ do {
+ const bool runRes = driver.Run(functionRegistry.Get());
+ UNIT_ASSERT(runRes);
+
+ driver.AddResults(res);
+ } while (!driver.Finished());
+ return res;
+ }
+
+ TVector<TString> RunProgram(const TString& programSrc, const THashMap<TString, TString>& tables, const TString& tmpDir = TString(), const TString& params = TString()) {
+ TRunSingleProgram driver(programSrc, Cerr);
+ driver.Tables = tables;
+ driver.TmpDir = tmpDir;
+ driver.Parameters = params;
+ return Run(driver);
+ }
+
+ void ExecuteAndCheckRanges(const TString& toExecute, const TString& expectedExecuteResult) {
+ auto s = TStringBuilder() <<
+ "(\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (block '" << toExecute << "))\n"
+ "(let world (Write! world res_sink (Key) data '()))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+ //Cerr << "PROG: " << s << "\n";
+ auto res = RunProgram(s, THashMap<TString, TString>());
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ //Cerr << "RES: " << res[0] << "\n";
+ UNIT_ASSERT_NO_DIFF(expectedExecuteResult, res[0]);
+ }
+
+ void ExtractAndExecuteRanges(const TString& prog, const TString& expectedExecuteResult) {
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+
+ TExprNode::TPtr filterLambda = LocateFilterLambda(exprRoot);
+
+ THashSet<TString> usedColumns;
+ using NDetail::TPredicateRangeExtractor;
+
+ TPredicateExtractorSettings settings;
+ settings.HaveNextValueCallable = true;
+ auto extractor = MakePredicateRangeExtractor(settings);
+
+ UNIT_ASSERT(extractor->Prepare(filterLambda, *filterLambda->Head().Head().GetTypeAnn(), usedColumns, exprCtx, *typesCtx));
+
+ auto buildResult = extractor->BuildComputeNode({ "x", "y"}, exprCtx);
+ UNIT_ASSERT(buildResult.ComputeNode);
+
+ ExecuteAndCheckRanges(DumpNode(*buildResult.ComputeNode, exprCtx), expectedExecuteResult);
+ }
+
+ TString GetOptionalSrc() {
+ return
+ "use plato;\n"
+ "\n"
+ "$src = [\n"
+ " <|x:1/0, y:2/0|>,\n"
+ " <|x:1/0, y:1|>,\n"
+ " <|x:1, y:1/0|>,\n"
+ "];\n"
+ "\n";
+ }
+
+ TString GetNonOptionsSrc() {
+ return
+ "use plato;\n"
+ "\n"
+ "$src = [\n"
+ " <|x:1, y:2|>,\n"
+ " <|x:3, y:4|>,\n"
+ "];\n"
+ "\n";
+ }
+
+ TString GetNonOptionsSrc4() {
+ return
+ "use plato;\n"
+ "\n"
+ "$src = [\n"
+ " <|x:1, y:2, z:3, t:4|>,\n"
+ " <|x:3, y:4, z:5, t:6|>,\n"
+ "];\n"
+ "\n";
+ }
+
+ Y_UNIT_TEST(PropagateNot) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where not (3 > x or not(y > 3));";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Int32 '\"3\"))\n"
+ "(return (RangeAnd (Range $2 (lambda '($4) (Coalesce (>= (Member $4 '\"x\") $3) (Bool 'false)))) (Range $2 (lambda '($5) (Coalesce (> (Member $5 '\"y\") $3) (Bool 'false))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x", "y" });
+ }
+
+ Y_UNIT_TEST(RangeRestInOr) {
+ TString prog = GetNonOptionsSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (3 > x or (x + y) > 3);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (DataType 'Int32))\n"
+ "(return (RangeRest (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (block '(\n"
+ " (let $3 (Int32 '\"3\"))\n"
+ " (let $4 (Member $2 '\"x\"))\n"
+ " (return (Or (> $3 $4) (> (+ $4 (Member $2 '\"y\")) $3)))\n"
+ ")))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, {});
+ }
+
+ Y_UNIT_TEST(RangeRestDueToDisjointColumnSet) {
+ TString prog = GetNonOptionsSrc4() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x = 1 and y = 2) or (z = 3 and t = 4);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (DataType 'Int32))\n"
+ "(let $2 (StructType '('\"t\" $1) '('\"x\" $1) '('\"y\" $1) '('\"z\" $1)))\n"
+ "(return (RangeRest $2 (lambda '($3) (Or (And (== (Member $3 '\"x\") (Int32 '1)) (== (Member $3 '\"y\") (Int32 '\"2\"))) (And (== (Member $3 '\"z\") (Int32 '\"3\")) (== (Member $3 '\"t\") (Int32 '\"4\")))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, {});
+ }
+
+ Y_UNIT_TEST(Exists) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x is null;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(return (Range (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (Not (Exists (Member $2 '\"x\"))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x" });
+ }
+
+#if 0
+ Y_UNIT_TEST(SqlNotIn) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where y not in (1, 2, 3);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(return (Range (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (block '(\n"
+ " (let $3 '((Int32 '\"1\") (Int32 '\"2\") (Int32 '\"3\")))\n"
+ " (let $4 (SqlIn $3 (Member $2 '\"y\") '()))\n"
+ " (return (Not (Coalesce $4 (Bool 'true))))\n"
+ ")))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "y" });
+ }
+#endif
+
+ Y_UNIT_TEST(SqlIn) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x in (4294967295u, 2u, 1u) AND y != 100;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Bool 'false))\n"
+ "(return (RangeAnd (Range $2 (lambda '($4) (block '(\n"
+ " (let $5 '((Uint32 '\"4294967295\") (Uint32 '\"2\") (Uint32 '1)))\n"
+ " (let $6 (SqlIn $5 (Member $4 '\"x\") '()))\n"
+ " (return (Coalesce $6 $3))\n"
+ ")))) (Range $2 (lambda '($7) (Coalesce (!= (Member $7 '\"y\") (Int32 '\"100\")) $3)))))\n"
+ ")\n";
+
+ TString expectedComputeRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (IfPresent (Map (Just '((Uint32 '\"4294967295\") (Uint32 '\"2\") (Uint32 '1))) (lambda '($4) (AsList (Nth $4 '0) (Nth $4 '1) (Nth $4 '2)))) (lambda '($5) (block '(\n"
+ " (let $6 (Collect (Take (FlatMap $5 (lambda '($8) (block '(\n"
+ " (let $9 (RangeFor '== $8 $1))\n"
+ " (return (RangeMultiply (Uint64 '10000) $9))\n"
+ " )))) (Uint64 '10001))))\n"
+ " (let $7 '((Nothing (OptionalType $1)) (Int32 '0)))\n"
+ " (return (IfStrict (> (Length $6) (Uint64 '10000)) (AsRange '($7 $7)) (RangeUnion $6)))\n"
+ "))) (RangeEmpty $1)))\n"
+ "(let $3 (RangeFor '!= (Int32 '\"100\") $1))\n"
+ "(return (RangeFinalize (RangeMultiply (Uint64 '10000) (RangeUnion (RangeIntersect (RangeMultiply (Uint64 '10000) $2 $3))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x", "y" }, expectedComputeRanges);
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["1"]];[#];"0"];[[["1"]];[["100"]];"0"]];[[[["1"]];[["100"]];"0"];[[["1"]];#;"1"]];[[[["2"]];[#];"0"];[[["2"]];[["100"]];"0"]];[[[["2"]];[["100"]];"0"];[[["2"]];#;"1"]]]}]})__";
+ ExecuteAndCheckRanges(expectedComputeRanges, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(AndWithOr) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x > 2 or (x < 1 and y > 0);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Bool 'false))\n"
+ "(return (RangeOr (Range $2 (lambda '($4) (Coalesce (> (Member $4 '\"x\") (Int32 '\"2\")) $3))) (RangeAnd (Range $2 (lambda '($5) (Coalesce (< (Member $5 '\"x\") (Int32 '1)) $3))) (Range $2 (lambda '($6) (Coalesce (> (Member $6 '\"y\") "
+ "(Int32 '0)) $3))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "y", "x" });
+ }
+
+ Y_UNIT_TEST(OrWithConst) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where 1 > 2 or x > 2;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (Int32 '\"2\"))\n"
+ "(let $2 (OptionalType (DataType 'Int32)))\n"
+ "(return (RangeOr (RangeConst (> (Int32 '1) $1)) (Range (StructType '('\"x\" $2) '('\"y\" $2)) (lambda '($3) (Coalesce (> (Member $3 '\"x\") $1) (Bool 'false))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x" }, exprCtx, *typesCtx, expectedRanges, { "x" });
+ }
+
+ Y_UNIT_TEST(AndWithAllConst) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where 3 > 0 and 1 > 2;";
+
+ TString expectedRanges =
+ "(\n"
+ "(return (RangeConst (And (> (Int32 '\"3\") (Int32 '0)) (> (Int32 '1) (Int32 '\"2\")))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { });
+ }
+
+ Y_UNIT_TEST(OrWithDifferentColumns) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x > 0 or 1 < y;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(return (RangeRest (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (block '(\n"
+ " (let $3 (Bool 'false))\n"
+ " (return (Or (Coalesce (> (Member $2 '\"x\") (Int32 '0)) $3) (Coalesce (< (Int32 '1) (Member $2 '\"y\")) $3)))\n"
+ ")))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { });
+ }
+
+ Y_UNIT_TEST(TupleEquals) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < y and (x, y) == (1, 2);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Bool 'false))\n"
+ "(return (RangeAnd (RangeRest $2 (lambda '($4) (Coalesce (< (Member $4 '\"x\") (Member $4 '\"y\")) $3))) (Range $2 (lambda '($5) (Coalesce (== (Member $5 '\"x\") (Int32 '1)) $3))) (Range $2 (lambda '($6) (Coalesce (== (Member $6 '\"y\") (Int32 '\"2\")) $3)))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x", "y" });
+ }
+
+ Y_UNIT_TEST(TupleLessOrEquals) {
+ TString prog = GetNonOptionsSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x, y) <= (1, 2);";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (DataType 'Int32))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Int32 '1))\n"
+ "(let $4 (Int32 '\"2\"))\n"
+ "(return (RangeOr (RangeAnd (Range $2 (lambda '($5) (== (Member $5 '\"x\") $3))) (Range $2 (lambda '($6) (== (Member $6 '\"y\") $4)))) (Range $2 (lambda '($7) (< (Member $7 '\"x\") $3))) (RangeAnd (Range $2 (lambda '($8) (== (Memb"
+ "er $8 '\"x\") $3))) (Range $2 (lambda '($9) (< (Member $9 '\"y\") $4))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x", "y" });
+ }
+
+ Y_UNIT_TEST(BasicRange) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 1 + 1;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(return (Range (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (block '(\n"
+ " (let $3 (Int32 '1))\n"
+ " (return (Coalesce (< (Member $2 '\"x\") (+ $3 $3)) (Bool 'false)))\n"
+ ")))))\n"
+ ")\n";
+
+ TString expectedComputeRanges =
+ "(\n"
+ "(let $1 (Int32 '1))\n"
+ "(return (RangeFinalize (RangeMultiply (Uint64 '10000) (RangeUnion (RangeFor '< (+ $1 $1) (OptionalType (DataType 'Int32)))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x" }, exprCtx, *typesCtx, expectedRanges, { "x" }, expectedComputeRanges);
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];"0"];[[["2"]];"0"]]]}]})__";
+ ExecuteAndCheckRanges(expectedComputeRanges, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(BasicRange2) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 1 + 1 and x >= 0 or x < -10";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(let $2 (StructType '('\"x\" $1) '('\"y\" $1)))\n"
+ "(let $3 (Bool 'false))\n"
+ "(return (RangeOr (RangeAnd (Range $2 (lambda '($4) (block '(\n"
+ " (let $5 (Int32 '1))\n"
+ " (return (Coalesce (< (Member $4 '\"x\") (+ $5 $5)) $3))\n"
+ ")))) (Range $2 (lambda '($6) (Coalesce (>= (Member $6 '\"x\") (Int32 '0)) $3)))) (Range $2 (lambda '($7) (Coalesce (< (Member $7 '\"x\") (Int32 '\"-10\")) $3)))))\n"
+ ")\n";
+
+ TString expectedComputeRanges =
+ "(\n"
+ "(let $1 (Int32 '1))\n"
+ "(let $2 (OptionalType (DataType 'Int32)))\n"
+ "(let $3 (RangeFor '< (+ $1 $1) $2))\n"
+ "(let $4 (RangeFor '>= (Int32 '0) $2))\n"
+ "(let $5 (RangeFor '< (Int32 '\"-10\") $2))\n"
+ "(let $6 '((Nothing (OptionalType $2)) (Int32 '0)))\n"
+ "(return (RangeFinalize (RangeMultiply (Uint64 '10000) (RangeUnion (RangeMultiply (Uint64 '10000) (RangeUnion (RangeIntersect (RangeIntersect $3 $4)) $5) (AsRange '($6 $6)))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x", "y" }, exprCtx, *typesCtx, expectedRanges, { "x" }, expectedComputeRanges);
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["-10"]];#;"0"]];[[[["0"]];#;"1"];[[["2"]];#;"0"]]]}]})__";
+ ExecuteAndCheckRanges(expectedComputeRanges, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(BasicIntRounding) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 4294967295u;";
+
+ TString expectedRanges =
+ "(\n"
+ "(let $1 (OptionalType (DataType 'Int32)))\n"
+ "(return (Range (StructType '('\"x\" $1) '('\"y\" $1)) (lambda '($2) (Coalesce (< (Member $2 '\"x\") (Uint32 '\"4294967295\")) (Bool 'false)))))\n"
+ ")\n";
+
+ TString expectedComputeRanges =
+ "(\n"
+ "(return (RangeFinalize (RangeMultiply (Uint64 '10000) (RangeUnion (RangeFor '< (Uint32 '\"4294967295\") (OptionalType (DataType 'Int32)))))))\n"
+ ")\n";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ CheckRanges(exprRoot, { "x" }, exprCtx, *typesCtx, expectedRanges, { "x" }, expectedComputeRanges);
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];"0"];[[["2147483647"]];"1"]]]}]})__";
+ ExecuteAndCheckRanges(expectedComputeRanges, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeWithFalseConst) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 1 and 1+1 > 2;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeWithTrueConst) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 1 and 1+1 >= 2;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["1"]];#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeSinglePoint) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x == 10;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["10"]];#;"1"];[[["10"]];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeWithoutSinglePoint) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x != 10;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["10"]];#;"0"]];[[[["10"]];#;"0"];[#;#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeUnionOverlapping) {
+ // (1, 3) union (2, 4]
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x > 1 and x < 3) or (x <= 4 and x > 2);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["1"]];#;"0"];[[["4"]];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeUnionAdjacent) {
+ // (1, 3) union [3, 4)
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x > 1 and x < 3) or (x < 4 and x >= 3);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["1"]];#;"0"];[[["4"]];#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeUnionDisjoint) {
+ // (-inf, 2) union [3, 4)
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 2 or (x < 4 and x >= 3);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["2"]];#;"0"]];[[[["3"]];#;"1"];[[["3"]];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeIntersect3) {
+ // ((-inf, 3) or [5, +inf)) intersect [2, 6)
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x < 3 or x >= 5) and (x >= 2 and x < 6);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["2"]];#;"1"];[[["2"]];#;"1"]];[[[["5"]];#;"1"];[[["5"]];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(TupleLess) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x, y) < (5, 1 + 3);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["5"]];#;"0"]];[[[["5"]];[#];"0"];[[["5"]];[["4"]];"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(TupleLessOrEquals2) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where (x, y) <= (5, 1 + 3);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["5"]];#;"0"]];[[[["5"]];[#];"0"];[[["5"]];[["5"]];"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(DateRounding) {
+ TString prefixDt =
+ "use plato;\n"
+ "$src = [<|x:CurrentUtcDatetime(), y:1|>, <|x:CurrentUtcDatetime(), y:2|>];\n"
+ "insert into Output with truncate\n";
+
+ ExtractAndExecuteRanges(prefixDt + "select * from as_table($src) where x > Timestamp('2105-12-31T23:59:59.123456Z');",
+ R"__({"Write"=[{"Data"=[]}]})__");
+ ExtractAndExecuteRanges(prefixDt + "select * from as_table($src) where x > Timestamp('2105-12-31T23:59:58.123456Z');",
+ R"__({"Write"=[{"Data"=[[[["4291747199"];#;"1"];[#;#;"0"]]]}]})__");
+ ExtractAndExecuteRanges(prefixDt + "select * from as_table($src) where x < Timestamp('2105-12-31T23:59:59.123456Z');",
+ R"__({"Write"=[{"Data"=[[[#;#;"0"];[["4291747199"];#;"1"]]]}]})__");
+
+ TString prefixDate =
+ "use plato;\n"
+ "$src = [<|x:Just(CurrentUtcDate()), y:1|>, <|x:CurrentUtcDate(), y:2|>];\n"
+ "insert into Output with truncate\n";
+
+ ExtractAndExecuteRanges(prefixDate + "select * from as_table($src) where x > Datetime('2105-12-31T01:00:00Z');",
+ R"__({"Write"=[{"Data"=[]}]})__");
+ ExtractAndExecuteRanges(prefixDate + "select * from as_table($src) where x > Timestamp('2105-12-30T23:59:59Z');",
+ R"__({"Write"=[{"Data"=[[[[["49672"]];#;"1"];[#;#;"0"]]]}]})__");
+ ExtractAndExecuteRanges(prefixDate + "select * from as_table($src) where x < Timestamp('2105-12-31T23:59:59Z');",
+ R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["49672"]];#;"1"]]]}]})__");
+ }
+
+ Y_UNIT_TEST(RangeRestInAnd) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:1, y:2, z:3|>, <|x:1/0, y:1/0, z:3|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x < 10 and z in (1, 2);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["10"]];#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(RangeRestInAnd2) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:1, y:2, z:3|>, <|x:1/0, y:1/0, z:3|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x == 10 and y > 123 and z in (1, 2);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[["10"]];[["123"]];"0"];[[["10"]];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(SecondIndexAndTuple) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where y > 2 and (x, y) <= (5, 1 + 3);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[[["5"]];#;"0"]];[[[["5"]];[#];"0"];[[["5"]];[["5"]];"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(IsNotNull) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x is not null;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"0"];[#;#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(IsNull) {
+ TString prog = GetOptionalSrc() +
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x is null;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[[#];#;"1"];[[#];#;"1"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(NaNComparison) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:Double('nan'), y:1|>, <|x:Double('inf'), y:2|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x > Double('nan');";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(NaNEquals) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:Decimal('nan', 15, 10), y:1|>, <|x:Decimal('inf', 15, 10), y:2|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x = Decimal('nan', 15, 10);";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+ Y_UNIT_TEST(NaNNotEquals) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:Float('nan'), y:1|>, <|x:Float('inf'), y:2|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x != 0.0f/0.0f;";
+
+ TString expectedExecuteResult = R"__({"Write"=[{"Data"=[[[#;#;"0"];[#;#;"0"]]]}]})__";
+ ExtractAndExecuteRanges(prog, expectedExecuteResult);
+ }
+
+
+ Y_UNIT_TEST(PointPrefixLen) {
+ TString prog =
+ "use plato;\n"
+ "$src = [<|x:1, y:1, z:1|>, <|x:2, y:2, z:2|>];\n"
+ "insert into Output with truncate\n"
+ "select * from as_table($src) where x = 1 and y in (1, 2, 3) and z > 0;";
+
+ TExprContext exprCtx;
+ TTypeAnnotationContextPtr typesCtx;
+ TExprNode::TPtr exprRoot = ParseAndOptimize(prog, exprCtx, typesCtx);
+ TExprNode::TPtr filterLambda = LocateFilterLambda(exprRoot);
+
+ THashSet<TString> usedColumns;
+ using NDetail::TPredicateRangeExtractor;
+
+ TPredicateExtractorSettings settings;
+ settings.HaveNextValueCallable = true;
+ auto extractor = MakePredicateRangeExtractor(settings);
+
+ UNIT_ASSERT(extractor->Prepare(filterLambda, *filterLambda->Head().Head().GetTypeAnn(), usedColumns, exprCtx, *typesCtx));
+
+ auto buildResult = extractor->BuildComputeNode({ "x", "y", "z"}, exprCtx);
+ UNIT_ASSERT(buildResult.ComputeNode);
+ UNIT_ASSERT(buildResult.UsedPrefixLen == 3);
+ UNIT_ASSERT(buildResult.PointPrefixLen == 2);
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/extract_predicate/ut/ya.make b/ydb/library/yql/core/extract_predicate/ut/ya.make
new file mode 100644
index 0000000000..8c76114c59
--- /dev/null
+++ b/ydb/library/yql/core/extract_predicate/ut/ya.make
@@ -0,0 +1,35 @@
+UNITTEST_FOR(ydb/library/yql/core/extract_predicate)
+
+SRCS(
+ extract_predicate_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ ydb/library/yql/core/facade
+ ydb/library/yql/core/services
+ ydb/library/yql/public/udf
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/core/type_ann
+ ydb/library/yql/core/ut_common
+ ydb/library/yql/providers/config
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/result/provider
+ ydb/library/yql/providers/yt/gateway/file
+ ydb/library/yql/providers/yt/provider
+ ydb/library/yql/sql/pg
+)
+
+YQL_LAST_ABI_VERSION()
+
+IF (SANITIZER_TYPE OR WITH_VALGRIND)
+ TIMEOUT(1800)
+ SIZE(LARGE)
+ TAG(ya:fat)
+ELSE()
+ TIMEOUT(600)
+ SIZE(MEDIUM)
+ENDIF()
+
+END()
diff --git a/ydb/library/yql/core/url_preprocessing/pattern_group.cpp b/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
new file mode 100644
index 0000000000..8185a84a7f
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/pattern_group.cpp
@@ -0,0 +1,28 @@
+#include "pattern_group.h"
+
+namespace NYql {
+
+void TPatternGroup::Add(const TString& pattern, const TString& alias) {
+ auto it = CompiledPatterns.find(pattern);
+ if (it != CompiledPatterns.end()) {
+ return;
+ }
+
+ CompiledPatterns.emplace(pattern, std::make_pair(TRegExMatch(pattern), alias));
+}
+
+bool TPatternGroup::IsEmpty() const {
+ return CompiledPatterns.empty();
+}
+
+TMaybe<TString> TPatternGroup::Match(const TString& s) const {
+ for (auto& p : CompiledPatterns) {
+ if (p.second.first.Match(s.c_str())) {
+ return p.second.second;
+ }
+ }
+
+ return Nothing();
+}
+
+}
diff --git a/ydb/library/yql/core/url_preprocessing/pattern_group.h b/ydb/library/yql/core/url_preprocessing/pattern_group.h
new file mode 100644
index 0000000000..3137d4a28c
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/pattern_group.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <library/cpp/regex/pcre/regexp.h>
+#include <util/generic/map.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+
+#include <utility>
+
+namespace NYql {
+
+class TPatternGroup {
+public:
+ TPatternGroup() = default;
+ void Add(const TString& pattern, const TString& alias);
+ bool IsEmpty() const;
+ TMaybe<TString> Match(const TString& s) const;
+
+private:
+ TMap<TString, std::pair<TRegExMatch, TString>> CompiledPatterns;
+};
+
+}
diff --git a/ydb/library/yql/core/url_preprocessing/url_mapper.cpp b/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
new file mode 100644
index 0000000000..0c646ed0d6
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/url_mapper.cpp
@@ -0,0 +1,19 @@
+#include "url_mapper.h"
+
+namespace NYql {
+
+void TUrlMapper::AddMapping(const TString& pattern, const TString& targetUrl) {
+ CustomSchemes.push_back(TCustomScheme(pattern, targetUrl));
+}
+
+bool TUrlMapper::MapUrl(const TString& url, TString& mappedUrl) const {
+ for (const auto& sc : CustomSchemes) {
+ if (sc.Pattern.Match(url.data())) {
+ mappedUrl = TRegExSubst(sc.TargetUrlSubst).Replace(url.data());
+ return true;
+ }
+ }
+ return false;
+}
+
+}
diff --git a/ydb/library/yql/core/url_preprocessing/url_mapper.h b/ydb/library/yql/core/url_preprocessing/url_mapper.h
new file mode 100644
index 0000000000..f6438ecca3
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/url_mapper.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <library/cpp/regex/pcre/regexp.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+class TUrlMapper {
+public:
+ void AddMapping(const TString& pattern, const TString& targetUrl);
+ bool MapUrl(const TString& url, TString& mappedUrl) const;
+
+private:
+ struct TCustomScheme {
+ TCustomScheme(const TString& pattern, const TString& url)
+ : Pattern(pattern)
+ , TargetUrlHolder(url)
+ , TargetUrlSubst(pattern.data()) {
+ if (0 == TargetUrlSubst.ParseReplacement(TargetUrlHolder.data())) {
+ ythrow yexception() << "Bad url replacement: " << TargetUrlHolder;
+ }
+ }
+ TRegExMatch Pattern;
+ TString TargetUrlHolder;
+ TRegExSubst TargetUrlSubst;
+ };
+
+private:
+ TVector<TCustomScheme> CustomSchemes;
+};
+
+}
diff --git a/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp b/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
new file mode 100644
index 0000000000..1be039704f
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/url_preprocessing.cpp
@@ -0,0 +1,56 @@
+#include "url_preprocessing.h"
+
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/yexception.h>
+
+namespace NYql {
+
+void TUrlPreprocessing::Configure(bool restrictedUser, const TGatewaysConfig& cfg) {
+ RestrictedUser_ = restrictedUser;
+
+ try {
+ if (cfg.HasFs()) {
+ const auto fsCfg = cfg.GetFs();
+ for (auto& s: fsCfg.GetCustomSchemes()) {
+ Mapper_.AddMapping(s.GetPattern(), s.GetTargetUrl());
+ }
+ if (restrictedUser) {
+ for (auto& a: fsCfg.GetExternalAllowedUrls()) {
+ AllowedUrls_.Add(a.GetPattern(), a.GetAlias());
+ }
+ } else {
+ for (auto& a: fsCfg.GetAllowedUrls()) {
+ AllowedUrls_.Add(a.GetPattern(), a.GetAlias());
+ }
+ }
+ }
+ } catch (const yexception& e) {
+ ythrow yexception() << "UrlPreprocessing: " << e.what();
+ }
+}
+
+std::pair<TString, TString> TUrlPreprocessing::Preprocess(const TString& url) {
+ TString convertedUrl;
+
+ if (!Mapper_.MapUrl(url, convertedUrl)) {
+ convertedUrl = url;
+ } else {
+ YQL_LOG(INFO) << "Remap url from " << url << " to " << convertedUrl;
+ }
+
+ TString alias;
+ if (RestrictedUser_ || !AllowedUrls_.IsEmpty()) {
+ if (auto a = AllowedUrls_.Match(convertedUrl)) {
+ alias = *a;
+ } else {
+ YQL_LOG(WARN) << "Url " << convertedUrl << " is not in allowed list, reject downloading";
+ ythrow yexception() << "It is not allowed to download url " << url;
+ }
+ }
+ YQL_LOG(INFO) << "UrlPreprocessing: " << convertedUrl << ", alias=" << alias;
+ return {convertedUrl, alias};
+}
+
+} // NYql
diff --git a/ydb/library/yql/core/url_preprocessing/url_preprocessing.h b/ydb/library/yql/core/url_preprocessing/url_preprocessing.h
new file mode 100644
index 0000000000..8877dd93a8
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/url_preprocessing.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <ydb/library/yql/core/url_preprocessing/pattern_group.h>
+#include <ydb/library/yql/core/url_preprocessing/url_mapper.h>
+
+#include <ydb/library/yql/core/yql_user_data.h>
+
+#include <util/generic/string.h>
+#include <util/generic/ptr.h>
+
+#include <vector>
+#include <utility>
+
+namespace NYql {
+
+class TGatewaysConfig;
+
+class TUrlPreprocessing: public IUrlPreprocessing {
+public:
+ using TPtr = TIntrusivePtr<TUrlPreprocessing>;
+
+ TUrlPreprocessing(const TGatewaysConfig& cfg) {
+ Configure(false, cfg);
+ }
+ TUrlPreprocessing() = default;
+ ~TUrlPreprocessing() = default;
+
+ void Configure(bool restrictedUser, const TGatewaysConfig& cfg);
+ std::pair<TString, TString> Preprocess(const TString& url);
+
+private:
+ bool RestrictedUser_ = false;
+ TUrlMapper Mapper_;
+ TPatternGroup AllowedUrls_;
+};
+
+}
diff --git a/ydb/library/yql/core/url_preprocessing/ya.make b/ydb/library/yql/core/url_preprocessing/ya.make
new file mode 100644
index 0000000000..d2a613545d
--- /dev/null
+++ b/ydb/library/yql/core/url_preprocessing/ya.make
@@ -0,0 +1,24 @@
+LIBRARY()
+
+SRCS(
+ url_mapper.cpp
+ pattern_group.cpp
+ url_preprocessing.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/providers/common/proto
+ ydb/library/yql/utils/log
+ library/cpp/regex/pcre
+)
+
+END()
+
+
+# Unittests for url_preprocessing contain what seems to be a sensitive
+# information about Yandex internals, so let's not export them for now.
+IF (NOT EXPORT_CMAKE)
+ RECURSE(
+ ut
+ )
+ENDIF()
diff --git a/ydb/library/yql/core/ut/ya.make b/ydb/library/yql/core/ut/ya.make
new file mode 100644
index 0000000000..9f52136b99
--- /dev/null
+++ b/ydb/library/yql/core/ut/ya.make
@@ -0,0 +1,42 @@
+UNITTEST_FOR(ydb/library/yql/core)
+
+SRCS(
+ yql_csv_ut.cpp
+ yql_execution_ut.cpp
+ yql_expr_constraint_ut.cpp
+ yql_expr_discover_ut.cpp
+ yql_expr_optimize_ut.cpp
+ yql_expr_providers_ut.cpp
+ yql_expr_type_annotation_ut.cpp
+ yql_library_compiler_ut.cpp
+ yql_udf_index_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ ydb/library/yql/core/facade
+ ydb/library/yql/core/services
+ ydb/library/yql/public/udf
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/core/type_ann
+ ydb/library/yql/core/ut_common
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/schema/parser
+ ydb/library/yql/providers/result/provider
+ ydb/library/yql/providers/yt/gateway/file
+ ydb/library/yql/providers/yt/provider
+ ydb/library/yql/sql/pg
+)
+
+YQL_LAST_ABI_VERSION()
+
+IF (SANITIZER_TYPE OR WITH_VALGRIND)
+ TIMEOUT(1800)
+ SIZE(LARGE)
+ TAG(ya:fat)
+ELSE()
+ TIMEOUT(600)
+ SIZE(MEDIUM)
+ENDIF()
+
+END()
diff --git a/ydb/library/yql/core/ut/yql_csv_ut.cpp b/ydb/library/yql/core/ut/yql_csv_ut.cpp
new file mode 100644
index 0000000000..2ae83f9096
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_csv_ut.cpp
@@ -0,0 +1,312 @@
+#include "yql_csv.h"
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/stream/mem.h>
+
+using namespace NYql;
+using namespace NUtils;
+
+TVector<TString> ReadOneLineCsv(const TString& csv)
+{
+ TMemoryInput memIn(csv);
+ TCsvInputStream in(memIn);
+ return in.ReadLine();
+}
+
+TVector<TString> ReadOneLineCsvData(const TStringBuf& csvdata)
+{
+ TCsvInputBuffer in(csvdata);
+ return in.ReadLine();
+}
+
+
+template<typename T>
+TString WriteCsv(T&& functor) {
+ TStringStream ss;
+ TCsvOutputStream out(ss);
+ functor(out);
+ return ss.Str();
+}
+
+Y_UNIT_TEST_SUITE(TYqlUtils) {
+
+ Y_UNIT_TEST(CsvReadTest) {
+ {
+ TVector<TString> columns = ReadOneLineCsv("");
+ UNIT_ASSERT_EQUAL(columns.size(), 0);
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("one");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"one\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("one;two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"one\";\"two\";\"three\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"one\";two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("one;\"two\";three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("one;two;\"three\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv(";two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("one;two;");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv(";");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"\";");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv(";\"\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv("\"ab;cd\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "ab;cd");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsv(
+ "\\\";"
+ "\"\\\"\";"
+ "\\\"\\\";"
+ "\"\\\"\\\"\";"
+ "\\\"\\\"\\\"\\\";"
+ "\"\\\"\\\"\\\"\\\"\";"
+ "\\\\\";"
+ "\"\\\\\"\";"
+ "\\\\\"\\\\\";"
+ "\"\\\\\"\\\\\"\";"
+ "\\\\\"\\\\\"\\\\\"\\\\\";"
+ "\"\\\\\"\\\\\"\\\\\"\\\\\"\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 12);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "\"\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[3], "\"\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[4], "\"\"\"\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[5], "\"\"\"\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[6], "\\\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[7], "\\\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[8], "\\\"\\\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[9], "\\\"\\\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[10], "\\\"\\\"\\\"\\\"");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[11], "\\\"\\\"\\\"\\\"");
+ }
+ }
+
+ Y_UNIT_TEST(CsvReadFromBufferTest) {
+ {
+ TVector<TString> columns = ReadOneLineCsvData("");
+ UNIT_ASSERT_EQUAL(columns.size(), 0);
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("one");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"one\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("one;two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"one\";\"two\";\"three\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"one\";two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("one;\"two\";three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("one;two;\"three\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData(";two;three");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "three");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("one;two;");
+ UNIT_ASSERT_EQUAL(columns.size(), 3);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "one");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "two");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[2], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData(";");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"\";");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData(";\"\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 2);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "");
+ UNIT_ASSERT_STRINGS_EQUAL(columns[1], "");
+ }
+
+ {
+ TVector<TString> columns = ReadOneLineCsvData("\"ab;cd\"");
+ UNIT_ASSERT_EQUAL(columns.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(columns[0], "ab;cd");
+ }
+ }
+
+ Y_UNIT_TEST(CsvWriteTest) {
+ {
+ TString str = WriteCsv([](TCsvOutputStream& out) {
+ out << "";
+ });
+ UNIT_ASSERT_STRINGS_EQUAL(str, "\"\"");
+ }
+
+ {
+ TString str = WriteCsv([](TCsvOutputStream& out) {
+ out << "one";
+ });
+ UNIT_ASSERT_STRINGS_EQUAL(str, "\"one\"");
+ }
+
+ {
+ TString str = WriteCsv([](TCsvOutputStream& out) {
+ out << "one" << "two";
+ });
+ UNIT_ASSERT_STRINGS_EQUAL(str, "\"one\";\"two\"");
+ }
+
+ {
+ TString str = WriteCsv([](TCsvOutputStream& out) {
+ out << "one" << "two" << Endl
+ << 1 << 2;
+ });
+ UNIT_ASSERT_STRINGS_EQUAL(str, "\"one\";\"two\"\n"
+ "\"1\";\"2\"");
+ }
+ }
+}
+
diff --git a/ydb/library/yql/core/ut/yql_execution_ut.cpp b/ydb/library/yql/core/ut/yql_execution_ut.cpp
new file mode 100644
index 0000000000..3c405952b0
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_execution_ut.cpp
@@ -0,0 +1,792 @@
+#include "yql_execution.h"
+
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_proposed_by_data.h>
+#include <ydb/library/yql/core/yql_opt_rewrite_io.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/schema/parser/yql_type_parser.h>
+#include <ydb/library/yql/providers/result/provider/yql_result_provider.h>
+
+#include <ydb/library/yql/core/facade/yql_facade.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/system/user.h>
+#include <util/system/tempfile.h>
+#include <util/system/defaults.h>
+#include <util/system/fstat.h>
+#include <util/folder/path.h>
+#include <util/folder/tempdir.h>
+#include <util/string/cast.h>
+#include <util/string/builder.h>
+#include <util/system/sanitizers.h>
+
+namespace NYql {
+
+ static TString BuildFileNameForTmpTable(TStringBuf table, TStringBuf tmpDir) {
+ return TStringBuilder() << tmpDir << LOCSLASH_C << table.substr(4) << ".tmp";
+ }
+
+ struct TRunSingleProgram {
+ TString Src;
+ TString TmpDir;
+ TString Parameters;
+ IOutputStream& Err;
+ TVector<TString> Res;
+ THashMap<TString, TString> Tables;
+
+ TRunSingleProgram(const TString& src, IOutputStream& err)
+ : Src(src)
+ , Err(err)
+ {
+ }
+
+ bool Run(
+ const NKikimr::NMiniKQL::IFunctionRegistry* funcReg
+ ) {
+ auto yqlNativeServices = NFile::TYtFileServices::Make(funcReg, Tables, {}, TmpDir);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+
+ TVector<TDataProviderInitializer> dataProvidersInit;
+ dataProvidersInit.push_back(GetYtNativeDataProviderInitializer(ytGateway));
+ TProgramFactory factory(true, funcReg, 0ULL, dataProvidersInit, "ut");
+
+ TProgramPtr program = factory.Create("-stdin-", Src);
+ program->ConfigureYsonResultFormat(NYson::EYsonFormat::Text);
+ if (!Parameters.empty()) {
+ program->SetParametersYson(Parameters);
+ }
+
+ if (!program->ParseYql() || !program->Compile(GetUsername())) {
+ program->PrintErrorsTo(Err);
+ return false;
+ }
+
+ TProgram::TStatus status = program->Run(GetUsername());
+ if (status == TProgram::TStatus::Error) {
+ program->PrintErrorsTo(Err);
+ }
+ Res = program->Results();
+ return status == TProgram::TStatus::Ok;
+ }
+
+ void AddResults(TVector<TString>& res) const {
+ res.insert(res.end(), Res.begin(), Res.end());
+ }
+
+ bool Finished() const {
+ return true;
+ }
+ };
+
+ struct TRunMultiplePrograms: public TRunSingleProgram {
+ TVector<TString> Srcs;
+ size_t Curr;
+
+ TRunMultiplePrograms(const TVector<TString>& srcs, IOutputStream& err)
+ : TRunSingleProgram(TString(), err)
+ , Srcs(srcs)
+ , Curr(0)
+ {
+ }
+
+ bool Run(
+ const NKikimr::NMiniKQL::IFunctionRegistry* funcReg
+ ) {
+ TString origTmpDir = TmpDir;
+ if (TmpDir) {
+ TFsPath newTmp = TFsPath(TmpDir) / ToString(Curr);
+ newTmp.MkDirs();
+ TmpDir = newTmp.GetPath();
+ }
+ Src = Srcs[Curr];
+ if (!TRunSingleProgram::Run(funcReg)) {
+ return false;
+ }
+ ui32 idx = 0;
+ for (auto& resStr: Res) {
+ NYT::TNode res;
+ if (!NCommon::ParseYson(res, resStr, Err)) {
+ return false;
+ }
+ if (!res.IsMap() || !res.HasKey("Write") || !res["Write"].IsList()) {
+ Err << "Invalid result: " << resStr << Endl;
+ return false;
+ }
+ for (auto& elem: res["Write"].AsList()) {
+ if (!elem.IsMap()) {
+ Err << "Invalid result element in result: " << resStr << Endl;
+ return false;
+ }
+ if (elem.HasKey("Ref")) {
+ if (!elem["Ref"].IsList()) {
+ Err << "Invalid reference in result: " << resStr << Endl;
+ return false;
+ }
+ for (auto& refElem: elem["Ref"].AsList()) {
+ if (!refElem.IsMap() || !refElem.HasKey("Reference")) {
+ Err << "Invalid reference in result: " << resStr << Endl;
+ return false;
+ }
+ if (!refElem["Remove"].AsBool()) {
+ continue;
+ }
+ const auto& ref = refElem["Reference"].AsList();
+ TStringStream name;
+ name << ref[0].AsString() << "." << ref[1].AsString() << ".Result" << Curr << "_" << idx;
+ Tables[name.Str()] = BuildFileNameForTmpTable(ref[2].AsString(), TmpDir);
+ ++idx;
+ }
+ }
+ }
+ }
+ ++Curr;
+ origTmpDir.swap(TmpDir);
+ return true;
+ }
+
+ bool Finished() const {
+ return Curr == Srcs.size();
+ }
+ };
+
+ template <typename TDriver>
+ TVector<TString> Run(TDriver& driver) {
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+
+ TVector<TString> res;
+ do {
+ const bool runRes = driver.Run(functionRegistry.Get());
+ UNIT_ASSERT(runRes);
+
+ driver.AddResults(res);
+ } while (!driver.Finished());
+ return res;
+ }
+
+ TVector<TString> RunProgram(const TString& programSrc, const THashMap<TString, TString>& tables, const TString& tmpDir = TString(), const TString& params = TString()) {
+ TRunSingleProgram driver(programSrc, Cerr);
+ driver.Tables = tables;
+ driver.TmpDir = tmpDir;
+ driver.Parameters = params;
+ return Run(driver);
+ }
+
+ static const TStringBuf KSV_ATTRS =
+ "{\"_yql_row_spec\" = {\"Type\" = [\"StructType\";["
+ "[\"key\";[\"DataType\";\"String\"]];"
+ "[\"subkey\";[\"DataType\";\"String\"]];"
+ "[\"value\";[\"DataType\";\"String\"]]"
+ "]]}}"
+ ;
+
+ Y_UNIT_TEST_SUITE(ExecutionYqlExpr) {
+ Y_UNIT_TEST(WriteToResultUsingIsolatedGraph) {
+ auto s = "(\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (AsList (String 'x)))\n"
+ "(let world (Write! world res_sink (Key) data '()))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ auto res = RunProgram(s, THashMap<TString, TString>());
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ UNIT_ASSERT_NO_DIFF("{\"Write\"=[{\"Data\"=[\"x\"]}]}", res[0]);
+ }
+
+ Y_UNIT_TEST(WriteToResultTableOutput) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ THashMap<TString, TString> tables;
+ tables["yt.plato.Input"] = inputFile.Name();
+
+ auto s = "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (AsList (String 'x)))\n"
+ "(let world (Write! world res_sink (Key) table1 '()))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ auto res = RunProgram(s, tables);
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Data\"=["
+ "[\"075\";\".\";\"abc\"];"
+ "[\"800\";\".\";\"ddd\"];"
+ "[\"020\";\".\";\"q\"];"
+ "[\"150\";\".\";\"qzz\"]"
+ "]}]}",
+ res[0]
+ );
+ }
+
+ Y_UNIT_TEST(WriteToResultTransformedTable) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ THashMap<TString, TString> tables;
+ tables["yt.plato.Input"] = inputFile.Name();
+
+ auto s = "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let table1low (FlatMap table1 (lambda '(item) (block '(\n"
+ " (let intValueOpt (FromString (Member item 'key) 'Int32))\n"
+ " (let ret (FlatMap intValueOpt (lambda '(item2) (block '(\n"
+ " (return (ListIf (< item2 (Int32 '100)) item))\n"
+ " )))))"
+ " (return ret)"
+ ")))))"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (AsList (String 'x)))\n"
+ "(let world (Write! world res_sink (Key) table1low '()))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ auto res = RunProgram(s, tables);
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Data\"=["
+ "[\"075\";\".\";\"abc\"];"
+ "[\"020\";\".\";\"q\"]"
+ "]}]}",
+ res[0]
+ );
+ }
+
+ Y_UNIT_TEST(DropTable) {
+ TTempFileHandle outputFile;
+ TTempFileHandle outputFileAttrs(outputFile.Name() + ".attr");
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"sv
+ ;
+
+ outputFile.Write(data.data(), data.size());
+ outputFile.FlushData();
+ outputFile.Close();
+ outputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ outputFileAttrs.FlushData();
+ outputFileAttrs.Close();
+ UNIT_ASSERT(TFileStat(outputFile.Name()).IsFile());
+
+ THashMap<TString, TString> tables;
+ tables["yt.plato.Output"] = outputFile.Name();
+
+ auto s = "(\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) (Void) '('('mode 'drop))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ RunProgram(s, tables);
+
+ UNIT_ASSERT(!TFileStat(outputFile.Name()).IsFile());
+ }
+
+ Y_UNIT_TEST(WriteToResultTableByRef) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ THashMap<TString, TString> tables;
+ tables["yt.plato.Input"] = inputFile.Name();
+
+ auto s = "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) table1 '('('ref))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ auto res = RunProgram(s, tables);
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Ref\"=["
+ "{\"Reference\"=[\"yt\";\"plato\";\"Input\"];\"Columns\"=[\"key\";\"subkey\";\"value\"];\"Remove\"=%false}"
+ "]}]}",
+ res[0]
+ );
+ }
+
+ Y_UNIT_TEST(WriteToResultTransformedTableByRef) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+ TTempDir tmpDir;
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ TVector<TString> progs;
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let table1low (FlatMap table1 (lambda '(item) (block '(\n"
+ " (let intValueOpt (FromString (Member item 'key) 'Int32))\n"
+ " (let ret (FlatMap intValueOpt (lambda '(item2) (block '(\n"
+ " (return (ListIf (< item2 (Int32 '100)) item))\n"
+ " )))))"
+ " (return ret)"
+ ")))))"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) table1low '('('ref))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Result0_0))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (AsList (String 'x)))\n"
+ "(let world (Write! world res_sink (Key) table1 '()))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ TRunMultiplePrograms driver(progs, Cerr);
+ driver.Tables["yt.plato.Input"] = inputFile.Name();
+ driver.TmpDir = tmpDir.Name();
+
+ auto res = Run(driver);
+
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 2);
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Ref\"=["
+ "{\"Reference\"=[\"yt\";\"plato\";\"tmp/bb686f68-2245bd5f-2318fa4e-1\"];\"Columns\"=[\"key\";\"subkey\";\"value\"];\"Remove\"=%true}"
+ "]}]}",
+ res[0]
+ );
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Data\"=["
+ "[\"075\";\".\";\"abc\"];"
+ "[\"020\";\".\";\"q\"]"
+ "]}]}",
+ res[1]
+ );
+ }
+
+ Y_UNIT_TEST(WriteAndTakeResult) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+ TTempDir tmpDir;
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ TVector<TString> progs;
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table (Right! x))\n"
+ "(let result (Map table (lambda '(item) (block '("
+ " (let res (Struct))"
+ " (let res (AddMember res 'k (Member item 'key)))"
+ " (let res (AddMember res 's (Member item 'subkey)))"
+ " (let res (AddMember res 'v (Member item 'value)))"
+ " (return res)"
+ ")))))"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) result '('('ref))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Result0_0))) '('k 's 'v) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table (Right! x))\n"
+ "(let result (Take table (Uint64 '2)))"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) result '('('type))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ TRunMultiplePrograms driver(progs, Cerr);
+ driver.Tables["yt.plato.Input"] = inputFile.Name();
+ driver.TmpDir = tmpDir.Name();
+
+ auto res = Run(driver);
+
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 2);
+
+ //~ Cerr << res[0] << Endl;
+ //~ Cerr << res[1] << Endl;
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{\"Ref\"=["
+ "{\"Reference\"=[\"yt\";\"plato\";\"tmp/bb686f68-2245bd5f-2318fa4e-1\"];\"Columns\"=[\"k\";\"s\";\"v\"];\"Remove\"=%true}"
+ "]}]}",
+ res[0]
+ );
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{"
+ "\"Type\"=[\"ListType\";[\"StructType\";["
+ "[\"k\";[\"DataType\";\"String\"]];[\"s\";[\"DataType\";\"String\"]];[\"v\";[\"DataType\";\"String\"]]"
+ "]]];"
+ "\"Data\"=["
+ "[\"075\";\".\";\"abc\"];"
+ "[\"800\";\".\";\"ddd\"]"
+ "]}"
+ "]}",
+ res[1]
+ );
+ }
+
+ Y_UNIT_TEST(WriteAndReadScheme) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+ TTempDir tmpDir;
+
+ TStringBuf data =
+ "{\"key\"=\"075\";\"subkey\"=\".\";\"value\"=\"abc\"};\n"
+ "{\"key\"=\"800\";\"subkey\"=\".\";\"value\"=\"ddd\"};\n"
+ "{\"key\"=\"020\";\"subkey\"=\".\";\"value\"=\"q\"};\n"
+ "{\"key\"=\"150\";\"subkey\"=\".\";\"value\"=\"qzz\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ TVector<TString> progs;
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table (Right! x))\n"
+ "(let result0 (Map table (lambda '(item) (block '("
+ " (return (AsStruct '('bar (Coalesce (FromString (Member item 'key) 'Uint64) (Uint64 '0)))))"
+ ")))))"
+ "(let result1 (Map result0 (lambda '(item) (block '("
+ " (return (AddMember (Struct) 'foo item))"
+ ")))))"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) result0 '('('ref))))\n"
+ "(let world (Write! world res_sink (Key) result1 '('('ref))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('tablescheme (String 'Result0_0))) (Void) '()))\n"
+ "(let world (Left! x))\n"
+ "(let scheme (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) scheme '('('type))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('tablescheme (String 'Result0_1))) (Void) '()))\n"
+ "(let world (Left! x))\n"
+ "(let scheme (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) scheme '('('type))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ TRunMultiplePrograms driver(progs, Cerr);
+ driver.Tables["yt.plato.Input"] = inputFile.Name();
+ driver.TmpDir = tmpDir.Name();
+
+ auto res = Run(driver);
+
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 3);
+
+ //~ Cerr << res[0] << Endl;
+ //~ Cerr << res[1] << Endl;
+ //~ Cerr << res[2] << Endl;
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=["
+ "{\"Ref\"=[{\"Reference\"=[\"yt\";\"plato\";\"tmp/bb686f68-2245bd5f-2318fa4e-1\"];\"Columns\"=[\"bar\"];\"Remove\"=%true}]};"
+ "{\"Ref\"=[{\"Reference\"=[\"yt\";\"plato\";\"tmp/7ae6459a-7382d1e7-7935c08e-2\"];\"Columns\"=[\"foo\"];\"Remove\"=%true}]}"
+ "]}",
+ res[0]
+ );
+ UNIT_ASSERT(res[1].find("\"Fields\"=[{\"Name\"=\"bar\"") != TString::npos);
+ UNIT_ASSERT(res[2].find("\"Fields\"=[{\"Name\"=\"foo\"") != TString::npos);
+ }
+
+ Y_UNIT_TEST(ExtendSortedWithNonSortedAndRead) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+ TTempFileHandle outputFile;
+ TTempFile outputFileAttr(outputFile.Name() + ".attr");
+ TTempDir tmpDir;
+
+ TStringBuf data =
+ "{\"key\"=\"foo\";\"subkey\"=\"wat\";\"value\"=\"222\"};\n"
+ "{\"key\"=\"bar\";\"subkey\"=\"wat\";\"value\"=\"111\"};\n"
+ "{\"key\"=\"jar\";\"subkey\"=\"wat\";\"value\"=\"333\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ TVector<TString> progs;
+ progs.push_back(
+ "(\n"
+ "(let source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world source (Key '('table (String 'Input))) '('key 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table (Right! x))\n"
+ "(let sorted (Sort table (Bool 'true) (lambda '(item) (Member item 'value))))\n"
+ "(let result (Extend table sorted))\n"
+ "(let sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world sink (Key '('table (String 'Output))) result '()))\n"
+ "(let world (Commit! world sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Output))) '('key 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let result (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) result '('('type))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ TRunMultiplePrograms driver(progs, Cerr);
+ driver.TmpDir = tmpDir.Name();
+ driver.Tables["yt.plato.Input"] = inputFile.Name();
+ driver.Tables["yt.plato.Output"] = outputFile.Name();
+
+ auto res = Run(driver);
+
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+
+ //~ Cerr << res[0] << Endl;
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{"
+ "\"Type\"=[\"ListType\";[\"StructType\";[[\"key\";[\"DataType\";\"String\"]];[\"value\";[\"DataType\";\"String\"]]]]];"
+ "\"Data\"=["
+ "[\"foo\";\"222\"];[\"bar\";\"111\"];[\"jar\";\"333\"];[\"foo\";\"222\"];[\"bar\";\"111\"];[\"jar\";\"333\"]"
+ "]}]}",
+ res[0]
+ );
+ }
+
+ Y_UNIT_TEST(OrderedExtendSortedWithNonSortedAndRead) {
+ TTempFileHandle inputFile;
+ TTempFileHandle inputFileAttrs(inputFile.Name() + ".attr");
+ TTempFileHandle outputFile;
+ TTempFile outputFileAttr(outputFile.Name() + ".attr");
+ TTempDir tmpDir;
+
+ TStringBuf data =
+ "{\"key\"=\"foo\";\"subkey\"=\"wat\";\"value\"=\"222\"};\n"
+ "{\"key\"=\"bar\";\"subkey\"=\"wat\";\"value\"=\"111\"};\n"
+ "{\"key\"=\"jar\";\"subkey\"=\"wat\";\"value\"=\"333\"};\n"sv
+ ;
+
+ inputFile.Write(data.data(), data.size());
+ inputFile.FlushData();
+ inputFileAttrs.Write(KSV_ATTRS.data(), KSV_ATTRS.size());
+ inputFileAttrs.FlushData();
+
+ TVector<TString> progs;
+ progs.push_back(
+ "(\n"
+ "(let source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world source (Key '('table (String 'Input))) '('key 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table (Right! x))\n"
+ "(let sorted (Sort table (Bool 'true) (lambda '(item) (Member item 'value))))\n"
+ "(let result (OrderedExtend table sorted))\n"
+ "(let sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world sink (Key '('table (String 'Output))) result '()))\n"
+ "(let world (Commit! world sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ progs.push_back(
+ "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Output))) '('key 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let result (Right! x))\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let mr_sink (DataSink 'yt 'plato))\n"
+ "(let world (Write! world res_sink (Key) result '('('type))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n"
+ );
+
+ TRunMultiplePrograms driver(progs, Cerr);
+ driver.TmpDir = tmpDir.Name();
+ driver.Tables["yt.plato.Input"] = inputFile.Name();
+ driver.Tables["yt.plato.Output"] = outputFile.Name();
+
+ auto res = Run(driver);
+
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+
+ //~ Cerr << res[0] << Endl;
+ UNIT_ASSERT_NO_DIFF(
+ "{\"Write\"=[{"
+ "\"Type\"=[\"ListType\";[\"StructType\";[[\"key\";[\"DataType\";\"String\"]];[\"value\";[\"DataType\";\"String\"]]]]];"
+ "\"Data\"=["
+ "[\"foo\";\"222\"];[\"bar\";\"111\"];[\"jar\";\"333\"];[\"bar\";\"111\"];[\"foo\";\"222\"];[\"jar\";\"333\"]"
+ "]}]}",
+ res[0]
+ );
+ }
+
+ Y_UNIT_TEST(TestParametersEvaluation) {
+ auto s = "(\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let data (Parameter '\"$foo\" (ParseType '\"Tuple<String, Int32 ? , List<Uint32>, Dict<Int32, Bool>, Struct<a : Void, b : Double>, Variant<Int32, Bool>>\")))\n"
+ "(let world (Write! world res_sink (Key) data '('('type))))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ auto params = R"__(
+{"$foo"={Data=[
+ bar;
+ "33";
+ ["1";"2";"3"];
+ [["7";%true];["12";%false]];
+ [#;"-1.7"];
+ ["0";"8"];
+]}}
+ )__";
+
+ auto res = RunProgram(s, THashMap<TString, TString>(), "", params);
+ UNIT_ASSERT_VALUES_EQUAL(res.size(), 1);
+ UNIT_ASSERT_NO_DIFF(R"__({"Write"=[{"Type"=["TupleType";[["DataType";"String"];["OptionalType";["DataType";"Int32"]];["ListType";["DataType";"Uint32"]];["DictType";["DataType";"Int32"];["DataType";"Bool"]];["StructType";[["a";["VoidType"]];["b";["DataType";"Double"]]]];["VariantType";["TupleType";[["DataType";"Int32"];["DataType";"Bool"]]]]]];"Data"=["bar";["33"];["1";"2";"3"];[["7";%true];["12";%false]];["Void";"-1.7"];["0";"8"]]}]})__", res[0]);
+ }
+ }
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_expr_constraint_ut.cpp b/ydb/library/yql/core/ut/yql_expr_constraint_ut.cpp
new file mode 100644
index 0000000000..e629d7e555
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_expr_constraint_ut.cpp
@@ -0,0 +1,3217 @@
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/services/yql_transform_pipeline.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TYqlExprConstraints) {
+
+ static TExprNode::TPtr ParseAndAnnotate(const TStringBuf program, TExprContext& exprCtx) {
+ TAstParseResult astRes = ParseAst(program);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ typeAnnotationContext->RandomProvider = CreateDeterministicRandomProvider(1);
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typeAnnotationContext.Get();
+
+ InitializeYtGateway(ytGateway, ytState);
+ typeAnnotationContext->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ typeAnnotationContext->AddDataSource(YtProviderName, CreateYtDataSource(ytState));
+
+ auto transformer = TTransformationPipeline(typeAnnotationContext)
+ .AddServiceTransformers()
+ .AddPreTypeAnnotation()
+ .AddExpressionEvaluation(*functionRegistry)
+ .AddIOAnnotation()
+ .AddTypeAnnotation()
+ .AddPostTypeAnnotation()
+ .Build();
+
+ const auto status = SyncTransform(*transformer, exprRoot, exprCtx);
+ if (status == IGraphTransformer::TStatus::Error)
+ Cerr << exprCtx.IssueManager.GetIssues().ToString() << Endl;
+ UNIT_ASSERT(status == IGraphTransformer::TStatus::Ok);
+ return exprRoot;
+ }
+
+ template <class TConstraint>
+ static void CheckConstraint(const TExprNode::TPtr& exprRoot, const TStringBuf nodeName, const TStringBuf constrStr) {
+ TExprNode* nodeToCheck = nullptr;
+ VisitExpr(exprRoot, [nodeName, &nodeToCheck] (const TExprNode::TPtr& node) {
+ if (node->IsCallable(nodeName)) {
+ nodeToCheck = node.Get();
+ }
+ return !nodeToCheck;
+ });
+ UNIT_ASSERT(nodeToCheck);
+ UNIT_ASSERT(nodeToCheck->GetState() == TExprNode::EState::ConstrComplete);
+ const auto constr = nodeToCheck->GetConstraint<TConstraint>();
+ if (constrStr.empty()) {
+ UNIT_ASSERT(!constr);
+ } else {
+ UNIT_ASSERT(constr);
+ UNIT_ASSERT(constr->IsApplicableToType(*nodeToCheck->GetTypeAnn()));
+ UNIT_ASSERT_VALUES_EQUAL(ToString(*constr), constrStr);
+ }
+ }
+
+ Y_UNIT_TEST(Sort) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list (Bool 'True) (lambda '(item) (Member item 'key))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(SortByTranspentIfPresent) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (Just (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v))))
+ (Just (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v))))
+ (Just (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v))))
+ ))
+ (let sorted (Sort list (Bool 'True) (lambda '(row) (FlatMap row (lambda '(item) (Just (Member item 'key)))))))
+ (let map (OrderedFlatMap sorted (lambda '(item) item)))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) map '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFlatMap", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(SortByDuplicateColumn) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'False) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'key) (Member item 'subkey)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(key[asc];subkey[asc])");
+ }
+
+ Y_UNIT_TEST(SortByFullRow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList (Utf8 's) (Utf8 'o) (Utf8 'r) (Utf8 't)))
+ (let sorted (Sort list (Bool 'True) (lambda '(item) item)))
+ (let map (OrderedMap sorted (lambda '(item) (AsStruct '('key item)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) map '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(/[asc])");
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedMap", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(SortByTuple) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list (Bool 'False) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(key[desc];subkey[desc])");
+ }
+
+ Y_UNIT_TEST(SortByTupleElements) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value '((String 'x) (String 'a) (String 'u))))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value '((String 'y) (String 'b) (String 'v))))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value '((String 'z) (String 'c) (String 'w))))
+ ))
+ (let sorted (Sort list '((Not (Bool 'False)) (Bool 'False) (Not (Bool 'True))) (lambda '(item) '((Nth (Member item 'value) '2) (Member item 'key) (Nth (Member item 'value) '0)))))
+ (let map (OrderedMap sorted (lambda '(item) (AsStruct '('tuple '((Nth (Member item 'value) '1) (Nth (Member item 'value) '2) (Nth (Member item 'value) '0) (Member item 'subkey) (Member item 'key)))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) map '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(value/2[asc];key[desc];value/0[desc])");
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedMap", "Sorted(tuple/1[asc];tuple/4[desc];tuple/2[desc])");
+ }
+
+ Y_UNIT_TEST(SortByColumnAndExpr) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'False) (Bool 'True)) (lambda '(item) '((Member item 'key) (SafeCast (Member item 'value) (DataType 'Utf8)) (Member item 'subkey)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(SortDesc) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'False)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Sort", "Sorted(key[asc];subkey[desc])");
+ }
+
+ Y_UNIT_TEST(SortedOverWideMap) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (ToFlow (Sort list '((Bool 'True) (Bool 'False)) (lambda '(item) '((Member item 'key) (Member item 'subkey))))))
+ (let expand (ExpandMap sorted (lambda '(item) (Member item 'key) (Member item 'subkey) (Member item 'value))))
+ (let wide (WideMap expand (lambda '(a b c) c b a)))
+ (let narrow (NarrowMap wide (lambda '(x y z) (AsStruct '('x x) '('y y) '('z z)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Collect narrow) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Collect", "Sorted(z[asc];y[desc])");
+ }
+
+ Y_UNIT_TEST(SortedOverWideTopSort) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let flow (ToFlow list))
+ (let expand (ExpandMap flow (lambda '(item) (Member item 'key) (Member item 'subkey) (Member item 'value))))
+ (let wide (WideTopSort expand (Uint64 '2) '('('2 (Bool 'False)) '('0 (Bool 'True)))))
+ (let narrow (NarrowMap wide (lambda '(x y z) (AsStruct '('x x) '('y y) '('z z)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Collect narrow) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Collect", "Sorted(z[desc];x[asc])");
+ }
+
+ Y_UNIT_TEST(TopSort) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (TopSort list (Uint64 '2) '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "TopSort", "Sorted(key[asc];subkey[asc])");
+ }
+
+ Y_UNIT_TEST(MergeWithFirstEmpty) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list (Bool 'True) (lambda '(item) (Member item 'key))))
+ (let empty (List (ListType (StructType '('key (DataType 'String)) '('subkey (DataType 'String)) '('value (DataType 'String))))))
+ (let merged (Merge empty sorted))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) merged '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Merge", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(MergeWithCloneColumns) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let sorted (Sort list (Bool 'False) (lambda '(item) (Member item 'key))))
+ (let clone (OrderedMap sorted (lambda '(row) (AsStruct '('key (Member row 'key)) '('subkey (Member row 'key)) '('value (Member row 'key))))))
+ (let merged (Merge clone sorted))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) merged '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Merge", "Sorted(key[desc])");
+ }
+
+ Y_UNIT_TEST(UnionMergeWithDiffTypes) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list1 (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list2 (AsList
+ (AsStruct '('key (String '4)) '('subkey (Utf8 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (Utf8 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (Utf8 'b)) '('value (String 'v)))
+ ))
+ (let sorted1 (Sort list1 (Bool 'False) (lambda '(item) '((Member item 'key) (Member item 'subkey) (Member item 'value)))))
+ (let sorted2 (Sort list2 (Bool 'False) (lambda '(item) '((Member item 'key) (Member item 'subkey) (Member item 'value)))))
+ (let merged (UnionMerge sorted1 sorted2))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) merged '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "UnionMerge", "Sorted(key[desc])");
+ }
+
+ Y_UNIT_TEST(ExtractMembersKey) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (ExtractMembers sorted '('key)))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "ExtractMembers", "Sorted(key[asc])");
+ }
+
+ Y_UNIT_TEST(ExtractMembersNonKey) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (ExtractMembers sorted '('value)))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "ExtractMembers", "");
+ }
+
+ Y_UNIT_TEST(OrderedLMap) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let sorted (Sort list '((Bool 'False) (Bool 'False)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (OrderedLMap sorted (lambda '(stream) (OrderedFlatMap stream (lambda '(item) (Just item))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedLMap", "Sorted(key[desc];subkey[desc])");
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFlatMap", "Sorted(key[desc];subkey[desc])");
+ }
+
+ Y_UNIT_TEST(OrderedFlatMap) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (OrderedFlatMap sorted (lambda '(item) (OptionalIf (== (Member item 'key) (String '1)) (AsStruct '('k (Member item 'key)) '('v (Member item 'value)))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFlatMap", "Sorted(k[asc])");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "OrderedFlatMap", "Unique((k))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "OrderedFlatMap", "Distinct((v))");
+ }
+
+ Y_UNIT_TEST(OrderedFlatMapWithEmptyFilter) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (OrderedFlatMap sorted (lambda '(item) (OptionalIf (Bool 'false) (AsStruct '('k (Member item 'key)) '('v (Member item 'value)))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFlatMap", "Sorted(k[asc])");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "OrderedFlatMap", "Unique((k))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "OrderedFlatMap", "Distinct((v))");
+ }
+
+ Y_UNIT_TEST(OrderedFlatMapNonKey) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (OrderedFlatMap sorted (lambda '(item) (OptionalIf (== (Member item 'key) (String '1)) (AsStruct '('key (Member item 'value)))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFlatMap", "");
+ }
+
+ Y_UNIT_TEST(OrderedFilter) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let sorted (OrderedFilter sorted (lambda '(item) (> (Member item 'key) (String '0)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) sorted '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedFilter", "Sorted(key[asc];subkey[asc])");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "OrderedFilter", "Unique((key)(subkey,value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "OrderedFilter", "Distinct((key,subkey)(value))");
+ }
+
+ Y_UNIT_TEST(OrderedMapNullifyOneColumn) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let sorted (Sort list (Bool 'False) (lambda '(row) '((Member row 'key) (Member row 'subkey) (Member row 'value)))))
+ (let map (OrderedMap sorted (lambda '(row) (AsStruct '('k (Member row 'key)) '('s (OptionalIf (AggrNotEquals (Member row 'key) (Member row 'value)) (Member row 'subkey))) '('v (Member row 'value))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) map '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "OrderedMap", "Sorted(k[desc])");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "OrderedMap", "Unique((k)(s,v))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "OrderedMap", "Distinct((v))");
+ }
+
+ Y_UNIT_TEST(FlattenMembers) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let sorted (Sort list '((Bool 'True) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let mapped (OrderedMap sorted (lambda '(item) '((AsStruct '('key (Member item 'key)) '('subkey (Member item 'subkey))) (AsStruct '('subkey (Member item 'subkey)) '('value (Member item 'value)))))))
+ (let flatten (OrderedMap mapped (lambda '(pair) (FlattenMembers '('one (Nth pair '0)) '('two (Nth pair '1))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (LazyList flatten) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+ ))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "LazyList", "Sorted(onekey[asc];onesubkey,twosubkey[asc])");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((onekey)(onesubkey,twovalue)(twosubkey,twovalue))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((onekey,onesubkey)(onekey,twosubkey)(twovalue))");
+ }
+
+ Y_UNIT_TEST(Passthrough) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'w)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'u)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ (AsStruct
+ '('key1 (Member m 'key))
+ '('key2 (Member m 'key))
+ '('subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Member m 'key1))
+ '('p.key2 (Member m 'key2))
+ '('p.subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap res) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "DivePrefixMembers", "Passthrough(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Unwrap", "Passthrough(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+
+ }
+
+ Y_UNIT_TEST(PassthroughOverTuple) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ '(
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Nth m '0))
+ '('p.key2 (Nth m '1))
+ '('p.subkey (Nth m '2))
+ '('value (Nth m '3))
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap res) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "DivePrefixMembers", "Passthrough(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Unwrap", "Passthrough(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ }
+
+ Y_UNIT_TEST(PassthroughOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) m0 m2 m3 m1)))
+ (let res (NarrowMap res (lambda '(m0 m1 m2 m3)
+ (AsStruct
+ '('p.key1 m0)
+ '('p.key2 m3)
+ '('p.subkey m1)
+ '('value m2)
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap (Head (Collect res))) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "DivePrefixMembers", "Passthrough(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Collect", "Passthrough(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ }
+
+ Y_UNIT_TEST(PassthroughExOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) (AsStruct '('xxx m0) '('yyy m2)) '(m1 m3))))
+ (let res (WideMap res (lambda '(s0 t1) '((Member s0 'xxx) (Nth t1 '1)) (ReplaceMember s0 'xxx (Nth t1 '0)))))
+ (let res (NarrowMap res (lambda '(t0 s1) (AddMember (AddMember s1 'one (Nth t0 '1)) 'two (Nth t0 '0)))))
+ (return (Just (Unwrap (Head (Collect res)))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Unwrap", "Passthrough(one:value,two:key,xxx:key,yyy:subkey)");
+ }
+
+ Y_UNIT_TEST(PassthroughCondense1) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Condense1 (ToFlow (Just item))
+ (lambda '(m)
+ (AsStruct
+ '('p.key1 (Member m 'key))
+ '('p.key2 (Member m 'key))
+ '('p.subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )
+ (lambda '(i s) (Bool 'false))
+ (lambda '(i s)
+ (AsStruct
+ '('p.key1 (Member i 'key))
+ '('p.key2 (Member s 'p.key2))
+ '('p.subkey (Member i 'subkey))
+ '('value (Member s 'value))
+ )
+ )
+ ))
+ (return (Just (DivePrefixMembers (Unwrap (Last (Collect res))) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "DivePrefixMembers", "Passthrough(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Collect", "Passthrough(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ }
+
+ Y_UNIT_TEST(PassthroughWideCondense1) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideCondense1 res
+ (lambda '(m0 m1 m2 m3) m0 m1 m2 m3)
+ (lambda '(i0 i1 i2 i3 s0 s1 s2 s3) (Bool 'false))
+ (lambda '(i0 i1 i2 i3 s0 s1 s2 s3) i0 s1 i2 s3)
+ ))
+ (let res (NarrowMap res (lambda '(m0 m1 m2 m3)
+ (AsStruct
+ '('p.key1 m0)
+ '('p.key2 m1)
+ '('p.subkey m2)
+ '('value m3)
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap (Head (Collect res))) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "DivePrefixMembers", "Passthrough(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TPassthroughConstraintNode>(exprRoot, "Collect", "Passthrough(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ }
+
+ Y_UNIT_TEST(Visit) {
+ const auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let list (AsList
+ (AsStruct '('key (String 'aaa)))
+ (AsStruct '('key (String 'bbb)))
+ (AsStruct '('key (String 'ccc)))
+))
+
+(let structType (StructType '('key (DataType 'String)) '('value (DataType 'String))))
+(let tupleType (TupleType structType structType structType))
+(let vt (VariantType tupleType))
+(let vlist (Extend
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '0))) '0 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '1))) '1 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '2))) '2 vt)))
+))
+
+(let vlist (FlatMap vlist (lambda '(item) (Visit item
+ '0 (lambda '(v) (Just (Variant (AsStruct '('key (Member v 'key)) '('value (Member v 'value))) '1 vt)))
+ (Nothing (OptionalType vt))
+))))
+
+(let res (Map vlist (lambda '(item) (VariantItem item))))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Extend", "Multi(0:{},1:{},2:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Visit", "Multi(1:{Passthrough(key:key,value:value)})");
+ CheckConstraint<TVarIndexConstraintNode>(exprRoot, "Visit", "VarIndex(1:0)");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "FlatMap", "Multi(1:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "VariantItem", "");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Map", "");
+ }
+
+ Y_UNIT_TEST(SwitchAsFilter) {
+ const auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let list (AsList
+ (AsStruct '('key (String 'aaa)))
+ (AsStruct '('key (String 'bbb)))
+ (AsStruct '('key (String 'ccc)))
+))
+
+(let structType (StructType '('key (DataType 'String)) '('value (DataType 'String))))
+(let tupleType (TupleType structType structType structType))
+(let vt (VariantType tupleType))
+(let vlist (Extend
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '0))) '0 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '1))) '1 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '2))) '2 vt)))
+))
+
+(let res (Switch (Iterator vlist) '0 '('0) (lambda '(item) item)))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Extend", "Multi(0:{},1:{},2:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Switch", "");
+ }
+
+ Y_UNIT_TEST(EmptyForMissingMultiOut) {
+ const auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let list (AsList
+ (AsStruct '('key (String 'aaa)))
+ (AsStruct '('key (String 'bbb)))
+ (AsStruct '('key (String 'ccc)))
+))
+
+(let structType (StructType '('key (DataType 'String)) '('value (DataType 'String))))
+(let tupleType3 (TupleType structType structType structType))
+(let vt3 (VariantType tupleType3))
+(let tupleType2 (TupleType structType structType))
+(let vt2 (VariantType tupleType2))
+(let vlist (Extend
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '0))) '0 vt3)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '1))) '1 vt3)))
+))
+
+(let vlist (FlatMap vlist (lambda '(item) (Visit item
+ '0 (lambda '(v) (Just (Variant v '0 vt2)))
+ '1 (lambda '(v) (Just (Variant v '0 vt2)))
+ '2 (lambda '(v) (Just (Variant v '1 vt2)))
+))))
+
+(let res (Map vlist (lambda '(item) (VariantItem item))))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Extend", "Multi(0:{},1:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "FlatMap", "Multi(0:{})");
+ }
+
+ Y_UNIT_TEST(SwitchWithReplicate) {
+ const auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let list (AsList
+ (AsStruct '('key (String 'aaa)))
+ (AsStruct '('key (String 'bbb)))
+ (AsStruct '('key (String 'ccc)))
+))
+
+(let vlist (Switch (Iterator list) '0 '('0) (lambda '(item) item) '('0) (lambda '(item) item)))
+
+(let res (Map vlist (lambda '(item) (VariantItem item))))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Switch", "Multi(0:{},1:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Map", "");
+ }
+
+ Y_UNIT_TEST(SwitchWithMultiOut) {
+ const auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let list (AsList
+ (AsStruct '('key (String 'aaa)))
+ (AsStruct '('key (String 'bbb)))
+ (AsStruct '('key (String 'ccc)))
+))
+
+(let structType (StructType '('key (DataType 'String)) '('value (DataType 'String))))
+(let tupleType (TupleType structType structType structType))
+(let vt (VariantType tupleType))
+(let vlist (Extend
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '0))) '0 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '1))) '1 vt)))
+ (Map list (lambda '(x) (Variant (AsStruct '('key (Member x 'key)) '('value (String '2))) '2 vt)))
+))
+
+(let vlist (Switch (Iterator vlist) '0 '('0) (lambda '(s) (Map s (lambda '(item) (Variant item '1 vt)))) '('1) (lambda '(s) s)))
+
+(let res (Map vlist (lambda '(item) (VariantItem item))))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Extend", "Multi(0:{},1:{},2:{})");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Switch", "Multi(1:{},3:{})");
+ }
+
+ Y_UNIT_TEST(MultiOutLMapWithEmptyInput) {
+ auto s = R"((
+(let mr_sink (DataSink 'yt (quote plato)))
+
+(let structType (StructType '('key (DataType 'String)) '('value (DataType 'String))))
+(let tupleType (TupleType structType structType structType))
+(let vt (VariantType tupleType))
+
+(let vlist (LMap (List (ListType structType)) (lambda '(stream)
+ (FlatMap stream (lambda '(item)
+ (Extend (AsList (Variant item '0 vt)) (AsList (Variant item '1 vt)))
+ ))
+)))
+
+(let vlist (Switch (Iterator vlist) '0 '('0) (lambda '(s) s) '('1) (lambda '(s) s)))
+
+(let res (Map vlist (lambda '(item) (VariantItem item))))
+
+(let world (Write! world mr_sink (Key '('table (String 'Output))) res '('('mode 'renew))))
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TEmptyConstraintNode>(exprRoot, "LMap", "Empty");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "LMap", "");
+ CheckConstraint<TEmptyConstraintNode>(exprRoot, "Switch", "Empty");
+ CheckConstraint<TMultiConstraintNode>(exprRoot, "Switch", "");
+ }
+
+ Y_UNIT_TEST(Unique) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'x)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'y)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'z)))
+ ))
+ (let list (AssumeUnique list '('key 'subkey) '('value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ (AsStruct
+ '('key1 (Member m 'key))
+ '('key2 (Member m 'key))
+ '('subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Member m 'key1))
+ '('p.key2 (Member m 'key2))
+ '('p.subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (return res)
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Take list (Uint64 '2)) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Take", "Unique((p.key1,p.subkey)(p.key2,p.subkey)(value))");
+ }
+
+ Y_UNIT_TEST(UniqueOverTuple) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'm)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'm)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'n)))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey 'value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ '(
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Nth m '0))
+ '('p.key2 (Nth m '1))
+ '('p.subkey (Nth m '2))
+ '('value (Nth m '3))
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap res) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPartOfUniqueConstraintNode>(exprRoot, "Unwrap", "PartOfUnique(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ CheckConstraint<TPartOfUniqueConstraintNode>(exprRoot, "DivePrefixMembers", "PartOfUnique(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "FlatMap", "Unique((key1)(key2))");
+ }
+
+ Y_UNIT_TEST(UniqueOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'x)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'y)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'z)))
+ ))
+ (let list (AssumeUnique list '('key 'subkey) '('value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) m0 m2 m3 m1)))
+ (let res (NarrowMap res (lambda '(m0 m1 m2 m3)
+ (AsStruct
+ '('p.key1 m0)
+ '('p.key2 m3)
+ '('p.subkey m1)
+ '('value m2)
+ )
+ )))
+ (return (Map res (lambda '(row) (DivePrefixMembers row '('p.)))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPartOfUniqueConstraintNode>(exprRoot, "NarrowMap", "PartOfUnique(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ CheckConstraint<TPartOfUniqueConstraintNode>(exprRoot, "Map", "PartOfUnique(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "FlatMap", "Unique((key1,subkey)(key2,subkey))");
+ }
+
+ Y_UNIT_TEST(UniqueExOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (AssumeUnique list))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) (AsStruct '('xxx m0) '('yyy m2)) '(m1 m3))))
+ (let res (WideMap res (lambda '(s0 t1) '((Member s0 'xxx) (Nth t1 '1)) (ReplaceMember s0 'xxx (Nth t1 '0)))))
+ (let res (NarrowMap res (lambda '(t0 s1) (AddMember (AddMember s1 'one (Nth t0 '1)) 'two (Nth t0 '0)))))
+ (return (Collect res))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (LazyList list) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((one,two,yyy)(one,xxx,yyy))");
+ }
+
+ Y_UNIT_TEST(Distinct) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'x)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'y)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'z)))
+ ))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ (AsStruct
+ '('key1 (Member m 'key))
+ '('key2 (Member m 'key))
+ '('subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Member m 'key1))
+ '('p.key2 (Member m 'key2))
+ '('p.subkey (Member m 'subkey))
+ '('value (Member m 'value))
+ )
+ )))
+ (return res)
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Take list (Uint64 '2)) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Take", "Distinct((p.key1,p.subkey)(p.key2,p.subkey)(value))");
+ }
+
+ Y_UNIT_TEST(DistinctOverTuple) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'm)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'm)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'n)))
+ ))
+ (let list (AssumeDistinct list '('key) '('subkey 'value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (Map (Just item) (lambda '(m)
+ '(
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )
+ )))
+ (let res (Map res (lambda '(m)
+ (AsStruct
+ '('p.key1 (Nth m '0))
+ '('p.key2 (Nth m '1))
+ '('p.subkey (Nth m '2))
+ '('value (Nth m '3))
+ )
+ )))
+ (return (Just (DivePrefixMembers (Unwrap res) '('p.))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPartOfDistinctConstraintNode>(exprRoot, "Unwrap", "PartOfDistinct(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ CheckConstraint<TPartOfDistinctConstraintNode>(exprRoot, "DivePrefixMembers", "PartOfDistinct(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "FlatMap", "Distinct((key1)(key2))");
+ }
+
+ Y_UNIT_TEST(DistinctOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'x)))
+ (AsStruct '('key (String '1)) '('subkey (String 'b)) '('value (String 'y)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'z)))
+ ))
+ (let list (AssumeDistinct list '('key 'subkey) '('value)))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) m0 m2 m3 m1)))
+ (let res (NarrowMap res (lambda '(m0 m1 m2 m3)
+ (AsStruct
+ '('p.key1 m0)
+ '('p.key2 m3)
+ '('p.subkey m1)
+ '('value m2)
+ )
+ )))
+ (return (Map res (lambda '(row) (DivePrefixMembers row '('p.)))))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TPartOfDistinctConstraintNode>(exprRoot, "NarrowMap", "PartOfDistinct(p.key1:key,p.key2:key,p.subkey:subkey,value:value)");
+ CheckConstraint<TPartOfDistinctConstraintNode>(exprRoot, "Map", "PartOfDistinct(key1:key,key2:key,subkey:subkey)");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "FlatMap", "Distinct((key1,subkey)(key2,subkey))");
+ }
+
+ Y_UNIT_TEST(DistinctExOverWideFlow) {
+ const auto s = R"((
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+ ))
+ (let list (AssumeDistinct list))
+ (let list (FlatMap list (lambda '(item) (block '(
+ (let res (ExpandMap (ToFlow (Just item)) (lambda '(m)
+ (Member m 'key)
+ (Member m 'key)
+ (Member m 'subkey)
+ (Member m 'value)
+ )))
+ (let res (WideMap res (lambda '(m0 m1 m2 m3) (AsStruct '('xxx m0) '('yyy m2)) '(m1 m3))))
+ (let res (WideMap res (lambda '(s0 t1) '((Member s0 'xxx) (Nth t1 '1)) (ReplaceMember s0 'xxx (Nth t1 '0)))))
+ (let res (NarrowMap res (lambda '(t0 s1) (AddMember (AddMember s1 'one (Nth t0 '1)) 'two (Nth t0 '0)))))
+ (return (Collect res))
+ )))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (LazyList list) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((one,two,yyy)(one,xxx,yyy))");
+ }
+
+ Y_UNIT_TEST(PartitionsByKeysWithCondense1) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let extractor (lambda '(item) '((Member item 'key) (Member item 'subkey))))
+ (let aggr (PartitionsByKeys list extractor (Void) (Void)
+ (lambda '(stream) (Condense1 stream (lambda '(row) row)
+ (lambda '(row state) (IsKeySwitch row state extractor extractor))
+ (lambda '(row state) (AsStruct '('key (Member row 'key)) '('subkey (Member row 'subkey)) '('value (Coalesce (Member row 'value) (Member state 'value)))))
+ ))
+ ))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Skip aggr (Uint64 '1)) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Skip", "Unique((key,subkey))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Skip", "Distinct((key,subkey))");
+ }
+
+ Y_UNIT_TEST(ShuffleByKeysInputUnique) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey)))
+ (let list (AssumeDistinct list '('key 'subkey)))
+ (let aggr (ShuffleByKeys list
+ (lambda '(item) '((Member item 'key) (Member item 'subkey)))
+ (lambda '(stream) (Take stream (Uint64 '100)))
+ ))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Skip aggr (Uint64 '1)) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Skip", "Unique((key)(subkey))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Skip", "Distinct((key,subkey))");
+ }
+
+ Y_UNIT_TEST(ShuffleByKeysHandlerUnique) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey)))
+ (let list (AssumeDistinct list '('key 'subkey)))
+ (let aggr (ShuffleByKeys list
+ (lambda '(item) '((Member item 'key) (Member item 'subkey)))
+ (lambda '(stream) (AssumeDistinct (AssumeUnique stream '('key) '('subkey)) '('key 'subkey)))
+ ))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) (Skip aggr (Uint64 '1)) '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Skip", "Unique((key)(subkey))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Skip", "Distinct((key,subkey))");
+ }
+
+ Y_UNIT_TEST(Reverse) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey)))
+ (let list (AssumeDistinct list '('key 'subkey)))
+ (let sorted (Sort list '((Bool 'False) (Bool 'True)) (lambda '(item) '((Member item 'key) (Member item 'subkey)))))
+ (let reverse (Reverse sorted))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) reverse '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Reverse", "Unique((key)(subkey))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Reverse", "Distinct((key,subkey))");
+ CheckConstraint<TSortedConstraintNode>(exprRoot, "Reverse", "Sorted(key[asc];subkey[desc])");
+ }
+
+ Y_UNIT_TEST(DictItems) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey)))
+ (let list (AssumeDistinct list '('key 'subkey)))
+ (let dict (ToDict list (lambda '(item) (Member item 'value)) (lambda '(item) (AsStruct '('k (Member item 'key)) '('s (Member item 'subkey)))) '('One 'Hashed)))
+ (let items (Map (DictItems dict) (lambda '(item) (AsStruct '('v (Nth item '0)) '('k (Member (Nth item '1) 'k)) '('s (Member (Nth item '1) 's))))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) items '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Map", "Unique((k)(s)(v))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Map", "Distinct((k,s)(v))");
+ }
+
+ Y_UNIT_TEST(DictKeys) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let dict (ToDict list (lambda '(item) (Member item 'value)) (lambda '(item) (AsStruct '('k (Member item 'key)) '('s (Member item 'subkey)))) '('One 'Hashed)))
+ (let items (Map (DictKeys dict) (lambda '(row) (AsStruct '('v row)))))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) items '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Map", "Unique((v))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Map", "Distinct((v))");
+ }
+
+ Y_UNIT_TEST(DictPayloads) {
+ const auto s = R"(
+(
+ (let mr_sink (DataSink 'yt (quote plato)))
+ (let list (AsList
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'c))) '('value (Just (String 'x))))
+ (AsStruct '('key (Just (String '1))) '('subkey (Just (String 'b))) '('value (Just (String 'y))))
+ (AsStruct '('key (Just (String '4))) '('subkey (Just (String 'b))) '('value (Just (String 'z))))
+ ))
+ (let list (AssumeUnique list '('key) '('subkey)))
+ (let list (AssumeDistinct list '('key 'subkey)))
+ (let dict (ToDict list (lambda '(item) (Member item 'value)) (lambda '(item) (AsStruct '('k (Member item 'key)) '('s (Member item 'subkey)))) '('One 'Hashed)))
+ (let items (DictPayloads dict))
+ (let world (Write! world mr_sink (Key '('table (String 'Output))) items '('('mode 'renew))))
+ (let world (Commit! world mr_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "DictPayloads", "Unique((k)(s))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "DictPayloads", "Distinct((k,s))");
+ }
+
+ Y_UNIT_TEST(GraceJoinInner) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Inner '('0 '1) '('0 '1) '('0 '0 '2 '1) '('0 '2 '1 '3 '2 '4) '()))
+ (let list (Collect (NarrowMap join (lambda '(lk lv rk rs rv) (AsStruct '('lk lk) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv)(rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((lv)(rk,rs)(rv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinLeft) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Left '('0 '1) '('0 '1) '('0 '0 '2 '1) '('0 '2 '1 '3 '2 '4) '()))
+ (let list (Collect (NarrowMap join (lambda '(lk lv rk rs rv) (AsStruct '('lk lk) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv)(rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((lv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinFull) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Full '('0 '1) '('0 '1) '('0 '0 '2 '1) '('0 '2 '1 '3 '2 '4) '()))
+ (let list (Collect (NarrowMap join (lambda '(lk lv rk rs rv) (AsStruct '('lk lk) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv)(rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "");
+ }
+
+ Y_UNIT_TEST(GraceJoinRight) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Right '('0 '1) '('0 '1) '('0 '0 '2 '1) '('0 '2 '1 '3 '2 '4) '()))
+ (let list (Collect (NarrowMap join (lambda '(lk lv rk rs rv) (AsStruct '('lk lk) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv)(rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((rk,rs)(rv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinExclusion) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Exclusion '('0 '1) '('0 '1) '('0 '0 '2 '1) '('0 '2 '1 '3 '2 '4) '()))
+ (let list (Collect (NarrowMap join (lambda '(lk lv rk rs rv) (AsStruct '('lk lk) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv)(rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "");
+ }
+
+ Y_UNIT_TEST(GraceJoinLeftSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'LeftSemi '('0 '1) '('0 '1) '('0 '2 '1 '1 '2 '0) '() '()))
+ (let list (Collect (NarrowMap join (lambda '(lv ls lk) (AsStruct '('lk lk) '('lv lv) '('ls ls))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lk,ls)(lv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((lk,ls)(lv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinLeftOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'LeftOnly '('0 '1) '('0 '1) '('0 '1 '2 '0) '() '()))
+ (let list (Collect (NarrowMap join (lambda '(lv lk) (AsStruct '('lk lk) '('lv lv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((lv))");
+ }
+
+
+ Y_UNIT_TEST(GraceJoinRightOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'RightOnly '('0 '1) '('0 '1) '() '('0 '2 '1 '1 '2 '0) '()))
+ (let list (Collect (NarrowMap join (lambda '(rv rs rk) (AsStruct '('rk rk) '('rv rv) '('rs rs))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((rk,rs)(rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((rk,rs)(rv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinRightSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'RightSemi '('0 '1) '('0 '1) '() '('0 '1 '2 '0) '()))
+ (let list (Collect (NarrowMap join (lambda '(rv rk) (AsStruct '('rk rk) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((rv))");
+ }
+
+ Y_UNIT_TEST(GraceJoinInnerBothAny) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '1)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '2)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '3)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '8)) '('subkey1 (Uint8 '4)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '0)) '('subkey2 (Uint8 '2)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '0)) '('subkey2 (Uint8 '2)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '0)) '('subkey2 (Uint8 '3)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '3)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '3)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'value2)))
+ (let list2 (AssumeDistinct list2 '('subkey2 'value2)))
+
+ (let flow1 (ExpandMap (ToFlow list1) (lambda '(item) (Member item 'key1) (Member item 'subkey1) (Member item 'value1))))
+ (let flow2 (ExpandMap (ToFlow list2) (lambda '(item) (Member item 'key2) (Member item 'subkey2) (Member item 'value2))))
+
+ (let join (GraceJoinCore flow1 flow2 'Inner '('0 '1) '('0 '1) '('0 '0 '1 '1 '2 '2) '('0 '3 '1 '4 '2 '5) '('LeftAny 'RightAny)))
+ (let list (Collect (NarrowMap join (lambda '(lk ls lv rk rs rv) (AsStruct '('lk lk) '('ls ls) '('lv lv) '('rk rk) '('rs rs) '('rv rv))))))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((lk)(lv)(rk,rv))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((ls)(lv)(rs,rv))");
+ }
+
+ Y_UNIT_TEST(MapJoinInnerOne) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '((Member item 'subkey2) (Member item 'value2))) '('One 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'Inner '('key1 'subkey1) '('key1 'key 'subkey1 'subkey 'value1 'value) '('0 's '1 'v)))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((key,subkey)(v)(value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((key,subkey)(v)(value))");
+ }
+
+ Y_UNIT_TEST(MapJoinInnerMany) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '((Member item 'subkey2) (Member item 'value2))) '('Many 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'Inner '('key1 'subkey1) '('key1 'key 'subkey1 'subkey 'value1 'value) '('0 's '1 'v)))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "");
+ }
+
+ Y_UNIT_TEST(MapJoinLeftOne) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '((Member item 'subkey2) (Member item 'value2))) '('One 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'Left '('key1 'subkey1) '('key1 'key 'subkey1 'subkey 'value1 'value) '('0 's '1 'v)))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((key,subkey)(v)(value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((key,subkey)(value))");
+ }
+
+ Y_UNIT_TEST(MapJoinLeftMany) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '((Member item 'subkey2) (Member item 'value2))) '('Many 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'Left '('key1 'subkey1) '('key1 'key 'subkey1 'subkey 'value1 'value) '('0 's '1 'v)))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "");
+ }
+
+ Y_UNIT_TEST(MapJoinLeftSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '()) '('One 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'LeftSemi '('key1 'subkey1) '('key1 'key 'subkey1 'subkey 'value1 'value) '()))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((key,subkey)(value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((key,subkey)(value))");
+ }
+
+ Y_UNIT_TEST(MapJoinLeftOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let dict (ToDict list2 (lambda '(item) '((Member item 'key2) (Member item 'subkey2))) (lambda '(item) '()) '('One 'Hashed)))
+
+ (let join (MapJoinCore (ToFlow list1) dict 'LeftOnly '('key1 'subkey1) '('key1 'key 'value1 'value) '()))
+ (let list (Collect join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) list '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "Collect", "Unique((value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "Collect", "Distinct((value))");
+ }
+
+ Y_UNIT_TEST(EquiJoinWithRenames) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '('Inner 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) '(
+ '('rename 'a.key1 'key_1)
+ '('rename 'a.key1 'key_2)
+ '('rename 'a.subkey1 'subkey_1)
+ '('rename 'a.subkey1 'subkey_2)
+ '('rename 'a.value1 '"")
+ '('rename 'b.key2 '"")
+ '('rename 'b.value2 'value_1)
+ '('rename 'b.value2 'value_2)
+ )))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((key_1,subkey_1)(key_1,subkey_2)(key_2,subkey_1)(key_2,subkey_2)(value_1)(value_2))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((key_1,subkey_1)(key_1,subkey_2)(key_2,subkey_1)(key_2,subkey_2)(value_1)(value_2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinWithPartialRenames) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '('Left 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) '(
+ '('rename 'a.key1 'key_1)
+ '('rename 'a.key1 'key_2)
+ '('rename 'a.value1 '"")
+ '('rename 'b.key2 '"")
+ '('rename 'b.value2 'value)
+ )))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.subkey1,key_1)(a.subkey1,key_2)(value))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.subkey1,key_1)(a.subkey1,key_2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerInner) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Inner 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerLeft) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Left 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerRight) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Right 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerFull) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Full 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerExclusion) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Exclusion 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerLeftOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('LeftOnly 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('a 'key1 'a 'subkey1) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerLeftSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('LeftSemi 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('a 'key1 'a 'subkey1) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerRightOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('RightOnly 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinInnerRightSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('RightSemi 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftInner) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('Inner 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftLeft) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('Left 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftRight) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('Right 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftFull) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('Full 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftExclusion) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('Exclusion 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftLeftOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('LeftOnly 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('a 'key1 'a 'subkey1) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftLeftSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('LeftSemi 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('a 'key1 'a 'subkey1) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((a.key1,a.subkey1)(a.value1)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((a.key1,a.subkey1)(a.value1))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftRightOnly) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('RightOnly 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinLeftRightSemi) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Left '('RightSemi 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '()))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "Unique((b.key2,b.subkey2)(b.value2)(c.key3,c.subkey3)(c.value3))");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "Distinct((b.key2,b.subkey2)(b.value2))");
+ }
+
+ Y_UNIT_TEST(EquiJoinFlatten) {
+ const auto s = R"(
+(
+ (let list1 (AsList
+ (AsStruct '('key1 (Int32 '1)) '('subkey1 (Uint8 '0)) '('value1 (String 'A)))
+ (AsStruct '('key1 (Int32 '7)) '('subkey1 (Uint8 '0)) '('value1 (String 'B)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '0)) '('value1 (String 'C)))
+ (AsStruct '('key1 (Int32 '4)) '('subkey1 (Uint8 '1)) '('value1 (String 'D)))
+ ))
+
+ (let list1 (AssumeUnique list1 '('key1 'subkey1) '('value1)))
+ (let list1 (AssumeDistinct list1 '('key1 'subkey1) '('value1)))
+
+ (let list2 (AsList
+ (AsStruct '('key2 (Int32 '9)) '('subkey2 (Uint8 '0)) '('value2 (String 'Z)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '0)) '('value2 (String 'Y)))
+ (AsStruct '('key2 (Int32 '3)) '('subkey2 (Uint8 '1)) '('value2 (String 'X)))
+ (AsStruct '('key2 (Int32 '4)) '('subkey2 (Uint8 '1)) '('value2 (String 'W)))
+ (AsStruct '('key2 (Int32 '8)) '('subkey2 (Uint8 '1)) '('value2 (String 'V)))
+ ))
+
+ (let list2 (AssumeUnique list2 '('key2 'subkey2) '('value2)))
+ (let list2 (AssumeDistinct list2 '('key2 'subkey2) '('value2)))
+
+ (let list3 (AsList
+ (AsStruct '('key3 (Int32 '1)) '('subkey3 (Uint8 '0)) '('value3 (String 'G)))
+ (AsStruct '('key3 (Int32 '4)) '('subkey3 (Uint8 '1)) '('value3 (String 'H)))
+ (AsStruct '('key3 (Int32 '2)) '('subkey3 (Uint8 '0)) '('value3 (String 'I)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '1)) '('value3 (String 'J)))
+ (AsStruct '('key3 (Int32 '3)) '('subkey3 (Uint8 '0)) '('value3 (String 'K)))
+ ))
+
+ (let list3 (AssumeUnique list3 '('key3 'subkey3) '('value3)))
+ (let list3 (AssumeDistinct list3 '('key3 'subkey3) '('value3)))
+
+ (let join (EquiJoin '(list1 'a) '(list2 'b) '(list3 'c) '('Inner '('Inner 'a 'b '('a 'key1 'a 'subkey1) '('b 'key2 'b 'subkey2) '()) 'c '('b 'key2 'b 'subkey2) '('c 'key3 'c 'subkey3) '()) '('('flatten))))
+ (let lazy (LazyList join))
+
+ (let res_sink (DataSink 'yt (quote plato)))
+ (let world (Write! world res_sink (Key '('table (String 'Output))) lazy '('('mode 'renew))))
+
+ (let world (Commit! world res_sink))
+ (return world)
+)
+ )";
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TUniqueConstraintNode>(exprRoot, "LazyList", "");
+ CheckConstraint<TDistinctConstraintNode>(exprRoot, "LazyList", "");
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_expr_discover_ut.cpp b/ydb/library/yql/core/ut/yql_expr_discover_ut.cpp
new file mode 100644
index 0000000000..704fdc9faa
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_expr_discover_ut.cpp
@@ -0,0 +1,152 @@
+#include "yql_opt_proposed_by_data.h"
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+#include <ydb/library/yql/core/services/yql_eval_expr.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/yson/writer.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TDiscoverYqlExpr) {
+
+ static TString Discover(const TString& ast) {
+ TAstParseResult astRes = ParseAst(ast);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ typeAnnotationContext->DiscoveryMode = true;
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typeAnnotationContext.Get();
+
+ InitializeYtGateway(ytGateway, ytState);
+ auto randomProvider = CreateDeterministicRandomProvider(1);
+ typeAnnotationContext->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ auto datasource = CreateYtDataSource(ytState);
+ typeAnnotationContext->AddDataSource(YtProviderName, datasource);
+ auto intentTransformer = CreateIntentDeterminationTransformer(*typeAnnotationContext);
+ TVector<TTransformStage> transformers;
+ const auto issueCode = TIssuesIds::DEFAULT_ERROR;
+ transformers.push_back(TTransformStage(CreateFunctorTransformer(
+ [=](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ return EvaluateExpression(input, output, *typeAnnotationContext, ctx, *functionRegistry, nullptr);
+ }),
+ "EvaluateExpression",
+ issueCode));
+ transformers.push_back(TTransformStage(
+ CreateIODiscoveryTransformer(*typeAnnotationContext),
+ "IODiscovery",
+ issueCode));
+ transformers.push_back(TTransformStage(
+ CreateEpochsTransformer(*typeAnnotationContext),
+ "Epochs",
+ issueCode));
+ transformers.push_back(TTransformStage(
+ intentTransformer,
+ "IntentDetermination",
+ issueCode));
+ auto fullTransformer = CreateCompositeGraphTransformer(transformers, true);
+
+ TStringStream str;
+ if (SyncTransform(*fullTransformer, exprRoot, exprCtx) == IGraphTransformer::TStatus::Ok) {
+ NYson::TYsonWriter writer(&str, NYson::EYsonFormat::Text);
+ datasource->CollectDiscoveredData(writer);
+ } else {
+ exprCtx.IssueManager.GetIssues().PrintTo(str);
+ }
+ return str.Str();
+ }
+
+ Y_UNIT_TEST(DiscoverYt) {
+ auto s = R"((
+(let mr_source (DataSource 'yt 'plato))
+(let x (Read! world mr_source
+ (Key '('table (String 'Input)))
+ '('key 'subkey 'value) '()))
+(let world (Left! x))
+(let table (Right! x))
+
+(let mr_sink (DataSink 'yt 'plato))
+(let world (Write! world mr_sink
+ (Key '('table (String 'Output)))
+ table '('('mode 'append))))
+
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ auto res = Discover(s);
+ UNIT_ASSERT_VALUES_EQUAL(res, "[[\"plato\";\"Input\";[\"read\"]];[\"plato\";\"Output\";[\"modify\"]]]");
+ }
+
+ Y_UNIT_TEST(ErrorOnRange) {
+ auto s = R"((
+(let mr_source (DataSource 'yt 'plato))
+(let range (MrTableRange '"" (lambda '($i) (And (>= $i (String '"Input1")) (<= $i (String '"Input2")))) '""))
+(let x (Read! world mr_source
+ (Key '('table range))
+ '('key 'subkey 'value) '()))
+(let world (Left! x))
+(let table (Right! x))
+
+(let mr_sink (DataSink 'yt 'plato))
+(let world (Write! world mr_sink
+ (Key '('table (String 'Output)))
+ table '('('mode 'append))))
+
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ auto res = Discover(s);
+ UNIT_ASSERT_VALUES_EQUAL(res, R"(<main>: Error: Default error
+ <main>:5:6: Error: MrTableRange/MrTableRangeStrict is not allowed in Discovery mode, code: 4600
+)");
+ }
+
+ Y_UNIT_TEST(ErrorOnTime) {
+ auto s = R"((
+(let mr_source (DataSource 'yt 'plato))
+(let x (Read! world mr_source
+ (Key '('table (String (EvaluateAtom (SafeCast (CurrentUtcDate) (DataType 'String))))))
+ '('key 'subkey 'value) '()))
+(let world (Left! x))
+(let table (Right! x))
+
+(let mr_sink (DataSink 'yt 'plato))
+(let world (Write! world mr_sink
+ (Key '('table (String 'Output)))
+ table '('('mode 'append))))
+
+(let world (Commit! world mr_sink))
+(return world)
+))";
+
+ auto res = Discover(s);
+ UNIT_ASSERT_VALUES_EQUAL(res, R"(<main>: Error: Default error
+ <main>:4:28: Error: At function: EvaluateAtom
+ <main>: Error: Type annotation, code: 1030
+ <main>:4:42: Error: At function: SafeCast
+ <main>:4:52: Error: At function: CurrentUtcDate
+ <main>:4:52: Error: CurrentUtcDate is not allowed in Discovery mode, code: 4600
+)");
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_expr_optimize_ut.cpp b/ydb/library/yql/core/ut/yql_expr_optimize_ut.cpp
new file mode 100644
index 0000000000..6a95e37117
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_expr_optimize_ut.cpp
@@ -0,0 +1,655 @@
+#include "yql_expr_optimize.h"
+#include "yql_opt_rewrite_io.h"
+#include "yql_opt_proposed_by_data.h"
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+#include <ydb/library/yql/core/facade/yql_facade.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TOptimizeYqlExpr) {
+ Y_UNIT_TEST(CombineAtoms) {
+ const auto s = "(\n"
+ "(let x (Combine '11 '333 '7))\n"
+ "(return x)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(return '113337)") != TString::npos);
+ }
+
+ Y_UNIT_TEST(RecursiveLambda) {
+ const auto s =
+ R"(
+ (
+ (let f**k (lambda '(x l) (+ x (Apply l x l))))
+ (return (Apply f**k (Uint32 '1) f**k))
+ )
+ )";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ for (size_t i = 0U; i < 0x100; ++i)
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ const auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ const auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT_EQUAL(0x101, std::count(strRes.cbegin(), strRes.cend(), '+'));
+ }
+
+ Y_UNIT_TEST(ApplyWideLambda) {
+ const auto s =
+ R"(
+ (
+ (let wide (lambda '(x y) (+ x y) (* x y) x y))
+ (return '('1 (Apply wide (Int32 '3) (Int32 '7)) '9))
+ )
+ )";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(return '('1 (+ $1 $2) (* $1 $2) $1 $2 '9))") != TString::npos);
+ }
+
+ Y_UNIT_TEST(ApplyThinLambda) {
+ const auto s =
+ R"(
+ (
+ (let wide (lambda '(x y)))
+ (return '('1 (Apply wide (Int32 '3) (Int32 '7)) '9))
+ )
+ )";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(return '('1 '9))") != TString::npos);
+ }
+
+ Y_UNIT_TEST(ApplyDeepLambda) {
+ const auto s = "# program\n"
+ "(\n"
+ "(let x (Uint64 '42))\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "(let l (lambda '(y) (block '(\n"
+ "\n"
+ "(let l (lambda '(y) (+ x y)))\n"
+ "\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "(return (Apply l (+ y x)))))))\n"
+ "\n"
+ "(let res_sink (DataSink 'result))\n"
+ "(let resKey (Apply l (Int64 '7)))\n"
+ "(let world (Write! world res_sink (Key) resKey '('('type))))\n"
+ "(let world (Commit! world res_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ while (true) {
+ const auto ret = ExpandApply(exprRoot, exprRoot, exprCtx);
+ if (ret.Level != IGraphTransformer::TStatus::Repeat) {
+ UNIT_ASSERT_EQUAL(ret.Level, IGraphTransformer::TStatus::Ok);
+ break;
+ }
+ }
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT_EQUAL(strRes.find("lambda"), TString::npos);
+ }
+
+ Y_UNIT_TEST(Nth) {
+ const auto s = "(\n"
+ "(let x '('11 '333 '7))\n"
+ "(return (Nth x '2))\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(return '7)") != TString::npos);
+ }
+
+ Y_UNIT_TEST(NthLargeIndex) {
+ const auto s = "(\n"
+ "(let x '('11 '333 '7))\n"
+ "(return (Nth x '3))\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Error, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+ UNIT_ASSERT_VALUES_EQUAL("<main>:3:17: Error: Index too large: (3 >= 3).\n", exprCtx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(NthWrongIndex) {
+ const auto s = "(\n"
+ "(let x '('11 '333 '7))\n"
+ "(return (Nth x 'Z))\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Error, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+ UNIT_ASSERT_VALUES_EQUAL("<main>:3:17: Error: Index 'Z' isn't UI32.\n", exprCtx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(NthArg) {
+ const auto s = "(\n"
+ "(let x (NthArg '1 (+ '37 '42)))\n"
+ "(return x)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Repeat, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(return '42)") != TString::npos);
+ }
+
+ Y_UNIT_TEST(NthArgLargeIndex) {
+ const auto s = "(\n"
+ "(let x (NthArg '2 (- '37 '42)))\n"
+ "(return x)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Error, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+ UNIT_ASSERT_VALUES_EQUAL("<main>:2:17: Error: Index too large: (2 >= 2).\n", exprCtx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(NthArgWrongIndex) {
+ const auto s = "(\n"
+ "(let x (NthArg 'bad (* '37 '42)))\n"
+ "(return x)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Error, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+ UNIT_ASSERT_VALUES_EQUAL("<main>:2:17: Error: Index 'bad' isn't UI32.\n", exprCtx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(NthArgNotCallable) {
+ const auto s = "(\n"
+ "(let x (NthArg '0 'bad))\n"
+ "(return x)\n"
+ ")\n";
+
+ const auto astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+ UNIT_ASSERT_EQUAL(IGraphTransformer::TStatus::Error, ExpandApply(exprRoot, exprRoot, exprCtx).Level);
+ UNIT_ASSERT_VALUES_EQUAL("<main>:2:20: Error: Expected callable, but got: Atom\n", exprCtx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(CheckIORewrite) {
+ auto s = "(\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let tresh (String '100))\n"
+ "(let table1low (Filter table1 (lambda '(item1) (> tresh (Member item1 'key)))))\n"
+ "(let mr_sink (DataSink 'yt (quote plato)))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) table1low '('('mode 'append))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ TAstParseResult astRes = ParseAst(s);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typeAnnotationContext.Get();
+
+ InitializeYtGateway(ytGateway, ytState);
+ auto randomProvider = CreateDeterministicRandomProvider(1);
+ typeAnnotationContext->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ typeAnnotationContext->AddDataSource(YtProviderName, CreateYtDataSource(ytState));
+ auto intentTransformer = CreateIntentDeterminationTransformer(*typeAnnotationContext);
+ TVector<TTransformStage> transformers;
+ const auto issueCode = TIssuesIds::DEFAULT_ERROR;
+ transformers.push_back(TTransformStage(
+ CreateIODiscoveryTransformer(*typeAnnotationContext),
+ "IODiscovery",
+ issueCode));
+ transformers.push_back(TTransformStage(
+ CreateEpochsTransformer(*typeAnnotationContext),
+ "Epochs",
+ issueCode));
+
+ // NOTE: add fake EvaluateExpression step to break infinite loop
+ // (created by Repeat on ExprEval step after RewriteIO completion)
+ transformers.push_back(TTransformStage(
+ CreateFunctorTransformer(
+ [](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ output = input;
+ ctx.Step.Done(TExprStep::ExprEval);
+ return IGraphTransformer::TStatus::Ok;
+ }
+ ),
+ "EvaluateExpression",
+ issueCode));
+
+ transformers.push_back(TTransformStage(
+ intentTransformer,
+ "IntentDetermination",
+ issueCode));
+ transformers.push_back(TTransformStage(
+ CreateTableMetadataLoader(*typeAnnotationContext),
+ "MetadataLoader",
+ issueCode));
+ // NOTE: metadata loader unconditionally drops ExpandApplyForLambdas flag
+ // in DoApplyAsyncChanges, so ExpandApply transformation is in such an unusual place
+ transformers.push_back(TTransformStage(
+ CreateFunctorTransformer(&ExpandApply),
+ "ExpandApply",
+ issueCode));
+ auto fullTransformer = CreateCompositeGraphTransformer(transformers, true);
+ bool success = SyncTransform(*fullTransformer, exprRoot, exprCtx) == IGraphTransformer::TStatus::Ok;
+ UNIT_ASSERT(success);
+
+ UNIT_ASSERT(RewriteIO(exprRoot, exprRoot, *typeAnnotationContext, exprCtx).Level == IGraphTransformer::TStatus::Ok);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ auto strRes = ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT(strRes.find("(YtReadTable!") != TString::npos);
+ UNIT_ASSERT(strRes.find("(Read!") == TString::npos);
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_expr_providers_ut.cpp b/ydb/library/yql/core/ut/yql_expr_providers_ut.cpp
new file mode 100644
index 0000000000..9330804e2a
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_expr_providers_ut.cpp
@@ -0,0 +1,283 @@
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+
+#include <ydb/library/yql/ast/yql_ast_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/facade/yql_facade.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/string/hex.h>
+#include <util/random/random.h>
+#include <util/system/sanitizers.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TCompileWithProvidersYqlExpr) {
+ static TAstParseResult ParseAstWithCheck(const TStringBuf& s) {
+ TAstParseResult res = ParseAst(s);
+ res.Issues.PrintTo(Cout);
+ UNIT_ASSERT(res.IsOk());
+ return res;
+ }
+
+ static void CompileExprWithCheck(TAstNode& root, TExprNode::TPtr& exprRoot, TExprContext& exprCtx,
+ ui32 annotationFlags = TExprAnnotationFlags::None)
+ {
+ const bool success = CompileExpr(root, exprRoot, exprCtx, nullptr, annotationFlags);
+ exprCtx.IssueManager.GetIssues().PrintTo(Cout);
+
+ UNIT_ASSERT(success);
+ UNIT_ASSERT_VALUES_EQUAL(exprRoot->GetState(), annotationFlags != TExprAnnotationFlags::None
+ ? TExprNode::EState::TypeComplete
+ : TExprNode::EState::Initial);
+ }
+
+ static void AnnotateExprWithCheck(TExprNode::TPtr& root, TExprContext& ctx, bool wholeProgram, TTypeAnnotationContextPtr typeAnnotationContext) {
+ const bool success = SyncAnnotateTypes(root, ctx, wholeProgram, *typeAnnotationContext);
+ ctx.IssueManager.GetIssues().PrintTo(Cout);
+ UNIT_ASSERT(success);
+ }
+
+ static void VerifyProgram(const TString& s) {
+ TAstParseResult astRes = ParseAstWithCheck(s);
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ CompileExprWithCheck(*astRes.Root, exprRoot, exprCtx);
+ UNIT_ASSERT(!exprRoot->GetTypeAnn());
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typeAnnotationContext.Get();
+
+ InitializeYtGateway(ytGateway, ytState);
+ typeAnnotationContext->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ typeAnnotationContext->AddDataSource(YtProviderName, CreateYtDataSource(ytState));
+ AnnotateExprWithCheck(exprRoot, exprCtx, true, typeAnnotationContext);
+ UNIT_ASSERT(exprRoot->GetTypeAnn());
+ //PrintExpr(Cout, *exprRoot, exprRoot, true);
+
+ auto convertedAst = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ //PrintAst(Cout, *convertedAst, TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ TStringStream strOrig;
+ convertedAst.Root->PrettyPrintTo(strOrig, TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+
+ //PrintAst(Cout, *AnnotatePositions(*convertedAst), TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+
+ auto convertedAstWithAnnotations1 = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::Position, true);
+ TString strAnn1res = convertedAstWithAnnotations1.Root->ToString(TAstPrintFlags::ShortQuote);
+ TAstParseResult astResAnn1 = ParseAstWithCheck(strAnn1res);
+ UNIT_ASSERT(astResAnn1.IsOk());
+
+ TMemoryPool pool(4096);
+ auto cleanAst1 = RemoveAnnotations(*convertedAstWithAnnotations1.Root, pool);
+ UNIT_ASSERT(cleanAst1);
+ TStringStream strClean1;
+ cleanAst1->PrettyPrintTo(strClean1, TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT_VALUES_EQUAL(strClean1.Str(), strOrig.Str());
+
+ auto convertedAstWithAnnotations2 = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::Types, true);
+ TString strAnn2res = convertedAstWithAnnotations2.Root->ToString(TAstPrintFlags::ShortQuote);
+
+ TAstParseResult astResAnn2 = ParseAstWithCheck(strAnn2res);
+ UNIT_ASSERT(astResAnn2.IsOk());
+
+ auto cleanAst2 = RemoveAnnotations(*convertedAstWithAnnotations2.Root, pool);
+ UNIT_ASSERT(cleanAst2);
+ TStringStream strClean2;
+ cleanAst2->PrettyPrintTo(strClean2, TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT_VALUES_EQUAL(strClean2.Str(), strOrig.Str());
+
+ TExprContext exprCtxAnnTypes;
+ TExprNode::TPtr exprRootAnnTypes;
+ CompileExprWithCheck(*astResAnn2.Root, exprRootAnnTypes, exprCtxAnnTypes, (ui32)TExprAnnotationFlags::Types);
+ UNIT_ASSERT(exprRootAnnTypes->GetTypeAnn());
+
+ auto convertedAstWithAnnotations3 = ConvertToAst(*exprRootAnnTypes, exprCtxAnnTypes, TExprAnnotationFlags::Types, true);
+ TString strAnn3res = convertedAstWithAnnotations3.Root->ToString(TAstPrintFlags::ShortQuote);
+ UNIT_ASSERT_EQUAL(strAnn2res, strAnn3res);
+
+ const ui32 flagsToParseBack[] = {
+ 0,
+ TAstPrintFlags::PerLine,
+ // TAstPrintFlags::ShortQuote, // -- generates invalid AST
+ TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote
+ };
+
+ for (ui32 index = 0; index < Y_ARRAY_SIZE(flagsToParseBack); ++index) {
+ auto flag = flagsToParseBack[index];
+ auto s2 = convertedAst.Root->ToString(flag);
+
+ TAstParseResult res2 = ParseAst(s2);
+ res2.Issues.PrintTo(Cout);
+ UNIT_ASSERT(res2.IsOk());
+
+ auto s3 = res2.Root->ToString(flag);
+ UNIT_ASSERT_STRINGS_EQUAL(s2, s3);
+
+ TExprContext exprCtx2;
+ TExprNode::TPtr exprRoot2;
+ CompileExprWithCheck(*res2.Root, exprRoot2, exprCtx2);
+
+ auto convertedAst2 = ConvertToAst(*exprRoot2, exprCtx2, TExprAnnotationFlags::None, true);
+ auto s4 = convertedAst2.Root->ToString(flag);
+ UNIT_ASSERT_STRINGS_EQUAL(s2, s4);
+ }
+ }
+
+ Y_UNIT_TEST(TestComplexProgramWithLamda) {
+ auto s = "(\n"
+ "#comment\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let tresh (String '100))\n"
+ "(let table1low (Filter table1 (lambda '(item) (< (Member item 'key) tresh))))\n"
+ "(let mr_sink (DataSink 'yt (quote plato)))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) table1low '('('mode 'append))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ VerifyProgram(s);
+ }
+
+ Y_UNIT_TEST(TestSerializeAtomAsPartialExpression) {
+ auto s = "(\n"
+ "(let x 'a)\n"
+ "(let y 'b)\n"
+ "(let z (quote (x y)))\n"
+ "(return z)\n"
+ ")\n";
+
+ TAstParseResult astRes = ParseAstWithCheck(s);
+ UNIT_ASSERT(astRes.IsOk());
+
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ CompileExprWithCheck(*astRes.Root, exprRoot, exprCtx);
+
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ AnnotateExprWithCheck(exprRoot, exprCtx, false, typeAnnotationContext);
+ UNIT_ASSERT(exprRoot->GetTypeAnn());
+ UNIT_ASSERT_VALUES_EQUAL(exprRoot->Child(0)->Content(), "a");
+
+ auto convertedAstWithTypes = ConvertToAst(*exprRoot->Child(0), exprCtx, TExprAnnotationFlags::Types, true);
+ TString strAnnRes = convertedAstWithTypes.Root->ToString(TAstPrintFlags::ShortQuote);
+ TAstParseResult astRes2 = ParseAstWithCheck(strAnnRes);
+ UNIT_ASSERT(astRes2.IsOk());
+
+ TExprContext exprCtx2;
+ TExprNode::TPtr exprRoot2;
+ CompileExprWithCheck(*astRes2.Root, exprRoot2, exprCtx2, (ui32)TExprAnnotationFlags::Types);
+ UNIT_ASSERT(exprRoot2->GetTypeAnn());
+ UNIT_ASSERT_VALUES_EQUAL(exprRoot2->Content(), "a");
+
+ ui32 annotationFlags = TExprAnnotationFlags::Types | TExprAnnotationFlags::Position;
+ auto convertedAstWithTypesAndPos = ConvertToAst(*exprRoot->Child(0), exprCtx, annotationFlags, true);
+ TString strPosAnnRes = convertedAstWithTypesAndPos.Root->ToString(TAstPrintFlags::ShortQuote);
+ TAstParseResult astRes3 = ParseAstWithCheck(strPosAnnRes);
+ UNIT_ASSERT(astRes3.IsOk());
+
+ TExprContext exprCtx3;
+ TExprNode::TPtr exprRoot3;
+ CompileExprWithCheck(*astRes3.Root, exprRoot3, exprCtx3, annotationFlags);
+ UNIT_ASSERT(exprRoot2->GetTypeAnn());
+ UNIT_ASSERT_VALUES_EQUAL(exprRoot2->Content(), "a");
+ }
+
+ Y_UNIT_TEST(TestComplexProgramWithLamdaAsBlock) {
+ auto s = "(\n"
+ "#comment\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ "(let world (Left! x))\n"
+ "(let table1 (Right! x))\n"
+ "(let table1low (Filter table1 (lambda '(item) (block '(\n"
+ " (let tresh (String '100))\n"
+ " (let predicate (< (Member item 'key) tresh))\n"
+ " (return predicate)\n"
+ ")))))\n"
+ "(let mr_sink (DataSink 'yt (quote plato)))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) table1low '('('mode 'append))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ VerifyProgram(s);
+ }
+
+ Y_UNIT_TEST(TestComplexProgramWithBlockAndLambdaAsBlock) {
+ auto s = "(\n"
+ "#comment\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let data (block '(\n"
+ " (let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ " (let world (Left! x))\n"
+ " (let table1 (Right! x))\n"
+ " (let table1low (Filter table1 (lambda '(item) (block '(\n"
+ " (let tresh (String '100))\n"
+ " (let predicate (< (Member item 'key) tresh))\n"
+ " (return predicate)\n"
+ " )))))\n"
+ " (return table1low)\n"
+ ")))\n"
+ "(let mr_sink (DataSink 'yt (quote plato)))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) data '('('mode 'append))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ VerifyProgram(s);
+ }
+
+ Y_UNIT_TEST(TestPerfCompile) {
+ auto s = "(\n"
+ "#comment\n"
+ "(let mr_source (DataSource 'yt 'plato))\n"
+ "(let data (block '(\n"
+ " (let x (Read! world mr_source (Key '('table (String 'Input))) '('key 'subkey 'value) '()))\n"
+ " (let world (Left! x))\n"
+ " (let table1 (Right! x))\n"
+ " (let table1low (Filter table1 (lambda '(item) (block '(\n"
+ " (let tresh (String '100))\n"
+ " (let predicate (< (Member item 'key) tresh))\n"
+ " (return predicate)\n"
+ " )))))\n"
+ " (return table1low)\n"
+ ")))\n"
+ "(let mr_sink (DataSink 'yt (quote plato)))\n"
+ "(let world (Write! world mr_sink (Key '('table (String 'Output))) data '('('mode 'append))))\n"
+ "(let world (Commit! world mr_sink))\n"
+ "(return world)\n"
+ ")\n";
+
+ TAstParseResult astRes = ParseAstWithCheck(s);
+ const ui32 n = NSan::PlainOrUnderSanitizer(10000, 1000);
+ auto t1 = TInstant::Now();
+ for (ui32 i = 0; i < n; ++i) {
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ CompileExprWithCheck(*astRes.Root, exprRoot, exprCtx);
+ }
+
+ auto t2 = TInstant::Now();
+ Cout << t2 - t1 << Endl;
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_expr_type_annotation_ut.cpp b/ydb/library/yql/core/ut/yql_expr_type_annotation_ut.cpp
new file mode 100644
index 0000000000..09ce7fd838
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_expr_type_annotation_ut.cpp
@@ -0,0 +1,53 @@
+#include "yql_expr_type_annotation.h"
+
+#include <library/cpp/testing/unittest/registar.h>
+
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(Misc) {
+ Y_UNIT_TEST(NormalizeName) {
+ auto CheckNoIssues = [](TString name, const TString& expected) {
+ UNIT_ASSERT_C(!NormalizeName(TPosition(), name), name);
+ UNIT_ASSERT_VALUES_EQUAL(name, expected);
+ };
+
+ auto CheckIssues = [](TString name, const TString& expected) {
+ UNIT_ASSERT_C(NormalizeName(TPosition(), name), name);
+ UNIT_ASSERT_VALUES_EQUAL(name, expected);
+ };
+
+ CheckNoIssues("", "");
+ CheckNoIssues("_", "_");
+ CheckNoIssues("__", "__");
+ CheckNoIssues("a", "a");
+ CheckNoIssues("abc", "abc");
+ CheckNoIssues("aBc", "abc");
+ CheckNoIssues("_aBc", "_abc");
+ CheckNoIssues("__aBc", "__abc");
+ CheckNoIssues("___aBc", "___abc");
+ CheckNoIssues("______aBc", "______abc");
+ CheckNoIssues("_a_Bc", "_abc");
+ CheckIssues("_a__Bc", "_a__Bc");
+ CheckNoIssues("_aBc_", "_abc");
+ CheckIssues("_aBc__", "_aBc__");
+ CheckNoIssues("aBc_", "abc");
+ CheckIssues("aBc__", "aBc__");
+ CheckIssues("aBc___", "aBc___");
+ CheckNoIssues("aB_c", "abc");
+ CheckIssues("aB__c", "aB__c");
+ CheckNoIssues("a_B_c", "abc");
+ CheckNoIssues("_a_B_c", "_abc");
+ CheckNoIssues("__a_B_c", "__abc");
+ CheckNoIssues("a_B_c_", "abc");
+ CheckNoIssues("a_B_c_d", "abcd");
+ CheckNoIssues("a_B_c_d_", "abcd");
+ CheckNoIssues("a_B_c_d_e", "abcde");
+ CheckNoIssues("_a_B_c_d_e", "_abcde");
+ CheckIssues("a_B_c_d_e_", "a_B_c_d_e_");
+ CheckIssues("a_B_c_d_e_f", "a_B_c_d_e_f");
+ CheckIssues("_a_B_c_d_e_f", "_a_B_c_d_e_f");
+ }
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/core/ut/yql_library_compiler_ut.cpp b/ydb/library/yql/core/ut/yql_library_compiler_ut.cpp
new file mode 100644
index 0000000000..68dcd76693
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_library_compiler_ut.cpp
@@ -0,0 +1,223 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+
+#include "yql_library_compiler.h"
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TLibraryCompilerTests) {
+
+ static const char* alias = "/lib/ut.yql";
+
+ static bool CompileAndLink(const THashMap<TString, TString>& libs, TExprContext& ctx) {
+ THashMap<TString, TLibraryCohesion> compiled;
+ for (const auto& lib : libs)
+ if (!CompileLibrary(alias, lib.second, ctx, compiled[lib.first]))
+ return false;
+
+ return LinkLibraries(compiled, ctx, ctx);
+ }
+
+ Y_UNIT_TEST(OnlyExportsTest) {
+ const auto s = "(\n"
+ "(let X 'Y)\n"
+ "(let ex '42)\n"
+ "(export ex)\n"
+ "(export X)\n"
+ ")\n";
+
+ TExprContext ctx;
+ TLibraryCohesion cohesion;
+ UNIT_ASSERT(CompileLibrary(alias, s, ctx, cohesion));
+ UNIT_ASSERT_VALUES_EQUAL(2, cohesion.Exports.Symbols().size());
+ UNIT_ASSERT(cohesion.Imports.empty());
+ }
+
+ Y_UNIT_TEST(ExportAndImportsTest) {
+ const auto s = "(\n"
+ "(import math_module '""/lib/yql/math.yql"")\n"
+ "(let mySqr2 (bind math_module 'sqr2))\n"
+ "(let mySqr3 (bind math_module 'sqr3))\n"
+ "(let ex '42)\n"
+ "(export ex)\n"
+ ")\n";
+
+ TExprContext ctx;
+ TLibraryCohesion cohesion;
+ UNIT_ASSERT(CompileLibrary(alias, s, ctx, cohesion));
+ UNIT_ASSERT_VALUES_EQUAL(1, cohesion.Exports.Symbols().size());
+ UNIT_ASSERT_VALUES_EQUAL(2, cohesion.Imports.size());
+ }
+
+ Y_UNIT_TEST(TestImportSelf) {
+ const auto xxx = "(\n"
+ "(import math_module '""/lib/yql/xxx.yql"")\n"
+ "(let myXxx (bind math_module 'xxx))\n"
+ "(let sqr (lambda '(x) (Apply myXxx x)))\n"
+ "(export sqr)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/xxx.yql", xxx}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(!CompileAndLink(libs, ctx));
+ UNIT_ASSERT_VALUES_EQUAL("/lib/ut.yql:3:13: Error: Library '/lib/yql/xxx.yql' tries to import itself.\n", ctx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(TestMissedModule) {
+ const auto aaa = "(\n"
+ "(import math_module '""/lib/yql/xxx.yql"")\n"
+ "(let myXxx (bind math_module 'xxx))\n"
+ "(let sqr (lambda '(x) (Apply x myXxx)))\n"
+ "(export sqr)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/aaa.yql", aaa}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(!CompileAndLink(libs, ctx));
+ UNIT_ASSERT_VALUES_EQUAL("/lib/ut.yql:3:13: Error: Library '/lib/yql/aaa.yql' has unresolved dependency from '/lib/yql/xxx.yql'.\n", ctx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(TestUnresolvedSymbol) {
+ const auto one = "(\n"
+ "(import math_module '""/lib/yql/two.yql"")\n"
+ "(let myTwo (bind math_module 'zzz))\n"
+ "(let one (lambda '(x) (Apply myTwo x)))\n"
+ "(export one)\n"
+ ")\n";
+
+ const auto two = "(\n"
+ "(let two (lambda '(x) (+ x x)))\n"
+ "(export two)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/one.yql", one},
+ {"/lib/yql/two.yql", two}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(!CompileAndLink(libs, ctx));
+ UNIT_ASSERT_VALUES_EQUAL("/lib/ut.yql:3:13: Error: Library '/lib/yql/one.yql' has unresolved symbol 'zzz' from '/lib/yql/two.yql'.\n", ctx.IssueManager.GetIssues().ToString());
+ }
+
+ Y_UNIT_TEST(TestCrossReference) {
+ const auto one = "(\n"
+ "(import math_module '""/lib/yql/two.yql"")\n"
+ "(let myTwo (bind math_module 'two))\n"
+ "(let one (lambda '(x) (Apply myTwo x)))\n"
+ "(export one)\n"
+ ")\n";
+
+ const auto two = "(\n"
+ "(import math_module '""/lib/yql/one.yql"")\n"
+ "(let myOne (bind math_module 'one))\n"
+ "(let two (lambda '(x) (Apply myOne x)))\n"
+ "(export two)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/one.yql", one},
+ {"/lib/yql/two.yql", two}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(!CompileAndLink(libs, ctx));
+ UNIT_ASSERT(ctx.IssueManager.GetIssues().ToString().Contains("Cross reference detected"));
+ }
+
+ Y_UNIT_TEST(TestCrorssDependencyWithoutCrossReference) {
+ const auto one = "(\n"
+ "(import math_module '""/lib/yql/two.yql"")\n"
+ "(let myTwo (bind math_module 'two))\n"
+ "(let one (lambda '(x) (Apply myTwo x)))\n"
+ "(export one)\n"
+ ")\n";
+
+ const auto two = "(\n"
+ "(import math_module '""/lib/yql/one.yql"")\n"
+ "(let myOne (bind math_module 'one))\n"
+ "(let two (lambda '(x) (+ x x)))\n"
+ "(export two)\n"
+ "(let exp (lambda '(x) (Apply myOne x)))\n"
+ "(export exp)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/one.yql", one},
+ {"/lib/yql/two.yql", two}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(CompileAndLink(libs, ctx));
+ }
+
+ Y_UNIT_TEST(TestCircleReference) {
+ const auto one = "(\n"
+ "(import math_module '""/lib/yql/two.yql"")\n"
+ "(let myTwo (bind math_module 'two))\n"
+ "(let one (lambda '(x) (Apply myTwo x)))\n"
+ "(export one)\n"
+ ")\n";
+
+ const auto two = "(\n"
+ "(import math_module '""/lib/yql/xxx.yql"")\n"
+ "(let myXxx (bind math_module 'xxx))\n"
+ "(let two (lambda '(x) (Apply myXxx x)))\n"
+ "(export two)\n"
+ ")\n";
+
+ const auto xxx = "(\n"
+ "(import math_module '""/lib/yql/one.yql"")\n"
+ "(let myOne (bind math_module 'one))\n"
+ "(let xxx (lambda '(x) (Apply myOne x)))\n"
+ "(export xxx)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/one.yql", one},
+ {"/lib/yql/two.yql", two},
+ {"/lib/yql/xxx.yql", xxx}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(!CompileAndLink(libs, ctx));
+ UNIT_ASSERT(ctx.IssueManager.GetIssues().ToString().Contains("Cross reference detected"));
+ }
+
+ Y_UNIT_TEST(TestForwarding) {
+ const auto one = "(\n"
+ "(let one '1)\n"
+ "(export one)\n"
+ ")\n";
+
+ const auto two = "(\n"
+ "(import math_module '""/lib/yql/one.yql"")\n"
+ "(let myOne (bind math_module 'one))\n"
+ "(export myOne)\n"
+ ")\n";
+
+ const auto xxx = "(\n"
+ "(import math_module '""/lib/yql/two.yql"")\n"
+ "(let xxx (bind math_module 'myOne))\n"
+ "(export xxx)\n"
+ ")\n";
+
+ const THashMap<TString, TString> libs = {
+ {"/lib/yql/one.yql", one},
+ {"/lib/yql/two.yql", two},
+ {"/lib/yql/xxx.yql", xxx}
+ };
+
+ TExprContext ctx;
+ UNIT_ASSERT(CompileAndLink(libs, ctx));
+ }
+}
+
+}
diff --git a/ydb/library/yql/core/ut/yql_udf_index_ut.cpp b/ydb/library/yql/core/ut/yql_udf_index_ut.cpp
new file mode 100644
index 0000000000..c0c6fc9397
--- /dev/null
+++ b/ydb/library/yql/core/ut/yql_udf_index_ut.cpp
@@ -0,0 +1,368 @@
+#include "yql_udf_index.h"
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYql;
+namespace {
+class TResourceBuilder {
+ TIntrusivePtr<TResourceInfo> Resource_ = new TResourceInfo();
+
+public:
+ explicit TResourceBuilder(const TDownloadLink& link) {
+ Resource_->Link = link;
+ }
+
+ TResourceBuilder& AddFunction(const TFunctionInfo& f) {
+ auto module = TString(NKikimr::NMiniKQL::ModuleName(TStringBuf(f.Name)));
+ Resource_->Modules.insert(module);
+ Resource_->SetFunctions({ f });
+ return *this;
+ }
+
+ TResourceInfo::TPtr Build() const {
+ return Resource_;
+ }
+};
+
+TFunctionInfo BuildFunctionInfo(const TString& name, int argCount) {
+ TFunctionInfo result;
+ result.Name = name;
+ result.ArgCount = argCount;
+ return result;
+}
+
+void EnsureFunctionsEqual(const TFunctionInfo& f1, const TFunctionInfo& f2) {
+ UNIT_ASSERT_VALUES_EQUAL(f1.Name, f2.Name);
+ UNIT_ASSERT_VALUES_EQUAL(f1.ArgCount, f2.ArgCount);
+}
+
+void EnsureLinksEqual(const TDownloadLink& link1, const TDownloadLink& link2) {
+ UNIT_ASSERT_VALUES_EQUAL(link1.IsUrl, link2.IsUrl);
+ UNIT_ASSERT_VALUES_EQUAL(link1.Path, link2.Path);
+}
+
+void EnsureContainsFunction(TUdfIndex::TPtr index, TString module, const TFunctionInfo& f) {
+ TFunctionInfo existingFunc;
+ UNIT_ASSERT(index->FindFunction(module, f.Name, existingFunc));
+ EnsureFunctionsEqual(f, existingFunc);
+}
+}
+
+Y_UNIT_TEST_SUITE(TUdfIndexTests) {
+ Y_UNIT_TEST(Empty) {
+ auto index1 = MakeIntrusive<TUdfIndex>();
+
+ UNIT_ASSERT(!index1->ContainsModule("M1"));
+ UNIT_ASSERT(index1->FindResourceByModule("M1") == nullptr);
+ TFunctionInfo f1;
+ UNIT_ASSERT(!index1->FindFunction("M1", "M1.F1", f1));
+
+ auto index2 = index1->Clone();
+ UNIT_ASSERT(!index2->ContainsModule("M1"));
+ UNIT_ASSERT(index2->FindResourceByModule("M1") == nullptr);
+ UNIT_ASSERT(!index2->FindFunction("M1", "M1.F1", f1));
+ }
+
+ Y_UNIT_TEST(SingleModuleAndFunction) {
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func1 = BuildFunctionInfo("M1.F1", 1);
+ auto link1 = TDownloadLink::File("file1");
+
+ TResourceBuilder b(link1);
+ b.AddFunction(func1);
+
+ index1->RegisterResource(b.Build(), TUdfIndex::EOverrideMode::RaiseError);
+ UNIT_ASSERT(index1->ContainsModule("M1"));
+ UNIT_ASSERT(!index1->ContainsModule("M2"));
+
+ UNIT_ASSERT(index1->FindResourceByModule("M2") == nullptr);
+ auto resource1 = index1->FindResourceByModule("M1");
+ UNIT_ASSERT(resource1 != nullptr);
+ EnsureLinksEqual(resource1->Link, link1);
+
+ TFunctionInfo f1;
+ UNIT_ASSERT(!index1->FindFunction("M2", "M2.F1", f1));
+
+ UNIT_ASSERT(index1->FindFunction("M1", "M1.F1", f1));
+ EnsureFunctionsEqual(f1, func1);
+
+ // ensure both indexes contain the same info
+ auto index2 = index1->Clone();
+
+ UNIT_ASSERT(index1->ContainsModule("M1"));
+ UNIT_ASSERT(index2->ContainsModule("M1"));
+
+ TFunctionInfo f2;
+ UNIT_ASSERT(index2->FindFunction("M1", "M1.F1", f2));
+ EnsureFunctionsEqual(f1, f2);
+
+ auto resource2 = index2->FindResourceByModule("M1");
+ UNIT_ASSERT(resource2 != nullptr);
+ }
+
+ Y_UNIT_TEST(SeveralModulesAndFunctions) {
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func11 = BuildFunctionInfo("M1.F1", 1);
+ auto func12 = BuildFunctionInfo("M1.F2", 2);
+ auto func13 = BuildFunctionInfo("M2.F1", 3);
+
+ auto link1 = TDownloadLink::File("file1");
+ auto resource1 = TResourceBuilder(link1)
+ .AddFunction(func11)
+ .AddFunction(func12)
+ .AddFunction(func13)
+ .Build();
+
+ auto func21 = BuildFunctionInfo("M3.F1", 4);
+ auto func22 = BuildFunctionInfo("M4.F2", 5);
+
+ auto link2 = TDownloadLink::Url("url1");
+ auto resource2 = TResourceBuilder(link2)
+ .AddFunction(func21)
+ .AddFunction(func22)
+ .Build();
+
+ index1->RegisterResource(resource1, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource2, TUdfIndex::EOverrideMode::RaiseError);
+
+ // check resources by module
+ UNIT_ASSERT(index1->FindResourceByModule("M5") == nullptr);
+ auto r11 = index1->FindResourceByModule("M1");
+ auto r12 = index1->FindResourceByModule("M2");
+ UNIT_ASSERT(r11 != nullptr && r12 != nullptr);
+ EnsureLinksEqual(r11->Link, link1);
+ EnsureLinksEqual(r12->Link, link1);
+
+ auto r21 = index1->FindResourceByModule("M3");
+ auto r22 = index1->FindResourceByModule("M4");
+ UNIT_ASSERT(r21 != nullptr && r22 != nullptr);
+ EnsureLinksEqual(r21->Link, link2);
+ EnsureLinksEqual(r22->Link, link2);
+
+ // check modules
+ UNIT_ASSERT(index1->ContainsModule("M1"));
+ UNIT_ASSERT(index1->ContainsModule("M2"));
+ UNIT_ASSERT(index1->ContainsModule("M3"));
+ UNIT_ASSERT(index1->ContainsModule("M4"));
+ UNIT_ASSERT(!index1->ContainsModule("M5"));
+
+ EnsureContainsFunction(index1, "M1", func11);
+ EnsureContainsFunction(index1, "M1", func12);
+ EnsureContainsFunction(index1, "M2", func13);
+ EnsureContainsFunction(index1, "M3", func21);
+ EnsureContainsFunction(index1, "M4", func22);
+
+ // works because M2 refers to the same resource
+ EnsureContainsFunction(index1, "M2", func11);
+
+ TFunctionInfo f;
+ // known func, but non-existent module
+ UNIT_ASSERT(!index1->FindFunction("M5", "M1.F1", f));
+ UNIT_ASSERT(!index1->FindFunction("M2", "M3.F1", f));
+ }
+
+ Y_UNIT_TEST(ConflictRaiseError) {
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func11 = BuildFunctionInfo("M1.F1", 1);
+ auto func12 = BuildFunctionInfo("M1.F2", 2);
+ auto func13 = BuildFunctionInfo("M2.F1", 3);
+
+ auto link1 = TDownloadLink::File("file1");
+ auto resource1 = TResourceBuilder(link1)
+ .AddFunction(func11)
+ .AddFunction(func12)
+ .AddFunction(func13)
+ .Build();
+
+ auto func21 = BuildFunctionInfo("M3.F1", 4);
+ auto func22 = BuildFunctionInfo("M2.F1", 5);
+
+ auto link2 = TDownloadLink::Url("url1");
+ auto resource2 = TResourceBuilder(link2)
+ .AddFunction(func21)
+ .AddFunction(func22)
+ .Build();
+
+ index1->RegisterResource(resource1, TUdfIndex::EOverrideMode::RaiseError);
+ UNIT_ASSERT_EXCEPTION_CONTAINS(index1->RegisterResource(resource2, TUdfIndex::EOverrideMode::RaiseError), std::exception, "Conflict during resource url1 registration");
+
+ // ensure state untouched
+ UNIT_ASSERT(index1->FindResourceByModule("M3") == nullptr);
+ auto r1 = index1->FindResourceByModule("M1");
+ auto r2 = index1->FindResourceByModule("M2");
+ UNIT_ASSERT(r1 != nullptr && r2 != nullptr);
+ EnsureLinksEqual(r1->Link, link1);
+ EnsureLinksEqual(r2->Link, link1);
+
+ EnsureContainsFunction(index1, "M1", func11);
+ EnsureContainsFunction(index1, "M1", func12);
+ EnsureContainsFunction(index1, "M2", func13);
+
+ TFunctionInfo f;
+ UNIT_ASSERT(!index1->FindFunction("M3", "M3.F1", f));
+ }
+
+ Y_UNIT_TEST(ConflictPreserveExisting) {
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func11 = BuildFunctionInfo("M1.F1", 1);
+ auto func12 = BuildFunctionInfo("M1.F2", 2);
+ auto func13 = BuildFunctionInfo("M2.F1", 3);
+
+ auto link1 = TDownloadLink::File("file1");
+ auto resource1 = TResourceBuilder(link1)
+ .AddFunction(func11)
+ .AddFunction(func12)
+ .AddFunction(func13)
+ .Build();
+
+ auto func21 = BuildFunctionInfo("M3.F1", 4);
+ auto func22 = BuildFunctionInfo("M2.F1", 5);
+
+ auto link2 = TDownloadLink::Url("url1");
+ auto resource2 = TResourceBuilder(link2)
+ .AddFunction(func21)
+ .AddFunction(func22)
+ .Build();
+
+ index1->RegisterResource(resource1, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource2, TUdfIndex::EOverrideMode::PreserveExisting);
+
+ // ensure state untouched
+ UNIT_ASSERT(index1->FindResourceByModule("M3") == nullptr);
+ auto r1 = index1->FindResourceByModule("M1");
+ auto r2 = index1->FindResourceByModule("M2");
+ UNIT_ASSERT(r1 != nullptr && r2 != nullptr);
+ EnsureLinksEqual(r1->Link, link1);
+ EnsureLinksEqual(r2->Link, link1);
+
+ EnsureContainsFunction(index1, "M1", func11);
+ EnsureContainsFunction(index1, "M1", func12);
+ EnsureContainsFunction(index1, "M2", func13);
+
+ TFunctionInfo f;
+ UNIT_ASSERT(!index1->FindFunction("M3", "M3.F1", f));
+ }
+
+ Y_UNIT_TEST(ConflictReplace1WithNew) {
+ // single resource will be replaced
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func11 = BuildFunctionInfo("M1.F1", 1);
+ auto func12 = BuildFunctionInfo("M1.F2", 2);
+ auto func13 = BuildFunctionInfo("M2.F1", 3);
+
+ auto link1 = TDownloadLink::File("file1");
+ auto resource1 = TResourceBuilder(link1)
+ .AddFunction(func11)
+ .AddFunction(func12)
+ .AddFunction(func13)
+ .Build();
+
+ auto func21 = BuildFunctionInfo("M3.F3", 4);
+ auto func22 = BuildFunctionInfo("M4.F4", 5);
+
+ auto link2 = TDownloadLink::Url("url1");
+ auto resource2 = TResourceBuilder(link2)
+ .AddFunction(func21)
+ .AddFunction(func22)
+ .Build();
+
+
+ auto func31 = BuildFunctionInfo("M5.F5", 6);
+ // conflict by module name
+ auto func32 = BuildFunctionInfo("M1.F7", 7);
+
+ auto link3 = TDownloadLink::Url("url3");
+ auto resource3 = TResourceBuilder(link3)
+ .AddFunction(func31)
+ .AddFunction(func32)
+ .Build();
+
+ index1->RegisterResource(resource1, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource2, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource3, TUdfIndex::EOverrideMode::ReplaceWithNew);
+
+ UNIT_ASSERT(index1->FindResourceByModule("M2") == nullptr);
+ auto r1 = index1->FindResourceByModule("M3");
+ UNIT_ASSERT(r1 != nullptr);
+ EnsureLinksEqual(r1->Link, link2);
+
+ auto r2 = index1->FindResourceByModule("M1");
+ UNIT_ASSERT(r2 != nullptr);
+ EnsureLinksEqual(r2->Link, link3);
+
+ // ensure untouched
+ EnsureContainsFunction(index1, "M3", func21);
+ EnsureContainsFunction(index1, "M4", func22);
+
+ EnsureContainsFunction(index1, "M5", func31);
+ EnsureContainsFunction(index1, "M1", func32);
+
+ // not here anymore
+ TFunctionInfo f;
+ UNIT_ASSERT(!index1->FindFunction("M1", "M1.F1", f));
+ UNIT_ASSERT(!index1->FindFunction("M1", "M1.F2", f));
+ UNIT_ASSERT(!index1->FindFunction("M2", "M2.F1", f));
+ }
+
+ Y_UNIT_TEST(ConflictReplace2WithNew) {
+ // both resources will be replaced
+ auto index1 = MakeIntrusive<TUdfIndex>();
+ auto func11 = BuildFunctionInfo("M1.F1", 1);
+ auto func12 = BuildFunctionInfo("M1.F2", 2);
+ auto func13 = BuildFunctionInfo("M2.F1", 3);
+
+ auto link1 = TDownloadLink::File("file1");
+ auto resource1 = TResourceBuilder(link1)
+ .AddFunction(func11)
+ .AddFunction(func12)
+ .AddFunction(func13)
+ .Build();
+
+ auto func21 = BuildFunctionInfo("M3.F3", 4);
+ auto func22 = BuildFunctionInfo("M4.F4", 5);
+
+ auto link2 = TDownloadLink::Url("url1");
+ auto resource2 = TResourceBuilder(link2)
+ .AddFunction(func21)
+ .AddFunction(func22)
+ .Build();
+
+
+ // conflict by module name
+ auto func31 = BuildFunctionInfo("M3.F7", 6);
+ // conflict by func name
+ auto func32 = BuildFunctionInfo("M1.F1", 7);
+
+ auto link3 = TDownloadLink::Url("url3");
+ auto resource3 = TResourceBuilder(link3)
+ .AddFunction(func31)
+ .AddFunction(func32)
+ .Build();
+
+ index1->RegisterResource(resource1, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource2, TUdfIndex::EOverrideMode::RaiseError);
+ index1->RegisterResource(resource3, TUdfIndex::EOverrideMode::ReplaceWithNew);
+
+ UNIT_ASSERT(index1->FindResourceByModule("M2") == nullptr);
+ UNIT_ASSERT(index1->FindResourceByModule("M4") == nullptr);
+ auto r1 = index1->FindResourceByModule("M3");
+ UNIT_ASSERT(r1 != nullptr);
+ EnsureLinksEqual(r1->Link, link3);
+
+ auto r2 = index1->FindResourceByModule("M1");
+ UNIT_ASSERT(r2 != nullptr);
+ EnsureLinksEqual(r2->Link, link3);
+
+ // ensure untouched
+ EnsureContainsFunction(index1, "M3", func31);
+ EnsureContainsFunction(index1, "M1", func32);
+
+ // not here anymore
+ TFunctionInfo f;
+ UNIT_ASSERT(!index1->FindFunction("M1", "M1.F2", f));
+ UNIT_ASSERT(!index1->FindFunction("M2", "M2.F1", f));
+
+ UNIT_ASSERT(!index1->FindFunction("M3", "M3.F3", f));
+ UNIT_ASSERT(!index1->FindFunction("M4", "M4.F4", f));
+ }
+}
diff --git a/ydb/library/yql/core/ut_common/ya.make b/ydb/library/yql/core/ut_common/ya.make
new file mode 100644
index 0000000000..bcc9000ebd
--- /dev/null
+++ b/ydb/library/yql/core/ut_common/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+SRCS(
+ yql_ut_common.cpp
+ yql_ut_common.h
+)
+
+PEERDIR(
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/dq/proto
+ ydb/library/yql/dq/expr_nodes
+)
+
+
+YQL_LAST_ABI_VERSION()
+
+
+END()
diff --git a/ydb/library/yql/core/ut_common/yql_ut_common.cpp b/ydb/library/yql/core/ut_common/yql_ut_common.cpp
new file mode 100644
index 0000000000..cef3f2723c
--- /dev/null
+++ b/ydb/library/yql/core/ut_common/yql_ut_common.cpp
@@ -0,0 +1,55 @@
+#include "yql_ut_common.h"
+
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <util/generic/guid.h>
+#include <util/system/user.h>
+#include <util/stream/file.h>
+
+namespace NYql {
+
+TTestTablesMapping::TTestTablesMapping()
+ : TmpInput()
+ , TmpInputAttr(TmpInput.Name() + ".attr")
+ , TmpOutput()
+ , TmpOutputAttr(TmpOutput.Name() + ".attr")
+{
+ {
+ TUnbufferedFileOutput tmpInput(TmpInput);
+ tmpInput << "{\"key\"=\"\";\"subkey\"=\"\";\"value\"=\"\"}" << Endl;
+ TUnbufferedFileOutput tmpInputAttr(TmpInputAttr);
+ tmpInputAttr << "{\"_yql_row_spec\" = {\"Type\" = [\"StructType\";["
+ << "[\"key\";[\"DataType\";\"String\"]];"
+ << "[\"subkey\";[\"DataType\";\"String\"]];"
+ << "[\"value\";[\"DataType\";\"String\"]]"
+ << "]]}}" << Endl;
+ }
+ insert(std::make_pair("yt.plato.Input", TmpInput.Name()));
+
+ {
+ TUnbufferedFileOutput tmpOutput(TmpOutput);
+ tmpOutput << "{\"key\"=\"\";\"subkey\"=\"\";\"value\"=\"\"}" << Endl;
+ TUnbufferedFileOutput tmpOutputAttr(TmpOutputAttr);
+ tmpOutputAttr << "{\"_yql_row_spec\" = {\"Type\" = [\"StructType\";["
+ << "[\"key\";[\"DataType\";\"String\"]];"
+ << "[\"subkey\";[\"DataType\";\"String\"]];"
+ << "[\"value\";[\"DataType\";\"String\"]]"
+ << "]]}}" << Endl;
+ }
+ insert(std::make_pair("yt.plato.Output", TmpOutput.Name()));
+}
+
+void InitializeYtGateway(IYtGateway::TPtr gateway, TYtState::TPtr ytState) {
+ ytState->SessionId = CreateGuidAsString();
+ gateway->OpenSession(
+ IYtGateway::TOpenSessionOptions(ytState->SessionId)
+ .UserName(GetUsername())
+ .ProgressWriter(&NullProgressWriter)
+ .OperationOptions(TYqlOperationOptions())
+ .RandomProvider(CreateDeterministicRandomProvider(1))
+ .TimeProvider(CreateDeterministicTimeProvider(10000000))
+ );
+}
+
+}
diff --git a/ydb/library/yql/core/ut_common/yql_ut_common.h b/ydb/library/yql/core/ut_common/yql_ut_common.h
new file mode 100644
index 0000000000..fd986dc4e0
--- /dev/null
+++ b/ydb/library/yql/core/ut_common/yql_ut_common.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+
+#include <util/system/tempfile.h>
+
+namespace NYql {
+
+struct TTestTablesMapping: public THashMap<TString, TString> {
+ TTempFileHandle TmpInput;
+ TTempFileHandle TmpInputAttr;
+ TTempFileHandle TmpOutput;
+ TTempFileHandle TmpOutputAttr;
+
+ TTestTablesMapping();
+};
+
+void InitializeYtGateway(IYtGateway::TPtr gateway, TYtState::TPtr ytState);
+
+}
diff --git a/ydb/library/yql/providers/stat/expr_nodes/ya.make b/ydb/library/yql/providers/stat/expr_nodes/ya.make
new file mode 100644
index 0000000000..b09f63e4b7
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/ya.make
@@ -0,0 +1,52 @@
+LIBRARY()
+
+PEERDIR(
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/providers/common/provider
+)
+
+SRCS(
+ yql_stat_expr_nodes.cpp
+)
+
+SRCDIR(
+ ydb/library/yql/core/expr_nodes_gen
+)
+
+IF(EXPORT_CMAKE)
+ RUN_PYTHON3(
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ yql_expr_nodes_gen.jnj
+ yql_stat_expr_nodes.json
+ yql_stat_expr_nodes.gen.h
+ yql_stat_expr_nodes.decl.inl.h
+ yql_stat_expr_nodes.defs.inl.h
+ IN yql_expr_nodes_gen.jnj
+ IN yql_stat_expr_nodes.json
+ OUT yql_stat_expr_nodes.gen.h
+ OUT yql_stat_expr_nodes.decl.inl.h
+ OUT yql_stat_expr_nodes.defs.inl.h
+ OUTPUT_INCLUDES
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h
+ ${ARCADIA_ROOT}/util/generic/hash_set.h
+ )
+ELSE()
+ RUN_PROGRAM(
+ ydb/library/yql/core/expr_nodes_gen/gen
+ yql_expr_nodes_gen.jnj
+ yql_stat_expr_nodes.json
+ yql_stat_expr_nodes.gen.h
+ yql_stat_expr_nodes.decl.inl.h
+ yql_stat_expr_nodes.defs.inl.h
+ IN yql_expr_nodes_gen.jnj
+ IN yql_stat_expr_nodes.json
+ OUT yql_stat_expr_nodes.gen.h
+ OUT yql_stat_expr_nodes.decl.inl.h
+ OUT yql_stat_expr_nodes.defs.inl.h
+ OUTPUT_INCLUDES
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h
+ ${ARCADIA_ROOT}/util/generic/hash_set.h
+ )
+ENDIF()
+
+END()
diff --git a/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
new file mode 100644
index 0000000000..7520ee7682
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.cpp
@@ -0,0 +1 @@
+#include "yql_stat_expr_nodes.h"
diff --git a/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h
new file mode 100644
index 0000000000..5934d23c34
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.gen.h>
+
+
+namespace NYql {
+namespace NNodes {
+
+#include <ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.decl.inl.h>
+
+class TStatDSource: public NGenerated::TStatDSourceStub<TExprBase, TCallable, TCoAtom> {
+public:
+ explicit TStatDSource(const TExprNode* node)
+ : TStatDSourceStub {node}
+ {
+ }
+
+ explicit TStatDSource(const TExprNode::TPtr& node)
+ : TStatDSourceStub {node}
+ {
+ }
+
+ static bool Match(const TExprNode* node) {
+ if (!TStatDSourceStub::Match(node)) {
+ return false;
+ }
+
+ if (node->Child(0)->Content() != StatProviderName) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+
+class TStatDSink: public NGenerated::TStatDSinkStub<TExprBase, TCallable, TCoAtom> {
+public:
+ explicit TStatDSink(const TExprNode* node)
+ : TStatDSinkStub {node}
+ {
+ }
+
+ explicit TStatDSink(const TExprNode::TPtr& node)
+ : TStatDSinkStub {node}
+ {
+ }
+
+ static bool Match(const TExprNode* node) {
+ if (!TStatDSinkStub::Match(node)) {
+ return false;
+ }
+
+ if (node->Child(0)->Content() != StatProviderName) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+#include <ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.defs.inl.h>
+
+} // namespace NNodes
+} // namespace NYql
diff --git a/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
new file mode 100644
index 0000000000..2b52d95b78
--- /dev/null
+++ b/ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.json
@@ -0,0 +1,111 @@
+{
+ "NodeRootType": "TExprBase",
+ "NodeBuilderBase": "TNodeBuilderBase",
+ "ListBuilderBase": "TListBuilderBase",
+ "FreeArgCallableBase": "TFreeArgCallable",
+ "FreeArgBuilderBase": "TFreeArgCallableBuilderBase",
+ "Nodes": [
+ {
+ "Name": "TStatDSource",
+ "Base": "TCallable",
+ "Definition": "Custom",
+ "Builder": {"Generate": "None"},
+ "Match": {"Type": "Callable", "Name": "DataSource"},
+ "Children": [
+ {"Index": 0, "Name": "Category", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TStatDSink",
+ "Base": "TCallable",
+ "Definition": "Custom",
+ "Builder": {"Generate": "None"},
+ "Match": {"Type": "Callable", "Name": "DataSink"},
+ "Children": [
+ {"Index": 0, "Name": "Category", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TStatRead",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "Read!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TStatDSource"}
+ ]
+ },
+ {
+ "Name": "TStatWrite",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "Write!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSink", "Type": "TStatDSink"}
+ ]
+ },
+ {
+ "Name": "TStatNamedSettingsBase",
+ "VarArgBase": "TCoNameValueTuple",
+ "Builder": {"Generate": "None"}
+ },
+ {
+ "Name": "TStatFields",
+ "Base": "TStatNamedSettingsBase",
+ "Match": {"Type": "Callable", "Name": "StatFields"}
+ },
+ {
+ "Name": "TStatMeta",
+ "Base": "TStatNamedSettingsBase",
+ "Match": {"Type": "Callable", "Name": "StatMeta"}
+ },
+ {
+ "Name": "TStatTable",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "StatTable"},
+ "Children": [
+ {"Index": 0, "Name": "Name", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Scale", "Type": "TCoAtom"},
+ {"Index": 2, "Name": "Cluster", "Type": "TCoAtom"},
+ {"Index": 3, "Name": "Fields", "Type": "TExprBase"},
+ {"Index": 4, "Name": "Meta", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TStatReadTableScheme",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "StatReadTableScheme!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TStatDSource"},
+ {"Index": 2, "Name": "Table", "Type": "TStatTable"},
+ {"Index": 3, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TStatWriteTable",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "StatWriteTable!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSink", "Type": "TStatDSink"},
+ {"Index": 2, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 3, "Name": "Table", "Type": "TStatTable"},
+ {"Index": 4, "Name": "ReplaceMask", "Type": "TCoAtomList"},
+ {"Index": 5, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TStatPublish",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "StatPublish!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSink", "Type": "TStatDSink"},
+ {"Index": 2, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 3, "Name": "Table", "Type": "TStatTable"}
+ ]
+ }
+ ]
+}
diff --git a/ydb/library/yql/providers/stat/uploader/ya.make b/ydb/library/yql/providers/stat/uploader/ya.make
new file mode 100644
index 0000000000..c7675a7582
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/ya.make
@@ -0,0 +1,11 @@
+LIBRARY()
+
+PEERDIR(
+ library/cpp/threading/future
+)
+
+SRCS(
+ yql_stat_uploader.cpp
+)
+
+END()
diff --git a/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp b/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
new file mode 100644
index 0000000000..9ec9f55648
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.cpp
@@ -0,0 +1 @@
+#include "yql_stat_uploader.h"
diff --git a/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h b/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h
new file mode 100644
index 0000000000..d84855f820
--- /dev/null
+++ b/ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+
+namespace NYql {
+
+struct TStatUploadOptions {
+ TString ProviderName;
+ TString SessionId;
+ ui32 PublicId;
+
+ TString Cluster;
+ TString Table;
+ TString Scale;
+ TVector<TString> ReplaceMask;
+
+ TString YtServer;
+ TString YtTable;
+ TString YtTx;
+ TString YtToken;
+ TMaybe<TString> YtPool;
+};
+
+
+class IStatUploader : public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<IStatUploader>;
+
+ virtual NThreading::TFuture<void> Upload(TStatUploadOptions&& options) = 0;
+
+ virtual ~IStatUploader() = default;
+};
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/stat/ya.make b/ydb/library/yql/providers/stat/ya.make
new file mode 100644
index 0000000000..234f935d74
--- /dev/null
+++ b/ydb/library/yql/providers/stat/ya.make
@@ -0,0 +1,5 @@
+
+RECURSE(
+ uploader
+ expr_nodes
+)
diff --git a/ydb/library/yql/providers/ya.make b/ydb/library/yql/providers/ya.make
index 5c17a8919d..f87df1712c 100644
--- a/ydb/library/yql/providers/ya.make
+++ b/ydb/library/yql/providers/ya.make
@@ -12,3 +12,11 @@ RECURSE(
generic
)
+
+# TODO(max42): Recurse unconditionally as a final step of YT-19210.
+IF (NOT EXPORT_CMAKE)
+ RECURSE(
+ yt
+ stat
+ )
+ENDIF()
diff --git a/ydb/library/yql/providers/yt/codec/codegen/ut/ya.make b/ydb/library/yql/providers/yt/codec/codegen/ut/ya.make
new file mode 100644
index 0000000000..5e7fb29384
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/ut/ya.make
@@ -0,0 +1,23 @@
+UNITTEST_FOR(ydb/library/yql/providers/yt/codec/codegen)
+
+SRCS(
+ yt_codec_cg_ut.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/sql
+ ydb/library/yql/sql/pg_dummy
+ ydb/library/yql/providers/yt/codec
+)
+
+YQL_LAST_ABI_VERSION()
+
+IF (MKQL_DISABLE_CODEGEN)
+ CFLAGS(
+ -DMKQL_DISABLE_CODEGEN
+ )
+ENDIF()
+
+END()
diff --git a/ydb/library/yql/providers/yt/codec/codegen/ut/yt_codec_cg_ut.cpp b/ydb/library/yql/providers/yt/codec/codegen/ut/yt_codec_cg_ut.cpp
new file mode 100644
index 0000000000..4aab203bcb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/ut/yt_codec_cg_ut.cpp
@@ -0,0 +1,893 @@
+#ifndef MKQL_DISABLE_CODEGEN
+#include "yt_codec_cg.h"
+#include <ydb/library/yql/providers/common/codec/yql_codec_buf.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/minikql/codegen/codegen.h>
+
+#include <llvm/IR/Module.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+namespace NYql {
+
+using namespace NCommon;
+using namespace NCodegen;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace std::string_view_literals;
+
+class TTestWriter : public IBlockWriter {
+public:
+ TTestWriter()
+ {
+ Buffer_.ReserveAndResize(1024);
+ }
+
+ void SetRecordBoundaryCallback(std::function<void()> /*callback*/) override {
+ }
+
+ std::pair<char*, char*> NextEmptyBlock() override {
+ if (FirstBuffer_) {
+ FirstBuffer_ = false;
+ auto ptr = Buffer_.Detach();
+ return {ptr, ptr + Buffer_.capacity()};
+ } else {
+ return {nullptr, nullptr};
+ }
+ }
+
+ void ReturnBlock(size_t avail, std::optional<size_t> /*lastRecordBoundary*/) override {
+ Buffer_.resize(avail);
+ }
+
+ void Finish() override {
+ }
+
+ TString Str() const {
+ return Buffer_;
+ }
+
+private:
+ TString Buffer_;
+ bool FirstBuffer_ = true;
+};
+
+struct TWriteSetup {
+ TWriteSetup(const char* funcName)
+ : Buf_(TestWriter_, nullptr)
+ {
+ Codegen_ = ICodegen::Make(ETarget::Native);
+ Codegen_->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ Func_ = Codegen_->GetModule().getFunction(funcName);
+ UNIT_ASSERT(Func_);
+ Codegen_->Verify();
+ YtCodecAddMappings(*Codegen_);
+ Codegen_->ExportSymbol(Func_);
+ Codegen_->Compile();
+ //Codegen_->GetModule().dump();
+ }
+
+ ICodegen::TPtr Codegen_;
+ llvm::Function* Func_;
+ TTestWriter TestWriter_;
+ TOutputBuf Buf_;
+};
+
+class TTestReader : public IBlockReader {
+public:
+ TTestReader(TStringBuf data)
+ : Data_(data)
+ {}
+
+ void SetDeadline(TInstant deadline) override {
+ Y_UNUSED(deadline);
+ }
+
+ std::pair<const char*, const char*> NextFilledBlock() override {
+ if (FirstBuffer_) {
+ FirstBuffer_ = false;
+ return std::make_pair(Data_.begin(), Data_.end());
+ } else {
+ return { nullptr, nullptr };
+ }
+ }
+
+ void ReturnBlock() override {
+ YQL_ENSURE(!FirstBuffer_);
+ }
+
+ bool Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex) override {
+ Y_UNUSED(rangeIndex);
+ Y_UNUSED(rowIndex);
+ return false;
+ }
+
+private:
+ TStringBuf Data_;
+ bool FirstBuffer_ = true;
+};
+
+struct TReadSetup {
+ TReadSetup(const char* funcName, TStringBuf readData)
+ : TestReader_(readData)
+ , Buf_(TestReader_, nullptr)
+ {
+ Codegen_ = ICodegen::Make(ETarget::Native);
+ Codegen_->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ Func_ = Codegen_->GetModule().getFunction(funcName);
+ UNIT_ASSERT(Func_);
+ Codegen_->Verify();
+ YtCodecAddMappings(*Codegen_);
+ Codegen_->ExportSymbol(Func_);
+ Codegen_->Compile();
+ //Codegen_->GetModule().dump();
+ }
+
+ ICodegen::TPtr Codegen_;
+ llvm::Function* Func_;
+ TTestReader TestReader_;
+ TInputBuf Buf_;
+};
+
+Y_UNIT_TEST_SUITE(TYtCodegenCodec) {
+ Y_UNIT_TEST(TestWriteJust) {
+ TWriteSetup setup("WriteJust");
+ typedef void(*TFunc)(TOutputBuf&);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\1")");
+ }
+
+ Y_UNIT_TEST(TestWriteNothing) {
+ TWriteSetup setup("WriteNothing");
+ typedef void(*TFunc)(TOutputBuf&);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\0")");
+ }
+
+ Y_UNIT_TEST(TestWriteBool) {
+ TWriteSetup setup("WriteBool");
+ typedef void(*TFunc)(TOutputBuf&, bool);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, true);
+ funcPtr(setup.Buf_, false);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\1\0")");
+ }
+
+ Y_UNIT_TEST(TestWrite8) {
+ TWriteSetup setup("Write8");
+ typedef void(*TFunc)(TOutputBuf&, ui8);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 3);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\3")");
+ }
+
+ Y_UNIT_TEST(TestWrite16) {
+ TWriteSetup setup("Write16");
+ typedef void(*TFunc)(TOutputBuf&, ui16);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 258);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\2\1")");
+ }
+
+ Y_UNIT_TEST(TestWrite32) {
+ TWriteSetup setup("Write32");
+ typedef void(*TFunc)(TOutputBuf&, ui32);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 258);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\2\1\0\0")");
+ }
+
+ Y_UNIT_TEST(TestWrite64) {
+ TWriteSetup setup("Write64");
+ typedef void(*TFunc)(TOutputBuf&, ui64);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 258);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("\2\1\0\0\0\0\0\0")");
+ }
+
+ Y_UNIT_TEST(TestWriteString) {
+ TWriteSetup setup("WriteString");
+ typedef void(*TFunc)(TOutputBuf&, const char*, ui32);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, "foo", 3);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), R"("foo")");
+ }
+
+ Y_UNIT_TEST(TestWriteDataRowSmall) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto boolType = TDataType::Create(NUdf::TDataType<bool>::Id, env);
+ auto int8Type = TDataType::Create(NUdf::TDataType<i8>::Id, env);
+ auto uint8Type = TDataType::Create(NUdf::TDataType<ui8>::Id, env);
+ auto int16Type = TDataType::Create(NUdf::TDataType<i16>::Id, env);
+ auto uint16Type = TDataType::Create(NUdf::TDataType<ui16>::Id, env);
+ auto int32Type = TDataType::Create(NUdf::TDataType<i32>::Id, env);
+ auto uint32Type = TDataType::Create(NUdf::TDataType<ui32>::Id, env);
+ auto int64Type = TDataType::Create(NUdf::TDataType<i64>::Id, env);
+ auto uint64Type = TDataType::Create(NUdf::TDataType<ui64>::Id, env);
+ auto floatType = TDataType::Create(NUdf::TDataType<float>::Id, env);
+ auto doubleType = TDataType::Create(NUdf::TDataType<double>::Id, env);
+
+ writer->AddField(boolType, false);
+ writer->AddField(int8Type, false);
+ writer->AddField(uint8Type, false);
+ writer->AddField(int16Type, false);
+ writer->AddField(uint16Type, false);
+ writer->AddField(int32Type, false);
+ writer->AddField(uint32Type, false);
+ writer->AddField(int64Type, false);
+ writer->AddField(uint64Type, false);
+ writer->AddField(floatType, false);
+ writer->AddField(doubleType, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(11, items);
+ items[0] = NUdf::TUnboxedValuePod(true);
+ items[1] = NUdf::TUnboxedValuePod(i8(-1));
+ items[2] = NUdf::TUnboxedValuePod(ui8(2));
+ items[3] = NUdf::TUnboxedValuePod(i16(-3));
+ items[4] = NUdf::TUnboxedValuePod(ui16(4));
+ items[5] = NUdf::TUnboxedValuePod(i32(-5));
+ items[6] = NUdf::TUnboxedValuePod(ui32(6));
+ items[7] = NUdf::TUnboxedValuePod(i64(-7));
+ items[8] = NUdf::TUnboxedValuePod(ui64(8));
+ items[9] = NUdf::TUnboxedValuePod(float(9));
+ items[10] = NUdf::TUnboxedValuePod(double(10));
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\1\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\2\0\0\0\0\0\0\0\xFD\xFF\xFF\xFF\xFF\xFF\xFF\xFF\4\0\0\0\0\0\0\0\xFB\xFF\xFF\xFF\xFF\xFF\xFF\xFF\6\0\0\0\0\0\0\0\xF9\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x08\0\0\0\0\0\0\0\0\0\0\0\0\0\"@\0\0\0\0\0\0$@")");
+ }
+
+ Y_UNIT_TEST(TestWriteDataRowString) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto stringType = TDataType::Create(NUdf::TDataType<char*>::Id, env);
+ auto utf8Type = TDataType::Create(NUdf::TDataType<NUdf::TUtf8>::Id, env);
+ auto jsonType = TDataType::Create(NUdf::TDataType<NUdf::TJson>::Id, env);
+ auto ysonType = TDataType::Create(NUdf::TDataType<NUdf::TYson>::Id, env);
+
+ writer->AddField(stringType, false);
+ writer->AddField(utf8Type, false);
+ writer->AddField(jsonType, false);
+ writer->AddField(ysonType, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(4, items);
+ items[0] = NUdf::TUnboxedValue(MakeString("aaa"));
+ items[1] = NUdf::TUnboxedValue(MakeString("VERY LOOOONG STRING"));
+ items[2] = NUdf::TUnboxedValue(MakeString("[1,2]"));
+ items[3] = NUdf::TUnboxedValue(MakeString("{foo=bar}"));
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\3\0\0\0aaa\x13\0\0\0VERY LOOOONG STRING\5\0\0\0[1,2]\t\0\0\0{foo=bar}")");
+ }
+ Y_UNIT_TEST(TestWriteDataRowDecimal) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto longintType = TDataDecimalType::Create(10, 4, env);
+
+ writer->AddField(longintType, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(1, items);
+ items[0] = NUdf::TUnboxedValuePod(NDecimal::TInt128(-5));
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\2\0\0\0~\xFB")");
+ }
+
+ Y_UNIT_TEST(TestWriteDataRowDecimalNan) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto longintType = TDataDecimalType::Create(10, 4, env);
+
+ writer->AddField(longintType, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(1, items);
+ items[0] = NUdf::TUnboxedValuePod(NDecimal::Nan());
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\1\0\0\0\xFF")");
+ }
+
+ Y_UNIT_TEST(TestWriteOptionalRow) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto int32Type = TDataType::Create(NUdf::TDataType<i32>::Id, env);
+ auto optInt32Type = TOptionalType::Create(int32Type, env);
+
+ writer->AddField(optInt32Type, false);
+ writer->AddField(optInt32Type, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(2, items);
+ items[0] = NUdf::TUnboxedValuePod();
+ items[1] = NUdf::TUnboxedValuePod(i32(5));
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\0\1\5\0\0\0\0\0\0\0")");
+ }
+
+ Y_UNIT_TEST(TestWriteContainerRow) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ auto writer = MakeYtCodecCgWriter<false>(codegen);
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ auto uint32Type = TDataType::Create(NUdf::TDataType<ui32>::Id, env);
+ auto optUint32Type = TOptionalType::Create(uint32Type, env);
+ auto list1Type = TListType::Create(optUint32Type, env);
+ auto list2Type = TOptionalType::Create(TListType::Create(optUint32Type, env), env);
+
+ writer->AddField(list1Type, false);
+ writer->AddField(list2Type, false);
+ writer->AddField(list2Type, false);
+
+ auto func = writer->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ NUdf::TUnboxedValue* list1items;
+ NUdf::TUnboxedValue list1 = holderFactory.CreateDirectArrayHolder(2, list1items);
+ list1items[0] = NUdf::TUnboxedValuePod(ui32(2));
+ list1items[1] = NUdf::TUnboxedValuePod(ui32(3));
+
+ NUdf::TUnboxedValue* list2items;
+ NUdf::TUnboxedValue list2 = holderFactory.CreateDirectArrayHolder(3, list2items);
+ list2items[0] = NUdf::TUnboxedValuePod(ui32(4));
+ list2items[1] = NUdf::TUnboxedValuePod(ui32(5));
+ list2items[2] = NUdf::TUnboxedValue();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(3, items);
+ items[0] = list1;
+ items[1] = NUdf::TUnboxedValue();
+ items[2] = list2;
+ typedef void(*TFunc)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestWriter testWriter;
+ TOutputBuf buf(testWriter, nullptr);
+ funcPtr(row, buf);
+ buf.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(testWriter.Str().Quote(), R"("\x0E\0\0\0[[\6\2;];[\6\3;];]\0\1\x10\0\0\0[[\6\4;];[\6\5;];#;]")");
+ }
+
+ Y_UNIT_TEST(TestReadBool) {
+ TReadSetup setup("ReadBool", "\1");
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT(val.Get<bool>());
+ }
+
+ Y_UNIT_TEST(TestReadInt8) {
+ TReadSetup setup("ReadInt8", "\xff\xff\xff\xff\xff\xff\xff\xff");
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<i8>(), -1);
+ }
+
+ Y_UNIT_TEST(TestReadUint8) {
+ TReadSetup setup("ReadUint8", "\2\0\0\0\0\0\0\0"sv);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui8>(), 2);
+ }
+
+ Y_UNIT_TEST(TestReadInt16) {
+ TReadSetup setup("ReadInt16", "\xff\xff\xff\xff\xff\xff\xff\xff");
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<i16>(), -1);
+ }
+
+ Y_UNIT_TEST(TestReadUint16) {
+ TReadSetup setup("ReadUint16", "\2\1\0\0\0\0\0\0"sv);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui16>(), 258);
+ }
+
+ Y_UNIT_TEST(TestReadInt32) {
+ TReadSetup setup("ReadInt32", "\xff\xff\xff\xff\xff\xff\xff\xff");
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<i32>(), -1);
+ }
+
+ Y_UNIT_TEST(TestReadUint32) {
+ TReadSetup setup("ReadUint32", "\2\1\0\0\0\0\0\0"sv);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui32>(), 258);
+ }
+
+ Y_UNIT_TEST(TestReadInt64) {
+ TReadSetup setup("ReadInt64", "\xff\xff\xff\xff\xff\xff\xff\xff");
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<i32>(), -1);
+ }
+
+ Y_UNIT_TEST(TestReadUint64) {
+ TReadSetup setup("ReadUint64", "\2\1\0\0\0\0\0\0"sv);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui64>(), 258);
+ }
+
+ Y_UNIT_TEST(TestReadDataRowSmall) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TScopedAlloc alloc(__LOCATION__);
+ TMemoryUsageInfo memInfo("test");
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto boolType = TDataType::Create(NUdf::TDataType<bool>::Id, env);
+ auto int8Type = TDataType::Create(NUdf::TDataType<i8>::Id, env);
+ auto uint8Type = TDataType::Create(NUdf::TDataType<ui8>::Id, env);
+ auto int16Type = TDataType::Create(NUdf::TDataType<i16>::Id, env);
+ auto uint16Type = TDataType::Create(NUdf::TDataType<ui16>::Id, env);
+ auto int32Type = TDataType::Create(NUdf::TDataType<i32>::Id, env);
+ auto uint32Type = TDataType::Create(NUdf::TDataType<ui32>::Id, env);
+ auto int64Type = TDataType::Create(NUdf::TDataType<i64>::Id, env);
+ auto uint64Type = TDataType::Create(NUdf::TDataType<ui64>::Id, env);
+ auto floatType = TDataType::Create(NUdf::TDataType<float>::Id, env);
+ auto doubleType = TDataType::Create(NUdf::TDataType<double>::Id, env);
+
+ reader->AddField(boolType, {}, false);
+ reader->AddField(int8Type, {}, false);
+ reader->AddField(uint8Type, {}, false);
+ reader->AddField(int16Type, {}, false);
+ reader->AddField(uint16Type, {}, false);
+ reader->AddField(int32Type, {}, false);
+ reader->AddField(uint32Type, {}, false);
+ reader->AddField(int64Type, {}, false);
+ reader->AddField(uint64Type, {}, false);
+ reader->AddField(floatType, {}, false);
+ reader->AddField(doubleType, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(11, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\1\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\2\0\0\0\0\0\0\0\xFD\xFF\xFF\xFF\xFF\xFF\xFF\xFF\4\0\0\0\0\0\0\0\xFB\xFF\xFF\xFF\xFF\xFF\xFF\xFF\6\0\0\0\0\0\0\0\xF9\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x08\0\0\0\0\0\0\0\0\0\0\0\0\0\"@\0\0\0\0\0\0$@"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT(items[0].Get<bool>());
+ UNIT_ASSERT_VALUES_EQUAL(items[1].Get<i8>(), i8(-1));
+ UNIT_ASSERT_VALUES_EQUAL(items[2].Get<ui8>(), ui8(2));
+ UNIT_ASSERT_VALUES_EQUAL(items[3].Get<i16>(), i16(-3));
+ UNIT_ASSERT_VALUES_EQUAL(items[4].Get<ui16>(), ui16(4));
+ UNIT_ASSERT_VALUES_EQUAL(items[5].Get<i32>(), i32(-5));
+ UNIT_ASSERT_VALUES_EQUAL(items[6].Get<ui32>(), ui32(6));
+ UNIT_ASSERT_VALUES_EQUAL(items[7].Get<i64>(), i64(-7));
+ UNIT_ASSERT_VALUES_EQUAL(items[8].Get<ui64>(), ui64(8));
+ UNIT_ASSERT_VALUES_EQUAL(items[9].Get<float>(), float(9));
+ UNIT_ASSERT_VALUES_EQUAL(items[10].Get<double>(), double(10));
+ }
+
+ Y_UNIT_TEST(TestReadDataRowString) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto stringType = TDataType::Create(NUdf::TDataType<char*>::Id, env);
+ auto utf8Type = TDataType::Create(NUdf::TDataType<NUdf::TUtf8>::Id, env);
+ auto jsonType = TDataType::Create(NUdf::TDataType<NUdf::TJson>::Id, env);
+ auto ysonType = TDataType::Create(NUdf::TDataType<NUdf::TYson>::Id, env);
+
+ reader->AddField(stringType, {}, false);
+ reader->AddField(utf8Type, {}, false);
+ reader->AddField(jsonType, {}, false);
+ reader->AddField(ysonType, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(4, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\3\0\0\0aaa\x13\0\0\0VERY LOOOONG STRING\5\0\0\0[1,2]\t\0\0\0{foo=bar}"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[0].AsStringRef()), "aaa");
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[1].AsStringRef()), "VERY LOOOONG STRING");
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[2].AsStringRef()), "[1,2]");
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[3].AsStringRef()), "{foo=bar}");
+ }
+
+#ifndef _win_
+ Y_UNIT_TEST(TestReadDataRowDecimal) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto longintType = TDataDecimalType::Create(10, 4, env);
+
+ reader->AddField(longintType, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(1, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\2\0\0\0~\xFB"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT_EQUAL(items[0].GetInt128(), -5);
+ }
+
+ Y_UNIT_TEST(TestReadDataRowDecimalNan) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto longintType = TDataDecimalType::Create(10, 4, env);
+
+ reader->AddField(longintType, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(1, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\1\0\0\0\xFF"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT_EQUAL(items[0].GetInt128(), NDecimal::Nan());
+ }
+#endif
+ Y_UNIT_TEST(TestReadOptionalRow) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto int32Type = TDataType::Create(NUdf::TDataType<i32>::Id, env);
+ auto optInt32Type = TOptionalType::Create(int32Type, env);
+
+ reader->AddField(optInt32Type, {}, false);
+ reader->AddField(optInt32Type, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(2, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\0\1\5\0\0\0\0\0\0\0"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT(!items[0]);
+ UNIT_ASSERT_VALUES_EQUAL(items[1].Get<i32>(), 5);
+ }
+
+ Y_UNIT_TEST(TestReadContainerRow) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+
+ auto uint32Type = TDataType::Create(NUdf::TDataType<ui32>::Id, env);
+ auto optUint32Type = TOptionalType::Create(uint32Type, env);
+ auto list1Type = TListType::Create(optUint32Type, env);
+ auto list2Type = TOptionalType::Create(TListType::Create(optUint32Type, env), env);
+
+ reader->AddField(list1Type, {}, false);
+ reader->AddField(list2Type, {}, false);
+ reader->AddField(list2Type, {}, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(3, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\x0E\0\0\0[[\6\2;];[\6\3;];]\0\1\x10\0\0\0[[\6\4;];[\6\5;];#;]"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+
+ UNIT_ASSERT_VALUES_EQUAL(items[0].GetListLength(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(items[0].GetElements()[0].Get<ui32>(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(items[0].GetElements()[1].Get<ui32>(), 3);
+
+ UNIT_ASSERT(!items[1]);
+
+ UNIT_ASSERT_VALUES_EQUAL(items[2].GetListLength(), 3);
+ UNIT_ASSERT_VALUES_EQUAL(items[2].GetElements()[0].Get<ui32>(), 4);
+ UNIT_ASSERT_VALUES_EQUAL(items[2].GetElements()[1].Get<ui32>(), 5);
+ UNIT_ASSERT(!items[2].GetElements()[2]);
+ }
+
+ Y_UNIT_TEST(TestReadDefaultRow) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto int32Type = TDataType::Create(NUdf::TDataType<i32>::Id, env);
+
+ reader->AddField(int32Type, NUdf::TUnboxedValuePod(7), false);
+ reader->AddField(int32Type, NUdf::TUnboxedValuePod(7), false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(2, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\0\1\5\0\0\0\0\0\0\0"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT_VALUES_EQUAL(items[0].Get<i32>(), 7);
+ UNIT_ASSERT_VALUES_EQUAL(items[1].Get<i32>(), 5);
+ }
+
+ Y_UNIT_TEST(TestReadDefaultRowString) {
+ auto codegen = ICodegen::Make(ETarget::Native);
+ codegen->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TMemoryUsageInfo memInfo("test");
+ TScopedAlloc alloc(__LOCATION__);
+ THolderFactory holderFactory(alloc.Ref(), memInfo);
+
+ auto reader = MakeYtCodecCgReader(codegen, holderFactory);
+ TTypeEnvironment env(alloc);
+ auto stringType = TDataType::Create(NUdf::TDataType<char*>::Id, env);
+
+ const NUdf::TUnboxedValue defStr = MakeString(NUdf::TStringRef::Of("VERY LOOONG STRING"));
+ reader->AddField(stringType, defStr, false);
+ reader->AddField(stringType, defStr, false);
+
+ auto func = reader->Build();
+ codegen->Verify();
+ YtCodecAddMappings(*codegen);
+ codegen->Compile();
+
+ NUdf::TUnboxedValue* items;
+ NUdf::TUnboxedValue row = holderFactory.CreateDirectArrayHolder(2, items);
+ typedef void(*TFunc)(NUdf::TUnboxedValue*, TInputBuf&);
+ auto funcPtr = (TFunc)codegen->GetPointerToFunction(func);
+
+ TTestReader testReader("\0\1\3\0\0\0foo"sv);
+ TInputBuf buf(testReader, nullptr);
+ funcPtr(items, buf);
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[0].AsStringRef()), "VERY LOOONG STRING");
+ UNIT_ASSERT_STRINGS_EQUAL(TStringBuf(items[1].AsStringRef()), "foo");
+ }
+
+ Y_UNIT_TEST(TestReadTzDate) {
+ // full size = 2 + 2 = 4
+ TStringBuf buf = "\x04\0\0\0\1\2\0\1"sv;
+ TReadSetup setup("ReadTzDate", buf);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui16>(), 258);
+ UNIT_ASSERT_VALUES_EQUAL(val.GetTimezoneId(), 1);
+ }
+
+ Y_UNIT_TEST(TestReadTzDatetime) {
+ // full size = 4 + 2 = 6
+ TStringBuf buf = "\x06\0\0\0\0\0\1\3\0\1"sv;
+ TReadSetup setup("ReadTzDatetime", buf);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui32>(), 259);
+ UNIT_ASSERT_VALUES_EQUAL(val.GetTimezoneId(), 1);
+ }
+
+ Y_UNIT_TEST(TestReadTzTimestamp) {
+ // full size = 8 + 2 = 10
+ TStringBuf buf = "\x0a\0\0\0\0\0\0\0\0\0\1\4\0\1"sv;
+ TReadSetup setup("ReadTzTimestamp", buf);
+ typedef void(*TFunc)(TInputBuf&, void*);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ NUdf::TUnboxedValue val;
+ funcPtr(setup.Buf_, &val);
+ UNIT_ASSERT_VALUES_EQUAL(val.Get<ui64>(), 260);
+ UNIT_ASSERT_VALUES_EQUAL(val.GetTimezoneId(), 1);
+ }
+
+ Y_UNIT_TEST(TestWriteTzDate) {
+ TWriteSetup setup("WriteTzDate");
+ typedef void(*TFunc)(TOutputBuf&, ui16, ui16);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 258, 1);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), TString("\x04\0\0\0\1\2\0\1"sv).Quote());
+ }
+
+ Y_UNIT_TEST(TestWriteTzDatetime) {
+ TWriteSetup setup("WriteTzDatetime");
+ typedef void(*TFunc)(TOutputBuf&, ui32, ui16);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 259, 1);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), TString("\x06\0\0\0\0\0\1\3\0\1"sv).Quote());
+ }
+
+ Y_UNIT_TEST(TestWriteTzTimestamp) {
+ TWriteSetup setup("WriteTzTimestamp");
+ typedef void(*TFunc)(TOutputBuf&, ui64, ui16);
+ auto funcPtr = (TFunc)setup.Codegen_->GetPointerToFunction(setup.Func_);
+ funcPtr(setup.Buf_, 260, 1);
+ setup.Buf_.Finish();
+ UNIT_ASSERT_STRINGS_EQUAL(setup.TestWriter_.Str().Quote(), TString("\x0a\0\0\0\0\0\0\0\0\0\1\4\0\1"sv).Quote());
+ }
+}
+
+}
+#endif
diff --git a/ydb/library/yql/providers/yt/codec/codegen/ya.make b/ydb/library/yql/providers/yt/codec/codegen/ya.make
new file mode 100644
index 0000000000..79b07b2726
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/ya.make
@@ -0,0 +1,85 @@
+LIBRARY()
+
+SRCS(
+ yt_codec_cg.cpp
+ yt_codec_cg.h
+)
+
+PEERDIR(
+ library/cpp/resource
+ ydb/library/binary_json
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/parser/pg_wrapper/interface
+ ydb/library/yql/utils
+)
+
+IF (NOT MKQL_DISABLE_CODEGEN)
+ PEERDIR(
+ contrib/libs/llvm12/lib/IR
+ contrib/libs/llvm12/lib/ExecutionEngine/MCJIT
+ contrib/libs/llvm12/lib/Linker
+ contrib/libs/llvm12/lib/Support
+ contrib/libs/llvm12/lib/Target/X86
+ contrib/libs/llvm12/lib/Target/X86/AsmParser
+ contrib/libs/llvm12/lib/Transforms/IPO
+ ydb/library/yql/minikql/codegen
+ )
+ LLVM_BC(
+ yt_codec_bc.cpp
+ NAME
+ YtCodecFuncs
+ SYMBOLS
+ WriteJust
+ WriteNothing
+ WriteBool
+ Write8
+ Write16
+ Write32
+ Write64
+ Write120
+ WriteDecimal32
+ WriteDecimal64
+ WriteDecimal128
+ WriteFloat
+ WriteDouble
+ WriteString
+ ReadBool
+ ReadInt8
+ ReadUint8
+ ReadInt16
+ ReadUint16
+ ReadInt32
+ ReadUint32
+ ReadInt64
+ ReadUint64
+ ReadInt120
+ ReadDecimal32
+ ReadDecimal64
+ ReadDecimal128
+ ReadFloat
+ ReadDouble
+ ReadOptional
+ SkipFixedData
+ SkipVarData
+ ReadTzDate
+ ReadTzDatetime
+ ReadTzTimestamp
+ WriteTzDate
+ WriteTzDatetime
+ WriteTzTimestamp
+ GetWrittenBytes
+ FillZero
+ )
+ELSE()
+ CFLAGS(
+ -DMKQL_DISABLE_CODEGEN
+ )
+ENDIF()
+
+YQL_LAST_ABI_VERSION()
+
+END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
new file mode 100644
index 0000000000..c0deefdcd4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_bc.cpp
@@ -0,0 +1,296 @@
+#define LLVM_BC
+#include "yt_codec_cg.h"
+#include <ydb/library/yql/utils/swap_bytes.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_buf.h>
+#include <ydb/library/yql/public/decimal/yql_decimal_serialize.h>
+#include <ydb/library/yql/public/decimal/yql_decimal.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <ydb/library/yql/public/decimal/yql_decimal_serialize.cpp>
+#include <ydb/library/yql/public/decimal/yql_decimal.cpp>
+
+using namespace NYql;
+
+extern "C" void WriteJust(void* vbuf) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.Write('\1');
+}
+
+extern "C" void WriteNothing(void* vbuf) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.Write('\0');
+}
+
+extern "C" void WriteBool(void* vbuf, bool value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.Write(value ? '\1' : '\0');
+}
+
+extern "C" void Write8(void* vbuf, ui8 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.Write(value);
+}
+
+extern "C" void Write16(void* vbuf, ui16 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany((const char*)&value, sizeof(value));
+}
+
+extern "C" void Write32(void* vbuf, ui32 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany((const char*)&value, sizeof(value));
+}
+
+extern "C" void Write64(void* vbuf, ui64 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany((const char*)&value, sizeof(value));
+}
+extern "C" void Write120(void* vbuf, const void* decimal) {
+ auto value = reinterpret_cast<const NDecimal::TInt128*>(decimal);
+ char b[sizeof(*value)];
+ const ui32 size = NDecimal::Serialize(*value, b);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany(reinterpret_cast<const char*>(&size), sizeof(size));
+ buf.WriteMany(b, size);
+}
+
+extern "C" void WriteDecimal32(void* vbuf, const void* decimal) {
+ auto value = reinterpret_cast<const NDecimal::TInt128*>(decimal);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ i32 data = NDecimal::ToYtDecimal<i32>(*value);
+ buf.WriteMany(reinterpret_cast<const char*>(&data), sizeof(data));
+}
+extern "C" void WriteDecimal64(void* vbuf, const void* decimal) {
+ auto value = reinterpret_cast<const NDecimal::TInt128*>(decimal);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ i64 data = NDecimal::ToYtDecimal<i64>(*value);
+ buf.WriteMany(reinterpret_cast<const char*>(&data), sizeof(data));
+}
+extern "C" void WriteDecimal128(void* vbuf, const void* decimal) {
+ auto value = reinterpret_cast<const NDecimal::TInt128*>(decimal);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ NDecimal::TInt128 data = NDecimal::ToYtDecimal<NDecimal::TInt128>(*value);
+ buf.WriteMany(reinterpret_cast<const char*>(&data), sizeof(data));
+}
+extern "C" void WriteFloat(void* vbuf, ui32 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ double data = (double)*(const float*)&value;
+ buf.WriteMany((const char*)&data, sizeof(data));
+}
+
+extern "C" void WriteDouble(void* vbuf, ui64 value) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany((const char*)&value, sizeof(value));
+}
+
+extern "C" void WriteString(void* vbuf, const char* buffer, ui32 len) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ buf.WriteMany(buffer, len);
+}
+
+extern "C" void ReadBool(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ char cmd = buf.Read();
+ new (vpod) NUdf::TUnboxedValuePod(cmd != 0);
+}
+
+extern "C" void ReadInt8(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(i8(data));
+}
+
+extern "C" void ReadUint8(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(ui8(data));
+}
+
+extern "C" void ReadInt16(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(i16(data));
+}
+
+extern "C" void ReadUint16(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(ui16(data));
+}
+
+extern "C" void ReadInt32(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(i32(data));
+}
+
+extern "C" void ReadUint32(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(ui32(data));
+}
+
+extern "C" void ReadInt64(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(data);
+}
+
+extern "C" void ReadUint64(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(data);
+}
+extern "C" void ReadInt120(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui32 size;
+ buf.ReadMany(reinterpret_cast<char*>(&size), sizeof(size));
+
+ if (size <= sizeof(NDecimal::TInt128)) {
+ char data[sizeof(NDecimal::TInt128)];
+ buf.ReadMany(data, size);
+ auto v = NDecimal::Deserialize(data, size).first;
+ if (v == NDecimal::Err()) {
+ ThrowBadDecimal();
+ } else {
+ new (vpod) NUdf::TUnboxedValuePod(v);
+ }
+ } else {
+ ThrowBadDecimal();
+ }
+}
+
+extern "C" void ReadDecimal32(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i32 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(NDecimal::FromYtDecimal(data));
+}
+extern "C" void ReadDecimal64(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ i64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(NDecimal::FromYtDecimal(data));
+}
+extern "C" void ReadDecimal128(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ NDecimal::TInt128 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(NDecimal::FromYtDecimal(data));
+}
+extern "C" void ReadFloat(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ double data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(float(data));
+}
+
+extern "C" void ReadDouble(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ double data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ new (vpod) NUdf::TUnboxedValuePod(data);
+}
+
+extern "C" ui8 ReadOptional(void* vbuf) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ return buf.Read();
+}
+
+extern "C" void SkipFixedData(void* vbuf, ui64 size) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ buf.SkipMany(size);
+}
+
+extern "C" void SkipVarData(void* vbuf) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ buf.SkipMany(size);
+}
+
+extern "C" void ReadTzDate(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ ui16 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ ui16 tzId;
+ buf.ReadMany((char*)&tzId, sizeof(tzId));
+ data = SwapBytes(data);
+ tzId = SwapBytes(tzId);
+ (new (vpod) NUdf::TUnboxedValuePod(data))->SetTimezoneId(tzId);
+}
+
+extern "C" void ReadTzDatetime(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ ui32 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ ui16 tzId;
+ buf.ReadMany((char*)&tzId, sizeof(tzId));
+ data = SwapBytes(data);
+ tzId = SwapBytes(tzId);
+ (new (vpod) NUdf::TUnboxedValuePod(data))->SetTimezoneId(tzId);
+}
+
+extern "C" void ReadTzTimestamp(void* vbuf, void* vpod) {
+ NCommon::TInputBuf& buf = *(NCommon::TInputBuf*)vbuf;
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ ui64 data;
+ buf.ReadMany((char*)&data, sizeof(data));
+ ui16 tzId;
+ buf.ReadMany((char*)&tzId, sizeof(tzId));
+ data = SwapBytes(data);
+ tzId = SwapBytes(tzId);
+ (new (vpod) NUdf::TUnboxedValuePod(data))->SetTimezoneId(tzId);
+}
+
+extern "C" void WriteTzDate(void* vbuf, ui16 value, ui16 tzId) {
+ value = SwapBytes(value);
+ tzId = SwapBytes(tzId);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ const ui32 size = sizeof(value) + sizeof(tzId);
+ buf.WriteMany((const char*)&size, sizeof(size));
+ buf.WriteMany((const char*)&value, sizeof(value));
+ buf.WriteMany((const char*)&tzId, sizeof(tzId));
+}
+
+extern "C" void WriteTzDatetime(void* vbuf, ui32 value, ui16 tzId) {
+ value = SwapBytes(value);
+ tzId = SwapBytes(tzId);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ const ui32 size = sizeof(value) + sizeof(tzId);
+ buf.WriteMany((const char*)&size, sizeof(size));
+ buf.WriteMany((const char*)&value, sizeof(value));
+ buf.WriteMany((const char*)&tzId, sizeof(tzId));
+}
+
+extern "C" void WriteTzTimestamp(void* vbuf, ui64 value, ui16 tzId) {
+ value = SwapBytes(value);
+ tzId = SwapBytes(tzId);
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ const ui32 size = sizeof(value) + sizeof(tzId);
+ buf.WriteMany((const char*)&size, sizeof(size));
+ buf.WriteMany((const char*)&value, sizeof(value));
+ buf.WriteMany((const char*)&tzId, sizeof(tzId));
+}
+
+extern "C" ui64 GetWrittenBytes(void* vbuf) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ return buf.GetWrittenBytes();
+}
+
+extern "C" void FillZero(void* vpod) {
+ new (vpod) NUdf::TUnboxedValuePod(NUdf::TUnboxedValuePod::Zero());
+}
diff --git a/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
new file mode 100644
index 0000000000..7547f3580d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.cpp
@@ -0,0 +1,906 @@
+#include "yt_codec_cg.h"
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/parser/pg_wrapper/interface/codec.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_buf.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+
+#ifndef MKQL_DISABLE_CODEGEN
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_builder.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+#include <ydb/library/yql/minikql/mkql_type_ops.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_codegen.h>
+#include <ydb/library/yql/minikql/codegen/codegen.h>
+
+#include <ydb/library/binary_json/read.h>
+#include <ydb/library/binary_json/write.h>
+
+#include <library/cpp/resource/resource.h>
+
+#include <llvm/IR/Module.h>
+#include <llvm/IR/Instructions.h>
+
+#endif
+
+namespace NYql {
+
+#ifndef MKQL_DISABLE_CODEGEN
+
+using namespace llvm;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+extern "C" void ThrowBadDecimal() {
+ throw yexception() << "Invalid decimal data";
+}
+
+extern "C" void YtCodecReadString(void* vbuf, void* vpod) {
+ NYql::NCommon::TInputBuf& buf = *(NYql::NCommon::TInputBuf*)vbuf;
+ NUdf::TUnboxedValue& pod = *(NUdf::TUnboxedValue*)vpod;
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ CHECK_STRING_LENGTH_UNSIGNED(size);
+ auto str = NUdf::TUnboxedValue(MakeStringNotFilled(size));
+ buf.ReadMany(str.AsStringRef().Data(), size);
+ pod = std::move(str);
+}
+
+extern "C" void YtCodecWriteJsonDocument(void* vbuf, const char* buffer, ui32 len) {
+ NCommon::TOutputBuf& buf = *(NCommon::TOutputBuf*)vbuf;
+ TStringBuf binaryJson(buffer, len);
+ const TString json = NBinaryJson::SerializeToJson(binaryJson);
+ const ui32 size = json.Size();
+ buf.WriteMany((const char*)&size, sizeof(size));
+ buf.WriteMany(json.Data(), size);
+}
+
+extern "C" void YtCodecReadJsonDocument(void* vbuf, void* vpod) {
+ NYql::NCommon::TInputBuf& buf = *(NYql::NCommon::TInputBuf*)vbuf;
+ NUdf::TUnboxedValue& pod = *(NUdf::TUnboxedValue*)vpod;
+
+ ui32 size;
+ buf.ReadMany((char*)&size, sizeof(size));
+ CHECK_STRING_LENGTH_UNSIGNED(size);
+
+ auto json = NUdf::TUnboxedValue(MakeStringNotFilled(size));
+ buf.ReadMany(json.AsStringRef().Data(), size);
+
+ const auto binaryJson = NBinaryJson::SerializeToBinaryJson(json.AsStringRef());
+ if (!binaryJson.Defined()) {
+ YQL_ENSURE(false, "Invalid JSON stored for JsonDocument type");
+ }
+
+ TStringBuf binaryJsonRef(binaryJson->Data(), binaryJson->Size());
+ pod = MakeString(binaryJsonRef);
+}
+
+template<bool Flat>
+class TYtCodecCgWriter : public IYtCodecCgWriter {
+public:
+ TYtCodecCgWriter(const std::unique_ptr<NCodegen::ICodegen>& codegen, const void* cookie)
+ : Codegen_(codegen)
+ {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ // input - pointer to struct UnboxedValue as int128 and instance of buffer, output - void
+ const auto funcType = Flat || Codegen_->GetEffectiveTarget() == NYql::NCodegen::ETarget::Windows ?
+ FunctionType::get(Type::getVoidTy(context), {PointerType::getUnqual(Type::getInt128Ty(context)), PointerType::getUnqual(Type::getInt8Ty(context))}, false):
+ FunctionType::get(Type::getVoidTy(context), {Type::getInt128Ty(context), PointerType::getUnqual(Type::getInt8Ty(context))}, false);
+ Func_ = cast<Function>(module.getOrInsertFunction((TStringBuilder() << (Flat ? "YtCodecCgWriterFlat." : "YtCodecCgWriter.") << cookie).data(), funcType).getCallee());
+ Block_ = BasicBlock::Create(context, "EntryBlock", Func_);
+ }
+
+ void AddField(NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags) override {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ auto args = Func_->arg_begin();
+ const auto valueType = Type::getInt128Ty(context);
+ const auto valueArg = &*args++;
+ const auto buf = &*args++;
+ const auto value = Flat || !valueArg->getType()->isPointerTy() ?
+ static_cast<Value*>(valueArg) : new LoadInst(valueType, valueArg, "row", Block_);
+ const auto index = ConstantInt::get(Type::getInt32Ty(context), Index_++);
+
+ const auto elemPtr = Flat ?
+ GetElementPtrInst::CreateInBounds(valueType, value, { index }, "elemPtr", Block_):
+ static_cast<Value*>(new AllocaInst(valueType, 0U, "elemPtr", Block_));
+
+ if constexpr (!Flat) {
+ CallBoxedValueVirtualMethod<NUdf::TBoxedValueAccessor::EMethod::GetElement>(elemPtr, value, Codegen_, Block_, index);
+ }
+
+ bool isOptional;
+ auto unwrappedType = UnpackOptional(type, isOptional);
+ if (!isOptional) {
+ GenerateRequired(elemPtr, buf, type, nativeYtTypeFlags, false);
+ } else {
+ const auto just = BasicBlock::Create(context, "just", Func_);
+ const auto nothing = BasicBlock::Create(context, "nothing", Func_);
+ const auto done = BasicBlock::Create(context, "done", Func_);
+
+ const auto zero = ConstantInt::get(valueType, 0ULL);
+ const auto elem = new LoadInst(valueType, elemPtr, "elem", Block_);
+ const auto check = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_NE, elem, zero, "exists", Block_);
+ BranchInst::Create(just, nothing, check, Block_);
+
+ {
+ Block_ = just;
+ CallInst::Create(module.getFunction("WriteJust"), { buf }, "", Block_);
+ if (unwrappedType->IsOptional()) {
+ const auto unwrappedElem = GetOptionalValue(context, elem, Block_);
+ const auto unwrappedElemPtr = new AllocaInst(valueType, 0U, "unwrapped", Block_);
+ new StoreInst(unwrappedElem, unwrappedElemPtr, Block_);
+ GenerateRequired(unwrappedElemPtr, buf, unwrappedType, nativeYtTypeFlags, true);
+ } else {
+ GenerateRequired(elemPtr, buf, unwrappedType, nativeYtTypeFlags, true);
+ }
+
+ BranchInst::Create(done, Block_);
+ }
+
+ {
+ Block_ = nothing;
+ CallInst::Create(module.getFunction("WriteNothing"), { buf }, "", Block_);
+ BranchInst::Create(done, Block_);
+ }
+
+ Block_ = done;
+ }
+ }
+
+ Function* Build() override {
+ ReturnInst::Create(Codegen_->GetContext(), Block_);
+ Codegen_->ExportSymbol(Func_);
+ return Func_;
+ }
+
+ void GenerateRequired(Value* elemPtr, Value* buf, NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags, bool wasOptional) {
+ if (type->IsData()) {
+ return GenerateData(elemPtr, buf, type, nativeYtTypeFlags);
+ }
+
+ if (!wasOptional && type->IsPg()) {
+ return GeneratePg(elemPtr, buf, static_cast<NKikimr::NMiniKQL::TPgType*>(type));
+ }
+
+ // call external writer
+ auto& context = Codegen_->GetContext();
+ const auto typeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)type);
+ const auto valType = Type::getInt128Ty(context);
+ if (nativeYtTypeFlags) {
+ const auto flagsConst = ConstantInt::get(Type::getInt64Ty(context), nativeYtTypeFlags);
+ const auto funcAddr = ConstantInt::get(Type::getInt64Ty(context), (ui64)&NYql::NCommon::WriteContainerNativeYtValue);
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), Type::getInt64Ty(context), PointerType::getUnqual(valType),
+ PointerType::getUnqual(Type::getInt8Ty(context))
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, flagsConst, elemPtr, buf }, "", Block_);
+ } else {
+ const auto funcAddr = ConstantInt::get(Type::getInt64Ty(context), (ui64)&NYql::NCommon::WriteYsonContainerValue);
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), PointerType::getUnqual(valType),
+ PointerType::getUnqual(Type::getInt8Ty(context))
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, elemPtr, buf }, "", Block_);
+ }
+ TCodegenContext ctx(Codegen_);
+ ctx.Func = Func_;
+ if constexpr (Flat)
+ ValueCleanup(EValueRepresentation::Any, elemPtr, ctx, Block_);
+ else
+ ValueUnRef(EValueRepresentation::Any, elemPtr, ctx, Block_);
+ }
+
+ void GenerateData(Value* elemPtr, Value* buf, NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags) {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ const auto elem = new LoadInst(Type::getInt128Ty(context), elemPtr, "elem", Block_);
+ const auto schemeType = static_cast<TDataType*>(type)->GetSchemeType();
+ switch (schemeType) {
+ case NUdf::TDataType<bool>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt1Ty(context), "data", Block_);
+ CallInst::Create(module.getFunction("WriteBool"), { buf, data }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<i8>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt8Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::SExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<ui8>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt8Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::ZExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<i16>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt16Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::SExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TDate>::Id:
+ case NUdf::TDataType<ui16>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt16Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::ZExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<i32>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt32Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::SExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TDatetime>::Id:
+ case NUdf::TDataType<ui32>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt32Ty(context), "data", Block_);
+ const auto ext = CastInst::Create(Instruction::ZExt, data, Type::getInt64Ty(context), "ext", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, ext }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<float>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt32Ty(context), "data", Block_);
+ CallInst::Create(module.getFunction("WriteFloat"), { buf, data }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<double>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt64Ty(context), "data", Block_);
+ CallInst::Create(module.getFunction("WriteDouble"), { buf, data }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTimestamp>::Id:
+ case NUdf::TDataType<ui64>::Id:
+ case NUdf::TDataType<NUdf::TInterval>::Id:
+ case NUdf::TDataType<i64>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt64Ty(context), "data", Block_);
+ CallInst::Create(module.getFunction("Write64"), { buf, data }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TDecimal>::Id: {
+ const auto ptr = new AllocaInst(Type::getInt128Ty(context), 0U, "ptr", Block_);
+ new StoreInst(GetterForInt128(elem, Block_), ptr, Block_);
+ const auto velemPtr = CastInst::Create(Instruction::BitCast, ptr, PointerType::getUnqual(Type::getInt8Ty(context)), "cast", Block_);
+ if (nativeYtTypeFlags & NTCF_DECIMAL) {
+ auto const params = static_cast<TDataDecimalType*>(type)->GetParams();
+ if (params.first < 10) {
+ CallInst::Create(module.getFunction("WriteDecimal32"), { buf, velemPtr }, "", Block_);
+ } else if (params.first < 19) {
+ CallInst::Create(module.getFunction("WriteDecimal64"), { buf, velemPtr }, "", Block_);
+ } else {
+ CallInst::Create(module.getFunction("WriteDecimal128"), { buf, velemPtr }, "", Block_);
+ }
+ } else {
+ CallInst::Create(module.getFunction("Write120"), { buf, velemPtr }, "", Block_);
+ }
+ break;
+ }
+
+ case NUdf::TDataType<char*>::Id:
+ case NUdf::TDataType<NUdf::TUtf8>::Id:
+ case NUdf::TDataType<NUdf::TJson>::Id:
+ case NUdf::TDataType<NUdf::TYson>::Id:
+ case NUdf::TDataType<NUdf::TUuid>::Id:
+ case NUdf::TDataType<NUdf::TDyNumber>::Id:
+ case NUdf::TDataType<NUdf::TJsonDocument>::Id: {
+ const auto type = Type::getInt8Ty(context);
+ const auto embType = FixedVectorType::get(type, 16);
+ const auto cast = CastInst::Create(Instruction::BitCast, elem, embType, "cast", Block_);
+ const auto mark = ExtractElementInst::Create(cast, ConstantInt::get(type, 15), "mark", Block_);
+
+ const auto bsize = ExtractElementInst::Create(cast, ConstantInt::get(type, 14), "bsize", Block_);
+ const auto esize = CastInst::Create(Instruction::ZExt, bsize, Type::getInt32Ty(context), "esize", Block_);
+
+ const auto sizeType = Type::getInt32Ty(context);
+ const auto strType = FixedVectorType::get(sizeType, 4);
+ const auto four = CastInst::Create(Instruction::BitCast, elem, strType, "four", Block_);
+ const auto ssize = ExtractElementInst::Create(four, ConstantInt::get(type, 2), "ssize", Block_);
+
+ const auto cemb = CastInst::Create(Instruction::Trunc, mark, Type::getInt1Ty(context), "cemb", Block_);
+ const auto size = SelectInst::Create(cemb, esize, ssize, "size", Block_);
+
+ const auto emb = BasicBlock::Create(context, "emb", Func_);
+ const auto str = BasicBlock::Create(context, "str", Func_);
+ const auto done = BasicBlock::Create(context, "done", Func_);
+
+ const auto scond = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_EQ, mark, ConstantInt::get(type, 2), "scond", Block_);
+ BranchInst::Create(str, emb, scond, Block_);
+
+ {
+ Block_ = emb;
+
+ const auto bytePtr = CastInst::Create(Instruction::BitCast, elemPtr, PointerType::getUnqual(Type::getInt8Ty(context)), "cast", Block_);
+ if (schemeType == NUdf::TDataType<NUdf::TJsonDocument>::Id) {
+ const auto fnType = FunctionType::get(Type::getVoidTy(context), {
+ PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context)),
+ sizeType,
+ }, false);
+ const auto func = module.getOrInsertFunction("YtCodecWriteJsonDocument", fnType);
+ CallInst::Create(func, { buf, bytePtr, size }, "", Block_);
+ } else {
+ CallInst::Create(module.getFunction("Write32"), { buf, size }, "", Block_);
+ CallInst::Create(module.getFunction("WriteString"), { buf, bytePtr, size }, "", Block_);
+ }
+ BranchInst::Create(done, Block_);
+ }
+
+ {
+ Block_ = str;
+
+ const auto foffs = ExtractElementInst::Create(four, ConstantInt::get(type, 3), "foffs", Block_);
+ const auto offs = BinaryOperator::CreateAnd(foffs, ConstantInt::get(foffs->getType(), 0xFFFFFF), "offs", Block_);
+ const auto skip = BinaryOperator::CreateAdd(offs, ConstantInt::get(offs->getType(), 16), "skip", Block_);
+
+ const auto half = CastInst::Create(Instruction::Trunc, elem, Type::getInt64Ty(context), "half", Block_);
+ const auto ptr = CastInst::Create(Instruction::IntToPtr, half, PointerType::getUnqual(type), "ptr", Block_);
+
+ const auto bytePtr = GetElementPtrInst::CreateInBounds(Type::getInt8Ty(context), ptr, { skip }, "bptr", Block_);
+
+ if (schemeType == NUdf::TDataType<NUdf::TJsonDocument>::Id) {
+ const auto fnType = FunctionType::get(Type::getVoidTy(context), {
+ PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context)),
+ sizeType,
+ }, false);
+ const auto func = module.getOrInsertFunction("YtCodecWriteJsonDocument", fnType);
+ CallInst::Create(func, { buf, bytePtr, size }, "", Block_);
+ } else {
+ CallInst::Create(module.getFunction("Write32"), { buf, size }, "", Block_);
+ CallInst::Create(module.getFunction("WriteString"), { buf, bytePtr, size }, "", Block_);
+ }
+
+ TCodegenContext ctx(Codegen_);
+ ctx.Func = Func_;
+ if constexpr (Flat)
+ ValueCleanup(EValueRepresentation::String, elemPtr, ctx, Block_);
+ else
+ ValueUnRef(EValueRepresentation::String, elemPtr, ctx, Block_);
+
+ BranchInst::Create(done, Block_);
+ }
+
+ Block_ = done;
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzDate>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt16Ty(context), "data", Block_);
+ const auto sizeType = Type::getInt16Ty(context);
+ const auto strType = FixedVectorType::get(sizeType, 8);
+ const auto eight = CastInst::Create(Instruction::BitCast, elem, strType, "eight", Block_);
+ const auto type = Type::getInt8Ty(context);
+ const auto tzId = ExtractElementInst::Create(eight, ConstantInt::get(type, 4), "id", Block_);
+ CallInst::Create(module.getFunction("WriteTzDate"), { buf, data, tzId }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzDatetime>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt32Ty(context), "data", Block_);
+ const auto sizeType = Type::getInt16Ty(context);
+ const auto strType = FixedVectorType::get(sizeType, 8);
+ const auto eight = CastInst::Create(Instruction::BitCast, elem, strType, "eight", Block_);
+ const auto type = Type::getInt8Ty(context);
+ const auto tzId = ExtractElementInst::Create(eight, ConstantInt::get(type, 4), "id", Block_);
+ CallInst::Create(module.getFunction("WriteTzDatetime"), { buf, data, tzId }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzTimestamp>::Id: {
+ const auto data = CastInst::Create(Instruction::Trunc, elem, Type::getInt64Ty(context), "data", Block_);
+ const auto sizeType = Type::getInt16Ty(context);
+ const auto strType = FixedVectorType::get(sizeType, 8);
+ const auto eight = CastInst::Create(Instruction::BitCast, elem, strType, "eight", Block_);
+ const auto type = Type::getInt8Ty(context);
+ const auto tzId = ExtractElementInst::Create(eight, ConstantInt::get(type, 4), "id", Block_);
+ CallInst::Create(module.getFunction("WriteTzTimestamp"), { buf, data, tzId }, "", Block_);
+ break;
+ }
+
+ default:
+ YQL_ENSURE(false, "Unsupported data type: " << schemeType);
+ }
+ }
+
+ void GeneratePg(Value* elemPtr, Value* buf, NKikimr::NMiniKQL::TPgType* type) {
+ auto& context = Codegen_->GetContext();
+ const auto typeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)type);
+ const auto valType = Type::getInt128Ty(context);
+ const auto funcAddr = ConstantInt::get(Type::getInt64Ty(context), (ui64)&NYql::NCommon::WriteSkiffPgValue);
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), PointerType::getUnqual(valType),
+ PointerType::getUnqual(Type::getInt8Ty(context))
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, elemPtr, buf }, "", Block_);
+ TCodegenContext ctx(Codegen_);
+ ctx.Func = Func_;
+ if constexpr (Flat)
+ ValueCleanup(EValueRepresentation::Any, elemPtr, ctx, Block_);
+ else
+ ValueUnRef(EValueRepresentation::Any, elemPtr, ctx, Block_);
+ }
+
+private:
+ const std::unique_ptr<NCodegen::ICodegen>& Codegen_;
+ Function* Func_;
+ BasicBlock* Block_;
+ ui32 Index_ = 0;
+};
+
+class TYtCodecCgReader : public IYtCodecCgReader {
+public:
+ TYtCodecCgReader(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ const void* cookie)
+ : Codegen_(codegen)
+ , HolderFactory_(holderFactory)
+ {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ // input - pointer to array of UnboxedValue as int128 and instance of buffer, output - void
+ const auto funcType = FunctionType::get(Type::getVoidTy(context), {PointerType::getUnqual(Type::getInt128Ty(context)), PointerType::getUnqual(Type::getInt8Ty(context))}, false);
+ Func_ = cast<Function>(module.getOrInsertFunction((TStringBuilder() << "YtCodecCgReader." << cookie).data(), funcType).getCallee());
+ Block_ = BasicBlock::Create(context, "EntryBlock", Func_);
+ }
+
+ void AddField(NKikimr::NMiniKQL::TType* type, const NKikimr::NUdf::TUnboxedValuePod& defValue, ui64 nativeYtTypeFlags) override {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ auto args = Func_->arg_begin();
+ const auto valuesPtr = &*args++;
+ const auto buf = &*args++;
+ const auto valueType = Type::getInt128Ty(context);
+ const auto indexVal = ConstantInt::get(Type::getInt32Ty(context), Index_);
+ const auto elemPtr = GetElementPtrInst::CreateInBounds(valueType, valuesPtr, { indexVal }, "elemPtr", Block_);
+ const auto velemPtr = CastInst::Create(Instruction::BitCast, elemPtr, PointerType::getUnqual(Type::getInt8Ty(context)), "cast", Block_);
+
+ bool isOptional;
+ auto unwrappedType = UnpackOptional(type, isOptional);
+ if (isOptional || defValue) {
+ const auto just = BasicBlock::Create(context, "just", Func_);
+ const auto nothing = BasicBlock::Create(context, "nothing", Func_);
+ const auto done = BasicBlock::Create(context, "done", Func_);
+
+ const auto optMarker = CallInst::Create(module.getFunction("ReadOptional"), { buf }, "optMarker", Block_);
+ const auto zero = ConstantInt::get(Type::getInt8Ty(context), 0);
+ const auto check = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_NE, optMarker, zero, "exists", Block_);
+ BranchInst::Create(just, nothing, check, Block_);
+
+ {
+ Block_ = just;
+ if (unwrappedType->IsData()) {
+ GenerateData(velemPtr, buf, static_cast<TDataType*>(unwrappedType), nativeYtTypeFlags);
+ } else {
+ GenerateContainer(velemPtr, buf, unwrappedType, true, nativeYtTypeFlags);
+ }
+
+ BranchInst::Create(done, Block_);
+ }
+
+ {
+ Block_ = nothing;
+ BranchInst::Create(done, Block_);
+ }
+
+ Block_ = done;
+ } else {
+ if (unwrappedType->IsData()) {
+ GenerateData(velemPtr, buf, static_cast<TDataType*>(unwrappedType), nativeYtTypeFlags);
+ } else if (unwrappedType->IsPg()) {
+ GeneratePg(velemPtr, buf, static_cast<TPgType*>(unwrappedType));
+ } else {
+ GenerateContainer(velemPtr, buf, unwrappedType, false, nativeYtTypeFlags);
+ }
+ }
+
+ if (defValue) {
+ // load value from elemPtr and use default if that is empty
+ const auto empty = BasicBlock::Create(context, "empty", Func_);
+ const auto ok = BasicBlock::Create(context, "ok", Func_);
+ const auto done = BasicBlock::Create(context, "done", Func_);
+
+ auto value = new LoadInst(valueType, elemPtr, "elem", Block_);
+ const auto zero = ConstantInt::get(valueType, 0ULL);
+ const auto check = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_EQ, value, zero, "empty", Block_);
+ BranchInst::Create(empty, ok, check, Block_);
+
+ {
+ Block_ = empty;
+ // copy def value and ref it
+ ArrayRef<uint64_t> bits((uint64_t*)&defValue, 2);
+ APInt defInt(128, bits);
+ const auto defValData = ConstantInt::get(valueType, defInt);
+ new StoreInst(defValData, elemPtr, Block_);
+ TCodegenContext ctx(Codegen_);
+ ctx.Func = Func_;
+ ValueAddRef(EValueRepresentation::Any, elemPtr, ctx, Block_);
+ BranchInst::Create(done, Block_);
+ }
+
+ {
+ Block_ = ok;
+ BranchInst::Create(done, Block_);
+ }
+
+ Block_ = done;
+ }
+
+ ++Index_;
+ }
+
+ void SkipField(NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags) override {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ auto args = Func_->arg_begin();
+ const auto valuesPtr = &*args++;
+ Y_UNUSED(valuesPtr);
+ const auto buf = &*args++;
+
+ bool isOptional;
+ auto unwrappedType = UnpackOptional(type, isOptional);
+ if (isOptional) {
+ const auto just = BasicBlock::Create(context, "just", Func_);
+ const auto nothing = BasicBlock::Create(context, "nothing", Func_);
+ const auto done = BasicBlock::Create(context, "done", Func_);
+
+ const auto optMarker = CallInst::Create(module.getFunction("ReadOptional"), { buf }, "optMarker", Block_);
+ const auto zero = ConstantInt::get(Type::getInt8Ty(context), 0);
+ const auto check = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_NE, optMarker, zero, "exists", Block_);
+ BranchInst::Create(just, nothing, check, Block_);
+
+ {
+ Block_ = just;
+ GenerateSkip(buf, unwrappedType, nativeYtTypeFlags);
+ BranchInst::Create(done, Block_);
+ }
+
+ {
+ Block_ = nothing;
+ BranchInst::Create(done, Block_);
+ }
+
+ Block_ = done;
+ } else {
+ GenerateSkip(buf, unwrappedType, nativeYtTypeFlags);
+ }
+
+ ++Index_;
+ }
+
+ void SkipOther() override {
+ ++Index_;
+ }
+
+ void SkipVirtual() override {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ auto args = Func_->arg_begin();
+ const auto valuesPtr = &*args++;
+ const auto indexVal = ConstantInt::get(Type::getInt32Ty(context), Index_);
+ const auto elemPtr = GetElementPtrInst::CreateInBounds(Type::getInt128Ty(context), valuesPtr, { indexVal }, "elemPtr", Block_);
+ const auto velemPtr = CastInst::Create(Instruction::BitCast, elemPtr, PointerType::getUnqual(Type::getInt8Ty(context)), "cast", Block_);
+
+ CallInst::Create(module.getFunction("FillZero"), { velemPtr }, "", Block_);
+
+ ++Index_;
+ }
+
+ llvm::Function* Build() override {
+ ReturnInst::Create(Codegen_->GetContext(), Block_);
+ Codegen_->ExportSymbol(Func_);
+ return Func_;
+ }
+
+private:
+ void GenerateData(Value* velemPtr, Value* buf, TDataType* dataType, ui64 nativeYtTypeFlags) {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+ const auto schemeType = dataType->GetSchemeType();
+ switch (schemeType) {
+ case NUdf::TDataType<bool>::Id: {
+ CallInst::Create(module.getFunction("ReadBool"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<i8>::Id: {
+ CallInst::Create(module.getFunction("ReadInt8"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<ui8>::Id: {
+ CallInst::Create(module.getFunction("ReadUint8"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<i16>::Id: {
+ CallInst::Create(module.getFunction("ReadInt16"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TDate>::Id:
+ case NUdf::TDataType<ui16>::Id: {
+ CallInst::Create(module.getFunction("ReadUint16"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<i32>::Id: {
+ CallInst::Create(module.getFunction("ReadInt32"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TDatetime>::Id:
+ case NUdf::TDataType<ui32>::Id: {
+ CallInst::Create(module.getFunction("ReadUint32"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TInterval>::Id:
+ case NUdf::TDataType<i64>::Id: {
+ CallInst::Create(module.getFunction("ReadInt64"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TTimestamp>::Id:
+ case NUdf::TDataType<ui64>::Id: {
+ CallInst::Create(module.getFunction("ReadUint64"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<float>::Id: {
+ CallInst::Create(module.getFunction("ReadFloat"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<double>::Id: {
+ CallInst::Create(module.getFunction("ReadDouble"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TDecimal>::Id: {
+ if (nativeYtTypeFlags & NTCF_DECIMAL) {
+ auto const params = static_cast<TDataDecimalType*>(dataType)->GetParams();
+ if (params.first < 10) {
+ CallInst::Create(module.getFunction("ReadDecimal32"), { buf, velemPtr }, "", Block_);
+ } else if (params.first < 19) {
+ CallInst::Create(module.getFunction("ReadDecimal64"), { buf, velemPtr }, "", Block_);
+ } else {
+ CallInst::Create(module.getFunction("ReadDecimal128"), { buf, velemPtr }, "", Block_);
+ }
+ } else {
+ CallInst::Create(module.getFunction("ReadInt120"), { buf, velemPtr }, "", Block_);
+ }
+ break;
+ }
+
+ case NUdf::TDataType<char*>::Id:
+ case NUdf::TDataType<NUdf::TUtf8>::Id:
+ case NUdf::TDataType<NUdf::TJson>::Id:
+ case NUdf::TDataType<NUdf::TYson>::Id:
+ case NUdf::TDataType<NUdf::TDyNumber>::Id:
+ case NUdf::TDataType<NUdf::TUuid>::Id:
+ case NUdf::TDataType<NUdf::TJsonDocument>::Id: {
+ const auto fnType = FunctionType::get(Type::getVoidTy(context), {
+ PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context))
+ }, false);
+
+ const char* funcName = "YtCodecReadString";
+ if (schemeType == NUdf::TDataType<NUdf::TJsonDocument>::Id) {
+ funcName = "YtCodecReadJsonDocument";
+ }
+
+ const auto func = module.getOrInsertFunction(funcName, fnType);
+ CallInst::Create(func, { buf, velemPtr }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzDate>::Id: {
+ CallInst::Create(module.getFunction("ReadTzDate"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzDatetime>::Id: {
+ CallInst::Create(module.getFunction("ReadTzDatetime"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TTzTimestamp>::Id: {
+ CallInst::Create(module.getFunction("ReadTzTimestamp"), { buf, velemPtr }, "", Block_);
+ break;
+ }
+
+ default:
+ YQL_ENSURE(false, "Unknown data type: " << schemeType);
+ }
+ }
+
+ void GeneratePg(Value* velemPtr, Value* buf, TPgType* type) {
+ auto& context = Codegen_->GetContext();
+ const auto funcAddr = ConstantInt::get(Type::getInt64Ty(context), (ui64)&NCommon::ReadSkiffPgValue);
+ const auto typeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)type);
+
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context))
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, velemPtr, buf}, "", Block_);
+ }
+
+ void GenerateContainer(Value* velemPtr, Value* buf, TType* type, bool wrapOptional, ui64 nativeYtTypeFlags) {
+ auto& context = Codegen_->GetContext();
+
+ const auto funcAddr = ConstantInt::get(Type::getInt64Ty(context), nativeYtTypeFlags ? (ui64)&NCommon::ReadContainerNativeYtValue : (ui64)&NCommon::ReadYsonContainerValue);
+ const auto typeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)type);
+ const auto holderFactoryConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)&HolderFactory_);
+ const auto wrapConst = ConstantInt::get(Type::getInt1Ty(context), wrapOptional);
+ if (nativeYtTypeFlags) {
+ const auto flagsConst = ConstantInt::get(Type::getInt64Ty(context), nativeYtTypeFlags);
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), Type::getInt64Ty(context), Type::getInt64Ty(context), PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context)), Type::getInt1Ty(context)
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, flagsConst, holderFactoryConst, velemPtr, buf, wrapConst }, "", Block_);
+ } else {
+ const auto funType = FunctionType::get(Type::getVoidTy(context), {
+ Type::getInt64Ty(context), Type::getInt64Ty(context), PointerType::getUnqual(Type::getInt8Ty(context)),
+ PointerType::getUnqual(Type::getInt8Ty(context)), Type::getInt1Ty(context)
+ }, false);
+
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, funcAddr, PointerType::getUnqual(funType), "ptr", Block_);
+ CallInst::Create(funType, funcPtr, { typeConst, holderFactoryConst, velemPtr, buf, wrapConst }, "", Block_);
+ }
+ }
+
+ void GenerateSkip(Value* buf, TType* type, ui64 nativeYtTypeFlags) {
+ auto& module = Codegen_->GetModule();
+ auto& context = Codegen_->GetContext();
+
+ if (type->IsData()) {
+ auto schemeType = static_cast<TDataType*>(type)->GetSchemeType();
+ switch (schemeType) {
+ case NUdf::TDataType<bool>::Id: {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(ui8));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ break;
+ }
+
+ case NUdf::TDataType<ui8>::Id:
+ case NUdf::TDataType<ui16>::Id:
+ case NUdf::TDataType<ui32>::Id:
+ case NUdf::TDataType<ui64>::Id:
+ case NUdf::TDataType<NUdf::TDate>::Id:
+ case NUdf::TDataType<NUdf::TDatetime>::Id:
+ case NUdf::TDataType<NUdf::TTimestamp>::Id: {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(ui64));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<i8>::Id:
+ case NUdf::TDataType<i16>::Id:
+ case NUdf::TDataType<i32>::Id:
+ case NUdf::TDataType<i64>::Id:
+ case NUdf::TDataType<NUdf::TInterval>::Id: {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(i64));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<float>::Id:
+ case NUdf::TDataType<double>::Id: {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(double));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TUtf8>::Id:
+ case NUdf::TDataType<char*>::Id:
+ case NUdf::TDataType<NUdf::TJson>::Id:
+ case NUdf::TDataType<NUdf::TYson>::Id:
+ case NUdf::TDataType<NUdf::TUuid>::Id:
+ case NUdf::TDataType<NUdf::TJsonDocument>::Id: {
+ CallInst::Create(module.getFunction("SkipVarData"), { buf }, "", Block_);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TDecimal>::Id: {
+ if (nativeYtTypeFlags & NTCF_DECIMAL) {
+ auto const params = static_cast<TDataDecimalType*>(type)->GetParams();
+ if (params.first < 10) {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(i32));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ } else if (params.first < 19) {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(i64));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ } else {
+ const auto sizeConst = ConstantInt::get(Type::getInt64Ty(context), (ui64)sizeof(NDecimal::TInt128));
+ CallInst::Create(module.getFunction("SkipFixedData"), { buf, sizeConst }, "", Block_);
+ }
+ } else {
+ CallInst::Create(module.getFunction("SkipVarData"), { buf }, "", Block_);
+ }
+ break;
+ }
+
+ default:
+ YQL_ENSURE(false, "Unknown data type: " << schemeType);
+ }
+ } else {
+ ythrow yexception() << "Skip of complex types is not supported";
+ }
+ }
+
+private:
+ const std::unique_ptr<NCodegen::ICodegen>& Codegen_;
+ const NKikimr::NMiniKQL::THolderFactory& HolderFactory_;
+ Function* Func_;
+ BasicBlock* Block_;
+ ui32 Index_ = 0;
+};
+
+TString GetYtCodecBitCode() {
+ auto bitcode = NResource::Find("/llvm_bc/YtCodecFuncs");
+ return bitcode;
+}
+
+void YtCodecAddMappings(NCodegen::ICodegen& codegen) {
+ codegen.AddGlobalMapping("OutputBufFlushThunk", (const void*)&NCommon::OutputBufFlushThunk);
+ codegen.AddGlobalMapping("OutputBufWriteManySlowThunk", (const void*)&NCommon::OutputBufWriteManySlowThunk);
+ codegen.AddGlobalMapping("InputBufReadSlowThunk", (const void*)&NCommon::InputBufReadSlowThunk);
+ codegen.AddGlobalMapping("InputBufReadManySlowThunk", (const void*)&NCommon::InputBufReadManySlowThunk);
+ codegen.AddGlobalMapping("InputBufSkipManySlowThunk", (const void*)&NCommon::InputBufSkipManySlowThunk);
+ codegen.AddGlobalMapping("YtCodecReadString", (const void*)&YtCodecReadString);
+ codegen.AddGlobalMapping("YtCodecWriteJsonDocument", (const void*)&YtCodecWriteJsonDocument);
+ codegen.AddGlobalMapping("YtCodecReadJsonDocument", (const void*)&YtCodecReadJsonDocument);
+ codegen.AddGlobalMapping("ThrowBadDecimal", (const void*)&ThrowBadDecimal);
+}
+
+template<bool Flat>
+THolder<IYtCodecCgWriter> MakeYtCodecCgWriter(const std::unique_ptr<NCodegen::ICodegen>& codegen, const void* cookie) {
+ return MakeHolder<TYtCodecCgWriter<Flat>>(codegen, cookie);
+}
+
+template THolder<IYtCodecCgWriter> MakeYtCodecCgWriter<true>(const std::unique_ptr<NCodegen::ICodegen>& codegen, const void* cookie);
+template THolder<IYtCodecCgWriter> MakeYtCodecCgWriter<false>(const std::unique_ptr<NCodegen::ICodegen>& codegen, const void* cookie);
+
+THolder<IYtCodecCgReader> MakeYtCodecCgReader(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory, const void* cookie) {
+ return MakeHolder<TYtCodecCgReader>(codegen, holderFactory, cookie);
+}
+
+#else
+TString GetYtCodecBitCode() {
+ ythrow yexception() << "No Codegen";
+}
+
+void YtCodecAddMappings(NCodegen::ICodegen& codegen) {
+ Y_UNUSED(codegen);
+ ythrow yexception() << "No Codegen";
+}
+
+THolder<IYtCodecCgWriter> MakeYtCodecCgWriter(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const void* cookie) {
+ Y_UNUSED(codegen);
+ Y_UNUSED(cookie);
+ ythrow yexception() << "No Codegen";
+}
+
+THolder<IYtCodecCgReader> MakeYtCodecCgReader(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ const void* cookie) {
+ Y_UNUSED(codegen);
+ Y_UNUSED(holderFactory);
+ Y_UNUSED(cookie);
+ ythrow yexception() << "No Codegen";
+}
+
+#endif
+
+}
diff --git a/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.h b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.h
new file mode 100644
index 0000000000..d89dc9772b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.h
@@ -0,0 +1,61 @@
+#pragma once
+#include <util/generic/string.h>
+
+namespace llvm {
+ class Function;
+}
+
+namespace NKikimr {
+namespace NMiniKQL {
+ class TType;
+ class THolderFactory;
+}
+
+}
+
+namespace NYql {
+
+namespace NUdf {
+class TUnboxedValue;
+class TUnboxedValuePod;
+}
+
+namespace NCodegen {
+ class ICodegen;
+}
+
+namespace NCommon {
+ class TInputBuf;
+ class TOutputBuf;
+}
+
+TString GetYtCodecBitCode();
+void YtCodecAddMappings(NCodegen::ICodegen& codegen);
+
+class IYtCodecCgWriter {
+public:
+ virtual ~IYtCodecCgWriter() = default;
+ virtual void AddField(NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags) = 0;
+ virtual llvm::Function* Build() = 0;
+};
+
+template<bool Flat>
+THolder<IYtCodecCgWriter> MakeYtCodecCgWriter(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const void* cookie = nullptr);
+
+class IYtCodecCgReader {
+public:
+ virtual ~IYtCodecCgReader() = default;
+ virtual void AddField(NKikimr::NMiniKQL::TType* type, const NYql::NUdf::TUnboxedValuePod& defValue, ui64 nativeYtTypeFlags) = 0;
+ virtual void SkipField(NKikimr::NMiniKQL::TType* type, ui64 nativeYtTypeFlags) = 0;
+ virtual void SkipOther() = 0;
+ virtual void SkipVirtual() = 0;
+ virtual llvm::Function* Build() = 0;
+};
+
+THolder<IYtCodecCgReader> MakeYtCodecCgReader(const std::unique_ptr<NCodegen::ICodegen>& codegen,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory, const void* cookie = nullptr);
+
+extern "C" void ThrowBadDecimal();
+
+}
diff --git a/ydb/library/yql/providers/yt/codec/ut/ya.make b/ydb/library/yql/providers/yt/codec/ut/ya.make
new file mode 100644
index 0000000000..5ab9bc2a89
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/ut/ya.make
@@ -0,0 +1,20 @@
+UNITTEST_FOR(ydb/library/yql/providers/yt/codec)
+
+SRCS(
+ yt_codec_io_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/sql
+ ydb/library/yql/sql/pg_dummy
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/yt/lib/yson_helpers
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/codec/ya.make b/ydb/library/yql/providers/yt/codec/ya.make
new file mode 100644
index 0000000000..ce64d963ad
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/ya.make
@@ -0,0 +1,45 @@
+LIBRARY()
+
+SRCS(
+ yt_codec_io.cpp
+ yt_codec_io.h
+ yt_codec_job.cpp
+ yt_codec_job.h
+ yt_codec.cpp
+ yt_codec.h
+)
+
+PEERDIR(
+ library/cpp/streams/brotli
+ library/cpp/yson
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/io
+ ydb/library/yql/minikql
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/schema/mkql
+ ydb/library/yql/providers/common/schema/parser
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+ ydb/library/yql/providers/yt/lib/skiff
+)
+
+IF (NOT MKQL_DISABLE_CODEGEN)
+ PEERDIR(
+ ydb/library/yql/providers/yt/codec/codegen
+ )
+ELSE()
+ CFLAGS(
+ -DMKQL_DISABLE_CODEGEN
+ )
+ENDIF()
+
+YQL_LAST_ABI_VERSION()
+
+END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec.cpp b/ydb/library/yql/providers/yt/codec/yt_codec.cpp
new file mode 100644
index 0000000000..3d49a5c699
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec.cpp
@@ -0,0 +1,657 @@
+#include "yt_codec.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/schema/parser/yql_type_parser.h>
+#include <ydb/library/yql/providers/common/schema/mkql/yql_mkql_schema.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node_builder.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/hash_set.h>
+#include <util/generic/map.h>
+#include <util/generic/xrange.h>
+#include <util/generic/ylimits.h>
+#include <util/generic/ptr.h>
+#include <util/stream/str.h>
+#include <util/string/builder.h>
+#include <util/digest/numeric.h>
+#include <util/str_stl.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+NYT::TNode ParseYsonSpec(bool inputSpec, const TString& spec) {
+ YQL_ENSURE(!spec.empty());
+
+ NYT::TNode attrs;
+ TStringStream err;
+ if (!NCommon::ParseYson(attrs, spec, err)) {
+ ythrow yexception() << "Invalid "
+ << (inputSpec ? "input" : "output")
+ << " attrs: " << err.Str();
+ }
+
+ return attrs;
+}
+
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx,
+ const TString& inputSpecs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ TType* itemType,
+ const THashSet<TString>& auxColumns,
+ const TString& outSpecs,
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats
+) {
+ NYT::TNode inAttrs, outAttrs;
+
+ if (inputSpecs) {
+ inAttrs = ParseYsonSpec(true, inputSpecs);
+ }
+
+ if (outSpecs) {
+ outAttrs = ParseYsonSpec(false, outSpecs);
+ }
+
+ Init(
+ codecCtx, inAttrs, inputGroups, tableNames,
+ itemType, auxColumns, outAttrs, jobStats
+ );
+}
+
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ TType* itemType,
+ const THashSet<TString>& auxColumns,
+ const NYT::TNode& outAttrs,
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats
+) {
+ if (!inAttrs.IsUndefined()) {
+ InitInput(codecCtx, inAttrs, inputGroups, tableNames, itemType, {}, auxColumns);
+ }
+ if (!outAttrs.IsUndefined()) {
+ InitOutput(codecCtx, outAttrs);
+ }
+ JobStats_ = jobStats;
+}
+
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx,
+ const TString& inputSpecs,
+ const TVector<TString>& tableNames,
+ const TMaybe<TVector<TString>>& columns
+) {
+ Init(codecCtx, ParseYsonSpec(true, inputSpecs), tableNames, columns);
+}
+
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<TString>& tableNames,
+ const TMaybe<TVector<TString>>& columns
+) {
+ InitInput(codecCtx, inAttrs, {}, tableNames, nullptr, columns, {});
+}
+
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx, const TString& outSpecs) {
+ Init(codecCtx, ParseYsonSpec(false, outSpecs));
+}
+
+void TMkqlIOSpecs::Init(NCommon::TCodecContext& codecCtx, const NYT::TNode& outAttrs) {
+ InitOutput(codecCtx, outAttrs);
+}
+
+void TMkqlIOSpecs::SetTableOffsets(const TVector<ui64>& offsets) {
+ YQL_ENSURE(Inputs.size() == offsets.size());
+ TableOffsets = offsets;
+}
+
+void TMkqlIOSpecs::Clear() {
+ JobStats_ = nullptr;
+ Inputs.clear();
+ InputGroups.clear();
+ Outputs.clear();
+ SystemFields_ = TSystemFields();
+}
+
+void TMkqlIOSpecs::LoadSpecInfo(bool inputSpec, const NYT::TNode& attrs, NCommon::TCodecContext& codecCtx, TSpecInfo& info) {
+ YQL_ENSURE(inputSpec || attrs.HasKey(YqlRowSpecAttribute), "Missing mandatory "
+ << TString{YqlRowSpecAttribute}.Quote() << " attribute");
+
+ if (attrs.HasKey(YqlRowSpecAttribute)) {
+ auto& rowSpec = attrs[YqlRowSpecAttribute];
+ TStringStream err;
+ info.Type = NCommon::ParseOrderAwareTypeFromYson(rowSpec[RowSpecAttrType], codecCtx, err);
+ YQL_ENSURE(info.Type, "Invalid row spec type: " << err.Str());
+ if (inputSpec && rowSpec.HasKey(RowSpecAttrStrictSchema)) {
+ info.StrictSchema = rowSpec[RowSpecAttrStrictSchema].IsInt64()
+ ? rowSpec[RowSpecAttrStrictSchema].AsInt64() != 0
+ : NYT::GetBool(rowSpec[RowSpecAttrStrictSchema]);
+
+ if (!info.StrictSchema) {
+ auto stringType = TDataType::Create(NUdf::TDataType<char*>::Id, codecCtx.Env);
+ auto othersDictType = TDictType::Create(stringType, stringType, codecCtx.Env);
+ info.Type = codecCtx.Builder.NewStructType(info.Type, YqlOthersColumnName, othersDictType);
+
+ // Extend input record type by weak fields with 'Yson' type
+ if (rowSpec.HasKey(RowSpecAttrWeakFields)) {
+ auto& weakFields = rowSpec[RowSpecAttrWeakFields].AsList();
+ auto weakType = codecCtx.Builder.NewOptionalType(codecCtx.Builder.NewDataType(NUdf::EDataSlot::Yson));
+ auto structType = AS_TYPE(TStructType, info.Type);
+ for (auto& field: weakFields) {
+ if (!structType->FindMemberIndex(field.AsString())) {
+ info.Type = codecCtx.Builder.NewStructType(info.Type, field.AsString(), weakType);
+ }
+ }
+ }
+ }
+ }
+ if (rowSpec.HasKey(RowSpecAttrDefaultValues)) {
+ YQL_ENSURE(inputSpec, "Unexpected DefaultValues attribute in output spec");
+ for (auto& value : rowSpec[RowSpecAttrDefaultValues].AsMap()) {
+ auto val = NYT::NodeFromYsonString(value.second.AsString());
+ YQL_ENSURE(val.IsString(), "DefaultValues contains non-string value: " << value.second.AsString());
+ info.DefaultValues[value.first] = val.AsString();
+ }
+ }
+
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ info.NativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ info.NativeYtTypeFlags = rowSpec[RowSpecAttrUseNativeYtTypes].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ info.NativeYtTypeFlags = rowSpec[RowSpecAttrUseTypeV2].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+
+ if (rowSpec.HasKey(RowSpecAttrSortedBy)) {
+ auto structType = AS_TYPE(TStructType, info.Type);
+ auto& sortedBy = rowSpec[RowSpecAttrSortedBy].AsList();
+ auto& sortedByType = rowSpec[RowSpecAttrSortedByTypes].AsList();
+ auto& sortedDirections = rowSpec[RowSpecAttrSortDirections].AsList();
+ for (size_t i: xrange(sortedBy.size())) {
+ if (!structType->FindMemberIndex(sortedBy[i].AsString())) {
+ auto fieldType = NCommon::ParseTypeFromYson(sortedByType[i], codecCtx.Builder, err);
+ YQL_ENSURE(fieldType, "Invalid SortedByTypes type: " << err.Str());
+ if (!sortedDirections.empty() && !sortedDirections[i].AsInt64()) {
+ fieldType = codecCtx.Builder.NewDataType(NUdf::EDataSlot::String);
+ }
+ info.AuxColumns.emplace(sortedBy[i].AsString(), fieldType);
+ }
+ }
+ }
+
+ if (rowSpec.HasKey(RowSpecAttrExplicitYson)) {
+ YQL_ENSURE(inputSpec, "Unexpected ExplicitYson attribute in output spec");
+ for (auto& value : rowSpec[RowSpecAttrExplicitYson].AsList()) {
+ YQL_ENSURE(value.IsString(), "ExplicitYson contains non-string value: " << NYT::NodeToYsonString(value));
+ info.ExplicitYson.emplace(value.AsString());
+ }
+ }
+
+ if (inputSpec && AS_TYPE(TStructType, info.Type)->GetMembersCount() == 0) {
+ auto fieldType = codecCtx.Builder.NewDataType(NUdf::EDataSlot::Bool);
+ fieldType = codecCtx.Builder.NewOptionalType(fieldType);
+ info.AuxColumns.emplace("_yql_fake_column", fieldType);
+ }
+ }
+ else {
+ info.Type = codecCtx.Builder.NewEmptyStructType();
+ auto stringType = TDataType::Create(NUdf::TDataType<char*>::Id, codecCtx.Env);
+ for (auto field: YAMR_FIELDS) {
+ info.Type = codecCtx.Builder.NewStructType(info.Type, field, stringType);
+ }
+ }
+ if (attrs.HasKey(YqlDynamicAttribute)) {
+ info.Dynamic = attrs[YqlDynamicAttribute].AsBool();
+ }
+ if (attrs.HasKey(YqlSysColumnPrefix)) {
+ for (auto& n: attrs[YqlSysColumnPrefix].AsList()) {
+ const TString sys = n.AsString();
+ const auto uniq = info.SysColumns.insert(sys).second;
+ YQL_ENSURE(uniq, "Duplicate system column: " << sys);
+ info.Type = codecCtx.Builder.NewStructType(info.Type, TString(YqlSysColumnPrefix).append(sys), TDataType::Create(GetSysColumnTypeId(sys), codecCtx.Env));
+ }
+ }
+}
+
+void TMkqlIOSpecs::InitDecoder(NCommon::TCodecContext& codecCtx,
+ const TSpecInfo& specInfo,
+ const THashMap<TString, ui32>& structColumns,
+ const THashSet<TString>& auxColumns,
+ TDecoderSpec& decoder
+) {
+ TStructType* inStruct = AS_TYPE(TStructType, specInfo.Type);
+ for (auto& col: structColumns) {
+ YQL_ENSURE(inStruct->FindMemberIndex(col.first) || specInfo.AuxColumns.contains(col.first), "Bad column " << col.first);
+ }
+ TStructTypeBuilder extendedStruct(codecCtx.Env);
+
+ for (ui32 index = 0; index < inStruct->GetMembersCount(); ++index) {
+ auto name = inStruct->GetMemberNameStr(index);
+ if (structColumns.contains(name.Str())) {
+ auto type = inStruct->GetMemberType(index);
+ extendedStruct.Add(name.Str(), type);
+ }
+ }
+ for (auto& aux: specInfo.AuxColumns) {
+ if (structColumns.contains(aux.first)) {
+ extendedStruct.Add(aux.first, aux.second);
+ }
+ }
+
+ auto rowType = extendedStruct.Build();
+ decoder.FieldsVec.resize(rowType->GetMembersCount());
+ decoder.DefaultValues.resize(rowType->GetMembersCount());
+ decoder.StructSize = structColumns.size();
+ decoder.NativeYtTypeFlags = specInfo.NativeYtTypeFlags;
+ decoder.Dynamic = specInfo.Dynamic;
+ THashSet<ui32> virtualColumns;
+
+ if (specInfo.SysColumns.contains("path")) {
+ if (auto pos = rowType->FindMemberIndex(YqlSysColumnPath)) {
+ virtualColumns.insert(*pos);
+ decoder.FillSysColumnPath = pos;
+ }
+ }
+ if (specInfo.SysColumns.contains("record")) {
+ if (auto pos = rowType->FindMemberIndex(YqlSysColumnRecord)) {
+ virtualColumns.insert(*pos);
+ decoder.FillSysColumnRecord = pos;
+ }
+ }
+ if (specInfo.SysColumns.contains("index")) {
+ if (auto pos = rowType->FindMemberIndex(YqlSysColumnIndex)) {
+ virtualColumns.insert(*pos);
+ decoder.FillSysColumnIndex = pos;
+ }
+ }
+ if (specInfo.SysColumns.contains("num")) {
+ if (auto pos = rowType->FindMemberIndex(YqlSysColumnNum)) {
+ virtualColumns.insert(*pos);
+ decoder.FillSysColumnNum = pos;
+ }
+ }
+ if (specInfo.SysColumns.contains("keyswitch")) {
+ if (auto pos = rowType->FindMemberIndex(YqlSysColumnKeySwitch)) {
+ virtualColumns.insert(*pos);
+ decoder.FillSysColumnKeySwitch = pos;
+ }
+ }
+
+ THashSet<ui32> usedPos;
+ for (ui32 index = 0; index < rowType->GetMembersCount(); ++index) {
+ auto name = rowType->GetMemberNameStr(index);
+ auto ndx = structColumns.FindPtr(name.Str());
+ YQL_ENSURE(ndx);
+ ui32 pos = *ndx;
+
+ YQL_ENSURE(usedPos.insert(pos).second, "Reused column position");
+
+ auto fieldType = rowType->GetMemberType(index);
+ TMkqlIOSpecs::TDecoderSpec::TDecodeField field;
+ field.StructIndex = pos;
+ field.Type = fieldType;
+ field.Name = name.Str();
+ field.Virtual = virtualColumns.contains(index);
+ field.ExplicitYson = specInfo.ExplicitYson.contains(name.Str());
+ decoder.FieldsVec[pos] = field;
+ decoder.Fields.emplace(field.Name, field);
+ auto it = specInfo.DefaultValues.find(name.Str());
+ if (it != specInfo.DefaultValues.end()) {
+ YQL_ENSURE(fieldType->GetKind() == TType::EKind::Data &&
+ AS_TYPE(TDataType, fieldType)->GetSchemeType() == NUdf::TDataType<char*>::Id,
+ "Default values are supported only for string fields");
+ decoder.DefaultValues[pos] = MakeString(it->second);
+ }
+
+ if (!specInfo.StrictSchema && name.Str() == YqlOthersColumnName) {
+ decoder.OthersStructIndex = pos;
+ bool isTuple;
+ bool encoded;
+ bool useIHash;
+ GetDictionaryKeyTypes(TDataType::Create(NUdf::TDataType<char*>::Id, codecCtx.Env),
+ decoder.OthersKeyTypes, isTuple, encoded, useIHash);
+ }
+ }
+
+ // Store all unused fields with Max<ui32>() position to correctly fill _other or skip aux
+ if (decoder.OthersStructIndex || !auxColumns.empty()) {
+ for (ui32 index = 0; index < inStruct->GetMembersCount(); ++index) {
+ auto name = inStruct->GetMemberNameStr(index);
+ if (!structColumns.contains(name.Str())) {
+ if (decoder.OthersStructIndex || auxColumns.contains(name.Str())) {
+ TMkqlIOSpecs::TDecoderSpec::TDecodeField field;
+ field.StructIndex = Max<ui32>();
+ field.Name = name.Str();
+ field.Type = inStruct->GetMemberType(index);
+ decoder.FieldsVec.push_back(field);
+ decoder.Fields.emplace(name.Str(), field);
+ if (!specInfo.StrictSchema && name.Str() == YqlOthersColumnName && !decoder.OthersStructIndex) {
+ decoder.OthersStructIndex = Max<ui32>();
+ bool isTuple;
+ bool encoded;
+ bool useIHash;
+ GetDictionaryKeyTypes(TDataType::Create(NUdf::TDataType<char*>::Id, codecCtx.Env),
+ decoder.OthersKeyTypes, isTuple, encoded, useIHash);
+ }
+ }
+ }
+ }
+ }
+ decoder.SkiffSize = decoder.FieldsVec.size();
+
+ for (auto& col: specInfo.AuxColumns) {
+ if (!structColumns.contains(col.first)) {
+ TMkqlIOSpecs::TDecoderSpec::TDecodeField field;
+ field.StructIndex = Max<ui32>();
+ field.Name = col.first;
+ field.Type = col.second;
+ decoder.FieldsVec.push_back(field);
+ auto res = decoder.Fields.emplace(col.first, field);
+ YQL_ENSURE(res.second, "Aux column " << col.first << " already added");
+ }
+ }
+}
+
+void TMkqlIOSpecs::PrepareInput(const TVector<ui32>& inputGroups) {
+ InputGroups = inputGroups;
+ THashSet<ui32> groups;
+ for (auto& x : InputGroups) {
+ groups.emplace(x);
+ }
+
+ for (ui32 group = 0; group < groups.size(); ++group) {
+ YQL_ENSURE(groups.contains(group), "Missing group: " << group);
+ }
+}
+
+void TMkqlIOSpecs::InitInput(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ TType* itemType,
+ const TMaybe<TVector<TString>>& columns,
+ const THashSet<TString>& auxColumns
+) {
+ PrepareInput(inputGroups);
+
+ Y_ENSURE(inAttrs.IsMap(), "Expect Map type of input meta attrs, but got type " << inAttrs.GetType());
+ Y_ENSURE(inAttrs.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ InputSpec = inAttrs;
+
+ TVariantType* itemVarType = nullptr;
+ if (!InputGroups.empty()) {
+ YQL_ENSURE(itemType, "Expect non-null item type");
+ YQL_ENSURE(TType::EKind::Variant == itemType->GetKind(), "Expect Variant item type, but got " << itemType->GetKindAsStr());
+ itemVarType = static_cast<TVariantType*>(itemType);
+ }
+
+ auto& inputSpecs = inAttrs[YqlIOSpecTables].AsList();
+ Inputs.resize(inputSpecs.size());
+ YQL_ENSURE(InputGroups.empty() || InputGroups.size() == Inputs.size());
+
+ bool useCommonColumns = true;
+ THashMap<TString, ui32> structColumns;
+ if (columns.Defined()) {
+ for (size_t i = 0; i < columns->size(); ++i) {
+ structColumns.insert({columns->at(i), (ui32)i});
+ }
+ }
+ else if (itemType && InputGroups.empty()) {
+ TStructType* itemTypeStruct = AS_TYPE(TStructType, itemType);
+ for (ui32 index = 0; index < itemTypeStruct->GetMembersCount(); ++index) {
+ structColumns.emplace(itemTypeStruct->GetMemberName(index), index);
+ }
+ }
+ else {
+ useCommonColumns = false;
+ }
+
+ THashMap<TString, TSpecInfo> specInfoRegistry;
+
+ for (size_t inputIndex = 0; inputIndex < inputSpecs.size(); ++inputIndex) {
+ try {
+ auto group = InputGroups.empty() ? 0 : inputGroups.at(inputIndex);
+ TSpecInfo localSpecInfo;
+ TSpecInfo* specInfo = &localSpecInfo;
+ TString decoderRefName = TStringBuilder() << "_internal" << inputIndex;
+ if (inputSpecs[inputIndex].IsString()) {
+ auto refName = inputSpecs[inputIndex].AsString();
+ decoderRefName = refName;
+ if (auto p = specInfoRegistry.FindPtr(refName)) {
+ specInfo = p;
+ } else {
+ Y_ENSURE(inAttrs.HasKey(YqlIOSpecRegistry) && inAttrs[YqlIOSpecRegistry].HasKey(refName), "Bad input registry reference: " << refName);
+ specInfo = &specInfoRegistry[refName];
+ LoadSpecInfo(true, inAttrs[YqlIOSpecRegistry][refName], codecCtx, *specInfo);
+ }
+ } else {
+ LoadSpecInfo(true, inputSpecs[inputIndex], codecCtx, localSpecInfo);
+ }
+
+ TStructType* inStruct = AS_TYPE(TStructType, specInfo->Type);
+ if (itemType) { // itemType may be null for operations without graph (TopSort f.e.)
+ inStruct = itemVarType
+ ? AS_TYPE(TStructType, itemVarType->GetAlternativeType(group))
+ : AS_TYPE(TStructType, itemType);
+ }
+
+ if (!useCommonColumns) {
+ structColumns.clear();
+ for (ui32 index = 0; index < inStruct->GetMembersCount(); ++index) {
+ auto col = inStruct->GetMemberName(index);
+ structColumns.emplace(col, index);
+ decoderRefName.append(';').append(ToString(col.length())).append(':').append(col); // Make decoder unique by input type and set of used columns
+ }
+ }
+ if (specInfo->Dynamic) {
+ decoderRefName.append(";dynamic");
+ }
+
+ if (auto p = Decoders.FindPtr(decoderRefName)) {
+ // Reuse already initialized decoder for the same schema and column set
+ Inputs[inputIndex] = p;
+ }
+ else {
+ TDecoderSpec* decoder = &Decoders[decoderRefName];
+ InitDecoder(codecCtx, *specInfo, structColumns, auxColumns, *decoder);
+ Inputs[inputIndex] = decoder;
+ }
+ }
+ catch (const yexception& e) {
+ ythrow yexception() << "Invalid decoder spec for " << inputIndex << " input: " << e;
+ }
+ }
+
+ if (!tableNames.empty()) {
+ YQL_ENSURE(tableNames.size() == Inputs.size());
+ for (auto& name: tableNames) {
+ TableNames.push_back(MakeString(name));
+ }
+ } else {
+ TableNames.resize(Inputs.size(), NUdf::TUnboxedValuePod::Zero());
+ }
+ TableOffsets.resize(Inputs.size(), 0ull);
+}
+
+void TMkqlIOSpecs::InitOutput(NCommon::TCodecContext& codecCtx, const NYT::TNode& outAttrs) {
+ Y_ENSURE(outAttrs.IsMap(), "Expect Map type of output meta attrs, but got type " << outAttrs.GetType());
+ Y_ENSURE(outAttrs.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ OutputSpec = outAttrs;
+
+ auto& outputSpecs = outAttrs[YqlIOSpecTables].AsList();
+ Outputs.resize(outputSpecs.size());
+
+ THashMap<TString, TSpecInfo> specInfoRegistry;
+ THashMap<TString, TStructType*> outTypeRegistry;
+
+ for (size_t i = 0; i < outputSpecs.size(); ++i) {
+ try {
+ TSpecInfo localSpecInfo;
+ TSpecInfo* specInfo = &localSpecInfo;
+ TString refName;
+ if (outputSpecs[i].IsString()) {
+ refName = outputSpecs[i].AsString();
+
+ if (auto p = specInfoRegistry.FindPtr(refName)) {
+ specInfo = p;
+ }
+ else {
+ Y_ENSURE(outAttrs.HasKey(YqlIOSpecRegistry) && outAttrs[YqlIOSpecRegistry].HasKey(refName), "Bad output registry reference: " << refName);
+ specInfo = &specInfoRegistry[refName];
+ LoadSpecInfo(false, outAttrs[YqlIOSpecRegistry][refName], codecCtx, *specInfo);
+ }
+
+ if (auto p = outTypeRegistry.FindPtr(refName)) {
+ Outputs[i].RowType = *p;
+ Outputs[i].NativeYtTypeFlags = specInfo->NativeYtTypeFlags;
+ continue;
+ }
+ } else {
+ LoadSpecInfo(false, outputSpecs[i], codecCtx, localSpecInfo);
+ }
+
+ auto structType = AS_TYPE(TStructType, specInfo->Type);
+ // Extend struct by aux columns
+ for (auto& col: specInfo->AuxColumns) {
+ structType = AS_TYPE(TStructType, codecCtx.Builder.NewStructType(structType, col.first, col.second));
+ }
+ if (refName) {
+ outTypeRegistry[refName] = structType;
+ }
+
+ Outputs[i].RowType = structType;
+ Outputs[i].NativeYtTypeFlags = specInfo->NativeYtTypeFlags;
+ } catch (const yexception& e) {
+ ythrow yexception() << "Invalid encoder spec for " << i << " output: " << e;
+ }
+ }
+}
+
+NYT::TFormat TMkqlIOSpecs::MakeOutputFormat() const {
+ if (!UseSkiff_ || Outputs.empty()) {
+ return NYT::TFormat::YsonBinary();
+ }
+
+ YQL_ENSURE(!OutputSpec.IsUndefined());
+ NYT::TNode formatConfig = TablesSpecToOutputSkiff(OutputSpec);
+ return NYT::TFormat(formatConfig);
+}
+
+NYT::TFormat TMkqlIOSpecs::MakeOutputFormat(size_t tableIndex) const {
+ Y_ENSURE(tableIndex < Outputs.size(), "Invalid output table index: " << tableIndex);
+
+ if (!UseSkiff_) {
+ return NYT::TFormat::YsonBinary();
+ }
+
+ YQL_ENSURE(!OutputSpec.IsUndefined());
+ NYT::TNode formatConfig = SingleTableSpecToOutputSkiff(OutputSpec, tableIndex);
+ return NYT::TFormat(formatConfig);
+}
+
+NYT::TFormat TMkqlIOSpecs::MakeInputFormat(const THashSet<TString>& auxColumns) const {
+ if (!UseSkiff_ || Inputs.empty()) {
+ return NYT::TFormat::YsonBinary();
+ }
+
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+ auto& skiffConfig = formatConfig.Attributes();
+ auto schemas = NYT::TNode::CreateList();
+ THashMap<const TDecoderSpec*, size_t> uniqDecoders;
+ for (size_t i = 0; i < Inputs.size(); ++i) {
+ auto input = Inputs[i];
+ size_t schemaId = uniqDecoders.size();
+ auto p = uniqDecoders.emplace(input, schemaId);
+ if (p.second) {
+ THashMap<TString, ui32> structColumns;
+ for (size_t f = 0; f < input->StructSize; ++f) {
+ structColumns[input->FieldsVec[f].Name] = f;
+ }
+
+ NYT::TNode tableSchema = SingleTableSpecToInputSkiffSchema(InputSpec, i, structColumns, auxColumns,
+ SystemFields_.HasFlags(ESystemField::RowIndex),
+ SystemFields_.HasFlags(ESystemField::RangeIndex),
+ SystemFields_.HasFlags(ESystemField::KeySwitch));
+ skiffConfig["skiff_schema_registry"][TStringBuilder() << "table" << schemaId] = tableSchema;
+ }
+ else {
+ schemaId = p.first->second;
+ }
+
+ schemas.Add(NYT::TNode(TStringBuilder() << "$table" << schemaId));
+ }
+
+ skiffConfig["table_skiff_schemas"] = schemas;
+ //Cerr << NYT::NodeToYsonString(skiffConfig) << Endl;
+ return NYT::TFormat(formatConfig);
+}
+
+NYT::TFormat TMkqlIOSpecs::MakeInputFormat(size_t tableIndex) const {
+ Y_ENSURE(tableIndex < Inputs.size(), "Invalid output table index: " << tableIndex);
+
+ if (!UseSkiff_) {
+ return NYT::TFormat::YsonBinary();
+ }
+
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+
+ auto input = Inputs[tableIndex];
+ THashMap<TString, ui32> structColumns;
+ for (size_t f = 0; f < input->StructSize; ++f) {
+ structColumns[input->FieldsVec[f].Name] = f;
+ }
+
+ NYT::TNode tableSchema = SingleTableSpecToInputSkiffSchema(InputSpec, tableIndex, structColumns, {},
+ SystemFields_.HasFlags(ESystemField::RowIndex),
+ SystemFields_.HasFlags(ESystemField::RangeIndex),
+ SystemFields_.HasFlags(ESystemField::KeySwitch));
+
+ auto& skiffConfig = formatConfig.Attributes();
+ skiffConfig["table_skiff_schemas"] = NYT::TNode::CreateList().Add(std::move(tableSchema));
+
+ return NYT::TFormat(formatConfig);
+}
+
+TMkqlIOCache::TMkqlIOCache(const TMkqlIOSpecs& specs, const THolderFactory& holderFactory)
+ : Specs_(specs)
+ , HolderFactory(holderFactory)
+{
+ THashSet<ui32> groups;
+ for (auto& x : Specs_.InputGroups) {
+ groups.emplace(x);
+ }
+
+ for (ui32 i = 0; i < Max<ui32>(1, groups.size()); ++i) {
+ RowCache_.emplace_back(MakeHolder<TPlainContainerCache>());
+ }
+
+ DecoderCache_.resize(Specs_.Inputs.size());
+ for (size_t i: xrange(Specs_.Inputs.size())) {
+ DecoderCache_[i].LastFields_.reserve(Specs_.Inputs[i]->FieldsVec.size());
+ for (auto& field: Specs_.Inputs[i]->FieldsVec) {
+ DecoderCache_[i].LastFields_.push_back(&field);
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec.h b/ydb/library/yql/providers/yt/codec/yt_codec.h
new file mode 100644
index 0000000000..2ad25cf12c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec.h
@@ -0,0 +1,246 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/maybe.h>
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/flags.h>
+
+namespace NYql {
+
+class TMkqlIOSpecs {
+public:
+ enum class ESystemField: ui32 {
+ KeySwitch = 1 << 0,
+ RowIndex = 1 << 1,
+ RangeIndex = 1 << 2,
+ };
+
+ Y_DECLARE_FLAGS(TSystemFields, ESystemField);
+
+ struct TSpecInfo {
+ NKikimr::NMiniKQL::TType* Type = nullptr;
+ bool StrictSchema = true;
+ THashMap<TString, TString> DefaultValues;
+ THashMap<TString, NKikimr::NMiniKQL::TType*> AuxColumns;
+ ui64 NativeYtTypeFlags = 0;
+ bool Dynamic = false;
+ THashSet<TString> SysColumns;
+ THashSet<TString> ExplicitYson;
+ };
+
+ struct TDecoderSpec {
+ struct TDecodeField {
+ TString Name;
+ ui32 StructIndex = 0;
+ NKikimr::NMiniKQL::TType* Type = nullptr;
+ bool Virtual = false;
+ bool ExplicitYson = false;
+ };
+
+ TMaybe<ui32> OthersStructIndex; // filled if scheme is not strict
+ NKikimr::NMiniKQL::TKeyTypes OthersKeyTypes;
+ THashMap<TString, TDecodeField> Fields;
+ TVector<TDecodeField> FieldsVec;
+ ui32 StructSize = 0; // Number of visible columns after decoding (excludes all aux columns)
+ ui32 SkiffSize = 0; // Number of columns expected by skiff (includes all visible columns and explicitly requested aux columns)
+ TVector<NKikimr::NUdf::TUnboxedValue> DefaultValues;
+ ui64 NativeYtTypeFlags = 0;
+ bool Dynamic = false;
+ TMaybe<ui32> FillSysColumnPath;
+ TMaybe<ui32> FillSysColumnRecord;
+ TMaybe<ui32> FillSysColumnIndex;
+ TMaybe<ui32> FillSysColumnNum;
+ TMaybe<ui32> FillSysColumnKeySwitch;
+ };
+
+ struct TEncoderSpec {
+ NKikimr::NMiniKQL::TStructType* RowType = nullptr;
+ ui64 NativeYtTypeFlags = 0;
+ };
+
+public:
+ // Job specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const TString& inputSpecs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ NKikimr::NMiniKQL::TType* itemType,
+ const THashSet<TString>& auxColumns,
+ const TString& outSpecs,
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats = nullptr
+ );
+
+ // Job specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ NKikimr::NMiniKQL::TType* itemType,
+ const THashSet<TString>& auxColumns,
+ const NYT::TNode& outAttrs,
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats = nullptr
+ );
+
+ // Pull specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const TString& inputSpecs,
+ const TVector<TString>& tableNames,
+ const TMaybe<TVector<TString>>& columns // Use Nothing to select all columns in original order
+ );
+
+ // Pull specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<TString>& tableNames,
+ const TMaybe<TVector<TString>>& columns // Use Nothing to select all columns in original order
+ );
+
+ // Fill specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const TString& outSpecs
+ );
+
+ // Fill specific initialization
+ void Init(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& outAttrs
+ );
+
+ void SetUseSkiff(const TString& optLLVM, TSystemFields sysFields = {}) {
+ UseSkiff_ = true;
+ OptLLVM_ = optLLVM;
+ SystemFields_ = sysFields;
+ }
+
+ void SetTableOffsets(const TVector<ui64>& offsets);
+
+ void Clear();
+
+ static void LoadSpecInfo(bool inputSpec, const NYT::TNode& attrs, NCommon::TCodecContext& codecCtx, TSpecInfo& info);
+
+ NYT::TFormat MakeInputFormat(const THashSet<TString>& auxColumns) const; // uses Inputs
+ NYT::TFormat MakeInputFormat(size_t tableIndex) const; // uses Inputs
+ NYT::TFormat MakeOutputFormat() const; // uses Outputs
+ NYT::TFormat MakeOutputFormat(size_t tableIndex) const; // uses Outputs
+
+public:
+ bool UseSkiff_ = false;
+ TString OptLLVM_;
+ TSystemFields SystemFields_;
+
+ NKikimr::NMiniKQL::IStatsRegistry* JobStats_ = nullptr;
+ THashMap<TString, TDecoderSpec> Decoders;
+ TVector<const TDecoderSpec*> Inputs;
+ TVector<ui32> InputGroups; // translation of tableindex->index of Inputs
+ TVector<TEncoderSpec> Outputs;
+
+ NYT::TNode InputSpec;
+ NYT::TNode OutputSpec;
+ TVector<NKikimr::NUdf::TUnboxedValue> TableNames;
+ TVector<ui64> TableOffsets;
+
+protected:
+ void PrepareInput(const TVector<ui32>& inputGroups);
+
+ void InitInput(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& inAttrs,
+ const TVector<ui32>& inputGroups,
+ const TVector<TString>& tableNames,
+ NKikimr::NMiniKQL::TType* itemType,
+ const TMaybe<TVector<TString>>& columns,
+ const THashSet<TString>& auxColumns
+ );
+
+ void InitDecoder(NCommon::TCodecContext& codecCtx,
+ const TSpecInfo& specInfo,
+ const THashMap<TString, ui32>& structColumns,
+ const THashSet<TString>& auxColumns,
+ TDecoderSpec& decoder
+ );
+
+ void InitOutput(NCommon::TCodecContext& codecCtx,
+ const NYT::TNode& outAttrs
+ );
+};
+
+Y_DECLARE_OPERATORS_FOR_FLAGS(TMkqlIOSpecs::TSystemFields);
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TMkqlIOCache {
+public:
+ TMkqlIOCache(const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory);
+
+ NKikimr::NUdf::TUnboxedValue NewRow(size_t tableIndex, NKikimr::NUdf::TUnboxedValue*& items) {
+ const auto group = Specs_.InputGroups.empty() ? 0 : Specs_.InputGroups[tableIndex];
+ return RowCache_[group]->NewArray(HolderFactory, Specs_.Inputs[tableIndex]->StructSize, items);
+ }
+
+ const TMkqlIOSpecs& GetSpecs() const {
+ return Specs_;
+ }
+ const NKikimr::NMiniKQL::THolderFactory& GetHolderFactory() {
+ return HolderFactory;
+ }
+
+ ui32 GetMaxOthersFields(size_t tableIndex) const {
+ return DecoderCache_.at(tableIndex).MaxOthersFields_;
+ }
+
+ void UpdateMaxOthersFields(size_t tableIndex, ui32 maxOthersFields) {
+ DecoderCache_[tableIndex].MaxOthersFields_ = Max<ui32>(DecoderCache_.at(tableIndex).MaxOthersFields_, maxOthersFields);
+ }
+
+ TVector<const TMkqlIOSpecs::TDecoderSpec::TDecodeField*>& GetLastFields(size_t tableIndex) {
+ return DecoderCache_.at(tableIndex).LastFields_;
+ }
+
+
+private:
+ const TMkqlIOSpecs& Specs_;
+ const NKikimr::NMiniKQL::THolderFactory& HolderFactory;
+ TVector<THolder<NKikimr::NMiniKQL::TPlainContainerCache>> RowCache_;
+
+ struct TDecoderCache {
+ ui32 MaxOthersFields_ = 0;
+ TVector<const TMkqlIOSpecs::TDecoderSpec::TDecodeField*> LastFields_;
+ };
+ TVector<TDecoderCache> DecoderCache_;
+};
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class IMkqlReaderImpl : public NYT::IReaderImplBase {
+public:
+ virtual ~IMkqlReaderImpl() = default;
+ virtual NKikimr::NUdf::TUnboxedValue GetRow() const = 0;
+};
+
+using IMkqlReaderImplPtr = TIntrusivePtr<IMkqlReaderImpl>;
+
+class IMkqlWriterImpl : public TThrRefBase {
+public:
+ virtual ~IMkqlWriterImpl() = default;
+ virtual void AddRow(const NUdf::TUnboxedValuePod row) = 0;
+ virtual void AddFlatRow(const NUdf::TUnboxedValuePod* row) = 0;
+ virtual void Finish() = 0;
+ virtual void Abort() = 0;
+};
+
+using IMkqlWriterImplPtr = TIntrusivePtr<IMkqlWriterImpl>;
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp b/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
new file mode 100644
index 0000000000..cadc05d90a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_io.cpp
@@ -0,0 +1,2263 @@
+#include "yt_codec_io.h"
+
+#include <ydb/library/yql/providers/common/codec/yql_restricted_yson.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#ifndef MKQL_DISABLE_CODEGEN
+#include <ydb/library/yql/providers/yt/codec/codegen/yt_codec_cg.h>
+#endif
+#include <ydb/library/yql/minikql/mkql_alloc.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/aligned_page_pool.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/swap_bytes.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/public/decimal/yql_decimal_serialize.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/detail.h>
+#include <library/cpp/yson/varint.h>
+#include <library/cpp/streams/brotli/brotli.h>
+
+#include <util/generic/yexception.h>
+#include <util/generic/string.h>
+#include <util/generic/queue.h>
+#include <util/generic/xrange.h>
+#include <util/system/guard.h>
+#include <util/system/yassert.h>
+#include <util/system/condvar.h>
+#include <util/system/mutex.h>
+#include <util/system/thread.h>
+#include <util/stream/file.h>
+
+#include <functional>
+
+namespace NYql {
+
+using namespace NCommon;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NYT;
+using namespace ::NYson::NDetail;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TStatKey InputDecodeTime("Job_InputDecodeTime", true);
+TStatKey InputReadTime("Job_InputReadTime", true);
+TStatKey OutputEncodeTime("Job_OutputEncodeTime", true);
+TStatKey OutputWriteTime("Job_OutputWriteTime", true);
+
+struct TBlock : public TThrRefBase {
+ size_t Avail_ = 0;
+ std::optional<size_t> LastRecordBoundary_;
+ const size_t Capacity_;
+ TArrayHolder<char> Buffer_;
+ TMaybe<yexception> Error_;
+
+ explicit TBlock(size_t capacity)
+ : Capacity_(capacity)
+ , Buffer_(new char[Capacity_])
+ {}
+};
+
+using TBlockPtr = TIntrusivePtr<TBlock>;
+
+struct TBufferManager {
+ const size_t BlockCount_;
+ const size_t BlockSize_;
+ TQueue<TBlockPtr> FreeBlocks_;
+ TQueue<TBlockPtr> FilledBlocks_;
+
+ TBufferManager(size_t blockCount, size_t blockSize)
+ : BlockCount_(blockCount)
+ , BlockSize_(blockSize)
+ {
+ // mark all blocks as free
+ for (size_t i = 0; i < BlockCount_; ++i) {
+ FreeBlocks_.push(MakeIntrusive<TBlock>(BlockSize_));
+ }
+ }
+};
+
+size_t LoadWithTimeout(IInputStream& source, char* buffer_in, size_t len, const TMaybe<TInstant>& deadline) {
+ char* buf = buffer_in;
+ while (len) {
+ const size_t ret = source.Read(buf, len);
+
+ buf += ret;
+ len -= ret;
+
+ if (ret == 0) {
+ break;
+ }
+
+ if (deadline && Now() > deadline) {
+ ythrow TTimeoutException() << "Read timeout";
+ }
+ }
+
+ return buf - buffer_in;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncBlockReader: public IBlockReader {
+public:
+ TAsyncBlockReader(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize)
+ : Source_(source)
+ , BufMan_(blockCount, blockSize)
+ , Thread_(TThread::TParams(&ThreadFunc, this).SetName("reader"))
+ {
+ Thread_.Start();
+ }
+
+ ~TAsyncBlockReader() {
+ with_lock(Mutex_) {
+ Shutdown_ = true;
+ CondRestore_.Signal();
+ CondFill_.Signal();
+ }
+
+ Thread_.Join();
+ }
+
+ void SetDeadline(TInstant deadline) override {
+ Deadline_ = deadline;
+ }
+
+ std::pair<const char*, const char*> NextFilledBlock() override {
+ TBlockPtr firstBlock;
+ with_lock(Mutex_) {
+ while (BufMan_.FilledBlocks_.empty()) {
+ CondRead_.WaitI(Mutex_);
+ }
+
+ firstBlock = BufMan_.FilledBlocks_.front();
+ }
+
+ if (firstBlock->Error_.Defined()) {
+ YQL_LOG_CTX_THROW *firstBlock->Error_;
+ }
+ return { firstBlock->Buffer_.Get(), firstBlock->Buffer_.Get() + firstBlock->Avail_ };
+ }
+
+ void ReturnBlock() override {
+ auto guard = Guard(Mutex_);
+
+ auto firstBlock = BufMan_.FilledBlocks_.front();
+ firstBlock->Avail_ = 0;
+ firstBlock->Error_.Clear();
+ BufMan_.FilledBlocks_.pop();
+ BufMan_.FreeBlocks_.push(firstBlock);
+
+ CondFill_.Signal();
+ }
+
+ bool Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex) override {
+ auto guard = Guard(Mutex_);
+
+ // Clean all filled blocks
+ while (!BufMan_.FilledBlocks_.empty()) {
+ auto block = BufMan_.FilledBlocks_.front();
+ block->Avail_ = 0;
+ block->Error_.Clear();
+ BufMan_.FilledBlocks_.pop();
+ BufMan_.FreeBlocks_.push(block);
+ }
+
+ if (!Source_.Retry(rangeIndex, rowIndex)) {
+ Shutdown_ = true;
+ CondRestore_.Signal();
+ return false;
+ }
+ CondRestore_.Signal();
+ return true;
+ }
+
+private:
+ static void* ThreadFunc(void* param) {
+ static_cast<TAsyncBlockReader*>(param)->Fetch();
+ return nullptr;
+ }
+
+ void Fetch() {
+ for (;;) {
+ TBlockPtr freeBlock;
+ with_lock(Mutex_) {
+ while (BufMan_.FreeBlocks_.empty() && !Shutdown_) {
+ CondFill_.WaitI(Mutex_);
+ }
+
+ if (Shutdown_) {
+ return;
+ }
+
+ freeBlock = BufMan_.FreeBlocks_.front();
+ BufMan_.FreeBlocks_.pop();
+ }
+
+ bool hasError = false;
+ try {
+ freeBlock->Avail_ = LoadWithTimeout(Source_, freeBlock->Buffer_.Get(), freeBlock->Capacity_, Deadline_);
+ }
+ catch (yexception& e) {
+ freeBlock->Error_ = std::move(e);
+ hasError = true;
+ }
+ catch (...) {
+ freeBlock->Error_ = (yexception() << CurrentExceptionMessage());
+ hasError = true;
+ }
+
+ const bool lastBlock = !freeBlock->Avail_;
+ with_lock(Mutex_) {
+ BufMan_.FilledBlocks_.push(freeBlock);
+ CondRead_.Signal();
+ if (hasError) {
+ CondRestore_.WaitI(Mutex_);
+ if (Shutdown_) {
+ return;
+ }
+ } else if (lastBlock) {
+ break;
+ }
+ }
+ }
+ }
+
+private:
+ TMaybe<TInstant> Deadline_;
+ NYT::TRawTableReader& Source_;
+ TBufferManager BufMan_;
+ TMutex Mutex_;
+ TCondVar CondFill_;
+ TCondVar CondRead_;
+ TCondVar CondRestore_;
+ bool Shutdown_ = false;
+ TThread Thread_;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TSyncBlockReader: public IBlockReader {
+public:
+ TSyncBlockReader(NYT::TRawTableReader& source, size_t blockSize)
+ : Source_(source)
+ , Block_(blockSize)
+ {
+ }
+
+ ~TSyncBlockReader() = default;
+
+ void SetDeadline(TInstant deadline) override {
+ Deadline_ = deadline;
+ }
+
+ std::pair<const char*, const char*> NextFilledBlock() override {
+ Block_.Avail_ = LoadWithTimeout(Source_, Block_.Buffer_.Get(), Block_.Capacity_, Deadline_);
+ return { Block_.Buffer_.Get(), Block_.Buffer_.Get() + Block_.Avail_ };
+ }
+
+ void ReturnBlock() override {
+ Block_.Avail_ = 0;
+ }
+
+ bool Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex) override {
+ Block_.Avail_ = 0;
+ return Source_.Retry(rangeIndex, rowIndex);
+ }
+
+private:
+ NYT::TRawTableReader& Source_;
+ TBlock Block_;
+ TMaybe<TInstant> Deadline_;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+THolder<IBlockReader> MakeBlockReader(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize) {
+ THolder<IBlockReader> res;
+ if (blockCount > 1) {
+ res.Reset(new TAsyncBlockReader(source, blockCount, blockSize));
+ } else {
+ res.Reset(new TSyncBlockReader(source, blockSize));
+ }
+ return res;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncBlockWriter: public IBlockWriter {
+public:
+ TAsyncBlockWriter(IOutputStream& target, size_t blockCount, size_t blockSize)
+ : Target_(target)
+ , BufMan_(blockCount, blockSize)
+ , Thread_(TThread::TParams(&ThreadFunc, this).SetName("writer"))
+ {
+ Thread_.Start();
+ }
+
+ ~TAsyncBlockWriter() {
+ Stop();
+ }
+
+ void SetRecordBoundaryCallback(std::function<void()> callback) override {
+ OnRecordBoundaryCallback_ = std::move(callback);
+ }
+
+ std::pair<char*, char*> NextEmptyBlock() override {
+ TBlockPtr firstBlock;
+ with_lock(Mutex_) {
+ while (BufMan_.FreeBlocks_.empty()) {
+ CondFill_.WaitI(Mutex_);
+ }
+ if (Error_.Defined()) {
+ YQL_LOG_CTX_THROW *Error_;
+ }
+
+ firstBlock = BufMan_.FreeBlocks_.front();
+ }
+
+ firstBlock->Avail_ = 0;
+ return { firstBlock->Buffer_.Get(), firstBlock->Buffer_.Get() + firstBlock->Capacity_ };
+ }
+
+ void ReturnBlock(size_t avail, std::optional<size_t> lastRecordBoundary) override {
+ if (!avail) {
+ return;
+ }
+
+ with_lock(Mutex_) {
+ auto firstBlock = BufMan_.FreeBlocks_.front();
+ firstBlock->Avail_ = avail;
+ firstBlock->LastRecordBoundary_ = lastRecordBoundary;
+ BufMan_.FilledBlocks_.push(firstBlock);
+ BufMan_.FreeBlocks_.pop();
+ }
+
+ CondWrite_.Signal();
+ }
+
+ void Finish() override {
+ Stop();
+ if (Error_.Defined()) {
+ YQL_LOG_CTX_THROW *Error_;
+ }
+ }
+
+private:
+ static void* ThreadFunc(void* param) {
+ static_cast<TAsyncBlockWriter*>(param)->Write();
+ return nullptr;
+ }
+
+ void Stop() {
+ with_lock(Mutex_) {
+ Shutdown_ = true;
+ }
+
+ CondWrite_.Signal();
+ Thread_.Join();
+ }
+
+ void Write() {
+ for (;;) {
+ TBlockPtr firstBlock;
+ bool needShutdown = false;
+ with_lock(Mutex_) {
+ while (BufMan_.FilledBlocks_.empty()) {
+ if (Shutdown_) {
+ needShutdown = true;
+ break;
+ }
+
+ CondWrite_.WaitI(Mutex_);
+ }
+
+ if (!needShutdown) {
+ firstBlock = BufMan_.FilledBlocks_.front();
+ BufMan_.FilledBlocks_.pop();
+ }
+ }
+
+ if (needShutdown) {
+ try {
+ Target_.Finish();
+ }
+ catch (yexception& e) {
+ auto guard = Guard(Mutex_);
+ Error_ = std::move(e);
+ }
+ catch (...) {
+ auto guard = Guard(Mutex_);
+ Error_ = (yexception() << CurrentExceptionMessage());
+ }
+ return;
+ }
+
+ try {
+ Target_.Write(firstBlock->Buffer_.Get(), firstBlock->LastRecordBoundary_.value_or(firstBlock->Avail_));
+ if (firstBlock->LastRecordBoundary_) {
+ if (OnRecordBoundaryCallback_) {
+ OnRecordBoundaryCallback_();
+ }
+ if (firstBlock->Avail_ > *firstBlock->LastRecordBoundary_) {
+ Target_.Write(firstBlock->Buffer_.Get() + *firstBlock->LastRecordBoundary_, firstBlock->Avail_ - *firstBlock->LastRecordBoundary_);
+ }
+ }
+
+ }
+ catch (yexception& e) {
+ auto guard = Guard(Mutex_);
+ Error_ = std::move(e);
+ }
+ catch (...) {
+ auto guard = Guard(Mutex_);
+ Error_ = (yexception() << CurrentExceptionMessage());
+ }
+
+ with_lock(Mutex_) {
+ firstBlock->Avail_ = 0;
+ BufMan_.FreeBlocks_.push(firstBlock);
+ }
+
+ CondFill_.Signal();
+ }
+ }
+
+private:
+ IOutputStream& Target_;
+ TBufferManager BufMan_;
+ TMutex Mutex_;
+ TCondVar CondFill_;
+ TCondVar CondWrite_;
+ bool Shutdown_ = false;
+ TThread Thread_;
+ TMaybe<yexception> Error_;
+ std::function<void()> OnRecordBoundaryCallback_;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TSyncBlockWriter: public IBlockWriter {
+public:
+ TSyncBlockWriter(IOutputStream& target, size_t blockSize)
+ : Target_(target)
+ , Block_(blockSize)
+ {
+ }
+
+ void SetRecordBoundaryCallback(std::function<void()> callback) override {
+ OnRecordBoundaryCallback_ = std::move(callback);
+ }
+
+ std::pair<char*, char*> NextEmptyBlock() override {
+ Block_.Avail_ = 0;
+ return { Block_.Buffer_.Get(), Block_.Buffer_.Get() + Block_.Capacity_ };
+ }
+
+ void ReturnBlock(size_t avail, std::optional<size_t> lastRecordBoundary) override {
+ if (!avail) {
+ return;
+ }
+
+ Target_.Write(Block_.Buffer_.Get(), lastRecordBoundary.value_or(avail));
+ if (lastRecordBoundary) {
+ if (OnRecordBoundaryCallback_) {
+ OnRecordBoundaryCallback_();
+ }
+ if (avail > *lastRecordBoundary) {
+ Target_.Write(Block_.Buffer_.Get() + *lastRecordBoundary, avail - *lastRecordBoundary);
+ }
+ }
+ }
+
+ void Finish() override {
+ Target_.Finish();
+ Finished_ = true;
+ }
+
+private:
+ IOutputStream& Target_;
+ TBlock Block_;
+ bool Finished_ = false;
+ std::function<void()> OnRecordBoundaryCallback_;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// TODO: share buffer manager between all writers (with at least <out table count> + 1 blocks)
+THolder<IBlockWriter> MakeBlockWriter(IOutputStream& target, size_t blockCount, size_t blockSize) {
+ THolder<IBlockWriter> res;
+ if (blockCount > 1) {
+ res.Reset(new TAsyncBlockWriter(target, blockCount, blockSize));
+ } else {
+ res.Reset(new TSyncBlockWriter(target, blockSize));
+ }
+ return res;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+inline void WriteWithLength(IOutputStream& out, TStringBuf val) {
+ ::NYson::WriteVarInt32(&out, val.size());
+ out.Write(val.data(), val.size());
+}
+
+inline void WriteWithLength(IOutputStream& out, const NUdf::TUnboxedValue& val) {
+ WriteWithLength(out, val.AsStringRef());
+}
+
+void WriteRowItems(TMkqlIOCache& specsCache, size_t tableIndex,
+ const TVector<TString>& items,
+ const TMaybe<THashMap<TString, TString>>& others,
+ IOutputStream& out)
+{
+ auto& decoder = *specsCache.GetSpecs().Inputs[tableIndex];
+ // Output
+ out.Write(BeginListSymbol);
+ for (ui32 index = 0; index < decoder.StructSize; ++index) {
+ if (!items[index].empty()) {
+ out.Write(items[index].data(), items[index].size());
+ out.Write(ListItemSeparatorSymbol);
+ continue;
+ }
+
+ if (decoder.OthersStructIndex && *decoder.OthersStructIndex == index) {
+ specsCache.UpdateMaxOthersFields(tableIndex, others->size());
+
+ out.Write(BeginListSymbol);
+ for (const auto& oField : *others) {
+ out.Write(BeginListSymbol);
+ out.Write(StringMarker);
+ WriteWithLength(out, oField.first);
+ out.Write(ListItemSeparatorSymbol);
+ out.Write(StringMarker);
+ WriteWithLength(out, oField.second);
+ out.Write(EndListSymbol);
+
+ out.Write(ListItemSeparatorSymbol);
+ }
+ out.Write(EndListSymbol);
+ out.Write(ListItemSeparatorSymbol);
+ continue;
+ }
+
+ // missing value
+ const auto& field = decoder.FieldsVec[index];
+ if (field.Type->IsOptional() || field.Type->IsVoid() || field.Type->IsNull()) {
+ out.Write(EntitySymbol);
+ out.Write(ListItemSeparatorSymbol);
+ continue;
+ }
+
+ const auto& defVal = decoder.DefaultValues[index];
+ YQL_ENSURE(defVal, "Missing field data: " << field.Name);
+
+ out.Write(StringMarker);
+ WriteWithLength(out, defVal);
+ out.Write(ListItemSeparatorSymbol);
+ }
+ out.Write(EndListSymbol);
+ out.Write(ListItemSeparatorSymbol);
+}
+
+} // unnamed
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+struct TMkqlReaderImpl::TDecoder {
+ TDecoder(TInputBuf& buf, const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory)
+ : Buf_(buf)
+ , SpecsCache_(specs, holderFactory)
+ {
+ }
+
+ virtual ~TDecoder() = default;
+ virtual bool DecodeNext(NKikimr::NUdf::TUnboxedValue*& items, TMaybe<NKikimr::NMiniKQL::TValuesDictHashMap>& others) = 0;
+
+ NUdf::TUnboxedValue ReadOtherField(char cmd) {
+ switch (cmd) {
+ case EntitySymbol:
+ return NUdf::TUnboxedValuePod::Embedded(TStringBuf("#"));
+
+ case TrueMarker:
+ return NUdf::TUnboxedValuePod::Embedded(TStringBuf("%true"));
+
+ case FalseMarker:
+ return NUdf::TUnboxedValuePod::Embedded(TStringBuf("%false"));
+
+ case Int64Marker: {
+ auto val = Buf_.ReadVarI64();
+ char format[100];
+ auto len = ToString(val, format, Y_ARRAY_SIZE(format));
+ return NUdf::TUnboxedValue(MakeString(NUdf::TStringRef(format, len)));
+ }
+
+ case Uint64Marker: {
+ auto val = Buf_.ReadVarUI64();
+ char format[100];
+ auto len = ToString(val, format, Y_ARRAY_SIZE(format) - 1);
+ format[len] = 'u';
+ return NUdf::TUnboxedValue(MakeString(NUdf::TStringRef(format, len + 1)));
+ }
+
+ case StringMarker: {
+ const i32 length = Buf_.ReadVarI32();
+ CHECK_STRING_LENGTH(length);
+ auto val = NUdf::TUnboxedValue(MakeStringNotFilled(length));
+ const auto& buf = val.AsStringRef();
+ Buf_.ReadMany(buf.Data(), buf.Size());
+ return val;
+ }
+
+ case DoubleMarker: {
+ double val;
+ Buf_.ReadMany((char*)&val, sizeof(val));
+ char format[100];
+ auto len = ToString(val, format, Y_ARRAY_SIZE(format));
+ return NUdf::TUnboxedValue(MakeString(NUdf::TStringRef(format, len)));
+ }
+
+ }
+
+ auto& yson = Buf_.YsonBuffer();
+ yson.clear();
+ CopyYsonWithAttrs(cmd, Buf_, yson);
+ return NUdf::TUnboxedValue(MakeString(NUdf::TStringRef(yson.data(), yson.size())));
+ }
+
+ void EndStream() {
+ Finished_ = true;
+ Valid_ = false;
+ KeySwitch_ = false;
+ }
+
+ void Reset(bool hasRangeIndices, ui32 tableIndex, bool ignoreStreamTableIndex) {
+ HasRangeIndices_ = hasRangeIndices;
+ TableIndex_ = tableIndex;
+ AtStart_ = true;
+ Valid_ = true;
+ Finished_ = false;
+ RowAlreadyRead_ = false;
+ RowIndex_.Clear();
+ RangeIndex_.Clear();
+ Row_.Clear();
+ IgnoreStreamTableIndex = ignoreStreamTableIndex;
+ // Don't reset KeySwitch_ here, because this method is called when switching to a next table. It shouldn't touch key switch flag
+ }
+
+public:
+ bool HasRangeIndices_ = false;
+ bool AtStart_ = true;
+ bool Valid_ = true;
+ bool Finished_ = false;
+ bool RowAlreadyRead_ = false;
+ ui32 TableIndex_ = 0;
+ TMaybe<ui64> RowIndex_;
+ TMaybe<ui32> RangeIndex_;
+ NKikimr::NUdf::TUnboxedValue Row_;
+ bool IgnoreStreamTableIndex = false;
+ bool KeySwitch_ = true;
+
+protected:
+ TInputBuf& Buf_;
+ TMkqlIOCache SpecsCache_;
+};
+
+
+class TYsonDecoder: public TMkqlReaderImpl::TDecoder {
+public:
+ TYsonDecoder(TInputBuf& buf, const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory)
+ : TMkqlReaderImpl::TDecoder(buf, specs, holderFactory)
+ {
+ }
+
+ virtual ~TYsonDecoder() = default;
+
+ bool DecodeNext(NUdf::TUnboxedValue*& items, TMaybe<TValuesDictHashMap>& others) final {
+ char cmd;
+ if (Finished_ || !Buf_.TryRead(cmd)) {
+ EndStream();
+ return false;
+ }
+
+ cmd = ReadAttrs(cmd);
+ if (!Valid_) {
+ return false;
+ }
+
+ CHECK_EXPECTED(cmd, BeginMapSymbol);
+ AtStart_ = false;
+ auto& decoder = *SpecsCache_.GetSpecs().Inputs[TableIndex_];
+ auto& lastFields = SpecsCache_.GetLastFields(TableIndex_);
+ Row_.Clear();
+ Row_ = SpecsCache_.NewRow(TableIndex_, items);
+ if (decoder.OthersStructIndex) {
+ others.ConstructInPlace(SpecsCache_.GetMaxOthersFields(TableIndex_),
+ TValueHasher(decoder.OthersKeyTypes, false, nullptr),
+ TValueEqual(decoder.OthersKeyTypes, false, nullptr));
+ }
+
+ ui32 fieldNo = 0;
+ cmd = Buf_.Read();
+
+ for (;;) {
+ if (cmd == EndMapSymbol) {
+ break;
+ }
+ CHECK_EXPECTED(cmd, StringMarker);
+ auto name = Buf_.ReadYtString(2); // KeyValueSeparatorSymbol + subsequent cmd
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ const TMkqlIOSpecs::TDecoderSpec::TDecodeField* field = nullptr;
+ if (!decoder.OthersStructIndex || name != YqlOthersColumnName) {
+ if (fieldNo < lastFields.size()) {
+ auto lastField = lastFields[fieldNo];
+ if (lastField->Name == name) {
+ field = lastField;
+ }
+ }
+
+ if (!field) {
+ auto it = decoder.Fields.find(name);
+ if (it != decoder.Fields.end()) {
+ field = &it->second;
+ }
+ }
+ }
+
+ if (!field) {
+ YQL_ENSURE(decoder.OthersStructIndex, "Unexpected field: " << name << " in strict scheme");
+ cmd = Buf_.Read();
+ // 'name' buffer may be invalidated in ReadOtherField()
+ auto key = NUdf::TUnboxedValue(MakeString(name));
+ auto isStringKey = (cmd != StringMarker) ? NUdf::TUnboxedValue() :
+ NUdf::TUnboxedValue(MakeString(TStringBuilder() << "_yql_" << name));
+ try {
+ auto value = ReadOtherField(cmd);
+ if (isStringKey) {
+ auto empty = NUdf::TUnboxedValue::Zero();
+ others->emplace(std::move(isStringKey), std::move(empty));
+ }
+ others->emplace(std::move(key), std::move(value));
+ }
+ catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read field: '" << TStringBuf(key.AsStringRef()) << "'\n" << e.what();
+ }
+ catch (...) {
+ ythrow yexception() << "Failed to read field: '" << TStringBuf(key.AsStringRef()) << "'\n" << CurrentExceptionMessage();
+ }
+ } else {
+ try {
+ if (Y_LIKELY(field->StructIndex != Max<ui32>())) {
+ items[field->StructIndex] = ReadField(field->Type);
+ } else {
+ SkipField(field->Type);
+ }
+ }
+ catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read field: '" << name << "'\n" << e.what();
+ }
+ catch (...) {
+ ythrow yexception() << "Failed to read field: '" << name << "'\n" << CurrentExceptionMessage();
+ }
+
+ if (fieldNo < lastFields.size()) {
+ lastFields[fieldNo] = field;
+ }
+
+ ++fieldNo;
+ }
+
+ cmd = Buf_.Read();
+ if (cmd == KeyedItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+
+ if (!Buf_.TryRead(cmd)) {
+ // don't EndStream here as it would throw away last record
+ // (GetRow throws on invalidated stream)
+ // instead it would be invalidated on next call
+ // Finished_ is set to not call TryRead next time
+ // because else TryRead causes an infinite loop
+ Finished_ = true;
+ return true;
+ }
+
+ CHECK_EXPECTED(cmd, ListItemSeparatorSymbol);
+
+ if (others) {
+ SpecsCache_.UpdateMaxOthersFields(TableIndex_, others->size());
+ }
+
+ return true;
+ }
+
+protected:
+ char ReadAttrs(char cmd) {
+ while (cmd == BeginAttributesSymbol) {
+ TMaybe<ui64> rowIndex;
+ TMaybe<ui32> rangeIndex;
+ cmd = Buf_.Read();
+
+ for (;;) {
+ if (cmd == EndAttributesSymbol) {
+ EXPECTED(Buf_, EntitySymbol);
+ // assume there's no control record at the end of the stream
+ EXPECTED(Buf_, ListItemSeparatorSymbol);
+
+ if (Valid_) {
+ if (!Buf_.TryRead(cmd)) {
+ EndStream();
+ }
+ }
+
+ break;
+ }
+
+ CHECK_EXPECTED(cmd, StringMarker);
+ auto name = Buf_.ReadYtString();
+ if (name == TStringBuf("row_index")) {
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ EXPECTED(Buf_, Int64Marker);
+ rowIndex = Buf_.ReadVarI64();
+ }
+ else if (name == TStringBuf("table_index")) {
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ EXPECTED(Buf_, Int64Marker);
+ const auto tableIndex = Buf_.ReadVarI64();
+ if (!IgnoreStreamTableIndex) {
+ TableIndex_ = tableIndex;
+ YQL_ENSURE(TableIndex_ < SpecsCache_.GetSpecs().Inputs.size());
+ }
+ }
+ else if (name == TStringBuf("key_switch")) {
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ EXPECTED(Buf_, TrueMarker);
+ if (!AtStart_) {
+ Valid_ = false;
+ KeySwitch_ = true;
+ }
+ }
+ else if (name == TStringBuf("range_index")) {
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ EXPECTED(Buf_, Int64Marker);
+ rangeIndex = Buf_.ReadVarI64();
+ }
+ else {
+ YQL_ENSURE(false, "Unsupported annotation:" << name);
+ }
+
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+ if (rowIndex) {
+ if (HasRangeIndices_) {
+ if (rangeIndex) {
+ RowIndex_ = rowIndex;
+ RangeIndex_ = rangeIndex;
+ }
+ } else {
+ RowIndex_ = rowIndex;
+ }
+ }
+
+ }
+ return cmd;
+ }
+
+ NUdf::TUnboxedValue ReadField(TType* type) {
+ auto cmd = Buf_.Read();
+ if (type->IsNull()) {
+ return NUdf::TUnboxedValue();
+ }
+
+ const bool isOptional = type->IsOptional();
+ if (isOptional) {
+ TType* uwrappedType = static_cast<TOptionalType*>(type)->GetItemType();
+ if (cmd == EntitySymbol) {
+ return NUdf::TUnboxedValue();
+ }
+
+ auto val = ReadYsonValue(uwrappedType, SpecsCache_.GetHolderFactory(), cmd, Buf_, true);
+ return val.Release().MakeOptional();
+ } else {
+ if (Y_LIKELY(cmd != EntitySymbol)) {
+ return ReadYsonValue(type, SpecsCache_.GetHolderFactory(), cmd, Buf_, true);
+ }
+
+ if (type->GetKind() == TType::EKind::Data && static_cast<TDataType*>(type)->GetSchemeType() == NUdf::TDataType<NUdf::TYson>::Id) {
+ return NUdf::TUnboxedValue::Embedded(TStringBuf("#"));
+ }
+
+ // Don't fail right here because field can be filled from DefaultValues
+ return NUdf::TUnboxedValue();
+ }
+ }
+
+ void SkipField(TType* type) {
+ auto cmd = Buf_.Read();
+ if (cmd != EntitySymbol) {
+ SkipValue(type->IsOptional() ? static_cast<TOptionalType*>(type)->GetItemType() : type, cmd);
+ }
+ }
+
+ void SkipValue(TType* type, char cmd) {
+ switch (type->GetKind()) {
+ case TType::EKind::Variant: {
+ auto varType = static_cast<TVariantType*>(type);
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ EXPECTED(Buf_, Uint64Marker);
+ ui64 index = Buf_.ReadVarUI64();
+ YQL_ENSURE(index < varType->GetAlternativesCount(), "Bad variant alternative: " << index << ", only " <<
+ varType->GetAlternativesCount() << " are available");
+ auto underlyingType = varType->GetUnderlyingType();
+ YQL_ENSURE(underlyingType->IsTuple() || underlyingType->IsStruct(), "Wrong underlying type");
+ TType* itemType;
+ if (underlyingType->IsTuple()) {
+ itemType = static_cast<TTupleType*>(underlyingType)->GetElementType(index);
+ } else {
+ itemType = static_cast<TStructType*>(underlyingType)->GetMemberType(index);
+ }
+
+ EXPECTED(Buf_, ListItemSeparatorSymbol);
+ cmd = Buf_.Read();
+ SkipValue(itemType, cmd);
+
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+
+ CHECK_EXPECTED(cmd, EndListSymbol);
+ break;
+ }
+
+ case TType::EKind::Data: {
+ auto schemeType = static_cast<TDataType*>(type)->GetSchemeType();
+ switch (schemeType) {
+ case NUdf::TDataType<bool>::Id:
+ YQL_ENSURE(cmd == FalseMarker || cmd == TrueMarker, "Expected either true or false, but got: " << cmd);
+ break;
+
+ case NUdf::TDataType<ui8>::Id:
+ case NUdf::TDataType<ui16>::Id:
+ case NUdf::TDataType<ui32>::Id:
+ case NUdf::TDataType<ui64>::Id:
+ case NUdf::TDataType<NUdf::TDate>::Id:
+ case NUdf::TDataType<NUdf::TDatetime>::Id:
+ case NUdf::TDataType<NUdf::TTimestamp>::Id:
+ CHECK_EXPECTED(cmd, Uint64Marker);
+ Buf_.ReadVarUI64();
+ break;
+
+ case NUdf::TDataType<i8>::Id:
+ case NUdf::TDataType<i16>::Id:
+ case NUdf::TDataType<i32>::Id:
+ case NUdf::TDataType<i64>::Id:
+ case NUdf::TDataType<NUdf::TInterval>::Id:
+ CHECK_EXPECTED(cmd, Int64Marker);
+ Buf_.ReadVarI64();
+ break;
+
+ case NUdf::TDataType<float>::Id:
+ case NUdf::TDataType<double>::Id:
+ CHECK_EXPECTED(cmd, DoubleMarker);
+ Buf_.SkipMany(sizeof(double));
+ break;
+
+ case NUdf::TDataType<NUdf::TDecimal>::Id:
+ case NUdf::TDataType<NUdf::TUtf8>::Id:
+ case NUdf::TDataType<char*>::Id:
+ case NUdf::TDataType<NUdf::TJson>::Id:
+ case NUdf::TDataType<NUdf::TTzDate>::Id:
+ case NUdf::TDataType<NUdf::TTzDatetime>::Id:
+ case NUdf::TDataType<NUdf::TTzTimestamp>::Id:
+ case NUdf::TDataType<NUdf::TDyNumber>::Id:
+ case NUdf::TDataType<NUdf::TUuid>::Id:
+ case NUdf::TDataType<NUdf::TJsonDocument>::Id: {
+ CHECK_EXPECTED(cmd, StringMarker);
+ const i32 length = Buf_.ReadVarI32();
+ CHECK_STRING_LENGTH(length);
+ Buf_.SkipMany(length);
+ break;
+ }
+
+ case NUdf::TDataType<NUdf::TYson>::Id: {
+ auto& yson = Buf_.YsonBuffer();
+ yson.clear();
+ CopyYsonWithAttrs(cmd, Buf_, yson);
+ break;
+ }
+
+ default:
+ YQL_ENSURE(false, "Unsupported data type: " << schemeType);
+ }
+ break;
+ }
+
+ case TType::EKind::Struct: {
+ auto structType = static_cast<TStructType*>(type);
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+
+ for (ui32 i = 0; i < structType->GetMembersCount(); ++i) {
+ SkipValue(structType->GetMemberType(i), cmd);
+
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+ CHECK_EXPECTED(cmd, EndListSymbol);
+ break;
+ }
+
+ case TType::EKind::List: {
+ auto itemType = static_cast<TListType*>(type)->GetItemType();
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+
+ for (;;) {
+ if (cmd == EndListSymbol) {
+ break;
+ }
+
+ SkipValue(itemType, cmd);
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+ break;
+ }
+
+ case TType::EKind::Optional: {
+ if (cmd == EntitySymbol) {
+ return;
+ }
+
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+ if (cmd == EndListSymbol) {
+ return;
+ }
+
+ SkipValue(static_cast<TOptionalType*>(type)->GetItemType(), cmd);
+
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+
+ CHECK_EXPECTED(cmd, EndListSymbol);
+ break;
+ }
+
+ case TType::EKind::Dict: {
+ auto dictType = static_cast<TDictType*>(type);
+ auto keyType = dictType->GetKeyType();
+ auto payloadType = dictType->GetPayloadType();
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+
+ for (;;) {
+ if (cmd == EndListSymbol) {
+ break;
+ }
+
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+ SkipValue(keyType, cmd);
+ EXPECTED(Buf_, ListItemSeparatorSymbol);
+ cmd = Buf_.Read();
+ SkipValue(payloadType, cmd);
+
+ cmd = Buf_.Read();
+ // skip inner list separator
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+
+ CHECK_EXPECTED(cmd, EndListSymbol);
+
+ cmd = Buf_.Read();
+ // skip outer list separator
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+ break;
+ }
+
+ case TType::EKind::Tuple: {
+ auto tupleType = static_cast<TTupleType*>(type);
+ CHECK_EXPECTED(cmd, BeginListSymbol);
+ cmd = Buf_.Read();
+
+ for (ui32 i = 0; i < tupleType->GetElementsCount(); ++i) {
+ SkipValue(tupleType->GetElementType(i), cmd);
+
+ cmd = Buf_.Read();
+ if (cmd == ListItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+
+ }
+
+ CHECK_EXPECTED(cmd, EndListSymbol);
+ break;
+ }
+
+ case TType::EKind::Void: {
+ if (cmd == EntitySymbol) {
+ return;
+ }
+
+ CHECK_EXPECTED(cmd, StringMarker);
+ i32 length = Buf_.ReadVarI32();
+ YQL_ENSURE(length == 4, "Expected Void");
+ char buffer[4];
+ Buf_.ReadMany(buffer, 4);
+ YQL_ENSURE(TStringBuf(buffer, 4) == TStringBuf("Void"), "Expected Void");
+ break;
+ }
+
+ default:
+ YQL_ENSURE(false, "Unsupported type: " << type->GetKindAsStr());
+ }
+ }
+};
+
+class TSkiffDecoderBase: public TMkqlReaderImpl::TDecoder {
+public:
+ TSkiffDecoderBase(TInputBuf& buf, const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory)
+ : TMkqlReaderImpl::TDecoder(buf, specs, holderFactory)
+ {
+ }
+
+ bool DecodeNext(NUdf::TUnboxedValue*& items, TMaybe<TValuesDictHashMap>& others) final {
+ char byte1;
+ if (!Buf_.TryRead(byte1)) {
+ EndStream();
+ return false;
+ }
+
+ char byte2 = Buf_.Read();
+ if (!IgnoreStreamTableIndex) {
+ ui16 tableIndex = (ui16(ui8(byte2)) << 8) | ui8(byte1);
+ TableIndex_ = tableIndex;
+ }
+ auto& spec = SpecsCache_.GetSpecs();
+ YQL_ENSURE(TableIndex_ < spec.Inputs.size());
+
+ auto& decoder = *spec.Inputs[TableIndex_];
+
+ if (spec.SystemFields_.HasFlags(TMkqlIOSpecs::ESystemField::RangeIndex)) {
+ auto cmd = Buf_.Read();
+ if (cmd) {
+ i64 value;
+ Buf_.ReadMany((char*)&value, sizeof(value));
+ YQL_ENSURE(HasRangeIndices_);
+ RangeIndex_ = value;
+ }
+ }
+
+ if (decoder.Dynamic) {
+ RowIndex_.Clear();
+ } else if (spec.SystemFields_.HasFlags(TMkqlIOSpecs::ESystemField::RowIndex)) {
+ auto cmd = Buf_.Read();
+ if (cmd) {
+ i64 value;
+ Buf_.ReadMany((char*)&value, sizeof(value));
+ RowIndex_ = value;
+ }
+ }
+
+ if (spec.SystemFields_.HasFlags(TMkqlIOSpecs::ESystemField::KeySwitch)) {
+ auto cmd = Buf_.Read();
+ if (!AtStart_ && cmd) {
+ Valid_ = false;
+ RowAlreadyRead_ = true;
+ KeySwitch_ = true;
+ }
+ }
+
+ AtStart_ = false;
+
+ Row_ = NUdf::TUnboxedValue();
+ Row_ = SpecsCache_.NewRow(TableIndex_, items);
+
+ DecodeItems(items);
+
+ if (decoder.OthersStructIndex) {
+ // parse yson with other fields
+ try {
+ others.ConstructInPlace(SpecsCache_.GetMaxOthersFields(TableIndex_),
+ TValueHasher(decoder.OthersKeyTypes, false, nullptr),
+ TValueEqual(decoder.OthersKeyTypes, false, nullptr));
+ ReadOtherSkiffFields(*others);
+ SpecsCache_.UpdateMaxOthersFields(TableIndex_, others->size());
+ } catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read others\n" << e.what();
+ } catch (...) {
+ ythrow yexception() << "Failed to read others\n" << CurrentExceptionMessage();
+ }
+ }
+ return true;
+ }
+
+protected:
+ virtual void DecodeItems(NUdf::TUnboxedValue* items) = 0;
+
+ void ReadOtherSkiffFields(TValuesDictHashMap& others) {
+ ui32 size;
+ Buf_.ReadMany((char*)&size, sizeof(size));
+ YQL_ENSURE(size > 0);
+ EXPECTED(Buf_, BeginMapSymbol);
+ char cmd = Buf_.Read();
+
+ for (;;) {
+ if (cmd == EndMapSymbol) {
+ break;
+ }
+
+ auto name = Buf_.ReadYtString(2); // KeyValueSeparatorSymbol + subsequent cmd
+ EXPECTED(Buf_, KeyValueSeparatorSymbol);
+ cmd = Buf_.Read();
+ // 'name' buffer may be invalidated in ReadOtherField()
+ auto key = NUdf::TUnboxedValue(MakeString(name));
+ auto isStringKey = (cmd != StringMarker) ? NUdf::TUnboxedValue() :
+ NUdf::TUnboxedValue(MakeString(TStringBuilder() << "_yql_" << name));
+ try {
+ auto value = ReadOtherField(cmd);
+ if (isStringKey) {
+ auto empty = NUdf::TUnboxedValue::Zero();
+ others.emplace(std::move(isStringKey), std::move(empty));
+ }
+ others.emplace(std::move(key), std::move(value));
+ } catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read field: '" << TStringBuf(key.AsStringRef()) << "'\n" << e.what();
+ } catch (...) {
+ ythrow yexception() << "Failed to read field: '" << TStringBuf(key.AsStringRef()) << "'\n" << CurrentExceptionMessage();
+ }
+
+ cmd = Buf_.Read();
+ if (cmd == KeyedItemSeparatorSymbol) {
+ cmd = Buf_.Read();
+ }
+ }
+ }
+};
+
+class TSkiffDecoder: public TSkiffDecoderBase {
+public:
+ TSkiffDecoder(TInputBuf& buf, const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory)
+ : TSkiffDecoderBase(buf, specs, holderFactory)
+ {
+ }
+
+protected:
+ void DecodeItems(NUdf::TUnboxedValue* items) final {
+ auto& decoder = *SpecsCache_.GetSpecs().Inputs[TableIndex_];
+ for (ui32 i = 0; i < decoder.SkiffSize; ++i) {
+ if (decoder.OthersStructIndex && i == *decoder.OthersStructIndex) {
+ continue;
+ }
+
+ if (decoder.FieldsVec[i].Virtual) {
+ items[i] = NUdf::TUnboxedValuePod::Zero();
+ continue;
+ }
+
+ try {
+ if (Y_UNLIKELY(decoder.FieldsVec[i].StructIndex == Max<ui32>())) {
+ SkipSkiffField(decoder.FieldsVec[i].Type, decoder.NativeYtTypeFlags);
+ } else if (decoder.NativeYtTypeFlags && !decoder.FieldsVec[i].ExplicitYson) {
+ items[i] = ReadSkiffFieldNativeYt(decoder.FieldsVec[i].Type, decoder.NativeYtTypeFlags);
+ } else if (decoder.DefaultValues[i]) {
+ auto val = ReadSkiffField(decoder.FieldsVec[i].Type, true);
+ items[i] = val ? NUdf::TUnboxedValue(val.Release().GetOptionalValue()) : decoder.DefaultValues[i];
+ } else {
+ items[i] = ReadSkiffField(decoder.FieldsVec[i].Type, false);
+ }
+ } catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read field: '" << decoder.FieldsVec[i].Name << "'\n" << e.what();
+ } catch (...) {
+ ythrow yexception() << "Failed to read field: '" << decoder.FieldsVec[i].Name << "'\n" << CurrentExceptionMessage();
+ }
+ }
+ }
+
+ NUdf::TUnboxedValue ReadSkiffField(TType* type, bool withDefVal) {
+ const bool isOptional = withDefVal || type->IsOptional();
+ TType* uwrappedType = type;
+ if (type->IsOptional()) {
+ uwrappedType = static_cast<TOptionalType*>(type)->GetItemType();
+ }
+
+ if (isOptional) {
+ auto marker = Buf_.Read();
+ if (!marker) {
+ return NUdf::TUnboxedValue();
+ }
+ }
+
+ if (uwrappedType->IsData()) {
+ return NCommon::ReadSkiffData(uwrappedType, 0, Buf_);
+ } else if (!isOptional && uwrappedType->IsPg()) {
+ return NCommon::ReadSkiffPg(static_cast<TPgType*>(uwrappedType), Buf_);
+ } else {
+ // yson content
+ ui32 size;
+ Buf_.ReadMany((char*)&size, sizeof(size));
+ CHECK_STRING_LENGTH_UNSIGNED(size);
+ // parse binary yson...
+ YQL_ENSURE(size > 0);
+ char cmd = Buf_.Read();
+ auto value = ReadYsonValue(uwrappedType, SpecsCache_.GetHolderFactory(), cmd, Buf_, true);
+ return isOptional ? value.Release().MakeOptional() : value;
+ }
+ }
+
+ NUdf::TUnboxedValue ReadSkiffFieldNativeYt(TType* type, ui64 nativeYtTypeFlags) {
+ return NCommon::ReadSkiffNativeYtValue(type, nativeYtTypeFlags, SpecsCache_.GetHolderFactory(), Buf_);
+ }
+
+ void SkipSkiffField(TType* type, ui64 nativeYtTypeFlags) {
+ const bool isOptional = type->IsOptional();
+ TType* uwrappedType = type;
+ if (type->IsOptional()) {
+ uwrappedType = static_cast<TOptionalType*>(type)->GetItemType();
+ }
+
+ if (isOptional) {
+ auto marker = Buf_.Read();
+ if (!marker) {
+ return;
+ }
+ }
+
+ if (uwrappedType->IsData()) {
+ auto schemeType = static_cast<TDataType*>(uwrappedType)->GetSchemeType();
+ switch (schemeType) {
+ case NUdf::TDataType<bool>::Id:
+ Buf_.SkipMany(sizeof(ui8));
+ break;
+
+ case NUdf::TDataType<ui8>::Id:
+ case NUdf::TDataType<ui16>::Id:
+ case NUdf::TDataType<ui32>::Id:
+ case NUdf::TDataType<ui64>::Id:
+ case NUdf::TDataType<NUdf::TDate>::Id:
+ case NUdf::TDataType<NUdf::TDatetime>::Id:
+ case NUdf::TDataType<NUdf::TTimestamp>::Id:
+ Buf_.SkipMany(sizeof(ui64));
+ break;
+
+ case NUdf::TDataType<i8>::Id:
+ case NUdf::TDataType<i16>::Id:
+ case NUdf::TDataType<i32>::Id:
+ case NUdf::TDataType<i64>::Id:
+ case NUdf::TDataType<NUdf::TInterval>::Id:
+ Buf_.SkipMany(sizeof(i64));
+ break;
+
+ case NUdf::TDataType<float>::Id:
+ case NUdf::TDataType<double>::Id:
+ Buf_.SkipMany(sizeof(double));
+ break;
+
+ case NUdf::TDataType<NUdf::TUtf8>::Id:
+ case NUdf::TDataType<char*>::Id:
+ case NUdf::TDataType<NUdf::TJson>::Id:
+ case NUdf::TDataType<NUdf::TYson>::Id:
+ case NUdf::TDataType<NUdf::TUuid>::Id:
+ case NUdf::TDataType<NUdf::TDyNumber>::Id:
+ case NUdf::TDataType<NUdf::TTzDate>::Id:
+ case NUdf::TDataType<NUdf::TTzDatetime>::Id:
+ case NUdf::TDataType<NUdf::TTzTimestamp>::Id:
+ case NUdf::TDataType<NUdf::TJsonDocument>::Id: {
+ ui32 size;
+ Buf_.ReadMany((char*)&size, sizeof(size));
+ CHECK_STRING_LENGTH_UNSIGNED(size);
+ Buf_.SkipMany(size);
+ break;
+ }
+ case NUdf::TDataType<NUdf::TDecimal>::Id: {
+ if (nativeYtTypeFlags & NTCF_DECIMAL) {
+ auto const params = static_cast<TDataDecimalType*>(type)->GetParams();
+ if (params.first < 10) {
+ Buf_.SkipMany(sizeof(i32));
+ } else if (params.first < 19) {
+ Buf_.SkipMany(sizeof(i64));
+ } else {
+ Buf_.SkipMany(sizeof(NDecimal::TInt128));
+ }
+ } else {
+ ui32 size;
+ Buf_.ReadMany((char*)&size, sizeof(size));
+ CHECK_STRING_LENGTH_UNSIGNED(size);
+ Buf_.SkipMany(size);
+ }
+ break;
+ }
+ default:
+ YQL_ENSURE(false, "Unsupported data type: " << schemeType);
+ }
+ } else {
+ ythrow yexception() << "Skip of complex types is not supported";
+ }
+ }
+};
+
+#ifndef MKQL_DISABLE_CODEGEN
+
+class TSkiffLLVMDecoder: public TSkiffDecoderBase {
+public:
+ TSkiffLLVMDecoder(TInputBuf& buf, const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory)
+ : TSkiffDecoderBase(buf, specs, holderFactory)
+ {
+ Codegen_ = NCodegen::ICodegen::Make(NCodegen::ETarget::Native);
+ Codegen_->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ TVector<llvm::Function*> funcs;
+ THashMap<const TMkqlIOSpecs::TDecoderSpec*, llvm::Function*> processedDecoders;
+ for (auto x : specs.Inputs) {
+ llvm::Function*& func = processedDecoders[x];
+ if (!func) {
+ auto rowReaderBuilder = MakeYtCodecCgReader(Codegen_, holderFactory, x);
+ for (ui32 i = 0; i < x->SkiffSize; ++i) {
+ if (x->OthersStructIndex && i == *x->OthersStructIndex) {
+ rowReaderBuilder->SkipOther();
+ continue;
+ }
+
+ if (x->FieldsVec[i].Virtual) {
+ rowReaderBuilder->SkipVirtual();
+ continue;
+ }
+
+ if (x->FieldsVec[i].StructIndex != Max<ui32>()) {
+ rowReaderBuilder->AddField(x->FieldsVec[i].Type, x->DefaultValues[i], x->FieldsVec[i].ExplicitYson ? 0 : x->NativeYtTypeFlags);
+ } else {
+ rowReaderBuilder->SkipField(x->FieldsVec[i].Type, x->NativeYtTypeFlags);
+ }
+ }
+
+ func = rowReaderBuilder->Build();
+ }
+ funcs.push_back(func);
+ }
+
+ Codegen_->Verify();
+ YtCodecAddMappings(*Codegen_);
+ Codegen_->Compile();
+ for (const auto& func : funcs) {
+ RowReaders_.push_back((TRowReader)Codegen_->GetPointerToFunction(func));
+ }
+ }
+
+protected:
+ void DecodeItems(NUdf::TUnboxedValue* items) final {
+ RowReaders_.at(TableIndex_)(items, Buf_);
+ }
+
+ NCodegen::ICodegen::TPtr Codegen_;
+ typedef void(*TRowReader)(NKikimr::NUdf::TUnboxedValue*, TInputBuf&);
+ TVector<TRowReader> RowReaders_;
+};
+
+#endif
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TMkqlReaderImpl::TMkqlReaderImpl(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize, ui32 tableIndex, bool ignoreStreamTableIndex)
+ : TMkqlReaderImpl()
+{
+ SetReader(source, blockCount, blockSize, tableIndex, ignoreStreamTableIndex);
+}
+
+TMkqlReaderImpl::TMkqlReaderImpl()
+ : TimerDecode_(InputDecodeTime, 100)
+ , TimerRead_(InputReadTime, 100)
+ , Buf_(&TimerRead_)
+{
+}
+
+TMkqlReaderImpl::~TMkqlReaderImpl() {
+}
+
+void TMkqlReaderImpl::SetReader(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize, ui32 tableIndex, bool ignoreStreamTableIndex) {
+ Reader_ = MakeBlockReader(source, blockCount, blockSize);
+ Buf_.SetSource(*Reader_);
+ HasRangeIndices_ = source.HasRangeIndices();
+ InitialTableIndex_ = tableIndex;
+ IgnoreStreamTableIndex_ = ignoreStreamTableIndex;
+ if (Decoder_) {
+ Decoder_->Reset(HasRangeIndices_, InitialTableIndex_, IgnoreStreamTableIndex_);
+ }
+}
+
+void TMkqlReaderImpl::SetSpecs(const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory) {
+ Specs_ = &specs;
+ HolderFactoryPtr = &holderFactory;
+ JobStats_ = specs.JobStats_;
+ Buf_.SetStats(JobStats_);
+ if (Specs_->UseSkiff_) {
+#ifndef MKQL_DISABLE_CODEGEN
+ if (Specs_->OptLLVM_ != "OFF") {
+ Decoder_.Reset(new TSkiffLLVMDecoder(Buf_, *Specs_, holderFactory));
+ }
+ else
+#endif
+ {
+ Decoder_.Reset(new TSkiffDecoder(Buf_, *Specs_, holderFactory));
+ }
+ } else {
+ Decoder_.Reset(new TYsonDecoder(Buf_, *Specs_, holderFactory));
+ }
+ Decoder_->TableIndex_ = InitialTableIndex_;
+ Decoder_->HasRangeIndices_ = HasRangeIndices_;
+ Decoder_->IgnoreStreamTableIndex = IgnoreStreamTableIndex_;
+}
+
+void TMkqlReaderImpl::Finish() {
+ if (Decoder_) {
+ Decoder_->Row_.Clear();
+ }
+ TimerDecode_.Report(JobStats_);
+ TimerRead_.Report(JobStats_);
+}
+
+void TMkqlReaderImpl::OnError(TStringBuf msg) {
+ YQL_LOG(ERROR) << "Reader error: " << msg;
+ Buf_.Reset();
+ if (!Reader_->Retry(Decoder_->RangeIndex_, Decoder_->RowIndex_)) {
+ ythrow yexception() << "Failed to read row, table index: " << Decoder_->TableIndex_ << ", row index: " <<
+ (Decoder_->RowIndex_.Defined() ? ToString(*Decoder_->RowIndex_) : "?") << "\n" << msg;
+ }
+}
+
+NUdf::TUnboxedValue TMkqlReaderImpl::GetRow() const {
+ CheckValidity();
+ CheckReadRow();
+ return Decoder_->Row_;
+}
+
+bool TMkqlReaderImpl::IsValid() const {
+ return Reader_ && Decoder_ && Decoder_->Valid_;
+}
+
+ui32 TMkqlReaderImpl::GetTableIndex() const {
+ CheckValidity();
+ CheckReadRow();
+ return Decoder_->TableIndex_;
+}
+
+ui32 TMkqlReaderImpl::GetRangeIndex() const {
+ CheckValidity();
+ CheckReadRow();
+ return Decoder_->RangeIndex_.GetOrElse(0);
+}
+
+ui64 TMkqlReaderImpl::GetRowIndex() const {
+ CheckValidity();
+ CheckReadRow();
+ return Decoder_->RowIndex_.GetOrElse(0UL);
+}
+
+void TMkqlReaderImpl::Next() {
+ auto guard = Guard(TimerDecode_);
+ CheckValidity();
+
+ if (Decoder_->RowAlreadyRead_) {
+ Decoder_->RowAlreadyRead_ = false;
+ return;
+ }
+
+ if (Decoder_->RowIndex_) {
+ ++*Decoder_->RowIndex_;
+ }
+
+ NUdf::TUnboxedValue* items = nullptr;
+ TMaybe<TValuesDictHashMap> others;
+
+ // Retrieable part
+ while (true) {
+ try {
+ if (Decoder_->DecodeNext(items, others)) {
+ break; // Retry loop
+ } else {
+ return;
+ }
+ } catch (const TYqlPanic& e) {
+ ythrow TYqlPanic() << "Failed to read row, table index: " << Decoder_->TableIndex_ << ", row index: " <<
+ (Decoder_->RowIndex_.Defined() ? ToString(*Decoder_->RowIndex_) : "?") << "\n" << e.what();
+ } catch (const TTimeoutException&) {
+ throw;
+ } catch (const yexception& e) {
+ OnError(e.AsStrBuf());
+ } catch (...) {
+ OnError(CurrentExceptionMessage());
+ }
+ }
+
+ // Unretrieable part
+ auto& decoder = *Specs_->Inputs[Decoder_->TableIndex_];
+ if (Specs_->UseSkiff_) {
+ if (decoder.OthersStructIndex && *decoder.OthersStructIndex != Max<ui32>()) {
+ items[*decoder.OthersStructIndex] = BuildOthers(decoder, *others);
+ }
+ } else {
+ for (ui32 index = 0; index < decoder.StructSize; ++index) {
+ if (items[index]) {
+ continue;
+ }
+
+ if (decoder.OthersStructIndex && *decoder.OthersStructIndex == index) {
+ items[index] = BuildOthers(decoder, *others);
+ continue;
+ }
+
+ // missing value
+ const auto& field = decoder.FieldsVec[index];
+ if (field.Type->IsOptional() || field.Type->IsNull() || field.Type->IsPg()) {
+ items[index] = NUdf::TUnboxedValuePod();
+ continue;
+ }
+
+ if (field.Type->IsVoid()) {
+ items[index] = NUdf::TUnboxedValue::Void();
+ continue;
+ }
+
+ if (field.Virtual) {
+ items[index] = NUdf::TUnboxedValue::Zero();
+ continue;
+ }
+
+ const auto& defVal = decoder.DefaultValues[index];
+ YQL_ENSURE(defVal, "Failed to read row, table index: " << Decoder_->TableIndex_ << ", row index: " <<
+ (Decoder_->RowIndex_.Defined() ? ToString(*Decoder_->RowIndex_) : "?") << ": missing field data: " << field.Name);
+ items[index] = defVal;
+ }
+ }
+ if (decoder.FillSysColumnPath) {
+ items[*decoder.FillSysColumnPath] = Specs_->TableNames.at(Decoder_->TableIndex_);
+ }
+ if (decoder.FillSysColumnIndex) {
+ items[*decoder.FillSysColumnIndex] = NUdf::TUnboxedValuePod(Decoder_->TableIndex_);
+ }
+ if (decoder.FillSysColumnKeySwitch) {
+ items[*decoder.FillSysColumnKeySwitch] = NUdf::TUnboxedValuePod(Decoder_->KeySwitch_);
+ }
+ if (auto row = Decoder_->RowIndex_) {
+ if (decoder.FillSysColumnRecord) {
+ items[*decoder.FillSysColumnRecord] = NUdf::TUnboxedValuePod(*row + 1);
+ }
+ if (decoder.FillSysColumnNum) {
+ items[*decoder.FillSysColumnNum] = NUdf::TUnboxedValuePod(Specs_->TableOffsets.at(Decoder_->TableIndex_) + *row + 1);
+ }
+ }
+ Decoder_->KeySwitch_ = false;
+}
+
+NUdf::TUnboxedValue TMkqlReaderImpl::BuildOthers(const TMkqlIOSpecs::TDecoderSpec& decoder, TValuesDictHashMap& others) {
+ if (others.empty()) {
+ return HolderFactoryPtr->GetEmptyContainer();
+ } else {
+ auto filler = [&others](TValuesDictHashMap& map) {
+ map.swap(others);
+ };
+
+ return HolderFactoryPtr->CreateDirectHashedDictHolder(filler, decoder.OthersKeyTypes, false, true, nullptr, nullptr, nullptr);
+ }
+}
+
+void TMkqlReaderImpl::NextKey() {
+ while (Decoder_->Valid_) {
+ Next();
+ }
+
+ if (Decoder_->Finished_) {
+ return;
+ }
+
+ Decoder_->Valid_ = true;
+ if (Decoder_->RowIndex_ && !Decoder_->RowAlreadyRead_) {
+ --*Decoder_->RowIndex_;
+ }
+}
+
+void TMkqlReaderImpl::CheckValidity() const {
+ YQL_ENSURE(Decoder_ && Decoder_->Valid_, "Iterator is not valid");
+}
+
+void TMkqlReaderImpl::CheckReadRow() const {
+ YQL_ENSURE(Decoder_ && !Decoder_->AtStart_, "Next() must be called");
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+struct TMkqlWriterImpl::TEncoder {
+ TEncoder(TOutputBuf& buf, const TMkqlIOSpecs& specs)
+ : Buf_(buf)
+ , Specs_(specs)
+ {
+ }
+ virtual ~TEncoder() = default;
+
+ virtual void EncodeNext(const NUdf::TUnboxedValuePod row) = 0;
+ virtual void EncodeNext(const NUdf::TUnboxedValuePod* row) = 0;
+
+protected:
+ struct TField {
+ TStringBuf Name;
+ TType* Type;
+ bool Optional;
+ };
+
+ static TVector<TField> GetFields(TStructType* type) {
+ TVector<TField> res;
+ res.reserve(type->GetMembersCount());
+ for (ui32 index = 0; index < type->GetMembersCount(); ++index) {
+ auto fieldType = type->GetMemberType(index);
+ const bool isOptional = fieldType->IsOptional();
+ if (isOptional) {
+ fieldType = static_cast<TOptionalType*>(fieldType)->GetItemType();
+ }
+ auto name = type->GetMemberName(index);
+ res.push_back(TField{name, fieldType, isOptional});
+ }
+ return res;
+ }
+
+protected:
+ TOutputBuf& Buf_;
+ const TMkqlIOSpecs& Specs_;
+};
+
+class TYsonEncoder: public TMkqlWriterImpl::TEncoder {
+public:
+ TYsonEncoder(TOutputBuf& buf, const TMkqlIOSpecs& specs, size_t tableIndex)
+ : TMkqlWriterImpl::TEncoder(buf, specs)
+ {
+ Fields_ = GetFields(Specs_.Outputs[tableIndex].RowType);
+ }
+
+ void EncodeNext(const NUdf::TUnboxedValuePod row) final {
+ Buf_.Write(BeginMapSymbol);
+
+ for (size_t index = 0; index < Fields_.size(); ++index) {
+ const TField& field = Fields_[index];
+ auto value = row.GetElement(index);
+ if (field.Optional) {
+ if (!value) {
+ continue;
+ }
+ value = value.Release().GetOptionalValue();
+ }
+
+ Buf_.Write(StringMarker);
+ Buf_.WriteVarI32(field.Name.size());
+ Buf_.WriteMany(field.Name.data(), field.Name.size());
+ Buf_.Write(KeyValueSeparatorSymbol);
+
+ WriteYsonValueInTableFormat(Buf_, field.Type, std::move(value), true);
+
+ Buf_.Write(KeyedItemSeparatorSymbol);
+ }
+ Buf_.Write(EndMapSymbol);
+ Buf_.Write(ListItemSeparatorSymbol);
+ Buf_.OnRecordBoundary();
+ }
+
+ void EncodeNext(const NUdf::TUnboxedValuePod* row) final {
+ Buf_.Write(BeginMapSymbol);
+
+ for (size_t index = 0; index < Fields_.size(); ++index) {
+ const TField& field = Fields_[index];
+ auto value = row[index];
+ if (field.Optional) {
+ if (!value) {
+ continue;
+ }
+ value = value.GetOptionalValue();
+ }
+
+ Buf_.Write(StringMarker);
+ Buf_.WriteVarI32(field.Name.size());
+ Buf_.WriteMany(field.Name.data(), field.Name.size());
+ Buf_.Write(KeyValueSeparatorSymbol);
+
+ WriteYsonValueInTableFormat(Buf_, field.Type, std::move(value), true);
+
+ Buf_.Write(KeyedItemSeparatorSymbol);
+ }
+ Buf_.Write(EndMapSymbol);
+ Buf_.Write(ListItemSeparatorSymbol);
+ Buf_.OnRecordBoundary();
+ }
+private:
+ TVector<TField> Fields_;
+};
+
+class TSkiffEncoderBase: public TMkqlWriterImpl::TEncoder {
+public:
+ TSkiffEncoderBase(TOutputBuf& buf, const TMkqlIOSpecs& specs)
+ : TMkqlWriterImpl::TEncoder(buf, specs)
+ {
+ }
+
+ void EncodeNext(const NUdf::TUnboxedValuePod row) final {
+ const ui16 tableIndexVal = 0; // Always should be zero
+ Buf_.WriteMany((const char*)&tableIndexVal, sizeof(tableIndexVal));
+ EncodeData(row);
+ Buf_.OnRecordBoundary();
+ }
+
+ void EncodeNext(const NUdf::TUnboxedValuePod* row) final {
+ const ui16 tableIndexVal = 0; // Always should be zero
+ Buf_.WriteMany((const char*)&tableIndexVal, sizeof(tableIndexVal));
+ EncodeData(row);
+ Buf_.OnRecordBoundary();
+ }
+
+protected:
+ virtual void EncodeData(const NUdf::TUnboxedValuePod row) = 0;
+ virtual void EncodeData(const NUdf::TUnboxedValuePod* row) = 0;
+};
+
+class TSkiffEncoder: public TSkiffEncoderBase {
+public:
+ TSkiffEncoder(TOutputBuf& buf, const TMkqlIOSpecs& specs, size_t tableIndex)
+ : TSkiffEncoderBase(buf, specs)
+ {
+ Fields_ = GetFields(Specs_.Outputs[tableIndex].RowType);
+ NativeYtTypeFlags_ = Specs_.Outputs[tableIndex].NativeYtTypeFlags;
+ }
+
+protected:
+ void EncodeData(const NUdf::TUnboxedValuePod row) final {
+ for (size_t index = 0; index < Fields_.size(); ++index) {
+ const TField& field = Fields_[index];
+ auto value = row.GetElement(index);
+ if (field.Optional) {
+ if (!value) {
+ Buf_.Write('\0');
+ continue;
+ }
+ Buf_.Write('\1');
+ value = value.Release().GetOptionalValue();
+ }
+ WriteSkiffValue(field.Type, value, field.Optional);
+ }
+ }
+
+ void EncodeData(const NUdf::TUnboxedValuePod* row) final {
+ for (size_t index = 0; index < Fields_.size(); ++index) {
+ const TField& field = Fields_[index];
+ auto value = row[index];
+ if (field.Optional) {
+ if (!value) {
+ Buf_.Write('\0');
+ continue;
+ }
+ Buf_.Write('\1');
+ value = value.GetOptionalValue();
+ }
+ WriteSkiffValue(field.Type, value, field.Optional);
+ }
+ }
+
+ void WriteSkiffValue(TType* type, const NUdf::TUnboxedValuePod& value, bool wasOptional) {
+ if (NativeYtTypeFlags_) {
+ NCommon::WriteSkiffNativeYtValue(type, NativeYtTypeFlags_, value, Buf_);
+ } else if (type->IsData()) {
+ NCommon::WriteSkiffData(type, 0, value, Buf_);
+ } else if (!wasOptional && type->IsPg()) {
+ NCommon::WriteSkiffPg(static_cast<TPgType*>(type), value, Buf_);
+ } else {
+ WriteYsonContainerValue(type, value, Buf_);
+ }
+ }
+
+protected:
+ TVector<TField> Fields_;
+ ui64 NativeYtTypeFlags_;
+};
+
+class TSkiffEmptySchemaEncoder: public TSkiffEncoderBase {
+public:
+ TSkiffEmptySchemaEncoder(TOutputBuf& buf, const TMkqlIOSpecs& specs)
+ : TSkiffEncoderBase(buf, specs)
+ {
+ }
+
+protected:
+ void EncodeData(const NUdf::TUnboxedValuePod row) final {
+ Y_UNUSED(row);
+ Buf_.Write('\0'); // Empty optional "_yql_fake_column"
+ }
+
+ void EncodeData(const NUdf::TUnboxedValuePod* row) final {
+ Y_UNUSED(row);
+ Buf_.Write('\0'); // Empty optional "_yql_fake_column"
+ }
+};
+
+class TSkiffLLVMEncoder: public TSkiffEncoderBase {
+public:
+ typedef void (*TRowWriter)(const NUdf::TUnboxedValuePod, TOutputBuf&);
+ typedef void (*TRowFlatWriter)(const NUdf::TUnboxedValuePod*, TOutputBuf&);
+
+ TSkiffLLVMEncoder(TOutputBuf& buf, const TMkqlIOSpecs& specs, TRowWriter rowWriter, TRowFlatWriter flatWriter)
+ : TSkiffEncoderBase(buf, specs)
+ , RowWriter_(rowWriter), RowFlatWriter_(flatWriter)
+ {
+ }
+
+protected:
+ void EncodeData(const NUdf::TUnboxedValuePod row) final {
+ RowWriter_(row, Buf_);
+ }
+
+ void EncodeData(const NUdf::TUnboxedValuePod* row) final {
+ RowFlatWriter_(row, Buf_);
+ }
+private:
+ TRowWriter RowWriter_;
+ TRowFlatWriter RowFlatWriter_;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TMkqlWriterImpl::TOutput::TOutput(IOutputStream& stream, size_t blockCount, size_t blockSize, TStatTimer& timerWrite)
+ : Writer_(MakeBlockWriter(stream, blockCount, blockSize))
+ , Buf_(*Writer_, &timerWrite)
+{
+}
+
+TMkqlWriterImpl::TOutput::TOutput(NYT::TRawTableWriterPtr stream, size_t blockSize, NKikimr::NMiniKQL::TStatTimer& timerWrite)
+ : Writer_(MakeBlockWriter(*stream, 0, blockSize))
+ , Buf_(*Writer_, &timerWrite)
+{
+ Writer_->SetRecordBoundaryCallback([stream](){ stream->NotifyRowEnd(); });
+}
+
+TMkqlWriterImpl::TMkqlWriterImpl(const TVector<IOutputStream*>& streams, size_t blockCount, size_t blockSize)
+ : TimerEncode_(OutputEncodeTime)
+ , TimerWrite_(OutputWriteTime)
+{
+ for (IOutputStream* stream: streams) {
+ Outputs_.push_back(MakeHolder<TOutput>(*stream, blockCount, blockSize, TimerWrite_));
+ }
+}
+
+TMkqlWriterImpl::TMkqlWriterImpl(IOutputStream& stream, size_t blockCount, size_t blockSize)
+ : TimerEncode_(OutputEncodeTime)
+ , TimerWrite_(OutputWriteTime)
+{
+ Outputs_.push_back(MakeHolder<TOutput>(stream, blockCount, blockSize, TimerWrite_));
+}
+
+TMkqlWriterImpl::TMkqlWriterImpl(const TVector<NYT::TRawTableWriterPtr>& streams, size_t blockSize)
+ : TimerEncode_(OutputEncodeTime)
+ , TimerWrite_(OutputWriteTime)
+{
+ for (auto& stream: streams) {
+ Outputs_.push_back(MakeHolder<TOutput>(stream, blockSize, TimerWrite_));
+ }
+}
+
+TMkqlWriterImpl::TMkqlWriterImpl(NYT::TRawTableWriterPtr stream, size_t blockSize)
+ : TimerEncode_(OutputEncodeTime)
+ , TimerWrite_(OutputWriteTime)
+{
+ Outputs_.push_back(MakeHolder<TOutput>(stream, blockSize, TimerWrite_));
+}
+
+TMkqlWriterImpl::~TMkqlWriterImpl() {
+}
+
+void TMkqlWriterImpl::SetSpecs(const TMkqlIOSpecs& specs) {
+ Specs_ = &specs;
+ JobStats_ = specs.JobStats_;
+
+#ifndef MKQL_DISABLE_CODEGEN
+ THashMap<TStructType*, std::pair<llvm::Function*, llvm::Function*>> llvmFunctions;
+ if (Specs_->UseSkiff_ && Specs_->OptLLVM_ != "OFF") {
+ for (size_t i: xrange(Specs_->Outputs.size())) {
+ auto rowType = Specs_->Outputs[i].RowType;
+ if (rowType->GetMembersCount() != 0 && !llvmFunctions.contains(rowType)) {
+ if (!Codegen_) {
+ Codegen_ = NCodegen::ICodegen::Make(NCodegen::ETarget::Native);
+ Codegen_->LoadBitCode(GetYtCodecBitCode(), "YtCodecFuncs");
+ }
+
+ const auto writer1 = MakeYtCodecCgWriter<false>(Codegen_, rowType);
+ const auto writer2 = MakeYtCodecCgWriter<true>(Codegen_, rowType);
+ for (ui32 index = 0; index < rowType->GetMembersCount(); ++index) {
+ auto fieldType = rowType->GetMemberType(index);
+ writer1->AddField(fieldType, Specs_->Outputs[i].NativeYtTypeFlags);
+ writer2->AddField(fieldType, Specs_->Outputs[i].NativeYtTypeFlags);
+ }
+
+ llvmFunctions.emplace(rowType, std::make_pair(writer1->Build(), writer2->Build()));
+ }
+ }
+ if (!llvmFunctions.empty()) {
+ Codegen_->Verify();
+ YtCodecAddMappings(*Codegen_);
+ Codegen_->Compile();
+ }
+ else {
+ Codegen_.reset();
+ }
+ }
+#endif
+
+ for (size_t i: xrange(Outputs_.size())) {
+ auto& out = Outputs_[i];
+ out->Buf_.SetStats(JobStats_);
+ if (Specs_->UseSkiff_) {
+ if (Specs_->Outputs[i].RowType->GetMembersCount() == 0) {
+ Encoders_.emplace_back(new TSkiffEmptySchemaEncoder(out->Buf_, *Specs_));
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ else if (auto p = llvmFunctions.FindPtr(Specs_->Outputs[i].RowType)) {
+ Encoders_.emplace_back(new TSkiffLLVMEncoder(out->Buf_, *Specs_,
+ (TSkiffLLVMEncoder::TRowWriter)Codegen_->GetPointerToFunction(p->first),
+ (TSkiffLLVMEncoder::TRowFlatWriter)Codegen_->GetPointerToFunction(p->second)
+ ));
+ }
+#endif
+ else {
+ Encoders_.emplace_back(new TSkiffEncoder(out->Buf_, *Specs_, i));
+ }
+ } else {
+ Encoders_.emplace_back(new TYsonEncoder(out->Buf_, *Specs_, i));
+ }
+ }
+}
+
+void TMkqlWriterImpl::AddRow(const NUdf::TUnboxedValuePod row) {
+ const auto guard = Guard<TStatTimer>(TimerEncode_);
+ if (Encoders_.size() == 1U) {
+ Encoders_.front()->EncodeNext(row);
+ } else {
+ const auto tableIndex = row.GetVariantIndex();
+ YQL_ENSURE(tableIndex < Encoders_.size(), "Wrong table index: " << tableIndex
+ << ", there are only " << Encoders_.size() << " outputs");
+ const auto item = row.GetVariantItem().Release();
+ Encoders_[tableIndex]->EncodeNext(item);
+ }
+
+ if (WriteLimit) {
+ ui64 res = 0;
+ for (auto& x : Outputs_) {
+ res += x->Buf_.GetWrittenBytes();
+ }
+ if (res > *WriteLimit) {
+ throw TMemoryLimitExceededException();
+ }
+ }
+}
+
+void TMkqlWriterImpl::AddFlatRow(const NUdf::TUnboxedValuePod* row) {
+ YQL_ENSURE(Encoders_.size() == 1U, "Expected single table.");
+ const auto guard = Guard<TStatTimer>(TimerEncode_);
+ Encoders_.front()->EncodeNext(row);
+ if (WriteLimit) {
+ ui64 res = 0;
+ for (auto& x : Outputs_) {
+ res += x->Buf_.GetWrittenBytes();
+ }
+ if (res > *WriteLimit) {
+ throw TMemoryLimitExceededException();
+ }
+ }
+}
+
+void TMkqlWriterImpl::Finish() {
+ if (!IsFinished_) {
+ IsFinished_ = true;
+ DoFinish(false);
+ }
+}
+
+void TMkqlWriterImpl::Abort() {
+ if (!IsFinished_) {
+ IsFinished_ = true;
+ DoFinish(true);
+ }
+}
+
+void TMkqlWriterImpl::DoFinish(bool abort) {
+ auto guard = Guard<TStatTimer>(TimerEncode_);
+ if (!abort) {
+ for (auto& x : Outputs_) {
+ x->Buf_.Finish();
+ }
+ }
+
+ Report();
+}
+
+void TMkqlWriterImpl::Report() {
+ TimerEncode_.Report(JobStats_);
+ TimerWrite_.Report(JobStats_);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+THolder<IInputStream> MakeStringInput(const TString& str, bool decompress) {
+ if (decompress) {
+ return MakeHolder<THoldingStream<TBrotliDecompress, TStringInput>>(MakeHolder<TStringInput>(str));
+ } else {
+ return MakeHolder<TStringInput>(str);
+ }
+}
+
+THolder<IInputStream> MakeFileInput(const TString& file, bool decompress) {
+ if (decompress) {
+ return MakeHolder<THoldingStream<TBrotliDecompress, TUnbufferedFileInput>>(MakeHolder<TUnbufferedFileInput>(file));
+ } else {
+ return MakeHolder<TUnbufferedFileInput>(file);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+NUdf::TUnboxedValue DecodeYamr(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TYaMRRow& row) {
+ NUdf::TUnboxedValue* items;
+ auto ret = specsCache.NewRow(tableIndex, items);
+ auto& decoder = *specsCache.GetSpecs().Inputs.at(tableIndex);
+ auto& lastFields = specsCache.GetLastFields(tableIndex);
+
+ TVector<TStringBuf> values{row.Key, row.SubKey, row.Value};
+ for (size_t i = 0; i < YAMR_FIELDS.size(); ++i) {
+ const TMkqlIOSpecs::TDecoderSpec::TDecodeField* field = nullptr;
+ if (i < lastFields.size()) {
+ auto lastField = lastFields[i];
+ if (lastField->Name == YAMR_FIELDS[i]) {
+ field = lastField;
+ }
+ }
+ if (!field) {
+ auto it = decoder.Fields.find(YAMR_FIELDS[i]);
+ if (it != decoder.Fields.end()) {
+ field = &it->second;
+ }
+ }
+ if (field) {
+ items[field->StructIndex] = MakeString(NUdf::TStringRef(values[i].data(), values[i].size()));
+ }
+ }
+
+ return ret;
+}
+
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TNode& value, IOutputStream& ysonOut) {
+ auto& decoder = *specsCache.GetSpecs().Inputs.at(tableIndex);
+
+ TVector<TString> items(decoder.StructSize);
+ TMaybe<THashMap<TString, TString>> others;
+ if (decoder.OthersStructIndex) {
+ others.ConstructInPlace();
+ others->reserve(specsCache.GetMaxOthersFields(tableIndex));
+ }
+
+ for (auto& node: value.AsMap()) {
+ auto& name = node.first;
+ const TMkqlIOSpecs::TDecoderSpec::TDecodeField* field = nullptr;
+ if (!decoder.OthersStructIndex || name != YqlOthersColumnName) {
+ auto it = decoder.Fields.find(name);
+ if (it != decoder.Fields.end()) {
+ field = &it->second;
+ }
+ }
+
+ if (!field && decoder.OthersStructIndex) {
+ if (node.second.IsString()) {
+ others->emplace(name, node.second.AsString());
+ } else if (node.second.IsEntity() || node.second.IsBool() || node.second.IsInt64() || node.second.IsUint64() || node.second.IsDouble()) {
+ others->emplace(name, NYT::NodeToYsonString(node.second, NYT::NYson::EYsonFormat::Text));
+ } else {
+ others->emplace(name, NYT::NodeToYsonString(node.second, NYT::NYson::EYsonFormat::Binary));
+ }
+ continue;
+ }
+
+ if (field->StructIndex != Max<ui32>()) {
+ NYT::TNode res = node.second;
+ auto dataType = field->Type;
+ if (field->Type->IsOptional()) {
+ dataType = static_cast<TOptionalType*>(field->Type)->GetItemType();
+ if (res.IsEntity()) {
+ res = NYT::TNode::CreateList();
+ } else {
+ res = NYT::TNode::CreateList().Add(node.second);
+ }
+ } else if (res.IsEntity()) {
+ res = NYT::TNode();
+ }
+ if (res.GetType() != NYT::TNode::Undefined) {
+ if (dataType->GetKind() == TType::EKind::Data && static_cast<TDataType*>(dataType)->GetSchemeType() == NUdf::TDataType<NUdf::TYson>::Id) {
+ items[field->StructIndex] = NCommon::EncodeRestrictedYson(res, NYT::NYson::EYsonFormat::Binary);
+ } else {
+ items[field->StructIndex] = NYT::NodeToYsonString(res, NYT::NYson::EYsonFormat::Binary);
+ }
+ }
+ }
+ }
+
+ WriteRowItems(specsCache, tableIndex, items, others, ysonOut);
+}
+
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TYaMRRow& value, IOutputStream& ysonOut) {
+ auto& decoder = *specsCache.GetSpecs().Inputs.at(tableIndex);
+ auto& lastFields = specsCache.GetLastFields(tableIndex);
+
+ TVector<TString> items(decoder.StructSize);
+ TVector<TStringBuf> values{value.Key, value.SubKey, value.Value};
+ for (size_t i = 0; i < YAMR_FIELDS.size(); ++i) {
+ const TMkqlIOSpecs::TDecoderSpec::TDecodeField* field = nullptr;
+ if (i < lastFields.size()) {
+ auto lastField = lastFields[i];
+ if (lastField->Name == YAMR_FIELDS[i]) {
+ field = lastField;
+ }
+ }
+ if (!field) {
+ auto it = decoder.Fields.find(YAMR_FIELDS[i]);
+ if (it != decoder.Fields.end()) {
+ field = &it->second;
+ }
+ }
+ if (field) {
+ items[field->StructIndex] = NYT::NodeToYsonString(NYT::TNode(values[i]), NYT::NYson::EYsonFormat::Binary);
+ }
+ }
+
+ WriteRowItems(specsCache, tableIndex, items, {}, ysonOut);
+}
+
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NUdf::TUnboxedValuePod& value, IOutputStream& ysonOut) {
+ auto& decoder = *specsCache.GetSpecs().Inputs.at(tableIndex);
+ TVector<TString> items(decoder.StructSize);
+ for (ui32 i = 0; i < decoder.StructSize; ++i) {
+ items[i] = NCommon::WriteYsonValue(value.GetElement(decoder.FieldsVec[i].StructIndex), decoder.FieldsVec[i].Type, nullptr, NYT::NYson::EYsonFormat::Binary);
+ }
+ WriteRowItems(specsCache, tableIndex, items, {}, ysonOut);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec_io.h b/ydb/library/yql/providers/yt/codec/yt_codec_io.h
new file mode 100644
index 0000000000..78aa1ba17c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_io.h
@@ -0,0 +1,161 @@
+#pragma once
+
+#include "yt_codec.h"
+#include <ydb/library/yql/providers/common/codec/yql_codec_buf.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/minikql/aligned_page_pool.h>
+
+#ifndef MKQL_DISABLE_CODEGEN
+#include <ydb/library/yql/minikql/codegen/codegen.h>
+#endif
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/io/stream_table_reader.h>
+
+#include <library/cpp/yson/zigzag.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/ptr.h>
+#include <util/generic/xrange.h>
+#include <util/generic/yexception.h>
+#include <util/generic/maybe.h>
+#include <util/stream/output.h>
+#include <util/stream/input.h>
+#include <util/stream/holder.h>
+#include <util/system/compiler.h>
+
+namespace NYql {
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TMkqlReaderImpl : public IMkqlReaderImpl {
+public:
+ struct TDecoder;
+
+ // Input source must be valid while TMkqlReaderImpl exists
+ TMkqlReaderImpl();
+ TMkqlReaderImpl(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize, ui32 tableIndex = 0, bool ignoreStreamTableIndex = false);
+ ~TMkqlReaderImpl();
+
+ void SetReader(NYT::TRawTableReader& source, size_t blockCount, size_t blockSize, ui32 tableIndex = 0, bool ignoreStreamTableIndex = false);
+ void SetSpecs(const TMkqlIOSpecs& specs, const NKikimr::NMiniKQL::THolderFactory& holderFactory);
+ void SetDeadline(TInstant deadline) {
+ Reader_->SetDeadline(deadline);
+ }
+ void SetNextBlockCallback(std::function<void()> cb) {
+ Buf_.SetNextBlockCallback(cb);
+ }
+
+ NKikimr::NUdf::TUnboxedValue GetRow() const override;
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+
+ void Finish();
+
+protected:
+ NKikimr::NUdf::TUnboxedValue BuildOthers(const TMkqlIOSpecs::TDecoderSpec& decoder, NKikimr::NMiniKQL::TValuesDictHashMap& others);
+
+ void CheckValidity() const;
+ void CheckReadRow() const;
+
+ void OnError(TStringBuf msg);
+
+protected:
+ NKikimr::NMiniKQL::TSamplingStatTimer TimerDecode_;
+ NKikimr::NMiniKQL::TSamplingStatTimer TimerRead_;
+ THolder<NCommon::IBlockReader> Reader_;
+ NCommon::TInputBuf Buf_;
+ THolder<TDecoder> Decoder_;
+
+ bool HasRangeIndices_ = false;
+ ui32 InitialTableIndex_ = 0;
+ bool IgnoreStreamTableIndex_ = false;
+
+ const TMkqlIOSpecs* Specs_ = nullptr;
+ const NKikimr::NMiniKQL::THolderFactory* HolderFactoryPtr = nullptr;
+ NKikimr::NMiniKQL::IStatsRegistry* JobStats_ = nullptr;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TMkqlWriterImpl: public IMkqlWriterImpl {
+protected:
+ struct TOutput {
+ TOutput(IOutputStream& streams, size_t blockCount, size_t blockSize, NKikimr::NMiniKQL::TStatTimer& timerWrite);
+ TOutput(NYT::TRawTableWriterPtr stream, size_t blockSize, NKikimr::NMiniKQL::TStatTimer& timerWrite);
+
+ THolder<NCommon::IBlockWriter> Writer_;
+ NCommon::TOutputBuf Buf_;
+ };
+
+public:
+ struct TEncoder;
+
+ // Output streams must be valid while TMkqlWriterImpl exists
+ TMkqlWriterImpl(const TVector<IOutputStream*>& streams, size_t blockCount, size_t blockSize);
+ TMkqlWriterImpl(IOutputStream& stream, size_t blockCount, size_t blockSize);
+ // client writer
+ TMkqlWriterImpl(const TVector<NYT::TRawTableWriterPtr>& streams, size_t blockSize);
+ TMkqlWriterImpl(NYT::TRawTableWriterPtr stream, size_t blockSize);
+
+ ~TMkqlWriterImpl();
+
+ void SetSpecs(const TMkqlIOSpecs& specs);
+
+ void AddRow(const NKikimr::NUdf::TUnboxedValuePod row) override;
+ void AddFlatRow(const NUdf::TUnboxedValuePod* row) override;
+
+ void SetWriteLimit(ui64 limit) {
+ WriteLimit = limit;
+ }
+
+ void Finish() override;
+ void Abort() override;
+
+ void Report();
+
+protected:
+ void WriteSkiffValue(NCommon::TOutputBuf& buf, NKikimr::NMiniKQL::TType* type, const NKikimr::NUdf::TUnboxedValuePod& value);
+ virtual void DoFinish(bool abort);
+
+protected:
+ NKikimr::NMiniKQL::TStatTimer TimerEncode_;
+ NKikimr::NMiniKQL::TStatTimer TimerWrite_;
+ const TMkqlIOSpecs* Specs_ = nullptr;
+ NKikimr::NMiniKQL::IStatsRegistry* JobStats_ = nullptr;
+ TVector<THolder<TOutput>> Outputs_;
+ bool IsFinished_ = false;
+ TVector<THolder<TEncoder>> Encoders_;
+#ifndef MKQL_DISABLE_CODEGEN
+ NCodegen::ICodegen::TPtr Codegen_;
+#endif
+ TMaybe<ui64> WriteLimit;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TMkqlInput: public THoldingStream<NYT::NDetail::TInputStreamProxy, IInputStream> {
+ using TBase = THoldingStream<NYT::NDetail::TInputStreamProxy, IInputStream>;
+public:
+ TMkqlInput(::THolder<IInputStream> h)
+ : TBase(std::move(h))
+ {
+ }
+};
+
+THolder<IInputStream> MakeStringInput(const TString& str, bool decompress);
+THolder<IInputStream> MakeFileInput(const TString& file, bool decompress);
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+NKikimr::NUdf::TUnboxedValue DecodeYamr(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TYaMRRow& row);
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TNode& value, IOutputStream& ysonOut);
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NYT::TYaMRRow& value, IOutputStream& ysonOut);
+void DecodeToYson(TMkqlIOCache& specsCache, size_t tableIndex, const NKikimr::NUdf::TUnboxedValuePod& value, IOutputStream& ysonOut);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec_io_ut.cpp b/ydb/library/yql/providers/yt/codec/yt_codec_io_ut.cpp
new file mode 100644
index 0000000000..91951c0f5c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_io_ut.cpp
@@ -0,0 +1,308 @@
+#include "yt_codec_io.h"
+
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_alloc.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_mem_info.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+
+#include <library/cpp/yson/node/node_visitor.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/stream/buffer.h>
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/buffer.h>
+#include <util/generic/ylimits.h>
+
+using namespace NYql;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+
+class TTestInput: public NYT::TRawTableReader {
+public:
+ TTestInput(const NYT::TNode::TListType& records, size_t fails, size_t retries, bool failAtStart, bool omitLastSemicolon)
+ : Records(records)
+ , Fails(fails)
+ , Retries(retries)
+ , FailAtStart(failAtStart)
+ , OmitLastSemicolon(omitLastSemicolon)
+ {
+ Prepare();
+ }
+
+ bool Retry(const TMaybe<ui32>& /*rangeIndex*/, const TMaybe<ui64>& rowIndex) override {
+ if (0 == Retries) {
+ return false;
+ }
+ --Retries;
+ Prepare(rowIndex.GetOrElse(0));
+ CurrentOffset = 0;
+ return true;
+ }
+
+ void ResetRetries() override {
+ }
+
+ // Returns 'true' if the input stream may contain table ranges.
+ // The TRawTableReader user is responsible to track active range index in this case
+ // in order to pass it to Retry().
+ bool HasRangeIndices() const override {
+ return false;
+ }
+
+protected:
+ void Prepare(size_t fromRowIndex = 0) {
+ Y_VERIFY(fromRowIndex < Records.size());
+ AtStart = true;
+ Data.Buffer().Clear();
+ Data.Rewind();
+ TBinaryYsonWriter writer(&Data);
+ NYT::TNodeVisitor visitor(&writer);
+ auto entity = NYT::TNode::CreateEntity();
+ entity.Attributes()("row_index", i64(fromRowIndex));
+ visitor.Visit(entity);
+ for (size_t i = fromRowIndex; i < Records.size(); ++i) {
+ Data.Write(';');
+ visitor.Visit(Records[i]);
+ }
+ if (!OmitLastSemicolon) {
+ Data.Write(';');
+ }
+ }
+
+ size_t DoRead(void* buf, size_t len) override {
+ size_t read = Data.Read(buf, len);
+ if (AtStart == FailAtStart && Fails > 0 && read > 0) {
+ --Fails;
+ throw yexception() << "Fail";
+ }
+ AtStart = false;
+ return read;
+ }
+
+private:
+ const NYT::TNode::TListType& Records;
+ TBufferStream Data;
+ size_t Fails;
+ size_t Retries;
+ const bool FailAtStart;
+ size_t CurrentOffset = 0;
+ bool AtStart = true;
+ bool OmitLastSemicolon = false;
+};
+
+struct TMkqlCodecFixture {
+ TIntrusivePtr<IFunctionRegistry> FunctionRegistry;
+ TScopedAlloc Alloc;
+ TTypeEnvironment Env;
+ TMemoryUsageInfo MemInfo;
+ THolderFactory HolderFactory;
+ NCommon::TCodecContext CodecCtx;
+ TMkqlIOSpecs Specs;
+
+ static const TString INPUT_SPEC;
+ static const TString VALUE;
+
+ TMkqlCodecFixture()
+ : FunctionRegistry(CreateFunctionRegistry(IBuiltinFunctionRegistry::TPtr()))
+ , Alloc(__LOCATION__)
+ , Env(Alloc)
+ , MemInfo("Test")
+ , HolderFactory(Alloc.Ref(), MemInfo, FunctionRegistry.Get())
+ , CodecCtx(Env, *FunctionRegistry, &HolderFactory)
+ {
+ Specs.Init(CodecCtx, INPUT_SPEC, {}, Nothing());
+ }
+
+ static TVector<NYT::TNode> Generate(size_t numRecords) {
+ TVector<NYT::TNode> data;
+ for (size_t i = 0; i < numRecords; i++) {
+ data.push_back(NYT::TNode()("key", i)("value", VALUE));
+ }
+ return data;
+ }
+
+ static void Validate(size_t rowIndex, const NUdf::TUnboxedValue& row) {
+ UNIT_ASSERT_VALUES_EQUAL(row.GetElement(0).Get<ui64>(), rowIndex + 1);
+ UNIT_ASSERT_VALUES_EQUAL(row.GetElement(1).Get<ui64>(), rowIndex);
+ auto value = row.GetElement(2);
+ UNIT_ASSERT_VALUES_EQUAL(TStringBuf(value.AsStringRef()), VALUE);
+ }
+};
+
+const TString TMkqlCodecFixture::INPUT_SPEC = R"({
+ tables = [{
+ "_yql_row_spec" = {
+ "Type" = [
+ "StructType"; [
+ ["key"; ["DataType"; "Uint64"]];
+ ["value"; ["DataType"; "String"]]
+ ]
+ ]
+ };
+ "_yql_sys_table" = [
+ "record"
+ ]
+ }]
+})";
+
+const TString TMkqlCodecFixture::VALUE = TString().append(100, 'z');
+
+} // unnamed
+
+Y_UNIT_TEST_SUITE(TMkqlCodec) {
+
+ void TestRead(size_t blockCount) {
+ TMkqlCodecFixture fixture;
+ auto data = TMkqlCodecFixture::Generate(10);
+ // With semicolon in end of input
+ {
+ TTestInput input(data, 0, 0, false, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ // Without semicolon in end of input
+ {
+ TTestInput input(data, 0, 0, false, true);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ }
+
+ Y_UNIT_TEST(ReadSync) {
+ TestRead(0);
+ }
+
+ Y_UNIT_TEST(ReadAsync) {
+ TestRead(4);
+ }
+
+ void TestReadFail(size_t blockCount) {
+ TMkqlCodecFixture fixture;
+ auto data = TMkqlCodecFixture::Generate(10);
+ // Before first record
+ {
+ TTestInput input(data, 1, 0, true, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ UNIT_ASSERT_EXCEPTION(reader.Next(), yexception);
+ }
+ // In the middle
+ {
+ TTestInput input(data, 1, 0, false, false);
+ TMkqlReaderImpl reader(input, blockCount, 2 * TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+ UNIT_ASSERT(reader.IsValid());
+ UNIT_ASSERT_EXCEPTION(reader.Next(), yexception);
+ }
+ }
+
+ Y_UNIT_TEST(ReadSyncFail) {
+ TestReadFail(0);
+ }
+
+ Y_UNIT_TEST(ReadAsyncFail) {
+ TestReadFail(4);
+ }
+
+ void TestReadRetry(size_t blockCount) {
+ TMkqlCodecFixture fixture;
+ auto data = TMkqlCodecFixture::Generate(10);
+ // Before first record
+ {
+ TTestInput input(data, 1, 1, true, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ // In the middle
+ {
+ TTestInput input(data, 3, 3, false, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ // Fail through time
+ {
+ TTestInput input(data, Max(), Max(), false, false);
+ TMkqlReaderImpl reader(input, blockCount, 2 * TMkqlCodecFixture::VALUE.size());
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ // Small buffer
+ {
+ TTestInput input(data, blockCount + 1, blockCount + 1, false, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size() / 2);
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ // Large buffer
+ {
+ TTestInput input(data, blockCount + 1, blockCount + 1, false, false);
+ TMkqlReaderImpl reader(input, blockCount, TMkqlCodecFixture::VALUE.size() * 2);
+ reader.SetSpecs(fixture.Specs, fixture.HolderFactory);
+ reader.Next();
+
+ for (size_t i = 0; i < 10; reader.Next(), i++) {
+ UNIT_ASSERT(reader.IsValid());
+ TMkqlCodecFixture::Validate(i, reader.GetRow());
+ }
+ UNIT_ASSERT(!reader.IsValid());
+ }
+ }
+
+ Y_UNIT_TEST(ReadSyncRetry) {
+ TestReadRetry(0);
+ }
+
+ Y_UNIT_TEST(ReadAsyncRetry) {
+ TestReadRetry(4);
+ }
+
+}
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp b/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
new file mode 100644
index 0000000000..4ad929a5c5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_job.cpp
@@ -0,0 +1,65 @@
+#include "yt_codec_job.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+
+#include <yt/cpp/mapreduce/io/job_reader.h>
+#include <yt/cpp/mapreduce/io/job_writer.h>
+
+#include <util/system/file.h>
+#include <util/stream/output.h>
+#include <util/generic/xrange.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+namespace NPrivate {
+
+TInStreamHolder::TInStreamHolder(const TFile& inHandle)
+ : Input(inHandle)
+ , Proxy(&Input)
+{
+}
+
+TOutStreamsHolder::TOutStreamsHolder(const TVector<TFile>& outHandles) {
+ Outputs.reserve(outHandles.size());
+ for (auto& h: outHandles) {
+ Outputs.emplace_back(h);
+ }
+}
+
+TVector<IOutputStream*> TOutStreamsHolder::GetVectorOfStreams() {
+ TVector<IOutputStream*> res(Reserve(Outputs.size()));
+ for (auto& s: Outputs) {
+ res.push_back(&s);
+ }
+ return res;
+}
+
+} // NPrivate
+
+TJobMkqlReaderImpl::TJobMkqlReaderImpl(const TFile& in)
+ : NPrivate::TInStreamHolder(in)
+ , TMkqlReaderImpl(GetYtStream(), YQL_JOB_CODEC_BLOCK_COUNT, YQL_JOB_CODEC_BLOCK_SIZE)
+{
+}
+
+TJobMkqlWriterImpl::TJobMkqlWriterImpl(const TMkqlIOSpecs& specs, const TVector<TFile>& outHandles)
+ : NPrivate::TOutStreamsHolder(outHandles)
+ , TMkqlWriterImpl(GetVectorOfStreams(), YQL_JOB_CODEC_BLOCK_COUNT, YQL_JOB_CODEC_BLOCK_SIZE)
+{
+ SetSpecs(specs);
+}
+
+void TJobMkqlWriterImpl::DoFinish(bool abort) {
+ TMkqlWriterImpl::DoFinish(abort);
+ if (!abort) {
+ for (auto& out: Outputs) {
+ out.Flush();
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/codec/yt_codec_job.h b/ydb/library/yql/providers/yt/codec/yt_codec_job.h
new file mode 100644
index 0000000000..e2a8232d2c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/codec/yt_codec_job.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "yt_codec_io.h"
+
+#include <yt/cpp/mapreduce/io/stream_table_reader.h>
+
+#include <util/system/file.h>
+#include <util/stream/file.h>
+
+
+namespace NYql {
+
+namespace NPrivate {
+ class TInStreamHolder {
+ public:
+ TInStreamHolder(const TFile& inHandle);
+ NYT::TRawTableReader& GetYtStream() {
+ return Proxy;
+ }
+
+ protected:
+ TUnbufferedFileInput Input;
+ NYT::NDetail::TInputStreamProxy Proxy;
+ };
+
+ class TOutStreamsHolder {
+ public:
+ TOutStreamsHolder(const TVector<TFile>& outHandles);
+ TVector<IOutputStream*> GetVectorOfStreams();
+
+ protected:
+ TVector<TUnbufferedFileOutput> Outputs;
+ };
+}
+
+class TJobMkqlReaderImpl: protected NPrivate::TInStreamHolder, public TMkqlReaderImpl {
+public:
+ TJobMkqlReaderImpl(const TFile& in);
+ ~TJobMkqlReaderImpl() = default;
+};
+
+
+class TJobMkqlWriterImpl: protected NPrivate::TOutStreamsHolder, public TMkqlWriterImpl {
+public:
+ TJobMkqlWriterImpl(const TMkqlIOSpecs& specs, const TVector<TFile>& outHandles);
+ ~TJobMkqlWriterImpl() = default;
+
+private:
+ void DoFinish(bool abort) override;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/common/ya.make b/ydb/library/yql/providers/yt/common/ya.make
new file mode 100644
index 0000000000..409a455f97
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/ya.make
@@ -0,0 +1,24 @@
+LIBRARY()
+
+SRCS(
+ yql_configuration.cpp
+ yql_names.cpp
+ yql_yt_settings.cpp
+)
+
+PEERDIR(
+ library/cpp/regex/pcre
+ library/cpp/string_utils/parse_size
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/ast
+ ydb/library/yql/utils/log
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/config
+)
+
+YQL_LAST_ABI_VERSION()
+
+GENERATE_ENUM_SERIALIZATION(yql_yt_settings.h)
+
+END()
diff --git a/ydb/library/yql/providers/yt/common/yql_configuration.cpp b/ydb/library/yql/providers/yt/common/yql_configuration.cpp
new file mode 100644
index 0000000000..02d5a2dd57
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_configuration.cpp
@@ -0,0 +1 @@
+#include "yql_configuration.h"
diff --git a/ydb/library/yql/providers/yt/common/yql_configuration.h b/ydb/library/yql/providers/yt/common/yql_configuration.h
new file mode 100644
index 0000000000..24bbe4a9a9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_configuration.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <util/system/types.h>
+#include <util/generic/size_literals.h>
+
+namespace NYql {
+
+constexpr size_t YQL_JOB_CODEC_BLOCK_COUNT = 16;
+constexpr size_t YQL_JOB_CODEC_BLOCK_SIZE = 1_MB;
+
+constexpr size_t YQL_JOB_CODEC_MEM = YQL_JOB_CODEC_BLOCK_COUNT * YQL_JOB_CODEC_BLOCK_SIZE + (30_MB);
+
+constexpr ui64 DEFAULT_TOP_SORT_LIMIT = 1000ULL;
+
+constexpr ui16 DEFAULT_WIDE_FLOW_LIMIT = 101U;
+
+constexpr bool DEFAULT_USE_FLOW = true;
+
+constexpr bool DEFAULT_USE_NATIVE_YT_TYPES = false;
+
+constexpr bool DEFAULT_USE_INTERMEDIATE_SCHEMA = true;
+
+constexpr bool DEFAULT_USE_SKIFF = true;
+
+constexpr bool DEFAULT_USE_SYS_COLUMNS = true;
+
+constexpr bool DEFAULT_USE_MULTISET_ATTRS = true;
+
+constexpr bool DEFAULT_MAP_JOIN_USE_FLOW = true;
+
+constexpr bool DefaultHybridDqExecution = false;
+constexpr auto DefaultHybridDqDataSizeLimitForOrdered = 1_GB;
+constexpr auto DefaultHybridDqDataSizeLimitForUnordered = 16_GB;
+
+constexpr bool DEFAULT_ROW_SPEC_COMPACT_FORM = false;
+
+constexpr bool DEFAULT_USE_NEW_PREDICATE_EXTRACTION = false;
+constexpr bool DEFAULT_PRUNE_KEY_FILTER_LAMBDA = false;
+constexpr bool DEFAULT_DQ_PRUNE_KEY_FILTER_LAMBDA = false;
+constexpr bool DEFAULT_MERGE_ADJACENT_POINT_RANGES = true;
+constexpr bool DEFAULT_KEY_FILTER_FOR_STARTS_WITH = true;
+constexpr ui64 DEFAULT_MAX_KEY_RANGE_COUNT = 1000ULL;
+
+constexpr bool DEFAULT_USE_NATIVE_DESC_SORT = false;
+
+constexpr ui64 DEFAULT_MAX_CHUNKS_FOR_DQ_READ = 500;
+
+constexpr bool DEFAULT_USE_KEY_BOUND_API = false;
+
+constexpr ui32 DEFAULT_MAX_OPERATION_FILES = 1000;
+
+constexpr bool DEFAULT_JOIN_COMMON_USE_MULTI_OUT = false;
+
+constexpr bool DEFAULT_USE_RPC_READER_IN_DQ = false;
+constexpr size_t DEFAULT_RPC_READER_INFLIGHT = 1;
+
+constexpr auto DEFAULT_SWITCH_MEMORY_LIMIT = 128_MB;
+
+constexpr ui32 DEFAULT_MAX_INPUT_TABLES = 3000;
+constexpr ui32 DEFAULT_MAX_OUTPUT_TABLES = 100;
+constexpr ui64 DEFAULT_APPLY_STORED_CONSTRAINTS = 0ULL;
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/common/yql_names.cpp b/ydb/library/yql/providers/yt/common/yql_names.cpp
new file mode 100644
index 0000000000..3cb0193565
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_names.cpp
@@ -0,0 +1,11 @@
+#include "yql_names.h"
+
+namespace NYql {
+
+const TVector<TStringBuf> YAMR_FIELDS = {
+ TStringBuf("key"),
+ TStringBuf("subkey"),
+ TStringBuf("value"),
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/common/yql_names.h b/ydb/library/yql/providers/yt/common/yql_names.h
new file mode 100644
index 0000000000..3b6adff996
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_names.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+const TStringBuf YqlRowSpecAttribute = "_yql_row_spec";
+
+const TStringBuf RowSpecAttrType = "Type";
+const TStringBuf RowSpecAttrTypePatch = "TypePatch";
+const TStringBuf RowSpecAttrStrictSchema = "StrictSchema";
+const TStringBuf RowSpecAttrUniqueKeys = "UniqueKeys";
+const TStringBuf RowSpecAttrSortDirections = "SortDirections";
+const TStringBuf RowSpecAttrSortMembers = "SortMembers";
+const TStringBuf RowSpecAttrSortedBy = "SortedBy";
+const TStringBuf RowSpecAttrSortedByTypes = "SortedByTypes";
+const TStringBuf RowSpecAttrDefaultValues = "DefaultValues";
+const TStringBuf RowSpecAttrWeakFields = "WeakFields";
+const TStringBuf RowSpecAttrUseTypeV2 = "UseTypeV2";
+const TStringBuf RowSpecAttrUseNativeYtTypes = "UseNativeYtTypes";
+const TStringBuf RowSpecAttrNativeYtTypeFlags = "NativeYtTypeFlags";
+const TStringBuf RowSpecAttrExplicitYson = "ExplicitYson";
+const TStringBuf RowSpecAttrConstraints = "Constraints";
+
+const TStringBuf YqlReadUdfAttribute = "_yql_read_udf";
+const TStringBuf YqlReadUdfTypeConfigAttribute = "_yql_read_udf_type_config";
+const TStringBuf YqlReadUdfRunConfigAttribute = "_yql_read_udf_run_config";
+const TStringBuf YqlViewPrefixAttribute = "_yql_view_";
+const TStringBuf YqlProtoFieldPrefixAttribute = "_yql_proto_field_";
+const TStringBuf YqlDynamicAttribute = "_yql_dynamic";
+
+const TStringBuf YqlSysColumnPrefix = "_yql_sys_table";
+const TStringBuf YqlSysColumnPath = "_yql_sys_tablepath";
+const TStringBuf YqlSysColumnRecord = "_yql_sys_tablerecord";
+const TStringBuf YqlSysColumnIndex = "_yql_sys_tableindex";
+const TStringBuf YqlSysColumnNum = "_yql_sys_tablenum";
+const TStringBuf YqlSysColumnKeySwitch = "_yql_sys_tablekeyswitch";
+
+const TStringBuf YqlOthersColumnName = "_other";
+const TStringBuf YqlTypeAttribute = "_yql_type";
+const TStringBuf YqlTypeView = "view";
+
+const TStringBuf QB2Premapper = "_qb2_premapper";
+
+// Generated by SQL parser
+const TStringBuf MrTableRangeName = "MrTableRange";
+const TStringBuf MrTableRangeStrictName = "MrTableRangeStrict";
+const TStringBuf MrTableConcatName = "MrTableConcat";
+const TStringBuf MrFolderName = "MrFolder";
+const TStringBuf MrRangeInputListInternal = "MrRangeInputListInternal";
+
+// YT related names
+const TStringBuf READ_SCHEMA_ATTR_NAME = "_read_schema";
+const TStringBuf INFER_SCHEMA_ATTR_NAME = "_infer_schema";
+const TStringBuf SCHEMA_ATTR_NAME = "schema";
+const TStringBuf SCHEMA_MODE_ATTR_NAME = "schema_mode";
+const TStringBuf FORMAT_ATTR_NAME = "_format";
+
+const TStringBuf KeyFilterName = "keyFilter";
+const TStringBuf CurrentYtClusterShortcut = "current";
+
+const TStringBuf YqlIOSpecTables = "tables";
+const TStringBuf YqlIOSpecRegistry = "registry";
+
+extern const TVector<TStringBuf> YAMR_FIELDS;
+
+}
diff --git a/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp b/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
new file mode 100644
index 0000000000..588f267f9c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_yt_settings.cpp
@@ -0,0 +1,516 @@
+#include "yql_yt_settings.h"
+
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/regex/pcre/regexp.h>
+
+#include <util/generic/yexception.h>
+#include <util/generic/size_literals.h>
+#include <util/generic/vector.h>
+#include <util/string/cast.h>
+#include <util/string/split.h>
+#include <util/system/compiler.h>
+
+namespace NYql {
+
+using namespace NCommon;
+
+namespace {
+
+// See https://wiki.yandex-team.ru/yt/userdoc/compression/#podderzhivaemyealgoritmyszhatija
+const TRegExMatch CODECS("none|snappy|zlib_[1-9]|lz4(_high_compression)?|quick_lz|zstd_([1-9]|1[0-9]|2[0-1])|brotli_([1-9]|1[0-1])|lzma_[0-9]|bzip2_[1-9]");
+
+} // namespace
+
+bool ValidateCompressionCodecValue(const TStringBuf& codec) {
+ return CODECS.Match(codec.data());
+}
+
+void MediaValidator(const NYT::TNode& value) {
+ if (!value.IsMap()) {
+ throw yexception() << "Expected yson map, but got " << value.GetType();
+ }
+ for (auto& p: value.AsMap()) {
+ if (!p.second.IsMap()) {
+ throw yexception() << "Expected yson map, but got " << p.second.GetType() << "\" for the " << p.first.Quote() << " key";
+ }
+ for (auto& pp: p.second.AsMap()) {
+ if (pp.first == "replication_factor") {
+ if (!pp.second.IsInt64()) {
+ throw yexception() << "Expected int64, but got \"" << pp.second.GetType() << "\" for the " << p.first << "." << pp.first << " key";
+ }
+ } else if (pp.first == "data_parts_only") {
+ if (!pp.second.IsBool()) {
+ throw yexception() << "Expected bool, but got \"" << pp.second.GetType() << "\" for the " << p.first << "." << pp.first << " key";
+ }
+ } else {
+ throw yexception() << "Expected key in "<< p.first <<".[replication_factor, data_parts_only], but got " << p.first << "." << pp.first << " key";
+ }
+ }
+ }
+}
+
+TYtConfiguration::TYtConfiguration()
+{
+ const auto codecValidator = [] (const TString&, TString str) {
+ if (!ValidateCompressionCodecValue(str)) {
+ throw yexception() << "Bad codec value";
+ }
+ };
+
+ const auto mediaValidator = [] (const TString&, const NYT::TNode& value) {
+ MediaValidator(value);
+ };
+
+ REGISTER_SETTING(*this, Auth)
+ .ValueSetter([this](const TString&, const TString& value) {
+ Auth = value;
+ for (auto& x: Tokens) {
+ x.second = value;
+ }
+ });
+ REGISTER_SETTING(*this, ExternalTx);
+ REGISTER_SETTING(*this, TmpFolder)
+ .ValueSetter([this](const TString& cluster, const TString& value) {
+ Y_UNUSED(cluster);
+ TmpFolder = value;
+ if (value && !TablesTmpFolder.Get().GetOrElse({})) {
+ TablesTmpFolder = value;
+ }
+ });
+ REGISTER_SETTING(*this, TablesTmpFolder);
+ REGISTER_SETTING(*this, TempTablesTtl);
+ REGISTER_SETTING(*this, KeepTempTables)
+ .ValueSetter([this](const TString& cluster, bool value) {
+ Y_UNUSED(cluster);
+ if (value) {
+ ReleaseTempData = EReleaseTempDataMode::Never;
+ }
+ });
+ REGISTER_SETTING(*this, InflightTempTablesLimit);
+ REGISTER_SETTING(*this, ReleaseTempData).Parser([](const TString& v) { return FromString<EReleaseTempDataMode>(v); });
+ REGISTER_SETTING(*this, IgnoreYamrDsv);
+ REGISTER_SETTING(*this, IgnoreWeakSchema);
+ REGISTER_SETTING(*this, InferSchema)
+ .Lower(1)
+ .Upper(1000)
+ .Parser([](const TString& v) { return v.empty() ? 1 : Max(1u, FromString<ui32>(v)); })
+ .Validator([this](const TString&, ui32) {
+ if (ForceInferSchema.Get()) {
+ throw yexception() << "InferSchema cannot be used together with ForceInferSchema";
+ }
+ });
+
+ REGISTER_SETTING(*this, ForceInferSchema)
+ .Lower(1)
+ .Upper(1000)
+ .Parser([](const TString& v) { return v.empty() ? 1 : Max(1u, FromString<ui32>(v)); })
+ .Validator([this](const TString&, ui32) {
+ if (InferSchema.Get()) {
+ throw yexception() << "ForceInferSchema cannot be used together with InferSchema";
+ }
+ });
+
+ REGISTER_SETTING(*this, InferSchemaTableCountThreshold);
+
+ REGISTER_SETTING(*this, QueryCacheMode).Parser([](const TString& v) { return FromString<EQueryCacheMode>(v); });
+ REGISTER_SETTING(*this, QueryCacheIgnoreTableRevision);
+ REGISTER_SETTING(*this, QueryCacheSalt);
+ REGISTER_SETTING(*this, QueryCacheTtl);
+ REGISTER_SETTING(*this, QueryCacheUseForCalc);
+ REGISTER_SETTING(*this, QueryCacheUseExpirationTimeout);
+
+ REGISTER_SETTING(*this, DefaultMemoryLimit);
+ REGISTER_SETTING(*this, DefaultMemoryReserveFactor).Lower(0.0).Upper(1.0);
+ REGISTER_SETTING(*this, DefaultMemoryDigestLowerBound).Lower(0.0).Upper(1.0);
+ REGISTER_SETTING(*this, DefaultMaxJobFails);
+ REGISTER_SETTING(*this, MaxRowWeight).Lower(1).Upper(128_MB);
+ REGISTER_SETTING(*this, MaxKeyWeight).Lower(1).Upper(256_KB);
+ REGISTER_SETTING(*this, BufferRowCount).Lower(1);
+ REGISTER_SETTING(*this, DataSizePerJob);
+ REGISTER_SETTING(*this, DataSizePerSortJob).Lower(10_MB);
+ REGISTER_SETTING(*this, DataSizePerMapJob);
+ REGISTER_SETTING(*this, DataSizePerPartition);
+ REGISTER_SETTING(*this, DefaultLocalityTimeout);
+ REGISTER_SETTING(*this, MapLocalityTimeout);
+ REGISTER_SETTING(*this, ReduceLocalityTimeout);
+ REGISTER_SETTING(*this, SortLocalityTimeout);
+ REGISTER_SETTING(*this, MinLocalityInputDataWeight);
+ REGISTER_SETTING(*this, MaxJobCount).Lower(1);
+ REGISTER_SETTING(*this, UserSlots).Lower(1);
+ REGISTER_SETTING(*this, Pool).NonEmpty();
+ REGISTER_SETTING(*this, DefaultOperationWeight).Lower(0.0);
+ REGISTER_SETTING(*this, DefaultMapSelectivityFactor).Lower(0.0);
+ REGISTER_SETTING(*this, NightlyCompress);
+ REGISTER_SETTING(*this, PublishedCompressionCodec).Validator(codecValidator);
+ REGISTER_SETTING(*this, TemporaryCompressionCodec).Validator(codecValidator);
+ // See https://wiki.yandex-team.ru/yt/userdoc/chunkowners/#replikacija
+ REGISTER_SETTING(*this, PublishedErasureCodec).Parser([](const TString& v) { return FromString<NYT::EErasureCodecAttr>(v); });
+ REGISTER_SETTING(*this, TemporaryErasureCodec).Parser([](const TString& v) { return FromString<NYT::EErasureCodecAttr>(v); });
+ REGISTER_SETTING(*this, ClientMapTimeout);
+ REGISTER_SETTING(*this, CoreDumpPath).NonEmpty();
+ REGISTER_SETTING(*this, UseTmpfs);
+ REGISTER_SETTING(*this, SuspendIfAccountLimitExceeded);
+ REGISTER_SETTING(*this, ExtraTmpfsSize);
+ REGISTER_SETTING(*this, OptimizeFor).Parser([](const TString& v) { return FromString<NYT::EOptimizeForAttr>(v); });
+ REGISTER_SETTING(*this, DefaultCluster)
+ .Validator([this] (const TString&, TString value) {
+ if (!ValidClusters.contains(value)) {
+ throw yexception() << "Unknown cluster name: " << value;
+ }
+ });
+ REGISTER_SETTING(*this, UseTypeV2)
+ .ValueSetter([this](const TString& cluster, bool value) {
+ Y_UNUSED(cluster);
+ UseNativeYtTypes = value;
+ });
+ REGISTER_SETTING(*this, UseNativeYtTypes);
+ REGISTER_SETTING(*this, UseNativeDescSort);
+ REGISTER_SETTING(*this, UseIntermediateSchema);
+ REGISTER_SETTING(*this, StaticPool);
+ REGISTER_SETTING(*this, UseFlow)
+ .ValueSetter([this](const TString&, bool value) {
+ UseFlow = value;
+ if (value) {
+ UseSystemColumns = true;
+ } else {
+ MapJoinUseFlow = false;
+ }
+ });
+ REGISTER_SETTING(*this, WideFlowLimit)
+ .ValueSetter([this](const TString&, ui16 value) {
+ WideFlowLimit = value;
+ if (value > 0) {
+ UseSystemColumns = true;
+ }
+ });
+ REGISTER_SETTING(*this, ExpirationDeadline)
+ .Lower(Now())
+ .ValueSetter([this] (const TString& cluster, TInstant value) {
+ ExpirationDeadline[cluster] = value;
+ ExpirationInterval.Clear();
+ });
+ REGISTER_SETTING(*this, ExpirationInterval)
+ .ValueSetter([this] (const TString& cluster, TDuration value) {
+ ExpirationInterval[cluster] = value;
+ ExpirationDeadline.Clear();
+ });
+ REGISTER_SETTING(*this, ScriptCpu).Lower(1.0).GlobalOnly();
+ REGISTER_SETTING(*this, PythonCpu).Lower(1.0).GlobalOnly();
+ REGISTER_SETTING(*this, JavascriptCpu).Lower(1.0).GlobalOnly();
+ REGISTER_SETTING(*this, ErasureCodecCpu).Lower(1.0);
+ REGISTER_SETTING(*this, ErasureCodecCpuForDq).Lower(1.0);
+
+ REGISTER_SETTING(*this, Owners)
+ .NonEmpty()
+ .ValueSetterWithRestore([this] (const TString& cluster, TSet<TString> owners) {
+ if (ALL_CLUSTERS == cluster) {
+ Owners.UpdateAll([&owners] (const TString&, TSet<TString>& val) {
+ val.insert(owners.begin(), owners.end());
+ });
+ } else {
+ Owners[cluster].insert(owners.begin(), owners.end());
+ }
+ });
+ REGISTER_SETTING(*this, OperationReaders).NonEmpty();
+ REGISTER_SETTING(*this, SchedulingTag);
+ REGISTER_SETTING(*this, SchedulingTagFilter);
+ REGISTER_SETTING(*this, PoolTrees)
+ .NonEmpty()
+ .ValueSetter([this] (const TString& cluster, TSet<TString> trees) {
+ HybridDqExecution = false;
+ if (ALL_CLUSTERS == cluster) {
+ PoolTrees.UpdateAll([&trees] (const TString&, TSet<TString>& val) {
+ val.insert(trees.begin(), trees.end());
+ });
+ } else {
+ PoolTrees[cluster].insert(trees.begin(), trees.end());
+ }
+ });
+ REGISTER_SETTING(*this, TentativePoolTrees)
+ .NonEmpty()
+ .ValueSetter([this] (const TString& cluster, TSet<TString> trees) {
+ if (ALL_CLUSTERS == cluster) {
+ TentativePoolTrees.UpdateAll([&trees] (const TString&, TSet<TString>& val) {
+ val.insert(trees.begin(), trees.end());
+ });
+ } else {
+ TentativePoolTrees[cluster].insert(trees.begin(), trees.end());
+ }
+ });
+ REGISTER_SETTING(*this, TentativeTreeEligibilitySampleJobCount);
+ REGISTER_SETTING(*this, TentativeTreeEligibilityMaxJobDurationRatio);
+ REGISTER_SETTING(*this, TentativeTreeEligibilityMinJobDuration);
+ REGISTER_SETTING(*this, UseDefaultTentativePoolTrees);
+ REGISTER_SETTING(*this, IntermediateAccount).NonEmpty();
+ REGISTER_SETTING(*this, IntermediateReplicationFactor).Lower(1).Upper(10);
+ REGISTER_SETTING(*this, PublishedReplicationFactor).Lower(1).Upper(10);
+ REGISTER_SETTING(*this, TemporaryReplicationFactor).Lower(1).Upper(10);
+ REGISTER_SETTING(*this, AutoMerge).Enum({"relaxed", "economy", "disabled"})
+ .ValueSetter([this](const TString& cluster, const TString& value) {
+ PublishedAutoMerge[cluster] = value;
+ TemporaryAutoMerge[cluster] = value;
+ });
+
+ REGISTER_SETTING(*this, PublishedAutoMerge).Enum({ "relaxed", "economy", "disabled" });
+ REGISTER_SETTING(*this, TemporaryAutoMerge).Enum({ "relaxed", "economy", "disabled" });
+ REGISTER_SETTING(*this, UseSkiff)
+ .ValueSetter([this](const TString& cluster, bool value) {
+ UseSkiff[cluster] = value;
+ if (!value) {
+ UseNativeYtTypes = false;
+ }
+ });
+ REGISTER_SETTING(*this, TableContentCompressLevel).Upper(11);
+ REGISTER_SETTING(*this, TableContentDeliveryMode).Parser([](const TString& v) { return FromString<ETableContentDeliveryMode>(v); });
+ REGISTER_SETTING(*this, TableContentMaxChunksForNativeDelivery).Upper(1000);
+ REGISTER_SETTING(*this, TableContentTmpFolder);
+ REGISTER_SETTING(*this, TableContentColumnarStatistics);
+ REGISTER_SETTING(*this, TableContentUseSkiff);
+ REGISTER_SETTING(*this, DisableJobSplitting);
+ REGISTER_SETTING(*this, UseColumnarStatistics)
+ .Parser([](const TString& v) {
+ // backward compatible parse from bool
+ bool value = true;
+ if (!v || TryFromString<bool>(v, value)) {
+ return value ? EUseColumnarStatisticsMode::Force : EUseColumnarStatisticsMode::Disable;
+ } else {
+ return FromString<EUseColumnarStatisticsMode>(v);
+ }
+ })
+ ;
+ REGISTER_SETTING(*this, ParallelOperationsLimit).Lower(1);
+ REGISTER_SETTING(*this, DefaultCalcMemoryLimit);
+ REGISTER_SETTING(*this, LayerPaths).NonEmpty()
+ .ValueSetter([this](const TString& cluster, const TVector<TString>& value) {
+ LayerPaths[cluster] = value;
+ HybridDqExecution = false;
+ });
+ REGISTER_SETTING(*this, _EnableDq);
+ // Deprecated. Use MaxInputTables instead
+ REGISTER_SETTING(*this, ExtendTableLimit).Lower(1).Upper(3000)
+ .ValueSetter([this] (const TString& cluster, ui32 value) {
+ Y_UNUSED(cluster);
+ MaxInputTables = value;
+ });
+ REGISTER_SETTING(*this, CommonJoinCoreLimit);
+ REGISTER_SETTING(*this, CombineCoreLimit).Lower(1_MB); // Min 1Mb
+ REGISTER_SETTING(*this, SwitchLimit).Lower(1_MB); // Min 1Mb
+ REGISTER_SETTING(*this, JoinMergeTablesLimit);
+ REGISTER_SETTING(*this, JoinMergeUseSmallAsPrimary);
+ REGISTER_SETTING(*this, JoinMergeReduceJobMaxSize).Lower(1); // YT requires max_data_size_per_job to be > 0, YT default is 200GB
+ REGISTER_SETTING(*this, JoinMergeUnsortedFactor).Lower(0.0);
+ REGISTER_SETTING(*this, JoinMergeForce);
+ REGISTER_SETTING(*this, MapJoinLimit);
+ REGISTER_SETTING(*this, MapJoinShardMinRows);
+ REGISTER_SETTING(*this, MapJoinShardCount).Lower(1).Upper(10);
+ REGISTER_SETTING(*this, MapJoinUseFlow);
+ REGISTER_SETTING(*this, EvaluationTableSizeLimit).Upper(10_MB); // Max 10Mb
+ REGISTER_SETTING(*this, LookupJoinLimit).Upper(10_MB); // Same as EvaluationTableSizeLimit
+ REGISTER_SETTING(*this, LookupJoinMaxRows).Upper(1000);
+ REGISTER_SETTING(*this, DisableOptimizers);
+ REGISTER_SETTING(*this, MaxInputTables).Lower(1).Upper(3000); // 3000 - default max limit on YT clusters
+ REGISTER_SETTING(*this, MaxOutputTables).Lower(1).Upper(100); // https://ml.yandex-team.ru/thread/yt/166633186212752141/
+ REGISTER_SETTING(*this, MaxInputTablesForSortedMerge).Lower(2).Upper(1000); // https://st.yandex-team.ru/YTADMINREQ-16742
+ REGISTER_SETTING(*this, MaxExtraJobMemoryToFuseOperations);
+ REGISTER_SETTING(*this, MaxReplicationFactorToFuseOperations).Lower(1.0);
+ REGISTER_SETTING(*this, MaxOperationFiles).Lower(2).Upper(1000);
+ REGISTER_SETTING(*this, GeobaseDownloadUrl);
+ REGISTER_SETTING(*this, MinPublishedAvgChunkSize);
+ REGISTER_SETTING(*this, MinTempAvgChunkSize);
+ REGISTER_SETTING(*this, TopSortMaxLimit);
+ REGISTER_SETTING(*this, TopSortSizePerJob).Lower(1);
+ REGISTER_SETTING(*this, TopSortRowMultiplierPerJob).Lower(1);
+ REGISTER_SETTING(*this, JoinUseColumnarStatistics)
+ .ValueSetter([this](const TString& arg, bool value) {
+ Y_UNUSED(arg);
+ if (!value) {
+ JoinCollectColumnarStatistics = EJoinCollectColumnarStatisticsMode::Disable;
+ }
+ });
+ REGISTER_SETTING(*this, JoinCollectColumnarStatistics)
+ .Parser([](const TString& v) { return FromString<EJoinCollectColumnarStatisticsMode>(v); });
+ REGISTER_SETTING(*this, JoinColumnarStatisticsFetcherMode)
+ .Parser([](const TString& v) { return FromString<NYT::EColumnarStatisticsFetcherMode>(v); });
+ REGISTER_SETTING(*this, JoinWaitAllInputs);
+ REGISTER_SETTING(*this, JoinAllowColumnRenames);
+ REGISTER_SETTING(*this, JoinMergeSetTopLevelFullSort);
+ REGISTER_SETTING(*this, JoinEnableStarJoin);
+ REGISTER_SETTING(*this, JobEnv)
+ .Parser([](const TString& v) { return NYT::NodeFromYsonString(v, ::NYson::EYsonType::Node); })
+ .Validator([] (const TString&, const NYT::TNode& value) {
+ if (!value.IsMap()) {
+ throw yexception() << "Expected yson map, but got " << value.GetType();
+ }
+ for (auto& p: value.AsMap()) {
+ if (!p.second.IsString()) {
+ throw yexception() << "Expected string, but got \"" << p.second.GetType() << "\" for the " << p.first.Quote() << " key";
+ }
+ }
+ });
+ REGISTER_SETTING(*this, OperationSpec)
+ .Parser([](const TString& v) { return NYT::NodeFromYsonString(v, ::NYson::EYsonType::Node); })
+ .Validator([] (const TString&, const NYT::TNode& value) {
+ if (!value.IsMap()) {
+ throw yexception() << "Expected yson map, but got " << value.GetType();
+ }
+ })
+ .ValueSetter([this](const TString& cluster, const NYT::TNode& spec) {
+ OperationSpec[cluster] = spec;
+ HybridDqExecution = false;
+ });
+ REGISTER_SETTING(*this, Annotations)
+ .Parser([](const TString& v) { return NYT::NodeFromYsonString(v); })
+ .Validator([] (const TString&, const NYT::TNode& value) {
+ if (!value.IsMap()) {
+ throw yexception() << "Expected yson map, but got " << value.GetType();
+ }
+ });
+ REGISTER_SETTING(*this, MaxSpeculativeJobCountPerTask);
+ REGISTER_SETTING(*this, LLVMMemSize);
+ REGISTER_SETTING(*this, LLVMPerNodeMemSize);
+ REGISTER_SETTING(*this, SamplingIoBlockSize);
+ REGISTER_SETTING(*this, BinaryTmpFolder);
+ REGISTER_SETTING(*this, BinaryExpirationInterval);
+ REGISTER_SETTING(*this, FolderInlineDataLimit);
+ REGISTER_SETTING(*this, FolderInlineItemsLimit);
+ REGISTER_SETTING(*this, TableContentMinAvgChunkSize);
+ REGISTER_SETTING(*this, TableContentMaxInputTables);
+ REGISTER_SETTING(*this, UseSystemColumns)
+ .ValueSetter([this](const TString&, bool value) {
+ UseSystemColumns = value;
+ if (!value) {
+ UseFlow = false;
+ WideFlowLimit = 0;
+ }
+ });
+ REGISTER_SETTING(*this, PublishedMedia)
+ .Parser([](const TString& v) { return NYT::NodeFromYsonString(v, ::NYson::EYsonType::Node); })
+ .Validator(mediaValidator);
+ REGISTER_SETTING(*this, TemporaryMedia)
+ .Parser([](const TString& v) { return NYT::NodeFromYsonString(v, ::NYson::EYsonType::Node); })
+ .Validator(mediaValidator);
+ REGISTER_SETTING(*this, PublishedPrimaryMedium);
+ REGISTER_SETTING(*this, TemporaryPrimaryMedium);
+ REGISTER_SETTING(*this, IntermediateDataMedium);
+ REGISTER_SETTING(*this, PrimaryMedium).ValueSetter([this](const TString& cluster, const TString& value) {
+ PublishedPrimaryMedium[cluster] = value;
+ TemporaryPrimaryMedium[cluster] = value;
+ IntermediateDataMedium[cluster] = value;
+ });
+ REGISTER_SETTING(*this, QueryCacheChunkLimit);
+ REGISTER_SETTING(*this, IgnoreTypeV3);
+ REGISTER_SETTING(*this, HybridDqExecution);
+ REGISTER_SETTING(*this, HybridDqDataSizeLimitForOrdered);
+ REGISTER_SETTING(*this, HybridDqDataSizeLimitForUnordered);
+ REGISTER_SETTING(*this, HybridDqExecutionFallback);
+ REGISTER_SETTING(*this, NativeYtTypeCompatibility)
+ .Parser([](const TString& v) {
+ ui64 res = 0;
+ TVector<TString> vec;
+ StringSplitter(v).SplitBySet(",;| ").AddTo(&vec);
+ for (auto& s: vec) {
+ if (s.empty()) {
+ throw yexception() << "Empty value item";
+ }
+ res |= FromString<ENativeTypeCompatFlags>(s);
+ }
+ return res;
+ });
+ REGISTER_SETTING(*this, _UseKeyBoundApi);
+ REGISTER_SETTING(*this, UseYqlRowSpecCompactForm);
+ REGISTER_SETTING(*this, UseNewPredicateExtraction);
+ REGISTER_SETTING(*this, PruneKeyFilterLambda);
+ REGISTER_SETTING(*this, DqPruneKeyFilterLambda);
+ REGISTER_SETTING(*this, MergeAdjacentPointRanges);
+ REGISTER_SETTING(*this, KeyFilterForStartsWith);
+ REGISTER_SETTING(*this, MaxKeyRangeCount).Upper(1000);
+ REGISTER_SETTING(*this, MaxChunksForDqRead).Lower(1);
+ REGISTER_SETTING(*this, NetworkProject);
+ REGISTER_SETTING(*this, FileCacheTtl);
+ REGISTER_SETTING(*this, _ImpersonationUser);
+ REGISTER_SETTING(*this, JoinCommonUseMapMultiOut);
+ REGISTER_SETTING(*this, _EnableYtPartitioning);
+ REGISTER_SETTING(*this, UseAggPhases);
+ REGISTER_SETTING(*this, UsePartitionsByKeysForFinalAgg);
+ REGISTER_SETTING(*this, _ForceJobSizeAdjuster);
+ REGISTER_SETTING(*this, _EnableWriteReorder);
+ REGISTER_SETTING(*this, EnforceJobUtc);
+ REGISTER_SETTING(*this, UseRPCReaderInDQ);
+ REGISTER_SETTING(*this, DQRPCReaderInflight).Lower(1);
+ REGISTER_SETTING(*this, MaxCpuUsageToFuseMultiOuts).Lower(1.0);
+ REGISTER_SETTING(*this, MaxReplicationFactorToFuseMultiOuts).Lower(1.0);
+ REGISTER_SETTING(*this, ApplyStoredConstraints)
+ .Parser([](const TString& v) {
+ ui64 res = 0;
+ TVector<TString> vec;
+ StringSplitter(v).SplitBySet(",;| ").AddTo(&vec);
+ for (auto& s: vec) {
+ if (s.empty()) {
+ throw yexception() << "Empty value item";
+ }
+ res |= ui64(FromString<EStoredConstraint>(s));
+ }
+ return res;
+ });
+}
+
+EReleaseTempDataMode GetReleaseTempDataMode(const TYtSettings& settings) {
+ return settings.ReleaseTempData.Get().GetOrElse(EReleaseTempDataMode::Finish);
+}
+
+EJoinCollectColumnarStatisticsMode GetJoinCollectColumnarStatisticsMode(const TYtSettings& settings) {
+ return settings.JoinCollectColumnarStatistics.Get().GetOrElse(EJoinCollectColumnarStatisticsMode::Async);
+}
+
+TYtSettings::TConstPtr TYtConfiguration::Snapshot() const {
+ return std::make_shared<const TYtSettings>(*this);
+}
+
+size_t TYtVersionedConfiguration::FindNodeVer(const TExprNode& node) {
+ auto it = NodeIdToVer.find(node.UniqueId());
+ if (it != NodeIdToVer.end()) {
+ return it->second;
+ }
+
+ size_t ver = 0;
+ for (auto& child: node.Children()) {
+ ver = Max<size_t>(ver, FindNodeVer(*child));
+ }
+ NodeIdToVer.emplace(node.UniqueId(), ver);
+ return ver;
+}
+
+void TYtVersionedConfiguration::FreezeZeroVersion() {
+ if (Y_UNLIKELY(FrozenSettings.empty())) {
+ FrozenSettings.push_back(Snapshot());
+ }
+}
+
+void TYtVersionedConfiguration::PromoteVersion(const TExprNode& node) {
+ NodeIdToVer[node.UniqueId()] = FrozenSettings.size();
+ FrozenSettings.push_back(Snapshot());
+}
+
+TYtSettings::TConstPtr TYtVersionedConfiguration::GetSettingsForNode(const TExprNode& node) {
+ FreezeZeroVersion();
+ size_t ver = FindNodeVer(node);
+ YQL_CLOG(DEBUG, ProviderYt) << "Using settings ver." << ver;
+ return FrozenSettings.at(ver);
+}
+
+TYtSettings::TConstPtr TYtVersionedConfiguration::GetSettingsVer(size_t ver) {
+ FreezeZeroVersion();
+ YQL_CLOG(DEBUG, ProviderYt) << "Using settings ver." << ver;
+ return FrozenSettings.at(ver);
+}
+
+void TYtVersionedConfiguration::ClearVersions() {
+ FrozenSettings.clear();
+ NodeIdToVer.clear();
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/common/yql_yt_settings.h b/ydb/library/yql/providers/yt/common/yql_yt_settings.h
new file mode 100644
index 0000000000..1aeef45770
--- /dev/null
+++ b/ydb/library/yql/providers/yt/common/yql_yt_settings.h
@@ -0,0 +1,333 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/config/yql_dispatch.h>
+#include <ydb/library/yql/providers/common/config/yql_setting.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <library/cpp/string_utils/parse_size/parse_size.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/client_method_options.h>
+#include <library/cpp/yson/node/node.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/guid.h>
+#include <util/generic/string.h>
+#include <util/generic/ptr.h>
+#include <util/generic/set.h>
+#include <util/generic/vector.h>
+
+#include <unordered_map>
+
+namespace NYql {
+
+enum class EQueryCacheMode {
+ Disable /* "disable" */,
+ Readonly /* "readonly" */,
+ Refresh /* "refresh" */,
+ Normal /* "normal" */,
+};
+
+enum class EReleaseTempDataMode {
+ Never /* "never" */,
+ Immediate /* "immediate" */,
+ Finish /* "finish" */,
+};
+
+enum class ETableContentDeliveryMode {
+ Native /* "native" */,
+ File /* "file" */,
+};
+
+enum class EJoinCollectColumnarStatisticsMode {
+ Disable /* "disable" */,
+ Sync /* "sync" */,
+ Async /* "async" */,
+};
+
+enum class EUseColumnarStatisticsMode {
+ Disable /* "disable" */,
+ Auto /* "auto" */,
+ Force /* "force" */,
+};
+
+enum class EStoredConstraint : ui64 {
+ None = 0ULL /* "None" */,
+ Sorted = 1ULL /* "Sorted" */,
+ Unique = 2ULL /* "Unique" */,
+ Distinct = 4ULL /* "Distinct" */,
+};
+
+struct TYtSettings {
+ using TConstPtr = std::shared_ptr<const TYtSettings>;
+
+ // should be static, because are used on earlier stages
+ NCommon::TConfSetting<TString, false> Auth;
+ NCommon::TConfSetting<TGUID, false> ExternalTx;
+ NCommon::TConfSetting<TString, false> TmpFolder;
+ NCommon::TConfSetting<TString, false> TablesTmpFolder;
+ NCommon::TConfSetting<TDuration, false> TempTablesTtl;
+ NCommon::TConfSetting<bool, false> KeepTempTables;
+ NCommon::TConfSetting<ui32, false> InflightTempTablesLimit;
+ NCommon::TConfSetting<EReleaseTempDataMode, false> ReleaseTempData;
+ NCommon::TConfSetting<bool, false> IgnoreYamrDsv;
+ NCommon::TConfSetting<bool, false> IgnoreWeakSchema;
+ NCommon::TConfSetting<ui32, false> InferSchema;
+ NCommon::TConfSetting<ui32, false> ForceInferSchema;
+ NCommon::TConfSetting<ui32, false> InferSchemaTableCountThreshold;
+ NCommon::TConfSetting<NSize::TSize, false> DefaultCalcMemoryLimit;
+ NCommon::TConfSetting<ui32, false> ParallelOperationsLimit;
+ NCommon::TConfSetting<EQueryCacheMode, false> QueryCacheMode;
+ NCommon::TConfSetting<bool, false> QueryCacheIgnoreTableRevision;
+ NCommon::TConfSetting<TString, false> QueryCacheSalt;
+ NCommon::TConfSetting<TDuration, false> QueryCacheTtl;
+ NCommon::TConfSetting<bool, false> QueryCacheUseExpirationTimeout;
+ NCommon::TConfSetting<bool, false> QueryCacheUseForCalc;
+ NCommon::TConfSetting<ui32, false> DefaultMaxJobFails;
+ NCommon::TConfSetting<TString, false> CoreDumpPath;
+ NCommon::TConfSetting<TString, false> DefaultCluster;
+ NCommon::TConfSetting<TString, false> StaticPool;
+ NCommon::TConfSetting<TString, false> BinaryTmpFolder;
+ NCommon::TConfSetting<TDuration, false> BinaryExpirationInterval;
+ NCommon::TConfSetting<bool, false> IgnoreTypeV3;
+ NCommon::TConfSetting<bool, false> _UseMultisetAttributes;
+ NCommon::TConfSetting<TDuration, false> FileCacheTtl;
+ NCommon::TConfSetting<TString, false> _ImpersonationUser;
+
+ // Job runtime
+ NCommon::TConfSetting<TString, true> Pool;
+ NCommon::TConfSetting<NSize::TSize, true> DefaultMemoryLimit;
+ NCommon::TConfSetting<double, true> DefaultMemoryReserveFactor;
+ NCommon::TConfSetting<double, true> DefaultMemoryDigestLowerBound;
+ NCommon::TConfSetting<NSize::TSize, true> MaxRowWeight;
+ NCommon::TConfSetting<NSize::TSize, true> MaxKeyWeight;
+ NCommon::TConfSetting<ui32, true> BufferRowCount;
+ NCommon::TConfSetting<NSize::TSize, true> DataSizePerJob;
+ NCommon::TConfSetting<NSize::TSize, true> DataSizePerSortJob;
+ NCommon::TConfSetting<NSize::TSize, true> DataSizePerMapJob;
+ NCommon::TConfSetting<NSize::TSize, true> DataSizePerPartition;
+ NCommon::TConfSetting<TDuration, true> DefaultLocalityTimeout;
+ NCommon::TConfSetting<TDuration, true> MapLocalityTimeout;
+ NCommon::TConfSetting<TDuration, true> ReduceLocalityTimeout;
+ NCommon::TConfSetting<TDuration, true> SortLocalityTimeout;
+ NCommon::TConfSetting<NSize::TSize, true> MinLocalityInputDataWeight;
+ NCommon::TConfSetting<ui64, true> MaxJobCount;
+ NCommon::TConfSetting<ui64, true> UserSlots;
+ NCommon::TConfSetting<double, true> DefaultOperationWeight;
+ NCommon::TConfSetting<double, true> DefaultMapSelectivityFactor;
+ NCommon::TConfSetting<bool, true> NightlyCompress;
+ NCommon::TConfSetting<TString, true> PublishedCompressionCodec;
+ NCommon::TConfSetting<TString, true> TemporaryCompressionCodec;
+ NCommon::TConfSetting<NYT::EErasureCodecAttr, true> PublishedErasureCodec;
+ NCommon::TConfSetting<NYT::EErasureCodecAttr, true> TemporaryErasureCodec;
+ NCommon::TConfSetting<TDuration, true> ClientMapTimeout; // TODO: yt_native
+ NCommon::TConfSetting<bool, true> UseTmpfs;
+ NCommon::TConfSetting<bool, true> SuspendIfAccountLimitExceeded;
+ NCommon::TConfSetting<NSize::TSize, true> ExtraTmpfsSize;
+ NCommon::TConfSetting<NYT::EOptimizeForAttr, true> OptimizeFor; // {scan, lookup}
+ NCommon::TConfSetting<TInstant, true> ExpirationDeadline;
+ NCommon::TConfSetting<TDuration, true> ExpirationInterval;
+ NCommon::TConfSetting<double, true> ScriptCpu;
+ NCommon::TConfSetting<double, true> PythonCpu;
+ NCommon::TConfSetting<double, true> JavascriptCpu;
+ NCommon::TConfSetting<double, true> ErasureCodecCpu;
+ NCommon::TConfSetting<double, true> ErasureCodecCpuForDq;
+ NCommon::TConfSetting<TSet<TString>, true> Owners;
+ NCommon::TConfSetting<TSet<TString>, true> OperationReaders;
+ NCommon::TConfSetting<TString, true> SchedulingTag;
+ NCommon::TConfSetting<TString, true> SchedulingTagFilter;
+ NCommon::TConfSetting<TSet<TString>, true> PoolTrees;
+ NCommon::TConfSetting<TSet<TString>, true> TentativePoolTrees;
+ NCommon::TConfSetting<ui32, true> TentativeTreeEligibilitySampleJobCount;
+ NCommon::TConfSetting<double, true> TentativeTreeEligibilityMaxJobDurationRatio;
+ NCommon::TConfSetting<ui32, true> TentativeTreeEligibilityMinJobDuration;
+ NCommon::TConfSetting<bool, true> UseDefaultTentativePoolTrees;
+ NCommon::TConfSetting<TString, true> IntermediateAccount;
+ NCommon::TConfSetting<ui32, true> IntermediateReplicationFactor;
+ NCommon::TConfSetting<ui32, true> PublishedReplicationFactor;
+ NCommon::TConfSetting<ui32, true> TemporaryReplicationFactor;
+ NCommon::TConfSetting<TString, true> AutoMerge; // {relaxed, economy, disabled}
+ NCommon::TConfSetting<TString, true> PublishedAutoMerge;
+ NCommon::TConfSetting<TString, true> TemporaryAutoMerge;
+ NCommon::TConfSetting<TVector<TString>, true> LayerPaths;
+ NCommon::TConfSetting<NYT::TNode, true> JobEnv;
+ NCommon::TConfSetting<NYT::TNode, true> OperationSpec;
+ NCommon::TConfSetting<NYT::TNode, true> Annotations;
+ NCommon::TConfSetting<bool, true> UseSkiff;
+ NCommon::TConfSetting<ui32, true> TableContentCompressLevel;
+ NCommon::TConfSetting<bool, true> DisableJobSplitting;
+ NCommon::TConfSetting<EUseColumnarStatisticsMode, true> UseColumnarStatistics;
+ NCommon::TConfSetting<ETableContentDeliveryMode, true> TableContentDeliveryMode;
+ NCommon::TConfSetting<bool, true> TableContentUseSkiff;
+ NCommon::TConfSetting<TString, true> TableContentTmpFolder;
+ NCommon::TConfSetting<bool, true> TableContentColumnarStatistics;
+ NCommon::TConfSetting<TString, true> GeobaseDownloadUrl;
+ NCommon::TConfSetting<ui32, true> MaxSpeculativeJobCountPerTask;
+ NCommon::TConfSetting<NSize::TSize, true> LLVMMemSize;
+ NCommon::TConfSetting<NSize::TSize, true> LLVMPerNodeMemSize;
+ NCommon::TConfSetting<NSize::TSize, true> SamplingIoBlockSize;
+ NCommon::TConfSetting<NYT::TNode, true> PublishedMedia;
+ NCommon::TConfSetting<NYT::TNode, true> TemporaryMedia;
+ NCommon::TConfSetting<TString, true> PublishedPrimaryMedium;
+ NCommon::TConfSetting<TString, true> TemporaryPrimaryMedium;
+ NCommon::TConfSetting<TString, true> IntermediateDataMedium;
+ NCommon::TConfSetting<TString, true> PrimaryMedium;
+ NCommon::TConfSetting<ui64, true> QueryCacheChunkLimit;
+ NCommon::TConfSetting<ui64, true> NativeYtTypeCompatibility;
+ NCommon::TConfSetting<bool, true> _UseKeyBoundApi;
+ NCommon::TConfSetting<TString, true> NetworkProject;
+ NCommon::TConfSetting<bool, true> _EnableYtPartitioning;
+ NCommon::TConfSetting<bool, true> _ForceJobSizeAdjuster;
+ NCommon::TConfSetting<bool, true> EnforceJobUtc;
+ NCommon::TConfSetting<bool, true> UseRPCReaderInDQ;
+ NCommon::TConfSetting<size_t, true> DQRPCReaderInflight;
+
+ // Optimizers
+ NCommon::TConfSetting<bool, true> _EnableDq;
+ NCommon::TConfSetting<ui32, false> ExtendTableLimit; // Deprecated. Use MaxInputTables instead
+ NCommon::TConfSetting<NSize::TSize, false> CommonJoinCoreLimit;
+ NCommon::TConfSetting<NSize::TSize, false> CombineCoreLimit;
+ NCommon::TConfSetting<NSize::TSize, false> SwitchLimit;
+ NCommon::TConfSetting<ui64, false> JoinMergeTablesLimit;
+ NCommon::TConfSetting<bool, false> JoinMergeUseSmallAsPrimary;
+ NCommon::TConfSetting<NSize::TSize, false> JoinMergeReduceJobMaxSize;
+ NCommon::TConfSetting<double, false> JoinMergeUnsortedFactor; // (>=0.0)
+ NCommon::TConfSetting<bool, false> JoinMergeForce;
+ NCommon::TConfSetting<NSize::TSize, false> MapJoinLimit;
+ NCommon::TConfSetting<ui64, false> MapJoinShardMinRows;
+ NCommon::TConfSetting<ui64, false> MapJoinShardCount; // [1-10]
+ NCommon::TConfSetting<bool, false> MapJoinUseFlow;
+ NCommon::TConfSetting<NSize::TSize, false> LookupJoinLimit;
+ NCommon::TConfSetting<ui64, false> LookupJoinMaxRows;
+ NCommon::TConfSetting<NSize::TSize, false> EvaluationTableSizeLimit;
+ NCommon::TConfSetting<TSet<TString>, false> DisableOptimizers;
+ NCommon::TConfSetting<ui32, false> MaxInputTables;
+ NCommon::TConfSetting<ui32, false> MaxInputTablesForSortedMerge;
+ NCommon::TConfSetting<ui32, false> MaxOutputTables;
+ NCommon::TConfSetting<NSize::TSize, false> MaxExtraJobMemoryToFuseOperations;
+ NCommon::TConfSetting<double, false> MaxReplicationFactorToFuseOperations;
+ NCommon::TConfSetting<ui32, false> MaxOperationFiles;
+ NCommon::TConfSetting<NSize::TSize, false> MinPublishedAvgChunkSize;
+ NCommon::TConfSetting<NSize::TSize, false> MinTempAvgChunkSize;
+ NCommon::TConfSetting<ui32, false> TopSortMaxLimit;
+ NCommon::TConfSetting<NSize::TSize, false> TopSortSizePerJob;
+ NCommon::TConfSetting<ui32, false> TopSortRowMultiplierPerJob;
+ NCommon::TConfSetting<bool, false> JoinUseColumnarStatistics; // Deprecated. Use JoinCollectColumnarStatistics instead
+ NCommon::TConfSetting<EJoinCollectColumnarStatisticsMode, false> JoinCollectColumnarStatistics;
+ NCommon::TConfSetting<NYT::EColumnarStatisticsFetcherMode, false> JoinColumnarStatisticsFetcherMode;
+ NCommon::TConfSetting<bool, false> JoinWaitAllInputs;
+ NCommon::TConfSetting<bool, false> JoinAllowColumnRenames;
+ NCommon::TConfSetting<bool, false> JoinMergeSetTopLevelFullSort;
+ NCommon::TConfSetting<bool, false> JoinEnableStarJoin;
+ NCommon::TConfSetting<NSize::TSize, false> FolderInlineDataLimit;
+ NCommon::TConfSetting<ui32, false> FolderInlineItemsLimit;
+ NCommon::TConfSetting<NSize::TSize, false> TableContentMinAvgChunkSize;
+ NCommon::TConfSetting<ui32, false> TableContentMaxInputTables;
+ NCommon::TConfSetting<ui32, false> TableContentMaxChunksForNativeDelivery;
+ NCommon::TConfSetting<bool, false> UseTypeV2;
+ NCommon::TConfSetting<bool, false> UseNativeYtTypes;
+ NCommon::TConfSetting<bool, false> UseNativeDescSort;
+ NCommon::TConfSetting<bool, false> UseIntermediateSchema;
+ NCommon::TConfSetting<bool, false> UseFlow;
+ NCommon::TConfSetting<ui16, false> WideFlowLimit;
+ NCommon::TConfSetting<bool, false> UseSystemColumns;
+ NCommon::TConfSetting<bool, false> HybridDqExecution;
+ NCommon::TConfSetting<NSize::TSize, false> HybridDqDataSizeLimitForOrdered;
+ NCommon::TConfSetting<NSize::TSize, false> HybridDqDataSizeLimitForUnordered;
+ NCommon::TConfSetting<bool, false> HybridDqExecutionFallback;
+ NCommon::TConfSetting<bool, false> UseYqlRowSpecCompactForm;
+ NCommon::TConfSetting<bool, false> UseNewPredicateExtraction;
+ NCommon::TConfSetting<bool, false> PruneKeyFilterLambda;
+ NCommon::TConfSetting<bool, false> DqPruneKeyFilterLambda;
+ NCommon::TConfSetting<bool, false> MergeAdjacentPointRanges;
+ NCommon::TConfSetting<bool, false> KeyFilterForStartsWith;
+ NCommon::TConfSetting<ui64, false> MaxKeyRangeCount;
+ NCommon::TConfSetting<ui64, false> MaxChunksForDqRead;
+ NCommon::TConfSetting<bool, false> JoinCommonUseMapMultiOut;
+ NCommon::TConfSetting<bool, false> UseAggPhases;
+ NCommon::TConfSetting<bool, false> UsePartitionsByKeysForFinalAgg;
+ NCommon::TConfSetting<bool, false> _EnableWriteReorder;
+ NCommon::TConfSetting<double, false> MaxCpuUsageToFuseMultiOuts;
+ NCommon::TConfSetting<double, false> MaxReplicationFactorToFuseMultiOuts;
+ NCommon::TConfSetting<ui64, false> ApplyStoredConstraints;
+};
+
+EReleaseTempDataMode GetReleaseTempDataMode(const TYtSettings& settings);
+EJoinCollectColumnarStatisticsMode GetJoinCollectColumnarStatisticsMode(const TYtSettings& settings);
+
+struct TYtConfiguration : public TYtSettings, public NCommon::TSettingDispatcher {
+ using TPtr = TIntrusivePtr<TYtConfiguration>;
+
+ TYtConfiguration();
+ TYtConfiguration(const TYtConfiguration&) = delete;
+
+ template <class TProtoConfig, typename TFilter>
+ void Init(const TProtoConfig& config, const TFilter& filter, TTypeAnnotationContext& typeCtx) {
+ TVector<TString> clusters(Reserve(config.ClusterMappingSize()));
+ for (auto& cluster: config.GetClusterMapping()) {
+ clusters.push_back(cluster.GetName());
+ Tokens[cluster.GetName()] = typeCtx.Credentials->FindCredentialContent("cluster:default_" + cluster.GetName(), "default_yt", cluster.GetYTToken());
+ }
+
+ auto impersonationUser = typeCtx.Credentials->FindCredential("impersonation_user_yt");
+ if (impersonationUser) {
+ _ImpersonationUser = impersonationUser->Content;
+ }
+
+ this->SetValidClusters(clusters);
+
+ // Init settings from config
+ this->Dispatch(config.GetDefaultSettings(), filter);
+ for (auto& cluster: config.GetClusterMapping()) {
+ this->Dispatch(cluster.GetName(), cluster.GetSettings(), filter);
+ }
+ this->FreezeDefaults();
+ }
+
+ TYtSettings::TConstPtr Snapshot() const;
+
+ THashMap<TString, TString> Tokens;
+};
+
+class TYtVersionedConfiguration: public TYtConfiguration {
+public:
+ using TPtr = TIntrusivePtr<TYtVersionedConfiguration>;
+
+ struct TState {
+ TVector<TYtSettings::TConstPtr> FrozenSettings;
+ std::unordered_map<ui64, size_t> NodeIdToVer;
+ TYtSettings::TConstPtr Snapshot;
+ };
+
+ TYtVersionedConfiguration() = default;
+ ~TYtVersionedConfiguration() = default;
+
+ size_t FindNodeVer(const TExprNode& node);
+ void FreezeZeroVersion();
+ void PromoteVersion(const TExprNode& node);
+ size_t GetLastVersion() const {
+ return FrozenSettings.empty() ? 0 : FrozenSettings.size() - 1;
+ }
+ TYtSettings::TConstPtr GetSettingsForNode(const TExprNode& node);
+ TYtSettings::TConstPtr GetSettingsVer(size_t ver);
+ void ClearVersions();
+
+ TState GetState() const {
+ return TState{FrozenSettings, NodeIdToVer, Snapshot()};
+ }
+
+ void RestoreState(TState&& state) {
+ FrozenSettings = std::move(state.FrozenSettings);
+ NodeIdToVer = std::move(state.NodeIdToVer);
+ *((TYtSettings*)this) = *state.Snapshot;
+ }
+
+private:
+ TVector<TYtSettings::TConstPtr> FrozenSettings;
+ std::unordered_map<ui64, size_t> NodeIdToVer;
+};
+
+bool ValidateCompressionCodecValue(const TStringBuf& codec);
+void MediaValidator(const NYT::TNode& value);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.cpp b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.cpp
new file mode 100644
index 0000000000..c02a693f69
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.cpp
@@ -0,0 +1,24 @@
+#include "dq_yt_factory.h"
+#include "dq_yt_reader.h"
+#include "dq_yt_writer.h"
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+TComputationNodeFactory GetDqYtFactory(NKikimr::NMiniKQL::IStatsRegistry* jobStats) {
+ return [=] (TCallable& callable, const TComputationNodeFactoryContext& ctx) -> IComputationNode* {
+ TStringBuf name = callable.GetType()->GetName();
+ if (name == "DqYtRead") {
+ return NDqs::WrapDqYtRead(callable, jobStats, ctx);
+ }
+
+ if (name == "YtDqRowsWideWrite") {
+ return NDqs::WrapYtDqRowsWideWrite(callable, ctx);
+ }
+
+ return nullptr;
+ };
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.h b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.h
new file mode 100644
index 0000000000..7f7a95da80
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_factory.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetDqYtFactory(NKikimr::NMiniKQL::IStatsRegistry* jobStats = nullptr);
+
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.cpp b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.cpp
new file mode 100644
index 0000000000..bb5e3d72cb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.cpp
@@ -0,0 +1,119 @@
+#include "dq_yt_reader.h"
+
+#include "dq_yt_reader_impl.h"
+
+#include "dq_yt_rpc_reader.h"
+
+namespace NYql::NDqs {
+
+using namespace NKikimr::NMiniKQL;
+
+class TDqYtReadWrapperHttp : public TDqYtReadWrapperBase<TDqYtReadWrapperHttp, TFileInputState> {
+public:
+using TInputType = NYT::TRawTableReaderPtr;
+ TDqYtReadWrapperHttp(const TComputationNodeFactoryContext& ctx, const TString& clusterName,
+ const TString& token, const NYT::TNode& inputSpec, const NYT::TNode& samplingSpec,
+ const TVector<ui32>& inputGroups,
+ TType* itemType, const TVector<TString>& tableNames, TVector<std::pair<NYT::TRichYPath, NYT::TFormat>>&& tables, NKikimr::NMiniKQL::IStatsRegistry* jobStats, size_t inflight)
+ : TDqYtReadWrapperBase<TDqYtReadWrapperHttp, TFileInputState>(ctx, clusterName, token, inputSpec, samplingSpec, inputGroups, itemType, tableNames, std::move(tables), jobStats, inflight) {}
+
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ TVector<NYT::TRawTableReaderPtr> rawReaders;
+
+ NYT::TCreateClientOptions createOpts;
+ if (Token) {
+ createOpts.Token(Token);
+ }
+
+ auto client = NYT::CreateClient(ClusterName, createOpts);
+ NYT::TTableReaderOptions readerOptions;
+ if (!SamplingSpec.IsUndefined()) {
+ readerOptions.Config(SamplingSpec);
+ }
+
+ for (auto [richYPath, format]: Tables) {
+ NYT::TRawTableReaderPtr reader;
+ const int lastAttempt = NYT::TConfig::Get()->ReadRetryCount - 1;
+ for (int attempt = 0; attempt <= lastAttempt; ++attempt) {
+ try {
+ if (richYPath.TransactionId_) {
+ auto transaction = client->AttachTransaction(richYPath.TransactionId_.GetRef());
+ richYPath.TransactionId_.Clear();
+ reader = transaction->CreateRawReader(richYPath, format, readerOptions.CreateTransaction(false));
+ } else {
+ reader = client->CreateRawReader(richYPath, format, readerOptions.CreateTransaction(true));
+ }
+ break;
+ } catch (const NYT::TErrorResponse& e) {
+ Cerr << "Error creating reader for " << richYPath.Path_ << ": " << e.what();
+ // Already retried inside CreateRawReader
+ throw;
+ } catch (const yexception& e) {
+ Cerr << "Error creating reader for " << richYPath.Path_ << ": " << e.what();
+ if (attempt == lastAttempt) {
+ throw;
+ }
+ NYT::NDetail::TWaitProxy::Get()->Sleep(NYT::TConfig::Get()->RetryInterval);
+ }
+ }
+ rawReaders.push_back(reader);
+ }
+
+ state = ctx.HolderFactory.Create<TDqYtReadWrapperBase<TDqYtReadWrapperHttp, TFileInputState>::TState>(Specs, ctx.HolderFactory, std::move(rawReaders), 4, 4_MB);
+ }
+};
+
+IComputationNode* WrapDqYtRead(TCallable& callable, NKikimr::NMiniKQL::IStatsRegistry* jobStats, const TComputationNodeFactoryContext& ctx) {
+ MKQL_ENSURE(callable.GetInputsCount() == 7 || callable.GetInputsCount() == 8, "Expected 7 or 8 arguments.");
+
+ TString clusterName(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ TString tokenName(AS_VALUE(TDataLiteral, callable.GetInput(1))->AsValue().AsStringRef());
+ TString inputSpec(AS_VALUE(TDataLiteral, callable.GetInput(2))->AsValue().AsStringRef());
+ TString samplingSpec(AS_VALUE(TDataLiteral, callable.GetInput(3))->AsValue().AsStringRef());
+ TString token;
+ if (auto sec = ctx.SecureParamsProvider) {
+ NUdf::TStringRef val;
+ if (sec->GetSecureParam(tokenName, val)) {
+ token = val;
+ }
+ }
+
+ TVector<ui32> inputGroups;
+ TVector<TString> tableNames;
+ TVector<std::pair<NYT::TRichYPath, NYT::TFormat>> tables; // richpath, skiff
+ TListLiteral* groupList = AS_VALUE(TListLiteral, callable.GetInput(4));
+ for (ui32 grp = 0; grp < groupList->GetItemsCount(); ++grp) {
+ TListLiteral* tableList = AS_VALUE(TListLiteral, groupList->GetItems()[grp]);
+ for (ui32 i = 0; i < tableList->GetItemsCount(); ++i) {
+ TTupleLiteral* tableTuple = AS_VALUE(TTupleLiteral, tableList->GetItems()[i]);
+ YQL_ENSURE(tableTuple->GetValuesCount() == 3);
+ tableNames.emplace_back(AS_VALUE(TDataLiteral, tableTuple->GetValue(0))->AsValue().AsStringRef());
+ inputGroups.push_back(grp);
+
+ NYT::TRichYPath richYPath;
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, tableTuple->GetValue(1))->AsValue().AsStringRef())));
+ tables.emplace_back(richYPath, NYT::TFormat(NYT::NodeFromYsonString(AS_VALUE(TDataLiteral, tableTuple->GetValue(2))->AsValue().AsStringRef())));
+ }
+ }
+ if (1 == groupList->GetItemsCount()) {
+ inputGroups.clear();
+ }
+#ifdef __linux__
+ size_t inflight(AS_VALUE(TDataLiteral, callable.GetInput(6))->AsValue().Get<size_t>());
+ if (inflight) {
+ return new TDqYtReadWrapperBase<TDqYtReadWrapperRPC, TParallelFileInputState>(ctx, clusterName, token,
+ NYT::NodeFromYsonString(inputSpec), samplingSpec ? NYT::NodeFromYsonString(samplingSpec) : NYT::TNode(),
+ inputGroups, static_cast<TType*>(callable.GetInput(5).GetNode()), tableNames, std::move(tables), jobStats, inflight);
+ } else {
+ return new TDqYtReadWrapperBase<TDqYtReadWrapperHttp, TFileInputState>(ctx, clusterName, token,
+ NYT::NodeFromYsonString(inputSpec), samplingSpec ? NYT::NodeFromYsonString(samplingSpec) : NYT::TNode(),
+ inputGroups, static_cast<TType*>(callable.GetInput(5).GetNode()), tableNames, std::move(tables), jobStats, inflight);
+ }
+#else
+ return new TDqYtReadWrapperBase<TDqYtReadWrapperHttp, TFileInputState>(ctx, clusterName, token,
+ NYT::NodeFromYsonString(inputSpec), samplingSpec ? NYT::NodeFromYsonString(samplingSpec) : NYT::TNode(),
+ inputGroups, static_cast<TType*>(callable.GetInput(5).GetNode()), tableNames, std::move(tables), jobStats, 0);
+#endif
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.h b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.h
new file mode 100644
index 0000000000..2dd5c2deef
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+namespace NYql::NDqs {
+
+NKikimr::NMiniKQL::IComputationNode* WrapDqYtRead(NKikimr::NMiniKQL::TCallable& callable, NKikimr::NMiniKQL::IStatsRegistry* jobStats, const NKikimr::NMiniKQL::TComputationNodeFactoryContext& ctx);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader_impl.h b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader_impl.h
new file mode 100644
index 0000000000..d7c7c1954f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_reader_impl.h
@@ -0,0 +1,202 @@
+#pragma once
+
+#include "dq_yt_reader.h"
+
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_codegen.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/size_literals.h>
+#include <util/stream/output.h>
+
+namespace NYql::NDqs {
+
+using namespace NKikimr::NMiniKQL;
+
+template<typename T, typename IS>
+class TDqYtReadWrapperBase : public TStatefulWideFlowCodegeneratorNode<TDqYtReadWrapperBase<T, IS>> {
+using TBaseComputation = TStatefulWideFlowCodegeneratorNode<TDqYtReadWrapperBase<T, IS>>;
+public:
+ TDqYtReadWrapperBase(const TComputationNodeFactoryContext& ctx, const TString& clusterName,
+ const TString& token, const NYT::TNode& inputSpec, const NYT::TNode& samplingSpec,
+ const TVector<ui32>& inputGroups,
+ TType* itemType, const TVector<TString>& tableNames, TVector<std::pair<NYT::TRichYPath, NYT::TFormat>>&& tables, NKikimr::NMiniKQL::IStatsRegistry* jobStats, size_t inflight) : TBaseComputation(ctx.Mutables, this, EValueRepresentation::Boxed, EValueRepresentation::Boxed)
+ , Width(AS_TYPE(TStructType, itemType)->GetMembersCount())
+ , CodecCtx(ctx.Env, ctx.FunctionRegistry, &ctx.HolderFactory)
+ , ClusterName(clusterName)
+ , Token(token)
+ , SamplingSpec(samplingSpec)
+ , Tables(std::move(tables))
+ , Inflight(inflight)
+ {
+ Specs.SetUseSkiff("", TMkqlIOSpecs::ESystemField::RowIndex | TMkqlIOSpecs::ESystemField::RangeIndex);
+ Specs.Init(CodecCtx, inputSpec, inputGroups, tableNames, itemType, {}, {}, jobStats);
+ }
+
+ class TState: public TComputationValue<TState>, public IS {
+ public:
+
+ template<typename... Args>
+ TState(TMemoryUsageInfo* memInfo, Args&&... args)
+ : TComputationValue<TState>(memInfo)
+ , IS(std::forward<Args>(args)...)
+ {
+ IS::SetNextBlockCallback([this]() { Yield_ = true; });
+ }
+
+ virtual ~TState() = default;
+
+ NUdf::TUnboxedValuePod FetchRecord() {
+ if (!AtStart_) {
+ IS::Next();
+ }
+ AtStart_ = false;
+
+ if (!IS::IsValid()) {
+ IS::Finish();
+ return NUdf::TUnboxedValuePod::MakeFinish();
+ }
+
+ if (Yield_) {
+ Yield_ = false;
+ AtStart_ = true;
+ return NUdf::TUnboxedValuePod::MakeYield();
+ }
+
+ return IS::GetCurrent().Release();
+ }
+
+ private:
+ bool AtStart_ = true;
+ bool Yield_ = false;
+ };
+
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ static_cast<const T*>(this)->MakeState(ctx, state);
+ }
+
+ EFetchResult DoCalculate(NUdf::TUnboxedValue& state, TComputationContext& ctx, NUdf::TUnboxedValue*const* output) const {
+ if (!state.HasValue()) {
+ MakeState(ctx, state);
+ }
+
+ if (const auto value = static_cast<TState&>(*state.AsBoxed()).FetchRecord(); value.IsFinish())
+ return EFetchResult::Finish;
+ else if (value.IsYield())
+ return EFetchResult::Yield;
+ else {
+ const auto elements = value.GetElements();
+ for (ui32 i = 0U; i < Width; ++i)
+ if (const auto out = *output++)
+ *out = elements[i];
+
+ }
+
+ return EFetchResult::One;
+ }
+
+#ifndef MKQL_DISABLE_CODEGEN
+ ICodegeneratorInlineWideNode::TGenerateResult DoGenGetValues(const TCodegenContext& ctx, Value* statePtr, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto valueType = Type::getInt128Ty(context);
+ const auto arrayType = ArrayType::get(valueType, Width);
+ const auto pointerType = PointerType::getUnqual(arrayType);
+ const auto structPtrType = PointerType::getUnqual(StructType::get(context));
+ const auto statusType = Type::getInt32Ty(context);
+
+ const auto stateType = StructType::get(context, {
+ structPtrType, // vtbl
+ Type::getInt32Ty(context), // ref
+ Type::getInt16Ty(context), // abi
+ Type::getInt16Ty(context), // reserved
+ structPtrType // meminfo
+ });
+
+ const auto statePtrType = PointerType::getUnqual(stateType);
+
+ const auto placeholder = new AllocaInst(pointerType, 0U, "paceholder", &ctx.Func->getEntryBlock().back());
+
+ const auto make = BasicBlock::Create(context, "make", ctx.Func);
+ const auto main = BasicBlock::Create(context, "main", ctx.Func);
+ const auto good = BasicBlock::Create(context, "good", ctx.Func);
+ const auto done = BasicBlock::Create(context, "done", ctx.Func);
+
+ BranchInst::Create(main, make, HasValue(statePtr, block), block);
+ block = make;
+
+ const auto self = CastInst::Create(Instruction::IntToPtr, ConstantInt::get(Type::getInt64Ty(context), uintptr_t(static_cast<const TDqYtReadWrapperBase<T, IS>*>(this))), structPtrType, "self", block);
+ const auto makeFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TDqYtReadWrapperBase<T, IS>::MakeState));
+ const auto makeType = FunctionType::get(Type::getVoidTy(context), {self->getType(), ctx.Ctx->getType(), statePtr->getType()}, false);
+ const auto makeFuncPtr = CastInst::Create(Instruction::IntToPtr, makeFunc, PointerType::getUnqual(makeType), "function", block);
+ CallInst::Create(makeType, makeFuncPtr, {self, ctx.Ctx, statePtr}, "", block);
+ BranchInst::Create(main, block);
+
+ block = main;
+
+ const auto state = new LoadInst(valueType, statePtr, "state", block);
+ const auto half = CastInst::Create(Instruction::Trunc, state, Type::getInt64Ty(context), "half", block);
+ const auto stateArg = CastInst::Create(Instruction::IntToPtr, half, statePtrType, "state_arg", block);
+
+ const auto func = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TState::FetchRecord));
+ const auto funcType = FunctionType::get(valueType, { statePtrType }, false);
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, func, PointerType::getUnqual(funcType), "fetch_func", block);
+ const auto fetch = CallInst::Create(funcType, funcPtr, { stateArg }, "fetch", block);
+
+ const auto result = PHINode::Create(statusType, 2U, "result", done);
+ const auto special = SelectInst::Create(IsYield(fetch, block), ConstantInt::get(statusType, static_cast<i32>(EFetchResult::Yield)), ConstantInt::get(statusType, static_cast<i32>(EFetchResult::Finish)), "special", block);
+ result->addIncoming(special, block);
+
+ BranchInst::Create(done, good, IsSpecial(fetch, block), block);
+
+ block = good;
+
+ const auto elements = CallBoxedValueVirtualMethod<NUdf::TBoxedValueAccessor::EMethod::GetElements>(pointerType, fetch, ctx.Codegen, block);
+ new StoreInst(elements, placeholder, block);
+
+ result->addIncoming(ConstantInt::get(statusType, static_cast<i32>(EFetchResult::One)), block);
+
+ BranchInst::Create(done, block);
+
+ block = done;
+
+ ICodegeneratorInlineWideNode::TGettersList getters;
+ getters.reserve(Width);
+ for (ui32 i = 0U; i < Width; ++i) {
+ getters.emplace_back([i, placeholder, pointerType, arrayType, valueType](const TCodegenContext& ctx, BasicBlock*& block) {
+ const auto indexType = Type::getInt32Ty(ctx.Codegen->GetContext());
+ const auto pointer = new LoadInst(pointerType, placeholder, (TString("pointer_") += ToString(i)).c_str(), block);
+ const auto ptr = GetElementPtrInst::CreateInBounds(arrayType, pointer, {ConstantInt::get(indexType, 0), ConstantInt::get(indexType, i)}, (TString("ptr_") += ToString(i)).c_str(), block);
+ const auto load = new LoadInst(valueType, ptr, (TString("load_") += ToString(i)).c_str(), block);
+ return load;
+ });
+ }
+
+ return {result, std::move(getters)};
+ }
+#endif
+ void RegisterDependencies() const final {}
+
+ const ui32 Width;
+ NCommon::TCodecContext CodecCtx;
+ TMkqlIOSpecs Specs;
+
+ TString ClusterName;
+ TString Token;
+ NYT::TNode SamplingSpec;
+ TVector<std::pair<NYT::TRichYPath, NYT::TFormat>> Tables;
+ size_t Inflight;
+};
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.cpp b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.cpp
new file mode 100644
index 0000000000..7829d6a8c4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.cpp
@@ -0,0 +1,328 @@
+#include "dq_yt_rpc_reader.h"
+
+#include "yt/cpp/mapreduce/common/helpers.h"
+
+#include <yt/yt/library/auth/auth.h>
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/api/rpc_proxy/client_impl.h>
+#include <yt/yt/client/api/rpc_proxy/config.h>
+#include <yt/yt/client/api/rpc_proxy/connection.h>
+#include <yt/yt/client/api/rpc_proxy/row_stream.h>
+
+namespace NYql::NDqs {
+
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+NYT::NYPath::TRichYPath ConvertYPathFromOld(const NYT::TRichYPath& richYPath) {
+ NYT::NYPath::TRichYPath tableYPath(richYPath.Path_);
+ const auto& rngs = richYPath.GetRanges();
+ if (rngs) {
+ TVector<NYT::NChunkClient::TReadRange> ranges;
+ for (const auto& rng: *rngs) {
+ auto& range = ranges.emplace_back();
+ if (rng.LowerLimit_.Offset_) {
+ range.LowerLimit().SetOffset(*rng.LowerLimit_.Offset_);
+ }
+
+ if (rng.LowerLimit_.TabletIndex_) {
+ range.LowerLimit().SetTabletIndex(*rng.LowerLimit_.TabletIndex_);
+ }
+
+ if (*rng.LowerLimit_.RowIndex_) {
+ range.LowerLimit().SetRowIndex(*rng.LowerLimit_.RowIndex_);
+ }
+
+ if (rng.UpperLimit_.Offset_) {
+ range.UpperLimit().SetOffset(*rng.UpperLimit_.Offset_);
+ }
+
+ if (rng.UpperLimit_.TabletIndex_) {
+ range.UpperLimit().SetTabletIndex(*rng.UpperLimit_.TabletIndex_);
+ }
+
+ if (*rng.UpperLimit_.RowIndex_) {
+ range.UpperLimit().SetRowIndex(*rng.UpperLimit_.RowIndex_);
+ }
+ }
+ tableYPath.SetRanges(std::move(ranges));
+ }
+
+ if (richYPath.Columns_) {
+ tableYPath.SetColumns(richYPath.Columns_->Parts_);
+ }
+
+ return tableYPath;
+}
+
+class TFakeRPCReader : public NYT::TRawTableReader {
+public:
+ TFakeRPCReader(NYT::TSharedRef&& payload) : Payload_(std::move(payload)), PayloadStream_(Payload_.Begin(), Payload_.Size()) {}
+
+ bool Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex) override {
+ Y_UNUSED(rangeIndex);
+ Y_UNUSED(rowIndex);
+ return false;
+ }
+
+ void ResetRetries() override {
+
+ }
+
+ bool HasRangeIndices() const override {
+ return true;
+ };
+
+ size_t DoRead(void* buf, size_t len) override {
+ if (!PayloadStream_.Exhausted()) {
+ return PayloadStream_.Read(buf, len);
+ }
+ return 0;
+ };
+
+ virtual ~TFakeRPCReader() override {
+ }
+private:
+ NYT::TSharedRef Payload_;
+ TMemoryInput PayloadStream_;
+};
+}
+#ifdef RPC_PRINT_TIME
+int cnt = 0;
+auto beg = std::chrono::steady_clock::now();
+std::mutex mtx;
+void print_add(int x) {
+ std::lock_guard l(mtx);
+ Cerr << (cnt += x) << " (" << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - beg).count() << "ms gone from process start)\n";
+}
+#endif
+
+struct TSettingsHolder {
+ NYT::NApi::IConnectionPtr Connection;
+ NYT::TIntrusivePtr<NYT::NApi::NRpcProxy::TClient> Client;
+};
+
+TParallelFileInputState::TParallelFileInputState(const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ TVector<NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr>&& rawInputs,
+ size_t blockSize,
+ size_t inflight,
+ std::unique_ptr<TSettingsHolder>&& settings)
+ : RecordByReader_(rawInputs.size(), 0)
+ , IsInputDone_(rawInputs.size(), 0)
+ , Spec_(&spec)
+ , HolderFactory_(holderFactory)
+ , RawInputs_(std::move(rawInputs))
+ , BlockSize_(blockSize)
+ , Inflight_(inflight)
+ , Settings_(std::move(settings))
+{
+#ifdef RPC_PRINT_TIME
+ print_add(1);
+#endif
+ YQL_ENSURE(Spec_->Inputs.size() == RawInputs_.size());
+ MkqlReader_.SetSpecs(*Spec_, HolderFactory_);
+ Valid_ = NextValue();
+}
+
+size_t TParallelFileInputState::GetTableIndex() const {
+ return CurrentInput_;
+}
+
+size_t TParallelFileInputState::GetRecordIndex() const {
+ return CurrentRecord_; // returns 1-based index
+}
+
+void TParallelFileInputState::SetNextBlockCallback(std::function<void()> cb) {
+ MkqlReader_.SetNextBlockCallback(cb);
+ OnNextBlockCallback_ = std::move(cb);
+}
+
+bool TParallelFileInputState::IsValid() const {
+ return Valid_;
+}
+
+NUdf::TUnboxedValue TParallelFileInputState::GetCurrent() {
+ return CurrentValue_;
+}
+
+void TParallelFileInputState::Next() {
+ Valid_ = NextValue();
+}
+
+void TParallelFileInputState::Finish() {
+ MkqlReader_.Finish();
+}
+bool TParallelFileInputState::RunNext() {
+ while (true) {
+ size_t InputIdx = 0;
+ {
+ std::lock_guard lock(Lock_);
+ if (CurrentInputIdx_ == RawInputs_.size() && CurrentInflight_ == 0 && Results_.empty() && !MkqlReader_.IsValid()) {
+ return false;
+ }
+
+ if (CurrentInflight_ >= Inflight_ || Results_.size() >= Inflight_) {
+ break;
+ }
+
+ while (CurrentInputIdx_ < IsInputDone_.size() && IsInputDone_[CurrentInputIdx_]) {
+ ++CurrentInputIdx_;
+ }
+
+ if (CurrentInputIdx_ == IsInputDone_.size()) {
+ break;
+ }
+ InputIdx = CurrentInputIdx_;
+ ++CurrentInputIdx_;
+ ++CurrentInflight_;
+ }
+
+ RawInputs_[InputIdx]->Read().SubscribeUnique(BIND([&, inputIdx = InputIdx
+#ifdef RPC_PRINT_TIME
+ , startAt = std::chrono::steady_clock::now()
+#endif
+ ](NYT::TErrorOr<NYT::TSharedRef>&& res_){
+#ifdef RPC_PRINT_TIME
+ auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - startAt).count();
+ if (elapsed > 1'000'000) {
+ Cerr << (TStringBuilder() "Warn: request took: " << elapsed << " mcs)\n");
+ }
+#endif
+ auto block = std::move(res_.ValueOrThrow());
+ NYT::NApi::NRpcProxy::NProto::TRowsetDescriptor descriptor;
+ NYT::NApi::NRpcProxy::NProto::TRowsetStatistics statistics;
+
+ auto CurrentPayload_ = std::move(NYT::NApi::NRpcProxy::DeserializeRowStreamBlockEnvelope(block, &descriptor, &statistics));
+ std::lock_guard lock(Lock_);
+ CurrentInputIdx_ = std::min(CurrentInputIdx_, inputIdx);
+ --CurrentInflight_;
+ // skip no-skiff data
+ if (descriptor.rowset_format() != NYT::NApi::NRpcProxy::NProto::RF_FORMAT) {
+ WaitPromise_.TrySet();
+ return;
+ }
+
+ if (CurrentPayload_.Empty()) {
+ IsInputDone_[inputIdx] = 1;
+ WaitPromise_.TrySet();
+ return;
+ }
+ Results_.emplace(std::move(TResult{inputIdx, std::move(CurrentPayload_)}));
+ WaitPromise_.TrySet();
+ }));
+ }
+ return true;
+}
+
+bool TParallelFileInputState::NextValue() {
+ for (;;) {
+ if (!RunNext()) {
+ Y_ENSURE(Results_.empty());
+#ifdef RPC_PRINT_TIME
+ print_add(-1);
+#endif
+ return false;
+ }
+ if (MkqlReader_.IsValid()) {
+ CurrentValue_ = std::move(MkqlReader_.GetRow());
+ if (!Spec_->InputGroups.empty()) {
+ CurrentValue_ = HolderFactory_.CreateVariantHolder(CurrentValue_.Release(), Spec_->InputGroups.at(CurrentInput_));
+ }
+ CurrentRecord_ = ++RecordByReader_[CurrentInput_];
+ MkqlReader_.Next();
+ return true;
+ }
+ bool needWait = false;
+ {
+ std::lock_guard lock(Lock_);
+ needWait = Results_.empty();
+ }
+ if (needWait) {
+ YQL_ENSURE(NYT::NConcurrency::WaitFor(WaitPromise_.ToFuture()).IsOK());
+ }
+ TResult result;
+ {
+ std::lock_guard lock(Lock_);
+
+ if (Results_.empty()) {
+ continue;
+ }
+ result = std::move(Results_.front());
+ Results_.pop();
+ WaitPromise_ = NYT::NewPromise<void>();
+ }
+ CurrentInput_ = result.Input_;
+ CurrentReader_ = MakeIntrusive<TFakeRPCReader>(std::move(result.Value_));
+ MkqlReader_.SetReader(*CurrentReader_, 1, BlockSize_, ui32(CurrentInput_), true);
+ MkqlReader_.Next();
+ }
+}
+
+void TDqYtReadWrapperRPC::MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ auto connectionConfig = NYT::New<NYT::NApi::NRpcProxy::TConnectionConfig>();
+ connectionConfig->ClusterUrl = ClusterName;
+ auto connection = CreateConnection(connectionConfig);
+
+ auto clientOptions = NYT::NApi::TClientOptions();
+
+ if (Token) {
+ clientOptions.Token = Token;
+ }
+
+ auto client = DynamicPointerCast<NYT::NApi::NRpcProxy::TClient>(connection->CreateClient(clientOptions));
+ Y_VERIFY(client);
+ auto apiServiceProxy = client->CreateApiServiceProxy();
+
+ TVector<NYT::TFuture<NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr>> waitFor;
+
+ for (auto [richYPath, format]: Tables) {
+ auto request = apiServiceProxy.ReadTable();
+ client->InitStreamingRequest(*request);
+
+ TString ppath;
+ auto tableYPath = ConvertYPathFromOld(richYPath);
+
+ NYT::NYPath::ToProto(&ppath, tableYPath);
+ request->set_path(ppath);
+ request->set_desired_rowset_format(NYT::NApi::NRpcProxy::NProto::ERowsetFormat::RF_FORMAT);
+
+ request->set_enable_table_index(true);
+ request->set_enable_range_index(true);
+ request->set_enable_row_index(true);
+ request->set_unordered(Inflight > 1);
+
+ // https://a.yandex-team.ru/arcadia/yt/yt_proto/yt/client/api/rpc_proxy/proto/api_service.proto?rev=r11519304#L2338
+ if (!SamplingSpec.IsUndefined()) {
+ TStringStream ss;
+ SamplingSpec.Save(&ss);
+ request->set_config(ss.Str());
+ }
+
+ if (richYPath.TransactionId_) {
+ ui32* dw = richYPath.TransactionId_->dw;
+ // P. S. No proper way to convert it
+ request->mutable_transactional_options()->mutable_transaction_id()->set_first((ui64)dw[3] | (ui64(dw[2]) << 32));
+ request->mutable_transactional_options()->mutable_transaction_id()->set_second((ui64)dw[1] | (ui64(dw[0]) << 32));
+ }
+
+ // Get skiff format yson string
+ TStringStream fmt;
+ format.Config.Save(&fmt);
+ request->set_format(fmt.Str());
+
+ waitFor.emplace_back(std::move(CreateRpcClientInputStream(std::move(request)).ApplyUnique(BIND([](NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr&& stream) {
+ // first packet contains meta, skip it
+ return stream->Read().ApplyUnique(BIND([stream = std::move(stream)](NYT::TSharedRef&&) {
+ return std::move(stream);
+ }));
+ }))));
+ }
+
+ TVector<NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr> rawInputs;
+ NYT::NConcurrency::WaitFor(NYT::AllSucceeded(waitFor)).ValueOrThrow().swap(rawInputs);
+ state = ctx.HolderFactory.Create<TDqYtReadWrapperBase<TDqYtReadWrapperRPC, TParallelFileInputState>::TState>(Specs, ctx.HolderFactory, std::move(rawInputs), 4_MB, Inflight, std::make_unique<TSettingsHolder>(TSettingsHolder{std::move(connection), std::move(client)}));
+}
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.h b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.h
new file mode 100644
index 0000000000..97040bfe3b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_rpc_reader.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "dq_yt_reader_impl.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYql::NDqs {
+
+using namespace NKikimr::NMiniKQL;
+
+struct TSettingsHolder;
+
+class TParallelFileInputState: public IInputState {
+struct TResult {
+ size_t Input_ = 0;
+ NYT::TSharedRef Value_;
+};
+public:
+ TParallelFileInputState(const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ TVector<NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr>&& rawInputs,
+ size_t blockSize,
+ size_t inflight,
+ std::unique_ptr<TSettingsHolder>&& client);
+
+ size_t GetTableIndex() const;
+
+ size_t GetRecordIndex() const;
+
+ void SetNextBlockCallback(std::function<void()> cb);
+protected:
+ virtual bool IsValid() const override;
+
+ virtual NUdf::TUnboxedValue GetCurrent() override;
+
+ virtual void Next() override;
+
+ void Finish();
+
+ bool RunNext();
+
+ bool NextValue();
+
+private:
+ std::mutex Lock_;
+ std::queue<TResult> Results_;
+ std::vector<size_t> RecordByReader_;
+ std::vector<bool> IsInputDone_;
+ NYT::TPromise<void> WaitPromise_ = NYT::NewPromise<void>();
+ NYT::TRawTableReaderPtr CurrentReader_ = nullptr;
+ size_t CurrentInflight_ = 0;
+ bool Eof_ = false;
+ const TMkqlIOSpecs* Spec_;
+ const NKikimr::NMiniKQL::THolderFactory& HolderFactory_;
+ TVector<NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr> RawInputs_;
+ const size_t BlockSize_;
+ TMkqlReaderImpl MkqlReader_;
+ size_t CurrentInput_ = 0;
+ size_t CurrentRecord_ = 1;
+ size_t Inflight_ = 1;
+ size_t CurrentInputIdx_ = 0;
+ bool Valid_ = true;
+ std::unique_ptr<TSettingsHolder> Settings_;
+ NUdf::TUnboxedValue CurrentValue_;
+ std::function<void()> OnNextBlockCallback_;
+};
+
+
+class TDqYtReadWrapperRPC : public TDqYtReadWrapperBase<TDqYtReadWrapperRPC, TParallelFileInputState> {
+public:
+using TInputType = NYT::NConcurrency::IAsyncZeroCopyInputStreamPtr;
+ TDqYtReadWrapperRPC(const TComputationNodeFactoryContext& ctx, const TString& clusterName,
+ const TString& token, const NYT::TNode& inputSpec, const NYT::TNode& samplingSpec,
+ const TVector<ui32>& inputGroups,
+ TType* itemType, const TVector<TString>& tableNames, TVector<std::pair<NYT::TRichYPath, NYT::TFormat>>&& tables, NKikimr::NMiniKQL::IStatsRegistry* jobStats, size_t inflight)
+ : TDqYtReadWrapperBase<TDqYtReadWrapperRPC, TParallelFileInputState>(ctx, clusterName, token, inputSpec, samplingSpec, inputGroups, itemType, tableNames, std::move(tables), jobStats, inflight) {}
+
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const;
+};
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.cpp b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.cpp
new file mode 100644
index 0000000000..31e60f4495
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.cpp
@@ -0,0 +1,282 @@
+#include "dq_yt_writer.h"
+
+#include <library/cpp/yson/node/node_io.h>
+#include <util/generic/size_literals.h>
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_codegen.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+
+#include <utility>
+
+namespace NYql::NDqs {
+namespace {
+
+using namespace NKikimr::NMiniKQL;
+using namespace NYT;
+
+class TYtDqWideWriteWrapper final : public TStatefulFlowCodegeneratorNode<TYtDqWideWriteWrapper> {
+ using TBaseComputation = TStatefulFlowCodegeneratorNode<TYtDqWideWriteWrapper>;
+
+ class TWriterState : public TComputationValue<TWriterState> {
+ public:
+ TWriterState(
+ TMemoryUsageInfo* memInfo,
+ IClientPtr&& client,
+ ITransactionPtr&& transaction,
+ THolder<TMkqlIOSpecs>&& specs,
+ TRawTableWriterPtr&& outStream,
+ THolder<TMkqlWriterImpl>&& writer
+ )
+ : TComputationValue<TWriterState>(memInfo)
+ , Client(std::move(client)), Transaction(std::move(transaction))
+ , Specs(std::move(specs)), OutStream(std::move(outStream)), Writer(std::move(writer))
+ {}
+
+ ~TWriterState() override {
+ try {
+ Finish();
+ } catch (...) {
+ }
+ }
+
+ void AddRow(const NUdf::TUnboxedValuePod* row) const {
+ Writer->AddFlatRow(row);
+ }
+
+ void Finish() {
+ if (!Finished) {
+ Writer->Finish();
+ OutStream->Finish();
+ }
+ Finished = true;
+ }
+ private:
+ bool Finished = false;
+ const IClientPtr Client;
+ const ITransactionPtr Transaction;
+ const THolder<TMkqlIOSpecs> Specs;
+ const TRawTableWriterPtr OutStream;
+ const THolder<TMkqlWriterImpl> Writer;
+ };
+
+public:
+ TYtDqWideWriteWrapper(TComputationMutables& mutables,
+ IComputationWideFlowNode* flow,
+ std::vector<EValueRepresentation>&& representations,
+ const std::string_view& clusterName,
+ const std::string_view& token,
+ const TRichYPath& path,
+ const NYT::TNode& outSpec,
+ const NYT::TNode& writerOptions,
+ THolder<NCommon::TCodecContext>&& codecCtx
+ )
+ : TBaseComputation(mutables, flow, EValueRepresentation::Embedded, EValueRepresentation::Boxed)
+ , Flow(flow)
+ , Representations(std::move(representations))
+ , ClusterName(clusterName)
+ , Token(token)
+ , Path(path)
+ , OutSpec(outSpec)
+ , WriterOptions(writerOptions)
+ , CodecCtx(std::move(codecCtx))
+ , Values(Representations.size()), Fields(GetPointers(Values))
+ {}
+
+ NUdf::TUnboxedValuePod DoCalculate(NUdf::TUnboxedValue& state, TComputationContext& ctx) const {
+ if (state.IsFinish()) {
+ return NUdf::TUnboxedValuePod::MakeFinish();
+ } else if (state.IsInvalid())
+ MakeState(ctx, state);
+
+ switch (const auto ptr = static_cast<TWriterState*>(state.AsBoxed().Get()); Flow->FetchValues(ctx, Fields.data())) {
+ case EFetchResult::One:
+ ptr->AddRow(Values.data());
+ return NUdf::TUnboxedValuePod::Void();
+ case EFetchResult::Yield:
+ return NUdf::TUnboxedValuePod::MakeYield();
+ case EFetchResult::Finish:
+ ptr->Finish();
+ state = NUdf::TUnboxedValuePod::MakeFinish();
+ return NUdf::TUnboxedValuePod::MakeFinish();
+ }
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ Value* DoGenerateGetValue(const TCodegenContext& ctx, Value* statePtr, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto valueType = Type::getInt128Ty(context);
+ const auto indexType = Type::getInt32Ty(context);
+ const auto structPtrType = PointerType::getUnqual(StructType::get(context));
+ const auto arrayType = ArrayType::get(valueType, Representations.size());
+ const auto values = new AllocaInst(arrayType, 0U, "values", &ctx.Func->getEntryBlock().back());
+
+ const auto init = BasicBlock::Create(context, "init", ctx.Func);
+ const auto next = BasicBlock::Create(context, "next", ctx.Func);
+ const auto work = BasicBlock::Create(context, "work", ctx.Func);
+ const auto done = BasicBlock::Create(context, "done", ctx.Func);
+ const auto exit = BasicBlock::Create(context, "exit", ctx.Func);
+
+ const auto output = PHINode::Create(valueType, 4U, "output", exit);
+ output->addIncoming(GetFinish(context), block);
+
+ const auto check = new LoadInst(valueType, statePtr, "check", block);
+ const auto choise = SwitchInst::Create(check, next, 2U, block);
+ choise->addCase(GetInvalid(context), init);
+ choise->addCase(GetFinish(context), exit);
+
+ block = init;
+
+ const auto ptrType = PointerType::getUnqual(StructType::get(context));
+ const auto self = CastInst::Create(Instruction::IntToPtr, ConstantInt::get(Type::getInt64Ty(context), uintptr_t(this)), ptrType, "self", block);
+ const auto makeFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtDqWideWriteWrapper::MakeState));
+ const auto makeType = FunctionType::get(Type::getVoidTy(context), {self->getType(), ctx.Ctx->getType(), statePtr->getType()}, false);
+ const auto makeFuncPtr = CastInst::Create(Instruction::IntToPtr, makeFunc, PointerType::getUnqual(makeType), "function", block);
+ CallInst::Create(makeType, makeFuncPtr, {self, ctx.Ctx, statePtr}, "", block);
+
+ BranchInst::Create(next, block);
+
+ block = next;
+
+ const auto result = GetNodeValues(Flow, ctx, block);
+
+ output->addIncoming(GetYield(context), block);
+
+ const auto way = SwitchInst::Create(result.first, work, 2U, block);
+ way->addCase(ConstantInt::get(indexType, static_cast<i32>(EFetchResult::Yield)), exit);
+ way->addCase(ConstantInt::get(indexType, static_cast<i32>(EFetchResult::Finish)), done);
+
+ {
+ block = work;
+
+ TSmallVec<Value*> fields;
+ fields.reserve(Representations.size());
+ for (ui32 i = 0U; i < Representations.size(); ++i) {
+ const auto pointer = GetElementPtrInst::CreateInBounds(arrayType, values, {ConstantInt::get(indexType, 0), ConstantInt::get(indexType, i)}, (TString("ptr_") += ToString(i)).c_str(), block);
+ fields.emplace_back(result.second[i](ctx, block));
+ new StoreInst(fields.back(), pointer, block);
+ }
+
+ const auto state = new LoadInst(valueType, statePtr, "state", block);
+ const auto half = CastInst::Create(Instruction::Trunc, state, Type::getInt64Ty(context), "half", block);
+ const auto stateArg = CastInst::Create(Instruction::IntToPtr, half, structPtrType, "state_arg", block);
+
+ const auto addFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TWriterState::AddRow));
+ const auto addType = FunctionType::get(Type::getVoidTy(context), {stateArg->getType(), values->getType()}, false);
+ const auto addPtr = CastInst::Create(Instruction::IntToPtr, addFunc, PointerType::getUnqual(addType), "write", block);
+ CallInst::Create(addType, addPtr, {stateArg, values}, "", block);
+
+ for (ui32 i = 0U; i < Representations.size(); ++i) {
+ ValueCleanup(Representations[i], fields[i], ctx, block);
+ }
+
+ output->addIncoming(GetFalse(context), block);
+ BranchInst::Create(exit, block);
+ }
+
+ {
+ block = done;
+
+ const auto state = new LoadInst(valueType, statePtr, "state", block);
+ const auto half = CastInst::Create(Instruction::Trunc, state, Type::getInt64Ty(context), "half", block);
+ const auto stateArg = CastInst::Create(Instruction::IntToPtr, half, structPtrType, "state_arg", block);
+
+ const auto finishFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TWriterState::Finish));
+ const auto finishType = FunctionType::get(Type::getVoidTy(context), {stateArg->getType()}, false);
+ const auto finishPtr = CastInst::Create(Instruction::IntToPtr, finishFunc, PointerType::getUnqual(finishType), "finish", block);
+ CallInst::Create(finishType, finishPtr, {stateArg}, "", block);
+
+ UnRefBoxed(statePtr, ctx, block);
+ new StoreInst(GetFinish(context), statePtr, block);
+
+ output->addIncoming(GetFinish(context), block);
+ BranchInst::Create(exit, block);
+ }
+
+ block = exit;
+ return output;
+ }
+#endif
+private:
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ TString token;
+ if (NUdf::TStringRef tokenRef(Token); ctx.Builder->GetSecureParam(tokenRef, tokenRef)) {
+ token = tokenRef;
+ }
+
+ NYT::TCreateClientOptions createOpts;
+ if (token) {
+ createOpts.Token(token);
+ }
+
+ auto client = NYT::CreateClient(ClusterName, createOpts);
+ auto transaction = client->AttachTransaction(Path.TransactionId_.GetRef());
+ auto specs = MakeHolder<TMkqlIOSpecs>();
+ specs->SetUseSkiff("");
+ specs->Init(*CodecCtx, OutSpec);
+ auto path = Path;
+ path.TransactionId_.Clear();
+ NYT::TTableWriterOptions options;
+ options.Config(WriterOptions);
+ auto outStream = transaction->CreateRawWriter(path, specs->MakeOutputFormat(), options);
+
+ auto writer = MakeHolder<TMkqlWriterImpl>(outStream, 4_MB);
+ writer->SetSpecs(*specs);
+
+ state = ctx.HolderFactory.Create<TWriterState>(std::move(client), std::move(transaction), std::move(specs), std::move(outStream), std::move(writer));
+ }
+
+ void RegisterDependencies() const final {
+ FlowDependsOn(Flow);
+ }
+
+ static std::vector<NUdf::TUnboxedValue*> GetPointers(std::vector<NUdf::TUnboxedValue>& array) {
+ std::vector<NUdf::TUnboxedValue*> pointers;
+ pointers.reserve(array.size());
+ std::transform(array.begin(), array.end(), std::back_inserter(pointers), [](NUdf::TUnboxedValue& v) { return std::addressof(v); });
+ return pointers;
+ }
+
+ IComputationWideFlowNode* const Flow;
+ const std::vector<EValueRepresentation> Representations;
+
+ const TString ClusterName;
+ const TString Token;
+ const TRichYPath Path;
+ const NYT::TNode OutSpec;
+ const NYT::TNode WriterOptions;
+ const THolder<NCommon::TCodecContext> CodecCtx;
+
+ std::vector<NUdf::TUnboxedValue> Values;
+ const std::vector<NUdf::TUnboxedValue*> Fields;
+};
+
+}
+
+IComputationNode* WrapYtDqRowsWideWrite(TCallable& callable, const TComputationNodeFactoryContext& ctx) {
+ YQL_ENSURE(callable.GetInputsCount() == 6, "Expected six args.");
+
+ const auto& clusterName = AS_VALUE(TDataLiteral, callable.GetInput(1))->AsValue().AsStringRef();
+ const auto& token = AS_VALUE(TDataLiteral, callable.GetInput(2))->AsValue().AsStringRef();
+
+ TRichYPath richYPath;
+ Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, callable.GetInput(3))->AsValue().AsStringRef())));
+
+ const NYT::TNode outSpec(NYT::NodeFromYsonString((AS_VALUE(TDataLiteral, callable.GetInput(4))->AsValue().AsStringRef())));
+ const NYT::TNode writerOptions(NYT::NodeFromYsonString((AS_VALUE(TDataLiteral, callable.GetInput(5))->AsValue().AsStringRef())));
+ const auto node = LocateNode(ctx.NodeLocator, callable, 0);
+
+ std::vector<EValueRepresentation> representations;
+ auto wideComponents = GetWideComponents(AS_TYPE(TFlowType, callable.GetInput(0).GetStaticType()));
+ representations.reserve(wideComponents.size());
+ for (ui32 i = 0U; i < wideComponents.size(); ++i) {
+ representations.emplace_back(GetValueRepresentation(wideComponents[i]));
+ }
+
+ auto codecCtx = MakeHolder<NCommon::TCodecContext>(ctx.Env, ctx.FunctionRegistry, &ctx.HolderFactory);
+ return new TYtDqWideWriteWrapper(ctx.Mutables, static_cast<IComputationWideFlowNode*>(node), std::move(representations), clusterName, token, richYPath, outSpec, writerOptions, std::move(codecCtx));
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.h b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.h
new file mode 100644
index 0000000000..de14269bef
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/dq_yt_writer.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+namespace NYql::NDqs {
+
+NKikimr::NMiniKQL::IComputationNode* WrapYtDqRowsWideWrite(NKikimr::NMiniKQL::TCallable& callable, const NKikimr::NMiniKQL::TComputationNodeFactoryContext& ctx);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/dq/ya.make b/ydb/library/yql/providers/yt/comp_nodes/dq/ya.make
new file mode 100644
index 0000000000..9f94d73e3d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/dq/ya.make
@@ -0,0 +1,35 @@
+LIBRARY()
+
+PEERDIR(
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/providers/yt/comp_nodes
+ ydb/library/yql/providers/yt/codec
+ ydb/library/yql/providers/common/codec
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/common
+ library/cpp/yson/node
+ yt/yt/core
+)
+
+IF(LINUX)
+ PEERDIR(
+ yt/yt/client
+ yt/yt/client/arrow
+ )
+
+ SRCS(
+ dq_yt_rpc_reader.cpp
+ )
+ENDIF()
+
+SRCS(
+ dq_yt_reader.cpp
+ dq_yt_factory.cpp
+ dq_yt_writer.cpp
+)
+
+
+YQL_LAST_ABI_VERSION()
+
+
+END()
diff --git a/ydb/library/yql/providers/yt/comp_nodes/ut/ya.make b/ydb/library/yql/providers/yt/comp_nodes/ut/ya.make
new file mode 100644
index 0000000000..ae557f77e2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/ut/ya.make
@@ -0,0 +1,29 @@
+UNITTEST_FOR(ydb/library/yql/providers/yt/comp_nodes)
+
+FORK_SUBTESTS()
+
+IF (SANITIZER_TYPE OR WITH_VALGRIND)
+ TIMEOUT(1800)
+ SIZE(LARGE)
+ TAG(ya:fat)
+ELSE()
+ TIMEOUT(600)
+ SIZE(MEDIUM)
+ENDIF()
+
+SRCS(
+ yql_mkql_output_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/random_provider
+ library/cpp/time_provider
+ ydb/library/yql/minikql/comp_nodes/llvm
+ ydb/library/yql/public/udf/service/exception_policy
+ ydb/library/yql/providers/yt/comp_nodes
+ ydb/library/yql/sql/pg_dummy
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/comp_nodes/ut/yql_mkql_output_ut.cpp b/ydb/library/yql/providers/yt/comp_nodes/ut/yql_mkql_output_ut.cpp
new file mode 100644
index 0000000000..45625efd75
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/ut/yql_mkql_output_ut.cpp
@@ -0,0 +1,319 @@
+#include <ydb/library/yql/minikql/comp_nodes/ut/mkql_computation_node_ut.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_impl.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+#include <ydb/library/yql/minikql/mkql_mem_info.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h>
+
+#include <util/generic/array_size.h>
+#include <util/generic/array_ref.h>
+
+#include <cstring>
+
+namespace NKikimr {
+namespace NMiniKQL {
+
+namespace {
+
+TIntrusivePtr<IRandomProvider> CreateRandomProvider() {
+ return CreateDeterministicRandomProvider(1);
+}
+
+TIntrusivePtr<ITimeProvider> CreateTimeProvider() {
+ return CreateDeterministicTimeProvider(10000000);
+}
+
+TComputationNodeFactory GetTestFactory(NYql::TMkqlWriterImpl& writer) {
+ return [&](TCallable& callable, const TComputationNodeFactoryContext& ctx) -> IComputationNode* {
+ if (callable.GetType()->GetName() == "YtOutput") {
+ return NYql::WrapYtOutput(callable, ctx, writer);
+ }
+ return GetBuiltinFactory()(callable, ctx);
+ };
+}
+
+template<bool UseLLVM>
+struct TSetup_ {
+ TSetup_()
+ : FunctionRegistry(CreateFunctionRegistry(CreateBuiltinRegistry()))
+ , RandomProvider(CreateRandomProvider())
+ , TimeProvider(CreateTimeProvider())
+ , Alloc(__LOCATION__)
+ , Env(MakeHolder<TTypeEnvironment>(Alloc))
+ , PgmBuilder(MakeHolder<TProgramBuilder>(*Env, *FunctionRegistry))
+ {}
+
+ TAutoPtr<IComputationGraph> BuildGraph(TRuntimeNode pgm, NYql::TMkqlWriterImpl& writer) {
+ Explorer.Walk(pgm.GetNode(), *Env);
+ TComputationPatternOpts opts(Alloc.Ref(), *Env, GetTestFactory(writer), FunctionRegistry.Get(),
+ NUdf::EValidateMode::None, NUdf::EValidatePolicy::Exception, UseLLVM ? "" : "OFF", EGraphPerProcess::Multi);
+ Pattern = MakeComputationPattern(Explorer, pgm, {}, opts);
+ return Pattern->Clone(opts.ToComputationOptions(*RandomProvider, *TimeProvider));
+ }
+
+ TIntrusivePtr<IFunctionRegistry> FunctionRegistry;
+ TIntrusivePtr<IRandomProvider> RandomProvider;
+ TIntrusivePtr<ITimeProvider> TimeProvider;
+
+ TScopedAlloc Alloc;
+ THolder<TTypeEnvironment> Env;
+ THolder<TProgramBuilder> PgmBuilder;
+
+ TExploringNodeVisitor Explorer;
+ IComputationPattern::TPtr Pattern;
+};
+
+template<bool LLVM>
+TRuntimeNode MakeYtWrite(TSetup_<LLVM>& setup, TRuntimeNode item) {
+ TProgramBuilder& pb = *setup.PgmBuilder;
+ TCallableBuilder callableBuilder(*setup.Env, "YtOutput", pb.NewFlowType(pb.NewVoid().GetStaticType()));
+ callableBuilder.Add(item);
+ return TRuntimeNode(callableBuilder.Build(), false);
+}
+
+} // unnamed
+
+Y_UNIT_TEST_SUITE(YtWriterTests) {
+ Y_UNIT_TEST_LLVM(SimpleYson) {
+ TSetup_<LLVM> setup;
+ TProgramBuilder& pb = *setup.PgmBuilder;
+
+ const auto key1 = pb.NewDataLiteral<ui32>(1);
+ const auto key2 = pb.NewDataLiteral<ui32>(2);
+ const auto key3 = pb.NewDataLiteral<ui32>(3);
+
+ const auto payload1 = pb.NewDataLiteral<NUdf::EDataSlot::String>("aaa");
+ const auto payload2 = pb.NewDataLiteral<NUdf::EDataSlot::String>("");
+ const auto payload3 = pb.NewDataLiteral<NUdf::EDataSlot::String>("qqq");
+
+ const auto keyType = pb.NewDataType(NUdf::TDataType<ui32>::Id);
+ const auto payloadType = pb.NewDataType(NUdf::TDataType<char*>::Id);
+ auto structType = pb.NewEmptyStructType();
+ structType = pb.NewStructType(structType, "payload", payloadType);
+ structType = pb.NewStructType(structType, "key", keyType);
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map1 = {
+ { "key", key1 },
+ { "payload", payload1 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map2 = {
+ { "key", key2 },
+ { "payload", payload2 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map3 = {
+ { "key", key3 },
+ { "payload", payload3 }
+ };
+
+ const auto list = pb.NewList(structType, {
+ pb.NewStruct(map2),
+ pb.NewStruct(map1),
+ pb.NewStruct(map3)
+ });
+
+ const auto pgmReturn = pb.Discard(pb.Map(pb.ToFlow(list),
+ [&](TRuntimeNode item) {
+ return MakeYtWrite(setup, item);
+ }
+ ));
+
+ const TString spec = R"({
+ tables = [{
+ "_yql_row_spec" = {
+ "Type" = [
+ "StructType"; [
+ ["key"; ["DataType"; "Uint32"]];
+ ["payload"; ["DataType"; "String"]]
+ ]
+ ]
+ }
+ }]
+ })";
+
+ NYql::NCommon::TCodecContext CodecCtx(*setup.Env, *setup.FunctionRegistry);
+ NYql::TMkqlIOSpecs specs;
+ specs.Init(CodecCtx, spec);
+
+ TStringStream strm;
+ NYql::TMkqlWriterImpl writer(strm, 0ULL, 1ULL << 20ULL);
+ writer.SetSpecs(specs);
+
+ const auto graph = setup.BuildGraph(pgmReturn, writer);
+ UNIT_ASSERT(graph->GetValue().IsFinish());
+ writer.Finish();
+ strm.Finish();
+
+ const auto& output = strm.Str();
+ const std::string_view expected("{\1\6key=\6\2;\1\x0Epayload=\1\0;};{\1\6key=\6\1;\1\x0Epayload=\1\6aaa;};{\1\6key=\6\3;\1\x0Epayload=\1\6qqq;};", 81);
+ UNIT_ASSERT_STRINGS_EQUAL(output, expected);
+ }
+
+ Y_UNIT_TEST_LLVM(SimpleSkiff) {
+ TSetup_<LLVM> setup;
+ TProgramBuilder& pb = *setup.PgmBuilder;
+
+ const auto key1 = pb.NewDataLiteral<ui32>(1);
+ const auto key2 = pb.NewDataLiteral<ui32>(2);
+ const auto key3 = pb.NewDataLiteral<ui32>(3);
+
+ const auto payload1 = pb.NewDataLiteral<NUdf::EDataSlot::String>("aaa");
+ const auto payload2 = pb.NewDataLiteral<NUdf::EDataSlot::String>("");
+ const auto payload3 = pb.NewDataLiteral<NUdf::EDataSlot::String>("qqq");
+
+ const auto keyType = pb.NewDataType(NUdf::TDataType<ui32>::Id);
+ const auto payloadType = pb.NewDataType(NUdf::TDataType<char*>::Id);
+ auto structType = pb.NewEmptyStructType();
+ structType = pb.NewStructType(structType, "payload", payloadType);
+ structType = pb.NewStructType(structType, "key", keyType);
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map1 = {
+ { "key", key1 },
+ { "payload", payload1 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map2 = {
+ { "key", key2 },
+ { "payload", payload2 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map3 = {
+ { "key", key3 },
+ { "payload", payload3 }
+ };
+
+ const auto list = pb.NewList(structType, {
+ pb.NewStruct(map2),
+ pb.NewStruct(map1),
+ pb.NewStruct(map3)
+ });
+
+ const auto pgmReturn = pb.Discard(MakeYtWrite(setup, pb.ToFlow(list)));
+
+ const TString spec = R"({
+ tables = [{
+ "_yql_row_spec" = {
+ "Type" = [
+ "StructType"; [
+ ["key"; ["DataType"; "Uint32"]];
+ ["payload"; ["DataType"; "String"]]
+ ]
+ ]
+ }
+ }]
+ })";
+
+ NYql::NCommon::TCodecContext CodecCtx(*setup.Env, *setup.FunctionRegistry);
+ NYql::TMkqlIOSpecs specs;
+ specs.SetUseSkiff(LLVM ? "": "OFF");
+ specs.Init(CodecCtx, spec);
+
+ TStringStream strm;
+ NYql::TMkqlWriterImpl writer(strm, 0ULL, 1ULL << 20ULL);
+ writer.SetSpecs(specs);
+
+ const auto graph = setup.BuildGraph(pgmReturn, writer);
+ UNIT_ASSERT(graph->GetValue().IsFinish());
+ writer.Finish();
+ strm.Finish();
+
+ const auto& output = strm.Str();
+ const std::string_view expected("\0\0\2\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\3\0\0\0aaa\0\0\3\0\0\0\0\0\0\0\3\0\0\0qqq", 48);
+ UNIT_ASSERT_STRINGS_EQUAL(output, expected);
+ }
+
+ Y_UNIT_TEST_LLVM(FlattenSkiff) {
+ TSetup_<LLVM> setup;
+ TProgramBuilder& pb = *setup.PgmBuilder;
+
+ const auto key1 = pb.NewDataLiteral<ui32>(1);
+ const auto key2 = pb.NewDataLiteral<ui32>(2);
+ const auto key3 = pb.NewDataLiteral<ui32>(3);
+
+ const auto payload1 = pb.NewDataLiteral<NUdf::EDataSlot::String>("aaa");
+ const auto payload2 = pb.NewDataLiteral<NUdf::EDataSlot::String>("");
+ const auto payload3 = pb.NewDataLiteral<NUdf::EDataSlot::String>("qqq");
+
+ const auto keyType = pb.NewDataType(NUdf::TDataType<ui32>::Id);
+ const auto payloadType = pb.NewDataType(NUdf::TDataType<char*>::Id);
+ auto structType = pb.NewEmptyStructType();
+ structType = pb.NewStructType(structType, "payload", payloadType);
+ structType = pb.NewStructType(structType, "key", keyType);
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map1 = {
+ { "key", key1 },
+ { "payload", payload1 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map2 = {
+ { "key", key2 },
+ { "payload", payload2 }
+ };
+
+ std::vector<std::pair<std::string_view, TRuntimeNode>> map3 = {
+ { "key", key3 },
+ { "payload", payload3 }
+ };
+
+ const auto list = pb.NewList(structType, {
+ pb.NewStruct(map2),
+ pb.NewStruct(map1),
+ pb.NewStruct(map3)
+ });
+
+ const auto flow = pb.ExpandMap(pb.ToFlow(list),
+ [&](TRuntimeNode item) -> TRuntimeNode::TList {
+ return { pb.Member(item, "key"), pb.Member(item, "payload")};
+ }
+ );
+
+ const auto pgmReturn = pb.Discard(MakeYtWrite(setup, flow));
+
+ const TString spec = R"({
+ tables = [{
+ "_yql_row_spec" = {
+ "Type" = [
+ "StructType"; [
+ ["key"; ["DataType"; "Uint32"]];
+ ["payload"; ["DataType"; "String"]]
+ ]
+ ]
+ }
+ }]
+ })";
+
+ NYql::NCommon::TCodecContext CodecCtx(*setup.Env, *setup.FunctionRegistry);
+ NYql::TMkqlIOSpecs specs;
+ specs.SetUseSkiff(LLVM ? "": "OFF");
+ specs.Init(CodecCtx, spec);
+
+ TStringStream strm;
+ NYql::TMkqlWriterImpl writer(strm, 0ULL, 1ULL << 20ULL);
+ writer.SetSpecs(specs);
+
+ const auto graph = setup.BuildGraph(pgmReturn, writer);
+ UNIT_ASSERT(graph->GetValue().IsFinish());
+ writer.Finish();
+ strm.Finish();
+
+ const auto& output = strm.Str();
+ const std::string_view expected("\0\0\2\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\3\0\0\0aaa\0\0\3\0\0\0\0\0\0\0\3\0\0\0qqq", 48);
+ UNIT_ASSERT_STRINGS_EQUAL(output, expected);
+ }
+}
+
+} // NMiniKQL
+} // NKikimr
diff --git a/ydb/library/yql/providers/yt/comp_nodes/ya.make b/ydb/library/yql/providers/yt/comp_nodes/ya.make
new file mode 100644
index 0000000000..b1d3159abd
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/ya.make
@@ -0,0 +1,30 @@
+LIBRARY()
+
+SRCS(
+ yql_mkql_file_input_state.cpp
+ yql_mkql_file_list.cpp
+ yql_mkql_input_stream.cpp
+ yql_mkql_input.cpp
+ yql_mkql_output.cpp
+ yql_mkql_table.cpp
+ yql_mkql_ungrouping_list.cpp
+)
+
+PEERDIR(
+ library/cpp/streams/brotli
+ ydb/library/yql/minikql
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/yt/codec
+ ydb/library/yql/providers/yt/expr_nodes
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
new file mode 100644
index 0000000000..31c2ea28ff
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.cpp
@@ -0,0 +1,74 @@
+#include "yql_mkql_file_input_state.h"
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/system/fs.h>
+
+namespace NYql {
+
+TFileInputState::TFileInputState(const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ TVector<NYT::TRawTableReaderPtr>&& rawReaders,
+ size_t blockCount,
+ size_t blockSize)
+ : Spec_(&spec)
+ , HolderFactory_(holderFactory)
+ , RawReaders_(std::move(rawReaders))
+ , BlockCount_(blockCount)
+ , BlockSize_(blockSize)
+{
+ YQL_ENSURE(Spec_->Inputs.size() == RawReaders_.size());
+ MkqlReader_.SetSpecs(*Spec_, HolderFactory_);
+ Valid_ = NextValue();
+}
+
+bool TFileInputState::NextValue() {
+ for (;;) {
+ if (CurrentInput_ >= RawReaders_.size()) {
+ return false;
+ }
+
+ if (!MkqlReader_.IsValid()) {
+ if (!RawReaders_[CurrentInput_]) {
+ ++CurrentInput_;
+ continue;
+ }
+
+ CurrentReader_ = std::move(RawReaders_[CurrentInput_]);
+ if (CurrentInput_ > 0 && OnNextBlockCallback_) {
+ OnNextBlockCallback_();
+ }
+ MkqlReader_.SetReader(*CurrentReader_, BlockCount_, BlockSize_, ui32(CurrentInput_), true);
+ MkqlReader_.Next();
+ CurrentRecord_ = 0;
+
+ if (!MkqlReader_.IsValid()) {
+ ++CurrentInput_;
+ continue;
+ }
+ }
+
+ CurrentValue_ = MkqlReader_.GetRow();
+ if (!Spec_->InputGroups.empty()) {
+ CurrentValue_ = HolderFactory_.CreateVariantHolder(CurrentValue_.Release(), Spec_->InputGroups.at(CurrentInput_));
+ }
+
+ MkqlReader_.Next();
+ ++CurrentRecord_;
+ return true;
+ }
+}
+
+TVector<NYT::TRawTableReaderPtr> MakeMkqlFileInputs(const TVector<TString>& files, bool decompress) {
+ TVector<NYT::TRawTableReaderPtr> rawReaders;
+ for (auto& file: files) {
+ if (!NFs::Exists(file)) {
+ rawReaders.emplace_back(nullptr);
+ continue;
+ }
+ rawReaders.emplace_back(MakeIntrusive<TMkqlInput>(MakeFileInput(file, decompress)));
+ }
+ return rawReaders;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h
new file mode 100644
index 0000000000..e83859abe6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "yql_mkql_input_stream.h"
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/ptr.h>
+
+
+namespace NYql {
+
+class TFileInputState: public IInputState {
+public:
+ TFileInputState(const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ TVector<NYT::TRawTableReaderPtr>&& rawReaders,
+ size_t blockCount,
+ size_t blockSize);
+
+ size_t GetTableIndex() const {
+ return CurrentInput_;
+ }
+
+ size_t GetRecordIndex() const {
+ return CurrentRecord_; // returns 1-based index
+ }
+
+ void SetNextBlockCallback(std::function<void()> cb) {
+ MkqlReader_.SetNextBlockCallback(cb);
+ OnNextBlockCallback_ = std::move(cb);
+ }
+
+protected:
+ virtual bool IsValid() const override {
+ return Valid_;
+ }
+
+ virtual NUdf::TUnboxedValue GetCurrent() override {
+ return CurrentValue_;
+ }
+
+ virtual void Next() override {
+ Valid_ = NextValue();
+ }
+
+ void Finish() {
+ MkqlReader_.Finish();
+ }
+
+ bool NextValue();
+
+private:
+ const TMkqlIOSpecs* Spec_;
+ const NKikimr::NMiniKQL::THolderFactory& HolderFactory_;
+ TVector<NYT::TRawTableReaderPtr> RawReaders_;
+ const size_t BlockCount_;
+ const size_t BlockSize_;
+ TMkqlReaderImpl MkqlReader_;
+ size_t CurrentInput_ = 0;
+ size_t CurrentRecord_ = 0;
+ bool Valid_ = true;
+ NUdf::TUnboxedValue CurrentValue_;
+ NYT::TRawTableReaderPtr CurrentReader_;
+ std::function<void()> OnNextBlockCallback_;
+};
+
+TVector<NYT::TRawTableReaderPtr> MakeMkqlFileInputs(const TVector<TString>& files, bool decompress);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
new file mode 100644
index 0000000000..5c65e85c8d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.cpp
@@ -0,0 +1,41 @@
+#include "yql_mkql_file_list.h"
+#include "yql_mkql_file_input_state.h"
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+TFileListValueBase::TIterator::TIterator(TMemoryUsageInfo* memInfo, THolder<IInputState>&& state, std::optional<ui64> length)
+ : TComputationValue(memInfo)
+ , State_(std::move(state))
+ , ExpectedLength_(std::move(length))
+{
+}
+
+bool TFileListValueBase::TIterator::Next(NUdf::TUnboxedValue& value) {
+ if (!AtStart_) {
+ State_->Next();
+ }
+ AtStart_ = false;
+ if (!State_->IsValid()) {
+ MKQL_ENSURE(!ExpectedLength_ || *ExpectedLength_ == 0, "Invalid file length");
+ return false;
+ }
+
+ if (ExpectedLength_) {
+ MKQL_ENSURE(*ExpectedLength_ > 0, "Invalid file length");
+ --(*ExpectedLength_);
+ }
+ value = State_->GetCurrent();
+ return true;
+}
+
+NUdf::TUnboxedValue TFileListValueBase::GetListIterator() const {
+ return NUdf::TUnboxedValuePod(new TIterator(GetMemInfo(), MakeState(), Length));
+}
+
+THolder<IInputState> TFileListValue::MakeState() const {
+ return MakeHolder<TFileInputState>(Spec, HolderFactory, MakeMkqlFileInputs(FilePaths, Decompress), BlockCount, BlockSize);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h
new file mode 100644
index 0000000000..8cb8ec7935
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "yql_mkql_input_stream.h"
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_custom_list.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+
+#include <optional>
+
+namespace NYql {
+
+class TFileListValueBase : public NKikimr::NMiniKQL::TCustomListValue {
+public:
+ TFileListValueBase(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo, const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory, std::optional<ui64> length)
+ : TCustomListValue(memInfo)
+ , Spec(spec)
+ , HolderFactory(holderFactory)
+ {
+ Length = length;
+ }
+
+protected:
+ class TIterator : public NKikimr::NMiniKQL::TComputationValue<TIterator> {
+ public:
+ TIterator(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo, THolder<IInputState>&& state, std::optional<ui64> length);
+
+ private:
+ bool Next(NUdf::TUnboxedValue& value) override;
+
+ bool AtStart_ = true;
+ THolder<IInputState> State_;
+ std::optional<ui64> ExpectedLength_;
+ };
+
+ NUdf::TUnboxedValue GetListIterator() const override;
+
+ virtual THolder<IInputState> MakeState() const = 0;
+
+protected:
+ const TMkqlIOSpecs& Spec;
+ const NKikimr::NMiniKQL::THolderFactory& HolderFactory;
+};
+
+class TFileListValue : public TFileListValueBase {
+public:
+ TFileListValue(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo,
+ const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ const TVector<TString>& filePaths,
+ bool decompress,
+ size_t blockCount,
+ size_t blockSize,
+ std::optional<ui64> length)
+ : TFileListValueBase(memInfo, spec, holderFactory, length)
+ , FilePaths(filePaths)
+ , Decompress(decompress)
+ , BlockCount(blockCount)
+ , BlockSize(blockSize)
+ {
+ }
+
+protected:
+ THolder<IInputState> MakeState() const override;
+
+private:
+ const TVector<TString> FilePaths;
+ const bool Decompress;
+ const size_t BlockCount;
+ const size_t BlockSize;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
new file mode 100644
index 0000000000..d0acf1a212
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.cpp
@@ -0,0 +1,393 @@
+#include "yql_mkql_input.h"
+#include "yql_mkql_table.h"
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_codegen.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+#include <ydb/library/yql/public/udf/udf_value_builder.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <algorithm>
+#include <functional>
+#include <array>
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+class TInputStateBase: public TComputationValue<TInputStateBase> {
+public:
+ TInputStateBase(TMemoryUsageInfo* memInfo,
+ const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input, TComputationContext& ctx,
+ const std::array<IComputationExternalNode*, 5>& argNodes)
+ : TComputationValue<TInputStateBase>(memInfo)
+ , SpecsCache_(specs, ctx.HolderFactory)
+ , TableState_(specs.TableNames, specs.TableOffsets, ctx, argNodes)
+ , Input_(input)
+ , IsValid_(input->IsValid())
+ {
+ }
+ virtual ~TInputStateBase() = default;
+
+ NUdf::TUnboxedValuePod FetchRecord() {
+ if (AtStart_) {
+ UpdateTableState(true);
+ } else {
+ ReadNext();
+ }
+ AtStart_ = false;
+
+ if (!IsValid_) {
+ return NUdf::TUnboxedValuePod();
+ }
+
+ const size_t tableIndex = Input_->GetTableIndex();
+ NUdf::TUnboxedValue result = GetCurrent(tableIndex);
+
+ auto& specs = SpecsCache_.GetSpecs();
+ if (!specs.InputGroups.empty()) {
+ result = SpecsCache_.GetHolderFactory().CreateVariantHolder(result.Release(), specs.InputGroups.at(tableIndex));
+ }
+ return result.Release().MakeOptional();
+ }
+
+protected:
+ virtual NUdf::TUnboxedValue GetCurrent(size_t tableIndex) = 0;
+
+ void ReadNext() {
+ if (Y_LIKELY(IsValid_)) {
+ Input_->Next();
+ IsValid_ = Input_->IsValid();
+ bool keySwitch = false;
+ if (!IsValid_) {
+ Input_->NextKey();
+ if (Input_->IsValid()) {
+ Input_->Next();
+ keySwitch = true;
+ IsValid_ = Input_->IsValid();
+ }
+ }
+ UpdateTableState(keySwitch);
+ }
+ }
+
+ void UpdateTableState(bool keySwitch) {
+ if (IsValid_) {
+ TableState_.Update(Input_->GetTableIndex(), Input_->GetRowIndex() + 1, keySwitch);
+ } else {
+ TableState_.Reset();
+ }
+ }
+
+protected:
+ TMkqlIOCache SpecsCache_;
+ TTableState TableState_;
+ NYT::IReaderImplBase* Input_;
+ bool IsValid_;
+ bool AtStart_ = true;
+};
+
+class TYamrInputState: public TInputStateBase {
+public:
+ TYamrInputState(TMemoryUsageInfo* memInfo, const TMkqlIOSpecs& specs, NYT::IYaMRReaderImpl* input, TComputationContext& ctx,
+ const std::array<IComputationExternalNode*, 5>& argNodes)
+ : TInputStateBase(memInfo, specs, input, ctx, argNodes)
+ {
+ }
+
+protected:
+ NUdf::TUnboxedValue GetCurrent(size_t tableIndex) final {
+ return NYql::DecodeYamr(SpecsCache_, tableIndex, static_cast<NYT::IYaMRReaderImpl*>(Input_)->GetRow());
+ }
+};
+
+class TYtInputState: public TInputStateBase {
+public:
+ TYtInputState(TMemoryUsageInfo* memInfo, const TMkqlIOSpecs& specs, TMkqlReaderImpl* input, TComputationContext& ctx,
+ const std::array<IComputationExternalNode*, 5>& argNodes)
+ : TInputStateBase(memInfo, specs, input, ctx, argNodes)
+ {
+ }
+
+protected:
+ NUdf::TUnboxedValue GetCurrent(size_t tableIndex) final {
+ Y_UNUSED(tableIndex);
+ return static_cast<TMkqlReaderImpl*>(Input_)->GetRow();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYtInputWrapper : public TMutableComputationNode<TYtInputWrapper> {
+ typedef TMutableComputationNode<TYtInputWrapper> TBaseComputation;
+public:
+ TYtInputWrapper(TComputationMutables& mutables, const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input,
+ std::array<IComputationExternalNode*, 5>&& argNodes, TComputationNodePtrVector&& dependentNodes)
+ : TBaseComputation(mutables)
+ , Spec_(specs)
+ , Input_(input)
+ , ArgNodes_(std::move(argNodes))
+ , DependentNodes_(std::move(dependentNodes))
+ , StateIndex_(mutables.CurValueIndex++)
+ {}
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ auto& state = ctx.MutableValues[StateIndex_];
+ if (!state.HasValue()) {
+ MakeState(ctx, state);
+ }
+
+ return static_cast<TInputStateBase&>(*state.AsBoxed()).FetchRecord();
+ }
+private:
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ if (const auto mkqlReader = dynamic_cast<TMkqlReaderImpl*>(Input_)) {
+ state = ctx.HolderFactory.Create<TYtInputState>(Spec_, mkqlReader, ctx, ArgNodes_);
+ } else if (const auto yamrReader = dynamic_cast<NYT::IYaMRReaderImpl*>(Input_)) {
+ state = ctx.HolderFactory.Create<TYamrInputState>(Spec_, yamrReader, ctx, ArgNodes_);
+ }
+ }
+
+ void RegisterDependencies() const final {
+ std::for_each(ArgNodes_.cbegin(), ArgNodes_.cend(), std::bind(&TYtInputWrapper::Own, this, std::placeholders::_1));
+ std::for_each(DependentNodes_.cbegin(), DependentNodes_.cend(), std::bind(&TYtInputWrapper::DependsOn, this, std::placeholders::_1));
+ }
+
+ const TMkqlIOSpecs& Spec_;
+ NYT::IReaderImplBase*const Input_;
+ const std::array<IComputationExternalNode*, 5> ArgNodes_;
+ const TComputationNodePtrVector DependentNodes_;
+ const ui32 StateIndex_;
+};
+
+class TYtBaseInputWrapper {
+protected:
+ TYtBaseInputWrapper(const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input)
+ : Spec_(specs), Input_(input)
+ {}
+
+ void MakeState(TComputationContext& ctx, NUdf::TUnboxedValue& state) const {
+ std::array<IComputationExternalNode*, 5U> stub;
+ stub.fill(nullptr);
+ if (const auto mkqlReader = dynamic_cast<TMkqlReaderImpl*>(Input_)) {
+ state = ctx.HolderFactory.Create<TYtInputState>(Spec_, mkqlReader, ctx, stub);
+ } else if (const auto yamrReader = dynamic_cast<NYT::IYaMRReaderImpl*>(Input_)) {
+ state = ctx.HolderFactory.Create<TYamrInputState>(Spec_, yamrReader, ctx, stub);
+ }
+ }
+private:
+ const TMkqlIOSpecs& Spec_;
+ NYT::IReaderImplBase*const Input_;
+};
+
+class TYtFlowInputWrapper : public TStatefulFlowCodegeneratorNode<TYtFlowInputWrapper>, private TYtBaseInputWrapper {
+using TBaseComputation = TStatefulFlowCodegeneratorNode<TYtFlowInputWrapper>;
+public:
+ TYtFlowInputWrapper(TComputationMutables& mutables, EValueRepresentation kind, const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input)
+ : TBaseComputation(mutables, this, kind, EValueRepresentation::Boxed), TYtBaseInputWrapper(specs, input)
+ {}
+
+ NUdf::TUnboxedValuePod DoCalculate(NUdf::TUnboxedValue& state, TComputationContext& ctx) const {
+ if (!state.HasValue()) {
+ MakeState(ctx, state);
+ }
+
+ if (const auto value = static_cast<TInputStateBase&>(*state.AsBoxed()).FetchRecord())
+ return value;
+ else
+ return NUdf::TUnboxedValuePod::MakeFinish();
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ Value* DoGenerateGetValue(const TCodegenContext& ctx, Value* statePtr, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto valueType = Type::getInt128Ty(context);
+ const auto structPtrType = PointerType::getUnqual(StructType::get(context));
+
+ const auto stateType = StructType::get(context, {
+ structPtrType, // vtbl
+ Type::getInt32Ty(context), // ref
+ Type::getInt16Ty(context), // abi
+ Type::getInt16Ty(context), // reserved
+ structPtrType // meminfo
+ });
+
+ const auto statePtrType = PointerType::getUnqual(stateType);
+
+ const auto make = BasicBlock::Create(context, "make", ctx.Func);
+ const auto main = BasicBlock::Create(context, "main", ctx.Func);
+
+ BranchInst::Create(main, make, HasValue(statePtr, block), block);
+ block = make;
+
+ const auto self = CastInst::Create(Instruction::IntToPtr, ConstantInt::get(Type::getInt64Ty(context), uintptr_t(static_cast<const TYtBaseInputWrapper*>(this))), structPtrType, "self", block);
+ const auto makeFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtFlowInputWrapper::MakeState));
+ const auto makeType = FunctionType::get(Type::getVoidTy(context), {self->getType(), ctx.Ctx->getType(), statePtr->getType()}, false);
+ const auto makeFuncPtr = CastInst::Create(Instruction::IntToPtr, makeFunc, PointerType::getUnqual(makeType), "function", block);
+ CallInst::Create(makeType, makeFuncPtr, {self, ctx.Ctx, statePtr}, "", block);
+ BranchInst::Create(main, block);
+
+ block = main;
+
+ const auto state = new LoadInst(valueType, statePtr, "state", block);
+ const auto half = CastInst::Create(Instruction::Trunc, state, Type::getInt64Ty(context), "half", block);
+ const auto stateArg = CastInst::Create(Instruction::IntToPtr, half, statePtrType, "state_arg", block);
+
+ const auto func = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TInputStateBase::FetchRecord));
+ const auto funcType = FunctionType::get(valueType, { statePtrType }, false);
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, func, PointerType::getUnqual(funcType), "fetch_func", block);
+ const auto fetch = CallInst::Create(funcType, funcPtr, { stateArg }, "fetch", block);
+ const auto result = SelectInst::Create(IsExists(fetch, block), fetch, GetFinish(context), "result", block);
+
+ return result;
+ }
+#endif
+private:
+ void RegisterDependencies() const final {}
+};
+
+class TYtWideInputWrapper : public TPairStateWideFlowCodegeneratorNode<TYtWideInputWrapper>, private TYtBaseInputWrapper {
+using TBaseComputation = TPairStateWideFlowCodegeneratorNode<TYtWideInputWrapper>;
+public:
+ TYtWideInputWrapper(TComputationMutables& mutables, ui32 width, const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input)
+ : TBaseComputation(mutables, this, EValueRepresentation::Boxed, EValueRepresentation::Embedded)
+ , TYtBaseInputWrapper(specs, input), Width(width)
+ {}
+
+ EFetchResult DoCalculate(NUdf::TUnboxedValue& state, NUdf::TUnboxedValue& current, TComputationContext& ctx, NUdf::TUnboxedValue*const* output) const {
+ if (!state.HasValue()) {
+ MakeState(ctx, state);
+ }
+
+ if (const auto value = static_cast<TInputStateBase&>(*state.AsBoxed()).FetchRecord()) {
+ const auto elements = value.GetElements();
+ current = NUdf::TUnboxedValuePod(reinterpret_cast<ui64>(elements));
+ for (ui32 i = 0U; i < Width; ++i)
+ if (const auto out = *output++)
+ *out = elements[i];
+
+ return EFetchResult::One;
+ }
+
+ return EFetchResult::Finish;
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ ICodegeneratorInlineWideNode::TGenerateResult DoGenGetValues(const TCodegenContext& ctx, Value* statePtr, Value* currentPtr, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto valueType = Type::getInt128Ty(context);
+ const auto arrayType = ArrayType::get(valueType, Width);
+ const auto pointerType = PointerType::getUnqual(arrayType);
+ const auto structPtrType = PointerType::getUnqual(StructType::get(context));
+ const auto statusType = Type::getInt32Ty(context);
+
+ const auto stateType = StructType::get(context, {
+ structPtrType, // vtbl
+ Type::getInt32Ty(context), // ref
+ Type::getInt16Ty(context), // abi
+ Type::getInt16Ty(context), // reserved
+ structPtrType // meminfo
+ });
+
+ const auto statePtrType = PointerType::getUnqual(stateType);
+
+ const auto placeholder = GetElementPtrInst::CreateInBounds(valueType, ctx.GetMutables(), {ConstantInt::get(statusType, static_cast<const IComputationNode*>(this)->GetIndex() + 1U)}, "placeholder", &ctx.Func->getEntryBlock().back());
+
+ const auto make = BasicBlock::Create(context, "make", ctx.Func);
+ const auto main = BasicBlock::Create(context, "main", ctx.Func);
+ const auto good = BasicBlock::Create(context, "good", ctx.Func);
+ const auto done = BasicBlock::Create(context, "done", ctx.Func);
+
+ BranchInst::Create(main, make, HasValue(statePtr, block), block);
+ block = make;
+
+ const auto self = CastInst::Create(Instruction::IntToPtr, ConstantInt::get(Type::getInt64Ty(context), uintptr_t(static_cast<const TYtBaseInputWrapper*>(this))), structPtrType, "self", block);
+ const auto makeFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtWideInputWrapper::MakeState));
+ const auto makeType = FunctionType::get(Type::getVoidTy(context), {self->getType(), ctx.Ctx->getType(), statePtr->getType()}, false);
+ const auto makeFuncPtr = CastInst::Create(Instruction::IntToPtr, makeFunc, PointerType::getUnqual(makeType), "function", block);
+ CallInst::Create(makeType, makeFuncPtr, {self, ctx.Ctx, statePtr}, "", block);
+ BranchInst::Create(main, block);
+
+ block = main;
+
+ const auto state = new LoadInst(valueType, statePtr, "state", block);
+ const auto half = CastInst::Create(Instruction::Trunc, state, Type::getInt64Ty(context), "half", block);
+ const auto stateArg = CastInst::Create(Instruction::IntToPtr, half, statePtrType, "state_arg", block);
+
+ const auto func = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TInputStateBase::FetchRecord));
+ const auto funcType = FunctionType::get(valueType, { statePtrType }, false);
+ const auto funcPtr = CastInst::Create(Instruction::IntToPtr, func, PointerType::getUnqual(funcType), "fetch_func", block);
+ const auto fetch = CallInst::Create(funcType, funcPtr, { stateArg }, "fetch", block);
+
+ const auto result = PHINode::Create(statusType, 2U, "result", done);
+
+ result->addIncoming(ConstantInt::get(statusType, static_cast<i32>(EFetchResult::Finish)), block);
+
+ BranchInst::Create(good, done, IsExists(fetch, block), block);
+
+ block = good;
+
+ const auto elements = CallBoxedValueVirtualMethod<NUdf::TBoxedValueAccessor::EMethod::GetElements>(pointerType, fetch, ctx.Codegen, block);
+ const auto integer = CastInst::Create(Instruction::PtrToInt, elements, Type::getInt64Ty(context), "integer", block);
+ const auto stored = SetterFor<ui64>(integer, context, block);
+ new StoreInst(stored, currentPtr, block);
+
+ result->addIncoming(ConstantInt::get(statusType, static_cast<i32>(EFetchResult::One)), block);
+
+ BranchInst::Create(done, block);
+
+ block = done;
+
+ TGettersList getters;
+ getters.reserve(Width);
+ for (ui32 i = 0U; i < Width; ++i) {
+ getters.emplace_back([i, placeholder, pointerType, valueType, arrayType](const TCodegenContext& ctx, BasicBlock*& block) {
+ auto& context = ctx.Codegen->GetContext();
+ const auto current = new LoadInst(valueType, placeholder, (TString("current_") += ToString(i)).c_str(), block);
+ const auto integer = GetterFor<ui64>(current, context, block);
+ const auto pointer = CastInst::Create(Instruction::IntToPtr, integer, pointerType, (TString("pointer_") += ToString(i)).c_str(), block);
+ const auto indexType = Type::getInt32Ty(context);
+ const auto ptr = GetElementPtrInst::CreateInBounds(arrayType, pointer, {ConstantInt::get(indexType, 0), ConstantInt::get(indexType, i)}, (TString("ptr_") += ToString(i)).c_str(), block);
+ const auto load = new LoadInst(valueType, ptr, (TString("load_") += ToString(i)).c_str(), block);
+ return load;
+ });
+ }
+
+ return {result, std::move(getters)};
+ }
+#endif
+private:
+ void RegisterDependencies() const final {}
+ const ui32 Width;
+};
+
+IComputationNode* WrapYtInput(TCallable& callable, const TComputationNodeFactoryContext& ctx, const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input)
+{
+ if (!callable.GetInputsCount()) {
+ if (const auto type = AS_TYPE(TFlowType, callable.GetType()->GetReturnType())->GetItemType(); type->IsTuple())
+ return new TYtWideInputWrapper(ctx.Mutables, AS_TYPE(TTupleType, type)->GetElementsCount(), specs, input);
+ else if (type->IsMulti())
+ return new TYtWideInputWrapper(ctx.Mutables, AS_TYPE(TMultiType, type)->GetElementsCount(), specs, input);
+ else if (type->IsStruct())
+ return new TYtFlowInputWrapper(ctx.Mutables, GetValueRepresentation(type), specs, input);
+
+ THROW yexception() << "Expected tuple or struct as flow item type.";
+ }
+
+ YQL_ENSURE(callable.GetInputsCount() >= 5, "Expected at least 5 args");
+
+ std::array<IComputationExternalNode*, 5> argNodes;
+ for (size_t i = 0; i < argNodes.size(); ++i) {
+ argNodes[i] = LocateExternalNode(ctx.NodeLocator, callable, i, false);
+ }
+
+ TComputationNodePtrVector dependentNodes(callable.GetInputsCount() - argNodes.size());
+ for (ui32 i = argNodes.size(); i < callable.GetInputsCount(); ++i) {
+ dependentNodes[i - argNodes.size()] = LocateNode(ctx.NodeLocator, callable, i);
+ }
+
+ return new TYtInputWrapper(ctx.Mutables, specs, input, std::move(argNodes), std::move(dependentNodes));
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.h
new file mode 100644
index 0000000000..31c765f2e5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::IComputationNode* WrapYtInput(NKikimr::NMiniKQL::TCallable& callable, const NKikimr::NMiniKQL::TComputationNodeFactoryContext& ctx,
+ const TMkqlIOSpecs& specs, NYT::IReaderImplBase* input);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
new file mode 100644
index 0000000000..df17616763
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.cpp
@@ -0,0 +1,37 @@
+#include "yql_mkql_input_stream.h"
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+
+TInputStreamValue::TInputStreamValue(TMemoryUsageInfo* memInfo, IInputState* state)
+ : TComputationValue<TInputStreamValue>(memInfo)
+ , State_(state)
+{
+}
+
+NUdf::IBoxedValuePtr TInputStreamValue::ToIndexDictImpl(const NUdf::IValueBuilder& builder) const {
+ Y_UNUSED(builder);
+ YQL_ENSURE(false, "Single-pass iterator cannot be used for index dict");
+ return {};
+}
+
+NUdf::EFetchStatus TInputStreamValue::Fetch(NUdf::TUnboxedValue& result) {
+ if (!AtStart_) {
+ State_->Next();
+ }
+ AtStart_ = false;
+
+ if (!State_->IsValid()) {
+ return NUdf::EFetchStatus::Finish;
+ }
+
+ result = State_->GetCurrent();
+ return NUdf::EFetchStatus::Ok;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.h
new file mode 100644
index 0000000000..285bbc56b9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input_stream.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_impl.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <util/generic/ptr.h>
+
+namespace NYql {
+
+struct IInputState {
+ virtual ~IInputState() = default;
+
+ virtual bool IsValid() const = 0;
+ virtual NUdf::TUnboxedValue GetCurrent() = 0;
+ virtual void Next() = 0;
+};
+
+class TInputStreamValue
+ : public NKikimr::NMiniKQL::TComputationValue<TInputStreamValue>
+{
+public:
+ TInputStreamValue(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo, IInputState* state);
+
+private:
+ virtual NUdf::IBoxedValuePtr ToIndexDictImpl(const NUdf::IValueBuilder& builder) const override;
+ virtual NUdf::EFetchStatus Fetch(NUdf::TUnboxedValue& result) override;
+
+ bool AtStart_ = true;
+ IInputState* State_;
+};
+
+class THoldingInputStreamValue : private THolder<IInputState>, public TInputStreamValue {
+public:
+ inline THoldingInputStreamValue(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo, IInputState* state)
+ : THolder<IInputState>(state)
+ , TInputStreamValue(memInfo, this->Get())
+ {
+ }
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
new file mode 100644
index 0000000000..d09f985648
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.cpp
@@ -0,0 +1,226 @@
+#include "yql_mkql_output.h"
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_codegen.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+
+namespace NYql {
+
+namespace {
+
+using namespace NKikimr::NMiniKQL;
+
+class TYtOutputWrapper : public TDecoratorCodegeneratorNode<TYtOutputWrapper> {
+ using TBaseComputation = TDecoratorCodegeneratorNode<TYtOutputWrapper>;
+public:
+ TYtOutputWrapper(IComputationNode* item, TMkqlWriterImpl& writer)
+ : TBaseComputation(item, EValueRepresentation::Embedded), Writer(writer)
+ {}
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext&, const NUdf::TUnboxedValuePod& value) const {
+ AddRowImpl(value);
+ return NUdf::TUnboxedValuePod::Void();
+ }
+
+#ifndef MKQL_DISABLE_CODEGEN
+ Value* DoGenerateGetValue(const TCodegenContext& ctx, Value* item, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+ if (true /*|| TODO: !Writer.GenAddRow(item, ctx, block)*/) {
+ const auto addFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtOutputWrapper::AddRowImpl));
+ const auto selfArg = ConstantInt::get(Type::getInt64Ty(context), ui64(this));
+ const auto arg = WrapArgumentForWindows(item, ctx, block);
+ const auto addType = FunctionType::get(Type::getVoidTy(context), {selfArg->getType(), arg->getType()}, false);
+ const auto addPtr = CastInst::Create(Instruction::IntToPtr, addFunc, PointerType::getUnqual(addType), "write", block);
+ CallInst::Create(addType, addPtr, {selfArg, arg}, "", block);
+ }
+ if (Node->IsTemporaryValue())
+ ValueCleanup(Node->GetRepresentation(), item, ctx, block);
+ return GetFalse(context);
+ }
+#endif
+private:
+ void AddRowImpl(NUdf::TUnboxedValuePod row) const {
+ Writer.AddRow(row);
+ }
+
+ TMkqlWriterImpl& Writer;
+};
+
+class TYtFlowOutputWrapper : public TStatelessFlowCodegeneratorNode<TYtFlowOutputWrapper> {
+using TBaseComputation = TStatelessFlowCodegeneratorNode<TYtFlowOutputWrapper>;
+public:
+ TYtFlowOutputWrapper(IComputationNode* flow, TMkqlWriterImpl& writer)
+ : TBaseComputation(flow, EValueRepresentation::Embedded), Flow(flow), Writer(writer)
+ {}
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ if (const auto value = Flow->GetValue(ctx); value.IsSpecial())
+ return value;
+ else
+ AddRowImpl(value);
+ return NUdf::TUnboxedValuePod::Void();
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ Value* DoGenerateGetValue(const TCodegenContext& ctx, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto item = GetNodeValue(Flow, ctx, block);
+
+ const auto work = BasicBlock::Create(context, "work", ctx.Func);
+ const auto pass = BasicBlock::Create(context, "pass", ctx.Func);
+
+ const auto result = PHINode::Create(item->getType(), 2U, "result", pass);
+ result->addIncoming(item, block);
+
+ BranchInst::Create(pass, work, IsSpecial(item, block), block);
+
+ block = work;
+
+ const auto addFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtFlowOutputWrapper::AddRowImpl));
+ const auto selfArg = ConstantInt::get(Type::getInt64Ty(context), ui64(this));
+ const auto arg = WrapArgumentForWindows(item, ctx, block);
+ const auto addType = FunctionType::get(Type::getVoidTy(context), {selfArg->getType(), arg->getType()}, false);
+ const auto addPtr = CastInst::Create(Instruction::IntToPtr, addFunc, PointerType::getUnqual(addType), "write", block);
+ CallInst::Create(addType, addPtr, {selfArg, arg}, "", block);
+
+ ValueCleanup(Flow->GetRepresentation(), item, ctx, block);
+
+ result->addIncoming(ConstantInt::get(item->getType(), 0), block);
+
+ BranchInst::Create(pass, block);
+
+ block = pass;
+ return result;
+ }
+#endif
+private:
+ void RegisterDependencies() const final {
+ FlowDependsOn(Flow);
+ }
+
+ void AddRowImpl(NUdf::TUnboxedValuePod row) const {
+ Writer.AddRow(row);
+ }
+
+ static std::vector<NUdf::TUnboxedValue*> GetPointers(std::vector<NUdf::TUnboxedValue>& array) {
+ std::vector<NUdf::TUnboxedValue*> pointers;
+ pointers.reserve(array.size());
+ std::transform(array.begin(), array.end(), std::back_inserter(pointers), [](NUdf::TUnboxedValue& v) { return std::addressof(v); });
+ return pointers;
+ }
+
+ IComputationNode *const Flow;
+ const std::vector<EValueRepresentation> Representations;
+
+ TMkqlWriterImpl& Writer;
+
+ std::vector<NUdf::TUnboxedValue> Values;
+ const std::vector<NUdf::TUnboxedValue*> Fields;
+};
+
+class TYtWideOutputWrapper : public TStatelessWideFlowCodegeneratorNode<TYtWideOutputWrapper> {
+using TBaseComputation = TStatelessWideFlowCodegeneratorNode<TYtWideOutputWrapper>;
+public:
+ TYtWideOutputWrapper(IComputationWideFlowNode* flow, TMkqlWriterImpl& writer, std::vector<EValueRepresentation>&& representations)
+ : TBaseComputation(flow), Flow(flow), Representations(std::move(representations)), Writer(writer), Values(Representations.size()), Fields(GetPointers(Values))
+ {}
+
+ EFetchResult DoCalculate(TComputationContext& ctx, NUdf::TUnboxedValue*const*) const {
+ if (const auto result = Flow->FetchValues(ctx, Fields.data()); EFetchResult::One != result)
+ return result;
+
+ AddRowImpl(static_cast<const NUdf::TUnboxedValuePod*>(Values.data()));
+
+ return EFetchResult::One;
+ }
+#ifndef MKQL_DISABLE_CODEGEN
+ TGenerateResult DoGenGetValues(const TCodegenContext& ctx, BasicBlock*& block) const {
+ auto& context = ctx.Codegen->GetContext();
+
+ const auto valueType = Type::getInt128Ty(context);
+ const auto indexType = Type::getInt32Ty(context);
+ const auto arrayType = ArrayType::get(valueType, Representations.size());
+
+ const auto values = new AllocaInst(arrayType, 0U, "values", &ctx.Func->getEntryBlock().back());
+
+ const auto result = GetNodeValues(Flow, ctx, block);
+
+ const auto good = CmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_SGT, result.first, ConstantInt::get(result.first->getType(), 0), "good", block);
+
+ const auto work = BasicBlock::Create(context, "work", ctx.Func);
+ const auto pass = BasicBlock::Create(context, "pass", ctx.Func);
+
+ BranchInst::Create(work, pass, good, block);
+
+ block = work;
+
+ TSmallVec<Value*> fields;
+ fields.reserve(Representations.size());
+ for (ui32 i = 0U; i < Representations.size(); ++i) {
+ const auto pointer = GetElementPtrInst::CreateInBounds(arrayType, values, {ConstantInt::get(indexType, 0), ConstantInt::get(indexType, i)}, (TString("ptr_") += ToString(i)).c_str(), block);
+ fields.emplace_back(result.second[i](ctx, block));
+ new StoreInst(fields.back(), pointer, block);
+ }
+
+ const auto addFunc = ConstantInt::get(Type::getInt64Ty(context), GetMethodPtr(&TYtWideOutputWrapper::AddRowImpl));
+ const auto selfArg = ConstantInt::get(Type::getInt64Ty(context), ui64(this));
+ const auto addType = FunctionType::get(Type::getVoidTy(context), {selfArg->getType(), values->getType()}, false);
+ const auto addPtr = CastInst::Create(Instruction::IntToPtr, addFunc, PointerType::getUnqual(addType), "write", block);
+ CallInst::Create(addType, addPtr, {selfArg, values}, "", block);
+
+ for (ui32 i = 0U; i < Representations.size(); ++i) {
+ ValueCleanup(Representations[i], fields[i], ctx, block);
+ }
+
+ BranchInst::Create(pass, block);
+
+ block = pass;
+ return {result.first, {}};
+
+ }
+#endif
+private:
+ void RegisterDependencies() const final {
+ FlowDependsOn(Flow);
+ }
+
+ void AddRowImpl(const NUdf::TUnboxedValuePod* row) const {
+ Writer.AddFlatRow(row);
+ }
+
+ static std::vector<NUdf::TUnboxedValue*> GetPointers(std::vector<NUdf::TUnboxedValue>& array) {
+ std::vector<NUdf::TUnboxedValue*> pointers;
+ pointers.reserve(array.size());
+ std::transform(array.begin(), array.end(), std::back_inserter(pointers), [](NUdf::TUnboxedValue& v) { return std::addressof(v); });
+ return pointers;
+ }
+
+ IComputationWideFlowNode *const Flow;
+ const std::vector<EValueRepresentation> Representations;
+
+ TMkqlWriterImpl& Writer;
+
+ std::vector<NUdf::TUnboxedValue> Values;
+ const std::vector<NUdf::TUnboxedValue*> Fields;
+};
+
+}
+
+IComputationNode* WrapYtOutput(TCallable& callable, const TComputationNodeFactoryContext& ctx, TMkqlWriterImpl& writer) {
+ YQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arg");
+ const auto item = LocateNode(ctx.NodeLocator, callable, 0);
+ if (const auto inputType = callable.GetInput(0).GetStaticType(); inputType->IsFlow()) {
+ if (const auto wide = dynamic_cast<IComputationWideFlowNode*>(item)) {
+ std::vector<EValueRepresentation> inputRepresentations;
+ auto wideComponents = GetWideComponents(AS_TYPE(TFlowType, inputType));
+ inputRepresentations.reserve(wideComponents.size());
+ for (ui32 i = 0U; i < wideComponents.size(); ++i)
+ inputRepresentations.emplace_back(GetValueRepresentation(wideComponents[i]));
+ return new TYtWideOutputWrapper(wide, writer, std::move(inputRepresentations));
+ }
+ return new TYtFlowOutputWrapper(item, writer);
+ }
+
+ return new TYtOutputWrapper(item, writer);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h
new file mode 100644
index 0000000000..83cafb14c3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::IComputationNode* WrapYtOutput(NKikimr::NMiniKQL::TCallable& callable,
+ const NKikimr::NMiniKQL::TComputationNodeFactoryContext& ctx, TMkqlWriterImpl& writer);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
new file mode 100644
index 0000000000..02e9c750c3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.cpp
@@ -0,0 +1,108 @@
+#include "yql_mkql_table.h"
+
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+TVector<NUdf::TUnboxedValue> MakeTablePaths(const TTypeEnvironment& env, const TVector<TString>& paths) {
+ TVector<NUdf::TUnboxedValue> tablePaths;
+ tablePaths.reserve(paths.size());
+ for (auto& path: paths) {
+ if (path) {
+ tablePaths.emplace_back(env.NewStringValue(path));
+ } else {
+ tablePaths.emplace_back();
+ }
+ }
+ return tablePaths;
+}
+
+TTableState::TTableState(const TTypeEnvironment& env, const TVector<TString>& paths, const TVector<ui64>& recordOffsets,
+ TComputationContext& ctx, const std::array<IComputationExternalNode*, 5>& nodes)
+ : TTableState(MakeTablePaths(env, paths), recordOffsets, ctx, nodes)
+{
+}
+
+TTableState::TTableState(const TVector<NUdf::TUnboxedValue>& paths, const TVector<ui64>& rowOffsets, TComputationContext& ctx,
+ const std::array<IComputationExternalNode*, 5>& nodes)
+{
+ auto tableIndexNode = nodes[0];
+ if (tableIndexNode) {
+ UpdateTableIndex = [&ctx, tableIndexNode] (bool valid, ui32 tableIndex) {
+ tableIndexNode->SetValue(ctx, valid ? NUdf::TUnboxedValuePod(tableIndex) : NUdf::TUnboxedValue::Zero());
+ };
+ } else {
+ UpdateTableIndex = [] (bool, ui32) {};
+ }
+
+ auto tablePathNode = nodes[1];
+ if (tablePathNode && paths) {
+ UpdateTablePath = [&ctx, tablePathNode, paths] (bool valid, ui32 tableIndex) {
+ if (valid && paths.at(tableIndex)) {
+ tablePathNode->SetValue(ctx, NUdf::TUnboxedValue(paths.at(tableIndex)));
+ } else {
+ tablePathNode->SetValue(ctx, NUdf::TUnboxedValue::Zero());
+ }
+ };
+ } else {
+ UpdateTablePath = [] (bool, ui32) {};
+ }
+
+ auto tableRecordNode = nodes[2];
+ if (tableRecordNode && paths) {
+ UpdateTableRecord = [&ctx, tableRecordNode, paths] (bool valid, ui32 tableIndex, ui64 rowNumber) {
+ if (valid && paths.at(tableIndex)) {
+ tableRecordNode->SetValue(ctx, NUdf::TUnboxedValuePod(rowNumber));
+ } else {
+ tableRecordNode->SetValue(ctx, NUdf::TUnboxedValue::Zero());
+ }
+ };
+ } else {
+ UpdateTableRecord = [] (bool, ui32, ui64) {};
+ }
+
+ auto isKeySwitchNode = nodes[3];
+ if (isKeySwitchNode) {
+ UpdateIsKeySwitch = [&ctx, isKeySwitchNode] (bool keySwitch) {
+// TODO:correct set key switch (as hidden column, as example).
+// isKeySwitchNode->SetValue(ctx, NUdf::TUnboxedValuePod(keySwitch));
+ ctx.MutableValues[isKeySwitchNode->GetIndex()] = NUdf::TUnboxedValuePod(keySwitch);
+ };
+ } else {
+ UpdateIsKeySwitch = [] (bool) {};
+ }
+
+ auto rowNumberNode = nodes[4];
+ if (rowNumberNode && rowOffsets) {
+ UpdateRowNumber = [&ctx, rowNumberNode, rowOffsets] (bool valid, ui32 tableIndex, ui64 rowNumber) {
+ if (valid) {
+ rowNumberNode->SetValue(ctx, NUdf::TUnboxedValuePod(rowOffsets.at(tableIndex) + rowNumber));
+ } else {
+ rowNumberNode->SetValue(ctx, NUdf::TUnboxedValue::Zero());
+ }
+ };
+ } else {
+ UpdateRowNumber = [] (bool, ui32, ui64) {};
+ }
+}
+
+void TTableState::Reset() {
+ UpdateTableIndex(false, 0);
+ UpdateTablePath(false, 0);
+ UpdateTableRecord(false, 0, 0);
+ UpdateIsKeySwitch(false);
+ UpdateRowNumber(false, 0, 0);
+}
+
+void TTableState::Update(ui32 tableIndex, ui64 tableRecord, bool keySwitch) {
+ UpdateTableIndex(true, tableIndex);
+ UpdateTablePath(true, tableIndex);
+ UpdateTableRecord(true, tableIndex, tableRecord);
+ UpdateIsKeySwitch(keySwitch);
+ UpdateRowNumber(true, tableIndex, tableRecord);
+}
+
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h
new file mode 100644
index 0000000000..80bc532da9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+#include <functional>
+#include <array>
+
+namespace NYql {
+
+TVector<NUdf::TUnboxedValue> MakeTablePaths(const NKikimr::NMiniKQL::TTypeEnvironment& env, const TVector<TString>& paths);
+
+class TTableState {
+public:
+ TTableState(const NKikimr::NMiniKQL::TTypeEnvironment& env, const TVector<TString>& paths,
+ const TVector<ui64>& recordOffsets, NKikimr::NMiniKQL::TComputationContext& ctx,
+ const std::array<NKikimr::NMiniKQL::IComputationExternalNode*, 5>& nodes);
+
+ TTableState(const TVector<NUdf::TUnboxedValue>& paths, const TVector<ui64>& recordOffsets,
+ NKikimr::NMiniKQL::TComputationContext& ctx,
+ const std::array<NKikimr::NMiniKQL::IComputationExternalNode*, 5>& nodes);
+
+ TTableState(const TTableState&) = default;
+ TTableState(TTableState&&) = default;
+ ~TTableState() = default;
+
+ void Reset();
+ void Update(ui32 tableIndex, ui64 tableRecord, bool keySwitch = false);
+
+private:
+ std::function<void(bool, ui32)> UpdateTableIndex;
+ std::function<void(bool, ui32)> UpdateTablePath;
+ std::function<void(bool, ui32, ui64)> UpdateTableRecord;
+ std::function<void(bool)> UpdateIsKeySwitch;
+ std::function<void(bool, ui32, ui64)> UpdateRowNumber;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
new file mode 100644
index 0000000000..fcf781e8dc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.cpp
@@ -0,0 +1,107 @@
+#include "yql_mkql_ungrouping_list.h"
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/minikql/computation/mkql_custom_list.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+
+class TYtUngroupingListWrapper : public TMutableComputationNode<TYtUngroupingListWrapper> {
+ typedef TMutableComputationNode<TYtUngroupingListWrapper> TBaseComputation;
+public:
+ class TListValue : public TCustomListValue {
+ public:
+ class TIterator : public TComputationValue<TIterator> {
+ public:
+ TIterator(TMemoryUsageInfo* memInfo, TComputationContext& ctx, NUdf::TUnboxedValue&& iter, IComputationExternalNode* isKeySwitchNode)
+ : TComputationValue<TIterator>(memInfo)
+ , ListIterator(std::move(iter))
+ , IsKeySwitchNode(isKeySwitchNode)
+ , Ctx(ctx)
+ {}
+
+ private:
+ bool Next(NUdf::TUnboxedValue& value) final {
+ for (;;) {
+ if (IsKeySwitchNode) {
+ Ctx.MutableValues[IsKeySwitchNode->GetIndex()] = NUdf::TUnboxedValuePod(!SubListIterator);
+ }
+ if (!SubListIterator) {
+ NUdf::TUnboxedValue pair;
+ if (!ListIterator.Next(pair)) {
+ return false;
+ }
+ SubListIterator = pair.GetListIterator();
+ }
+ if (SubListIterator.Next(value)) {
+ return true;
+ } else {
+ SubListIterator.Clear();
+ }
+ }
+ }
+
+ const NUdf::TUnboxedValue ListIterator;
+ NUdf::TUnboxedValue SubListIterator;
+ IComputationExternalNode* const IsKeySwitchNode;
+ TComputationContext& Ctx;
+ };
+
+ TListValue(TMemoryUsageInfo* memInfo, NUdf::TUnboxedValue&& list, IComputationExternalNode* isKeySwitchNode, TComputationContext& ctx)
+ : TCustomListValue(memInfo), List(std::move(list)), IsKeySwitchNode(isKeySwitchNode), Ctx(ctx)
+ {}
+
+ private:
+ NUdf::TUnboxedValue GetListIterator() const override {
+ return Ctx.HolderFactory.Create<TIterator>(Ctx, List.GetListIterator(), IsKeySwitchNode);
+ }
+
+ private:
+ const NUdf::TUnboxedValue List;
+ IComputationExternalNode* const IsKeySwitchNode;
+ TComputationContext& Ctx;
+ };
+
+ TYtUngroupingListWrapper(TComputationMutables& mutables, IComputationNode* list, IComputationExternalNode* isKeySwitchNode)
+ : TBaseComputation(mutables)
+ , List_(list)
+ , IsKeySwitchNode_(isKeySwitchNode)
+ {
+ }
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ return ctx.HolderFactory.Create<TListValue>(List_->GetValue(ctx), IsKeySwitchNode_, ctx);
+ }
+
+private:
+ void RegisterDependencies() const final {
+ DependsOn(List_);
+ Own(IsKeySwitchNode_);
+ }
+
+ IComputationNode* const List_;
+ IComputationExternalNode* const IsKeySwitchNode_;
+};
+
+} // unnamed
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+IComputationNode* WrapYtUngroupingList(TCallable& callable, const TComputationNodeFactoryContext& ctx) {
+ YQL_ENSURE(callable.GetInputsCount() == 2, "Expected 2 args");
+ const auto listItemType = AS_TYPE(TListType, callable.GetInput(0))->GetItemType();
+ const auto subListItemType = AS_TYPE(TListType, listItemType)->GetItemType();
+ YQL_ENSURE(subListItemType->IsStruct() || subListItemType->IsVariant());
+
+ const auto list = LocateNode(ctx.NodeLocator, callable, 0);
+ const auto isKeySwitch = LocateExternalNode(ctx.NodeLocator, callable, 1, false);
+ return new TYtUngroupingListWrapper(ctx.Mutables, list, isKeySwitch);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.h b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.h
new file mode 100644
index 0000000000..ea965249c5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::IComputationNode* WrapYtUngroupingList(NKikimr::NMiniKQL::TCallable& callable, const NKikimr::NMiniKQL::TComputationNodeFactoryContext& ctx);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/expr_nodes/ya.make b/ydb/library/yql/providers/yt/expr_nodes/ya.make
new file mode 100644
index 0000000000..33cd3ff44b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/ya.make
@@ -0,0 +1,52 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_expr_nodes.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/providers/common/provider
+)
+
+SRCDIR(
+ ydb/library/yql/core/expr_nodes_gen
+)
+
+IF (EXPORT_CMAKE)
+ RUN_PROGRAM(
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/gen/__main__.py
+ yql_expr_nodes_gen.jnj
+ yql_yt_expr_nodes.json
+ yql_yt_expr_nodes.gen.h
+ yql_yt_expr_nodes.decl.inl.h
+ yql_yt_expr_nodes.defs.inl.h
+ IN yql_expr_nodes_gen.jnj
+ IN yql_yt_expr_nodes.json
+ OUT yql_yt_expr_nodes.gen.h
+ OUT yql_yt_expr_nodes.decl.inl.h
+ OUT yql_yt_expr_nodes.defs.inl.h
+ OUTPUT_INCLUDES
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h
+ ${ARCADIA_ROOT}/util/generic/hash_set.h
+ )
+ELSE()
+ RUN_PROGRAM(
+ ydb/library/yql/core/expr_nodes_gen/gen
+ yql_expr_nodes_gen.jnj
+ yql_yt_expr_nodes.json
+ yql_yt_expr_nodes.gen.h
+ yql_yt_expr_nodes.decl.inl.h
+ yql_yt_expr_nodes.defs.inl.h
+ IN yql_expr_nodes_gen.jnj
+ IN yql_yt_expr_nodes.json
+ OUT yql_yt_expr_nodes.gen.h
+ OUT yql_yt_expr_nodes.decl.inl.h
+ OUT yql_yt_expr_nodes.defs.inl.h
+ OUTPUT_INCLUDES
+ ${ARCADIA_ROOT}/ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h
+ ${ARCADIA_ROOT}/util/generic/hash_set.h
+ )
+ENDIF()
+
+END()
diff --git a/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
new file mode 100644
index 0000000000..d6a0eb9563
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.cpp
@@ -0,0 +1,7 @@
+#include "yql_yt_expr_nodes.h"
+
+namespace NYql {
+namespace NNodes {
+
+} // namespace NNodes
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h
new file mode 100644
index 0000000000..6f0748a230
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.gen.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+
+namespace NYql {
+namespace NNodes {
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.decl.inl.h>
+
+class TYtDSource: public NGenerated::TYtDSourceStub<TExprBase, TCallable, TCoAtom> {
+public:
+ explicit TYtDSource(const TExprNode* node)
+ : TYtDSourceStub(node)
+ {
+ }
+
+ explicit TYtDSource(const TExprNode::TPtr& node)
+ : TYtDSourceStub(node)
+ {
+ }
+
+ static bool Match(const TExprNode* node) {
+ if (!TYtDSourceStub::Match(node)) {
+ return false;
+ }
+
+ if (node->Child(0)->Content() != YtProviderName) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+class TYtDSink: public NGenerated::TYtDSinkStub<TExprBase, TCallable, TCoAtom> {
+public:
+ explicit TYtDSink(const TExprNode* node)
+ : TYtDSinkStub(node)
+ {
+ }
+
+ explicit TYtDSink(const TExprNode::TPtr& node)
+ : TYtDSinkStub(node)
+ {
+ }
+
+ static bool Match(const TExprNode* node) {
+ if (!TYtDSinkStub::Match(node)) {
+ return false;
+ }
+
+ if (node->Child(0)->Content() != YtProviderName) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.defs.inl.h>
+
+} // namespace NNodes
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
new file mode 100644
index 0000000000..3b7c224fd6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.json
@@ -0,0 +1,462 @@
+{
+ "NodeRootType": "TExprBase",
+ "NodeBuilderBase": "TNodeBuilderBase",
+ "ListBuilderBase": "TListBuilderBase",
+ "FreeArgCallableBase": "TFreeArgCallable",
+ "FreeArgBuilderBase": "TFreeArgCallableBuilderBase",
+ "Nodes": [
+ {
+ "Name": "TYtDSource",
+ "Base": "TCallable",
+ "Definition": "Custom",
+ "Match": {"Type": "Callable", "Name": "DataSource"},
+ "Children": [
+ {"Index": 0, "Name": "Category", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TYtDSink",
+ "Base": "TCallable",
+ "Definition": "Custom",
+ "Match": {"Type": "Callable", "Name": "DataSink"},
+ "Children": [
+ {"Index": 0, "Name": "Category", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TYtNamedSettingsBase",
+ "VarArgBase": "TCoNameValueTuple",
+ "Builder": {"Generate": "None"}
+ },
+ {
+ "Name": "TYqlRowSpec",
+ "Base": "TYtNamedSettingsBase",
+ "Match": {"Type": "Callable", "Name": "YqlRowSpec"}
+ },
+ {
+ "Name": "TEpoch",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "Epoch"},
+ "Children": [
+ {"Index": 0, "Name": "Value", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TYtMeta",
+ "Base": "TYtNamedSettingsBase",
+ "Match": {"Type": "Callable", "Name": "YtMeta"}
+ },
+ {
+ "Name": "TYtStat",
+ "Base": "TYtNamedSettingsBase",
+ "Match": {"Type": "Callable", "Name": "YtStat"}
+ },
+ {
+ "Name": "TYtTableBase",
+ "Base": "TCallable",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"},
+ "Children": [
+ {"Index": 0, "Name": "Name", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "RowSpec", "Type": "TExprBase"},
+ {"Index": 2, "Name": "Meta", "Type": "TExprBase"},
+ {"Index": 3, "Name": "Stat", "Type": "TExprBase"},
+ {"Index": 4, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtTable",
+ "Base": "TYtTableBase",
+ "Match": {"Type": "Callable", "Name": "YtTable"},
+ "Children": [
+ {"Index": 5, "Name": "Epoch", "Type": "TExprBase"},
+ {"Index": 6, "Name": "CommitEpoch", "Type": "TExprBase"},
+ {"Index": 7, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TYtOutTable",
+ "Base": "TYtTableBase",
+ "Match": {"Type": "Callable", "Name": "YtOutTable"}
+ },
+ {
+ "Name": "TYtOutSection",
+ "ListBase": "TYtOutTable"
+ },
+ {
+ "Name": "TYtRangeItemBase",
+ "Base": "TCallable",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"}
+ },
+ {
+ "Name": "TYtKeyExact",
+ "Base": "TYtRangeItemBase",
+ "Match": {"Type": "Callable", "Name": "YtKeyExact"},
+ "Children": [
+ {"Index": 0, "Name": "Key", "Type": "TExprList"}
+ ]
+ },
+ {
+ "Name": "TYtKeyRange",
+ "Base": "TYtRangeItemBase",
+ "Match": {"Type": "Callable", "Name": "YtKeyRange"},
+ "Children": [
+ {"Index": 0, "Name": "Lower", "Type": "TExprList"},
+ {"Index": 1, "Name": "Upper", "Type": "TExprList"},
+ {"Index": 2, "Name": "Flags", "Type": "TCoAtomList", "Optional": true}
+ ]
+ },
+ {
+ "Name": "TYtRow",
+ "Base": "TYtRangeItemBase",
+ "Match": {"Type": "Callable", "Name": "YtRow"},
+ "Children": [
+ {"Index": 0, "Name": "Index", "Type": "TCoUint64"}
+ ]
+ },
+ {
+ "Name": "TYtRowRange",
+ "Base": "TYtRangeItemBase",
+ "Match": {"Type": "Callable", "Name": "YtRowRange"},
+ "Children": [
+ {"Index": 0, "Name": "Lower", "Type": "TExprBase"},
+ {"Index": 1, "Name": "Upper", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TYtPath",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtPath"},
+ "Children": [
+ {"Index": 0, "Name": "Table", "Type": "TExprBase"},
+ {"Index": 1, "Name": "Columns", "Type": "TExprBase"},
+ {"Index": 2, "Name": "Ranges", "Type": "TExprBase"},
+ {"Index": 3, "Name": "Stat", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TYtPathList",
+ "ListBase": "TYtPath"
+ },
+ {
+ "Name": "TYtSection",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtSection"},
+ "Children": [
+ {"Index": 0, "Name": "Paths", "Type": "TYtPathList"},
+ {"Index": 1, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtSectionList",
+ "ListBase": "TYtSection"
+ },
+ {
+ "Name": "TYtRead",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "Read!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TYtDSource"}
+ ]
+ },
+ {
+ "Name": "TYtReadTable",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtReadTable!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TYtDSource"},
+ {"Index": 2, "Name": "Input", "Type": "TYtSectionList"}
+ ]
+ },
+ {
+ "Name": "TYtReadTableScheme",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtReadTableScheme!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TYtDSource"},
+ {"Index": 2, "Name": "Table", "Type": "TYtTable"},
+ {"Index": 3, "Name": "Type", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TYtWrite",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "Write!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSink", "Type": "TYtDSink"}
+ ]
+ },
+ {
+ "Name": "TYtTableContent",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtTableContent"},
+ "Children": [
+ {"Index": 0, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 1, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtLength",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtLength"},
+ "Children": [
+ {"Index": 0, "Name": "Input", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TYtConfigure",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "YtConfigure!"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSource", "Type": "TYtDSource"}
+ ]
+ },
+ {
+ "Name": "TYtTablePropBase",
+ "Base": "TCallable",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"},
+ "Children": [
+ {"Index": 0, "Name": "DependsOn", "Type": "TCoDependsOn"}
+ ]
+ },
+ {
+ "Name": "TYtTablePath",
+ "Base": "TYtTablePropBase",
+ "Match": {"Type": "Callable", "Name": "YtTablePath"}
+ },
+ {
+ "Name": "TYtTableRecord",
+ "Base": "TYtTablePropBase",
+ "Match": {"Type": "Callable", "Name": "YtTableRecord"}
+ },
+ {
+ "Name": "TYtTableIndex",
+ "Base": "TYtTablePropBase",
+ "Match": {"Type": "Callable", "Name": "YtTableIndex"}
+ },
+ {
+ "Name": "TYtIsKeySwitch",
+ "Base": "TYtTablePropBase",
+ "Match": {"Type": "Callable", "Name": "YtIsKeySwitch"}
+ },
+ {
+ "Name": "TYtRowNumber",
+ "Base": "TYtTablePropBase",
+ "Match": {"Type": "Callable", "Name": "YtRowNumber"}
+ },
+ {
+ "Name": "TYtTableName",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtTableName"},
+ "Children": [
+ {"Index": 0, "Name": "Input", "Type": "TExprBase"}
+ ]
+ },
+ {
+ "Name": "TYtOpBase",
+ "Base": "TCallable",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"},
+ "Children": [
+ {"Index": 0, "Name": "World", "Type": "TExprBase"},
+ {"Index": 1, "Name": "DataSink", "Type": "TYtDSink"}
+ ]
+ },
+ {
+ "Name": "TYtOutputOpBase",
+ "Base": "TYtOpBase",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"},
+ "Children": [
+ {"Index": 2, "Name": "Output", "Type": "TYtOutSection"}
+ ]
+ },
+ {
+ "Name": "TYtTransientOpBase",
+ "Base": "TYtOutputOpBase",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"},
+ "Children": [
+ {"Index": 3, "Name": "Input", "Type": "TYtSectionList"},
+ {"Index": 4, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtWithUserJobsOpBase",
+ "Base": "TYtTransientOpBase",
+ "Match": {"Type": "CallableBase"},
+ "Builder": {"Generate": "None"}
+ },
+ {
+ "Name": "TYtOutput",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtOutput!"},
+ "Children": [
+ {"Index": 0, "Name": "Operation", "Type": "TExprBase"},
+ {"Index": 1, "Name": "OutIndex", "Type": "TCoAtom"},
+ {"Index": 2, "Name": "Mode", "Type": "TCoAtom", "Optional": true}
+ ]
+ },
+ {
+ "Name": "TYtWriteTable",
+ "Base": "TYtOpBase",
+ "Match": {"Type": "Callable", "Name": "YtWriteTable!"},
+ "Children": [
+ {"Index": 2, "Name": "Table", "Type": "TYtTable"},
+ {"Index": 3, "Name": "Content", "Type": "TExprBase"},
+ {"Index": 4, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtStatOutTable",
+ "Base": "TFreeArgCallable",
+ "Match": {"Type": "Callable", "Name": "YtStatOutTable"},
+ "Children": [
+ {"Index": 0, "Name": "Name", "Type": "TCoAtom"},
+ {"Index": 1, "Name": "Scale", "Type": "TCoAtom"},
+ {"Index": 2, "Name": "Cluster", "Type": "TCoAtom"}
+ ]
+ },
+ {
+ "Name": "TYtStatOut",
+ "Base": "TYtOpBase",
+ "Match": {"Type": "Callable", "Name": "YtStatOut!"},
+ "Children": [
+ {"Index": 2, "Name": "Input", "Type": "TYtOutput"},
+ {"Index": 3, "Name": "Table", "Type": "TYtStatOutTable"},
+ {"Index": 4, "Name": "ReplaceMask", "Type": "TCoAtomList"},
+ {"Index": 5, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtFill",
+ "Base": "TYtOutputOpBase",
+ "Match": {"Type": "Callable", "Name": "YtFill!"},
+ "Children": [
+ {"Index": 3, "Name": "Content", "Type": "TCoLambda"},
+ {"Index": 4, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtTouch",
+ "Base": "TYtOutputOpBase",
+ "Match": {"Type": "Callable", "Name": "YtTouch!"}
+ },
+ {
+ "Name": "TYtDqProcessWrite",
+ "Base": "TYtOutputOpBase",
+ "Match": {"Type": "Callable", "Name": "YtDqProcessWrite!"},
+ "Children": [
+ {"Index": 3, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 4, "Name": "Flags", "Type": "TCoAtomList", "Optional": true}
+ ]
+ },
+ {
+ "Name": "TYtDropTable",
+ "Base": "TYtOpBase",
+ "Match": {"Type": "Callable", "Name": "YtDropTable!"},
+ "Children": [
+ {"Index": 2, "Name": "Table", "Type": "TYtTable"}
+ ]
+ },
+ {
+ "Name": "TYtOutputList",
+ "ListBase": "TYtOutput"
+ },
+ {
+ "Name": "TYtPublish",
+ "Base": "TYtOpBase",
+ "Match": {"Type": "Callable", "Name": "YtPublish!"},
+ "Children": [
+ {"Index": 2, "Name": "Input", "Type": "TYtOutputList"},
+ {"Index": 3, "Name": "Publish", "Type": "TYtTable"},
+ {"Index": 4, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtSort",
+ "Base": "TYtTransientOpBase",
+ "Match": {"Type": "Callable", "Name": "YtSort!"}
+ },
+ {
+ "Name": "TYtMap",
+ "Base": "TYtWithUserJobsOpBase",
+ "Match": {"Type": "Callable", "Name": "YtMap!"},
+ "Children": [
+ {"Index": 5, "Name": "Mapper", "Type": "TCoLambda"}
+ ]
+ },
+ {
+ "Name": "TYtReduce",
+ "Base": "TYtWithUserJobsOpBase",
+ "Match": {"Type": "Callable", "Name": "YtReduce!"},
+ "Children": [
+ {"Index": 5, "Name": "Reducer", "Type": "TCoLambda"}
+ ]
+ },
+ {
+ "Name": "TYtMapReduce",
+ "Base": "TYtWithUserJobsOpBase",
+ "Match": {"Type": "Callable", "Name": "YtMapReduce!"},
+ "Children": [
+ {"Index": 5, "Name": "Mapper", "Type": "TExprBase"},
+ {"Index": 6, "Name": "Reducer", "Type": "TCoLambda"}
+ ]
+ },
+ {
+ "Name": "TYtCopy",
+ "Base": "TYtTransientOpBase",
+ "Match": {"Type": "Callable", "Name": "YtCopy!"}
+ },
+ {
+ "Name": "TYtMerge",
+ "Base": "TYtTransientOpBase",
+ "Match": {"Type": "Callable", "Name": "YtMerge!"}
+ },
+ {
+ "Name": "TYtEquiJoin",
+ "Base": "TYtTransientOpBase",
+ "Match": {"Type": "Callable", "Name": "YtEquiJoin!"},
+ "Children": [
+ {"Index": 5, "Name": "Joins", "Type": "TExprBase"},
+ {"Index": 6, "Name": "JoinOptions", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtDqWrite",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtDqWrite"},
+ "Children": [
+ {"Index": 0, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 1, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtDqWideWrite",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtDqWideWrite"},
+ "Children": [
+ {"Index": 0, "Name": "Input", "Type": "TExprBase"},
+ {"Index": 1, "Name": "Settings", "Type": "TCoNameValueTupleList"}
+ ]
+ },
+ {
+ "Name": "TYtTryFirst",
+ "Base": "TCallable",
+ "Match": {"Type": "Callable", "Name": "YtTryFirst!"},
+ "Children": [
+ {"Index": 0, "Name": "First", "Type": "TYtOutputOpBase"},
+ {"Index": 1, "Name": "Second", "Type": "TYtOutputOpBase"}
+ ]
+ }
+ ]
+}
diff --git a/ydb/library/yql/providers/yt/gateway/file/ya.make b/ydb/library/yql/providers/yt/gateway/file/ya.make
new file mode 100644
index 0000000000..9421497a40
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/ya.make
@@ -0,0 +1,49 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_file_comp_nodes.cpp
+ yql_yt_file_mkql_compiler.cpp
+ yql_yt_file_services.cpp
+ yql_yt_file.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ library/cpp/yson/node
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/core/file_storage/proto
+ ydb/library/yql/core/file_storage/http_download
+ ydb/library/yql/minikql/comp_nodes/llvm
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/utils/threading
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core/type_ann
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/comp_nodes
+ ydb/library/yql/providers/common/gateway
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/schema/expr
+ ydb/library/yql/providers/common/schema/mkql
+ ydb/library/yql/providers/result/expr_nodes
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/comp_nodes
+ ydb/library/yql/providers/yt/expr_nodes
+ ydb/library/yql/providers/yt/gateway/lib
+ ydb/library/yql/providers/yt/lib/infer_schema
+ ydb/library/yql/providers/yt/lib/lambda_builder
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+ ydb/library/yql/providers/yt/lib/res_pull
+ ydb/library/yql/providers/yt/lib/schema
+ ydb/library/yql/providers/yt/lib/yson_helpers
+ ydb/library/yql/providers/yt/provider
+ ydb/library/yql/parser/pg_wrapper
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.cpp b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.cpp
new file mode 100644
index 0000000000..43ecc6537d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.cpp
@@ -0,0 +1,1429 @@
+#include "yql_yt_file.h"
+#include "yql_yt_file_mkql_compiler.h"
+#include "yql_yt_file_comp_nodes.h"
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+#include <ydb/library/yql/providers/common/mkql/yql_type_mkql.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/providers/common/schema/mkql/yql_mkql_schema.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h>
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h>
+#include <ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h>
+#include <ydb/library/yql/providers/yt/lib/schema/schema.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_helpers.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/query_cache.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_visitor.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node_builder.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_pack.h>
+#include <ydb/library/yql/utils/threading/async_queue.h>
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node_visitor.h>
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <library/cpp/yson/writer.h>
+#include <library/cpp/yson/parser.h>
+
+#include <util/stream/file.h>
+#include <util/stream/str.h>
+#include <util/system/fs.h>
+#include <util/system/fstat.h>
+#include <util/string/split.h>
+#include <util/string/builder.h>
+#include <util/folder/path.h>
+#include <util/generic/yexception.h>
+#include <util/generic/xrange.h>
+#include <util/generic/ptr.h>
+#include <util/random/random.h>
+
+#include <algorithm>
+#include <iterator>
+#include <cmath>
+
+
+namespace NYql {
+
+using namespace NCommon;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NNodes;
+using namespace NThreading;
+
+namespace NFile {
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TIntrusivePtr<IFunctionRegistry> MakeFunctionRegistry(
+ const IFunctionRegistry& functionRegistry, const TUserDataTable& files) {
+ auto cloned = functionRegistry.Clone();
+ for (auto& d : files) {
+ if (d.first.IsFile() && d.second.Usage.Test(EUserDataBlockUsage::Udf)) {
+ YQL_ENSURE(d.second.Type == EUserDataType::PATH);
+ cloned->LoadUdfs(d.second.Data, {}, 0, d.second.CustomUdfPrefix);
+ }
+ }
+ return cloned;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+struct TSession {
+ TSession(const IYtGateway::TOpenSessionOptions& options, bool keepTempTables)
+ : RandomProvider_(options.RandomProvider())
+ , TimeProvider_(options.TimeProvider())
+ , KeepTempTables_(keepTempTables)
+ , InflightTempTablesLimit_(Max<ui32>())
+ , ConfigInitDone_(false)
+ {
+ }
+
+ ~TSession() {
+ if (!KeepTempTables_) {
+ for (auto& x : TempTables_) {
+ for (auto& path: x.second) {
+ try {
+ NFs::Remove(path);
+ NFs::Remove(path + ".attr");
+ } catch (...) {
+ }
+ }
+ }
+ }
+ }
+
+ void DeleteAtFinalize(const TYtSettings::TConstPtr& config, const TString& cluster, const TString& table) {
+ if (!ConfigInitDone_) {
+ InflightTempTablesLimit_ = config->InflightTempTablesLimit.Get().GetOrElse(Max<ui32>());
+ if (GetReleaseTempDataMode(*config) == EReleaseTempDataMode::Never) {
+ KeepTempTables_ = true;
+ }
+ ConfigInitDone_ = true;
+ }
+
+ auto& tempTables = TempTables_[cluster];
+ tempTables.insert(table);
+ if (tempTables.size() > InflightTempTablesLimit_) {
+ ythrow yexception() << "Too many temporary tables registered - limit is " << InflightTempTablesLimit_;
+ }
+ }
+
+ void CancelDeleteAtFinalize(const TString& cluster, const TString& table) {
+ TempTables_[cluster].erase(table);
+ }
+
+ const TIntrusivePtr<IRandomProvider> RandomProvider_;
+ const TIntrusivePtr<ITimeProvider> TimeProvider_;
+ bool KeepTempTables_;
+ ui32 InflightTempTablesLimit_;
+ bool ConfigInitDone_;
+
+ THashMap<TString, THashSet<TString>> TempTables_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+struct TFileYtLambdaBuilder: public TLambdaBuilder {
+ TFileYtLambdaBuilder(TScopedAlloc& alloc, const TSession& /*session*/,
+ TIntrusivePtr<IFunctionRegistry> customFunctionRegistry,
+ const NUdf::ISecureParamsProvider* secureParamsProvider)
+ : TLambdaBuilder(customFunctionRegistry.Get(), alloc, nullptr, CreateDeterministicRandomProvider(1), CreateDeterministicTimeProvider(10000000),
+ nullptr, nullptr, secureParamsProvider)
+ , CustomFunctionRegistry_(customFunctionRegistry)
+ {}
+
+ TIntrusivePtr<IFunctionRegistry> CustomFunctionRegistry_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TFileTransformProvider {
+public:
+ TFileTransformProvider(const TYtFileServices::TPtr& services, const TUserDataTable& userDataBlocks)
+ : Services(services)
+ , UserDataBlocks(userDataBlocks)
+ , ExtraArgs(std::make_shared<THashMap<TString, TRuntimeNode>>())
+ {
+ }
+
+ TCallableVisitFunc operator()(TInternName name) {
+ if (name == "FilePath") {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ MKQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arguments");
+ const TString name(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ auto block = TUserDataStorage::FindUserDataBlock(UserDataBlocks, name);
+ MKQL_ENSURE(block, "File not found: " << name);
+ MKQL_ENSURE(block->Type == EUserDataType::PATH, "FilePath not supported for non-file data block, name: "
+ << name << ", block type: " << block->Type);
+ return TProgramBuilder(env, *Services->GetFunctionRegistry()).NewDataLiteral<NUdf::EDataSlot::String>(block->Data);
+ };
+ }
+
+ if (name == "FolderPath") {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ MKQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arguments");
+ const TString name(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ auto folderName = TUserDataStorage::MakeFolderName(name);
+ TMaybe<TString> folderPath;
+ for (const auto& x : UserDataBlocks) {
+ if (!x.first.Alias().StartsWith(folderName)) {
+ continue;
+ }
+
+ MKQL_ENSURE(x.second.Type == EUserDataType::PATH, "FilePath not supported for non-file data block, name: "
+ << x.first.Alias() << ", block type: " << x.second.Type);
+ auto newFolderPath = x.second.Data.substr(0, x.second.Data.size() - (x.first.Alias().size() - folderName.size()));
+ if (!folderPath) {
+ folderPath = newFolderPath;
+ } else {
+ MKQL_ENSURE(*folderPath == newFolderPath, "File " << x.second.Data << " is out of directory " << *folderPath);
+ }
+ }
+
+ return TProgramBuilder(env, *Services->GetFunctionRegistry()).NewDataLiteral<NUdf::EDataSlot::String>(*folderPath);
+ };
+ }
+
+ if (name == "FileContent") {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ MKQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arguments");
+ const TString name(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ auto block = TUserDataStorage::FindUserDataBlock(UserDataBlocks, name);
+ MKQL_ENSURE(block, "File not found: " << name);
+ const TProgramBuilder pgmBuilder(env, *Services->GetFunctionRegistry());
+ if (block->Type == EUserDataType::PATH) {
+ auto content = TFileInput(block->Data).ReadAll();
+ return pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(content);
+ }
+ else if (block->Type == EUserDataType::RAW_INLINE_DATA) {
+ return pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(block->Data);
+ }
+ else if (Services->GetFileStorage() && block->Type == EUserDataType::URL) {
+ auto link = Services->GetFileStorage()->PutUrl(block->Data, "");
+ auto content = TFileInput(link->GetPath()).ReadAll();
+ return pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(content);
+ } else {
+ MKQL_ENSURE(false, "Unsupported block type");
+ }
+ };
+ }
+
+ if (name == TYtTableIndex::CallableName()) {
+ return [this, name](NMiniKQL::TCallable&, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{name.Str()}, NUdf::EDataSlot::Uint32, env);
+ };
+ }
+ if (name == TYtTablePath::CallableName()) {
+ return [this, name](NMiniKQL::TCallable&, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{name.Str()}, NUdf::EDataSlot::String, env);
+ };
+ }
+ if (name == TYtTableRecord::CallableName()) {
+ return [this, name](NMiniKQL::TCallable&, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{name.Str()}, NUdf::EDataSlot::Uint64, env);
+ };
+ }
+ if (name == TYtIsKeySwitch::CallableName()) {
+ return [this, name](NMiniKQL::TCallable&, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{name.Str()}, NUdf::EDataSlot::Bool, env);
+ };
+ }
+ if (name == TYtRowNumber::CallableName()) {
+ return [this, name](NMiniKQL::TCallable&, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{name.Str()}, NUdf::EDataSlot::Uint64, env);
+ };
+ }
+
+ if (name == TYtTableContent::CallableName()) {
+ return [name](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env,
+ TStringBuilder() << TYtTableContent::CallableName() << "File",
+ callable.GetType()->GetReturnType(), false);
+ for (ui32 i: xrange(callable.GetInputsCount())) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+
+ if (name == "YtTableInput") {
+ return [this, name](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env, "YtTableInputFile", callable.GetType()->GetReturnType(), false);
+ for (ui32 i: xrange(callable.GetInputsCount())) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ callableBuilder.Add(GetExtraArg(TString(TYtTableIndex::CallableName()), NUdf::EDataSlot::Uint32, env));
+ callableBuilder.Add(GetExtraArg(TString(TYtTablePath::CallableName()), NUdf::EDataSlot::String, env));
+ callableBuilder.Add(GetExtraArg(TString(TYtTableRecord::CallableName()), NUdf::EDataSlot::Uint64, env));
+ callableBuilder.Add(GetExtraArg(TString(TYtRowNumber::CallableName()), NUdf::EDataSlot::Uint64, env));
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+
+ if (name == "YtTableInputNoCtx") {
+ return [name](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env, "YtTableInputNoCtxFile", callable.GetType()->GetReturnType(), false);
+ for (ui32 i: xrange(callable.GetInputsCount())) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+
+ if (name == "YtUngroupingList") {
+ return [this, name](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env, "YtUngroupingListFile", callable.GetType()->GetReturnType(), false);
+ for (ui32 i: xrange(callable.GetInputsCount())) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ callableBuilder.Add(GetExtraArg(TString(TYtIsKeySwitch::CallableName()), NUdf::EDataSlot::Bool, env));
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+
+ return TCallableVisitFunc();
+ }
+
+private:
+ TRuntimeNode GetExtraArg(const TString& name, NUdf::EDataSlot slot, const TTypeEnvironment& env) {
+ TRuntimeNode& node = (*ExtraArgs)[name];
+ if (!node) {
+ TCallableBuilder builder(env, "Arg", TDataType::Create(NUdf::GetDataTypeInfo(slot).TypeId, env), true);
+ node = TRuntimeNode(builder.Build(), false);
+ }
+ return node;
+ }
+
+private:
+ TYtFileServices::TPtr Services;
+ const TUserDataTable& UserDataBlocks;
+ std::shared_ptr<THashMap<TString, TRuntimeNode>> ExtraArgs;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYtFileGateway : public IYtGateway {
+public:
+ TYtFileGateway(const TYtFileServices::TPtr& services, bool* emulateOutputForMultirunPtr)
+ : Services_(services)
+ , MkqlCompiler_(MakeIntrusive<NCommon::TMkqlCommonCallableCompiler>())
+ , EmulateOutputForMultirunPtr(emulateOutputForMultirunPtr)
+ , FakeQueue_(TAsyncQueue::Make(1, "FakePool"))
+ {
+ RegisterYtMkqlCompilers(*MkqlCompiler_);
+ RegisterYtFileMkqlCompilers(*MkqlCompiler_);
+ }
+
+ void OpenSession(TOpenSessionOptions&& options) final {
+ if (!Sessions.emplace(options.SessionId(), TSession(options, Services_->GetKeepTempTables())).second) {
+ ythrow yexception() << "Session already exists: " << options.SessionId();
+ }
+ }
+
+ void CloseSession(TCloseSessionOptions&& options) final {
+ Sessions.erase(options.SessionId());
+ }
+
+ void CleanupSession(TCleanupSessionOptions&& options) final {
+ Y_UNUSED(options);
+ }
+
+ template<typename T>
+ TSession* GetSession(const T& options) {
+ const auto session = Sessions.FindPtr(options.SessionId());
+ YQL_ENSURE(session);
+ return session;
+ }
+
+ template<typename T>
+ const TSession* GetSession(const T& options) const {
+ const auto session = Sessions.FindPtr(options.SessionId());
+ YQL_ENSURE(session);
+ return session;
+ }
+
+ TFuture<TFinalizeResult> Finalize(TFinalizeOptions&& /*options*/) final {
+ TFinalizeResult res;
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+
+ TFuture<TCanonizePathsResult> CanonizePaths(TCanonizePathsOptions&& options) final {
+ TCanonizePathsResult res;
+ std::transform(
+ options.Paths().begin(), options.Paths().end(),
+ std::back_inserter(res.Data),
+ [] (const TCanonizeReq& req) {
+ return CanonizedPath(req.Path());
+ });
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+
+ bool ShouldEmulateOutputForMultirun(const TTableReq& req) {
+ return EmulateOutputForMultirunPtr && *EmulateOutputForMultirunPtr &&
+ req.Cluster() == "plato" && req.Table() == "Output";
+ }
+
+ TFuture<TTableInfoResult> GetTableInfo(TGetTableInfoOptions&& options) final {
+ TTableInfoResult res;
+ try {
+ for (const TTableReq& req: options.Tables()) {
+ auto path = Services_->GetTablePath(req.Cluster(), req.Table(), req.Anonymous(), true);
+ const bool exists = NFs::Exists(path) && !ShouldEmulateOutputForMultirun(req);
+
+ res.Data.emplace_back();
+
+ res.Data.back().WriteLock = HasModifyIntents(req.Intents());
+
+ TYtTableMetaInfo::TPtr metaData = new TYtTableMetaInfo;
+ metaData->DoesExist = exists;
+ if (exists) {
+ try {
+ LoadTableMetaInfo(req, path, *metaData);
+ } catch (const yexception& e) {
+ throw yexception() << "Error loading " << req.Cluster() << '.' << req.Table() << " table metadata: " << e.what();
+ }
+ }
+ res.Data.back().Meta = metaData;
+
+ if (exists) {
+ TYtTableStatInfo::TPtr statData = new TYtTableStatInfo;
+ statData->Id = req.Table();
+ if (metaData->SqlView.empty()) {
+ try {
+ LoadTableStatInfo(path, *statData);
+ } catch (const yexception& e) {
+ throw yexception() << "Error loading " << req.Cluster() << '.' << req.Table() << " table stat: " << e.what();
+ }
+
+ auto fullTableName = TString(YtProviderName).append('.').append(req.Cluster()).append('.').append(req.Table());
+ Services_->LockPath(path, fullTableName);
+ }
+ res.Data.back().Stat = statData;
+ }
+ }
+
+ res.SetSuccess();
+ } catch (const yexception& e) {
+ res = NCommon::ResultFromException<TTableInfoResult>(e);
+ }
+ return MakeFuture(res);
+ }
+
+ TFuture<TTableRangeResult> GetTableRange(TTableRangeOptions&& options) final {
+ auto pos = options.Pos();
+ try {
+ TSession* session = GetSession(options);
+ TSet<TString> uniqueTables;
+ if (options.Prefix().empty() && options.Suffix().empty()) {
+ for (auto& x : Services_->GetTablesMapping()) {
+ TVector<TString> parts;
+ Split(x.first, ".", parts);
+ if (parts.size() > 2 && parts[0] == YtProviderName) {
+ if (!parts[2].StartsWith(TStringBuf("Input"))) {
+ continue;
+ }
+ uniqueTables.insert(parts[2]);
+ }
+ }
+ }
+
+ TTableRangeResult res;
+ res.SetSuccess();
+
+ if (!uniqueTables.empty()) {
+ if (auto filter = options.Filter()) {
+ auto exprCtx = options.ExprCtx();
+ YQL_ENSURE(exprCtx);
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ auto secureParamsProvider = MakeSimpleSecureParamsProvider(options.SecureParams());
+ TFileYtLambdaBuilder builder(alloc, *session,
+ MakeFunctionRegistry(*Services_->GetFunctionRegistry(), options.UserDataBlocks()), secureParamsProvider.get());
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *Services_->GetFunctionRegistry());
+
+ TVector<TRuntimeNode> strings;
+ for (auto& x: uniqueTables) {
+ strings.push_back(pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(x));
+ }
+
+ auto inputNode = pgmBuilder.AsList(strings);
+ auto data = pgmBuilder.Filter(inputNode, [&](TRuntimeNode item) {
+ TMkqlBuildContext ctx(*MkqlCompiler_, pgmBuilder, *exprCtx, filter->UniqueId(), {{&filter->Head().Head(), item}});
+ return pgmBuilder.Coalesce(MkqlBuildExpr(filter->Tail(), ctx), pgmBuilder.NewDataLiteral(false));
+ });
+
+ data = builder.TransformAndOptimizeProgram(data, TFileTransformProvider(Services_, options.UserDataBlocks()));
+ TExploringNodeVisitor explorer;
+ auto nodeFactory = GetYtFileFullFactory(Services_);
+ auto compGraph = builder.BuildGraph(nodeFactory, options.UdfValidateMode(),
+ NUdf::EValidatePolicy::Exception, options.OptLLVM(), EGraphPerProcess::Multi, explorer, data);
+ compGraph->Prepare();
+ const TBindTerminator bind(compGraph->GetTerminator());
+ const auto& value = compGraph->GetValue();
+ const auto it = value.GetListIterator();
+ for (NUdf::TUnboxedValue current; it.Next(current);) {
+ res.Tables.push_back(TCanonizedPath{TString(current.AsStringRef()), Nothing(), {}});
+ }
+ }
+ else {
+ std::transform(
+ uniqueTables.begin(), uniqueTables.end(),
+ std::back_inserter(res.Tables),
+ [] (const TString& path) {
+ return TCanonizedPath{path, Nothing(), {}};
+ });
+ }
+ }
+
+ return MakeFuture(res);
+ } catch (const yexception& e) {
+ return MakeFuture(NCommon::ResultFromException<TTableRangeResult>(e, pos));
+ }
+ }
+
+ TFuture<TFolderResult> GetFolder(TFolderOptions&& options) final {
+ auto pos = options.Pos();
+ try {
+ TSet<TString> uniqueTables;
+ if (options.Prefix().empty()) {
+ for (auto& x : Services_->GetTablesMapping()) {
+ TVector<TString> parts;
+ Split(x.first, ".", parts);
+ if (parts.size() > 2 && parts[0] == YtProviderName) {
+ if (!parts[2].StartsWith(TStringBuf("Input"))) {
+ continue;
+ }
+ uniqueTables.insert(parts[2]);
+ }
+ }
+ }
+
+ TFolderResult res;
+ res.SetSuccess();
+
+ for (auto& table : uniqueTables) {
+ TFolderResult::TFolderItem item;
+ item.Path = table;
+ item.Type = "table";
+ auto allAttrs = LoadTableAttrs(Services_->GetTablePath(options.Cluster(), table, false, true));
+ auto attrs = NYT::TNode::CreateMap();
+ for (const auto& attrName : options.Attributes()) {
+ if (attrName && allAttrs.HasKey(attrName)) {
+ attrs[attrName] = allAttrs[attrName];
+ }
+ }
+
+ item.Attributes = NYT::NodeToYsonString(attrs);
+ res.Items.push_back(item);
+ }
+
+ return MakeFuture(res);
+ } catch (const yexception& e) {
+ return MakeFuture(NCommon::ResultFromException<TFolderResult>(e, pos));
+ }
+ }
+
+ TFuture<TResOrPullResult> ResOrPull(const TExprNode::TPtr& node, TExprContext& ctx, TResOrPullOptions&& options) final {
+ TResOrPullResult res;
+ auto nodePos = ctx.GetPosition(node->Pos());
+ try {
+ TSession* session = GetSession(options);
+ TVector<TString> columns(NCommon::GetResOrPullColumnHints(*node));
+ if (columns.empty()) {
+ columns = NCommon::GetStructFields(node->Child(0)->GetTypeAnn());
+ }
+
+ TStringStream out;
+ NYson::TYsonWriter writer(&out, NCommon::GetYsonFormat(options.FillSettings()), ::NYson::EYsonType::Node, false);
+ writer.OnBeginMap();
+ if (NCommon::HasResOrPullOption(*node, "type")) {
+ writer.OnKeyedItem("Type");
+ NCommon::WriteResOrPullType(writer, node->Child(0)->GetTypeAnn(), columns);
+ }
+
+ bool truncated = false;
+ if (TStringBuf("Result") == node->Content()) {
+ truncated = ExecuteResult(*session, writer, NNodes::TResult(node).Input(), ctx, std::move(options), columns);
+ } else if (TStringBuf("Pull") == node->Content()) {
+ truncated = ExecutePull(*session, writer, NNodes::TPull(node), ctx, std::move(options), columns);
+ } else {
+ ythrow yexception() << "Don't know how to execute " << node->Content();
+ }
+
+ if (truncated) {
+ writer.OnKeyedItem("Truncated");
+ writer.OnBooleanScalar(true);
+ }
+
+ writer.OnEndMap();
+ res.Data = out.Str();
+ res.SetSuccess();
+ } catch (const yexception& e) {
+ res = NCommon::ResultFromException<TResOrPullResult>(e, nodePos);
+ }
+
+ return MakeFuture(res);
+ }
+
+ TFuture<TRunResult> Run(const TExprNode::TPtr& node, TExprContext& ctx, TRunOptions&& options) final {
+ TRunResult res;
+ auto nodePos = ctx.GetPosition(node->Pos());
+ if (auto reduce = TMaybeNode<TYtReduce>(node)) {
+ auto maxDataSizePerJob = NYql::GetMaxJobSizeForFirstAsPrimary(reduce.Cast().Settings().Ref());
+ // YT wants max_data_size_per_job > 0
+ if (maxDataSizePerJob && *maxDataSizePerJob <= 1) {
+ TIssue rootIssue = YqlIssue(nodePos, TIssuesIds::YT_MAX_DATAWEIGHT_PER_JOB_EXCEEDED);
+ res.SetStatus(TIssuesIds::UNEXPECTED);
+ res.AddIssue(rootIssue);
+ return MakeFuture(res);
+ }
+ }
+
+ try {
+ TSession* session = GetSession(options);
+ if (TYtTouch::Match(node.Get())) {
+ res.OutTableStats = ExecuteTouch(options.Config(), *session, TYtTouch(node));
+ res.SetSuccess();
+ }
+ else if (TYtOutputOpBase::Match(node.Get())) {
+ res.OutTableStats = ExecuteOpWithOutput(*session, node, ctx, std::move(options));
+ res.SetSuccess();
+ }
+ else if (TYtDropTable::Match(node.Get())) {
+ ExecuteDrop(node);
+ res.SetSuccess();
+ }
+ else {
+ res.AddIssue(TIssue(nodePos, TStringBuilder() << "Unsupported function: " << node->Content()));
+ }
+ }
+ catch (const TNodeException& e) {
+ res.SetException(e, ctx.GetPosition(e.Pos()));
+ }
+ catch (const yexception& e) {
+ res.SetException(e, nodePos);
+ }
+ return MakeFuture(res);
+ }
+
+
+ TFuture<TRunResult> Prepare(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) const final {
+ TRunResult res;
+ auto nodePos = ctx.GetPosition(node->Pos());
+
+ try {
+ auto session = GetSession(options);
+ res.OutTableStats = ExecutePrepare(options, *session, TYtOutputOpBase(node));
+ res.SetSuccess();
+ }
+ catch (const TNodeException& e) {
+ res.SetException(e, ctx.GetPosition(e.Pos()));
+ }
+ catch (const yexception& e) {
+ res.SetException(e, nodePos);
+ }
+ return MakeFuture(res);
+ }
+
+ TFuture<TRunResult> GetTableStat(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) override {
+ TRunResult res;
+ auto nodePos = ctx.GetPosition(node->Pos());
+
+ try {
+ auto session = GetSession(options);
+ const TYtOutputOpBase op(node);
+ const auto cluster = op.DataSink().Cluster().StringValue();
+ Y_ENSURE(1U == op.Output().Size(), "Single output expected.");
+ const auto table = op.Output().Item(0);
+
+ TYtOutTableInfo tableInfo(table);
+ auto outTablePath = Services_->GetTablePath(cluster, tableInfo.Name, true);
+ TFsQueryCacheItem queryCacheItem(*options.Config(), cluster, Services_->GetTmpDir(), options.OperationHash(), outTablePath);
+
+ NYT::TNode outSpec = NYT::TNode::CreateList();
+ tableInfo.RowSpec->FillCodecNode(outSpec.Add()[YqlRowSpecAttribute]);
+ outSpec = NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, std::move(outSpec));
+
+ auto content = Services_->GetTableContent(tableInfo.Name);
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ TMemoryUsageInfo memInfo("Stat");
+ TTypeEnvironment env(alloc);
+ TProgramBuilder pgmBuilder(env, *Services_->GetFunctionRegistry());
+ THolderFactory holderFactory(alloc.Ref(), memInfo, Services_->GetFunctionRegistry());
+
+ NCommon::TCodecContext codecCtx(env, *Services_->GetFunctionRegistry(), &holderFactory);
+ TMkqlIOSpecs spec;
+ spec.Init(codecCtx, outSpec);
+
+ TStringStream out;
+ TMkqlWriterImpl writer(out, 0, 4_MB);
+ writer.SetSpecs(spec);
+
+ TStringStream err;
+ auto type = BuildType(*tableInfo.RowSpec->GetType(), pgmBuilder, err);
+ TValuePacker packer(true, type);
+ for (auto& c: content) {
+ auto val = packer.Unpack(c, holderFactory);
+ writer.AddRow(val);
+ }
+ writer.Finish();
+
+ WriteOutTable(options.Config(), *session, cluster, tableInfo, out.Str());
+ queryCacheItem.Store();
+
+ auto statInfo = MakeIntrusive<TYtTableStatInfo>();
+ LoadTableStatInfo(outTablePath, *statInfo);
+ statInfo->Id = tableInfo.Name;
+
+ res.OutTableStats.emplace_back(statInfo->Id, statInfo);
+ res.SetSuccess();
+ }
+ catch (const TNodeException& e) {
+ res.SetException(e, ctx.GetPosition(e.Pos()));
+ }
+ catch (const yexception& e) {
+ res.SetException(e, nodePos);
+ }
+ return MakeFuture(res);
+ }
+
+
+ TFuture<TCalcResult> Calc(const TExprNode::TListType& nodes, TExprContext& ctx, TCalcOptions&& options) final {
+ TCalcResult res;
+ Y_UNUSED(ctx);
+ // TODO: fixme
+ try {
+ TSession* session = GetSession(options);
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ auto secureParamsProvider = MakeSimpleSecureParamsProvider(options.SecureParams());
+ TFileYtLambdaBuilder builder(alloc, *session,
+ MakeFunctionRegistry(*Services_->GetFunctionRegistry(), options.UserDataBlocks()), secureParamsProvider.get());
+ auto nodeFactory = GetYtFileFullFactory(Services_);
+ for (auto& node: nodes) {
+ auto data = builder.BuildLambda(*MkqlCompiler_, node, ctx);
+ auto transform = TFileTransformProvider(Services_, options.UserDataBlocks());
+ data = builder.TransformAndOptimizeProgram(data, transform);
+ TExploringNodeVisitor explorer;
+ auto compGraph = builder.BuildGraph(nodeFactory, options.UdfValidateMode(),
+ NUdf::EValidatePolicy::Exception, options.OptLLVM(), EGraphPerProcess::Multi, explorer, data, {data.GetNode()});
+ const TBindTerminator bind(compGraph->GetTerminator());
+ compGraph->Prepare();
+ auto value = compGraph->GetValue();
+ res.Data.push_back(NCommon::ValueToNode(value, data.GetStaticType()));
+ }
+ res.SetSuccess();
+ } catch (const yexception& e) {
+ res = NCommon::ResultFromException<TCalcResult>(e);
+ }
+ return MakeFuture(res);
+ }
+
+ TFuture<TPublishResult> Publish(const TExprNode::TPtr& node, TExprContext& exprCtx, TPublishOptions&& options) final {
+ TPublishResult res;
+ try {
+ TSession* session = GetSession(options);
+
+ auto publish = TYtPublish(node);
+
+ auto mode = NYql::GetSetting(publish.Settings().Ref(), EYtSettingType::Mode);
+ bool append = mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Append;
+ auto cluster = TString{publish.DataSink().Cluster().Value()};
+
+ bool isAnonymous = NYql::HasSetting(publish.Publish().Settings().Ref(), EYtSettingType::Anonymous);
+ auto destFilePath = Services_->GetTablePath(cluster, publish.Publish().Name().Value(), isAnonymous, true);
+
+ append = append && NFs::Exists(destFilePath);
+
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TFileYtLambdaBuilder builder(alloc, *session,
+ MakeFunctionRegistry(*Services_->GetFunctionRegistry(), {}), nullptr);
+
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), builder.GetFunctionRegistry());
+ TMkqlBuildContext ctx(*MkqlCompiler_, pgmBuilder, exprCtx);
+
+ auto dstRowSpec = options.DestinationRowSpec();
+ NYT::TNode spec;
+ dstRowSpec->FillCodecNode(spec[YqlRowSpecAttribute]);
+
+ std::vector<std::pair<std::string_view, TType*>> members;
+ members.reserve(dstRowSpec->GetType()->GetItems().size());
+ for (auto& item : dstRowSpec->GetType()->GetItems()) {
+ members.emplace_back(item->GetName(), NCommon::BuildType(publish.Ref(), *item->GetItemType(), pgmBuilder));
+ }
+ for (size_t i: xrange(dstRowSpec->SortedBy.size())) {
+ if (!dstRowSpec->GetType()->FindItem(dstRowSpec->SortedBy[i])) {
+ members.emplace_back(dstRowSpec->SortedBy[i], NCommon::BuildType(publish.Ref(), *dstRowSpec->SortedByTypes[i], pgmBuilder));
+ }
+ }
+ auto srcType = pgmBuilder.NewStructType(members);
+
+ TVector<TRuntimeNode> inputs;
+ if (append) {
+ inputs.push_back(BuildRuntimeTableInput("YtTableInputNoCtx", srcType, cluster, publish.Publish().Name().Value(), NYT::NodeToYsonString(spec), isAnonymous, ctx));
+ }
+
+ for (auto out: publish.Input()) {
+ inputs.push_back(BuildTableContentCall("YtTableInputNoCtx", srcType, cluster, out.Ref(), Nothing(), ctx, false));
+ }
+
+ auto data = pgmBuilder.Extend(inputs);
+ if (inputs.size() > 1 && dstRowSpec->IsSorted()) {
+ data = SortListBy(data, dstRowSpec->GetForeignSort(), ctx);
+ }
+ data = BuildTableOutput(data, ctx);
+
+ auto transform = TFileTransformProvider(Services_, {});
+ data = builder.TransformAndOptimizeProgram(data, transform);
+
+ TExploringNodeVisitor explorer;
+ auto nodeFactory = GetYtFileFullFactory(Services_);
+ auto compGraph = builder.BuildGraph(nodeFactory, NUdf::EValidateMode::None,
+ NUdf::EValidatePolicy::Exception, options.OptLLVM(), EGraphPerProcess::Multi, explorer, data, {data.GetNode()});
+ const TBindTerminator bind(compGraph->GetTerminator());
+ compGraph->Prepare();
+
+ NYT::TNode outSpec = NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, NYT::TNode::CreateList().Add(spec));
+
+ TVector<TString> outTableContent = GetFileWriteResult(
+ builder.GetTypeEnvironment(),
+ builder.GetFunctionRegistry(),
+ compGraph->GetContext(),
+ compGraph->GetValue(),
+ outSpec);
+ YQL_ENSURE(1 == outTableContent.size());
+
+ {
+ TMemoryInput in(outTableContent.front());
+ TOFStream of(destFilePath);
+ TDoubleHighPrecisionYsonWriter writer(&of, ::NYson::EYsonType::ListFragment);
+ NYson::TYsonParser parser(&writer, &in, ::NYson::EYsonType::ListFragment);
+ parser.Parse();
+ }
+
+ {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ TString srcFilePath = Services_->GetTablePath(cluster, GetOutTable(publish.Input().Item(0)).Cast<TYtOutTable>().Name().Value(), true);
+ if (NFs::Exists(srcFilePath + ".attr")) {
+ TIFStream input(srcFilePath + ".attr");
+ attrs = NYT::NodeFromYsonStream(&input);
+ }
+
+ const auto nativeYtTypeCompatibility = options.Config()->NativeYtTypeCompatibility.Get(cluster).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = options.Config()->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+ dstRowSpec->FillAttrNode(attrs[YqlRowSpecAttribute], nativeYtTypeCompatibility, rowSpecCompactForm);
+ if (!append || !attrs.HasKey("schema")) {
+ attrs["schema"] = RowSpecToYTSchema(spec[YqlRowSpecAttribute], nativeYtTypeCompatibility).ToNode();
+ }
+ TOFStream ofAttr(destFilePath + ".attr");
+ ofAttr.Write(NYT::NodeToYsonString(attrs, NYson::EYsonFormat::Pretty));
+ }
+
+ if (isAnonymous) {
+ session->DeleteAtFinalize(options.Config(), cluster, destFilePath);
+ }
+
+ res.SetSuccess();
+ }
+ catch (const TNodeException& e) {
+ res.SetException(e, exprCtx.GetPosition(e.Pos()));
+ }
+ catch (const yexception& e) {
+ res.SetException(e, exprCtx.GetPosition(node->Pos()));
+ }
+ return MakeFuture(res);
+ }
+
+ TFuture<TCommitResult> Commit(TCommitOptions&& options) final {
+ Y_UNUSED(options);
+ TCommitResult res;
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+
+ TFuture<TDropTrackablesResult> DropTrackables(TDropTrackablesOptions&& options) final {
+ TDropTrackablesResult res;
+ try {
+ TSession* session = GetSession(options);
+ // check for overrides from command line
+ if (session->KeepTempTables_) {
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+
+ for (const auto& i : options.Pathes()) {
+
+ const TString& cluster = i.Cluster;
+ const TString& path = i.Path;
+
+ auto tmpPath = Services_->GetTablePath(cluster, path, true);
+
+ session->CancelDeleteAtFinalize(cluster, tmpPath);
+
+ NFs::Remove(tmpPath);
+ NFs::Remove(tmpPath + ".attr");
+ }
+ res.SetSuccess();
+ }
+ catch (const yexception& e) {
+ res.SetException(e);
+ }
+ return MakeFuture(res);
+ }
+
+ TFuture<TPathStatResult> PathStat(TPathStatOptions&& options) final {
+ bool onlyCached = false;
+ return MakeFuture(DoPathStat(std::move(options), onlyCached));
+ }
+
+ TPathStatResult TryPathStat(TPathStatOptions&& options) final {
+ bool onlyCached = true;
+ return DoPathStat(std::move(options), onlyCached);
+ }
+
+ bool TryParseYtUrl(const TString& url, TString* cluster, TString* path) const final {
+ Y_UNUSED(url);
+ Y_UNUSED(cluster);
+ Y_UNUSED(path);
+ return false;
+ }
+
+ TString GetClusterServer(const TString& cluster) const final {
+ return cluster;
+ }
+
+ NYT::TRichYPath GetRealTable(const TString& sessionId, const TString& cluster, const TString& table, ui32 epoch, const TString& tmpFolder) const final {
+ Y_UNUSED(sessionId);
+ Y_UNUSED(cluster);
+ Y_UNUSED(epoch);
+ Y_UNUSED(tmpFolder);
+ return NYT::TRichYPath().Path(table);
+ }
+
+ NYT::TRichYPath GetWriteTable(const TString& sessionId, const TString& cluster, const TString& table, const TString& tmpFolder) const final {
+ Y_UNUSED(sessionId);
+ Y_UNUSED(cluster);
+ auto realTableName = NYql::TransformPath(tmpFolder, table, true, "");
+ realTableName = NYT::AddPathPrefix(realTableName, NYT::TConfig::Get()->Prefix);
+ NYT::TRichYPath res{realTableName};
+ res.TransactionId(TGUID());
+ return res;
+ }
+
+ TFullResultTableResult PrepareFullResultTable(TFullResultTableOptions&& options) final {
+ try {
+ TString cluster = options.Cluster();
+ auto outTable = options.OutTable();
+ TSession* session = GetSession(options);
+
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ for (auto& a: options.OutTable().Meta->Attrs) {
+ attrs[a.first] = a.second;
+ }
+ const auto nativeYtTypeCompatibility = options.Config()->NativeYtTypeCompatibility.Get(TString{cluster}).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = options.Config()->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+ options.OutTable().RowSpec->FillAttrNode(attrs[YqlRowSpecAttribute], nativeYtTypeCompatibility, rowSpecCompactForm);
+ NYT::TNode rowSpecYson;
+ options.OutTable().RowSpec->FillCodecNode(rowSpecYson);
+ attrs["schema"] = RowSpecToYTSchema(rowSpecYson, nativeYtTypeCompatibility).ToNode();
+
+ NYT::TNode outSpec = NYT::TNode::CreateList();
+ outSpec.Add(NYT::TNode::CreateMap()(TString{YqlRowSpecAttribute}, rowSpecYson));
+ outSpec = NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, std::move(outSpec));
+
+ TFullResultTableResult res;
+
+ TString name = TStringBuilder() << "tmp/" << GetGuidAsString(session->RandomProvider_->GenGuid());
+ TString path = Services_->GetTablePath(cluster, name, true);
+
+ res.Server = cluster;
+ res.Path = path;
+ res.RefName = name;
+ res.CodecSpec = NYT::NodeToYsonString(outSpec);
+ res.TableAttrs = NYT::NodeToYsonString(attrs);
+
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TFullResultTableResult>();
+ }
+ }
+
+ TString GetDefaultClusterName() const final {
+ return {};
+ }
+
+ void SetStatUploader(IStatUploader::TPtr statUploader) final {
+ Y_UNUSED(statUploader);
+ }
+
+ void RegisterMkqlCompiler(NCommon::TMkqlCallableCompilerBase& compiler) override {
+ RegisterDqYtFileMkqlCompilers(compiler);
+ }
+
+ TGetTablePartitionsResult GetTablePartitions(TGetTablePartitionsOptions&& options) override {
+ const TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ auto res = TGetTablePartitionsResult();
+ TVector<NYT::TRichYPath> paths;
+ for (const auto& pathInfo: options.Paths()) {
+ const auto tablePath = TransformPath(tmpFolder, pathInfo->Table->Name, pathInfo->Table->IsTemp, options.SessionId());
+ NYT::TRichYPath richYtPath{NYT::AddPathPrefix(tablePath, NYT::TConfig::Get()->Prefix)};
+ pathInfo->FillRichYPath(richYtPath); // n.b. throws exception, if there is no RowSpec (we assume it is always there)
+ paths.push_back(std::move(richYtPath));
+ }
+ res.Partitions.Partitions.push_back({});
+ res.Partitions.Partitions.back().TableRanges = std::move(paths);
+ res.SetSuccess();
+ return res;
+ }
+
+private:
+ static NYT::TNode LoadTableAttrs(const TString& path) {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ if (NFs::Exists(path + ".attr")) {
+ attrs = NYT::NodeFromYsonString(TIFStream(path + ".attr").ReadAll());
+ };
+ return attrs;
+ }
+
+ static void LoadTableMetaInfo(const TTableReq& req, const TString& path, TYtTableMetaInfo& info) {
+ NYT::TNode attrs = LoadTableAttrs(path);
+
+ TransferTableAttributes(attrs, [&info] (const TString& name, const TString& val) {
+ info.Attrs[name] = val;
+ });
+
+ if (attrs.HasKey(YqlDynamicAttribute)) {
+ info.IsDynamic = attrs[YqlDynamicAttribute].AsBool();
+ }
+
+ if (attrs.HasKey(YqlTypeAttribute)) {
+ auto type = attrs[YqlTypeAttribute];
+ YQL_ENSURE(type.AsString() == YqlTypeView);
+ info.SqlView = TIFStream(path).ReadAll();
+ auto attrVer = type.Attributes()["syntax_version"];
+ info.SqlViewSyntaxVersion = attrVer.IsUndefined() ? 0 : attrVer.AsInt64();
+ info.CanWrite = false;
+ return;
+ }
+
+ info.CanWrite = true;
+ info.YqlCompatibleScheme = ValidateTableSchema(
+ req.Table(), attrs, req.IgnoreYamrDsv(), req.IgnoreWeakSchema()
+ );
+
+ NYT::TNode schemaAttrs;
+ if (req.ForceInferSchema() && req.InferSchemaRows() > 0) {
+ info.Attrs.erase(YqlRowSpecAttribute);
+ if (!req.Intents().HasFlags(
+ TYtTableIntent::Override | TYtTableIntent::Append | TYtTableIntent::Drop | TYtTableIntent::Flush)) {
+ auto list = LoadTableContent(path);
+ if (!list.AsList().empty()) {
+ auto inferedSchemaAttrs = GetSchemaFromAttributes(attrs, true, req.IgnoreWeakSchema());
+ inferedSchemaAttrs[INFER_SCHEMA_ATTR_NAME] = InferSchemaFromSample(list, req.Table(), req.InferSchemaRows());
+ info.InferredScheme = true;
+ schemaAttrs = std::move(inferedSchemaAttrs);
+ }
+ }
+ } else {
+ if (info.YqlCompatibleScheme) {
+ schemaAttrs = GetSchemaFromAttributes(attrs, false, req.IgnoreWeakSchema());
+ }
+ else if (!info.Attrs.contains(YqlRowSpecAttribute)
+ && req.InferSchemaRows() > 0
+ && !req.Intents().HasFlags(TYtTableIntent::Override | TYtTableIntent::Append | TYtTableIntent::Drop | TYtTableIntent::Flush)) {
+ auto list = LoadTableContent(path);
+ if (!list.AsList().empty()) {
+ schemaAttrs[INFER_SCHEMA_ATTR_NAME] = InferSchemaFromSample(list, req.Table(), req.InferSchemaRows());
+ info.InferredScheme = true;
+ }
+ }
+ }
+
+ if (!schemaAttrs.IsUndefined()) {
+ for (auto& item: schemaAttrs.AsMap()) {
+ info.Attrs[item.first] = NYT::NodeToYsonString(item.second, NYson::EYsonFormat::Text);
+ }
+ }
+ }
+
+ static void LoadTableStatInfo(const TString& path, TYtTableStatInfo& info) {
+ NYT::TNode inputList = LoadTableContent(path);
+ info.RecordsCount = inputList.AsList().size();
+ if (!info.IsEmpty()) {
+ info.DataSize = TFileStat(path).Size;
+ info.ChunkCount = 1;
+ }
+ }
+
+ static NYT::TNode LoadTableContent(const TString& path) {
+ NYT::TNode inputList = NYT::TNode::CreateList();
+ if (TFileStat(path).Size) {
+ TIFStream input(path);
+ NYT::TNodeBuilder builder(&inputList);
+ NYson::TYsonParser parser(&builder, &input, ::NYson::EYsonType::ListFragment);
+ parser.Parse();
+ }
+ return inputList;
+ }
+
+ bool ExecuteResult(TSession& session, NYson::TYsonWriter& writer, TExprBase input, TExprContext& exprCtx,
+ TResOrPullOptions&& options, const TVector<TString>& columns) const
+ {
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ auto secureParamsProvider = MakeSimpleSecureParamsProvider(options.SecureParams());
+ TFileYtLambdaBuilder builder(alloc, session,
+ MakeFunctionRegistry(*Services_->GetFunctionRegistry(), options.UserDataBlocks()), secureParamsProvider.get());
+ auto data = builder.BuildLambda(*MkqlCompiler_, input.Ptr(), exprCtx);
+ auto transform = TFileTransformProvider(Services_, options.UserDataBlocks());
+ data = builder.TransformAndOptimizeProgram(data, transform);
+
+ TExploringNodeVisitor explorer;
+ auto nodeFactory = GetYtFileFullFactory(Services_);
+ auto compGraph = builder.BuildGraph(nodeFactory, options.UdfValidateMode(),
+ NUdf::EValidatePolicy::Exception, options.OptLLVM(), EGraphPerProcess::Multi, explorer, data, {data.GetNode()});
+ const TBindTerminator bind(compGraph->GetTerminator());
+ compGraph->Prepare();
+
+ TExecuteResOrPull resultData(options.FillSettings().RowsLimitPerWrite,
+ options.FillSettings().AllResultsBytesLimit, MakeMaybe(columns));
+
+ resultData.WriteValue(compGraph->GetValue(), data.GetStaticType());
+ auto dataRes = resultData.Finish();
+
+ writer.OnKeyedItem("Data");
+ writer.OnRaw(options.FillSettings().Discard ? "#" : dataRes);
+
+ return resultData.IsTruncated();
+ }
+
+ bool ExecutePull(TSession& session, NYson::TYsonWriter& writer, TPull pull, TExprContext& exprCtx,
+ TResOrPullOptions&& options, const TVector<TString>& columns) const
+ {
+ bool truncated = false;
+ bool writeRef = NCommon::HasResOrPullOption(pull.Ref(), "ref");
+
+ if (!writeRef) {
+ truncated = ExecuteResult(session, writer, pull, exprCtx, std::move(options), columns);
+ writeRef = truncated && NCommon::HasResOrPullOption(pull.Ref(), "autoref");
+ }
+
+ if (writeRef && !options.FillSettings().Discard) {
+ auto cluster = GetClusterName(pull.Input());
+ writer.OnKeyedItem("Ref");
+ writer.OnBeginList();
+ for (auto& tableInfo: GetInputTableInfos(pull.Input())) {
+ writer.OnListItem();
+ if (tableInfo->IsTemp) {
+ auto outPath = Services_->GetTablePath(cluster, tableInfo->Name, true);
+ session.CancelDeleteAtFinalize(TString{cluster}, outPath);
+ }
+ NYql::WriteTableReference(writer, YtProviderName, cluster, tableInfo->Name, tableInfo->IsTemp, columns);
+ }
+ writer.OnEndList();
+ }
+
+ return truncated;
+ }
+
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> ExecuteOpWithOutput(TSession& session,
+ const TExprNode::TPtr& node, TExprContext& exprCtx,
+ TRunOptions&& options) const
+ {
+ TYtOutputOpBase op(node);
+
+ auto cluster = TString{op.DataSink().Cluster().Value()};
+ TVector<TString> outTablePaths;
+ TVector<TYtOutTableInfo> outTableInfos;
+ for (auto table: op.Output()) {
+ TString name = TStringBuilder() << "tmp/" << GetGuidAsString(session.RandomProvider_->GenGuid());
+
+ outTablePaths.push_back(Services_->GetTablePath(cluster, name, true));
+
+ outTableInfos.emplace_back(table);
+ outTableInfos.back().Name = name;
+ }
+
+ TFsQueryCacheItem queryCacheItem(*options.Config(), cluster, Services_->GetTmpDir(), options.OperationHash(), outTablePaths);
+ if (!queryCacheItem.Lookup(FakeQueue_)) {
+ TScopedAlloc alloc(__LOCATION__, TAlignedPagePoolCounters(),
+ Services_->GetFunctionRegistry()->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ auto secureParamsProvider = MakeSimpleSecureParamsProvider(options.SecureParams());
+ TFileYtLambdaBuilder builder(alloc, session,
+ MakeFunctionRegistry(*Services_->GetFunctionRegistry(), options.UserDataBlocks()), secureParamsProvider.get());
+ auto data = builder.BuildLambda(*MkqlCompiler_, node, exprCtx);
+ auto transform = TFileTransformProvider(Services_, options.UserDataBlocks());
+ data = builder.TransformAndOptimizeProgram(data, transform);
+
+ TExploringNodeVisitor explorer;
+ auto nodeFactory = GetYtFileFullFactory(Services_);
+ auto compGraph = builder.BuildGraph(nodeFactory, options.UdfValidateMode(),
+ NUdf::EValidatePolicy::Exception, options.OptLLVM(), EGraphPerProcess::Multi, explorer, data, {data.GetNode()});
+ const TBindTerminator bind(compGraph->GetTerminator());
+ compGraph->Prepare();
+
+ WriteOutTables(builder, options.Config(), session, cluster, outTableInfos, compGraph.Get());
+ queryCacheItem.Store();
+ }
+
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> outStat;
+ for (size_t i: xrange(outTableInfos.size())) {
+ TYtTableStatInfo::TPtr statInfo = MakeIntrusive<TYtTableStatInfo>();
+ statInfo->Id = outTableInfos[i].Name;
+ LoadTableStatInfo(outTablePaths.at(i), *statInfo);
+
+ outStat.emplace_back(statInfo->Id, statInfo);
+ }
+ return outStat;
+ }
+
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> ExecuteTouch(const TYtSettings::TConstPtr& config, TSession& session, const TYtTouch& op) const {
+ auto cluster = op.DataSink().Cluster().Value();
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> outStat;
+ for (auto table: op.Output()) {
+ TString name = TStringBuilder() << "tmp/" << GetGuidAsString(session.RandomProvider_->GenGuid());
+
+ TYtOutTableInfo tableInfo(table);
+ tableInfo.Name = name;
+ WriteOutTable(config, session, cluster, tableInfo, {});
+
+ TYtTableStatInfo::TPtr statInfo = MakeIntrusive<TYtTableStatInfo>();
+ statInfo->Id = name;
+ LoadTableStatInfo(Services_->GetTablePath(cluster, name, true), *statInfo);
+
+ outStat.emplace_back(statInfo->Id, statInfo);
+ }
+
+ return outStat;
+ }
+
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> ExecutePrepare(const TPrepareOptions& options, const TSession& session, const TYtOutputOpBase& op) const {
+ const auto cluster = op.DataSink().Cluster().StringValue();
+ YQL_ENSURE(op.Output().Size() == 1U);
+
+ const TString name = TStringBuilder() << "tmp/" << GetGuidAsString(session.RandomProvider_->GenGuid());
+ const auto path = Services_->GetTablePath(cluster, name, true);
+
+ TFsQueryCacheItem queryCacheItem(*options.Config(), cluster, Services_->GetTmpDir(), options.OperationHash(), path);
+ if (queryCacheItem.Lookup(FakeQueue_)) {
+ TYtTableStatInfo::TPtr statInfo = MakeIntrusive<TYtTableStatInfo>();
+ statInfo->Id = name;
+ LoadTableStatInfo(path, *statInfo);
+
+ return {{statInfo->Id, std::move(statInfo)}};
+ }
+
+ TYtOutTableInfo tableInfo(op.Output().Item(0));
+ tableInfo.Name = name;
+ WriteOutTable(options.Config(), const_cast<TSession&>(session), cluster, tableInfo, {});
+ return {{name, nullptr}};
+ }
+
+ void ExecuteDrop(const TExprNode::TPtr& node) const {
+ TYtDropTable op(node);
+ auto table = op.Table();
+ bool isAnonymous = NYql::HasSetting(table.Settings().Ref(), EYtSettingType::Anonymous);
+ auto path = Services_->GetTablePath(op.DataSink().Cluster().Value(), table.Name().Value(), isAnonymous, true);
+
+ NFs::Remove(path);
+ NFs::Remove(path + ".attr");
+ }
+
+ void WriteOutTables(TLambdaBuilder& builder, const TYtSettings::TConstPtr& config, TSession& session, TStringBuf cluster,
+ const TVector<TYtOutTableInfo>& outTableInfos, IComputationGraph* compGraph) const
+ {
+ NYT::TNode outSpec = NYT::TNode::CreateList();
+ for (auto table: outTableInfos) {
+ table.RowSpec->FillCodecNode(outSpec.Add()[YqlRowSpecAttribute]);
+ }
+ outSpec = NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, std::move(outSpec));
+
+ TVector<TString> outTableContent = GetFileWriteResult(
+ builder.GetTypeEnvironment(),
+ builder.GetFunctionRegistry(),
+ compGraph->GetContext(),
+ compGraph->GetValue(),
+ outSpec);
+ YQL_ENSURE(outTableInfos.size() == outTableContent.size());
+
+ for (size_t i: xrange(outTableInfos.size())) {
+ WriteOutTable(config, session, cluster, outTableInfos[i], outTableContent[i]);
+ }
+ }
+
+ void WriteOutTable(const TYtSettings::TConstPtr& config, TSession& session, TStringBuf cluster,
+ const TYtOutTableInfo& outTableInfo, TStringBuf binaryYson) const
+ {
+ auto outPath = Services_->GetTablePath(cluster, outTableInfo.Name, true);
+ session.DeleteAtFinalize(config, TString{cluster}, outPath);
+ if (binaryYson) {
+ TMemoryInput in(binaryYson);
+ TOFStream of(outPath);
+ TDoubleHighPrecisionYsonWriter writer(&of, ::NYson::EYsonType::ListFragment);
+ NYson::TYsonParser parser(&writer, &in, ::NYson::EYsonType::ListFragment);
+ parser.Parse();
+ }
+ else {
+ YQL_ENSURE(TFile(outPath, CreateAlways | WrOnly).IsOpen(), "Failed to create " << outPath.Quote() << " file");
+ }
+
+ {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ for (auto& a: outTableInfo.Meta->Attrs) {
+ attrs[a.first] = a.second;
+ }
+ const auto nativeYtTypeCompatibility = config->NativeYtTypeCompatibility.Get(TString{cluster}).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = config->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+ outTableInfo.RowSpec->FillAttrNode(attrs[YqlRowSpecAttribute], nativeYtTypeCompatibility, rowSpecCompactForm);
+ NYT::TNode rowSpecYson;
+ outTableInfo.RowSpec->FillCodecNode(rowSpecYson);
+ attrs["schema"] = RowSpecToYTSchema(rowSpecYson, nativeYtTypeCompatibility).ToNode();
+ TOFStream ofAttr(outPath + ".attr");
+ NYson::TYsonWriter writer(&ofAttr, NYson::EYsonFormat::Pretty, ::NYson::EYsonType::Node);
+ NYT::TNodeVisitor visitor(&writer);
+ visitor.Visit(attrs);
+ }
+ }
+
+ TSet<TString>& GetColumnarStatHistory(NYT::TRichYPath ytPath) {
+ YQL_ENSURE(ytPath.Columns_.Defined());
+ ytPath.Columns_.Clear();
+ return ColumnarStatHistory[NYT::NodeToCanonicalYsonString(NYT::PathToNode(ytPath), NYson::EYsonFormat::Text)];
+ }
+
+ TPathStatResult DoPathStat(TPathStatOptions&& options, bool onlyCached) {
+ TPathStatResult res;
+ res.DataSize.reserve(options.Paths().size());
+
+ auto extractSysColumns = [] (NYT::TRichYPath& ytPath) -> TVector<TString> {
+ TVector<TString> res;
+ if (ytPath.Columns_) {
+ auto it = std::remove_if(
+ ytPath.Columns_->Parts_.begin(),
+ ytPath.Columns_->Parts_.end(),
+ [] (const TString& col) { return col.StartsWith(YqlSysColumnPrefix); }
+ );
+ res.assign(it, ytPath.Columns_->Parts_.end());
+ ytPath.Columns_->Parts_.erase(it, ytPath.Columns_->Parts_.end());
+ }
+ return res;
+ };
+
+ for (auto& req: options.Paths()) {
+ auto path = Services_->GetTablePath(options.Cluster(), req.Path().Path_, req.IsTemp());
+
+ const NYT::TNode attrs = LoadTableAttrs(path);
+ bool inferSchema = attrs.HasKey("infer_schema") && attrs["infer_schema"].AsBool();
+
+ res.DataSize.push_back(0);
+ auto ytPath = req.Path();
+ if (auto sysColumns = extractSysColumns(ytPath)) {
+ NYT::TNode inputList = LoadTableContent(path);
+ auto records = inputList.AsList().size();
+ records = GetUsedRows(ytPath, records).GetOrElse(records);
+ for (auto col: sysColumns) {
+ auto size = 0;
+ if (col == YqlSysColumnNum || col == YqlSysColumnRecord) {
+ size = sizeof(ui64);
+ } else if (col == YqlSysColumnIndex) {
+ size = sizeof(ui32);
+ } else if (col == YqlSysColumnPath && !req.IsTemp()) {
+ size = req.Path().Path_.size();
+ }
+ res.DataSize.back() += size * records;
+ }
+ }
+
+ if (ytPath.Columns_ && !inferSchema) {
+ TSet<TString>& columnarStatsHistory = GetColumnarStatHistory(ytPath);
+
+ if (onlyCached) {
+ bool allColumnsPresent = AllOf(ytPath.Columns_->Parts_, [&](const auto& c) {
+ return columnarStatsHistory.contains(c);
+ });
+
+ if (!allColumnsPresent) {
+ return res;
+ }
+ }
+
+ TIFStream in(path);
+ TStringStream out;
+ TSet<TStringBuf> columns(ytPath.Columns_->Parts_.begin(), ytPath.Columns_->Parts_.end());
+ THashMap<TStringBuf, TStringBuf> renames;
+ if (ytPath.RenameColumns_) {
+ renames.insert(ytPath.RenameColumns_->begin(), ytPath.RenameColumns_->end());
+ }
+
+ TBinaryYsonWriter writer(&out, ::NYson::EYsonType::ListFragment);
+ TColumnFilteringConsumer filter(&writer, columns, renames);
+ NYson::TYsonParser parser(&filter, &in, ::NYson::EYsonType::ListFragment);
+ parser.Parse();
+
+ for (auto& c : columns) {
+ columnarStatsHistory.insert(TString(c));
+ }
+ res.DataSize.back() += out.Str().size();
+ } else {
+ res.DataSize.back() += TFileStat(path).Size;
+ }
+ }
+ res.SetSuccess();
+ return res;
+ }
+
+
+private:
+ TYtFileServices::TPtr Services_;
+ TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler> MkqlCompiler_;
+ THashMap<TString, TSession> Sessions;
+ bool* EmulateOutputForMultirunPtr;
+ THashMap<TString, TSet<TString>> ColumnarStatHistory;
+ TAsyncQueue::TPtr FakeQueue_;
+};
+
+} // NFile
+
+IYtGateway::TPtr CreateYtFileGateway(const NFile::TYtFileServices::TPtr& services, bool* emulateOutputForMultirunPtr) {
+ return new NFile::TYtFileGateway(services, emulateOutputForMultirunPtr);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h
new file mode 100644
index 0000000000..7b9c150b9b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "yql_yt_file_services.h"
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/folder/dirut.h>
+#include <util/system/mutex.h>
+
+#include <vector>
+
+namespace NKikimr {
+namespace NMiniKQL {
+class IComputationPatternCache;
+}
+}
+
+namespace NYql {
+
+IYtGateway::TPtr CreateYtFileGateway(const NFile::TYtFileServices::TPtr& services,
+ bool* emulateOutputForMultirunPtr = nullptr);
+
+}
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.cpp b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.cpp
new file mode 100644
index 0000000000..3d632ef3fe
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.cpp
@@ -0,0 +1,537 @@
+#include "yql_yt_file_comp_nodes.h"
+#include "yql_yt_file.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_table.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_ungrouping_list.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/providers/common/comp_nodes/yql_factory.h>
+#include <ydb/library/yql/parser/pg_wrapper/interface/comp_factory.h>
+#include <ydb/library/yql/public/udf/udf_version.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_impl.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_pack.h>
+#include <ydb/library/yql/minikql/computation/mkql_custom_list.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+
+#include <library/cpp/yson/node/node_io.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <library/cpp/yson/parser.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/stream/str.h>
+#include <util/stream/file.h>
+#include <util/stream/buffer.h>
+#include <util/system/fs.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/xrange.h>
+#include <util/generic/ylimits.h>
+#include <util/generic/maybe.h>
+#include <util/generic/size_literals.h>
+
+#include <utility>
+#include <algorithm>
+#include <iterator>
+#include <array>
+
+namespace NYql::NFile {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+
+struct TColumnsInfo {
+ TMaybe<NYT::TSortColumns> Columns;
+ TMaybe<NYT::TRichYPath::TRenameColumnsDescriptor> RenameColumns;
+};
+
+class TTextYsonInput: public NYT::TRawTableReader {
+public:
+ TTextYsonInput(const TString& file, const TColumnsInfo& columnsInfo) {
+ TIFStream in(file);
+
+ TBinaryYsonWriter writer(&Input_, ::NYson::EYsonType::ListFragment);
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("row_index");
+ writer.OnInt64Scalar(0);
+ writer.OnEndAttributes();
+ writer.OnEntity();
+ writer.OnListItem();
+ NYT::NYson::IYsonConsumer* consumer = &writer;
+ THolder<TColumnFilteringConsumer> filter;
+ if (columnsInfo.Columns || columnsInfo.RenameColumns) {
+ TMaybe<TSet<TStringBuf>> columns;
+ TMaybe<THashMap<TStringBuf, TStringBuf>> renames;
+ if (columnsInfo.Columns) {
+ columns.ConstructInPlace(columnsInfo.Columns->Parts_.begin(), columnsInfo.Columns->Parts_.end());
+ }
+ if (columnsInfo.RenameColumns) {
+ renames.ConstructInPlace(columnsInfo.RenameColumns->begin(), columnsInfo.RenameColumns->end());
+ }
+
+ filter.Reset(new TColumnFilteringConsumer(consumer, columns, renames));
+ consumer = filter.Get();
+ }
+ NYson::TYsonParser parser(consumer, &in, ::NYson::EYsonType::ListFragment);
+ parser.Parse();
+ }
+
+ bool Retry(const TMaybe<ui32>& /* rangeIndex */, const TMaybe<ui64>& /* rowIndex */) override {
+ return false;
+ }
+
+ void ResetRetries() override {
+ }
+
+ bool HasRangeIndices() const override {
+ return false;
+ }
+
+protected:
+ size_t DoRead(void* buf, size_t len) override {
+ return Input_.Read(buf, len);
+ }
+
+private:
+ TBufferStream Input_;
+};
+
+TVector<NYT::TRawTableReaderPtr> MakeTextYsonInputs(const TVector<std::pair<TString, TColumnsInfo>>& files) {
+ TVector<NYT::TRawTableReaderPtr> rawReaders;
+ for (auto& file: files) {
+ if (!NFs::Exists(file.first)) {
+ rawReaders.emplace_back(nullptr);
+ continue;
+ }
+ rawReaders.emplace_back(MakeIntrusive<TTextYsonInput>(file.first, file.second));
+ }
+ return rawReaders;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TFileInputStateWithTableState: public TFileInputState {
+public:
+ TFileInputStateWithTableState(const TMkqlIOSpecs& spec, const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ TVector<NYT::TRawTableReaderPtr>&& rawReaders,
+ size_t blockCount, size_t blockSize, TTableState&& tableState)
+ : TFileInputState(spec, holderFactory, std::move(rawReaders), blockCount, blockSize)
+ , TableState_(std::move(tableState))
+ {
+ UpdateTableState();
+ }
+
+protected:
+ void Next() override {
+ TFileInputState::Next();
+ UpdateTableState();
+ }
+
+ void UpdateTableState() {
+ if (IsValid()) {
+ TableState_.Update(GetTableIndex(), GetRecordIndex());
+ } else {
+ TableState_.Reset();
+ }
+ }
+
+private:
+ TTableState TableState_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TTextYsonFileListValue : public TFileListValueBase {
+public:
+ TTextYsonFileListValue(NKikimr::NMiniKQL::TMemoryUsageInfo* memInfo,
+ const TMkqlIOSpecs& spec,
+ const NKikimr::NMiniKQL::THolderFactory& holderFactory,
+ const TVector<std::pair<TString, TColumnsInfo>>& tablePaths,
+ TTableState&& tableState, std::optional<ui64> length)
+ : TFileListValueBase(memInfo, spec, holderFactory, length)
+ , TablePaths_(tablePaths)
+ , TableState_(std::move(tableState))
+ {
+ }
+
+protected:
+ THolder<IInputState> MakeState() const override {
+ return MakeHolder<TFileInputStateWithTableState>(Spec, HolderFactory, MakeTextYsonInputs(TablePaths_),
+ 0u, 1_MB, TTableState(TableState_));
+ }
+
+private:
+ TVector<std::pair<TString, TColumnsInfo>> TablePaths_;
+ TTableState TableState_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+template <bool TableContent>
+class TYtTableFileWrapper : public TMutableComputationNode<TYtTableFileWrapper<TableContent>> {
+ typedef TMutableComputationNode<TYtTableFileWrapper<TableContent>> TBaseComputation;
+public:
+ TYtTableFileWrapper(TComputationMutables& mutables, NCommon::TCodecContext& codecCtx,
+ TVector<std::pair<TString, TColumnsInfo>>&& tablePaths,
+ const NYT::TNode& inputSpecs, const TVector<ui32>& groups, TType* itemType,
+ TVector<TString>&& tableNames, TVector<ui64>&& rowOffsets, THashSet<TString>&& auxColumns,
+ std::array<IComputationExternalNode*, 5>&& argNodes, std::optional<ui64> length)
+ : TBaseComputation(mutables)
+ , TablePaths_(std::move(tablePaths))
+ , ArgNodes_(std::move(argNodes))
+ , Length_(std::move(length))
+ {
+ Spec_.Init(codecCtx, inputSpecs, groups, tableNames, itemType, auxColumns, NYT::TNode());
+ if (!rowOffsets.empty()) {
+ Spec_.SetTableOffsets(rowOffsets);
+ }
+ }
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ if (TableContent) {
+ return ctx.HolderFactory.Create<TTextYsonFileListValue>(Spec_, ctx.HolderFactory, TablePaths_,
+ TTableState(Spec_.TableNames, Spec_.TableOffsets, ctx, ArgNodes_), Length_);
+ }
+ else {
+ THolder<TFileInputState> inputState(new TFileInputStateWithTableState(Spec_, ctx.HolderFactory, MakeTextYsonInputs(TablePaths_),
+ 0, 1_MB, TTableState(Spec_.TableNames, Spec_.TableOffsets, ctx, ArgNodes_)));
+ NUdf::TUnboxedValue singlePassIter(ctx.HolderFactory.Create<THoldingInputStreamValue>(inputState.Release()));
+ return ctx.HolderFactory.Create<TForwardListValue>(std::move(singlePassIter));
+ }
+ }
+
+private:
+ void RegisterDependencies() const final {
+ for (auto node: ArgNodes_) {
+ TBaseComputation::Own(node);
+ }
+ }
+
+ TMkqlIOSpecs Spec_;
+ const TVector<std::pair<TString, TColumnsInfo>> TablePaths_;
+ const std::array<IComputationExternalNode*, 5> ArgNodes_;
+ std::optional<ui64> Length_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TFileWriteApplyContext : public NUdf::IApplyContext {
+public:
+ TFileWriteApplyContext(TMkqlWriterImpl& writer)
+ : Writer(writer)
+ {
+ }
+
+ void WriteStream(const NUdf::TUnboxedValue& res) {
+ NUdf::TUnboxedValue value;
+ for (auto status = res.Fetch(value); status != NUdf::EFetchStatus::Finish; status = res.Fetch(value)) {
+ if (status != NUdf::EFetchStatus::Yield)
+ Writer.AddRow(value);
+ }
+ }
+
+private:
+ TMkqlWriterImpl& Writer;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TFileWriteWrapper : public TMutableComputationNode<TFileWriteWrapper> {
+ typedef TMutableComputationNode<TFileWriteWrapper> TBaseComputation;
+public:
+ class TResult : public TComputationValue<TResult> {
+ public:
+ TResult(TMemoryUsageInfo* memInfo, const NUdf::TUnboxedValue& result)
+ : TComputationValue(memInfo)
+ , Result(result)
+ {
+ }
+
+ private:
+ void Apply(NUdf::IApplyContext& applyContext) const override {
+ CheckedCast<TFileWriteApplyContext*>(&applyContext)->WriteStream(Result);
+ }
+
+ const NUdf::TUnboxedValue& Result;
+ };
+
+
+ TFileWriteWrapper(TComputationMutables& mutables, IComputationNode* result)
+ : TBaseComputation(mutables)
+ , Result(result)
+ , StreamValueIndex(mutables.CurValueIndex++)
+ {
+ }
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ auto& streamRef = ctx.MutableValues[StreamValueIndex];
+ streamRef = Result->GetValue(ctx);
+ return ctx.HolderFactory.Create<TResult>(streamRef);
+ }
+
+private:
+ void RegisterDependencies() const final {
+ DependsOn(Result);
+ }
+
+ IComputationNode* const Result;
+ const ui32 StreamValueIndex;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TDqWriteWrapper : public TMutableComputationNode<TDqWriteWrapper> {
+ typedef TMutableComputationNode<TDqWriteWrapper> TBaseComputation;
+public:
+ TDqWriteWrapper(TComputationMutables& mutables, IComputationNode* item, const TString& path, TType* itemType, const TYtFileServices::TPtr& services)
+ : TBaseComputation(mutables)
+ , Item(item)
+ , Path(path)
+ , Packer(true, itemType)
+ , Services(services)
+ {
+ }
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ auto value = Item->GetValue(ctx);
+ Services->PushTableContent(Path, TString{Packer.Pack(value)});
+ return {};
+ }
+
+private:
+ void RegisterDependencies() const final {
+ DependsOn(Item);
+ }
+
+ IComputationNode* const Item;
+ const TString Path;
+ TValuePacker Packer;
+ TYtFileServices::TPtr Services;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+template <bool TableContent>
+IComputationNode* WrapYtTableFile(NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx,
+ const TYtFileServices::TPtr& services, bool noLocks)
+{
+ if (TableContent) {
+ YQL_ENSURE(callable.GetInputsCount() == 4, "Expected 4 args");
+ } else {
+ YQL_ENSURE(callable.GetInputsCount() == 8 || callable.GetInputsCount() == 4, "Expected 8 or 4 args");
+ }
+ const TString cluster(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+
+ TType* itemType = AS_TYPE(NMiniKQL::TListType, callable.GetType()->GetReturnType())->GetItemType();
+ TVector<std::pair<TString, TColumnsInfo>> tablePaths;
+ TVector<TString> tableNames;
+ TVector<ui64> rowOffsets;
+ ui64 currentRowOffset = 0;
+ TVector<ui32> groups;
+ NYT::TNode inputAttrs = NYT::TNode::CreateList();
+ THashSet<TString> auxColumns;
+
+ NCommon::TCodecContext codecCtx(ctx.Env, ctx.FunctionRegistry, &ctx.HolderFactory);
+
+ TListLiteral* groupList = AS_VALUE(TListLiteral, callable.GetInput(1));
+ const bool multiGroup = groupList->GetItemsCount() > 1;
+
+ TVector<THashSet<TStringBuf>> outColumnGroups;
+ if (multiGroup) {
+ TVariantType* itemVarType = AS_TYPE(TVariantType, itemType);
+ for (ui32 g = 0; g < groupList->GetItemsCount(); ++g) {
+ NMiniKQL::TStructType* itemStruct = AS_TYPE(TStructType, itemVarType->GetAlternativeType(g));
+ auto& outColumns = outColumnGroups.emplace_back();
+ for (ui32 index = 0; index < itemStruct->GetMembersCount(); ++index) {
+ outColumns.insert(itemStruct->GetMemberName(index));
+ }
+ }
+ }
+ else {
+ NMiniKQL::TStructType* itemStruct = AS_TYPE(NMiniKQL::TStructType, itemType);
+ auto& outColumns = outColumnGroups.emplace_back();
+ for (ui32 index = 0; index < itemStruct->GetMembersCount(); ++index) {
+ outColumns.insert(itemStruct->GetMemberName(index));
+ }
+ }
+
+ for (ui32 g = 0; g < groupList->GetItemsCount(); ++g) {
+ TListLiteral* tableList = AS_VALUE(TListLiteral, groupList->GetItems()[g]);
+ auto& outColumns = outColumnGroups[g];
+ currentRowOffset = 0;
+ for (ui32 t = 0; t < tableList->GetItemsCount(); ++t) {
+ TTupleLiteral* tuple = AS_VALUE(TTupleLiteral, tableList->GetItems()[t]);
+ YQL_ENSURE(tuple->GetValuesCount() == 7, "Expect 7 elements in the table tuple");
+
+ NYT::TRichYPath richYPath;
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, tuple->GetValue(0))->AsValue().AsStringRef())));
+
+ const bool isTemporary = AS_VALUE(TDataLiteral, tuple->GetValue(1))->AsValue().Get<bool>();
+ auto tableMeta = NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, tuple->GetValue(2))->AsValue().AsStringRef()));
+ TMkqlIOSpecs::TSpecInfo specInfo;
+ TMkqlIOSpecs::LoadSpecInfo(true, tableMeta, codecCtx, specInfo);
+ NMiniKQL::TStructType* itemStruct = AS_TYPE(NMiniKQL::TStructType, specInfo.Type);
+ for (ui32 index = 0; index < itemStruct->GetMembersCount(); ++index) {
+ // Ignore extra columns, which are not selected from the table
+ if (!outColumns.contains(itemStruct->GetMemberName(index))) {
+ auxColumns.insert(TString{itemStruct->GetMemberName(index)});
+ }
+ }
+ for (auto& aux: specInfo.AuxColumns) {
+ if (!outColumns.contains(aux.first)) {
+ auxColumns.insert(aux.first);
+ }
+ }
+
+ inputAttrs.Add(tableMeta);
+ auto path = services->GetTablePath(cluster, richYPath.Path_, isTemporary, noLocks);
+ tableNames.push_back(isTemporary ? TString() : richYPath.Path_);
+ tablePaths.emplace_back(path, TColumnsInfo{richYPath.Columns_, richYPath.RenameColumns_});
+ if (!richYPath.Columns_ && !isTemporary && specInfo.StrictSchema) {
+ TVector<TString> columns;
+ for (ui32 index = 0; index < itemStruct->GetMembersCount(); ++index) {
+ columns.emplace_back(itemStruct->GetMemberName(index));
+ }
+ for (auto& aux: specInfo.AuxColumns) {
+ columns.push_back(aux.first);
+ }
+ tablePaths.back().second.Columns.ConstructInPlace(std::move(columns));
+ }
+ if (multiGroup) {
+ groups.push_back(g);
+ }
+ rowOffsets.push_back(currentRowOffset);
+ currentRowOffset += AS_VALUE(TDataLiteral, tuple->GetValue(4))->AsValue().Get<ui64>();
+ }
+ }
+
+ std::optional<ui64> length;
+ TTupleLiteral* lengthTuple = AS_VALUE(TTupleLiteral, callable.GetInput(3));
+ if (lengthTuple->GetValuesCount() > 0) {
+ YQL_ENSURE(lengthTuple->GetValuesCount() == 1, "Expect 1 element in the length tuple");
+ length = AS_VALUE(TDataLiteral, lengthTuple->GetValue(0))->AsValue().Get<ui64>();
+ }
+
+ std::array<IComputationExternalNode*, 5> argNodes;
+ argNodes.fill(nullptr);
+
+ if (!TableContent && callable.GetInputsCount() == 8) {
+ argNodes[0] = LocateExternalNode(ctx.NodeLocator, callable, 4, false); // TableIndex
+ argNodes[1] = LocateExternalNode(ctx.NodeLocator, callable, 5, false); // TablePath
+ argNodes[2] = LocateExternalNode(ctx.NodeLocator, callable, 6, false); // TableRecord
+ argNodes[4] = LocateExternalNode(ctx.NodeLocator, callable, 7, false); // RowNumber
+ }
+
+ // sampling arg is ignored in the file provider
+ return new TYtTableFileWrapper<TableContent>(ctx.Mutables, codecCtx, std::move(tablePaths),
+ NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, std::move(inputAttrs)),
+ groups, itemType, std::move(tableNames), std::move(rowOffsets), std::move(auxColumns), std::move(argNodes), length);
+}
+
+IComputationNode* WrapFileWrite(NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx) {
+ YQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arg");
+ const auto list = LocateNode(ctx.NodeLocator, callable, 0);
+ return new TFileWriteWrapper(ctx.Mutables, list);
+}
+
+IComputationNode* WrapDqWrite(NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx, const TYtFileServices::TPtr& services) {
+ YQL_ENSURE(callable.GetInputsCount() == 2, "Expected 2 args");
+ const auto item = LocateNode(ctx.NodeLocator, callable, 0);
+ TType* itemType = callable.GetInput(0).GetStaticType();
+ auto path = TString(AS_VALUE(TDataLiteral, callable.GetInput(1))->AsValue().AsStringRef());
+ return new TDqWriteWrapper(ctx.Mutables, item, path, itemType, services);
+}
+
+} // unnamed
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYtFileFactory {
+public:
+ TYtFileFactory(const TYtFileServices::TPtr& services)
+ : Services_(services)
+ {
+ }
+
+ IComputationNode* operator() (NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx) {
+ auto name = callable.GetType()->GetName();
+ if (name.ChopSuffix("File")) {
+ if (name == TStringBuf("YtWrite")) {
+ return WrapFileWrite(callable, ctx);
+ }
+ if (name == TStringBuf("DqWrite")) {
+ return WrapDqWrite(callable, ctx, Services_);
+ }
+ if (name == NNodes::TYtTableContent::CallableName()) {
+ return WrapYtTableFile<true>(callable, ctx, Services_, false);
+ }
+ if (name == "YtUngroupingList") {
+ return WrapYtUngroupingList(callable, ctx);
+ }
+ if (name == "YtTableInput") {
+ return WrapYtTableFile<false>(callable, ctx, Services_, false);
+ }
+ if (name == "YtTableInputNoCtx") {
+ return WrapYtTableFile<false>(callable, ctx, Services_, true);
+ }
+ }
+
+ return nullptr;
+ }
+private:
+ TYtFileServices::TPtr Services_;
+ TMaybe<ui32> ExprContextObject;
+};
+
+TComputationNodeFactory GetYtFileFactory(const TYtFileServices::TPtr& services) {
+ return TYtFileFactory(services);
+}
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetYtFileFullFactory(const TYtFileServices::TPtr& services) {
+ return NMiniKQL::GetCompositeWithBuiltinFactory({
+ GetYtFileFactory(services),
+ NMiniKQL::GetYqlFactory(),
+ GetPgFactory()
+ });
+}
+
+TVector<TString> GetFileWriteResult(const TTypeEnvironment& env, const IFunctionRegistry& functionRegistry, TComputationContext& ctx, const NKikimr::NUdf::TUnboxedValue& value,
+ const NYT::TNode& outSpecs)
+{
+ NCommon::TCodecContext codecCtx(env, functionRegistry, &ctx.HolderFactory);
+ TMkqlIOSpecs spec;
+ spec.Init(codecCtx, outSpecs);
+
+ TVector<TStringStream> streams(spec.Outputs.size());
+ {
+ TVector<IOutputStream*> out(Reserve(spec.Outputs.size()));
+ std::transform(streams.begin(), streams.end(), std::back_inserter(out), [] (TStringStream& s) { return &s; });
+ TMkqlWriterImpl writer(out, 0, 4_MB);
+ writer.SetSpecs(spec);
+
+ TFileWriteApplyContext applyCtx(writer);
+ ApplyChanges(value, applyCtx);
+ writer.Finish();
+ }
+ TVector<TString> res;
+ res.reserve(spec.Outputs.size());
+ std::transform(streams.begin(), streams.end(), std::back_inserter(res), [] (TStringStream& s) { return s.Str(); });
+ return res;
+}
+
+
+} // NYql::NFile
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.h b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.h
new file mode 100644
index 0000000000..68de66d8ec
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_comp_nodes.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "yql_yt_file_services.h"
+
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_impl.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <library/cpp/yson/node/node.h>
+
+namespace NYql::NFile {
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetYtFileFactory(const TYtFileServices::TPtr& services);
+NKikimr::NMiniKQL::TComputationNodeFactory GetYtFileFullFactory(const TYtFileServices::TPtr& services);
+TVector<TString> GetFileWriteResult(const NKikimr::NMiniKQL::TTypeEnvironment& env, const NKikimr::NMiniKQL::IFunctionRegistry& functionRegistry,
+ NKikimr::NMiniKQL::TComputationContext& ctx, const NKikimr::NUdf::TUnboxedValue& value, const NYT::TNode& outSpecs);
+
+} // NYql::NFile
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.cpp b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.cpp
new file mode 100644
index 0000000000..8e4e714a22
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.cpp
@@ -0,0 +1,1076 @@
+#include "yql_yt_file_mkql_compiler.h"
+
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_table.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_helpers.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/common/mkql/yql_type_mkql.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/defs.h>
+
+#include <yt/cpp/mapreduce/interface/node.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/xrange.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NNodes;
+
+namespace {
+
+bool IsValidKeyNode(TExprBase node) {
+ if (node.Maybe<TCoNull>() || node.Maybe<TCoNothing>()) {
+ return true;
+ }
+
+ if (auto maybeJust = node.Maybe<TCoJust>()) {
+ node = maybeJust.Cast().Input();
+ }
+
+ return TCoDataCtor::Match(node.Raw());
+}
+
+bool HasRanges(const TExprNode& input, bool withKeyUsage) {
+ if (input.IsCallable(TYtOutTable::CallableName())) {
+ return false;
+ }
+ auto sectionList = TYtSectionList(&input);
+ return AnyOf(sectionList, [&](const TYtSection& section) {
+ return AnyOf(section.Paths(), [&](const TYtPath& path) {
+ TYtPathInfo pathInfo(path);
+ auto ranges = pathInfo.Ranges;
+ return ranges && (!withKeyUsage || ranges->GetUsedKeyPrefixLength());
+ });
+ });
+}
+
+bool HasRangesWithKeyColumns(const TExprNode& input) {
+ return HasRanges(input, /*withKeyUsage*/true);
+}
+
+TRuntimeNode ApplyPathRanges(TRuntimeNode inputList, const TExprNode& input, NCommon::TMkqlBuildContext& ctx) {
+ if (!HasRanges(input, false)) {
+ return inputList;
+ }
+
+ ui32 tableIndex = 0;
+ auto makeSectionFilter = [&](TYtSection section, TRuntimeNode item) {
+ ui64 currentOffset = 0;
+ TRuntimeNode filter = ctx.ProgramBuilder.NewDataLiteral(true);
+
+ TRuntimeNode rowNumber = ctx.ProgramBuilder.Member(item, YqlSysColumnNum);
+ rowNumber = ctx.ProgramBuilder.Sub(
+ ctx.ProgramBuilder.Max(
+ rowNumber,
+ ctx.ProgramBuilder.NewDataLiteral(ui64(1))
+ ),
+ ctx.ProgramBuilder.NewDataLiteral(ui64(1))
+ );
+
+ TRuntimeNode tableIndexCall = ctx.ProgramBuilder.Member(item, YqlSysColumnIndex);
+
+ TVector<std::pair<TRuntimeNode, TRuntimeNode>> tableIndexDictItems;
+
+ for (auto p: section.Paths()) {
+ TYtPathInfo pathInfo(p);
+ if (pathInfo.Ranges) {
+ TRuntimeNode condition = ctx.ProgramBuilder.NewDataLiteral(false);
+ for (auto& range: pathInfo.Ranges->GetRanges()) {
+ TRuntimeNode data;
+ // We can't use switch by index there since msvc deny to compile it.
+ // Anyway, visit works faster because we're making only one check instead of two.
+ std::visit([&](const auto& val) {
+ using TValue = std::decay_t<decltype(val)>;
+ if constexpr (std::is_same_v<TValue, TYtRangesInfo::TRowSingle>) {
+ data = ctx.ProgramBuilder.AggrEquals(rowNumber, ctx.ProgramBuilder.NewDataLiteral(currentOffset + val.Offset));
+ } else if constexpr (std::is_same_v<TValue, TYtRangesInfo::TRowRange>) {
+ if (val.Lower) {
+ data = ctx.ProgramBuilder.AggrGreaterOrEqual(rowNumber, ctx.ProgramBuilder.NewDataLiteral(currentOffset + *val.Lower));
+ }
+ if (val.Upper) {
+ auto upper = ctx.ProgramBuilder.AggrLess(rowNumber, ctx.ProgramBuilder.NewDataLiteral(currentOffset + *val.Upper));
+ data = data ? ctx.ProgramBuilder.And({data, upper}) : upper;
+ }
+ } else if constexpr (std::is_same_v<TValue, TYtRangesInfo::TKeySingle>) {
+ YQL_ENSURE(pathInfo.Table->RowSpec);
+ auto sortedBy = pathInfo.Table->RowSpec->SortedBy;
+ YQL_ENSURE(val.Key.size() <= sortedBy.size());
+ size_t sortedByIndex = 0;
+ for (auto& node : val.Key) {
+ TRuntimeNode keyPart;
+ YQL_ENSURE(IsValidKeyNode(node), "Range should be calculated");
+ if (TCoNull::Match(node.Raw())) {
+ keyPart = ctx.ProgramBuilder.Not(ctx.ProgramBuilder.Exists(
+ ctx.ProgramBuilder.Member(item, sortedBy[sortedByIndex++])));
+ } else if (TCoJust::Match(node.Raw()) || TCoNothing::Match(node.Raw())) {
+ keyPart = ctx.ProgramBuilder.AggrEquals(
+ ctx.ProgramBuilder.Member(item, sortedBy[sortedByIndex++]),
+ NCommon::MkqlBuildExpr(node.Ref(), ctx));
+ } else {
+ keyPart = ctx.ProgramBuilder.Equals(
+ ctx.ProgramBuilder.Member(item, sortedBy[sortedByIndex++]),
+ NCommon::MkqlBuildExpr(node.Ref(), ctx));
+ }
+ data = data ? ctx.ProgramBuilder.And({data, keyPart}) : keyPart;
+ }
+ } else if constexpr (std::is_same_v<TValue, TYtRangesInfo::TKeyRange>) {
+ YQL_ENSURE(pathInfo.Table->RowSpec);
+ auto sortedBy = pathInfo.Table->RowSpec->SortedBy;
+ if (!val.Lower.empty()) {
+ YQL_ENSURE(val.Lower.size() <= sortedBy.size());
+ for (size_t i: xrange(val.Lower.size() - 1)) {
+ YQL_ENSURE(IsValidKeyNode(val.Lower[i]), "Lower range should be calculated");
+ TRuntimeNode keyPart;
+ TRuntimeNode member = ctx.ProgramBuilder.Member(item, sortedBy[i]);
+ TRuntimeNode expr = NCommon::MkqlBuildExpr(val.Lower[i].Ref(), ctx);
+
+ if (TCoNull::Match(val.Lower[i].Raw())) {
+ keyPart = ctx.ProgramBuilder.Not(ctx.ProgramBuilder.Exists(member));
+ } else if (TCoJust::Match(val.Lower[i].Raw()) || TCoNothing::Match(val.Lower[i].Raw())) {
+ keyPart = ctx.ProgramBuilder.AggrGreaterOrEqual(member, expr);
+ } else {
+ keyPart = ctx.ProgramBuilder.GreaterOrEqual(member, expr);
+ }
+ data = data ? ctx.ProgramBuilder.And({data, keyPart}) : keyPart;
+ }
+ TRuntimeNode keyPart;
+ TRuntimeNode member = ctx.ProgramBuilder.Member(item, sortedBy[val.Lower.size() - 1]);
+ TRuntimeNode expr = NCommon::MkqlBuildExpr(val.Lower.back().Ref(), ctx);
+ if (TCoJust::Match(val.Lower.back().Raw()) || TCoNothing::Match(val.Lower.back().Raw())) {
+ keyPart = val.LowerInclude
+ ? ctx.ProgramBuilder.AggrGreaterOrEqual(member, expr)
+ : ctx.ProgramBuilder.AggrGreater(member, expr);
+ } else {
+ keyPart = val.LowerInclude
+ ? ctx.ProgramBuilder.GreaterOrEqual(member, expr)
+ : ctx.ProgramBuilder.Greater(member, expr);
+ }
+ data = data ? ctx.ProgramBuilder.And({data, keyPart}) : keyPart;
+ }
+ if (!val.Upper.empty()) {
+ YQL_ENSURE(val.Upper.size() <= sortedBy.size());
+ for (size_t i: xrange(val.Upper.size() - 1)) {
+ YQL_ENSURE(IsValidKeyNode(val.Upper[i]), "Upper range should be calculated");
+ TRuntimeNode keyPart;
+ TRuntimeNode member = ctx.ProgramBuilder.Member(item, sortedBy[i]);
+ TRuntimeNode expr = NCommon::MkqlBuildExpr(val.Upper[i].Ref(), ctx);
+ if (TCoNull::Match(val.Upper[i].Raw())) {
+ keyPart = ctx.ProgramBuilder.Not(ctx.ProgramBuilder.Exists(member));
+ } else if (TCoJust::Match(val.Upper[i].Raw()) || TCoNothing::Match(val.Upper[i].Raw())) {
+ keyPart = ctx.ProgramBuilder.AggrLessOrEqual(member, expr);
+ } else {
+ keyPart = ctx.ProgramBuilder.LessOrEqual(member, expr);
+ }
+ data = data ? ctx.ProgramBuilder.And({data, keyPart}) : keyPart;
+ }
+ TRuntimeNode keyPart;
+ TRuntimeNode member = ctx.ProgramBuilder.Member(item, sortedBy[val.Upper.size() - 1]);
+ TRuntimeNode expr = NCommon::MkqlBuildExpr(val.Upper.back().Ref(), ctx);
+ if (TCoJust::Match(val.Upper.back().Raw()) || TCoNothing::Match(val.Upper.back().Raw())) {
+ keyPart = val.UpperInclude
+ ? ctx.ProgramBuilder.AggrLessOrEqual(member, expr)
+ : ctx.ProgramBuilder.AggrLess(member, expr);
+ } else {
+ keyPart = val.UpperInclude
+ ? ctx.ProgramBuilder.LessOrEqual(member, expr)
+ : ctx.ProgramBuilder.Less(member, expr);
+ }
+ data = data ? ctx.ProgramBuilder.And({data, keyPart}) : keyPart;
+ }
+ }
+ }, range);
+ condition = ctx.ProgramBuilder.Or({condition, data});
+ }
+ if (condition.GetStaticType()->IsOptional() != filter.GetStaticType()->IsOptional()) {
+ if (condition.GetStaticType()->IsOptional()) {
+ filter = ctx.ProgramBuilder.NewOptional(filter);
+ } else {
+ condition = ctx.ProgramBuilder.NewOptional(condition);
+ }
+ }
+
+ filter = ctx.ProgramBuilder.If(
+ ctx.ProgramBuilder.AggrEquals(tableIndexCall, ctx.ProgramBuilder.NewDataLiteral(tableIndex)),
+ condition,
+ filter);
+
+ tableIndexDictItems.emplace_back(ctx.ProgramBuilder.NewDataLiteral<ui32>(tableIndex), ctx.ProgramBuilder.NewVoid());
+ }
+ ++tableIndex;
+ currentOffset += pathInfo.Table->Stat->RecordsCount;
+ }
+
+ if (!tableIndexDictItems.empty()) {
+ auto dictType = ctx.ProgramBuilder.NewDictType(
+ ctx.ProgramBuilder.NewDataType(NUdf::TDataType<ui32>::Id),
+ ctx.ProgramBuilder.GetTypeEnvironment().GetTypeOfVoid(),
+ false
+ );
+ auto dict = ctx.ProgramBuilder.NewDict(dictType, tableIndexDictItems);
+ filter = ctx.ProgramBuilder.Or({filter, ctx.ProgramBuilder.Not(ctx.ProgramBuilder.Contains(dict, tableIndexCall))});
+ }
+
+ if (filter.GetStaticType()->IsOptional()) {
+ filter = ctx.ProgramBuilder.Coalesce(filter, ctx.ProgramBuilder.NewDataLiteral(false));
+ }
+
+ return filter;
+ };
+
+ auto sectionList = TYtSectionList(&input);
+ if (sectionList.Size() > 1) {
+ inputList = ctx.ProgramBuilder.OrderedFilter(inputList, [&](TRuntimeNode varItem) -> TRuntimeNode {
+ YQL_ENSURE(AS_TYPE(TVariantType, varItem)->GetAlternativesCount() == sectionList.Size());
+ return ctx.ProgramBuilder.VisitAll(varItem, [&](ui32 index, TRuntimeNode item) -> TRuntimeNode {
+ return makeSectionFilter(sectionList.Item(index), item);
+ });
+ });
+ } else {
+ inputList = ctx.ProgramBuilder.OrderedFilter(inputList, [&](TRuntimeNode item) -> TRuntimeNode {
+ return makeSectionFilter(sectionList.Item(0), item);
+ });
+ }
+
+ return inputList;
+}
+
+TRuntimeNode ApplySampling(TRuntimeNode list, const TExprNode& input, NCommon::TMkqlBuildContext& ctx)
+{
+ if (input.IsCallable(TYtOutTable::CallableName())) {
+ return list;
+ }
+
+ auto getSamplingPercent = [](TYtSection section) -> TMaybe<double> {
+ if (auto setting = NYql::GetSetting(section.Settings().Ref(), EYtSettingType::Sample)) {
+ return FromString<double>(setting->Child(1)->Child(1)->Content());
+ }
+ return Nothing();
+ };
+
+ // At least one table has ranges
+ auto sectonList = TYtSectionList(&input);
+ TMaybe<double> percent = getSamplingPercent(sectonList.Item(0));
+
+ if (!percent) {
+ return list;
+ }
+
+ auto oneHundredNode = ctx.ProgramBuilder.NewDataLiteral<double>(100.);
+ auto percentNode = ctx.ProgramBuilder.NewDataLiteral<double>(*percent);
+ auto trueNode = ctx.ProgramBuilder.NewDataLiteral<bool>(true);
+ auto falseNode = ctx.ProgramBuilder.NewDataLiteral<bool>(false);
+ list = ctx.ProgramBuilder.ChainMap(list, oneHundredNode, [&] (TRuntimeNode item, TRuntimeNode state) -> TRuntimeNodePair {
+ auto tuple = ctx.ProgramBuilder.If(ctx.ProgramBuilder.AggrGreaterOrEqual(state, oneHundredNode),
+ ctx.ProgramBuilder.NewTuple({trueNode, ctx.ProgramBuilder.Add(ctx.ProgramBuilder.Sub(state, oneHundredNode), percentNode)}),
+ ctx.ProgramBuilder.NewTuple({falseNode, ctx.ProgramBuilder.Add(state, percentNode)})
+ );
+ return {
+ ctx.ProgramBuilder.NewTuple({item, ctx.ProgramBuilder.Nth(tuple, 0)}),
+ ctx.ProgramBuilder.Nth(tuple, 1)
+ };
+ });
+ list = ctx.ProgramBuilder.Filter(list, [&] (TRuntimeNode item) {
+ return ctx.ProgramBuilder.Nth(item, 1);
+ });
+ list = ctx.ProgramBuilder.Map(list, [&] (TRuntimeNode item) {
+ return ctx.ProgramBuilder.Nth(item, 0);
+ });
+ return list;
+}
+
+TRuntimeNode ApplyPathRangesAndSampling(TRuntimeNode inputList, TType* itemType, const TExprNode& input, NCommon::TMkqlBuildContext& ctx) {
+ inputList = ApplyPathRanges(inputList, input, ctx);
+ TType* actualItemType = inputList.GetStaticType();
+ if (actualItemType->IsStream()) {
+ actualItemType = AS_TYPE(TStreamType, actualItemType)->GetItemType();
+ } else if (actualItemType->IsFlow()) {
+ actualItemType = AS_TYPE(TFlowType, actualItemType)->GetItemType();
+ } else {
+ YQL_ENSURE(actualItemType->IsList());
+ actualItemType = AS_TYPE(TListType, actualItemType)->GetItemType();
+ }
+
+ auto dropExtraMembers = [&](TRuntimeNode item, TStructType* structType, TStructType* actualStructType) {
+ TVector<TStringBuf> toDrop;
+ for (ui32 i = 0; i < actualStructType->GetMembersCount(); ++i) {
+ auto member = actualStructType->GetMemberName(i);
+ if (!structType->FindMemberIndex(member)) {
+ toDrop.push_back(member);
+ }
+ }
+
+ for (auto& member : toDrop) {
+ item = ctx.ProgramBuilder.RemoveMember(item, member, true);
+ }
+
+ return item;
+ };
+
+ if (itemType->IsStruct()) {
+ YQL_ENSURE(actualItemType->IsStruct());
+ const auto structType = AS_TYPE(TStructType, itemType);
+ const auto actualStructType = AS_TYPE(TStructType, actualItemType);
+ YQL_ENSURE(actualStructType->GetMembersCount() >= structType->GetMembersCount());
+
+ if (actualStructType->GetMembersCount() > structType->GetMembersCount()) {
+ inputList = ctx.ProgramBuilder.Map(inputList, [&](TRuntimeNode item) {
+ return dropExtraMembers(item, structType, actualStructType);
+ });
+ }
+ } else {
+ const auto varType = AS_TYPE(TVariantType, itemType);
+ const auto actualVarType = AS_TYPE(TVariantType, actualItemType);
+
+ const auto tupleType = AS_TYPE(TTupleType, varType->GetUnderlyingType());
+ const auto actualTupleType = AS_TYPE(TTupleType, actualVarType->GetUnderlyingType());
+
+ inputList = ctx.ProgramBuilder.Map(inputList, [&](TRuntimeNode varItem) {
+ return ctx.ProgramBuilder.VisitAll(varItem, [&](ui32 index, TRuntimeNode item) {
+ const auto structType = AS_TYPE(TStructType, tupleType->GetElementType(index));
+ const auto actualStructType = AS_TYPE(TStructType, actualTupleType->GetElementType(index));
+ YQL_ENSURE(actualStructType->GetMembersCount() >= structType->GetMembersCount());
+
+ item = dropExtraMembers(item, structType, actualStructType);
+ return ctx.ProgramBuilder.NewVariant(item, index, varType);
+ });
+ });
+ }
+ inputList = ApplySampling(inputList, input, ctx);
+ return inputList;
+}
+
+TRuntimeNode ToList(TRuntimeNode list, NCommon::TMkqlBuildContext& ctx) {
+ const auto listType = list.GetStaticType();
+ if (listType->IsOptional()) {
+ return ctx.ProgramBuilder.ToList(list);
+ } else if (listType->IsList()) {
+ return list;
+ } else if (listType->IsFlow() || listType->IsStream()) {
+ return ctx.ProgramBuilder.ForwardList(list);
+ } else {
+ YQL_ENSURE(false, "Expected list, stream or optional");
+ }
+}
+
+TType* BuildInputType(TYtSectionList input, NCommon::TMkqlBuildContext& ctx) {
+ TVector<TType*> items;
+ for (auto section: input) {
+ items.push_back(NCommon::BuildType(input.Ref(), *section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder));
+ }
+ return items.size() == 1
+ ? items.front()
+ : ctx.ProgramBuilder.NewVariantType(ctx.ProgramBuilder.NewTupleType(items));
+}
+
+TType* BuildOutputType(TYtOutSection output, NCommon::TMkqlBuildContext& ctx) {
+ TVector<TType*> items;
+ for (auto table: output) {
+ items.push_back(NCommon::BuildType(output.Ref(), *table.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder));
+ }
+ return items.size() == 1
+ ? items.front()
+ : ctx.ProgramBuilder.NewVariantType(ctx.ProgramBuilder.NewTupleType(items));
+}
+
+TRuntimeNode ExpandFlow(TRuntimeNode flow, NCommon::TMkqlBuildContext& ctx) {
+ const auto structType = AS_TYPE(TStructType, AS_TYPE(TFlowType, flow.GetStaticType())->GetItemType());
+ return ctx.ProgramBuilder.ExpandMap(flow,
+ [&](TRuntimeNode item) {
+ TRuntimeNode::TList fields;
+ fields.reserve(structType->GetMembersCount());
+ auto i = 0U;
+ std::generate_n(std::back_inserter(fields), structType->GetMembersCount(), [&](){ return ctx.ProgramBuilder.Member(item, structType->GetMemberName(i++)); });
+ return fields;
+ });
+}
+
+TRuntimeNode NarrowFlow(TRuntimeNode flow, const TStructType& structType, NCommon::TMkqlBuildContext& ctx) {
+ return ctx.ProgramBuilder.NarrowMap(flow,
+ [&](TRuntimeNode::TList items) {
+ TSmallVec<const std::pair<std::string_view, TRuntimeNode>> fields;
+ fields.reserve(structType.GetMembersCount());
+ auto i = 0U;
+ std::transform(items.cbegin(), items.cend(), std::back_inserter(fields), [&](TRuntimeNode item) {
+ return std::make_pair(structType.GetMemberName(i++), item);
+ });
+ return ctx.ProgramBuilder.NewStruct(fields);
+ });
+}
+
+TRuntimeNode NarrowFlowOutput(TPositionHandle pos, TRuntimeNode flow, const TStructExprType* type, NCommon::TMkqlBuildContext& ctx) {
+ if (const auto width = GetWideComponentsCount(AS_TYPE(TFlowType, flow.GetStaticType())); type->GetSize() < width) {
+ auto items = type->GetItems();
+ auto i = 0U;
+ do items.emplace_back(ctx.ExprCtx.MakeType<TItemExprType>(TString("_yql_column_") += ToString(i++), ctx.ExprCtx.MakeType<TDataExprType>(EDataSlot::String)));
+ while (items.size() < width);
+ type = ctx.ExprCtx.MakeType<TStructExprType>(items);
+ }
+
+ return NarrowFlow(flow, *AS_TYPE(TStructType, NCommon::BuildType(pos, *type, ctx.ProgramBuilder)), ctx);
+}
+
+TRuntimeNode NarrowFlow(TRuntimeNode flow, TYtOutputOpBase op, NCommon::TMkqlBuildContext& ctx) {
+ auto type = op.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().back()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ return NarrowFlowOutput(op.Pos(), flow, type, ctx);
+}
+
+TRuntimeNode BuildTableInput(TType* outItemType, TStringBuf clusterName, const TExprNode& input, NCommon::TMkqlBuildContext& ctx,
+ const THashSet<TString>& extraSysColumns, bool forceKeyColumns)
+{
+ return BuildTableContentCall("YtTableInput", outItemType, clusterName, input, Nothing(), ctx, false, extraSysColumns, forceKeyColumns);
+}
+
+} // unnamed
+
+TRuntimeNode ToStream(TRuntimeNode list, NCommon::TMkqlBuildContext& ctx) {
+ const auto listType = list.GetStaticType();
+ if (listType->IsFlow()) {
+ return ctx.ProgramBuilder.FromFlow(list);
+ } else if (listType->IsOptional()) {
+ return ctx.ProgramBuilder.Iterator(ctx.ProgramBuilder.ToList(list), {});
+ } else if (listType->IsList()) {
+ return ctx.ProgramBuilder.Iterator(list, {});
+ } else if (listType->IsStream()) {
+ return list;
+ } else {
+ YQL_ENSURE(false, "Expected list, stream or optional");
+ }
+}
+
+TRuntimeNode SortListBy(TRuntimeNode list, const TVector<std::pair<TString, bool>>& sortBy, NCommon::TMkqlBuildContext& ctx) {
+ TRuntimeNode sortDirections;
+ std::function<TRuntimeNode(TRuntimeNode item)> keySelector;
+ if (sortBy.size() == 1) {
+ sortDirections = ctx.ProgramBuilder.NewDataLiteral(sortBy.front().second);
+ keySelector = [&ctx, &sortBy](TRuntimeNode item) {
+ return ctx.ProgramBuilder.Member(item, sortBy.front().first);
+ };
+ } else {
+ TVector<TRuntimeNode> tupleItems{sortBy.size()};
+ std::transform(sortBy.cbegin(), sortBy.cend(), tupleItems.begin(), [&ctx](const auto& it) { return ctx.ProgramBuilder.NewDataLiteral(it.second); });
+ sortDirections = ctx.ProgramBuilder.NewTuple(tupleItems);
+ keySelector = [&ctx, &sortBy](TRuntimeNode item) {
+ TVector<TRuntimeNode> members;
+ for (auto& it: sortBy) {
+ members.push_back(ctx.ProgramBuilder.Member(item, it.first));
+ }
+ return ctx.ProgramBuilder.NewTuple(members);
+ };
+ }
+
+ return ctx.ProgramBuilder.Sort(list, sortDirections, keySelector);
+}
+
+TRuntimeNode BuildTableOutput(TRuntimeNode list, NCommon::TMkqlBuildContext& ctx) {
+ list = ToStream(list, ctx);
+
+ TCallableBuilder fileWriteCall(ctx.ProgramBuilder.GetTypeEnvironment(), "YtWriteFile", ctx.ProgramBuilder.GetTypeEnvironment().GetTypeOfVoid());
+ fileWriteCall.Add(list);
+
+ return ctx.ProgramBuilder.AsList(TRuntimeNode(fileWriteCall.Build(), false));
+}
+
+TRuntimeNode BuildDqWrite(TRuntimeNode item, TStringBuf path, NCommon::TMkqlBuildContext& ctx) {
+ TCallableBuilder fileWriteCall(ctx.ProgramBuilder.GetTypeEnvironment(), "DqWriteFile", ctx.ProgramBuilder.GetTypeEnvironment().GetTypeOfVoid());
+ fileWriteCall.Add(item);
+ fileWriteCall.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(path));
+
+ return TRuntimeNode(fileWriteCall.Build(), false);
+}
+
+TRuntimeNode BuildRuntimeTableInput(TStringBuf callName,
+ TType* outItemType,
+ TStringBuf clusterName,
+ TStringBuf tableName,
+ TStringBuf spec,
+ bool isTemp,
+ NCommon::TMkqlBuildContext& ctx)
+{
+ auto outListType = ctx.ProgramBuilder.NewListType(outItemType);
+ TType* const strType = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<char*>::Id);
+ TType* const boolType = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<bool>::Id);
+ TType* const ui64Type = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<ui64>::Id);
+ TType* const ui32Type = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<ui32>::Id);
+ TType* const tupleTypeTables = ctx.ProgramBuilder.NewTupleType({strType, boolType, strType, ui64Type, ui64Type, boolType, ui32Type});
+ TType* const listTypeGroup = ctx.ProgramBuilder.NewListType(tupleTypeTables);
+
+ TCallableBuilder call(ctx.ProgramBuilder.GetTypeEnvironment(), callName, outListType);
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(clusterName)); // cluster name
+
+ TVector<TRuntimeNode> groups;
+ groups.push_back(
+ ctx.ProgramBuilder.NewList(tupleTypeTables, {ctx.ProgramBuilder.NewTuple(tupleTypeTables, {
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(NYT::PathToNode(NYT::TRichYPath(TString{tableName})))),
+ ctx.ProgramBuilder.NewDataLiteral(isTemp),
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(spec),
+ ctx.ProgramBuilder.NewDataLiteral(ui64(1)),
+ ctx.ProgramBuilder.NewDataLiteral(ui64(1)),
+ ctx.ProgramBuilder.NewDataLiteral(false),
+ ctx.ProgramBuilder.NewDataLiteral(ui32(0)),
+ })})
+ );
+
+ call.Add(ctx.ProgramBuilder.NewList(listTypeGroup, groups));
+ call.Add(ctx.ProgramBuilder.NewEmptyTuple()); // Sampling
+ call.Add(ctx.ProgramBuilder.NewEmptyTuple()); // length
+
+ return TRuntimeNode(call.Build(), false);
+}
+
+void RegisterYtFileMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler) {
+ compiler.OverrideCallable(TYtTableContent::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ TYtTableContent tableContent(&node);
+ TMaybe<ui64> itemsCount;
+ if (auto setting = NYql::GetSetting(tableContent.Settings().Ref(), EYtSettingType::ItemsCount)) {
+ itemsCount = FromString<ui64>(setting->Child(1)->Content());
+ }
+ const auto itemType = NCommon::BuildType(node, *node.GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder);
+ TRuntimeNode values;
+ if (auto maybeRead = tableContent.Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+
+ const bool hasRangesOrSampling = AnyOf(read.Input(), [](const TYtSection& s) {
+ return NYql::HasSetting(s.Settings().Ref(), EYtSettingType::Sample)
+ || AnyOf(s.Paths(), [](const TYtPath& p) { return !p.Ranges().Maybe<TCoVoid>(); });
+ });
+ if (hasRangesOrSampling) {
+ itemsCount.Clear();
+ }
+
+ const bool forceKeyColumns = HasRangesWithKeyColumns(read.Input().Ref());
+ values = BuildTableContentCall(
+ TYtTableContent::CallableName(),
+ itemType,
+ read.DataSource().Cluster().Value(), read.Input().Ref(), itemsCount, ctx, true, THashSet<TString>{"num", "index"}, forceKeyColumns);
+ values = ApplyPathRangesAndSampling(values, itemType, read.Input().Ref(), ctx);
+ } else {
+ auto output = tableContent.Input().Cast<TYtOutput>();
+ values = BuildTableContentCall(
+ TYtTableContent::CallableName(),
+ itemType,
+ GetOutputOp(output).DataSink().Cluster().Value(), output.Ref(), itemsCount, ctx, true);
+ }
+
+ return values;
+ });
+
+ compiler.AddCallable({TYtSort::CallableName(), TYtCopy::CallableName(), TYtMerge::CallableName()},
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+
+ TYtTransientOpBase ytOp(&node);
+ TYtOutTableInfo outTableInfo(ytOp.Output().Item(0));
+
+ TMaybe<ui64> limit;
+ if (ytOp.Maybe<TYtSort>()) {
+ limit = GetLimit(ytOp.Settings().Ref());
+ }
+
+ const TStructExprType* inputType = ytOp.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ auto mkqlInputType = NCommon::BuildType(ytOp.Input().Ref(), *inputType, ctx.ProgramBuilder);
+ for (size_t i: xrange(outTableInfo.RowSpec->SortedBy.size())) {
+ if (!inputType->FindItem(outTableInfo.RowSpec->SortedBy[i])) {
+ mkqlInputType = ctx.ProgramBuilder.NewStructType(mkqlInputType, outTableInfo.RowSpec->SortedBy[i],
+ NCommon::BuildType(ytOp.Input().Ref(), *outTableInfo.RowSpec->SortedByTypes[i], ctx.ProgramBuilder));
+ }
+ }
+
+ const bool forceKeyColumns = HasRangesWithKeyColumns(ytOp.Input().Ref());
+ TRuntimeNode values = BuildTableInput(mkqlInputType,
+ ytOp.DataSink().Cluster().Value(), ytOp.Input().Ref(), ctx, THashSet<TString>{"num", "index"}, forceKeyColumns);
+
+ values = ApplyPathRangesAndSampling(values, mkqlInputType, ytOp.Input().Ref(), ctx);
+
+ if ((ytOp.Maybe<TYtMerge>() && outTableInfo.RowSpec->IsSorted() && ytOp.Input().Item(0).Paths().Size() > 1)
+ || ytOp.Maybe<TYtSort>())
+ {
+ values = SortListBy(values, outTableInfo.RowSpec->GetForeignSort(), ctx);
+ }
+ if (limit) {
+ values = ctx.ProgramBuilder.Take(values, ctx.ProgramBuilder.NewDataLiteral(*limit));
+ }
+ auto res = BuildTableOutput(values, ctx);
+ return res;
+ });
+
+ compiler.AddCallable(TYtMap::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+
+ TYtMap ytMap(&node);
+
+ const auto itemType = BuildInputType(ytMap.Input(), ctx);
+ const bool forceKeyColumns = HasRangesWithKeyColumns(ytMap.Input().Ref());
+ TRuntimeNode values = BuildTableInput(
+ itemType,
+ ytMap.DataSink().Cluster().Value(), ytMap.Input().Ref(), ctx,
+ THashSet<TString>{"num", "index"}, forceKeyColumns);
+
+ const auto arg = ytMap.Mapper().Args().Arg(0).Raw();
+ values = arg->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Flow ?
+ ctx.ProgramBuilder.ToFlow(values) : ctx.ProgramBuilder.Iterator(values, {});
+ values = ApplyPathRangesAndSampling(values, itemType, ytMap.Input().Ref(), ctx);
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytMap.Mapper().Args().Arg(0).Ref().GetTypeAnn()).GetKind())
+ values = ExpandFlow(values, ctx);
+
+ NCommon::TMkqlBuildContext innerCtx(ctx, {{arg, values}}, ytMap.Mapper().Ref().UniqueId());
+ values = NCommon::MkqlBuildExpr(ytMap.Mapper().Body().Ref(), innerCtx);
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytMap.Mapper().Body().Ref().GetTypeAnn()).GetKind())
+ values = NarrowFlow(values, ytMap, ctx);
+
+ auto res = BuildTableOutput(values, ctx);
+ return res;
+ });
+
+ compiler.AddCallable(TYtReduce::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+
+ TYtReduce ytReduce(&node);
+
+ TType* itemType = BuildInputType(ytReduce.Input(), ctx);
+ const bool multiSection = itemType->GetKind() == TType::EKind::Variant;
+
+ const bool forceKeyColumns = HasRangesWithKeyColumns(ytReduce.Input().Ref());
+ TRuntimeNode values = BuildTableInput(itemType,
+ ytReduce.DataSink().Cluster().Value(), ytReduce.Input().Ref(), ctx,
+ THashSet<TString>{"num", "index"}, forceKeyColumns);
+
+ values = ApplyPathRangesAndSampling(values, itemType, ytReduce.Input().Ref(), ctx);
+
+ TVector<TString> reduceBy = NYql::GetSettingAsColumnList(ytReduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ TVector<std::pair<TString, bool>> sortBy = NYql::GetSettingAsColumnPairList(ytReduce.Settings().Ref(), EYtSettingType::SortBy);
+ TVector<bool> opt;
+ if (multiSection) {
+ opt.resize(Max(reduceBy.size(), sortBy.size()), false);
+ auto varType = AS_TYPE(TVariantType, AS_TYPE(TListType, values)->GetItemType());
+ for (ui32 i = 0; i < varType->GetAlternativesCount(); ++i) {
+ TStructType* structType = AS_TYPE(TStructType, varType->GetAlternativeType(i));
+ for (size_t c: xrange(reduceBy.size())) {
+ opt[c] = opt[c] || structType->GetMemberType(structType->GetMemberIndex(reduceBy[c]))->IsOptional();
+ }
+ if (!sortBy.empty() && sortBy.size() > reduceBy.size()) {
+ for (size_t c: xrange(reduceBy.size(), sortBy.size())) {
+ opt[c] = opt[c] || structType->GetMemberType(structType->GetMemberIndex(sortBy[c].first))->IsOptional();
+ }
+ }
+ }
+ }
+
+ auto dict = ctx.ProgramBuilder.ToHashedDict(values, true, [&](TRuntimeNode item) {
+ if (multiSection) {
+ return ctx.ProgramBuilder.VisitAll(item, [&](ui32, TRuntimeNode varItem) {
+ TVector<TRuntimeNode> keyItems;
+ for (size_t c: xrange(reduceBy.size())) {
+ auto key = ctx.ProgramBuilder.Member(varItem, reduceBy[c]);
+ if (opt[c] && !key.GetStaticType()->IsOptional()) {
+ key = ctx.ProgramBuilder.NewOptional(key);
+ }
+ keyItems.push_back(key);
+ }
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ });
+ }
+ TVector<TRuntimeNode> keyItems;
+ for (auto& column: reduceBy) {
+ keyItems.push_back(ctx.ProgramBuilder.Member(item, column));
+ }
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ }, [&](TRuntimeNode item) {
+ return item;
+ });
+
+ values = ctx.ProgramBuilder.DictPayloads(dict);
+ if (!sortBy.empty() && sortBy.size() > reduceBy.size()) {
+ sortBy.erase(sortBy.begin(), sortBy.begin() + reduceBy.size());
+ }
+
+ // sort partial lists
+ if (!sortBy.empty() || multiSection) {
+ size_t keySize = sortBy.size() + multiSection;
+
+ TRuntimeNode sortDirections;
+ if (keySize > 1) {
+ TVector<TRuntimeNode> tupleItems(keySize, ctx.ProgramBuilder.NewDataLiteral(true));
+ std::transform(sortBy.cbegin(), sortBy.cend(), tupleItems.begin(), [&ctx](const auto& it) { return ctx.ProgramBuilder.NewDataLiteral(it.second); });
+ sortDirections = ctx.ProgramBuilder.NewTuple(tupleItems);
+ } else {
+ sortDirections = ctx.ProgramBuilder.NewDataLiteral(sortBy.empty() || sortBy.front().second);
+ }
+
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode list) {
+ list = ctx.ProgramBuilder.Sort(list, sortDirections, [&](TRuntimeNode item) {
+ if (multiSection) {
+ return ctx.ProgramBuilder.VisitAll(item, [&](ui32 ndx, TRuntimeNode varItem) {
+ TVector<TRuntimeNode> keyItems;
+ for (size_t c: xrange(sortBy.size())) {
+ auto key = ctx.ProgramBuilder.Member(varItem, sortBy[c].first);
+ if (opt[c + reduceBy.size()] && !key.GetStaticType()->IsOptional()) {
+ key = ctx.ProgramBuilder.NewOptional(key);
+ }
+ keyItems.push_back(key);
+ }
+ keyItems.push_back(ctx.ProgramBuilder.NewDataLiteral(ndx));
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ });
+ }
+ TVector<TRuntimeNode> keyItems;
+ for (auto& column: sortBy) {
+ keyItems.push_back(ctx.ProgramBuilder.Member(item, column.first));
+ }
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ });
+
+ return list;
+ });
+ }
+
+ const auto arg = ytReduce.Reducer().Args().Arg(0).Raw();
+ if (NYql::HasSetting(ytReduce.Settings().Ref(), EYtSettingType::KeySwitch)) {
+ itemType = multiSection ?
+ NCommon::BuildType(ytReduce.Reducer().Ref(), GetSeqItemType(*arg->GetTypeAnn()), ctx.ProgramBuilder):
+ ctx.ProgramBuilder.NewStructType(itemType, YqlSysColumnKeySwitch, ctx.ProgramBuilder.NewDataType(EDataSlot::Bool));
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode list) {
+ list = ctx.ProgramBuilder.Enumerate(list);
+ list = ctx.ProgramBuilder.Map(list, [&](TRuntimeNode item) {
+ auto grpSwitch = ctx.ProgramBuilder.Equals(ctx.ProgramBuilder.Nth(item, 0), ctx.ProgramBuilder.NewDataLiteral(ui64(0)));
+ if (multiSection) {
+ return ctx.ProgramBuilder.VisitAll(ctx.ProgramBuilder.Nth(item, 1), [&](ui32 ndx, TRuntimeNode varItem) {
+ return ctx.ProgramBuilder.NewVariant(ctx.ProgramBuilder.AddMember(varItem, YqlSysColumnKeySwitch, grpSwitch), ndx, itemType);
+ });
+ }
+ return ctx.ProgramBuilder.AddMember(ctx.ProgramBuilder.Nth(item, 1), YqlSysColumnKeySwitch, grpSwitch);
+ });
+ return list;
+ });
+ }
+
+ TCallableBuilder callableBuilder(ctx.ProgramBuilder.GetTypeEnvironment(), "YtUngroupingList",
+ ctx.ProgramBuilder.NewListType(itemType));
+ callableBuilder.Add(values);
+ values = TRuntimeNode(callableBuilder.Build(), false);
+
+ values = arg->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Flow ?
+ ctx.ProgramBuilder.ToFlow(values) : ctx.ProgramBuilder.Iterator(values, {});
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytReduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()).GetKind())
+ values = ExpandFlow(values, ctx);
+
+ NCommon::TMkqlBuildContext innerCtx(ctx, {{arg, values}}, ytReduce.Reducer().Ref().UniqueId());
+
+ values = NCommon::MkqlBuildExpr(ytReduce.Reducer().Body().Ref(), innerCtx);
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytReduce.Reducer().Body().Ref().GetTypeAnn()).GetKind())
+ values = NarrowFlow(values, ytReduce, ctx);
+
+ // TODO: preserve sorting in reduce processing instead of sorting according to output spec
+ TYtOutTableInfo outTableInfo(ytReduce.Output().Item(0));
+ if (outTableInfo.RowSpec->IsSorted()) {
+ values = SortListBy(values, outTableInfo.RowSpec->GetForeignSort(), ctx);
+ }
+
+ auto res = BuildTableOutput(values, ctx);
+ return res;
+ });
+
+ compiler.AddCallable(TYtMapReduce::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+
+ TYtMapReduce ytMapReduce(&node);
+
+ bool hasMap = !ytMapReduce.Mapper().Maybe<TCoVoid>();
+ const auto itemType = BuildInputType(ytMapReduce.Input(), ctx);
+ const bool forceKeyColumns = HasRangesWithKeyColumns(ytMapReduce.Input().Ref());
+ TRuntimeNode values = BuildTableInput(itemType,
+ ytMapReduce.DataSink().Cluster().Value(), ytMapReduce.Input().Ref(), ctx,
+ THashSet<TString>{"num", "index"}, forceKeyColumns);
+
+ values = ApplyPathRangesAndSampling(values, itemType, ytMapReduce.Input().Ref(), ctx);
+
+ const auto outputItemType = BuildOutputType(ytMapReduce.Output(), ctx);
+ const size_t outputsCount = ytMapReduce.Output().Ref().ChildrenSize();
+
+ size_t mapDirectOutputsCount = 0;
+ TRuntimeNode mapDirectOutputs;
+ if (hasMap) {
+ const auto& mapper = ytMapReduce.Mapper().Cast<TCoLambda>();
+ if (const auto arg = mapper.Args().Arg(0).Raw(); arg != mapper.Body().Raw()) {
+ values = arg->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Flow ?
+ ctx.ProgramBuilder.ToFlow(values) : ctx.ProgramBuilder.Iterator(values, {});
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytMapReduce.Mapper().Cast<TCoLambda>().Args().Arg(0).Ref().GetTypeAnn()).GetKind())
+ values = ExpandFlow(values, ctx);
+
+ NCommon::TMkqlBuildContext innerCtx(ctx, {{arg, values}}, ytMapReduce.Mapper().Ref().UniqueId());
+
+ const auto& body = ytMapReduce.Mapper().Cast<TCoLambda>().Body().Ref();
+ values = NCommon::MkqlBuildExpr(body, innerCtx);
+
+ const auto& mapOutItemType = GetSeqItemType(*body.GetTypeAnn());
+ if (const auto mapOutputTypeSetting = NYql::GetSetting(ytMapReduce.Settings().Ref(), EYtSettingType::MapOutputType)) {
+ if (ETypeAnnotationKind::Multi == mapOutItemType.GetKind()) {
+ values = NarrowFlow(values, *AS_TYPE(TStructType, NCommon::BuildType(body, *mapOutputTypeSetting->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType(), ctx.ProgramBuilder)), ctx);
+ }
+ }
+
+ values = ToList(values, ctx);
+
+ if (mapOutItemType.GetKind() == ETypeAnnotationKind::Variant) {
+ auto tupleType = mapOutItemType.Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ YQL_ENSURE(tupleType->GetSize() > 0);
+ mapDirectOutputsCount = tupleType->GetSize() - 1;
+ YQL_ENSURE(mapDirectOutputsCount < outputsCount);
+
+ values = ctx.ProgramBuilder.Collect(values);
+
+ mapDirectOutputs = ctx.ProgramBuilder.OrderedFlatMap(values, [&](TRuntimeNode mapOut) {
+ return ctx.ProgramBuilder.VisitAll(mapOut, [&](ui32 index, TRuntimeNode varitem) {
+ if (index == 0) {
+ return ctx.ProgramBuilder.NewEmptyOptional(ctx.ProgramBuilder.NewOptionalType(outputItemType));
+ }
+ return ctx.ProgramBuilder.NewOptional(ctx.ProgramBuilder.NewVariant(varitem, index - 1, outputItemType));
+ });
+ });
+
+ auto toReduceType = NCommon::BuildType(body, *tupleType->GetItems().front(), ctx.ProgramBuilder);
+ values = ctx.ProgramBuilder.OrderedFlatMap(values, [&](TRuntimeNode mapOut) {
+ return ctx.ProgramBuilder.VisitAll(mapOut, [&](ui32 index, TRuntimeNode varitem) {
+ if (index == 0) {
+ return ctx.ProgramBuilder.NewOptional(varitem);
+ }
+ return ctx.ProgramBuilder.NewEmptyOptional(ctx.ProgramBuilder.NewOptionalType(toReduceType));
+ });
+ });
+ }
+ }
+ }
+
+ TVector<TString> reduceBy = NYql::GetSettingAsColumnList(ytMapReduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ auto dict = ctx.ProgramBuilder.ToHashedDict(values, true, [&](TRuntimeNode item) {
+ TVector<TRuntimeNode> keyItems;
+ for (auto& column: reduceBy) {
+ keyItems.push_back(ctx.ProgramBuilder.Member(item, column));
+ }
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ }, [&](TRuntimeNode item) {
+ return item;
+ });
+
+ values = ctx.ProgramBuilder.DictPayloads(dict);
+ TVector<std::pair<TString, bool>> sortBy = NYql::GetSettingAsColumnPairList(ytMapReduce.Settings().Ref(), EYtSettingType::SortBy);
+ TVector<TString> filterBy = NYql::GetSettingAsColumnList(ytMapReduce.Settings().Ref(), EYtSettingType::ReduceFilterBy);
+ if (!sortBy.empty() && sortBy.size() > reduceBy.size()) {
+ // sort partial lists
+ sortBy.erase(sortBy.begin(), sortBy.begin() + reduceBy.size());
+
+ TRuntimeNode sortDirections;
+ if (sortBy.size() > 1) {
+ TVector<TRuntimeNode> tupleItems(sortBy.size(), ctx.ProgramBuilder.NewDataLiteral(true));
+ std::transform(sortBy.cbegin(), sortBy.cend(), tupleItems.begin(), [&ctx](const auto& it) { return ctx.ProgramBuilder.NewDataLiteral(it.second); });
+ sortDirections = ctx.ProgramBuilder.NewTuple(tupleItems);
+ } else {
+ sortDirections = ctx.ProgramBuilder.NewDataLiteral(sortBy.front().second);
+ }
+
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode list) {
+ list = ctx.ProgramBuilder.Sort(list, sortDirections, [&](TRuntimeNode item) {
+ TVector<TRuntimeNode> keyItems;
+ for (auto& column: sortBy) {
+ keyItems.push_back(ctx.ProgramBuilder.Member(item, column.first));
+ }
+ return keyItems.size() == 1
+ ? keyItems.front()
+ : ctx.ProgramBuilder.NewTuple(keyItems);
+ });
+
+ if (NYql::HasSetting(ytMapReduce.Settings().Ref(), EYtSettingType::ReduceFilterBy)) {
+ list = ctx.ProgramBuilder.OrderedMap(list, [&filterBy, &ctx] (TRuntimeNode item) {
+ TRuntimeNode res = ctx.ProgramBuilder.NewEmptyStruct();
+ for (auto& column: filterBy) {
+ res = ctx.ProgramBuilder.AddMember(res, column, ctx.ProgramBuilder.Member(item, column));
+ }
+ return res;
+ });
+ }
+
+ return list;
+ });
+ }
+ else if (!filterBy.empty()) {
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode list) {
+ list = ctx.ProgramBuilder.OrderedMap(list, [&filterBy, &ctx] (TRuntimeNode item) {
+ TRuntimeNode res = ctx.ProgramBuilder.NewEmptyStruct();
+ for (auto& column: filterBy) {
+ res = ctx.ProgramBuilder.AddMember(res, column, ctx.ProgramBuilder.Member(item, column));
+ }
+ return res;
+ });
+
+ return list;
+ });
+ }
+
+ const auto arg = ytMapReduce.Reducer().Args().Arg(0).Raw();
+ const auto reduceInputTypeSetting = NYql::GetSetting(ytMapReduce.Settings().Ref(), EYtSettingType::ReduceInputType);
+ const auto reduceInputType = reduceInputTypeSetting ? reduceInputTypeSetting->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType() : &GetSeqItemType(*arg->GetTypeAnn());
+ TType* mkqlItemType = NCommon::BuildType(ytMapReduce.Reducer().Ref(), *reduceInputType, ctx.ProgramBuilder);
+ if (NYql::HasSetting(ytMapReduce.Settings().Ref(), EYtSettingType::KeySwitch)) {
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode list) {
+ list = ctx.ProgramBuilder.Enumerate(list);
+ list = ctx.ProgramBuilder.Map(list, [&](TRuntimeNode item) {
+ auto grpSwitch = ctx.ProgramBuilder.Equals(ctx.ProgramBuilder.Nth(item, 0), ctx.ProgramBuilder.NewDataLiteral(ui64(0)));
+ return ctx.ProgramBuilder.AddMember(ctx.ProgramBuilder.Nth(item, 1), YqlSysColumnKeySwitch, grpSwitch);
+ });
+ return list;
+ });
+ }
+
+ TCallableBuilder callableBuilder(ctx.ProgramBuilder.GetTypeEnvironment(), "YtUngroupingList",
+ ctx.ProgramBuilder.NewListType(mkqlItemType));
+ callableBuilder.Add(values);
+ values = TRuntimeNode(callableBuilder.Build(), false);
+
+ values = arg->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Flow ?
+ ctx.ProgramBuilder.ToFlow(values) : ctx.ProgramBuilder.Iterator(values, {});
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*arg->GetTypeAnn()).GetKind())
+ values = ExpandFlow(values, ctx);
+
+ NCommon::TMkqlBuildContext innerCtx(ctx, {{arg, values}}, ytMapReduce.Reducer().Ref().UniqueId());
+
+ values = NCommon::MkqlBuildExpr(ytMapReduce.Reducer().Body().Ref(), innerCtx);
+
+ const auto& reduceOutItemType = GetSeqItemType(*ytMapReduce.Reducer().Body().Ref().GetTypeAnn());
+
+ if (ETypeAnnotationKind::Multi == reduceOutItemType.GetKind()) {
+ auto type = ytMapReduce.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().back()->Cast<TListExprType>()->GetItemType();
+ if (type->GetKind() == ETypeAnnotationKind::Variant) {
+ type = type->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>()->GetItems().back();
+ }
+ values = NarrowFlowOutput(ytMapReduce.Pos(), values, type->Cast<TStructExprType>(), ctx);
+ }
+
+ if (mapDirectOutputsCount > 0) {
+ // remap reduce output to new indexes
+ values = ctx.ProgramBuilder.OrderedMap(values, [&](TRuntimeNode redueOutItem) {
+ if (reduceOutItemType.GetKind() == ETypeAnnotationKind::Variant) {
+ return ctx.ProgramBuilder.VisitAll(redueOutItem, [&](ui32 idx, TRuntimeNode item) {
+ return ctx.ProgramBuilder.NewVariant(item, idx + mapDirectOutputsCount, outputItemType);
+ });
+ }
+ return ctx.ProgramBuilder.NewVariant(redueOutItem, mapDirectOutputsCount, outputItemType);
+ });
+
+ // prepend with map output
+ values = ctx.ProgramBuilder.Extend(mapDirectOutputs, ToList(values, ctx));
+ }
+
+ auto res = BuildTableOutput(values, ctx);
+ return res;
+ });
+
+ compiler.AddCallable(TYtFill::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+
+ TYtFill ytFill(&node);
+
+ auto values = NCommon::MkqlBuildExpr(ytFill.Content().Body().Ref(), ctx);
+
+ if (ETypeAnnotationKind::Multi == GetSeqItemType(*ytFill.Content().Body().Ref().GetTypeAnn()).GetKind())
+ values = NarrowFlow(values, ytFill, ctx);
+
+ auto res = BuildTableOutput(values, ctx);
+ return res;
+ });
+
+ compiler.AddCallable("Pull",
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ TPull pull(&node);
+ auto clusterName = GetClusterName(pull.Input());
+ const auto itemType = NCommon::BuildType(pull.Input().Ref(), *pull.Input().Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder);
+ if (auto out = pull.Input().Maybe<TYtOutput>()) {
+
+ return BuildTableInput(
+ itemType,
+ clusterName, pull.Input().Ref(), ctx, THashSet<TString>{}, false);
+
+ } else {
+ auto read = pull.Input().Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+
+ const bool forceKeyColumns = HasRangesWithKeyColumns(read.Cast().Input().Ref());
+ TRuntimeNode values = BuildTableInput(
+ itemType,
+ clusterName, read.Cast().Input().Ref(), ctx,
+ THashSet<TString>{"num", "index"}, forceKeyColumns);
+
+ values = ApplyPathRangesAndSampling(values, itemType, read.Cast().Input().Ref(), ctx);
+
+ return values;
+ }
+ });
+} // RegisterYtFileMkqlCompilers
+
+
+void RegisterDqYtFileMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler) {
+ compiler.OverrideCallable(TDqReadWideWrap::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ const auto wrapper = TDqReadWrapBase(&node);
+ if (wrapper.Input().Maybe<TYtReadTable>().IsValid()) {
+ auto ytRead = wrapper.Input().Cast<TYtReadTable>();
+ auto cluster = TString{ytRead.DataSource().Cluster().Value()};
+ const auto outputType = NCommon::BuildType(wrapper.Ref(),
+ *ytRead.Input().Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[0]->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder);
+
+ auto values = BuildTableContentCall("YtTableInputFile", outputType, cluster,
+ ytRead.Input().Ref(), Nothing(), ctx, false, THashSet<TString>{"num", "index"});
+ values = ApplyPathRangesAndSampling(values, outputType, ytRead.Input().Ref(), ctx);
+
+ return ExpandFlow(ctx.ProgramBuilder.ToFlow(values), ctx);
+ }
+
+ return TRuntimeNode();
+ });
+
+ compiler.OverrideCallable(TYtDqWideWrite::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ const auto wideWrite = TYtDqWideWrite(&node);
+
+ auto values = NCommon::MkqlBuildExpr(wideWrite.Input().Ref(), ctx);
+
+ TYtOutTable table{GetSetting(wideWrite.Settings().Ref(), "outTable")->Child(1)};
+ auto inputItemType = NCommon::BuildType(wideWrite.Input().Ref(), GetSeqItemType(*table.Ref().GetTypeAnn()), ctx.ProgramBuilder);
+
+ auto structType = AS_TYPE(TStructType, inputItemType);
+ values = NarrowFlow(values, *structType, ctx);
+ values = ctx.ProgramBuilder.Map(values, [&](TRuntimeNode item) {
+ return BuildDqWrite(item, table.Name().Value(), ctx);
+ });
+ return values;
+ });
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.h b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.h
new file mode 100644
index 0000000000..90a884062f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_mkql_compiler.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+
+#include <utility>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::TRuntimeNode BuildTableOutput(NKikimr::NMiniKQL::TRuntimeNode list, NCommon::TMkqlBuildContext& ctx);
+NKikimr::NMiniKQL::TRuntimeNode BuildRuntimeTableInput(TStringBuf callName, NKikimr::NMiniKQL::TType* outItemType, TStringBuf clusterName,
+ TStringBuf tableName, TStringBuf spec, bool isTemp, NCommon::TMkqlBuildContext& ctx);
+
+NKikimr::NMiniKQL::TRuntimeNode SortListBy(NKikimr::NMiniKQL::TRuntimeNode list, const TVector<std::pair<TString, bool>>& sortBy, NCommon::TMkqlBuildContext& ctx);
+
+void RegisterYtFileMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler);
+void RegisterDqYtFileMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler);
+
+}
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.cpp b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.cpp
new file mode 100644
index 0000000000..c26d3425d8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.cpp
@@ -0,0 +1,88 @@
+#include "yql_yt_file_services.h"
+
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+
+#include <util/system/guard.h>
+#include <util/system/fs.h>
+#include <util/generic/yexception.h>
+#include <util/folder/path.h>
+#include <util/string/cast.h>
+#include <util/random/random.h>
+#include <util/stream/file.h>
+#include <util/stream/input.h>
+
+#include <algorithm>
+#include <iterator>
+
+namespace NYql::NFile {
+
+TYtFileServices::~TYtFileServices() {
+ for (auto& x: Locks) {
+ try {
+ NFs::Remove(x.second);
+ NFs::Remove(x.second + ".attr");
+ } catch (...) {
+ }
+ }
+}
+
+TString TYtFileServices::GetTablePath(TStringBuf cluster, TStringBuf table, bool isTemp, bool noLocks) {
+ if (isTemp) {
+ return TString(TFsPath(TmpDir) / TString(table.substr(4)).append(TStringBuf(".tmp")));
+ }
+
+ auto fullTableName = TString(YtProviderName).append('.').append(cluster).append('.').append(table);
+ if (!noLocks) {
+ auto guard = Guard(Mutex);
+ if (auto p = Locks.FindPtr(fullTableName)) {
+ return *p;
+ }
+ }
+ if (auto p = TablesMapping.FindPtr(fullTableName)) {
+ return *p;
+ }
+ ythrow yexception() << "Table not found: " << cluster << '.' << table;
+}
+
+void TYtFileServices::LockPath(const TString& path, const TString& fullTableName) {
+ auto name = TFsPath(path).GetName();
+ auto lockPath = TFsPath(TmpDir) / (name + ToString(RandomNumber<float>()));
+ while (true) {
+ try {
+ TUnbufferedFileInput src(path);
+ TUnbufferedFileOutput dst(TFile(lockPath, CreateNew | WrOnly | Seq));
+ TransferData(&src, &dst);
+ break;
+ } catch (const TFileError& e) {
+ lockPath = TFsPath(TmpDir) / (name + ToString(RandomNumber<float>()));
+ }
+ }
+ if (NFs::Exists(path + ".attr")) {
+ NFs::Copy(path + ".attr", lockPath.GetPath() + ".attr");
+ }
+ auto guard = Guard(Mutex);
+ if (auto p = Locks.FindPtr(fullTableName)) {
+ try {
+ NFs::Remove(*p);
+ NFs::Remove(*p + ".attr");
+ } catch (...) {
+ }
+ }
+ Locks[fullTableName] = lockPath;
+}
+
+void TYtFileServices::PushTableContent(const TString& path, const TString& content) {
+ auto guard = Guard(Mutex);
+ Contents.emplace(path, content);
+}
+
+std::vector<TString> TYtFileServices::GetTableContent(const TString& path) {
+ auto guard = Guard(Mutex);
+ auto range = Contents.equal_range(path);
+ std::vector<TString> res;
+ std::transform(range.first, range.second, std::back_inserter(res), [](const auto& p) { return p.second; });
+ Contents.erase(path);
+ return res;
+}
+
+} // NYql::NFile
diff --git a/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h
new file mode 100644
index 0000000000..43cc413f19
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/file/yql_yt_file_services.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/core/file_storage/file_storage.h>
+#include <ydb/library/yql/core/file_storage/proto/file_storage.pb.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/ptr.h>
+#include <util/system/spinlock.h>
+#include <util/folder/dirut.h>
+
+#include <vector>
+#include <unordered_map>
+
+namespace NYql::NFile {
+
+class TYtFileServices: public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<TYtFileServices>;
+ ~TYtFileServices();
+
+ static TPtr Make(const NKikimr::NMiniKQL::IFunctionRegistry* registry, const THashMap<TString, TString>& mapping = {},
+ TFileStoragePtr fileStorage = {}, const TString& tmpDir = {}, bool keepTempTables = false)
+ {
+ return new TYtFileServices(registry, mapping, fileStorage, tmpDir.empty() ? GetSystemTempDir() : tmpDir, keepTempTables);
+ }
+
+ const NKikimr::NMiniKQL::IFunctionRegistry* GetFunctionRegistry() const {
+ return FunctionRegistry;
+ }
+
+ const TString& GetTmpDir() const {
+ return TmpDir;
+ }
+
+ THashMap<TString, TString>& GetTablesMapping() {
+ return TablesMapping;
+ }
+
+ bool GetKeepTempTables() const {
+ return KeepTempTables;
+ }
+
+ TString GetTablePath(TStringBuf cluster, TStringBuf table, bool isTemp, bool noLocks = false);
+
+ void LockPath(const TString& path, const TString& fullTableName);
+
+ void PushTableContent(const TString& path, const TString& content);
+ std::vector<TString> GetTableContent(const TString& path);
+
+ TFileStoragePtr GetFileStorage() const {
+ return FileStorage;
+ }
+
+private:
+ TYtFileServices(const NKikimr::NMiniKQL::IFunctionRegistry* registry, const THashMap<TString, TString>& mapping, TFileStoragePtr fileStorage, const TString& tmpDir, bool keepTempTables)
+ : FunctionRegistry(registry)
+ , TablesMapping(mapping)
+ , TmpDir(tmpDir)
+ , KeepTempTables(keepTempTables)
+ {
+ FileStorage = fileStorage;
+ if (!FileStorage) {
+ TFileStorageConfig params;
+ FileStorage = CreateFileStorage(params);
+ }
+ }
+
+ TFileStoragePtr FileStorage;
+ const NKikimr::NMiniKQL::IFunctionRegistry* FunctionRegistry;
+ THashMap<TString, TString> TablesMapping; // [cluster].[name] -> [file path]
+ TString TmpDir;
+ bool KeepTempTables;
+
+ std::unordered_multimap<TString, TString> Contents; // path -> pickled content
+ THashMap<TString, TString> Locks;
+ TAdaptiveLock Mutex;
+};
+
+} // NYql::NFile
diff --git a/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp b/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
new file mode 100644
index 0000000000..7a93795b64
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/query_cache.cpp
@@ -0,0 +1,379 @@
+#include "query_cache.h"
+#include "yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <util/generic/algorithm.h>
+#include <util/string/hex.h>
+#include <util/system/fs.h>
+#include <util/system/error.h>
+
+#include <errno.h>
+
+namespace NYql {
+
+using namespace NYT;
+
+namespace {
+ bool IsRace(const NYT::TYtError& e) {
+ if (e.ContainsErrorCode(NClusterErrorCodes::NTransactionClient::NoSuchTransaction)) {
+ return false;
+ }
+
+ return e.ContainsErrorCode(NClusterErrorCodes::NYTree::ResolveError) ||
+ e.ContainsErrorCode(NClusterErrorCodes::NCypressClient::ConcurrentTransactionLockConflict);
+ }
+}
+
+TFsQueryCacheItem::TFsQueryCacheItem(const TYtSettings& config, const TString& cluster, const TString& tmpDir,
+ const TString& hash, const TString& outputTablePath)
+ : TQueryCacheItemBase<TFsQueryCacheItem>(hash.empty() ? EQueryCacheMode::Disable : config.QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable))
+ , OutputTablePaths(1, outputTablePath)
+ , CachedPaths(1, TFsPath(tmpDir) / "query_cache" / cluster / (HexEncode(hash) + ".tmp"))
+{
+}
+
+TFsQueryCacheItem::TFsQueryCacheItem(const TYtSettings& config, const TString& cluster, const TString& tmpDir,
+ const TString& hash, const TVector<TString>& outputTablePaths)
+ : TQueryCacheItemBase<TFsQueryCacheItem>(hash.empty() ? EQueryCacheMode::Disable : config.QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable))
+ , OutputTablePaths(outputTablePaths)
+{
+ for (size_t i = 0; i < outputTablePaths.size(); ++i) {
+ auto outputHash = (THashBuilder() << hash << i).Finish();
+ CachedPaths.push_back(TFsPath(tmpDir) / "query_cache" / cluster / (HexEncode(outputHash) + ".tmp"));
+ }
+}
+
+NThreading::TFuture<bool> TFsQueryCacheItem::LookupImpl(const TAsyncQueue::TPtr& /*queue*/) {
+ for (auto cachePath: CachedPaths) {
+ Cerr << "Check path: " << cachePath << "\n";
+ if (!cachePath.Exists()) {
+ return NThreading::MakeFuture<bool>(false);
+ }
+ }
+
+ for (size_t i = 0; i < CachedPaths.size(); ++i) {
+ Cerr << "Copy from: " << CachedPaths.at(i) << " to " << OutputTablePaths.at(i) << "\n";
+ NFs::HardLinkOrCopy(CachedPaths.at(i), OutputTablePaths.at(i));
+ NFs::HardLinkOrCopy(CachedPaths.at(i).GetPath() + ".attr", OutputTablePaths.at(i) + ".attr");
+ }
+
+ return NThreading::MakeFuture<bool>(true);
+}
+
+void TFsQueryCacheItem::StoreImpl() {
+ for (size_t i = 0; i < CachedPaths.size(); ++i) {
+ Cerr << "Make dir: " << CachedPaths.at(i).Parent() << "\n";
+ CachedPaths.at(i).Parent().MkDirs();
+ if (CachedPaths.at(i).Exists()) {
+ if (Mode != EQueryCacheMode::Refresh) {
+ continue;
+ }
+
+ Cerr << "Remove: " << CachedPaths.at(i) << "\n";
+ if (!NFs::Remove(CachedPaths.at(i)) && LastSystemError() != ENOENT) {
+ ythrow TSystemError();
+ }
+
+ if (!NFs::Remove(CachedPaths.at(i).GetPath() + ".attr") && LastSystemError() != ENOENT) {
+ ythrow TSystemError();
+ }
+ }
+
+ Cerr << "Copy from: " << OutputTablePaths.at(i) << " to " << CachedPaths.at(i) << "\n";
+ if (!NFs::HardLink(OutputTablePaths.at(i), CachedPaths.at(i))) {
+ if (LastSystemError() != EEXIST || Mode != EQueryCacheMode::Normal) {
+ ythrow TSystemError();
+ }
+ }
+
+ if (!NFs::HardLink(OutputTablePaths.at(i) + ".attr", CachedPaths.at(i).GetPath() + ".attr")) {
+ if (LastSystemError() != EEXIST || Mode != EQueryCacheMode::Normal) {
+ ythrow TSystemError();
+ }
+ }
+ }
+}
+
+TYtQueryCacheItem::TYtQueryCacheItem(EQueryCacheMode mode, const TTransactionCache::TEntry::TPtr& entry, const TString& hash,
+ const TVector<TString>& dstTables, const TVector<NYT::TNode>& dstSpecs,
+ const TString& userName, const TString& tmpFolder, const NYT::TNode& mergeSpec,
+ const NYT::TNode& tableAttrs, ui64 chunkLimit, bool useExpirationTimeout, bool useMultiSet,
+ const std::pair<TString, TString>& logCtx)
+ : TQueryCacheItemBase<TYtQueryCacheItem>(hash.empty() ? EQueryCacheMode::Disable : mode)
+ , Entry(entry)
+ , DstTables(dstTables)
+ , DstSpecs(dstSpecs)
+ , CachePath(GetCachePath(userName, tmpFolder))
+ , ChunkLimit(chunkLimit)
+ , UseExpirationTimeout(useExpirationTimeout)
+ , UseMultiSet(useMultiSet)
+ , LogCtx(logCtx)
+ , MergeSpec(mergeSpec)
+ , TableAttrs(tableAttrs)
+{
+ if (!hash.empty()) {
+ for (size_t i = 0; i < dstTables.size(); ++i) {
+ auto outputHash = (THashBuilder() << hash << i).Finish();
+ auto path = MakeCachedPath(outputHash);
+ CachedPaths.push_back(path);
+ SortedCachedPaths[path] = i;
+ }
+ }
+}
+
+TString TYtQueryCacheItem::GetCachePath(const TString& userName, const TString& tmpFolder) {
+ auto path = tmpFolder;
+ if (path.empty()) {
+ path = "tmp/yql/" + userName;
+ }
+
+ if (!path.EndsWith('/')) {
+ path += "/";
+ }
+
+ return path + "query_cache";
+}
+
+TString TYtQueryCacheItem::MakeCachedPath(const TString& hash) {
+ YQL_ENSURE(hash.size() == 32);
+
+ auto hex = HexEncode(hash);
+ return TString::Join(CachePath, "/", hex.substr(0, 2), "/", hex.substr(2, 2), "/", hex);
+}
+
+NThreading::TFuture<bool> TYtQueryCacheItem::LookupImpl(const TAsyncQueue::TPtr& queue) {
+ if (Entry->CacheTxId) {
+ return NThreading::MakeFuture<bool>(false);
+ }
+
+ Entry->CreateDefaultTmpFolder();
+ CreateParents(DstTables, Entry->CacheTx);
+
+ NThreading::TFuture futureLock = NThreading::MakeFuture();
+ if (Mode == EQueryCacheMode::Refresh || Mode == EQueryCacheMode::Normal) {
+ CreateParents(CachedPaths, Entry->CacheTx);
+ // get a lock for all paths before return
+ if (!LockTx) {
+ LockTx = Entry->Tx->StartTransaction(TStartTransactionOptions().Attributes(Entry->TransactionSpec));
+ }
+ for (const auto& sortedEntry : SortedCachedPaths) {
+ TString cachedPath = CachedPaths[sortedEntry.second];
+ futureLock = futureLock.Apply([cachedPath, lockTx = LockTx, logCtx = LogCtx](const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ if (f.HasException()) {
+ return f;
+ }
+ auto pos = cachedPath.rfind("/");
+ auto dir = cachedPath.substr(0, pos);
+ auto childKey = cachedPath.substr(pos + 1) + ".lock";
+ YQL_CLOG(INFO, ProviderYt) << "Wait for " << cachedPath;
+ for (ui32 retriesLeft = 10; retriesLeft != 0; --retriesLeft) {
+ try {
+ return lockTx->Lock(dir, NYT::ELockMode::LM_SHARED, NYT::TLockOptions()
+ .Waitable(true).ChildKey(childKey))->GetAcquiredFuture();
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Retry, retriesLeft: " << retriesLeft;
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Wait complete";
+ return NThreading::MakeFuture();
+ });
+ }
+ }
+
+ return futureLock.Apply([queue, cachedPaths = CachedPaths, dstTables = DstTables, entry = Entry,
+ useExpirationTimeout = UseExpirationTimeout, logCtx = LogCtx](const auto& f) {
+ f.GetValue();
+ return queue->Async([=]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ bool hit = true;
+ try {
+ for (size_t i = 0; i < cachedPaths.size(); ++i) {
+ YQL_CLOG(INFO, ProviderYt) << "Check path: " << cachedPaths.at(i);
+ entry->Tx->Copy(cachedPaths.at(i), dstTables.at(i), TCopyOptions().Force(true));
+ }
+ } catch (const TErrorResponse& e) {
+ if (!e.IsResolveError()) {
+ throw;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Query cache miss";
+ hit = false;
+ }
+ if (!hit) {
+ return false;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Query cache hit";
+ if (useExpirationTimeout) {
+ return true;
+ }
+ for (const auto& cachedPath: cachedPaths) {
+ try {
+ entry->CacheTx->Set(cachedPath + "/@touched", TNode(true));
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+ try {
+ entry->CacheTx->Set(cachedPath + "/@expiration_time",
+ TNode((Now() + entry->CacheTtl).ToStringUpToSeconds()));
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+ }
+ return true;
+ });
+ });
+}
+
+void TYtQueryCacheItem::StoreImpl() {
+ if (Entry->CacheTxId) {
+ return;
+ }
+
+ CreateParents(CachedPaths, Entry->CacheTx);
+ try {
+ Entry->CacheTx->Set(CachePath + "/@prune_empty_map_nodes", TNode(true));
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+
+ NYT::ITransactionPtr nestedReadTx;
+ NYT::ITransactionPtr nestedWriteTx;
+
+ auto handle = [&](const NYT::TYtError& e) {
+ if (nestedReadTx) {
+ nestedReadTx->Abort();
+ }
+ if (nestedWriteTx) {
+ nestedWriteTx->Abort();
+ }
+
+ if (!IsRace(e) || Mode == EQueryCacheMode::Refresh) {
+ throw;
+ }
+ };
+
+ try {
+ nestedReadTx = Entry->Tx->StartTransaction(TStartTransactionOptions().Attributes(Entry->TransactionSpec));
+ nestedWriteTx = Entry->CacheTx->StartTransaction(TStartTransactionOptions().Attributes(Entry->TransactionSpec));
+
+ TVector<NThreading::TFuture<void>> futures;
+ futures.reserve(CachedPaths.size());
+ for (size_t i = 0; i < CachedPaths.size(); ++i) {
+ YQL_CLOG(INFO, ProviderYt) << "Store to query cache, from " << DstTables.at(i) << " into " << CachedPaths.at(i);
+ if (ChunkLimit) {
+ NYT::TNode attrs = nestedReadTx->Get(DstTables.at(i) + "&/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute("chunk_count")
+ .AddAttribute("compression_codec")
+ .AddAttribute("erasure_codec")
+ .AddAttribute("optimize_for")
+ .AddAttribute("media")
+ .AddAttribute("primary_medium")
+ .AddAttribute("schema")
+ ));
+
+ auto inputChunks = attrs["chunk_count"].AsInt64();
+ if ((ui64)inputChunks <= ChunkLimit) {
+ YQL_CLOG(INFO, ProviderYt) << "Use Concatenate to store cache (chunks: " <<
+ inputChunks << ", limit: " << ChunkLimit << ")";
+
+ attrs.AsMap().erase("chunk_count");
+ nestedWriteTx->Create(CachedPaths.at(i), NYT::NT_TABLE, TCreateOptions().Force(true).Attributes(attrs));
+ nestedWriteTx->Concatenate(
+ { NYT::TRichYPath(DstTables.at(i)).TransactionId(nestedReadTx->GetId()) },
+ NYT::TRichYPath(CachedPaths.at(i)),
+ TConcatenateOptions()
+ );
+ continue;
+ } else {
+ YQL_CLOG(INFO, ProviderYt) << "Use Merge to store cache (chunks: " <<
+ inputChunks << ", limit: " << ChunkLimit << ")";
+ }
+ }
+
+ auto operation = nestedWriteTx->Merge(TMergeOperationSpec()
+ .AddInput(NYT::TRichYPath(DstTables.at(i))
+ .TransactionId(nestedReadTx->GetId()))
+ .Output(CachedPaths.at(i))
+ .Mode(EMergeMode::MM_ORDERED),
+ TOperationOptions().Spec(MergeSpec).Wait(false));
+ futures.push_back(operation->Watch());
+ }
+
+ auto all = NThreading::WaitExceptionOrAll(futures);
+ all.GetValueSync();
+
+ nestedReadTx->Abort();
+ nestedReadTx = {};
+
+ nestedWriteTx->Commit();
+ if (LockTx) {
+ LockTx->Abort();
+ }
+
+ if (UseExpirationTimeout) {
+ TableAttrs["expiration_timeout"] = TNode(Entry->CacheTtl.MilliSeconds());
+ } else {
+ TableAttrs["expiration_time"] = TNode((Now() + Entry->CacheTtl).ToStringUpToSeconds());
+ }
+
+ for (size_t i = 0; i < CachedPaths.size(); ++i) {
+ SetTableAttrs(DstSpecs[i], CachedPaths[i]);
+ }
+ } catch (const TErrorResponse& e) {
+ handle(e.GetError());
+ } catch (const NYT::TOperationFailedError& e) {
+ handle(e.GetError());
+ }
+}
+
+void TYtQueryCacheItem::SetTableAttrs(const NYT::TNode& spec, const TString& cachedPath) {
+ NYT::TNode attrs = spec;
+ NYT::MergeNodes(attrs, TableAttrs);
+ if (UseMultiSet) {
+ try {
+ Entry->CacheTx->MultisetAttributes(cachedPath + "/@", attrs.AsMap(), NYT::TMultisetAttributesOptions());
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+ } else {
+ auto batchSet = Entry->CacheTx->CreateBatchRequest();
+ TVector<NThreading::TFuture<void>> batchSetRes;
+ for (auto& attr : attrs.AsMap()) {
+ batchSetRes.push_back(batchSet->Set(TStringBuilder() << cachedPath << "/@" << attr.first, attr.second));
+ }
+
+ batchSet->ExecuteBatch();
+ for (auto& x : batchSetRes) {
+ try {
+ x.GetValueSync();
+ } catch (const TErrorResponse& e) {
+ if (!IsRace(e.GetError())) {
+ throw;
+ }
+ }
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/query_cache.h b/ydb/library/yql/providers/yt/gateway/lib/query_cache.h
new file mode 100644
index 0000000000..0a98f44164
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/query_cache.h
@@ -0,0 +1,112 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h>
+#include <ydb/library/yql/utils/threading/async_queue.h>
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/folder/path.h>
+#include <util/thread/pool.h>
+
+namespace NYql {
+
+template <class TDerived>
+class TQueryCacheItemBase {
+public:
+ TQueryCacheItemBase(EQueryCacheMode mode)
+ : Mode(mode)
+ {
+ }
+
+ // returns true if cache was used
+ bool Lookup(const TAsyncQueue::TPtr& queue) {
+ return LookupAsync(queue).GetValueSync();
+ }
+
+ [[nodiscard]]
+ NThreading::TFuture<bool> LookupAsync(const TAsyncQueue::TPtr& queue) {
+ if (Mode == EQueryCacheMode::Disable || Mode == EQueryCacheMode::Refresh) {
+ return NThreading::MakeFuture<bool>(false);
+ }
+ if (Found) {
+ return *Found;
+ }
+
+ Found = static_cast<TDerived*>(this)->LookupImpl(queue);
+ return *Found;
+
+ }
+
+ void Store() {
+ if (Mode == EQueryCacheMode::Disable || Mode == EQueryCacheMode::Readonly) {
+ return;
+ }
+ static_cast<TDerived*>(this)->StoreImpl();
+ }
+
+ bool Hit() const {
+ return Found && Found->GetValueSync();
+ }
+
+protected:
+ const EQueryCacheMode Mode;
+
+private:
+ TMaybe<NThreading::TFuture<bool>> Found;
+};
+
+
+class TFsQueryCacheItem: public TQueryCacheItemBase<TFsQueryCacheItem> {
+public:
+ TFsQueryCacheItem(const TYtSettings& config, const TString& cluster, const TString& tmpDir, const TString& hash,
+ const TString& outputTablePath);
+ TFsQueryCacheItem(const TYtSettings& config, const TString& cluster, const TString& tmpDir, const TString& hash,
+ const TVector<TString>& outputTablePaths);
+
+ // returns true if cache was used
+ NThreading::TFuture<bool> LookupImpl(const TAsyncQueue::TPtr& queue);
+ void StoreImpl();
+
+private:
+ const TVector<TString> OutputTablePaths;
+ TVector<TFsPath> CachedPaths;
+};
+
+class TYtQueryCacheItem: public TQueryCacheItemBase<TYtQueryCacheItem> {
+public:
+ TYtQueryCacheItem(EQueryCacheMode mode, const TTransactionCache::TEntry::TPtr& entry, const TString& hash,
+ const TVector<TString>& dstTables, const TVector<NYT::TNode>& dstSpecs, const TString& userName,
+ const TString& tmpFolder, const NYT::TNode& mergeSpec,
+ const NYT::TNode& tableAttrs, ui64 chunkLimit, bool useExpirationTimeout, bool useMultiSet,
+ const std::pair<TString, TString>& logCtx);
+
+ // returns true if cache was used
+ NThreading::TFuture<bool> LookupImpl(const TAsyncQueue::TPtr& queue);
+ void StoreImpl();
+
+private:
+ static TString GetCachePath(const TString& userName, const TString& tmpFolder);
+ TString MakeCachedPath(const TString& hash);
+ static void PrepareParentFolders(const TString& path, NYT::IClientBasePtr tx);
+ void SetTableAttrs(const NYT::TNode& spec, const TString& cachedPath);
+
+private:
+ const TTransactionCache::TEntry::TPtr Entry;
+ const TVector<TString> DstTables;
+ const TVector<NYT::TNode> DstSpecs;
+ const TString CachePath;
+ TVector<TString> CachedPaths;
+ const ui64 ChunkLimit;
+ const bool UseExpirationTimeout;
+ const bool UseMultiSet;
+ const std::pair<TString, TString> LogCtx;
+ TMap<TString, ui32> SortedCachedPaths;
+ const NYT::TNode MergeSpec;
+ NYT::TNode TableAttrs;
+ NYT::ITransactionPtr LockTx;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp b/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
new file mode 100644
index 0000000000..b4299b4179
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/temp_files.cpp
@@ -0,0 +1,19 @@
+#include "temp_files.h"
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+TTempFiles::TTempFiles(const TString& tmpDir)
+ : TmpDir(tmpDir)
+{
+}
+
+TString TTempFiles::AddFile(const TString& fileName) {
+ TFsPath filePath = TmpDir / fileName;
+ YQL_ENSURE(!filePath.Exists(), "Twice usage of the " << fileName << " temp file");
+ Files.emplace_back(MakeHolder<TTempFile>(filePath));
+ return filePath;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/temp_files.h b/ydb/library/yql/providers/yt/gateway/lib/temp_files.h
new file mode 100644
index 0000000000..98cdd17634
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/temp_files.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <util/folder/path.h>
+#include <util/system/tempfile.h>
+#include <util/generic/string.h>
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+struct TTempFiles {
+ TTempFiles(const TString& tmpDir);
+
+ TString AddFile(const TString& fileName);
+
+ const TFsPath TmpDir;
+ TVector<THolder<TTempFile>> Files;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp b/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
new file mode 100644
index 0000000000..2e05f1d503
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.cpp
@@ -0,0 +1,464 @@
+#include "transaction_cache.h"
+#include "yt_helpers.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/system/guard.h>
+#include <util/generic/yexception.h>
+#include <util/generic/guid.h>
+#include <util/generic/scope.h>
+#include <util/folder/path.h>
+
+namespace NYql {
+
+using namespace NYT;
+
+void TTransactionCache::TEntry::DeleteAtFinalizeUnlocked(const TString& table, bool isInternal)
+{
+ auto inserted = TablesToDeleteAtFinalize.insert(table);
+ if (!isInternal && inserted.second) {
+ if (++ExternalTempTablesCount > InflightTempTablesLimit) {
+ YQL_LOG_CTX_THROW yexception() << "Too many temporary tables registered - limit is " << InflightTempTablesLimit;
+ }
+ }
+}
+
+bool TTransactionCache::TEntry::CancelDeleteAtFinalizeUnlocked(const TString& table, bool isInternal)
+{
+ auto erased = TablesToDeleteAtFinalize.erase(table);
+ if (!isInternal) {
+ YQL_ENSURE(erased <= ExternalTempTablesCount);
+ ExternalTempTablesCount -= erased;
+ }
+ return erased != 0;
+}
+
+
+void TTransactionCache::TEntry::RemoveInternal(const TString& table) {
+ bool existed;
+ with_lock(Lock_) {
+ existed = CancelDeleteAtFinalizeUnlocked(table, true);
+ }
+ if (existed) {
+ DoRemove(table);
+ }
+}
+
+void TTransactionCache::TEntry::DoRemove(const TString& table) {
+ if (!KeepTables) {
+ YQL_CLOG(INFO, ProviderYt) << "Removing " << table.Quote() << " on " << Server;
+ Tx->Remove(table, TRemoveOptions().Force(true));
+ }
+}
+
+void TTransactionCache::TEntry::Finalize(const TString& clusterName) {
+ NYT::ITransactionPtr binarySnapshotTx;
+ decltype(SnapshotTxs) snapshotTxs;
+ THashSet<TString> toDelete;
+ decltype(CheckpointTxs) checkpointTxs;
+ decltype(WriteTxs) writeTxs;
+ with_lock(Lock_) {
+ binarySnapshotTx.Swap(BinarySnapshotTx);
+ snapshotTxs.swap(SnapshotTxs);
+ LastSnapshotTx.Drop();
+ toDelete.swap(TablesToDeleteAtFinalize);
+ ExternalTempTablesCount = 0;
+ checkpointTxs.swap(CheckpointTxs);
+ writeTxs.swap(WriteTxs);
+ }
+
+ for (auto& item: writeTxs) {
+ item.second->Abort();
+ }
+
+ for (auto& item: checkpointTxs) {
+ item.second->Abort();
+ }
+
+ if (binarySnapshotTx) {
+ binarySnapshotTx->Abort();
+ }
+
+ for (auto& item: snapshotTxs) {
+ item.second->Abort();
+ }
+
+ for (auto i : toDelete) {
+ DoRemove(i);
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Committing tx " << GetGuidAsString(Tx->GetId()) << " on " << clusterName;
+ Tx->Commit();
+}
+
+TMaybe<ui64> TTransactionCache::TEntry::GetColumnarStat(NYT::TRichYPath ytPath) const {
+ YQL_ENSURE(ytPath.Columns_.Defined());
+ TVector<TString> columns(std::move(ytPath.Columns_->Parts_));
+ ytPath.Columns_.Clear();
+
+ auto guard = Guard(Lock_);
+ if (auto p = StatisticsCache.FindPtr(NYT::NodeToCanonicalYsonString(NYT::PathToNode(ytPath), NYT::NYson::EYsonFormat::Text))) {
+ ui64 sum = p->LegacyChunksDataWeight;
+ for (auto& column: columns) {
+ if (auto c = p->ColumnDataWeight.FindPtr(column)) {
+ sum += *c;
+ } else {
+ return Nothing();
+ }
+ }
+ return sum;
+ }
+ return Nothing();
+}
+
+void TTransactionCache::TEntry::UpdateColumnarStat(NYT::TRichYPath ytPath, ui64 size) {
+ YQL_ENSURE(ytPath.Columns_.Defined());
+ TVector<TString> columns(std::move(ytPath.Columns_->Parts_));
+ ytPath.Columns_.Clear();
+
+ auto guard = Guard(Lock_);
+ NYT::TTableColumnarStatistics& cacheColumnStat = StatisticsCache[NYT::NodeToCanonicalYsonString(NYT::PathToNode(ytPath), NYT::NYson::EYsonFormat::Text)];
+ cacheColumnStat.LegacyChunksDataWeight = size;
+ for (auto& c: cacheColumnStat.ColumnDataWeight) {
+ c.second = 0;
+ }
+ for (auto& c: columns) {
+ cacheColumnStat.ColumnDataWeight[c] = 0;
+ }
+}
+
+void TTransactionCache::TEntry::UpdateColumnarStat(NYT::TRichYPath ytPath, const NYT::TTableColumnarStatistics& columnStat) {
+ ytPath.Columns_.Clear();
+ auto guard = Guard(Lock_);
+ NYT::TTableColumnarStatistics& cacheColumnStat = StatisticsCache[NYT::NodeToCanonicalYsonString(NYT::PathToNode(ytPath), NYT::NYson::EYsonFormat::Text)];
+ cacheColumnStat.LegacyChunksDataWeight = columnStat.LegacyChunksDataWeight;
+ cacheColumnStat.TimestampTotalWeight = columnStat.TimestampTotalWeight;
+ for (auto& c: columnStat.ColumnDataWeight) {
+ cacheColumnStat.ColumnDataWeight[c.first] = c.second;
+ }
+}
+
+ITransactionPtr TTransactionCache::TEntry::GetSnapshotTx(bool createTx) {
+ auto guard = Guard(Lock_);
+ if (createTx || !LastSnapshotTx) {
+ LastSnapshotTx = Tx->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ SnapshotTxs.emplace(LastSnapshotTx->GetId(), LastSnapshotTx);
+ }
+ return LastSnapshotTx;
+}
+
+NYT::ITransactionPtr TTransactionCache::TEntry::GetSnapshotTx(const NYT::TTransactionId& id) const {
+ auto guard = Guard(Lock_);
+ auto p = SnapshotTxs.FindPtr(id);
+ YQL_ENSURE(p, "Unknown snapshot transaction id=" << GetGuidAsString(id));
+ return *p;
+}
+
+NYT::ITransactionPtr TTransactionCache::TEntry::GetCheckpointTx(const TString& tablePath) const {
+ auto guard = Guard(Lock_);
+ auto p = CheckpointTxs.FindPtr(tablePath);
+ YQL_ENSURE(p, "No transaction found for checkpoint " << tablePath.Quote());
+ return *p;
+}
+
+NYT::ITransactionPtr TTransactionCache::TEntry::GetOrCreateCheckpointTx(const TString& tablePath) {
+ auto guard = Guard(Lock_);
+ auto& tx = CheckpointTxs[tablePath];
+ if (!tx) {
+ tx = Client->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ YQL_CLOG(INFO, ProviderYt) << "Started checkpoint tx " << GetGuidAsString(tx->GetId());
+ }
+ return tx;
+}
+
+void TTransactionCache::TEntry::CommitCheckpointTx(const TString& tablePath) {
+ auto guard = Guard(Lock_);
+ auto p = CheckpointTxs.FindPtr(tablePath);
+ YQL_ENSURE(p, "No transaction found for checkpoint " << tablePath.Quote());
+ YQL_CLOG(INFO, ProviderYt) << "Commiting checkpoint tx " << GetGuidAsString((*p)->GetId());
+ (*p)->Commit();
+ CheckpointTxs.erase(tablePath);
+}
+
+NYT::TTransactionId TTransactionCache::TEntry::AllocWriteTx() {
+ auto guard = Guard(Lock_);
+ auto writeTx = Tx->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ WriteTxs.emplace(writeTx->GetId(), writeTx);
+ YQL_CLOG(INFO, ProviderYt) << "Allocated write tx " << GetGuidAsString(writeTx->GetId());
+ return writeTx->GetId();
+
+}
+
+void TTransactionCache::TEntry::CompleteWriteTx(const NYT::TTransactionId& id, bool abort) {
+ auto guard = Guard(Lock_);
+ auto p = WriteTxs.FindPtr(id);
+ YQL_ENSURE(p, "No transaction found: " << GetGuidAsString(id));
+ YQL_CLOG(INFO, ProviderYt) << (abort ? "Aborting" : "Commiting") << " write tx " << GetGuidAsString(id);
+ if (abort) {
+ (*p)->Abort();
+ } else {
+ (*p)->Commit();
+ }
+ WriteTxs.erase(id);
+}
+
+std::pair<TString, NYT::TTransactionId> TTransactionCache::TEntry::GetBinarySnapshot(TString remoteTmpFolder, const TString& md5, const TString& localPath, TDuration expirationInterval) {
+ if (remoteTmpFolder.StartsWith(NYT::TConfig::Get()->Prefix)) {
+ remoteTmpFolder = remoteTmpFolder.substr(NYT::TConfig::Get()->Prefix.size());
+ }
+ TString remotePath = TFsPath(remoteTmpFolder) / md5;
+
+ ITransactionPtr snapshotTx;
+ with_lock(Lock_) {
+ if (!BinarySnapshotTx) {
+ BinarySnapshotTx = Client->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ }
+ snapshotTx = BinarySnapshotTx;
+ if (auto p = BinarySnapshots.FindPtr(remotePath)) {
+ return std::make_pair(*p, snapshotTx->GetId());
+ }
+ }
+ CreateParents({remotePath}, Client);
+
+ NYT::ILockPtr fileLock;
+ ITransactionPtr lockTx;
+ NYT::ILockPtr waitLock;
+
+ for (bool uploaded = false; ;) {
+ try {
+ YQL_CLOG(INFO, ProviderYt) << "Taking snapshot of " << remotePath;
+ fileLock = snapshotTx->Lock(remotePath, NYT::ELockMode::LM_SNAPSHOT);
+ break;
+ } catch (const TErrorResponse& e) {
+ if (!e.IsResolveError()) {
+ throw;
+ }
+ }
+ YQL_ENSURE(!uploaded, "Fail to take snapshot");
+ if (!lockTx) {
+ auto pos = remotePath.rfind("/");
+ auto dir = remotePath.substr(0, pos);
+ auto childKey = remotePath.substr(pos + 1) + ".lock";
+
+ lockTx = Client->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ YQL_CLOG(INFO, ProviderYt) << "Waiting for " << dir << '/' << childKey;
+ waitLock = lockTx->Lock(dir, NYT::ELockMode::LM_SHARED, TLockOptions().Waitable(true).ChildKey(childKey));
+ waitLock->GetAcquiredFuture().GetValueSync();
+ // Try to take snapshot again after waiting lock. Someone else may complete uploading the file at the moment
+ continue;
+ }
+ // Lock is already taken and file still doesn't exist
+ YQL_CLOG(INFO, ProviderYt) << "Start uploading " << localPath << " to " << remotePath;
+ Y_SCOPE_EXIT(localPath, remotePath) {
+ YQL_CLOG(INFO, ProviderYt) << "Complete uploading " << localPath << " to " << remotePath;
+ };
+ auto uploadTx = Client->StartTransaction(TStartTransactionOptions().Attributes(TransactionSpec));
+ try {
+ auto out = uploadTx->CreateFileWriter(TRichYPath(remotePath).Executable(true), TFileWriterOptions().CreateTransaction(false));
+ TIFStream in(localPath);
+ TransferData(&in, out.Get());
+ out->Finish();
+ uploadTx->Commit();
+ } catch (...) {
+ uploadTx->Abort();
+ throw;
+ }
+ // Continue with taking snapshot lock after uploading
+ uploaded = true;
+ }
+
+ TString snapshotPath = TStringBuilder() << '#' << GetGuidAsString(fileLock->GetLockedNodeId());
+ YQL_CLOG(INFO, ProviderYt) << "Snapshot of " << remotePath << ": " << snapshotPath;
+ with_lock(Lock_) {
+ BinarySnapshots[remotePath] = snapshotPath;
+ }
+
+ if (expirationInterval) {
+ TString expirationTime = (Now() + expirationInterval).ToStringUpToSeconds();
+ try {
+ YQL_CLOG(INFO, ProviderYt) << "Prolonging expiration time for " << remotePath << " up to " << expirationTime;
+ Client->Set(remotePath + "/@expiration_time", expirationTime);
+ } catch (...) {
+ // log and ignore the error
+ YQL_CLOG(ERROR, ProviderYt) << "Error setting expiration time for " << remotePath << ": " << CurrentExceptionMessage();
+ }
+ }
+
+ return std::make_pair(snapshotPath, snapshotTx->GetId());
+}
+
+void TTransactionCache::TEntry::CreateDefaultTmpFolder() {
+ if (DefaultTmpFolder) {
+ Client->Create(DefaultTmpFolder, NYT::NT_MAP, NYT::TCreateOptions().Recursive(true).IgnoreExisting(true));
+ }
+}
+
+TTransactionCache::TTransactionCache(const TString& userName)
+ : UserName_(userName)
+{}
+
+TTransactionCache::TEntry::TPtr TTransactionCache::GetEntry(const TString& server) {
+ auto res = TryGetEntry(server);
+ if (!res) {
+ YQL_LOG_CTX_THROW yexception() << "GetEntry() failed for " << server;
+ }
+ return res;
+}
+
+TTransactionCache::TEntry::TPtr TTransactionCache::TryGetEntry(const TString& server) {
+ auto guard = Guard(Lock_);
+ auto it = TxMap_.find(server);
+ if (it != TxMap_.end()) {
+ return it->second;
+ }
+ return {};
+}
+
+TTransactionCache::TEntry::TPtr TTransactionCache::GetOrCreateEntry(const TString& server, const TString& token,
+ const TMaybe<TString>& impersonationUser, const TSpecProvider& specProvider, const TYtSettings::TConstPtr& config)
+{
+ TEntry::TPtr createdEntry = nullptr;
+ NYT::TTransactionId externalTx = config->ExternalTx.Get().GetOrElse(TGUID());
+ with_lock(Lock_) {
+ auto it = TxMap_.find(server);
+ if (it != TxMap_.end()) {
+ return it->second;
+ }
+
+ TString tmpFolder = config->TablesTmpFolder.Get().GetOrElse("");
+
+ createdEntry = MakeIntrusive<TEntry>();
+ createdEntry->Server = server;
+ auto createClientOptions = TCreateClientOptions().Token(token);
+ if (impersonationUser) {
+ createClientOptions = createClientOptions.ImpersonationUser(*impersonationUser);
+ }
+ createdEntry->Client = CreateClient(server, createClientOptions);
+ createdEntry->TransactionSpec = specProvider();
+ if (externalTx) {
+ createdEntry->ExternalTx = createdEntry->Client->AttachTransaction(externalTx);
+ createdEntry->Tx = createdEntry->ExternalTx->StartTransaction(TStartTransactionOptions().Attributes(createdEntry->TransactionSpec));
+ } else {
+ createdEntry->Tx = createdEntry->Client->StartTransaction(TStartTransactionOptions().Attributes(createdEntry->TransactionSpec));
+ }
+ createdEntry->CacheTx = createdEntry->Client;
+ createdEntry->CacheTtl = config->QueryCacheTtl.Get().GetOrElse(TDuration::Days(7));
+ if (!tmpFolder.empty()) {
+ auto fullTmpFolder = AddPathPrefix(tmpFolder, NYT::TConfig::Get()->Prefix);
+ bool existsGlobally = createdEntry->Client->Exists(fullTmpFolder);
+ bool existsInTx = externalTx && createdEntry->ExternalTx->Exists(fullTmpFolder);
+ if (!existsGlobally && existsInTx) {
+ createdEntry->CacheTx = createdEntry->ExternalTx;
+ createdEntry->CacheTxId = createdEntry->ExternalTx->GetId();
+ }
+ } else {
+ createdEntry->DefaultTmpFolder = NYT::AddPathPrefix("tmp/yql/" + UserName_, NYT::TConfig::Get()->Prefix);
+ }
+ createdEntry->InflightTempTablesLimit = config->InflightTempTablesLimit.Get().GetOrElse(Max<ui32>());
+ createdEntry->KeepTables = GetReleaseTempDataMode(*config) == EReleaseTempDataMode::Never;
+
+ TxMap_.emplace(server, createdEntry);
+ }
+ if (externalTx) {
+ YQL_CLOG(INFO, ProviderYt) << "Attached to external tx " << GetGuidAsString(externalTx);
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Created tx " << GetGuidAsString(createdEntry->Tx->GetId()) << " on " << server;
+ return createdEntry;
+}
+
+void TTransactionCache::Commit(const TString& server) {
+ ITransactionPtr tx;
+ THashSet<TString> tablesToDelete;
+ with_lock(Lock_) {
+ auto it = TxMap_.find(server);
+ if (it != TxMap_.end()) {
+ auto entry = it->second;
+ tablesToDelete.swap(entry->TablesToDeleteAtCommit);
+ if (!tablesToDelete.empty()) {
+ tx = entry->Tx;
+ }
+ }
+ }
+ if (tx) {
+ for (auto& table : tablesToDelete) {
+ YQL_CLOG(INFO, ProviderYt) << "Removing " << table.Quote() << " on " << server;
+ tx->Remove(table, TRemoveOptions().Force(true));
+ }
+ }
+}
+
+void TTransactionCache::Finalize() {
+ THashMap<TString, TEntry::TPtr> txMap;
+ with_lock(Lock_) {
+ txMap.swap(TxMap_);
+ }
+ for (auto& item: txMap) {
+ item.second->Finalize(item.first);
+ }
+}
+
+void TTransactionCache::AbortAll() {
+ THashMap<TString, TEntry::TPtr> txMap;
+ with_lock(Lock_) {
+ txMap.swap(TxMap_);
+ }
+ for (auto& item : txMap) {
+ auto entry = item.second;
+
+ for (auto& item: entry->SnapshotTxs) {
+ try {
+ YQL_CLOG(DEBUG, ProviderYt) << "AbortAll(): Aborting Snapshot tx " << GetGuidAsString(item.second->GetId());
+ item.second->Abort();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+ for (auto& item : entry->CheckpointTxs) {
+ try {
+ YQL_CLOG(DEBUG, ProviderYt) << "AbortAll(): Aborting Checkpoint tx " << GetGuidAsString(item.second->GetId());
+ item.second->Abort();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+ for (auto& item: entry->WriteTxs) {
+ try {
+ YQL_CLOG(DEBUG, ProviderYt) << "AbortAll(): Aborting Write tx " << GetGuidAsString(item.second->GetId());
+ item.second->Abort();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+ if (entry->BinarySnapshotTx) {
+ YQL_CLOG(INFO, ProviderYt) << "AbortAll(): Aborting BinarySnapshot tx " << GetGuidAsString(entry->BinarySnapshotTx->GetId());
+ try {
+ entry->BinarySnapshotTx->Abort();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+
+ if (entry->Tx) {
+ YQL_CLOG(INFO, ProviderYt) << "Aborting tx " << GetGuidAsString(entry->Tx->GetId()) << " on " << item.first;
+ try {
+ entry->Tx->Abort();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+
+ if (entry->Client) {
+ YQL_CLOG(INFO, ProviderYt) << "Shutting down client";
+ try {
+ entry->Client->Shutdown();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h b/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h
new file mode 100644
index 0000000000..b99912ee1e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h
@@ -0,0 +1,146 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+
+#include <ydb/library/yql/core/file_storage/storage.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <util/generic/string.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/hash.h>
+#include <util/generic/ptr.h>
+#include <util/generic/variant.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+#include <util/datetime/base.h>
+#include <util/system/mutex.h>
+
+#include <utility>
+#include <functional>
+#include <tuple>
+#include <vector>
+#include <exception>
+
+namespace NYql {
+
+class TTransactionCache {
+public:
+ using TSpecProvider = std::function<NYT::TNode()>;
+
+ struct TEntry : public TThrRefBase {
+ TString Server;
+ NYT::IClientPtr Client;
+ NYT::ITransactionPtr Tx;
+ NYT::ITransactionPtr ExternalTx;
+ NYT::IClientBasePtr CacheTx;
+ NYT::TTransactionId CacheTxId;
+ TDuration CacheTtl;
+ THashMap<NYT::TTransactionId, NYT::ITransactionPtr> SnapshotTxs;
+ THashMap<NYT::TTransactionId, NYT::ITransactionPtr> WriteTxs;
+ NYT::ITransactionPtr LastSnapshotTx;
+ THashSet<TString> TablesToDeleteAtFinalize;
+ THashSet<TString> TablesToDeleteAtCommit;
+ ui32 InflightTempTablesLimit = Max<ui32>();
+ bool KeepTables = false;
+ THashMap<std::pair<TString, ui32>, std::tuple<TString, NYT::TTransactionId, ui64>> Snapshots; // {tablepath, epoch} -> {table_id, transaction_id, revision}
+ NYT::TNode TransactionSpec;
+ THashMap<TString, NYT::TTableColumnarStatistics> StatisticsCache;
+ THashMap<TString, TString> BinarySnapshots; // remote path -> snapshot path
+ NYT::ITransactionPtr BinarySnapshotTx;
+ THashMap<TString, NYT::ITransactionPtr> CheckpointTxs;
+ TString DefaultTmpFolder;
+ THashMap<std::tuple<TString, TString, TString>, std::vector<NYT::TRichYPath>> RangeCache;
+ THashMap<TString, std::pair<std::vector<TString>, std::vector<std::exception_ptr>>> PartialRangeCache;
+ THashMap<TString, std::variant<std::vector<std::tuple<TString, TString, TString>>, TFileLinkPtr>> FolderCache;
+
+ TMutex Lock_;
+
+ inline void DeleteAtFinalize(const TString& table) {
+ with_lock(Lock_) {
+ DeleteAtFinalizeUnlocked(table, false);
+ }
+ }
+
+ inline void DeleteAtFinalizeInternal(const TString& table) {
+ with_lock(Lock_) {
+ DeleteAtFinalizeUnlocked(table, true);
+ }
+ }
+
+ inline void CancelDeleteAtFinalize(const TString& table) {
+ with_lock(Lock_) {
+ CancelDeleteAtFinalizeUnlocked(table, false);
+ }
+ }
+
+ void RemoveInternal(const TString& table);
+ void Finalize(const TString& clusterName);
+
+ template<typename T>
+ T CancelDeleteAtFinalize(const T& range) {
+ T filteredRange;
+ with_lock(Lock_) {
+ for (const auto& i : range) {
+ if (CancelDeleteAtFinalizeUnlocked(i, false)) {
+ filteredRange.insert(filteredRange.end(), i);
+ }
+ }
+ }
+ return filteredRange;
+ }
+
+ inline NYT::IClientBasePtr GetRoot() const {
+ if (ExternalTx) {
+ return ExternalTx;
+ }
+ return Client;
+ }
+
+ NYT::ITransactionPtr GetSnapshotTx(bool createTx);
+ NYT::ITransactionPtr GetSnapshotTx(const NYT::TTransactionId& id) const;
+
+ NYT::ITransactionPtr GetCheckpointTx(const TString& tablePath) const;
+ NYT::ITransactionPtr GetOrCreateCheckpointTx(const TString& tablePath);
+ void CommitCheckpointTx(const TString& tablePath);
+
+ NYT::TTransactionId AllocWriteTx();
+ void CompleteWriteTx(const NYT::TTransactionId& id, bool abort);
+
+ TMaybe<ui64> GetColumnarStat(NYT::TRichYPath ytPath) const;
+ void UpdateColumnarStat(NYT::TRichYPath ytPath, ui64 size);
+ void UpdateColumnarStat(NYT::TRichYPath ytPath, const NYT::TTableColumnarStatistics& columnStat);
+
+ std::pair<TString, NYT::TTransactionId> GetBinarySnapshot(TString remoteTmpFolder, const TString& md5, const TString& localPath, TDuration expirationInterval);
+
+ void CreateDefaultTmpFolder();
+
+ using TPtr = TIntrusivePtr<TEntry>;
+
+ private:
+ void DeleteAtFinalizeUnlocked(const TString& table, bool isInternal);
+ bool CancelDeleteAtFinalizeUnlocked(const TString& table, bool isInternal);
+ void DoRemove(const TString& table);
+
+ size_t ExternalTempTablesCount = 0;
+ };
+
+ TTransactionCache(const TString& userName);
+
+ TEntry::TPtr GetEntry(const TString& server);
+ TEntry::TPtr GetOrCreateEntry(const TString& server, const TString& token, const TMaybe<TString>& impersonationUser, const TSpecProvider& specProvider, const TYtSettings::TConstPtr& config);
+ TEntry::TPtr TryGetEntry(const TString& server);
+
+ void Commit(const TString& server);
+ void Finalize();
+ void AbortAll();
+
+private:
+ TMutex Lock_;
+ THashMap<TString, TEntry::TPtr> TxMap_;
+ const TString UserName_;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp b/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
new file mode 100644
index 0000000000..b9e9d2a890
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/user_files.cpp
@@ -0,0 +1,97 @@
+#include "user_files.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/system/guard.h>
+#include <library/cpp/string_utils/url/url.h>
+
+
+namespace NYql {
+
+TUserFiles::TUserFiles(const TYtUrlMapper& urlMapper, const TString& activeCluster)
+ : UrlMapper(urlMapper)
+ , ActiveCluster(activeCluster)
+{
+}
+
+void TUserFiles::AddFile(const TUserDataKey& key, const TUserDataBlock& block) {
+ with_lock(Mutex) {
+ if (Files.contains(key.Alias())) {
+ return;
+ }
+ }
+
+
+ TFileInfo userFile;
+ userFile.IsUdf = block.Usage.Test(EUserDataBlockUsage::Udf);
+
+ // we can optimize file copy if file resides on the same cluster
+ // and provide only link
+ TString cluster;
+ TString remotePath;
+ if ((block.Type == EUserDataType::URL) &&
+ UrlMapper.MapYtUrl(block.Data, &cluster, &remotePath) &&
+ (cluster == CurrentYtClusterShortcut || cluster == ActiveCluster)) {
+ userFile.RemotePath = remotePath;
+ userFile.RemoteMemoryFactor = 1.0;
+ YQL_CLOG(INFO, Default) << "Using remote file " << userFile.RemotePath.Quote() << " from " << ActiveCluster.Quote();
+ } else {
+ if (!block.FrozenFile) {
+ YQL_LOG_CTX_THROW yexception() << "File with key " << key << " is not frozen";
+ }
+
+ userFile.Path = block.FrozenFile;
+ userFile.InMemorySize = userFile.Path->GetSize();
+ }
+
+ with_lock(Mutex) {
+ Files[key.Alias()] = std::move(userFile);
+ }
+}
+
+bool TUserFiles::HasFilePath(const TString& name) const {
+ auto guard = Guard(Mutex);
+ return Files.FindPtr(name) != nullptr;
+}
+
+TString TUserFiles::GetFilePath(const TString& name) const {
+ auto guard = Guard(Mutex);
+ auto x = Files.FindPtr(name);
+ YQL_ENSURE(x);
+ return x->Path->GetPath();
+}
+
+bool TUserFiles::FindFolder(const TString& name, TVector<TString>& files) const {
+ auto guard = Guard(Mutex);
+ auto prefix = TUserDataStorage::MakeFolderName(name);
+ for (auto& x : Files) {
+ if (x.first.StartsWith(prefix)) {
+ files.push_back(x.first);
+ }
+ }
+
+ return !files.empty();
+}
+
+const TUserFiles::TFileInfo* TUserFiles::GetFile(const TString& name) const {
+ auto guard = Guard(Mutex);
+ return Files.FindPtr(name);
+}
+
+THashMap<TString, TUserFiles::TFileInfo> TUserFiles::GetFiles() const {
+ auto guard = Guard(Mutex);
+ return Files;
+}
+
+inline bool TUserFiles::IsEmpty() const {
+ auto guard = Guard(Mutex);
+ return Files.empty();
+}
+
+
+
+} // NYql
+
+
diff --git a/ydb/library/yql/providers/yt/gateway/lib/user_files.h b/ydb/library/yql/providers/yt/gateway/lib/user_files.h
new file mode 100644
index 0000000000..58ff9231a0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/user_files.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h>
+#include <ydb/library/yql/core/file_storage/storage.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/yql_user_data.h>
+
+#include <util/system/mutex.h>
+#include <util/generic/ptr.h>
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+
+class IThreadPool;
+
+namespace NYql {
+
+class TYtGatewayConfig;
+
+class TUserFiles: public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<TUserFiles>;
+
+ struct TFileInfo {
+ TFileLinkPtr Path; // Real path in storage
+ bool IsUdf = false;
+ ui64 InMemorySize = 0;
+ TString RemotePath;
+ double RemoteMemoryFactor = 0.;
+ };
+
+public:
+ TUserFiles(const TYtUrlMapper& urlMapper, const TString& activeCluster);
+
+ void AddFile(const TUserDataKey& key, const TUserDataBlock& block);
+
+ bool HasFilePath(const TString& name) const;
+ bool FindFolder(const TString& name, TVector<TString>& files) const;
+ TString GetFilePath(const TString& name) const;
+ const TFileInfo* GetFile(const TString& name) const;
+ THashMap<TString, TFileInfo> GetFiles() const;
+ bool IsEmpty() const;
+
+private:
+ const TYtUrlMapper& UrlMapper;
+ const TString ActiveCluster;
+ THashMap<TString, TFileInfo> Files;
+ TMutex Mutex;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/ya.make b/ydb/library/yql/providers/yt/gateway/lib/ya.make
new file mode 100644
index 0000000000..47f72369c0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/ya.make
@@ -0,0 +1,41 @@
+LIBRARY()
+
+SRCS(
+ query_cache.cpp
+ query_cache.h
+ temp_files.cpp
+ temp_files.h
+ transaction_cache.cpp
+ transaction_cache.h
+ user_files.cpp
+ user_files.h
+ yt_helpers.cpp
+ yt_helpers.h
+)
+
+PEERDIR(
+ library/cpp/regex/pcre
+ library/cpp/string_utils/url
+ library/cpp/threading/future
+ library/cpp/yson/node
+ yt/cpp/mapreduce/client
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/public/issue
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/utils/threading
+ ydb/library/yql/core/type_ann
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/gateway
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/lib/hash
+ ydb/library/yql/providers/yt/lib/res_pull
+ ydb/library/yql/providers/yt/lib/url_mapper
+ ydb/library/yql/providers/yt/lib/yson_helpers
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp b/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
new file mode 100644
index 0000000000..13dbab139a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.cpp
@@ -0,0 +1,645 @@
+#include "yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h>
+#include <ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h>
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/providers/common/gateway/yql_provider_gateway.h>
+#include <ydb/library/yql/core/issue/yql_issue.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/minikql/aligned_page_pool.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/threading/future/future.h>
+
+#include <util/string/split.h>
+#include <util/system/env.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/hash.h>
+#include <util/generic/map.h>
+#include <util/generic/set.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/yexception.h>
+#include <util/generic/algorithm.h>
+
+namespace NYql {
+
+namespace {
+EYqlIssueCode IssueCodeForYtError(const NYT::TYtError& error) {
+ if (error.ContainsErrorCode(NYT::NClusterErrorCodes::NSecurityClient::AuthorizationError) ||
+ error.ContainsErrorCode(NYT::NClusterErrorCodes::NSecurityClient::AuthenticationError)) {
+ return TIssuesIds::YT_ACCESS_DENIED;
+ }
+
+ if (error.ContainsErrorCode(NYT::NClusterErrorCodes::NChunkPools::MaxDataWeightPerJobExceeded)) {
+ return TIssuesIds::YT_MAX_DATAWEIGHT_PER_JOB_EXCEEDED;
+ }
+
+ return TIssuesIds::DEFAULT_ERROR;
+}
+}
+
+TMaybe<ui64> GetUsedRows(const NYT::TRichYPath& table, ui64 tableRowCount) {
+ TMaybe<ui64> rows;
+ if (auto ranges = table.GetRanges()) {
+ rows = 0;
+ for (const NYT::TReadRange& readRange: *ranges) {
+ if (readRange.Exact_.RowIndex_) {
+ rows = rows.GetOrElse(0) + 1;
+ } else if (readRange.LowerLimit_.RowIndex_ || readRange.UpperLimit_.RowIndex_) {
+ ui64 range = tableRowCount;
+ if (readRange.UpperLimit_.RowIndex_) {
+ range = *readRange.UpperLimit_.RowIndex_;
+ }
+ if (readRange.LowerLimit_.RowIndex_) {
+ range -= Min<ui64>(range, *readRange.LowerLimit_.RowIndex_);
+ }
+ rows = rows.GetOrElse(0) + range;
+ } else {
+ return Nothing();
+ }
+ }
+ }
+ return rows;
+}
+
+TMaybe<ui64> GetUncompressedFileSize(NYT::ITransactionPtr tx, const TString& path) {
+ if (!tx->Exists(path)) {
+ return 0;
+ }
+
+ NYT::TNode attrs = tx->Get(path + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("uncompressed_data_size"))
+ ));
+
+ return GetDataWeight(attrs);
+}
+
+TString TransformPath(TStringBuf tmpFolder, TStringBuf name, bool isTempTable, TStringBuf userName) {
+ TString path;
+ path.assign(name);
+ if (isTempTable && tmpFolder) {
+ path = tmpFolder;
+ if (!tmpFolder.EndsWith('/')) {
+ path += '/';
+ }
+ path += name;
+ }
+ if (path.StartsWith("//")) {
+ return path.substr(2);
+ }
+
+ if (isTempTable && !tmpFolder && path.StartsWith("tmp/")) {
+ TStringBuilder builder;
+ builder << "tmp/yql/";
+ if (userName) {
+ builder << userName << '/';
+ }
+ builder << path.substr(4);
+ path = builder;
+ }
+
+ return path;
+}
+
+namespace {
+
+THashSet<TStringBuf> DEPRECATED_YQL_ATTRS = {
+ TStringBuf("_yql_key_meta"),
+ TStringBuf("_yql_subkey_meta"),
+ TStringBuf("_yql_value_meta"),
+};
+
+THashSet<TStringBuf> TEST_YQL_ATTRS = {
+ YqlDynamicAttribute,
+};
+
+THashSet<TStringBuf> SERVICE_YQL_ATTRS = {
+ TStringBuf("_yql_runner"),
+ TStringBuf("_yql_op_id"),
+ TStringBuf("_yql_op_title"),
+ TStringBuf("_yql_query_name"),
+};
+
+}
+
+IYtGateway::TCanonizedPath CanonizedPath(const TString& path) {
+ NYT::TRichYPath richYPath(path);
+ if (path.StartsWith('<')) {
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(path));
+ }
+ size_t pos = 0;
+ if ((pos = richYPath.Path_.find('{')) != TString::npos) {
+ size_t end = richYPath.Path_.find('}');
+ YQL_ENSURE(end != TString::npos && end > pos);
+ TVector<TString> columns;
+ StringSplitter(richYPath.Path_.substr(pos + 1, end - pos - 1)).Split(',').AddTo(&columns);
+ richYPath.Columns(columns);
+ richYPath.Path_ = richYPath.Path_.substr(0, pos);
+ }
+
+ if ((pos = richYPath.Path_.find('[')) != TString::npos) {
+ size_t end = richYPath.Path_.find(']');
+ YQL_ENSURE(end != TString::npos && end > pos);
+ TString rangeString = richYPath.Path_.substr(pos + 1, end - pos - 1);
+ richYPath.Path_ = richYPath.Path_.substr(0, pos);
+ TVector<TString> ranges;
+ size_t startPos = 0;
+ int insideParens = 0;
+ for (size_t i = 0; i < rangeString.length(); ++i) {
+ switch (rangeString.at(i)) {
+ case '(':
+ ++insideParens;
+ break;
+ case ')':
+ --insideParens;
+ break;
+ case ',':
+ if (0 == insideParens) {
+ ranges.push_back(rangeString.substr(startPos, i - startPos));
+ startPos = i + 1;
+ }
+ break;
+ }
+ }
+ if (startPos < rangeString.length()) {
+ ranges.push_back(rangeString.substr(startPos));
+ }
+ auto toReadLimit = [] (const TString& s) -> NYT::TReadLimit {
+ if (s.StartsWith('#')) {
+ return NYT::TReadLimit().RowIndex(FromString<i64>(s.substr(1)));
+ } else if (s.StartsWith('(')) {
+ YQL_ENSURE(s.EndsWith(')'));
+ TVector<TString> keys;
+ StringSplitter(s.substr(1, s.length() - 2)).Split(',').AddTo(&keys);
+ NYT::TKey complexKey;
+ for (auto& key: keys) {
+ complexKey.Add(NYT::NodeFromYsonString(key));
+ }
+ return NYT::TReadLimit().Key(complexKey);
+ } else {
+ return NYT::TReadLimit().Key(NYT::NodeFromYsonString(s));
+ }
+ };
+
+ richYPath.MutableRanges().ConstructInPlace();
+ for (TString& r: ranges) {
+ if ((pos = r.find(':')) != TString::npos) {
+ NYT::TReadRange range;
+ if (TString lower = r.substr(0, pos)) {
+ range.LowerLimit(toReadLimit(lower));
+ }
+ if (TString upper = r.substr(pos + 1)) {
+ range.UpperLimit(toReadLimit(upper));
+ }
+ richYPath.AddRange(range);
+ } else {
+ richYPath.AddRange(NYT::TReadRange().Exact(toReadLimit(r)));
+ }
+ }
+ }
+ return {
+ richYPath.Path_,
+ richYPath.Columns_.Defined() ? richYPath.Columns_->Parts_ : TMaybe<TVector<TString>>(),
+ richYPath.GetRanges()
+ };
+};
+
+NYT::TNode GetUserAttributes(NYT::ITransactionPtr tx, TString path, TMaybe<bool> includeYqlAttrs) {
+ path.append("/@");
+ NYT::TNode attrs = tx->Get(path, NYT::TGetOptions()
+ .AttributeFilter(NYT::TAttributeFilter()
+ .AddAttribute("user_attribute_keys")
+ )
+ );
+ if (attrs.HasKey("user_attribute_keys")) {
+ NYT::TAttributeFilter filter;
+ for (auto key: attrs["user_attribute_keys"].AsList()) {
+ if (key.AsString().StartsWith("_yql") && includeYqlAttrs) {
+ if (!*includeYqlAttrs
+ || TEST_YQL_ATTRS.contains(key.AsString())
+ || DEPRECATED_YQL_ATTRS.contains(key.AsString())
+ || SERVICE_YQL_ATTRS.contains(key.AsString())) {
+ continue;
+ }
+ }
+ filter.AddAttribute(key.AsString());
+ }
+ if (!filter.Attributes_.empty()) {
+ return tx->Get(path, NYT::TGetOptions().AttributeFilter(filter));
+ }
+ }
+ return NYT::TNode::CreateMap();
+}
+
+void TransferTableAttributes(const NYT::TNode& attributes, const std::function<void(const TString&,const TString&)>& receiver)
+{
+ for (const auto& attr : attributes.AsMap()) {
+ const TString& attrName = attr.first;
+ const NYT::TNode& attrValue = attr.second;
+
+ if (attrName == FORMAT_ATTR_NAME) {
+ receiver(attrName, NYT::NodeToYsonString(attrValue));
+ }
+ else if (attrName.StartsWith(TStringBuf("_yql"))) {
+ if (attrName == YqlRowSpecAttribute) {
+ receiver(attrName, NYT::NodeToYsonString(attrValue));
+ } else if (!TEST_YQL_ATTRS.contains(attrName)
+ && !DEPRECATED_YQL_ATTRS.contains(attrName)
+ && !SERVICE_YQL_ATTRS.contains(attr.first)) {
+ try {
+ receiver(attrName, attrValue.ConvertTo<TString>());
+ } catch (const NYT::TNode::TTypeError&) {
+ throw yexception() << "Unexpected value of '" << attrName << "' attribute: " << NYT::NodeToYsonString(attrValue);
+ }
+ }
+ }
+ }
+}
+
+NYT::TNode FilterYqlAttributes(const NYT::TNode& attributes)
+{
+ NYT::TNode res = NYT::TNode::CreateMap();
+ for (const auto& attr : attributes.AsMap()) {
+ if (attr.first.StartsWith(TStringBuf("_yql"))
+ && !TEST_YQL_ATTRS.contains(attr.first)
+ && !DEPRECATED_YQL_ATTRS.contains(attr.first)
+ && !SERVICE_YQL_ATTRS.contains(attr.first)) {
+
+ res[attr.first] = attr.second;
+ }
+ }
+ return res;
+}
+
+template <bool YAMRED_DSV>
+static bool IterateRows(NYT::ITransactionPtr tx,
+ NYT::TRichYPath path,
+ ui32 tableIndex,
+ TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec,
+ const TTableLimiter& limiter,
+ const TMaybe<TSampleParams>& sampling)
+{
+ const ui64 startRecordInTable = limiter.GetTableStart();
+ const ui64 endRecordInTable = limiter.GetTableZEnd(); // 0 means the entire table usage
+
+ if (startRecordInTable || endRecordInTable) {
+ YQL_ENSURE(path.GetRanges().Empty());
+ NYT::TReadRange readRange;
+ if (startRecordInTable) {
+ readRange.LowerLimit(NYT::TReadLimit().RowIndex(startRecordInTable));
+ }
+ if (endRecordInTable) {
+ readRange.UpperLimit(NYT::TReadLimit().RowIndex(endRecordInTable));
+ }
+ path.AddRange(readRange);
+ }
+
+ NYT::TTableReaderOptions readerOptions;
+ if (sampling && sampling->Mode == EYtSampleMode::Bernoulli) {
+ NYT::TNode spec = NYT::TNode::CreateMap();
+ spec["sampling_rate"] = sampling->Percentage / 100.;
+ if (sampling->Repeat) {
+ spec["sampling_seed"] = static_cast<i64>(sampling->Repeat);
+ }
+ readerOptions.Config(spec);
+ }
+
+ if (!YAMRED_DSV && exec.GetColumns()) {
+ if (!specsCache.GetSpecs().Inputs[tableIndex]->OthersStructIndex) {
+ path.Columns(*exec.GetColumns());
+ }
+ }
+
+ if (YAMRED_DSV) {
+ path.Columns_.Clear();
+ auto reader = tx->CreateTableReader<NYT::TYaMRRow>(path, readerOptions);
+ for (; reader->IsValid(); reader->Next()) {
+ if (!exec.WriteNext(specsCache, reader->GetRow(), tableIndex)) {
+ return true;
+ }
+ }
+ } else {
+ auto format = specsCache.GetSpecs().MakeInputFormat(tableIndex);
+ auto rawReader = tx->CreateRawReader(path, format, readerOptions);
+ TMkqlReaderImpl reader(*rawReader, 0, 4 << 10, tableIndex);
+ reader.SetSpecs(specsCache.GetSpecs(), specsCache.GetHolderFactory());
+
+ for (reader.Next(); reader.IsValid(); reader.Next()) {
+ if (!exec.WriteNext(specsCache, reader.GetRow(), tableIndex)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool IterateYamredRows(NYT::ITransactionPtr tx,
+ const NYT::TRichYPath& table,
+ ui32 tableIndex,
+ TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec,
+ const TTableLimiter& limiter,
+ const TMaybe<TSampleParams>& sampling)
+{
+ return IterateRows<true>(tx, table, tableIndex, specsCache, exec, limiter, sampling);
+}
+
+bool IterateYsonRows(NYT::ITransactionPtr tx,
+ const NYT::TRichYPath& table,
+ ui32 tableIndex,
+ TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec,
+ const TTableLimiter& limiter,
+ const TMaybe<TSampleParams>& sampling)
+{
+ return IterateRows<false>(tx, table, tableIndex, specsCache, exec, limiter, sampling);
+}
+
+bool SelectRows(NYT::IClientPtr client,
+ const TString& table,
+ ui32 tableIndex,
+ TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec,
+ TTableLimiter& limiter)
+{
+ ui64 startRecordInTable = limiter.GetTableStart();
+ const ui64 endRecordInTable = limiter.GetTableZEnd(); // 0 means the entire table usage
+ TStringStream sqlBuilder;
+ const auto& columns = exec.GetColumns();
+ if (columns) {
+ bool isFirstColumn = true;
+ for (auto& x : *columns) {
+ if (!isFirstColumn) {
+ sqlBuilder << ", ";
+ }
+
+ isFirstColumn = false;
+ sqlBuilder << "[" << x << "]";
+ }
+ } else {
+ sqlBuilder << "*";
+ }
+
+ sqlBuilder << " FROM [";
+ sqlBuilder << NYT::AddPathPrefix(table, NYT::TConfig::Get()->Prefix);
+ sqlBuilder << "]";
+ if (exec.GetRowsLimit()) {
+ ui64 effectiveLimit = endRecordInTable;
+ if (!effectiveLimit) {
+ effectiveLimit = startRecordInTable + *exec.GetRowsLimit() + 1;
+ } else {
+ effectiveLimit = Min(effectiveLimit, *exec.GetRowsLimit() + 1);
+ }
+
+ sqlBuilder << " LIMIT " << effectiveLimit;
+ }
+
+ ui64 processed = 0;
+ bool ret = false;
+ auto rows = client->SelectRows(sqlBuilder.Str());
+ for (const auto& row : rows) {
+ ++processed;
+ if (processed <= startRecordInTable) {
+ continue;
+ }
+
+ if (!exec.WriteNext(specsCache, row, tableIndex)) {
+ ret = true;
+ break;
+ }
+
+ if (endRecordInTable) {
+ if (processed >= endRecordInTable) {
+ break;
+ }
+ }
+ }
+
+ limiter.Skip(processed);
+ return ret;
+}
+
+NYT::TNode YqlOpOptionsToSpec(const TYqlOperationOptions& opOpts, const TString& userName, const TVector<std::pair<TString, TString>>& code)
+{
+ NYT::TNode spec = NYT::TNode::CreateMap();
+
+ if (auto title = opOpts.Title.GetOrElse(TString())) {
+ spec["title"] = title;
+ } else {
+ TStringBuilder titleBuilder;
+ titleBuilder << "YQL operation (";
+ if (opOpts.QueryName) {
+ titleBuilder << *opOpts.QueryName;
+ }
+ if (opOpts.Id) {
+ if (opOpts.QueryName) {
+ titleBuilder << ", ";
+ }
+ titleBuilder << *opOpts.Id;
+ }
+ titleBuilder << " by " << userName << ')';
+ spec["title"] = titleBuilder;
+ }
+
+ NYT::TNode& description = spec["description"];
+ description["yql_runner"] = opOpts.Runner;
+
+ if (auto id = opOpts.Id.GetOrElse(TString())) {
+ description["yql_op_id"] = id;
+ }
+
+ if (auto url = opOpts.Url.GetOrElse(TString())) {
+ NYT::TNode& urlNode = description["yql_op_url"];
+ urlNode = url;
+ // Mark as URL for YT UI (see https://clubs.at.yandex-team.ru/yt/2364)
+ urlNode.Attributes()["_type_tag"] = "url";
+ }
+
+ if (auto title = opOpts.Title.GetOrElse(TString())) {
+ description["yql_op_title"] = title;
+ }
+
+ if (auto name = opOpts.QueryName.GetOrElse(TString())) {
+ description["yql_query_name"] = name;
+ }
+
+ static constexpr size_t OP_CODE_LIMIT = 1ul << 17; // 128Kb
+
+ if (!code.empty()) {
+ size_t remaining = OP_CODE_LIMIT;
+ NYT::TNode& codeNode = description["yql_op_code"];
+ for (auto& c: code) {
+ TString snippet = c.second;
+ if (!remaining) {
+ snippet = "__truncated__";
+ } else if (snippet.length() > remaining) {
+ // Keep the end part of the code as more interesting
+ snippet = TStringBuilder() << "__truncated__\n" << TStringBuf(snippet).Last(remaining) << "\n__truncated__";
+ }
+ codeNode[c.first] = snippet;
+ remaining -= Min(remaining, snippet.length());
+ }
+ }
+
+ if (auto attrs = opOpts.AttrsYson.GetOrElse(TString())) {
+ NYT::TNode userAttrs = NYT::NodeFromYsonString(attrs);
+ for (const auto& item: userAttrs.AsMap()) {
+ const TString& key = item.first;
+ const NYT::TNode& value = item.second;
+
+ if (key != TStringBuf("runner") &&
+ key != TStringBuf("op_id") &&
+ key != TStringBuf("op_url") &&
+ key != TStringBuf("op_title") &&
+ key != TStringBuf("query_name") &&
+ key != TStringBuf("op_code"))
+ {
+ // do not allow to override specific attrs
+ description[TString("yql_") + key] = value;
+ }
+ }
+ }
+
+ return spec;
+}
+
+NYT::TNode YqlOpOptionsToAttrs(const TYqlOperationOptions& opOpts) {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+
+ attrs["_yql_runner"] = opOpts.Runner;
+ if (auto id = opOpts.Id.GetOrElse(TString())) {
+ attrs["_yql_op_id"] = id;
+ }
+ if (auto title = opOpts.Title.GetOrElse(TString())) {
+ attrs["_yql_op_title"] = title;
+ }
+ if (auto name = opOpts.QueryName.GetOrElse(TString())) {
+ attrs["_yql_query_name"] = name;
+ }
+ return attrs;
+}
+
+void CreateParents(const TVector<TString>& tables, NYT::IClientBasePtr tx) {
+ auto batchExists = tx->CreateBatchRequest();
+ TVector<NThreading::TFuture<void>> batchExistsRes;
+
+ THashSet<TString> uniqFolders;
+ auto batchCreateParent = tx->CreateBatchRequest();
+ TVector<NThreading::TFuture<NYT::TLockId>> batchCreateParentRes;
+
+ for (auto& table: tables) {
+ auto slash = table.rfind('/');
+ if (TString::npos != slash) {
+ TString folder = table.substr(0, slash);
+ if (uniqFolders.insert(folder).second) {
+ batchExistsRes.push_back(
+ batchExists->Exists(folder).Apply([&batchCreateParentRes, &batchCreateParent, folder](const NThreading::TFuture<bool>& f) {
+ if (!f.GetValue()) {
+ batchCreateParentRes.push_back(batchCreateParent->Create(folder, NYT::NT_MAP,
+ NYT::TCreateOptions().Recursive(true).IgnoreExisting(true)));
+ }
+ })
+ );
+ }
+ }
+ }
+
+ batchExists->ExecuteBatch();
+ ForEach(batchExistsRes.begin(), batchExistsRes.end(), [] (const NThreading::TFuture<void>& f) {
+ f.GetValue();
+ });
+
+ if (!batchCreateParentRes.empty()) {
+ batchCreateParent->ExecuteBatch();
+ ForEach(batchCreateParentRes.begin(), batchCreateParentRes.end(), [] (const NThreading::TFuture<NYT::TLockId>& f) {
+ f.GetValue();
+ });
+ }
+}
+
+static void FillResultFromOperationError(NCommon::TOperationResult& result, const NYT::TOperationFailedError& e, TPosition pos) {
+ TString errMsg = GetEnv("YQL_DETERMINISTIC_MODE") ? e.GetError().ShortDescription() : TString(e.what());
+ EYqlIssueCode rootIssueCode = IssueCodeForYtError(e.GetError());
+ TIssue rootIssue = YqlIssue(pos, rootIssueCode, errMsg);
+
+ if (!e.GetFailedJobInfo().empty()) {
+ TSet<TString> uniqueErrors;
+ for (auto& failedJob: e.GetFailedJobInfo()) {
+ TStringBuf message = failedJob.Stderr;
+ auto parsedPos = TryParseTerminationMessage(message);
+ if (message.size() < failedJob.Stderr.size()) {
+ if (uniqueErrors.emplace(message).second) {
+ rootIssue.AddSubIssue(MakeIntrusive<TIssue>(YqlIssue(parsedPos.GetOrElse(pos), TIssuesIds::DEFAULT_ERROR, TString{message})));
+ }
+ } else {
+ TString errorDescription = failedJob.Error.ShortDescription();
+ if (uniqueErrors.insert(errorDescription).second) {
+ rootIssue.AddSubIssue(MakeIntrusive<TIssue>(YqlIssue(pos, TIssuesIds::UNEXPECTED, errorDescription)));
+ }
+ }
+ }
+ }
+
+ result.SetStatus(TIssuesIds::DEFAULT_ERROR);
+ result.AddIssue(rootIssue);
+}
+
+static void FillResultFromErrorResponse(NCommon::TOperationResult& result, const NYT::TErrorResponse& e, TPosition pos) {
+ TString errMsg = GetEnv("YQL_DETERMINISTIC_MODE") ? e.GetError().ShortDescription() : TString(e.what());
+
+ EYqlIssueCode rootIssueCode = IssueCodeForYtError(e.GetError());
+ TIssue rootIssue = YqlIssue(pos, rootIssueCode, errMsg);
+
+ result.SetStatus(TIssuesIds::DEFAULT_ERROR);
+ result.AddIssue(rootIssue);
+}
+
+void FillResultFromCurrentException(NCommon::TOperationResult& result, TPosition pos) {
+ try {
+ throw;
+ } catch (const NYT::TOperationFailedError& e) {
+ FillResultFromOperationError(result, e, pos);
+ } catch (const NYT::TErrorResponse& e) {
+ FillResultFromErrorResponse(result, e, pos);
+ } catch (const std::exception& e) {
+ result.SetException(e, pos);
+ } catch (const NKikimr::TMemoryLimitExceededException&) {
+ result.SetStatus(TIssuesIds::UNEXPECTED);
+ result.AddIssue(TIssue(pos, "Memory limit exceeded in MKQL runtime"));
+ } catch (...) {
+ result.SetStatus(TIssuesIds::UNEXPECTED);
+ result.AddIssue(TIssue(pos, CurrentExceptionMessage()));
+ }
+}
+
+void EnsureSpecDoesntUseNativeYtTypes(const NYT::TNode& spec, TStringBuf tableName, bool read) {
+ if (spec.HasKey(YqlRowSpecAttribute)) {
+ const auto& rowSpec = spec[YqlRowSpecAttribute];
+ bool useNativeYtTypes = false;
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ useNativeYtTypes = rowSpec[RowSpecAttrUseNativeYtTypes].AsBool();
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ useNativeYtTypes = rowSpec[RowSpecAttrUseTypeV2].AsBool();
+ } else if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ useNativeYtTypes = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64() > 0;
+ }
+ if (useNativeYtTypes) {
+ throw yexception() << "Cannot " << (read ? "read" : "modify") << " table \"" << tableName << "\" with type_v3 schema using yson codec";
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h b/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h
new file mode 100644
index 0000000000..f36a93abb2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <ydb/library/yql/public/issue/yql_issue.h>
+#include <ydb/library/yql/providers/common/gateway/yql_provider_gateway.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/map.h>
+
+#include <functional>
+#include <utility>
+
+namespace NYql {
+
+class TMkqlIOCache;
+class TExecuteResOrPull;
+class TTableLimiter;
+struct TYqlOperationOptions;
+
+namespace NCommon {
+
+class TOperationResult;
+
+}
+
+TMaybe<ui64> GetUsedRows(const NYT::TRichYPath& table, ui64 tableRowCount);
+TMaybe<ui64> GetUncompressedFileSize(NYT::ITransactionPtr tx, const TString& path);
+
+TString TransformPath(TStringBuf tmpFolder, TStringBuf name, bool isTempTable, TStringBuf userName);
+
+NYT::TNode GetUserAttributes(NYT::ITransactionPtr tx, TString path, TMaybe<bool> includeYqlAttrs = Nothing());
+void TransferTableAttributes(const NYT::TNode& attributes, const std::function<void(const TString&,const TString&)>& receiver);
+NYT::TNode FilterYqlAttributes(const NYT::TNode& attributes);
+
+bool IterateYamredRows(NYT::ITransactionPtr tx, const NYT::TRichYPath& table, ui32 tableIndex, TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec, const TTableLimiter& limiter, const TMaybe<TSampleParams>& sampling = {});
+bool IterateYsonRows(NYT::ITransactionPtr tx, const NYT::TRichYPath& table, ui32 tableIndex, TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec, const TTableLimiter& limiter, const TMaybe<TSampleParams>& sampling = {});
+bool SelectRows(NYT::IClientPtr client, const TString& table, ui32 tableIndex, TMkqlIOCache& specsCache,
+ TExecuteResOrPull& exec, TTableLimiter& limiter);
+
+NYT::TNode YqlOpOptionsToSpec(const TYqlOperationOptions& opOpts, const TString& userName, const TVector<std::pair<TString, TString>>& code = {});
+NYT::TNode YqlOpOptionsToAttrs(const TYqlOperationOptions& opOpts);
+
+void CreateParents(const TVector<TString>& tables, NYT::IClientBasePtr tx);
+
+// must be used inside 'catch' because it rethrows current exception to analyze it's type
+void FillResultFromCurrentException(NCommon::TOperationResult& result, TPosition pos = {});
+
+// must be used inside 'catch' because it rethrows current exception to analyze it's type
+template<typename TResult>
+static TResult ResultFromCurrentException(TPosition pos = {}) {
+ TResult result;
+ FillResultFromCurrentException(result, pos);
+ return result;
+}
+
+IYtGateway::TCanonizedPath CanonizedPath(const TString& path);
+
+void EnsureSpecDoesntUseNativeYtTypes(const NYT::TNode& spec, TStringBuf tableName, bool read);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/ya.make b/ydb/library/yql/providers/yt/gateway/native/ya.make
new file mode 100644
index 0000000000..27ac2da5f5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/ya.make
@@ -0,0 +1,65 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_exec_ctx.cpp
+ yql_yt_lambda_builder.cpp
+ yql_yt_native.cpp
+ yql_yt_op_tracker.cpp
+ yql_yt_qb2.cpp
+ yql_yt_session.cpp
+ yql_yt_spec.cpp
+ yql_yt_transform.cpp
+)
+
+PEERDIR(
+ library/cpp/containers/sorted_vector
+ library/cpp/digest/md5
+ library/cpp/random_provider
+ library/cpp/streams/brotli
+ library/cpp/threading/future
+ library/cpp/time_provider
+ library/cpp/yson
+ library/cpp/yson/node
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/ast
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/minikql/comp_nodes/llvm
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/utils/threading
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core/issue
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/comp_nodes
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/common/proto
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/schema/expr
+ ydb/library/yql/providers/result/expr_nodes
+ ydb/library/yql/providers/stat/expr_nodes
+ ydb/library/yql/providers/stat/uploader
+ ydb/library/yql/providers/yt/codec
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/expr_nodes
+ ydb/library/yql/providers/yt/gateway/lib
+ ydb/library/yql/providers/yt/job
+ ydb/library/yql/providers/yt/lib/expr_traits
+ ydb/library/yql/providers/yt/lib/infer_schema
+ ydb/library/yql/providers/yt/lib/lambda_builder
+ ydb/library/yql/providers/yt/lib/log
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+ ydb/library/yql/providers/yt/lib/res_pull
+ ydb/library/yql/providers/yt/lib/schema
+ ydb/library/yql/providers/yt/lib/skiff
+ ydb/library/yql/providers/yt/lib/url_mapper
+ ydb/library/yql/providers/yt/lib/yson_helpers
+ ydb/library/yql/providers/yt/lib/init_yt_api
+ ydb/library/yql/providers/yt/lib/config_clusters
+ ydb/library/yql/providers/yt/provider
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
new file mode 100644
index 0000000000..ed5647d2a2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.cpp
@@ -0,0 +1,410 @@
+#include "yql_yt_exec_ctx.h"
+
+#include "yql_yt_spec.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_table.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/providers/yt/lib/schema/schema.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/yexception.h>
+#include <util/generic/xrange.h>
+#include <util/string/builder.h>
+#include <util/system/guard.h>
+#include <util/system/platform.h>
+
+#include <type_traits>
+
+namespace NYql {
+
+namespace NNative {
+
+using namespace NNodes;
+
+TExecContextBase::TExecContextBase(const TYtNativeServices& services,
+ const TConfigClusters::TPtr& clusters,
+ const TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler>& mkqlCompiler,
+ const TSession::TPtr& session,
+ const TString& cluster,
+ const TYtUrlMapper& urlMapper)
+ : FunctionRegistry_(services.FunctionRegistry)
+ , FileStorage_(services.FileStorage)
+ , Config_(services.Config)
+ , Clusters_(clusters)
+ , MkqlCompiler_(mkqlCompiler)
+ , Session_(session)
+ , Cluster_(cluster)
+ , UrlMapper_(urlMapper)
+ , DisableAnonymousClusterAccess_(services.DisableAnonymousClusterAccess)
+ , Hidden(session->SessionId_.EndsWith("_hidden"))
+{
+ YtServer_ = Clusters_->GetServer(Cluster_);
+ LogCtx_ = NYql::NLog::CurrentLogContextPath();
+}
+
+
+void TExecContextBase::MakeUserFiles(const TUserDataTable& userDataBlocks) {
+ const TString& activeYtCluster = Clusters_->GetYtName(Cluster_);
+ UserFiles_ = MakeIntrusive<TUserFiles>(UrlMapper_, activeYtCluster);
+ for (const auto& file: userDataBlocks) {
+ auto block = file.second;
+ if (!Config_->GetMrJobUdfsDir().empty() && block.Usage.Test(EUserDataBlockUsage::Udf) && block.Type == EUserDataType::PATH) {
+ TFsPath path = block.Data;
+ TString fileName = path.Basename();
+#ifdef _win_
+ TStringBuf changedName(fileName);
+ changedName.ChopSuffix(".dll");
+ fileName = TString("lib") + changedName + ".so";
+#endif
+ block.Data = TFsPath(Config_->GetMrJobUdfsDir()) / fileName;
+ TString md5;
+ if (block.FrozenFile) {
+ md5 = block.FrozenFile->GetMd5();
+ }
+ block.FrozenFile = CreateFakeFileLink(block.Data, md5);
+ }
+
+ UserFiles_->AddFile(file.first, block);
+ }
+}
+
+void TExecContextBase::SetInput(TExprBase input, bool forcePathColumns, const THashSet<TString>& extraSysColumns, const TYtSettings::TConstPtr& settings) {
+ const TString tmpFolder = settings->TablesTmpFolder.Get().GetOrElse(TString());
+
+ NYT::TNode extraSysColumnsNode;
+ for (auto sys: extraSysColumns) {
+ extraSysColumnsNode.Add(sys);
+ }
+
+ if (auto out = input.Maybe<TYtOutput>()) { // Pull case
+ auto tableInfo = TYtTableBaseInfo::Parse(out.Cast());
+ YQL_CLOG(INFO, ProviderYt) << "Input: " << Cluster_ << '.' << tableInfo->Name;
+ NYT::TRichYPath richYPath(NYql::TransformPath(tmpFolder, tableInfo->Name, true, Session_->UserName_));
+
+ auto spec = tableInfo->GetCodecSpecNode();
+ if (!extraSysColumnsNode.IsUndefined()) {
+ spec[YqlSysColumnPrefix] = extraSysColumnsNode;
+ }
+
+ InputTables_.emplace_back(
+ richYPath.Path_,
+ richYPath,
+ true,
+ true,
+ *tableInfo,
+ spec,
+ 0
+ );
+ }
+ else {
+ TMaybe<bool> hasScheme;
+ size_t loggedTable = 0;
+ const auto entry = Config_->GetLocalChainTest() ? TTransactionCache::TEntry::TPtr() : GetEntry();
+
+ auto fillSection = [&] (TYtSection section, ui32 group) {
+ TVector<TStringBuf> columns;
+ auto sysColumnsSetting = NYql::GetSettingAsColumnList(section.Settings().Ref(), EYtSettingType::SysColumns);
+ if (forcePathColumns) {
+ for (auto& colType: section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ if (!colType->GetName().StartsWith(YqlSysColumnPrefix) || sysColumnsSetting.empty()) {
+ columns.push_back(colType->GetName());
+ }
+ }
+ }
+ NYT::TNode sysColumns = extraSysColumnsNode;
+ for (auto sys: sysColumnsSetting) {
+ if (!extraSysColumns.contains(sys)) {
+ sysColumns.Add(sys);
+ }
+ }
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ if (loggedTable++ < 10) {
+ YQL_CLOG(INFO, ProviderYt) << "Input: " << Cluster_ << '.' << pathInfo.Table->Name << '[' << group << ']';
+ }
+ // Table may have aux columns. Exclude them by specifying explicit columns from the type
+ if (forcePathColumns && pathInfo.Table->RowSpec && !pathInfo.HasColumns()) {
+ pathInfo.SetColumns(columns);
+ }
+ auto name = NYql::TransformPath(tmpFolder, pathInfo.Table->Name, pathInfo.Table->IsTemp, Session_->UserName_);
+ NYT::TRichYPath richYPath;
+ if ((pathInfo.Table->IsTemp && !pathInfo.Table->IsAnonymous) || !entry) {
+ richYPath.Path(name);
+ } else {
+ auto p = entry->Snapshots.FindPtr(std::make_pair(name, pathInfo.Table->Epoch.GetOrElse(0)));
+ YQL_ENSURE(p, "Table " << pathInfo.Table->Name << " has no snapshot");
+ richYPath.Path(std::get<0>(*p)).TransactionId(std::get<1>(*p)).OriginalPath(NYT::AddPathPrefix(name, NYT::TConfig::Get()->Prefix));
+ }
+ pathInfo.FillRichYPath(richYPath);
+
+ auto spec = pathInfo.GetCodecSpecNode();
+ if (!sysColumns.IsUndefined()) {
+ spec[YqlSysColumnPrefix] = sysColumns;
+ }
+
+ InputTables_.emplace_back(
+ name,
+ richYPath,
+ pathInfo.Table->IsTemp,
+ !pathInfo.Table->RowSpec || pathInfo.Table->RowSpec->StrictSchema,
+ *pathInfo.Table,
+ spec,
+ group
+ );
+ if (NYql::HasSetting(pathInfo.Table->Settings.Ref(), EYtSettingType::WithQB)) {
+ auto p = pathInfo.Table->Meta->Attrs.FindPtr(QB2Premapper);
+ YQL_ENSURE(p, "Expect " << QB2Premapper << " in meta attrs");
+ InputTables_.back().QB2Premapper = NYT::NodeFromYsonString(*p);
+ }
+ const bool tableHasScheme = InputTables_.back().Spec.HasKey(YqlRowSpecAttribute);
+ if (!hasScheme) {
+ hasScheme = tableHasScheme;
+ } else {
+ YQL_ENSURE(*hasScheme == tableHasScheme, "Mixed Yamr/Yson input table formats");
+ }
+ }
+ if (0 == group) {
+ Sampling = NYql::GetSampleParams(section.Settings().Ref());
+ } else {
+ YQL_ENSURE(NYql::GetSampleParams(section.Settings().Ref()) == Sampling, "Different sampling settings");
+ }
+ };
+
+ if (entry) {
+ with_lock(entry->Lock_) {
+ ui32 group = 0;
+ for (auto section: input.Cast<TYtSectionList>()) {
+ fillSection(section, group);
+ ++group;
+ }
+ }
+ } else {
+ ui32 group = 0;
+ for (auto section: input.Cast<TYtSectionList>()) {
+ fillSection(section, group);
+ ++group;
+ }
+ }
+
+ if (hasScheme && !*hasScheme) {
+ YamrInput = true;
+ }
+ if (loggedTable > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total input tables=" << loggedTable;
+ }
+ }
+}
+
+void TExecContextBase::SetOutput(TYtOutSection output, const TYtSettings::TConstPtr& settings, const TString& opHash) {
+ const TString tmpFolder = settings->TablesTmpFolder.Get().GetOrElse(TString());
+ const auto nativeYtTypeCompatibility = settings->NativeYtTypeCompatibility.Get(Cluster_).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = settings->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+ size_t loggedTable = 0;
+ TVector<TString> outTablePaths;
+ TVector<NYT::TNode> outTableSpecs;
+ for (auto table: output) {
+ TYtOutTableInfo tableInfo(table);
+ TString outTableName = tableInfo.Name;
+ if (outTableName.empty()) {
+ outTableName = TStringBuilder() << "tmp/" << GetGuidAsString(Session_->RandomProvider_->GenGuid());
+ }
+ TString outTablePath = NYql::TransformPath(tmpFolder, outTableName, true, Session_->UserName_);
+ auto attrSpec = tableInfo.GetAttrSpecNode(nativeYtTypeCompatibility, rowSpecCompactForm);
+ OutTables_.emplace_back(
+ outTableName,
+ outTablePath,
+ tableInfo.GetCodecSpecNode(),
+ attrSpec,
+ ToYTSortColumns(tableInfo.RowSpec->GetForeignSort())
+ );
+ outTablePaths.push_back(outTablePath);
+ outTableSpecs.push_back(std::move(attrSpec));
+ if (loggedTable++ < 10) {
+ YQL_CLOG(INFO, ProviderYt) << "Output: " << Cluster_ << '.' << outTableName;
+ }
+ }
+ if (loggedTable > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total output tables=" << loggedTable;
+ }
+
+ SetCacheItem(outTablePaths, outTableSpecs, tmpFolder, settings, opHash);
+}
+
+void TExecContextBase::SetSingleOutput(const TYtOutTableInfo& outTable, const TYtSettings::TConstPtr& settings) {
+ const TString tmpFolder = settings->TablesTmpFolder.Get().GetOrElse(TString());
+ TString outTableName = TStringBuilder() << "tmp/" << GetGuidAsString(Session_->RandomProvider_->GenGuid());
+ TString outTablePath = NYql::TransformPath(tmpFolder, outTableName, true, Session_->UserName_);
+
+ const auto nativeYtTypeCompatibility = settings->NativeYtTypeCompatibility.Get(Cluster_).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = settings->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+
+ OutTables_.emplace_back(
+ outTableName,
+ outTablePath,
+ outTable.GetCodecSpecNode(),
+ outTable.GetAttrSpecNode(nativeYtTypeCompatibility, rowSpecCompactForm),
+ ToYTSortColumns(outTable.RowSpec->GetForeignSort())
+ );
+
+ YQL_CLOG(INFO, ProviderYt) << "Output: " << Cluster_ << '.' << outTableName;
+}
+
+void TExecContextBase::SetCacheItem(const TVector<TString>& outTablePaths, const TVector<NYT::TNode>& outTableSpecs,
+ const TString& tmpFolder, const TYtSettings::TConstPtr& settings, const TString& opHash) {
+ const bool testRun = Config_->GetLocalChainTest();
+ if (!testRun && !Hidden) {
+ NYT::TNode mergeSpec = Session_->CreateSpecWithDesc();
+ NYT::TNode tableAttrs = Session_->CreateTableAttrs();
+
+ auto entry = GetOrCreateEntry(settings);
+ FillSpec(mergeSpec, *this, settings, entry, 0., Nothing());
+ auto chunkLimit = settings->QueryCacheChunkLimit.Get(Cluster_).GetOrElse(0);
+
+ QueryCacheItem.Reset(new TYtQueryCacheItem(settings->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable),
+ entry, opHash, outTablePaths, outTableSpecs, Session_->UserName_, tmpFolder, mergeSpec, tableAttrs, chunkLimit,
+ settings->QueryCacheUseExpirationTimeout.Get().GetOrElse(false),
+ settings->_UseMultisetAttributes.Get().GetOrElse(DEFAULT_USE_MULTISET_ATTRS), LogCtx_));
+ }
+}
+
+TString TExecContextBase::GetInputSpec(bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags, bool intermediateInput) const {
+ return GetSpecImpl(InputTables_, 0, InputTables_.size(), {}, ensureOldTypesOnly, nativeTypeCompatibilityFlags, intermediateInput);
+}
+
+TString TExecContextBase::GetOutSpec(bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags) const {
+ return GetSpecImpl(OutTables_, 0, OutTables_.size(), {}, ensureOldTypesOnly, nativeTypeCompatibilityFlags, false);
+}
+
+TString TExecContextBase::GetOutSpec(size_t beginIdx, size_t endIdx, NYT::TNode initialOutSpec, bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags) const {
+ return GetSpecImpl(OutTables_, beginIdx, endIdx, initialOutSpec, ensureOldTypesOnly, nativeTypeCompatibilityFlags, false);
+}
+
+template <class TTableType>
+TString TExecContextBase::GetSpecImpl(const TVector<TTableType>& tables, size_t beginIdx, size_t endIdx, NYT::TNode initialSpec, bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags, bool intermediateInput) {
+ YQL_ENSURE(beginIdx <= endIdx);
+ YQL_ENSURE(endIdx <= tables.size());
+ NYT::TNode specNode = initialSpec;
+ if (initialSpec.IsUndefined()) {
+ specNode = NYT::TNode::CreateMap();
+ }
+ NYT::TNode& tablesNode = specNode[YqlIOSpecTables];
+
+ auto updateFlags = [nativeTypeCompatibilityFlags](NYT::TNode& spec) {
+ if (spec.HasKey(YqlRowSpecAttribute)) {
+ auto& rowSpec = spec[YqlRowSpecAttribute];
+ ui64 nativeYtTypeFlags = 0;
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrUseNativeYtTypes].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrUseTypeV2].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+ rowSpec[RowSpecAttrNativeYtTypeFlags] = (nativeYtTypeFlags & nativeTypeCompatibilityFlags);
+ }
+ };
+
+ if (!intermediateInput && (endIdx - beginIdx) > 1) {
+ NYT::TNode& registryNode = specNode[YqlIOSpecRegistry];
+ THashMap<TString, TString> uniqSpecs;
+ for (size_t i = beginIdx; i < endIdx; ++i) {
+ auto& table = tables[i];
+ TString refName = TStringBuilder() << "$table" << uniqSpecs.size();
+ auto spec = table.Spec;
+ if (ensureOldTypesOnly) {
+ EnsureSpecDoesntUseNativeYtTypes(spec, table.Name, std::is_same<TTableType, TInputInfo>::value);
+ } else {
+ updateFlags(spec);
+ }
+ auto res = uniqSpecs.emplace(NYT::NodeToCanonicalYsonString(spec), refName);
+ if (res.second) {
+ registryNode[refName] = std::move(spec);
+ }
+ else {
+ refName = res.first->second;
+ }
+ tablesNode.Add(refName);
+ }
+ }
+ else {
+ auto& table = tables[beginIdx];
+ auto spec = table.Spec;
+ if (ensureOldTypesOnly) {
+ EnsureSpecDoesntUseNativeYtTypes(spec, table.Name, std::is_same<TTableType, TInputInfo>::value);
+ } else {
+ updateFlags(spec);
+ }
+
+ tablesNode.Add(std::move(spec));
+ }
+ return NYT::NodeToYsonString(specNode);
+}
+
+TTransactionCache::TEntry::TPtr TExecContextBase::GetOrCreateEntry(const TYtSettings::TConstPtr& settings) const {
+ auto token = GetAuth(settings);
+ auto impersonationUser = GetImpersonationUser(settings);
+ if (!token && DisableAnonymousClusterAccess_) {
+ // do not use ythrow here for better error message
+ throw yexception() << "Accessing YT cluster " << Cluster_ << " without OAuth token is not allowed";
+ }
+
+ return Session_->TxCache_.GetOrCreateEntry(YtServer_, token, impersonationUser, [s = Session_]() { return s->CreateSpecWithDesc(); }, settings);
+}
+
+TExpressionResorceUsage TExecContextBase::ScanExtraResourceUsageImpl(const TExprNode& node, const TYtSettings::TConstPtr& config, bool withInput) {
+ auto extraUsage = ScanExtraResourceUsage(node, *config);
+ if (withInput && AnyOf(InputTables_, [](const auto& input) { return input.Erasure; })) {
+ if (auto codecCpu = config->ErasureCodecCpu.Get(Cluster_)) {
+ extraUsage.Cpu *= *codecCpu;
+ }
+ }
+ return extraUsage;
+}
+
+TString TExecContextBase::GetAuth(const TYtSettings::TConstPtr& config) const {
+ auto auth = config->Auth.Get();
+ if (!auth || auth->empty()) {
+ auth = Clusters_->GetAuth(Cluster_);
+ }
+
+ return auth.GetOrElse(TString());
+}
+
+TMaybe<TString> TExecContextBase::GetImpersonationUser(const TYtSettings::TConstPtr& config) const {
+ return config->_ImpersonationUser.Get();
+}
+
+ui64 TExecContextBase::EstimateLLVMMem(size_t nodes, const TString& llvmOpt, const TYtSettings::TConstPtr& config) const {
+ ui64 memUsage = 0;
+ if (llvmOpt != "OFF") {
+ if (auto usage = config->LLVMMemSize.Get(Cluster_)) {
+ memUsage += *usage;
+ }
+ if (auto usage = config->LLVMPerNodeMemSize.Get(Cluster_)) {
+ memUsage += *usage * nodes;
+ }
+ }
+
+ return memUsage;
+}
+
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.h
new file mode 100644
index 0000000000..67dd3346e8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_exec_ctx.h
@@ -0,0 +1,295 @@
+#pragma once
+
+#include "yql_yt_native.h"
+#include "yql_yt_session.h"
+
+#include <ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/query_cache.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/user_files.h>
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_table.h>
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+
+#include <ydb/library/yql/core/yql_user_data.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/file_storage/file_storage.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/utils/log/context.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/threading/future/future.h>
+#include <library/cpp/threading/future/async.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/size_literals.h>
+#include <util/generic/hash_set.h>
+
+#include <utility>
+
+
+namespace NYql {
+
+namespace NNative {
+
+struct TInputInfo {
+ TInputInfo() = default;
+ TInputInfo(const TString& name, const NYT::TRichYPath& path, bool temp, bool strict, const TYtTableBaseInfo& info, const NYT::TNode& spec, ui32 group = 0)
+ : Name(name)
+ , Path(path)
+ , Temp(temp)
+ , Dynamic(info.Meta->IsDynamic)
+ , Strict(strict)
+ , Records(info.Stat->RecordsCount)
+ , DataSize(info.Stat->DataSize)
+ , Spec(spec)
+ , Group(group)
+ , Lookup(info.Meta->Attrs.Value("optimize_for", "scan") != "scan")
+ , Erasure(info.Meta->Attrs.Value("erasure_codec", "none") != "none")
+ {
+ }
+
+ TString Name;
+ NYT::TRichYPath Path;
+ bool Temp = false;
+ bool Dynamic = false;
+ bool Strict = true;
+ ui64 Records = 0;
+ ui64 DataSize = 0;
+ NYT::TNode Spec;
+ NYT::TNode QB2Premapper;
+ ui32 Group = 0;
+ bool Lookup = false;
+ bool Erasure = false;
+};
+
+struct TOutputInfo {
+ TOutputInfo() = default;
+ TOutputInfo(const TString& name, const TString& path, const NYT::TNode& codecSpec, const NYT::TNode& attrSpec,
+ const NYT::TSortColumns& sortedBy)
+ : Name(name)
+ , Path(path)
+ , Spec(codecSpec)
+ , AttrSpec(attrSpec)
+ , SortedBy(sortedBy)
+ {
+ }
+ TString Name;
+ TString Path;
+ NYT::TNode Spec;
+ NYT::TNode AttrSpec;
+ NYT::TSortColumns SortedBy;
+};
+
+class TExecContextBase: public TThrRefBase {
+protected:
+ TExecContextBase(const TYtNativeServices& services,
+ const TConfigClusters::TPtr& clusters,
+ const TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler>& mkqlCompiler,
+ const TSession::TPtr& session,
+ const TString& cluster,
+ const TYtUrlMapper& urlMapper);
+
+public:
+ TString GetInputSpec(bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags, bool intermediateInput) const;
+ TString GetOutSpec(bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags) const;
+ TString GetOutSpec(size_t beginIdx, size_t endIdx, NYT::TNode initialOutSpec, bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags) const;
+
+ TTransactionCache::TEntry::TPtr GetEntry() const {
+ return Session_->TxCache_.GetEntry(YtServer_);
+ }
+
+ TTransactionCache::TEntry::TPtr TryGetEntry() const {
+ return Session_->TxCache_.TryGetEntry(YtServer_);
+ }
+
+ TTransactionCache::TEntry::TPtr GetOrCreateEntry(const TYtSettings::TConstPtr& settings) const;
+
+protected:
+ void MakeUserFiles(const TUserDataTable& userDataBlocks);
+
+ void SetInput(NNodes::TExprBase input, bool forcePathColumns, const THashSet<TString>& extraSysColumns, const TYtSettings::TConstPtr& settings);
+ void SetOutput(NNodes::TYtOutSection output, const TYtSettings::TConstPtr& settings, const TString& opHash);
+ void SetSingleOutput(const TYtOutTableInfo& outTable, const TYtSettings::TConstPtr& settings);
+ void SetCacheItem(const TVector<TString>& outTablePaths, const TVector<NYT::TNode>& outTableSpecs,
+ const TString& tmpFolder, const TYtSettings::TConstPtr& settings, const TString& opHash);
+
+ template <class TTableType>
+ static TString GetSpecImpl(const TVector<TTableType>& tables, size_t beginIdx, size_t endIdx, NYT::TNode initialOutSpec, bool ensureOldTypesOnly, ui64 nativeTypeCompatibilityFlags, bool intermediateInput);
+
+ NThreading::TFuture<void> MakeOperationWaiter(const NYT::IOperationPtr& op, const TMaybe<ui32>& publicId) const {
+ return Session_->OpTracker_->MakeOperationWaiter(op, publicId, YtServer_, Session_->ProgressWriter_, Session_->StatWriter_);
+ }
+
+ TString GetAuth(const TYtSettings::TConstPtr& config) const;
+ TMaybe<TString> GetImpersonationUser(const TYtSettings::TConstPtr& config) const;
+
+ ui64 EstimateLLVMMem(size_t nodes, const TString& llvmOpt, const TYtSettings::TConstPtr& config) const;
+
+ TExpressionResorceUsage ScanExtraResourceUsageImpl(const TExprNode& node, const TYtSettings::TConstPtr& config, bool withInput);
+
+ NThreading::TFuture<NThreading::TAsyncSemaphore::TPtr> AcquireOperationLock() {
+ return Session_->OperationSemaphore->AcquireAsync();
+ }
+
+public:
+ const NKikimr::NMiniKQL::IFunctionRegistry* FunctionRegistry_ = nullptr;
+ TFileStoragePtr FileStorage_;
+ TYtGatewayConfigPtr Config_;
+ TConfigClusters::TPtr Clusters_;
+ TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler> MkqlCompiler_;
+ TSession::TPtr Session_;
+ TString Cluster_;
+ TString YtServer_;
+ TUserFiles::TPtr UserFiles_;
+ TVector<std::pair<TString, TString>> CodeSnippets_;
+ std::pair<TString, TString> LogCtx_;
+ TVector<TInputInfo> InputTables_;
+ bool YamrInput = false;
+ TMaybe<TSampleParams> Sampling;
+ TVector<TOutputInfo> OutTables_;
+ THolder<TYtQueryCacheItem> QueryCacheItem;
+ const TYtUrlMapper& UrlMapper_;
+ bool DisableAnonymousClusterAccess_;
+ bool Hidden = false;
+};
+
+
+template <class T>
+class TExecContext: public TExecContextBase {
+public:
+ using TPtr = ::TIntrusivePtr<TExecContext>;
+ using TOptions = T;
+
+ TExecContext(const TYtNativeServices& services,
+ const TConfigClusters::TPtr& clusters,
+ const TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler>& mkqlCompiler,
+ TOptions&& options,
+ const TSession::TPtr& session,
+ const TString& cluster,
+ const TYtUrlMapper& urlMapper)
+ : TExecContextBase(services, clusters, mkqlCompiler, session, cluster, urlMapper)
+ , Options_(std::move(options))
+ {
+ }
+
+ void MakeUserFiles() {
+ TExecContextBase::MakeUserFiles(Options_.UserDataBlocks());
+ }
+
+ void SetInput(NNodes::TExprBase input, bool forcePathColumns, const THashSet<TString>& extraSysColumns) {
+ TExecContextBase::SetInput(input, forcePathColumns, extraSysColumns, Options_.Config());
+ }
+
+ void SetOutput(NNodes::TYtOutSection output) {
+ TExecContextBase::SetOutput(output, Options_.Config(), Options_.OperationHash());
+ }
+
+ void SetSingleOutput(const TYtOutTableInfo& outTable) {
+ TExecContextBase::SetSingleOutput(outTable, Options_.Config());
+ }
+
+ void SetCacheItem(const TVector<TString>& outTablePaths, const TVector<NYT::TNode>& outTableSpecs, const TString& tmpFolder) {
+ TExecContextBase::SetCacheItem(outTablePaths, outTableSpecs, tmpFolder, Options_.Config(), Options_.OperationHash());
+ }
+
+ TExpressionResorceUsage ScanExtraResourceUsage(const TExprNode& node, bool withInput) {
+ return TExecContextBase::ScanExtraResourceUsageImpl(node, Options_.Config(), withInput);
+ }
+
+ TTransactionCache::TEntry::TPtr GetOrCreateEntry() const {
+ return TExecContextBase::GetOrCreateEntry(Options_.Config());
+ }
+
+ TString GetAuth() const {
+ return TExecContextBase::GetAuth(Options_.Config());
+ }
+
+ ui64 EstimateLLVMMem(size_t nodes) const {
+ return TExecContextBase::EstimateLLVMMem(nodes, Options_.OptLLVM(), Options_.Config());
+ }
+
+ void SetNodeExecProgress(const TString& stage) {
+ auto publicId = Options_.PublicId();
+ if (!publicId) {
+ return;
+ }
+ auto progress = TOperationProgress(TString(YtProviderName), *publicId,
+ TOperationProgress::EState::InProgress, stage);
+ Session_->ProgressWriter_(progress);
+ }
+
+ [[nodiscard]]
+ NThreading::TFuture<bool> LookupQueryCacheAsync() {
+ if (QueryCacheItem) {
+ SetNodeExecProgress("Awaiting cache");
+ return QueryCacheItem->LookupAsync(Session_->Queue_);
+ }
+ return NThreading::MakeFuture(false);
+ }
+
+ void StoreQueryCache() {
+ if (QueryCacheItem) {
+ SetNodeExecProgress("Storing to cache");
+ QueryCacheItem->Store();
+ }
+ }
+
+ template <bool WithState = true>
+ [[nodiscard]]
+ NThreading::TFuture<void> RunOperation(std::function<NYT::IOperationPtr()>&& opFactory) {
+ if constexpr (WithState) {
+ SetNodeExecProgress("Waiting for concurrency limit");
+ }
+ Session_->EnsureInitializedSemaphore(Options_.Config());
+ return TExecContextBase::AcquireOperationLock().Apply([opFactory = std::move(opFactory), self = TIntrusivePtr(this)](const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(self->LogCtx_);
+ auto lock = f.GetValue()->MakeAutoRelease();
+ auto op = opFactory();
+ op->GetPreparedFuture().Subscribe([op, self](const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(self->LogCtx_);
+ if (!f.HasException()) {
+ if constexpr (WithState) {
+ self->SetNodeExecProgress("Starting operation");
+ }
+ try {
+ op->Start();
+ } catch (...) {
+ // Promise will be initialized with exception inside of TOperation::Start()
+ }
+ }
+ });
+ // opFactory factory may contain locked resources. Explicitly wait preparation before destroying it
+ op->GetPreparedFuture().GetValueSync();
+
+ NThreading::TFuture<void> res;
+ if constexpr (WithState) {
+ res = self->TExecContextBase::MakeOperationWaiter(op, self->Options_.PublicId());
+ } else {
+ res = self->TExecContextBase::MakeOperationWaiter(op, Nothing());
+ }
+ return res.Apply([queue = self->Session_->Queue_, unlock = lock.DeferRelease()](const auto& f) {
+ if (f.HasException()) {
+ f.TryRethrow();
+ }
+ return queue->Async([unlock = std::move(unlock), f]() {
+ return unlock(f);
+ });
+ });
+ });
+ }
+
+ TOptions Options_;
+};
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
new file mode 100644
index 0000000000..a048a872ab
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.cpp
@@ -0,0 +1,159 @@
+#include "yql_yt_lambda_builder.h"
+
+#include "yql_yt_native.h"
+#include "yql_yt_session.h"
+
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h>
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+#include <ydb/library/yql/providers/common/mkql/yql_type_mkql.h>
+#include <ydb/library/yql/providers/common/comp_nodes/yql_factory.h>
+#include <ydb/library/yql/parser/pg_wrapper/interface/comp_factory.h>
+
+#include <ydb/library/yql/public/udf/udf_value.h>
+#include <ydb/library/yql/minikql/mkql_alloc.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_serialization.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_string_util.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/ylimits.h>
+
+namespace NYql {
+
+namespace NNative {
+
+using namespace NNodes;
+using namespace NCommon;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetGatewayNodeFactory(TMkqlWriterImpl* writer, TUserFiles::TPtr files) {
+ TMaybe<ui32> exprContextObject;
+ return [exprContextObject, writer, files](NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx) mutable -> IComputationNode* {
+ if (callable.GetType()->GetName() == TYtTablePath::CallableName()
+ || callable.GetType()->GetName() == TYtTableIndex::CallableName()
+ || callable.GetType()->GetName() == TYtTableRecord::CallableName()
+ || callable.GetType()->GetName() == TYtIsKeySwitch::CallableName()
+ || callable.GetType()->GetName() == TYtRowNumber::CallableName()
+ ) {
+ return ctx.NodeFactory.CreateImmutableNode(NUdf::TUnboxedValuePod::Zero());
+ }
+
+ if (files) {
+ if (callable.GetType()->GetName() == "FilePathJob" || callable.GetType()->GetName() == "FileContentJob") {
+ const TString fullFileName(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ auto path = files->GetFile(fullFileName)->Path->GetPath();
+ auto content = callable.GetType()->GetName() == "FileContentJob" ? TFileInput(path).ReadAll() : path.GetPath();
+ return ctx.NodeFactory.CreateImmutableNode(MakeString(content));
+ }
+ }
+
+ if (callable.GetType()->GetName() == "YtOutput") {
+ YQL_ENSURE(writer);
+ return WrapYtOutput(callable, ctx, *writer);
+ }
+
+ if (!exprContextObject) {
+ exprContextObject = ctx.Mutables.CurValueIndex++;
+ }
+
+ auto yql = GetYqlFactory(*exprContextObject)(callable, ctx);
+ if (yql) {
+ return yql;
+ }
+
+ auto pg = GetPgFactory()(callable, ctx);
+ if (pg) {
+ return pg;
+ }
+
+ return GetBuiltinFactory()(callable, ctx);
+ };
+}
+
+
+TNativeYtLambdaBuilder::TNativeYtLambdaBuilder(TScopedAlloc& alloc, const IFunctionRegistry* functionRegistry, const TSession& session,
+ const NKikimr::NUdf::ISecureParamsProvider* secureParamsProvider)
+ : TLambdaBuilder(functionRegistry, alloc, nullptr,
+ session.RandomProvider_, session.TimeProvider_, nullptr, nullptr, secureParamsProvider)
+{
+}
+
+TNativeYtLambdaBuilder::TNativeYtLambdaBuilder(TScopedAlloc& alloc, const TYtNativeServices& services, const TSession& session)
+ : TNativeYtLambdaBuilder(alloc, services.FunctionRegistry, session)
+{
+}
+
+TString TNativeYtLambdaBuilder::BuildLambdaWithIO(const IMkqlCallableCompiler& compiler, TCoLambda lambda, TExprContext& exprCtx) {
+ TProgramBuilder pgmBuilder(GetTypeEnvironment(), *FunctionRegistry);
+ TArgumentsMap arguments(1U);
+ if (lambda.Args().Size() > 0) {
+ const auto arg = lambda.Args().Arg(0);
+ const auto argType = arg.Ref().GetTypeAnn();
+ auto inputItemType = NCommon::BuildType(arg.Ref(), *GetSeqItemType(argType), pgmBuilder);
+ switch (bool isStream = true; argType->GetKind()) {
+ case ETypeAnnotationKind::Flow:
+ if (ETypeAnnotationKind::Multi == argType->Cast<TFlowExprType>()->GetItemType()->GetKind()) {
+ arguments.emplace(arg.Raw(), TRuntimeNode(TCallableBuilder(GetTypeEnvironment(), "YtInput", pgmBuilder.NewFlowType(inputItemType)).Build(), false));
+ break;
+ }
+ isStream = false;
+ [[fallthrough]]; // AUTOGENERATED_FALLTHROUGH_FIXME
+ case ETypeAnnotationKind::Stream: {
+ auto inputStream = pgmBuilder.SourceOf(isStream ?
+ pgmBuilder.NewStreamType(pgmBuilder.GetTypeEnvironment().GetTypeOfVoid()) : pgmBuilder.NewFlowType(pgmBuilder.GetTypeEnvironment().GetTypeOfVoid()));
+
+ inputItemType = pgmBuilder.NewOptionalType(inputItemType);
+ inputStream = pgmBuilder.Map(inputStream, [&] (TRuntimeNode item) {
+ TCallableBuilder inputCall(GetTypeEnvironment(), "YtInput", inputItemType);
+ inputCall.Add(item);
+ return TRuntimeNode(inputCall.Build(), false);
+ });
+ inputStream = pgmBuilder.TakeWhile(inputStream, [&] (TRuntimeNode item) {
+ return pgmBuilder.Exists(item);
+ });
+
+ inputStream = pgmBuilder.FlatMap(inputStream, [&] (TRuntimeNode item) {
+ return item;
+ });
+
+ arguments[arg.Raw()] = inputStream;
+ break;
+ }
+ default:
+ YQL_ENSURE(false, "Unsupported lambda argument type: " << arg.Ref().GetTypeAnn()->GetKind());
+ }
+ }
+ TMkqlBuildContext ctx(compiler, pgmBuilder, exprCtx, lambda.Ref().UniqueId(), std::move(arguments));
+ TRuntimeNode outStream = MkqlBuildExpr(lambda.Body().Ref(), ctx);
+ if (outStream.GetStaticType()->IsFlow()) {
+ TCallableBuilder outputCall(GetTypeEnvironment(), "YtOutput", pgmBuilder.NewFlowType(GetTypeEnvironment().GetTypeOfVoid()));
+ outputCall.Add(outStream);
+ outStream = TRuntimeNode(outputCall.Build(), false);
+ } else {
+ outStream = pgmBuilder.Map(outStream, [&] (TRuntimeNode item) {
+ TCallableBuilder outputCall(GetTypeEnvironment(), "YtOutput", GetTypeEnvironment().GetTypeOfVoid());
+ outputCall.Add(item);
+ return TRuntimeNode(outputCall.Build(), false);
+ });
+ }
+ outStream = pgmBuilder.Discard(outStream);
+
+ return SerializeRuntimeNode(outStream, GetTypeEnvironment());
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.h
new file mode 100644
index 0000000000..fbc9a4f50f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_lambda_builder.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/user_files.h>
+
+namespace NKikimr {
+
+namespace NMiniKQL {
+class TScopedAlloc;
+class IFunctionRegistry;
+}
+
+}
+
+namespace NYql {
+
+namespace NUdf {
+class ISecureParamsProvider;
+}
+
+namespace NCommon {
+class IMkqlCallableCompiler;
+}
+
+struct TYtNativeServices;
+
+namespace NNative {
+
+struct TSession;
+
+struct TNativeYtLambdaBuilder: public TLambdaBuilder {
+ TNativeYtLambdaBuilder(NKikimr::NMiniKQL::TScopedAlloc& alloc, const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry,
+ const TSession& session, const NKikimr::NUdf::ISecureParamsProvider* secureParamsProvider = nullptr);
+
+ TNativeYtLambdaBuilder(NKikimr::NMiniKQL::TScopedAlloc& alloc, const TYtNativeServices& services, const TSession& session);
+
+ TString BuildLambdaWithIO(const NCommon::IMkqlCallableCompiler& compiler, NNodes::TCoLambda lambda, TExprContext& exprCtx);
+};
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetGatewayNodeFactory(TMkqlWriterImpl* writer = nullptr, TUserFiles::TPtr files = nullptr);
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
new file mode 100644
index 0000000000..e790df6aea
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.cpp
@@ -0,0 +1,4907 @@
+#include "yql_yt_native.h"
+
+#include "yql_yt_lambda_builder.h"
+#include "yql_yt_qb2.h"
+#include "yql_yt_session.h"
+#include "yql_yt_spec.h"
+#include "yql_yt_transform.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h>
+#include <ydb/library/yql/providers/yt/lib/log/yt_logger.h>
+
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h>
+#include <ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h>
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h>
+#include <ydb/library/yql/providers/yt/lib/schema/schema.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/job/yql_job_base.h>
+#include <ydb/library/yql/providers/yt/job/yql_job_calc.h>
+#include <ydb/library/yql/providers/yt/job/yql_job_registry.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_helpers.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h>
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h>
+#include <ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h>
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/issue/yql_issue.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node_serialization.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/threading/future/async.h>
+#include <library/cpp/threading/future/future.h>
+#include <library/cpp/containers/sorted_vector/sorted_vector.h>
+#include <library/cpp/streams/brotli/brotli.h>
+
+#include <util/folder/tempdir.h>
+#include <util/generic/ptr.h>
+#include <util/generic/yexception.h>
+#include <util/generic/xrange.h>
+#include <util/generic/size_literals.h>
+#include <util/generic/scope.h>
+#include <util/stream/null.h>
+#include <util/stream/str.h>
+#include <util/stream/input.h>
+#include <util/stream/file.h>
+#include <util/system/execpath.h>
+#include <util/system/guard.h>
+#include <util/system/shellcommand.h>
+#include <util/ysaveload.h>
+
+#include <algorithm>
+#include <iterator>
+#include <variant>
+#include <exception>
+
+namespace NYql {
+
+using namespace NYT;
+using namespace NCommon;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NNodes;
+using namespace NThreading;
+
+namespace NNative {
+
+namespace {
+ THashMap<TString, std::pair<NYT::TNode, TString>> TestTables;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void DumpLocalTable(const TString& tableContent, const TString& path) {
+ if (!path) {
+ return;
+ }
+
+ TFileOutput out(path);
+ out.Write(tableContent.Data(), tableContent.Size());
+ out.Flush();
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+template<typename TOptions>
+struct TMetaPerServerRequest {
+ TVector<size_t> TableIndicies;
+ typename TExecContext<TOptions>::TPtr ExecContext;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TString ToColumn(const NYT::TSortColumn& item) {
+ return TStringBuilder() << item.Name() << '(' << (item.SortOrder() == NYT::ESortOrder::SO_ASCENDING) << ')';
+}
+
+TString ToColumn(const std::pair<TString, int>& item) {
+ return TStringBuilder() << item.first << '(' << bool(item.second) << ')';
+}
+
+template <typename T>
+TString ToColumnList(const TVector<T>& list) {
+ TStringBuilder builder;
+ builder << '[';
+ for (auto& col: list) {
+ builder << ToColumn(col) << ',';
+ }
+ if (!list.empty()) {
+ builder.pop_back();
+ }
+ return (builder << ']');
+}
+
+TString GetType(const NYT::TNode& attr) {
+ if (!attr.HasKey("type")) {
+ return "unknown";
+ }
+
+ return attr["type"].AsString();
+}
+
+TString GetAttrType(const NYT::TNode& node) {
+ if (!node.HasAttributes()) {
+ return "unknown";
+ }
+
+ return GetType(node.GetAttributes());
+}
+
+const NYT::TJobBinaryConfig GetJobBinary(const NYT::TRawMapOperationSpec& spec) {
+ return spec.MapperSpec_.GetJobBinary();
+}
+
+const NYT::TJobBinaryConfig GetJobBinary(const NYT::TRawMapReduceOperationSpec& spec) {
+ return spec.MapperSpec_.GetJobBinary();
+}
+
+const NYT::TJobBinaryConfig GetJobBinary(const NYT::TRawReduceOperationSpec& spec) {
+ return spec.ReducerSpec_.GetJobBinary();
+}
+
+const TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> GetLocalFiles(const NYT::TRawMapOperationSpec& spec) {
+ return spec.MapperSpec_.GetLocalFiles();
+}
+
+const TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> GetLocalFiles(const NYT::TRawMapReduceOperationSpec& spec) {
+ return spec.MapperSpec_.GetLocalFiles();
+}
+
+const TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> GetLocalFiles(const NYT::TRawReduceOperationSpec& spec) {
+ return spec.ReducerSpec_.GetLocalFiles();
+}
+
+const TVector<TRichYPath> GetRemoteFiles(const NYT::TRawMapOperationSpec& spec) {
+ return spec.MapperSpec_.Files_;
+}
+
+const TVector<TRichYPath> GetRemoteFiles(const NYT::TRawMapReduceOperationSpec& spec) {
+ return spec.MapperSpec_.Files_;
+}
+
+const TVector<TRichYPath> GetRemoteFiles(const NYT::TRawReduceOperationSpec& spec) {
+ return spec.ReducerSpec_.Files_;
+}
+
+template <typename TType>
+inline TType OptionFromString(const TString& value) {
+ if constexpr (std::is_same_v<TString, TType>) {
+ return value;
+ } else if constexpr (std::is_same_v<NYT::TNode, TType>) {
+ return NYT::NodeFromYsonString(value);
+ } else {
+ return FromString<TType>(value);
+ }
+}
+
+template <typename TType>
+inline TType OptionFromNode(const NYT::TNode& value) {
+ if constexpr (std::is_same_v<NYT::TNode, TType>) {
+ return value;
+ } else if constexpr (std::is_integral_v<TType>) {
+ return static_cast<TType>(value.AsInt64());
+ } else {
+ return FromString<TType>(value.AsString());
+ }
+}
+
+} // unnamed
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYtNativeGateway : public IYtGateway {
+public:
+ TYtNativeGateway(const TYtNativeServices& services)
+ : Services_(services)
+ , Clusters_(MakeIntrusive<TConfigClusters>(*Services_.Config))
+ , MkqlCompiler_(MakeIntrusive<NCommon::TMkqlCommonCallableCompiler>())
+ , UrlMapper_(*Services_.Config)
+ {
+ RegisterYtMkqlCompilers(*MkqlCompiler_);
+ SetYtLoggerGlobalBackend(
+ Services_.Config->HasYtLogLevel() ? Services_.Config->GetYtLogLevel() : -1,
+ Services_.Config->GetYtDebugLogSize(),
+ Services_.Config->GetYtDebugLogFile(),
+ Services_.Config->GetYtDebugLogAlwaysWrite()
+ );
+ }
+
+ ~TYtNativeGateway() {
+ }
+
+ void OpenSession(TOpenSessionOptions&& options) final {
+ TString sessionId = options.SessionId();
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ TSession::TPtr session = MakeIntrusive<TSession>(std::move(options), Services_.Config->GetGatewayThreads());
+ with_lock(Mutex_) {
+ if (!Sessions_.insert({sessionId, session}).second) {
+ YQL_LOG_CTX_THROW yexception() << "Session already exists: " << sessionId;
+ }
+ }
+ }
+
+ void CloseSession(TCloseSessionOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ with_lock(Mutex_) {
+ auto it = Sessions_.find(options.SessionId());
+ if (it != Sessions_.end()) {
+ auto session = it->second;
+ Sessions_.erase(it);
+ session->Close();
+ session.Drop();
+ }
+ }
+ } catch (const yexception& e) {
+ YQL_CLOG(ERROR, ProviderYt) << e.what();
+ }
+ }
+
+ void CleanupSession(TCleanupSessionOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ if (auto session = GetSession(options.SessionId(), false)) {
+ session->TxCache_.AbortAll();
+ if (session->OperationSemaphore) {
+ session->OperationSemaphore->Cancel();
+ session->OperationSemaphore.Drop();
+ }
+ }
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+
+ TFuture<TFinalizeResult> Finalize(TFinalizeOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ auto logCtx = NYql::NLog::CurrentLogContextPath();
+ return session->Queue_->Async([session, logCtx, abort=options.Abort()] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ return ExecFinalize(session, abort);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TFinalizeResult>());
+ }
+ }
+
+ TFuture<TCanonizePathsResult> CanonizePaths(TCanonizePathsOptions&& options) final {
+ if (Services_.Config->GetLocalChainTest()) {
+ TCanonizePathsResult res;
+ std::transform(
+ options.Paths().begin(), options.Paths().end(),
+ std::back_inserter(res.Data),
+ [] (const TCanonizeReq& req) {
+ return CanonizedPath(req.Path());
+ });
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+
+ TVector<TCanonizeReq> paths(std::move(options.Paths()));
+ if (YQL_CLOG_ACTIVE(INFO, ProviderYt)) {
+ for (size_t i: xrange(Min<size_t>(paths.size(), 10))) {
+ YQL_CLOG(INFO, ProviderYt) << paths[i].Cluster() << '.' << paths[i].Path();
+ }
+ if (paths.size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total tables=" << paths.size();
+ }
+ }
+
+ THashMap<TString, TMetaPerServerRequest<TCanonizePathsOptions>> reqPerServer;
+ for (size_t i: xrange(paths.size())) {
+ TCanonizeReq& path = paths[i];
+ auto cluster = path.Cluster();
+ auto ytServer = Clusters_->GetServer(cluster);
+ auto& r = reqPerServer[ytServer];
+ if (r.TableIndicies.empty()) {
+ r.ExecContext = MakeExecCtx(TCanonizePathsOptions(options), session, cluster, nullptr, nullptr);
+ }
+ TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ path.Path(NYql::TransformPath(tmpFolder, path.Path(), false, session->UserName_));
+ r.TableIndicies.push_back(i);
+ }
+
+ auto logCtx = NYql::NLog::CurrentLogContextPath();
+ return session->Queue_->Async([session, paths = std::move(paths), reqPerServer = std::move(reqPerServer), logCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ return ExecCanonizePaths(paths, reqPerServer);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TCanonizePathsResult>());
+ }
+ }
+
+ TFuture<TTableInfoResult> GetTableInfo(TGetTableInfoOptions&& options) final {
+ if (Services_.Config->GetLocalChainTest()) {
+ TTableInfoResult result;
+ for (const auto& t : options.Tables()) {
+ const auto it = TestTables.find(t.Table());
+ result.Data.emplace_back();
+ auto& table = result.Data.back();
+ table.Meta = MakeIntrusive<TYtTableMetaInfo>();
+ if (table.Meta->DoesExist = TestTables.cend() != it) {
+ table.Meta->Attrs.emplace(YqlRowSpecAttribute, NYT::NodeToYsonString(it->second.first));
+ table.Stat = MakeIntrusive<TYtTableStatInfo>();
+ table.Stat->Id = "stub";
+ // Prevent empty table optimizations
+ table.Stat->RecordsCount = 1;
+ table.Stat->DataSize = 1;
+ table.Stat->ChunkCount = 1;
+ }
+
+ table.WriteLock = HasModifyIntents(t.Intents());
+ }
+ result.SetSuccess();
+ return MakeFuture<TTableInfoResult>(std::move(result));
+ }
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+
+ TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+
+ YQL_CLOG(INFO, ProviderYt) << "ReadOnly=" << options.ReadOnly() << ", Epoch=" << options.Epoch();
+ TVector<TTableReq> tables(std::move(options.Tables()));
+ if (YQL_CLOG_ACTIVE(INFO, ProviderYt)) {
+ for (size_t i: xrange(Min<size_t>(tables.size(), 10))) {
+ YQL_CLOG(INFO, ProviderYt) << tables[i].Cluster() << '.' << tables[i].Table()
+ << ", LockOnly=" << tables[i].LockOnly() << ", Intents=" << tables[i].Intents();
+ }
+ if (tables.size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total tables=" << tables.size();
+ }
+ }
+
+ THashMap<TString, TMetaPerServerRequest<TGetTableInfoOptions>> reqPerServer;
+ for (size_t i: xrange(tables.size())) {
+ TTableReq& table = tables[i];
+ auto cluster = table.Cluster();
+ auto ytServer = Clusters_->GetServer(cluster);
+ auto& r = reqPerServer[ytServer];
+ if (r.TableIndicies.empty()) {
+ r.ExecContext = MakeExecCtx(TGetTableInfoOptions(options), session, cluster, nullptr, nullptr);
+ }
+ table.Table(NYql::TransformPath(tmpFolder, table.Table(), table.Anonymous(), session->UserName_));
+ r.TableIndicies.push_back(i);
+ }
+
+ auto logCtx = NYql::NLog::CurrentLogContextPath();
+ bool readOnly = options.ReadOnly();
+ ui32 epoch = options.Epoch();
+ return session->Queue_->Async([session, tables = std::move(tables), reqPerServer = std::move(reqPerServer), readOnly, epoch, logCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ return ExecGetTableInfo(tables, reqPerServer, readOnly, epoch);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TTableInfoResult>());
+ }
+ }
+
+ TFuture<TTableRangeResult> GetTableRange(TTableRangeOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+
+ YQL_CLOG(INFO, ProviderYt) << "Server=" << options.Cluster() << ", Prefix=" << options.Prefix()
+ << ", Suffix=" << options.Suffix() << ", Filter=" << (options.Filter() ? "(present)" : "null");
+
+ auto pos = options.Pos();
+
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+
+ TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TString tmpTablePath = NYql::TransformPath(tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(session->RandomProvider_->GenGuid()), true, session->UserName_);
+
+ auto cluster = options.Cluster();
+ auto filter = options.Filter();
+ auto exprCtx = options.ExprCtx();
+
+ TExpressionResorceUsage extraUsage;
+ TString lambda;
+ if (filter) {
+ YQL_ENSURE(exprCtx);
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ Services_.FunctionRegistry->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ NKikimr::NMiniKQL::TTypeEnvironment typeEnv(alloc);
+ TProgramBuilder pgmBuilder(typeEnv, *Services_.FunctionRegistry);
+
+ auto returnType = pgmBuilder.NewListType(pgmBuilder.NewTupleType({
+ pgmBuilder.NewDataType(NUdf::TDataType<char*>::Id),
+ pgmBuilder.NewDataType(NUdf::TDataType<ui64>::Id)
+ }));
+ TCallableBuilder inputNodeBuilder(typeEnv, MrRangeInputListInternal, returnType);
+ auto inputNode = TRuntimeNode(inputNodeBuilder.Build(), false);
+ auto pgm = pgmBuilder.Filter(inputNode, [&](TRuntimeNode item) {
+ TMkqlBuildContext ctx(*MkqlCompiler_, pgmBuilder, *exprCtx, filter->UniqueId(), {{&filter->Head().Head(), pgmBuilder.Nth(item, 0)}});
+ return pgmBuilder.Coalesce(MkqlBuildExpr(filter->Tail(), ctx), pgmBuilder.NewDataLiteral(false));
+ });
+ lambda = SerializeRuntimeNode(pgm, typeEnv);
+ extraUsage = ScanExtraResourceUsage(filter->Tail(), *options.Config());
+ }
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, filter, exprCtx);
+ if (lambda) {
+ return session->Queue_->Async([execCtx, tmpTablePath, lambda, extraUsage] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecGetTableRange(execCtx, tmpTablePath, lambda, extraUsage);
+ });
+ }
+
+ return session->Queue_->Async([execCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ return ExecGetTableRange(execCtx);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TTableRangeResult>(pos));
+ }
+ }
+
+ TFuture<TFolderResult> GetFolder(TFolderOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+
+ YQL_CLOG(INFO, ProviderYt) << "Server=" << options.Cluster() << ", Prefix=" << options.Prefix();
+
+ auto pos = options.Pos();
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+
+ auto cluster = options.Cluster();
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+
+ return session->Queue_->Async([execCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ return ExecGetFolder(execCtx);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TFolderResult>(pos));
+ }
+ }
+
+ TFuture<TResOrPullResult> ResOrPull(const TExprNode::TPtr& node, TExprContext& ctx, TResOrPullOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ YQL_CLOG(INFO, ProviderYt) << node->Content();
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+ if (auto pull = TMaybeNode<TPull>(node)) {
+ return DoPull(session, pull.Cast(), ctx, std::move(options));
+ } else if (auto result = TMaybeNode<TResult>(node)) {
+ return DoResult(session, result.Cast(), ctx, std::move(options));
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Don't know how to execute " << node->Content();
+ }
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TResOrPullResult>());
+ }
+ }
+
+ TFuture<TRunResult> Run(const TExprNode::TPtr& node, TExprContext& ctx, TRunOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ auto nodePos = ctx.GetPosition(node->Pos());
+ try {
+ YQL_CLOG(INFO, ProviderYt) << node->Content();
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+
+ TYtOpBase opBase(node);
+
+ auto cluster = TString{opBase.DataSink().Cluster().Value()};
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, opBase.Raw(), &ctx);
+
+ if (auto transientOp = opBase.Maybe<TYtTransientOpBase>()) {
+ THashSet<TString> extraSysColumns;
+ if (NYql::HasSetting(transientOp.Settings().Ref(), EYtSettingType::KeySwitch)
+ && !transientOp.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>().IsValid()) {
+ extraSysColumns.insert("keyswitch");
+ }
+ execCtx->SetInput(transientOp.Cast().Input(), opBase.Maybe<TYtWithUserJobsOpBase>().IsValid(), extraSysColumns);
+ }
+ if (auto outputOp = opBase.Maybe<TYtOutputOpBase>()) {
+ execCtx->SetOutput(outputOp.Cast().Output());
+ }
+
+ TFuture<void> future;
+ if (auto op = opBase.Maybe<TYtSort>()) {
+ future = DoSort(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtCopy>()) {
+ future = DoCopy(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtMerge>()) {
+ future = DoMerge(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtMap>()) {
+ future = DoMap(op.Cast(), execCtx, ctx);
+ } else if (auto op = opBase.Maybe<TYtReduce>()) {
+ future = DoReduce(op.Cast(), execCtx, ctx);
+ } else if (auto op = opBase.Maybe<TYtMapReduce>()) {
+ future = DoMapReduce(op.Cast(), execCtx, ctx);
+ } else if (auto op = opBase.Maybe<TYtFill>()) {
+ future = DoFill(op.Cast(), execCtx, ctx);
+ } else if (auto op = opBase.Maybe<TYtTouch>()) {
+ future = DoTouch(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtDropTable>()) {
+ future = DoDrop(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtStatOut>()) {
+ future = DoStatOut(op.Cast(), execCtx);
+ } else if (auto op = opBase.Maybe<TYtDqProcessWrite>()) {
+ future = DoTouch(op.Cast(), execCtx); // Do touch just for creating temporary tables.
+ } else {
+ ythrow yexception() << "Don't know how to execute " << node->Content();
+ }
+
+ if (Services_.Config->GetLocalChainTest()) {
+ return future.Apply([execCtx](const TFuture<void>&) {
+ TRunResult result;
+ result.OutTableStats.reserve(execCtx->OutTables_.size());
+ for (const auto& table : execCtx->OutTables_) {
+ result.OutTableStats.emplace_back(table.Name, MakeIntrusive<TYtTableStatInfo>());
+ result.OutTableStats.back().second->Id = "stub";
+ }
+ result.SetSuccess();
+ return MakeFuture<TRunResult>(std::move(result));
+ });
+ }
+
+ return future.Apply([execCtx, pos = nodePos](const TFuture<void>& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ try {
+ f.GetValue(); // rethrow error if any
+ execCtx->StoreQueryCache();
+ execCtx->SetNodeExecProgress("Fetching attributes of output tables");
+ return MakeRunResult(execCtx->OutTables_, execCtx->GetEntry());
+ } catch (...) {
+ return ResultFromCurrentException<TRunResult>(pos);
+ }
+ });
+
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TRunResult>(nodePos));
+ }
+ }
+
+ TFuture<TCalcResult> Calc(const TExprNode::TListType& nodes, TExprContext& ctx, TCalcOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+ auto cluster = options.Cluster();
+
+ if (YQL_CLOG_ACTIVE(INFO, ProviderYt)) {
+ for (size_t i: xrange(Min<size_t>(nodes.size(), 10))) {
+ YQL_CLOG(INFO, ProviderYt) << "Cluster=" << cluster << ": " << nodes[i]->Content();
+ }
+ if (nodes.size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total nodes to calc=" << nodes.size();
+ }
+ }
+
+ TExpressionResorceUsage extraUsage;
+ TString lambda;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ Services_.FunctionRegistry->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *session);
+ TVector<TRuntimeNode> tupleNodes;
+ for (auto& node: nodes) {
+ tupleNodes.push_back(builder.BuildLambda(*MkqlCompiler_, node, ctx));
+ auto nodeUsage = ScanExtraResourceUsage(*node, *options.Config());
+ extraUsage.Cpu = Max<double>(extraUsage.Cpu, nodeUsage.Cpu);
+ extraUsage.Memory = Max<ui64>(extraUsage.Memory, nodeUsage.Memory);
+ }
+ lambda = SerializeRuntimeNode(builder.MakeTuple(tupleNodes), builder.GetTypeEnvironment());
+ }
+
+ TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TString tmpTablePath = NYql::TransformPath(tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(session->RandomProvider_->GenGuid()), true, session->UserName_);
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+
+ return session->Queue_->Async([execCtx, lambda, extraUsage, tmpTablePath] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecCalc(lambda, extraUsage, tmpTablePath, execCtx, {}, TNodeResultFactory());
+ })
+ .Apply([execCtx](const TFuture<NYT::TNode>& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ try {
+ const NYT::TNode& node = f.GetValue();
+ YQL_ENSURE(node.IsList());
+ TCalcResult res;
+ for (auto& n: node.AsList()) {
+ res.Data.push_back(n);
+ }
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TCalcResult>();
+ }
+ });
+
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TCalcResult>());
+ }
+ }
+
+ TFuture<TPublishResult> Publish(const TExprNode::TPtr& node, TExprContext& ctx, TPublishOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ auto nodePos = ctx.GetPosition(node->Pos());
+ try {
+ auto publish = TYtPublish(node);
+ EYtWriteMode mode = EYtWriteMode::Renew;
+ if (const auto modeSetting = NYql::GetSetting(publish.Settings().Ref(), EYtSettingType::Mode)) {
+ mode = FromString<EYtWriteMode>(modeSetting->Child(1)->Content());
+ }
+ const bool initial = NYql::HasSetting(publish.Settings().Ref(), EYtSettingType::Initial);
+ const bool monotonicKeys = NYql::HasSetting(publish.Settings().Ref(), EYtSettingType::MonotonicKeys);
+
+ std::unordered_map<EYtSettingType, TString> strOpts;
+ for (const auto& setting : publish.Settings().Ref().Children()) {
+ if (setting->ChildrenSize() == 2) {
+ strOpts.emplace(FromString<EYtSettingType>(setting->Head().Content()), setting->Tail().Content());
+ } else if (setting->ChildrenSize() == 1) {
+ strOpts.emplace(FromString<EYtSettingType>(setting->Head().Content()), TString());;
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Mode: " << mode << ", IsInitial: " << initial;
+
+ TSession::TPtr session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+
+ auto cluster = publish.DataSink().Cluster().StringValue();
+
+ TVector<TString> src;
+ ui64 chunksCount = 0;
+ ui64 dataSize = 0;
+ for (auto out: publish.Input()) {
+ auto outTable = GetOutTable(out).Cast<TYtOutTable>();
+ src.emplace_back(outTable.Name().Value());
+ auto stat = TYtTableStatInfo(outTable.Stat());
+ chunksCount += stat.ChunkCount;
+ dataSize += stat.DataSize;
+ if (src.size() <= 10) {
+ YQL_CLOG(INFO, ProviderYt) << "Input: " << cluster << '.' << src.back();
+ }
+ }
+ if (src.size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total input tables=" << src.size();
+ }
+
+ bool combineChunks = false;
+ if (auto minChunkSize = options.Config()->MinPublishedAvgChunkSize.Get()) {
+ combineChunks = *minChunkSize == 0
+ || (chunksCount > 1 && dataSize > chunksCount && (dataSize / chunksCount) < minChunkSize->GetValue());
+ }
+
+ const auto dst = publish.Publish().Name().StringValue();
+ YQL_CLOG(INFO, ProviderYt) << "Output: " << cluster << '.' << dst;
+ if (combineChunks) {
+ YQL_CLOG(INFO, ProviderYt) << "Use chunks combining";
+ }
+ if (Services_.Config->GetLocalChainTest()) {
+ if (!src.empty()) {
+ const auto& path = NYql::TransformPath(options.Config()->TablesTmpFolder.Get().GetOrElse(TString()), src.front(), true, session->UserName_);
+ const auto it = TestTables.find(path);
+ YQL_ENSURE(TestTables.cend() != it);
+ YQL_ENSURE(TestTables.emplace(dst, it->second).second);
+ }
+
+ TPublishResult result;
+ result.SetSuccess();
+ return MakeFuture<TPublishResult>(std::move(result));
+ }
+
+
+ bool isAnonymous = NYql::HasSetting(publish.Publish().Settings().Ref(), EYtSettingType::Anonymous);
+ const ui32 dstEpoch = TEpochInfo::Parse(publish.Publish().Epoch().Ref()).GetOrElse(0);
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, node.Get(), &ctx);
+
+ return session->Queue_->Async([execCtx, src = std::move(src), dst, dstEpoch, isAnonymous, mode, initial, monotonicKeys, combineChunks, strOpts = std::move(strOpts)] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ return ExecPublish(execCtx, src, dst, dstEpoch, isAnonymous, mode, initial, monotonicKeys, combineChunks, strOpts);
+ })
+ .Apply([nodePos] (const TFuture<void>& f) {
+ try {
+ f.GetValue();
+ TPublishResult res;
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TPublishResult>(nodePos);
+ }
+ });
+
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TPublishResult>(nodePos));
+ }
+ }
+
+ TFuture<TCommitResult> Commit(TCommitOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ auto cluster = options.Cluster();
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+ return session->Queue_->Async([execCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ try {
+ execCtx->Session_->TxCache_.Commit(execCtx->YtServer_);
+
+ TCommitResult res;
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TCommitResult>();
+ }
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TCommitResult>());
+ }
+ }
+
+ TFuture<TDropTrackablesResult> DropTrackables(TDropTrackablesOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+
+ if (YQL_CLOG_ACTIVE(INFO, ProviderYt)) {
+ for (size_t i: xrange(Min<size_t>(options.Pathes().size(), 10))) {
+ const auto& path = options.Pathes()[i].Path;
+ const auto& cluster = options.Pathes()[i].Cluster;
+ YQL_CLOG(INFO, ProviderYt) << "Dropping temporary table '" << path << "' on cluster '" << cluster << "'";
+ }
+ if (options.Pathes().size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total dropping tables=" << options.Pathes().size();
+ }
+ }
+
+ THashMap<TString, TVector<TString>> pathsByCluster;
+ for (const auto& i : options.Pathes()) {
+ pathsByCluster[i.Cluster].push_back(i.Path);
+ }
+
+ TVector<TFuture<void>> futures;
+ for (const auto& i : pathsByCluster) {
+ auto cluster = i.first;
+ auto paths = i.second;
+
+
+ auto execCtx = MakeExecCtx(TDropTrackablesOptions(options), session, cluster, nullptr, nullptr);
+
+ futures.push_back(session->Queue_->Async([execCtx, paths] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ return ExecDropTrackables(paths, execCtx);
+ }));
+ }
+
+ return WaitExceptionOrAll(futures).Apply([] (const TFuture<void>& f) {
+ try {
+ f.GetValue(); // rethrow error if any
+
+ TDropTrackablesResult res;
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TDropTrackablesResult>();
+ }
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TDropTrackablesResult>());
+ }
+ }
+
+ TFuture<TPathStatResult> PathStat(TPathStatOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ auto logCtx = NYql::NLog::CurrentLogContextPath();
+ auto cluster = options.Cluster();
+
+ if (YQL_CLOG_ACTIVE(INFO, ProviderYt)) {
+ for (size_t i: xrange(Min<size_t>(options.Paths().size(), 10))) {
+ YQL_CLOG(INFO, ProviderYt) << "Cluster: " << cluster << ", table: " << NYT::NodeToYsonString(NYT::PathToNode(options.Paths()[i].Path()));
+ }
+ if (options.Paths().size() > 10) {
+ YQL_CLOG(INFO, ProviderYt) << "...total tables=" << options.Paths().size();
+ }
+ }
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+
+ return session->Queue_->Async([execCtx, logCtx] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ bool onlyCached = false;
+ return ExecPathStat(execCtx, onlyCached);
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TPathStatResult>());
+ }
+ }
+
+ TPathStatResult TryPathStat(TPathStatOptions&& options) final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+
+ TSession::TPtr session = GetSession(options.SessionId());
+ auto logCtx = NYql::NLog::CurrentLogContextPath();
+ auto cluster = options.Cluster();
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ bool onlyCached = true;
+ return ExecPathStat(execCtx, onlyCached);
+ }
+
+ bool TryParseYtUrl(const TString& url, TString* cluster, TString* path) const final {
+ TString server;
+ if (!UrlMapper_.MapYtUrl(url, &server, path)) {
+ return false;
+ }
+
+ if (cluster) {
+ *cluster = Clusters_->GetNameByYtName(server);
+ }
+ return true;
+ }
+
+ TString GetDefaultClusterName() const final {
+ return Clusters_->GetDefaultClusterName();
+ }
+
+ TString GetClusterServer(const TString& cluster) const final {
+ return Clusters_->TryGetServer(cluster);
+ }
+
+ NYT::TRichYPath GetRealTable(const TString& sessionId, const TString& cluster, const TString& table, ui32 epoch, const TString& tmpFolder) const final {
+ auto richYPath = NYT::TRichYPath(table);
+ if (TSession::TPtr session = GetSession(sessionId, true)) {
+ if (auto ytServer = Clusters_->TryGetServer(cluster)) {
+ auto entry = session->TxCache_.GetEntry(ytServer);
+ if (auto p = entry->Snapshots.FindPtr(std::make_pair(table, epoch))) {
+ richYPath.Path(std::get<0>(*p)).TransactionId(std::get<1>(*p)).OriginalPath(NYT::AddPathPrefix(table, NYT::TConfig::Get()->Prefix));
+ } else {
+ auto realTableName = NYql::TransformPath(tmpFolder, table, true, session->UserName_);
+ realTableName = NYT::AddPathPrefix(realTableName, NYT::TConfig::Get()->Prefix);
+ richYPath = NYT::TRichYPath(realTableName);
+ richYPath.TransactionId(session->TxCache_.GetEntry(ytServer)->Tx->GetId());
+ }
+ }
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Real table path: " << NYT::NodeToYsonString(NYT::PathToNode(richYPath), NYT::NYson::EYsonFormat::Text);
+ return richYPath;
+ }
+
+ NYT::TRichYPath GetWriteTable(const TString& sessionId, const TString& cluster, const TString& table, const TString& tmpFolder) const final {
+ if (TSession::TPtr session = GetSession(sessionId, true)) {
+ if (auto ytServer = Clusters_->TryGetServer(cluster)) {
+ auto entry = session->TxCache_.GetEntry(ytServer);
+ auto realTableName = NYql::TransformPath(tmpFolder, table, true, session->UserName_);
+ realTableName = NYT::AddPathPrefix(realTableName, NYT::TConfig::Get()->Prefix);
+ auto richYPath = NYT::TRichYPath(realTableName);
+ richYPath.TransactionId(entry->Tx->GetId());
+ YQL_CLOG(DEBUG, ProviderYt) << "Write table path: " << NYT::NodeToYsonString(NYT::PathToNode(richYPath), NYT::NYson::EYsonFormat::Text);
+ return richYPath;
+ }
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "(Alternative) Write table path: " << NYT::NodeToYsonString(NYT::PathToNode(NYT::TRichYPath(table)), NYT::NYson::EYsonFormat::Text);
+ return NYT::TRichYPath(table);
+ }
+
+ TFuture<TRunResult> Prepare(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) const final {
+ YQL_LOG_CTX_SCOPE(TStringBuf("Gateway"), __FUNCTION__);
+ auto nodePos = ctx.GetPosition(node->Pos());
+ try {
+ YQL_CLOG(INFO, ProviderYt) << node->Content();
+ const auto session = GetSession(options.SessionId());
+ session->EnsureInitializedSemaphore(options.Config());
+
+ TYtOutputOpBase opBase(node);
+
+ const auto cluster = TString{opBase.DataSink().Cluster().Value()};
+ const auto execCtx = MakeExecCtx(std::move(options), session, cluster, opBase.Raw(), &ctx);
+ execCtx->SetOutput(opBase.Output());
+
+ auto future = DoPrepare(opBase, execCtx);
+
+ return future.Apply([execCtx, pos = nodePos](const TFuture<bool>& f) {
+ try {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ if (f.GetValue()) {
+ return MakeRunResult(execCtx->OutTables_, execCtx->GetEntry());
+ } else {
+ TRunResult res;
+ res.SetSuccess();
+ std::transform(execCtx->OutTables_.cbegin(), execCtx->OutTables_.cend(), std::back_inserter(res.OutTableStats),
+ [](const TOutputInfo& info) -> std::pair<TString, TYtTableStatInfo::TPtr> { return { info.Name, nullptr }; });
+ return res;
+ }
+ } catch (...) {
+ return ResultFromCurrentException<TRunResult>(pos);
+ }
+ });
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TRunResult>(nodePos));
+ }
+ }
+
+ NThreading::TFuture<TRunResult> GetTableStat(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) final {
+ if (TSession::TPtr session = GetSession(options.SessionId(), false)) {
+ const TYtOutputOpBase opBase(node);
+ if (const auto cluster = TString{opBase.DataSink().Cluster().Value()}; auto ytServer = Clusters_->TryGetServer(cluster)) {
+ auto entry = session->TxCache_.GetEntry(ytServer);
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, opBase.Raw(), &ctx);
+ execCtx->SetOutput(opBase.Output());
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "TODO: Support multi out.");
+ const auto tableName = execCtx->OutTables_.front().Name;
+ const auto tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ const auto realTableName = NYT::AddPathPrefix(NYql::TransformPath(tmpFolder, execCtx->OutTables_.front().Name, true, session->UserName_), NYT::TConfig::Get()->Prefix);
+ auto batchGet = entry->Tx->CreateBatchRequest();
+ auto f = batchGet->Get(realTableName + "/@", TGetOptions()
+ .AttributeFilter(TAttributeFilter()
+ .AddAttribute(TString("id"))
+ .AddAttribute(TString("dynamic"))
+ .AddAttribute(TString("row_count"))
+ .AddAttribute(TString("chunk_row_count"))
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("data_weight"))
+ .AddAttribute(TString("chunk_count"))
+ .AddAttribute(TString("modification_time"))
+ .AddAttribute(TString("sorted_by"))
+ .AddAttribute(TString("revision"))
+ .AddAttribute(TString("content_revision"))
+ )
+ ).Apply([tableName, execCtx = std::move(execCtx)](const TFuture<NYT::TNode>& f) {
+ execCtx->StoreQueryCache();
+ auto attrs = f.GetValue();
+ auto statInfo = MakeIntrusive<TYtTableStatInfo>();
+ statInfo->Id = tableName;
+ statInfo->RecordsCount = GetTableRowCount(attrs);
+ statInfo->DataSize = GetDataWeight(attrs).GetOrElse(0);
+ statInfo->ChunkCount = attrs["chunk_count"].AsInt64();
+ TString strModifyTime = attrs["modification_time"].AsString();
+ statInfo->ModifyTime = TInstant::ParseIso8601(strModifyTime).Seconds();
+ statInfo->TableRevision = attrs["revision"].IntCast<ui64>();
+ statInfo->Revision = GetContentRevision(attrs);
+ TRunResult result;
+ result.OutTableStats.emplace_back(statInfo->Id, statInfo);
+ result.SetSuccess();
+ return result;
+ });
+
+ batchGet->ExecuteBatch();
+
+ return f;
+ }
+ }
+
+ return MakeFuture(TRunResult());
+ }
+
+ TFullResultTableResult PrepareFullResultTable(TFullResultTableOptions&& options) override {
+ try {
+ TString cluster = options.Cluster();
+ auto outTable = options.OutTable();
+ TSession::TPtr session = GetSession(options.SessionId(), true);
+
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, nullptr, nullptr);
+ execCtx->SetSingleOutput(outTable);
+
+ const auto& out = execCtx->OutTables_.front();
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ PrepareAttributes(attrs, out, execCtx, cluster, true);
+
+ TFullResultTableResult res;
+ if (auto entry = execCtx->TryGetEntry()) {
+ res.RootTransactionId = GetGuidAsString(entry->Tx->GetId());
+ if (entry->CacheTxId) {
+ res.ExternalTransactionId = GetGuidAsString(entry->CacheTxId);
+ }
+ }
+
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(cluster).GetOrElse(NTCF_LEGACY);
+ res.Server = execCtx->YtServer_;
+ res.Path = NYT::AddPathPrefix(out.Path, NYT::TConfig::Get()->Prefix);
+ res.RefName = out.Path;
+ res.CodecSpec = execCtx->GetOutSpec(false, nativeTypeCompat);
+ res.TableAttrs = NYT::NodeToYsonString(attrs);
+
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TFullResultTableResult>();
+ }
+ }
+
+ void SetStatUploader(IStatUploader::TPtr statUploader) final {
+ YQL_ENSURE(!StatUploader_, "StatUploader already set");
+ StatUploader_ = statUploader;
+ }
+
+ void RegisterMkqlCompiler(NCommon::TMkqlCallableCompilerBase& compiler) override {
+ Y_UNUSED(compiler);
+ }
+
+ TGetTablePartitionsResult GetTablePartitions(TGetTablePartitionsOptions&& options) override {
+ try {
+ TSession::TPtr session = GetSession(options.SessionId());
+ const TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+
+ auto execCtx = MakeExecCtx(std::move(options), session, options.Cluster(), nullptr, nullptr);
+ auto entry = execCtx->GetOrCreateEntry();
+
+ TVector<NYT::TRichYPath> paths;
+ for (const auto& pathInfo: execCtx->Options_.Paths()) {
+ const auto tablePath = TransformPath(tmpFolder, pathInfo->Table->Name, pathInfo->Table->IsTemp, session->UserName_);
+ NYT::TRichYPath richYtPath{NYT::AddPathPrefix(tablePath, NYT::TConfig::Get()->Prefix)};
+ if (auto p = entry->Snapshots.FindPtr(std::make_pair(pathInfo->Table->Name, pathInfo->Table->Epoch.GetOrElse(0)))) {
+ richYtPath.Path(std::get<0>(*p)).TransactionId(std::get<1>(*p)).OriginalPath(NYT::AddPathPrefix(pathInfo->Table->Name, NYT::TConfig::Get()->Prefix));
+ }
+ pathInfo->FillRichYPath(richYtPath); // n.b. throws exception, if there is no RowSpec (we assume it is always there)
+ paths.push_back(std::move(richYtPath));
+ }
+
+ auto apiOptions = NYT::TGetTablePartitionsOptions()
+ .PartitionMode(NYT::ETablePartitionMode::Unordered)
+ .DataWeightPerPartition(execCtx->Options_.DataSizePerJob())
+ .MaxPartitionCount(execCtx->Options_.MaxPartitions())
+ .AdjustDataWeightPerPartition(execCtx->Options_.AdjustDataWeightPerPartition());
+ auto res = TGetTablePartitionsResult();
+ res.Partitions = entry->Tx->GetTablePartitions(paths, apiOptions);
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TGetTablePartitionsResult>();
+ }
+ }
+
+private:
+ class TNodeResultBuilder {
+ public:
+ void WriteValue(const NUdf::TUnboxedValue& value, TType* type) {
+ if (type->IsTuple()) {
+ auto tupleType = AS_TYPE(NMiniKQL::TTupleType, type);
+ for (ui32 i: xrange(tupleType->GetElementsCount())) {
+ Node_.Add(NCommon::ValueToNode(value.GetElement(i), tupleType->GetElementType(i)));
+ }
+ } else if (type->IsList()) {
+ auto itemType = AS_TYPE(NMiniKQL::TListType, type)->GetItemType();
+ const auto it = value.GetListIterator();
+ for (NUdf::TUnboxedValue item; it.Next(item);) {
+ Node_.Add(NCommon::ValueToNode(item, itemType));
+ }
+ } else {
+ Node_.Add(NCommon::ValueToNode(value, type));
+ }
+ }
+
+ bool WriteNext(const NYT::TNode& item) {
+ Node_.Add(item);
+ return true;
+ }
+
+ NYT::TNode Make() {
+ if (Node_.IsUndefined()) {
+ return NYT::TNode::CreateList();
+ }
+ return std::move(Node_);
+ }
+ private:
+ NYT::TNode Node_;
+ };
+
+ struct TNodeResultFactory {
+ using TResult = NYT::TNode;
+
+ bool UseResultYson() const {
+ return false;
+ }
+
+ THolder<TNodeResultBuilder> operator()() const {
+ return MakeHolder<TNodeResultBuilder>();
+ }
+ };
+
+ class TExprResultBuilder: public TExecuteResOrPull {
+ public:
+ TExprResultBuilder(TMaybe<ui64> rowLimit, TMaybe<ui64> byteLimit, const TVector<TString>& columns)
+ : TExecuteResOrPull(rowLimit, byteLimit, MakeMaybe(columns))
+ {
+ }
+
+ bool WriteNext(const NYT::TNode& item) {
+ return TExecuteResOrPull::WriteNext(NYT::NodeToYsonString(item, NYT::NYson::EYsonFormat::Binary));
+ }
+
+ std::pair<TString, bool> Make() {
+ return {Finish(), IsTruncated()};
+ }
+ };
+
+ class TExprResultFactory {
+ public:
+ using TResult = std::pair<TString, bool>;
+
+ TExprResultFactory(TMaybe<ui64> rowLimit, TMaybe<ui64> byteLimit, const TVector<TString>& columns, bool hasListResult)
+ : RowLimit_(rowLimit)
+ , ByteLimit_(byteLimit)
+ , Columns_(columns)
+ , HasListResult_(hasListResult)
+ {
+ }
+
+ bool UseResultYson() const {
+ return true;
+ }
+
+ THolder<TExprResultBuilder> operator()() const {
+ auto res = MakeHolder<TExprResultBuilder>(RowLimit_, ByteLimit_, Columns_);
+ if (HasListResult_) {
+ res->SetListResult();
+ }
+ return res;
+ }
+ private:
+ const TMaybe<ui64> RowLimit_;
+ const TMaybe<ui64> ByteLimit_;
+ const TVector<TString> Columns_;
+ const bool HasListResult_;
+ };
+
+ static TFinalizeResult ExecFinalize(const TSession::TPtr& session, bool abort) {
+ try {
+ TFinalizeResult res;
+ if (abort) {
+ YQL_CLOG(INFO, ProviderYt) << "Aborting all transactions for hidden query";
+ session->TxCache_.AbortAll();
+ } else {
+ session->TxCache_.Finalize();
+ }
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TFinalizeResult>();
+ }
+ }
+
+ static TCanonizePathsResult ExecCanonizePaths(const TVector<TCanonizeReq>& paths,
+ const THashMap<TString, TMetaPerServerRequest<TCanonizePathsOptions>>& reqPerServer)
+ {
+ try {
+ TCanonizePathsResult res;
+ res.SetSuccess();
+ res.Data.resize(paths.size());
+
+ for (auto& grp: reqPerServer) {
+ auto entry = grp.second.ExecContext->GetOrCreateEntry();
+ auto batch = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes(Reserve(grp.second.TableIndicies.size()));
+
+ for (auto idx: grp.second.TableIndicies) {
+ const TCanonizeReq& canonReq = paths[idx];
+ batchRes.push_back(batch->CanonizeYPath(canonReq.Path()).Apply([idx, &res] (const TFuture<TRichYPath>& f) {
+ auto& normalizedPath = f.GetValue();
+ TString path = normalizedPath.Path_;
+ if (path.StartsWith(TConfig::Get()->Prefix)) {
+ path = path.substr(TConfig::Get()->Prefix.size());
+ }
+ res.Data[idx].Path = path;
+ if (normalizedPath.Columns_) {
+ res.Data[idx].Columns.ConstructInPlace(normalizedPath.Columns_->Parts_);
+ }
+ res.Data[idx].Ranges = normalizedPath.GetRanges();
+ }));
+
+ }
+ batch->ExecuteBatch();
+ for (size_t i: xrange(batchRes.size())) {
+ try {
+ batchRes[i].GetValue();
+ }
+ catch (...) {
+ FillResultFromCurrentException(res, paths.at(grp.second.TableIndicies.at(i)).Pos());
+ }
+ }
+ }
+
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TCanonizePathsResult>();
+ }
+ }
+
+ static TVector<std::pair<size_t, TString>> BatchLockTables(const NYT::ITransactionPtr& tx, const TVector<TTableReq>& tables,
+ const TVector<size_t>& tablesToLock, TMaybe<ELockMode> lockMode = {})
+ {
+ auto batchLock = tx->CreateBatchRequest();
+ TVector<TFuture<std::pair<size_t, TString>>> batchLockRes;
+ batchLockRes.reserve(tablesToLock.size());
+
+ for (auto idx: tablesToLock) {
+ const TTableReq& tableReq = tables[idx];
+
+ auto tablePath = tableReq.Table();
+ ELockMode mode = lockMode.GetOrElse(HasExclusiveModifyIntents(tableReq.Intents()) ? LM_EXCLUSIVE : LM_SHARED);
+
+ batchLockRes.push_back(batchLock->Lock(tablePath, mode).Apply([idx](const TFuture<ILockPtr>& res) {
+ try {
+ auto lock = res.GetValue();
+ TString lockId = TStringBuilder() << '#' << GetGuidAsString(lock->GetId());
+ return std::make_pair(idx, lockId);
+ } catch (const TErrorResponse& e) {
+ if (!e.IsResolveError()) {
+ throw;
+ }
+ return std::make_pair(idx, TString());
+ }
+ }));
+ }
+
+ batchLock->ExecuteBatch();
+
+ auto batchGet = tx->CreateBatchRequest();
+ TVector<TFuture<std::pair<size_t, TString>>> batchGetRes;
+ batchGetRes.reserve(tablesToLock.size());
+
+ for (auto& f: batchLockRes) {
+ auto lockRes = f.GetValue();
+ size_t idx = lockRes.first;
+ TString lockId = lockRes.second;
+ if (lockId) {
+ batchGetRes.push_back(batchGet->Get(lockId + "/@node_id").Apply([idx](const TFuture<NYT::TNode>& res) {
+ TString id = TStringBuilder() << '#' << res.GetValue().AsString();
+ return std::make_pair(idx, id);
+ }));
+ } else {
+ batchGetRes.push_back(MakeFuture(lockRes));
+ }
+ }
+ batchGet->ExecuteBatch();
+
+ TVector<std::pair<size_t, TString>> res;
+ res.reserve(tablesToLock.size());
+
+ std::transform(batchGetRes.begin(), batchGetRes.end(), std::back_inserter(res),
+ [] (const TFuture<std::pair<size_t, TString>>& f) { return f.GetValue(); });
+
+ return res;
+ }
+
+ // Returns tables, which require additional snapshot lock
+ static TVector<size_t> ProcessTablesToXLock(
+ const TExecContext<TGetTableInfoOptions>::TPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ const NYT::ITransactionPtr& lockTx,
+ const TVector<TTableReq>& tables,
+ const TVector<size_t>& tablesToXLock,
+ ui32 epoch,
+ TTableInfoResult& res)
+ {
+ NSorted::TSimpleMap<size_t, TString> existingIdxs;
+
+ auto lockIds = BatchLockTables(lockTx, tables, tablesToXLock);
+
+ if (0 == epoch) {
+ auto batchGet = lockTx->CreateBatchRequest();
+ TVector<TFuture<void>> batchGetRes;
+ with_lock(entry->Lock_) {
+ for (auto& lockRes: lockIds) {
+ const TTableReq& tableReq = tables[lockRes.first];
+ auto tablePath = tableReq.Table();
+ if (auto p = entry->Snapshots.FindPtr(std::make_pair(tablePath, epoch))) {
+ const ui64 revision = std::get<2>(*p);
+ if (lockRes.second) {
+ batchGetRes.push_back(batchGet->Get(lockRes.second + "/@revision").Apply([revision, tablePath](const TFuture<NYT::TNode>& f) {
+ const NYT::TNode& attr = f.GetValue();
+ if (attr.IntCast<ui64>() != revision) {
+ YQL_LOG_CTX_THROW TErrorException(TIssuesIds::YT_CONCURRENT_TABLE_MODIF)
+ << "Table " << tablePath.Quote()
+ << " was modified before taking exclusive lock for it."
+ << " Aborting query to prevent data lost";
+ }
+ }));
+ } else {
+ YQL_LOG_CTX_THROW TErrorException(TIssuesIds::YT_CONCURRENT_TABLE_MODIF)
+ << "Table " << tablePath.Quote()
+ << " was dropped before taking exclusive lock for it."
+ << " Aborting query to prevent data lost";
+
+ }
+ }
+ }
+ }
+ if (batchGetRes) {
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchGetRes).GetValue();
+ }
+ }
+
+ auto batchGetSort = lockTx->CreateBatchRequest();
+ TVector<TFuture<std::pair<size_t, bool>>> batchGetSortRes;
+ TVector<TString> ensureParents;
+ TVector<TString> ensureParentsTmp;
+ auto batchLock = lockTx->CreateBatchRequest();
+ TVector<TFuture<void>> batchLockRes;
+
+ for (auto& lockRes: lockIds) {
+ size_t idx = lockRes.first;
+ TString id = lockRes.second;
+ const TTableReq& tableReq = tables[idx];
+ auto tablePath = tableReq.Table();
+ TYtTableMetaInfo::TPtr metaRes;
+ if (!tableReq.LockOnly()) {
+ metaRes = res.Data[idx].Meta = MakeIntrusive<TYtTableMetaInfo>();
+ }
+ const bool loadMeta = !tableReq.LockOnly();
+ const bool exclusive = HasExclusiveModifyIntents(tableReq.Intents());
+ if (id) {
+ if (metaRes) {
+ metaRes->DoesExist = true;
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Lock " << tablePath.Quote() << " with "
+ << (exclusive ? LM_EXCLUSIVE : LM_SHARED)
+ << " mode (" << id << ')';
+
+ if (loadMeta) {
+ existingIdxs.emplace_back(idx, id);
+ }
+ if (!exclusive) {
+ batchGetSortRes.push_back(batchGetSort->Get(id + "/@", TGetOptions().AttributeFilter(TAttributeFilter().AddAttribute("sorted_by")))
+ .Apply([idx](const TFuture<NYT::TNode>& f) {
+ const NYT::TNode& attrs = f.GetValue();
+ return std::make_pair(idx, attrs.HasKey("sorted_by") && !attrs["sorted_by"].AsList().empty());
+ })
+ );
+ }
+ } else {
+ if (metaRes) {
+ metaRes->DoesExist = false;
+ }
+ tablePath = NYT::AddPathPrefix(tablePath, NYT::TConfig::Get()->Prefix);
+ TString folder;
+ TString tableName = tablePath;
+ auto slash = tableName.rfind('/');
+ if (TString::npos != slash) {
+ folder = tableName.substr(0, slash);
+ tableName = tableName.substr(slash + 1);
+ if (folder == "/") {
+ folder = "#" + lockTx->Get("//@id").AsString();
+ } else {
+ (tableReq.Anonymous() ? ensureParentsTmp : ensureParents).push_back(tablePath);
+ }
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Lock " << tableName.Quote() << " child of "
+ << folder.Quote() << " with " << LM_SHARED << " mode";
+ batchLockRes.push_back(batchLock->Lock(folder, LM_SHARED,
+ TLockOptions().ChildKey(tableName)). Apply([] (const TFuture<ILockPtr>& f) { f.GetValue(); }));
+ }
+ }
+
+ TVector<size_t> tablesToUpgradeLock;
+ if (batchGetSortRes) {
+ batchGetSort->ExecuteBatch();
+ for (auto& f: batchGetSortRes) {
+ auto& sortRes = f.GetValue();
+ if (sortRes.second) {
+ tablesToUpgradeLock.push_back(sortRes.first);
+ }
+ }
+ if (tablesToUpgradeLock) {
+ auto upgradeLockRes = BatchLockTables(lockTx, tables, tablesToUpgradeLock, LM_EXCLUSIVE);
+ for (auto& upgradedRes: upgradeLockRes) {
+ size_t idx = upgradedRes.first;
+ TString id = upgradedRes.second;
+ const TTableReq& tableReq = tables[idx];
+ YQL_CLOG(INFO, ProviderYt) << "Upgrade " << tableReq.Table().Quote() << " lock to " << LM_EXCLUSIVE << " mode (" << id << ')';
+ if (!tableReq.LockOnly()) {
+ existingIdxs[idx] = id; // Override existing record
+ }
+ }
+ }
+ }
+
+ if (ensureParentsTmp) {
+ CreateParents(ensureParentsTmp, entry->CacheTx);
+ }
+ if (ensureParents) {
+ CreateParents(ensureParents, entry->GetRoot());
+ }
+
+ if (batchLockRes) {
+ batchLock->ExecuteBatch();
+ WaitExceptionOrAll(batchLockRes).GetValue();
+ }
+
+ if (existingIdxs) {
+ FillMetadataResult(execCtx, entry, lockTx, existingIdxs, tables, res);
+ }
+
+ return tablesToUpgradeLock;
+ }
+
+ static TTableInfoResult ExecGetTableInfo(const TVector<TTableReq>& tables,
+ const THashMap<TString, TMetaPerServerRequest<TGetTableInfoOptions>>& reqPerServer, bool readOnly, ui32 epoch)
+ {
+ try {
+ TTableInfoResult res;
+ res.Data.resize(tables.size());
+
+ for (auto& grp: reqPerServer) {
+ auto entry = grp.second.ExecContext->GetOrCreateEntry();
+
+ NSorted::TSimpleMap<size_t, TString> existingIdxs;
+
+ TVector<size_t> checkpointsToXLock;
+ TVector<size_t> tablesToXLock;
+ for (auto idx: grp.second.TableIndicies) {
+ const TTableReq& tableReq = tables[idx];
+ if (HasModifyIntents(tableReq.Intents())) {
+ if (tableReq.Intents().HasFlags(TYtTableIntent::Flush)) {
+ checkpointsToXLock.push_back(idx);
+ } else {
+ tablesToXLock.push_back(idx);
+ }
+ res.Data[idx].WriteLock = true;
+ }
+ }
+
+ TVector<size_t> tablesToSLock;
+ bool makeUniqSLock = false;
+ if (!readOnly) {
+ if (tablesToXLock || checkpointsToXLock) {
+ entry->CreateDefaultTmpFolder();
+ }
+ if (tablesToXLock) {
+ tablesToSLock = ProcessTablesToXLock(grp.second.ExecContext, entry, entry->Tx, tables, tablesToXLock, epoch, res);
+ makeUniqSLock = !tablesToSLock.empty();
+ }
+
+ // each checkpoint has unique transaction
+ for (auto idx: checkpointsToXLock) {
+ auto lockTx = entry->GetOrCreateCheckpointTx(tables[idx].Table());
+ ProcessTablesToXLock(grp.second.ExecContext, entry, lockTx, tables, {idx}, epoch, res);
+ }
+ }
+
+ for (auto idx: grp.second.TableIndicies) {
+ const TTableReq& tableReq = tables[idx];
+ if (!tableReq.LockOnly() && (readOnly || HasReadIntents(tableReq.Intents()))) {
+ auto metaRes = res.Data[idx].Meta;
+ if (!metaRes || metaRes->DoesExist) {
+ tablesToSLock.push_back(idx);
+ }
+ }
+ }
+
+ if (tablesToSLock) {
+ if (makeUniqSLock) {
+ std::sort(tablesToSLock.begin(), tablesToSLock.end());
+ tablesToSLock.erase(std::unique(tablesToSLock.begin(), tablesToSLock.end()), tablesToSLock.end());
+ }
+
+ auto snapshotTx = entry->GetSnapshotTx(epoch != 0);
+ auto snapshotTxId = snapshotTx->GetId();
+ auto snapshotTxIdStr = GetGuidAsString(snapshotTxId);
+
+ auto lockIds = BatchLockTables(snapshotTx, tables, tablesToSLock, LM_SNAPSHOT);
+
+ TVector<std::tuple<TString, TString, size_t>> locks;
+
+ for (auto& lockRes: lockIds) {
+ size_t idx = lockRes.first;
+ TString id = lockRes.second;
+
+ const TTableReq& tableReq = tables[idx];
+
+ bool loadMeta = false;
+ auto metaRes = res.Data[idx].Meta;
+ if (!metaRes) {
+ metaRes = res.Data[idx].Meta = MakeIntrusive<TYtTableMetaInfo>();
+ loadMeta = true;
+ }
+
+ auto tablePath = tableReq.Table();
+ if (id) {
+ if (loadMeta) {
+ metaRes->DoesExist = true;
+ existingIdxs.emplace_back(idx, id);
+ }
+ locks.emplace_back(tablePath, id, idx);
+ YQL_CLOG(INFO, ProviderYt) << "Snapshot " << tablePath.Quote() << " -> " << id << ", tx=" << snapshotTxIdStr;
+ } else {
+ YQL_ENSURE(loadMeta);
+ metaRes->DoesExist = false;
+ }
+ }
+
+ if (existingIdxs) {
+ FillMetadataResult(grp.second.ExecContext, entry, snapshotTx, existingIdxs, tables, res);
+ }
+
+ if (locks) {
+ with_lock(entry->Lock_) {
+ for (auto& l: locks) {
+ entry->Snapshots[std::make_pair(std::get<0>(l), epoch)] = std::make_tuple(std::get<1>(l), snapshotTxId, res.Data[std::get<2>(l)].Stat->TableRevision);
+ }
+ }
+ }
+ }
+ }
+
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TTableInfoResult>();
+ }
+ }
+
+ static TFuture<TTableRangeResult> ExecGetTableRange(const TExecContext<TTableRangeOptions>::TPtr& execCtx,
+ const TString& tmpTablePath = {}, TString filterLambda = {}, const TExpressionResorceUsage& extraUsage = {})
+ {
+ auto pos = execCtx->Options_.Pos();
+ try {
+ auto entry = execCtx->GetOrCreateEntry();
+ auto deterministicMode = execCtx->Session_->DeterministicMode_;
+
+ TString prefix = execCtx->Options_.Prefix();
+ TString suffix = execCtx->Options_.Suffix();
+
+ auto cacheKey = std::make_tuple(prefix, suffix, filterLambda);
+ with_lock(entry->Lock_) {
+ if (auto p = entry->RangeCache.FindPtr(cacheKey)) {
+ YQL_CLOG(INFO, ProviderYt) << "Found range in cache for key ('" << prefix << "','" << suffix << "',<filter with size " << filterLambda.Size() << ">) - number of items " << p->size();
+ return MakeFuture(MakeTableRangeResult(*p, deterministicMode));
+ }
+ }
+
+ if (!prefix.empty() && !entry->Tx->Exists(prefix)) {
+ YQL_CLOG(INFO, ProviderYt) << "Storing empty range to cache with key ('" << std::get<0>(cacheKey) << "','" << std::get<1>(cacheKey) << "',<filter with size " << std::get<2>(cacheKey).size() << ">)";
+ with_lock(entry->Lock_) {
+ entry->RangeCache.emplace(std::move(cacheKey), std::vector<NYT::TRichYPath>{});
+ }
+
+ TTableRangeResult rangeRes;
+ rangeRes.SetSuccess();
+ return MakeFuture(rangeRes);
+ }
+
+ std::vector<TString> names;
+ std::vector<std::exception_ptr> errors;
+
+ bool foundInPartialCache = false;
+ with_lock(entry->Lock_) {
+ if (auto p = entry->PartialRangeCache.FindPtr(prefix)) {
+ std::tie(names, errors) = *p;
+ foundInPartialCache = true;
+ }
+ }
+
+ if (!foundInPartialCache) {
+ auto typeAttrFilter = TAttributeFilter().AddAttribute("type").AddAttribute("_yql_type").AddAttribute("broken");
+ auto nodeList = entry->Tx->List(prefix,
+ TListOptions().AttributeFilter(typeAttrFilter));
+
+ TVector<std::variant<TString, std::exception_ptr>> types(Reserve(nodeList.size()));
+ {
+ auto batchGet = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes;
+ for (size_t i: xrange(nodeList.size())) {
+ auto& node = nodeList[i];
+ auto type = GetAttrType(node);
+ if (type == "link") {
+ types.emplace_back(type);
+ if (!node.GetAttributes().HasKey("broken") || !node.GetAttributes()["broken"].AsBool()) {
+ batchRes.push_back(batchGet->Get(prefix + "/" + node.AsString(), TGetOptions().AttributeFilter(typeAttrFilter))
+ .Apply([i, &types] (const TFuture<NYT::TNode>& f) {
+ try {
+ types[i] = GetAttrType(f.GetValue());
+ } catch (...) {
+ types[i] = std::current_exception();
+ }
+ }));
+ }
+ } else {
+ types.push_back(type);
+ }
+ }
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchRes).GetValue();
+ }
+
+ names.reserve(types.size());
+ errors.reserve(types.size());
+ for (size_t i: xrange(nodeList.size())) {
+ auto& node = nodeList[i];
+ if (auto type = std::get_if<TString>(&types[i])) {
+ if (TStringBuf("map_node") == *type && !suffix.empty()) {
+ names.push_back(node.AsString());
+ errors.emplace_back();
+ } else if (TStringBuf("table") == *type && suffix.empty()) {
+ names.push_back(node.AsString());
+ errors.emplace_back();
+ } else if (TStringBuf("document") == *type && suffix.empty()) {
+ if (node.HasAttributes()) {
+ auto& attrs = node.GetAttributes();
+ if (attrs.HasKey("_yql_type") && attrs["_yql_type"].AsString() == "view") {
+ names.push_back(node.AsString());
+ errors.emplace_back();
+ }
+ }
+ }
+ } else {
+ auto exptr = std::get<std::exception_ptr>(types[i]);
+ if (filterLambda) {
+ // Delayed error processing
+ names.push_back(node.AsString());
+ errors.push_back(std::move(exptr));
+ } else {
+ std::rethrow_exception(exptr);
+ }
+ }
+ }
+ YQL_ENSURE(names.size() == errors.size());
+ YQL_CLOG(INFO, ProviderYt) << "Got " << names.size() << " items in folder '" << prefix << "'. Storing to partial cache";
+ with_lock(entry->Lock_) {
+ entry->PartialRangeCache.emplace(prefix, std::make_pair(names, errors));
+ }
+ } else {
+ YQL_CLOG(INFO, ProviderYt) << "Found range in partial cache for '" << prefix << "' - number of items " << names.size();
+ }
+
+ if (filterLambda && !names.empty()) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Executing range filter";
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+
+ TRuntimeNode root = DeserializeRuntimeNode(filterLambda, builder.GetTypeEnvironment());
+
+ root = builder.TransformAndOptimizeProgram(root, [&](TInternName name)->TCallableVisitFunc {
+ if (name == MrRangeInputListInternal) {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env)->TRuntimeNode {
+ Y_UNUSED(callable);
+ Y_UNUSED(env);
+ TVector<TRuntimeNode> inputs;
+ for (size_t i = 0; i < names.size(); ++i) {
+ inputs.push_back(pgmBuilder.NewTuple({
+ pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(names[i]),
+ pgmBuilder.NewDataLiteral(ui64(i))
+ }));
+ }
+ auto inputNode = pgmBuilder.AsList(inputs);
+ return inputNode;
+ };
+ }
+ return TCallableVisitFunc();
+ });
+ filterLambda = SerializeRuntimeNode(root, builder.GetTypeEnvironment());
+ }
+
+ auto logCtx = execCtx->LogCtx_;
+ return ExecCalc(filterLambda, extraUsage, tmpTablePath, execCtx, entry, TNodeResultFactory())
+ .Apply([logCtx, prefix, suffix, entry, deterministicMode, pos, errors = std::move(errors), cacheKey = std::move(cacheKey)](const TFuture<NYT::TNode>& f) mutable {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ std::vector<TString> names;
+ try {
+ const NYT::TNode& node = f.GetValue();
+ YQL_ENSURE(node.IsList());
+ for (auto& n: node.AsList()) {
+ YQL_ENSURE(n.IsList());
+ if (auto err = errors.at(n.AsList().at(1).AsUint64())) {
+ std::rethrow_exception(err);
+ }
+ names.push_back(n.AsList().at(0).AsString());
+ }
+ return MakeTableRangeResult(std::move(names), std::move(cacheKey), prefix, suffix, entry, deterministicMode);
+ } catch (...) {
+ return ResultFromCurrentException<TTableRangeResult>(pos);
+ }
+ });
+ }
+ return MakeFuture(MakeTableRangeResult(std::move(names), std::move(cacheKey), prefix, suffix, entry, deterministicMode));
+
+ } catch (...) {
+ return MakeFuture(ResultFromCurrentException<TTableRangeResult>(pos));
+ }
+ }
+
+ static TTableRangeResult MakeTableRangeResult(const std::vector<NYT::TRichYPath>& paths, bool deterministicMode) {
+ TTableRangeResult rangeRes;
+ rangeRes.SetSuccess();
+
+ for (auto& normalizedPath: paths) {
+ TCanonizedPath canonPath;
+ canonPath.Path = normalizedPath.Path_;
+ if (normalizedPath.Columns_) {
+ canonPath.Columns.ConstructInPlace(normalizedPath.Columns_->Parts_);
+ }
+ canonPath.Ranges = normalizedPath.GetRanges();
+ rangeRes.Tables.push_back(std::move(canonPath));
+ }
+ if (deterministicMode) {
+ SortBy(rangeRes.Tables, [] (const TCanonizedPath& path) { return path.Path; });
+ }
+ return rangeRes;
+ }
+
+ static TTableRangeResult MakeTableRangeResult(std::vector<TString>&& names, std::tuple<TString, TString, TString>&& cacheKey,
+ TString prefix, TString suffix, const TTransactionCache::TEntry::TPtr& entry, bool deterministicMode)
+ {
+ TTableRangeResult rangeRes;
+ rangeRes.SetSuccess();
+ std::vector<NYT::TRichYPath> cached;
+ if (prefix) {
+ prefix.append('/');
+ }
+ if (suffix) {
+ if (!names.empty()) {
+ auto batchCanonize = entry->Tx->CreateBatchRequest();
+ auto batchExists = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchCanonizeRes;
+ TVector<TFuture<void>> batchExistsRes;
+ for (TString& name: names) {
+ name.prepend(prefix).append('/').append(suffix);
+ batchCanonizeRes.push_back(batchCanonize->CanonizeYPath(name)
+ .Apply([&batchExists, &batchExistsRes, &rangeRes, &cached] (const TFuture<TRichYPath>& f) {
+ TCanonizedPath canonPath;
+ auto normalizedPath = f.GetValue();
+ if (normalizedPath.Path_.StartsWith(TConfig::Get()->Prefix)) {
+ normalizedPath.Path_ = normalizedPath.Path_.substr(TConfig::Get()->Prefix.size());
+ }
+ canonPath.Path = normalizedPath.Path_;
+ if (normalizedPath.Columns_) {
+ canonPath.Columns.ConstructInPlace(normalizedPath.Columns_->Parts_);
+ }
+ canonPath.Ranges = normalizedPath.GetRanges();
+ batchExistsRes.push_back(batchExists->Exists(canonPath.Path)
+ .Apply([canonPath = std::move(canonPath), normalizedPath = std::move(normalizedPath), &rangeRes, &cached] (const NThreading::TFuture<bool>& f) {
+ if (f.GetValue()) {
+ rangeRes.Tables.push_back(std::move(canonPath));
+ cached.push_back(std::move(normalizedPath));
+ }
+ }));
+ }));
+ }
+ batchCanonize->ExecuteBatch();
+ WaitExceptionOrAll(batchCanonizeRes).GetValue();
+
+ batchExists->ExecuteBatch();
+ WaitExceptionOrAll(batchExistsRes).GetValue();
+ }
+ }
+ else {
+ if (prefix.StartsWith(TConfig::Get()->Prefix)) {
+ prefix = prefix.substr(TConfig::Get()->Prefix.size());
+ }
+ for (auto& name: names) {
+ auto fullName = prefix + name;
+ rangeRes.Tables.push_back(TCanonizedPath{fullName, Nothing(), {}});
+ cached.push_back(NYT::TRichYPath(fullName));
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Storing " << cached.size() << " items to range cache with key ('" << std::get<0>(cacheKey) << "','" << std::get<1>(cacheKey) << "',<filter with size " << std::get<2>(cacheKey).size() << ">)";
+ with_lock(entry->Lock_) {
+ entry->RangeCache.emplace(std::move(cacheKey), std::move(cached));
+ }
+
+ if (deterministicMode) {
+ SortBy(rangeRes.Tables, [] (const TCanonizedPath& path) { return path.Path; });
+ }
+ return rangeRes;
+ }
+
+ static TFolderResult ExecGetFolder(const TExecContext<TFolderOptions>::TPtr& execCtx)
+ {
+ try {
+ auto entry = execCtx->GetOrCreateEntry();
+
+ TFolderResult res;
+ res.SetSuccess();
+
+ TString prefix = execCtx->Options_.Prefix();
+ TString cacheKey = prefix;
+
+ TSet<TString> uniqueAttrs;
+ for (const auto& attr : execCtx->Options_.Attributes()) {
+ uniqueAttrs.insert(attr);
+ }
+ // Make key with sorted attrs
+ std::for_each(uniqueAttrs.cbegin(), uniqueAttrs.cend(), [&cacheKey](const auto& attr) { cacheKey.append('&').append(attr); } );
+
+ with_lock(entry->Lock_) {
+ if (auto p = entry->FolderCache.FindPtr(cacheKey)) {
+ if (auto file = std::get_if<TFileLinkPtr>(p)) {
+ res.File = *file;
+ YQL_CLOG(INFO, ProviderYt) << "Found folder in cache with key ('" << cacheKey << "') with tmp file " << res.File->GetPath().GetPath();
+ } else {
+ const auto& list = std::get<std::vector<std::tuple<TString, TString, TString>>>(*p);
+ YQL_CLOG(INFO, ProviderYt) << "Found folder in cache with key ('" << cacheKey << "') with " << list.size() << " items";
+ for (auto& el: list) {
+ TFolderResult::TFolderItem item;
+ std::tie(item.Type, item.Path, item.Attributes) = el;
+ res.Items.push_back(std::move(item));
+ }
+ }
+ return res;
+ }
+ }
+
+ if (!prefix.empty() && !entry->Tx->Exists(prefix)) {
+ YQL_CLOG(INFO, ProviderYt) << "Storing empty folder in cache with key ('" << cacheKey << "')";
+ with_lock(entry->Lock_) {
+ entry->FolderCache[cacheKey] = std::vector<std::tuple<TString, TString, TString>>{};
+ }
+ return res;
+ }
+
+
+ TSet<TString> uniqueAttrsWithSystemAttrs = uniqueAttrs;
+ uniqueAttrsWithSystemAttrs.insert("type");
+ uniqueAttrsWithSystemAttrs.insert("target_path");
+ uniqueAttrsWithSystemAttrs.insert("broken");
+
+ auto makeAttrFilter = [](const TSet<TString>& attrs) {
+ NYT::TAttributeFilter ret;
+ for (const auto& attr : attrs) {
+ ret.AddAttribute(attr);
+ }
+ return ret;
+ };
+
+ auto nodeList = entry->Tx->List(prefix, TListOptions().AttributeFilter(makeAttrFilter(uniqueAttrsWithSystemAttrs)));
+ res.Items.reserve(nodeList.size());
+
+ uniqueAttrsWithSystemAttrs.erase("target_path");
+ uniqueAttrsWithSystemAttrs.erase("broken");
+
+ if (prefix) {
+ prefix.append('/');
+ }
+
+ THolder<TOFStream> out;
+ ui32 count = 0;
+ ui64 size = 0;
+ const ui32 countLimit = execCtx->Options_.Config()->FolderInlineItemsLimit.Get().GetOrElse(100);
+ const ui64 sizeLimit = execCtx->Options_.Config()->FolderInlineDataLimit.Get().GetOrElse(100_KB);
+ TString file;
+ Y_DEFER {
+ if (file) {
+ NFs::Remove(file);
+ }
+ };
+
+ auto writeItem = [&uniqueAttrs, &out, &res, &count, &file,
+ countLimit, &size, sizeLimit, execCtx](const NYT::TNode& node, TString path) {
+ auto type = GetAttrType(node);
+ if (path.StartsWith(NYT::TConfig::Get()->Prefix)) {
+ path = path.substr(NYT::TConfig::Get()->Prefix.size());
+ }
+ NYT::TNode retAttrs = NYT::TNode::CreateMap();
+ auto& nodeAttrs = node.GetAttributes();
+ for (const auto& attrName : uniqueAttrs) {
+ if (attrName && nodeAttrs.HasKey(attrName)) {
+ retAttrs[attrName] = nodeAttrs[attrName];
+ }
+ }
+ auto attrs = NYT::NodeToYsonString(retAttrs);
+
+ if (!out) {
+ ++count;
+ size += type.length() + path.length() + attrs.length();
+ if (count <= countLimit && size <= sizeLimit) {
+ TFolderResult::TFolderItem item;
+ item.Type = std::move(type);
+ item.Path = std::move(path);
+ item.Attributes = std::move(attrs);
+ res.Items.push_back(std::move(item));
+ return;
+ }
+ file = execCtx->FileStorage_->GetTemp() / GetGuidAsString(execCtx->Session_->RandomProvider_->GenGuid());
+ out = MakeHolder<TOFStream>(file);
+ for (auto& item: res.Items) {
+ ::SaveMany(out.Get(), item.Type, item.Path, item.Attributes);
+ }
+ res.Items.clear();
+ YQL_CLOG(INFO, ProviderYt) << "Folder limit exceeded. Writing items to file " << file;
+ }
+
+ ::SaveMany(out.Get(), type, path, attrs);
+ };
+
+ auto batchGet = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes;
+
+ for (const auto& node : nodeList) {
+ auto path = prefix + node.AsString();
+ auto type = GetAttrType(node);
+ if (type != "link") {
+ writeItem(node, path);
+ }
+ else if (!node.GetAttributes()["broken"].AsBool()) {
+ auto targetPath = node.GetAttributes()["target_path"].AsString();
+ batchRes.push_back(
+ batchGet->Get(targetPath, TGetOptions().AttributeFilter(makeAttrFilter(uniqueAttrsWithSystemAttrs)))
+ .Apply([path, writeItem](const NThreading::TFuture<NYT::TNode>& f) {
+ try {
+ writeItem(f.GetValue(), path);
+ } catch (...) {
+ writeItem(NYT::TNode::CreateMap(), path);
+ }
+ })
+ );
+ }
+ }
+
+ if (!batchRes.empty()) {
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchRes).GetValue();
+ }
+ if (out) {
+ ::SaveSize(out.Get(), 0);
+ out.Destroy();
+ res.File = CreateFakeFileLink(file, TString(), true);
+ file = {};
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Folder items count=" << count << ", size=" << size;
+
+ if (res.File) {
+ YQL_CLOG(INFO, ProviderYt) << "Storing folder tmp file " << res.File->GetPath().GetPath() << " in cache with key ('" << cacheKey << "')";
+ with_lock(entry->Lock_) {
+ entry->FolderCache[cacheKey] = res.File;
+ }
+ } else {
+ YQL_CLOG(INFO, ProviderYt) << "Storing folder with " << res.Items.size() << " items in cache with key ('" << cacheKey << "')";
+ std::vector<std::tuple<TString, TString, TString>> cache;
+ for (const auto& item: res.Items) {
+ cache.emplace_back(item.Type, item.Path, item.Attributes);
+ }
+ with_lock(entry->Lock_) {
+ entry->FolderCache[cacheKey] = std::move(cache);
+ }
+ }
+
+ return res;
+
+ } catch (const NYT::TErrorResponse &e) {
+ if (e.GetError().ContainsErrorCode(NYT::NClusterErrorCodes::NRpc::NoSuchMethod)) {
+ TFolderResult res;
+ TString errMsg = execCtx->Options_.Prefix() + " is not a folder.";
+ TIssue rootIssue = YqlIssue(execCtx->Options_.Pos(), TIssuesIds::YT_FOLDER_INPUT_IS_NOT_A_FOLDER, errMsg);
+ res.SetStatus(TIssuesIds::YT_FOLDER_INPUT_IS_NOT_A_FOLDER);
+ res.AddIssue(rootIssue);
+ return res;
+ }
+ return ResultFromCurrentException<TFolderResult>(execCtx->Options_.Pos());
+ } catch (...) {
+ return ResultFromCurrentException<TFolderResult>(execCtx->Options_.Pos());
+ }
+ }
+
+ static TFuture<void> ExecPublish(
+ const TExecContext<TPublishOptions>::TPtr& execCtx,
+ const TVector<TString>& src,
+ const TString& dst,
+ const ui32 dstEpoch,
+ const bool isAnonymous,
+ EYtWriteMode mode,
+ const bool initial,
+ const bool monotonicKeys,
+ const bool combineChunks,
+ const std::unordered_map<EYtSettingType, TString>& strOpts)
+ {
+ TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ auto cluster = execCtx->Cluster_;
+ auto entry = execCtx->GetEntry();
+
+ TVector<TString> srcPaths;
+ for (auto& p: src) {
+ srcPaths.push_back(NYql::TransformPath(tmpFolder, p, true, execCtx->Session_->UserName_));
+ }
+
+ auto dstPath = NYql::TransformPath(tmpFolder, dst, isAnonymous, execCtx->Session_->UserName_);
+ if (execCtx->Hidden) {
+ const auto origDstPath = dstPath;
+ dstPath = NYql::TransformPath(
+ tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(CreateDefaultRandomProvider()->GenGuid()),
+ true,
+ execCtx->Session_->UserName_);
+ if (entry->Tx->Exists(origDstPath) && EYtWriteMode::Flush != mode) {
+ entry->Tx->Copy(
+ origDstPath,
+ dstPath,
+ TCopyOptions().Force(true));
+ entry->DeleteAtFinalizeInternal(dstPath);
+ }
+ if (EYtWriteMode::Flush == mode) {
+ mode = EYtWriteMode::Renew;
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Hidden query publish destination: " << dstPath;
+ }
+
+ auto publishTx = EYtWriteMode::Flush == mode ? entry->GetCheckpointTx(dstPath) : entry->Tx;
+
+ if (isAnonymous) {
+ entry->CreateDefaultTmpFolder();
+ }
+ CreateParents(TVector<TString>{dstPath}, entry->GetRoot());
+
+ const bool exists = entry->Tx->Exists(dstPath);
+ if ((EYtWriteMode::Append == mode || EYtWriteMode::RenewKeepMeta == mode) && !exists) {
+ mode = EYtWriteMode::Renew;
+ }
+ if (isAnonymous) {
+ entry->DeleteAtFinalize(dstPath);
+ }
+
+ TYqlRowSpecInfo::TPtr rowSpec = execCtx->Options_.DestinationRowSpec();
+
+ bool appendToSorted = false;
+ if (EYtWriteMode::Append == mode && !monotonicKeys) {
+ NYT::TNode attrs = entry->Tx->Get(dstPath + "/@", TGetOptions()
+ .AttributeFilter(TAttributeFilter()
+ .AddAttribute(TString("sorted_by"))
+ )
+ );
+ appendToSorted = attrs.HasKey("sorted_by") && !attrs["sorted_by"].AsList().empty();
+ }
+
+ NYT::TNode storageAttrs = NYT::TNode::CreateMap();
+ auto yqlAttrs = NYT::TNode::CreateMap();
+ if (appendToSorted || EYtWriteMode::RenewKeepMeta == mode) {
+ yqlAttrs = GetUserAttributes(entry->Tx, dstPath, false);
+ storageAttrs = entry->Tx->Get(dstPath + "/@", TGetOptions()
+ .AttributeFilter(TAttributeFilter()
+ .AddAttribute("compression_codec")
+ .AddAttribute("erasure_codec")
+ .AddAttribute("replication_factor")
+ .AddAttribute("media")
+ .AddAttribute("primary_medium")
+ )
+ );
+ }
+
+ NYT::MergeNodes(yqlAttrs, GetUserAttributes(entry->Tx, srcPaths.back(), true));
+ NYT::MergeNodes(yqlAttrs, YqlOpOptionsToAttrs(execCtx->Session_->OperationOptions_));
+ NYT::TNode& rowSpecNode = yqlAttrs[YqlRowSpecAttribute];
+ const auto nativeYtTypeCompatibility = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(cluster).GetOrElse(NTCF_LEGACY);
+ const bool rowSpecCompactForm = execCtx->Options_.Config()->UseYqlRowSpecCompactForm.Get().GetOrElse(DEFAULT_ROW_SPEC_COMPACT_FORM);
+ rowSpec->FillAttrNode(rowSpecNode, nativeYtTypeCompatibility, rowSpecCompactForm);
+
+ const auto multiSet = execCtx->Options_.Config()->_UseMultisetAttributes.Get().GetOrElse(DEFAULT_USE_MULTISET_ATTRS);
+
+ auto commitCheckpoint = [entry, dstPath, mode] (const TFuture<void>& f) {
+ f.GetValue();
+ if (EYtWriteMode::Flush == mode) {
+ entry->CommitCheckpointTx(dstPath);
+ }
+ };
+
+ if (EYtWriteMode::Renew == mode) {
+ const auto expirationIt = strOpts.find(EYtSettingType::Expiration);
+ bool isTimestamp = false, isDuration = false;
+ TInstant stamp;
+ TDuration duration;
+ if (expirationIt != strOpts.cend()) {
+ isDuration = TDuration::TryParse(expirationIt->second, duration);
+ if (!isDuration) {
+ isTimestamp = TInstant::TryParseIso8601(expirationIt->second, stamp);
+ }
+ }
+ const TMaybe<TInstant> deadline =
+ execCtx->Options_.Config()->ExpirationDeadline.Get(cluster);
+ const TMaybe<TDuration> interval =
+ execCtx->Options_.Config()->ExpirationInterval.Get(cluster);
+ if (deadline || isTimestamp) {
+ yqlAttrs["expiration_time"] = isTimestamp ? stamp.ToStringUpToSeconds()
+ : deadline->ToStringUpToSeconds();
+ } else if (interval || isDuration) {
+ yqlAttrs["expiration_time"] = isDuration ? (Now() + duration).ToStringUpToSeconds()
+ : (Now() + *interval).ToStringUpToSeconds();
+ }
+ if (execCtx->Options_.Config()->NightlyCompress.Get(cluster).GetOrElse(false)) {
+ yqlAttrs["force_nightly_compress"] = true;
+ }
+ }
+
+ const auto userAttrsIt = strOpts.find(EYtSettingType::UserAttrs);
+ if (userAttrsIt != strOpts.cend()) {
+ const NYT::TNode mapNode = NYT::NodeFromYsonString(userAttrsIt->second);
+ const auto& map = mapNode.AsMap();
+ for (auto it = map.cbegin(); it != map.cend(); ++it) {
+ yqlAttrs[it->first] = it->second;
+ }
+ }
+
+ bool forceMerge = combineChunks;
+ bool forceTransform = false;
+
+#define DEFINE_OPT(name, attr, transform) \
+ auto dst##name = isAnonymous \
+ ? execCtx->Options_.Config()->Temporary##name.Get(cluster) \
+ : execCtx->Options_.Config()->Published##name.Get(cluster); \
+ if (EYtWriteMode::RenewKeepMeta == mode && storageAttrs.HasKey(attr) \
+ && execCtx->Options_.Config()->Temporary##name.Get(cluster)) { \
+ dst##name = OptionFromNode<decltype(dst##name)::value_type>(storageAttrs[attr]); \
+ } \
+ if (const auto it = strOpts.find(EYtSettingType::name); it != strOpts.cend()) { \
+ dst##name = OptionFromString<decltype(dst##name)::value_type>(it->second); \
+ } \
+ if (dst##name && dst##name != execCtx->Options_.Config()->Temporary##name.Get(cluster)) { \
+ forceMerge = true; \
+ forceTransform = forceTransform || transform; \
+ YQL_CLOG(INFO, ProviderYt) << "Option " #name " forces merge"; \
+ }
+
+ DEFINE_OPT(CompressionCodec, "compression_codec", true);
+ DEFINE_OPT(ErasureCodec, "erasure_codec", true);
+ DEFINE_OPT(ReplicationFactor, "replication_factor", false);
+ DEFINE_OPT(Media, "media", true);
+ DEFINE_OPT(PrimaryMedium, "primary_medium", true);
+
+#undef DEFINE_OPT
+
+ TFuture<void> res;
+ if (EYtWriteMode::Flush == mode || EYtWriteMode::Append == mode || srcPaths.size() > 1 || forceMerge) {
+ TFuture<bool> cacheCheck = MakeFuture<bool>(false);
+ if (EYtWriteMode::Flush != mode && isAnonymous) {
+ execCtx->SetCacheItem({dstPath}, {NYT::TNode::CreateMap()}, tmpFolder);
+ cacheCheck = execCtx->LookupQueryCacheAsync();
+ }
+ res = cacheCheck.Apply([mode, srcPaths, execCtx, rowSpec, forceTransform,
+ appendToSorted, initial, entry, dstPath, dstEpoch, yqlAttrs, combineChunks,
+ dstCompressionCodec, dstErasureCodec, dstReplicationFactor, dstMedia, dstPrimaryMedium,
+ nativeYtTypeCompatibility, publishTx, cluster,
+ commitCheckpoint] (const auto& f) mutable
+ {
+ if (f.GetValue()) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ // Use explicit columns for source tables to cut aux columns
+ TVector<TString> columns;
+ for (auto item: rowSpec->GetType()->GetItems()) {
+ columns.emplace_back(item->GetName());
+ }
+ for (auto item: rowSpec->GetAuxColumns()) {
+ columns.emplace_back(item.first);
+ }
+
+ TMergeOperationSpec mergeSpec;
+ if (appendToSorted) {
+ if (initial) {
+ auto p = entry->Snapshots.FindPtr(std::make_pair(dstPath, dstEpoch));
+ YQL_ENSURE(p, "Table " << dstPath << " has no snapshot");
+ mergeSpec.AddInput(TRichYPath(std::get<0>(*p)).TransactionId(std::get<1>(*p)).OriginalPath(NYT::AddPathPrefix(dstPath, NYT::TConfig::Get()->Prefix)).Columns(columns));
+ } else {
+ mergeSpec.AddInput(TRichYPath(dstPath).Columns(columns));
+ }
+ }
+ for (auto& s: srcPaths) {
+ auto path = TRichYPath(s).Columns(columns);
+ if (EYtWriteMode::Flush == mode) {
+ path.TransactionId(entry->Tx->GetId());
+ }
+ mergeSpec.AddInput(path);
+ }
+
+ auto ytDst = TRichYPath(dstPath);
+ if (EYtWriteMode::Append == mode && !appendToSorted) {
+ ytDst.Append(true);
+ } else {
+ NYT::TNode fullSpecYson;
+ rowSpec->FillCodecNode(fullSpecYson);
+ const auto schema = RowSpecToYTSchema(fullSpecYson, nativeYtTypeCompatibility);
+ ytDst.Schema(schema);
+
+ if (EYtWriteMode::Append != mode && EYtWriteMode::RenewKeepMeta != mode) {
+ yqlAttrs["schema"] = schema.ToNode();
+ if (dstCompressionCodec) {
+ yqlAttrs["compression_codec"] = *dstCompressionCodec;
+ }
+ if (dstErasureCodec) {
+ yqlAttrs["erasure_codec"] = ToString(*dstErasureCodec);
+ }
+ if (dstReplicationFactor) {
+ yqlAttrs["replication_factor"] = static_cast<i64>(*dstReplicationFactor);
+ }
+ if (dstMedia) {
+ yqlAttrs["media"] = *dstMedia;
+ }
+ if (dstPrimaryMedium) {
+ yqlAttrs["primary_medium"] = *dstPrimaryMedium;
+ }
+ if (auto optimizeFor = execCtx->Options_.Config()->OptimizeFor.Get(cluster)) {
+ if (schema.Columns().size()) {
+ yqlAttrs["optimize_for"] = ToString(*optimizeFor);
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Creating " << dstPath << " with attrs: " << NYT::NodeToYsonString(yqlAttrs);
+ publishTx->Create(dstPath, NT_TABLE, TCreateOptions().Force(true).Attributes(yqlAttrs));
+
+ yqlAttrs.Clear();
+ }
+ }
+ mergeSpec.Output(ytDst);
+ mergeSpec.ForceTransform(forceTransform);
+
+ if (rowSpec->IsSorted()) {
+ mergeSpec.Mode(MM_SORTED);
+ mergeSpec.MergeBy(ToYTSortColumns(rowSpec->GetForeignSort()));
+ } else {
+ mergeSpec.Mode(MM_ORDERED);
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc();
+ EYtOpProps flags = EYtOpProp::PublishedAutoMerge;
+ if (combineChunks) {
+ flags |= EYtOpProp::PublishedChunkCombine;
+ }
+
+ FillSpec(spec, *execCtx, entry, 0., Nothing(), flags);
+
+ if (combineChunks) {
+ mergeSpec.CombineChunks(true);
+ }
+
+ return execCtx->RunOperation([publishTx, mergeSpec = std::move(mergeSpec), spec = std::move(spec)]() {
+ return publishTx->Merge(mergeSpec, TOperationOptions().StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).CreateOutputTables(false).Spec(spec));
+ })
+ .Apply([execCtx](const auto& f){
+ f.GetValue();
+ execCtx->StoreQueryCache();
+ });
+ });
+ }
+ else {
+ publishTx->Copy(srcPaths.front(), dstPath, TCopyOptions().Force(true));
+ res = MakeFuture();
+ }
+
+ std::function<void(const TFuture<void>&)> setAttrs = [logCtx = execCtx->LogCtx_, entry, publishTx, dstPath, mode, yqlAttrs, multiSet] (const TFuture<void>& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(logCtx);
+ f.GetValue();
+ if (yqlAttrs.IsUndefined()) {
+ return;
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Setting attrs for " << dstPath << ": " << NYT::NodeToYsonString(yqlAttrs);
+ if (multiSet) {
+ try {
+ publishTx->MultisetAttributes(dstPath + "/@", yqlAttrs.AsMap(), NYT::TMultisetAttributesOptions());
+ }
+ catch (const TErrorResponse& e) {
+ if (EYtWriteMode::Append != mode || !e.IsConcurrentTransactionLockConflict()) {
+ throw;
+ }
+ }
+ } else {
+ auto batch = publishTx->CreateBatchRequest();
+
+ TVector<TFuture<void>> batchRes;
+
+ for (auto& attr: yqlAttrs.AsMap()) {
+ batchRes.push_back(batch->Set(TStringBuilder() << dstPath << "/@" << attr.first, attr.second));
+ }
+
+ batch->ExecuteBatch();
+ ForEach(batchRes.begin(), batchRes.end(), [mode] (const TFuture<void>& f) {
+ try {
+ f.GetValue();
+ }
+ catch (const TErrorResponse& e) {
+ if (EYtWriteMode::Append != mode || !e.IsConcurrentTransactionLockConflict()) {
+ throw;
+ }
+ }
+ });
+ }
+ };
+ return res.Apply(setAttrs).Apply(commitCheckpoint);
+ }
+
+ static TFuture<void> ExecDropTrackables(const TVector<TString>& paths,
+ const TExecContext<TDropTrackablesOptions>::TPtr& execCtx)
+ {
+ if (paths.empty()) {
+ return MakeFuture();
+ }
+
+ const auto tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TVector<TString> toRemove;
+ for (const auto& p : paths) {
+ toRemove.push_back(NYql::TransformPath(tmpFolder, p, true, execCtx->Session_->UserName_));
+ }
+
+ if (execCtx->Config_->GetLocalChainTest()) {
+ for (const auto& path : toRemove) {
+ YQL_ENSURE(TestTables.erase(path));
+ }
+ return MakeFuture();
+ }
+
+ const auto entry = execCtx->GetEntry();
+
+ toRemove = entry->CancelDeleteAtFinalize(toRemove);
+ if (toRemove.empty()) {
+ return MakeFuture();
+ }
+
+ auto batch = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchResults;
+ for (const auto& p : toRemove) {
+ batchResults.push_back(batch->Remove(p, TRemoveOptions().Force(true)));
+ }
+ batch->ExecuteBatch();
+ return WaitExceptionOrAll(batchResults);
+ }
+
+ static void FillMetadataResult(
+ const TExecContext<TGetTableInfoOptions>::TPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ const ITransactionPtr& tx,
+ const NSorted::TSimpleMap<size_t, TString>& idxs,
+ const TVector<TTableReq>& tables,
+ TTableInfoResult& result)
+ {
+ TVector<NYT::TNode> attributes(tables.size());
+ {
+ auto batchGet = tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes(Reserve(idxs.size()));
+ for (auto& idx: idxs) {
+ batchRes.push_back(batchGet->Get(idx.second + "/@").Apply([&attributes, idx] (const TFuture<NYT::TNode>& res) {
+ attributes[idx.first] = res.GetValue();
+ }));
+ }
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchRes).GetValue();
+ }
+ {
+ auto batchGet = tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes;
+ auto getOpts = TGetOptions()
+ .AttributeFilter(TAttributeFilter()
+ .AddAttribute("type")
+ .AddAttribute(TString{QB2Premapper})
+ .AddAttribute(TString{YqlRowSpecAttribute})
+ );
+ for (auto& idx: idxs) {
+ batchRes.push_back(batchGet->Get(tables[idx.first].Table() + "&/@", getOpts).Apply([idx, &attributes](const TFuture<NYT::TNode>& f) {
+ NYT::TNode attrs = f.GetValue();
+ if (GetType(attrs) == "link") {
+ // override some attributes by the link ones
+ if (attrs.HasKey(QB2Premapper)) {
+ attributes[idx.first][QB2Premapper] = attrs[QB2Premapper];
+ }
+ if (attrs.HasKey(YqlRowSpecAttribute)) {
+ attributes[idx.first][YqlRowSpecAttribute] = attrs[YqlRowSpecAttribute];
+ }
+ }
+ }));
+ }
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchRes).GetValue();
+ }
+
+ auto batchGet = tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchRes;
+
+ TVector<std::pair<size_t, TString>> idxsToInferFromContent;
+
+ for (auto& idx: idxs) {
+ try {
+ NYT::TNode& attrs = attributes[idx.first];
+
+ TYtTableMetaInfo::TPtr metaInfo = result.Data[idx.first].Meta;
+ TYtTableStatInfo::TPtr statInfo = MakeIntrusive<TYtTableStatInfo>();
+ result.Data[idx.first].Stat = statInfo;
+
+ auto type = GetType(attrs);
+ ui16 viewSyntaxVersion = 0;
+ if (type == "document") {
+ if (attrs.HasKey(YqlTypeAttribute)) {
+ auto typeAttr = attrs[YqlTypeAttribute];
+ type = typeAttr.AsString();
+ auto verAttr = typeAttr.Attributes()["syntax_version"];
+ viewSyntaxVersion = verAttr.IsUndefined() ? 0 : verAttr.AsInt64();
+ }
+ }
+
+ if (type != "table" && type != "replicated_table" && type != YqlTypeView) {
+ YQL_LOG_CTX_THROW TErrorException(TIssuesIds::YT_ENTRY_NOT_TABLE_OR_VIEW) << "Input " << tables[idx.first].Table() << " is not a table or a view, got: " << type;
+ }
+
+ statInfo->Id = attrs["id"].AsString();
+ statInfo->TableRevision = attrs["revision"].IntCast<ui64>();
+ statInfo->Revision = GetContentRevision(attrs);
+
+ if (type == YqlTypeView) {
+ batchRes.push_back(batchGet->Get(idx.second).Apply([metaInfo, viewSyntaxVersion](const TFuture<NYT::TNode>& f) {
+ metaInfo->SqlView = f.GetValue().AsString();
+ metaInfo->SqlViewSyntaxVersion = viewSyntaxVersion;
+ metaInfo->CanWrite = false;
+ }));
+ continue;
+ }
+
+ bool isDynamic = attrs.AsMap().contains("dynamic") && NYT::GetBool(attrs["dynamic"]);
+ auto rowCount = attrs[isDynamic ? "chunk_row_count" : "row_count"].AsInt64();
+ statInfo->RecordsCount = rowCount;
+ statInfo->DataSize = GetDataWeight(attrs).GetOrElse(0);
+ statInfo->ChunkCount = attrs["chunk_count"].AsInt64();
+ TString strModifyTime = attrs["modification_time"].AsString();
+ statInfo->ModifyTime = TInstant::ParseIso8601(strModifyTime).Seconds();
+ metaInfo->IsDynamic = isDynamic;
+ if (statInfo->IsEmpty()) {
+ YQL_CLOG(INFO, ProviderYt) << "Empty table : " << tables[idx.first].Table() << ", modify time: " << strModifyTime << ", revision: " << statInfo->Revision;
+ }
+
+ bool schemaValid = ValidateTableSchema(
+ tables[idx.first].Table(), attrs,
+ tables[idx.first].IgnoreYamrDsv(), tables[idx.first].IgnoreWeakSchema()
+ );
+
+ metaInfo->YqlCompatibleScheme = schemaValid;
+
+ TransferTableAttributes(attrs, [metaInfo] (const TString& name, const TString& value) {
+ metaInfo->Attrs[name] = value;
+ });
+
+ if (attrs.AsMap().contains("erasure_codec") && attrs["erasure_codec"].AsString() != "none") {
+ metaInfo->Attrs["erasure_codec"] = attrs["erasure_codec"].AsString();
+ }
+ if (attrs.AsMap().contains("optimize_for") && attrs["optimize_for"].AsString() != "scan") {
+ metaInfo->Attrs["optimize_for"] = attrs["optimize_for"].AsString();
+ }
+
+ NYT::TNode schemaAttrs;
+ if (tables[idx.first].ForceInferSchema() && tables[idx.first].InferSchemaRows() > 0) {
+ metaInfo->Attrs.erase(YqlRowSpecAttribute);
+ if (isDynamic) {
+ schemaAttrs = GetSchemaFromAttributes(attrs, false, tables[idx.first].IgnoreWeakSchema());
+ }
+ else {
+ idxsToInferFromContent.push_back(idx);
+ }
+ }
+ else {
+ if (attrs.HasKey(QB2Premapper)) {
+ metaInfo->Attrs[QB2Premapper] = NYT::NodeToYsonString(attrs[QB2Premapper], NYT::NYson::EYsonFormat::Text);
+ metaInfo->Attrs[TString{YqlRowSpecAttribute}.append("_qb2")] = NYT::NodeToYsonString(
+ QB2PremapperToRowSpec(attrs[QB2Premapper], attrs[SCHEMA_ATTR_NAME]), NYT::NYson::EYsonFormat::Text);
+ }
+
+ if (schemaValid) {
+ schemaAttrs = GetSchemaFromAttributes(attrs, false, tables[idx.first].IgnoreWeakSchema());
+ }
+ else if (!attrs.HasKey(YqlRowSpecAttribute) && !isDynamic && tables[idx.first].InferSchemaRows() > 0) {
+ idxsToInferFromContent.push_back(idx);
+ }
+ }
+
+ if (!schemaAttrs.IsUndefined()) {
+ for (auto& item: schemaAttrs.AsMap()) {
+ metaInfo->Attrs[item.first] = NYT::NodeToYsonString(item.second, NYT::NYson::EYsonFormat::Text);
+ }
+ }
+ } catch (const TErrorException& e) {
+ throw;
+ } catch (...) {
+ throw yexception() << "Error loading '" << tables[idx.first].Table() << "' table metadata: " << CurrentExceptionMessage();
+ }
+ }
+ if (batchRes) {
+ batchGet->ExecuteBatch();
+ WaitExceptionOrAll(batchRes).GetValue();
+ }
+
+ if (idxsToInferFromContent) {
+ TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TString tmpTablePath = NYql::TransformPath(tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(execCtx->Session_->RandomProvider_->GenGuid()), true, execCtx->Session_->UserName_);
+
+ auto inferResult = ExecInferSchema(tmpTablePath, execCtx, entry, tx, idxsToInferFromContent, tables);
+ for (size_t i : xrange(idxsToInferFromContent.size())) {
+ size_t idx = idxsToInferFromContent[i].first;
+ NYT::TNode& attrs = attributes[idx];
+ TYtTableMetaInfo::TPtr metaInfo = result.Data[idx].Meta;
+
+ if (auto inferSchema = inferResult[i]) {
+ NYT::TNode schemaAttrs;
+ if (tables[idx].ForceInferSchema()) {
+ schemaAttrs = GetSchemaFromAttributes(attrs, true, tables[idx].IgnoreWeakSchema());
+ }
+ schemaAttrs[INFER_SCHEMA_ATTR_NAME] = *inferSchema;
+ for (auto& item: schemaAttrs.AsMap()) {
+ metaInfo->Attrs[item.first] = NYT::NodeToYsonString(item.second, NYT::NYson::EYsonFormat::Text);
+ }
+ metaInfo->InferredScheme = true;
+ }
+ }
+ }
+ }
+
+ using TMaybeSchema = TMaybe<NYT::TNode>;
+ static TVector<TMaybeSchema> ExecInferSchema(const TString& tmpTable,
+ const TExecContext<TGetTableInfoOptions>::TPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ const ITransactionPtr& tx,
+ const TVector<std::pair<size_t, TString>>& idxs,
+ const TVector<TTableReq>& tables)
+ {
+ size_t jobThreshold = execCtx->Options_.Config()->InferSchemaTableCountThreshold.Get().GetOrElse(Max<ui32>());
+
+ TVector<TMaybeSchema> result;
+ if (idxs.size() <= jobThreshold) {
+ for (auto& idx : idxs) {
+ YQL_ENSURE(tables[idx.first].InferSchemaRows() > 0);
+ result.push_back(InferSchemaFromTableContents(tx, idx.second, tables[idx.first].Table(), tables[idx.first].InferSchemaRows()));
+ }
+ return result;
+ }
+
+ YQL_ENSURE(!idxs.empty());
+
+ TRawMapOperationSpec mapOpSpec;
+ mapOpSpec.Format(TFormat::YsonBinary());
+ auto job = MakeIntrusive<TYqlInferSchemaJob>();
+
+ {
+ TUserJobSpec userJobSpec;
+ FillUserJobSpec(userJobSpec, execCtx, {}, 0, 0, false);
+ mapOpSpec.MapperSpec(userJobSpec);
+ }
+
+ TVector<TString> inputTables;
+ for (auto& idx : idxs) {
+ YQL_ENSURE(tables[idx.first].InferSchemaRows() > 0);
+ inputTables.push_back(tables[idx.first].Table());
+ auto path = NYT::TRichYPath(idx.second)
+ .AddRange(NYT::TReadRange::FromRowIndices(0, tables[idx.first].InferSchemaRows()));
+ mapOpSpec.AddInput(path);
+ }
+ mapOpSpec.AddOutput(tmpTable);
+ job->SetTableNames(inputTables);
+
+ FillOperationSpec(mapOpSpec, execCtx);
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, 0., Nothing(), EYtOpProp::WithMapper);
+ spec["job_count"] = 1;
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ auto tmpTx = tx->StartTransaction();
+ PrepareTempDestination(tmpTable, execCtx, entry, tmpTx);
+
+ execCtx->RunOperation<false>([tmpTx, job, mapOpSpec = std::move(mapOpSpec), opOpts = std::move(opOpts)](){
+ return tmpTx->RawMap(mapOpSpec, job, opOpts);
+ }).GetValueSync();
+
+ result.resize(idxs.size());
+ auto reader = tmpTx->CreateTableReader<NYT::TNode>(tmpTable);
+ for (; reader->IsValid(); reader->Next()) {
+ auto& row = reader->GetRow();
+ size_t tableIdx = row["index"].AsUint64();
+ YQL_ENSURE(tableIdx < idxs.size());
+
+ auto schema = NYT::NodeFromYsonString(row["schema"].AsString());
+ if (schema.IsString()) {
+ YQL_LOG_CTX_THROW yexception() << schema.AsString();
+ }
+ result[tableIdx] = schema;
+ }
+ reader.Drop();
+ tmpTx->Abort();
+ return result;
+ }
+
+
+ TFuture<TResOrPullResult> DoPull(const TSession::TPtr& session, NNodes::TPull pull, TExprContext& ctx, TResOrPullOptions&& options) {
+ if (options.FillSettings().Discard) {
+ TResOrPullResult res;
+ res.SetSuccess();
+ return MakeFuture(res);
+ }
+ TVector<TString> columns(NCommon::GetResOrPullColumnHints(pull.Ref()));
+ if (columns.empty()) {
+ columns = NCommon::GetStructFields(pull.Input().Ref().GetTypeAnn());
+ }
+
+ bool ref = NCommon::HasResOrPullOption(pull.Ref(), "ref");
+ bool autoRef = NCommon::HasResOrPullOption(pull.Ref(), "autoref");
+
+ auto cluster = TString{GetClusterName(pull.Input())};
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, pull.Raw(), &ctx);
+
+ if (auto read = pull.Input().Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ execCtx->SetInput(read.Cast().Input(), false, {});
+ } else {
+ execCtx->SetInput(pull.Input(), false, {});
+ }
+
+ TRecordsRange range;
+ if (!ref) {
+ if (auto read = pull.Input().Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ YQL_ENSURE(read.Cast().Input().Size() == 1);
+ range.Fill(read.Cast().Input().Item(0).Settings().Ref());
+ }
+ }
+
+ TString type;
+ if (NCommon::HasResOrPullOption(pull.Ref(), "type")) {
+ TStringStream typeYson;
+ ::NYson::TYsonWriter typeWriter(&typeYson);
+ NCommon::WriteResOrPullType(typeWriter, pull.Input().Ref().GetTypeAnn(), columns);
+ type = typeYson.Str();
+ }
+
+ auto pos = ctx.GetPosition(pull.Pos());
+
+ return session->Queue_->Async([type, ref, range, autoRef, execCtx, columns, pos] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ try {
+ TResOrPullResult res;
+ TStringStream out;
+ ::NYson::TYsonWriter writer(&out, NCommon::GetYsonFormat(execCtx->Options_.FillSettings()), ::NYson::EYsonType::Node, false);
+ writer.OnBeginMap();
+ if (type) {
+ writer.OnKeyedItem("Type");
+ writer.OnRaw(type);
+ }
+
+ bool truncated = false;
+ if (!ref) {
+ truncated = ExecPull(execCtx, writer, range, columns);
+ }
+ if (ref || (truncated && autoRef)) {
+ writer.OnKeyedItem("Ref");
+ writer.OnBeginList();
+ TVector<TString> keepTables;
+ for (auto& table: execCtx->InputTables_) {
+ writer.OnListItem();
+ if (table.Temp) {
+ keepTables.push_back(table.Name);
+ }
+ NYql::WriteTableReference(writer, YtProviderName, execCtx->Cluster_, table.Name, table.Temp, columns);
+ }
+ writer.OnEndList();
+ if (!keepTables.empty()) {
+ auto entry = execCtx->GetEntry();
+ // TODO: check anonymous tables
+ entry->CancelDeleteAtFinalize(keepTables);
+ }
+ }
+
+ if (truncated) {
+ writer.OnKeyedItem("Truncated");
+ writer.OnBooleanScalar(true);
+ }
+
+ writer.OnEndMap();
+ res.Data = out.Str();
+ res.SetSuccess();
+
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TResOrPullResult>(pos);
+ }
+ });
+ }
+
+ static bool ExecPull(const TExecContext<TResOrPullOptions>::TPtr& execCtx,
+ ::NYson::TYsonWriter& writer,
+ const TRecordsRange& range,
+ const TVector<TString>& columns)
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TMemoryUsageInfo memInfo("Pull");
+ TTypeEnvironment env(alloc);
+ THolderFactory holderFactory(alloc.Ref(), memInfo, execCtx->FunctionRegistry_);
+ NCommon::TCodecContext codecCtx(env, *execCtx->FunctionRegistry_, &holderFactory);
+
+ bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+
+ TVector<TString> tables;
+ for (const TInputInfo& table: execCtx->InputTables_) {
+ auto tablePath = table.Path;
+ tables.push_back(table.Temp ? TString() : table.Name);
+ }
+
+ TMkqlIOSpecs specs;
+ if (useSkiff) {
+ specs.SetUseSkiff(execCtx->Options_.OptLLVM(), testRun ? TMkqlIOSpecs::ESystemField(0) : TMkqlIOSpecs::ESystemField::RangeIndex | TMkqlIOSpecs::ESystemField::RowIndex);
+ }
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ specs.Init(codecCtx, execCtx->GetInputSpec(!useSkiff, nativeTypeCompat, false), tables, columns);
+
+ TExecuteResOrPull pullData(execCtx->Options_.FillSettings().RowsLimitPerWrite,
+ execCtx->Options_.FillSettings().AllResultsBytesLimit, MakeMaybe(columns));
+ TMkqlIOCache specsCache(specs, holderFactory);
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1U, "Support single input only.");
+ const auto itI = TestTables.find(execCtx->InputTables_.front().Path.Path_);
+ YQL_ENSURE(TestTables.cend() != itI);
+
+ TMkqlInput input(MakeStringInput(std::move(itI->second.second), false));
+ TMkqlReaderImpl reader(input, 0, 4 << 10, 0);
+ reader.SetSpecs(specs, holderFactory);
+ for (reader.Next(); reader.IsValid(); reader.Next()) {
+ if (!pullData.WriteNext(specsCache, reader.GetRow(), 0)) {
+ return true;
+ }
+ }
+ } else if (auto limiter = TTableLimiter(range)) {
+ auto entry = execCtx->GetEntry();
+ bool stop = false;
+ for (size_t i = 0; i < execCtx->InputTables_.size(); ++i) {
+ TString srcTableName = execCtx->InputTables_[i].Name;
+ NYT::TRichYPath srcTable = execCtx->InputTables_[i].Path;
+ bool isDynamic = execCtx->InputTables_[i].Dynamic;
+ ui64 recordsCount = execCtx->InputTables_[i].Records;
+ if (!isDynamic) {
+ if (!limiter.NextTable(recordsCount)) {
+ continue;
+ }
+ } else {
+ limiter.NextDynamicTable();
+ }
+
+ if (isDynamic) {
+ YQL_ENSURE(srcTable.GetRanges().Empty());
+ stop = NYql::SelectRows(entry->Client, srcTableName, i, specsCache, pullData, limiter);
+ } else {
+ auto readTx = entry->Tx;
+ if (srcTable.TransactionId_) {
+ readTx = entry->GetSnapshotTx(*srcTable.TransactionId_);
+ srcTable.TransactionId_.Clear();
+ }
+ if (execCtx->YamrInput) {
+ stop = NYql::IterateYamredRows(readTx, srcTable, i, specsCache, pullData, limiter, execCtx->Sampling);
+ } else {
+ stop = NYql::IterateYsonRows(readTx, srcTable, i, specsCache, pullData, limiter, execCtx->Sampling);
+ }
+ }
+ if (stop || limiter.Exceed()) {
+ break;
+ }
+ }
+ }
+
+ specs.Clear();
+ writer.OnKeyedItem("Data");
+ writer.OnBeginList(); // Pull returns list fragment
+ writer.OnRaw(pullData.Finish(), ::NYson::EYsonType::ListFragment);
+ writer.OnEndList();
+
+ return pullData.IsTruncated();
+ }
+
+ TFuture<TResOrPullResult> DoResult(const TSession::TPtr& session, NNodes::TResult result, TExprContext& ctx, TResOrPullOptions&& options) {
+ TVector<TString> columns(NCommon::GetResOrPullColumnHints(result.Ref()));
+ if (columns.empty()) {
+ columns = NCommon::GetStructFields(result.Input().Ref().GetTypeAnn());
+ }
+
+ TString lambda;
+ bool hasListResult = false;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ Services_.FunctionRegistry->SupportsSizedAllocators());
+ alloc.SetLimit(options.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *session);
+ auto rootNode = builder.BuildLambda(*MkqlCompiler_, result.Input().Ptr(), ctx);
+ hasListResult = rootNode.GetStaticType()->IsList();
+ lambda = SerializeRuntimeNode(rootNode, builder.GetTypeEnvironment());
+ }
+ auto extraUsage = ScanExtraResourceUsage(result.Input().Ref(), *options.Config());
+
+ TString type;
+ if (NCommon::HasResOrPullOption(result.Ref(), "type")) {
+ TStringStream typeYson;
+ ::NYson::TYsonWriter typeWriter(&typeYson);
+ NCommon::WriteResOrPullType(typeWriter, result.Input().Ref().GetTypeAnn(), columns);
+ type = typeYson.Str();
+ }
+
+ TString cluster = options.UsedCluster();
+ if (cluster.empty()) {
+ cluster = options.Config()->DefaultCluster.Get().GetOrElse(TString());
+ }
+ if (cluster.empty()) {
+ cluster = Clusters_->GetDefaultClusterName();
+ }
+ TString tmpFolder = options.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TString tmpTablePath = NYql::TransformPath(tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(session->RandomProvider_->GenGuid()), true, session->UserName_);
+ bool discard = options.FillSettings().Discard;
+ auto execCtx = MakeExecCtx(std::move(options), session, cluster, result.Input().Raw(), &ctx);
+ auto pos = ctx.GetPosition(result.Pos());
+
+ return session->Queue_->Async([lambda, hasListResult, extraUsage, tmpTablePath, execCtx, columns] () {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecCalc(lambda, extraUsage, tmpTablePath, execCtx, {},
+ TExprResultFactory(execCtx->Options_.FillSettings().RowsLimitPerWrite,
+ execCtx->Options_.FillSettings().AllResultsBytesLimit, columns, hasListResult),
+ &columns);
+ })
+ .Apply([type, execCtx, discard, pos] (const TFuture<std::pair<TString, bool>>& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ try {
+ const std::pair<TString, bool>& value = f.GetValue();
+
+ TResOrPullResult res;
+ TStringStream out;
+ ::NYson::TYsonWriter writer(discard ? (IOutputStream*)&Cnull : (IOutputStream*)&out, NCommon::GetYsonFormat(execCtx->Options_.FillSettings()), ::NYson::EYsonType::Node, true);
+ writer.OnBeginMap();
+ if (type) {
+ writer.OnKeyedItem("Type");
+ writer.OnRaw(type);
+ }
+
+ writer.OnKeyedItem("Data");
+ writer.OnRaw(value.first);
+
+ if (value.second) {
+ writer.OnKeyedItem("Truncated");
+ writer.OnBooleanScalar(true);
+ }
+
+ writer.OnEndMap();
+ if (!discard) {
+ res.Data = out.Str();
+ }
+ res.SetSuccess();
+
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TResOrPullResult>(pos);
+ }
+ });
+ }
+
+ TFuture<void> DoSort(TYtSort /*sort*/, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1);
+
+ return execCtx->Session_->Queue_->Async([execCtx]() {
+ return execCtx->LookupQueryCacheAsync().Apply([execCtx] (const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ bool cacheHit = f.GetValue();
+ TVector<TRichYPath> outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+
+ bool hasNonStrict = false;
+ TSortOperationSpec sortOpSpec;
+ for (const auto& table: execCtx->InputTables_) {
+ if (!table.Strict) {
+ hasNonStrict = true;
+ }
+ sortOpSpec.AddInput(table.Path);
+ }
+
+ sortOpSpec.Output(outYPaths.front());
+ sortOpSpec.SortBy(execCtx->OutTables_.front().SortedBy);
+ sortOpSpec.SchemaInferenceMode(ESchemaInferenceMode::FromOutput);
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+
+ FillSpec(spec, *execCtx, entry, 0., Nothing(), EYtOpProp::IntermediateData);
+ if (hasNonStrict) {
+ spec["schema_inference_mode"] = "from_output"; // YTADMINREQ-17692
+ }
+
+ return execCtx->RunOperation([entry, sortOpSpec = std::move(sortOpSpec), spec = std::move(spec)](){
+ return entry->Tx->Sort(sortOpSpec, TOperationOptions().StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec));
+ });
+ });
+ });
+ }
+
+ TFuture<void> DoCopy(TYtCopy /*copy*/, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1);
+ YQL_ENSURE(execCtx->InputTables_.front().Temp);
+ YQL_ENSURE(execCtx->OutTables_.size() == 1);
+
+ return execCtx->Session_->Queue_->Async([execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ execCtx->QueryCacheItem.Destroy(); // Don't use cache for YtCopy
+ TOutputInfo& out = execCtx->OutTables_.front();
+
+ entry->DeleteAtFinalize(out.Path);
+
+ entry->CreateDefaultTmpFolder();
+ CreateParents({out.Path}, entry->CacheTx);
+ entry->Tx->Copy(execCtx->InputTables_.front().Name, out.Path, TCopyOptions().Force(true));
+
+ });
+ }
+
+ TFuture<void> DoMerge(TYtMerge merge, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1);
+ bool forceTransform = NYql::HasSetting(merge.Settings().Ref(), EYtSettingType::ForceTransform);
+ bool combineChunks = NYql::HasSetting(merge.Settings().Ref(), EYtSettingType::CombineChunks);
+ TMaybe<ui64> limit = GetLimit(merge.Settings().Ref());
+
+ return execCtx->Session_->Queue_->Async([forceTransform, combineChunks, limit, execCtx]() {
+ return execCtx->LookupQueryCacheAsync().Apply([forceTransform, combineChunks, limit, execCtx] (const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ bool cacheHit = f.GetValue();
+ TVector<TRichYPath> outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+
+ bool hasNonStrict = false;
+ TMergeOperationSpec mergeOpSpec;
+ for (const auto& table: execCtx->InputTables_) {
+ if (!table.Strict) {
+ hasNonStrict = true;
+ }
+ mergeOpSpec.AddInput(table.Path);
+ }
+
+ if (execCtx->OutTables_.front().SortedBy.Parts_.empty()) {
+ mergeOpSpec.Mode(EMergeMode::MM_ORDERED);
+ if (limit) {
+ outYPaths.front().RowCountLimit(*limit);
+ }
+ } else {
+ mergeOpSpec.Mode(EMergeMode::MM_SORTED);
+ mergeOpSpec.MergeBy(execCtx->OutTables_.front().SortedBy);
+ }
+
+ mergeOpSpec.Output(outYPaths.front());
+
+ mergeOpSpec.ForceTransform(forceTransform);
+ mergeOpSpec.CombineChunks(combineChunks);
+ mergeOpSpec.SchemaInferenceMode(ESchemaInferenceMode::FromOutput);
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ EYtOpProps flags = EYtOpProp::AllowSampling;
+ if (combineChunks) {
+ flags |= EYtOpProp::TemporaryChunkCombine;
+ }
+ FillSpec(spec, *execCtx, entry, 0., Nothing(), flags);
+ if (hasNonStrict) {
+ spec["schema_inference_mode"] = "from_output"; // YTADMINREQ-17692
+ }
+
+ return execCtx->RunOperation([entry, mergeOpSpec = std::move(mergeOpSpec), spec = std::move(spec)](){
+ return entry->Tx->Merge(mergeOpSpec, TOperationOptions().StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec));
+ });
+ });
+ });
+ }
+
+ static TFuture<void> ExecMap(
+ bool ordered,
+ const TMaybe<ui64>& jobCount,
+ const TMaybe<ui64>& limit,
+ const TVector<TString>& sortLimitBy,
+ TString mapLambda,
+ const TString& inputType,
+ const TExpressionResorceUsage& extraUsage,
+ const TExecContext<TRunOptions>::TPtr& execCtx
+ ) {
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+ TFuture<bool> ret = testRun ? MakeFuture<bool>(false) : execCtx->LookupQueryCacheAsync();
+ return ret.Apply([ordered, jobCount, limit, sortLimitBy, mapLambda,
+ inputType, extraUsage, execCtx, testRun] (const auto& f) mutable
+ {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ TTransactionCache::TEntry::TPtr entry;
+ TVector<TRichYPath> outYPaths;
+ if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Test mode support single output only.");
+ const auto& out = execCtx->OutTables_.front();
+ outYPaths.emplace_back(TRichYPath(out.Path).Schema(RowSpecToYTSchema(TestTables[out.Path].first = out.Spec[YqlRowSpecAttribute], NTCF_NONE)));
+ } else {
+ entry = execCtx->GetEntry();
+ bool cacheHit = f.GetValue();
+ outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ }
+
+ TRawMapOperationSpec mapOpSpec;
+ auto job = MakeIntrusive<TYqlUserJob>();
+
+ job->SetInputType(inputType);
+
+ for (size_t i: xrange(execCtx->OutTables_.size())) {
+ if (!execCtx->OutTables_[i].SortedBy.Parts_.empty()) {
+ mapOpSpec.Ordered(true);
+ }
+ else if (limit && sortLimitBy.empty()) {
+ outYPaths[i].RowCountLimit(*limit);
+ }
+ mapOpSpec.AddOutput(outYPaths[i]);
+ }
+
+ TVector<ui32> groups;
+ TVector<TString> tables;
+ TVector<ui64> rowOffsets;
+ ui64 currentRowOffset = 0;
+ TSet<TString> remapperAllFiles;
+ TRemapperMap remapperMap;
+
+ bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+ bool hasTablesWithoutQB2Premapper = false;
+
+ for (const TInputInfo& table: execCtx->InputTables_) {
+ auto tablePath = table.Path;
+ if (!table.QB2Premapper.IsUndefined()) {
+ bool tableUseSkiff = false;
+
+ ProcessTableQB2Premapper(table.QB2Premapper, table.Name, tablePath, mapOpSpec.GetInputs().size(),
+ remapperMap, remapperAllFiles, tableUseSkiff);
+
+ useSkiff = useSkiff && tableUseSkiff;
+ }
+ else {
+ hasTablesWithoutQB2Premapper = true;
+ }
+
+ if (!groups.empty() && groups.back() != table.Group) {
+ currentRowOffset = 0;
+ }
+
+ mapOpSpec.AddInput(tablePath);
+ groups.push_back(table.Group);
+ tables.push_back(table.Temp ? TString() : table.Name);
+ rowOffsets.push_back(currentRowOffset);
+ currentRowOffset += table.Records;
+ }
+
+ bool forceYsonInputFormat = false;
+
+ if (useSkiff && !remapperMap.empty()) {
+ // Disable skiff in case of mix of QB2 and normal tables
+ if (hasTablesWithoutQB2Premapper) {
+ useSkiff = false;
+ } else {
+ UpdateQB2PremapperUseSkiff(remapperMap, useSkiff);
+ forceYsonInputFormat = useSkiff;
+ }
+ }
+
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ job->SetInputSpec(execCtx->GetInputSpec(!useSkiff || forceYsonInputFormat, nativeTypeCompat, false));
+ job->SetOutSpec(execCtx->GetOutSpec(!useSkiff, nativeTypeCompat));
+ if (!groups.empty() && groups.back() != 0) {
+ job->SetInputGroups(groups);
+ }
+ job->SetTableNames(tables);
+ job->SetRowOffsets(rowOffsets);
+
+ if (ordered) {
+ mapOpSpec.Ordered(true);
+ }
+
+ job->SetYamrInput(execCtx->YamrInput);
+ job->SetUseSkiff(useSkiff, testRun ? TMkqlIOSpecs::ESystemField(0) : TMkqlIOSpecs::ESystemField::RowIndex);
+
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+ {
+ TUserJobSpec userJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ builder.UpdateLambdaCode(mapLambda, nodeCount, transform);
+ job->SetLambdaCode(mapLambda);
+ job->SetOptLLVM(execCtx->Options_.OptLLVM());
+ job->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ transform.ApplyJobProps(*job);
+ transform.ApplyUserJobSpec(userJobSpec, testRun);
+
+ ui64 fileMemUsage = transform.GetUsedMemory();
+ for (auto& f: remapperAllFiles) {
+ fileMemUsage += GetUncompressedFileSize(entry->Tx, f).GetOrElse(i64(1) << 10);
+ userJobSpec.AddFile(TRichYPath(f).Executable(true));
+ }
+ if (!remapperMap.empty()) {
+ fileMemUsage += 512_MB;
+ }
+
+ FillUserJobSpec(userJobSpec, execCtx, extraUsage, fileMemUsage, execCtx->EstimateLLVMMem(nodeCount), testRun,
+ GetQB2PremapperPrefix(remapperMap, useSkiff));
+
+ mapOpSpec.MapperSpec(userJobSpec);
+ }
+ FillOperationSpec(mapOpSpec, execCtx);
+ auto formats = job->GetIOFormats(execCtx->FunctionRegistry_);
+ mapOpSpec.InputFormat(forceYsonInputFormat ? NYT::TFormat::YsonBinary() : formats.first);
+ mapOpSpec.OutputFormat(formats.second);
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1U, "Support single input only.");
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Support single output only.");
+
+ const auto itI = TestTables.find(execCtx->InputTables_.front().Path.Path_);
+ YQL_ENSURE(TestTables.cend() != itI);
+ const auto itO = TestTables.find(execCtx->OutTables_.front().Path);
+ YQL_ENSURE(TestTables.cend() != itO);
+
+ TStringInput in(itI->second.second);
+ TStringOutput out(itO->second.second);
+
+ LocalRawMapReduce(mapOpSpec, job.Get(), &in, &out);
+ DumpLocalTable(itO->second.second, execCtx->Config_->GetLocalChainFile());
+ return MakeFuture();
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, extraUsage.Cpu, Nothing(),
+ EYtOpProp::TemporaryAutoMerge | EYtOpProp::WithMapper | EYtOpProp::WithUserJobs | EYtOpProp::AllowSampling);
+
+ if (jobCount) {
+ spec["job_count"] = static_cast<i64>(*jobCount);
+ }
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, job, mapOpSpec = std::move(mapOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawMap(mapOpSpec, job, opOpts);
+ });
+ });
+ }
+
+ TFuture<void> DoMap(TYtMap map, const TExecContext<TRunOptions>::TPtr& execCtx, TExprContext& ctx) {
+ const bool ordered = NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered);
+ TMaybe<ui64> jobCount;
+ if (auto setting = NYql::GetSetting(map.Settings().Ref(), EYtSettingType::JobCount)) {
+ jobCount = FromString<ui64>(setting->Child(1)->Content());
+ }
+
+ TString mapLambda;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *execCtx->Session_);
+ mapLambda = builder.BuildLambdaWithIO(*MkqlCompiler_, map.Mapper(), ctx);
+ }
+
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(map.Settings().Ref(), EYtSettingType::SortLimitBy);
+ TMaybe<ui64> limit = GetLimit(map.Settings().Ref());
+ if (limit && !sortLimitBy.empty() && *limit > execCtx->Options_.Config()->TopSortMaxLimit.Get().GetOrElse(DEFAULT_TOP_SORT_LIMIT)) {
+ limit.Clear();
+ }
+ auto extraUsage = execCtx->ScanExtraResourceUsage(map.Mapper().Body().Ref(), true);
+ TString inputType = NCommon::WriteTypeToYson(GetSequenceItemType(map.Input().Size() == 1U ? TExprBase(map.Input().Item(0)) : TExprBase(map.Mapper().Args().Arg(0)), true));
+
+ return execCtx->Session_->Queue_->Async([ordered, jobCount, limit, sortLimitBy, mapLambda, inputType, extraUsage, execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecMap(ordered, jobCount, limit, sortLimitBy, mapLambda, inputType, extraUsage, execCtx);
+ });
+ }
+
+ static TFuture<void> ExecReduce(const TVector<std::pair<TString, bool>>& reduceBy,
+ const TVector<std::pair<TString, bool>>& sortBy,
+ bool joinReduce,
+ const TMaybe<ui64>& maxDataSizePerJob,
+ bool useFirstAsPrimary,
+ const TMaybe<ui64>& limit,
+ const TVector<TString>& sortLimitBy,
+ TString reduceLambda,
+ const TString& inputType,
+ const TExpressionResorceUsage& extraUsage,
+ const TExecContext<TRunOptions>::TPtr& execCtx
+ ) {
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+ TFuture<bool> ret = testRun ? MakeFuture<bool>(false) : execCtx->LookupQueryCacheAsync();
+ return ret.Apply([reduceBy, sortBy, joinReduce, maxDataSizePerJob, useFirstAsPrimary, limit,
+ sortLimitBy, reduceLambda, inputType, extraUsage, execCtx, testRun]
+ (const auto& f) mutable
+ {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ TTransactionCache::TEntry::TPtr entry;
+ TVector<TRichYPath> outYPaths;
+ if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Test mode support single output only.");
+ const auto& out = execCtx->OutTables_.front();
+ outYPaths.emplace_back(TRichYPath(out.Path).Schema(RowSpecToYTSchema(TestTables[out.Path].first = out.Spec[YqlRowSpecAttribute], NTCF_NONE)));
+ } else {
+ entry = execCtx->GetEntry();
+ const bool cacheHit = f.GetValue();
+ outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ }
+
+ TRawReduceOperationSpec reduceOpSpec;
+ auto job = MakeIntrusive<TYqlUserJob>();
+
+ job->SetInputType(inputType);
+
+ for (size_t i: xrange(execCtx->OutTables_.size())) {
+ if (limit && sortLimitBy.empty()) {
+ outYPaths[i].RowCountLimit(*limit);
+ }
+ reduceOpSpec.AddOutput(outYPaths[i]);
+ }
+
+ TVector<ui32> groups;
+ TVector<TString> tables;
+ TVector<ui64> rowOffsets;
+ ui64 currentRowOffset = 0;
+ YQL_ENSURE(!execCtx->InputTables_.empty());
+ const ui32 primaryGroup = useFirstAsPrimary ? execCtx->InputTables_.front().Group : execCtx->InputTables_.back().Group;
+ for (const auto& table : execCtx->InputTables_) {
+ if (joinReduce) {
+ auto yPath = table.Path;
+ if (table.Group == primaryGroup) {
+ yPath.Primary(true);
+ } else {
+ yPath.Foreign(true);
+ }
+ reduceOpSpec.AddInput(yPath);
+ } else {
+ reduceOpSpec.AddInput(table.Path);
+ }
+ if (!groups.empty() && groups.back() != table.Group) {
+ currentRowOffset = 0;
+ }
+
+ groups.push_back(table.Group);
+ tables.push_back(table.Temp ? TString() : table.Name);
+ rowOffsets.push_back(currentRowOffset);
+ currentRowOffset += table.Records;
+ }
+
+ const bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ job->SetInputSpec(execCtx->GetInputSpec(!useSkiff, nativeTypeCompat, false));
+ job->SetOutSpec(execCtx->GetOutSpec(!useSkiff, nativeTypeCompat));
+ YQL_ENSURE(!groups.empty());
+ if (groups.back() != 0) {
+ job->SetInputGroups(groups);
+ }
+ job->SetTableNames(tables);
+ job->SetRowOffsets(rowOffsets);
+
+ if (joinReduce) {
+ reduceOpSpec.JoinBy(ToYTSortColumns(reduceBy));
+ reduceOpSpec.EnableKeyGuarantee(false);
+ } else {
+ reduceOpSpec.ReduceBy(ToYTSortColumns(reduceBy));
+ }
+
+ if (!sortBy.empty()) {
+ reduceOpSpec.SortBy(ToYTSortColumns(sortBy));
+ } else {
+ reduceOpSpec.SortBy(ToYTSortColumns(reduceBy));
+ }
+
+ THashSet<TString> auxColumns;
+ std::for_each(reduceBy.begin(), reduceBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ if (!sortBy.empty()) {
+ std::for_each(sortBy.begin(), sortBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ }
+ job->SetAuxColumns(auxColumns);
+
+ job->SetUseSkiff(useSkiff, TMkqlIOSpecs::ESystemField::RowIndex | TMkqlIOSpecs::ESystemField::KeySwitch);
+ job->SetYamrInput(execCtx->YamrInput);
+
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+ {
+ TUserJobSpec userJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ builder.UpdateLambdaCode(reduceLambda, nodeCount, transform);
+ job->SetLambdaCode(reduceLambda);
+ job->SetOptLLVM(execCtx->Options_.OptLLVM());
+ job->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ transform.ApplyJobProps(*job);
+ transform.ApplyUserJobSpec(userJobSpec, testRun);
+ FillUserJobSpec(userJobSpec, execCtx, extraUsage, transform.GetUsedMemory(), execCtx->EstimateLLVMMem(nodeCount), testRun);
+ reduceOpSpec.ReducerSpec(userJobSpec);
+ }
+ FillOperationSpec(reduceOpSpec, execCtx);
+ auto formats = job->GetIOFormats(execCtx->FunctionRegistry_);
+ reduceOpSpec.InputFormat(formats.first);
+ reduceOpSpec.OutputFormat(formats.second);
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1U, "Support single input only.");
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Support single output only.");
+
+ const auto itI = TestTables.find(execCtx->InputTables_.front().Path.Path_);
+ YQL_ENSURE(TestTables.cend() != itI);
+ const auto itO = TestTables.find(execCtx->OutTables_.front().Path);
+ YQL_ENSURE(TestTables.cend() != itO);
+
+ TStringInput in(itI->second.second);
+ TStringOutput out(itO->second.second);
+
+ LocalRawMapReduce(reduceOpSpec, job.Get(), &in, &out);
+ DumpLocalTable(itO->second.second, execCtx->Config_->GetLocalChainFile());
+ return MakeFuture();
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, extraUsage.Cpu, Nothing(),
+ EYtOpProp::TemporaryAutoMerge | EYtOpProp::WithReducer | EYtOpProp::WithUserJobs | EYtOpProp::AllowSampling);
+
+ if (maxDataSizePerJob) {
+ spec["max_data_size_per_job"] = static_cast<i64>(*maxDataSizePerJob);
+ }
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, job, reduceOpSpec = std::move(reduceOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawReduce(reduceOpSpec, job, opOpts);
+ });
+ });
+ }
+
+ TFuture<void> DoReduce(TYtReduce reduce, const TExecContext<TRunOptions>::TPtr &execCtx, TExprContext& ctx) {
+ auto reduceBy = NYql::GetSettingAsColumnPairList(reduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ auto sortBy = NYql::GetSettingAsColumnPairList(reduce.Settings().Ref(), EYtSettingType::SortBy);
+ bool joinReduce = NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::JoinReduce);
+ auto maxDataSizePerJob = NYql::GetMaxJobSizeForFirstAsPrimary(reduce.Settings().Ref());
+ bool useFirstAsPrimary = NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::FirstAsPrimary);
+
+ TString reduceLambda;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *execCtx->Session_);
+ reduceLambda = builder.BuildLambdaWithIO(*MkqlCompiler_, reduce.Reducer(), ctx);
+ }
+
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(reduce.Settings().Ref(), EYtSettingType::SortLimitBy);
+ TMaybe<ui64> limit = GetLimit(reduce.Settings().Ref());
+ if (limit && !sortLimitBy.empty() && *limit > execCtx->Options_.Config()->TopSortMaxLimit.Get().GetOrElse(DEFAULT_TOP_SORT_LIMIT)) {
+ limit.Clear();
+ }
+ auto extraUsage = execCtx->ScanExtraResourceUsage(reduce.Reducer().Body().Ref(), true);
+ const auto inputTypeSet = NYql::GetSetting(reduce.Settings().Ref(), EYtSettingType::ReduceInputType);
+ TString inputType = NCommon::WriteTypeToYson(inputTypeSet
+ ? inputTypeSet->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType()
+ : GetSequenceItemType(reduce.Reducer().Args().Arg(0), true)
+ );
+
+ return execCtx->Session_->Queue_->Async([reduceBy, sortBy, joinReduce, maxDataSizePerJob, useFirstAsPrimary, limit, sortLimitBy, reduceLambda, inputType, extraUsage, execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecReduce(reduceBy, sortBy, joinReduce, maxDataSizePerJob, useFirstAsPrimary, limit,
+ sortLimitBy, reduceLambda, inputType, extraUsage, execCtx);
+ });
+ }
+
+ static TFuture<void> ExecMapReduce(
+ const TVector<std::pair<TString, bool>>& reduceBy,
+ const TVector<std::pair<TString, bool>>& sortBy,
+ const TMaybe<ui64>& limit,
+ const TVector<TString>& sortLimitBy,
+ TString mapLambda,
+ const TString& mapInputType,
+ size_t mapDirectOutputs,
+ const TExpressionResorceUsage& mapExtraUsage,
+ TString reduceLambda,
+ const TString& reduceInputType,
+ const TExpressionResorceUsage& reduceExtraUsage,
+ NYT::TNode intermediateMeta,
+ const NYT::TNode& intermediateSchema,
+ const TExecContext<TRunOptions>::TPtr& execCtx
+ ) {
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+ TFuture<bool> ret = testRun ? MakeFuture<bool>(false) : execCtx->LookupQueryCacheAsync();
+ return ret.Apply([reduceBy, sortBy, limit, sortLimitBy, mapLambda, mapInputType, mapDirectOutputs,
+ mapExtraUsage, reduceLambda, reduceInputType, reduceExtraUsage,
+ intermediateMeta, intermediateSchema, execCtx, testRun]
+ (const auto& f) mutable
+ {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ TTransactionCache::TEntry::TPtr entry;
+ TVector<TRichYPath> outYPaths;
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Test mode support single output only.");
+ const auto& out = execCtx->OutTables_.front();
+ outYPaths.emplace_back(TRichYPath(out.Path).Schema(RowSpecToYTSchema(TestTables[out.Path].first = out.Spec[YqlRowSpecAttribute], NTCF_NONE)));
+ } else {
+ entry = execCtx->GetEntry();
+ const bool cacheHit = f.GetValue();
+ outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ }
+
+ TRawMapReduceOperationSpec mapReduceOpSpec;
+ YQL_ENSURE(mapDirectOutputs < execCtx->OutTables_.size());
+
+ for (size_t i: xrange(execCtx->OutTables_.size())) {
+ if (limit && sortLimitBy.empty()) {
+ outYPaths[i].RowCountLimit(*limit);
+ }
+ if (i < mapDirectOutputs) {
+ mapReduceOpSpec.AddMapOutput(outYPaths[i]);
+ } else {
+ mapReduceOpSpec.AddOutput(outYPaths[i]);
+ }
+ }
+
+ bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+ const bool reduceUseSkiff = useSkiff;
+ bool hasTablesWithoutQB2Premapper = false;
+
+ TVector<ui32> groups;
+ TVector<TString> tables;
+ TVector<ui64> rowOffsets;
+ ui64 currentRowOffset = 0;
+ TSet<TString> remapperAllFiles;
+ TRemapperMap remapperMap;
+ for (auto& table: execCtx->InputTables_) {
+ auto tablePath = table.Path;
+ if (!table.QB2Premapper.IsUndefined()) {
+ bool tableUseSkiff = false;
+
+ ProcessTableQB2Premapper(table.QB2Premapper, table.Name, tablePath, mapReduceOpSpec.GetInputs().size(),
+ remapperMap, remapperAllFiles, tableUseSkiff);
+
+ useSkiff = useSkiff && tableUseSkiff;
+ }
+ else {
+ hasTablesWithoutQB2Premapper = true;
+ }
+ if (!groups.empty() && groups.back() != table.Group) {
+ currentRowOffset = 0;
+ }
+
+ mapReduceOpSpec.AddInput(tablePath);
+ groups.push_back(table.Group);
+ tables.push_back(table.Temp ? TString() : table.Name);
+ rowOffsets.push_back(currentRowOffset);
+ currentRowOffset += table.Records;
+ }
+
+ bool forceYsonInputFormat = false;
+
+ if (useSkiff && !remapperMap.empty()) {
+ // Disable skiff in case of mix of QB2 and normal tables
+ if (hasTablesWithoutQB2Premapper) {
+ useSkiff = false;
+ } else {
+ UpdateQB2PremapperUseSkiff(remapperMap, useSkiff);
+ forceYsonInputFormat = useSkiff;
+ }
+ }
+
+ NYT::TNode mapSpec = intermediateMeta;
+ mapSpec.AsMap().erase(YqlSysColumnPrefix);
+
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+
+ NYT::TNode mapOutSpec = NYT::TNode::CreateMap();
+ mapOutSpec[YqlIOSpecTables] = NYT::TNode::CreateList();
+ mapOutSpec[YqlIOSpecTables].Add(mapSpec);
+ TString mapOutSpecStr;
+ if (mapDirectOutputs) {
+ mapOutSpecStr = execCtx->GetOutSpec(0, mapDirectOutputs, mapOutSpec, !reduceUseSkiff, nativeTypeCompat);
+ } else {
+ mapOutSpecStr = NYT::NodeToYsonString(mapOutSpec);
+ }
+
+ auto mapJob = MakeIntrusive<TYqlUserJob>();
+ mapJob->SetInputType(mapInputType);
+ mapJob->SetInputSpec(execCtx->GetInputSpec(!useSkiff || forceYsonInputFormat, nativeTypeCompat, false));
+ mapJob->SetOutSpec(mapOutSpecStr);
+ if (!groups.empty() && groups.back() != 0) {
+ mapJob->SetInputGroups(groups);
+ }
+ mapJob->SetTableNames(tables);
+ mapJob->SetRowOffsets(rowOffsets);
+ mapJob->SetUseSkiff(useSkiff, TMkqlIOSpecs::ESystemField::RowIndex);
+ mapJob->SetYamrInput(execCtx->YamrInput);
+
+ auto reduceJob = MakeIntrusive<TYqlUserJob>();
+ reduceJob->SetInputType(reduceInputType);
+ reduceJob->SetInputSpec(NYT::NodeToYsonString(NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, NYT::TNode::CreateList().Add(intermediateMeta))));
+ reduceJob->SetOutSpec(execCtx->GetOutSpec(mapDirectOutputs, execCtx->OutTables_.size(), {}, !reduceUseSkiff, nativeTypeCompat));
+
+ mapReduceOpSpec.ReduceBy(ToYTSortColumns(reduceBy));
+ if (!sortBy.empty()) {
+ mapReduceOpSpec.SortBy(ToYTSortColumns(sortBy));
+ } else {
+ mapReduceOpSpec.SortBy(ToYTSortColumns(reduceBy));
+ }
+
+ THashSet<TString> auxColumns;
+ std::for_each(reduceBy.begin(), reduceBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ if (!sortBy.empty()) {
+ std::for_each(sortBy.begin(), sortBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ }
+ reduceJob->SetAuxColumns(auxColumns);
+
+ reduceJob->SetUseSkiff(reduceUseSkiff, TMkqlIOSpecs::ESystemField::KeySwitch);
+
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+ {
+ TUserJobSpec mapUserJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ builder.UpdateLambdaCode(mapLambda, nodeCount, transform);
+ mapJob->SetLambdaCode(mapLambda);
+ mapJob->SetOptLLVM(execCtx->Options_.OptLLVM());
+ mapJob->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ transform.ApplyJobProps(*mapJob);
+ transform.ApplyUserJobSpec(mapUserJobSpec, testRun);
+
+ for (auto& f: remapperAllFiles) {
+ mapUserJobSpec.AddFile(TRichYPath(f).Executable(true));
+ }
+
+ FillUserJobSpec(mapUserJobSpec, execCtx, mapExtraUsage, transform.GetUsedMemory(), execCtx->EstimateLLVMMem(nodeCount), testRun,
+ GetQB2PremapperPrefix(remapperMap, useSkiff));
+
+ mapReduceOpSpec.MapperSpec(mapUserJobSpec);
+ }
+
+ {
+ TUserJobSpec reduceUserJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ builder.UpdateLambdaCode(reduceLambda, nodeCount, transform);
+ reduceJob->SetLambdaCode(reduceLambda);
+ reduceJob->SetOptLLVM(execCtx->Options_.OptLLVM());
+ reduceJob->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ transform.ApplyJobProps(*reduceJob);
+ transform.ApplyUserJobSpec(reduceUserJobSpec, testRun);
+ FillUserJobSpec(reduceUserJobSpec, execCtx, reduceExtraUsage, transform.GetUsedMemory(), execCtx->EstimateLLVMMem(nodeCount), testRun);
+ mapReduceOpSpec.ReducerSpec(reduceUserJobSpec);
+ }
+ FillOperationSpec(mapReduceOpSpec, execCtx);
+ auto formats = mapJob->GetIOFormats(execCtx->FunctionRegistry_);
+ if (!intermediateSchema.IsUndefined() && formats.second.Config.AsString() == "skiff") {
+ formats.second.Config.Attributes()["override_intermediate_table_schema"] = intermediateSchema;
+ }
+ mapReduceOpSpec.MapperInputFormat(forceYsonInputFormat ? NYT::TFormat::YsonBinary() : formats.first);
+ mapReduceOpSpec.MapperOutputFormat(formats.second);
+ formats = reduceJob->GetIOFormats(execCtx->FunctionRegistry_);
+ if (!intermediateSchema.IsUndefined() && formats.first.Config.AsString() == "skiff") {
+ formats.first.Config.Attributes()["override_intermediate_table_schema"] = intermediateSchema;
+ }
+ mapReduceOpSpec.ReducerInputFormat(formats.first);
+ mapReduceOpSpec.ReducerOutputFormat(formats.second);
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1U, "Support single input only.");
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Support single output only.");
+
+ const auto itI = TestTables.find(execCtx->InputTables_.front().Path.Path_);
+ YQL_ENSURE(TestTables.cend() != itI);
+ const auto itO = TestTables.find(execCtx->OutTables_.front().Path);
+ YQL_ENSURE(TestTables.cend() != itO);
+
+ TStringInput in(itI->second.second);
+ TStringOutput out(itO->second.second);
+
+ LocalRawMapReduce(mapReduceOpSpec, reduceJob.Get(), &in, &out);
+ DumpLocalTable(itO->second.second, execCtx->Config_->GetLocalChainFile());
+ return MakeFuture();
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, mapExtraUsage.Cpu, reduceExtraUsage.Cpu,
+ EYtOpProp::IntermediateData | EYtOpProp::WithMapper | EYtOpProp::WithReducer | EYtOpProp::WithUserJobs | EYtOpProp::AllowSampling);
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, mapJob, reduceJob, mapReduceOpSpec = std::move(mapReduceOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawMapReduce(mapReduceOpSpec, mapJob, {}, reduceJob, opOpts);
+ });
+ });
+ }
+
+ static TFuture<void> ExecMapReduce(
+ const TVector<std::pair<TString, bool>>& reduceBy,
+ const TVector<std::pair<TString, bool>>& sortBy,
+ const TMaybe<ui64>& limit,
+ const TVector<TString>& sortLimitBy,
+ TString reduceLambda,
+ const TString& reduceInputType,
+ const TExpressionResorceUsage& reduceExtraUsage,
+ const NYT::TNode& intermediateSchema,
+ const TExecContext<TRunOptions>::TPtr& execCtx
+ ) {
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+ TFuture<bool> ret = testRun ? MakeFuture<bool>(false) : execCtx->LookupQueryCacheAsync();
+ return ret.Apply([reduceBy, sortBy, limit, sortLimitBy, reduceLambda, reduceInputType,
+ reduceExtraUsage, intermediateSchema, execCtx, testRun]
+ (const auto& f) mutable
+ {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ TTransactionCache::TEntry::TPtr entry;
+ TVector<TRichYPath> outYPaths;
+ if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Test mode support single output only.");
+ const auto& out = execCtx->OutTables_.front();
+ outYPaths.emplace_back(TRichYPath(out.Path).Schema(RowSpecToYTSchema(TestTables[out.Path].first = out.Spec[YqlRowSpecAttribute], NTCF_NONE)));
+ } else {
+ entry = execCtx->GetEntry();
+ const bool cacheHit = f.GetValue();
+ outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ }
+
+ TRawMapReduceOperationSpec mapReduceOpSpec;
+
+ for (size_t i: xrange(execCtx->OutTables_.size())) {
+ if (limit && sortLimitBy.empty()) {
+ outYPaths[i].RowCountLimit(*limit);
+ }
+ mapReduceOpSpec.AddOutput(outYPaths[i]);
+ }
+
+ TVector<ui32> groups;
+ for (auto& table: execCtx->InputTables_) {
+ mapReduceOpSpec.AddInput(table.Path);
+ groups.push_back(table.Group);
+ }
+
+ auto reduceJob = MakeIntrusive<TYqlUserJob>();
+ reduceJob->SetInputType(reduceInputType);
+ if (!groups.empty() && groups.back() != 0) {
+ reduceJob->SetInputGroups(groups);
+ }
+
+ const bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ reduceJob->SetInputSpec(execCtx->GetInputSpec(!useSkiff, intermediateSchema.IsUndefined() ? 0ul : nativeTypeCompat, true)); // Explicitly disable native types for intermediate data because of YT limitations
+ reduceJob->SetOutSpec(execCtx->GetOutSpec(!useSkiff, nativeTypeCompat));
+
+ mapReduceOpSpec.ReduceBy(ToYTSortColumns(reduceBy));
+ if (!sortBy.empty()) {
+ mapReduceOpSpec.SortBy(ToYTSortColumns(sortBy));
+ } else {
+ mapReduceOpSpec.SortBy(ToYTSortColumns(reduceBy));
+ }
+
+ THashSet<TString> auxColumns;
+ std::for_each(reduceBy.begin(), reduceBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ if (!sortBy.empty()) {
+ std::for_each(sortBy.begin(), sortBy.end(), [&auxColumns](const auto& it) { auxColumns.insert(it.first); });
+ }
+ reduceJob->SetAuxColumns(auxColumns);
+
+ reduceJob->SetUseSkiff(useSkiff, TMkqlIOSpecs::ESystemField::KeySwitch);
+ reduceJob->SetYamrInput(execCtx->YamrInput);
+
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+ {
+ TUserJobSpec reduceUserJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ builder.UpdateLambdaCode(reduceLambda, nodeCount, transform);
+ reduceJob->SetLambdaCode(reduceLambda);
+ reduceJob->SetOptLLVM(execCtx->Options_.OptLLVM());
+ reduceJob->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ transform.ApplyJobProps(*reduceJob);
+ transform.ApplyUserJobSpec(reduceUserJobSpec, testRun);
+ FillUserJobSpec(reduceUserJobSpec, execCtx, reduceExtraUsage, transform.GetUsedMemory(), execCtx->EstimateLLVMMem(nodeCount), testRun);
+ mapReduceOpSpec.ReducerSpec(reduceUserJobSpec);
+ }
+ FillOperationSpec(mapReduceOpSpec, execCtx);
+ auto formats = reduceJob->GetIOFormats(execCtx->FunctionRegistry_);
+ if (!intermediateSchema.IsUndefined() && formats.first.Config.AsString() == "skiff") {
+ formats.first.Config.Attributes()["override_intermediate_table_schema"] = intermediateSchema;
+ }
+ mapReduceOpSpec.ReducerInputFormat(formats.first);
+ mapReduceOpSpec.ReducerOutputFormat(formats.second);
+
+ if (testRun) {
+ YQL_ENSURE(execCtx->InputTables_.size() == 1U, "Support single input only.");
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Support single output only.");
+
+ const auto itI = TestTables.find(execCtx->InputTables_.front().Path.Path_);
+ YQL_ENSURE(TestTables.cend() != itI);
+ const auto itO = TestTables.find(execCtx->OutTables_.front().Path);
+ YQL_ENSURE(TestTables.cend() != itO);
+
+ TStringInput in(itI->second.second);
+ TStringOutput out(itO->second.second);
+
+ LocalRawMapReduce(mapReduceOpSpec, reduceJob.Get(), &in, &out);
+ DumpLocalTable(itO->second.second, execCtx->Config_->GetLocalChainFile());
+ return MakeFuture();
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, 0., reduceExtraUsage.Cpu,
+ EYtOpProp::IntermediateData | EYtOpProp::WithReducer | EYtOpProp::WithUserJobs | EYtOpProp::AllowSampling);
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, reduceJob, mapReduceOpSpec = std::move(mapReduceOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawMapReduce(mapReduceOpSpec, {}, {}, reduceJob, opOpts);
+ });
+ });
+ }
+
+ TFuture<void> DoMapReduce(TYtMapReduce mapReduce, const TExecContext<TRunOptions>::TPtr& execCtx, TExprContext& ctx) {
+ auto reduceBy = NYql::GetSettingAsColumnPairList(mapReduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ auto sortBy = NYql::GetSettingAsColumnPairList(mapReduce.Settings().Ref(), EYtSettingType::SortBy);
+
+ const bool useNativeTypes = execCtx->Options_.Config()->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES);
+ const bool useIntermediateSchema = execCtx->Options_.Config()->UseIntermediateSchema.Get().GetOrElse(DEFAULT_USE_INTERMEDIATE_SCHEMA);
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+
+ NYT::TNode intermediateMeta;
+ NYT::TNode intermediateSchema;
+ TString mapLambda;
+ TExpressionResorceUsage mapExtraUsage;
+ TString mapInputType;
+ size_t mapDirectOutputs = 0;
+ if (!mapReduce.Mapper().Maybe<TCoVoid>()) {
+ const auto mapTypeSet = NYql::GetSetting(mapReduce.Settings().Ref(), EYtSettingType::MapOutputType);
+ auto mapResultItem = mapTypeSet ?
+ mapTypeSet->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType():
+ GetSequenceItemType(mapReduce.Mapper(), true);
+
+ if (mapResultItem->GetKind() == ETypeAnnotationKind::Variant) {
+ auto items = mapResultItem->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>()->GetItems();
+ YQL_ENSURE(!items.empty());
+ mapDirectOutputs = items.size() - 1;
+ mapResultItem = items.front();
+ }
+
+ intermediateMeta = NYT::TNode::CreateMap();
+ intermediateMeta[YqlRowSpecAttribute][RowSpecAttrType] = NCommon::TypeToYsonNode(mapResultItem);
+ if (useIntermediateSchema && useNativeTypes) {
+ intermediateMeta[YqlRowSpecAttribute][RowSpecAttrNativeYtTypeFlags] = (GetNativeYtTypeFlags(*mapResultItem->Cast<TStructExprType>()) & nativeTypeCompat);
+ intermediateSchema = RowSpecToYTSchema(intermediateMeta[YqlRowSpecAttribute], nativeTypeCompat).ToNode();
+ } else {
+ intermediateMeta[YqlRowSpecAttribute][RowSpecAttrNativeYtTypeFlags] = 0ul; // Explicitly disable native types for intermediate data because of YT limitations
+ }
+ if (NYql::HasSetting(mapReduce.Settings().Ref(), EYtSettingType::KeySwitch)) {
+ intermediateMeta[YqlSysColumnPrefix].Add("keyswitch");
+ }
+ mapExtraUsage = execCtx->ScanExtraResourceUsage(mapReduce.Mapper().Cast<TCoLambda>().Body().Ref(), true);
+
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *execCtx->Session_);
+ mapLambda = builder.BuildLambdaWithIO(*MkqlCompiler_, mapReduce.Mapper().Cast<TCoLambda>(), ctx);
+ mapInputType = NCommon::WriteTypeToYson(GetSequenceItemType(mapReduce.Input().Size() == 1U ?
+ TExprBase(mapReduce.Input().Item(0)) : TExprBase(mapReduce.Mapper().Cast<TCoLambda>().Args().Arg(0)), true));
+ } else if (useIntermediateSchema && useNativeTypes) {
+ YQL_ENSURE(mapReduce.Input().Size() == 1);
+ const TTypeAnnotationNode* itemType = GetSequenceItemType(mapReduce.Input().Item(0), false);
+ if (auto flags = GetNativeYtTypeFlags(*itemType->Cast<TStructExprType>())) {
+ auto rowSpec = NYT::TNode::CreateMap();
+ rowSpec[RowSpecAttrType] = NCommon::TypeToYsonNode(itemType);
+ rowSpec[RowSpecAttrNativeYtTypeFlags] = (flags & nativeTypeCompat);
+ intermediateSchema = RowSpecToYTSchema(rowSpec, nativeTypeCompat).ToNode();
+ }
+ }
+ TString reduceLambda;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *execCtx->Session_);
+ reduceLambda = builder.BuildLambdaWithIO(*MkqlCompiler_, mapReduce.Reducer(), ctx);
+ }
+ TExpressionResorceUsage reduceExtraUsage = execCtx->ScanExtraResourceUsage(mapReduce.Reducer().Body().Ref(), false);
+
+ const auto inputTypeSet = NYql::GetSetting(mapReduce.Settings().Ref(), EYtSettingType::ReduceInputType);
+ TString reduceInputType = NCommon::WriteTypeToYson(inputTypeSet ?
+ inputTypeSet->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType():
+ GetSequenceItemType(mapReduce.Reducer().Args().Arg(0), false)
+ );
+
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(mapReduce.Settings().Ref(), EYtSettingType::SortLimitBy);
+ TMaybe<ui64> limit = GetLimit(mapReduce.Settings().Ref());
+ if (limit && !sortLimitBy.empty() && *limit > execCtx->Options_.Config()->TopSortMaxLimit.Get().GetOrElse(DEFAULT_TOP_SORT_LIMIT)) {
+ limit.Clear();
+ }
+
+ return execCtx->Session_->Queue_->Async([reduceBy, sortBy, limit, sortLimitBy, mapLambda, mapInputType, mapDirectOutputs, mapExtraUsage, reduceLambda, reduceInputType, reduceExtraUsage, intermediateMeta, intermediateSchema, execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ if (mapLambda) {
+ return ExecMapReduce(reduceBy, sortBy, limit, sortLimitBy, mapLambda, mapInputType, mapDirectOutputs, mapExtraUsage,
+ reduceLambda, reduceInputType, reduceExtraUsage, intermediateMeta, intermediateSchema, execCtx);
+ } else {
+ return ExecMapReduce(reduceBy, sortBy, limit, sortLimitBy, reduceLambda, reduceInputType, reduceExtraUsage, intermediateSchema, execCtx);
+ }
+ });
+ }
+
+ static TFuture<void> ExecSafeFill(const TVector<TRichYPath>& outYPaths,
+ TRuntimeNode root,
+ const TString& outSpec,
+ const TExecContext<TRunOptions>::TPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ const TNativeYtLambdaBuilder& builder,
+ TScopedAlloc& alloc
+ ) {
+ NYT::TTableWriterOptions writerOptions;
+ auto maxRowWeight = execCtx->Options_.Config()->MaxRowWeight.Get(execCtx->Cluster_);
+ auto maxKeyWeight = execCtx->Options_.Config()->MaxKeyWeight.Get(execCtx->Cluster_);
+ bool hasSecureParams = !execCtx->Options_.SecureParams().empty();
+
+ if (maxRowWeight || maxKeyWeight || hasSecureParams) {
+ NYT::TNode config;
+ if (maxRowWeight) {
+ config["max_row_weight"] = static_cast<i64>(*maxRowWeight);
+ }
+ if (maxKeyWeight) {
+ config["max_key_weight"] = static_cast<i64>(*maxKeyWeight);
+ }
+ if (hasSecureParams) {
+ FillSecureVault(config, execCtx->Options_.SecureParams());
+ }
+ writerOptions.Config(config);
+ }
+
+ NCommon::TCodecContext codecCtx(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TMkqlIOSpecs specs;
+ if (execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF)) {
+ specs.SetUseSkiff(execCtx->Options_.OptLLVM());
+ }
+ specs.Init(codecCtx, outSpec);
+
+ TVector<TRawTableWriterPtr> writers;
+ for (size_t i: xrange(outYPaths.size())) {
+ auto writer = entry->Tx->CreateRawWriter(outYPaths[i], specs.MakeOutputFormat(i), writerOptions);
+ writers.push_back(writer);
+ }
+
+ TMkqlWriterImpl mkqlWriter(writers, 4_MB);
+ mkqlWriter.SetSpecs(specs);
+ mkqlWriter.SetWriteLimit(alloc.GetLimit());
+
+ TExploringNodeVisitor explorer;
+ auto localGraph = builder.BuildLocalGraph(GetGatewayNodeFactory(&mkqlWriter, execCtx->UserFiles_),
+ execCtx->Options_.UdfValidateMode(),
+ NUdf::EValidatePolicy::Exception, "OFF" /* don't use LLVM locally */, EGraphPerProcess::Multi, explorer, root);
+ auto& graph = std::get<0>(localGraph);
+ const TBindTerminator bind(graph->GetTerminator());
+ graph->Prepare();
+ auto value = graph->GetValue();
+
+ if (root.GetStaticType()->IsStream()) {
+ NUdf::TUnboxedValue item;
+ const auto status = value.Fetch(item);
+ YQL_ENSURE(NUdf::EFetchStatus::Finish == status);
+ } else {
+ YQL_ENSURE(value.IsFinish());
+ }
+
+ mkqlWriter.Finish();
+ for (auto& writer: writers) {
+ writer->Finish();
+ }
+
+ return MakeFuture();
+ }
+
+ static TFuture<void> ExecFill(TString lambda,
+ const TExpressionResorceUsage& extraUsage,
+ const TString& tmpTable,
+ const TExecContext<TRunOptions>::TPtr& execCtx)
+ {
+ const bool testRun = execCtx->Config_->GetLocalChainTest();
+ TFuture<bool> ret = testRun ? MakeFuture<bool>(false) : execCtx->LookupQueryCacheAsync();
+ return ret.Apply([lambda, extraUsage, tmpTable, execCtx, testRun] (const auto& f) mutable {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ TTransactionCache::TEntry::TPtr entry;
+ TVector<TRichYPath> outYPaths;
+ if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Test mode support single output only.");
+ const auto& out = execCtx->OutTables_.front();
+ outYPaths.emplace_back(TRichYPath(out.Path).Schema(RowSpecToYTSchema(TestTables[out.Path].first = out.Spec[YqlRowSpecAttribute], NTCF_NONE)));
+ } else {
+ entry = execCtx->GetEntry();
+ bool cacheHit = f.GetValue();
+ outYPaths = PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ if (cacheHit) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ }
+
+ const bool useSkiff = execCtx->Options_.Config()->UseSkiff.Get(execCtx->Cluster_).GetOrElse(DEFAULT_USE_SKIFF);
+
+ TIntrusivePtr<TYqlUserJob> job;
+ TRawMapOperationSpec mapOpSpec;
+
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+
+ bool localRun = !testRun &&
+ (execCtx->Config_->HasExecuteUdfLocallyIfPossible()
+ ? execCtx->Config_->GetExecuteUdfLocallyIfPossible() : false);
+ {
+ TUserJobSpec userJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_);
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ TRuntimeNode root = builder.UpdateLambdaCode(lambda, nodeCount, transform);
+ if (transform.CanExecuteInternally() && !testRun) {
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ return ExecSafeFill(outYPaths, root, execCtx->GetOutSpec(!useSkiff, nativeTypeCompat),
+ execCtx, entry, builder, alloc);
+ }
+
+ localRun = localRun && transform.CanExecuteLocally();
+
+ job = MakeIntrusive<TYqlUserJob>();
+ transform.ApplyJobProps(*job);
+ transform.ApplyUserJobSpec(userJobSpec, localRun || testRun);
+
+ FillUserJobSpec(userJobSpec, execCtx, extraUsage, transform.GetUsedMemory(),
+ execCtx->EstimateLLVMMem(nodeCount), localRun || testRun);
+ mapOpSpec.MapperSpec(userJobSpec);
+ }
+
+ job->SetLambdaCode(lambda);
+ job->SetOptLLVM(execCtx->Options_.OptLLVM());
+ job->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(execCtx->Cluster_).GetOrElse(NTCF_LEGACY);
+ job->SetOutSpec(execCtx->GetOutSpec(!useSkiff, nativeTypeCompat));
+ job->SetUseSkiff(useSkiff, 0);
+
+ mapOpSpec.AddInput(tmpTable);
+
+ for (size_t i: xrange(execCtx->OutTables_.size())) {
+ mapOpSpec.AddOutput(outYPaths[i]);
+ }
+
+ FillOperationSpec(mapOpSpec, execCtx);
+ const auto formats = job->GetIOFormats(execCtx->FunctionRegistry_);
+ mapOpSpec.InputFormat(formats.first);
+ mapOpSpec.OutputFormat(formats.second);
+
+ if (localRun && mapOpSpec.MapperSpec_.Files_.empty() && execCtx->OutTables_.size() == 1U) {
+ return LocalFillJob(mapOpSpec, job.Get(), entry);
+ } else if (testRun) {
+ YQL_ENSURE(execCtx->OutTables_.size() == 1U, "Support single output only.");
+
+ const TString dummy(NYT::NodeListToYsonString({NYT::TNode()("input", "dummy")}));
+ TStringInput in(dummy);
+
+ const auto itO = TestTables.find(execCtx->OutTables_.front().Path);
+ YQL_ENSURE(TestTables.cend() != itO);
+
+ TStringOutput out(itO->second.second);
+
+ LocalRawMapReduce(mapOpSpec, job.Get(), &in, &out);
+ DumpLocalTable(itO->second.second, execCtx->Config_->GetLocalChainFile());
+ return MakeFuture();
+ } else {
+ PrepareTempDestination(tmpTable, execCtx, entry, entry->Tx);
+ auto writer = entry->Tx->CreateTableWriter<NYT::TNode>(tmpTable);
+ writer->AddRow(NYT::TNode()("input", "dummy"));
+ writer->Finish();
+ }
+
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, extraUsage.Cpu, Nothing(),
+ EYtOpProp::TemporaryAutoMerge | EYtOpProp::WithMapper | EYtOpProp::WithUserJobs);
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, job, mapOpSpec = std::move(mapOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawMap(mapOpSpec, job, opOpts);
+ })
+ .Apply([tmpTable, entry](const TFuture<void>& f){
+ f.GetValue();
+ entry->RemoveInternal(tmpTable);
+ });
+ });
+ }
+
+ TFuture<void> DoFill(TYtFill fill, const TExecContext<TRunOptions>::TPtr& execCtx, TExprContext& ctx) {
+ TString lambda;
+ {
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ Services_.FunctionRegistry->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ TNativeYtLambdaBuilder builder(alloc, Services_, *execCtx->Session_);
+ lambda = builder.BuildLambdaWithIO(*MkqlCompiler_, fill.Content(), ctx);
+ }
+ auto extraUsage = execCtx->ScanExtraResourceUsage(fill.Content().Ref(), false);
+
+ TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ TString tmpTablePath = NYql::TransformPath(tmpFolder,
+ TStringBuilder() << "tmp/" << GetGuidAsString(execCtx->Session_->RandomProvider_->GenGuid()), true, execCtx->Session_->UserName_);
+
+ return execCtx->Session_->Queue_->Async([lambda, tmpTablePath, extraUsage, execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ execCtx->MakeUserFiles();
+ return ExecFill(lambda, extraUsage, tmpTablePath, execCtx);
+ });
+ }
+
+ TFuture<void> DoTouch(TYtOutputOpBase touch, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ Y_UNUSED(touch);
+ return execCtx->Session_->Queue_->Async([execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ PrepareDestinations(execCtx->OutTables_, execCtx, entry, true);
+ });
+ }
+
+ TFuture<bool> DoPrepare(TYtOutputOpBase write, const TExecContext<TPrepareOptions>::TPtr& execCtx) const {
+ Y_UNUSED(write);
+ return execCtx->Session_->Queue_->Async([execCtx]() {
+ return execCtx->LookupQueryCacheAsync().Apply([execCtx](const auto& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ bool cacheHit = f.GetValue();
+ PrepareDestinations(execCtx->OutTables_, execCtx, entry, !cacheHit);
+ execCtx->QueryCacheItem.Destroy();
+ return cacheHit;
+ });
+ });
+ }
+
+ TFuture<void> DoDrop(TYtDropTable drop, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ auto table = drop.Table();
+ bool isAnonymous = NYql::HasSetting(table.Settings().Ref(), EYtSettingType::Anonymous);
+ TString path = NYql::TransformPath(tmpFolder, table.Name().Value(), isAnonymous, execCtx->Session_->UserName_);
+ YQL_CLOG(INFO, ProviderYt) << "Dropping: " << execCtx->Cluster_ << '.' << path;
+
+ return execCtx->Session_->Queue_->Async([path, execCtx]() {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto entry = execCtx->GetEntry();
+ entry->Tx->Remove(path, TRemoveOptions().Force(true));
+ });
+ }
+
+ TFuture<void> DoStatOut(TYtStatOut statOut, const TExecContext<TRunOptions>::TPtr& execCtx) {
+ auto input = statOut.Input();
+
+ TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+
+ TString ytTable = TString{GetOutTable(input).Cast<TYtOutTable>().Name().Value()};
+ ytTable = NYql::TransformPath(tmpFolder, ytTable, true, execCtx->Session_->UserName_);
+ ytTable = NYT::AddPathPrefix(ytTable, NYT::TConfig::Get()->Prefix);
+
+ TVector<TString> replaceMask;
+ for (const auto& item: statOut.ReplaceMask().Ptr()->Children()) {
+ replaceMask.push_back(TString{TCoAtom(item).Value()});
+ }
+
+ auto statUploadOptions = TStatUploadOptions {
+ TString{YtProviderName},
+ execCtx->Options_.SessionId(),
+ *execCtx->Options_.PublicId(),
+ TString{statOut.Table().Cluster().Value()},
+ TString{statOut.Table().Name().Value()},
+ TString{statOut.Table().Scale().Value()},
+ std::move(replaceMask),
+ execCtx->YtServer_,
+ std::move(ytTable),
+ GetGuidAsString(execCtx->GetEntry()->Tx->GetId()),
+ execCtx->GetAuth(),
+ NNative::GetPool(*execCtx, execCtx->Options_.Config())
+ };
+ execCtx->SetNodeExecProgress("Running");
+ return StatUploader_->Upload(std::move(statUploadOptions));
+ }
+
+ static ui64 CalcDataSize(const NYT::TRichYPath& ytPath, const NYT::TNode& attrs) {
+ ui64 res = attrs["uncompressed_data_size"].IntCast<ui64>();
+ const auto records = attrs["chunk_row_count"].IntCast<ui64>();
+ if (auto usedRows = GetUsedRows(ytPath, records)) {
+ res *= double(*usedRows) / double(records);
+ }
+ return res;
+ }
+
+ static bool AllPathColumnsAreInSchema(const NYT::TRichYPath& ytPath, const NYT::TNode& attrs) {
+ YQL_ENSURE(ytPath.Columns_.Defined());
+
+ if (!attrs.HasKey("schema")) {
+ YQL_CLOG(INFO, ProviderYt) << "Missing YT schema for " << ytPath.Path_;
+ return false;
+ }
+
+ TSet<TString> columns(ytPath.Columns_->Parts_.begin(), ytPath.Columns_->Parts_.end());
+
+ for (const auto& schemaColumn : attrs["schema"].AsList()) {
+ auto it = columns.find(schemaColumn["name"].AsString());
+ if (it != columns.end()) {
+ columns.erase(it);
+ }
+ if (columns.empty()) {
+ break;
+ }
+ }
+
+ if (!columns.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "Columns {" << JoinSeq(", ", columns) << "} are missing in YT schema for table "
+ << ytPath.Path_ << ", assuming uncompressed data size";
+ }
+
+ return columns.empty();
+ }
+
+ static TPathStatResult ExecPathStat(const TExecContext<TPathStatOptions>::TPtr& execCtx, bool onlyCached) {
+ try {
+ TPathStatResult res;
+ res.DataSize.resize(execCtx->Options_.Paths().size(), 0);
+
+ auto entry = execCtx->GetOrCreateEntry();
+ auto tx = entry->Tx;
+ const TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ const NYT::EOptimizeForAttr tmpOptimizeFor = execCtx->Options_.Config()->OptimizeFor.Get(execCtx->Cluster_).GetOrElse(NYT::EOptimizeForAttr::OF_LOOKUP_ATTR);
+ TVector<NYT::TRichYPath> ytPaths(Reserve(execCtx->Options_.Paths().size()));
+ TVector<size_t> pathMap;
+
+ auto extractSysColumns = [] (NYT::TRichYPath& ytPath) -> TVector<TString> {
+ TVector<TString> res;
+ if (ytPath.Columns_) {
+ auto it = std::remove_if(
+ ytPath.Columns_->Parts_.begin(),
+ ytPath.Columns_->Parts_.end(),
+ [] (const TString& col) { return col.StartsWith(YqlSysColumnPrefix); }
+ );
+ res.assign(it, ytPath.Columns_->Parts_.end());
+ ytPath.Columns_->Parts_.erase(it, ytPath.Columns_->Parts_.end());
+ }
+ return res;
+ };
+
+ for (size_t i: xrange(execCtx->Options_.Paths().size())) {
+ auto& req = execCtx->Options_.Paths()[i];
+ NYT::TRichYPath ytPath = req.Path();
+ auto tablePath = NYql::TransformPath(tmpFolder, ytPath.Path_, req.IsTemp(), execCtx->Session_->UserName_);
+ if (req.IsTemp() && !req.IsAnonymous()) {
+ ytPath.Path_ = NYT::AddPathPrefix(tablePath, NYT::TConfig::Get()->Prefix);
+ NYT::TNode attrs;
+ if (auto sysColumns = extractSysColumns(ytPath)) {
+ attrs = tx->Get(ytPath.Path_ + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("chunk_row_count"))
+ ));
+ auto records = attrs["chunk_row_count"].IntCast<ui64>();
+ records = GetUsedRows(ytPath, records).GetOrElse(records);
+ for (auto col: sysColumns) {
+ auto size = 0;
+ if (col == YqlSysColumnNum || col == YqlSysColumnRecord) {
+ size = sizeof(ui64);
+ } else if (col == YqlSysColumnIndex) {
+ size = sizeof(ui32);
+ }
+ // zero size for YqlSysColumnPath for temp tables
+ size *= records;
+ res.DataSize[i] += size;
+ YQL_CLOG(INFO, ProviderYt) << "Adding stat for " << col << ": " << size << " (virtual)";
+ }
+ }
+ if (auto val = entry->GetColumnarStat(ytPath)) {
+ res.DataSize[i] += *val;
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << ": " << res.DataSize[i] << " (from cache)";
+ } else if (onlyCached) {
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << " is missing in cache - sync path stat failed";
+ return res;
+ } else if (NYT::EOptimizeForAttr::OF_SCAN_ATTR == tmpOptimizeFor) {
+ pathMap.push_back(i);
+ ytPaths.push_back(ytPath);
+ } else {
+ // Use entire table size for lookup tables (YQL-7257)
+ if (attrs.IsUndefined()) {
+ attrs = tx->Get(ytPath.Path_ + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("chunk_row_count"))
+ ));
+ }
+ auto size = CalcDataSize(ytPath, attrs);
+ res.DataSize[i] += size;
+ entry->UpdateColumnarStat(ytPath, size);
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << ": " << res.DataSize[i] << " (uncompressed_data_size for lookup)";
+ }
+ } else {
+ auto p = entry->Snapshots.FindPtr(std::make_pair(tablePath, req.Epoch()));
+ YQL_ENSURE(p, "Table " << tablePath << " (epoch=" << req.Epoch() << ") has no snapshot");
+ ytPath.Path(std::get<0>(*p)).TransactionId(std::get<1>(*p));
+ NYT::TNode attrs;
+ if (auto sysColumns = extractSysColumns(ytPath)) {
+ attrs = entry->Client->AttachTransaction(std::get<1>(*p))->Get(std::get<0>(*p) + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("optimize_for"))
+ .AddAttribute(TString("chunk_row_count"))
+ .AddAttribute(TString("schema"))
+ ));
+ auto records = attrs["chunk_row_count"].IntCast<ui64>();
+ records = GetUsedRows(ytPath, records).GetOrElse(records);
+ for (auto col: sysColumns) {
+ auto size = 0;
+ if (col == YqlSysColumnNum || col == YqlSysColumnRecord) {
+ size = sizeof(ui64);
+ } else if (col == YqlSysColumnIndex) {
+ size = sizeof(ui32);
+ } else if (col == YqlSysColumnPath && !req.IsTemp()) {
+ size = tablePath.size();
+ }
+ size *= records;
+ res.DataSize[i] += size;
+ YQL_CLOG(INFO, ProviderYt) << "Adding stat for " << col << ": " << size << " (virtual)";
+ }
+ }
+ if (auto val = entry->GetColumnarStat(ytPath)) {
+ res.DataSize[i] += *val;
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << " (epoch=" << req.Epoch() << "): " << res.DataSize[i] << " (from cache)";
+ } else if (onlyCached) {
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << " (epoch=" << req.Epoch() << ") is missing in cache - sync path stat failed";
+ return res;
+ } else {
+ if (attrs.IsUndefined()) {
+ attrs = entry->Client->AttachTransaction(std::get<1>(*p))->Get(std::get<0>(*p) + "/@", NYT::TGetOptions().AttributeFilter(
+ NYT::TAttributeFilter()
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("optimize_for"))
+ .AddAttribute(TString("chunk_row_count"))
+ .AddAttribute(TString("schema"))
+ ));
+ }
+ if (attrs.HasKey("optimize_for") && attrs["optimize_for"] == "scan" &&
+ AllPathColumnsAreInSchema(req.Path(), attrs))
+ {
+ pathMap.push_back(i);
+ ytPaths.push_back(ytPath);
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << " (epoch=" << req.Epoch() << ") add for request with path " << ytPath.Path_;
+ } else {
+ // Use entire table size for lookup tables (YQL-7257)
+ auto size = CalcDataSize(ytPath, attrs);
+ res.DataSize[i] += size;
+ entry->UpdateColumnarStat(ytPath, size);
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << req.Path().Path_ << " (epoch=" << req.Epoch() << "): " << res.DataSize[i] << " (uncompressed_data_size for lookup)";
+ }
+ }
+ }
+ }
+
+ if (ytPaths) {
+ YQL_ENSURE(!onlyCached);
+ auto fetchMode = execCtx->Options_.Config()->JoinColumnarStatisticsFetcherMode.Get().GetOrElse(NYT::EColumnarStatisticsFetcherMode::Fallback);
+ auto columnStats = tx->GetTableColumnarStatistics(ytPaths, NYT::TGetTableColumnarStatisticsOptions().FetcherMode(fetchMode));
+ YQL_ENSURE(pathMap.size() == columnStats.size());
+ for (size_t i: xrange(columnStats.size())) {
+ auto& columnStat = columnStats[i];
+ const ui64 weight = columnStat.LegacyChunksDataWeight +
+ Accumulate(columnStat.ColumnDataWeight.begin(), columnStat.ColumnDataWeight.end(), 0ull,
+ [](ui64 sum, decltype(*columnStat.ColumnDataWeight.begin())& v) { return sum + v.second; });
+
+ res.DataSize[pathMap[i]] += weight;
+ entry->UpdateColumnarStat(ytPaths[i], columnStat);
+ YQL_CLOG(INFO, ProviderYt) << "Stat for " << execCtx->Options_.Paths()[pathMap[i]].Path().Path_ << ": " << weight << " (fetched)";
+ }
+ }
+
+ res.SetSuccess();
+ return res;
+ } catch (...) {
+ return ResultFromCurrentException<TPathStatResult>();
+ }
+ }
+
+ static TRunResult MakeRunResult(const TVector<TOutputInfo>& outTables, const TTransactionCache::TEntry::TPtr& entry) {
+ TRunResult res;
+ res.SetSuccess();
+
+ if (outTables.empty()) {
+ return res;
+ }
+
+ auto batchGet = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<TYtTableStatInfo::TPtr>> batchRes(Reserve(outTables.size()));
+ for (auto& out: outTables) {
+ batchRes.push_back(
+ batchGet->Get(out.Path + "/@", TGetOptions()
+ .AttributeFilter(TAttributeFilter()
+ .AddAttribute(TString("id"))
+ .AddAttribute(TString("dynamic"))
+ .AddAttribute(TString("row_count"))
+ .AddAttribute(TString("chunk_row_count"))
+ .AddAttribute(TString("uncompressed_data_size"))
+ .AddAttribute(TString("data_weight"))
+ .AddAttribute(TString("chunk_count"))
+ .AddAttribute(TString("modification_time"))
+ .AddAttribute(TString("schema"))
+ .AddAttribute(TString("revision"))
+ .AddAttribute(TString("content_revision"))
+ )
+ ).Apply([out](const TFuture<NYT::TNode>& f) {
+
+ auto attrs = f.GetValue();
+
+ TString expectedSortedBy = ToColumnList(out.SortedBy.Parts_);
+ TString realSortedBy = TString("[]");
+ if (attrs.HasKey("schema")) {
+ auto keyColumns = KeyColumnsFromSchema(attrs["schema"]);
+ realSortedBy = ToColumnList(keyColumns.Keys);
+ }
+ YQL_ENSURE(expectedSortedBy == realSortedBy, "Output table " << out.Path
+ << " has unexpected \"sorted_by\" value. Expected: " << expectedSortedBy
+ << ", actual: " << realSortedBy);
+
+ auto statInfo = MakeIntrusive<TYtTableStatInfo>();
+ statInfo->Id = attrs["id"].AsString();
+ statInfo->RecordsCount = GetTableRowCount(attrs);
+ statInfo->DataSize = GetDataWeight(attrs).GetOrElse(0);
+ statInfo->ChunkCount = attrs["chunk_count"].AsInt64();
+ TString strModifyTime = attrs["modification_time"].AsString();
+ statInfo->ModifyTime = TInstant::ParseIso8601(strModifyTime).Seconds();
+ statInfo->TableRevision = attrs["revision"].IntCast<ui64>();
+ statInfo->Revision = GetContentRevision(attrs);
+ return statInfo;
+ })
+ );
+ }
+
+ batchGet->ExecuteBatch();
+
+ for (size_t i: xrange(outTables.size())) {
+ res.OutTableStats.emplace_back(outTables[i].Name, batchRes[i].GetValue());
+ }
+
+ return res;
+ }
+
+ template <class TSpec>
+ static void LocalRawMapReduce(const TSpec& spec, IRawJob* mapper, IInputStream* in, IOutputStream* out)
+ {
+ YQL_ENSURE(GetRemoteFiles(spec).empty(), "Unexpected remote files in spec");
+ const TTempDir tmp;
+ for (const auto& f : GetLocalFiles(spec)) {
+ TFsPath src(std::get<0U>(f));
+ if (src.IsRelative()) {
+ src = (TFsPath::Cwd() / src).Fix();
+ }
+ const TFsPath dst(tmp.Path().Child(src.GetName()));
+ YQL_ENSURE(NFs::SymLink(src, dst), "Can't make symlink " << dst << " on " << src);
+ }
+
+ struct TJobBinaryPathVisitor {
+ TFsPath operator()(const TJobBinaryDefault&) const {
+ return GetPersistentExecPath();
+ }
+ TFsPath operator()(const TJobBinaryLocalPath& item) const {
+ return item.Path;
+ }
+ TFsPath operator()(const TJobBinaryCypressPath&) const {
+ ythrow yexception() << "LocalRawMap: unexpected TJobBinaryCypressPath";
+ }
+ };
+ TFsPath src = std::visit(TJobBinaryPathVisitor(), GetJobBinary(spec));
+
+ if (src.IsRelative()) {
+ src = (TFsPath::Cwd() / src).Fix();
+ }
+ const TFsPath dst(tmp.Path().Child(src.GetName()));
+ YQL_ENSURE(NFs::SymLink(src, dst), "Can't make symlink " << dst << " on " << src);
+
+ TString jobstate;
+ TStringOutput job(jobstate);
+ mapper->Save(job);
+ job.Finish();
+
+ if (!jobstate.empty()) {
+ TFile(tmp.Path().Child("jobstate"), CreateNew | WrOnly).Write(jobstate.data(), jobstate.size());
+ }
+
+ TShellCommandOptions opts;
+ opts.SetUseShell(false).SetDetachSession(false).SetInputStream(in).SetOutputStream(out);
+
+ opts.Environment.emplace("YQL_SUPPRESS_JOB_STATISTIC", '1');
+ opts.Environment.emplace("YT_JOB_ID", '0');
+ opts.Environment.emplace("YT_USE_CLIENT_PROTOBUF", TConfig::Get()->UseClientProtobuf ? '1' : '0');
+
+ TList<TString> args;
+ args.emplace_back("--yt-map");
+ args.emplace_back(TJobFactory::Get()->GetJobName(mapper));
+ args.emplace_back(ToString(spec.GetOutputs().size()));
+ args.emplace_back(jobstate.empty() ? '0' : '1');
+
+ TShellCommand command(dst.GetPath(), args, opts, tmp.Path());
+ switch (const auto status = command.Run().GetStatus()) {
+ case TShellCommand::SHELL_FINISHED: break;
+ case TShellCommand::SHELL_ERROR: YQL_LOG_CTX_THROW yexception() << command.GetError();
+ case TShellCommand::SHELL_INTERNAL_ERROR: YQL_LOG_CTX_THROW yexception() << command.GetInternalError();
+ default: YQL_LOG_CTX_THROW yexception() << "Unexpected run status: " << int(status);
+ }
+ out->Finish();
+
+ YQL_CLOG(INFO, ProviderYt) << command.GetError();
+
+ YQL_ENSURE(command.GetExitCode() == 0, "Job returns: " << command.GetExitCode());
+ }
+
+ template <class TResultFactory>
+ static TFuture<typename TResultFactory::TResult>
+ LocalCalcJob(const TRawMapOperationSpec& spec, IRawJob* mapper, const TString& lambda, TResultFactory&& factory)
+ {
+ const auto& yson = NYT::NodeListToYsonString({NYT::TNode()("input", lambda)});
+ TStringInput in(yson);
+ TStringStream out;
+
+ LocalRawMapReduce(spec, mapper, &in, &out);
+
+ const auto& builder = factory();
+ for (const auto& reader = NYT::CreateTableReader<NYT::TNode>(&out, NYT::TTableReaderOptions()); reader->IsValid(); reader->Next()) {
+ auto& row = reader->GetRow();
+ if (!builder->WriteNext(row["output"])) {
+ break;
+ }
+ }
+ return MakeFuture(builder->Make());
+ }
+
+ static TFuture<void>
+ LocalFillJob(const TRawMapOperationSpec& spec, IRawJob* mapper, TTransactionCache::TEntry::TPtr entry)
+ {
+ const auto& yson = NYT::NodeListToYsonString({NYT::TNode()("input", "dummy")});
+ TStringInput in(yson);
+
+ const auto writer = entry->Tx->CreateRawWriter(spec.GetOutputs().front(), *spec.OutputFormat_);
+
+ LocalRawMapReduce(spec, mapper, &in, writer.Get());
+ return MakeFuture();
+ }
+
+ template <class TExecParamsPtr, class TResultFactory>
+ static TFuture<typename TResultFactory::TResult> ExecCalc(
+ TString lambda,
+ const TExpressionResorceUsage& extraUsage,
+ const TString& tmpTable,
+ const TExecParamsPtr& execCtx,
+ TTransactionCache::TEntry::TPtr entry,
+ TResultFactory&& factory,
+ const TVector<TString>* columns = nullptr
+ )
+ {
+ TRawMapOperationSpec mapOpSpec;
+ mapOpSpec.Format(TFormat::YsonBinary());
+ TIntrusivePtr<TYqlCalcJob> job;
+ auto tmpFiles = std::make_shared<TTempFiles>(execCtx->FileStorage_->GetTemp());
+
+ bool localRun = execCtx->Config_->HasExecuteUdfLocallyIfPossible() ? execCtx->Config_->GetExecuteUdfLocallyIfPossible() : false;
+ {
+ TUserJobSpec userJobSpec;
+ TScopedAlloc alloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ execCtx->FunctionRegistry_->SupportsSizedAllocators());
+ alloc.SetLimit(execCtx->Options_.Config()->DefaultCalcMemoryLimit.Get().GetOrElse(0));
+ auto secureParamsProvider = MakeSimpleSecureParamsProvider(execCtx->Options_.SecureParams());
+ TNativeYtLambdaBuilder builder(alloc, execCtx->FunctionRegistry_, *execCtx->Session_, secureParamsProvider.get());
+ TProgramBuilder pgmBuilder(builder.GetTypeEnvironment(), *execCtx->FunctionRegistry_);
+ TGatewayTransformer transform(execCtx, entry, pgmBuilder, *tmpFiles);
+ size_t nodeCount = 0;
+ TRuntimeNode root = builder.UpdateLambdaCode(lambda, nodeCount, transform);
+ if (transform.CanExecuteInternally()) {
+ TExploringNodeVisitor explorer;
+ auto localGraph = builder.BuildLocalGraph(GetGatewayNodeFactory(nullptr, execCtx->UserFiles_),
+ execCtx->Options_.UdfValidateMode(), NUdf::EValidatePolicy::Exception,
+ "OFF" /* don't use LLVM locally */, EGraphPerProcess::Multi, explorer, root);
+ auto& graph = std::get<0>(localGraph);
+ const TBindTerminator bind(graph->GetTerminator());
+ graph->Prepare();
+ auto value = graph->GetValue();
+ auto builder = factory();
+ builder->WriteValue(value, root.GetStaticType());
+ return MakeFuture(builder->Make());
+ }
+ localRun = localRun && transform.CanExecuteLocally();
+ {
+ TStringInput in(lambda);
+ TStringStream out;
+ TBrotliCompress compressor(&out, 8);
+ TransferData(&in, &compressor);
+ compressor.Finish();
+ lambda = out.Str();
+ }
+ job = MakeIntrusive<TYqlCalcJob>();
+ transform.ApplyJobProps(*job);
+ transform.ApplyUserJobSpec(userJobSpec, localRun);
+ FillUserJobSpec(userJobSpec, execCtx, extraUsage, transform.GetUsedMemory(), execCtx->EstimateLLVMMem(nodeCount), localRun);
+ mapOpSpec.MapperSpec(userJobSpec);
+ }
+
+ if (columns) {
+ job->SetColumns(*columns);
+ }
+ job->SetUseResultYson(factory.UseResultYson());
+ job->SetOptLLVM(execCtx->Options_.OptLLVM());
+ job->SetUdfValidateMode(execCtx->Options_.UdfValidateMode());
+
+ mapOpSpec.AddInput(tmpTable);
+ mapOpSpec.AddOutput(tmpTable);
+ FillOperationSpec(mapOpSpec, execCtx);
+
+ if (localRun && mapOpSpec.MapperSpec_.Files_.empty()) {
+ return LocalCalcJob(mapOpSpec, job.Get(), lambda, std::move(factory));
+ }
+
+ if (!entry) {
+ entry = execCtx->GetOrCreateEntry();
+ }
+
+ const TString tmpFolder = execCtx->Options_.Config()->TablesTmpFolder.Get().GetOrElse(TString());
+ execCtx->SetCacheItem({tmpTable}, {NYT::TNode::CreateMap()}, tmpFolder);
+
+ TFuture<bool> future = execCtx->Config_->GetLocalChainTest()
+ ? MakeFuture<bool>(false)
+ : execCtx->LookupQueryCacheAsync();
+ return future
+ .Apply([execCtx, entry, mapOpSpec = std::move(mapOpSpec), job, tmpTable, lambda, extraUsage, tmpFiles] (const TFuture<bool>& f) {
+ if (f.GetValue()) {
+ execCtx->QueryCacheItem.Destroy();
+ return MakeFuture();
+ }
+ NYT::TNode spec = execCtx->Session_->CreateSpecWithDesc(execCtx->CodeSnippets_);
+ FillSpec(spec, *execCtx, entry, extraUsage.Cpu, Nothing(), EYtOpProp::WithMapper);
+
+ PrepareTempDestination(tmpTable, execCtx, entry, entry->Tx);
+
+ {
+ auto writer = entry->Tx->CreateTableWriter<NYT::TNode>(tmpTable, NYT::TTableWriterOptions().Config(NYT::TNode()("max_row_weight", 128_MB)));
+ writer->AddRow(NYT::TNode()("input", lambda));
+ writer->Finish();
+ }
+
+ TOperationOptions opOpts;
+ FillOperationOptions(opOpts, execCtx, entry);
+ opOpts.StartOperationMode(TOperationOptions::EStartOperationMode::AsyncPrepare).Spec(spec);
+
+ return execCtx->RunOperation([entry, execCtx, job, mapOpSpec = std::move(mapOpSpec), opOpts = std::move(opOpts), tmpFiles]() {
+ execCtx->SetNodeExecProgress("Uploading artifacts");
+ return entry->Tx->RawMap(mapOpSpec, job, opOpts);
+ });
+ })
+ .Apply([tmpTable, entry, factory = std::move(factory)](const auto& f) {
+ f.GetValue();
+ auto builder = factory();
+ auto reader = entry->Tx->CreateTableReader<NYT::TNode>(tmpTable);
+ for (; reader->IsValid(); reader->Next()) {
+ auto& row = reader->GetRow();
+ if (!builder->WriteNext(row["output"])) {
+ break;
+ }
+ }
+ return builder->Make();
+ })
+ .Apply([tmpTable, execCtx, entry](const TFuture<typename TResultFactory::TResult>& f) {
+ YQL_LOG_CTX_ROOT_SESSION_SCOPE(execCtx->LogCtx_);
+ auto res = f.GetValue(); // rethrow error if any
+ execCtx->StoreQueryCache();
+ entry->RemoveInternal(tmpTable);
+ return res;
+ });
+ }
+
+ template <class TExecParamsPtr>
+ static void PrepareCommonAttributes(
+ NYT::TNode& attrs,
+ const TExecParamsPtr& execCtx,
+ const TString& cluster,
+ bool createTable)
+ {
+ if (auto compressionCodec = execCtx->Options_.Config()->TemporaryCompressionCodec.Get(cluster)) {
+ attrs["compression_codec"] = *compressionCodec;
+ }
+ if (auto erasureCodec = execCtx->Options_.Config()->TemporaryErasureCodec.Get(cluster)) {
+ attrs["erasure_codec"] = ToString(*erasureCodec);
+ }
+ if (auto optimizeFor = execCtx->Options_.Config()->OptimizeFor.Get(cluster)) {
+ attrs["optimize_for"] = ToString(*optimizeFor);
+ }
+ if (auto ttl = execCtx->Options_.Config()->TempTablesTtl.Get().GetOrElse(TDuration::Zero())) {
+ attrs["expiration_timeout"] = ttl.MilliSeconds();
+ }
+
+ if (createTable) {
+ if (auto replicationFactor = execCtx->Options_.Config()->TemporaryReplicationFactor.Get(cluster)) {
+ attrs["replication_factor"] = static_cast<i64>(*replicationFactor);
+ }
+ if (auto media = execCtx->Options_.Config()->TemporaryMedia.Get(cluster)) {
+ attrs["media"] = *media;
+ }
+ if (auto primaryMedium = execCtx->Options_.Config()->TemporaryPrimaryMedium.Get(cluster)) {
+ attrs["primary_medium"] = *primaryMedium;
+ }
+ }
+ }
+
+ template <class TExecParamsPtr>
+ static void PrepareAttributes(
+ NYT::TNode& attrs,
+ const TOutputInfo& out,
+ const TExecParamsPtr& execCtx,
+ const TString& cluster,
+ bool createTable)
+ {
+ PrepareCommonAttributes<TExecParamsPtr>(attrs, execCtx, cluster, createTable);
+
+ NYT::MergeNodes(attrs, out.AttrSpec);
+
+ if (createTable) {
+ const auto nativeTypeCompat = execCtx->Options_.Config()->NativeYtTypeCompatibility.Get(cluster).GetOrElse(NTCF_LEGACY);
+ attrs["schema"] = RowSpecToYTSchema(out.Spec[YqlRowSpecAttribute], nativeTypeCompat).ToNode();
+ }
+ }
+
+ template <class TExecParamsPtr>
+ static TVector<TRichYPath> PrepareDestinations(
+ const TVector<TOutputInfo>& outTables,
+ const TExecParamsPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ bool createTables)
+ {
+ auto cluster = execCtx->Cluster_;
+
+ TVector<TRichYPath> res;
+ for (auto& out: outTables) {
+ res.push_back(TRichYPath(out.Path));
+ entry->DeleteAtFinalize(out.Path);
+ }
+
+ if (createTables) {
+ TVector<TString> outPaths;
+ auto batchCreate = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<TLockId>> batchCreateRes;
+
+ for (auto& out: outTables) {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+
+ PrepareAttributes(attrs, out, execCtx, cluster, true);
+
+ YQL_CLOG(INFO, ProviderYt) << "Create tmp table " << out.Path << ", attrs: " << NYT::NodeToYsonString(attrs);
+
+ // Force table recreation, because some tables may exist after query cache lookup
+ batchCreateRes.push_back(batchCreate->Create(out.Path, NT_TABLE, TCreateOptions().Force(true).Attributes(attrs)));
+ outPaths.push_back(out.Path);
+ }
+ entry->CreateDefaultTmpFolder();
+ CreateParents(outPaths, entry->CacheTx);
+
+ batchCreate->ExecuteBatch();
+ WaitExceptionOrAll(batchCreateRes).GetValue();
+ }
+ else {
+ // set attributes in transactions
+ const auto multiSet = execCtx->Options_.Config()->_UseMultisetAttributes.Get().GetOrElse(DEFAULT_USE_MULTISET_ATTRS);
+ if (multiSet) {
+ for (auto& out: outTables) {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ PrepareAttributes(attrs, out, execCtx, cluster, false);
+ YQL_CLOG(INFO, ProviderYt) << "Update tmp table " << out.Path << ", attrs: " << NYT::NodeToYsonString(attrs);
+ entry->Tx->MultisetAttributes(out.Path + "/@", attrs.AsMap(), NYT::TMultisetAttributesOptions());
+ }
+ } else {
+ auto batchSet = entry->Tx->CreateBatchRequest();
+ TVector<TFuture<void>> batchSetRes;
+
+ for (auto& out: outTables) {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+
+ PrepareAttributes(attrs, out, execCtx, cluster, false);
+ YQL_CLOG(INFO, ProviderYt) << "Update tmp table " << out.Path << ", attrs: " << NYT::NodeToYsonString(attrs);
+ for (auto& attr: attrs.AsMap()) {
+ batchSetRes.push_back(batchSet->Set(TStringBuilder() << out.Path << "/@" << attr.first, attr.second));
+ }
+ }
+
+ batchSet->ExecuteBatch();
+ WaitExceptionOrAll(batchSetRes).GetValue();
+ }
+ }
+
+ return res;
+ }
+
+ template <class TExecParamsPtr>
+ static void PrepareTempDestination(
+ const TString& tmpTable,
+ const TExecParamsPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ const NYT::ITransactionPtr tx)
+ {
+ auto cluster = execCtx->Cluster_;
+
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ PrepareCommonAttributes(attrs, execCtx, cluster, true);
+
+ YQL_CLOG(INFO, ProviderYt) << "Table " << tmpTable << ", attrs: " << NYT::NodeToYsonString(attrs);
+
+ entry->CreateDefaultTmpFolder();
+ CreateParents(TVector<TString>{tmpTable}, entry->CacheTx);
+
+ tx->Create(tmpTable, NT_TABLE, TCreateOptions().Force(true).Attributes(attrs));
+ entry->DeleteAtFinalizeInternal(tmpTable);
+ }
+
+ TSession::TPtr GetSession(const TString& sessionId, bool failIfNotExists = true) const {
+ auto guard = Guard(Mutex_);
+ if (auto p = Sessions_.FindPtr(sessionId)) {
+ return *p;
+ }
+ if (failIfNotExists) {
+ YQL_LOG_CTX_THROW yexception() << "Session doesn't exist: " << sessionId;
+ }
+ return {};
+ }
+
+ template <class TOptions>
+ typename TExecContext<TOptions>::TPtr MakeExecCtx(
+ TOptions&& options,
+ const TSession::TPtr& session,
+ const TString& cluster,
+ const TExprNode* root,
+ TExprContext* exprCtx) const
+ {
+ auto ctx = MakeIntrusive<TExecContext<TOptions>>(Services_, Clusters_, MkqlCompiler_, std::move(options), session, cluster, UrlMapper_);
+ if (root) {
+ YQL_ENSURE(exprCtx);
+ if (TYtTransientOpBase::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("settings",
+ ConvertToAst(*root->Child(TYtTransientOpBase::idx_Settings), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ if (TYtMap::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("mapper",
+ ConvertToAst(*root->Child(TYtMap::idx_Mapper), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ } else if (TYtReduce::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("reducer",
+ ConvertToAst(*root->Child(TYtReduce::idx_Reducer), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ } else if (TYtMapReduce::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("mapper",
+ ConvertToAst(*root->Child(TYtMapReduce::idx_Mapper), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ ctx->CodeSnippets_.emplace_back("reducer",
+ ConvertToAst(*root->Child(TYtMapReduce::idx_Reducer), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ }
+ } else if (TYtFill::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("lambda",
+ ConvertToAst(*root->Child(TYtFill::idx_Content), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ } else if (TYtPublish::Match(root)) {
+ ctx->CodeSnippets_.emplace_back("settings",
+ ConvertToAst(*root->Child(TYtPublish::idx_Settings), *exprCtx, 0, true)
+ .Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ } else {
+ ctx->CodeSnippets_.emplace_back("code",
+ ConvertToAst(*root, *exprCtx, 0, true).Root->ToString(TAstPrintFlags::ShortQuote | TAstPrintFlags::PerLine | TAstPrintFlags::AdaptArbitraryContent));
+ }
+ }
+ return ctx;
+ }
+
+private:
+ const TYtNativeServices Services_;
+ const TConfigClusters::TPtr Clusters_;
+ TIntrusivePtr<NCommon::TMkqlCommonCallableCompiler> MkqlCompiler_;
+ TMutex Mutex_;
+ THashMap<TString, TSession::TPtr> Sessions_;
+ const TYtUrlMapper UrlMapper_;
+ IStatUploader::TPtr StatUploader_;
+};
+
+} // NNative
+
+IYtGateway::TPtr CreateYtNativeGateway(const TYtNativeServices& services) {
+ return MakeIntrusive<NNative::TYtNativeGateway>(services);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.h
new file mode 100644
index 0000000000..e8bd95ab35
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_native.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+
+#include <ydb/library/yql/core/file_storage/file_storage.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+
+#include <util/generic/ptr.h>
+
+namespace NYql {
+
+class TYtGatewayConfig;
+using TYtGatewayConfigPtr = std::shared_ptr<TYtGatewayConfig>;
+
+struct TYtNativeServices {
+ const NKikimr::NMiniKQL::IFunctionRegistry* FunctionRegistry = nullptr;
+
+ TFileStoragePtr FileStorage;
+ TYtGatewayConfigPtr Config;
+ // allow anonymous access for tests
+ bool DisableAnonymousClusterAccess = false;
+};
+
+IYtGateway::TPtr CreateYtNativeGateway(const TYtNativeServices& services);
+
+}
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
new file mode 100644
index 0000000000..31c95058be
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.cpp
@@ -0,0 +1,224 @@
+#include "yql_yt_op_tracker.h"
+
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/job_statistics.h>
+
+#include <util/datetime/base.h>
+#include <util/system/guard.h>
+#include <util/system/yassert.h>
+
+namespace NYql {
+
+namespace NNative {
+
+using namespace NThreading;
+
+// see https://yt.yandex-team.ru/docs/problems/jobstatistics.html for full list
+const static TStringBuf YT_STATISTICS[] = {
+ "job_proxy/cpu/system",
+ "job_proxy/cpu/user",
+ "data/input/chunk_count",
+ "data/input/row_count",
+ "data/input/data_weight",
+ "time/exec",
+ "time/total",
+ "time/prepare",
+ "time/artifact_download",
+ "user_job/cpu/user",
+ "user_job/cpu/system",
+ "user_job/max_memory",
+ "user_job/woodpecker",
+};
+
+const static TStringBuf CUSTOM_STATISTICS[] = {
+ "CodeGen_CompileTime",
+ "CodeGen_GenerateTime",
+ "CodeGen_FullTime",
+ "Combine_FlushesCount",
+ "Combine_MaxRowsCount",
+ "Job_ElapsedTime",
+ "Job_InputBytes",
+ "Job_InputDecodeTime",
+ "Job_InputReadTime",
+ "Job_OutputBytes",
+ "Job_OutputEncodeTime",
+ "Job_OutputWriteTime",
+ "Job_SystemTime",
+ "Job_UserTime",
+ "Job_InitTime",
+ "Job_CalcTime",
+ "Join_Spill_Count",
+ "Join_Spill_MaxFileSize",
+ "Join_Spill_MaxRowsCount",
+ "PagePool_AllocCount",
+ "PagePool_PageAllocCount",
+ "PagePool_PageHitCount",
+ "PagePool_PageMissCount",
+ "PagePool_PeakAllocated",
+ "PagePool_PeakUsed",
+ "Switch_FlushesCount",
+ "Switch_MaxRowsCount",
+ "Udf_AppliesCount",
+};
+
+TOperationTracker::TOperationTracker()
+ : Thread_(TThread::TParams{Tracker, (void*)this}.SetName("yt_op_tracker"))
+{
+ Running_ = true;
+ Thread_.Start();
+}
+
+TOperationTracker::~TOperationTracker() {
+ Y_VERIFY(!Thread_.Running());
+}
+
+void TOperationTracker::Stop() {
+ if (Running_) {
+ Running_ = false;
+ Thread_.Join();
+ }
+}
+
+TFuture<void> TOperationTracker::MakeOperationWaiter(const NYT::IOperationPtr& operation, TMaybe<ui32> publicId,
+ const TString& ytServer, const TOperationProgressWriter& progressWriter, const TStatWriter& statWriter)
+{
+ auto future = operation->GetStartedFuture().Apply([operation](const auto& f) {
+ f.GetValue();
+ return operation->Watch();
+ });
+ if (!publicId) {
+ return future;
+ }
+
+ TOperationProgress progress(TString(YtProviderName), *publicId,
+ TOperationProgress::EState::InProgress);
+
+ auto filter = NYT::TOperationAttributeFilter();
+ filter.Add(NYT::EOperationAttribute::State);
+
+ auto checker = [future, operation, ytServer, progress, progressWriter, filter] () mutable {
+ bool done = future.Wait(TDuration::Zero());
+
+ if (!done) {
+ TString stage;
+ bool writeProgress = true;
+ if (operation->IsStarted()) {
+ if (!progress.RemoteId) {
+ progress.RemoteId = ytServer + "/" + GetGuidAsString(operation->GetId());
+ }
+ if (auto briefProgress = operation->GetBriefProgress()) {
+ progress.Counters.ConstructInPlace();
+ progress.Counters->Completed = briefProgress->Completed;
+ progress.Counters->Running = briefProgress->Running;
+ progress.Counters->Total = briefProgress->Total;
+ progress.Counters->Aborted = briefProgress->Aborted;
+ progress.Counters->Failed = briefProgress->Failed;
+ progress.Counters->Lost = briefProgress->Lost;
+ progress.Counters->Pending = briefProgress->Pending;
+ stage = "Running";
+ } else {
+ auto state = operation->GetAttributes(NYT::TGetOperationOptions().AttributeFilter(filter)).State;
+ if (state) {
+ stage = *state;
+ stage.to_upper(0, 1);
+ }
+ }
+ } else {
+ // Not started yet
+ writeProgress = false;
+ stage = operation->GetStatus();
+ }
+ if (!stage.empty() && stage != progress.Stage.first) {
+ progress.Stage = TOperationProgress::TStage(stage, TInstant::Now());
+ writeProgress = true;
+ }
+ if (writeProgress) {
+ progressWriter(progress);
+ }
+ }
+ return !done;
+ };
+
+ with_lock(Mutex_) {
+ // TODO: limit number of running operations
+ RunningOperations_.push_back(checker);
+ }
+
+ // Make a final progress write
+ return future.Apply([operation, progress, progressWriter, statWriter, ytServer] (const TFuture<void>& f) mutable {
+ f.GetValue();
+ if (auto briefProgress = operation->GetBriefProgress()) {
+ progress.Counters.ConstructInPlace();
+ progress.Counters->Completed = briefProgress->Completed;
+ progress.Counters->Running = briefProgress->Running;
+ progress.Counters->Total = briefProgress->Total;
+ progress.Counters->Aborted = briefProgress->Aborted;
+ progress.Counters->Failed = briefProgress->Failed;
+ progress.Counters->Lost = briefProgress->Lost;
+ progress.Counters->Pending = briefProgress->Pending;
+ }
+ auto operationStatistic = operation->GetJobStatistics();
+
+ TVector<TOperationStatistics::TEntry> statEntries;
+ for (auto statName : YT_STATISTICS) {
+ if (operationStatistic.HasStatistics(statName)) {
+ auto st = operationStatistic.GetStatistics(statName);
+ statEntries.emplace_back(TString{statName}, st.Sum(), st.Max(), st.Min(), st.Avg(), st.Count());
+ }
+ }
+
+ for (auto statName : CUSTOM_STATISTICS) {
+ if (operationStatistic.HasCustomStatistics(statName)) {
+ auto st = operationStatistic.GetCustomStatistics(statName);
+ statEntries.emplace_back(TString{statName}, st.Sum(), st.Max(), st.Min(), st.Avg(), st.Count());
+ }
+ }
+
+ statEntries.emplace_back("_cluster", ytServer);
+ statEntries.emplace_back("_id", GetGuidAsString(operation->GetId()));
+ statWriter(progress.Id, statEntries);
+ progressWriter(progress);
+ });
+}
+
+void* TOperationTracker::Tracker(void* param) {
+ static_cast<TOperationTracker*>(param)->Tracker();
+ return nullptr;
+
+}
+
+void TOperationTracker::Tracker() {
+ while (Running_) {
+ decltype(RunningOperations_) ops;
+ with_lock(Mutex_) {
+ ops.reserve(RunningOperations_.size());
+ ops.swap(RunningOperations_);
+ }
+ decltype(RunningOperations_) activeOps(Reserve(ops.size()));
+ for (auto& op: ops) {
+ try {
+ if (op()) {
+ activeOps.push_back(op);
+ }
+ } catch (...) {
+ }
+ if (!Running_) {
+ break;
+ }
+ }
+ if (!Running_) {
+ break;
+ }
+ with_lock(Mutex_) {
+ RunningOperations_.insert(RunningOperations_.end(), activeOps.begin(), activeOps.end());
+ }
+
+ Sleep(TDuration::Seconds(1));
+ }
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.h
new file mode 100644
index 0000000000..1c195f8de4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_op_tracker.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_execution.h>
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/system/mutex.h>
+#include <util/system/thread.h>
+
+#include <atomic>
+#include <functional>
+
+namespace NYql {
+
+namespace NNative {
+
+class TOperationTracker: public TThrRefBase {
+public:
+ using TPtr = ::TIntrusivePtr<TOperationTracker>;
+
+ TOperationTracker();
+ ~TOperationTracker();
+
+ void Stop();
+
+ NThreading::TFuture<void> MakeOperationWaiter(const NYT::IOperationPtr& operation, TMaybe<ui32> publicId,
+ const TString& ytServer, const TOperationProgressWriter& writer, const TStatWriter& statWriter);
+
+private:
+ static void* Tracker(void* param);
+
+ void Tracker();
+
+private:
+ TMutex Mutex_;
+ TVector<std::function<bool()>> RunningOperations_;
+ std::atomic<bool> Running_{false};
+ TThread Thread_;
+};
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
new file mode 100644
index 0000000000..59235f7447
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.cpp
@@ -0,0 +1,152 @@
+#include "yql_yt_qb2.h"
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/string/join.h>
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+namespace NNative {
+
+void ProcessTableQB2Premapper(const NYT::TNode& remapper,
+ const TString& tableName,
+ NYT::TRichYPath& tablePath,
+ size_t tableNum,
+ TRemapperMap& remapperMap,
+ TSet<TString>& remapperAllFiles,
+ bool& useSkiff)
+{
+ THashMap<TString, TVector<TString>> requiredColumnsForField;
+ THashMap<TString, TVector<TString>> requiredFilesForField;
+ THashSet<TString> passtroughFields;
+ bool allPasstrough = true;
+ for (const auto& field: remapper["fields"].AsMap()) {
+ auto& requiredColumns = requiredColumnsForField[field.first];
+ for (const auto& column: field.second["required_columns"].AsList()) {
+ requiredColumns.push_back(column.AsString());
+ }
+
+ for (const auto& file: field.second["required_dict_paths"].AsList()) {
+ requiredFilesForField[field.first].push_back(file.AsString());
+ }
+
+ bool passthrough = false;
+ if (field.second.HasKey("passthrough")) {
+ passthrough = field.second["passthrough"].AsBool();
+ // check real field name
+ if (passthrough && (requiredColumns.size() != 1 || requiredColumns.front() != field.first)) {
+ passthrough = false;
+ }
+ }
+
+ if (passthrough) {
+ passtroughFields.insert(field.first);
+ }
+ }
+
+ TVector<TString> remappedFields;
+ if (tablePath.Columns_) {
+ TSet<TString> requiredColumns;
+ for (const auto& field : tablePath.Columns_->Parts_) {
+ allPasstrough = allPasstrough && passtroughFields.contains(field);
+ remappedFields.push_back(field);
+ auto columns = requiredColumnsForField.FindPtr(field);
+ YQL_ENSURE(columns, "Unknown column name in remapper specification: " << field << ", table: " << tableName);
+ requiredColumns.insert(columns->begin(), columns->end());
+ }
+
+ if (!allPasstrough) {
+ for (const auto& field : tablePath.Columns_->Parts_) {
+ auto files = requiredFilesForField.FindPtr(field);
+ if (files) {
+ remapperAllFiles.insert(files->begin(), files->end());
+ }
+ }
+ tablePath.Columns(TVector<TString>(requiredColumns.begin(), requiredColumns.end()));
+ }
+ }
+ else {
+ // add all fields
+ for (const auto& field: remapper["fields"].AsMap()) {
+ allPasstrough = allPasstrough && passtroughFields.contains(field.first);
+ remappedFields.push_back(field.first);
+ }
+
+ if (!allPasstrough) {
+ for (const auto& x : requiredFilesForField) {
+ remapperAllFiles.insert(x.second.begin(), x.second.end());
+ }
+ }
+ }
+
+ if (!allPasstrough) {
+ remapperAllFiles.insert(remapper["binary_path"].AsString());
+
+ TRemapperKey key = std::make_tuple(remapper["command_prefix"].AsString(), remapper["log_name"].AsString(), JoinSeq(",", remappedFields));
+ remapperMap[key].push_back(tableNum);
+
+ if (remapper.HasKey("skiff")) {
+ const auto& skiffMap = remapper["skiff"];
+ useSkiff = skiffMap.HasKey("output") && skiffMap["output"].AsBool();
+ } else {
+ useSkiff = false;
+ }
+ }
+
+}
+
+TString GetQB2PremapperPrefix(const TRemapperMap& remapperMap, bool useSkiff) {
+ TString remapperPrefix;
+ TString prevPremapperBinary;
+ for (const auto& x : remapperMap) {
+ if (remapperPrefix.empty()) {
+ remapperPrefix.append("set -o pipefail; ");
+ }
+
+ const auto& premapperBinary = std::get<0>(x.first);
+ const auto& logName = std::get<1>(x.first);
+ const auto& fields = std::get<2>(x.first);
+ if (premapperBinary != prevPremapperBinary) {
+ if (!prevPremapperBinary.empty()) {
+ remapperPrefix.append(" | ");
+ }
+
+ prevPremapperBinary = premapperBinary;
+ remapperPrefix.append(premapperBinary);
+ }
+
+ if (useSkiff) {
+ remapperPrefix.append(" --output-format skiff");
+ }
+
+ remapperPrefix.append(" -l ").append(logName);
+ remapperPrefix.append(" -f ").append(fields);
+ remapperPrefix.append(" -t ").append(JoinSeq(",", x.second));
+ }
+
+ if (!remapperPrefix.empty()) {
+ remapperPrefix.append(" | ");
+ }
+ return remapperPrefix;
+}
+
+void UpdateQB2PremapperUseSkiff(const TRemapperMap& remapperMap, bool& useSkiff) {
+ THashSet<TString> remapperBinaries;
+
+ for (const auto& pair : remapperMap) {
+ remapperBinaries.insert(std::get<0>(pair.first));
+ }
+
+ // explicitly disable qb2 premapper's skiff mode
+ // as it won't work with pipeline from GetQB2PremapperPrefix
+ if (remapperBinaries.size() > 1) {
+ useSkiff = false;
+ }
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.h
new file mode 100644
index 0000000000..0a80eaa8a0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_qb2.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <util/generic/set.h>
+#include <util/generic/string.h>
+
+#include <map>
+#include <tuple>
+#include <vector>
+
+namespace NYql {
+
+namespace NNative {
+
+using TRemapperKey = std::tuple<TString, TString, TString>; // binary path (must be first), log name, all fields
+using TRemapperPayload = std::vector<ui32>; // table indicies
+using TRemapperMap = std::map<TRemapperKey, TRemapperPayload>;
+
+void ProcessTableQB2Premapper(const NYT::TNode& remapper,
+ const TString& tableName,
+ NYT::TRichYPath& tablePath,
+ size_t tableNum,
+ TRemapperMap& remapperMap,
+ TSet<TString>& remapperAllFiles,
+ bool& useSkiff);
+
+TString GetQB2PremapperPrefix(const TRemapperMap& remapperMap, bool useSkiff);
+
+void UpdateQB2PremapperUseSkiff(const TRemapperMap& remapperMap, bool& useSkiff);
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
new file mode 100644
index 0000000000..d9eef11818
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.cpp
@@ -0,0 +1,64 @@
+#include "yql_yt_session.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/init_yt_api/init.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/system/env.h>
+
+namespace NYql {
+
+namespace NNative {
+
+TSession::TSession(IYtGateway::TOpenSessionOptions&& options, size_t numThreads)
+ : UserName_(std::move(options.UserName()))
+ , ProgressWriter_(std::move(options.ProgressWriter()))
+ , StatWriter_(std::move(options.StatWriter()))
+ , OperationOptions_(std::move(options.OperationOptions()))
+ , RandomProvider_(std::move(options.RandomProvider()))
+ , TimeProvider_(std::move(options.TimeProvider()))
+ , DeterministicMode_(GetEnv("YQL_DETERMINISTIC_MODE"))
+ , OperationSemaphore(nullptr)
+ , TxCache_(UserName_)
+ , SessionId_(options.SessionId_)
+{
+ InitYtApiOnce(OperationOptions_.AttrsYson);
+
+ Queue_ = TAsyncQueue::Make(numThreads, "YtGateway");
+ OpTracker_ = MakeIntrusive<TOperationTracker>();
+}
+
+void TSession::Close() {
+ if (OperationSemaphore) {
+ OperationSemaphore->Cancel();
+ }
+ try {
+ TxCache_.AbortAll();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ OpTracker_->Stop();
+ Queue_->Stop();
+}
+
+NYT::TNode TSession::CreateSpecWithDesc(const TVector<std::pair<TString, TString>>& code) const {
+ return YqlOpOptionsToSpec(OperationOptions_, UserName_, code);
+}
+
+NYT::TNode TSession::CreateTableAttrs() const {
+ return YqlOpOptionsToAttrs(OperationOptions_);
+}
+
+void TSession::EnsureInitializedSemaphore(const TYtSettings::TConstPtr& settings) {
+ with_lock(Mutex_) {
+ if (!OperationSemaphore) {
+ const size_t parallelOperationsLimit = settings->ParallelOperationsLimit.Get().GetOrElse(1U << 20);
+ OperationSemaphore = NThreading::TAsyncSemaphore::Make(parallelOperationsLimit);
+ }
+ }
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.h
new file mode 100644
index 0000000000..26009cc9fa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_session.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "yql_yt_op_tracker.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+
+#include <ydb/library/yql/core/yql_execution.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/utils/threading/async_queue.h>
+
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/threading/future/async_semaphore.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/system/mutex.h>
+#include <util/system/sem.h>
+#include <util/system/tempfile.h>
+#include <util/thread/pool.h>
+
+#include <utility>
+
+namespace NYql {
+
+namespace NNative {
+
+struct TSession: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TSession>;
+
+ TSession(IYtGateway::TOpenSessionOptions&& options, size_t numThreads);
+ ~TSession() = default;
+
+ void Close();
+ NYT::TNode CreateSpecWithDesc(const TVector<std::pair<TString, TString>>& code = {}) const;
+ NYT::TNode CreateTableAttrs() const;
+
+ void EnsureInitializedSemaphore(const TYtSettings::TConstPtr& settings);
+
+ const TString UserName_;
+ const TOperationProgressWriter ProgressWriter_;
+ const TStatWriter StatWriter_;
+ const TYqlOperationOptions OperationOptions_;
+ const TIntrusivePtr<IRandomProvider> RandomProvider_;
+ const TIntrusivePtr<ITimeProvider> TimeProvider_;
+ const bool DeterministicMode_;
+ TAsyncQueue::TPtr Queue_;
+ TOperationTracker::TPtr OpTracker_;
+ NThreading::TAsyncSemaphore::TPtr OperationSemaphore;
+ TMutex Mutex_;
+
+ TTransactionCache TxCache_;
+ TString SessionId_;
+};
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
new file mode 100644
index 0000000000..42824e2db1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.cpp
@@ -0,0 +1,580 @@
+#include "yql_yt_spec.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/writer.h>
+#include <library/cpp/digest/md5/md5.h>
+
+#include <util/stream/str.h>
+#include <util/system/env.h>
+#include <util/system/execpath.h>
+#include <util/generic/size_literals.h>
+
+
+namespace NYql {
+
+namespace NNative {
+
+namespace {
+
+ui64 GetCombiningDataSizePerJob(ui64 dataSizePerJob, TMaybe<ui64> minChunkSize) {
+ static const ui64 DefaultCombineChunkSize = 1_GB;
+ ui64 result = dataSizePerJob;
+ if (!minChunkSize.Defined() || *minChunkSize == 0) {
+ result = Max(result, DefaultCombineChunkSize);
+ } else {
+ result = Max(result, *minChunkSize);
+ }
+ return result;
+}
+
+const TString& GetPersistentExecPathMd5()
+{
+ static TString md5 = MD5::File(GetPersistentExecPath());
+ return md5;
+}
+
+}
+
+TMaybe<TString> GetPool(
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings)
+{
+ TMaybe<TString> pool;
+
+ if (auto val = settings->Pool.Get(execCtx.Cluster_)) {
+ pool = *val;
+ }
+ else if (auto val = settings->StaticPool.Get()) {
+ pool = *val;
+ }
+ else if (settings->Auth.Get().GetOrElse(TString()).empty()) {
+ pool = execCtx.Session_->UserName_;
+ }
+
+ return pool;
+}
+
+void FillSpec(NYT::TNode& spec,
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings,
+ const TTransactionCache::TEntry::TPtr& entry,
+ double extraCpu,
+ const TMaybe<double>& secondExtraCpu,
+ EYtOpProps opProps)
+{
+ auto& cluster = execCtx.Cluster_;
+
+ if (auto val = settings->OperationSpec.Get(cluster)) {
+ NYT::TNode tmpSpec = *val;
+ NYT::MergeNodes(tmpSpec, spec);
+ spec = std::move(tmpSpec);
+ }
+
+ auto& sampling = execCtx.Sampling;
+ auto maxRowWeight = settings->MaxRowWeight.Get(cluster);
+ auto maxKeyWeight = settings->MaxKeyWeight.Get(cluster);
+ auto bufferRowCount = settings->BufferRowCount.Get(cluster);
+
+ if (maxRowWeight || maxKeyWeight || bufferRowCount || (sampling && opProps.HasFlags(EYtOpProp::AllowSampling))) {
+ NYT::TNode jobIO;
+ if (maxRowWeight) {
+ jobIO["table_writer"]["max_row_weight"] = static_cast<i64>(*maxRowWeight);
+ }
+ if (maxKeyWeight) {
+ jobIO["table_writer"]["max_key_weight"] = static_cast<i64>(*maxKeyWeight);
+ }
+ if (bufferRowCount) {
+ jobIO["buffer_row_count"] = static_cast<i64>(*bufferRowCount);
+ }
+ if (!jobIO.IsUndefined() && opProps.HasFlags(EYtOpProp::IntermediateData)) {
+ // Both Sort and MapReduce
+ spec["sort_job_io"] = jobIO;
+ if (opProps.HasFlags(EYtOpProp::WithUserJobs)) {
+ // MapReduce
+ spec["reduce_job_io"] = jobIO;
+ }
+ else {
+ // Sort
+ spec["partition_job_io"] = jobIO;
+ spec["merge_job_io"] = jobIO;
+ }
+ }
+
+ // Set sampling only for input jobs
+ if (sampling && opProps.HasFlags(EYtOpProp::AllowSampling)) {
+ if (sampling->Mode == EYtSampleMode::System) {
+ NYT::TNode systemSamplingParams = NYT::TNode::CreateMap();
+ systemSamplingParams["sampling_rate"] = sampling->Percentage / 100.;
+ if (auto blockSize = settings->SamplingIoBlockSize.Get(cluster)) {
+ systemSamplingParams["io_block_size"] = static_cast<i64>(*blockSize);
+ }
+ spec["sampling"] = systemSamplingParams;
+ } else if (sampling->Mode == EYtSampleMode::Bernoulli) {
+ jobIO["table_reader"]["sampling_rate"] = sampling->Percentage / 100.;
+ if (sampling->Repeat) {
+ jobIO["table_reader"]["sampling_seed"] = static_cast<i64>(sampling->Repeat);
+ }
+ }
+ }
+ if (!jobIO.IsUndefined()) {
+ if (!opProps.HasFlags(EYtOpProp::IntermediateData)) {
+ // Merge, Map, Reduce
+ spec["job_io"] = jobIO;
+ } else if (opProps.HasFlags(EYtOpProp::WithUserJobs)) {
+ // MapReduce
+ spec["map_job_io"] = jobIO;
+ }
+ }
+ }
+
+ if (opProps.HasFlags(EYtOpProp::IntermediateData)) {
+ const auto intermediateMedium = settings->IntermediateDataMedium.Get(cluster);
+ if (intermediateMedium) {
+ spec["intermediate_data_medium"] = *intermediateMedium;
+ }
+ }
+
+ ui64 dataSizePerJob = settings->DataSizePerJob.Get(cluster).GetOrElse(0);
+ if (opProps.HasFlags(EYtOpProp::PublishedChunkCombine)) {
+ dataSizePerJob = GetCombiningDataSizePerJob(dataSizePerJob, settings->MinPublishedAvgChunkSize.Get());
+ } else if (opProps.HasFlags(EYtOpProp::TemporaryChunkCombine)) {
+ dataSizePerJob = GetCombiningDataSizePerJob(dataSizePerJob, settings->MinTempAvgChunkSize.Get());
+ }
+
+ if (opProps.HasFlags(EYtOpProp::IntermediateData) && opProps.HasFlags(EYtOpProp::WithUserJobs)) { // MapReduce
+ ui64 dataSizePerMapJob = dataSizePerJob;
+ ui64 dataSizePerPartition = dataSizePerJob;
+ if (auto val = settings->DataSizePerMapJob.Get(cluster).GetOrElse(0)) {
+ dataSizePerMapJob = val;
+ }
+
+ if (auto val = settings->DataSizePerPartition.Get(cluster).GetOrElse(0)) {
+ dataSizePerPartition = val;
+ }
+
+ if (dataSizePerMapJob) {
+ if (extraCpu != 0.) {
+ dataSizePerMapJob /= extraCpu;
+ }
+ spec["data_size_per_map_job"] = static_cast<i64>(Max<ui64>(dataSizePerMapJob, 1));
+ }
+ if (dataSizePerPartition) {
+ auto secondExtraCpuVal = secondExtraCpu.GetOrElse(extraCpu);
+ if (secondExtraCpuVal != 0) {
+ dataSizePerPartition /= secondExtraCpuVal;
+ }
+ spec["partition_data_size"] = static_cast<i64>(Max<ui64>(dataSizePerPartition, 1));
+ }
+
+ if (auto val = settings->DataSizePerSortJob.Get(cluster)) {
+ spec["data_size_per_sort_job"] = static_cast<i64>(*val);
+ }
+
+ } else if (!opProps.HasFlags(EYtOpProp::IntermediateData)) { // Exclude Sort
+ if (dataSizePerJob) {
+ if (extraCpu != 0.) {
+ dataSizePerJob /= extraCpu;
+ }
+ spec["data_size_per_job"] = static_cast<i64>(Max<ui64>(dataSizePerJob, 1));
+ }
+ }
+
+ if (auto val = settings->Annotations.Get(cluster)) {
+ spec["annotations"] = *val;
+ }
+
+ if (auto val = settings->MaxJobCount.Get(cluster)) {
+ spec["max_job_count"] = static_cast<i64>(*val);
+ }
+
+ if (auto val = settings->UserSlots.Get(cluster)) {
+ spec["resource_limits"]["user_slots"] = static_cast<i64>(*val);
+ }
+
+ if (auto pool = GetPool(execCtx, settings)) {
+ spec["pool"] = *pool;
+ }
+
+ if (auto val = settings->SchedulingTag.Get(cluster)) {
+ spec["scheduling_tag"] = *val;
+ }
+
+ if (auto val = settings->SchedulingTagFilter.Get(cluster)) {
+ spec["scheduling_tag_filter"] = *val;
+ }
+
+ if (auto val = settings->PoolTrees.Get(cluster)) {
+ NYT::TNode trees = NYT::TNode::CreateList();
+ for (auto& tree : *val) {
+ trees.AsList().push_back(tree);
+ }
+ spec["pool_trees"] = trees;
+ }
+
+ if (auto val = settings->TentativePoolTrees.Get(cluster)) {
+ NYT::TNode trees = NYT::TNode::CreateList();
+ NYT::TNode tree_eligibility = NYT::TNode::CreateMap();
+
+ for (auto& tree : *val) {
+ trees.AsList().push_back(tree);
+ }
+
+ if (auto v = settings->TentativeTreeEligibilitySampleJobCount.Get(cluster)) {
+ tree_eligibility["sample_job_count"] = *v;
+ }
+
+ if (auto v = settings->TentativeTreeEligibilityMaxJobDurationRatio.Get(cluster)) {
+ tree_eligibility["max_tentative_job_duration_ratio"] = *v;
+ }
+
+ if (auto v = settings->TentativeTreeEligibilityMinJobDuration.Get(cluster)) {
+ tree_eligibility["min_job_duration"] = *v;
+ }
+
+ spec["tentative_pool_trees"] = trees;
+ spec["tentative_tree_eligibility"] = tree_eligibility;
+ }
+
+ if (auto val = settings->UseDefaultTentativePoolTrees.Get(cluster)) {
+ spec["use_default_tentative_pool_trees"] = *val;
+ }
+
+ if (auto val = settings->DefaultOperationWeight.Get(cluster)) {
+ spec["weight"] = *val;
+ }
+
+ if (auto val = settings->DefaultMapSelectivityFactor.Get(cluster)) {
+ spec["map_selectivity_factor"] = *val;
+ }
+
+ NYT::TNode aclList;
+ TSet<TString> ownersSet = settings->Owners.Get(cluster).GetOrElse(TSet<TString>());
+ if (!ownersSet.empty()) {
+ NYT::TNode owners = NYT::TNode::CreateList();
+ for (auto& o : ownersSet) {
+ owners.Add(o);
+ }
+
+ NYT::TNode acl = NYT::TNode::CreateMap();
+ acl["subjects"] = owners;
+ acl["action"] = "allow";
+ acl["permissions"] = NYT::TNode::CreateList().Add("read").Add("manage");
+
+ aclList.Add(std::move(acl));
+ }
+ if (auto val = settings->OperationReaders.Get(cluster)) {
+ NYT::TNode readers;
+ for (auto& o : *val) {
+ if (!ownersSet.contains(o)) {
+ readers.Add(o);
+ }
+ }
+ if (!readers.IsUndefined()) {
+ NYT::TNode acl = NYT::TNode::CreateMap();
+ acl["subjects"] = readers;
+ acl["action"] = "allow";
+ acl["permissions"] = NYT::TNode::CreateList().Add("read");
+
+ aclList.Add(std::move(acl));
+ }
+ }
+ if (!aclList.IsUndefined()) {
+ spec["acl"] = std::move(aclList);
+ }
+
+ if (opProps.HasFlags(EYtOpProp::IntermediateData)) {
+ if (auto val = settings->IntermediateAccount.Get(cluster)) {
+ spec["intermediate_data_account"] = *val;
+ }
+ else if (auto tmpFolder = settings->TablesTmpFolder.Get().GetOrElse(TString())) {
+ auto attrs = entry->Tx->Get(tmpFolder + "/@", NYT::TGetOptions().AttributeFilter(NYT::TAttributeFilter().AddAttribute(TString("account"))));
+ if (attrs.HasKey("account")) {
+ spec["intermediate_data_account"] = attrs["account"];
+ }
+ }
+
+ // YT merges this ACL with operation ACL
+ // By passing empty list, we allow only user+owners accessing the intermediate data
+ // (note: missing "intermediate_data_acl" actually implies "everyone=read")
+ spec["intermediate_data_acl"] = NYT::TNode::CreateList();
+
+ if (auto val = settings->IntermediateReplicationFactor.Get(cluster)) {
+ spec["intermediate_data_replication_factor"] = static_cast<i64>(*val);
+ }
+
+ }
+
+ if (opProps.HasFlags(EYtOpProp::TemporaryAutoMerge)) {
+ if (auto val = settings->TemporaryAutoMerge.Get(cluster)) {
+ spec["auto_merge"]["mode"] = *val;
+ }
+ }
+
+ if (opProps.HasFlags(EYtOpProp::PublishedAutoMerge)) {
+ if (auto val = settings->PublishedAutoMerge.Get(cluster)) {
+ spec["auto_merge"]["mode"] = *val;
+ }
+ }
+
+ if (settings->UseTmpfs.Get(cluster).GetOrElse(false)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["tmpfs_path"] = TString("_yql_tmpfs");
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["tmpfs_path"] = TString("_yql_tmpfs");
+ }
+ }
+ if (GetEnv(TString("YQL_DETERMINISTIC_MODE"))) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["environment"]["YQL_DETERMINISTIC_MODE"] = TString("1");
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["environment"]["YQL_DETERMINISTIC_MODE"] = TString("1");
+ }
+ }
+ if (auto envMap = settings->JobEnv.Get(cluster)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ for (auto& p: envMap->AsMap()) {
+ spec["mapper"]["environment"][p.first] = p.second;
+ }
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ for (auto& p: envMap->AsMap()) {
+ spec["reducer"]["environment"][p.first] = p.second;
+ }
+ }
+ }
+
+ if (settings->EnforceJobUtc.Get(cluster).GetOrElse(false)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["environment"]["TZ"] = TString("UTC0");
+ }
+
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["environment"]["TZ"] = TString("UTC0");
+ }
+ }
+
+ if (settings->SuspendIfAccountLimitExceeded.Get(cluster).GetOrElse(false)) {
+ spec["suspend_operation_if_account_limit_exceeded"] = true;
+ }
+
+ if (settings->DisableJobSplitting.Get(cluster).GetOrElse(false)) {
+ spec["enable_job_splitting"] = false;
+ }
+
+ if (auto val = settings->DefaultMemoryReserveFactor.Get(cluster)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["memory_reserve_factor"] = *val;
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["memory_reserve_factor"] = *val;
+ }
+ }
+
+ if (auto val = settings->DefaultMemoryDigestLowerBound.Get(cluster)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["user_job_memory_digest_lower_bound"] = *val;
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["user_job_memory_digest_lower_bound"] = *val;
+ }
+ }
+
+ if (auto val = settings->DefaultLocalityTimeout.Get(cluster)) {
+ spec["locality_timeout"] = static_cast<i64>((*val).Seconds());
+ }
+
+ if (auto val = settings->MapLocalityTimeout.Get(cluster)) {
+ spec["map_locality_timeout"] = static_cast<i64>((*val).Seconds());
+ }
+
+ if (auto val = settings->ReduceLocalityTimeout.Get(cluster)) {
+ spec["reduce_locality_timeout"] = static_cast<i64>((*val).Seconds());
+ }
+
+ if (auto val = settings->SortLocalityTimeout.Get(cluster)) {
+ spec["sort_locality_timeout"] = static_cast<i64>((*val).Seconds());
+ }
+
+ if (auto val = settings->MinLocalityInputDataWeight.Get(cluster)) {
+ spec["min_locality_input_data_weight"] = static_cast<i64>(*val);
+ }
+
+ if (auto val = settings->UseColumnarStatistics.Get(cluster)) {
+ bool flag = true;
+ switch (*val) {
+ case EUseColumnarStatisticsMode::Force:
+ break;
+ case EUseColumnarStatisticsMode::Disable:
+ flag = false;
+ break;
+ case EUseColumnarStatisticsMode::Auto:
+ if (AnyOf(execCtx.InputTables_, [](const auto& input) { return input.Lookup; })) {
+ flag = false;
+ }
+ break;
+ }
+ spec["input_table_columnar_statistics"]["enabled"] = flag;
+ }
+
+ if (opProps.HasFlags(EYtOpProp::WithUserJobs)) {
+ spec["user_file_columnar_statistics"]["enabled"] = settings->TableContentColumnarStatistics.Get(cluster).GetOrElse(true);
+ }
+
+ if (auto val = settings->LayerPaths.Get(cluster)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ NYT::TNode& layersNode = spec["mapper"]["layer_paths"];
+ for (auto& path: *val) {
+ layersNode.Add(NYT::AddPathPrefix(path, NYT::TConfig::Get()->Prefix));
+ }
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ NYT::TNode& layersNode = spec["reducer"]["layer_paths"];
+ for (auto& path: *val) {
+ layersNode.Add(NYT::AddPathPrefix(path, NYT::TConfig::Get()->Prefix));
+ }
+ }
+ }
+
+ if (auto val = settings->MaxSpeculativeJobCountPerTask.Get(cluster)) {
+ spec["max_speculative_job_count_per_task"] = i64(*val);
+ }
+
+ if (auto val = settings->NetworkProject.Get(cluster)) {
+ if (opProps.HasFlags(EYtOpProp::WithMapper)) {
+ spec["mapper"]["network_project"] = *val;
+ }
+ if (opProps.HasFlags(EYtOpProp::WithReducer)) {
+ spec["reducer"]["network_project"] = *val;
+ }
+ }
+
+ if (auto val = settings->_ForceJobSizeAdjuster.Get(cluster)) {
+ spec["force_job_size_adjuster"] = *val;
+ }
+}
+
+void FillSecureVault(NYT::TNode& spec, const IYtGateway::TSecureParams& secureParams) {
+ if (secureParams.empty()) {
+ return;
+ }
+ TStringStream out;
+ NYson::TYsonWriter writer(&out, NYson::EYsonFormat::Text);
+ writer.OnBeginMap();
+ for (const auto& it : secureParams) {
+ writer.OnKeyedItem(it.first);
+ writer.OnStringScalar(it.second);
+ }
+ writer.OnEndMap();
+ spec["secure_vault"]["secure_params"] = out.Str();
+}
+
+void FillUserJobSpecImpl(NYT::TUserJobSpec& spec,
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings,
+ const TExpressionResorceUsage& extraUsage,
+ ui64 fileMemUsage,
+ ui64 llvmMemUsage,
+ bool localRun,
+ const TString& cmdPrefix)
+{
+ auto cluster = execCtx.Cluster_;
+ auto mrJobBin = execCtx.Config_->GetMrJobBin();
+ TMaybe<TString> mrJobBinMd5;
+ if (!mrJobBin.empty()) {
+ if (execCtx.Config_->HasMrJobBinMd5()) {
+ mrJobBinMd5 = execCtx.Config_->GetMrJobBinMd5();
+ } else {
+ YQL_CLOG(WARN, ProviderYt) << "MrJobBin without MD5";
+ }
+ }
+
+ const TString binTmpFolder = settings->BinaryTmpFolder.Get().GetOrElse(TString());
+ if (!localRun && binTmpFolder) {
+ const TDuration binExpiration = settings->BinaryExpirationInterval.Get().GetOrElse(TDuration());
+ TTransactionCache::TEntry::TPtr entry = execCtx.GetOrCreateEntry(settings);
+ TString bin = mrJobBin.empty() ? GetPersistentExecPath() : mrJobBin;
+ const auto binSize = TFileStat(bin).Size;
+ YQL_ENSURE(binSize != 0);
+
+ if (mrJobBin.empty()) {
+ mrJobBinMd5 = GetPersistentExecPathMd5();
+ } else if (!mrJobBinMd5) {
+ if (GetEnv("YQL_LOCAL") == "1") {
+ // do not calculate heavy md5 in local mode (YQL-15353)
+ mrJobBinMd5 = MD5::Calc(mrJobBin);
+ } else {
+ mrJobBinMd5 = MD5::File(mrJobBin);
+ }
+ }
+
+ auto mrJobSnapshot = entry->GetBinarySnapshot(binTmpFolder, *mrJobBinMd5, bin, binExpiration);
+ spec.JobBinaryCypressPath(mrJobSnapshot.first, mrJobSnapshot.second);
+ fileMemUsage += binSize;
+ }
+ else if (!mrJobBin.empty()) {
+ const auto binSize = TFileStat(mrJobBin).Size;
+ YQL_ENSURE(binSize != 0);
+ spec.JobBinaryLocalPath(mrJobBin, mrJobBinMd5);
+ fileMemUsage += binSize;
+ }
+ auto defaultMemoryLimit = settings->DefaultMemoryLimit.Get(cluster).GetOrElse(0);
+ ui64 tmpFsSize = settings->UseTmpfs.Get(cluster).GetOrElse(false)
+ ? (ui64)settings->ExtraTmpfsSize.Get(cluster).GetOrElse(8_MB)
+ : ui64(0);
+
+ if (defaultMemoryLimit || fileMemUsage || llvmMemUsage || extraUsage.Memory || tmpFsSize) {
+ const ui64 memIoBuffers = YQL_JOB_CODEC_MEM * (static_cast<size_t>(!execCtx.InputTables_.empty()) + execCtx.OutTables_.size());
+ const ui64 finalMemLimit = Max<ui64>(
+ defaultMemoryLimit,
+ 128_MB + fileMemUsage + extraUsage.Memory + tmpFsSize + memIoBuffers,
+ llvmMemUsage + memIoBuffers // LLVM consumes memory only once on job start, but after IO initialization
+ );
+ YQL_CLOG(DEBUG, ProviderYt) << "Job memory limit: " << finalMemLimit
+ << " (from options: " << defaultMemoryLimit
+ << ", files: " << fileMemUsage
+ << ", llvm: " << llvmMemUsage
+ << ", extra: " << extraUsage.Memory
+ << ", extra tmpfs: " << tmpFsSize
+ << ", I/O buffers: " << memIoBuffers
+ << ")";
+ spec.MemoryLimit(static_cast<i64>(finalMemLimit));
+ }
+
+ if (cmdPrefix) {
+ spec.JobCommandPrefix(cmdPrefix);
+ }
+}
+
+void FillOperationOptionsImpl(NYT::TOperationOptions& opOpts,
+ const TYtSettings::TConstPtr& settings,
+ const TTransactionCache::TEntry::TPtr& entry)
+{
+ opOpts.UseTableFormats(true);
+ opOpts.CreateOutputTables(false);
+ if (TString tmpFolder = settings->TmpFolder.Get().GetOrElse(TString())) {
+ opOpts.FileStorage(tmpFolder);
+
+ if (!entry->CacheTxId.IsEmpty()) {
+ opOpts.FileStorageTransactionId(entry->CacheTxId);
+
+ // We need to switch to random-path-upload cache mode because of
+ // specified 'FileStorageTransactionId' (see https://st.yandex-team.ru/YT-8462).
+ opOpts.FileCacheMode(NYT::TOperationOptions::EFileCacheMode::CachelessRandomPathUpload);
+ }
+ }
+ if (auto ttl = settings->FileCacheTtl.Get().GetOrElse(TDuration::Days(7))) {
+ opOpts.FileExpirationTimeout(ttl);
+ }
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.h
new file mode 100644
index 0000000000..66a8aece43
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_spec.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include "yql_yt_exec_ctx.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h>
+
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/flags.h>
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+#include <util/generic/typetraits.h>
+
+namespace NYql {
+
+namespace NNative {
+
+enum class EYtOpProp: ui32 {
+ IntermediateData = 1 << 0,
+ TemporaryAutoMerge = 1 << 1,
+ PublishedAutoMerge = 1 << 2,
+ WithMapper = 1 << 3,
+ WithReducer = 1 << 4,
+ WithUserJobs = 1 << 5,
+ AllowSampling = 1 << 6,
+ TemporaryChunkCombine = 1 << 7,
+ PublishedChunkCombine = 1 << 8,
+};
+
+Y_DECLARE_FLAGS(EYtOpProps, EYtOpProp);
+Y_DECLARE_OPERATORS_FOR_FLAGS(EYtOpProps);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TMaybe<TString> GetPool(
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings);
+
+void FillSpec(NYT::TNode& spec,
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings,
+ const TTransactionCache::TEntry::TPtr& entry,
+ double extraCpu,
+ const TMaybe<double>& secondExtraCpu,
+ EYtOpProps opProps = 0);
+
+void FillSecureVault(NYT::TNode& spec, const IYtGateway::TSecureParams& secureParams);
+
+void FillUserJobSpecImpl(NYT::TUserJobSpec& spec,
+ const TExecContextBase& execCtx,
+ const TYtSettings::TConstPtr& settings,
+ const TExpressionResorceUsage& extraUsage,
+ ui64 fileMemUsage,
+ ui64 llvmMemUsage,
+ bool localRun,
+ const TString& cmdPrefix);
+
+void FillOperationOptionsImpl(NYT::TOperationOptions& opOpts,
+ const TYtSettings::TConstPtr& settings,
+ const TTransactionCache::TEntry::TPtr& entry);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+namespace NPrivate {
+ Y_HAS_MEMBER(SecureParams);
+}
+
+template <class TOptions>
+inline void FillSpec(NYT::TNode& spec,
+ const TExecContext<TOptions>& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry,
+ double extraCpu,
+ const TMaybe<double>& secondExtraCpu,
+ EYtOpProps opProps = 0)
+{
+ FillSpec(spec, execCtx, execCtx.Options_.Config(), entry, extraCpu, secondExtraCpu, opProps);
+ if constexpr (NPrivate::THasSecureParams<TOptions>::value) {
+ FillSecureVault(spec, execCtx.Options_.SecureParams());
+ }
+}
+
+template <class TDerived, class TExecParamsPtr>
+inline void FillOperationSpec(NYT::TUserOperationSpecBase<TDerived>& spec, const TExecParamsPtr& execCtx) {
+ if (auto val = execCtx->Options_.Config()->DefaultMaxJobFails.Get()) {
+ spec.MaxFailedJobCount(*val);
+ }
+ if (auto val = execCtx->Options_.Config()->CoreDumpPath.Get()) {
+ spec.CoreTablePath(*val);
+ }
+}
+
+template <class TExecParamsPtr>
+inline void FillUserJobSpec(NYT::TUserJobSpec& spec,
+ const TExecParamsPtr& execCtx,
+ const TExpressionResorceUsage& extraUsage,
+ ui64 fileMemUsage,
+ ui64 llvmMemUsage,
+ bool localRun,
+ const TString& cmdPrefix = {})
+{
+ FillUserJobSpecImpl(spec, *execCtx, execCtx->Options_.Config(), extraUsage, fileMemUsage, llvmMemUsage, localRun, cmdPrefix);
+}
+
+template <class TExecParamsPtr>
+inline void FillOperationOptions(NYT::TOperationOptions& opOpts,
+ const TExecParamsPtr& execCtx,
+ const TTransactionCache::TEntry::TPtr& entry)
+{
+ FillOperationOptionsImpl(opOpts, execCtx->Options_.Config(), entry);
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp b/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
new file mode 100644
index 0000000000..fd9265f19b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.cpp
@@ -0,0 +1,530 @@
+#include "yql_yt_transform.h"
+
+#include <ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/codec/yt_codec.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/streams/brotli/brotli.h>
+
+#include <util/system/env.h>
+#include <util/folder/path.h>
+#include <util/generic/maybe.h>
+#include <util/generic/size_literals.h>
+
+namespace NYql {
+
+namespace NNative {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NYT;
+using namespace NNodes;
+
+TGatewayTransformer::TGatewayTransformer(const TExecContextBase& execCtx, TYtSettings::TConstPtr settings, const TString& optLLVM,
+ TUdfModulesTable udfModules, IUdfResolver::TPtr udfResolver, TTransactionCache::TEntry::TPtr entry,
+ TProgramBuilder& builder, TTempFiles& tmpFiles, TMaybe<ui32> publicId)
+ : ExecCtx_(execCtx)
+ , Settings_(std::move(settings))
+ , UdfModules_(std::move(udfModules))
+ , UdfResolver_(std::move(udfResolver))
+ , Entry_(std::move(entry))
+ , PgmBuilder_(builder)
+ , TmpFiles_(tmpFiles)
+ , PublicId_(publicId)
+ , RemoteExecutionFlag_(std::make_shared<bool>(false))
+ , UntrustedUdfFlag_(std::make_shared<bool>(false))
+ , UsedMem_(std::make_shared<ui64>(ui64(0)))
+ , JobFileAliases_(std::make_shared<THashMap<TString, TString>>())
+ , JobUdfs_(std::make_shared<THashMap<TString, TString>>())
+ , UniqFiles_(std::make_shared<THashMap<TString, TString>>())
+ , RemoteFiles_(std::make_shared<TVector<NYT::TRichYPath>>())
+ , LocalFiles_(std::make_shared<TVector<std::pair<TString, TString>>>())
+ , DeferredUdfFiles_(std::make_shared<TVector<std::pair<TString, TString>>>())
+
+{
+ if (optLLVM != "OFF") {
+ *UsedMem_ = 128_MB;
+ }
+}
+
+TCallableVisitFunc TGatewayTransformer::operator()(TInternName name) {
+ if (name == TYtTableContent::CallableName()) {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ YQL_ENSURE(callable.GetInputsCount() == 4, "Expected 4 args");
+ *RemoteExecutionFlag_ = true;
+
+ const TString cluster(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ const TString& server = ExecCtx_.Clusters_->GetServer(cluster);
+ const TString tmpFolder = Settings_->TablesTmpFolder.Get().GetOrElse(TString());
+ TTransactionCache::TEntry::TPtr entry = ExecCtx_.Session_->TxCache_.GetEntry(server);
+ auto tx = entry->Tx;
+
+ auto deliveryMode = Settings_->TableContentDeliveryMode.Get(cluster).GetOrElse(ETableContentDeliveryMode::Native);
+ bool useSkiff = Settings_->TableContentUseSkiff.Get(cluster).GetOrElse(DEFAULT_USE_SKIFF);
+ const bool ensureOldTypesOnly = !useSkiff;
+ const ui64 maxChunksForNativeDelivery = Settings_->TableContentMaxChunksForNativeDelivery.Get().GetOrElse(1000ul);
+ TString contentTmpFolder = Settings_->TableContentTmpFolder.Get(cluster).GetOrElse(TString());
+ if (contentTmpFolder.StartsWith("//")) {
+ contentTmpFolder = contentTmpFolder.substr(2);
+ }
+ if (contentTmpFolder.EndsWith('/')) {
+ contentTmpFolder.remove(contentTmpFolder.length() - 1);
+ }
+
+ TString uniqueId = GetGuidAsString(ExecCtx_.Session_->RandomProvider_->GenGuid());
+
+ TListLiteral* groupList = AS_VALUE(TListLiteral, callable.GetInput(1));
+ YQL_ENSURE(groupList->GetItemsCount() == 1);
+ TListLiteral* tableList = AS_VALUE(TListLiteral, groupList->GetItems()[0]);
+
+ NYT::TNode specNode = NYT::TNode::CreateMap();
+ NYT::TNode& tablesNode = specNode[YqlIOSpecTables];
+ NYT::TNode& registryNode = specNode[YqlIOSpecRegistry];
+ THashMap<TString, TString> uniqSpecs;
+ TVector<NYT::TRichYPath> richPaths;
+ TVector<NYT::TNode> formats;
+
+ THashMap<TString, ui32> structColumns;
+ if (useSkiff) {
+ auto itemType = AS_TYPE(TListType, callable.GetType()->GetReturnType())->GetItemType();
+ TStructType* itemTypeStruct = AS_TYPE(TStructType, itemType);
+ if (itemTypeStruct->GetMembersCount() == 0) {
+ useSkiff = false; // TODO: YT-12235
+ } else {
+ for (ui32 index = 0; index < itemTypeStruct->GetMembersCount(); ++index) {
+ structColumns.emplace(itemTypeStruct->GetMemberName(index), index);
+ }
+ }
+ }
+
+ for (ui32 i = 0; i < tableList->GetItemsCount(); ++i) {
+ TTupleLiteral* tuple = AS_VALUE(TTupleLiteral, tableList->GetItems()[i]);
+ YQL_ENSURE(tuple->GetValuesCount() == 7, "Expect 7 elements in the Tuple item");
+
+ TString refName = TStringBuilder() << "$table" << uniqSpecs.size();
+ TString specStr = TString(AS_VALUE(TDataLiteral, tuple->GetValue(2))->AsValue().AsStringRef());
+ const auto specNode = NYT::NodeFromYsonString(specStr);
+
+ NYT::TRichYPath richYPath;
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, tuple->GetValue(0))->AsValue().AsStringRef())));
+ const bool isTemporary = AS_VALUE(TDataLiteral, tuple->GetValue(1))->AsValue().Get<bool>();
+ const bool isAnonymous = AS_VALUE(TDataLiteral, tuple->GetValue(5))->AsValue().Get<bool>();
+ const ui32 epoch = AS_VALUE(TDataLiteral, tuple->GetValue(6))->AsValue().Get<ui32>();
+
+ auto tablePath = TransformPath(tmpFolder, richYPath.Path_, isTemporary, ExecCtx_.Session_->UserName_);
+
+ auto res = uniqSpecs.emplace(specStr, refName);
+ if (res.second) {
+ registryNode[refName] = specNode;
+ }
+ else {
+ refName = res.first->second;
+ }
+ tablesNode.Add(refName);
+ if (useSkiff) {
+ formats.push_back(SingleTableSpecToInputSkiff(specNode, structColumns, false, false, false));
+ } else {
+ if (ensureOldTypesOnly && specNode.HasKey(YqlRowSpecAttribute)) {
+ EnsureSpecDoesntUseNativeYtTypes(specNode, tablePath, true);
+ }
+ NYT::TNode formatNode("yson");
+ formatNode.Attributes()["format"] = "binary";
+ formats.push_back(formatNode);
+ }
+
+ if (isTemporary && !isAnonymous) {
+ richYPath.Path_ = NYT::AddPathPrefix(tablePath, NYT::TConfig::Get()->Prefix);
+ } else {
+ auto p = entry->Snapshots.FindPtr(std::make_pair(tablePath, epoch));
+ YQL_ENSURE(p, "Table " << tablePath << " has no snapshot");
+ richYPath.Path(std::get<0>(*p)).TransactionId(std::get<1>(*p));
+ }
+ richPaths.push_back(richYPath);
+
+ const ui64 chunkCount = AS_VALUE(TDataLiteral, tuple->GetValue(3))->AsValue().Get<ui64>();
+ if (chunkCount > maxChunksForNativeDelivery) {
+ deliveryMode = ETableContentDeliveryMode::File;
+ YQL_CLOG(DEBUG, ProviderYt) << "Switching to file delivery mode, because table "
+ << tablePath.Quote() << " has too many chunks: " << chunkCount;
+ }
+ }
+
+ for (size_t i = 0; i < richPaths.size(); ++i) {
+ NYT::TRichYPath richYPath = richPaths[i];
+
+ TString richYPathDesc = NYT::NodeToYsonString(NYT::PathToNode(richYPath));
+ TString fileName = TStringBuilder() << uniqueId << '_' << i;
+
+ if (ETableContentDeliveryMode::Native == deliveryMode) {
+ richYPath.Format(formats[i]);
+ richYPath.FileName(fileName);
+ RemoteFiles_->push_back(richYPath);
+
+ YQL_CLOG(DEBUG, ProviderYt) << "Passing table " << richYPathDesc << " as remote file "
+ << fileName.Quote();
+ }
+ else {
+ NYT::TTableReaderOptions readerOptions;
+ readerOptions.CreateTransaction(false);
+
+ auto readerTx = tx;
+ if (richYPath.TransactionId_) {
+ readerTx = entry->GetSnapshotTx(*richYPath.TransactionId_);
+ richYPath.TransactionId_.Clear();
+ }
+
+ TTupleLiteral* samplingTuple = AS_VALUE(TTupleLiteral, callable.GetInput(2));
+ if (samplingTuple->GetValuesCount() != 0) {
+ YQL_ENSURE(samplingTuple->GetValuesCount() == 3);
+ double samplingPercent = AS_VALUE(TDataLiteral, samplingTuple->GetValue(0))->AsValue().Get<double>();
+ ui64 samplingSeed = AS_VALUE(TDataLiteral, samplingTuple->GetValue(1))->AsValue().Get<ui64>();
+ bool isSystemSampling = AS_VALUE(TDataLiteral, samplingTuple->GetValue(2))->AsValue().Get<bool>();
+ if (!isSystemSampling) {
+ NYT::TNode spec = NYT::TNode::CreateMap();
+ spec["sampling_rate"] = samplingPercent / 100.;
+ if (samplingSeed) {
+ spec["sampling_seed"] = static_cast<i64>(samplingSeed);
+ }
+ readerOptions.Config(spec);
+ }
+ }
+
+ TRawTableReaderPtr reader;
+ const int lastAttempt = NYT::TConfig::Get()->ReadRetryCount - 1;
+ for (int attempt = 0; attempt <= lastAttempt; ++attempt) {
+ try {
+ reader = readerTx->CreateRawReader(richYPath, NYT::TFormat(formats[i]), readerOptions);
+ break;
+ } catch (const NYT::TErrorResponse& e) {
+ YQL_CLOG(ERROR, ProviderYt) << "Error creating reader for " << richYPathDesc << ": " << e.what();
+ // Already retried inside CreateRawReader
+ throw;
+ } catch (const yexception& e) {
+ YQL_CLOG(ERROR, ProviderYt) << "Error creating reader for " << richYPathDesc << ": " << e.what();
+ if (attempt == lastAttempt) {
+ throw;
+ }
+ NYT::NDetail::TWaitProxy::Get()->Sleep(NYT::TConfig::Get()->RetryInterval);
+ }
+ }
+
+ if (contentTmpFolder) {
+ entry->GetRoot()->Create(contentTmpFolder, NT_MAP,
+ TCreateOptions().Recursive(true).IgnoreExisting(true));
+
+ auto remotePath = TString(contentTmpFolder).append('/').append(fileName);
+
+ while (true) {
+ try {
+ auto out = tx->CreateFileWriter(TRichYPath(remotePath));
+ TBrotliCompress compressor(out.Get(), Settings_->TableContentCompressLevel.Get(cluster).GetOrElse(8));
+ TransferData(reader.Get(), &compressor);
+ compressor.Finish();
+ out->Finish();
+ } catch (const yexception& e) {
+ YQL_CLOG(ERROR, ProviderYt) << "Error transferring " << richYPathDesc << " to " << remotePath << ": " << e.what();
+ if (reader->Retry(Nothing(), Nothing())) {
+ continue;
+ }
+ throw;
+ }
+ break;
+ }
+ entry->DeleteAtFinalize(remotePath);
+ YQL_CLOG(DEBUG, ProviderYt) << "Passing table " << richYPathDesc << " as remote file " << remotePath.Quote();
+
+ RemoteFiles_->push_back(NYT::TRichYPath(NYT::AddPathPrefix(remotePath, NYT::TConfig::Get()->Prefix)).FileName(fileName));
+
+ } else {
+ TString outPath = TmpFiles_.AddFile(fileName);
+
+ if (PublicId_) {
+ auto progress = TOperationProgress(TString(YtProviderName), *PublicId_,
+ TOperationProgress::EState::InProgress, "Preparing table content");
+ ExecCtx_.Session_->ProgressWriter_(progress);
+ }
+ while (true) {
+ try {
+ TOFStream out(outPath);
+ out.SetFinishPropagateMode(false);
+ out.SetFlushPropagateMode(false);
+ TBrotliCompress compressor(&out, Settings_->TableContentCompressLevel.Get(cluster).GetOrElse(8));
+ TransferData(reader.Get(), &compressor);
+ compressor.Finish();
+ out.Finish();
+ } catch (const TIoException& e) {
+ YQL_CLOG(ERROR, ProviderYt) << "Error reading " << richYPathDesc << ": " << e.what();
+ // Don't retry IO errors
+ throw;
+ } catch (const yexception& e) {
+ YQL_CLOG(ERROR, ProviderYt) << "Error reading " << richYPathDesc << ": " << e.what();
+ if (reader->Retry(Nothing(), Nothing())) {
+ continue;
+ }
+ throw;
+ }
+ break;
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Passing table " << richYPathDesc << " as file "
+ << fileName.Quote() << " (size=" << TFileStat(outPath).Size << ')';
+
+ LocalFiles_->emplace_back(outPath, TString());
+ }
+ }
+ }
+
+ TCallableBuilder call(env,
+ TStringBuilder() << TYtTableContent::CallableName() << TStringBuf("Job"),
+ callable.GetType()->GetReturnType());
+
+ call.Add(PgmBuilder_.NewDataLiteral<NUdf::EDataSlot::String>(uniqueId));
+ call.Add(PgmBuilder_.NewDataLiteral(tableList->GetItemsCount()));
+ call.Add(PgmBuilder_.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(specNode)));
+ call.Add(PgmBuilder_.NewDataLiteral(useSkiff));
+ call.Add(PgmBuilder_.NewDataLiteral(ETableContentDeliveryMode::File == deliveryMode)); // use compression
+ call.Add(callable.GetInput(3)); // length
+ return TRuntimeNode(call.Build(), false);
+ };
+ }
+
+ if (name == TStringBuf("Udf") || name == TStringBuf("ScriptUdf")) {
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& /*env*/) {
+ YQL_ENSURE(callable.GetInputsCount() > 0, "Expected at least one argument");
+ const TString udfName(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ const auto moduleName = ModuleName(udfName);
+
+ *UntrustedUdfFlag_ = *UntrustedUdfFlag_ ||
+ callable.GetType()->GetName() == TStringBuf("ScriptUdf") ||
+ !ExecCtx_.FunctionRegistry_->IsLoadedUdfModule(moduleName) ||
+ moduleName == TStringBuf("Geo");
+
+ const auto udfPath = FindUdfPath(moduleName);
+ if (!udfPath.StartsWith(NMiniKQL::StaticModulePrefix)) {
+ const auto fileInfo = ExecCtx_.UserFiles_->GetFile(udfPath);
+ YQL_ENSURE(fileInfo, "Unknown udf path " << udfPath);
+ AddFile(udfPath, *fileInfo, FindUdfPrefix(moduleName));
+ }
+
+ if (moduleName == TStringBuf("Geo")) {
+ if (const auto fileInfo = ExecCtx_.UserFiles_->GetFile("/home/geodata6.bin")) {
+ AddFile("./geodata6.bin", *fileInfo);
+ }
+ }
+
+ return TRuntimeNode(&callable, false);
+ };
+ }
+
+ if (name == TStringBuf("FilePath") || name == TStringBuf("FileContent") || name == TStringBuf("FolderPath")) {
+ if (name == TStringBuf("FolderPath")) {
+ *RemoteExecutionFlag_ = true;
+ }
+ return [&](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ YQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 arguments");
+ const TString fileName(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ const TString fullFileName = TUserDataStorage::MakeFullName(fileName);
+
+ auto callableName = callable.GetType()->GetName();
+ if (callableName == TStringBuf("FolderPath")) {
+ TVector<TString> files;
+ if (!ExecCtx_.UserFiles_->FindFolder(fullFileName, files)) {
+ ythrow yexception() << "Folder not found: " << fullFileName;
+ }
+ for (const auto& x : files) {
+ auto fileInfo = ExecCtx_.UserFiles_->GetFile(x);
+ YQL_ENSURE(fileInfo, "File not found: " << x);
+ AddFile(x, *fileInfo);
+ }
+ } else {
+ auto fileInfo = ExecCtx_.UserFiles_->GetFile(fullFileName);
+ YQL_ENSURE(fileInfo, "File not found: " << fullFileName);
+ AddFile(fileName, *fileInfo);
+ }
+
+ TStringBuilder jobCallable;
+ if (callableName == TStringBuf("FolderPath")) {
+ jobCallable << "FilePath";
+ } else {
+ jobCallable << callableName;
+ }
+
+ TCallableBuilder builder(env, jobCallable << TStringBuf("Job"),
+ callable.GetType()->GetReturnType(), false);
+ builder.Add(PgmBuilder_.NewDataLiteral<NUdf::EDataSlot::String>(fullFileName));
+ return TRuntimeNode(builder.Build(), false);
+ };
+ }
+
+ if (name == TYtTablePath::CallableName()
+ || name == TYtTableIndex::CallableName()
+ || name == TYtTableRecord::CallableName()
+ || name == TYtIsKeySwitch::CallableName()
+ || name == TYtRowNumber::CallableName())
+ {
+ *RemoteExecutionFlag_ = true;
+ }
+
+ return TCallableVisitFunc();
+}
+
+void TGatewayTransformer::ApplyJobProps(TYqlJobBase& job) {
+ for (auto& x: *JobFileAliases_) {
+ job.AddFileAlias(x.first, x.second);
+ }
+ JobFileAliases_->clear();
+
+ for (auto& x: *JobUdfs_) {
+ job.AddUdfModule(x.first, x.second);
+ }
+ JobUdfs_->clear();
+ UniqFiles_->clear();
+}
+
+void TGatewayTransformer::ApplyUserJobSpec(NYT::TUserJobSpec& spec, bool localRun) {
+ if (!RemoteFiles_->empty()) {
+ YQL_ENSURE(!localRun, "Unexpected remote files");
+ for (auto& file: *RemoteFiles_) {
+ spec.AddFile(file);
+ }
+ }
+ bool fakeChecksum = (GetEnv("YQL_LOCAL") == "1"); // YQL-15353
+ for (auto& file: *LocalFiles_) {
+ TAddLocalFileOptions opts;
+ if (!fakeChecksum && file.second) {
+ opts.MD5CheckSum(file.second);
+ }
+ spec.AddLocalFile(file.first, opts);
+ }
+ const TString binTmpFolder = Settings_->BinaryTmpFolder.Get().GetOrElse(TString());
+ if (localRun || !binTmpFolder) {
+ for (auto& file: *DeferredUdfFiles_) {
+ TAddLocalFileOptions opts;
+ if (!fakeChecksum && file.second) {
+ opts.MD5CheckSum(file.second);
+ }
+ YQL_ENSURE(TFileStat(file.first).Size != 0);
+ spec.AddLocalFile(file.first, opts);
+ }
+ } else {
+ const TDuration binExpiration = Settings_->BinaryExpirationInterval.Get().GetOrElse(TDuration());
+ auto entry = GetEntry();
+ for (auto& file: *DeferredUdfFiles_) {
+ YQL_ENSURE(TFileStat(file.first).Size != 0);
+ auto snapshot = entry->GetBinarySnapshot(binTmpFolder, file.second, file.first, binExpiration);
+ spec.AddFile(TRichYPath(snapshot.first).TransactionId(snapshot.second).FileName(TFsPath(file.first).GetName()).Executable(true));
+ }
+ }
+ RemoteFiles_->clear();
+ LocalFiles_->clear();
+ DeferredUdfFiles_->clear();
+}
+
+NYT::ITransactionPtr TGatewayTransformer::GetTx() {
+ return GetEntry()->Tx;
+}
+
+TTransactionCache::TEntry::TPtr TGatewayTransformer::GetEntry() {
+ if (!Entry_) {
+ Entry_ = ExecCtx_.GetOrCreateEntry(Settings_);
+ }
+ return Entry_;
+}
+
+void TGatewayTransformer::AddFile(TString alias,
+ const TUserFiles::TFileInfo& fileInfo, const TString& udfPrefix) {
+ if (alias.StartsWith('/')) {
+ alias = alias.substr(1);
+ }
+ if (alias.StartsWith(TStringBuf("home/"))) {
+ alias = alias.substr(TStringBuf("home/").length());
+ }
+
+ TString basename;
+ if (fileInfo.Path) {
+ // Pass only unique files to YT
+ auto insertRes = UniqFiles_->insert({fileInfo.Path->GetMd5(), fileInfo.Path->GetPath()});
+ TString filePath;
+ if (insertRes.second) {
+ filePath = fileInfo.Path->GetPath();
+ *UsedMem_ += fileInfo.InMemorySize;
+ if (fileInfo.IsUdf) {
+ DeferredUdfFiles_->emplace_back(filePath, fileInfo.Path->GetMd5());
+ } else {
+ LocalFiles_->emplace_back(filePath, fileInfo.Path->GetMd5());
+ }
+ } else {
+ filePath = insertRes.first->second;
+ }
+
+ basename = TFsPath(filePath).GetName();
+ if (alias && alias != basename) {
+ JobFileAliases_->insert({alias, basename});
+ }
+
+ } else {
+ *RemoteExecutionFlag_ = true;
+ TString filePath = NYT::AddPathPrefix(fileInfo.RemotePath, NYT::TConfig::Get()->Prefix);
+ TRichYPath remoteFile(filePath);
+ if (alias) {
+ remoteFile.FileName(alias);
+ filePath = alias;
+ } else {
+ alias = TFsPath(filePath).GetName();
+ }
+ auto insertRes = UniqFiles_->insert({alias, remoteFile.Path_});
+ if (insertRes.second) {
+ RemoteFiles_->push_back(remoteFile.Executable(true));
+ if (fileInfo.RemoteMemoryFactor > 0.) {
+ *UsedMem_ += fileInfo.RemoteMemoryFactor * GetUncompressedFileSize(GetTx(), remoteFile.Path_).GetOrElse(ui64(1) << 10);
+ }
+ } else {
+ YQL_ENSURE(remoteFile.Path_ == insertRes.first->second, "Duplicate file alias " << alias.Quote()
+ << " for different files " << remoteFile.Path_.Quote() << " and " << insertRes.first->second.Quote());
+ }
+ basename = TFsPath(filePath).GetName();
+ }
+
+ if (fileInfo.IsUdf) {
+ if (alias) {
+ JobUdfs_->insert({"./" + alias, udfPrefix});
+ } else {
+ JobUdfs_->insert({"./" + basename, udfPrefix});
+ }
+ }
+}
+
+TString TGatewayTransformer::FindUdfPath(const TStringBuf moduleName) const {
+ if (auto udfInfo = UdfModules_.FindPtr(moduleName)) {
+ return TUserDataStorage::MakeFullName(udfInfo->FileAlias);
+ }
+
+ TMaybe<TFilePathWithMd5> udfPathWithMd5 = UdfResolver_->GetSystemModulePath(moduleName);
+ YQL_ENSURE(udfPathWithMd5.Defined());
+ return TFsPath(udfPathWithMd5->Path).GetName();
+}
+
+TString TGatewayTransformer::FindUdfPrefix(const TStringBuf moduleName) const {
+ if (auto udfInfo = UdfModules_.FindPtr(moduleName)) {
+ return udfInfo->Prefix;
+ }
+ return TString();
+}
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.h b/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.h
new file mode 100644
index 0000000000..70db8115aa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/native/yql_yt_transform.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include "yql_yt_exec_ctx.h"
+
+#include <ydb/library/yql/providers/yt/gateway/lib/temp_files.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/transaction_cache.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/user_files.h>
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/yt/job/yql_job_base.h>
+
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/core/yql_udf_resolver.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_visitor.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/ptr.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+
+namespace NYql {
+
+namespace NNative {
+
+class TGatewayTransformer {
+public:
+ TGatewayTransformer(const TExecContextBase& execCtx, TYtSettings::TConstPtr settings, const TString& optLLVM,
+ TUdfModulesTable udfModules, IUdfResolver::TPtr udfResolver, TTransactionCache::TEntry::TPtr entry,
+ NKikimr::NMiniKQL::TProgramBuilder& builder, TTempFiles& tmpFiles, TMaybe<ui32> publicId);
+
+ template <class TExecContextPtr>
+ TGatewayTransformer(const TExecContextPtr& execCtx, TTransactionCache::TEntry::TPtr entry,
+ NKikimr::NMiniKQL::TProgramBuilder& builder, TTempFiles& tmpFiles)
+ : TGatewayTransformer(*execCtx, execCtx->Options_.Config(), execCtx->Options_.OptLLVM(),
+ execCtx->Options_.UdfModules(), execCtx->Options_.UdfResolver(), std::move(entry),
+ builder, tmpFiles, execCtx->Options_.PublicId())
+ {
+ }
+
+ NKikimr::NMiniKQL::TCallableVisitFunc operator()(NKikimr::NMiniKQL::TInternName name);
+
+ inline bool CanExecuteInternally() const {
+ return !*RemoteExecutionFlag_ && !*UntrustedUdfFlag_;
+ }
+
+ inline bool CanExecuteLocally() const {
+ return !*RemoteExecutionFlag_;
+ }
+
+ inline ui64 GetUsedMemory() const {
+ return *UsedMem_;
+ }
+
+ void ApplyJobProps(TYqlJobBase& job);
+ void ApplyUserJobSpec(NYT::TUserJobSpec& spec, bool localRun);
+
+private:
+ NYT::ITransactionPtr GetTx();
+ TTransactionCache::TEntry::TPtr GetEntry();
+ void AddFile(TString alias, const TUserFiles::TFileInfo& fileInfo, const TString& udfPrefix = {});
+ TString FindUdfPath(const TStringBuf moduleName) const;
+ TString FindUdfPrefix(const TStringBuf moduleName) const;
+
+private:
+ const TExecContextBase& ExecCtx_;
+ TYtSettings::TConstPtr Settings_;
+ TUdfModulesTable UdfModules_;
+ IUdfResolver::TPtr UdfResolver_;
+
+ TTransactionCache::TEntry::TPtr Entry_;
+ NKikimr::NMiniKQL::TProgramBuilder& PgmBuilder_;
+ TTempFiles& TmpFiles_;
+ TMaybe<ui32> PublicId_;
+
+ // Wrap to shared ptr because TGatewayTransformer is passed by value
+ std::shared_ptr<bool> RemoteExecutionFlag_;
+ std::shared_ptr<bool> UntrustedUdfFlag_;
+ std::shared_ptr<ui64> UsedMem_;
+ std::shared_ptr<THashMap<TString, TString>> JobFileAliases_;
+ std::shared_ptr<THashMap<TString, TString>> JobUdfs_;
+ std::shared_ptr<THashMap<TString, TString>> UniqFiles_;
+ std::shared_ptr<TVector<NYT::TRichYPath>> RemoteFiles_;
+ std::shared_ptr<TVector<std::pair<TString, TString>>> LocalFiles_;
+ std::shared_ptr<TVector<std::pair<TString, TString>>> DeferredUdfFiles_;
+};
+
+} // NNative
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/gateway/profile/ya.make b/ydb/library/yql/providers/yt/gateway/profile/ya.make
new file mode 100644
index 0000000000..3132b176fa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/profile/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_profiling.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/utils/log
+ ydb/library/yql/providers/yt/provider
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.cpp b/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.cpp
new file mode 100644
index 0000000000..dd9a1d2645
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.cpp
@@ -0,0 +1,172 @@
+#include "yql_yt_profiling.h"
+
+#include <ydb/library/yql/utils/log/profile.h>
+
+#include <util/generic/ptr.h>
+
+using namespace NThreading;
+
+namespace NYql {
+namespace {
+
+class TProfilingYtGateway final: public IYtGateway {
+public:
+ TProfilingYtGateway(IYtGateway::TPtr&& slave)
+ : Slave_(std::move(slave))
+ {
+ }
+
+ void OpenSession(TOpenSessionOptions&& options) final {
+ YQL_PROFILE_FUNC(TRACE);
+ Slave_->OpenSession(std::move(options));
+ }
+
+ void CloseSession(TCloseSessionOptions&& options) final {
+ YQL_PROFILE_FUNC(TRACE);
+ Slave_->CloseSession(std::move(options));
+ }
+
+ void CleanupSession(TCleanupSessionOptions&& options) final {
+ YQL_PROFILE_FUNC(TRACE);
+ Slave_->CleanupSession(std::move(options));
+ }
+
+ TFuture<TFinalizeResult> Finalize(TFinalizeOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Finalize(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TCanonizePathsResult> CanonizePaths(TCanonizePathsOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->CanonizePaths(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TTableInfoResult> GetTableInfo(TGetTableInfoOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->GetTableInfo(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TTableRangeResult> GetTableRange(TTableRangeOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->GetTableRange(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TFolderResult> GetFolder(TFolderOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->GetFolder(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TResOrPullResult> ResOrPull(const TExprNode::TPtr& node, TExprContext& ctx, TResOrPullOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->ResOrPull(node, ctx, std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TRunResult> Run(const TExprNode::TPtr& node, TExprContext& ctx, TRunOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Run(node, ctx, std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TRunResult> Prepare(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) const final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Prepare(node, ctx, std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TCalcResult> Calc(const TExprNode::TListType& nodes, TExprContext& ctx, TCalcOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Calc(nodes, ctx, std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TPublishResult> Publish(const TExprNode::TPtr& node, TExprContext& ctx, TPublishOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Publish(node, ctx, std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TCommitResult> Commit(TCommitOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->Commit(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TDropTrackablesResult> DropTrackables(TDropTrackablesOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->DropTrackables(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TFuture<TPathStatResult> PathStat(TPathStatOptions&& options) final {
+ auto profileScope = YQL_PROFILE_FUNC_VAL(TRACE);
+ auto future = Slave_->PathStat(std::move(options));
+ return YQL_PROFILE_BIND_VAL(future, profileScope);
+ }
+
+ TPathStatResult TryPathStat(TPathStatOptions&& options) final {
+ YQL_PROFILE_FUNC(TRACE);
+ return Slave_->TryPathStat(std::move(options));
+ }
+
+ bool TryParseYtUrl(const TString& url, TString* cluster, TString* path) const final {
+ YQL_PROFILE_FUNC(TRACE);
+ return Slave_->TryParseYtUrl(url, cluster, path);
+ }
+
+ TString GetDefaultClusterName() const final {
+ YQL_PROFILE_FUNC(TRACE);
+ return Slave_->GetDefaultClusterName();
+ }
+
+ TString GetClusterServer(const TString& cluster) const final {
+ return Slave_->GetClusterServer(cluster);
+ }
+
+ NYT::TRichYPath GetRealTable(const TString& sessionId, const TString& cluster, const TString& table, ui32 epoch, const TString& tmpFolder) const final {
+ return Slave_->GetRealTable(sessionId, cluster, table, epoch, tmpFolder);
+ }
+
+ NYT::TRichYPath GetWriteTable(const TString& sessionId, const TString& cluster, const TString& table, const TString& tmpFolder) const final {
+ return Slave_->GetWriteTable(sessionId, cluster, table, tmpFolder);
+ }
+
+ NThreading::TFuture<TRunResult> GetTableStat(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) final {
+ return Slave_->GetTableStat(node, ctx, std::move(options));
+ }
+
+ TFullResultTableResult PrepareFullResultTable(TFullResultTableOptions&& options) final {
+ YQL_PROFILE_FUNC(TRACE);
+ return Slave_->PrepareFullResultTable(std::move(options));
+ }
+
+ void SetStatUploader(IStatUploader::TPtr statUploader) final {
+ YQL_PROFILE_FUNC(TRACE);
+ Slave_->SetStatUploader(statUploader);
+ }
+
+ void RegisterMkqlCompiler(NCommon::TMkqlCallableCompilerBase& compiler) final {
+ Y_UNUSED(compiler);
+ }
+
+ TGetTablePartitionsResult GetTablePartitions(TGetTablePartitionsOptions&& options) override {
+ YQL_PROFILE_FUNC(TRACE);
+ return Slave_->GetTablePartitions(std::move(options));
+ }
+
+private:
+ IYtGateway::TPtr Slave_;
+};
+
+} // namespace
+
+IYtGateway::TPtr CreateYtProfilingGateway(IYtGateway::TPtr slave) {
+ return MakeIntrusive<TProfilingYtGateway>(std::move(slave));
+}
+
+} // namspace NYql
diff --git a/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.h b/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.h
new file mode 100644
index 0000000000..7058295499
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/profile/yql_yt_profiling.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_gateway.h>
+
+namespace NYql {
+
+IYtGateway::TPtr CreateYtProfilingGateway(IYtGateway::TPtr slave);
+
+} // namspace NYql
diff --git a/ydb/library/yql/providers/yt/gateway/ya.make b/ydb/library/yql/providers/yt/gateway/ya.make
new file mode 100644
index 0000000000..053aebfabe
--- /dev/null
+++ b/ydb/library/yql/providers/yt/gateway/ya.make
@@ -0,0 +1,6 @@
+RECURSE(
+ file
+ lib
+ native
+ profile
+)
diff --git a/ydb/library/yql/providers/yt/job/ya.make b/ydb/library/yql/providers/yt/job/ya.make
new file mode 100644
index 0000000000..f921cbc5f3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/ya.make
@@ -0,0 +1,40 @@
+LIBRARY()
+
+SRCS(
+ yql_job_base.cpp
+ yql_job_calc.cpp
+ yql_job_factory.cpp
+ yql_job_infer_schema.cpp
+ yql_job_registry.h
+ yql_job_stats_writer.cpp
+ yql_job_table_content.cpp
+ yql_job_user.cpp
+)
+
+PEERDIR(
+ library/cpp/random_provider
+ library/cpp/streams/brotli
+ library/cpp/time_provider
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ library/cpp/yt/user_job_statistics
+ ydb/library/yql/minikql/comp_nodes/llvm
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/utils/backtrace
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/comp_nodes
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/common/schema/mkql
+ ydb/library/yql/providers/common/schema/parser
+ ydb/library/yql/providers/yt/codec
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/comp_nodes
+ ydb/library/yql/providers/yt/lib/infer_schema
+ ydb/library/yql/providers/yt/lib/lambda_builder
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/job/yql_job_base.cpp b/ydb/library/yql/providers/yt/job/yql_job_base.cpp
new file mode 100644
index 0000000000..73eff1eac2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_base.cpp
@@ -0,0 +1,298 @@
+#include "yql_job_base.h"
+#include "yql_job_stats_writer.h"
+#include "yql_job_factory.h"
+
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/utils/backtrace/backtrace.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/debug_info.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <util/generic/yexception.h>
+#include <util/folder/path.h>
+#include <util/system/rusage.h>
+#include <util/system/env.h>
+#include <util/system/fs.h>
+#include <util/system/error.h>
+#include <util/system/datetime.h>
+#include <util/datetime/cputimer.h>
+#include <util/ysaveload.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+TStatKey Job_ThreadsCount("Job_ThreadsCount", false);
+TStatKey Job_ElapsedTime("Job_ElapsedTime", false);
+TStatKey Job_UserTime("Job_UserTime", false);
+TStatKey Job_SystemTime("Job_SystemTime", false);
+TStatKey Job_MajorPageFaults("Job_MajorPageFaults", false);
+
+class TEnvSecureParamsProvider : public NUdf::ISecureParamsProvider {
+public:
+ TEnvSecureParamsProvider(const TString& envName)
+ {
+ const TString& yson = GetEnv(envName + "_secure_params");
+ // Absent variable is not an error
+ if (!yson)
+ return;
+ auto attrs = NYT::NodeFromYsonString(yson);
+ YQL_ENSURE(attrs.IsMap());
+
+ SecureMap = attrs.AsMap();
+ }
+
+ bool GetSecureParam(NUdf::TStringRef key, NUdf::TStringRef& value) const override {
+ auto p = SecureMap.FindPtr(TString(key.Data(), key.Size()));
+ if (!p)
+ return false;
+ if (!p->IsString())
+ return false;
+
+ value = p->AsString();
+ return true;
+ }
+
+private:
+ NYT::TNode::TMapType SecureMap;
+};
+
+NKikimr::NUdf::TCounter TJobCountersProvider::GetCounter(const NKikimr::NUdf::TStringRef& module,
+ const NKikimr::NUdf::TStringRef& name, bool deriv) {
+ Y_UNUSED(deriv);
+ auto fullName = std::make_pair(TString(TStringBuf(module)), TString(TStringBuf(name)));
+ return NKikimr::NUdf::TCounter(&Counters_[fullName]);
+}
+
+NKikimr::NUdf::TScopedProbe TJobCountersProvider::GetScopedProbe(const NKikimr::NUdf::TStringRef& module,
+ const NKikimr::NUdf::TStringRef& name) {
+ auto fullName = std::make_pair(TString(TStringBuf(module)), TString(TStringBuf(name)));
+ return NKikimr::NUdf::TScopedProbe(this, &Probes_[fullName]);
+}
+
+void TJobCountersProvider::Acquire(void* cookie) {
+ auto state = (TProbeState*)cookie;
+ state->LastAcquire = GetCycleCount();
+}
+
+void TJobCountersProvider::Release(void* cookie) {
+ auto state = (TProbeState*)cookie;
+ state->TotalCycles += GetCycleCount() - state->LastAcquire;
+}
+
+TString MakeLocalPath(TString fileName) {
+ TString localPath = fileName;
+ if (localPath.StartsWith(TStringBuf("/home/"))) {
+ localPath = localPath.substr(TStringBuf("/home").length()); // Keep leading slash
+ }
+ if (!localPath.StartsWith('/')) {
+ localPath.prepend('/');
+ }
+ localPath.prepend('.');
+ return localPath;
+}
+
+class TJobTransformProvider {
+public:
+ TJobTransformProvider(THashMap<TString, TRuntimeNode>* extraArgs)
+ : ExtraArgs(extraArgs)
+ {
+ }
+
+ TCallableVisitFunc operator()(TInternName name) {
+ if (name == "FilePathJob") {
+ return [](TCallable& callable, const TTypeEnvironment& env) {
+ YQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 argument");
+ const TString path(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ return TRuntimeNode(BuildDataLiteral(MakeLocalPath(path), NUdf::TDataType<char*>::Id, env), true);
+ };
+ }
+
+ if (name == "FileContentJob") {
+ return [](TCallable& callable, const TTypeEnvironment& env) {
+ YQL_ENSURE(callable.GetInputsCount() == 1, "Expected 1 argument");
+ const TString path(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ auto content = TFileInput(MakeLocalPath(path)).ReadAll();
+ return TRuntimeNode(BuildDataLiteral(content, NUdf::TDataType<char*>::Id, env), true);
+ };
+ }
+
+ auto cutName = name.Str();
+ if (cutName.SkipPrefix("Yt")) {
+ if (cutName == "TableIndex" || cutName == "TablePath" || cutName == "TableRecord" || cutName == "IsKeySwitch" || cutName == "RowNumber") {
+ return [this](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ return GetExtraArg(TString{callable.GetType()->GetName()},
+ *AS_TYPE(TDataType, callable.GetType()->GetReturnType())->GetDataSlot(), env);
+ };
+ }
+ if (cutName == "Input") {
+ // Rename and add additional args
+ return [this](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env,
+ TStringBuilder() << callable.GetType()->GetName() << "Job", callable.GetType()->GetReturnType(), false);
+
+ if (const auto args = callable.GetInputsCount()) {
+ callableBuilder.Add(GetExtraArg("YtTableIndex", NUdf::EDataSlot::Uint32, env));
+ callableBuilder.Add(GetExtraArg("YtTablePath", NUdf::EDataSlot::String, env));
+ callableBuilder.Add(GetExtraArg("YtTableRecord", NUdf::EDataSlot::Uint64, env));
+ callableBuilder.Add(GetExtraArg("YtIsKeySwitch", NUdf::EDataSlot::Bool, env));
+ callableBuilder.Add(GetExtraArg("YtRowNumber", NUdf::EDataSlot::Uint64, env));
+ for (ui32 i: xrange(args)) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ }
+
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+ if (cutName == "Output") {
+ // Rename
+ return [](NMiniKQL::TCallable& callable, const TTypeEnvironment& env) {
+ TCallableBuilder callableBuilder(env,
+ TStringBuilder() << callable.GetType()->GetName() << "Job", callable.GetType()->GetReturnType(), false);
+ for (ui32 i: xrange(callable.GetInputsCount())) {
+ callableBuilder.Add(callable.GetInput(i));
+ }
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+ }
+
+ return TCallableVisitFunc();
+ }
+
+private:
+ TRuntimeNode GetExtraArg(const TString& name, NUdf::EDataSlot slot, const TTypeEnvironment& env) {
+ YQL_ENSURE(ExtraArgs, "Unexpected " << name << " usage");
+ TRuntimeNode& node = (*ExtraArgs)[name];
+ if (!node) {
+ TCallableBuilder builder(env, "Arg", TDataType::Create(NUdf::GetDataTypeInfo(slot).TypeId, env), true);
+ node = TRuntimeNode(builder.Build(), false);
+ }
+ return node;
+ }
+
+private:
+ THashMap<TString, TRuntimeNode>* ExtraArgs;
+};
+
+TYqlJobBase::~TYqlJobBase() {
+ try {
+ if (JobStats) {
+ JobStats->SetStat(Job_ElapsedTime, (GetCycleCount() - StartCycles) / GetCyclesPerMillisecond());
+ TRusage ru;
+ ru.Fill();
+ JobStats->SetStat(Job_UserTime, ru.Utime.MilliSeconds());
+ JobStats->SetStat(Job_SystemTime, ru.Stime.MilliSeconds());
+ JobStats->SetStat(Job_MajorPageFaults, ru.MajorPageFaults);
+
+ WriteJobStats(JobStats.Get(), JobCountersProvider);
+ }
+ } catch (...) {
+ /* do not throw exceptions in destructor */
+ }
+}
+
+void TYqlJobBase::Init() {
+ StartCycles = GetCycleCount();
+ StartTime = ThreadCPUTime();
+
+ auto funcRegistry = CreateFunctionRegistry(CreateBuiltinRegistry())->Clone();
+ funcRegistry->SetBackTraceCallback(&NYql::NBacktrace::KikimrBackTrace);
+ if (GetEnv(TString("YQL_DETERMINISTIC_MODE"))) {
+ RandomProvider = CreateDeterministicRandomProvider(1);
+ TimeProvider = CreateDeterministicTimeProvider(10000000);
+ }
+ else {
+ RandomProvider = CreateDefaultRandomProvider();
+ TimeProvider = CreateDefaultTimeProvider();
+ }
+
+ const bool hasTmpfs = TFsPath("_yql_tmpfs").IsDirectory();
+ Y_UNUSED(hasTmpfs); // _win_ compiler
+
+ for (const auto& a: FileAliases) {
+ TFsPath lnk = TFsPath(".") / a.first;
+ TFsPath parent = lnk.Parent();
+ if (!parent.Exists()) {
+ parent.MkDirs();
+ } else {
+ // Local mode support. Overwrite existing link if any
+ if (lnk.IsSymlink()) {
+ lnk.DeleteIfExists();
+ }
+ }
+#ifndef _win_
+ TFsPath lnkTarget = TFsPath(".").RealPath() / a.second;
+ if (hasTmpfs && !a.first.EndsWith(TStringBuf(".debug"))) {
+ NFs::Copy(lnkTarget, TFsPath("_yql_tmpfs") / a.second);
+ lnkTarget = TFsPath("_yql_tmpfs") / a.second;
+ }
+ YQL_ENSURE(NFs::SymLink(lnkTarget, lnk), "Failed to create file alias from "
+ << lnkTarget.GetPath().Quote() << " to " << lnk.GetPath().Quote()
+ << ": " << LastSystemErrorText());
+#endif
+ }
+
+ FillStaticModules(*funcRegistry);
+ for (const auto& mod: UdfModules) {
+ auto path = mod.first;
+#ifdef _win_
+ path += ".dll";
+#endif
+ funcRegistry->LoadUdfs(path, {}, 0, mod.second);
+ }
+
+ FunctionRegistry.Reset(funcRegistry.Release());
+
+ Alloc.Reset(new TScopedAlloc(__LOCATION__, NKikimr::TAlignedPagePoolCounters(),
+ FunctionRegistry->SupportsSizedAllocators()));
+ Env.Reset(new TTypeEnvironment(*Alloc));
+ CodecCtx.Reset(new NCommon::TCodecContext(*Env, *FunctionRegistry));
+ if (!GetEnv(TString("YQL_SUPPRESS_JOB_STATISTIC"))) {
+ JobStats = CreateDefaultStatsRegistry();
+ }
+ SecureParamsProvider.Reset(new TEnvSecureParamsProvider("YT_SECURE_VAULT"));
+}
+
+void TYqlJobBase::Save(IOutputStream& s) const {
+ ::SaveMany(&s,
+ UdfModules,
+ FileAliases,
+ UdfValidateMode,
+ OptLLVM,
+ TableNames
+ );
+}
+
+void TYqlJobBase::Load(IInputStream& s) {
+ ::LoadMany(&s,
+ UdfModules,
+ FileAliases,
+ UdfValidateMode,
+ OptLLVM,
+ TableNames
+ );
+}
+
+void TYqlJobBase::Do(const NYT::TRawJobContext& jobContext) {
+ DoImpl(jobContext.GetInputFile(), jobContext.GetOutputFileList());
+ if (JobStats) {
+ JobStats->SetStat(Job_ThreadsCount, GetRunnigThreadsCount());
+ }
+}
+
+TCallableVisitFuncProvider TYqlJobBase::MakeTransformProvider(THashMap<TString, TRuntimeNode>* extraArgs) const {
+ return TJobTransformProvider(extraArgs);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_base.h b/ydb/library/yql/providers/yt/job/yql_job_base.h
new file mode 100644
index 0000000000..74d22095f1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_base.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/public/udf/udf_validate.h>
+#include <ydb/library/yql/public/udf/udf_counter.h>
+#include <ydb/library/yql/minikql/mkql_node_visitor.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/io/job_reader.h>
+#include <yt/cpp/mapreduce/io/job_writer.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/ptr.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/hash.h>
+
+
+namespace NKikimr {
+ namespace NMiniKQL {
+ class IFunctionRegistry;
+ }
+}
+
+class IRandomProvider;
+class ITimeProvider;
+
+namespace NYql {
+
+struct TJobCountersProvider : public NKikimr::NUdf::ICountersProvider, public NKikimr::NUdf::IScopedProbeHost {
+ NKikimr::NUdf::TCounter GetCounter(const NKikimr::NUdf::TStringRef& module, const NKikimr::NUdf::TStringRef& name, bool deriv) override;
+ NKikimr::NUdf::TScopedProbe GetScopedProbe(const NKikimr::NUdf::TStringRef& module, const NKikimr::NUdf::TStringRef& name) override;
+ void Acquire(void* cookie) override;
+ void Release(void* cookie) override;
+
+ struct TProbeState {
+ i64 TotalCycles = 0;
+ i64 LastAcquire;
+ };
+
+ THashMap<std::pair<TString, TString>, i64> Counters_;
+ THashMap<std::pair<TString, TString>, TProbeState> Probes_;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYqlJobBase: public NYT::IRawJob {
+protected:
+ TYqlJobBase() = default;
+ virtual ~TYqlJobBase();
+
+public:
+ void AddUdfModule(const TString& udfModule, const TString& udfPrefix) {
+ UdfModules.insert({udfModule, udfPrefix});
+ }
+ void AddFileAlias(const TString& alias, const TString& filePath) {
+ FileAliases[alias] = filePath;
+ }
+
+ void SetUdfValidateMode(NKikimr::NUdf::EValidateMode mode) {
+ UdfValidateMode = mode;
+ }
+
+ void SetOptLLVM(const TString& optLLVM) {
+ OptLLVM = optLLVM;
+ }
+
+ void SetTableNames(const TVector<TString>& tableNames) {
+ TableNames = tableNames;
+ }
+
+ void Do(const NYT::TRawJobContext& jobContext) override;
+ void Save(IOutputStream& stream) const override;
+ void Load(IInputStream& stream) override;
+
+protected:
+ NKikimr::NMiniKQL::TCallableVisitFuncProvider MakeTransformProvider(THashMap<TString, NKikimr::NMiniKQL::TRuntimeNode>* extraArgs = nullptr) const;
+
+ void Init();
+
+ virtual void DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) = 0;
+
+protected:
+ // Serializable part (don't forget to add new members to Save/Load)
+ THashMap<TString, TString> UdfModules; // udf module path -> udf module prefix
+ THashMap<TString, TString> FileAliases;
+ NKikimr::NUdf::EValidateMode UdfValidateMode = NKikimr::NUdf::EValidateMode::None;
+ TString OptLLVM;
+ TVector<TString> TableNames;
+ // End serializable part
+
+ ui64 StartCycles = 0;
+ ui64 StartTime = 0;
+ TIntrusivePtr<NKikimr::NMiniKQL::IFunctionRegistry> FunctionRegistry;
+ TIntrusivePtr<IRandomProvider> RandomProvider;
+ TIntrusivePtr<ITimeProvider> TimeProvider;
+ THolder<NKikimr::NMiniKQL::TScopedAlloc> Alloc;
+ THolder<NKikimr::NMiniKQL::TTypeEnvironment> Env;
+ NKikimr::NMiniKQL::IStatsRegistryPtr JobStats;
+ TJobCountersProvider JobCountersProvider;
+ THolder<NKikimr::NUdf::ISecureParamsProvider> SecureParamsProvider;
+ THolder<NCommon::TCodecContext> CodecCtx;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_calc.cpp b/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
new file mode 100644
index 0000000000..3b8f3c4081
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_calc.cpp
@@ -0,0 +1,105 @@
+#include "yql_job_calc.h"
+#include "yql_job_factory.h"
+
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_node_serialization.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+#include <yt/cpp/mapreduce/io/node_table_reader.h>
+#include <yt/cpp/mapreduce/io/node_table_writer.h>
+
+#include <library/cpp/streams/brotli/brotli.h>
+
+#include <util/generic/xrange.h>
+#include <util/generic/yexception.h>
+#include <util/stream/str.h>
+#include <util/stream/input.h>
+#include <util/ysaveload.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+void TYqlCalcJob::Save(IOutputStream& stream) const {
+ TYqlJobBase::Save(stream);
+ ::Save(&stream, Columns_);
+ ::Save(&stream, UseResultYson_);
+}
+
+void TYqlCalcJob::Load(IInputStream& stream) {
+ TYqlJobBase::Load(stream);
+ ::Load(&stream, Columns_);
+ ::Load(&stream, UseResultYson_);
+}
+
+void TYqlCalcJob::DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) {
+ NYT::TTableReader<NYT::TNode> reader(MakeIntrusive<NYT::TNodeTableReader>(MakeIntrusive<NYT::TJobReader>(inHandle)));
+ NYT::TTableWriter<NYT::TNode> writer(MakeIntrusive<NYT::TNodeTableWriter>(MakeHolder<NYT::TJobWriter>(outHandles)));
+
+ Init();
+
+ TLambdaBuilder builder(FunctionRegistry.Get(), *Alloc,Env.Get(),
+ RandomProvider.Get(), TimeProvider.Get(), JobStats.Get(), nullptr, SecureParamsProvider.Get());
+
+ std::function<void(const NUdf::TUnboxedValuePod&, TType*, TVector<ui32>*)> flush;
+ if (UseResultYson_) {
+ flush = [&writer] (const NUdf::TUnboxedValuePod& v, TType* type, TVector<ui32>* structPositions) {
+ NYT::TNode row = NYT::TNode::CreateMap();
+ NYT::TNodeBuilder nodeBuilder(&row["output"]);
+ NCommon::WriteYsonValue(nodeBuilder, v, type, structPositions);
+ writer.AddRow(row);
+ };
+ }
+ else {
+ flush = [&writer] (const NUdf::TUnboxedValuePod& v, TType* type, TVector<ui32>* /*structPositions*/) {
+ writer.AddRow(NYT::TNode()("output", NCommon::ValueToNode(v, type)));
+ };
+ }
+
+ auto factory = GetJobFactory(*CodecCtx, OptLLVM, nullptr, nullptr, nullptr);
+ for (; reader.IsValid(); reader.Next()) {
+ const auto& row = reader.GetRow();
+ TStringStream lambda;
+ {
+ TStringInput in(row["input"].AsString());
+ TBrotliDecompress decompress(&in);
+ TransferData(&decompress, &lambda);
+ }
+ TRuntimeNode rootNode = DeserializeRuntimeNode(lambda.Str(), *Env);
+ rootNode = builder.TransformAndOptimizeProgram(rootNode, MakeTransformProvider());
+ TType* outType = rootNode.GetStaticType();
+ TExploringNodeVisitor explorer;
+ auto graph = builder.BuildGraph(
+ factory,
+ UdfValidateMode,
+ NUdf::EValidatePolicy::Fail, OptLLVM,
+ EGraphPerProcess::Single,
+ explorer, rootNode);
+ const TBindTerminator bind(graph->GetTerminator());
+ graph->Prepare();
+ auto value = graph->GetValue();
+ if (outType->IsTuple()) {
+ auto tupleType = AS_TYPE(NMiniKQL::TTupleType, outType);
+ for (ui32 i: xrange(tupleType->GetElementsCount())) {
+ flush(value.GetElement(i), tupleType->GetElementType(i), nullptr);
+ }
+ } else if (outType->IsList()) {
+ auto itemType = AS_TYPE(NMiniKQL::TListType, outType)->GetItemType();
+ TMaybe<TVector<ui32>> structPositions = NCommon::CreateStructPositions(itemType, Columns_.Get());
+ const auto it = value.GetListIterator();
+ for (NUdf::TUnboxedValue item; it.Next(item);) {
+ flush(item, itemType, structPositions.Get());
+ }
+ } else {
+ flush(value, outType, nullptr);
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_calc.h b/ydb/library/yql/providers/yt/job/yql_job_calc.h
new file mode 100644
index 0000000000..4e5f2398a2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_calc.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "yql_job_base.h"
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+
+namespace NYql {
+
+class TYqlCalcJob : public TYqlJobBase {
+public:
+ TYqlCalcJob() = default;
+ ~TYqlCalcJob() = default;
+
+ void SetColumns(const TVector<TString>& columns) {
+ Columns_ = columns;
+ }
+
+ void SetUseResultYson(bool flag) {
+ UseResultYson_ = flag;
+ }
+
+ void Save(IOutputStream& stream) const override;
+ void Load(IInputStream& stream) override;
+
+protected:
+ void DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) override;
+
+private:
+ TMaybe<TVector<TString>> Columns_;
+ bool UseResultYson_ = false;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_factory.cpp b/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
new file mode 100644
index 0000000000..b9caf8976a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_factory.cpp
@@ -0,0 +1,57 @@
+#include "yql_job_factory.h"
+#include "yql_job_table_content.h"
+
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_input.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_output.h>
+#include <ydb/library/yql/providers/common/comp_nodes/yql_factory.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+#include <ydb/library/yql/parser/pg_wrapper/interface/comp_factory.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+TComputationNodeFactory GetJobFactory(NYql::NCommon::TCodecContext& codecCtx, const TString& optLLVM,
+ const TMkqlIOSpecs* specs, NYT::IReaderImplBase* reader, TJobMkqlWriterImpl* writer)
+{
+ TMaybe<ui32> exprContextObject;
+ return [&codecCtx, optLLVM, specs, reader, writer, exprContextObject](NMiniKQL::TCallable& callable, const TComputationNodeFactoryContext& ctx) mutable -> IComputationNode* {
+ TStringBuf name = callable.GetType()->GetName();
+ if (name.SkipPrefix("Yt") && name.ChopSuffix("Job")) {
+ if (name == "TableContent") {
+ return WrapYtTableContentJob(codecCtx, ctx.Mutables, callable, optLLVM);
+ }
+ if (name == "Input") {
+ YQL_ENSURE(reader);
+ YQL_ENSURE(specs);
+ return WrapYtInput(callable, ctx, *specs, reader);
+ }
+ if (name == "Output") {
+ YQL_ENSURE(writer);
+ return WrapYtOutput(callable, ctx, *writer);
+ }
+ }
+
+ if (!exprContextObject) {
+ exprContextObject = ctx.Mutables.CurValueIndex++;
+ }
+
+ auto yql = GetYqlFactory(*exprContextObject)(callable, ctx);
+ if (yql) {
+ return yql;
+ }
+
+ auto pg = GetPgFactory()(callable, ctx);
+ if (pg) {
+ return pg;
+ }
+
+ return GetBuiltinFactory()(callable, ctx);
+ };
+}
+
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_factory.h b/ydb/library/yql/providers/yt/job/yql_job_factory.h
new file mode 100644
index 0000000000..51216e00ff
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_factory.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_job.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::TComputationNodeFactory GetJobFactory(NYql::NCommon::TCodecContext& codecCtx,
+ const TString& optLLVM, const TMkqlIOSpecs* specs, NYT::IReaderImplBase* reader, TJobMkqlWriterImpl* writer);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp b/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
new file mode 100644
index 0000000000..84e0f2847b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_infer_schema.cpp
@@ -0,0 +1,49 @@
+#include "yql_job_infer_schema.h"
+
+#include <ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h>
+
+#include <yt/cpp/mapreduce/io/node_table_reader.h>
+#include <yt/cpp/mapreduce/io/node_table_writer.h>
+#include <library/cpp/yson/node/node_io.h>
+
+namespace NYql {
+
+void TYqlInferSchemaJob::DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) {
+ NYT::TTableReader<NYT::TNode> reader(MakeIntrusive<NYT::TNodeTableReader>(MakeIntrusive<NYT::TJobReader>(inHandle)));
+ NYT::TTableWriter<NYT::TNode> writer(MakeIntrusive<NYT::TNodeTableWriter>(MakeHolder<NYT::TJobWriter>(outHandles)));
+
+ Init();
+
+ THashMap<ui32, TStreamSchemaInferer> infererByTableIndex;
+
+ for (; reader.IsValid(); reader.Next()) {
+ ui32 idx = reader.GetTableIndex();
+
+ YQL_ENSURE(idx < TableNames.size());
+ TString tableName = TableNames[idx];
+
+ auto it = infererByTableIndex.find(idx);
+
+ if (it == infererByTableIndex.end()) {
+ it = infererByTableIndex.insert({ idx, TStreamSchemaInferer(tableName) }).first;
+ }
+
+ it->second.AddRow(reader.GetRow());
+ }
+
+ for (const auto &i : infererByTableIndex) {
+ ui32 idx = i.first;
+
+ NYT::TNode schema;
+ try {
+ schema = i.second.GetSchema();
+ } catch (const std::exception& e) {
+ schema = NYT::TNode(e.what());
+ }
+
+ NYT::TNode output = NYT::TNode()("index", idx)("schema", NYT::NodeToYsonString(schema));
+ writer.AddRow(output);
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_infer_schema.h b/ydb/library/yql/providers/yt/job/yql_job_infer_schema.h
new file mode 100644
index 0000000000..8124eb7f34
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_infer_schema.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "yql_job_base.h"
+#include "yql_job_factory.h"
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYql {
+
+class TYqlInferSchemaJob : public TYqlJobBase {
+public:
+ TYqlInferSchemaJob() = default;
+ ~TYqlInferSchemaJob() = default;
+
+protected:
+ void DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) override;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_registry.h b/ydb/library/yql/providers/yt/job/yql_job_registry.h
new file mode 100644
index 0000000000..e5a9d715bd
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_registry.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "yql_job_calc.h"
+#include "yql_job_infer_schema.h"
+#include "yql_job_user.h"
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+namespace NYql {
+
+REGISTER_NAMED_RAW_JOB("TYqlCalcJob", TYqlCalcJob);
+REGISTER_NAMED_RAW_JOB("TYqlInferSchemaJob", TYqlInferSchemaJob);
+REGISTER_NAMED_RAW_JOB("TYqlUserJob", TYqlUserJob);
+
+}
diff --git a/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp b/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
new file mode 100644
index 0000000000..fb08d2fcc2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_stats_writer.cpp
@@ -0,0 +1,41 @@
+#include "yql_job_stats_writer.h"
+#include "yql_job_base.h"
+
+#include <library/cpp/yt/user_job_statistics/user_job_statistics.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+using NKikimr::NMiniKQL::IStatsRegistry;
+using NKikimr::NMiniKQL::TStatKey;
+
+namespace NYql {
+
+void WriteJobStats(const IStatsRegistry* stats, const TJobCountersProvider& countersProvider, IOutputStream* out) {
+ NYtTools::TUserJobStatsProxy statsWriter;
+ statsWriter.Init(out);
+ auto& stream = *statsWriter.GetStream();
+
+ stats->ForEachStat([&stream](const TStatKey& key, i64 value) {
+ auto node = NYT::TNode{}(TString(key.GetName()), value);
+ stream << NYT::NodeToYsonString(node, NYson::EYsonFormat::Text) << ";\n";
+ });
+
+ for (const auto& x : countersProvider.Counters_) {
+ auto moduleMap = NYT::TNode{}(x.first.second, x.second);
+ auto counterMap = NYT::TNode{}(x.first.first, moduleMap);
+ auto udfMap = NYT::TNode{}("Counter", counterMap);
+ auto node = NYT::TNode{}("Udf", udfMap);
+ stream << NYT::NodeToYsonString(node, NYson::EYsonFormat::Text) << ";\n";
+ }
+
+ for (const auto& x : countersProvider.Probes_) {
+ auto moduleMap = NYT::TNode{}(x.first.second, i64(1000.0 * x.second.TotalCycles / GetCyclesPerMillisecond()));
+ auto timeMap = NYT::TNode{}(x.first.first, moduleMap);
+ auto udfMap = NYT::TNode{}("TimeUsec", timeMap);
+ auto node = NYT::TNode{}("Udf", udfMap);
+ stream << NYT::NodeToYsonString(node, NYson::EYsonFormat::Text) << ";\n";
+ }
+
+ stream.Flush();
+}
+
+} // namspace NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_stats_writer.h b/ydb/library/yql/providers/yt/job/yql_job_stats_writer.h
new file mode 100644
index 0000000000..7c4e4efe2c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_stats_writer.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+
+#include <util/stream/output.h>
+
+namespace NYql {
+
+struct TJobCountersProvider;
+
+/**
+ * @brief Writes stats to out stream (if defined) or to special YT-job file
+ * descriptor (see https://wiki.yandex-team.ru/yt/userdoc/jobs/#sborstatistikivdzhobax).
+ */
+void WriteJobStats(
+ const NKikimr::NMiniKQL::IStatsRegistry* stats,
+ const TJobCountersProvider& countersProvider,
+ IOutputStream* out = nullptr);
+
+} // namspace NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp b/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
new file mode 100644
index 0000000000..4f9244fb89
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_table_content.cpp
@@ -0,0 +1,78 @@
+#include "yql_job_table_content.h"
+
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_input_state.h>
+#include <ydb/library/yql/providers/yt/comp_nodes/yql_mkql_file_list.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_impl.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/defs.h>
+
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/size_literals.h>
+
+#include <type_traits>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+class TYtTableContentJobWrapper : public TMutableComputationNode<TYtTableContentJobWrapper> {
+ typedef TMutableComputationNode<TYtTableContentJobWrapper> TBaseComputation;
+public:
+ TYtTableContentJobWrapper(TComputationMutables& mutables, NCommon::TCodecContext& codecCtx,
+ TVector<TString>&& files, const TString& inputSpec, TType* listType, bool useSkiff, bool decompress, const TString& optLLVM,
+ std::optional<ui64> length)
+ : TBaseComputation(mutables)
+ , Files_(std::move(files))
+ , Decompress_(decompress)
+ , Length_(std::move(length))
+ {
+ if (useSkiff) {
+ Spec_.SetUseSkiff(optLLVM);
+ }
+ Spec_.Init(codecCtx, inputSpec, {}, {}, AS_TYPE(TListType, listType)->GetItemType(), {}, TString());
+ }
+
+ NUdf::TUnboxedValuePod DoCalculate(TComputationContext& ctx) const {
+ return ctx.HolderFactory.Create<TFileListValue>(Spec_, ctx.HolderFactory, Files_, Decompress_, 4, 1_MB, Length_);
+ }
+
+private:
+ void RegisterDependencies() const final {}
+
+ TMkqlIOSpecs Spec_;
+ TVector<TString> Files_;
+ const bool Decompress_;
+ const std::optional<ui64> Length_;
+};
+
+IComputationNode* WrapYtTableContentJob(NCommon::TCodecContext& codecCtx,
+ TComputationMutables& mutables, TCallable& callable, const TString& optLLVM)
+{
+ MKQL_ENSURE(callable.GetInputsCount() == 6, "Expected 6 arguments");
+ TString uniqueId(AS_VALUE(TDataLiteral, callable.GetInput(0))->AsValue().AsStringRef());
+ const ui32 tablesCount = AS_VALUE(TDataLiteral, callable.GetInput(1))->AsValue().Get<ui32>();
+ TString inputSpec(AS_VALUE(TDataLiteral, callable.GetInput(2))->AsValue().AsStringRef());
+ const bool useSkiff = AS_VALUE(TDataLiteral, callable.GetInput(3))->AsValue().Get<bool>();
+ const bool decompress = AS_VALUE(TDataLiteral, callable.GetInput(4))->AsValue().Get<bool>();
+
+ std::optional<ui64> length;
+ TTupleLiteral* lengthTuple = AS_VALUE(TTupleLiteral, callable.GetInput(5));
+ if (lengthTuple->GetValuesCount() > 0) {
+ YQL_ENSURE(lengthTuple->GetValuesCount() == 1, "Expect 1 element in the length tuple");
+ length = AS_VALUE(TDataLiteral, lengthTuple->GetValue(0))->AsValue().Get<ui64>();
+ }
+
+ TVector<TString> files;
+ for (ui32 index = 0; index < tablesCount; ++index) {
+ files.push_back(TStringBuilder() << uniqueId << '_' << index);
+ }
+
+ return new TYtTableContentJobWrapper(mutables, codecCtx, std::move(files), inputSpec,
+ callable.GetType()->GetReturnType(), useSkiff, decompress, optLLVM, length);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_table_content.h b/ydb/library/yql/providers/yt/job/yql_job_table_content.h
new file mode 100644
index 0000000000..4e16a3b66c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_table_content.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::IComputationNode* WrapYtTableContentJob(
+ NYql::NCommon::TCodecContext& codecCtx,
+ NKikimr::NMiniKQL::TComputationMutables& mutables,
+ NKikimr::NMiniKQL::TCallable& callable, const TString& optLLVM);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_user.cpp b/ydb/library/yql/providers/yt/job/yql_job_user.cpp
new file mode 100644
index 0000000000..50b0f20cd4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_user.cpp
@@ -0,0 +1,236 @@
+#include "yql_job_user.h"
+#include "yql_job_factory.h"
+
+#include <ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/providers/common/schema/parser/yql_type_parser.h>
+#include <ydb/library/yql/providers/common/schema/mkql/yql_mkql_schema.h>
+#include <ydb/library/yql/minikql/mkql_node_serialization.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <yt/cpp/mapreduce/client/structured_table_formats.h>
+#include <yt/cpp/mapreduce/io/yamr_table_reader.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/xrange.h>
+#include <util/generic/yexception.h>
+#include <util/stream/str.h>
+#include <util/system/rusage.h>
+#include <util/system/datetime.h>
+#include <util/ysaveload.h>
+
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+
+ const static TStatKey Mkql_TotalRuntimeNodes("Mkql_TotalRuntimeNodes", false);
+ const static TStatKey Mkql_BuildGraphRssDelta("Mkql_BuildGraphRssDelta", false);
+ const static TStatKey Job_InitTime("Job_InitTime", false);
+ const static TStatKey Job_CalcTime("Job_CalcTime", false);
+
+ NYT::TFormat MakeTableYaMRFormat(const TString& inputSpec) {
+ NYT::TNode inAttrs;
+ TStringStream err;
+ if (!NCommon::ParseYson(inAttrs, inputSpec, err)) {
+ ythrow yexception() << "Invalid input attrs: " << err.Str();
+ }
+ YQL_ENSURE(inAttrs.IsMap(), "Expect Map type of output meta attrs, but got type " << inAttrs.GetType());
+ YQL_ENSURE(inAttrs.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ auto& inputSpecs = inAttrs[YqlIOSpecTables].AsList();
+ YQL_ENSURE(!inputSpecs.empty(), "Expect list with at least one element in input attrs: " << inputSpec);
+
+ TVector<TMaybe<NYT::TNode>> formats;
+ THashMap<TString, NYT::TNode> specRegistry;
+ for (auto& attrs: inputSpecs) {
+ NYT::TNode spec;
+ if (attrs.IsString()) {
+ auto refName = attrs.AsString();
+ if (auto p = specRegistry.FindPtr(refName)) {
+ spec = *p;
+ } else {
+ YQL_ENSURE(inAttrs.HasKey(YqlIOSpecRegistry) && inAttrs[YqlIOSpecRegistry].HasKey(refName), "Bad input registry reference: " << refName);
+ NYT::TNode& r = specRegistry[refName];
+ r = inAttrs[YqlIOSpecRegistry][refName];
+ spec = r;
+ }
+ } else {
+ spec = attrs;
+ }
+ formats.push_back(spec.HasKey(FORMAT_ATTR_NAME) ? MakeMaybe(spec[FORMAT_ATTR_NAME]) : Nothing());
+ }
+
+ NYT::TNode format = NYT::GetCommonTableFormat(formats).GetOrElse(NYT::TNode("yamred_dsv"));
+ format.Attributes()["lenval"] = true;
+ format.Attributes()["has_subkey"] = true;
+ format.Attributes()["enable_table_index"] = true;
+ return NYT::TFormat(format);
+ }
+}
+
+
+std::pair<NYT::TFormat, NYT::TFormat> TYqlUserJob::GetIOFormats(const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry) const {
+ if (!UseSkiff) {
+ return std::make_pair(YamrInput ? MakeTableYaMRFormat(InputSpec) : NYT::TFormat::YsonBinary(), NYT::TFormat::YsonBinary());
+ }
+ TScopedAlloc alloc(__LOCATION__);
+ TTypeEnvironment env(alloc);
+ NCommon::TCodecContext codecCtx(env, *functionRegistry);
+
+ TType* itemType = nullptr;
+ if (InputType) {
+ TStringStream err;
+ TProgramBuilder pgmBuilder(env, *functionRegistry);
+ itemType = NCommon::ParseTypeFromYson(TStringBuf{InputType}, pgmBuilder, err);
+ YQL_ENSURE(itemType, << err.Str());
+ }
+
+ TMkqlIOSpecs specs;
+ specs.SetUseSkiff(OptLLVM, SkiffSysFields);
+ specs.Init(codecCtx, InputSpec, InputGroups, TableNames, itemType, AuxColumns, OutSpec);
+
+ return std::make_pair(YamrInput ? MakeTableYaMRFormat(InputSpec) : specs.MakeInputFormat(AuxColumns), specs.MakeOutputFormat());
+}
+
+void TYqlUserJob::Save(IOutputStream& s) const {
+ TYqlJobBase::Save(s);
+ ::SaveMany(&s,
+ UseSkiff,
+ SkiffSysFields,
+ YamrInput,
+ LambdaCode,
+ InputSpec,
+ OutSpec,
+ InputGroups,
+ AuxColumns,
+ InputType,
+ RowOffsets
+ );
+}
+
+void TYqlUserJob::Load(IInputStream& s) {
+ TYqlJobBase::Load(s);
+ ::LoadMany(&s,
+ UseSkiff,
+ SkiffSysFields,
+ YamrInput,
+ LambdaCode,
+ InputSpec,
+ OutSpec,
+ InputGroups,
+ AuxColumns,
+ InputType,
+ RowOffsets
+ );
+}
+
+void TYqlUserJob::DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) {
+ TYqlJobBase::Init();
+
+ TLambdaBuilder builder(FunctionRegistry.Get(), *Alloc,
+ Env.Get(), RandomProvider.Get(), TimeProvider.Get(), JobStats.Get(), &JobCountersProvider, SecureParamsProvider.Get());
+
+ TType* itemType = nullptr;
+ if (InputType) {
+ TStringStream err;
+ TProgramBuilder pgmBuilder(*Env, *FunctionRegistry);
+ itemType = NCommon::ParseTypeFromYson(TStringBuf{InputType}, pgmBuilder, err);
+ YQL_ENSURE(itemType, << err.Str());
+ }
+
+ YQL_ENSURE(LambdaCode);
+ TRuntimeNode rootNode = DeserializeRuntimeNode(LambdaCode, *Env);
+ THashMap<TString, TRuntimeNode> extraArgs;
+ rootNode = builder.TransformAndOptimizeProgram(rootNode, MakeTransformProvider(&extraArgs));
+
+ MkqlIOSpecs.Reset(new TMkqlIOSpecs());
+ if (UseSkiff) {
+ MkqlIOSpecs->SetUseSkiff(OptLLVM, SkiffSysFields);
+ }
+ MkqlIOSpecs->Init(*CodecCtx, InputSpec, InputGroups, TableNames, itemType, AuxColumns, OutSpec, JobStats.Get());
+ if (!RowOffsets.empty()) {
+ MkqlIOSpecs->SetTableOffsets(RowOffsets);
+ }
+
+ TIntrusivePtr<TJobMkqlWriterImpl> mkqlWriter = MakeIntrusive<TJobMkqlWriterImpl>(*MkqlIOSpecs, outHandles);
+ TIntrusivePtr<NYT::IReaderImplBase> reader;
+
+ if (itemType) {
+ if (YamrInput) {
+ reader = MakeIntrusive<NYT::TYaMRTableReader>(MakeIntrusive<NYT::TJobReader>(inHandle));
+ }
+ else {
+ reader = MakeIntrusive<TJobMkqlReaderImpl>(inHandle);
+ }
+ }
+
+ std::vector<NKikimr::NMiniKQL::TNode*> entryPoints(1, rootNode.GetNode());
+ for (auto& item: extraArgs) {
+ entryPoints.push_back(item.second.GetNode());
+ }
+ auto maxRss = TRusage::Get().MaxRss;
+ CompGraph = builder.BuildGraph(
+ GetJobFactory(*CodecCtx, OptLLVM, MkqlIOSpecs.Get(), reader.Get(), mkqlWriter.Get()),
+ UdfValidateMode,
+ NUdf::EValidatePolicy::Fail, OptLLVM,
+ EGraphPerProcess::Single,
+ Explorer,
+ rootNode,
+ std::move(entryPoints)
+ );
+
+ MKQL_SET_STAT(JobStats, Mkql_BuildGraphRssDelta, TRusage::Get().MaxRss - maxRss);
+ MKQL_SET_STAT(JobStats, Mkql_TotalRuntimeNodes, Explorer.GetNodes().size());
+ MKQL_SET_STAT(JobStats, Job_InitTime, (ThreadCPUTime() - StartTime) / 1000);
+
+ auto beginCalcTime = ThreadCPUTime();
+
+ if (CompGraph) {
+ for (size_t i: xrange(extraArgs.size())) {
+ if (auto entry = CompGraph->GetEntryPoint(i + 1, false)) {
+ entry->SetValue(CompGraph->GetContext(), NUdf::TUnboxedValue::Zero());
+ }
+ }
+
+ CodecCtx->HolderFactory = &CompGraph->GetHolderFactory();
+ CompGraph->Prepare();
+ BindTerminator.Reset(new TBindTerminator(CompGraph->GetTerminator()));
+
+ if (auto mkqlReader = dynamic_cast<TJobMkqlReaderImpl*>(reader.Get())) {
+ mkqlReader->SetSpecs(*MkqlIOSpecs, CompGraph->GetHolderFactory());
+ mkqlReader->Next(); // Prefetch first record to unify behavior with TYaMRTableReader
+ }
+ }
+
+ NUdf::TUnboxedValue value = CompGraph->GetValue();
+ if (rootNode.GetStaticType()->IsStream()) {
+ NUdf::TUnboxedValue item;
+ const auto status = value.Fetch(item);
+ YQL_ENSURE(status == NUdf::EFetchStatus::Finish);
+ } else {
+ YQL_ENSURE(value.IsFinish());
+ }
+
+ MKQL_SET_STAT(JobStats, Job_CalcTime, (ThreadCPUTime() - beginCalcTime) / 1000);
+
+ if (auto mkqlReader = dynamic_cast<TJobMkqlReaderImpl*>(reader.Get())) {
+ mkqlReader->Finish();
+ }
+ reader.Drop();
+ mkqlWriter->Finish();
+ mkqlWriter.Drop();
+
+ MkqlIOSpecs->Clear();
+ MkqlIOSpecs.Destroy();
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/job/yql_job_user.h b/ydb/library/yql/providers/yt/job/yql_job_user.h
new file mode 100644
index 0000000000..dddcbbf280
--- /dev/null
+++ b/ydb/library/yql/providers/yt/job/yql_job_user.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "yql_job_base.h"
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_job.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/computation/mkql_value_builder.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_node_visitor.h>
+#include <ydb/library/yql/minikql/mkql_alloc.h>
+#include <ydb/library/yql/minikql/mkql_terminator.h>
+
+#include <yt/cpp/mapreduce/interface/format.h>
+#include <yt/cpp/mapreduce/io/job_reader.h>
+#include <yt/cpp/mapreduce/io/job_writer.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/ptr.h>
+#include <util/generic/hash_set.h>
+
+#include <utility>
+
+namespace NKikimr {
+ namespace NMiniKQL {
+ class IFunctionRegistry;
+ }
+}
+
+namespace NYql {
+
+class TYqlUserJob: public TYqlJobBase {
+public:
+ TYqlUserJob()
+ : TYqlJobBase()
+ {
+ }
+ virtual ~TYqlUserJob() = default;
+
+ void SetUseSkiff(bool useSkiff, TMkqlIOSpecs::TSystemFields sysFields) {
+ UseSkiff = useSkiff;
+ SkiffSysFields = sysFields;
+ }
+
+ void SetYamrInput(bool yamrInput) {
+ YamrInput = yamrInput;
+ }
+
+ void SetLambdaCode(const TString& code) {
+ LambdaCode = code;
+ }
+
+ void SetInputSpec(const TString& spec) {
+ InputSpec = spec;
+ }
+
+ void SetInputGroups(const TVector<ui32>& inputGroups) {
+ InputGroups = inputGroups;
+ }
+
+ void SetOutSpec(const TString& spec) {
+ OutSpec = spec;
+ }
+
+ void SetAuxColumns(const THashSet<TString>& auxColumns) {
+ AuxColumns = auxColumns;
+ }
+
+ void SetInputType(const TString& type) {
+ InputType = type;
+ }
+
+ void SetRowOffsets(const TVector<ui64>& rowOffsets) {
+ RowOffsets = rowOffsets;
+ }
+
+ std::pair<NYT::TFormat, NYT::TFormat> GetIOFormats(const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry) const;
+
+ void Save(IOutputStream& s) const override;
+ void Load(IInputStream& s) override;
+
+protected:
+ void DoImpl(const TFile& inHandle, const TVector<TFile>& outHandles) final;
+
+protected:
+ // Serializable part (don't forget to add new members to Save/Load)
+ bool UseSkiff = false;
+ TMkqlIOSpecs::TSystemFields SkiffSysFields;
+ bool YamrInput = false;
+ TString LambdaCode;
+ TString InputSpec;
+ TString OutSpec;
+ TVector<ui32> InputGroups;
+ THashSet<TString> AuxColumns;
+ TString InputType;
+ TVector<ui64> RowOffsets;
+ // End of serializable part
+
+ NKikimr::NMiniKQL::TExploringNodeVisitor Explorer;
+ THolder<NKikimr::NMiniKQL::IComputationGraph> CompGraph;
+ THolder<NKikimr::NMiniKQL::TBindTerminator> BindTerminator;
+
+ THolder<TMkqlIOSpecs> MkqlIOSpecs;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp b/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
new file mode 100644
index 0000000000..af3acb0a2f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.cpp
@@ -0,0 +1,110 @@
+#include "config_clusters.h"
+
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/yexception.h>
+
+namespace NYql {
+
+TConfigClusters::TConfigClusters(const TYtGatewayConfig& config) {
+ for (auto& cluster: config.GetClusterMapping()) {
+ if (!cluster.GetName()) {
+ ythrow yexception() << "TYtGatewayConfig: Cluster name must be specified";
+ }
+ if (Clusters_.contains(cluster.GetName())) {
+ ythrow yexception() << "TYtGatewayConfig: Duplicate cluster name: " << cluster.GetName();
+ }
+
+ TClusterInfo& info = Clusters_[cluster.GetName()];
+ if (cluster.GetCluster()) {
+ info.RealName = cluster.GetCluster();
+ info.YtName = cluster.HasYTName() ? cluster.GetYTName() : cluster.GetName();
+ TString ytName = info.YtName;
+ ytName.to_lower();
+ YtName2Name_.emplace(ytName, cluster.GetName());
+ } else {
+ ythrow yexception() << "TYtGatewayConfig: Cluster address must be specified";
+ }
+
+ if (cluster.HasYTToken()) {
+ info.Token = cluster.GetYTToken();
+ }
+
+ if (cluster.GetDefault()) {
+ if (DefaultClusterName_) {
+ ythrow yexception() << "TYtGatewayConfig: More than one default cluster (current: "
+ << cluster.GetName() << ", previous: " << DefaultClusterName_ << ")";
+ }
+ DefaultClusterName_ = cluster.GetName();
+ }
+ }
+}
+
+const TString& TConfigClusters::GetServer(const TString& name) const {
+ if (const TClusterInfo* info = Clusters_.FindPtr(name)) {
+ return info->RealName;
+ } else {
+ ythrow yexception() << "Unknown cluster name: " << name;
+ }
+}
+
+TString TConfigClusters::TryGetServer(const TString& name) const {
+ if (const TClusterInfo* info = Clusters_.FindPtr(name)) {
+ return info->RealName;
+ } else {
+ return {};
+ }
+}
+
+const TString& TConfigClusters::GetYtName(const TString& name) const {
+ if (const TClusterInfo* info = Clusters_.FindPtr(name)) {
+ return info->YtName;
+ } else {
+ ythrow yexception() << "Unknown cluster name: " << name;
+ }
+}
+
+TString TConfigClusters::GetNameByYtName(const TString& ytName) const {
+ TString ytNameCopy = ytName;
+ ytNameCopy.to_lower();
+
+ if (const TString* name = YtName2Name_.FindPtr(ytNameCopy)) {
+ return *name;
+ }
+
+ // no exception
+ return ytName;
+}
+
+TMaybe<TString> TConfigClusters::GetAuth(const TString& name) const {
+ if (const TClusterInfo* info = Clusters_.FindPtr(name)) {
+ return info->Token;
+ }
+ return Nothing();
+}
+
+void TConfigClusters::GetAllClusters(TVector<TString>& names) const {
+ names.clear();
+ for (const auto& c: Clusters_) {
+ names.push_back(c.first);
+ }
+}
+
+const TString& TConfigClusters::GetDefaultClusterName() const {
+ if (!DefaultClusterName_) {
+ ythrow yexception() << "TYtGatewayConfig: No default cluster";
+ }
+ return DefaultClusterName_;
+}
+
+TString TConfigClusters::GetDefaultYtServer(const TYtGatewayConfig& config) {
+ for (auto& cluster: config.GetClusterMapping()) {
+ if (cluster.GetDefault()) {
+ return cluster.GetCluster();
+ }
+ }
+ return {};
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h b/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h
new file mode 100644
index 0000000000..31cd9ff3d0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/config_clusters.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <util/generic/maybe.h>
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/ptr.h>
+
+namespace NYql {
+
+class TYtGatewayConfig;
+
+class TConfigClusters: public TThrRefBase {
+private:
+ struct TClusterInfo {
+ TString RealName;
+ TString YtName;
+ TMaybe<TString> Token;
+ };
+public:
+ using TPtr = TIntrusivePtr<TConfigClusters>;
+
+ explicit TConfigClusters(const TYtGatewayConfig& config);
+
+ const TString& GetServer(const TString& name) const;
+ TString TryGetServer(const TString& name) const;
+ const TString& GetYtName(const TString& name) const;
+ TString GetNameByYtName(const TString& ytName) const;
+ TMaybe<TString> GetAuth(const TString& name) const;
+ void GetAllClusters(TVector<TString>& names) const;
+ const TString& GetDefaultClusterName() const;
+
+ static TString GetDefaultYtServer(const TYtGatewayConfig& config);
+
+private:
+ THashMap<TString, TClusterInfo> Clusters_;
+ // ytName.to_lower() -> Name
+ THashMap<TString, TString> YtName2Name_;
+ TString DefaultClusterName_;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/config_clusters/ya.make b/ydb/library/yql/providers/yt/lib/config_clusters/ya.make
new file mode 100644
index 0000000000..ad80977d48
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/config_clusters/ya.make
@@ -0,0 +1,12 @@
+LIBRARY()
+
+SRCS(
+ config_clusters.cpp
+ config_clusters.h
+)
+
+PEERDIR(
+ ydb/library/yql/providers/common/proto
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/ya.make b/ydb/library/yql/providers/yt/lib/expr_traits/ya.make
new file mode 100644
index 0000000000..70ab18a081
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+SRCS(
+ yql_expr_traits.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/ast
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/utils/log
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/expr_nodes
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp b/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
new file mode 100644
index 0000000000..a4d86a5230
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.cpp
@@ -0,0 +1,436 @@
+#include "yql_expr_traits.h"
+
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node_holders.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/size_literals.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+ namespace {
+ // Mapping of comparison op to its normalized form
+ const THashSet<TStringBuf> RANGE_COMPARISON_OPS = {
+ NNodes::TCoCmpLess::CallableName(),
+ NNodes::TCoCmpLessOrEqual::CallableName(),
+ NNodes::TCoCmpEqual::CallableName(),
+ NNodes::TCoCmpGreater::CallableName(),
+ NNodes::TCoCmpGreaterOrEqual::CallableName(),
+ NNodes::TCoCmpStartsWith::CallableName(),
+ };
+
+ }
+
+ bool IsRangeComparison(const TStringBuf& operation) {
+ return RANGE_COMPARISON_OPS.contains(operation);
+ }
+
+ void ScanResourceUsage(const TExprNode& input, const TYtSettings& config, const TTypeAnnotationContext* types,
+ TMap<TStringBuf, ui64>* memoryUsage, TMap<TStringBuf, double>* cpuUsage, size_t* files)
+ {
+ VisitExpr(input, [&](const TExprNode& node) {
+ if (NNodes::TYtOutput::Match(&node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+
+ if (memoryUsage) {
+ if (node.IsCallable("CommonJoinCore")) {
+ if (auto memLimitSetting = GetSetting(*node.Child(5), "memLimit")) {
+ (*memoryUsage)["CommonJoinCore"] += FromString<ui64>(memLimitSetting->Child(1)->Content());
+ }
+ } else if (node.IsCallable("WideCombiner")) {
+ (*memoryUsage)["WideCombiner"] += FromString<ui64>(node.Child(1U)->Content());
+ } else if (NNodes::TCoCombineCore::Match(&node)) {
+ (*memoryUsage)["CombineCore"] += FromString<ui64>(node.Child(NNodes::TCoCombineCore::idx_MemLimit)->Content());
+ }
+ }
+
+ if (node.IsCallable("Udf")) { // TODO: use YQL-369 when it's ready
+ if (files) {
+ ++*files;
+ }
+ if (node.Child(0)->Content().StartsWith("Geo.")) {
+ if (files) {
+ ++*files; // geobase
+ }
+ if (memoryUsage) {
+ (*memoryUsage)["Geo module"] = 2_GB; // Take into account only once
+ }
+ }
+ if (memoryUsage && node.Child(0)->Content().StartsWith("UserSessions.")) {
+ (*memoryUsage)["UserSessions module"] = 512_MB; // Take into account only once
+ }
+ }
+
+ if (NNodes::TYtTableContent::Match(&node)) {
+ if (files) {
+ auto content = NNodes::TYtTableContent(&node);
+ if (auto read = content.Input().Maybe<NNodes::TYtReadTable>()) {
+ for (auto section: read.Cast().Input()) {
+ *files += section.Paths().Size();
+ }
+ } else {
+ // YtOutput
+ ++*files;
+ }
+ }
+ if (memoryUsage) {
+ if (auto setting = NYql::GetSetting(*node.Child(NNodes::TYtTableContent::idx_Settings), "memUsage")) {
+ (*memoryUsage)["YtTableContent"] += FromString<ui64>(setting->Child(1)->Content());
+ }
+ }
+ // Increase CPU only for CROSS JOIN. Check "rowFactor" as CROSS JOIN flag
+ if (cpuUsage && HasSetting(*node.Child(NNodes::TYtTableContent::idx_Settings), "rowFactor")) {
+ if (auto setting = NYql::GetSetting(*node.Child(NNodes::TYtTableContent::idx_Settings), "itemsCount")) {
+ (*cpuUsage)["YtTableContent"] = double(FromString<ui64>(setting->Child(1)->Content()));
+ }
+ }
+ }
+
+ if (NNodes::TCoSwitch::Match(&node)) {
+ if (memoryUsage) {
+ (*memoryUsage)["Switch"] += FromString<ui64>(node.Child(1)->Content());
+ }
+ if (cpuUsage) {
+ const auto& inputItemType = GetSeqItemType(*node.Head().GetTypeAnn());
+ ui32 inputStreamsCount = 1;
+ if (inputItemType.GetKind() == ETypeAnnotationKind::Variant) {
+ auto underlyingType = inputItemType.Cast<TVariantExprType>()->GetUnderlyingType();
+ inputStreamsCount = underlyingType->Cast<TTupleExprType>()->GetSize();
+ }
+
+ TVector<ui32> streamUsage(inputStreamsCount, 0);
+ for (ui32 i = 2; i < node.ChildrenSize(); i += 2) {
+ for (auto& child : node.Child(i)->Children()) {
+ ++streamUsage[FromString<ui32>(child->Content())];
+ }
+ }
+ auto maxStreamUsage = *MaxElement(streamUsage.begin(), streamUsage.end());
+ if (maxStreamUsage > 1) {
+ double usage = maxStreamUsage;
+ if (auto prev = cpuUsage->FindPtr("Switch")) {
+ usage *= *prev;
+ }
+ (*cpuUsage)["Switch"] = usage;
+ }
+ }
+ }
+
+ if (node.IsCallable(TStringBuf("ScriptUdf"))) {
+ if (files) {
+ ++*files;
+ }
+ if (cpuUsage) {
+ if (auto cpu = config.ScriptCpu.Get(NCommon::ALL_CLUSTERS)) {
+ (*cpuUsage)["ScriptUdf"] = *cpu; // Take into account only once
+ }
+ if (node.Child(0)->Content().EndsWith("Javascript")) {
+ if (auto cpu = config.JavascriptCpu.Get(NCommon::ALL_CLUSTERS)) {
+ (*cpuUsage)["Javascript module"] = *cpu; // Take into account only once
+ }
+ }
+ }
+ if (node.Child(0)->Content().Contains("Python")) {
+ if (memoryUsage) {
+ (*memoryUsage)["Python module"] = 512_MB; // Take into account only once
+ }
+ if (cpuUsage) {
+ if (auto cpu = config.PythonCpu.Get(NCommon::ALL_CLUSTERS)) {
+ (*cpuUsage)["Python module"] = *cpu; // Take into account only once
+ }
+ }
+ }
+ if (node.ChildrenSize() >= 5) {
+ if (cpuUsage) {
+ if (auto cpuSetting = GetSetting(*node.Child(4), "cpu")) {
+ double usage = FromString<double>(cpuSetting->Child(1)->Content());
+ if (auto prev = cpuUsage->FindPtr("ScriptUdf from settings arg")) {
+ usage *= *prev;
+ }
+ (*cpuUsage)["ScriptUdf from settings arg"] = usage;
+ }
+ }
+ if (memoryUsage) {
+ if (auto extraMemSetting = GetSetting(*node.Child(4), "extraMem")) {
+ (*memoryUsage)["ScriptUdf from settings arg"] += FromString<ui64>(extraMemSetting->Child(1)->Content());
+ }
+ }
+ }
+ }
+ if (files) {
+ if (node.IsCallable("FilePath") || node.IsCallable("FileContent")) {
+ ++*files;
+ }
+ else if (node.IsCallable("FolderPath")) {
+ YQL_ENSURE(types);
+ const auto& name = node.Head().Content();
+ if (auto blocks = types->UserDataStorage->FindUserDataFolder(name)) {
+ *files += blocks->size();
+ }
+ }
+ }
+ return true;
+ });
+ }
+
+ TExpressionResorceUsage ScanExtraResourceUsage(const TExprNode& input, const TYtSettings& config) {
+ TExpressionResorceUsage ret;
+ TMap<TStringBuf, ui64> memory;
+ TMap<TStringBuf, double> cpu;
+ ScanResourceUsage(input, config, nullptr, &memory, &cpu, nullptr);
+
+ for (auto& m: memory) {
+ ret.Memory += m.second;
+ YQL_CLOG(DEBUG, ProviderYt) << "Increased extraMemUsage for " << m.first << " to " << ret.Memory << " (by " << m.second << ")";
+ }
+ for (auto& c: cpu) {
+ ret.Cpu *= c.second;
+ YQL_CLOG(DEBUG, ProviderYt) << "Increased cpu for " << c.first << " to " << ret.Cpu;
+ }
+ return ret;
+ }
+
+ namespace {
+ bool IsPrimitiveType(const TTypeAnnotationNode* type) {
+ if (ETypeAnnotationKind::Data != type->GetKind()) {
+ return false;
+ }
+ using namespace NKikimr::NUdf;
+ switch (type->Cast<TDataExprType>()->GetSlot()) {
+ case EDataSlot::Bool:
+ case EDataSlot::Uint8:
+ case EDataSlot::Int8:
+ case EDataSlot::Uint16:
+ case EDataSlot::Int16:
+ case EDataSlot::Uint32:
+ case EDataSlot::Int32:
+ case EDataSlot::Uint64:
+ case EDataSlot::Int64:
+ case EDataSlot::Float:
+ case EDataSlot::Double:
+ case EDataSlot::Date:
+ case EDataSlot::Datetime:
+ case EDataSlot::Timestamp:
+ case EDataSlot::Interval:
+ return true;
+ case EDataSlot::String:
+ case EDataSlot::Utf8:
+ case EDataSlot::Yson:
+ case EDataSlot::Json:
+ case EDataSlot::Decimal:
+ case EDataSlot::Uuid:
+ case EDataSlot::TzDate:
+ case EDataSlot::TzDatetime:
+ case EDataSlot::TzTimestamp:
+ case EDataSlot::DyNumber:
+ case EDataSlot::JsonDocument:
+ break;
+ }
+ return false;
+ }
+
+ bool IsSmallType(const TTypeAnnotationNode* type) {
+ if (!IsPrimitiveType(type)) {
+ return false;
+ }
+ using namespace NKikimr::NUdf;
+ switch (type->Cast<TDataExprType>()->GetSlot()) {
+ case EDataSlot::Uint64: // Doesn't fit to 7 bytes in common case
+ case EDataSlot::Int64: // Doesn't fit to 7 bytes in common case
+ case EDataSlot::Double: // Doesn't fit to 7 bytes
+ case EDataSlot::Timestamp:
+ case EDataSlot::Interval:
+ return false;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ }
+
+ void CalcToDictFactors(
+ const TTypeAnnotationNode* keyType,
+ const TTypeAnnotationNode* payloadType,
+ EDictType type, bool many, bool compact,
+ double& sizeFactor, ui64& rowFactor) {
+ sizeFactor = 1.;
+ rowFactor = 0ULL;
+ type = SelectDictType(type, keyType);
+
+ // See https://st.yandex-team.ru/YQL-848#1473441162000 for measures
+ if (compact) {
+ if (!many && ETypeAnnotationKind::Void == payloadType->GetKind()) {
+ if (IsPrimitiveType(keyType)) {
+ // THashedDictSetAccumulator<THashedSingleFixedCompactSetTraits>
+ sizeFactor = 0.;
+ rowFactor = 16;
+ } else {
+ // THashedDictSetAccumulator<THashedCompactSetTraits>
+ sizeFactor = 3.0;
+ rowFactor = 26;
+ }
+ } else {
+ if (IsPrimitiveType(keyType)) {
+ // THashedDictMapAccumulator<THashedSingleFixedCompactMapTraits>
+ sizeFactor = (!IsSmallType(payloadType)) * 3.3;
+ rowFactor = 28;
+ } else {
+ // THashedDictMapAccumulator<THashedCompactMapTraits>
+ sizeFactor = (!IsSmallType(keyType) + !IsSmallType(payloadType)) * 3.3;
+ rowFactor = 28;
+ }
+ }
+ } else if (type == EDictType::Hashed) {
+ // Not tuned. Should not be used with large dicts
+ sizeFactor = 1.5;
+ rowFactor = 46;
+ } else {
+ sizeFactor = 1.5;
+ rowFactor = sizeof(NKikimr::NMiniKQL::TKeyPayloadPair);
+ }
+ }
+
+ TMaybe<TIssue> CalcToDictFactors(const TExprNode& toDictNode, TExprContext& ctx, double& sizeFactor, ui64& rowFactor) {
+ YQL_ENSURE(toDictNode.IsCallable({"ToDict", "SqueezeToDict", "SqlIn"}));
+
+ TMaybe<bool> isMany;
+ TMaybe<EDictType> type;
+ bool isCompact = false;
+ TMaybe<ui64> itemsCount;
+ if (toDictNode.IsCallable("SqlIn")) {
+ if (auto typeAnn = toDictNode.Head().GetTypeAnn(); typeAnn->GetKind() == ETypeAnnotationKind::List
+ && typeAnn->Cast<TListExprType>()->GetItemType()->GetKind() == ETypeAnnotationKind::Struct) {
+ isMany = false;
+ type = EDictType::Auto;
+ isCompact = true;
+
+ auto structType = typeAnn->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ YQL_ENSURE(structType->GetSize() == 1);
+ auto dictKeyType = structType->GetItems()[0]->GetItemType();
+
+ CalcToDictFactors(
+ dictKeyType,
+ ctx.MakeType<TVoidExprType>(),
+ *type, false/*isMany*/, true/*isCompact*/,
+ sizeFactor, rowFactor);
+ }
+ return {};
+ } else if (auto err = ParseToDictSettings(toDictNode, ctx, type, isMany, itemsCount, isCompact)) {
+ return err;
+ }
+
+ CalcToDictFactors(
+ toDictNode.Child(1)->GetTypeAnn(),
+ toDictNode.Child(2)->GetTypeAnn(),
+ *type, *isMany, isCompact,
+ sizeFactor, rowFactor);
+
+ return {};
+ }
+
+ namespace {
+
+ THashSet<TStringBuf> TABLE_CONTENT_CONSUMER = {
+ TStringBuf("Fold"),
+ TStringBuf("Fold1"),
+ TStringBuf("Apply"),
+ TStringBuf("ToOptional"),
+ TStringBuf("Head"),
+ TStringBuf("Last"),
+ TStringBuf("ToDict"),
+ TStringBuf("SqueezeToDict"),
+ TStringBuf("Iterator"), // Why?
+ TStringBuf("Collect"),
+ TStringBuf("Length"),
+ TStringBuf("HasItems"),
+ TStringBuf("SqlIn"),
+ };
+
+ bool HasExternalArgsImpl(const TExprNode& root, TNodeSet& visited, TNodeSet& activeArgs) {
+ if (!visited.emplace(&root).second) {
+ return false;
+ }
+
+ if (root.Type() == TExprNode::Argument) {
+ if (activeArgs.find(&root) == activeArgs.cend()) {
+ return true;
+ }
+ }
+
+ if (root.Type() == TExprNode::Lambda) {
+ root.Child(0)->ForEachChild([&](const TExprNode& arg) {
+ activeArgs.emplace(&arg);
+ });
+ }
+
+ bool res = false;
+ root.ForEachChild([&](const TExprNode& child) {
+ res = res || HasExternalArgsImpl(child, visited, activeArgs);
+ });
+ return res;
+ }
+
+ bool HasExternalArgs(const TExprNode& node) {
+ bool res = false;
+ node.ForEachChild([&](const TExprNode& child) {
+ if (!res && child.Type() == TExprNode::Lambda) {
+ TNodeSet visited;
+ TNodeSet activeArgs;
+ res = res || HasExternalArgsImpl(child, visited, activeArgs);
+ }
+ });
+ return res;
+ }
+ }
+
+ bool GetTableContentConsumerNodes(const TExprNode& node, const TExprNode& rootNode,
+ const TParentsMap& parentsMap, TNodeSet& consumers)
+ {
+ const auto parents = parentsMap.find(&node);
+ if (parents == parentsMap.cend()) {
+ return true;
+ }
+
+ for (const auto& parent : parents->second) {
+ if (parent == &rootNode) {
+ continue;
+ }
+ else if (parent->Type() == TExprNode::Arguments) {
+ continue;
+ }
+ else if (parent->IsCallable("DependsOn")) {
+ continue;
+ }
+ else if (parent->IsCallable(TABLE_CONTENT_CONSUMER)) {
+ if (HasExternalArgs(*parent)) {
+ return false;
+ }
+ consumers.insert(parent);
+ }
+ else if (const auto kind = parent->GetTypeAnn()->GetKind(); ETypeAnnotationKind::Flow == kind || ETypeAnnotationKind::List == kind || ETypeAnnotationKind::Stream == kind) {
+ if (HasExternalArgs(*parent)) {
+ return false;
+ }
+ if (!GetTableContentConsumerNodes(*parent, rootNode, parentsMap, consumers)) {
+ return false;
+ }
+ }
+ else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h b/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h
new file mode 100644
index 0000000000..97077a9f74
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/map.h>
+
+namespace NYql {
+
+struct TYtSettings;
+struct TTypeAnnotationContext;
+
+bool IsRangeComparison(const TStringBuf& operation);
+
+struct TExpressionResorceUsage {
+ ui64 Memory = 0;
+ double Cpu = 1.0;
+};
+
+void ScanResourceUsage(const TExprNode& input, const TYtSettings& config, const TTypeAnnotationContext* types,
+ TMap<TStringBuf, ui64>* memoryUsage, TMap<TStringBuf, double>* cpuUsage, size_t* files);
+TExpressionResorceUsage ScanExtraResourceUsage(const TExprNode& input, const TYtSettings& config);
+
+void CalcToDictFactors(const TTypeAnnotationNode* keyType, const TTypeAnnotationNode* payloadType,
+ EDictType type, bool many, bool compact, double& sizeFactor, ui64& rowFactor);
+TMaybe<TIssue> CalcToDictFactors(const TExprNode& toDictNode, TExprContext& ctx, double& sizeFactor, ui64& rowFactor);
+
+bool GetTableContentConsumerNodes(const TExprNode& node, const TExprNode& rootNode,
+ const TParentsMap& parentsMap, TNodeSet& consumers);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/ya.make b/ydb/library/yql/providers/yt/lib/graph_reorder/ya.make
new file mode 100644
index 0000000000..09c60aea56
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+SRCS(
+ yql_graph_reorder.cpp
+ yql_graph_reorder_old.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/ast
+ ydb/library/yql/utils/log
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/providers/common/provider
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
new file mode 100644
index 0000000000..459074d835
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.cpp
@@ -0,0 +1,358 @@
+#include "yql_graph_reorder.h"
+
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <tuple>
+
+namespace NYql {
+
+using namespace NNodes;
+
+TDependencyUpdater::TDependencyUpdater(TStringBuf provider, TStringBuf newConfigureName)
+ : Provider(provider)
+ , NewConfigureName(newConfigureName)
+{
+}
+
+void TDependencyUpdater::ScanConfigureDeps(const TExprNode::TPtr& node) {
+ VisitedNodes.insert(node.Get());
+
+ TMaybe<TExprNode*> prevConfigure;
+ TMaybe<TVector<TExprNode*>> prevReads;
+ bool popRead = false;
+ // Don't use TCoConfigure().DataSource() - result provider uses DataSink here
+ const bool isConfigure = TCoConfigure::Match(node.Get()) && node->Child(TCoConfigure::idx_DataSource)->Child(0)->Content() == Provider;
+ const bool isNewConfigure = node->IsCallable(NewConfigureName);
+ if (isConfigure || isNewConfigure) {
+ if (LastConfigure) {
+ auto& map = NodeToConfigureDeps[LastConfigure];
+ map.emplace(node, map.size());
+ }
+ if (isConfigure) {
+ Configures.push_back(node);
+ // All Configure! nodes must be present in NodeToConfigureDeps
+ NodeToConfigureDeps.emplace(node.Get(), TSyncMap());
+ }
+ prevConfigure = LastConfigure;
+ LastConfigure = node.Get();
+
+ for (auto read: LastReads) {
+ auto& map = NodeToConfigureDeps[read];
+ map.emplace(node, map.size());
+ }
+ prevReads.ConstructInPlace();
+ prevReads->swap(LastReads);
+ }
+ else if (TCoRead::Match(node.Get()) && TCoRead(node).DataSource().Category().Value() == Provider) {
+ LastReads.push_back(node.Get());
+ popRead = true;
+ }
+
+ if (!isNewConfigure) { // Assume all nodes under provider specific Configure are already processed
+ for (const auto& child : node->Children()) {
+ if (VisitedNodes.cend() == VisitedNodes.find(child.Get()) || (child->Content() != SyncName && child->Content().EndsWith('!'))) {
+ ScanConfigureDeps(child);
+ }
+ }
+ }
+
+ if (prevConfigure) {
+ LastConfigure = *prevConfigure;
+ }
+ if (prevReads) {
+ LastReads.swap(*prevReads);
+ }
+ if (popRead) {
+ LastReads.pop_back();
+ }
+};
+
+void TDependencyUpdater::ScanNewReadDeps(const TExprNode::TPtr& input, TExprContext& ctx) {
+ THashMap<ui32, TExprNode::TPtr> commits;
+ VisitExprByFirst(input, [&](const TExprNode::TPtr& node) {
+ if (TCoCommit::Match(node.Get())) {
+ auto commit = TCoCommit(node);
+ if (commit.DataSink().Cast<TCoDataSink>().Category().Value() == Provider) {
+ auto settings = NCommon::ParseCommitSettings(commit, ctx);
+ if (!settings.Epoch) {
+ return false;
+ }
+
+ ui32 commitEpoch = FromString<ui32>(settings.Epoch.Cast().Value());
+ commits.emplace(commitEpoch, node);
+ }
+ }
+ return true;
+ });
+
+ TNodeMap<ui32> maxEpochs;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (TCoRead::Match(node.Get()) && TCoRead(node).DataSource().Category().Value() == Provider) {
+ if (TMaybe<ui32> maxEpoch = GetReadEpoch(node)) {
+ auto world = node->Child(0);
+ if (*maxEpoch == 0) {
+ if (world->Type() != TExprNode::World
+ && !TCoSync::Match(world)
+ && !world->IsCallable(NewConfigureName))
+ {
+ NewReadToCommitDeps[node.Get()] = TExprNode::TPtr();
+ }
+ }
+ else if (*maxEpoch > 0) {
+ auto commit = commits.find(*maxEpoch);
+ YQL_ENSURE(commits.cend() != commit);
+ if (world != commit->second.Get()
+ && !TCoSync::Match(world)
+ && !world->IsCallable(NewConfigureName))
+ {
+ NewReadToCommitDeps[node.Get()] = commit->second;
+ }
+ }
+ }
+ }
+ return true;
+ });
+}
+
+void TDependencyUpdater::ScanNewWriteDeps(const TExprNode::TPtr& input) {
+ TExprNode::TPtr lastWorld;
+ TExprNode::TPtr lastConfigure;
+ THashMap<TString, std::tuple<TExprNode::TPtr, size_t, TExprNode::TPtr>> writesByTarget; // target table -> (last Write! node, order between different writes, last Configure! dependency)
+ VisitExprByFirst(input,
+ [&](const TExprNode::TPtr& node) {
+ // Initially Sync! and NewConfigureName cannot appear in graph.
+ // So if we catch them then this part of graph is already processed.
+ if (node->IsCallable(NewConfigureName)) {
+ lastConfigure = node;
+ return false;
+ }
+ if (TCoSync::Match(node.Get()) || node->IsCallable(NewConfigureName)) {
+ lastWorld = node;
+ return false;
+ }
+ return true;
+ },
+ [&](const TExprNode::TPtr& node) {
+ if (TCoConfigure::Match(node.Get()) && node->Child(TCoConfigure::idx_DataSource)->Child(0)->Content() == Provider) {
+ lastConfigure = node;
+ return true;
+ }
+
+ if (TCoWrite::Match(node.Get())) {
+ auto category = TCoWrite(node).DataSink().Category().Value();
+ TString target;
+ if (Provider == category) {
+ target = GetWriteTarget(node);
+ } else if (ResultProviderName == category) {
+ target = category;
+ }
+ if (target) {
+ auto& prevWrite = writesByTarget[target];
+ if (std::get<0>(prevWrite)) {
+ if (node->HeadPtr() != std::get<0>(prevWrite)) {
+ NewWriteDeps[node.Get()].push_back(std::get<0>(prevWrite));
+ }
+ if (lastConfigure && lastConfigure != std::get<2>(prevWrite)) {
+ auto& map = NodeToConfigureDeps[node.Get()];
+ map.emplace(lastConfigure, map.size());
+ std::get<2>(prevWrite) = lastConfigure;
+ }
+ } else {
+ std::get<1>(prevWrite) = writesByTarget.size();
+ if (lastWorld && node->HeadPtr() != lastWorld) {
+ NewWriteDeps[node.Get()].push_back(lastWorld);
+ }
+ if (lastConfigure) {
+ auto& map = NodeToConfigureDeps[node.Get()];
+ map.emplace(lastConfigure, map.size());
+ std::get<2>(prevWrite) = lastConfigure;
+ }
+ }
+ std::get<0>(prevWrite) = node;
+ return true;
+ }
+ }
+ if (!writesByTarget.empty()) {
+ if (writesByTarget.size() > 1) {
+ auto& deps = NewWriteDeps[node.Get()];
+ std::vector<std::pair<TExprNode::TPtr, size_t>> values;
+ std::transform(writesByTarget.begin(), writesByTarget.end(), std::back_inserter(values),
+ [](const auto& v) { return std::make_pair(std::get<0>(v.second), std::get<1>(v.second)); });
+ std::sort(values.begin(), values.end(), [](const auto& l, const auto& r) { return l.second < r.second; });
+ std::transform(values.begin(), values.end(), std::back_inserter(deps), [](const auto& v) { return v.first; });
+ } else {
+ if (node->HeadPtr() != std::get<0>(writesByTarget.begin()->second)) {
+ NewWriteDeps[node.Get()].push_back(std::get<0>(writesByTarget.begin()->second));
+ }
+ }
+ }
+
+ lastWorld = node;
+ writesByTarget.clear();
+ return true;
+ }
+ );
+}
+
+TExprNode::TPtr TDependencyUpdater::MakeSync(const TSyncMap& nodes, TExprNode::TChildrenType extra, TPositionHandle pos, TExprContext& ctx) {
+ using TPair = std::pair<TExprNode::TPtr, ui64>;
+ TVector<TPair> sortedList(nodes.cbegin(), nodes.cend());
+ Sort(sortedList, [](const TPair& x, const TPair& y) { return x.second < y.second; });
+ TExprNode::TListType syncChildren;
+ syncChildren.reserve(sortedList.size() + extra.size());
+ std::transform(sortedList.begin(), sortedList.end(), std::back_inserter(syncChildren), [](const TPair& x) { return x.first; });
+ if (extra) {
+ syncChildren.insert(syncChildren.end(), extra.begin(), extra.end());
+ }
+ syncChildren.erase(std::remove_if(syncChildren.begin(), syncChildren.end(), [](const auto& n) { return n->IsWorld(); } ), syncChildren.end());
+ return syncChildren.empty()
+ ? ctx.NewWorld(pos)
+ : syncChildren.size() == 1
+ ? syncChildren.front()
+ : ctx.NewCallable(pos, TCoSync::CallableName(), std::move(syncChildren));
+}
+
+IGraphTransformer::TStatus TDependencyUpdater::ReorderGraph(const TExprNode::TPtr& input, TExprNode::TPtr& output,
+ TExprContext& ctx)
+{
+ ScanConfigureDeps(input);
+ ScanNewReadDeps(input, ctx);
+ ScanNewWriteDeps(input);
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ IGraphTransformer::TStatus status = IGraphTransformer::TStatus::Ok;
+
+ output = input;
+ if (!NewReadToCommitDeps.empty() || !NewWriteDeps.empty()) {
+ if (!NodeToConfigureDeps.empty()) {
+ // Exclude all our Configure! from the graph
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& /*ctx*/) -> TExprNode::TPtr {
+ if (TCoConfigure::Match(node.Get()) && NodeToConfigureDeps.contains(node.Get())) {
+ // Skip Configure! itself
+ return node;
+ }
+ if (node->ChildrenSize() > 0) {
+ TExprNode::TPtr next = node->ChildPtr(0);
+ while (TCoConfigure::Match(next.Get()) && NodeToConfigureDeps.contains(next.Get())) {
+ next = next->ChildPtr(0);
+ }
+ if (next != node->ChildPtr(0)) {
+ node->ChildRef(0) = next;
+ }
+ }
+ return node;
+ }, ctx, settings));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ // Update root
+ while (TCoConfigure::Match(output.Get()) && NodeToConfigureDeps.contains(output.Get())) {
+ output = output->ChildPtr(0);
+ }
+
+ // Make a separate chain of Configure! nodes
+ for (auto& item: NodeToConfigureDeps) {
+ if (!TCoConfigure::Match(item.first)) {
+ continue;
+ }
+ if (item.second.empty()) {
+ if (item.first->Child(0)->Type() != TExprNode::World) {
+ item.first->ChildRef(0) = ctx.NewWorld(item.first->Pos());
+ }
+ }
+ else {
+ item.first->ChildRef(0) = MakeSync(item.second, {}, item.first->Pos(), ctx);
+ }
+ }
+ }
+
+ // Reorder graph
+ TNodeOnNodeOwnedMap oldReadDeps;
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (node->ChildrenSize() > 0 && TCoLeft::Match(node->Child(0))) {
+ auto read = node->Child(0)->Child(0);
+ auto found = oldReadDeps.find(read);
+ if (found != oldReadDeps.cend()) {
+ node->ChildRef(0) = found->second;
+ }
+ else if (read->ChildrenSize() > 1 && read->Child(1)->Child(0)->Content() == Provider) {
+ node->ChildRef(0) = read->ChildPtr(0);
+ }
+ }
+
+ if (TCoRight::Match(node.Get())) {
+ auto read = node->ChildPtr(0);
+ auto confSet = NodeToConfigureDeps.FindPtr(read.Get());
+ auto found = NewReadToCommitDeps.find(read.Get());
+ if (NewReadToCommitDeps.cend() != found) {
+ TExprNode::TPtr newWorld;
+ if (!found->second) {
+ if (confSet) {
+ YQL_ENSURE(!confSet->empty());
+ newWorld = MakeSync(*confSet, {}, read->Pos(), ctx);
+ }
+ else if (read->Child(0)->Type() != TExprNode::World) {
+ newWorld = ctx.NewWorld(read->Pos());
+ }
+ }
+ else {
+ if (confSet) {
+ newWorld = MakeSync(*confSet, {found->second}, read->Pos(), ctx);
+ }
+ else if (read->Child(0) != found->second.Get()) {
+ newWorld = found->second;
+ }
+ }
+
+ if (newWorld) {
+ oldReadDeps[read.Get()] = read->ChildPtr(0);
+ read->ChildRef(0) = newWorld;
+ }
+ }
+ else if (confSet) {
+ read->ChildRef(0) = MakeSync(*confSet, {read->HeadPtr()}, read->Pos(), ctx);
+ }
+ }
+ else if (!TCoRead::Match(node.Get())) {
+ auto deps = NewWriteDeps.Value(node.Get(), TExprNode::TListType());
+ if (auto confSet = NodeToConfigureDeps.FindPtr(node.Get())) {
+ if (deps.empty()) {
+ deps.push_back(node->HeadPtr());
+ }
+ node->ChildRef(0) = MakeSync(*confSet, deps, node->Pos(), ctx);
+ } else if (!deps.empty()) {
+ node->ChildRef(0) = MakeSync({}, deps, node->Pos(), ctx);
+ }
+ }
+ return node;
+ }, ctx, settings));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "DependencyUpdater-ReorderGraph";
+ }
+
+ if (!NodeToConfigureDeps.empty()) {
+ // Rename Configure! nodes
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ // Don't use TCoConfigure().DataSource() - result provider uses DataSink here
+ if (TCoConfigure::Match(node.Get()) && node->Child(TCoConfigure::idx_DataSource)->Child(0)->Content() == Provider) {
+ return ctx.RenameNode(*node, NewConfigureName);
+ }
+ return node;
+ }, ctx, settings));
+ }
+
+ return status;
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.h b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.h
new file mode 100644
index 0000000000..132f979787
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/digest/numeric.h>
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/maybe.h>
+
+namespace NYql {
+
+// Scans (Configure!/Read!/Write!/Commit!)<-Configure! dependencies
+class TDependencyUpdater {
+public:
+ TDependencyUpdater(TStringBuf provider, TStringBuf newConfigureName);
+ virtual ~TDependencyUpdater() = default;
+
+ IGraphTransformer::TStatus ReorderGraph(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx);
+
+protected:
+ virtual TMaybe<ui32> GetReadEpoch(const TExprNode::TPtr& readNode) const = 0;
+ virtual TString GetWriteTarget(const TExprNode::TPtr& writeNode) const = 0;
+
+ void ScanConfigureDeps(const TExprNode::TPtr& node);
+ void ScanNewReadDeps(const TExprNode::TPtr& input, TExprContext& ctx);
+ void ScanNewWriteDeps(const TExprNode::TPtr& input);
+ static TExprNode::TPtr MakeSync(const TSyncMap& nodes, TExprNode::TChildrenType extra, TPositionHandle pos, TExprContext& ctx);
+
+private:
+
+ const TStringBuf Provider;
+ const TStringBuf NewConfigureName;
+ THashMap<TExprNode*, TSyncMap> NodeToConfigureDeps;
+ TExprNode::TListType Configures; // To protect Configure! from die
+ TNodeOnNodeOwnedMap NewReadToCommitDeps;
+ TNodeSet VisitedNodes;
+ TExprNode* LastConfigure = nullptr;
+ TVector<TExprNode*> LastReads;
+ THashMap<TExprNode*, TExprNode::TListType> NewWriteDeps;
+};
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
new file mode 100644
index 0000000000..adff9393c3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.cpp
@@ -0,0 +1,276 @@
+#include "yql_graph_reorder_old.h"
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+TDependencyUpdaterOld::TDependencyUpdaterOld(TStringBuf provider, TStringBuf newConfigureName)
+ : Provider(provider)
+ , NewConfigureName(newConfigureName)
+{
+}
+
+void TDependencyUpdaterOld::ScanConfigureDeps(const TExprNode::TPtr& node) {
+ VisitedNodes.insert(node.Get());
+
+ TMaybe<TExprNode*> prevConfigure;
+ TMaybe<TExprNode*> prevWrite;
+ TMaybe<TVector<TExprNode*>> prevReads;
+ bool popRead = false;
+ // Don't use TCoConfigure().DataSource() - result provider uses DataSink here
+ const bool isConfigure = TCoConfigure::Match(node.Get()) && node->Child(TCoConfigure::idx_DataSource)->Child(0)->Content() == Provider;
+ const bool isNewConfigure = node->IsCallable(NewConfigureName);
+ if (isConfigure || isNewConfigure) {
+ if (LastConfigure) {
+ NodeToConfigureDeps[LastConfigure].insert(node);
+ }
+ if (isConfigure) {
+ Configures.push_back(node);
+ // All Configure! nodes must be present in NodeToConfigureDeps
+ NodeToConfigureDeps.emplace(node.Get(), THashSet<TExprNode::TPtr, TTExprNodePtrHash>());
+ }
+ prevConfigure = LastConfigure;
+ LastConfigure = node.Get();
+
+ if (LastWrite) {
+ NodeToConfigureDeps[LastWrite].insert(node);
+ }
+ prevWrite = LastWrite;
+ LastWrite = nullptr;
+
+ for (auto read: LastReads) {
+ NodeToConfigureDeps[read].insert(node);
+ }
+ prevReads.ConstructInPlace();
+ prevReads->swap(LastReads);
+ }
+ else if (TCoRead::Match(node.Get()) && TCoRead(node).DataSource().Category().Value() == Provider) {
+ LastReads.push_back(node.Get());
+ popRead = true;
+ }
+ else if (TCoWrite::Match(node.Get())) {
+ auto category = TCoWrite(node).DataSink().Category().Value();
+ if (Provider == category || ResultProviderName == category) {
+ prevWrite = LastWrite;
+ LastWrite = node.Get();
+ }
+ }
+
+ if (!isNewConfigure) { // Assume all nodes under provider specific Configure are already processed
+ for (const auto& child : node->Children()) {
+ if (VisitedNodes.cend() == VisitedNodes.find(child.Get()) || child->Content().EndsWith('!')) {
+ ScanConfigureDeps(child);
+ }
+ }
+ }
+
+ if (prevConfigure) {
+ LastConfigure = *prevConfigure;
+ }
+ if (prevWrite) {
+ LastWrite = *prevWrite;
+ }
+ if (prevReads) {
+ LastReads.swap(*prevReads);
+ }
+ if (popRead) {
+ LastReads.pop_back();
+ }
+};
+
+void TDependencyUpdaterOld::ScanNewReadDeps(const TExprNode::TPtr& input, TExprContext& ctx) {
+ THashMap<ui32, TExprNode::TPtr> commits;
+ VisitExprByFirst(input, [&](const TExprNode::TPtr& node) {
+ if (TCoCommit::Match(node.Get())) {
+ auto commit = TCoCommit(node);
+ if (commit.DataSink().Cast<TCoDataSink>().Category().Value() == Provider) {
+ auto settings = NCommon::ParseCommitSettings(commit, ctx);
+ if (!settings.Epoch) {
+ return false;
+ }
+
+ ui32 commitEpoch = FromString<ui32>(settings.Epoch.Cast().Value());
+ commits.emplace(commitEpoch, node);
+ }
+ }
+ return true;
+ });
+
+ TNodeMap<ui32> maxEpochs;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (TCoRead::Match(node.Get()) && TCoRead(node).DataSource().Category().Value() == Provider) {
+ if (TMaybe<ui32> maxEpoch = GetReadEpoch(node, ctx)) {
+ auto world = node->Child(0);
+ if (*maxEpoch == 0) {
+ if (world->Type() != TExprNode::World
+ && !TCoSync::Match(world)
+ && !world->IsCallable(NewConfigureName))
+ {
+ NewReadToCommitDeps[node.Get()] = TExprNode::TPtr();
+ }
+ }
+ else if (*maxEpoch > 0) {
+ auto commit = commits.find(*maxEpoch);
+ YQL_ENSURE(commits.cend() != commit);
+ if (world != commit->second.Get()
+ && !TCoSync::Match(world)
+ && !world->IsCallable(NewConfigureName))
+ {
+ NewReadToCommitDeps[node.Get()] = commit->second;
+ }
+ }
+ }
+ }
+ return true;
+ });
+}
+
+IGraphTransformer::TStatus TDependencyUpdaterOld::ReorderGraph(const TExprNode::TPtr& input, TExprNode::TPtr& output,
+ TExprContext& ctx)
+{
+ ScanConfigureDeps(input);
+ ScanNewReadDeps(input, ctx);
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ IGraphTransformer::TStatus status = IGraphTransformer::TStatus::Ok;
+
+ output = input;
+ if (!NewReadToCommitDeps.empty()) {
+ if (!NodeToConfigureDeps.empty()) {
+ // Exclude all our Configure! from the graph
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& /*ctx*/) -> TExprNode::TPtr {
+ if (TCoConfigure::Match(node.Get()) && NodeToConfigureDeps.contains(node.Get())) {
+ // Skip Configure! itself
+ return node;
+ }
+ if (node->ChildrenSize() > 0) {
+ TExprNode::TPtr next = node->ChildPtr(0);
+ while (TCoConfigure::Match(next.Get()) && NodeToConfigureDeps.contains(next.Get())) {
+ next = next->ChildPtr(0);
+ }
+ if (next != node->ChildPtr(0)) {
+ node->ChildRef(0) = next;
+ }
+ }
+ return node;
+ }, ctx, settings));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ // Update root
+ while (TCoConfigure::Match(output.Get()) && NodeToConfigureDeps.contains(output.Get())) {
+ output = output->ChildPtr(0);
+ }
+
+ // Make a separate chain of Configure! nodes
+ for (auto& item: NodeToConfigureDeps) {
+ if (!TCoConfigure::Match(item.first)) {
+ continue;
+ }
+ if (item.second.empty()) {
+ if (item.first->Child(0)->Type() != TExprNode::World) {
+ item.first->ChildRef(0) = ctx.NewWorld(item.first->Pos());
+ }
+ }
+ else if (item.second.size() > 1) {
+ item.first->ChildRef(0) = ctx.NewCallable(item.first->Pos(), TCoSync::CallableName(), TExprNode::TListType(item.second.begin(), item.second.end()));
+ }
+ else if (item.first->ChildPtr(0) != *item.second.begin()) {
+ item.first->ChildRef(0) = *item.second.begin();
+ }
+ }
+ }
+
+ // Reorder graph
+ TNodeOnNodeOwnedMap oldReadDeps;
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (node->ChildrenSize() > 0 && TCoLeft::Match(node->Child(0))) {
+ auto read = node->Child(0)->Child(0);
+ auto found = oldReadDeps.find(read);
+ if (found != oldReadDeps.cend()) {
+ node->ChildRef(0) = found->second;
+ }
+ else if (read->ChildrenSize() > 1 && read->Child(1)->Child(0)->Content() == Provider) {
+ node->ChildRef(0) = read->ChildPtr(0);
+ }
+ }
+
+ if (TCoRight::Match(node.Get())) {
+ auto read = node->ChildPtr(0);
+ auto confSet = NodeToConfigureDeps.FindPtr(read.Get());
+ auto found = NewReadToCommitDeps.find(read.Get());
+ if (NewReadToCommitDeps.cend() != found) {
+ TExprNode::TPtr newWorld;
+ if (!found->second) {
+ if (confSet) {
+ YQL_ENSURE(!confSet->empty());
+ newWorld = confSet->size() == 1
+ ? *confSet->begin()
+ : ctx.NewCallable(read->Pos(), TCoSync::CallableName(), TExprNode::TListType(confSet->begin(), confSet->end()));
+ }
+ else if (read->Child(0)->Type() != TExprNode::World) {
+ newWorld = ctx.NewWorld(read->Pos());
+ }
+ }
+ else {
+ if (confSet) {
+ auto syncChildren = TExprNode::TListType(confSet->begin(), confSet->end());
+ syncChildren.push_back(found->second);
+ newWorld = ctx.NewCallable(read->Pos(), TCoSync::CallableName(), std::move(syncChildren));
+ }
+ else if (read->Child(0) != found->second.Get()) {
+ newWorld = found->second;
+ }
+ }
+
+ if (newWorld) {
+ oldReadDeps[read.Get()] = read->ChildPtr(0);
+ read->ChildRef(0) = newWorld;
+ }
+ }
+ else if (confSet) {
+ auto syncChildren = TExprNode::TListType(confSet->begin(), confSet->end());
+ syncChildren.push_back(read->ChildPtr(0));
+ read->ChildRef(0) = ctx.NewCallable(read->Pos(), TCoSync::CallableName(), std::move(syncChildren));
+ }
+ }
+ else if (TCoWrite::Match(node.Get())) {
+ if (auto confSet = NodeToConfigureDeps.FindPtr(node.Get())) {
+ auto syncChildren = TExprNode::TListType(confSet->begin(), confSet->end());
+ syncChildren.push_back(node->ChildPtr(0));
+ node->ChildRef(0) = ctx.NewCallable(node->Pos(), TCoSync::CallableName(), std::move(syncChildren));
+ }
+ }
+ return node;
+ }, ctx, settings));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "DependencyUpdater-ReorderGraph";
+ }
+
+ if (!NodeToConfigureDeps.empty()) {
+ // Rename Configure! nodes
+ status = status.Combine(OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ // Don't use TCoConfigure().DataSource() - result provider uses DataSink here
+ if (TCoConfigure::Match(node.Get()) && node->Child(TCoConfigure::idx_DataSource)->Child(0)->Content() == Provider) {
+ return ctx.RenameNode(*node, NewConfigureName);
+ }
+ return node;
+ }, ctx, settings));
+ }
+
+ return status;
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.h b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.h
new file mode 100644
index 0000000000..757594088d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/digest/numeric.h>
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/maybe.h>
+
+namespace NYql {
+
+// Snans (Configure!/Read!/Write!/Commit!)<-Configure! dependencies
+class TDependencyUpdaterOld {
+public:
+ TDependencyUpdaterOld(TStringBuf provider, TStringBuf newConfigureName);
+ virtual ~TDependencyUpdaterOld() = default;
+
+ IGraphTransformer::TStatus ReorderGraph(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx);
+
+protected:
+ virtual TMaybe<ui32> GetReadEpoch(const TExprNode::TPtr& readNode, TExprContext& ctx) const = 0;
+
+ void ScanConfigureDeps(const TExprNode::TPtr& node);
+ void ScanNewReadDeps(const TExprNode::TPtr& input, TExprContext& ctx);
+
+private:
+ struct TTExprNodePtrHash {
+ inline size_t operator()(const TExprNode::TPtr& node) const {
+ return NumericHash(node.Get());
+ }
+ };
+
+ const TStringBuf Provider;
+ const TStringBuf NewConfigureName;
+ THashMap<TExprNode*, THashSet<TExprNode::TPtr, TTExprNodePtrHash>> NodeToConfigureDeps;
+ TExprNode::TListType Configures; // To protect Configure! from die
+ TNodeOnNodeOwnedMap NewReadToCommitDeps;
+ TNodeSet VisitedNodes;
+ TExprNode* LastConfigure = nullptr;
+ TExprNode* LastWrite = nullptr;
+ TVector<TExprNode*> LastReads;
+};
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/lib/hash/ya.make b/ydb/library/yql/providers/yt/lib/hash/ya.make
new file mode 100644
index 0000000000..6692b7595f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ yql_hash_builder.cpp
+ yql_op_hash.cpp
+)
+
+PEERDIR(
+ contrib/libs/openssl
+ ydb/library/yql/ast
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/core
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp b/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
new file mode 100644
index 0000000000..8a2a843f3f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.cpp
@@ -0,0 +1,27 @@
+#include "yql_hash_builder.h"
+
+namespace NYql {
+
+THashBuilder::THashBuilder() {
+ SHA256_Init(&Sha256);
+}
+
+void THashBuilder::Update(const void* data, size_t len) {
+ SHA256_Update(&Sha256, data, len);
+}
+
+void THashBuilder::Update(TStringBuf data) {
+ SHA256_Update(&Sha256, data.data(), data.size());
+}
+
+void THashBuilder::Update(const TString& data) {
+ SHA256_Update(&Sha256, data.data(), data.size());
+}
+
+TString THashBuilder::Finish() {
+ unsigned char hash[SHA256_DIGEST_LENGTH];
+ SHA256_Final(hash, &Sha256);
+ return TString((const char*)hash, (const char*)hash + SHA256_DIGEST_LENGTH);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h b/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h
new file mode 100644
index 0000000000..7e7269f7f5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+
+#include <contrib/libs/openssl/include/openssl/sha.h>
+
+#include <utility>
+#include <type_traits>
+
+namespace NYql {
+
+class THashBuilder {
+public:
+ THashBuilder();
+
+ template <typename T, std::enable_if_t<std::is_pod<std::remove_cv_t<T>>::value>* = nullptr>
+ void Update(T data) {
+ Update(&data, sizeof(data));
+ }
+ void Update(TStringBuf data);
+ void Update(const TString& data);
+
+ TString Finish();
+
+private:
+ void Update(const void* data, size_t len);
+
+private:
+ SHA256_CTX Sha256;
+};
+
+template <class T>
+static inline THashBuilder& operator<<(THashBuilder& builder, const T& t) {
+ builder.Update(t);
+ return builder;
+}
+
+template <class T>
+static inline THashBuilder&& operator<<(THashBuilder&& builder, const T& t) {
+ builder.Update(t);
+ return std::move(builder);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp b/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
new file mode 100644
index 0000000000..5f3f5fe9d5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.cpp
@@ -0,0 +1,137 @@
+#include "yql_op_hash.h"
+#include "yql_hash_builder.h"
+
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+void TNodeHashCalculator::UpdateFileHash(THashBuilder& builder, TStringBuf alias) const {
+ auto block = Types.UserDataStorage->FindUserDataBlock(alias);
+ YQL_ENSURE(block, "File " << alias << " not found");
+ if (block->FrozenFile || block->Type != EUserDataType::URL) {
+ YQL_ENSURE(block->FrozenFile, "File " << alias << " is not frozen");
+ YQL_ENSURE(block->FrozenFile->GetMd5(), "MD5 for file " << alias << " is empty");
+
+ builder << alias << (ui32)block->Type << block->FrozenFile->GetMd5();
+ return;
+ }
+
+ // temporary approach: for YT remote files we support URL hashing rather than file content hashing
+ // todo: rework it and use file metadata
+ builder << alias << (ui32)block->Type << block->Data;
+}
+
+bool TNodeHashCalculator::UpdateChildrenHash(THashBuilder& builder, const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel, size_t fromIndex) const {
+ for (size_t i = fromIndex; i < node.ChildrenSize(); ++i) {
+ auto childHash = GetHashImpl(*node.Child(i), argIndex, frameLevel);
+ if (childHash.empty()) {
+ return false;
+ }
+
+ builder << childHash;
+ }
+ return true;
+}
+
+TString TNodeHashCalculator::GetHashImpl(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const {
+ auto it = NodeHash.find(node.UniqueId());
+ if (it != NodeHash.end()) {
+ return it->second;
+ }
+
+ bool isHashable = true;
+ TString myHash;
+ ui32 typeNum = node.Type();
+ THashBuilder builder;
+ builder << Salt << typeNum;
+ switch (node.Type()) {
+ case TExprNode::List: {
+ if (!UpdateChildrenHash(builder, node, argIndex, frameLevel)) {
+ isHashable = false;
+ }
+ break;
+ }
+ case TExprNode::Atom: {
+ builder << node.Content();
+ break;
+ }
+ case TExprNode::Callable: {
+ if (auto p = Hashers.FindPtr(node.Content())) {
+ auto callableHash = (*p)(node, argIndex, frameLevel);
+ if (callableHash.empty()) {
+ isHashable = false;
+ }
+ else {
+ builder << callableHash;
+ }
+ }
+ else {
+ builder << node.Content();
+ if (node.ChildrenSize() > 0 && node.Child(0)->GetTypeAnn()->GetKind() == ETypeAnnotationKind::World) {
+ YQL_CLOG(ERROR, ProviderYt) << "Cannot calculate hash for " << node.Content();
+ isHashable = false;
+ }
+ else {
+ if (!UpdateChildrenHash(builder, node, argIndex, frameLevel)) {
+ isHashable = false;
+ }
+ else {
+ if (node.Content() == "Udf" && node.ChildrenSize() == 7 && !node.Child(6)->Content().empty()) {
+ // an udf from imported file, use hash of file
+ auto alias = node.Child(6)->Content();
+ UpdateFileHash(builder, alias);
+ } else if (node.Content() == "FilePath" || node.Content() == "FileContent") {
+ auto alias = node.Child(0)->Content();
+ UpdateFileHash(builder, alias);
+ } else if (node.Content() == "FolderPath") {
+ auto alias = node.Child(0)->Content();
+ auto blocks = Types.UserDataStorage->FindUserDataFolder(alias);
+ YQL_ENSURE(blocks, "Folder" << alias << " not found");
+ // keys for blocks must be ordered (not a hashmap)
+ for (const auto& x : *blocks) {
+ UpdateFileHash(builder, x.first.Alias());
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ case TExprNode::Lambda: {
+ ui32 pos = 0;
+ for (const auto& arg : node.Child(0)->Children()) {
+ // argument is described by it's frame level (starting from 1) and position
+ YQL_ENSURE(argIndex.insert(std::make_pair(arg.Get(), std::make_pair(frameLevel + 1, pos++ ))).second);
+ }
+
+ if (!UpdateChildrenHash(builder, node, argIndex, frameLevel + 1, 1)) {
+ isHashable = false;
+ }
+ break;
+ }
+
+ case TExprNode::Argument: {
+ auto it = argIndex.find(&node);
+ YQL_ENSURE(it != argIndex.end());
+ builder << (frameLevel - it->second.first) << it->second.second;
+ break;
+ }
+
+ case TExprNode::World:
+ isHashable = false;
+ break;
+ default:
+ YQL_ENSURE(false, "unexpected");
+ }
+
+ if (isHashable) {
+ myHash = builder.Finish();
+ }
+
+ NodeHash.emplace(node.UniqueId(), myHash);
+ return myHash;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.h b/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.h
new file mode 100644
index 0000000000..6742a5e5fc
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/hash/yql_op_hash.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+#include <util/generic/hash.h>
+
+#include <utility>
+#include <functional>
+
+namespace NYql {
+
+struct TTypeAnnotationContext;
+class THashBuilder;
+
+class TNodeHashCalculator {
+public:
+ using TArgIndex = THashMap<const TExprNode*, std::pair<ui32, ui32>>;
+ using THasher = std::function<TString(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel)>;
+ using THasherMap = THashMap<TStringBuf, THasher>;
+
+ TNodeHashCalculator(const TTypeAnnotationContext& types, std::unordered_map<ui64, TString>& nodeHash, const TString& salt)
+ : Types(types)
+ , NodeHash(nodeHash)
+ , Salt(salt)
+ {
+ }
+
+ TString GetHash(const TExprNode& node) const {
+ TArgIndex argIndex;
+ return GetHashImpl(node, argIndex, 0);
+ }
+
+protected:
+ void UpdateFileHash(THashBuilder& builder, TStringBuf alias) const;
+ bool UpdateChildrenHash(THashBuilder& builder, const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel, size_t fromIndex = 0) const;
+ TString GetHashImpl(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const;
+
+protected:
+ const TTypeAnnotationContext& Types;
+ std::unordered_map<ui64, TString>& NodeHash;
+ THasherMap Hashers;
+ TString Salt;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp b/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
new file mode 100644
index 0000000000..f43978a3c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.cpp
@@ -0,0 +1,112 @@
+#include "infer_schema.h"
+
+#include <ydb/library/yql/core/issue/yql_issue.h>
+#include <ydb/library/yql/public/issue/yql_issue.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/yexception.h>
+
+
+namespace NYql {
+
+TStreamSchemaInferer::TStreamSchemaInferer(const TString& tableName)
+ : TableName(tableName)
+ , UsedRows(0)
+{
+}
+
+void TStreamSchemaInferer::AddRow(const NYT::TNode& row)
+{
+ for (const auto& column : row.AsMap()) {
+ if (column.first.StartsWith("$")) {
+ // Skip system columns
+ continue;
+ }
+ TString columnType;
+ switch (column.second.GetType()) {
+ case NYT::TNode::String:
+ columnType = "string";
+ break;
+ case NYT::TNode::Int64:
+ columnType = "int64";
+ break;
+ case NYT::TNode::Uint64:
+ columnType = "uint64";
+ break;
+ case NYT::TNode::Double:
+ columnType = "double";
+ break;
+ case NYT::TNode::Bool:
+ columnType = "boolean";
+ break;
+ case NYT::TNode::List:
+ columnType = "any";
+ break;
+ case NYT::TNode::Map:
+ columnType = "any";
+ break;
+ case NYT::TNode::Null:
+ continue;
+ default:
+ YQL_LOG_CTX_THROW yexception() <<
+ "Cannot infer schema for table " << TableName <<
+ ", undefined NYT::TNode";
+ }
+
+ auto& type = ColumnTypes[column.first];
+ if (type.empty()) {
+ type = columnType;
+ } else if (type != columnType) {
+ type = "any";
+ }
+ }
+ ++UsedRows;
+}
+
+NYT::TNode TStreamSchemaInferer::GetSchema() const
+{
+ NYT::TNode schema = NYT::TNode::CreateList();
+ for (auto& x : ColumnTypes) {
+ auto& columnNode = schema.Add();
+ columnNode["name"] = x.first;
+ columnNode["type"] = x.second;
+ }
+
+ if (schema.Empty()) {
+ YQL_LOG_CTX_THROW TErrorException(TIssuesIds::YT_INFER_SCHEMA) <<
+ "Cannot infer schema for table " << TableName <<
+ ", first " << UsedRows << " row(s) has no columns";
+ }
+ schema.Attributes()["strict"] = false;
+ return schema;
+}
+
+NYT::TNode InferSchemaFromSample(const NYT::TNode& sample, const TString& tableName, ui32 rows)
+{
+ TStreamSchemaInferer inferer(tableName);
+ const size_t useRows = Min<size_t>(rows, sample.AsList().size());
+
+ for (size_t i = 0; i < useRows; ++i) {
+ inferer.AddRow(sample.AsList()[i]);
+ }
+
+ return inferer.GetSchema();
+}
+
+TMaybe<NYT::TNode> InferSchemaFromTableContents(const NYT::ITransactionPtr& tx, const TString& tableId, const TString& tableName, ui32 rows)
+{
+ auto reader = tx->CreateTableReader<NYT::TNode>(NYT::TRichYPath(tableId).AddRange(NYT::TReadRange::FromRowIndices(0, rows)));
+ if (!reader->IsValid()) {
+ return Nothing();
+ }
+
+ TStreamSchemaInferer inferer(tableName);
+ for (ui32 i = 0; reader->IsValid() && i < rows; ++i) {
+ inferer.AddRow(reader->GetRow());
+ reader->Next();
+ }
+
+ return inferer.GetSchema();
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h b/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h
new file mode 100644
index 0000000000..3e467cee20
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/infer_schema.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+#include <util/generic/map.h>
+
+#include <functional>
+#include <utility>
+
+namespace NYql {
+
+class TStreamSchemaInferer
+{
+public:
+ explicit TStreamSchemaInferer(const TString& tableName);
+ void AddRow(const NYT::TNode& row);
+ NYT::TNode GetSchema() const;
+
+private:
+ const TString TableName;
+ TMap<TString, TString> ColumnTypes;
+ size_t UsedRows;
+};
+
+NYT::TNode InferSchemaFromSample(const NYT::TNode& sample, const TString& tableName, ui32 rows);
+TMaybe<NYT::TNode> InferSchemaFromTableContents(const NYT::ITransactionPtr& tx, const TString& tableId, const TString& tableName, ui32 rows);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/infer_schema/ya.make b/ydb/library/yql/providers/yt/lib/infer_schema/ya.make
new file mode 100644
index 0000000000..e9753b36af
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/infer_schema/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+SRCS(
+ infer_schema.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/public/issue
+ ydb/library/yql/utils/log
+ ydb/library/yql/core/issue
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp b/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
new file mode 100644
index 0000000000..95619580f5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/init.cpp
@@ -0,0 +1,96 @@
+#include "init.h"
+
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/client/init.h>
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/singleton.h>
+#include <util/system/compiler.h>
+#include <util/system/env.h>
+
+namespace NYql {
+
+namespace {
+
+class TInitYtApi {
+public:
+ TInitYtApi()
+ : DeterministicMode_(GetEnv("YQL_DETERMINISTIC_MODE"))
+ {
+ YQL_CLOG(DEBUG, ProviderYt) << "Entering YT API init";
+ if (NYT::NDetail::GetInitStatus() == NYT::NDetail::EInitStatus::NotInitialized) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Init jobless YT API";
+ NYT::JoblessInitialize();
+ }
+ NYT::TConfig::Get()->RetryCount = 1 << 6;
+ NYT::TConfig::Get()->StartOperationRetryCount = 1 << 10;
+ if (GetEnv("YT_FORCE_IPV6").empty()) {
+ NYT::TConfig::Get()->ForceIpV6 = true;
+ }
+ if (NYT::TConfig::Get()->Prefix.empty()) {
+ NYT::TConfig::Get()->Prefix = "//";
+ }
+ NYT::TConfig::Get()->UseAsyncTxPinger = true;
+ NYT::TConfig::Get()->AsyncHttpClientThreads = 2;
+ NYT::TConfig::Get()->AsyncTxPingerPoolThreads = 2;
+ YQL_CLOG(DEBUG, ProviderYt) << "Using YT global prefix: " << NYT::TConfig::Get()->Prefix;
+ }
+
+ void UpdateProps(const TMaybe<TString>& attrs) {
+ if (!attrs) {
+ return;
+ }
+ NYT::TNode node;
+ try {
+ node = NYT::NodeFromYsonString(*attrs);
+ node.AsMap();
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << "Cannot parse yql operation attrs: " << CurrentExceptionMessage();
+ return;
+ }
+ if (DeterministicMode_) {
+ TString prefix;
+ if (auto p = node.AsMap().FindPtr("test_prefix")) {
+ prefix = p->AsString();
+ }
+ if (prefix) {
+ if (!prefix.StartsWith("//")) {
+ prefix.prepend("//");
+ }
+ if (!prefix.EndsWith('/')) {
+ prefix.append('/');
+ }
+ if (NYT::TConfig::Get()->Prefix != prefix) {
+ NYT::TConfig::Get()->Prefix = prefix;
+ YQL_CLOG(DEBUG, ProviderYt) << "Using YT global prefix: " << prefix;
+ }
+ }
+ }
+ if (auto p = node.AsMap().FindPtr("poller_interval")) {
+ try {
+ if (auto d = TDuration::Parse(p->AsString()); d >= TDuration::MilliSeconds(200) && d <= TDuration::Seconds(10)) {
+ NYT::TConfig::Get()->WaitLockPollInterval = d;
+ YQL_CLOG(DEBUG, ProviderYt) << "Set poller_interval to " << d;
+ } else {
+ YQL_CLOG(ERROR, ProviderYt) << "Invalid poller_interval value " << p->AsString();
+ }
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << "Cannot parse poller_interval: " << CurrentExceptionMessage();
+ }
+ }
+ }
+private:
+ const bool DeterministicMode_;
+};
+
+}
+
+void InitYtApiOnce(const TMaybe<TString>& attrs) {
+ Singleton<TInitYtApi>()->UpdateProps(attrs);
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/init.h b/ydb/library/yql/providers/yt/lib/init_yt_api/init.h
new file mode 100644
index 0000000000..1925b314ba
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/init.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <util/generic/string.h>
+#include <util/generic/maybe.h>
+
+namespace NYql {
+
+void InitYtApiOnce(const TMaybe<TString>& attrs = Nothing());
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/init_yt_api/ya.make b/ydb/library/yql/providers/yt/lib/init_yt_api/ya.make
new file mode 100644
index 0000000000..9248d44ec6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/init_yt_api/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+SRCS(
+ init.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/utils/log
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/client
+ library/cpp/yson/node
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/ya.make b/ydb/library/yql/providers/yt/lib/key_filter/ya.make
new file mode 100644
index 0000000000..9cb5960da4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ yql_key_filter.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core
+ ydb/library/yql/utils
+ ydb/library/yql/ast
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp b/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
new file mode 100644
index 0000000000..aac063d10f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.cpp
@@ -0,0 +1,177 @@
+#include "yql_key_filter.h"
+
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/string/builder.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+void TSortMembersCollection::AddTableInfo(size_t tableIndex, const TString& tableName,
+ const TVector<TString>& sortMembers,
+ const TTypeAnnotationNode::TListType& sortedByTypes,
+ const TVector<bool>& sortDirections)
+{
+ THashMap<TString, TMemberDescrPtr>* nextMembers = &Members;
+ for (size_t i = 0; i < sortMembers.size(); ++i) {
+ if (!sortDirections.empty() && !sortDirections.at(i)) {
+ break;
+ }
+
+ TMemberDescrPtr member;
+ if (auto p = nextMembers->FindPtr(sortMembers[i])) {
+ member = *p;
+ }
+ else {
+ member = MakeIntrusive<TMemberDescr>();
+ YQL_ENSURE(IsDataOrOptionalOfData(sortedByTypes[i], member->IsColumnOptional, member->ColumnType),
+ "Table " << tableName.Quote() << ", field " << sortMembers[i].Quote()
+ << " has incompatible type " << *sortedByTypes[i]);
+ nextMembers->emplace(sortMembers[i], member);
+ }
+
+ member->Tables.insert(tableIndex);
+ nextMembers = &member->NextMembers;
+ }
+}
+
+bool TSortMembersCollection::ApplyRanges(const TKeyFilterPredicates& ranges, TExprContext& ctx) {
+ if (!ApplyRangesImpl(0, ranges, ctx)) {
+ return false;
+ }
+
+ DropMembersWithoutRanges(Members);
+
+ // Keep table indexes only in leafs
+ ApplyRecurs(Members, [&] (const TString& /*name*/, TMemberDescr& member) -> bool {
+ if (!member.NextMembers.empty()) {
+ for (auto& item: member.NextMembers) {
+ for (auto t: item.second->Tables) {
+ member.Tables.erase(t);
+ }
+ }
+ return true;
+ }
+ return false;
+ });
+
+ return true;
+}
+
+bool TSortMembersCollection::ApplyRanges(const TVector<TKeyFilterPredicates>& ranges, TExprContext& ctx) {
+ for (size_t i = 0; i < ranges.size(); ++i) {
+ if (!ApplyRangesImpl(i, ranges[i], ctx)) {
+ return false;
+ }
+ }
+
+ // Ensure that all OR clauses have at least first key
+ for (auto& item: Members) {
+ for (size_t i = 0; i < ranges.size(); ++i) {
+ if (!item.second->Ranges.contains(i)) {
+ item.second->Ranges.clear();
+ break;
+ }
+ }
+ }
+
+ DropMembersWithoutRanges(Members);
+
+ // Keep table indexes only in leafs
+ ApplyRecurs(Members, [&] (const TString& /*name*/, TMemberDescr& member) -> bool {
+ if (!member.NextMembers.empty()) {
+ for (auto& item: member.NextMembers) {
+ for (auto t: item.second->Tables) {
+ member.Tables.erase(t);
+ }
+ }
+ return true;
+ }
+ return false;
+ });
+
+ return true;
+}
+
+bool TSortMembersCollection::ApplyRangesImpl(size_t groupIndex, const TKeyFilterPredicates& ranges, TExprContext& ctx) {
+ bool hasErrors = false;
+ ApplyRecurs(Members, [&] (const TString& name, TMemberDescr& member) -> bool {
+ if (hasErrors) {
+ return false;
+ }
+
+ auto memberRanges = ranges.equal_range(name);
+ if (memberRanges.first == memberRanges.second) {
+ return false;
+ }
+
+ // ensure that we can compare right part of every range with current column
+ bool foundEqualComparisons = false;
+ for (auto it = memberRanges.first; it != memberRanges.second; ++it) {
+ TString cmpOp;
+ TExprNode::TPtr value;
+ std::tie(cmpOp, value) = it->second;
+ const TDataExprType* dataType = nullptr;
+
+ if (value) {
+ bool isOptional = false;
+ if (!EnsureDataOrOptionalOfData(*value, isOptional, dataType, ctx)) {
+ hasErrors = true;
+ return false;
+ }
+
+ if (!GetSuperType(dataType->GetSlot(), member.ColumnType->GetSlot())) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()),
+ TStringBuilder() << "Column " << name.Quote()
+ << " of " << *static_cast<const TTypeAnnotationNode*>(member.ColumnType)
+ << " type cannot be compared with " << *value->GetTypeAnn()));
+ hasErrors = true;
+ return false;
+ }
+ }
+ else {
+ // Not(Exists(member)) case
+ if (!member.IsColumnOptional) {
+ return false;
+ }
+ }
+
+ member.Ranges.emplace(groupIndex, std::make_tuple(cmpOp, value, dataType));
+ if (cmpOp == TCoCmpEqual::CallableName()) {
+ foundEqualComparisons = true;
+ }
+ }
+ // ensure that ranges contain at least one '==' operation to continue with next member of the compound key
+ return foundEqualComparisons;
+ });
+
+ return !hasErrors;
+}
+
+void TSortMembersCollection::DropMembersWithoutRanges(TMemberDescrMap& members) {
+ TVector<TString> toDrop;
+ for (auto& item: members) {
+ if (item.second->Ranges.empty()) {
+ toDrop.push_back(item.first);
+ }
+ else {
+ DropMembersWithoutRanges(item.second->NextMembers);
+ }
+ }
+ for (auto& name: toDrop) {
+ members.erase(name);
+ }
+}
+
+void TSortMembersCollection::ApplyRecurs(TMemberDescrMap& members, const std::function<bool(const TString&, TMemberDescr&)>& func) {
+ for (auto& item: members) {
+ if (func(item.first, *item.second)) {
+ ApplyRecurs(item.second->NextMembers, func);
+ }
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.h b/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.h
new file mode 100644
index 0000000000..f4ed2e21b3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/hash.h>
+#include <util/generic/hash_multi_map.h>
+#include <util/generic/ptr.h>
+#include <util/generic/set.h>
+#include <util/generic/map.h>
+#include <util/generic/strbuf.h>
+
+#include <tuple>
+#include <utility>
+#include <functional>
+
+namespace NYql {
+
+using TKeyFilterPredicates = THashMultiMap<TString, std::pair<TString, TExprNode::TPtr>>;
+
+class TSortMembersCollection {
+public:
+ struct TMemberDescr;
+ using TMemberDescrPtr = TIntrusivePtr<TMemberDescr>;
+ using TMemberDescrMap = THashMap<TString, TMemberDescrPtr>;
+
+ struct TMemberDescr: public TThrRefBase {
+ const TDataExprType* ColumnType = nullptr;
+ bool IsColumnOptional = false;
+ TMultiMap<size_t, std::tuple<TString, TExprNode::TPtr, const TDataExprType*>> Ranges;
+ TSet<size_t> Tables;
+ TMemberDescrMap NextMembers;
+ };
+
+ TSortMembersCollection() = default;
+ ~TSortMembersCollection() = default;
+
+ bool Empty() const {
+ return Members.empty();
+ }
+
+ void AddTableInfo(size_t tableIndex, const TString& tableName,
+ const TVector<TString>& sortMembers,
+ const TTypeAnnotationNode::TListType& sortedByTypes,
+ const TVector<bool>& sortDirections);
+
+ bool ApplyRanges(const TKeyFilterPredicates& ranges, TExprContext& ctx);
+ bool ApplyRanges(const TVector<TKeyFilterPredicates>& ranges, TExprContext& ctx);
+
+protected:
+ bool ApplyRangesImpl(size_t groupIndex, const TKeyFilterPredicates& ranges, TExprContext& ctx);
+ static void DropMembersWithoutRanges(TMemberDescrMap& members);
+ static void ApplyRecurs(TMemberDescrMap& members,
+ const std::function<bool(const TString&, TMemberDescr&)>& func);
+
+protected:
+ TMemberDescrMap Members;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp b/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
new file mode 100644
index 0000000000..3859fd029a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.cpp
@@ -0,0 +1,223 @@
+#include "lambda_builder.h"
+
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/minikql/mkql_opt_literal.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_serialization.h>
+#include <ydb/library/yql/minikql/comp_nodes/mkql_factories.h>
+
+#include <util/generic/strbuf.h>
+#include <util/system/env.h>
+
+namespace NYql {
+
+using namespace NCommon;
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+TLambdaBuilder::TLambdaBuilder(const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry,
+ NKikimr::NMiniKQL::TScopedAlloc& alloc,
+ const NKikimr::NMiniKQL::TTypeEnvironment* env,
+ const TIntrusivePtr<IRandomProvider>& randomProvider,
+ const TIntrusivePtr<ITimeProvider>& timeProvider,
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats,
+ NKikimr::NUdf::ICountersProvider* counters,
+ const NKikimr::NUdf::ISecureParamsProvider* secureParamsProvider)
+ : FunctionRegistry(functionRegistry)
+ , Alloc(alloc)
+ , RandomProvider(randomProvider)
+ , TimeProvider(timeProvider)
+ , JobStats(jobStats)
+ , Counters(counters)
+ , SecureParamsProvider(secureParamsProvider)
+ , Env(env)
+{
+}
+
+TLambdaBuilder::~TLambdaBuilder() {
+}
+
+void TLambdaBuilder::SetExternalEnv(const NKikimr::NMiniKQL::TTypeEnvironment* env) {
+ Env = env;
+}
+
+const NKikimr::NMiniKQL::TTypeEnvironment* TLambdaBuilder::CreateTypeEnv() const {
+ YQL_ENSURE(!EnvPtr);
+ EnvPtr = std::make_shared<NKikimr::NMiniKQL::TTypeEnvironment>(Alloc);
+ return EnvPtr.get();
+}
+
+TRuntimeNode TLambdaBuilder::BuildLambda(const IMkqlCallableCompiler& compiler, const TExprNode::TPtr& lambdaNode,
+ TExprContext& exprCtx, TArgumentsMap&& arguments) const
+{
+ TProgramBuilder pgmBuilder(GetTypeEnvironment(), *FunctionRegistry);
+ TMkqlBuildContext ctx(compiler, pgmBuilder, exprCtx, lambdaNode->UniqueId(), std::move(arguments));
+ return MkqlBuildExpr(*lambdaNode, ctx);
+}
+
+TRuntimeNode TLambdaBuilder::TransformAndOptimizeProgram(NKikimr::NMiniKQL::TRuntimeNode root,
+ TCallableVisitFuncProvider funcProvider) {
+ TExploringNodeVisitor explorer;
+ explorer.Walk(root.GetNode(), GetTypeEnvironment());
+ bool wereChanges = false;
+ TRuntimeNode program = SinglePassVisitCallables(root, explorer, funcProvider, GetTypeEnvironment(), true, wereChanges);
+ program = LiteralPropagationOptimization(program, GetTypeEnvironment(), true);
+ return program;
+}
+
+THolder<IComputationGraph> TLambdaBuilder::BuildGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NUdf::EValidateMode validateMode,
+ NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ TExploringNodeVisitor& explorer,
+ TRuntimeNode root) const
+{
+ return BuildGraph(factory, validateMode, validatePolicy, optLLVM, graphPerProcess, explorer, root, {root.GetNode()});
+}
+
+std::tuple<
+ THolder<NKikimr::NMiniKQL::IComputationGraph>,
+ TIntrusivePtr<IRandomProvider>,
+ TIntrusivePtr<ITimeProvider>
+>
+TLambdaBuilder::BuildLocalGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NUdf::EValidateMode validateMode,
+ NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ TExploringNodeVisitor& explorer,
+ TRuntimeNode root) const
+{
+ auto randomProvider = RandomProvider;
+ auto timeProvider = TimeProvider;
+ if (GetEnv(TString("YQL_DETERMINISTIC_MODE"))) {
+ randomProvider = CreateDeterministicRandomProvider(1);
+ timeProvider = CreateDeterministicTimeProvider(10000000);
+ }
+
+ return std::make_tuple(BuildGraph(factory, validateMode, validatePolicy, optLLVM, graphPerProcess, explorer, root, {root.GetNode()},
+ randomProvider, timeProvider), randomProvider, timeProvider);
+}
+
+class TComputationGraphProxy: public IComputationGraph {
+public:
+ TComputationGraphProxy(IComputationPattern::TPtr&& pattern, THolder<IComputationGraph>&& graph)
+ : Pattern(std::move(pattern))
+ , Graph(std::move(graph))
+ {}
+
+ void Prepare() final {
+ Graph->Prepare();
+ }
+ NUdf::TUnboxedValue GetValue() final {
+ return Graph->GetValue();
+ }
+ TComputationContext& GetContext() final {
+ return Graph->GetContext();
+ }
+ IComputationExternalNode* GetEntryPoint(size_t index, bool require) override {
+ return Graph->GetEntryPoint(index, require);
+ }
+ const TArrowKernelsTopology* GetKernelsTopology() override {
+ return Graph->GetKernelsTopology();
+ }
+ const TComputationNodePtrDeque& GetNodes() const final {
+ return Graph->GetNodes();
+ }
+ void Invalidate() final {
+ return Graph->Invalidate();
+ }
+ TMemoryUsageInfo& GetMemInfo() const final {
+ return Graph->GetMemInfo();
+ }
+ const THolderFactory& GetHolderFactory() const final {
+ return Graph->GetHolderFactory();
+ }
+ ITerminator* GetTerminator() const final {
+ return Graph->GetTerminator();
+ }
+ bool SetExecuteLLVM(bool value) final {
+ return Graph->SetExecuteLLVM(value);
+ }
+ TString SaveGraphState() final {
+ return Graph->SaveGraphState();
+ }
+ void LoadGraphState(TStringBuf state) final {
+ Graph->LoadGraphState(state);
+ }
+private:
+ IComputationPattern::TPtr Pattern;
+ THolder<IComputationGraph> Graph;
+};
+
+
+THolder<IComputationGraph> TLambdaBuilder::BuildGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NUdf::EValidateMode validateMode,
+ NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ TExploringNodeVisitor& explorer,
+ TRuntimeNode& root,
+ std::vector<NKikimr::NMiniKQL::TNode*>&& entryPoints,
+ TIntrusivePtr<IRandomProvider> randomProvider,
+ TIntrusivePtr<ITimeProvider> timeProvider) const
+{
+ if (!randomProvider) {
+ randomProvider = RandomProvider;
+ }
+
+ if (!timeProvider) {
+ timeProvider = TimeProvider;
+ }
+
+ TString serialized;
+
+ TComputationPatternOpts patternOpts(Alloc.Ref(), GetTypeEnvironment());
+ patternOpts.SetOptions(factory, FunctionRegistry, validateMode, validatePolicy, optLLVM, graphPerProcess, JobStats, Counters, SecureParamsProvider);
+ auto preparePatternFunc = [&]() {
+ if (serialized) {
+ auto tupleRunTimeNodes = DeserializeRuntimeNode(serialized, GetTypeEnvironment());
+ auto tupleNodes = static_cast<const TTupleLiteral*>(tupleRunTimeNodes.GetNode());
+ root = tupleNodes->GetValue(0);
+ for (size_t index = 0; index < entryPoints.size(); ++index) {
+ entryPoints[index] = tupleNodes->GetValue(1 + index).GetNode();
+ }
+ }
+ explorer.Walk(root.GetNode(), GetTypeEnvironment());
+ auto pattern = MakeComputationPattern(explorer, root, entryPoints, patternOpts);
+ for (const auto& node : explorer.GetNodes()) {
+ node->SetCookie(0);
+ }
+ return pattern;
+ };
+
+ auto pattern = preparePatternFunc();
+ YQL_ENSURE(pattern);
+
+ const TComputationOptsFull computeOpts(JobStats, Alloc.Ref(), *randomProvider, *timeProvider, validatePolicy, SecureParamsProvider);
+ auto graph = pattern->Clone(computeOpts);
+ return MakeHolder<TComputationGraphProxy>(std::move(pattern), std::move(graph));
+}
+
+TRuntimeNode TLambdaBuilder::MakeTuple(const TVector<TRuntimeNode>& items) const {
+ TProgramBuilder pgmBuilder(GetTypeEnvironment(), *FunctionRegistry);
+ return pgmBuilder.NewTuple(items);
+}
+
+TRuntimeNode TLambdaBuilder::UpdateLambdaCode(TString& code, size_t& nodes, TCallableVisitFuncProvider funcProvider) {
+ TRuntimeNode rootNode = DeserializeRuntimeNode(code, GetTypeEnvironment());
+ rootNode = TransformAndOptimizeProgram(rootNode, funcProvider);
+
+ TExploringNodeVisitor explorer;
+ explorer.Walk(rootNode.GetNode(), GetTypeEnvironment());
+ code = SerializeRuntimeNode(explorer, rootNode, GetTypeEnvironment());
+ nodes = explorer.GetNodes().size();
+
+ return rootNode;
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h b/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h
new file mode 100644
index 0000000000..dddab90742
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/lambda_builder.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/minikql/mkql_node_visitor.h>
+#include <ydb/library/yql/minikql/computation/mkql_computation_node.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_stats_registry.h>
+#include <ydb/library/yql/minikql/mkql_node.h>
+#include <ydb/library/yql/minikql/mkql_alloc.h>
+#include <ydb/library/yql/public/udf/udf_validate.h>
+
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/time_provider/time_provider.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+
+#include <tuple>
+
+namespace NYql {
+
+class TPatternCache;
+
+class TLambdaBuilder {
+public:
+ using TArgumentsMap = NCommon::TMkqlBuildContext::TArgumentsMap;
+
+ TLambdaBuilder(const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry,
+ NKikimr::NMiniKQL::TScopedAlloc& alloc,
+ const NKikimr::NMiniKQL::TTypeEnvironment* env = nullptr,
+ const TIntrusivePtr<IRandomProvider>& randomProvider = {},
+ const TIntrusivePtr<ITimeProvider>& timeProvider = {},
+ NKikimr::NMiniKQL::IStatsRegistry* jobStats = nullptr,
+ NKikimr::NUdf::ICountersProvider* counters = nullptr,
+ const NKikimr::NUdf::ISecureParamsProvider *secureParamsProvider = nullptr);
+
+ ~TLambdaBuilder();
+
+ const NKikimr::NMiniKQL::TTypeEnvironment& GetTypeEnvironment() const {
+ if (!Env) {
+ Env = CreateTypeEnv();
+ }
+ return *Env;
+ }
+
+ const NKikimr::NMiniKQL::IFunctionRegistry& GetFunctionRegistry() const {
+ return *FunctionRegistry;
+ }
+
+ NKikimr::NMiniKQL::TRuntimeNode BuildLambda(
+ const NCommon::IMkqlCallableCompiler& compiler,
+ const TExprNode::TPtr& lambdaNode,
+ TExprContext& exprCtx,
+ TArgumentsMap&& arguments = {}
+ ) const;
+
+ NKikimr::NMiniKQL::TRuntimeNode TransformAndOptimizeProgram(NKikimr::NMiniKQL::TRuntimeNode root,
+ NKikimr::NMiniKQL::TCallableVisitFuncProvider funcProvider);
+
+ THolder<NKikimr::NMiniKQL::IComputationGraph> BuildGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NKikimr::NUdf::EValidateMode validateMode,
+ NKikimr::NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ NKikimr::NMiniKQL::TExploringNodeVisitor& explorer,
+ NKikimr::NMiniKQL::TRuntimeNode root) const;
+ THolder<NKikimr::NMiniKQL::IComputationGraph> BuildGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NKikimr::NUdf::EValidateMode validateMode,
+ NKikimr::NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ NKikimr::NMiniKQL::TExploringNodeVisitor& explorer,
+ NKikimr::NMiniKQL::TRuntimeNode& root,
+ std::vector<NKikimr::NMiniKQL::TNode*>&& entryPoints,
+ TIntrusivePtr<IRandomProvider> randomProvider = {},
+ TIntrusivePtr<ITimeProvider> timeProvider = {}) const;
+ std::tuple<
+ THolder<NKikimr::NMiniKQL::IComputationGraph>,
+ TIntrusivePtr<IRandomProvider>,
+ TIntrusivePtr<ITimeProvider>
+ >
+ BuildLocalGraph(
+ const NKikimr::NMiniKQL::TComputationNodeFactory& factory,
+ NKikimr::NUdf::EValidateMode validateMode,
+ NKikimr::NUdf::EValidatePolicy validatePolicy,
+ const TString& optLLVM,
+ NKikimr::NMiniKQL::EGraphPerProcess graphPerProcess,
+ NKikimr::NMiniKQL::TExploringNodeVisitor& explorer,
+ NKikimr::NMiniKQL::TRuntimeNode root) const;
+
+ NKikimr::NMiniKQL::TRuntimeNode MakeTuple(const TVector<NKikimr::NMiniKQL::TRuntimeNode>& items) const;
+
+ NKikimr::NMiniKQL::TRuntimeNode UpdateLambdaCode(TString& code, size_t& nodes, NKikimr::NMiniKQL::TCallableVisitFuncProvider funcProvider);
+
+protected:
+ const NKikimr::NMiniKQL::IFunctionRegistry* FunctionRegistry;
+ NKikimr::NMiniKQL::TScopedAlloc& Alloc;
+ const TIntrusivePtr<IRandomProvider> RandomProvider;
+ const TIntrusivePtr<ITimeProvider> TimeProvider;
+ NKikimr::NMiniKQL::IStatsRegistry* const JobStats;
+ NKikimr::NUdf::ICountersProvider* const Counters;
+ const NKikimr::NUdf::ISecureParamsProvider* SecureParamsProvider;
+
+ /// TODO: remove?
+ void SetExternalEnv(const NKikimr::NMiniKQL::TTypeEnvironment* env);
+private:
+ const NKikimr::NMiniKQL::TTypeEnvironment* CreateTypeEnv() const;
+ mutable std::shared_ptr<NKikimr::NMiniKQL::TTypeEnvironment> InjectedEnvPtr;
+ mutable std::shared_ptr<NKikimr::NMiniKQL::TTypeEnvironment> EnvPtr;
+ mutable const NKikimr::NMiniKQL::TTypeEnvironment* Env;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/lambda_builder/ya.make b/ydb/library/yql/providers/yt/lib/lambda_builder/ya.make
new file mode 100644
index 0000000000..65d153de22
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/lambda_builder/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+SRCS(
+ lambda_builder.cpp
+)
+
+PEERDIR(
+ library/cpp/random_provider
+ library/cpp/time_provider
+ ydb/library/yql/ast
+ ydb/library/yql/minikql/computation/llvm
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/providers/common/mkql
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/log/ya.make b/ydb/library/yql/providers/yt/lib/log/ya.make
new file mode 100644
index 0000000000..21a1272ef8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+SRCS(
+ yt_logger.cpp
+ yt_logger.h
+)
+
+PEERDIR(
+ ydb/library/yql/providers/yt/lib/init_yt_api
+ yt/cpp/mapreduce/interface/logging
+ ydb/library/yql/utils/log
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp b/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
new file mode 100644
index 0000000000..2b33b29d66
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/yt_logger.cpp
@@ -0,0 +1,152 @@
+#include <ydb/library/yql/providers/yt/lib/init_yt_api/init.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/interface/logging/logger.h>
+
+#include <util/stream/printf.h>
+#include <util/stream/str.h>
+#include <util/stream/file.h>
+#include <util/system/spinlock.h>
+#include <util/system/guard.h>
+#include <util/system/file.h>
+#include <util/generic/mem_copy.h>
+
+
+namespace NYql {
+
+class TGlobalLoggerImpl: public NYT::ILogger
+{
+public:
+ TGlobalLoggerImpl(int cutLevel, size_t debugLogBufferSize, const TString& debugLogFile, bool debugLogAlwaysWrite)
+ : CutLevel_(cutLevel)
+ , BufferSize_(debugLogBufferSize)
+ , DebugLogFile_(debugLogFile)
+ , DebugLogAlwaysWrite_(debugLogAlwaysWrite)
+ {
+ if (BufferSize_ && DebugLogFile_) {
+ Buffer_ = TArrayHolder<char>(new char[BufferSize_]);
+ }
+ }
+
+ ~TGlobalLoggerImpl() {
+ FlushYtDebugLog();
+ }
+
+ void FlushYtDebugLog() {
+ THolder<char, TDeleteArray> buffer;
+ with_lock(BufferLock_) {
+ buffer.Swap(Buffer_);
+ }
+ if (buffer) {
+ try {
+ TUnbufferedFileOutput out(TFile(DebugLogFile_, OpenAlways | WrOnly | Seq | ForAppend | NoReuse));
+ if (BufferFull_ && BufferWritePos_ < BufferSize_) {
+ out.Write(buffer.Get() + BufferWritePos_, BufferSize_ - BufferWritePos_);
+ }
+ if (BufferWritePos_ > 0) {
+ out.Write(buffer.Get(), BufferWritePos_);
+ }
+ } catch (...) {
+ YQL_CLOG(ERROR, ProviderYt) << CurrentExceptionMessage();
+ }
+ }
+ }
+
+ void DropYtDebugLog() {
+ if (DebugLogAlwaysWrite_) {
+ FlushYtDebugLog();
+ return;
+ }
+ with_lock(BufferLock_) {
+ Buffer_.Destroy();
+ }
+ }
+
+ void Log(ELevel level, const TSourceLocation& sl, const char* format, va_list args) override {
+ NLog::ELevel yqlLevel = NLog::ELevel::TRACE;
+ switch (level) {
+ case FATAL:
+ yqlLevel = NLog::ELevel::FATAL;
+ break;
+ case ERROR:
+ yqlLevel = NLog::ELevel::ERROR;
+ break;
+ case INFO:
+ yqlLevel = NLog::ELevel::INFO;
+ break;
+ case DEBUG:
+ yqlLevel = NLog::ELevel::DEBUG;
+ break;
+ }
+ const bool needLog = int(level) <= CutLevel_ && NLog::YqlLogger().NeedToLog(NLog::EComponent::ProviderYt, yqlLevel);
+ with_lock(BufferLock_) {
+ if (!needLog && !Buffer_) {
+ return;
+ }
+ }
+
+ TStringStream stream;
+ NLog::YqlLogger().WriteLogPrefix(&stream, NLog::EComponent::ProviderYt, yqlLevel, sl.File, sl.Line);
+ NLog::OutputLogCtx(&stream, true);
+ Printf(stream, format, args);
+ stream << Endl;
+
+ if (needLog) {
+ NLog::YqlLogger().Write(NLog::ELevelHelpers::ToLogPriority(yqlLevel), stream.Str().data(), stream.Str().length());
+ }
+ with_lock(BufferLock_) {
+ if (Buffer_) {
+ const char* ptr = stream.Str().data();
+ size_t remaining = stream.Str().length();
+ while (remaining) {
+ const size_t write = Min(remaining, BufferSize_ - BufferWritePos_);
+ MemCopy(Buffer_.Get() + BufferWritePos_, ptr, write);
+ ptr += write;
+ BufferWritePos_ += write;
+ remaining -= write;
+ if (BufferWritePos_ >= BufferSize_) {
+ BufferWritePos_ = 0;
+ BufferFull_ = true;
+ }
+ }
+ }
+ }
+ }
+
+private:
+ int CutLevel_;
+ THolder<char, TDeleteArray> Buffer_;
+ size_t BufferSize_ = 0;
+ size_t BufferWritePos_ = 0;
+ bool BufferFull_ = false;
+ TSpinLock BufferLock_;
+ TString DebugLogFile_;
+ const bool DebugLogAlwaysWrite_;
+};
+
+void SetYtLoggerGlobalBackend(int level, size_t debugLogBufferSize, const TString& debugLogFile, bool debugLogAlwaysWrite) {
+ // Important to initialize YT API before setting logger. Otherwise YT API initialization will rest it to default
+ InitYtApiOnce();
+ if (level >= 0 || (debugLogBufferSize && debugLogFile)) {
+ NYT::SetLogger(new TGlobalLoggerImpl(level, debugLogBufferSize, debugLogFile, debugLogAlwaysWrite));
+ } else {
+ NYT::SetLogger(NYT::ILoggerPtr());
+ }
+}
+
+void FlushYtDebugLog() {
+ auto logger = NYT::GetLogger();
+ if (auto yqlLogger = dynamic_cast<TGlobalLoggerImpl*>(logger.Get())) {
+ yqlLogger->FlushYtDebugLog();
+ }
+}
+
+void DropYtDebugLog() {
+ auto logger = NYT::GetLogger();
+ if (auto yqlLogger = dynamic_cast<TGlobalLoggerImpl*>(logger.Get())) {
+ yqlLogger->DropYtDebugLog();
+ }
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/log/yt_logger.h b/ydb/library/yql/providers/yt/lib/log/yt_logger.h
new file mode 100644
index 0000000000..6f541d28f4
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/log/yt_logger.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+namespace NYql {
+
+// Use NYT::ILogger::ELevel for level
+void SetYtLoggerGlobalBackend(int level, size_t debugLogBufferSize = 0, const TString& debugLogFile = TString(), bool debugLogAlwaysWrite = false);
+void FlushYtDebugLog();
+void DropYtDebugLog();
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp b/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
new file mode 100644
index 0000000000..cfb9f4f88e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.cpp
@@ -0,0 +1,97 @@
+#include "mkql_helpers.h"
+
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/utility.h>
+#include <util/generic/ylimits.h>
+#include <util/generic/hash.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+namespace {
+
+bool UpdateRecordsRange(TRecordsRange& range, TStringBuf settingName, ui64 data)
+{
+ if (settingName == TStringBuf("take")) {
+ range.Limit = Min(data, range.Limit.GetOrElse(Max<ui64>()));
+ } else if (settingName == TStringBuf("skip")) {
+ if (range.Limit.Defined()) {
+ if (data >= range.Limit.GetRef()) {
+ range.Limit = 0;
+ range.Offset = 0;
+ return false;
+ }
+
+ range.Offset = data;
+ range.Limit = range.Limit.GetRef() - data;
+ } else {
+ ui64 prevOffset = range.Offset.GetOrElse(0);
+ if (data > Max<ui64>() - prevOffset) {
+ range.Limit = 0;
+ range.Offset = 0;
+ return false;
+ }
+
+ range.Offset = data + prevOffset;
+ }
+ }
+ return true;
+}
+
+const THashMap<TStringBuf, NUdf::TDataTypeId> SYS_COLUMN_TYPE_IDS = {
+ {"path", NUdf::TDataType<char*>::Id},
+ {"record", NUdf::TDataType<ui64>::Id},
+ {"index", NUdf::TDataType<ui32>::Id},
+ {"num", NUdf::TDataType<ui64>::Id},
+ {"keyswitch", NUdf::TDataType<bool>::Id},
+};
+
+}
+
+void TRecordsRange::Fill(const TExprNode& settingsNode) {
+ Offset.Clear();
+ Limit.Clear();
+
+ for (auto& setting: settingsNode.Children()) {
+ if (setting->ChildrenSize() == 0) {
+ continue;
+ }
+
+ auto settingName = setting->Child(0)->Content();
+ if (settingName != TStringBuf("take") && settingName != TStringBuf("skip")) {
+ continue;
+ }
+ YQL_ENSURE(setting->Child(1)->IsCallable("Uint64"));
+ if (!UpdateRecordsRange(*this, settingName, NYql::FromString<ui64>(*setting->Child(1)->Child(0), NUdf::EDataSlot::Uint64))) {
+ break;
+ }
+ }
+}
+
+TType* GetRecordType(TType* type) {
+ if (type->GetKind() == TType::EKind::List) {
+ return AS_TYPE(TListType, type)->GetItemType();
+ } else if (type->GetKind() == TType::EKind::Optional) {
+ return AS_TYPE(TOptionalType, type)->GetItemType();
+ } else if (type->GetKind() == TType::EKind::Stream) {
+ return AS_TYPE(TStreamType, type)->GetItemType();
+ }
+
+ return type;
+}
+
+NUdf::TDataTypeId GetSysColumnTypeId(TStringBuf column) {
+ auto p = SYS_COLUMN_TYPE_IDS.FindPtr(column);
+ YQL_ENSURE(p, "Unsupported system column:" << column);
+ return *p;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h b/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h
new file mode 100644
index 0000000000..09bd57e4ba
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/strbuf.h>
+
+namespace NYql {
+
+NKikimr::NMiniKQL::TType* GetRecordType(NKikimr::NMiniKQL::TType* type);
+
+class TExprNode;
+
+struct TRecordsRange {
+ TMaybe<ui64> Offset;
+ TMaybe<ui64> Limit;
+
+ explicit operator bool() const {
+ return Offset.Defined() || Limit.Defined();
+ }
+
+ void Fill(const TExprNode& settingsNode);
+};
+
+NUdf::TDataTypeId GetSysColumnTypeId(TStringBuf column);
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/mkql_helpers/ya.make b/ydb/library/yql/providers/yt/lib/mkql_helpers/ya.make
new file mode 100644
index 0000000000..8640ac9aa9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/mkql_helpers/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ mkql_helpers.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/minikql
+ ydb/library/yql/core
+ ydb/library/yql/ast
+ ydb/library/yql/utils
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp b/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
new file mode 100644
index 0000000000..bab2518be2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.cpp
@@ -0,0 +1,74 @@
+#include "res_or_pull.h"
+
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+
+#include <util/stream/holder.h>
+#include <util/stream/str.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+
+TExecuteResOrPull::TExecuteResOrPull(TMaybe<ui64> rowLimit, TMaybe<ui64> byteLimit, const TMaybe<TVector<TString>>& columns)
+ : Rows(rowLimit)
+ , Bytes(byteLimit)
+ , Columns(columns)
+ , Out(new THoldingStream<TCountingOutput>(THolder(new TStringOutput(Result))))
+ , Writer(new NYson::TYsonWriter(Out.Get(), NYson::EYsonFormat::Binary, ::NYson::EYsonType::Node, true))
+ , IsList(false)
+ , Truncated(false)
+ , Row(0)
+{
+}
+
+ui64 TExecuteResOrPull::GetWrittenSize() const {
+ YQL_ENSURE(Out, "GetWritten() must be callled before Finish()");
+ return Out->Counter();
+}
+
+TString TExecuteResOrPull::Finish() {
+ if (IsList) {
+ Writer->OnEndList();
+ }
+ Writer.Destroy();
+ Out.Destroy();
+ return Result;
+}
+
+bool TExecuteResOrPull::WriteNext(TStringBuf val) {
+ if (IsList) {
+ if (!HasCapacity()) {
+ Truncated = true;
+ return false;
+ }
+ Writer->OnListItem();
+ }
+ Writer->OnRaw(val, ::NYson::EYsonType::Node);
+ ++Row;
+ return IsList;
+}
+
+void TExecuteResOrPull::WriteValue(const NUdf::TUnboxedValue& value, TType* type) {
+ if (type->IsList()) {
+ auto inputType = AS_TYPE(TListType, type)->GetItemType();
+ TMaybe<TVector<ui32>> structPositions = NCommon::CreateStructPositions(inputType, Columns.Defined() ? Columns.Get() : nullptr);
+ SetListResult();
+ const auto it = value.GetListIterator();
+ for (NUdf::TUnboxedValue item; it.Next(item); ++Row) {
+ if (!HasCapacity()) {
+ Truncated = true;
+ break;
+ }
+ Writer->OnListItem();
+ NCommon::WriteYsonValue(*Writer, item, inputType, structPositions.Get());
+ }
+ } else {
+ NCommon::WriteYsonValue(*Writer, value, type, nullptr);
+ }
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h b/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h
new file mode 100644
index 0000000000..d5d7fbcd7b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/res_or_pull.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/codec/yt_codec_io.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+
+#include <ydb/library/yql/minikql/mkql_node.h>
+
+#include <library/cpp/yson/writer.h>
+
+#include <util/stream/length.h>
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/ptr.h>
+#include <util/generic/noncopyable.h>
+
+namespace NYql {
+
+class TExecuteResOrPull : public TNonCopyable {
+public:
+ TExecuteResOrPull(TMaybe<ui64> rowLimit, TMaybe<ui64> byteLimit, const TMaybe<TVector<TString>>& columns);
+
+ bool HasCapacity() const {
+ return (!Rows || Row < *Rows) && (!Bytes || Out->Counter() < *Bytes);
+ }
+
+ bool IsTruncated() const {
+ return Truncated;
+ }
+
+ ui64 GetWrittenSize() const;
+
+ ui64 GetWrittenRows() const {
+ return Row;
+ }
+
+ TString Finish();
+ TMaybe<ui64> GetRowsLimit() const {
+ return Rows;
+ }
+
+ void SetListResult() {
+ if (!IsList) {
+ IsList = true;
+ Writer->OnBeginList();
+ }
+ }
+
+ const TMaybe<TVector<TString>>& GetColumns() const {
+ return Columns;
+ }
+
+ bool WriteNext(TStringBuf val);
+
+ template <class TRec>
+ bool WriteNext(TMkqlIOCache& specCache, const TRec& rec, ui32 tableIndex) {
+ if (!HasCapacity()) {
+ Truncated = true;
+ return false;
+ }
+ NYql::DecodeToYson(specCache, tableIndex, rec, *Out);
+ ++Row;
+ return true;
+ }
+
+ void WriteValue(const NKikimr::NUdf::TUnboxedValue& value, NKikimr::NMiniKQL::TType* type);
+
+protected:
+ const TMaybe<ui64> Rows;
+ const TMaybe<ui64> Bytes;
+ const TMaybe<TVector<TString>> Columns;
+ TString Result;
+ THolder<TCountingOutput> Out;
+ THolder<NYson::TYsonWriter> Writer;
+ bool IsList;
+ bool Truncated;
+ ui64 Row;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp b/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
new file mode 100644
index 0000000000..a1b06cc284
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.cpp
@@ -0,0 +1,60 @@
+#include "table_limiter.h"
+
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+
+#include <util/generic/ylimits.h>
+
+namespace NYql {
+
+TTableLimiter::TTableLimiter(const TRecordsRange& range)
+ : Start(range.Offset.GetOrElse(0ULL))
+ , End(range.Limit.Defined() ? Start + *range.Limit : Max())
+ , Current(0ULL)
+ , TableStart(0ULL)
+ , TableEnd(Max())
+{
+}
+
+bool TTableLimiter::NextTable(ui64 recordCount) {
+ TableStart = 0ULL;
+ TableEnd = Max();
+ if (!recordCount) { // Skip empty tables
+ return false;
+ }
+ if (Start && Current + recordCount <= Start) {
+ Current += recordCount;
+ return false;
+ }
+ if (Start && Current < Start && Current + recordCount > Start) {
+ TableStart = Start - Current;
+ }
+
+ if (Current < End && Current + recordCount > End) {
+ TableEnd = End - Current;
+ }
+
+ Current += recordCount;
+ return true;
+}
+
+void TTableLimiter::NextDynamicTable() {
+ TableStart = 0ULL;
+ TableEnd = Max();
+ if (Start && Current < Start) {
+ TableStart = Start - Current;
+ }
+
+ if (Current < End) {
+ TableEnd = End - Current;
+ }
+}
+
+void TTableLimiter::Skip(ui64 recordCount) {
+ Current += recordCount;
+}
+
+ui64 TTableLimiter::GetTableZEnd() const {
+ return Max<ui64>() == TableEnd ? 0ULL : TableEnd;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h b/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h
new file mode 100644
index 0000000000..e5aaea7bda
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <util/system/defaults.h>
+
+namespace NYql {
+
+struct TRecordsRange;
+
+class TTableLimiter {
+public:
+ TTableLimiter(const TRecordsRange& range);
+
+ bool NextTable(ui64 recordCount);
+ void NextDynamicTable();
+ inline ui64 GetTableStart() const {
+ return TableStart;
+ }
+ inline ui64 GetTableEnd() const {
+ return TableEnd;
+ }
+ ui64 GetTableZEnd() const;
+ inline bool Exceed() const {
+ return Current >= End;
+ }
+ inline explicit operator bool() const {
+ return !Exceed();
+ }
+ void Skip(ui64 recordCount);
+private:
+ ui64 Start;
+ ui64 End;
+ ui64 Current;
+ ui64 TableStart;
+ ui64 TableEnd;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/res_pull/ya.make b/ydb/library/yql/providers/yt/lib/res_pull/ya.make
new file mode 100644
index 0000000000..da667e98ba
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/res_pull/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+SRCS(
+ res_or_pull.cpp
+ table_limiter.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ ydb/library/yql/minikql
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/yt/codec
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/ya.make b/ydb/library/yql/providers/yt/lib/row_spec/ya.make
new file mode 100644
index 0000000000..33b0a15a13
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/ya.make
@@ -0,0 +1,24 @@
+LIBRARY()
+
+SRCS(
+ yql_row_spec.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ ydb/library/yql/ast
+ ydb/library/yql/core/expr_nodes_gen
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core/issue
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/schema
+ ydb/library/yql/providers/common/schema/expr
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/expr_nodes
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp b/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
new file mode 100644
index 0000000000..0d7af3f98d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.cpp
@@ -0,0 +1,1616 @@
+#include "yql_row_spec.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/schema/schema.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/providers/common/schema/yql_schema_utils.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/issue/yql_issue.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <util/generic/cast.h>
+#include <util/generic/xrange.h>
+#include <util/string/builder.h>
+
+#include <algorithm>
+
+namespace NYql {
+
+namespace {
+
+ui64 GetNativeYtTypeFlagsImpl(const TTypeAnnotationNode* itemType) {
+ switch (itemType->GetKind()) {
+ case ETypeAnnotationKind::Pg: {
+ auto name = itemType->Cast<TPgExprType>()->GetName();
+ if (name == "float4") {
+ return NTCF_FLOAT | NTCF_NO_YT_SUPPORT;
+ }
+
+ return NTCF_NO_YT_SUPPORT;
+ }
+ case ETypeAnnotationKind::Data:
+ switch (itemType->Cast<TDataExprType>()->GetSlot()) {
+ case EDataSlot::Date:
+ case EDataSlot::Datetime:
+ case EDataSlot::Timestamp:
+ case EDataSlot::Interval:
+ return NTCF_DATE;
+ case EDataSlot::Json:
+ return NTCF_JSON;
+ case EDataSlot::Float:
+ return NTCF_FLOAT;
+ case EDataSlot::Decimal:
+ return NTCF_DECIMAL;
+ case EDataSlot::Uuid:
+ case EDataSlot::TzDate:
+ case EDataSlot::TzDatetime:
+ case EDataSlot::TzTimestamp:
+ case EDataSlot::DyNumber:
+ case EDataSlot::JsonDocument:
+ return NTCF_NO_YT_SUPPORT;
+ default:
+ return NTCF_NONE;
+ }
+ case ETypeAnnotationKind::Null:
+ return NTCF_NULL;
+ case ETypeAnnotationKind::Void:
+ return NTCF_VOID;
+ case ETypeAnnotationKind::Optional:
+ return NTCF_COMPLEX | GetNativeYtTypeFlagsImpl(itemType->Cast<TOptionalExprType>()->GetItemType());
+ case ETypeAnnotationKind::List:
+ return NTCF_COMPLEX | GetNativeYtTypeFlagsImpl(itemType->Cast<TListExprType>()->GetItemType());
+ case ETypeAnnotationKind::Dict: {
+ auto dictType = itemType->Cast<TDictExprType>();
+ return NTCF_COMPLEX | GetNativeYtTypeFlagsImpl(dictType->GetKeyType()) | GetNativeYtTypeFlagsImpl(dictType->GetPayloadType());
+ }
+ case ETypeAnnotationKind::Variant:
+ return NTCF_COMPLEX | GetNativeYtTypeFlagsImpl(itemType->Cast<TVariantExprType>()->GetUnderlyingType());
+ case ETypeAnnotationKind::Struct: {
+ ui64 flags = NTCF_COMPLEX;
+ for (auto item: itemType->Cast<TStructExprType>()->GetItems()) {
+ flags |= GetNativeYtTypeFlagsImpl(item->GetItemType());
+ }
+ return flags;
+ }
+ case ETypeAnnotationKind::Tuple: {
+ ui64 flags = NTCF_COMPLEX;
+ for (auto item: itemType->Cast<TTupleExprType>()->GetItems()) {
+ flags |= GetNativeYtTypeFlagsImpl(item);
+ }
+ return flags;
+ }
+ case ETypeAnnotationKind::Tagged:
+ return NTCF_COMPLEX | GetNativeYtTypeFlagsImpl(itemType->Cast<TTaggedExprType>()->GetBaseType());
+ case ETypeAnnotationKind::EmptyDict:
+ case ETypeAnnotationKind::EmptyList:
+ return NTCF_COMPLEX;
+ case ETypeAnnotationKind::World:
+ case ETypeAnnotationKind::Unit:
+ case ETypeAnnotationKind::Item:
+ case ETypeAnnotationKind::Callable:
+ case ETypeAnnotationKind::Generic:
+ case ETypeAnnotationKind::Error:
+ case ETypeAnnotationKind::Resource:
+ case ETypeAnnotationKind::Stream:
+ case ETypeAnnotationKind::Flow:
+ case ETypeAnnotationKind::Multi:
+ case ETypeAnnotationKind::Type:
+ case ETypeAnnotationKind::Block:
+ case ETypeAnnotationKind::Scalar:
+ case ETypeAnnotationKind::LastType:
+ break;
+ }
+ return NTCF_NONE;
+}
+
+}
+
+ui64 GetNativeYtTypeFlags(const TStructExprType& type, const NCommon::TStructMemberMapper& mapper) {
+ ui64 flags = 0;
+ for (auto item: type.GetItems()) {
+ if (!mapper || mapper(item->GetName())) {
+ const TTypeAnnotationNode* itemType = item->GetItemType();
+ bool wasOptional = false;
+ if (itemType->GetKind() == ETypeAnnotationKind::Optional) {
+ wasOptional = true;
+ itemType = itemType->Cast<TOptionalExprType>()->GetItemType();
+ }
+
+ if (wasOptional && itemType->GetKind() == ETypeAnnotationKind::Pg) {
+ flags |= NTCF_COMPLEX;
+ }
+
+ flags |= GetNativeYtTypeFlagsImpl(itemType);
+ }
+ }
+ flags &= ~NTCF_NO_YT_SUPPORT;
+ return flags;
+}
+
+using namespace NNodes;
+
+bool TYqlRowSpecInfo::Parse(const TString& rowSpecYson, TExprContext& ctx, const TPositionHandle& pos) {
+ try {
+ return Parse(NYT::NodeFromYsonString(rowSpecYson), ctx, pos);
+ } catch (const std::exception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), TStringBuilder() << "Failed to parse row spec: " << e.what()));
+ return false;
+ }
+}
+
+bool TYqlRowSpecInfo::Parse(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos) {
+ *this = {};
+ try {
+ if (!ParseType(rowSpecAttr, ctx, pos) || !ParseSort(rowSpecAttr, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(rowSpecAttr);
+ ParseDefValues(rowSpecAttr);
+ ParseConstraints(rowSpecAttr);
+ ParseConstraintsNode(ctx);
+ } catch (const std::exception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), TStringBuilder() << "Failed to parse row spec: " << e.what()));
+ return false;
+ }
+
+ return Validate(ctx, pos);
+}
+
+bool TYqlRowSpecInfo::ParsePatched(const NYT::TNode& rowSpecAttr, const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos) {
+ auto schemaAttr = attrs.FindPtr(SCHEMA_ATTR_NAME);
+ if (!schemaAttr) {
+ YQL_LOG_CTX_THROW yexception() << YqlRowSpecAttribute << " with " << RowSpecAttrTypePatch << " attribute requires YT schema to be present";
+ }
+ auto schema = NYT::NodeFromYsonString(*schemaAttr);
+ auto strict = schema.GetAttributes()["strict"];
+ if (!strict.IsUndefined() && !NYT::GetBool(strict)) {
+ YQL_LOG_CTX_THROW yexception() << YqlRowSpecAttribute << " with " << RowSpecAttrTypePatch << " attribute can only be used with 'strict' schema";
+ }
+ auto mode = schema.GetAttributes()[SCHEMA_MODE_ATTR_NAME];
+ if (!mode.IsUndefined() && mode.AsString() == "weak") {
+ YQL_LOG_CTX_THROW yexception() << YqlRowSpecAttribute << " with " << RowSpecAttrTypePatch << " attribute can only be used with 'strong' schema";
+ }
+
+ auto schemaAsRowSpec = YTSchemaToRowSpec(schema);
+ if (!ParseType(schemaAsRowSpec, ctx, pos) || !ParseSort(schemaAsRowSpec, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(schemaAsRowSpec);
+
+ auto typePatch = NCommon::ParseTypeFromYson(rowSpecAttr[RowSpecAttrTypePatch], ctx, ctx.GetPosition(pos));
+ if (!typePatch) {
+ return false;
+ }
+
+ if (typePatch->GetKind() != ETypeAnnotationKind::Struct) {
+ YQL_LOG_CTX_THROW yexception() << "Row spec TypePatch has a non struct type: " << *typePatch;
+ }
+
+ if (!ParseSort(rowSpecAttr, ctx, pos)) {
+ return false;
+ }
+
+ TSet<TString> auxFields;
+ for (size_t i = 0; i < SortedBy.size(); ++i) {
+ if ((i >= SortMembers.size() || SortedBy[i] != SortMembers[i]) && IsSystemMember(SortedBy[i])) {
+ auxFields.insert(SortedBy[i]);
+ }
+ }
+ auto typePatchStruct = typePatch->Cast<TStructExprType>();
+ if (typePatchStruct->GetSize() || !auxFields.empty()) {
+ // Patch Type
+ auto updatedItems = Type->GetItems();
+ for (auto& patchItem: typePatchStruct->GetItems()) {
+ auto name = patchItem->GetName();
+ YQL_ENSURE(!auxFields.contains(name));
+ auto itemPos = Type->FindItem(name);
+ if (!itemPos) {
+ throw yexception() << "Row spec TypePatch refers to unknown field: " << name;
+ }
+ updatedItems[*itemPos] = patchItem;
+ }
+ for (auto it = auxFields.rbegin(); it != auxFields.rend(); ++it) {
+ auto itemPos = Type->FindItem(*it);
+ if (!itemPos) {
+ throw yexception() << "Row spec SortedBy refers to unknown field: " << *it;
+ }
+ YQL_ENSURE(*itemPos < updatedItems.size(), "Something wrong!");
+ updatedItems.erase(updatedItems.begin() + *itemPos);
+ }
+ Type = ctx.MakeType<TStructExprType>(updatedItems);
+
+ // Patch TypeNode
+ THashMap<TString, const NYT::TNode*> patchNodes;
+ for (auto& item: rowSpecAttr[RowSpecAttrTypePatch][1].AsList()) {
+ patchNodes.emplace(item[0].AsString(), &item);
+ }
+ if (auxFields.empty()) {
+ for (auto& item: TypeNode[1].AsList()) {
+ if (auto p = patchNodes.FindPtr(item[0].AsString())) {
+ item = **p;
+ }
+ }
+ } else {
+ auto& membersList = TypeNode[1].AsList();
+ NYT::TNode::TListType newMembers;
+ newMembers.reserve(membersList.size());
+ for (auto& item: membersList) {
+ if (!auxFields.contains(item[0].AsString())) {
+ if (auto p = patchNodes.FindPtr(item[0].AsString())) {
+ newMembers.push_back(**p);
+ } else {
+ newMembers.push_back(item);
+ }
+ }
+ }
+ membersList = std::move(newMembers);
+
+ // Patch Columns
+ TColumnOrder newColumns;
+ for (auto& col: *Columns) {
+ if (!auxFields.contains(col)) {
+ newColumns.push_back(col);
+ }
+ }
+ Columns = std::move(newColumns);
+ }
+ YQL_ENSURE(Type->GetSize() == TypeNode[1].AsList().size());
+ }
+
+ TYTSortInfo sortInfo = KeyColumnsFromSchema(schema);
+ if (!ValidateSort(sortInfo, ctx, pos)) {
+ return false;
+ }
+
+ ParseFlags(rowSpecAttr);
+ ParseDefValues(rowSpecAttr);
+ ParseConstraints(rowSpecAttr);
+ ParseConstraintsNode(ctx);
+ return true;
+}
+
+bool TYqlRowSpecInfo::ParseFull(const NYT::TNode& rowSpecAttr, const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos) {
+ if (!ParseType(rowSpecAttr, ctx, pos) || !ParseSort(rowSpecAttr, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(rowSpecAttr);
+ ParseDefValues(rowSpecAttr);
+ ParseConstraints(rowSpecAttr);
+ ParseConstraintsNode(ctx);
+
+ if (auto schemaAttr = attrs.FindPtr(SCHEMA_ATTR_NAME)) {
+ auto schema = NYT::NodeFromYsonString(*schemaAttr);
+ auto modeAttr = schema.GetAttributes()[SCHEMA_MODE_ATTR_NAME];
+ const bool weak = !modeAttr.IsUndefined() && modeAttr.AsString() == "weak";
+ // Validate type for non weak schema only
+ if (!weak) {
+ auto schemaAsRowSpec = YTSchemaToRowSpec(schema);
+ auto type = NCommon::ParseTypeFromYson(schemaAsRowSpec[RowSpecAttrType], ctx, ctx.GetPosition(pos));
+ if (!type) {
+ return false;
+ }
+ if (type->GetKind() != ETypeAnnotationKind::Struct) {
+ YQL_LOG_CTX_THROW yexception() << "YT schema type has a non struct type: " << *type;
+ }
+ THashSet<TStringBuf> auxFields;
+ for (auto& col: SortedBy) {
+ auxFields.insert(col);
+ }
+ TStringBuilder hiddenFields;
+ for (auto item: type->Cast<TStructExprType>()->GetItems()) {
+ if (!Type->FindItem(item->GetName()) && !auxFields.contains(item->GetName())) {
+ if (hiddenFields.size() > 100) {
+ hiddenFields << ", ...";
+ break;
+ }
+ if (!hiddenFields.empty()) {
+ hiddenFields << ", ";
+ }
+ hiddenFields << item->GetName();
+ }
+ }
+ if (!hiddenFields.empty()) {
+ hiddenFields.prepend("Table attribute '_yql_row_spec' hides fields: ");
+ if (!ctx.AddWarning(YqlIssue(ctx.GetPosition(pos), EYqlIssueCode::TIssuesIds_EIssueCode_YT_ROWSPEC_HIDES_FIELDS, hiddenFields))) {
+ return false;
+ }
+ }
+ }
+ TYTSortInfo sortInfo = KeyColumnsFromSchema(schema);
+ if (!ValidateSort(sortInfo, ctx, pos)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Priority:
+// 1. YqlRowSpec(TypePatch) + Schema
+// 2. YqlRowSpec(Type)
+// 3. _infer_schema + schema(SortBy)
+// 4. _read_schema + schema(SortBy)
+// 5. schema
+bool TYqlRowSpecInfo::Parse(const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos) {
+ *this = {};
+ try {
+ if (auto rowSpecAttr = attrs.FindPtr(YqlRowSpecAttribute)) {
+ auto rowSpec = NYT::NodeFromYsonString(*rowSpecAttr);
+ if (rowSpec.HasKey(RowSpecAttrTypePatch)) {
+ if (!ParsePatched(rowSpec, attrs, ctx, pos)) {
+ return false;
+ }
+ } else {
+ if (!ParseFull(rowSpec, attrs, ctx, pos)) {
+ return false;
+ }
+ }
+ } else if (auto inferSchemaAttr = attrs.FindPtr(INFER_SCHEMA_ATTR_NAME)) {
+ auto inferSchema = NYT::NodeFromYsonString(*inferSchemaAttr);
+
+ TYTSortInfo sortInfo;
+ auto schemaAttr = attrs.FindPtr(SCHEMA_ATTR_NAME);
+ if (schemaAttr) {
+ auto schema = NYT::NodeFromYsonString(*schemaAttr);
+ sortInfo = KeyColumnsFromSchema(schema);
+ MergeInferredSchemeWithSort(inferSchema, sortInfo);
+ }
+ auto schemaAsRowSpec = YTSchemaToRowSpec(inferSchema, schemaAttr ? &sortInfo : nullptr);
+ if (!ParseType(schemaAsRowSpec, ctx, pos) || !ParseSort(schemaAsRowSpec, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(schemaAsRowSpec);
+ } else if (auto readSchema = attrs.FindPtr(READ_SCHEMA_ATTR_NAME)) {
+ TYTSortInfo sortInfo;
+ if (auto schemaAttr = attrs.FindPtr(SCHEMA_ATTR_NAME)) {
+ sortInfo = KeyColumnsFromSchema(NYT::NodeFromYsonString(*schemaAttr));
+ }
+ auto schemaAsRowSpec = YTSchemaToRowSpec(NYT::NodeFromYsonString(*readSchema), &sortInfo);
+ if (!ParseType(schemaAsRowSpec, ctx, pos) || !ParseSort(schemaAsRowSpec, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(schemaAsRowSpec);
+ } else if (auto schema = attrs.FindPtr(SCHEMA_ATTR_NAME)) {
+ auto schemaAsRowSpec = YTSchemaToRowSpec(NYT::NodeFromYsonString(*schema));
+ if (!ParseType(schemaAsRowSpec, ctx, pos) || !ParseSort(schemaAsRowSpec, ctx, pos)) {
+ return false;
+ }
+ ParseFlags(schemaAsRowSpec);
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Table has no supported schema attributes";
+ }
+ } catch (const std::exception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), TStringBuilder() << "Failed to parse row spec: " << e.what()));
+ return false;
+ }
+
+ return Validate(ctx, pos);
+}
+
+bool TYqlRowSpecInfo::ParseType(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos) {
+ if (!rowSpecAttr.HasKey(RowSpecAttrType)) {
+ YQL_LOG_CTX_THROW yexception() << "Row spec doesn't have mandatory Type attribute";
+ }
+ TVector<TString> columns;
+ auto type = NCommon::ParseOrderAwareTypeFromYson(rowSpecAttr[RowSpecAttrType], columns, ctx, ctx.GetPosition(pos));
+ if (!type) {
+ return false;
+ }
+ if (type->GetKind() != ETypeAnnotationKind::Struct) {
+ YQL_LOG_CTX_THROW yexception() << "Row spec defines not a struct type";
+ }
+
+ Type = type->Cast<TStructExprType>();
+ TypeNode = rowSpecAttr[RowSpecAttrType];
+ Columns = std::move(columns);
+
+ if (rowSpecAttr.HasKey(RowSpecAttrStrictSchema)) {
+ // Backward compatible parse. Old code saves 'StrictSchema' as Int64
+ StrictSchema = rowSpecAttr[RowSpecAttrStrictSchema].IsInt64()
+ ? rowSpecAttr[RowSpecAttrStrictSchema].AsInt64() != 0
+ : NYT::GetBool(rowSpecAttr[RowSpecAttrStrictSchema]);
+ if (!StrictSchema) {
+ auto items = Type->GetItems();
+ auto dictType = ctx.MakeType<TDictExprType>(
+ ctx.MakeType<TDataExprType>(EDataSlot::String),
+ ctx.MakeType<TDataExprType>(EDataSlot::String));
+ items.push_back(ctx.MakeType<TItemExprType>(YqlOthersColumnName, dictType));
+ Type = ctx.MakeType<TStructExprType>(items);
+ Columns->push_back(TString(YqlOthersColumnName));
+ }
+ }
+
+ return true;
+}
+
+bool TYqlRowSpecInfo::ParseSort(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos) {
+ if (rowSpecAttr.HasKey(RowSpecAttrSortMembers) || rowSpecAttr.HasKey(RowSpecAttrSortedBy) || rowSpecAttr.HasKey(RowSpecAttrSortDirections)) {
+ ClearSortness();
+ }
+ if (rowSpecAttr.HasKey(RowSpecAttrSortDirections)) {
+ for (auto& item: rowSpecAttr[RowSpecAttrSortDirections].AsList()) {
+ SortDirections.push_back(item.AsInt64() != 0);
+ }
+ }
+
+ auto loadColumnList = [&] (TStringBuf name, TVector<TString>& columns) {
+ if (rowSpecAttr.HasKey(name)) {
+ auto& list = rowSpecAttr[name].AsList();
+ for (const auto& item : list) {
+ columns.push_back(item.AsString());
+ }
+ }
+ };
+
+ loadColumnList(RowSpecAttrSortMembers, SortMembers);
+ loadColumnList(RowSpecAttrSortedBy, SortedBy);
+
+ if (rowSpecAttr.HasKey(RowSpecAttrSortedByTypes)) {
+ auto& list = rowSpecAttr[RowSpecAttrSortedByTypes].AsList();
+ for (auto& type : list) {
+ if (auto sortType = NCommon::ParseTypeFromYson(type, ctx, ctx.GetPosition(pos))) {
+ SortedByTypes.push_back(sortType);
+ } else {
+ return false;
+ }
+ }
+ }
+
+ if (rowSpecAttr.HasKey(RowSpecAttrUniqueKeys)) {
+ UniqueKeys = NYT::GetBool(rowSpecAttr[RowSpecAttrUniqueKeys]);
+ }
+ return true;
+}
+
+void TYqlRowSpecInfo::ParseFlags(const NYT::TNode& rowSpecAttr) {
+ if (rowSpecAttr.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ NativeYtTypeFlags = rowSpecAttr[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpecAttr.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ NativeYtTypeFlags = NYT::GetBool(rowSpecAttr[RowSpecAttrUseNativeYtTypes]) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpecAttr.HasKey(RowSpecAttrUseTypeV2)) {
+ NativeYtTypeFlags = NYT::GetBool(rowSpecAttr[RowSpecAttrUseTypeV2]) ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+ if (NativeYtTypeFlags) {
+ NativeYtTypeFlags &= NYql::GetNativeYtTypeFlags(*Type);
+ }
+}
+
+void TYqlRowSpecInfo::ParseDefValues(const NYT::TNode& rowSpecAttr) {
+ if (rowSpecAttr.HasKey(RowSpecAttrDefaultValues)) {
+ for (auto& value : rowSpecAttr[RowSpecAttrDefaultValues].AsMap()) {
+ DefaultValues[value.first] = NYT::NodeFromYsonString(value.second.AsString()).AsString();
+ }
+ }
+}
+
+bool TYqlRowSpecInfo::HasNonTrivialSort() const {
+ return Sorted && std::any_of(Sorted->GetContent().cbegin(), Sorted->GetContent().cend(),
+ [](const TSortedConstraintNode::TContainerType::value_type& item) {return 1U != item.first.size() || 1U != item.first.front().size(); });
+}
+
+NYT::TNode TYqlRowSpecInfo::GetConstraintsNode() const {
+ if (ConstraintsNode.HasValue())
+ return ConstraintsNode;
+
+ auto map = NYT::TNode::CreateMap();
+
+ const auto pathToNode = [](const TConstraintNode::TPathType& path) -> NYT::TNode {
+ if (1U == path.size())
+ return TStringBuf(path.front());
+
+ auto list = NYT::TNode::CreateList();
+ for (const auto& col : path)
+ list.Add(TStringBuf(col));
+ return list;
+ };
+
+ if (HasNonTrivialSort()) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& item : Sorted->GetContent()) {
+ auto pair = NYT::TNode::CreateList();
+ auto set = NYT::TNode::CreateList();
+ for (const auto& path : item.first)
+ set.Add(pathToNode(path));
+ pair.Add(set).Add(item.second);
+ list.Add(pair);
+ }
+ map[Sorted->GetName()] = list;
+ }
+
+ if (Unique) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& item : Unique->GetAllSets()) {
+ auto set = NYT::TNode::CreateList();
+ for (const auto& path : item)
+ set.Add(pathToNode(path));
+ list.Add(set);
+ }
+ map[Unique->GetName()] = list;
+ }
+
+ if (Distinct) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& item : Distinct->GetAllSets()) {
+ auto set = NYT::TNode::CreateList();
+ for (const auto& path : item)
+ set.Add(pathToNode(path));
+ list.Add(set);
+ }
+ map[Distinct->GetName()] = list;
+ }
+ return map;
+}
+
+void TYqlRowSpecInfo::FillConstraints(NYT::TNode& attrs) const {
+ if (HasNonTrivialSort() || Unique || Distinct)
+ attrs[RowSpecAttrConstraints] = GetConstraintsNode();
+}
+
+void TYqlRowSpecInfo::ParseConstraintsNode(TExprContext& ctx) {
+ if (!ConstraintsNode.HasValue())
+ return;
+
+ const auto nodeToPath = [&ctx](const NYT::TNode& node) -> TConstraintNode::TPathType {
+ if (node.IsString())
+ return {ctx.AppendString(node.AsString())};
+
+ TConstraintNode::TPathType path;
+ for (const auto& col : node.AsList())
+ path.emplace_back(ctx.AppendString(col.AsString()));
+ return path;
+ };
+
+ const auto& constraints = ConstraintsNode.AsMap();
+
+ if (const auto it = constraints.find(TSortedConstraintNode::Name()); constraints.cend() != it) {
+ TSortedConstraintNode::TContainerType sorted;
+ for (const auto& pair : it->second.AsList()) {
+ TSortedConstraintNode::TSetType set;
+ for (const auto& path : pair.AsList().front().AsList())
+ set.insert_unique(nodeToPath(path));
+ sorted.emplace_back(std::move(set), pair.AsList().back().AsBool());
+ }
+ if (!sorted.empty())
+ Sorted = ctx.MakeConstraint<TSortedConstraintNode>(std::move(sorted));
+ }
+ if (const auto it = constraints.find(TUniqueConstraintNode::Name()); constraints.cend() != it) {
+ TUniqueConstraintNode::TFullSetType sets;
+ for (const auto& item : it->second.AsList()) {
+ TUniqueConstraintNode::TSetType set;
+ for (const auto& path : item.AsList())
+ set.insert_unique(nodeToPath(path));
+ sets.insert_unique(std::move(set));
+ }
+ if (!sets.empty())
+ Unique = ctx.MakeConstraint<TUniqueConstraintNode>(std::move(sets));
+ }
+ if (const auto it = constraints.find(TDistinctConstraintNode::Name()); constraints.cend() != it) {
+ TDistinctConstraintNode::TFullSetType sets;
+ for (const auto& item : it->second.AsList()) {
+ TDistinctConstraintNode::TSetType set;
+ for (const auto& path : item.AsList())
+ set.insert_unique(nodeToPath(path));
+ sets.insert_unique(std::move(set));
+ }
+ if (!sets.empty())
+ Distinct = ctx.MakeConstraint<TDistinctConstraintNode>(std::move(sets));
+ }
+}
+
+TConstraintSet TYqlRowSpecInfo::GetSomeConstraints(ui64 mask, TExprContext& ctx) {
+ TConstraintSet set;
+ if (mask) {
+ ParseConstraintsNode(ctx);
+ if (Sorted && (ui64(EStoredConstraint::Sorted) & mask))
+ set.AddConstraint(Sorted);
+ if (Unique && (ui64(EStoredConstraint::Unique) & mask))
+ set.AddConstraint(Unique);
+ if (Distinct && (ui64(EStoredConstraint::Distinct) & mask))
+ set.AddConstraint(Distinct);
+ }
+ return set;
+}
+
+TConstraintSet TYqlRowSpecInfo::GetAllConstraints(TExprContext& ctx) {
+ return GetSomeConstraints(ui64(EStoredConstraint::Sorted) | ui64(EStoredConstraint::Unique) | ui64(EStoredConstraint::Distinct), ctx);
+}
+
+void TYqlRowSpecInfo::ParseConstraints(const NYT::TNode& rowSpecAttr) {
+ if (rowSpecAttr.HasKey(RowSpecAttrConstraints))
+ ConstraintsNode = rowSpecAttr[RowSpecAttrConstraints];
+}
+
+bool TYqlRowSpecInfo::ValidateSort(const TYTSortInfo& sortInfo, TExprContext& ctx, const TPositionHandle& pos) {
+ if (sortInfo.Keys.empty() && IsSorted()) {
+ ClearSortness();
+ if (!ctx.AddWarning(YqlIssue(ctx.GetPosition(pos), EYqlIssueCode::TIssuesIds_EIssueCode_YT_ROWSPEC_DIFF_SORT,
+ "Table attribute '_yql_row_spec' defines sorting, but the table is not really sorted. The sorting will be ignored."))) {
+ return false;
+ }
+ }
+ else if (!sortInfo.Keys.empty() && !IsSorted()) {
+ if (!ctx.AddWarning(YqlIssue(ctx.GetPosition(pos), EYqlIssueCode::TIssuesIds_EIssueCode_YT_ROWSPEC_DIFF_SORT,
+ "Table attribute '_yql_row_spec' hides the table sorting. The sorting will not be used in query optimization."))) {
+ return false;
+ }
+ } else if (IsSorted()) {
+ bool diff = false;
+ if (SortedBy.size() > sortInfo.Keys.size()) {
+ ClearSortness(sortInfo.Keys.size());
+ diff = true;
+ }
+ auto backendSort = GetForeignSort();
+ for (size_t i = 0; i < backendSort.size(); ++i) {
+ if (backendSort[i].first != sortInfo.Keys[i].first || backendSort[i].second != (bool)sortInfo.Keys[i].second) {
+ ClearSortness(i);
+ diff = true;
+ break;
+ }
+ }
+ if (diff) {
+ TStringBuilder warning;
+ warning << "Table attribute '_yql_row_spec' defines sorting, which differs from the actual one. The table will be assumed ";
+ if (IsSorted()) {
+ warning << "ordered by ";
+ for (size_t i: xrange(SortMembers.size())) {
+ if (i != 0) {
+ warning << ',';
+ }
+ warning << SortMembers[i] << '('
+ << (SortDirections[i] ? "asc" : "desc") << ")";
+ }
+ } else {
+ warning << "unordered";
+ }
+ if (!ctx.AddWarning(YqlIssue(ctx.GetPosition(pos), EYqlIssueCode::TIssuesIds_EIssueCode_YT_ROWSPEC_DIFF_SORT, warning))) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool TYqlRowSpecInfo::Validate(const TExprNode& node, TExprContext& ctx, const TStructExprType*& type, TMaybe<TColumnOrder>& columnOrder) {
+ type = nullptr;
+ columnOrder = {};
+ if (!EnsureCallable(node, ctx)) {
+ return false;
+ }
+ if (!node.IsCallable(TYqlRowSpec::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYqlRowSpec::CallableName()
+ << " callable, but got " << node.Content()));
+ return false;
+ }
+
+ TVector<TString> sortedBy;
+ TVector<TString> sortMembers;
+ size_t sortDirectionsCount = 0;
+ TTypeAnnotationNode::TListType sortedByTypes;
+ THashSet<TStringBuf> defaultNames;
+ TVector<TString> explicitYson;
+ bool extendNonStrict = false;
+ bool strict = true;
+ for (auto child: node.Children()) {
+ if (!EnsureTupleSize(*child, 2, ctx)) {
+ return false;
+ }
+ const TExprNode* name = child->Child(0);
+ TExprNode* value = child->Child(1);
+ if (!EnsureAtom(*name, ctx)) {
+ return false;
+ }
+ bool flagValue = false;
+ if (name->Content() == RowSpecAttrStrictSchema) {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ if (!TryFromString(value->Content(), strict)) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), TStringBuilder() << "Bad value of "
+ << TString{RowSpecAttrStrictSchema}.Quote() << " attribute: " << value->Content()));
+ return false;
+ }
+ } else if (name->Content() == RowSpecAttrUseTypeV2 || name->Content() == RowSpecAttrUseNativeYtTypes) {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ if (!TryFromString(value->Content(), flagValue)) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), TStringBuilder() << "Bad value of "
+ << TString{name->Content()}.Quote() << " attribute: " << value->Content()));
+ return false;
+ }
+ } else if (name->Content() == RowSpecAttrNativeYtTypeFlags) {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ ui64 flags = 0;
+ if (!TryFromString(value->Content(), flags)) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), TStringBuilder() << "Bad value of "
+ << TString{name->Content()}.Quote() << " attribute: " << value->Content()));
+ return false;
+ }
+ } else if (name->Content() == RowSpecAttrUniqueKeys) {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ if (!TryFromString(value->Content(), flagValue)) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), TStringBuilder() << "Bad value of "
+ << TString{RowSpecAttrUniqueKeys}.Quote() << " attribute: " << value->Content()));
+ return false;
+ }
+ } else if (name->Content() == RowSpecAttrType) {
+ const TTypeAnnotationNode* rawType = nullptr;
+ if (value->Type() == TExprNode::Atom) {
+ columnOrder.ConstructInPlace();
+ rawType = NCommon::ParseOrderAwareTypeFromYson(value->Content(), *columnOrder, ctx, ctx.GetPosition(value->Pos()));
+ if (!rawType) {
+ return false;
+ }
+ extendNonStrict = true;
+ } else {
+ if (!EnsureType(*value, ctx)) {
+ return false;
+ }
+ rawType = value->GetTypeAnn()->Cast<TTypeExprType>()->GetType();
+ }
+ if (!EnsureStructType(value->Pos(), *rawType, ctx)) {
+ return false;
+ }
+ type = rawType->Cast<TStructExprType>();
+ } else if (name->Content() == RowSpecAttrSortedBy) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureAtom(*item, ctx)) {
+ return false;
+ }
+ sortedBy.push_back(TString{item->Content()});
+ }
+ } else if (name->Content() == RowSpecAttrSortMembers) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureAtom(*item, ctx)) {
+ return false;
+ }
+ sortMembers.push_back(TString{item->Content()});
+ }
+ } else if (name->Content() == RowSpecAttrSortDirections) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureCallable(*item, ctx)) {
+ return false;
+ }
+ if (!item->IsCallable(TCoBool::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(item->Pos()), TStringBuilder() << "Expected " << TCoBool::CallableName()
+ << ", but got " << item->Content()));
+ return false;
+ }
+ }
+ sortDirectionsCount = value->Children().size();
+ } else if (name->Content() == RowSpecAttrSortedByTypes) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureType(*item, ctx)) {
+ return false;
+ }
+ sortedByTypes.push_back(item->GetTypeAnn()->Cast<TTypeExprType>()->GetType());
+ }
+ } else if (name->Content() == RowSpecAttrDefaultValues) {
+ if (!EnsureTupleMinSize(*value, 1, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureTupleSize(*item, 2, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& atom: item->Children()) {
+ if (!EnsureAtom(*atom, ctx)) {
+ return false;
+ }
+ }
+ if (!defaultNames.insert(item->Child(0)->Content()).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(item->Child(0)->Pos()), TStringBuilder() << "Duplicate "
+ << TString{RowSpecAttrDefaultValues}.Quote() << " key: " << item->Child(0)->Content()));
+ return false;
+ }
+ }
+ } else if (name->Content() == RowSpecAttrExplicitYson) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (const TExprNode::TPtr& item: value->Children()) {
+ if (!EnsureAtom(*item, ctx)) {
+ return false;
+ }
+ explicitYson.emplace_back(item->Content());
+ }
+ } else if (name->Content() == RowSpecAttrConstraints) {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Unsupported "
+ << TYqlRowSpec::CallableName() << " option: " << name->Content()));
+ return false;
+ }
+ }
+ if (!type) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrType}.Quote()
+ << " option is mandatory for " << TYqlRowSpec::CallableName()));
+ return false;
+ }
+ if (sortedBy.size() != sortDirectionsCount) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrSortDirections}.Quote()
+ << " should have the same size as " << TString{RowSpecAttrSortedBy}.Quote()));
+ return false;
+ }
+ if (sortedBy.size() != sortedByTypes.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrSortedByTypes}.Quote()
+ << " should have the same size as " << TString{RowSpecAttrSortedBy}.Quote()));
+ return false;
+ }
+ if (sortMembers.size() > sortedBy.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrSortMembers}.Quote()
+ << " should have the size not greater than " << TString{RowSpecAttrSortedBy}.Quote() << " size"));
+ return false;
+ }
+ for (auto& field: sortMembers) {
+ if (!type->FindItem(field)) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrSortMembers}.Quote()
+ << " uses unknown field " << field.Quote()));
+ return false;
+ }
+ }
+ for (size_t i: xrange(sortedBy.size())) {
+ if (auto ndx = type->FindItem(sortedBy[i])) {
+ if (!IsSameAnnotation(*type->GetItems()[*ndx]->GetItemType(), *sortedByTypes[i])) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrSortedByTypes}.Quote()
+ << " for " << sortedBy[i].Quote() << " field uses unequal type"));
+ return false;
+ }
+ }
+ }
+ for (auto& field: defaultNames) {
+ if (!type->FindItem(field)) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrDefaultValues}.Quote()
+ << " uses unknown field " << TString{field}.Quote()));
+ return false;
+ }
+ }
+ for (auto& field: explicitYson) {
+ if (!type->FindItem(field)) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << TString{RowSpecAttrExplicitYson}.Quote()
+ << " uses unknown field " << field.Quote()));
+ return false;
+ }
+ }
+ if (!strict && extendNonStrict) {
+ auto items = type->GetItems();
+ auto dictType = ctx.MakeType<TDictExprType>(
+ ctx.MakeType<TDataExprType>(EDataSlot::String),
+ ctx.MakeType<TDataExprType>(EDataSlot::String));
+ items.push_back(ctx.MakeType<TItemExprType>(YqlOthersColumnName, dictType));
+ if (columnOrder) {
+ columnOrder->push_back(TString(YqlOthersColumnName));
+ }
+ type = ctx.MakeType<TStructExprType>(items);
+ }
+
+ return true;
+}
+
+bool TYqlRowSpecInfo::Validate(TExprContext& ctx, TPositionHandle positionHandle) {
+ auto pos = ctx.GetPosition(positionHandle);
+ if (SortedBy.size() != SortDirections.size()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrSortDirections}.Quote()
+ << " should have the same size as " << TString{RowSpecAttrSortedBy}.Quote()));
+ return false;
+ }
+ if (SortedBy.size() != SortedByTypes.size()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrSortedByTypes}.Quote()
+ << " should have the same size as " << TString{RowSpecAttrSortedBy}.Quote()));
+ return false;
+ }
+ if (SortMembers.size() > SortedBy.size()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrSortMembers}.Quote()
+ << " should have the size not greater than " << TString{RowSpecAttrSortedBy}.Quote() << " size"));
+ return false;
+ }
+ for (auto& field: SortMembers) {
+ if (!Type->FindItem(field)) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrSortMembers}.Quote()
+ << " uses unknown field " << field.Quote()));
+ return false;
+ }
+ }
+ for (size_t i: xrange(SortedBy.size())) {
+ if (auto ndx = Type->FindItem(SortedBy[i])) {
+ if (!IsSameAnnotation(*Type->GetItems()[*ndx]->GetItemType(), *SortedByTypes[i])) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrSortedByTypes}.Quote()
+ << " for " << SortedBy[i].Quote() << " field uses unequal type"));
+ return false;
+ }
+ }
+ }
+ for (auto& field: DefaultValues) {
+ if (!Type->FindItem(field.first)) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << TString{RowSpecAttrDefaultValues}.Quote()
+ << " uses unknown field " << field.first.Quote()));
+ return false;
+ }
+ }
+ return true;
+}
+
+void TYqlRowSpecInfo::Parse(NNodes::TExprBase node, bool withTypes) {
+ *this = {};
+ FromNode = node;
+ for (auto child: node.Cast<TYqlRowSpec>()) {
+ auto setting = child.Cast<TCoNameValueTuple>();
+
+ if (setting.Name().Value() == RowSpecAttrNativeYtTypeFlags) {
+ NativeYtTypeFlags = FromString<ui64>(setting.Value().Cast<TCoAtom>().Value());
+ } else if (setting.Name().Value() == RowSpecAttrUseNativeYtTypes) {
+ NativeYtTypeFlags = FromString<bool>(setting.Value().Cast<TCoAtom>().Value()) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (setting.Name().Value() == RowSpecAttrUseTypeV2) {
+ NativeYtTypeFlags = FromString<bool>(setting.Value().Cast<TCoAtom>().Value()) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (setting.Name().Value() == RowSpecAttrStrictSchema) {
+ StrictSchema = FromString<bool>(setting.Value().Cast<TCoAtom>().Value());
+ } else if (setting.Name().Value() == RowSpecAttrUniqueKeys) {
+ UniqueKeys = FromString<bool>(setting.Value().Cast<TCoAtom>().Value());
+ } else if (setting.Name().Value() == RowSpecAttrType) {
+ auto& val = setting.Value().Cast().Ref();
+ if (withTypes) {
+ if (val.Type() == TExprNode::Atom) {
+ TypeNode = NYT::NodeFromYsonString(val.Content());
+ Columns = NCommon::ExtractColumnOrderFromYsonStructType(TypeNode);
+ }
+ Type = node.Ref().GetTypeAnn()->Cast<TStructExprType>();
+ }
+ } else if (setting.Name().Value() == RowSpecAttrConstraints) {
+ ConstraintsNode = NYT::NodeFromYsonString(setting.Value().Cast().Ref().Content());
+ } else if (setting.Name().Value() == RowSpecAttrSortedBy) {
+ for (auto item: setting.Value().Cast<TCoAtomList>()) {
+ SortedBy.push_back(TString{item.Value()});
+ }
+ } else if (setting.Name().Value() == RowSpecAttrSortMembers) {
+ for (auto item: setting.Value().Cast<TCoAtomList>()) {
+ SortMembers.push_back(TString{item.Value()});
+ }
+ } else if (setting.Name().Value() == RowSpecAttrSortDirections) {
+ for (auto item: setting.Value().Cast<TExprList>()) {
+ SortDirections.push_back(TStringBuf("true") == item.Cast<TCoBool>().Literal().Value());
+ }
+ } else if (setting.Name().Value() == RowSpecAttrSortedByTypes) {
+ for (auto item: setting.Value().Cast<TExprList>()) {
+ SortedByTypes.push_back(withTypes ? item.Ref().GetTypeAnn()->Cast<TTypeExprType>()->GetType() : nullptr);
+ }
+ } else if (setting.Name().Value() == RowSpecAttrDefaultValues) {
+ for (auto item: setting.Value().Cast<TExprList>()) {
+ auto atomList = item.Cast<TCoAtomList>();
+ DefaultValues[TString{atomList.Item(0).Value()}] = TString{atomList.Item(1).Value()};
+ }
+ } else if (setting.Name().Value() == RowSpecAttrExplicitYson) {
+ for (auto item: setting.Value().Cast<TCoAtomList>()) {
+ ExplicitYson.emplace_back(item.Value());
+ }
+ } else {
+ YQL_ENSURE(false, "Unexpected option " << setting.Name().Value());
+ }
+ }
+ if (Columns && !StrictSchema) {
+ Columns->push_back(TString(YqlOthersColumnName));
+ }
+}
+
+ui64 TYqlRowSpecInfo::GetNativeYtTypeFlags(const NCommon::TStructMemberMapper& mapper) const {
+ return mapper ? (NativeYtTypeFlags & NYql::GetNativeYtTypeFlags(*Type, mapper)) : NativeYtTypeFlags;
+}
+
+NYT::TNode TYqlRowSpecInfo::GetTypeNode(const NCommon::TStructMemberMapper& mapper) const {
+ if (!TypeNode.IsUndefined()) {
+ if (!mapper) {
+ return TypeNode;
+ }
+ YQL_ENSURE(TypeNode.IsList() && TypeNode.Size() == 2 && TypeNode[0].IsString() && TypeNode[0].AsString() == "StructType" && TypeNode[1].IsList());
+
+ NYT::TNode members = NYT::TNode::CreateList();
+ for (auto& member : TypeNode[1].AsList()) {
+ YQL_ENSURE(member.IsList() && member.Size() == 2 && member[0].IsString());
+
+ if (auto name = mapper(member[0].AsString())) {
+ members.Add(NYT::TNode::CreateList().Add(*name).Add(member[1]));
+ }
+ }
+ return NYT::TNode::CreateList().Add("StructType").Add(members);
+ }
+ NYT::TNode typeNode;
+ NYT::TNodeBuilder nodeBuilder(&typeNode);
+ NCommon::SaveStructTypeToYson(nodeBuilder, Type, Columns, mapper);
+ return typeNode;
+}
+
+void TYqlRowSpecInfo::SetType(const TStructExprType* type, TMaybe<ui64> nativeYtTypeFlags) {
+ Type = type;
+ Columns = {};
+ TypeNode = {};
+ if (nativeYtTypeFlags) {
+ NativeYtTypeFlags = *nativeYtTypeFlags;
+ }
+ NativeYtTypeFlags &= NYql::GetNativeYtTypeFlags(*Type);
+}
+
+void TYqlRowSpecInfo::SetColumnOrder(const TMaybe<TColumnOrder>& columns) {
+ TypeNode = {};
+ Columns = columns;
+}
+
+TString TYqlRowSpecInfo::ToYsonString() const {
+ NYT::TNode attrs = NYT::TNode::CreateMap();
+ FillCodecNode(attrs[YqlRowSpecAttribute]);
+ return NYT::NodeToCanonicalYsonString(attrs);
+}
+
+void TYqlRowSpecInfo::CopyTypeOrders(const NYT::TNode& typeNode) {
+ YQL_ENSURE(Type);
+ if (!TypeNode.IsUndefined() || 0 == NativeYtTypeFlags) {
+ return;
+ }
+
+ YQL_ENSURE(typeNode.IsList() && typeNode.Size() == 2 && typeNode[0].IsString() && typeNode[0].AsString() == "StructType" && typeNode[1].IsList());
+
+ THashMap<TString, NYT::TNode> fromMembers;
+ for (auto& member : typeNode[1].AsList()) {
+ YQL_ENSURE(member.IsList() && member.Size() == 2 && member[0].IsString());
+
+ fromMembers.emplace(member[0].AsString(), member[1]);
+ }
+
+ NYT::TNode members = NYT::TNode::CreateList();
+ TVector<TString> columns;
+ if (Columns.Defined() && Columns->size() == Type->GetSize()) {
+ columns = *Columns;
+ } else {
+ for (auto& item : Type->GetItems()) {
+ columns.emplace_back(item->GetName());
+ }
+ }
+ for (auto name: columns) {
+ if (!StrictSchema && name == YqlOthersColumnName) {
+ continue;
+ }
+ auto origType = Type->FindItemType(name);
+ YQL_ENSURE(origType);
+ auto origTypeNode = NCommon::TypeToYsonNode(origType);
+ auto it = fromMembers.find(name);
+ if (it == fromMembers.end() || !NCommon::EqualsYsonTypesIgnoreStructOrder(origTypeNode, it->second)) {
+ members.Add(NYT::TNode::CreateList().Add(name).Add(origTypeNode));
+ } else {
+ members.Add(NYT::TNode::CreateList().Add(name).Add(it->second));
+ }
+ }
+
+ TypeNode = NYT::TNode::CreateList().Add("StructType").Add(members);
+}
+
+void TYqlRowSpecInfo::FillTypeTransform(NYT::TNode& attrs, TStringBuf typeNameAttr, const NCommon::TStructMemberMapper& mapper) const {
+ if (!TypeNode.IsUndefined()) {
+ YQL_ENSURE(TypeNode.IsList() && TypeNode.Size() == 2 && TypeNode[0].IsString() && TypeNode[0].AsString() == "StructType" && TypeNode[1].IsList());
+
+ NYT::TNode members = NYT::TNode::CreateList();
+ for (auto& member : TypeNode[1].AsList()) {
+ YQL_ENSURE(member.IsList() && member.Size() == 2 && member[0].IsString());
+
+ if (auto name = mapper(member[0].AsString())) {
+ members.Add(NYT::TNode::CreateList().Add(*name).Add(member[1]));
+ }
+ }
+ attrs[typeNameAttr] = NYT::TNode::CreateList().Add("StructType").Add(members);
+
+ } else {
+ NYT::TNodeBuilder specAttrBuilder(&attrs[typeNameAttr]);
+ NCommon::SaveStructTypeToYson(specAttrBuilder, Type, Columns, mapper);
+ }
+}
+
+void TYqlRowSpecInfo::FillSort(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper) const {
+ TVector<bool> sortDirections;
+ TVector<TString> sortedBy;
+ TVector<TString> sortMembers;
+ TTypeAnnotationNode::TListType sortedByTypes;
+
+ bool curUniqueKeys = UniqueKeys;
+ const TVector<bool>* curSortDirections = &SortDirections;
+ const TVector<TString>* curSortedBy = &SortedBy;
+ const TVector<TString>* curSortMembers = &SortMembers;
+ const TTypeAnnotationNode::TListType* curSortedByTypes = &SortedByTypes;
+ if (mapper) {
+ sortDirections = SortDirections;
+ sortedBy = SortedBy;
+ sortMembers = SortMembers;
+ sortedByTypes = SortedByTypes;
+
+ curSortDirections = &sortDirections;
+ curSortedBy = &sortedBy;
+ curSortMembers = &sortMembers;
+ curSortedByTypes = &sortedByTypes;
+ for (size_t i = 0; i < sortedBy.size(); ++i) {
+ if (Type->FindItem(sortedBy[i])) {
+ if (auto name = mapper(sortedBy[i])) {
+ sortedBy[i] = TString{*name};
+ if (i < sortMembers.size()) {
+ sortMembers[i] = sortedBy[i];
+ }
+ } else {
+ if (i < sortMembers.size()) {
+ sortMembers.erase(sortMembers.begin() + i, sortMembers.end());
+ }
+ sortedBy.erase(sortedBy.begin() + i, sortedBy.end());
+ sortedByTypes.erase(sortedByTypes.begin() + i, sortedByTypes.end());
+ sortDirections.erase(sortDirections.begin() + i, sortDirections.end());
+ curUniqueKeys = false;
+ break;
+ }
+ }
+ }
+ }
+ if (!curSortedBy->empty()) {
+ attrs[RowSpecAttrUniqueKeys] = curUniqueKeys;
+ }
+
+ if (!curSortDirections->empty()) {
+ auto list = NYT::TNode::CreateList();
+ for (bool dir: *curSortDirections) {
+ list.Add(dir ? 1 : 0);
+ }
+ attrs[RowSpecAttrSortDirections] = list;
+ }
+
+ auto saveColumnList = [&attrs] (TStringBuf name, const TVector<TString>& columns) {
+ if (!columns.empty()) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& item : columns) {
+ list.Add(item);
+ }
+ attrs[name] = list;
+ }
+ };
+
+ saveColumnList(RowSpecAttrSortMembers, *curSortMembers);
+ saveColumnList(RowSpecAttrSortedBy, *curSortedBy);
+
+ if (!curSortedByTypes->empty()) {
+ auto list = NYT::TNode::CreateList();
+ for (auto type: *curSortedByTypes) {
+ list.Add(NCommon::TypeToYsonNode(type));
+ }
+ attrs[RowSpecAttrSortedByTypes] = list;
+ }
+}
+
+void TYqlRowSpecInfo::FillDefValues(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper) const {
+ if (!DefaultValues.empty()) {
+ auto map = NYT::TNode::CreateMap();
+ if (mapper) {
+ for (const auto& val: DefaultValues) {
+ if (auto name = mapper(val.first)) {
+ map[*name] = NYT::NodeToYsonString(NYT::TNode(val.second));
+ }
+ }
+ } else {
+ for (const auto& val: DefaultValues) {
+ map[val.first] = NYT::NodeToYsonString(NYT::TNode(val.second));
+ }
+ }
+ if (!map.AsMap().empty()) {
+ attrs[RowSpecAttrDefaultValues] = map;
+ }
+ }
+}
+
+void TYqlRowSpecInfo::FillFlags(NYT::TNode& attrs) const {
+ attrs[RowSpecAttrStrictSchema] = StrictSchema;
+ attrs[RowSpecAttrNativeYtTypeFlags] = NativeYtTypeFlags;
+ // Backward compatibility. TODO: remove after releasing compatibility flags
+ if (NativeYtTypeFlags != 0) {
+ attrs[RowSpecAttrUseNativeYtTypes] = true;
+ }
+}
+
+void TYqlRowSpecInfo::FillExplicitYson(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper) const {
+ TVector<TString> localExplicitYson;
+ const TVector<TString>* curExplicitYson = &ExplicitYson;
+ if (mapper) {
+ for (size_t i = 0; i < ExplicitYson.size(); ++i) {
+ if (Type->FindItem(ExplicitYson[i])) {
+ if (auto name = mapper(ExplicitYson[i])) {
+ localExplicitYson.emplace_back(TString{*name});
+ }
+ }
+ }
+ curExplicitYson = &localExplicitYson;
+ }
+ if (!curExplicitYson->empty()) {
+ auto list = NYT::TNode::CreateList();
+ for (const auto& item : *curExplicitYson) {
+ list.Add(item);
+ }
+ attrs[RowSpecAttrExplicitYson] = list;
+ }
+}
+
+void TYqlRowSpecInfo::FillCodecNode(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper) const {
+ attrs = NYT::TNode::CreateMap();
+
+ attrs[RowSpecAttrType] = GetTypeNode(mapper);
+ FillSort(attrs, mapper);
+ FillDefValues(attrs, mapper);
+ FillFlags(attrs);
+ FillExplicitYson(attrs, mapper);
+}
+
+void TYqlRowSpecInfo::FillAttrNode(NYT::TNode& attrs, ui64 nativeTypeCompatibility, bool useCompactForm) const {
+ attrs = NYT::TNode::CreateMap();
+
+ if (!useCompactForm) {
+ auto otherFilter = [strict = StrictSchema](TStringBuf name) -> TMaybe<TStringBuf> {
+ if (!strict && name == YqlOthersColumnName) {
+ return Nothing();
+ }
+ return MakeMaybe(name);
+ };
+ attrs[RowSpecAttrType] = GetTypeNode(otherFilter);
+ }
+
+ THashSet<TStringBuf> patchedFields;
+ for (auto item: Type->GetItems()) {
+ const TTypeAnnotationNode* itemType = item->GetItemType();
+ // Top-level strict Yson is converted to Yson? in YT schema
+ if (itemType->GetKind() == ETypeAnnotationKind::Data && itemType->Cast<TDataExprType>()->GetSlot() == EDataSlot::Yson) {
+ patchedFields.insert(item->GetName());
+ } else {
+ if (itemType->GetKind() == ETypeAnnotationKind::Optional) {
+ itemType = itemType->Cast<TOptionalExprType>()->GetItemType();
+ }
+ auto flags = GetNativeYtTypeFlagsImpl(itemType);
+ if (flags != (flags & NativeYtTypeFlags & nativeTypeCompatibility)) {
+ patchedFields.insert(item->GetName());
+ }
+ }
+ }
+
+ attrs[RowSpecAttrTypePatch] = GetTypeNode([&patchedFields](TStringBuf name) -> TMaybe<TStringBuf> {
+ return patchedFields.contains(name) ? MakeMaybe(name) : Nothing();
+ });
+
+ if (!useCompactForm || HasAuxColumns() || AnyOf(SortedBy, [&patchedFields](const auto& name) { return patchedFields.contains(name); } )) {
+ FillSort(attrs);
+ }
+ FillDefValues(attrs);
+ FillFlags(attrs);
+ FillConstraints(attrs);
+}
+
+NNodes::TExprBase TYqlRowSpecInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto rowSpecBuilder = Build<TYqlRowSpec>(ctx, pos);
+
+ auto otherFilter = [strict = StrictSchema](TStringBuf name) -> TMaybe<TStringBuf> {
+ if (!strict && name == YqlOthersColumnName) {
+ return Nothing();
+ }
+ return MakeMaybe(name);
+ };
+ rowSpecBuilder
+ .Add()
+ .Name()
+ .Value(RowSpecAttrNativeYtTypeFlags, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(ToString(NativeYtTypeFlags), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Add()
+ .Name()
+ .Value(RowSpecAttrStrictSchema, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(StrictSchema ? TStringBuf("1") : TStringBuf("0"), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Add()
+ .Name()
+ .Value(RowSpecAttrUniqueKeys, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(UniqueKeys ? TStringBuf("1") : TStringBuf("0"), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Add()
+ .Name()
+ .Value(RowSpecAttrType, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(NYT::NodeToYsonString(GetTypeNode(otherFilter), NYson::EYsonFormat::Text), TNodeFlags::MultilineContent)
+ .Build()
+ .Build();
+
+ if (ConstraintsNode.HasValue() || HasNonTrivialSort() || Unique || Distinct) {
+ rowSpecBuilder.Add()
+ .Name()
+ .Value(RowSpecAttrConstraints, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(NYT::NodeToYsonString(GetConstraintsNode(), NYson::EYsonFormat::Text), TNodeFlags::MultilineContent)
+ .Build()
+ .Build();
+ }
+
+ if (!SortDirections.empty()) {
+ auto listBuilder = Build<TExprList>(ctx, pos);
+ for (bool dir: SortDirections) {
+ listBuilder.Add<TCoBool>()
+ .Literal<TCoAtom>()
+ .Value(dir ? TStringBuf("true") : TStringBuf("false"), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+ rowSpecBuilder
+ .Add()
+ .Name()
+ .Value(RowSpecAttrSortDirections, TNodeFlags::Default)
+ .Build()
+ .Value(listBuilder.Done())
+ .Build();
+ }
+
+ auto saveColumnList = [&] (TStringBuf name, const TVector<TString>& columns) {
+ if (!columns.empty()) {
+ auto listBuilder = Build<TExprList>(ctx, pos);
+ for (auto& column: columns) {
+ listBuilder.Add<TCoAtom>()
+ .Value(column)
+ .Build();
+ }
+ rowSpecBuilder
+ .Add()
+ .Name()
+ .Value(name, TNodeFlags::Default)
+ .Build()
+ .Value(listBuilder.Done())
+ .Build();
+ }
+ };
+
+ saveColumnList(RowSpecAttrSortMembers, SortMembers);
+ saveColumnList(RowSpecAttrSortedBy, SortedBy);
+
+ if (!SortedByTypes.empty()) {
+ auto listBuilder = Build<TExprList>(ctx, pos);
+ for (auto type: SortedByTypes) {
+ listBuilder.Add(TExprBase(NCommon::BuildTypeExpr(pos, *type, ctx)));
+ }
+ rowSpecBuilder
+ .Add()
+ .Name()
+ .Value(RowSpecAttrSortedByTypes, TNodeFlags::Default)
+ .Build()
+ .Value(listBuilder.Done())
+ .Build();
+ }
+
+ if (!DefaultValues.empty()) {
+ auto listBuilder = Build<TExprList>(ctx, pos);
+ for (const auto& val: DefaultValues) {
+ listBuilder.Add<TCoAtomList>()
+ .Add().Value(val.first).Build()
+ .Add().Value(val.second).Build()
+ .Build();
+ }
+ rowSpecBuilder
+ .Add()
+ .Name()
+ .Value(RowSpecAttrDefaultValues, TNodeFlags::Default)
+ .Build()
+ .Value(listBuilder.Done())
+ .Build();
+ }
+ saveColumnList(RowSpecAttrExplicitYson, ExplicitYson);
+
+ return rowSpecBuilder.Done();
+}
+
+bool TYqlRowSpecInfo::HasAuxColumns() const {
+ for (auto& x: SortedBy) {
+ if (!Type->FindItem(x)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+TVector<std::pair<TString, const TTypeAnnotationNode*>> TYqlRowSpecInfo::GetAuxColumns() const {
+ TVector<std::pair<TString, const TTypeAnnotationNode*>> res;
+ for (size_t i: xrange(SortedBy.size())) {
+ if (!Type->FindItem(SortedBy[i])) {
+ res.emplace_back(SortedBy[i], SortedByTypes[i]);
+ }
+ }
+ return res;
+}
+
+const TStructExprType* TYqlRowSpecInfo::GetExtendedType(TExprContext& ctx) const {
+ if (!IsSorted()) {
+ return Type;
+ }
+ bool extended = false;
+ TVector<const TItemExprType*> items = Type->GetItems();
+ for (size_t i: xrange(SortedBy.size())) {
+ if (!Type->FindItem(SortedBy[i])) {
+ items.push_back(ctx.MakeType<TItemExprType>(SortedBy[i], SortedByTypes[i]));
+ extended = true;
+ }
+ }
+ return extended ? ctx.MakeType<TStructExprType>(items) : Type;
+}
+
+bool TYqlRowSpecInfo::CopySortness(const TYqlRowSpecInfo& from, ECopySort mode) {
+ SortDirections = from.SortDirections;
+ SortMembers = from.SortMembers;
+ SortedBy = from.SortedBy;
+ SortedByTypes = from.SortedByTypes;
+ UniqueKeys = from.UniqueKeys;
+ bool sortIsChanged = false;
+ if (ECopySort::Exact != mode) {
+ YQL_ENSURE(SortMembers.size() <= SortedBy.size());
+ for (size_t i = 0; i < SortMembers.size(); ++i) {
+ const auto itemNdx = Type->FindItem(SortMembers[i]);
+ if (!itemNdx || (SortedBy[i] == SortMembers[i] && Type->GetItems()[*itemNdx]->GetItemType() != SortedByTypes[i])) {
+ sortIsChanged = ClearSortness(i);
+ break;
+ } else if (ECopySort::Pure == mode && SortedBy[i] != SortMembers[i]) {
+ sortIsChanged = ClearSortness(i);
+ break;
+ }
+ }
+ if (ECopySort::WithCalc != mode) {
+ if (SortMembers.size() < SortedBy.size()) {
+ sortIsChanged = ClearSortness(SortMembers.size()) || sortIsChanged;
+ }
+ }
+ }
+ return sortIsChanged;
+}
+
+void TYqlRowSpecInfo::CopyConstraints(const TYqlRowSpecInfo& from) {
+ ConstraintsNode = from.ConstraintsNode;
+ Sorted = from.Sorted;
+ Unique = from.Unique;
+ Distinct = from.Distinct;
+}
+
+bool TYqlRowSpecInfo::KeepPureSortOnly() {
+ bool sortIsChanged = false;
+ for (size_t i = 0; i < SortMembers.size(); ++i) {
+ if (!Type->FindItem(SortMembers[i])) {
+ sortIsChanged = ClearSortness(i);
+ break;
+ } else if (SortedBy[i] != SortMembers[i]) {
+ sortIsChanged = ClearSortness(i);
+ break;
+ }
+ }
+ if (SortMembers.size() < SortedBy.size()) {
+ sortIsChanged = ClearSortness(SortMembers.size()) || sortIsChanged;
+ }
+ return sortIsChanged;
+}
+
+bool TYqlRowSpecInfo::ClearNativeDescendingSort() {
+ for (size_t i = 0; i < SortDirections.size(); ++i) {
+ if (!SortDirections[i] && Type->FindItem(SortedBy[i])) {
+ return ClearSortness(i);
+ }
+ }
+ return false;
+}
+
+bool TYqlRowSpecInfo::MakeCommonSortness(const TYqlRowSpecInfo& from) {
+ bool sortIsChanged = false;
+ UniqueKeys = false; // Merge of two and more tables cannot have unique keys
+ const size_t resultSize = Min<size_t>(SortMembers.size(), from.SortMembers.size()); // Truncate all calculated columns
+ if (SortedBy.size() > resultSize) {
+ sortIsChanged = ClearSortness(resultSize);
+ }
+ for (size_t i = 0; i < resultSize; ++i) {
+ if (SortMembers[i] != from.SortMembers[i] || SortedBy[i] != from.SortedBy[i] || SortedByTypes[i] != from.SortedByTypes[i] || SortDirections[i] != from.SortDirections[i]) {
+ sortIsChanged = ClearSortness(i) || sortIsChanged;
+ break;
+ }
+ }
+ return sortIsChanged;
+}
+
+bool TYqlRowSpecInfo::CompareSortness(const TYqlRowSpecInfo& with, bool checkUniqueFlag) const {
+ return SortDirections == with.SortDirections
+ && SortMembers == with.SortMembers
+ && SortedBy == with.SortedBy
+ && SortedByTypes.size() == with.SortedByTypes.size()
+ && std::equal(SortedByTypes.cbegin(), SortedByTypes.cend(), with.SortedByTypes.cbegin(), TTypeAnnotationNode::TEqual())
+ && (!checkUniqueFlag || UniqueKeys == with.UniqueKeys);
+}
+
+bool TYqlRowSpecInfo::ClearSortness(size_t fromMember) {
+ if (fromMember <= SortMembers.size()) {
+ SortMembers.erase(SortMembers.begin() + fromMember, SortMembers.end());
+ SortedBy.erase(SortedBy.begin() + fromMember, SortedBy.end());
+ SortedByTypes.erase(SortedByTypes.begin() + fromMember, SortedByTypes.end());
+ SortDirections.erase(SortDirections.begin() + fromMember, SortDirections.end());
+ UniqueKeys = false;
+ return true;
+ }
+ return false;
+}
+
+const TSortedConstraintNode* TYqlRowSpecInfo::MakeSortConstraint(TExprContext& ctx) const {
+ if (!SortMembers.empty()) {
+ TSortedConstraintNode::TContainerType sorted;
+ for (auto i = 0U; i < SortMembers.size(); ++i) {
+ const auto column = ctx.AppendString(SortMembers[i]);
+ sorted.emplace_back(TSortedConstraintNode::TSetType{TConstraintNode::TPathType{column}}, i >= SortDirections.size() || SortDirections[i]);
+ }
+ return ctx.MakeConstraint<TSortedConstraintNode>(std::move(sorted));
+ }
+ return nullptr;
+}
+
+const TDistinctConstraintNode* TYqlRowSpecInfo::MakeDistinctConstraint(TExprContext& ctx) const {
+ if (UniqueKeys && !SortMembers.empty() && SortedBy.size() == SortMembers.size()) {
+ std::vector<std::string_view> uniqColumns(SortMembers.size());
+ std::transform(SortMembers.cbegin(), SortMembers.cend(), uniqColumns.begin(), std::bind(&TExprContext::AppendString, std::ref(ctx), std::placeholders::_1));
+ return ctx.MakeConstraint<TDistinctConstraintNode>(uniqColumns);
+ }
+ return nullptr;
+}
+
+TVector<std::pair<TString, bool>> TYqlRowSpecInfo::GetForeignSort() const {
+ TVector<std::pair<TString, bool>> res;
+ for (size_t i = 0; i < SortedBy.size(); ++i) {
+ res.emplace_back(SortedBy[i], Type->FindItem(SortedBy[i]) ? SortDirections.at(i) : true);
+ }
+ return res;
+}
+
+void TYqlRowSpecInfo::SetConstraints(const TConstraintSet& constraints) {
+ ConstraintsNode.Clear();
+ Sorted = constraints.GetConstraint<TSortedConstraintNode>();
+ Unique = constraints.GetConstraint<TUniqueConstraintNode>();
+ Distinct = constraints.GetConstraint<TDistinctConstraintNode>();
+}
+
+TConstraintSet TYqlRowSpecInfo::GetConstraints() const {
+ TConstraintSet set;
+ if (Sorted)
+ set.AddConstraint(Sorted);
+ if (Unique)
+ set.AddConstraint(Unique);
+ if (Distinct)
+ set.AddConstraint(Distinct);
+ return set;
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h b/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h
new file mode 100644
index 0000000000..78232cce74
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h
@@ -0,0 +1,155 @@
+#pragma once
+
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/ast/yql_constraint.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+#include <util/generic/ptr.h>
+#include <util/generic/map.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/maybe.h>
+
+#include <functional>
+
+namespace NYql {
+
+class TStructExprType;
+struct TYTSortInfo;
+
+struct TYqlRowSpecInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYqlRowSpecInfo>;
+
+ enum class ECopySort {
+ Pure,
+ WithDesc,
+ WithCalc,
+ Exact,
+ };
+
+ TYqlRowSpecInfo() {
+ }
+
+ TYqlRowSpecInfo(NNodes::TExprBase node, bool withTypes = true) {
+ Parse(node, withTypes);
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx, const TStructExprType*& type, TMaybe<TColumnOrder>& columnOrder);
+ bool Parse(const TString& rowSpecYson, TExprContext& ctx, const TPositionHandle& pos = {});
+ bool Parse(const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos = {});
+ bool Parse(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos = {});
+ void Parse(NNodes::TExprBase node, bool withTypes = true);
+ bool Validate(TExprContext& ctx, TPositionHandle pos);
+
+ TString ToYsonString() const;
+ void FillCodecNode(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper = {}) const;
+ void FillAttrNode(NYT::TNode& attrs, ui64 nativeTypeCompatibility, bool useCompactForm) const;
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const;
+
+ bool IsSorted() const {
+ return !SortedBy.empty();
+ }
+ bool HasAuxColumns() const;
+ TVector<std::pair<TString, const TTypeAnnotationNode*>> GetAuxColumns() const;
+ // Includes aux columns
+ const TStructExprType* GetExtendedType(TExprContext& ctx) const;
+ // Returns true if sortness is changed
+ bool CopySortness(const TYqlRowSpecInfo& from, ECopySort mode = ECopySort::Pure);
+ // Returns true if sortness is changed
+ bool MakeCommonSortness(const TYqlRowSpecInfo& from);
+ bool CompareSortness(const TYqlRowSpecInfo& with, bool checkUniqueFlag = true) const;
+ // Returns true if sortness is changed
+ bool ClearSortness(size_t fromMember = 0);
+ // Returns true if sortness is changed
+ bool KeepPureSortOnly();
+ bool ClearNativeDescendingSort();
+ const TSortedConstraintNode* MakeSortConstraint(TExprContext& ctx) const;
+ const TDistinctConstraintNode* MakeDistinctConstraint(TExprContext& ctx) const;
+ void CopyConstraints(const TYqlRowSpecInfo& from);
+
+ const TStructExprType* GetType() const {
+ return Type;
+ }
+
+ NYT::TNode GetTypeNode(const NCommon::TStructMemberMapper& mapper = {}) const;
+
+ const TMaybe<TColumnOrder>& GetColumnOrder() const {
+ return Columns;
+ }
+
+ void SetType(const TStructExprType* type, TMaybe<ui64> nativeYtTypeFlags = Nothing());
+ void SetColumnOrder(const TMaybe<TColumnOrder>& columnOrder);
+ void SetConstraints(const TConstraintSet& constraints);
+ TConstraintSet GetConstraints() const;
+ TConstraintSet GetSomeConstraints(ui64 mask, TExprContext& ctx);
+ TConstraintSet GetAllConstraints(TExprContext& ctx);
+
+ void CopyType(const TYqlRowSpecInfo& from) {
+ Type = from.Type;
+ Columns = from.Columns;
+ TypeNode = from.TypeNode;
+ NativeYtTypeFlags = from.NativeYtTypeFlags;
+ }
+
+ void CopyTypeOrders(const NYT::TNode& typeNode);
+
+ ui64 GetNativeYtTypeFlags(const NCommon::TStructMemberMapper& mapper = {}) const;
+
+ TMaybe<NYT::TNode> GetNativeYtType(const NCommon::TStructMemberMapper& mapper = {}) const {
+ return 0 != GetNativeYtTypeFlags(mapper) ? MakeMaybe(GetTypeNode(mapper)) : Nothing();
+ }
+
+ TVector<std::pair<TString, bool>> GetForeignSort() const;
+
+ NNodes::TMaybeNode<NNodes::TExprBase> FromNode;
+
+ bool StrictSchema = true;
+ bool UniqueKeys = false;
+
+ TVector<bool> SortDirections;
+ TVector<TString> SortedBy;
+ TVector<TString> SortMembers;
+ TTypeAnnotationNode::TListType SortedByTypes;
+ TMap<TString, TString> DefaultValues;
+ TVector<TString> ExplicitYson;
+
+private:
+ void FillTypeTransform(NYT::TNode& attrs, TStringBuf typeNameAttr, const NCommon::TStructMemberMapper& mapper) const;
+ void FillSort(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper = {}) const;
+ void FillDefValues(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper = {}) const;
+ void FillFlags(NYT::TNode& attrs) const;
+ void FillConstraints(NYT::TNode& attrs) const;
+ void FillExplicitYson(NYT::TNode& attrs, const NCommon::TStructMemberMapper& mapper) const;
+ bool ParsePatched(const NYT::TNode& rowSpecAttr, const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos);
+ bool ParseFull(const NYT::TNode& rowSpecAttr, const THashMap<TString, TString>& attrs, TExprContext& ctx, const TPositionHandle& pos);
+ bool ParseType(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos);
+ bool ParseSort(const NYT::TNode& rowSpecAttr, TExprContext& ctx, const TPositionHandle& pos);
+ void ParseFlags(const NYT::TNode& rowSpecAttr);
+ void ParseConstraints(const NYT::TNode& rowSpecAttr);
+ void ParseConstraintsNode(TExprContext& ctx);
+ void ParseDefValues(const NYT::TNode& rowSpecAttr);
+ bool ValidateSort(const TYTSortInfo& sortInfo, TExprContext& ctx, const TPositionHandle& pos);
+
+ NYT::TNode GetConstraintsNode() const;
+ bool HasNonTrivialSort() const;
+private:
+ const TStructExprType* Type = nullptr;
+ TMaybe<TColumnOrder> Columns;
+ NYT::TNode TypeNode;
+ ui64 NativeYtTypeFlags = 0ul;
+
+ NYT::TNode ConstraintsNode;
+ const TSortedConstraintNode* Sorted = nullptr;
+ const TUniqueConstraintNode* Unique = nullptr;
+ const TDistinctConstraintNode* Distinct = nullptr;
+};
+
+ui64 GetNativeYtTypeFlags(const TStructExprType& type, const NCommon::TStructMemberMapper& mapper = {});
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/schema/schema.cpp b/ydb/library/yql/providers/yt/lib/schema/schema.cpp
new file mode 100644
index 0000000000..19261a5fa5
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/schema.cpp
@@ -0,0 +1,1018 @@
+#include "schema.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/string/cast.h>
+#include <util/generic/yexception.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/set.h>
+#include <util/generic/hash.h>
+
+
+namespace NYql {
+
+static TString ConvertYtDataType(const TString& ytType, ui64& nativeYtTypeFlags) {
+ TString yqlType;
+ if (ytType == "string") {
+ yqlType = "String";
+ }
+ else if (ytType == "utf8") {
+ yqlType = "Utf8";
+ }
+ else if (ytType == "int64") {
+ yqlType = "Int64";
+ }
+ else if (ytType == "uint64") {
+ yqlType = "Uint64";
+ }
+ else if (ytType == "int32") {
+ yqlType = "Int32";
+ }
+ else if (ytType == "uint32") {
+ yqlType = "Uint32";
+ }
+ else if (ytType == "int16") {
+ yqlType = "Int16";
+ }
+ else if (ytType == "uint16") {
+ yqlType = "Uint16";
+ }
+ else if (ytType == "int8") {
+ yqlType = "Int8";
+ }
+ else if (ytType == "uint8") {
+ yqlType = "Uint8";
+ }
+ else if (ytType == "double") {
+ yqlType = "Double";
+ }
+ else if (ytType == "float") {
+ nativeYtTypeFlags |= NTCF_FLOAT;
+ yqlType = "Float";
+ }
+ else if (ytType == "boolean") { // V2
+ yqlType = "Bool";
+ }
+ else if (ytType == "bool") { // V3
+ yqlType = "Bool";
+ }
+ else if (ytType == "date") {
+ nativeYtTypeFlags |= NTCF_DATE;
+ yqlType = "Date";
+ }
+ else if (ytType == "datetime") {
+ nativeYtTypeFlags |= NTCF_DATE;
+ yqlType = "Datetime";
+ }
+ else if (ytType == "timestamp") {
+ nativeYtTypeFlags |= NTCF_DATE;
+ yqlType = "Timestamp";
+ }
+ else if (ytType == "interval") {
+ nativeYtTypeFlags |= NTCF_DATE;
+ yqlType = "Interval";
+ }
+ else if (ytType == "yson") { // V3
+ yqlType = "Yson";
+ }
+ else if (ytType == "json") {
+ nativeYtTypeFlags |= NTCF_JSON;
+ yqlType = "Json";
+ }
+ else if (ytType == "any") {
+ yqlType = "Yson";
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Unknown type " << ytType.Quote() << " in yson schema";
+ }
+
+ return yqlType;
+}
+
+static NYT::TNode ConvertNativeYtType(const NYT::TNode& raw, bool root, bool& hasYson, ui64& nativeYtTypeFlags) {
+ if (raw.IsString()) {
+ if (raw.AsString() == "null") {
+ nativeYtTypeFlags |= NTCF_NULL;
+ return NYT::TNode().Add("NullType");
+ }
+
+ if (raw.AsString() == "void") {
+ nativeYtTypeFlags |= NTCF_VOID;
+ return NYT::TNode().Add("VoidType");
+ }
+
+ hasYson = hasYson || "yson" == raw.AsString();
+ return NYT::TNode()
+ .Add("DataType")
+ .Add(ConvertYtDataType(raw.AsString(), nativeYtTypeFlags));
+ }
+
+ const auto& typeName = raw["type_name"].AsString();
+ if (typeName == "decimal") {
+ nativeYtTypeFlags |= NTCF_DECIMAL;
+ return NYT::TNode()
+ .Add("DataType")
+ .Add("Decimal")
+ .Add(ToString(raw["precision"].AsInt64()))
+ .Add(ToString(raw["scale"].AsInt64()));
+ } else if (typeName == "list") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ return NYT::TNode()
+ .Add("ListType")
+ .Add(ConvertNativeYtType(raw["item"], false, hasYson, nativeYtTypeFlags));
+ } else if (typeName == "optional") {
+ if (!root) {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ }
+ return NYT::TNode()
+ .Add("OptionalType")
+ .Add(ConvertNativeYtType(raw["item"], false, hasYson, nativeYtTypeFlags));
+ } else if (typeName == "tuple") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ auto list = NYT::TNode::CreateList();
+ for (const auto& x : raw["elements"].AsList()) {
+ list.Add(ConvertNativeYtType(x["type"], false, hasYson, nativeYtTypeFlags));
+ }
+ return NYT::TNode()
+ .Add("TupleType")
+ .Add(list);
+ } else if (typeName == "struct") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ auto list = NYT::TNode::CreateList();
+ for (const auto& x : raw["members"].AsList()) {
+ list.Add(NYT::TNode()
+ .Add(x["name"].AsString())
+ .Add(ConvertNativeYtType(x["type"], false, hasYson, nativeYtTypeFlags)));
+ }
+ return NYT::TNode()
+ .Add("StructType")
+ .Add(list);
+ } else if (typeName == "variant") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ auto list = NYT::TNode::CreateList();
+ if (raw.HasKey("elements")) {
+ for (const auto& x : raw["elements"].AsList()) {
+ list.Add(ConvertNativeYtType(x["type"], false, hasYson, nativeYtTypeFlags));
+ }
+
+ return NYT::TNode()
+ .Add("VariantType")
+ .Add(NYT::TNode()
+ .Add("TupleType")
+ .Add(list));
+ } else {
+ for (const auto& x : raw["members"].AsList()) {
+ list.Add(NYT::TNode()
+ .Add(x["name"].AsString())
+ .Add(ConvertNativeYtType(x["type"], false, hasYson, nativeYtTypeFlags)));
+ }
+
+ return NYT::TNode()
+ .Add("VariantType")
+ .Add(NYT::TNode()
+ .Add("StructType")
+ .Add(list));
+ }
+ } else if (typeName == "tagged") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ auto tag = raw["tag"].AsString();
+ if (tag == "_EmptyList") {
+ return NYT::TNode().Add("EmptyListType");
+ }
+
+ if (tag == "_EmptyDict") {
+ return NYT::TNode().Add("EmptyDictType");
+ }
+
+ if (tag == "_Void") {
+ return NYT::TNode().Add("VoidType");
+ }
+
+ if (tag == "_Null") {
+ return NYT::TNode().Add("NullType");
+ }
+
+ return NYT::TNode()
+ .Add("TaggedType")
+ .Add(tag)
+ .Add(ConvertNativeYtType(raw["item"], false, hasYson, nativeYtTypeFlags));
+ } else if (typeName == "dict") {
+ nativeYtTypeFlags |= NTCF_COMPLEX;
+ return NYT::TNode()
+ .Add("DictType")
+ .Add(ConvertNativeYtType(raw["key"], false, hasYson, nativeYtTypeFlags))
+ .Add(ConvertNativeYtType(raw["value"], false, hasYson, nativeYtTypeFlags));
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Unknown metatype " << typeName.Quote() << " in yson schema";
+ }
+}
+
+static std::pair<TString, NYT::TNode> ExtractYtType(const NYT::TNode& entry, bool strictSchema,
+ THashSet<TString>& anyColumns, ui64& nativeYtTypeFlags) {
+ TMaybe<NYT::TNode> nativeTypeRaw;
+ TMaybe<TString> fieldName;
+ TMaybe<TString> fieldType;
+ bool nullable = true;
+ bool hasYson = false;
+
+ for (const auto& it : entry.AsMap()) {
+ if (it.first == "name") {
+ fieldName = it.second.AsString();
+ if (!strictSchema && *fieldName == YqlOthersColumnName) {
+ YQL_LOG_CTX_THROW yexception() << "Non-strict schema contains '_other' column, which conflicts with YQL virtual column";
+ }
+ }
+ else if (it.first == "type") {
+ fieldType = it.second.AsString();
+ hasYson = hasYson || ("any" == fieldType);
+ }
+ else if (it.first == "nullable") {
+ nullable = it.second.AsBool();
+ }
+ else if (it.first == "required") {
+ nullable = !it.second.AsBool();
+ }
+ else if (it.first == "type_v3") {
+ nativeTypeRaw = it.second;
+ }
+ }
+
+ if (!fieldName) {
+ YQL_LOG_CTX_THROW yexception() << "No 'name' in schema tuple";
+ }
+ if (!fieldType && !nativeTypeRaw) {
+ YQL_LOG_CTX_THROW yexception() << "No 'type' or 'type_v3' in schema tuple";
+ }
+
+ if (nativeTypeRaw) {
+ fieldType.Clear();
+ }
+
+ if (nativeTypeRaw && nativeTypeRaw->IsString()) {
+ fieldType = nativeTypeRaw->AsString();
+ }
+
+ NYT::TNode typeNode;
+ if (fieldType && fieldType != "null" && fieldType != "void" && fieldType != "decimal") {
+ TString yqlType = ConvertYtDataType(*fieldType, nativeYtTypeFlags);
+ auto dataTypeNode = NYT::TNode()
+ .Add("DataType")
+ .Add(yqlType);
+
+ typeNode = nullable ? NYT::TNode()
+ .Add("OptionalType")
+ .Add(dataTypeNode) : dataTypeNode;
+
+ } else {
+ typeNode = ConvertNativeYtType(nativeTypeRaw ? *nativeTypeRaw : NYT::TNode(fieldType.GetOrElse({})), true, hasYson, nativeYtTypeFlags);
+ }
+ if (hasYson) {
+ anyColumns.insert(*fieldName);
+ }
+ return { *fieldName, typeNode };
+}
+
+NYT::TNode YTSchemaToRowSpec(const NYT::TNode& schema, const TYTSortInfo* sortInfo) {
+ NYT::TNode rowSpec;
+ auto& rowType = rowSpec[RowSpecAttrType];
+ rowType.Add("StructType");
+ auto& resultTypes = rowType.Add();
+ resultTypes = NYT::TNode::CreateList();
+
+ const auto* strictAttr = schema.GetAttributes().AsMap().FindPtr("strict");
+ const bool strictSchema = !strictAttr || NYT::GetBool(*strictAttr);
+ rowSpec[RowSpecAttrStrictSchema] = static_cast<int>(strictSchema);
+
+ THashSet<TString> anyColumns;
+ NYT::TNode sortedBy = NYT::TNode::CreateList();
+ NYT::TNode sortedByTypes;
+ NYT::TNode sortDirections;
+ ui64 nativeYtTypeFlags = 0;
+
+ THashMap<TString, NYT::TNode> types;
+ for (const auto& entry : schema.AsList()) {
+ if (!entry.IsMap()) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid table schema: list element is not a map node";
+ }
+
+ auto ft = ExtractYtType(entry, strictSchema, anyColumns, nativeYtTypeFlags);
+ types[ft.first] = ft.second;
+ }
+ rowSpec[RowSpecAttrNativeYtTypeFlags] = nativeYtTypeFlags;
+
+ TYTSortInfo localSortInfo;
+ if (!sortInfo) {
+ localSortInfo = KeyColumnsFromSchema(schema);
+ sortInfo = &localSortInfo;
+ }
+ bool uniqueKeys = sortInfo->Unique;
+ for (const auto& field : sortInfo->Keys) {
+ const auto& fieldName = field.first;
+ auto* type = types.FindPtr(fieldName);
+ if (!type) {
+ uniqueKeys = false;
+ break;
+ }
+ sortedBy.Add(fieldName);
+ sortedByTypes.Add(*type);
+ sortDirections.Add(field.second);
+ }
+
+ for (const auto& entry : schema.AsList()) {
+ for (const auto& it : entry.AsMap()) {
+ if (it.first != "name") {
+ continue;
+ }
+ const auto& fieldName = it.second.AsString();
+ auto* type = types.FindPtr(fieldName);
+ if (!type) {
+ continue;
+ }
+ resultTypes.Add(NYT::TNode()
+ .Add(fieldName)
+ .Add(*type));
+ }
+ }
+
+ if (!sortedBy.Empty()) {
+ bool hasAnyInKey = false;
+ for (const auto& key : sortedBy.AsList()) {
+ if (anyColumns.contains(key.AsString())) {
+ hasAnyInKey = true;
+ break;
+ }
+ }
+
+ if (!hasAnyInKey) {
+ rowSpec[RowSpecAttrSortedBy] = sortedBy;
+ rowSpec[RowSpecAttrSortedByTypes] = sortedByTypes;
+ rowSpec[RowSpecAttrSortDirections] = sortDirections;
+ rowSpec[RowSpecAttrSortMembers] = sortedBy;
+ if (uniqueKeys) {
+ rowSpec[RowSpecAttrUniqueKeys] = uniqueKeys;
+ }
+ }
+ }
+
+ return rowSpec;
+}
+
+NYT::TNode QB2PremapperToRowSpec(const NYT::TNode& qb2, const NYT::TNode& originalScheme) {
+ NYT::TNode rowSpec;
+ auto& rowType = rowSpec[RowSpecAttrType];
+ rowType.Add("StructType");
+ auto& resultTypes = rowType.Add();
+
+ rowSpec[RowSpecAttrStrictSchema] = 1;
+
+ THashSet<TString> anyColumns;
+ THashMap<TString, NYT::TNode> types;
+ THashSet<TString> passthroughFields;
+ for (const auto& field: qb2["fields"].AsMap()) {
+ auto ytType = field.second["type"].AsString();
+ const auto& reqColumns = field.second["required_columns"].AsList();
+
+ bool isNullable = true;
+ if (field.second.HasKey("nullable")) {
+ isNullable = field.second["nullable"].AsBool();
+ }
+
+ bool passthrough = false;
+ if (field.second.HasKey("passthrough")) {
+ passthrough = field.second["passthrough"].AsBool();
+ }
+
+ // check real field name
+ if (passthrough && (reqColumns.size() != 1 || reqColumns[0].AsString() != field.first)) {
+ passthrough = false;
+ }
+
+ if (passthrough) {
+ passthroughFields.insert(field.first);
+ }
+
+ if ("yson" == ytType || "any" == ytType) {
+ anyColumns.insert(field.first);
+ }
+
+ ui64 nativeYtTypeFlags = 0;
+ TString yqlType = ConvertYtDataType(ytType, nativeYtTypeFlags);
+ YQL_ENSURE(0 == nativeYtTypeFlags, "QB2 premapper with native YT types is not supported");
+ auto dataTypeNode = NYT::TNode()
+ .Add("DataType")
+ .Add(yqlType);
+
+ auto typeNode = isNullable ? NYT::TNode()
+ .Add("OptionalType")
+ .Add(dataTypeNode) : dataTypeNode;
+
+ types[field.first] = typeNode;
+ }
+
+ auto sortInfo = KeyColumnsFromSchema(originalScheme);
+
+ // switch off sort if some key isn't marked as passtrough or has 'any' type
+ for (auto& x: sortInfo.Keys) {
+ if (!passthroughFields.contains(x.first) || anyColumns.contains(x.first)) {
+ sortInfo.Keys.clear();
+ break;
+ }
+ }
+
+ NYT::TNode sortedBy = NYT::TNode::CreateList();
+ NYT::TNode sortedByTypes;
+ NYT::TNode sortDirections;
+
+ for (const auto& field : sortInfo.Keys) {
+ const auto& fieldName = field.first;
+ auto* type = types.FindPtr(fieldName);
+ if (!type) {
+ sortInfo.Unique = false;
+ break;
+ }
+ resultTypes.Add(NYT::TNode()
+ .Add(fieldName)
+ .Add(*type));
+
+ sortedBy.Add(fieldName);
+ sortedByTypes.Add(*type);
+ sortDirections.Add(field.second);
+
+ types.erase(fieldName);
+ }
+
+ for (auto entry: types) {
+ resultTypes.Add(NYT::TNode()
+ .Add(entry.first)
+ .Add(entry.second));
+ }
+
+ if (!sortedBy.Empty()) {
+ rowSpec[RowSpecAttrSortedBy] = sortedBy;
+ rowSpec[RowSpecAttrSortedByTypes] = sortedByTypes;
+ rowSpec[RowSpecAttrSortDirections] = sortDirections;
+ rowSpec[RowSpecAttrSortMembers] = sortedBy;
+ if (sortInfo.Unique) {
+ rowSpec[RowSpecAttrUniqueKeys] = sortInfo.Unique;
+ }
+ }
+
+ return rowSpec;
+}
+
+NYT::TNode GetSchemaFromAttributes(const NYT::TNode& attributes, bool onlySystem, bool ignoreWeakSchema) {
+ NYT::TNode result;
+ auto trySchema = [&] (const TStringBuf& attrName) -> TMaybe<NYT::TNode> {
+ if (attributes.HasKey(attrName)) {
+ const auto& attr = attributes[attrName];
+ if (attr.IsList() && !attr.Empty()) {
+ return attr;
+ }
+ }
+ return Nothing();
+ };
+
+ auto readSchema = !onlySystem ? trySchema(READ_SCHEMA_ATTR_NAME) : Nothing();
+ auto schema = trySchema(SCHEMA_ATTR_NAME);
+
+ if (readSchema) {
+ result[READ_SCHEMA_ATTR_NAME] = *readSchema;
+ }
+
+ if (schema) {
+ if (!ignoreWeakSchema || !attributes.HasKey(SCHEMA_MODE_ATTR_NAME) ||
+ attributes[SCHEMA_MODE_ATTR_NAME].AsString() != TStringBuf("weak")) {
+ result[SCHEMA_ATTR_NAME] = *schema;
+ }
+ }
+ return result;
+}
+
+TYTSortInfo KeyColumnsFromSchema(const NYT::TNode& schema) {
+ TYTSortInfo result;
+ for (const auto& entry : schema.AsList()) {
+ if (!entry.IsMap()) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid schema: list element is not a map node";
+ }
+
+ TMaybe<TString> fieldName;
+ TMaybe<int> sortDirection;
+
+ for (const auto& it : entry.AsMap()) {
+ if (it.first == "name") {
+ fieldName = it.second.AsString();
+ } else if (it.first == "sort_order") {
+ if (!it.second.IsEntity()) {
+ if (!it.second.IsString()) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid schema sort order: " << NYT::NodeToYsonString(entry);
+ }
+ if (it.second == "ascending") {
+ sortDirection = 1;
+ } else if (it.second == "descending") {
+ sortDirection = 0;
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Invalid schema sort order: " << NYT::NodeToYsonString(entry);
+ }
+ }
+ }
+ }
+
+ if (fieldName && sortDirection) {
+ result.Keys.emplace_back(*fieldName, *sortDirection);
+ }
+ }
+ result.Unique = schema.GetAttributes().HasKey("unique_keys") && NYT::GetBool(schema.GetAttributes()["unique_keys"]);
+ return result;
+}
+
+bool ValidateTableSchema(
+ const TString& tableName, const NYT::TNode& attributes,
+ bool ignoreYamrDsv, bool ignoreWeakSchema
+) {
+ if (attributes.HasKey(YqlRowSpecAttribute)) {
+ return true;
+ }
+
+ auto hasYTSchema = [&] (const TStringBuf& attrName) {
+ if (attributes.HasKey(attrName)) {
+ const auto& schema = attributes[attrName];
+ if (!schema.IsList()) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid schema, " <<
+ tableName << "/@" << attrName << " is not a list node";
+ }
+ return !schema.Empty();
+ }
+ return false;
+ };
+
+ if (hasYTSchema(READ_SCHEMA_ATTR_NAME)) {
+ return true;
+ }
+
+ if (hasYTSchema(SCHEMA_ATTR_NAME) && !(
+ ignoreWeakSchema &&
+ attributes.HasKey(SCHEMA_MODE_ATTR_NAME) &&
+ attributes[SCHEMA_MODE_ATTR_NAME].AsString() == TStringBuf("weak")))
+ {
+ return true;
+ }
+
+ if (!ignoreYamrDsv && attributes.HasKey(FORMAT_ATTR_NAME) &&
+ attributes[FORMAT_ATTR_NAME].AsString() == TStringBuf("yamred_dsv"))
+ {
+ return true;
+ }
+
+ return false;
+}
+
+void MergeInferredSchemeWithSort(NYT::TNode& schema, TYTSortInfo& sortInfo) {
+ if (sortInfo.Keys.empty()) {
+ return;
+ }
+
+ TSet<TString> keys;
+ for (const auto& x : sortInfo.Keys) {
+ keys.insert(x.first);
+ }
+
+ TSet<TString> unusedKeys = keys;
+ for (const auto& column : schema.AsList()) {
+ const auto& name = column["name"].AsString();
+ unusedKeys.erase(name);
+ }
+
+ // add all unused keys with yson type, update sortness
+ for (const auto& key : unusedKeys) {
+ auto map = NYT::TNode::CreateMap();
+ map["name"] = key;
+ map["type"] = "any";
+ schema.Add(map);
+ }
+
+ for (ui32 i = 0; i < sortInfo.Keys.size(); ++i) {
+ if (unusedKeys.contains(sortInfo.Keys[i].first)) {
+ sortInfo.Keys.erase(sortInfo.Keys.begin() + i, sortInfo.Keys.end());
+ sortInfo.Unique = false;
+ break;
+ }
+ }
+}
+
+std::pair<NYT::EValueType, bool> RowSpecYqlTypeToYtType(const NYT::TNode& rowSpecType, ui64 nativeYtTypeFlags) {
+ const auto* type = &rowSpecType;
+
+ while ((*type)[0] == TStringBuf("TaggedType")) {
+ type = &(*type)[2];
+ }
+
+ if ((*type)[0] == TStringBuf("PgType")) {
+ const auto& name = (*type)[1].AsString();
+ NYT::EValueType ytType;
+ if (name == "bool") {
+ ytType = NYT::VT_BOOLEAN;
+ } else if (name == "int2") {
+ ytType = NYT::VT_INT16;
+ } else if (name == "int4") {
+ ytType = NYT::VT_INT32;
+ } else if (name == "int8") {
+ ytType = NYT::VT_INT64;
+ } else if (name == "float8") {
+ ytType = NYT::VT_DOUBLE;
+ } else if (name == "float4") {
+ ytType = (nativeYtTypeFlags & NTCF_FLOAT) ? NYT::VT_FLOAT : NYT::VT_DOUBLE;
+ } else if (name == "text" || name == "varchar" || name == "cstring") {
+ ytType = NYT::VT_UTF8;
+ } else {
+ ytType = NYT::VT_STRING;
+ }
+
+ return { ytType, false };
+ }
+
+ bool required = true;
+ if ((*type)[0] == TStringBuf("OptionalType")) {
+ type = &(*type)[1];
+ required = false;
+ }
+
+ if ((*type)[0] == TStringBuf("NullType")) {
+ return {NYT::VT_NULL, false};
+ }
+
+ if ((*type)[0] == TStringBuf("VoidType")) {
+ return {NYT::VT_VOID, false};
+ }
+
+ if ((*type)[0] != TStringBuf("DataType")) {
+ return {NYT::VT_ANY, false};
+ }
+
+ const auto& yqlType = (*type)[1].AsString();
+ NYT::EValueType ytType;
+ if (yqlType == TStringBuf("String") || yqlType == TStringBuf("Longint") || yqlType == TStringBuf("Uuid") || yqlType == TStringBuf("JsonDocument") || yqlType == TStringBuf("DyNumber")) {
+ ytType = NYT::VT_STRING;
+ } else if (yqlType == TStringBuf("Json")) {
+ ytType = (nativeYtTypeFlags & NTCF_JSON) ? NYT::VT_JSON : NYT::VT_STRING;
+ } else if (yqlType == TStringBuf("Decimal")) {
+ ytType = NYT::VT_STRING;
+ } else if (yqlType == TStringBuf("Utf8")) {
+ ytType = NYT::VT_UTF8;
+ } else if (yqlType == TStringBuf("Int64")) {
+ ytType = NYT::VT_INT64;
+ } else if (yqlType == TStringBuf("Interval")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? NYT::VT_INTERVAL : NYT::VT_INT64;
+ } else if (yqlType == TStringBuf("Int32")) {
+ ytType = NYT::VT_INT32;
+ } else if (yqlType == TStringBuf("Int16")) {
+ ytType = NYT::VT_INT16;
+ } else if (yqlType == TStringBuf("Int8")) {
+ ytType = NYT::VT_INT8;
+ } else if (yqlType == TStringBuf("Uint64")) {
+ ytType = NYT::VT_UINT64;
+ } else if (yqlType == TStringBuf("Timestamp")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? NYT::VT_TIMESTAMP : NYT::VT_UINT64;
+ } else if (yqlType == TStringBuf("Uint32")) {
+ ytType = NYT::VT_UINT32;
+ } else if (yqlType == TStringBuf("Datetime")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? NYT::VT_DATETIME : NYT::VT_UINT32;
+ } else if (yqlType == TStringBuf("Uint16")) {
+ ytType = NYT::VT_UINT16;
+ } else if (yqlType == TStringBuf("Date")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? NYT::VT_DATE : NYT::VT_UINT16;
+ } else if (yqlType == TStringBuf("Uint8")) {
+ ytType = NYT::VT_UINT8;
+ } else if (yqlType == TStringBuf("Float")) {
+ ytType = (nativeYtTypeFlags & NTCF_FLOAT) ? NYT::VT_FLOAT : NYT::VT_DOUBLE;
+ } else if (yqlType == TStringBuf("Double")) {
+ ytType = NYT::VT_DOUBLE;
+ } else if (yqlType == TStringBuf("Bool")) {
+ ytType = NYT::VT_BOOLEAN;
+ } else if (yqlType == TStringBuf("Yson")) {
+ ytType = NYT::VT_ANY;
+ required = false;
+ } else if (yqlType == TStringBuf("TzDate") || yqlType == TStringBuf("TzDatetime") || yqlType == TStringBuf("TzTimestamp")) {
+ ytType = NYT::VT_STRING;
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Unknown type " << yqlType.Quote() << " in row spec";
+ }
+
+ return { ytType, required };
+}
+
+NYT::TNode RowSpecYqlTypeToYtNativeType(const NYT::TNode& rowSpecType, ui64 nativeYtTypeFlags) {
+ const auto* type = &rowSpecType;
+
+ if ((*type)[0] == TStringBuf("DataType")) {
+ const auto& yqlType = (*type)[1].AsString();
+ TString ytType;
+ if (yqlType == TStringBuf("String") || yqlType == TStringBuf("Longint") || yqlType == TStringBuf("Uuid") || yqlType == TStringBuf("JsonDocument") || yqlType == TStringBuf("DyNumber")) {
+ ytType = "string";
+ } else if (yqlType == TStringBuf("Json")) {
+ ytType = (nativeYtTypeFlags & NTCF_JSON) ? "json" : "string";
+ } else if (yqlType == TStringBuf("Utf8")) {
+ ytType = "utf8";
+ } else if (yqlType == TStringBuf("Int64")) {
+ ytType = "int64";
+ } else if (yqlType == TStringBuf("Int32")) {
+ ytType = "int32";
+ } else if (yqlType == TStringBuf("Int16")) {
+ ytType = "int16";
+ } else if (yqlType == TStringBuf("Int8")) {
+ ytType = "int8";
+ } else if (yqlType == TStringBuf("Uint64")) {
+ ytType = "uint64";
+ } else if (yqlType == TStringBuf("Uint32")) {
+ ytType = "uint32";
+ } else if (yqlType == TStringBuf("Uint16")) {
+ ytType = "uint16";
+ } else if (yqlType == TStringBuf("Uint8")) {
+ ytType = "uint8";
+ } else if (yqlType == TStringBuf("Double")) {
+ ytType = "double";
+ } else if (yqlType == TStringBuf("Float")) {
+ ytType = (nativeYtTypeFlags & NTCF_FLOAT) ? "float" : "double";
+ } else if (yqlType == TStringBuf("Bool")) {
+ ytType = "bool";
+ } else if (yqlType == TStringBuf("Yson")) {
+ ytType = "yson";
+ } else if (yqlType == TStringBuf("TzDate") || yqlType == TStringBuf("TzDatetime") || yqlType == TStringBuf("TzTimestamp")) {
+ ytType = "string";
+ } else if (yqlType == TStringBuf("Date")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? "date" : "uint16";
+ } else if (yqlType == TStringBuf("Datetime")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? "datetime" : "uint32";
+ } else if (yqlType == TStringBuf("Timestamp")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? "timestamp" : "uint64";
+ } else if (yqlType == TStringBuf("Interval")) {
+ ytType = (nativeYtTypeFlags & NTCF_DATE) ? "interval" : "int64";
+ } else if (yqlType == TStringBuf("Decimal")) {
+ if (nativeYtTypeFlags & NTCF_DECIMAL) {
+ try {
+ return NYT::TNode::CreateMap()
+ ("type_name", "decimal")
+ ("precision", FromString<int>((*type)[2].AsString()))
+ ("scale", FromString<int>((*type)[3].AsString()))
+ ;
+ } catch (...) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid Decimal type in row spec: " << CurrentExceptionMessage();
+ }
+ } else {
+ ytType = "string";
+ }
+ } else {
+ YQL_LOG_CTX_THROW yexception() << "Not supported data type: " << yqlType;
+ }
+
+ return NYT::TNode(ytType);
+ } else if ((*type)[0] == TStringBuf("OptionalType")) {
+ return NYT::TNode::CreateMap()
+ ("type_name", "optional")
+ ("item", RowSpecYqlTypeToYtNativeType((*type)[1], nativeYtTypeFlags))
+ ;
+ } else if ((*type)[0] == TStringBuf("ListType")) {
+ return NYT::TNode::CreateMap()
+ ("type_name", "list")
+ ("item", RowSpecYqlTypeToYtNativeType((*type)[1], nativeYtTypeFlags))
+ ;
+ } else if ((*type)[0] == TStringBuf("TupleType")) {
+ auto elements = NYT::TNode::CreateList();
+ for (const auto& x : (*type)[1].AsList()) {
+ elements.Add(NYT::TNode::CreateMap()("type", RowSpecYqlTypeToYtNativeType(x, nativeYtTypeFlags)));
+ }
+
+ return NYT::TNode::CreateMap()
+ ("type_name", "tuple")
+ ("elements", elements)
+ ;
+ } else if ((*type)[0] == TStringBuf("StructType")) {
+ auto members = NYT::TNode::CreateList();
+ for (const auto& x : (*type)[1].AsList()) {
+ members.Add(NYT::TNode::CreateMap()
+ ("name", x[0].AsString())
+ ("type", RowSpecYqlTypeToYtNativeType(x[1], nativeYtTypeFlags))
+ );
+ }
+
+ return NYT::TNode::CreateMap()
+ ("type_name", "struct")
+ ("members", members)
+ ;
+ } else if ((*type)[0] == TStringBuf("VariantType")) {
+ auto base = (*type)[1];
+ if (base[0] == TStringBuf("TupleType")) {
+ auto elements = NYT::TNode::CreateList();
+ for (const auto& x : base[1].AsList()) {
+ elements.Add(NYT::TNode::CreateMap()("type", RowSpecYqlTypeToYtNativeType(x, nativeYtTypeFlags)));
+ }
+
+ return NYT::TNode::CreateMap()
+ ("type_name", "variant")
+ ("elements", elements)
+ ;
+
+ } else if (base[0] == TStringBuf("StructType")) {
+ auto members = NYT::TNode::CreateList();
+ for (const auto& x : base[1].AsList()) {
+ members.Add(NYT::TNode::CreateMap()
+ ("name", x[0].AsString())
+ ("type", RowSpecYqlTypeToYtNativeType(x[1], nativeYtTypeFlags))
+ );
+ }
+
+ return NYT::TNode::CreateMap()
+ ("type_name", "variant")
+ ("members", members)
+ ;
+ } else {
+ YQL_ENSURE(false, "Not supported variant base type: " << base[0].AsString());
+ }
+ } else if ((*type)[0] == TStringBuf("VoidType")) {
+ if (nativeYtTypeFlags & NTCF_VOID) {
+ return NYT::TNode("void");
+ }
+ return NYT::TNode::CreateMap({
+ {"type_name", "tagged"},
+ {"tag", "_Void"},
+ {"item", "null"}
+ });
+
+ } else if ((*type)[0] == TStringBuf("EmptyListType")) {
+ return NYT::TNode::CreateMap({
+ {"type_name", "tagged"},
+ {"tag", "_EmptyList"},
+ {"item", "null"}
+ });
+ } else if ((*type)[0] == TStringBuf("EmptyDictType")) {
+ return NYT::TNode::CreateMap({
+ {"type_name", "tagged"},
+ {"tag", "_EmptyDict"},
+ {"item", "null"}
+ });
+ } else if ((*type)[0] == TStringBuf("NullType")) {
+ if (nativeYtTypeFlags & NTCF_NULL) {
+ return NYT::TNode("null");
+ }
+ return NYT::TNode::CreateMap({
+ {"type_name", "tagged"},
+ {"tag", "_Null"},
+ {"item", "null"}
+ });
+ } else if ((*type)[0] == TStringBuf("TaggedType")) {
+ return NYT::TNode::CreateMap()
+ ("type_name", "tagged")
+ ("tag", (*type)[1])
+ ("item", RowSpecYqlTypeToYtNativeType((*type)[2], nativeYtTypeFlags))
+ ;
+ } else if ((*type)[0] == TStringBuf("DictType")) {
+ return NYT::TNode::CreateMap()
+ ("type_name", "dict")
+ ("key", RowSpecYqlTypeToYtNativeType((*type)[1], nativeYtTypeFlags))
+ ("value", RowSpecYqlTypeToYtNativeType((*type)[2], nativeYtTypeFlags))
+ ;
+ } else if ((*type)[0] == TStringBuf("PgType")) {
+ const auto& name = (*type)[1].AsString();
+ TString ytType;
+ if (name == "bool") {
+ ytType = "bool";
+ } else if (name == "int2") {
+ ytType = "int16";
+ } else if (name == "int4") {
+ ytType = "int32";
+ } else if (name == "int8") {
+ ytType = "int64";
+ } else if (name == "float8") {
+ ytType = "double";;
+ } else if (name == "float4") {
+ ytType = (nativeYtTypeFlags & NTCF_FLOAT) ? "float" : "double";
+ } else if (name == "text" || name == "varchar" || name == "cstring") {
+ ytType = "utf8";
+ } else {
+ ytType = "string";
+ }
+
+ return NYT::TNode::CreateMap()
+ ("type_name", "optional")
+ ("item", NYT::TNode(ytType));
+ }
+
+ YQL_ENSURE(false, "Not supported type: " << (*type)[0].AsString());
+}
+
+NYT::TTableSchema RowSpecToYTSchema(const NYT::TNode& rowSpec, ui64 nativeTypeCompatibility) {
+ NYT::TTableSchema schema;
+ const auto& rowSpecMap = rowSpec.AsMap();
+
+ const auto* rootType = rowSpecMap.FindPtr(RowSpecAttrType);
+ if (!rootType || !rootType->IsList()) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid Type in row spec";
+ }
+
+ ui64 nativeYtTypeFlags = 0;
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseNativeYtTypes]) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseTypeV2]) ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+ nativeYtTypeFlags &= nativeTypeCompatibility;
+ bool useNativeTypes = nativeYtTypeFlags & (NTCF_COMPLEX | NTCF_DECIMAL);
+
+ THashMap<TString, std::pair<NYT::EValueType, bool>> fieldTypes;
+ THashMap<TString, NYT::TNode> fieldNativeTypes;
+ TVector<TString> columns;
+
+ for (const auto& entry : rootType->AsList()[1].AsList()) {
+ TString column = entry[0].AsString();
+ if (useNativeTypes) {
+ fieldNativeTypes[column] = RowSpecYqlTypeToYtNativeType(entry[1], nativeYtTypeFlags);
+ } else {
+ fieldTypes[column] = RowSpecYqlTypeToYtType(entry[1], nativeYtTypeFlags);
+ }
+ columns.push_back(column);
+ }
+
+ const auto* sortedBy = rowSpecMap.FindPtr(RowSpecAttrSortedBy);
+ const auto* sortedByTypes = rowSpecMap.FindPtr(RowSpecAttrSortedByTypes);
+ const auto* sortDirections = rowSpecMap.FindPtr(RowSpecAttrSortDirections);
+
+ THashSet<TString> keyColumns;
+ if (sortedBy && sortedByTypes) {
+ auto sortedByType = sortedByTypes->AsList().begin();
+ auto sortDir = sortDirections->AsList().begin();
+ for (const auto& column : sortedBy->AsList()) {
+ const auto& columnString = column.AsString();
+
+ keyColumns.insert(columnString);
+
+ auto columnNode = NYT::TColumnSchema()
+ .Name(columnString);
+
+ bool auxField = false;
+ if (useNativeTypes) {
+ auto ytType = RowSpecYqlTypeToYtNativeType(*sortedByType, nativeYtTypeFlags);
+ columnNode.RawTypeV3(ytType);
+ auxField = 0 == fieldNativeTypes.erase(columnString);
+ } else {
+ auto ytType = RowSpecYqlTypeToYtType(*sortedByType, nativeYtTypeFlags);
+ columnNode.Type(ytType.first, /*required*/ ytType.second);
+ auxField = 0 == fieldTypes.erase(columnString);
+ }
+
+ columnNode.SortOrder(sortDir->AsInt64() || auxField ? NYT::SO_ASCENDING : NYT::SO_DESCENDING);
+ schema.AddColumn(columnNode);
+ ++sortedByType;
+ ++sortDir;
+ }
+ }
+
+ for (const auto& column : columns) {
+ if (keyColumns.contains(column)) {
+ continue;
+ }
+ if (useNativeTypes) {
+ auto field = fieldNativeTypes.find(column);
+ YQL_ENSURE(field != fieldNativeTypes.end());
+ schema.AddColumn(NYT::TColumnSchema()
+ .Name(field->first)
+ .RawTypeV3(field->second));
+ } else {
+ auto field = fieldTypes.find(column);
+ YQL_ENSURE(field != fieldTypes.end());
+ schema.AddColumn(NYT::TColumnSchema()
+ .Name(field->first)
+ .Type(field->second.first, /*required*/ field->second.second));
+ }
+ }
+
+ // add fake column to avoid slow 0-columns YT schema
+ if (schema.Columns().empty()) {
+ schema.AddColumn(NYT::TColumnSchema()
+ .Name("_yql_fake_column")
+ .Type(NYT::EValueType::VT_BOOLEAN, /*required*/ false));
+ }
+
+ if (rowSpec.HasKey(RowSpecAttrUniqueKeys)) {
+ schema.UniqueKeys(NYT::GetBool(rowSpec[RowSpecAttrUniqueKeys]));
+ }
+
+ return schema;
+}
+
+NYT::TSortColumns ToYTSortColumns(const TVector<std::pair<TString, bool>>& sortColumns) {
+ NYT::TSortColumns res;
+ for (auto& item: sortColumns) {
+ res.Add(NYT::TSortColumn().Name(item.first).SortOrder(item.second ? NYT::ESortOrder::SO_ASCENDING : NYT::ESortOrder::SO_DESCENDING));
+ }
+ return res;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/schema/schema.h b/ydb/library/yql/providers/yt/lib/schema/schema.h
new file mode 100644
index 0000000000..694533ea69
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/schema.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+
+#include <utility>
+
+namespace NYql {
+
+struct TYTSortInfo {
+ TVector<std::pair<TString, int>> Keys;
+ bool Unique = false;
+};
+
+
+NYT::TNode YTSchemaToRowSpec(const NYT::TNode& schema, const TYTSortInfo* sortInfo = nullptr);
+NYT::TNode QB2PremapperToRowSpec(const NYT::TNode& qb2, const NYT::TNode& originalScheme);
+NYT::TNode GetSchemaFromAttributes(const NYT::TNode& attributes, bool onlySystem = false, bool ignoreWeakSchema = false);
+TYTSortInfo KeyColumnsFromSchema(const NYT::TNode& schema);
+bool ValidateTableSchema(const TString& tableName, const NYT::TNode& attributes, bool ignoreYamrDsv, bool ignoreWeakSchema = false);
+void MergeInferredSchemeWithSort(NYT::TNode& schema, TYTSortInfo& sortInfo);
+NYT::TTableSchema RowSpecToYTSchema(const NYT::TNode& rowSpec, ui64 nativeTypeCompatibility);
+NYT::TSortColumns ToYTSortColumns(const TVector<std::pair<TString, bool>>& sortColumns);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/schema/ya.make b/ydb/library/yql/providers/yt/lib/schema/ya.make
new file mode 100644
index 0000000000..f7fd644fe8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/schema/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+SRCS(
+ schema.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/yt/common
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/skiff/ya.make b/ydb/library/yql/providers/yt/lib/skiff/ya.make
new file mode 100644
index 0000000000..db747863de
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+SRCS(
+ yql_skiff_schema.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/schema/skiff
+ ydb/library/yql/utils
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp b/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
new file mode 100644
index 0000000000..945920a4da
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.cpp
@@ -0,0 +1,384 @@
+#include "yql_skiff_schema.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/schema/skiff/yql_skiff_schema.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/hash_set.h>
+#include <util/generic/xrange.h>
+#include <util/generic/map.h>
+
+
+namespace NYql {
+
+namespace {
+
+NYT::TNode RowSpecToInputSkiff(const NYT::TNode& attrs, const THashMap<TString, ui32>& structColumns,
+ const THashSet<TString>& auxColumns, bool rowIndex, bool rangeIndex, bool keySwitch)
+{
+
+ YQL_ENSURE(attrs.HasKey(YqlRowSpecAttribute), "Missing mandatory "
+ << TString{YqlRowSpecAttribute}.Quote() << " attribute");
+
+ const auto& rowSpec = attrs[YqlRowSpecAttribute];
+
+ ui64 nativeYtTypeFlags = 0;
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseNativeYtTypes]) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseTypeV2]) ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+
+ THashSet<TString> fieldsWithDefValue;
+ if (rowSpec.HasKey(RowSpecAttrDefaultValues)) {
+ for (auto& value : rowSpec[RowSpecAttrDefaultValues].AsMap()) {
+ fieldsWithDefValue.insert(value.first);
+ }
+ }
+
+ THashSet<TString> fieldsWithExplicitYson;
+ if (rowSpec.HasKey(RowSpecAttrExplicitYson)) {
+ for (auto& value : rowSpec[RowSpecAttrExplicitYson].AsList()) {
+ fieldsWithExplicitYson.emplace(value.AsString());
+ }
+ }
+
+ auto typeNode = NCommon::ParseSkiffTypeFromYson(rowSpec[RowSpecAttrType], nativeYtTypeFlags);
+ TMap<TString, NYT::TNode> typeColumns; // Must be ordered
+ const auto optType = NYT::TNode("variant8");
+ for (auto item: typeNode["children"].AsList()) {
+ auto name = item["name"].AsString();
+ if (fieldsWithExplicitYson.contains(name)) {
+ item = NYT::TNode()
+ ("name", name)
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(NYT::TNode()("wire_type", "yson32"))
+ );
+
+ } else if (fieldsWithDefValue.contains(name) && item["wire_type"] != optType) {
+ item = NYT::TNode()
+ ("name", name)
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(std::move(item))
+ );
+ }
+ YQL_ENSURE(typeColumns.emplace(name, std::move(item)).second, "Duplicate struct column: " << name);
+ }
+
+ THashMap<TString, NYT::TNode> sortAuxColumns;
+ if (rowSpec.HasKey(RowSpecAttrSortedBy)) {
+ auto& sortedBy = rowSpec[RowSpecAttrSortedBy].AsList();
+ auto& sortedByType = rowSpec[RowSpecAttrSortedByTypes].AsList();
+ auto& sortedDirections = rowSpec[RowSpecAttrSortDirections].AsList();
+ for (size_t i: xrange(sortedBy.size())) {
+ auto name = sortedBy[i].AsString();
+ if (!typeColumns.contains(name)) {
+ NYT::TNode fieldType;
+ if (!sortedDirections.empty() && !sortedDirections[i].AsInt64()) {
+ fieldType = NYT::TNode()("wire_type", "string32");
+ } else {
+ fieldType = NCommon::ParseSkiffTypeFromYson(sortedByType[i], nativeYtTypeFlags);
+ }
+ fieldType["name"] = name;
+ sortAuxColumns.emplace(name, std::move(fieldType));
+ }
+ }
+ }
+
+ bool strictSchema = true;
+ if (rowSpec.HasKey(RowSpecAttrStrictSchema)) {
+ strictSchema = rowSpec[RowSpecAttrStrictSchema].IsInt64()
+ ? rowSpec[RowSpecAttrStrictSchema].AsInt64() != 0
+ : NYT::GetBool(rowSpec[RowSpecAttrStrictSchema]);
+ }
+
+ if (!strictSchema) {
+ if (rowSpec.HasKey(RowSpecAttrWeakFields)) {
+ for (auto& field: rowSpec[RowSpecAttrWeakFields].AsList()) {
+ if (!typeColumns.contains(field.AsString())) {
+ auto column = NYT::TNode()
+ ("name", field)
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(NYT::TNode()("wire_type", "yson32"))
+ );
+
+ typeColumns.emplace(field.AsString(), std::move(column));
+ }
+ }
+ }
+ }
+
+ NYT::TNode tableSchema;
+ tableSchema["wire_type"] = "tuple";
+ auto& tableColumns = tableSchema["children"];
+ tableColumns = NYT::TNode::CreateList();
+
+ if (rangeIndex) {
+ tableColumns.Add(NYT::TNode()
+ ("name", "$range_index")
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(NYT::TNode()("wire_type", "int64"))
+ )
+ );
+ }
+
+ const bool dynamic = attrs.HasKey(YqlDynamicAttribute) && attrs[YqlDynamicAttribute].AsBool();
+
+ if (!dynamic && rowIndex) {
+ tableColumns.Add(NYT::TNode()
+ ("name", "$row_index")
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(NYT::TNode()("wire_type", "int64"))
+ )
+ );
+ }
+
+ if (keySwitch) {
+ tableColumns.Add(NYT::TNode()
+ ("name", "$key_switch")
+ ("wire_type", "boolean")
+ );
+ }
+
+ TMap<ui32, NYT::TNode> columns;
+ const bool hasOthers = !strictSchema && structColumns.contains(YqlOthersColumnName);
+ for (auto& item: typeColumns) {
+ if (auto p = structColumns.FindPtr(item.first)) {
+ ui32 pos = *p;
+ YQL_ENSURE(columns.emplace(pos, item.second).second, "Reused column position");
+ }
+ }
+ for (auto& item: sortAuxColumns) {
+ if (auto p = structColumns.FindPtr(item.first)) {
+ ui32 pos = *p;
+ YQL_ENSURE(columns.emplace(pos, item.second).second, "Reused column position");
+ }
+ }
+ for (auto& item: columns) {
+ tableColumns.Add(item.second);
+ }
+
+ if (hasOthers || !auxColumns.empty()) {
+ for (auto& item: typeColumns) {
+ if (!structColumns.contains(item.first)) {
+ if (hasOthers || auxColumns.contains(item.first)) {
+ tableColumns.Add(item.second);
+ }
+ }
+ }
+ if (hasOthers) {
+ tableColumns.Add(NYT::TNode()
+ ("name", "$other_columns")
+ ("wire_type", "yson32")
+ );
+ }
+ }
+ return tableSchema;
+}
+
+NYT::TNode RowSpecToOutputSkiff(const NYT::TNode& attrs) {
+ YQL_ENSURE(attrs.HasKey(YqlRowSpecAttribute), "Missing mandatory "
+ << TString{YqlRowSpecAttribute}.Quote() << " attribute");
+
+ const auto& rowSpec = attrs[YqlRowSpecAttribute];
+ ui64 nativeYtTypeFlags = 0;
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseNativeYtTypes]) ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ nativeYtTypeFlags = NYT::GetBool(rowSpec[RowSpecAttrUseTypeV2]) ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+
+ auto typeNode = NCommon::ParseSkiffTypeFromYson(rowSpec[RowSpecAttrType], nativeYtTypeFlags);
+ TMap<TString, NYT::TNode> typeColumns; // Must be ordered
+ for (auto item: typeNode["children"].AsList()) {
+ auto name = item["name"].AsString();
+ YQL_ENSURE(typeColumns.emplace(name, std::move(item)).second, "Duplicate struct column: " << name);
+ }
+
+ if (rowSpec.HasKey(RowSpecAttrSortedBy)) {
+ auto& sortedBy = rowSpec[RowSpecAttrSortedBy].AsList();
+ auto& sortedByType = rowSpec[RowSpecAttrSortedByTypes].AsList();
+ auto& sortedDirections = rowSpec[RowSpecAttrSortDirections].AsList();
+ for (size_t i: xrange(sortedBy.size())) {
+ auto name = sortedBy[i].AsString();
+ if (!typeColumns.contains(name)) {
+ NYT::TNode fieldType;
+ if (!sortedDirections.empty() && !sortedDirections[i].AsInt64()) {
+ fieldType = NYT::TNode()("wire_type", "string32");
+ } else {
+ fieldType = NCommon::ParseSkiffTypeFromYson(sortedByType[i], nativeYtTypeFlags);
+ }
+ fieldType["name"] = name;
+ YQL_ENSURE(typeColumns.emplace(name, std::move(fieldType)).second, "Duplicate struct aux column: " << name);
+ }
+ }
+ }
+
+ if (typeColumns.empty()) {
+ typeColumns.emplace("_yql_fake_column", NYT::TNode()
+ ("name", "_yql_fake_column")
+ ("wire_type", "variant8")
+ ("children", NYT::TNode()
+ .Add(NYT::TNode()("wire_type", "nothing"))
+ .Add(NYT::TNode()("wire_type", "boolean"))
+ )
+ );
+ }
+
+ NYT::TNode tableSchema;
+ tableSchema["wire_type"] = "tuple";
+ auto& tableColumns = tableSchema["children"];
+ tableColumns = NYT::TNode::CreateList();
+ for (auto& item: typeColumns) {
+ tableColumns.Add(item.second);
+ }
+
+ return tableSchema;
+}
+
+}
+
+NYT::TNode SingleTableSpecToInputSkiff(const NYT::TNode& spec, const THashMap<TString, ui32>& structColumns, bool rowIndex, bool rangeIndex, bool keySwitch) {
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+ auto& skiffConfig = formatConfig.Attributes();
+ auto& skiffSchemas = skiffConfig["table_skiff_schemas"];
+ skiffSchemas.Add(RowSpecToInputSkiff(spec, structColumns, {}, rowIndex, rangeIndex, keySwitch));
+
+ return formatConfig;
+}
+
+NYT::TNode SingleTableSpecToInputSkiffSchema(const NYT::TNode& spec, size_t tableIndex, const THashMap<TString, ui32>& structColumns, const THashSet<TString>& auxColumns, bool rowIndex, bool rangeIndex, bool keySwitch) {
+ YQL_ENSURE(spec.IsMap(), "Expect Map type of input meta attrs, but got type " << spec.GetType());
+ YQL_ENSURE(spec.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ const auto& inputSpecs = spec[YqlIOSpecTables].AsList();
+ YQL_ENSURE(tableIndex < inputSpecs.size());
+
+ if (inputSpecs[tableIndex].IsString()) {
+ auto refName = inputSpecs[tableIndex].AsString();
+ YQL_ENSURE(spec.HasKey(YqlIOSpecRegistry) && spec[YqlIOSpecRegistry].HasKey(refName), "Bad input registry reference: " << refName);
+ return RowSpecToInputSkiff(spec[YqlIOSpecRegistry][refName], structColumns, auxColumns, rowIndex, rangeIndex, keySwitch);
+ } else {
+ return RowSpecToInputSkiff(inputSpecs[tableIndex], structColumns, auxColumns, rowIndex, rangeIndex, keySwitch);
+ }
+}
+
+NYT::TNode TablesSpecToInputSkiff(const NYT::TNode& spec, const THashMap<TString, ui32>& structColumns, bool rowIndex, bool rangeIndex, bool keySwitch) {
+ YQL_ENSURE(spec.IsMap(), "Expect Map type of input meta attrs, but got type " << spec.GetType());
+ YQL_ENSURE(spec.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ const auto& inputSpecs = spec[YqlIOSpecTables].AsList();
+
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+ auto& skiffConfig = formatConfig.Attributes();
+ auto skiffSchemas = NYT::TNode::CreateList();
+ THashMap<TString, size_t> uniqSchemas;
+ for (size_t inputIndex = 0; inputIndex < inputSpecs.size(); ++inputIndex) {
+ if (inputSpecs[inputIndex].IsString()) {
+ auto refName = inputSpecs[inputIndex].AsString();
+
+ size_t schemaId = uniqSchemas.size();
+ auto p = uniqSchemas.emplace(refName, schemaId);
+ if (p.second) {
+ YQL_ENSURE(spec.HasKey(YqlIOSpecRegistry) && spec[YqlIOSpecRegistry].HasKey(refName), "Bad input registry reference: " << refName);
+ NYT::TNode tableSchema = RowSpecToInputSkiff(spec[YqlIOSpecRegistry][refName], structColumns, {}, rowIndex, rangeIndex, keySwitch);
+ skiffConfig["skiff_schema_registry"][TStringBuilder() << "table" << schemaId] = std::move(tableSchema);
+ }
+ else {
+ schemaId = p.first->second;
+ }
+
+ skiffSchemas.Add(NYT::TNode(TStringBuilder() << "$table" << schemaId));
+ } else {
+ NYT::TNode tableSchema = RowSpecToInputSkiff(inputSpecs[inputIndex], structColumns, {}, rowIndex, rangeIndex, keySwitch);
+ skiffSchemas.Add(std::move(tableSchema));
+ }
+ }
+
+ skiffConfig["table_skiff_schemas"] = std::move(skiffSchemas);
+ return formatConfig;
+}
+
+NYT::TNode TablesSpecToOutputSkiff(const NYT::TNode& spec) {
+ YQL_ENSURE(spec.IsMap(), "Expect Map type of output meta attrs, but got type " << spec.GetType());
+ YQL_ENSURE(spec.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ const auto& outSpecs = spec[YqlIOSpecTables].AsList();
+
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+ auto& skiffConfig = formatConfig.Attributes();
+ auto skiffSchemas = NYT::TNode::CreateList();
+ THashMap<TString, size_t> uniqSchemas;
+ for (size_t outIndex = 0; outIndex < outSpecs.size(); ++outIndex) {
+ if (outSpecs[outIndex].IsString()) {
+ auto refName = outSpecs[outIndex].AsString();
+
+ size_t schemaId = uniqSchemas.size();
+ auto p = uniqSchemas.emplace(refName, schemaId);
+ if (p.second) {
+ YQL_ENSURE(spec.HasKey(YqlIOSpecRegistry) && spec[YqlIOSpecRegistry].HasKey(refName), "Bad output registry reference: " << refName);
+ NYT::TNode tableSchema = RowSpecToOutputSkiff(spec[YqlIOSpecRegistry][refName]);
+ skiffConfig["skiff_schema_registry"][TStringBuilder() << "table" << schemaId] = std::move(tableSchema);
+ }
+ else {
+ schemaId = p.first->second;
+ }
+
+ skiffSchemas.Add(NYT::TNode(TStringBuilder() << "$table" << schemaId));
+ } else {
+ NYT::TNode tableSchema = RowSpecToOutputSkiff(outSpecs[outIndex]);
+ skiffSchemas.Add(std::move(tableSchema));
+ }
+ }
+
+ skiffConfig["table_skiff_schemas"] = std::move(skiffSchemas);
+ return formatConfig;
+}
+
+NYT::TNode SingleTableSpecToOutputSkiff(const NYT::TNode& spec, size_t tableIndex) {
+ YQL_ENSURE(spec.IsMap(), "Expect Map type of output meta attrs, but got type " << spec.GetType());
+ YQL_ENSURE(spec.HasKey(YqlIOSpecTables), "Expect " << TString{YqlIOSpecTables}.Quote() << " key");
+
+ const auto& outSpecs = spec[YqlIOSpecTables].AsList();
+ YQL_ENSURE(tableIndex < outSpecs.size());
+
+ NYT::TNode formatConfig = NYT::TNode("skiff");
+ auto& skiffConfig = formatConfig.Attributes();
+ auto skiffSchemas = NYT::TNode::CreateList();
+ THashMap<TString, size_t> uniqSchemas;
+ if (outSpecs[tableIndex].IsString()) {
+ auto refName = outSpecs[tableIndex].AsString();
+ YQL_ENSURE(spec.HasKey(YqlIOSpecRegistry) && spec[YqlIOSpecRegistry].HasKey(refName), "Bad output registry reference: " << refName);
+ NYT::TNode tableSchema = RowSpecToOutputSkiff(spec[YqlIOSpecRegistry][refName]);
+ skiffSchemas.Add(std::move(tableSchema));
+ } else {
+ NYT::TNode tableSchema = RowSpecToOutputSkiff(outSpecs[tableIndex]);
+ skiffSchemas.Add(std::move(tableSchema));
+ }
+
+ skiffConfig["table_skiff_schemas"] = std::move(skiffSchemas);
+ return formatConfig;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h b/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h
new file mode 100644
index 0000000000..a92a508c85
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/string.h>
+#include <util/generic/hash.h>
+
+
+namespace NYql {
+
+NYT::TNode TablesSpecToInputSkiff(const NYT::TNode& spec, const THashMap<TString, ui32>& structColumns, bool rowIndex, bool rangeIndex, bool keySwitch);
+NYT::TNode SingleTableSpecToInputSkiff(const NYT::TNode& spec, const THashMap<TString, ui32>& structColumns, bool rowIndex, bool rangeIndex, bool keySwitch);
+NYT::TNode SingleTableSpecToInputSkiffSchema(const NYT::TNode& spec, size_t tableIndex, const THashMap<TString, ui32>& structColumns, const THashSet<TString>& auxColumns, bool rowIndex, bool rangeIndex, bool keySwitch);
+
+NYT::TNode TablesSpecToOutputSkiff(const NYT::TNode& spec);
+NYT::TNode SingleTableSpecToOutputSkiff(const NYT::TNode& spec, size_t tableIndex);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/ya.make b/ydb/library/yql/providers/yt/lib/url_mapper/ya.make
new file mode 100644
index 0000000000..9de4ba5b52
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_url_mapper.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/providers/common/proto
+ library/cpp/regex/pcre
+ library/cpp/uri
+ library/cpp/cgiparam
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp b/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
new file mode 100644
index 0000000000..5c25604f03
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.cpp
@@ -0,0 +1,65 @@
+#include "yql_yt_url_mapper.h"
+
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+
+#include <library/cpp/uri/http_url.h>
+#include <library/cpp/cgiparam/cgiparam.h>
+
+#include <util/generic/algorithm.h>
+#include <util/generic/yexception.h>
+
+namespace NYql {
+
+TYtUrlMapper::TYtUrlMapper(const TYtGatewayConfig& config)
+ : RemoteFilePatterns_(BuildRemoteFilePatterns(config))
+{
+}
+
+bool TYtUrlMapper::MapYtUrl(const TString& url, TString* cluster, TString* path) const {
+ THttpURL parsedUrl;
+ if (THttpURL::ParsedOK != parsedUrl.Parse(url, THttpURL::FeaturesAll | NUri::TFeature::FeatureConvertHostIDN | NUri::TFeature::FeatureNoRelPath, {}, 65536)) {
+ return false;
+ }
+
+ const auto rawScheme = parsedUrl.GetField(NUri::TField::FieldScheme);
+ auto host = parsedUrl.GetHost();
+ if (NUri::EqualNoCase(rawScheme, "yt") || host.StartsWith("yt.yandex") || host.EndsWith(".yt.yandex-team.ru") || host.EndsWith(".yt.yandex.net")) {
+ if (cluster) {
+ if (host.StartsWith("yt.")) {
+ host = parsedUrl.GetField(NUri::TField::FieldPath);
+ host = host.Skip(1).Before('/');
+ } else {
+ host.ChopSuffix(".yt.yandex-team.ru");
+ host.ChopSuffix(".yt.yandex.net");
+ }
+ *cluster = host;
+ }
+
+ if (path) {
+ if (!FindMatchingRemoteFilePattern(url)) {
+ return false;
+ }
+ TCgiParameters params(parsedUrl.GetField(NUri::TField::FieldQuery));
+ *path = params.Has("path") ? params.Get("path") : TString{TStringBuf(parsedUrl.GetField(NUri::TField::FieldPath)).Skip(1)};
+ }
+
+ return true;
+ }
+ return false;
+}
+
+TVector<TRegExMatch> TYtUrlMapper::BuildRemoteFilePatterns(const TYtGatewayConfig& config) {
+ TVector<TRegExMatch> res;
+ for (auto& fp : config.GetRemoteFilePatterns()) {
+ res.emplace_back(fp.GetPattern());
+ }
+ return res;
+}
+
+bool TYtUrlMapper::FindMatchingRemoteFilePattern(const TString& url) const {
+ return AnyOf(RemoteFilePatterns_, [&](auto& p) {
+ return p.Match(url.data());
+ });
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h b/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h
new file mode 100644
index 0000000000..d0dfd7ecdf
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/url_mapper/yql_yt_url_mapper.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <library/cpp/regex/pcre/regexp.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+
+namespace NYql {
+
+class TYtGatewayConfig;
+
+class TYtUrlMapper {
+public:
+ explicit TYtUrlMapper(const TYtGatewayConfig& config);
+ bool MapYtUrl(const TString& url, TString* cluster, TString* path) const;
+
+private:
+ static TVector<TRegExMatch> BuildRemoteFilePatterns(const TYtGatewayConfig& config);
+ bool FindMatchingRemoteFilePattern(const TString& url) const;
+
+private:
+ const TVector<TRegExMatch> RemoteFilePatterns_;
+};
+}
diff --git a/ydb/library/yql/providers/yt/lib/ya.make b/ydb/library/yql/providers/yt/lib/ya.make
new file mode 100644
index 0000000000..945ca0ed28
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/ya.make
@@ -0,0 +1,17 @@
+RECURSE(
+ expr_traits
+ graph_reorder
+ hash
+ infer_schema
+ init_yt_api
+ key_filter
+ lambda_builder
+ log
+ mkql_helpers
+ res_pull
+ row_spec
+ schema
+ skiff
+ url_mapper
+ yson_helpers
+)
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/ya.make b/ydb/library/yql/providers/yt/lib/yson_helpers/ya.make
new file mode 100644
index 0000000000..739a8b2e0d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+SRCS(
+ yson_helpers.cpp
+)
+
+PEERDIR(
+ library/cpp/yson
+ library/cpp/yson/node
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/providers/yt/common
+)
+
+END()
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp b/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
new file mode 100644
index 0000000000..fdd39c6871
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.cpp
@@ -0,0 +1,310 @@
+#include "yson_helpers.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/log/context.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/detail.h>
+
+#include <util/generic/yexception.h>
+
+namespace NYql {
+
+TBinaryYsonWriter::TBinaryYsonWriter(IOutputStream* stream, NYson::EYsonType type)
+ : NYson::TYsonWriter(stream, NYson::EYsonFormat::Binary, type, false)
+{
+ if (type != ::NYson::EYsonType::Node) {
+ Stack_.push_back(false);
+ }
+}
+
+void TBinaryYsonWriter::OnBeginList() {
+ Stack_.push_back(false);
+ NYson::TYsonWriter::OnBeginList();
+}
+
+void TBinaryYsonWriter::OnListItem() {
+ Stack_.back() = true;
+ NYson::TYsonWriter::OnListItem();
+}
+
+void TBinaryYsonWriter::OnEndList() {
+ if (Stack_.back()) {
+ Stream->Write(NYson::NDetail::ListItemSeparatorSymbol);
+ }
+ Stack_.pop_back();
+ NYson::TYsonWriter::OnEndList();
+}
+
+void TBinaryYsonWriter::OnBeginMap() {
+ Stack_.push_back(false);
+ NYson::TYsonWriter::OnBeginMap();
+}
+
+void TBinaryYsonWriter::OnKeyedItem(TStringBuf key) {
+ Stack_.back() = true;
+ NYson::TYsonWriter::OnKeyedItem(key);
+}
+
+void TBinaryYsonWriter::OnEndMap() {
+ if (Stack_.back()) {
+ Stream->Write(NYson::NDetail::KeyedItemSeparatorSymbol);
+ }
+ Stack_.pop_back();
+ NYson::TYsonWriter::OnEndMap();
+}
+
+void TBinaryYsonWriter::OnBeginAttributes() {
+ Stack_.push_back(false);
+ NYson::TYsonWriter::OnBeginAttributes();
+}
+
+void TBinaryYsonWriter::OnEndAttributes() {
+ if (Stack_.back()) {
+ Stream->Write(NYson::NDetail::KeyedItemSeparatorSymbol);
+ }
+ Stack_.pop_back();
+ NYson::TYsonWriter::OnEndAttributes();
+}
+
+
+TColumnFilteringConsumer::TColumnFilteringConsumer(NYT::NYson::IYsonConsumer* parent, const TMaybe<TSet<TStringBuf>>& columns,
+ const TMaybe<THashMap<TStringBuf, TStringBuf>>& renameColumns)
+ : Parent_(parent)
+ , Columns_(columns)
+ , RenameColumns_(renameColumns)
+{
+}
+
+void TColumnFilteringConsumer::OnStringScalar(TStringBuf value) {
+ if (Enable_) {
+ Parent_->OnStringScalar(value);
+ }
+}
+
+void TColumnFilteringConsumer::OnInt64Scalar(i64 value) {
+ if (Enable_) {
+ Parent_->OnInt64Scalar(value);
+ }
+}
+
+void TColumnFilteringConsumer::OnUint64Scalar(ui64 value) {
+ if (Enable_) {
+ Parent_->OnUint64Scalar(value);
+ }
+}
+
+void TColumnFilteringConsumer::OnDoubleScalar(double value) {
+ if (Enable_) {
+ Parent_->OnDoubleScalar(value);
+ }
+}
+
+void TColumnFilteringConsumer::OnBooleanScalar(bool value) {
+ if (Enable_) {
+ Parent_->OnBooleanScalar(value);
+ }
+}
+
+void TColumnFilteringConsumer::OnEntity() {
+ if (Enable_) {
+ Parent_->OnEntity();
+ }
+}
+
+void TColumnFilteringConsumer::OnBeginList() {
+ Stack_.push_back(Enable_);
+ if (Enable_) {
+ Parent_->OnBeginList();
+ }
+}
+
+void TColumnFilteringConsumer::OnListItem() {
+ if (Enable_) {
+ Parent_->OnListItem();
+ }
+}
+
+void TColumnFilteringConsumer::OnEndList() {
+ Enable_ = Stack_.back();
+ Stack_.pop_back();
+ if (Enable_) {
+ Parent_->OnEndList();
+ }
+}
+
+void TColumnFilteringConsumer::OnBeginMap() {
+ Stack_.push_back(Enable_);
+ if (Enable_) {
+ Parent_->OnBeginMap();
+ }
+}
+
+void TColumnFilteringConsumer::OnKeyedItem(TStringBuf key) {
+ TStringBuf outputKey = key;
+ if (Stack_.size() == 1) {
+ if (RenameColumns_) {
+ auto r = RenameColumns_->find(key);
+ if (r != RenameColumns_->end()) {
+ outputKey = r->second;
+ }
+ }
+ Enable_ = !Columns_ || Columns_->find(outputKey) != Columns_->end();
+ }
+ if (Enable_) {
+ Parent_->OnKeyedItem(outputKey);
+ }
+}
+
+void TColumnFilteringConsumer::OnEndMap() {
+ Enable_ = Stack_.back();
+ Stack_.pop_back();
+ if (Enable_) {
+ Parent_->OnEndMap();
+ }
+}
+
+void TColumnFilteringConsumer::OnBeginAttributes() {
+ if (Stack_.back()) {
+ Parent_->OnBeginAttributes();
+ }
+}
+
+void TColumnFilteringConsumer::OnEndAttributes() {
+ if (Enable_) {
+ Parent_->OnEndAttributes();
+ }
+}
+
+void TColumnFilteringConsumer::OnRaw(TStringBuf yson, NYson::EYsonType type) {
+ if (Enable_) {
+ Parent_->OnRaw(yson, type);
+ }
+}
+
+TDoubleHighPrecisionYsonWriter::TDoubleHighPrecisionYsonWriter(IOutputStream* stream, NYson::EYsonType type, bool enableRaw)
+ : TYsonWriter(stream, NYson::EYsonFormat::Text, type, enableRaw)
+{
+}
+
+void TDoubleHighPrecisionYsonWriter::OnDoubleScalar(double value) {
+ TString str;
+ if (std::isfinite(value)) {
+ str = ::FloatToString(value);
+ } else {
+ if (std::isnan(value)) {
+ str = TStringBuf("%nan");
+ } else if (value > 0) {
+ str = TStringBuf("%inf");
+ } else {
+ str = TStringBuf("%-inf");
+ }
+ }
+
+ Stream->Write(str);
+ if (str.find('.') == TString::npos && str.find('e') == TString::npos && std::isfinite(value)) {
+ Stream->Write(".");
+ }
+ EndNode();
+}
+
+void WriteTableReference(NYson::TYsonWriter& writer, TStringBuf provider, TStringBuf cluster, TStringBuf table,
+ bool remove, const TVector<TString>& columns)
+{
+ writer.OnBeginMap();
+ writer.OnKeyedItem("Reference");
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnStringScalar(provider);
+ writer.OnListItem();
+ writer.OnStringScalar(cluster);
+ writer.OnListItem();
+ writer.OnStringScalar(table);
+ writer.OnEndList();
+ if (!columns.empty()) {
+ writer.OnKeyedItem("Columns");
+ writer.OnBeginList();
+ for (auto& column: columns) {
+ writer.OnListItem();
+ writer.OnStringScalar(column);
+ }
+ writer.OnEndList();
+ }
+ writer.OnKeyedItem("Remove");
+ writer.OnBooleanScalar(remove);
+ writer.OnEndMap();
+}
+
+bool HasYqlRowSpec(const NYT::TNode& inAttrs)
+{
+ bool first = true;
+ bool hasScheme = false;
+ for (auto& attrs: inAttrs.AsList()) {
+ bool tableHasScheme = attrs.HasKey(YqlRowSpecAttribute);
+ if (first) {
+ hasScheme = tableHasScheme;
+ first = false;
+ } else {
+ YQL_ENSURE(hasScheme == tableHasScheme, "Different schemas of input tables");
+ }
+ }
+ return hasScheme;
+}
+
+bool HasYqlRowSpec(const TString& inputAttrs)
+{
+ NYT::TNode inAttrs;
+ try {
+ inAttrs = NYT::NodeFromYsonString(inputAttrs);
+ } catch (const yexception& e) {
+ YQL_LOG_CTX_THROW yexception() << "Invalid input meta attrs: " << e.what();
+ }
+ YQL_ENSURE(inAttrs.IsList(), "Expect List type of input meta attrs, but got type " << inAttrs.GetType());
+ return HasYqlRowSpec(inAttrs);
+}
+
+bool HasStrictSchema(const NYT::TNode& attrs)
+{
+ if (attrs.HasKey(YqlRowSpecAttribute) && attrs[YqlRowSpecAttribute].HasKey(RowSpecAttrStrictSchema)) {
+ auto& strictSchemaAttr = attrs[YqlRowSpecAttribute][RowSpecAttrStrictSchema];
+ return strictSchemaAttr.IsInt64()
+ ? strictSchemaAttr.AsInt64() != 0
+ : NYT::GetBool(strictSchemaAttr);
+ }
+ return true;
+}
+
+TMaybe<ui64> GetDataWeight(const NYT::TNode& tableAttrs) {
+ ui64 ret = 0;
+ bool hasAttr = false;
+ if (tableAttrs.AsMap().contains("data_weight")) {
+ ret = (ui64)tableAttrs["data_weight"].AsInt64();
+ hasAttr = true;
+ }
+
+ if (!ret && tableAttrs.AsMap().contains("uncompressed_data_size")) {
+ ret = (ui64)tableAttrs["uncompressed_data_size"].AsInt64();
+ hasAttr = true;
+ }
+
+ if (hasAttr) {
+ return ret;
+ }
+
+ return Nothing();
+}
+
+ui64 GetTableRowCount(const NYT::TNode& tableAttrs) {
+ return NYT::GetBool(tableAttrs[TStringBuf("dynamic")])
+ ? tableAttrs[TStringBuf("chunk_row_count")].AsInt64()
+ : tableAttrs[TStringBuf("row_count")].AsInt64();
+}
+
+ui64 GetContentRevision(const NYT::TNode& tableAttrs) {
+ return tableAttrs.HasKey("content_revision")
+ ? tableAttrs["content_revision"].IntCast<ui64>()
+ : tableAttrs["revision"].IntCast<ui64>();
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h b/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h
new file mode 100644
index 0000000000..aabc8f5aa2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/generic/fwd.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/set.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+
+namespace NYql {
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class TBinaryYsonWriter : public NYson::TYsonWriter {
+public:
+ TBinaryYsonWriter(IOutputStream* stream, NYson::EYsonType type = ::NYson::EYsonType::Node);
+
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+private:
+ TVector<bool> Stack_;
+};
+
+class TColumnFilteringConsumer: public NYT::NYson::IYsonConsumer {
+public:
+ TColumnFilteringConsumer(NYT::NYson::IYsonConsumer* parent, const TMaybe<TSet<TStringBuf>>& columns,
+ const TMaybe<THashMap<TStringBuf, TStringBuf>>& renameColumns);
+
+ virtual void OnStringScalar(TStringBuf value) final;
+ virtual void OnInt64Scalar(i64 value) final;
+ virtual void OnUint64Scalar(ui64 value) final;
+ virtual void OnDoubleScalar(double value) final;
+ virtual void OnBooleanScalar(bool value) final;
+ virtual void OnEntity() final;
+ virtual void OnBeginList() final;
+ virtual void OnListItem() final;
+ virtual void OnEndList() final;
+ virtual void OnBeginMap() final;
+ virtual void OnKeyedItem(TStringBuf key) final;
+ virtual void OnEndMap() final;
+ virtual void OnBeginAttributes() final;
+ virtual void OnEndAttributes() final;
+ virtual void OnRaw(TStringBuf yson, NYson::EYsonType type) final;
+private:
+ NYT::NYson::IYsonConsumer* Parent_;
+ bool Enable_ = true;
+ TVector<bool> Stack_;
+ TMaybe<TSet<TStringBuf>> Columns_;
+ TMaybe<THashMap<TStringBuf, TStringBuf>> RenameColumns_;
+};
+
+class TDoubleHighPrecisionYsonWriter: public NYson::TYsonWriter {
+public:
+ TDoubleHighPrecisionYsonWriter(IOutputStream* stream, NYson::EYsonType type = ::NYson::EYsonType::Node, bool enableRaw = false);
+ void OnDoubleScalar(double value);
+};
+
+void WriteTableReference(NYson::TYsonWriter& writer, TStringBuf provider, TStringBuf cluster, TStringBuf table,
+ bool remove, const TVector<TString>& columns);
+
+bool HasYqlRowSpec(const NYT::TNode& inAttrs);
+bool HasYqlRowSpec(const TString& inputAttrs);
+
+bool HasStrictSchema(const NYT::TNode& attrs);
+
+TMaybe<ui64> GetDataWeight(const NYT::TNode& inAttrs);
+ui64 GetTableRowCount(const NYT::TNode& tableAttrs);
+ui64 GetContentRevision(const NYT::TNode& tableAttrs);
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/mkql_dq/ya.make b/ydb/library/yql/providers/yt/mkql_dq/ya.make
new file mode 100644
index 0000000000..97c92a7b6f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/mkql_dq/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_dq_transform.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/minikql
+ ydb/library/yql/public/udf
+ ydb/library/yql/utils
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.cpp b/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.cpp
new file mode 100644
index 0000000000..03696f7da8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.cpp
@@ -0,0 +1,130 @@
+#include "yql_yt_dq_transform.h"
+
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/mkql_node_printer.h>
+#include <ydb/library/yql/minikql/defs.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/node/serialize.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/guid.h>
+
+
+namespace NYql {
+
+using namespace NKikimr;
+
+class TYtDqTaskTransform {
+public:
+ TYtDqTaskTransform(THashMap<TString, TString> taskParams, const NMiniKQL::IFunctionRegistry& functionRegistry)
+ : TaskParams(std::move(taskParams))
+ , FunctionRegistry(functionRegistry)
+ {
+ }
+
+ NMiniKQL::TCallableVisitFunc operator()(NMiniKQL::TInternName name) {
+ if (TaskParams.contains("yt") && name == "DqYtRead") {
+ return [this](NMiniKQL::TCallable& callable, const NMiniKQL::TTypeEnvironment& env) {
+ using namespace NMiniKQL;
+
+ TProgramBuilder pgmBuilder(env, FunctionRegistry);
+
+ YQL_ENSURE(callable.GetInputsCount() == 7 || callable.GetInputsCount() == 8, "Expected 7 or 8 arguments.");
+
+ TCallableBuilder callableBuilder(env, callable.GetType()->GetName(), callable.GetType()->GetReturnType(), false);
+ callableBuilder.Add(callable.GetInput(0));
+ callableBuilder.Add(callable.GetInput(1));
+ callableBuilder.Add(callable.GetInput(2));
+ callableBuilder.Add(callable.GetInput(3));
+
+ if (callable.GetInputsCount() == 7U)
+ callableBuilder.Add(callable.GetInput(4));
+ else {
+ auto params = NYT::NodeFromYsonString(TaskParams.Value("yt", TString())).AsMap();
+
+ TVector<TRuntimeNode> newGrpList;
+ TListLiteral* groupList = AS_VALUE(TListLiteral, callable.GetInput(4));
+ for (ui32 grp = 0; grp < groupList->GetItemsCount(); ++grp) {
+ TListLiteral* tableList = AS_VALUE(TListLiteral, groupList->GetItems()[grp]);
+ TVector<TRuntimeNode> newTableList;
+ for (ui32 i = 0; i < tableList->GetItemsCount(); ++i) {
+ TString paramsKey = TStringBuilder() << grp << "/" << i;
+
+ TTupleLiteral* tableTuple = AS_VALUE(TTupleLiteral, tableList->GetItems()[i]);
+ YQL_ENSURE(tableTuple->GetValuesCount() == 3);
+
+ NYT::TRichYPath richYPath;
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, tableTuple->GetValue(1))->AsValue().AsStringRef())));
+
+ if (params.contains(paramsKey)) {
+ NYT::TRichYPath ranges;
+ NYT::Deserialize(ranges, params[paramsKey]);
+ richYPath.MutableRanges() = ranges.GetRanges();
+ } else {
+ richYPath.MutableRanges().ConstructInPlace();
+ }
+
+ newTableList.push_back(pgmBuilder.NewTuple({
+ tableTuple->GetValue(0),
+ pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(NYT::PathToNode(richYPath))),
+ tableTuple->GetValue(2),
+ }));
+ }
+ newGrpList.push_back(pgmBuilder.NewList(newTableList.front().GetStaticType(), newTableList));
+ }
+ callableBuilder.Add(pgmBuilder.NewList(newGrpList.front().GetStaticType(), newGrpList));
+ }
+ callableBuilder.Add(callable.GetInput(5));
+ callableBuilder.Add(callable.GetInput(6));
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+ if (name == "YtDqRowsWideWrite") {
+ YQL_ENSURE(TaskParams.contains("yt.write.tx"), "Expected nested transaction");
+ return [this](NMiniKQL::TCallable& callable, const NMiniKQL::TTypeEnvironment& env) -> NMiniKQL::TRuntimeNode {
+ using namespace NMiniKQL;
+
+ TProgramBuilder pgmBuilder(env, FunctionRegistry);
+
+ YQL_ENSURE(callable.GetInputsCount() == 6, "Expected six arguments.");
+
+ TCallableBuilder callableBuilder(env, callable.GetType()->GetName(), callable.GetType()->GetReturnType(), false);
+ callableBuilder.Add(callable.GetInput(0));
+ callableBuilder.Add(callable.GetInput(1));
+ callableBuilder.Add(callable.GetInput(2));
+
+ NYT::TRichYPath richYPath;
+ NYT::Deserialize(richYPath, NYT::NodeFromYsonString(TString(AS_VALUE(TDataLiteral, callable.GetInput(3))->AsValue().AsStringRef())));
+ richYPath.TransactionId(GetGuid(TaskParams.Value("yt.write.tx", TString())));
+ callableBuilder.Add(pgmBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(NYT::PathToNode(richYPath))));
+
+ callableBuilder.Add(callable.GetInput(4));
+ callableBuilder.Add(callable.GetInput(5));
+
+ return TRuntimeNode(callableBuilder.Build(), false);
+ };
+ }
+
+ return NMiniKQL::TCallableVisitFunc();
+ }
+
+private:
+ THashMap<TString, TString> TaskParams;
+ const NMiniKQL::IFunctionRegistry& FunctionRegistry;
+};
+
+TTaskTransformFactory CreateYtDqTaskTransformFactory() {
+ return [] (const THashMap<TString, TString>& taskParams, const NKikimr::NMiniKQL::IFunctionRegistry* funcRegistry) -> NKikimr::NMiniKQL::TCallableVisitFuncProvider {
+ return TYtDqTaskTransform(taskParams, *funcRegistry);
+ };
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.h b/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.h
new file mode 100644
index 0000000000..3c5dfc0493
--- /dev/null
+++ b/ydb/library/yql/providers/yt/mkql_dq/yql_yt_dq_transform.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <ydb/library/yql/dq/integration/transform/yql_dq_task_transform.h>
+
+namespace NYql {
+
+TTaskTransformFactory CreateYtDqTaskTransformFactory();
+
+}
diff --git a/ydb/library/yql/providers/yt/opt/ya.make b/ydb/library/yql/providers/yt/opt/ya.make
new file mode 100644
index 0000000000..979964a50f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_join.cpp
+ yql_yt_key_selector.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/providers/yt/lib/row_spec
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core
+ ydb/library/yql/ast
+)
+
+
+ YQL_LAST_ABI_VERSION()
+
+
+END()
diff --git a/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp b/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
new file mode 100644
index 0000000000..22a8750f70
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/yql_yt_join.cpp
@@ -0,0 +1,676 @@
+#include "yql_yt_join.h"
+
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+TChoice Invert(TChoice choice) {
+ switch (choice) {
+ case TChoice::None:
+ case TChoice::Both:
+ return choice;
+ case TChoice::Left:
+ return TChoice::Right;
+ case TChoice::Right:
+ return TChoice::Left;
+ }
+}
+
+bool IsLeftOrRight(TChoice choice) {
+ return choice == TChoice::Left || choice == TChoice::Right;
+}
+
+TChoice Merge(TChoice a, TChoice b) {
+ return TChoice(ui8(a) | ui8(b));
+}
+
+TMaybe<ui64> TMapJoinSettings::CalculatePartSize(ui64 rows) const {
+ if (MapJoinShardCount > 1 && rows > MapJoinShardMinRows) {
+ ui64 partSize = (rows + MapJoinShardCount - 1) / MapJoinShardCount;
+ if (partSize < MapJoinShardMinRows) {
+ // make less parts
+ partSize = MapJoinShardMinRows;
+ ui64 count = (rows + partSize - 1) / partSize;
+ if (count > 1) {
+ return partSize;
+ }
+
+ return {};
+ }
+ else {
+ return partSize;
+ }
+ }
+
+ return {};
+}
+
+THashSet<TString> BuildJoinKeys(const TJoinLabel& label, const TExprNode& keys) {
+ THashSet<TString> result;
+ for (ui32 i = 0; i < keys.ChildrenSize(); i += 2) {
+ auto tableName = keys.Child(i)->Content();
+ auto column = keys.Child(i + 1)->Content();
+ result.insert(label.MemberName(tableName, column));
+ }
+
+ return result;
+}
+
+TVector<TString> BuildJoinKeyList(const TJoinLabel& label, const TExprNode& keys) {
+ TVector<TString> result;
+ for (ui32 i = 0; i < keys.ChildrenSize(); i += 2) {
+ auto tableName = keys.Child(i)->Content();
+ auto column = keys.Child(i + 1)->Content();
+ result.push_back(label.MemberName(tableName, column));
+ }
+
+ return result;
+}
+
+TVector<const TTypeAnnotationNode*> BuildJoinKeyType(const TJoinLabel& label, const TExprNode& keys) {
+ TVector<const TTypeAnnotationNode*> ret;
+ for (ui32 i = 0; i < keys.ChildrenSize(); i += 2) {
+ auto tableName = keys.Child(i)->Content();
+ auto column = keys.Child(i + 1)->Content();
+ auto type = label.FindColumn(tableName, column);
+ ret.push_back(*type);
+ }
+
+ return ret;
+}
+
+TMap<TString, const TTypeAnnotationNode*> BuildJoinKeyTypeMap(const TJoinLabel& label, const TExprNode& keys) {
+ auto names = BuildJoinKeyList(label, keys);
+ auto types = BuildJoinKeyType(label, keys);
+
+ YQL_ENSURE(names.size() == types.size());
+
+ TMap<TString, const TTypeAnnotationNode*> ret;
+ for (size_t i = 0; i < names.size(); ++i) {
+ ret[names[i]] = types[i];
+ }
+ return ret;
+}
+
+TVector<const TTypeAnnotationNode*> RemoveNullsFromJoinKeyType(const TVector<const TTypeAnnotationNode*>& inputKeyType) {
+ TVector<const TTypeAnnotationNode*> ret;
+ for (const auto& x : inputKeyType) {
+ ret.push_back(RemoveOptionalType(x));
+ }
+
+ return ret;
+}
+
+const TTypeAnnotationNode* AsDictKeyType(const TVector<const TTypeAnnotationNode*>& inputKeyType, TExprContext& ctx) {
+ YQL_ENSURE(inputKeyType.size() > 0);
+ if (inputKeyType.size() == 1) {
+ return inputKeyType.front();
+ } else {
+ return ctx.MakeType<TTupleExprType>(inputKeyType);
+ }
+}
+
+void SwapJoinType(TPositionHandle pos, TExprNode::TPtr& joinType, TExprContext& ctx) {
+ if (joinType->Content() == "RightSemi") {
+ joinType = ctx.NewAtom(pos, "LeftSemi");
+ }
+ else if (joinType->Content() == "RightOnly") {
+ joinType = ctx.NewAtom(pos, "LeftOnly");
+ }
+ else if (joinType->Content() == "Right") {
+ joinType = ctx.NewAtom(pos, "Left");
+ }
+ else if (joinType->Content() == "LeftSemi") {
+ joinType = ctx.NewAtom(pos, "RightSemi");
+ }
+ else if (joinType->Content() == "LeftOnly") {
+ joinType = ctx.NewAtom(pos, "RightOnly");
+ }
+ else if (joinType->Content() == "Left") {
+ joinType = ctx.NewAtom(pos, "Right");
+ }
+}
+
+const TStructExprType* MakeOutputJoinColumns(const THashMap<TString, const TTypeAnnotationNode*>& columnTypes,
+ const TJoinLabel& label, TExprContext& ctx) {
+ TVector<const TItemExprType*> resultFields;
+ for (auto& x : label.InputType->GetItems()) {
+ TString fullName = label.FullName(x->GetName());
+ if (auto columnType = columnTypes.FindPtr(fullName)) {
+ resultFields.push_back(ctx.MakeType<TItemExprType>(fullName, *columnType));
+ }
+ }
+
+ return ctx.MakeType<TStructExprType>(resultFields);
+}
+
+const TTypeAnnotationNode* UnifyJoinKeyType(TPositionHandle pos, const TVector<const TTypeAnnotationNode*>& types, TExprContext& ctx) {
+ TTypeAnnotationNode::TListType t = types;
+ const TTypeAnnotationNode* commonType = CommonType(pos, t, ctx);
+ if (commonType && !commonType->IsOptionalOrNull()) {
+ NUdf::TCastResultOptions options = 0;
+ for (auto type : types) {
+ options |= CastResult<true>(type, commonType);
+ }
+
+ YQL_ENSURE(!(options & NKikimr::NUdf::ECastOptions::Impossible));
+ if (options & NKikimr::NUdf::ECastOptions::MayFail) {
+ commonType = ctx.MakeType<TOptionalExprType>(commonType);
+ }
+ }
+ return commonType;
+}
+
+TVector<const TTypeAnnotationNode*> UnifyJoinKeyType(TPositionHandle pos, const TVector<const TTypeAnnotationNode*>& left,
+ const TVector<const TTypeAnnotationNode*>& right, TExprContext& ctx)
+{
+ YQL_ENSURE(left.size() == right.size());
+ TVector<const TTypeAnnotationNode*> ret;
+ ret.reserve(left.size());
+ for (size_t i = 0; i < left.size(); ++i) {
+ ret.push_back(UnifyJoinKeyType(pos, { left[i], right[i] }, ctx));
+ }
+
+ return ret;
+}
+
+namespace {
+bool NeedSkipNulls(const TTypeAnnotationNode& keyType, const TTypeAnnotationNode& unifiedKeyType) {
+ if (keyType.HasOptionalOrNull()) {
+ return true;
+ }
+
+ auto options = CastResult<true>(&keyType, &unifiedKeyType);
+ YQL_ENSURE(!(options & NKikimr::NUdf::ECastOptions::Impossible));
+ return options & NKikimr::NUdf::ECastOptions::MayFail;
+}
+
+}
+
+TExprNode::TPtr RemapNonConvertibleItems(const TExprNode::TPtr& input, const TJoinLabel& label,
+ const TExprNode& keys, const TVector<const TTypeAnnotationNode*>& unifiedKeyTypes,
+ TExprNode::TListType& columnNodes, TExprNode::TListType& columnNodesForSkipNull, TExprContext& ctx) {
+
+ YQL_ENSURE(keys.ChildrenSize() % 2 == 0);
+ YQL_ENSURE(keys.ChildrenSize() > 0);
+
+ auto result = input;
+ auto keysCount = keys.ChildrenSize() / 2;
+
+ size_t unifiedKeysCount = unifiedKeyTypes.size();
+ YQL_ENSURE(keysCount == unifiedKeysCount);
+
+ columnNodes.clear();
+ columnNodesForSkipNull.clear();
+ columnNodes.reserve(keysCount);
+
+ for (ui32 i = 0; i < keysCount; ++i) {
+ const TTypeAnnotationNode* unifiedType = unifiedKeyTypes[i];
+
+ auto tableName = keys.Child(2 * i)->Content();
+ auto column = keys.Child(2 * i + 1)->Content();
+ auto memberName = label.MemberName(tableName, column);
+ const TTypeAnnotationNode* inputType = *label.FindColumn(tableName, column);
+
+ auto arg = ctx.NewArgument(input->Pos(), "arg");
+ auto columnValue = ctx.Builder(input->Pos())
+ .Callable("Member")
+ .Add(0, arg)
+ .Atom(1, memberName)
+ .Seal()
+ .Build();
+
+ auto remapped = RemapNonConvertibleMemberForJoin(input->Pos(), columnValue, *inputType, *unifiedType, ctx);
+ if (remapped != columnValue) {
+ auto newColumn = TStringBuilder() << "_yql_ej_convert_column_" << i;
+ TString newMemberName = label.MemberName(tableName, newColumn);
+
+ auto lambdaBody = ctx.Builder(input->Pos())
+ .Callable("AddMember")
+ .Add(0, arg)
+ .Atom(1, newMemberName)
+ .Add(2, std::move(remapped))
+ .Seal()
+ .Build();
+
+ auto lambda = ctx.NewLambda(input->Pos(), ctx.NewArguments(input->Pos(), {std::move(arg)}), std::move(lambdaBody));
+
+ result = ctx.Builder(input->Pos())
+ .Callable("Map")
+ .Add(0, result)
+ .Add(1, lambda)
+ .Seal()
+ .Build();
+
+ columnNodes.push_back(ctx.NewAtom(input->Pos(), newMemberName));
+ if (NeedSkipNulls(*inputType, *unifiedType)) {
+ columnNodesForSkipNull.push_back(columnNodes.back());
+ }
+ } else {
+ columnNodes.push_back(ctx.NewAtom(input->Pos(), memberName));
+ }
+ }
+
+ return result;
+}
+
+// generate Map/Reduce transformations which filter out null keys for each optional and non-convertible key
+// TODO: merge common code with MakeCommonJoinCoreReduceLambda
+TCommonJoinCoreLambdas MakeCommonJoinCoreLambdas(TPositionHandle pos, TExprContext& ctx, const TJoinLabel& label,
+ const TJoinLabel& otherLabel, const TVector<const TTypeAnnotationNode*>& outputKeyType,
+ const TExprNode& keyColumnsNode, TStringBuf joinType,
+ const TStructExprType* myOutputSchemeType, const TStructExprType* otherOutputSchemeType,
+ ui32 tableIndex, bool useSortedReduce, ui32 sortIndex,
+ const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ bool myData, bool otherData, const TVector<TString>& ytReduceByColumns)
+{
+ TCommonJoinCoreLambdas result;
+ auto arg = ctx.NewArgument(pos, "item");
+
+ TVector<const TItemExprType*> items;
+ TExprNode::TListType outStructItems;
+ TVector<const TTypeAnnotationNode*> inputKeyType;
+
+ if (joinType != "Cross") {
+ YQL_ENSURE(ytReduceByColumns.size() == outputKeyType.size());
+ for (ui32 i = 0; i < outputKeyType.size(); ++i) {
+ const auto inputTable = keyColumnsNode.Child(2 * i)->Content();
+ const auto columnName = keyColumnsNode.Child(2 * i + 1)->Content();
+
+ const TString memberName = label.MemberName(inputTable, columnName);
+
+ auto keyNode = ctx.NewCallable(pos, "Member", { arg, ctx.NewAtom(pos, memberName) });
+
+ const auto fullName = FullColumnName(inputTable, columnName);
+ const auto originalType = *label.FindColumn(inputTable, columnName);
+ inputKeyType.push_back(originalType);
+
+ auto targetType = outputKeyType[i];
+ keyNode = RemapNonConvertibleMemberForJoin(pos, keyNode, *originalType, *targetType, ctx);
+
+ auto keyItem = ctx.Builder(pos)
+ .List()
+ .Atom(0, ytReduceByColumns[i])
+ .Add(1, keyNode)
+ .Seal().Build();
+ outStructItems.emplace_back(std::move(keyItem));
+ items.emplace_back(ctx.MakeType<TItemExprType>(ytReduceByColumns[i], targetType));
+ }
+ } else {
+ YQL_ENSURE(ytReduceByColumns.size() == 1);
+ auto keyNode = ctx.Builder(pos)
+ .List()
+ .Atom(0, ytReduceByColumns.front())
+ .Callable(1, "Uint32")
+ .Atom(0, "0", TNodeFlags::Default)
+ .Seal()
+ .Seal().Build();
+ outStructItems.emplace_back(std::move(keyNode));
+ items.emplace_back(ctx.MakeType<TItemExprType>(ytReduceByColumns.front(), ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+ }
+
+ if (useSortedReduce) {
+ outStructItems.emplace_back(
+ ctx.Builder(pos)
+ .List()
+ .Atom(0, "_yql_sort")
+ .Callable(1, "Uint32")
+ .Atom(0, ToString(sortIndex), TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Build());
+ items.emplace_back(ctx.MakeType<TItemExprType>("_yql_sort", ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+ }
+
+ // add payload
+ {
+ YQL_ENSURE(tableIndex <= 1);
+ TTypeAnnotationNode::TListType variantItems(2);
+
+ variantItems[tableIndex] = (const TTypeAnnotationNode*)label.InputType;
+ variantItems[1 - tableIndex] = otherLabel.InputType;
+
+ auto variantType = ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(std::move(variantItems)));
+
+ outStructItems.emplace_back(
+ ctx.Builder(pos)
+ .List()
+ .Atom(0, "_yql_join_payload")
+ .Callable(1, "Variant")
+ .Add(0, arg)
+ .Atom(1, ToString(tableIndex), TNodeFlags::Default)
+ .Add(2, ExpandType(pos, *variantType, ctx))
+ .Seal()
+ .Seal()
+ .Build());
+ }
+
+ TExprNodeList filterKeyColumns;
+ if (joinType == "Inner" || (joinType == (tableIndex == 0 ? "Right" : "Left")) || joinType == "LeftSemi"
+ || (joinType == (tableIndex == 0 ? "RightOnly" : "LeftOnly")) || joinType == "RightSemi")
+ {
+ YQL_ENSURE(ytReduceByColumns.size() == outputKeyType.size());
+ YQL_ENSURE(inputKeyType.size() == outputKeyType.size());
+ for (size_t i = 0; i < ytReduceByColumns.size(); ++i) {
+ if (NeedSkipNulls(*inputKeyType[i], *outputKeyType[i])) {
+ filterKeyColumns.push_back(ctx.NewAtom(pos, ytReduceByColumns[i]));
+ }
+ }
+ }
+
+ auto body = ctx.NewCallable(pos, "Just", { ctx.NewCallable(pos, "AsStruct", std::move(outStructItems)) });
+ if (!filterKeyColumns.empty()) {
+ body = ctx.NewCallable(pos, "SkipNullMembers", { body, ctx.NewList(pos, std::move(filterKeyColumns)) });
+ }
+
+ result.MapLambda = ctx.NewLambda(pos, ctx.NewArguments(pos, { std::move(arg) }), std::move(body));
+
+ arg = ctx.NewArgument(pos, "item");
+
+ // fill myRow using arg
+ TExprNode::TListType outValueItems;
+ auto& myRowItems = myOutputSchemeType->GetItems();
+ if (myData) {
+ for (auto item : myRowItems) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto inputTable = label.TableName(item->GetName());
+ auto columnName = label.ColumnName(item->GetName());
+ TString memberName = label.AddLabel ? label.MemberName(inputTable, columnName) : TString(item->GetName());
+
+ auto pair = ctx.Builder(pos)
+ .List()
+ .Atom(0, item->GetName())
+ .Callable(1, "Member")
+ .Callable(0, "Just")
+ .Add(0, arg)
+ .Seal()
+ .Atom(1, memberName)
+ .Seal()
+ .Seal()
+ .Build();
+ outValueItems.push_back(pair);
+ items.emplace_back(ctx.MakeType<TItemExprType>(item->GetName(), item->GetItemType()->IsOptionalOrNull() ? item->GetItemType() : ctx.MakeType<TOptionalExprType>(item->GetItemType())));
+ }
+ }
+
+ if (otherData) {
+ auto& otherRowItems = otherOutputSchemeType->GetItems();
+ for (auto item : otherRowItems) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto columnType = item->GetItemType();
+ if (!columnType->IsOptionalOrNull()) {
+ columnType = ctx.MakeType<TOptionalExprType>(columnType);
+ }
+
+ auto pair = ctx.Builder(pos)
+ .List()
+ .Atom(0, item->GetName())
+ .Callable(1, "Nothing")
+ .Add(0, ExpandType(pos, *columnType, ctx))
+ .Seal()
+ .Seal().Build();
+ outValueItems.emplace_back(std::move(pair));
+ items.emplace_back(ctx.MakeType<TItemExprType>(item->GetName(), item->GetItemType()->IsOptionalOrNull() ? item->GetItemType() : ctx.MakeType<TOptionalExprType>(item->GetItemType())));
+ }
+ }
+
+ auto tableIndexNode = ctx.Builder(pos)
+ .List()
+ .Atom(0, "_yql_table_index")
+ .Callable(1, "Uint32")
+ .Atom(0, ToString(tableIndex), TNodeFlags::Default)
+ .Seal()
+ .Seal().Build();
+ outValueItems.emplace_back(std::move(tableIndexNode));
+ items.emplace_back(ctx.MakeType<TItemExprType>("_yql_table_index", ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+
+ result.ReduceLambda = ctx.NewLambda(pos, ctx.NewArguments(pos, { std::move(arg) }),
+ ctx.NewCallable(pos, "AsStruct", std::move(outValueItems)));
+ result.CommonJoinCoreInputType = ctx.MakeType<TStructExprType>(items);
+ return result;
+}
+
+TExprNode::TPtr PrepareForCommonJoinCore(TPositionHandle pos, TExprContext& ctx, const TExprNode::TPtr& input,
+ const TExprNode::TPtr& reduceLambdaZero, const TExprNode::TPtr& reduceLambdaOne)
+{
+ return ctx.Builder(pos)
+ .Callable("OrderedMap")
+ .Add(0, input)
+ .Lambda(1)
+ .Param("item")
+ .Callable("FlattenMembers")
+ .List(0)
+ .Atom(0, "")
+ .Callable(1, "RemoveMember")
+ .Arg(0, "item")
+ .Atom(1, "_yql_join_payload")
+ .Seal()
+ .Seal()
+ .List(1)
+ .Atom(0, "")
+ .Callable(1, "Visit")
+ .Callable(0, "Member")
+ .Arg(0, "item")
+ .Atom(1, "_yql_join_payload")
+ .Seal()
+ .Atom(1, "0", TNodeFlags::Default)
+ .Add(2, reduceLambdaZero)
+ .Atom(3, "1", TNodeFlags::Default)
+ .Add(4, reduceLambdaOne)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+}
+
+// generate Reduce-only transformations which filter out null keys for each optional and non-convertible key
+TCommonJoinCoreLambdas MakeCommonJoinCoreReduceLambda(TPositionHandle pos, TExprContext& ctx, const TJoinLabel& label,
+ const TVector<const TTypeAnnotationNode*>& outputKeyType, const TExprNode& keyColumnsNode,
+ TStringBuf joinType, const TStructExprType* myOutputSchemeType, const TStructExprType* otherOutputSchemeType,
+ ui32 tableIndex, bool useSortedReduce, ui32 sortIndex,
+ const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ bool myData, bool otherData, const TVector<TString>& ytReduceByColumns)
+{
+
+ TExprNode::TListType keyNodes;
+ TVector<std::pair<TStringBuf, TStringBuf>> inputKeyColumns;
+ TVector<const TItemExprType*> items;
+ TVector<const TTypeAnnotationNode*> keyTypes;
+
+ TTypeAnnotationNode::TListType outputKeyColumnsTypes;
+ if (joinType != "Cross") {
+ for (ui32 i = 0; i < outputKeyType.size(); ++i) {
+ inputKeyColumns.push_back({ keyColumnsNode.Child(2 * i)->Content(),
+ keyColumnsNode.Child(2 * i + 1)->Content() });
+
+ outputKeyColumnsTypes.push_back(outputKeyType[i]);
+ }
+ }
+
+ auto arg = ctx.NewArgument(pos, "item");
+ for (ui32 i = 0; i < inputKeyColumns.size(); ++i) {
+ const auto& inputTable = inputKeyColumns[i].first;
+ const auto& columnName = inputKeyColumns[i].second;
+ const TString memberName = label.MemberName(inputTable, columnName);
+
+ keyNodes.emplace_back(ctx.NewCallable(pos, "Member", { arg, ctx.NewAtom(pos, memberName) }));
+
+ auto& keyNode = keyNodes.back();
+ const auto fullName = FullColumnName(inputTable, columnName);
+ const auto originalType = *label.FindColumn(inputTable, columnName);
+
+ auto targetType = outputKeyColumnsTypes[i];
+ if (!targetType->IsOptionalOrNull()) {
+ targetType = ctx.MakeType<TOptionalExprType>(targetType);
+ }
+ keyNode = RemapNonConvertibleMemberForJoin(pos, keyNode, *originalType, *targetType, ctx);
+ keyTypes.emplace_back(targetType);
+ }
+
+ // fill myRow using arg
+ TExprNode::TListType outValueItems;
+ auto& myRowItems = myOutputSchemeType->GetItems();
+ if (myData) {
+ for (auto item : myRowItems) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto inputTable = label.TableName(item->GetName());
+ auto columnName = label.ColumnName(item->GetName());
+ TString memberName = label.AddLabel ? label.MemberName(inputTable, columnName) : TString(item->GetName());
+ auto columnValue = ctx.Builder(pos)
+ .Callable("Member")
+ .Add(0, arg)
+ .Atom(1, memberName)
+ .Seal()
+ .Build();
+
+ if (!item->GetItemType()->IsOptionalOrNull()) {
+ columnValue = ctx.NewCallable(pos, "Just", { columnValue });
+ }
+
+ auto pair = ctx.Builder(pos)
+ .List()
+ .Atom(0, item->GetName())
+ .Add(1, columnValue)
+ .Seal()
+ .Build();
+ outValueItems.push_back(pair);
+ items.emplace_back(ctx.MakeType<TItemExprType>(item->GetName(), item->GetItemType()->IsOptionalOrNull() ? item->GetItemType() : ctx.MakeType<TOptionalExprType>(item->GetItemType())));
+ }
+ }
+
+ if (otherData) {
+ auto& otherRowItems = otherOutputSchemeType->GetItems();
+ for (auto item : otherRowItems) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto columnType = item->GetItemType();
+ if (!columnType->IsOptionalOrNull()) {
+ columnType = ctx.MakeType<TOptionalExprType>(columnType);
+ }
+
+ auto pair = ctx.Builder(pos)
+ .List()
+ .Atom(0, item->GetName())
+ .Callable(1, "Nothing")
+ .Add(0, ExpandType(pos, *columnType, ctx))
+ .Seal()
+ .Seal().Build();
+ outValueItems.emplace_back(std::move(pair));
+ items.emplace_back(ctx.MakeType<TItemExprType>(item->GetName(), item->GetItemType()->IsOptionalOrNull() ? item->GetItemType() : ctx.MakeType<TOptionalExprType>(item->GetItemType())));
+ }
+ }
+
+ auto tableIndexNode = ctx.Builder(pos)
+ .List()
+ .Atom(0, "_yql_table_index")
+ .Callable(1, "Uint32")
+ .Atom(0, ToString(tableIndex), TNodeFlags::Default)
+ .Seal()
+ .Seal().Build();
+ outValueItems.emplace_back(std::move(tableIndexNode));
+ items.emplace_back(ctx.MakeType<TItemExprType>("_yql_table_index", ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+
+ TExprNode::TListType outStructItems;
+ outStructItems = outValueItems;
+
+ if (useSortedReduce) {
+ outStructItems.emplace_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, "_yql_sort")
+ .Callable(1, "Uint32")
+ .Atom(0, ToString(sortIndex), TNodeFlags::Default)
+ .Seal()
+ .Seal().Build());
+ items.emplace_back(ctx.MakeType<TItemExprType>("_yql_sort", ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+ }
+
+ // fill keys in out struct
+ if (joinType == "Cross") {
+ auto keyNode = ctx.Builder(pos)
+ .List()
+ .Atom(0, ytReduceByColumns.front())
+ .Callable(1, "Uint32")
+ .Atom(0, "0", TNodeFlags::Default)
+ .Seal()
+ .Seal().Build();
+ outStructItems.emplace_back(std::move(keyNode));
+ items.emplace_back(ctx.MakeType<TItemExprType>(ytReduceByColumns.front(), ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+ } else {
+ for (ui32 i = 0; i < keyNodes.size(); ++i) {
+ auto keyNode = ctx.Builder(pos)
+ .List()
+ .Atom(0, ytReduceByColumns[i])
+ .Add(1, keyNodes[i])
+ .Seal().Build();
+ outStructItems.emplace_back(std::move(keyNode));
+ items.emplace_back(ctx.MakeType<TItemExprType>(ytReduceByColumns[i], keyTypes[i]));
+ }
+ }
+
+ TExprNodeList filterKeyColumns;
+
+ if (joinType == "Inner" || (joinType == (tableIndex == 0 ? "Right" : "Left")) || joinType == "LeftSemi"
+ || (joinType == (tableIndex == 0 ? "RightOnly" : "LeftOnly")) || joinType == "RightSemi")
+ {
+ for (const auto &keyCol : ytReduceByColumns) {
+ filterKeyColumns.push_back(ctx.NewAtom(pos, keyCol));
+ }
+ }
+
+ auto body = ctx.Builder(pos)
+ .Callable("SkipNullMembers")
+ .Callable(0, "Just")
+ .Add(0, ctx.NewCallable(pos, "AsStruct", std::move(outStructItems)))
+ .Seal()
+ .Add(1, ctx.NewList(pos, std::move(filterKeyColumns)))
+ .Seal()
+ .Build();
+
+ TCommonJoinCoreLambdas result;
+ result.ReduceLambda = ctx.NewLambda(pos, ctx.NewArguments(pos, { std::move(arg) }), std::move(body));
+ result.CommonJoinCoreInputType = ctx.MakeType<TStructExprType>(items);
+ return result;
+}
+
+void AddJoinRemappedColumn(TPositionHandle pos, const TExprNode::TPtr& pairArg, TExprNode::TListType& joinedBodyChildren,
+ TStringBuf name, TStringBuf newName, TExprContext& ctx)
+{
+ auto member = ctx.Builder(pos)
+ .Callable("Member")
+ .Add(0, pairArg)
+ .Atom(1, name)
+ .Seal()
+ .Build();
+
+ joinedBodyChildren.emplace_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, newName)
+ .Add(1, std::move(member))
+ .Seal()
+ .Build());
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/opt/yql_yt_join.h b/ydb/library/yql/providers/yt/opt/yql_yt_join.h
new file mode 100644
index 0000000000..9900d87c5d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/yql_yt_join.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_join.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/ast/yql_expr_builder.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+#include <util/generic/set.h>
+
+namespace NYql {
+
+enum class TChoice : ui8 {
+ None = 0,
+ Left,
+ Right,
+ Both,
+};
+
+TChoice Invert(TChoice choice);
+bool IsLeftOrRight(TChoice choice);
+TChoice Merge(TChoice a, TChoice b);
+
+struct TMapJoinSettings {
+ ui64 MapJoinLimit = 0;
+ ui64 MapJoinShardMinRows = 1;
+ ui64 MapJoinShardCount = 1;
+ bool SwapTables = false;
+ ui64 LeftRows = 0;
+ ui64 RightRows = 0;
+ ui64 LeftSize = 0;
+ ui64 RightSize = 0;
+ ui64 LeftMemSize = 0;
+ ui64 RightMemSize = 0;
+ bool LeftUnique = false;
+ bool RightUnique = false;
+ ui64 LeftCount = 0;
+ ui64 RightCount = 0;
+
+ TMaybe<ui64> CalculatePartSize(ui64 rows) const;
+};
+
+THashSet<TString> BuildJoinKeys(const TJoinLabel& label, const TExprNode& keys);
+TVector<TString> BuildJoinKeyList(const TJoinLabel& label, const TExprNode& keys);
+TVector<const TTypeAnnotationNode*> BuildJoinKeyType(const TJoinLabel& label, const TExprNode& keys);
+TMap<TString, const TTypeAnnotationNode*> BuildJoinKeyTypeMap(const TJoinLabel& label, const TExprNode& keys);
+TVector<const TTypeAnnotationNode*> RemoveNullsFromJoinKeyType(const TVector<const TTypeAnnotationNode*>& inputKeyType);
+const TTypeAnnotationNode* AsDictKeyType(const TVector<const TTypeAnnotationNode*>& inputKeyType, TExprContext& ctx);
+void SwapJoinType(TPositionHandle pos, TExprNode::TPtr& joinType, TExprContext& ctx);
+const TStructExprType* MakeOutputJoinColumns(const THashMap<TString, const TTypeAnnotationNode*>& columnTypes,
+ const TJoinLabel& label, TExprContext& ctx);
+const TTypeAnnotationNode* UnifyJoinKeyType(TPositionHandle pos, const TVector<const TTypeAnnotationNode*>& types, TExprContext& ctx);
+TVector<const TTypeAnnotationNode*> UnifyJoinKeyType(TPositionHandle pos, const TVector<const TTypeAnnotationNode*>& left,
+ const TVector<const TTypeAnnotationNode*>& right, TExprContext& ctx);
+TExprNode::TPtr RemapNonConvertibleItems(const TExprNode::TPtr& input, const TJoinLabel& label,
+ const TExprNode& keys, const TVector<const TTypeAnnotationNode*>& unifiedKeyTypes,
+ TExprNode::TListType& columnNodes, TExprNode::TListType& columnNodesForSkipNull, TExprContext& ctx);
+
+struct TCommonJoinCoreLambdas {
+ TExprNode::TPtr MapLambda;
+ TExprNode::TPtr ReduceLambda;
+ const TTypeAnnotationNode* CommonJoinCoreInputType = nullptr;
+};
+
+TCommonJoinCoreLambdas MakeCommonJoinCoreLambdas(TPositionHandle pos, TExprContext& ctx, const TJoinLabel& label,
+ const TJoinLabel& otherLabel, const TVector<const TTypeAnnotationNode*>& outputKeyType,
+ const TExprNode& keyColumnsNode, TStringBuf joinType,
+ const TStructExprType* myOutputSchemeType, const TStructExprType* otherOutputSchemeType,
+ ui32 tableIndex, bool useSortedReduce, ui32 sortIndex,
+ const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ bool myData, bool otherData, const TVector<TString>& ytReduceByColumns);
+TExprNode::TPtr PrepareForCommonJoinCore(TPositionHandle pos, TExprContext& ctx, const TExprNode::TPtr& input,
+ const TExprNode::TPtr& reduceLambdaZero, const TExprNode::TPtr& reduceLambdaOne);
+
+
+TCommonJoinCoreLambdas MakeCommonJoinCoreReduceLambda(TPositionHandle pos, TExprContext& ctx, const TJoinLabel& label,
+ const TVector<const TTypeAnnotationNode*>& outputKeyType,
+ const TExprNode& keyColumnsNode, TStringBuf joinType,
+ const TStructExprType* myOutputSchemeType, const TStructExprType* otherOutputSchemeType,
+ ui32 tableIndex, bool useSortedReduce, ui32 sortIndex,
+ const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ bool myData, bool otherData, const TVector<TString>& ytReduceByColumns);
+
+void AddJoinRemappedColumn(TPositionHandle pos, const TExprNode::TPtr& pairArg, TExprNode::TListType& joinedBodyChildren,
+ TStringBuf name, TStringBuf newName, TExprContext& ctx);
+
+}
diff --git a/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp b/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
new file mode 100644
index 0000000000..e1a8745706
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.cpp
@@ -0,0 +1,327 @@
+#include "yql_yt_key_selector.h"
+
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+
+#include <util/generic/strbuf.h>
+#include <util/string/type.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+static TMaybeNode<TCoLambda> GetEqualVisitKeyExtractorLambda(const TCoLambda& lambda) {
+ const auto members = GetCommonKeysFromVariantSelector(lambda);
+ if (!members.empty()) {
+ auto maybeVisit = lambda.Body().Maybe<TCoVisit>();
+ return TCoLambda(maybeVisit.Raw()->Child(2));
+ }
+ return {};
+}
+
+TKeySelectorBuilder::TKeySelectorBuilder(TPositionHandle pos, TExprContext& ctx, bool useNativeDescSort, const TTypeAnnotationNode* itemType)
+ : Pos_(pos)
+ , Ctx_(ctx)
+ , Arg_(Build<TCoArgument>(ctx, pos).Name("item").Done().Ptr())
+ , LambdaBody_(Arg_)
+ , NonStructInput(itemType && itemType->GetKind() != ETypeAnnotationKind::Struct)
+ , UseNativeDescSort(useNativeDescSort)
+{
+ if (itemType) {
+ if (itemType->GetKind() == ETypeAnnotationKind::Struct) {
+ StructType = itemType->Cast<TStructExprType>();
+ FieldTypes_ = StructType->GetItems();
+ }
+ else {
+ FieldTypes_.push_back(ctx.MakeType<TItemExprType>("_yql_original_row", itemType));
+ LambdaBody_ = Build<TCoAsStruct>(Ctx_, Pos_)
+ .Add<TExprList>()
+ .Add<TCoAtom>()
+ .Value("_yql_original_row")
+ .Build()
+ .Add(Arg_)
+ .Build()
+ .Done()
+ .Ptr();
+ }
+ }
+}
+
+void TKeySelectorBuilder::ProcessKeySelector(const TExprNode::TPtr& keySelectorLambda, const TExprNode::TPtr& sortDirections) {
+ auto lambda = TCoLambda(keySelectorLambda);
+ if (auto maybeLambda = GetEqualVisitKeyExtractorLambda(lambda)) {
+ lambda = maybeLambda.Cast();
+ }
+
+ auto keySelectorBody = lambda.Body();
+ auto keySelectorArg = lambda.Args().Arg(0).Ptr();
+
+ const bool allAscending = !sortDirections;
+ const bool multiKey = sortDirections
+ ? (sortDirections->IsList() && sortDirections->ChildrenSize() != 1)
+ : keySelectorBody.Maybe<TExprList>() || keySelectorBody.Ref().GetTypeAnn()->GetKind() == ETypeAnnotationKind::Tuple;
+
+ if (multiKey) {
+ if (keySelectorBody.Maybe<TExprList>()) {
+ auto columnList = keySelectorBody.Cast<TExprList>();
+ for (size_t i = 0; i < columnList.Size(); ++i) {
+ AddColumn<false, false>(keySelectorLambda,
+ columnList.Item(i).Ptr(),
+ allAscending || IsTrue(TExprList(sortDirections).Item(i).Cast<TCoBool>().Literal().Value()),
+ i,
+ keySelectorArg);
+ }
+ } else {
+ size_t tupleSize = keySelectorBody.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().size();
+ for (size_t i = 0; i < tupleSize; ++i) {
+ AddColumn<true, false>(keySelectorLambda,
+ keySelectorBody.Ptr(),
+ allAscending || IsTrue(TExprList(sortDirections).Item(i).Cast<TCoBool>().Literal().Value()),
+ i,
+ keySelectorArg);
+ }
+ }
+ } else {
+ const bool ascending = allAscending || (
+ sortDirections->IsList()
+ ? IsTrue(TExprList(sortDirections).Item(0).Cast<TCoBool>().Literal().Value())
+ : IsTrue(TCoBool(sortDirections).Literal().Value())
+ );
+ AddColumn<false, true>(keySelectorLambda,
+ keySelectorBody.Ptr(),
+ ascending,
+ 0,
+ keySelectorArg);
+ }
+}
+
+void TKeySelectorBuilder::ProcessConstraint(const TSortedConstraintNode& sortConstraint) {
+ YQL_ENSURE(StructType);
+ for (const auto& item : sortConstraint.GetContent()) {
+ bool good = false;
+ for (const auto& path : item.first) {
+ if (path.size() == 1U) {
+ const auto& column = path.front();
+ const auto pos = StructType->FindItem(column);
+ YQL_ENSURE(pos, "Column " << column << " is missing in struct type");
+ AddColumn(column, StructType->GetItems()[*pos]->GetItemType(), item.second);
+ good = true;
+ break;
+ }
+ }
+ if (!good)
+ break;
+ }
+}
+
+void TKeySelectorBuilder::ProcessRowSpec(const TYqlRowSpecInfo& rowSpec) {
+ auto& columns = rowSpec.SortMembers;
+ auto& dirs = rowSpec.SortDirections;
+ YQL_ENSURE(columns.size() <= dirs.size());
+ YQL_ENSURE(StructType);
+ for (size_t i = 0; i < columns.size(); ++i) {
+ auto pos = StructType->FindItem(columns[i]);
+ YQL_ENSURE(pos, "Column " << columns[i] << " is missing in struct type");
+ AddColumn(columns[i], StructType->GetItems()[*pos]->GetItemType(), dirs[i]);
+ }
+}
+
+TExprNode::TPtr TKeySelectorBuilder::MakeRemapLambda(bool ordered) const {
+ return Build<TCoLambda>(Ctx_, Pos_)
+ .Args({TStringBuf("stream")})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input(TStringBuf("stream"))
+ .Lambda()
+ .Args(TCoArgument(Arg_))
+ .Body<TCoJust>()
+ .Input(LambdaBody_)
+ .Build()
+ .Build()
+ .Build()
+ .Done()
+ .Ptr();
+}
+
+template <bool ComputedTuple, bool SingleColumn>
+void TKeySelectorBuilder::AddColumn(const TExprNode::TPtr& rootLambda, const TExprNode::TPtr& keyNode, bool ascending, size_t columnIndex, const TExprNode::TPtr& structArg) {
+ const TTypeAnnotationNode* columnType = nullptr;
+ if (ComputedTuple) {
+ columnType = keyNode->GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[columnIndex];
+ } else {
+ columnType = keyNode->GetTypeAnn();
+ }
+
+ auto presortColumnType = columnType;
+ bool needPresort = false;
+ if (ascending) {
+ needPresort = RemoveOptionalType(columnType)->GetKind() != ETypeAnnotationKind::Data;
+ } else {
+ needPresort = true;
+ }
+
+ if (needPresort) {
+ presortColumnType = Ctx_.MakeType<TDataExprType>(EDataSlot::String);
+ }
+
+ auto maybeMember = TMaybeNode<TCoMember>(keyNode);
+ if (!ComputedTuple && maybeMember
+ && (maybeMember.Cast().Struct().Raw() == structArg.Get()
+ || maybeMember.Cast().Struct().Maybe<TCoVariantItem>().Variant().Raw() == structArg.Get()))
+ {
+ auto memberName = TString{maybeMember.Cast().Name().Value()};
+ if (!HasComputedColumn_) {
+ Members_.push_back(memberName);
+ }
+ if (!UniqMemberColumns_.insert(memberName).second) {
+ return;
+ }
+ // Reset descending presort only for non-computed fields
+ if (!ascending && UseNativeDescSort && RemoveOptionalType(columnType)->GetKind() == ETypeAnnotationKind::Data) {
+ needPresort = false;
+ }
+
+ if (!needPresort) {
+ Columns_.push_back(memberName);
+ ColumnTypes_.push_back(columnType);
+ SortDirections_.push_back(ascending);
+ ForeignSortDirections_.push_back(ascending);
+ if (NonStructInput) {
+ FieldTypes_.push_back(Ctx_.MakeType<TItemExprType>(memberName, columnType));
+ auto key = TExprBase(Ctx_.ReplaceNode(rootLambda->TailPtr(), rootLambda->Head().Head(), Arg_));
+ if (!SingleColumn && rootLambda->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Tuple) {
+ key = Build<TCoNth>(Ctx_, Pos_)
+ .Tuple(key)
+ .Index().Value(ToString(columnIndex)).Build()
+ .Done();
+ }
+ LambdaBody_ = Build<TCoAddMember>(Ctx_, Pos_)
+ .Struct(LambdaBody_)
+ .Name().Value(memberName).Build()
+ .Item(key)
+ .Done()
+ .Ptr();
+ }
+ return;
+ }
+ } else {
+ HasComputedColumn_ = true;
+ }
+
+ NeedMap_ = true;
+
+ auto key = TExprBase(Ctx_.ReplaceNode(rootLambda->TailPtr(), rootLambda->Head().Head(), Arg_));
+ if (ComputedTuple || (!SingleColumn && rootLambda->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Tuple)) {
+ key = Build<TCoNth>(Ctx_, Pos_)
+ .Tuple(key)
+ .Index().Value(ToString(columnIndex)).Build()
+ .Done();
+ }
+ if (needPresort) {
+ if (ascending) {
+ key = Build<TCoAscending>(Ctx_, Pos_)
+ .Input(key)
+ .Done();
+ } else {
+ key = Build<TCoDescending>(Ctx_, Pos_)
+ .Input(key)
+ .Done();
+ }
+ columnType = presortColumnType;
+ }
+
+ TString column = TStringBuilder() << "_yql_column_" << Index_++;
+ Columns_.push_back(column);
+ ColumnTypes_.push_back(columnType);
+ SortDirections_.push_back(ascending);
+ ForeignSortDirections_.push_back(true);
+ FieldTypes_.push_back(Ctx_.MakeType<TItemExprType>(column, presortColumnType));
+
+ LambdaBody_ = Build<TCoAddMember>(Ctx_, Pos_)
+ .Struct(LambdaBody_)
+ .Name().Value(column).Build()
+ .Item(key)
+ .Done()
+ .Ptr();
+}
+
+void TKeySelectorBuilder::AddColumn(const TStringBuf memberName, const TTypeAnnotationNode* columnType, bool ascending) {
+ auto presortColumnType = columnType;
+ bool needPresort = false;
+ if (ascending) {
+ needPresort = RemoveOptionalType(columnType)->GetKind() != ETypeAnnotationKind::Data;
+ } else {
+ needPresort = !UseNativeDescSort || RemoveOptionalType(columnType)->GetKind() != ETypeAnnotationKind::Data;
+ }
+
+ if (needPresort) {
+ presortColumnType = Ctx_.MakeType<TDataExprType>(EDataSlot::String);
+ }
+
+ Members_.emplace_back(memberName);
+ if (!UniqMemberColumns_.emplace(memberName).second) {
+ return;
+ }
+ if (!needPresort) {
+ Columns_.emplace_back(memberName);
+ ColumnTypes_.push_back(columnType);
+ SortDirections_.push_back(ascending);
+ ForeignSortDirections_.push_back(ascending);
+ return;
+ }
+
+ NeedMap_ = true;
+
+ TExprBase key = Build<TCoMember>(Ctx_, Pos_)
+ .Struct(Arg_)
+ .Name()
+ .Value(memberName)
+ .Build()
+ .Done();
+
+ if (needPresort) {
+ if (ascending) {
+ key = Build<TCoAscending>(Ctx_, Pos_)
+ .Input(key)
+ .Done();
+ } else {
+ key = Build<TCoDescending>(Ctx_, Pos_)
+ .Input(key)
+ .Done();
+ }
+ columnType = presortColumnType;
+ }
+
+ TString column = TStringBuilder() << "_yql_column_" << Index_++;
+ Columns_.push_back(column);
+ ColumnTypes_.push_back(columnType);
+ SortDirections_.push_back(ascending);
+ ForeignSortDirections_.push_back(true);
+ FieldTypes_.push_back(Ctx_.MakeType<TItemExprType>(column, presortColumnType));
+
+ LambdaBody_ = Build<TCoAddMember>(Ctx_, Pos_)
+ .Struct(LambdaBody_)
+ .Name().Value(column).Build()
+ .Item(key)
+ .Done()
+ .Ptr();
+}
+
+TVector<std::pair<TString, bool>> TKeySelectorBuilder::ForeignSortColumns() const {
+ TVector<std::pair<TString, bool>> res;
+ YQL_ENSURE(ForeignSortDirections_.size() == Columns_.size());
+ for (size_t i = 0; i < Columns_.size(); ++i) {
+ res.emplace_back(Columns_[i], ForeignSortDirections_[i]);
+ }
+ return res;
+}
+
+void TKeySelectorBuilder::FillRowSpecSort(TYqlRowSpecInfo& rowSpec) {
+ rowSpec.SortMembers = Members_;
+ rowSpec.SortedBy = Columns_;
+ rowSpec.SortedByTypes = ColumnTypes_;
+ rowSpec.SortDirections = SortDirections_;
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h b/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h
new file mode 100644
index 0000000000..2cd4ae662c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/ast/yql_constraint.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/hash_set.h>
+
+namespace NYql {
+
+class TKeySelectorBuilder {
+public:
+ TKeySelectorBuilder(TPositionHandle pos, TExprContext& ctx, bool useNativeDescSort, const TTypeAnnotationNode* itemType = nullptr);
+
+ void ProcessKeySelector(const TExprNode::TPtr& keySelectorLambda, const TExprNode::TPtr& sortDirections = {});
+ void ProcessConstraint(const TSortedConstraintNode& sortConstraint);
+ void ProcessRowSpec(const TYqlRowSpecInfo& rowSpec);
+
+ bool NeedMap() const {
+ return NeedMap_;
+ }
+
+ TExprNode::TPtr MakeRemapLambda(bool ordered = false) const;
+
+ const TStructExprType* MakeRemapType() const {
+ return Ctx_.MakeType<TStructExprType>(FieldTypes_);
+ }
+
+ const TVector<bool>& SortDirections() const {
+ return SortDirections_;
+ }
+
+ const TVector<TString>& Columns() const {
+ return Columns_;
+ }
+
+ const TVector<TString>& Members() const {
+ return Members_;
+ }
+
+ TVector<std::pair<TString, bool>> ForeignSortColumns() const;
+
+ void FillRowSpecSort(TYqlRowSpecInfo& rowSpec);
+
+private:
+ template <bool ComputedTuple, bool SingleColumn>
+ void AddColumn(const TExprNode::TPtr& rootLambda, const TExprNode::TPtr& keyNode, bool ascending, size_t columnIndex, const TExprNode::TPtr& structArg);
+ void AddColumn(const TStringBuf memberName, const TTypeAnnotationNode* columnType, bool ascending);
+
+private:
+ TPositionHandle Pos_;
+ TExprContext& Ctx_;
+ const TStructExprType* StructType = nullptr;
+ TVector<const TItemExprType*> FieldTypes_;
+ bool NeedMap_ = false;
+ TExprNode::TPtr Arg_;
+ TExprNode::TPtr LambdaBody_;
+ TVector<bool> SortDirections_;
+ TVector<bool> ForeignSortDirections_;
+ TVector<TString> Columns_;
+ TVector<TString> Members_;
+ TTypeAnnotationNode::TListType ColumnTypes_;
+ bool HasComputedColumn_ = false;
+ ui32 Index_ = 0;
+ THashSet<TString> UniqMemberColumns_;
+ const bool NonStructInput;
+ const bool UseNativeDescSort;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/ut/ya.make b/ydb/library/yql/providers/yt/provider/ut/ya.make
new file mode 100644
index 0000000000..ad6164cd4b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ut/ya.make
@@ -0,0 +1,33 @@
+UNITTEST_FOR(ydb/library/yql/providers/yt/provider)
+
+SIZE(SMALL)
+
+SRCS(
+ yql_yt_dq_integration_ut.cpp
+ yql_yt_epoch_ut.cpp
+)
+
+PEERDIR(
+ ydb/library/yql/providers/yt/lib/schema
+ ydb/library/yql/providers/yt/provider
+ ydb/library/yql/providers/yt/gateway/file
+ ydb/library/yql/core/ut_common
+ ydb/library/yql/ast
+ ydb/library/yql/public/udf/service/terminate_policy
+ ydb/library/yql/core/services
+ ydb/library/yql/core
+ ydb/library/yql/providers/common/gateway
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/config
+ ydb/library/yql/providers/config
+ ydb/library/yql/providers/dq/common
+ ydb/library/yql/providers/dq/provider
+ ydb/library/yql/providers/result/provider
+ ydb/library/yql/sql/v1
+ ydb/library/yql/minikql/invoke_builtins/llvm
+ ydb/library/yql/sql/pg
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/library/yql/providers/yt/provider/ut/yql_yt_dq_integration_ut.cpp b/ydb/library/yql/providers/yt/provider/ut/yql_yt_dq_integration_ut.cpp
new file mode 100644
index 0000000000..fd02e155f0
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ut/yql_yt_dq_integration_ut.cpp
@@ -0,0 +1,126 @@
+#include <ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.h>
+#include <ydb/library/yql/providers/dq/common/yql_dq_settings.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYql;
+
+Y_UNIT_TEST_SUITE(TSchedulerTest) {
+ Y_UNIT_TEST(Ranges_table4_table5_test0) {
+ TYtState::TPtr state = new TYtState;
+ auto dqIntegration = CreateYtDqIntegration(state.Get());
+ TDqSettings settings;
+ settings.DataSizePerJob = 1000;
+ TVector<TString> partitions;
+ size_t maxTasks = 4;
+ auto astStr = "(\n"
+ "(let $4 (Void))\n"
+ "(let $5 (YtMeta '('\"CanWrite\" '\"1\") '('\"DoesExist\" '\"1\") '('\"YqlCompatibleScheme\" '\"1\") '('\"InferredScheme\" '\"0\") '('\"IsDynamic\" '\"0\") '('\"Attrs\" '('('\"optimize_for\" '\"lookup\")))))\n"
+ "(let $6 '('\"RecordsCount\" '\"100\"))\n"
+ "(let $7 '('\"DataSize\" '\"1284\"))\n"
+ "(let $8 '('\"ChunkCount\" '\"1\"))\n"
+ "(let $9 (YtStat '('\"Id\" '\"1dd1d-fb585-3f40191-36b851ee\") $6 $7 $8 '('\"ModifyTime\" '\"1614862780\") '('\"Revision\" '\"524591601530245\")))\n"
+ "(let $10 (YtTable '\"home/yql/test-table5\" $4 $5 $9 '() (Void) (Void) '\"freud\"))\n"
+ "(let $11 '('\"key\" '\"value\"))\n"
+ "(let $12 (YtPath $10 $11 (Void) (Void)))\n"
+ "(let $13 (YtStat '('\"Id\" '\"1dd1d-fb274-3f40191-5a564b01\") $6 $7 $8 '('\"ModifyTime\" '\"1614862777\") '('\"Revision\" '\"524591601529460\")))\n"
+ "(let $14 (YtTable '\"home/yql/test-table4\" $4 $5 $13 '() (Void) (Void) '\"freud\"))\n"
+ "(let $15 (YtPath $14 $11 (Void) (Void)))\n"
+ "(return (YtReadTable! world (DataSource '\"yt\" '\"freud\") '((YtSection '($12 $15) '('('\"unordered\"))))))\n"
+ ")\n";
+ auto astRes = ParseAst(astStr);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx_;
+ TExprNode::TPtr exprRoot_;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot_, exprCtx_, nullptr));
+ TString cluster;
+ const auto result = dqIntegration->Partition(settings, maxTasks, *exprRoot_, partitions, &cluster, exprCtx_, false);
+ const auto expected = 428;
+ UNIT_ASSERT_VALUES_EQUAL(result, expected);
+ UNIT_ASSERT_VALUES_EQUAL(partitions.size(), 3);
+ }
+
+ Y_UNIT_TEST(Ranges_table4_table7_test1) {
+ TYtState::TPtr state = new TYtState;
+ auto dqIntegration = CreateYtDqIntegration(state.Get());
+ TDqSettings settings;
+ settings.DataSizePerJob = 1000;
+ TVector<TString> partitions;
+ size_t maxTasks = 2;
+ auto astStr = "(\n"
+ "(let $4 (Void))\n"
+ "(let $5 (YtMeta '('\"CanWrite\" '\"1\") '('\"DoesExist\" '\"1\") '('\"YqlCompatibleScheme\" '\"1\") '('\"InferredScheme\" '\"0\") '('\"IsDynamic\" '\"0\") '('\"Attrs\" '('('\"optimize_for\" '\"lookup\")))))\n"
+ "(let $6 '('\"RecordsCount\" '\"100\"))\n"
+ "(let $7 '('\"DataSize\" '\"1284\"))\n"
+ "(let $8 '('\"ChunkCount\" '\"1\"))\n"
+ "(let $9 (YtStat '('\"Id\" '\"1dd1d-fb585-3f40191-36b851ee\") $6 $7 $8 '('\"ModifyTime\" '\"1614862780\") '('\"Revision\" '\"524591601530245\")))\n"
+ "(let $10 (YtTable '\"home/yql/test-table5\" $4 $5 $9 '() (Void) (Void) '\"freud\"))\n"
+ "(let $11 '('\"key\" '\"value\"))\n"
+ "(let $12 (YtPath $10 $11 (Void) (Void)))\n"
+ "(let $13 (YtStat '('\"Id\" '\"1dd1d-fb703-3f40191-46023c3c\") $6 $7 $8 '('\"ModifyTime\" '\"1614862781\") '('\"Revision\" '\"524591601530627\")))\n"
+ "(let $14 (YtTable '\"home/yql/test-table6\" $4 $5 $13 '() (Void) (Void) '\"freud\"))\n"
+ "(let $15 (YtPath $14 $11 (Void) (Void)))\n"
+ "(let $16 (YtStat '('\"Id\" '\"1dd1d-fb274-3f40191-5a564b01\") $6 $7 $8 '('\"ModifyTime\" '\"1614862777\") '('\"Revision\" '\"524591601529460\")))\n"
+ "(let $17 (YtTable '\"home/yql/test-table4\" $4 $5 $16 '() (Void) (Void) '\"freud\"))\n"
+ "(let $18 (YtPath $17 $11 (Void) (Void)))\n"
+ "(let $19 (YtStat '('\"Id\" '\"1dd1d-fb881-3f40191-9d1d7a9a\") $6 $7 $8 '('\"ModifyTime\" '\"1614862782\") '('\"Revision\" '\"524591601531009\")))\n"
+ "(let $20 (YtTable '\"home/yql/test-table7\" $4 $5 $19 '() (Void) (Void) '\"freud\"))\n"
+ "(let $21 (YtPath $20 $11 (Void) (Void)))\n"
+ "(let $22 '($12 $15 $18 $21))\n"
+ "(return (YtReadTable! world (DataSource '\"yt\" '\"freud\") '((YtSection $22 '('('\"unordered\"))))))\n"
+ ")\n";
+ auto astRes = ParseAst(astStr);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx_;
+ TExprNode::TPtr exprRoot_;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot_, exprCtx_, nullptr));
+ TString cluster;
+ const auto result = dqIntegration->Partition(settings, maxTasks, *exprRoot_, partitions, &cluster, exprCtx_, false);
+ const auto expected = 642;
+ UNIT_ASSERT_VALUES_EQUAL(result, expected);
+ UNIT_ASSERT_VALUES_EQUAL(partitions.size(), 2);
+ }
+
+ Y_UNIT_TEST(Ranges_table4_table7_test2) {
+ TYtState::TPtr state = new TYtState;
+ auto dqIntegration = CreateYtDqIntegration(state.Get());
+ TDqSettings settings;
+ settings.DataSizePerJob = 1000;
+ TVector<TString> partitions;
+ size_t maxTasks = 10;
+ auto astStr = "(\n"
+ "(let $4 (Void))\n"
+ "(let $5 (YtMeta '('\"CanWrite\" '\"1\") '('\"DoesExist\" '\"1\") '('\"YqlCompatibleScheme\" '\"1\") '('\"InferredScheme\" '\"0\") '('\"IsDynamic\" '\"0\") '('\"Attrs\" '('('\"optimize_for\" '\"lookup\")))))\n"
+ "(let $6 '('\"RecordsCount\" '\"100\"))\n"
+ "(let $7 '('\"DataSize\" '\"1284\"))\n"
+ "(let $8 '('\"ChunkCount\" '\"1\"))\n"
+ "(let $9 (YtStat '('\"Id\" '\"1dd1d-fb585-3f40191-36b851ee\") $6 $7 $8 '('\"ModifyTime\" '\"1614862780\") '('\"Revision\" '\"524591601530245\")))\n"
+ "(let $10 (YtTable '\"home/yql/test-table5\" $4 $5 $9 '() (Void) (Void) '\"freud\"))\n"
+ "(let $11 '('\"key\" '\"value\"))\n"
+ "(let $12 (YtPath $10 $11 (Void) (Void)))\n"
+ "(let $13 (YtStat '('\"Id\" '\"1dd1d-fb703-3f40191-46023c3c\") $6 $7 $8 '('\"ModifyTime\" '\"1614862781\") '('\"Revision\" '\"524591601530627\")))\n"
+ "(let $14 (YtTable '\"home/yql/test-table6\" $4 $5 $13 '() (Void) (Void) '\"freud\"))\n"
+ "(let $15 (YtPath $14 $11 (Void) (Void)))\n"
+ "(let $16 (YtStat '('\"Id\" '\"1dd1d-fb274-3f40191-5a564b01\") $6 $7 $8 '('\"ModifyTime\" '\"1614862777\") '('\"Revision\" '\"524591601529460\")))\n"
+ "(let $17 (YtTable '\"home/yql/test-table4\" $4 $5 $16 '() (Void) (Void) '\"freud\"))\n"
+ "(let $18 (YtPath $17 $11 (Void) (Void)))\n"
+ "(let $19 (YtStat '('\"Id\" '\"1dd1d-fb881-3f40191-9d1d7a9a\") $6 $7 $8 '('\"ModifyTime\" '\"1614862782\") '('\"Revision\" '\"524591601531009\")))\n"
+ "(let $20 (YtTable '\"home/yql/test-table7\" $4 $5 $19 '() (Void) (Void) '\"freud\"))\n"
+ "(let $21 (YtPath $20 $11 (Void) (Void)))\n"
+ "(let $22 '($12 $15 $18 $21))\n"
+ "(return (YtReadTable! world (DataSource '\"yt\" '\"freud\") '((YtSection $22 '('('\"unordered\"))))))\n"
+ ")\n";
+ auto astRes = ParseAst(astStr);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx_;
+ TExprNode::TPtr exprRoot_;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot_, exprCtx_, nullptr));
+ TString cluster;
+ const auto result = dqIntegration->Partition(settings, maxTasks, *exprRoot_, partitions, &cluster, exprCtx_, false);
+ const auto expected = 214;
+ UNIT_ASSERT_VALUES_EQUAL(result, expected);
+ UNIT_ASSERT_VALUES_EQUAL(partitions.size(), 6);
+ }
+
+}
+
diff --git a/ydb/library/yql/providers/yt/provider/ut/yql_yt_epoch_ut.cpp b/ydb/library/yql/providers/yt/provider/ut/yql_yt_epoch_ut.cpp
new file mode 100644
index 0000000000..639f6f04ad
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ut/yql_yt_epoch_ut.cpp
@@ -0,0 +1,519 @@
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/gateway/file/yql_yt_file.h>
+#include <ydb/library/yql/core/ut_common/yql_ut_common.h>
+
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/config/yql_setting.h>
+#include <ydb/library/yql/providers/common/config/yql_dispatch.h>
+#include <ydb/library/yql/providers/config/yql_config_provider.h>
+#include <ydb/library/yql/providers/result/provider/yql_result_provider.h>
+#include <ydb/library/yql/providers/dq/provider/yql_dq_state.h>
+#include <ydb/library/yql/providers/dq/provider/yql_dq_datasink.h>
+#include <ydb/library/yql/providers/dq/provider/yql_dq_datasource.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/services/yql_transform_pipeline.h>
+#include <ydb/library/yql/minikql/mkql_function_registry.h>
+#include <ydb/library/yql/minikql/invoke_builtins/mkql_builtins.h>
+#include <ydb/library/yql/sql/v1/sql.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+
+Y_UNIT_TEST_SUITE(TYqlEpoch) {
+
+ static TString ParseAndOptimize(const TString& program) {
+ google::protobuf::Arena arena;
+ NSQLTranslation::TTranslationSettings settings;
+ settings.Arena = &arena;
+ settings.ClusterMapping["plato"] = YtProviderName;
+ settings.SyntaxVersion = 1;
+
+ TAstParseResult astRes = NSQLTranslationV1::SqlToYql(program, settings);
+ UNIT_ASSERT(astRes.IsOk());
+ TExprContext exprCtx;
+ TExprNode::TPtr exprRoot;
+ UNIT_ASSERT(CompileExpr(*astRes.Root, exprRoot, exprCtx, nullptr));
+
+ auto functionRegistry = NKikimr::NMiniKQL::CreateFunctionRegistry(NKikimr::NMiniKQL::CreateBuiltinRegistry());
+ TTestTablesMapping testTables;
+ auto yqlNativeServices = NFile::TYtFileServices::Make(functionRegistry.Get(), testTables);
+ auto ytGateway = CreateYtFileGateway(yqlNativeServices);
+ auto typeAnnotationContext = MakeIntrusive<TTypeAnnotationContext>();
+ typeAnnotationContext->RandomProvider = CreateDeterministicRandomProvider(1);
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->Gateway = ytGateway;
+ ytState->Types = typeAnnotationContext.Get();
+ ytState->Configuration->Dispatch(NCommon::ALL_CLUSTERS, "_EnableWriteReorder", "true", NCommon::TSettingDispatcher::EStage::CONFIG);
+
+ InitializeYtGateway(ytGateway, ytState);
+ typeAnnotationContext->AddDataSink(YtProviderName, CreateYtDataSink(ytState));
+ typeAnnotationContext->AddDataSource(YtProviderName, CreateYtDataSource(ytState));
+
+ TDqStatePtr dqState = new TDqState(nullptr, {}, functionRegistry.Get(), {}, {}, {}, typeAnnotationContext.Get(), {}, {}, {}, nullptr, {}, {}, {}, false, {});
+ typeAnnotationContext->AddDataSink(DqProviderName, CreateDqDataSink(dqState));
+ typeAnnotationContext->AddDataSource(DqProviderName, CreateDqDataSource(dqState, [](const TDqStatePtr&) { return new TNullTransformer; }));
+
+ typeAnnotationContext->AddDataSource(ConfigProviderName, CreateConfigProvider(*typeAnnotationContext, nullptr, ""));
+
+ auto writerFactory = [] () { return CreateYsonResultWriter(NYson::EYsonFormat::Binary); };
+ auto resultConfig = MakeIntrusive<TResultProviderConfig>(*typeAnnotationContext,
+ *functionRegistry, IDataProvider::EResultFormat::Yson, ToString((ui32)NYson::EYsonFormat::Binary), writerFactory);
+ resultConfig->SupportsResultPosition = true;
+ auto resultProvider = CreateResultProvider(resultConfig);
+ typeAnnotationContext->AddDataSink(ResultProviderName, resultProvider);
+
+ auto transformer = TTransformationPipeline(typeAnnotationContext)
+ .AddServiceTransformers()
+ .AddPreTypeAnnotation()
+ .AddPreIOAnnotation()
+ .Add(
+ CreateFunctorTransformer(
+ [&](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ TOptimizeExprSettings settings{nullptr};
+ settings.VisitChanges = true;
+ return OptimizeExpr(input, output,
+ [&](const TExprNode::TPtr& node, TExprContext& /*ctx*/) -> TExprNode::TPtr {
+ if (!node->IsWorld() && !node->Content().EndsWith('!') && node->Content() != "YtTable") {
+ if (node->ChildrenSize() > 0) {
+ return node->HeadPtr();
+ }
+ }
+ return node;
+ },
+ ctx, settings);
+ }),
+ "Opt"
+ )
+ .Build();
+
+ exprCtx.Step.Done(TExprStep::ExprEval);
+ const auto status = SyncTransform(*transformer, exprRoot, exprCtx);
+ if (status == IGraphTransformer::TStatus::Error)
+ Cerr << exprCtx.IssueManager.GetIssues().ToString() << Endl;
+ UNIT_ASSERT(status == IGraphTransformer::TStatus::Ok);
+
+ auto ast = ConvertToAst(*exprRoot, exprCtx, TExprAnnotationFlags::None, true);
+ return ast.Root->ToString(TAstPrintFlags::PerLine | TAstPrintFlags::ShortQuote);
+ }
+
+ static struct TTestCase {
+ const char* Name;
+ const char* SQL;
+ const char* Expected;
+ } TESTCASES[] = {
+ {
+"Write1Write2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! world '"yt" $3 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(return (Commit! (Sync! $2 $4) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"ConfWrite1Write2",
+R"(
+use plato;
+pragma yt.Pool="1";
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! $1 '"yt" $2 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! $1 '"yt" $4 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(return (Commit! (Sync! $3 $5) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1Write2Write1Write2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! $2 '"yt" $3 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $5 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $6 (Write! world '"yt" $5 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $7 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $8 (Write! $6 '"yt" $7 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $4 $8) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1Write1Write2Write2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! $2 '"yt" $3 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $5 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $6 (Write! world '"yt" $5 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $7 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $8 (Write! $6 '"yt" $7 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $4 $8) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1Write1ConfWrite2Write2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+insert into @out1 select * from Input;
+pragma yt.Pool="1";
+insert into @out2 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! $2 '"yt" $3 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $5 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $6 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $7 (Write! $5 '"yt" $6 (Right! (Read! $5 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $8 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $9 (Write! $7 '"yt" $8 (Right! (Read! $5 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $4 $9) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"ConfWrite1Write2Write1Write2",
+R"(
+use plato;
+pragma yt.Pool="1";
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! $1 '"yt" $2 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! $3 '"yt" $4 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $6 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $7 (Write! $1 '"yt" $6 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $8 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $9 (Write! $7 '"yt" $8 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $5 $9) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1ConfWrite2Write1Write2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+pragma yt.Pool="1";
+insert into @out2 select * from Input;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! world '"yt" $2 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! (Sync! $1 $3) '"yt" $4 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $6 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $7 (Write! $1 '"yt" $6 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $8 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $9 (Write! $7 '"yt" $8 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $5 $9) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"ConfWrite1ConfWrite2Write1Write2",
+R"(
+use plato;
+pragma yt.Pool="1";
+insert into @out1 select * from Input;
+pragma yt.Pool="2";
+insert into @out2 select * from Input;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtConfigure! $1 '"yt" '"Attr" '"pool" '"2"))
+(let $3 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! $1 '"yt" $3 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $5 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $6 (Write! (Sync! $2 $4) '"yt" $5 (Right! (Read! $2 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $7 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $8 (Write! $2 '"yt" $7 (Right! (Read! $2 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $9 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $10 (Write! $8 '"yt" $9 (Right! (Read! $2 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $6 $10) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1ConfWrite1Write2ConfWrite2",
+R"(
+use plato;
+insert into @out1 select * from Input;
+pragma yt.Pool="1";
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+pragma yt.Pool="2";
+insert into @out2 select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! world '"yt" $2 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! (Sync! $1 $3) '"yt" $4 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $6 (YtConfigure! $1 '"yt" '"Attr" '"pool" '"2"))
+(let $7 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $8 (Write! $1 '"yt" $7 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $9 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $10 (Write! (Sync! $6 $8) '"yt" $9 (Right! (Read! $6 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $5 $10) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"Write1Write2Conf",
+R"(
+use plato;
+insert into @out1 select * from Input;
+insert into @out2 select * from Input;
+pragma yt.Pool="1";
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! world '"yt" $3 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(return (Commit! (Sync! $2 $4) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"WriteResultWrite",
+R"(
+use plato;
+insert into @out1 select * from Input;
+select * from Input;
+insert into @out1 select * from Input;
+)",
+R"((
+(let $1 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $2 (Write! world '"yt" $1 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $3 (Write! world 'result (Key) (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $4 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! (Commit! (Sync! $2 $3) 'result) '"yt" $4 (Right! (Read! world '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! $5 '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"OtherConfMix",
+R"(
+use plato;
+pragma yt.Pool="1";
+pragma dq.AnalyzeQuery="1";
+pragma yt.Pool="2";
+select * from Input;
+pragma dq.AnalyzeQuery="1";
+pragma yt.Pool="3";
+select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtConfigure! $1 '"yt" '"Attr" '"pool" '"2"))
+(let $3 (YtConfigure! $2 '"yt" '"Attr" '"pool" '"3"))
+(let $4 (Configure! world '"dq" '"Attr" '"analyzequery" '"1"))
+(let $5 (Write! (Sync! $2 $4) 'result (Key) (Right! (Read! $2 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $6 (Configure! (Commit! $5 'result) '"dq" '"Attr" '"analyzequery" '"1"))
+(let $7 (Write! (Sync! $3 $6) 'result (Key) (Right! (Read! $3 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(return (Commit! (Commit! $7 'result) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"ReadAfterCommit",
+R"(
+use plato;
+pragma yt.Pool = "1";
+insert into @temp
+select * from Input;
+commit;
+select * from @temp;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"temp" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! $1 '"yt" $2 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (Commit! $3 '"yt" '('('"epoch" '"1"))))
+(let $5 (Write! (Sync! $1 $4) 'result (Key) (Right! (Read! (Sync! $1 $4) '"yt" '((YtTable '"temp" (Void) (Void) (Void) '('('"anonymous")) '1 (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(return (Commit! (Commit! $5 'result) '"yt" '('('"epoch" '"2"))))
+)
+)"
+ },
+ {
+"ConfWrite1CommitConfWrite2CommitResult1CommitConfResult2",
+R"(
+use plato;
+pragma yt.Pool = "1";
+insert into @out1
+select * from Input;
+commit;
+pragma yt.Pool = "2";
+insert into @out2
+select * from @out1;
+select * from @out1;
+commit;
+pragma yt.Pool = "3";
+select * from @out2;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtConfigure! $1 '"yt" '"Attr" '"pool" '"2"))
+(let $3 (YtConfigure! $2 '"yt" '"Attr" '"pool" '"3"))
+(let $4 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $5 (Write! $1 '"yt" $4 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $6 (Commit! $5 '"yt" '('('"epoch" '"1"))))
+(let $7 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '2 '"plato"))
+(let $8 (Write! (Sync! $2 $6) '"yt" $7 (Right! (Read! (Sync! $2 $6) '"yt" '((YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) '1 (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $9 (Write! (Sync! $2 $6) 'result (Key) (Right! (Read! (Sync! $2 $6) '"yt" '((YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) '1 (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $10 (Commit! (Commit! (Sync! $8 $9) 'result) '"yt" '('('"epoch" '"2"))))
+(let $11 (Write! (Sync! $3 $10) 'result (Key) (Right! (Read! (Sync! $3 $10) '"yt" '((YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) '2 (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(return (Commit! (Commit! $11 'result) '"yt" '('('"epoch" '"3"))))
+)
+)"
+ },
+ {
+"ManyResults",
+R"(
+use plato;
+pragma yt.Pool = "1";
+select * from Input;
+select * from Input;
+select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (Write! $1 'result (Key) (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $3 (Write! (Sync! $1 (Commit! $2 'result)) 'result (Key) (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $4 (Write! (Sync! $1 (Commit! $3 'result)) 'result (Key) (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(return (Commit! (Commit! $4 'result) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"ConfPlusSeveralAppends",
+R"(
+use plato;
+pragma DqEngine="auto";
+pragma yt.Pool = "1";
+insert into @out
+select * from Input;
+insert into @out
+select * from Input;
+insert into @out
+select * from Input;
+insert into @out
+select * from Input;
+insert into @out
+select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (Configure! world '"config" '"DqEngine" '"auto"))
+(let $3 (YtTable '"out" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $4 (Write! (Sync! $1 $2) '"yt" $3 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $5 (YtTable '"out" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $6 (Write! $4 '"yt" $5 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $7 (YtTable '"out" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $8 (Write! $6 '"yt" $7 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $9 (YtTable '"out" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $10 (Write! $8 '"yt" $9 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(let $11 (YtTable '"out" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $12 (Write! $10 '"yt" $11 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! $12 '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ {
+"SecondAppendAfterResWrite",
+R"(
+use plato;
+pragma yt.Pool = "1";
+insert into @out1
+select * from Input;
+select * from Input;
+insert into @out2
+select * from Input;
+insert into @out1
+select * from Input;
+)",
+R"((
+(let $1 (YtConfigure! world '"yt" '"Attr" '"pool" '"1"))
+(let $2 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $3 (Write! $1 '"yt" $2 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $4 (Write! $1 'result (Key) (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('type) '('autoref))))
+(let $5 (Commit! (Sync! $3 $4) 'result))
+(let $6 (YtTable '"out2" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $7 (Write! (Sync! $1 $5) '"yt" $6 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append) '('"initial"))))
+(let $8 (YtTable '"out1" (Void) (Void) (Void) '('('"anonymous")) (Void) '1 '"plato"))
+(let $9 (Write! (Sync! $1 $5) '"yt" $8 (Right! (Read! $1 '"yt" '((YtTable '"Input" (Void) (Void) (Void) '() (Void) (Void) '"plato")) (Void) '())) '('('mode 'append))))
+(return (Commit! (Sync! $7 $9) '"yt" '('('"epoch" '"1"))))
+)
+)"
+ },
+ };
+
+ struct TTestRegistration {
+ TTestRegistration() {
+ for (size_t i = 0; i < sizeof(TESTCASES) / sizeof(TTestCase); ++i) {
+ TCurrentTest::AddTest(TESTCASES[i].Name,
+ [i](NUnitTest::TTestContext&) {
+ auto res = NTestSuiteTYqlEpoch::ParseAndOptimize(TString{TESTCASES[i].SQL});
+ UNIT_ASSERT_STRINGS_EQUAL(res, TESTCASES[i].Expected);
+ },
+ false
+ );
+ }
+ }
+ };
+ static const NTestSuiteTYqlEpoch::TTestRegistration testRegistration;
+
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/provider/ya.make b/ydb/library/yql/providers/yt/provider/ya.make
new file mode 100644
index 0000000000..a6dd4cb13d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/ya.make
@@ -0,0 +1,100 @@
+LIBRARY()
+
+SRCS(
+ yql_yt_datasink_constraints.cpp
+ yql_yt_datasink_exec.cpp
+ yql_yt_datasink_finalize.cpp
+ yql_yt_datasink_trackable.cpp
+ yql_yt_datasink_type_ann.cpp
+ yql_yt_datasink.cpp
+ yql_yt_datasource_constraints.cpp
+ yql_yt_datasource_exec.cpp
+ yql_yt_datasource_type_ann.cpp
+ yql_yt_datasource.cpp
+ yql_yt_epoch.cpp
+ yql_yt_gateway.cpp
+ yql_yt_horizontal_join.cpp
+ yql_yt_helpers.cpp
+ yql_yt_intent_determination.cpp
+ yql_yt_io_discovery.cpp
+ yql_yt_join_impl.cpp
+ yql_yt_key.cpp
+ yql_yt_load_table_meta.cpp
+ yql_yt_load_columnar_stats.cpp
+ yql_yt_logical_optimize.cpp
+ yql_yt_mkql_compiler.cpp
+ yql_yt_op_hash.cpp
+ yql_yt_op_settings.cpp
+ yql_yt_optimize.cpp
+ yql_yt_peephole.cpp
+ yql_yt_physical_finalizing.cpp
+ yql_yt_physical_optimize.cpp
+ yql_yt_provider_impl.cpp
+ yql_yt_provider.cpp
+ yql_yt_provider.h
+ yql_yt_provider_impl.h
+ yql_yt_table_desc.cpp
+ yql_yt_table.cpp
+ yql_yt_dq_integration.cpp
+ yql_yt_dq_hybrid.cpp
+ yql_yt_wide_flow.cpp
+)
+
+PEERDIR(
+ library/cpp/yson/node
+ library/cpp/disjoint_sets
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ ydb/library/yql/ast
+ ydb/library/yql/core/extract_predicate
+ ydb/library/yql/public/udf
+ ydb/library/yql/public/udf/tz
+ ydb/library/yql/sql
+ ydb/library/yql/utils
+ ydb/library/yql/utils/log
+ ydb/library/yql/core
+ ydb/library/yql/core/expr_nodes
+ ydb/library/yql/core/issue
+ ydb/library/yql/core/issue/protos
+ ydb/library/yql/core/peephole_opt
+ ydb/library/yql/core/type_ann
+ ydb/library/yql/core/file_storage
+ ydb/library/yql/dq/integration
+ ydb/library/yql/dq/opt
+ ydb/library/yql/minikql
+ ydb/library/yql/providers/common/codec
+ ydb/library/yql/providers/common/config
+ ydb/library/yql/providers/common/dq
+ ydb/library/yql/providers/common/mkql
+ ydb/library/yql/providers/common/proto
+ ydb/library/yql/providers/common/activation
+ ydb/library/yql/providers/common/provider
+ ydb/library/yql/providers/common/schema/expr
+ ydb/library/yql/providers/common/transform
+ ydb/library/yql/providers/dq/common
+ ydb/library/yql/providers/dq/expr_nodes
+ ydb/library/yql/providers/result/expr_nodes
+ ydb/library/yql/providers/stat/expr_nodes
+ ydb/library/yql/providers/yt/common
+ ydb/library/yql/providers/yt/expr_nodes
+ ydb/library/yql/providers/yt/lib/expr_traits
+ ydb/library/yql/providers/yt/lib/graph_reorder
+ ydb/library/yql/providers/yt/lib/hash
+ ydb/library/yql/providers/yt/lib/key_filter
+ ydb/library/yql/providers/yt/lib/mkql_helpers
+ ydb/library/yql/providers/yt/lib/res_pull
+ ydb/library/yql/providers/yt/lib/row_spec
+ ydb/library/yql/providers/yt/lib/skiff
+ ydb/library/yql/providers/yt/lib/yson_helpers
+ ydb/library/yql/providers/yt/opt
+)
+
+YQL_LAST_ABI_VERSION()
+
+GENERATE_ENUM_SERIALIZATION(yql_yt_op_settings.h)
+
+END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
new file mode 100644
index 0000000000..075fa08709
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink.cpp
@@ -0,0 +1,549 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_dq_integration.h"
+
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_data_provider_impl.h>
+#include <ydb/library/yql/providers/common/transform/yql_lazy_init.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/string/cast.h>
+#include <util/string/builder.h>
+#include <util/generic/set.h>
+#include <util/generic/utility.h>
+#include <util/generic/ylimits.h>
+
+#include <algorithm>
+#include <iterator>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtDataSinkTrackableNodeProcessor : public TTrackableNodeProcessorBase {
+public:
+ TYtDataSinkTrackableNodeProcessor(const TYtState::TPtr& state, bool collectNodes)
+ : CollectNodes(collectNodes)
+ , CleanupTransformer(collectNodes ? CreateYtDataSinkTrackableNodesCleanupTransformer(state) : nullptr)
+ {
+ }
+
+ void GetUsedNodes(const TExprNode& input, TVector<TString>& usedNodeIds) override {
+ usedNodeIds.clear();
+ if (!CollectNodes) {
+ return;
+ }
+
+ if (TMaybeNode<TYtOutputOpBase>(&input)) {
+ for (size_t i = TYtOutputOpBase::idx_Output + 1; i < input.ChildrenSize(); ++i) {
+ ScanForUsedOutputTables(*input.Child(i), usedNodeIds);
+ }
+ } else if (TMaybeNode<TYtPublish>(&input)) {
+ ScanForUsedOutputTables(*input.Child(TYtPublish::idx_Input), usedNodeIds);
+ } else if (TMaybeNode<TYtStatOut>(&input)) {
+ ScanForUsedOutputTables(*input.Child(TYtStatOut::idx_Input), usedNodeIds);
+ }
+ }
+
+ void GetCreatedNodes(const TExprNode& node, TVector<TExprNodeAndId>& created, TExprContext& ctx) override {
+ created.clear();
+ if (!CollectNodes) {
+ return;
+ }
+
+ if (auto maybeOp = TMaybeNode<TYtOutputOpBase>(&node)) {
+ TString clusterName = TString{maybeOp.Cast().DataSink().Cast<TYtDSink>().Cluster().Value()};
+ auto clusterPtr = maybeOp.Cast().DataSink().Ptr();
+ for (auto table: maybeOp.Cast().Output()) {
+ TString tableName = TString{table.Name().Value()};
+ auto tablePtr = table.Ptr();
+
+ TExprNodeAndId nodeAndId;
+ nodeAndId.Id = MakeUsedNodeId(clusterName, tableName);
+ nodeAndId.Node = ctx.NewList(tablePtr->Pos(), {clusterPtr, tablePtr});
+
+ created.push_back(std::move(nodeAndId));
+ }
+ }
+ }
+
+ IGraphTransformer& GetCleanupTransformer() override {
+ return CollectNodes ? *CleanupTransformer : NullTransformer_;
+ }
+
+private:
+ const bool CollectNodes;
+ THolder<IGraphTransformer> CleanupTransformer;
+};
+
+class TYtDataSink : public TDataProviderBase {
+public:
+ TYtDataSink(TYtState::TPtr state)
+ : State_(state)
+ , IntentDeterminationTransformer_([this]() { return CreateYtIntentDeterminationTransformer(State_); })
+ , TypeAnnotationTransformer_([this]() { return CreateYtDataSinkTypeAnnotationTransformer(State_); })
+ , ConstraintTransformer_([this]() { return CreateYtDataSinkConstraintTransformer(State_, false); })
+ , SubConstraintTransformer_([this]() { return CreateYtDataSinkConstraintTransformer(State_, true); })
+ , ExecTransformer_([this]() { return CreateYtDataSinkExecTransformer(State_); })
+ , LogicalOptProposalTransformer_([this]() { return CreateYtLogicalOptProposalTransformer(State_); })
+ , PhysicalOptProposalTransformer_([this]() { return CreateYtPhysicalOptProposalTransformer(State_); })
+ , PhysicalFinalizingTransformer_([this]() {
+ auto transformer = CreateYtPhysicalFinalizingTransformer(State_);
+ if (State_->IsHybridEnabled())
+ transformer = CreateYtDqHybridTransformer(State_, std::move(transformer));
+ return transformer;
+ })
+ , FinalizingTransformer_([this]() { return CreateYtDataSinkFinalizingTransformer(State_); })
+ , TrackableNodeProcessor_([this]() {
+ auto mode = GetReleaseTempDataMode(*State_->Configuration);
+ bool collectNodes = mode == EReleaseTempDataMode::Immediate;
+ return MakeHolder<TYtDataSinkTrackableNodeProcessor>(State_, collectNodes);
+ })
+ {
+ }
+
+ TStringBuf GetName() const override {
+ return YtProviderName;
+ }
+
+ IGraphTransformer& GetIntentDeterminationTransformer() override {
+ return *IntentDeterminationTransformer_;
+ }
+
+ IGraphTransformer& GetTypeAnnotationTransformer(bool instantOnly) override {
+ Y_UNUSED(instantOnly);
+ return *TypeAnnotationTransformer_;
+ }
+
+ IGraphTransformer& GetConstraintTransformer(bool instantOnly, bool subGraph) override {
+ Y_UNUSED(instantOnly);
+ return subGraph ? *SubConstraintTransformer_ : *ConstraintTransformer_;
+ }
+
+ IGraphTransformer& GetLogicalOptProposalTransformer() override {
+ return *LogicalOptProposalTransformer_;
+ }
+
+ IGraphTransformer& GetPhysicalOptProposalTransformer() override {
+ return *PhysicalOptProposalTransformer_;
+ }
+
+ IGraphTransformer& GetPhysicalFinalizingTransformer() override {
+ return *PhysicalFinalizingTransformer_;
+ }
+
+ IGraphTransformer& GetCallableExecutionTransformer() override {
+ return *ExecTransformer_;
+ }
+
+ IGraphTransformer& GetFinalizingTransformer() override {
+ return *FinalizingTransformer_;
+ }
+
+ bool CollectStatistics(NYson::TYsonWriter& writer, bool totalOnly) override {
+ if (State_->Statistics.empty()) {
+ return false;
+ }
+
+ NCommon::WriteStatistics(writer, totalOnly, State_->Statistics);
+
+ return true;
+ }
+
+ bool ValidateParameters(TExprNode& node, TExprContext& ctx, TMaybe<TString>& cluster) override {
+ if (node.IsCallable(TCoDataSink::CallableName())) {
+ if (!EnsureArgsCount(node, 2, ctx)) {
+ return false;
+ }
+
+ if (node.Child(0)->Content() == YtProviderName) {
+ if (!node.Child(1)->IsCallable("EvaluateAtom")) {
+ if (!EnsureAtom(*node.Child(1), ctx)) {
+ return false;
+ }
+
+ if (node.Child(1)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(1)->Pos()), "Empty cluster name"));
+ return false;
+ }
+
+ cluster = TString(node.Child(1)->Content());
+ }
+
+ return true;
+ }
+ }
+
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), "Invalid Yt DataSink parameters"));
+ return false;
+ }
+
+ bool CanParse(const TExprNode& node) override {
+ if (node.IsCallable(TCoWrite::CallableName())) {
+ return TYtDSink::Match(node.Child(1));
+ }
+
+ return TypeAnnotationTransformer_->CanParse(node);
+ }
+
+ void FillModifyCallables(THashSet<TStringBuf>& callables) override {
+ callables.insert(TYtWriteTable::CallableName());
+ callables.insert(TYtDropTable::CallableName());
+ callables.insert(TYtConfigure::CallableName());
+ }
+
+ bool IsWrite(const TExprNode& node) override {
+ return TYtWriteTable::Match(&node);
+ }
+
+ TExprNode::TPtr RewriteIO(const TExprNode::TPtr& node, TExprContext& ctx) override {
+ YQL_ENSURE(TMaybeNode<TYtWrite>(node).DataSink());
+ auto mode = NYql::GetSetting(*node->Child(4), EYtSettingType::Mode);
+ if (mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Drop) {
+ if (!node->Child(3)->IsCallable("Void")) {
+ ctx.AddError(TIssue(ctx.GetPosition(node->Child(3)->Pos()), TStringBuilder()
+ << "Expected Void, but got: " << node->Child(3)->Content()));
+ return {};
+ }
+
+ TExprNode::TListType children = node->ChildrenList();
+ children.resize(3);
+ return ctx.NewCallable(node->Pos(), TYtDropTable::CallableName(), std::move(children));
+ } else {
+ auto res = ctx.RenameNode(*node, TYtWriteTable::CallableName());
+ if ((!mode || FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Renew) && NYql::HasSetting(*node->Child(4), EYtSettingType::KeepMeta)) {
+ auto settings = NYql::AddSetting(
+ *NYql::RemoveSettings(*node->Child(4), EYtSettingType::Mode | EYtSettingType::KeepMeta, ctx),
+ EYtSettingType::Mode,
+ ctx.NewAtom(node->Child(4)->Pos(), ToString(EYtWriteMode::RenewKeepMeta), TNodeFlags::ArbitraryContent),
+ ctx);
+ res = ctx.ChangeChild(*res, TYtWriteTable::idx_Settings, std::move(settings));
+ }
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ res = ctx.ChangeChild(*res, TYtWriteTable::idx_Content,
+ ctx.Builder(node->Pos())
+ .Callable("RemovePrefixMembers")
+ .Add(0, node->ChildPtr(TYtWriteTable::idx_Content))
+ .List(1)
+ .Atom(0, YqlSysColumnPrefix)
+ .Seal()
+ .Seal()
+ .Build()
+ );
+ }
+ return res;
+ }
+ }
+
+ void PostRewriteIO() final {
+ State_->TablesData->CleanupCompiledSQL();
+ }
+
+ void Reset() final {
+ TDataProviderBase::Reset();
+ State_->Reset();
+ }
+
+ bool CanExecute(const TExprNode& node) override {
+ return ExecTransformer_->CanExec(node);
+ }
+
+ bool ValidateExecution(const TExprNode& node, TExprContext& ctx) override {
+ if (TYtDqProcessWrite::Match(&node)) {
+ auto dqProvider = State_->Types->DataSourceMap.FindPtr(DqProviderName);
+ YQL_ENSURE(dqProvider);
+ return (*dqProvider)->ValidateExecution(TYtDqProcessWrite(&node).Input().Ref(), ctx);
+ }
+ return true;
+ }
+
+ void GetRequiredChildren(const TExprNode& node, TExprNode::TListType& children) override {
+ if (CanExecute(node)) {
+ children.push_back(node.ChildPtr(0));
+ if (TYtTransientOpBase::Match(&node)) {
+ children.push_back(node.ChildPtr(TYtTransientOpBase::idx_Input));
+ } else if (TYtPublish::Match(&node)) {
+ children.push_back(node.ChildPtr(TYtPublish::idx_Input));
+ } else if (TYtStatOut::Match(&node)) {
+ children.push_back(node.ChildPtr(TYtStatOut::idx_Input));
+ }
+ }
+ }
+
+ bool GetDependencies(const TExprNode& node, TExprNode::TListType& children, bool compact) override {
+ Y_UNUSED(compact);
+ if (CanExecute(node)) {
+ children.emplace_back(node.HeadPtr());
+
+ if (TMaybeNode<TYtOutputOpBase>(&node)) {
+ for (size_t i = TYtOutputOpBase::idx_Output + 1; i < node.ChildrenSize(); ++i) {
+ ScanPlanDependencies(node.ChildPtr(i), children);
+ }
+ } else if (TMaybeNode<TYtPublish>(&node)) {
+ ScanPlanDependencies(node.ChildPtr(TYtPublish::idx_Input), children);
+ } else if (TMaybeNode<TYtStatOut>(&node)) {
+ ScanPlanDependencies(node.ChildPtr(TYtStatOut::idx_Input), children);
+ }
+
+ return !TYtDqProcessWrite::Match(&node);
+ }
+
+ return false;
+ }
+
+ void WritePlanDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(&node)) {
+ writer.OnKeyedItem("InputColumns");
+ auto op = maybeOp.Cast();
+ if (op.Input().Size() > 1) {
+ writer.OnBeginList();
+ for (auto section: op.Input()) {
+ writer.OnListItem();
+ WriteColumns(writer, section.Paths().Item(0).Columns());
+ }
+ writer.OnEndList();
+ }
+ else {
+ WriteColumns(writer, op.Input().Item(0).Paths().Item(0).Columns());
+ }
+
+ if (op.Maybe<TYtMap>() || op.Maybe<TYtMapReduce>() || op.Maybe<TYtMerge>() ||
+ op.Maybe<TYtReduce>() || op.Maybe<TYtSort>() || op.Maybe<TYtEquiJoin>())
+ {
+ TSet<TString> keyFilterColumns;
+ for (auto section: op.Input()) {
+ for (auto col : GetKeyFilterColumns(section, EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2)) {
+ keyFilterColumns.insert(TString(col));
+ }
+ for (auto path: section.Paths()) {
+ size_t keyLength = 0;
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ for (auto item: path.Ranges().Cast<TExprList>()) {
+ if (auto keyExact = item.Maybe<TYtKeyExact>()) {
+ keyLength = Max(keyLength, keyExact.Cast().Key().Size());
+ } else if (auto keyRange = item.Maybe<TYtKeyRange>()) {
+ keyLength = Max(keyLength, keyRange.Cast().Lower().Size(), keyRange.Cast().Upper().Size());
+ }
+ }
+ }
+ if (keyLength) {
+ auto rowSpec = TYtTableBaseInfo::GetRowSpec(path.Table());
+ YQL_ENSURE(rowSpec);
+ YQL_ENSURE(keyLength <= rowSpec->SortedBy.size());
+ keyFilterColumns.insert(rowSpec->SortedBy.begin(), rowSpec->SortedBy.begin() + keyLength);
+ }
+ }
+ }
+
+ if (!keyFilterColumns.empty()) {
+ writer.OnKeyedItem("InputKeyFilterColumns");
+ writer.OnBeginList();
+ for (auto column : keyFilterColumns) {
+ writer.OnListItem();
+ writer.OnStringScalar(column);
+ }
+ writer.OnEndList();
+ }
+ }
+
+ static const EYtSettingType specialSettings[] = {EYtSettingType::FirstAsPrimary, EYtSettingType::JoinReduce};
+ if (AnyOf(specialSettings, [&op](const auto& setting) { return NYql::HasSetting(op.Settings().Ref(), setting); })) {
+ writer.OnKeyedItem("Settings");
+ writer.OnBeginMap();
+
+ for (auto setting: specialSettings) {
+ if (NYql::HasSetting(op.Settings().Ref(), setting)) {
+ writer.OnKeyedItem(ToString(setting));
+ writer.OnStringScalar("true");
+ }
+ }
+
+ if (NYql::UseJoinReduceForSecondAsPrimary(op.Settings().Ref())) {
+ writer.OnKeyedItem(NYql::JoinReduceForSecondAsPrimaryName);
+ writer.OnStringScalar("true");
+ }
+
+ writer.OnEndMap();
+ }
+ }
+
+ if (auto maybeOp = TMaybeNode<TYtMap>(&node)) {
+ writer.OnKeyedItem("Streams");
+ writer.OnBeginMap();
+ NCommon::WriteStreams(writer, "Mapper", maybeOp.Cast().Mapper());
+ writer.OnEndMap();
+ } else if (auto maybeOp = TMaybeNode<TYtReduce>(&node)) {
+ writer.OnKeyedItem("Streams");
+ writer.OnBeginMap();
+ NCommon::WriteStreams(writer, "Reducer", maybeOp.Cast().Reducer());
+ writer.OnEndMap();
+ } else if (auto maybeOp = TMaybeNode<TYtMapReduce>(&node)) {
+ writer.OnKeyedItem("Streams");
+ writer.OnBeginMap();
+ if (auto maybeLambda = maybeOp.Cast().Mapper().Maybe<TCoLambda>()) {
+ NCommon::WriteStreams(writer, "Mapper", maybeLambda.Cast());
+ }
+
+ NCommon::WriteStreams(writer, "Reducer", maybeOp.Cast().Reducer());
+ writer.OnEndMap();
+ }
+ }
+
+ TString GetProviderPath(const TExprNode& node) override {
+ return TStringBuilder() << YtProviderName << '.' << node.Child(1)->Content();
+ }
+
+ void WriteDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ writer.OnKeyedItem("Cluster");
+ writer.OnStringScalar(node.Child(1)->Content());
+ }
+
+ void GetInputs(const TExprNode& node, TVector<TPinInfo>& inputs) override {
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(&node)) {
+ auto op = maybeOp.Cast();
+ for (auto section: op.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto maybeTable = path.Table().Maybe<TYtTable>()) {
+ inputs.push_back(TPinInfo(nullptr, op.DataSink().Raw(), path.Raw(), MakeTableDisplayName(maybeTable.Cast(), false), false));
+ }
+ else {
+ auto tmpTable = GetOutTable(path.Table());
+ inputs.push_back(TPinInfo(nullptr, op.DataSink().Raw(), tmpTable.Raw(), MakeTableDisplayName(tmpTable, false), true));
+ }
+ }
+ }
+ }
+ else if (auto maybePublish = TMaybeNode<TYtPublish>(&node)) {
+ auto publish = maybePublish.Cast();
+ for (auto out: publish.Input()) {
+ auto tmpTable = GetOutTable(out);
+ inputs.push_back(TPinInfo(nullptr, publish.DataSink().Raw(), tmpTable.Raw(), MakeTableDisplayName(tmpTable, false), true));
+ }
+ } else if (auto maybeStatOut = TMaybeNode<TYtStatOut>(&node)) {
+ auto statOut = maybeStatOut.Cast();
+ auto table = GetOutTable(statOut.Input());
+ inputs.push_back(TPinInfo(nullptr, statOut.DataSink().Raw(), table.Raw(), MakeTableDisplayName(table, false), true));
+ }
+ }
+
+ void GetOutputs(const TExprNode& node, TVector<TPinInfo>& outputs) override {
+ if (auto maybeOp = TMaybeNode<TYtOutputOpBase>(&node)) {
+ auto op = maybeOp.Cast();
+ for (auto table: op.Output()) {
+ outputs.push_back(TPinInfo(nullptr, op.DataSink().Raw(), table.Raw(), MakeTableDisplayName(table, true), true));
+ }
+ }
+ else if (auto maybePublish = TMaybeNode<TYtPublish>(&node)) {
+ auto publish = maybePublish.Cast();
+ outputs.push_back(TPinInfo(nullptr, publish.DataSink().Raw(), publish.Publish().Raw(), MakeTableDisplayName(publish.Publish(), true), false));
+ } else if (auto maybeStatOut = TMaybeNode<TYtStatOut>(&node)) {
+ auto statOut = maybeStatOut.Cast();
+ auto statTable = statOut.Table();
+ outputs.push_back(TPinInfo(nullptr, statOut.DataSink().Raw(), statTable.Raw(), "(tmp)", true));
+ } else if (auto maybeWrite = TMaybeNode<TYtWriteTable>(&node)) {
+ auto write = maybeWrite.Cast();
+ outputs.push_back(TPinInfo(nullptr, write.DataSink().Raw(), write.Table().Raw(), MakeTableDisplayName(write.Table(), true), true));
+ }
+ }
+
+ void WritePinDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ writer.OnKeyedItem("Table");
+ if (auto path = TMaybeNode<TYtPath>(&node)) {
+ const auto& table = path.Cast().Table().Cast<TYtTable>();
+ writer.OnStringScalar(table.Name().Value());
+ writer.OnKeyedItem("Type");
+ auto rowType = path.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ NCommon::WriteTypeToYson(writer, rowType);
+ } else if (auto table = TMaybeNode<TYtTable>(&node)) {
+ writer.OnStringScalar(table.Cast().Name().Value());
+ const auto tableInfo = TYtTableInfo(table.Cast());
+ auto& desc = State_->TablesData->GetTable(tableInfo.Cluster, tableInfo.Name, tableInfo.CommitEpoch);
+ if (desc.RowSpec) {
+ writer.OnKeyedItem("Type");
+ auto rowType = desc.RowSpec->GetType();
+ NCommon::WriteTypeToYson(writer, rowType);
+ }
+ } else {
+ writer.OnStringScalar("(tmp)");
+ }
+ }
+
+ TString GetOperationDisplayName(const TExprNode& node) override {
+ if (auto maybeCommit = TMaybeNode<TCoCommit>(&node)) {
+ auto commit = maybeCommit.Cast();
+
+ TStringBuilder res;
+ res << node.Content() << " on " << commit.DataSink().Cast<TYtDSink>().Cluster().Value();
+ if (commit.Settings()) {
+ if (auto epochNode = NYql::GetSetting(commit.Settings().Cast().Ref(), "epoch")) {
+ res << " #" << epochNode->Child(1)->Content();
+ }
+ }
+ return res;
+ }
+
+ return TString{node.Content()};
+ }
+
+ ITrackableNodeProcessor& GetTrackableNodeProcessor() override {
+ return *TrackableNodeProcessor_;
+ }
+
+ IDqIntegration* GetDqIntegration() override {
+ return State_->DqIntegration_.Get();
+ }
+
+private:
+ static void WriteColumns(NYson::TYsonWriter& writer, TExprBase columns) {
+ if (auto maybeList = columns.Maybe<TExprList>()) {
+ writer.OnBeginList();
+ for (const auto& column : maybeList.Cast()) {
+ writer.OnListItem();
+ if (auto atom = column.Maybe<TCoAtom>()) {
+ writer.OnStringScalar(atom.Cast().Value());
+ }
+ else {
+ writer.OnStringScalar(column.Cast<TExprList>().Item(0).Cast<TCoAtom>().Value());
+ }
+ }
+ writer.OnEndList();
+ } else if (columns.Maybe<TCoVoid>()) {
+ writer.OnStringScalar("*");
+ } else {
+ writer.OnStringScalar("?");
+ }
+ }
+
+private:
+ TYtState::TPtr State_;
+ TLazyInitHolder<IGraphTransformer> IntentDeterminationTransformer_;
+ TLazyInitHolder<TVisitorTransformerBase> TypeAnnotationTransformer_;
+ TLazyInitHolder<IGraphTransformer> ConstraintTransformer_;
+ TLazyInitHolder<IGraphTransformer> SubConstraintTransformer_;
+ TLazyInitHolder<TExecTransformerBase> ExecTransformer_;
+ TLazyInitHolder<IGraphTransformer> LogicalOptProposalTransformer_;
+ TLazyInitHolder<IGraphTransformer> PhysicalOptProposalTransformer_;
+ TLazyInitHolder<IGraphTransformer> PhysicalFinalizingTransformer_;
+ TLazyInitHolder<IGraphTransformer> FinalizingTransformer_;
+ TLazyInitHolder<ITrackableNodeProcessor> TrackableNodeProcessor_;
+};
+
+TIntrusivePtr<IDataProvider> CreateYtDataSink(TYtState::TPtr state) {
+ return new TYtDataSink(state);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
new file mode 100644
index 0000000000..644e0b07ef
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_constraints.cpp
@@ -0,0 +1,461 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_join_impl.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/core/yql_expr_constraint.h>
+#include <ydb/library/yql/ast/yql_constraint.h>
+
+#include <util/generic/xrange.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtDataSinkConstraintTransformer : public TVisitorTransformerBase {
+public:
+ TYtDataSinkConstraintTransformer(TYtState::TPtr state, bool subGraph)
+ : TVisitorTransformerBase(false)
+ , State_(state)
+ , SubGraph(subGraph)
+ {
+ AddHandler({TYtOutTable::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleOutTable));
+ AddHandler({TYtOutput::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleOutput));
+ AddHandler({TYtSort::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleTransientOp));
+ AddHandler({TYtCopy::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleTransientOp));
+ AddHandler({TYtMerge::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleTransientOp));
+ AddHandler({TYtMap::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleUserJobOp<TYtMap::idx_Mapper, TYtMap::idx_Mapper>));
+ AddHandler({TYtReduce::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleUserJobOp<TYtReduce::idx_Reducer, TYtReduce::idx_Reducer>));
+ AddHandler({TYtMapReduce::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleUserJobOp<TYtMapReduce::idx_Mapper, TYtMapReduce::idx_Reducer>));
+ AddHandler({TYtWriteTable::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleWriteTable));
+ AddHandler({TYtFill::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleFill));
+ AddHandler({TYtTouch::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleTouch));
+ AddHandler({TYtDropTable::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleDefault));
+ AddHandler({TCoCommit::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleCommit));
+ AddHandler({TYtPublish::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandlePublish));
+ AddHandler({TYtEquiJoin::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleEquiJoin));
+ AddHandler({TYtStatOutTable::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleDefault));
+ AddHandler({TYtStatOut::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleDefault));
+ AddHandler({TYtDqProcessWrite ::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleDqProcessWrite));
+ AddHandler({TYtTryFirst ::CallableName()}, Hndl(&TYtDataSinkConstraintTransformer::HandleTryFirst));
+ }
+private:
+ static void CopyExcept(TExprNode* dst, const TExprNode& from, const TStringBuf& except) {
+ for (const auto c: from.GetAllConstraints()) {
+ if (c->GetName() != except) {
+ dst->AddConstraint(c);
+ }
+ }
+ }
+
+ TStatus HandleOutTable(TExprBase input, TExprContext& ctx) {
+ const auto table = input.Cast<TYtOutTable>();
+ TConstraintSet set;
+ if (!table.RowSpec().Maybe<TCoVoid>()) {
+ TYqlRowSpecInfo rowSpec(table.RowSpec(), false);
+ set = rowSpec.GetAllConstraints(ctx);
+
+ if (!set.GetConstraint<TSortedConstraintNode>()) {
+ if (const auto sorted = rowSpec.MakeSortConstraint(ctx))
+ set.AddConstraint(sorted);
+ }
+ }
+
+ if (!(set.GetConstraint<TDistinctConstraintNode>() || set.GetConstraint<TUniqueConstraintNode>())) {
+ if (const auto& uniqueBy = NYql::GetSetting(table.Settings().Ref(), EYtSettingType::UniqueBy)) {
+ std::vector<std::string_view> columns;
+ columns.reserve(uniqueBy->Tail().ChildrenSize());
+ uniqueBy->Tail().ForEachChild([&columns](const TExprNode& column) { columns.emplace_back(column.Content()); });
+ set.AddConstraint(ctx.MakeConstraint<TUniqueConstraintNode>(columns));
+ set.AddConstraint(ctx.MakeConstraint<TDistinctConstraintNode>(columns));
+ }
+ }
+
+ if (!table.Stat().Maybe<TCoVoid>()) {
+ if (TYtTableStatInfo(table.Stat()).IsEmpty()) {
+ set.AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ }
+ input.Ptr()->SetConstraints(set);
+ return TStatus::Ok;
+ }
+
+ TStatus HandleOutput(TExprBase input, TExprContext& ctx) {
+ auto out = input.Cast<TYtOutput>();
+ auto op = GetOutputOp(out);
+ const bool skipSort = IsUnorderedOutput(out);
+ if (op.Output().Size() > 1) {
+ if (auto multi = op.Ref().GetConstraint<TMultiConstraintNode>()) {
+ if (auto constraints = multi->GetItem(FromString<ui32>(out.OutIndex().Value()))) {
+ for (auto c: constraints->GetAllConstraints()) {
+ if (!skipSort || c->GetName() != TSortedConstraintNode::Name()) {
+ input.Ptr()->AddConstraint(c);
+ }
+ }
+ } else {
+ input.Ptr()->AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ }
+ if (auto empty = op.Ref().GetConstraint<TEmptyConstraintNode>()) {
+ input.Ptr()->AddConstraint(empty);
+ }
+ } else {
+ for (auto c: op.Ref().GetAllConstraints()) {
+ if (!skipSort || c->GetName() != TSortedConstraintNode::Name()) {
+ input.Ptr()->AddConstraint(c);
+ }
+ }
+ }
+
+ return TStatus::Ok;
+ }
+
+ template <size_t InLambdaNdx, size_t OutLambdaNdx>
+ TStatus HandleUserJobOp(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ auto op = TYtWithUserJobsOpBase(input);
+ TExprNode::TPtr lambda = input->ChildPtr(InLambdaNdx);
+ size_t lambdaNdx = InLambdaNdx;
+
+ bool singleLambda = InLambdaNdx == OutLambdaNdx;
+ if (!singleLambda && TCoVoid::Match(lambda.Get())) {
+ singleLambda = true;
+ lambda = input->ChildPtr(OutLambdaNdx);
+ lambdaNdx = OutLambdaNdx;
+ }
+
+ const auto filter = NYql::HasSetting(op.Settings().Ref(), EYtSettingType::Ordered) ?
+ [](const std::string_view& name) { return TEmptyConstraintNode::Name() == name || TUniqueConstraintNode::Name() == name || TDistinctConstraintNode::Name() == name || TSortedConstraintNode::Name() == name; }:
+ [](const std::string_view& name) { return TEmptyConstraintNode::Name() == name || TUniqueConstraintNode::Name() == name || TDistinctConstraintNode::Name() == name; };
+ TConstraintNode::TListType argConstraints;
+ if (op.Input().Size() > 1) {
+ TMultiConstraintNode::TMapType multiItems;
+ bool allEmpty = true;
+ for (ui32 index = 0; index < op.Input().Size(); ++index) {
+ auto section = op.Input().Item(index);
+ if (!section.Ref().GetConstraint<TEmptyConstraintNode>()) {
+ multiItems.push_back(std::make_pair(index, section.Ref().GetConstraintSet()));
+ multiItems.back().second.FilterConstraints(filter);
+ allEmpty = false;
+ }
+ }
+ if (!multiItems.empty()) {
+ argConstraints.push_back(ctx.MakeConstraint<TMultiConstraintNode>(std::move(multiItems)));
+ } else if (allEmpty) {
+ argConstraints.push_back(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ } else {
+ auto set = op.Input().Item(0).Ref().GetConstraintSet();
+ set.FilterConstraints(filter);
+ argConstraints = set.GetAllConstraints();
+ if (singleLambda) {
+ if (const auto& reduceBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::ReduceBy); !reduceBy.empty()) {
+ TChoppedConstraintNode::TFullSetType sets;
+ sets.reserve(reduceBy.size());
+ std::transform(reduceBy.cbegin(), reduceBy.cend(), std::back_inserter(sets), [&ctx](const TString& column) { return TConstraintNode::TSetType{TConstraintNode::TPathType(1U, ctx.AppendString(column))}; });
+ argConstraints.push_back(ctx.MakeConstraint<TChoppedConstraintNode>(std::move(sets)));
+ }
+ }
+ }
+
+ auto status = UpdateLambdaConstraints(lambda, ctx, {argConstraints});
+ if (lambda != input->ChildPtr(lambdaNdx)) {
+ output = ctx.ChangeChild(*input, lambdaNdx, std::move(lambda));
+ status = status.Combine(TStatus::Repeat);
+ return status;
+ }
+ if (status != TStatus::Ok) {
+ return status;
+ }
+
+ if (!singleLambda) {
+ TConstraintNode::TListType argConstraints;
+ if (auto empty = lambda->GetConstraint<TEmptyConstraintNode>()) {
+ argConstraints.push_back(empty);
+ }
+
+ if (const auto& reduceBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::ReduceBy); !reduceBy.empty()) {
+ TChoppedConstraintNode::TFullSetType sets;
+ sets.reserve(reduceBy.size());
+ std::transform(reduceBy.cbegin(), reduceBy.cend(), std::back_inserter(sets), [&ctx](const TString& column) { return TConstraintNode::TSetType{TConstraintNode::TPathType(1U, ctx.AppendString(column))}; });
+ argConstraints.push_back(ctx.MakeConstraint<TChoppedConstraintNode>(std::move(sets)));
+ }
+
+ TExprNode::TPtr outLambda = input->ChildPtr(OutLambdaNdx);
+ auto status = UpdateLambdaConstraints(outLambda, ctx, {argConstraints});
+ if (outLambda != input->ChildPtr(OutLambdaNdx)) {
+ output = ctx.ChangeChild(*input, OutLambdaNdx, std::move(outLambda));
+ status = status.Combine(TStatus::Repeat);
+ }
+ if (status != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ SetResultConstraint(input, *input->Child(OutLambdaNdx), op.Output(), ctx);
+ if (op.Input().Size() == 1) {
+ if (auto empty = op.Input().Item(0).Ref().GetConstraint<TEmptyConstraintNode>()) {
+ input->AddConstraint(empty);
+ }
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleFill(TExprBase input, TExprContext& ctx) {
+ auto fill = input.Cast<TYtFill>();
+ auto status = UpdateLambdaConstraints(fill.Content().Ref());
+ if (status != TStatus::Ok) {
+ return status;
+ }
+
+ SetResultConstraint(input.Ptr(), fill.Content().Ref(), fill.Output(), ctx);
+ return TStatus::Ok;
+ }
+
+ static TConstraintNode::TListType GetConstraintsForInputArgument(const TConstraintSet& set, TExprContext& ctx) {
+ TConstraintNode::TListType argsConstraints;
+ if (auto mapping = TPartOfUniqueConstraintNode::GetCommonMapping(set.GetConstraint<TUniqueConstraintNode>()); !mapping.empty()) {
+ argsConstraints.emplace_back(ctx.MakeConstraint<TPartOfUniqueConstraintNode>(std::move(mapping)));
+ }
+ if (auto mapping = TPartOfDistinctConstraintNode::GetCommonMapping(set.GetConstraint<TDistinctConstraintNode>()); !mapping.empty()) {
+ argsConstraints.emplace_back(ctx.MakeConstraint<TPartOfDistinctConstraintNode>(std::move(mapping)));
+ }
+ return argsConstraints;
+ }
+
+ TStatus HandleEquiJoin(TExprBase input, TExprContext& ctx) {
+ const auto equiJoin = input.Cast<TYtEquiJoin>();
+ TStatus status = TStatus::Ok;
+ for (const auto i : xrange(equiJoin.Input().Size())) {
+ const auto premapIndex = i + 7U;
+ if (equiJoin.Ref().Child(premapIndex)->IsLambda()) {
+ status = status.Combine(UpdateLambdaConstraints(equiJoin.Ptr()->ChildRef(premapIndex), ctx, {GetConstraintsForInputArgument(equiJoin.Input().Item(i).Ref().GetConstraintSet(), ctx)}));
+ if (status == TStatus::Error)
+ return status;
+ }
+ }
+ if (status != TStatus::Ok)
+ return status;
+
+ input.Ptr()->SetConstraints(ImportYtEquiJoin(equiJoin, ctx)->Constraints);
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTransientOp(TExprBase input, TExprContext& ctx) {
+ auto status = HandleDefault(input, ctx);
+ if (status != TStatus::Ok) {
+ return status;
+ }
+ auto op = input.Cast<TYtTransientOpBase>();
+ YQL_ENSURE(op.Input().Size() == 1);
+ YQL_ENSURE(op.Output().Size() == 1);
+
+ input.Ptr()->CopyConstraints(op.Output().Item(0).Ref());
+ if (auto empty = op.Input().Item(0).Ref().GetConstraint<TEmptyConstraintNode>()) {
+ input.Ptr()->AddConstraint(empty);
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTouch(TExprBase input, TExprContext& ctx) {
+ auto status = HandleDefault(input, ctx);
+ if (status != TStatus::Ok) {
+ return status;
+ }
+ input.Ptr()->CopyConstraints(input.Cast<TYtTouch>().Output().Item(0).Ref());
+ input.Ptr()->AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleDefault(TExprBase input, TExprContext& /*ctx*/) {
+ return UpdateAllChildLambdasConstraints(input.Ref());
+ }
+
+ TStatus HandleWriteTable(TExprBase input, TExprContext& ctx) {
+ if (SubGraph) {
+ return TStatus::Ok;
+ }
+
+ auto writeTable = input.Cast<TYtWriteTable>();
+
+ const bool initialWrite = NYql::HasSetting(writeTable.Settings().Ref(), EYtSettingType::Initial);
+ const auto outTableInfo = TYtTableInfo(writeTable.Table());
+
+ if (auto commitEpoch = outTableInfo.CommitEpoch.GetOrElse(0)) {
+ const auto cluster = TString{writeTable.DataSink().Cluster().Value()};
+ TYtTableDescription& nextDescription = State_->TablesData->GetModifTable(cluster, outTableInfo.Name, commitEpoch);
+
+ if (initialWrite) {
+ nextDescription.ConstraintsReady = false;
+ nextDescription.Constraints.Clear();
+ if (nextDescription.IsReplaced) {
+ nextDescription.Constraints = writeTable.Content().Ref().GetConstraintSet();
+ } else {
+ const TYtTableDescription& description = State_->TablesData->GetTable(cluster, outTableInfo.Name, outTableInfo.Epoch);
+ YQL_ENSURE(description.ConstraintsReady);
+ nextDescription.Constraints = description.Constraints;
+ }
+ }
+
+ if (!initialWrite || !nextDescription.IsReplaced) {
+ if (const auto tableSort = nextDescription.Constraints.RemoveConstraint<TSortedConstraintNode>()) {
+ if (const auto contentSort = writeTable.Content().Ref().GetConstraint<TSortedConstraintNode>()) {
+ if (const auto commonSort = tableSort->MakeCommon(contentSort, ctx)) {
+ nextDescription.Constraints.AddConstraint(commonSort);
+ }
+ }
+ }
+ if (!writeTable.Content().Ref().GetConstraint<TEmptyConstraintNode>()) {
+ nextDescription.Constraints.RemoveConstraint<TEmptyConstraintNode>();
+ }
+ nextDescription.Constraints.RemoveConstraint<TUniqueConstraintNode>();
+ nextDescription.Constraints.RemoveConstraint<TDistinctConstraintNode>();
+ }
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandlePublish(TExprBase input, TExprContext& ctx) {
+ if (SubGraph) {
+ return TStatus::Ok;
+ }
+
+ auto publish = input.Cast<TYtPublish>();
+
+ const bool initialWrite = NYql::HasSetting(publish.Settings().Ref(), EYtSettingType::Initial);
+ const auto outTableInfo = TYtTableInfo(publish.Publish());
+
+ if (auto commitEpoch = outTableInfo.CommitEpoch.GetOrElse(0)) {
+
+ const auto cluster = TString{publish.DataSink().Cluster().Value()};
+ const auto tableName = TString{TYtTableInfo::GetTableLabel(publish.Publish())};
+ TYtTableDescription& nextDescription = State_->TablesData->GetModifTable(cluster, tableName, commitEpoch);
+
+ if (initialWrite) {
+ nextDescription.ConstraintsReady = false;
+ nextDescription.Constraints.Clear();
+ if (nextDescription.IsReplaced) {
+ nextDescription.Constraints = publish.Input().Item(0).Ref().GetConstraintSet();
+ } else {
+ const TYtTableDescription& description = State_->TablesData->GetTable(cluster, tableName, outTableInfo.Epoch);
+ YQL_ENSURE(description.ConstraintsReady);
+ nextDescription.Constraints = description.Constraints;
+ }
+ }
+
+ const size_t from = nextDescription.IsReplaced ? 1 : 0;
+ auto tableSort = nextDescription.Constraints.RemoveConstraint<TSortedConstraintNode>();
+ for (size_t i = from; i < publish.Input().Size() && tableSort; ++i) {
+ tableSort = tableSort->MakeCommon(publish.Input().Item(i).Ref().GetConstraint<TSortedConstraintNode>(), ctx);
+ }
+ if (tableSort) {
+ nextDescription.Constraints.AddConstraint(tableSort);
+ }
+ if (AnyOf(publish.Input(), [](const TYtOutput& out) { return !out.Ref().GetConstraint<TEmptyConstraintNode>(); })) {
+ nextDescription.Constraints.RemoveConstraint<TEmptyConstraintNode>();
+ }
+ if (publish.Input().Size() > (nextDescription.IsReplaced ? 1 : 0)) {
+ nextDescription.Constraints.RemoveConstraint<TUniqueConstraintNode>();
+ nextDescription.Constraints.RemoveConstraint<TDistinctConstraintNode>();
+ }
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleCommit(TExprBase input, TExprContext& ctx) {
+ if (SubGraph) {
+ return TStatus::Ok;
+ }
+
+ const auto commit = input.Cast<TCoCommit>();
+ const auto settings = NCommon::ParseCommitSettings(commit, ctx);
+
+ if (settings.Epoch) {
+ const ui32 epoch = FromString(settings.Epoch.Cast().Value());
+ for (const auto& clusterAndTable : State_->TablesData->GetAllEpochTables(epoch)) {
+ State_->TablesData->GetModifTable(clusterAndTable.first, clusterAndTable.second, epoch).SetConstraintsReady();
+ }
+ }
+ return TStatus::Ok;
+ }
+
+private:
+ void SetResultConstraint(const TExprNode::TPtr& input, const TExprNode& source, const TYtOutSection& outputs, TExprContext& ctx) {
+ if (outputs.Size() == 1) {
+ auto out = outputs.Item(0);
+ input->CopyConstraints(out.Ref());
+ if (auto empty = source.GetConstraint<TEmptyConstraintNode>()) {
+ input->AddConstraint(empty);
+ if (!out.Stat().Maybe<TCoVoid>() && State_->Types->IsConstraintCheckEnabled<TEmptyConstraintNode>()) {
+ YQL_ENSURE(out.Ref().GetConstraint<TEmptyConstraintNode>(), "Invalid Empty constraint");
+ }
+ }
+ } else {
+ TMultiConstraintNode::TMapType multiItems;
+ auto multi = source.GetConstraint<TMultiConstraintNode>();
+ auto empty = source.GetConstraint<TEmptyConstraintNode>();
+ if (multi) {
+ multiItems = multi->GetItems();
+ }
+ for (ui32 i = 0; i < outputs.Size(); ++i) {
+ auto out = outputs.Item(i);
+ bool addEmpty = false;
+ if ((multi && !multiItems.has(i)) || empty) {
+ if (!out.Stat().Maybe<TCoVoid>() && State_->Types->IsConstraintCheckEnabled<TEmptyConstraintNode>()) {
+ YQL_ENSURE(out.Ref().GetConstraint<TEmptyConstraintNode>(), "Invalid Empty constraint");
+ }
+ addEmpty = true;
+ }
+ multiItems[i] = out.Ref().GetConstraintSet();
+ if (addEmpty) {
+ multiItems[i].AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ }
+ if (!multiItems.empty()) {
+ input->AddConstraint(ctx.MakeConstraint<TMultiConstraintNode>(std::move(multiItems)));
+ }
+ if (empty) {
+ input->AddConstraint(empty);
+ }
+ }
+ }
+
+ TStatus HandleDqProcessWrite(TExprBase input, TExprContext& ctx) {
+ if (const auto status = HandleDefault(input, ctx); status != TStatus::Ok) {
+ return status;
+ }
+ bool complete = input.Ref().GetState() >= TExprNode::EState::ExecutionComplete;
+ if (!complete && input.Ref().HasResult()) {
+ const auto& result = input.Ref().GetResult();
+ complete = result.IsWorld();
+ }
+ if (complete)
+ input.Ptr()->CopyConstraints(input.Cast<TYtDqProcessWrite>().Output().Item(0).Ref());
+ else
+ CopyExcept(input.Ptr().Get(), input.Cast<TYtDqProcessWrite>().Output().Item(0).Ref(), TEmptyConstraintNode::Name());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTryFirst(TExprBase input, TExprContext&) {
+ input.Ptr()->CopyConstraints(input.Ref().Tail());
+ return TStatus::Ok;
+ }
+private:
+ const TYtState::TPtr State_;
+ const bool SubGraph;
+};
+
+}
+
+THolder<IGraphTransformer> CreateYtDataSinkConstraintTransformer(TYtState::TPtr state, bool subGraph) {
+ return THolder(new TYtDataSinkConstraintTransformer(state, subGraph));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
new file mode 100644
index 0000000000..76ec801f74
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_exec.cpp
@@ -0,0 +1,920 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_op_hash.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_optimize.h"
+
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/gateway/lib/yt_helpers.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_helpers.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_exec.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/yql_execution.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/ast/yql_ast.h>
+
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/dq/expr_nodes/dq_expr_nodes.h>
+#include <ydb/library/yql/dq/opt/dq_opt.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/generic/overloaded.h>
+#include <util/generic/xrange.h>
+#include <util/generic/ylimits.h>
+#include <util/generic/guid.h>
+#include <util/generic/maybe.h>
+#include <util/generic/scope.h>
+#include <util/string/cast.h>
+#include <util/string/hex.h>
+
+#include <algorithm>
+#include <memory>
+
+namespace NYql {
+
+namespace {
+
+using namespace NNodes;
+using namespace NThreading;
+
+bool NeedFallback(const TIssues& issues) {
+ for (const auto& issue : issues)
+ if (TIssuesIds::DQ_GATEWAY_NEED_FALLBACK_ERROR == issue.GetCode())
+ return true;
+
+ return false;
+}
+
+TIssue WrapIssuesOnHybridFallback(TPosition pos, const TIssues& issues) {
+ TIssue result(pos, "Hybrid execution fallback on YT");
+ result.SetCode(TIssuesIds::DQ_GATEWAY_NEED_FALLBACK_ERROR, TSeverityIds::S_WARNING);
+
+ const std::function<void(TIssue& issue)> toWarning = [&](TIssue& issue) {
+ if (issue.Severity == TSeverityIds::S_ERROR || issue.Severity == TSeverityIds::S_FATAL) {
+ issue.Severity = TSeverityIds::S_WARNING;
+ }
+ for (const auto& subissue : issue.GetSubIssues()) {
+ toWarning(*subissue);
+ }
+ };
+
+ for (const auto& issue : issues) {
+ TIssuePtr warning(new TIssue(issue));
+ toWarning(*warning);
+ result.AddSubIssue(std::move(warning));
+ }
+
+ return result;
+}
+
+class TYtDataSinkExecTransformer : public TExecTransformerBase {
+public:
+ TYtDataSinkExecTransformer(TYtState::TPtr state)
+ : State_(state)
+ , Delegated_(new TNodeMap<TDelegatedInfo>())
+ {
+ AddHandler(
+ {
+ TYtSort::CallableName(),
+ TYtMap::CallableName(),
+ TYtCopy::CallableName(),
+ TYtMerge::CallableName(),
+ TYtMapReduce::CallableName(),
+ },
+ RequireAllOf({TYtTransientOpBase::idx_World, TYtTransientOpBase::idx_Input}),
+ Hndl(&TYtDataSinkExecTransformer::HandleOutputOp<true>)
+ );
+ AddHandler(
+ {
+ TYtFill::CallableName(),
+ TYtTouch::CallableName(),
+ },
+ RequireFirst(),
+ Hndl(&TYtDataSinkExecTransformer::HandleOutputOp<true>)
+ );
+ AddHandler({TYtReduce::CallableName()}, RequireAllOf({TYtTransientOpBase::idx_World, TYtTransientOpBase::idx_Input}), Hndl(&TYtDataSinkExecTransformer::HandleReduce));
+ AddHandler({TYtOutput::CallableName()}, RequireFirst(), Pass());
+ AddHandler({TYtPublish::CallableName()}, RequireAllOf({TYtPublish::idx_World, TYtPublish::idx_Input}), Hndl(&TYtDataSinkExecTransformer::HandlePublish));
+ AddHandler({TYtDropTable::CallableName()}, RequireFirst(), Hndl(&TYtDataSinkExecTransformer::HandleDrop));
+ AddHandler({TCoCommit::CallableName()}, RequireFirst(), Hndl(&TYtDataSinkExecTransformer::HandleCommit));
+ AddHandler({TYtEquiJoin::CallableName()}, RequireSequenceOf({TYtEquiJoin::idx_World, TYtEquiJoin::idx_Input}),
+ Hndl(&TYtDataSinkExecTransformer::HandleEquiJoin));
+ AddHandler({TYtStatOut::CallableName()}, RequireAllOf({TYtStatOut::idx_World, TYtStatOut::idx_Input}),
+ Hndl(&TYtDataSinkExecTransformer::HandleStatOut));
+ AddHandler({TYtDqProcessWrite::CallableName()}, RequireFirst(),
+ Hndl(&TYtDataSinkExecTransformer::HandleYtDqProcessWrite));
+ AddHandler({TYtTryFirst::CallableName()}, RequireFirst(), Hndl(&TYtDataSinkExecTransformer::HandleTryFirst));
+ }
+
+ void Rewind() override {
+ Delegated_->clear();
+ TExecTransformerBase::Rewind();
+ }
+
+private:
+ static TExprNode::TPtr FinalizeOutputOp(const TYtState::TPtr& state, const TString& operationHash,
+ const IYtGateway::TRunResult& res, const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx, bool markFinished)
+ {
+ auto outSection = TYtOutputOpBase(input).Output();
+ YQL_ENSURE(outSection.Size() == res.OutTableStats.size(), "Invalid output table count in IYtGateway::TRunResult");
+ TExprNode::TListType newOutTables;
+ for (size_t i: xrange(outSection.Size())) {
+ TYtOutTable outTable = outSection.Item(i);
+ TExprNode::TListType children = outTable.Raw()->ChildrenList();
+ if (auto& name = children[TYtOutTable::idx_Name]; name->IsAtom("") && !res.OutTableStats[i].first.empty())
+ name = ctx.NewAtom(name->Pos(), res.OutTableStats[i].first);
+ if (const auto stat = res.OutTableStats[i].second)
+ children[TYtOutTable::idx_Stat] = stat->ToExprNode(ctx, outTable.Pos()).Ptr();
+
+ newOutTables.push_back(ctx.ChangeChildren(outTable.Ref(), std::move(children)));
+ }
+ output = ctx.ChangeChild(*input, TYtOutputOpBase::idx_Output, ctx.NewList(outSection.Pos(), std::move(newOutTables)));
+ state->NodeHash.emplace(output->UniqueId(), operationHash);
+ return markFinished ? ctx.NewWorld(input->Pos()) : ctx.NewAtom(input->Pos(), "");
+ }
+
+ using TLaunchOpResult = std::variant<TFuture<IYtGateway::TRunResult>, TStatusCallbackPair>;
+ TLaunchOpResult LaunchOutputOp(TString& operationHash, const TExprNode::TPtr& input, TExprContext& ctx) {
+ TYtOutputOpBase op(input);
+
+ if (auto opInput = op.Maybe<TYtTransientOpBase>().Input()) {
+ bool error = false;
+ for (auto section: opInput.Cast()) {
+ for (auto path: section.Paths()) {
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ auto tableInfo = TYtTableInfo(table.Cast());
+ if (!tableInfo.Meta) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Table " << tableInfo.Name.Quote() << " has no metadata"));
+ error = true;
+ }
+ if (!tableInfo.Stat) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Table " << tableInfo.Name.Quote() << " has no stat"));
+ error = true;
+ }
+ }
+ }
+ }
+ if (error) {
+ return SyncError();
+ }
+ }
+
+ auto cluster = TString{op.DataSink().Cluster().Value()};
+ // Scan entire node because inner lambda may contain YtTableContent with YtPath
+ TExprNode::TListType needCalc = GetNodesToCalculate(input);
+ if (!needCalc.empty()) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Calculating nodes for " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+ return CalculateNodes(State_, input, cluster, needCalc, ctx);
+ }
+
+ auto outSection = op.Output();
+
+ size_t outWithoutName = 0;
+ for (auto out: outSection) {
+ if (out.Name().Value().empty()) {
+ ++outWithoutName;
+ }
+ }
+ if (outWithoutName != outSection.Size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(outSection.Pos()), TStringBuilder() << "Incomplete execution of "
+ << input->Content() << ", #" << input->UniqueId()));
+ return SyncError();
+ }
+
+ input->SetState(TExprNode::EState::ExecutionInProgress);
+
+ auto newWorld = ctx.NewWorld(input->Child(0)->Pos());
+ newWorld->SetTypeAnn(input->Child(0)->GetTypeAnn());
+ newWorld->SetState(TExprNode::EState::ConstrComplete);
+
+ TExprNode::TPtr clonedNode = ctx.ChangeChild(*input, 0, std::move(newWorld));
+ clonedNode->SetTypeAnn(input->GetTypeAnn());
+ clonedNode->CopyConstraints(*input);
+
+ TExprNode::TPtr optimizedNode = clonedNode;
+ if (const auto status = SubstTables(optimizedNode, State_, false, ctx); status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ const auto settings = State_->Configuration->GetSettingsForNode(*input);
+ TUserDataTable crutches = State_->Types->UserDataStorageCrutches;
+ if (const auto& defaultGeobase = settings->GeobaseDownloadUrl.Get(cluster)) {
+ auto& userDataBlock = (crutches[TUserDataKey::File(TStringBuf("/home/geodata6.bin"))] = TUserDataBlock{EUserDataType::URL, {}, *defaultGeobase, {}, {}});
+ userDataBlock.Usage.Set(EUserDataBlockUsage::Path);
+ }
+
+ bool hasNonDeterministicFunctions = false;
+ if (const auto status = PeepHoleOptimizeBeforeExec<true>(optimizedNode, optimizedNode, State_, hasNonDeterministicFunctions, ctx); status.Level != TStatus::Ok) {
+ return SyncStatus(status);
+ }
+
+ TUserDataTable files;
+ auto filesRes = NCommon::FreezeUsedFiles(*optimizedNode, files, *State_->Types, ctx, MakeUserFilesDownloadFilter(*State_->Gateway, TString(cluster)), crutches);
+ if (filesRes.first.Level != TStatus::Ok) {
+ if (filesRes.first.Level != TStatus::Error) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Freezing files for " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+ }
+ return filesRes;
+ }
+
+ THashMap<TString, TString> secureParams;
+ NCommon::FillSecureParams(optimizedNode, *State_->Types, secureParams);
+
+ auto config = State_->Configuration->GetSettingsForNode(*input);
+ const auto queryCacheMode = config->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable);
+ if (queryCacheMode != EQueryCacheMode::Disable) {
+ if (!hasNonDeterministicFunctions) {
+ operationHash = TYtNodeHashCalculator(State_, cluster, config).GetHash(*optimizedNode);
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Operation hash: " << HexEncode(operationHash).Quote()
+ << ", cache mode: " << queryCacheMode;
+ }
+
+ YQL_CLOG(DEBUG, ProviderYt) << "Executing " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+
+ return State_->Gateway->Run(optimizedNode, ctx,
+ IYtGateway::TRunOptions(State_->SessionId)
+ .UserDataBlocks(files)
+ .UdfModules(State_->Types->UdfModules)
+ .UdfResolver(State_->Types->UdfResolver)
+ .UdfValidateMode(State_->Types->ValidateMode)
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ .Config(std::move(config))
+ .OptLLVM(State_->Types->OptLLVM.GetOrElse(TString()))
+ .OperationHash(operationHash)
+ .SecureParams(secureParams)
+ );
+ }
+
+ template <bool MarkFinished>
+ TStatusCallbackPair HandleOutputOp(const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (input->HasResult() && input->GetResult().Type() == TExprNode::World) {
+ return SyncOk();
+ }
+
+ TString operationHash;
+ TLaunchOpResult callbackPairOrFuture = LaunchOutputOp(operationHash, input, ctx);
+ return std::visit(TOverloaded{
+ [&](const TStatusCallbackPair& pair) {
+ return pair;
+ },
+ [&](TFuture<IYtGateway::TRunResult>& future) {
+ return WrapModifyFuture(future, [operationHash, state = State_](const IYtGateway::TRunResult& res,
+ const TExprNode::TPtr& input,
+ TExprNode::TPtr& output,
+ TExprContext& ctx)
+ {
+ return FinalizeOutputOp(state, operationHash, res, input, output, ctx, MarkFinished);
+ });
+ }
+ }, callbackPairOrFuture);
+ }
+
+ TStatusCallbackPair HandleTryFirst(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext&) {
+ auto statWriter = [this](TStringBuf name) {
+ with_lock(State_->StatisticsMutex) {
+ State_->Statistics[Max<ui32>()].Entries.emplace_back(TString{name}, 0, 0, 0, 0, 1);
+ }
+ };
+
+ switch (input->Head().GetState()) {
+ case TExprNode::EState::ExecutionComplete:
+ statWriter("HybridExecution");
+ output = input->HeadPtr();
+ break;
+ case TExprNode::EState::Error: {
+ statWriter("HybridFallback");
+ if (State_->Configuration->HybridDqExecutionFallback.Get().GetOrElse(true)) {
+ output = input->TailPtr();
+ } else {
+ input->SetState(TExprNode::EState::Error);
+ return SyncError();
+ }
+ break;
+ }
+ default:
+ Y_UNREACHABLE();
+ }
+ return SyncStatus(TStatus(TStatus::Repeat, true));
+ }
+
+ TStatusCallbackPair HandleReduce(const TExprNode::TPtr& input, TExprContext& ctx) {
+ TYtReduce reduce(input);
+
+ if (!NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::FirstAsPrimary)) {
+ return HandleOutputOp<true>(input, ctx);
+ }
+
+ if (input->HasResult() && input->GetResult().Type() == TExprNode::World) {
+ return SyncOk();
+ }
+
+ TString operationHash;
+ TLaunchOpResult callbackPairOrFuture = LaunchOutputOp(operationHash, input, ctx);
+ if (auto* pair = std::get_if<TStatusCallbackPair>(&callbackPairOrFuture)) {
+ return *pair;
+ }
+
+ auto future = std::get<TFuture<IYtGateway::TRunResult>>(callbackPairOrFuture);
+ return std::make_pair(IGraphTransformer::TStatus::Async, future.Apply(
+ [operationHash, state = State_](const TFuture<IYtGateway::TRunResult>& completedFuture) {
+ return TAsyncTransformCallback(
+ [completedFuture, operationHash, state](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ output = input;
+ const auto& res = completedFuture.GetValue();
+ if (!res.Success()) {
+ YQL_ENSURE(!res.Issues().Empty());
+ if (res.Issues().begin()->IssueCode == TIssuesIds::YT_MAX_DATAWEIGHT_PER_JOB_EXCEEDED) {
+ YQL_CLOG(INFO, ProviderYt) << "Execution of node: " << input->Content()
+ << " with FirstAsPrimary exceeds max dataweight per job, rebuilding node";
+ TYtReduce reduce(input);
+ const bool joinReduceForSecond = NYql::UseJoinReduceForSecondAsPrimary(
+ reduce.Settings().Ref());
+ auto settings = NYql::RemoveSettings(reduce.Settings().Ref(),
+ EYtSettingType::FirstAsPrimary | EYtSettingType::JoinReduce, ctx);
+ if (joinReduceForSecond) {
+ settings = NYql::AddSetting(*settings, EYtSettingType::JoinReduce, nullptr, ctx);
+ }
+
+ output = ctx.ChangeChild(*input, TYtReduce::idx_Settings, std::move(settings));
+ output->SetState(TExprNode::EState::Initial);
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ }
+ }
+
+ TIssueScopeGuard issueScope(ctx.IssueManager, [&]() {
+ return MakeIntrusive<TIssue>(
+ ctx.GetPosition(input->Pos()),
+ TStringBuilder() << "Execution of node: " << input->Content() << " with TryFirstAsPrimary");
+ });
+ res.ReportIssues(ctx.IssueManager);
+
+ if (!res.Success()) {
+ input->SetState(TExprNode::EState::Error);
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Error);
+ } else {
+ TExprNode::TPtr resultNode = FinalizeOutputOp(state, operationHash, res, input, output, ctx, true);
+ input->SetState(TExprNode::EState::ExecutionComplete);
+ output->SetResult(std::move(resultNode));
+ if (input != output) {
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ }
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Ok);
+ }
+ });
+ }));
+ }
+
+ TStatusCallbackPair HandleDrop(const TExprNode::TPtr& input, TExprContext& ctx) {
+ input->SetState(TExprNode::EState::ExecutionInProgress);
+
+ auto drop = TYtDropTable(input);
+
+ auto newWorld = ctx.ShallowCopy(*input->Child(0));
+ newWorld->SetTypeAnn(input->Child(0)->GetTypeAnn());
+ newWorld->SetState(TExprNode::EState::ExecutionComplete);
+
+ TExprNode::TPtr clonedNode = ctx.ChangeChild(*input, 0, std::move(newWorld));
+ clonedNode->SetTypeAnn(input->GetTypeAnn());
+ clonedNode->CopyConstraints(*input);
+
+ auto status = SubstTables(clonedNode, State_, true, ctx);
+ if (status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ auto future = State_->Gateway->Run(clonedNode, ctx,
+ IYtGateway::TRunOptions(State_->SessionId)
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ .Config(State_->Configuration->GetSettingsForNode(*input))
+ );
+
+ return WrapFuture(future, [](const IYtGateway::TRunResult& /*res*/, const TExprNode::TPtr& input, TExprContext& ctx) {
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+
+ TStatusCallbackPair HandlePublish(const TExprNode::TPtr& input, TExprContext& ctx) {
+ auto publish = TYtPublish(input);
+ auto cluster = TString{publish.DataSink().Cluster().Value()};
+ auto path = TString{ publish.Publish().Name().Value() };
+
+ auto commitEpoch = TEpochInfo::Parse(publish.Publish().CommitEpoch().Ref()).GetOrElse(0);
+ TYtTableDescription& nextDescription = State_->TablesData->GetModifTable(cluster, path, commitEpoch);
+
+ auto config = State_->Configuration->GetSettingsForNode(*input);
+
+ const auto mode = NYql::GetSetting(publish.Settings().Ref(), EYtSettingType::Mode);
+ const bool initial = NYql::HasSetting(publish.Settings().Ref(), EYtSettingType::Initial);
+
+ auto dataHash = TYtNodeHashCalculator(State_, cluster, config).GetHash(publish.Input().Ref());
+ YQL_CLOG(INFO, ProviderYt) << "Publish data hash \"" << HexEncode(dataHash) << "\" for table " << cluster << "." << path << "#" << commitEpoch;
+ TString nextHash;
+ if (nextDescription.IsReplaced && initial) {
+ nextHash = dataHash;
+ } else {
+ auto epoch = TEpochInfo::Parse(publish.Publish().Epoch().Ref()).GetOrElse(0);
+ const TYtTableDescription& readDescription = State_->TablesData->GetTable(cluster, path, epoch);
+ TString prevHash;
+ if (!initial) {
+ prevHash = nextDescription.Hash.GetOrElse({});
+ } else {
+ if (readDescription.Hash) {
+ prevHash = *readDescription.Hash;
+ } else {
+ prevHash = TYtNodeHashCalculator(State_, cluster, config).GetHash(publish.Publish().Ref());
+ }
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Publish prev content hash \"" << HexEncode(prevHash) << "\" for table " << cluster << "." << path << "#" << commitEpoch;
+ if (!prevHash.empty() && !dataHash.empty()) {
+ THashBuilder builder;
+ builder << TYtNodeHashCalculator::MakeSalt(config, cluster) << prevHash << dataHash;
+ nextHash = builder.Finish();
+ }
+ }
+ nextDescription.Hash = nextHash;
+ if (!nextDescription.Hash->Empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "Using publish hash \"" << HexEncode(*nextDescription.Hash) << "\" for table " << cluster << "." << path << "#" << commitEpoch;
+ }
+
+ input->SetState(TExprNode::EState::ExecutionInProgress);
+
+ auto newWorld = ctx.NewWorld(input->Child(0)->Pos());
+ newWorld->SetTypeAnn(input->Child(0)->GetTypeAnn());
+ newWorld->SetState(TExprNode::EState::ExecutionComplete);
+
+ TExprNode::TPtr clonedNode = ctx.ChangeChild(*input, TYtPublish::idx_World, std::move(newWorld));
+ clonedNode->SetTypeAnn(publish.Ref().GetTypeAnn());
+ clonedNode->SetState(TExprNode::EState::ConstrComplete);
+
+ auto status = SubstTables(clonedNode, State_, true, ctx);
+ if (status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ auto future = State_->Gateway->Publish(clonedNode, ctx,
+ IYtGateway::TPublishOptions(State_->SessionId)
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ .DestinationRowSpec(nextDescription.RowSpec)
+ .Config(std::move(config))
+ .OptLLVM(State_->Types->OptLLVM.GetOrElse(TString()))
+ .OperationHash(nextDescription.Hash.GetOrElse(""))
+ );
+
+ return WrapFuture(future, [](const IYtGateway::TPublishResult& /*res*/, const TExprNode::TPtr& input, TExprContext& ctx) {
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+
+ TStatusCallbackPair HandleCommit(const TExprNode::TPtr& input, TExprContext& ctx) {
+ auto commit = TCoCommit(input);
+ auto settings = NCommon::ParseCommitSettings(commit, ctx);
+ YQL_ENSURE(settings.Epoch);
+
+ ui32 epoch = FromString(settings.Epoch.Cast().Value());
+ auto cluster = commit.DataSink().Cast<TYtDSink>().Cluster().Value();
+
+ auto settingsVer = State_->Configuration->FindNodeVer(*input);
+ auto future = State_->Gateway->Commit(
+ IYtGateway::TCommitOptions(State_->SessionId)
+ .Cluster(TString{cluster})
+ );
+
+ if (State_->EpochDependencies.contains(epoch)) {
+ auto state = State_;
+ return WrapModifyFuture(future, [state, epoch, settingsVer](const IYtGateway::TCommitResult& /*res*/, const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ input->SetState(TExprNode::EState::ExecutionComplete); // Don't execute this node again
+ output = ctx.ExactShallowCopy(*input);
+ state->LoadEpochMetadata.ConstructInPlace(epoch, settingsVer);
+ ctx.Step.Repeat(TExprStep::LoadTablesMetadata);
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+
+ return WrapFuture(future, [](const IYtGateway::TCommitResult& /*res*/, const TExprNode::TPtr& input, TExprContext& ctx) {
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+
+ TStatusCallbackPair HandleEquiJoin(const TExprNode::TPtr& input, TExprContext& ctx) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Execution of "
+ << input->Content() << ", #" << input->UniqueId() << " should not be reached"));
+ return SyncError();
+ }
+
+ TStatusCallbackPair HandleStatOut(const TExprNode::TPtr& input, TExprContext& ctx) {
+ Y_UNUSED(ctx);
+
+ auto config = State_->Configuration->GetSettingsForNode(*input);
+ auto future = State_->Gateway->Run(input, ctx,
+ IYtGateway::TRunOptions(State_->SessionId)
+ .Config(std::move(config))
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ );
+
+ return WrapFuture(
+ future,
+ [](const NCommon::TOperationResult& result, const TExprNode::TPtr& input, TExprContext& ctx) {
+ Y_UNUSED(result);
+ return ctx.NewWorld(input->Pos());
+ }
+ );
+ }
+
+ TStatusCallbackPair HandleYtDqProcessWrite(const TExprNode::TPtr& input, TExprContext& ctx) {
+ const TYtDqProcessWrite op(input);
+ const auto section = op.Output().Cast<TYtOutSection>();
+ Y_ENSURE(section.Size() == 1, "TYtDqProcessWrite expects 1 output table but got " << section.Size());
+ const TYtOutTable tmpTable = section.Item(0);
+
+ if (!input->HasResult()) {
+ if (!tmpTable.Name().Value().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), TStringBuilder() << "Incomplete execution of "
+ << input->Content() << ", #" << input->UniqueId()));
+ return SyncError();
+ }
+
+ input->SetState(TExprNode::EState::ExecutionInProgress);
+
+ return MakeTableForDqWrite(input, ctx);
+ }
+ else if (const auto& result = input->GetResult(); result.IsAtom()) {
+ if (result.IsAtom("")) {
+ // Second iteration: do the actual write.
+ return RunDqWrite(input, ctx, tmpTable);
+ } else if(result.IsAtom("FallbackOnError")) {
+ return SyncOk();
+ } else {
+ // Third iteration: collect temporary table statistics.
+ Y_ENSURE(result.IsAtom("DQ_completed"), "Unexpected result atom: " << result.Content());
+ return CollectDqWrittenTableStats(input, ctx);
+ }
+ }
+ else {
+ // Fourth iteration: everything is done, return ok status.
+ Y_ENSURE(input->GetResult().Type() == TExprNode::World, "Unexpected result type: " << input->GetResult().Type());
+ return SyncOk();
+ }
+ }
+
+private:
+ TStatusCallbackPair MakeTableForDqWrite(const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (input->HasResult() && input->GetResult().Type() == TExprNode::World) {
+ return SyncOk();
+ }
+
+ auto newWorld = ctx.NewWorld(input->Head().Pos());
+ newWorld->SetTypeAnn(input->Head().GetTypeAnn());
+ newWorld->SetState(TExprNode::EState::ConstrComplete);
+
+ auto optimizedNode = ctx.ChangeChild(*input, 0, std::move(newWorld));
+ optimizedNode->SetTypeAnn(input->GetTypeAnn());
+ optimizedNode->CopyConstraints(*input);
+
+ if (const auto status = SubstTables(optimizedNode, State_, false, ctx); status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ const TYtDqProcessWrite op(input);
+ const auto cluster = op.DataSink().Cluster().StringValue();
+ const auto config = State_->Configuration->GetSettingsForNode(*input);
+
+ TUserDataTable crutches = State_->Types->UserDataStorageCrutches;
+ if (const auto& defaultGeobase = config->GeobaseDownloadUrl.Get(cluster)) {
+ auto& userDataBlock = (crutches[TUserDataKey::File(TStringBuf("/home/geodata6.bin"))] = TUserDataBlock{EUserDataType::URL, {}, *defaultGeobase, {}, {}});
+ userDataBlock.Usage.Set(EUserDataBlockUsage::Path);
+ }
+
+ bool hasNonDeterministicFunctions = false;
+ if (const auto status = PeepHoleOptimizeBeforeExec<false>(optimizedNode, optimizedNode, State_, hasNonDeterministicFunctions, ctx); status.Level != TStatus::Ok) {
+ return SyncStatus(status);
+ }
+
+ TUserDataTable files;
+ if (const auto filesRes = NCommon::FreezeUsedFiles(*optimizedNode, files, *State_->Types, ctx, MakeUserFilesDownloadFilter(*State_->Gateway, TString(cluster)), crutches);
+ filesRes.first.Level != TStatus::Ok) {
+ if (filesRes.first.Level != TStatus::Error) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Freezing files for " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+ }
+ return filesRes;
+ }
+
+ TString operationHash;
+ if (const auto queryCacheMode = config->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable); queryCacheMode != EQueryCacheMode::Disable) {
+ if (!hasNonDeterministicFunctions) {
+ operationHash = TYtNodeHashCalculator(State_, cluster, config).GetHash(*input);
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Operation hash: " << HexEncode(operationHash).Quote() << ", cache mode: " << queryCacheMode;
+ }
+
+ YQL_CLOG(DEBUG, ProviderYt) << "Preparing " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+
+ auto future = State_->Gateway->Prepare(input, ctx,
+ IYtGateway::TPrepareOptions(State_->SessionId)
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ .Config(std::move(config))
+ .OperationHash(operationHash)
+ );
+
+ return WrapModifyFuture(future, [operationHash, state = State_](const IYtGateway::TRunResult& res,
+ const TExprNode::TPtr& input,
+ TExprNode::TPtr& output,
+ TExprContext& ctx)
+ {
+ return FinalizeOutputOp(state, operationHash, res, input, output, ctx, bool(res.OutTableStats.front().second));
+ });
+ }
+
+ TStatusCallbackPair RunDqWrite(const TExprNode::TPtr& input, TExprContext& ctx, const TYtOutTable& tmpTable) {
+ const TYtDqProcessWrite op(input);
+
+ IDataProvider* dqProvider = nullptr;
+ TExprNode::TPtr delegatedNode;
+
+ if (auto it = Delegated_->find(input.Get()); it != Delegated_->end()) {
+ dqProvider = it->second.DelegatedProvider;
+ delegatedNode = it->second.DelegatedNode;
+ }
+
+ if (!delegatedNode) {
+ const auto cluster = op.DataSink().Cluster();
+ const auto config = State_->Configuration->GetSettingsForNode(*input);
+ const auto tmpFolder = config->TablesTmpFolder.Get().GetOrElse(TString());
+ auto clusterStr = TString{cluster.Value()};
+
+ TMaybeNode<TCoSecureParam> secParams;
+ if (State_->Configuration->Auth.Get().GetOrElse(TString())) {
+ secParams = Build<TCoSecureParam>(ctx, op.Ref().Pos()).Name().Build(TString("cluster:default_").append(clusterStr)).Done();
+ }
+
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto maybeWrite = TMaybeNode<TYtDqWideWrite>(node)) {
+ auto server = State_->Gateway->GetClusterServer(clusterStr);
+ YQL_ENSURE(server, "Invalid YT cluster: " << clusterStr);
+
+ NYT::TRichYPath realTable = State_->Gateway->GetWriteTable(State_->SessionId, clusterStr, tmpTable.Name().StringValue(), tmpFolder);
+ realTable.Append(true);
+ YQL_ENSURE(realTable.TransactionId_.Defined(), "Expected TransactionId");
+
+ NYT::TNode spec;
+ TYqlRowSpecInfo(tmpTable.RowSpec()).FillCodecNode(spec[YqlRowSpecAttribute]);
+ NYT::TNode outSpec = NYT::TNode::CreateMap()(TString{YqlIOSpecTables}, NYT::TNode::CreateList().Add(spec));
+ NYT::TNode writerOptions = NYT::TNode::CreateMap();
+
+ if (config->MaxRowWeight.Get(clusterStr)) {
+ auto maxRowWeight = config->MaxRowWeight.Get(clusterStr)->GetValue();
+ writerOptions["max_row_weight"] = static_cast<i64>(maxRowWeight);
+ }
+
+ auto settings = Build<TCoNameValueTupleList>(ctx, node->Pos())
+ .Add()
+ .Name().Value("table", TNodeFlags::Default).Build()
+ .Value<TCoAtom>().Value(NYT::NodeToYsonString(NYT::PathToNode(realTable))).Build()
+ .Build()
+ .Add()
+ .Name().Value("server", TNodeFlags::Default).Build()
+ .Value<TCoAtom>().Value(server).Build()
+ .Build()
+ .Add()
+ .Name().Value("outSpec", TNodeFlags::Default).Build()
+ .Value<TCoAtom>().Value(NYT::NodeToYsonString(outSpec)).Build()
+ .Build()
+ .Add()
+ .Name().Value("secureParams", TNodeFlags::Default).Build()
+ .Value(secParams)
+ .Build()
+ .Add()
+ .Name().Value("tx", TNodeFlags::Default).Build()
+ .Value<TCoAtom>().Value(GetGuidAsString(*realTable.TransactionId_), TNodeFlags::Default).Build()
+ .Build()
+ .Add()
+ .Name().Value("outTable", TNodeFlags::Default).Build()
+ .Value(tmpTable)
+ .Build()
+ .Add()
+ .Name().Value("writerOptions", TNodeFlags::Default).Build()
+ .Value<TCoAtom>().Value(NYT::NodeToYsonString(writerOptions)).Build()
+ .Build()
+ .Done().Ptr();
+
+ node->ChildRef(TYtDqWideWrite::idx_Settings) = settings;
+
+ auto atomType = ctx.MakeType<TUnitExprType>();
+
+ VisitExpr(node->ChildRef(TYtDqWideWrite::idx_Settings), [&atomType](const TExprNode::TPtr& tupleNode) {
+ if (tupleNode->GetTypeAnn() == nullptr) {
+ tupleNode->SetTypeAnn(atomType);
+ tupleNode->SetState(TExprNode::EState::ConstrComplete);
+ }
+ return true;
+ });
+
+ return false;
+ }
+
+ return true;
+ });
+
+ delegatedNode = input->ChildPtr(TYtDqProcessWrite::idx_Input);
+ if (const auto status = SubstTables(delegatedNode, State_, false, ctx); status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+ bool hasNonDeterministicFunctions = false;
+ if (const auto status = PeepHoleOptimizeBeforeExec<false>(delegatedNode, delegatedNode, State_, hasNonDeterministicFunctions, ctx); status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ delegatedNode = Build<TPull>(ctx, delegatedNode->Pos())
+ .Input(std::move(delegatedNode))
+ .BytesLimit()
+ .Value(TString())
+ .Build()
+ .RowsLimit()
+ .Value(0U)
+ .Build()
+ .FormatDetails()
+ .Value(ui32(NYson::EYsonFormat::Binary))
+ .Build()
+ .Settings()
+ .Build()
+ .Format()
+ .Value(0U)
+ .Build()
+ .PublicId()
+ .Value(ToString(State_->Types->TranslateOperationId(input->UniqueId())))
+ .Build()
+ .Discard()
+ .Value(ToString(true), TNodeFlags::Default)
+ .Build()
+ .Origin(input)
+ .Done()
+ .Ptr();
+
+ auto atomType = ctx.MakeType<TUnitExprType>();
+
+ for (auto idx: {TResOrPullBase::idx_BytesLimit, TResOrPullBase::idx_RowsLimit, TResOrPullBase::idx_FormatDetails,
+ TResOrPullBase::idx_Format, TResOrPullBase::idx_PublicId, TResOrPullBase::idx_Discard }) {
+ delegatedNode->Child(idx)->SetTypeAnn(atomType);
+ delegatedNode->Child(idx)->SetState(TExprNode::EState::ConstrComplete);
+ }
+
+ delegatedNode->SetTypeAnn(input->GetTypeAnn());
+ delegatedNode->SetState(TExprNode::EState::ConstrComplete);
+ }
+
+ if (!dqProvider) {
+ if (auto p = State_->Types->DataSourceMap.FindPtr(DqProviderName)) {
+ dqProvider = p->Get();
+ }
+ }
+ YQL_ENSURE(dqProvider);
+
+ input->SetState(TExprNode::EState::ExecutionInProgress);
+ TExprNode::TPtr delegatedNodeOutput;
+
+ if (auto status = dqProvider->GetCallableExecutionTransformer().Transform(delegatedNode, delegatedNodeOutput, ctx); status.Level != TStatus::Async) {
+ YQL_ENSURE(status.Level != TStatus::Ok, "Asynchronous execution is expected in a happy path.");
+ if (const auto flags = op.Flags()) {
+ for (const auto& atom : flags.Cast()) {
+ if (atom.Value() == "FallbackOnError") {
+ input->SetResult(atom.Ptr());
+ input->SetState(TExprNode::EState::Error);
+ if (const auto issies = ctx.AssociativeIssues.extract(delegatedNode.Get())) {
+ if (NeedFallback(issies.mapped())) {
+ ctx.IssueManager.RaiseIssue(WrapIssuesOnHybridFallback(ctx.GetPosition(input->Pos()), issies.mapped()));
+ return SyncStatus(IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true));
+ } else {
+ ctx.IssueManager.RaiseIssues(issies.mapped());
+ return SyncStatus(status);
+ }
+ }
+ }
+ }
+ }
+
+ if (const auto issies = ctx.AssociativeIssues.extract(delegatedNode.Get())) {
+ ctx.IssueManager.RaiseIssues(issies.mapped());
+ }
+ return SyncStatus(status);
+ }
+
+ (*Delegated_)[input.Get()] = TDelegatedInfo{dqProvider, delegatedNode};
+
+ auto dqFuture = dqProvider->GetCallableExecutionTransformer().GetAsyncFuture(*delegatedNode);
+
+ TAsyncTransformCallbackFuture callbackFuture = dqFuture.Apply(
+ [delegated = std::weak_ptr<TNodeMap<TDelegatedInfo>>(Delegated_), state = State_](const TFuture<void>& completedFuture) {
+ return TAsyncTransformCallback(
+ [completedFuture, delegated, state](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ completedFuture.GetValue();
+ TExprNode::TPtr delegatedNode;
+ IDataProvider* dqProvider = nullptr;
+ auto lock = delegated.lock();
+ if (lock) {
+ if (auto it = lock->find(input.Get()); it != lock->end()) {
+ delegatedNode = it->second.DelegatedNode;
+ dqProvider = it->second.DelegatedProvider;
+ }
+ }
+ YQL_ENSURE(delegatedNode && dqProvider);
+ TExprNode::TPtr delegatedNodeOutput;
+ const auto dqWriteStatus = dqProvider->GetCallableExecutionTransformer()
+ .ApplyAsyncChanges(delegatedNode, delegatedNodeOutput, ctx);
+
+ YQL_ENSURE(dqWriteStatus != TStatus::Async, "ApplyAsyncChanges should not return Async.");
+
+ if (dqWriteStatus == TStatus::Repeat) {
+ output = input;
+ input->SetState(TExprNode::EState::ExecutionRequired);
+ return dqWriteStatus;
+ }
+ lock->erase(input.Get());
+
+ if (dqWriteStatus != TStatus::Ok) {
+ output = input;
+ if (const auto flags = TYtDqProcessWrite(input).Flags()) {
+ for (const auto& atom : flags.Cast()) {
+ if (atom.Value() == "FallbackOnError") {
+ output->SetResult(atom.Ptr());
+ output->SetState(TExprNode::EState::Error);
+ if (const auto issies = ctx.AssociativeIssues.extract(delegatedNode.Get())) {
+ if (NeedFallback(issies.mapped())) {
+ ctx.IssueManager.RaiseIssue(WrapIssuesOnHybridFallback(ctx.GetPosition(input->Pos()), issies.mapped()));
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ } else {
+ ctx.IssueManager.RaiseIssues(issies.mapped());
+ return dqWriteStatus;
+ }
+ }
+ }
+ }
+ }
+ if (const auto issies = ctx.AssociativeIssues.extract(delegatedNode.Get())) {
+ ctx.IssueManager.RaiseIssues(issies.mapped());
+ }
+ return dqWriteStatus;
+ }
+
+ input->SetState(TExprNode::EState::ExecutionComplete);
+ output = ctx.ShallowCopy(*input);
+ output->SetResult(ctx.NewAtom(input->Pos(), "DQ_completed"));
+ if (const auto it = state->NodeHash.find(input->UniqueId()); state->NodeHash.cend() != it) {
+ auto hash = state->NodeHash.extract(it);
+ hash.key() = output->UniqueId();
+ state->NodeHash.insert(std::move(hash));
+ }
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ });
+ });
+
+ return std::make_pair(IGraphTransformer::TStatus::Async, callbackFuture);
+ }
+
+ TStatusCallbackPair CollectDqWrittenTableStats(const TExprNode::TPtr& input, TExprContext& ctx) {
+ auto statsFuture = State_->Gateway->GetTableStat(input, ctx,
+ IYtGateway::TPrepareOptions(State_->SessionId)
+ .PublicId(State_->Types->TranslateOperationId(input->UniqueId()))
+ .Config(State_->Configuration->GetSettingsForNode(*input))
+ .OperationHash(State_->NodeHash[input->UniqueId()])
+ );
+
+ return WrapFutureCallback(
+ statsFuture,
+ [state = State_](const IYtGateway::TRunResult& res, const TExprNode::TPtr& in, TExprNode::TPtr& out, TExprContext& ctx) {
+ auto result = FinalizeOutputOp(state, state->NodeHash[in->UniqueId()], res, in, out, ctx, true);
+ out->SetResult(std::move(result));
+ in->SetState(TExprNode::EState::ExecutionComplete);
+ if (in != out) {
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ }
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Ok);
+ }
+ );
+ }
+
+ const TYtState::TPtr State_;
+
+ struct TDelegatedInfo {
+ IDataProvider* DelegatedProvider;
+ TExprNode::TPtr DelegatedNode;
+ };
+ std::shared_ptr<TNodeMap<TDelegatedInfo>> Delegated_;
+};
+
+}
+
+THolder<TExecTransformerBase> CreateYtDataSinkExecTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSinkExecTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
new file mode 100644
index 0000000000..9a207259d8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_finalize.cpp
@@ -0,0 +1,38 @@
+#include "yql_yt_provider_impl.h"
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtDataSinkFinalizingTransformer: public TAsyncCallbackTransformer<TYtDataSinkFinalizingTransformer> {
+public:
+ TYtDataSinkFinalizingTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+ }
+
+ std::pair<TStatus, TAsyncTransformCallbackFuture> CallbackTransform(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ Y_UNUSED(ctx);
+ output = input;
+ auto future = State_->Gateway->Finalize(
+ IYtGateway::TFinalizeOptions(State_->SessionId)
+ .Config(State_->Configuration->Snapshot())
+ .Abort(State_->Types->HiddenMode == EHiddenMode::Force)
+ );
+ return WrapFuture(future, [](const IYtGateway::TFinalizeResult& res, const TExprNode::TPtr& input, TExprContext& ctx) {
+ Y_UNUSED(res);
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+
+private:
+ TYtState::TPtr State_;
+};
+
+THolder<IGraphTransformer> CreateYtDataSinkFinalizingTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSinkFinalizingTransformer(state));
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
new file mode 100644
index 0000000000..f594a8025a
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_trackable.cpp
@@ -0,0 +1,62 @@
+#include "yql_yt_provider_impl.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtDataSinkTrackableCleanupTransformer : public TAsyncCallbackTransformer<TYtDataSinkTrackableCleanupTransformer> {
+public:
+ TYtDataSinkTrackableCleanupTransformer(TYtState::TPtr state)
+ : State_(state) {
+ }
+
+ std::pair<TStatus, TAsyncTransformCallbackFuture>
+ CallbackTransform(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ Y_UNUSED(ctx);
+ output = input;
+ auto options = IYtGateway::TDropTrackablesOptions(State_->SessionId)
+ .Config(State_->Configuration->GetSettingsForNode(*input));
+
+
+ YQL_ENSURE(input->IsList());
+ input->ForEachChild([&options](const TExprNode& node)
+ {
+ YQL_ENSURE(node.IsList());
+ YQL_ENSURE(node.ChildrenSize() == 2);
+ auto ytDataSink = TMaybeNode<TYtDSink>(node.Child(0));
+ YQL_ENSURE(ytDataSink);
+ auto ytOutTable = TMaybeNode<TYtOutTable>(node.Child(1));
+ YQL_ENSURE(ytOutTable);
+
+ IYtGateway::TDropTrackablesOptions::TClusterAndPath clusterAndPath;
+ clusterAndPath.Path = TString{ytOutTable.Cast().Name().Value()};
+ clusterAndPath.Cluster = TString{ytDataSink.Cast().Cluster().Value()};
+
+ options.Pathes().push_back(clusterAndPath);
+ });
+
+ auto future = State_->Gateway->DropTrackables(std::move(options));
+
+ return WrapFuture(future,
+ [](const IYtGateway::TDropTrackablesResult& res, const TExprNode::TPtr& input, TExprContext& ctx) {
+ Y_UNUSED(res);
+ return ctx.NewWorld(input->Pos());
+ });
+ }
+private:
+ TYtState::TPtr State_;
+};
+
+}
+
+THolder<IGraphTransformer> CreateYtDataSinkTrackableNodesCleanupTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSinkTrackableCleanupTransformer(state));
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
new file mode 100644
index 0000000000..3e5115a842
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasink_type_ann.cpp
@@ -0,0 +1,2001 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_table.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_join.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/xrange.h>
+
+namespace NYql {
+
+namespace {
+
+bool IsWideRepresentation(const TTypeAnnotationNode* leftType, const TTypeAnnotationNode* rightType) {
+ const auto structType = dynamic_cast<const TStructExprType*>(leftType);
+ const auto multiType = dynamic_cast<const TMultiExprType*>(rightType);
+ if (!structType || !multiType || structType->GetSize() != multiType->GetSize())
+ return false;
+
+ const auto& structItems = structType->GetItems();
+ const auto& multiItems = multiType->GetItems();
+
+ for (auto i = 0U; i < multiItems.size(); ++i)
+ if (!IsSameAnnotation(*multiItems[i], *structItems[i]->GetItemType()))
+ return false;
+
+ return true;
+}
+
+const TTypeAnnotationNode* MakeInputType(const TTypeAnnotationNode* itemType, const TExprNode::TPtr& setting, TExprContext& ctx) {
+ if (!setting)
+ return ctx.MakeType<TStreamExprType>(itemType);
+
+ if (const auto structType = dynamic_cast<const TStructExprType*>(itemType)) {
+ if (ui32 limit; structType && 2U == setting->ChildrenSize() && TryFromString<ui32>(setting->Tail().Content(), limit) && structType->GetSize() < limit && structType->GetSize() > 0U) {
+ TTypeAnnotationNode::TListType types;
+ const auto& items = structType->GetItems();
+ types.reserve(items.size());
+ std::transform(items.cbegin(), items.cend(), std::back_inserter(types), std::bind(&TItemExprType::GetItemType, std::placeholders::_1));
+ return ctx.MakeType<TFlowExprType>(ctx.MakeType<TMultiExprType>(types));
+ }
+ }
+
+ return ctx.MakeType<TFlowExprType>(itemType);
+}
+
+using namespace NNodes;
+
+class TYtDataSinkTypeAnnotationTransformer : public TVisitorTransformerBase {
+public:
+ TYtDataSinkTypeAnnotationTransformer(TYtState::TPtr state)
+ : TVisitorTransformerBase(true)
+ , State_(state)
+ {
+ AddHandler({TYtOutTable::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleOutTable));
+ AddHandler({TYtOutput::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleOutput));
+ AddHandler({TYtSort::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleSort));
+ AddHandler({TYtCopy::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleCopy));
+ AddHandler({TYtMerge::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleMerge));
+ AddHandler({TYtMap::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleMap));
+ AddHandler({TYtReduce::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleReduce));
+ AddHandler({TYtMapReduce::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleMapReduce));
+ AddHandler({TYtWriteTable::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleWriteTable));
+ AddHandler({TYtFill::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleFill));
+ AddHandler({TYtTouch::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleTouch));
+ AddHandler({TYtDropTable::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleDropTable));
+ AddHandler({TCoCommit::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleCommit));
+ AddHandler({TYtPublish::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandlePublish));
+ AddHandler({TYtEquiJoin::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleEquiJoin));
+ AddHandler({TYtStatOutTable::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleStatOutTable));
+ AddHandler({TYtStatOut::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleStatOut));
+ AddHandler({TYtDqProcessWrite ::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleYtDqProcessWrite));
+ AddHandler({TYtDqWrite::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleDqWrite<false>));
+ AddHandler({TYtDqWideWrite::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleDqWrite<true>));
+ AddHandler({TYtTryFirst::CallableName()}, Hndl(&TYtDataSinkTypeAnnotationTransformer::HandleTryFirst));
+ }
+
+private:
+ static bool ValidateOpBase(const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (!EnsureWorldType(*input->Child(TYtOpBase::idx_World), ctx)) {
+ return false;
+ }
+
+ if (!EnsureSpecificDataSink(*input->Child(TYtOpBase::idx_DataSink), YtProviderName, ctx)) {
+ return false;
+ }
+ return true;
+ }
+
+ static bool ValidateOutputOpBase(const TExprNode::TPtr& input, TExprContext& ctx, bool multiIO) {
+ if (!ValidateOpBase(input, ctx)) {
+ return false;
+ }
+
+ // Output
+ if (multiIO) {
+ if (!EnsureTupleMinSize(*input->Child(TYtOutputOpBase::idx_Output), 1, ctx)) {
+ return false;
+ }
+ } else {
+ if (!EnsureTupleSize(*input->Child(TYtOutputOpBase::idx_Output), 1, ctx)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ TStatus ValidateAndUpdateTransientOpBase(const TExprNode::TPtr& input, TExprNode::TPtr& output,
+ TExprContext& ctx, bool multiIO, EYtSettingTypes allowedSectionSettings) const {
+ if (!ValidateOutputOpBase(input, ctx, multiIO)) {
+ return TStatus::Error;
+ }
+
+ // Input
+ if (multiIO) {
+ if (!EnsureTupleMinSize(*input->Child(TYtTransientOpBase::idx_Input), 1, ctx)) {
+ return TStatus::Error;
+ }
+ } else {
+ if (!EnsureTupleSize(*input->Child(TYtTransientOpBase::idx_Input), 1, ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ for (auto& section: input->Child(TYtTransientOpBase::idx_Input)->Children()) {
+ if (!section->IsCallable(TYtSection::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(section->Pos()), TStringBuilder() << "Expected " << TYtSection::CallableName()));
+ return TStatus::Error;
+ }
+ }
+
+ auto clusterName = TString{TYtDSink(input->ChildPtr(TYtTransientOpBase::idx_DataSink)).Cluster().Value()};
+ TMaybe<bool> yamrFormat;
+ TMaybe<TSampleParams> sampling;
+ for (size_t i = 0; i < input->Child(TYtTransientOpBase::idx_Input)->ChildrenSize(); ++i) {
+ auto section = TYtSection(input->Child(TYtTransientOpBase::idx_Input)->ChildPtr(i));
+ for (auto setting: section.Settings()) {
+ if (!allowedSectionSettings.HasFlags(FromString<EYtSettingType>(setting.Name().Value()))) {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), TStringBuilder()
+ << "Settings " << TString{setting.Name().Value()}.Quote() << " is not allowed in "
+ << input->Content() << " section"));
+ return TStatus::Error;
+ }
+ }
+
+ if (0 == i) {
+ sampling = NYql::GetSampleParams(section.Settings().Ref());
+ } else if (NYql::GetSampleParams(section.Settings().Ref()) != sampling) {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), "Sections have different sample values"));
+ return TStatus::Error;
+ }
+
+ for (auto path: section.Paths()) {
+ if (auto maybeTable = path.Table().Maybe<TYtTable>()) {
+ auto table = maybeTable.Cast();
+ auto tableName = table.Name().Value();
+ if (!NYql::HasSetting(table.Settings().Ref(), EYtSettingType::UserSchema)) {
+ // Don't validate already substituted anonymous tables
+ if (!TYtTableInfo::HasSubstAnonymousLabel(table)) {
+ const TYtTableDescription& tableDesc = State_->TablesData->GetTable(clusterName,
+ TString{tableName},
+ TEpochInfo::Parse(table.Epoch().Ref()));
+
+ if (!tableDesc.Validate(ctx.GetPosition(table.Pos()), clusterName, tableName,
+ NYql::HasSetting(table.Settings().Ref(), EYtSettingType::WithQB), State_->AnonymousLabels, ctx)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+ if (yamrFormat) {
+ if (*yamrFormat != path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Mixed Yamr/Yson input table formats"));
+ return TStatus::Error;
+ }
+ } else {
+ yamrFormat = path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+ }
+ }
+ }
+
+ // Basic Settings validation
+ if (!EnsureTuple(*input->Child(TYtTransientOpBase::idx_Settings), ctx)) {
+ return TStatus::Error;
+ }
+
+ for (auto& setting: input->Child(TYtTransientOpBase::idx_Settings)->Children()) {
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*setting->Child(0), ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ auto opInput = input->ChildPtr(TYtTransientOpBase::idx_Input);
+ auto newInput = ValidateAndUpdateTablesMeta(opInput, clusterName, State_->TablesData, State_->Types->UseTableMetaFromGraph, ctx);
+ if (!newInput) {
+ return TStatus::Error;
+ }
+ else if (newInput != opInput) {
+ output = ctx.ChangeChild(*input, TYtTransientOpBase::idx_Input, std::move(newInput));
+ return TStatus::Repeat;
+ }
+
+ return TStatus::Ok;
+ }
+
+ static bool ValidateOutputType(const TTypeAnnotationNode* itemType, TPositionHandle positionHandle, TYtOutSection outTables,
+ size_t beginIdx, size_t endIdx, bool useExtendedType, TExprContext& ctx)
+ {
+ YQL_ENSURE(beginIdx <= endIdx);
+ YQL_ENSURE(endIdx <= outTables.Ref().ChildrenSize());
+ YQL_ENSURE(itemType);
+ const size_t outTablesSize = endIdx - beginIdx;
+ TPosition pos = ctx.GetPosition(positionHandle);
+
+ if (!EnsurePersistableType(positionHandle, *itemType, ctx)) {
+ return false;
+ }
+
+ if (itemType->GetKind() == ETypeAnnotationKind::Variant) {
+ if (itemType->Cast<TVariantExprType>()->GetUnderlyingType()->GetKind() == ETypeAnnotationKind::Tuple) {
+ auto tupleType = itemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ if (tupleType->GetSize() != outTablesSize) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Expected variant of "
+ << outTablesSize << " items, but got: " << tupleType->GetSize()));
+ return false;
+ }
+ for (size_t i = beginIdx; i < endIdx; ++i) {
+ auto rowSpec = TYqlRowSpecInfo(outTables.Item(i).RowSpec());
+ const TTypeAnnotationNode* tableItemType = nullptr;
+ if (useExtendedType) {
+ tableItemType = rowSpec.GetExtendedType(ctx);
+ } else if (!GetSequenceItemType(outTables.Item(i).Ref(), tableItemType, ctx)) {
+ return false;
+ }
+ if (tableItemType->HasBareYson() && 0 != rowSpec.GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Strict Yson type is not allowed to write, please use Optional<Yson>, item type: "
+ << *tableItemType));
+ return false;
+ }
+
+ if (!IsSameAnnotation(*tupleType->GetItems()[i - beginIdx], *tableItemType)) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Output table " << i << " row type differs from the write row type: "
+ << GetTypeDiff(*tableItemType, *tupleType->GetItems()[i - beginIdx])));
+ return false;
+ }
+ }
+ } else {
+ auto structType = itemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TStructExprType>();
+ if (structType->GetSize() != outTablesSize) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Expected variant of "
+ << outTablesSize << " items, but got: " << structType->GetSize()));
+ return false;
+ }
+ for (size_t i = beginIdx; i < endIdx; ++i) {
+ auto rowSpec = TYqlRowSpecInfo(outTables.Item(i).RowSpec());
+ const TTypeAnnotationNode* tableItemType = nullptr;
+ if (useExtendedType) {
+ tableItemType = rowSpec.GetExtendedType(ctx);
+ } else if (!GetSequenceItemType(outTables.Item(i).Ref(), tableItemType, ctx)) {
+ return false;
+ }
+
+ if (tableItemType->HasBareYson() && 0 != rowSpec.GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Strict Yson type is not allowed to write, please use Optional<Yson>, item type: "
+ << *tableItemType));
+ return false;
+ }
+
+ if (!IsSameAnnotation(*structType->GetItems()[i - beginIdx]->GetItemType(), *tableItemType)) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Output table " << i << " row type differs from the write row type: "
+ << GetTypeDiff(*tableItemType, *structType->GetItems()[i - beginIdx]->GetItemType())));
+ return false;
+ }
+ }
+ }
+ } else {
+ if (outTablesSize != 1) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Expected variant of "
+ << outTablesSize << " items, but got single item: " << *itemType));
+ return false;
+ }
+ auto rowSpec = TYqlRowSpecInfo(outTables.Item(0).RowSpec());
+ const TTypeAnnotationNode* tableItemType = nullptr;
+ if (useExtendedType) {
+ tableItemType = rowSpec.GetExtendedType(ctx);
+ } else if (!GetSequenceItemType(outTables.Item(0).Ref(), tableItemType, ctx)) {
+ return false;
+ }
+
+ if (tableItemType->HasBareYson() && 0 != rowSpec.GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Strict Yson type is not allowed to write, please use Optional<Yson>, item type: "
+ << *tableItemType));
+ return false;
+ }
+
+ if (!(IsSameAnnotation(*itemType, *tableItemType) || IsWideRepresentation(tableItemType, itemType))) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Output table row type differs from the write row type: "
+ << GetTypeDiff(*tableItemType, *itemType)));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static bool ValidateOutputType(const TTypeAnnotationNode* itemType, TPositionHandle positionHandle, TYtOutSection outTables,
+ TExprContext& ctx, bool useExtendedType = false)
+ {
+ return ValidateOutputType(itemType, positionHandle, outTables, 0, outTables.Ref().ChildrenSize(), useExtendedType, ctx);
+ }
+
+ static bool ValidateOutputType(const TExprNode& list, TYtOutSection outTables, TExprContext& ctx, bool useExtendedType = false) {
+ const TTypeAnnotationNode* itemType = GetSequenceItemType(list.Pos(), list.GetTypeAnn(), true, ctx);
+ if (nullptr == itemType) {
+ return false;
+ }
+ return ValidateOutputType(itemType, list.Pos(), outTables, ctx, useExtendedType);
+ }
+
+ static bool ValidateColumnListSetting(TPosition pos, TExprContext& ctx, const TTypeAnnotationNode* itemType,
+ const TVector<TString>& columns, EYtSettingType settingType, bool isInput)
+ {
+ TStringBuf inOut = isInput ? " input" : " output";
+ if (itemType->GetKind() == ETypeAnnotationKind::Variant) {
+ TTypeAnnotationNode::TListType columnTypes(columns.size(), nullptr);
+ auto tupleType = itemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ for (size_t t: xrange(tupleType->GetSize())) {
+ auto structType = tupleType->GetItems()[t]->Cast<TStructExprType>();
+ for (size_t i: xrange(columns.size())) {
+ auto id = structType->FindItem(columns[i]);
+ if (!id.Defined()) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << settingType << " field " << columns[i] << " is missing in "
+ << t << inOut << " section type"));
+ return false;
+ }
+ auto columnType = structType->GetItems()[*id]->GetItemType();
+ // Clear optionality before comparing
+ if (columnType->GetKind() == ETypeAnnotationKind::Optional) {
+ columnType = columnType->Cast<TOptionalExprType>()->GetItemType();
+ }
+ if (0 == t) {
+ columnTypes[i] = columnType;
+ } else if (!IsSameAnnotation(*columnTypes[i], *columnType)) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << settingType << " field " << columns[i] << " has different type in "
+ << t << inOut << " section type: "
+ << GetTypeDiff(*columnTypes[i], *columnType)));
+ return false;
+ }
+ }
+ }
+ } else if (ETypeAnnotationKind::Struct == itemType->GetKind()) {
+ auto structType = itemType->Cast<TStructExprType>();
+ for (size_t i: xrange(columns.size())) {
+ auto id = structType->FindItem(columns[i]);
+ if (!id.Defined()) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << settingType << " field " << columns[i] << " is missing in" << inOut << " type"));
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ TStatus ValidateTableWrite(const TPosition& pos, const TExprNode::TPtr& table, TExprNode::TPtr& content, const TTypeAnnotationNode* itemType,
+ const TVector<TYqlRowSpecInfo::TPtr>& contentRowSpecs, const TString& cluster, const EYtWriteMode mode, const bool initialWrite, const bool monotonicKeys, TExprContext& ctx) const
+ {
+ YQL_ENSURE(itemType);
+ if (content && !EnsurePersistableType(content->Pos(), *itemType, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!initialWrite && mode != EYtWriteMode::Append) {
+ ctx.AddError(TIssue(pos, TStringBuilder() <<
+ "Replacing " << TString{table->Child(TYtTable::idx_Name)->Content()}.Quote() << " table content after another table modifications in the same transaction"));
+ return TStatus::Error;
+ }
+
+ auto outTableInfo = TYtTableInfo(table);
+ TYtTableDescription& description = State_->TablesData->GetModifTable(cluster, outTableInfo.Name, outTableInfo.Epoch);
+
+ auto meta = description.Meta;
+ YQL_ENSURE(meta);
+
+ if (meta->SqlView) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Modification of " << outTableInfo.Name.Quote() << " view is not supported"));
+ return TStatus::Error;
+ }
+
+ if (meta->IsDynamic) {
+ ctx.AddError(TIssue(pos, TStringBuilder() <<
+ "Modification of dynamic table " << outTableInfo.Name.Quote() << " is not supported"));
+ return TStatus::Error;
+ }
+
+ bool replaceMeta = !meta->DoesExist || (mode != EYtWriteMode::Append && mode != EYtWriteMode::RenewKeepMeta);
+ bool checkLayout = meta->DoesExist && (mode == EYtWriteMode::Append || mode == EYtWriteMode::RenewKeepMeta || description.IsReplaced);
+
+ if (monotonicKeys && initialWrite && replaceMeta) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Insert with "
+ << ToString(EYtSettingType::MonotonicKeys).Quote()
+ << " setting cannot be used with a non-existent table"));
+ return TStatus::Error;
+ }
+
+ if (auto commitEpoch = outTableInfo.CommitEpoch.GetOrElse(0)) {
+ // Check type compatibility with previous epoch
+ if (auto nextDescription = State_->TablesData->FindTable(cluster, outTableInfo.Name, commitEpoch)) {
+ if (nextDescription->Meta) {
+ if (!nextDescription->Meta->DoesExist) {
+ ctx.AddError(TIssue(pos, TStringBuilder() <<
+ "Table " << outTableInfo.Name << " is modified and dropped in the same transaction"));
+ return TStatus::Error;
+ }
+ checkLayout = !nextDescription->IsReplaced;
+ } else {
+ checkLayout = !replaceMeta;
+ }
+ } else {
+ checkLayout = !replaceMeta;
+ }
+ }
+
+ if (checkLayout) {
+ auto rowSpec = description.RowSpec;
+ TString modeStr = EYtWriteMode::RenewKeepMeta == mode ? "truncate with keep meta" : ToString(mode);
+ if (!rowSpec) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote()
+ << " does not have any scheme attribute supported by YQL, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+ if (description.IgnoreTypeV3) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " has IgnoreTypeV3 remapper, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+ if (description.HasUdfApply) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " has udf remappers, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+ if (meta->InferredScheme) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " has inferred schema, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+ if (!rowSpec->StrictSchema) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " has non-strict schema, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+ if (meta->Attrs.contains(QB2Premapper)) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " has qb2 remapper, " << modeStr << " is not allowed"));
+ return TStatus::Error;
+ }
+
+ if (!IsSameAnnotation(*description.RowType, *itemType)) {
+ if (content) {
+ auto expectedType = ctx.MakeType<TListExprType>(description.RowType);
+ auto status = TryConvertTo(content, *expectedType, ctx);
+ if (status.Level != TStatus::Error) {
+ return status;
+ }
+ }
+
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " row type differs from the written row type: "
+ << GetTypeDiff(*description.RowType, *itemType)));
+ return TStatus::Error;
+ }
+ }
+
+ TMaybe<TColumnOrder> contentColumnOrder;
+ if (content) {
+ contentColumnOrder = State_->Types->LookupColumnOrder(*content);
+ if (content->IsCallable("AssumeColumnOrder")) {
+ YQL_ENSURE(contentColumnOrder);
+ content = content->HeadPtr();
+ }
+ }
+
+ if (auto commitEpoch = outTableInfo.CommitEpoch.GetOrElse(0)) {
+ TYtTableDescription& nextDescription = State_->TablesData->GetOrAddTable(cluster, outTableInfo.Name, commitEpoch);
+
+ if (!nextDescription.Meta) {
+ nextDescription.RowType = itemType;
+ nextDescription.RawRowType = itemType;
+ nextDescription.IsReplaced = replaceMeta;
+
+ TYtTableMetaInfo::TPtr nextMetadata = (nextDescription.Meta = MakeIntrusive<TYtTableMetaInfo>());
+ nextMetadata->DoesExist = true;
+ nextMetadata->YqlCompatibleScheme = true;
+
+ TYqlRowSpecInfo::TPtr nextRowSpec = (nextDescription.RowSpec = MakeIntrusive<TYqlRowSpecInfo>());
+ if (replaceMeta) {
+ nextRowSpec->SetType(itemType->Cast<TStructExprType>(), State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ nextRowSpec->SetColumnOrder(contentColumnOrder);
+ } else {
+ nextRowSpec->CopyType(*description.RowSpec);
+ }
+
+ if (!replaceMeta) {
+ nextRowSpec->StrictSchema = !description.RowSpec || description.RowSpec->StrictSchema;
+ nextMetadata->Attrs = meta->Attrs;
+ }
+ }
+ else {
+ if (!IsSameAnnotation(*nextDescription.RowType, *itemType)) {
+ if (content) {
+ auto expectedType = ctx.MakeType<TListExprType>(nextDescription.RowType);
+ auto status = TryConvertTo(content, *expectedType, ctx);
+ if (status.Level != TStatus::Error) {
+ return status;
+ }
+ }
+
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << outTableInfo.Name.Quote() << " row type differs from the appended row type: "
+ << GetTypeDiff(*nextDescription.RowType, *itemType)));
+ return TStatus::Error;
+ }
+ }
+
+ YQL_ENSURE(nextDescription.RowSpec);
+ if (contentRowSpecs) {
+ size_t from = 0;
+ if (initialWrite) {
+ ++nextDescription.WriteValidateCount;
+ if (nextDescription.IsReplaced) {
+ nextDescription.RowSpec->CopySortness(*contentRowSpecs.front(), TYqlRowSpecInfo::ECopySort::Exact);
+ if (auto contentNativeType = contentRowSpecs.front()->GetNativeYtType()) {
+ nextDescription.RowSpec->CopyTypeOrders(*contentNativeType);
+ }
+ from = 1;
+ } else {
+ nextDescription.MonotonicKeys = monotonicKeys;
+ if (description.RowSpec) {
+ nextDescription.RowSpec->CopySortness(*description.RowSpec, TYqlRowSpecInfo::ECopySort::Exact);
+ const auto currNativeType = description.RowSpec->GetNativeYtType();
+ if (currNativeType && nextDescription.RowSpec->GetNativeYtType() != currNativeType) {
+ nextDescription.RowSpec->CopyTypeOrders(*currNativeType);
+ }
+ }
+ }
+ if (monotonicKeys && !nextDescription.RowSpec->IsSorted()) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Insert with "
+ << ToString(EYtSettingType::MonotonicKeys).Quote()
+ << " setting cannot be used with a unsorted table"));
+ return TStatus::Error;
+ }
+ } else {
+ if (!nextDescription.MonotonicKeys) {
+ nextDescription.MonotonicKeys = monotonicKeys;
+ } else if (*nextDescription.MonotonicKeys != monotonicKeys) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "All appends within the same commit should have the same "
+ << ToString(EYtSettingType::MonotonicKeys).Quote()
+ << " flag"));
+ return TStatus::Error;
+ }
+ }
+
+ const bool uniqueKeys = nextDescription.RowSpec->UniqueKeys;
+ for (size_t s = from; s < contentRowSpecs.size(); ++s) {
+ const bool hasSortChanges = nextDescription.RowSpec->MakeCommonSortness(*contentRowSpecs[s]);
+ const bool breaksSorting = hasSortChanges || !nextDescription.RowSpec->CompareSortness(*contentRowSpecs[s], false);
+ if (monotonicKeys) {
+ if (breaksSorting) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Inserts with "
+ << ToString(EYtSettingType::MonotonicKeys).Quote()
+ << " setting must not change output table sorting"));
+ return TStatus::Error;
+ }
+ nextDescription.RowSpec->UniqueKeys = uniqueKeys;
+ }
+ if (nextDescription.WriteValidateCount < 2) {
+ TStringBuilder warning;
+ if (breaksSorting) {
+ warning << "Sort order of written data differs from the order of "
+ << outTableInfo.Name.Quote() << " table content. Result table content will be ";
+ if (nextDescription.RowSpec->IsSorted()) {
+ warning << "ordered by ";
+ for (size_t i: xrange(nextDescription.RowSpec->SortMembers.size())) {
+ if (i != 0) {
+ warning << ',';
+ }
+ warning << nextDescription.RowSpec->SortMembers[i] << '('
+ << (nextDescription.RowSpec->SortDirections[i] ? "asc" : "desc") << ")";
+ }
+ } else {
+ warning << "unordered";
+ }
+ } else if (uniqueKeys && !nextDescription.RowSpec->UniqueKeys) {
+ warning << "Result table content will have non unique keys";
+ }
+
+ if (warning && !ctx.AddWarning(YqlIssue(pos, EYqlIssueCode::TIssuesIds_EIssueCode_YT_SORT_ORDER_CHANGE, warning))) {
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+ }
+ else if (replaceMeta) {
+ description.RowType = itemType;
+ description.RawRowType = itemType;
+ description.IsReplaced = true;
+
+ description.Meta->DoesExist = true;
+ description.Meta->YqlCompatibleScheme = true;
+ if (!description.RowSpec) {
+ description.RowSpec = MakeIntrusive<TYqlRowSpecInfo>();
+ }
+ description.RowSpec->SetType(itemType->Cast<TStructExprType>());
+ description.RowSpec->SetColumnOrder(contentColumnOrder);
+ }
+
+ return TStatus::Ok;
+ }
+
+ static const TTypeAnnotationNode* GetInputItemType(TYtSectionList input, TExprContext& ctx) {
+ TTypeAnnotationNode::TListType items;
+ for (auto section: input.Cast<TYtSectionList>()) {
+ items.push_back(section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType());
+ }
+ return items.size() == 1
+ ? items.front()
+ : ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(items));
+ }
+
+ static const TTypeAnnotationNode* MakeOutputOperationType(TYtOutputOpBase op, TExprContext& ctx) {
+ TTypeAnnotationNode::TListType items;
+ for (auto out: op.Output()) {
+ items.push_back(out.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType());
+ }
+
+ const TTypeAnnotationNode* itemType = items.size() == 1
+ ? items.front()
+ : ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(items));
+
+ return ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{
+ op.World().Ref().GetTypeAnn(),
+ ctx.MakeType<TListExprType>(itemType)
+ });
+ }
+
+private:
+ TStatus HandleOutTable(TExprBase input, TExprContext& ctx) {
+ if (!TYtOutTableInfo::Validate(input.Ref(), ctx)) {
+ return TStatus::Error;
+ }
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TListExprType>(input.Cast<TYtOutTable>().RowSpec().Ref().GetTypeAnn()));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleOutput(TExprBase input, TExprContext& ctx) {
+ if (!EnsureMinMaxArgsCount(input.Ref(), 2, 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ const auto op = input.Ref().Child(TYtOutput::idx_Operation);
+ if (!(TYtOutputOpBase::Match(op) || TYtTryFirst::Match(op))) {
+ ctx.AddError(TIssue(ctx.GetPosition(op->Pos()), TStringBuilder() << "Expect YT operation, but got: "
+ << op->Content()));
+ return TStatus::Error;
+ }
+ const auto opOut = TYtTryFirst::Match(op) ?
+ op->Tail().Child(TYtOutputOpBase::idx_Output):
+ op->Child(TYtOutputOpBase::idx_Output);
+
+ if (!EnsureAtom(*input.Ptr()->Child(TYtOutput::idx_OutIndex), ctx)) {
+ return TStatus::Error;
+ }
+
+ size_t ndx = 0;
+ if (!TryFromString<size_t>(input.Ptr()->Child(TYtOutput::idx_OutIndex)->Content(), ndx) || ndx >= opOut->ChildrenSize()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ptr()->Child(TYtOutput::idx_Operation)->Pos()), TStringBuilder()
+ << "Bad " << TYtOutput::CallableName() << " output index value: " << input.Ptr()->Child(TYtOutput::idx_OutIndex)->Content()));
+ return TStatus::Error;
+ }
+
+ if (input.Ref().ChildrenSize() == 3) {
+ if (!EnsureAtom(*input.Ref().Child(TYtOutput::idx_Mode), ctx)) {
+ return TStatus::Error;
+ }
+ if (input.Ref().Child(TYtOutput::idx_Mode)->Content() != ToString(EYtSettingType::Unordered)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtOutput::idx_Mode)->Pos()), TStringBuilder()
+ << "Bad " << TYtOutput::CallableName() << " mode: " << input.Ref().Child(TYtOutput::idx_Mode)->Content()));
+ return TStatus::Error;
+ }
+ }
+
+ input.Ptr()->SetTypeAnn(opOut->Child(ndx)->GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleSort(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, false,
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto sort = TYtSort(input);
+
+ TYtOutTableInfo outTableInfo(sort.Output().Item(0));
+ if (!outTableInfo.RowSpec) {
+ ctx.AddError(TIssue(ctx.GetPosition(sort.Output().Item(0).Pos()),
+ TStringBuilder() << TString{YqlRowSpecAttribute}.Quote() << " of " << TYtSort::CallableName() << " output table should be filled"));
+ return TStatus::Error;
+ }
+
+ if (outTableInfo.RowSpec->SortedBy.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(sort.Output().Item(0).Pos()),
+ TStringBuilder() << "SortedBy attribute of " << TString{YqlRowSpecAttribute}.Quote()
+ << " should be filled in output table"));
+ return TStatus::Error;
+ }
+ auto inputType = sort.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ for (auto& field: outTableInfo.RowSpec->SortedBy) {
+ if (!inputType->FindItem(field)) {
+ ctx.AddError(TIssue(ctx.GetPosition(sort.Output().Item(0).Pos()),
+ TStringBuilder() << "SortedBy attribute of " << TString{YqlRowSpecAttribute}.Quote()
+ << " refers to unknown field " << field.Quote()));
+ return TStatus::Error;
+ }
+ }
+
+ if (!ValidateSettings(sort.Settings().Ref(), EYtSettingType::Limit | EYtSettingType::NoDq, ctx)) {
+ return TStatus::Error;
+ }
+
+ for (TYtSection section: sort.Input()) {
+ for (TYtPath path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ if (pathInfo.RequiresRemap()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtSort::CallableName()
+ << " cannot be applied to tables with QB2 premapper, inferred, yamred_dsv, or non-strict schemas"));
+ return TStatus::Error;
+ }
+ if (pathInfo.Table->RowSpec && pathInfo.GetNativeYtTypeFlags() != outTableInfo.RowSpec->GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtSort::CallableName()
+ << " has different input/output native YT types"));
+ return TStatus::Error;
+ }
+ }
+ }
+
+ auto outTypeItems = outTableInfo.RowSpec->GetType()->GetItems();
+ const auto& directions = outTableInfo.RowSpec->SortDirections;
+ const auto& sortedBy = outTableInfo.RowSpec->SortedBy;
+ for (auto& aux: outTableInfo.RowSpec->GetAuxColumns()) {
+ bool adjust = false;
+ for (ui32 i = 0; i < directions.size(); ++i) {
+ if (!directions[i] && sortedBy[i] == aux.first) {
+ adjust = true;
+ break;
+ }
+ }
+
+ auto type = aux.second;
+ if (adjust) {
+ type = ctx.MakeType<TDataExprType>(EDataSlot::String);
+ }
+
+ outTypeItems.push_back(ctx.MakeType<TItemExprType>(aux.first, type));
+ }
+ auto outType = ctx.MakeType<TStructExprType>(outTypeItems);
+
+ if (!IsSameAnnotation(*inputType, *outType)) {
+ ctx.AddError(TIssue(ctx.GetPosition(sort.Output().Item(0).Pos()), TStringBuilder()
+ << "Sort's output row type differs from input row type: "
+ << GetTypeDiff(*inputType, *outType)));
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(sort, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleCopy(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, false, {});
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto copy = TYtCopy(input);
+
+ // YtCopy! has exactly one input table
+ if (!EnsureArgsCount(copy.Input().Item(0).Paths().Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ TYqlRowSpecInfo outRowSpec(copy.Output().Item(0).RowSpec());
+ for (TYtSection section: copy.Input()) {
+ for (TYtPath path: section.Paths()) {
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName() << " cannot be used with range selection"));
+ return TStatus::Error;
+ }
+ auto tableInfo = TYtTableBaseInfo::Parse(path.Table());
+ if (!tableInfo->IsTemp) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName() << " cannot be used with non-temporary tables"));
+ return TStatus::Error;
+ }
+ if (tableInfo->Meta->IsDynamic) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName() << " cannot be used with dynamic tables"));
+ return TStatus::Error;
+ }
+ auto tableColumnsSize = tableInfo->RowSpec ? tableInfo->RowSpec->GetType()->GetSize() : YAMR_FIELDS.size();
+ if (!path.Columns().Maybe<TCoVoid>() && path.Columns().Cast<TExprList>().Size() != tableColumnsSize) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName() << " cannot be used with column selection"));
+ return TStatus::Error;
+ }
+ if (tableInfo->RequiresRemap()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName()
+ << " cannot be applied to tables with QB2 premapper, inferred, yamred_dsv, or non-strict schemas"));
+ return TStatus::Error;
+ }
+ if (tableInfo->RowSpec && tableInfo->RowSpec->GetNativeYtTypeFlags() != outRowSpec.GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtCopy::CallableName()
+ << " has different input/output native YT types"));
+ return TStatus::Error;
+ }
+ }
+ }
+
+ auto inputRowSpec = TYtTableBaseInfo::GetRowSpec(copy.Input().Item(0).Paths().Item(0).Table());
+ if ((inputRowSpec && !inputRowSpec->CompareSortness(outRowSpec)) || (!inputRowSpec && outRowSpec.IsSorted())) {
+ ctx.AddError(TIssue(ctx.GetPosition(copy.Output().Item(0).Pos()), TStringBuilder()
+ << "Input/output tables have different sort order"));
+ return TStatus::Error;
+ }
+
+ // YtCopy! has no settings
+ if (!EnsureTupleSize(copy.Settings().MutableRef(), 0, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputType(copy.Input().Item(0).Paths().Item(0).Ref(), copy.Output(), ctx)) {
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(copy, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleMerge(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, false,
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto merge = TYtMerge(input);
+
+ if (!ValidateSettings(merge.Settings().Ref(), EYtSettingType::ForceTransform | EYtSettingType::CombineChunks | EYtSettingType::Limit | EYtSettingType::KeepSorted | EYtSettingType::NoDq, ctx)) {
+ return TStatus::Error;
+ }
+
+ TYqlRowSpecInfo outRowSpec(merge.Output().Item(0).RowSpec());
+ TVector<TString> outSortedBy = outRowSpec.SortedBy;
+ for (auto section: merge.Input()) {
+ for (auto path: section.Paths()) {
+ if (!IsSameAnnotation(*path.Ref().GetTypeAnn(), *merge.Output().Item(0).Ref().GetTypeAnn())) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder()
+ << "Input/output tables should have the same scheme. Found diff: "
+ << GetTypeDiff(*path.Ref().GetTypeAnn(), *merge.Output().Item(0).Ref().GetTypeAnn())));
+ return TStatus::Error;
+ }
+ TYtPathInfo pathInfo(path);
+ if (pathInfo.RequiresRemap()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtMerge::CallableName()
+ << " cannot be applied to tables with QB2 premapper, inferred, yamred_dsv, or non-strict schemas"));
+ return TStatus::Error;
+ }
+ if (pathInfo.GetNativeYtTypeFlags() != outRowSpec.GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder() << TYtMerge::CallableName()
+ << " has different input/output native YT types"));
+ return TStatus::Error;
+ }
+ auto inputRowSpec = pathInfo.Table->RowSpec;
+ TVector<TString> inSortedBy;
+ TVector<bool> inSortDir;
+ if (inputRowSpec) {
+ inSortedBy = inputRowSpec->SortedBy;
+ inSortDir = inputRowSpec->SortDirections;
+ }
+ if (!outSortedBy.empty()) {
+ if (outSortedBy.size() > inSortedBy.size() ||
+ !std::equal(outSortedBy.begin(), outSortedBy.end(), inSortedBy.begin())) {
+
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder()
+ << "Output table has sortedBy columns " << JoinSeq(",", outSortedBy).Quote()
+ << ", which is not a subset of " << TString{TYtTableBaseInfo::GetTableName(path.Table())}.Quote()
+ << " input table sortedBy columns " << JoinSeq(",", inSortedBy).Quote()));
+ return TStatus::Error;
+ }
+ if (!std::equal(outRowSpec.SortDirections.begin(), outRowSpec.SortDirections.end(), inSortDir.begin())) {
+ ctx.AddError(TIssue(ctx.GetPosition(path.Pos()), TStringBuilder()
+ << "Input/output tables have different sort directions"));
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+
+ if (!ValidateOutputType(merge.Input().Item(0).Ref(), merge.Output(), ctx)) {
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(merge, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleMap(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 6, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, true,
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample | EYtSettingType::SysColumns);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto map = TYtMap(input);
+
+ const EYtSettingTypes accpeted = EYtSettingType::Ordered
+ | EYtSettingType::Limit
+ | EYtSettingType::SortLimitBy
+ | EYtSettingType::WeakFields
+ | EYtSettingType::Sharded
+ | EYtSettingType::JobCount
+ | EYtSettingType::Flow
+ | EYtSettingType::KeepSorted
+ | EYtSettingType::NoDq;
+ if (!ValidateSettings(map.Settings().Ref(), accpeted, ctx)) {
+ return TStatus::Error;
+ }
+ if (map.Output().Size() != 1 && NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Limit)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << EYtSettingType::Limit << " setting is not allowed for operation with multiple outputs"));
+ return TStatus::Error;
+ }
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(map.Settings().Ref(), EYtSettingType::SortLimitBy);
+ if (!sortLimitBy.empty()) {
+ auto outItemType = TYqlRowSpecInfo(map.Output().Item(0).RowSpec()).GetExtendedType(ctx);
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, outItemType, sortLimitBy, EYtSettingType::SortLimitBy, false)) {
+ return TStatus::Error;
+ }
+ }
+
+ status = ConvertToLambda(input->ChildRef(TYtMap::idx_Mapper), ctx, 1);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ const auto inputItemType = GetInputItemType(map.Input(), ctx);
+ const auto useFlow = NYql::GetSetting(map.Settings().Ref(), EYtSettingType::Flow);
+ const auto lambdaInputType = MakeInputType(inputItemType, useFlow, ctx);
+
+ auto& lambda = input->ChildRef(TYtMap::idx_Mapper);
+ if (!UpdateLambdaAllArgumentsTypes(lambda, {lambdaInputType}, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!lambda->GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ if (!(useFlow ? EnsureFlowType(*lambda, ctx) : EnsureStreamType(*lambda, ctx))) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputType(*lambda, map.Output(), ctx, true)) {
+ lambda->SetTypeAnn(nullptr);
+ lambda->SetState(TExprNode::EState::Initial);
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(map, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleReduce(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 6, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, true,
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample | EYtSettingType::SysColumns);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto reduce = TYtReduce(input);
+
+ const EYtSettingTypes accepted = EYtSettingType::ReduceBy
+ | EYtSettingType::Limit
+ | EYtSettingType::SortLimitBy
+ | EYtSettingType::SortBy
+ | EYtSettingType::JoinReduce
+ | EYtSettingType::FirstAsPrimary
+ | EYtSettingType::Flow
+ | EYtSettingType::KeepSorted
+ | EYtSettingType::KeySwitch
+ | EYtSettingType::ReduceInputType
+ | EYtSettingType::NoDq;
+
+ if (!ValidateSettings(reduce.Settings().Ref(), accepted, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (reduce.Output().Size() != 1 && NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::Limit)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << EYtSettingType::Limit << " setting is not allowed in operation with multiple outputs"));
+ return TStatus::Error;
+ }
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(reduce.Settings().Ref(), EYtSettingType::SortLimitBy);
+ if (!sortLimitBy.empty()) {
+ auto outItemType = reduce.Output().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, outItemType, sortLimitBy, EYtSettingType::SortLimitBy, false)) {
+ return TStatus::Error;
+ }
+ }
+
+ status = ConvertToLambda(input->ChildRef(TYtReduce::idx_Reducer), ctx, 1);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ TVector<TString> reduceBy = NYql::GetSettingAsColumnList(reduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ if (reduceBy.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Empty reduceBy option is not allowed in " << input->Content()));
+ return TStatus::Error;
+ }
+
+ auto inputItemType = GetInputItemType(reduce.Input(), ctx);
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, inputItemType, reduceBy, EYtSettingType::ReduceBy, true)) {
+ return TStatus::Error;
+ }
+
+ if (NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::SortBy)) {
+ TVector<TString> sortBy = NYql::GetSettingAsColumnList(reduce.Settings().Ref(), EYtSettingType::SortBy);
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, inputItemType, sortBy, EYtSettingType::SortBy, true)) {
+ return TStatus::Error;
+ }
+ }
+
+ if (NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::KeySwitch)) {
+ if (inputItemType->GetKind() == ETypeAnnotationKind::Variant) {
+ auto underTupleType = inputItemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ TTypeAnnotationNode::TListType newTupleItems;
+ for (size_t i = 0; i < underTupleType->GetSize(); ++i) {
+ auto tupleItemType = underTupleType->GetItems()[i];
+ auto items = tupleItemType->Cast<TStructExprType>()->GetItems();
+ items.push_back(ctx.MakeType<TItemExprType>(YqlSysColumnKeySwitch, ctx.MakeType<TDataExprType>(EDataSlot::Bool)));
+ newTupleItems.push_back(ctx.MakeType<TStructExprType>(items));
+ }
+ inputItemType = ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(newTupleItems));
+ } else {
+ auto items = inputItemType->Cast<TStructExprType>()->GetItems();
+ items.push_back(ctx.MakeType<TItemExprType>(YqlSysColumnKeySwitch, ctx.MakeType<TDataExprType>(EDataSlot::Bool)));
+ inputItemType = ctx.MakeType<TStructExprType>(items);
+ }
+ }
+
+ if (NYql::HasSetting(reduce.Settings().Ref(), EYtSettingType::JoinReduce)) {
+ // Require at least two sections
+ if (!EnsureTupleMinSize(reduce.Input().MutableRef(), 2, ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ const auto useFlow = NYql::GetSetting(reduce.Settings().Ref(), EYtSettingType::Flow);
+ const auto lambdaInputType = MakeInputType(inputItemType, useFlow, ctx);
+
+ auto& lambda = input->ChildRef(TYtReduce::idx_Reducer);
+ if (!UpdateLambdaAllArgumentsTypes(lambda, {lambdaInputType}, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!lambda->GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ if (!(useFlow ? EnsureFlowType(*lambda, ctx) : EnsureStreamType(*lambda, ctx))) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputType(*lambda, reduce.Output(), ctx)) {
+ lambda->SetTypeAnn(nullptr);
+ lambda->SetState(TExprNode::EState::Initial);
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(reduce, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleMapReduce(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 7, ctx)) {
+ return TStatus::Error;
+ }
+
+ const bool hasMapLambda = !TCoVoid::Match(input->Child(TYtMapReduce::idx_Mapper));
+ EYtSettingTypes sectionSettings = EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample;
+ if (hasMapLambda) {
+ sectionSettings |= EYtSettingType::SysColumns;
+ }
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, true, sectionSettings);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto mapReduce = TYtMapReduce(input);
+
+ const auto acceptedSettings = EYtSettingType::ReduceBy
+ | EYtSettingType::ReduceFilterBy
+ | EYtSettingType::SortBy
+ | EYtSettingType::Limit
+ | EYtSettingType::SortLimitBy
+ | EYtSettingType::WeakFields
+ | EYtSettingType::Flow
+ | EYtSettingType::KeySwitch
+ | EYtSettingType::MapOutputType
+ | EYtSettingType::ReduceInputType
+ | EYtSettingType::NoDq;
+ if (!ValidateSettings(mapReduce.Settings().Ref(), acceptedSettings, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (mapReduce.Output().Size() != 1 && NYql::HasSetting(mapReduce.Settings().Ref(), EYtSettingType::Limit)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << EYtSettingType::Limit << " setting is not allowed in operation with multiple outputs"));
+ return TStatus::Error;
+ }
+ TVector<TString> sortLimitBy = NYql::GetSettingAsColumnList(mapReduce.Settings().Ref(), EYtSettingType::SortLimitBy);
+ if (!sortLimitBy.empty()) {
+ auto outItemType = mapReduce.Output().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, outItemType, sortLimitBy, EYtSettingType::SortLimitBy, false)) {
+ return TStatus::Error;
+ }
+ }
+
+ // Ensure output table is not sorted
+ for (auto out: mapReduce.Output()) {
+ if (TYqlRowSpecInfo(out.RowSpec()).IsSorted()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << TYtMapReduce::CallableName() << " cannot produce sorted output"));
+ return TStatus::Error;
+ }
+ }
+
+ status = TStatus::Ok;
+ if (hasMapLambda) {
+ status = status.Combine(ConvertToLambda(input->ChildRef(TYtMapReduce::idx_Mapper), ctx, 1));
+ }
+ status = status.Combine(ConvertToLambda(input->ChildRef(TYtMapReduce::idx_Reducer), ctx, 1));
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ auto itemType = GetInputItemType(mapReduce.Input(), ctx);
+ const auto useFlow = NYql::GetSetting(mapReduce.Settings().Ref(), EYtSettingType::Flow);
+
+ auto& mapLambda = input->ChildRef(TYtMapReduce::idx_Mapper);
+ TTypeAnnotationNode::TListType mapDirectOutputTypes;
+ if (hasMapLambda) {
+ const auto mapLambdaInputType = MakeInputType(itemType, useFlow, ctx);
+
+ if (!UpdateLambdaAllArgumentsTypes(mapLambda, {mapLambdaInputType}, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!mapLambda->GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ if (!(useFlow ? EnsureFlowType(*mapLambda, ctx) : EnsureStreamType(*mapLambda, ctx))) {
+ return TStatus::Error;
+ }
+
+ itemType = GetSequenceItemType(mapLambda->Pos(), mapLambda->GetTypeAnn(), true, ctx);
+ if (!itemType) {
+ return TStatus::Error;
+ }
+
+ if (!EnsurePersistableType(mapLambda->Pos(), *itemType, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (itemType->GetKind() == ETypeAnnotationKind::Variant) {
+ auto tupleType = itemType->Cast<TVariantExprType>()->GetUnderlyingType();
+ if (tupleType->GetKind() != ETypeAnnotationKind::Tuple) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << "Expected Variant over Tuple as map output type, but got: " << *itemType));
+ return TStatus::Error;
+ }
+
+ auto mapOutputs = tupleType->Cast<TTupleExprType>()->GetItems();
+ YQL_ENSURE(!mapOutputs.empty(), "Got Variant over empty tuple");
+ itemType = mapOutputs.front();
+ for (size_t i = 1; i < mapOutputs.size(); ++i) {
+ if (mapOutputs[i]->GetKind() != ETypeAnnotationKind::Struct) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << "Expected Struct as map direct output #" << i << ", but got: " << *mapOutputs[i]));
+ return TStatus::Error;
+ }
+ }
+ mapDirectOutputTypes.assign(mapOutputs.begin() + 1, mapOutputs.end());
+ }
+
+ if (const auto kind = itemType->GetKind(); ETypeAnnotationKind::Multi != kind && ETypeAnnotationKind::Struct != kind) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << "Expected Struct or Multi as map output item type, but got: " << *itemType));
+ return TStatus::Error;
+ }
+ }
+
+ TVector<TString> reduceBy = NYql::GetSettingAsColumnList(mapReduce.Settings().Ref(), EYtSettingType::ReduceBy);
+ if (reduceBy.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << "Empty " << EYtSettingType::ReduceBy << " option is not allowed in " << input->Content()));
+ return TStatus::Error;
+ }
+
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, itemType, reduceBy, EYtSettingType::ReduceBy, true)) {
+ return TStatus::Error;
+ }
+
+ if (NYql::HasSetting(mapReduce.Settings().Ref(), EYtSettingType::SortBy)) {
+ TVector<TString> sortBy = NYql::GetSettingAsColumnList(mapReduce.Settings().Ref(), EYtSettingType::SortBy);
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, itemType, sortBy, EYtSettingType::SortBy, true)) {
+ return TStatus::Error;
+ }
+ }
+
+ if (const auto reduceInputTypeSetting = NYql::GetSetting(mapReduce.Settings().Ref(), EYtSettingType::ReduceInputType)) {
+ itemType = reduceInputTypeSetting->Tail().GetTypeAnn()->Cast<TTypeExprType>()->GetType();
+ } else {
+ if (NYql::HasSetting(mapReduce.Settings().Ref(), EYtSettingType::ReduceFilterBy)) {
+ TVector<TString> reduceFilterBy = NYql::GetSettingAsColumnList(mapReduce.Settings().Ref(), EYtSettingType::ReduceFilterBy);
+ if (!ValidateColumnListSetting(ctx.GetPosition(input->Pos()), ctx, itemType, reduceFilterBy, EYtSettingType::ReduceFilterBy, true)) {
+ return TStatus::Error;
+ }
+
+ auto structType = itemType->Cast<TStructExprType>();
+ TVector<const TItemExprType*> filteredItems;
+ for (auto& column: reduceFilterBy) {
+ filteredItems.push_back(structType->GetItems()[*structType->FindItem(column)]);
+ }
+
+ itemType = ctx.MakeType<TStructExprType>(filteredItems);
+ }
+
+ if (NYql::HasSetting(mapReduce.Settings().Ref(), EYtSettingType::KeySwitch)) {
+ if (itemType->GetKind() == ETypeAnnotationKind::Variant) {
+ auto underTupleType = itemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ TTypeAnnotationNode::TListType newTupleItems;
+ for (size_t i = 0; i < underTupleType->GetSize(); ++i) {
+ auto tupleItemType = underTupleType->GetItems()[i];
+ auto items = tupleItemType->Cast<TStructExprType>()->GetItems();
+ items.push_back(ctx.MakeType<TItemExprType>(YqlSysColumnKeySwitch, ctx.MakeType<TDataExprType>(EDataSlot::Bool)));
+ newTupleItems.push_back(ctx.MakeType<TStructExprType>(items));
+ }
+ itemType = ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(newTupleItems));
+ } else {
+ auto items = itemType->Cast<TStructExprType>()->GetItems();
+ items.push_back(ctx.MakeType<TItemExprType>(YqlSysColumnKeySwitch, ctx.MakeType<TDataExprType>(EDataSlot::Bool)));
+ itemType = ctx.MakeType<TStructExprType>(items);
+ }
+ }
+ }
+
+ auto& reduceLambda = input->ChildRef(TYtMapReduce::idx_Reducer);
+ const auto reduceLambdaInputType = MakeInputType(itemType, useFlow, ctx);
+
+ if (!UpdateLambdaAllArgumentsTypes(reduceLambda, {reduceLambdaInputType}, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!reduceLambda->GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ if (!(useFlow ? EnsureFlowType(*reduceLambda, ctx) : EnsureStreamType(*reduceLambda, ctx))) {
+ return TStatus::Error;
+ }
+
+ size_t mapDirects = mapDirectOutputTypes.size();
+ if (mapDirects == 0) {
+ if (!ValidateOutputType(*reduceLambda, mapReduce.Output(), ctx)) {
+ return TStatus::Error;
+ }
+ } else {
+ const size_t outputsSize = mapReduce.Output().Ref().ChildrenSize();
+ if (mapDirects >= outputsSize) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder()
+ << "Too many map lambda direct outputs: " << mapDirects << ". Total outputs is " << outputsSize));
+ return TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* mapOut = ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(mapDirectOutputTypes));
+ if (!ValidateOutputType(mapOut, mapLambda->Pos(), mapReduce.Output(), 0, mapDirects, false, ctx)) {
+ return TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* reduceOutType = GetSequenceItemType(reduceLambda->Pos(), reduceLambda->GetTypeAnn(), true, ctx);
+ if (!reduceOutType) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputType(reduceOutType, reduceLambda->Pos(), mapReduce.Output(), mapDirects, outputsSize, false, ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(mapReduce, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleWriteTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (input->Child(TYtWriteTable::idx_Content)->IsList()) {
+ output = ctx.ChangeChild(*input, TYtWriteTable::idx_Content,
+ Build<TCoAsList>(ctx, input->Pos())
+ .Add(input->Child(TYtWriteTable::idx_Content)->ChildrenList())
+ .Done()
+ .Ptr()
+ );
+ return TStatus::Repeat;
+ }
+
+ if (input->Child(TYtWriteTable::idx_Content)->GetTypeAnn()->GetKind() == ETypeAnnotationKind::EmptyList) {
+ output = ctx.ChangeChild(*input, TYtWriteTable::idx_Content,
+ Build<TCoList>(ctx, input->Pos())
+ .ListType<TCoListType>()
+ .ItemType<TCoStructType>()
+ .Build()
+ .Build()
+ .Done().Ptr()
+ );
+ return TStatus::Repeat;
+ }
+
+ if (!ValidateOpBase(input, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto table = input->ChildPtr(TYtWriteTable::idx_Table);
+ if (!EnsureCallable(*table, ctx)) {
+ return TStatus::Error;
+ }
+ if (!table->IsCallable(TYtTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(table->Pos()), TStringBuilder()
+ << "Unexpected callable: " << table->Content()));
+ return TStatus::Error;
+ }
+
+ auto settings = input->Child(TYtWriteTable::idx_Settings);
+ if (!EnsureTuple(*settings, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateSettings(*settings, EYtSettingType::Mode
+ | EYtSettingType::Initial
+ | EYtSettingType::CompressionCodec
+ | EYtSettingType::ErasureCodec
+ | EYtSettingType::ReplicationFactor
+ | EYtSettingType::UserAttrs
+ | EYtSettingType::Media
+ | EYtSettingType::PrimaryMedium
+ | EYtSettingType::Expiration
+ | EYtSettingType::MonotonicKeys
+ , ctx))
+ {
+ return TStatus::Error;
+ }
+
+ TExprNode::TPtr newTable;
+ auto status = UpdateTableMeta(table, newTable, State_->TablesData, false, State_->Types->UseTableMetaFromGraph, ctx);
+ if (TStatus::Ok != status.Level) {
+ if (TStatus::Error != status.Level && newTable != table) {
+ output = ctx.ChangeChild(*input, TYtWriteTable::idx_Table, std::move(newTable));
+ }
+ return status.Combine(TStatus::Repeat);
+ }
+
+ EYtWriteMode mode = EYtWriteMode::Renew;
+ if (auto modeSetting = NYql::GetSetting(*settings, EYtSettingType::Mode)) {
+ mode = FromString<EYtWriteMode>(modeSetting->Child(1)->Content());
+ }
+ const bool initialWrite = NYql::HasSetting(*settings, EYtSettingType::Initial);
+ const bool monotonicKeys = NYql::HasSetting(*settings, EYtSettingType::MonotonicKeys);
+
+ auto writeTable = TYtWriteTable(input);
+ auto cluster = writeTable.DataSink().Cluster().StringValue();
+
+ const TTypeAnnotationNode* itemType = nullptr;
+ if (!GetSequenceItemType(writeTable.Content().Ref(), itemType, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto content = writeTable.Content().Ptr();
+ status = ValidateTableWrite(ctx.GetPosition(input->Pos()), table, content, itemType, {}, cluster, mode, initialWrite, monotonicKeys, ctx);
+ if (TStatus::Error == status.Level) {
+ return status;
+ }
+ else if (content != writeTable.Content().Ptr()) {
+ output = ctx.ChangeChild(*input, TYtWriteTable::idx_Content, std::move(content));
+ return status.Combine(TStatus::Repeat);
+ }
+
+ input->SetTypeAnn(writeTable.World().Ref().GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleFill(const TExprNode::TPtr& input, TExprNode::TPtr& /*output*/, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputOpBase(input, ctx, true)) {
+ return TStatus::Error;
+ }
+
+ auto status = ConvertToLambda(input->ChildRef(TYtFill::idx_Content), ctx, 0);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ // Basic Settings validation
+ if (!EnsureTuple(*input->Child(TYtFill::idx_Settings), ctx)) {
+ return TStatus::Error;
+ }
+
+ for (auto& setting: input->Child(TYtFill::idx_Settings)->Children()) {
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*setting->Child(0), ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ const EYtSettingTypes accepted = EYtSettingType::Flow | EYtSettingType::KeepSorted | EYtSettingType::NoDq;
+ if (!ValidateSettings(*input->Child(TYtFill::idx_Settings), accepted, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto fill = TYtFill(input);
+ auto lambda = fill.Content();
+ if (!lambda.Args().Ref().GetTypeAnn()) {
+ if (!UpdateLambdaArgumentsType(lambda.Ref(), ctx))
+ return TStatus::Error;
+ return TStatus::Repeat;
+ }
+
+ if (!lambda.Ref().GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ const bool useFlow = NYql::HasSetting(fill.Settings().Ref(), EYtSettingType::Flow);
+ if (!(useFlow ? EnsureFlowType(lambda.Ref(), ctx) : EnsureStreamType(lambda.Ref(), ctx))) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputType(lambda.Ref(), fill.Output(), ctx, true)) {
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(fill, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTouch(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOutputOpBase(input.Ptr(), ctx, true)) {
+ return TStatus::Error;
+ }
+
+ TYtTouch touch = input.Cast<TYtTouch>();
+
+ input.Ptr()->SetTypeAnn(MakeOutputOperationType(touch, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleDropTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOpBase(input, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto table = input->ChildPtr(TYtDropTable::idx_Table);
+ if (!EnsureCallable(*table, ctx)) {
+ return TStatus::Error;
+ }
+ if (!table->IsCallable(TYtTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(table->Pos()), TStringBuilder() << "Expected " << TYtTable::CallableName()
+ << " callable, but got " << table->Content()));
+ return TStatus::Error;
+ }
+
+ auto dropTable = TYtDropTable(input);
+ if (!TYtTableInfo::HasSubstAnonymousLabel(dropTable.Table())) {
+ TExprNode::TPtr newTable;
+ auto status = UpdateTableMeta(table, newTable, State_->TablesData, false, State_->Types->UseTableMetaFromGraph, ctx);
+ if (TStatus::Ok != status.Level) {
+ if (TStatus::Error != status.Level && newTable != table) {
+ output = ctx.ChangeChild(*input, TYtWriteTable::idx_Table, std::move(newTable));
+ }
+ return status.Combine(TStatus::Repeat);
+ }
+ auto tableInfo = TYtTableInfo(dropTable.Table());
+ YQL_ENSURE(tableInfo.Meta);
+ if (tableInfo.Meta->SqlView) {
+ ctx.AddError(TIssue(ctx.GetPosition(dropTable.Table().Pos()), TStringBuilder()
+ << "Drop of " << tableInfo.Name.Quote() << " view is not supported"));
+ return TStatus::Error;
+ }
+
+ if (tableInfo.Meta->IsDynamic) {
+ ctx.AddError(TIssue(ctx.GetPosition(dropTable.Table().Pos()), TStringBuilder() <<
+ "Drop of dynamic table " << tableInfo.Name.Quote() << " is not supported"));
+ return TStatus::Error;
+ }
+
+ if (auto commitEpoch = tableInfo.CommitEpoch) {
+ TYtTableDescription& nextDescription = State_->TablesData->GetOrAddTable(
+ TString{dropTable.DataSink().Cluster().Value()},
+ tableInfo.Name,
+ commitEpoch
+ );
+
+ TYtTableMetaInfo::TPtr nextMetadata = nextDescription.Meta;
+ if (!nextMetadata) {
+ nextDescription.RowType = nullptr;
+ nextDescription.RawRowType = nullptr;
+
+ nextMetadata = nextDescription.Meta = MakeIntrusive<TYtTableMetaInfo>();
+ nextMetadata->DoesExist = false;
+ }
+ else if (nextMetadata->DoesExist) {
+ ctx.AddError(TIssue(ctx.GetPosition(dropTable.Table().Pos()), TStringBuilder() <<
+ "Table " << tableInfo.Name << " is modified and dropped in the same transaction"));
+ return TStatus::Error;
+ }
+ }
+ }
+
+ input->SetTypeAnn(dropTable.World().Ref().GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleCommit(TExprBase input, TExprContext& ctx) {
+ auto commit = input.Cast<TCoCommit>();
+ auto settings = NCommon::ParseCommitSettings(commit, ctx);
+
+ if (settings.Epoch) {
+ ui32 epoch = 0;
+ if (!TryFromString(settings.Epoch.Cast().Value(), epoch)) {
+ ctx.AddError(TIssue(ctx.GetPosition(commit.Pos()), TStringBuilder() << "Bad commit epoch: "
+ << settings.Epoch.Cast().Value()));
+ return TStatus::Error;
+ }
+ }
+
+ if (!settings.EnsureModeEmpty(ctx)) {
+ return TStatus::Error;
+ }
+ if (!settings.EnsureOtherEmpty(ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(commit.World().Ref().GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandlePublish(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOpBase(input, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureTupleMinSize(*input->Child(TYtPublish::idx_Input), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* itemType = nullptr;
+ for (auto& child: input->Child(TYtPublish::idx_Input)->Children()) {
+ if (!EnsureCallable(*child, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!child->IsCallable(TYtOutput::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << "Expected " << TYtOutput::CallableName() << " callable, but got "
+ << child->Content()));
+ return TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* childItemType = nullptr;
+ if (!GetSequenceItemType(*child, childItemType, ctx)) {
+ return TStatus::Error;
+ }
+ if (nullptr == itemType) {
+ itemType = childItemType;
+ } else if (!IsSameAnnotation(*itemType, *childItemType)) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << TYtPublish::CallableName() << " inputs have different item types: "
+ << GetTypeDiff(*itemType, *childItemType)));
+ return TStatus::Error;
+ }
+ }
+
+ auto table = input->ChildPtr(TYtPublish::idx_Publish);
+ if (!EnsureCallable(*table, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!table->IsCallable(TYtTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(table->Pos()), TStringBuilder() << "Expected " << TYtTable::CallableName()
+ << " callable, but got " << table->Content()));
+ return TStatus::Error;
+ }
+
+ auto settings = input->Child(TYtPublish::idx_Settings);
+ if (!EnsureTuple(*settings, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateSettings(*settings, EYtSettingType::Mode
+ | EYtSettingType::Initial
+ | EYtSettingType::CompressionCodec
+ | EYtSettingType::ErasureCodec
+ | EYtSettingType::ReplicationFactor
+ | EYtSettingType::UserAttrs
+ | EYtSettingType::Media
+ | EYtSettingType::PrimaryMedium
+ | EYtSettingType::Expiration
+ | EYtSettingType::MonotonicKeys
+ , ctx))
+ {
+ return TStatus::Error;
+ }
+
+ if (!NYql::HasSetting(*table->Child(TYtTable::idx_Settings), EYtSettingType::Anonymous)
+ || !table->Child(TYtTable::idx_Name)->Content().StartsWith("tmp/"))
+ {
+ EYtWriteMode mode = EYtWriteMode::Renew;
+ if (auto modeSetting = NYql::GetSetting(*settings, EYtSettingType::Mode)) {
+ mode = FromString<EYtWriteMode>(modeSetting->Child(1)->Content());
+ }
+ const bool initialWrite = NYql::HasSetting(*settings, EYtSettingType::Initial);
+ const bool monotonicKeys = NYql::HasSetting(*settings, EYtSettingType::MonotonicKeys);
+
+ auto publish = TYtPublish(input);
+
+ TVector<TYqlRowSpecInfo::TPtr> contentRowSpecs;
+ for (auto out: publish.Input()) {
+ contentRowSpecs.push_back(MakeIntrusive<TYqlRowSpecInfo>(GetOutTable(out).Cast<TYtOutTable>().RowSpec()));
+ if (IsUnorderedOutput(out)) {
+ contentRowSpecs.back()->ClearSortness();
+ }
+ }
+ TExprNode::TPtr content; // Don't try to convert content
+ auto status = ValidateTableWrite(ctx.GetPosition(input->Pos()), table, content, itemType, contentRowSpecs, TString{publish.DataSink().Cluster().Value()}, mode, initialWrite, monotonicKeys, ctx);
+ if (TStatus::Ok != status.Level) {
+ return status;
+ }
+
+ TExprNode::TPtr newTable;
+ status = UpdateTableMeta(table, newTable, State_->TablesData, false, State_->Types->UseTableMetaFromGraph, ctx);
+ if (TStatus::Ok != status.Level) {
+ if (TStatus::Error != status.Level && newTable != table) {
+ output = ctx.ChangeChild(*input, TYtPublish::idx_Publish, std::move(newTable));
+ }
+ return status.Combine(TStatus::Repeat);
+ }
+ }
+
+ input->SetTypeAnn(input->Child(TYtPublish::idx_World)->GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleEquiJoin(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureMinArgsCount(*input, 8, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto status = ValidateAndUpdateTransientOpBase(input, output, ctx, true,
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::JoinLabel | EYtSettingType::StatColumns | EYtSettingType::SysColumns);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ // Only one output
+ if (!EnsureTupleSize(*input->Child(TYtEquiJoin::idx_Output), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateSettings(*input->Child(TYtEquiJoin::idx_Settings), EYtSettingType::Limit, ctx)) {
+ return TStatus::Error;
+ }
+
+ const auto inputsCount = input->ChildPtr(TYtEquiJoin::idx_Input)->ChildrenSize();
+ if (!EnsureArgsCount(*input, 7U + inputsCount, ctx)) {
+ return TStatus::Error;
+ }
+
+ TJoinLabels labels;
+ for (ui32 i = 0; i < inputsCount; ++i) {
+ const TYtSection section(input->ChildPtr(TYtEquiJoin::idx_Input)->ChildPtr(i));
+ if (auto label = NYql::GetSetting(section.Settings().Ref(), EYtSettingType::JoinLabel)) {
+ auto itemType = GetSequenceItemType(section, false, ctx);
+ if (!itemType || !EnsurePersistableType(section.Pos(), *itemType, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (auto& lambda = input->ChildRef(i + 7U); lambda->IsLambda()) {
+ if (!UpdateLambdaAllArgumentsTypes(lambda, {GetSequenceItemType(section, false, ctx)}, ctx))
+ return TStatus::Error;
+
+ if (!lambda->GetTypeAnn()) {
+ return TStatus::Repeat;
+ }
+
+ itemType = GetSequenceItemType(TExprBase(lambda), false, ctx);
+ if (!itemType || !EnsurePersistableType(lambda->Pos(), *itemType, ctx)) {
+ return TStatus::Error;
+ }
+ } else if (!lambda->IsCallable("Void")) {
+ ctx.AddError(TIssue(ctx.GetPosition(lambda->Pos()), TStringBuilder()
+ << "Premap node should be either lambda or Void, got: " << lambda->Content()));
+ return TStatus::Error;
+ }
+
+ if (auto err = labels.Add(ctx, *label->Child(1), itemType->Cast<TStructExprType>())) {
+ ctx.AddError(*err);
+ return TStatus::Error;
+ }
+ }
+ else {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), TStringBuilder()
+ << "Setting \"" << EYtSettingType::JoinLabel
+ << "\" is required in " << input->Content() << " section"));
+ return TStatus::Error;
+ }
+ }
+
+ TJoinOptions joinOptions;
+ status = ValidateEquiJoinOptions(input->Pos(), *input->Child(TYtEquiJoin::idx_JoinOptions), joinOptions, ctx);
+ if (status != TStatus::Ok) {
+ return status;
+ }
+
+ const TStructExprType* resultType = nullptr;
+ status = EquiJoinAnnotation(input->Pos(), resultType, labels,
+ *input->Child(TYtEquiJoin::idx_Joins), joinOptions, ctx);
+ if (status != TStatus::Ok) {
+ return status;
+ }
+
+ const TYtEquiJoin equiJoin(input);
+ if (!ValidateOutputType(resultType, equiJoin.Pos(), equiJoin.Output(), ctx)) {
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(MakeOutputOperationType(equiJoin, ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleStatOutTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ Y_UNUSED(output);
+
+ if (!EnsureMinArgsCount(*input, 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*input->Child(TYtStatOutTable::idx_Name), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*input->Child(TYtStatOutTable::idx_Scale), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*input->Child(TYtStatOutTable::idx_Cluster), ctx)) {
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(ctx.MakeType<TUnitExprType>());
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleStatOut(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ Y_UNUSED(output);
+
+ if (!EnsureArgsCount(*input, 6, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateOpBase(input, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto inputChild = input->Child(TYtStatOut::idx_Input);
+ if (!TMaybeNode<TYtOutput>(inputChild)) {
+ ctx.AddError(TIssue(ctx.GetPosition(inputChild->Pos()), TStringBuilder()
+ << "Unexpected input: " << inputChild->Content()));
+ return TStatus::Error;
+ }
+
+ auto table = input->Child(TYtStatOut::idx_Table);
+ if (!table->IsCallable(TYtStatOutTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(table->Pos()), TStringBuilder() <<
+ "Unexpected callable: " << table->Content()
+ ));
+ return TStatus::Error;
+ }
+
+ auto replaceMask = input->Child(TYtStatOut::idx_ReplaceMask);
+ if (!EnsureTuple(*replaceMask, ctx)) {
+ return TStatus::Error;
+ }
+
+ for (const auto& child: replaceMask->Children()) {
+ if (!EnsureAtom(*child, ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ auto settings = input->Child(TYtStatOut::idx_Settings);
+ if (!EnsureTuple(*settings, ctx)) {
+ return TStatus::Error;
+ }
+
+ for (auto& child: settings->Children()) {
+ if (!EnsureTupleMinSize(*child, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*child->Child(0), ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ input->SetTypeAnn(input->Child(TYtStatOut::idx_World)->GetTypeAnn());
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleYtDqProcessWrite(const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (!ValidateOutputOpBase(input, ctx, false)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureMinMaxArgsCount(*input, 4, 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureCallable(*input->Child(3), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (input->ChildrenSize() > 4 && !EnsureTupleOfAtoms(input->Tail(), ctx)) {
+ return TStatus::Error;
+ }
+
+ auto processWrite = TYtDqProcessWrite(input);
+
+ input->SetTypeAnn(MakeOutputOperationType(processWrite, ctx));
+ return TStatus::Ok;
+ }
+
+ template <bool Wide>
+ TStatus HandleDqWrite(const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ if constexpr (Wide) {
+ if (!EnsureWideFlowType(input->Head(), ctx)) {
+ return TStatus::Error;
+ }
+ } else {
+ if (!EnsureNewSeqType<false, false, true>(input->Head(), ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ if (!EnsureTuple(input->Tail(), ctx)) {
+ return TStatus::Error;
+ }
+
+ for (auto& setting: input->Tail().Children()) {
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureAtom(*setting->Child(0), ctx)) {
+ return TStatus::Error;
+ }
+ }
+
+ input->SetTypeAnn(MakeSequenceType(input->Head().GetTypeAnn()->GetKind(), *ctx.MakeType<TVoidExprType>(), ctx));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTryFirst(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!TYtOutputOpBase::Match(input.Ref().Child(TYtTryFirst::idx_First))) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtTryFirst::idx_First)->Pos()), TStringBuilder() << "Expect YT operation, but got: "
+ << input.Ref().Child(TYtTryFirst::idx_First)->Content()));
+ return TStatus::Error;
+ }
+
+ if (!TYtOutputOpBase::Match(input.Ref().Child(TYtTryFirst::idx_Second))) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtTryFirst::idx_Second)->Pos()), TStringBuilder() << "Expect YT operation, but got: "
+ << input.Ref().Child(TYtTryFirst::idx_Second)->Content()));
+ return TStatus::Error;
+ }
+
+ if (!IsSameAnnotation(*input.Ref().Child(TYtTryFirst::idx_First)->GetTypeAnn(), *input.Ref().Child(TYtTryFirst::idx_Second)->GetTypeAnn())) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder() << "Both argumensts must be same type."));
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(input.Ref().Head().GetTypeAnn());
+ return TStatus::Ok;
+ }
+private:
+ const TYtState::TPtr State_;
+};
+
+}
+
+THolder<TVisitorTransformerBase> CreateYtDataSinkTypeAnnotationTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSinkTypeAnnotationTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
new file mode 100644
index 0000000000..fc2d5e6ff7
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasource.cpp
@@ -0,0 +1,838 @@
+#include "yql_yt_provider.h"
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_key.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_dq_integration.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/provider/yql_data_provider_impl.h>
+#include <ydb/library/yql/providers/common/transform/yql_lazy_init.h>
+#include <ydb/library/yql/providers/common/config/yql_configuration_transformer.h>
+#include <ydb/library/yql/providers/common/config/yql_dispatch.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtDataSourceTrackableNodeProcessor : public TTrackableNodeProcessorBase {
+public:
+ TYtDataSourceTrackableNodeProcessor(bool collectNodes)
+ : CollectNodes(collectNodes)
+ {
+ }
+
+ void GetUsedNodes(const TExprNode& input, TVector<TString>& usedNodeIds) override {
+ usedNodeIds.clear();
+ if (!CollectNodes) {
+ return;
+ }
+
+ if (auto maybeResPull = TMaybeNode<TResPull>(&input)) {
+ ScanForUsedOutputTables(maybeResPull.Cast().Data().Ref(), usedNodeIds);
+ } else if (auto maybeResFill = TMaybeNode<TResFill>(&input)){
+ ScanForUsedOutputTables(maybeResFill.Cast().Data().Ref(), usedNodeIds);
+ } else if (auto maybeResIf = TMaybeNode<TResIf>(&input)){
+ ScanForUsedOutputTables(maybeResIf.Cast().Condition().Ref(), usedNodeIds);
+ } else if (TMaybeNode<TYtReadTable>(&input)) {
+ ScanForUsedOutputTables(*input.Child(TYtReadTable::idx_Input), usedNodeIds);
+ } else if (TMaybeNode<TYtReadTableScheme>(&input)) {
+ ScanForUsedOutputTables(*input.Child(TYtReadTableScheme::idx_Type), usedNodeIds);
+ }
+ }
+
+private:
+ const bool CollectNodes;
+};
+
+namespace {
+
+const THashSet<TStringBuf> CONFIGURE_CALLABLES = {
+ TCoConfigure::CallableName(),
+ TYtConfigure::CallableName(),
+};
+
+}
+
+class TYtDataSource : public TDataProviderBase {
+public:
+ TYtDataSource(TYtState::TPtr state)
+ : State_(state)
+ , ConfigurationTransformer_([this]() {
+ return MakeHolder<NCommon::TProviderConfigurationTransformer>(State_->Configuration, *State_->Types, TString{YtProviderName}, CONFIGURE_CALLABLES);
+ })
+ , IODiscoveryTransformer_([this]() { return CreateYtIODiscoveryTransformer(State_); })
+ , EpochTransformer_([this]() { return CreateYtEpochTransformer(State_); })
+ , IntentDeterminationTransformer_([this]() { return CreateYtIntentDeterminationTransformer(State_); })
+ , LoadMetaDataTransformer_([this]() { return CreateYtLoadTableMetadataTransformer(State_); })
+ , TypeAnnotationTransformer_([this]() { return CreateYtDataSourceTypeAnnotationTransformer(State_); })
+ , ConstraintTransformer_([this]() { return CreateYtDataSourceConstraintTransformer(State_); })
+ , ExecTransformer_([this]() { return CreateYtDataSourceExecTransformer(State_); })
+ , TrackableNodeProcessor_([this]() {
+ auto mode = GetReleaseTempDataMode(*State_->Configuration);
+ bool collectNodes = mode == EReleaseTempDataMode::Immediate;
+ return MakeHolder<TYtDataSourceTrackableNodeProcessor>(collectNodes);
+ })
+ {
+ }
+
+ TStringBuf GetName() const override {
+ return YtProviderName;
+ }
+
+ IGraphTransformer& GetConfigurationTransformer() override {
+ return *ConfigurationTransformer_;
+ }
+
+ IGraphTransformer& GetIODiscoveryTransformer() override {
+ return *IODiscoveryTransformer_;
+ }
+
+ IGraphTransformer& GetEpochsTransformer() override {
+ return *EpochTransformer_;
+ }
+
+ IGraphTransformer& GetIntentDeterminationTransformer() override {
+ return *IntentDeterminationTransformer_;
+ }
+
+ IGraphTransformer& GetTypeAnnotationTransformer(bool instantOnly) override {
+ Y_UNUSED(instantOnly);
+ return *TypeAnnotationTransformer_;
+ }
+
+ IGraphTransformer& GetConstraintTransformer(bool instantOnly, bool subGraph) override {
+ Y_UNUSED(instantOnly);
+ Y_UNUSED(subGraph);
+ return *ConstraintTransformer_;
+ }
+
+ IGraphTransformer& GetLoadTableMetadataTransformer() override {
+ return *LoadMetaDataTransformer_;
+ }
+
+ IGraphTransformer& GetCallableExecutionTransformer() override {
+ return *ExecTransformer_;
+ }
+
+ bool Initialize(TExprContext& ctx) override {
+ auto category = YtProviderName;
+ auto cred = State_->Types->Credentials->FindCredential(TString("default_").append(category));
+ if (cred) {
+ if (cred->Category != category) {
+ ctx.AddError(TIssue({}, TStringBuilder()
+ << "Mismatch default credential category, expected: " << category
+ << ", but found: " << cred->Category));
+ return false;
+ }
+ State_->Configuration->Auth = cred->Content;
+ }
+
+ return true;
+ }
+
+ const THashMap<TString, TString>* GetClusterTokens() override {
+ return &State_->Configuration->Tokens;
+ }
+
+ bool ValidateParameters(TExprNode& node, TExprContext& ctx, TMaybe<TString>& cluster) override {
+ if (node.IsCallable(TCoDataSource::CallableName())) {
+ if (!EnsureArgsCount(node, 2, ctx)) {
+ return false;
+ }
+
+ if (node.Child(0)->Content() == YtProviderName) {
+ if (!node.Child(1)->IsCallable("EvaluateAtom")) {
+ if (!EnsureAtom(*node.Child(1), ctx)) {
+ return false;
+ }
+
+ if (node.Child(1)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(1)->Pos()), "Empty cluster name"));
+ return false;
+ }
+
+ cluster = TString(node.Child(1)->Content());
+ }
+
+ return true;
+ }
+ }
+
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), "Invalid Yt DataSource parameters"));
+ return false;
+ }
+
+ bool CanParse(const TExprNode& node) override {
+ if (node.IsCallable(TCoRead::CallableName())) {
+ return TYtDSource::Match(node.Child(1));
+ }
+
+ return TypeAnnotationTransformer_->CanParse(node);
+ }
+
+ TExprNode::TPtr RewriteIO(const TExprNode::TPtr& node, TExprContext& ctx) override {
+ YQL_CLOG(INFO, ProviderYt) << "RewriteIO";
+ if (auto left = TMaybeNode<TCoLeft>(node)) {
+ return left.Input().Maybe<TYtRead>().World().Cast().Ptr();
+ }
+
+ auto read = TCoRight(node).Input().Cast<TYtRead>();
+ if (NYql::HasSetting(read.Arg(4).Ref(), EYtSettingType::Scheme)) {
+ YQL_ENSURE(read.Arg(2).Maybe<TExprList>().Item(0).Maybe<TYtPath>());
+
+ auto newRead = InjectUdfRemapperOrView(read, ctx, true);
+
+ return Build<TCoRight>(ctx, node->Pos())
+ .Input<TYtReadTableScheme>()
+ .World(read.World())
+ .DataSource(read.DataSource())
+ .Table(read.Arg(2).Cast<TExprList>().Item(0).Cast<TYtPath>().Table().Cast<TYtTable>())
+ .Type<TCoTypeOf>()
+ .Value(newRead)
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ YQL_ENSURE(read.Arg(2).Maybe<TExprList>().Item(0).Maybe<TYtPath>()); // At least one table
+ return InjectUdfRemapperOrView(read, ctx, false);
+ }
+
+ void PostRewriteIO() final {
+ State_->TablesData->CleanupCompiledSQL();
+ }
+
+ void Reset() final {
+ TDataProviderBase::Reset();
+ State_->TablesData = MakeIntrusive<TYtTablesData>();
+ State_->Configuration->ClearVersions();
+ State_->NodeHash.clear();
+ State_->Checkpoints.clear();
+ }
+
+ void EnterEvaluation(ui64 id) final {
+ State_->EnterEvaluation(id);
+ }
+
+ void LeaveEvaluation(ui64 id) final {
+ State_->LeaveEvaluation(id);
+ }
+
+ bool IsPersistent(const TExprNode& node) override {
+ return IsYtProviderInput(NNodes::TExprBase(&node));
+ }
+
+ bool IsRead(const TExprNode& node) override {
+ return TYtReadTable::Match(&node);
+ }
+
+ bool CanBuildResult(const TExprNode& node, TSyncMap& syncList) override {
+ TString usedCluster;
+ return IsYtCompleteIsolatedLambda(node, syncList, usedCluster, true, false);
+ }
+
+ bool GetExecWorld(const TExprNode::TPtr& node, TExprNode::TPtr& root) override {
+ auto read = TMaybeNode<TYtReadTable>(node);
+ if (!read) {
+ return false;
+ }
+
+ root = read.Cast().World().Ptr();
+ return true;
+ }
+
+ bool CanEvaluate(const TExprNode& node) override {
+ return TYtConfigure::Match(&node);
+ }
+
+ bool CanPullResult(const TExprNode& node, TSyncMap& syncList, bool& canRef) override {
+ Y_UNUSED(syncList);
+ canRef = true;
+ auto input = NNodes::TExprBase(&node);
+ return IsYtProviderInput(input) || input.Maybe<TCoRight>().Input().Maybe<TYtReadTableScheme>();
+ }
+
+ TExprNode::TPtr CleanupWorld(const TExprNode::TPtr& node, TExprContext& ctx) override {
+ return YtCleanupWorld(node, ctx, State_);
+ }
+
+ TExprNode::TPtr OptimizePull(const TExprNode::TPtr& source, const TFillSettings& fillSettings, TExprContext& ctx,
+ IOptimizationContext& optCtx) override
+ {
+ Y_UNUSED(optCtx);
+
+ auto maybeRight = TMaybeNode<TCoRight>(source);
+ if (!maybeRight || !fillSettings.RowsLimitPerWrite) {
+ return source;
+ }
+
+ auto maybeRead = maybeRight.Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ return source;
+ }
+ auto read = maybeRead.Cast();
+ if (read.Input().Size() > 1) {
+ return source;
+ }
+ auto section = read.Input().Item(0);
+ ui64 totalCount = 0;
+ for (auto path: section.Paths()) {
+ TYtTableBaseInfo::TPtr tableInfo = TYtTableBaseInfo::Parse(path.Table());
+ if (!tableInfo->Meta || !tableInfo->Stat || tableInfo->Meta->IsDynamic) {
+ totalCount = Max<ui64>();
+ break;
+ }
+ totalCount += tableInfo->Stat->RecordsCount;
+ }
+
+ if (totalCount <= *fillSettings.RowsLimitPerWrite) {
+ return source;
+ }
+
+ auto newSettings = NYql::AddSetting(
+ section.Settings().Ref(),
+ EYtSettingType::Take,
+ Build<TCoUint64>(ctx, read.Pos())
+ .Literal()
+ .Value(ToString(*fillSettings.RowsLimitPerWrite))
+ .Build()
+ .Done().Ptr(),
+ ctx);
+
+ return Build<TCoRight>(ctx, maybeRight.Cast().Pos())
+ .Input<TYtReadTable>()
+ .World(read.World())
+ .DataSource(read.DataSource())
+ .Input()
+ .Add()
+ .Paths(section.Paths())
+ .Settings(newSettings)
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ bool CanExecute(const TExprNode& node) override {
+ return ExecTransformer_->CanExec(node);
+ }
+
+ void GetRequiredChildren(const TExprNode& node, TExprNode::TListType& children) override {
+ if (CanExecute(node) && !TYtTable::Match(&node)) {
+ children.push_back(node.ChildPtr(0));
+ if (TYtReadTable::Match(&node)) {
+ children.push_back(node.ChildPtr(TYtReadTable::idx_Input));
+ }
+ }
+ }
+
+ bool GetDependencies(const TExprNode& node, TExprNode::TListType& children, bool compact) override {
+ Y_UNUSED(compact);
+ if (CanExecute(node)) {
+ children.push_back(node.ChildPtr(0));
+ if (node.Content() == TYtConfigure::CallableName()) {
+ return false;
+ }
+
+ if (TMaybeNode<TYtReadTable>(&node)) {
+ ScanPlanDependencies(node.ChildPtr(TYtReadTable::idx_Input), children);
+ }
+
+ if (TMaybeNode<TYtReadTableScheme>(&node)) {
+ ScanPlanDependencies(node.ChildPtr(TYtReadTableScheme::idx_Type), children);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void GetResultDependencies(const TExprNode::TPtr& node, TExprNode::TListType& children, bool compact) override {
+ Y_UNUSED(compact);
+ ScanPlanDependencies(node, children);
+ }
+
+ void WritePlanDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ if (auto maybeRead = TMaybeNode<TYtReadTable>(&node)) {
+ writer.OnKeyedItem("InputColumns");
+ auto read = maybeRead.Cast();
+ if (read.Input().Size() > 1) {
+ writer.OnBeginList();
+ for (auto section: read.Input()) {
+ writer.OnListItem();
+ NCommon::WriteColumns(writer, section.Paths().Item(0).Columns());
+ }
+ writer.OnEndList();
+ }
+ else {
+ NCommon::WriteColumns(writer, read.Input().Item(0).Paths().Item(0).Columns());
+ }
+ }
+ }
+
+ void WritePullDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ writer.OnKeyedItem("PullOperation");
+ writer.OnStringScalar(node.Child(0)->Content());
+ }
+
+ TString GetProviderPath(const TExprNode& node) override {
+ return TStringBuilder() << YtProviderName << '.' << node.Child(1)->Content();
+ }
+
+ void WriteDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ writer.OnKeyedItem("Cluster");
+ writer.OnStringScalar(node.Child(1)->Content());
+ }
+
+ void GetInputs(const TExprNode& node, TVector<TPinInfo>& inputs) override {
+ if (auto maybeRead = TMaybeNode<TYtReadTable>(&node)) {
+ auto read = maybeRead.Cast();
+ for (auto section: read.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto maybeTable = path.Table().Maybe<TYtTable>()) {
+ inputs.push_back(TPinInfo(read.DataSource().Raw(), nullptr, maybeTable.Cast().Raw(), MakeTableDisplayName(maybeTable.Cast(), false), false));
+ }
+ else {
+ auto tmpTable = GetOutTable(path.Table());
+ inputs.push_back(TPinInfo(read.DataSource().Raw(), nullptr, tmpTable.Raw(), MakeTableDisplayName(tmpTable, false), true));
+ }
+ }
+ }
+ }
+ else if (auto maybeReadScheme = TMaybeNode<TYtReadTableScheme>(&node)) {
+ auto readScheme = maybeReadScheme.Cast();
+ inputs.push_back(TPinInfo(readScheme.DataSource().Raw(), nullptr, readScheme.Table().Raw(), MakeTableDisplayName(readScheme.Table(), false), false));
+ }
+ }
+
+ void WritePinDetails(const TExprNode& node, NYson::TYsonWriter& writer) override {
+ writer.OnKeyedItem("Table");
+ if (auto table = TMaybeNode<TYtTable>(&node)) {
+ writer.OnStringScalar(table.Cast().Name().Value());
+ }
+ else {
+ writer.OnStringScalar("(tmp)");
+ }
+ }
+
+ ITrackableNodeProcessor& GetTrackableNodeProcessor() override {
+ return *TrackableNodeProcessor_;
+ }
+
+ bool CollectDiscoveredData(NYson::TYsonWriter& writer) override {
+ THashMap<std::pair<TString, TString>, THashSet<TString>> tables;
+ State_->TablesData->ForEach([&tables](const TString& cluster, const TString& table, ui32 /*epoch*/, const TYtTableDescription& tableDesc) {
+ if (!tableDesc.IsAnonymous) {
+ TStringBuf intent;
+ if (tableDesc.Intents & TYtTableIntent::Drop) {
+ intent = "drop";
+ } else if (tableDesc.Intents & (TYtTableIntent::Override | TYtTableIntent::Append)) {
+ intent = "modify";
+ } else if (tableDesc.Intents & TYtTableIntent::Flush) {
+ intent = "flush";
+ } else {
+ intent = "read";
+ }
+ tables[std::make_pair(cluster, table)].emplace(intent);
+ }
+ });
+ if (tables.empty()) {
+ return false;
+ }
+ writer.OnBeginList();
+ for (auto& item: tables) {
+ writer.OnListItem();
+
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnStringScalar(item.first.first);
+ writer.OnListItem();
+ writer.OnStringScalar(item.first.second);
+ writer.OnListItem();
+
+ writer.OnBeginList();
+ for (auto& intent: item.second) {
+ writer.OnListItem();
+ writer.OnStringScalar(intent);
+ }
+ writer.OnEndList();
+
+ writer.OnEndList();
+ }
+ writer.OnEndList();
+ return true;
+ }
+
+ IDqIntegration* GetDqIntegration() override {
+ return State_->DqIntegration_.Get();
+ }
+
+private:
+ TExprNode::TPtr InjectUdfRemapperOrView(TYtRead readNode, TExprContext& ctx, bool fromReadSchema) {
+ const bool weakConcat = NYql::HasSetting(readNode.Arg(4).Ref(), EYtSettingType::WeakConcat);
+ const bool ignoreNonExisting = NYql::HasSetting(readNode.Arg(4).Ref(), EYtSettingType::IgnoreNonExisting);
+ const bool warnNonExisting = NYql::HasSetting(readNode.Arg(4).Ref(), EYtSettingType::WarnNonExisting);
+ const bool inlineContent = NYql::HasSetting(readNode.Arg(4).Ref(), EYtSettingType::Inline);
+ const auto cleanReadSettings = NYql::RemoveSettings(readNode.Arg(4).Ref(),
+ EYtSettingType::WeakConcat | EYtSettingType::Inline | EYtSettingType::Scheme | EYtSettingType::IgnoreNonExisting | EYtSettingType::WarnNonExisting, ctx);
+
+ const bool useSysColumns = !fromReadSchema && State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS);
+ bool hasSysColumns = false;
+
+ auto cluster = TString{readNode.DataSource().Cluster().Value()};
+
+ bool hasNonExisting = false;
+ bool readAllFields = false;
+ TSet<TString> subsetFields;
+ TExprNode::TListType tableReads;
+ for (auto node: readNode.Arg(2).Cast<TExprList>()) {
+ TYtPath path = node.Cast<TYtPath>();
+ const bool hasMultipleUserRanges = path.Ranges().Maybe<TExprList>() && path.Ranges().Cast<TExprList>().Size() > 1;
+ TYtTable table = path.Table().Cast<TYtTable>();
+ if (!path.Columns().Maybe<TCoVoid>() && !readNode.Arg(3).Maybe<TCoVoid>()) {
+ ctx.AddError(TIssue(ctx.GetPosition(readNode.Pos()), TStringBuilder()
+ << "Column selection in both read and YPath for table " << table.Name().Value()));
+ return {};
+ }
+
+ auto epoch = TEpochInfo::Parse(table.Epoch().Ref());
+ const TYtTableDescription& tableDesc = State_->TablesData->GetTable(cluster, TString{table.Name().Value()}, epoch);
+
+ auto tableMeta = tableDesc.Meta;
+ auto tableStat = tableDesc.Stat;
+
+ TExprBase origColumnList = path.Columns();
+ if (!path.Columns().Maybe<TCoVoid>()) {
+ for (auto child: path.Columns().Ref().Children()) {
+ subsetFields.insert(TString{child->Content()});
+ }
+ }
+ else if (!readNode.Arg(3).Maybe<TCoVoid>()) {
+ for (auto child: readNode.Arg(3).Ref().Children()) {
+ subsetFields.insert(TString{child->Content()});
+ }
+ origColumnList = readNode.Arg(3);
+ YQL_ENSURE(origColumnList.Ref().IsList());
+ }
+ else {
+ readAllFields = true;
+ }
+
+ bool skipTable = false;
+ if (weakConcat
+ && !tableDesc.View // Don't skip view
+ && epoch.GetOrElse(0) == 0
+ && tableStat && tableStat->IsEmpty()
+ && tableMeta && !tableMeta->YqlCompatibleScheme) {
+ // skip empty tables without YQL compatible scheme
+ skipTable = true;
+ }
+
+ if (ignoreNonExisting
+ && epoch.GetOrElse(0) == 0
+ && tableMeta && !tableMeta->DoesExist) {
+ // skip non-existing tables
+ hasNonExisting = true;
+ skipTable = true;
+ if (warnNonExisting) {
+ auto issue = TIssue(ctx.GetPosition(table.Pos()), TStringBuilder()
+ << "Table " << cluster << '.' << table.Name().Value() << " doesn't exist");
+ SetIssueCode(EYqlIssueCode::TIssuesIds_EIssueCode_YT_WARN_TABLE_DOES_NOT_EXIST, issue);
+ if (!ctx.AddWarning(issue)) {
+ return {};
+ }
+ }
+ }
+
+ if (skipTable) {
+ auto userSchema = GetSetting(table.Settings().Ref(), EYtSettingType::UserSchema);
+ if (userSchema) {
+ tableReads.push_back(ctx.Builder(table.Pos())
+ .Callable("Right!")
+ .Add(0, BuildEmptyTablesRead(table.Pos(), *userSchema, ctx))
+ .Seal()
+ .Build());
+ }
+
+ continue;
+ }
+
+ auto updatedSettings = NYql::RemoveSetting(table.Settings().Ref(), EYtSettingType::XLock, ctx);
+
+ TString view;
+ if (auto viewNode = NYql::GetSetting(*updatedSettings, EYtSettingType::View)) {
+ view = TString{viewNode->Child(1)->Content()};
+ // truncate view
+ updatedSettings = NYql::RemoveSetting(*updatedSettings, EYtSettingType::View, ctx);
+ }
+
+ bool withQB = tableDesc.QB2RowSpec && view != "raw";
+ if (withQB) {
+ if (inlineContent) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder()
+ << "Table with QB2 premapper cannot be inlined: " << cluster << '.' << table.Name().Value()));
+ return {};
+ }
+ if (tableDesc.RowSpec && 0 != tableDesc.RowSpec->GetNativeYtTypeFlags()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder()
+ << "Table with type_v3 schema cannot be used with QB2 premapper: " << cluster << '.' << table.Name().Value()));
+ return {};
+ }
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::WithQB, {}, ctx);
+ }
+
+ table = Build<TYtTable>(ctx, table.Pos())
+ .InitFrom(table)
+ .Settings(updatedSettings)
+ .Done();
+
+ const bool needRewrite = weakConcat
+ || tableDesc.UdfApplyLambda
+ || !view.empty()
+ || withQB;
+
+ path = Build<TYtPath>(ctx, path.Pos())
+ .Table(table)
+ .Columns(needRewrite ? Build<TCoVoid>(ctx, path.Columns().Pos()).Done() : origColumnList)
+ .Ranges(path.Ranges())
+ .Stat(path.Stat())
+ .Done();
+
+ bool tableSysColumns = useSysColumns && !tableDesc.View && (view.empty() || view == "raw");
+ if (tableSysColumns) {
+ hasSysColumns = true;
+ }
+
+ TExprNode::TPtr effectiveSettings = cleanReadSettings;
+ if (tableSysColumns) {
+ effectiveSettings = NYql::AddSettingAsColumnList(*effectiveSettings, EYtSettingType::SysColumns, {TString{"path"}, TString{"record"}}, ctx);
+ }
+
+ if (hasMultipleUserRanges) {
+ effectiveSettings = NYql::AddSetting(*effectiveSettings, EYtSettingType::Unordered, nullptr, ctx);
+ effectiveSettings = NYql::AddSetting(*effectiveSettings, EYtSettingType::NonUnique, nullptr, ctx);
+ }
+
+ auto origReadNode = Build<TYtReadTable>(ctx, readNode.Pos())
+ .World(readNode.World())
+ .DataSource(readNode.DataSource())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings(effectiveSettings)
+ .Build()
+ .Build()
+ .Done();
+
+ TExprNode::TPtr rightOverRead = inlineContent
+ ? Build<TYtTableContent>(ctx, readNode.Pos())
+ .Input(origReadNode)
+ .Settings().Build()
+ .Done().Ptr()
+ : Build<TCoRight>(ctx, readNode.Pos())
+ .Input(origReadNode)
+ .Done().Ptr();
+
+ TExprNode::TPtr newReadNode = rightOverRead;
+
+ if (tableDesc.View) {
+ auto root = tableDesc.View->CompiledSql;
+ if (readNode.World().Ref().Type() != TExprNode::World) {
+ // Inject original Read! dependencies
+ auto status = OptimizeExpr(root, root, [&readNode](const TExprNode::TPtr& node, TExprContext& ctx) {
+ if (node->ChildrenSize() > 0 && node->Child(0)->Type() == TExprNode::World) {
+ return ctx.ChangeChild(*node, 0, readNode.World().Ptr());
+ }
+ return node;
+ }, ctx, TOptimizeExprSettings(nullptr));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+ }
+
+ newReadNode = root;
+ ctx.Step
+ .Repeat(TExprStep::ExprEval)
+ .Repeat(TExprStep::DiscoveryIO)
+ .Repeat(TExprStep::Epochs)
+ .Repeat(TExprStep::Intents)
+ .Repeat(TExprStep::LoadTablesMetadata)
+ .Repeat(TExprStep::RewriteIO);
+ }
+
+ if (tableDesc.UdfApplyLambda) {
+ if (tableSysColumns) {
+ newReadNode = ctx.Builder(readNode.Pos())
+ .Callable(ctx.IsConstraintEnabled<TSortedConstraintNode>() ? TCoOrderedMap::CallableName() : TCoMap::CallableName())
+ .Add(0, newReadNode)
+ .Lambda(1)
+ .Param("row")
+ .Callable("AddMember")
+ .Callable(0, "AddMember")
+ .Apply(0, tableDesc.UdfApplyLambda)
+ .With(0)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "row")
+ .List(1)
+ .Atom(0, YqlSysColumnPrefix, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Atom(1, YqlSysColumnRecord, TNodeFlags::Default)
+ .Callable(2, "Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnRecord, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Atom(1, YqlSysColumnPath, TNodeFlags::Default)
+ .Callable(2, "Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnPath, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ newReadNode = ctx.Builder(readNode.Pos())
+ .Callable(ctx.IsConstraintEnabled<TSortedConstraintNode>() ? TCoOrderedMap::CallableName() : TCoMap::CallableName())
+ .Add(0, newReadNode)
+ .Add(1, tableDesc.UdfApplyLambda)
+ .Seal()
+ .Build();
+ }
+ }
+
+ if (!view.empty()) {
+ auto viewMeta = tableDesc.Views.FindPtr(view);
+ YQL_ENSURE(viewMeta, "Unknown view: " << view);
+ auto root = viewMeta->CompiledSql;
+ YQL_ENSURE(root, "View is not initialized: " << view);
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(root, root, [newReadNode, rightOverRead, &readNode](const TExprNode::TPtr& node, TExprContext& ctx) {
+ if (auto world = TMaybeNode<TCoLeft>(node).Input().Maybe<TCoRead>().World()) {
+ return world.Cast().Ptr();
+ }
+
+ if (auto maybeRead = TMaybeNode<TCoRight>(node).Input().Maybe<TCoRead>()) {
+ TCoRead read = maybeRead.Cast();
+ TYtInputKeys keys;
+ if (!keys.Parse(read.Arg(2).Ref(), ctx)) {
+ return TExprNode::TPtr();
+ }
+ YQL_ENSURE(keys.GetKeys().size() == 1, "Expected single table name");
+ auto tableName = keys.GetKeys().front().GetPath();
+ if (tableName == TStringBuf("self")) {
+ return newReadNode;
+ }
+
+ if (tableName == TStringBuf("self_raw")) {
+ return rightOverRead;
+ }
+
+ YQL_ENSURE(false, "Unknown table name (should be self or self_raw): " << tableName);
+ }
+
+ // Inject original Read! dependencies
+ if (node->ChildrenSize() > 0 && node->Child(0)->Type() == TExprNode::World) {
+ return ctx.ChangeChild(*node, 0, readNode.World().Ptr());
+ }
+
+ return node;
+ }, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+ newReadNode = root;
+ ctx.Step
+ .Repeat(TExprStep::ExpandApplyForLambdas)
+ .Repeat(TExprStep::ExprEval)
+ .Repeat(TExprStep::RewriteIO);
+ }
+
+ if (needRewrite && !weakConcat && !origColumnList.Maybe<TCoVoid>()) {
+ TSet<TString> names;
+ for (auto child: origColumnList.Ref().Children()) {
+ names.emplace(child->Content());
+ }
+ if (tableSysColumns) {
+ names.insert(TString{YqlSysColumnPath});
+ names.insert(TString{YqlSysColumnRecord});
+ }
+ newReadNode = FilterByFields(readNode.Pos(), newReadNode, names, ctx, false);
+ }
+
+ tableReads.push_back(newReadNode);
+ }
+
+ if (tableReads.empty()) {
+ if (hasNonExisting) {
+ ctx.AddError(TIssue(ctx.GetPosition(readNode.Pos()), "The list of tables is empty"));
+ return {};
+ }
+
+ return Build<TCoList>(ctx, readNode.Pos())
+ .ListType<TCoListType>()
+ .ItemType<TCoStructType>()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ TStringBuf extendName = ctx.IsConstraintEnabled<TSortedConstraintNode>()
+ ? (weakConcat ? TCoUnionMerge::CallableName() : TCoMerge::CallableName())
+ : (weakConcat ? TCoUnionAll::CallableName() : TCoExtend::CallableName());
+
+ auto ret = ctx.NewCallable(readNode.Pos(), extendName, std::move(tableReads));
+
+ if (weakConcat && !readAllFields) {
+ if (hasSysColumns) {
+ subsetFields.insert(TString{YqlSysColumnPath});
+ subsetFields.insert(TString{YqlSysColumnRecord});
+ }
+ ret = FilterByFields(readNode.Pos(), ret, subsetFields, ctx, false);
+ }
+
+ return ret;
+ }
+
+private:
+ TYtState::TPtr State_;
+ TLazyInitHolder<IGraphTransformer> ConfigurationTransformer_;
+ TLazyInitHolder<IGraphTransformer> IODiscoveryTransformer_;
+ TLazyInitHolder<IGraphTransformer> EpochTransformer_;
+ TLazyInitHolder<IGraphTransformer> IntentDeterminationTransformer_;
+ TLazyInitHolder<IGraphTransformer> LoadMetaDataTransformer_;
+ TLazyInitHolder<TVisitorTransformerBase> TypeAnnotationTransformer_;
+ TLazyInitHolder<IGraphTransformer> ConstraintTransformer_;
+ TLazyInitHolder<TExecTransformerBase> ExecTransformer_;
+ TLazyInitHolder<TYtDataSourceTrackableNodeProcessor> TrackableNodeProcessor_;
+};
+
+TIntrusivePtr<IDataProvider> CreateYtDataSource(TYtState::TPtr state) {
+ return new TYtDataSource(state);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
new file mode 100644
index 0000000000..1d1398ca23
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_constraints.cpp
@@ -0,0 +1,198 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_table.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/core/yql_expr_constraint.h>
+#include <ydb/library/yql/ast/yql_constraint.h>
+
+#include <util/generic/variant.h>
+
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtDataSourceConstraintTransformer : public TVisitorTransformerBase {
+public:
+ TYtDataSourceConstraintTransformer(TYtState::TPtr state)
+ : TVisitorTransformerBase(true)
+ , State_(state)
+ {
+ AddHandler({TYtTable::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleTable));
+ AddHandler({TYtPath::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandlePath));
+ AddHandler({TYtSection::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleSection));
+ AddHandler({TYtReadTable::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleReadTable));
+ AddHandler({TYtTableContent::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleTableContent));
+
+ AddHandler({TYtIsKeySwitch::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYqlRowSpec::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TEpoch::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtMeta::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtStat::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtRow::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtRowRange::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtKeyExact::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtKeyRange::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtReadTableScheme::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtLength::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtConfigure::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtTablePath::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtTableRecord::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtTableIndex::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtRowNumber::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ AddHandler({TYtTableName::CallableName()}, Hndl(&TYtDataSourceConstraintTransformer::HandleDefault));
+ }
+
+ TStatus HandleTable(TExprBase input, TExprContext& ctx) {
+ const auto table = input.Cast<TYtTable>();
+ const auto epoch = TEpochInfo::Parse(table.Epoch().Ref());
+ const auto tableName = TString{TYtTableInfo::GetTableLabel(table)};
+ TYtTableDescription& tableDesc = State_->TablesData->GetModifTable(TString{table.Cluster().Value()}, tableName, epoch);
+ if (epoch) {
+ if (!tableDesc.ConstraintsReady) {
+ if (State_->Types->EvaluationInProgress) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Table " << tableName.Quote() << " is used before commit"));
+ return TStatus::Error;
+ }
+ return TStatus(TStatus::Repeat, true);
+ }
+ input.Ptr()->SetConstraints(tableDesc.Constraints);
+ } else if (!table.RowSpec().Maybe<TCoVoid>()) {
+ TYqlRowSpecInfo rowSpec(table.RowSpec(), false);
+ auto set = rowSpec.GetSomeConstraints(State_->Configuration->ApplyStoredConstraints.Get().GetOrElse(DEFAULT_APPLY_STORED_CONSTRAINTS), ctx);
+
+ if (!set.GetConstraint<TSortedConstraintNode>()) {
+ if (const auto sorted = rowSpec.MakeSortConstraint(ctx))
+ set.AddConstraint(sorted);
+ }
+
+ if (!set.GetConstraint<TDistinctConstraintNode>()) {
+ if (const auto distinct = rowSpec.MakeDistinctConstraint(ctx)) {
+ set.AddConstraint(ctx.MakeConstraint<TUniqueConstraintNode>(TUniqueConstraintNode::TFullSetType(distinct->GetAllSets())));
+ set.AddConstraint(distinct);
+ }
+ }
+ input.Ptr()->SetConstraints(set);
+ if (!tableDesc.ConstraintsReady) {
+ tableDesc.Constraints = set;
+ tableDesc.SetConstraintsReady();
+ }
+ }
+ if (!table.Stat().Maybe<TCoVoid>()) {
+ if (TYtTableStatInfo(table.Stat()).IsEmpty() && !TYtTableMetaInfo(table.Meta()).IsDynamic) {
+ input.Ptr()->AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandlePath(TExprBase input, TExprContext& ctx) {
+ auto path = input.Cast<TYtPath>();
+ const auto outItemType = path.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ const auto filter = [outItemType](const TConstraintNode::TPathType& path) { return !path.empty() && outItemType->FindItem(path.front()); };
+
+ if (const auto sort = path.Table().Ref().GetConstraint<TSortedConstraintNode>()) {
+ if (const auto filtered = sort->FilterFields(ctx, filter)) {
+ path.Ptr()->AddConstraint(filtered);
+ }
+ }
+
+ if (const auto uniq = path.Table().Ref().GetConstraint<TUniqueConstraintNode>()) {
+ if (const auto filtered = uniq->FilterFields(ctx, filter)) {
+ path.Ptr()->AddConstraint(filtered);
+ }
+ }
+
+ if (const auto dist = path.Table().Ref().GetConstraint<TDistinctConstraintNode>()) {
+ if (const auto filtered = dist->FilterFields(ctx, filter)) {
+ path.Ptr()->AddConstraint(filtered);
+ }
+ }
+
+ if (auto empty = path.Table().Ref().GetConstraint<TEmptyConstraintNode>()) {
+ path.Ptr()->AddConstraint(empty);
+ } else if (path.Ranges().Maybe<TExprList>()) {
+ auto rangeInfo = TYtRangesInfo(path.Ranges());
+ if (rangeInfo.IsEmpty()) {
+ path.Ptr()->AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleSection(TExprBase input, TExprContext& ctx) {
+ auto section = input.Cast<TYtSection>();
+ if (section.Paths().Size() == 1) {
+ auto path = section.Paths().Item(0);
+ if (!NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered)) {
+ if (auto sorted = path.Ref().GetConstraint<TSortedConstraintNode>()) {
+ input.Ptr()->AddConstraint(sorted);
+ }
+ }
+ if (!NYql::HasSetting(section.Settings().Ref(), EYtSettingType::NonUnique)) {
+ if (const auto unique = path.Ref().GetConstraint<TUniqueConstraintNode>()) {
+ input.Ptr()->AddConstraint(unique);
+ }
+ if (const auto distinct = path.Ref().GetConstraint<TDistinctConstraintNode>()) {
+ input.Ptr()->AddConstraint(distinct);
+ }
+ }
+ }
+
+ TVector<const TConstraintSet*> allConstraints;
+ for (const auto& path : section.Paths()) {
+ allConstraints.push_back(&path.Ref().GetConstraintSet());
+ }
+
+ if (auto empty = TEmptyConstraintNode::MakeCommon(allConstraints, ctx)) {
+ input.Ptr()->AddConstraint(empty);
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleReadTable(TExprBase input, TExprContext& ctx) {
+ auto read = input.Cast<TYtReadTable>();
+ if (read.Input().Size() == 1) {
+ auto section = read.Input().Item(0);
+ input.Ptr()->CopyConstraints(section.Ref());
+ } else {
+ TMultiConstraintNode::TMapType multiItems;
+ for (ui32 index = 0; index < read.Input().Size(); ++index) {
+ auto section = read.Input().Item(index);
+ if (!section.Ref().GetConstraint<TEmptyConstraintNode>()) {
+ multiItems.push_back(std::make_pair(index, section.Ref().GetConstraintSet()));
+ }
+ }
+ input.Ptr()->AddConstraint(ctx.MakeConstraint<TMultiConstraintNode>(std::move(multiItems)));
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTableContent(TExprBase input, TExprContext& /*ctx*/) {
+ TYtTableContent tableContent = input.Cast<TYtTableContent>();
+ input.Ptr()->CopyConstraints(tableContent.Input().Ref());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleDefault(TExprBase input, TExprContext& /*ctx*/) {
+ return UpdateAllChildLambdasConstraints(input.Ref());
+ }
+
+private:
+ const TYtState::TPtr State_;
+};
+
+}
+
+THolder<IGraphTransformer> CreateYtDataSourceConstraintTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSourceConstraintTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
new file mode 100644
index 0000000000..032c396598
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_exec.cpp
@@ -0,0 +1,242 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_optimize.h"
+#include "yql_yt_op_hash.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_exec.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/yql_execution.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/string/hex.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtDataSourceExecTransformer : public TExecTransformerBase {
+public:
+ TYtDataSourceExecTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+
+ AddHandler({TStringBuf("Result"), TStringBuf("Pull")}, RequireNone(), Hndl(&TYtDataSourceExecTransformer::HandleResOrPull));
+ AddHandler(
+ {
+ TYtReadTableScheme::CallableName(),
+ TYtPath::CallableName(),
+ TYtSection::CallableName(),
+ },
+ RequireFirst(),
+ Pass());
+ AddHandler({TYtReadTable::CallableName()}, RequireSequenceOf({TYtReadTable::idx_World, TYtReadTable::idx_Input}), Pass());
+ AddHandler({TYtTable::CallableName()}, RequireNone(), Pass());
+ AddHandler({TYtConfigure::CallableName()}, RequireFirst(), Hndl(&TYtDataSourceExecTransformer::HandleConfigure));
+ }
+
+protected:
+ TString WriteTableScheme(TYtReadTableScheme readScheme, NYson::EYsonFormat ysonFormat, bool withType) {
+ TStringStream out;
+ NYson::TYsonWriter writer(&out, ysonFormat, ::NYson::EYsonType::Node, true);
+ writer.OnBeginMap();
+
+ if (withType) {
+ writer.OnKeyedItem("Type");
+ auto valueType = readScheme.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1];
+ NCommon::WriteTypeToYson(writer, valueType);
+ }
+
+ auto cluster = TString{readScheme.DataSource().Cluster().Value()};
+ auto tableName = TString{readScheme.Table().Name().Value()};
+ auto view = NYql::GetSetting(readScheme.Table().Settings().Ref(), EYtSettingType::View);
+ TString viewName = view ? TString{view->Child(1)->Content()} : TString();
+
+ const TYtTableDescription& tableDesc = State_->TablesData->GetTable(cluster, tableName,
+ TEpochInfo::Parse(readScheme.Table().Epoch().Ref()));
+
+ writer.OnKeyedItem("Data");
+ tableDesc.ToYson(writer, cluster, tableName, viewName);
+
+ writer.OnEndMap();
+ return out.Str();
+ }
+
+protected:
+ TStatusCallbackPair HandleResOrPull(const TExprNode::TPtr& input, TExprContext& ctx) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Executing " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+ TResOrPullBase resOrPull(input);
+
+ IDataProvider::TFillSettings fillSettings = NCommon::GetFillSettings(resOrPull.Ref());
+ YQL_ENSURE(fillSettings.Format == IDataProvider::EResultFormat::Yson);
+
+ auto data = resOrPull.Input();
+ if (auto maybePull = resOrPull.Maybe<TPull>()) {
+ TExprNode::TListType needCalc = GetNodesToCalculate(data.Ptr());
+ if (!needCalc.empty()) {
+ return CalculateNodes(State_, data.Ptr(), TString{GetClusterName(data)}, needCalc, ctx);
+ }
+
+ if (auto maybeScheme = data.Maybe<TCoRight>().Input().Maybe<TYtReadTableScheme>()) {
+ TString result = WriteTableScheme(maybeScheme.Cast(), NCommon::GetYsonFormat(fillSettings),
+ NCommon::HasResOrPullOption(resOrPull.Ref(), "type"));
+ input->SetState(TExprNode::EState::ExecutionComplete);
+ input->SetResult(ctx.NewAtom(input->Pos(), result));
+ return SyncOk();
+ }
+
+ if (auto read = data.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ auto newRead = ctx.ExactShallowCopy(read.Cast().Ref());
+ newRead->ChildRef(0) = ctx.NewWorld(read.Cast().World().Pos());
+ newRead->Child(0)->SetTypeAnn(ctx.MakeType<TWorldExprType>());
+ newRead->Child(0)->SetState(TExprNode::EState::ExecutionComplete);
+ newRead->CopyConstraints(read.Cast().Ref());
+
+ auto newRight = Build<TCoRight>(ctx, data.Pos())
+ .Input(newRead)
+ .Done();
+ newRight.Ptr()->SetTypeAnn(data.Ref().GetTypeAnn());
+ newRight.Ptr()->CopyConstraints(data.Ref());
+
+ data = newRight;
+ }
+ }
+
+ TString usedCluster;
+ TSyncMap syncList;
+ if (!IsYtIsolatedLambda(data.Ref(), syncList, usedCluster, true, false)) {
+ ctx.AddError(TIssue(ctx.GetPosition(data.Pos()), TStringBuilder() << "Failed to execute node due to bad graph: " << input->Content()));
+ return SyncError();
+ }
+ if (usedCluster.empty()) {
+ usedCluster = State_->Configuration->DefaultCluster.Get().GetOrElse(State_->Gateway->GetDefaultClusterName());
+ }
+
+ TExprNode::TPtr optimizedInput = data.Ptr();
+ if (const auto status = SubstTables(optimizedInput, State_, false, ctx); status.Level == TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ const auto settings = State_->Configuration->GetSettingsForNode(*input);
+ TUserDataTable crutches = State_->Types->UserDataStorageCrutches;
+ if (const auto& defaultGeobase = settings->GeobaseDownloadUrl.Get(usedCluster)) {
+ auto& userDataBlock = (crutches[TUserDataKey::File(TStringBuf("/home/geodata6.bin"))] = TUserDataBlock{EUserDataType::URL, {}, *defaultGeobase, {}, {}});
+ userDataBlock.Usage.Set(EUserDataBlockUsage::Path);
+ }
+
+ bool hasNonDeterministicFunctions = false;
+ if (const auto status = PeepHoleOptimizeBeforeExec<true>(optimizedInput, optimizedInput, State_, hasNonDeterministicFunctions, ctx); status.Level != IGraphTransformer::TStatus::Ok) {
+ return SyncStatus(status);
+ }
+
+ TUserDataTable files;
+ auto filesRes = NCommon::FreezeUsedFiles(*optimizedInput, files, *State_->Types, ctx, MakeUserFilesDownloadFilter(*State_->Gateway, usedCluster), crutches);
+ if (filesRes.first.Level != TStatus::Ok) {
+ return filesRes;
+ }
+
+ THashMap<TString, TString> secureParams;
+ NCommon::FillSecureParams(optimizedInput, *State_->Types, secureParams);
+
+ auto optimizeChildren = input->ChildrenList();
+ optimizeChildren[0] = optimizedInput;
+ resOrPull = TResOrPullBase(ctx.ExactChangeChildren(resOrPull.Ref(), std::move(optimizeChildren)));
+
+ TString operationHash;
+ if (resOrPull.Maybe<TResult>()) {
+ const auto queryCacheMode = settings->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable);
+ if (queryCacheMode != EQueryCacheMode::Disable) {
+ if (!hasNonDeterministicFunctions && settings->QueryCacheUseForCalc.Get().GetOrElse(true)) {
+ operationHash = TYtNodeHashCalculator(State_, usedCluster, settings).GetHash(*optimizedInput);
+ if (!operationHash.empty()) {
+ // Update hash with columns hint. See YQL-10405
+ TVector<TString> columns = NCommon::GetResOrPullColumnHints(resOrPull.Ref());
+ THashBuilder builder;
+ builder << TYtNodeHashCalculator::MakeSalt(settings, usedCluster) << operationHash << columns.size();
+ for (auto& col: columns) {
+ builder << col;
+ }
+ operationHash = builder.Finish();
+ }
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Operation hash: " << HexEncode(operationHash).Quote()
+ << ", cache mode: " << queryCacheMode;
+ }
+ }
+
+ auto publicId = resOrPull.PublicId().Value()
+ ? MakeMaybe(FromString<ui32>(resOrPull.PublicId().Value()))
+ : Nothing();
+
+ auto future = State_->Gateway->ResOrPull(resOrPull.Ptr(), ctx,
+ IYtGateway::TResOrPullOptions(State_->SessionId)
+ .FillSettings(fillSettings)
+ .UserDataBlocks(files)
+ .UdfModules(State_->Types->UdfModules)
+ .UdfResolver(State_->Types->UdfResolver)
+ .UdfValidateMode(State_->Types->ValidateMode)
+ .PublicId(publicId)
+ .Config(State_->Configuration->GetSettingsForNode(resOrPull.Origin().Ref()))
+ .UsedCluster(usedCluster)
+ .OptLLVM(State_->Types->OptLLVM.GetOrElse(TString()))
+ .OperationHash(operationHash)
+ .SecureParams(secureParams)
+ );
+
+ return WrapFuture(future, [](const IYtGateway::TResOrPullResult& res, const TExprNode::TPtr& input, TExprContext& ctx) {
+ auto ret = ctx.NewAtom(input->Pos(), res.Data);
+ return ret;
+ });
+ }
+
+ TStatusCallbackPair HandleConfigure(const TExprNode::TPtr& input, TExprContext& ctx) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Executing " << input->Content() << " (UniqueId=" << input->UniqueId() << ")";
+ auto configure = TYtConfigure(input);
+ auto clusterName = TString{configure.DataSource().Cluster().Value()};
+ State_->Configuration->FreezeZeroVersion();
+ if (configure.Arg(2).Cast<TCoAtom>().Value() == TStringBuf("Attr")) {
+ auto name = TString{configure.Arg(3).Cast<TCoAtom>().Value()};
+ TMaybe<TString> value;
+ if (configure.Args().Count() == 5) {
+ value = TString{configure.Arg(4).Cast<TCoAtom>().Value()};
+ }
+ if (State_->Configuration->Dispatch(clusterName, name, value, NCommon::TSettingDispatcher::EStage::RUNTIME)) {
+ State_->Configuration->PromoteVersion(*input);
+ YQL_CLOG(DEBUG, ProviderYt) << "Setting pragma "
+ << (NCommon::ALL_CLUSTERS == clusterName ? YtProviderName : clusterName) << '.'
+ << name << '=' << (value ? value->Quote() : "(default)")
+ << " in ver." << State_->Configuration->GetLastVersion();
+ }
+ }
+
+ input->SetState(TExprNode::EState::ExecutionComplete);
+ input->SetResult(ctx.NewWorld(input->Pos()));
+ return SyncOk();
+ }
+
+ void Rewind() final {
+ State_->Configuration->ClearVersions();
+ TExecTransformerBase::Rewind();
+ }
+
+private:
+ const TYtState::TPtr State_;
+};
+
+}
+
+THolder<TExecTransformerBase> CreateYtDataSourceExecTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSourceExecTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
new file mode 100644
index 0000000000..2ffcea422d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_datasource_type_ann.cpp
@@ -0,0 +1,1004 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/provider/yql_yt_table.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/string/builder.h>
+#include <util/generic/xrange.h>
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtDataSourceTypeAnnotationTransformer : public TVisitorTransformerBase {
+public:
+ TYtDataSourceTypeAnnotationTransformer(TYtState::TPtr state)
+ : TVisitorTransformerBase(true)
+ , State_(state)
+ {
+ AddHandler({TEpoch::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleAux<TEpochInfo>));
+ AddHandler({TYtMeta::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleAux<TYtTableMetaInfo>));
+ AddHandler({TYtStat::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleAux<TYtTableStatInfo>));
+ AddHandler({TYqlRowSpec::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleRowSpec));
+ AddHandler({TYtTable::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTable));
+ AddHandler({TYtRow::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleRow));
+ AddHandler({TYtRowRange::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleRowRange));
+ AddHandler({TYtKeyExact::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleKeyExact));
+ AddHandler({TYtKeyRange::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleKeyRange));
+ AddHandler({TYtPath::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandlePath));
+ AddHandler({TYtSection::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleSection));
+ AddHandler({TYtReadTable::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleReadTable));
+ AddHandler({TYtReadTableScheme::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleReadTableScheme));
+ AddHandler({TYtTableContent::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTableContent));
+ AddHandler({TYtLength::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleLength));
+ AddHandler({TYtConfigure::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleConfigure));
+ AddHandler({TYtTablePath::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTablePath));
+ AddHandler({TYtTableRecord::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTableRecord));
+ AddHandler({TYtRowNumber::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTableRecord));
+ AddHandler({TYtTableIndex::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTableIndex));
+ AddHandler({TYtIsKeySwitch::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleIsKeySwitch));
+ AddHandler({TYtTableName::CallableName()}, Hndl(&TYtDataSourceTypeAnnotationTransformer::HandleTableName));
+ }
+
+ TStatus HandleUnit(TExprBase input, TExprContext& ctx) {
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TUnitExprType>());
+ return TStatus::Ok;
+ }
+
+ template <class TValidator>
+ TStatus HandleAux(TExprBase input, TExprContext& ctx) {
+ if (!TValidator::Validate(input.Ref(), ctx)) {
+ return TStatus::Error;
+ }
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TUnitExprType>());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleRowSpec(TExprBase input, TExprContext& ctx) {
+ const TStructExprType* type = nullptr;
+ TMaybe<TColumnOrder> columnOrder;
+ if (!TYqlRowSpecInfo::Validate(input.Ref(), ctx, type, columnOrder)) {
+ return TStatus::Error;
+ }
+ input.Ptr()->SetTypeAnn(type);
+ if (columnOrder) {
+ return State_->Types->SetColumnOrder(input.Ref(), *columnOrder, ctx);
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ EYtSettingTypes accepted = EYtSettingType::InferScheme | EYtSettingType::ForceInferScheme
+ | EYtSettingType::DoNotFailOnInvalidSchema | EYtSettingType::Anonymous
+ | EYtSettingType::WithQB | EYtSettingType::View | EYtSettingType::UserSchema
+ | EYtSettingType::UserColumns | EYtSettingType::IgnoreTypeV3;
+
+ if (!TYtTableInfo::Validate(*input, accepted, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!TYtTableInfo::HasSubstAnonymousLabel(TExprBase(input))) {
+ auto status = UpdateTableMeta(input, output, State_->TablesData, false, State_->Types->UseTableMetaFromGraph, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ auto table = TYtTable(input);
+
+ const TTypeAnnotationNode* itemType = nullptr;
+ TMaybe<TColumnOrder> columnOrder;
+ if (table.RowSpec().Maybe<TCoVoid>()) {
+ columnOrder.ConstructInPlace();
+ TVector<const TItemExprType*> items;
+ for (auto& name : YAMR_FIELDS) {
+ items.push_back(ctx.MakeType<TItemExprType>(name, ctx.MakeType<TDataExprType>(EDataSlot::String)));
+ columnOrder->push_back(TString(name));
+ }
+ itemType = ctx.MakeType<TStructExprType>(items);
+ } else {
+ itemType = table.RowSpec().Ref().GetTypeAnn();
+ columnOrder = State_->Types->LookupColumnOrder(table.RowSpec().Ref());
+ }
+
+ input->SetTypeAnn(ctx.MakeType<TListExprType>(itemType));
+ if (columnOrder) {
+ return State_->Types->SetColumnOrder(*input, *columnOrder, ctx);
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleRow(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!input.Ref().Child(TYtRow::idx_Index)->IsCallable(TCoUint64::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtRow::idx_Index)->Pos()), TStringBuilder() << "Expected " << TCoUint64::CallableName()));
+ return TStatus::Error;
+ }
+
+ return HandleUnit(input, ctx);
+ }
+
+ TStatus HandleRowRange(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!input.Ref().Child(TYtRowRange::idx_Lower)->IsCallable(TCoVoid::CallableName())
+ && !input.Ref().Child(TYtRowRange::idx_Lower)->IsCallable(TCoUint64::CallableName()))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtRowRange::idx_Lower)->Pos()), TStringBuilder()
+ << "Expected " << TCoUint64::CallableName() << " or " << TCoVoid::CallableName()));
+ return TStatus::Error;
+ }
+
+ if (!input.Ref().Child(TYtRowRange::idx_Upper)->IsCallable(TCoVoid::CallableName())
+ && !input.Ref().Child(TYtRowRange::idx_Upper)->IsCallable(TCoUint64::CallableName()))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtRowRange::idx_Upper)->Pos()), TStringBuilder()
+ << "Expected " << TCoUint64::CallableName() << " or " << TCoVoid::CallableName()));
+ return TStatus::Error;
+ }
+
+ if (input.Ref().Child(TYtRowRange::idx_Lower)->IsCallable(TCoVoid::CallableName())
+ && input.Ref().Child(TYtRowRange::idx_Upper)->IsCallable(TCoVoid::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Either Lower or Upper part of " << TYtRowRange::CallableName() << " should be specified"));
+ return TStatus::Error;
+ }
+
+ return HandleUnit(input, ctx);
+ }
+
+ static bool ValidateKeyLiteral(TExprBase node, TExprContext& ctx) {
+ if (node.Maybe<TCoNull>()) {
+ return true;
+ }
+
+ bool isOptional;
+ const TDataExprType* dataType;
+ if (!EnsureDataOrOptionalOfData(node.Ref(), isOptional, dataType, ctx)) {
+ return false;
+ }
+
+ if (isOptional) {
+ if (node.Maybe<TCoNothing>()) {
+ return true;
+ }
+ auto maybeJust = node.Maybe<TCoJust>();
+ if (!maybeJust) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected Optional literal - Nothing or Just"));
+ return false;
+ }
+ node = maybeJust.Cast().Input();
+ }
+
+ if (!node.Maybe<TCoDataCtor>()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected one of Data value"));
+ return false;
+ }
+ return true;
+ }
+
+ TStatus HandleKeyExact(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureTupleMinSize(*input.Ref().Child(TYtKeyExact::idx_Key), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ TExprNodeList keyValues = input.Ref().Child(TYtKeyExact::idx_Key)->ChildrenList();
+ if (AnyOf(keyValues, [&ctx](const auto& child) { return !ValidateKeyLiteral(TExprBase(child), ctx); })) {
+ return TStatus::Error;
+ }
+
+ return HandleUnit(input, ctx);
+ }
+
+ TStatus HandleKeyRange(TExprBase input, TExprContext& ctx) {
+ if (!EnsureMinArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureMaxArgsCount(input.Ref(), 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureTuple(*input.Ref().Child(TYtKeyRange::idx_Lower), ctx)) {
+ return TStatus::Error;
+ }
+
+ const TExprNodeList& lowerValues = input.Ref().Child(TYtKeyRange::idx_Lower)->ChildrenList();
+ if (AnyOf(lowerValues, [&ctx](const auto& child) { return !ValidateKeyLiteral(TExprBase(child), ctx); })) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureTuple(*input.Ref().Child(TYtKeyRange::idx_Upper), ctx)) {
+ return TStatus::Error;
+ }
+
+ const TExprNodeList& upperValues = input.Ref().Child(TYtKeyRange::idx_Upper)->ChildrenList();
+ if (AnyOf(upperValues, [&ctx](const auto& child) { return !ValidateKeyLiteral(TExprBase(child), ctx); })) {
+ return TStatus::Error;
+ }
+
+ if (lowerValues.empty() && upperValues.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Either Lower or Upper part of " << TYtKeyRange::CallableName() << " should be specified"));
+ return TStatus::Error;
+ }
+
+ for (size_t i : xrange(std::min(lowerValues.size(), upperValues.size()))) {
+ const TTypeAnnotationNode* lowerType = lowerValues[i]->GetTypeAnn();
+ const TTypeAnnotationNode* upperType = upperValues[i]->GetTypeAnn();
+ if (lowerType->GetKind() == ETypeAnnotationKind::Null || upperType->GetKind() == ETypeAnnotationKind::Null) {
+ continue;
+ }
+
+ if (!IsSameAnnotation(*lowerType, *upperType)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Lower/Upper type mismatch at index " << i << ": lower: " << *lowerType << ", upper: " << *upperType));
+ return TStatus::Error;
+ }
+ }
+
+ if (input.Ref().ChildrenSize() == 3) {
+ if (!EnsureTupleMinSize(*input.Ref().Child(TYtKeyRange::idx_Flags), 1, ctx)) {
+ return TStatus::Error;
+ }
+ for (auto& atom: input.Ref().Child(TYtKeyRange::idx_Flags)->Children()) {
+ if (!EnsureAtom(*atom, ctx)) {
+ return TStatus::Error;
+ }
+ if (atom->Content() == TStringBuf("excludeLower")) {
+ if (lowerValues.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(atom->Pos()), TStringBuilder()
+ << "Expected non-empty Lower part for 'excludeLower' setting"));
+ return TStatus::Error;
+ }
+ } else if (atom->Content() == TStringBuf("includeUpper")) {
+ if (upperValues.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(atom->Pos()), TStringBuilder()
+ << "Expected non-empty Upper part for 'includeUpper' setting"));
+ return TStatus::Error;
+ }
+ } else if (atom->Content() != TStringBuf("useKeyBound")) {
+ ctx.AddError(TIssue(ctx.GetPosition(atom->Pos()), TStringBuilder()
+ << "Expected 'excludeLower', 'includeUpper' or 'useKeyBound', but got " << atom->Content()));
+ return TStatus::Error;
+ }
+ }
+ }
+
+ return HandleUnit(input, ctx);
+ }
+
+ TStatus HandlePath(TExprBase input, TExprContext& ctx) {
+ if (!TYtPathInfo::Validate(input.Ref(), ctx)) {
+ return TStatus::Error;
+ }
+
+ TYtPathInfo pathInfo(input);
+ if (pathInfo.Table->Meta && !pathInfo.Table->Meta->DoesExist) {
+ if (NYql::HasSetting(pathInfo.Table->Settings.Ref(), EYtSettingType::Anonymous)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Anonymous table " << pathInfo.Table->Name.Quote() << " must be materialized. Use COMMIT before reading from it."));
+ }
+ else {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Table " << pathInfo.Table->Name.Quote() << " does not exist").SetCode(TIssuesIds::YT_TABLE_NOT_FOUND, TSeverityIds::S_ERROR));
+ }
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* itemType = nullptr;
+ TExprNode::TPtr newFields;
+ auto status = pathInfo.GetType(itemType, newFields, ctx, input.Pos());
+ if (newFields) {
+ input.Ptr()->ChildRef(TYtPath::idx_Columns) = newFields;
+ }
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TListExprType>(itemType));
+ if (auto columnOrder = State_->Types->LookupColumnOrder(input.Ref().Head())) {
+ if (pathInfo.Columns) {
+ auto& renames = pathInfo.Columns->GetRenames();
+ if (renames) {
+ for (auto &col : *columnOrder) {
+ if (auto renamed = renames->FindPtr(col)) {
+ col = *renamed;
+ }
+ }
+ }
+ }
+
+ // sync with output type (add weak columns, etc.)
+ TSet<TStringBuf> allColumns = GetColumnsOfStructOrSequenceOfStruct(*itemType);
+ EraseIf(*columnOrder, [&](const TString& col) { return !allColumns.contains(col); });
+ for (auto& col : *columnOrder) {
+ allColumns.erase(allColumns.find(col));
+ }
+ for (auto& col : allColumns) {
+ columnOrder->push_back(TString(col));
+ }
+
+ return State_->Types->SetColumnOrder(input.Ref(), *columnOrder, ctx);
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleSection(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+ if (!EnsureTupleMinSize(*input.Ref().Child(TYtSection::idx_Paths), 1, ctx)) {
+ return TStatus::Error;
+ }
+ for (auto& child: input.Ref().Child(TYtSection::idx_Paths)->Children()) {
+ if (!child->IsCallable(TYtPath::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Expected " << TYtPath::CallableName()));
+ return TStatus::Error;
+ }
+ }
+ if (!EnsureTuple(*input.Ref().Child(TYtSection::idx_Settings), ctx)) {
+ return TStatus::Error;
+ }
+ EYtSettingTypes acceptedSettings = EYtSettingType::KeyFilter
+ | EYtSettingType::KeyFilter2
+ | EYtSettingType::Take
+ | EYtSettingType::Skip
+ | EYtSettingType::DirectRead
+ | EYtSettingType::Sample
+ | EYtSettingType::JoinLabel
+ | EYtSettingType::Unordered
+ | EYtSettingType::NonUnique
+ | EYtSettingType::UserSchema
+ | EYtSettingType::UserColumns
+ | EYtSettingType::StatColumns
+ | EYtSettingType::SysColumns;
+ if (!ValidateSettings(*input.Ref().Child(TYtSection::idx_Settings), acceptedSettings, ctx)) {
+ return TStatus::Error;
+ }
+
+ const TTypeAnnotationNode* tableType = nullptr;
+ TMaybe<bool> yamrFormat;
+ for (auto child: input.Cast<TYtSection>().Paths()) {
+ if (tableType) {
+ if (!IsSameAnnotation(*tableType, *child.Ref().GetTypeAnn())) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "All section tables must have the same scheme. Found diff: "
+ << GetTypeDiff(*tableType, *child.Ref().GetTypeAnn())));
+ return TStatus::Error;
+ }
+ } else {
+ tableType = child.Ref().GetTypeAnn();
+ }
+ if (yamrFormat) {
+ if (*yamrFormat != child.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder() << "Mixed Yamr/Yson input table formats"));
+ return TStatus::Error;
+ }
+ } else {
+ yamrFormat = child.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+ }
+ }
+
+ const auto sysColumns = NYql::GetSettingAsColumnList(*input.Ref().Child(TYtSection::idx_Settings), EYtSettingType::SysColumns);
+ if (!sysColumns.empty()) {
+ if (AnyOf(sysColumns, [](const TString& col) { return col == "path" || col == "record"; })) {
+ for (auto& child: input.Ref().Child(TYtSection::idx_Paths)->Children()) {
+ if (!TYtTable::Match(child->Child(TYtPath::idx_Table))) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()),
+ TStringBuilder() << EYtSettingType::SysColumns << " setting cannot be used with operation results"));
+ return TStatus::Error;
+ }
+ }
+ }
+
+ const auto structType = tableType->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ auto columns = structType->GetItems();
+ for (auto sys: sysColumns) {
+ auto sysColName = TString(YqlSysColumnPrefix).append(sys);
+ if (!structType->FindItem(sysColName)) {
+ try {
+ columns.push_back(ctx.MakeType<TItemExprType>(sysColName, ctx.MakeType<TDataExprType>(NUdf::GetDataSlot(GetSysColumnTypeId(sys)))));
+ } catch (const TYqlPanic&) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Ref().Child(TYtSection::idx_Settings)->Pos()),
+ TStringBuilder() << "Unsupported system column " << sys.Quote()));
+ return TStatus::Error;
+ }
+ }
+ }
+ tableType = ctx.MakeType<TListExprType>(ctx.MakeType<TStructExprType>(columns));
+ }
+
+ auto keyFilters = GetAllSettingValues(*input.Ref().Child(TYtSection::idx_Settings), EYtSettingType::KeyFilter);
+ if (keyFilters.size() > 0) {
+ TVector<std::pair<TString, TYqlRowSpecInfo::TPtr>> rowSpecs;
+ for (auto path: input.Cast<TYtSection>().Paths()) {
+ TYtPathInfo pathInfo(path);
+ rowSpecs.emplace_back(pathInfo.Table->Name, pathInfo.Table->RowSpec);
+ }
+
+ TIssueScopeGuard issueScope(ctx.IssueManager, [pos = ctx.GetPosition(input.Pos())]() {
+ return MakeIntrusive<TIssue>(pos, TStringBuilder() << "Setting " << EYtSettingType::KeyFilter);
+ });
+
+ for (auto keyFilter: keyFilters) {
+ if (keyFilter->ChildrenSize() > 0) {
+ TMaybe<size_t> tableIndex;
+ if (keyFilter->ChildrenSize() > 1) {
+ tableIndex = FromString<size_t>(keyFilter->Child(1)->Content());
+ if (tableIndex >= rowSpecs.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyFilter->Child(1)->Pos()), TStringBuilder()
+ << "Invalid table index value: " << *tableIndex));
+ return TStatus::Error;
+ }
+ }
+ auto andGrp = TCoNameValueTupleList(keyFilter->ChildPtr(0));
+ size_t memberIndex = 0;
+ for (auto keyPredicates: andGrp) {
+ auto validateMember = [&] (const TString& tableName, const TYqlRowSpecInfo::TPtr& rowSpec) {
+ if (!rowSpec || !rowSpec->IsSorted()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Setting " << EYtSettingType::KeyFilter << " cannot be used with unsorted table "
+ << tableName));
+ return false;
+ }
+ if (memberIndex >= rowSpec->SortMembers.size()
+ || rowSpec->SortMembers[memberIndex] != keyPredicates.Name().Value())
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Predicate column " << TString{keyPredicates.Name().Value()}.Quote() << " doesn't match "
+ << tableName.Quote() << " table sort columns"));
+ return false;
+ }
+ if (!rowSpec->SortDirections[memberIndex]) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Predicate column " << TString{keyPredicates.Name().Value()}.Quote()
+ << " has descending sort order in "
+ << tableName.Quote() << " table"));
+ return false;
+ }
+ return true;
+ };
+
+ if (tableIndex) {
+ auto& rowSpec = rowSpecs[*tableIndex];
+ if (!validateMember(rowSpec.first, rowSpec.second)) {
+ return TStatus::Error;
+ }
+ }
+ else {
+ for (auto& rowSpec: rowSpecs) {
+ if (!validateMember(rowSpec.first, rowSpec.second)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ for (auto cmp: keyPredicates.Value().Cast<TCoNameValueTupleList>()) {
+ TExprBase value = cmp.Value().Cast();
+ if (!IsNull(value.Ref())) {
+ bool isOptional = false;
+ const TDataExprType* valueType = nullptr;
+ if (!EnsureDataOrOptionalOfData(value.Ref(), isOptional, valueType, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto& rowSpec = rowSpecs[tableIndex.GetOrElse(0)];
+ const TDataExprType* columnType = nullptr;
+ if (!EnsureDataOrOptionalOfData(rowSpec.second->FromNode.Cast().Pos(), rowSpec.second->SortedByTypes[memberIndex], isOptional, columnType, ctx)) {
+ return TStatus::Error;
+ }
+ if (valueType->GetSlot() != columnType->GetSlot()
+ && !GetSuperType(valueType->GetSlot(), columnType->GetSlot()))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Predicate " << TString{keyPredicates.Name().Value()}.Quote() << " value type "
+ << *value.Ref().GetTypeAnn() << " is incompatible with "
+ << *rowSpec.second->SortedByTypes[memberIndex] << " column type of "
+ << rowSpec.first.Quote() << " table"));
+ return TStatus::Error;
+ }
+ }
+ }
+ ++memberIndex;
+ }
+ }
+ }
+ }
+
+ keyFilters = GetAllSettingValues(*input.Ref().Child(TYtSection::idx_Settings), EYtSettingType::KeyFilter2);
+ if (keyFilters.size() > 0) {
+ TVector<std::pair<TString, TYqlRowSpecInfo::TPtr>> rowSpecs;
+ for (auto path: input.Cast<TYtSection>().Paths()) {
+ TYtPathInfo pathInfo(path);
+ rowSpecs.emplace_back(pathInfo.Table->Name, pathInfo.Table->RowSpec);
+ }
+
+ TIssueScopeGuard issueScope(ctx.IssueManager, [pos = ctx.GetPosition(input.Pos())]() {
+ return MakeIntrusive<TIssue>(pos, TStringBuilder() << "Setting " << EYtSettingType::KeyFilter2);
+ });
+
+ TSet<size_t> processedIndexes;
+ for (auto keyFilter: keyFilters) {
+ if (keyFilter->ChildrenSize() == 0) {
+ continue;
+ }
+
+ TMaybe<TVector<size_t>> indexes;
+ if (keyFilter->ChildrenSize() == 3) {
+ indexes.ConstructInPlace();
+ for (auto& idxNode : keyFilter->Tail().ChildrenList()) {
+ YQL_ENSURE(idxNode->IsAtom());
+ indexes->push_back(FromString<size_t>(idxNode->Content()));
+ }
+ }
+
+ TVector<TStringBuf> usedKeys;
+ if (auto usedKeysSetting = GetSetting(*keyFilter->Child(1), "usedKeys")) {
+ for (auto& key : usedKeysSetting->Tail().ChildrenList()) {
+ YQL_ENSURE(key->IsAtom());
+ usedKeys.push_back(key->Content());
+ }
+ }
+
+ auto& computeNode = keyFilter->Head();
+ auto boundaryTypes = computeNode.GetTypeAnn()->Cast<TListExprType>()->GetItemType()
+ ->Cast<TTupleExprType>()->GetItems().front()->Cast<TTupleExprType>()->GetItems();
+
+ YQL_ENSURE(boundaryTypes.size() > 1);
+ // drop include/exclude flag
+ boundaryTypes.resize(boundaryTypes.size() - 1);
+
+ for (size_t i = 0; i < (indexes ? indexes->size() : rowSpecs.size()); ++i) {
+ size_t idx = indexes ? (*indexes)[i] : i;
+ if (idx >= rowSpecs.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyFilter->Pos()), TStringBuilder()
+ << "Invalid table index " << idx << ": got only " << rowSpecs.size() << " input tables"));
+ return TStatus::Error;
+ }
+ if (!processedIndexes.insert(idx).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Duplicate table index " << idx));
+ return TStatus::Error;
+ }
+
+ const auto& nameAndSpec = rowSpecs[idx];
+ if (!nameAndSpec.second || !nameAndSpec.second->IsSorted()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "Setting " << EYtSettingType::KeyFilter2 << " cannot be used with unsorted table "
+ << nameAndSpec.first.Quote()));
+ return TStatus::Error;
+ }
+
+ if (nameAndSpec.second->SortedByTypes.size() < boundaryTypes.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "KeyFilter type size mismatch for index " << idx));
+ return TStatus::Error;
+ }
+
+ if (nameAndSpec.second->SortedBy.size() < usedKeys.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "KeyFilter used keys size mismatch for index " << idx));
+ return TStatus::Error;
+ }
+
+ YQL_ENSURE(usedKeys.size() <= boundaryTypes.size());
+
+ for (size_t j = 0; j < boundaryTypes.size(); ++j) {
+ auto specType = nameAndSpec.second->SortedByTypes[j];
+ auto boundaryType = boundaryTypes[j]->Cast<TOptionalExprType>()->GetItemType();
+ if (!IsSameAnnotation(*specType, *boundaryType)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "KeyFilter type mismatch for index " << idx << ", table name "
+ << nameAndSpec.first.Quote() << ", types: " << *specType
+ << " vs " << *boundaryType));
+ return TStatus::Error;
+ }
+
+ if (j < usedKeys.size()) {
+ auto specName = nameAndSpec.second->SortedBy[j];
+ auto usedKey = usedKeys[j];
+ if (specName != usedKey) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()), TStringBuilder()
+ << "KeyFilter key column name mismatch for index " << idx << ", table name "
+ << nameAndSpec.first.Quote() << ", names: " << specName << " vs " << usedKey));
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ input.Ptr()->SetTypeAnn(tableType);
+
+ auto paths = input.Ref().Child(TYtSection::idx_Paths)->ChildrenList();
+ YQL_ENSURE(!paths.empty());
+ auto common = State_->Types->LookupColumnOrder(*paths.front());
+ if (!common) {
+ return TStatus::Ok;
+ }
+
+ for (ui32 i = 1; i < paths.size(); ++i) {
+ auto current = State_->Types->LookupColumnOrder(*paths[i]);
+ if (!current || common != current) {
+ return TStatus::Ok;
+ }
+ }
+
+ // add system columns
+ auto extraColumns = sysColumns;
+ for (auto& sys: extraColumns) {
+ sys = TString(YqlSysColumnPrefix).append(sys);
+ }
+ Sort(extraColumns);
+
+ common->insert(common->end(), extraColumns.begin(), extraColumns.end());
+ return State_->Types->SetColumnOrder(input.Ref(), *common, ctx);
+ }
+
+ TStatus HandleReadTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 3, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureWorldType(*input->Child(TYtReadTable::idx_World), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureSpecificDataSource(*input->Child(TYtReadTable::idx_DataSource), YtProviderName, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureTupleMinSize(*input->Child(TYtReadTable::idx_Input), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ for (auto& section: input->Child(TYtReadTable::idx_Input)->Children()) {
+ if (!section->IsCallable(TYtSection::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(section->Pos()), TStringBuilder() << "Expected " << TYtSection::CallableName()));
+ return TStatus::Error;
+ }
+ }
+
+ auto cluster = TString{TYtDSource(input->ChildPtr(TYtReadTable::idx_DataSource)).Cluster().Value()};
+ TMaybe<bool> yamrFormat;
+ TMaybe<TSampleParams> sampling;
+ for (size_t i = 0; i < input->Child(TYtReadTable::idx_Input)->ChildrenSize(); ++i) {
+ auto section = TYtSection(input->Child(TYtReadTable::idx_Input)->ChildPtr(i));
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::JoinLabel)) {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), TStringBuilder()
+ << "Setting \"" << EYtSettingType::JoinLabel << "\" is not allowed in " << TYtReadTable::CallableName()));
+ return TStatus::Error;
+ }
+
+ if (0 == i) {
+ sampling = NYql::GetSampleParams(section.Settings().Ref());
+ } else if (NYql::GetSampleParams(section.Settings().Ref()) != sampling) {
+ ctx.AddError(TIssue(ctx.GetPosition(section.Pos()), "Sections have different sample values"));
+ return TStatus::Error;
+ }
+
+ for (auto path: section.Paths()) {
+ if (auto maybeTable = path.Table().Maybe<TYtTable>()) {
+ auto table = maybeTable.Cast();
+ auto tableName = table.Name().Value();
+ if (!NYql::HasSetting(table.Settings().Ref(), EYtSettingType::UserSchema)) {
+ // Don't validate already substituted anonymous tables
+ if (!NYql::HasSetting(table.Settings().Ref(), EYtSettingType::Anonymous) || !tableName.StartsWith("tmp/")) {
+ const TYtTableDescription& tableDesc = State_->TablesData->GetTable(cluster,
+ TString{tableName},
+ TEpochInfo::Parse(table.Epoch().Ref()));
+
+ if (!tableDesc.Validate(ctx.GetPosition(table.Pos()), cluster, tableName,
+ NYql::HasSetting(table.Settings().Ref(), EYtSettingType::WithQB), State_->AnonymousLabels, ctx)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+ if (yamrFormat) {
+ if (*yamrFormat != path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid()) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Pos()), TStringBuilder() << "Mixed Yamr/Yson input table formats"));
+ return TStatus::Error;
+ }
+ } else {
+ yamrFormat = path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+ }
+ }
+ }
+
+ auto readInput = input->ChildPtr(TYtReadTable::idx_Input);
+ auto newInput = ValidateAndUpdateTablesMeta(readInput, cluster, State_->TablesData, State_->Types->UseTableMetaFromGraph, ctx);
+ if (!newInput) {
+ return TStatus::Error;
+ }
+ else if (newInput != readInput) {
+ output = ctx.ChangeChild(*input, TYtReadTable::idx_Input, std::move(newInput));
+ return TStatus::Repeat;
+ }
+
+ TTypeAnnotationNode::TListType items;
+ for (auto section: TYtSectionList(input->ChildPtr(TYtReadTable::idx_Input))) {
+ items.push_back(section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType());
+ }
+ auto itemType = items.size() == 1
+ ? items.front()
+ : ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(items));
+
+ input->SetTypeAnn(ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{
+ input->Child(TYtReadTable::idx_World)->GetTypeAnn(),
+ ctx.MakeType<TListExprType>(itemType)
+ }));
+
+ if (items.size() == 1) {
+ if (auto columnOrder = State_->Types->LookupColumnOrder(input->Child(TYtReadTable::idx_Input)->Head())) {
+ return State_->Types->SetColumnOrder(*input, *columnOrder, ctx);
+ }
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleReadTableScheme(const TExprNode::TPtr& input, TExprNode::TPtr& /*output*/, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 4, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureWorldType(*input->Child(TYtReadTableScheme::idx_World), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureSpecificDataSource(*input->Child(TYtReadTableScheme::idx_DataSource), YtProviderName, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!input->Child(TYtReadTableScheme::idx_Table)->IsCallable(TYtTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(input->Child(TYtReadTableScheme::idx_Table)->Pos()), TStringBuilder()
+ << "Expected " << TYtTable::CallableName()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureType(*input->Child(TYtReadTableScheme::idx_Type), ctx)) {
+ return TStatus::Error;
+ }
+
+ auto tableType = input->Child(TYtReadTableScheme::idx_Type)->GetTypeAnn()->Cast<TTypeExprType>()->GetType();
+ if (!EnsureListType(input->Child(TYtReadTableScheme::idx_Type)->Pos(), *tableType, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto rowType = tableType->Cast<TListExprType>()->GetItemType();
+ if (!EnsureStructType(input->Child(TYtReadTableScheme::idx_Type)->Pos(), *rowType, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto readScheme = TYtReadTableScheme(input);
+ auto cluster = TString{readScheme.DataSource().Cluster().Value()};
+ auto tableName = TString{readScheme.Table().Name().Value()};
+ auto view = NYql::GetSetting(readScheme.Table().Settings().Ref(), EYtSettingType::View);
+ TString viewName = view ? TString{view->Child(1)->Content()} : TString();
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(cluster, tableName,
+ TEpochInfo::Parse(readScheme.Table().Epoch().Ref()));
+
+ // update RowType in description
+ if (!viewName.empty()) {
+ tableDesc.Views[viewName].RowType = rowType;
+ } else if (tableDesc.View.Defined()) {
+ tableDesc.View->RowType = rowType;
+ } else {
+ tableDesc.RowType = rowType;
+ }
+
+ input->SetTypeAnn(ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{
+ input->Child(TYtReadTableScheme::idx_World)->GetTypeAnn(),
+ ctx.MakeType<TDataExprType>(EDataSlot::Yson)
+ }));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTableContent(TExprBase input, TExprContext& ctx) {
+ if (!EnsureArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ const auto tableContent = input.Cast<TYtTableContent>();
+
+ if (!tableContent.Input().Ref().IsCallable(TYtReadTable::CallableName())
+ && !tableContent.Input().Ref().IsCallable(TYtOutput::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(tableContent.Input().Pos()), TStringBuilder()
+ << "Expected " << TYtReadTable::CallableName() << " or " << TYtOutput::CallableName()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureTuple(tableContent.Settings().MutableRef(), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!ValidateSettings(tableContent.Settings().Ref(), EYtSettingType::MemUsage | EYtSettingType::ItemsCount | EYtSettingType::RowFactor | EYtSettingType::Split, ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(tableContent.Input().Maybe<TYtOutput>()
+ ? tableContent.Input().Ref().GetTypeAnn()
+ : tableContent.Input().Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().back());
+
+ if (auto columnOrder = State_->Types->LookupColumnOrder(tableContent.Input().Ref())) {
+ return State_->Types->SetColumnOrder(input.Ref(), *columnOrder, ctx);
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleLength(const TExprNode::TPtr& input, TExprNode::TPtr& /*output*/, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto lenInput = input->Child(TYtLength::idx_Input);
+ if (lenInput->IsCallable(TYtReadTable::CallableName())) {
+ if (TYtReadTable(lenInput).Input().Size() != 1) {
+ ctx.AddError(TIssue(ctx.GetPosition(lenInput->Pos()), TStringBuilder()
+ << "Unsupported " << TYtReadTable::CallableName() << " with multiple sections"));
+ return TStatus::Error;
+ }
+ }
+ else if (!lenInput->IsCallable(TYtOutput::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(lenInput->Pos()), TStringBuilder()
+ << "Expected " << TYtReadTable::CallableName() << " or " << TYtOutput::CallableName()));
+ return TStatus::Error;
+ }
+
+ input->SetTypeAnn(ctx.MakeType<TDataExprType>(EDataSlot::Uint64));;
+ return TStatus::Ok;
+ }
+
+ TStatus HandleConfigure(TExprBase input, TExprContext& ctx) {
+ if (!EnsureMinArgsCount(input.Ref(), 2, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureWorldType(*input.Ptr()->Child(TYtConfigure::idx_World), ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureSpecificDataSource(*input.Ptr()->Child(TYtConfigure::idx_DataSource), YtProviderName, ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(input.Ref().Child(TYtConfigure::idx_World)->GetTypeAnn());
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTablePath(TExprBase input, TExprContext& ctx) {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()),
+ TStringBuilder() << "Unsupported callable " << input.Ref().Content()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureDependsOn(*input.Ptr()->Child(TYtTablePath::idx_DependsOn), ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TDataExprType>(EDataSlot::String));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTableRecord(TExprBase input, TExprContext& ctx) {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()),
+ TStringBuilder() << "Unsupported callable " << input.Ref().Content()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureDependsOn(*input.Ptr()->Child(TYtTablePropBase::idx_DependsOn), ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TDataExprType>(EDataSlot::Uint64));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTableIndex(TExprBase input, TExprContext& ctx) {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()),
+ TStringBuilder() << "Unsupported callable " << input.Ref().Content()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureDependsOn(*input.Ptr()->Child(TYtTableIndex::idx_DependsOn), ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TDataExprType>(EDataSlot::Uint32));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleIsKeySwitch(TExprBase input, TExprContext& ctx) {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ ctx.AddError(TIssue(ctx.GetPosition(input.Pos()),
+ TStringBuilder() << "Unsupported callable " << input.Ref().Content()));
+ return TStatus::Error;
+ }
+
+ if (!EnsureArgsCount(input.Ref(), 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!EnsureDependsOn(*input.Ptr()->Child(TYtIsKeySwitch::idx_DependsOn), ctx)) {
+ return TStatus::Error;
+ }
+
+ input.Ptr()->SetTypeAnn(ctx.MakeType<TDataExprType>(EDataSlot::Bool));
+ return TStatus::Ok;
+ }
+
+ TStatus HandleTableName(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ if (!EnsureArgsCount(*input, 1, ctx)) {
+ return TStatus::Error;
+ }
+
+ output = ctx.Builder(input->Pos())
+ .Callable("Substring")
+ .Add(0, input->ChildPtr(0))
+ .Callable(1, "Inc")
+ .Callable(0, "RFind")
+ .Add(0, input->ChildPtr(0))
+ .Callable(1, "String").Atom(0, "/").Seal()
+ .Callable(2, "Null").Seal()
+ .Seal()
+ .Seal()
+ .Callable(2, "Null").Seal()
+ .Seal()
+ .Build();
+
+ return TStatus::Repeat;
+ }
+private:
+ const TYtState::TPtr State_;
+};
+
+}
+
+THolder<TVisitorTransformerBase> CreateYtDataSourceTypeAnnotationTransformer(TYtState::TPtr state) {
+ return THolder(new TYtDataSourceTypeAnnotationTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
new file mode 100644
index 0000000000..f433534af6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_dq_hybrid.cpp
@@ -0,0 +1,645 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_optimize.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_optimize.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/core/yql_data_provider.h>
+#include <ydb/library/yql/dq/expr_nodes/dq_expr_nodes.h>
+#include <ydb/library/yql/dq/opt/dq_opt_phy.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+
+#include <util/generic/size_literals.h>
+
+#include <algorithm>
+#include <type_traits>
+
+namespace NYql {
+
+namespace {
+
+using namespace NNodes;
+using namespace NDq;
+
+class TYtDqHybridTransformer : public TOptimizeTransformerBase {
+public:
+ TYtDqHybridTransformer(TYtState::TPtr state, THolder<IGraphTransformer>&& finalizer)
+ : TOptimizeTransformerBase(state->Types, NLog::EComponent::ProviderYt, state->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>()))
+ , State_(std::move(state)), Finalizer_(std::move(finalizer))
+ {
+#define HNDL(name) "YtDqHybrid-"#name, Hndl(&TYtDqHybridTransformer::name)
+ AddHandler(0, &TYtFill::Match, HNDL(TryYtFillByDq));
+ AddHandler(0, &TYtSort::Match, HNDL(TryYtSortByDq));
+ AddHandler(0, &TYtMap::Match, HNDL(TryYtMapByDq));
+ AddHandler(0, &TYtReduce::Match, HNDL(TryYtReduceByDq));
+ AddHandler(0, &TYtMapReduce::Match, HNDL(TryYtMapReduceByDq));
+ AddHandler(0, &TYtMerge::Match, HNDL(TryYtMergeByDq));
+#undef HNDL
+ }
+private:
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ if (const auto status = Finalizer_->Transform(input, output, ctx); status.Level != TStatus::Ok)
+ return status;
+
+ return TOptimizeTransformerBase::DoTransform(input, output, ctx);
+ }
+
+ void Rewind() final {
+ Finalizer_->Rewind();
+ TOptimizeTransformerBase::Rewind();
+ }
+
+ bool CanReplaceOnHybrid(const TYtOutputOpBase& operation) const {
+ if (!State_->IsHybridEnabledForCluster(operation.DataSink().Cluster().Value()))
+ return false;
+
+ if (operation.Ref().StartsExecution() || operation.Ref().HasResult())
+ return false;
+
+ if (operation.Output().Size() != 1U)
+ return false;
+
+ if (const auto& trans = operation.Maybe<TYtTransientOpBase>(); trans && trans.Cast().Input().Size() != 1U)
+ return false;
+
+ return !HasSetting(*operation.Ref().Child(4U), EYtSettingType::NoDq);
+ }
+
+ std::optional<std::array<ui64, 2U>> CanReadHybrid(const TYtSection& section) const {
+ if (HasSettingsExcept(section.Settings().Ref(), DqReadSupportedSettings))
+ return std::nullopt;
+
+ std::array<ui64, 2U> stat = {{0ULL, 0ULL}};
+ for (const auto& path : section.Paths()) {
+ if (const TYtPathInfo info(path); info.Ranges)
+ return std::nullopt;
+ else if (const auto& tableInfo = info.Table;
+ !tableInfo || !tableInfo->Stat || !tableInfo->Meta || !tableInfo->RowSpec || tableInfo->Meta->IsDynamic || NYql::HasSetting(tableInfo->Settings.Ref(), EYtSettingType::WithQB))
+ return std::nullopt;
+ else {
+ auto tableSize = tableInfo->Stat->DataSize;
+ if (tableInfo->Meta->Attrs.Value("erasure_codec", "none") != "none") {
+ if (const auto codecCpu = State_->Configuration->ErasureCodecCpuForDq.Get(tableInfo->Cluster)) {
+ tableSize *=* codecCpu;
+ }
+ }
+
+ stat.front() += tableSize;
+ stat.back() += tableInfo->Stat->ChunkCount;
+ }
+ }
+ return stat;
+ }
+
+ TMaybeNode<TExprBase> TryYtFillByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto fill = node.Cast<TYtFill>(); CanReplaceOnHybrid(fill)) {
+ const auto sizeLimit = State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered);
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ if (bool flow = false; !FindNode(fill.Content().Ptr(), [&flow, sizeLimit, chunksLimit, this] (const TExprNode::TPtr& node) {
+ if (node->IsCallable(TCoForwardList::CallableName()))
+ return true;
+ if (node->IsCallable(TCoCollect::CallableName()) && ETypeAnnotationKind::List != node->Head().GetTypeAnn()->GetKind())
+ return true;
+ if (const auto tableContent = TMaybeNode<TYtTableContent>(node)) {
+ if (!flow)
+ return true;
+ if (const auto& maybeRead = tableContent.Cast().Input().Maybe<TYtReadTable>()) {
+ if (const auto& read = maybeRead.Cast(); 1U != read.Input().Size())
+ return true;
+ else {
+ const auto stat = CanReadHybrid(read.Input().Item(0));
+ return !stat || stat->front() > sizeLimit || stat->back() > chunksLimit;
+ }
+ }
+ }
+ flow = node->IsCallable(TCoToFlow::CallableName()) && node->Head().IsCallable(TYtTableContent::CallableName());
+ return false;
+ })) {
+ return Build<TYtTryFirst>(ctx, fill.Pos())
+ .First<TYtDqProcessWrite>()
+ .World(fill.World())
+ .DataSink(fill.DataSink())
+ .Output(fill.Output())
+ .Input<TDqCnResult>()
+ .Output()
+ .Stage<TDqStage>()
+ .Inputs().Build()
+ .Program<TCoLambda>()
+ .Args({})
+ .Body<TDqWrite>()
+ .Input(CloneCompleteFlow(fill.Content().Body().Ptr(), ctx))
+ .Provider().Value(YtProviderName).Build()
+ .Settings<TCoNameValueTupleList>()
+ .Add()
+ .Name().Value("table").Build()
+ .Value(fill.Output().Item(0)).Build()
+ .Build()
+ .Build()
+ .Build()
+ .Settings(TDqStageSettings{.SinglePartition = true}.BuildNode(ctx, fill.Pos()))
+ .Build()
+ .Index().Build(ctx.GetIndexAsString(0), TNodeFlags::Default)
+ .Build()
+ .ColumnHints().Build()
+ .Build()
+ .Flags().Add().Build("FallbackOnError", TNodeFlags::Default).Build()
+ .Build()
+ .Second<TYtFill>()
+ .InitFrom(fill)
+ .Settings(NYql::AddSetting(fill.Settings().Ref(), EYtSettingType::NoDq, {}, ctx))
+ .Build()
+ .Done();
+ }
+ }
+
+ return node;
+ }
+
+ TExprBase MakeYtSortByDq(const TYtTransientOpBase& sort, TExprContext& ctx) const {
+ TSyncMap syncList;
+ const auto& paths = sort.Input().Item(0).Paths();
+ for (auto i = 0U; i < paths.Size(); ++i) {
+ if (const auto mayOut = paths.Item(i).Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(mayOut.Cast()).Ptr(), syncList.size());
+ }
+ }
+ auto newWorld = ApplySyncListToWorld(sort.World().Ptr(), syncList, ctx);
+
+ TExprNode::TPtr direct, selector;
+ if (const auto& sorted = TYtOutTableInfo(sort.Output().Item(0)).RowSpec->GetForeignSort(); !sorted.empty()) {
+ TExprNode::TListType nodes(sorted.size());
+ std::transform(sorted.cbegin(), sorted.cend(), nodes.begin(), [&](const std::pair<TString, bool>& item) { return MakeBool(sort.Pos(), item.second, ctx); });
+ direct = nodes.size() > 1U ? ctx.NewList(sort.Pos(), std::move(nodes)) : std::move(nodes.front());
+ nodes.resize(sorted.size());
+ auto arg = ctx.NewArgument(sort.Pos(), "row");
+ std::transform(sorted.cbegin(), sorted.cend(), nodes.begin(), [&](const std::pair<TString, bool>& item) {
+ return ctx.NewCallable(sort.Pos(), "Member", {arg, ctx.NewAtom(sort.Pos(), item.first)});
+ });
+ selector = ctx.NewLambda(sort.Pos(), ctx.NewArguments(sort.Pos(), {std::move(arg)}), nodes.size() > 1U ? ctx.NewList(sort.Pos(), std::move(nodes)) : std::move(nodes.front()));
+ }
+
+ const auto input = Build<TCoToFlow>(ctx, sort.Pos())
+ .Input<TYtTableContent>()
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource<TYtDSource>()
+ .Category(sort.DataSink().Category())
+ .Cluster(sort.DataSink().Cluster())
+ .Build()
+ .Input(sort.Input())
+ .Build()
+ .Settings().Build()
+ .Build()
+ .Done();
+
+ TExprNode::TPtr limit;
+ if (const auto& limitNode = NYql::GetSetting(sort.Settings().Ref(), EYtSettingType::Limit)) {
+ limit = GetLimitExpr(limitNode, ctx);
+ }
+
+ auto work = direct && selector ?
+ limit ?
+ Build<TCoTopSort>(ctx, sort.Pos())
+ .Input(input)
+ .Count(std::move(limit))
+ .SortDirections(std::move(direct))
+ .KeySelectorLambda(std::move(selector))
+ .Done().Ptr():
+ Build<TCoSort>(ctx, sort.Pos())
+ .Input(input)
+ .SortDirections(std::move(direct))
+ .KeySelectorLambda(std::move(selector))
+ .Done().Ptr():
+ limit ?
+ Build<TCoTake>(ctx, sort.Pos())
+ .Input(input)
+ .Count(std::move(limit))
+ .Done().Ptr():
+ input.Ptr();
+
+ auto settings = NYql::AddSetting(sort.Settings().Ref(), EYtSettingType::NoDq, {}, ctx);
+ auto operation = ctx.ChangeChild(sort.Ref(), TYtTransientOpBase::idx_Settings, std::move(settings));
+
+ return Build<TYtTryFirst>(ctx, sort.Pos())
+ .First<TYtDqProcessWrite>()
+ .World(std::move(newWorld))
+ .DataSink(sort.DataSink())
+ .Output(sort.Output())
+ .Input<TDqCnResult>()
+ .Output()
+ .Stage<TDqStage>()
+ .Inputs().Build()
+ .Program<TCoLambda>()
+ .Args({})
+ .Body<TDqWrite>()
+ .Input(std::move(work))
+ .Provider().Value(YtProviderName).Build()
+ .Settings<TCoNameValueTupleList>()
+ .Add()
+ .Name().Value("table", TNodeFlags::Default).Build()
+ .Value(sort.Output().Item(0)).Build()
+ .Build()
+ .Build()
+ .Build()
+ .Settings(TDqStageSettings{.SinglePartition = true}.BuildNode(ctx, sort.Pos()))
+ .Build()
+ .Index().Build(ctx.GetIndexAsString(0), TNodeFlags::Default)
+ .Build()
+ .ColumnHints().Build()
+ .Build()
+ .Flags().Add().Build("FallbackOnError", TNodeFlags::Default).Build()
+ .Build()
+ .Second(std::move(operation))
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> TryYtSortByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto sort = node.Cast<TYtSort>(); CanReplaceOnHybrid(sort)) {
+ if (const auto sizeLimit = State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered)) {
+ const auto info = TYtTableBaseInfo::Parse(sort.Input().Item(0).Paths().Item(0).Table());
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ if (const auto stat = CanReadHybrid(sort.Input().Item(0)); stat && stat->front() <= sizeLimit && stat->back() <= chunksLimit) {
+ YQL_CLOG(INFO, ProviderYt) << "Sort on DQ with equivalent input size " << stat->front() << " and " << stat->back() << " chunks.";
+ return MakeYtSortByDq(sort, ctx);
+ }
+ }
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> TryYtMergeByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto merge = node.Cast<TYtMerge>(); CanReplaceOnHybrid(merge) && !NYql::HasSetting(merge.Settings().Ref(), EYtSettingType::CombineChunks)) {
+ if (const auto sizeLimit = State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered)) {
+ const auto info = TYtTableBaseInfo::Parse(merge.Input().Item(0).Paths().Item(0).Table());
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ if (const auto stat = CanReadHybrid(merge.Input().Item(0)); stat && stat->front() <= sizeLimit && stat->back() <= chunksLimit) {
+ YQL_CLOG(INFO, ProviderYt) << "Merge on DQ with equivalent input size " << stat->front() << " and " << stat->back() << " chunks.";
+ return MakeYtSortByDq(merge, ctx);
+ }
+ }
+ }
+
+ return node;
+ }
+
+ bool CanExecuteInHybrid(const TExprNode::TPtr& handler, ui64 chunksLimit, ui64 sizeLimit) const {
+ bool flow = false;
+ return IsYieldTransparent(handler, *State_->Types) &&
+ !FindNode(handler, [&flow, sizeLimit, chunksLimit, this] (const TExprNode::TPtr& node) {
+ if (TCoScriptUdf::Match(node.Get()) && NKikimr::NMiniKQL::IsSystemPython(NKikimr::NMiniKQL::ScriptTypeFromStr(node->Head().Content()))) {
+ return true;
+ }
+
+ if (const auto& tableContent = TMaybeNode<TYtTableContent>(node)) {
+ if (!flow)
+ return true;
+ if (const auto& maybeRead = tableContent.Cast().Input().Maybe<TYtReadTable>()) {
+ if (const auto& read = maybeRead.Cast(); 1U != read.Input().Size())
+ return true;
+ else {
+ const auto stat = CanReadHybrid(read.Input().Item(0));
+ return !stat || stat->front() > sizeLimit || stat->back() > chunksLimit;
+ }
+ }
+ }
+ flow = node->IsCallable(TCoToFlow::CallableName()) && node->Head().IsCallable(TYtTableContent::CallableName());
+ return false;
+ });
+ }
+
+ TMaybeNode<TExprBase> TryYtMapByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto map = node.Cast<TYtMap>(); CanReplaceOnHybrid(map)) {
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ const bool ordered = NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered);
+ const auto sizeLimit = ordered ?
+ State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered):
+ State_->Configuration->HybridDqDataSizeLimitForUnordered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForUnordered);
+ if (const auto stat = CanReadHybrid(map.Input().Item(0)); stat && stat->front() <= sizeLimit && stat->back() <= chunksLimit) {
+ if (CanExecuteInHybrid(map.Mapper().Ptr(), chunksLimit, sizeLimit)) {
+ YQL_CLOG(INFO, ProviderYt) << "Map on DQ with equivalent input size " << stat->front() << " and " << stat->back() << " chunks.";
+ TSyncMap syncList;
+ const auto& paths = map.Input().Item(0).Paths();
+ for (auto i = 0U; i < paths.Size(); ++i) {
+ if (const auto mayOut = paths.Item(i).Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(mayOut.Cast()).Ptr(), syncList.size());
+ }
+ }
+ auto newWorld = ApplySyncListToWorld(map.World().Ptr(), syncList, ctx);
+
+ auto settings = ctx.NewList(map.Input().Pos(), {});
+ if (!ordered) {
+ settings = NYql::AddSetting(*settings, EYtSettingType::Split, nullptr, ctx);
+ }
+
+ auto stage = Build<TDqStage>(ctx, map.Pos())
+ .Inputs().Build()
+ .Program<TCoLambda>()
+ .Args({})
+ .Body<TDqWrite>()
+ .Input<TExprApplier>()
+ .Apply(map.Mapper())
+ .With<TCoToFlow>(0)
+ .Input<TYtTableContent>()
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource<TYtDSource>()
+ .Category(map.DataSink().Category())
+ .Cluster(map.DataSink().Cluster())
+ .Build()
+ .Input(map.Input())
+ .Build()
+ .Settings(std::move(settings))
+ .Build()
+ .Build()
+ .Build()
+ .Provider().Value(YtProviderName).Build()
+ .Settings<TCoNameValueTupleList>()
+ .Add()
+ .Name().Value("table", TNodeFlags::Default).Build()
+ .Value(map.Output().Item(0)).Build()
+ .Build()
+ .Build()
+ .Build()
+ .Settings(TDqStageSettings{.SinglePartition = ordered}.BuildNode(ctx, map.Pos()))
+ .Done();
+
+ if (!ordered) {
+ stage = Build<TDqStage>(ctx, map.Pos())
+ .Inputs()
+ .Add<TDqCnUnionAll>()
+ .Output()
+ .Stage(std::move(stage))
+ .Index().Build(ctx.GetIndexAsString(0), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Build()
+ .Program().Args({"pass"}).Body("pass").Build()
+ .Settings(TDqStageSettings().BuildNode(ctx, map.Pos()))
+ .Done();
+ }
+
+ return Build<TYtTryFirst>(ctx, map.Pos())
+ .First<TYtDqProcessWrite>()
+ .World(std::move(newWorld))
+ .DataSink(map.DataSink())
+ .Output(map.Output())
+ .Input<TDqCnResult>()
+ .Output()
+ .Stage(std::move(stage))
+ .Index().Build(ctx.GetIndexAsString(0), TNodeFlags::Default)
+ .Build()
+ .ColumnHints().Build()
+ .Build()
+ .Flags().Add().Build("FallbackOnError", TNodeFlags::Default).Build()
+ .Build()
+ .Second<TYtMap>()
+ .InitFrom(map)
+ .Settings(NYql::AddSetting(map.Settings().Ref(), EYtSettingType::NoDq, {}, ctx))
+ .Build()
+ .Done();
+ }
+ }
+ }
+
+ return node;
+ }
+
+ template<class TYtOperation>
+ TExprBase MakeYtReduceByDq(const TYtOperation& reduce, TExprContext& ctx) const {
+ TSyncMap syncList;
+ const auto& paths = reduce.Input().Item(0).Paths();
+ for (auto i = 0U; i < paths.Size(); ++i) {
+ if (const auto mayOut = paths.Item(i).Table().template Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(mayOut.Cast()).Ptr(), syncList.size());
+ }
+ }
+ auto newWorld = ApplySyncListToWorld(reduce.World().Ptr(), syncList, ctx);
+
+ auto keys = NYql::GetSettingAsColumnAtomList(reduce.Settings().Ref(), EYtSettingType::ReduceBy);
+
+ auto sortKeys = ctx.NewCallable(reduce.Pos(), "Void", {});
+ auto sortDirs = sortKeys;
+
+ const auto keysBuilder = [](TExprNode::TListType& keys, TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ if (1U == keys.size()) {
+ parent.Callable("Member")
+ .Arg(0, "row")
+ .Add(1, std::move(keys.front()))
+ .Seal();
+ } else {
+ parent.List()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (auto i = 0U; i < keys.size(); ++i) {
+ parent.Callable(i, "Member")
+ .Arg(0, "row")
+ .Add(1, std::move(keys[i]))
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal();
+ }
+ return parent;
+ };
+
+ if (auto sortBy = NYql::GetSettingAsColumnAtomPairList(reduce.Settings().Ref(), EYtSettingType::SortBy); !sortBy.empty()) {
+ for (auto it = keys.cbegin(); !sortBy.empty() && keys.cend() != it && *it == sortBy.front().first; ++it)
+ sortBy.erase(sortBy.cbegin());
+
+ if (!sortBy.empty()) {
+ TExprNode::TListType sort, dirs;
+ sort.reserve(sortBy.size());
+ dirs.reserve(sortBy.size());
+
+ for (auto& item : sortBy) {
+ sort.emplace_back(std::move(item.first));
+ dirs.emplace_back(MakeBool(reduce.Pos(), item.second, ctx));
+ }
+
+ sortDirs = 1U == dirs.size() ? std::move(dirs.front()) : ctx.NewList(reduce.Pos(), std::move(dirs));
+ sortKeys = ctx.Builder(reduce.Pos())
+ .Lambda()
+ .Param("row")
+ .Do(std::bind(keysBuilder, std::ref(sort), std::placeholders::_1))
+ .Seal().Build();
+ }
+ }
+
+ const auto extract = TCoLambda(ctx.Builder(reduce.Pos())
+ .Lambda()
+ .Param("row")
+ .Do(std::bind(keysBuilder, std::ref(keys), std::placeholders::_1))
+ .Seal().Build());
+
+ const bool hasGetSysKeySwitch = bool(FindNode(reduce.Reducer().Body().Ptr(),
+ [](const TExprNode::TPtr& node) {
+ return !TYtOutput::Match(node.Get());
+ },
+ [](const TExprNode::TPtr& node) {
+ return TCoMember::Match(node.Get()) && node->Head().IsArgument() && node->Tail().IsAtom(YqlSysColumnKeySwitch);
+ }
+ ));
+
+ auto input = Build<TCoToFlow>(ctx, reduce.Pos())
+ .template Input<TYtTableContent>()
+ .template Input<TYtReadTable>()
+ .template World<TCoWorld>().Build()
+ .template DataSource<TYtDSource>()
+ .Category(reduce.DataSink().Category())
+ .Cluster(reduce.DataSink().Cluster())
+ .Build()
+ .Input(reduce.Input())
+ .Build()
+ .Settings().Build()
+ .Build()
+ .Done().Ptr();
+
+ if constexpr (std::is_same<TYtOperation, TYtMapReduce>::value) {
+ if (const auto maybeMapper = reduce.Mapper().template Maybe<TCoLambda>()) {
+ input = Build<TExprApplier>(ctx, reduce.Pos()).Apply(maybeMapper.Cast()).With(0, TExprBase(std::move(input))).Done().Ptr();
+ }
+ }
+
+ auto arg = ctx.NewArgument(reduce.Pos(), "list");
+
+ auto body = hasGetSysKeySwitch ? Build<TCoChain1Map>(ctx, reduce.Pos())
+ .Input(arg)
+ .InitHandler()
+ .Args({"first"})
+ .template Body<TCoAddMember>()
+ .Struct("first")
+ .Name().Value(YqlSysColumnKeySwitch).Build()
+ .Item(MakeBool<false>(reduce.Pos(), ctx))
+ .Build()
+ .Build()
+ .UpdateHandler()
+ .Args({"next", "prev"})
+ .template Body<TCoAddMember>()
+ .Struct("next")
+ .Name().Value(YqlSysColumnKeySwitch).Build()
+ .template Item<TCoAggrNotEqual>()
+ .template Left<TExprApplier>()
+ .Apply(extract).With(0, "next")
+ .Build()
+ .template Right<TExprApplier>()
+ .Apply(extract).With(0, "prev")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr() : arg;
+
+ const auto& items = GetSeqItemType(*reduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()).template Cast<TStructExprType>()->GetItems();
+ TExprNode::TListType fields(items.size());
+ std::transform(items.cbegin(), items.cend(), fields.begin(), [&](const TItemExprType* item) { return ctx.NewAtom(reduce.Pos(), item->GetName()); });
+ body = Build<TExprApplier>(ctx, reduce.Pos())
+ .Apply(reduce.Reducer())
+ .template With<TCoExtractMembers>(0)
+ .Input(std::move(body))
+ .Members().Add(std::move(fields)).Build()
+ .Build()
+ .Done().Ptr();
+
+ auto reducer = ctx.NewLambda(reduce.Pos(), ctx.NewArguments(reduce.Pos(), {std::move(arg)}), std::move(body));
+
+ return Build<TYtTryFirst>(ctx, reduce.Pos())
+ .template First<TYtDqProcessWrite>()
+ .World(std::move(newWorld))
+ .DataSink(reduce.DataSink())
+ .Output(reduce.Output())
+ .template Input<TDqCnResult>()
+ .Output()
+ .template Stage<TDqStage>()
+ .Inputs().Build()
+ .template Program<TCoLambda>()
+ .Args({})
+ .template Body<TDqWrite>()
+ .template Input<TCoPartitionsByKeys>()
+ .Input(std::move(input))
+ .KeySelectorLambda(extract)
+ .SortDirections(std::move(sortDirs))
+ .SortKeySelectorLambda(std::move(sortKeys))
+ .ListHandlerLambda(std::move(reducer))
+ .Build()
+ .Provider().Value(YtProviderName).Build()
+ .template Settings<TCoNameValueTupleList>()
+ .Add()
+ .Name().Value("table", TNodeFlags::Default).Build()
+ .Value(reduce.Output().Item(0)).Build()
+ .Build()
+ .Build()
+ .Build()
+ .Settings(TDqStageSettings{.SinglePartition = true}.BuildNode(ctx, reduce.Pos()))
+ .Build()
+ .Index().Build(ctx.GetIndexAsString(0), TNodeFlags::Default)
+ .Build()
+ .ColumnHints().Build()
+ .Build()
+ .Flags().Add().Build("FallbackOnError", TNodeFlags::Default).Build()
+ .Build()
+ .template Second<TYtOperation>()
+ .InitFrom(reduce)
+ .Settings(NYql::AddSetting(reduce.Settings().Ref(), EYtSettingType::NoDq, {}, ctx))
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> TryYtReduceByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto reduce = node.Cast<TYtReduce>(); CanReplaceOnHybrid(reduce)) {
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ const auto sizeLimit = State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered);
+ if (const auto stat = CanReadHybrid(reduce.Input().Item(0)); stat && stat->front() <= sizeLimit && stat->back() <= chunksLimit) {
+ if (CanExecuteInHybrid(reduce.Reducer().Ptr(), chunksLimit, sizeLimit)) {
+ if (ETypeAnnotationKind::Struct == GetSeqItemType(*reduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()).GetKind()) {
+ YQL_CLOG(INFO, ProviderYt) << "Reduce on DQ with equivalent input size " << stat->front() << " and " << stat->back() << " chunks.";
+ return MakeYtReduceByDq(reduce, ctx);
+ }
+ }
+ }
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> TryYtMapReduceByDq(TExprBase node, TExprContext& ctx) const {
+ if (const auto mapReduce = node.Cast<TYtMapReduce>(); CanReplaceOnHybrid(mapReduce)) {
+ const auto chunksLimit = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ);
+ const auto sizeLimit = State_->Configuration->HybridDqDataSizeLimitForOrdered.Get().GetOrElse(DefaultHybridDqDataSizeLimitForOrdered);
+ if (const auto stat = CanReadHybrid(mapReduce.Input().Item(0)); stat && stat->front() <= sizeLimit && stat->back() <= chunksLimit) {
+ if (CanExecuteInHybrid(mapReduce.Reducer().Ptr(), chunksLimit, sizeLimit) && CanExecuteInHybrid(mapReduce.Mapper().Ptr(), chunksLimit, sizeLimit)) {
+ if (ETypeAnnotationKind::Struct == GetSeqItemType(*mapReduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()).GetKind()) {
+ YQL_CLOG(INFO, ProviderYt) << "MapReduce on DQ with equivalent input size " << stat->front() << " and " << stat->back() << " chunks.";
+ return MakeYtReduceByDq(mapReduce, ctx);
+ }
+ }
+ }
+ }
+
+ return node;
+ }
+
+ const TYtState::TPtr State_;
+ const THolder<IGraphTransformer> Finalizer_;
+};
+
+} // namespce
+
+THolder<IGraphTransformer> CreateYtDqHybridTransformer(TYtState::TPtr state, THolder<IGraphTransformer>&& finalizer) {
+ return THolder<IGraphTransformer>(new TYtDqHybridTransformer(std::move(state), std::move(finalizer)));
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
new file mode 100644
index 0000000000..54f1686d0b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.cpp
@@ -0,0 +1,582 @@
+#include "yql_yt_dq_integration.h"
+#include "yql_yt_table.h"
+#include "yql_yt_mkql_compiler.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/lib/yson_helpers/yson_helpers.h>
+
+#include <ydb/library/yql/providers/common/dq/yql_dq_integration_impl.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/dq/common/yql_dq_settings.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/iterator/enumerate.h>
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/size_literals.h>
+#include <util/generic/utility.h>
+
+
+namespace NYql {
+
+static const THashSet<TStringBuf> UNSUPPORTED_YT_PRAGMAS = {"maxrowweight", "pooltrees", "layerpaths", "operationspec"};
+
+using namespace NNodes;
+
+class TYtDqIntegration: public TDqIntegrationBase {
+public:
+ TYtDqIntegration(TYtState* state)
+ : State_(state)
+ {
+ }
+
+ TVector<TMaybe<TVector<ui64>>> EstimateColumnStats(TExprContext& ctx, const TString& cluster, const TVector<TVector<TYtPathInfo::TPtr>>& groupIdPathInfos, ui64& sumAllTableSizes) {
+ TVector<TMaybe<TVector<ui64>>> groupIdColumnarStats;
+ for (const auto& pathInfos: groupIdPathInfos) {
+ TVector<TString> columns;
+ bool hasLookups = false;
+ for (const auto& pathInfo: pathInfos) {
+ if (const auto cols = pathInfo->Columns) {
+ if (columns.empty() && cols->GetColumns()) {
+ for (auto& column : *cols->GetColumns()) {
+ columns.push_back(column.Name);
+ }
+ }
+
+ if (pathInfo->Table->Meta && pathInfo->Table->Meta->Attrs.Value("optimize_for", "scan") == "lookup") {
+ hasLookups = true;
+ }
+ }
+ }
+
+ TMaybe<TVector<ui64>> columnarStat;
+
+ if (!columns.empty() && !hasLookups) {
+ columnarStat = EstimateDataSize(cluster, pathInfos, {columns}, *State_, ctx);
+ }
+
+ groupIdColumnarStats.push_back(columnarStat);
+ for (const auto& [pathId, path] : Enumerate(pathInfos)){
+ const auto& tableInfo = *path->Table;
+ ui64 dataSize = columnarStat ? (*columnarStat)[pathId] : tableInfo.Stat->DataSize;
+ sumAllTableSizes += dataSize;
+ }
+ }
+ return groupIdColumnarStats;
+ }
+
+ ui64 Partition(const TDqSettings& config, size_t maxTasks, const TExprNode& node,
+ TVector<TString>& serializedPartitions, TString* clusterName, TExprContext& ctx, bool canFallback) override
+ {
+ auto dataSizePerJob = config.DataSizePerJob.Get().GetOrElse(TDqSettings::TDefault::DataSizePerJob);
+ if (!TMaybeNode<TYtReadTable>(&node).IsValid()) {
+ return 0;
+ }
+
+ const auto ytRead = TYtReadTable(&node);
+ const auto cluster = ytRead.DataSource().Cluster().StringValue();
+
+ if (clusterName) {
+ *clusterName = cluster;
+ }
+
+ TVector<TVector<TYtPathInfo::TPtr>> groupIdPathInfos;
+ bool hasErasure = false;
+ ui64 chunksCount = 0;
+ for (const auto& input : ytRead.Input()) {
+ TVector<TYtPathInfo::TPtr> pathInfos;
+ for (const auto& path : input.Paths()) {
+ TYtPathInfo::TPtr pathInfo = new TYtPathInfo(path);
+ pathInfos.push_back(pathInfo);
+ if (pathInfo->Table->Meta && pathInfo->Table->Meta->Attrs.Value("erasure_codec", "none") != "none") {
+ hasErasure = true;
+ }
+ chunksCount += pathInfo->Table->Stat->ChunkCount;
+ }
+ groupIdPathInfos.push_back(pathInfos);
+ }
+
+ if (auto maxChunks = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ); canFallback && chunksCount > maxChunks) {
+ throw TFallbackError() << "DQ cannot execute the query. Cause: table with too many chunks";
+ }
+
+ if (hasErasure) {
+ if (auto codecCpu = State_->Configuration->ErasureCodecCpuForDq.Get(cluster)) {
+ dataSizePerJob = Max(ui64(dataSizePerJob / *codecCpu), 10_KB);
+ } else {
+ hasErasure = false;
+ }
+ }
+
+ ui64 maxDataSizePerJob = 0;
+ if (State_->Configuration->_EnableYtPartitioning.Get(cluster).GetOrElse(false)) {
+ TVector<TYtPathInfo::TPtr> paths;
+ TVector<TString> keys;
+ TMaybe<double> sample;
+ for (const auto& [groupId, pathInfos] : Enumerate(groupIdPathInfos)) {
+ if (auto sampleSetting = GetSetting(ytRead.Input().Item(groupId).Settings().Ref(), EYtSettingType::Sample)) {
+ sample = FromString<double>(sampleSetting->Child(1)->Child(1)->Content());
+ }
+ for (const auto& [pathId, pathInfo] : Enumerate(pathInfos)) {
+ auto tableName = pathInfo->Table->Name;
+ if (pathInfo->Table->IsAnonymous && !TYtTableInfo::HasSubstAnonymousLabel(pathInfo->Table->FromNode.Cast())) {
+ tableName = State_->AnonymousLabels.Value(std::make_pair(cluster, tableName), TString());
+ YQL_ENSURE(tableName, "Unaccounted anonymous table: " << pathInfo->Table->Name);
+ pathInfo->Table->Name = tableName;
+ }
+
+ paths.push_back(pathInfo);
+ keys.emplace_back(TStringBuilder() << groupId << "/" << pathId);
+ }
+ }
+ if (sample && *sample > 0) {
+ dataSizePerJob /= *sample;
+ }
+
+ auto res = State_->Gateway->GetTablePartitions(NYql::IYtGateway::TGetTablePartitionsOptions(State_->SessionId)
+ .Cluster(cluster)
+ .MaxPartitions(maxTasks)
+ .DataSizePerJob(dataSizePerJob)
+ .AdjustDataWeightPerPartition(!canFallback)
+ .Config(State_->Configuration->Snapshot())
+ .Paths(std::move(paths)));
+ if (!res.Success()) {
+ const auto message = TStringBuilder() << "DQ cannot execute the query. Cause: failed to partition table";
+ YQL_CLOG(ERROR, ProviderDq) << message;
+ auto issue = YqlIssue(TPosition(), TIssuesIds::DQ_GATEWAY_NEED_FALLBACK_ERROR, message);
+ for (auto& subIssue: res.Issues()) {
+ issue.AddSubIssue(MakeIntrusive<TIssue>(subIssue));
+ }
+
+ ctx.IssueManager.RaiseIssues(res.Issues());
+ if (canFallback) {
+ throw TFallbackError() << message;
+ } else {
+ throw TErrorException(TIssuesIds::DQ_GATEWAY_NEED_FALLBACK_ERROR) << message;
+ }
+ }
+
+ serializedPartitions.reserve(res.Partitions.Partitions.size());
+ for (const auto& partition : res.Partitions.Partitions) {
+ NYT::TNode part = NYT::TNode::CreateMap();
+ for (const auto& [pathId, path]: Enumerate(partition.TableRanges)) {
+ // n.b. we're expecting YT API to return ranges in the same order as they were passed
+ part[keys[pathId]] = NYT::PathToNode(path);
+ }
+ serializedPartitions.push_back(NYT::NodeToYsonString(part));
+ YQL_CLOG(TRACE, ProviderDq) << "Partition: " << NYT::NodeToYsonString(part, ::NYson::EYsonFormat::Pretty);
+ }
+ } else {
+ TVector<TVector<std::tuple<ui64, ui64, NYT::TRichYPath>>> partitionTuplesArr;
+
+ ui64 sumAllTableSizes = 0;
+
+ TVector<TMaybe<TVector<ui64>>> groupIdColumnarStats = EstimateColumnStats(ctx, cluster, groupIdPathInfos, sumAllTableSizes);
+
+ ui64 parts = (sumAllTableSizes + dataSizePerJob - 1) / dataSizePerJob;
+ if (canFallback && hasErasure && parts > maxTasks) {
+ std::string_view message = "DQ cannot execute the query. Cause: too big table with erasure codec";
+ YQL_CLOG(INFO, ProviderDq) << message;
+ throw TFallbackError() << message;
+ }
+ parts = Min<ui64>(parts, maxTasks);
+ parts = Max<ui64>(parts, 1);
+ partitionTuplesArr.resize(parts);
+ serializedPartitions.resize(parts);
+
+ for (const auto& [groupId, input] : Enumerate(ytRead.Input())) {
+ TMaybe<double> sample;
+ auto sampleSetting = GetSetting(input.Settings().Ref(), EYtSettingType::Sample);
+ if (sampleSetting) {
+ sample = FromString<double>(sampleSetting->Child(1)->Child(1)->Content());
+ }
+ for (const auto& [pathId, path] : Enumerate(groupIdPathInfos[groupId])) {
+ const auto& tableInfo = *path->Table;
+ YQL_ENSURE(tableInfo.Stat, "Table has no stat.");
+ auto columnarStat = groupIdColumnarStats[groupId];
+ ui64 dataSize = columnarStat ? (*columnarStat)[pathId] : tableInfo.Stat->DataSize;
+ if (sample) {
+ dataSize *=* sample;
+ }
+ maxDataSizePerJob = Max(maxDataSizePerJob, (dataSize + parts - 1) / parts);
+ ui64 rowsPerPart = (tableInfo.Stat->RecordsCount + parts - 1) / parts;
+ for (ui64 from = 0, i = 0; from < tableInfo.Stat->RecordsCount; from += rowsPerPart, i++) {
+ ui64 to = Min(from + rowsPerPart, tableInfo.Stat->RecordsCount);
+ NYT::TRichYPath path;
+ path.AddRange(NYT::TReadRange::FromRowIndices(from, to));
+ partitionTuplesArr[i].push_back({groupId, pathId, path});
+ }
+ }
+ }
+ int i = 0;
+ for (const auto& partitionTuples: partitionTuplesArr) {
+ TStringStream out;
+ NYson::TYsonWriter writer((IOutputStream*)&out);
+ writer.OnBeginMap();
+ for (const auto& partitionTuple : partitionTuples) {
+ writer.OnKeyedItem(TStringBuilder() << std::get<0>(partitionTuple) << "/" << std::get<1>(partitionTuple));
+ writer.OnRaw(NYT::NodeToYsonString(NYT::PathToNode(std::get<2>(partitionTuple))));
+ }
+ writer.OnEndMap();
+ serializedPartitions[i++] = out.Str();
+ }
+ }
+ return maxDataSizePerJob;
+ }
+
+ void AddInfo(TExprContext& ctx, const TString& message, bool skipIssues) {
+ if (!skipIssues) {
+ YQL_CLOG(INFO, ProviderDq) << message;
+ TIssue info("DQ cannot execute the query. Cause: " + message);
+ info.Severity = TSeverityIds::S_INFO;
+ ctx.IssueManager.RaiseIssue(info);
+ }
+ }
+
+ bool CheckPragmas(const TExprNode& node, TExprContext& ctx, bool skipIssues) override {
+ if (TYtConfigure::Match(&node)) {
+ if (node.ChildrenSize() >= 5) {
+ if (node.Child(2)->Content() == "Attr" && node.Child(3)->Content() == "maxrowweight") {
+ if (FromString<NSize::TSize>(node.Child(4)->Content()).GetValue()>NSize::FromMegaBytes(128)) {
+ State_->OnlyNativeExecution = true;
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ if (node.ChildrenSize() >= 4) {
+ if (node.Child(2)->Content() == "Attr" && UNSUPPORTED_YT_PRAGMAS.contains(node.Child(3)->Content())) {
+ AddInfo(ctx, TStringBuilder() << "unsupported yt pragma: " << node.Child(3)->Content(), skipIssues);
+ State_->OnlyNativeExecution = true;
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ bool CanRead(const TExprNode& node, TExprContext& ctx, bool skipIssues) override {
+ if (TYtConfigure::Match(&node)) {
+ return CheckPragmas(node, ctx, skipIssues);
+ } else if (auto maybeRead = TMaybeNode<TYtReadTable>(&node)) {
+ auto cluster = maybeRead.Cast().DataSource().Cluster().StringValue();
+ if (!State_->Configuration->_EnableDq.Get(cluster).GetOrElse(true)) {
+ AddInfo(ctx, TStringBuilder() << "disabled for cluster " << cluster, skipIssues);
+ return false;
+ }
+
+ for (auto section: maybeRead.Cast().Input()) {
+ if (HasSettingsExcept(maybeRead.Cast().Input().Item(0).Settings().Ref(), DqReadSupportedSettings)) {
+ TStringBuilder info;
+ info << "unsupported path settings: ";
+ if (maybeRead.Cast().Input().Item(0).Settings().Size() > 0) {
+ for (auto& setting : maybeRead.Cast().Input().Item(0).Settings().Ref().Children()) {
+ if (setting->ChildrenSize() != 0) {
+ info << setting->Child(0)->Content() << ",";
+ }
+ }
+ }
+ AddInfo(ctx, info, skipIssues);
+ return false;
+ }
+ for (auto path: section.Paths()) {
+ if (!path.Table().Maybe<TYtTable>()) {
+ AddInfo(ctx, "non-table path", skipIssues);
+ return false;
+ } else {
+ auto pathInfo = TYtPathInfo(path);
+ auto tableInfo = pathInfo.Table;
+ auto epoch = TEpochInfo::Parse(path.Table().Maybe<TYtTable>().CommitEpoch().Ref());
+ if (!tableInfo->Stat) {
+ AddInfo(ctx, "table without statistics", skipIssues);
+ return false;
+ } else if (!tableInfo->RowSpec) {
+ AddInfo(ctx, "table without row spec", skipIssues);
+ return false;
+ } else if (!tableInfo->Meta) {
+ AddInfo(ctx, "table without meta", skipIssues);
+ return false;
+ } else if (tableInfo->IsAnonymous) {
+ AddInfo(ctx, "anonymous table", skipIssues);
+ return false;
+ } else if ((!epoch.Empty() && *epoch.Get() > 0)) {
+ AddInfo(ctx, "table with non-empty epoch", skipIssues);
+ return false;
+ } else if (NYql::HasSetting(tableInfo->Settings.Ref(), EYtSettingType::WithQB)) {
+ AddInfo(ctx, "table with QB2 premapper", skipIssues);
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+ AddInfo(ctx, TStringBuilder() << "unsupported callable: " << node.Content(), skipIssues);
+ return false;
+ }
+
+ TMaybe<ui64> EstimateReadSize(ui64 dataSizePerJob, ui32 maxTasksPerStage, const TExprNode& node, TExprContext& ctx) override {
+ if (auto maybeRead = TMaybeNode<TYtReadTable>(&node)) {
+
+ ui64 dataSize = 0;
+ bool hasErasure = false;
+ ui64 chunksCount = 0;
+ auto cluster = maybeRead.Cast().DataSource().Cluster().StringValue();
+
+ const auto canUseYtPartitioningApi = State_->Configuration->_EnableYtPartitioning.Get(cluster).GetOrElse(false);
+
+ TVector<TVector<TYtPathInfo::TPtr>> groupIdPathInfos;
+
+ for (auto section: maybeRead.Cast().Input()) {
+ groupIdPathInfos.emplace_back();
+ for (const auto& path: section.Paths()) {
+ auto pathInfo = MakeIntrusive<TYtPathInfo>(path);
+ auto tableInfo = pathInfo->Table;
+
+ YQL_ENSURE(tableInfo);
+ if (!tableInfo->Stat) {
+ continue;
+ }
+
+ if (pathInfo->Ranges && !canUseYtPartitioningApi) {
+ AddErrorWrap(ctx, node.Pos(), "table with ranges");
+ return Nothing();
+ } else if (tableInfo->Meta->IsDynamic && !canUseYtPartitioningApi) {
+ AddErrorWrap(ctx, node.Pos(), "dynamic table");
+ return Nothing();
+ } else { //
+ if (tableInfo->Meta->Attrs.Value("erasure_codec", "none") != "none") {
+ hasErasure = true;
+ }
+ chunksCount += tableInfo->Stat->ChunkCount;
+ }
+ groupIdPathInfos.back().emplace_back(pathInfo);
+ }
+ }
+
+ EstimateColumnStats(ctx, cluster, groupIdPathInfos, dataSize);
+
+ if (hasErasure) { //
+ if (auto codecCpu = State_->Configuration->ErasureCodecCpuForDq.Get(cluster)) {
+ dataSizePerJob = Max(ui64(dataSizePerJob / *codecCpu), 10_KB);
+ const ui64 parts = (dataSize + dataSizePerJob - 1) / dataSizePerJob;
+ if (parts > maxTasksPerStage) {
+ AddErrorWrap(ctx, node.Pos(), "too big table with erasure codec");
+ return Nothing();
+ }
+ }
+ }
+
+ if (auto maxChunks = State_->Configuration->MaxChunksForDqRead.Get().GetOrElse(DEFAULT_MAX_CHUNKS_FOR_DQ_READ); chunksCount > maxChunks) {
+ AddErrorWrap(ctx, node.Pos(), "table with too many chunks");
+ return Nothing();
+ }
+
+ return dataSize;
+ }
+ AddErrorWrap(ctx, node.Pos(), TStringBuilder() << "unsupported callable: " << node.Content());
+ return Nothing();
+ }
+
+ void AddErrorWrap(TExprContext& ctx, const NYql::TPositionHandle& where, const TString& cause) {
+ ctx.AddError(YqlIssue(ctx.GetPosition(where), TIssuesIds::DQ_OPTIMIZE_ERROR, TStringBuilder() << "DQ cannot execute the query. Cause: " << cause));
+ }
+
+ TExprNode::TPtr WrapRead(const TDqSettings&, const TExprNode::TPtr& read, TExprContext& ctx) override {
+ if (auto maybeYtReadTable = TMaybeNode<TYtReadTable>(read)) {
+ TMaybeNode<TCoSecureParam> secParams;
+ if (State_->Configuration->Auth.Get().GetOrElse(TString())) {
+ const auto cluster = maybeYtReadTable.Cast().DataSource().Cluster();
+ secParams = Build<TCoSecureParam>(ctx, read->Pos()).Name().Build(TString("cluster:default_").append(cluster)).Done();
+ }
+ return Build<TDqReadWrap>(ctx, read->Pos())
+ .Input(maybeYtReadTable.Cast())
+ .Flags().Build()
+ .Token(secParams)
+ .Done().Ptr();
+ }
+ return read;
+ }
+
+ TMaybe<bool> CanWrite(const TDqSettings&, const TExprNode& node, TExprContext& ctx) override {
+ if (auto maybeWrite = TMaybeNode<TYtWriteTable>(&node)) {
+ auto cluster = TString{maybeWrite.Cast().DataSink().Cluster().Value()};
+ auto tableName = TString{TYtTableInfo::GetTableLabel(maybeWrite.Cast().Table())};
+ auto epoch = TEpochInfo::Parse(maybeWrite.Cast().Table().CommitEpoch().Ref());
+
+ auto tableDesc = State_->TablesData->GetTable(cluster, tableName, epoch);
+
+ if (!State_->Configuration->_EnableDq.Get(cluster).GetOrElse(true)) {
+ AddInfo(ctx, TStringBuilder() << "disabled for cluster " << cluster, false);
+ return false;
+ }
+
+ if (!tableDesc.Meta) {
+ AddInfo(ctx, "write to table without meta", false);
+ return false;
+ }
+ if (tableDesc.Meta->IsDynamic) {
+ AddInfo(ctx, "write to dynamic table", false);
+ return false;
+ }
+
+ const auto content = maybeWrite.Cast().Content().Raw();
+ if (const auto sorted = content->GetConstraint<TSortedConstraintNode>()) {
+ if (const auto distinct = content->GetConstraint<TDistinctConstraintNode>()) {
+ if (distinct->IsOrderBy(*sorted)) {
+ AddInfo(ctx, "unsupported write of unique data", false);
+ return false;
+ }
+ }
+ if (!content->IsCallable({"Sort", "TopSort", "AssumeSorted"})) {
+ AddInfo(ctx, "unsupported write of sorted data", false);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return Nothing();
+ }
+
+ void RegisterMkqlCompiler(NCommon::TMkqlCallableCompilerBase& compiler) override {
+ RegisterDqYtMkqlCompilers(compiler, State_);
+ State_->Gateway->RegisterMkqlCompiler(compiler);
+ }
+
+ bool CanFallback() override {
+ return true;
+ }
+
+ void Annotate(const TExprNode& node, THashMap<TString, TString>& params) override {
+ if (!node.IsCallable("YtDqWideWrite")) {
+ return;
+ }
+
+ YQL_ENSURE(!params.contains("yt.write"), "Duplicate 'yt.write' graph parameter");
+
+ TString server;
+ TString tx;
+ TString token;
+
+ for (const auto& setting: node.Child(1)->Children()) {
+ if (setting->ChildrenSize() != 2) {
+ continue;
+ }
+
+ if (setting->Child(0)->IsAtom("server")) {
+ server = setting->Child(1)->Content();
+ } else if (setting->Child(0)->IsAtom("tx")) {
+ tx = setting->Child(1)->Content();
+ } else if (setting->Child(0)->IsAtom("secureParams")) {
+ if (setting->ChildrenSize() > 1) {
+ TCoSecureParam secure(setting->Child(1));
+ token = secure.Name().StringValue();
+ }
+ }
+ }
+ YQL_ENSURE(server, "YtDqWideWrite: server parameter is expected");
+ YQL_ENSURE(tx, "YtDqWideWrite: tx parameter is expected");
+
+ auto param = NYT::NodeToYsonString(NYT::TNode()("root_tx", tx)("server", server)("token", token));
+ params["yt.write"] = param;
+ YQL_CLOG(INFO, ProviderYt) << "DQ annotate: adding yt.write=" << param;
+ }
+
+ bool PrepareFullResultTableParams(const TExprNode& root, TExprContext& ctx, THashMap<TString, TString>& params, THashMap<TString, TString>& secureParams) override {
+ const auto resOrPull = TResOrPullBase(&root);
+
+ if (FromString<bool>(resOrPull.Discard().Value())) {
+ return false;
+ }
+
+ auto input = resOrPull.Input().Ptr();
+ std::set<std::string_view> usedClusters;
+ VisitExpr(*input, [&usedClusters](const TExprNode& node) {
+ if (auto ds = TMaybeNode<TYtDSource>(&node)) {
+ usedClusters.insert(ds.Cast().Cluster().Value());
+ return false;
+ }
+ if (auto ds = TMaybeNode<TYtDSink>(&node)) {
+ usedClusters.insert(ds.Cast().Cluster().Value());
+ return false;
+ }
+ return true;
+ });
+ TString cluster;
+ if (usedClusters.empty()) {
+ cluster = State_->Configuration->DefaultCluster.Get().GetOrElse(State_->Gateway->GetDefaultClusterName());
+ } else {
+ cluster = TString{*usedClusters.begin()};
+ }
+
+ const auto type = GetSequenceItemType(input->Pos(), input->GetTypeAnn(), false, ctx);
+ YQL_ENSURE(type);
+ TYtOutTableInfo outTableInfo(type->Cast<TStructExprType>(), State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+
+ const auto res = State_->Gateway->PrepareFullResultTable(
+ IYtGateway::TFullResultTableOptions(State_->SessionId)
+ .Cluster(cluster)
+ .Config(State_->Configuration->GetSettingsForNode(resOrPull.Origin().Ref()))
+ .OutTable(outTableInfo)
+ );
+
+ auto param = NYT::TNode()("cluster", cluster)("server", res.Server)("path", res.Path)("refName", res.RefName)("codecSpec", res.CodecSpec)("tableAttrs", res.TableAttrs);
+ if (res.RootTransactionId) {
+ param("root_tx", *res.RootTransactionId);
+ if (res.ExternalTransactionId) {
+ param("external_tx", *res.ExternalTransactionId);
+ }
+ } else if (auto externalTx = State_->Configuration->ExternalTx.Get().GetOrElse(TGUID())) {
+ param("external_tx", GetGuidAsString(externalTx));
+ }
+ TString tokenName;
+ if (auto auth = State_->Configuration->Auth.Get().GetOrElse(TString())) {
+ tokenName = TString("cluster:default_").append(cluster);
+ if (!secureParams.contains(tokenName)) {
+ secureParams[tokenName] = auth;
+ }
+ }
+ param("token", tokenName);
+
+ const auto strParam = NYT::NodeToYsonString(param);
+ params["yt.full_result_table"] = strParam;
+ YQL_CLOG(INFO, ProviderYt) << "DQ prepare full result table params: adding yt.full_result_table=" << strParam;
+ return true;
+ }
+
+ void WriteFullResultTableRef(NYson::TYsonWriter& writer, const TVector<TString>& columns, const THashMap<TString, TString>& graphParams) override {
+ auto p = graphParams.FindPtr("yt.full_result_table");
+ YQL_ENSURE(p, "Expected 'yt.full_result_table' parameter");
+ auto param = NYT::NodeFromYsonString(*p);
+ const auto cluster = param["cluster"];
+ YQL_ENSURE(cluster.IsString(), "Expected 'cluster' sub-parameter");
+ const auto refName = param["refName"];
+ YQL_ENSURE(refName.IsString(), "Expected 'refName' sub-parameter");
+ NYql::WriteTableReference(writer, YtProviderName, cluster.AsString(), refName.AsString(), true, columns);
+ }
+
+private:
+ TYtState* State_;
+};
+
+THolder<IDqIntegration> CreateYtDqIntegration(TYtState* state) {
+ Y_VERIFY(state);
+ return MakeHolder<TYtDqIntegration>(state);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.h b/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.h
new file mode 100644
index 0000000000..e4cfac0983
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_dq_integration.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+
+#include <ydb/library/yql/dq/integration/yql_dq_integration.h>
+
+#include <util/generic/ptr.h>
+
+namespace NYql {
+
+THolder<IDqIntegration> CreateYtDqIntegration(TYtState* state);
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
new file mode 100644
index 0000000000..ae4693445d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_epoch.cpp
@@ -0,0 +1,467 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_table.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder_old.h>
+#include <ydb/library/yql/providers/yt/lib/graph_reorder/yql_graph_reorder.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/strbuf.h>
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtEpochTransformer : public TSyncTransformerBase {
+public:
+ TYtEpochTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+ }
+
+ void Rewind() final {
+ State_->NextEpochId = 1;
+ }
+
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+ if (ctx.Step.IsDone(TExprStep::Epochs)) {
+ return TStatus::Ok;
+ }
+
+ TNodeMap<ui32> commitEpochs;
+ TStatus status = AssignCommitEpochs(input, output, ctx, commitEpochs);
+ if (status.Level == TStatus::Error) {
+ return status;
+ }
+
+ TNodeMap<THashSet<TString>> tableWritesBeforeCommit;
+ status = status.Combine(AssignWriteEpochs(output, output, ctx, commitEpochs, tableWritesBeforeCommit));
+ if (status.Level == TStatus::Error) {
+ return status;
+ }
+
+ status = status.Combine(AssignUseEpochs(output, output, ctx, commitEpochs, tableWritesBeforeCommit));
+ if (status.Level == TStatus::Error) {
+ return status;
+ }
+
+ if (State_->Configuration->_EnableWriteReorder.Get().GetOrElse(false)) {
+ return status.Combine(TYtDependencyUpdater().ReorderGraph(output, output, ctx));
+ } else {
+ return status.Combine(TYtDependencyUpdaterOld().ReorderGraph(output, output, ctx));
+ }
+ }
+
+private:
+ class TYtDependencyUpdater: public TDependencyUpdater {
+ public:
+ TYtDependencyUpdater()
+ : TDependencyUpdater(YtProviderName, TYtConfigure::CallableName())
+ {
+ }
+
+ TMaybe<ui32> GetReadEpoch(const TExprNode::TPtr& readNode) const final {
+ TYtRead read(readNode);
+ TMaybe<ui32> maxEpoch;
+ if (auto list = read.Arg(2).Maybe<TExprList>()) {
+ for (auto item: list.Cast()) {
+ TMaybeNode<TYtTable> table = item.Maybe<TYtPath>().Table().Maybe<TYtTable>();
+ if (!table) {
+ table = item.Maybe<TYtTable>();
+ }
+ if (table) {
+ maxEpoch = Max(maxEpoch.GetOrElse(0), TEpochInfo::Parse(table.Cast().Epoch().Ref()).GetOrElse(0));
+ }
+ }
+ }
+ return maxEpoch;
+ }
+
+ TString GetWriteTarget(const TExprNode::TPtr& node) const final {
+ TYtWrite write(node);
+ return TStringBuilder() << "yt;" << write.DataSink().Cluster().Value() << ';' << TYtTableInfo(write.Arg(2)).Name;
+ }
+ };
+
+ class TYtDependencyUpdaterOld: public TDependencyUpdaterOld {
+ public:
+ TYtDependencyUpdaterOld()
+ : TDependencyUpdaterOld(YtProviderName, TYtConfigure::CallableName())
+ {
+ }
+
+ TMaybe<ui32> GetReadEpoch(const TExprNode::TPtr& readNode, TExprContext& /*ctx*/) const final {
+ TYtRead read(readNode);
+ TMaybe<ui32> maxEpoch;
+ if (auto list = read.Arg(2).Maybe<TExprList>()) {
+ for (auto item: list.Cast()) {
+ TMaybeNode<TYtTable> table = item.Maybe<TYtPath>().Table().Maybe<TYtTable>();
+ if (!table) {
+ table = item.Maybe<TYtTable>();
+ }
+ if (table) {
+ maxEpoch = Max(maxEpoch.GetOrElse(0), TEpochInfo::Parse(table.Cast().Epoch().Ref()).GetOrElse(0));
+ }
+ }
+ }
+ return maxEpoch;
+ }
+ };
+
+ TStatus AssignCommitEpochs(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx,
+ TNodeMap<ui32>& commitEpochs)
+ {
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ // Assign uniq epoch for each commit
+ if (auto maybeCommit = TMaybeNode<TCoCommit>(node)) {
+ auto commit = maybeCommit.Cast();
+
+ if (!commit.DataSink().Maybe<TYtDSink>()) {
+ return node;
+ }
+
+ auto settings = NCommon::ParseCommitSettings(commit, ctx);
+ if (settings.Epoch) {
+ commitEpochs[node.Get()] = FromString<ui32>(settings.Epoch.Cast().Value());
+ return node;
+ }
+
+ const ui32 commitEpoch = State_->NextEpochId++;
+ settings.Epoch = Build<TCoAtom>(ctx, commit.Pos())
+ .Value(ToString(commitEpoch))
+ .Done();
+
+ auto ret = Build<TCoCommit>(ctx, commit.Pos())
+ .World(commit.World())
+ .DataSink(commit.DataSink())
+ .Settings(settings.BuildNode(ctx))
+ .Done();
+
+ commitEpochs[ret.Raw()] = commitEpoch;
+
+ return ret.Ptr();
+ }
+ return node;
+ }, ctx, settings);
+
+ if (input != output) {
+ YQL_CLOG(INFO, ProviderYt) << "Epoch-AssignCommitEpochs";
+ }
+
+ return status;
+ }
+
+ TVector<TYtWrite> FindWritesBeforeCommit(const TCoCommit& commit) const {
+ auto cluster = commit.DataSink().Cast<TYtDSink>().Cluster().Value();
+
+ TVector<TYtWrite> writes;
+ VisitExprByFirst(commit.Ptr(), [&](const TExprNode::TPtr& node) {
+ if (auto maybeWrite = TMaybeNode<TYtWrite>(node)) {
+ if (auto ds = maybeWrite.DataSink()) { // Validate provider
+ if (ds.Cast().Cluster().Value() == cluster) {
+ writes.push_back(maybeWrite.Cast());
+ }
+ }
+ } else if (auto ds = TMaybeNode<TCoCommit>(node).DataSink().Maybe<TYtDSink>()) {
+ if (ds.Cast().Cluster().Value() == cluster && commit.Ptr().Get() != node.Get()) {
+ return false; // Stop traversing
+ }
+ }
+ return true;
+ });
+
+ return writes;
+ }
+
+ TStatus AssignWriteEpochs(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx,
+ const TNodeMap<ui32>& commitEpochs, TNodeMap<THashSet<TString>>& tableWritesBeforeCommit)
+ {
+ if (commitEpochs.empty()) {
+ return TStatus::Ok;
+ }
+
+ TExprNode::TListType commits;
+ VisitExprByFirst(input, [&](const TExprNode::TPtr& node) {
+ if (auto ds = TMaybeNode<TCoCommit>(node).DataSink().Maybe<TYtDSink>()) {
+ commits.push_back(node);
+ }
+ return true;
+ });
+
+ TNodeMap<ui32> writeCommitEpochs;
+ // Find all writes before commits
+ for (auto& commitNode: commits) {
+ auto commit = TCoCommit(commitNode);
+ auto it = commitEpochs.find(commitNode.Get());
+ YQL_ENSURE(it != commitEpochs.end());
+ const ui32 commitEpoch = it->second;
+
+ TVector<TYtWrite> writes = FindWritesBeforeCommit(commit);
+ THashMap<TString, TVector<TExprNode::TPtr>> writesByTable;
+ THashSet<TString> tableNames;
+ for (auto write: writes) {
+ TYtTableInfo tableInfo(write.Arg(2));
+ if (!tableInfo.CommitEpoch.Defined()) {
+ writeCommitEpochs[write.Raw()] = commitEpoch;
+ } else if (*tableInfo.CommitEpoch != commitEpoch) {
+ ctx.AddError(TIssue(ctx.GetPosition(write.Pos()), TStringBuilder()
+ << "Table " << tableInfo.Name.Quote() << " write belongs to multiple commits with different epochs"));
+ return TStatus::Error;
+ }
+ tableNames.insert(tableInfo.Name);
+
+ // Graph is traversed from the end, so store the last write for each table, which is
+ // actually executed first
+ writesByTable[tableInfo.Name].push_back(write.Ptr());
+ }
+ for (auto& w: writesByTable) {
+ if (auto cnt = std::count_if(w.second.begin(), w.second.end(), [](const auto& p) { return NYql::HasSetting(*p->Child(4), EYtSettingType::Initial); })) {
+ YQL_ENSURE(cnt == 1, "Multiple 'initial' writes to the same table " << w.first);
+ continue;
+ }
+ if (w.second.size() > 1) {
+ std::stable_sort(w.second.begin(), w.second.end(), [](const auto& p1, const auto& p2) { return p1->UniqueId() < p2->UniqueId(); });
+ }
+ w.second.front()->ChildRef(4) = NYql::AddSetting(*w.second.front()->Child(4), EYtSettingType::Initial, {}, ctx);
+ }
+ if (!tableNames.empty()) {
+ tableWritesBeforeCommit.emplace(commitNode.Get(), std::move(tableNames));
+ }
+ }
+
+ if (writeCommitEpochs.empty()) {
+ return TStatus::Ok;
+ }
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (node->IsCallable(TYtWrite::CallableName())) {
+ const auto it = writeCommitEpochs.find(node.Get());
+ if (writeCommitEpochs.cend() != it) {
+ TYtTableInfo tableInfo = node->ChildPtr(2);
+ tableInfo.CommitEpoch = it->second;
+ node->ChildRef(2) = tableInfo.ToExprNode(ctx, node->Pos()).Ptr();
+ }
+ }
+ return node;
+ }, ctx, settings);
+
+ YQL_CLOG(INFO, ProviderYt) << "Epoch-AssignWriteEpochs";
+ return status;
+ }
+
+ TVector<const TExprNode*> FindCommitsBeforeNode(const TExprNode& startNode, TStringBuf cluster,
+ const TNodeMap<THashSet<TString>>& tableWritesBeforeCommit)
+ {
+ TVector<const TExprNode*> commits;
+ VisitExprByFirst(startNode, [&](const TExprNode& node) {
+ if (auto ds = TMaybeNode<TCoCommit>(&node).DataSink().Maybe<TYtDSink>()) {
+ if (tableWritesBeforeCommit.find(&node) != tableWritesBeforeCommit.end() && ds.Cast().Cluster().Value() == cluster) {
+ commits.push_back(&node);
+ }
+ }
+ return true;
+ });
+ return commits;
+ }
+
+ TStatus AssignUseEpochs(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx,
+ const TNodeMap<ui32>& commitEpochs, const TNodeMap<THashSet<TString>>& tableWritesBeforeCommit)
+ {
+ TNodeMap<TNodeMap<ui32>> ioEpochs;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto ds = TMaybeNode<TYtRead>(node).DataSource()) {
+ if (tableWritesBeforeCommit.empty()) {
+ return true;
+ }
+ auto clusterName = ds.Cast().Cluster().Value();
+ auto commitDeps = FindCommitsBeforeNode(*node, clusterName, tableWritesBeforeCommit);
+ if (commitDeps.empty()) {
+ return true;
+ }
+
+ TYtRead read(node);
+ if (auto list = read.Arg(2).Maybe<TExprList>()) {
+ THashMap<TString, ui32> tableEpochs;
+ for (auto item: list.Cast()) {
+ TMaybeNode<TYtTable> tableNode = item.Maybe<TYtPath>().Table().Maybe<TYtTable>();
+ if (!tableNode) {
+ tableNode = item.Maybe<TYtTable>();
+ }
+
+ if (tableNode) {
+ TYtTableInfo tableInfo(tableNode.Cast());
+ if (!tableInfo.Epoch.Defined()) {
+ TMaybe<ui32> epoch;
+ if (auto p = tableEpochs.FindPtr(tableInfo.Name)) {
+ epoch = *p;
+ }
+ else {
+ for (auto commit: commitDeps) {
+ auto itWrites = tableWritesBeforeCommit.find(commit);
+ if (itWrites != tableWritesBeforeCommit.end() && itWrites->second.contains(tableInfo.Name)) {
+ auto itCommit = commitEpochs.find(commit);
+ YQL_ENSURE(itCommit != commitEpochs.end());
+ epoch = Max<ui32>(itCommit->second, epoch.GetOrElse(0));
+ }
+ }
+ }
+ tableEpochs[tableInfo.Name] = epoch.GetOrElse(0);
+
+ if (0 != epoch.GetOrElse(0)) {
+ ioEpochs[node.Get()][tableNode.Raw()] = *epoch;
+ State_->EpochDependencies[*epoch].emplace(clusterName, tableInfo.Name);
+ }
+ }
+ }
+ }
+ }
+ }
+ else if (auto ds = TMaybeNode<TYtWrite>(node).DataSink()) {
+ if (tableWritesBeforeCommit.empty()) {
+ return true;
+ }
+ auto clusterName = ds.Cast().Cluster().Value();
+ auto commitDeps = FindCommitsBeforeNode(*node, clusterName, tableWritesBeforeCommit);
+ if (commitDeps.empty()) {
+ return true;
+ }
+
+ TYtWrite write(node);
+ if (write.Arg(2).Maybe<TYtTable>()) {
+ TYtTableInfo tableInfo(write.Arg(2));
+ if (!tableInfo.Epoch.Defined()) {
+
+ TMaybe<ui32> epoch;
+ for (auto commit: commitDeps) {
+ auto itWrites = tableWritesBeforeCommit.find(commit);
+ if (itWrites != tableWritesBeforeCommit.end() && itWrites->second.contains(tableInfo.Name)) {
+ auto itCommit = commitEpochs.find(commit);
+ YQL_ENSURE(itCommit != commitEpochs.end());
+ epoch = Max<ui32>(itCommit->second, epoch.GetOrElse(0));
+ }
+ }
+
+ if (0 != epoch.GetOrElse(0)) {
+ ioEpochs[node.Get()][write.Arg(2).Raw()] = *epoch;
+ State_->EpochDependencies[*epoch].emplace(clusterName, tableInfo.Name);
+ }
+ }
+ }
+ }
+ else if (auto maybeRead = TMaybeNode<TYtReadTable>(node)) {
+ auto cluster = TString{maybeRead.Cast().DataSource().Cluster().Value()};
+ for (auto section: maybeRead.Cast().Input()) {
+ for (auto path: section.Paths()) {
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Cast().Name().Value());
+ }
+ }
+ }
+ }
+ }
+ else if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(node)) {
+ auto cluster = TString{maybeOp.Cast().DataSink().Cluster().Value()};
+ for (auto section: maybeOp.Cast().Input()) {
+ for (auto path: section.Paths()) {
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Cast().Name().Value());
+ }
+ }
+ }
+ }
+ }
+ else if (auto maybePublish = TMaybeNode<TYtPublish>(node)) {
+ auto cluster = TString{maybePublish.Cast().DataSink().Cluster().Value()};
+ auto table = maybePublish.Cast().Publish();
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Name().Value());
+ }
+ }
+ else if (auto maybeDrop = TMaybeNode<TYtDropTable>(node)) {
+ auto cluster = TString{maybeDrop.Cast().DataSink().Cluster().Value()};
+ auto table = maybeDrop.Cast().Table();
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Name().Value());
+ }
+ }
+ else if (auto maybeScheme = TMaybeNode<TYtReadTableScheme>(node)) {
+ auto cluster = TString{maybeScheme.Cast().DataSource().Cluster().Value()};
+ auto table = maybeScheme.Cast().Table();
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Name().Value());
+ }
+ }
+ else if (auto maybeWrite = TMaybeNode<TYtWriteTable>(node)) {
+ auto cluster = TString{maybeWrite.Cast().DataSink().Cluster().Value()};
+ auto table = maybeWrite.Cast().Table();
+ if (auto epoch = TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0)) {
+ State_->EpochDependencies[epoch].emplace(cluster, table.Name().Value());
+ }
+ }
+ return true;
+ });
+
+ if (ioEpochs.empty()) {
+ return TStatus::Ok;
+ }
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (TYtRead::Match(node.Get()) || TYtWrite::Match(node.Get())) {
+ const auto it = ioEpochs.find(node.Get());
+ if (ioEpochs.cend() != it) {
+ auto tables = node->ChildPtr(2);
+ auto& tableEpochs = it->second;
+ TOptimizeExprSettings subSettings(nullptr);
+ subSettings.VisitChanges = true;
+ auto subStatus = OptimizeExpr(tables, tables, [&tableEpochs](const TExprNode::TPtr& subNode, TExprContext& ctx) -> TExprNode::TPtr {
+ if (TYtTable::Match(subNode.Get())) {
+ auto tableIt = tableEpochs.find(subNode.Get());
+ if (tableEpochs.cend() != tableIt) {
+ TYtTableInfo tableInfo = subNode;
+ tableInfo.Epoch = tableIt->second;
+ return tableInfo.ToExprNode(ctx, subNode->Pos()).Ptr();
+ }
+ }
+ return subNode;
+ }, ctx, subSettings);
+ if (subStatus.Level != TStatus::Ok) {
+ return {};
+ }
+
+ ioEpochs.erase(it);
+ node->ChildRef(2) = std::move(tables);
+ }
+ }
+
+ return node;
+ }, ctx, settings);
+
+ YQL_CLOG(INFO, ProviderYt) << "Epoch-AssignUseEpochs";
+ return status;
+ }
+
+private:
+ TYtState::TPtr State_;
+};
+
+THolder<IGraphTransformer> CreateYtEpochTransformer(TYtState::TPtr state) {
+ return THolder(new TYtEpochTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
new file mode 100644
index 0000000000..fc217a7bb7
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_gateway.cpp
@@ -0,0 +1,4 @@
+#include "yql_yt_gateway.h"
+
+namespace NYql {
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_gateway.h b/ydb/library/yql/providers/yt/provider/yql_yt_gateway.h
new file mode 100644
index 0000000000..18fabc03f2
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_gateway.h
@@ -0,0 +1,559 @@
+#pragma once
+
+#include "yql_yt_table.h"
+#include "yql_yt_table_desc.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/providers/stat/uploader/yql_stat_uploader.h>
+
+#include <ydb/library/yql/providers/common/gateway/yql_provider_gateway.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_data_provider.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/core/yql_execution.h>
+#include <ydb/library/yql/core/file_storage/storage.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <library/cpp/time_provider/time_provider.h>
+#include <library/cpp/random_provider/random_provider.h>
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/public.h>
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/typetraits.h>
+#include <util/generic/string.h>
+#include <util/generic/hash.h>
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+
+#include <utility>
+
+namespace NYql {
+
+namespace NCommon {
+ class TMkqlCallableCompilerBase;
+}
+
+class IYtGateway : public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<IYtGateway>;
+
+#define OPTION_FIELD_METHODS(type, name) \
+ public: \
+ TSelf& name(TTypeTraits<type>::TFuncParam arg##name)& { \
+ name##_ = arg##name; \
+ return *this; \
+ } \
+ TSelf&& name(TTypeTraits<type>::TFuncParam arg##name)&& { \
+ name##_ = arg##name; \
+ return std::move(*this); \
+ } \
+ TTypeTraits<type>::TFuncParam name() const { \
+ return name##_; \
+ } \
+ type& name() { \
+ return name##_; \
+ }
+
+#define OPTION_FIELD(type, name) \
+ private: \
+ type name##_; \
+ OPTION_FIELD_METHODS(type, name)
+
+#define OPTION_FIELD_DEFAULT(type, name, def) \
+ private: \
+ type name##_ = def; \
+ OPTION_FIELD_METHODS(type, name)
+
+
+ //////////////////////////////////////////////////////////////
+
+ using TSecureParams = THashMap<TString, TString>;
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCommonOptions {
+ TString SessionId_;
+
+ TCommonOptions(const TString& sessionId)
+ : SessionId_(sessionId)
+ {
+ }
+
+ const TString& SessionId() const {
+ return SessionId_;
+ }
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TOpenSessionOptions : public TCommonOptions {
+ using TSelf = TOpenSessionOptions;
+
+ TOpenSessionOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, UserName)
+ OPTION_FIELD(TOperationProgressWriter, ProgressWriter)
+ OPTION_FIELD(TYqlOperationOptions, OperationOptions)
+ OPTION_FIELD(TIntrusivePtr<IRandomProvider>, RandomProvider)
+ OPTION_FIELD(TIntrusivePtr<ITimeProvider>, TimeProvider)
+ OPTION_FIELD(TStatWriter, StatWriter)
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCloseSessionOptions : public TCommonOptions {
+ using TSelf = TCloseSessionOptions;
+
+ TCloseSessionOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCleanupSessionOptions : public TCommonOptions {
+ using TSelf = TCleanupSessionOptions;
+
+ TCleanupSessionOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TFinalizeOptions : public TCommonOptions {
+ using TSelf = TFinalizeOptions;
+
+ TFinalizeOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD_DEFAULT(bool, Abort, false)
+ };
+
+ struct TFinalizeResult : public NCommon::TOperationResult {
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCanonizeReq {
+ using TSelf = TCanonizeReq;
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TString, Path)
+ OPTION_FIELD(TPosition, Pos)
+ };
+
+ struct TCanonizePathsOptions : public TCommonOptions {
+ using TSelf = TCanonizePathsOptions;
+
+ TCanonizePathsOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TVector<TCanonizeReq>, Paths)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ };
+
+ struct TCanonizedPath {
+ TString Path;
+ TMaybe<TVector<TString>> Columns;
+ TMaybe<TVector<NYT::TReadRange>> Ranges;
+ };
+
+ struct TCanonizePathsResult: public NCommon::TOperationResult {
+ TVector<TCanonizedPath> Data;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TTableReq {
+ using TSelf = TTableReq;
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TString, Table)
+ OPTION_FIELD_DEFAULT(bool, LockOnly, false)
+ OPTION_FIELD(TYtTableIntents, Intents)
+ OPTION_FIELD_DEFAULT(ui32, InferSchemaRows, 0)
+ OPTION_FIELD_DEFAULT(bool, ForceInferSchema, false)
+ OPTION_FIELD_DEFAULT(bool, Anonymous, false)
+ OPTION_FIELD_DEFAULT(bool, IgnoreYamrDsv, false)
+ OPTION_FIELD_DEFAULT(bool, IgnoreWeakSchema, false)
+ };
+
+ struct TGetTableInfoOptions : public TCommonOptions {
+ using TSelf = TGetTableInfoOptions;
+
+ TGetTableInfoOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TVector<TTableReq>, Tables)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD_DEFAULT(bool, ReadOnly, false)
+ OPTION_FIELD_DEFAULT(ui32, Epoch, 0)
+ };
+
+ struct TTableInfoResult: public NCommon::TOperationResult {
+ struct TTableData {
+ TYtTableMetaInfo::TPtr Meta;
+ TYtTableStatInfo::TPtr Stat;
+ bool WriteLock = false;
+ };
+ TVector<TTableData> Data;
+ };
+
+ struct TTableStatResult: public NCommon::TOperationResult {
+ TYtTableStatInfo::TPtr Data;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TTableRangeOptions : public TCommonOptions {
+ using TSelf = TTableRangeOptions;
+
+ TTableRangeOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TString, Prefix)
+ OPTION_FIELD_DEFAULT(const TExprNode*, Filter, nullptr)
+ OPTION_FIELD_DEFAULT(TExprContext*, ExprCtx, nullptr)
+ OPTION_FIELD(TString, Suffix)
+ OPTION_FIELD(TUserDataTable, UserDataBlocks)
+ OPTION_FIELD(TUdfModulesTable, UdfModules)
+ OPTION_FIELD(IUdfResolver::TPtr, UdfResolver)
+ OPTION_FIELD_DEFAULT(NKikimr::NUdf::EValidateMode, UdfValidateMode, NKikimr::NUdf::EValidateMode::None)
+ OPTION_FIELD(TMaybe<ui32>, PublicId)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, OptLLVM)
+ OPTION_FIELD(TString, OperationHash)
+ OPTION_FIELD(TPosition, Pos)
+ OPTION_FIELD(TSecureParams, SecureParams)
+ };
+
+ struct TTableRangeResult : public NCommon::TOperationResult {
+ TVector<TCanonizedPath> Tables;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TFolderOptions : public TCommonOptions {
+ using TSelf = TFolderOptions;
+
+ TFolderOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TString, Prefix)
+ OPTION_FIELD(TVector<TString>, Attributes)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TPosition, Pos)
+ };
+
+ struct TFolderResult : public NCommon::TOperationResult {
+ struct TFolderItem {
+ TString Path;
+ TString Type;
+ TString Attributes;
+ };
+ TVector<TFolderItem> Items;
+ TFileLinkPtr File;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TResOrPullOptions : public TCommonOptions {
+ using TSelf = TResOrPullOptions;
+
+ TResOrPullOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TUserDataTable, UserDataBlocks)
+ OPTION_FIELD(TUdfModulesTable, UdfModules)
+ OPTION_FIELD(IUdfResolver::TPtr, UdfResolver)
+ OPTION_FIELD_DEFAULT(NKikimr::NUdf::EValidateMode, UdfValidateMode, NKikimr::NUdf::EValidateMode::None)
+ OPTION_FIELD(IDataProvider::TFillSettings, FillSettings)
+ OPTION_FIELD(TMaybe<ui32>, PublicId)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, UsedCluster)
+ OPTION_FIELD(TString, OptLLVM)
+ OPTION_FIELD(TString, OperationHash)
+ OPTION_FIELD(TSecureParams, SecureParams)
+ };
+
+ struct TResOrPullResult : public NCommon::TOperationResult {
+ TString Data;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TRunOptions : public TCommonOptions {
+ using TSelf = TRunOptions;
+
+ TRunOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TUserDataTable, UserDataBlocks)
+ OPTION_FIELD(TUdfModulesTable, UdfModules)
+ OPTION_FIELD(IUdfResolver::TPtr, UdfResolver)
+ OPTION_FIELD_DEFAULT(NKikimr::NUdf::EValidateMode, UdfValidateMode, NKikimr::NUdf::EValidateMode::None)
+ OPTION_FIELD(TMaybe<ui32>, PublicId);
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, OptLLVM)
+ OPTION_FIELD(TString, OperationHash)
+ OPTION_FIELD(TSecureParams, SecureParams)
+ };
+
+ struct TRunResult : public NCommon::TOperationResult {
+ // Return pair of table name, table stat for each output table
+ TVector<std::pair<TString, TYtTableStatInfo::TPtr>> OutTableStats;
+ };
+
+
+ //////////////////////////////////////////////////////////////
+
+ struct TPrepareOptions : public TCommonOptions {
+ using TSelf = TPrepareOptions;
+
+ TPrepareOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TMaybe<ui32>, PublicId)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, OperationHash)
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCalcOptions : public TCommonOptions {
+ using TSelf = TCalcOptions;
+
+ TCalcOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TUserDataTable, UserDataBlocks)
+ OPTION_FIELD(TUdfModulesTable, UdfModules)
+ OPTION_FIELD(IUdfResolver::TPtr, UdfResolver)
+ OPTION_FIELD_DEFAULT(NKikimr::NUdf::EValidateMode, UdfValidateMode, NKikimr::NUdf::EValidateMode::None)
+ OPTION_FIELD(TMaybe<ui32>, PublicId);
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, OptLLVM)
+ OPTION_FIELD(TString, OperationHash)
+ OPTION_FIELD(TSecureParams, SecureParams)
+ };
+
+ struct TCalcResult : public NCommon::TOperationResult {
+ TVector<NYT::TNode> Data;
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TPublishOptions : public TCommonOptions {
+ using TSelf = TPublishOptions;
+
+ TPublishOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TMaybe<ui32>, PublicId)
+ OPTION_FIELD(TYqlRowSpecInfo::TPtr, DestinationRowSpec)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TString, OptLLVM)
+ OPTION_FIELD(TString, OperationHash)
+ };
+
+ struct TPublishResult : public NCommon::TOperationResult {
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TCommitOptions : public TCommonOptions {
+ using TSelf = TCommitOptions;
+
+ TCommitOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ };
+
+ struct TCommitResult : public NCommon::TOperationResult {
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TDropTrackablesOptions : public TCommonOptions {
+ using TSelf = TDropTrackablesOptions;
+
+ struct TClusterAndPath
+ {
+ TString Cluster;
+ TString Path;
+ };
+
+ TDropTrackablesOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TVector<TClusterAndPath>, Pathes)
+ };
+
+ struct TDropTrackablesResult : public NCommon::TOperationResult {
+ };
+
+ //////////////////////////////////////////////////////////////
+
+ struct TPathStatReq {
+ using TSelf = TPathStatReq;
+
+ OPTION_FIELD(NYT::TRichYPath, Path)
+ OPTION_FIELD_DEFAULT(bool, IsTemp, false)
+ OPTION_FIELD_DEFAULT(bool, IsAnonymous, false)
+ OPTION_FIELD_DEFAULT(ui32, Epoch, 0)
+ };
+
+ struct TPathStatOptions : public TCommonOptions {
+ using TSelf = TPathStatOptions;
+
+ TPathStatOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TVector<TPathStatReq>, Paths)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ };
+
+ struct TPathStatResult: public NCommon::TOperationResult {
+ TVector<ui64> DataSize;
+ };
+
+ struct TFullResultTableOptions : public TCommonOptions {
+ using TSelf = TFullResultTableOptions;
+
+ TFullResultTableOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ OPTION_FIELD(TYtOutTableInfo, OutTable)
+ };
+
+ struct TFullResultTableResult: public NCommon::TOperationResult {
+ TMaybe<TString> RootTransactionId;
+ TMaybe<TString> ExternalTransactionId;
+ TString Server;
+ TString Path;
+ TString RefName;
+ TString CodecSpec;
+ TString TableAttrs;
+ };
+
+ struct TGetTablePartitionsOptions : public TCommonOptions {
+ using TSelf = TGetTablePartitionsOptions;
+
+ TGetTablePartitionsOptions(const TString& sessionId)
+ : TCommonOptions(sessionId)
+ {
+ }
+
+ OPTION_FIELD(TString, Cluster)
+ OPTION_FIELD(TVector<TYtPathInfo::TPtr>, Paths)
+ OPTION_FIELD_DEFAULT(size_t, DataSizePerJob, 0)
+ OPTION_FIELD_DEFAULT(size_t, MaxPartitions, 0)
+ OPTION_FIELD_DEFAULT(bool, AdjustDataWeightPerPartition, true)
+ OPTION_FIELD(TYtSettings::TConstPtr, Config)
+ };
+
+ struct TGetTablePartitionsResult: public NCommon::TOperationResult {
+ NYT::TMultiTablePartitions Partitions;
+ };
+
+public:
+ virtual ~IYtGateway() = default;
+
+ virtual void OpenSession(TOpenSessionOptions&& options) = 0;
+
+ virtual void CloseSession(TCloseSessionOptions&& options) = 0;
+
+ virtual void CleanupSession(TCleanupSessionOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TFinalizeResult> Finalize(TFinalizeOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TCanonizePathsResult> CanonizePaths(TCanonizePathsOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TTableInfoResult> GetTableInfo(TGetTableInfoOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TTableRangeResult> GetTableRange(TTableRangeOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TFolderResult> GetFolder(TFolderOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TResOrPullResult> ResOrPull(const TExprNode::TPtr& node, TExprContext& ctx, TResOrPullOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TRunResult> Run(const TExprNode::TPtr& node, TExprContext& ctx, TRunOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TRunResult> Prepare(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) const = 0;
+ virtual NThreading::TFuture<TRunResult> GetTableStat(const TExprNode::TPtr& node, TExprContext& ctx, TPrepareOptions&& options) = 0 ;
+
+ virtual NThreading::TFuture<TCalcResult> Calc(const TExprNode::TListType& nodes, TExprContext& ctx, TCalcOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TPublishResult> Publish(const TExprNode::TPtr& node, TExprContext& ctx, TPublishOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TCommitResult> Commit(TCommitOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TDropTrackablesResult> DropTrackables(TDropTrackablesOptions&& options) = 0;
+
+ virtual NThreading::TFuture<TPathStatResult> PathStat(TPathStatOptions&& options) = 0;
+ virtual TPathStatResult TryPathStat(TPathStatOptions&& options) = 0;
+
+ virtual bool TryParseYtUrl(const TString& url, TString* cluster, TString* path) const = 0;
+
+ virtual TString GetDefaultClusterName() const = 0;
+ virtual TString GetClusterServer(const TString& cluster) const = 0;
+ virtual NYT::TRichYPath GetRealTable(const TString& sessionId, const TString& cluster, const TString& table, ui32 epoch, const TString& tmpFolder) const = 0;
+ virtual NYT::TRichYPath GetWriteTable(const TString& sessionId, const TString& cluster, const TString& table, const TString& tmpFolder) const = 0;
+
+ virtual TFullResultTableResult PrepareFullResultTable(TFullResultTableOptions&& options) = 0;
+
+ virtual void SetStatUploader(IStatUploader::TPtr statUploader) = 0;
+
+ virtual void RegisterMkqlCompiler(NCommon::TMkqlCallableCompilerBase& compiler) = 0;
+
+ virtual TGetTablePartitionsResult GetTablePartitions(TGetTablePartitionsOptions&& options) = 0;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
new file mode 100644
index 0000000000..648cfae44e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_helpers.cpp
@@ -0,0 +1,2121 @@
+#include "yql_yt_helpers.h"
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_op_hash.h"
+
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/issue/protos/issue_id.pb.h>
+#include <ydb/library/yql/core/peephole_opt/yql_opt_peephole_physical.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_expr_constraint.h>
+#include <ydb/library/yql/core/yql_expr_csee.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/string/cast.h>
+#include <util/string/hex.h>
+#include <util/generic/xrange.h>
+#include <util/generic/utility.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/bitmap.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+bool IsYtIsolatedLambdaImpl(const TExprNode& lambdaBody, TSyncMap& syncList, TString* usedCluster, bool supportsReads, bool supportsDq, TNodeSet& visited) {
+ if (!visited.insert(&lambdaBody).second) {
+ return true;
+ }
+
+ if (TMaybeNode<TCoTypeOf>(&lambdaBody)) {
+ return true;
+ }
+
+ if (auto maybeLength = TMaybeNode<TYtLength>(&lambdaBody)) {
+ if (auto maybeRead = maybeLength.Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{read.DataSource().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(read.Ptr(), syncList.size());
+ }
+ if (auto maybeOutput = maybeLength.Input().Maybe<TYtOutput>()) {
+ auto output = maybeOutput.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{GetOutputOp(output).DataSink().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(output.Operation().Ptr(), syncList.size());
+ }
+ return true;
+ }
+
+ if (auto maybeContent = TMaybeNode<TYtTableContent>(&lambdaBody)) {
+ if (auto maybeRead = maybeContent.Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{read.DataSource().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(read.Ptr(), syncList.size());
+ }
+ if (auto maybeOutput = maybeContent.Input().Maybe<TYtOutput>()) {
+ auto output = maybeOutput.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{GetOutputOp(output).DataSink().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(output.Operation().Ptr(), syncList.size());
+ }
+ return true;
+ }
+
+ if (auto maybeContent = TMaybeNode<TDqReadWideWrap>(&lambdaBody)) {
+ if (!supportsDq) {
+ return false;
+ }
+ if (auto maybeRead = maybeContent.Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{read.DataSource().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(read.Ptr(), syncList.size());
+ }
+ if (auto maybeOutput = maybeContent.Input().Maybe<TYtOutput>()) {
+ auto output = maybeOutput.Cast();
+ if (supportsReads && usedCluster && !UpdateUsedCluster(*usedCluster, TString{GetOutputOp(output).DataSink().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(output.Operation().Ptr(), syncList.size());
+ }
+ return true;
+ }
+
+ if (!supportsDq && (TDqConnection::Match(&lambdaBody) || TDqPhyPrecompute::Match(&lambdaBody) || TDqStageBase::Match(&lambdaBody) || TDqSourceWrapBase::Match(&lambdaBody))) {
+ return false;
+ }
+
+ if (supportsReads) {
+ if (auto maybeRead = TMaybeNode<TCoRight>(&lambdaBody).Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (usedCluster && !UpdateUsedCluster(*usedCluster, TString{read.DataSource().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(read.Ptr(), syncList.size());
+ return true;
+ } else if (auto out = TMaybeNode<TYtOutput>(&lambdaBody)) {
+ auto op = GetOutputOp(out.Cast());
+ if (usedCluster && !UpdateUsedCluster(*usedCluster, TString{op.DataSink().Cluster().Value()})) {
+ return false;
+ }
+ syncList.emplace(op.Ptr(), syncList.size());
+ return true;
+ }
+ }
+
+ if (lambdaBody.IsCallable("WithWorld")) {
+ syncList.emplace(lambdaBody.ChildPtr(1), syncList.size());
+ return true;
+ }
+
+ if (!lambdaBody.GetTypeAnn()->IsComposable()) {
+ return false;
+ }
+
+ for (auto& child : lambdaBody.Children()) {
+ if (!IsYtIsolatedLambdaImpl(*child, syncList, usedCluster, supportsReads, supportsDq, visited)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+IGraphTransformer::TStatus EstimateDataSize(TVector<ui64>& result, TSet<TString>& requestedColumns,
+ const TString& cluster, const TVector<TYtPathInfo::TPtr>& paths,
+ const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx, bool sync)
+{
+ result.clear();
+ result.resize(paths.size(), 0);
+ requestedColumns.clear();
+
+ const bool useColumnarStat = GetJoinCollectColumnarStatisticsMode(*state.Configuration) != EJoinCollectColumnarStatisticsMode::Disable
+ && !state.Types->UseTableMetaFromGraph;
+
+ TVector<size_t> reqMap;
+ TVector<IYtGateway::TPathStatReq> pathStatReqs;
+ for (size_t i: xrange(paths.size())) {
+ const TYtPathInfo::TPtr& pathInfo = paths[i];
+ YQL_ENSURE(pathInfo->Table->Stat);
+ result[i] = pathInfo->Table->Stat->DataSize;
+ if (pathInfo->Ranges) {
+ if (auto usedRows = pathInfo->Ranges->GetUsedRows(pathInfo->Table->Stat->RecordsCount)) {
+ if (usedRows.GetRef() && pathInfo->Table->Stat->RecordsCount) {
+ result[i] *= double(usedRows.GetRef()) / double(pathInfo->Table->Stat->RecordsCount);
+ } else {
+ result[i] = 0;
+ }
+ }
+ }
+
+ if (useColumnarStat) {
+ TMaybe<TVector<TString>> overrideColumns;
+ if (columns && pathInfo->Table->RowSpec && (pathInfo->Table->RowSpec->StrictSchema || nullptr == FindPtr(*columns, YqlOthersColumnName))) {
+ overrideColumns = columns;
+ }
+
+ auto ytPath = BuildYtPathForStatRequest(cluster, *pathInfo, overrideColumns, state, ctx);
+ if (!ytPath) {
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ if (ytPath->Columns_) {
+ pathStatReqs.push_back(
+ IYtGateway::TPathStatReq()
+ .Path(*ytPath)
+ .IsTemp(pathInfo->Table->IsTemp)
+ .IsAnonymous(pathInfo->Table->IsAnonymous)
+ .Epoch(pathInfo->Table->Epoch.GetOrElse(0))
+ );
+ reqMap.push_back(i);
+ }
+ }
+ }
+
+ if (!pathStatReqs.empty()) {
+ for (auto& req : pathStatReqs) {
+ YQL_ENSURE(req.Path().Columns_);
+ requestedColumns.insert(req.Path().Columns_->Parts_.begin(), req.Path().Columns_->Parts_.end());
+ }
+
+ IYtGateway::TPathStatResult pathStats;
+ IYtGateway::TPathStatOptions pathStatOptions =
+ IYtGateway::TPathStatOptions(state.SessionId)
+ .Cluster(cluster)
+ .Paths(pathStatReqs)
+ .Config(state.Configuration->Snapshot());
+ if (sync) {
+ auto future = state.Gateway->PathStat(std::move(pathStatOptions));
+ pathStats = future.GetValueSync();
+ pathStats.ReportIssues(ctx.IssueManager);
+ if (!pathStats.Success()) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ } else {
+ pathStats = state.Gateway->TryPathStat(std::move(pathStatOptions));
+ if (!pathStats.Success()) {
+ return IGraphTransformer::TStatus::Repeat;
+ }
+ }
+
+ YQL_ENSURE(pathStats.DataSize.size() == reqMap.size());
+ for (size_t i: xrange(pathStats.DataSize.size())) {
+ result[reqMap[i]] = pathStats.DataSize[i];
+ }
+ }
+
+ return IGraphTransformer::TStatus::Ok;
+}
+
+bool NeedCalc(NNodes::TExprBase node) {
+ auto type = node.Ref().GetTypeAnn();
+ if (type->IsSingleton()) {
+ return false;
+ }
+
+ if (type->GetKind() == ETypeAnnotationKind::Optional) {
+ if (node.Maybe<TCoNothing>()) {
+ return false;
+ }
+ if (auto maybeJust = node.Maybe<TCoJust>()) {
+ return NeedCalc(maybeJust.Cast().Input());
+ }
+ return true;
+ }
+
+ if (type->GetKind() == ETypeAnnotationKind::Tuple) {
+ if (auto maybeTuple = node.Maybe<TExprList>()) {
+ return AnyOf(maybeTuple.Cast(), [](const auto& item) { return NeedCalc(item); });
+ }
+ return true;
+ }
+
+ if (type->GetKind() == ETypeAnnotationKind::List) {
+ if (node.Maybe<TCoList>()) {
+ YQL_ENSURE(node.Ref().ChildrenSize() == 1, "Should be rewritten to AsList");
+ return false;
+ }
+ if (auto maybeAsList = node.Maybe<TCoAsList>()) {
+ return AnyOf(maybeAsList.Cast().Args(), [](const auto& item) { return NeedCalc(NNodes::TExprBase(item)); });
+ }
+ return true;
+ }
+
+ YQL_ENSURE(type->GetKind() == ETypeAnnotationKind::Data,
+ "Object of type " << *type << " should not be considered for calculation");
+
+ return !node.Maybe<TCoDataCtor>();
+}
+
+} // unnamed
+
+bool UpdateUsedCluster(TString& usedCluster, const TString& newCluster) {
+ if (!usedCluster) {
+ usedCluster = newCluster;
+ } else if (usedCluster != newCluster) {
+ return false;
+ }
+ return true;
+}
+
+bool IsYtIsolatedLambda(const TExprNode& lambdaBody, TSyncMap& syncList, bool supportsReads, bool supportsDq) {
+ TNodeSet visited;
+ return IsYtIsolatedLambdaImpl(lambdaBody, syncList, nullptr, supportsReads, supportsDq, visited);
+}
+
+bool IsYtIsolatedLambda(const TExprNode& lambdaBody, TSyncMap& syncList, TString& usedCluster, bool supportsReads, bool supportsDq) {
+ TNodeSet visited;
+ return IsYtIsolatedLambdaImpl(lambdaBody, syncList, &usedCluster, supportsReads, supportsDq, visited);
+}
+
+bool IsYtCompleteIsolatedLambda(const TExprNode& lambda, TSyncMap& syncList, bool supportsReads, bool supportsDq) {
+ return lambda.IsComplete() && IsYtIsolatedLambda(lambda, syncList, supportsReads, supportsDq);
+}
+
+bool IsYtCompleteIsolatedLambda(const TExprNode& lambda, TSyncMap& syncList, TString& usedCluster, bool supportsReads, bool supportsDq) {
+ return lambda.IsComplete() && IsYtIsolatedLambda(lambda, syncList, usedCluster, supportsReads, supportsDq);
+}
+
+TExprNode::TPtr YtCleanupWorld(const TExprNode::TPtr& input, TExprContext& ctx, TYtState::TPtr state) {
+ TExprNode::TPtr output = input;
+
+ TNodeOnNodeOwnedMap remaps;
+ VisitExpr(output, [&remaps, &ctx](const TExprNode::TPtr& node) {
+ if (TYtLength::Match(node.Get())) {
+ return false;
+ }
+
+ if (TYtTableContent::Match(node.Get())) {
+ return false;
+ }
+
+ if (auto read = TMaybeNode<TCoRight>(node).Input().Maybe<TYtReadTable>()) {
+ remaps[node.Get()] = Build<TYtTableContent>(ctx, node->Pos())
+ .Input(read.Cast())
+ .Settings().Build()
+ .Done().Ptr();
+
+ return false;
+ }
+
+ if (TYtReadTable::Match(node.Get())) {
+ return false;
+ }
+
+ if (node->IsCallable("WithWorld")) {
+ remaps[node.Get()] = node->HeadPtr();
+ return false;
+ }
+
+ TDynBitMap outs;
+ for (size_t i = 0; i < node->ChildrenSize(); ++i) {
+ if (TYtOutput::Match(node->Child(i))) {
+ outs.Set(i);
+ }
+ }
+ if (!outs.Empty()) {
+ auto res = node;
+ Y_FOR_EACH_BIT(i, outs) {
+ res = ctx.ChangeChild(*res, i,
+ Build<TYtTableContent>(ctx, node->Pos())
+ .Input(node->ChildPtr(i))
+ .Settings().Build()
+ .Done().Ptr()
+ );
+ }
+ remaps[node.Get()] = res;
+ }
+
+ if (TYtOutput::Match(node.Get())) {
+ return false;
+ }
+
+ return true;
+ });
+
+ if (output->IsLambda() && TYtOutput::Match(output->Child(1))) {
+ remaps[output->Child(1)] = Build<TYtTableContent>(ctx, output->Child(1)->Pos())
+ .Input(output->ChildPtr(1))
+ .Settings().Build()
+ .Done().Ptr();
+ }
+
+ IGraphTransformer::TStatus status = IGraphTransformer::TStatus::Ok;
+ if (!remaps.empty()) {
+ TOptimizeExprSettings settings(state->Types);
+ settings.VisitChanges = true;
+ settings.VisitTuples = true;
+ status = RemapExpr(output, output, remaps, ctx, settings);
+ }
+
+ remaps.clear();
+ TNodeSet visitedReadTables;
+ ui64 sumSize = 0;
+ TMaybe<TPositionHandle> bigPos;
+ VisitExpr(output, [&remaps, &ctx, &visitedReadTables, &sumSize, &bigPos, state](const TExprNode::TPtr& node) {
+ if (auto maybeRead = TMaybeNode<TYtReadTable>(node)) {
+ if (state->Types->EvaluationInProgress &&
+ state->Configuration->EvaluationTableSizeLimit.Get() &&
+ visitedReadTables.emplace(maybeRead.Cast().Raw()).second) {
+ for (auto section : TYtSectionList(maybeRead.Cast().Input())) {
+ for (auto path : section.Paths()) {
+ auto info = TYtTableBaseInfo::Parse(path.Table());
+ if (info && info->Stat) {
+ sumSize += info->Stat->DataSize;
+ if (info->Stat->DataSize > *state->Configuration->EvaluationTableSizeLimit.Get()) {
+ bigPos = path.Table().Pos();
+ }
+ }
+ }
+ }
+ }
+
+ if (maybeRead.Cast().World().Ref().Type() != TExprNode::World) {
+ remaps[node.Get()] = ctx.ChangeChild(*node, 0, ctx.NewWorld(node->Pos()));
+ }
+ return false;
+ }
+ if (TYtOutput::Match(node.Get())) {
+ return false;
+ }
+ return true;
+ });
+
+ if (state->Types->EvaluationInProgress && state->Configuration->EvaluationTableSizeLimit.Get()) {
+ if (sumSize > *state->Configuration->EvaluationTableSizeLimit.Get()) {
+ ctx.AddError(TIssue(ctx.GetPosition(bigPos.GetOrElse(input->Pos())), TStringBuilder() << "Too large table(s) for evaluation pass: "
+ << sumSize << " > " << *state->Configuration->EvaluationTableSizeLimit.Get()));
+ return nullptr;
+ }
+ }
+
+ if (!remaps.empty()) {
+ TOptimizeExprSettings settings(state->Types);
+ settings.VisitChanges = true;
+ status = status.Combine(RemapExpr(output, output, remaps, ctx, settings));
+ }
+
+ YQL_ENSURE(status.Level != IGraphTransformer::TStatus::Error, "Bad input graph");
+
+ if (state->Types->EvaluationInProgress) {
+ status = status.Combine(SubstTables(output, state, false, ctx));
+ YQL_ENSURE(status.Level != IGraphTransformer::TStatus::Error, "Subst tables failed");
+ }
+
+ return output;
+}
+
+TYtOutputOpBase GetOutputOp(TYtOutput output) {
+ if (const auto tr = output.Operation().Maybe<TYtTryFirst>()) {
+ return tr.Cast().Second();
+ }
+ return output.Operation().Cast<TYtOutputOpBase>();
+}
+
+TVector<TYtTableBaseInfo::TPtr> GetInputTableInfos(TExprBase input) {
+ TVector<TYtTableBaseInfo::TPtr> res;
+ if (auto out = input.Maybe<TYtOutput>()) {
+ res.push_back(MakeIntrusive<TYtOutTableInfo>(GetOutTable(out.Cast())));
+ res.back()->IsUnordered = IsUnorderedOutput(out.Cast());
+ } else {
+ auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+ for (auto section: read.Cast().Input()) {
+ for (auto path: section.Paths()) {
+ res.push_back(TYtTableBaseInfo::Parse(path.Table()));
+ }
+ }
+ }
+ return res;
+}
+
+TVector<TYtPathInfo::TPtr> GetInputPaths(TExprBase input) {
+ TVector<TYtPathInfo::TPtr> res;
+ if (auto out = input.Maybe<TYtOutput>()) {
+ res.push_back(MakeIntrusive<TYtPathInfo>());
+ res.back()->Table = MakeIntrusive<TYtOutTableInfo>(GetOutTable(out.Cast()));
+ res.back()->Table->IsUnordered = IsUnorderedOutput(out.Cast());
+ } else {
+ auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+ for (auto section: read.Cast().Input()) {
+ for (auto path: section.Paths()) {
+ res.push_back(MakeIntrusive<TYtPathInfo>(path));
+ }
+ }
+ }
+ return res;
+}
+
+TStringBuf GetClusterName(NNodes::TExprBase input) {
+ if (auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ return read.Cast().DataSource().Cluster().Value();
+ } else if (auto output = input.Maybe<TYtOutput>()) {
+ return GetOutputOp(output.Cast()).DataSink().Cluster().Value();
+ } else if (auto op = input.Maybe<TCoRight>().Input().Maybe<TYtOutputOpBase>()) {
+ return op.Cast().DataSink().Cluster().Value();
+ } else {
+ YQL_ENSURE(false, "Unknown operation input");
+ }
+}
+
+bool IsYtProviderInput(NNodes::TExprBase input, bool withVariantList) {
+ if (input.Maybe<TYtOutput>()) {
+ return true;
+ }
+ if (auto maybeYtInput = input.Maybe<TCoRight>().Input()) {
+ if (withVariantList && maybeYtInput.Maybe<TYtOutputOpBase>()) {
+ return true;
+ }
+ if (auto maybeRead = maybeYtInput.Maybe<TYtReadTable>()) {
+ return withVariantList || maybeRead.Cast().Input().Size() == 1;
+ }
+ }
+ return false;
+}
+
+bool IsConstExpSortDirections(NNodes::TExprBase sortDirections) {
+ if (sortDirections.Maybe<TCoBool>()) {
+ return true;
+ } else if (sortDirections.Maybe<TExprList>()) {
+ for (auto child: sortDirections.Cast<TExprList>()) {
+ if (!child.Maybe<TCoBool>()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+TExprNode::TListType GetNodesToCalculate(const TExprNode::TPtr& input) {
+ TExprNode::TListType needCalc;
+ TNodeSet uniqNodes;
+ VisitExpr(input, [&needCalc, &uniqNodes](const TExprNode::TPtr& node) {
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(node)) {
+ auto op = maybeOp.Cast();
+ for (auto setting: op.Settings()) {
+ switch (FromString<EYtSettingType>(setting.Name().Value())) {
+ case EYtSettingType::Limit:
+ for (auto expr: setting.Value().Cast().Ref().Children()) {
+ for (auto item: expr->Children()) {
+ if (uniqNodes.insert(item->Child(1)).second) {
+ if (NeedCalc(TExprBase(item->Child(1)))) {
+ needCalc.push_back(item->ChildPtr(1));
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ else if (auto maybeSection = TMaybeNode<TYtSection>(node)) {
+ TYtSection section = maybeSection.Cast();
+ for (auto setting: section.Settings()) {
+ switch (FromString<EYtSettingType>(setting.Name().Value())) {
+ case EYtSettingType::Take:
+ case EYtSettingType::Skip:
+ if (uniqNodes.insert(setting.Value().Cast().Raw()).second) {
+ if (NeedCalc(setting.Value().Cast())) {
+ needCalc.push_back(setting.Value().Cast().Ptr());
+ }
+ }
+ break;
+ case EYtSettingType::KeyFilter: {
+ auto value = setting.Value().Cast<TExprList>();
+ if (value.Size() > 0) {
+ for (auto member: value.Item(0).Cast<TCoNameValueTupleList>()) {
+ for (auto cmp: member.Value().Cast<TCoNameValueTupleList>()) {
+ if (cmp.Value() && uniqNodes.insert(cmp.Value().Cast().Raw()).second) {
+ if (NeedCalc(cmp.Value().Cast())) {
+ needCalc.push_back(cmp.Value().Cast().Ptr());
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ case EYtSettingType::KeyFilter2: {
+ auto value = setting.Value().Cast<TExprList>();
+ if (value.Size() > 0) {
+ if (uniqNodes.insert(value.Item(0).Raw()).second && NeedCalc(value.Item(0))) {
+ needCalc.push_back(value.Item(0).Ptr());
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+ else if (TMaybeNode<TYtOutput>(node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+ return true;
+ });
+ return needCalc;
+}
+
+bool HasNodesToCalculate(const TExprNode::TPtr& input) {
+ bool needCalc = false;
+ VisitExpr(input, [&needCalc](const TExprNode::TPtr& node) {
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(node)) {
+ auto op = maybeOp.Cast();
+ for (auto setting: op.Settings()) {
+ switch (FromString<EYtSettingType>(setting.Name().Value())) {
+ case EYtSettingType::Limit:
+ for (auto expr: setting.Value().Cast().Ref().Children()) {
+ for (auto item: expr->Children()) {
+ if (NeedCalc(TExprBase(item->Child(1)))) {
+ needCalc = true;
+ return false;
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ else if (auto maybeSection = TMaybeNode<TYtSection>(node)) {
+ TYtSection section = maybeSection.Cast();
+ for (auto setting: section.Settings()) {
+ switch (FromString<EYtSettingType>(setting.Name().Value())) {
+ case EYtSettingType::Take:
+ case EYtSettingType::Skip:
+ if (NeedCalc(setting.Value().Cast())) {
+ needCalc = true;
+ return false;
+ }
+ break;
+ case EYtSettingType::KeyFilter: {
+ auto value = setting.Value().Cast<TExprList>();
+ if (value.Size() > 0) {
+ for (auto member: value.Item(0).Cast<TCoNameValueTupleList>()) {
+ for (auto cmp: member.Value().Cast<TCoNameValueTupleList>()) {
+ if (cmp.Value() && NeedCalc(cmp.Value().Cast())) {
+ needCalc = true;
+ return false;
+ }
+ }
+ }
+ }
+ break;
+ }
+ case EYtSettingType::KeyFilter2: {
+ auto value = setting.Value().Cast<TExprList>();
+ if (value.Size() > 0) {
+ if (value.Item(0).Raw() && NeedCalc(value.Item(0))) {
+ needCalc = true;
+ return false;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+ else if (TMaybeNode<TYtOutput>(node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+ return !needCalc;
+ });
+ return needCalc;
+}
+
+std::pair<IGraphTransformer::TStatus, TAsyncTransformCallbackFuture> CalculateNodes(TYtState::TPtr state,
+ const TExprNode::TPtr& input,
+ const TString& cluster,
+ const TExprNode::TListType& needCalc,
+ TExprContext& ctx)
+{
+ YQL_ENSURE(!needCalc.empty());
+ YQL_ENSURE(!input->HasResult(), "Infinitive calculation loop detected");
+ TNodeMap<size_t> calcNodes;
+ TUserDataTable files;
+
+ TExprNode::TPtr list = ctx.NewList(input->Pos(), TExprNode::TListType(needCalc));
+ TTypeAnnotationNode::TListType tupleTypes;
+ std::transform(needCalc.cbegin(), needCalc.cend(), std::back_inserter(tupleTypes), [](const TExprNode::TPtr& n) { return n->GetTypeAnn(); });
+ list->SetTypeAnn(ctx.MakeType<TTupleExprType>(tupleTypes));
+ list->SetState(TExprNode::EState::ConstrComplete);
+
+ auto status = SubstTables(list, state, /*anonOnly*/true, ctx);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ auto callableTransformer = CreateExtCallableTypeAnnotationTransformer(*state->Types);
+ auto typeTransformer = CreateTypeAnnotationTransformer(callableTransformer, *state->Types);
+
+ TExprNode::TPtr optimized;
+ bool hasNonDeterministicFunctions = false;
+ status = PeepHoleOptimizeNode(list, optimized, ctx, *state->Types, typeTransformer.Get(), hasNonDeterministicFunctions);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return SyncStatus(status);
+ }
+
+ auto filesRes = NCommon::FreezeUsedFiles(*optimized, files, *state->Types, ctx, MakeUserFilesDownloadFilter(*state->Gateway, cluster));
+ if (filesRes.first.Level != IGraphTransformer::TStatus::Ok) {
+ return filesRes;
+ }
+
+ TString calcHash;
+ auto config = state->Configuration->GetSettingsForNode(*input);
+ const auto queryCacheMode = config->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable);
+ if (queryCacheMode != EQueryCacheMode::Disable) {
+ if (!hasNonDeterministicFunctions && config->QueryCacheUseForCalc.Get().GetOrElse(true)) {
+ calcHash = TYtNodeHashCalculator(state, cluster, config).GetHash(*list);
+ }
+ YQL_CLOG(DEBUG, ProviderYt) << "Calc hash: " << HexEncode(calcHash).Quote()
+ << ", cache mode: " << queryCacheMode;
+ }
+
+ for (size_t i: xrange(needCalc.size())) {
+ calcNodes.emplace(needCalc[i].Get(), i);
+ }
+
+ THashMap<TString, TString> secureParams;
+ NCommon::FillSecureParams(input, *state->Types, secureParams);
+
+ auto future = state->Gateway->Calc(optimized->ChildrenList(), ctx,
+ IYtGateway::TCalcOptions(state->SessionId)
+ .Cluster(cluster)
+ .UserDataBlocks(files)
+ .UdfModules(state->Types->UdfModules)
+ .UdfResolver(state->Types->UdfResolver)
+ .UdfValidateMode(state->Types->ValidateMode)
+ .PublicId(state->Types->TranslateOperationId(input->UniqueId()))
+ .Config(state->Configuration->GetSettingsForNode(*input))
+ .OptLLVM(state->Types->OptLLVM.GetOrElse(TString()))
+ .OperationHash(calcHash)
+ .SecureParams(secureParams)
+ );
+ return WrapFutureCallback(future, [state, calcNodes](const IYtGateway::TCalcResult& res, const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ YQL_ENSURE(res.Data.size() == calcNodes.size());
+
+ TProcessedNodesSet processedNodes;
+ if (TYtOpBase::Match(input.Get())) {
+ processedNodes.insert(input->Child(TYtOpBase::idx_World)->UniqueId());
+ }
+ VisitExpr(input, [&processedNodes](const TExprNode::TPtr& node) {
+ if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ TNodeOnNodeOwnedMap remaps;
+ for (auto& it: calcNodes) {
+ auto node = it.first;
+ auto type = node->GetTypeAnn();
+ YQL_ENSURE(type);
+ NYT::TNode data = res.Data[it.second];
+ remaps.emplace(node, NCommon::NodeToExprLiteral(node->Pos(), *type, data, ctx));
+ }
+ TOptimizeExprSettings settings(state->Types);
+ settings.VisitChanges = true;
+ settings.VisitStarted = true;
+ settings.ProcessedNodes = &processedNodes;
+ auto status = RemapExpr(input, output, remaps, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+ input->SetState(TExprNode::EState::ExecutionComplete);
+ output->SetResult(ctx.NewAtom(output->Pos(), "calc")); // Special marker to check infinitive loop
+ return status.Combine(IGraphTransformer::TStatus::Repeat);
+ });
+}
+
+TMaybe<ui64> GetLimit(const TExprNode& settings) {
+ auto limitNode = NYql::GetSetting(settings, EYtSettingType::Limit);
+ if (!limitNode) {
+ return Nothing();
+ }
+ limitNode = limitNode->ChildPtr(1);
+
+ TMaybe<ui64> limit;
+ for (auto part: limitNode->Children()) {
+ TRecordsRange partialRange;
+ partialRange.Fill(*part);
+ if (!partialRange.Limit.Defined()) {
+ return Nothing();
+ }
+
+ // check overflow
+ if (std::numeric_limits<ui64>::max() - partialRange.Limit.GetRef() < partialRange.Offset.GetOrElse(0)) {
+ return Nothing();
+ }
+
+ if (!limit.Defined()) {
+ limit = partialRange.Limit.GetRef() + partialRange.Offset.GetOrElse(0);
+ } else {
+ limit = Max(limit.GetRef(), partialRange.Limit.GetRef() + partialRange.Offset.GetOrElse(0));
+ }
+ }
+
+ return limit == std::numeric_limits<ui64>::max() ? Nothing() : limit;
+}
+
+TExprNode::TPtr GetLimitExpr(const TExprNode::TPtr& limitSetting, TExprContext& ctx) {
+ auto limitItems = limitSetting->ChildPtr(1);
+ TExprNode::TListType limitValues;
+ for (const auto& child : limitItems->Children()) {
+ TExprNode::TPtr skip, take;
+ for (auto& setting: child->Children()) {
+ if (setting->ChildrenSize() == 0) {
+ continue;
+ }
+
+ auto settingName = setting->Child(0)->Content();
+ if (settingName == TStringBuf("take")) {
+ take = setting->ChildPtr(1);
+ } else if (settingName == TStringBuf("skip")) {
+ skip = setting->ChildPtr(1);
+ }
+ }
+
+ if (!take) {
+ return nullptr;
+ }
+
+ if (skip) {
+ limitValues.push_back(ctx.NewCallable(child->Pos(), "+", { take, skip }));
+ } else {
+ limitValues.push_back(take);
+ }
+ }
+
+ if (limitValues.empty()) {
+ return nullptr;
+ }
+
+ if (limitValues.size() == 1) {
+ return limitValues.front();
+ }
+
+ return ctx.NewCallable(limitSetting->Pos(), "Max", std::move(limitValues));
+}
+
+IGraphTransformer::TStatus UpdateTableMeta(const TExprNode::TPtr& tableNode, TExprNode::TPtr& newTableNode,
+ const TYtTablesData::TPtr& tablesData, bool checkSqlView, bool updateRowSpecType, TExprContext& ctx)
+{
+ newTableNode = tableNode;
+ TYtTableInfo tableInfo = tableNode;
+ const TYtTableDescription& tableDesc = tablesData->GetTable(tableInfo.Cluster, tableInfo.Name, tableInfo.Epoch);
+ const bool withQB = NYql::HasSetting(tableInfo.Settings.Ref(), EYtSettingType::WithQB);
+ const bool hasUserSchema = NYql::HasSetting(tableInfo.Settings.Ref(), EYtSettingType::UserSchema);
+ const bool hasUserColumns = NYql::HasSetting(tableInfo.Settings.Ref(), EYtSettingType::UserColumns);
+ bool update = false;
+
+ auto rowSpec = withQB ? tableDesc.QB2RowSpec : tableDesc.RowSpec;
+ if (updateRowSpecType) {
+ if (rowSpec && tableInfo.RowSpec && !rowSpec->GetType()) {
+ rowSpec->CopyType(*tableInfo.RowSpec);
+ rowSpec->SortedByTypes = tableInfo.RowSpec->SortedByTypes;
+ }
+ }
+
+ if (!tableInfo.Stat) {
+ if (tableDesc.Stat) {
+ tableInfo.Stat = tableDesc.Stat;
+ update = true;
+ }
+ else if (tableDesc.Meta && tableDesc.Meta->DoesExist && tableInfo.Epoch.GetOrElse(0) == 0) {
+ ctx.AddError(TIssue(ctx.GetPosition(tableNode->Pos()), TStringBuilder() <<
+ "Table " << tableInfo.Name << " stat was not loaded"));
+ return IGraphTransformer::TStatus::Error;
+ }
+ }
+ if (!tableInfo.Meta) {
+ if (!tableDesc.Meta) {
+ if (tableInfo.Epoch.GetOrElse(0) != 0) {
+ return IGraphTransformer::TStatus(IGraphTransformer::TStatus::Repeat, true);
+ }
+ ctx.AddError(TIssue(ctx.GetPosition(tableNode->Pos()), TStringBuilder() <<
+ "Table " << tableInfo.Name << " metadata was not loaded"));
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ tableInfo.Meta = tableDesc.Meta;
+ tableInfo.RowSpec = rowSpec;
+ update = true;
+ }
+ else if (rowSpec && !tableInfo.RowSpec) {
+ tableInfo.RowSpec = rowSpec;
+ update = true;
+ }
+
+ if (checkSqlView && tableInfo.Meta->SqlView) {
+ ctx.AddError(TIssue(ctx.GetPosition(tableNode->Pos()), TStringBuilder()
+ << "Reading from " << tableInfo.Name.Quote() << " view is not supported"));
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ if (hasUserSchema || hasUserColumns) {
+ const auto setting = GetSetting(tableInfo.Settings.Ref(), hasUserSchema ? EYtSettingType::UserSchema : EYtSettingType::UserColumns);
+ auto type = setting->Child(1)->GetTypeAnn()->Cast<TTypeExprType>()->GetType()->Cast<TStructExprType>();
+ auto prevRowSpec = tableInfo.RowSpec;
+ TVector<TString> explicitYson;
+ if (prevRowSpec && hasUserColumns) {
+ const bool hasNativeFlags = prevRowSpec->GetNativeYtTypeFlags() != 0;
+ // patch original type
+ auto items = prevRowSpec->GetType()->GetItems();
+ for (const auto& newItem : type->GetItems()) {
+ if (auto pos = prevRowSpec->GetType()->FindItem(newItem->GetName())) {
+ if (hasNativeFlags) {
+ bool isOptional = false;
+ const TDataExprType* dataType = nullptr;
+ if (IsDataOrOptionalOfData(items[*pos]->GetItemType(), isOptional, dataType)
+ && dataType->GetSlot() == EDataSlot::Yson
+ && !IsDataOrOptionalOfData(newItem->GetItemType()))
+ {
+ explicitYson.emplace_back(newItem->GetName());
+ }
+ }
+ items[*pos] = ctx.MakeType<TItemExprType>(newItem->GetName(), newItem->GetItemType());
+ } else {
+ items.push_back(newItem);
+ }
+ }
+
+ type = ctx.MakeType<TStructExprType>(items);
+ }
+
+ if ((prevRowSpec && !IsSameAnnotation(*prevRowSpec->GetType(), *type)) || (!prevRowSpec && hasUserSchema)) {
+ update = true;
+
+ auto strict = hasUserSchema;
+ if (hasUserColumns) {
+ if (prevRowSpec) {
+ strict = prevRowSpec->StrictSchema;
+ }
+ }
+
+ tableInfo.RowSpec = MakeIntrusive<TYqlRowSpecInfo>();
+ tableInfo.RowSpec->SetType(type, prevRowSpec ? prevRowSpec->GetNativeYtTypeFlags() : 0ul);
+ tableInfo.RowSpec->UniqueKeys = false;
+ tableInfo.RowSpec->StrictSchema = strict;
+ tableInfo.RowSpec->ExplicitYson = explicitYson;
+
+ if (prevRowSpec) {
+ if (auto nativeType = prevRowSpec->GetNativeYtType()) {
+ tableInfo.RowSpec->CopyTypeOrders(*nativeType);
+ }
+ if (prevRowSpec->IsSorted()) {
+ tableInfo.RowSpec->CopySortness(*prevRowSpec, TYqlRowSpecInfo::ECopySort::WithDesc);
+ tableInfo.RowSpec->MakeCommonSortness(*prevRowSpec); // Truncated keys with changed types
+ }
+ }
+ }
+ } else {
+ if (!update && rowSpec && tableInfo.RowSpec && (!rowSpec->CompareSortness(*tableInfo.RowSpec) || rowSpec->GetNativeYtType() != tableInfo.RowSpec->GetNativeYtType())) {
+ tableInfo.RowSpec = rowSpec;
+ update = true;
+ }
+ }
+
+ if (update) {
+ newTableNode = tableInfo.ToExprNode(ctx, tableNode->Pos()).Ptr();
+ return IGraphTransformer::TStatus::Repeat;
+ }
+
+ return IGraphTransformer::TStatus::Ok;
+}
+
+TExprNode::TPtr ValidateAndUpdateTablesMeta(const TExprNode::TPtr& input, TStringBuf cluster, const TYtTablesData::TPtr& tablesData, bool updateRowSpecType, TExprContext& ctx) {
+ TNodeSet tables;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto maybeTable = TMaybeNode<TYtTable>(node)) {
+ tables.insert(maybeTable.Cast().Raw());
+ return false;
+ }
+ else if (TMaybeNode<TYtOutput>(node)) {
+ // Don't traverse deeper to inner operations
+ return false;
+ }
+ return true;
+ });
+
+ if (!tables.empty()) {
+ bool valid = true;
+ for (auto table: tables) {
+ if (cluster != table->Child(TYtTable::idx_Cluster)->Content()) {
+ ctx.AddError(TIssue(ctx.GetPosition(table->Child(TYtTable::idx_Cluster)->Pos()), TStringBuilder()
+ << "Table " << TString{table->Child(TYtTable::idx_Name)->Content()}.Quote()
+ << " cluster doesn't match DataSource/DataSink cluster: "
+ << TString{table->Child(TYtTable::idx_Cluster)->Content()}.Quote() << " != " << TString{cluster}.Quote()));
+ valid = false;
+ }
+ }
+ if (!valid) {
+ return {};
+ }
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ TExprNode::TPtr output = input;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (tables.find(node.Get()) != tables.cend()) {
+ if (!TYtTableInfo::HasSubstAnonymousLabel(TExprBase(node))) {
+ TExprNode::TPtr newNode;
+ auto status = UpdateTableMeta(node, newNode, tablesData, true, updateRowSpecType, ctx);
+ if (IGraphTransformer::TStatus::Error == status.Level) {
+ return {};
+ }
+ return newNode;
+ }
+ }
+ return node;
+ }, ctx, settings);
+
+ if (IGraphTransformer::TStatus::Error == status.Level) {
+ return {};
+ }
+ return output;
+ }
+
+ return input;
+}
+
+TExprNode::TPtr ResetTableMeta(const TExprNode::TPtr& tableNode, TExprContext& ctx) {
+ TExprNode::TListType children;
+ for (auto id: {TYtTable::idx_Meta, TYtTable::idx_Stat, TYtTable::idx_RowSpec}) {
+ if (!TCoVoid::Match(tableNode->Child(id))) {
+ if (children.empty()) {
+ children = tableNode->ChildrenList();
+ }
+ children[id] = ctx.NewCallable(tableNode->Pos(), TCoVoid::CallableName(), {});
+ }
+ }
+ if (children.empty()) {
+ return tableNode;
+ }
+ return ctx.ChangeChildren(*tableNode, std::move(children));
+}
+
+TExprNode::TPtr ResetOutTableMeta(const TExprNode::TPtr& tableNode, TExprContext& ctx) {
+ TExprNode::TListType children;
+ if (!TCoVoid::Match(tableNode->Child(TYtOutTable::idx_Stat))) {
+ if (children.empty()) {
+ children = tableNode->ChildrenList();
+ }
+ children[TYtOutTable::idx_Stat] = ctx.NewCallable(tableNode->Pos(), TCoVoid::CallableName(), {});
+ }
+
+ if (tableNode->Child(TYtOutTable::idx_Name)->Content()) {
+ if (children.empty()) {
+ children = tableNode->ChildrenList();
+ }
+ children[TYtOutTable::idx_Name] = ctx.NewAtom(tableNode->Pos(), TStringBuf());
+ }
+
+ if (children.empty()) {
+ return tableNode;
+ }
+ return ctx.ChangeChildren(*tableNode, std::move(children));
+}
+
+TExprNode::TPtr ResetTablesMeta(const TExprNode::TPtr& input, TExprContext& ctx, bool resetTmpOnly) {
+ TNodeSet tables;
+ TNodeSet outTables;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto maybeTable = TMaybeNode<TYtTable>(node)) {
+ if (!resetTmpOnly) {
+ if (!TCoVoid::Match(maybeTable.Stat().Raw()) || !TCoVoid::Match(maybeTable.Meta().Raw()) || !TCoVoid::Match(maybeTable.RowSpec().Raw())) {
+ tables.insert(maybeTable.Raw());
+ }
+ }
+ return false;
+ }
+ else if (auto maybeTable = TMaybeNode<TYtOutTable>(node)) {
+ if (!TCoVoid::Match(maybeTable.Stat().Raw()) || maybeTable.Cast().Name().Value()) {
+ outTables.insert(maybeTable.Raw());
+ }
+ return false;
+ }
+ else if (TMaybeNode<TYtOutput>(node)) {
+ // Don't traverse deeper to inner operations
+ return false;
+ }
+ return true;
+ });
+
+ if (!tables.empty() || !outTables.empty()) {
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ TExprNode::TPtr output = input;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (tables.find(node.Get()) != tables.cend()) {
+ return ResetTableMeta(node, ctx);
+ }
+ else if (outTables.find(node.Get()) != outTables.cend()) {
+ return ResetOutTableMeta(node, ctx);
+ }
+ return node;
+ }, ctx, settings);
+
+ if (IGraphTransformer::TStatus::Error == status.Level) {
+ return {};
+ }
+ return output;
+ }
+
+ return input;
+}
+
+std::pair<TExprBase, TString> GetOutTableWithCluster(TExprBase ytOutput) {
+ const auto output = ytOutput.Cast<TYtOutput>();
+ const auto op = GetOutputOp(output);
+ const auto cluster = TString{ op.DataSink().Cluster().Value() };
+ size_t ndx = 0;
+ YQL_ENSURE(TryFromString<size_t>(output.OutIndex().Value(), ndx), "Bad " << TYtOutput::CallableName() << " output index value");
+ const auto opOut = op.Output();
+ YQL_ENSURE(ndx < opOut.Size());
+ return { opOut.Item(ndx), cluster };
+}
+
+TExprBase GetOutTable(TExprBase ytOutput) {
+ return GetOutTableWithCluster(ytOutput).first;
+}
+
+TMaybeNode<TCoFlatMapBase> GetFlatMapOverInputStream(TCoLambda opLambda, const TParentsMap& parentsMap) {
+ TMaybeNode<TCoFlatMapBase> map;
+ if (const auto it = parentsMap.find(opLambda.Args().Arg(0).Raw()); parentsMap.cend() != it) {
+ for (const auto& parent : it->second) {
+ if (!map) {
+ if (map = TMaybeNode<TCoFlatMapBase>(parent))
+ continue;
+ }
+
+ if (!TCoDependsOn::Match(parent)) {
+ map = {};
+ break;
+ }
+ }
+ }
+
+ return map;
+}
+
+TMaybeNode<TCoFlatMapBase> GetFlatMapOverInputStream(TCoLambda opLambda) {
+ TParentsMap parentsMap;
+ GatherParents(opLambda.Body().Ref(), parentsMap);
+ return GetFlatMapOverInputStream(opLambda, parentsMap);
+}
+
+TExprNode::TPtr ToOutTableWithHash(TExprBase output, const TYtState::TPtr& state, TExprContext& ctx) {
+ auto [outTableNode, cluster] = GetOutTableWithCluster(output);
+ auto outTable = outTableNode.Ptr();
+ auto hash = TYtNodeHashCalculator(state, cluster, state->Configuration->Snapshot()).GetHash(output.Ref());
+ outTable = ctx.ChangeChild(*outTable, TYtOutTable::idx_Settings,
+ NYql::AddSetting(*outTable->Child(TYtOutTable::idx_Settings), EYtSettingType::OpHash, ctx.NewAtom(output.Pos(), HexEncode(hash)), ctx)
+ );
+ return outTable;
+}
+
+IGraphTransformer::TStatus SubstTables(TExprNode::TPtr& input, const TYtState::TPtr& state, bool anonOnly, TExprContext& ctx)
+{
+ TProcessedNodesSet processedNodes;
+ VisitExpr(input, [&processedNodes](const TExprNode::TPtr& node) {
+ if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ TOptimizeExprSettings settings(state->Types);
+ settings.VisitChanges = true;
+ settings.VisitStarted = true;
+ settings.CustomInstantTypeTransformer = state->Types->CustomInstantTypeTransformer.Get();
+ settings.ProcessedNodes = &processedNodes;
+
+ TExprNode::TPtr optimizedInput = input;
+ auto status = OptimizeExpr(optimizedInput, optimizedInput, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (auto maybeTable = TMaybeNode<TYtTable>(node)) {
+ auto table = maybeTable.Cast();
+ if (auto anon = NYql::GetSetting(table.Settings().Ref(), EYtSettingType::Anonymous)) {
+ if (anon->ChildrenSize() == 1) {
+ TString cluster = TString{table.Cluster().Value()};
+ TString anonTableName = TString{table.Name().Value()};
+ TString realTableName = state->AnonymousLabels.Value(std::make_pair(cluster, anonTableName), TString());
+ if (!realTableName) {
+ ctx.AddError(TIssue(ctx.GetPosition(table.Pos()), TStringBuilder() << "Unaccounted anonymous table: " << cluster << '.' << anonTableName));
+ return {};
+ }
+ auto children = node->ChildrenList();
+ children[TYtTable::idx_Name] = ctx.NewAtom(node->Pos(), realTableName);
+ children[TYtTable::idx_Settings] = NYql::AddSetting(
+ *NYql::RemoveSetting(table.Settings().Ref(), EYtSettingType::Anonymous, ctx),
+ EYtSettingType::Anonymous, ctx.NewAtom(node->Pos(), anonTableName), ctx);
+ return ctx.ChangeChildren(*node, std::move(children));
+ }
+ }
+ }
+
+ return node;
+ }, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ if (!anonOnly) {
+ const bool useQueryCache = state->Configuration->QueryCacheMode.Get().GetOrElse(EQueryCacheMode::Disable) != EQueryCacheMode::Disable
+ && state->Configuration->QueryCacheUseForCalc.Get().GetOrElse(true);
+
+ TNodeOnNodeOwnedMap toOpt;
+ VisitExpr(optimizedInput, [&toOpt, &state, useQueryCache, &ctx](const TExprNode::TPtr& node) {
+ if (auto maybePath = TMaybeNode<TYtPath>(node)) {
+ if (maybePath.Table().Maybe<TYtOutput>()) {
+ auto path = maybePath.Cast();
+ toOpt[node.Get()] = Build<TYtPath>(ctx, node->Pos())
+ .InitFrom(path)
+ .Table(useQueryCache ? ToOutTableWithHash(path.Table(), state, ctx) : GetOutTable(path.Table()).Ptr())
+ .Done().Ptr();
+ }
+ return false;
+ }
+ if (TMaybeNode<TYtLength>(node).Input().Maybe<TYtOutput>()) {
+ auto length = TYtLength(node);
+ toOpt[node.Get()] = Build<TYtLength>(ctx, node->Pos())
+ .InitFrom(length)
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource(ctx.RenameNode(GetOutputOp(length.Input().Cast<TYtOutput>()).DataSink().Ref(), TYtDSource::CallableName()))
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table(useQueryCache ? ToOutTableWithHash(length.Input(), state, ctx) : GetOutTable(length.Input()).Ptr())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ return false;
+ }
+ if (TMaybeNode<TYtTableContent>(node).Input().Maybe<TYtOutput>()) {
+ auto content = TYtTableContent(node);
+ toOpt[node.Get()] = Build<TYtTableContent>(ctx, node->Pos())
+ .InitFrom(content)
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource(ctx.RenameNode(GetOutputOp(content.Input().Cast<TYtOutput>()).DataSink().Ref(), TYtDSource::CallableName()))
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table(useQueryCache ? ToOutTableWithHash(content.Input(), state, ctx) : GetOutTable(content.Input()).Ptr())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ return false;
+ }
+ if (auto maybeOut = TMaybeNode<TYtOutput>(node)) {
+ auto out = maybeOut.Cast();
+ toOpt[node.Get()] = Build<TCoRight>(ctx, node->Pos())
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource(ctx.RenameNode(GetOutputOp(out).DataSink().Ref(), TYtDSource::CallableName()))
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table(useQueryCache ? ToOutTableWithHash(out, state, ctx) : GetOutTable(out).Ptr())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ return false;
+ }
+ return true;
+ });
+
+ if (!toOpt.empty()) {
+ settings.ProcessedNodes = nullptr;
+ status = RemapExpr(optimizedInput, optimizedInput, toOpt, ctx, settings);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+ }
+ }
+
+ if (optimizedInput != input) {
+ auto typeTransformer = CreateTypeAnnotationTransformer(CreateExtCallableTypeAnnotationTransformer(*state->Types, true), *state->Types);
+ auto constrTransformer = CreateConstraintTransformer(*state->Types, true, true);
+ TVector<TTransformStage> transformers;
+ const auto issueCode = TIssuesIds::CORE_TYPE_ANN;
+ transformers.push_back(TTransformStage(typeTransformer, "TypeAnnotation", issueCode));
+ transformers.push_back(TTransformStage(
+ CreateFunctorTransformer([](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) { return UpdateCompletness(input, output, ctx); }),
+ "UpdateCompletness", issueCode));
+ transformers.push_back(TTransformStage(constrTransformer, "Constraints", issueCode));
+ auto fullTransformer = CreateCompositeGraphTransformer(transformers, false);
+ status = InstantTransform(*fullTransformer, optimizedInput, ctx);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ input = optimizedInput;
+ }
+
+ return IGraphTransformer::TStatus::Ok;
+}
+
+TYtPath CopyOrTrivialMap(TPositionHandle pos, TExprBase world, TYtDSink dataSink, const TTypeAnnotationNode& scheme,
+ TYtSection section, TExprContext& ctx, const TYtState::TPtr& state, const TCopyOrTrivialMapOpts& opts)
+{
+ bool tryKeepSortness = opts.TryKeepSortness;
+ const bool singleInput = section.Paths().Size() == 1;
+ bool needMap = false;
+ const auto sysColumns = NYql::GetSetting(section.Settings().Ref(), EYtSettingType::SysColumns);
+ bool useExplicitColumns = false;
+ bool exactCopySort = false;
+ bool hasAux = false;
+ TVector<std::pair<TYqlRowSpecInfo::TPtr, bool>> rowSpecs;
+ TYtOutTableInfo outTable(scheme.Cast<TStructExprType>(), state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ TMaybe<NYT::TNode> nativeType;
+ bool first = true;
+ const bool useNativeDescSort = state->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ const bool hasRowSpec = !!pathInfo.Table->RowSpec;
+ const bool tableHasAux = hasRowSpec && pathInfo.Table->RowSpec->HasAuxColumns();
+ TMaybe<NYT::TNode> currentNativeType;
+ if (hasRowSpec) {
+ currentNativeType = pathInfo.GetNativeYtType();
+ }
+ if (first) {
+ nativeType = currentNativeType;
+ first = false;
+ }
+ const bool needTableMap = pathInfo.RequiresRemap() || bool(sysColumns)
+ || outTable.RowSpec->GetNativeYtTypeFlags() != pathInfo.GetNativeYtTypeFlags()
+ || currentNativeType != nativeType;
+ useExplicitColumns = useExplicitColumns || !pathInfo.Table->IsTemp || (tableHasAux && pathInfo.HasColumns());
+ needMap = needMap || needTableMap;
+ hasAux = hasAux || tableHasAux;
+ if (tryKeepSortness) {
+ if (pathInfo.Table->IsUnordered || (opts.RangesResetSort && pathInfo.Ranges && pathInfo.Ranges->GetRangesCount() > 1)) {
+ tryKeepSortness = false;
+ }
+ rowSpecs.emplace_back(pathInfo.Table->RowSpec, needTableMap);
+
+ exactCopySort = singleInput && pathInfo.Table->IsTemp && hasRowSpec
+ && IsSameAnnotation(scheme, *pathInfo.Table->RowSpec->GetType());
+ }
+ }
+ if (!needMap && nativeType) {
+ outTable.RowSpec->CopyTypeOrders(*nativeType);
+ }
+ useExplicitColumns = useExplicitColumns || (!tryKeepSortness && hasAux);
+
+ bool trimSort = false;
+ const bool sortConstraintEnabled = ctx.IsConstraintEnabled<TSortedConstraintNode>();
+ if (tryKeepSortness) {
+ bool sortIsChanged = false;
+ for (size_t i = 0; i < rowSpecs.size(); ++i) {
+ if (!rowSpecs[i].first) {
+ sortIsChanged = outTable.RowSpec->ClearSortness();
+ continue;
+ }
+ if (0 == i) {
+ TYqlRowSpecInfo::ECopySort mode = TYqlRowSpecInfo::ECopySort::Pure;
+ if (rowSpecs[i].second) {
+ if (sortConstraintEnabled) {
+ mode = TYqlRowSpecInfo::ECopySort::WithDesc;
+ }
+ } else {
+ mode = exactCopySort
+ ? TYqlRowSpecInfo::ECopySort::Exact
+ : TYqlRowSpecInfo::ECopySort::WithDesc;
+ }
+ sortIsChanged = outTable.RowSpec->CopySortness(*rowSpecs[i].first, mode);
+ } else {
+ sortIsChanged = outTable.RowSpec->MakeCommonSortness(*rowSpecs[i].first) || sortIsChanged;
+ if (rowSpecs[i].second && !sortConstraintEnabled) {
+ sortIsChanged = outTable.RowSpec->KeepPureSortOnly() || sortIsChanged;
+ }
+ }
+ }
+
+ useExplicitColumns = useExplicitColumns || (sortIsChanged && hasAux);
+ tryKeepSortness = outTable.RowSpec->IsSorted();
+ trimSort = !tryKeepSortness;
+ }
+ outTable.SetUnique(opts.SectionUniq, pos, ctx);
+
+ if (tryKeepSortness) {
+ if (needMap && !singleInput) {
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ if (!opts.LimitNodes.empty()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Limit))
+ .Build()
+ .Value<TExprList>()
+ .Add(opts.LimitNodes)
+ .Build()
+ .Build();
+ }
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ TExprNode::TPtr mapSectionSettings = ctx.NewList(section.Pos(), {});
+ TExprNode::TPtr sectionSettings = section.Settings().Ptr();
+ if (sysColumns) {
+ mapSectionSettings = NYql::AddSetting(*mapSectionSettings, EYtSettingType::SysColumns, sysColumns->ChildPtr(1), ctx);
+ sectionSettings = NYql::RemoveSetting(*sectionSettings, EYtSettingType::SysColumns, ctx);
+ }
+
+ auto getPathUniq = [] (const TYtPath& path) {
+ if (path.Ref().GetState() != TExprNode::EState::Initial) {
+ return path.Ref().GetConstraint<TDistinctConstraintNode>();
+ }
+ // Dynamically constructed YtPath for YtOutput
+ return path.Table().Ref().GetConstraint<TDistinctConstraintNode>();
+ };
+ TVector<TYtPath> updatedPaths;
+ YQL_ENSURE(rowSpecs.size() == section.Paths().Size());
+ for (size_t i = 0; i < section.Paths().Size(); ++i) {
+ auto path = section.Paths().Item(i);
+ if (rowSpecs[i].second) {
+ TYtOutTableInfo mapOutTable(scheme.Cast<TStructExprType>(), state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ YQL_ENSURE(rowSpecs[i].first);
+ mapOutTable.SetUnique(getPathUniq(path), path.Pos(), ctx);
+ auto mapper = Build<TCoLambda>(ctx, path.Pos())
+ .Args({"stream"})
+ .Body("stream")
+ .Done().Ptr();
+
+ mapOutTable.RowSpec->CopySortness(*rowSpecs[i].first, sortConstraintEnabled ? TYqlRowSpecInfo::ECopySort::WithDesc : TYqlRowSpecInfo::ECopySort::Pure);
+ if (sortConstraintEnabled) {
+ TKeySelectorBuilder builder(path.Pos(), ctx, useNativeDescSort, scheme.Cast<TStructExprType>());
+ builder.ProcessRowSpec(*mapOutTable.RowSpec);
+ if (builder.NeedMap()) {
+ mapper = builder.MakeRemapLambda(true);
+ }
+ }
+
+ path = Build<TYtPath>(ctx, path.Pos())
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .World(world)
+ .DataSink(dataSink)
+ .Input()
+ .Add()
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings(mapSectionSettings)
+ .Build()
+ .Build()
+ .Output()
+ .Add(mapOutTable.ToExprNode(ctx, path.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Build()
+ .OutIndex()
+ .Value("0")
+ .Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ }
+ updatedPaths.push_back(path);
+ }
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(sectionSettings)
+ .Done();
+ needMap = false;
+ }
+ } else if (!trimSort) {
+ section = MakeUnorderedSection(section, ctx);
+ }
+
+ if (needMap) {
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ if (!opts.LimitNodes.empty()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Limit))
+ .Build()
+ .Value<TExprList>()
+ .Add(opts.LimitNodes)
+ .Build()
+ .Build();
+ }
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ auto mapper = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body("stream")
+ .Done().Ptr();
+
+ if (sortConstraintEnabled && outTable.RowSpec->IsSorted()) {
+ TKeySelectorBuilder builder(pos, ctx, useNativeDescSort, scheme.Cast<TStructExprType>());
+ builder.ProcessRowSpec(*outTable.RowSpec);
+ if (builder.NeedMap()) {
+ mapper = builder.MakeRemapLambda(true);
+ }
+ }
+
+ return Build<TYtPath>(ctx, pos)
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .World(world)
+ .DataSink(dataSink)
+ .Input()
+ .Add(section)
+ .Build()
+ .Output()
+ .Add(outTable.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Build()
+ .OutIndex()
+ .Value("0")
+ .Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ }
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ForceTransform))
+ .Build()
+ .Build();
+ }
+ if (opts.CombineChunks) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::CombineChunks))
+ .Build()
+ .Build();
+ }
+ if (!opts.LimitNodes.empty()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Limit))
+ .Build()
+ .Value<TExprList>()
+ .Add(opts.LimitNodes)
+ .Build()
+ .Build();
+ }
+
+ if (useExplicitColumns) {
+ TSet<TStringBuf> columns;
+ for (auto item: outTable.RowSpec->GetType()->GetItems()) {
+ columns.insert(item->GetName());
+ }
+ for (auto item: outTable.RowSpec->GetAuxColumns()) {
+ columns.insert(item.first);
+ }
+
+ section = UpdateInputFields(section, std::move(columns), ctx, false);
+ }
+
+ return Build<TYtPath>(ctx, pos)
+ .Table<TYtOutput>()
+ .Operation<TYtMerge>()
+ .World(world)
+ .DataSink(dataSink)
+ .Input()
+ .Add(section)
+ .Build()
+ .Output()
+ .Add(outTable.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Build()
+ .OutIndex()
+ .Value(TStringBuf("0"))
+ .Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+}
+
+namespace {
+
+template <class T>
+const TExprNode* GetSingleParent(const TExprNode* node, const TParentsMap& parentsMap) {
+ if (T::Match(node)) {
+ auto parentsIt = parentsMap.find(node);
+ YQL_ENSURE(parentsIt != parentsMap.cend());
+ if (parentsIt->second.size() != 1) {
+ return nullptr;
+ }
+ return *parentsIt->second.begin();
+ }
+ return node;
+}
+
+}
+
+
+bool IsOutputUsedMultipleTimes(const TExprNode& op, const TParentsMap& parentsMap) {
+ const TExprNode* node = &op;
+ node = GetSingleParent<TYtOutputOpBase>(node, parentsMap);
+ if (nullptr == node) {
+ return true;
+ }
+ node = GetSingleParent<TYtOutput>(node, parentsMap);
+ if (nullptr == node) {
+ return true;
+ }
+ node = GetSingleParent<TYtPath>(node, parentsMap);
+ if (nullptr == node) {
+ return true;
+ }
+ node = GetSingleParent<TYtPathList>(node, parentsMap);
+ if (nullptr == node) {
+ return true;
+ }
+ node = GetSingleParent<TYtSection>(node, parentsMap);
+ if (nullptr == node) {
+ return true;
+ }
+ node = GetSingleParent<TYtSectionList>(node, parentsMap);
+ return node == nullptr;
+}
+
+TMaybe<NYT::TRichYPath> BuildYtPathForStatRequest(const TString& cluster, const TYtPathInfo& pathInfo,
+ const TMaybe<TVector<TString>>& overrideColumns, const TYtState& state, TExprContext& ctx)
+{
+ auto ytPath = NYT::TRichYPath(pathInfo.Table->Name);
+ pathInfo.FillRichYPath(ytPath);
+ if (overrideColumns) {
+ ytPath.Columns(*overrideColumns);
+ }
+
+ if (ytPath.Columns_ && dynamic_cast<TYtTableInfo*>(pathInfo.Table.Get()) && pathInfo.Table->IsAnonymous
+ && !TYtTableInfo::HasSubstAnonymousLabel(pathInfo.Table->FromNode.Cast())) {
+ TString realTableName = state.AnonymousLabels.Value(std::make_pair(cluster, pathInfo.Table->Name), TString());
+ if (!realTableName) {
+ TPositionHandle pos;
+ if (pathInfo.FromNode) {
+ pos = pathInfo.FromNode.Cast().Pos();
+ }
+ ctx.AddError(TIssue(ctx.GetPosition(pos), TStringBuilder() << "Unaccounted anonymous table: " << cluster << '.' << pathInfo.Table->Name));
+ return {};
+ }
+ ytPath.Path_ = realTableName;
+ }
+
+ return ytPath;
+}
+
+TMaybe<TVector<ui64>> EstimateDataSize(const TString& cluster, const TVector<TYtPathInfo::TPtr>& paths,
+ const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx)
+{
+ TVector<ui64> result;
+ TSet<TString> requestedColumns;
+
+ bool sync = true;
+
+ auto status = EstimateDataSize(result, requestedColumns, cluster, paths, columns, state, ctx, sync);
+ if (status != IGraphTransformer::TStatus::Ok) {
+ return {};
+ }
+
+ return result;
+}
+
+IGraphTransformer::TStatus TryEstimateDataSize(TVector<ui64>& result, TSet<TString>& requestedColumns,
+ const TString& cluster, const TVector<TYtPathInfo::TPtr>& paths,
+ const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx)
+{
+ bool sync = false;
+ return EstimateDataSize(result, requestedColumns, cluster, paths, columns, state, ctx, sync);
+}
+
+TYtSection UpdateInputFields(TYtSection section, TExprBase fields, TExprContext& ctx) {
+ auto settings = section.Settings().Ptr();
+
+ auto sysColumns = NYql::GetSettingAsColumnList(*settings, EYtSettingType::SysColumns);
+ if (!sysColumns.empty()) {
+ if (auto list = fields.Maybe<TExprList>()) {
+ TMap<TStringBuf, TExprNode::TPtr> fieldMap;
+ for (auto item: list.Cast()) {
+ if (auto atom = item.Maybe<TCoAtom>()) {
+ fieldMap.emplace(atom.Cast().Value(), item.Ptr());
+ } else {
+ fieldMap.emplace(item.Cast<TCoAtomList>().Item(0).Value(), item.Ptr());
+ }
+ }
+ TVector<TString> updatedSysColumns;
+ for (auto sys: sysColumns) {
+ auto sysColName = TString(YqlSysColumnPrefix).append(sys);
+ if (fieldMap.contains(sysColName)) {
+ updatedSysColumns.push_back(sys);
+ fieldMap.erase(sysColName);
+ }
+ }
+ if (updatedSysColumns.size() != sysColumns.size()) {
+ settings = NYql::RemoveSetting(*settings, EYtSettingType::SysColumns, ctx);
+ if (!updatedSysColumns.empty()) {
+ settings = NYql::AddSettingAsColumnList(*settings, EYtSettingType::SysColumns, updatedSysColumns, ctx);
+ }
+ }
+ if (fieldMap.size() != list.Cast().Size()) {
+ TExprNode::TListType children;
+ std::transform(fieldMap.begin(), fieldMap.end(), std::back_inserter(children), [](const auto& pair) { return pair.second; });
+ fields = TExprBase(ctx.NewList(fields.Pos(), std::move(children)));
+ }
+ }
+ }
+
+ auto pathsBuilder = Build<TYtPathList>(ctx, section.Paths().Pos());
+ for (const auto& path : section.Paths()) {
+ pathsBuilder.Add<TYtPath>()
+ .InitFrom(path)
+ .Columns(fields)
+ .Build();
+ }
+ return Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths(pathsBuilder.Done())
+ .Settings(settings)
+ .Done();
+}
+
+TYtSection UpdateInputFields(TYtSection section, TSet<TStringBuf>&& members, TExprContext& ctx, bool hasWeakFields) {
+ auto settings = section.Settings().Ptr();
+
+ auto sysColumns = NYql::GetSettingAsColumnList(*settings, EYtSettingType::SysColumns);
+ if (!sysColumns.empty()) {
+ TVector<TString> updatedSysColumns;
+ for (auto sys: sysColumns) {
+ auto sysColName = TString(YqlSysColumnPrefix).append(sys);
+ if (members.contains(sysColName)) {
+ updatedSysColumns.push_back(sys);
+ members.erase(sysColName);
+ }
+ }
+ if (updatedSysColumns.size() != sysColumns.size()) {
+ settings = NYql::RemoveSetting(*settings, EYtSettingType::SysColumns, ctx);
+ if (!updatedSysColumns.empty()) {
+ settings = NYql::AddSettingAsColumnList(*settings, EYtSettingType::SysColumns, updatedSysColumns, ctx);
+ }
+ }
+ }
+
+ auto fields = ToAtomList(members, section.Pos(), ctx);
+ auto pathsBuilder = Build<TYtPathList>(ctx, section.Paths().Pos());
+ for (const auto& path : section.Paths()) {
+ if (!hasWeakFields || path.Columns().Maybe<TCoVoid>()) {
+ pathsBuilder.Add<TYtPath>()
+ .InitFrom(path)
+ .Columns(fields)
+ .Build();
+ } else {
+ THashMap<TStringBuf, TExprNode::TPtr> weakFields;
+ for (auto col: path.Columns().Cast<TExprList>()) {
+ if (col.Ref().ChildrenSize() == 2) {
+ weakFields[col.Ref().Child(0)->Content()] = col.Ptr();
+ }
+ }
+ TExprNode::TListType updatedColumns;
+ for (auto member: fields->Children()) {
+ if (auto p = weakFields.FindPtr(member->Content())) {
+ updatedColumns.push_back(*p);
+ } else {
+ updatedColumns.push_back(member);
+ }
+ }
+ pathsBuilder.Add<TYtPath>()
+ .InitFrom(path)
+ .Columns(ctx.NewList(path.Pos(), std::move(updatedColumns)))
+ .Build();
+ }
+ }
+ return Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths(pathsBuilder.Done())
+ .Settings(settings)
+ .Done();
+}
+
+TYtPath MakeUnorderedPath(TYtPath path, bool hasLimits, TExprContext& ctx) {
+ bool makeUnordered = false;
+ bool keepSort = false;
+ if (auto maybeOut = path.Table().Maybe<TYtOutput>()) {
+ const auto out = maybeOut.Cast();
+ if (!IsUnorderedOutput(out)) {
+ makeUnordered = true;
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ for (auto range: path.Ranges().Cast<TExprList>()) {
+ if (range.Maybe<TYtKeyExact>() || range.Maybe<TYtKeyRange>()) {
+ makeUnordered = false;
+ } else if (range.Maybe<TYtRow>() || range.Maybe<TYtRowRange>()) {
+ hasLimits = true;
+ }
+ }
+ }
+ }
+ if (auto settings = GetOutputOp(out).Maybe<TYtTransientOpBase>().Settings()) {
+ hasLimits = hasLimits || NYql::HasSetting(settings.Ref(), EYtSettingType::Limit);
+ keepSort = NYql::HasSetting(settings.Ref(), EYtSettingType::KeepSorted);
+ } else if (auto settings = GetOutputOp(out).Maybe<TYtFill>().Settings()) {
+ keepSort = NYql::HasSetting(settings.Ref(), EYtSettingType::KeepSorted);
+ }
+ keepSort = keepSort || GetOutputOp(out).Maybe<TYtSort>();
+ }
+ if (makeUnordered && hasLimits && keepSort) {
+ makeUnordered = false;
+ }
+ if (makeUnordered) {
+ return Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .InitFrom(path.Table().Cast<TYtOutput>())
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered))
+ .Build()
+ .Build()
+ .Done();
+ }
+ return path;
+
+}
+
+template<bool WithUnorderedSetting>
+TYtSection MakeUnorderedSection(TYtSection section, TExprContext& ctx) {
+ if (HasNonEmptyKeyFilter(section)) {
+ if constexpr (WithUnorderedSetting)
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths(section.Paths())
+ .Settings(NYql::AddSetting(section.Settings().Ref(), EYtSettingType::Unordered, {}, ctx))
+ .Done();
+ else
+ return section;
+ }
+ const bool hasLimits = NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip);
+ bool hasUpdated = false;
+ TVector<TYtPath> updatedPaths;
+ for (auto path: section.Paths()) {
+ updatedPaths.push_back(MakeUnorderedPath(path, hasLimits, ctx));
+ hasUpdated = hasUpdated || updatedPaths.back().Raw() != path.Raw();
+ }
+
+ if constexpr (WithUnorderedSetting) {
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(NYql::AddSetting(section.Settings().Ref(), EYtSettingType::Unordered, {}, ctx))
+ .Done();
+ } else {
+ if (!hasUpdated)
+ return section;
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(section.Settings())
+ .Done();
+ }
+}
+
+template TYtSection MakeUnorderedSection<true>(TYtSection section, TExprContext& ctx);
+template TYtSection MakeUnorderedSection<false>(TYtSection section, TExprContext& ctx);
+
+TYtSection ClearUnorderedSection(TYtSection section, TExprContext& ctx) {
+ const bool hasUnorderedOut = AnyOf(section.Paths(), [](const auto& path) { auto out = path.Table().template Maybe<TYtOutput>(); return out && IsUnorderedOutput(out.Cast()); });
+ if (hasUnorderedOut) {
+ TVector<TYtPath> updatedPaths;
+ for (auto path: section.Paths()) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ if (IsUnorderedOutput(out.Cast())) {
+ path = Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .InitFrom(out.Cast())
+ .Mode(TMaybeNode<TCoAtom>())
+ .Build()
+ .Done();
+ }
+ }
+ updatedPaths.push_back(path);
+ }
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Done();
+ }
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered)) {
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(NYql::RemoveSetting(section.Settings().Ref(), EYtSettingType::Unordered, ctx))
+ .Done();
+ }
+ return section;
+}
+
+TYtDSource GetDataSource(TExprBase input, TExprContext& ctx) {
+ TMaybeNode<TExprBase> n = input;
+ if (auto right = input.Maybe<TCoRight>()) {
+ n = right.Input();
+ } else if (auto content = input.Maybe<TYtTableContent>()) {
+ n = content.Input();
+ }
+ if (auto read = n.Maybe<TYtReadTable>())
+ return read.Cast().DataSource();
+ if (auto out = n.Maybe<TYtOutput>()) {
+ return TYtDSource(ctx.RenameNode(GetOutputOp(out.Cast()).DataSink().Ref(), "DataSource"));
+ } else {
+ YQL_ENSURE(false, "Unknown operation input");
+ }
+}
+
+TExprNode::TPtr BuildEmptyTablesRead(TPositionHandle pos, const TExprNode& userSchema, TExprContext& ctx) {
+ if (!EnsureArgsCount(userSchema, 2, ctx)) {
+ return {};
+ }
+
+ return ctx.Builder(pos)
+ .Callable("Cons!")
+ .World(0)
+ .Callable(1, "List")
+ .Callable(0, "ListType")
+ .Add(0, userSchema.ChildPtr(1))
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+}
+
+TExprNode::TPtr GetFlowSettings(TPositionHandle pos, const TYtState& state, TExprContext& ctx, TExprNode::TPtr settings) {
+ if (!settings) {
+ settings = ctx.NewList(pos, {});
+ }
+ if (state.Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settings = NYql::AddSetting(*settings, EYtSettingType::Flow, {}, ctx);
+ }
+ return settings;
+}
+
+TVector<TStringBuf> GetKeyFilterColumns(const NNodes::TYtSection& section, EYtSettingTypes kind) {
+ TVector<TStringBuf> result;
+ if (kind.HasFlags(EYtSettingType::KeyFilter) && NYql::HasSetting(section.Settings().Ref(), EYtSettingType::KeyFilter)) {
+ for (auto keyFilter: NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter)) {
+ auto value = TExprList(keyFilter);
+ if (value.Size() > 0) {
+ for (auto member: value.Item(0).Cast<TCoNameValueTupleList>()) {
+ result.emplace_back(member.Name().Value());
+ }
+ }
+ }
+ }
+ if (kind.HasFlags(EYtSettingType::KeyFilter2) && NYql::HasSetting(section.Settings().Ref(), EYtSettingType::KeyFilter2)) {
+ for (auto keyFilter: NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter2)) {
+ auto value = TExprList(keyFilter);
+ if (value.Size() > 0) {
+ for (auto member: value.Item(1).Cast<TCoNameValueTupleList>()) {
+ if (member.Name().Value() == "usedKeys") {
+ for (auto key : member.Value().Cast<TCoAtomList>()) {
+ result.emplace_back(key.Value());
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+}
+
+bool HasNonEmptyKeyFilter(const NNodes::TYtSection& section) {
+ auto hasChildren = [](const auto& node) { return node->ChildrenSize() > 0; };
+ return AnyOf(NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter), hasChildren) ||
+ AnyOf(NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter2), hasChildren);
+}
+
+TYtReadTable ConvertContentInputToRead(TExprBase input, TMaybeNode<TCoNameValueTupleList> settings, TExprContext& ctx, TMaybeNode<TCoAtomList> customFields) {
+ TExprNode::TPtr world;
+ TVector<TYtSection> sections;
+ TExprBase columns = customFields ? TExprBase(customFields.Cast()) : TExprBase(Build<TCoVoid>(ctx, input.Pos()).Done());
+ if (auto out = input.Maybe<TYtOutput>()) {
+ world = ctx.NewWorld(input.Pos());
+ if (!settings) {
+ settings = Build<TCoNameValueTupleList>(ctx, input.Pos()).Done();
+ }
+ sections.push_back(Build<TYtSection>(ctx, input.Pos())
+ .Paths()
+ .Add()
+ .Table(out.Cast())
+ .Columns(columns)
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings(settings.Cast())
+ .Done());
+ }
+ else {
+ auto read = input.Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+ world = read.Cast().World().Ptr();
+ for (auto section: read.Cast().Input()) {
+ if (settings) {
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(MergeSettings(section.Settings().Ref(), settings.Cast().Ref(), ctx))
+ .Done();
+ }
+
+ if (customFields) {
+ section = UpdateInputFields(section, customFields.Cast(), ctx);
+ }
+
+ sections.push_back(section);
+ }
+ }
+
+ return Build<TYtReadTable>(ctx, input.Pos())
+ .World(world)
+ .DataSource(GetDataSource(input, ctx))
+ .Input()
+ .Add(sections)
+ .Build()
+ .Done();
+}
+
+size_t GetMapDirectOutputsCount(const NNodes::TYtMapReduce& mapReduce) {
+ if (mapReduce.Mapper().Maybe<TCoVoid>()) {
+ return 0;
+ }
+ const auto& mapOutputType = GetSeqItemType(*mapReduce.Mapper().Ref().GetTypeAnn());
+ if (mapOutputType.GetKind() != ETypeAnnotationKind::Variant) {
+ return 0;
+ }
+
+ auto numVariants = mapOutputType.Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>()->GetSize();
+ YQL_ENSURE(numVariants > 1);
+ return numVariants - 1;
+}
+
+bool HasYtRowNumber(const TExprNode& node) {
+ bool hasRowNumber = false;
+ VisitExpr(node, [&hasRowNumber](const TExprNode& n) {
+ if (TYtRowNumber::Match(&n)) {
+ hasRowNumber = true;
+ } else if (TYtOutput::Match(&n)) {
+ return false;
+ }
+ return !hasRowNumber;
+ });
+ return hasRowNumber;
+}
+
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_helpers.h b/ydb/library/yql/providers/yt/provider/yql_yt_helpers.h
new file mode 100644
index 0000000000..43accb080c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_helpers.h
@@ -0,0 +1,138 @@
+#pragma once
+
+#include "yql_yt_table.h"
+#include "yql_yt_gateway.h"
+#include "yql_yt_provider.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+#include <util/generic/set.h>
+
+namespace NYql {
+
+bool UpdateUsedCluster(TString& usedCluster, const TString& newCluster);
+bool IsYtIsolatedLambda(const TExprNode& lambdaBody, TSyncMap& syncList, TString& usedCluster, bool supportsReads, bool supportsDq);
+bool IsYtCompleteIsolatedLambda(const TExprNode& lambdaBody, TSyncMap& syncList, bool supportsReads, bool supportsDq);
+bool IsYtCompleteIsolatedLambda(const TExprNode& lambdaBody, TSyncMap& syncList, TString& usedCluster, bool supportsReads, bool supportsDq);
+TExprNode::TPtr YtCleanupWorld(const TExprNode::TPtr& input, TExprContext& ctx, TYtState::TPtr state);
+TVector<TYtTableBaseInfo::TPtr> GetInputTableInfos(NNodes::TExprBase input);
+TVector<TYtPathInfo::TPtr> GetInputPaths(NNodes::TExprBase input);
+TStringBuf GetClusterName(NNodes::TExprBase input);
+bool IsYtProviderInput(NNodes::TExprBase input, bool withVariantList = false);
+bool IsConstExpSortDirections(NNodes::TExprBase sortDirections);
+TExprNode::TListType GetNodesToCalculate(const TExprNode::TPtr& input);
+bool HasNodesToCalculate(const TExprNode::TPtr& input);
+std::pair<IGraphTransformer::TStatus, TAsyncTransformCallbackFuture> CalculateNodes(TYtState::TPtr state, const TExprNode::TPtr& input,
+ const TString& cluster, const TExprNode::TListType& needCalc, TExprContext& ctx);
+TMaybe<ui64> GetLimit(const TExprNode& settings);
+TExprNode::TPtr GetLimitExpr(const TExprNode::TPtr& limitSetting, TExprContext& ctx);
+IGraphTransformer::TStatus UpdateTableMeta(const TExprNode::TPtr& tableNode, TExprNode::TPtr& newTableNode,
+ const TYtTablesData::TPtr& tablesData, bool checkSqlView, bool updateRowSpecType, TExprContext& ctx);
+TExprNode::TPtr ValidateAndUpdateTablesMeta(const TExprNode::TPtr& input, TStringBuf cluster, const TYtTablesData::TPtr& tablesData, bool updateRowSpecType, TExprContext& ctx);
+TExprNode::TPtr ResetTablesMeta(const TExprNode::TPtr& input, TExprContext& ctx, bool resetTmpOnly = false);
+NNodes::TExprBase GetOutTable(NNodes::TExprBase ytOutput);
+std::pair<NNodes::TExprBase, TString> GetOutTableWithCluster(NNodes::TExprBase ytOutput);
+NNodes::TMaybeNode<NNodes::TCoFlatMapBase> GetFlatMapOverInputStream(NNodes::TCoLambda opLambda, const TParentsMap& parentsMap);
+NNodes::TMaybeNode<NNodes::TCoFlatMapBase> GetFlatMapOverInputStream(NNodes::TCoLambda opLambda);
+IGraphTransformer::TStatus SubstTables(TExprNode::TPtr& input, const TYtState::TPtr& state, bool anonOnly, TExprContext& ctx);
+
+struct TCopyOrTrivialMapOpts {
+ TCopyOrTrivialMapOpts& SetLimitNodes(const TExprNode::TListType& limitNodes) {
+ LimitNodes = limitNodes;
+ return *this;
+ }
+
+ TCopyOrTrivialMapOpts& SetTryKeepSortness(bool tryKeepSortness = true) {
+ TryKeepSortness = tryKeepSortness;
+ return *this;
+ }
+
+ TCopyOrTrivialMapOpts& SetSectionUniq(const TDistinctConstraintNode* sectionUniq) {
+ SectionUniq = sectionUniq;
+ return *this;
+ }
+
+ TCopyOrTrivialMapOpts& SetCombineChunks(bool combineChunks = true) {
+ CombineChunks = combineChunks;
+ return *this;
+ }
+
+ TCopyOrTrivialMapOpts& SetRangesResetSort(bool rangesResetSort = true) {
+ RangesResetSort = rangesResetSort;
+ return *this;
+ }
+
+ TExprNode::TListType LimitNodes;
+ bool TryKeepSortness = false;
+ const TDistinctConstraintNode* SectionUniq = nullptr;
+ bool CombineChunks = false;
+ bool RangesResetSort = true;
+};
+
+NNodes::TYtPath CopyOrTrivialMap(TPositionHandle pos, NNodes::TExprBase world, NNodes::TYtDSink dataSink,
+ const TTypeAnnotationNode& scheme, NNodes::TYtSection section, TExprContext& ctx, const TYtState::TPtr& state,
+ const TCopyOrTrivialMapOpts& opts);
+bool IsOutputUsedMultipleTimes(const TExprNode& op, const TParentsMap& parentsMap);
+
+TMaybe<TVector<ui64>> EstimateDataSize(const TString& cluster, const TVector<TYtPathInfo::TPtr>& paths,
+ const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx);
+IGraphTransformer::TStatus TryEstimateDataSize(TVector<ui64>& result, TSet<TString>& requestedColumns,
+ const TString& cluster, const TVector<TYtPathInfo::TPtr>& paths,
+ const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx);
+TMaybe<NYT::TRichYPath> BuildYtPathForStatRequest(const TString& cluster, const TYtPathInfo& pathInfo,
+ const TMaybe<TVector<TString>>& overrideColumns, const TYtState& state, TExprContext& ctx);
+
+NNodes::TYtSection UpdateInputFields(NNodes::TYtSection section, NNodes::TExprBase fields, TExprContext& ctx);
+NNodes::TYtSection UpdateInputFields(NNodes::TYtSection section, TSet<TStringBuf>&& fields, TExprContext& ctx, bool hasWeakFields);
+NNodes::TYtPath MakeUnorderedPath(NNodes::TYtPath path, bool hasLimits, TExprContext& ctx);
+template<bool WithUnorderedSetting = false>
+NNodes::TYtSection MakeUnorderedSection(NNodes::TYtSection section, TExprContext& ctx);
+NNodes::TYtSection ClearUnorderedSection(NNodes::TYtSection section, TExprContext& ctx);
+NNodes::TYtDSource GetDataSource(NNodes::TExprBase input, TExprContext& ctx);
+TExprNode::TPtr BuildEmptyTablesRead(TPositionHandle pos, const TExprNode& userSchema, TExprContext& ctx);
+TExprNode::TPtr GetFlowSettings(TPositionHandle pos, const TYtState& state, TExprContext& ctx, TExprNode::TPtr settings = {});
+TVector<TStringBuf> GetKeyFilterColumns(const NNodes::TYtSection& section, EYtSettingTypes kind);
+bool HasNonEmptyKeyFilter(const NNodes::TYtSection& section);
+
+NNodes::TYtOutputOpBase GetOutputOp(NNodes::TYtOutput output);
+
+inline bool IsUnorderedOutput(NNodes::TYtOutput out) {
+ return out.Mode() && FromString<EYtSettingType>(out.Mode().Cast().Value()) == EYtSettingType::Unordered;
+}
+
+template <typename TGateway>
+std::function<bool(const TString&)> MakeUserFilesDownloadFilter(const TGateway& gateway, const TString& activeCluster) {
+ // hold activeCluster by value to support temp strings
+ return [&gateway, activeCluster](const TString& url) {
+ if (activeCluster.empty()) {
+ // todo: we lost our opportunity to skip download in this case, improve it
+ return true;
+ }
+
+ TString extractedCluster;
+ TString extractedPath;
+ if (!gateway.TryParseYtUrl(url, &extractedCluster, &extractedPath/* don't remove - triggers additional check against allowed patterns*/)) {
+ return true;
+ }
+
+ return (extractedCluster != activeCluster) && (extractedCluster != CurrentYtClusterShortcut);
+ };
+}
+
+NNodes::TYtReadTable ConvertContentInputToRead(NNodes::TExprBase input, NNodes::TMaybeNode<NNodes::TCoNameValueTupleList> settings, TExprContext& ctx, NNodes::TMaybeNode<NNodes::TCoAtomList> customFields = {});
+
+size_t GetMapDirectOutputsCount(const NNodes::TYtMapReduce& mapReduce);
+
+bool HasYtRowNumber(const TExprNode& node);
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
new file mode 100644
index 0000000000..d9ca1f18a8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.cpp
@@ -0,0 +1,1933 @@
+#include "yql_yt_horizontal_join.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_optimize.h"
+
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/xrange.h>
+#include <util/generic/algorithm.h>
+#include <util/string/cast.h>
+#include <util/digest/numeric.h>
+
+
+template <>
+struct THash<std::set<ui64>> {
+ inline size_t operator()(const std::set<ui64>& s) const {
+ size_t hash = s.size();
+ for (auto& v: s) {
+ hash = CombineHashes(NumericHash(v), hash);
+ }
+ return hash;
+ }
+};
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+ THashSet<TStringBuf> UDF_HORIZONTAL_JOIN_WHITE_LIST = {
+ "Streaming",
+ };
+}
+
+bool THorizontalJoinBase::IsGoodForHorizontalJoin(TYtMap map) const {
+ // Map already executed or in progress
+ if (map.Ref().GetState() == TExprNode::EState::ExecutionComplete
+ || map.Ref().GetState() == TExprNode::EState::ExecutionInProgress
+ || (map.Ref().HasResult() && map.Ref().GetResult().Type() == TExprNode::World)) {
+ return false;
+ }
+
+ // Another node depends on this Map by world
+ if (HasWorldDeps.find(map.Raw()) != HasWorldDeps.end()) {
+ return false;
+ }
+
+ // Map has multiple outputs or input sections
+ if (map.Input().Size() != 1 || map.Output().Size() != 1) {
+ return false;
+ }
+
+ // Map has output limit or is sharded MapJoin
+ if (NYql::HasAnySetting(map.Settings().Ref(), EYtSettingType::Limit | EYtSettingType::SortLimitBy | EYtSettingType::Sharded | EYtSettingType::JobCount)) {
+ return false;
+ }
+
+ if (!IsYieldTransparent(map.Mapper().Ptr(), *State_->Types)) {
+ return false;
+ }
+
+ bool good = true;
+ const TExprNode* innerLambdaArg = nullptr;
+ if (auto maybeInnerLambda = GetFlatMapOverInputStream(map.Mapper()).Lambda()) {
+ innerLambdaArg = maybeInnerLambda.Cast().Args().Arg(0).Raw();
+ }
+
+ VisitExpr(map.Mapper().Body().Ref(), [&good, innerLambdaArg](const TExprNode& node) -> bool {
+ if (!good) {
+ return false;
+ }
+ if (TYtOutput::Match(&node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+ else if (TYtRowNumber::Match(&node)) {
+ good = false;
+ }
+ else if (auto p = TMaybeNode<TYtTablePath>(&node)) {
+ // Support only YtTableProps in FlatMap over input stream. Other YtTableProps cannot be properly integrated into the Switch
+ if (p.Cast().DependsOn().Input().Raw() != innerLambdaArg) {
+ good = false;
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableRecord>(&node)) {
+ if (p.Cast().DependsOn().Input().Raw() != innerLambdaArg) {
+ good = false;
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableIndex>(&node)) {
+ if (p.Cast().DependsOn().Input().Raw() != innerLambdaArg) {
+ good = false;
+ }
+ }
+ return good;
+ });
+ return good;
+}
+
+TCoLambda THorizontalJoinBase::CleanupAuxColumns(TCoLambda lambda, TExprContext& ctx) const {
+ auto maybeInnerLambda = GetFlatMapOverInputStream(lambda).Lambda();
+ if (!maybeInnerLambda) {
+ return lambda;
+ }
+
+ TExprNode::TPtr innerLambda = maybeInnerLambda.Cast().Ptr();
+ TExprNode::TPtr clonedInnerLambda = ctx.DeepCopyLambda(*innerLambda);
+ TExprNode::TPtr innerLambdaArg = TCoLambda(clonedInnerLambda).Args().Arg(0).Ptr();
+
+ TExprNode::TPtr extendedArg = ctx.NewArgument(lambda.Pos(), "extendedArg");
+
+ bool hasTablePath = false;
+ bool hasTableRecord = false;
+ bool hasTableIndex = false;
+ TNodeOnNodeOwnedMap replaces;
+ VisitExpr(*clonedInnerLambda, [&](const TExprNode& node) {
+ if (TYtOutput::Match(&node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+ if (auto p = TMaybeNode<TYtTablePath>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTablePath = true;
+ replaces[&node] = Build<TCoMember>(ctx, node.Pos())
+ .Struct(extendedArg)
+ .Name()
+ .Value("_yql_table_path")
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableRecord>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTableRecord = true;
+ replaces[&node] = Build<TCoMember>(ctx, node.Pos())
+ .Struct(extendedArg)
+ .Name()
+ .Value("_yql_table_record")
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableIndex>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTableIndex = true;
+ replaces[&node] = Build<TCoMember>(ctx, node.Pos())
+ .Struct(extendedArg)
+ .Name()
+ .Value("_yql_table_index")
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ return true;
+ });
+
+ if (replaces.empty()) {
+ return lambda;
+ }
+
+ TExprNode::TPtr cleanArg = extendedArg;
+ if (hasTablePath) {
+ cleanArg = Build<TCoForceRemoveMember>(ctx, cleanArg->Pos())
+ .Struct(cleanArg)
+ .Name()
+ .Value("_yql_table_path")
+ .Build()
+ .Done().Ptr();
+ }
+ if (hasTableRecord) {
+ cleanArg = Build<TCoForceRemoveMember>(ctx, cleanArg->Pos())
+ .Struct(cleanArg)
+ .Name()
+ .Value("_yql_table_record")
+ .Build()
+ .Done().Ptr();
+ }
+ if (hasTableIndex) {
+ cleanArg = Build<TCoForceRemoveMember>(ctx, cleanArg->Pos())
+ .Struct(cleanArg)
+ .Name()
+ .Value("_yql_table_index")
+ .Build()
+ .Done().Ptr();
+ }
+
+ replaces[innerLambdaArg.Get()] = cleanArg;
+
+ auto body = ctx.ReplaceNodes(clonedInnerLambda->TailPtr(), replaces);
+
+ return Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(lambda)
+ .With(lambda.Args().Arg(0), "stream")
+ .With(TExprBase(innerLambda), TExprBase(ctx.NewLambda(clonedInnerLambda->Pos(), ctx.NewArguments(extendedArg->Pos(), { extendedArg }), std::move(body))))
+ .Build()
+ .Done();
+}
+
+void THorizontalJoinBase::ClearJoinGroup() {
+ DataSink = {};
+ UsesTablePath.Clear();
+ UsesTableRecord.Clear();
+ UsesTableIndex.Clear();
+ OpSettings = {};
+ MemUsage.clear();
+ JoinedMaps.clear();
+ UsedFiles = 1; // jobstate
+}
+
+void THorizontalJoinBase::AddToJoinGroup(TYtMap map) {
+ if (auto maybeInnerLambda = GetFlatMapOverInputStream(map.Mapper()).Lambda()) {
+ TExprNode::TPtr innerLambda = maybeInnerLambda.Cast().Ptr();
+ TExprNode::TPtr innerLambdaArg = maybeInnerLambda.Cast().Args().Arg(0).Ptr();
+
+ bool hasTablePath = false;
+ bool hasTableRecord = false;
+ bool hasTableIndex = false;
+ VisitExpr(*innerLambda, [&](const TExprNode& node) {
+ if (TYtOutput::Match(&node)) {
+ // Stop traversing dependent operations
+ return false;
+ }
+ if (auto p = TMaybeNode<TYtTablePath>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTablePath = true;
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableRecord>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTableRecord = true;
+ }
+ }
+ else if (auto p = TMaybeNode<TYtTableIndex>(&node)) {
+ auto input = p.Cast().DependsOn().Input();
+ if (input.Ptr() == innerLambdaArg) {
+ hasTableIndex = true;
+ }
+ }
+ return true;
+ });
+
+ if (hasTablePath) {
+ UsesTablePath.Set(JoinedMaps.size());
+ }
+ if (hasTableRecord) {
+ UsesTableRecord.Set(JoinedMaps.size());
+ }
+ if (hasTableIndex) {
+ UsesTableIndex.Set(JoinedMaps.size());
+ }
+ }
+
+ if (!DataSink) {
+ DataSink = map.DataSink();
+ }
+
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered)) {
+ OpSettings |= EYtSettingType::Ordered;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields)) {
+ OpSettings |= EYtSettingType::WeakFields;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Flow)) {
+ OpSettings |= EYtSettingType::Flow;
+ }
+
+ JoinedMaps.push_back(map);
+}
+
+TCoLambda THorizontalJoinBase::BuildMapperWithAuxColumnsForSingleInput(TPositionHandle pos, bool ordered, TExprContext& ctx) const {
+ auto mapLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body("row")
+ .Done();
+
+ if (!UsesTablePath.Empty()) {
+ mapLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(mapLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_path")
+ .Build()
+ .Item<TYtTablePath>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ if (!UsesTableRecord.Empty()) {
+ mapLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(mapLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_record")
+ .Build()
+ .Item<TYtTableRecord>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ if (!UsesTableIndex.Empty()) {
+ mapLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(mapLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_index")
+ .Build()
+ .Item<TYtTableIndex>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ return Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoMapBase>()
+ .CallableName(ordered ? TCoOrderedMap::CallableName() : TCoMap::CallableName())
+ .Input("stream")
+ .Lambda(mapLambda)
+ .Build()
+ .Done();
+}
+
+TCoLambda THorizontalJoinBase::BuildMapperWithAuxColumnsForMultiInput(TPositionHandle pos, bool ordered, TExprContext& ctx) const {
+ TVector<TExprBase> tupleTypes;
+ for (size_t i: xrange(JoinedMaps.size())) {
+ auto itemType = JoinedMaps[i].Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ TVector<const TItemExprType*> extraItems;
+ if (UsesTablePath.Get(i)) {
+ extraItems.push_back(ctx.MakeType<TItemExprType>("_yql_table_path",
+ ctx.MakeType<TDataExprType>(EDataSlot::String)));
+ }
+ if (UsesTableRecord.Get(i)) {
+ extraItems.push_back(ctx.MakeType<TItemExprType>("_yql_table_record",
+ ctx.MakeType<TDataExprType>(EDataSlot::Uint64)));
+ }
+ if (UsesTableIndex.Get(i)) {
+ extraItems.push_back(ctx.MakeType<TItemExprType>("_yql_table_index",
+ ctx.MakeType<TDataExprType>(EDataSlot::Uint32)));
+ }
+ if (!extraItems.empty()) {
+ auto items = itemType->Cast<TStructExprType>()->GetItems();
+ items.insert(items.end(), extraItems.begin(), extraItems.end());
+ itemType = ctx.MakeType<TStructExprType>(items);
+ }
+
+ tupleTypes.push_back(TExprBase(ExpandType(pos, *itemType, ctx)));
+ }
+ TExprBase varType = Build<TCoVariantType>(ctx, pos)
+ .UnderlyingType<TCoTupleType>()
+ .Add(tupleTypes)
+ .Build()
+ .Done();
+
+ TVector<TExprBase> visitArgs;
+ for (size_t i: xrange(JoinedMaps.size())) {
+ visitArgs.push_back(Build<TCoAtom>(ctx, pos).Value(ToString(i)).Done());
+ auto visitLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body("row")
+ .Done();
+
+ if (UsesTablePath.Get(i)) {
+ visitLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(visitLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_path")
+ .Build()
+ .Item<TYtTablePath>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ if (UsesTableRecord.Get(i)) {
+ visitLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(visitLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_record")
+ .Build()
+ .Item<TYtTableRecord>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ if (UsesTableIndex.Get(i)) {
+ visitLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoAddMember>()
+ .Struct<TExprApplier>()
+ .Apply(visitLambda)
+ .With(0, "row")
+ .Build()
+ .Name()
+ .Value("_yql_table_index")
+ .Build()
+ .Item<TYtTableIndex>()
+ .DependsOn()
+ .Input("row")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ visitArgs.push_back(Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoVariant>()
+ .Item<TExprApplier>()
+ .Apply(visitLambda)
+ .With(0, "row")
+ .Build()
+ .Index()
+ .Value(ToString(i))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Done());
+ }
+
+ return Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoMapBase>()
+ .CallableName(ordered ? TCoOrderedMap::CallableName() : TCoMap::CallableName())
+ .Input("stream")
+ .Lambda()
+ .Args({"var"})
+ .Body<TCoVisit>()
+ .Input("var")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+}
+
+TCoLambda THorizontalJoinBase::MakeSwitchLambda(size_t mapIndex, size_t fieldsCount, bool singleInput, TExprContext& ctx) const {
+ auto map = JoinedMaps[mapIndex];
+ auto lambda = map.Mapper();
+
+ if (UsesTablePath.Get(mapIndex) || UsesTableRecord.Get(mapIndex) || UsesTableIndex.Get(mapIndex)) {
+ lambda = CleanupAuxColumns(lambda, ctx);
+ }
+
+ if (singleInput) {
+ auto inputItemType = map.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ if (inputItemType->GetSize() != fieldsCount || !UsesTablePath.Empty() || !UsesTableRecord.Empty() || !UsesTableIndex.Empty()) {
+ TVector<TStringBuf> fields;
+ for (auto itemType: inputItemType->GetItems()) {
+ fields.push_back(itemType->GetName());
+ }
+ if (UsesTablePath.Get(mapIndex)) {
+ fields.push_back("_yql_table_path");
+ }
+ if (UsesTableRecord.Get(mapIndex)) {
+ fields.push_back("_yql_table_record");
+ }
+ if (UsesTableIndex.Get(mapIndex)) {
+ fields.push_back("_yql_table_index");
+ }
+
+ auto [placeHolder, lambdaWithPlaceholder] = ReplaceDependsOn(lambda.Ptr(), ctx, State_->Types);
+ YQL_ENSURE(placeHolder);
+
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(TCoLambda(lambdaWithPlaceholder))
+ .With<TCoExtractMembers>(0)
+ .Input("stream")
+ .Members(ToAtomList(fields, lambda.Pos(), ctx))
+ .Build()
+ .With(TExprBase(placeHolder), "stream")
+ .Build()
+ .Done();
+ }
+ }
+
+ return lambda;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+IGraphTransformer::TStatus THorizontalJoinOptimizer::Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) {
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = ProcessedNodes;
+ return OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (auto maybeRead = TMaybeNode<TCoRight>(node).Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ auto readInput = read.Input().Ptr();
+ auto newInput = HandleList(readInput, true, ctx);
+ if (newInput != readInput) {
+ if (newInput) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-HorizontalJoin";
+ return ctx.ChangeChild(*node, TCoRight::idx_Input,
+ ctx.ChangeChild(read.Ref(), TYtReadTable::idx_Input, std::move(newInput))
+ );
+ }
+ return {};
+ }
+ }
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(node)) {
+ if (!node->HasResult() || node->GetResult().Type() != TExprNode::World) {
+ auto op = maybeOp.Cast();
+ auto opInput = op.Input().Ptr();
+ auto newInput = HandleList(opInput, true, ctx);
+ if (newInput != opInput) {
+ if (newInput) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-HorizontalJoin";
+ return ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Input, std::move(newInput));
+ }
+ return {};
+ }
+ }
+ }
+ if (auto maybePublish = TMaybeNode<TYtPublish>(node)) {
+ if (TExprNode::EState::ExecutionComplete != node->GetState()
+ && TExprNode::EState::ExecutionInProgress != node->GetState()) {
+ auto publish = maybePublish.Cast();
+ auto pubInput = publish.Input().Ptr();
+ if (pubInput->ChildrenSize() > 1) {
+ auto newInput = HandleList(pubInput, false, ctx);
+ if (newInput != pubInput) {
+ if (newInput) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-HorizontalJoin";
+ return ctx.ChangeChild(publish.Ref(), TYtPublish::idx_Input, std::move(newInput));
+ }
+ return {};
+ }
+ }
+ }
+ }
+ return node;
+ }, ctx, settings);
+}
+
+TExprNode::TPtr THorizontalJoinOptimizer::HandleList(const TExprNode::TPtr& node, bool sectionList, TExprContext& ctx) {
+ auto groups = CollectGroups(node, sectionList);
+ if (groups.empty()) {
+ return node;
+ }
+
+ for (auto& group: groups) {
+ if (group.second.size() < 2) {
+ continue;
+ }
+
+ ClearJoinGroup();
+
+ for (auto& mapGrp: group.second) {
+ TYtMap map = std::get<0>(mapGrp);
+ const size_t sectionNum = std::get<1>(mapGrp);
+ const size_t pathNum = std::get<2>(mapGrp);
+
+ size_t outNdx = JoinedMaps.size();
+ const TExprNode* columns = nullptr;
+ const TExprNode* ranges = nullptr;
+ if (sectionList) {
+ auto path = TYtSection(node->Child(sectionNum)).Paths().Item(pathNum);
+ columns = path.Columns().Raw();
+ ranges = path.Ranges().Raw();
+ }
+
+ auto uniqIt = UniqMaps.find(map.Raw());
+ if (uniqIt != UniqMaps.end()) {
+ // Map output is used multiple times
+ // Move all {section,path} pairs to ExclusiveOuts
+ ExclusiveOuts[uniqIt->second].emplace_back(sectionNum, pathNum);
+
+ auto it = GroupedOuts.find(std::make_tuple(sectionNum, columns, ranges));
+ if (it != GroupedOuts.end()) {
+ auto itOut = it->second.find(uniqIt->second);
+ if (itOut != it->second.end()) {
+ ExclusiveOuts[uniqIt->second].emplace_back(sectionNum, itOut->second);
+ it->second.erase(itOut);
+ }
+ if (it->second.empty()) {
+ GroupedOuts.erase(it);
+ }
+ }
+ continue;
+ }
+
+ const size_t mapInputCount = map.Input().Item(0).Paths().Size();
+ if (InputCount + mapInputCount > MaxTables) {
+ if (MakeJoinedMap(node->Pos(), ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "HorizontalJoin: split by max input tables: " << (InputCount + mapInputCount);
+ }
+ // Reinit outNdx because MakeJoinedMap() clears JoinedMaps
+ outNdx = JoinedMaps.size();
+ }
+
+ size_t outputCountIncrement = 0;
+ auto outRowSpec = TYtTableBaseInfo::GetRowSpec(map.Output().Item(0));
+ const bool sortedOut = outRowSpec && outRowSpec->IsSorted();
+
+ if (sortedOut) {
+ if (ExclusiveOuts.find(outNdx) == ExclusiveOuts.end()) {
+ // Sorted output cannot be joined with others
+ outputCountIncrement = 1;
+ }
+ }
+ else if (GroupedOuts.find(std::make_tuple(sectionNum, columns, ranges)) == GroupedOuts.end()) {
+ outputCountIncrement = 1;
+ }
+
+ if (ExclusiveOuts.size() + GroupedOuts.size() + outputCountIncrement > MaxOutTables) {
+ // MakeJoinedMap() clears ExclusiveOuts and GroupedOuts
+ const auto outCount = ExclusiveOuts.size() + GroupedOuts.size();
+ if (MakeJoinedMap(node->Pos(), ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "HorizontalJoin: split by max output tables: " << outCount;
+ }
+ outputCountIncrement = 1;
+ // Reinit outNdx because MakeJoinedMap() clears JoinedMaps
+ outNdx = JoinedMaps.size();
+ }
+
+ TExprNode::TPtr updatedLambda = map.Mapper().Ptr();
+ if (MaxJobMemoryLimit) {
+ auto status = UpdateTableContentMemoryUsage(map.Mapper().Ptr(), updatedLambda, State_, ctx);
+ if (status.Level != IGraphTransformer::TStatus::Ok) {
+ return {};
+ }
+ }
+
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ auto currMemory = Accumulate(MemUsage.begin(), MemUsage.end(), SwitchMemoryLimit,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ // Take into account codec input/output buffers (one for all inputs and one per output)
+ currMemory += YQL_JOB_CODEC_MEM * (ExclusiveOuts.size() + GroupedOuts.size() + outputCountIncrement + 1);
+
+ if ((MaxJobMemoryLimit && currMemory > *MaxJobMemoryLimit) || UsedFiles > MaxOperationFiles) {
+ const auto usedFiles = UsedFiles; // Save value, because MakeJoinedMap will clear it
+ if (MakeJoinedMap(node->Pos(), ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "HorizontalJoin: split by limits. Memory: " << currMemory << ", files: " << usedFiles;
+ }
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ // Reinit outNdx because MakeJoinedMap() clears joinedMapOuts
+ outNdx = JoinedMaps.size();
+ }
+
+ InputCount += mapInputCount;
+
+ if (map.World().Ref().Type() != TExprNode::World) {
+ Worlds.emplace(map.World().Ptr(), Worlds.size());
+ }
+
+ if (sortedOut) {
+ ExclusiveOuts[outNdx].emplace_back(sectionNum, pathNum);
+ } else {
+ GroupedOuts[std::make_tuple(sectionNum, columns, ranges)][outNdx] = pathNum;
+ }
+ UniqMaps.emplace(map.Raw(), outNdx);
+
+ auto section = map.Input().Item(0);
+ if (section.Paths().Size() == 1) {
+ auto path = section.Paths().Item(0);
+ GroupedInputs.emplace(path.Table().Raw(), path.Ranges().Raw(), section.Settings().Raw(), NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields));
+ } else {
+ GroupedInputs.emplace(section.Raw(), nullptr, nullptr, NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields));
+ }
+
+ AddToJoinGroup(map);
+ }
+ MakeJoinedMap(node->Pos(), ctx);
+ }
+
+ if (InputSubsts.empty()) {
+ return node;
+ }
+
+ return RebuildList(node, sectionList, ctx);
+}
+
+void THorizontalJoinOptimizer::AddToGroups(TYtMap map, size_t s, size_t p, const TExprNode* section,
+ THashMap<TGroupKey, TVector<std::tuple<TYtMap, size_t, size_t>>>& groups,
+ TNodeMap<TMaybe<TGroupKey>>& processedMaps) const
+{
+ auto processedIt = processedMaps.find(map.Raw());
+ if (processedIt != processedMaps.end()) {
+ if (processedIt->second) {
+ groups[*processedIt->second].emplace_back(map, s, p);
+ }
+ return;
+ }
+
+ bool good = IsGoodForHorizontalJoin(map);
+
+ // Map output has multiple readers
+ if (good) {
+ auto readersIt = OpDeps.find(map.Raw());
+ if (readersIt == OpDeps.end()) {
+ good = false;
+ }
+ else if (readersIt->second.size() != 1) {
+ // Allow multiple readers from the same section
+ for (auto& r: readersIt->second) {
+ if (std::get<1>(r) != section) {
+ good = false;
+ break;
+ }
+ }
+ }
+ }
+
+ // Gather not yet completed dependencies (input data and world). Not more than one is allowed
+ const TExprNode* dep = nullptr;
+ bool yamr = false;
+ bool qb2 = false;
+ if (good) {
+ TNodeSet deps;
+ for (auto path: map.Input().Item(0).Paths()) {
+ yamr = path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+ if (auto setting = path.Table().Maybe<TYtTable>().Settings()) {
+ qb2 = qb2 || NYql::HasSetting(setting.Ref(), EYtSettingType::WithQB);
+ }
+ if (auto op = path.Table().Maybe<TYtOutput>().Operation()) {
+ const TExprNode* opNode = op.Cast().Raw();
+ if (opNode->GetState() != TExprNode::EState::ExecutionComplete
+ || !opNode->HasResult()
+ || opNode->GetResult().Type() != TExprNode::World) {
+ deps.insert(opNode);
+ }
+ }
+ }
+ if (map.World().Ref().Type() != TExprNode::World
+ && map.World().Ref().GetState() != TExprNode::EState::ExecutionComplete) {
+ deps.insert(map.World().Raw());
+ }
+
+ if (deps.size() > 1) {
+ good = false;
+ } else if (deps.size() == 1) {
+ dep = *deps.begin();
+ }
+ }
+
+ if (good) {
+ ui32 flags = 0;
+ if (yamr) {
+ flags |= EFeatureFlags::YAMR;
+ }
+ if (qb2) {
+ flags |= EFeatureFlags::QB2;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Flow)) {
+ flags |= EFeatureFlags::Flow;
+ }
+ auto key = TGroupKey {
+ TString{map.DataSink().Cluster().Value()},
+ NYql::GetSampleParams(map.Input().Item(0).Settings().Ref()),
+ dep,
+ flags
+ };
+ groups[key].emplace_back(map, s, p);
+ processedMaps.emplace_hint(processedIt, map.Raw(), key);
+ } else {
+ processedMaps.emplace_hint(processedIt, map.Raw(), Nothing());
+ }
+}
+
+THashMap<THorizontalJoinOptimizer::TGroupKey, TVector<std::tuple<TYtMap, size_t, size_t>>> THorizontalJoinOptimizer::CollectGroups(const TExprNode::TPtr& node, bool sectionList) const {
+
+ THashMap<TGroupKey, TVector<std::tuple<TYtMap, size_t, size_t>>> groups; // group key -> Vector{Map, section_num, path_num}
+ TNodeMap<TMaybe<TGroupKey>> processedMaps;
+
+ if (sectionList) {
+ auto sections = TYtSectionList(node);
+ for (size_t s = 0; s < sections.Size(); ++s) {
+ auto section = sections.Item(s);
+ for (size_t p = 0, pathCount = section.Paths().Size(); p < pathCount; ++p) {
+ auto path = section.Paths().Item(p);
+ if (auto maybeMap = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtMap>()) {
+ AddToGroups(maybeMap.Cast(), s, p, section.Raw(), groups, processedMaps);
+ }
+ }
+ }
+ } else {
+ auto list = TYtOutputList(node);
+ for (size_t s = 0; s < list.Size(); ++s) {
+ auto out = list.Item(s);
+ if (auto maybeMap = out.Operation().Maybe<TYtMap>()) {
+ // Treat each output as a separate section to prevent output concat
+ AddToGroups(maybeMap.Cast(), s, 0, nullptr, groups, processedMaps);
+ }
+ }
+ }
+
+ return groups;
+}
+
+void THorizontalJoinOptimizer::ClearJoinGroup() {
+ THorizontalJoinBase::ClearJoinGroup();
+ Worlds.clear();
+ UniqMaps.clear();
+ GroupedOuts.clear();
+ ExclusiveOuts.clear();
+ InputCount = 0;
+ GroupedInputs.clear();
+}
+
+bool THorizontalJoinOptimizer::MakeJoinedMap(TPositionHandle pos, TExprContext& ctx) {
+ bool res = false;
+ if (JoinedMaps.size() > 1) {
+
+ const bool singleInput = GroupedInputs.size() == 1;
+ const bool ordered = OpSettings.HasFlags(EYtSettingType::Ordered);
+
+ TCoLambda mapper = (!UsesTablePath.Empty() || !UsesTableRecord.Empty() || !UsesTableIndex.Empty())
+ ? (singleInput ? BuildMapperWithAuxColumnsForSingleInput(pos, ordered, ctx) : BuildMapperWithAuxColumnsForMultiInput(pos, ordered, ctx))
+ : Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body("stream")
+ .Done();
+
+ TSet<TStringBuf> usedFields;
+ TSet<TStringBuf> weakFields;
+ if (singleInput) {
+ for (auto map: JoinedMaps) {
+ for (auto itemType: map.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ usedFields.insert(itemType->GetName());
+ }
+ if (std::get<3>(*GroupedInputs.begin())) {
+ for (auto path: map.Input().Item(0).Paths()) {
+ if (auto columns = path.Columns().Maybe<TExprList>()) {
+ for (auto child: columns.Cast()) {
+ if (auto maybeTuple = child.Maybe<TCoAtomList>()) {
+ auto tuple = maybeTuple.Cast();
+ if (tuple.Item(1).Value() == "weak") {
+ weakFields.insert(tuple.Item(0).Value());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ TVector<TExprBase> switchArgs;
+ for (size_t i: xrange(JoinedMaps.size())) {
+ auto lambda = MakeSwitchLambda(i, usedFields.size(), singleInput, ctx);
+
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, lambda.Pos())
+ .Add()
+ .Value(singleInput ? TString("0") : ToString(i))
+ .Build()
+ .Done()
+ );
+ switchArgs.push_back(lambda);
+ }
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (OpSettings.HasFlags(EYtSettingType::Ordered)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Ordered)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::WeakFields)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::WeakFields)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::Flow)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Flow)).Build()
+ .Build();
+ }
+
+ mapper = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoSwitch>()
+ .Input<TExprApplier>()
+ .Apply(mapper)
+ .With(0, "stream")
+ .Build()
+ .BufferBytes()
+ .Value(ToString(SwitchMemoryLimit))
+ .Build()
+ .FreeArgs()
+ .Add(switchArgs)
+ .Build()
+ .Build()
+ .Done();
+
+ // outRemap[oldOutIndex] -> {newOutIndex, drop_flag, TVector{section_num, path_num}}
+ TVector<std::tuple<size_t, bool, TVector<std::pair<size_t, size_t>>>> outRemap;
+ outRemap.resize(JoinedMaps.size());
+ const bool lessOuts = ExclusiveOuts.size() + GroupedOuts.size() < JoinedMaps.size();
+ size_t nextNewOutIndex = 0;
+ TVector<TYtOutTable> joinedMapOuts;
+ for (auto& out: ExclusiveOuts) {
+ auto old_out_num = out.first;
+ auto& inputs = out.second;
+ if (lessOuts) {
+ outRemap[old_out_num] = std::make_tuple(nextNewOutIndex++, false, inputs);
+ joinedMapOuts.push_back(JoinedMaps[old_out_num].Output().Item(0));
+ } else {
+ outRemap[old_out_num] = std::make_tuple(old_out_num, false, inputs);
+ }
+ }
+ for (auto& out: GroupedOuts) {
+ auto& grp = out.first;
+ auto& inputs = out.second;
+ TMaybe<size_t> newIndex;
+ size_t baseOldIndex = 0;
+ TVector<std::pair<size_t, size_t>> pairsSecPath;
+ for (auto in: inputs) {
+ auto old_out_num = in.first;
+ auto path_num = in.second;
+ if (!newIndex) {
+ newIndex = nextNewOutIndex++;
+ baseOldIndex = old_out_num;
+ }
+ std::get<0>(outRemap[old_out_num]) = *newIndex;
+ pairsSecPath.emplace_back(std::get<0>(grp), path_num);
+ }
+ if (lessOuts) {
+ outRemap[baseOldIndex] = std::make_tuple(*newIndex, true, std::move(pairsSecPath));
+ joinedMapOuts.push_back(JoinedMaps[baseOldIndex].Output().Item(0));
+ } else {
+ YQL_ENSURE(pairsSecPath.size() == 1);
+ outRemap[baseOldIndex] = std::make_tuple(baseOldIndex, true, std::move(pairsSecPath));
+ }
+ }
+
+ if (lessOuts) {
+ YQL_ENSURE(ExclusiveOuts.size() + GroupedOuts.size() == nextNewOutIndex, "Output table count mismatch: " << (ExclusiveOuts.size() + GroupedOuts.size()) << " != " << nextNewOutIndex);
+ if (nextNewOutIndex == 1) {
+ mapper = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(mapper)
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVariantItem>()
+ .Variant("item")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ else if (nextNewOutIndex < outRemap.size()) {
+ TVector<TExprBase> tupleTypes;
+ for (auto out: joinedMapOuts) {
+ auto itemType = out.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ tupleTypes.push_back(TExprBase(ExpandType(pos, *itemType, ctx)));
+ }
+ TExprBase varType = Build<TCoVariantType>(ctx, pos)
+ .UnderlyingType<TCoTupleType>()
+ .Add(tupleTypes)
+ .Build()
+ .Done();
+
+ TVector<TExprBase> visitArgs;
+ for (size_t i: xrange(outRemap.size())) {
+ visitArgs.push_back(Build<TCoAtom>(ctx, pos).Value(ToString(i)).Done());
+ visitArgs.push_back(Build<TCoLambda>(ctx, pos)
+ .Args({"row"})
+ .Body<TCoVariant>()
+ .Item("row")
+ .Index()
+ .Value(ToString(std::get<0>(outRemap[i])))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Done());
+ }
+
+ mapper = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(mapper)
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ } else {
+ for (auto map: JoinedMaps) {
+ joinedMapOuts.push_back(map.Output().Item(0));
+ }
+ }
+
+ TVector<TYtSection> joinedMapSections;
+ if (singleInput) {
+ auto section = JoinedMaps.front().Input().Item(0);
+ auto itemType = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+
+ TMaybeNode<TExprBase> columns;
+ if (!weakFields.empty()) {
+ auto columnsBuilder = Build<TExprList>(ctx, pos);
+ for (auto& field: usedFields) {
+ if (weakFields.contains(field)) {
+ columnsBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(field)
+ .Build()
+ .Add()
+ .Value("weak")
+ .Build()
+ .Build();
+ }
+ else {
+ columnsBuilder
+ .Add<TCoAtom>()
+ .Value(field)
+ .Build();
+ }
+ }
+ columns = columnsBuilder.Done();
+ }
+ else if (usedFields.size() != itemType->GetSize()) {
+ columns = TExprBase(ToAtomList(usedFields, pos, ctx));
+ }
+
+ if (columns) {
+ TVector<TYtPath> paths;
+ for (const auto& path : section.Paths()) {
+ paths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Columns(columns.Cast())
+ .Stat<TCoVoid>().Build()
+ .Done());
+ }
+
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Done();
+ }
+ joinedMapSections.push_back(section);
+ }
+ else {
+ for (auto map: JoinedMaps) {
+ joinedMapSections.push_back(map.Input().Item(0));
+ }
+ }
+
+ auto joinedMap = Build<TYtMap>(ctx, pos)
+ .World(NYql::ApplySyncListToWorld(ctx.NewWorld(pos), Worlds, ctx))
+ .DataSink(DataSink.Cast())
+ .Output()
+ .Add(joinedMapOuts)
+ .Build()
+ .Input()
+ .Add(joinedMapSections)
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Done();
+
+ // {section_num, path_num} -> {joined_map, out_num}
+ for (auto& r: outRemap) {
+ auto outIndex = std::get<0>(r);
+ auto dropFlag = std::get<1>(r);
+ auto& inputs = std::get<2>(r);
+ if (!inputs.empty()) {
+ InputSubsts.emplace(inputs.front(), std::make_pair(joinedMap, outIndex));
+ if (dropFlag) {
+ for (size_t i = 1; i < inputs.size(); ++i) {
+ InputSubsts.emplace(inputs[i], Nothing());
+ }
+ }
+ else {
+ for (size_t i = 1; i < inputs.size(); ++i) {
+ InputSubsts.emplace(inputs[i], std::make_pair(joinedMap, outIndex));
+ }
+ }
+ }
+ }
+ res = true;
+ }
+
+ ClearJoinGroup();
+ return res;
+}
+
+TExprNode::TPtr THorizontalJoinOptimizer::RebuildList(const TExprNode::TPtr& node, bool sectionList, TExprContext& ctx) {
+ TExprNode::TPtr res;
+ if (sectionList) {
+ TVector<TYtSection> updatedSections;
+ for (size_t s = 0; s < node->ChildrenSize(); ++s) {
+ auto section = TYtSection(node->ChildPtr(s));
+ updatedSections.push_back(section);
+
+ TVector<TYtPath> updatedPaths;
+ bool hasUpdatedPaths = false;
+ for (size_t p = 0, pathCount = section.Paths().Size(); p < pathCount; ++p) {
+ auto path = section.Paths().Item(p);
+ auto it = InputSubsts.find(std::make_pair(s, p));
+ if (it != InputSubsts.end()) {
+ if (it->second.Defined()) {
+ updatedPaths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .Operation(it->second->first)
+ .OutIndex()
+ .Value(ToString(it->second->second))
+ .Build()
+ .Mode(path.Table().Maybe<TYtOutput>().Mode())
+ .Build()
+ .Done());
+ }
+ hasUpdatedPaths = true;
+ }
+ else {
+ updatedPaths.push_back(path);
+ }
+ }
+
+ if (hasUpdatedPaths) {
+ updatedSections.back() = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Done();
+ }
+ }
+ res = Build<TYtSectionList>(ctx, node->Pos()).Add(updatedSections).Done().Ptr();
+ }
+ else {
+ TVector<TYtOutput> updatedOuts;
+ for (size_t s = 0; s < node->ChildrenSize(); ++s) {
+ auto out = TYtOutput(node->ChildPtr(s));
+
+ auto it = InputSubsts.find(std::make_pair(s, 0));
+ if (it != InputSubsts.end()) {
+ if (it->second.Defined()) {
+ updatedOuts.push_back(Build<TYtOutput>(ctx, out.Pos())
+ .Operation(it->second->first)
+ .OutIndex()
+ .Value(ToString(it->second->second))
+ .Build()
+ .Mode(out.Mode())
+ .Done());
+ }
+ }
+ else {
+ updatedOuts.push_back(out);
+ }
+ }
+ res = Build<TYtOutputList>(ctx, node->Pos()).Add(updatedOuts).Done().Ptr();
+ }
+ InputSubsts.clear();
+ return res;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+IGraphTransformer::TStatus TMultiHorizontalJoinOptimizer::Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) {
+ THashMap<TGroupKey, TVector<TYtMap>> mapGroups;
+
+ for (auto& x: OpDeps) {
+ auto writer = x.first;
+ if (auto maybeMap = TMaybeNode<TYtMap>(writer)) {
+ auto map = maybeMap.Cast();
+ if (IsGoodForHorizontalJoin(map)) {
+ bool good = true;
+ const TExprNode* dep = nullptr;
+ bool yamr = false;
+ bool qb2 = false;
+ TNodeSet deps;
+ for (auto path: map.Input().Item(0).Paths()) {
+ yamr = path.Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+ if (auto setting = path.Table().Maybe<TYtTable>().Settings()) {
+ qb2 = qb2 || NYql::HasSetting(setting.Ref(), EYtSettingType::WithQB);
+ }
+ if (auto op = path.Table().Maybe<TYtOutput>().Operation()) {
+ const TExprNode* opNode = op.Cast().Raw();
+ if (opNode->GetState() != TExprNode::EState::ExecutionComplete
+ || !opNode->HasResult()
+ || opNode->GetResult().Type() != TExprNode::World) {
+ deps.insert(opNode);
+ }
+ }
+ }
+ if (map.World().Ref().Type() != TExprNode::World
+ && map.World().Ref().GetState() != TExprNode::EState::ExecutionComplete) {
+ deps.insert(map.World().Raw());
+ }
+
+ if (deps.size() > 1) {
+ good = false;
+ } else if (deps.size() == 1) {
+ dep = *deps.begin();
+ }
+
+ if (good) {
+ std::set<ui64> readerIds;
+ for (auto& reader: x.second) {
+ readerIds.insert(std::get<0>(reader)->UniqueId());
+ }
+ ui32 flags = 0;
+ if (yamr) {
+ flags |= EFeatureFlags::YAMR;
+ }
+ if (qb2) {
+ flags |= EFeatureFlags::QB2;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Flow)) {
+ flags |= EFeatureFlags::Flow;
+ }
+ auto key = TGroupKey {
+ TString{map.DataSink().Cluster().Value()},
+ readerIds,
+ NYql::GetSampleParams(map.Input().Item(0).Settings().Ref()),
+ dep,
+ flags
+ };
+ mapGroups[key].push_back(map);
+ }
+ }
+ }
+ }
+ if (mapGroups.empty()) {
+ return IGraphTransformer::TStatus::Ok;
+ }
+
+ for (auto& grp: mapGroups) {
+ if (grp.second.size() > 1) {
+ ::Sort(grp.second.begin(), grp.second.end(),
+ [](const TYtMap& m1, const TYtMap& m2) { return m1.Ref().UniqueId() < m2.Ref().UniqueId(); });
+
+ if (!HandleGroup(grp.second, ctx)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ if (!OutputSubsts.empty()) {
+ break;
+ }
+ }
+ }
+
+ if (!OutputSubsts.empty()) {
+ TNodeOnNodeOwnedMap toOptimize;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto maybeOut = TMaybeNode<TYtOutput>(node)) {
+ auto out = maybeOut.Cast();
+ auto it = OutputSubsts.find(out.Operation().Raw());
+ if (it != OutputSubsts.end()) {
+ YQL_ENSURE(out.OutIndex().Value() == "0");
+ toOptimize[node.Get()] = Build<TYtOutput>(ctx, out.Pos())
+ .Operation(it->second.first)
+ .OutIndex()
+ .Value(ToString(it->second.second))
+ .Build()
+ .Mode(out.Mode())
+ .Done().Ptr();
+ }
+ }
+ return true;
+ });
+
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-MultiHorizontalJoin";
+ return RemapExpr(input, output, toOptimize, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ return IGraphTransformer::TStatus::Ok;
+}
+
+bool TMultiHorizontalJoinOptimizer::HandleGroup(const TVector<TYtMap>& maps, TExprContext& ctx) {
+ for (TYtMap map: maps) {
+ const size_t mapInputCount = map.Input().Item(0).Paths().Size();
+ if (InputCount + mapInputCount > MaxTables) {
+ if (MakeJoinedMap(ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "MultiHorizontalJoin: split by max input tables: " << (InputCount + mapInputCount);
+ }
+ }
+
+ if (JoinedMaps.size() + 1 > MaxOutTables) {
+ if (MakeJoinedMap(ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "MultiHorizontalJoin: split by max output tables: " << (JoinedMaps.size() + 1);
+ }
+ }
+
+ TExprNode::TPtr updatedLambda = map.Mapper().Ptr();
+ if (MaxJobMemoryLimit) {
+ if (UpdateTableContentMemoryUsage(map.Mapper().Ptr(), updatedLambda, State_, ctx).Level != IGraphTransformer::TStatus::Ok) {
+ return false;
+ }
+ }
+
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ auto currMemory = Accumulate(MemUsage.begin(), MemUsage.end(), SwitchMemoryLimit,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ // Take into account codec input/output buffers (one for all inputs and one per output)
+ currMemory += YQL_JOB_CODEC_MEM * (JoinedMaps.size() + 2);
+
+ if ((MaxJobMemoryLimit && currMemory > *MaxJobMemoryLimit) || UsedFiles > MaxOperationFiles) {
+ const auto usedFiles = UsedFiles; // Save value, because MakeJoinedMap will clear it
+ if (MakeJoinedMap(ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "MultiHorizontalJoin: split by limits. Memory: " << currMemory << ", files: " << usedFiles;
+ }
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ }
+
+ InputCount += mapInputCount;
+
+ if (map.World().Ref().Type() != TExprNode::World) {
+ Worlds.emplace(map.World().Ptr(), Worlds.size());
+ }
+
+ auto section = map.Input().Item(0);
+ if (section.Paths().Size() == 1) {
+ auto path = section.Paths().Item(0);
+ GroupedInputs.emplace(path.Table().Raw(), path.Ranges().Raw(), section.Settings().Raw(), NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields));
+ } else {
+ GroupedInputs.emplace(section.Raw(), nullptr, nullptr, NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields));
+ }
+
+ AddToJoinGroup(map);
+ }
+
+ MakeJoinedMap(ctx);
+ return true;
+};
+
+void TMultiHorizontalJoinOptimizer::ClearJoinGroup() {
+ THorizontalJoinBase::ClearJoinGroup();
+ Worlds.clear();
+ InputCount = 0;
+ GroupedInputs.clear();
+}
+
+bool TMultiHorizontalJoinOptimizer::MakeJoinedMap(TExprContext& ctx) {
+ bool res = false;
+ if (JoinedMaps.size() > 1) {
+ TPositionHandle pos = JoinedMaps.front().Ref().Pos();
+
+ const bool singleInput = GroupedInputs.size() == 1;
+ const bool ordered = OpSettings.HasFlags(EYtSettingType::Ordered);
+
+ TCoLambda mapper = (!UsesTablePath.Empty() || !UsesTableRecord.Empty() || !UsesTableIndex.Empty())
+ ? (singleInput ? BuildMapperWithAuxColumnsForSingleInput(pos, ordered, ctx) : BuildMapperWithAuxColumnsForMultiInput(pos, ordered, ctx))
+ : Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body("stream")
+ .Done();
+
+ TSet<TStringBuf> usedFields;
+ TSet<TStringBuf> weakFields;
+ if (singleInput) {
+ for (auto map: JoinedMaps) {
+ for (auto itemType: map.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ usedFields.insert(itemType->GetName());
+ }
+ if (std::get<3>(*GroupedInputs.begin())) {
+ for (auto path: map.Input().Item(0).Paths()) {
+ if (auto columns = path.Columns().Maybe<TExprList>()) {
+ for (auto child: columns.Cast()) {
+ if (auto maybeTuple = child.Maybe<TCoAtomList>()) {
+ auto tuple = maybeTuple.Cast();
+ if (tuple.Item(1).Value() == "weak") {
+ weakFields.insert(tuple.Item(0).Value());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ TVector<TExprBase> switchArgs;
+ for (size_t i: xrange(JoinedMaps.size())) {
+ auto lambda = MakeSwitchLambda(i, usedFields.size(), singleInput, ctx);
+
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, lambda.Pos())
+ .Add()
+ .Value(singleInput ? TString("0") : ToString(i))
+ .Build()
+ .Done()
+ );
+ switchArgs.push_back(lambda);
+ }
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (OpSettings.HasFlags(EYtSettingType::Ordered)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Ordered)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::WeakFields)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::WeakFields)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::Flow)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Flow)).Build()
+ .Build();
+ }
+
+ mapper = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoSwitch>()
+ .Input<TExprApplier>()
+ .Apply(mapper)
+ .With(0, "stream")
+ .Build()
+ .BufferBytes()
+ .Value(ToString(SwitchMemoryLimit))
+ .Build()
+ .FreeArgs()
+ .Add(switchArgs)
+ .Build()
+ .Build()
+ .Done();
+
+ TVector<TYtOutTable> joinedMapOuts;
+ for (auto map: JoinedMaps) {
+ joinedMapOuts.push_back(map.Output().Item(0));
+ }
+
+ TVector<TYtSection> joinedMapSections;
+ if (singleInput) {
+ auto section = JoinedMaps.front().Input().Item(0);
+ auto itemType = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+
+ TMaybeNode<TExprBase> columns;
+ if (!weakFields.empty()) {
+ auto columnsBuilder = Build<TExprList>(ctx, pos);
+ for (auto& field: usedFields) {
+ if (weakFields.contains(field)) {
+ columnsBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(field)
+ .Build()
+ .Add()
+ .Value("weak")
+ .Build()
+ .Build();
+ }
+ else {
+ columnsBuilder
+ .Add<TCoAtom>()
+ .Value(field)
+ .Build();
+ }
+ }
+ columns = columnsBuilder.Done();
+ }
+ else if (usedFields.size() != itemType->GetSize()) {
+ columns = TExprBase(ToAtomList(usedFields, pos, ctx));
+ }
+
+ if (columns) {
+ TVector<TYtPath> paths;
+ for (const auto& path : section.Paths()) {
+ paths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Columns(columns.Cast())
+ .Stat<TCoVoid>().Build()
+ .Done());
+ }
+
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Done();
+ }
+ joinedMapSections.push_back(section);
+ }
+ else {
+ for (auto map: JoinedMaps) {
+ joinedMapSections.push_back(map.Input().Item(0));
+ }
+ }
+
+ auto joinedMap = Build<TYtMap>(ctx, pos)
+ .World(NYql::ApplySyncListToWorld(ctx.NewWorld(pos), Worlds, ctx))
+ .DataSink(DataSink.Cast())
+ .Output()
+ .Add(joinedMapOuts)
+ .Build()
+ .Input()
+ .Add(joinedMapSections)
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Done();
+
+ for (size_t i: xrange(JoinedMaps.size())) {
+ auto map = JoinedMaps[i];
+ OutputSubsts.emplace(map.Raw(), std::make_pair(joinedMap, i));
+ }
+
+ res = true;
+ }
+
+ ClearJoinGroup();
+ return res;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void TOutHorizontalJoinOptimizer::ClearJoinGroup() {
+ THorizontalJoinBase::ClearJoinGroup();
+ UsedFields.clear();
+ WeakFields.clear();
+ UsedSysFields.clear();
+}
+
+bool TOutHorizontalJoinOptimizer::MakeJoinedMap(TPositionHandle pos, const TGroupKey& key, const TStructExprType* itemType, TExprContext& ctx) {
+ bool res = false;
+ if (JoinedMaps.size() > 1) {
+ const bool ordered = OpSettings.HasFlags(EYtSettingType::Ordered);
+ auto mapper = (!UsesTablePath.Empty() || !UsesTableRecord.Empty() || !UsesTableIndex.Empty())
+ ? BuildMapperWithAuxColumnsForSingleInput(pos, ordered, ctx)
+ : Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body("stream")
+ .Done();
+
+ TVector<TExprBase> switchArgs;
+ for (size_t i: xrange(JoinedMaps.size())) {
+
+ auto lambda = MakeSwitchLambda(i, UsedFields.size(), true, ctx);
+
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, lambda.Pos())
+ .Add()
+ .Value("0")
+ .Build()
+ .Done()
+ );
+ switchArgs.push_back(lambda);
+ }
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (OpSettings.HasFlags(EYtSettingType::Ordered)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Ordered)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::WeakFields)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::WeakFields)).Build()
+ .Build();
+ }
+ if (OpSettings.HasFlags(EYtSettingType::Flow)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::Flow)).Build()
+ .Build();
+ }
+
+ for (auto sys: UsedSysFields) {
+ UsedFields.erase(TString(YqlSysColumnPrefix).append(sys));
+ }
+
+ TExprBase columns = Build<TCoVoid>(ctx, pos).Done();
+ if (!WeakFields.empty()) {
+ auto columnsBuilder = Build<TExprList>(ctx, pos);
+ for (auto& field: UsedFields) {
+ if (WeakFields.contains(field)) {
+ columnsBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(field)
+ .Build()
+ .Add()
+ .Value("weak")
+ .Build()
+ .Build();
+ }
+ else {
+ columnsBuilder
+ .Add<TCoAtom>()
+ .Value(field)
+ .Build();
+ }
+ }
+ columns = columnsBuilder.Done();
+ }
+ else if (UsedFields.size() != itemType->GetSize()) {
+ columns = TExprBase(ToAtomList(UsedFields, pos, ctx));
+ }
+
+ TVector<TYtOutTable> joinedMapOuts;
+ for (auto& map: JoinedMaps) {
+ joinedMapOuts.push_back(map.Output().Item(0));
+ }
+
+ auto sectionSettingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (std::get<4>(key)) {
+ sectionSettingsBuilder.Add(ctx.ShallowCopy(*std::get<4>(key)));
+ }
+ if (!UsedSysFields.empty()) {
+ sectionSettingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::SysColumns))
+ .Build()
+ .Value(ToAtomList(UsedSysFields, pos, ctx))
+ .Build()
+ ;
+ }
+
+ auto joinedMap = Build<TYtMap>(ctx, pos)
+ .World(JoinedMaps.front().World())
+ .DataSink(DataSink.Cast())
+ .Output()
+ .Add(joinedMapOuts)
+ .Build()
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table(ctx.ShallowCopy(*std::get<2>(key)))
+ .Columns(columns)
+ .Ranges(ctx.ShallowCopy(*std::get<3>(key)))
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings(sectionSettingsBuilder.Done())
+ .Build()
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper()
+ .Args({"stream"})
+ .Body<TCoSwitch>()
+ .Input<TExprApplier>()
+ .Apply(mapper)
+ .With(0, "stream")
+ .Build()
+ .BufferBytes()
+ .Value(ToString(SwitchMemoryLimit))
+ .Build()
+ .FreeArgs()
+ .Add(switchArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+
+ for (size_t i: xrange(JoinedMaps.size())) {
+ auto map = JoinedMaps[i];
+ OutputSubsts.emplace(map.Raw(), std::make_pair(joinedMap, i));
+ }
+
+ res = true;
+ }
+
+ ClearJoinGroup();
+
+ return res;
+}
+
+bool TOutHorizontalJoinOptimizer::HandleGroup(TPositionHandle pos, const TGroupKey& key, const TVector<TYtMap>& maps, TExprContext& ctx) {
+ auto itemType = std::get<2>(key)->GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+
+ for (TYtMap map: maps) {
+ if (JoinedMaps.size() + 1 > MaxOutTables) {
+ if (MakeJoinedMap(pos, key, itemType, ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "OutHorizontalJoin: split by max output tables: " << (JoinedMaps.size() + 1);
+ }
+ }
+
+ TExprNode::TPtr updatedLambda = map.Mapper().Ptr();
+ if (MaxJobMemoryLimit) {
+ if (UpdateTableContentMemoryUsage(map.Mapper().Ptr(), updatedLambda, State_, ctx).Level != IGraphTransformer::TStatus::Ok) {
+ return false;
+ }
+ }
+
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ auto currMemory = Accumulate(MemUsage.begin(), MemUsage.end(), SwitchMemoryLimit,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ // Take into account codec input/output buffers (one for all inputs and one per output)
+ currMemory += YQL_JOB_CODEC_MEM * (JoinedMaps.size() + 2);
+
+ if ((MaxJobMemoryLimit && currMemory > *MaxJobMemoryLimit) || UsedFiles > MaxOperationFiles) {
+ const auto usedFiles = UsedFiles; // Save value, because MakeJoinedMap will clear it
+ if (MakeJoinedMap(pos, key, itemType, ctx)) {
+ YQL_CLOG(INFO, ProviderYt) << "OutHorizontalJoin: split by limits. Memory: " << currMemory << ", files: " << usedFiles;
+ }
+ ScanResourceUsage(*updatedLambda, *State_->Configuration, State_->Types, &MemUsage, nullptr, &UsedFiles);
+ }
+
+ AddToJoinGroup(map);
+
+ for (auto itemType: map.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ UsedFields.insert(itemType->GetName());
+ }
+
+ auto sysColumns = NYql::GetSettingAsColumnList(map.Input().Item(0).Settings().Ref(), EYtSettingType::SysColumns);
+ UsedSysFields.insert(sysColumns.begin(), sysColumns.end());
+
+ if (auto columns = map.Input().Item(0).Paths().Item(0).Columns().Maybe<TExprList>()) {
+ for (auto child: columns.Cast()) {
+ if (auto maybeTuple = child.Maybe<TCoAtomList>()) {
+ auto tuple = maybeTuple.Cast();
+ if (tuple.Item(1).Value() == "weak") {
+ WeakFields.insert(tuple.Item(0).Value());
+ }
+ }
+ }
+ }
+ }
+
+ MakeJoinedMap(pos, key, itemType, ctx);
+ return true;
+};
+
+bool TOutHorizontalJoinOptimizer::IsGoodForOutHorizontalJoin(const TExprNode* op) {
+ auto it = ProcessedOps.find(op);
+ if (it != ProcessedOps.end()) {
+ return it->second;
+ }
+
+ bool res = false;
+ if (auto maybeMap = TMaybeNode<TYtMap>(op)) {
+ auto map = maybeMap.Cast();
+
+ res = IsGoodForHorizontalJoin(map)
+ && map.Input().Item(0).Paths().Size() == 1
+ && !NYql::HasAnySetting(map.Input().Item(0).Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip)
+ && !HasNonEmptyKeyFilter(map.Input().Item(0));
+ }
+ ProcessedOps.emplace(op, res);
+ return res;
+};
+
+IGraphTransformer::TStatus TOutHorizontalJoinOptimizer::Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) {
+
+ THashMap<TGroupKey, TVector<TYtMap>> opGroups;
+ THashMap<TGroupKey, TVector<TYtMap>> tableGroups;
+
+ for (auto& x: OpDeps) {
+ auto writer = x.first;
+ if (IsGoodForOutHorizontalJoin(writer)) {
+ auto map = TYtMap(writer);
+ auto section = map.Input().Item(0);
+ auto path = section.Paths().Item(0);
+
+ // Only input table. Temp outputs are processed above
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ ui32 flags = 0;
+ if (NYql::HasSetting(table.Settings().Ref(), EYtSettingType::WithQB)) {
+ flags |= EFeatureFlags::QB2;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields)) {
+ flags |= EFeatureFlags::WeakField;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Flow)) {
+ flags |= EFeatureFlags::Flow;
+ }
+
+ auto key = TGroupKey{
+ TString{map.DataSink().Cluster().Value()},
+ map.World().Raw(),
+ table.Cast().Raw(),
+ path.Ranges().Raw(),
+ NYql::GetSetting(section.Settings().Ref(), EYtSettingType::Sample).Get(),
+ flags
+ };
+ tableGroups[key].push_back(map);
+ }
+ }
+
+ opGroups.clear();
+ for (auto& reader: x.second) {
+ if (IsGoodForOutHorizontalJoin(std::get<0>(reader))) {
+ auto map = TYtMap(std::get<0>(reader));
+ auto section = map.Input().Item(0);
+
+ ui32 flags = 0;
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::WeakFields)) {
+ flags |= EFeatureFlags::WeakField;
+ }
+ if (NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Flow)) {
+ flags |= EFeatureFlags::Flow;
+ }
+
+ auto key = TGroupKey{
+ TString{map.DataSink().Cluster().Value()},
+ map.World().Raw(),
+ std::get<2>(reader),
+ section.Paths().Item(0).Ranges().Raw(),
+ NYql::GetSetting(section.Settings().Ref(), EYtSettingType::Sample).Get(),
+ flags
+ };
+ opGroups[key].push_back(map);
+ }
+ }
+ if (opGroups.empty()) {
+ continue;
+ }
+
+ for (auto& group: opGroups) {
+ if (group.second.size() < 2) {
+ continue;
+ }
+
+ Sort(group.second.begin(), group.second.end(),
+ [](const TYtMap& m1, const TYtMap& m2) { return m1.Ref().UniqueId() < m2.Ref().UniqueId(); });
+
+ if (!HandleGroup(writer->Pos(), group.first, group.second, ctx)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ if (!OutputSubsts.empty()) {
+ break;
+ }
+ }
+
+ if (!OutputSubsts.empty()) {
+ break;
+ }
+ }
+
+ if (OutputSubsts.empty() && !tableGroups.empty()) {
+ for (auto& group: tableGroups) {
+ if (group.second.size() < 2) {
+ continue;
+ }
+
+ Sort(group.second.begin(), group.second.end(),
+ [](const TYtMap& m1, const TYtMap& m2) { return m1.Ref().UniqueId() < m2.Ref().UniqueId(); });
+
+ if (!HandleGroup(std::get<2>(group.first)->Pos(), group.first, group.second, ctx)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ if (!OutputSubsts.empty()) {
+ break;
+ }
+ }
+ }
+
+ if (!OutputSubsts.empty()) {
+ TNodeOnNodeOwnedMap toOptimize;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ if (auto maybeOut = TMaybeNode<TYtOutput>(node)) {
+ auto out = maybeOut.Cast();
+ auto it = OutputSubsts.find(out.Operation().Raw());
+ if (it != OutputSubsts.end()) {
+ YQL_ENSURE(out.OutIndex().Value() == "0");
+ toOptimize[node.Get()] = Build<TYtOutput>(ctx, out.Pos())
+ .Operation(it->second.first)
+ .OutIndex()
+ .Value(ToString(it->second.second))
+ .Build()
+ .Mode(out.Mode())
+ .Done().Ptr();
+ }
+ }
+ return true;
+ });
+
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-OutHorizontalJoin";
+ return RemapExpr(input, output, toOptimize, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ return IGraphTransformer::TStatus::Ok;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.h b/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.h
new file mode 100644
index 0000000000..aad783760b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_horizontal_join.h
@@ -0,0 +1,174 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+#include "yql_yt_optimize.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/bitmap.h>
+#include <util/generic/map.h>
+#include <util/generic/set.h>
+#include <util/generic/maybe.h>
+#include <util/generic/hash.h>
+#include <util/generic/size_literals.h>
+
+#include <utility>
+#include <tuple>
+
+namespace NYql {
+
+class THorizontalJoinBase {
+public:
+ THorizontalJoinBase(const TYtState::TPtr& state, const TOpDeps& opDeps, const TNodeSet& hasWorldDeps)
+ : State_(state)
+ , OpDeps(opDeps)
+ , HasWorldDeps(hasWorldDeps)
+ , MaxTables(State_->Configuration->MaxInputTables.Get().GetOrElse(DEFAULT_MAX_INPUT_TABLES))
+ , MaxOutTables(State_->Configuration->MaxOutputTables.Get().GetOrElse(DEFAULT_MAX_OUTPUT_TABLES))
+ , SwitchMemoryLimit(State_->Configuration->SwitchLimit.Get().GetOrElse(DEFAULT_SWITCH_MEMORY_LIMIT))
+ , MaxJobMemoryLimit(State_->Configuration->MaxExtraJobMemoryToFuseOperations.Get())
+ , MaxOperationFiles(State_->Configuration->MaxOperationFiles.Get().GetOrElse(DEFAULT_MAX_OPERATION_FILES))
+ , UsedFiles(1) // jobstate
+ {
+ }
+
+protected:
+ enum EFeatureFlags: ui32 {
+ YAMR = 1 << 1,
+ Flow = 1 << 2,
+ WeakField = 1 << 3,
+ QB2 = 1 << 4,
+ };
+
+ bool IsGoodForHorizontalJoin(NNodes::TYtMap map) const;
+ NNodes::TCoLambda CleanupAuxColumns(NNodes::TCoLambda lambda, TExprContext& ctx) const;
+ void ClearJoinGroup();
+ void AddToJoinGroup(NNodes::TYtMap map);
+ NNodes::TCoLambda BuildMapperWithAuxColumnsForSingleInput(TPositionHandle pos, bool ordered, TExprContext& ctx) const;
+ NNodes::TCoLambda BuildMapperWithAuxColumnsForMultiInput(TPositionHandle pos, bool ordered, TExprContext& ctx) const;
+ NNodes::TCoLambda MakeSwitchLambda(size_t mapIndex, size_t fieldsCount, bool singleInput, TExprContext& ctx) const;
+
+protected:
+ TYtState::TPtr State_;
+ const TOpDeps& OpDeps;
+ const TNodeSet& HasWorldDeps;
+
+ const size_t MaxTables;
+ const size_t MaxOutTables;
+ const ui64 SwitchMemoryLimit;
+ const TMaybe<ui64> MaxJobMemoryLimit;
+ const size_t MaxOperationFiles;
+
+ NNodes::TMaybeNode<NNodes::TYtDSink> DataSink;
+ TDynBitMap UsesTablePath;
+ TDynBitMap UsesTableRecord;
+ TDynBitMap UsesTableIndex;
+ EYtSettingTypes OpSettings;
+ TMap<TStringBuf, ui64> MemUsage;
+ size_t UsedFiles;
+ TVector<NNodes::TYtMap> JoinedMaps;
+};
+
+class THorizontalJoinOptimizer: public THorizontalJoinBase {
+ // Cluster, Sampling, Dependency, Flags
+ using TGroupKey = std::tuple<TString, TMaybe<TSampleParams>, const TExprNode*, ui32>;
+
+public:
+ THorizontalJoinOptimizer(const TYtState::TPtr& state, const TOpDeps& opDeps, const TNodeSet& hasWorldDeps, TProcessedNodesSet* processedNodes)
+ : THorizontalJoinBase(state, opDeps, hasWorldDeps)
+ , ProcessedNodes(processedNodes)
+ {
+ }
+
+ IGraphTransformer::TStatus Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx);
+
+private:
+ TExprNode::TPtr HandleList(const TExprNode::TPtr& node, bool sectionList, TExprContext& ctx);
+ void AddToGroups(NNodes::TYtMap map, size_t s, size_t p, const TExprNode* section,
+ THashMap<TGroupKey, TVector<std::tuple<NNodes::TYtMap, size_t, size_t>>>& groups,
+ TNodeMap<TMaybe<TGroupKey>>& processedMaps) const;
+ THashMap<TGroupKey, TVector<std::tuple<NNodes::TYtMap, size_t, size_t>>> CollectGroups(const TExprNode::TPtr& node, bool sectionList) const;
+ void ClearJoinGroup();
+ bool MakeJoinedMap(TPositionHandle pos, TExprContext& ctx);
+ TExprNode::TPtr RebuildList(const TExprNode::TPtr& node, bool sectionList, TExprContext& ctx);
+
+private:
+ TSyncMap Worlds;
+ // operation -> joined_map_out
+ TNodeMap<size_t> UniqMaps;
+ // {section, columns, ranges} -> TMap{out_num -> path_num}
+ TMap<std::tuple<size_t, const TExprNode*, const TExprNode*>, TMap<size_t, size_t>> GroupedOuts;
+ // out_num -> TVector{section_num, path_num}. Cannot be joined with other outputs
+ TMap<size_t, TVector<std::pair<size_t, size_t>>> ExclusiveOuts;
+ size_t InputCount = 0;
+ // {section or table, ranges, section settings, WeakField}
+ TSet<std::tuple<const TExprNode*, const TExprNode*, const TExprNode*, bool>> GroupedInputs;
+
+ // {section_num, path_num} -> {joined_map, out_num}
+ TMap<std::pair<size_t, size_t>, TMaybe<std::pair<NNodes::TYtMap, size_t>>> InputSubsts;
+
+ TProcessedNodesSet* ProcessedNodes;
+};
+
+class TMultiHorizontalJoinOptimizer: public THorizontalJoinBase {
+ // Cluster, readers, Sampling, Dependency, Flags
+ using TGroupKey = std::tuple<TString, std::set<ui64>, TMaybe<TSampleParams>, const TExprNode*, ui32>;
+
+public:
+ TMultiHorizontalJoinOptimizer(const TYtState::TPtr& state, const TOpDeps& opDeps, const TNodeSet& hasWorldDeps)
+ : THorizontalJoinBase(state, opDeps, hasWorldDeps)
+ {
+ }
+
+ IGraphTransformer::TStatus Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx);
+
+private:
+ bool HandleGroup(const TVector<NNodes::TYtMap>& maps, TExprContext& ctx);
+ void ClearJoinGroup();
+ bool MakeJoinedMap(TExprContext& ctx);
+
+private:
+ TSyncMap Worlds;
+ size_t InputCount = 0;
+ // {section or table, ranges, section settings, WeakField}
+ TSet<std::tuple<const TExprNode*, const TExprNode*, const TExprNode*, bool>> GroupedInputs;
+
+ TNodeMap<std::pair<NNodes::TYtMap, size_t>> OutputSubsts; // original map -> joined map, out index
+};
+
+class TOutHorizontalJoinOptimizer: public THorizontalJoinBase {
+ // Group by: Cluster, World, Input, Range, Sampling, Flags
+ using TGroupKey = std::tuple<TString, const TExprNode*, const TExprNode*, const TExprNode*, const TExprNode*, ui32>;
+
+public:
+ TOutHorizontalJoinOptimizer(const TYtState::TPtr& state, const TOpDeps& opDeps, const TNodeSet& hasWorldDeps)
+ : THorizontalJoinBase(state, opDeps, hasWorldDeps)
+ {
+ }
+
+ IGraphTransformer::TStatus Optimize(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx);
+
+private:
+ void ClearJoinGroup();
+ bool MakeJoinedMap(TPositionHandle pos, const TGroupKey& key, const TStructExprType* itemType, TExprContext& ctx);
+ bool HandleGroup(TPositionHandle pos, const TGroupKey& key, const TVector<NNodes::TYtMap>& maps, TExprContext& ctx);
+ bool IsGoodForOutHorizontalJoin(const TExprNode* op);
+
+private:
+ TSet<TStringBuf> UsedFields;
+ TSet<TStringBuf> WeakFields;
+ TSet<TString> UsedSysFields;
+
+ TNodeMap<std::pair<NNodes::TYtMap, size_t>> OutputSubsts; // original map -> joined map, out index
+ TNodeMap<bool> ProcessedOps;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
new file mode 100644
index 0000000000..5a857f8369
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_intent_determination.cpp
@@ -0,0 +1,468 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_table.h"
+#include "yql_yt_table_desc.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/string/cast.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtIntentDeterminationTransformer : public TVisitorTransformerBase {
+public:
+ TYtIntentDeterminationTransformer(TYtState::TPtr state)
+ : TVisitorTransformerBase(false)
+ , State_(state)
+ {
+ // Handle initial callables after SQL parse
+ AddHandler({TYtRead::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleRead));
+ AddHandler({TYtWrite::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleWrite));
+
+ // Handle callables for already parsed/optimized AST
+ AddHandler({TYtReadTable::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleReadTable));
+ AddHandler({TYtReadTableScheme::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleReadTableScheme));
+ AddHandler({TYtDropTable::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleDropTable));
+ AddHandler({TYtPublish::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandlePublish));
+ AddHandler({TYtSort::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtMap::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtReduce::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtMapReduce::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtCopy::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtMerge::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtEquiJoin::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOperation));
+ AddHandler({TYtFill::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOutOperation));
+ AddHandler({TYtTouch::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleOutOperation));
+ AddHandler({TYtWriteTable::CallableName()}, Hndl(&TYtIntentDeterminationTransformer::HandleWriteTable));
+ }
+
+ TStatus HandleRead(TExprBase input, TExprContext& ctx) {
+ TYtRead read = input.Cast<TYtRead>();
+ if (!EnsureArgsCount(read.Ref(), 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto cluster = TString{read.DataSource().Cluster().Value()};
+
+ EYtSettingTypes acceptedSettings = EYtSettingType::View | EYtSettingType::Anonymous
+ | EYtSettingType::InferScheme | EYtSettingType::ForceInferScheme
+ | EYtSettingType::DoNotFailOnInvalidSchema | EYtSettingType::XLock
+ | EYtSettingType::UserSchema | EYtSettingType::UserColumns | EYtSettingType::IgnoreTypeV3;
+ for (auto path: read.Arg(2).Cast<TExprList>()) {
+ if (auto table = path.Maybe<TYtPath>().Table()) {
+ if (!TYtTableInfo::Validate(table.Cast().Ref(), acceptedSettings, ctx)) {
+ return TStatus::Error;
+ }
+
+ TYtTableInfo tableInfo(table.Cast(), false);
+ if (!ProcessInputTableIntent(ctx.GetPosition(input.Pos()), cluster, tableInfo, ctx)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleWrite(TExprBase input, TExprContext& ctx) {
+ TYtWrite write = input.Cast<TYtWrite>();
+ if (!EnsureArgsCount(write.Ref(), 5, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (!TYtTableInfo::Validate(write.Arg(2).Ref(), EYtSettingType::Mode | EYtSettingType::Initial | EYtSettingType::Anonymous, ctx)) {
+ return TStatus::Error;
+ }
+
+ auto cluster = TString{write.DataSink().Cluster().Value()};
+ TYtTableInfo tableInfo(write.Arg(2), false);
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(
+ cluster,
+ tableInfo.Name,
+ tableInfo.Epoch
+ );
+
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::Anonymous)) {
+ tableDesc.IsAnonymous = true;
+ RegisterAnonymouseTable(cluster, tableInfo.Name);
+ }
+
+ if (auto mode = NYql::GetSetting(write.Arg(4).Ref(), EYtSettingType::Mode)) {
+ try {
+ switch (FromString<EYtWriteMode>(mode->Child(1)->Content())) {
+ case EYtWriteMode::Drop:
+ tableDesc.Intents |= TYtTableIntent::Drop;
+ break;
+ case EYtWriteMode::Append:
+ tableDesc.Intents |= TYtTableIntent::Append;
+ break;
+ case EYtWriteMode::Renew:
+ case EYtWriteMode::RenewKeepMeta:
+ tableDesc.Intents |= TYtTableIntent::Override;
+ break;
+ case EYtWriteMode::Flush:
+ tableDesc.Intents |= TYtTableIntent::Flush;
+ break;
+ default:
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtWrite::CallableName() << " mode: " << mode->Child(1)->Content()));
+ return TStatus::Error;
+
+ }
+ } catch (const yexception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtWrite::CallableName() << " mode: " << mode->Child(1)->Content() << ", " << e.what()));
+ return TStatus::Error;
+ }
+ } else {
+ tableDesc.Intents |= TYtTableIntent::Override;
+ }
+
+ if (!ValidateOutputTableIntent(ctx.GetPosition(input.Pos()), tableDesc.Intents, cluster, tableInfo.Name, ctx)) {
+ return TStatus::Error;
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleReadTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ TYtReadTable read = TYtReadTable(input);
+
+ auto cluster = TString{read.DataSource().Cluster().Value()};
+
+ for (auto section: read.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ TYtTableInfo tableInfo(table.Cast(), false);
+ if (!ProcessInputTableIntent(ctx.GetPosition(input->Pos()), cluster, tableInfo, ctx)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleReadTableScheme(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ TYtReadTableScheme scheme = TYtReadTableScheme(input);
+
+ auto cluster = TString{scheme.DataSource().Cluster().Value()};
+
+ TYtTableInfo tableInfo(scheme.Table(), false);
+ if (!ProcessInputTableIntent(ctx.GetPosition(input->Pos()), cluster, tableInfo, ctx)) {
+ return TStatus::Error;
+ }
+
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus HandleOperation(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ auto op = TYtTransientOpBase(input);
+ auto cluster = TString{op.DataSink().Cluster().Value()};
+
+ for (auto section: op.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto table = path.Table().Maybe<TYtTable>()) {
+ TYtTableInfo tableInfo(table.Cast(), false);
+ if (!ProcessInputTableIntent(ctx.GetPosition(input->Pos()), cluster, tableInfo, ctx)) {
+ return TStatus::Error;
+ }
+ }
+ }
+ }
+
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleOutOperation(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleDropTable(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ auto drop = TYtDropTable(input);
+
+ auto cluster = TString{drop.DataSink().Cluster().Value()};
+ TYtTableInfo tableInfo(drop.Table(), false);
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(
+ cluster,
+ tableInfo.Name,
+ tableInfo.Epoch
+ );
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::Anonymous)) {
+ tableDesc.IsAnonymous = true;
+ RegisterAnonymouseTable(cluster, tableInfo.Name);
+ }
+ tableDesc.Intents |= TYtTableIntent::Drop;
+
+ UpdateDescriptorMeta(tableDesc, tableInfo);
+
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandlePublish(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) {
+ auto publish = TYtPublish(input);
+
+ auto cluster = TString{publish.DataSink().Cluster().Value()};
+ TYtTableInfo tableInfo(publish.Publish(), false);
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(
+ cluster,
+ tableInfo.Name,
+ tableInfo.Epoch
+ );
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::Anonymous)) {
+ tableDesc.IsAnonymous = true;
+ RegisterAnonymouseTable(cluster, tableInfo.Name);
+ }
+ if (auto mode = NYql::GetSetting(publish.Settings().Ref(), EYtSettingType::Mode)) {
+ try {
+ switch (FromString<EYtWriteMode>(mode->Child(1)->Content())) {
+ case EYtWriteMode::Append:
+ tableDesc.Intents |= TYtTableIntent::Append;
+ break;
+ case EYtWriteMode::Renew:
+ case EYtWriteMode::RenewKeepMeta:
+ tableDesc.Intents |= TYtTableIntent::Override;
+ break;
+ case EYtWriteMode::Flush:
+ tableDesc.Intents |= TYtTableIntent::Flush;
+ break;
+ default:
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtPublish::CallableName() << " mode: " << mode->Child(1)->Content()));
+ return TStatus::Error;
+
+ }
+ } catch (const yexception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtPublish::CallableName() << " mode: " << mode->Child(1)->Content() << ", " << e.what()));
+ return TStatus::Error;
+ }
+ } else {
+ tableDesc.Intents |= TYtTableIntent::Override;
+ }
+
+ if (!ValidateOutputTableIntent(ctx.GetPosition(input->Pos()), tableDesc.Intents, cluster, tableInfo.Name, ctx)) {
+ return TStatus::Error;
+ }
+
+ UpdateDescriptorMeta(tableDesc, tableInfo);
+
+ output = ResetTablesMeta(input, ctx, State_->Types->UseTableMetaFromGraph);
+ if (!output) {
+ return TStatus::Error;
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus HandleWriteTable(TExprBase input, TExprContext& ctx) {
+ TYtWriteTable write = input.Cast<TYtWriteTable>();
+
+ auto cluster = TString{write.DataSink().Cluster().Value()};
+ TYtTableInfo tableInfo(write.Table(), false);
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(
+ cluster,
+ tableInfo.Name,
+ tableInfo.Epoch
+ );
+
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::Anonymous)) {
+ tableDesc.IsAnonymous = true;
+ RegisterAnonymouseTable(cluster, tableInfo.Name);
+ }
+
+ if (auto mode = NYql::GetSetting(write.Settings().Ref(), EYtSettingType::Mode)) {
+ try {
+ switch (FromString<EYtWriteMode>(mode->Child(1)->Content())) {
+ case EYtWriteMode::Drop:
+ tableDesc.Intents |= TYtTableIntent::Drop;
+ break;
+ case EYtWriteMode::Append:
+ tableDesc.Intents |= TYtTableIntent::Append;
+ break;
+ case EYtWriteMode::Renew:
+ case EYtWriteMode::RenewKeepMeta:
+ tableDesc.Intents |= TYtTableIntent::Override;
+ break;
+ case EYtWriteMode::Flush:
+ tableDesc.Intents |= TYtTableIntent::Flush;
+ break;
+ default:
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtWrite::CallableName() << " mode: " << mode->Child(1)->Content()));
+ return TStatus::Error;
+
+ }
+ } catch (const yexception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(mode->Child(1)->Pos()), TStringBuilder() << "Unsupported "
+ << TYtWrite::CallableName() << " mode: " << mode->Child(1)->Content() << ", " << e.what()));
+ return TStatus::Error;
+ }
+ } else {
+ tableDesc.Intents |= TYtTableIntent::Override;
+ }
+
+ if (!ValidateOutputTableIntent(ctx.GetPosition(input.Pos()), tableDesc.Intents, cluster, tableInfo.Name, ctx)) {
+ return TStatus::Error;
+ }
+
+ UpdateDescriptorMeta(tableDesc, tableInfo);
+
+ return TStatus::Ok;
+ }
+
+private:
+ void UpdateDescriptorMeta(TYtTableDescription& tableDesc, const TYtTableInfo& tableInfo) const {
+ if (!State_->Types->UseTableMetaFromGraph) {
+ return;
+ }
+
+ if (tableInfo.Stat && !tableDesc.Stat) {
+ if (tableDesc.Stat = tableInfo.Stat) {
+ tableDesc.Stat->FromNode = {};
+ }
+ }
+
+ if (tableInfo.Meta && !tableDesc.Meta) {
+ tableDesc.Meta = tableInfo.Meta;
+ tableDesc.Meta->FromNode = {};
+ if (NYql::HasSetting(tableInfo.Settings.Ref(), EYtSettingType::WithQB)) {
+ tableDesc.QB2RowSpec = tableInfo.RowSpec;
+ tableDesc.QB2RowSpec->FromNode = {};
+ } else {
+ if (tableDesc.RowSpec = tableInfo.RowSpec) {
+ tableDesc.RowSpec->FromNode = {};
+ }
+ }
+ }
+ }
+
+ bool ProcessInputTableIntent(TPosition pos, const TString& cluster, const TYtTableInfo& tableInfo, TExprContext& ctx) {
+ if (!State_->Checkpoints.empty() && State_->Checkpoints.contains(std::make_pair(cluster, tableInfo.Name))) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Reading from checkpoint " << tableInfo.Name.Quote() << " is not allowed"));
+ return false;
+ }
+
+ TYtTableDescription& tableDesc = State_->TablesData->GetOrAddTable(
+ cluster,
+ tableInfo.Name,
+ tableInfo.Epoch
+ );
+
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::Anonymous)) {
+ tableDesc.IsAnonymous = true;
+ RegisterAnonymouseTable(cluster, tableInfo.Name);
+ }
+
+ TYtTableIntents intents = TYtTableIntent::Read;
+ if (tableInfo.Settings) {
+ auto view = NYql::GetSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::View);
+ if (view) {
+ if (tableInfo.Epoch.GetOrElse(0)) {
+ ctx.AddError(TIssue(pos, TStringBuilder()
+ << "Table " << tableInfo.Name.Quote() << " cannot have any view after replacing its content"));
+ return false;
+ }
+ tableDesc.Views.insert({TString{view->Child(1)->Content()}, {}});
+ intents = TYtTableIntent::View; // Override Read intent
+ }
+ if (NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::XLock)) {
+ intents |= TYtTableIntent::Override;
+ }
+ }
+ tableDesc.Intents |= intents;
+ if (tableInfo.Epoch.GetOrElse(0) == 0) {
+ if (!NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::UserSchema)) {
+ TExprNode::TPtr perForceInfer = NYql::GetSetting(tableInfo.Settings.Cast().Ref(),
+ EYtSettingType::ForceInferScheme);
+ TExprNode::TPtr perInfer = NYql::GetSetting(tableInfo.Settings.Cast().Ref(),
+ EYtSettingType::InferScheme);
+ tableDesc.InferSchemaRows = State_->Configuration->InferSchema.Get().OrElse(State_->Configuration->ForceInferSchema.Get()).GetOrElse(0);
+ if (perForceInfer) {
+ tableDesc.InferSchemaRows = (perForceInfer->ChildrenSize() == 2)
+ ? FromString<ui32>(perForceInfer->Tail().Content())
+ : 1;
+ } else if (perInfer) {
+ tableDesc.InferSchemaRows = (perInfer->ChildrenSize() == 2)
+ ? FromString<ui32>(perInfer->Tail().Content())
+ : 1;
+ }
+ tableDesc.ForceInferSchema = perForceInfer
+ || State_->Configuration->ForceInferSchema.Get().GetOrElse(0) > 0;
+ }
+ tableDesc.IgnoreTypeV3 = State_->Configuration->IgnoreTypeV3.Get().GetOrElse(false)
+ || NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::IgnoreTypeV3);
+ }
+ tableDesc.FailOnInvalidSchema = !NYql::HasSetting(tableInfo.Settings.Cast().Ref(), EYtSettingType::DoNotFailOnInvalidSchema);
+
+ UpdateDescriptorMeta(tableDesc, tableInfo);
+
+ return true;
+ }
+
+ bool ValidateOutputTableIntent(
+ TPosition pos,
+ TYtTableIntents intents,
+ const TString& cluster,
+ const TString& tableName,
+ TExprContext& ctx)
+ {
+ if (
+ !intents.HasFlags(TYtTableIntent::Flush) &&
+ !State_->Checkpoints.empty() &&
+ State_->Checkpoints.contains(std::make_pair(cluster, tableName)))
+ {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Writing to checkpoint " << tableName.Quote() << " is not allowed"));
+ return false;
+ }
+
+ return true;
+ }
+
+ void RegisterAnonymouseTable(const TString& cluster, const TString& label) {
+ auto& path = State_->AnonymousLabels[std::make_pair(cluster, label)];
+ if (path.empty()) {
+ path = "tmp/" + GetGuidAsString(State_->Types->RandomProvider->GenGuid());
+ YQL_CLOG(INFO, ProviderYt) << "Anonymous label " << cluster << '.' << label << ": " << path;
+ }
+ }
+private:
+ TYtState::TPtr State_;
+};
+
+THolder<IGraphTransformer> CreateYtIntentDeterminationTransformer(TYtState::TPtr state) {
+ return THolder(new TYtIntentDeterminationTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
new file mode 100644
index 0000000000..a83adf3349
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_io_discovery.cpp
@@ -0,0 +1,775 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_key.h"
+#include "yql_yt_gateway.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_constraint.h>
+#include <ydb/library/yql/core/type_ann/type_ann_core.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/peephole_opt/yql_opt_peephole_physical.h>
+#include <ydb/library/yql/core/issue/protos/issue_id.pb.h>
+#include <ydb/library/yql/core/issue/yql_issue.h>
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/hash.h>
+#include <util/string/cast.h>
+#include <util/string/strip.h>
+
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtIODiscoveryTransformer : public TGraphTransformerBase {
+public:
+ TYtIODiscoveryTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+ }
+
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+ if (ctx.Step.IsDone(TExprStep::DiscoveryIO)) {
+ return TStatus::Ok;
+ }
+
+ TVector<IYtGateway::TCanonizeReq> paths;
+ const bool discoveryMode = State_->Types->DiscoveryMode;
+ const bool evaluationInProgress = State_->Types->EvaluationInProgress;
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (auto maybeRead = TMaybeNode<TYtRead>(node)) {
+ if (!maybeRead.DataSource()) { // Validates provider
+ return node;
+ }
+ auto read = maybeRead.Cast();
+ auto ds = read.DataSource();
+ if (!EnsureArgsCount(read.Ref(), 5, ctx)) {
+ return {};
+ }
+
+ if (discoveryMode && evaluationInProgress) {
+ ctx.AddError(YqlIssue(ctx.GetPosition(read.Pos()), TIssuesIds::YQL_NOT_ALLOWED_IN_DISCOVERY,
+ TStringBuilder() << node->Content() << " is not allowed in Discovery mode"));
+ return {};
+ }
+
+ TYtInputKeys keys;
+ if (!keys.Parse(read.Arg(2).Ref(), ctx)) {
+ return {};
+ }
+
+ if (keys.IsProcessed()) {
+ // Already processed
+ return node;
+ }
+
+ if (keys.GetKeys().empty()) {
+ auto userSchema = GetSetting(*read.Ref().Child(4), EYtSettingType::UserSchema);
+ if (userSchema) {
+ return BuildEmptyTablesRead(read.Pos(), *userSchema, ctx);
+ }
+
+ ctx.AddError(TIssue(ctx.GetPosition(read.Arg(2).Pos()), "The list of tables is empty"));
+ return {};
+ }
+
+ if (keys.GetType() == TYtKey::EType::TableScheme) {
+ return ConvertTableScheme(read, keys.GetKeys().front(), ctx);
+ }
+
+ if (discoveryMode) {
+ for (auto& key: keys.GetKeys()) {
+ auto keyPos = ctx.GetPosition(key.GetNode()->Pos());
+ if (key.GetRange()) {
+ ctx.AddError(YqlIssue(ctx.GetPosition(read.Arg(2).Pos()), TIssuesIds::YQL_NOT_ALLOWED_IN_DISCOVERY,
+ TStringBuilder() << MrTableRangeName << '/' << MrTableRangeStrictName << " is not allowed in Discovery mode"));
+ return {};
+ }
+ else if (key.GetFolder()) {
+ ctx.AddError(YqlIssue(ctx.GetPosition(read.Arg(2).Pos()), TIssuesIds::YQL_NOT_ALLOWED_IN_DISCOVERY,
+ TStringBuilder() << MrFolderName << " is not allowed in Discovery mode"));
+ return {};
+ }
+ }
+ }
+ if (AllOf(keys.GetKeys(), [] (const TYtKey& key) { return key.IsAnonymous(); })) {
+ return ConvertTableRead(read, keys, ctx);
+ }
+ return node;
+ }
+ else if (auto maybeWrite = TMaybeNode<TYtWrite>(node)) {
+ if (!maybeWrite.DataSink()) { // Validates provider
+ return node;
+ }
+ auto write = maybeWrite.Cast();
+ auto ds = write.DataSink();
+ if (!EnsureArgsCount(write.Ref(), 5, ctx)) {
+ return {};
+ }
+
+ if (!EnsureTuple(write.Arg(4).MutableRef(), ctx)) {
+ return {};
+ }
+
+ TYtOutputKey key;
+ if (!key.Parse(write.Arg(2).Ref(), ctx)) {
+ return {};
+ }
+ if (key.GetType() == TYtKey::EType::Undefined) {
+ // Already processed
+ return node;
+ }
+
+ auto mode = NYql::GetSetting(*node->ChildPtr(4), EYtSettingType::Mode);
+ const bool flush = mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Flush;
+
+ TYtTableInfo tableInfo(key, ds.Cluster().Value());
+ if (key.IsAnonymous()) {
+ if (flush) {
+ ctx.AddError(TIssue(
+ ctx.GetPosition(write.Pos()),
+ TStringBuilder() << "Using anonymous tables as checkpoints is not allowed"));
+ return {};
+ }
+ tableInfo.Settings = Build<TCoNameValueTupleList>(ctx, write.Pos())
+ .Add()
+ .Name().Value(ToString(EYtSettingType::Anonymous)).Build()
+ .Build()
+ .Done();
+ } else if (tableInfo.Name.StartsWith("//")) {
+ tableInfo.Name = tableInfo.Name.substr(2);
+ }
+
+ if (flush) {
+ auto setKey = std::make_pair(ds.Cluster().Value(), tableInfo.Name);
+ if (State_->Checkpoints.contains(setKey)) {
+ ctx.AddError(TIssue(
+ ctx.GetPosition(write.Pos()),
+ TStringBuilder() << "Table " << tableInfo.Name.Quote() << " already used as checkpoint"));
+ return {};
+ }
+ State_->Checkpoints.emplace(std::move(setKey));
+ }
+ node->ChildRef(2) = tableInfo.ToExprNode(ctx, write.Pos()).Ptr();
+ return node;
+ }
+
+ return node;
+ }, ctx, settings);
+
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ status = OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (auto maybeRead = TMaybeNode<TYtRead>(node)) {
+ if (!maybeRead.DataSource()) { // Validates provider
+ return node;
+ }
+ auto read = maybeRead.Cast();
+ auto ds = read.DataSource();
+ if (!EnsureArgsCount(read.Ref(), 5, ctx)) {
+ return {};
+ }
+
+ TYtInputKeys keys;
+ if (!keys.Parse(read.Arg(2).Ref(), ctx)) {
+ return {};
+ }
+
+ if (keys.IsProcessed()) {
+ // Already processed
+ return node;
+ }
+
+ if (keys.GetKeys().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(read.Arg(2).Pos()), "The list of tables is empty"));
+ return {};
+ }
+
+ if (keys.GetType() != TYtKey::EType::TableScheme) {
+ auto cluster = TString{ds.Cluster().Value()};
+ for (auto& key: keys.GetKeys()) {
+ auto keyPos = ctx.GetPosition(key.GetNode()->Pos());
+ if (key.GetRange()) {
+ PendingRanges_.emplace(std::make_pair(cluster, *key.GetRange()), std::make_pair(keyPos, NThreading::TFuture<IYtGateway::TTableRangeResult>()));
+ }
+ else if (key.GetFolder()) {
+ PendingFolders_.emplace(std::make_pair(cluster, *key.GetFolder()), std::make_pair(keyPos, NThreading::TFuture<IYtGateway::TFolderResult>()));
+ }
+ else if (!key.IsAnonymous()) {
+ if (PendingCanonizations_.insert(std::make_pair(std::make_pair(cluster, key.GetPath()), paths.size())).second) {
+ paths.push_back(IYtGateway::TCanonizeReq()
+ .Cluster(cluster)
+ .Path(key.GetPath())
+ .Pos(keyPos)
+ );
+ }
+ }
+ }
+ }
+ return node;
+ }
+
+ return node;
+ }, ctx, TOptimizeExprSettings(nullptr));
+
+ if (status.Level != TStatus::Ok) {
+ PendingCanonizations_.clear();
+ PendingFolders_.clear();
+ PendingRanges_.clear();
+ return status;
+ }
+
+ if (PendingRanges_.empty() && PendingFolders_.empty() && PendingCanonizations_.empty()) {
+ return status;
+ }
+
+ TVector<NThreading::TFuture<void>> allFutures;
+ if (!PendingCanonizations_.empty()) {
+ CanonizeFuture_ = State_->Gateway->CanonizePaths(
+ IYtGateway::TCanonizePathsOptions(State_->SessionId)
+ .Paths(std::move(paths))
+ .Config(State_->Configuration->Snapshot())
+ );
+ allFutures.push_back(CanonizeFuture_.IgnoreResult());
+ }
+
+ for (auto& x : PendingRanges_) {
+ auto& cluster = x.first.first;
+ auto& range = x.first.second;
+ auto filterLambda = range.Filter;
+ TUserDataTable files;
+ if (filterLambda) {
+ const auto transformer = CreateTypeAnnotationTransformer(CreateExtCallableTypeAnnotationTransformer(*State_->Types), *State_->Types);
+ const auto constraints = CreateConstraintTransformer(*State_->Types, true);
+ const auto peephole = MakePeepholeOptimization(State_->Types);
+ while (const auto stringType = ctx.MakeType<TDataExprType>(EDataSlot::String)) {
+ if (!UpdateLambdaAllArgumentsTypes(filterLambda, {stringType}, ctx)) {
+ return TStatus::Error;
+ }
+
+ if (const auto status = transformer->Transform(filterLambda, filterLambda, ctx); status.Level == TStatus::Error) {
+ return status;
+ } else if (status.Level == TStatus::Repeat) {
+ continue;
+ }
+
+ bool isOptional;
+ if (const TDataExprType* dataType = nullptr;
+ !(EnsureDataOrOptionalOfData(*filterLambda, isOptional, dataType, ctx) && EnsureSpecificDataType(filterLambda->Pos(), *dataType, EDataSlot::Bool, ctx))) {
+ return TStatus::Error;
+ }
+
+ if (const auto status = UpdateLambdaConstraints(*filterLambda); status.Level == TStatus::Error) {
+ return status;
+ }
+
+ if (const auto status = constraints->Transform(filterLambda, filterLambda, ctx); status.Level == TStatus::Error) {
+ return status;
+ } else if (status.Level == TStatus::Repeat) {
+ continue;
+ }
+
+ if (const auto status = peephole->Transform(filterLambda, filterLambda, ctx); status.Level == TStatus::Error) {
+ return status;
+ } else if (status.Level == TStatus::Repeat) {
+ continue;
+ }
+
+ break;
+ }
+
+ if (!NCommon::FreezeUsedFilesSync(*filterLambda, files, *State_->Types, ctx, MakeUserFilesDownloadFilter(*State_->Gateway, cluster))) {
+ return TStatus::Error;
+ }
+ }
+
+ auto result = State_->Gateway->GetTableRange(
+ IYtGateway::TTableRangeOptions(State_->SessionId)
+ .Cluster(cluster)
+ .Prefix(StripStringRight(range.Prefix, EqualsStripAdapter('/')))
+ .Suffix(StripStringLeft(range.Suffix, EqualsStripAdapter('/')))
+ .Filter(filterLambda.Get())
+ .ExprCtx(filterLambda ? &ctx : nullptr)
+ .UserDataBlocks(files)
+ .UdfModules(State_->Types->UdfModules)
+ .UdfResolver(State_->Types->UdfResolver)
+ .UdfValidateMode(State_->Types->ValidateMode)
+ .Config(State_->Configuration->Snapshot())
+ .OptLLVM(State_->Types->OptLLVM.GetOrElse(TString()))
+ .Pos(x.second.first)
+ );
+ allFutures.push_back(result.IgnoreResult());
+ x.second.second = result;
+ }
+
+ for (auto& x : PendingFolders_) {
+ auto& cluster = x.first.first;
+ auto& folder = x.first.second;
+ auto result = State_->Gateway->GetFolder(
+ IYtGateway::TFolderOptions(State_->SessionId)
+ .Cluster(cluster)
+ .Prefix(folder.Prefix)
+ .Attributes(folder.Attributes)
+ .Config(State_->Configuration->Snapshot())
+ .Pos(x.second.first)
+ );
+ allFutures.push_back(result.IgnoreResult());
+ x.second.second = result;
+ }
+
+ AllFuture_ = NThreading::WaitExceptionOrAll(allFutures);
+ return TStatus::Async;
+ }
+
+ NThreading::TFuture<void> DoGetAsyncFuture(const TExprNode& input) final {
+ Y_UNUSED(input);
+ return AllFuture_;
+ }
+
+ TStatus DoApplyAsyncChanges(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+
+ if (!PendingCanonizations_.empty()) {
+ auto& res = CanonizeFuture_.GetValue();
+ res.ReportIssues(ctx.IssueManager);
+
+ if (!res.Success()) {
+ PendingCanonizations_.clear();
+ PendingRanges_.clear();
+ CanonizeFuture_ = {};
+ AllFuture_ = {};
+
+ return TStatus::Error;
+ }
+ }
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (!TMaybeNode<TYtRead>(node).DataSource()) {
+ return node;
+ }
+ auto read = TYtRead(node);
+ auto cluster = TString{read.DataSource().Cluster().Value()};
+
+ TYtInputKeys keys;
+ if (!keys.Parse(*node->Child(2), ctx)) {
+ return {};
+ }
+
+ if (keys.GetType() == TYtKey::EType::Folder) {
+ auto p = PendingFolders_.FindPtr(std::make_pair(cluster, *keys.GetKeys().front().GetFolder()));
+ YQL_ENSURE(p);
+ auto& res = p->second.GetValue();
+ res.ReportIssues(ctx.IssueManager);
+ if (!res.Success()) {
+ return {};
+ }
+
+ if (res.File) {
+ TString alias;
+ if (auto p = FolderFileToAlias_.FindPtr(res.File->GetPath().GetPath())) {
+ alias = *p;
+ } else {
+ alias = TString("_yql_folder").append(ToString(FolderFileToAlias_.size()));
+ FolderFileToAlias_.emplace(res.File->GetPath().GetPath(), alias);
+
+ TUserDataBlock tmpBlock;
+ tmpBlock.Type = EUserDataType::PATH;
+ tmpBlock.Data = res.File->GetPath().GetPath();
+ tmpBlock.Usage.Set(EUserDataBlockUsage::Path);
+ tmpBlock.FrozenFile = res.File;
+
+ State_->Types->UserDataStorage->AddUserDataBlock(alias, tmpBlock);
+ }
+
+ return ctx.Builder(node->Pos())
+ .Callable("Cons!")
+ .World(0)
+ .Callable(1, "AssumeColumnOrder")
+ .Callable(0, "Collect")
+ .Callable(0, "Apply")
+ .Callable(0, "Udf")
+ .Atom(0, "File.FolderListFromFile")
+ .Seal()
+ .Callable(1, "FilePath")
+ .Atom(0, alias)
+ .Seal()
+ .Seal()
+ .Seal()
+ .List(1)
+ .Atom(0, "Path")
+ .Atom(1, "Type")
+ .Atom(2, "Attributes")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ TVector<TExprBase> listItems;
+ for (auto& item: res.Items) {
+ listItems.push_back(Build<TCoAsStruct>(ctx, node->Pos())
+ .Add()
+ .Add<TCoAtom>()
+ .Value("Path")
+ .Build()
+ .Add<TCoString>()
+ .Literal()
+ .Value(item.Path)
+ .Build()
+ .Build()
+ .Build()
+ .Add()
+ .Add<TCoAtom>()
+ .Value("Type")
+ .Build()
+ .Add<TCoString>()
+ .Literal()
+ .Value(item.Type)
+ .Build()
+ .Build()
+ .Build()
+ .Add()
+ .Add<TCoAtom>()
+ .Value("Attributes")
+ .Build()
+ .Add<TCoYson>()
+ .Literal()
+ .Value(item.Attributes)
+ .Build()
+ .Build()
+ .Build()
+ .Done()
+ );
+ }
+
+ return Build<TCoCons>(ctx, node->Pos())
+ .World(read.World())
+ .Input<TCoAssumeColumnOrder>()
+ .Input<TCoList>()
+ .ListType<TCoListType>()
+ .ItemType<TCoStructType>()
+ .Add<TExprList>()
+ .Add<TCoAtom>()
+ .Value("Path")
+ .Build()
+ .Add<TCoDataType>()
+ .Type()
+ .Value("String")
+ .Build()
+ .Build()
+ .Build()
+ .Add<TExprList>()
+ .Add<TCoAtom>()
+ .Value("Type")
+ .Build()
+ .Add<TCoDataType>()
+ .Type()
+ .Value("String")
+ .Build()
+ .Build()
+ .Build()
+ .Add<TExprList>()
+ .Add<TCoAtom>()
+ .Value("Attributes")
+ .Build()
+ .Add<TCoDataType>()
+ .Type()
+ .Value("Yson")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .FreeArgs()
+ .Add(listItems)
+ .Build()
+ .Build()
+ .ColumnOrder<TCoAtomList>()
+ .Add()
+ .Value("Path")
+ .Build()
+ .Add()
+ .Value("Type")
+ .Build()
+ .Add()
+ .Value("Attributes")
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ if (keys.GetType() != TYtKey::EType::Table) {
+ return node;
+ }
+
+ TVector<TExprBase> tableSettings;
+ TVector<TExprBase> readSettings;
+ SplitReadSettings(read, tableSettings, readSettings, ctx);
+
+ bool hasErrors = false;
+ bool isStrict = keys.GetStrictConcat();
+ TVector<TExprBase> tables;
+
+ for (auto& key : keys.GetKeys()) {
+ if (key.GetRange()) {
+ auto p = PendingRanges_.FindPtr(std::make_pair(cluster, *key.GetRange()));
+ YQL_ENSURE(p);
+ auto& res = p->second.GetValue();
+ res.ReportIssues(ctx.IssueManager);
+
+ if (res.Success()) {
+ for (auto& oneTable: res.Tables) {
+ TYtTableInfo tableInfo;
+ tableInfo.Name = oneTable.Path;
+ tableInfo.Cluster = cluster;
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, key.GetNode()->Pos()).Add(tableSettings);
+ if (key.GetView()) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::View)).Build()
+ .Value<TCoAtom>().Value(key.GetView()).Build()
+ .Build();
+ }
+ tableInfo.Settings = settingsBuilder.Done();
+
+ TYtPathInfo pathInfo;
+ if (oneTable.Columns) {
+ pathInfo.SetColumns(*oneTable.Columns);
+ }
+ if (oneTable.Ranges) {
+ pathInfo.Ranges = MakeIntrusive<TYtRangesInfo>();
+ pathInfo.Ranges->Parse(*oneTable.Ranges, ctx, key.GetNode()->Pos());
+ }
+ tables.push_back(pathInfo.ToExprNode(ctx, key.GetNode()->Pos(), tableInfo.ToExprNode(ctx, key.GetNode()->Pos())));
+ }
+ isStrict = isStrict && key.GetRange()->IsStrict;
+ } else {
+ hasErrors = true;
+ }
+ }
+ else if (key.IsAnonymous()) {
+ TYtTableInfo table(key, cluster);
+ table.Settings = Build<TCoNameValueTupleList>(ctx, read.Pos())
+ .Add(tableSettings)
+ .Add()
+ .Name().Value(ToString(EYtSettingType::Anonymous)).Build()
+ .Build()
+ .Done();
+ auto path = Build<TYtPath>(ctx, read.Pos())
+ .Table(table.ToExprNode(ctx, read.Pos()).Cast<TYtTable>())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ tables.push_back(path);
+ }
+ else {
+ auto p = PendingCanonizations_.FindPtr(std::make_pair(cluster, key.GetPath()));
+ YQL_ENSURE(p);
+ auto& oneTable = CanonizeFuture_.GetValue().Data.at(*p);
+ if (oneTable.Path.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node->Pos()), TStringBuilder() << "Bad table name: " << key.GetPath()));
+ hasErrors = true;
+ continue;
+ }
+ TYtTableInfo tableInfo;
+ tableInfo.Name = oneTable.Path;
+ tableInfo.Cluster = cluster;
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, key.GetNode()->Pos()).Add(tableSettings);
+ if (key.GetView()) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::View)).Build()
+ .Value<TCoAtom>().Value(key.GetView()).Build()
+ .Build();
+ }
+ tableInfo.Settings = settingsBuilder.Done();
+
+ TYtPathInfo pathInfo;
+ if (oneTable.Columns) {
+ pathInfo.SetColumns(*oneTable.Columns);
+ }
+ if (oneTable.Ranges) {
+ pathInfo.Ranges = MakeIntrusive<TYtRangesInfo>();
+ pathInfo.Ranges->Parse(*oneTable.Ranges, ctx, key.GetNode()->Pos());
+ }
+ tables.push_back(pathInfo.ToExprNode(ctx, key.GetNode()->Pos(), tableInfo.ToExprNode(ctx, key.GetNode()->Pos())));
+ }
+ }
+ if (hasErrors) {
+ return {};
+ }
+
+ if (!tables.size()) {
+ auto userSchema = GetSetting(read.Arg(4).Ref(), EYtSettingType::UserSchema);
+ if (userSchema) {
+ return BuildEmptyTablesRead(node->Pos(), *userSchema, ctx);
+ }
+
+ ctx.AddError(TIssue(ctx.GetPosition(node->Pos()), "The list of tables is empty"));
+ return {};
+ }
+
+ if (!isStrict && tables.size() > 1) {
+ readSettings.push_back(Build<TCoNameValueTuple>(ctx, read.Pos()).Name().Value(ToString(EYtSettingType::WeakConcat)).Build().Done());
+ }
+
+ auto res = read.Ptr();
+ res->ChildRef(2) = Build<TExprList>(ctx, read.Pos()).Add(tables).Done().Ptr();
+ res->ChildRef(4) = Build<TCoNameValueTupleList>(ctx, read.Pos()).Add(readSettings).Done().Ptr();
+ return res;
+
+ }, ctx, settings);
+
+ PendingCanonizations_.clear();
+ PendingRanges_.clear();
+ PendingFolders_.clear();
+ CanonizeFuture_ = {};
+ AllFuture_ = {};
+
+ return status;
+ }
+
+ void Rewind() final {
+ PendingRanges_.clear();
+ PendingFolders_.clear();
+ PendingCanonizations_.clear();
+ CanonizeFuture_ = {};
+ AllFuture_ = {};
+ }
+
+private:
+ TExprNode::TPtr ConvertTableScheme(TYtRead read, const TYtKey& key, TExprContext& ctx) {
+ if (!read.Arg(3).Ref().IsCallable(TCoVoid::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(read.Arg(3).Pos()), TStringBuilder()
+ << "Expected Void, but got: " << read.Arg(3).Ref().Content()));
+ return {};
+ }
+
+ if (!EnsureTuple(*read.Raw()->Child(4), ctx)) {
+ return {};
+ }
+
+ TYtTableInfo tableInfo(key, read.DataSource().Cluster().Value());
+
+ auto settings = NYql::RemoveSetting(read.Arg(4).Ref(), EYtSettingType::DoNotFailOnInvalidSchema, ctx);
+ if (key.GetView()) {
+ settings = NYql::AddSetting(*settings, EYtSettingType::View, ctx.NewAtom(key.GetNode()->Pos(), key.GetView()), ctx);
+ }
+ tableInfo.Settings = TExprBase(settings);
+
+ auto res = read.Ptr();
+ res->ChildRef(2) = Build<TExprList>(ctx, read.Pos())
+ .Add<TYtPath>()
+ .Table(tableInfo.ToExprNode(ctx, read.Pos()).Cast<TYtTable>())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Done().Ptr();
+ res->ChildRef(4) = Build<TCoNameValueTupleList>(ctx, read.Pos())
+ .Add()
+ .Name().Value(ToString(EYtSettingType::Scheme)).Build()
+ .Build()
+ .Done().Ptr();
+ return res;
+ }
+
+ void SplitReadSettings(TYtRead read, TVector<TExprBase>& tableSettings, TVector<TExprBase>& readSettings, TExprContext& ctx) {
+ if (auto list = read.Arg(4).Maybe<TCoNameValueTupleList>()) {
+ for (auto setting: list.Cast()) {
+ auto type = FromString<EYtSettingType>(setting.Name().Value());
+ if (ToString(type) != setting.Name().Value()) {
+ // Normalize setting name
+ setting = Build<TCoNameValueTuple>(ctx, setting.Pos())
+ .InitFrom(setting)
+ .Name()
+ .Value(ToString(type))
+ .Build()
+ .Done();
+ }
+ if (type & (EYtSettingType::InferScheme | EYtSettingType::ForceInferScheme |
+ EYtSettingType::DoNotFailOnInvalidSchema | EYtSettingType::XLock |
+ EYtSettingType::UserSchema | EYtSettingType::UserColumns | EYtSettingType::IgnoreTypeV3)) {
+ tableSettings.push_back(setting);
+ } else {
+ readSettings.push_back(setting);
+ }
+ }
+ }
+ }
+
+ TExprNode::TPtr ConvertTableRead(TYtRead read, const TYtInputKeys& keys, TExprContext& ctx) {
+ TVector<TExprBase> tableSettings;
+ TVector<TExprBase> readSettings;
+ SplitReadSettings(read, tableSettings, readSettings, ctx);
+
+ auto cluster = read.DataSource().Cluster().Value();
+ TVector<TExprBase> tables;
+ for (auto& key: keys.GetKeys()) {
+ TYtTableInfo table(key, cluster);
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, read.Pos()).Add(tableSettings);
+ if (key.GetView()) {
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::View)).Build()
+ .Value<TCoAtom>().Value(key.GetView()).Build()
+ .Build();
+ }
+ if (key.IsAnonymous()) {
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::Anonymous)).Build()
+ .Build();
+ }
+ table.Settings = settingsBuilder.Done();
+ auto path = Build<TYtPath>(ctx, read.Pos())
+ .Table(table.ToExprNode(ctx, read.Pos()).Cast<TYtTable>())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ tables.push_back(path);
+ }
+ if (!keys.GetStrictConcat() && keys.GetKeys().size() > 1) {
+ readSettings.push_back(Build<TCoNameValueTuple>(ctx, read.Pos()).Name().Value(ToString(EYtSettingType::WeakConcat)).Build().Done());
+ }
+
+ auto res = read.Ptr();
+ res->ChildRef(2) = Build<TExprList>(ctx, read.Pos()).Add(tables).Done().Ptr();
+ res->ChildRef(4) = Build<TCoNameValueTupleList>(ctx, read.Pos()).Add(readSettings).Done().Ptr();
+ return res;
+ }
+
+private:
+ TYtState::TPtr State_;
+
+ THashMap<std::pair<TString, TYtKey::TRange>, std::pair<TPosition, NThreading::TFuture<IYtGateway::TTableRangeResult>>> PendingRanges_;
+ THashMap<std::pair<TString, TYtKey::TFolderList>, std::pair<TPosition, NThreading::TFuture<IYtGateway::TFolderResult>>> PendingFolders_;
+ THashMap<std::pair<TString, TString>, size_t> PendingCanonizations_; // cluster, original table path -> positions in canon result
+ NThreading::TFuture<IYtGateway::TCanonizePathsResult> CanonizeFuture_;
+ NThreading::TFuture<void> AllFuture_;
+
+ THashMap<TString, TString> FolderFileToAlias_;
+};
+
+THolder<IGraphTransformer> CreateYtIODiscoveryTransformer(TYtState::TPtr state) {
+ return THolder(new TYtIODiscoveryTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
new file mode 100644
index 0000000000..bb0d02d4fb
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.cpp
@@ -0,0 +1,4761 @@
+#include "yql_yt_join_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_table.h"
+
+#include <ydb/library/yql/providers/yt/opt/yql_yt_join.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/core/yql_join.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/string/join.h>
+#include <util/string/cast.h>
+#include <util/string/builder.h>
+#include <util/generic/xrange.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/yexception.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/vector.h>
+#include <util/generic/map.h>
+#include <util/generic/size_literals.h>
+#include <util/generic/strbuf.h>
+
+namespace NYql {
+
+using TStatus = IGraphTransformer::TStatus;
+
+namespace {
+
+TYtJoinNode::TPtr FindYtEquiJoinLeaf(const std::vector<TYtJoinNodeLeaf::TPtr>& leaves, TStringBuf table) {
+ for (auto& leaf : leaves) {
+ if (!leaf) {
+ continue;
+ }
+
+ if (Find(leaf->Scope.begin(), leaf->Scope.end(), table) != leaf->Scope.end()) {
+ return leaf;
+ }
+ }
+
+ ythrow yexception() << "Table " << TString{table}.Quote() << " not found";
+}
+
+void GatherEquiJoinKeyColumns(const TExprNode::TPtr& columns, THashSet<TString>& keyColumns) {
+ for (ui32 i = 0; i < columns->ChildrenSize(); i += 2) {
+ auto table = columns->Child(i)->Content();
+ auto column = columns->Child(i + 1)->Content();
+ keyColumns.insert({ FullColumnName(table, column) });
+ }
+}
+
+TYtJoinNodeOp::TPtr ImportYtEquiJoinRecursive(const TVector<TYtJoinNodeLeaf::TPtr>& leaves, const TYtJoinNodeOp* parent,
+ THashSet<TString>& drops, const TExprNode& joinTree, TExprContext& ctx)
+{
+ TYtJoinNodeOp::TPtr result(new TYtJoinNodeOp());
+ result->Parent = parent;
+ result->JoinKind = joinTree.HeadPtr();
+ result->LeftLabel = joinTree.ChildPtr(3);
+ result->RightLabel = joinTree.ChildPtr(4);
+ result->LinkSettings = GetEquiJoinLinkSettings(*joinTree.Child(5));
+
+ THashSet<TString> leftKeys, rightKeys;
+ GatherEquiJoinKeyColumns(result->LeftLabel, leftKeys);
+ GatherEquiJoinKeyColumns(result->RightLabel, rightKeys);
+ if (!result->JoinKind->IsAtom({"RightOnly", "RightSemi"})) {
+ for (const auto& key : leftKeys) {
+ if (drops.contains(key)) {
+ result->OutputRemoveColumns.insert(key);
+ }
+ }
+ }
+
+ if (!result->JoinKind->IsAtom({"LeftOnly", "LeftSemi"})) {
+ for (const auto& key : rightKeys) {
+ if (drops.contains(key)) {
+ result->OutputRemoveColumns.insert(key);
+ }
+ }
+ }
+
+ std::vector<std::string_view> lCheck, rCheck;
+
+ lCheck.reserve(leftKeys.size());
+ for (const auto& key : leftKeys) {
+ drops.erase(key);
+ lCheck.emplace_back(key);
+ }
+
+ rCheck.reserve(rightKeys.size());
+ for (const auto& key : rightKeys) {
+ drops.erase(key);
+ rCheck.emplace_back(key);
+ }
+
+ result->Left = joinTree.Child(1)->IsAtom() ?
+ FindYtEquiJoinLeaf(leaves, joinTree.Child(1)->Content()):
+ ImportYtEquiJoinRecursive(leaves, result.Get(), drops, *joinTree.Child(1), ctx);
+
+ result->Right = joinTree.Child(2)->IsAtom() ?
+ FindYtEquiJoinLeaf(leaves, joinTree.Child(2)->Content()):
+ ImportYtEquiJoinRecursive(leaves, result.Get(), drops, *joinTree.Child(2), ctx);
+
+ result->Scope.insert(result->Scope.end(), result->Left->Scope.cbegin(), result->Left->Scope.cend());
+ result->Scope.insert(result->Scope.end(), result->Right->Scope.cbegin(), result->Right->Scope.cend());
+
+ const std::string_view joinKind = result->JoinKind->Content();
+ const bool singleSide = joinKind.ends_with("Only") || joinKind.ends_with("Semi");
+ const bool rightSide = joinKind.starts_with("Right");
+ const bool leftSide = joinKind.starts_with("Left");
+
+ const auto lUnique = result->Left->Constraints.GetConstraint<TUniqueConstraintNode>();
+ const auto rUnique = result->Right->Constraints.GetConstraint<TUniqueConstraintNode>();
+
+ const bool lAny = result->LinkSettings.LeftHints.contains("unique") || result->LinkSettings.LeftHints.contains("any");
+ const bool rAny = result->LinkSettings.RightHints.contains("unique") || result->LinkSettings.RightHints.contains("any");
+
+ const bool lOneRow = lAny || lUnique && lUnique->ContainsCompleteSet(lCheck);
+ const bool rOneRow = rAny || rUnique && rUnique->ContainsCompleteSet(rCheck);
+
+ if (singleSide) {
+ if (leftSide)
+ result->Constraints.AddConstraint(lUnique);
+ else if (rightSide)
+ result->Constraints.AddConstraint(rUnique);
+ } else if (!result->JoinKind->IsAtom("Cross")) {
+ const bool exclusion = result->JoinKind->IsAtom("Exclusion");
+ const bool useLeft = lUnique && (rOneRow || exclusion);
+ const bool useRight = rUnique && (lOneRow || exclusion);
+
+ if (useLeft && !useRight)
+ result->Constraints.AddConstraint(lUnique);
+ else if (useRight && !useLeft)
+ result->Constraints.AddConstraint(rUnique);
+ else if (useLeft && useRight)
+ result->Constraints.AddConstraint(TUniqueConstraintNode::Merge(lUnique, rUnique, ctx));
+ }
+
+ const auto lDistinct = result->Left->Constraints.GetConstraint<TDistinctConstraintNode>();
+ const auto rDistinct = result->Right->Constraints.GetConstraint<TDistinctConstraintNode>();
+
+ if (singleSide) {
+ if (leftSide)
+ result->Constraints.AddConstraint(lDistinct);
+ else if (rightSide)
+ result->Constraints.AddConstraint(rDistinct);
+ } else if (!result->JoinKind->IsAtom("Cross")) {
+ const bool inner = result->JoinKind->IsAtom("Inner");
+ const bool useLeft = lDistinct && rOneRow && (inner || leftSide);
+ const bool useRight = rDistinct && lOneRow && (inner || rightSide);
+
+ if (useLeft && !useRight)
+ result->Constraints.AddConstraint(lDistinct);
+ else if (useRight && !useLeft)
+ result->Constraints.AddConstraint(rDistinct);
+ else if (useLeft && useRight)
+ result->Constraints.AddConstraint(TDistinctConstraintNode::Merge(lDistinct, rDistinct, ctx));
+ }
+
+ const auto lEmpty = result->Left->Constraints.GetConstraint<TEmptyConstraintNode>();
+ const auto rEmpty = result->Right->Constraints.GetConstraint<TEmptyConstraintNode>();
+
+ if (lEmpty || rEmpty) {
+ if (result->JoinKind->IsAtom({"Inner", "LeftSemi", "RightSemi", "Cross"}))
+ result->Constraints.AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ else if (leftSide && lEmpty)
+ result->Constraints.AddConstraint(lEmpty);
+ else if (rightSide && rEmpty)
+ result->Constraints.AddConstraint(rEmpty);
+ else if (lEmpty && rEmpty)
+ result->Constraints.AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
+ }
+
+ return result;
+}
+
+bool IsEffectivelyUnique(const TEquiJoinLinkSettings& linkSettings, const TMapJoinSettings& settings, bool isLeft) {
+ auto& hints = isLeft ? linkSettings.LeftHints : linkSettings.RightHints;
+ return hints.contains("unique") || hints.contains("any") || (isLeft ? settings.LeftUnique : settings.RightUnique);
+}
+
+bool HasNonTrivialAny(const TEquiJoinLinkSettings& linkSettings, const TMapJoinSettings& settings, TChoice side) {
+ YQL_ENSURE(IsLeftOrRight(side));
+ auto& hints = (side == TChoice::Left) ? linkSettings.LeftHints : linkSettings.RightHints;
+ bool unique = ((side == TChoice::Left) ? settings.LeftUnique : settings.RightUnique) || hints.contains("unique");
+ return hints.contains("any") && !unique;
+}
+
+ui64 CalcInMemorySize(const TJoinLabels& labels, const TYtJoinNodeOp& op,
+ TExprContext& ctx, const TMapJoinSettings& settings, bool isLeft, bool needPayload, ui64 size, bool mapJoinUseFlow)
+{
+ const TJoinLabel& label = labels.Inputs[isLeft ? 0 : 1];
+
+ const ui64 rows = isLeft ? settings.LeftRows : settings.RightRows;
+
+ if (op.JoinKind->Content() == "Cross") {
+ if (mapJoinUseFlow) {
+ return size + rows * (1ULL + label.InputType->GetSize()) * sizeof(NKikimr::NUdf::TUnboxedValuePod); // Table content after Collect
+ } else {
+ ui64 avgOtherSideWeight = (isLeft ? settings.RightSize : settings.LeftSize) / (isLeft ? settings.RightRows : settings.LeftRows);
+
+ ui64 rowFactor = (1 + label.InputType->GetSize()) * sizeof(NKikimr::NUdf::TUnboxedValuePod); // Table content after Collect
+ rowFactor += (1 + label.InputType->GetSize() + labels.Inputs[isLeft ? 1 : 0].InputType->GetSize()) * sizeof(NKikimr::NUdf::TUnboxedValuePod); // Table content after Map with added left side
+ rowFactor += avgOtherSideWeight; // Average added left side for each row after Map
+
+ return 2 * size + rows * rowFactor;
+ }
+ }
+
+ const bool isUniqueKey = IsEffectivelyUnique(op.LinkSettings, settings, isLeft);
+
+ bool many = needPayload && !isUniqueKey;
+
+ auto inputKeyType = BuildJoinKeyType(label, *(isLeft ? op.LeftLabel : op.RightLabel));
+ auto keyType = AsDictKeyType(inputKeyType, ctx);
+ const TTypeAnnotationNode* payloadType = needPayload ? label.InputType : (const TTypeAnnotationNode*)ctx.MakeType<TVoidExprType>();
+
+ double sizeFactor = 1.;
+ ui64 rowFactor = 0;
+ CalcToDictFactors(keyType, payloadType, EDictType::Hashed, many, true, sizeFactor, rowFactor);
+
+ return size * sizeFactor + rows * rowFactor;
+}
+
+IGraphTransformer::TStatus TryEstimateDataSizeChecked(TVector<ui64>& result, TYtSection& inputSection, const TString& cluster,
+ const TVector<TYtPathInfo::TPtr>& paths, const TMaybe<TVector<TString>>& columns, const TYtState& state, TExprContext& ctx)
+{
+ if (GetJoinCollectColumnarStatisticsMode(*state.Configuration) == EJoinCollectColumnarStatisticsMode::Sync) {
+ auto syncResult = EstimateDataSize(cluster, paths, columns, state, ctx);
+ if (!syncResult) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ result = std::move(*syncResult);
+ return IGraphTransformer::TStatus::Ok;
+ }
+
+ TSet<TString> requestedColumns;
+ auto status = TryEstimateDataSize(result, requestedColumns, cluster, paths, columns, state, ctx);
+ if (status == TStatus::Repeat) {
+ bool hasStatColumns = NYql::HasSetting(inputSection.Settings().Ref(), EYtSettingType::StatColumns);
+ if (hasStatColumns) {
+ auto oldColumns = NYql::GetSettingAsColumnList(inputSection.Settings().Ref(), EYtSettingType::StatColumns);
+ TSet<TString> oldColumnSet(oldColumns.begin(), oldColumns.end());
+
+ bool alreadyRequested = AllOf(requestedColumns, [&](const auto& c) {
+ return oldColumnSet.contains(c);
+ });
+
+ YQL_ENSURE(!alreadyRequested);
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Stat missing for columns: " << JoinSeq(", ", requestedColumns) << ", rebuilding section";
+ TVector<TString> requestedColumnList(requestedColumns.begin(), requestedColumns.end());
+
+ inputSection = Build<TYtSection>(ctx, inputSection.Ref().Pos())
+ .InitFrom(inputSection)
+ .Settings(NYql::AddSettingAsColumnList(inputSection.Settings().Ref(), EYtSettingType::StatColumns, requestedColumnList, ctx))
+ .Done();
+ }
+ return status;
+}
+
+
+TStatus UpdateInMemorySizeSetting(TMapJoinSettings& settings, TYtSection& inputSection, const TJoinLabels& labels,
+ const TYtJoinNodeOp& op, TExprContext& ctx, bool isLeft,
+ const TStructExprType* itemType, const TVector<TString>& joinKeyList, const TYtState::TPtr& state, const TString& cluster,
+ const TVector<TYtPathInfo::TPtr>& tables, bool mapJoinUseFlow)
+{
+ ui64 size = isLeft ? settings.LeftSize : settings.RightSize;
+ const bool needPayload = (op.JoinKind->Content() == "Inner"
+ || op.JoinKind->Content() == (isLeft ? "Right" : "Left"));
+
+ if (!needPayload && op.JoinKind->Content() != "Cross") {
+ if (joinKeyList.size() < itemType->GetSize()) {
+ TVector<ui64> dataSizes;
+ auto status = TryEstimateDataSizeChecked(dataSizes, inputSection, cluster, tables, joinKeyList, *state, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ size = Accumulate(dataSizes.begin(), dataSizes.end(), 0ull, [](ui64 sum, ui64 v) { return sum + v; });;
+ }
+ }
+
+ (isLeft ? settings.LeftMemSize : settings.RightMemSize) =
+ CalcInMemorySize(labels, op, ctx, settings, isLeft, needPayload, size, mapJoinUseFlow);
+ return TStatus::Ok;
+}
+
+TYtJoinNodeLeaf::TPtr ConvertYtEquiJoinToLeaf(const TYtJoinNodeOp& op, TPositionHandle pos, TExprContext& ctx) {
+ auto joinLabelBuilder = Build<TCoAtomList>(ctx, pos);
+ for (auto& x : op.Scope) {
+ joinLabelBuilder.Add().Value(x).Build();
+ }
+ auto label = joinLabelBuilder.Done().Ptr();
+ auto section = Build<TYtSection>(ctx, pos)
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation(op.Output.Cast())
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinLabel))
+ .Build()
+ .Value(label)
+ .Build()
+ .Build()
+ .Done();
+
+ TYtJoinNodeLeaf::TPtr leaf = MakeIntrusive<TYtJoinNodeLeaf>(section, TMaybeNode<TCoLambda>());
+ leaf->Scope = op.Scope;
+ leaf->Label = label;
+ return leaf;
+}
+
+void UpdateSortPrefix(bool initial, TVector<TString>& sortedKeys, const TYqlRowSpecInfo::TPtr& rowSpec,
+ bool& areUniqueKeys, const THashSet<TString>& keySet, TMaybeNode<TCoLambda> premap)
+{
+ if (!rowSpec) {
+ areUniqueKeys = false;
+ sortedKeys.clear();
+ return;
+ }
+ if (initial) {
+ sortedKeys = rowSpec->SortedBy;
+ TMaybe<THashSet<TStringBuf>> passthroughFields;
+ bool isPassthrough = true;
+
+ if (premap) {
+ isPassthrough = IsPassthroughLambda(premap.Cast(), &passthroughFields);
+ }
+
+ for (size_t pos = 0; isPassthrough && pos < rowSpec->SortDirections.size(); ++pos) {
+ if (!rowSpec->SortDirections[pos] ||
+ (premap && passthroughFields && !(*passthroughFields).contains(rowSpec->SortedBy[pos])))
+ {
+ sortedKeys.resize(pos);
+ break;
+ }
+ }
+ areUniqueKeys = areUniqueKeys && (rowSpec->SortedBy.size() == sortedKeys.size());
+ }
+ else {
+ size_t newPrefixLength = Min(sortedKeys.size(), rowSpec->SortedBy.size());
+ for (size_t pos = 0; pos < newPrefixLength; ++pos) {
+ if (sortedKeys[pos] != rowSpec->SortedBy[pos] || !rowSpec->SortDirections[pos]) {
+ newPrefixLength = pos;
+ break;
+ }
+ }
+
+ areUniqueKeys = areUniqueKeys && (newPrefixLength == sortedKeys.size());
+ sortedKeys.resize(newPrefixLength);
+ }
+
+ areUniqueKeys = areUniqueKeys && rowSpec->UniqueKeys && sortedKeys.size() == keySet.size();
+ ui32 foundKeys = 0;
+ for (auto key : sortedKeys) {
+ if (keySet.contains(key)) {
+ ++foundKeys;
+ } else {
+ break;
+ }
+ }
+
+ if (foundKeys != keySet.size()) {
+ areUniqueKeys = false;
+ sortedKeys.clear();
+ }
+}
+
+TVector<TString> BuildCompatibleSortWith(const TVector<TString>& otherSortedKeys, const TVector<TString>& otherJoinKeyList,
+ const TVector<TString>& myJoinKeyList)
+{
+ YQL_ENSURE(myJoinKeyList.size() == otherJoinKeyList.size());
+ YQL_ENSURE(otherSortedKeys.size() >= otherJoinKeyList.size());
+
+ THashMap<TString, size_t> otherJoinListPos;
+ for (size_t i = 0; i < otherJoinKeyList.size(); ++i) {
+ otherJoinListPos[otherJoinKeyList[i]] = i;
+ }
+
+ YQL_ENSURE(otherJoinListPos.size() == otherJoinKeyList.size());
+
+
+ TVector<TString> mySortedKeys;
+ mySortedKeys.reserve(myJoinKeyList.size());
+
+ for (size_t i = 0; i < Min(myJoinKeyList.size(), otherSortedKeys.size()); ++i) {
+ auto otherKey = otherSortedKeys[i];
+ auto joinPos = otherJoinListPos.find(otherKey);
+ YQL_ENSURE(joinPos != otherJoinListPos.end());
+
+ mySortedKeys.push_back(myJoinKeyList[joinPos->second]);
+ }
+
+
+ return mySortedKeys;
+}
+
+
+TVector<TString> BuildCommonSortPrefix(const TVector<TString>& leftSortedKeys, const TVector<TString>& rightSortedKeys,
+ const TVector<TString>& leftJoinKeyList, const TVector<TString>& rightJoinKeyList,
+ THashMap<TString, TString>& leftKeyRenames, THashMap<TString, TString>& rightKeyRenames,
+ bool allowColumnRenames)
+{
+ TVector<TString> result;
+
+ YQL_ENSURE(leftJoinKeyList.size() == rightJoinKeyList.size());
+ size_t joinSize = leftJoinKeyList.size();
+
+ THashMap<TString, size_t> leftJoinListPos;
+ THashMap<TString, size_t> rightJoinListPos;
+ if (allowColumnRenames) {
+ for (size_t i = 0; i < joinSize; ++i) {
+ leftJoinListPos[leftJoinKeyList[i]] = i;
+ rightJoinListPos[rightJoinKeyList[i]] = i;
+ }
+ }
+
+ for (size_t pos = 0; pos < Min(leftSortedKeys.size(), rightSortedKeys.size(), joinSize); ++pos) {
+ auto left = leftSortedKeys[pos];
+ auto right = rightSortedKeys[pos];
+ if (left == right) {
+ result.push_back(left);
+ continue;
+ }
+
+ if (!allowColumnRenames) {
+ break;
+ }
+
+ auto l = leftJoinListPos.find(left);
+ YQL_ENSURE(l != leftJoinListPos.end());
+ auto leftPos = l->second;
+
+ auto r = rightJoinListPos.find(right);
+ YQL_ENSURE(r != rightJoinListPos.end());
+ auto rightPos = r->second;
+
+ if (leftPos != rightPos) {
+ break;
+ }
+
+ TString rename = TStringBuilder() << "_yql_join_renamed_column_" << leftPos;
+ leftKeyRenames[left] = rename;
+ rightKeyRenames[right] = rename;
+
+ result.push_back(rename);
+ }
+
+ return result;
+}
+
+TYtSection SectionApplyRenames(const TYtSection& section, const THashMap<TString, TString>& renames, TExprContext& ctx) {
+ if (!renames) {
+ return section;
+ }
+
+ TVector<TYtPath> updatedPaths;
+ for (size_t p = 0; p < section.Paths().Size(); ++p) {
+ auto path = section.Paths().Item(p);
+
+ TYtColumnsInfo columnsInfo(path.Columns());
+ columnsInfo.SetRenames(renames);
+
+ updatedPaths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Columns(columnsInfo.ToExprNode(ctx, path.Columns().Pos()))
+ .Done()
+ );
+ }
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Done();
+}
+
+TVector<TString> KeysApplyInputRenames(const TVector<TString>& keys, const THashMap<TString, TString>& renames) {
+ auto result = keys;
+ for (auto& i : result) {
+ auto r = renames.find(i);
+ if (r != renames.end()) {
+ i = r->second;
+ }
+ }
+ return result;
+}
+
+TExprNode::TPtr BuildReverseRenamingLambda(TPositionHandle pos, const THashMap<TString, TString>& renames, TExprContext& ctx) {
+ auto arg = ctx.NewArgument(pos, "item");
+ auto body = arg;
+
+ for (auto& r : renames) {
+ auto from = r.second;
+ auto to = r.first;
+ body = ctx.Builder(body->Pos())
+ .Callable("RemoveMember")
+ .Callable(0, "AddMember")
+ .Add(0, body)
+ .Atom(1, to)
+ .Callable(2, "Member")
+ .Add(0, body)
+ .Atom(1, from)
+ .Seal()
+ .Seal()
+ .Atom(1, from)
+ .Seal()
+ .Build();
+ }
+
+ return ctx.NewLambda(pos, ctx.NewArguments(pos, { arg }), std::move(body));
+}
+
+TMaybeNode<TCoLambda> GetPremapLambda(const TYtJoinNodeLeaf& leaf) {
+ return leaf.Premap;
+}
+
+ETypeAnnotationKind DeriveCommonSequenceKind(ETypeAnnotationKind one, ETypeAnnotationKind two) {
+ if (one == ETypeAnnotationKind::Stream || two == ETypeAnnotationKind::Stream) {
+ return ETypeAnnotationKind::Stream;
+ }
+
+ if (one == ETypeAnnotationKind::List || two == ETypeAnnotationKind::List) {
+ return ETypeAnnotationKind::List;
+ }
+
+ return ETypeAnnotationKind::Optional;
+}
+
+void ApplyInputPremap(TExprNode::TPtr& lambda, const TMaybeNode<TCoLambda>& premap, ETypeAnnotationKind commonKind,
+ const THashMap<TString, TString>& renames, TExprContext& ctx)
+{
+ TStringBuf converter;
+ if (commonKind == ETypeAnnotationKind::Stream) {
+ converter = "ToStream";
+ } else if (commonKind == ETypeAnnotationKind::List) {
+ converter = "ToList";
+ } else if (commonKind == ETypeAnnotationKind::Optional) {
+ converter = "ToSequence";
+ } else {
+ YQL_ENSURE(false, "unexpected common kind");
+ }
+
+ auto reverseRenamingLambda = BuildReverseRenamingLambda(lambda->Pos(), renames, ctx);
+
+ if (premap) {
+ lambda = ctx.Builder(lambda->Pos())
+ .Lambda()
+ .Param("item")
+ .Callable(converter)
+ .Callable(0, "FlatMap")
+ .Apply(0, premap.Cast().Ptr())
+ .With(0)
+ .Apply(reverseRenamingLambda)
+ .With(0, "item")
+ .Seal()
+ .Done()
+ .Seal()
+ .Add(1, lambda)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ lambda = ctx.Builder(lambda->Pos())
+ .Lambda()
+ .Param("item")
+ .Callable(converter)
+ .Apply(0, lambda)
+ .With(0)
+ .Apply(reverseRenamingLambda)
+ .With(0, "item")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+}
+
+void ApplyInputPremap(TExprNode::TPtr& lambda, const TYtJoinNodeLeaf& leaf,
+ const TYtJoinNodeLeaf& otherLeaf, TExprContext& ctx,
+ const THashMap<TString, TString>& renames = THashMap<TString, TString>())
+{
+ auto premap = GetPremapLambda(leaf);
+ auto otherPremap = GetPremapLambda(otherLeaf);
+
+ auto commonKind = ETypeAnnotationKind::Optional;
+ if (premap) {
+ YQL_ENSURE(EnsureSeqOrOptionalType(premap.Cast().Ref(), ctx));
+ commonKind = DeriveCommonSequenceKind(commonKind, premap.Cast().Ref().GetTypeAnn()->GetKind());
+ }
+ if (otherPremap) {
+ YQL_ENSURE(EnsureSeqOrOptionalType(otherPremap.Cast().Ref(), ctx));
+ commonKind = DeriveCommonSequenceKind(commonKind, otherPremap.Cast().Ref().GetTypeAnn()->GetKind());
+ }
+
+ ApplyInputPremap(lambda, premap, commonKind, renames, ctx);
+}
+
+
+
+TString RenamesToString(const THashMap<TString, TString>& renames) {
+ TVector<TString> renamesStr;
+ renamesStr.reserve(renames.size());
+
+ for (auto& r : renames) {
+ renamesStr.emplace_back(r.first + " -> " + r.second);
+ }
+
+ return JoinSeq(", ", renamesStr);
+}
+
+struct TEquivKeys {
+ TSet<TString> Keys;
+ const TTypeAnnotationNode* Type = nullptr;
+};
+
+TTypeAnnotationNode::TListType GetTypes(const TVector<TEquivKeys>& input) {
+ TTypeAnnotationNode::TListType result;
+ result.reserve(input.size());
+ for (auto& i : input) {
+ result.push_back(i.Type);
+ }
+ return result;
+}
+
+void FlattenKeys(TVector<TString>& keys, TTypeAnnotationNode::TListType& types, const TVector<TEquivKeys>& input) {
+ for (auto& i : input) {
+ auto type = i.Type;
+ for (auto& key : i.Keys) {
+ keys.push_back(key);
+ types.push_back(type);
+ }
+ }
+}
+
+void LogOutputSideSort(const TVector<TEquivKeys>& outSort, bool isLeft) {
+ TVector<TString> strs;
+ for (auto& s : outSort) {
+ strs.push_back(TString::Join("{", JoinSeq(", ", s.Keys), "}"));
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Output sort for " << (isLeft ? "left" : "right")
+ << " side: [" << JoinSeq(", ", strs) << "]";
+}
+
+void BuildOutputSideSort(TVector<TEquivKeys>& outSort, bool isLeft, TStringBuf joinType,
+ const TStructExprType& outType, const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ const TJoinLabel& label, const TVector<TString>& inputSort)
+{
+ YQL_ENSURE(joinType != "Cross");
+
+ if (joinType == "Exclusion" || joinType == "Full") {
+ // output is not sorted for either side
+ return;
+ }
+
+ if (isLeft && (joinType == "Right" || joinType == "RightOnly" || joinType == "RightSemi")) {
+ // no data or not sorted
+ return;
+ }
+
+ if (!isLeft && (joinType == "Left" || joinType == "LeftOnly" || joinType == "LeftSemi")) {
+ // no data or not sorted
+ return;
+ }
+
+ for (auto& key : inputSort) {
+ auto name = label.FullName(key);
+
+ TVector<TStringBuf> newNames;
+ newNames.push_back(name);
+ if (auto renamed = renameMap.FindPtr(name)) {
+ newNames = *renamed;
+ }
+
+ if (newNames.empty()) {
+ // stop on first deleted sort key
+ return;
+ }
+
+ TEquivKeys keys;
+
+ for (auto n : newNames) {
+ auto maybeIdx = outType.FindItem(n);
+ if (!maybeIdx.Defined()) {
+ return;
+ }
+
+ bool inserted = keys.Keys.insert(ToString(n)).second;
+ YQL_ENSURE(inserted, "Duplicate key: " << n);
+
+ if (!keys.Type) {
+ keys.Type = outType.GetItems()[*maybeIdx]->GetItemType();
+ } else {
+ YQL_ENSURE(keys.Type == outType.GetItems()[*maybeIdx]->GetItemType());
+ }
+ }
+
+ outSort.emplace_back(std::move(keys));
+ }
+}
+
+TVector<TString> MatchSort(const THashSet<TString>& desiredKeys, const TVector<TEquivKeys>& sideSort) {
+ TVector<TString> result;
+
+ THashSet<TString> matchedKeys;
+ for (auto& k : sideSort) {
+ // only single alternative
+ YQL_ENSURE(k.Keys.size() == 1);
+ auto key = *k.Keys.begin();
+ if (desiredKeys.contains(key)) {
+ matchedKeys.insert(key);
+ result.push_back(key);
+ } else {
+ break;
+ }
+ }
+
+ if (matchedKeys.size() != desiredKeys.size()) {
+ result.clear();
+ }
+
+ return result;
+}
+
+TVector<TStringBuf> MatchSort(TTypeAnnotationNode::TListType& types, const TVector<TStringBuf>& desiredSort, const TVector<TEquivKeys>& sideSort) {
+ TVector<TStringBuf> result;
+ types.clear();
+ for (size_t i = 0, j = 0; i < desiredSort.size() && j < sideSort.size(); ++i) {
+ auto key = desiredSort[i];
+ if (sideSort[j].Keys.contains(key) ||
+ (j + 1 < sideSort.size() && sideSort[++j].Keys.contains(key)))
+ {
+ result.push_back(key);
+ types.push_back(sideSort[j].Type);
+ } else {
+ break;
+ }
+ }
+
+ if (result.size() != desiredSort.size()) {
+ result.clear();
+ types.clear();
+ }
+
+ return result;
+}
+
+size_t GetSortWeight(TMap<TVector<TStringBuf>, size_t>& weights, const TVector<TStringBuf>& sort, const TSet<TVector<TStringBuf>>& sorts) {
+ if (sort.empty() || !sorts.contains(sort)) {
+ return 0;
+ }
+
+ if (auto w = weights.FindPtr(sort)) {
+ return *w;
+ }
+
+ size_t weight = 1;
+ auto currSort = sort;
+ for (size_t i = currSort.size() - 1; i > 0; --i) {
+ currSort.resize(i);
+ auto currWeight = GetSortWeight(weights, currSort, sorts);
+ if (currWeight) {
+ weight += currWeight;
+ break;
+ }
+ }
+
+ weights[sort] = weight;
+ return weight;
+}
+
+
+void BuildOutputSort(const TYtOutTableInfo& outTableInfo, const TYtJoinNodeOp& op, TStringBuf joinType, bool setTopLevelFullSort,
+ const TStructExprType& outItemType, const TMap<TStringBuf, TVector<TStringBuf>>& renameMap,
+ const TSet<TVector<TStringBuf>>& topLevelSorts,
+ const TVector<TString>& leftSortedKeys, const TVector<TString>& rightSortedKeys,
+ const TJoinLabel& leftLabel, const TJoinLabel& rightLabel)
+{
+ {
+ TVector<TString> strs;
+ for (auto& r : renameMap) {
+ strs.push_back(TString::Join(r.first, " -> {", JoinSeq(", ", r.second), "}"));
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Join renames: [" << JoinSeq(", ", strs) << "]";
+ }
+
+ {
+ TVector<TString> strs;
+ for (auto& s: topLevelSorts) {
+ strs.push_back(TString::Join("{", JoinSeq(", ", s), "}"));
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Top level sorts: [" << JoinSeq(", ", strs) << "]";
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Output Type: " << static_cast<const TTypeAnnotationNode&>(outItemType);
+
+ YQL_CLOG(INFO, ProviderYt) << "Deriving output sort order for " << (op.Parent ? "intermediate " : "final ")
+ << joinType << " join. Input left side sort: [" << JoinSeq(", ", leftSortedKeys)
+ << "], input right side sort: [" << JoinSeq(", ", rightSortedKeys) << "]";
+
+ TVector<TEquivKeys> outLeftSorted;
+ BuildOutputSideSort(outLeftSorted, true, joinType, outItemType, renameMap, leftLabel, leftSortedKeys);
+ LogOutputSideSort(outLeftSorted, true);
+
+ TVector<TEquivKeys> outRightSorted;
+ BuildOutputSideSort(outRightSorted, false, joinType, outItemType, renameMap, rightLabel, rightSortedKeys);
+ LogOutputSideSort(outRightSorted, false);
+
+ TVector<TString> outSorted;
+ TTypeAnnotationNode::TListType outSortTypes;
+
+ if (op.Parent) {
+ if (op.Parent->JoinKind->Content() != "Cross") {
+ THashSet<TString> desiredOutputSortKeys;
+ YQL_ENSURE(&op == op.Parent->Left.Get() || &op == op.Parent->Right.Get());
+ auto parentLabel = (&op == op.Parent->Left.Get()) ? op.Parent->LeftLabel : op.Parent->RightLabel;
+ for (ui32 i = 0; i < parentLabel->ChildrenSize(); i += 2) {
+ auto table = parentLabel->Child(i)->Content();
+ auto column = parentLabel->Child(i + 1)->Content();
+ desiredOutputSortKeys.insert(FullColumnName(table, column));
+ }
+
+ YQL_ENSURE(!desiredOutputSortKeys.empty());
+
+ YQL_CLOG(INFO, ProviderYt) << "Desired output sort keys for next join: [" << JoinSeq(", ", desiredOutputSortKeys) << "]";
+
+ THashSet<TString> matchedSortKeys;
+
+ auto leftMatch = MatchSort(desiredOutputSortKeys, outLeftSorted);
+ auto rightMatch = MatchSort(desiredOutputSortKeys, outRightSorted);
+
+ // choose longest sort
+ if (leftMatch.size() > rightMatch.size() || (leftMatch.size() == rightMatch.size() && leftMatch <= rightMatch)) {
+ outSorted = leftMatch;
+ outSortTypes = GetTypes(outLeftSorted);
+ } else {
+ outSorted = rightMatch;
+ outSortTypes = GetTypes(outRightSorted);
+ }
+ }
+ } else if (setTopLevelFullSort) {
+ FlattenKeys(outSorted, outSortTypes, outLeftSorted);
+ FlattenKeys(outSorted, outSortTypes, outRightSorted);
+ } else {
+ TMap<TVector<TStringBuf>, size_t> weights;
+
+ size_t bestWeight = 0;
+ TVector<TStringBuf> bestSort;
+
+ for (auto& requestedSort : topLevelSorts) {
+ TTypeAnnotationNode::TListType leftMatchTypes;
+ auto leftMatch = MatchSort(leftMatchTypes, requestedSort, outLeftSorted);
+ auto weight = GetSortWeight(weights, leftMatch, topLevelSorts);
+ if (weight > bestWeight) {
+ bestWeight = weight;
+ bestSort = leftMatch;
+ outSortTypes = leftMatchTypes;
+ }
+
+ TTypeAnnotationNode::TListType rightMatchTypes;
+ auto rightMatch = MatchSort(rightMatchTypes, requestedSort, outRightSorted);
+ weight = GetSortWeight(weights, rightMatch, topLevelSorts);
+ if (weight > bestWeight) {
+ bestWeight = weight;
+ bestSort = rightMatch;
+ outSortTypes = rightMatchTypes;
+ }
+ }
+
+ outSorted.clear();
+ outSorted.reserve(bestSort.size());
+ for (auto& k : bestSort) {
+ outSorted.push_back(ToString(k));
+ }
+ }
+
+ YQL_ENSURE(outSortTypes.size() >= outSorted.size());
+ outSortTypes.resize(outSorted.size());
+ if (!outSorted.empty()) {
+ outTableInfo.RowSpec->SortMembers = outSorted;
+ outTableInfo.RowSpec->SortedBy = outSorted;
+ outTableInfo.RowSpec->SortedByTypes = outSortTypes;
+ outTableInfo.RowSpec->SortDirections = TVector<bool>(outSorted.size(), true);
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Resulting output sort: [" << JoinSeq(", ", outTableInfo.RowSpec->SortedBy) << "]";
+}
+
+const TStructExprType* MakeIntermediateEquiJoinTableType(TPositionHandle pos, const TExprNode& joinTree,
+ const TJoinLabels& labels, const THashSet<TString>& outputRemoveColumns, TExprContext& ctx)
+{
+ auto reduceColumnTypes = GetJoinColumnTypes(joinTree, labels, ctx);
+ TVector<const TItemExprType*> structItems;
+ for (auto& x : reduceColumnTypes) {
+ if (!outputRemoveColumns.contains(x.first)) {
+ structItems.push_back(ctx.MakeType<TItemExprType>(x.first, x.second));
+ }
+ }
+
+ auto reduceResultType = ctx.MakeType<TStructExprType>(structItems);
+ if (!reduceResultType->Validate(pos, ctx)) {
+ return nullptr;
+ }
+
+ return reduceResultType;
+}
+
+void AddAnyJoinOptionsToCommonJoinCore(TExprNode::TListType& options, bool swapTables, const TEquiJoinLinkSettings& linkSettings, TPositionHandle pos, TExprContext& ctx) {
+ TExprNode::TListType anySettings;
+
+ const auto& leftHints = swapTables ? linkSettings.RightHints : linkSettings.LeftHints;
+ const auto& rightHints = swapTables ? linkSettings.LeftHints : linkSettings.RightHints;
+
+ if (leftHints.contains("any")) {
+ anySettings.push_back(ctx.NewAtom(pos, "left", TNodeFlags::Default));
+ }
+
+ if (rightHints.contains("any")) {
+ anySettings.push_back(ctx.NewAtom(pos, "right", TNodeFlags::Default));
+ }
+
+ if (!anySettings.empty()) {
+ options.push_back(
+ ctx.Builder(pos)
+ .List()
+ .Atom(0, "any", TNodeFlags::Default)
+ .Add(1, ctx.NewList(pos, std::move(anySettings)))
+ .Seal()
+ .Build());
+ }
+}
+
+TExprNode::TPtr BuildYtReduceLambda(TPositionHandle pos, const TExprNode::TPtr& groupArg, TExprNode::TPtr&& flatMapLambdaBody, const bool sysColumns, TExprContext& ctx)
+{
+ TExprNode::TPtr chopperHandler = ctx.NewLambda(pos, ctx.NewArguments(pos, {ctx.NewArgument(pos, "stup"), groupArg }), std::move(flatMapLambdaBody));
+ TExprNode::TPtr chopperSwitch;
+
+ if (sysColumns) {
+ chopperSwitch = ctx.Builder(pos)
+ .Lambda()
+ .Param("key")
+ .Param("item")
+ .Callable("SqlExtractKey")
+ .Arg(0, "item")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ chopperHandler = ctx.Builder(pos)
+ .Lambda()
+ .Param("key")
+ .Param("group")
+ .Apply(chopperHandler)
+ .With(0, "key")
+ .With(1)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "group")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ else {
+ chopperSwitch = ctx.Builder(pos)
+ .Lambda()
+ .Param("key")
+ .Param("item")
+ .Callable("YtIsKeySwitch")
+ .Callable(0, "DependsOn")
+ .Arg(0, "item")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ return ctx.Builder(pos)
+ .Lambda()
+ .Param("flow")
+ .Callable("Chopper")
+ .Arg(0, "flow")
+ .Lambda(1)
+ .Param("item")
+ .Callable("Uint64") // Fake const key
+ .Atom(0, "0", TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Add(2, chopperSwitch)
+ .Add(3, chopperHandler)
+ .Seal()
+ .Seal()
+ .Build();
+}
+
+struct TMergeJoinSortInfo {
+ TChoice AdditionalSort = TChoice::None;
+ TChoice NeedRemapBeforeSort = TChoice::None;
+
+ const TStructExprType* LeftBeforePremap = nullptr;
+ const TStructExprType* RightBeforePremap = nullptr;
+
+ TVector<TString> LeftSortedKeys;
+ TVector<TString> RightSortedKeys;
+
+ TVector<TString> CommonSortedKeys;
+
+ THashMap<TString, TString> LeftKeyRenames;
+ THashMap<TString, TString> RightKeyRenames;
+};
+
+TMergeJoinSortInfo Invert(const TMergeJoinSortInfo& info) {
+ auto result = info;
+ result.AdditionalSort = Invert(result.AdditionalSort);
+ result.NeedRemapBeforeSort = Invert(result.NeedRemapBeforeSort);
+ std::swap(result.LeftBeforePremap, result.RightBeforePremap);
+ std::swap(result.LeftSortedKeys, result.RightSortedKeys);
+ std::swap(result.LeftKeyRenames, result.RightKeyRenames);
+ return result;
+}
+
+TYtSection SectionApplyAdditionalSort(const TYtSection& section, const TYtEquiJoin& equiJoin, const TVector<TString>& sortTableOrder, const TStructExprType* sortTableType,
+ bool needRemapBeforeSort, const TYtState& state, TExprContext& ctx)
+{
+ auto pos = equiJoin.Pos();
+ auto inputWorld = equiJoin.World();
+ auto inputSection = section;
+
+ TTypeAnnotationNode::TListType sortedByTypes;
+ for (auto& column: sortTableOrder) {
+ auto ndx = sortTableType->FindItem(column);
+ YQL_ENSURE(ndx.Defined(), "Missing column " << column);
+ sortedByTypes.push_back(sortTableType->GetItems()[*ndx]->GetItemType());
+ }
+
+ TVector<bool> sortDirections(sortTableOrder.size(), true);
+
+ if (needRemapBeforeSort) {
+ inputSection = Build<TYtSection>(ctx, pos)
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .World(inputWorld)
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add(inputSection)
+ .Build()
+ .Output()
+ .Add(TYtOutTableInfo(sortTableType, state.Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE)
+ .ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(pos, state, ctx))
+ .Mapper()
+ .Args({"list"})
+ .Body("list")
+ .Build()
+ .Build()
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings().Build()
+ .Done();
+
+ inputWorld = Build<TCoWorld>(ctx, pos).Done();
+ }
+
+ TYtOutTableInfo sortOut(sortTableType, state.Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ sortOut.RowSpec->SortMembers = sortTableOrder;
+ sortOut.RowSpec->SortedBy = sortTableOrder;
+ sortOut.RowSpec->SortedByTypes = sortedByTypes;
+ sortOut.RowSpec->SortDirections = sortDirections;
+
+ return Build<TYtSection>(ctx, pos)
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation<TYtSort>()
+ .World(inputWorld)
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add(inputSection)
+ .Build()
+ .Output()
+ .Add(sortOut.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings().Build()
+ .Build()
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings().Build()
+ .Done();
+}
+
+
+bool RewriteYtMergeJoin(TYtEquiJoin equiJoin, const TJoinLabels& labels, TYtJoinNodeOp& op,
+ const TYtJoinNodeLeaf& leftLeaf, const TYtJoinNodeLeaf& rightLeaf, const TYtState::TPtr& state, TExprContext& ctx,
+ bool swapTables, bool joinReduce, bool tryFirstAsPrimary, bool joinReduceForSecond,
+ const TMergeJoinSortInfo sortInfo, bool& skipped)
+{
+ skipped = false;
+ auto pos = equiJoin.Pos();
+
+ auto leftKeyColumns = op.LeftLabel;
+ auto rightKeyColumns = op.RightLabel;
+ if (swapTables) {
+ DoSwap(leftKeyColumns, rightKeyColumns);
+ }
+
+ auto joinType = op.JoinKind;
+ if (swapTables) {
+ SwapJoinType(pos, joinType, ctx);
+ }
+
+ auto& mainLabel = labels.Inputs[swapTables ? 1 : 0];
+ auto& smallLabel = labels.Inputs[swapTables ? 0 : 1];
+
+ auto joinTree = ctx.NewList(pos, {
+ joinType,
+ ctx.NewAtom(pos, leftLeaf.Scope[0]),
+ ctx.NewAtom(pos, rightLeaf.Scope[0]),
+ leftKeyColumns,
+ rightKeyColumns,
+ ctx.NewList(pos, {})
+ });
+
+ auto columnTypes = GetJoinColumnTypes(*joinTree, labels, "Inner", ctx);
+ auto finalColumnTypes = GetJoinColumnTypes(*joinTree, labels, ctx);
+
+ auto outputLeftSchemeType = MakeOutputJoinColumns(columnTypes, mainLabel, ctx);
+ auto outputRightSchemeType = MakeOutputJoinColumns(columnTypes, smallLabel, ctx);
+
+ auto inputKeyTypeLeft = BuildJoinKeyType(mainLabel, *leftKeyColumns);
+ auto inputKeyTypeRight = BuildJoinKeyType(smallLabel, *rightKeyColumns);
+ auto filteredKeyTypeLeft = AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeLeft), ctx);
+ auto filteredKeyTypeRight = AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeRight), ctx);
+ if (!IsSameAnnotation(*filteredKeyTypeLeft, *filteredKeyTypeRight)) {
+ YQL_CLOG(INFO, ProviderYt) << "Mismatch key types, left: " << *AsDictKeyType(inputKeyTypeLeft, ctx) << ", right: " << *AsDictKeyType(inputKeyTypeRight, ctx);
+ skipped = true;
+ return true;
+ }
+
+ auto outputKeyType = UnifyJoinKeyType(pos, inputKeyTypeLeft, inputKeyTypeRight, ctx);
+
+ TExprNode::TListType leftMembersNodes;
+ TExprNode::TListType rightMembersNodes;
+ TExprNode::TListType requiredMembersNodes;
+ bool hasData[2] = { false, false };
+ TMap<TStringBuf, TVector<TStringBuf>> renameMap;
+ TSet<TVector<TStringBuf>> topLevelSorts;
+ if (!op.Parent) {
+ renameMap = LoadJoinRenameMap(equiJoin.JoinOptions().Ref());
+ topLevelSorts = LoadJoinSortSets(equiJoin.JoinOptions().Ref());
+ }
+
+ if (joinType->Content() != "RightSemi" && joinType->Content() != "RightOnly") {
+ hasData[0] = true;
+ for (auto x : outputLeftSchemeType->GetItems()) {
+ auto name = ctx.NewAtom(pos, x->GetName());
+ if (auto renamed = renameMap.FindPtr(x->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto finalColumnType = finalColumnTypes[x->GetName()];
+ if (!finalColumnType->IsOptionalOrNull()) {
+ requiredMembersNodes.push_back(name);
+ }
+
+ leftMembersNodes.push_back(name);
+ }
+ }
+
+ if (joinType->Content() != "LeftSemi" && joinType->Content() != "LeftOnly") {
+ hasData[1] = true;
+ for (auto x : outputRightSchemeType->GetItems()) {
+ auto name = ctx.NewAtom(pos, x->GetName());
+ if (auto renamed = renameMap.FindPtr(x->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto finalColumnType = finalColumnTypes[x->GetName()];
+ if (!finalColumnType->IsOptionalOrNull()) {
+ requiredMembersNodes.push_back(name);
+ }
+
+ rightMembersNodes.push_back(name);
+ }
+ }
+
+ TCommonJoinCoreLambdas cjcLambdas[2];
+ TVector<TString> keys = KeysApplyInputRenames(BuildJoinKeyList(mainLabel, *leftKeyColumns), sortInfo.LeftKeyRenames);
+ for (ui32 index = 0; index < 2; ++index) {
+ auto& label = (index == 0) ? mainLabel : smallLabel;
+
+ auto keyColumnsNode = (index == 0) ? leftKeyColumns : rightKeyColumns;
+
+ auto myOutputSchemeType = (index == 0) ? outputLeftSchemeType : outputRightSchemeType;
+ auto otherOutputSchemeType = (index == 1) ? outputLeftSchemeType : outputRightSchemeType;
+
+ cjcLambdas[index] = MakeCommonJoinCoreReduceLambda(pos, ctx, label, outputKeyType, *keyColumnsNode,
+ joinType->Content(), myOutputSchemeType, otherOutputSchemeType, index, false,
+ 0, renameMap, hasData[index], hasData[1 - index], keys);
+
+ auto& leaf = (index == 0) ? leftLeaf : rightLeaf;
+ auto& otherLeaf = (index == 1) ? leftLeaf : rightLeaf;
+ auto& keyRenames = (index == 0) ? sortInfo.LeftKeyRenames : sortInfo.RightKeyRenames;
+ ApplyInputPremap(cjcLambdas[index].ReduceLambda, leaf, otherLeaf, ctx, keyRenames);
+ }
+ YQL_ENSURE(cjcLambdas[0].CommonJoinCoreInputType == cjcLambdas[1].CommonJoinCoreInputType, "Must be same type from both side of join.");
+
+ auto groupArg = ctx.NewArgument(pos, "group");
+ auto convertedList = ctx.Builder(pos)
+ .Callable("FlatMap")
+ .Add(0, groupArg)
+ .Lambda(1)
+ .Param("item")
+ .Callable("Visit")
+ .Arg(0, "item")
+ .Atom(1, 0U)
+ .Add(2, cjcLambdas[0].ReduceLambda)
+ .Atom(3, 1U)
+ .Add(4, cjcLambdas[1].ReduceLambda)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TListType optionNodes;
+ AddAnyJoinOptionsToCommonJoinCore(optionNodes, swapTables, op.LinkSettings, pos, ctx);
+ if (auto memLimit = state->Configuration->CommonJoinCoreLimit.Get()) {
+ optionNodes.push_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, "memLimit", TNodeFlags::Default)
+ .Atom(1, ToString(*memLimit), TNodeFlags::Default)
+ .Seal()
+ .Build());
+ }
+ optionNodes.push_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, "sorted", TNodeFlags::Default)
+ .Atom(1, "left", TNodeFlags::Default)
+ .Seal()
+ .Build());
+
+ TExprNode::TListType keyMembersNodes;
+ YQL_ENSURE(sortInfo.CommonSortedKeys.size() == outputKeyType.size());
+ for (auto& x : sortInfo.CommonSortedKeys) {
+ keyMembersNodes.push_back(ctx.NewAtom(pos, x));
+ }
+
+ auto joinedRawStream = ctx.NewCallable(pos, "CommonJoinCore", { convertedList, joinType,
+ ctx.NewList(pos, std::move(leftMembersNodes)), ctx.NewList(pos, std::move(rightMembersNodes)),
+ ctx.NewList(pos, std::move(requiredMembersNodes)), ctx.NewList(pos, std::move(keyMembersNodes)),
+ ctx.NewList(pos, std::move(optionNodes)), ctx.NewAtom(pos, "_yql_table_index", TNodeFlags::Default) });
+ auto joinedRaw = joinedRawStream;
+
+ const TStructExprType* outItemType = nullptr;
+ if (!op.Parent) {
+ if (auto type = GetSequenceItemType(equiJoin.Pos(),
+ equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1],
+ false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return false;
+ }
+ } else {
+ outItemType = MakeIntermediateEquiJoinTableType(pos, *joinTree, labels, op.OutputRemoveColumns, ctx);
+ if (!outItemType) {
+ return false;
+ }
+ }
+
+ auto joined = ctx.Builder(pos)
+ .Callable("Map")
+ .Add(0, joinedRaw)
+ .Add(1, BuildJoinRenameLambda(pos, renameMap, *outItemType, ctx).Ptr())
+ .Seal()
+ .Build();
+
+ TYtOutTableInfo outTableInfo(outItemType, state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTableInfo.RowSpec->SetConstraints(op.Constraints);
+ outTableInfo.SetUnique(op.Constraints.GetConstraint<TDistinctConstraintNode>(), pos, ctx);
+ const bool setTopLevelFullSort = state->Configuration->JoinMergeSetTopLevelFullSort.Get().GetOrElse(false);
+
+ BuildOutputSort(outTableInfo, op, joinType->Content(), setTopLevelFullSort, *outItemType, renameMap, topLevelSorts,
+ sortInfo.LeftSortedKeys, sortInfo.RightSortedKeys, mainLabel, smallLabel);
+
+ auto reduceWorld = equiJoin.World();
+ auto leftSection = Build<TYtSection>(ctx, leftLeaf.Section.Pos())
+ .InitFrom(leftLeaf.Section)
+ .Settings(NYql::RemoveSettings(leftLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Done();
+ auto rightSection = Build<TYtSection>(ctx, rightLeaf.Section.Pos())
+ .InitFrom(rightLeaf.Section)
+ .Settings(NYql::RemoveSettings(rightLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Done();
+
+
+ if (sortInfo.AdditionalSort != TChoice::None) {
+ if (sortInfo.AdditionalSort == TChoice::Left || sortInfo.AdditionalSort == TChoice::Both) {
+ bool needRemapBeforeSort = sortInfo.NeedRemapBeforeSort == TChoice::Left || sortInfo.NeedRemapBeforeSort == TChoice::Both;
+ leftSection = SectionApplyAdditionalSort(leftSection, equiJoin, sortInfo.LeftSortedKeys, sortInfo.LeftBeforePremap, needRemapBeforeSort, *state, ctx);
+ }
+
+ if (sortInfo.AdditionalSort == TChoice::Right || sortInfo.AdditionalSort == TChoice::Both) {
+ bool needRemapBeforeSort = sortInfo.NeedRemapBeforeSort == TChoice::Right || sortInfo.NeedRemapBeforeSort == TChoice::Both;
+ rightSection = SectionApplyAdditionalSort(rightSection, equiJoin, sortInfo.RightSortedKeys, sortInfo.RightBeforePremap, needRemapBeforeSort, *state, ctx);
+ }
+
+ reduceWorld = Build<TCoWorld>(ctx, pos).Done();
+ }
+
+ leftSection = SectionApplyRenames(leftSection, sortInfo.LeftKeyRenames, ctx);
+ rightSection = SectionApplyRenames(rightSection, sortInfo.RightKeyRenames, ctx);
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos)
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ReduceBy), TNodeFlags::Default)
+ .Build()
+ .Value(ToAtomList(sortInfo.CommonSortedKeys, pos, ctx))
+ .Build();
+
+ if (joinReduce) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinReduce), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ if (tryFirstAsPrimary) {
+ const ui64 maxJobSize = state->Configuration->JoinMergeReduceJobMaxSize.Get().GetOrElse(8_GB);
+ auto subSettingsBuilder = Build<TCoNameValueTupleList>(ctx, pos)
+ .Add()
+ .Name()
+ .Value(MaxJobSizeForFirstAsPrimaryName, TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(ToString(maxJobSize), TNodeFlags::Default)
+ .Build()
+ .Build();
+ if (joinReduceForSecond) {
+ subSettingsBuilder
+ .Add()
+ .Name()
+ .Value(JoinReduceForSecondAsPrimaryName, TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::FirstAsPrimary), TNodeFlags::Default)
+ .Build()
+ .Value(subSettingsBuilder.Done())
+ .Build();
+ }
+
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ const auto useSystemColumns = state->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS);
+ if (useSystemColumns) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeySwitch), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ op.Output = Build<TYtReduce>(ctx, pos)
+ .World(reduceWorld)
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add(leftSection)
+ .Add(rightSection)
+ .Build()
+ .Output()
+ .Add(outTableInfo.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Reducer(BuildYtReduceLambda(pos, groupArg, std::move(joined), useSystemColumns, ctx))
+ .Done();
+
+ return true;
+}
+
+bool RewriteYtMapJoin(TYtEquiJoin equiJoin, const TJoinLabels& labels, bool isLookupJoin,
+ TYtJoinNodeOp& op, const TYtJoinNodeLeaf& leftLeaf, const TYtJoinNodeLeaf& rightLeaf,
+ TExprContext& ctx, const TMapJoinSettings& settings, bool useShards, const TYtState::TPtr& state)
+{
+ auto pos = equiJoin.Pos();
+ auto joinType = op.JoinKind;
+ if (settings.SwapTables) {
+ SwapJoinType(pos, joinType, ctx);
+ }
+
+ auto& mainLabel = labels.Inputs[settings.SwapTables ? 1 : 0];
+ auto& smallLabel = labels.Inputs[settings.SwapTables ? 0 : 1];
+
+ TStringBuf strategyName = isLookupJoin ? "LookupJoin" : "MapJoin";
+
+ auto const& leftHints = settings.SwapTables ? op.LinkSettings.RightHints : op.LinkSettings.LeftHints;
+ auto const& rightHints = settings.SwapTables ? op.LinkSettings.LeftHints : op.LinkSettings.RightHints;
+
+ YQL_ENSURE(!leftHints.contains("any"));
+
+ const bool isUniqueKey = rightHints.contains("unique") || settings.RightUnique || rightHints.contains("any");
+ if (isUniqueKey) {
+ YQL_CLOG(INFO, ProviderYt) << strategyName << " assumes unique keys for the small table";
+ }
+
+ ui64 partCount = 1;
+ ui64 partRows = settings.RightRows;
+ if ((settings.RightSize > 0) && useShards) {
+ partCount = (settings.RightMemSize + settings.MapJoinLimit - 1) / settings.MapJoinLimit;
+ partRows = (settings.RightRows + partCount - 1) / partCount;
+ }
+
+ if (partCount > 1) {
+ YQL_ENSURE(!isLookupJoin);
+ YQL_CLOG(INFO, ProviderYt) << strategyName << " sharded into " << partCount << " parts, each " << partRows << " rows";
+ }
+
+ auto leftKeyColumns = settings.SwapTables ? op.RightLabel : op.LeftLabel;
+ auto rightKeyColumns = settings.SwapTables ? op.LeftLabel : op.RightLabel;
+ auto joinTree = ctx.NewList(pos, {
+ joinType,
+ ctx.NewAtom(pos, leftLeaf.Scope[0]),
+ ctx.NewAtom(pos, rightLeaf.Scope[0]),
+ leftKeyColumns,
+ rightKeyColumns,
+ ctx.NewList(pos, {})
+ });
+
+ auto columnTypes = GetJoinColumnTypes(*joinTree, labels, ctx);
+ auto outputLeftSchemeType = MakeOutputJoinColumns(columnTypes, mainLabel, ctx);
+ auto outputRightSchemeType = MakeOutputJoinColumns(columnTypes, smallLabel, ctx);
+
+ auto inputKeyTypeLeft = BuildJoinKeyType(mainLabel, *leftKeyColumns);
+ auto inputKeyTypeRight = BuildJoinKeyType(smallLabel, *rightKeyColumns);
+ auto outputKeyType = UnifyJoinKeyType(pos, inputKeyTypeLeft, inputKeyTypeRight, ctx);
+
+ TMap<TStringBuf, TVector<TStringBuf>> renameMap;
+ if (!op.Parent) {
+ renameMap = LoadJoinRenameMap(equiJoin.JoinOptions().Ref());
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (!op.Parent) {
+ if (auto type = GetSequenceItemType(equiJoin.Pos(),
+ equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1],
+ false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return false;
+ }
+ } else {
+ outItemType = MakeIntermediateEquiJoinTableType(pos, *joinTree, labels, op.OutputRemoveColumns, ctx);
+ if (!outItemType) {
+ return false;
+ }
+ }
+
+ auto mainPaths = MakeUnorderedSection(leftLeaf.Section, ctx).Paths();
+ auto mainSettings = NYql::RemoveSettings(leftLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx);
+ auto smallPaths = MakeUnorderedSection(rightLeaf.Section, ctx).Paths();
+ auto smallSettings = NYql::RemoveSettings(rightLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx);
+ if (!NYql::HasSetting(*smallSettings, EYtSettingType::Unordered)) {
+ smallSettings = NYql::AddSetting(*smallSettings, EYtSettingType::Unordered, {}, ctx);
+ }
+ auto smallKeyColumns = rightKeyColumns;
+
+ TSyncMap syncList;
+ for (auto path: smallPaths) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+ auto mapWorld = ApplySyncListToWorld(equiJoin.World().Ptr(), syncList, ctx);
+
+ auto mapSettingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ if (partCount > 1) {
+ mapSettingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Sharded))
+ .Build()
+ .Build();
+ }
+
+ const bool isCross = joinType->IsAtom("Cross");
+
+ auto tableContentSettings = ctx.NewList(pos, {});
+ if (isCross) {
+ ui64 rowFactor = (1 + smallLabel.InputType->GetSize()) * sizeof(NKikimr::NUdf::TUnboxedValuePod); // Table content after Collect
+ rowFactor += (1 + smallLabel.InputType->GetSize() + mainLabel.InputType->GetSize()) * sizeof(NKikimr::NUdf::TUnboxedValuePod); // Table content after Map with added left side
+ rowFactor += settings.LeftSize / settings.LeftRows; // Average added left side for each row after Map
+
+ tableContentSettings = NYql::AddSetting(*tableContentSettings, EYtSettingType::RowFactor, ctx.NewAtom(pos, ToString(rowFactor), TNodeFlags::Default), ctx);
+ }
+
+ auto mapJoinUseFlow = state->Configuration->MapJoinUseFlow.Get().GetOrElse(DEFAULT_MAP_JOIN_USE_FLOW);
+
+ TYtOutTableInfo outTableInfo(outItemType, state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTableInfo.RowSpec->SetConstraints(op.Constraints);
+ outTableInfo.SetUnique(op.Constraints.GetConstraint<TDistinctConstraintNode>(), pos, ctx);
+
+ TVector<TYtMap> maps;
+ for (ui64 partNo = 0; partNo < partCount; ++partNo) {
+ auto listArg = ctx.NewArgument(pos, "list");
+
+ ui64 dictItemsCount = settings.RightRows;
+ auto readSettings = smallSettings;
+
+ if (partCount > 1) {
+ ui64 start = partRows * partNo;
+ ui64 finish = Min(start + partRows, settings.RightRows);
+ readSettings = NYql::AddSetting(*readSettings, EYtSettingType::Skip,
+ ctx.Builder(pos)
+ .Callable("Uint64")
+ .Atom(0, ToString(start), TNodeFlags::Default)
+ .Seal()
+ .Build(), ctx);
+
+ readSettings = NYql::AddSetting(*readSettings, EYtSettingType::Take,
+ ctx.Builder(pos)
+ .Callable("Uint64")
+ .Atom(0, ToString(finish - start), TNodeFlags::Default)
+ .Seal()
+ .Build(), ctx);
+
+ readSettings = NYql::AddSetting(*readSettings, EYtSettingType::Unordered, {}, ctx);
+
+ dictItemsCount = finish - start;
+ }
+
+ auto tableContent = Build<TYtTableContent>(ctx, pos)
+ .Input<TYtReadTable>()
+ .World<TCoWorld>().Build()
+ .DataSource(ctx.RenameNode(equiJoin.DataSink().Ref(), TYtDSource::CallableName()))
+ .Input()
+ .Add()
+ .Paths(smallPaths)
+ .Settings(readSettings)
+ .Build()
+ .Build()
+ .Build()
+ .Settings(tableContentSettings)
+ .Done().Ptr();
+
+ TExprNode::TPtr lookupJoinFilterLambda;
+ if (isLookupJoin) {
+ TExprNode::TPtr tableContentAsJoinKeysTupleList;
+ YQL_ENSURE(leftKeyColumns->ChildrenSize() == rightKeyColumns->ChildrenSize());
+
+ TVector<TString> mainJoinMembers;
+ TVector<TString> smallJoinMembers;
+
+ for (ui32 i = 0; i < leftKeyColumns->ChildrenSize(); i += 2) {
+ TString mainMemberName = mainLabel.MemberName(leftKeyColumns->Child(i)->Content(),
+ leftKeyColumns->Child(i + 1)->Content());
+
+ TString smallMemberName = smallLabel.MemberName(rightKeyColumns->Child(i)->Content(),
+ rightKeyColumns->Child(i + 1)->Content());
+
+ mainJoinMembers.push_back(mainMemberName);
+ smallJoinMembers.push_back(smallMemberName);
+ }
+
+ auto status = SubstTables(tableContent, state, false, ctx);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return false;
+ }
+
+ ctx.Step.Repeat(TExprStep::ExprEval);
+
+ tableContent = ctx.Builder(pos)
+ .Callable("EvaluateExpr")
+ .Add(0, tableContent)
+ .Seal()
+ .Build();
+
+ lookupJoinFilterLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Callable("Coalesce")
+ .Callable(0, "SqlIn")
+ .Callable(0, "EvaluateExpr")
+ .Callable(0, "FlatMap")
+ .Add(0, tableContent)
+ .Lambda(1)
+ .Param("input")
+ .Callable("Just")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ auto builder = parent.List(0);
+ for (ui32 i = 0; i < smallJoinMembers.size(); ++i) {
+ builder
+ .Callable(i, "Member")
+ .Arg(0, "input")
+ .Atom(1, smallJoinMembers[i])
+ .Seal();
+ }
+ return builder.Seal();
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ auto builder = parent.List(1);
+ for (ui32 i = 0; i < mainJoinMembers.size(); ++i) {
+ builder
+ .Callable(i, "Member")
+ .Arg(0, "item")
+ .Atom(1, mainJoinMembers[i])
+ .Seal();
+ }
+ return builder.Seal();
+ })
+ .List(2)
+ .Seal()
+ .Seal()
+ .Callable(1, "Bool")
+ .Atom(0, "false", TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ if (auto premap = GetPremapLambda(rightLeaf)) {
+ tableContent = ctx.Builder(tableContent->Pos())
+ .Callable("FlatMap")
+ .Add(0, tableContent)
+ .Add(1, premap.Cast().Ptr())
+ .Seal()
+ .Build();
+ }
+
+ const bool needPayload = joinType->IsAtom({"Inner", "Left"});
+
+ // don't produce nulls
+ TExprNode::TPtr smallKeySelector;
+ if (!isCross) {
+ TExprNode::TListType remappedMembers;
+ TExprNode::TListType remappedMembersToSkipNull;
+ tableContent = RemapNonConvertibleItems(tableContent, smallLabel, *rightKeyColumns, outputKeyType, remappedMembers, remappedMembersToSkipNull, ctx);
+ if (!remappedMembersToSkipNull.empty()) {
+ tableContent = ctx.NewCallable(pos, "SkipNullMembers", { tableContent, ctx.NewList(pos, std::move(remappedMembersToSkipNull)) });
+ }
+
+ auto arg = ctx.NewArgument(pos, "item");
+ TExprNode::TListType tupleItems;
+ YQL_ENSURE(2 * remappedMembers.size() == rightKeyColumns->ChildrenSize());
+ for (auto memberNameNode : remappedMembers) {
+ auto member = ctx.Builder(pos)
+ .Callable("Member")
+ .Add(0, arg)
+ .Add(1, memberNameNode)
+ .Seal()
+ .Build();
+
+ tupleItems.push_back(member);
+ }
+
+ TExprNode::TPtr lambdaBody;
+ if (tupleItems.size() == 1) {
+ lambdaBody = tupleItems.front();
+ } else {
+ lambdaBody = ctx.NewList(pos, std::move(tupleItems));
+ }
+
+ smallKeySelector = ctx.NewLambda(pos, ctx.NewArguments(pos, { arg }), std::move(lambdaBody));
+ } else {
+ tableContent = Build<TCoCollect>(ctx, pos)
+ .Input(tableContent)
+ .Done().Ptr();
+ }
+
+ // may produce null in keys
+ TExprNode::TPtr smallPayloadSelector;
+ if (!isCross) {
+ if (needPayload) {
+ smallPayloadSelector = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Callable("AsStruct")
+ .Do([&](TExprNodeBuilder& parent)->TExprNodeBuilder& {
+ ui32 index = 0;
+ for (auto x : smallLabel.EnumerateAllMembers()) {
+ parent.List(index++)
+ .Atom(0, x)
+ .Callable(1, "Member")
+ .Arg(0, "item")
+ .Atom(1, x)
+ .Seal();
+ }
+
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ else {
+ smallPayloadSelector = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Callable("Void")
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+
+ TExprNode::TPtr dict;
+ if (!isCross && !mapJoinUseFlow) {
+ dict = ctx.Builder(pos)
+ .Callable("ToDict")
+ .Add(0, tableContent)
+ .Add(1, smallKeySelector)
+ .Add(2, smallPayloadSelector)
+ .List(3)
+ .Atom(0, "Hashed")
+ .Atom(1, needPayload && !isUniqueKey ? "Many" : "One")
+ .Atom(2, "Compact")
+ .List(3)
+ .Atom(0, "ItemsCount")
+ .Atom(1, ToString(dictItemsCount))
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ auto mainArg = ctx.NewArgument(pos, "mainRow");
+ auto lookupArg = ctx.NewArgument(pos, "lookupRow");
+ TExprNode::TListType joinedOutNodes;
+ TExprNode::TListType rightRenameNodes;
+ TExprNode::TListType leftRenameNodes;
+ for (ui32 index = 0; index < outputLeftSchemeType->GetSize(); ++index) {
+ auto item = outputLeftSchemeType->GetItems()[index];
+ TVector<TStringBuf> newNames;
+ newNames.push_back(item->GetName());
+ TStringBuf part1;
+ TStringBuf part2;
+ SplitTableName(item->GetName(), part1, part2);
+ TString memberName = mainLabel.MemberName(part1, part2);
+ if (!op.Parent) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ newNames = *renamed;
+ }
+ } else if (op.OutputRemoveColumns.contains(item->GetName())) {
+ newNames = {};
+ }
+
+ for (auto newName : newNames) {
+ leftRenameNodes.push_back(ctx.NewAtom(pos, memberName));
+ leftRenameNodes.push_back(ctx.NewAtom(pos, newName));
+ AddJoinRemappedColumn(pos, mainArg, joinedOutNodes, memberName, newName, ctx);
+ }
+ }
+
+ if (needPayload || isCross) {
+ for (ui32 index = 0; index < outputRightSchemeType->GetSize(); ++index) {
+ auto item = outputRightSchemeType->GetItems()[index];
+ TVector<TStringBuf> newNames;
+ newNames.push_back(item->GetName());
+ TStringBuf part1;
+ TStringBuf part2;
+ SplitTableName(item->GetName(), part1, part2);
+ TString memberName = smallLabel.MemberName(part1, part2);
+ if (!op.Parent) {
+ if (auto renamed = renameMap.FindPtr(item->GetName())) {
+ newNames = *renamed;
+ }
+ } else if (op.OutputRemoveColumns.contains(item->GetName())) {
+ newNames = {};
+ }
+
+ for (auto newName : newNames) {
+ rightRenameNodes.push_back(ctx.NewAtom(pos, memberName));
+ rightRenameNodes.push_back(ctx.NewAtom(pos, newName));
+ AddJoinRemappedColumn(pos, lookupArg, joinedOutNodes, memberName, newName, ctx);
+ }
+ }
+ }
+
+ TExprNode::TPtr joined;
+ if (!isCross) {
+ TExprNode::TListType leftKeyColumnNodes;
+ TExprNode::TListType leftKeyColumnNodesNullable;
+ auto mapInput = RemapNonConvertibleItems(listArg, mainLabel, *leftKeyColumns, outputKeyType, leftKeyColumnNodes, leftKeyColumnNodesNullable, ctx);
+ if (mapJoinUseFlow) {
+ joined = ctx.Builder(pos)
+ .Callable("FlatMap")
+ .Callable(0, "SqueezeToDict")
+ .Callable(0, "ToFlow")
+ .Add(0, std::move(tableContent))
+ .Callable(1, "DependsOn")
+ .Add(0, listArg)
+ .Seal()
+ .Seal()
+ .Add(1, std::move(smallKeySelector))
+ .Add(2, std::move(smallPayloadSelector))
+ .List(3)
+ .Atom(0, "Hashed", TNodeFlags::Default)
+ .Atom(1, needPayload && !isUniqueKey ? "Many" : "One", TNodeFlags::Default)
+ .Atom(2, "Compact", TNodeFlags::Default)
+ .List(3)
+ .Atom(0, "ItemsCount", TNodeFlags::Default)
+ .Atom(1, ToString(dictItemsCount), TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Lambda(1)
+ .Param("dict")
+ .Callable("MapJoinCore")
+ .Add(0, std::move(mapInput))
+ .Arg(1, "dict")
+ .Add(2, joinType)
+ .Add(3, ctx.NewList(pos, std::move(leftKeyColumnNodes)))
+ .Add(4, ctx.NewList(pos, std::move(leftRenameNodes)))
+ .Add(5, ctx.NewList(pos, std::move(rightRenameNodes)))
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ joined = ctx.Builder(pos)
+ .Callable("MapJoinCore")
+ .Add(0, mapInput)
+ .Add(1, dict)
+ .Add(2, joinType)
+ .Add(3, ctx.NewList(pos, std::move(leftKeyColumnNodes)))
+ .Add(4, ctx.NewList(pos, std::move(leftRenameNodes)))
+ .Add(5, ctx.NewList(pos, std::move(rightRenameNodes)))
+ .Seal()
+ .Build();
+ }
+ }
+ else {
+ auto joinedOut = ctx.NewCallable(pos, "AsStruct", std::move(joinedOutNodes));
+ auto joinedBody = ctx.Builder(pos)
+ .Callable("Map")
+ .Callable(0, "ToFlow")
+ .Add(0, std::move(tableContent))
+ .Callable(1, "DependsOn")
+ .Add(0, listArg)
+ .Seal()
+ .Seal()
+ .Lambda(1)
+ .Param("smallRow")
+ .ApplyPartial(nullptr, std::move(joinedOut)).WithNode(*lookupArg, "smallRow").Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto joinedLambda = ctx.NewLambda(pos, ctx.NewArguments(pos, { mainArg }), std::move(joinedBody));
+ joined = ctx.Builder(pos)
+ .Callable("FlatMap")
+ .Add(0, listArg)
+ .Add(1, std::move(joinedLambda))
+ .Seal()
+ .Build();
+ }
+
+ auto mapLambda = ctx.NewLambda(pos, ctx.NewArguments(pos, {std::move(listArg)}), std::move(joined));
+ if (const auto premap = GetPremapLambda(leftLeaf)) {
+ TExprNode::TPtr placeHolder;
+ std::tie(placeHolder, mapLambda) = ReplaceDependsOn(std::move(mapLambda), ctx, state->Types);
+
+ mapLambda = ctx.Builder(mapLambda->Pos())
+ .Lambda()
+ .Param("list")
+ .Apply(mapLambda)
+ .With(0)
+ .Callable("FlatMap")
+ .Arg(0, "list")
+ .Add(1, premap.Cast().Ptr())
+ .Seal()
+ .Done()
+ .WithNode(*placeHolder, "list")
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ // since premap doesn't affect key columns we can apply lookup join filter before premap
+ if (lookupJoinFilterLambda) {
+ TExprNode::TPtr placeHolder;
+ std::tie(placeHolder, mapLambda) = ReplaceDependsOn(std::move(mapLambda), ctx, state->Types);
+
+ mapLambda = ctx.Builder(mapLambda->Pos())
+ .Lambda()
+ .Param("list")
+ .Apply(mapLambda)
+ .With(0)
+ .Callable("Filter")
+ .Arg(0, "list")
+ .Add(1, std::move(lookupJoinFilterLambda))
+ .Seal()
+ .Done()
+ .WithNode(*placeHolder, "list")
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ mapSettingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ auto map = Build<TYtMap>(ctx, pos)
+ .World(mapWorld)
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add()
+ .Paths(mainPaths)
+ .Settings(mainSettings)
+ .Build()
+ .Build()
+ .Output()
+ .Add(outTableInfo.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(mapSettingsBuilder.Done())
+ .Mapper(mapLambda)
+ .Done();
+
+ maps.push_back(map);
+ }
+
+ if (maps.size() == 1) {
+ op.Output = maps.front();
+ }
+ else {
+ TVector<TYtPath> paths;
+ for (auto map: maps) {
+ paths.push_back(Build<TYtPath>(ctx, pos)
+ .Table<TYtOutput>()
+ .Operation(map)
+ .OutIndex().Value(0U).Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done()
+ );
+ }
+ op.Output = Build<TYtMerge>(ctx, pos)
+ .World<TCoWorld>().Build()
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Output()
+ .Add(outTableInfo.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Build()
+ .Done();
+ }
+
+ return true;
+}
+
+TCoLambda BuildIdentityLambda(TPositionHandle pos, TExprContext& ctx) {
+ return Build<TCoLambda>(ctx, pos)
+ .Args({"item"})
+ .Body("item")
+ .Done();
+}
+
+TCoLambda BuildMapCombinerSideLambda(const TYtState::TPtr& state, const TMap<TString, const TTypeAnnotationNode*>& keys, TPositionHandle pos, TExprContext& ctx) {
+
+ auto pickleLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Callable("StablePickle")
+ .Arg(0, "item")
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto identityLambda = BuildIdentityLambda(pos, ctx).Ptr();
+
+ TCoLambda keyExtractor(ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ auto builder = parent.List();
+ size_t i = 0;
+ for (const auto& [name, type] : keys) {
+ bool needPickle = RemoveOptionalType(type)->GetKind() != ETypeAnnotationKind::Data;
+ builder
+ .Apply(i++, needPickle ? pickleLambda : identityLambda)
+ .With(0)
+ .Callable("Member")
+ .Arg(0, "item")
+ .Atom(1, name)
+ .Seal()
+ .Done()
+ .Seal();
+ }
+ return builder.Seal();
+ })
+ .Seal()
+ .Build());
+
+
+ return Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoCombineCore>()
+ .Input("stream")
+ .KeyExtractor(keyExtractor)
+ .InitHandler()
+ .Args({"key", "item"})
+ .Body("item")
+ .Build()
+ .UpdateHandler()
+ .Args({"key", "item", "state"})
+ .Body("state")
+ .Build()
+ .FinishHandler()
+ .Args({"key", "state"})
+ .Body<TCoJust>()
+ .Input("state")
+ .Build()
+ .Build()
+ .MemLimit<TCoAtom>()
+ .Value(ToString(state->Configuration->CombineCoreLimit.Get().GetOrElse(0)))
+ .Build()
+ .Build()
+ .Done();
+}
+
+TCoLambda BuildMapCombinerLambda(const TYtState::TPtr& state, const TJoinLabels& labels, const TYtJoinNodeOp& op, TPositionHandle pos, TExprContext& ctx)
+{
+ TCoLambda identityLambda = BuildIdentityLambda(pos, ctx);
+
+ bool leftAny = op.LinkSettings.LeftHints.contains("any");
+ bool rightAny = op.LinkSettings.RightHints.contains("any");
+
+ if (!leftAny && !rightAny) {
+ return identityLambda;
+ }
+
+ auto leftKeyColumns = op.LeftLabel;
+ auto rightKeyColumns = op.RightLabel;
+
+ YQL_ENSURE(leftKeyColumns);
+ YQL_ENSURE(rightKeyColumns);
+
+ auto leftKeyTypes = BuildJoinKeyTypeMap(labels.Inputs[0], *leftKeyColumns);
+ auto rightKeyTypes = BuildJoinKeyTypeMap(labels.Inputs[1], *rightKeyColumns);
+
+ return Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoSwitch>()
+ .Input("stream")
+ .BufferBytes()
+ .Value(ToString(state->Configuration->SwitchLimit.Get().GetOrElse(DEFAULT_SWITCH_MEMORY_LIMIT)))
+ .Build()
+ .FreeArgs()
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(0U)
+ .Build()
+ .Build()
+ .Add(leftAny ? BuildMapCombinerSideLambda(state, leftKeyTypes, pos, ctx) : identityLambda)
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(1U)
+ .Build()
+ .Build()
+ .Add(rightAny ? BuildMapCombinerSideLambda(state, rightKeyTypes, pos, ctx) : identityLambda)
+ .Build()
+ .Build()
+ .Done();
+}
+
+bool JoinKeysMayHaveNulls(const TVector<const TTypeAnnotationNode*>& inputKeyTypes, const TVector<const TTypeAnnotationNode*>& unifiedKeyTypes) {
+ YQL_ENSURE(inputKeyTypes.size() == unifiedKeyTypes.size());
+ for (size_t i = 0; i < inputKeyTypes.size(); ++i) {
+ if (inputKeyTypes[i]->HasOptionalOrNull()) {
+ return true;
+ }
+
+ NUdf::TCastResultOptions options = CastResult<true>(inputKeyTypes[i], unifiedKeyTypes[i]);
+ YQL_ENSURE(!(options & NKikimr::NUdf::ECastOptions::Impossible));
+ if (options & NKikimr::NUdf::ECastOptions::MayFail) {
+ return true;
+ }
+ }
+ return false;
+}
+
+TExprNode::TPtr BuildSideSplitNullsLambda(TPositionHandle pos, bool mayHaveNulls, const TExprNode::TPtr& inputItem,
+ const TVector<TString>& keyColumns, const TString& sidePrefix,
+ const TCoLambda& joinRenamingLambda, const TStructExprType& joinOutputType,
+ const TExprNode::TPtr& outputVariantType, size_t outputVariantIndex, TExprContext& ctx)
+{
+ if (!mayHaveNulls) {
+ return ctx.Builder(pos)
+ .Lambda()
+ .Param("side")
+ .Callable("Variant")
+ .Add(0, inputItem)
+ .Atom(1, 0U)
+ .Add(2, outputVariantType)
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ return ctx.Builder(pos)
+ .Lambda()
+ .Param("side")
+ .Callable("If")
+ .Callable(0, "HasNull")
+ .List(0)
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < keyColumns.size(); ++i) {
+ parent
+ .Callable(i, "Member")
+ .Add(0, inputItem)
+ .Atom(1, keyColumns[i])
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Callable(1, "Variant")
+ .Apply(0, joinRenamingLambda.Ptr())
+ .With(0)
+ .Callable("StrictCast")
+ .Callable(0, "FlattenMembers")
+ .List(0)
+ .Atom(0, sidePrefix)
+ .Arg(1, "side")
+ .Seal()
+ .Seal()
+ .Add(1, ExpandType(pos, joinOutputType, ctx))
+ .Seal()
+ .Done()
+ .Seal()
+ .Atom(1, ToString(outputVariantIndex), TNodeFlags::Default)
+ .Add(2, outputVariantType)
+ .Seal()
+ .Callable(2, "Variant")
+ .Add(0, inputItem)
+ .Atom(1, 0U)
+ .Add(2, outputVariantType)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+}
+
+bool RewriteYtCommonJoin(TYtEquiJoin equiJoin, const TJoinLabels& labels, TYtJoinNodeOp& op,
+ const TYtJoinNodeLeaf& leftLeaf, const TYtJoinNodeLeaf& rightLeaf, const TYtState::TPtr& state, TExprContext& ctx,
+ bool leftUnique, bool rightUnique, ui64 leftSize, ui64 rightSize)
+{
+ const auto pos = equiJoin.Pos();
+
+ const auto leftNotFat = leftUnique || op.LinkSettings.LeftHints.contains("unique") || op.LinkSettings.LeftHints.contains("small");
+ const auto rightNotFat = rightUnique || op.LinkSettings.RightHints.contains("unique") || op.LinkSettings.RightHints.contains("small");
+ bool leftFirst = false;
+ if (leftNotFat != rightNotFat) {
+ // non-fat will be first
+ leftFirst = leftNotFat;
+ } else {
+ // small table will be first
+ leftFirst = leftSize < rightSize;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "CommonJoin, << " << (leftFirst ? "left" : "right") << " table moved first";
+
+ auto leftKeyColumns = op.LeftLabel;
+ auto rightKeyColumns = op.RightLabel;
+
+ auto joinType = op.JoinKind;
+ auto joinTree = ctx.NewList(pos, {
+ joinType,
+ ctx.NewAtom(pos, leftLeaf.Scope[0]),
+ ctx.NewAtom(pos, rightLeaf.Scope[0]),
+ leftKeyColumns,
+ rightKeyColumns,
+ ctx.NewList(pos, {})
+ });
+
+ auto columnTypes = GetJoinColumnTypes(*joinTree, labels, "Inner", ctx);
+ auto finalColumnTypes = GetJoinColumnTypes(*joinTree, labels, ctx);
+ auto outputLeftSchemeType = MakeOutputJoinColumns(columnTypes, labels.Inputs[0], ctx);
+ auto outputRightSchemeType = MakeOutputJoinColumns(columnTypes, labels.Inputs[1], ctx);
+
+ TVector<const TTypeAnnotationNode*> outputKeyType;
+ TVector<const TTypeAnnotationNode*> inputKeyTypeLeft;
+ TVector<const TTypeAnnotationNode*> inputKeyTypeRight;
+ const bool isCrossJoin = joinType->IsAtom("Cross");
+ if (isCrossJoin) {
+ outputKeyType = TVector<const TTypeAnnotationNode*>(1, ctx.MakeType<TDataExprType>(EDataSlot::Uint32));
+ } else {
+ inputKeyTypeLeft = BuildJoinKeyType(labels.Inputs[0], *leftKeyColumns);
+ inputKeyTypeRight = BuildJoinKeyType(labels.Inputs[1], *rightKeyColumns);
+ outputKeyType = UnifyJoinKeyType(pos, inputKeyTypeLeft, inputKeyTypeRight, ctx);
+ }
+
+ TVector<TString> ytReduceByColumns;
+ for (size_t i = 0; i < outputKeyType.size(); ++i) {
+ ytReduceByColumns.push_back(TStringBuilder() << "_yql_join_column_" << i);
+ }
+
+ TExprNode::TListType leftMembersNodes;
+ TExprNode::TListType rightMembersNodes;
+ TExprNode::TListType requiredMembersNodes;
+ bool hasData[2] = { false, false };
+ TMap<TStringBuf, TVector<TStringBuf>> renameMap;
+ if (!op.Parent) {
+ renameMap = LoadJoinRenameMap(equiJoin.JoinOptions().Ref());
+ }
+
+ if (!joinType->IsAtom({"RightSemi", "RightOnly"})) {
+ hasData[0] = true;
+ for (auto x : outputLeftSchemeType->GetItems()) {
+ auto name = ctx.NewAtom(pos, x->GetName());
+ if (auto renamed = renameMap.FindPtr(x->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto finalColumnType = finalColumnTypes[x->GetName()];
+ if (!finalColumnType->IsOptionalOrNull()) {
+ requiredMembersNodes.push_back(name);
+ }
+
+ leftMembersNodes.push_back(name);
+ }
+ }
+
+ if (!joinType->IsAtom({"LeftSemi", "LeftOnly"})) {
+ hasData[1] = true;
+ for (auto x : outputRightSchemeType->GetItems()) {
+ auto name = ctx.NewAtom(pos, x->GetName());
+ if (auto renamed = renameMap.FindPtr(x->GetName())) {
+ if (renamed->empty()) {
+ continue;
+ }
+ }
+
+ auto finalColumnType = finalColumnTypes[x->GetName()];
+ if (!finalColumnType->IsOptionalOrNull()) {
+ requiredMembersNodes.push_back(name);
+ }
+
+ rightMembersNodes.push_back(name);
+ }
+ }
+
+ TCommonJoinCoreLambdas cjcLambdas[2];
+ for (ui32 index = 0; index < 2; ++index) {
+ auto keyColumnsNode = (index == 0) ? leftKeyColumns : rightKeyColumns;
+
+ auto& label = labels.Inputs[index];
+ auto& otherLabel = labels.Inputs[1 - index];
+
+ auto myOutputSchemeType = (index == 0) ? outputLeftSchemeType : outputRightSchemeType;
+ auto otherOutputSchemeType = (index == 1) ? outputLeftSchemeType : outputRightSchemeType;
+
+ cjcLambdas[index] = MakeCommonJoinCoreLambdas(pos, ctx, label, otherLabel, outputKeyType, *keyColumnsNode,
+ joinType->Content(), myOutputSchemeType, otherOutputSchemeType, index, true,
+ leftFirst ? index : 1 - index, renameMap, hasData[index], hasData[1 - index], ytReduceByColumns);
+
+ auto& leaf = (index == 0) ? leftLeaf : rightLeaf;
+ auto& otherLeaf = (index == 1) ? leftLeaf : rightLeaf;
+ ApplyInputPremap(cjcLambdas[index].MapLambda, leaf, otherLeaf, ctx);
+ }
+ YQL_ENSURE(cjcLambdas[0].CommonJoinCoreInputType == cjcLambdas[1].CommonJoinCoreInputType, "Must be same type from both side of join.");
+
+ auto groupArg = ctx.NewArgument(pos, "group");
+
+ TExprNode::TListType optionNodes;
+ AddAnyJoinOptionsToCommonJoinCore(optionNodes, false, op.LinkSettings, pos, ctx);
+ if (auto memLimit = state->Configuration->CommonJoinCoreLimit.Get()) {
+ optionNodes.push_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, "memLimit", TNodeFlags::Default)
+ .Atom(1, ToString(*memLimit), TNodeFlags::Default)
+ .Seal()
+ .Build());
+ }
+ optionNodes.push_back(ctx.Builder(pos)
+ .List()
+ .Atom(0, "sorted", TNodeFlags::Default)
+ .Atom(1, leftFirst ? "left" : "right", TNodeFlags::Default)
+ .Seal()
+ .Build());
+
+ TExprNode::TListType keyMembersNodes;
+ if (!isCrossJoin) {
+ for (auto& x : ytReduceByColumns) {
+ keyMembersNodes.push_back(ctx.NewAtom(pos, x));
+ }
+ }
+
+ auto convertedList = PrepareForCommonJoinCore(pos, ctx, groupArg, cjcLambdas[0].ReduceLambda,
+ cjcLambdas[1].ReduceLambda);
+ auto joinedRawStream = ctx.NewCallable(pos, "CommonJoinCore", { convertedList, joinType,
+ ctx.NewList(pos, std::move(leftMembersNodes)), ctx.NewList(pos, std::move(rightMembersNodes)),
+ ctx.NewList(pos, std::move(requiredMembersNodes)), ctx.NewList(pos, std::move(keyMembersNodes)),
+ ctx.NewList(pos, std::move(optionNodes)), ctx.NewAtom(pos, "_yql_table_index", TNodeFlags::Default) });
+ auto joinedRaw = joinedRawStream;
+
+ const TStructExprType* outItemTypeBeforeRename = MakeIntermediateEquiJoinTableType(pos, *joinTree, labels, {}, ctx);
+ if (!outItemTypeBeforeRename) {
+ return false;
+ }
+ const TStructExprType* outItemType = nullptr;
+ if (!op.Parent) {
+ if (auto type = GetSequenceItemType(equiJoin.Pos(),
+ equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1],
+ false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return false;
+ }
+ } else {
+ outItemType = MakeIntermediateEquiJoinTableType(pos, *joinTree, labels, op.OutputRemoveColumns, ctx);
+ if (!outItemType) {
+ return false;
+ }
+ }
+
+ const TCoLambda joinRenamingLambda = BuildJoinRenameLambda(pos, renameMap, *outItemType, ctx);
+
+ auto joined = ctx.Builder(pos)
+ .Callable("Map")
+ .Add(0, joinedRaw)
+ .Add(1, joinRenamingLambda.Ptr())
+ .Seal()
+ .Build();
+
+ TVector<TString> ytSortByColumns;
+ ytSortByColumns = ytReduceByColumns;
+ ytSortByColumns.push_back("_yql_sort");
+
+ auto mapCombinerLambda = BuildMapCombinerLambda(state, labels, op, pos, ctx);
+
+ TExprNode::TPtr chopperHandler = ctx.NewLambda(pos, ctx.NewArguments(pos, {ctx.NewArgument(pos, "stup"), groupArg }), std::move(joined));
+ TExprNode::TPtr chopperSwitch;
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+
+ if (state->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ chopperSwitch = ctx.Builder(pos)
+ .Lambda()
+ .Param("key")
+ .Param("item")
+ .Callable("SqlExtractKey")
+ .Arg(0, "item")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ chopperHandler = ctx.Builder(pos)
+ .Lambda()
+ .Param("key")
+ .Param("group")
+ .Apply(chopperHandler)
+ .With(0, "key")
+ .With(1)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "group")
+ .List(1)
+ .Atom(0, YqlSysColumnPrefix, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeySwitch), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+ else {
+ chopperSwitch = Build<TCoLambda>(ctx, pos)
+ .Args({"key", "item"})
+ .Body<TYtIsKeySwitch>()
+ .DependsOn()
+ .Input("item")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ auto reducer = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoChopper>()
+ .Input("stream")
+ .KeyExtractor()
+ .Args({"item"})
+ .Body<TCoUint64>()
+ .Literal()
+ .Value(0U)
+ .Build()
+ .Build()
+ .Build()
+ .GroupSwitch(chopperSwitch)
+ .Handler(chopperHandler)
+ .Build()
+ .Done();
+
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ReduceBy), TNodeFlags::Default)
+ .Build()
+ .Value(ToAtomList(ytReduceByColumns, pos, ctx))
+ .Build()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::SortBy), TNodeFlags::Default)
+ .Build()
+ .Value(ToAtomList(ytSortByColumns, pos, ctx))
+ .Build();
+
+ auto mapLambda = Build<TCoLambda>(ctx, pos)
+ .Args({"flow"})
+ .Body<TCoOrderedFlatMap>()
+ .Input<TExprApplier>()
+ .Apply(mapCombinerLambda)
+ .With(0, "flow")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add<TCoAtom>()
+ .Value(0U)
+ .Build()
+ .Add(cjcLambdas[0].MapLambda)
+ .Add<TCoAtom>()
+ .Value(1U)
+ .Build()
+ .Add(cjcLambdas[1].MapLambda)
+ .Build()
+ .Build()
+ .Build()
+ .Build().Done().Ptr();
+
+ TYtOutTableInfo outInfo(outItemType, state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outInfo.RowSpec->SetConstraints(op.Constraints);
+ outInfo.SetUnique(op.Constraints.GetConstraint<TDistinctConstraintNode>(), pos, ctx);
+ const auto outTableInfo = outInfo.ToExprNode(ctx, pos).Ptr();
+
+ TExprNode::TListType mapReduceOutputs;
+ TVector<bool> mapReduceOutputsSingleValue;
+
+ mapReduceOutputs.push_back(outTableInfo);
+ if (!isCrossJoin && state->Configuration->JoinCommonUseMapMultiOut.Get().GetOrElse(DEFAULT_JOIN_COMMON_USE_MULTI_OUT)) {
+ bool leftNulls = false;
+ if (!leftNotFat && joinType->IsAtom({"Left", "Full", "Exclusion"})) {
+ leftNulls = JoinKeysMayHaveNulls(inputKeyTypeLeft, outputKeyType);
+ if (leftNulls) {
+ mapReduceOutputs.push_back(outTableInfo);
+ mapReduceOutputsSingleValue.push_back(op.LinkSettings.LeftHints.contains("any"));
+ }
+ }
+
+ bool rightNulls = false;
+ if (!rightNotFat && joinType->IsAtom({"Right", "Full", "Exclusion"})) {
+ rightNulls = JoinKeysMayHaveNulls(inputKeyTypeRight, outputKeyType);
+ if (rightNulls) {
+ mapReduceOutputs.push_back(outTableInfo);
+ mapReduceOutputsSingleValue.push_back(op.LinkSettings.RightHints.contains("any"));
+ }
+ }
+
+ if (leftNulls || rightNulls) {
+ TExprNode::TPtr itemArg = ctx.NewArgument(pos, "item");
+ TExprNode::TListType outputVarTypeItems;
+
+ // output to reducer
+ outputVarTypeItems.push_back(ctx.NewCallable(pos, "TypeOf", { itemArg }));
+
+ // direct outputs
+ size_t leftOutputIndex = 0;
+ if (leftNulls) {
+ leftOutputIndex = outputVarTypeItems.size();
+ outputVarTypeItems.push_back(ExpandType(pos, *outItemType, ctx));
+ }
+ size_t rightOutputIndex = 0;
+ if (rightNulls) {
+ rightOutputIndex = outputVarTypeItems.size();
+ outputVarTypeItems.push_back(ExpandType(pos, *outItemType, ctx));
+ }
+
+ auto variantType = ctx.Builder(pos)
+ .Callable("VariantType")
+ .Add(0, ctx.NewCallable(pos, "TupleType", std::move(outputVarTypeItems)))
+ .Seal()
+ .Build();
+
+ const TString leftSidePrefix = labels.Inputs[0].AddLabel ? (TStringBuilder() << labels.Inputs[0].Tables[0] << ".") : TString();
+ TExprNode::TPtr leftVisitLambda = BuildSideSplitNullsLambda(pos, leftNulls, itemArg, ytReduceByColumns, leftSidePrefix,
+ joinRenamingLambda, *outItemTypeBeforeRename, variantType, leftOutputIndex, ctx);
+
+ const TString rightSidePrefix = labels.Inputs[1].AddLabel ? (TStringBuilder() << labels.Inputs[1].Tables[0] << ".") : TString();
+ TExprNode::TPtr rightVisitLambda = BuildSideSplitNullsLambda(pos, rightNulls, itemArg, ytReduceByColumns, rightSidePrefix,
+ joinRenamingLambda, *outItemTypeBeforeRename, variantType, rightOutputIndex, ctx);
+
+ auto splitLambdaBody = ctx.Builder(pos)
+ .Callable("Visit")
+ .Callable(0, "Member")
+ .Add(0, itemArg)
+ .Atom(1, "_yql_join_payload", TNodeFlags::Default)
+ .Seal()
+ .Atom(1, 0U)
+ .Add(2, leftVisitLambda)
+ .Atom(3, 1U)
+ .Add(4, rightVisitLambda)
+ .Seal()
+ .Build();
+
+ mapLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("stream")
+ .Callable("OrderedMap")
+ .Apply(0, mapLambda)
+ .With(0, "stream")
+ .Seal()
+ .Add(1, ctx.NewLambda(pos, ctx.NewArguments(pos, { itemArg }), std::move(splitLambdaBody)))
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ const auto mapreduce = Build<TYtMapReduce>(ctx, pos)
+ .World(equiJoin.World())
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add()
+ .Paths(MakeUnorderedSection(leftLeaf.Section, ctx).Paths())
+ .Settings(NYql::RemoveSettings(leftLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Build()
+ .Add()
+ .Paths(MakeUnorderedSection(rightLeaf.Section, ctx).Paths())
+ .Settings(NYql::RemoveSettings(rightLeaf.Section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Build()
+ .Build()
+ .Output(ctx.NewList(pos, std::move(mapReduceOutputs)))
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapLambda)
+ .Reducer(reducer)
+ .Done();
+
+ if (mapReduceOutputsSingleValue.empty()) {
+ op.Output = mapreduce;
+ } else {
+ TVector<TYtPath> paths;
+ ui32 idx = 0U;
+ for (bool single: mapReduceOutputsSingleValue) {
+ TExprBase ranges = Build<TCoVoid>(ctx, pos).Done();
+ if (single) {
+ ranges = Build<TExprList>(ctx, pos)
+ .Add<TYtRow>()
+ .Index<TCoUint64>()
+ .Literal()
+ .Value(0U)
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ paths.push_back(Build<TYtPath>(ctx, pos)
+ .Table<TYtOutput>()
+ .Operation(mapreduce)
+ .OutIndex().Value(idx++).Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges(ranges)
+ .Stat<TCoVoid>().Build()
+ .Done()
+ );
+ }
+
+ paths.push_back(Build<TYtPath>(ctx, pos)
+ .Table<TYtOutput>()
+ .Operation(mapreduce)
+ .OutIndex().Value(idx++).Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done()
+ );
+
+ op.Output = Build<TYtMerge>(ctx, pos)
+ .World(equiJoin.World())
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Output()
+ .Add(outTableInfo)
+ .Build()
+ .Settings()
+ .Build()
+ .Done();
+ }
+
+ return true;
+}
+
+bool RewriteYtEmptyJoin(TYtEquiJoin equiJoin, const TJoinLabels& labels, TYtJoinNodeOp& op,
+ const TYtJoinNodeLeaf& leftLeaf, const TYtJoinNodeLeaf& rightLeaf, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ auto pos = equiJoin.Pos();
+
+ YQL_CLOG(INFO, ProviderYt) << "EmptyJoin";
+
+ const TStructExprType* outItemType = nullptr;
+ if (!op.Parent) {
+ if (auto type = GetSequenceItemType(pos, equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1], false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return false;
+ }
+ } else {
+ auto joinTree = ctx.NewList(pos, {
+ op.JoinKind,
+ ctx.NewAtom(pos, leftLeaf.Scope[0]),
+ ctx.NewAtom(pos, rightLeaf.Scope[0]),
+ op.LeftLabel,
+ op.RightLabel,
+ ctx.NewList(pos, {})
+ });
+
+ outItemType = MakeIntermediateEquiJoinTableType(pos, *joinTree, labels, op.OutputRemoveColumns, ctx);
+ if (!outItemType) {
+ return false;
+ }
+ }
+
+ TSyncMap syncList;
+ for (auto path: leftLeaf.Section.Paths()) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+ for (auto path: rightLeaf.Section.Paths()) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+
+ TYtOutTableInfo outTableInfo(outItemType, state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTableInfo.RowSpec->SetConstraints(op.Constraints);
+ outTableInfo.SetUnique(op.Constraints.GetConstraint<TDistinctConstraintNode>(), pos, ctx);
+
+ op.Output = Build<TYtTouch>(ctx, pos)
+ .World(ApplySyncListToWorld(equiJoin.World().Ptr(), syncList, ctx))
+ .DataSink(equiJoin.DataSink())
+ .Output()
+ .Add(outTableInfo.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Done();
+
+ return true;
+}
+
+struct TJoinSideStats {
+ TString TableNames;
+ bool HasUniqueKeys = false;
+ bool IsDynamic = false;
+ bool NeedsRemap = false;
+
+ TVector<TString> SortedKeys;
+
+ ui64 RowsCount = 0;
+ ui64 Size = 0;
+};
+
+enum class ESizeStatCollectMode {
+ NoSize,
+ RawSize,
+ ColumnarSize,
+};
+
+TStatus CollectJoinSideStats(ESizeStatCollectMode sizeMode, TJoinSideStats& stats, TYtSection& inputSection,
+ const TYtState& state, const TString& cluster,
+ const TVector<TYtPathInfo::TPtr>& tableInfo, const THashSet<TString>& joinKeys,
+ bool isCross, TMaybeNode<TCoLambda> premap, TExprContext& ctx)
+{
+ stats = {};
+
+ stats.HasUniqueKeys = !isCross;
+ stats.IsDynamic = AnyOf(tableInfo, [](const TYtPathInfo::TPtr& path) {
+ return path->Table->Meta->IsDynamic;
+ });
+ const ui64 nativeTypeFlags = state.Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) && inputSection.Ref().GetTypeAnn()
+ ? GetNativeYtTypeFlags(*inputSection.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>())
+ : 0ul;
+ TMaybe<NYT::TNode> firstNativeType;
+ if (!tableInfo.empty()) {
+ firstNativeType = tableInfo.front()->GetNativeYtType();
+ }
+ stats.NeedsRemap = NYql::HasSetting(inputSection.Settings().Ref(), EYtSettingType::SysColumns)
+ || AnyOf(tableInfo, [nativeTypeFlags, firstNativeType](const TYtPathInfo::TPtr& path) {
+ return path->RequiresRemap()
+ || path->Table->RowSpec->HasAuxColumns() // TODO: remove
+ || nativeTypeFlags != path->GetNativeYtTypeFlags()
+ || firstNativeType != path->GetNativeYtType();
+ });
+
+ bool first = true;
+ for (auto& path: tableInfo) {
+ if (sizeMode != ESizeStatCollectMode::NoSize) {
+ YQL_ENSURE(path->Table->Stat);
+ auto tableRecords = path->Table->Stat->RecordsCount;
+ if (path->Ranges) {
+ tableRecords = path->Ranges->GetUsedRows(tableRecords).GetOrElse(tableRecords);
+ }
+ stats.RowsCount += tableRecords;
+ stats.Size += path->Table->Stat->DataSize;
+ }
+
+ if (!stats.TableNames.empty()) {
+ stats.TableNames += " | ";
+ }
+
+ stats.TableNames += path->Table->Name;
+ if (!isCross) {
+ UpdateSortPrefix(first, stats.SortedKeys, path->Table->RowSpec, stats.HasUniqueKeys, joinKeys, premap);
+ }
+ first = false;
+ }
+
+ if (sizeMode != ESizeStatCollectMode::ColumnarSize) {
+ return TStatus::Ok;
+ }
+
+ TVector<ui64> dataSizes;
+ auto status = TryEstimateDataSizeChecked(dataSizes, inputSection, cluster, tableInfo, {}, state, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ stats.Size = Accumulate(dataSizes.begin(), dataSizes.end(), 0ull, [](ui64 sum, ui64 v) { return sum + v; });
+ return TStatus::Ok;
+}
+
+TStatus CollectPathsAndLabels(TVector<TYtPathInfo::TPtr>& tables, TJoinLabels& labels,
+ const TStructExprType*& itemType, const TStructExprType*& itemTypeBeforePremap,
+ const TYtJoinNodeLeaf& leaf, TExprContext& ctx)
+{
+ tables = {};
+ itemType = nullptr;
+ itemTypeBeforePremap = nullptr;
+
+ TExprBase input = leaf.Section;
+ if (auto type = GetSequenceItemType(input, false, ctx)) {
+ itemTypeBeforePremap = type->Cast<TStructExprType>();
+ } else {
+ return TStatus::Error;
+ }
+
+ if (leaf.Premap) {
+ input = leaf.Premap.Cast();
+ }
+
+ if (auto type = GetSequenceItemType(input, false, ctx)) {
+ itemType = type->Cast<TStructExprType>();
+ } else {
+ return TStatus::Error;
+ }
+
+ if (auto err = labels.Add(ctx, *leaf.Label, itemType)) {
+ ctx.AddError(*err);
+ return TStatus::Error;
+ }
+
+ for (auto path: leaf.Section.Paths()) {
+ auto pathInfo = MakeIntrusive<TYtPathInfo>(path);
+ tables.push_back(pathInfo);
+ }
+
+ return TStatus::Ok;
+}
+
+TStatus CollectPathsAndLabelsReady(TMaybe<TVector<TYtPathInfo::TPtr>>& tables, TJoinLabels& labels,
+ const TStructExprType*& itemType, const TStructExprType*& itemTypeBeforePremap,
+ const TYtJoinNodeLeaf& leaf, TExprContext& ctx)
+{
+ TVector<TYtPathInfo::TPtr> existingTables;
+ TStatus result = CollectPathsAndLabels(existingTables, labels, itemType, itemTypeBeforePremap, leaf, ctx);
+ if (result != TStatus::Ok) {
+ return result;
+ }
+
+ bool ready = AllOf(existingTables, [](const auto& pathInfo) { return bool(pathInfo->Table->Stat); });
+ if (ready) {
+ tables = std::move(existingTables);
+ }
+ return TStatus::Ok;
+}
+
+
+TStatus CollectStatsAndMapJoinSettings(ESizeStatCollectMode sizeMode, TMapJoinSettings& mapSettings,
+ TJoinSideStats& leftStats, TJoinSideStats& rightStats,
+ const TMaybe<TVector<TYtPathInfo::TPtr>>& leftTables, const THashSet<TString>& leftJoinKeys,
+ const TMaybe<TVector<TYtPathInfo::TPtr>>& rightTables, const THashSet<TString>& rightJoinKeys,
+ TYtJoinNodeLeaf* leftLeaf, TYtJoinNodeLeaf* rightLeaf, const TYtState& state, bool isCross,
+ TString cluster, TExprContext& ctx)
+{
+ mapSettings = {};
+ leftStats = {};
+ rightStats = {};
+
+ if (leftTables.Defined()) {
+ YQL_ENSURE(leftLeaf);
+ auto premap = GetPremapLambda(*leftLeaf);
+ auto joinSideStatus = CollectJoinSideStats(sizeMode, leftStats, leftLeaf->Section, state, cluster,
+ *leftTables, leftJoinKeys, isCross, premap, ctx);
+ if (joinSideStatus.Level != TStatus::Ok) {
+ return joinSideStatus;
+ }
+
+ mapSettings.LeftRows = leftStats.RowsCount;
+ mapSettings.LeftSize = leftStats.Size;
+ mapSettings.LeftCount = leftTables->size();
+ mapSettings.LeftUnique = leftStats.HasUniqueKeys && mapSettings.LeftCount == 1;
+ }
+
+ if (rightTables.Defined()) {
+ YQL_ENSURE(rightLeaf);
+ auto premap = GetPremapLambda(*rightLeaf);
+ auto joinSideStatus = CollectJoinSideStats(sizeMode, rightStats, rightLeaf->Section, state, cluster,
+ *rightTables, rightJoinKeys, isCross, premap, ctx);
+ if (joinSideStatus.Level != TStatus::Ok) {
+ return joinSideStatus;
+ }
+
+ mapSettings.RightRows = rightStats.RowsCount;
+ mapSettings.RightSize = rightStats.Size;
+ mapSettings.RightCount = rightTables->size();
+ mapSettings.RightUnique = rightStats.HasUniqueKeys && mapSettings.RightCount == 1;
+ }
+
+ if (sizeMode == ESizeStatCollectMode::RawSize) {
+ mapSettings.LeftMemSize = mapSettings.LeftSize;
+ mapSettings.RightMemSize = mapSettings.RightSize;
+ }
+
+ return TStatus::Ok;
+}
+
+
+TStatus RewriteYtEquiJoinLeaf(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, TYtJoinNodeLeaf& leftLeaf,
+ TYtJoinNodeLeaf& rightLeaf, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ TJoinLabels labels;
+
+ TMaybe<TVector<TYtPathInfo::TPtr>> leftTables;
+ TMaybe<TVector<TYtPathInfo::TPtr>> rightTables;
+ const TStructExprType* leftItemType = nullptr;
+ const TStructExprType* leftItemTypeBeforePremap = nullptr;
+ const TStructExprType* rightItemType = nullptr;
+ const TStructExprType* rightItemTypeBeforePremap = nullptr;
+
+ {
+ auto status = CollectPathsAndLabelsReady(leftTables, labels, leftItemType, leftItemTypeBeforePremap, leftLeaf, ctx);
+ if (status != TStatus::Ok) {
+ YQL_ENSURE(status.Level == TStatus::Error);
+ return status;
+ }
+
+ status = CollectPathsAndLabelsReady(rightTables, labels, rightItemType, rightItemTypeBeforePremap, rightLeaf, ctx);
+ if (status != TStatus::Ok) {
+ YQL_ENSURE(status.Level == TStatus::Error);
+ return status;
+ }
+ }
+
+ const auto joinType = op.JoinKind->Content();
+ const auto disableOptimizers = state->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>());
+ const unsigned readyCount = unsigned(leftTables.Defined()) + rightTables.Defined();
+ if (!readyCount) {
+ return TStatus::Repeat;
+ } else if (readyCount == 1 && (disableOptimizers.contains("EarlyMapJoin") || joinType == "Cross")) {
+ return TStatus::Repeat;
+ }
+
+ bool empty = false;
+ if (leftTables.Defined()
+ && 0ul == Accumulate(*leftTables, 0ul, [] (ui64 sum, const TYtPathInfo::TPtr& p) { return sum + p->Table->Stat->RecordsCount; })
+ && AllOf(*leftTables, [](const TYtPathInfo::TPtr& p) { return !p->Table->Meta->IsDynamic; })
+ ) {
+ if (joinType == "Inner" || joinType == "Left" || joinType == "LeftOnly" || joinType == "LeftSemi" || joinType == "RightSemi" || joinType == "Cross") {
+ empty = true;
+ YQL_CLOG(INFO, ProviderYt) << "Left join side is empty";
+ }
+ }
+ if (!empty
+ && rightTables.Defined()
+ && 0ul == Accumulate(*rightTables, 0ul, [] (ui64 sum, const TYtPathInfo::TPtr& p) { return sum + p->Table->Stat->RecordsCount; })
+ && AllOf(*rightTables, [](const TYtPathInfo::TPtr& p) { return !p->Table->Meta->IsDynamic; })
+ ) {
+ if (joinType == "Inner" || joinType == "Right" || joinType == "RightOnly" || joinType == "RightSemi" || joinType == "LeftSemi" || joinType == "Cross") {
+ empty = true;
+ YQL_CLOG(INFO, ProviderYt) << "Right join side is empty";
+ }
+ }
+ if (empty) {
+ return RewriteYtEmptyJoin(equiJoin, labels, op, leftLeaf, rightLeaf, state, ctx)
+ ? TStatus::Ok
+ : TStatus::Error;
+ }
+
+ const bool isCross = joinType == "Cross";
+ auto leftJoinKeys = BuildJoinKeys(labels.Inputs[0], *op.LeftLabel);
+ auto rightJoinKeys = BuildJoinKeys(labels.Inputs[1], *op.RightLabel);
+ auto leftJoinKeyList = BuildJoinKeyList(labels.Inputs[0], *op.LeftLabel);
+ auto rightJoinKeyList = BuildJoinKeyList(labels.Inputs[1], *op.RightLabel);
+ YQL_ENSURE(leftJoinKeys.size() <= leftJoinKeyList.size());
+ YQL_ENSURE(rightJoinKeys.size() <= rightJoinKeyList.size());
+ YQL_ENSURE(leftJoinKeyList.size() == rightJoinKeyList.size());
+ if (!isCross) {
+ YQL_CLOG(INFO, ProviderYt) << "leftJoinKeys: " << JoinSeq(",", leftJoinKeyList)
+ << ", rightJoinKeys: " << JoinSeq(",", rightJoinKeyList);
+ }
+
+ auto const& linkSettings = op.LinkSettings;
+ ui64 mergeTablesLimit = 0;
+ TMaybe<bool> mergeUseSmallAsPrimary;
+ double mergeUnsortedFactor = 0.2;
+ bool forceMergeJoin = false;
+ if (auto limit = state->Configuration->JoinMergeTablesLimit.Get()) {
+ YQL_CLOG(INFO, ProviderYt) << "JoinMergeTablesLimit: " << *limit;
+ mergeTablesLimit = *limit;
+
+ if (mergeUseSmallAsPrimary = state->Configuration->JoinMergeUseSmallAsPrimary.Get()) {
+ YQL_CLOG(INFO, ProviderYt) << "JoinMergeUseSmallAsPrimary: " << *mergeUseSmallAsPrimary;
+ }
+
+ if (auto factor = state->Configuration->JoinMergeUnsortedFactor.Get()) {
+ YQL_CLOG(INFO, ProviderYt) << "JoinMergeUnsortedFactor: " << *factor;
+ mergeUnsortedFactor = *factor;
+ }
+
+ if (auto force = state->Configuration->JoinMergeForce.Get()) {
+ YQL_CLOG(INFO, ProviderYt) << "JoinMergeForce: " << *force;
+ forceMergeJoin = *force;
+ } else if (linkSettings.ForceSortedMerge) {
+ YQL_CLOG(INFO, ProviderYt) << "Got forceSortedMerge from link settings";
+ forceMergeJoin = true;
+ }
+ }
+
+ auto cluster = TString{equiJoin.DataSink().Cluster().Value()};
+
+ TMapJoinSettings mapSettings;
+ TJoinSideStats leftStats;
+ TJoinSideStats rightStats;
+
+ const bool allowLookupJoin = !isCross && leftTables.Defined() && rightTables.Defined();
+ if (allowLookupJoin) {
+ auto status = CollectStatsAndMapJoinSettings(ESizeStatCollectMode::RawSize, mapSettings, leftStats, rightStats,
+ leftTables, leftJoinKeys, rightTables, rightJoinKeys,
+ &leftLeaf, &rightLeaf, *state, isCross, cluster, ctx);
+ if (status.Level != TStatus::Ok) {
+ return (status.Level == TStatus::Repeat) ? TStatus::Ok : status;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Considering LookupJoin: left table(s): "
+ << leftStats.TableNames << " with size: " << mapSettings.LeftSize << ", rows: "
+ << mapSettings.LeftRows << ", dynamic : " << leftStats.IsDynamic << ", right table(s): "
+ << rightStats.TableNames << " with size: " << mapSettings.RightSize << ", rows: "
+ << mapSettings.RightRows << ", dynamic : " << rightStats.IsDynamic;
+
+ YQL_CLOG(INFO, ProviderYt) << "Join kind: " << op.JoinKind->Content() << ", left unique: " << leftStats.HasUniqueKeys
+ << ", right unique: " << rightStats.HasUniqueKeys << ", left sorted prefix: ["
+ << JoinSeq(",", leftStats.SortedKeys) << "], right sorted prefix: ["
+ << JoinSeq(",", rightStats.SortedKeys) << "]";
+
+ auto lookupJoinLimit = Min(state->Configuration->LookupJoinLimit.Get().GetOrElse(0),
+ state->Configuration->EvaluationTableSizeLimit.Get().GetOrElse(Max<ui64>()));
+ auto lookupJoinMaxRows = state->Configuration->LookupJoinMaxRows.Get().GetOrElse(0);
+
+ bool isLeftSorted = leftStats.SortedKeys.size() >= leftJoinKeys.size();
+ bool isRightSorted = rightStats.SortedKeys.size() >= rightJoinKeys.size();
+
+ // TODO: support ANY for left side via Reduce with PartitionByKey
+ bool leftAny = linkSettings.LeftHints.contains("any");
+ bool rightAny = linkSettings.RightHints.contains("any");
+
+ bool isLeftAllowLookupJoin = !leftAny && isLeftSorted && (mapSettings.RightRows < lookupJoinMaxRows) &&
+ (mapSettings.RightMemSize < lookupJoinLimit) &&
+ (joinType == "Inner" || joinType == "LeftSemi") && !rightStats.IsDynamic;
+
+ bool isRightAllowLookupJoin = !rightAny && isRightSorted && (mapSettings.LeftRows < lookupJoinMaxRows) &&
+ (mapSettings.LeftMemSize < lookupJoinLimit) &&
+ (joinType == "Inner" || joinType == "RightSemi") && !leftStats.IsDynamic;
+
+ YQL_CLOG(INFO, ProviderYt) << "LookupJoin: isLeftAllowLookupJoin: " << isLeftAllowLookupJoin
+ << ", isRightAllowLookupJoin: " << isRightAllowLookupJoin;
+
+ if (isLeftAllowLookupJoin || isRightAllowLookupJoin) {
+ bool swapTables = false;
+ if (isLeftAllowLookupJoin && isRightAllowLookupJoin) {
+ swapTables = (mapSettings.LeftSize < mapSettings.RightSize);
+ }
+ else if (!isLeftAllowLookupJoin) {
+ swapTables = true;
+ }
+
+ mapSettings.SwapTables = swapTables;
+
+ if (swapTables) {
+ DoSwap(mapSettings.LeftRows, mapSettings.RightRows);
+ DoSwap(mapSettings.LeftSize, mapSettings.RightSize);
+ DoSwap(mapSettings.LeftMemSize, mapSettings.RightMemSize);
+ DoSwap(mapSettings.LeftUnique, mapSettings.RightUnique);
+ YQL_CLOG(INFO, ProviderYt) << "Selected LookupJoin: filter over the right table, use content of the left one";
+ return RewriteYtMapJoin(equiJoin, labels, true, op, rightLeaf, leftLeaf, ctx, mapSettings, false, state) ?
+ TStatus::Ok : TStatus::Error;
+ } else {
+ YQL_CLOG(INFO, ProviderYt) << "Selected LookupJoin: filter over the left table, use content of the right one";
+ return RewriteYtMapJoin(equiJoin, labels, true, op, leftLeaf, rightLeaf, ctx, mapSettings, false, state) ?
+ TStatus::Ok : TStatus::Error;
+ }
+ }
+ }
+
+ {
+ auto status = CollectStatsAndMapJoinSettings(ESizeStatCollectMode::ColumnarSize, mapSettings, leftStats, rightStats,
+ leftTables, leftJoinKeys, rightTables, rightJoinKeys,
+ &leftLeaf, &rightLeaf, *state, isCross, cluster, ctx);
+ if (status.Level != TStatus::Ok) {
+ return (status.Level == TStatus::Repeat) ? TStatus::Ok : status;
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Left table(s): "
+ << (leftTables.Defined() ? leftStats.TableNames : "(not ready)") << " with size: " << mapSettings.LeftSize << ", rows: "
+ << mapSettings.LeftRows << ", dynamic : " << leftStats.IsDynamic << ", right table(s): "
+ << (rightTables.Defined() ? rightStats.TableNames : "(not ready)") << " with size: " << mapSettings.RightSize << ", rows: "
+ << mapSettings.RightRows << ", dynamic : " << rightStats.IsDynamic;
+
+ YQL_CLOG(INFO, ProviderYt) << "Join kind: " << op.JoinKind->Content() << ", left hints: " <<
+ (linkSettings.LeftHints ? JoinSeq(",", linkSettings.LeftHints) : "none") << ", right hints: " <<
+ (linkSettings.RightHints ? JoinSeq(",", linkSettings.RightHints) : "none") << ", left unique: " << leftStats.HasUniqueKeys
+ << ", right unique: " << rightStats.HasUniqueKeys << ", left sorted prefix: ["
+ << JoinSeq(",", leftStats.SortedKeys) << "], right sorted prefix: ["
+ << JoinSeq(",", rightStats.SortedKeys) << "]";
+
+ bool allowOrderedJoin = !isCross && leftTables.Defined() && rightTables.Defined();
+
+ TMergeJoinSortInfo sortInfo;
+ sortInfo.LeftSortedKeys = leftStats.SortedKeys;
+ sortInfo.RightSortedKeys = rightStats.SortedKeys;
+
+ sortInfo.LeftBeforePremap = leftItemTypeBeforePremap;
+ sortInfo.RightBeforePremap = rightItemTypeBeforePremap;
+
+ if (allowOrderedJoin) {
+ bool isLeftSorted = sortInfo.LeftSortedKeys.size() >= leftJoinKeys.size();
+ bool isRightSorted = sortInfo.RightSortedKeys.size() >= rightJoinKeys.size();
+
+ if (allowOrderedJoin && !isLeftSorted && !isRightSorted && !forceMergeJoin) {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because both sides are unsorted";
+ allowOrderedJoin = false;
+ }
+
+ if (allowOrderedJoin && (leftJoinKeys.size() != leftJoinKeyList.size() ||
+ rightJoinKeys.size() != rightJoinKeyList.size()))
+ {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because side(s) contain duplicate join keys";
+ allowOrderedJoin = false;
+ }
+
+ if (allowOrderedJoin && !forceMergeJoin && (mapSettings.LeftCount + mapSettings.RightCount > mergeTablesLimit)) {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because there are too many tables, mergeTablesLimit: " << mergeTablesLimit;
+ allowOrderedJoin = false;
+ }
+
+ if (allowOrderedJoin && (!isLeftSorted || !isRightSorted)) {
+ if (!isLeftSorted && !isRightSorted) {
+ YQL_ENSURE(forceMergeJoin);
+ sortInfo.AdditionalSort = TChoice::Both;
+ sortInfo.LeftSortedKeys = leftJoinKeyList;
+ sortInfo.RightSortedKeys = BuildCompatibleSortWith(sortInfo.LeftSortedKeys, leftJoinKeyList, rightJoinKeyList);
+ } else if (!isRightSorted) {
+ if (!forceMergeJoin && mapSettings.RightSize > mapSettings.LeftSize * mergeUnsortedFactor) {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because unsorted right table is too big";
+ allowOrderedJoin = false;
+ } else {
+ sortInfo.AdditionalSort = TChoice::Right;
+ sortInfo.RightSortedKeys = BuildCompatibleSortWith(sortInfo.LeftSortedKeys, leftJoinKeyList, rightJoinKeyList);
+ }
+ } else {
+ if (!forceMergeJoin && mapSettings.LeftSize > mapSettings.RightSize * mergeUnsortedFactor) {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because unsorted left table is too big";
+ allowOrderedJoin = false;
+ } else {
+ sortInfo.AdditionalSort = TChoice::Left;
+ sortInfo.LeftSortedKeys = BuildCompatibleSortWith(sortInfo.RightSortedKeys, rightJoinKeyList, leftJoinKeyList);
+ }
+ }
+
+ if (allowOrderedJoin && !isLeftSorted) {
+ isLeftSorted = true;
+ YQL_ENSURE(sortInfo.LeftSortedKeys.size() == leftJoinKeys.size());
+ if (leftStats.NeedsRemap) {
+ sortInfo.NeedRemapBeforeSort = Merge(sortInfo.NeedRemapBeforeSort, TChoice::Left);
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Added sort of the left table"
+ << (leftStats.NeedsRemap ? " with additional remapping" : "");
+ }
+
+ if (allowOrderedJoin && !isRightSorted) {
+ isRightSorted = true;
+ YQL_ENSURE(sortInfo.RightSortedKeys.size() == rightJoinKeys.size());
+ if (rightStats.NeedsRemap) {
+ sortInfo.NeedRemapBeforeSort = Merge(sortInfo.NeedRemapBeforeSort, TChoice::Right);
+ }
+ YQL_CLOG(INFO, ProviderYt) << "Added sort of the right table"
+ << (rightStats.NeedsRemap ? " with additional remapping" : "");
+ }
+ }
+
+ if (allowOrderedJoin) {
+ YQL_ENSURE(isLeftSorted);
+ YQL_ENSURE(isRightSorted);
+
+ YQL_ENSURE(sortInfo.LeftSortedKeys.size() >= leftJoinKeys.size());
+ YQL_ENSURE(sortInfo.RightSortedKeys.size() >= rightJoinKeys.size());
+
+ // TODO: in some cases we can allow output to be sorted more strictly
+ sortInfo.LeftSortedKeys.resize(leftJoinKeys.size());
+ sortInfo.RightSortedKeys.resize(rightJoinKeys.size());
+
+ const bool allowColumnRenames = state->Configuration->JoinAllowColumnRenames.Get().GetOrElse(true)
+ // TODO: remove next line after https://st.yandex-team.ru/YT-12738 is fixed
+ && !leftStats.IsDynamic && !rightStats.IsDynamic;
+
+ sortInfo.CommonSortedKeys = BuildCommonSortPrefix(sortInfo.LeftSortedKeys, sortInfo.RightSortedKeys,
+ leftJoinKeyList, rightJoinKeyList, sortInfo.LeftKeyRenames, sortInfo.RightKeyRenames, allowColumnRenames);
+
+ YQL_CLOG(INFO, ProviderYt) << "Common sorted prefix (with JoinAllowColumnRenames: " << allowColumnRenames << ") is ["
+ << JoinSeq(", ", sortInfo.CommonSortedKeys) << "], left sorted prefix: ["
+ << JoinSeq(", ", sortInfo.LeftSortedKeys) << "], right sorted prefix: ["
+ << JoinSeq(", ", sortInfo.RightSortedKeys) << "], left renames: ["
+ << RenamesToString(sortInfo.LeftKeyRenames) << "], right renames: ["
+ << RenamesToString(sortInfo.RightKeyRenames) << "]";
+
+ if (sortInfo.CommonSortedKeys.size() < leftJoinKeys.size()) {
+ YQL_CLOG(INFO, ProviderYt) << "Skipped OrderedJoin, because table sort prefixes are incompatible with "
+ "join keys";
+ allowOrderedJoin = false;
+ }
+ }
+ }
+
+ if (allowOrderedJoin) {
+ if (joinType.EndsWith("Semi")) {
+ const bool leftSemi = joinType == "LeftSemi";
+ const auto& dictLabel = leftSemi ? labels.Inputs[1] : labels.Inputs[0];
+ auto& dictHints = leftSemi ? op.LinkSettings.RightHints : op.LinkSettings.LeftHints;
+
+ op.JoinKind = ctx.NewAtom(op.JoinKind->Pos(), "Inner");
+ dictHints.insert("any");
+
+ auto columnsToRemove = dictLabel.EnumerateAllColumns();
+ op.OutputRemoveColumns.insert(columnsToRemove.begin(), columnsToRemove.end());
+
+ YQL_CLOG(INFO, ProviderYt) << "Will rewrite ordered " << joinType << " to ANY Inner + remove columns ["
+ << JoinSeq(", ", columnsToRemove) << "]";
+ return TStatus::Ok;
+ }
+
+ bool allowPrimaryLeft = (joinType == "Inner" || joinType == "Left" || joinType == "LeftOnly");
+ bool allowPrimaryRight = (joinType == "Inner" || joinType == "Right" || joinType == "RightOnly");
+
+ bool swapTables = false;
+ bool useJoinReduce = false;
+ bool useJoinReduceForSecond = false;
+ bool tryFirstAsPrimary = false;
+
+ if (allowPrimaryLeft != allowPrimaryRight) {
+ swapTables = allowPrimaryLeft;
+ auto primary = swapTables ? TChoice::Left : TChoice::Right;
+ useJoinReduce = !HasNonTrivialAny(linkSettings, mapSettings, primary);
+ } else if (allowPrimaryLeft) {
+ YQL_ENSURE(allowPrimaryRight);
+ // both tables can be chosen as primary
+ bool biggerHasUniqueKeys = mapSettings.RightSize > mapSettings.LeftSize ?
+ mapSettings.RightUnique : mapSettings.LeftUnique;
+
+ if (biggerHasUniqueKeys) {
+ // it is safe to use smaller table as primary
+ swapTables = mapSettings.RightSize > mapSettings.LeftSize;
+ } else if (mergeUseSmallAsPrimary) {
+ // explicit setting
+ if (*mergeUseSmallAsPrimary) {
+ // use smaller table as primary
+ swapTables = mapSettings.RightSize > mapSettings.LeftSize;
+ } else {
+ // use bigger table as primary
+ swapTables = mapSettings.LeftSize > mapSettings.RightSize;
+ }
+ } else {
+ // make bigger table last one, and try first (smaller) as primary
+ swapTables = mapSettings.LeftSize > mapSettings.RightSize;
+ tryFirstAsPrimary = true;
+ }
+
+ auto primary = swapTables ? TChoice::Left : TChoice::Right;
+ if (tryFirstAsPrimary) {
+ useJoinReduceForSecond = !HasNonTrivialAny(linkSettings, mapSettings, primary);
+ useJoinReduce = !HasNonTrivialAny(linkSettings, mapSettings, Invert(primary));
+ } else {
+ useJoinReduce = !HasNonTrivialAny(linkSettings, mapSettings, primary);
+ }
+ } else {
+ // try to move non-fat table to the left, otherwise keep them as is
+ if (mapSettings.LeftUnique != mapSettings.RightUnique) {
+ swapTables = mapSettings.RightUnique;
+ } else {
+ swapTables = mapSettings.LeftSize > mapSettings.RightSize;
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "OrderedJoin allowPrimaryLeft : " << allowPrimaryLeft
+ << ", allowPrimaryRight " << allowPrimaryRight;
+
+ YQL_CLOG(INFO, ProviderYt) << "Selected OrderedJoin over the " << (swapTables ? "left" : "right")
+ << " table as primary, will use join reduce: " << useJoinReduce << ", will try other primary: " << tryFirstAsPrimary
+ << ", will use join reduce for other primary: " << useJoinReduceForSecond;
+
+ bool skipped = false;
+ if (!RewriteYtMergeJoin(equiJoin, labels, op,
+ swapTables ? rightLeaf : leftLeaf,
+ swapTables ? leftLeaf : rightLeaf,
+ state, ctx, swapTables, useJoinReduce, tryFirstAsPrimary, useJoinReduceForSecond,
+ swapTables ? Invert(sortInfo) : sortInfo,
+ skipped))
+ {
+ return TStatus::Error;
+ }
+
+ if (!skipped) {
+ return TStatus::Ok;
+ }
+ }
+
+ if (auto mapJoinLimit = state->Configuration->MapJoinLimit.Get(); mapJoinLimit && !forceMergeJoin) {
+ YQL_CLOG(INFO, ProviderYt) << "MapJoinLimit: " << *mapJoinLimit;
+ mapSettings.MapJoinLimit = *mapJoinLimit;
+
+ if (mapSettings.MapJoinLimit) {
+ mapSettings.MapJoinShardMinRows = state->Configuration->MapJoinShardMinRows.Get().GetOrElse(1);
+ mapSettings.MapJoinShardCount = state->Configuration->MapJoinShardCount.Get().GetOrElse(1);
+
+ ui64 rightLimit = mapSettings.MapJoinLimit;
+ ui64 leftLimit = mapSettings.MapJoinLimit;
+ auto leftPartSize = mapSettings.CalculatePartSize(mapSettings.LeftRows);
+ auto rightPartSize = mapSettings.CalculatePartSize(mapSettings.RightRows);
+ TMaybe<ui64> leftPartCount;
+ TMaybe<ui64> rightPartCount;
+ if (leftPartSize) {
+ YQL_ENSURE(leftTables.Defined());
+ leftPartCount = (mapSettings.LeftRows + *leftPartSize - 1) / *leftPartSize;
+ }
+
+ if (rightPartSize) {
+ YQL_ENSURE(rightTables.Defined());
+ rightPartCount = (mapSettings.RightRows + *rightPartSize - 1) / *rightPartSize;
+ }
+
+ bool allowShardRight = false;
+ const bool rightUnique = linkSettings.RightHints.contains("unique") || mapSettings.RightUnique;
+ const bool denyShardRight = linkSettings.RightHints.contains("any") && !rightUnique;
+ // TODO: currently we disable sharding when other side is not ready
+ if (leftTables.Defined() && rightPartCount && !denyShardRight && ((joinType == "Inner") || (joinType == "Cross") ||
+ (joinType == "LeftSemi" && rightUnique))) {
+ allowShardRight = true;
+ rightLimit *= *rightPartCount;
+ }
+
+ bool allowShardLeft = false;
+ const bool leftUnique = linkSettings.LeftHints.contains("unique") || mapSettings.LeftUnique;
+ const bool denyShardLeft = linkSettings.LeftHints.contains("any") && !leftUnique;
+ // TODO: currently we disable sharding when other side is not ready
+ if (rightTables.Defined() && leftPartCount && !denyShardLeft && ((joinType == "Inner") || (joinType == "Cross") ||
+ (joinType == "RightSemi" && leftUnique))) {
+ allowShardLeft = true;
+ leftLimit *= *leftPartCount;
+ }
+
+ auto mapJoinUseFlow = state->Configuration->MapJoinUseFlow.Get().GetOrElse(DEFAULT_MAP_JOIN_USE_FLOW);
+ if (leftTables.Defined()) {
+ auto status = UpdateInMemorySizeSetting(mapSettings, leftLeaf.Section, labels, op, ctx, true, leftItemType, leftJoinKeyList, state, cluster, *leftTables, mapJoinUseFlow);
+ if (status.Level != TStatus::Ok) {
+ return (status.Level == TStatus::Repeat) ? TStatus::Ok : status;
+ }
+ }
+
+ if (rightTables.Defined()) {
+ auto status = UpdateInMemorySizeSetting(mapSettings, rightLeaf.Section, labels, op, ctx, false, rightItemType, rightJoinKeyList, state, cluster, *rightTables, mapJoinUseFlow);
+ if (status.Level != TStatus::Ok) {
+ return (status.Level == TStatus::Repeat) ? TStatus::Ok : status;
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "MapJoinShardMinRows: " << mapSettings.MapJoinShardMinRows
+ << ", MapJoinShardCount: " << mapSettings.MapJoinShardCount
+ << ", left is present: " << leftTables.Defined()
+ << ", left size limit: " << leftLimit << ", left mem size:" << mapSettings.LeftMemSize
+ << ", right is present: " << rightTables.Defined()
+ << ", right size limit: " << rightLimit << ", right mem size: " << mapSettings.RightMemSize;
+
+ bool leftAny = linkSettings.LeftHints.contains("any");
+ bool rightAny = linkSettings.RightHints.contains("any");
+
+ const bool isLeftAllowMapJoin = !leftAny && rightTables.Defined() && (mapSettings.RightMemSize <= rightLimit) &&
+ (joinType == "Inner" || joinType == "Left" || joinType == "LeftOnly" || joinType == "LeftSemi" || joinType == "Cross")
+ && !rightStats.IsDynamic;
+ const bool isRightAllowMapJoin = !rightAny && leftTables.Defined() && (mapSettings.LeftMemSize <= leftLimit) &&
+ (joinType == "Inner" || joinType == "Right" || joinType == "RightOnly" || joinType == "RightSemi" || joinType == "Cross")
+ && !leftStats.IsDynamic;
+ YQL_CLOG(INFO, ProviderYt) << "MapJoin: isLeftAllowMapJoin: " << isLeftAllowMapJoin
+ << ", isRightAllowMapJoin: " << isRightAllowMapJoin;
+
+ if (isLeftAllowMapJoin || isRightAllowMapJoin) {
+ bool swapTables = false;
+ if (isLeftAllowMapJoin && isRightAllowMapJoin) {
+ swapTables = (mapSettings.LeftSize < mapSettings.RightSize);
+ }
+ else if (!isLeftAllowMapJoin) {
+ swapTables = true;
+ }
+
+ mapSettings.SwapTables = swapTables;
+
+ if (swapTables) {
+ DoSwap(mapSettings.LeftRows, mapSettings.RightRows);
+ DoSwap(mapSettings.LeftSize, mapSettings.RightSize);
+ DoSwap(mapSettings.LeftMemSize, mapSettings.RightMemSize);
+ DoSwap(mapSettings.LeftUnique, mapSettings.RightUnique);
+ YQL_CLOG(INFO, ProviderYt) << "Selected MapJoin: map over the right table, use content of the left one";
+ return RewriteYtMapJoin(equiJoin, labels, false, op, rightLeaf, leftLeaf, ctx, mapSettings, allowShardLeft, state) ?
+ TStatus::Ok : TStatus::Error;
+ } else {
+ YQL_CLOG(INFO, ProviderYt) << "Selected MapJoin: map over the left table, use content of the right one";
+ return RewriteYtMapJoin(equiJoin, labels, false, op, leftLeaf, rightLeaf, ctx, mapSettings, allowShardRight, state) ?
+ TStatus::Ok : TStatus::Error;
+ }
+ }
+ }
+ }
+
+ if (leftTables.Defined() && rightTables.Defined()) {
+ YQL_CLOG(INFO, ProviderYt) << "Selected CommonJoin";
+ return RewriteYtCommonJoin(equiJoin, labels, op, leftLeaf, rightLeaf, state, ctx, leftStats.HasUniqueKeys,
+ rightStats.HasUniqueKeys, mapSettings.LeftSize, mapSettings.RightSize) ?
+ TStatus::Ok : TStatus::Error;
+ } else {
+ return TStatus::Repeat;
+ }
+}
+
+TExprBase MakePremap(const TYtJoinNodeLeaf& leaf, TExprContext& ctx) {
+ if (leaf.Premap) {
+ return leaf.Premap.Cast();
+ }
+ return Build<TCoVoid>(ctx, leaf.Section.Pos()).Done();
+}
+
+TExprNode::TPtr ExportJoinTree(const TYtJoinNodeOp& op, TPositionHandle pos, TVector<TYtSection>& inputSections,
+ TVector<TExprBase>& premaps, THashSet<TString>& outputRemoveColumns, TExprContext& ctx)
+{
+ TExprNode::TPtr left;
+ if (auto leaf = dynamic_cast<const TYtJoinNodeLeaf*>(op.Left.Get())) {
+ left = ctx.NewAtom(pos, op.Left->Scope[0]);
+ inputSections.push_back(leaf->Section);
+ premaps.push_back(MakePremap(*leaf, ctx));
+ } else {
+ left = ExportJoinTree(*dynamic_cast<const TYtJoinNodeOp*>(op.Left.Get()), pos, inputSections, premaps,
+ outputRemoveColumns, ctx);
+ }
+
+ TExprNode::TPtr right;
+ if (auto leaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Right.Get())) {
+ right = ctx.NewAtom(pos, op.Right->Scope[0]);
+ inputSections.push_back(leaf->Section);
+ premaps.push_back(MakePremap(*leaf, ctx));
+ }
+ else {
+ right = ExportJoinTree(*dynamic_cast<const TYtJoinNodeOp*>(op.Right.Get()), pos, inputSections, premaps,
+ outputRemoveColumns, ctx);
+ }
+
+ outputRemoveColumns.insert(op.OutputRemoveColumns.begin(), op.OutputRemoveColumns.end());
+
+ return ctx.NewList(pos, {
+ op.JoinKind,
+ left,
+ right,
+ op.LeftLabel,
+ op.RightLabel,
+ BuildEquiJoinLinkSettings(op.LinkSettings, ctx),
+ });
+}
+
+void CombineJoinStatus(TStatus& status, TStatus other) {
+ YQL_ENSURE(status == TStatus::Repeat || status == TStatus::Ok || status == TStatus::Error);
+
+ switch (other.Level) {
+ case TStatus::Error:
+ status = TStatus::Error;
+ break;
+ case TStatus::Ok:
+ if (status != TStatus::Error) {
+ status = TStatus::Ok;
+ }
+ break;
+ case TStatus::Repeat:
+ break;
+ default:
+ YQL_ENSURE(false, "Unexpected join status");
+ }
+}
+
+TStatus RewriteYtEquiJoinLeaves(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx) {
+ TYtJoinNodeLeaf* leftLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Left.Get());
+ TYtJoinNodeLeaf* rightLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Right.Get());
+
+ TStatus result = TStatus::Repeat;
+ if (!leftLeaf) {
+ auto& leftOp = *dynamic_cast<TYtJoinNodeOp*>(op.Left.Get());
+ CombineJoinStatus(result, RewriteYtEquiJoinLeaves(equiJoin, leftOp, state, ctx));
+ if (result.Level == TStatus::Error) {
+ return result;
+ }
+ if (result.Level == TStatus::Ok && leftOp.Output) {
+ // convert to leaf
+ op.Left = ConvertYtEquiJoinToLeaf(leftOp, equiJoin.Pos(), ctx);
+ }
+ }
+
+ if (!rightLeaf) {
+ auto& rightOp = *dynamic_cast<TYtJoinNodeOp*>(op.Right.Get());
+ CombineJoinStatus(result, RewriteYtEquiJoinLeaves(equiJoin, rightOp, state, ctx));
+ if (result.Level == TStatus::Error) {
+ return result;
+ }
+ if (result.Level == TStatus::Ok && rightOp.Output) {
+ // convert to leaf
+ op.Right = ConvertYtEquiJoinToLeaf(rightOp, equiJoin.Pos(), ctx);
+ }
+ }
+
+ if (leftLeaf && rightLeaf) {
+ CombineJoinStatus(result, RewriteYtEquiJoinLeaf(equiJoin, op, *leftLeaf, *rightLeaf, state, ctx));
+ }
+
+ return result;
+}
+
+enum class EStarRewriteStatus {
+ WaitInput,
+ None,
+ Ok,
+ Error,
+};
+
+bool IsJoinKindCompatibleWithStar(TStringBuf kind) {
+ return kind == "Inner" ||
+ kind == "Left" || kind == "LeftSemi" || kind == "LeftOnly" ||
+ kind == "Right" || kind == "RightSemi" || kind == "RightOnly";
+}
+
+bool IsSideSuitableForStarJoin(TStringBuf joinKind, const TEquiJoinLinkSettings& linkSettings, const TMapJoinSettings& mapJoinSettings, bool isLeft)
+{
+ YQL_ENSURE(IsJoinKindCompatibleWithStar(joinKind));
+
+ if (joinKind == (isLeft ? "Left" : "Right") || joinKind == "Inner")
+ {
+ // other side should be unique
+ return IsEffectivelyUnique(linkSettings, mapJoinSettings, !isLeft);
+ } else if (joinKind.StartsWith(isLeft ? "Left" : "Right")) {
+ return true;
+ }
+
+ return false;
+}
+
+bool ExtractJoinKeysForStarJoin(const TExprNode& labelNode, TString& label, TVector<TString>& keyList) {
+ YQL_ENSURE(labelNode.ChildrenSize() > 0);
+ YQL_ENSURE(labelNode.ChildrenSize() % 2 == 0);
+ label = {};
+ keyList = {};
+ keyList.reserve(labelNode.ChildrenSize() / 2);
+
+ for (size_t i = 0; i < labelNode.ChildrenSize(); i += 2) {
+ YQL_ENSURE(labelNode.Child(i)->IsAtom());
+ YQL_ENSURE(labelNode.Child(i + 1)->IsAtom());
+ auto table = labelNode.Child(i)->Content();
+ auto key = labelNode.Child(i + 1)->Content();
+
+ if (!label) {
+ label = table;
+ } else if (label != table) {
+ return false;
+ }
+
+ keyList.emplace_back(key);
+ }
+ return true;
+}
+
+TVector<const TTypeAnnotationNode*> BuildJoinKeyType(const TStructExprType& input, const TVector<TString>& keys) {
+ TVector<const TTypeAnnotationNode*> result;
+ result.reserve(keys.size());
+ for (auto& key : keys) {
+ auto maybeIndex = input.FindItem(key);
+ YQL_ENSURE(maybeIndex);
+ result.push_back(input.GetItems()[*maybeIndex]->GetItemType());
+ }
+ return result;
+}
+
+constexpr auto joinFixedArgsCount = 7U;
+
+const TStructExprType* GetJoinInputType(TYtEquiJoin equiJoin, size_t inputIndex, TExprContext& ctx) {
+ const auto premap = TMaybeNode<TCoLambda>(equiJoin.Ref().ChildPtr(joinFixedArgsCount + inputIndex));
+ TExprBase input = premap ? TExprBase(premap.Cast()) : TExprBase(equiJoin.Input().Item(inputIndex));
+ if (auto type = GetSequenceItemType(input, false, ctx)) {
+ return type->Cast<TStructExprType>();
+ }
+ return nullptr;
+}
+
+void CollectPossibleStarJoins(const TYtEquiJoin& equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, EStarRewriteStatus& collectStatus, TExprContext& ctx) {
+ YQL_ENSURE(!op.StarOptions);
+ if (collectStatus != EStarRewriteStatus::Ok) {
+ return;
+ }
+
+ auto leftLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Left.Get());
+ auto rightLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Right.Get());
+
+ auto leftOp = dynamic_cast<TYtJoinNodeOp*>(op.Left.Get());
+ auto rightOp = dynamic_cast<TYtJoinNodeOp*>(op.Right.Get());
+
+ if (!leftLeaf) {
+ YQL_ENSURE(leftOp);
+ CollectPossibleStarJoins(equiJoin, *leftOp, state, collectStatus, ctx);
+ }
+
+ if (!rightLeaf) {
+ YQL_ENSURE(rightOp);
+ CollectPossibleStarJoins(equiJoin, *rightOp, state, collectStatus, ctx);
+ }
+
+ if (collectStatus != EStarRewriteStatus::Ok) {
+ return;
+ }
+
+ if (!leftLeaf && !rightLeaf) {
+ // join of two joins is never a star join
+ return;
+ }
+
+ auto joinKind = op.JoinKind->Content();
+ if (!IsJoinKindCompatibleWithStar(joinKind) ||
+ (leftLeaf && leftLeaf->Scope.size() != 1) ||
+ (rightLeaf && rightLeaf->Scope.size() != 1))
+ {
+ return;
+ }
+
+ YQL_ENSURE(!leftLeaf || leftLeaf->Label->IsAtom());
+ YQL_ENSURE(!rightLeaf || rightLeaf->Label->IsAtom());
+
+ TJoinLabels labels;
+
+ TMaybe<TVector<TYtPathInfo::TPtr>> leftTables;
+ TMaybe<TVector<TYtPathInfo::TPtr>> rightTables;
+ const TStructExprType* leftItemType = nullptr;
+ const TStructExprType* leftItemTypeBeforePremap = nullptr;
+ const TStructExprType* rightItemType = nullptr;
+ const TStructExprType* rightItemTypeBeforePremap = nullptr;
+
+ THashSet<TString> leftJoinKeys;
+ THashSet<TString> rightJoinKeys;
+
+ TVector<TString> leftJoinKeyList;
+ TVector<TString> rightJoinKeyList;
+
+ if (leftLeaf) {
+ leftTables.ConstructInPlace();
+ auto status = CollectPathsAndLabels(*leftTables, labels, leftItemType, leftItemTypeBeforePremap, *leftLeaf, ctx);
+ if (status != TStatus::Ok) {
+ YQL_ENSURE(status.Level == TStatus::Error);
+ collectStatus = EStarRewriteStatus::Error;
+ return;
+ }
+
+ leftJoinKeys = BuildJoinKeys(labels.Inputs[0], *op.LeftLabel);
+ leftJoinKeyList = BuildJoinKeyList(labels.Inputs[0], *op.LeftLabel);
+ YQL_ENSURE(leftJoinKeys.size() <= leftJoinKeyList.size());
+ }
+
+ if (rightLeaf) {
+ rightTables.ConstructInPlace();
+ auto status = CollectPathsAndLabels(*rightTables, labels, rightItemType, rightItemTypeBeforePremap, *rightLeaf, ctx);
+ if (status != TStatus::Ok) {
+ YQL_ENSURE(status.Level == TStatus::Error);
+ collectStatus = EStarRewriteStatus::Error;
+ return;
+ }
+
+ rightJoinKeys = BuildJoinKeys(labels.Inputs[leftLeaf ? 1 : 0], *op.RightLabel);
+ rightJoinKeyList = BuildJoinKeyList(labels.Inputs[leftLeaf ? 1 : 0], *op.RightLabel);
+ }
+
+
+ auto cluster = TString{equiJoin.DataSink().Cluster().Value()};
+
+ TMapJoinSettings mapSettings;
+ TJoinSideStats leftStats;
+ TJoinSideStats rightStats;
+
+ {
+ bool isCross = false;
+ auto status = CollectStatsAndMapJoinSettings(ESizeStatCollectMode::NoSize, mapSettings, leftStats, rightStats,
+ leftTables, leftJoinKeys, rightTables, rightJoinKeys,
+ leftLeaf, rightLeaf, *state, isCross, cluster, ctx);
+
+ switch (status.Level) {
+ case TStatus::Error:
+ collectStatus = EStarRewriteStatus::Error;
+ break;
+ case TStatus::Ok:
+ break;
+ default:
+ YQL_ENSURE(false, "Unexpected collect stats status");
+ }
+
+ if (collectStatus != EStarRewriteStatus::Ok) {
+ return;
+ }
+ }
+
+ if (leftLeaf) {
+ if (leftStats.SortedKeys.size() < leftJoinKeys.size()) {
+ // left is not sorted
+ return;
+ }
+
+ if (leftJoinKeyList.size() != leftJoinKeys.size()) {
+ // right side contains duplicate join keys
+ return;
+ }
+ }
+
+ if (rightLeaf) {
+ if (rightStats.SortedKeys.size() < rightJoinKeys.size()) {
+ // right is not sorted
+ return;
+ }
+
+ if (rightJoinKeyList.size() != rightJoinKeys.size()) {
+ // right side contains duplicate join keys
+ return;
+ }
+ }
+
+ auto addStarOption = [&](bool isLeft) {
+
+ const auto& joinKeys = isLeft ? leftJoinKeys : rightJoinKeys;
+
+ TYtStarJoinOption starJoinOption;
+ starJoinOption.StarKeys.insert(joinKeys.begin(), joinKeys.end());
+ starJoinOption.StarInputIndex = isLeft ? leftLeaf->Index : rightLeaf->Index;
+ starJoinOption.StarLabel = isLeft ? leftLeaf->Label->Content() : rightLeaf->Label->Content();
+ starJoinOption.StarSortedKeys = isLeft ? leftStats.SortedKeys : rightStats.SortedKeys;
+
+ YQL_CLOG(INFO, ProviderYt) << "Adding " << (isLeft ? rightLeaf->Label->Content() : leftLeaf->Label->Content())
+ << " [" << JoinSeq(", ", isLeft ? rightJoinKeyList : leftJoinKeyList)
+ << "] to star " << starJoinOption.StarLabel << " [" << JoinSeq(", ", starJoinOption.StarKeys)
+ << "]";
+
+ op.StarOptions.emplace_back(std::move(starJoinOption));
+ };
+
+ if (leftLeaf && rightLeaf) {
+ YQL_ENSURE(leftLeaf->Label->Content() != rightLeaf->Label->Content());
+
+ auto inputKeyTypeLeft = BuildJoinKeyType(*leftItemType, leftJoinKeyList);
+ auto inputKeyTypeRight = BuildJoinKeyType(*rightItemType, rightJoinKeyList);
+ if (!IsSameAnnotation(*AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeLeft), ctx),
+ *AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeRight), ctx)))
+ {
+ // key types should match for merge star join to work
+ return;
+ }
+
+ if (IsSideSuitableForStarJoin(joinKind, op.LinkSettings, mapSettings, true)) {
+ addStarOption(true);
+ }
+
+ if (IsSideSuitableForStarJoin(joinKind, op.LinkSettings, mapSettings, false)) {
+ addStarOption(false);
+ }
+ } else {
+
+ auto childOp = leftLeaf ? rightOp : leftOp;
+
+ bool allowNonUnique = joinKind.EndsWith("Semi") || joinKind.EndsWith("Only");
+ bool allowKind = true;
+ if (joinKind.StartsWith("Left")) {
+ allowKind = leftLeaf == nullptr;
+ } else if (joinKind.StartsWith("Right")) {
+ allowKind = leftLeaf != nullptr;
+ }
+
+ if (childOp->StarOptions && allowKind && (allowNonUnique || IsEffectivelyUnique(op.LinkSettings, mapSettings, leftLeaf != nullptr))) {
+
+ const auto& leafJoinKeyList = leftLeaf ? leftJoinKeyList : rightJoinKeyList;
+ TString leafLabel = leftLeaf ? TString{leftLeaf->Label->Content()} : TString{rightLeaf->Label->Content()};
+
+ auto inputKeyTypeLeaf = BuildJoinKeyType(leftLeaf ? *leftItemType : *rightItemType, leafJoinKeyList);
+
+ TString childLabel;
+ TVector<TString> childKeyList;
+
+ if (ExtractJoinKeysForStarJoin(leftLeaf ? *op.RightLabel : *op.LeftLabel, childLabel, childKeyList)) {
+ TSet<TString> childKeys(childKeyList.begin(), childKeyList.end());
+ for (const auto& childOption : childOp->StarOptions) {
+ if (leafLabel == childOption.StarLabel || childLabel != childOption.StarLabel ||
+ childKeys != childOption.StarKeys)
+ {
+ continue;
+ }
+
+ auto starInputType = GetJoinInputType(equiJoin, childOption.StarInputIndex, ctx);
+ YQL_ENSURE(starInputType);
+ auto inputKeyTypeChild = BuildJoinKeyType(*starInputType, childKeyList);
+
+ if (!IsSameAnnotation(*AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeChild), ctx),
+ *AsDictKeyType(RemoveNullsFromJoinKeyType(inputKeyTypeLeaf), ctx)))
+ {
+ // key types should match for merge star join to work
+ return;
+ }
+
+
+ TYtStarJoinOption option = childOption;
+ YQL_CLOG(INFO, ProviderYt) << "Adding " << leafLabel << " [" << JoinSeq(", ", leafJoinKeyList)
+ << "] to star " << option.StarLabel << " [" << JoinSeq(", ", option.StarKeys)
+ << "]";
+
+ op.StarOptions.emplace_back(option);
+ }
+ YQL_ENSURE(op.StarOptions.size() <= 1);
+ }
+ }
+ }
+}
+
+const TStructExprType* AddLabelToStructType(const TStructExprType& input, TStringBuf label, TExprContext& ctx) {
+ TVector<const TItemExprType*> structItems;
+ for (auto& i : input.GetItems()) {
+ structItems.push_back(ctx.MakeType<TItemExprType>(FullColumnName(label, i->GetName()), i->GetItemType()));
+ }
+ return ctx.MakeType<TStructExprType>(structItems);
+}
+
+const TStructExprType* MakeStructMembersOptional(const TStructExprType& input, TExprContext& ctx) {
+ TVector<const TItemExprType*> structItems;
+ for (auto& i : input.GetItems()) {
+ if (i->GetItemType()->GetKind() == ETypeAnnotationKind::Optional) {
+ structItems.push_back(i);
+ } else {
+ structItems.push_back(ctx.MakeType<TItemExprType>(i->GetName(), ctx.MakeType<TOptionalExprType>(i->GetItemType())));
+ }
+ }
+ return ctx.MakeType<TStructExprType>(structItems);
+}
+
+EStarRewriteStatus RewriteYtEquiJoinStarSingleChain(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx) {
+
+ YQL_ENSURE(op.StarOptions.size() == 1);
+ const auto& starOption = op.StarOptions.front();
+
+ auto starLabel = starOption.StarLabel;
+ const auto& starKeys = starOption.StarKeys;
+ const auto& starSortedKeys = starOption.StarSortedKeys;
+ const auto starInputIndex = starOption.StarInputIndex;
+ const auto starPremap = TMaybeNode<TCoLambda>(equiJoin.Ref().ChildPtr(joinFixedArgsCount + starInputIndex));
+
+ auto starInputType = GetJoinInputType(equiJoin, starInputIndex, ctx);
+ if (!starInputType) {
+ return EStarRewriteStatus::Error;
+ }
+
+ THashMap<TString, TString> starRenames;
+ TSet<TString> renamedStarKeys;
+ TVector<TString> renamedStarSortedKeys;
+ {
+ size_t idx = 0;
+ for (auto& c : starOption.StarKeys) {
+ auto renamed = TStringBuilder() << "_yql_star_join_column_" << idx++;
+ starRenames[c] = renamed;
+ renamedStarKeys.insert(renamed);
+ }
+
+ for (auto& c : starOption.StarSortedKeys) {
+ if (!starKeys.contains(c)) {
+ break;
+ }
+ YQL_ENSURE(starRenames.contains(c));
+ renamedStarSortedKeys.push_back(starRenames[c]);
+ }
+ }
+
+ TYtJoinNodeOp* currOp = &op;
+
+ struct TStarJoinStep {
+ TStringBuf JoinKind;
+ TString Label;
+
+ size_t ReduceIndex = Max<size_t>();
+ TMaybeNode<TCoLambda> Premap;
+
+ const TStructExprType* InputType = nullptr;
+
+ TVector<TString> StarKeyList;
+ TVector<TString> KeyList;
+
+ THashMap<TString, TString> Renames;
+ };
+
+ TVector<TStarJoinStep> starChain;
+ TVector<TYtSection> reduceSections;
+ ETypeAnnotationKind commonPremapKind = ETypeAnnotationKind::Optional;
+ if (starPremap) {
+ commonPremapKind = DeriveCommonSequenceKind(commonPremapKind, starPremap.Cast().Ref().GetTypeAnn()->GetKind());
+ }
+
+ THashSet<TString> unusedKeys;
+ while (currOp) {
+ const TYtStarJoinOption* option = nullptr;
+ for (auto& opt : currOp->StarOptions) {
+ if (opt.StarLabel == starLabel) {
+ option = &opt;
+ }
+ }
+ YQL_ENSURE(option);
+ YQL_ENSURE(starLabel == option->StarLabel);
+ YQL_ENSURE(starKeys == option->StarKeys);
+ YQL_ENSURE(starInputIndex == option->StarInputIndex);
+
+ auto leftLeaf = dynamic_cast<TYtJoinNodeLeaf*>(currOp->Left.Get());
+ auto rightLeaf = dynamic_cast<TYtJoinNodeLeaf*>(currOp->Right.Get());
+ TYtJoinNodeLeaf* leaf = nullptr;
+ const TExprNode* leafLabelNode = nullptr;
+ const TExprNode* starLabelNode = nullptr;
+
+ TYtJoinNodeOp* nextOp = nullptr;
+ if (leftLeaf && rightLeaf) {
+ leaf = (option->StarInputIndex == leftLeaf->Index) ? rightLeaf : leftLeaf;
+ leafLabelNode = (option->StarInputIndex == leftLeaf->Index) ? currOp->RightLabel.Get() : currOp->LeftLabel.Get();
+ starLabelNode = (option->StarInputIndex == leftLeaf->Index) ? currOp->LeftLabel.Get() : currOp->RightLabel.Get();
+ } else {
+ leaf = leftLeaf ? leftLeaf : rightLeaf;
+ leafLabelNode = leftLeaf ? currOp->LeftLabel.Get() : currOp->RightLabel.Get();
+ starLabelNode = leftLeaf ? currOp->RightLabel.Get() : currOp->LeftLabel.Get();
+
+ nextOp = leftLeaf ? dynamic_cast<TYtJoinNodeOp*>(currOp->Right.Get()) :
+ dynamic_cast<TYtJoinNodeOp*>(currOp->Left.Get());
+ YQL_ENSURE(nextOp);
+ }
+ YQL_ENSURE(leaf);
+ YQL_ENSURE(leafLabelNode);
+ YQL_ENSURE(starLabelNode);
+
+ TStarJoinStep starJoinStep;
+ starJoinStep.JoinKind = currOp->JoinKind->Content();
+ starJoinStep.InputType = GetJoinInputType(equiJoin, leaf->Index, ctx);
+ if (!starJoinStep.InputType) {
+ return EStarRewriteStatus::Error;
+ }
+
+ YQL_ENSURE(ExtractJoinKeysForStarJoin(*leafLabelNode, starJoinStep.Label, starJoinStep.KeyList));
+ TString sl;
+ YQL_ENSURE(ExtractJoinKeysForStarJoin(*starLabelNode, sl, starJoinStep.StarKeyList));
+ YQL_ENSURE(sl == starLabel);
+ YQL_ENSURE(starJoinStep.KeyList.size() == starJoinStep.StarKeyList.size());
+
+ for (auto i : xrange(starJoinStep.KeyList.size())) {
+ auto starColumn = starJoinStep.StarKeyList[i];
+ auto otherColumn = starJoinStep.KeyList[i];
+
+ YQL_ENSURE(starRenames.contains(starColumn));
+ auto starRenamed = starRenames[starColumn];
+ YQL_ENSURE(starRenamed);
+
+ auto& renamed = starJoinStep.Renames[otherColumn];
+ YQL_ENSURE(!renamed || renamed == starRenamed);
+ renamed = starRenamed;
+ }
+
+ starJoinStep.ReduceIndex = leaf->Index; // will be updated
+ starJoinStep.Premap = TMaybeNode<TCoLambda>(equiJoin.Ref().ChildPtr(joinFixedArgsCount + leaf->Index));
+ if (starJoinStep.Premap) {
+ YQL_ENSURE(EnsureSeqOrOptionalType(starJoinStep.Premap.Cast().Ref(), ctx));
+ commonPremapKind = DeriveCommonSequenceKind(commonPremapKind, starJoinStep.Premap.Cast().Ref().GetTypeAnn()->GetKind());
+ }
+ starChain.emplace_back(std::move(starJoinStep));
+
+ unusedKeys.insert(currOp->OutputRemoveColumns.begin(), currOp->OutputRemoveColumns.end());
+
+ currOp = nextOp;
+ }
+
+ // we start doing joins from leafs
+ std::reverse(starChain.begin(), starChain.end());
+
+ const TStructExprType* chainOutputType = AddLabelToStructType(*starInputType, starLabel, ctx);
+ reduceSections.reserve(starChain.size() + 1);
+
+ TVector<size_t> innerIndexes;
+ TVector<size_t> semiIndexes;
+ TVector<size_t> onlyIndexes;
+ TVector<size_t> leftIndexes;
+ for (auto& item : starChain) {
+ auto section = equiJoin.Input().Item(item.ReduceIndex);
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Done();
+
+ section = SectionApplyRenames(section, item.Renames, ctx);
+
+ reduceSections.push_back(section);
+ item.ReduceIndex = reduceSections.size() - 1;
+
+ if (item.JoinKind == "Inner") {
+ innerIndexes.push_back(item.ReduceIndex);
+ } else if (item.JoinKind.EndsWith("Semi")) {
+ semiIndexes.push_back(item.ReduceIndex);
+ } else if (item.JoinKind.EndsWith("Only")) {
+ onlyIndexes.push_back(item.ReduceIndex);
+ } else if (item.JoinKind == "Left" || item.JoinKind == "Right") {
+ leftIndexes.push_back(item.ReduceIndex);
+ } else {
+ YQL_ENSURE(false, "Unexpected join type in Star JOIN");
+ }
+
+ if (item.JoinKind.EndsWith("Semi") || item.JoinKind.EndsWith("Only")) {
+ // output type remains the same as input
+ continue;
+ }
+
+ const TStructExprType* inputType = AddLabelToStructType(*item.InputType, item.Label, ctx);
+ if (item.JoinKind != "Inner") {
+ inputType = MakeStructMembersOptional(*inputType, ctx);
+ }
+
+ TVector<const TItemExprType*> outputTypeItems = chainOutputType->GetItems();
+ const auto& inputTypeItems = inputType->GetItems();
+ outputTypeItems.insert(outputTypeItems.end(), inputTypeItems.begin(), inputTypeItems.end());
+
+ chainOutputType = ctx.MakeType<TStructExprType>(outputTypeItems);
+ }
+
+ {
+ auto section = equiJoin.Input().Item(starInputIndex);
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::JoinLabel | EYtSettingType::StatColumns, ctx))
+ .Done();
+
+ section = SectionApplyRenames(section, starRenames, ctx);
+ reduceSections.push_back(section);
+ }
+
+ YQL_ENSURE(reduceSections.size() == starChain.size() + 1);
+ YQL_ENSURE(innerIndexes.size() + semiIndexes.size() + onlyIndexes.size() + leftIndexes.size() == starChain.size());
+
+ const auto pos = equiJoin.Pos();
+
+ const TStructExprType* outItemType = nullptr;
+ if (!op.Parent) {
+ if (auto type = GetSequenceItemType(pos,
+ equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1],
+ false, ctx))
+ {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return EStarRewriteStatus::Error;
+ }
+ } else {
+ TVector<const TItemExprType*> structItems = chainOutputType->GetItems();
+ EraseIf(structItems, [&](const TItemExprType* item) { return unusedKeys.contains(item->GetName()); });
+ outItemType = ctx.MakeType<TStructExprType>(structItems);
+ }
+
+ const TVariantExprType* inputVariant = nullptr;
+ {
+ TTypeAnnotationNode::TListType items;
+ items.reserve(reduceSections.size());
+ for (const auto& item: starChain) {
+ items.push_back(item.InputType);
+ }
+ items.push_back(starInputType);
+ inputVariant = ctx.MakeType<TVariantExprType>(ctx.MakeType<TTupleExprType>(items));
+ }
+
+ TYtOutTableInfo outTableInfo(outItemType, state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ // TODO: mark output sorted
+ Y_UNUSED(starSortedKeys);
+
+
+ YQL_CLOG(INFO, ProviderYt) << "Processing star join " << starLabel << ":[" << JoinSeq(",", starKeys) << "] of length " << starChain.size();
+ for (auto& item : starChain) {
+ YQL_CLOG(INFO, ProviderYt) << "Join: " << item.JoinKind << ", " << item.Label << ":[" << JoinSeq(",", item.KeyList)
+ << "] -> " << starLabel << ":[" << JoinSeq(",", item.StarKeyList) << "]";
+ }
+ YQL_CLOG(INFO, ProviderYt) << "StarJoin result type is " << *(const TTypeAnnotationNode*)chainOutputType;
+
+ TExprNode::TPtr groupArg = ctx.NewArgument(pos, "group");
+ TExprNode::TPtr nullFilteredRenamedAndPremappedStream = ctx.Builder(pos)
+ .Callable("FlatMap")
+ .Add(0, groupArg)
+ .Lambda(1)
+ .Param("item")
+ .Callable("Visit")
+ .Arg(0, "item")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < reduceSections.size(); ++i) {
+ bool isStarInput = (i == reduceSections.size() - 1);
+ TSet<TString> skipNullMemberColumns;
+ if (!isStarInput || innerIndexes) {
+ // skip null for all inputs except for star input when there are no inner joins
+ skipNullMemberColumns = renamedStarKeys;
+ }
+
+ auto premapLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Callable("Just")
+ .Arg(0, "item")
+ .Seal()
+ .Seal()
+ .Build();
+
+ ApplyInputPremap(premapLambda, isStarInput ? starPremap : starChain[i].Premap,
+ commonPremapKind, isStarInput ? starRenames : starChain[i].Renames, ctx);
+
+ parent
+ .Atom(2 * i + 1, ToString(i))
+ .Lambda(2 * i + 2)
+ .Param("unpackedVariant")
+ .Callable("FlatMap")
+ .Callable(0, "FlatMap")
+ .Callable(0, "SkipNullMembers")
+ .Callable(0, "Just")
+ .Arg(0, "unpackedVariant")
+ .Seal()
+ .Add(1, ToAtomList(skipNullMemberColumns, pos, ctx))
+ .Seal()
+ .Lambda(1)
+ .Param("filtered")
+ .Apply(premapLambda)
+ .With(0, "filtered")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Lambda(1)
+ .Param("filteredRenamedAndPremapped")
+ .Callable("Just")
+ .Callable(0, "Variant")
+ .Arg(0, "filteredRenamedAndPremapped")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Add(2, ExpandType(pos, *inputVariant, ctx))
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto buildOptionalInnersFromState = ctx.Builder(pos)
+ .Lambda()
+ .Param("state")
+ .Callable("TryRemoveAllOptionals")
+ .List(0)
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < innerIndexes.size(); ++i) {
+ parent
+ .Callable(i, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(innerIndexes[i]), TNodeFlags::Default)
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr buildResultRow = ctx.Builder(pos)
+ .Lambda()
+ .Param("starItem")
+ .Param("inners")
+ .Param("state")
+ .Callable("FlattenMembers")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 seqno = 0;
+ parent
+ .List(seqno++)
+ .Atom(0, starLabel + ".")
+ .Arg(1, "starItem")
+ .Seal();
+
+ for (ui32 i = 0; i < innerIndexes.size(); ++i) {
+ parent
+ .List(seqno++)
+ .Atom(0, starChain[innerIndexes[i]].Label + ".")
+ .Callable(1, "Nth")
+ .Arg(0, "inners")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Seal()
+ .Seal();
+ }
+
+ for (ui32 i = 0; i < leftIndexes.size(); ++i) {
+ parent
+ .List(seqno++)
+ .Atom(0, starChain[leftIndexes[i]].Label + ".")
+ .Callable(1, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(leftIndexes[i]), TNodeFlags::Default)
+ .Seal()
+ .Seal();
+ }
+
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr performStarChainLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("starItem")
+ .Param("state")
+ .Callable("FlatMap")
+ .Apply(0, buildOptionalInnersFromState)
+ .With(0, "state")
+ .Seal()
+ .Lambda(1)
+ .Param("inners")
+ .Callable("Filter")
+ .Callable(0, "Just")
+ .Apply(0, buildResultRow)
+ .With(0, "starItem")
+ .With(1, "inners")
+ .With(2, "state")
+ .Seal()
+ .Seal()
+ .Lambda(1)
+ .Param("unused")
+ .Callable("And")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 seqno = 0;
+ for (auto i : onlyIndexes) {
+ parent
+ .Callable(seqno++, "Not")
+ .Callable(0, "Exists")
+ .Callable(0, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal();
+ }
+
+ for (auto i : semiIndexes) {
+ parent
+ .Callable(seqno++, "Exists")
+ .Callable(0, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Seal()
+ .Seal();
+ }
+
+ if (seqno == 0) {
+ parent
+ .Callable(seqno, "Bool")
+ .Atom(0, "true", TNodeFlags::Default)
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto foldMapStateNode = ctx.Builder(pos)
+ .List()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < starChain.size(); ++i) {
+ auto joinKind = starChain[i].JoinKind;
+ bool isEmptyStructInState = joinKind.EndsWith("Semi") || joinKind.EndsWith("Only");
+ TVector<const TItemExprType*> noItems;
+ auto stateItemType = isEmptyStructInState ? ctx.MakeType<TStructExprType>(noItems) : starChain[i].InputType;
+ parent
+ .Callable(i, "Nothing")
+ .Add(0, ExpandType(pos, *ctx.MakeType<TOptionalExprType>(stateItemType), ctx))
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr emptyResultNode = ctx.Builder(pos)
+ .Callable("Nothing")
+ .Add(0, ExpandType(pos, *ctx.MakeType<TOptionalExprType>(chainOutputType), ctx))
+ .Seal()
+ .Build();
+
+ auto updateStateLambdaBuilder = [&](size_t inputIndex, size_t tupleSize, bool useEmptyStruct) {
+ YQL_ENSURE(inputIndex < tupleSize);
+ return ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .List()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < tupleSize; ++i) {
+ if (i != inputIndex) {
+ parent
+ .Callable(i, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Seal();
+ } else if (useEmptyStruct) {
+ parent
+ .Callable(i, "Just")
+ .Callable(0, "AsStruct")
+ .Seal()
+ .Seal();
+ } else {
+ parent
+ .Callable(i, "Just")
+ .Arg(0, "item")
+ .Seal();
+ }
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Build();
+ };
+
+ TExprNode::TPtr foldMapVisitLambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Callable("Visit")
+ .Arg(0, "item")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ for (ui32 i = 0; i < starChain.size(); ++i) {
+ auto joinKind = starChain[i].JoinKind;
+ bool isEmptyStructInState = joinKind.EndsWith("Semi") || joinKind.EndsWith("Only");
+ parent
+ .Atom(2 * i + 1, ToString(i), TNodeFlags::Default)
+ .Lambda(2 * i + 2)
+ .Param("var")
+ .List()
+ .Add(0, emptyResultNode)
+ .Callable(1, "If")
+ .Callable(0, "Exists")
+ .Callable(0, "Nth")
+ .Arg(0, "state")
+ .Atom(1, ToString(i), TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Arg(1, "state")
+ .Apply(2, updateStateLambdaBuilder(i, starChain.size(), isEmptyStructInState))
+ .With(0, "var")
+ .With(1, "state")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal();
+ }
+ return parent;
+ })
+ .Atom(2 * starChain.size() + 1, ToString(starChain.size()))
+ .Lambda(2 * starChain.size() + 2)
+ .Param("var")
+ .List()
+ .Apply(0, performStarChainLambda)
+ .With(0, "var")
+ .With(1, "state")
+ .Seal()
+ .Arg(1, "state")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr foldedStream = ctx.Builder(pos)
+ .Callable("FoldMap")
+ .Add(0, nullFilteredRenamedAndPremappedStream)
+ .Add(1, foldMapStateNode)
+ .Lambda(2)
+ .Param("item")
+ .Param("state")
+ .List()
+ .Callable(0, "Nth")
+ .Apply(0, foldMapVisitLambda)
+ .With(0, "item")
+ .With(1, "state")
+ .Seal()
+ .Atom(1, "0", TNodeFlags::Default)
+ .Seal()
+ .Callable(1, "Nth")
+ .Apply(0, foldMapVisitLambda)
+ .With(0, "item")
+ .With(1, "state")
+ .Seal()
+ .Atom(1, "1", TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TMap<TStringBuf, TVector<TStringBuf>> renameMap;
+ if (!op.Parent) {
+ renameMap = LoadJoinRenameMap(equiJoin.JoinOptions().Ref());
+ }
+ TExprNode::TPtr finalRenamingLambda = BuildJoinRenameLambda(pos, renameMap, *outItemType, ctx).Ptr();
+
+ TExprNode::TPtr finalRenamedStream = ctx.Builder(pos)
+ .Callable("FlatMap")
+ .Add(0, foldedStream)
+ .Lambda(1)
+ .Param("optItem")
+ .Callable("Map")
+ .Arg(0, "optItem")
+ .Lambda(1)
+ .Param("item")
+ .Apply(finalRenamingLambda)
+ .With(0, "item")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, pos)
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ReduceBy))
+ .Build()
+ .Value(ToAtomList(renamedStarSortedKeys, pos, ctx))
+ .Build()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinReduce))
+ .Build()
+ .Build();
+
+ if (state->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ const auto useSystemColumns = state->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS);
+ if (useSystemColumns) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeySwitch))
+ .Build()
+ .Build();
+ }
+
+ auto reduceOp = Build<TYtReduce>(ctx, pos)
+ .World(equiJoin.World())
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add(reduceSections)
+ .Build()
+ .Output()
+ .Add(outTableInfo.ToExprNode(ctx, pos).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Reducer(BuildYtReduceLambda(pos, groupArg, std::move(finalRenamedStream), useSystemColumns, ctx))
+ .Done();
+
+ op.Output = reduceOp;
+ return EStarRewriteStatus::Ok;
+}
+
+void CombineStarStatus(EStarRewriteStatus& status, EStarRewriteStatus other) {
+ YQL_ENSURE(status == EStarRewriteStatus::Error || status == EStarRewriteStatus::None ||
+ status == EStarRewriteStatus::Ok);
+ switch (other) {
+ case EStarRewriteStatus::Error:
+ status = other;
+ break;
+ case EStarRewriteStatus::Ok:
+ if (status != EStarRewriteStatus::Error) {
+ status = other;
+ }
+ break;
+ case EStarRewriteStatus::None:
+ break;
+ default:
+ YQL_ENSURE(false, "Unexpected star join status");
+ }
+}
+
+EStarRewriteStatus RewriteYtEquiJoinStarChains(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx) {
+ TYtJoinNodeLeaf* leftLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Left.Get());
+ TYtJoinNodeLeaf* rightLeaf = dynamic_cast<TYtJoinNodeLeaf*>(op.Right.Get());
+
+ EStarRewriteStatus result = EStarRewriteStatus::None;
+ if (op.StarOptions) {
+ if (leftLeaf && rightLeaf) {
+ // too trivial star join - let RewriteYtEquiJoinLeaves() to handle it
+ return result;
+ }
+ return RewriteYtEquiJoinStarSingleChain(equiJoin, op, state, ctx);
+ }
+
+ if (!leftLeaf) {
+ auto leftOp = dynamic_cast<TYtJoinNodeOp*>(op.Left.Get());
+ YQL_ENSURE(leftOp);
+ CombineStarStatus(result, RewriteYtEquiJoinStarChains(equiJoin, *leftOp, state, ctx));
+ if (result == EStarRewriteStatus::Error) {
+ return result;
+ }
+ if (result == EStarRewriteStatus::Ok && leftOp->Output) {
+ op.Left = ConvertYtEquiJoinToLeaf(*leftOp, equiJoin.Pos(), ctx);
+ }
+ }
+
+ if (!rightLeaf) {
+ auto rightOp = dynamic_cast<TYtJoinNodeOp*>(op.Right.Get());
+ YQL_ENSURE(rightOp);
+ CombineStarStatus(result, RewriteYtEquiJoinStarChains(equiJoin, *rightOp, state, ctx));
+ if (result == EStarRewriteStatus::Error) {
+ return result;
+ }
+ if (result == EStarRewriteStatus::Ok && rightOp->Output) {
+ op.Right = ConvertYtEquiJoinToLeaf(*rightOp, equiJoin.Pos(), ctx);
+ }
+ }
+
+ return result;
+}
+
+EStarRewriteStatus RewriteYtEquiJoinStar(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx) {
+ const bool enableStarJoin = state->Configuration->JoinEnableStarJoin.Get().GetOrElse(false);
+ if (!enableStarJoin) {
+ return EStarRewriteStatus::None;
+ }
+
+ const bool allowColumnRenames = state->Configuration->JoinAllowColumnRenames.Get().GetOrElse(true);
+ if (!allowColumnRenames) {
+ return EStarRewriteStatus::None;
+ }
+
+ EStarRewriteStatus collectStatus = EStarRewriteStatus::Ok;
+ CollectPossibleStarJoins(equiJoin, op, state, collectStatus, ctx);
+ if (collectStatus != EStarRewriteStatus::Ok) {
+ return collectStatus;
+ }
+
+ return RewriteYtEquiJoinStarChains(equiJoin, op, state, ctx);
+}
+
+} // namespace
+
+TYtJoinNodeOp::TPtr ImportYtEquiJoin(TYtEquiJoin equiJoin, TExprContext& ctx) {
+ TVector<TYtJoinNodeLeaf::TPtr> leaves;
+ TMaybeNode<TExprBase> world;
+ for (const size_t i: xrange(equiJoin.Input().Size())) {
+ auto leaf = MakeIntrusive<TYtJoinNodeLeaf>(equiJoin.Input().Item(i), TMaybeNode<TCoLambda>(equiJoin.Ref().ChildPtr(joinFixedArgsCount + i)));
+ leaf->Label = NYql::GetSetting(leaf->Section.Settings().Ref(), EYtSettingType::JoinLabel)->Child(1);
+ leaf->Index = i;
+ TConstraintNode::TPathReduce rename;
+ if (leaf->Label->IsAtom()) {
+ leaf->Scope.emplace_back(leaf->Label->Content());
+ rename = [&](const TConstraintNode::TPathType& path) -> std::vector<TConstraintNode::TPathType> {
+ auto result = path;
+ TStringBuilder sb;
+ sb << leaf->Scope.front() << '.' << result.front();
+ result.front() = ctx.AppendString(sb);
+ return {result};
+ };
+ } else {
+ for (const auto& x : leaf->Label->Children()) {
+ leaf->Scope.emplace_back(x->Content());
+ }
+ }
+
+ const auto sourceConstraints = leaf->Section.Ref().GetConstraintSet();
+ if (const auto distinct = sourceConstraints.GetConstraint<TDistinctConstraintNode>())
+ if (const auto complete = leaf->Premap ? TPartOfDistinctConstraintNode::MakeComplete(ctx, leaf->Premap.Cast().Body().Ref().GetConstraint<TPartOfDistinctConstraintNode>(), distinct) : distinct)
+ if (const auto d = complete->RenameFields(ctx, rename))
+ leaf->Constraints.AddConstraint(d);
+ if (const auto unique = sourceConstraints.GetConstraint<TUniqueConstraintNode>())
+ if (const auto complete = leaf->Premap ? TPartOfUniqueConstraintNode::MakeComplete(ctx, leaf->Premap.Cast().Body().Ref().GetConstraint<TPartOfUniqueConstraintNode>(), unique) : unique)
+ if (const auto u = complete->RenameFields(ctx, rename))
+ leaf->Constraints.AddConstraint(u);
+ if (const auto empty = sourceConstraints.GetConstraint<TEmptyConstraintNode>())
+ leaf->Constraints.AddConstraint(empty);
+
+ leaves.push_back(std::move(leaf));
+ }
+
+ const auto& renameMap = LoadJoinRenameMap(equiJoin.JoinOptions().Ref());
+ THashSet<TString> drops;
+ for (const auto& [column, renames] : renameMap) {
+ if (renames.empty()) {
+ drops.insert(ToString(column));
+ }
+ }
+
+ const auto root = ImportYtEquiJoinRecursive(leaves, nullptr, drops, equiJoin.Joins().Ref(), ctx);
+
+ if (const auto renames = LoadJoinRenameMap(equiJoin.JoinOptions().Ref()); !renames.empty()) {
+ const auto rename = [&renames](const TConstraintNode::TPathType& path) -> std::vector<TConstraintNode::TPathType> {
+ if (path.empty())
+ return {};
+
+ const auto it = renames.find(path.front());
+ if (renames.cend() == it)
+ return {path};
+ if (it->second.empty())
+ return {};
+
+ std::vector<TConstraintNode::TPathType> res(it->second.size());
+ std::transform(it->second.cbegin(), it->second.cend(), res.begin(), [&path](const std::string_view& newName) {
+ auto newPath = path;
+ newPath.front() = newName;
+ return newPath;
+ });
+ return res;
+ };
+
+ TConstraintSet set;
+ if (const auto unique = root->Constraints.GetConstraint<TUniqueConstraintNode>())
+ if (const auto u = unique->RenameFields(ctx, rename))
+ set.AddConstraint(u);
+ if (const auto distinct = root->Constraints.GetConstraint<TDistinctConstraintNode>())
+ if (const auto d = distinct->RenameFields(ctx, rename))
+ set.AddConstraint(d);
+ if (const auto empty = root->Constraints.GetConstraint<TEmptyConstraintNode>())
+ set.AddConstraint(empty);
+ root->Constraints = set;
+ }
+
+ return root;
+}
+
+IGraphTransformer::TStatus RewriteYtEquiJoin(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx) {
+ switch (RewriteYtEquiJoinStar(equiJoin, op, state, ctx)) {
+ case EStarRewriteStatus::Error:
+ return IGraphTransformer::TStatus::Error;
+ case EStarRewriteStatus::Ok:
+ return IGraphTransformer::TStatus::Ok;
+ case EStarRewriteStatus::WaitInput:
+ return IGraphTransformer::TStatus::Repeat;
+ case EStarRewriteStatus::None:
+ break;
+ }
+
+ return RewriteYtEquiJoinLeaves(equiJoin, op, state, ctx);
+}
+
+TMaybeNode<TExprBase> ExportYtEquiJoin(TYtEquiJoin equiJoin, const TYtJoinNodeOp& op, TExprContext& ctx,
+ const TYtState::TPtr& state) {
+ if (op.Output) {
+ return op.Output.Cast();
+ }
+
+ TVector<TYtSection> sections;
+ TVector<TExprBase> premaps;
+ THashSet<TString> outputRemoveColumns;
+ auto joinTree = ExportJoinTree(op, equiJoin.Pos(), sections, premaps, outputRemoveColumns, ctx);
+
+ TExprNode::TPtr joinSettings = equiJoin.JoinOptions().Ptr();
+ if (outputRemoveColumns) {
+ auto renameMap = LoadJoinRenameMap(*joinSettings);
+ for (auto& c : outputRemoveColumns) {
+ YQL_ENSURE(renameMap[c].empty(), "Rename map contains non-trivial renames for column " << c);
+ }
+
+ joinSettings = RemoveSetting(*joinSettings, "rename", ctx);
+ TExprNode::TListType joinSettingsNodes = joinSettings->ChildrenList();
+
+ AppendEquiJoinRenameMap(joinSettings->Pos(), renameMap, joinSettingsNodes, ctx);
+ joinSettings = ctx.ChangeChildren(*joinSettings, std::move(joinSettingsNodes));
+ }
+
+ auto outItemType = GetSequenceItemType(equiJoin.Pos(),
+ equiJoin.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1],
+ false, ctx);
+ if (!outItemType) {
+ return {};
+ }
+
+ const auto join = Build<TYtEquiJoin>(ctx, equiJoin.Pos())
+ .World(equiJoin.World())
+ .DataSink(equiJoin.DataSink())
+ .Input()
+ .Add(sections)
+ .Build()
+ .Output()
+ .Add(TYtOutTableInfo(outItemType->Cast<TStructExprType>(),
+ state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE).ToExprNode(ctx, equiJoin.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Build()
+ .Joins(joinTree)
+ .JoinOptions(joinSettings)
+ .Done();
+ auto children = join.Ref().ChildrenList();
+ children.reserve(children.size() + premaps.size());
+ std::transform(premaps.cbegin(), premaps.cend(), std::back_inserter(children), std::bind(&TExprBase::Ptr, std::placeholders::_1));
+ return TExprBase(ctx.ChangeChildren(join.Ref(), std::move(children)));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h
new file mode 100644
index 0000000000..4d4f1e9d81
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_join_impl.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <ydb/library/yql/core/yql_join.h>
+
+#include <ydb/library/yql/providers/yt/provider/yql_yt_provider.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+#include <util/generic/ylimits.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+struct TYtJoinNodeOp;
+
+struct TYtJoinNode: public TRefCounted<TYtJoinNode, TSimpleCounter> {
+ using TPtr = TIntrusivePtr<TYtJoinNode>;
+ virtual ~TYtJoinNode() = default;
+
+ TVector<TString> Scope;
+ TConstraintSet Constraints;
+};
+
+struct TYtJoinNodeLeaf : TYtJoinNode {
+ using TPtr = TIntrusivePtr<TYtJoinNodeLeaf>;
+
+ TYtJoinNodeLeaf(TYtSection section, TMaybeNode<TCoLambda> premap)
+ : Section(section)
+ , Premap(premap)
+ {
+ }
+
+ TYtSection Section;
+ TMaybeNode<TCoLambda> Premap;
+ TExprNode::TPtr Label;
+ size_t Index = Max<size_t>();
+};
+
+struct TYtStarJoinOption {
+ TSet<TString> StarKeys;
+ TVector<TString> StarSortedKeys;
+ size_t StarInputIndex = Max<size_t>();
+ TString StarLabel;
+};
+
+struct TYtJoinNodeOp : TYtJoinNode {
+ using TPtr = TIntrusivePtr<TYtJoinNodeOp>;
+
+ TYtJoinNode::TPtr Left;
+ TYtJoinNode::TPtr Right;
+ TExprNode::TPtr JoinKind;
+ TExprNode::TPtr LeftLabel;
+ TExprNode::TPtr RightLabel;
+ TEquiJoinLinkSettings LinkSettings;
+ const TYtJoinNodeOp* Parent = nullptr;
+ TVector<TYtStarJoinOption> StarOptions;
+ TMaybeNode<TYtOutputOpBase> Output;
+ THashSet<TString> OutputRemoveColumns;
+};
+
+TYtJoinNodeOp::TPtr ImportYtEquiJoin(TYtEquiJoin equiJoin, TExprContext& ctx);
+IGraphTransformer::TStatus RewriteYtEquiJoin(TYtEquiJoin equiJoin, TYtJoinNodeOp& op, const TYtState::TPtr& state, TExprContext& ctx);
+TMaybeNode<TExprBase> ExportYtEquiJoin(TYtEquiJoin equiJoin, const TYtJoinNodeOp& op, TExprContext& ctx, const TYtState::TPtr& state);
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
new file mode 100644
index 0000000000..fd9cd4c452
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_key.cpp
@@ -0,0 +1,246 @@
+#include "yql_yt_key.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/split.h>
+#include <util/generic/strbuf.h>
+
+namespace NYql {
+
+namespace {
+
+THashSet<TStringBuf> EXT_KEY_CALLABLES = {
+ TStringBuf("Key"),
+ TStringBuf("TempTable"),
+ MrFolderName,
+};
+
+THashSet<TStringBuf> KEY_CALLABLES = {
+ TStringBuf("Key"),
+ TStringBuf("TempTable"),
+};
+
+}
+
+bool TYtKey::Parse(const TExprNode& key, TExprContext& ctx) {
+ if (!key.IsCallable(EXT_KEY_CALLABLES)) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), TStringBuf("Expected key")));
+ return false;
+ }
+
+ KeyNode = &key;
+ if (key.IsCallable(TStringBuf("TempTable"))) {
+ if (key.ChildrenSize() != 1) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), TStringBuf("TempTable must have a label")));
+ return false;
+ }
+
+ if (!EnsureAtom(*key.Child(0), ctx)) {
+ return false;
+ }
+ Type = EType::Table;
+ Anonymous = true;
+ Path = key.Child(0)->Content();
+ return true;
+ }
+ else if (key.IsCallable(MrFolderName)) {
+ if (key.ChildrenSize() != 2 || !key.Child(0)->IsAtom() || !key.Child(1)->IsAtom()) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), TStringBuilder() << "Incorrect format of " << MrFolderName));
+ return false;
+ }
+ Folder.ConstructInPlace();
+ Folder->Prefix = key.Child(0)->Content();
+ Split(TString(key.Child(1)->Content()), ";", Folder->Attributes);
+ Type = EType::Folder;
+ return true;
+ }
+
+ if (key.ChildrenSize() < 1) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), TStringBuf("Key must have at least one component - table or tablescheme, "
+ " and may have second tag - view")));
+ return false;
+ }
+
+ auto tagName = key.Child(0)->Child(0)->Content();
+ if (tagName == TStringBuf("table")) {
+ Type = EType::Table;
+ if (key.ChildrenSize() > 3) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), "Too many tags"));
+ return false;
+ }
+ } else if (tagName == TStringBuf("tablescheme")) {
+ Type = EType::TableScheme;
+ if (key.ChildrenSize() > 3) {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), "Too many tags"));
+ return false;
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Child(0)->Pos()), TString("Unexpected tag: ") + tagName));
+ return false;
+ }
+
+ const TExprNode* nameNode = key.Child(0)->Child(1);
+ if (nameNode->IsCallable("String")) {
+ if (!EnsureArgsCount(*nameNode, 1, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*nameNode->Child(0), ctx)) {
+ return false;
+ }
+
+ const TExprNode* tableName = nameNode->Child(0);
+
+ if (tableName->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(tableName->Pos()), "Table name must not be empty"));
+ return false;
+ }
+ Path = tableName->Content();
+ }
+ else if (nameNode->IsCallable(MrTableRangeName) || nameNode->IsCallable(MrTableRangeStrictName)) {
+ if (EType::TableScheme == Type) {
+ ctx.AddError(TIssue(ctx.GetPosition(nameNode->Pos()), "MrTableRange[Strict] must not be used with tablescheme tag"));
+ return false;
+ }
+
+ if (!EnsureMinArgsCount(*nameNode, 1, ctx)) {
+ return false;
+ }
+
+ if (!EnsureMaxArgsCount(*nameNode, 3, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*nameNode->Child(0), ctx)) {
+ return false;
+ }
+
+ if (nameNode->ChildrenSize() > 2){
+ if (!EnsureAtom(*nameNode->Child(2), ctx)) {
+ return false;
+ }
+ }
+
+ Range.ConstructInPlace();
+ Range->Prefix = nameNode->Child(0)->Content();
+ if (nameNode->ChildrenSize() > 1) {
+ Range->Filter = nameNode->Child(1);
+ }
+ if (nameNode->ChildrenSize() > 2) {
+ Range->Suffix = nameNode->Child(2)->Content();
+ }
+ Range->IsStrict = nameNode->Content() == MrTableRangeStrictName;
+
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(key.Pos()), "Expected String or MrTableRange[Strict]"));
+ }
+
+ if (key.ChildrenSize() > 1) {
+ for (ui32 i = 1; i < key.ChildrenSize(); ++i) {
+ auto tag = key.Child(i)->Child(0);
+ if (tag->Content() == TStringBuf("view")) {
+ const TExprNode* viewNode = key.Child(i)->Child(1);
+ if (!viewNode->IsCallable("String")) {
+ ctx.AddError(TIssue(ctx.GetPosition(viewNode->Pos()), "Expected String"));
+ return false;
+ }
+
+ if (viewNode->ChildrenSize() != 1 || !EnsureAtom(*viewNode->Child(0), ctx)) {
+ ctx.AddError(TIssue(ctx.GetPosition(viewNode->Child(0)->Pos()), "Dynamic views names are not supported"));
+ return false;
+ }
+
+ View = viewNode->Child(0)->Content();
+ if (View.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(viewNode->Child(0)->Pos()), "View name must not be empty"));
+ return false;
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(tag->Pos()), TStringBuilder() << "Unexpected tag: " << tag->Content()));
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool TYtInputKeys::Parse(const TExprNode& keysNode, TExprContext& ctx) {
+ bool hasNonKeys = false;
+ TVector<const TExprNode*> keys;
+ if (keysNode.Type() == TExprNode::List) {
+ keysNode.ForEachChild([&](const TExprNode& k) {
+ if (k.IsCallable(KEY_CALLABLES)) {
+ keys.push_back(&k);
+ } else {
+ hasNonKeys = true;
+ }
+ });
+ } else if (keysNode.IsCallable(MrTableConcatName)) {
+ keysNode.ForEachChild([&](const TExprNode& k){
+ keys.push_back(&k);
+ });
+ StrictConcat = false;
+ } else {
+ if (keysNode.IsCallable(EXT_KEY_CALLABLES)) {
+ keys.push_back(&keysNode);
+ } else {
+ hasNonKeys = true;
+ }
+ }
+ if (hasNonKeys) {
+ if (!keys.empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keysNode.Pos()), "Mixed keys"));
+ return false;
+ }
+ HasNonKeys = true;
+ return true;
+ }
+ for (auto k: keys) {
+ Keys.emplace_back();
+ if (!Keys.back().Parse(*k, ctx)) {
+ return false;
+ }
+ if (TYtKey::EType::Undefined == Type) {
+ Type = Keys.back().GetType();
+ } else if (Type != Keys.back().GetType()) {
+ ctx.AddError(TIssue(ctx.GetPosition(k->Child(0)->Pos()), "Multiple keys must have the same tag"));
+ return false;
+ }
+ }
+ if (TYtKey::EType::Table != Type && Keys.size() > 1) {
+ ctx.AddError(TIssue(ctx.GetPosition(keysNode.Pos()), "Expected single key"));
+ return false;
+ }
+ return true;
+}
+
+bool TYtOutputKey::Parse(const TExprNode& keyNode, TExprContext& ctx) {
+ if (!keyNode.IsCallable(KEY_CALLABLES)) {
+ return true;
+ }
+ if (!TYtKey::Parse(keyNode, ctx)) {
+ return false;
+ }
+
+ if (GetType() != TYtKey::EType::Table) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyNode.Child(0)->Child(0)->Pos()),
+ TStringBuilder() << "Unexpected tag: " << keyNode.Child(0)->Child(0)->Content()));
+ return false;
+ }
+
+ if (!GetView().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyNode.Pos()), "Unexpected view for output key"));
+ return false;
+ }
+
+ if (GetRange().Defined()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyNode.Pos()), "Unexpected MrTableRange[Strict] for output key"));
+ return false;
+ }
+ return true;
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_key.h b/ydb/library/yql/providers/yt/provider/yql_yt_key.h
new file mode 100644
index 0000000000..14c17479ad
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_key.h
@@ -0,0 +1,157 @@
+#pragma once
+
+#include <ydb/library/yql/utils/yql_panic.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/maybe.h>
+#include <util/generic/vector.h>
+#include <util/digest/numeric.h>
+#include <util/str_stl.h>
+
+namespace NYql {
+
+class TYtKey {
+public:
+ enum class EType {
+ Undefined,
+ Table,
+ TableScheme,
+ Folder,
+ };
+
+ struct TRange {
+ TString Prefix;
+ TExprNode::TPtr Filter;
+ TString Suffix;
+ bool IsStrict = true;
+
+ friend bool operator ==(const TRange& left, const TRange& right) {
+ return left.Prefix == right.Prefix
+ && left.Filter == right.Filter
+ && left.Suffix == right.Suffix
+ && left.IsStrict == right.IsStrict;
+ }
+ };
+
+ struct TFolderList {
+ TString Prefix;
+ TVector<TString> Attributes;
+
+ friend bool operator ==(const TFolderList& left, const TFolderList& right) {
+ return left.Prefix == right.Prefix
+ && left.Attributes == right.Attributes;
+ }
+ };
+
+public:
+ TYtKey() {
+ }
+
+ EType GetType() const {
+ return Type;
+ }
+
+ const TExprNode* GetNode() const {
+ return KeyNode;
+ }
+
+ const TString& GetPath() const {
+ YQL_ENSURE(Type != EType::Undefined);
+ return Path;
+ }
+
+ const TString& GetView() const {
+ YQL_ENSURE(Type != EType::Undefined);
+ return View;
+ }
+
+ bool IsAnonymous() const {
+ return Anonymous;
+ }
+
+ const TMaybe<TRange>& GetRange() const {
+ YQL_ENSURE(Type != EType::Undefined);
+ return Range;
+ }
+
+ const TMaybe<TFolderList>& GetFolder() const {
+ YQL_ENSURE(Type != EType::Undefined);
+ return Folder;
+ }
+
+ bool Parse(const TExprNode& key, TExprContext& ctx);
+
+private:
+ EType Type = EType::Undefined;
+ const TExprNode* KeyNode = nullptr;
+ TString Path;
+ TString View;
+ bool Anonymous = false;
+ TMaybe<TRange> Range;
+ TMaybe<TFolderList> Folder;
+};
+
+class TYtInputKeys {
+public:
+ TYtInputKeys() {
+ }
+
+ TYtKey::EType GetType() const {
+ return Type;
+ }
+
+ const TVector<TYtKey>& GetKeys() const {
+ return Keys;
+ }
+
+ bool IsProcessed() const {
+ return HasNonKeys;
+ }
+
+ bool GetStrictConcat() const {
+ return StrictConcat;
+ }
+
+ bool Parse(const TExprNode& keysNode, TExprContext& ctx);
+
+private:
+ TYtKey::EType Type = TYtKey::EType::Undefined;
+ TVector<TYtKey> Keys;
+ bool StrictConcat = true;
+ bool HasNonKeys = false;
+};
+
+class TYtOutputKey: public TYtKey {
+public:
+ TYtOutputKey() {
+ }
+
+ bool Parse(const TExprNode& keyNode, TExprContext& ctx);
+};
+
+}
+
+template <>
+struct hash<NYql::TYtKey::TRange> {
+ inline size_t operator()(const NYql::TYtKey::TRange& r) const {
+ size_t res = ComputeHash(r.Prefix);
+ res = CombineHashes(res, NumericHash(r.Filter.Get()));
+ res = CombineHashes(res, ComputeHash(r.Suffix));
+ res = CombineHashes(res, size_t(r.IsStrict));
+ return res;
+ }
+};
+
+template <>
+struct hash<NYql::TYtKey::TFolderList> {
+ inline size_t operator()(const NYql::TYtKey::TFolderList& r) const {
+ size_t res = ComputeHash(r.Prefix);
+ res = CombineHashes(res, r.Attributes.size());
+ for (auto& a: r.Attributes) {
+ res = CombineHashes(res, ComputeHash(a));
+ }
+ return res;
+ }
+};
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
new file mode 100644
index 0000000000..c7837d8e83
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_load_columnar_stats.cpp
@@ -0,0 +1,230 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_gateway.h"
+#include "yql_yt_helpers.h"
+
+#include <library/cpp/yson/node/node_io.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+
+#include <library/cpp/threading/future/async.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+
+namespace NYql {
+
+namespace {
+
+using namespace NNodes;
+
+class TPathStatusState : public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<TPathStatusState>;
+
+ void EnsureNoInflightRequests() const {
+ TGuard<TMutex> guard(Lock);
+ YQL_ENSURE(PathStatResults.empty());
+ }
+
+ TNodeMap<IYtGateway::TPathStatResult> PullPathStatResults() {
+ TNodeMap<IYtGateway::TPathStatResult> results;
+ TGuard<TMutex> guard(Lock);
+ results.swap(PathStatResults);
+ return results;
+ }
+
+ void MarkReady(TExprNode* node, const IYtGateway::TPathStatResult& result) {
+ TGuard<TMutex> guard(Lock);
+ YQL_ENSURE(PathStatResults.count(node) == 0);
+ PathStatResults[node] = result;
+ }
+
+private:
+ mutable TMutex Lock;
+ TNodeMap<IYtGateway::TPathStatResult> PathStatResults;
+};
+
+class TYtLoadColumnarStatsTransformer : public TGraphTransformerBase {
+public:
+ TYtLoadColumnarStatsTransformer(TYtState::TPtr state)
+ : State_(state)
+ , PathStatusState(new TPathStatusState)
+ {
+ }
+
+ void Rewind() final {
+ PathStatusState = MakeIntrusive<TPathStatusState>();
+ AsyncFuture = {};
+ }
+
+private:
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) override {
+ output = input;
+
+ PathStatusState->EnsureNoInflightRequests();
+ TVector<std::pair<IYtGateway::TPathStatOptions, TExprNode*>> pathStatArgs;
+ bool hasError = false;
+ TNodeOnNodeOwnedMap sectionRewrites;
+ VisitExpr(input, [this, &pathStatArgs, &hasError, &sectionRewrites, &ctx](const TExprNode::TPtr& node) {
+ if (auto maybeSection = TMaybeNode<TYtSection>(node)) {
+ TYtSection section = maybeSection.Cast();
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::StatColumns)) {
+ auto columnList = NYql::GetSettingAsColumnList(section.Settings().Ref(), EYtSettingType::StatColumns);
+
+ TMaybe<TString> cluster;
+ TVector<IYtGateway::TPathStatReq> pathStatReqs;
+ size_t idx = 0;
+ for (auto path: section.Paths()) {
+ bool hasStat = false;
+ if (path.Table().Maybe<TYtTable>().Stat().Maybe<TYtStat>()) {
+ hasStat = true;
+ } else if (path.Table().Maybe<TYtOutTable>().Stat().Maybe<TYtStat>()) {
+ hasStat = true;
+ } else if (auto ytOutput = path.Table().Maybe<TYtOutput>()) {
+ auto outTable = GetOutTable(ytOutput.Cast());
+ if (outTable.Maybe<TYtOutTable>().Stat().Maybe<TYtStat>()) {
+ hasStat = true;
+ }
+ }
+
+ if (!hasStat) {
+ YQL_CLOG(INFO, ProviderYt) << "Removing columnar stat from YtSection #" << section.Ref().UniqueId()
+ << " due to missing stats in path #" << idx;
+
+ sectionRewrites[section.Raw()] = Build<TYtSection>(ctx, section.Ref().Pos())
+ .InitFrom(section)
+ .Settings(NYql::RemoveSetting(section.Settings().Ref(), EYtSettingType::StatColumns, ctx))
+ .Done().Ptr();
+ }
+
+ if (!sectionRewrites.empty()) {
+ // no need to prepare columnar stat requests in this case
+ return !hasError;
+ }
+
+ TYtPathInfo pathInfo(path);
+ YQL_ENSURE(pathInfo.Table->Stat);
+
+ TString currCluster;
+ if (auto ytTable = path.Table().Maybe<TYtTable>()) {
+ currCluster = TString{ytTable.Cast().Cluster().Value()};
+ } else {
+ currCluster = TString{GetOutputOp(path.Table().Cast<TYtOutput>()).DataSink().Cluster().Value()};
+ }
+ YQL_ENSURE(currCluster);
+
+ if (cluster) {
+ YQL_ENSURE(currCluster == *cluster);
+ } else {
+ cluster = currCluster;
+ }
+
+ auto ytPath = BuildYtPathForStatRequest(*cluster, pathInfo, columnList, *State_, ctx);
+ if (!ytPath) {
+ hasError = true;
+ return false;
+ }
+
+ pathStatReqs.push_back(
+ IYtGateway::TPathStatReq()
+ .Path(*ytPath)
+ .IsTemp(pathInfo.Table->IsTemp)
+ .IsAnonymous(pathInfo.Table->IsAnonymous)
+ .Epoch(pathInfo.Table->Epoch.GetOrElse(0))
+ );
+
+ ++idx;
+ }
+
+ if (pathStatReqs) {
+ auto pathStatOptions = IYtGateway::TPathStatOptions(State_->SessionId)
+ .Cluster(*cluster)
+ .Paths(pathStatReqs)
+ .Config(State_->Configuration->Snapshot());
+
+ auto tryResult = State_->Gateway->TryPathStat(IYtGateway::TPathStatOptions(pathStatOptions));
+ if (!tryResult.Success()) {
+ pathStatArgs.emplace_back(std::move(pathStatOptions), node.Get());
+ }
+ }
+ }
+ }
+ return !hasError;
+ });
+
+ if (hasError) {
+ return TStatus::Error;
+ }
+
+ if (!sectionRewrites.empty()) {
+ auto status = RemapExpr(input, output, sectionRewrites, ctx, TOptimizeExprSettings(State_->Types));
+ YQL_ENSURE(status.Level != TStatus::Ok);
+ return status;
+ }
+
+ if (pathStatArgs.empty()) {
+ return TStatus::Ok;
+ }
+
+ TVector<NThreading::TFuture<void>> futures;
+ YQL_CLOG(INFO, ProviderYt) << "Starting " << pathStatArgs.size() << " requests for columnar stats";
+ for (auto& arg : pathStatArgs) {
+ IYtGateway::TPathStatOptions& options = arg.first;
+ TExprNode* node = arg.second;
+
+ auto future = State_->Gateway->PathStat(std::move(options));
+
+ futures.push_back(future.Apply([pathStatusState = PathStatusState, node](const NThreading::TFuture<IYtGateway::TPathStatResult>& result) {
+ pathStatusState->MarkReady(node, result.GetValueSync());
+ }));
+ }
+
+ AsyncFuture = WaitExceptionOrAll(futures);
+ return TStatus::Async;
+ }
+
+ NThreading::TFuture<void> DoGetAsyncFuture(const TExprNode& input) override {
+ Y_UNUSED(input);
+ return AsyncFuture;
+ }
+
+ TStatus DoApplyAsyncChanges(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) override {
+ output = input;
+
+ TNodeMap<IYtGateway::TPathStatResult> results = PathStatusState->PullPathStatResults();
+ YQL_ENSURE(!results.empty());
+
+ for (auto& item : results) {
+ auto& node = item.first;
+ auto& result = item.second;
+ if (!result.Success()) {
+ TIssueScopeGuard issueScope(ctx.IssueManager, [&]() {
+ return MakeIntrusive<TIssue>(
+ ctx.GetPosition(node->Pos()),
+ TStringBuilder() << "Execution of node: " << node->Content()
+ );
+ });
+ result.ReportIssues(ctx.IssueManager);
+ return TStatus::Error;
+ }
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "Applied " << results.size() << " results of columnar stats";
+ return TStatus::Repeat;
+ }
+
+ TYtState::TPtr State_;
+ TPathStatusState::TPtr PathStatusState;
+ NThreading::TFuture<void> AsyncFuture;
+};
+
+} // namespace
+
+THolder<IGraphTransformer> CreateYtLoadColumnarStatsTransformer(TYtState::TPtr state) {
+ return THolder(new TYtLoadColumnarStatsTransformer(state));
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
new file mode 100644
index 0000000000..843fbd9950
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_load_table_meta.cpp
@@ -0,0 +1,298 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_gateway.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+
+#include <library/cpp/threading/future/async.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/algorithm.h>
+#include <util/string/join.h>
+
+#include <utility>
+
+namespace NYql {
+
+namespace {
+
+using namespace NNodes;
+
+class TYtLoadTableMetadataTransformer : public TGraphTransformerBase {
+private:
+ struct TLoadContext: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TLoadContext>;
+
+ ui32 Epoch = 0;
+ IYtGateway::TTableInfoResult Result;
+ TVector<std::pair<std::pair<TString, TString>, TYtTableDescription*>> Tables;
+ };
+public:
+ TYtLoadTableMetadataTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+ }
+
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+
+ if (ctx.Step.IsDone(TExprStep::LoadTablesMetadata)) {
+ return TStatus::Ok;
+ }
+
+ ui32 loadEpoch = 0;
+ size_t settingsVer = 0;
+ if (State_->LoadEpochMetadata.Defined()) {
+ std::tie(loadEpoch, settingsVer) = *State_->LoadEpochMetadata;
+ State_->LoadEpochMetadata.Clear();
+ }
+
+ LoadCtx = MakeIntrusive<TLoadContext>();
+ LoadCtx->Epoch = loadEpoch;
+ IYtGateway::TGetTableInfoOptions opts{State_->SessionId};
+
+ if (0 != loadEpoch) {
+ auto p = State_->EpochDependencies.FindPtr(loadEpoch);
+ YQL_ENSURE(p && !p->empty());
+ for (auto& clusterAndTable: *p) {
+ TYtTableDescription& tableDesc = State_->TablesData->GetModifTable(clusterAndTable.first, clusterAndTable.second, loadEpoch);
+ TString tableName = clusterAndTable.second;
+ if (tableDesc.IsAnonymous) {
+ tableName = State_->AnonymousLabels.Value(std::make_pair(clusterAndTable.first, tableName), TString());
+ YQL_ENSURE(tableName, "Unaccounted anonymous table: " << clusterAndTable.first << '.' << clusterAndTable.second);
+ }
+ opts.Tables().push_back(IYtGateway::TTableReq()
+ .Cluster(clusterAndTable.first)
+ .Table(tableName)
+ .Intents(tableDesc.Intents)
+ .InferSchemaRows(0)
+ .ForceInferSchema(false)
+ .Anonymous(tableDesc.IsAnonymous)
+ .IgnoreYamrDsv(State_->Configuration->IgnoreYamrDsv.Get().GetOrElse(false))
+ .IgnoreWeakSchema(State_->Configuration->IgnoreWeakSchema.Get().GetOrElse(false))
+ );
+ LoadCtx->Tables.emplace_back(clusterAndTable, &tableDesc);
+ }
+ } else {
+ for (auto& clusterAndTable: State_->TablesData->GetAllZeroEpochTables()) {
+ TYtTableDescription& tableDesc = State_->TablesData->GetModifTable(clusterAndTable.first, clusterAndTable.second, loadEpoch);
+ if (tableDesc.Meta) {
+ if (!tableDesc.HasWriteLock && HasModifyIntents(tableDesc.Intents)) {
+ TStringBuilder msg;
+ msg << "Table " << clusterAndTable.second.Quote()
+ << " is locked exclusively after taking snapshot lock."
+ << " This may lead query failure if YQL detected concurrent table modification before taking exclusive lock."
+ << " Add WITH XLOCK hint to all reads from this table to resolve this warning";
+ if (!ctx.AddWarning(YqlIssue(TPosition(), EYqlIssueCode::TIssuesIds_EIssueCode_YT_LATE_TABLE_XLOCK, msg))) {
+ return TStatus::Error;
+ }
+ }
+
+ // skip if table already has loaded metadata and has only read intents
+ if (State_->Types->IsReadOnly || State_->Types->UseTableMetaFromGraph || tableDesc.HasWriteLock || !HasModifyIntents(tableDesc.Intents)) {
+ // Intents/views can be updated since evaluation phase
+ if (!tableDesc.FillViews(clusterAndTable.first, clusterAndTable.second, ctx, State_->Types->Modules.get())) {
+ return TStatus::Error;
+ }
+ continue;
+ }
+ }
+ TString tableName = clusterAndTable.second;
+ if (tableDesc.IsAnonymous) {
+ tableName = State_->AnonymousLabels.Value(std::make_pair(clusterAndTable.first, tableName), TString());
+ YQL_ENSURE(tableName, "Unaccounted anonymous table: " << clusterAndTable.first << '.' << clusterAndTable.second);
+ }
+ opts.Tables().push_back(IYtGateway::TTableReq()
+ .Cluster(clusterAndTable.first)
+ .Table(tableName)
+ .LockOnly(bool(tableDesc.Meta))
+ .Intents(tableDesc.Intents)
+ .InferSchemaRows(tableDesc.InferSchemaRows)
+ .ForceInferSchema(tableDesc.ForceInferSchema)
+ .Anonymous(tableDesc.IsAnonymous)
+ .IgnoreYamrDsv(State_->Configuration->IgnoreYamrDsv.Get().GetOrElse(false))
+ .IgnoreWeakSchema(State_->Configuration->IgnoreWeakSchema.Get().GetOrElse(false))
+ );
+ LoadCtx->Tables.emplace_back(clusterAndTable, &tableDesc);
+ }
+ }
+
+ if (opts.Tables().empty()) {
+ return TStatus::Ok;
+ }
+
+ auto config = loadEpoch ? State_->Configuration->GetSettingsVer(settingsVer) : State_->Configuration->Snapshot();
+ opts.Config(config).ReadOnly(State_->Types->IsReadOnly).Epoch(loadEpoch);
+
+ auto future = State_->Gateway->GetTableInfo(std::move(opts));
+ auto loadCtx = LoadCtx;
+
+ AsyncFuture = future.Apply(
+ [loadCtx](const NThreading::TFuture<IYtGateway::TTableInfoResult>& completedFuture) {
+ loadCtx->Result = completedFuture.GetValue();
+ YQL_ENSURE(!loadCtx->Result.Success() || loadCtx->Result.Data.size() == loadCtx->Tables.size());
+ });
+
+ return TStatus::Async;
+ }
+
+ NThreading::TFuture<void> DoGetAsyncFuture(const TExprNode& input) final {
+ Y_UNUSED(input);
+ return AsyncFuture;
+ }
+
+ TStatus DoApplyAsyncChanges(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+ // Raise errors if any
+ AsyncFuture.GetValue();
+
+ LoadCtx->Result.ReportIssues(ctx.IssueManager);
+ if (!LoadCtx->Result.Success()) {
+ LoadCtx.Reset();
+ return TStatus::Error;
+ }
+
+ ctx.Step.Repeat(TExprStep::ExpandApplyForLambdas);
+ THashMap<std::pair<TString, TString>, TYtTableDescription*> tableDescrs;
+ for (size_t i = 0 ; i < LoadCtx->Result.Data.size(); ++i) {
+ tableDescrs.emplace(LoadCtx->Tables[i]);
+
+ TString cluster = LoadCtx->Tables[i].first.first;
+ TString tableName = LoadCtx->Tables[i].first.second;
+ TYtTableDescription& tableDesc = *LoadCtx->Tables[i].second;
+
+ if (LoadCtx->Result.Data[i].WriteLock) {
+ tableDesc.HasWriteLock = true;
+ }
+
+ TIssueScopeGuard issueScope(ctx.IssueManager, [tableName]() {
+ return MakeIntrusive<TIssue>(TStringBuilder() << "Table " << tableName);
+ });
+
+ if (auto meta = LoadCtx->Result.Data[i].Meta) {
+ tableDesc.Meta = meta;
+ const auto schemaAttrs = std::initializer_list<TStringBuf>{YqlRowSpecAttribute, SCHEMA_ATTR_NAME, READ_SCHEMA_ATTR_NAME, INFER_SCHEMA_ATTR_NAME};
+ if (AnyOf(schemaAttrs, [&tableDesc](TStringBuf attr) { return tableDesc.Meta->Attrs.contains(attr); })) {
+ auto rowSpec = MakeIntrusive<TYqlRowSpecInfo>();
+ if (!rowSpec->Parse(tableDesc.Meta->Attrs, ctx)) {
+ return TStatus::Error;
+ }
+ if (!State_->Configuration->UseNativeDescSort.Get().GetOrElse(false) && rowSpec->ClearNativeDescendingSort()) {
+ if (!ctx.AddWarning(YqlIssue(TPosition(), EYqlIssueCode::TIssuesIds_EIssueCode_YT_NATIVE_DESC_SORT_IGNORED, "Native descending sort is ignored"))) {
+ return TStatus::Error;
+ }
+ }
+ // Some sanity checks
+ if (tableDesc.RowSpec && tableDesc.RowSpec->IsSorted()) {
+ YQL_ENSURE(rowSpec->IsSorted(), "Bad predicted sort for the table '"
+ << cluster << "." << tableName << "' (epoch=" << LoadCtx->Epoch << "), expected sorted, but actually not.");
+ YQL_ENSURE(rowSpec->SortedBy.size() >= tableDesc.RowSpec->SortedBy.size()
+ && Equal(tableDesc.RowSpec->SortedBy.begin(), tableDesc.RowSpec->SortedBy.end(), rowSpec->SortedBy.begin()),
+ "Bad predicted SortedBy for the table '"
+ << cluster << "." << tableName << "' (epoch=" << LoadCtx->Epoch << "), expected: [" << JoinSeq(",", tableDesc.RowSpec->SortedBy)
+ << "], actual: [" << JoinSeq(",", rowSpec->SortedBy) << ']');
+ YQL_ENSURE(rowSpec->SortMembers.size() >= tableDesc.RowSpec->SortMembers.size()
+ && Equal(tableDesc.RowSpec->SortMembers.begin(), tableDesc.RowSpec->SortMembers.end(), rowSpec->SortMembers.begin()),
+ "Bad predicted SortMembers for the table '"
+ << cluster << "." << tableName << "' (epoch=" << LoadCtx->Epoch << "), expected: [" << JoinSeq(",", tableDesc.RowSpec->SortMembers)
+ << "], actual: [" << JoinSeq(",", rowSpec->SortMembers) << ']');
+ }
+ tableDesc.RowSpec = rowSpec;
+ ForEach(std::begin(schemaAttrs), std::end(schemaAttrs), [&tableDesc](TStringBuf attr) { tableDesc.Meta->Attrs.erase(attr); });
+
+ if (LoadCtx->Epoch != 0 && tableDesc.RawRowType) {
+ // Some sanity checks
+ YQL_ENSURE(IsSameAnnotation(*tableDesc.RawRowType, *tableDesc.RowSpec->GetType()), "Scheme diff: " << GetTypeDiff(*tableDesc.RawRowType, *tableDesc.RowSpec->GetType()));
+ }
+ }
+ if (auto rowSpecAttr = tableDesc.Meta->Attrs.FindPtr(TString{YqlRowSpecAttribute}.append("_qb2"))) {
+ auto rowSpec = MakeIntrusive<TYqlRowSpecInfo>();
+ if (!rowSpec->Parse(*rowSpecAttr, ctx)) {
+ return TStatus::Error;
+ }
+ tableDesc.QB2RowSpec = rowSpec;
+ tableDesc.Meta->Attrs.erase(TString{YqlRowSpecAttribute}.append("_qb2"));
+ }
+
+ if (0 == LoadCtx->Epoch) {
+ if (!tableDesc.Fill(cluster, tableName, ctx, State_->Types->Modules.get())) {
+ return TStatus::Error;
+ }
+ }
+ }
+
+ if (auto stat = LoadCtx->Result.Data[i].Stat) {
+ tableDesc.Stat = stat;
+ }
+ }
+
+ TStatus status = TStatus::Ok;
+ if (LoadCtx->Epoch) {
+ TNodeMap<TYtTableDescription*> tableNodes;
+ VisitExpr(input, [&](const TExprNode::TPtr& node) {
+ TString cluster;
+ if (auto maybeTable = TMaybeNode<TYtTable>(node)) {
+ TYtTable table = maybeTable.Cast();
+ if (auto p = tableDescrs.FindPtr(std::make_pair(TString{table.Cluster().Value()}, TString{table.Name().Value()}))) {
+ if (TEpochInfo::Parse(table.Epoch().Ref()).GetOrElse(0) == LoadCtx->Epoch) {
+ tableNodes.emplace(node.Get(), *p);
+ }
+ }
+ return false;
+ }
+ return true;
+ });
+
+ TOptimizeExprSettings settings(nullptr);
+ settings.VisitChanges = true;
+ settings.VisitStarted = true;
+ status = OptimizeExpr(input, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ auto it = tableNodes.find(node.Get());
+ if (it != tableNodes.cend()) {
+ TYtTableInfo table = node;
+
+ const TYtTableDescription& tableDesc = *it->second;
+ table.Stat = tableDesc.Stat;
+ table.Meta = tableDesc.Meta;
+ table.RowSpec = tableDesc.RowSpec;
+
+ return table.ToExprNode(ctx, node->Pos()).Ptr();
+ }
+ return node;
+ }, ctx, settings);
+ if (input != output) {
+ status = status.Combine(TStatus(TStatus::Repeat, true));
+ }
+ }
+
+ LoadCtx.Reset();
+ return status;
+ }
+
+ void Rewind() final {
+ LoadCtx.Reset();
+ AsyncFuture = {};
+ }
+
+private:
+ TYtState::TPtr State_;
+ TLoadContext::TPtr LoadCtx;
+ NThreading::TFuture<void> AsyncFuture;
+};
+
+} // namespace
+
+THolder<IGraphTransformer> CreateYtLoadTableMetadataTransformer(TYtState::TPtr state) {
+ return THolder(new TYtLoadTableMetadataTransformer(state));
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
new file mode 100644
index 0000000000..292afc7666
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_logical_optimize.cpp
@@ -0,0 +1,2653 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_table.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/opt/yql_yt_join.h>
+#include <ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/common/transform/yql_optimize.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/core/yql_aggregate_expander.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_opt_window.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_join.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <library/cpp/disjoint_sets/disjoint_sets.h>
+
+#include <util/generic/set.h>
+#include <util/generic/algorithm.h>
+#include <util/generic/vector.h>
+#include <util/generic/map.h>
+
+#include <utility>
+
+namespace NYql {
+
+using namespace NNodes;
+
+class TYtLogicalOptProposalTransformer : public TOptimizeTransformerBase {
+public:
+ TYtLogicalOptProposalTransformer(TYtState::TPtr state)
+ : TOptimizeTransformerBase(state->Types, NLog::EComponent::ProviderYt, state->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>()))
+ , State_(state)
+ {
+#define HNDL(name) "LogicalOptimizer-"#name, Hndl(&TYtLogicalOptProposalTransformer::name)
+ AddHandler(0, &TYtMap::Match, HNDL(DirectRow));
+ AddHandler(0, Names({TYtReduce::CallableName(), TYtMapReduce::CallableName()}), HNDL(IsKeySwitch));
+ AddHandler(0, &TCoLeft::Match, HNDL(TrimReadWorld));
+ AddHandler(0, &TCoCalcOverWindowBase::Match, HNDL(CalcOverWindow));
+ AddHandler(0, &TCoCalcOverWindowGroup::Match, HNDL(CalcOverWindow));
+ AddHandler(0, &TCoSort::Match, HNDL(SortOverAlreadySorted));
+ AddHandler(0, &TCoNth::Match, HNDL(Demux));
+ AddHandler(0, &TYtMap::Match, HNDL(VarianItemOverInput));
+ AddHandler(0, &TYtWithUserJobsOpBase::Match, HNDL(VisitOverInputWithEqualLambdas));
+ AddHandler(0, &TYtWithUserJobsOpBase::Match, HNDL(UnorderedOverInput));
+ AddHandler(0, &TCoFlatMapBase::Match, HNDL(DirectRowInFlatMap));
+ AddHandler(0, &TCoUnorderedBase::Match, HNDL(Unordered));
+ AddHandler(0, &TCoAggregate::Match, HNDL(CountAggregate));
+
+ AddHandler(1, &TCoFilterNullMembers::Match, HNDL(FilterNullMemebers<TCoFilterNullMembers>));
+ AddHandler(1, &TCoSkipNullMembers::Match, HNDL(FilterNullMemebers<TCoSkipNullMembers>));
+ AddHandler(1, &TCoFlatMapBase::Match, HNDL(FuseFlatmaps));
+ AddHandler(1, &TCoZip::Match, HNDL(Zip));
+ AddHandler(1, &TCoZipAll::Match, HNDL(ZipAll));
+ AddHandler(1, &TYtWithUserJobsOpBase::Match, HNDL(OutputInFlatMap));
+ AddHandler(1, &TYtOutput::Match, HNDL(BypassCopy));
+ AddHandler(1, &TCoAggregateBase::Match, HNDL(Aggregate));
+ AddHandler(1, &TCoExtractMembers::Match, HNDL(ExtractMembers));
+ AddHandler(1, &TCoExtractMembers::Match, HNDL(ExtractMembersOverContent));
+ AddHandler(1, &TCoExtractMembers::Match, HNDL(ExtractMembersOverDqWrap));
+ AddHandler(1, &TCoRight::Match, HNDL(PushdownReadColumns));
+ AddHandler(1, &TYtTransientOpBase::Match, HNDL(PushdownOpColumns));
+ AddHandler(1, &TCoCountBase::Match, HNDL(TakeOrSkip));
+ AddHandler(1, &TCoCountBase::Match, HNDL(TakeOrSkipOverDqWrap));
+ AddHandler(1, &TCoEquiJoin::Match, HNDL(SelfInnerJoinWithSameKeys));
+ AddHandler(1, &TCoExtendBase::Match, HNDL(ExtendOverSameMap));
+ AddHandler(1, &TCoFlatMapBase::Match, HNDL(FlatMapOverExtend));
+ AddHandler(1, &TCoTake::Match, HNDL(TakeOverExtend));
+ AddHandler(1, &TCoExtendBase::Match, HNDL(ExtendOverDqWrap));
+
+ AddHandler(2, &TCoEquiJoin::Match, HNDL(ConvertToCommonTypeForForcedMergeJoin));
+ AddHandler(2, &TCoShuffleByKeys::Match, HNDL(ShuffleByKeys));
+#undef HNDL
+ }
+
+protected:
+ TYtSection PushdownSectionColumns(TYtSection section, TExprContext& ctx, const TGetParents& getParents) const {
+ if (HasNonEmptyKeyFilter(section)) {
+ // wait until key filter values are calculated and pushed to Path/Ranges
+ return section;
+ }
+ bool hasNewPath = false;
+ TVector<TExprBase> paths;
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ for (auto path: section.Paths()) {
+ paths.push_back(path);
+
+ auto columns = TYtColumnsInfo(path.Columns());
+ if (!columns.HasColumns()) {
+ // No column filter
+ continue;
+ }
+
+ auto type = path.Table().Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ if (type->GetSize() <= columns.GetColumns()->size()) {
+ // The same column set as original type
+ continue;
+ }
+
+ // Use operation output only
+ if (auto op = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtTransientOpBase>()) {
+ if (op.Raw()->StartsExecution() || op.Raw()->HasResult()) {
+ // Operation is already executed
+ continue;
+ }
+
+ if (IsOutputUsedMultipleTimes(op.Cast().Ref(), *getParents())) {
+ // Operation output is used multiple times
+ continue;
+ }
+
+ if (op.Cast().Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1]->Cast<TListExprType>()->GetItemType()->GetKind() == ETypeAnnotationKind::Variant) {
+ // Operation with multi-output
+ continue;
+ }
+
+ if (NYql::HasSetting(op.Settings().Ref(), EYtSettingType::SortLimitBy)) {
+ continue;
+ }
+
+ const TYqlRowSpecInfo::TPtr rowSpec = TYtTableBaseInfo::GetRowSpec(path.Table());
+ YQL_ENSURE(rowSpec);
+
+ if (rowSpec->HasAuxColumns()) {
+ continue;
+ }
+
+ TSet<TString> effectiveColumns;
+ bool keepColumns = columns.GetRenames().Defined();
+ for (auto& column : columns.GetColumns().GetRef()) {
+ keepColumns = keepColumns || !column.Type.empty();
+ effectiveColumns.insert(column.Name);
+ }
+
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ // add columns which are implicitly used by path.Ranges(), but not included in path.Columns();
+ const auto ranges = TYtRangesInfo(path.Ranges());
+ const size_t usedKeyPrefix = ranges.GetUsedKeyPrefixLength();
+ YQL_ENSURE(usedKeyPrefix <= rowSpec->SortedBy.size());
+ for (size_t i = 0; i < usedKeyPrefix; ++i) {
+ bool inserted = effectiveColumns.insert(rowSpec->SortedBy[i]).second;
+ keepColumns = keepColumns || inserted;
+ }
+ }
+
+ if (type->GetSize() <= effectiveColumns.size()) {
+ // The same column set as original type
+ continue;
+ }
+
+ if (auto maybeMap = op.Maybe<TYtMap>()) {
+ TYtMap map = maybeMap.Cast();
+ TVector<const TItemExprType*> structItems;
+
+ auto mapper = ctx.Builder(map.Mapper().Pos())
+ .Lambda()
+ .Param("stream")
+ .Callable(NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered) ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Apply(0, map.Mapper().Ref())
+ .With(0, "stream")
+ .Seal()
+ .Lambda(1)
+ .Param("item")
+ .Callable(TCoJust::CallableName())
+ .Callable(0, TCoAsStruct::CallableName())
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ size_t index = 0;
+ for (const auto& column: effectiveColumns) {
+ auto pos = type->FindItem(column);
+ YQL_ENSURE(pos);
+ structItems.push_back(type->GetItems()[*pos]);
+ parent
+ .List(index++)
+ .Atom(0, column)
+ .Callable(1, TCoMember::CallableName())
+ .Arg(0, "item")
+ .Atom(1, column)
+ .Seal()
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto outStructType = ctx.MakeType<TStructExprType>(structItems);
+ TYtOutTableInfo mapOut(outStructType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+
+ if (ctx.IsConstraintEnabled<TSortedConstraintNode>()) {
+ if (const auto s = path.Table().Ref().GetConstraint<TSortedConstraintNode>()) {
+ if (const auto sorted = s->FilterFields(ctx, [outStructType](const TConstraintNode::TPathType& path) { return !path.empty() && outStructType->FindItem(path.front()); }) ) {
+ TKeySelectorBuilder builder(map.Mapper().Pos(), ctx, useNativeDescSort, outStructType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*mapOut.RowSpec);
+
+ if (builder.NeedMap()) {
+ mapper = ctx.Builder(map.Mapper().Pos())
+ .Lambda()
+ .Param("stream")
+ .Apply(builder.MakeRemapLambda(true))
+ .With(0)
+ .Apply(*mapper)
+ .With(0, "stream")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+ }
+ } else {
+ mapOut.RowSpec->CopySortness(TYqlRowSpecInfo(map.Output().Item(0).RowSpec()));
+ }
+ mapOut.SetUnique(path.Ref().GetConstraint<TDistinctConstraintNode>(), map.Mapper().Pos(), ctx);
+
+ TExprBase newColumns = Build<TCoVoid>(ctx, path.Pos()).Done();
+ if (keepColumns) {
+ newColumns = path.Columns();
+ }
+ hasNewPath = true;
+ paths.back() = Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .InitFrom(map)
+ .Output()
+ .Add(mapOut.ToExprNode(ctx, map.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Mapper(mapper)
+ .Build()
+ .OutIndex().Value(TStringBuf("0")).Build()
+ .Mode(path.Table().Cast<TYtOutput>().Mode())
+ .Build()
+ .Columns(newColumns)
+ .Stat<TCoVoid>().Build()
+ .Done();
+ }
+ else if (auto maybeMerge = op.Maybe<TYtMerge>()) {
+ TYtMerge merge = maybeMerge.Cast();
+
+ auto prevRowSpec = TYqlRowSpecInfo(merge.Output().Item(0).RowSpec());
+ auto prevOutType = merge.Output().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ TVector<const TItemExprType*> structItems;
+ for (const auto& column: effectiveColumns) {
+ auto pos = prevOutType->FindItem(column);
+ YQL_ENSURE(pos);
+ structItems.push_back(prevOutType->GetItems()[*pos]);
+ }
+
+ TYtOutTableInfo mergeOut(ctx.MakeType<TStructExprType>(structItems), prevRowSpec.GetNativeYtTypeFlags());
+ mergeOut.RowSpec->CopySortness(prevRowSpec, TYqlRowSpecInfo::ECopySort::WithDesc);
+ if (auto nativeType = prevRowSpec.GetNativeYtType()) {
+ mergeOut.RowSpec->CopyTypeOrders(*nativeType);
+ }
+ mergeOut.SetUnique(path.Ref().GetConstraint<TDistinctConstraintNode>(), merge.Pos(), ctx);
+
+ TSet<TStringBuf> columnSet(effectiveColumns.begin(), effectiveColumns.end());
+ if (mergeOut.RowSpec->HasAuxColumns()) {
+ for (auto item: mergeOut.RowSpec->GetAuxColumns()) {
+ columnSet.insert(item.first);
+ }
+ }
+ TExprBase newColumns = Build<TCoVoid>(ctx, path.Pos()).Done();
+ if (keepColumns) {
+ newColumns = path.Columns();
+ }
+
+ hasNewPath = true;
+ paths.back() = Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .Operation<TYtMerge>()
+ .InitFrom(merge)
+ .Input()
+ .Add(UpdateInputFields(merge.Input().Item(0), std::move(columnSet), ctx, false))
+ .Build()
+ .Output()
+ .Add(mergeOut.ToExprNode(ctx, merge.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Build()
+ .OutIndex().Value(TStringBuf("0")).Build()
+ .Mode(path.Table().Cast<TYtOutput>().Mode())
+ .Build()
+ .Columns(newColumns)
+ .Stat<TCoVoid>().Build()
+ .Done();
+ }
+ }
+ }
+ if (hasNewPath) {
+ return Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Done();
+ }
+ return section;
+ }
+
+protected:
+ TMaybeNode<TExprBase> CountAggregate(TExprBase node, TExprContext& ctx) const;
+
+ TMaybeNode<TExprBase> Aggregate(TExprBase node, TExprContext& ctx) const {
+ auto aggregate = node.Cast<TCoAggregateBase>();
+
+ auto input = aggregate.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(input)};
+ TSyncMap syncList;
+
+ for (auto handler: aggregate.Handlers()) {
+ auto trait = handler.Trait();
+ if (auto maybeAggTrait = trait.Maybe<TCoAggregationTraits>()) {
+ const auto& t = maybeAggTrait.Cast();
+ TVector<TExprBase> lambdas = {
+ t.InitHandler(),
+ t.UpdateHandler(),
+ t.SaveHandler(),
+ t.LoadHandler(),
+ t.MergeHandler(),
+ t.FinishHandler(),
+ };
+ for (auto lambda : lambdas) {
+ if (!IsYtCompleteIsolatedLambda(lambda.Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+ }
+ } else if (trait.Ref().IsCallable("AggApply")) {
+ if (!IsYtCompleteIsolatedLambda(*trait.Ref().Child(2), syncList, cluster, true, false)) {
+ return node;
+ }
+ }
+ }
+
+ auto usePhases = State_->Configuration->UseAggPhases.Get().GetOrElse(false);
+ auto usePartitionsByKeys = State_->Configuration->UsePartitionsByKeysForFinalAgg.Get().GetOrElse(true);
+ TAggregateExpander aggExpander(true, usePartitionsByKeys, false, node.Ptr(), ctx, *State_->Types, false, false, usePhases);
+ return aggExpander.ExpandAggregate();
+ }
+
+ TMaybeNode<TExprBase> DirectRow(TExprBase node, TExprContext& ctx) const {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ return node;
+ }
+
+ auto map = node.Cast<TYtMap>();
+
+ auto mapper = map.Mapper();
+ auto maybeLambda = GetFlatMapOverInputStream(mapper).Lambda();
+ if (!maybeLambda) {
+ return node;
+ }
+
+ TCoLambda lambda = maybeLambda.Cast();
+ auto arg = lambda.Args().Arg(0);
+
+ TNodeSet nodesToOptimize;
+ TProcessedNodesSet processedNodes;
+ processedNodes.insert(map.World().Ref().UniqueId());
+ VisitExpr(lambda.Ptr(), [&nodesToOptimize, &processedNodes, arg](const TExprNode::TPtr& node) {
+ if (TCoTablePath::Match(node.Get())) {
+ if (node->ChildrenSize() == 0 || node->Child(0)->Child(0) == arg.Raw()) {
+ nodesToOptimize.insert(node.Get());
+ }
+ }
+ else if (TCoTableRecord::Match(node.Get())) {
+ if (node->ChildrenSize() == 0 || node->Child(0)->Child(0) == arg.Raw()) {
+ nodesToOptimize.insert(node.Get());
+ }
+ }
+ else if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TExprNode::TPtr newBody = lambda.Body().Ptr();
+ TPositionHandle tablePos;
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ auto status = OptimizeExpr(newBody, newBody, [&nodesToOptimize, &tablePos, arg](const TExprNode::TPtr& input, TExprContext& ctx) -> TExprNode::TPtr {
+ if (nodesToOptimize.find(input.Get()) != nodesToOptimize.end()) {
+ if (TCoTablePath::Match(input.Get())) {
+ tablePos = input->Pos();
+ if (input->ChildrenSize() == 1) {
+ return ctx.RenameNode(*input, TYtTablePath::CallableName());
+ }
+ return Build<TYtTablePath>(ctx, input->Pos())
+ .DependsOn()
+ .Input(arg)
+ .Build()
+ .Done().Ptr();
+ }
+ else if (TCoTableRecord::Match(input.Get())) {
+ tablePos = input->Pos();
+ if (input->ChildrenSize() == 1) {
+ return ctx.RenameNode(*input, TYtTableRecord::CallableName());
+ }
+ return Build<TYtTableRecord>(ctx, input->Pos())
+ .DependsOn()
+ .Input(arg)
+ .Build()
+ .Done().Ptr();
+ }
+ }
+
+ return input;
+ }, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ if (status.Level == IGraphTransformer::TStatus::Ok) {
+ return node;
+ }
+
+ auto newLambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({TStringBuf("row")})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(newBody))
+ .With(arg, TStringBuf("row"))
+ .Build()
+ .Done();
+
+ auto newMapper = Build<TCoLambda>(ctx, mapper.Pos())
+ .Args({TStringBuf("stream")})
+ .Body<TExprApplier>()
+ .Apply(mapper)
+ .With(mapper.Args().Arg(0), TStringBuf("stream"))
+ .With(lambda, newLambda)
+ .Build()
+ .Done();
+
+ bool stop = false;
+ for (auto section: map.Input()) {
+ for (auto path: section.Paths()) {
+ if (path.Table().Maybe<TYtOutput>()) {
+ auto issue = TIssue(ctx.GetPosition(tablePos), "TablePath(), TableName() and TableRecordIndex() will be empty for temporary tables.\n"
+ "Please consult documentation https://yql.yandex-team.ru/docs/yt/builtins/basic#tablepath for possible workaround");
+ SetIssueCode(EYqlIssueCode::TIssuesIds_EIssueCode_YT_TABLE_PATH_RECORD_FOR_TMP, issue);
+ if (!ctx.AddWarning(issue)) {
+ return nullptr;
+ }
+ stop = true;
+ break;
+ }
+ }
+ if (stop) {
+ break;
+ }
+ }
+
+ return ctx.ChangeChild(node.Ref(), TYtMap::idx_Mapper, newMapper.Ptr());
+ }
+
+ TMaybeNode<TExprBase> IsKeySwitch(TExprBase node, TExprContext& ctx) const {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ return node;
+ }
+
+ auto lambda = node.Maybe<TYtMapReduce>() ? node.Cast<TYtMapReduce>().Reducer() : node.Cast<TYtReduce>().Reducer();
+ TNodeSet nodesToOptimize;
+ TProcessedNodesSet processedNodes;
+ processedNodes.insert(node.Cast<TYtWithUserJobsOpBase>().World().Ref().UniqueId());
+ VisitExpr(lambda.Ptr(), [&nodesToOptimize, &processedNodes](const TExprNode::TPtr& node) {
+ if (TCoIsKeySwitch::Match(node.Get())) {
+ nodesToOptimize.insert(node.Get());
+ }
+ else if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TExprNode::TPtr newBody = lambda.Body().Ptr();
+ TPosition tablePos;
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ TStatus status = OptimizeExpr(newBody, newBody, [&](const TExprNode::TPtr& input, TExprContext& ctx) -> TExprNode::TPtr {
+ if (nodesToOptimize.find(input.Get()) != nodesToOptimize.end()) {
+ if (TCoIsKeySwitch::Match(input.Get())) {
+ return
+ Build<TYtIsKeySwitch>(ctx, input->Pos())
+ .DependsOn()
+ .Input(input->HeadPtr())
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ return input;
+ }, ctx, settings);
+
+ if (status.Level == TStatus::Error) {
+ return {};
+ }
+
+ if (status.Level == TStatus::Ok) {
+ return node;
+ }
+
+ auto newLambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({TStringBuf("stream")})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(newBody))
+ .With(lambda.Args().Arg(0), TStringBuf("stream"))
+ .Build()
+ .Done();
+
+ return ctx.ChangeChild(node.Ref(), node.Maybe<TYtMapReduce>() ? TYtMapReduce::idx_Reducer : TYtReduce::idx_Reducer, newLambda.Ptr());
+ }
+
+ template <typename T>
+ TMaybeNode<TExprBase> FilterNullMemebers(TExprBase node, TExprContext& ctx) const {
+ auto filterNullMembers = node.Cast<T>();
+ if (!IsYtProviderInput(filterNullMembers.Input())) {
+ return node;
+ }
+
+ YQL_ENSURE(filterNullMembers.Ptr()->GetTypeAnn()->GetKind() == ETypeAnnotationKind::List);
+
+ return Build<TCoOrderedFlatMap>(ctx, filterNullMembers.Pos())
+ .Input(filterNullMembers.Input())
+ .Lambda()
+ .Args({"item"})
+ .template Body<T>()
+ .template Input<TCoJust>()
+ .Input("item")
+ .Build()
+ .Members(filterNullMembers.Members())
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> TrimReadWorld(TExprBase node, TExprContext& ctx) const {
+ auto maybeRead = node.Cast<TCoLeft>().Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ return node;
+ }
+
+ auto read = maybeRead.Cast();
+ TExprNode::TListType worlds(1, read.World().Ptr());
+ for (auto section: read.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ worlds.push_back(
+ Build<TCoLeft>(ctx, node.Pos())
+ .Input(out.Cast().Operation())
+ .Done().Ptr()
+ );
+ }
+ }
+ }
+
+ return TExprBase(worlds.size() == 1 ? worlds.front() : ctx.NewCallable(node.Pos(), TCoSync::CallableName(), std::move(worlds)));
+ }
+
+ TMaybeNode<TExprBase> CalcOverWindow(TExprBase node, TExprContext& ctx) const {
+ auto list = node.Cast<TCoInputBase>().Input();
+ if (!IsYtProviderInput(list)) {
+ return node;
+ }
+
+ TVector<TYtTableBaseInfo::TPtr> tableInfos = GetInputTableInfos(list);
+ if (AllOf(tableInfos, [](const TYtTableBaseInfo::TPtr& info) { return !info->Meta->IsDynamic; })) {
+ TExprNodeList calcs = ExtractCalcsOverWindow(node.Ptr(), ctx);
+ TSet<TStringBuf> rowNumberCols;
+ for (auto& calcNode : calcs) {
+ TCoCalcOverWindowTuple calc(calcNode);
+ if (calc.Keys().Size() != 0 || !calc.SessionSpec().Maybe<TCoVoid>() || !calc.SortSpec().Maybe<TCoVoid>()) {
+ continue;
+ }
+ bool needOptimize = false;
+ auto frames = calc.Frames().Ref().ChildrenList();
+ for (const auto& win : frames) {
+ YQL_ENSURE(TCoWinOnBase::Match(win.Get()));
+ auto args = win->ChildrenList();
+ needOptimize = args.size() > 1 &&
+ // We rewrite RowNumber() into YtMap if it is the only window function for some frame
+ // (hence AllOf is used below)
+ // If the frame consist of RowNumber() and other window functions, we just calculate RowNumber() along with them
+ AllOf(args.begin() + 1, args.end(),
+ [](const auto& arg) {
+ return arg->Child(1)->IsCallable("RowNumber");
+ });
+ if (needOptimize) {
+ break;
+ }
+ }
+
+ if (!needOptimize) {
+ continue;
+ }
+
+ for (auto& win : frames) {
+ YQL_ENSURE(TCoWinOnBase::Match(win.Get()));
+ auto winOnArgs = win->ChildrenList();
+
+ TExprNodeList newWinOnArgs;
+ newWinOnArgs.push_back(std::move(winOnArgs[0]));
+
+ for (size_t i = 1; i < winOnArgs.size(); ++i) {
+ auto labelAtom = winOnArgs[i]->Child(0);
+ YQL_ENSURE(labelAtom->IsAtom());
+ auto trait = winOnArgs[i]->Child(1);
+
+ if (trait->IsCallable("RowNumber")) {
+ rowNumberCols.insert(labelAtom->Content());
+ } else {
+ newWinOnArgs.push_back(std::move(winOnArgs[i]));
+ }
+ }
+
+ win = ctx.ChangeChildren(*win, std::move(newWinOnArgs));
+ }
+
+ calcNode = Build<TCoCalcOverWindowTuple>(ctx, calc.Pos())
+ .Keys(calc.Keys())
+ .SortSpec(calc.SortSpec())
+ .Frames(ctx.NewList(calc.Frames().Pos(), std::move(frames)))
+ .SessionSpec(calc.SessionSpec())
+ .SessionColumns(calc.SessionColumns())
+ .Done().Ptr();
+ }
+
+ if (rowNumberCols) {
+ auto rowArg = ctx.NewArgument(node.Pos(), "row");
+ auto body = rowArg;
+
+ const bool useSysColumns = State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS);
+ const auto sysColumnNum = TString(YqlSysColumnPrefix).append("num");
+ if (useSysColumns) {
+ for (auto& col : rowNumberCols) {
+ body = ctx.Builder(node.Pos())
+ .Callable("AddMember")
+ .Add(0, body)
+ .Atom(1, col)
+ .Callable(2, "Member")
+ .Add(0, rowArg)
+ .Atom(1, sysColumnNum)
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ body = ctx.Builder(node.Pos())
+ .Callable("ForceRemoveMember")
+ .Add(0, body)
+ .Atom(1, sysColumnNum)
+ .Seal()
+ .Build();
+
+ auto settings = Build<TCoNameValueTupleList>(ctx, list.Pos())
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::SysColumns))
+ .Build()
+ .Value(ToAtomList(TVector<TStringBuf>{"num"}, list.Pos(), ctx))
+ .Build()
+ .Done();
+
+ if (auto right = list.Maybe<TCoRight>()) {
+ list = right.Cast().Input();
+ }
+
+ list = Build<TCoRight>(ctx, list.Pos())
+ .Input(ConvertContentInputToRead(list, settings, ctx))
+ .Done();
+ } else {
+ for (auto& col : rowNumberCols) {
+ body = ctx.Builder(node.Pos())
+ .Callable("AddMember")
+ .Add(0, body)
+ .Atom(1, col)
+ .Callable(2, "YtRowNumber")
+ .Callable(0, "DependsOn")
+ .Add(0, rowArg)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+
+ auto input = ctx.Builder(node.Pos())
+ .Callable("OrderedMap")
+ .Add(0, list.Ptr())
+ .Add(1, ctx.NewLambda(node.Pos(), ctx.NewArguments(node.Pos(), {rowArg}), std::move(body)))
+ .Seal()
+ .Build();
+
+ YQL_CLOG(INFO, ProviderYt) << "Replaced " << rowNumberCols.size() << " RowNumber()s with " << (useSysColumns ? sysColumnNum : TString("YtRowNumber()"));
+
+ return RebuildCalcOverWindowGroup(node.Pos(), input, calcs, ctx);
+ }
+ }
+
+ return ExpandCalcOverWindow(node.Ptr(), ctx);
+ }
+
+ TMaybeNode<TExprBase> SortOverAlreadySorted(TExprBase node, TExprContext& ctx) const {
+ auto sort = node.Cast<TCoSort>();
+
+ if (!IsConstExpSortDirections(sort.SortDirections())) {
+ return node;
+ }
+
+ auto list = sort.Input();
+ if (!IsYtProviderInput(list)) {
+ return node;
+ }
+
+ TVector<TYtPathInfo::TPtr> pathInfos = GetInputPaths(list);
+ if (pathInfos.size() > 1) {
+ return node;
+ }
+ TYtPathInfo::TPtr pathInfo = pathInfos.front();
+ if (pathInfo->Columns || pathInfo->Ranges || !pathInfo->Table->RowSpec || !pathInfo->Table->RowSpec->IsSorted()) {
+ return node;
+ }
+ const TStructExprType* itemType = nullptr;
+ if (auto type = GetSequenceItemType(node, false, ctx)) {
+ itemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ TKeySelectorBuilder builder(node.Pos(), ctx, useNativeDescSort, itemType);
+ builder.ProcessKeySelector(sort.KeySelectorLambda().Ptr(), sort.SortDirections().Ptr());
+
+ if (builder.Members().size() < builder.Columns().size()) {
+ return node;
+ }
+
+ auto rowSpec = pathInfo->Table->RowSpec;
+ for (size_t i = 0; i < builder.Members().size(); ++i) {
+ if (i >= rowSpec->SortMembers.size()) {
+ return node;
+ }
+
+ if (rowSpec->SortMembers[i] != builder.Members()[i]) {
+ return node;
+ }
+
+ if (!rowSpec->SortDirections[i] || !builder.SortDirections()[i]) {
+ return node;
+ }
+ }
+
+ return list;
+ }
+
+ TMaybeNode<TExprBase> Demux(TExprBase node, TExprContext& ctx) const {
+ auto nth = node.Cast<TCoNth>();
+ auto input = nth.Tuple().Maybe<TCoDemux>().Input().Maybe<TCoRight>().Input();
+ if (!input) {
+ return node;
+ }
+
+ if (auto op = input.Maybe<TYtOutputOpBase>()) {
+ return Build<TYtOutput>(ctx, node.Pos())
+ .Operation(op.Cast())
+ .OutIndex(nth.Index())
+ .Done();
+ }
+ if (auto maybeRead = input.Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ auto ndx = FromString<size_t>(nth.Index().Value());
+ YQL_ENSURE(ndx < read.Input().Size());
+ return Build<TCoRight>(ctx, node.Pos())
+ .Input<TYtReadTable>()
+ .InitFrom(read)
+ .Input()
+ .Add(read.Input().Item(ndx))
+ .Build()
+ .Build()
+ .Done();
+ }
+ return node;
+
+ }
+
+ TMaybeNode<TExprBase> VarianItemOverInput(TExprBase node, TExprContext& ctx) const {
+ auto map = node.Cast<TYtMap>();
+ if (map.Input().Size() == 1) {
+ return node;
+ }
+
+ // All sections should have equal settings
+ const TExprNode* sectionSettings = nullptr;
+ for (auto section: map.Input()) {
+ if (nullptr == sectionSettings) {
+ sectionSettings = section.Settings().Raw();
+ } else if (sectionSettings != section.Settings().Raw()) {
+ return node;
+ }
+ }
+
+ auto mapper = map.Mapper();
+
+ TParentsMap parentsMap;
+ GatherParents(mapper.Body().Ref(), parentsMap);
+
+ auto maybeLambda = GetFlatMapOverInputStream(mapper, parentsMap).Lambda();
+ if (!maybeLambda) {
+ return node;
+ }
+
+ TCoLambda lambda = maybeLambda.Cast();
+ auto arg = lambda.Args().Arg(0);
+
+ // Check arg is used only in VariantItem
+ auto it = parentsMap.find(arg.Raw());
+ if (it == parentsMap.cend() || it->second.size() != 1 || !TCoVariantItem::Match(*it->second.begin())) {
+ return node;
+ }
+
+ // Substitute VariantItem(arg) by arg
+ TNodeOnNodeOwnedMap nodesToOptimize;
+ nodesToOptimize.emplace(*it->second.begin(), arg.Ptr());
+ TExprNode::TPtr newMapper;
+ auto status = RemapExpr(mapper.Ptr(), newMapper, nodesToOptimize, ctx, TOptimizeExprSettings{nullptr});
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ // Join all paths to single section
+ auto newPaths = Build<TYtPathList>(ctx, map.Input().Pos());
+ for (auto section: map.Input()) {
+ newPaths.Add(section.Paths().Ref().ChildrenList());
+ }
+
+ return Build<TYtMap>(ctx, map.Pos())
+ .InitFrom(map)
+ .Input()
+ .Add()
+ .Paths(newPaths.Done())
+ .Settings(map.Input().Item(0).Settings())
+ .Build()
+ .Build()
+ .Mapper(TCoLambda(newMapper))
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> VisitOverInputWithEqualLambdas(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() == 1) {
+ return node;
+ }
+
+ if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ size_t lambdaIdx = op.Maybe<TYtMapReduce>()
+ ? TYtMapReduce::idx_Mapper
+ : op.Maybe<TYtReduce>()
+ ? TYtReduce::idx_Reducer
+ : TYtMap::idx_Mapper;
+
+ auto opLambda = TCoLambda(op.Ref().ChildPtr(lambdaIdx));
+
+ TParentsMap parentsMap;
+ GatherParents(opLambda.Body().Ref(), parentsMap);
+
+ auto maybeLambda = GetFlatMapOverInputStream(opLambda, parentsMap).Lambda();
+ if (!maybeLambda) {
+ return node;
+ }
+
+ TCoLambda lambda = maybeLambda.Cast();
+ auto arg = lambda.Args().Arg(0);
+
+ // Check arg is used only in Visit
+ auto it = parentsMap.find(arg.Raw());
+ if (it == parentsMap.cend() || it->second.size() != 1 || !TCoVisit::Match(*it->second.begin())) {
+ return node;
+ }
+
+ using TGroupKey = std::pair<const TExprNode*, const TExprNode*>; // lambda, section settings
+ TMap<TGroupKey, TVector<size_t>> groupedInputs; // key -> {section id}
+ TVector<TExprNode::TPtr> visitLambdas;
+ visitLambdas.resize(op.Input().Size());
+
+ const TExprNode* visit = *it->second.begin();
+ for (ui32 index = 1; index < visit->ChildrenSize(); ++index) {
+ if (visit->Child(index)->IsAtom()) {
+ size_t inputNum = FromString<size_t>(visit->Child(index)->Content());
+ YQL_ENSURE(inputNum < op.Input().Size());
+
+ ++index;
+
+ groupedInputs[std::make_pair(visit->Child(index), op.Input().Item(inputNum).Settings().Raw())].push_back(inputNum);
+ visitLambdas[inputNum] = visit->ChildPtr(index);
+ }
+ }
+
+ if (groupedInputs.empty() || AllOf(groupedInputs, [](const decltype(groupedInputs)::value_type& grp) { return grp.second.size() <= 1; })) {
+ return node;
+ }
+
+ TVector<TVector<size_t>> groups;
+ groups.reserve(groupedInputs.size());
+ for (auto& grp: groupedInputs) {
+ groups.push_back(std::move(grp.second));
+ }
+ ::Sort(groups.begin(), groups.end(),
+ [] (const decltype(groups)::value_type& v1, const decltype(groups)::value_type& v2) {
+ return v1.front() < v2.front();
+ }
+ );
+
+ // Rebuild input
+ TVector<TYtSection> newSections;
+ for (auto& grp: groups) {
+ TVector<TYtPath> paths;
+ for (size_t sectionNum: grp) {
+ auto oldSection = op.Input().Item(sectionNum);
+ paths.insert(paths.end(), oldSection.Paths().begin(), oldSection.Paths().end());
+ }
+ auto firstOldSection = op.Input().Item(grp.front());
+ newSections.push_back(Build<TYtSection>(ctx, firstOldSection.Pos())
+ .InitFrom(firstOldSection)
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Done());
+ }
+
+ // Rebuild lambda
+ TExprNode::TPtr newVisit = ctx.Builder(visit->Pos())
+ .Callable(TCoVisit::CallableName())
+ .Add(0, visit->ChildPtr(0))
+ .Do([&](TExprNodeBuilder& builder) -> TExprNodeBuilder& {
+ for (size_t i = 0; i < groups.size(); ++i) {
+ builder.Atom(i * 2 + 1, ToString(i));
+ builder.Add(i * 2 + 2, visitLambdas.at(groups[i].front()));
+ }
+ if (visit->ChildrenSize() % 2 == 0) { // has default value
+ builder.Add(groups.size() * 2 + 1, visit->TailPtr());
+ }
+ return builder;
+ })
+ .Seal()
+ .Build();
+ TNodeOnNodeOwnedMap nodesToOptimize;
+ nodesToOptimize.emplace(visit, newVisit);
+ TExprNode::TPtr newOpLambda;
+ auto status = RemapExpr(opLambda.Ptr(), newOpLambda, nodesToOptimize, ctx, TOptimizeExprSettings{nullptr});
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), TYtWithUserJobsOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(newSections)
+ .Done().Ptr());
+
+ res = ctx.ChangeChild(*res, lambdaIdx, std::move(newOpLambda));
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> UnorderedOverInput(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+
+ if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ size_t lambdaIdx = op.Maybe<TYtMapReduce>()
+ ? TYtMapReduce::idx_Mapper
+ : op.Maybe<TYtReduce>()
+ ? TYtReduce::idx_Reducer
+ : TYtMap::idx_Mapper;
+
+ auto opLambda = TCoLambda(op.Ref().ChildPtr(lambdaIdx));
+ auto arg = opLambda.Args().Arg(0);
+
+ TParentsMap parentsMap;
+ GatherParents(opLambda.Body().Ref(), parentsMap);
+
+ auto it = parentsMap.find(arg.Raw());
+ if (it == parentsMap.cend()) {
+ return node;
+ }
+
+ // Substitute Unordered(arg) by arg
+ TNodeOnNodeOwnedMap nodesToOptimize;
+ for (auto n: it->second) {
+ if (TCoUnordered::Match(n)) {
+ nodesToOptimize.emplace(n, arg.Ptr());
+ } else if (!TCoDependsOn::Match(n)) {
+ return node;
+ }
+ }
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TExprNode::TPtr newOpLambda;
+ auto status = RemapExpr(opLambda.Ptr(), newOpLambda, nodesToOptimize, ctx, TOptimizeExprSettings{nullptr});
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), lambdaIdx, std::move(newOpLambda));
+
+ TVector<TYtSection> updatedSections;
+ for (auto section: op.Input()) {
+ updatedSections.push_back(MakeUnorderedSection(section, ctx));
+ }
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done().Ptr());
+
+ if (NYql::HasSetting(op.Settings().Ref(), EYtSettingType::Ordered)) {
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_Settings, NYql::RemoveSettings(op.Settings().Ref(), EYtSettingType::Ordered, ctx));
+ }
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> FuseFlatmaps(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto outerFlatmap = node.Cast<TCoFlatMapBase>();
+ if (!outerFlatmap.Input().Maybe<TCoFlatMapBase>()) {
+ return node;
+ }
+
+ auto innerFlatmap = outerFlatmap.Input().Cast<TCoFlatMapBase>();
+ if (!IsYtProviderInput(innerFlatmap.Input())) {
+ return node;
+ }
+
+ if (FindNode(innerFlatmap.Lambda().Body().Ptr(),
+ [](const TExprNode::TPtr& node) { return !TYtOutput::Match(node.Get()); },
+ [](const TExprNode::TPtr& node) { return TCoNonDeterministicBase::Match(node.Get()); })) {
+
+ // If inner FlatMap uses non-deterministic functions then disallow to split it in case of multiusage
+ const TParentsMap* parentsMap = getParents();
+ auto parentsIt = parentsMap->find(innerFlatmap.Raw());
+ YQL_ENSURE(parentsIt != parentsMap->cend());
+ if (parentsIt->second.size() > 1) {
+ return node;
+ }
+ }
+
+ const auto flatMapName = outerFlatmap.Maybe<TCoOrderedFlatMap>() && innerFlatmap.Maybe<TCoOrderedFlatMap>()
+ ? TCoOrderedFlatMap::CallableName()
+ : TCoFlatMap::CallableName();
+
+ auto [placeHolder, lambdaWithPlaceholder] = ReplaceDependsOn(outerFlatmap.Lambda().Ptr(), ctx, State_->Types);
+ if (!placeHolder) {
+ return {};
+ }
+
+ return Build<TCoFlatMapBase>(ctx, outerFlatmap.Pos())
+ .CallableName(flatMapName)
+ .Input(innerFlatmap.Input())
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(flatMapName)
+ .Input<TExprApplier>()
+ .Apply(innerFlatmap.Lambda())
+ .With(0, "item")
+ .Build()
+ .Lambda()
+ .Args({"outerItem"})
+ .Body<TExprApplier>()
+ .Apply(TCoLambda(lambdaWithPlaceholder))
+ .With(0, "outerItem")
+ .With(TExprBase(placeHolder), "item")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> Zip(TExprBase node, TExprContext& ctx) const {
+ auto zip = node.Cast<TCoZip>();
+ if (zip.ArgCount() != 2) {
+ return node;
+ }
+
+ auto lhsList = zip.Arg(0);
+ auto rhsList = zip.Arg(1);
+ if (!IsYtProviderInput(lhsList) || !IsYtProviderInput(rhsList)) {
+ return node;
+ }
+
+ return Build<TCoMap>(ctx, zip.Pos())
+ .Input<TCoJoin>()
+ .LeftInput<TCoEnumerate>()
+ .Input(lhsList)
+ .Build()
+ .RightInput<TCoEnumerate>()
+ .Input(rhsList)
+ .Build()
+ .LeftLambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Build()
+ .RightLambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Build()
+ .JoinKind()
+ .Value("Inner")
+ .Build()
+ .Build()
+ .Lambda()
+ .Args({"m"})
+ .Body<TExprList>()
+ .Add<TCoNth>()
+ .Tuple<TCoNth>()
+ .Tuple("m")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Add<TCoNth>()
+ .Tuple<TCoNth>()
+ .Tuple("m")
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> ZipAll(TExprBase node, TExprContext& ctx) const {
+ auto zip = node.Cast<TCoZipAll>();
+ if (zip.ArgCount() != 2) {
+ return node;
+ }
+
+ auto lhsList = zip.Arg(0);
+ auto rhsList = zip.Arg(1);
+ if (!IsYtProviderInput(lhsList) || !IsYtProviderInput(rhsList)) {
+ return node;
+ }
+
+ return Build<TCoMap>(ctx, zip.Pos())
+ .Input<TCoJoin>()
+ .LeftInput<TCoEnumerate>()
+ .Input(lhsList)
+ .Build()
+ .RightInput<TCoEnumerate>()
+ .Input(rhsList)
+ .Build()
+ .LeftLambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Build()
+ .RightLambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Build()
+ .JoinKind()
+ .Value("Full")
+ .Build()
+ .Build()
+ .Lambda()
+ .Args({"m"})
+ .Body<TExprList>()
+ .Add<TCoMap>()
+ .Input<TCoNth>()
+ .Tuple("m")
+ .Index()
+ .Value("0")
+ .Build()
+ .Build()
+ .Lambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Add<TCoMap>()
+ .Input<TCoNth>()
+ .Tuple("m")
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Lambda()
+ .Args({"p"})
+ .Body<TCoNth>()
+ .Tuple("p")
+ .Index()
+ .Value("1")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> ExtractMembers(TExprBase node, TExprContext& ctx) const {
+ auto extract = node.Cast<TCoExtractMembers>();
+ auto input = extract.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ TExprNode::TPtr world;
+ TVector<TYtSection> sections;
+ if (auto out = input.Maybe<TYtOutput>()) {
+ world = ctx.NewWorld(input.Pos());
+ sections.push_back(Build<TYtSection>(ctx, input.Pos())
+ .Paths()
+ .Add()
+ .Table(out.Cast())
+ .Columns(extract.Members())
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings().Build()
+ .Done());
+ }
+ else {
+ auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+ world = read.Cast().World().Ptr();
+
+ for (auto section: read.Cast().Input()) {
+ sections.push_back(UpdateInputFields(section, extract.Members(), ctx));
+ }
+ }
+
+ return Build<TCoRight>(ctx, extract.Pos())
+ .Input<TYtReadTable>()
+ .World(world)
+ .DataSource(GetDataSource(input, ctx))
+ .Input()
+ .Add(sections)
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> ExtractMembersOverContent(TExprBase node, TExprContext& ctx) const {
+ auto extractMembers = node.Cast<TCoExtractMembers>();
+
+ TExprBase tableContent = extractMembers.Input();
+ if (!tableContent.Maybe<TYtTableContent>()) {
+ return node;
+ }
+
+ return Build<TYtTableContent>(ctx, tableContent.Pos())
+ .InitFrom(tableContent.Cast<TYtTableContent>())
+ .Input(ConvertContentInputToRead(tableContent.Cast<TYtTableContent>().Input(), {}, ctx, extractMembers.Members()))
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> ExtractMembersOverDqWrap(TExprBase node, TExprContext& ctx) {
+ auto extract = node.Cast<TCoExtractMembers>();
+
+ if (auto maybeYtRead = extract.Input().Maybe<TDqReadWrapBase>().Input().Maybe<TYtReadTable>()) {
+ auto ytRead = maybeYtRead.Cast();
+
+ TVector<TYtSection> sections;
+ for (auto section: ytRead.Input()) {
+ sections.push_back(UpdateInputFields(section, extract.Members(), ctx));
+ }
+ auto updatedRead = Build<TYtReadTable>(ctx, ytRead.Pos())
+ .InitFrom(ytRead)
+ .Input()
+ .Add(sections)
+ .Build()
+ .Done().Ptr();
+
+ return TExprBase(ctx.ChangeChild(extract.Input().Ref(), TDqReadWrapBase::idx_Input, std::move(updatedRead)));
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> TakeOrSkip(TExprBase node, TExprContext& ctx) const {
+ auto countBase = node.Cast<TCoCountBase>();
+ auto input = countBase.Input();
+ if (!input.Maybe<TYtTableContent>()) {
+ return node;
+ }
+
+ input = input.Cast<TYtTableContent>().Input();
+
+ TYtDSource dataSource = GetDataSource(input, ctx);
+ TString cluster = TString{dataSource.Cluster().Value()};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(countBase.Count().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ EYtSettingType settingType = node.Maybe<TCoSkip>() ? EYtSettingType::Skip : EYtSettingType::Take;
+
+ auto settings = Build<TCoNameValueTupleList>(ctx, countBase.Pos())
+ .Add()
+ .Name()
+ .Value(ToString(settingType))
+ .Build()
+ .Value(countBase.Count())
+ .Build()
+ .Done();
+
+ return ctx.ChangeChild(countBase.Input().Ref(), TYtTableContent::idx_Input, ConvertContentInputToRead(input, settings, ctx).Ptr());
+ }
+
+ TMaybeNode<TExprBase> TakeOrSkipOverDqWrap(TExprBase node, TExprContext& ctx) {
+ auto countBase = node.Cast<TCoCountBase>();
+
+ if (auto maybeYtRead = countBase.Input().Maybe<TDqReadWrapBase>().Input().Maybe<TYtReadTable>()) {
+ auto ytRead = maybeYtRead.Cast();
+ if (ytRead.Input().Size() != 1) {
+ return node;
+ }
+
+ TYtDSource dataSource = GetDataSource(ytRead, ctx);
+ TString cluster = dataSource.Cluster().StringValue();
+
+ if (!State_->Configuration->_EnableYtPartitioning.Get(cluster).GetOrElse(false)) {
+ return node;
+ }
+
+ // TODO: support via precomputes
+ if (!TCoIntegralCtor::Match(countBase.Count().Raw())) {
+ return node;
+ }
+
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(countBase.Count().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ TYtSection section = ytRead.Input().Item(0);
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample)) {
+ return node;
+ }
+ if (AnyOf(section.Paths(), [](const auto& path) { TYtPathInfo pathInfo(path); return (pathInfo.Table->Meta && pathInfo.Table->Meta->IsDynamic) || pathInfo.Ranges; })) {
+ return node;
+ }
+
+ EYtSettingType settingType = node.Maybe<TCoSkip>() ? EYtSettingType::Skip : EYtSettingType::Take;
+
+ auto updatedRead = Build<TYtReadTable>(ctx, ytRead.Pos())
+ .InitFrom(ytRead)
+ .Input()
+ .Add()
+ .InitFrom(section)
+ .Settings(NYql::AddSetting(section.Settings().Ref(), settingType, countBase.Count().Ptr(), ctx))
+ .Build()
+ .Build()
+ .Done().Ptr();
+
+ return TExprBase(ctx.ChangeChild(countBase.Input().Ref(), TDqReadWrapBase::idx_Input, std::move(updatedRead)));
+
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> BypassCopy(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto srcOut = node.Cast<TYtOutput>();
+ auto maybeCopy = srcOut.Operation().Maybe<TYtCopy>();
+ if (!maybeCopy) {
+ return node;
+ }
+ auto copy = maybeCopy.Cast();
+ if (copy.World().Ref().Type() != TExprNode::World) {
+ return node;
+ }
+
+ if (copy.Ref().HasResult()) {
+ return node;
+ }
+
+ TYtPath path = copy.Input().Item(0).Paths().Item(0);
+ if (!path.Table().Maybe<TYtOutput>()) {
+ return node;
+ }
+
+ const auto parentsMap = getParents();
+ auto parentsIt = parentsMap->find(copy.Raw());
+ for (auto p: parentsIt->second) {
+ if (TCoLeft::Match(p)) {
+ return node;
+ }
+ }
+
+
+ auto res = path.Table().Cast<TYtOutput>();
+ if (IsUnorderedOutput(srcOut)) {
+ res = Build<TYtOutput>(ctx, res.Pos())
+ .InitFrom(res)
+ .Mode(srcOut.Mode())
+ .Done();
+ }
+ return res;
+ }
+
+ TMaybeNode<TExprBase> PushdownReadColumns(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto maybeRead = node.Cast<TCoRight>().Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ return node;
+ }
+
+ auto read = maybeRead.Cast();
+
+ bool hasNewSection = false;
+ TVector<TYtSection> sections;
+ for (auto section: read.Input()) {
+ sections.push_back(PushdownSectionColumns(section, ctx, getParents));
+ if (section.Raw() != sections.back().Raw()) {
+ hasNewSection = true;
+ }
+ }
+
+ if (!hasNewSection) {
+ return node;
+ }
+
+ return Build<TCoRight>(ctx, node.Pos())
+ .Input<TYtReadTable>()
+ .InitFrom(read)
+ .Input()
+ .Add(sections)
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> PushdownOpColumns(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtTransientOpBase>();
+
+ bool hasNewSection = false;
+ TVector<TYtSection> sections;
+ for (auto section: op.Input()) {
+ sections.push_back(PushdownSectionColumns(section, ctx, getParents));
+ if (section.Raw() != sections.back().Raw()) {
+ hasNewSection = true;
+ }
+ }
+
+ if (!hasNewSection) {
+ return node;
+ }
+
+ return TExprBase(ctx.ChangeChild(node.Ref(), TYtTransientOpBase::idx_Input, Build<TYtSectionList>(ctx, op.Input().Pos()).Add(sections).Done().Ptr()));
+ }
+
+ struct TExtendOverSameMapGroupLess {
+ bool operator() (const std::pair<bool, TCoLambda>& left, const std::pair<bool, TCoLambda>& right) const {
+ return std::make_pair(left.first, left.second.Ref().UniqueId()) < std::make_pair(right.first, right.second.Ref().UniqueId());
+ }
+ };
+
+ TMaybeNode<TExprBase> ExtendOverSameMap(TExprBase node, TExprContext& ctx) const {
+ auto extend = node.Cast<TCoExtendBase>();
+
+ // Don't apply to OrderedExtend because input is reordered
+ if (extend.Maybe<TCoOrderedExtend>()) {
+ return node;
+ }
+
+ TVector<TExprBase> retChildren;
+ TVector<TCoFlatMapBase> flatMaps;
+ bool keepOrder = extend.Maybe<TCoMerge>().IsValid();
+ for (auto child : extend) {
+ if (auto maybeFlatMap = child.Maybe<TCoFlatMapBase>()) {
+ auto flatMap = maybeFlatMap.Cast();
+ keepOrder = keepOrder && flatMap.Maybe<TCoOrderedFlatMap>();
+ auto input = flatMap.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+ if (input.Ref().UseCount() > 2) { // Additional reference is owned by 'input' local var
+ retChildren.push_back(child);
+ } else {
+ flatMaps.push_back(flatMap);
+ }
+ }
+ else {
+ return node;
+ }
+ }
+
+ // group by YAMR input and lambda nodes
+ std::map<std::pair<bool, TCoLambda>, TVector<TCoFlatMapBase>, TExtendOverSameMapGroupLess> grouped;
+
+ for (auto flatmap : flatMaps) {
+ // All YtRead inputs either YAMR or not, so check only first one. YtOutput cannot be YAMR
+ const bool yamr = flatmap.Input().Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input()
+ .Item(0).Paths().Item(0).Table().Maybe<TYtTable>().RowSpec().Maybe<TCoVoid>().IsValid();
+
+ grouped[std::make_pair(yamr, flatmap.Lambda())].emplace_back(flatmap);
+ }
+
+ size_t singleWithoutContextDependent = 0;
+ TVector<std::pair<bool, TCoLambda>> contextDependentLambdas;
+ for (auto& x : grouped) {
+ if (IsTablePropsDependent(x.first.second.Ref())) {
+ contextDependentLambdas.push_back(x.first);
+ } else if (x.second.size() == 1) {
+ ++singleWithoutContextDependent;
+ }
+ }
+
+ if (grouped.size() == flatMaps.size() ||
+ contextDependentLambdas.size() + singleWithoutContextDependent == grouped.size())
+ {
+ return node;
+ }
+
+ for (auto& x : contextDependentLambdas) {
+ retChildren.insert(retChildren.end(), grouped[x].begin(), grouped[x].end());
+ grouped.erase(x);
+ }
+
+ for (auto& x: grouped) {
+ TExprNode::TListType subChildren;
+
+ for (auto flatMap: x.second) {
+ subChildren.push_back(flatMap.Input().Ptr());
+ }
+
+ auto flatMapInput = ctx.NewCallable(node.Pos(), extend.Ref().Content(), std::move(subChildren));
+
+ retChildren.push_back(TExprBase(ctx.Builder(node.Pos())
+ .Callable(keepOrder ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Add(0, flatMapInput)
+ .Add(1, x.first.second.Ptr())
+ .Seal()
+ .Build()));
+ }
+
+ if (extend.Maybe<TCoMerge>()) {
+ return Build<TCoMerge>(ctx, node.Pos()).Add(retChildren).Done();
+ }
+
+ return Build<TCoExtend>(ctx, node.Pos()).Add(retChildren).Done();
+ }
+
+ static bool IsExtendWithFlatMaps(TExprBase node, bool requireChildFlatMap) {
+ if (!node.Maybe<TCoExtendBase>()) {
+ return false;
+ }
+
+ auto extend = node.Cast<TCoExtendBase>();
+ auto type = extend.Ref().GetTypeAnn();
+ if (type->GetKind() != ETypeAnnotationKind::List ||
+ type->Cast<TListExprType>()->GetItemType()->GetKind() != ETypeAnnotationKind::Struct) {
+ return false;
+ }
+
+ bool hasFlatMap = false;
+ for (auto child : extend) {
+ if (IsYtProviderInput(child)) {
+ continue;
+ }
+
+ if (auto mayFlatMap = child.Maybe<TCoFlatMapBase>()) {
+ if (IsYtProviderInput(mayFlatMap.Cast().Input())) {
+ hasFlatMap = true;
+ continue;
+ }
+ }
+ return false;
+ }
+
+ return !requireChildFlatMap || hasFlatMap;
+ }
+
+ TMaybeNode<TExprBase> FlatMapOverExtend(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto flatMap = node.Cast<TCoFlatMapBase>();
+
+ auto input = flatMap.Input();
+ TVector<TCoInputBase> intermediates;
+ while (input.Maybe<TCoCountBase>() || input.Maybe<TCoFilterNullMembersBase>()) {
+ intermediates.push_back(input.Cast<TCoInputBase>());
+ input = input.Cast<TCoInputBase>().Input();
+ }
+ if (!IsExtendWithFlatMaps(input, true)) {
+ return node;
+ }
+
+ const TParentsMap* parentsMap = getParents();
+ auto parentsIt = parentsMap->find(input.Raw());
+ YQL_ENSURE(parentsIt != parentsMap->cend());
+ if (parentsIt->second.size() > 1) {
+ return node;
+ }
+
+ const bool ordered = flatMap.Maybe<TCoOrderedFlatMap>() && !input.Maybe<TCoExtend>();
+ TExprNode::TListType extendChildren;
+ for (auto child: input.Ref().Children()) {
+ extendChildren.push_back(ctx.Builder(child->Pos())
+ .Callable(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Add(0, child)
+ .Add(1, flatMap.Lambda().Ptr())
+ .Seal()
+ .Build());
+ }
+ TStringBuf extendName = input.Maybe<TCoMerge>()
+ ? TCoMerge::CallableName()
+ : (ordered ? TCoOrderedExtend::CallableName() : TCoExtend::CallableName());
+
+ auto res = ctx.NewCallable(node.Pos(), extendName, std::move(extendChildren));
+ for (auto it = intermediates.rbegin(); it != intermediates.rend(); ++it) {
+ res = ctx.ChangeChild(it->Ref(), TCoInputBase::idx_Input, std::move(res));
+ }
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> TakeOverExtend(TExprBase node, TExprContext& ctx) const {
+ auto take = node.Cast<TCoTake>();
+
+ if (!IsExtendWithFlatMaps(take.Input(), true)) {
+ return node;
+ }
+
+ TExprNode::TListType extendChildren;
+ for (auto child : take.Input().Ref().Children()) {
+ extendChildren.push_back(ctx.Builder(child->Pos())
+ .Callable(TCoTake::CallableName())
+ .Add(0, child)
+ .Add(1, take.Count().Ptr())
+ .Seal()
+ .Build());
+ }
+
+ return Build<TCoLimit>(ctx, node.Pos())
+ .Input(ctx.NewCallable(node.Pos(), take.Input().Ref().Content(), std::move(extendChildren)))
+ .Count(take.Count())
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> ExtendOverDqWrap(TExprBase node, TExprContext& ctx) const {
+ auto extend = node.Cast<TCoExtendBase>();
+ const TExprNode* flags = nullptr;
+ const TExprNode* token = nullptr;
+ TString cluster;
+ const TExprNode* world = nullptr;
+ bool first = true;
+ TVector<TYtPath> paths;
+ for (auto child: extend) {
+ if (!TDqReadWrapBase::Match(child.Raw())) {
+ return node;
+ }
+ auto dqReadWrap = child.Cast<TDqReadWrapBase>();
+ if (!dqReadWrap.Input().Maybe<TYtReadTable>()) {
+ return node;
+ }
+ auto ytRead = dqReadWrap.Input().Cast<TYtReadTable>();
+ if (ytRead.Input().Size() != 1 || ytRead.Input().Item(0).Settings().Size() != 0) {
+ return node;
+ }
+
+ if (first) {
+ flags = dqReadWrap.Flags().Raw();
+ token = dqReadWrap.Token().Raw();
+ cluster = ytRead.DataSource().Cluster().StringValue();
+ world = ytRead.World().Raw();
+ first = false;
+ } else if (flags != dqReadWrap.Flags().Raw() || token != dqReadWrap.Token().Raw() || cluster != ytRead.DataSource().Cluster().Value() || world != ytRead.World().Raw()) {
+ return node;
+ }
+ paths.insert(paths.end(), ytRead.Input().Item(0).Paths().begin(), ytRead.Input().Item(0).Paths().end());
+ }
+ auto dqReadWrap = extend.Arg(0);
+ auto newRead = Build<TYtReadTable>(ctx, extend.Pos())
+ .InitFrom(dqReadWrap.Cast<TDqReadWrapBase>().Input().Cast<TYtReadTable>())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+
+ return TExprBase(ctx.ChangeChild(dqReadWrap.Ref(), TDqReadWrapBase::idx_Input, std::move(newRead)));
+ }
+
+ TMaybeNode<TExprBase> DirectRowInFlatMap(TExprBase node, TExprContext& ctx) const {
+ if (State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS)) {
+ return node;
+ }
+
+ auto flatMap = node.Cast<TCoFlatMapBase>();
+
+ const auto& outItemType = GetSeqItemType(*flatMap.Ref().GetTypeAnn());
+ if (outItemType.GetKind() != ETypeAnnotationKind::Struct) {
+ return node;
+ }
+
+ auto input = flatMap.Input();
+ TVector<TCoInputBase> intermediates;
+ while (input.Maybe<TCoCountBase>() || input.Maybe<TCoFilterNullMembersBase>()) {
+ intermediates.push_back(input.Cast<TCoInputBase>());
+ input = input.Cast<TCoInputBase>().Input();
+ }
+ if (!IsExtendWithFlatMaps(input, false)) {
+ return node;
+ }
+
+ TNodeSet nodesToOptimize;
+ TProcessedNodesSet processedNodes;
+ auto originalArg = flatMap.Lambda().Args().Arg(0).Raw();
+ VisitExpr(flatMap.Lambda().Ptr(), [&nodesToOptimize, &processedNodes, originalArg](const TExprNode::TPtr& node) {
+ if (TCoTablePath::Match(node.Get()) && (node->ChildrenSize() == 0 || node->Child(0)->Child(0) == originalArg)) {
+ nodesToOptimize.insert(node.Get());
+ }
+ else if (TCoTableRecord::Match(node.Get()) && (node->ChildrenSize() == 0 || node->Child(0)->Child(0) == originalArg)) {
+ nodesToOptimize.insert(node.Get());
+ }
+ else if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ // move TablePath/TableRecord if any
+ const auto& extend = input.Ref();
+ const bool ordered = flatMap.Maybe<TCoOrderedFlatMap>() && !input.Maybe<TCoExtend>();
+ TExprNode::TListType updatedExtendInputs;
+ for (auto& x : extend.Children()) {
+ auto updatedInput = ctx.Builder(flatMap.Pos())
+ .Callable(ordered ? "OrderedMap" : "Map")
+ .Add(0, x)
+ .Lambda(1)
+ .Param("row")
+ .Callable("AddMember")
+ .Callable(0, "AddMember")
+ .Arg(0, "row")
+ .Atom(1, "_yql_table_path")
+ .Callable(2, "TablePath")
+ .Callable(0, "DependsOn")
+ .Arg(0, "row")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Atom(1, "_yql_table_record")
+ .Callable(2, "TableRecord")
+ .Callable(0, "DependsOn")
+ .Arg(0, "row")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ updatedExtendInputs.push_back(updatedInput);
+ }
+
+ TStringBuf extendName = input.Maybe<TCoMerge>()
+ ? TCoMerge::CallableName()
+ : (ordered ? TCoOrderedExtend::CallableName() : TCoExtend::CallableName());
+
+ auto newInput = ctx.NewCallable(extend.Pos(), extendName, std::move(updatedExtendInputs));
+ for (auto it = intermediates.rbegin(); it != intermediates.rend(); ++it) {
+ newInput = ctx.ChangeChild(it->Ref(), TCoInputBase::idx_Input, std::move(newInput));
+ }
+ auto arg = ctx.NewArgument(flatMap.Pos(), "row");
+ TExprNode::TPtr newBody = flatMap.Lambda().Body().Ptr();
+
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ auto status = OptimizeExpr(newBody, newBody, [&](const TExprNode::TPtr& input, TExprContext& ctx)->TExprNode::TPtr {
+ if (nodesToOptimize.find(input.Get()) != nodesToOptimize.end()) {
+ if (input->IsCallable("TablePath")) {
+ return ctx.NewCallable(input->Pos(), "Member", { arg, ctx.NewAtom(input->Pos(), "_yql_table_path") });
+ }
+
+ if (input->IsCallable("TableRecord")) {
+ return ctx.NewCallable(input->Pos(), "Member", { arg, ctx.NewAtom(input->Pos(), "_yql_table_record") });
+ }
+ }
+
+ return input;
+ }, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ newBody = ctx.ReplaceNode(std::move(newBody), flatMap.Lambda().Args().Arg(0).Ref(), arg);
+ auto newLambda = ctx.NewLambda(flatMap.Pos(), ctx.NewArguments(flatMap.Pos(), { arg }), std::move(newBody));
+ auto res = ctx.NewCallable(flatMap.Pos(), ordered ? "OrderedFlatMap" : "FlatMap", { std::move(newInput), std::move(newLambda) });
+
+ res = ctx.Builder(flatMap.Pos())
+ .Callable(ordered ? "OrderedMap" : "Map")
+ .Add(0, res)
+ .Lambda(1)
+ .Param("row")
+ .Callable("ForceRemoveMember")
+ .Callable(0, "ForceRemoveMember")
+ .Arg(0, "row")
+ .Atom(1, "_yql_table_path")
+ .Seal()
+ .Atom(1, "_yql_table_record")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> OutputInFlatMap(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() != 1) {
+ return node;
+ }
+
+ TMaybeNode<TCoLambda> maybeOpLambda;
+ size_t opLambdaIdx = 0;
+ if (auto mapReduce = op.Maybe<TYtMapReduce>()) {
+ maybeOpLambda = mapReduce.Mapper().Maybe<TCoLambda>();
+ opLambdaIdx = TYtMapReduce::idx_Mapper;
+ } else if (auto reduce = op.Maybe<TYtReduce>()) {
+ maybeOpLambda = reduce.Reducer();
+ opLambdaIdx = TYtReduce::idx_Reducer;
+ } else {
+ maybeOpLambda = op.Maybe<TYtMap>().Mapper();
+ opLambdaIdx = TYtMap::idx_Mapper;
+ }
+ if (!maybeOpLambda) {
+ return node;
+ }
+ auto opLambda = maybeOpLambda.Cast();
+
+ auto maybeFlatMap = GetFlatMapOverInputStream(opLambda);
+ if (!maybeFlatMap) {
+ return node;
+ }
+
+ auto flatMap = maybeFlatMap.Cast();
+ TCoLambda lambda = flatMap.Lambda();
+
+ auto finalNode = lambda.Body();
+ const bool isListIf = finalNode.Maybe<TCoListIf>().IsValid();
+ const bool isAsList1 = finalNode.Maybe<TCoAsList>() && finalNode.Cast<TCoAsList>().ArgCount() == 1;
+ const bool removeLastOp = finalNode.Maybe<TCoToList>() ||
+ finalNode.Maybe<TCoForwardList>() || finalNode.Maybe<TCoIterator>();
+ if (!isListIf && !isAsList1 && !removeLastOp) {
+ return node;
+ }
+
+ TNodeOnNodeOwnedMap nodesToOptimize;
+ if (isAsList1) {
+ nodesToOptimize[flatMap.Raw()] = Build<TCoFlatMapBase>(ctx, flatMap.Pos())
+ .CallableName(flatMap.CallableName())
+ .Input(flatMap.Input())
+ .Lambda()
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(ctx.NewCallable(lambda.Pos(), "Just", {finalNode.Ref().HeadPtr()})))
+ .With(lambda.Args().Arg(0), "stream")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ if (isListIf) {
+ nodesToOptimize[flatMap.Raw()] = Build<TCoFlatMapBase>(ctx, flatMap.Pos())
+ .CallableName(flatMap.CallableName())
+ .Input(flatMap.Input())
+ .Lambda()
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(ctx.NewCallable(lambda.Pos(), "OptionalIf", {finalNode.Ref().HeadPtr(), finalNode.Ref().TailPtr()})))
+ .With(lambda.Args().Arg(0), "stream")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ if (removeLastOp) {
+ nodesToOptimize[flatMap.Raw()] = Build<TCoFlatMapBase>(ctx, flatMap.Pos())
+ .CallableName(flatMap.CallableName())
+ .Input(flatMap.Input())
+ .Lambda()
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(finalNode.Ref().HeadPtr()))
+ .With(lambda.Args().Arg(0), "stream")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ TProcessedNodesSet processedNodes;
+ VisitExpr(opLambda.Ptr(), [&processedNodes](const TExprNode::TPtr& node) {
+ if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ TExprNode::TPtr newOpLambda;
+ auto status = RemapExpr(opLambda.Ptr(), newOpLambda, nodesToOptimize, ctx, settings);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ return TExprBase(ctx.ChangeChild(op.Ref(), opLambdaIdx, ctx.DeepCopyLambda(*newOpLambda)));
+ }
+
+ static void CollectEquiJoinLinks(TCoEquiJoinTuple joinTree, TVector<std::pair<TString, TString>>& links,
+ const std::function<bool(TExprBase linkSettings)>& collectPred)
+ {
+ if (!joinTree.LeftScope().Maybe<TCoAtom>()) {
+ CollectEquiJoinLinks(joinTree.LeftScope().Cast<TCoEquiJoinTuple>(), links, collectPred);
+ }
+
+ if (!joinTree.RightScope().Maybe<TCoAtom>()) {
+ CollectEquiJoinLinks(joinTree.RightScope().Cast<TCoEquiJoinTuple>(), links, collectPred);
+ }
+
+ if (collectPred(joinTree.Options())) {
+ YQL_ENSURE(joinTree.LeftKeys().Size() == joinTree.RightKeys().Size());
+ YQL_ENSURE(joinTree.LeftKeys().Size() % 2 == 0);
+ for (ui32 i = 0; i < joinTree.LeftKeys().Size(); i += 2) {
+
+ auto leftKey = FullColumnName(joinTree.LeftKeys().Item(i).Value(), joinTree.LeftKeys().Item(i + 1).Value());
+ auto rightKey = FullColumnName(joinTree.RightKeys().Item(i).Value(), joinTree.RightKeys().Item(i + 1).Value());
+
+ links.emplace_back(leftKey, rightKey);
+ }
+ }
+ }
+
+ struct TRemapTarget {
+ TString Name;
+ const TTypeAnnotationNode* Type;
+ };
+
+ static void ApplyJoinKeyRemapsLeaf(TExprNode::TPtr& keysNode, const THashMap<TStringBuf, THashMap<TStringBuf, TRemapTarget>>& memberRemapsByLabel,
+ TExprContext& ctx)
+ {
+ YQL_ENSURE(keysNode->IsList());
+ TExprNodeList keys = keysNode->ChildrenList();
+ YQL_ENSURE(keys.size() % 2 == 0);
+ for (size_t i = 0; i < keys.size(); i += 2) {
+ TCoAtom label(keys[i]);
+ if (auto it = memberRemapsByLabel.find(label.Value()); it != memberRemapsByLabel.end()) {
+ TCoAtom column(keys[i + 1]);
+ if (auto remapIt = it->second.find(column.Value()); remapIt != it->second.end()) {
+ keys[i + 1] = ctx.NewAtom(column.Pos(), remapIt->second.Name);
+ }
+ }
+ }
+ keysNode = ctx.NewList(keysNode->Pos(), std::move(keys));
+ }
+
+ static void ApplyJoinKeyRemaps(TExprNode::TPtr& joinTree, const THashMap<TStringBuf, THashMap<TStringBuf, TRemapTarget>>& memberRemapsByLabel,
+ TExprContext& ctx)
+ {
+ YQL_ENSURE(joinTree->IsList());
+ TExprNodeList children = joinTree->ChildrenList();
+
+ auto& leftScope = children[TCoEquiJoinTuple::idx_LeftScope];
+ if (!leftScope->IsAtom()) {
+ ApplyJoinKeyRemaps(leftScope, memberRemapsByLabel, ctx);
+ }
+
+ auto& rightScope = children[TCoEquiJoinTuple::idx_RightScope];
+ if (!rightScope->IsAtom()) {
+ ApplyJoinKeyRemaps(rightScope, memberRemapsByLabel, ctx);
+ }
+
+ auto& leftKeys = children[TCoEquiJoinTuple::idx_LeftKeys];
+ ApplyJoinKeyRemapsLeaf(leftKeys, memberRemapsByLabel, ctx);
+
+ auto& rightKeys = children[TCoEquiJoinTuple::idx_RightKeys];
+ ApplyJoinKeyRemapsLeaf(rightKeys, memberRemapsByLabel, ctx);
+
+ joinTree = ctx.NewList(joinTree->Pos(), std::move(children));
+ }
+
+ TMaybeNode<TExprBase> ConvertToCommonTypeForForcedMergeJoin(TExprBase node, TExprContext& ctx) const {
+ auto equiJoin = node.Cast<TCoEquiJoin>();
+ YQL_ENSURE(equiJoin.ArgCount() >= 4);
+
+ bool hasYtInputs = false;
+ THashMap<TStringBuf, size_t> inputPosByLabel;
+ TVector<const TStructExprType*> inputTypes;
+ for (size_t i = 0; i < equiJoin.ArgCount() - 2; ++i) {
+ auto input = equiJoin.Arg(i).Cast<TCoEquiJoinInput>();
+ if (!input.Scope().Maybe<TCoAtom>()) {
+ return node;
+ }
+ TStringBuf label = input.Scope().Cast<TCoAtom>().Value();
+ bool inserted = inputPosByLabel.emplace(label, i).second;
+ YQL_ENSURE(inserted);
+ hasYtInputs = hasYtInputs || IsYtProviderInput(input.List());
+ inputTypes.push_back(GetSeqItemType(*input.List().Ref().GetTypeAnn()).Cast<TStructExprType>());
+ }
+
+ if (!hasYtInputs) {
+ return node;
+ }
+
+ bool forceSortedMerge = false;
+ if (auto force = State_->Configuration->JoinMergeForce.Get()) {
+ forceSortedMerge = *force;
+ }
+
+ auto collectPred = [forceSortedMerge](TExprBase linkSettingsNode) {
+ return forceSortedMerge || GetEquiJoinLinkSettings(linkSettingsNode.Ref()).ForceSortedMerge;
+ };
+
+ TVector<std::pair<TString, TString>> links;
+ CollectEquiJoinLinks(equiJoin.Arg(equiJoin.ArgCount() - 2).Cast<TCoEquiJoinTuple>(), links, collectPred);
+
+ TVector<TStringBuf> joinKeys;
+ for (const auto& [left, right] : links) {
+ joinKeys.push_back(left);
+ joinKeys.push_back(right);
+ }
+ SortUnique(joinKeys);
+
+ TDisjointSets disjointSets(joinKeys.size());
+ for (const auto& [left, right] : links) {
+ size_t leftPos = LowerBound(joinKeys.begin(), joinKeys.end(), left) - joinKeys.begin();
+ size_t rightPos = LowerBound(joinKeys.begin(), joinKeys.end(), right) - joinKeys.begin();
+
+ YQL_ENSURE(leftPos < joinKeys.size());
+ YQL_ENSURE(rightPos < joinKeys.size());
+
+ disjointSets.UnionSets(leftPos, rightPos);
+ }
+
+ TMap<size_t, TSet<size_t>> keySetsByCanonicElement;
+ for (size_t i = 0; i < joinKeys.size(); ++i) {
+ size_t canonicElement = disjointSets.CanonicSetElement(i);
+ keySetsByCanonicElement[canonicElement].insert(i);
+ }
+
+ THashMap<TStringBuf, THashMap<TStringBuf, TRemapTarget>> memberRemapsByLabel;
+ size_t genIdx = 0;
+ TVector<TString> toDrop;
+ for (auto& [_, keySet] : keySetsByCanonicElement) {
+ TVector<const TTypeAnnotationNode*> srcKeyTypes;
+ TVector<TStringBuf> srcKeyNames;
+ TVector<const TStructExprType*> srcInputTypes;
+ for (auto& keyIdx : keySet) {
+ const TStringBuf key = joinKeys[keyIdx];
+ TStringBuf label;
+ TStringBuf column;
+ SplitTableName(key, label, column);
+
+ auto it = inputPosByLabel.find(label);
+ YQL_ENSURE(it != inputPosByLabel.end());
+
+ const size_t inputIdx = it->second;
+ YQL_ENSURE(inputIdx < inputTypes.size());
+
+ auto columnType = inputTypes[inputIdx]->FindItemType(column);
+ YQL_ENSURE(columnType);
+
+ srcKeyNames.push_back(key);
+ srcKeyTypes.push_back(columnType);
+ srcInputTypes.push_back(inputTypes[inputIdx]);
+ }
+
+ // derive common type for all join keys in key set
+ const TTypeAnnotationNode* commonType = UnifyJoinKeyType(equiJoin.Pos(), srcKeyTypes, ctx);
+ YQL_ENSURE(commonType);
+
+ const TTypeAnnotationNode* commonTypeNoOpt = RemoveOptionalType(commonType);
+
+ bool needRemap = !IsDataOrOptionalOfData(commonType);
+ for (size_t i = 0; i < srcKeyNames.size(); ++i) {
+ TStringBuf srcKey = srcKeyNames[i];
+ const TTypeAnnotationNode* srcKeyType = srcKeyTypes[i];
+ const TStructExprType* inputType = srcInputTypes[i];
+
+ if (needRemap || !IsSameAnnotation(*RemoveOptionalType(srcKeyType), *commonTypeNoOpt)) {
+ TStringBuf label;
+ TStringBuf column;
+ SplitTableName(srcKey, label, column);
+
+ TString targetName;
+ do {
+ targetName = TStringBuilder() << "_yql_normalized_join_key" << genIdx++;
+ } while (inputType->FindItem(targetName));
+
+ TRemapTarget target;
+ target.Type = commonType;
+ target.Name = targetName;
+ toDrop.push_back(FullColumnName(label, targetName));
+
+ memberRemapsByLabel[label][column] = target;
+ }
+ }
+ }
+
+ TExprNodeList equiJoinArgs = equiJoin.Ref().ChildrenList();
+ for (auto& [label, remaps] : memberRemapsByLabel) {
+ auto it = inputPosByLabel.find(label);
+ YQL_ENSURE(it != inputPosByLabel.end());
+
+ const size_t inputPos = it->second;
+
+ TExprNode::TPtr& equiJoinInput = equiJoinArgs[inputPos];
+
+ auto row = ctx.NewArgument(equiJoinInput->Pos(), "row");
+ auto body = row;
+ for (auto& [srcColumn, target] : remaps) {
+ auto srcType = inputTypes[inputPos]->FindItemType(srcColumn);
+ YQL_ENSURE(srcType);
+ auto srcValue = ctx.Builder(body->Pos())
+ .Callable("Member")
+ .Add(0, row)
+ .Atom(1, srcColumn)
+ .Seal()
+ .Build();
+ auto targetValue = RemapNonConvertibleMemberForJoin(srcValue->Pos(), srcValue, *srcType, *target.Type, ctx);
+ body = ctx.Builder(body->Pos())
+ .Callable("AddMember")
+ .Add(0, body)
+ .Atom(1, target.Name)
+ .Add(2, targetValue)
+ .Seal()
+ .Build();
+ }
+
+ auto remapLambda = ctx.NewLambda(row->Pos(), ctx.NewArguments(row->Pos(), { row }), std::move(body));
+ equiJoinInput = ctx.Builder(equiJoinInput->Pos())
+ .List()
+ .Callable(0, "OrderedMap")
+ .Add(0, equiJoinInput->HeadPtr())
+ .Add(1, remapLambda)
+ .Seal()
+ .Add(1, equiJoinInput->TailPtr())
+ .Seal()
+ .Build();
+ }
+
+ if (!memberRemapsByLabel.empty()) {
+ ApplyJoinKeyRemaps(equiJoinArgs[equiJoin.ArgCount() - 2], memberRemapsByLabel, ctx);
+ TExprNode::TPtr result = ctx.ChangeChildren(equiJoin.Ref(), std::move(equiJoinArgs));
+
+ auto row = ctx.NewArgument(equiJoin.Pos(), "row");
+ auto removed = row;
+ for (auto column : toDrop) {
+ removed = ctx.Builder(equiJoin.Pos())
+ .Callable("ForceRemoveMember")
+ .Add(0, removed)
+ .Atom(1, column)
+ .Seal()
+ .Build();
+ }
+
+ auto removeLambda = ctx.NewLambda(equiJoin.Pos(), ctx.NewArguments(equiJoin.Pos(), { row }), std::move(removed));
+
+ result = ctx.Builder(equiJoin.Pos())
+ .Callable("OrderedMap")
+ .Add(0, result)
+ .Add(1, removeLambda)
+ .Seal()
+ .Build();
+
+ return result;
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> SelfInnerJoinWithSameKeys(TExprBase node, TExprContext& ctx) const {
+ auto equiJoin = node.Cast<TCoEquiJoin>();
+ // optimize self intersect (inner join) over (filtered) tables with unique keys
+ if (equiJoin.ArgCount() != 4) {
+ return node;
+ }
+
+ auto tree = equiJoin.Arg(2).Cast<TCoEquiJoinTuple>();
+ if (tree.Type().Value() != "Inner") {
+ return node;
+ }
+
+ auto left = equiJoin.Arg(0).Cast<TCoEquiJoinInput>().List();
+ auto right = equiJoin.Arg(1).Cast<TCoEquiJoinInput>().List();
+ TMaybeNode<TExprBase> leftInput;
+ TMaybeNode<TExprBase> rightInput;
+ TMaybe<THashSet<TStringBuf>> leftPassthroughFields;
+ TMaybe<THashSet<TStringBuf>> rightPassthroughFields;
+ TMaybe<TSet<TString>> leftReadFields;
+ TMaybe<TSet<TString>> rightReadFields;
+ TExprNode::TPtr leftPredicate;
+ TExprNode::TPtr leftArg;
+ TExprNode::TPtr leftValue;
+ TExprNode::TPtr rightPredicate;
+ TExprNode::TPtr rightArg;
+ TExprNode::TPtr rightValue;
+
+ if (auto extract = left.Maybe<TCoExtractMembers>()) {
+ leftReadFields.ConstructInPlace();
+ for (auto f: extract.Cast().Members()) {
+ leftReadFields->emplace(f.Value());
+ }
+ left = extract.Cast().Input();
+ }
+
+ if (auto extract = right.Maybe<TCoExtractMembers>()) {
+ rightReadFields.ConstructInPlace();
+ for (auto f: extract.Cast().Members()) {
+ rightReadFields->emplace(f.Value());
+ }
+ right = extract.Cast().Input();
+ }
+
+ if (IsYtProviderInput(left)) {
+ leftInput = left;
+ } else if (auto maybeFlatMap = left.Maybe<TCoFlatMapBase>()) {
+ auto flatMap = maybeFlatMap.Cast();
+ const auto& input = flatMap.Input();
+ if (IsYtProviderInput(input) && IsPassthroughFlatMap(flatMap, &leftPassthroughFields)) {
+ const auto& lambda = flatMap.Lambda();
+ const auto& body = lambda.Body();
+ leftArg = lambda.Args().Arg(0).Ptr();
+ if (body.Maybe<TCoOptionalIf>() || body.Maybe<TCoListIf>()) {
+ leftValue = body.Cast<TCoConditionalValueBase>().Value().Ptr();
+ leftPredicate = body.Cast<TCoConditionalValueBase>().Predicate().Ptr();
+ } else {
+ leftValue = body.Ref().ChildPtr(0);
+ }
+
+ leftInput = input;
+ }
+ }
+
+ if (IsYtProviderInput(right)) {
+ rightInput = right;
+ } else if (auto maybeFlatMap = right.Maybe<TCoFlatMapBase>()) {
+ auto flatMap = maybeFlatMap.Cast();
+ const auto& input = flatMap.Input();
+ if (IsYtProviderInput(input) && IsPassthroughFlatMap(flatMap, &rightPassthroughFields)) {
+ const auto& lambda = flatMap.Lambda();
+ const auto& body = lambda.Body();
+ rightArg = lambda.Args().Arg(0).Ptr();
+ if (body.Maybe<TCoOptionalIf>() || body.Maybe<TCoListIf>()) {
+ rightValue = body.Cast<TCoConditionalValueBase>().Value().Ptr();
+ rightPredicate = body.Cast<TCoConditionalValueBase>().Predicate().Ptr();
+ } else {
+ rightValue = body.Ref().ChildPtr(0);
+ }
+
+ rightInput = input;
+ }
+ }
+
+ if (!leftInput || !rightInput) {
+ return node;
+ }
+
+ auto leftCluster = GetClusterName(leftInput.Cast());
+ auto rightCluster = GetClusterName(rightInput.Cast());
+ if (leftCluster != rightCluster) {
+ return node;
+ }
+
+ // check that key and settings are same
+ if (leftInput.Maybe<TYtOutput>() && rightInput.Maybe<TYtOutput>()) {
+ if (leftInput.Cast().Raw() != rightInput.Cast().Raw()) {
+ return node;
+ }
+ }
+ else {
+ auto leftSection = leftInput.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0);
+ auto rightSection = rightInput.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0);
+ if (!leftSection || !rightSection) {
+ return node;
+ }
+
+ if (!EqualSettingsExcept(leftSection.Settings().Cast().Ref(), rightSection.Settings().Cast().Ref(), EYtSettingType::Unordered)) {
+ return node;
+ }
+
+ // only one table
+ if (leftSection.Cast().Paths().Size() > 1 || rightSection.Cast().Paths().Size() > 1) {
+ return node;
+ }
+
+ auto leftPath = leftSection.Cast().Paths().Item(0);
+ auto rightPath = rightSection.Cast().Paths().Item(0);
+
+ if (leftPath.Table().Raw() != rightPath.Table().Raw()) {
+ return node;
+ }
+
+ if (leftPath.Ranges().Raw() != rightPath.Ranges().Raw()) {
+ return node;
+ }
+ }
+
+ TYtTableBaseInfo::TPtr tableInfo = GetInputTableInfos(leftInput.Cast()).front();
+ if (tableInfo->IsUnordered || !tableInfo->RowSpec || !tableInfo->RowSpec->IsSorted() || !tableInfo->RowSpec->UniqueKeys) {
+ return node;
+ }
+
+ // check keys
+ THashSet<TStringBuf> joinColumns;
+ for (ui32 i = 0; i < tree.LeftKeys().Size(); i += 2) {
+ const auto& leftColumn = tree.LeftKeys().Item(i + 1).Value();
+ const auto& rightColumn = tree.RightKeys().Item(i + 1).Value();
+ if (leftColumn != rightColumn) {
+ return node;
+ }
+
+ joinColumns.emplace(leftColumn);
+ }
+
+ if (tableInfo->RowSpec->SortedBy.size() != joinColumns.size()) {
+ return node;
+ }
+
+ for (auto& field : tableInfo->RowSpec->SortedBy) {
+ if (leftPassthroughFields && !leftPassthroughFields->contains(field)) {
+ return node;
+ }
+
+ if (rightPassthroughFields && !rightPassthroughFields->contains(field)) {
+ return node;
+ }
+
+ if (!joinColumns.contains(field)) {
+ return node;
+ }
+ }
+
+ // now we could rewrite join with single flatmap
+ TExprBase commonInput = leftInput.Cast();
+ if (leftInput.Maybe<TCoRight>()) {
+ auto read = leftInput.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Cast();
+ if (!leftReadFields) {
+ if (auto columns = leftInput.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Paths().Item(0).Columns().Maybe<TCoAtomList>()) {
+ leftReadFields.ConstructInPlace();
+ for (auto f: columns.Cast()) {
+ leftReadFields->emplace(f.Value());
+ }
+ }
+ }
+ if (!rightReadFields) {
+ if (auto columns = rightInput.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Paths().Item(0).Columns().Maybe<TCoAtomList>()) {
+ rightReadFields.ConstructInPlace();
+ for (auto f: columns.Cast()) {
+ rightReadFields->emplace(f.Value());
+ }
+ }
+ }
+ if (leftReadFields && rightReadFields) {
+ TSet<TString> commonFields;
+ commonFields.insert(leftReadFields->begin(), leftReadFields->end());
+ commonFields.insert(rightReadFields->begin(), rightReadFields->end());
+ auto columnsBuilder = Build<TCoAtomList>(ctx, node.Pos());
+ for (auto f: commonFields) {
+ columnsBuilder.Add().Value(f).Build();
+ }
+ commonInput = Build<TCoRight>(ctx, leftInput.Cast().Pos())
+ .Input<TYtReadTable>()
+ .World(read.World())
+ .DataSource(read.DataSource())
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .InitFrom(read.Input().Item(0).Paths().Item(0))
+ .Columns(columnsBuilder.Done())
+ .Build()
+ .Build()
+ .Settings(read.Input().Item(0).Settings())
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ }
+
+ auto joinLambdaArg = ctx.NewArgument(node.Pos(), "read");
+ auto leftReadArg = joinLambdaArg;
+ if (leftReadFields) {
+ leftReadArg = FilterByFields(node.Pos(), leftReadArg, *leftReadFields, ctx, true);
+ }
+
+ auto rightReadArg = joinLambdaArg;
+ if (rightReadFields) {
+ rightReadArg = FilterByFields(node.Pos(), rightReadArg, *rightReadFields, ctx, true);
+ }
+
+ auto truth = ctx.NewCallable(node.Pos(), "Bool", { ctx.NewAtom(node.Pos(), "true") });
+ auto leftPrefix = equiJoin.Arg(0).Cast<TCoEquiJoinInput>().Scope().Cast<TCoAtom>().Value();
+ auto rightPrefix = equiJoin.Arg(1).Cast<TCoEquiJoinInput>().Scope().Cast<TCoAtom>().Value();
+ auto joinLambdaBody = ctx.Builder(node.Pos())
+ .Callable("OptionalIf")
+ .Callable(0, "And")
+ .Add(0, leftPredicate ? ctx.ReplaceNode(std::move(leftPredicate), *leftArg, leftReadArg) : truth)
+ .Add(1, rightPredicate ? ctx.ReplaceNode(std::move(rightPredicate), *rightArg, rightReadArg) : truth)
+ .Seal()
+ .Callable(1, "FlattenMembers")
+ .List(0)
+ .Atom(0, TString(leftPrefix) + ".")
+ .Add(1, leftValue ? ctx.ReplaceNode(std::move(leftValue), *leftArg, leftReadArg) : leftReadArg)
+ .Seal()
+ .List(1)
+ .Atom(0, TString(rightPrefix) + ".")
+ .Add(1, rightValue ? ctx.ReplaceNode(std::move(rightValue), *rightArg, rightReadArg) : rightReadArg)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ auto joinLambda = ctx.NewLambda(node.Pos(), ctx.NewArguments(node.Pos(), { joinLambdaArg }), std::move(joinLambdaBody));
+ auto joinFlatMap = Build<TCoFlatMap>(ctx, node.Pos())
+ .Input(commonInput)
+ .Lambda(joinLambda)
+ .Done();
+
+ auto ret = joinFlatMap.Ptr();
+
+ // have to apply renames
+ auto settings = equiJoin.Arg(equiJoin.ArgCount() - 1);
+ auto renameMap = LoadJoinRenameMap(settings.Ref());
+
+ if (!renameMap.empty()) {
+ const TStructExprType* itemType = nullptr;
+ if (auto type = GetSequenceItemType(node, false, ctx)) {
+ itemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ ret = ctx.Builder(node.Pos())
+ .Callable("Map")
+ .Add(0, ret)
+ .Add(1, BuildJoinRenameLambda(node.Pos(), renameMap, *itemType, ctx).Ptr())
+ .Seal()
+ .Build();
+ }
+
+ return TExprBase(ret);
+ }
+
+ TMaybeNode<TExprBase> Unordered(TExprBase node, TExprContext& ctx) const {
+ const auto unordered = node.Cast<TCoUnorderedBase>();
+ if (const auto tableContent = unordered.Input().Maybe<TYtTableContent>()) {
+ if (const auto out = tableContent.Input().Maybe<TYtOutput>()) {
+ if (IsUnorderedOutput(out.Cast()))
+ return tableContent;
+ return Build<TYtTableContent>(ctx, node.Pos())
+ .InitFrom(tableContent.Cast())
+ .Input<TYtOutput>()
+ .InitFrom(out.Cast())
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Done();
+ } else if (const auto read = tableContent.Input().Maybe<TYtReadTable>()) {
+ TVector<TYtSection> sections;
+ for (auto section: read.Cast().Input()) {
+ sections.push_back(MakeUnorderedSection<true>(section, ctx));
+ }
+ return Build<TYtTableContent>(ctx, node.Pos())
+ .InitFrom(tableContent.Cast())
+ .Input<TYtReadTable>()
+ .InitFrom(read.Cast())
+ .Input()
+ .Add(sections)
+ .Build()
+ .Build()
+ .Done();
+ }
+ } else if (const auto input = unordered.Input(); IsYtProviderInput(input, true)) {
+ if (const auto out = input.Maybe<TYtOutput>()) {
+ if (unordered.Maybe<TCoUnorderedSubquery>() && out.Cast().Operation().Maybe<TYtSort>()) {
+ if (!WarnUnroderedSubquery(node.Ref(), ctx)) {
+ return {};
+ }
+ }
+ if (IsUnorderedOutput(out.Cast())) {
+ // Already unordered. Just drop
+ return input;
+ }
+ return Build<TYtOutput>(ctx, node.Pos())
+ .InitFrom(out.Cast())
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered), TNodeFlags::Default)
+ .Build()
+ .Done();
+ } else if (const auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ TExprNode::TListType sections(read.Cast().Input().Size());
+ for (auto i = 0U; i < sections.size(); ++i) {
+ sections[i] = MakeUnorderedSection<true>(read.Cast().Input().Item(i), ctx).Ptr();
+ }
+
+ return Build<TCoRight>(ctx, node.Pos())
+ .Input<TYtReadTable>()
+ .InitFrom(read.Cast())
+ .Input()
+ .Add(std::move(sections))
+ .Build()
+ .Build()
+ .Done();
+ }
+ } else if (const auto maybeYtRead = unordered.Input().Maybe<TDqReadWrapBase>().Input().Maybe<TYtReadTable>()) {
+ const auto ytRead = maybeYtRead.Cast();
+ TExprNode::TListType sections(ytRead.Input().Size());
+ for (auto i = 0U; i < sections.size(); ++i) {
+ sections[i] = MakeUnorderedSection<true>(ytRead.Input().Item(i), ctx).Ptr();
+ }
+
+ auto updatedRead = Build<TYtReadTable>(ctx, ytRead.Pos())
+ .InitFrom(ytRead)
+ .Input()
+ .Add(std::move(sections))
+ .Build()
+ .Done().Ptr();
+
+ return TExprBase(ctx.ChangeChild(unordered.Input().Ref(), TDqReadWrapBase::idx_Input, std::move(updatedRead)));
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> ShuffleByKeys(TExprBase node, TExprContext& ctx) const {
+ auto list = node.Cast<TCoInputBase>().Input();
+ if (!IsYtProviderInput(list)) {
+ return node;
+ }
+
+ auto shuffle = node.Cast<TCoShuffleByKeys>();
+
+ return Build<TCoPartitionsByKeys>(ctx, node.Pos())
+ .Input(list)
+ .SortDirections<TCoVoid>()
+ .Build()
+ .SortKeySelectorLambda<TCoVoid>()
+ .Build()
+ .KeySelectorLambda(shuffle.KeySelectorLambda())
+ .ListHandlerLambda<TCoLambda>()
+ .Args({ TStringBuf("stream") })
+ .Body<TCoForwardList>()
+ .Stream<TExprApplier>()
+ .Apply(shuffle.ListHandlerLambda().Body())
+ .With(shuffle.ListHandlerLambda().Args().Arg(0), TStringBuf("stream"))
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+private:
+ TYtState::TPtr State_;
+};
+
+NYql::NNodes::TMaybeNode<NYql::NNodes::TExprBase> TYtLogicalOptProposalTransformer::CountAggregate(TExprBase node, TExprContext& ctx) const {
+ auto aggregate = node.Cast<TCoAggregate>();
+
+ auto input = aggregate.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ return TAggregateExpander::CountAggregateRewrite(aggregate, ctx, State_->Types->UseBlocks);
+}
+
+THolder<IGraphTransformer> CreateYtLogicalOptProposalTransformer(TYtState::TPtr state) {
+ return THolder(new TYtLogicalOptProposalTransformer(state));
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
new file mode 100644
index 0000000000..49be1369be
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.cpp
@@ -0,0 +1,535 @@
+#include "yql_yt_mkql_compiler.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/providers/yt/lib/skiff/yql_skiff_schema.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/mkql/yql_type_mkql.h>
+#include <ydb/library/yql/minikql/mkql_program_builder.h>
+#include <ydb/library/yql/minikql/mkql_node_cast.h>
+#include <ydb/library/yql/minikql/defs.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/utils/log/context.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <util/generic/yexception.h>
+#include <util/generic/xrange.h>
+#include <util/string/cast.h>
+
+namespace NYql {
+
+using namespace NKikimr;
+using namespace NKikimr::NMiniKQL;
+using namespace NNodes;
+
+TRuntimeNode BuildTableContentCall(TStringBuf callName,
+ TType* outItemType,
+ TStringBuf clusterName,
+ const TExprNode& input,
+ const TMaybe<ui64>& itemsCount,
+ NCommon::TMkqlBuildContext& ctx,
+ bool forceColumns,
+ const THashSet<TString>& extraSysColumns,
+ bool forceKeyColumns)
+{
+ forceColumns = forceColumns || forceKeyColumns;
+ TType* const strType = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<char*>::Id);
+ TType* const boolType = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<bool>::Id);
+ TType* const ui64Type = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<ui64>::Id);
+ TType* const ui32Type = ctx.ProgramBuilder.NewDataType(NUdf::TDataType<ui32>::Id);
+ TType* const tupleTypeTables = ctx.ProgramBuilder.NewTupleType({strType, boolType, strType, ui64Type, ui64Type, boolType, ui32Type});
+ TType* const listTypeGroup = ctx.ProgramBuilder.NewListType(tupleTypeTables);
+
+ const TExprNode* settings = nullptr;
+ TMaybe<TSampleParams> sampling;
+ TVector<TRuntimeNode> groups;
+ if (input.IsCallable(TYtOutput::CallableName())) {
+ YQL_ENSURE(!forceKeyColumns);
+ auto outTableInfo = TYtOutTableInfo(GetOutTable(TExprBase(&input)));
+ YQL_ENSURE(outTableInfo.Stat, "Table " << outTableInfo.Name.Quote() << " has no Stat");
+ auto richYPath = NYT::TRichYPath(outTableInfo.Name);
+ if (forceColumns && outTableInfo.RowSpec->HasAuxColumns()) {
+ NYT::TSortColumns columns;
+ for (auto& item: outTableInfo.RowSpec->GetType()->GetItems()) {
+ columns.Add(TString{item->GetName()});
+ }
+ richYPath.Columns(columns);
+ }
+ TString spec;
+ if (!extraSysColumns.empty()) {
+ auto specNode = outTableInfo.GetCodecSpecNode();
+ auto structType = AS_TYPE(TStructType, outItemType);
+ NYT::TNode columns;
+ for (auto& col: extraSysColumns) {
+ const auto fullName = TString(YqlSysColumnPrefix).append(col);
+ if (!structType->FindMemberIndex(fullName)) {
+ columns.Add(col);
+ outItemType = ctx.ProgramBuilder.NewStructType(outItemType, fullName, ctx.ProgramBuilder.NewDataType(GetSysColumnTypeId(col)));
+ }
+ }
+ if (!columns.IsUndefined()) {
+ specNode[YqlSysColumnPrefix] = std::move(columns);
+ }
+ spec = NYT::NodeToCanonicalYsonString(specNode);
+ } else {
+ spec = outTableInfo.RowSpec->ToYsonString();
+ }
+ groups.push_back(
+ ctx.ProgramBuilder.NewList(tupleTypeTables, {ctx.ProgramBuilder.NewTuple(tupleTypeTables, {
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(NYT::PathToNode(richYPath))),
+ ctx.ProgramBuilder.NewDataLiteral(true),
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(spec),
+ ctx.ProgramBuilder.NewDataLiteral(outTableInfo.Stat->ChunkCount),
+ ctx.ProgramBuilder.NewDataLiteral(outTableInfo.Stat->RecordsCount),
+ ctx.ProgramBuilder.NewDataLiteral(false),
+ ctx.ProgramBuilder.NewDataLiteral(ui32(0)),
+ })})
+ );
+ }
+ else {
+ auto sectionList = TYtSectionList(&input);
+ TVector<TType*> sectionTypes;
+ bool rebuildType = false;
+ for (size_t i: xrange(sectionList.Size())) {
+ auto section = sectionList.Item(i);
+ TType* secType = outItemType;
+ if (sectionList.Size() > 1) {
+ const auto varType = AS_TYPE(TVariantType, outItemType);
+ const auto tupleType = AS_TYPE(TTupleType, varType->GetUnderlyingType());
+ secType = tupleType->GetElementType(i);
+ }
+ TVector<TRuntimeNode> tableTuples;
+ TVector<TString> columns;
+ if (forceColumns) {
+ for (auto& colType: section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ columns.push_back(ToString(colType->GetName()));
+ }
+ }
+
+ TStructType* structType = AS_TYPE(TStructType, secType);
+ if (forceKeyColumns) {
+ TMap<TString, const TTypeAnnotationNode*> extraKeyColumns;
+ for (auto p: section.Paths()) {
+ TYtPathInfo pathInfo(p);
+ if (!pathInfo.Ranges || !pathInfo.Table->RowSpec || !pathInfo.Table->RowSpec->IsSorted()) {
+ continue;
+ }
+ size_t usedKeyPrefix = pathInfo.Ranges->GetUsedKeyPrefixLength();
+ for (size_t i = 0; i < usedKeyPrefix; ++i) {
+ TString key = pathInfo.Table->RowSpec->SortedBy[i];
+ if (!structType->FindMemberIndex(key)) {
+ auto itemType = pathInfo.Table->RowSpec->GetType()->FindItemType(key);
+ YQL_ENSURE(itemType);
+
+ auto it = extraKeyColumns.find(key);
+ if (it == extraKeyColumns.end()) {
+ extraKeyColumns[key] = itemType;
+ } else {
+ YQL_ENSURE(IsSameAnnotation(*it->second, *itemType),
+ "Extra key columns should be of same type in all paths");
+ }
+ }
+ }
+ }
+
+ for (auto& [key, keyType] : extraKeyColumns) {
+ for (auto p: section.Paths()) {
+ TYtPathInfo pathInfo(p);
+ auto currKeyType = pathInfo.Table->RowSpec->GetType()->FindItemType(key);
+ YQL_ENSURE(currKeyType,
+ "Column " << key <<
+ " is used only in key filter in one YtPath and missing in another YPath in same section");
+ YQL_ENSURE(IsSameAnnotation(*keyType, *currKeyType),
+ "Extra key columns should be of same type in all paths");
+ }
+
+ secType = ctx.ProgramBuilder.NewStructType(secType, key,
+ NCommon::BuildType(section.Ref(), *keyType, ctx.ProgramBuilder));
+ rebuildType = true;
+ }
+
+ for (auto& k : extraKeyColumns) {
+ columns.push_back(k.first);
+ }
+ }
+
+ NYT::TNode sysColumns;
+ for (auto& col: extraSysColumns) {
+ const auto fullName = TString(YqlSysColumnPrefix).append(col);
+ if (!structType->FindMemberIndex(fullName)) {
+ sysColumns.Add(col);
+ secType = ctx.ProgramBuilder.NewStructType(secType, fullName, ctx.ProgramBuilder.NewDataType(GetSysColumnTypeId(col)));
+ rebuildType = true;
+ }
+ }
+ sectionTypes.push_back(secType);
+ for (auto col: NYql::GetSettingAsColumnList(section.Settings().Ref(), EYtSettingType::SysColumns)) {
+ sysColumns.Add(col);
+ }
+
+ for (auto p: section.Paths()) {
+ TYtPathInfo pathInfo(p);
+ YQL_ENSURE(pathInfo.Table->Stat, "Table " << pathInfo.Table->Name.Quote() << " has no Stat");
+ // Table may have aux columns. Exclude them by specifying explicit columns from the type
+ if (forceColumns && pathInfo.Table->RowSpec && (forceKeyColumns || !pathInfo.HasColumns())) {
+ pathInfo.SetColumns(columns);
+ }
+ TString spec;
+ if (!sysColumns.IsUndefined()) {
+ auto specNode = pathInfo.GetCodecSpecNode();
+ specNode[YqlSysColumnPrefix] = sysColumns;
+ spec = NYT::NodeToCanonicalYsonString(specNode);
+ } else {
+ spec = pathInfo.GetCodecSpecStr();
+ }
+
+ TVector<TRuntimeNode> tupleItems;
+ NYT::TRichYPath richTPath(pathInfo.Table->Name);
+ pathInfo.FillRichYPath(richTPath);
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(NYT::PathToNode(richTPath))));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(pathInfo.Table->IsTemp));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(spec));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(pathInfo.Table->Stat->ChunkCount));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(pathInfo.Table->Stat->RecordsCount));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(pathInfo.Table->IsAnonymous));
+ tupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(pathInfo.Table->Epoch.GetOrElse(0)));
+
+ tableTuples.push_back(ctx.ProgramBuilder.NewTuple(tupleTypeTables, tupleItems));
+ }
+ groups.push_back(ctx.ProgramBuilder.NewList(tupleTypeTables, tableTuples));
+ // All sections have the same sampling settings
+ sampling = GetSampleParams(section.Settings().Ref());
+ }
+ if (sectionList.Size() == 1) {
+ settings = sectionList.Item(0).Settings().Raw();
+ if (rebuildType) {
+ outItemType = sectionTypes.front();
+ }
+ } else if (rebuildType) {
+ outItemType = ctx.ProgramBuilder.NewVariantType(ctx.ProgramBuilder.NewTupleType(sectionTypes));
+ }
+ }
+
+ TVector<TRuntimeNode> samplingTupleItems;
+ if (sampling) {
+ samplingTupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(sampling->Percentage));
+ samplingTupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(sampling->Repeat));
+ bool isSystemSampling = sampling->Mode == EYtSampleMode::System;
+ samplingTupleItems.push_back(ctx.ProgramBuilder.NewDataLiteral(isSystemSampling));
+ }
+
+ auto outListType = ctx.ProgramBuilder.NewListType(outItemType);
+
+ TCallableBuilder call(ctx.ProgramBuilder.GetTypeEnvironment(), callName, outListType);
+
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(clusterName)); // cluster name
+ call.Add(ctx.ProgramBuilder.NewList(listTypeGroup, groups));
+ call.Add(ctx.ProgramBuilder.NewTuple(samplingTupleItems));
+
+ if (itemsCount) {
+ call.Add(ctx.ProgramBuilder.NewTuple({ctx.ProgramBuilder.NewDataLiteral(*itemsCount)}));
+ } else {
+ call.Add(ctx.ProgramBuilder.NewEmptyTuple());
+ }
+
+ auto res = TRuntimeNode(call.Build(), false);
+
+ if (settings) {
+ for (auto child: settings->Children()) {
+ switch (FromString<EYtSettingType>(child->Child(0)->Content())) {
+ case EYtSettingType::Take:
+ res = ctx.ProgramBuilder.Take(res, NCommon::MkqlBuildExpr(*child->Child(1), ctx));
+ break;
+ case EYtSettingType::Skip:
+ res = ctx.ProgramBuilder.Skip(res, NCommon::MkqlBuildExpr(*child->Child(1), ctx));
+ break;
+ case EYtSettingType::Sample:
+ case EYtSettingType::DirectRead:
+ case EYtSettingType::KeyFilter:
+ case EYtSettingType::KeyFilter2:
+ case EYtSettingType::Unordered:
+ case EYtSettingType::NonUnique:
+ case EYtSettingType::SysColumns:
+ break;
+ default:
+ YQL_LOG_CTX_THROW yexception() << "Unsupported table content setting " << TString{child->Child(0)->Content()}.Quote();
+ }
+ }
+ }
+
+ return res;
+}
+
+TRuntimeNode BuildTableContentCall(TType* outItemType,
+ TStringBuf clusterName,
+ const TExprNode& input,
+ const TMaybe<ui64>& itemsCount,
+ NCommon::TMkqlBuildContext& ctx,
+ bool forceColumns,
+ const THashSet<TString>& extraSysColumns,
+ bool forceKeyColumns)
+{
+ return BuildTableContentCall(TYtTableContent::CallableName(), outItemType, clusterName, input, itemsCount, ctx, forceColumns, extraSysColumns, forceKeyColumns);
+}
+
+template<bool NeedPartitionRanges>
+TRuntimeNode BuildDqYtInputCall(
+ TType* outputType,
+ TType* itemType,
+ const TString& clusterName,
+ const TString& tokenName,
+ const TYtSectionList& sectionList,
+ const TYtState::TPtr& state,
+ NCommon::TMkqlBuildContext& ctx,
+ size_t inflight)
+{
+ NYT::TNode specNode = NYT::TNode::CreateMap();
+ NYT::TNode& tablesNode = specNode[YqlIOSpecTables];
+ NYT::TNode& registryNode = specNode[YqlIOSpecRegistry];
+ THashMap<TString, TString> uniqSpecs;
+ NYT::TNode samplingSpec;
+ const ui64 nativeTypeCompat = state->Configuration->NativeYtTypeCompatibility.Get(clusterName).GetOrElse(NTCF_LEGACY);
+
+ auto updateFlags = [nativeTypeCompat](NYT::TNode& spec) {
+ if (spec.HasKey(YqlRowSpecAttribute)) {
+ auto& rowSpec = spec[YqlRowSpecAttribute];
+ ui64 nativeYtTypeFlags = 0;
+ if (rowSpec.HasKey(RowSpecAttrNativeYtTypeFlags)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrNativeYtTypeFlags].AsUint64();
+ } else {
+ if (rowSpec.HasKey(RowSpecAttrUseNativeYtTypes)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrUseNativeYtTypes].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ } else if (rowSpec.HasKey(RowSpecAttrUseTypeV2)) {
+ nativeYtTypeFlags = rowSpec[RowSpecAttrUseTypeV2].AsBool() ? NTCF_LEGACY : NTCF_NONE;
+ }
+ }
+ rowSpec[RowSpecAttrNativeYtTypeFlags] = ui64(nativeYtTypeFlags & nativeTypeCompat);
+ }
+ };
+
+ TVector<TRuntimeNode> groups;
+ for (size_t i: xrange(sectionList.Size())) {
+ auto section = sectionList.Item(i);
+ for (auto& child: section.Settings().Ref().Children()) {
+ switch (FromString<EYtSettingType>(child->Child(0)->Content())) {
+ case EYtSettingType::Sample:
+ case EYtSettingType::SysColumns:
+ case EYtSettingType::Unordered:
+ case EYtSettingType::NonUnique:
+ break;
+ case EYtSettingType::KeyFilter:
+ case EYtSettingType::KeyFilter2:
+ YQL_ENSURE(child->Child(1)->ChildrenSize() == 0, "Unsupported KeyFilter setting");
+ break;
+ default:
+ YQL_ENSURE(false, "Unsupported settings");
+ break;
+ }
+ }
+
+ TVector<TStringBuf> columns;
+ THashMap<TString, ui32> structColumns;
+ ui32 index = 0;
+ for (auto& colType: section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetItems()) {
+ columns.push_back(colType->GetName());
+ structColumns.emplace(colType->GetName(), index++);
+ }
+
+ NYT::TNode sysColumns;
+ for (auto col: NYql::GetSettingAsColumnList(section.Settings().Ref(), EYtSettingType::SysColumns)) {
+ sysColumns.Add(col);
+ }
+
+ TVector<TRuntimeNode> tableTuples;
+ for (auto p: section.Paths()) {
+ TYtPathInfo pathInfo(p);
+ // Table may have aux columns. Exclude them by specifying explicit columns from the type
+ if (pathInfo.Table->RowSpec && !pathInfo.HasColumns()) {
+ pathInfo.SetColumns(columns);
+ }
+ auto specNode = pathInfo.GetCodecSpecNode();
+ if (!sysColumns.IsUndefined()) {
+ specNode[YqlSysColumnPrefix] = sysColumns;
+ }
+ updateFlags(specNode);
+ TString refName = TStringBuilder() << "$table" << uniqSpecs.size();
+ auto res = uniqSpecs.emplace(NYT::NodeToCanonicalYsonString(specNode), refName);
+ if (res.second) {
+ registryNode[refName] = specNode;
+ }
+ else {
+ refName = res.first->second;
+ }
+ tablesNode.Add(refName);
+ auto skiffNode = SingleTableSpecToInputSkiff(specNode, structColumns, true, true, false);
+ const auto tmpFolder = state->Configuration->TablesTmpFolder.Get().GetOrElse({});
+ auto tableName = pathInfo.Table->Name;
+ if (pathInfo.Table->IsAnonymous && !TYtTableInfo::HasSubstAnonymousLabel(pathInfo.Table->FromNode.Cast())) {
+ tableName = state->AnonymousLabels.Value(std::make_pair(clusterName, tableName), TString());
+ YQL_ENSURE(tableName, "Unaccounted anonymous table: " << pathInfo.Table->Name);
+ }
+
+ NYT::TRichYPath richYPath = state->Gateway->GetRealTable(state->SessionId, clusterName, tableName, pathInfo.Table->Epoch.GetOrElse(0), tmpFolder);
+ pathInfo.FillRichYPath(richYPath);
+ auto pathNode = NYT::PathToNode(richYPath);
+
+ tableTuples.push_back(ctx.ProgramBuilder.NewTuple({
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(pathInfo.Table->IsTemp ? TString() : tableName),
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(pathNode)),
+ ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(skiffNode)),
+ }));
+ }
+ groups.push_back(ctx.ProgramBuilder.NewList(tableTuples.front().GetStaticType(), tableTuples));
+ // All sections have the same sampling settings
+ if (samplingSpec.IsUndefined()) {
+ if (auto sampling = GetSampleParams(section.Settings().Ref())) {
+ YQL_ENSURE(sampling->Mode != EYtSampleMode::System);
+ samplingSpec["sampling_rate"] = sampling->Percentage / 100.;
+ if (sampling->Repeat) {
+ samplingSpec["sampling_seed"] = static_cast<i64>(sampling->Repeat);
+ }
+ }
+ }
+ }
+
+ auto server = state->Gateway->GetClusterServer(clusterName);
+ YQL_ENSURE(server, "Invalid YT cluster: " << clusterName);
+
+ TCallableBuilder call(ctx.ProgramBuilder.GetTypeEnvironment(), "DqYtRead", outputType);
+
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(server));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(tokenName));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(NYT::NodeToYsonString(specNode)));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(samplingSpec.IsUndefined() ? TString() : NYT::NodeToYsonString(samplingSpec)));
+ call.Add(ctx.ProgramBuilder.NewList(groups.front().GetStaticType(), groups));
+ call.Add(TRuntimeNode(itemType, true));
+
+ call.Add(ctx.ProgramBuilder.NewDataLiteral(inflight));
+ if constexpr (NeedPartitionRanges)
+ call.Add(ctx.ProgramBuilder.NewVoid());
+
+ return TRuntimeNode(call.Build(), false);
+}
+
+void RegisterYtMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler) {
+ compiler.AddCallable(TYtLength::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ auto length = TYtLength(&node);
+ ui64 lengthRes = 0;
+ if (auto out = length.Input().Maybe<TYtOutput>()) {
+ auto info = TYtOutTableInfo(GetOutTable(out.Cast()));
+ YQL_ENSURE(info.Stat);
+ lengthRes = info.Stat->RecordsCount;
+ } else {
+ auto read = length.Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown length input");
+ YQL_ENSURE(read.Cast().Input().Size() == 1, "Unsupported read with multiple sections");
+ for (auto path: read.Cast().Input().Item(0).Paths()) {
+ auto info = TYtTableBaseInfo::Parse(path.Table());
+ YQL_ENSURE(info->Stat);
+ lengthRes += info->Stat->RecordsCount;
+ }
+ }
+ return ctx.ProgramBuilder.NewDataLiteral<ui64>(lengthRes);
+ });
+
+ compiler.AddCallable(TYtTableContent::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ TYtTableContent tableContent(&node);
+ TMaybe<ui64> itemsCount;
+ if (auto setting = NYql::GetSetting(tableContent.Settings().Ref(), EYtSettingType::ItemsCount)) {
+ itemsCount = FromString<ui64>(setting->Child(1)->Content());
+ }
+ if (auto maybeRead = tableContent.Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ return BuildTableContentCall(
+ NCommon::BuildType(node, *node.GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder),
+ read.DataSource().Cluster().Value(), read.Input().Ref(), itemsCount, ctx, true);
+ } else {
+ auto output = tableContent.Input().Cast<TYtOutput>();
+ return BuildTableContentCall(
+ NCommon::BuildType(node, *node.GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx.ProgramBuilder),
+ GetOutputOp(output).DataSink().Cluster().Value(), output.Ref(), itemsCount, ctx, true);
+ }
+ });
+
+ compiler.AddCallable({TYtTablePath::CallableName(), TYtTableRecord::CallableName(), TYtTableIndex::CallableName(), TYtIsKeySwitch::CallableName(), TYtRowNumber::CallableName()},
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ auto dataSlot = node.GetTypeAnn()->Cast<TDataExprType>()->GetSlot();
+
+ TCallableBuilder call(ctx.ProgramBuilder.GetTypeEnvironment(), node.Content(),
+ ctx.ProgramBuilder.NewDataType(dataSlot));
+
+ call.Add(NCommon::MkqlBuildExpr(*node.Child(0), ctx));
+ return TRuntimeNode(call.Build(), false);
+ });
+}
+
+void RegisterDqYtMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler, const TYtState::TPtr& state) {
+
+ compiler.ChainCallable(TDqReadWideWrap::CallableName(),
+ [state](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ if (const auto& wrapper = TDqReadWideWrap(&node); wrapper.Input().Maybe<TYtReadTable>().IsValid()) {
+ const auto ytRead = wrapper.Input().Cast<TYtReadTable>();
+ const auto readType = ytRead.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().back();
+ const auto inputItemType = NCommon::BuildType(wrapper.Input().Ref(), GetSeqItemType(*readType), ctx.ProgramBuilder);
+ const auto cluster = ytRead.DataSource().Cluster().StringValue();
+ size_t isRPC = state->Configuration->UseRPCReaderInDQ.Get(cluster).GetOrElse(DEFAULT_USE_RPC_READER_IN_DQ) ? state->Configuration->DQRPCReaderInflight.Get(cluster).GetOrElse(DEFAULT_RPC_READER_INFLIGHT) : 0;
+
+ const auto outputType = NCommon::BuildType(wrapper.Ref(), *wrapper.Ref().GetTypeAnn(), ctx.ProgramBuilder);
+ TString tokenName;
+ if (auto secureParams = wrapper.Token()) {
+ tokenName = secureParams.Cast().Name().StringValue();
+ }
+
+ bool solid = false;
+ for (const auto& flag : wrapper.Flags())
+ if (solid = flag.Value() == "Solid")
+ break;
+
+ if (solid)
+ return BuildDqYtInputCall<false>(outputType, inputItemType, cluster, tokenName, ytRead.Input(), state, ctx, isRPC);
+ else
+ return BuildDqYtInputCall<true>(outputType, inputItemType, cluster, tokenName, ytRead.Input(), state, ctx, isRPC);
+ }
+
+ return TRuntimeNode();
+ });
+
+ compiler.AddCallable(TYtDqWideWrite::CallableName(),
+ [](const TExprNode& node, NCommon::TMkqlBuildContext& ctx) {
+ const auto write = TYtDqWideWrite(&node);
+ const auto outType = NCommon::BuildType(write.Ref(), *write.Ref().GetTypeAnn(), ctx.ProgramBuilder);
+ const auto arg = MkqlBuildExpr(write.Input().Ref(), ctx);
+
+ TString server{GetSetting(write.Settings().Ref(), "server")->Child(1)->Content()};
+ TString table{GetSetting(write.Settings().Ref(), "table")->Child(1)->Content()};
+ TString outSpec{GetSetting(write.Settings().Ref(), "outSpec")->Child(1)->Content()};
+ auto secureSetting = GetSetting(write.Settings().Ref(), "secureParams");
+ TString writerOptions{GetSetting(write.Settings().Ref(), "writerOptions")->Child(1)->Content()};
+
+ TString tokenName;
+ if (secureSetting->ChildrenSize() > 1) {
+ TCoSecureParam secure(secureSetting->Child(1));
+ tokenName = secure.Name().StringValue();
+ }
+
+ TCallableBuilder call(ctx.ProgramBuilder.GetTypeEnvironment(), "YtDqRowsWideWrite", outType);
+ call.Add(arg);
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(server));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(tokenName));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(table));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(outSpec));
+ call.Add(ctx.ProgramBuilder.NewDataLiteral<NUdf::EDataSlot::String>(writerOptions));
+
+ return TRuntimeNode(call.Build(), false);
+ });
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h b/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h
new file mode 100644
index 0000000000..0bf32ba4d6
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_mkql_compiler.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+
+#include <ydb/library/yql/providers/common/mkql/yql_provider_mkql.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+
+#include <utility>
+
+namespace NYql {
+
+class TExprNode;
+class TTypeAnnotationNode;
+
+NKikimr::NMiniKQL::TRuntimeNode BuildTableContentCall(NKikimr::NMiniKQL::TType* outItemType,
+ TStringBuf clusterName,
+ const TExprNode& input,
+ const TMaybe<ui64>& itemsCount,
+ NCommon::TMkqlBuildContext& ctx,
+ bool forceColumns,
+ const THashSet<TString>& extraSysColumns = {},
+ bool forceKeyColumns = false);
+
+NKikimr::NMiniKQL::TRuntimeNode BuildTableContentCall(TStringBuf callName,
+ NKikimr::NMiniKQL::TType* outItemType,
+ TStringBuf clusterName,
+ const TExprNode& input,
+ const TMaybe<ui64>& itemsCount,
+ NCommon::TMkqlBuildContext& ctx,
+ bool forceColumns,
+ const THashSet<TString>& extraSysColumns = {},
+ bool forceKeyColumns = false);
+
+void RegisterYtMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler);
+void RegisterDqYtMkqlCompilers(NCommon::TMkqlCallableCompilerBase& compiler, const TYtState::TPtr& state);
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
new file mode 100644
index 0000000000..cd575a608d
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.cpp
@@ -0,0 +1,228 @@
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_hash.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/hash/yql_hash_builder.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/string/cast.h>
+#include <util/string/hex.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+TYtNodeHashCalculator::TYtNodeHashCalculator(const TYtState::TPtr& state, const TString& cluster, const TYtSettings::TConstPtr& config)
+ : TNodeHashCalculator(*state->Types, state->NodeHash, MakeSalt(config, cluster))
+ , DontFailOnMissingParentOpHash(state->Types->EvaluationInProgress)
+ , State(state)
+ , Cluster(cluster)
+ , Configuration(config)
+{
+ auto opHasher = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ return GetOperationHash(node, argIndex, frameLevel);
+ };
+ Hashers[TYtReadTable::CallableName()] = opHasher;
+ Hashers[TYtFill::CallableName()] = opHasher;
+ Hashers[TYtSort::CallableName()] = opHasher;
+ Hashers[TYtMerge::CallableName()] = opHasher;
+ Hashers[TYtMap::CallableName()] = opHasher;
+ Hashers[TYtReduce::CallableName()] = opHasher;
+ Hashers[TYtMapReduce::CallableName()] = opHasher;
+ Hashers[TYtCopy::CallableName()] = opHasher;
+ Hashers[TYtTouch::CallableName()] = opHasher;
+ Hashers[TYtDqProcessWrite::CallableName()] = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ if (const auto flags = TYtDqProcessWrite(&node).Flags()) {
+ // Only for hybrid for now.
+ for (const auto& atom : flags.Cast()) {
+ if (atom.Value() == "FallbackOnError") {
+ return GetOperationHash(node, argIndex, frameLevel);
+ }
+ }
+ }
+ return TString();
+ };
+
+ Hashers[TYtOutput::CallableName()] = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ return GetOutputHash(node, argIndex, frameLevel);
+ };
+ Hashers[TYtOutTable::CallableName()] = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ return GetOutTableHash(node, argIndex, frameLevel);
+ };
+ Hashers[TYtTable::CallableName()] = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ return GetTableHash(node, argIndex, frameLevel);
+ };
+
+ Hashers[TYtStat::CallableName()] = [this] (const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) {
+ Y_UNUSED(argIndex);
+ Y_UNUSED(frameLevel);
+
+ TYtTableStatInfo stat{TExprBase(&node)};
+
+ const bool ignoreRevision = Configuration->QueryCacheIgnoreTableRevision.Get().GetOrElse(false);
+
+ THashBuilder builder;
+ builder << stat.Id;
+ if (!ignoreRevision) {
+ builder << stat.Revision;
+ // TODO remove after https://st.yandex-team.ru/YT-9914
+ builder << stat.ChunkCount;
+ builder << stat.DataSize;
+ builder << stat.RecordsCount;
+ }
+
+ return builder.Finish();
+ };
+}
+
+TString TYtNodeHashCalculator::MakeSalt(const TYtSettings::TConstPtr& config, const TString& cluster) {
+ auto salt = config->QueryCacheSalt.Get().GetOrElse(TString());
+ THashBuilder builder;
+ bool update = false;
+ if (auto val = config->LayerPaths.Get(cluster)) {
+ update = true;
+ for (const auto& path : *val) {
+ builder << path;
+ }
+ }
+ if (auto val = config->NativeYtTypeCompatibility.Get(cluster)) {
+ update = true;
+ builder << *val;
+ }
+ if (auto val = config->OptimizeFor.Get(cluster)) {
+ update = true;
+ builder << *val;
+ }
+ if (update) {
+ builder << salt;
+ salt = builder.Finish();
+ }
+ return salt;
+}
+
+TString TYtNodeHashCalculator::GetOutputHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const {
+ Y_UNUSED(argIndex);
+ Y_UNUSED(frameLevel);
+
+ auto output = TYtOutput(&node);
+ auto opUniqueId = GetOutputOp(output).Ref().UniqueId();
+ auto it = NodeHash.find(opUniqueId);
+ if (it == NodeHash.end()) {
+ if (DontFailOnMissingParentOpHash) {
+ return TString();
+ }
+ YQL_ENSURE(false, "Cannot find hash for operation " << GetOutputOp(output).Ref().Content()
+ << ", #" << opUniqueId);
+ }
+ if (it->second.empty()) {
+ return TString();
+ }
+ return (THashBuilder() << it->second << FromString<size_t>(output.OutIndex().Value())).Finish();
+}
+
+TString TYtNodeHashCalculator::GetOutTableHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const {
+ auto opHash = NYql::GetSetting(*node.Child(TYtOutTable::idx_Settings), EYtSettingType::OpHash);
+ if (opHash && opHash->Child(1)->Content().empty()) {
+ return TString();
+ }
+ THashBuilder builder;
+ builder << node.Content();
+ for (size_t i = 0; i < node.ChildrenSize(); ++i) {
+ if (i != TYtOutTable::idx_Name && i != TYtOutTable::idx_Stat && i != TYtOutTable::idx_Settings) {
+ if (auto partHash = GetHashImpl(*node.Child(i), argIndex, frameLevel)) {
+ builder << partHash;
+ }
+ else {
+ return TString();
+ }
+ }
+ }
+
+ for (auto& setting : node.Child(TYtOutTable::idx_Settings)->Children()) {
+ if (auto partHash = GetHashImpl(*setting, argIndex, frameLevel)) {
+ builder << partHash;
+ }
+ else {
+ return TString();
+ }
+ }
+
+ // OpHash is present for substitutions YtOutput->YtOutTable. It already includes publish-related hashes from the parent operation
+ if (!opHash) {
+ if (auto compressionCodec = Configuration->TemporaryCompressionCodec.Get(Cluster)) {
+ builder << *compressionCodec;
+ }
+ if (auto erasureCodec = Configuration->TemporaryErasureCodec.Get(Cluster)) {
+ builder << ToString(*erasureCodec);
+ }
+ if (auto media = Configuration->TemporaryMedia.Get(Cluster)) {
+ builder << NYT::NodeToYsonString(*media);
+ }
+ if (auto primaryMedium = Configuration->TemporaryPrimaryMedium.Get(Cluster)) {
+ builder << *primaryMedium;
+ }
+ if (auto replicationFactor = Configuration->TemporaryReplicationFactor.Get(Cluster)) {
+ builder << *replicationFactor;
+ }
+ }
+
+ return builder.Finish();
+}
+
+TString TYtNodeHashCalculator::GetTableHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const {
+ TYtTable ytTable{ &node };
+ auto epoch = TEpochInfo::Parse(ytTable.Epoch().Ref()).GetOrElse(0);
+ const auto tableLabel = TString(TYtTableInfo::GetTableLabel(ytTable));
+ const auto& tableDescription = State->TablesData->GetTable(TString(ytTable.Cluster().Value()), tableLabel, epoch);
+ if (tableDescription.Hash) {
+ if (!tableDescription.Hash.Empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "Use hash " << HexEncode(*tableDescription.Hash) << " for table " <<
+ ytTable.Cluster().Value() << "." << tableLabel << "#" << epoch;
+ }
+
+ return *tableDescription.Hash;
+ }
+
+ const bool ignoreRevision = Configuration->QueryCacheIgnoreTableRevision.Get().GetOrElse(false);
+ if (!ignoreRevision && TYtTableMetaInfo(ytTable.Meta()).IsDynamic) {
+ return TString();
+ }
+
+ THashBuilder builder;
+ builder << node.Content();
+ builder << tableLabel;
+ for (size_t i = 0; i < node.ChildrenSize(); ++i) {
+ if (i != TYtTable::idx_Name && i != TYtTable::idx_Meta) {
+ if (auto partHash = GetHashImpl(*node.Child(i), argIndex, frameLevel)) {
+ builder << partHash;
+ }
+ else {
+ return TString();
+ }
+ }
+ }
+ return builder.Finish();
+}
+
+TString TYtNodeHashCalculator::GetOperationHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const {
+ THashBuilder builder;
+ builder << node.Content();
+ for (size_t i = 0; i < node.ChildrenSize(); ++i) {
+ if (i != TYtReadTable::idx_World) {
+ if (auto partHash = GetHashImpl(*node.Child(i), argIndex, frameLevel)) {
+ builder << partHash;
+ }
+ else {
+ return TString();
+ }
+ }
+ }
+ return builder.Finish();
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.h b/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.h
new file mode 100644
index 0000000000..2a96aa4b2e
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_op_hash.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+
+#include <ydb/library/yql/providers/yt/lib/hash/yql_op_hash.h>
+
+namespace NYql {
+
+class TYtNodeHashCalculator: public TNodeHashCalculator {
+public:
+ TYtNodeHashCalculator(const TYtState::TPtr& state, const TString& cluster, const TYtSettings::TConstPtr& config);
+
+ static TString MakeSalt(const TYtSettings::TConstPtr& config, const TString& cluster);
+
+private:
+ TString GetOutputHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const;
+ TString GetOutTableHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const;
+ TString GetTableHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const;
+ TString GetOperationHash(const TExprNode& node, TArgIndex& argIndex, ui32 frameLevel) const;
+
+private:
+ const bool DontFailOnMissingParentOpHash;
+ const TYtState::TPtr State;
+ const TString Cluster;
+ const TYtSettings::TConstPtr Configuration;
+};
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
new file mode 100644
index 0000000000..5e34b188de
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.cpp
@@ -0,0 +1,1092 @@
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+
+#include <util/string/cast.h>
+#include <util/generic/hash_set.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+
+namespace NYql {
+
+namespace {
+
+bool ValidateColumnSettings(TExprNode& columnsSettings, TExprContext& ctx, TVector<TString>& columns, bool maybeEmpty) {
+ if (maybeEmpty) {
+ if (!EnsureTuple(columnsSettings, ctx)) {
+ return false;
+ }
+ } else {
+ if (!EnsureTupleMinSize(columnsSettings, 1, ctx)) {
+ return false;
+ }
+ }
+
+ for (auto& child : columnsSettings.Children()) {
+ if (!EnsureAtom(*child, ctx)) {
+ return false;
+ }
+ columns.push_back(TString(child->Content()));
+ }
+ return true;
+}
+
+bool ValidateColumnPairSettings(TExprNode& columnsSettings, TExprContext& ctx, TVector<TString>& columns) {
+ if (!EnsureTupleMinSize(columnsSettings, 1, ctx)) {
+ return false;
+ }
+
+ for (auto& child : columnsSettings.Children()) {
+ if (child->IsList()) {
+ if (!EnsureTupleSize(*child, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*child->Child(0), ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*child->Child(1), ctx)) {
+ return false;
+ }
+ bool res = false;
+ if (!TryFromString(child->Child(1)->Content(), res)) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Child(1)->Pos()), TStringBuilder() << "Expected bool value, but got: " << child->Child(1)->Content()));
+ return false;
+ }
+ columns.emplace_back(child->Child(0)->Content());
+ } else {
+ if (HasError(child->GetTypeAnn(), ctx) || !child->IsAtom()) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Expected tuple or atom, but got: " << child->Type()));
+ return false;
+ }
+ columns.emplace_back(child->Content());
+ }
+ }
+ return true;
+}
+
+bool ValidateColumnSubset(const TPositionHandle& pos, TExprContext& ctx, const TVector<TString>& sortBy, const TVector<TString>& reduceBy) {
+
+ if (sortBy.size() < reduceBy.size()) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), "reduceBy column list must be prefix of sortBy column list"));
+ return false;
+ }
+ for (size_t i = 0; i < reduceBy.size(); ++i) {
+ if (sortBy[i] != reduceBy[i]) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), "reduceBy column list must be prefix of sortBy column list"));
+ return false;
+ }
+ }
+ return true;
+}
+
+const THashSet<TStringBuf> SYS_COLUMNS = {
+ "path",
+ "record",
+ "index",
+ "num",
+};
+
+} // unnamed
+
+bool ValidateSettings(const TExprNode& settingsNode, EYtSettingTypes accepted, TExprContext& ctx) {
+ TMaybe<TVector<TString>> sortBy;
+ TMaybe<TVector<TString>> reduceBy;
+ bool isTruncate = true;
+ EYtSettingTypes used;
+ for (auto setting: settingsNode.Children()) {
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return false;
+ }
+ auto nameNode = setting->Child(0);
+ if (!EnsureAtom(*nameNode, ctx)) {
+ return false;
+ }
+ EYtSettingType type;
+ if (!TryFromString(nameNode->Content(), type)) {
+ ctx.AddError(TIssue(ctx.GetPosition(nameNode->Pos()), TStringBuilder() << "Unknown setting name: " << nameNode->Content()));
+ return false;
+ }
+ if (!accepted.HasFlags(type)) {
+ ctx.AddError(TIssue(ctx.GetPosition(nameNode->Pos()), TStringBuilder() << "Setting " << nameNode->Content() << " is not accepted here"));
+ return false;
+ }
+ used |= type;
+
+ TIssueScopeGuard issueScope(ctx.IssueManager, [setting, type, &ctx]() {
+ return MakeIntrusive<TIssue>(ctx.GetPosition(setting->Pos()), TStringBuilder() << "Setting " << type);
+ });
+
+ switch (type) {
+ case EYtSettingType::KeyFilter: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ auto value = setting->Child(1);
+ if (!EnsureTupleMaxSize(*value, 2, ctx)) {
+ return false;
+ }
+ if (value->ChildrenSize() > 0) {
+ if (value->ChildrenSize() == 2) {
+ if (!EnsureAtom(*value->Child(1), ctx)) {
+ return false;
+ }
+ size_t tableIndex = 0;
+ if (!TryFromString(value->Child(1)->Content(), tableIndex)) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Child(1)->Pos()), TStringBuilder()
+ << "Bad table index value: " << value->Child(1)->Content()));
+ return false;
+ }
+ }
+ auto andList = value->Child(0);
+ if (!EnsureTupleMinSize(*andList, 1, ctx)) {
+ return false;
+ }
+ for (auto keyPredicates: andList->Children()) {
+ if (!EnsureTupleSize(*keyPredicates, 2, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*keyPredicates->Child(0), ctx)) {
+ return false;
+ }
+ if (keyPredicates->Child(0)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyPredicates->Child(0)->Pos()), "Column name should be a non-empty atom"));
+ return false;
+ }
+ if (!EnsureTupleMinSize(*keyPredicates->Child(1), 1, ctx)) {
+ return false;
+ }
+
+ for (auto cmp: keyPredicates->Child(1)->Children()) {
+ if (!EnsureTupleSize(*cmp, 2, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*cmp->Child(0), ctx)) {
+ return false;
+ }
+ if (cmp->Child(0)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(cmp->Child(0)->Pos()), "Comparison should be a non-empty atom"));
+ return false;
+ }
+ if (!IsRangeComparison(cmp->Child(0)->Content())) {
+ ctx.AddError(TIssue(ctx.GetPosition(cmp->Child(0)->Pos()), TStringBuilder()
+ << "Unsupported compare operation "
+ << TString{cmp->Child(0)->Content()}.Quote()));
+ return false;
+ }
+
+ if (!IsNull(*cmp->Child(1))) {
+ bool isOptional = false;
+ const TDataExprType* dataType = nullptr;
+ if (!EnsureDataOrOptionalOfData(*cmp->Child(1), isOptional, dataType, ctx)) {
+ return false;
+ }
+ if (!EnsureComparableDataType(cmp->Child(1)->Pos(), dataType->GetSlot(), ctx)) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ case EYtSettingType::KeyFilter2: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ auto value = setting->Child(1);
+ if (!EnsureTupleMaxSize(*value, 3, ctx)) {
+ return false;
+ }
+ if (value->ChildrenSize() > 0) {
+ if (!EnsureTupleMinSize(*value, 2, ctx)) {
+ return false;
+ }
+ if (value->ChildrenSize() == 3) {
+ if (!EnsureTupleOfAtoms(value->Tail(), ctx)) {
+ return false;
+ }
+
+ THashSet<size_t> indexes;
+ for (auto& indexNode : value->Tail().ChildrenList()) {
+ size_t tableIndex = 0;
+ if (!TryFromString(indexNode->Content(), tableIndex)) {
+ ctx.AddError(TIssue(ctx.GetPosition(indexNode->Pos()), TStringBuilder()
+ << "Bad table index value: " << indexNode->Content()));
+ return false;
+ }
+ if (!indexes.insert(tableIndex).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(indexNode->Pos()), TStringBuilder()
+ << "Duplicate table index value: " << tableIndex));
+ return false;
+ }
+ }
+ }
+
+ auto& computeNode = value->Head();
+ const TTypeAnnotationNode* computeNodeType = computeNode.GetTypeAnn();
+ YQL_ENSURE(computeNodeType);
+ if (!EnsureListType(computeNode.Pos(), *computeNodeType, ctx)) {
+ return false;
+ }
+
+ auto computeNodeListItemType = computeNodeType->Cast<TListExprType>()->GetItemType();
+ if (!EnsureTupleTypeSize(computeNode.Pos(), computeNodeListItemType, 2, ctx)) {
+ return false;
+ }
+
+ auto computeNodeTupleItems = computeNodeListItemType->Cast<TTupleExprType>()->GetItems();
+ YQL_ENSURE(computeNodeTupleItems.size() == 2);
+ if (!IsSameAnnotation(*computeNodeTupleItems[0], *computeNodeTupleItems[1])) {
+ ctx.AddError(TIssue(ctx.GetPosition(computeNode.Pos()), TStringBuilder()
+ << "Expecting compute expression to return list of 2-element tuples of same type, got: "
+ << *computeNodeType));
+ return false;
+ }
+
+ if (!EnsureTupleType(computeNode.Pos(), *computeNodeTupleItems.front(), ctx)) {
+ return false;
+ }
+
+ auto rangeTypeItems = computeNodeTupleItems.front()->Cast<TTupleExprType>()->GetItems();
+ if (rangeTypeItems.size() < 2) {
+ ctx.AddError(TIssue(ctx.GetPosition(computeNode.Pos()), TStringBuilder()
+ << "Expecting range boundary to be a tuple with at least two elements, got: "
+ << *computeNodeTupleItems.front()));
+ return false;
+ }
+
+ if (!EnsureSpecificDataType(computeNode.Pos(), *rangeTypeItems.back(), EDataSlot::Int32,ctx)) {
+ return false;
+ }
+
+ for (size_t i = 0; i + 1 < rangeTypeItems.size(); ++i) {
+ auto boundaryType = rangeTypeItems[i];
+ YQL_ENSURE(boundaryType);
+ if (!EnsureOptionalType(computeNode.Pos(), *boundaryType, ctx)) {
+ return false;
+ }
+
+ bool isOptional;
+ const TDataExprType* data = nullptr;
+ if (!EnsureDataOrOptionalOfData(computeNode.Pos(), RemoveOptionalType(boundaryType), isOptional, data, ctx)) {
+ return false;
+ }
+
+ if (!EnsureComparableDataType(computeNode.Pos(), data->GetSlot(), ctx)) {
+ return false;
+ }
+ }
+
+ auto& settingsNode = *value->Child(1);
+ if (!EnsureTuple(settingsNode, ctx)) {
+ return false;
+ }
+
+ for (auto& setting : settingsNode.ChildrenList()) {
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(setting->Head(), ctx)) {
+ return false;
+ }
+
+ auto name = setting->Head().Content();
+ if (name == "usedKeys") {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+
+ if (!EnsureTupleMinSize(setting->Tail(), 1, ctx)) {
+ return false;
+ }
+
+ THashSet<TStringBuf> keys;
+ for (auto& key : setting->Tail().ChildrenList()) {
+ if (!EnsureAtom(*key, ctx)) {
+ return false;
+ }
+
+ if (!keys.insert(key->Content()).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(key->Pos()), TStringBuilder()
+ << "Duplicate key column name: " << key->Content()));
+ return false;
+ }
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Head().Pos()), TStringBuilder()
+ << "Unknown option : '" << setting->Head().Content() << "' for "
+ << ToString(EYtSettingType::KeyFilter).Quote()));
+ return false;
+ }
+ }
+ }
+ break;
+ }
+ case EYtSettingType::Take:
+ case EYtSettingType::Skip:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureSpecificDataType(*setting->Child(1), EDataSlot::Uint64, ctx)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::UserSchema:
+ case EYtSettingType::UserColumns: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (setting->Child(1)->GetTypeAnn()) {
+ if (!EnsureType(*setting->Child(1), ctx)) {
+ return false;
+ }
+ auto type = setting->Child(1)->GetTypeAnn()->Cast<TTypeExprType>()->GetType();
+ if (!EnsureStructType(setting->Child(1)->Pos(), *type, ctx)) {
+ return false;
+ }
+
+ if (!EnsurePersistableType(setting->Child(1)->Pos(), *type, ctx)) {
+ return false;
+ }
+ }
+
+ break;
+ }
+ case EYtSettingType::StatColumns: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ TVector<TString> columns;
+ const bool maybeEmpty = true;
+ if (!ValidateColumnSettings(*setting->Child(1), ctx, columns, maybeEmpty)) {
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::SysColumns: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ TVector<TString> columns;
+ const bool maybeEmpty = false;
+ if (!ValidateColumnSettings(*setting->Child(1), ctx, columns, maybeEmpty)) {
+ return false;
+ }
+ for (auto col: columns) {
+ if (!SYS_COLUMNS.contains(col)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Pos()), TStringBuilder()
+ << "Unsupported system column " << col.Quote()));
+ return false;
+ }
+ }
+ break;
+ }
+ case EYtSettingType::InferScheme:
+ case EYtSettingType::ForceInferScheme:
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return false;
+ }
+ if (!EnsureTupleMaxSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (setting->ChildrenSize() == 2) {
+ if (!EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ ui32 val;
+ if (!TryFromString(setting->Tail().Content(), val) || val < 1 || val > 1000) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Bad value " << TString{setting->Tail().Content()}.Quote()));
+ return false;
+ }
+ }
+ break;
+ case EYtSettingType::Ordered:
+ case EYtSettingType::Initial:
+ case EYtSettingType::DoNotFailOnInvalidSchema:
+ case EYtSettingType::DirectRead:
+ case EYtSettingType::Scheme:
+ case EYtSettingType::WeakConcat:
+ case EYtSettingType::IgnoreNonExisting:
+ case EYtSettingType::WarnNonExisting:
+ case EYtSettingType::ForceTransform:
+ case EYtSettingType::CombineChunks:
+ case EYtSettingType::WithQB:
+ case EYtSettingType::Inline:
+ case EYtSettingType::WeakFields:
+ case EYtSettingType::Sharded:
+ case EYtSettingType::XLock:
+ case EYtSettingType::JoinReduce:
+ case EYtSettingType::Unordered:
+ case EYtSettingType::NonUnique:
+ case EYtSettingType::KeepSorted:
+ case EYtSettingType::KeySwitch:
+ case EYtSettingType::IgnoreTypeV3:
+ case EYtSettingType::NoDq:
+ case EYtSettingType::Split:
+ case EYtSettingType::KeepMeta:
+ case EYtSettingType::MonotonicKeys:
+ if (!EnsureTupleSize(*setting, 1, ctx)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::Flow:
+ case EYtSettingType::Anonymous:
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return false;
+ }
+ if (!EnsureTupleMaxSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (setting->ChildrenSize() == 2) {
+ if (!EnsureAtom(*setting->Child(1), ctx)) {
+ return false;
+ }
+ }
+ break;
+ case EYtSettingType::FirstAsPrimary:
+ if (!EnsureTupleMinSize(*setting, 1, ctx)) {
+ return false;
+ }
+ if (!EnsureTupleMaxSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (setting->ChildrenSize() == 2) {
+ for (auto childSetting : setting->Child(1)->Children()) {
+ if (!EnsureTupleMinSize(*childSetting, 1, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*childSetting->Child(0), ctx)) {
+ return false;
+ }
+
+ const auto childSettingName = childSetting->Child(0)->Content();
+ if (childSettingName == MaxJobSizeForFirstAsPrimaryName) {
+ if (!EnsureTupleSize(*childSetting, 2, ctx)) {
+ return false;
+ }
+ ui64 value;
+ if (!EnsureAtom(*childSetting->Child(1), ctx)) {
+ return false;
+ }
+ if (!TryFromString(childSetting->Child(1)->Content(), value)) {
+ ctx.AddError(TIssue(ctx.GetPosition(childSetting->Child(1)->Pos()), TStringBuilder()
+ << "Bad value " << TString{childSetting->Child(1)->Content()}.Quote()));
+ return false;
+ }
+ if (!value) {
+ ctx.AddError(TIssue(ctx.GetPosition(childSetting->Child(1)->Pos()), TStringBuilder()
+ << MaxJobSizeForFirstAsPrimaryName << " value should not be zero"));
+ return false;
+ }
+ } else if (childSettingName == JoinReduceForSecondAsPrimaryName) {
+ if (!EnsureTupleSize(*childSetting, 1, ctx)) {
+ return false;
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(childSetting->Pos()), TStringBuilder()
+ << "Unknown " << ToString(EYtSettingType::FirstAsPrimary) << " subsetting " << TString{childSettingName}.Quote()));
+ return false;
+ }
+ }
+ }
+ break;
+ case EYtSettingType::JobCount:
+ case EYtSettingType::MemUsage:
+ case EYtSettingType::ItemsCount:
+ case EYtSettingType::RowFactor: {
+ ui64 value;
+ if (!EnsureAtom(*setting->Child(1), ctx)) {
+ return false;
+ }
+ if (!TryFromString(setting->Child(1)->Content(), value)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Pos()), TStringBuilder()
+ << "Bad value " << TString{setting->Child(1)->Content()}.Quote()));
+ return false;
+ }
+ if ((EYtSettingType::JobCount == type || EYtSettingType::FirstAsPrimary == type) && 0 == value) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Pos()), "Value should not be zero"));
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::View:
+ case EYtSettingType::OpHash:
+ if (!EnsureArgsCount(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*setting->Child(1), ctx)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::Mode: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*setting->Child(1), ctx)) {
+ return false;
+ }
+ EYtWriteMode mode;
+ if (!TryFromString(setting->Child(1)->Content(), mode)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Pos()), TStringBuilder()
+ << "Unsupported mode value " << TString{setting->Child(1)->Content()}.Quote()));
+ return false;
+ }
+ isTruncate = EYtWriteMode::Renew == mode;
+ break;
+ }
+ case EYtSettingType::Limit:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureTuple(*setting->Child(1), ctx)) {
+ return false;
+ }
+ for (auto expr: setting->Child(1)->Children()) {
+ if (!EnsureTuple(*expr, ctx)) {
+ return false;
+ }
+ for (auto item: expr->Children()) {
+ if (!EnsureTupleMinSize(*item, 1, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*item->Child(0), ctx)) {
+ return false;
+ }
+ if (FromString<EYtSettingType>(item->Child(0)->Content()) & (EYtSettingType::Take | EYtSettingType::Skip)) {
+ if (!EnsureArgsCount(*item, 2, ctx)) {
+ return false;
+ }
+
+ if (!EnsureSpecificDataType(*item->Child(1), EDataSlot::Uint64, ctx)) {
+ return false;
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(item->Pos()), TStringBuilder()
+ << "Unsupported item " << TString{item->Child(0)->Content()}.Quote()));
+ return false;
+ }
+ }
+ }
+ break;
+ case EYtSettingType::ReduceFilterBy:
+ case EYtSettingType::UniqueBy: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ TVector<TString> columns;
+ if (!ValidateColumnSettings(*setting->Child(1), ctx, columns, EYtSettingType::ReduceFilterBy == type)) {
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::SortLimitBy: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ TVector<TString> columns;
+ if (!ValidateColumnPairSettings(*setting->Child(1), ctx, columns)) {
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::SortBy:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+
+ sortBy.ConstructInPlace();
+ if (!ValidateColumnPairSettings(*setting->Child(1), ctx, *sortBy)) {
+ return false;
+ }
+ if (reduceBy.Defined() && !ValidateColumnSubset(setting->Child(1)->Pos(), ctx, *sortBy, *reduceBy)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::ReduceBy:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+
+ reduceBy.ConstructInPlace();
+ if (!ValidateColumnPairSettings(*setting->Child(1), ctx, *reduceBy)) {
+ return false;
+ }
+
+ if (sortBy.Defined() && !ValidateColumnSubset(setting->Child(1)->Pos(), ctx, *sortBy, *reduceBy)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::Sample:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureTupleSize(*setting->Child(1), 3, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*setting->Child(1)->Child(0), ctx)) {
+ return false;
+ }
+ {
+ EYtSampleMode sampleMode = EYtSampleMode::System;
+ if (!TryFromString(setting->Child(1)->Child(0)->Content(), sampleMode)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Child(0)->Pos()), TStringBuilder()
+ << "Bad sample mode value: " << setting->Child(1)->Child(0)->Content()));
+ return false;
+ }
+ }
+ if (!EnsureAtom(*setting->Child(1)->Child(1), ctx)) {
+ return false;
+ }
+ {
+ double samplePercentage = 0;
+ if (!TryFromString(setting->Child(1)->Child(1)->Content(), samplePercentage) ||
+ !(samplePercentage >= 0. && samplePercentage <= 100.))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Child(1)->Pos()), TStringBuilder()
+ << "Bad sample percentage value: " << setting->Child(1)->Child(1)->Content()));
+ return false;
+ }
+ }
+ if (!EnsureAtom(*setting->Child(1)->Child(2), ctx)) {
+ return false;
+ }
+ {
+ ui64 sampleRepeat = 0;
+ if (!TryFromString(setting->Child(1)->Child(2)->Content(), sampleRepeat)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Child(1)->Child(2)->Pos()), TStringBuilder()
+ << "Bad sample repeat value: " << setting->Child(1)->Child(2)->Content()));
+ return false;
+ }
+ }
+ break;
+ case EYtSettingType::JoinLabel:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::MapOutputType:
+ case EYtSettingType::ReduceInputType:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureType(setting->Tail(), ctx)) {
+ return false;
+ }
+ break;
+ case EYtSettingType::ErasureCodec: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ NYT::EErasureCodecAttr codec;
+ if (!TryFromString(setting->Tail().Content(), codec)) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Unsupported erasure codec value " << TString{setting->Tail().Content()}.Quote()));
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::CompressionCodec:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!ValidateCompressionCodecValue(setting->Tail().Content())) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Unsupported compression codec value " << TString{setting->Tail().Content()}.Quote()));
+ return false;
+ }
+ break;
+ case EYtSettingType::Expiration: {
+ if (!EnsureTupleSize(*setting, 2, ctx) || !EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ TInstant ti;
+ TDuration td;
+ if (!TInstant::TryParseIso8601(setting->Tail().Content(), ti) &&
+ !TDuration::TryParse(setting->Tail().Content(), td))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Unsupported Expiration value " << TString{setting->Tail().Content()}.Quote()));
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::ReplicationFactor: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ ui32 replicationFactor;
+ if (!TryFromString(setting->Tail().Content(), replicationFactor) ||
+ replicationFactor < 1 || replicationFactor > 10)
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Unsupported Replication Factor value:"
+ << TString{setting->Tail().Content()}.Quote()));
+ return false;
+ }
+ break;
+ }
+ case EYtSettingType::UserAttrs: {
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ NYT::TNode mapNode;
+ try {
+ mapNode = NYT::NodeFromYsonString(setting->Tail().Content());
+ } catch (const std::exception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Failed to parse Yson: " << e.what()));
+ return false;
+ }
+ if (!mapNode.IsMap()) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Expected Yson map, got: " << mapNode.GetType()));
+ return false;
+ }
+ const auto& map = mapNode.AsMap();
+ for (auto it = map.cbegin(); it != map.cend(); ++it) {
+ if (!it->second.HasValue()) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Expected Yson map key having value: "
+ << it->first.Quote()));
+ return false;
+ }
+ }
+ break;
+ }
+ case EYtSettingType::Media:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ try {
+ MediaValidator(NYT::NodeFromYsonString(setting->Tail().Content()));
+ } catch (const std::exception& e) {
+ ctx.AddError(TIssue(ctx.GetPosition(setting->Tail().Pos()), TStringBuilder()
+ << "Incorrect media: " << e.what()));
+ return false;
+ }
+ break;
+ case EYtSettingType::PrimaryMedium:
+ if (!EnsureTupleSize(*setting, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(setting->Tail(), ctx)) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (used.HasFlags(EYtSettingType::InferScheme) && used.HasFlags(EYtSettingType::ForceInferScheme)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << "Setting " << ToString(EYtSettingType::InferScheme).Quote()
+ << " is not allowed with " << ToString(EYtSettingType::ForceInferScheme).Quote()));
+ return false;
+ }
+ if (used.HasFlags(EYtSettingType::SortLimitBy) && !used.HasFlags(EYtSettingType::Limit)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << "Setting " << ToString(EYtSettingType::SortLimitBy).Quote()
+ << " is not allowed without " << ToString(EYtSettingType::Limit).Quote()));
+ return false;
+ }
+ if (used.HasFlags(EYtSettingType::KeyFilter) && used.HasFlags(EYtSettingType::KeyFilter2)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << "Settings " << ToString(EYtSettingType::KeyFilter).Quote()
+ << " and " << ToString(EYtSettingType::KeyFilter2).Quote() << " can not be used together"));
+ return false;
+ }
+ if (used.HasFlags(EYtSettingType::Anonymous) && used.HasFlags(EYtSettingType::Expiration)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << ToString(EYtSettingType::Expiration).Quote()
+ << " setting cannot be used for anonymous tables"));
+ return false;
+ }
+
+ if (isTruncate) {
+ for (auto type: {EYtSettingType::MonotonicKeys}) {
+ if (used.HasFlags(type)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << ToString(type).Quote()
+ << " setting can not be used with TRUNCATE mode"));
+ return false;
+ }
+ }
+ } else {
+ for (auto type: {EYtSettingType::Expiration, EYtSettingType::Media, EYtSettingType::PrimaryMedium, EYtSettingType::KeepMeta}) {
+ if (used.HasFlags(type)) {
+ ctx.AddError(TIssue(ctx.GetPosition(settingsNode.Pos()), TStringBuilder()
+ << ToString(type).Quote()
+ << " setting can only be used with TRUNCATE mode"));
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+TExprNode::TPtr GetSetting(const TExprNode& settings, EYtSettingType type) {
+ for (auto& setting : settings.Children()) {
+ if (setting->ChildrenSize() != 0 && FromString<EYtSettingType>(setting->Child(0)->Content()) == type) {
+ return setting;
+ }
+ }
+ return nullptr;
+}
+
+TExprNode::TPtr UpdateSettingValue(const TExprNode& settings, EYtSettingType type, TExprNode::TPtr&& value, TExprContext& ctx) {
+ for (ui32 index = 0U; index < settings.ChildrenSize(); ++index) {
+ if (settings.Child(index)->ChildrenSize() != 0 && FromString<EYtSettingType>(settings.Child(index)->Head().Content()) == type) {
+ const auto setting = settings.Child(index);
+
+ auto newSetting = ctx.ChangeChildren(*setting, value ? TExprNode::TListType{setting->HeadPtr(), std::move(value)} : TExprNode::TListType{setting->HeadPtr()});
+ return ctx.ChangeChild(settings, index, std::move(newSetting));
+ }
+ }
+ return {};
+}
+
+TExprNode::TListType GetAllSettingValues(const TExprNode& settings, EYtSettingType type) {
+ TExprNode::TListType res;
+ for (auto& setting : settings.Children()) {
+ if (setting->ChildrenSize() == 2 && FromString<EYtSettingType>(setting->Child(0)->Content()) == type) {
+ res.push_back(setting->ChildPtr(1));
+ }
+ }
+ return res;
+}
+
+bool HasSetting(const TExprNode& settings, EYtSettingType type) {
+ return bool(GetSetting(settings, type));
+}
+
+bool HasAnySetting(const TExprNode& settings, EYtSettingTypes types) {
+ for (auto& child: settings.Children()) {
+ if (child->ChildrenSize() != 0 && types.HasFlags(FromString<EYtSettingType>(child->Child(0)->Content()))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool HasSettingsExcept(const TExprNode& settings, EYtSettingTypes types) {
+ for (auto& child: settings.Children()) {
+ if (child->ChildrenSize() != 0 && !types.HasFlags(FromString<EYtSettingType>(child->Child(0)->Content()))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool EqualSettingsExcept(const TExprNode& lhs, const TExprNode& rhs, EYtSettingTypes types) {
+ if (&lhs == &rhs)
+ return true;
+
+ TNodeSet set(lhs.ChildrenSize());
+ for (const auto& child: lhs.Children()) {
+ if (child->ChildrenSize() != 0 && !types.HasFlags(FromString<EYtSettingType>(child->Head().Content()))) {
+ set.emplace(child.Get());
+ }
+ }
+ for (const auto& child: rhs.Children()) {
+ if (child->ChildrenSize() != 0 && !types.HasFlags(FromString<EYtSettingType>(child->Head().Content()))) {
+ if (!set.erase(child.Get()))
+ return false;
+ }
+ }
+ return set.empty();
+}
+
+TExprNode::TPtr RemoveSetting(const TExprNode& settings, EYtSettingType type, TExprContext& ctx) {
+ TExprNode::TListType children;
+ for (auto child: settings.Children()) {
+ if (child->ChildrenSize() != 0 && FromString<EYtSettingType>(child->Child(0)->Content()) == type) {
+ continue;
+ }
+ children.push_back(child);
+ }
+
+ return ctx.NewList(settings.Pos(), std::move(children));
+}
+
+TExprNode::TPtr RemoveSettings(const TExprNode& settings, EYtSettingTypes types, TExprContext& ctx) {
+ TExprNode::TListType children;
+ for (auto child: settings.Children()) {
+ if (child->ChildrenSize() != 0 && types.HasFlags(FromString<EYtSettingType>(child->Child(0)->Content()))) {
+ continue;
+ }
+ children.push_back(child);
+ }
+
+ return ctx.NewList(settings.Pos(), std::move(children));
+}
+
+TExprNode::TPtr KeepOnlySettings(const TExprNode& settings, EYtSettingTypes types, TExprContext& ctx) {
+ TExprNode::TListType children;
+ for (auto child: settings.Children()) {
+ if (child->ChildrenSize() != 0 && !types.HasFlags(FromString<EYtSettingType>(child->Child(0)->Content()))) {
+ continue;
+ }
+ children.push_back(child);
+ }
+
+ return ctx.NewList(settings.Pos(), std::move(children));
+}
+
+TExprNode::TPtr AddSetting(const TExprNode& settings, EYtSettingType type, const TExprNode::TPtr& value, TExprContext& ctx) {
+ return AddSetting(settings, settings.Pos(), ToString(type), value, ctx);
+}
+
+TVector<TString> GetSettingAsColumnList(const TExprNode& settings, EYtSettingType type) {
+ TVector<TString> result;
+ if (auto node = GetSetting(settings, type)) {
+ for (auto& column : node->Child(1)->Children()) {
+ if (column->IsAtom()) {
+ result.emplace_back(column->Content());
+ } else {
+ YQL_ENSURE(column->IsList() && column->ChildrenSize() > 0);
+ result.emplace_back(column->Child(0)->Content());
+ }
+ }
+ }
+ return result;
+}
+
+TVector<std::pair<TString, bool>> GetSettingAsColumnPairList(const TExprNode& settings, EYtSettingType type) {
+ TVector<std::pair<TString, bool>> result;
+ if (auto node = GetSetting(settings, type)) {
+ for (auto& column : node->Child(1)->Children()) {
+ if (column->IsAtom()) {
+ result.emplace_back(column->Content(), true);
+ } else {
+ result.emplace_back(column->Child(0)->Content(), FromString<bool>(column->Child(1)->Content()));
+ }
+ }
+ }
+ return result;
+}
+
+TExprNode::TListType GetSettingAsColumnAtomList(const TExprNode& settings, EYtSettingType type) {
+ TExprNode::TListType result;
+ if (const auto node = GetSetting(settings, type)) {
+ for (const auto& column : node->Child(1)->Children()) {
+ if (column->IsAtom()) {
+ result.emplace_back(column);
+ } else {
+ YQL_ENSURE(column->IsList() && column->ChildrenSize() > 0);
+ result.emplace_back(column->HeadPtr());
+ }
+ }
+ }
+ return result;
+}
+
+std::vector<std::pair<TExprNode::TPtr, bool>> GetSettingAsColumnAtomPairList(const TExprNode& settings, EYtSettingType type) {
+ std::vector<std::pair<TExprNode::TPtr, bool>> result;
+ if (const auto node = GetSetting(settings, type)) {
+ for (const auto& column : node->Child(1)->Children()) {
+ if (column->IsAtom()) {
+ result.emplace_back(column, true);
+ } else {
+ result.emplace_back(column->HeadPtr(), FromString<bool>(column->Child(1)->Content()));
+ }
+ }
+ }
+ return result;
+}
+
+TExprNode::TPtr AddSettingAsColumnList(const TExprNode& settings, EYtSettingType type,
+ const TVector<TString>& columns, TExprContext& ctx)
+{
+ return AddSetting(settings, type, ToAtomList(columns, settings.Pos(), ctx), ctx);
+}
+
+TExprNode::TPtr ToColumnPairList(const TVector<std::pair<TString, bool>>& columns, TPositionHandle pos, TExprContext& ctx) {
+ if (AllOf(columns, [](const auto& pair) { return pair.second; })) {
+ TExprNode::TListType children;
+ for (auto& pair: columns) {
+ children.push_back(ctx.NewAtom(pos, pair.first));
+ }
+
+ return ctx.NewList(pos, std::move(children));
+ } else {
+ TExprNode::TListType children;
+ for (auto& pair: columns) {
+ children.push_back(ctx.NewList(pos, {
+ ctx.NewAtom(pos, pair.first),
+ ctx.NewAtom(pos, pair.second ? "1" : "0", TNodeFlags::Default),
+ }));
+ }
+
+ return ctx.NewList(pos, std::move(children));
+ }
+}
+
+TExprNode::TPtr AddSettingAsColumnPairList(const TExprNode& settings, EYtSettingType type,
+ const TVector<std::pair<TString, bool>>& columns, TExprContext& ctx)
+{
+ return AddSetting(settings, type, ToColumnPairList(columns, settings.Pos(), ctx), ctx);
+}
+
+TMaybe<TSampleParams> GetSampleParams(const TExprNode& settings) {
+ if (auto setting = GetSetting(settings, EYtSettingType::Sample)) {
+ auto value = setting->Child(1);
+ YQL_ENSURE(value->ChildrenSize() == 3);
+ return TSampleParams {
+ FromString<EYtSampleMode>(value->Child(0)->Content()),
+ FromString<double>(value->Child(1)->Content()),
+ FromString<ui64>(value->Child(2)->Content())
+ };
+ }
+
+ return Nothing();
+}
+
+TMaybe<ui64> GetMaxJobSizeForFirstAsPrimary(const TExprNode& settings) {
+ if (auto setting = NYql::GetSetting(settings, EYtSettingType::FirstAsPrimary)) {
+ if (setting->ChildrenSize() > 1) {
+ if (auto subsetting = NYql::GetSetting(*setting->Child(1), MaxJobSizeForFirstAsPrimaryName)) {
+ return FromString<ui64>(subsetting->Child(1)->Content());
+ }
+ }
+ }
+
+ return Nothing();
+}
+
+bool UseJoinReduceForSecondAsPrimary(const TExprNode &settings) {
+ if (auto setting = NYql::GetSetting(settings, EYtSettingType::FirstAsPrimary)) {
+ if (setting->ChildrenSize() > 1) {
+ return NYql::HasSetting(*setting->Child(1), JoinReduceForSecondAsPrimaryName);
+ }
+ }
+
+ return false;
+}
+
+ui32 GetMinChildrenForIndexedKeyFilter(EYtSettingType type) {
+ if (type == EYtSettingType::KeyFilter) {
+ return 2u;
+ }
+ YQL_ENSURE(type == EYtSettingType::KeyFilter2);
+ return 3u;
+}
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h b/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
new file mode 100644
index 0000000000..c0a27d1ec3
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_op_settings.h
@@ -0,0 +1,177 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/generic/flags.h>
+#include <util/system/types.h>
+#include <util/string/cast.h>
+#include <util/str_stl.h>
+
+#include <utility>
+
+namespace NYql {
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+enum class EYtWriteMode: ui32 {
+ Renew /* "renew" */,
+ RenewKeepMeta /* "renew_keep_meta" */,
+ Append /* "append" */,
+ Drop /* "drop" */,
+ Flush /* "flush" */,
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+enum class EYtSampleMode: ui32 {
+ System /* "system" */,
+ Bernoulli /* "bernoulli" */,
+};
+
+struct TSampleParams {
+ EYtSampleMode Mode;
+ double Percentage;
+ ui64 Repeat;
+
+ friend bool operator==(const TSampleParams& l, const TSampleParams& r) {
+ return l.Mode == r.Mode
+ && l.Percentage == r.Percentage
+ && l.Repeat == r.Repeat;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+enum class EYtSettingType: ui64 {
+ // Table reads
+ Initial = 1ull << 0 /* "initial" */,
+ InferScheme = 1ull << 1 /* "infer_scheme" "inferscheme" "infer_schema" "inferschema" */,
+ ForceInferScheme = 1ull << 2 /* "force_infer_schema" "forceinferschema" */,
+ DoNotFailOnInvalidSchema = 1ull << 3 /* "do_not_fail_on_invalid_schema" */,
+ DirectRead = 1ull << 4 /* "direct_read" "directread"*/,
+ View = 1ull << 5 /* "view" */,
+ Mode = 1ull << 6 /* "mode" */,
+ Scheme = 1ull << 7 /* "scheme" */,
+ WeakConcat = 1ull << 8 /* "weak_concat" */,
+ Anonymous = 1ull << 9 /* "anonymous" */,
+ WithQB = 1ull << 10 /* "with_qb" */,
+ Inline = 1ull << 11 /* "inline" */,
+ Sample = 1ull << 12 /* "sample" */,
+ JoinLabel = 1ull << 13 /* "joinLabel" */,
+ IgnoreNonExisting = 1ull << 14 /* "ignore_non_existing" "ignorenonexisting" */,
+ WarnNonExisting = 1ull << 15 /* "warn_non_existing" "warnnonexisting" */,
+ XLock = 1ull << 16 /* "xlock" */,
+ Unordered = 1ull << 17 /* "unordered" */,
+ NonUnique = 1ull << 18 /* "nonUnique" */,
+ UserSchema = 1ull << 19 /* "userschema" */,
+ UserColumns = 1ull << 20 /* "usercolumns" */,
+ StatColumns = 1ull << 21 /* "statcolumns" */,
+ SysColumns = 1ull << 22 /* "syscolumns" */,
+ IgnoreTypeV3 = 1ull << 23 /* "ignoretypev3" "ignore_type_v3" */,
+ // Table content
+ MemUsage = 1ull << 24 /* "memUsage" */,
+ ItemsCount = 1ull << 25 /* "itemsCount" */,
+ RowFactor = 1ull << 26 /* "rowFactor" */,
+ // Operations
+ Ordered = 1ull << 27 /* "ordered" */,
+ KeyFilter = 1ull << 28 /* "keyFilter" */,
+ KeyFilter2 = 1ull << 29 /* "keyFilter2" */,
+ Take = 1ull << 30 /* "take" */,
+ Skip = 1ull << 31 /* "skip" */,
+ Limit = 1ull << 32 /* "limit" */,
+ SortLimitBy = 1ull << 33 /* "sortLimitBy" */,
+ SortBy = 1ull << 34 /* "sortBy" */,
+ ReduceBy = 1ull << 35 /* "reduceBy" */,
+ ReduceFilterBy = 1ull << 36 /* "reduceFilterBy" */,
+ ForceTransform = 1ull << 37 /* "forceTransform" */,
+ WeakFields = 1ull << 38 /* "weakFields" */,
+ Sharded = 1ull << 39 /* "sharded" */,
+ CombineChunks = 1ull << 40 /* "combineChunks" */,
+ JobCount = 1ull << 41 /* "jobCount" */,
+ JoinReduce = 1ull << 42 /* "joinReduce" */,
+ FirstAsPrimary = 1ull << 43 /* "firstAsPrimary" */,
+ Flow = 1ull << 44 /* "flow" */,
+ KeepSorted = 1ull << 45 /* "keepSorted" */,
+ KeySwitch = 1ull << 46 /* "keySwitch" */,
+ // Out tables
+ UniqueBy = 1ull << 47 /* "uniqueBy" */,
+ OpHash = 1ull << 48 /* "opHash" */,
+ MapOutputType = 1ull << 49 /* "mapOutputType" */,
+ ReduceInputType = 1ull << 50 /* "reduceInputType" */,
+ NoDq = 1ull << 51 /* "noDq" */,
+ Split = 1ull << 52 /* "split" */,
+ // Write hints
+ CompressionCodec = 1ull << 53 /* "compression_codec" "compressioncodec"*/,
+ ErasureCodec = 1ull << 54 /* "erasure_codec" "erasurecodec" */,
+ Expiration = 1ull << 55 /* "expiration" */,
+ ReplicationFactor = 1ull << 56 /* "replication_factor" "replicationfactor" */,
+ UserAttrs = 1ull << 57 /* "user_attrs", "userattrs" */,
+ Media = 1ull << 58 /* "media" */,
+ PrimaryMedium = 1ull << 59 /* "primary_medium", "primarymedium" */,
+ KeepMeta = 1ull << 60 /* "keep_meta", "keepmeta" */,
+ MonotonicKeys = 1ull << 61 /* "monotonic_keys", "monotonickeys" */,
+};
+
+Y_DECLARE_FLAGS(EYtSettingTypes, EYtSettingType);
+Y_DECLARE_OPERATORS_FOR_FLAGS(EYtSettingTypes);
+
+constexpr auto DqReadSupportedSettings = EYtSettingType::SysColumns | EYtSettingType::Sample | EYtSettingType::Unordered | EYtSettingType::NonUnique;
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+bool ValidateSettings(const TExprNode& settingsNode, EYtSettingTypes accepted, TExprContext& ctx);
+
+template <class TContainer>
+TExprNode::TPtr ToAtomList(const TContainer& columns, TPositionHandle pos, TExprContext& ctx) {
+ TExprNode::TListType children;
+ for (auto& column : columns) {
+ children.push_back(ctx.NewAtom(pos, column));
+ }
+
+ return ctx.NewList(pos, std::move(children));
+}
+
+TExprNode::TPtr ToColumnPairList(const TVector<std::pair<TString, bool>>& columns, TPositionHandle pos, TExprContext& ctx);
+
+TExprNode::TPtr GetSetting(const TExprNode& settings, EYtSettingType type);
+TExprNode::TPtr UpdateSettingValue(const TExprNode& settings, EYtSettingType type, TExprNode::TPtr&& value, TExprContext& ctx);
+
+TExprNode::TListType GetAllSettingValues(const TExprNode& settings, EYtSettingType type);
+TVector<TString> GetSettingAsColumnList(const TExprNode& settings, EYtSettingType type);
+TVector<std::pair<TString, bool>> GetSettingAsColumnPairList(const TExprNode& settings, EYtSettingType type);
+
+TExprNode::TListType GetSettingAsColumnAtomList(const TExprNode& settings, EYtSettingType type);
+std::vector<std::pair<TExprNode::TPtr, bool>> GetSettingAsColumnAtomPairList(const TExprNode& settings, EYtSettingType type);
+
+bool HasSetting(const TExprNode& settings, EYtSettingType type);
+bool HasAnySetting(const TExprNode& settings, EYtSettingTypes types);
+bool HasSettingsExcept(const TExprNode& settings, EYtSettingTypes types);
+bool EqualSettingsExcept(const TExprNode& lhs, const TExprNode& rhs, EYtSettingTypes types);
+
+TExprNode::TPtr RemoveSetting(const TExprNode& settings, EYtSettingType type, TExprContext& ctx);
+TExprNode::TPtr RemoveSettings(const TExprNode& settings, EYtSettingTypes types, TExprContext& ctx);
+TExprNode::TPtr KeepOnlySettings(const TExprNode& settings, EYtSettingTypes types, TExprContext& ctx);
+TExprNode::TPtr AddSetting(const TExprNode& settings, EYtSettingType type, const TExprNode::TPtr& value, TExprContext& ctx);
+TExprNode::TPtr AddSettingAsColumnList(const TExprNode& settings, EYtSettingType type,
+ const TVector<TString>& columns, TExprContext& ctx);
+TExprNode::TPtr AddSettingAsColumnPairList(const TExprNode& settings, EYtSettingType type,
+ const TVector<std::pair<TString, bool>>& columns, TExprContext& ctx);
+TMaybe<TSampleParams> GetSampleParams(const TExprNode& settings);
+
+const TStringBuf JoinReduceForSecondAsPrimaryName = "joinReduceForSecond";
+const TStringBuf MaxJobSizeForFirstAsPrimaryName = "maxJobSize";
+
+TMaybe<ui64> GetMaxJobSizeForFirstAsPrimary(const TExprNode& settings);
+bool UseJoinReduceForSecondAsPrimary(const TExprNode& settings);
+
+ui32 GetMinChildrenForIndexedKeyFilter(EYtSettingType type);
+
+} // NYql
+
+
+template <>
+struct THash<NYql::TSampleParams> {
+ size_t operator()(const NYql::TSampleParams& p) const {
+ return CombineHashes(NumericHash(static_cast<ui32>(p.Mode)), CombineHashes(THash<double>()(p.Percentage), NumericHash(p.Repeat)));
+ }
+};
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
new file mode 100644
index 0000000000..ffa2509037
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_optimize.cpp
@@ -0,0 +1,674 @@
+#include "yql_yt_optimize.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_table.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_provider_impl.h"
+
+#include <ydb/library/yql/providers/yt/lib/res_pull/table_limiter.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/type_ann/type_ann_expr.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/core/yql_expr_constraint.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_csee.h>
+#include <ydb/library/yql/public/udf/udf_value.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/core/services/yql_transform_pipeline.h>
+
+#include <util/generic/xrange.h>
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+#include <util/generic/size_literals.h>
+#include <util/generic/maybe.h>
+
+#include <utility>
+
+namespace NYql {
+
+using namespace NNodes;
+namespace {
+TMaybeNode<TYtSection> MaterializeSectionIfRequired(TExprBase world, TYtSection section, TYtDSink dataSink, bool keepSortness,
+ const TExprNode::TListType& limitNodes, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ const bool hasLimit = NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip);
+ bool needMaterialize = hasLimit && NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample);
+ bool hasDynamic = false;
+ if (!needMaterialize) {
+ bool hasRanges = false;
+ for (TYtPath path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ hasDynamic = hasDynamic || (pathInfo.Table->Meta && pathInfo.Table->Meta->IsDynamic);
+ hasRanges = hasRanges || pathInfo.Ranges;
+ }
+ needMaterialize = hasRanges || (hasLimit && hasDynamic);
+ }
+
+ if (needMaterialize) {
+ auto scheme = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ auto path = CopyOrTrivialMap(section.Pos(),
+ world, dataSink,
+ *scheme,
+ Build<TYtSection>(ctx, section.Pos())
+ .Paths(section.Paths())
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(),
+ EYtSettingType::Take | EYtSettingType::Skip |
+ EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::JoinLabel |
+ EYtSettingType::Unordered | EYtSettingType::NonUnique | EYtSettingType::StatColumns, ctx))
+ .Done(),
+ ctx,
+ state,
+ TCopyOrTrivialMapOpts()
+ .SetTryKeepSortness(keepSortness || (!ctx.IsConstraintEnabled<TSortedConstraintNode>() && (!hasDynamic || (!hasLimit && limitNodes.empty()))))
+ .SetRangesResetSort(false)
+ .SetSectionUniq(section.Ref().GetConstraint<TDistinctConstraintNode>())
+ .SetLimitNodes(limitNodes)
+ );
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings(NYql::RemoveSetting(section.Settings().Ref(), EYtSettingType::Sample, ctx))
+ .Done();
+ }
+
+ return {};
+}
+
+TMaybeNode<TYtSection> UpdateSectionWithRange(TExprBase world, TYtSection section, const TRecordsRange& range,
+ TYtDSink dataSink, bool keepSortness, bool allowWorldDeps, bool allowMaterialize, TSyncMap& syncList, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ bool isEmptyInput = allowWorldDeps;
+ TVector<TYtPath> updatedPaths;
+ TVector<TYtPath> skippedPaths;
+ if (auto limiter = TTableLimiter(range)) {
+ if (auto materialized = MaterializeSectionIfRequired(world, section, dataSink, keepSortness,
+ {NYql::KeepOnlySettings(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::SysColumns, ctx)}, state, ctx))
+ {
+ if (!allowMaterialize || state->Types->EvaluationInProgress) {
+ // Keep section as is
+ return {};
+ }
+ if (!allowWorldDeps) {
+ if (const auto out = materialized.Paths().Item(0).Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+ return materialized;
+ }
+
+ for (size_t i: xrange(section.Paths().Size())) {
+ auto path = section.Paths().Item(i);
+ TYtPathInfo pathInfo(path);
+ if (!pathInfo.Table->Stat) {
+ // Not all tables have required info
+ return {};
+ }
+
+ ui64 startRecordInTable = 0;
+ ui64 endRecordInTable = 0;
+
+ if (pathInfo.Table->Stat->RecordsCount) {
+ if (!limiter.NextTable(pathInfo.Table->Stat->RecordsCount)) {
+ if (allowWorldDeps) {
+ skippedPaths.push_back(path);
+ } else {
+ pathInfo.Stat.Drop();
+ pathInfo.Ranges = TYtRangesInfo::MakeEmptyRange();
+ updatedPaths.push_back(pathInfo.ToExprNode(ctx, path.Pos(), path.Table()).Cast<TYtPath>());
+ }
+ continue;
+ }
+ startRecordInTable = limiter.GetTableStart();
+ endRecordInTable = limiter.GetTableZEnd(); // 0 means the entire table usage
+ }
+
+ if (startRecordInTable || endRecordInTable) {
+ pathInfo.Stat.Drop();
+ pathInfo.Ranges = MakeIntrusive<TYtRangesInfo>();
+ TYtRangesInfo::TRowRange range;
+ if (startRecordInTable) {
+ range.Lower = startRecordInTable;
+ }
+ if (endRecordInTable) {
+ range.Upper = endRecordInTable;
+ }
+ pathInfo.Ranges->AddRowRange(range);
+ updatedPaths.push_back(pathInfo.ToExprNode(ctx, path.Pos(), path.Table()).Cast<TYtPath>());
+ } else {
+ updatedPaths.push_back(path);
+ }
+ isEmptyInput = false;
+ if (limiter.Exceed()) {
+ if (allowWorldDeps) {
+ for (size_t j = i + 1; j < section.Paths().Size(); ++j) {
+ skippedPaths.push_back(section.Paths().Item(j));
+ }
+ } else {
+ for (size_t j = i + 1; j < section.Paths().Size(); ++j) {
+ auto path = section.Paths().Item(j);
+ path = Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Ranges<TExprList>()
+ .Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ updatedPaths.push_back(path);
+ }
+ }
+ break;
+ }
+ }
+ } else if (!allowWorldDeps) {
+ for (auto path: section.Paths()) {
+ updatedPaths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Ranges<TExprList>()
+ .Build()
+ .Stat<TCoVoid>().Build()
+ .Done());
+ }
+ }
+
+ if (isEmptyInput) {
+ skippedPaths.assign(section.Paths().begin(), section.Paths().end());
+ }
+ for (auto path: skippedPaths) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+
+ if (isEmptyInput) {
+ return MakeEmptySection(section, dataSink, keepSortness, state, ctx);
+ }
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip, ctx))
+ .Done();
+}
+
+void EnableKeyBoundApi(TYtPathInfo& pathInfo, const TYtState::TPtr& state) {
+ if (!pathInfo.Ranges) {
+ return;
+ }
+ YQL_ENSURE(pathInfo.Table);
+ const bool useKeyBoundApi =
+ state->Configuration->_UseKeyBoundApi.Get(pathInfo.Table->Cluster).GetOrElse(DEFAULT_USE_KEY_BOUND_API);
+ pathInfo.Ranges->SetUseKeyBoundApi(useKeyBoundApi);
+}
+
+TMaybeNode<TYtSection> UpdateSectionWithLegacyFilters(TYtSection section, const TVector<TExprBase>& filters, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ TVector<TExprBase> commonFilters;
+ TMap<size_t, TVector<TExprBase>> tableFilters;
+ for (auto filter: filters) {
+ auto filterList = filter.Cast<TExprList>();
+ if (filterList.Size() == 2) {
+ tableFilters[FromString<size_t>(filterList.Item(1).Cast<TCoAtom>().Value())].push_back(filterList.Item(0));
+ }
+ else {
+ commonFilters.push_back(filterList.Item(0));
+ }
+ }
+
+ TVector<TYtPath> updatedPaths;
+ size_t tableIndex = 0;
+ for (auto path: section.Paths()) {
+ if (commonFilters.size() == filters.size()) {
+ TYtPathInfo pathInfo(path);
+ pathInfo.Stat.Drop();
+ pathInfo.Ranges = TYtRangesInfo::ApplyLegacyKeyFilters(commonFilters, pathInfo.Table->RowSpec, ctx);
+ EnableKeyBoundApi(pathInfo, state);
+ updatedPaths.push_back(pathInfo.ToExprNode(ctx, path.Pos(), path.Table()).Cast<TYtPath>());
+ }
+ else {
+ TVector<TExprBase> pathFilters = commonFilters;
+ if (auto p = tableFilters.FindPtr(tableIndex)) {
+ pathFilters.insert(pathFilters.end(), p->begin(), p->end());
+ }
+ if (pathFilters.empty()) {
+ updatedPaths.push_back(path);
+ }
+ else {
+ TYtPathInfo pathInfo(path);
+ pathInfo.Stat.Drop();
+ pathInfo.Ranges = TYtRangesInfo::ApplyLegacyKeyFilters(pathFilters, pathInfo.Table->RowSpec, ctx);
+ EnableKeyBoundApi(pathInfo, state);
+ updatedPaths.push_back(pathInfo.ToExprNode(ctx, path.Pos(), path.Table()).Cast<TYtPath>());
+ }
+ }
+ ++tableIndex;
+ }
+
+ auto updatedSettings = NYql::RemoveSetting(section.Settings().Ref(), EYtSettingType::KeyFilter, ctx);
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::KeyFilter, ctx.NewList(section.Pos(), {}), ctx);
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(updatedSettings)
+ .Done();
+}
+
+TMaybeNode<TYtSection> UpdateSectionWithFilters(TYtSection section, const TVector<TExprBase>& filters, const TYtState::TPtr& state, TExprContext& ctx) {
+ TMap<size_t, TExprNode::TPtr> filtersByTableIndex;
+ TExprNode::TPtr commonFilter;
+ for (auto filter: filters) {
+ auto filterList = filter.Cast<TExprList>();
+ auto computedFilter = filterList.Item(0).Ptr();
+ if (filterList.Size() == 3) {
+ for (auto idxNode : filterList.Item(2).Cast<TCoAtomList>()) {
+ size_t idx = FromString<size_t>(idxNode.Value());
+ YQL_ENSURE(!filtersByTableIndex.contains(idx));
+ filtersByTableIndex[idx] = computedFilter;
+ }
+ } else {
+ YQL_ENSURE(!commonFilter);
+ commonFilter = computedFilter;
+ }
+ }
+
+ YQL_ENSURE(filtersByTableIndex.empty() && commonFilter || !commonFilter && !filtersByTableIndex.empty());
+
+ TVector<TYtPath> updatedPaths;
+ size_t tableIndex = 0;
+ for (auto path: section.Paths()) {
+ TExprNode::TPtr filter;
+ if (commonFilter) {
+ filter = commonFilter;
+ } else {
+ auto it = filtersByTableIndex.find(tableIndex);
+ if (it != filtersByTableIndex.end()) {
+ filter = it->second;
+ }
+ }
+
+ if (!filter) {
+ updatedPaths.push_back(path);
+ } else {
+ TYtPathInfo pathInfo(path);
+ pathInfo.Stat.Drop();
+ pathInfo.Ranges = TYtRangesInfo::ApplyKeyFilter(*filter);
+ EnableKeyBoundApi(pathInfo, state);
+ updatedPaths.push_back(pathInfo.ToExprNode(ctx, path.Pos(), path.Table()).Cast<TYtPath>());
+ }
+ ++tableIndex;
+ }
+
+ auto updatedSettings = NYql::RemoveSetting(section.Settings().Ref(), EYtSettingType::KeyFilter2, ctx);
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::KeyFilter2, ctx.NewList(section.Pos(), {}), ctx);
+
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(updatedSettings)
+ .Done();
+}
+
+} //namespace
+
+TMaybeNode<TYtSection> UpdateSectionWithSettings(TExprBase world, TYtSection section, TYtDSink dataSink, bool keepSortness, bool allowWorldDeps, bool allowMaterialize,
+ TSyncMap& syncList, const TYtState::TPtr& state, TExprContext& ctx)
+{
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::DirectRead)) {
+ return {};
+ }
+
+ if (!NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2)) {
+ return {};
+ }
+
+ if (HasNodesToCalculate(section.Ptr())) {
+ return {};
+ }
+
+ TRecordsRange range;
+ TVector<TExprBase> keyFilters;
+ bool legacyKeyFilters = false;
+ for (auto s: section.Settings()) {
+ switch (FromString<EYtSettingType>(s.Name().Value())) {
+ case EYtSettingType::KeyFilter:
+ legacyKeyFilters = true;
+ [[fallthrough]];
+ case EYtSettingType::KeyFilter2:
+ if (s.Value().Cast<TExprList>().Size() > 0) {
+ keyFilters.push_back(s.Value().Cast());
+ }
+ break;
+ default:
+ // Skip other settings
+ break;
+ }
+ }
+ range.Fill(section.Settings().Ref());
+
+ if (range.Limit || range.Offset) {
+ return UpdateSectionWithRange(world, section, range, dataSink, keepSortness, allowWorldDeps, allowMaterialize, syncList, state, ctx);
+ }
+ if (!keyFilters.empty()) {
+ return legacyKeyFilters ? UpdateSectionWithLegacyFilters(section, keyFilters, state, ctx) : UpdateSectionWithFilters(section, keyFilters, state, ctx);
+ }
+
+ return {};
+}
+
+TYtSection MakeEmptySection(TYtSection section, NNodes::TYtDSink dataSink, bool keepSortness, const TYtState::TPtr& state, TExprContext& ctx) {
+ TYtOutTableInfo outTable(GetSequenceItemType(section, false)->Cast<TStructExprType>(),
+ state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ if (section.Paths().Size() == 1) {
+ auto srcTableInfo = TYtTableBaseInfo::Parse(section.Paths().Item(0).Table());
+ if (keepSortness && srcTableInfo->RowSpec && srcTableInfo->RowSpec->IsSorted()) {
+ outTable.RowSpec->CopySortness(*srcTableInfo->RowSpec, TYqlRowSpecInfo::ECopySort::WithCalc);
+ }
+ }
+ outTable.SetUnique(section.Ref().GetConstraint<TDistinctConstraintNode>(), section.Pos(), ctx);
+ return Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation<TYtTouch>()
+ .World<TCoWorld>().Build()
+ .DataSink(dataSink)
+ .Output()
+ .Add(outTable.ToExprNode(ctx, section.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Build()
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample, ctx))
+ .Done();
+}
+
+TExprNode::TPtr OptimizeReadWithSettings(const TExprNode::TPtr& node, bool allowWorldDeps, bool allowMaterialize, TSyncMap& syncList,
+ const TYtState::TPtr& state, TExprContext& ctx)
+{
+ auto read = TYtReadTable(node);
+ auto dataSink = TYtDSink(ctx.RenameNode(read.DataSource().Ref(), "DataSink"));
+
+ bool hasUpdates = false;
+ TVector<TExprBase> updatedSections;
+ for (auto section: read.Input()) {
+ updatedSections.push_back(section);
+ const bool keepSort = ctx.IsConstraintEnabled<TSortedConstraintNode>() && !NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered);
+ if (auto updatedSection = UpdateSectionWithSettings(read.World(), section, dataSink, keepSort, allowWorldDeps, allowMaterialize, syncList, state, ctx)) {
+ updatedSections.back() = updatedSection.Cast();
+ hasUpdates = true;
+ }
+ }
+ if (!hasUpdates) {
+ return node;
+ }
+
+ auto res = ctx.ChangeChild(read.Ref(), TYtReadTable::idx_Input,
+ Build<TYtSectionList>(ctx, read.Input().Pos())
+ .Add(updatedSections)
+ .Done().Ptr());
+
+ return res;
+}
+
+IGraphTransformer::TStatus UpdateTableContentMemoryUsage(const TExprNode::TPtr& input, TExprNode::TPtr& output, const TYtState::TPtr& state, TExprContext& ctx) {
+ auto current = input;
+ output.Reset();
+ for (;;) {
+ TProcessedNodesSet ignoreNodes;
+ VisitExpr(current, [&ignoreNodes](const TExprNode::TPtr& node) {
+ if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ ignoreNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ TOptimizeExprSettings settings(state->Types);
+ settings.CustomInstantTypeTransformer = state->Types->CustomInstantTypeTransformer.Get();
+ settings.ProcessedNodes = &ignoreNodes;
+
+ TParentsMap parentsMap;
+ GatherParents(*current, parentsMap);
+
+ TExprNode::TPtr newCurrent;
+ auto status = OptimizeExpr(current, newCurrent,
+ [&parentsMap, current, state](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (auto maybeContent = TMaybeNode<TYtTableContent>(node)) {
+ auto content = maybeContent.Cast();
+ if (NYql::HasSetting(content.Settings().Ref(), EYtSettingType::MemUsage)) {
+ return node;
+ }
+
+ ui64 collectRowFactor = 0;
+ if (auto setting = NYql::GetSetting(content.Settings().Ref(), EYtSettingType::RowFactor)) {
+ collectRowFactor = FromString<ui64>(setting->Child(1)->Content());
+ } else {
+ const auto contentItemType = content.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ size_t fieldsCount = 0;
+ switch (contentItemType->GetKind()) {
+ case ETypeAnnotationKind::Struct:
+ fieldsCount = contentItemType->Cast<TStructExprType>()->GetSize();
+ break;
+ case ETypeAnnotationKind::Tuple:
+ fieldsCount = contentItemType->Cast<TTupleExprType>()->GetSize();
+ break;
+ default:
+ break;
+ }
+ collectRowFactor = 2 * (1 + fieldsCount) * sizeof(NKikimr::NUdf::TUnboxedValuePod);
+ }
+
+ bool wrapToCollect = false;
+ TVector<std::pair<double, ui64>> factors; // first: sizeFactor, second: rowFactor
+ TNodeSet tableContentConsumers;
+ if (!GetTableContentConsumerNodes(*node, *current, parentsMap, tableContentConsumers)) {
+ wrapToCollect = true;
+ factors.emplace_back(2., collectRowFactor);
+ }
+ else {
+ for (auto consumer: tableContentConsumers) {
+ if (consumer->IsCallable({"ToDict","SqueezeToDict", "SqlIn"})) {
+ double sizeFactor = 1.;
+ ui64 rowFactor = 0ULL;
+ if (auto err = CalcToDictFactors(*consumer, ctx, sizeFactor, rowFactor)) {
+ ctx.AddError(*err);
+ return {};
+ }
+ factors.emplace_back(sizeFactor, rowFactor);
+ }
+ else if (consumer->IsCallable("Collect")) {
+ factors.emplace_back(2., collectRowFactor);
+ }
+ }
+ }
+
+ ui64 memUsage = 0;
+ ui64 itemsCount = 0;
+ bool useItemsCount = !NYql::HasSetting(content.Settings().Ref(), EYtSettingType::ItemsCount);
+
+ if (factors.empty()) {
+ // No ToDict or Collect consumers. Assume memory usage equals to max row size on YT
+ memUsage = 16_MB;
+ useItemsCount = false;
+ }
+ else {
+ if (auto maybeRead = content.Input().Maybe<TYtReadTable>()) {
+ TVector<ui64> records;
+ TVector<TYtPathInfo::TPtr> tableInfos;
+ bool hasNotCalculated = false;
+ for (auto section: maybeRead.Cast().Input()) {
+ for (auto path: section.Paths()) {
+ TYtPathInfo::TPtr info = MakeIntrusive<TYtPathInfo>(path);
+ if (info->Table->Stat) {
+ ui64 tableRecord = info->Table->Stat->RecordsCount;
+ if (info->Ranges) {
+ const auto used = info->Ranges->GetUsedRows(tableRecord);
+ tableRecord = used.GetOrElse(tableRecord);
+ if (used) {
+ itemsCount += *used;
+ } else {
+ useItemsCount = false;
+ }
+ } else {
+ itemsCount += tableRecord;
+ }
+ if (info->Table->Meta->IsDynamic) {
+ useItemsCount = false;
+ }
+ records.push_back(tableRecord);
+ tableInfos.push_back(info);
+ }
+ else {
+ YQL_CLOG(INFO, ProviderYt) << "Assume 1Gb memory usage for YtTableContent #"
+ << node->UniqueId() << " because input table is not calculated yet";
+ memUsage += 1_GB;
+ hasNotCalculated = true;
+ useItemsCount = false;
+ break;
+ }
+ }
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample)) {
+ useItemsCount = false;
+ }
+ if (hasNotCalculated) {
+ break;
+ }
+ }
+ if (!hasNotCalculated && !tableInfos.empty()) {
+ if (auto dataSizes = EstimateDataSize(TString{maybeRead.Cast().DataSource().Cluster().Value()}, tableInfos, Nothing(), *state, ctx)) {
+ YQL_ENSURE(dataSizes->size() == records.size());
+ for (size_t i: xrange(records.size())) {
+ for (auto& factor: factors) {
+ memUsage += factor.first * dataSizes->at(i) + factor.second * records.at(i);
+ }
+ }
+ } else {
+ return {};
+ }
+ }
+ }
+ else {
+ TYtOutTableInfo info(GetOutTable(content.Input().Cast<TYtOutput>()));
+ if (info.Stat) {
+ const ui64 dataSize = info.Stat->DataSize;
+ const ui64 records = info.Stat->RecordsCount;
+ for (auto& factor: factors) {
+ memUsage += factor.first * dataSize + factor.second * records;
+ }
+ itemsCount += records;
+ }
+ else {
+ YQL_CLOG(INFO, ProviderYt) << "Assume 1Gb memory usage for YtTableContent #"
+ << node->UniqueId() << " because input table is not calculated yet";
+ memUsage += 1_GB;
+ useItemsCount = false;
+ }
+ }
+ }
+
+ auto settings = content.Settings().Ptr();
+ settings = NYql::AddSetting(*settings, EYtSettingType::MemUsage, ctx.NewAtom(node->Pos(), ToString(memUsage), TNodeFlags::Default), ctx);
+ if (useItemsCount) {
+ settings = NYql::AddSetting(*settings, EYtSettingType::ItemsCount, ctx.NewAtom(node->Pos(), ToString(itemsCount), TNodeFlags::Default), ctx);
+ }
+
+ return ctx.WrapByCallableIf(wrapToCollect, "Collect", ctx.ChangeChild(*node, TYtTableContent::idx_Settings, std::move(settings)));
+ }
+ return node;
+ },
+ ctx, settings);
+
+ if (IGraphTransformer::TStatus::Error == status.Level) {
+ ctx.AddError(TIssue(ctx.GetPosition(current->Pos()), TStringBuilder() << "Failed to update YtTableContent memory usage in node: " << current->Content()));
+ return status;
+ }
+
+ if (newCurrent != current) {
+ if (current->IsLambda()) {
+ YQL_ENSURE(newCurrent->IsLambda());
+ YQL_ENSURE(newCurrent->Head().ChildrenSize() == current->Head().ChildrenSize());
+ for (size_t i = 0; i < newCurrent->Head().ChildrenSize(); ++i) {
+ newCurrent->Head().Child(i)->SetTypeAnn(current->Head().Child(i)->GetTypeAnn());
+ newCurrent->Head().Child(i)->CopyConstraints(*current->Head().Child(i));
+ }
+ }
+ auto typeTransformer = CreateTypeAnnotationTransformer(CreateExtCallableTypeAnnotationTransformer(*state->Types, true), *state->Types);
+ auto constrTransformer = CreateConstraintTransformer(*state->Types, true, true);
+ TVector<TTransformStage> transformers;
+ const auto issueCode = TIssuesIds::CORE_TYPE_ANN;
+ transformers.push_back(TTransformStage(typeTransformer, "TypeAnnotation", issueCode));
+ transformers.push_back(TTransformStage(
+ CreateFunctorTransformer([](const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) { return UpdateCompletness(input, output, ctx); }),
+ "UpdateCompletness", issueCode));
+ transformers.push_back(TTransformStage(constrTransformer, "Constraints", issueCode));
+ auto fullTransformer = CreateCompositeGraphTransformer(transformers, false);
+ status = InstantTransform(*fullTransformer, newCurrent, ctx);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return status;
+ }
+
+ current = newCurrent;
+ continue;
+ }
+
+ output = current;
+ return IGraphTransformer::TStatus::Ok;
+ }
+}
+
+template<bool WithWideFlow>
+struct TPeepholePipelineConfigurator : public IPipelineConfigurator {
+ TPeepholePipelineConfigurator(TYtState::TPtr state): State_(std::move(state)) {}
+private:
+ void AfterCreate(TTransformationPipeline*) const final {}
+
+ void AfterTypeAnnotation(TTransformationPipeline* pipeline) const final {
+ pipeline->Add(CreateTYtPeepholeTransformer(State_), "Peephole");
+ if constexpr (WithWideFlow) {
+ pipeline->Add(CreateTYtWideFlowTransformer(State_), "WideFlow");
+ }
+ }
+
+ void AfterOptimize(TTransformationPipeline*) const final {}
+
+ const TYtState::TPtr State_;
+};
+
+template<bool ForNativeExecution>
+IGraphTransformer::TStatus PeepHoleOptimizeBeforeExec(TExprNode::TPtr input, TExprNode::TPtr& output,
+ const TYtState::TPtr& state, bool& hasNonDeterministicFunctions, TExprContext& ctx)
+{
+ if constexpr (ForNativeExecution) {
+ if (const auto status = UpdateTableContentMemoryUsage(input, output, state, ctx);
+ status.Level != IGraphTransformer::TStatus::Ok) {
+ return status;
+ }
+ }
+
+ const TPeepholePipelineConfigurator<ForNativeExecution> wideFlowTransformers(state);
+ TPeepholeSettings peepholeSettings;
+ peepholeSettings.CommonConfig = &wideFlowTransformers;
+ return PeepHoleOptimizeNode(output, output, ctx, *state->Types, nullptr, hasNonDeterministicFunctions, peepholeSettings);
+}
+
+template
+IGraphTransformer::TStatus PeepHoleOptimizeBeforeExec<true>(TExprNode::TPtr input, TExprNode::TPtr& output,
+ const TYtState::TPtr& state, bool& hasNonDeterministicFunctions, TExprContext& ctx);
+
+template
+IGraphTransformer::TStatus PeepHoleOptimizeBeforeExec<false>(TExprNode::TPtr input, TExprNode::TPtr& output,
+ const TYtState::TPtr& state, bool& hasNonDeterministicFunctions, TExprContext& ctx);
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_optimize.h b/ydb/library/yql/providers/yt/provider/yql_yt_optimize.h
new file mode 100644
index 0000000000..05287adf36
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_optimize.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/peephole_opt/yql_opt_peephole_physical.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+#include <tuple>
+
+namespace NYql {
+
+// writer -> (reader, YtSection, YtOutput, YtPath)
+using TOpDeps = TNodeMap<TVector<std::tuple<const TExprNode*, const TExprNode*, const TExprNode*, const TExprNode*>>>;
+
+NNodes::TMaybeNode<NNodes::TYtSection> UpdateSectionWithSettings(NNodes::TExprBase world, NNodes::TYtSection section, NNodes::TYtDSink dataSink,
+ bool keepSortness, bool allowWorldDeps, bool allowMaterialize, TSyncMap& syncList, const TYtState::TPtr& state, TExprContext& ctx);
+
+NNodes::TYtSection MakeEmptySection(NNodes::TYtSection section, NNodes::TYtDSink dataSink,
+ bool keepSortness, const TYtState::TPtr& state, TExprContext& ctx);
+
+TExprNode::TPtr OptimizeReadWithSettings(const TExprNode::TPtr& node, bool allowWorldDeps, bool allowMaterialize, TSyncMap& syncList,
+ const TYtState::TPtr& state, TExprContext& ctx);
+
+IGraphTransformer::TStatus UpdateTableContentMemoryUsage(const TExprNode::TPtr& input, TExprNode::TPtr& output,
+ const TYtState::TPtr& state, TExprContext& ctx);
+
+template<bool ForNativeExecution>
+IGraphTransformer::TStatus PeepHoleOptimizeBeforeExec(TExprNode::TPtr input, TExprNode::TPtr& output,
+ const TYtState::TPtr& state, bool& hasNonDeterministicFunctions, TExprContext& ctx);
+
+} //NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
new file mode 100644
index 0000000000..d405a27059
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_peephole.cpp
@@ -0,0 +1,64 @@
+#include "yql_yt_provider_impl.h"
+
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/providers/common/transform/yql_optimize.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+class TYtPeepholeTransformer : public TOptimizeTransformerBase {
+public:
+ TYtPeepholeTransformer(TYtState::TPtr state)
+ : TOptimizeTransformerBase(state ? state->Types : nullptr, NLog::EComponent::ProviderYt, {}), State_(state)
+ {
+#define HNDL(name) "Peephole-"#name, Hndl(&TYtPeepholeTransformer::name)
+ AddHandler(0, &TYtLength::Match, HNDL(OptimizeLength));
+#undef HNDL
+ }
+private:
+ TMaybeNode<TExprBase> OptimizeLength(TExprBase node, TExprContext& ctx) {
+ std::optional<size_t> lengthRes;
+ const auto& input = node.Cast<TYtLength>().Input();
+ if (const auto& out = input.Maybe<TYtOutput>()) {
+ if (const auto& info = TYtTableBaseInfo::Parse(out.Cast()); info->Stat) {
+ lengthRes = info->Stat->RecordsCount;
+ }
+ } else if (const auto& read = input.Maybe<TYtReadTable>()) {
+ lengthRes = 0ULL;
+ for (auto path: read.Cast().Input().Item(0).Paths()) {
+ if (const auto& info = TYtTableBaseInfo::Parse(path.Table()); info->Stat && info->Meta && !info->Meta->IsDynamic && path.Ranges().Maybe<TCoVoid>())
+ lengthRes = *lengthRes + info->Stat->RecordsCount;
+ else {
+ lengthRes = std::nullopt;
+ break;
+ }
+ }
+ }
+
+ if (lengthRes) {
+ return Build<TCoUint64>(ctx, node.Pos())
+ .Literal().Value(ToString(*lengthRes), TNodeFlags::Default).Build()
+ .Done();
+ }
+
+ return node;
+ }
+
+ const TYtState::TPtr State_;
+};
+
+}
+
+THolder<IGraphTransformer> CreateTYtPeepholeTransformer(TYtState::TPtr state) {
+ return MakeHolder<TYtPeepholeTransformer>(std::move(state));
+}
+
+} // namespace NYql
+
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
new file mode 100644
index 0000000000..0ee052297b
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_physical_finalizing.cpp
@@ -0,0 +1,2544 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_provider.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_optimize.h"
+#include "yql_yt_horizontal_join.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/utils/log/log.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/string.h>
+#include <util/generic/xrange.h>
+#include <util/generic/bitmap.h>
+#include <util/generic/map.h>
+#include <util/generic/hash.h>
+#include <util/generic/algorithm.h>
+#include <util/string/cast.h>
+#include <util/str_stl.h>
+
+#include <utility>
+#include <unordered_set>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+TYtOutputOpBase GetRealOperation(TExprBase op) {
+ if (const auto mayTry = op.Maybe<TYtTryFirst>())
+ return mayTry.Cast().First();
+ return op.Cast<TYtOutputOpBase>();
+}
+
+class TYtPhysicalFinalizingTransformer : public TSyncTransformerBase {
+public:
+ TYtPhysicalFinalizingTransformer(TYtState::TPtr state)
+ : State_(state)
+ {
+ }
+
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) final {
+ output = input;
+
+ TOpDeps opDeps;
+ TNodeSet hasWorldDeps; // Operations, which have world dependencies on them from another nodes
+ TNodeSet toCombine;
+ TNodeSet neverCombine;
+ TNodeSet lefts;
+ TNodeMap<TString> commits;
+
+ bool enableChunkCombining = IsChunkCombiningEnabled();
+
+ VisitExpr(input, [&](const TExprNode::TPtr& node)->bool {
+ if (auto maybeOp = TMaybeNode<TYtTransientOpBase>(node)) {
+ auto op = maybeOp.Cast();
+ for (auto section: op.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto maybeOutput = path.Table().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(op.Raw(), section.Raw(), out.Raw(), path.Raw());
+ if (enableChunkCombining) {
+ CollectForCombine(out, toCombine, neverCombine);
+ }
+ }
+ }
+ }
+ }
+ else if (auto maybeRead = TMaybeNode<TYtReadTable>(node)) {
+ auto read = maybeRead.Cast();
+ for (auto section: read.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto maybeOutput = path.Table().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(read.Raw(), section.Raw(), out.Raw(), path.Raw());
+ if (enableChunkCombining) {
+ CollectForCombine(out, toCombine, neverCombine);
+ }
+ }
+ }
+ }
+ }
+ else if (auto maybePublish = TMaybeNode<TYtPublish>(node)) {
+ auto publish = maybePublish.Cast();
+ for (auto out: publish.Input()) {
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(publish.Raw(), nullptr, out.Raw(), nullptr);
+ }
+ }
+ else if (auto maybeLength = TMaybeNode<TYtLength>(node)) {
+ auto length = maybeLength.Cast();
+ if (auto maybeOutput = length.Input().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(length.Raw(), nullptr, out.Raw(), nullptr);
+ }
+ }
+ else if (auto maybeTableContent = TMaybeNode<TYtTableContent>(node)) {
+ auto tableContent = maybeTableContent.Cast();
+ if (auto maybeOutput = tableContent.Input().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(tableContent.Raw(), nullptr, out.Raw(), nullptr);
+ if (enableChunkCombining) {
+ CollectForCombine(out, toCombine, neverCombine);
+ }
+ }
+ }
+ else if (auto maybeResWrite = TMaybeNode<TResWriteBase>(node)) {
+ auto resWrite = maybeResWrite.Cast();
+ if (auto maybeOutput = resWrite.Data().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(resWrite.Raw(), nullptr, out.Raw(), nullptr);
+ if (enableChunkCombining) {
+ CollectForCombine(out, toCombine, neverCombine);
+ }
+ }
+ }
+ else if (auto maybeSqlIn = TMaybeNode<TCoSqlIn>(node)) {
+ auto sqlIn = maybeSqlIn.Cast();
+ if (auto maybeOutput = sqlIn.Collection().Maybe<TYtOutput>()) {
+ auto out = maybeOutput.Cast();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(sqlIn.Raw(), nullptr, out.Raw(), nullptr);
+ }
+ }
+ else if (auto maybeStatOut = TMaybeNode<TYtStatOut>(node)) {
+ auto statOut = maybeStatOut.Cast();
+ auto out = statOut.Input();
+ opDeps[GetRealOperation(out.Operation()).Raw()].emplace_back(statOut.Raw(), nullptr, out.Raw(), nullptr);
+ if (enableChunkCombining) {
+ CollectForCombine(out, toCombine, neverCombine);
+ }
+ }
+ else if (auto maybeLeft = TMaybeNode<TCoLeft>(node)) {
+ if (auto maybeOp = maybeLeft.Input().Maybe<TYtOutputOpBase>()) {
+ hasWorldDeps.insert(maybeOp.Cast().Raw());
+ }
+ lefts.insert(maybeLeft.Raw());
+ }
+ else if (auto maybeCommit = TMaybeNode<TCoCommit>(node)) {
+ if (auto ds = maybeCommit.DataSink().Maybe<TYtDSink>()) {
+ if (ProcessedMergePublish.find(node->UniqueId()) == ProcessedMergePublish.end()) {
+ commits.emplace(node.Get(), ds.Cast().Cluster().Value());
+ }
+ }
+ }
+
+ return true;
+ });
+
+ const auto disableOptimizers = State_->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>());
+
+ if (!disableOptimizers.contains("MergePublish")) {
+ for (auto& c: commits) {
+ THashMap<TString, TVector<const TExprNode*>> groupedPublishesByDst;
+ VisitExprByFirst(*c.first, [&](const TExprNode& node) {
+ if (auto maybePub = TMaybeNode<TYtPublish>(&node)) {
+ auto pub = maybePub.Cast();
+ if (pub.DataSink().Cluster().Value() == c.second && !NYql::HasSetting(pub.Settings().Ref(), EYtSettingType::MonotonicKeys)) {
+ groupedPublishesByDst[TString{pub.Publish().Name().Value()}].push_back(&node);
+ }
+ } else if (auto maybeCommit = TMaybeNode<TCoCommit>(&node)) {
+ if (auto maybeSink = maybeCommit.DataSink().Maybe<TYtDSink>()) {
+ // Stop traversing when got another commit on the same cluster
+ return maybeSink.Cast().Cluster().Value() != c.second || &node == c.first;
+ }
+ }
+ return true;
+ });
+ TVector<TString> dstTables;
+ for (auto& grp: groupedPublishesByDst) {
+ if (grp.second.size() > 1) {
+ dstTables.push_back(grp.first);
+ }
+ }
+ if (dstTables.size() > 1) {
+ ::Sort(dstTables);
+ }
+ for (auto& tbl: dstTables) {
+ auto& grp = groupedPublishesByDst[tbl];
+ while (grp.size() > 1) {
+ TNodeOnNodeOwnedMap replaceMap;
+ // Optimize only two YtPublish nodes at once to properly update world dependencies
+ auto last = TYtPublish(grp[0]);
+ auto prev = TYtPublish(grp[1]);
+ if (last.World().Raw() != prev.Raw()) {
+ // Has extra dependencies. Don't merge
+ grp.erase(grp.begin());
+ continue;
+ }
+
+ auto mode = NYql::GetSetting(last.Settings().Ref(), EYtSettingType::Mode);
+ YQL_ENSURE(mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Append);
+ YQL_ENSURE(!NYql::HasSetting(last.Settings().Ref(), EYtSettingType::Initial));
+
+ replaceMap.emplace(grp[0],
+ Build<TYtPublish>(ctx, grp[0]->Pos())
+ .InitFrom(prev)
+ .Input()
+ .Add(prev.Input().Ref().ChildrenList())
+ .Add(last.Input().Ref().ChildrenList())
+ .Build()
+ .Done().Ptr()
+ );
+ replaceMap.emplace(grp[1], prev.World().Ptr());
+
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-MergePublish";
+ return RemapExpr(input, output, replaceMap, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ }
+ ProcessedMergePublish.insert(c.first->UniqueId());
+ }
+ }
+
+ TStatus status = TStatus::Ok;
+ if (!disableOptimizers.contains("BypassMergeBeforeLength")) {
+ status = BypassMergeBeforeLength(input, output, opDeps, lefts, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("AlignPublishTypes")) {
+ status = AlignPublishTypes(input, output, opDeps, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("SuppressOuts")) {
+ status = OptimizeUnusedOuts(input, output, opDeps, lefts, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("UnorderedOuts") && ctx.IsConstraintEnabled<TSortedConstraintNode>()) {
+ status = OptimizeUnorderedOuts(input, output, opDeps, lefts, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("FieldSubsetForMultiUsage")) {
+ status = OptimizeFieldSubsetForMultiUsage(input, output, opDeps, lefts, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (State_->Configuration->MaxInputTables.Get().Defined() || State_->Configuration->MaxInputTablesForSortedMerge.Get().Defined()) {
+ // Run it after UnorderedOuts, because sorted YtMerge may become unsorted after UnorderedOuts
+ status = SplitLargeInputs(input, output, ctx, !disableOptimizers.contains("SplitLargeMapInputs"));
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("MergeMultiOuts")) {
+ status = OptimizeMultiOuts(input, output, opDeps, lefts, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("FuseMultiOutsWithOuterMaps")) {
+ status = FuseMultiOutsWithOuterMaps(input, output, opDeps, lefts, hasWorldDeps, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (enableChunkCombining) {
+ EraseNodesIf(toCombine, [&neverCombine](const auto& entry) { return neverCombine.count(entry) != 0; });
+ if (!toCombine.empty()) {
+ status = AddChunkCombining(input, output, toCombine, lefts, opDeps, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+ toCombine.clear();
+ neverCombine.clear();
+ }
+ lefts.clear();
+
+ TParentsMap limits; // op -> settings with limits
+ TNodeSet noLimits; // writers
+ for (auto& x: opDeps) {
+ auto writer = x.first;
+ if (!TYtTransientOpBase::Match(writer)) {
+ continue;
+ }
+ if (NYql::HasSetting(*writer->Child(TYtTransientOpBase::idx_Settings), EYtSettingType::Limit)) {
+ continue;
+ }
+ if (writer->HasResult() && writer->GetResult().Type() == TExprNode::World) {
+ continue;
+ }
+
+ bool canHaveLimit = TYtTransientOpBase(writer).Output().Size() == 1;
+ if (canHaveLimit) {
+ TString usedCluster;
+ for (auto item: x.second) {
+ if (!std::get<1>(item)) { // YtLength, YtPublish
+ canHaveLimit = false;
+ break;
+ }
+ for (auto path: TYtSection(std::get<1>(item)).Paths()) {
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ canHaveLimit = false;
+ break;
+ }
+ }
+ if (!canHaveLimit) {
+ break;
+ }
+ bool hasTake = false;
+ for (auto setting: TYtSection(std::get<1>(item)).Settings()) {
+ auto kind = FromString<EYtSettingType>(setting.Name().Value());
+ if (EYtSettingType::Take == kind || EYtSettingType::Skip == kind) {
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(setting.Value().Ref(), syncList, usedCluster, true, false) || !syncList.empty()) {
+ hasTake = false;
+ break;
+ }
+ if (EYtSettingType::Take == kind) {
+ hasTake = true;
+ }
+ }
+ }
+
+ if (!hasTake) {
+ canHaveLimit = false;
+ break;
+ }
+ }
+ }
+
+ if (!canHaveLimit) {
+ noLimits.insert(writer);
+ continue;
+ }
+
+ auto& limit = limits[writer];
+ for (auto item: x.second) {
+ limit.insert(TYtSection(std::get<1>(item)).Settings().Raw());
+ }
+ }
+
+ for (auto writer : noLimits) {
+ limits.erase(writer);
+ }
+
+ status = OptimizeExpr(output, output, [&](const TExprNode::TPtr& node, TExprContext& ctx) {
+ if (!node->HasResult() || node->GetResult().Type() != TExprNode::World) {
+ const bool opWithJobs = TYtWithUserJobsOpBase::Match(node.Get());
+ if (opWithJobs || TYtSort::Match(node.Get()) || TYtMerge::Match(node.Get())) {
+ auto ret = Limits(node, limits, ctx);
+ if (ret != node) {
+ if (ret) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-Limits";
+ }
+ return ret;
+ }
+
+ if (opWithJobs) {
+ ret = LengthOverPhysicalList(node, opDeps, ctx);
+ if (ret != node) {
+ if (ret) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-LengthOverPhysicalList";
+ }
+ return ret;
+ }
+ ret = TopSortForProducers(node, opDeps, ctx);
+ if (ret != node) {
+ if (ret) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-TopSortForProducers";
+ }
+ return ret;
+ }
+ }
+ }
+ }
+ return node;
+ }, ctx, TOptimizeExprSettings(nullptr));
+
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+
+ if (!disableOptimizers.contains("HorizontalJoin")) {
+ status = THorizontalJoinOptimizer(State_, opDeps, hasWorldDeps, &ProcessedHorizontalJoin).Optimize(output, output, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("MultiHorizontalJoin")) {
+ status = TMultiHorizontalJoinOptimizer(State_, opDeps, hasWorldDeps).Optimize(output, output, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ if (!disableOptimizers.contains("OutHorizontalJoin")) {
+ status = TOutHorizontalJoinOptimizer(State_, opDeps, hasWorldDeps).Optimize(output, output, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+
+ return status;
+ }
+
+ void Rewind() final {
+ ProcessedMergePublish.clear();
+ ProcessedSplitLargeInputs.clear();
+ ProcessedUnusedOuts.clear();
+ ProcessedMultiOuts.clear();
+ ProcessedHorizontalJoin.clear();
+ ProcessedFieldSubsetForMultiUsage.clear();
+ ProcessedFuseWithOuterMaps.clear();
+ }
+
+private:
+
+ static THashSet<TStringBuf> OPS_WITH_SORTED_OUTPUT;
+
+ void CollectForCombine(const TYtOutput& output, TNodeSet& toCombine, TNodeSet& neverCombine)
+ {
+ if (neverCombine.find(output.Raw()) != neverCombine.end()) {
+ return;
+ }
+
+ const auto op = GetOutputOp(output);
+
+ if (auto maybeMerge = op.Maybe<TYtMerge>()) {
+ auto merge = maybeMerge.Cast();
+
+ if (NYql::HasSetting(merge.Settings().Ref(), EYtSettingType::CombineChunks)) {
+
+ // inputs of combining merge should never be combined
+ for (auto section: merge.Input()) {
+ for (auto path: section.Paths()) {
+ if (auto maybeOut = path.Table().Maybe<TYtOutput>()) {
+ auto out = maybeOut.Cast();
+ neverCombine.insert(out.Raw());
+ }
+ }
+ }
+ return;
+ }
+ }
+
+ if (!op.Ref().HasResult()) {
+ return;
+ }
+
+ auto outTable = GetOutTable(output).Cast<TYtOutTable>();
+ auto tableInfo = TYtTableBaseInfo::Parse(outTable);
+
+ TMaybe<ui64> maybeLimit = State_->Configuration->MinTempAvgChunkSize.Get();
+
+ if (!maybeLimit.Defined()) {
+ return;
+ }
+
+ ui64 limit = *maybeLimit.Get();
+
+ if (limit == 0) {
+ // always combine
+ toCombine.insert(output.Raw());
+ return;
+
+ }
+
+ YQL_ENSURE(tableInfo->Stat);
+ ui64 chunksCount = tableInfo->Stat->ChunkCount;
+ ui64 dataSize = tableInfo->Stat->DataSize;
+ if (chunksCount > 1 && dataSize > chunksCount) {
+ ui64 chunkSize = dataSize / chunksCount;
+
+ if (chunkSize < limit) {
+ toCombine.insert(output.Raw());
+ }
+ }
+ }
+
+ bool IsChunkCombiningEnabled()
+ {
+ return State_->Configuration->MinTempAvgChunkSize.Get().Defined();
+ }
+
+ TStatus AddChunkCombining(TExprNode::TPtr input, TExprNode::TPtr& output, const TNodeSet& toCombine,
+ const TNodeSet& lefts, const TOpDeps& opDeps, TExprContext& ctx)
+ {
+ TNodeOnNodeOwnedMap replaces;
+ TNodeOnNodeOwnedMap newOps; // Old output -> new op
+ for (auto node: toCombine) {
+ TYtOutput ytOutput(node);
+ const auto oldOp = GetOutputOp(ytOutput);
+
+ auto combiningOp =
+ Build<TYtMerge>(ctx, oldOp.Pos())
+ .World<TCoWorld>().Build()
+ .DataSink(oldOp.DataSink())
+ .Output()
+ .Add()
+ .InitFrom(GetOutTable(ytOutput).Cast<TYtOutTable>())
+ .Name().Value("").Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table<TYtOutput>() // clone to exclude infinitive loop in RemapExpr
+ .InitFrom(ytOutput)
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings<TCoNameValueTupleList>()
+ .Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Add()
+ .Name().Value(ToString(EYtSettingType::CombineChunks)).Build()
+ .Build()
+ .Build()
+ .Done();
+
+ auto newYtOutput =
+ Build<TYtOutput>(ctx, ytOutput.Pos())
+ .Operation(combiningOp)
+ .OutIndex().Value("0").Build()
+ .Done();
+
+ replaces[node] = newYtOutput.Ptr();
+ newOps[node] = combiningOp.Ptr();
+ }
+
+ if (!newOps.empty()) {
+ for (auto node: lefts) {
+ TCoLeft left(node);
+
+ if (auto maybeOp = left.Input().Maybe<TYtOutputOpBase>()) {
+ auto op = maybeOp.Cast();
+ auto depsIt = opDeps.find(op.Raw());
+ if (depsIt != opDeps.end()) {
+ TExprNode::TListType toSync;
+ for (const auto& dep : depsIt->second) {
+ const TExprNode* oldOutput = std::get<2>(dep);
+ auto it = newOps.find(oldOutput);
+ if (it != newOps.end()) {
+ auto world = ctx.NewCallable(left.Pos(), TCoLeft::CallableName(), { it->second });
+ toSync.push_back(world);
+ }
+ }
+ if (!toSync.empty()) {
+ auto newLeft = ctx.NewCallable(left.Pos(), TCoSync::CallableName(), std::move(toSync));
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-AddChunkCombining-Worlds";
+ replaces[node] = newLeft;
+ }
+ }
+ }
+ }
+ }
+
+ if (!replaces.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-AddChunkCombining";
+ return RemapExpr(input, output, replaces, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus OptimizeFieldSubsetForMultiUsage(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, TExprContext& ctx) {
+ TVector<std::pair<const TOpDeps::value_type*, THashSet<TString>>> matchedOps;
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ for (auto& x: opDeps) {
+ auto writer = x.first;
+
+ if (!ProcessedFieldSubsetForMultiUsage.insert(writer->UniqueId()).second) {
+ continue;
+ }
+
+ if (writer->StartsExecution() || (writer->HasResult() && writer->GetResult().Type() == TExprNode::World)) {
+ continue;
+ }
+ if (!TYtMap::Match(writer) && !TYtMerge::Match(writer)) {
+ continue;
+ }
+ if (writer->GetTypeAnn()->Cast<TTupleExprType>()->GetItems()[1]->Cast<TListExprType>()->GetItemType()->GetKind() == ETypeAnnotationKind::Variant) {
+ // Operation with multi-output
+ continue;
+ }
+ if (NYql::HasSetting(*writer->Child(TYtTransientOpBase::idx_Settings), EYtSettingType::SortLimitBy)) {
+ continue;
+ }
+
+ auto outTable = TYtTransientOpBase(writer).Output().Item(0);
+ const TYqlRowSpecInfo rowSpec(outTable.RowSpec());
+ if (rowSpec.HasAuxColumns()) {
+ continue;
+ }
+
+ auto type = outTable.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+
+ bool good = true;
+ THashSet<TString> usedColumns;
+ for (auto& item: x.second) {
+ if (auto rawSection = std::get<1>(item)) {
+ if (HasNonEmptyKeyFilter(TYtSection(rawSection))) {
+ // wait until key filter values are calculated and pushed to Path/Ranges
+ good = false;
+ break;
+ }
+ }
+ auto rawPath = std::get<3>(item);
+ if (!rawPath) {
+ if (TYtLength::Match(std::get<0>(item))) {
+ continue;
+ }
+ good = false;
+ break;
+ }
+ auto path = TYtPath(rawPath);
+
+ auto columns = TYtColumnsInfo(path.Columns());
+ if (!columns.HasColumns()) {
+ good = false;
+ break;
+ }
+
+ if (type->GetSize() <= columns.GetColumns()->size()) {
+ good = false;
+ break;
+ }
+ std::transform(columns.GetColumns()->cbegin(), columns.GetColumns()->cend(),
+ std::inserter(usedColumns, usedColumns.end()),
+ [] (const TYtColumnsInfo::TColumn& c) { return c.Name; }
+ );
+
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ // add columns which are implicitly used by path.Ranges(), but not included in path.Columns();
+ const auto ranges = TYtRangesInfo(path.Ranges());
+ const size_t usedKeyPrefix = ranges.GetUsedKeyPrefixLength();
+ YQL_ENSURE(usedKeyPrefix <= rowSpec.SortedBy.size());
+ for (size_t i = 0; i < usedKeyPrefix; ++i) {
+ usedColumns.insert(rowSpec.SortedBy[i]);
+ }
+ }
+
+ if (type->GetSize() <= usedColumns.size()) {
+ good = false;
+ break;
+ }
+ }
+
+ if (good && usedColumns.size() < type->GetSize()) {
+ matchedOps.emplace_back(&x, std::move(usedColumns));
+ }
+ }
+
+ if (matchedOps.empty()) {
+ return TStatus::Ok;
+ }
+
+ TNodeOnNodeOwnedMap replaces;
+ TNodeOnNodeOwnedMap newOps;
+ for (auto& item: matchedOps) {
+ auto writer = item.first->first;
+ auto& columns = item.second;
+ auto outTable = TYtTransientOpBase(writer).Output().Item(0);
+ auto type = outTable.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+
+ TVector<const TItemExprType*> structItems;
+ for (auto& column: columns) {
+ auto pos = type->FindItem(column);
+ YQL_ENSURE(pos);
+ structItems.push_back(type->GetItems()[*pos]);
+ }
+ auto outStructType = ctx.MakeType<TStructExprType>(structItems);
+ auto distinct = outTable.Ref().GetConstraint<TDistinctConstraintNode>();
+ if (distinct) {
+ distinct = distinct->FilterFields(ctx, [&columns](const TConstraintNode::TPathType& path) { return !path.empty() && columns.contains(path.front()); });
+ }
+
+ TExprNode::TPtr newOp;
+ if (auto maybeMap = TMaybeNode<TYtMap>(writer)) {
+ TYtMap map = maybeMap.Cast();
+
+ auto mapper = ctx.Builder(map.Mapper().Pos())
+ .Lambda()
+ .Param("stream")
+ .Callable(NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered) ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Apply(0, map.Mapper().Ref())
+ .With(0, "stream")
+ .Seal()
+ .Lambda(1)
+ .Param("item")
+ .Callable(TCoJust::CallableName())
+ .Callable(0, TCoAsStruct::CallableName())
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ size_t index = 0;
+ for (auto& column: columns) {
+ parent
+ .List(index++)
+ .Atom(0, column)
+ .Callable(1, TCoMember::CallableName())
+ .Arg(0, "item")
+ .Atom(1, column)
+ .Seal()
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TYtOutTableInfo mapOut(outStructType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+
+ if (ctx.IsConstraintEnabled<TSortedConstraintNode>()) {
+ if (auto sorted = outTable.Ref().GetConstraint<TSortedConstraintNode>()) {
+ auto prefixLength = sorted->GetContent().size();
+ for (size_t i = 0; i < prefixLength; ++i) {
+ bool found = false;
+ for (const auto& path : sorted->GetContent()[i].first)
+ if (found = path.size() == 1U && columns.contains(path.front()))
+ break;
+
+ if (!found)
+ prefixLength = i;
+ }
+
+ if (sorted = sorted->CutPrefix(prefixLength, ctx)) {
+ TKeySelectorBuilder builder(map.Mapper().Pos(), ctx, useNativeDescSort, outStructType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*mapOut.RowSpec);
+
+ if (builder.NeedMap()) {
+ mapper = ctx.Builder(map.Mapper().Pos())
+ .Lambda()
+ .Param("stream")
+ .Apply(builder.MakeRemapLambda(true))
+ .With(0)
+ .Apply(mapper)
+ .With(0, "stream")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+ }
+ } else {
+ mapOut.RowSpec->CopySortness(TYqlRowSpecInfo(outTable.RowSpec()));
+ }
+ mapOut.SetUnique(distinct, map.Mapper().Pos(), ctx);
+
+ newOp = Build<TYtMap>(ctx, map.Pos())
+ .InitFrom(map)
+ .Output()
+ .Add(mapOut.ToExprNode(ctx, map.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Mapper(std::move(mapper))
+ .Done().Ptr();
+ }
+ else {
+ auto merge = TYtMerge(writer);
+ auto prevRowSpec = TYqlRowSpecInfo(merge.Output().Item(0).RowSpec());
+ TYtOutTableInfo mergeOut(outStructType, prevRowSpec.GetNativeYtTypeFlags());
+ mergeOut.RowSpec->CopySortness(prevRowSpec, TYqlRowSpecInfo::ECopySort::WithDesc);
+ mergeOut.SetUnique(distinct, merge.Pos(), ctx);
+ if (auto nativeType = prevRowSpec.GetNativeYtType()) {
+ mergeOut.RowSpec->CopyTypeOrders(*nativeType);
+ }
+
+ TSet<TStringBuf> columnSet;
+ for (auto& column: columns) {
+ columnSet.insert(column);
+ }
+ if (mergeOut.RowSpec->HasAuxColumns()) {
+ for (auto item: mergeOut.RowSpec->GetAuxColumns()) {
+ columnSet.insert(item.first);
+ }
+ }
+
+ newOp = Build<TYtMerge>(ctx, merge.Pos())
+ .InitFrom(merge)
+ .Input()
+ .Add(UpdateInputFields(merge.Input().Item(0), std::move(columnSet), ctx, false))
+ .Build()
+ .Output()
+ .Add(mergeOut.ToExprNode(ctx, merge.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Done().Ptr();
+ }
+
+ newOps[writer] = newOp;
+ for (auto& reader: item.first->second) {
+ if (TYtLength::Match(std::get<0>(reader))) {
+ auto rawLen = std::get<0>(reader);
+ auto len = TYtLength(rawLen);
+ replaces[rawLen] = Build<TYtLength>(ctx, len.Pos())
+ .InitFrom(len)
+ .Input(ctx.ChangeChild(len.Input().Ref(), TYtOutput::idx_Operation, TExprNode::TPtr(newOp)))
+ .Done().Ptr();
+ } else {
+ auto rawPath = std::get<3>(reader);
+ auto path = TYtPath(rawPath);
+ replaces[rawPath] = Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table(ctx.ChangeChild(path.Table().Ref(), TYtOutput::idx_Operation, TExprNode::TPtr(newOp)))
+ .Done().Ptr();
+ }
+ }
+ }
+ if (!lefts.empty() && !newOps.empty()) {
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ auto newIt = newOps.find(left.Input().Raw());
+ if (newIt != newOps.end()) {
+ replaces[node] = ctx.ChangeChild(*node, TCoLeft::idx_Input, TExprNode::TPtr(newIt->second));
+ }
+ }
+ }
+
+ if (!replaces.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-FieldSubsetForMultiUsage";
+ return RemapExpr(input, output, replaces, ctx, TOptimizeExprSettings{State_->Types});
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus OptimizeUnorderedOuts(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, TExprContext& ctx) {
+ TVector<const TOpDeps::value_type*> matchedOps;
+ for (auto& x: opDeps) {
+ auto writer = x.first;
+ if (!TYtEquiJoin::Match(writer) && !writer->StartsExecution() && (!writer->HasResult() || writer->GetResult().Type() != TExprNode::World)) {
+ matchedOps.push_back(&x);
+ }
+ }
+
+ if (matchedOps.empty()) {
+ return TStatus::Ok;
+ }
+
+ Sort(matchedOps, [](const TOpDeps::value_type* m1, const TOpDeps::value_type* m2) { return m1->first->UniqueId() < m2->first->UniqueId(); });
+
+ TNodeOnNodeOwnedMap replaces;
+ TNodeOnNodeOwnedMap newOps;
+ for (auto x: matchedOps) {
+ auto writer = x->first;
+ TDynBitMap orderedOuts;
+ TDynBitMap unorderedOuts;
+ for (auto& item: x->second) {
+ auto out = TYtOutput(std::get<2>(item));
+ if (IsUnorderedOutput(out)) {
+ unorderedOuts.Set(FromString<size_t>(out.OutIndex().Value()));
+ } else {
+ orderedOuts.Set(FromString<size_t>(out.OutIndex().Value()));
+ }
+ }
+ if (!unorderedOuts.Empty()) {
+ if (orderedOuts.Empty() || !writer->IsCallable(OPS_WITH_SORTED_OUTPUT)) {
+ TExprNode::TPtr newOp = MakeUnorderedOp(*writer, unorderedOuts, ctx);
+ if (newOp) {
+ newOps[writer] = newOp;
+ }
+ for (auto& item: x->second) {
+ auto out = std::get<2>(item);
+ replaces[out] = Build<TYtOutput>(ctx, out->Pos())
+ .Operation(newOp ? newOp : out->ChildPtr(TYtOutput::idx_Operation))
+ .OutIndex(out->ChildPtr(TYtOutput::idx_OutIndex))
+ .Done().Ptr();
+ }
+ }
+ }
+ }
+ if (!lefts.empty() && !newOps.empty()) {
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ auto newIt = newOps.find(left.Input().Raw());
+ if (newIt != newOps.end()) {
+ replaces[node] = ctx.ChangeChild(*node, TCoLeft::idx_Input, TExprNode::TPtr(newIt->second));
+ }
+ }
+ }
+
+ if (!replaces.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-UnorderedOuts";
+ return RemapExpr(input, output, replaces, ctx, TOptimizeExprSettings{State_->Types});
+ }
+ return TStatus::Ok;
+ }
+
+ TExprNode::TPtr MakeUnorderedOp(const TExprNode& node, const TDynBitMap& unorderedOuts, TExprContext& ctx) const {
+ if (!node.IsCallable(OPS_WITH_SORTED_OUTPUT)) {
+ return {};
+ }
+ auto op = TYtOutputOpBase(&node);
+
+ bool hasOtherSortedOuts = false;
+ bool changedOutSort = false;
+ TVector<TYtOutTable> outTables;
+ TExprNode::TListType filterColumns(op.Output().Size());
+ for (size_t i = 0; i < op.Output().Size(); ++i) {
+ auto out = op.Output().Item(i);
+ if (unorderedOuts.Test(i)) {
+ auto rowSpec = TYtTableBaseInfo::GetRowSpec(out);
+ YQL_ENSURE(rowSpec);
+ if (rowSpec->IsSorted()) {
+ if (rowSpec->HasAuxColumns()) {
+ TVector<TString> columns;
+ for (auto item: rowSpec->GetType()->GetItems()) {
+ columns.emplace_back(item->GetName());
+ }
+ filterColumns[i] = ToAtomList(columns, node.Pos(), ctx);
+ }
+ rowSpec->ClearSortness();
+ outTables.push_back(TYtOutTable(ctx.ChangeChild(out.Ref(), TYtOutTable::idx_RowSpec, rowSpec->ToExprNode(ctx, out.Pos()).Ptr())));
+ changedOutSort = true;
+ } else {
+ outTables.push_back(out);
+ }
+ } else {
+ if (TYtTableBaseInfo::GetRowSpec(out)->IsSorted()) {
+ hasOtherSortedOuts = true;
+ }
+ outTables.push_back(out);
+ }
+ }
+
+ bool isFill = false;
+ int lambdaIdx = -1;
+ TExprNode::TPtr lambda;
+ if (TYtMap::Match(&node)) {
+ lambdaIdx = TYtMap::idx_Mapper;
+ } else if (TYtReduce::Match(&node)) {
+ lambdaIdx = TYtReduce::idx_Reducer;
+ } else if (TYtFill::Match(&node)) {
+ lambdaIdx = TYtFill::idx_Content;
+ isFill = true;
+ }
+ if (-1 != lambdaIdx && !hasOtherSortedOuts) {
+ if (isFill) {
+ if (node.ChildPtr(lambdaIdx)->GetConstraint<TSortedConstraintNode>()) {
+ lambda = Build<TCoLambda>(ctx, node.ChildPtr(lambdaIdx)->Pos())
+ .Args({})
+ .Body<TCoUnordered>()
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(node.ChildPtr(lambdaIdx)))
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ } else {
+ TProcessedNodesSet processedNodes;
+ TNodeOnNodeOwnedMap remaps;
+ VisitExpr(node.ChildPtr(lambdaIdx), [&processedNodes, &remaps, &ctx](const TExprNode::TPtr& node) {
+ if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ auto name = node->Content();
+ if (node->IsCallable() && node->ChildrenSize() > 0 && name.SkipPrefix("Ordered")) {
+ const auto inputKind = node->Child(0)->GetTypeAnn()->GetKind();
+ if (inputKind == ETypeAnnotationKind::Stream || inputKind == ETypeAnnotationKind::Flow) {
+ remaps[node.Get()] = ctx.RenameNode(*node, name);
+ }
+ }
+ return true;
+ });
+ if (!remaps.empty()) {
+ TOptimizeExprSettings settings{State_->Types};
+ settings.ProcessedNodes = &processedNodes;
+ auto status = RemapExpr(node.ChildPtr(lambdaIdx), lambda, remaps, ctx, settings);
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+ }
+ }
+ }
+
+ TExprNode::TPtr res;
+ if (changedOutSort) {
+ res = ctx.ChangeChild(node, TYtOutputOpBase::idx_Output,
+ Build<TYtOutSection>(ctx, op.Pos()).Add(outTables).Done().Ptr());
+
+ if (TYtSort::Match(&node)) {
+ res = ctx.RenameNode(*res, TYtMerge::CallableName());
+ }
+
+ if (lambdaIdx != -1 && AnyOf(filterColumns, [](const TExprNode::TPtr& p) { return !!p; })) {
+ if (!lambda) {
+ lambda = node.ChildPtr(lambdaIdx);
+ }
+ if (op.Output().Size() == 1) {
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoExtractMembers>()
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .With(0, "stream")
+ .Build()
+ .Members(filterColumns[0])
+ .Build()
+ .Done().Ptr();
+ } else {
+ auto varType = ExpandType(lambda->Pos(), *GetSeqItemType(node.Child(lambdaIdx)->GetTypeAnn()), ctx);
+ TVector<TExprBase> visitArgs;
+ for (size_t i = 0; i < op.Output().Size(); ++i) {
+ visitArgs.push_back(Build<TCoAtom>(ctx, lambda->Pos()).Value(ToString(i)).Done());
+ if (filterColumns[i]) {
+ visitArgs.push_back(Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"row"})
+ .Body<TCoVariant>()
+ .Item<TCoUnwrap>()
+ .Optional<TCoExtractMembers>()
+ .Input<TCoJust>()
+ .Input("row")
+ .Build()
+ .Members(filterColumns[i])
+ .Build()
+ .Build()
+ .Index()
+ .Value(ToString(i))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Done());
+ } else {
+ visitArgs.push_back(Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"row"})
+ .Body<TCoVariant>()
+ .Item("row")
+ .Index()
+ .Value(ToString(i))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Done());
+ }
+ }
+
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(hasOtherSortedOuts ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"var"})
+ .Body<TCoJust>()
+ .Input<TCoVisit>()
+ .Input("var")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ }
+
+ if (lambda) {
+ res = ctx.ChangeChild(res ? *res : node, lambdaIdx, std::move(lambda));
+ }
+
+ if (op.Maybe<TYtTransientOpBase>()) {
+ auto trOp = TYtTransientOpBase(&node);
+ if (!hasOtherSortedOuts && NYql::HasSetting(trOp.Settings().Ref(), EYtSettingType::Ordered)) {
+ res = ctx.ChangeChild(res ? *res : node, TYtTransientOpBase::idx_Settings,
+ NYql::RemoveSetting(trOp.Settings().Ref(), EYtSettingType::Ordered, ctx));
+ }
+
+ if (TYtMap::Match(&node)) {
+ Fill(filterColumns.begin(), filterColumns.end(), TExprNode::TPtr());
+ filterColumns.resize(trOp.Input().Size());
+ }
+
+ if (!TYtReduce::Match(&node)) {
+ // Push Unordered and columns to operation inputs
+ bool changedInput = false;
+ TVector<TYtSection> updatedSections;
+ for (size_t i = 0; i < trOp.Input().Size(); ++i) {
+ auto section = trOp.Input().Item(i);
+ if (!hasOtherSortedOuts) {
+ section = MakeUnorderedSection(section, ctx);
+ if (section.Raw() != trOp.Input().Item(i).Raw()) {
+ changedInput = true;
+ }
+ }
+ if (filterColumns[i]) {
+ section = UpdateInputFields(section, TExprBase(filterColumns[i]), ctx);
+ changedInput = true;
+ }
+ updatedSections.push_back(section);
+ }
+
+ if (changedInput) {
+ res = ctx.ChangeChild(res ? *res : node, TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, trOp.Pos()).Add(updatedSections).Done().Ptr());
+ }
+ }
+ }
+
+ return res;
+ }
+
+ TStatus SplitLargeInputs(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx, bool splitMap) {
+ auto maxTables = State_->Configuration->MaxInputTables.Get();
+ auto maxSortedTables = State_->Configuration->MaxInputTablesForSortedMerge.Get();
+
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &ProcessedSplitLargeInputs;
+
+ return OptimizeExpr(input, output, [maxTables, maxSortedTables, splitMap, this](const TExprNode::TPtr& node, TExprContext& ctx) {
+ if (TYtTransientOpBase::Match(node.Get()) && !node->StartsExecution() && !node->HasResult()) {
+ auto op = TYtTransientOpBase(node);
+ const bool sortedMerge = TYtMerge::Match(node.Get()) && TYqlRowSpecInfo(TYtMerge(node).Output().Item(0).RowSpec()).IsSorted();
+ const bool keepSort = sortedMerge || TYtReduce::Match(node.Get()) || TYtEquiJoin::Match(node.Get());
+ auto limit = maxTables;
+ if (maxSortedTables.Defined() && sortedMerge) {
+ limit = maxSortedTables;
+ }
+
+ if (limit) {
+ if (splitMap && TYtMap::Match(node.Get())
+ && op.Input().Size() == 1 && op.Output().Size() == 1
+ && !NYql::HasAnySetting(op.Settings().Ref(), EYtSettingType::SortLimitBy | EYtSettingType::Sharded | EYtSettingType::JobCount | EYtSettingType::Ordered | EYtSettingType::KeepSorted)
+ && op.Input().Item(0).Paths().Size() > *limit
+ && !NYql::HasAnySetting(op.Input().Item(0).Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip))
+ {
+ auto section = op.Input().Item(0);
+ TVector<TYtPath> newPaths;
+ TVector<TYtPath> paths;
+ TVector<TYtPath> prevPaths(section.Paths().begin(), section.Paths().end());
+ while (!prevPaths.empty()) {
+ size_t count = Min<size_t>(*limit, prevPaths.size());
+ YQL_ENSURE(count > 0);
+
+ newPaths.push_back(
+ Build<TYtPath>(ctx, op.Pos())
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .InitFrom(op.Cast<TYtMap>())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(TVector<TYtPath>(prevPaths.begin(), prevPaths.begin() + count))
+ .Build()
+ .Settings(section.Settings())
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done()
+ );
+ prevPaths.erase(prevPaths.begin(), prevPaths.begin() + count);
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-SplitLargeMapInputs";
+ return Build<TYtMerge>(ctx, op.Pos())
+ .World<TCoWorld>().Build()
+ .DataSink(op.DataSink())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(newPaths)
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Output()
+ .Add(op.Output().Item(0))
+ .Build()
+ .Settings(NYql::KeepOnlySettings(op.Settings().Ref(), EYtSettingType::Limit, ctx))
+ .Done().Ptr();
+ }
+
+ TVector<TYtSection> updatedSections;
+ bool hasUpdates = false;
+ for (auto section: op.Input()) {
+ const EYtSettingType kfType = NYql::HasSetting(section.Settings().Ref(), EYtSettingType::KeyFilter2) ?
+ EYtSettingType::KeyFilter2 : EYtSettingType::KeyFilter;
+ const auto keyFiltersValues = NYql::GetAllSettingValues(section.Settings().Ref(), kfType);
+ const bool hasKeyFilters = AnyOf(keyFiltersValues,
+ [](const TExprNode::TPtr& keyFilter) { return keyFilter->ChildrenSize() > 0; });
+ const bool hasTableKeyFilters = AnyOf(keyFiltersValues,
+ [kfType](const TExprNode::TPtr& keyFilter) {
+ return keyFilter->ChildrenSize() >= GetMinChildrenForIndexedKeyFilter(kfType);
+ });
+
+ if (hasTableKeyFilters) {
+ // Cannot optimize it
+ updatedSections.push_back(section);
+ } else if (section.Paths().Size() > *limit) {
+ auto scheme = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ TVector<TYtPath> paths;
+ TVector<TYtPath> prevPaths(section.Paths().begin(), section.Paths().end());
+ const size_t min = NYql::HasSetting(section.Settings().Ref(), EYtSettingType::SysColumns) ? 0 : 1;
+ while (prevPaths.size() > min) {
+ size_t count = Min<size_t>(*limit, prevPaths.size());
+ YQL_ENSURE(count > 0);
+
+ auto path = CopyOrTrivialMap(section.Pos(),
+ op.World(), op.DataSink(),
+ *scheme,
+ Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(TVector<TYtPath>(prevPaths.begin(), prevPaths.begin() + count))
+ .Build()
+ .Settings(NYql::KeepOnlySettings(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::SysColumns, ctx))
+ .Done(),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(keepSort).SetRangesResetSort(false));
+
+ paths.push_back(path);
+ prevPaths.erase(prevPaths.begin(), prevPaths.begin() + count);
+ }
+ auto settings = section.Settings().Ptr();
+ if (!prevPaths.empty()) {
+ if (hasKeyFilters) {
+ // Modify keyFilters to point to remaining tables only
+ const size_t from = paths.size();
+ const size_t to = from + prevPaths.size();
+ auto keyFiltersValues = NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter);
+ settings = NYql::RemoveSetting(*settings, EYtSettingType::KeyFilter, ctx);
+ for (auto val: keyFiltersValues) {
+ for (size_t i = from; i < to; ++i) {
+ auto children = val->ChildrenList();
+ children.push_back(ctx.NewAtom(section.Pos(), ToString(i), TNodeFlags::Default));
+ settings = NYql::AddSetting(*settings, EYtSettingType::KeyFilter,
+ ctx.NewList(section.Pos(), std::move(children)), ctx);
+ }
+ }
+ keyFiltersValues = NYql::GetAllSettingValues(section.Settings().Ref(), EYtSettingType::KeyFilter2);
+ settings = NYql::RemoveSetting(*settings, EYtSettingType::KeyFilter2, ctx);
+ for (auto val: keyFiltersValues) {
+ TExprNode::TListType indicies;
+ for (size_t i = from; i < to; ++i) {
+ indicies.push_back(ctx.NewAtom(section.Pos(), ToString(i), TNodeFlags::Default));
+ }
+ auto children = val->ChildrenList();
+ children.push_back(ctx.NewList(section.Pos(), std::move(indicies)));
+ settings = NYql::AddSetting(*settings, EYtSettingType::KeyFilter2,
+ ctx.NewList(section.Pos(), std::move(children)), ctx);
+ }
+ }
+ paths.insert(paths.end(), prevPaths.begin(), prevPaths.end());
+ } else {
+ // All keyFilters are pushed to children operations. Can remove them from the current
+ if (hasKeyFilters) {
+ settings = NYql::RemoveSettings(*settings, EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2, ctx);
+ }
+ }
+ updatedSections.push_back(Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings(settings)
+ .Done());
+ hasUpdates = true;
+ } else {
+ updatedSections.push_back(section);
+ }
+ }
+ if (hasUpdates) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-SplitLargeInputs";
+ return ctx.ChangeChild(*node, TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos()).Add(updatedSections).Done().Ptr());
+ }
+ }
+ }
+ return node;
+ }, ctx, settings);
+ }
+
+ static TExprNode::TPtr RebuildLambdaWithLessOuts(TExprNode::TPtr lambda, bool withArg, bool ordered, const TDynBitMap& usedOuts,
+ const TYtOutSection& outs, TVector<TYtOutTable>& resTables, TExprContext& ctx)
+ {
+ if (usedOuts.Count() == 1) {
+ auto ndx = usedOuts.FirstNonZeroBit();
+ resTables.push_back(outs.Item(ndx));
+ if (withArg) {
+ auto oldLambda = TCoLambda(lambda);
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .template Input<TExprApplier>()
+ .Apply(oldLambda)
+ .With(oldLambda.Args().Arg(0), "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .template Body<TCoGuess>()
+ .Variant("item")
+ .Index()
+ .Value(ToString(ndx))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ else {
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .template Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .template Body<TCoGuess>()
+ .Variant("item")
+ .Index()
+ .Value(ToString(ndx))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ else {
+ TVector<TExprBase> tupleTypes;
+ for (size_t i = 0; i < outs.Size(); ++i) {
+ if (usedOuts.Test(i)) {
+ auto out = outs.Item(i);
+ tupleTypes.push_back(TExprBase(ExpandType(out.Pos(), *out.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType(), ctx)));
+ }
+ }
+ TExprBase varType = Build<TCoVariantType>(ctx, lambda->Pos())
+ .UnderlyingType<TCoTupleType>()
+ .Add(tupleTypes)
+ .Build()
+ .Done();
+
+ TVector<TExprBase> visitArgs;
+ size_t nextVarIndex = 0;
+ for (size_t i = 0; i < outs.Size(); ++i) {
+ if (usedOuts.Test(i)) {
+ visitArgs.push_back(Build<TCoAtom>(ctx, lambda->Pos()).Value(ToString(i)).Done());
+ visitArgs.push_back(Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"item"})
+ .template Body<TCoJust>()
+ .template Input<TCoVariant>()
+ .Item("item")
+ .Index()
+ .Value(ToString(nextVarIndex++))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Build()
+ .Done()
+ );
+ resTables.push_back(outs.Item(i));
+ }
+ }
+ visitArgs.push_back(Build<TCoNothing>(ctx, lambda->Pos())
+ .template OptionalType<TCoOptionalType>()
+ .ItemType(varType)
+ .Build()
+ .Done());
+
+ if (withArg) {
+ auto oldLambda = TCoLambda(lambda);
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .template Input<TExprApplier>()
+ .Apply(oldLambda)
+ .With(oldLambda.Args().Arg(0), "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .template Body<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ else {
+ lambda = Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .template Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .template Body<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ return lambda;
+ }
+
+ TStatus BypassMergeBeforeLength(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, TExprContext& ctx) {
+ TNodeOnNodeOwnedMap replaces;
+ for (auto& x: opDeps) {
+ if (TYtMerge::Match(x.first) && x.second.size() > 0 && AllOf(x.second, [](const auto& item) { return TYtLength::Match(std::get<0>(item)); } )) {
+ auto merge = TYtMerge(x.first);
+ if (merge.Ref().HasResult()) {
+ continue;
+ }
+
+ if (NYql::HasSetting(merge.Settings().Ref(), EYtSettingType::Limit)) {
+ continue;
+ }
+
+ auto section = merge.Input().Item(0);
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample)) {
+ continue;
+ }
+ if (NYql::HasNonEmptyKeyFilter(section)) {
+ continue;
+ }
+
+ if (AnyOf(section.Paths(), [](const TYtPath& path) { return !path.Ranges().Maybe<TCoVoid>() || TYtTableBaseInfo::GetMeta(path.Table())->IsDynamic; })) {
+ continue;
+ }
+ // Dependency on more than 1 operation
+ if (1 < Accumulate(section.Paths(), 0ull, [](ui64 val, const TYtPath& path) { return val + path.Table().Maybe<TYtOutput>().IsValid(); })) {
+ continue;
+ }
+
+ TSyncMap syncList;
+ for (auto path: section.Paths()) {
+ if (auto out = path.Table().Maybe<TYtOutput>()) {
+ syncList.emplace(GetOutputOp(out.Cast()).Ptr(), syncList.size());
+ }
+ }
+ auto newWorld = ApplySyncListToWorld(merge.World().Ptr(), syncList, ctx);
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ if (left.Input().Raw() == merge.Raw()) {
+ replaces[node] = newWorld;
+ }
+ }
+
+ for (auto& item : x.second) {
+ auto len = TYtLength(std::get<0>(item));
+ auto out = len.Input().Cast<TYtOutput>();
+
+ TExprNode::TPtr newLen;
+ if (section.Paths().Size() == 1) {
+ if (auto maybeOp = section.Paths().Item(0).Table().Maybe<TYtOutput>()) {
+ if (IsUnorderedOutput(out)) {
+ newLen = Build<TYtLength>(ctx, len.Pos())
+ .InitFrom(len)
+ .Input<TYtOutput>()
+ .InitFrom(maybeOp.Cast())
+ .Mode(out.Mode())
+ .Build()
+ .Done().Ptr();
+ } else {
+ newLen = Build<TYtLength>(ctx, len.Pos())
+ .InitFrom(len)
+ .Input(maybeOp.Cast())
+ .Done().Ptr();
+ }
+ }
+ }
+ if (!newLen) {
+ newLen = Build<TYtLength>(ctx, len.Pos())
+ .InitFrom(len)
+ .Input<TYtReadTable>()
+ .World(ctx.NewWorld(len.Pos()))
+ .DataSource(GetDataSource(len.Input(), ctx))
+ .Input()
+ .Add(IsUnorderedOutput(out) ? MakeUnorderedSection(section, ctx) : section)
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ replaces[len.Raw()] = newLen;
+ }
+ }
+ }
+
+ if (!replaces.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-BypassMergeBeforeLength";
+ return RemapExpr(input, output, replaces, ctx, TOptimizeExprSettings(State_->Types));
+ }
+
+ return TStatus::Ok;
+ }
+
+ TStatus OptimizeUnusedOuts(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, TExprContext& ctx) {
+ TNodeOnNodeOwnedMap replaces;
+ TNodeOnNodeOwnedMap newOps;
+ for (auto& x: opDeps) {
+ auto writer = x.first;
+ if (const size_t outCount = TYtOutputOpBase(writer).Output().Size(); outCount > 1 && writer->GetState() != TExprNode::EState::ExecutionComplete
+ && writer->GetState() != TExprNode::EState::ExecutionInProgress
+ && (!writer->HasResult() || writer->GetResult().Type() != TExprNode::World)
+ && ProcessedUnusedOuts.find(writer->UniqueId()) == ProcessedUnusedOuts.end())
+ {
+ TDynBitMap usedOuts;
+ for (auto& item: x.second) {
+ usedOuts.Set(FromString<size_t>(std::get<2>(item)->Child(TYtOutput::idx_OutIndex)->Content()));
+ }
+ if (!usedOuts.Empty() && usedOuts.Count() < outCount) {
+ TExprNode::TPtr newOp = SuppressUnusedOuts(*writer, usedOuts, ctx);
+ newOps[writer] = newOp;
+ TVector<size_t> remappedIndicies(outCount, Max<size_t>());
+ size_t newIndex = 0;
+ for (size_t i = 0; i < outCount; ++i) {
+ if (usedOuts.Test(i)) {
+ remappedIndicies[i] = newIndex++;
+ }
+ }
+ for (auto& item: x.second) {
+ auto oldOutput = TYtOutput(std::get<2>(item));
+ auto oldNdx = FromString<size_t>(oldOutput.OutIndex().Value());
+ YQL_ENSURE(oldNdx < remappedIndicies.size());
+ auto newNdx = remappedIndicies[oldNdx];
+ YQL_ENSURE(newNdx != Max<size_t>());
+ replaces[oldOutput.Raw()] = Build<TYtOutput>(ctx, oldOutput.Pos())
+ .Operation(newOp)
+ .OutIndex()
+ .Value(ToString(newNdx))
+ .Build()
+ .Mode(oldOutput.Mode())
+ .Done().Ptr();
+ }
+ }
+ }
+ ProcessedUnusedOuts.insert(writer->UniqueId());
+ }
+ if (!lefts.empty() && !newOps.empty()) {
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ auto newIt = newOps.find(left.Input().Raw());
+ if (newIt != newOps.end()) {
+ replaces[node] = ctx.ChangeChild(*node, TCoLeft::idx_Input, TExprNode::TPtr(newIt->second));
+ }
+ }
+ }
+
+ if (!replaces.empty()) {
+ return RemapExpr(input, output, replaces, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ return TStatus::Ok;
+ }
+
+ TStatus AlignPublishTypes(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, TExprContext& ctx) {
+ TNodeOnNodeOwnedMap remap;
+ TMap<size_t, size_t> outUsage;
+ TNodeMap<TNodeOnNodeOwnedMap> updatePublish;
+ for (auto& x : opDeps) {
+ outUsage.clear();
+ bool hasPublish = false;
+ for (auto& item : x.second) {
+ auto reader = std::get<0>(item);
+ if (TYtPublish::Match(reader) || TYtCopy::Match(reader) || TYtMerge::Match(reader) || TYtSort::Match(reader)) {
+ const auto opIndex = FromString<size_t>(std::get<2>(item)->Child(TYtOutput::idx_OutIndex)->Content());
+ ++outUsage[opIndex];
+ hasPublish = hasPublish || TYtPublish::Match(reader);
+ }
+ }
+ if (!hasPublish) {
+ continue;
+ }
+
+ const TYtOutputOpBase operation(x.first);
+ const bool canUpdateOp = !operation.Ref().StartsExecution() && !operation.Ref().HasResult()
+ && !operation.Maybe<TYtCopy>() && !operation.Maybe<TYtMerge>() && !operation.Maybe<TYtSort>();
+
+ auto origOutput = operation.Output().Ptr();
+ auto newOutput = origOutput;
+ for (auto& item : x.second) {
+ auto reader = std::get<0>(item);
+ if (auto maybePublish = TMaybeNode<TYtPublish>(reader)) {
+ auto publish = maybePublish.Cast();
+ const auto out = TYtOutput(std::get<2>(item));
+ const size_t opIndex = FromString<size_t>(out.OutIndex().Value());
+ const bool unordered = IsUnorderedOutput(out);
+ TYtTableInfo dstInfo = publish.Publish();
+ const auto& desc = State_->TablesData->GetTable(dstInfo.Cluster, dstInfo.Name, dstInfo.CommitEpoch);
+ if (!desc.RowSpec) {
+ continue;
+ }
+ auto table = operation.Output().Item(opIndex);
+ if (auto outRowSpec = TYtTableBaseInfo::GetRowSpec(table)) {
+ auto mode = NYql::GetSetting(publish.Settings().Ref(), EYtSettingType::Mode);
+ const bool append = mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Append;
+
+ const bool diffNativeType = desc.RowSpec->GetNativeYtTypeFlags() != outRowSpec->GetNativeYtTypeFlags()
+ || desc.RowSpec->GetNativeYtType() != outRowSpec->GetNativeYtType();
+ const bool diffColumnOrder = State_->Types->OrderedColumns && !append && desc.RowSpec->GetTypeNode() != outRowSpec->GetTypeNode();
+
+ if (diffNativeType || diffColumnOrder) {
+ outRowSpec->CopyType(*desc.RowSpec);
+ TExprNode::TPtr newTable = ctx.ChangeChild(table.Ref(), TYtOutTable::idx_RowSpec,
+ outRowSpec->ToExprNode(ctx, table.RowSpec().Pos()).Ptr());
+
+ if (canUpdateOp && outUsage[opIndex] <= 1) {
+ YQL_CLOG(INFO, ProviderYt) << "AlignPublishTypes: change " << opIndex << " output of " << operation.Ref().Content();
+ newOutput = ctx.ChangeChild(*newOutput, opIndex, std::move(newTable));
+ } else if (diffNativeType) {
+ YQL_CLOG(INFO, ProviderYt) << "AlignPublishTypes: add remap op for " << opIndex << " output of " << operation.Ref().Content();
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, out.Pos());
+ if (!unordered && outRowSpec->IsSorted()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+ if (State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ updatePublish[reader][out.Raw()] = Build<TYtOutput>(ctx, out.Pos())
+ .Operation<TYtMap>()
+ .World<TCoWorld>().Build()
+ .DataSink(operation.DataSink())
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .InitFrom(out)
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings().Build()
+ .Build()
+ .Build()
+ .Output()
+ .Add(std::move(newTable))
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper()
+ .Args({"stream"})
+ .Body("stream")
+ .Build()
+ .Build()
+ .OutIndex().Build(0U)
+ .Mode(out.Mode())
+ .Done().Ptr();
+ }
+ }
+ }
+ }
+ }
+ if (newOutput != origOutput) {
+ remap[operation.Raw()] = ctx.ChangeChild(operation.Ref(), TYtOutputOpBase::idx_Output, std::move(newOutput));
+ }
+ }
+
+ if (!updatePublish.empty()) {
+ for (auto& item: updatePublish) {
+ auto publish = TYtPublish(item.first);
+ const TNodeOnNodeOwnedMap& updateOuts = item.second;
+ auto origInput = publish.Input().Ptr();
+ auto newInput = origInput;
+ for (size_t i = 0; i < publish.Input().Size(); ++i) {
+ if (auto it = updateOuts.find(publish.Input().Item(i).Raw()); it != updateOuts.end()) {
+ newInput = ctx.ChangeChild(*newInput, i, TExprNode::TPtr(it->second));
+ }
+ }
+ YQL_ENSURE(newInput != origInput);
+ remap[publish.Raw()] = ctx.ChangeChild(publish.Ref(), TYtPublish::idx_Input, std::move(newInput));
+ }
+ }
+
+ if (!remap.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-AlignPublishTypes";
+ return RemapExpr(input, output, remap, ctx, TOptimizeExprSettings(State_->Types));
+ }
+
+ return TStatus::Ok;
+ }
+
+ static TExprNode::TPtr SuppressUnusedOuts(const TExprNode& node, const TDynBitMap& usedOuts, TExprContext& ctx) {
+ auto op = TYtOutputOpBase(&node);
+ size_t lambdaNdx = 0;
+ bool withArg = true;
+ bool ordered = false;
+ if (auto ytMap = op.Maybe<TYtMap>()) {
+ lambdaNdx = TYtMap::idx_Mapper;
+ ordered = NYql::HasSetting(ytMap.Cast().Settings().Ref(), EYtSettingType::Ordered);
+ } else if (op.Maybe<TYtReduce>()) {
+ lambdaNdx = TYtReduce::idx_Reducer;
+ } else if (op.Maybe<TYtMapReduce>()) {
+ lambdaNdx = TYtMapReduce::idx_Reducer;
+ } else if (op.Maybe<TYtFill>()) {
+ lambdaNdx = TYtFill::idx_Content;
+ withArg = false;
+ } else {
+ YQL_ENSURE(false, "Unsupported operation " << node.Content());
+ }
+ TExprNode::TPtr lambda = node.ChildPtr(lambdaNdx);
+ TVector<TYtOutTable> resTables;
+
+ lambda = RebuildLambdaWithLessOuts(lambda, withArg, ordered, usedOuts, op.Output(), resTables, ctx);
+
+ auto res = ctx.ChangeChild(node, TYtOutputOpBase::idx_Output,
+ Build<TYtOutSection>(ctx, op.Pos()).Add(resTables).Done().Ptr());
+
+ res = ctx.ChangeChild(*res, lambdaNdx, std::move(lambda));
+
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-SuppressOuts";
+ return res;
+ }
+
+ static TExprNode::TPtr Limits(const TExprNode::TPtr& node, const TParentsMap& limits, TExprContext& ctx) {
+ auto limitIt = limits.find(node.Get());
+ if (limitIt == limits.end()) {
+ return node;
+ }
+ auto op = TYtTransientOpBase(node);
+ TExprNode::TListType limitNodeChildren;
+ for (auto readerSettings: limitIt->second) {
+ TExprNode::TListType readerNodeChildren;
+ for (auto setting : readerSettings->Children()) {
+ if (setting->ChildrenSize() > 0) {
+ auto kind = FromString<EYtSettingType>(setting->Child(0)->Content());
+ if (EYtSettingType::Take == kind || EYtSettingType::Skip == kind) {
+ readerNodeChildren.push_back(setting);
+ }
+ }
+ }
+
+ auto readerNode = ctx.NewList(op.Pos(), std::move(readerNodeChildren));
+ limitNodeChildren.push_back(readerNode);
+ }
+
+ return ctx.ChangeChild(*node, TYtTransientOpBase::idx_Settings,
+ NYql::AddSetting(op.Settings().Ref(), EYtSettingType::Limit, ctx.NewList(op.Pos(), std::move(limitNodeChildren)), ctx));
+ }
+
+ static TExprNode::TPtr LengthOverPhysicalList(const TExprNode::TPtr& node, const TOpDeps& opDeps, TExprContext& ctx) {
+ auto op = TYtWithUserJobsOpBase(node);
+
+ // TODO: support multi-output
+ if (op.Output().Size() != 1) {
+ return node;
+ }
+ auto outTable = op.Output().Item(0);
+ if (outTable.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>()->GetSize() == 0) {
+ // already empty struct
+ return node;
+ }
+
+ auto readersIt = opDeps.find(op.Raw());
+ if (readersIt == opDeps.end() || readersIt->second.size() != 1) {
+ return node;
+ }
+ auto reader = std::get<0>(readersIt->second.front());
+ if (!TYtLength::Match(reader)) {
+ return node;
+ }
+
+ // only YtLength is used, rewrite
+ auto lambdaIdx = op.Maybe<TYtMap>()
+ ? TYtMap::idx_Mapper
+ : op.Maybe<TYtReduce>()
+ ? TYtReduce::idx_Reducer
+ : TYtMapReduce::idx_Reducer;
+
+ // Rebuild lambda
+ auto lambda = TCoLambda(node->ChildPtr(lambdaIdx));
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TCoMap>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoAsStruct>()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+
+ // Rebuild output table
+ TYtOutTableInfo outTableInfo(outTable);
+ outTableInfo.RowSpec->ClearSortness();
+ outTableInfo.RowSpec->SetType(ctx.MakeType<TStructExprType>(TVector<const TItemExprType*>()));
+
+ auto newOp = ctx.ShallowCopy(*node);
+ if (NYql::HasSetting(op.Settings().Ref(), EYtSettingType::Ordered)) {
+ newOp->ChildRef(TYtWithUserJobsOpBase::idx_Settings) = NYql::RemoveSetting(op.Settings().Ref(), EYtSettingType::Ordered, ctx);
+ }
+
+ newOp->ChildRef(lambdaIdx) = lambda.Ptr();
+ newOp->ChildRef(TYtWithUserJobsOpBase::idx_Output) =
+ Build<TYtOutSection>(ctx, op.Output().Pos())
+ .Add(outTableInfo.ToExprNode(ctx, outTable.Pos()).Cast<TYtOutTable>())
+ .Done().Ptr();
+
+ return newOp;
+ }
+
+ static TExprNode::TPtr TopSortForProducers(const TExprNode::TPtr& node, const TOpDeps& opDeps, TExprContext& ctx) {
+ auto op = TYtWithUserJobsOpBase(node);
+
+ if (op.Output().Size() != 1) {
+ return node;
+ }
+
+ if (NYql::HasAnySetting(op.Settings().Ref(), EYtSettingType::SortLimitBy | EYtSettingType::Limit)) {
+ return node;
+ }
+
+ auto readersIt = opDeps.find(op.Raw());
+ if (readersIt == opDeps.end()) {
+ return node;
+ }
+
+ TExprNode::TListType limits;
+ TVector<std::pair<TString, bool>> sortBy;
+ for (auto item: readersIt->second) {
+ if (!TYtSort::Match(std::get<0>(item))) {
+ return node;
+ }
+ auto sort = TYtSort(std::get<0>(item));
+ if (!NYql::HasSetting(sort.Settings().Ref(), EYtSettingType::Limit)) {
+ return node;
+ }
+
+ auto opSortBy = TYtOutTableInfo(sort.Output().Item(0)).RowSpec->GetForeignSort();
+ if (sortBy.empty()) {
+ sortBy = std::move(opSortBy);
+ }
+ else if (opSortBy != sortBy) {
+ return node;
+ }
+ auto limitValues = NYql::GetSetting(sort.Settings().Ref(), EYtSettingType::Limit)->Child(1);
+ limits.insert(limits.end(), limitValues->Children().begin(), limitValues->Children().end());
+ }
+
+ if (limits.empty()) {
+ return node;
+ }
+
+ auto newSettings = NYql::AddSetting(op.Settings().Ref(), EYtSettingType::Limit,
+ ctx.NewList(node->Pos(), std::move(limits)), ctx);
+ newSettings = NYql::AddSettingAsColumnPairList(*newSettings, EYtSettingType::SortLimitBy, sortBy, ctx);
+ newSettings = NYql::RemoveSetting(*newSettings, EYtSettingType::Ordered, ctx);
+
+ auto newOp = ctx.ShallowCopy(*node);
+ newOp->ChildRef(TYtWithUserJobsOpBase::idx_Settings) = newSettings;
+
+ auto outTable = op.Output().Item(0);
+ TYtOutTableInfo outTableInfo(outTable);
+ if (outTableInfo.RowSpec->IsSorted()) {
+ outTableInfo.RowSpec->ClearSortness();
+ newOp->ChildRef(TYtWithUserJobsOpBase::idx_Output) =
+ Build<TYtOutSection>(ctx, op.Output().Pos())
+ .Add(outTableInfo.ToExprNode(ctx, outTable.Pos()).Cast<TYtOutTable>())
+ .Done().Ptr();
+ }
+
+ return newOp;
+ }
+
+ TStatus OptimizeMultiOuts(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, TExprContext& ctx) {
+ TNodeOnNodeOwnedMap rewrite;
+ TNodeOnNodeOwnedMap newOps;
+ for (auto& x: opDeps) {
+ auto writer = x.first;
+ const TYtOutputOpBase op(writer);
+ if (const size_t outCount = op.Output().Size(); outCount > 1 && !BeingExecuted(*writer)
+ && (!op.Maybe<TYtMapReduce>() || GetMapDirectOutputsCount(op.Maybe<TYtMapReduce>().Cast()) == 0) // TODO: optimize this case
+ && ProcessedMultiOuts.find(writer->UniqueId()) == ProcessedMultiOuts.end())
+ {
+ // Outputs cannot be merged if: 1) there are ranges over output, 2) output is used multiple times in the same section, 3) output is sorted
+ TSet<size_t> exclusiveOuts;
+ TVector<const TTypeAnnotationNode*> outTypes;
+ for (size_t i: xrange(op.Output().Size())) {
+ if (TYqlRowSpecInfo(op.Output().Item(i).RowSpec()).IsSorted()) {
+ exclusiveOuts.insert(i);
+ }
+ outTypes.push_back(op.Output().Item(i).Ref().GetTypeAnn());
+ }
+
+ TMap<size_t, TSet<const TExprNode*>> outUsage; // output num -> set of readers (sections or operations)
+ THashMap<size_t, TSet<std::pair<const TExprNode*, const TExprNode*>>> duplicateCheck; // output num -> set of readers (operation + section)
+ for (auto& reader: x.second) {
+ const auto out = TYtOutput(std::get<2>(reader));
+ const auto outIndex = FromString<size_t>(out.OutIndex().Value());
+ YQL_ENSURE(outIndex < outTypes.size());
+ const auto path = std::get<3>(reader);
+ if (path && !TCoVoid::Match(path->Child(TYtPath::idx_Ranges))) {
+ exclusiveOuts.insert(outIndex);
+ }
+ auto section = std::get<1>(reader); // section
+ auto op = std::get<0>(reader); // operation
+ // Section may be used multiple times in different operations
+ // So, check only unique pair of operation + section
+ if (!duplicateCheck[outIndex].insert(std::make_pair(op, section)).second) {
+ exclusiveOuts.insert(outIndex);
+ }
+ outUsage[outIndex].insert(section ? section : op);
+ }
+
+ // Group by {{set of sections}, type}
+ TMap<std::pair<TSet<const TExprNode*>, const TTypeAnnotationNode*>, TVector<size_t>> groupedOuts;
+ for (auto& item: outUsage) {
+ if (!exclusiveOuts.contains(item.first)) {
+ groupedOuts[std::make_pair(item.second, outTypes[item.first])].push_back(item.first);
+ }
+ }
+ if (AnyOf(groupedOuts, [](const auto& item) { return item.second.size() > 1; })) {
+ size_t nextNewOutIndex = 0;
+ TVector<TYtOutTable> joinedOutTables;
+ TVector<std::pair<size_t, bool>> outRemap; // old index -> new index, drop flag
+ outRemap.resize(outCount);
+ for (auto old_out_num: exclusiveOuts) {
+ outRemap[old_out_num] = std::make_pair(nextNewOutIndex++, false);
+ joinedOutTables.push_back(op.Output().Item(old_out_num));
+ }
+ TVector<TVector<size_t>> sortedGroups;
+ sortedGroups.reserve(groupedOuts.size());
+ for (auto& item: groupedOuts) {
+ sortedGroups.push_back(std::move(item.second));
+ }
+ ::Sort(sortedGroups, [](const TVector<size_t>& v1, const TVector<size_t>& v2) { return v1.front() < v2.front(); });
+ for (auto& outs: sortedGroups) {
+ TMaybe<size_t> newIndex;
+ bool drop = false;
+ for (auto old_out_num: outs) {
+ if (!newIndex) {
+ newIndex = nextNewOutIndex++;
+ joinedOutTables.push_back(op.Output().Item(old_out_num));
+ }
+ outRemap[old_out_num] = std::make_pair(*newIndex, drop);
+ drop = true;
+ }
+ }
+
+ size_t lambdaNdx = 0;
+ bool ordered = false;
+ if (auto ytMap = op.Maybe<TYtMap>()) {
+ lambdaNdx = TYtMap::idx_Mapper;
+ ordered = NYql::HasSetting(ytMap.Cast().Settings().Ref(), EYtSettingType::Ordered);
+ } else if (op.Maybe<TYtReduce>()) {
+ lambdaNdx = TYtReduce::idx_Reducer;
+ } else if (op.Maybe<TYtMapReduce>()) {
+ lambdaNdx = TYtMapReduce::idx_Reducer;
+ } else if (op.Maybe<TYtFill>()) {
+ lambdaNdx = TYtFill::idx_Content;
+ } else {
+ YQL_ENSURE(false, "Unsupported operation " << writer->Content());
+ }
+ TExprNode::TPtr lambda = writer->ChildPtr(lambdaNdx);
+ lambda = RebuildLambdaWithMergedOuts(lambda, nextNewOutIndex == 1, ordered, joinedOutTables, outRemap, ctx);
+
+ auto newOp = ctx.ChangeChild(op.Ref(), TYtOutputOpBase::idx_Output,
+ Build<TYtOutSection>(ctx, op.Pos()).Add(joinedOutTables).Done().Ptr());
+
+ newOp = ctx.ChangeChild(*newOp, lambdaNdx, std::move(lambda));
+
+ rewrite[writer] = newOp;
+ newOps[writer] = newOp;
+
+ TVector<TExprNode::TPtr> newOuts;
+ newOuts.resize(outCount);
+ TNodeSet processed;
+ for (auto& reader: x.second) {
+ auto oldOutput = TYtOutput(std::get<2>(reader));
+ if (processed.insert(oldOutput.Raw()).second) {
+ auto oldNdx = FromString<size_t>(oldOutput.OutIndex().Value());
+ YQL_ENSURE(oldNdx < outRemap.size());
+ auto newOut = Build<TYtOutput>(ctx, oldOutput.Pos())
+ .Operation(newOp)
+ .OutIndex()
+ .Value(outRemap[oldNdx].second ? TString("canary") : ToString(outRemap[oldNdx].first)) // Insert invalid "canary" index for dropped outputs
+ .Build()
+ .Mode(oldOutput.Mode())
+ .Done().Ptr();
+ rewrite[oldOutput.Raw()] = newOut;
+ if (!outRemap[oldNdx].second) {
+ newOuts[oldNdx] = newOut;
+ }
+ }
+ }
+
+ for (auto& reader: x.second) {
+ if (auto rawSection = std::get<1>(reader)) {
+ if (processed.insert(rawSection).second) {
+ auto section = TYtSection(rawSection);
+ bool updated = false;
+ TVector<TYtPath> updatedPaths;
+ for (auto path: section.Paths()) {
+ if (path.Table().Maybe<TYtOutput>().Operation().Raw() == writer) {
+ auto oldNdx = FromString<size_t>(path.Table().Cast<TYtOutput>().OutIndex().Value());
+ if (newOuts[oldNdx]) {
+ updatedPaths.push_back(Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table(newOuts[oldNdx])
+ .Done());
+ }
+ updated = true;
+ } else {
+ updatedPaths.push_back(path);
+ }
+ }
+ if (updated) {
+ YQL_ENSURE(!updatedPaths.empty());
+ rewrite[rawSection] = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ }
+ else if (TYtPublish::Match(std::get<0>(reader))) {
+ auto publish = TYtPublish(std::get<0>(reader));
+ if (processed.insert(publish.Raw()).second) {
+ bool updated = false;
+ TExprNode::TListType updatedOuts;
+ for (auto out: publish.Input()) {
+ if (out.Operation().Raw() == writer) {
+ auto oldNdx = FromString<size_t>(out.OutIndex().Value());
+ if (newOuts[oldNdx]) {
+ updatedOuts.push_back(newOuts[oldNdx]);
+ }
+ updated = true;
+ } else {
+ updatedOuts.push_back(out.Ptr());
+ }
+ }
+ if (updated) {
+ YQL_ENSURE(!updatedOuts.empty());
+ rewrite[publish.Raw()] = Build<TYtPublish>(ctx, publish.Pos())
+ .InitFrom(publish)
+ .Input()
+ .Add(updatedOuts)
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ ProcessedMultiOuts.insert(writer->UniqueId());
+ }
+ if (!lefts.empty() && !newOps.empty()) {
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ auto newIt = newOps.find(left.Input().Raw());
+ if (newIt != newOps.end()) {
+ rewrite[node] = ctx.ChangeChild(*node, TCoLeft::idx_Input, TExprNode::TPtr(newIt->second));
+ }
+ }
+ }
+
+ if (!rewrite.empty()) {
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-MergeMultiOuts";
+ return RemapExpr(input, output, rewrite, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ return TStatus::Ok;
+ }
+
+ static TExprNode::TPtr RebuildLambdaWithMergedOuts(const TExprNode::TPtr& lambda, bool singleOutput, bool ordered,
+ const TVector<TYtOutTable>& joinedOutTables, const TVector<std::pair<size_t, bool>>& outRemap, TExprContext& ctx)
+ {
+ if (singleOutput) {
+ if (lambda->Child(0)->ChildrenSize()) {
+ return Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVariantItem>()
+ .Variant("item")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ } else {
+ return Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVariantItem>()
+ .Variant("item")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ } else { // nextNewOutIndex > 1
+ TVector<TExprBase> tupleTypes;
+ for (auto out: joinedOutTables) {
+ auto itemType = out.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ tupleTypes.push_back(TExprBase(ExpandType(out.Pos(), *itemType, ctx)));
+ }
+ TExprBase varType = Build<TCoVariantType>(ctx, lambda->Pos())
+ .UnderlyingType<TCoTupleType>()
+ .Add(tupleTypes)
+ .Build()
+ .Done();
+
+ TVector<TExprBase> visitArgs;
+ for (size_t i: xrange(outRemap.size())) {
+ visitArgs.push_back(Build<TCoAtom>(ctx, lambda->Pos()).Value(ToString(i)).Done());
+ visitArgs.push_back(Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"row"})
+ .Body<TCoVariant>()
+ .Item("row")
+ .Index()
+ .Value(ToString(outRemap[i].first))
+ .Build()
+ .VarType(varType)
+ .Build()
+ .Done());
+ }
+
+ if (lambda->Child(0)->ChildrenSize()) {
+ return Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .With(0, "stream")
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ } else {
+ return Build<TCoLambda>(ctx, lambda->Pos())
+ .Args({})
+ .Body<TCoFlatMapBase>()
+ .CallableName(ordered ? TCoOrderedFlatMap::CallableName() : TCoFlatMap::CallableName())
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .Build()
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoVisit>()
+ .Input("item")
+ .FreeArgs()
+ .Add(visitArgs)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ }
+
+ TStatus FuseMultiOutsWithOuterMaps(TExprNode::TPtr input, TExprNode::TPtr& output, const TOpDeps& opDeps, const TNodeSet& lefts, const TNodeSet& hasWorldDeps, TExprContext& ctx) {
+ const ui64 switchLimit = State_->Configuration->SwitchLimit.Get().GetOrElse(DEFAULT_SWITCH_MEMORY_LIMIT);
+ const auto maxOperationFiles = State_->Configuration->MaxOperationFiles.Get().GetOrElse(DEFAULT_MAX_OPERATION_FILES);
+ const auto maxJobMemoryLimit = State_->Configuration->MaxExtraJobMemoryToFuseOperations.Get();
+ const auto maxOutTables = State_->Configuration->MaxOutputTables.Get().GetOrElse(DEFAULT_MAX_OUTPUT_TABLES);
+ const auto maxOuterCpuUsage = State_->Configuration->MaxCpuUsageToFuseMultiOuts.Get().GetOrElse(2.0);
+ const auto maxOuterReplicationFactor = State_->Configuration->MaxReplicationFactorToFuseMultiOuts.Get().GetOrElse(2.0);
+
+ for (auto& x: opDeps) {
+ if (TYtWithUserJobsOpBase::Match(x.first) && !BeingExecuted(*x.first) && !hasWorldDeps.contains(x.first)
+ && !NYql::HasAnySetting(*x.first->Child(TYtWithUserJobsOpBase::idx_Settings), EYtSettingType::Limit | EYtSettingType::SortLimitBy | EYtSettingType::JobCount)) {
+
+ const auto op = TYtWithUserJobsOpBase(x.first);
+
+ if (op.Maybe<TYtMapReduce>() && GetMapDirectOutputsCount(op.Cast<TYtMapReduce>()) != 0) { // TODO: optimize this case
+ continue;
+ }
+
+ size_t lambdaNdx = 0;
+ if (auto ytMap = op.Maybe<TYtMap>()) {
+ lambdaNdx = TYtMap::idx_Mapper;
+ } else if (op.Maybe<TYtReduce>()) {
+ lambdaNdx = TYtReduce::idx_Reducer;
+ } else if (op.Maybe<TYtMapReduce>()) {
+ lambdaNdx = TYtMapReduce::idx_Reducer;
+ } else {
+ YQL_ENSURE(false, "Unsupported operation " << op.Ref().Content());
+ }
+
+ auto lambda = TCoLambda(op.Ref().ChildPtr(lambdaNdx));
+ if (!IsYieldTransparent(lambda.Ptr(), *State_->Types)) {
+ continue;
+ }
+
+ std::map<size_t, std::pair<std::vector<const TExprNode*>, std::vector<const TExprNode*>>> maps; // output -> pair<vector<YtMap>, vector<other YtOutput's>>
+ for (size_t i = 0; i < x.second.size(); ++i) {
+ auto reader = std::get<0>(x.second[i]);
+ if (BeingExecuted(*reader)) {
+ maps.clear();
+ break;
+ }
+ const auto out = std::get<2>(x.second[i]);
+ const auto opIndex = FromString<size_t>(out->Child(TYtOutput::idx_OutIndex)->Content());
+ auto& item = maps[opIndex];
+ const TExprNode* matched = nullptr;
+ const auto newPair = ProcessedFuseWithOuterMaps.insert(std::make_pair(x.first->UniqueId(), reader->UniqueId())).second;
+ if (newPair && TYtMap::Match(reader)) {
+ const auto outerMap = TYtMap(reader);
+ if ((outerMap.World().Ref().IsWorld() || outerMap.World().Raw() == op.World().Raw())
+ && outerMap.Input().Size() == 1 && outerMap.DataSink().Cluster().Value() == op.DataSink().Cluster().Value()
+ && NYql::HasSetting(op.Settings().Ref(), EYtSettingType::Flow) == NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Flow)
+ && !HasYtRowNumber(outerMap.Mapper().Body().Ref())
+ && IsYieldTransparent(outerMap.Mapper().Ptr(), *State_->Types)
+ && (!op.Maybe<TYtMapReduce>() || AllOf(outerMap.Output(), [](const auto& out) { return !TYtTableBaseInfo::GetRowSpec(out)->IsSorted(); }))) {
+
+ const auto outerSection = outerMap.Input().Item(0);
+ if (outerSection.Paths().Size() == 1 && outerSection.Settings().Size() == 0) {
+ const auto outerPath = outerSection.Paths().Item(0);
+ if (outerPath.Ranges().Maybe<TCoVoid>()) {
+ matched = reader;
+ }
+ }
+ }
+ }
+ if (matched) {
+ item.first.push_back(matched);
+ } else {
+ item.second.push_back(out);
+ }
+ }
+
+ // Check limits
+ if (AnyOf(maps, [](const auto& item) { return item.second.first.size() > 0; })) {
+ TMap<TStringBuf, ui64> memUsage;
+ size_t currenFiles = 1; // jobstate. Take into account only once
+ size_t currOutTables = op.Output().Size();
+
+ TExprNode::TPtr updatedBody = lambda.Body().Ptr();
+ if (maxJobMemoryLimit) {
+ auto status = UpdateTableContentMemoryUsage(lambda.Body().Ptr(), updatedBody, State_, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+ ScanResourceUsage(*updatedBody, *State_->Configuration, State_->Types, maxJobMemoryLimit ? &memUsage : nullptr, nullptr, &currenFiles);
+
+ TMap<TStringBuf, ui64> newMemUsage;
+ TMap<TStringBuf, double> cpuUsage;
+ for (auto& item: maps) {
+ if (!item.second.first.empty()) {
+ for (auto it = item.second.first.begin(); it != item.second.first.end(); ) {
+ const auto outerMap = TYtMap(*it);
+
+ const size_t outTablesDelta = outerMap.Output().Size() - size_t(item.second.second.empty());
+
+ updatedBody = outerMap.Mapper().Body().Ptr();
+ if (maxJobMemoryLimit) {
+ auto status = UpdateTableContentMemoryUsage(outerMap.Mapper().Body().Ptr(), updatedBody, State_, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ }
+ TMap<TStringBuf, ui64>* pMemUsage = nullptr;
+ if (maxJobMemoryLimit) {
+ pMemUsage = &newMemUsage;
+ newMemUsage = memUsage;
+ }
+ size_t newCurrenFiles = currenFiles;
+ cpuUsage.clear();
+ ScanResourceUsage(*updatedBody, *State_->Configuration, State_->Types, pMemUsage, &cpuUsage, &newCurrenFiles);
+
+ auto usedMemory = Accumulate(memUsage.begin(), memUsage.end(), switchLimit,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ // Take into account codec input/output buffers (one for all inputs and one per output)
+ usedMemory += YQL_JOB_CODEC_MEM * (currOutTables + outTablesDelta + 1);
+
+ const auto usedCpu = Accumulate(cpuUsage.begin(), cpuUsage.end(), 1.0,
+ [](double prod, const std::pair<const TStringBuf, double>& val) { return prod * val.second; });
+
+ const auto replicationFactor = NCommon::GetDataReplicationFactor(outerMap.Mapper().Ref(), ctx);
+
+ bool skip = false;
+ if (replicationFactor > maxOuterReplicationFactor) {
+ YQL_CLOG(DEBUG, ProviderYt) << "FuseMultiOutsWithOuterMaps: skip by replication factor limites " << replicationFactor << " > " << maxOuterReplicationFactor;
+ skip = true;
+ }
+ else if (outTablesDelta && currOutTables + outTablesDelta > maxOutTables) {
+ YQL_CLOG(DEBUG, ProviderYt) << "FuseMultiOutsWithOuterMaps: skip by out table limits " << (currOutTables + outTablesDelta) << " > " << maxOutTables;
+ skip = true;
+ }
+ else if (newCurrenFiles > maxOperationFiles) {
+ YQL_CLOG(DEBUG, ProviderYt) << "FuseMultiOutsWithOuterMaps: skip by files limits " << newCurrenFiles << " > " << maxOperationFiles;
+ skip = true;
+ }
+ else if (maxJobMemoryLimit && usedMemory > *maxJobMemoryLimit) {
+ YQL_CLOG(DEBUG, ProviderYt) << "FuseMultiOutsWithOuterMaps: skip by memory limits " << usedMemory << " > " << *maxJobMemoryLimit;
+ skip = true;
+ }
+ else if (usedCpu > maxOuterCpuUsage) {
+ YQL_CLOG(DEBUG, ProviderYt) << "FuseMultiOutsWithOuterMaps: skip by cpu limits " << usedCpu << " > " << maxOuterCpuUsage;
+ skip = true;
+ }
+ if (skip) {
+ // Move to other usages
+ it = item.second.first.erase(it);
+ item.second.second.push_back(outerMap.Input().Item(0).Paths().Item(0).Table().Raw());
+ continue;
+ }
+ currenFiles = newCurrenFiles;
+ memUsage = std::move(newMemUsage);
+ currOutTables += outTablesDelta;
+ ++it;
+ }
+ }
+ }
+ }
+
+ if (AnyOf(maps, [](const auto& item) { return item.second.first.size() > 0; })) {
+ TNodeOnNodeOwnedMap remaps;
+ TNodeSet removed;
+ TNodeMap<size_t> newOutIndicies; // old YtOutput -> new index
+
+ TVector<TExprBase> switchArgs;
+ TVector<TYtOutTable> outs;
+ for (size_t i = 0; i < op.Output().Size(); ++i) {
+ auto& item = maps[i];
+
+ if (item.first.empty() || !item.second.empty()) {
+ for (const auto out: item.second) {
+ newOutIndicies[out] = outs.size();
+ }
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, lambda.Pos())
+ .Add()
+ .Value(ctx.GetIndexAsString(i))
+ .Build()
+ .Done());
+ switchArgs.push_back(
+ Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"flow"})
+ .Body("flow")
+ .Done());
+ outs.push_back(op.Output().Item(i));
+ }
+
+ for (auto reader: item.first) {
+ if (auto it = opDeps.find(reader); it != opDeps.end()) {
+ for (auto& deps: it->second) {
+ const auto out = std::get<2>(deps);
+ const auto oldIndex = FromString<size_t>(out->Child(TYtOutput::idx_OutIndex)->Content());
+ newOutIndicies[out] = outs.size() + oldIndex;
+ }
+ }
+ const auto outerMap = TYtMap(reader);
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, lambda.Pos())
+ .Add()
+ .Value(ctx.GetIndexAsString(i))
+ .Build()
+ .Done());
+
+ TExprNode::TListType members;
+ for (auto item : GetSeqItemType(*outerMap.Mapper().Args().Arg(0).Ref().GetTypeAnn()).Cast<TStructExprType>()->GetItems()) {
+ members.push_back(ctx.NewAtom(outerMap.Mapper().Pos(), item->GetName()));
+ }
+
+ auto [placeHolder, lambdaWithPlaceholder] = ReplaceDependsOn(outerMap.Mapper().Ptr(), ctx, State_->Types);
+ YQL_ENSURE(placeHolder);
+
+ switchArgs.push_back(Build<TCoLambda>(ctx, outerMap.Mapper().Pos())
+ .Args({"flow"})
+ .Body<TExprApplier>()
+ .Apply(TCoLambda(lambdaWithPlaceholder))
+ .With<TCoExtractMembers>(0)
+ .Input("flow")
+ .Members()
+ .Add(members)
+ .Build()
+ .Build()
+ .With(TExprBase(placeHolder), "flow")
+ .Build()
+ .Done());
+
+ outs.insert(outs.end(), outerMap.Output().begin(), outerMap.Output().end());
+ removed.insert(reader);
+ }
+ }
+
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"flow"})
+ .Body<TCoSwitch>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With(0, "flow")
+ .Build()
+ .BufferBytes()
+ .Value(ToString(switchLimit))
+ .Build()
+ .FreeArgs()
+ .Add(switchArgs)
+ .Build()
+ .Build()
+ .Done();
+
+ auto newOp = ctx.ChangeChild(op.Ref(), TYtOutputOpBase::idx_Output,
+ Build<TYtOutSection>(ctx, op.Pos()).Add(outs).Done().Ptr());
+
+ newOp = ctx.ChangeChild(*newOp, lambdaNdx, lambda.Ptr());
+
+ remaps[op.Raw()] = newOp;
+ for (auto node: lefts) {
+ TCoLeft left(node);
+ if (removed.contains(left.Input().Raw())) {
+ remaps[node] = ctx.ChangeChild(*node, TCoLeft::idx_Input, TExprNode::TPtr(newOp));
+ }
+ }
+ for (auto& item: newOutIndicies) {
+ remaps[item.first] = Build<TYtOutput>(ctx, item.first->Pos())
+ .InitFrom(TYtOutput(item.first))
+ .Operation(newOp)
+ .OutIndex()
+ .Value(ctx.GetIndexAsString(item.second))
+ .Build()
+ .Done().Ptr();
+ }
+ YQL_CLOG(INFO, ProviderYt) << "PhysicalFinalizing-FuseMultiOutsWithOuterMaps";
+ return RemapExpr(input, output, remaps, ctx, TOptimizeExprSettings(State_->Types));
+ }
+ }
+ }
+ return TStatus::Ok;
+ }
+
+ bool BeingExecuted(const TExprNode& node) {
+ return node.GetState() > TExprNode::EState::ExecutionRequired || node.HasResult();
+ }
+
+private:
+ TYtState::TPtr State_;
+ TProcessedNodesSet ProcessedMergePublish;
+ TProcessedNodesSet ProcessedSplitLargeInputs;
+ TProcessedNodesSet ProcessedUnusedOuts;
+ TProcessedNodesSet ProcessedMultiOuts;
+ TProcessedNodesSet ProcessedHorizontalJoin;
+ TProcessedNodesSet ProcessedFieldSubsetForMultiUsage;
+ std::unordered_set<std::pair<ui64, ui64>, THash<std::pair<ui64, ui64>>> ProcessedFuseWithOuterMaps;
+};
+
+THashSet<TStringBuf> TYtPhysicalFinalizingTransformer::OPS_WITH_SORTED_OUTPUT = {
+ TYtMerge::CallableName(),
+ TYtMap::CallableName(),
+ TYtSort::CallableName(),
+ TYtReduce::CallableName(),
+ TYtFill::CallableName(),
+};
+
+}
+
+THolder<IGraphTransformer> CreateYtPhysicalFinalizingTransformer(TYtState::TPtr state) {
+ return THolder(new TYtPhysicalFinalizingTransformer(state));
+}
+
+} // NYql
+
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
new file mode 100644
index 0000000000..ef2dd51a0c
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_physical_optimize.cpp
@@ -0,0 +1,7383 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_table.h"
+#include "yql_yt_join_impl.h"
+#include "yql_yt_optimize.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/lib/mkql_helpers/mkql_helpers.h>
+#include <ydb/library/yql/providers/yt/lib/expr_traits/yql_expr_traits.h>
+#include <ydb/library/yql/providers/yt/lib/key_filter/yql_key_filter.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/yt/opt/yql_yt_key_selector.h>
+#include <ydb/library/yql/providers/stat/expr_nodes/yql_stat_expr_nodes.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider.h>
+#include <ydb/library/yql/providers/common/transform/yql_optimize.h>
+#include <ydb/library/yql/providers/common/codec/yql_codec_type_flags.h>
+#include <ydb/library/yql/providers/dq/expr_nodes/dqs_expr_nodes.h>
+#include <ydb/library/yql/dq/integration/yql_dq_integration.h>
+#include <ydb/library/yql/providers/result/expr_nodes/yql_res_expr_nodes.gen.h>
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/core/yql_join.h>
+#include <ydb/library/yql/core/yql_type_helpers.h>
+#include <ydb/library/yql/core/yql_data_provider.h>
+#include <ydb/library/yql/dq/expr_nodes/dq_expr_nodes.h>
+#include <ydb/library/yql/dq/opt/dq_opt_phy.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/core/extract_predicate/extract_predicate.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+#include <util/string/cast.h>
+#include <util/string/type.h>
+#include <util/generic/xrange.h>
+#include <util/generic/maybe.h>
+#include <util/generic/map.h>
+#include <util/generic/set.h>
+#include <util/generic/size_literals.h>
+
+#include <algorithm>
+#include <type_traits>
+
+namespace NYql {
+
+namespace {
+
+using namespace NNodes;
+using namespace NDq;
+
+class TYtPhysicalOptProposalTransformer : public TOptimizeTransformerBase {
+public:
+ TYtPhysicalOptProposalTransformer(TYtState::TPtr state)
+ : TOptimizeTransformerBase(state->Types, NLog::EComponent::ProviderYt, state->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>()))
+ , State_(state)
+ {
+#define HNDL(name) "PhysicalOptimizer-"#name, Hndl(&TYtPhysicalOptProposalTransformer::name)
+ AddHandler(0, &TCoMux::Match, HNDL(Mux));
+ AddHandler(0, &TYtWriteTable::Match, HNDL(Write));
+ AddHandler(0, &TYtWriteTable::Match, HNDL(DqWrite));
+ AddHandler(0, Names({TCoLength::CallableName(), TCoHasItems::CallableName()}), HNDL(Length));
+ AddHandler(0, &TCoSort::Match, HNDL(Sort));
+ AddHandler(0, &TYtSort::Match, HNDL(YtSortOverAlreadySorted));
+ AddHandler(0, &TCoPartitionByKeyBase::Match, HNDL(PartitionByKey));
+ AddHandler(0, &TCoFlatMapBase::Match, HNDL(FlatMap));
+ AddHandler(0, &TCoCombineByKey::Match, HNDL(CombineByKey));
+ AddHandler(0, &TCoLMap::Match, HNDL(LMap<TCoLMap>));
+ AddHandler(0, &TCoOrderedLMap::Match, HNDL(LMap<TCoOrderedLMap>));
+ AddHandler(0, &TCoEquiJoin::Match, HNDL(EquiJoin));
+ AddHandler(0, &TCoCountBase::Match, HNDL(TakeOrSkip));
+ AddHandler(0, &TYtWriteTable::Match, HNDL(Fill));
+ AddHandler(0, &TResPull::Match, HNDL(ResPull));
+ if (State_->Configuration->UseNewPredicateExtraction.Get().GetOrElse(DEFAULT_USE_NEW_PREDICATE_EXTRACTION)) {
+ AddHandler(0, Names({TYtMap::CallableName(), TYtMapReduce::CallableName()}), HNDL(ExtractKeyRange));
+ AddHandler(0, &TCoFlatMapBase::Match, HNDL(ExtractKeyRangeDqReadWrap));
+ } else {
+ AddHandler(0, Names({TYtMap::CallableName(), TYtMapReduce::CallableName()}), HNDL(ExtractKeyRangeLegacy));
+ }
+ AddHandler(0, &TCoExtendBase::Match, HNDL(ExtendDqReadWraps));
+ AddHandler(0, &TCoExtendBase::Match, HNDL(Extend));
+ AddHandler(0, &TCoAssumeSorted::Match, HNDL(AssumeSorted));
+ AddHandler(0, &TYtMapReduce::Match, HNDL(AddTrivialMapperForNativeYtTypes));
+ AddHandler(0, &TYtDqWrite::Match, HNDL(YtDqWrite));
+ AddHandler(0, &TYtDqProcessWrite::Match, HNDL(YtDqProcessWrite));
+ AddHandler(0, &TYtTransientOpBase::Match, HNDL(ZeroSample));
+
+ AddHandler(1, &TYtMap::Match, HNDL(FuseInnerMap));
+ AddHandler(1, &TYtMap::Match, HNDL(FuseOuterMap));
+ AddHandler(1, Names({TYtMap::CallableName(), TYtMapReduce::CallableName()}), HNDL(MapFieldsSubset));
+ AddHandler(1, Names({TYtMapReduce::CallableName(), TYtReduce::CallableName()}), HNDL(ReduceFieldsSubset));
+ AddHandler(1, Names({TYtMap::CallableName(), TYtMapReduce::CallableName()}), HNDL(MultiMapFieldsSubset));
+ AddHandler(1, Names({TYtMapReduce::CallableName(), TYtReduce::CallableName()}), HNDL(MultiReduceFieldsSubset));
+ AddHandler(1, Names({TYtMap::CallableName(), TYtMapReduce::CallableName()}), HNDL(WeakFields));
+ AddHandler(1, &TYtTransientOpBase::Match, HNDL(BypassMerge));
+ AddHandler(1, &TYtPublish::Match, HNDL(BypassMergeBeforePublish));
+ AddHandler(1, Or({&TYtWithUserJobsOpBase::Match, &TYtFill::Match}), HNDL(TableContentWithSettings));
+ AddHandler(1, Or({&TYtWithUserJobsOpBase::Match, &TYtFill::Match}), HNDL(NonOptimalTableContent));
+ AddHandler(1, &TCoRight::Match, HNDL(ReadWithSettings));
+ AddHandler(1, &TDqReadWrapBase::Match, HNDL(DqReadWrapWithSettings));
+ AddHandler(1, &TYtTransientOpBase::Match, HNDL(PushDownKeyExtract));
+ AddHandler(1, &TYtTransientOpBase::Match, HNDL(TransientOpWithSettings));
+ AddHandler(1, &TYtSort::Match, HNDL(TopSort));
+ AddHandler(1, &TYtWithUserJobsOpBase::Match, HNDL(EmbedLimit));
+ AddHandler(1, &TYtMerge::Match, HNDL(PushMergeLimitToInput));
+
+ AddHandler(2, &TYtEquiJoin::Match, HNDL(RuntimeEquiJoin));
+ AddHandler(2, &TStatWriteTable::Match, HNDL(ReplaceStatWriteTable));
+ AddHandler(2, &TYtMap::Match, HNDL(MapToMerge));
+ AddHandler(2, &TYtPublish::Match, HNDL(UnorderedPublishTarget));
+ AddHandler(2, &TYtMap::Match, HNDL(PushDownYtMapOverSortedMerge));
+ AddHandler(2, &TYtMerge::Match, HNDL(MergeToCopy));
+#undef HNDL
+ }
+private:
+ static TYtDSink GetDataSink(TExprBase input, TExprContext& ctx) {
+ if (auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ return TYtDSink(ctx.RenameNode(read.Cast().DataSource().Ref(), "DataSink"));
+ } else if (auto out = input.Maybe<TYtOutput>()) {
+ return GetOutputOp(out.Cast()).DataSink();
+ } else {
+ YQL_ENSURE(false, "Unknown operation input");
+ }
+ }
+
+ static TExprBase GetWorld(TExprBase input, TMaybeNode<TExprBase> main, TExprContext& ctx) {
+ if (!main) {
+ main = ctx.NewWorld(input.Pos());
+ }
+ TSyncMap syncList;
+ if (auto maybeRead = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (read.World().Ref().Type() != TExprNode::World) {
+ syncList.emplace(read.World().Ptr(), syncList.size());
+ }
+ } else {
+ YQL_ENSURE(input.Maybe<TYtOutput>(), "Unknown operation input: " << input.Ref().Content());
+ }
+ return TExprBase(ApplySyncListToWorld(main.Cast().Ptr(), syncList, ctx));
+ }
+
+ template <class TExpr>
+ TMaybeNode<TExpr> CleanupWorld(TExpr node, TExprContext& ctx) const {
+ return TMaybeNode<TExpr>(YtCleanupWorld(node.Ptr(), ctx, State_));
+ }
+
+ struct TConvertInputOpts {
+ TMaybeNode<TCoNameValueTupleList> Settings_;
+ TMaybeNode<TCoAtomList> CustomFields_;
+ bool KeepDirecRead_;
+ bool MakeUnordered_;
+ bool ClearUnordered_;
+
+ TConvertInputOpts()
+ : KeepDirecRead_(false)
+ , MakeUnordered_(false)
+ , ClearUnordered_(false)
+ {
+ }
+
+ TConvertInputOpts& Settings(const TMaybeNode<TCoNameValueTupleList>& settings) {
+ Settings_ = settings;
+ return *this;
+ }
+
+ TConvertInputOpts& CustomFields(const TMaybeNode<TCoAtomList>& customFields) {
+ CustomFields_ = customFields;
+ return *this;
+ }
+
+ TConvertInputOpts& ExplicitFields(const TYqlRowSpecInfo& rowSpec, TPositionHandle pos, TExprContext& ctx) {
+ TVector<TString> columns;
+ for (auto item: rowSpec.GetType()->GetItems()) {
+ columns.emplace_back(item->GetName());
+ }
+ for (auto item: rowSpec.GetAuxColumns()) {
+ columns.emplace_back(item.first);
+ }
+ CustomFields_ = ToAtomList(columns, pos, ctx);
+ return *this;
+ }
+
+ TConvertInputOpts& ExplicitFields(const TStructExprType& type, TPositionHandle pos, TExprContext& ctx) {
+ TVector<TString> columns;
+ for (auto item: type.GetItems()) {
+ columns.emplace_back(item->GetName());
+ }
+ CustomFields_ = ToAtomList(columns, pos, ctx);
+ return *this;
+ }
+
+ TConvertInputOpts& KeepDirecRead(bool keepDirecRead = true) {
+ KeepDirecRead_ = keepDirecRead;
+ return *this;
+ }
+
+ TConvertInputOpts& MakeUnordered(bool makeUnordered = true) {
+ YQL_ENSURE(!makeUnordered || !ClearUnordered_);
+ MakeUnordered_ = makeUnordered;
+ return *this;
+ }
+
+ TConvertInputOpts& ClearUnordered() {
+ YQL_ENSURE(!MakeUnordered_);
+ ClearUnordered_ = true;
+ return *this;
+ }
+ };
+
+ static TYtSectionList ConvertInputTable(TExprBase input, TExprContext& ctx, const TConvertInputOpts& opts = {}) {
+ TVector<TYtSection> sections;
+ TExprBase columns = opts.CustomFields_ ? TExprBase(opts.CustomFields_.Cast()) : TExprBase(Build<TCoVoid>(ctx, input.Pos()).Done());
+ if (auto out = input.Maybe<TYtOutput>()) {
+ auto settings = opts.Settings_;
+ if (!settings) {
+ settings = Build<TCoNameValueTupleList>(ctx, input.Pos()).Done();
+ }
+ TMaybeNode<TCoAtom> mode = out.Mode();
+ if (opts.MakeUnordered_) {
+ mode = Build<TCoAtom>(ctx, out.Cast().Pos()).Value(ToString(EYtSettingType::Unordered)).Done();
+ } else if (opts.ClearUnordered_) {
+ mode = {};
+ }
+ sections.push_back(Build<TYtSection>(ctx, input.Pos())
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .InitFrom(out.Cast())
+ .Mode(mode)
+ .Build()
+ .Columns(columns)
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings(settings.Cast())
+ .Done());
+ }
+ else {
+ auto read = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ YQL_ENSURE(read, "Unknown operation input");
+
+ for (auto section: read.Cast().Input()) {
+ bool makeUnordered = opts.MakeUnordered_;
+
+ auto mergedSettings = section.Settings().Ptr();
+ if (NYql::HasSetting(*mergedSettings, EYtSettingType::Unordered)) {
+ mergedSettings = NYql::RemoveSetting(*mergedSettings, EYtSettingType::Unordered, ctx);
+ makeUnordered = false;
+ }
+ if (!opts.KeepDirecRead_) {
+ mergedSettings = NYql::RemoveSetting(*mergedSettings, EYtSettingType::DirectRead, ctx);
+ }
+ if (opts.Settings_) {
+ mergedSettings = MergeSettings(*mergedSettings, opts.Settings_.Cast().Ref(), ctx);
+ }
+
+ section = Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(mergedSettings)
+ .Done();
+
+ if (makeUnordered) {
+ section = MakeUnorderedSection(section, ctx);
+ } else if (opts.ClearUnordered_) {
+ section = ClearUnorderedSection(section, ctx);
+ }
+
+ if (opts.CustomFields_) {
+ section = UpdateInputFields(section, opts.CustomFields_.Cast(), ctx);
+ }
+
+ sections.push_back(section);
+ }
+ }
+
+ return Build<TYtSectionList>(ctx, input.Pos())
+ .Add(sections)
+ .Done();
+ }
+
+ static bool CollectMemberPaths(TExprBase row, const TExprNode::TPtr& lookupItem,
+ TMap<TStringBuf, TVector<ui32>>& memberPaths, TVector<ui32>& currPath)
+ {
+ if (lookupItem->IsCallable("Member") && lookupItem->Child(0) == row.Raw()) {
+ TStringBuf memberName = lookupItem->Child(1)->Content();
+ memberPaths.insert({ memberName, currPath });
+ return true;
+ }
+
+ if (lookupItem->IsList()) {
+ for (ui32 i = 0; i < lookupItem->ChildrenSize(); ++i) {
+ currPath.push_back(i);
+ auto res = CollectMemberPaths(row, lookupItem->ChildPtr(i), memberPaths, currPath);
+ currPath.pop_back();
+ if (!res) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return !IsDepended(*lookupItem, row.Ref());
+ }
+
+ static bool CollectMemberPaths(TExprBase row, const TExprNode::TPtr& lookupItem,
+ TMap<TStringBuf, TVector<ui32>>& memberPaths)
+ {
+ TVector<ui32> currPath;
+ return CollectMemberPaths(row, lookupItem, memberPaths, currPath);
+ }
+
+ static bool CollectKeyPredicatesFromLookup(TExprBase row, TCoLookupBase lookup, TVector<TKeyFilterPredicates>& ranges,
+ size_t maxTables)
+ {
+ TExprNode::TPtr collection;
+ if (lookup.Collection().Ref().IsList()) {
+ collection = lookup.Collection().Ptr();
+ } else if (auto maybeAsList = lookup.Collection().Maybe<TCoAsList>()) {
+ collection = maybeAsList.Cast().Ptr();
+ } else if (auto maybeDictFromKeys = lookup.Collection().Maybe<TCoDictFromKeys>()) {
+ collection = maybeDictFromKeys.Cast().Keys().Ptr();
+ } else {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": unsupported collection " << lookup.Collection().Ref().Content();
+ return false;
+ }
+
+ auto size = collection->ChildrenSize();
+ if (!size) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": empty keys collection";
+ return false;
+ }
+
+ if (size + ranges.size() > maxTables) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": too many dict keys - " << size;
+ return false;
+ }
+
+ if (IsDepended(*collection, row.Ref())) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": depends on lambda arg";
+ return false;
+ }
+
+ TExprNode::TPtr lookupItem = lookup.Lookup().Ptr();
+ TMap<TStringBuf, TVector<ui32>> memberPaths;
+ if (!CollectMemberPaths(row, lookupItem, memberPaths)) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": unsupported lookup item";
+ return false;
+ }
+
+ if (memberPaths.empty()) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": no key predicates in lookup item";
+ return false;
+ }
+
+ for (auto& collectionItem : collection->Children()) {
+ ranges.emplace_back();
+ for (auto& memberAndPath : memberPaths) {
+ auto member = memberAndPath.first;
+ auto& path = memberAndPath.second;
+ auto value = collectionItem;
+ for (auto idx : path) {
+ if (!value->IsList() || idx >= value->ChildrenSize()) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": unexpected literal structure";
+ return false;
+ }
+ value = value->ChildPtr(idx);
+ }
+ ranges.back().emplace(member, std::make_pair(TString{TCoCmpEqual::CallableName()}, value));
+ }
+ }
+
+ return true;
+ }
+
+ static bool CollectKeyPredicatesAnd(TExprBase row, const std::vector<TExprBase>& predicates, TVector<TKeyFilterPredicates>& ranges, size_t maxTables)
+ {
+ TVector<TKeyFilterPredicates> leftRanges;
+ for (const auto& predicate : predicates) {
+ TVector<TKeyFilterPredicates> rightRanges;
+ if (CollectKeyPredicates(row, predicate, rightRanges, maxTables)) {
+ if (leftRanges.empty()) {
+ leftRanges = std::move(rightRanges);
+ } else {
+ const auto total = leftRanges.size() * rightRanges.size();
+ if (total + ranges.size() > maxTables) {
+ YQL_CLOG(DEBUG, ProviderYt) << __func__ << ": too many tables - " << (total + ranges.size());
+ return false;
+ }
+
+ if (1U == total) {
+ leftRanges.front().insert(rightRanges.front().cbegin(), rightRanges.front().cend());
+ } else {
+ TVector<TKeyFilterPredicates> temp;
+ temp.reserve(total);
+ for (const auto& left : leftRanges) {
+ for (const auto& right : rightRanges) {
+ temp.emplace_back(left);
+ temp.back().insert(right.cbegin(), right.cend());
+ }
+ }
+ leftRanges = std::move(temp);
+ }
+ }
+ }
+ }
+
+ if (leftRanges.empty()) {
+ return false;
+ }
+
+ std::move(leftRanges.begin(), leftRanges.end(), std::back_inserter(ranges));
+ return true;
+ }
+
+ static bool CollectKeyPredicatesOr(TExprBase row, const std::vector<TExprBase>& predicates, TVector<TKeyFilterPredicates>& ranges, size_t maxTables)
+ {
+ for (const auto& predicate : predicates) {
+ if (!CollectKeyPredicates(row, predicate, ranges, maxTables)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static bool CollectKeyPredicates(TExprBase row, TExprBase predicate, TVector<TKeyFilterPredicates>& ranges, size_t maxTables)
+ {
+ if (const auto maybeAnd = predicate.Maybe<TCoAnd>()) {
+ const auto size = maybeAnd.Cast().Args().size();
+ std::vector<TExprBase> predicates;
+ predicates.reserve(size);
+ for (auto i = 0U; i < size; ++i) {
+ predicates.emplace_back(maybeAnd.Cast().Arg(i));
+ }
+ return CollectKeyPredicatesAnd(row, predicates, ranges, maxTables);
+ }
+
+ if (const auto maybeOr = predicate.Maybe<TCoOr>()) {
+ const auto size = maybeOr.Cast().Args().size();
+ std::vector<TExprBase> predicates;
+ predicates.reserve(size);
+ for (auto i = 0U; i < size; ++i) {
+ predicates.emplace_back(maybeOr.Cast().Arg(i));
+ }
+ return CollectKeyPredicatesOr(row, predicates, ranges, maxTables);
+ }
+
+ TMaybeNode<TCoCompare> maybeCompare = predicate.Maybe<TCoCompare>();
+ if (auto maybeLiteral = predicate.Maybe<TCoCoalesce>().Value().Maybe<TCoBool>().Literal()) {
+ if (maybeLiteral.Cast().Value() == "false") {
+ maybeCompare = predicate.Cast<TCoCoalesce>().Predicate().Maybe<TCoCompare>();
+ }
+ }
+
+ auto getRowMember = [row] (TExprBase expr) {
+ if (auto maybeMember = expr.Maybe<TCoMember>()) {
+ if (maybeMember.Cast().Struct().Raw() == row.Raw()) {
+ return maybeMember;
+ }
+ }
+
+ return TMaybeNode<TCoMember>();
+ };
+
+ if (maybeCompare) {
+ auto left = maybeCompare.Cast().Left();
+ auto right = maybeCompare.Cast().Right();
+
+ TMaybeNode<TCoMember> maybeMember = getRowMember(left);
+ TMaybeNode<TExprBase> maybeValue = right;
+ bool invert = false;
+ if (!maybeMember) {
+ maybeMember = getRowMember(right);
+ maybeValue = left;
+ invert = true;
+ }
+
+ if (!maybeMember) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": comparison with non-member";
+ return false;
+ }
+
+ const auto value = maybeValue.Cast();
+ if (IsDepended(value.Ref(), row.Ref())) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": depends on lambda arg";
+ return false;
+ }
+
+ auto column = maybeMember.Cast().Name().Value();
+ TString cmpOp = TString{maybeCompare.Cast().Ref().Content()};
+ if (!IsRangeComparison(cmpOp)) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": non-range comparison " << cmpOp;
+ return false;
+ }
+
+ if (invert) {
+ if (TCoCmpStartsWith::CallableName() == cmpOp)
+ return false;
+
+ switch (cmpOp.front()) {
+ case '<': cmpOp.replace(0, 1, 1, '>'); break;
+ case '>': cmpOp.replace(0, 1, 1, '<'); break;
+ default: break;
+ }
+ }
+
+ ranges.emplace_back();
+ ranges.back().emplace(column, std::make_pair(cmpOp, value.Ptr()));
+ return true;
+ }
+
+ if (auto maybeLookup = predicate.Maybe<TCoLookupBase>()) {
+ return CollectKeyPredicatesFromLookup(row, maybeLookup.Cast(), ranges, maxTables);
+ }
+
+ if (auto maybeLiteral = predicate.Maybe<TCoCoalesce>().Value().Maybe<TCoBool>().Literal()) {
+ if (maybeLiteral.Cast().Value() == "false") {
+ if (auto maybeLookup = predicate.Maybe<TCoCoalesce>().Predicate().Maybe<TCoLookupBase>()) {
+ return CollectKeyPredicatesFromLookup(row, maybeLookup.Cast(), ranges, maxTables);
+ }
+ }
+ }
+
+ if (auto maybeNotExists = predicate.Maybe<TCoNot>().Value().Maybe<TCoExists>().Optional()) {
+ TMaybeNode<TCoMember> maybeMember = getRowMember(maybeNotExists.Cast());
+ if (!maybeMember) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": Not Exists for non-member";
+ return false;
+ }
+ auto column = maybeMember.Cast().Name().Value();
+ ranges.emplace_back();
+ ranges.back().emplace(column, std::make_pair(TString{TCoCmpEqual::CallableName()}, TExprNode::TPtr()));
+ return true;
+ }
+
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": unsupported predicate " << predicate.Ref().Content();
+ return false;
+ }
+
+ static TVector<TYtOutTable> ConvertMultiOutTables(TPositionHandle pos, const TTypeAnnotationNode* outItemType, TExprContext& ctx,
+ const TYtState::TPtr& state, const TMultiConstraintNode* multi = nullptr) {
+ TVector<TYtOutTable> outTables;
+ YQL_ENSURE(outItemType->GetKind() == ETypeAnnotationKind::Variant);
+ const TTupleExprType* tupleType = outItemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+ size_t ndx = 0;
+ const ui64 nativeTypeFlags = state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE;
+ for (auto tupleItemType: tupleType->GetItems()) {
+ TYtOutTableInfo outTableInfo(tupleItemType->Cast<TStructExprType>(), nativeTypeFlags);
+ const TConstraintSet* constraints = multi ? multi->GetItem(ndx) : nullptr;
+ if (constraints)
+ outTableInfo.RowSpec->SetConstraints(*constraints);
+ outTables.push_back(outTableInfo.SetUnique(constraints ? constraints->GetConstraint<TDistinctConstraintNode>() : nullptr, pos, ctx).ToExprNode(ctx, pos).Cast<TYtOutTable>());
+ ++ndx;
+ }
+ return outTables;
+ }
+
+ static TVector<TYtOutTable> ConvertOutTables(TPositionHandle pos, const TTypeAnnotationNode* outItemType, TExprContext& ctx,
+ const TYtState::TPtr& state, const TConstraintSet* constraint = nullptr) {
+ if (outItemType->GetKind() == ETypeAnnotationKind::Variant) {
+ return ConvertMultiOutTables(pos, outItemType, ctx, state, constraint ? constraint->GetConstraint<TMultiConstraintNode>() : nullptr);
+ }
+
+ TYtOutTableInfo outTableInfo(outItemType->Cast<TStructExprType>(), state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ if (constraint)
+ outTableInfo.RowSpec->SetConstraints(*constraint);
+ return TVector<TYtOutTable>{outTableInfo.SetUnique(constraint ? constraint->GetConstraint<TDistinctConstraintNode>() : nullptr, pos, ctx).ToExprNode(ctx, pos).Cast<TYtOutTable>()};
+ }
+
+ static TVector<TYtOutTable> ConvertMultiOutTablesWithSortAware(TExprNode::TPtr& lambda, bool& ordered, TPositionHandle pos,
+ const TTypeAnnotationNode* outItemType, TExprContext& ctx, const TYtState::TPtr& state, const TConstraintSet& constraints) {
+
+ YQL_ENSURE(outItemType->GetKind() == ETypeAnnotationKind::Variant);
+
+ const ui64 nativeTypeFlags = state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE;
+ const bool useNativeDescSort = state->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ const auto multi = constraints.GetConstraint<TMultiConstraintNode>();
+ const TTupleExprType* tupleType = outItemType->Cast<TVariantExprType>()->GetUnderlyingType()->Cast<TTupleExprType>();
+
+ ordered = false;
+ TVector<TYtOutTable> outTables;
+ size_t ndx = 0;
+ TVector<TExprBase> switchArgs;
+ for (auto tupleItemType: tupleType->GetItems()) {
+ const TConstraintSet* itemConstraints = multi ? multi->GetItem(ndx) : nullptr;
+ TYtOutTableInfo outTable(tupleItemType->Cast<TStructExprType>(), nativeTypeFlags);
+ TExprNode::TPtr remapper;
+ if (auto sorted = itemConstraints ? itemConstraints->GetConstraint<TSortedConstraintNode>() : nullptr) {
+ TKeySelectorBuilder builder(pos, ctx, useNativeDescSort, tupleItemType->Cast<TStructExprType>());
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+ if (builder.NeedMap()) {
+ remapper = builder.MakeRemapLambda(true);
+ }
+ ordered = true;
+ }
+ if (remapper) {
+ if (ndx > 0 && switchArgs.empty()) {
+ for (size_t i = 0; i < ndx; ++i) {
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, pos)
+ .Add()
+ .Value(i)
+ .Build()
+ .Done());
+ switchArgs.push_back(Build<TCoLambda>(ctx, pos).Args({"stream"}).Body("stream").Done());
+ }
+ }
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, pos)
+ .Add()
+ .Value(ndx)
+ .Build()
+ .Done());
+ switchArgs.push_back(TExprBase(remapper));
+ } else if (!switchArgs.empty()) {
+ switchArgs.push_back(
+ Build<TCoAtomList>(ctx, pos)
+ .Add()
+ .Value(ndx)
+ .Build()
+ .Done());
+ switchArgs.push_back(Build<TCoLambda>(ctx, pos).Args({"stream"}).Body("stream").Done());
+ }
+ if (itemConstraints)
+ outTable.RowSpec->SetConstraints(*itemConstraints);
+ outTables.push_back(outTable
+ .SetUnique(itemConstraints ? itemConstraints->GetConstraint<TDistinctConstraintNode>() : nullptr, pos, ctx)
+ .ToExprNode(ctx, pos).Cast<TYtOutTable>()
+ );
+ ++ndx;
+ }
+ if (!switchArgs.empty()) {
+ lambda = Build<TCoLambda>(ctx, pos)
+ .Args({"stream"})
+ .Body<TCoSwitch>()
+ .Input<TExprApplier>()
+ .Apply(TCoLambda(lambda))
+ .With(0, "stream")
+ .Build()
+ .BufferBytes()
+ .Value(state->Configuration->SwitchLimit.Get().GetOrElse(DEFAULT_SWITCH_MEMORY_LIMIT))
+ .Build()
+ .FreeArgs()
+ .Add(switchArgs)
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ return outTables;
+ }
+
+ static TYtOutTable ConvertSingleOutTableWithSortAware(TExprNode::TPtr& lambda, bool& ordered, TPositionHandle pos,
+ const TTypeAnnotationNode* outItemType, TExprContext& ctx, const TYtState::TPtr& state, const TConstraintSet& constraints) {
+
+ const ui64 nativeTypeFlags = state->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE;
+ const bool useNativeDescSort = state->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ const auto outStructType = outItemType->Cast<TStructExprType>();
+
+ ordered = false;
+ TYtOutTableInfo outTable(outStructType, nativeTypeFlags);
+ if (auto sorted = constraints.GetConstraint<TSortedConstraintNode>()) {
+ TKeySelectorBuilder builder(pos, ctx, useNativeDescSort, outStructType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+
+ if (builder.NeedMap()) {
+ lambda = ctx.Builder(pos)
+ .Lambda()
+ .Param("stream")
+ .Apply(builder.MakeRemapLambda(true))
+ .With(0)
+ .Apply(*lambda)
+ .With(0, "stream")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ ordered = true;
+ }
+ outTable.RowSpec->SetConstraints(constraints);
+ outTable.SetUnique(constraints.GetConstraint<TDistinctConstraintNode>(), pos, ctx);
+ return outTable.ToExprNode(ctx, pos).Cast<TYtOutTable>();
+ }
+
+ static TVector<TYtOutTable> ConvertOutTablesWithSortAware(TExprNode::TPtr& lambda, bool& ordered, TPositionHandle pos,
+ const TTypeAnnotationNode* outItemType, TExprContext& ctx, const TYtState::TPtr& state, const TConstraintSet& constraints) {
+ TVector<TYtOutTable> outTables;
+ if (outItemType->GetKind() == ETypeAnnotationKind::Variant) {
+ return ConvertMultiOutTablesWithSortAware(lambda, ordered, pos, outItemType, ctx, state, constraints);
+ }
+
+ return TVector<TYtOutTable>{ConvertSingleOutTableWithSortAware(lambda, ordered, pos, outItemType, ctx, state, constraints)};
+ }
+
+ static TExprBase WrapOp(TYtOutputOpBase op, TExprContext& ctx) {
+ if (op.Output().Size() > 1) {
+ return Build<TCoRight>(ctx, op.Pos())
+ .Input(op)
+ .Done();
+ }
+
+ return Build<TYtOutput>(ctx, op.Pos())
+ .Operation(op)
+ .OutIndex().Value("0").Build()
+ .Done();
+ }
+
+ static TCoLambda MapEmbedInputFieldsFilter(TCoLambda lambda, bool ordered, TCoAtomList fields, TExprContext& ctx) {
+ auto filter = [&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 index = 0;
+ for (const auto& x : fields) {
+ parent
+ .List(index++)
+ .Add(0, x.Ptr())
+ .Callable(1, "Member")
+ .Arg(0, "row")
+ .Add(1, x.Ptr())
+ .Seal()
+ .Seal();
+ }
+
+ return parent;
+ };
+
+ return TCoLambda(ctx.Builder(lambda.Pos())
+ .Lambda()
+ .Param("stream")
+ .Apply(lambda.Ptr()).With(0)
+ .Callable(ordered ? "OrderedFlatMap" : "FlatMap")
+ .Arg(0, "stream")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Just")
+ .Callable(0, "AsStruct")
+ .Do(filter)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build());
+ }
+
+
+ class TYtSortMembersCollection: public TSortMembersCollection {
+ public:
+ void BuildKeyFilters(TPositionHandle pos, size_t tableCount, size_t orGroupCount, TExprNode::TListType& result, TExprContext& ctx) {
+ TExprNode::TListType prefix;
+ for (size_t orGroup = 0; orGroup < orGroupCount; ++orGroup) {
+ BuildKeyFiltersImpl(pos, tableCount, orGroup, prefix, Members, result, ctx);
+ }
+ }
+
+ private:
+ void BuildKeyFiltersImpl(TPositionHandle pos, size_t tableCount, size_t orGroup, TExprNode::TListType& prefix,
+ const TMemberDescrMap& members, TExprNode::TListType& result, TExprContext& ctx)
+ {
+ for (auto& item: members) {
+ size_t prefixLen = prefix.size();
+
+ auto keyPredicateBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ auto iterRange = item.second->Ranges.equal_range(orGroup);
+ if (iterRange.first != iterRange.second) {
+ for (auto it = iterRange.first; it != iterRange.second; ++it) {
+ TString cmpOp;
+ TExprNode::TPtr value;
+ const TDataExprType* dataType = nullptr;
+ std::tie(cmpOp, value, dataType) = it->second;
+
+ if (!value) {
+ keyPredicateBuilder
+ .Add()
+ .Name()
+ .Value(cmpOp)
+ .Build()
+ .Value<TCoNull>()
+ .Build()
+ .Build();
+ } else {
+ keyPredicateBuilder
+ .Add()
+ .Name()
+ .Value(cmpOp)
+ .Build()
+ .Value(value)
+ .Build();
+ }
+ }
+
+ prefix.push_back(
+ Build<TCoNameValueTuple>(ctx, pos)
+ .Name()
+ .Value(item.first)
+ .Build()
+ .Value(keyPredicateBuilder.Done())
+ .Done().Ptr()
+ );
+ }
+
+ if (!item.second->Tables.empty()) {
+ YQL_ENSURE(!prefix.empty());
+ if (item.second->Tables.size() == tableCount) {
+ result.push_back(Build<TCoNameValueTuple>(ctx, pos)
+ .Name()
+ .Value(ToString(EYtSettingType::KeyFilter))
+ .Build()
+ .Value<TExprList>()
+ .Add<TExprList>()
+ .Add(prefix)
+ .Build()
+ .Build()
+ .Done().Ptr()
+ );
+ }
+ else {
+ for (auto tableNdx: item.second->Tables) {
+ result.push_back(Build<TCoNameValueTuple>(ctx, pos)
+ .Name()
+ .Value(ToString(EYtSettingType::KeyFilter))
+ .Build()
+ .Value<TExprList>()
+ .Add<TExprList>()
+ .Add(prefix)
+ .Build()
+ .Add<TCoAtom>()
+ .Value(ToString(tableNdx))
+ .Build()
+ .Build()
+ .Done().Ptr()
+ );
+ }
+ }
+ }
+ YQL_ENSURE(item.second->Tables.size() != tableCount || item.second->NextMembers.empty());
+ BuildKeyFiltersImpl(pos, tableCount, orGroup, prefix, item.second->NextMembers, result, ctx);
+
+ prefix.erase(prefix.begin() + prefixLen, prefix.end());
+ }
+ }
+ };
+
+ static bool IsAscending(const TExprNode& node) {
+ TMaybe<bool> ascending;
+ if (node.IsCallable("Bool")) {
+ ascending = IsTrue(node.Child(0)->Content());
+ }
+ return ascending.Defined() && *ascending;
+ }
+
+ static bool CollectSortSet(const TExprNode& sortNode, TSet<TVector<TStringBuf>>& sortSets) {
+ YQL_ENSURE(sortNode.IsCallable("Sort"));
+
+ auto directions = sortNode.ChildPtr(1);
+
+ auto lambdaArg = sortNode.Child(2)->Child(0)->Child(0);
+ auto lambdaBody = sortNode.Child(2)->ChildPtr(1);
+
+ TExprNode::TListType directionItems;
+ if (directions->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Tuple) {
+ directionItems = directions->ChildrenList();
+ } else {
+ directionItems.push_back(directions);
+ }
+
+ if (AnyOf(directionItems, [](const TExprNode::TPtr& direction) { return !IsAscending(*direction); })) {
+ return false;
+ }
+
+ TExprNode::TListType lambdaBodyItems;
+ if (directions->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Tuple) {
+ lambdaBodyItems = lambdaBody->ChildrenList();
+ } else {
+ lambdaBodyItems.push_back(lambdaBody);
+ }
+
+ TVector<TStringBuf> sortBy;
+ for (auto& item : lambdaBodyItems) {
+ if (!item->IsCallable("Member") || item->Child(0) != lambdaArg) {
+ return false;
+ }
+ YQL_ENSURE(item->Child(1)->IsAtom());
+ sortBy.push_back(item->Child(1)->Content());
+ }
+
+ return sortSets.insert(sortBy).second;
+ }
+
+ static TExprNode::TPtr CollectPreferredSortsForEquiJoinOutput(TExprBase join, const TExprNode::TPtr& options,
+ TExprContext& ctx, const TParentsMap& parentsMap)
+ {
+ auto parentsIt = parentsMap.find(join.Raw());
+ if (parentsIt == parentsMap.end()) {
+ return options;
+ }
+
+ TSet<TVector<TStringBuf>> sortSets = LoadJoinSortSets(*options);
+ size_t collected = 0;
+ for (auto& parent : parentsIt->second) {
+ if (parent->IsCallable("Sort") && CollectSortSet(*parent, sortSets)) {
+ ++collected;
+ }
+ }
+
+ if (!collected) {
+ return options;
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": Collected " << collected << " new sorts";
+
+ auto removedOptions = RemoveSetting(*options, "preferred_sort", ctx);
+ TExprNode::TListType optionsNodes = removedOptions->ChildrenList();
+ AppendEquiJoinSortSets(options->Pos(), sortSets, optionsNodes, ctx);
+
+ return ctx.NewList(options->Pos(), std::move(optionsNodes));
+ }
+
+ static bool CanExtraColumnBePulledIntoEquiJoin(const TTypeAnnotationNode* type) {
+ if (type->GetKind() == ETypeAnnotationKind::Optional) {
+ type = type->Cast<TOptionalExprType>()->GetItemType();
+ }
+
+ switch (type->GetKind()) {
+ case ETypeAnnotationKind::Data:
+ return IsFixedSizeData(type);
+ case ETypeAnnotationKind::Null:
+ case ETypeAnnotationKind::Void:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool IsYtOrPlainTablePropsDependent(NNodes::TExprBase input) {
+ bool found = false;
+ VisitExpr(input.Ref(), [&found](const TExprNode& n) {
+ found = found || TYtTablePropBase::Match(&n) || TCoTablePropBase::Match(&n);
+ return !found;
+ });
+ return found;
+ }
+
+ static bool IsLambdaSuitableForPullingIntoEquiJoin(const TCoFlatMapBase& flatMap, const TExprNode& label,
+ const THashMap<TStringBuf, THashSet<TStringBuf>>& tableKeysMap,
+ const TExprNode* extractedMembers)
+ {
+ if (!label.IsAtom()) {
+ return false;
+ }
+
+ auto inputSeqItem = SilentGetSequenceItemType(flatMap.Input().Ref(), false);
+ if (!inputSeqItem || !inputSeqItem->IsPersistable()) {
+ return false;
+ }
+
+ auto outputSeqItem = SilentGetSequenceItemType(flatMap.Ref(), false);
+ if (!outputSeqItem || !outputSeqItem->IsPersistable()) {
+ return false;
+ }
+
+ if (IsYtOrPlainTablePropsDependent(flatMap.Lambda().Body())) {
+ return false;
+ }
+
+ // allow only projective FlatMaps
+ if (!IsJustOrSingleAsList(flatMap.Lambda().Body().Ref())) {
+ return false;
+ }
+
+ // all input column should be either renamed, removed or passed as is
+ // all join keys should be passed as-is
+ // only fixed-size data type can be added
+ auto arg = flatMap.Lambda().Args().Arg(0).Raw();
+ auto outItem = flatMap.Lambda().Body().Ref().Child(0);
+
+ if (outItem->IsCallable("AsStruct")) {
+ size_t joinKeysPassed = 0;
+ auto it = tableKeysMap.find(label.Content());
+ const size_t joinKeysCount = (it == tableKeysMap.end()) ? 0 : it->second.size();
+
+ TMaybe<THashSet<TStringBuf>> filteredMemberSet;
+ if (extractedMembers) {
+ filteredMemberSet.ConstructInPlace();
+ for (auto member : extractedMembers->ChildrenList()) {
+ YQL_ENSURE(member->IsAtom());
+ filteredMemberSet->insert(member->Content());
+ }
+ }
+ for (auto& item : outItem->Children()) {
+ TStringBuf outMemberName = item->Child(0)->Content();
+ if (filteredMemberSet && !filteredMemberSet->contains(outMemberName)) {
+ // member will be filtered out by parent ExtractMembers
+ continue;
+ }
+ if (item->Child(1)->IsCallable("Member") && item->Child(1)->Child(0) == arg) {
+ TStringBuf inMemberName = item->Child(1)->Child(1)->Content();
+ bool isJoinKey = joinKeysCount && it->second.contains(outMemberName);
+ if (isJoinKey && inMemberName != outMemberName) {
+ return false;
+ }
+ joinKeysPassed += isJoinKey;
+ } else if (!CanExtraColumnBePulledIntoEquiJoin(item->Child(1)->GetTypeAnn())) {
+ return false;
+ }
+ }
+
+ YQL_ENSURE(joinKeysPassed <= joinKeysCount);
+ return joinKeysPassed == joinKeysCount;
+ } else {
+ return outItem == arg;
+ }
+ }
+
+ static bool CanBePulledIntoParentEquiJoin(const TCoFlatMapBase& flatMap, const TGetParents& getParents) {
+ const TParentsMap* parents = getParents();
+ YQL_ENSURE(parents);
+
+ auto equiJoinParents = CollectEquiJoinOnlyParents(flatMap, *parents);
+ if (equiJoinParents.empty()) {
+ return false;
+ }
+
+ bool suitable = true;
+ for (auto it = equiJoinParents.begin(); it != equiJoinParents.end() && suitable; ++it) {
+ TCoEquiJoin equiJoin(it->Node);
+ auto inputIndex = it->Index;
+
+ auto equiJoinTree = equiJoin.Arg(equiJoin.ArgCount() - 2);
+ THashMap<TStringBuf, THashSet<TStringBuf>> tableKeysMap =
+ CollectEquiJoinKeyColumnsByLabel(equiJoinTree.Ref());
+
+ auto input = equiJoin.Arg(inputIndex).Cast<TCoEquiJoinInput>();
+
+ suitable = suitable && IsLambdaSuitableForPullingIntoEquiJoin(flatMap, input.Scope().Ref(), tableKeysMap,
+ it->ExtractedMembers);
+ }
+
+ return suitable;
+ }
+
+ static TExprNode::TPtr BuildYtEquiJoinPremap(TExprBase list, TMaybeNode<TCoLambda> premapLambda, TExprContext& ctx) {
+ if (auto type = GetSequenceItemType(list, false, ctx)) {
+ if (!EnsurePersistableType(list.Pos(), *type, ctx)) {
+ return {};
+ }
+ if (premapLambda) {
+ return premapLambda.Cast().Ptr();
+ }
+ return Build<TCoVoid>(ctx, list.Pos()).Done().Ptr();
+ }
+ return {};
+ }
+
+ TMaybe<bool> CanFuseLambdas(const TCoLambda& innerLambda, const TCoLambda& outerLambda, TExprContext& ctx) const {
+ auto maxJobMemoryLimit = State_->Configuration->MaxExtraJobMemoryToFuseOperations.Get();
+ auto maxOperationFiles = State_->Configuration->MaxOperationFiles.Get().GetOrElse(DEFAULT_MAX_OPERATION_FILES);
+ TMap<TStringBuf, ui64> memUsage;
+
+ TExprNode::TPtr updatedBody = innerLambda.Body().Ptr();
+ if (maxJobMemoryLimit) {
+ auto status = UpdateTableContentMemoryUsage(innerLambda.Body().Ptr(), updatedBody, State_, ctx);
+ if (status.Level != TStatus::Ok) {
+ return {};
+ }
+ }
+ size_t innerFiles = 1; // jobstate. Take into account only once
+ ScanResourceUsage(*updatedBody, *State_->Configuration, State_->Types, maxJobMemoryLimit ? &memUsage : nullptr, nullptr, &innerFiles);
+
+ auto prevMemory = Accumulate(memUsage.begin(), memUsage.end(), 0ul,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ updatedBody = outerLambda.Body().Ptr();
+ if (maxJobMemoryLimit) {
+ auto status = UpdateTableContentMemoryUsage(outerLambda.Body().Ptr(), updatedBody, State_, ctx);
+ if (status.Level != TStatus::Ok) {
+ return {};
+ }
+ }
+ size_t outerFiles = 0;
+ ScanResourceUsage(*updatedBody, *State_->Configuration, State_->Types, maxJobMemoryLimit ? &memUsage : nullptr, nullptr, &outerFiles);
+
+ auto currMemory = Accumulate(memUsage.begin(), memUsage.end(), 0ul,
+ [](ui64 sum, const std::pair<const TStringBuf, ui64>& val) { return sum + val.second; });
+
+ if (maxJobMemoryLimit && currMemory != prevMemory && currMemory > *maxJobMemoryLimit) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Memory usage: innerLambda=" << prevMemory
+ << ", joinedLambda=" << currMemory << ", MaxJobMemoryLimit=" << *maxJobMemoryLimit;
+ return false;
+ }
+ if (innerFiles + outerFiles > maxOperationFiles) {
+ YQL_CLOG(DEBUG, ProviderYt) << "Files usage: innerLambda=" << innerFiles
+ << ", outerLambda=" << outerFiles << ", MaxOperationFiles=" << maxOperationFiles;
+ return false;
+ }
+
+ if (auto maxReplcationFactor = State_->Configuration->MaxReplicationFactorToFuseOperations.Get()) {
+ double replicationFactor1 = NCommon::GetDataReplicationFactor(innerLambda.Ref(), ctx);
+ double replicationFactor2 = NCommon::GetDataReplicationFactor(outerLambda.Ref(), ctx);
+ YQL_CLOG(DEBUG, ProviderYt) << "Replication factors: innerLambda=" << replicationFactor1
+ << ", outerLambda=" << replicationFactor2 << ", MaxReplicationFactorToFuseOperations=" << *maxReplcationFactor;
+
+ if (replicationFactor1 > 1.0 && replicationFactor2 > 1.0 && replicationFactor1 * replicationFactor2 > *maxReplcationFactor) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // label -> pair(<asc sort keys>, <inputs matched by keys>)
+ static THashMap<TStringBuf, std::pair<TVector<TStringBuf>, ui32>> CollectTableSortKeysUsage(const TYtState::TPtr& state, const TCoEquiJoin& equiJoin) {
+ THashMap<TStringBuf, std::pair<TVector<TStringBuf>, ui32>> tableSortKeys;
+ for (size_t i = 0; i + 2 < equiJoin.ArgCount(); ++i) {
+ auto joinInput = equiJoin.Arg(i).Cast<TCoEquiJoinInput>();
+ auto list = joinInput.List();
+ if (joinInput.Scope().Ref().IsAtom()) {
+ TVector<TStringBuf> sortKeys;
+ if (const auto sorted = list.Ref().GetConstraint<TSortedConstraintNode>()) {
+ for (const auto& key : sorted->GetContent()) {
+ if (key.second && !key.first.empty() && key.first.front().size() == 1U) {
+ sortKeys.emplace_back(key.first.front().front());
+ } else {
+ break;
+ }
+ }
+ }
+ tableSortKeys[joinInput.Scope().Ref().Content()] = std::make_pair(std::move(sortKeys), 0);
+ }
+ }
+
+ // Only Lookup and Merge strategies use a table sort
+ if (state->Configuration->JoinMergeTablesLimit.Get().GetOrElse(0)
+ || (state->Configuration->LookupJoinLimit.Get().GetOrElse(0) && state->Configuration->LookupJoinMaxRows.Get().GetOrElse(0))) {
+ TVector<const TExprNode*> joinTreeNodes;
+ joinTreeNodes.push_back(equiJoin.Arg(equiJoin.ArgCount() - 2).Raw());
+ while (!joinTreeNodes.empty()) {
+ const TExprNode* joinTree = joinTreeNodes.back();
+ joinTreeNodes.pop_back();
+
+ if (!joinTree->Child(1)->IsAtom()) {
+ joinTreeNodes.push_back(joinTree->Child(1));
+ }
+
+ if (!joinTree->Child(2)->IsAtom()) {
+ joinTreeNodes.push_back(joinTree->Child(2));
+ }
+
+ if (joinTree->Child(0)->Content() != "Cross") {
+ THashMap<TStringBuf, THashSet<TStringBuf>> tableJoinKeys;
+ for (auto keys: {joinTree->Child(3), joinTree->Child(4)}) {
+ for (ui32 i = 0; i < keys->ChildrenSize(); i += 2) {
+ auto tableName = keys->Child(i)->Content();
+ auto column = keys->Child(i + 1)->Content();
+ tableJoinKeys[tableName].insert(column);
+ }
+ }
+
+ for (auto& [label, joinKeys]: tableJoinKeys) {
+ if (auto sortKeys = tableSortKeys.FindPtr(label)) {
+ if (joinKeys.size() <= sortKeys->first.size()) {
+ bool matched = true;
+ for (size_t i = 0; i < joinKeys.size(); ++i) {
+ if (!joinKeys.contains(sortKeys->first[i])) {
+ matched = false;
+ break;
+ }
+ }
+ if (matched) {
+ ++sortKeys->second;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return tableSortKeys;
+ }
+
+ static TCoLambda FallbackLambdaInput(TCoLambda lambda, TExprContext& ctx) {
+ if (const auto narrow = FindNode(lambda.Ref().TailPtr(), [&](const TExprNode::TPtr& node) { return node->IsCallable("NarrowMap") && &lambda.Args().Arg(0).Ref() == &node->Head(); })) {
+ return TCoLambda(ctx.DeepCopyLambda(lambda.Ref(), ctx.ReplaceNodes(lambda.Ref().TailPtr(), {{narrow.Get(), narrow->HeadPtr()}})));
+ }
+
+ return lambda;
+ }
+
+ static TCoLambda FallbackLambdaOutput(TCoLambda lambda, TExprContext& ctx) {
+ if (auto body = lambda.Ref().TailPtr(); body->IsCallable("ExpandMap")) {
+ return TCoLambda(ctx.DeepCopyLambda(lambda.Ref(), body->HeadPtr()));
+ }
+
+ return lambda;
+ }
+
+ TCoLambda MakeJobLambdaNoArg(TExprBase content, TExprContext& ctx) const {
+ if (State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ content = Build<TCoToFlow>(ctx, content.Pos()).Input(content).Done();
+ } else {
+ content = Build<TCoToStream>(ctx, content.Pos()).Input(content).Done();
+ }
+
+ return Build<TCoLambda>(ctx, content.Pos()).Args({}).Body(content).Done();
+ }
+
+ template<bool WithList>
+ TCoLambda MakeJobLambda(TCoLambda lambda, bool useFlow, TExprContext& ctx) const {
+ if (useFlow) {
+ if constexpr (WithList) {
+ return Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"flow"})
+ .Body<TCoToFlow>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With<TCoForwardList>(0)
+ .Stream("flow")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ } else {
+ return Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"flow"})
+ .Body<TCoToFlow>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With<TCoFromFlow>(0)
+ .Input("flow")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+ } else {
+ if constexpr (WithList) {
+ return Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TCoToStream>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With<TCoForwardList>(0)
+ .Stream("stream")
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ } else {
+ return Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TCoToStream>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With(0, "stream")
+ .Build()
+ .Build()
+ .Done();
+ }
+ }
+ }
+private:
+ TMaybeNode<TExprBase> Mux(TExprBase node, TExprContext& ctx) const {
+ auto mux = node.Cast<TCoMux>();
+ const TTypeAnnotationNode* muxItemTypeNode = GetSeqItemType(mux.Ref().GetTypeAnn());
+ if (!muxItemTypeNode) {
+ return node;
+ }
+ auto muxItemType = muxItemTypeNode->Cast<TVariantExprType>();
+ if (muxItemType->GetUnderlyingType()->GetKind() != ETypeAnnotationKind::Tuple) {
+ return node;
+ }
+
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ bool allAreTables = true;
+ bool hasTables = false;
+ bool allAreTableContents = true;
+ bool hasContents = false;
+ TString resultCluster;
+ TMaybeNode<TYtDSource> dataSource;
+ for (auto child: mux.Input().Cast<TExprList>()) {
+ bool isTable = IsYtProviderInput(child);
+ bool isContent = child.Maybe<TYtTableContent>().IsValid();
+ if (!isTable && !isContent) {
+ // Don't match foreign provider input
+ if (child.Maybe<TCoRight>()) {
+ return node;
+ }
+ } else {
+ if (!dataSource) {
+ dataSource = GetDataSource(child, ctx);
+ }
+
+ if (!resultCluster) {
+ resultCluster = TString{dataSource.Cast().Cluster().Value()};
+ }
+ else if (resultCluster != dataSource.Cast().Cluster().Value()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder()
+ << "Different source clusters in Mux: " << resultCluster
+ << " and " << dataSource.Cast().Cluster().Value()));
+ return {};
+ }
+ }
+ allAreTables = allAreTables && isTable;
+ hasTables = hasTables || isTable;
+ allAreTableContents = allAreTableContents && isContent;
+ hasContents = hasContents || isContent;
+ }
+
+ if (!hasTables && !hasContents) {
+ return node;
+ }
+
+ auto dataSink = TYtDSink(ctx.RenameNode(dataSource.Ref(), "DataSink"));
+ if (allAreTables || allAreTableContents) {
+ TVector<TExprBase> worlds;
+ TVector<TYtSection> sections;
+ for (auto child: mux.Input().Cast<TExprList>()) {
+ auto read = child.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ if (!read) {
+ read = child.Maybe<TYtTableContent>().Input().Maybe<TYtReadTable>();
+ }
+ if (read) {
+ YQL_ENSURE(read.Cast().Input().Size() == 1);
+ auto section = read.Cast().Input().Item(0);
+ sections.push_back(section);
+ if (allAreTables) {
+ worlds.push_back(GetWorld(child, {}, ctx));
+ }
+ } else {
+ YQL_ENSURE(child.Maybe<TYtOutput>(), "Unknown Mux element: " << child.Ref().Content());
+ sections.push_back(
+ Build<TYtSection>(ctx, child.Pos())
+ .Paths()
+ .Add()
+ .Table(child) // child is TYtOutput
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Done()
+ );
+ }
+ }
+
+ auto world = worlds.empty()
+ ? TExprBase(ctx.NewWorld(mux.Pos()))
+ : worlds.size() == 1
+ ? worlds.front()
+ : Build<TCoSync>(ctx, mux.Pos()).Add(worlds).Done();
+
+ auto resRead = Build<TYtReadTable>(ctx, mux.Pos())
+ .World(world)
+ .DataSource(dataSource.Cast())
+ .Input()
+ .Add(sections)
+ .Build()
+ .Done();
+
+ return allAreTables
+ ? Build<TCoRight>(ctx, mux.Pos())
+ .Input(resRead)
+ .Done().Cast<TExprBase>()
+ : Build<TYtTableContent>(ctx, mux.Pos())
+ .Input(resRead)
+ .Settings().Build()
+ .Done().Cast<TExprBase>();
+ }
+
+ if (!hasTables) {
+ return node;
+ }
+
+ TVector<TExprBase> newMuxParts;
+ for (auto child: mux.Input().Cast<TExprList>()) {
+ if (!IsYtProviderInput(child)) {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(child.Ref(), syncList, resultCluster, true, false)) {
+ return node;
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(child, false, ctx)) {
+ if (!EnsurePersistableType(child.Pos(), *type, ctx)) {
+ return {};
+ }
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ TYtOutTableInfo outTable(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ auto content = child;
+ if (auto sorted = child.Ref().GetConstraint<TSortedConstraintNode>()) {
+ TKeySelectorBuilder builder(child.Pos(), ctx, useNativeDescSort, outItemType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+
+ if (builder.NeedMap()) {
+ content = Build<TExprApplier>(ctx, child.Pos())
+ .Apply(TCoLambda(builder.MakeRemapLambda(true)))
+ .With(0, content)
+ .Done();
+ }
+
+ } else if (auto unordered = content.Maybe<TCoUnorderedBase>()) {
+ content = unordered.Cast().Input();
+ }
+ outTable.RowSpec->SetConstraints(child.Ref().GetConstraintSet());
+ outTable.SetUnique(child.Ref().GetConstraint<TDistinctConstraintNode>(), child.Pos(), ctx);
+
+ auto cleanup = CleanupWorld(content, ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ newMuxParts.push_back(
+ Build<TYtOutput>(ctx, child.Pos())
+ .Operation<TYtFill>()
+ .World(ApplySyncListToWorld(ctx.NewWorld(child.Pos()), syncList, ctx))
+ .DataSink(dataSink)
+ .Content(MakeJobLambdaNoArg(cleanup.Cast(), ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, child.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(child.Pos(), *State_, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done()
+ );
+ }
+ else {
+ newMuxParts.push_back(child);
+ }
+ }
+
+ return Build<TCoMux>(ctx, mux.Pos())
+ .Input<TExprList>()
+ .Add(newMuxParts)
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> Sort(TExprBase node, TExprContext& ctx) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto sort = node.Cast<TCoSort>();
+ if (!IsYtProviderInput(sort.Input())) {
+ return node;
+ }
+
+ auto sortDirections = sort.SortDirections();
+ if (!IsConstExpSortDirections(sortDirections)) {
+ return node;
+ }
+
+ auto keySelectorLambda = sort.KeySelectorLambda();
+ auto cluster = TString{GetClusterName(sort.Input())};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(keySelectorLambda.Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ const TStructExprType* outType = nullptr;
+ if (auto type = GetSequenceItemType(node, false, ctx)) {
+ outType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ TVector<TYtPathInfo::TPtr> inputInfos = GetInputPaths(sort.Input());
+
+ TMaybe<NYT::TNode> firstNativeType;
+ if (!inputInfos.empty()) {
+ firstNativeType = inputInfos.front()->GetNativeYtType();
+ }
+ auto maybeReadSettings = sort.Input().Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Settings();
+ const ui64 nativeTypeFlags = State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES)
+ ? GetNativeYtTypeFlags(*outType)
+ : 0ul;
+ const bool needMap = (maybeReadSettings && NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::SysColumns))
+ || AnyOf(inputInfos, [nativeTypeFlags, firstNativeType] (const TYtPathInfo::TPtr& path) {
+ return path->RequiresRemap()
+ || nativeTypeFlags != path->GetNativeYtTypeFlags()
+ || firstNativeType != path->GetNativeYtType();
+ });
+
+ bool useExplicitColumns = AnyOf(inputInfos, [] (const TYtPathInfo::TPtr& path) {
+ return !path->Table->IsTemp || (path->Table->RowSpec && path->Table->RowSpec->HasAuxColumns());
+ });
+
+ const bool needMerge = maybeReadSettings && NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::Sample);
+
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+
+ TKeySelectorBuilder builder(node.Pos(), ctx, useNativeDescSort, outType);
+ builder.ProcessKeySelector(keySelectorLambda.Ptr(), sortDirections.Ptr());
+
+ TYtOutTableInfo sortOut(outType, nativeTypeFlags);
+ builder.FillRowSpecSort(*sortOut.RowSpec);
+ sortOut.SetUnique(sort.Ref().GetConstraint<TDistinctConstraintNode>(), node.Pos(), ctx);
+
+ TExprBase sortInput = sort.Input();
+ TExprBase world = TExprBase(ApplySyncListToWorld(GetWorld(sortInput, {}, ctx).Ptr(), syncList, ctx));
+ bool unordered = ctx.IsConstraintEnabled<TSortedConstraintNode>();
+ if (needMap || builder.NeedMap()) {
+ auto mapper = builder.MakeRemapLambda();
+
+ auto mapperClean = CleanupWorld(TCoLambda(mapper), ctx);
+ if (!mapperClean) {
+ return {};
+ }
+
+ TYtOutTableInfo mapOut(builder.MakeRemapType(), nativeTypeFlags);
+ mapOut.SetUnique(sort.Ref().GetConstraint<TDistinctConstraintNode>(), node.Pos(), ctx);
+
+ sortInput = Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMap>()
+ .World(world)
+ .DataSink(GetDataSink(sort.Input(), ctx))
+ .Input(ConvertInputTable(sort.Input(), ctx, TConvertInputOpts().MakeUnordered(unordered)))
+ .Output()
+ .Add(mapOut.ToExprNode(ctx, node.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(node.Pos(), *State_, ctx))
+ .Mapper(mapperClean.Cast())
+ .Build()
+ .OutIndex()
+ .Value(0U)
+ .Build()
+ .Done();
+ world = TExprBase(ctx.NewWorld(node.Pos()));
+ unordered = false;
+ }
+ else if (needMerge) {
+ TYtOutTableInfo mergeOut(outType, nativeTypeFlags);
+ mergeOut.SetUnique(sort.Ref().GetConstraint<TDistinctConstraintNode>(), node.Pos(), ctx);
+ if (firstNativeType) {
+ mergeOut.RowSpec->CopyTypeOrders(*firstNativeType);
+ sortOut.RowSpec->CopyTypeOrders(*firstNativeType);
+ }
+
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*mergeOut.RowSpec, node.Pos(), ctx);
+ useExplicitColumns = false;
+ }
+
+ sortInput = Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMerge>()
+ .World(world)
+ .DataSink(GetDataSink(sort.Input(), ctx))
+ .Input(ConvertInputTable(sort.Input(), ctx, opts.MakeUnordered(unordered)))
+ .Output()
+ .Add(mergeOut.ToExprNode(ctx, node.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ForceTransform))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex()
+ .Value(0U)
+ .Build()
+ .Done();
+ world = TExprBase(ctx.NewWorld(node.Pos()));
+ unordered = false;
+ } else if (firstNativeType) {
+ sortOut.RowSpec->CopyTypeOrders(*firstNativeType);
+ }
+
+ bool canUseMerge = !needMap && !needMerge;
+ if (auto maxTablesForSortedMerge = State_->Configuration->MaxInputTablesForSortedMerge.Get()) {
+ if (inputInfos.size() > *maxTablesForSortedMerge) {
+ canUseMerge = false;
+ }
+ }
+
+ if (canUseMerge) {
+ TYqlRowSpecInfo commonSorted = *sortOut.RowSpec;
+ for (auto& pathInfo: inputInfos) {
+ commonSorted.MakeCommonSortness(*pathInfo->Table->RowSpec);
+ }
+ // input is sorted at least as strictly as output
+ if (!sortOut.RowSpec->CompareSortness(commonSorted)) {
+ canUseMerge = false;
+ }
+ }
+
+ sortOut.RowSpec->SetConstraints(sort.Ref().GetConstraintSet());
+
+ if (canUseMerge) {
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*sortOut.RowSpec, node.Pos(), ctx);
+ }
+
+ return Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMerge>()
+ .World(world)
+ .DataSink(GetDataSink(sortInput, ctx))
+ .Input(ConvertInputTable(sortInput, ctx, opts.ClearUnordered()))
+ .Output()
+ .Add(sortOut.ToExprNode(ctx, node.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeepSorted))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*sortOut.RowSpec, node.Pos(), ctx);
+ }
+
+ return Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtSort>()
+ .World(world)
+ .DataSink(GetDataSink(sortInput, ctx))
+ .Input(ConvertInputTable(sortInput, ctx, opts.MakeUnordered(unordered)))
+ .Output()
+ .Add(sortOut.ToExprNode(ctx, node.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings().Build()
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> YtSortOverAlreadySorted(TExprBase node, TExprContext& ctx) const {
+ auto sort = node.Cast<TYtSort>();
+
+ if (auto maxTablesForSortedMerge = State_->Configuration->MaxInputTablesForSortedMerge.Get()) {
+ if (sort.Input().Item(0).Paths().Size() > *maxTablesForSortedMerge) {
+ return node;
+ }
+ }
+
+ const TYqlRowSpecInfo outRowSpec(sort.Output().Item(0).RowSpec());
+
+ TYqlRowSpecInfo commonSorted = outRowSpec;
+ auto section = sort.Input().Item(0);
+ for (auto path: section.Paths()) {
+ commonSorted.MakeCommonSortness(*TYtTableBaseInfo::GetRowSpec(path.Table()));
+ }
+
+ if (outRowSpec.CompareSortness(commonSorted)) {
+ // input is sorted at least as strictly as output
+ auto res = ctx.RenameNode(sort.Ref(), TYtMerge::CallableName());
+ res = ctx.ChangeChild(*res, TYtMerge::idx_Settings,
+ Build<TCoNameValueTupleList>(ctx, sort.Pos())
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeepSorted))
+ .Build()
+ .Build()
+ .Done().Ptr()
+ );
+ section = ClearUnorderedSection(section, ctx);
+ if (section.Ptr() != sort.Input().Item(0).Ptr()) {
+ res = ctx.ChangeChild(*res, TYtMerge::idx_Input, Build<TYtSectionList>(ctx, sort.Input().Pos()).Add(section).Done().Ptr());
+ }
+ return TExprBase(res);
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> PartitionByKey(TExprBase node, TExprContext& ctx) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto partByKey = node.Cast<TCoPartitionByKeyBase>();
+
+ TExprBase input = partByKey.Input();
+ TCoLambda keySelectorLambda = partByKey.KeySelectorLambda();
+ TCoLambda handlerLambda = partByKey.ListHandlerLambda();
+
+ if (!IsYtProviderInput(input, true)) {
+ return node;
+ }
+
+ auto outItemType = SilentGetSequenceItemType(handlerLambda.Body().Ref(), true);
+ if (!outItemType || !outItemType->IsPersistable()) {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(input)};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(keySelectorLambda.Ref(), syncList, cluster, true, false)
+ || !IsYtCompleteIsolatedLambda(handlerLambda.Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ const auto inputItemType = GetSequenceItemType(input, true, ctx);
+ if (!inputItemType) {
+ return {};
+ }
+ const bool multiInput = (inputItemType->GetKind() == ETypeAnnotationKind::Variant);
+ bool useSystemColumns = State_->Configuration->UseSystemColumns.Get().GetOrElse(DEFAULT_USE_SYS_COLUMNS);
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+
+ TVector<TYtPathInfo::TPtr> inputPaths = GetInputPaths(input);
+ bool needMap = AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) {
+ return path->RequiresRemap();
+ });
+
+ bool forceMapper = false;
+ if (auto maybeRead = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ forceMapper = AnyOf(maybeRead.Cast().Input(), [] (const TYtSection& section) {
+ return NYql::HasSetting(section.Settings().Ref(), EYtSettingType::SysColumns);
+ });
+ }
+
+ if (!multiInput) {
+ const ui64 nativeTypeFlags = State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES)
+ ? GetNativeYtTypeFlags(*inputItemType->Cast<TStructExprType>())
+ : 0ul;
+
+ TMaybe<NYT::TNode> firstNativeType;
+ if (!inputPaths.empty()) {
+ firstNativeType = inputPaths.front()->GetNativeYtType();
+ }
+
+ forceMapper = forceMapper || AnyOf(inputPaths, [nativeTypeFlags, firstNativeType] (const TYtPathInfo::TPtr& path) {
+ return nativeTypeFlags != path->GetNativeYtTypeFlags()
+ || firstNativeType != path->GetNativeYtType();
+ });
+ }
+
+ bool useExplicitColumns = AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) {
+ return !path->Table->IsTemp
+ || (path->Table->RowSpec && path->Table->RowSpec->HasAuxColumns());
+ });
+
+ TKeySelectorBuilder builder(node.Pos(), ctx, useNativeDescSort, inputItemType);
+ builder.ProcessKeySelector(keySelectorLambda.Ptr(), {});
+
+ TVector<std::pair<TString, bool>> reduceByColumns = builder.ForeignSortColumns();
+ TVector<std::pair<TString, bool>> sortByColumns;
+
+ if (!partByKey.SortDirections().Maybe<TCoVoid>()) {
+ TExprBase sortDirections = partByKey.SortDirections();
+ if (!IsConstExpSortDirections(sortDirections)) {
+ return node;
+ }
+
+ TCoLambda sortKeySelectorLambda = partByKey.SortKeySelectorLambda().Cast<TCoLambda>();
+ if (!IsYtCompleteIsolatedLambda(sortKeySelectorLambda.Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ builder.ProcessKeySelector(sortKeySelectorLambda.Ptr(), sortDirections.Ptr());
+ sortByColumns = builder.ForeignSortColumns();
+ }
+
+ TExprBase mapper = Build<TCoVoid>(ctx, node.Pos()).Done();
+ needMap = needMap || builder.NeedMap();
+
+ bool hasInputSampling = false;
+ // Read sampling settings from the first section only, because all sections should have the same sampling settings
+ if (auto maybeReadSettings = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Settings()) {
+ hasInputSampling = NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::Sample);
+ }
+
+ bool canUseReduce = !needMap;
+ if (canUseReduce) {
+ TVector<std::pair<TString, bool>> sortPrefix;
+ for (auto& pathInfo: inputPaths) {
+ if (pathInfo->Table->IsUnordered
+ || !pathInfo->Table->RowSpec
+ || !pathInfo->Table->RowSpec->IsSorted()
+ || pathInfo->Table->RowSpec->SortedBy.size() < builder.Columns().size())
+ {
+ canUseReduce = false;
+ break;
+ }
+ if (sortPrefix.empty()) {
+ // reduceBy columns can be in any order, with any ascending
+ THashMap<TString, bool> partColumnSet(reduceByColumns.begin(), reduceByColumns.end());
+ auto sortedBy = pathInfo->Table->RowSpec->GetForeignSort();
+ const bool equalReduceByPrefix = AllOf(sortedBy.begin(), sortedBy.begin() + reduceByColumns.size(),
+ [&partColumnSet](const std::pair<TString, bool>& c) {
+ return partColumnSet.contains(c.first);
+ });
+
+ // sortBy suffix should exactly match
+ const bool equalSortBySuffix = equalReduceByPrefix && (sortByColumns.empty()
+ || std::equal(sortByColumns.begin() + reduceByColumns.size(), sortByColumns.end(), sortedBy.begin() + reduceByColumns.size()));
+
+ if (equalSortBySuffix) {
+ sortPrefix.assign(sortedBy.begin(), sortedBy.begin() + builder.Columns().size());
+ // All other tables should have the same sort order as the first one
+ } else {
+ canUseReduce = false;
+ break;
+ }
+ } else {
+ auto sortedBy = pathInfo->Table->RowSpec->GetForeignSort();
+ if (!std::equal(sortPrefix.begin(), sortPrefix.end(), sortedBy.begin())) {
+ canUseReduce = false;
+ break;
+ }
+ }
+ }
+ if (canUseReduce) {
+ const auto reduceBySize = reduceByColumns.size();
+ reduceByColumns.assign(sortPrefix.begin(), sortPrefix.begin() + reduceBySize);
+ if (!sortByColumns.empty()) {
+ sortByColumns = std::move(sortPrefix);
+ }
+ }
+ }
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, node.Pos());
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::ReduceBy)).Build()
+ .Value(TExprBase(ToColumnPairList(reduceByColumns, node.Pos(), ctx)))
+ .Build();
+
+ if (!sortByColumns.empty()) {
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::SortBy)).Build()
+ .Value(TExprBase(ToColumnPairList(sortByColumns, node.Pos(), ctx)))
+ .Build();
+ }
+ if (!canUseReduce && multiInput) {
+ needMap = true; // YtMapReduce with empty mapper doesn't support table indices
+ }
+
+ bool useReduceFlow = State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW);
+ bool useMapFlow = useReduceFlow;
+
+ const bool newPartsByKeys = bool(partByKey.Maybe<TCoPartitionsByKeys>());
+
+ // Convert reduce output to stream
+ if (newPartsByKeys) {
+ if (useSystemColumns) {
+ TNodeSet nodesToOptimize;
+ TProcessedNodesSet processedNodes;
+ auto arg = handlerLambda.Args().Arg(0).Ptr();
+ VisitExpr(handlerLambda.Body().Ptr(), [&nodesToOptimize, &processedNodes, arg](const TExprNode::TPtr& node) {
+ if (TMaybeNode<TCoChopper>(node).GroupSwitch().Body().Maybe<TCoIsKeySwitch>()) {
+ if (IsDepended(node->Head(), *arg)) {
+ nodesToOptimize.insert(node.Get());
+ }
+ }
+ else if (TMaybeNode<TCoCondense1>(node).SwitchHandler().Body().Maybe<TCoIsKeySwitch>()) {
+ if (IsDepended(node->Head(), *arg)) {
+ nodesToOptimize.insert(node.Get());
+ }
+ }
+ else if (TMaybeNode<TCoCondense>(node).SwitchHandler().Body().Maybe<TCoIsKeySwitch>()) {
+ if (IsDepended(node->Head(), *arg)) {
+ nodesToOptimize.insert(node.Get());
+ }
+ }
+ else if (TYtOutput::Match(node.Get())) {
+ // Stop traversing dependent operations
+ processedNodes.insert(node->UniqueId());
+ return false;
+ }
+ return true;
+ });
+
+ if (!nodesToOptimize.empty()) {
+ TOptimizeExprSettings settings(State_->Types);
+ settings.ProcessedNodes = &processedNodes; // Prevent optimizer to go deeper than current operation
+ TExprNode::TPtr newBody = handlerLambda.Body().Ptr();
+ auto status = OptimizeExpr(newBody, newBody, [&](const TExprNode::TPtr& node, TExprContext& ctx) -> TExprNode::TPtr {
+ if (nodesToOptimize.find(node.Get()) == nodesToOptimize.end()) {
+ return node;
+ }
+
+ if (auto maybeChopper = TMaybeNode<TCoChopper>(node)) {
+ auto chopper = maybeChopper.Cast();
+
+ auto chopperSwitch = ctx.Builder(chopper.GroupSwitch().Pos())
+ .Lambda()
+ .Param("key")
+ .Param("item")
+ .Callable("SqlExtractKey")
+ .Arg(0, "item")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr chopperHandler;
+ TExprNode::TPtr chopperKeyExtract;
+ if (!canUseReduce && multiInput) {
+ chopperKeyExtract = ctx.Builder(chopper.Handler().Pos())
+ .Lambda()
+ .Param("item")
+ .Apply(chopper.KeyExtractor().Ptr())
+ .With(0)
+ .Callable("Member")
+ .Arg(0, "item")
+ .Atom(1, "_yql_original_row", TNodeFlags::Default)
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+
+ chopperHandler = ctx.Builder(chopper.Handler().Pos())
+ .Lambda()
+ .Param("key")
+ .Param("group")
+ .Apply(chopper.Handler().Ptr())
+ .With(0, "key")
+ .With(1)
+ .Callable("Map")
+ .Arg(0, "group")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, "_yql_original_row", TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ chopperKeyExtract = ctx.Builder(chopper.Handler().Pos())
+ .Lambda()
+ .Param("item")
+ .Apply(chopper.KeyExtractor().Ptr())
+ .With(0)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "item")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+
+ chopperHandler = ctx.Builder(chopper.Handler().Pos())
+ .Lambda()
+ .Param("key")
+ .Param("group")
+ .Apply(chopper.Handler().Ptr())
+ .With(0, "key")
+ .With(1)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "group")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ TNodeOnNodeOwnedMap deepClones;
+ return Build<TCoChopper>(ctx, chopper.Pos())
+ .Input(chopper.Input())
+ .KeyExtractor(chopperKeyExtract)
+ .GroupSwitch(chopperSwitch)
+ .Handler(chopperHandler)
+ .Done().Ptr();
+ }
+ else if (auto maybeCondense = TMaybeNode<TCoCondense1>(node)) {
+ auto condense = maybeCondense.Cast();
+ auto switchHandler = ctx.Builder(condense.SwitchHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Callable("SqlExtractKey")
+ .Arg(0, "item")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr initHandler;
+ TExprNode::TPtr updateHandler;
+
+ if (!canUseReduce && multiInput) {
+ initHandler = ctx.Builder(condense.InitHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Apply(condense.InitHandler().Ptr())
+ .With(0)
+ .Callable("Member")
+ .Arg(0, "item")
+ .Atom(1, "_yql_original_row", TNodeFlags::Default)
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+
+ updateHandler = ctx.Builder(condense.UpdateHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Apply(condense.UpdateHandler().Ptr())
+ .With(0)
+ .Callable("Member")
+ .Arg(0, "item")
+ .Atom(1, "_yql_original_row", TNodeFlags::Default)
+ .Seal()
+ .Done()
+ .With(1, "state")
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ initHandler = ctx.Builder(condense.InitHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Apply(condense.InitHandler().Ptr())
+ .With(0)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "item")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+
+ updateHandler = ctx.Builder(condense.UpdateHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Apply(condense.UpdateHandler().Ptr())
+ .With(0)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "item")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .With(1, "state")
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ return Build<TCoCondense1>(ctx, condense.Pos())
+ .Input(condense.Input())
+ .InitHandler(initHandler)
+ .SwitchHandler(switchHandler)
+ .UpdateHandler(updateHandler)
+ .Done().Ptr();
+ }
+ else if (auto maybeCondense = TMaybeNode<TCoCondense>(node)) {
+ auto condense = maybeCondense.Cast();
+
+ auto switchHandler = ctx.Builder(condense.SwitchHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Callable("SqlExtractKey")
+ .Arg(0, "item")
+ .Lambda(1)
+ .Param("row")
+ .Callable("Member")
+ .Arg(0, "row")
+ .Atom(1, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ TExprNode::TPtr updateHandler;
+ if (!canUseReduce && multiInput) {
+ updateHandler = ctx.Builder(condense.UpdateHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Apply(condense.UpdateHandler().Ptr())
+ .With(0)
+ .Callable("Member")
+ .Arg(0, "item")
+ .Atom(1, "_yql_original_row", TNodeFlags::Default)
+ .Seal()
+ .Done()
+ .With(1, "state")
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ updateHandler = ctx.Builder(condense.UpdateHandler().Pos())
+ .Lambda()
+ .Param("item")
+ .Param("state")
+ .Apply(condense.UpdateHandler().Ptr())
+ .With(0)
+ .Callable("RemovePrefixMembers")
+ .Arg(0, "item")
+ .List(1)
+ .Atom(0, YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Seal()
+ .Seal()
+ .Done()
+ .With(1, "state")
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ return Build<TCoCondense>(ctx, condense.Pos())
+ .Input(condense.Input())
+ .State(condense.State())
+ .SwitchHandler(switchHandler)
+ .UpdateHandler(updateHandler)
+ .Done().Ptr();
+ }
+
+ return node;
+ }, ctx, settings);
+
+ if (status.Level == TStatus::Error) {
+ return {};
+ }
+
+ if (status.Level == TStatus::Ok) {
+ useSystemColumns = false;
+ }
+ else {
+ handlerLambda = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({TStringBuf("stream")})
+ .Body<TExprApplier>()
+ .Apply(TExprBase(newBody))
+ .With(handlerLambda.Args().Arg(0), TStringBuf("stream"))
+ .Build()
+ .Done();
+ }
+ }
+ else {
+ useSystemColumns = false;
+ }
+ }
+
+ if (!useSystemColumns) {
+ auto preReduceLambda = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"stream"})
+ .Body("stream")
+ .Done();
+
+ if (!canUseReduce && multiInput) {
+ preReduceLambda = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"stream"})
+ .Body<TCoFlatMap>()
+ .Input("stream")
+ .Lambda()
+ .Args({"item"})
+ .Body<TCoJust>()
+ .Input<TCoMember>()
+ .Struct("item")
+ .Name()
+ .Value("_yql_original_row")
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ handlerLambda = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(handlerLambda)
+ .With<TExprApplier>(0)
+ .Apply(preReduceLambda)
+ .With(0, "stream")
+ .Build()
+ .Build()
+ .Done();
+ }
+ }
+ else {
+ TExprNode::TPtr groupSwitch;
+ TExprNode::TPtr keyExtractor;
+ TExprNode::TPtr handler;
+
+ if (useSystemColumns) {
+ groupSwitch = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"key", "item"})
+ .Body<TCoSqlExtractKey>()
+ .Item("item")
+ .Extractor()
+ .Args({"row"})
+ .Body<TCoMember>()
+ .Struct("row")
+ .Name()
+ .Value(YqlSysColumnKeySwitch, TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ } else {
+ groupSwitch = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"key", "item"})
+ .Body<TYtIsKeySwitch>()
+ .DependsOn()
+ .Input("item")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ if (!canUseReduce && multiInput) {
+ keyExtractor = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"item"})
+ .Body<TExprApplier>()
+ .Apply(keySelectorLambda)
+ .With<TCoMember>(0)
+ .Struct("item")
+ .Name()
+ .Value("_yql_original_row")
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+
+ handler = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"item"})
+ .Body<TCoMember>()
+ .Struct("item")
+ .Name()
+ .Value("_yql_original_row")
+ .Build()
+ .Build()
+ .Done().Ptr();
+ } else {
+ keyExtractor = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"item"})
+ .Body<TExprApplier>()
+ .Apply(keySelectorLambda)
+ .With(0, "item")
+ .Build()
+ .Done().Ptr();
+
+ handler = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"item"})
+ .Body<TCoRemovePrefixMembers>()
+ .Input("item")
+ .Prefixes()
+ .Add()
+ .Value(YqlSysColumnKeySwitch)
+ .Build()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ handlerLambda = Build<TCoLambda>(ctx, handlerLambda.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(handlerLambda)
+ .With<TCoGroupingCore>(0)
+ .Input("stream")
+ .GroupSwitch(groupSwitch)
+ .KeyExtractor(keyExtractor)
+ .ConvertHandler(handler)
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ auto handlerLambdaCleanup = CleanupWorld(handlerLambda, ctx);
+ if (!handlerLambdaCleanup) {
+ return {};
+ }
+
+ const auto mapOutputType = needMap ? builder.MakeRemapType() : nullptr;
+ auto reduceInputType = mapOutputType ? mapOutputType : inputItemType;
+ if (useSystemColumns) {
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::KeySwitch)).Build()
+ .Build();
+
+ if (ETypeAnnotationKind::Struct == reduceInputType->GetKind()) {
+ auto items = reduceInputType->Cast<TStructExprType>()->GetItems();
+ items.emplace_back(ctx.MakeType<TItemExprType>(YqlSysColumnKeySwitch, ctx.MakeType<TDataExprType>(EDataSlot::Bool)));
+ reduceInputType = ctx.MakeType<TStructExprType>(items);
+ }
+ }
+
+ TVector<TString> filterColumns;
+ if (needMap) {
+ if (auto maybeMapper = CleanupWorld(TCoLambda(builder.MakeRemapLambda()), ctx)) {
+ mapper = maybeMapper.Cast();
+ } else {
+ return {};
+ }
+
+ if (builder.NeedMap() || multiInput) {
+ if (multiInput) {
+ filterColumns.emplace_back("_yql_original_row");
+ }
+ else {
+ for (auto& item: inputItemType->Cast<TStructExprType>()->GetItems()) {
+ filterColumns.emplace_back(item->GetName());
+ }
+ }
+
+ if (ETypeAnnotationKind::Struct == reduceInputType->GetKind()) {
+ const std::unordered_set<std::string_view> set(filterColumns.cbegin(), filterColumns.cend());
+ TVector<const TItemExprType*> items;
+ items.reserve(set.size());
+ for (const auto& item : reduceInputType->Cast<TStructExprType>()->GetItems())
+ if (const auto& name = item->GetName(); YqlSysColumnKeySwitch == name || set.cend() != set.find(name))
+ items.emplace_back(item);
+ reduceInputType = ctx.MakeType<TStructExprType>(items);
+ }
+ }
+ }
+
+ auto reducer = newPartsByKeys ?
+ MakeJobLambda<true>(handlerLambdaCleanup.Cast(), useReduceFlow, ctx):
+ MakeJobLambda<false>(handlerLambdaCleanup.Cast(), useReduceFlow, ctx);
+
+ if (useReduceFlow) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ if (canUseReduce) {
+ auto reduce = Build<TYtReduce>(ctx, node.Pos())
+ .World(ApplySyncListToWorld(GetWorld(input, {}, ctx).Ptr(), syncList, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx))
+ .Output()
+ .Add(ConvertOutTables(node.Pos(), outItemType, ctx, State_, &partByKey.Ref().GetConstraintSet()))
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Reducer(reducer)
+ .Done();
+ return WrapOp(reduce, ctx);
+ }
+
+ if (needMap && (builder.NeedMap() || multiInput)) {
+ settingsBuilder
+ .Add()
+ .Name().Value(ToString(EYtSettingType::ReduceFilterBy)).Build()
+ .Value(TExprBase(ToAtomList(filterColumns, node.Pos(), ctx)))
+ .Build();
+ }
+
+ bool unordered = ctx.IsConstraintEnabled<TSortedConstraintNode>();
+ TExprBase world = GetWorld(input, {}, ctx);
+ if (hasInputSampling) {
+
+ if (forceMapper && !needMap) {
+ mapper = Build<TCoLambda>(ctx, node.Pos()).Args({"stream"}).Body("stream").Done();
+ needMap = true;
+ }
+
+ if (needMap) {
+ input = Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMap>()
+ .World(world)
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx, TConvertInputOpts().MakeUnordered(unordered)))
+ .Output()
+ .Add(ConvertOutTables(node.Pos(), mapOutputType ? mapOutputType : inputItemType, ctx, State_))
+ .Build()
+ .Settings(GetFlowSettings(node.Pos(), *State_, ctx))
+ .Mapper(MakeJobLambda<false>(mapper.Cast<TCoLambda>(), useMapFlow, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+
+ mapper = Build<TCoVoid>(ctx, node.Pos()).Done();
+ needMap = false;
+ forceMapper = false;
+ }
+ else {
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*inputItemType->Cast<TStructExprType>(), node.Pos(), ctx);
+ }
+
+ input = Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMerge>()
+ .World(world)
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx, opts.MakeUnordered(unordered)))
+ .Output()
+ .Add(ConvertOutTables(node.Pos(), inputItemType, ctx, State_))
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::ForceTransform))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+ world = TExprBase(ctx.NewWorld(node.Pos()));
+ unordered = false;
+ }
+
+ if (needMap) {
+ if (multiInput) {
+ input = Build<TYtOutput>(ctx, node.Pos())
+ .Operation<TYtMap>()
+ .World(world)
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx, TConvertInputOpts().MakeUnordered(unordered)))
+ .Output()
+ .Add(ConvertOutTables(node.Pos(), mapOutputType, ctx, State_))
+ .Build()
+ .Settings(GetFlowSettings(node.Pos(), *State_, ctx))
+ .Mapper(MakeJobLambda<false>(mapper.Cast<TCoLambda>(), useMapFlow, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+
+ mapper = Build<TCoVoid>(ctx, node.Pos()).Done();
+ world = TExprBase(ctx.NewWorld(node.Pos()));
+ unordered = false;
+ } else {
+ useMapFlow = useReduceFlow;
+ mapper = MakeJobLambda<false>(mapper.Cast<TCoLambda>(), useMapFlow, ctx);
+ }
+ }
+
+ if (forceMapper && mapper.Maybe<TCoVoid>()) {
+ mapper = MakeJobLambda<false>(Build<TCoLambda>(ctx, node.Pos()).Args({"stream"}).Body("stream").Done(), useMapFlow, ctx);
+ }
+ auto mapReduce = Build<TYtMapReduce>(ctx, node.Pos())
+ .World(ApplySyncListToWorld(world.Ptr(), syncList, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx, TConvertInputOpts().MakeUnordered(unordered)))
+ .Output()
+ .Add(ConvertOutTables(node.Pos(), outItemType, ctx, State_, &partByKey.Ref().GetConstraintSet()))
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Reducer(reducer)
+ .Done();
+ return WrapOp(mapReduce, ctx);
+ }
+
+ TMaybeNode<TExprBase> FlatMap(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto flatMap = node.Cast<TCoFlatMapBase>();
+
+ const auto disableOptimizers = State_->Configuration->DisableOptimizers.Get().GetOrElse(TSet<TString>());
+ if (!disableOptimizers.contains("EquiJoinPremap") && CanBePulledIntoParentEquiJoin(flatMap, getParents)) {
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": " << flatMap.Ref().Content() << " can be pulled into parent EquiJoin";
+ return node;
+ }
+
+ auto input = flatMap.Input();
+ if (!IsYtProviderInput(input, true)) {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(input)};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(flatMap.Lambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ auto outItemType = SilentGetSequenceItemType(flatMap.Lambda().Body().Ref(), true);
+ if (!outItemType || !outItemType->IsPersistable()) {
+ return node;
+ }
+
+ auto cleanup = CleanupWorld(flatMap.Lambda(), ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ auto mapper = ctx.Builder(node.Pos())
+ .Lambda()
+ .Param("stream")
+ .Callable(flatMap.Ref().Content())
+ .Arg(0, "stream")
+ .Lambda(1)
+ .Param("item")
+ .Apply(cleanup.Cast().Ptr())
+ .With(0, "item")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+
+ bool sortedOutput = false;
+ TVector<TYtOutTable> outTables = ConvertOutTablesWithSortAware(mapper, sortedOutput, flatMap.Pos(),
+ outItemType, ctx, State_, flatMap.Ref().GetConstraintSet());
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, flatMap.Pos());
+ if (sortedOutput) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ } else if (TCoOrderedFlatMap::Match(flatMap.Raw()) && outTables.size() == 1) {
+ mapper = ctx.Builder(node.Pos())
+ .Lambda()
+ .Param("stream")
+ .Callable(TCoUnordered::CallableName())
+ .Apply(0, mapper)
+ .With(0, "stream")
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ if (State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ auto ytMap = Build<TYtMap>(ctx, node.Pos())
+ .World(ApplySyncListToWorld(GetWorld(input, {}, ctx).Ptr(), syncList, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx))
+ .Output()
+ .Add(outTables)
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(std::move(mapper))
+ .Done();
+
+ return WrapOp(ytMap, ctx);
+ }
+
+ TMaybeNode<TExprBase> CombineByKey(TExprBase node, TExprContext& ctx) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto combineByKey = node.Cast<TCoCombineByKey>();
+
+ auto input = combineByKey.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ if (!GetSequenceItemType(input, false, ctx)) {
+ return {};
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = SilentGetSequenceItemType(combineByKey.FinishHandlerLambda().Body().Ref(), false); type && type->IsPersistable()) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(input)};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(combineByKey.PreMapLambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+ if (!IsYtCompleteIsolatedLambda(combineByKey.KeySelectorLambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+ if (!IsYtCompleteIsolatedLambda(combineByKey.InitHandlerLambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+ if (!IsYtCompleteIsolatedLambda(combineByKey.UpdateHandlerLambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+ if (!IsYtCompleteIsolatedLambda(combineByKey.FinishHandlerLambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ auto preMapLambda = CleanupWorld(combineByKey.PreMapLambda(), ctx);
+ auto keySelectorLambda = CleanupWorld(combineByKey.KeySelectorLambda(), ctx);
+ auto initHandlerLambda = CleanupWorld(combineByKey.InitHandlerLambda(), ctx);
+ auto updateHandlerLambda = CleanupWorld(combineByKey.UpdateHandlerLambda(), ctx);
+ auto finishHandlerLambda = CleanupWorld(combineByKey.FinishHandlerLambda(), ctx);
+ if (!preMapLambda || !keySelectorLambda || !initHandlerLambda || !updateHandlerLambda || !finishHandlerLambda) {
+ return {};
+ }
+
+ auto lambdaBuilder = Build<TCoLambda>(ctx, combineByKey.Pos());
+ TMaybe<TCoLambda> lambdaRet;
+ if (!IsDepended(keySelectorLambda.Cast().Body().Ref(), keySelectorLambda.Cast().Args().Arg(0).Ref())) {
+ lambdaBuilder
+ .Args({TStringBuf("stream")})
+ .Body<TCoFlatMap>()
+ .Input<TCoCondense1>()
+ .Input<TCoFlatMap>()
+ .Input(TStringBuf("stream"))
+ .Lambda()
+ .Args({TStringBuf("item")})
+ .Body<TExprApplier>()
+ .Apply(preMapLambda.Cast())
+ .With(0, TStringBuf("item"))
+ .Build()
+ .Build()
+ .Build()
+ .InitHandler()
+ .Args({TStringBuf("item")})
+ .Body<TExprApplier>()
+ .Apply(initHandlerLambda.Cast())
+ .With(0, keySelectorLambda.Cast().Body())
+ .With(1, TStringBuf("item"))
+ .Build()
+ .Build()
+ .SwitchHandler()
+ .Args({TStringBuf("item"), TStringBuf("state")})
+ .Body<TCoBool>()
+ .Literal().Build("false")
+ .Build()
+ .Build()
+ .UpdateHandler()
+ .Args({TStringBuf("item"), TStringBuf("state")})
+ .Body<TExprApplier>()
+ .Apply(updateHandlerLambda.Cast())
+ .With(0, keySelectorLambda.Cast().Body())
+ .With(1, TStringBuf("item"))
+ .With(2, TStringBuf("state"))
+ .Build()
+ .Build()
+ .Build()
+ .Lambda()
+ .Args({TStringBuf("state")})
+ .Body<TExprApplier>()
+ .Apply(finishHandlerLambda.Cast())
+ .With(0, keySelectorLambda.Cast().Body())
+ .With(1, TStringBuf("state"))
+ .Build()
+ .Build()
+ .Build();
+
+ lambdaRet = lambdaBuilder.Done();
+ } else {
+ lambdaBuilder
+ .Args({TStringBuf("stream")})
+ .Body<TCoCombineCore>()
+ .Input<TCoFlatMap>()
+ .Input(TStringBuf("stream"))
+ .Lambda()
+ .Args({TStringBuf("item")})
+ .Body<TExprApplier>()
+ .Apply(preMapLambda.Cast())
+ .With(0, TStringBuf("item"))
+ .Build()
+ .Build()
+ .Build()
+ .KeyExtractor()
+ .Args({TStringBuf("item")})
+ .Body<TExprApplier>()
+ .Apply(keySelectorLambda.Cast())
+ .With(0, TStringBuf("item"))
+ .Build()
+ .Build()
+ .InitHandler()
+ .Args({TStringBuf("key"), TStringBuf("item")})
+ .Body<TExprApplier>()
+ .Apply(initHandlerLambda.Cast())
+ .With(0, TStringBuf("key"))
+ .With(1, TStringBuf("item"))
+ .Build()
+ .Build()
+ .UpdateHandler()
+ .Args({TStringBuf("key"), TStringBuf("item"), TStringBuf("state")})
+ .Body<TExprApplier>()
+ .Apply(updateHandlerLambda.Cast())
+ .With(0, TStringBuf("key"))
+ .With(1, TStringBuf("item"))
+ .With(2, TStringBuf("state"))
+ .Build()
+ .Build()
+ .FinishHandler()
+ .Args({TStringBuf("key"), TStringBuf("state")})
+ .Body<TExprApplier>()
+ .Apply(finishHandlerLambda.Cast())
+ .With(0, TStringBuf("key"))
+ .With(1, TStringBuf("state"))
+ .Build()
+ .Build()
+ .MemLimit()
+ .Value(ToString(State_->Configuration->CombineCoreLimit.Get().GetOrElse(0)))
+ .Build()
+ .Build();
+
+ lambdaRet = lambdaBuilder.Done();
+ }
+
+ if (HasContextFuncs(*lambdaRet->Ptr())) {
+ lambdaRet = Build<TCoLambda>(ctx, combineByKey.Pos())
+ .Args({ TStringBuf("stream") })
+ .Body<TCoWithContext>()
+ .Name()
+ .Value("Agg")
+ .Build()
+ .Input<TExprApplier>()
+ .Apply(*lambdaRet)
+ .With(0, TStringBuf("stream"))
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TYtOutTableInfo combineOut(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+
+ return Build<TYtOutput>(ctx, combineByKey.Pos())
+ .Operation<TYtMap>()
+ .World(ApplySyncListToWorld(GetWorld(input, {}, ctx).Ptr(), syncList, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx))
+ .Output()
+ .Add(combineOut.ToExprNode(ctx, combineByKey.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(combineByKey.Pos(), *State_, ctx))
+ .Mapper(*lambdaRet)
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> DqWrite(TExprBase node, TExprContext& ctx, IOptimizationContext& optCtx, const TGetParents& getParents) const {
+ auto write = node.Cast<TYtWriteTable>();
+ if (!TDqCnUnionAll::Match(write.Content().Raw())) {
+ return node;
+ }
+
+ const TStructExprType* outItemType;
+ if (auto type = GetSequenceItemType(write.Content(), false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return node;
+ }
+
+ if (!NDq::IsSingleConsumerConnection(write.Content().Cast<TDqCnUnionAll>(), *getParents())) {
+ return node;
+ }
+
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(write.Content().Ref(), syncList, true, true)) {
+ return node;
+ }
+
+ const ui64 nativeTypeFlags = State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE;
+ TYtOutTableInfo outTable(outItemType, nativeTypeFlags);
+
+ const auto dqUnion = write.Content().Cast<TDqCnUnionAll>();
+
+ TMaybeNode<TCoSort> sort;
+ TMaybeNode<TCoTopSort> topSort;
+ TMaybeNode<TDqCnMerge> mergeConnection;
+ auto topLambdaBody = dqUnion.Output().Stage().Program().Body();
+
+ // Look for the sort-only stage or DcCnMerge connection.
+ bool sortOnlyLambda = true;
+ auto& topNode = SkipCallables(topLambdaBody.Ref(), {"ToFlow", "FromFlow", "ToStream"});
+ if (auto maybeSort = TMaybeNode<TCoSort>(&topNode)) {
+ sort = maybeSort;
+ } else if (auto maybeTopSort = TMaybeNode<TCoTopSort>(&topNode)) {
+ topSort = maybeTopSort;
+ } else {
+ sortOnlyLambda = false;
+ if (auto inputs = dqUnion.Output().Stage().Inputs(); inputs.Size() == 1 && inputs.Item(0).Maybe<TDqCnMerge>().IsValid()) {
+ if (topNode.IsArgument()) {
+ mergeConnection = inputs.Item(0).Maybe<TDqCnMerge>();
+ } else if (topNode.IsCallable(TDqReplicate::CallableName()) && topNode.Head().IsArgument()) {
+ auto ndx = FromString<size_t>(dqUnion.Output().Index().Value());
+ YQL_ENSURE(ndx + 1 < topNode.ChildrenSize());
+ if (&topNode.Child(ndx + 1)->Head().Head() == &topNode.Child(ndx + 1)->Tail()) { // trivial lambda
+ mergeConnection = inputs.Item(0).Maybe<TDqCnMerge>();
+ }
+ }
+ }
+ }
+
+ if (sortOnlyLambda) {
+ auto& bottomNode = SkipCallables(topNode.Head(), {"ToFlow", "FromFlow", "ToStream"});
+ sortOnlyLambda = bottomNode.IsArgument();
+ }
+
+ TCoLambda writeLambda = Build<TCoLambda>(ctx, write.Pos())
+ .Args({"stream"})
+ .Body<TDqWrite>()
+ .Input("stream")
+ .Provider().Value(YtProviderName).Build()
+ .Settings().Build()
+ .Build()
+ .Done();
+
+ if (sortOnlyLambda && (sort || topSort)) {
+ // Add Unordered callable to kill sort in a stage. Sorting will be handled by YtSort.
+ writeLambda = Build<TCoLambda>(ctx, write.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(writeLambda)
+ .With<TCoUnordered>(0)
+ .Input("stream")
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TDqConnection> result;
+ if (GetStageOutputsCount(dqUnion.Output().Stage()) > 1) {
+ result = Build<TDqCnUnionAll>(ctx, write.Pos())
+ .Output()
+ .Stage<TDqStage>()
+ .Inputs()
+ .Add(dqUnion)
+ .Build()
+ .Program(writeLambda)
+ .Settings(TDqStageSettings().BuildNode(ctx, write.Pos()))
+ .Build()
+ .Index().Build("0")
+ .Build()
+ .Done().Ptr();
+ } else {
+ result = NDq::DqPushLambdaToStageUnionAll(dqUnion, writeLambda, {}, ctx, optCtx);
+ if (!result) {
+ return {};
+ }
+ }
+
+ result = CleanupWorld(result.Cast(), ctx);
+
+ auto dqCnResult = Build<TDqCnResult>(ctx, write.Pos())
+ .Output()
+ .Stage<TDqStage>()
+ .Inputs()
+ .Add(result.Cast())
+ .Build()
+ .Program()
+ .Args({"row"})
+ .Body("row")
+ .Build()
+ .Settings(TDqStageSettings().BuildNode(ctx, write.Pos()))
+ .Build()
+ .Index().Build("0")
+ .Build()
+ .ColumnHints() // TODO: set column hints
+ .Build()
+ .Done().Ptr();
+
+ auto writeOp = Build<TYtDqProcessWrite>(ctx, write.Pos())
+ .World(ApplySyncListToWorld(ctx.NewWorld(write.Pos()), syncList, ctx))
+ .DataSink(write.DataSink().Ptr())
+ .Output()
+ .Add(outTable.ToExprNode(ctx, write.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Input(dqCnResult)
+ .Done().Ptr();
+
+ auto writeOutput = Build<TYtOutput>(ctx, write.Pos())
+ .Operation(writeOp)
+ .OutIndex().Value(TStringBuf("0")).Build()
+ .Done().Ptr();
+
+ if (sort) {
+ writeOutput = Build<TCoSort>(ctx, write.Pos())
+ .Input(writeOutput)
+ .SortDirections(sort.SortDirections().Cast())
+ .KeySelectorLambda(sort.KeySelectorLambda().Cast())
+ .Done().Ptr();
+ } else if (topSort) {
+ writeOutput = Build<TCoTake>(ctx, write.Pos())
+ .Count(topSort.Count().Cast())
+ .Input<TCoSort>()
+ .Input(writeOutput)
+ .SortDirections(topSort.SortDirections().Cast())
+ .KeySelectorLambda(topSort.KeySelectorLambda().Cast())
+ .Build()
+ .Done().Ptr();
+ } else if (mergeConnection) {
+ writeOutput = Build<TCoSort>(ctx, write.Pos())
+ .Input(writeOutput)
+ .SortDirections(
+ ctx.Builder(write.Pos())
+ .List()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ size_t index = 0;
+ for (auto col: mergeConnection.Cast().SortColumns()) {
+ parent.Callable(index++, "Bool")
+ .Atom(0, col.SortDirection().Value() == "Asc" ? "1" : "0", TNodeFlags::Default)
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Build()
+ )
+ .KeySelectorLambda(
+ ctx.Builder(write.Pos())
+ .Lambda()
+ .Param("item")
+ .List()
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ size_t index = 0;
+ for (auto col: mergeConnection.Cast().SortColumns()) {
+ parent.Callable(index++, "Member")
+ .Arg(0, "item")
+ .Atom(1, col.Column().Value())
+ .Seal();
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Build()
+ )
+ .Done().Ptr();
+ }
+
+ return ctx.ChangeChild(write.Ref(), TYtWriteTable::idx_Content, std::move(writeOutput));
+ }
+
+ TMaybeNode<TExprBase> YtDqProcessWrite(TExprBase node, TExprContext& ctx) const {
+ const auto& write = node.Cast<TYtDqProcessWrite>();
+ if (const auto& contents = FindNodes(write.Input().Ptr(),
+ [] (const TExprNode::TPtr& node) { return !TYtOutputOpBase::Match(node.Get()); },
+ [] (const TExprNode::TPtr& node) { return node->IsCallable({TCoToFlow::CallableName(), TCoIterator::CallableName()}) && node->Head().IsCallable(TYtTableContent::CallableName()); });
+ !contents.empty()) {
+ TNodeOnNodeOwnedMap replaces(contents.size());
+ const bool addToken = !State_->Configuration->Auth.Get().GetOrElse(TString()).empty();
+
+ for (const auto& cont : contents) {
+ const TYtTableContent content(cont->HeadPtr());
+ auto input = content.Input();
+ const auto output = input.Maybe<TYtOutput>();
+ const auto structType = GetSeqItemType(output ? output.Cast().Ref().GetTypeAnn() : input.Ref().GetTypeAnn()->Cast<TTupleExprType>()->GetItems().back())->Cast<TStructExprType>();
+ if (output) {
+ input = ConvertContentInputToRead(output.Cast(), {}, ctx);
+ }
+ TMaybeNode<TCoSecureParam> secParams;
+ if (addToken) {
+ const auto cluster = input.Cast<TYtReadTable>().DataSource().Cluster();
+ secParams = Build<TCoSecureParam>(ctx, node.Pos()).Name().Build(TString("cluster:default_").append(cluster)).Done();
+ }
+
+ TExprNode::TListType flags;
+ if (!NYql::HasSetting(content.Settings().Ref(), EYtSettingType::Split))
+ flags.emplace_back(ctx.NewAtom(cont->Pos(), "Solid", TNodeFlags::Default));
+
+ const auto read = Build<TDqReadWideWrap>(ctx, cont->Pos())
+ .Input(input)
+ .Flags().Add(std::move(flags)).Build()
+ .Token(secParams)
+ .Done();
+
+ auto narrow = ctx.Builder(cont->Pos())
+ .Callable("NarrowMap")
+ .Add(0, read.Ptr())
+ .Lambda(1)
+ .Params("fields", structType->GetSize())
+ .Callable(TCoAsStruct::CallableName())
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : structType->GetItems()) {
+ parent.List(i)
+ .Atom(0, item->GetName())
+ .Arg(1, "fields", i)
+ .Seal();
+ ++i;
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal().Build();
+
+ narrow = ctx.WrapByCallableIf(cont->IsCallable(TCoIterator::CallableName()), TCoFromFlow::CallableName(), std::move(narrow));
+ replaces.emplace(cont.Get(), std::move(narrow));
+ }
+
+ return Build<TYtDqProcessWrite>(ctx, write.Pos())
+ .InitFrom(write)
+ .Input(ctx.ReplaceNodes(write.Input().Ptr(), replaces))
+ .Done();
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> Write(TExprBase node, TExprContext& ctx) const {
+ auto write = node.Cast<TYtWriteTable>();
+ if (!IsYtProviderInput(write.Content())) {
+ return node;
+ }
+
+ auto cluster = TString{write.DataSink().Cluster().Value()};
+ auto srcCluster = GetClusterName(write.Content());
+ if (cluster != srcCluster) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder()
+ << "Result from cluster " << TString{srcCluster}.Quote()
+ << " cannot be written to a different destination cluster " << cluster.Quote()));
+ return {};
+ }
+
+ TVector<TYtPathInfo::TPtr> inputPaths = GetInputPaths(write.Content());
+ TYtTableInfo::TPtr outTableInfo = MakeIntrusive<TYtTableInfo>(write.Table());
+
+ const auto mode = NYql::GetSetting(write.Settings().Ref(), EYtSettingType::Mode);
+ const bool renew = !mode || FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Renew;
+ const bool flush = mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Flush;
+ const bool transactionalOverrideTarget = NYql::GetSetting(write.Settings().Ref(), EYtSettingType::Initial)
+ && !flush && (renew || !outTableInfo->Meta->DoesExist);
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(write.Content(), false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ auto maybeReadSettings = write.Content().Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Settings();
+
+ const TYtTableDescription& nextDescription = State_->TablesData->GetTable(cluster, outTableInfo->Name, outTableInfo->CommitEpoch);
+ const ui64 nativeTypeFlags = nextDescription.RowSpec->GetNativeYtTypeFlags();
+
+ TMaybe<NYT::TNode> firstNativeType;
+ ui64 firstNativeTypeFlags = 0;
+ if (!inputPaths.empty()) {
+ firstNativeType = inputPaths.front()->GetNativeYtType();
+ firstNativeTypeFlags = inputPaths.front()->GetNativeYtTypeFlags();
+ }
+
+ bool requiresMap = (maybeReadSettings && NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::SysColumns))
+ || AnyOf(inputPaths, [firstNativeType] (const TYtPathInfo::TPtr& path) {
+ return path->RequiresRemap() || firstNativeType != path->GetNativeYtType();
+ });
+
+ const bool requiresMerge = !requiresMap && (
+ AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) {
+ return path->Ranges || path->HasColumns() || path->Table->Meta->IsDynamic || path->Table->FromNode.Maybe<TYtTable>();
+ })
+ || (maybeReadSettings && NYql::HasAnySetting(maybeReadSettings.Ref(),
+ EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Sample))
+ || (nextDescription.RowSpec->GetColumnOrder().Defined() && AnyOf(inputPaths, [colOrder = *nextDescription.RowSpec->GetColumnOrder()] (const TYtPathInfo::TPtr& path) {
+ return path->Table->RowSpec->GetColumnOrder().Defined() && path->Table->RowSpec->GetColumnOrder() != colOrder;
+ }))
+ );
+
+ TMaybeNode<TCoAtom> outMode;
+ if (ctx.IsConstraintEnabled<TSortedConstraintNode>() && maybeReadSettings && NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::Unordered)) {
+ outMode = Build<TCoAtom>(ctx, write.Pos()).Value(ToString(EYtSettingType::Unordered)).Done();
+ }
+
+ TVector<TYtOutput> publishInput;
+ if (requiresMap || requiresMerge) {
+ TExprNode::TPtr mapper;
+ if (requiresMap) {
+ mapper = Build<TCoLambda>(ctx, write.Pos())
+ .Args({"stream"})
+ .Body("stream")
+ .Done().Ptr();
+ }
+
+ // For YtMerge passthrough native flags as is. AlignPublishTypes optimizer will add additional remapping
+ TYtOutTableInfo outTable(outItemType, requiresMerge ? firstNativeTypeFlags : nativeTypeFlags);
+ if (firstNativeType) {
+ outTable.RowSpec->CopyTypeOrders(*firstNativeType);
+ }
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, write.Pos());
+ bool useExplicitColumns = requiresMerge && AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) {
+ return !path->Table->IsTemp;
+ });
+ if (requiresMap) {
+ if (ctx.IsConstraintEnabled<TSortedConstraintNode>()) {
+ if (auto sorted = write.Content().Ref().GetConstraint<TSortedConstraintNode>()) {
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ TKeySelectorBuilder builder(write.Pos(), ctx, useNativeDescSort, outItemType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+
+ if (builder.NeedMap()) {
+ mapper = ctx.Builder(write.Pos())
+ .Lambda()
+ .Param("stream")
+ .Apply(builder.MakeRemapLambda(true))
+ .With(0)
+ .Apply(mapper)
+ .With(0, "stream")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+ } else {
+ if (inputPaths.size() == 1 && inputPaths.front()->Table->RowSpec && inputPaths.front()->Table->RowSpec->IsSorted()) {
+ outTable.RowSpec->CopySortness(*inputPaths.front()->Table->RowSpec);
+ }
+ }
+ }
+ else { // requiresMerge
+ // TODO: should we keep sort if multiple inputs?
+ if (outMode || AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) { return path->Table->IsUnordered; })) {
+ useExplicitColumns = useExplicitColumns || AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) { return path->Table->RowSpec->HasAuxColumns(); });
+ }
+ else {
+ const bool exactCopySort = inputPaths.size() == 1 && !inputPaths.front()->HasColumns();
+ bool hasAux = inputPaths.front()->Table->RowSpec->HasAuxColumns();
+ bool sortIsChanged = inputPaths.front()->Table->IsUnordered
+ ? inputPaths.front()->Table->RowSpec->IsSorted()
+ : outTable.RowSpec->CopySortness(*inputPaths.front()->Table->RowSpec,
+ exactCopySort ? TYqlRowSpecInfo::ECopySort::Exact : TYqlRowSpecInfo::ECopySort::WithDesc);
+ useExplicitColumns = useExplicitColumns || (inputPaths.front()->HasColumns() && hasAux);
+
+ for (size_t i = 1; i < inputPaths.size(); ++i) {
+ sortIsChanged = outTable.RowSpec->MakeCommonSortness(*inputPaths[i]->Table->RowSpec) || sortIsChanged;
+ const bool tableHasAux = inputPaths[i]->Table->RowSpec->HasAuxColumns();
+ hasAux = hasAux || tableHasAux;
+ if (inputPaths[i]->HasColumns() && tableHasAux) {
+ useExplicitColumns = true;
+ }
+ }
+ useExplicitColumns = useExplicitColumns || (sortIsChanged && hasAux);
+ }
+
+ if (maybeReadSettings && NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::Sample)) {
+ settingsBuilder.Add()
+ .Name().Value(ToString(EYtSettingType::ForceTransform)).Build()
+ .Build();
+ }
+ }
+ outTable.SetUnique(write.Content().Ref().GetConstraint<TDistinctConstraintNode>(), write.Pos(), ctx);
+ outTable.RowSpec->SetConstraints(write.Content().Ref().GetConstraintSet());
+
+ TMaybeNode<TYtTransientOpBase> op;
+ if (requiresMap) {
+ if (outTable.RowSpec->IsSorted()) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ }
+ if (State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ op = Build<TYtMap>(ctx, write.Pos())
+ .World(GetWorld(write.Content(), {}, ctx))
+ .DataSink(write.DataSink())
+ .Input(ConvertInputTable(write.Content(), ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, write.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Done();
+ }
+ else {
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*outTable.RowSpec, write.Pos(), ctx);
+ }
+ op = Build<TYtMerge>(ctx, write.Pos())
+ .World(GetWorld(write.Content(), {}, ctx))
+ .DataSink(write.DataSink())
+ .Input(ConvertInputTable(write.Content(), ctx, opts))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, write.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Done();
+ }
+
+ publishInput.push_back(Build<TYtOutput>(ctx, write.Pos())
+ .Operation(op.Cast())
+ .OutIndex().Value(0U).Build()
+ .Mode(outMode)
+ .Done());
+ }
+ else {
+ if (auto out = write.Content().Maybe<TYtOutput>()) {
+ publishInput.push_back(out.Cast());
+ } else {
+ for (auto path: write.Content().Cast<TCoRight>().Input().Cast<TYtReadTable>().Input().Item(0).Paths()) {
+ publishInput.push_back(Build<TYtOutput>(ctx, path.Table().Pos())
+ .InitFrom(path.Table().Cast<TYtOutput>())
+ .Mode(outMode)
+ .Done());
+ }
+ }
+ }
+
+ auto publishSettings = write.Settings();
+ if (transactionalOverrideTarget) {
+ publishSettings = TCoNameValueTupleList(NYql::RemoveSetting(publishSettings.Ref(), EYtSettingType::Mode, ctx));
+ }
+
+ return Build<TYtPublish>(ctx, write.Pos())
+ .World(write.World())
+ .DataSink(write.DataSink())
+ .Input()
+ .Add(publishInput)
+ .Build()
+ .Publish(write.Table().Cast<TYtTable>())
+ .Settings(publishSettings)
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> Fill(TExprBase node, TExprContext& ctx) const {
+ auto write = node.Cast<TYtWriteTable>();
+
+ auto mode = NYql::GetSetting(write.Settings().Ref(), EYtSettingType::Mode);
+
+ if (mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Drop) {
+ return node;
+ }
+
+ auto cluster = TString{write.DataSink().Cluster().Value()};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(write.Content().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ if (FindNode(write.Content().Ptr(),
+ [] (const TExprNode::TPtr& node) { return !TMaybeNode<TYtOutputOpBase>(node).IsValid(); },
+ [] (const TExprNode::TPtr& node) { return TMaybeNode<TDqConnection>(node).IsValid(); })) {
+ return node;
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(write.Content(), false, ctx)) {
+ if (!EnsurePersistableType(write.Content().Pos(), *type, ctx)) {
+ return {};
+ }
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+ TYtOutTableInfo outTable(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ auto content = write.Content();
+ if (auto sorted = content.Ref().GetConstraint<TSortedConstraintNode>()) {
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ TKeySelectorBuilder builder(node.Pos(), ctx, useNativeDescSort, outItemType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+
+ if (builder.NeedMap()) {
+ content = Build<TExprApplier>(ctx, content.Pos())
+ .Apply(TCoLambda(builder.MakeRemapLambda(true)))
+ .With(0, content)
+ .Done();
+ outItemType = builder.MakeRemapType();
+ }
+
+ } else if (auto unordered = content.Maybe<TCoUnorderedBase>()) {
+ content = unordered.Cast().Input();
+ }
+ outTable.RowSpec->SetConstraints(write.Content().Ref().GetConstraintSet());
+ outTable.SetUnique(write.Content().Ref().GetConstraint<TDistinctConstraintNode>(), node.Pos(), ctx);
+
+ TYtTableInfo::TPtr pubTableInfo = MakeIntrusive<TYtTableInfo>(write.Table());
+ const bool renew = !mode || FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Renew;
+ const bool flush = mode && FromString<EYtWriteMode>(mode->Child(1)->Content()) == EYtWriteMode::Flush;
+ const bool transactionalOverrideTarget = NYql::GetSetting(write.Settings().Ref(), EYtSettingType::Initial)
+ && !flush && (renew || !pubTableInfo->Meta->DoesExist);
+
+ auto publishSettings = write.Settings();
+ if (transactionalOverrideTarget) {
+ publishSettings = TCoNameValueTupleList(NYql::RemoveSetting(publishSettings.Ref(), EYtSettingType::Mode, ctx));
+ }
+
+ auto cleanup = CleanupWorld(content, ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ return Build<TYtPublish>(ctx, write.Pos())
+ .World(write.World())
+ .DataSink(write.DataSink())
+ .Input()
+ .Add()
+ .Operation<TYtFill>()
+ .World(ApplySyncListToWorld(ctx.NewWorld(write.Pos()), syncList, ctx))
+ .DataSink(write.DataSink())
+ .Content(MakeJobLambdaNoArg(cleanup.Cast(), ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, write.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(write.Pos(), *State_, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Build()
+ .Build()
+ .Publish(write.Table())
+ .Settings(publishSettings)
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> TakeOrSkip(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto countBase = node.Cast<TCoCountBase>();
+ auto input = countBase.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(input)};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(countBase.Count().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ auto count = CleanupWorld(countBase.Count(), ctx);
+ if (!count) {
+ return {};
+ }
+
+ EYtSettingType settingType = node.Maybe<TCoSkip>() ? EYtSettingType::Skip : EYtSettingType::Take;
+
+ auto settings = Build<TCoNameValueTupleList>(ctx, countBase.Pos())
+ .Add()
+ .Name()
+ .Value(ToString(settingType))
+ .Build()
+ .Value(count.Cast())
+ .Build()
+ .Done();
+
+ if (!ctx.IsConstraintEnabled<TSortedConstraintNode>()) {
+ if (auto maybeMap = input.Maybe<TYtOutput>().Operation().Maybe<TYtMap>()) {
+ TYtMap map = maybeMap.Cast();
+ if (!IsOutputUsedMultipleTimes(map.Ref(), *getParents())) {
+ TYtOutTableInfo mapOut(map.Output().Item(0));
+ if (mapOut.RowSpec->IsSorted()) {
+ mapOut.RowSpec->ClearSortness();
+ input = Build<TYtOutput>(ctx, input.Pos())
+ .InitFrom(input.Cast<TYtOutput>())
+ .Operation<TYtMap>()
+ .InitFrom(map)
+ .Output()
+ .Add(mapOut.ToExprNode(ctx, map.Output().Item(0).Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Build()
+ .Done();
+ }
+ }
+ }
+ }
+
+ auto res = Build<TCoRight>(ctx, countBase.Pos())
+ .Input<TYtReadTable>()
+ .World(ApplySyncListToWorld(GetWorld(input, {}, ctx).Ptr(), syncList, ctx))
+ .DataSource(GetDataSource(input, ctx))
+ .Input(ConvertInputTable(input, ctx, TConvertInputOpts().KeepDirecRead(true).Settings(settings)))
+ .Build()
+ .Done();
+ return KeepColumnOrder(res.Ptr(), node.Ref(), ctx, *State_->Types);
+ }
+
+ TMaybeNode<TExprBase> ExtendDqReadWraps(TExprBase node, TExprContext& ctx) const {
+ auto extend = node.Cast<TCoExtendBase>();
+
+ // TODO: group TYtReadTable by token/settings:
+ // (Merge (DqReadWrap YtReadTable_group1) (DqReadWrap YtReadTable_group1) (DqReadWrap OtherReads))
+
+ TExprNode::TPtr dqReadWrapNode;
+ TExprNode::TPtr dataSource;
+ TVector<TExprBase> worlds;
+ TVector<TYtPath> paths;
+ TString prevToken;
+ TExprNode::TPtr settings;
+
+ for (auto child: extend) {
+ auto maybeDqReadWrap = child.Maybe<TDqReadWrap>();
+ if (maybeDqReadWrap && !maybeDqReadWrap.Cast().Flags().Size()) {
+ auto dqReadWrap = maybeDqReadWrap.Cast();
+ auto maybeYtReadTable = dqReadWrap.Input().Maybe<TYtReadTable>();
+ auto token = dqReadWrap.Token();
+ TStringBuf tokenStr = token.IsValid() ? token.Name().Cast().Value() : TStringBuf();
+ if (prevToken && prevToken != tokenStr) {
+ return node;
+ }
+ dqReadWrapNode = dqReadWrap.Ptr();
+ if (maybeYtReadTable) {
+ auto ytReadTable = maybeYtReadTable.Cast();
+
+ if (ytReadTable.Input().Size() != 1) {
+ return node;
+ }
+
+ auto section = ytReadTable.Input().Item(0);
+ if (settings && settings != section.Settings().Ptr()) {
+ return node;
+ }
+ if (dataSource && dataSource != ytReadTable.DataSource().Ptr()) {
+ return node;
+ }
+ dataSource = ytReadTable.DataSource().Ptr();
+ settings = section.Settings().Ptr();
+ paths.insert(paths.end(), section.Paths().begin(), section.Paths().end());
+ worlds.push_back(ytReadTable.World());
+ } else {
+ return node;
+ }
+ prevToken = tokenStr;
+ } else {
+ return node;
+ }
+ }
+
+ auto newWorld = Build<TCoSync>(ctx, node.Pos()).Add(worlds).Done();
+
+ TYtReadTable read = Build<TYtReadTable>(ctx, node.Pos())
+ .World(newWorld)
+ .DataSource(dataSource)
+ .Input()
+ .Add()
+ .Paths().Add(paths).Build()
+ .Settings(settings)
+ .Build()
+ .Build()
+ .Done();
+
+ return Build<TDqReadWrap>(ctx, node.Pos())
+ .Input(read)
+ .Flags().Build()
+ .Token(TExprBase(dqReadWrapNode).Cast<TDqReadWrap>().Token())
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> Extend(TExprBase node, TExprContext& ctx) const {
+ auto extend = node.Cast<TCoExtendBase>();
+
+ bool allAreTables = true;
+ bool hasTables = false;
+ bool allAreTableContents = true;
+ bool hasContents = false;
+ bool keepSort = !ctx.IsConstraintEnabled<TSortedConstraintNode>() || (bool)extend.Ref().GetConstraint<TSortedConstraintNode>();
+ TString resultCluster;
+ TMaybeNode<TYtDSource> dataSource;
+
+ for (auto child: extend) {
+ bool isTable = IsYtProviderInput(child);
+ bool isContent = child.Maybe<TYtTableContent>().IsValid();
+ if (!isTable && !isContent) {
+ // Don't match foreign provider input
+ if (child.Maybe<TCoRight>()) {
+ return node;
+ }
+ } else {
+ auto currentDataSource = GetDataSource(child, ctx);
+ auto currentCluster = TString{currentDataSource.Cluster().Value()};
+ if (!dataSource) {
+ dataSource = currentDataSource;
+ resultCluster = currentCluster;
+ } else if (resultCluster != currentCluster) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder()
+ << "Different source clusters in " << extend.Ref().Content() << ": " << resultCluster
+ << " and " << currentCluster));
+ return {};
+ }
+ }
+ allAreTables = allAreTables && isTable;
+ hasTables = hasTables || isTable;
+ allAreTableContents = allAreTableContents && isContent;
+ hasContents = hasContents || isContent;
+ }
+
+ if (!hasTables && !hasContents) {
+ return node;
+ }
+
+ auto dataSink = TYtDSink(ctx.RenameNode(dataSource.Ref(), "DataSink"));
+ if (allAreTables || allAreTableContents) {
+ TVector<TExprBase> worlds;
+ TVector<TYtPath> paths;
+ TExprNode::TListType newExtendParts;
+ bool updateChildren = false;
+ bool unordered = false;
+ bool nonUniq = false;
+ for (auto child: extend) {
+ newExtendParts.push_back(child.Ptr());
+
+ auto read = child.Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ if (!read) {
+ read = child.Maybe<TYtTableContent>().Input().Maybe<TYtReadTable>();
+ }
+ if (read) {
+ YQL_ENSURE(read.Cast().Input().Size() == 1);
+ auto section = read.Cast().Input().Item(0);
+ unordered = unordered || NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered);
+ nonUniq = nonUniq || NYql::HasSetting(section.Settings().Ref(), EYtSettingType::NonUnique);
+ TExprNode::TPtr settings = NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::DirectRead | EYtSettingType::Unordered | EYtSettingType::NonUnique, ctx);
+ if (settings->ChildrenSize() != 0) {
+ if (State_->Types->EvaluationInProgress || allAreTableContents) {
+ return node;
+ }
+ auto scheme = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ auto path = CopyOrTrivialMap(section.Pos(),
+ read.Cast().World(), dataSink,
+ *scheme,
+ TYtSection(ctx.ChangeChild(section.Ref(), TYtSection::idx_Settings, std::move(settings))),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(keepSort).SetRangesResetSort(!keepSort).SetSectionUniq(section.Ref().GetConstraint<TDistinctConstraintNode>()));
+ updateChildren = true;
+ newExtendParts.back() = allAreTableContents
+ ? ctx.ChangeChild(child.Ref(), TYtTableContent::idx_Input, path.Table().Ptr())
+ : path.Table().Ptr();
+ } else {
+ paths.insert(paths.end(), section.Paths().begin(), section.Paths().end());
+ if (allAreTables) {
+ worlds.push_back(GetWorld(child, {}, ctx));
+ }
+ }
+ } else {
+ YQL_ENSURE(child.Maybe<TYtOutput>(), "Unknown extend element: " << child.Ref().Content());
+ paths.push_back(
+ Build<TYtPath>(ctx, child.Pos())
+ .Table(child) // child is TYtOutput
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done()
+ );
+ }
+ }
+
+ if (updateChildren) {
+ return TExprBase(ctx.ChangeChildren(extend.Ref(), std::move(newExtendParts)));
+ }
+
+ newExtendParts.clear();
+
+ auto world = worlds.empty()
+ ? TExprBase(ctx.NewWorld(extend.Pos()))
+ : worlds.size() == 1
+ ? worlds.front()
+ : Build<TCoSync>(ctx, extend.Pos()).Add(worlds).Done();
+
+ auto scheme = extend.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+
+ if (keepSort && extend.Maybe<TCoMerge>() && paths.size() > 1) {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+ auto path = CopyOrTrivialMap(extend.Pos(),
+ world, dataSink,
+ *scheme,
+ Build<TYtSection>(ctx, extend.Pos())
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings()
+ .Build()
+ .Done(),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(keepSort).SetRangesResetSort(!keepSort).SetSectionUniq(extend.Ref().GetConstraint<TDistinctConstraintNode>()));
+ world = TExprBase(ctx.NewWorld(extend.Pos()));
+ paths.assign(1, path);
+ }
+
+ if (paths.size() == 1 && paths.front().Columns().Maybe<TCoVoid>() && paths.front().Ranges().Maybe<TCoVoid>()) {
+ return allAreTables
+ ? paths.front().Table()
+ : Build<TYtTableContent>(ctx, extend.Pos())
+ .Input(paths.front().Table())
+ .Settings().Build()
+ .Done().Cast<TExprBase>();
+ }
+
+ auto newSettings = ctx.NewList(extend.Pos(), {});
+ if (nonUniq) {
+ newSettings = NYql::AddSetting(*newSettings, EYtSettingType::NonUnique, {}, ctx);
+ }
+ auto newSection = Build<TYtSection>(ctx, extend.Pos())
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings(newSettings)
+ .Done();
+ if (unordered) {
+ newSection = MakeUnorderedSection<true>(newSection, ctx);
+ }
+
+ auto resRead = Build<TYtReadTable>(ctx, extend.Pos())
+ .World(world)
+ .DataSource(dataSource.Cast())
+ .Input()
+ .Add(newSection)
+ .Build()
+ .Done();
+
+ return allAreTables
+ ? Build<TCoRight>(ctx, extend.Pos())
+ .Input(resRead)
+ .Done().Cast<TExprBase>()
+ : Build<TYtTableContent>(ctx, extend.Pos())
+ .Input(resRead)
+ .Settings().Build()
+ .Done().Cast<TExprBase>();
+ }
+
+ if (!hasTables) {
+ return node;
+ }
+
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+ TExprNode::TListType newExtendParts;
+ for (auto child: extend) {
+ if (!IsYtProviderInput(child)) {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(child.Ref(), syncList, resultCluster, true, false)) {
+ return node;
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(child, false, ctx)) {
+ if (!EnsurePersistableType(child.Pos(), *type, ctx)) {
+ return {};
+ }
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ TYtOutTableInfo outTable(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ auto content = child;
+ auto sorted = child.Ref().GetConstraint<TSortedConstraintNode>();
+ if (keepSort && sorted) {
+ TKeySelectorBuilder builder(child.Pos(), ctx, useNativeDescSort, outItemType);
+ builder.ProcessConstraint(*sorted);
+ builder.FillRowSpecSort(*outTable.RowSpec);
+
+ if (builder.NeedMap()) {
+ content = Build<TExprApplier>(ctx, child.Pos())
+ .Apply(TCoLambda(builder.MakeRemapLambda(true)))
+ .With(0, content)
+ .Done();
+ outItemType = builder.MakeRemapType();
+ }
+
+ } else if (auto unordered = content.Maybe<TCoUnorderedBase>()) {
+ content = unordered.Cast().Input();
+ }
+ outTable.RowSpec->SetConstraints(child.Ref().GetConstraintSet());
+ outTable.SetUnique(child.Ref().GetConstraint<TDistinctConstraintNode>(), child.Pos(), ctx);
+
+ auto cleanup = CleanupWorld(content, ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ newExtendParts.push_back(
+ Build<TYtOutput>(ctx, child.Pos())
+ .Operation<TYtFill>()
+ .World(ApplySyncListToWorld(ctx.NewWorld(child.Pos()), syncList, ctx))
+ .DataSink(dataSink)
+ .Content(MakeJobLambdaNoArg(cleanup.Cast(), ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, child.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(child.Pos(), *State_, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done().Ptr()
+ );
+ }
+ else {
+ newExtendParts.push_back(child.Ptr());
+ }
+ }
+
+ return TExprBase(ctx.ChangeChildren(extend.Ref(), std::move(newExtendParts)));
+ }
+
+ TMaybeNode<TExprBase> Length(TExprBase node, TExprContext& ctx) const {
+ TExprBase list = node.Maybe<TCoLength>()
+ ? node.Cast<TCoLength>().List()
+ : node.Cast<TCoHasItems>().List();
+
+ TExprBase ytLengthInput = list;
+ if (auto content = list.Maybe<TYtTableContent>()) {
+ ytLengthInput = content.Cast().Input();
+ } else if (!IsYtProviderInput(list)) {
+ return node;
+ }
+
+ if (auto right = ytLengthInput.Maybe<TCoRight>()) {
+ ytLengthInput = right.Cast().Input();
+ }
+ // Now ytLengthInput is either YtReadTable or YtOutput
+
+ TVector<TCoNameValueTuple> takeSkip;
+ if (auto maybeRead = ytLengthInput.Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ YQL_ENSURE(read.Input().Size() == 1);
+ TYtSection section = read.Input().Item(0);
+ bool needMaterialize = NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample)
+ || AnyOf(section.Paths(), [](const TYtPath& path) { return !path.Ranges().Maybe<TCoVoid>() || TYtTableBaseInfo::GetMeta(path.Table())->IsDynamic; });
+ for (auto s: section.Settings()) {
+ switch (FromString<EYtSettingType>(s.Name().Value())) {
+ case EYtSettingType::Take:
+ case EYtSettingType::Skip:
+ takeSkip.push_back(s);
+ break;
+ default:
+ // Skip other settings
+ break;
+ }
+ }
+
+ if (needMaterialize) {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto scheme = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ ytLengthInput = CopyOrTrivialMap(section.Pos(),
+ TExprBase(ctx.NewWorld(section.Pos())),
+ TYtDSink(ctx.RenameNode(read.DataSource().Ref(), "DataSink")),
+ *scheme,
+ Build<TYtSection>(ctx, section.Pos())
+ .Paths(section.Paths())
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip
+ | EYtSettingType::DirectRead | EYtSettingType::Unordered | EYtSettingType::NonUnique, ctx))
+ .Done(),
+ ctx, State_, TCopyOrTrivialMapOpts()).Table();
+ }
+ else {
+ auto settings = section.Settings().Ptr();
+ if (!takeSkip.empty()) {
+ settings = NYql::RemoveSettings(*settings, EYtSettingType::Take | EYtSettingType::Skip
+ | EYtSettingType::DirectRead | EYtSettingType::Unordered | EYtSettingType::NonUnique, ctx);
+ }
+
+ if (read.World().Ref().Type() == TExprNode::World && settings->ChildrenSize() == 0 && section.Paths().Size() == 1 && section.Paths().Item(0).Table().Maybe<TYtOutput>()) {
+ // Simplify
+ ytLengthInput = Build<TYtOutput>(ctx, section.Paths().Item(0).Table().Pos())
+ .InitFrom(section.Paths().Item(0).Table().Cast<TYtOutput>())
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered))
+ .Build()
+ .Done();
+
+ } else {
+ ytLengthInput = Build<TYtReadTable>(ctx, read.Pos())
+ .InitFrom(read)
+ .Input()
+ .Add()
+ .InitFrom(MakeUnorderedSection(section, ctx))
+ .Settings(settings)
+ .Build()
+ .Build()
+ .Done();
+ }
+ }
+ }
+ else {
+ ytLengthInput = Build<TYtOutput>(ctx, ytLengthInput.Pos())
+ .InitFrom(ytLengthInput.Cast<TYtOutput>())
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered))
+ .Build()
+ .Done();
+ }
+
+ TExprBase res = Build<TYtLength>(ctx, node.Pos())
+ .Input(ytLengthInput)
+ .Done();
+
+ for (TCoNameValueTuple s: takeSkip) {
+ switch (FromString<EYtSettingType>(s.Name().Value())) {
+ case EYtSettingType::Take:
+ res = Build<TCoMin>(ctx, node.Pos())
+ .Add(res)
+ .Add(s.Value().Cast())
+ .Done();
+ break;
+ case EYtSettingType::Skip:
+ res = Build<TCoMinus>(ctx, node.Pos())
+ .Left<TCoMax>()
+ .Add(res)
+ .Add(s.Value().Cast())
+ .Build()
+ .Right(s.Value().Cast())
+ .Done();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (node.Maybe<TCoHasItems>()) {
+ res = Build<TCoAggrNotEqual>(ctx, node.Pos())
+ .Left(res)
+ .Right<TCoUint64>()
+ .Literal()
+ .Value(0U)
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ return res;
+ }
+
+ TMaybeNode<TExprBase> ResPull(TExprBase node, TExprContext& ctx) const {
+ auto resPull = node.Cast<TResPull>();
+
+ auto maybeRead = resPull.Data().Maybe<TCoRight>().Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ // Nothing to optimize in case of ResPull! over YtOutput!
+ return node;
+ }
+
+ auto read = maybeRead.Cast();
+ if (read.Input().Size() != 1) {
+ return node;
+ }
+ auto section = read.Input().Item(0);
+ auto scheme = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ bool directRead = NYql::HasSetting(section.Settings().Ref(), EYtSettingType::DirectRead);
+ const bool hasSettings = NYql::HasAnySetting(section.Settings().Ref(),
+ EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample | EYtSettingType::SysColumns);
+
+ const ui64 nativeTypeFlags = State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES)
+ ? GetNativeYtTypeFlags(*scheme->Cast<TStructExprType>())
+ : 0ul;
+
+ bool requiresMapOrMerge = false;
+ bool hasRanges = false;
+ bool hasNonTemp = false;
+ bool hasDynamic = false;
+ bool first = true;
+ TMaybe<NYT::TNode> firstNativeType;
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ if (first) {
+ first = false;
+ firstNativeType = pathInfo.GetNativeYtType();
+ }
+ requiresMapOrMerge = requiresMapOrMerge || pathInfo.Table->RequiresRemap()
+ || !IsSameAnnotation(*scheme, *pathInfo.Table->RowSpec->GetType())
+ || nativeTypeFlags != pathInfo.GetNativeYtTypeFlags()
+ || firstNativeType != pathInfo.GetNativeYtType();
+ hasRanges = hasRanges || pathInfo.Ranges;
+ hasNonTemp = hasNonTemp || !pathInfo.Table->IsTemp;
+ hasDynamic = hasDynamic || pathInfo.Table->Meta->IsDynamic;
+ }
+
+ if (!requiresMapOrMerge && !hasRanges && !hasSettings)
+ return node;
+
+ // Ignore DirectRead pragma for temporary tables and dynamic tables with sampling or ranges
+ if (!hasNonTemp || (hasDynamic && (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Sample) || hasRanges))) {
+ directRead = false;
+ }
+
+ if (directRead) {
+ return node;
+ }
+
+ bool keepSorted = ctx.IsConstraintEnabled<TSortedConstraintNode>()
+ ? (!NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered) && !hasNonTemp && section.Paths().Size() == 1) // single sorted input from operation
+ : (!hasDynamic || !NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip)); // compatibility - all except dynamic with limit
+ auto path = CopyOrTrivialMap(read.Pos(),
+ read.World(),
+ TYtDSink(ctx.RenameNode(read.DataSource().Ref(), "DataSink")),
+ *scheme,
+ Build<TYtSection>(ctx, section.Pos())
+ .Paths(section.Paths())
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::DirectRead | EYtSettingType::Unordered | EYtSettingType::NonUnique, ctx))
+ .Done(),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(keepSorted).SetSectionUniq(section.Ref().GetConstraint<TDistinctConstraintNode>()));
+
+ auto newData = path.Columns().Maybe<TCoVoid>() && path.Ranges().Maybe<TCoVoid>()
+ ? path.Table()
+ : Build<TCoRight>(ctx, resPull.Pos())
+ .Input<TYtReadTable>()
+ .World(ctx.NewWorld(resPull.Pos()))
+ .DataSource(read.DataSource())
+ .Input()
+ .Add()
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+
+ return ctx.ChangeChild(resPull.Ref(), TResPull::idx_Data, newData.Ptr());
+ }
+
+ struct TRangeBuildResult {
+ TVector<TString> Keys;
+ TSet<size_t> TableIndexes;
+ IPredicateRangeExtractor::TBuildResult BuildResult;
+ };
+
+ static TMaybeNode<TCoLambda> GetLambdaWithPredicate(TCoLambda lambda) {
+ if (auto innerFlatMap = lambda.Body().Maybe<TCoFlatMapBase>()) {
+ if (auto arg = innerFlatMap.Input().Maybe<TCoFilterNullMembersBase>().Input().Maybe<TCoJust>().Input()) {
+ if (arg.Cast().Raw() == lambda.Args().Arg(0).Raw()) {
+ lambda = innerFlatMap.Lambda().Cast();
+ }
+ }
+ }
+ if (!lambda.Body().Maybe<TCoConditionalValueBase>()) {
+ return {};
+ }
+ return lambda;
+ }
+
+ TMaybe<TVector<TRangeBuildResult>> ExtractKeyRangeFromLambda(TCoLambda lambda, TYtSection section, TExprContext& ctx) const {
+ YQL_ENSURE(lambda.Body().Maybe<TCoConditionalValueBase>().IsValid());
+
+ TMap<TVector<TString>, TSet<size_t>> tableIndexesBySortKey;
+ TMap<size_t, TString> tableNamesByIndex;
+ for (size_t tableIndex = 0; tableIndex < section.Paths().Size(); ++tableIndex) {
+ TYtPathInfo pathInfo(section.Paths().Item(tableIndex));
+ if (pathInfo.Ranges) {
+ return TVector<TRangeBuildResult>{};
+ }
+ TYtTableBaseInfo::TPtr tableInfo = pathInfo.Table;
+ if (tableInfo->RowSpec) {
+ auto rowSpec = tableInfo->RowSpec;
+ if (rowSpec->IsSorted()) {
+ YQL_ENSURE(rowSpec->SortMembers.size() <= rowSpec->SortDirections.size());
+ TVector<TString> keyPrefix;
+ for (size_t i = 0; i < rowSpec->SortMembers.size(); ++i) {
+ if (!rowSpec->SortDirections[i]) {
+ // TODO: allow native descending YT sort if UseYtKeyBounds is enabled
+ break;
+ }
+ keyPrefix.push_back(rowSpec->SortMembers[i]);
+ }
+ if (!keyPrefix.empty()) {
+ tableIndexesBySortKey[keyPrefix].insert(tableIndex);
+ }
+ tableNamesByIndex[tableIndex] = tableInfo->Name;
+ }
+ }
+ }
+ if (tableIndexesBySortKey.empty()) {
+ return TVector<TRangeBuildResult>{};
+ }
+
+ TPredicateExtractorSettings rangeSettings;
+ rangeSettings.MergeAdjacentPointRanges = State_->Configuration->MergeAdjacentPointRanges.Get().GetOrElse(DEFAULT_MERGE_ADJACENT_POINT_RANGES);
+ rangeSettings.HaveNextValueCallable = State_->Configuration->KeyFilterForStartsWith.Get().GetOrElse(DEFAULT_KEY_FILTER_FOR_STARTS_WITH);
+ rangeSettings.MaxRanges = State_->Configuration->MaxKeyRangeCount.Get().GetOrElse(DEFAULT_MAX_KEY_RANGE_COUNT);
+
+ THashSet<TString> possibleIndexKeys;
+ auto rowType = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+ auto extractor = MakePredicateRangeExtractor(rangeSettings);
+ if (!extractor->Prepare(lambda.Ptr(), *rowType, possibleIndexKeys, ctx, *State_->Types)) {
+ return Nothing();
+ }
+
+ TVector<TRangeBuildResult> results;
+ for (auto& [keys, tableIndexes] : tableIndexesBySortKey) {
+ if (AllOf(keys, [&possibleIndexKeys](const TString& key) { return !possibleIndexKeys.contains(key); })) {
+ continue;
+ }
+
+ TRangeBuildResult result;
+ result.Keys = keys;
+ result.TableIndexes = tableIndexes;
+ result.BuildResult = extractor->BuildComputeNode(keys, ctx);
+ auto& compute = result.BuildResult.ComputeNode;
+ if (compute) {
+ compute = ctx.NewCallable(compute->Pos(), "EvaluateExprIfPure", { compute });
+ results.push_back(result);
+
+ TVector<TString> tableNames;
+ for (const auto& idx : tableIndexes) {
+ YQL_ENSURE(tableNamesByIndex.contains(idx));
+ tableNames.push_back(tableNamesByIndex[idx]);
+ }
+
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__
+ << ": Will use key filter for tables [" << JoinSeq(",", tableNames) << "] with key columns ["
+ << JoinSeq(",", keys) << "]";
+ }
+ }
+
+ return results;
+ }
+
+ TExprNode::TPtr UpdateSectionWithKeyRanges(TPositionHandle pos, TYtSection section, const TVector<TRangeBuildResult>& results, TExprContext& ctx) const {
+ const bool sameSort = results.size() == 1 && results.front().TableIndexes.size() == section.Paths().Size();
+
+ auto newSettingsChildren = section.Settings().Ref().ChildrenList();
+
+ TExprNode::TListType updatedPaths = section.Paths().Ref().ChildrenList();
+ bool hasPathUpdates = false;
+ for (auto& result : results) {
+ TExprNodeList items = { result.BuildResult.ComputeNode };
+
+ TExprNodeList usedKeys;
+ YQL_ENSURE(result.BuildResult.UsedPrefixLen <= result.Keys.size());
+ for (size_t i = 0; i < result.BuildResult.UsedPrefixLen; ++i) {
+ usedKeys.push_back(ctx.NewAtom(pos, result.Keys[i]));
+ }
+ auto usedKeysNode = ctx.NewList(pos, std::move(usedKeys));
+ auto usedKeysSetting = ctx.NewList(pos, { ctx.NewAtom(pos, "usedKeys"), usedKeysNode });
+ items.push_back(ctx.NewList(pos, { usedKeysSetting }));
+
+ for (const auto& idx : result.TableIndexes) {
+ if (auto out = TYtPath(updatedPaths[idx]).Table().Maybe<TYtOutput>(); out && IsUnorderedOutput(out.Cast())) {
+ updatedPaths[idx] = Build<TYtPath>(ctx, updatedPaths[idx]->Pos())
+ .InitFrom(TYtPath(updatedPaths[idx]))
+ .Table<TYtOutput>()
+ .InitFrom(out.Cast())
+ .Mode(TMaybeNode<TCoAtom>())
+ .Build()
+ .Done().Ptr();
+ hasPathUpdates = true;
+ }
+ }
+
+ if (!sameSort) {
+ TExprNodeList idxs;
+ for (const auto& idx : result.TableIndexes) {
+ idxs.push_back(ctx.NewAtom(pos, idx));
+ }
+ items.push_back(ctx.NewList(pos, std::move(idxs)));
+ }
+ newSettingsChildren.push_back(
+ Build<TCoNameValueTuple>(ctx, pos)
+ .Name()
+ .Value(ToString(EYtSettingType::KeyFilter2))
+ .Build()
+ .Value(ctx.NewList(pos, std::move(items)))
+ .Done().Ptr());
+ }
+
+ auto newSection = ctx.ChangeChild(section.Ref(), TYtSection::idx_Settings,
+ ctx.NewList(section.Settings().Pos(), std::move(newSettingsChildren)));
+ if (hasPathUpdates) {
+ newSection = ctx.ChangeChild(*newSection, TYtSection::idx_Paths,
+ ctx.NewList(section.Paths().Pos(), std::move(updatedPaths)));
+ }
+ return newSection;
+ }
+
+ TMaybeNode<TExprBase> ExtractKeyRangeDqReadWrap(TExprBase node, TExprContext& ctx) const {
+ auto flatMap = node.Cast<TCoFlatMapBase>();
+ auto maybeYtRead = flatMap.Input().Maybe<TDqReadWrapBase>().Input().Maybe<TYtReadTable>();
+ if (!maybeYtRead) {
+ return node;
+ }
+ auto ytRead = maybeYtRead.Cast();
+ if (ytRead.Input().Size() > 1) {
+ return node;
+ }
+
+ auto section = ytRead.Input().Item(0);
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip)) {
+ return node;
+ }
+
+ auto maybeLambda = GetLambdaWithPredicate(flatMap.Lambda());
+ if (!maybeLambda) {
+ return node;
+ }
+ auto lambda = maybeLambda.Cast();
+ auto maybeResult = ExtractKeyRangeFromLambda(lambda, section, ctx);
+ if (!maybeResult) {
+ return {};
+ }
+ auto results = *maybeResult;
+ if (results.empty()) {
+ return node;
+ }
+ auto predPos = lambda.Body().Cast<TCoConditionalValueBase>().Predicate().Pos();
+ auto newSection = UpdateSectionWithKeyRanges(predPos, section, results, ctx);
+
+ auto newYtRead = Build<TYtReadTable>(ctx, ytRead.Pos())
+ .InitFrom(ytRead)
+ .Input()
+ .Add(newSection)
+ .Build()
+ .Done().Ptr();
+
+ auto newFlatMap = ctx.ChangeChild(flatMap.Ref(), TCoFlatMapBase::idx_Input,
+ ctx.ChangeChild(flatMap.Input().Ref(), TDqReadWrapBase::idx_Input, std::move(newYtRead))
+ );
+
+ const bool sameSort = results.size() == 1 && results.front().TableIndexes.size() == section.Paths().Size();
+ const bool pruneLambda = State_->Configuration->DqPruneKeyFilterLambda.Get().GetOrElse(DEFAULT_DQ_PRUNE_KEY_FILTER_LAMBDA);
+ if (sameSort && pruneLambda) {
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": Will prune key filter lambda";
+ newFlatMap = ctx.ReplaceNodes(std::move(newFlatMap), {{ lambda.Raw(), results.front().BuildResult.PrunedLambda }});
+ }
+
+ return newFlatMap;
+ }
+
+ TMaybeNode<TExprBase> ExtractKeyRange(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtTransientOpBase>();
+ if (op.Input().Size() > 1) {
+ // Extract key ranges before horizontal joins
+ return node;
+ }
+ if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ auto section = op.Input().Item(0);
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip)) {
+ return node;
+ }
+
+ TCoLambda mapper = op.Maybe<TYtMap>() ? op.Cast<TYtMap>().Mapper() : op.Cast<TYtMapReduce>().Mapper().Cast<TCoLambda>();
+ auto maybeLambda = GetFlatMapOverInputStream(mapper).Lambda();
+ if (!maybeLambda) {
+ return node;
+ }
+ maybeLambda = GetLambdaWithPredicate(maybeLambda.Cast());
+ if (!maybeLambda) {
+ return node;
+ }
+ auto lambda = maybeLambda.Cast();
+ auto maybeResult = ExtractKeyRangeFromLambda(lambda, section, ctx);
+ if (!maybeResult) {
+ return {};
+ }
+ auto results = *maybeResult;
+ if (results.empty()) {
+ return node;
+ }
+
+ auto predPos = lambda.Body().Cast<TCoConditionalValueBase>().Predicate().Pos();
+ auto newSection = UpdateSectionWithKeyRanges(predPos, section, results, ctx);
+
+ auto newOp = ctx.ChangeChild(op.Ref(),
+ TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(newSection)
+ .Done().Ptr()
+ );
+
+ const bool sameSort = results.size() == 1 && results.front().TableIndexes.size() == section.Paths().Size();
+ const bool pruneLambda = State_->Configuration->PruneKeyFilterLambda.Get().GetOrElse(DEFAULT_PRUNE_KEY_FILTER_LAMBDA);
+ if (sameSort && pruneLambda) {
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": Will prune key filter lambda";
+ newOp = ctx.ReplaceNodes(std::move(newOp), {{ lambda.Raw(), results.front().BuildResult.PrunedLambda }});
+ }
+
+ return newOp;
+ }
+
+ // All keyFilter settings are combined by OR.
+ // keyFilter value := '(<memberItem>+) <optional tableIndex>
+ // <memberItem> := '(<memberName> '(<cmpItem>+))
+ // <cmpItem> := '(<cmpOp> <value>)
+ TMaybeNode<TExprBase> ExtractKeyRangeLegacy(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtTransientOpBase>();
+ if (op.Input().Size() > 1) {
+ // Extract key ranges before horizontal joins
+ return node;
+ }
+ if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ auto section = op.Input().Item(0);
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2 | EYtSettingType::Take | EYtSettingType::Skip)) {
+ return node;
+ }
+
+ TYtSortMembersCollection sortMembers;
+ for (size_t tableIndex = 0; tableIndex < section.Paths().Size(); ++tableIndex) {
+ TYtPathInfo pathInfo(section.Paths().Item(tableIndex));
+ if (pathInfo.Ranges) {
+ return node;
+ }
+ TYtTableBaseInfo::TPtr tableInfo = pathInfo.Table;
+ if (tableInfo->RowSpec && tableInfo->RowSpec->IsSorted() && !tableInfo->RowSpec->SortMembers.empty()) {
+ sortMembers.AddTableInfo(tableIndex, tableInfo->Name,
+ tableInfo->RowSpec->SortMembers,
+ tableInfo->RowSpec->SortedByTypes,
+ tableInfo->RowSpec->SortDirections);
+ }
+ }
+ if (sortMembers.Empty()) {
+ return node;
+ }
+
+ TCoLambda mapper = op.Maybe<TYtMap>() ? op.Cast<TYtMap>().Mapper() : op.Cast<TYtMapReduce>().Mapper().Cast<TCoLambda>();
+ auto maybeLambda = GetFlatMapOverInputStream(mapper).Lambda();
+ if (!maybeLambda) {
+ return node;
+ }
+ TCoLambda lambda = maybeLambda.Cast();
+ if (auto innerFlatMap = lambda.Body().Maybe<TCoFlatMapBase>()) {
+ if (auto arg = innerFlatMap.Input().Maybe<TCoFilterNullMembersBase>().Input().Maybe<TCoJust>().Input()) {
+ if (arg.Cast().Raw() == lambda.Args().Arg(0).Raw()) {
+ lambda = innerFlatMap.Lambda().Cast();
+ }
+ }
+ }
+ if (!lambda.Body().Maybe<TCoConditionalValueBase>()) {
+ return node;
+ }
+ auto predicate = lambda.Body().Cast<TCoConditionalValueBase>().Predicate();
+ if (predicate.Ref().Type() != TExprNode::Callable) {
+ return node;
+ }
+
+ const size_t maxTables = State_->Configuration->MaxInputTables.Get().GetOrElse(DEFAULT_MAX_INPUT_TABLES);
+ TVector<TKeyFilterPredicates> ranges;
+ if (!CollectKeyPredicates(lambda.Args().Arg(0), predicate, ranges, maxTables)) {
+ return node;
+ }
+
+ if (ranges.size() > maxTables) {
+ YQL_CLOG(DEBUG, ProviderYt) << __FUNCTION__ << ": too many tables - " << ranges.size();
+ return node;
+ }
+
+ if (!sortMembers.ApplyRanges(ranges, ctx)) {
+ return {};
+ }
+
+ if (sortMembers.Empty()) {
+ return node;
+ }
+
+ auto newSettingsChildren = section.Settings().Ref().ChildrenList();
+ sortMembers.BuildKeyFilters(predicate.Pos(), section.Paths().Size(), ranges.size(), newSettingsChildren, ctx);
+
+ auto newSection = ctx.ChangeChild(section.Ref(), TYtSection::idx_Settings,
+ ctx.NewList(section.Settings().Pos(), std::move(newSettingsChildren)));
+ return ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(newSection)
+ .Done().Ptr()
+ );
+ }
+
+ template <typename TLMapType>
+ TMaybeNode<TExprBase> LMap(TExprBase node, TExprContext& ctx) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto lmap = node.Cast<TLMapType>();
+
+ if (!IsYtProviderInput(lmap.Input(), true)) {
+ return node;
+ }
+
+ const auto inItemType = GetSequenceItemType(lmap.Input(), true, ctx);
+ if (!inItemType) {
+ return {};
+ }
+ const auto outItemType = SilentGetSequenceItemType(lmap.Lambda().Body().Ref(), true);
+ if (!outItemType || !outItemType->IsPersistable()) {
+ return node;
+ }
+
+ auto cluster = TString{GetClusterName(lmap.Input())};
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(lmap.Lambda().Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ auto cleanup = CleanupWorld(lmap.Lambda(), ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ auto mapper = cleanup.Cast().Ptr();
+ bool sortedOutput = false;
+ TVector<TYtOutTable> outTables = ConvertOutTablesWithSortAware(mapper, sortedOutput, lmap.Pos(),
+ outItemType, ctx, State_, lmap.Ref().GetConstraintSet());
+
+ const bool useFlow = State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW);
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, lmap.Pos());
+ if (std::is_same<TLMapType, TCoOrderedLMap>::value) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ }
+
+ if (useFlow) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ auto map = Build<TYtMap>(ctx, lmap.Pos())
+ .World(ApplySyncListToWorld(GetWorld(lmap.Input(), {}, ctx).Ptr(), syncList, ctx))
+ .DataSink(GetDataSink(lmap.Input(), ctx))
+ .Input(ConvertInputTable(lmap.Input(), ctx))
+ .Output()
+ .Add(outTables)
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(MakeJobLambda<false>(TCoLambda(mapper), useFlow, ctx))
+ .Done();
+
+ return WrapOp(map, ctx);
+ }
+
+ TMaybeNode<TExprBase> FuseInnerMap(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto outerMap = node.Cast<TYtMap>();
+ if (outerMap.Input().Size() != 1 || outerMap.Input().Item(0).Paths().Size() != 1) {
+ return node;
+ }
+
+ TYtPath path = outerMap.Input().Item(0).Paths().Item(0);
+ auto maybeInnerMap = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtMap>();
+ if (!maybeInnerMap) {
+ return node;
+ }
+ TYtMap innerMap = maybeInnerMap.Cast();
+
+ if (innerMap.Ref().StartsExecution() || innerMap.Ref().HasResult()) {
+ return node;
+ }
+ if (innerMap.Output().Size() > 1) {
+ return node;
+ }
+ if (outerMap.DataSink().Cluster().Value() != innerMap.DataSink().Cluster().Value()) {
+ return node;
+ }
+ if (NYql::HasAnySetting(innerMap.Settings().Ref(), EYtSettingType::Limit | EYtSettingType::SortLimitBy | EYtSettingType::JobCount)) {
+ return node;
+ }
+ if (NYql::HasAnySetting(outerMap.Input().Item(0).Settings().Ref(),
+ EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::DirectRead | EYtSettingType::Sample | EYtSettingType::SysColumns))
+ {
+ return node;
+ }
+ if (NYql::HasSetting(innerMap.Settings().Ref(), EYtSettingType::Flow) != NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Flow)) {
+ return node;
+ }
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ if (NYql::HasNonEmptyKeyFilter(outerMap.Input().Item(0))) {
+ return node;
+ }
+
+ const TParentsMap* parentsMap = getParents();
+ if (IsOutputUsedMultipleTimes(innerMap.Ref(), *parentsMap)) {
+ // Inner map output is used more than once
+ return node;
+ }
+ // Check world dependencies
+ auto parentsIt = parentsMap->find(innerMap.Raw());
+ YQL_ENSURE(parentsIt != parentsMap->cend());
+ for (auto dep: parentsIt->second) {
+ if (!TYtOutput::Match(dep)) {
+ return node;
+ }
+ }
+
+ auto innerLambda = innerMap.Mapper();
+ auto outerLambda = outerMap.Mapper();
+ if (HasYtRowNumber(outerLambda.Body().Ref())) {
+ return node;
+ }
+
+ auto fuseRes = CanFuseLambdas(innerLambda, outerLambda, ctx);
+ if (!fuseRes) {
+ // Some error
+ return {};
+ }
+ if (!*fuseRes) {
+ // Cannot fuse
+ return node;
+ }
+
+ const bool unorderedOut = IsUnorderedOutput(path.Table().Cast<TYtOutput>());
+
+ auto [placeHolder, lambdaWithPlaceholder] = ReplaceDependsOn(outerLambda.Ptr(), ctx, State_->Types);
+ if (!placeHolder) {
+ return {};
+ }
+
+ if (lambdaWithPlaceholder != outerLambda.Ptr()) {
+ outerLambda = TCoLambda(lambdaWithPlaceholder);
+ }
+
+ innerLambda = FallbackLambdaOutput(innerLambda, ctx);
+ if (unorderedOut) {
+ innerLambda = Build<TCoLambda>(ctx, innerLambda.Pos())
+ .Args({"stream"})
+ .Body<TCoUnordered>()
+ .Input<TExprApplier>()
+ .Apply(innerLambda)
+ .With(0, "stream")
+ .Build()
+ .Build()
+ .Done();
+ }
+ outerLambda = FallbackLambdaInput(outerLambda, ctx);
+
+ if (!path.Columns().Maybe<TCoVoid>()) {
+ const bool ordered = !unorderedOut && NYql::HasSetting(innerMap.Settings().Ref(), EYtSettingType::Ordered)
+ && NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Ordered);
+ outerLambda = MapEmbedInputFieldsFilter(outerLambda, ordered, path.Columns().Cast<TCoAtomList>(), ctx);
+ } else if (TYqlRowSpecInfo(innerMap.Output().Item(0).RowSpec()).HasAuxColumns()) {
+ auto itemType = GetSequenceItemType(path, false, ctx);
+ if (!itemType) {
+ return {};
+ }
+ TSet<TStringBuf> fields;
+ for (auto item: itemType->Cast<TStructExprType>()->GetItems()) {
+ fields.insert(item->GetName());
+ }
+ const bool ordered = !unorderedOut && NYql::HasSetting(innerMap.Settings().Ref(), EYtSettingType::Ordered)
+ && NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Ordered);
+ outerLambda = MapEmbedInputFieldsFilter(outerLambda, ordered, TCoAtomList(ToAtomList(fields, node.Pos(), ctx)), ctx);
+ }
+
+ const auto mergedSettings = MergeSettings(
+ *NYql::RemoveSettings(outerMap.Settings().Ref(), EYtSettingType::Flow, ctx),
+ *NYql::RemoveSettings(innerMap.Settings().Ref(), EYtSettingType::Ordered | EYtSettingType::KeepSorted, ctx), ctx);
+
+ return Build<TYtMap>(ctx, node.Pos())
+ .InitFrom(outerMap)
+ .World<TCoSync>()
+ .Add(innerMap.World())
+ .Add(outerMap.World())
+ .Build()
+ .Input(innerMap.Input())
+ .Mapper()
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(outerLambda)
+ .With<TExprApplier>(0)
+ .Apply(innerLambda)
+ .With(0, "stream")
+ .Build()
+ .With(TExprBase(placeHolder), "stream")
+ .Build()
+ .Build()
+ .Settings(mergedSettings)
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> FuseOuterMap(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto outerMap = node.Cast<TYtMap>();
+ if (outerMap.Input().Size() != 1 || outerMap.Input().Item(0).Paths().Size() != 1) {
+ return node;
+ }
+
+ TYtPath path = outerMap.Input().Item(0).Paths().Item(0);
+ auto maybeInner = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtWithUserJobsOpBase>();
+ if (!maybeInner) {
+ return node;
+ }
+ if (!maybeInner.Maybe<TYtReduce>() && !maybeInner.Maybe<TYtMapReduce>()) {
+ return node;
+ }
+ auto inner = maybeInner.Cast();
+
+ if (inner.Ref().StartsExecution() || inner.Ref().HasResult()) {
+ return node;
+ }
+ if (inner.Output().Size() > 1) {
+ return node;
+ }
+ if (outerMap.DataSink().Cluster().Value() != inner.DataSink().Cluster().Value()) {
+ return node;
+ }
+ if (NYql::HasAnySetting(inner.Settings().Ref(), EYtSettingType::Limit | EYtSettingType::SortLimitBy | EYtSettingType::JobCount)) {
+ return node;
+ }
+ if (outerMap.Input().Item(0).Settings().Size() != 0) {
+ return node;
+ }
+ if (NYql::HasSetting(inner.Settings().Ref(), EYtSettingType::Flow) != NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Flow)) {
+ return node;
+ }
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ return node;
+ }
+ if (inner.Maybe<TYtMapReduce>()) {
+ for (auto out: outerMap.Output()) {
+ if (TYqlRowSpecInfo(out.RowSpec()).IsSorted()) {
+ return node;
+ }
+ }
+ }
+
+ const TParentsMap* parentsMap = getParents();
+ if (IsOutputUsedMultipleTimes(inner.Ref(), *parentsMap)) {
+ // Inner output is used more than once
+ return node;
+ }
+ // Check world dependencies
+ auto parentsIt = parentsMap->find(inner.Raw());
+ YQL_ENSURE(parentsIt != parentsMap->cend());
+ for (auto dep: parentsIt->second) {
+ if (!TYtOutput::Match(dep)) {
+ return node;
+ }
+ }
+
+ auto outerLambda = outerMap.Mapper();
+ if (HasYtRowNumber(outerLambda.Body().Ref())) {
+ return node;
+ }
+
+ auto lambda = inner.Maybe<TYtMapReduce>() ? inner.Cast<TYtMapReduce>().Reducer() : inner.Cast<TYtReduce>().Reducer();
+
+ auto fuseRes = CanFuseLambdas(lambda, outerLambda, ctx);
+ if (!fuseRes) {
+ // Some error
+ return {};
+ }
+ if (!*fuseRes) {
+ // Cannot fuse
+ return node;
+ }
+
+ auto [placeHolder, lambdaWithPlaceholder] = ReplaceDependsOn(outerLambda.Ptr(), ctx, State_->Types);
+ if (!placeHolder) {
+ return {};
+ }
+
+ if (lambdaWithPlaceholder != outerLambda.Ptr()) {
+ outerLambda = TCoLambda(lambdaWithPlaceholder);
+ }
+
+ lambda = FallbackLambdaOutput(lambda, ctx);
+ outerLambda = FallbackLambdaInput(outerLambda, ctx);
+
+ if (!path.Columns().Maybe<TCoVoid>()) {
+ const bool ordered = inner.Maybe<TYtReduce>() && TYqlRowSpecInfo(inner.Output().Item(0).RowSpec()).IsSorted()
+ && NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Ordered);
+
+ outerLambda = MapEmbedInputFieldsFilter(outerLambda, ordered, path.Columns().Cast<TCoAtomList>(), ctx);
+ } else if (inner.Maybe<TYtReduce>() && TYqlRowSpecInfo(inner.Output().Item(0).RowSpec()).HasAuxColumns()) {
+ auto itemType = GetSequenceItemType(path, false, ctx);
+ if (!itemType) {
+ return {};
+ }
+ TSet<TStringBuf> fields;
+ for (auto item: itemType->Cast<TStructExprType>()->GetItems()) {
+ fields.insert(item->GetName());
+ }
+ const bool ordered = NYql::HasSetting(outerMap.Settings().Ref(), EYtSettingType::Ordered);
+ outerLambda = MapEmbedInputFieldsFilter(outerLambda, ordered, TCoAtomList(ToAtomList(fields, node.Pos(), ctx)), ctx);
+ }
+
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(outerLambda)
+ .With<TExprApplier>(0)
+ .Apply(lambda)
+ .With(0, "stream")
+ .Build()
+ .With(TExprBase(placeHolder), "stream")
+ .Build()
+ .Done();
+
+ auto res = ctx.ChangeChild(inner.Ref(),
+ inner.Maybe<TYtMapReduce>() ? TYtMapReduce::idx_Reducer : TYtReduce::idx_Reducer,
+ lambda.Ptr());
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_Output, outerMap.Output().Ptr());
+
+ auto mergedSettings = NYql::RemoveSettings(outerMap.Settings().Ref(), EYtSettingType::Ordered | EYtSettingType::Sharded | EYtSettingType::Flow, ctx);
+ mergedSettings = MergeSettings(inner.Settings().Ref(), *mergedSettings, ctx);
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_Settings, std::move(mergedSettings));
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_World,
+ Build<TCoSync>(ctx, inner.Pos())
+ .Add(inner.World())
+ .Add(outerMap.World())
+ .Done().Ptr());
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> AssumeSorted(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto assume = node.Cast<TCoAssumeSorted>();
+ auto input = assume.Input();
+ if (!IsYtProviderInput(input)) {
+ return node;
+ }
+
+ auto sorted = node.Ref().GetConstraint<TSortedConstraintNode>();
+ if (!sorted) {
+ // Drop AssumeSorted with unsupported sort modes
+ return input;
+ }
+
+ auto maybeOp = input.Maybe<TYtOutput>().Operation();
+ bool needSeparateOp = !maybeOp
+ || maybeOp.Raw()->StartsExecution()
+ || (maybeOp.Raw()->HasResult() && maybeOp.Raw()->GetResult().Type() == TExprNode::World)
+ || IsOutputUsedMultipleTimes(maybeOp.Ref(), *getParents())
+ || maybeOp.Maybe<TYtMapReduce>()
+ || maybeOp.Maybe<TYtEquiJoin>();
+
+ bool canMerge = false;
+ bool equalSort = false;
+ if (auto inputSort = input.Ref().GetConstraint<TSortedConstraintNode>()) {
+ if (sorted->IsPrefixOf(*inputSort)) {
+ canMerge = true;
+ equalSort = sorted->Equals(*inputSort);
+ }
+ }
+ if (equalSort && maybeOp.Maybe<TYtSort>()) {
+ return input;
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(node, false, ctx)) {
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ const bool useNativeDescSort = State_->Configuration->UseNativeDescSort.Get().GetOrElse(DEFAULT_USE_NATIVE_DESC_SORT);
+
+ TKeySelectorBuilder builder(assume.Pos(), ctx, useNativeDescSort, outItemType);
+ builder.ProcessConstraint(*sorted);
+ needSeparateOp = needSeparateOp || (builder.NeedMap() && !equalSort);
+
+ if (needSeparateOp) {
+ TYtOutTableInfo outTable(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTable.RowSpec->SetConstraints(assume.Ref().GetConstraintSet());
+
+ if (auto maybeReadSettings = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>().Input().Item(0).Settings()) {
+ if (NYql::HasSetting(maybeReadSettings.Ref(), EYtSettingType::SysColumns)) {
+ canMerge = false;
+ }
+ }
+ auto inputPaths = GetInputPaths(input);
+ TMaybe<NYT::TNode> firstNativeType;
+ if (!inputPaths.empty()) {
+ firstNativeType = inputPaths.front()->GetNativeYtType();
+ }
+
+ canMerge = canMerge && AllOf(inputPaths, [&outTable, firstNativeType] (const TYtPathInfo::TPtr& path) {
+ return !path->RequiresRemap()
+ && path->GetNativeYtTypeFlags() == outTable.RowSpec->GetNativeYtTypeFlags()
+ && firstNativeType == path->GetNativeYtType();
+ });
+ if (canMerge) {
+ outTable.RowSpec->CopySortness(*inputPaths.front()->Table->RowSpec, TYqlRowSpecInfo::ECopySort::WithDesc);
+ outTable.RowSpec->ClearSortness(sorted->GetContent().size());
+ outTable.SetUnique(assume.Ref().GetConstraint<TDistinctConstraintNode>(), assume.Pos(), ctx);
+ if (firstNativeType) {
+ outTable.RowSpec->CopyTypeOrders(*firstNativeType);
+ }
+
+ YQL_ENSURE(sorted->GetContent().size() == outTable.RowSpec->SortMembers.size());
+ const bool useExplicitColumns = AnyOf(inputPaths, [] (const TYtPathInfo::TPtr& path) {
+ return !path->Table->IsTemp || (path->Table->RowSpec && path->Table->RowSpec->HasAuxColumns());
+ });
+
+ TConvertInputOpts opts;
+ if (useExplicitColumns) {
+ opts.ExplicitFields(*outTable.RowSpec, assume.Pos(), ctx);
+ }
+
+ return Build<TYtOutput>(ctx, assume.Pos())
+ .Operation<TYtMerge>()
+ .World(GetWorld(input, {}, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx, opts))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, assume.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeepSorted))
+ .Build()
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+ else {
+ builder.FillRowSpecSort(*outTable.RowSpec);
+ outTable.SetUnique(assume.Ref().GetConstraint<TDistinctConstraintNode>(), assume.Pos(), ctx);
+
+ TCoLambda mapper = builder.NeedMap()
+ ? Build<TCoLambda>(ctx, assume.Pos())
+ .Args({"stream"})
+ .Body<TExprApplier>()
+ .Apply(TCoLambda(builder.MakeRemapLambda(true)))
+ .With(0, "stream")
+ .Build()
+ .Done()
+ : Build<TCoLambda>(ctx, assume.Pos())
+ .Args({"stream"})
+ .Body("stream")
+ .Done();
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, assume.Pos());
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::KeepSorted))
+ .Build()
+ .Build()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Ordered))
+ .Build()
+ .Build();
+ if (State_->Configuration->UseFlow.Get().GetOrElse(DEFAULT_USE_FLOW)) {
+ settingsBuilder
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::Flow))
+ .Build()
+ .Build();
+ }
+
+ return Build<TYtOutput>(ctx, assume.Pos())
+ .Operation<TYtMap>()
+ .World(GetWorld(input, {}, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(ConvertInputTable(input, ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, assume.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(settingsBuilder.Done())
+ .Mapper(mapper)
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+ }
+
+ auto op = GetOutputOp(input.Cast<TYtOutput>());
+ TExprNode::TPtr newOp = op.Ptr();
+ if (!op.Maybe<TYtSort>()) {
+ if (auto settings = op.Maybe<TYtTransientOpBase>().Settings()) {
+ if (!NYql::HasSetting(settings.Ref(), EYtSettingType::KeepSorted)) {
+ newOp = ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Settings, NYql::AddSetting(settings.Ref(), EYtSettingType::KeepSorted, {}, ctx));
+ }
+ } else if (auto settings = op.Maybe<TYtFill>().Settings()) {
+ if (!NYql::HasSetting(settings.Ref(), EYtSettingType::KeepSorted)) {
+ newOp = ctx.ChangeChild(op.Ref(), TYtFill::idx_Settings, NYql::AddSetting(settings.Ref(), EYtSettingType::KeepSorted, {}, ctx));
+ }
+ }
+ }
+ if (!equalSort) {
+ const size_t index = FromString(input.Cast<TYtOutput>().OutIndex().Value());
+ TYtOutTableInfo outTable(op.Output().Item(index));
+ builder.FillRowSpecSort(*outTable.RowSpec);
+ outTable.RowSpec->SetConstraints(assume.Ref().GetConstraintSet());
+ outTable.SetUnique(assume.Ref().GetConstraint<TDistinctConstraintNode>(), assume.Pos(), ctx);
+
+ TVector<TYtOutTable> outputs;
+ for (size_t i = 0; i < op.Output().Size(); ++i) {
+ if (index == i) {
+ outputs.push_back(outTable.ToExprNode(ctx, op.Pos()).Cast<TYtOutTable>());
+ } else {
+ outputs.push_back(op.Output().Item(i));
+ }
+ }
+
+ newOp = ctx.ChangeChild(*newOp, TYtOutputOpBase::idx_Output, Build<TYtOutSection>(ctx, op.Pos()).Add(outputs).Done().Ptr());
+ }
+
+ return Build<TYtOutput>(ctx, assume.Pos())
+ .Operation(newOp)
+ .OutIndex(input.Cast<TYtOutput>().OutIndex())
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> LambdaFieldsSubset(TYtWithUserJobsOpBase op, size_t lambdaIdx, TExprContext& ctx, const TGetParents& getParents) const {
+ auto lambda = TCoLambda(op.Ref().ChildPtr(lambdaIdx));
+
+ bool hasUpdates = false;
+ TYtSection section = op.Input().Item(0);
+
+ const TParentsMap* parentsMap = getParents();
+ auto parents = parentsMap->find(lambda.Args().Arg(0).Raw());
+ if (parents == parentsMap->cend()) {
+ // Argument is not used in lambda body
+ return op;
+ }
+ if (parents->second.size() == 1 && TCoExtractMembers::Match(*parents->second.begin())) {
+ auto members = TCoExtractMembers(*parents->second.begin()).Members();
+ TSet<TStringBuf> memberSet;
+ std::for_each(members.begin(), members.end(), [&memberSet](const auto& m) { memberSet.insert(m.Value()); });
+ auto reduceBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::ReduceBy);
+ memberSet.insert(reduceBy.cbegin(), reduceBy.cend());
+ auto sortBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::SortBy);
+ memberSet.insert(sortBy.cbegin(), sortBy.cend());
+
+ auto itemType = GetSeqItemType(lambda.Args().Arg(0).Ref().GetTypeAnn())->Cast<TStructExprType>();
+ if (memberSet.size() < itemType->GetSize()) {
+ section = UpdateInputFields(section, std::move(memberSet), ctx, NYql::HasSetting(op.Settings().Ref(), EYtSettingType::WeakFields));
+ hasUpdates = true;
+ }
+ }
+
+ if (!hasUpdates) {
+ return op;
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(section)
+ .Done().Ptr());
+
+ res = ctx.ChangeChild(*res, lambdaIdx, ctx.DeepCopyLambda(lambda.Ref()));
+
+ return TExprBase(res);
+
+ }
+
+ TMaybeNode<TExprBase> MapFieldsSubset(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() != 1) {
+ return node;
+ }
+ if (auto map = op.Maybe<TYtMap>()) {
+ return LambdaFieldsSubset(op, TYtMap::idx_Mapper, ctx, getParents);
+ } else if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>()) {
+ return LambdaFieldsSubset(op, TYtMapReduce::idx_Mapper, ctx, getParents);
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> ReduceFieldsSubset(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() != 1) {
+ return node;
+ }
+ if (auto reduce = op.Maybe<TYtReduce>()) {
+ return LambdaFieldsSubset(op, TYtReduce::idx_Reducer, ctx, getParents);
+ } else if (!op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>()) {
+ return LambdaFieldsSubset(op, TYtMapReduce::idx_Reducer, ctx, getParents);
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> LambdaVisitFieldsSubset(TYtWithUserJobsOpBase op, size_t lambdaIdx, TExprContext& ctx, const TGetParents& getParents) const {
+ auto opLambda = TCoLambda(op.Ref().ChildPtr(lambdaIdx));
+
+ const TParentsMap* parentsMap = getParents();
+ auto maybeLambda = GetFlatMapOverInputStream(opLambda, *parentsMap).Lambda();
+ if (!maybeLambda) {
+ return op;
+ }
+
+ TCoLambda lambda = maybeLambda.Cast();
+ auto arg = lambda.Args().Arg(0);
+
+ // Check arg is used only in Visit
+ auto it = parentsMap->find(arg.Raw());
+ if (it == parentsMap->cend() || it->second.size() != 1 || !TCoVisit::Match(*it->second.begin())) {
+ return op;
+ }
+
+ const TExprNode* visit = *it->second.begin();
+ TVector<std::pair<size_t, TSet<TStringBuf>>> sectionFields;
+ for (ui32 index = 1; index < visit->ChildrenSize(); ++index) {
+ if (visit->Child(index)->IsAtom()) {
+ size_t inputNum = FromString<size_t>(visit->Child(index)->Content());
+ YQL_ENSURE(inputNum < op.Input().Size());
+
+ ++index;
+ auto visitLambda = visit->ChildPtr(index);
+
+ TSet<TStringBuf> memberSet;
+ if (HaveFieldsSubset(visitLambda->TailPtr(), visitLambda->Head().Head(), memberSet, *parentsMap)) {
+ auto reduceBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::ReduceBy);
+ memberSet.insert(reduceBy.cbegin(), reduceBy.cend());
+ auto sortBy = NYql::GetSettingAsColumnList(op.Settings().Ref(), EYtSettingType::SortBy);
+ memberSet.insert(sortBy.cbegin(), sortBy.cend());
+
+ auto itemType = visitLambda->Head().Head().GetTypeAnn()->Cast<TStructExprType>();
+ if (memberSet.size() < itemType->GetSize()) {
+ sectionFields.emplace_back(inputNum, std::move(memberSet));
+ }
+ }
+ }
+ }
+
+ if (sectionFields.empty()) {
+ return op;
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), lambdaIdx, ctx.DeepCopyLambda(opLambda.Ref()));
+
+ TVector<TYtSection> updatedSections(op.Input().begin(), op.Input().end());
+ const bool hasWeak = NYql::HasSetting(op.Settings().Ref(), EYtSettingType::WeakFields);
+ for (auto& pair: sectionFields) {
+ auto& section = updatedSections[pair.first];
+ auto& memberSet = pair.second;
+ section = UpdateInputFields(section, std::move(memberSet), ctx, hasWeak);
+ }
+
+ res = ctx.ChangeChild(*res, TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done().Ptr());
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> MultiMapFieldsSubset(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() < 2) {
+ return node;
+ }
+ if (auto map = op.Maybe<TYtMap>()) {
+ return LambdaVisitFieldsSubset(op, TYtMap::idx_Mapper, ctx, getParents);
+ } else if (op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>()) {
+ return LambdaVisitFieldsSubset(op, TYtMapReduce::idx_Mapper, ctx, getParents);
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> MultiReduceFieldsSubset(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Input().Size() < 2) {
+ return node;
+ }
+ if (auto reduce = op.Maybe<TYtReduce>()) {
+ return LambdaVisitFieldsSubset(op, TYtReduce::idx_Reducer, ctx, getParents);
+ } else if (!op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>()) {
+ return LambdaVisitFieldsSubset(op, TYtMapReduce::idx_Reducer, ctx, getParents);
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> WeakFields(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+
+ if (op.Input().Size() > 1) {
+ return node;
+ }
+
+ if (NYql::HasSetting(op.Settings().Ref(), EYtSettingType::WeakFields)) {
+ return node;
+ }
+
+ auto section = op.Input().Item(0);
+ auto inputType = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ if (!inputType->FindItem(YqlOthersColumnName)) {
+ return node;
+ }
+
+ for (auto path: section.Paths()) {
+ TYtTableBaseInfo::TPtr info = TYtTableBaseInfo::Parse(path.Table());
+ if (!info->RowSpec || info->RowSpec->StrictSchema || info->Meta->Attrs.contains(QB2Premapper)) {
+ return node;
+ }
+ }
+
+ TMaybeNode<TCoLambda> maybeMapper;
+ if (auto map = op.Maybe<TYtMap>()) {
+ maybeMapper = map.Mapper();
+ } else {
+ maybeMapper = op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoLambda>();
+ }
+ if (!maybeMapper) {
+ return node;
+ }
+ auto mapper = maybeMapper.Cast();
+ auto mapperIdx = op.Maybe<TYtMap>() ? TYtMap::idx_Mapper : TYtMapReduce::idx_Mapper;
+
+ auto lambdaBody = mapper.Body();
+ if (!lambdaBody.Maybe<TCoFlatMapBase>() && !lambdaBody.Maybe<TCoCombineCore>()) {
+ return node;
+ }
+
+ TVector<TExprBase> stack{lambdaBody};
+ auto input = lambdaBody.Cast<TCoInputBase>().Input();
+ while (input.Raw() != mapper.Args().Arg(0).Raw()) {
+ TMaybe<THashSet<TStringBuf>> passthroughFields;
+ if (!input.Maybe<TCoFlatMapBase>()
+ || !IsPassthroughFlatMap(input.Cast<TCoFlatMapBase>(), &passthroughFields)
+ || (passthroughFields && !passthroughFields->contains(YqlOthersColumnName)))
+ {
+ return node;
+ }
+ stack.push_back(input);
+ input = input.Cast<TCoFlatMapBase>().Input();
+ }
+
+ auto getMemberColumn = [] (const TExprNode* node, const TExprNode* arg) {
+ if (auto maybeMember = TMaybeNode<TCoMember>(node)) {
+ if (maybeMember.Cast().Struct().Raw() == arg) {
+ return maybeMember.Cast().Name().Value();
+ }
+ }
+ return TStringBuf();
+ };
+
+ THashMap<const TExprNode*, TExprNode::TPtr> weaks; // map TryWeakMemberFromDict -> row argument
+ THashSet<const TExprNode*> otherMembers;
+ TSet<TStringBuf> finalWeakColumns;
+ const TParentsMap* parentsMap = getParents();
+ for (size_t pass: xrange(stack.size())) {
+ auto consumer = stack[pass];
+
+ TExprNode::TListType rowArgs;
+ if (auto maybeCombineCore = consumer.Maybe<TCoCombineCore>()) {
+ auto combineCore = maybeCombineCore.Cast();
+ rowArgs.push_back(combineCore.KeyExtractor().Args().Arg(0).Ptr());
+ rowArgs.push_back(combineCore.InitHandler().Args().Arg(1).Ptr());
+ rowArgs.push_back(combineCore.UpdateHandler().Args().Arg(1).Ptr());
+ }
+ else {
+ rowArgs.push_back(consumer.Cast<TCoFlatMapBase>().Lambda().Args().Arg(0).Ptr());
+ }
+
+ for (const auto& rowArg : rowArgs) {
+ auto rowArgParentsIt = parentsMap->find(rowArg.Get());
+ YQL_ENSURE(rowArgParentsIt != parentsMap->end());
+ for (const auto& memberNode: rowArgParentsIt->second) {
+ if (auto column = getMemberColumn(memberNode, rowArg.Get())) {
+ if (column != YqlOthersColumnName) {
+ continue;
+ }
+ } else {
+ return node;
+ }
+
+ if (pass > 0) {
+ otherMembers.insert(memberNode);
+ }
+
+ auto justArgIt = parentsMap->find(memberNode);
+ YQL_ENSURE(justArgIt != parentsMap->end());
+ for (const auto& justNode: justArgIt->second) {
+ if (!justNode->IsCallable("Just") || justNode->Child(0) != memberNode) {
+ if (pass > 0) {
+ continue;
+ }
+ return node;
+ }
+
+ auto weakIt = parentsMap->find(justNode);
+ YQL_ENSURE(weakIt != parentsMap->end());
+ for (const auto& weakNode : weakIt->second) {
+ if (!weakNode->IsCallable("TryWeakMemberFromDict") || weakNode->Child(0) != justNode) {
+ if (pass > 0) {
+ continue;
+ }
+ return node;
+ }
+
+ weaks.insert(std::make_pair(weakNode, rowArg));
+ if (pass == 0) {
+ finalWeakColumns.insert(weakNode->Child(3)->Content());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ TSet<TStringBuf> filteredFields;
+ for (auto item: inputType->GetItems()) {
+ if (item->GetName() != YqlOthersColumnName) {
+ filteredFields.insert(item->GetName());
+ }
+ }
+
+ TSet<TStringBuf> weakFields;
+ TExprNode::TPtr newLambda;
+ TOptimizeExprSettings settings(State_->Types);
+ settings.VisitChanges = true;
+ auto status = OptimizeExpr(mapper.Ptr(), newLambda, [&](const TExprNode::TPtr& input, TExprContext& ctx) {
+ if (auto maybeTryWeak = TMaybeNode<TCoTryWeakMemberFromDict>(input)) {
+ auto it = weaks.find(input.Get());
+ if (it == weaks.end()) {
+ return input;
+ }
+ auto tryWeak = maybeTryWeak.Cast();
+ auto weakName = tryWeak.Name().Value();
+ if (!filteredFields.contains(weakName)) {
+ weakFields.insert(weakName);
+ }
+
+ TExprBase member = Build<TCoMember>(ctx, input->Pos())
+ .Struct(it->second)
+ .Name(tryWeak.Name())
+ .Done();
+
+ const TStructExprType* structType = it->second->GetTypeAnn()->Cast<TStructExprType>();
+ auto structMemberPos = structType->FindItem(weakName);
+ bool notYsonMember = false;
+ if (structMemberPos) {
+ auto structMemberType = structType->GetItems()[*structMemberPos]->GetItemType();
+ if (structMemberType->GetKind() == ETypeAnnotationKind::Optional) {
+ structMemberType = structMemberType->Cast<TOptionalExprType>()->GetItemType();
+ }
+ if (structMemberType->GetKind() != ETypeAnnotationKind::Data) {
+ notYsonMember = true;
+ } else {
+ auto structMemberSlot = structMemberType->Cast<TDataExprType>()->GetSlot();
+ if (structMemberSlot != EDataSlot::Yson && structMemberSlot != EDataSlot::String) {
+ notYsonMember = true;
+ }
+ }
+ }
+
+ TExprBase fromYson = (notYsonMember || tryWeak.Type().Value() == "Yson")
+ ? member
+ : Build<TCoFromYsonSimpleType>(ctx, input->Pos())
+ .Value(member)
+ .Type(tryWeak.Type())
+ .Done();
+
+ if (tryWeak.RestDict().Maybe<TCoNothing>()) {
+ return fromYson.Ptr();
+ }
+
+ return Build<TCoCoalesce>(ctx, input->Pos())
+ .Predicate(fromYson)
+ .Value<TCoTryWeakMemberFromDict>()
+ .InitFrom(tryWeak)
+ .OtherDict<TCoNull>()
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+
+ if (stack.size() > 1) {
+ if (auto maybeStruct = TMaybeNode<TCoAsStruct>(input)) {
+ auto asStruct = maybeStruct.Cast();
+ for (size_t i: xrange(asStruct.ArgCount())) {
+ auto list = asStruct.Arg(i);
+ if (list.Item(0).Cast<TCoAtom>().Value() == YqlOthersColumnName && otherMembers.contains(list.Item(1).Raw())) {
+ // rebuild AsStruct without other fields
+ auto row = list.Item(1).Cast<TCoMember>().Struct();
+ auto newChildren = input->ChildrenList();
+ newChildren.erase(newChildren.begin() + i);
+ // and with weak fields for combiner core
+ for (ui32 j = 0; j < newChildren.size(); ++j) {
+ finalWeakColumns.erase(newChildren[j]->Child(0)->Content());
+ }
+
+ for (auto column : finalWeakColumns) {
+ newChildren.push_back(Build<TExprList>(ctx, input->Pos())
+ .Add<TCoAtom>()
+ .Value(column)
+ .Build()
+ .Add<TCoMember>()
+ .Struct(row)
+ .Name()
+ .Value(column)
+ .Build()
+ .Build()
+ .Done().Ptr());
+ }
+
+ return ctx.ChangeChildren(*input, std::move(newChildren));
+ }
+ }
+
+ return input;
+ }
+ }
+
+ return input;
+ }, ctx, settings);
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return nullptr;
+ }
+
+ // refresh CombineCore lambdas
+ auto child = newLambda->Child(TCoLambda::idx_Body);
+ if (TCoCombineCore::Match(child)) {
+ child->ChildRef(TCoCombineCore::idx_KeyExtractor) = ctx.DeepCopyLambda(*child->Child(TCoCombineCore::idx_KeyExtractor));
+ child->ChildRef(TCoCombineCore::idx_InitHandler) = ctx.DeepCopyLambda(*child->Child(TCoCombineCore::idx_InitHandler));
+ child->ChildRef(TCoCombineCore::idx_UpdateHandler) = ctx.DeepCopyLambda(*child->Child(TCoCombineCore::idx_UpdateHandler));
+ }
+
+ // refresh flatmaps too
+ if (stack.size() > 1) {
+ child = child->Child(TCoInputBase::idx_Input);
+ for (size_t i = 1; i < stack.size(); ++i) {
+ child->ChildRef(TCoFlatMapBase::idx_Lambda) = ctx.DeepCopyLambda(*child->Child(TCoFlatMapBase::idx_Lambda));
+ child = child->Child(TCoInputBase::idx_Input);
+ }
+ }
+
+ auto columnsBuilder = Build<TExprList>(ctx, op.Pos());
+ for (auto& field: filteredFields) {
+ columnsBuilder
+ .Add<TCoAtom>()
+ .Value(field)
+ .Build();
+ }
+ for (auto& field: weakFields) {
+ columnsBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(field)
+ .Build()
+ .Add()
+ .Value("weak")
+ .Build()
+ .Build();
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), TYtWithUserJobsOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(UpdateInputFields(section, columnsBuilder.Done(), ctx))
+ .Done().Ptr());
+
+ res = ctx.ChangeChild(*res, TYtWithUserJobsOpBase::idx_Settings, NYql::AddSetting(op.Settings().Ref(), EYtSettingType::WeakFields, {}, ctx));
+ res = ctx.ChangeChild(*res, mapperIdx, std::move(newLambda));
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> EquiJoin(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ if (State_->Types->EvaluationInProgress) {
+ return node;
+ }
+
+ auto equiJoin = node.Cast<TCoEquiJoin>();
+
+ TMaybeNode<TYtDSink> dataSink;
+ TString usedCluster;
+ for (size_t i = 0; i + 2 < equiJoin.ArgCount(); ++i) {
+ auto list = equiJoin.Arg(i).Cast<TCoEquiJoinInput>().List();
+ if (auto maybeExtractMembers = list.Maybe<TCoExtractMembers>()) {
+ list = maybeExtractMembers.Cast().Input();
+ }
+ if (auto maybeFlatMap = list.Maybe<TCoFlatMapBase>()) {
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(maybeFlatMap.Cast().Lambda().Ref(), syncList, usedCluster, true, false)) {
+ return node;
+ }
+ list = maybeFlatMap.Cast().Input();
+ }
+ if (!IsYtProviderInput(list)) {
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(list.Ref(), syncList, usedCluster, true, false)) {
+ return node;
+ }
+ continue;
+ }
+
+ if (!dataSink) {
+ dataSink = GetDataSink(list, ctx);
+ }
+ auto cluster = ToString(GetClusterName(list));
+ if (!UpdateUsedCluster(usedCluster, cluster)) {
+ return node;
+ }
+ }
+
+ if (!dataSink) {
+ return node;
+ }
+
+ THashMap<TStringBuf, std::pair<TVector<TStringBuf>, ui32>> tableSortKeysUsage =
+ CollectTableSortKeysUsage(State_, equiJoin);
+
+ // label -> join keys
+ THashMap<TStringBuf, THashSet<TStringBuf>> tableKeysMap =
+ CollectEquiJoinKeyColumnsByLabel(equiJoin.Arg(equiJoin.ArgCount() - 2).Ref());
+
+ TNodeOnNodeOwnedMap updatedInputs;
+ TExprNode::TListType sections;
+ TExprNode::TListType premaps;
+ TSyncMap worldList;
+ for (size_t i = 0; i + 2 < equiJoin.ArgCount(); ++i) {
+ auto joinInput = equiJoin.Arg(i).Cast<TCoEquiJoinInput>();
+ auto list = joinInput.List();
+
+ TMaybeNode<TCoLambda> premapLambda;
+ TExprNode::TPtr extractedMembers;
+
+ auto listStepForward = list;
+ if (auto maybeExtractMembers = listStepForward.Maybe<TCoExtractMembers>()) {
+ extractedMembers = maybeExtractMembers.Cast().Members().Ptr();
+ listStepForward = maybeExtractMembers.Cast().Input();
+ }
+
+ if (auto maybeFlatMap = listStepForward.Maybe<TCoFlatMapBase>()) {
+ auto flatMap = maybeFlatMap.Cast();
+ if (IsLambdaSuitableForPullingIntoEquiJoin(flatMap, joinInput.Scope().Ref(), tableKeysMap, extractedMembers.Get())) {
+ if (!IsYtCompleteIsolatedLambda(flatMap.Lambda().Ref(), worldList, usedCluster, true, false)) {
+ return node;
+ }
+
+ auto maybeLambda = CleanupWorld(flatMap.Lambda(), ctx);
+ if (!maybeLambda) {
+ return {};
+ }
+
+ auto lambda = ctx.DeepCopyLambda(maybeLambda.Cast().Ref());
+ if (!extractedMembers) {
+ premapLambda = lambda;
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": Collected input #" << i << " as premap";
+ } else {
+ premapLambda = ctx.Builder(lambda->Pos())
+ .Lambda()
+ .Param("item")
+ .Callable("ExtractMembers")
+ .Apply(0, lambda)
+ .With(0, "item")
+ .Seal()
+ .Add(1, extractedMembers)
+ .Seal()
+ .Seal()
+ .Build();
+ YQL_CLOG(INFO, ProviderYt) << __FUNCTION__ << ": Collected input #" << i << " as premap with extract members";
+ }
+
+ list = flatMap.Input();
+ }
+ }
+
+ TExprNode::TPtr section;
+ if (!IsYtProviderInput(list)) {
+ auto& newSection = updatedInputs[list.Raw()];
+ if (newSection) {
+ section = Build<TYtSection>(ctx, list.Pos())
+ .InitFrom(TYtSection(newSection))
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinLabel))
+ .Build()
+ .Value(joinInput.Scope())
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ else {
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(list.Ref(), syncList, usedCluster, true, false)) {
+ return node;
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(list, false, ctx)) {
+ if (!EnsurePersistableType(list.Pos(), *type, ctx)) {
+ return {};
+ }
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ TYtOutTableInfo outTable(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTable.RowSpec->SetConstraints(list.Ref().GetConstraintSet());
+ outTable.SetUnique(list.Ref().GetConstraint<TDistinctConstraintNode>(), list.Pos(), ctx);
+
+ auto cleanup = CleanupWorld(list, ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ section = newSection = Build<TYtSection>(ctx, list.Pos())
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation<TYtFill>()
+ .World(ApplySyncListToWorld(ctx.NewWorld(list.Pos()), syncList, ctx))
+ .DataSink(dataSink.Cast())
+ .Content(MakeJobLambdaNoArg(cleanup.Cast(), ctx))
+ .Output()
+ .Add(outTable.ToExprNode(ctx, list.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(list.Pos(), *State_, ctx))
+ .Build()
+ .OutIndex().Value(0U).Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinLabel))
+ .Build()
+ .Value(joinInput.Scope())
+ .Build()
+ .Build()
+ .Done().Ptr();
+ }
+ }
+ else {
+ auto settings = Build<TCoNameValueTupleList>(ctx, list.Pos())
+ .Add()
+ .Name()
+ .Value(ToString(EYtSettingType::JoinLabel))
+ .Build()
+ .Value(joinInput.Scope())
+ .Build()
+ .Done();
+
+ TExprNode::TPtr world;
+ if (auto maybeRead = list.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (read.World().Ref().Type() != TExprNode::World) {
+ world = read.World().Ptr();
+ }
+ }
+
+ bool makeUnordered = false;
+ if (ctx.IsConstraintEnabled<TSortedConstraintNode>() && joinInput.Scope().Ref().IsAtom()) {
+ makeUnordered = (0 == tableSortKeysUsage[joinInput.Scope().Ref().Content()].second);
+ }
+
+ auto sectionList = ConvertInputTable(list, ctx, TConvertInputOpts().Settings(settings).MakeUnordered(makeUnordered));
+ YQL_ENSURE(sectionList.Size() == 1, "EquiJoin input should contain exactly one section, but has: " << sectionList.Size());
+ bool clearWorld = false;
+ auto sectionNode = sectionList.Item(0);
+
+ if (NYql::HasSetting(sectionNode.Settings().Ref(), EYtSettingType::Sample)) {
+ auto scheme = list.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+
+ auto path = CopyOrTrivialMap(sectionNode.Pos(),
+ TExprBase(world ? world : ctx.NewWorld(sectionNode.Pos())),
+ dataSink.Cast(),
+ *scheme,
+ Build<TYtSection>(ctx, sectionNode.Pos())
+ .InitFrom(sectionNode)
+ .Settings(NYql::RemoveSettings(sectionNode.Settings().Ref(), EYtSettingType::StatColumns
+ | EYtSettingType::JoinLabel | EYtSettingType::Unordered, ctx))
+ .Done(),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(!makeUnordered).SetSectionUniq(list.Ref().GetConstraint<TDistinctConstraintNode>()));
+
+ clearWorld = true;
+
+ sectionNode = Build<TYtSection>(ctx, sectionNode.Pos())
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings(NYql::RemoveSettings(sectionNode.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip |
+ EYtSettingType::Sample, ctx))
+ .Done();
+ }
+
+ if (!clearWorld && world) {
+ worldList.emplace(world, worldList.size());
+ }
+
+ section = sectionNode.Ptr();
+ }
+
+ YQL_ENSURE(section);
+ sections.push_back(section);
+ auto premap = BuildYtEquiJoinPremap(list, premapLambda, ctx);
+ if (!premap) {
+ return {};
+ }
+ premaps.push_back(premap);
+ }
+
+ const TStructExprType* outItemType = nullptr;
+ if (auto type = GetSequenceItemType(node, false, ctx)) {
+ if (!EnsurePersistableType(node.Pos(), *type, ctx)) {
+ return {};
+ }
+ outItemType = type->Cast<TStructExprType>();
+ } else {
+ return {};
+ }
+
+ auto parentsMap = getParents();
+ YQL_ENSURE(parentsMap);
+ auto joinOptions = CollectPreferredSortsForEquiJoinOutput(node, equiJoin.Arg(equiJoin.ArgCount() - 1).Ptr(), ctx, *parentsMap);
+
+ const auto join = Build<TYtEquiJoin>(ctx, node.Pos())
+ .World(ApplySyncListToWorld(ctx.NewWorld(node.Pos()), worldList, ctx))
+ .DataSink(dataSink.Cast())
+ .Input()
+ .Add(sections)
+ .Build()
+ .Output()
+ .Add(TYtOutTableInfo(outItemType, State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE)
+ .ToExprNode(ctx, node.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings()
+ .Build()
+ .Joins(equiJoin.Arg(equiJoin.ArgCount() - 2))
+ .JoinOptions(joinOptions)
+ .Done();
+
+ auto children = join.Ref().ChildrenList();
+ children.reserve(children.size() + premaps.size());
+ std::move(premaps.begin(), premaps.end(), std::back_inserter(children));
+ return Build<TYtOutput>(ctx, node.Pos())
+ .Operation(ctx.ChangeChildren(join.Ref(), std::move(children)))
+ .OutIndex().Value(0U).Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> RuntimeEquiJoin(TExprBase node, TExprContext& ctx) const {
+ auto equiJoin = node.Cast<TYtEquiJoin>();
+ const bool waitAllInputs = State_->Configuration->JoinWaitAllInputs.Get().GetOrElse(false);
+ if (waitAllInputs) {
+ for (auto section: equiJoin.Input()) {
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ if (!pathInfo.Table->Stat) {
+ return node;
+ }
+ }
+ }
+ }
+
+ const auto tree = ImportYtEquiJoin(equiJoin, ctx);
+ const auto rewriteStatus = RewriteYtEquiJoin(equiJoin, *tree, State_, ctx);
+
+ switch (rewriteStatus.Level) {
+ case TStatus::Repeat:
+ YQL_ENSURE(!waitAllInputs);
+ return node;
+ case TStatus::Error:
+ return {};
+ case TStatus::Ok:
+ break;
+ default:
+ YQL_ENSURE(false, "Unexpected rewrite status");
+ }
+
+ return ExportYtEquiJoin(equiJoin, *tree, ctx, State_);
+ }
+
+ TMaybeNode<TExprBase> TableContentWithSettings(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtOutputOpBase>();
+
+ TExprNode::TPtr res = op.Ptr();
+
+ TNodeSet nodesToOptimize;
+ VisitExpr(res, [&nodesToOptimize](const TExprNode::TPtr& input) {
+ if (auto read = TMaybeNode<TYtLength>(input).Input().Maybe<TYtReadTable>()) {
+ nodesToOptimize.insert(read.Cast().Raw());
+ return false;
+ }
+
+ if (auto read = TMaybeNode<TYtTableContent>(input).Input().Maybe<TYtReadTable>()) {
+ nodesToOptimize.insert(read.Cast().Raw());
+ return false;
+ }
+ if (TYtOutput::Match(input.Get())) {
+ return false;
+ }
+ return true;
+ });
+
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TSyncMap syncList;
+ auto status = OptimizeExpr(res, res, [&syncList, &nodesToOptimize, state = State_](const TExprNode::TPtr& input, TExprContext& ctx) -> TExprNode::TPtr {
+ if (nodesToOptimize.find(input.Get()) != nodesToOptimize.end()) {
+ return OptimizeReadWithSettings(input, false, true, syncList, state, ctx);
+ }
+ return input;
+ }, ctx, TOptimizeExprSettings(State_->Types));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ if (status.Level == IGraphTransformer::TStatus::Ok) {
+ return node;
+ }
+
+ if (!syncList.empty()) {
+ using TPair = std::pair<TExprNode::TPtr, ui64>;
+ TVector<TPair> sortedList(syncList.cbegin(), syncList.cend());
+ TExprNode::TListType syncChildren;
+ syncChildren.push_back(res->ChildPtr(TYtOutputOpBase::idx_World));
+ ::Sort(sortedList, [](const TPair& x, const TPair& y) { return x.second < y.second; });
+ for (auto& x: sortedList) {
+ auto world = ctx.NewCallable(node.Pos(), TCoLeft::CallableName(), { x.first });
+ syncChildren.push_back(world);
+ }
+
+ res = ctx.ChangeChild(*res, TYtOutputOpBase::idx_World,
+ ctx.NewCallable(node.Pos(), TCoSync::CallableName(), std::move(syncChildren)));
+ }
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> NonOptimalTableContent(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtOutputOpBase>();
+
+ TExprNode::TPtr res = op.Ptr();
+
+ TNodeSet nodesToOptimize;
+ VisitExpr(res, [&nodesToOptimize](const TExprNode::TPtr& input) {
+ if (TYtTableContent::Match(input.Get())) {
+ nodesToOptimize.insert(input.Get());
+ return false;
+ }
+ if (TYtOutput::Match(input.Get())) {
+ return false;
+ }
+ return true;
+ });
+
+ if (nodesToOptimize.empty()) {
+ return node;
+ }
+
+ TSyncMap syncList;
+ const auto maxTables = State_->Configuration->TableContentMaxInputTables.Get().GetOrElse(1000);
+ const auto minChunkSize = State_->Configuration->TableContentMinAvgChunkSize.Get().GetOrElse(1_GB);
+ const auto maxChunks = State_->Configuration->TableContentMaxChunksForNativeDelivery.Get().GetOrElse(1000ul);
+ auto state = State_;
+ auto world = res->ChildPtr(TYtOutputOpBase::idx_World);
+ auto status = OptimizeExpr(res, res, [&syncList, &nodesToOptimize, maxTables, minChunkSize, maxChunks, state, world](const TExprNode::TPtr& input, TExprContext& ctx) -> TExprNode::TPtr {
+ if (nodesToOptimize.find(input.Get()) != nodesToOptimize.end()) {
+ if (auto read = TYtTableContent(input).Input().Maybe<TYtReadTable>()) {
+ bool materialize = false;
+ const bool singleSection = 1 == read.Cast().Input().Size();
+ TVector<TYtSection> newSections;
+ for (auto section: read.Cast().Input()) {
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Sample | EYtSettingType::SysColumns)) {
+ materialize = true;
+ }
+ else if (section.Paths().Size() > maxTables) {
+ materialize = true;
+ }
+ else {
+ TMaybeNode<TYtMerge> oldOp;
+ if (section.Paths().Size() == 1) {
+ oldOp = section.Paths().Item(0).Table().Maybe<TYtOutput>().Operation().Maybe<TYtMerge>();
+ }
+ if (!oldOp.IsValid() || !NYql::HasSetting(oldOp.Cast().Settings().Ref(), EYtSettingType::CombineChunks)) {
+ for (auto path: section.Paths()) {
+ TYtTableBaseInfo::TPtr tableInfo = TYtTableBaseInfo::Parse(path.Table());
+ if (auto tableStat = tableInfo->Stat) {
+ if (tableStat->ChunkCount > maxChunks || (tableStat->ChunkCount > 1 && tableStat->DataSize / tableStat->ChunkCount < minChunkSize)) {
+ materialize = true;
+ break;
+ }
+ }
+ if (!tableInfo->IsTemp && tableInfo->Meta) {
+ auto p = tableInfo->Meta->Attrs.FindPtr("erasure_codec");
+ if (p && *p != "none") {
+ materialize = true;
+ break;
+ }
+ else if (tableInfo->Meta->IsDynamic) {
+ materialize = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (materialize) {
+ auto path = CopyOrTrivialMap(section.Pos(),
+ TExprBase(world),
+ TYtDSink(ctx.RenameNode(read.DataSource().Ref(), "DataSink")),
+ *section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType(),
+ Build<TYtSection>(ctx, section.Pos())
+ .Paths(section.Paths())
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::Unordered | EYtSettingType::NonUnique, ctx))
+ .Done(),
+ ctx, state,
+ TCopyOrTrivialMapOpts()
+ .SetTryKeepSortness(!NYql::HasSetting(section.Settings().Ref(), EYtSettingType::Unordered))
+ .SetSectionUniq(section.Ref().GetConstraint<TDistinctConstraintNode>())
+ .SetCombineChunks(true)
+ );
+
+ syncList[path.Table().Cast<TYtOutput>().Operation().Ptr()] = syncList.size();
+
+ if (singleSection) {
+ return ctx.ChangeChild(*input, TYtTableContent::idx_Input, path.Table().Ptr());
+ } else {
+ newSections.push_back(Build<TYtSection>(ctx, section.Pos())
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings().Build()
+ .Done());
+ }
+
+ } else {
+ newSections.push_back(section);
+ }
+
+ }
+ if (materialize) {
+ auto newRead = Build<TYtReadTable>(ctx, read.Cast().Pos())
+ .InitFrom(read.Cast())
+ .Input()
+ .Add(newSections)
+ .Build()
+ .Done();
+
+ return ctx.ChangeChild(*input, TYtTableContent::idx_Input, newRead.Ptr());
+ }
+ }
+ else if (auto out = TYtTableContent(input).Input().Maybe<TYtOutput>()) {
+ auto oldOp = GetOutputOp(out.Cast());
+ if (!oldOp.Maybe<TYtMerge>() || !NYql::HasSetting(oldOp.Cast<TYtMerge>().Settings().Ref(), EYtSettingType::CombineChunks)) {
+ auto outTable = GetOutTable(out.Cast());
+ TYtOutTableInfo tableInfo(outTable);
+ if (auto tableStat = tableInfo.Stat) {
+ if (tableStat->ChunkCount > maxChunks || (tableStat->ChunkCount > 1 && tableStat->DataSize / tableStat->ChunkCount < minChunkSize)) {
+ auto newOp = Build<TYtMerge>(ctx, input->Pos())
+ .World(world)
+ .DataSink(oldOp.DataSink())
+ .Output()
+ .Add()
+ .InitFrom(outTable.Cast<TYtOutTable>())
+ .Name().Value("").Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table(out.Cast())
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings<TCoNameValueTupleList>()
+ .Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Add()
+ .Name().Value(ToString(EYtSettingType::CombineChunks)).Build()
+ .Build()
+ .Add()
+ .Name().Value(ToString(EYtSettingType::ForceTransform)).Build()
+ .Build()
+ .Build()
+ .Done();
+
+ syncList[newOp.Ptr()] = syncList.size();
+
+ auto newOutput = Build<TYtOutput>(ctx, input->Pos())
+ .Operation(newOp)
+ .OutIndex().Value(0U).Build()
+ .Done().Ptr();
+
+ return ctx.ChangeChild(*input, TYtTableContent::idx_Input, std::move(newOutput));
+ }
+ }
+ }
+ }
+ }
+ return input;
+ }, ctx, TOptimizeExprSettings(State_->Types));
+
+ if (status.Level == IGraphTransformer::TStatus::Error) {
+ return {};
+ }
+
+ if (status.Level == IGraphTransformer::TStatus::Ok) {
+ return node;
+ }
+
+ if (!syncList.empty()) {
+ using TPair = std::pair<TExprNode::TPtr, ui64>;
+ TVector<TPair> sortedList(syncList.cbegin(), syncList.cend());
+ TExprNode::TListType syncChildren;
+ syncChildren.push_back(res->ChildPtr(TYtOutputOpBase::idx_World));
+ ::Sort(sortedList, [](const TPair& x, const TPair& y) { return x.second < y.second; });
+ for (auto& x: sortedList) {
+ auto world = ctx.NewCallable(node.Pos(), TCoLeft::CallableName(), { x.first });
+ syncChildren.push_back(world);
+ }
+
+ res = ctx.ChangeChild(*res, TYtOutputOpBase::idx_World,
+ ctx.NewCallable(node.Pos(), TCoSync::CallableName(), std::move(syncChildren)));
+ }
+
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> ReadWithSettings(TExprBase node, TExprContext& ctx) const {
+ auto maybeRead = node.Cast<TCoRight>().Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ return node;
+ }
+
+ auto read = maybeRead.Cast().Ptr();
+ TSyncMap syncList;
+ auto ret = OptimizeReadWithSettings(read, true, true, syncList, State_, ctx);
+ if (ret != read) {
+ if (ret) {
+ if (!syncList.empty()) {
+ ret = ctx.ChangeChild(*ret, TYtReadTable::idx_World,
+ ApplySyncListToWorld(ret->ChildPtr(TYtReadTable::idx_World), syncList, ctx));
+ }
+ return Build<TCoRight>(ctx, node.Pos())
+ .Input(ret)
+ .Done();
+ }
+ return {};
+ }
+ return node;
+ }
+
+ TMaybeNode<TExprBase> DqReadWrapWithSettings(TExprBase node, TExprContext& ctx) const {
+ auto maybeRead = node.Cast<TDqReadWrapBase>().Input().Maybe<TYtReadTable>();
+ if (!maybeRead) {
+ return node;
+ }
+
+ TYtDSource dataSource = GetDataSource(maybeRead.Cast(), ctx);
+ if (!State_->Configuration->_EnableYtPartitioning.Get(dataSource.Cluster().StringValue()).GetOrElse(false)) {
+ return node;
+ }
+
+ auto read = maybeRead.Cast().Ptr();
+ TSyncMap syncList;
+ auto ret = OptimizeReadWithSettings(read, false, false, syncList, State_, ctx);
+ if (ret != read) {
+ if (ret) {
+ YQL_ENSURE(syncList.empty());
+ return TExprBase(ctx.ChangeChild(node.Ref(), TDqReadWrapBase::idx_Input, std::move(ret)));
+ }
+ return {};
+ }
+ return node;
+ }
+
+ TMaybeNode<TExprBase> TransientOpWithSettings(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtTransientOpBase>();
+
+ if (node.Ref().HasResult() && node.Ref().GetResult().Type() == TExprNode::World) {
+ return node;
+ }
+
+ bool keepSortness = false;
+ if (op.Maybe<TYtReduce>()) {
+ keepSortness = true;
+ } else if (op.Maybe<TYtCopy>() || op.Maybe<TYtMerge>() || op.Maybe<TYtMap>()) {
+ keepSortness = AnyOf(op.Output(), [] (const TYtOutTable& out) {
+ return TYqlRowSpecInfo(out.RowSpec()).IsSorted();
+ });
+ }
+
+ bool hasUpdates = false;
+ TVector<TExprBase> updatedSections;
+ TSyncMap syncList;
+ for (auto section: op.Input()) {
+ updatedSections.push_back(section);
+
+ if (auto updatedSection = UpdateSectionWithSettings(op.World(), section, op.DataSink(), keepSortness, true, true, syncList, State_, ctx)) {
+ updatedSections.back() = updatedSection.Cast();
+ hasUpdates = true;
+ }
+ }
+ if (!hasUpdates) {
+ return node;
+ }
+
+ auto res = ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done().Ptr());
+ if (!syncList.empty()) {
+ res = ctx.ChangeChild(*res, TYtTransientOpBase::idx_World,
+ ApplySyncListToWorld(res->ChildPtr(TYtTransientOpBase::idx_World), syncList, ctx));
+ }
+ // Transform YtCopy to YtMerge in case of ranges
+ if (op.Maybe<TYtCopy>()) {
+ if (AnyOf(updatedSections.front().Cast<TYtSection>().Paths(), [](TYtPath path) { return !path.Ranges().Maybe<TCoVoid>(); })) {
+ res = ctx.RenameNode(*res, TYtMerge::CallableName());
+ }
+ }
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> ZeroSample(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtTransientOpBase>();
+
+ if (node.Ref().HasResult() && node.Ref().GetResult().Type() == TExprNode::World) {
+ return node;
+ }
+
+ bool hasUpdates = false;
+ TVector<TExprBase> updatedSections;
+ for (auto section: op.Input()) {
+ TMaybe<TSampleParams> sampling = NYql::GetSampleParams(section.Settings().Ref());
+ if (sampling && sampling->Percentage == 0.0) {
+ hasUpdates = true;
+ updatedSections.push_back(MakeEmptySection(section, op.DataSink(), true, State_, ctx));
+ } else {
+ updatedSections.push_back(section);
+ }
+ }
+
+ if (!hasUpdates) {
+ return node;
+ }
+
+ return ctx.ChangeChild(op.Ref(), TYtTransientOpBase::idx_Input,
+ Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done().Ptr());
+ }
+
+ TMaybeNode<TExprBase> ReplaceStatWriteTable(TExprBase node, TExprContext& ctx) const {
+ auto write = node.Cast<TStatWriteTable>();
+ auto input = write.Input();
+
+ TMaybeNode<TYtOutput> newInput;
+
+ if (!IsYtProviderInput(input, false)) {
+ if (!EnsurePersistable(input.Ref(), ctx)) {
+ return {};
+ }
+
+ TString cluster;
+ TSyncMap syncList;
+ if (!IsYtCompleteIsolatedLambda(input.Ref(), syncList, cluster, true, false)) {
+ return node;
+ }
+
+ const TTypeAnnotationNode* outItemType;
+ if (!GetSequenceItemType(input.Ref(), outItemType, ctx)) {
+ return {};
+ }
+ if (!EnsurePersistableType(input.Pos(), *outItemType, ctx)) {
+ return {};
+ }
+
+ auto cleanup = CleanupWorld(input, ctx);
+ if (!cleanup) {
+ return {};
+ }
+
+ TYtOutTableInfo outTable {outItemType->Cast<TStructExprType>(),
+ State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE};
+ outTable.RowSpec->SetConstraints(input.Ref().GetConstraintSet());
+ outTable.SetUnique(input.Ref().GetConstraint<TDistinctConstraintNode>(), input.Pos(), ctx);
+
+ if (!cluster) {
+ cluster = State_->Configuration->DefaultCluster
+ .Get().GetOrElse(State_->Gateway->GetDefaultClusterName());
+ }
+
+ input = Build<TYtOutput>(ctx, write.Pos())
+ .Operation<TYtFill>()
+ .World(ApplySyncListToWorld(ctx.NewWorld(write.Pos()), syncList, ctx))
+ .DataSink<TYtDSink>()
+ .Category()
+ .Value(YtProviderName)
+ .Build()
+ .Cluster()
+ .Value(cluster)
+ .Build()
+ .Build()
+ .Output()
+ .Add(outTable.ToExprNode(ctx, write.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Content(MakeJobLambdaNoArg(cleanup.Cast(), ctx))
+ .Settings(GetFlowSettings(write.Pos(), *State_, ctx))
+ .Build()
+ .OutIndex()
+ .Value("0")
+ .Build()
+ .Done();
+ }
+
+ if (auto maybeRead = input.Maybe<TCoRight>().Input().Maybe<TYtReadTable>()) {
+ auto read = maybeRead.Cast();
+ if (read.Input().Size() != 1) {
+ ctx.AddError(TIssue(ctx.GetPosition(read.Input().Pos()), TStringBuilder() <<
+ "Unexpected read with several sections on " << node.Ptr()->Content()
+ ));
+ return {};
+ }
+
+ auto section = read.Input().Item(0);
+ auto scheme = section.Ptr()->GetTypeAnn()->Cast<TListExprType>()->GetItemType();
+
+ auto path = CopyOrTrivialMap(section.Pos(),
+ GetWorld(input, {}, ctx),
+ GetDataSink(input, ctx),
+ *scheme,
+ Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Settings(NYql::RemoveSettings(section.Settings().Ref(), EYtSettingType::Unordered | EYtSettingType::Unordered, ctx))
+ .Done(),
+ ctx, State_,
+ TCopyOrTrivialMapOpts().SetTryKeepSortness(true).SetSectionUniq(section.Ref().GetConstraint<TDistinctConstraintNode>()));
+
+ YQL_ENSURE(
+ path.Ranges().Maybe<TCoVoid>(),
+ "Unexpected slices: " << path.Ranges().Ref().Content()
+ );
+
+ YQL_ENSURE(
+ path.Table().Maybe<TYtOutput>().Operation(),
+ "Unexpected node: " << path.Table().Ref().Content()
+ );
+
+ newInput = path.Table().Cast<TYtOutput>();
+ } else if (auto op = input.Maybe<TYtOutput>().Operation()) {
+ newInput = input.Cast<TYtOutput>();
+ } else {
+ YQL_ENSURE(false, "Unexpected operation input: " << input.Ptr()->Content());
+ }
+
+ auto table = ctx.RenameNode(write.Table().Ref(), TYtStatOutTable::CallableName());
+
+ return Build<TYtStatOut>(ctx, write.Pos())
+ .World(GetWorld(input, {}, ctx))
+ .DataSink(GetDataSink(input, ctx))
+ .Input(newInput.Cast())
+ .Table(table)
+ .ReplaceMask(write.ReplaceMask())
+ .Settings(write.Settings())
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> TopSort(TExprBase node, TExprContext& ctx) const {
+ auto sort = node.Cast<TYtSort>();
+
+ auto settings = sort.Settings();
+ auto limitSetting = NYql::GetSetting(settings.Ref(), EYtSettingType::Limit);
+ if (!limitSetting) {
+ return node;
+ }
+ if (HasNodesToCalculate(node.Ptr())) {
+ return node;
+ }
+
+ if (NYql::HasAnySetting(sort.Input().Item(0).Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip)) {
+ return node;
+ }
+
+ if (NYql::HasNonEmptyKeyFilter(sort.Input().Item(0))) {
+ return node;
+ }
+
+ const ui64 maxLimit = State_->Configuration->TopSortMaxLimit.Get().GetOrElse(DEFAULT_TOP_SORT_LIMIT);
+ TMaybe<ui64> limit = GetLimit(settings.Ref());
+ if (!limit || *limit > maxLimit) {
+ // Keep empty "limit" setting to prevent repeated Limits optimization
+ if (limitSetting->ChildPtr(1)->ChildrenSize() != 0) {
+ auto updatedSettings = NYql::RemoveSetting(settings.Ref(), EYtSettingType::Limit, ctx);
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::Limit, ctx.NewList(node.Pos(), {}), ctx);
+ return TExprBase(ctx.ChangeChild(node.Ref(), TYtSort::idx_Settings, std::move(updatedSettings)));
+ }
+ return node;
+ }
+
+ ui64 size = 0;
+ ui64 rows = 0;
+ for (auto path: sort.Input().Item(0).Paths()) {
+ auto tableInfo = TYtTableBaseInfo::Parse(path.Table());
+ if (!tableInfo->Stat) {
+ return node;
+ }
+ ui64 tableSize = tableInfo->Stat->DataSize;
+ ui64 tableRows = tableInfo->Stat->RecordsCount;
+
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ if (TMaybe<ui64> usedRows = TYtRangesInfo(path.Ranges()).GetUsedRows(tableRows)) {
+ // Make it proportional to used rows
+ tableSize = tableSize * usedRows.GetRef() / tableRows;
+ tableRows = usedRows.GetRef();
+ } else {
+ // non-row ranges are present
+ return node;
+ }
+ }
+ size += tableSize;
+ rows += tableRows;
+ }
+
+ if (rows <= *limit) {
+ // Just do YtSort
+ // Keep empty "limit" setting to prevent repeated Limits optimization
+ auto updatedSettings = NYql::RemoveSetting(settings.Ref(), EYtSettingType::Limit, ctx);
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::Limit, ctx.NewList(node.Pos(), {}), ctx);
+ return TExprBase(ctx.ChangeChild(node.Ref(), TYtSort::idx_Settings, std::move(updatedSettings)));
+ }
+
+ const ui64 sizePerJob = State_->Configuration->TopSortSizePerJob.Get().GetOrElse(128_MB);
+ const ui64 rowMultiplierPerJob = State_->Configuration->TopSortRowMultiplierPerJob.Get().GetOrElse(10u);
+ ui64 partsBySize = size / sizePerJob;
+ ui64 partsByRecords = rows / (rowMultiplierPerJob * limit.GetRef());
+ ui64 jobCount = Max<ui64>(Min<ui64>(partsBySize, partsByRecords), 1);
+ if (partsBySize <= 1 || partsByRecords <= 10) {
+ jobCount = 1;
+ }
+
+ auto sortedBy = TYqlRowSpecInfo(sort.Output().Item(0).RowSpec()).GetForeignSort();
+ auto updatedSettings = NYql::AddSettingAsColumnPairList(sort.Settings().Ref(), EYtSettingType::SortLimitBy, sortedBy, ctx);
+ if (jobCount <= 5000) {
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::JobCount, ctx.NewAtom(sort.Pos(), ToString(jobCount)), ctx);
+ }
+
+ auto inputItemType = GetSequenceItemType(sort.Input().Item(0), false, ctx);
+ if (!inputItemType) {
+ return {};
+ }
+
+ if (jobCount == 1) {
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::KeepSorted, {}, ctx);
+
+ auto mapper = Build<TCoLambda>(ctx, sort.Pos())
+ .Args({"stream"})
+ .Body<TCoTopSort>()
+ .Input("stream")
+ .Count<TCoUint64>()
+ .Literal()
+ .Value(ToString(*limit))
+ .Build()
+ .Build()
+ .SortDirections([&sortedBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortedBy.size())) {
+ listBuilder.Callable(i, TCoBool::CallableName())
+ .Atom(0, sortedBy[i].second ? "True" : "False")
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .KeySelectorLambda()
+ .Args({"item"})
+ .Body([&sortedBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortedBy.size())) {
+ listBuilder.Callable(i, TCoMember::CallableName())
+ .Arg(0, "item")
+ .Atom(1, sortedBy[i].first)
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .Build()
+ .Build().Done();
+
+ // Final map
+ return Build<TYtMap>(ctx, sort.Pos())
+ .World(sort.World())
+ .DataSink(sort.DataSink())
+ .Input(sort.Input())
+ .Output(sort.Output())
+ .Settings(GetFlowSettings(sort.Pos(), *State_, ctx, updatedSettings))
+ .Mapper(mapper)
+ .Done();
+ }
+
+ auto mapper = Build<TCoLambda>(ctx, sort.Pos())
+ .Args({"stream"})
+ .Body<TCoTop>()
+ .Input("stream")
+ .Count<TCoUint64>()
+ .Literal()
+ .Value(ToString(*limit))
+ .Build()
+ .Build()
+ .SortDirections([&sortedBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortedBy.size())) {
+ listBuilder.Callable(i, TCoBool::CallableName())
+ .Atom(0, sortedBy[i].second ? "True" : "False")
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .KeySelectorLambda()
+ .Args({"item"})
+ .Body([&sortedBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortedBy.size())) {
+ listBuilder.Callable(i, TCoMember::CallableName())
+ .Arg(0, "item")
+ .Atom(1, sortedBy[i].first)
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .Build()
+ .Build().Done();
+
+ TYtOutTableInfo outTable(inputItemType->Cast<TStructExprType>(), State_->Configuration->UseNativeYtTypes.Get().GetOrElse(DEFAULT_USE_NATIVE_YT_TYPES) ? NTCF_ALL : NTCF_NONE);
+ outTable.RowSpec->SetConstraints(sort.Ref().GetConstraintSet());
+
+ return Build<TYtSort>(ctx, sort.Pos())
+ .InitFrom(sort)
+ .World<TCoWorld>().Build()
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .World(sort.World())
+ .DataSink(sort.DataSink())
+ .Input(sort.Input())
+ .Output()
+ .Add(outTable.SetUnique(sort.Ref().GetConstraint<TDistinctConstraintNode>(), sort.Pos(), ctx).ToExprNode(ctx, sort.Pos()).Cast<TYtOutTable>())
+ .Build()
+ .Settings(GetFlowSettings(sort.Pos(), *State_, ctx, updatedSettings))
+ .Mapper(mapper)
+ .Build()
+ .OutIndex()
+ .Value(0U)
+ .Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> EmbedLimit(TExprBase node, TExprContext& ctx) const {
+ auto op = node.Cast<TYtWithUserJobsOpBase>();
+ if (op.Output().Size() != 1) {
+ return node;
+ }
+ auto settings = op.Settings();
+ auto limitSetting = NYql::GetSetting(settings.Ref(), EYtSettingType::Limit);
+ if (!limitSetting) {
+ return node;
+ }
+ if (HasNodesToCalculate(node.Ptr())) {
+ return node;
+ }
+
+ TMaybe<ui64> limit = GetLimit(settings.Ref());
+ if (!limit) {
+ return node;
+ }
+
+ auto sortLimitBy = NYql::GetSettingAsColumnPairList(settings.Ref(), EYtSettingType::SortLimitBy);
+ if (!sortLimitBy.empty() && *limit > State_->Configuration->TopSortMaxLimit.Get().GetOrElse(DEFAULT_TOP_SORT_LIMIT)) {
+ return node;
+ }
+
+ size_t lambdaIdx = op.Maybe<TYtMapReduce>()
+ ? TYtMapReduce::idx_Reducer
+ : op.Maybe<TYtReduce>()
+ ? TYtReduce::idx_Reducer
+ : TYtMap::idx_Mapper;
+
+ auto lambda = TCoLambda(op.Ref().ChildPtr(lambdaIdx));
+ if (IsEmptyContainer(lambda.Body().Ref()) || IsEmpty(lambda.Body().Ref(), *State_->Types)) {
+ return node;
+ }
+
+ if (sortLimitBy.empty()) {
+ if (lambda.Body().Maybe<TCoTake>()) {
+ return node;
+ }
+
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TCoTake>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With(0, "stream")
+ .Build()
+ .Count<TCoUint64>()
+ .Literal()
+ .Value(ToString(*limit))
+ .Build()
+ .Build()
+ .Build()
+ .Done();
+ } else {
+ if (lambda.Body().Maybe<TCoTopBase>()) {
+ return node;
+ }
+
+ if (const auto& body = lambda.Body().Ref(); body.IsCallable("ExpandMap") && body.Head().IsCallable({"Top", "TopSort"})) {
+ return node;
+ }
+
+ lambda = Build<TCoLambda>(ctx, lambda.Pos())
+ .Args({"stream"})
+ .Body<TCoTop>()
+ .Input<TExprApplier>()
+ .Apply(lambda)
+ .With(0, "stream")
+ .Build()
+ .Count<TCoUint64>()
+ .Literal()
+ .Value(ToString(*limit))
+ .Build()
+ .Build()
+ .SortDirections([&sortLimitBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortLimitBy.size())) {
+ listBuilder.Callable(i, TCoBool::CallableName())
+ .Atom(0, sortLimitBy[i].second ? "True" : "False")
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .KeySelectorLambda()
+ .Args({"item"})
+ .Body([&sortLimitBy] (TExprNodeBuilder& builder) {
+ auto listBuilder = builder.List();
+ for (size_t i: xrange(sortLimitBy.size())) {
+ listBuilder.Callable(i, TCoMember::CallableName())
+ .Arg(0, "item")
+ .Atom(1, sortLimitBy[i].first)
+ .Seal();
+ }
+ listBuilder.Seal();
+ })
+ .Build()
+ .Build().Done();
+
+ if (auto& l = lambda.Ref(); l.Tail().Head().IsCallable("ExpandMap")) {
+ lambda = TCoLambda(ctx.ChangeChild(l, 1U, ctx.SwapWithHead(l.Tail())));
+ }
+ }
+
+ return TExprBase(ctx.ChangeChild(op.Ref(), lambdaIdx, lambda.Ptr()));
+ }
+
+ TMaybeNode<TExprBase> PushMergeLimitToInput(TExprBase node, TExprContext& ctx) const {
+ if (node.Ref().HasResult() && node.Ref().GetResult().Type() != TExprNode::World) {
+ return node;
+ }
+
+ auto op = node.Cast<TYtMerge>();
+
+ auto settings = op.Settings();
+ auto limitSetting = NYql::GetSetting(settings.Ref(), EYtSettingType::Limit);
+ if (!limitSetting) {
+ return node;
+ }
+
+ auto section = op.Input().Item(0);
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Skip | EYtSettingType::Sample)) {
+ return node;
+ }
+ if (NYql::HasNonEmptyKeyFilter(section)) {
+ return node;
+ }
+
+ if (AnyOf(section.Paths(), [](const TYtPath& path) { return !path.Ranges().Maybe<TCoVoid>().IsValid(); })) {
+ return node;
+ }
+
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ // Dynamic tables don't support range selectors
+ if (pathInfo.Table->Meta->IsDynamic) {
+ return node;
+ }
+ }
+
+ TExprNode::TPtr effectiveLimit = GetLimitExpr(limitSetting, ctx);
+ if (!effectiveLimit) {
+ return node;
+ }
+
+ auto sectionSettings = section.Settings().Ptr();
+ auto sectionLimitSetting = NYql::GetSetting(*sectionSettings, EYtSettingType::Take);
+ if (sectionLimitSetting) {
+ effectiveLimit = ctx.NewCallable(node.Pos(), "Min", { effectiveLimit, sectionLimitSetting->ChildPtr(1) });
+ sectionSettings = NYql::RemoveSetting(*sectionSettings, EYtSettingType::Take, ctx);
+ }
+
+ sectionSettings = NYql::AddSetting(*sectionSettings, EYtSettingType::Take, effectiveLimit, ctx);
+
+ // Keep empty "limit" setting to prevent repeated Limits optimization
+ auto updatedSettings = NYql::RemoveSetting(settings.Ref(), EYtSettingType::Limit, ctx);
+ updatedSettings = NYql::AddSetting(*updatedSettings, EYtSettingType::Limit, ctx.NewList(node.Pos(), {}), ctx);
+
+ return Build<TYtMerge>(ctx, op.Pos())
+ .InitFrom(op)
+ .Input()
+ .Add()
+ .InitFrom(section)
+ .Settings(sectionSettings)
+ .Build()
+ .Build()
+ .Settings(updatedSettings)
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> PushDownKeyExtract(TExprBase node, TExprContext& ctx) const {
+ if (node.Ref().HasResult() && node.Ref().GetResult().Type() != TExprNode::World) {
+ return node;
+ }
+
+ auto op = node.Cast<TYtTransientOpBase>();
+
+ auto getInnerOpForUpdate = [] (const TYtPath& path, const TVector<TStringBuf>& usedKeyFilterColumns) -> TMaybeNode<TYtTransientOpBase> {
+ auto maybeOp = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtTransientOpBase>();
+ if (!maybeOp) {
+ return {};
+ }
+ auto innerOp = maybeOp.Cast();
+ if (innerOp.Ref().StartsExecution() || innerOp.Ref().HasResult()) {
+ return {};
+ }
+
+ if (!innerOp.Maybe<TYtMerge>() && !innerOp.Maybe<TYtMap>()) {
+ return {};
+ }
+
+ if (innerOp.Input().Size() != 1 || innerOp.Output().Size() != 1) {
+ return {};
+ }
+
+ if (NYql::HasSetting(innerOp.Settings().Ref(), EYtSettingType::Limit)) {
+ return {};
+ }
+ const auto outSorted = innerOp.Output().Item(0).Ref().GetConstraint<TSortedConstraintNode>();
+ if (!outSorted) {
+ return {};
+ }
+ for (auto path: innerOp.Input().Item(0).Paths()) {
+ const auto inputSorted = path.Ref().GetConstraint<TSortedConstraintNode>();
+ if (!inputSorted || !inputSorted->Includes(*outSorted)) {
+ return {};
+ }
+ }
+
+ auto innerSection = innerOp.Input().Item(0);
+ if (NYql::HasSettingsExcept(innerSection.Settings().Ref(), EYtSettingType::SysColumns)) {
+ return {};
+ }
+
+ if (auto maybeMap = innerOp.Maybe<TYtMap>()) {
+ // lambda must be passthrough for columns used in key filter
+ // TODO: use passthrough constraints here
+ TCoLambda lambda = maybeMap.Cast().Mapper();
+ TMaybe<THashSet<TStringBuf>> passthroughColumns;
+ bool analyzeJustMember = true;
+ if (&lambda.Args().Arg(0).Ref() != &lambda.Body().Ref()) {
+ auto maybeInnerFlatMap = GetFlatMapOverInputStream(lambda);
+ if (!maybeInnerFlatMap) {
+ return {};
+ }
+
+ if (!IsPassthroughFlatMap(maybeInnerFlatMap.Cast(), &passthroughColumns, analyzeJustMember)) {
+ return {};
+ }
+ }
+
+ if (passthroughColumns &&
+ !AllOf(usedKeyFilterColumns, [&](const TStringBuf& col) { return passthroughColumns->contains(col); }))
+ {
+ return {};
+ }
+ }
+
+ return maybeOp;
+ };
+
+ bool hasUpdates = false;
+ TVector<TExprBase> updatedSections;
+ for (auto section: op.Input()) {
+ bool hasPathUpdates = false;
+ TVector<TYtPath> updatedPaths;
+ auto settings = section.Settings().Ptr();
+ const EYtSettingType kfType = NYql::HasSetting(*settings, EYtSettingType::KeyFilter2) ?
+ EYtSettingType::KeyFilter2 : EYtSettingType::KeyFilter;
+ const auto keyFilters = NYql::GetAllSettingValues(*settings, kfType);
+ // Non empty filters and without table index
+ const bool haveNonEmptyKeyFiltersWithoutIndex =
+ AnyOf(keyFilters, [](const TExprNode::TPtr& f) { return f->ChildrenSize() > 0; }) &&
+ AllOf(keyFilters, [&](const TExprNode::TPtr& f) { return f->ChildrenSize() < GetMinChildrenForIndexedKeyFilter(kfType); });
+
+ bool allPathUpdated = true;
+ if (haveNonEmptyKeyFiltersWithoutIndex) {
+
+ TSyncMap syncList;
+ for (auto filter: keyFilters) {
+ if (!IsYtCompleteIsolatedLambda(*filter, syncList, true, false)) {
+ return node;
+ }
+ }
+
+ // TODO: should actually be true for both kf1/kf2 - enforce in ValidateSettings()
+ YQL_ENSURE(kfType == EYtSettingType::KeyFilter || keyFilters.size() == 1);
+ const auto kfColumns = GetKeyFilterColumns(section, kfType);
+ YQL_ENSURE(!kfColumns.empty());
+ for (auto path: section.Paths()) {
+ if (auto maybeOp = getInnerOpForUpdate(path, kfColumns)) {
+ auto innerOp = maybeOp.Cast();
+ if (kfType == EYtSettingType::KeyFilter2) {
+ // check input/output keyFilter columns are of same type
+ const TStructExprType* inputType =
+ innerOp.Input().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ const TStructExprType* outputType =
+ innerOp.Output().Item(0).Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ bool sameTypes = true;
+ for (auto& keyColumn : kfColumns) {
+ auto inPos = inputType->FindItem(keyColumn);
+ auto outPos = outputType->FindItem(keyColumn);
+ YQL_ENSURE(inPos);
+ YQL_ENSURE(outPos);
+ const TTypeAnnotationNode* inColumnType = inputType->GetItems()[*inPos]->GetItemType();
+ const TTypeAnnotationNode* outColumnType = outputType->GetItems()[*outPos]->GetItemType();
+ if (!IsSameAnnotation(*inColumnType, *outColumnType)) {
+ sameTypes = false;
+ break;
+ }
+ }
+
+ if (!sameTypes) {
+ // TODO: improve
+ updatedPaths.push_back(path);
+ allPathUpdated = false;
+ continue;
+ }
+ }
+
+ auto innerOpSection = innerOp.Input().Item(0);
+ auto updatedSection = Build<TYtSection>(ctx, innerOpSection.Pos())
+ .InitFrom(innerOpSection)
+ .Settings(NYql::MergeSettings(innerOpSection.Settings().Ref(), *NYql::KeepOnlySettings(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2, ctx), ctx))
+ .Done();
+
+ auto updatedSectionList = Build<TYtSectionList>(ctx, innerOp.Input().Pos()).Add(updatedSection).Done();
+ auto updatedInnerOp = ctx.ChangeChild(innerOp.Ref(), TYtTransientOpBase::idx_Input, updatedSectionList.Ptr());
+ if (!syncList.empty()) {
+ updatedInnerOp = ctx.ChangeChild(*updatedInnerOp, TYtTransientOpBase::idx_World, ApplySyncListToWorld(innerOp.World().Ptr(), syncList, ctx));
+ }
+
+ updatedPaths.push_back(
+ Build<TYtPath>(ctx, path.Pos())
+ .InitFrom(path)
+ .Table<TYtOutput>()
+ .InitFrom(path.Table().Cast<TYtOutput>())
+ .Operation(updatedInnerOp)
+ .Build()
+ .Done());
+
+ hasPathUpdates = true;
+ } else {
+ updatedPaths.push_back(path);
+ allPathUpdated = false;
+ }
+ }
+ }
+ if (hasPathUpdates) {
+ hasUpdates = true;
+ if (allPathUpdated) {
+ settings = NYql::RemoveSettings(*settings, EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2, ctx);
+ settings = NYql::AddSetting(*settings, kfType, ctx.NewList(section.Pos(), {}), ctx);
+ }
+ updatedSections.push_back(
+ Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Settings(settings)
+ .Done());
+ } else {
+ updatedSections.push_back(section);
+ }
+ }
+
+ if (!hasUpdates) {
+ return node;
+ }
+
+ auto sectionList = Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done();
+
+ return TExprBase(ctx.ChangeChild(node.Ref(), TYtTransientOpBase::idx_Input, sectionList.Ptr()));
+ }
+
+ TMaybeNode<TExprBase> BypassMerge(TExprBase node, TExprContext& ctx) const {
+ if (node.Ref().HasResult()) {
+ return node;
+ }
+
+ auto op = node.Cast<TYtTransientOpBase>();
+ if (op.Maybe<TYtCopy>()) {
+ return node;
+ }
+
+ if (op.Maybe<TYtWithUserJobsOpBase>()) {
+ size_t lambdaIdx = op.Maybe<TYtMapReduce>()
+ ? TYtMapReduce::idx_Mapper
+ : op.Maybe<TYtReduce>()
+ ? TYtReduce::idx_Reducer
+ : TYtMap::idx_Mapper;
+
+ bool usesTableIndex = false;
+ VisitExpr(op.Ref().ChildPtr(lambdaIdx), [&usesTableIndex](const TExprNode::TPtr& n) {
+ if (TYtTableIndex::Match(n.Get())) {
+ usesTableIndex = true;
+ } else if (TYtOutput::Match(n.Get())) {
+ return false;
+ }
+ return !usesTableIndex;
+ });
+ if (usesTableIndex) {
+ return node;
+ }
+ }
+
+ auto maxTables = State_->Configuration->MaxInputTables.Get();
+ auto maxSortedTables = State_->Configuration->MaxInputTablesForSortedMerge.Get();
+ const bool opOrdered = NYql::HasSetting(op.Settings().Ref(), EYtSettingType::Ordered);
+ bool hasUpdates = false;
+ TVector<TExprBase> updatedSections;
+ TSyncMap syncList;
+ for (auto section: op.Input()) {
+ const EYtSettingType kfType = NYql::HasSetting(section.Settings().Ref(), EYtSettingType::KeyFilter2) ?
+ EYtSettingType::KeyFilter2 : EYtSettingType::KeyFilter;
+ const auto keyFiltersValues = NYql::GetAllSettingValues(section.Settings().Ref(), kfType);
+ const bool hasTableKeyFilters = AnyOf(keyFiltersValues,
+ [kfType](const TExprNode::TPtr& keyFilter) {
+ return keyFilter->ChildrenSize() >= GetMinChildrenForIndexedKeyFilter(kfType);
+ });
+ const bool hasTakeSkip = NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip);
+
+ bool hasPathUpdates = false;
+ TVector<TYtPath> updatedPaths;
+ size_t inputCount = section.Paths().Size();
+ if (!hasTableKeyFilters) {
+ for (auto path: section.Paths()) {
+ updatedPaths.push_back(path);
+
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ bool pathLimits = false;
+ for (auto range: path.Ranges().Cast<TExprList>()) {
+ if (range.Maybe<TYtRow>() || range.Maybe<TYtRowRange>()) {
+ pathLimits = true;
+ break;
+ }
+ }
+ if (pathLimits) {
+ continue;
+ }
+ }
+ auto maybeInnerMerge = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtMerge>();
+ if (!maybeInnerMerge) {
+ continue;
+ }
+ auto innerMerge = maybeInnerMerge.Cast();
+
+ if (innerMerge.Ref().StartsExecution() || innerMerge.Ref().HasResult()) {
+ continue;
+ }
+
+ if (NYql::HasSettingsExcept(innerMerge.Settings().Ref(), EYtSettingType::KeepSorted | EYtSettingType::Limit)) {
+ continue;
+ }
+
+ if (auto limitSetting = NYql::GetSetting(innerMerge.Settings().Ref(), EYtSettingType::Limit)) {
+ if (limitSetting->ChildPtr(1)->ChildrenSize()) {
+ continue;
+ }
+ }
+
+ auto innerMergeSection = innerMerge.Input().Item(0);
+
+ bool hasIncompatibleSettings = false;
+ TExprNode::TListType innerMergeKeyFiltersValues;
+ for (auto& setting : innerMergeSection.Settings().Ref().Children()) {
+ const auto type = FromString<EYtSettingType>(setting->Child(0)->Content());
+ if (setting->ChildrenSize() == 2 && (type == EYtSettingType::KeyFilter || type == EYtSettingType::KeyFilter2)) {
+ innerMergeKeyFiltersValues.push_back(setting->ChildPtr(1));
+ } else {
+ hasIncompatibleSettings = true;
+ break;
+ }
+ }
+
+ if (hasIncompatibleSettings) {
+ continue;
+ }
+ if (AnyOf(innerMergeKeyFiltersValues, [](const TExprNode::TPtr& keyFilter) { return keyFilter->ChildrenSize() > 0; })) {
+ continue;
+ }
+
+ const bool unordered = IsUnorderedOutput(path.Table().Cast<TYtOutput>());
+ auto mergeOutRowSpec = TYqlRowSpecInfo(innerMerge.Output().Item(0).RowSpec());
+ if (innerMergeSection.Paths().Size() > 1) {
+ const bool sortedMerge = mergeOutRowSpec.IsSorted();
+ if (hasTakeSkip && sortedMerge) {
+ continue;
+ }
+ // Only YtMap can change semantic if substitute single sorted input by multiple sorted ones.
+ // Other operations (YtMerge, YtReduce, YtMapReduce, YtEquiJoin, YtSort) can be safely optimized.
+ // YtCopy cannot, but it is ignored early
+ if (op.Maybe<TYtMap>() && opOrdered && !unordered && sortedMerge) {
+ continue;
+ }
+ auto limit = maxTables;
+ if (maxSortedTables && (op.Maybe<TYtReduce>() || (op.Maybe<TYtMerge>() && TYqlRowSpecInfo(op.Output().Item(0).RowSpec()).IsSorted()))) {
+ limit = maxSortedTables;
+ }
+ if (limit && (inputCount - 1 + innerMergeSection.Paths().Size()) > *limit) {
+ continue;
+ }
+
+ if (mergeOutRowSpec.GetAllConstraints(ctx).GetConstraint<TDistinctConstraintNode>()) {
+ continue;
+ }
+ }
+
+ hasPathUpdates = true;
+ updatedPaths.pop_back();
+ TMaybeNode<TExprBase> columns;
+ if (!path.Columns().Maybe<TCoVoid>()) {
+ columns = path.Columns();
+ } else if ((op.Maybe<TYtWithUserJobsOpBase>() || op.Maybe<TYtEquiJoin>()) && mergeOutRowSpec.HasAuxColumns()) {
+ TVector<TStringBuf> items;
+ for (auto item: mergeOutRowSpec.GetType()->GetItems()) {
+ items.push_back(item->GetName());
+ }
+ columns = ToAtomList(items, op.Pos(), ctx);
+ }
+
+ if (!columns.IsValid() && path.Ranges().Maybe<TCoVoid>() && !unordered) {
+ for (auto mergePath: innerMergeSection.Paths()) {
+ updatedPaths.push_back(mergePath);
+ }
+ } else {
+ for (auto mergePath: innerMergeSection.Paths()) {
+ auto builder = Build<TYtPath>(ctx, mergePath.Pos()).InitFrom(mergePath);
+
+ if (columns) {
+ builder.Columns(columns.Cast());
+ }
+ if (!path.Ranges().Maybe<TCoVoid>()) {
+ builder.Ranges(path.Ranges());
+ }
+
+ auto updatedPath = builder.Done();
+ if (unordered) {
+ updatedPath = MakeUnorderedPath(updatedPath, false, ctx);
+ }
+
+ updatedPaths.push_back(updatedPath);
+ }
+ }
+
+ if (innerMerge.World().Ref().Type() != TExprNode::World) {
+ syncList.emplace(innerMerge.World().Ptr(), syncList.size());
+ }
+ inputCount += innerMergeSection.Paths().Size() - 1;
+ }
+ }
+ if (hasPathUpdates) {
+ hasUpdates = true;
+ updatedSections.push_back(
+ Build<TYtSection>(ctx, section.Pos())
+ .InitFrom(section)
+ .Paths()
+ .Add(updatedPaths)
+ .Build()
+ .Done());
+ } else {
+ updatedSections.push_back(section);
+ }
+ }
+ if (!hasUpdates) {
+ return node;
+ }
+
+ auto sectionList = Build<TYtSectionList>(ctx, op.Input().Pos())
+ .Add(updatedSections)
+ .Done();
+
+ auto res = ctx.ChangeChild(node.Ref(), TYtTransientOpBase::idx_Input, sectionList.Ptr());
+ if (!syncList.empty()) {
+ res = ctx.ChangeChild(*res, TYtTransientOpBase::idx_World, ApplySyncListToWorld(op.World().Ptr(), syncList, ctx));
+ }
+ return TExprBase(res);
+ }
+
+ TMaybeNode<TExprBase> BypassMergeBeforePublish(TExprBase node, TExprContext& ctx) const {
+ if (node.Ref().HasResult()) {
+ return node;
+ }
+
+ auto publish = node.Cast<TYtPublish>();
+
+ auto cluster = publish.DataSink().Cluster().StringValue();
+ auto path = publish.Publish().Name().StringValue();
+ auto commitEpoch = TEpochInfo::Parse(publish.Publish().CommitEpoch().Ref()).GetOrElse(0);
+
+ auto dstRowSpec = State_->TablesData->GetTable(cluster, path, commitEpoch).RowSpec;
+
+ auto maxTables = dstRowSpec->IsSorted() ? State_->Configuration->MaxInputTablesForSortedMerge.Get() : State_->Configuration->MaxInputTables.Get();
+ bool hasUpdates = false;
+ TVector<TYtOutput> updateInputs;
+ size_t inputCount = publish.Input().Size();
+ for (auto out: publish.Input()) {
+ updateInputs.push_back(out);
+ if (auto maybeMerge = out.Operation().Maybe<TYtMerge>()) {
+ auto merge = maybeMerge.Cast();
+
+ if (!merge.World().Ref().IsWorld()) {
+ continue;
+ }
+
+ if (merge.Ref().StartsExecution() || merge.Ref().HasResult()) {
+ continue;
+ }
+
+ if (merge.Settings().Size() != 0) {
+ continue;
+ }
+
+ auto mergeSection = merge.Input().Item(0);
+ if (NYql::HasSettingsExcept(mergeSection.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2)) {
+ continue;
+ }
+ if (HasNonEmptyKeyFilter(mergeSection)) {
+ continue;
+ }
+
+ if (maxTables && inputCount + mergeSection.Paths().Size() - 1 > *maxTables) {
+ continue;
+ }
+
+ if (mergeSection.Paths().Size() < 2) {
+ continue;
+ }
+
+ if (!AllOf(mergeSection.Paths(), [](TYtPath path) {
+ return path.Table().Maybe<TYtOutput>()
+ && path.Columns().Maybe<TCoVoid>()
+ && path.Ranges().Maybe<TCoVoid>()
+ && !TYtTableBaseInfo::GetMeta(path.Table())->IsDynamic;
+ })) {
+ continue;
+ }
+
+ if (dstRowSpec->GetColumnOrder().Defined() && AnyOf(mergeSection.Paths(), [colOrder = *dstRowSpec->GetColumnOrder()](auto path) {
+ auto rowSpec = TYtTableBaseInfo::GetRowSpec(path.Table());
+ return rowSpec->GetColumnOrder().Defined() && rowSpec->GetColumnOrder() != colOrder;
+ })) {
+ continue;
+ }
+
+ hasUpdates = true;
+ inputCount += mergeSection.Paths().Size() - 1;
+ updateInputs.pop_back();
+ if (IsUnorderedOutput(out)) {
+ std::transform(mergeSection.Paths().begin(), mergeSection.Paths().end(), std::back_inserter(updateInputs),
+ [mode = out.Mode(), &ctx](TYtPath path) {
+ auto origOut = path.Table().Cast<TYtOutput>();
+ return Build<TYtOutput>(ctx, origOut.Pos())
+ .InitFrom(origOut)
+ .Mode(mode)
+ .Done();
+ }
+ );
+ } else {
+ std::transform(mergeSection.Paths().begin(), mergeSection.Paths().end(), std::back_inserter(updateInputs),
+ [](TYtPath path) {
+ return path.Table().Cast<TYtOutput>();
+ }
+ );
+ }
+ }
+ }
+ if (hasUpdates) {
+ return Build<TYtPublish>(ctx, publish.Pos())
+ .InitFrom(publish)
+ .Input()
+ .Add(updateInputs)
+ .Build()
+ .Done().Ptr();
+ }
+ return node;
+ }
+
+ TMaybeNode<TExprBase> MapToMerge(TExprBase node, TExprContext& ctx) const {
+ auto map = node.Cast<TYtMap>();
+
+ auto mapper = map.Mapper();
+ if (mapper.Body().Raw() != mapper.Args().Arg(0).Raw()) {
+ // Only trivial lambda
+ return node;
+ }
+
+ if (map.Ref().HasResult()) {
+ return node;
+ }
+
+ if (map.Input().Size() > 1 || map.Output().Size() > 1) {
+ return node;
+ }
+
+ if (NYql::HasAnySetting(map.Settings().Ref(), EYtSettingType::JobCount | EYtSettingType::WeakFields | EYtSettingType::Sharded | EYtSettingType::SortLimitBy)) {
+ return node;
+ }
+
+ auto section = map.Input().Item(0);
+ if (NYql::HasSetting(section.Settings().Ref(), EYtSettingType::SysColumns)) {
+ return node;
+ }
+ bool useExplicitColumns = false;
+ const auto outRowSpec = TYqlRowSpecInfo(map.Output().Item(0).RowSpec());
+ const auto nativeType = outRowSpec.GetNativeYtType();
+ const auto nativeTypeFlags = outRowSpec.GetNativeYtTypeFlags();
+
+ for (auto path: section.Paths()) {
+ TYtPathInfo pathInfo(path);
+ if (pathInfo.RequiresRemap()) {
+ return node;
+ }
+ if (nativeType != pathInfo.GetNativeYtType()
+ || nativeTypeFlags != pathInfo.GetNativeYtTypeFlags()) {
+ return node;
+ }
+ if (!pathInfo.HasColumns() && (!pathInfo.Table->IsTemp || (pathInfo.Table->RowSpec && pathInfo.Table->RowSpec->HasAuxColumns()))) {
+ useExplicitColumns = true;
+ }
+ }
+
+ if (auto outSorted = map.Output().Item(0).Ref().GetConstraint<TSortedConstraintNode>()) {
+ auto inputSorted = map.Input().Item(0).Ref().GetConstraint<TSortedConstraintNode>();
+ if (!inputSorted || !outSorted->IsPrefixOf(*inputSorted)) {
+ // Don't convert YtMap, which produces sorted output from unsorted input
+ return node;
+ }
+ if (auto maxTablesForSortedMerge = State_->Configuration->MaxInputTablesForSortedMerge.Get()) {
+ if (map.Input().Item(0).Paths().Size() > *maxTablesForSortedMerge) {
+ return node;
+ }
+ }
+ }
+
+ if (useExplicitColumns) {
+ auto inputStructType = section.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ TSet<TStringBuf> columns;
+ for (auto item: inputStructType->GetItems()) {
+ columns.insert(item->GetName());
+ }
+
+ section = UpdateInputFields(section, std::move(columns), ctx, false);
+ }
+
+ return Build<TYtMerge>(ctx, node.Pos())
+ .World(map.World())
+ .DataSink(map.DataSink())
+ .Output(map.Output())
+ .Input()
+ .Add(section)
+ .Build()
+ .Settings(NYql::KeepOnlySettings(map.Settings().Ref(), EYtSettingType::Limit | EYtSettingType::KeepSorted, ctx))
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> UnorderedPublishTarget(TExprBase node, TExprContext& ctx) const {
+ auto publish = node.Cast<TYtPublish>();
+
+ auto cluster = TString{publish.DataSink().Cluster().Value()};
+ auto pubTableInfo = TYtTableInfo(publish.Publish());
+ if (auto commitEpoch = pubTableInfo.CommitEpoch.GetOrElse(0)) {
+ const TYtTableDescription& nextDescription = State_->TablesData->GetTable(cluster, pubTableInfo.Name, commitEpoch);
+ YQL_ENSURE(nextDescription.RowSpec);
+ if (!nextDescription.RowSpec->IsSorted()) {
+ bool modified = false;
+ TVector<TYtOutput> outs;
+ for (auto out: publish.Input()) {
+ if (!IsUnorderedOutput(out) && TYqlRowSpecInfo(GetOutTable(out).Cast<TYtOutTable>().RowSpec()).IsSorted()) {
+ outs.push_back(Build<TYtOutput>(ctx, out.Pos())
+ .InitFrom(out)
+ .Mode()
+ .Value(ToString(EYtSettingType::Unordered))
+ .Build()
+ .Done());
+ modified = true;
+ } else {
+ outs.push_back(out);
+ }
+ }
+ if (modified) {
+ return Build<TYtPublish>(ctx, publish.Pos())
+ .InitFrom(publish)
+ .Input()
+ .Add(outs)
+ .Build()
+ .Done();
+ }
+ }
+ }
+ return node;
+ }
+
+ TMaybeNode<TExprBase> AddTrivialMapperForNativeYtTypes(TExprBase node, TExprContext& ctx) const {
+ if (State_->Configuration->UseIntermediateSchema.Get().GetOrElse(DEFAULT_USE_INTERMEDIATE_SCHEMA)) {
+ return node;
+ }
+
+ auto op = node.Cast<TYtMapReduce>();
+ if (!op.Maybe<TYtMapReduce>().Mapper().Maybe<TCoVoid>()) {
+ return node;
+ }
+
+ bool needMapper = AnyOf(op.Input(), [](const TYtSection& section) {
+ return AnyOf(section.Paths(), [](const TYtPath& path) {
+ auto rowSpec = TYtTableBaseInfo::GetRowSpec(path.Table());
+ return rowSpec && 0 != rowSpec->GetNativeYtTypeFlags();
+ });
+ });
+
+ if (!needMapper) {
+ return node;
+ }
+
+ auto mapper = Build<TCoLambda>(ctx, node.Pos()).Args({"stream"}).Body("stream").Done();
+
+ return ctx.ChangeChild(node.Ref(), TYtMapReduce::idx_Mapper, mapper.Ptr());
+ }
+
+ TMaybeNode<TExprBase> YtDqWrite(TExprBase node, TExprContext& ctx) const {
+ const auto write = node.Cast<TYtDqWrite>();
+
+ if (ETypeAnnotationKind::Stream == write.Ref().GetTypeAnn()->GetKind()) {
+ return Build<TCoFromFlow>(ctx, write.Pos())
+ .Input<TYtDqWrite>()
+ .Settings(write.Settings())
+ .Input<TCoToFlow>()
+ .Input(write.Input())
+ .Build()
+ .Build().Done();
+ }
+
+ const auto& items = GetSeqItemType(write.Input().Ref().GetTypeAnn())->Cast<TStructExprType>()->GetItems();
+ auto expand = ctx.Builder(write.Pos())
+ .Callable("ExpandMap")
+ .Add(0, write.Input().Ptr())
+ .Lambda(1)
+ .Param("item")
+ .Do([&](TExprNodeBuilder& lambda) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : items) {
+ lambda.Callable(i++, "Member")
+ .Arg(0, "item")
+ .Atom(1, item->GetName())
+ .Seal();
+ }
+ return lambda;
+ })
+ .Seal()
+ .Seal().Build();
+
+ return Build<TCoDiscard>(ctx, write.Pos())
+ .Input<TYtDqWideWrite>()
+ .Input(std::move(expand))
+ .Settings(write.Settings())
+ .Build().Done();
+ }
+
+ TMaybeNode<TExprBase> PushDownYtMapOverSortedMerge(TExprBase node, TExprContext& ctx, const TGetParents& getParents) const {
+ auto map = node.Cast<TYtMap>();
+
+ if (map.Ref().HasResult()) {
+ return node;
+ }
+
+ if (map.Input().Size() > 1 || map.Output().Size() > 1) {
+ return node;
+ }
+
+ if (NYql::HasAnySetting(map.Settings().Ref(), EYtSettingType::Sharded | EYtSettingType::JobCount)) {
+ return node;
+ }
+
+ if (!NYql::HasSetting(map.Settings().Ref(), EYtSettingType::Ordered)) {
+ return node;
+ }
+
+ auto section = map.Input().Item(0);
+ if (section.Paths().Size() > 1) {
+ return node;
+ }
+ if (NYql::HasSettingsExcept(section.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2)) {
+ return node;
+ }
+ if (NYql::HasNonEmptyKeyFilter(section)) {
+ return node;
+ }
+ auto path = section.Paths().Item(0);
+ if (!path.Columns().Maybe<TCoVoid>() || !path.Ranges().Maybe<TCoVoid>()) {
+ return node;
+ }
+ auto maybeMerge = path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtMerge>();
+ if (!maybeMerge) {
+ return node;
+ }
+ auto merge = maybeMerge.Cast();
+ if (merge.Ref().StartsExecution() || merge.Ref().HasResult()) {
+ return node;
+ }
+ const auto rowSpec = TYqlRowSpecInfo(merge.Output().Item(0).RowSpec());
+ if (!rowSpec.IsSorted()) {
+ return node;
+ }
+
+ auto mergeSection = merge.Input().Item(0);
+ if (NYql::HasSettingsExcept(mergeSection.Settings().Ref(), EYtSettingType::KeyFilter | EYtSettingType::KeyFilter2)) {
+ return node;
+ }
+ if (NYql::HasNonEmptyKeyFilter(mergeSection)) {
+ return node;
+ }
+ if (merge.Settings().Size() > 0) {
+ return node;
+ }
+
+ const TParentsMap* parentsMap = getParents();
+ if (IsOutputUsedMultipleTimes(merge.Ref(), *parentsMap)) {
+ // Merge output is used more than once
+ return node;
+ }
+
+ auto world = map.World().Ptr();
+ if (!merge.World().Ref().IsWorld()) {
+ world = Build<TCoSync>(ctx, map.Pos()).Add(world).Add(merge.World()).Done().Ptr();
+ }
+ TVector<TYtPath> paths;
+ for (auto path: mergeSection.Paths()) {
+ auto newPath = Build<TYtPath>(ctx, map.Pos())
+ .Table<TYtOutput>()
+ .Operation<TYtMap>()
+ .InitFrom(map)
+ .World(world)
+ .Input()
+ .Add()
+ .Paths()
+ .Add(path)
+ .Build()
+ .Settings(section.Settings())
+ .Build()
+ .Build()
+ .Build()
+ .OutIndex().Value("0").Build()
+ .Build()
+ .Columns<TCoVoid>().Build()
+ .Ranges<TCoVoid>().Build()
+ .Stat<TCoVoid>().Build()
+ .Done();
+ paths.push_back(std::move(newPath));
+ }
+
+ return Build<TYtMerge>(ctx, node.Pos())
+ .World<TCoWorld>().Build()
+ .DataSink(merge.DataSink())
+ .Output(map.Output()) // Rewrite output type from YtMap
+ .Input()
+ .Add()
+ .Paths()
+ .Add(paths)
+ .Build()
+ .Settings(mergeSection.Settings())
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Done();
+ }
+
+ TMaybeNode<TExprBase> MergeToCopy(TExprBase node, TExprContext& ctx) const {
+ auto merge = node.Cast<TYtMerge>();
+
+ if (merge.Ref().HasResult()) {
+ return node;
+ }
+
+ if (merge.Input().Item(0).Paths().Size() > 1) {
+ return node;
+ }
+
+ if (NYql::HasAnySetting(merge.Settings().Ref(), EYtSettingType::ForceTransform | EYtSettingType::CombineChunks)) {
+ return node;
+ }
+
+ auto limitNode = NYql::GetSetting(merge.Settings().Ref(), EYtSettingType::Limit);
+ if (limitNode && limitNode->ChildrenSize() > 0) {
+ return node;
+ }
+
+ TYtSection section = merge.Input().Item(0);
+ TYtPath path = section.Paths().Item(0);
+ if (!path.Ranges().Maybe<TCoVoid>() || !path.Ref().GetTypeAnn()->Equals(*path.Table().Ref().GetTypeAnn())) {
+ return node;
+ }
+ if (path.Table().Maybe<TYtOutput>().Operation().Maybe<TYtEquiJoin>()) {
+ // YtEquiJoin may change output sort after rewrite
+ return node;
+ }
+ auto tableInfo = TYtTableBaseInfo::Parse(path.Table());
+ if (path.Table().Maybe<TYtTable>() || tableInfo->Meta->IsDynamic || !tableInfo->RowSpec || !tableInfo->RowSpec->StrictSchema) {
+ return node;
+ }
+ if (tableInfo->IsUnordered && tableInfo->RowSpec->IsSorted()) {
+ return node;
+ }
+ if (NYql::HasAnySetting(section.Settings().Ref(), EYtSettingType::Take | EYtSettingType::Skip | EYtSettingType::Sample)) {
+ return node;
+ }
+ if (NYql::HasNonEmptyKeyFilter(section)) {
+ return node;
+ }
+ TYtOutTableInfo outTableInfo(merge.Output().Item(0));
+ if (!tableInfo->RowSpec->CompareSortness(*outTableInfo.RowSpec)) {
+ return node;
+ }
+
+ return Build<TYtCopy>(ctx, node.Pos())
+ .World(merge.World())
+ .DataSink(merge.DataSink())
+ .Output(merge.Output())
+ .Input()
+ .Add()
+ .Paths()
+ .Add()
+ .InitFrom(path)
+ .Columns<TCoVoid>().Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Build()
+ .Build()
+ .Settings()
+ .Build()
+ .Done();
+ }
+private:
+ const TYtState::TPtr State_;
+};
+
+class TAsyncSyncCompositeTransformer : public TGraphTransformerBase {
+public:
+ TAsyncSyncCompositeTransformer(THolder<IGraphTransformer>&& async, THolder<IGraphTransformer>&& sync)
+ : Async(std::move(async))
+ , Sync(std::move(sync))
+ {
+ }
+private:
+ TStatus DoTransform(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) override {
+ auto status = Async->Transform(input, output, ctx);
+ if (status.Level != TStatus::Ok) {
+ return status;
+ }
+ return InstantTransform(*Sync, output, ctx, true);
+ }
+
+ NThreading::TFuture<void> DoGetAsyncFuture(const TExprNode& input) override {
+ return Async->GetAsyncFuture(input);
+ }
+
+ TStatus DoApplyAsyncChanges(TExprNode::TPtr input, TExprNode::TPtr& output, TExprContext& ctx) override {
+ return Async->ApplyAsyncChanges(input, output, ctx);
+ }
+
+ void Rewind() final {
+ Async->Rewind();
+ Sync->Rewind();
+ }
+
+ const THolder<IGraphTransformer> Async;
+ const THolder<IGraphTransformer> Sync;
+
+};
+
+} // namespce
+
+THolder<IGraphTransformer> CreateYtPhysicalOptProposalTransformer(TYtState::TPtr state) {
+ return THolder(new TAsyncSyncCompositeTransformer(CreateYtLoadColumnarStatsTransformer(state),
+ THolder<IGraphTransformer>(new TYtPhysicalOptProposalTransformer(state))));
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
new file mode 100644
index 0000000000..c4a2fe6492
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_provider.cpp
@@ -0,0 +1,504 @@
+#include "yql_yt_provider.h"
+#include "yql_yt_dq_integration.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/providers/common/provider/yql_provider_names.h>
+#include <ydb/library/yql/providers/common/proto/gateways_config.pb.h>
+#include <ydb/library/yql/providers/common/activation/yql_activation.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+
+#include <util/generic/singleton.h>
+
+namespace NYql {
+
+bool TYtTableDescription::Fill(const TString& cluster, const TString& table, TExprContext& ctx, IModuleResolver* moduleResolver) {
+ const TStructExprType* type = RowSpec ? RowSpec->GetType() : nullptr;
+ if (!type) {
+ TVector<const TItemExprType*> items;
+ if (Meta->YqlCompatibleScheme) {
+ for (auto& name : YAMR_FIELDS) {
+ items.push_back(ctx.MakeType<TItemExprType>(name, ctx.MakeType<TDataExprType>(EDataSlot::String)));
+ }
+ }
+ type = ctx.MakeType<TStructExprType>(items);
+ }
+
+ if (!TYtTableDescriptionBase::Fill(TString{YtProviderName}, cluster,
+ table, type, Meta->SqlView, Meta->SqlViewSyntaxVersion, Meta->Attrs, ctx, moduleResolver)) {
+ return false;
+ }
+ if (QB2RowSpec) {
+ RowType = QB2RowSpec->GetType();
+ }
+ return true;
+}
+
+void TYtTableDescription::ToYson(NYson::TYsonWriter& writer, const TString& cluster, const TString& table, const TString& view) const
+{
+ YQL_ENSURE(Stat);
+ YQL_ENSURE(Meta);
+ const bool isView = !view.empty() || View.Defined();
+ const TYtViewDescription* viewMeta = !view.empty() ? Views.FindPtr(view) : View.Get();
+
+ writer.OnBeginMap();
+ writer.OnKeyedItem("Cluster");
+ writer.OnStringScalar(cluster);
+ writer.OnKeyedItem("Name");
+ writer.OnStringScalar(table);
+ if (isView) {
+ YQL_ENSURE(viewMeta);
+ writer.OnKeyedItem("View");
+ writer.OnStringScalar(!view.empty() ? view : table);
+ writer.OnKeyedItem("Sql");
+ writer.OnStringScalar(viewMeta->Sql);
+ } else {
+ writer.OnKeyedItem("DoesExist");
+ writer.OnBooleanScalar(Meta->DoesExist);
+ writer.OnKeyedItem("IsEmpty");
+ writer.OnBooleanScalar(Stat->IsEmpty());
+ writer.OnKeyedItem("IsSorted");
+ writer.OnBooleanScalar(RowSpec && RowSpec->IsSorted());
+ writer.OnKeyedItem("IsDynamic");
+ writer.OnBooleanScalar(Meta->IsDynamic);
+ writer.OnKeyedItem("UniqueKeys");
+ writer.OnBooleanScalar(RowSpec && RowSpec->UniqueKeys);
+ writer.OnKeyedItem("CanWrite");
+ writer.OnBooleanScalar(Meta->CanWrite);
+ writer.OnKeyedItem("RecordsCount");
+ writer.OnInt64Scalar(Stat->RecordsCount);
+ writer.OnKeyedItem("DataSize");
+ writer.OnInt64Scalar(Stat->DataSize);
+ writer.OnKeyedItem("ChunkCount");
+ writer.OnInt64Scalar(Stat->ChunkCount);
+ writer.OnKeyedItem("ModifyTime");
+ writer.OnInt64Scalar(Stat->ModifyTime);
+ writer.OnKeyedItem("Id");
+ writer.OnStringScalar(Stat->Id);
+ writer.OnKeyedItem("Revision");
+ writer.OnUint64Scalar(Stat->Revision);
+ writer.OnKeyedItem("IsRealData");
+
+ bool isRealData = !Meta->Attrs.contains(QB2Premapper) && !Meta->Attrs.contains(YqlReadUdfAttribute) && !IgnoreTypeV3;
+ if (isRealData) {
+ for (auto& x: Meta->Attrs) {
+ if (x.first.StartsWith(YqlProtoFieldPrefixAttribute)) {
+ isRealData = false;
+ break;
+ }
+ }
+ }
+ writer.OnBooleanScalar(isRealData);
+ writer.OnKeyedItem("YqlCompatibleSchema");
+ writer.OnBooleanScalar(Meta->YqlCompatibleScheme);
+ }
+
+ const TTypeAnnotationNode* rowType = viewMeta
+ ? viewMeta->RowType
+ : QB2RowSpec
+ ? QB2RowSpec->GetType()
+ : RowType;
+ auto rowSpec = QB2RowSpec ? QB2RowSpec : RowSpec;
+
+ // fields
+
+ auto writeSortOrder = [&writer](TMaybe<size_t> order, TMaybe<bool> ascending) {
+ writer.OnKeyedItem("ClusterSortOrder");
+ writer.OnBeginList();
+ if (order) {
+ writer.OnListItem();
+ writer.OnInt64Scalar(*order);
+ }
+ writer.OnEndList();
+
+ writer.OnKeyedItem("Ascending");
+ writer.OnBeginList();
+ if (ascending) {
+ writer.OnListItem();
+ writer.OnBooleanScalar(*ascending);
+ }
+ writer.OnEndList();
+ };
+
+ writer.OnKeyedItem("Fields");
+ writer.OnBeginList();
+ if ((isView || Meta->DoesExist) && rowType->GetKind() == ETypeAnnotationKind::Struct) {
+ for (auto& item: rowType->Cast<TStructExprType>()->GetItems()) {
+ writer.OnListItem();
+
+ auto name = item->GetName();
+ writer.OnBeginMap();
+
+ writer.OnKeyedItem("Name");
+ writer.OnStringScalar(name);
+
+ writer.OnKeyedItem("Type");
+ NCommon::WriteTypeToYson(writer, item->GetItemType());
+
+ size_t fieldIdx = rowSpec && rowSpec->IsSorted()
+ ? FindIndex(rowSpec->SortMembers, name)
+ : NPOS;
+
+ if (fieldIdx != NPOS) {
+ bool ascending = !rowSpec->SortDirections.empty()
+ ? rowSpec->SortDirections.at(fieldIdx)
+ : true;
+ writeSortOrder(fieldIdx, ascending);
+ } else {
+ writeSortOrder(Nothing(), Nothing());
+ }
+ writer.OnEndMap();
+ }
+ }
+ writer.OnEndList();
+
+ writer.OnKeyedItem("RowType");
+ NCommon::WriteTypeToYson(writer, rowType);
+
+ if (!isView) {
+ // meta attr
+ writer.OnKeyedItem("MetaAttr");
+ writer.OnBeginMap();
+
+ for (const auto& attr : Meta->Attrs) {
+ if (attr.first.StartsWith("_yql")) {
+ continue;
+ }
+
+ writer.OnKeyedItem(attr.first);
+ writer.OnStringScalar(attr.second);
+ }
+
+ writer.OnEndMap();
+
+ // views
+ writer.OnKeyedItem("Views");
+ writer.OnBeginList();
+
+ for (const auto& attr : Meta->Attrs) {
+ if (!attr.first.StartsWith(YqlViewPrefixAttribute) || attr.first.size() == YqlViewPrefixAttribute.size()) {
+ continue;
+ }
+
+ writer.OnListItem();
+ writer.OnStringScalar(attr.first.substr(YqlViewPrefixAttribute.size()));
+ }
+
+ writer.OnEndList();
+ }
+
+ writer.OnEndMap();
+}
+
+bool TYtTableDescription::Validate(TPosition pos, TStringBuf cluster, TStringBuf tableName, bool withQB,
+ const THashMap<std::pair<TString, TString>, TString>& anonymousLabels, TExprContext& ctx) const {
+ auto rowSpec = withQB ? QB2RowSpec : RowSpec;
+ if (FailOnInvalidSchema
+ && !rowSpec
+ && !Meta->YqlCompatibleScheme
+ && !Meta->InferredScheme
+ ) {
+ TMaybe<TString> anonLabel;
+ for (const auto& x : anonymousLabels) {
+ if (x.first.first == cluster && x.second == tableName) {
+ anonLabel = x.first.second;
+ break;
+ }
+ }
+
+ if (anonLabel) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Anonymous table '" << *anonLabel << "' must be materialized. Use COMMIT before reading from it."));
+ } else if (InferSchemaRows > 0) {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Cannot infer schema for table "
+ << TString{tableName}.Quote() << ", table is empty").SetCode(TIssuesIds::YT_SCHEMELESS_TABLE, TSeverityIds::S_ERROR));
+ } else {
+ ctx.AddError(TIssue(pos, TStringBuilder() << "Table "
+ << TString{tableName}.Quote()
+ << " does not have any scheme attribute supported by YQL,"
+ << " you can add \"PRAGMA yt.InferSchema = '1';\" to"
+ << " your query in order to use types of first data row"
+ << " as scheme.").SetCode(TIssuesIds::YT_SCHEMELESS_TABLE, TSeverityIds::S_ERROR));
+ }
+ return false;
+ }
+ return true;
+}
+
+void TYtTableDescription::SetConstraintsReady() {
+ ConstraintsReady = true;
+ if (RowSpec && Constraints) {
+ RowSpec->SetConstraints(Constraints);
+ if (const auto sorted = Constraints.GetConstraint<TSortedConstraintNode>()) {
+ if (const auto distinct = Constraints.GetConstraint<TDistinctConstraintNode>()) {
+ RowSpec->UniqueKeys = distinct->IsOrderBy(*sorted);
+ }
+ }
+ }
+}
+
+bool TYtTableDescription::FillViews(const TString& cluster, const TString& table, TExprContext& ctx, IModuleResolver* moduleResolver) {
+ return TYtTableDescriptionBase::FillViews(TString{YtProviderName}, cluster, table, Meta->Attrs, ctx, moduleResolver);
+}
+
+const TYtTableDescription& TYtTablesData::GetTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) const {
+ auto p = Tables.FindPtr(std::make_tuple(cluster, table, epoch.GetOrElse(0)));
+ YQL_ENSURE(p, "Table description is not defined: " << cluster << '.' << table << "[epoch=" << epoch.GetOrElse(0) << ']');
+ return *p;
+}
+
+const TYtTableDescription* TYtTablesData::FindTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) const {
+ return Tables.FindPtr(std::make_tuple(cluster, table, epoch.GetOrElse(0)));
+}
+
+TYtTableDescription& TYtTablesData::GetOrAddTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) {
+ return Tables[std::make_tuple(cluster, table, epoch.GetOrElse(0))];
+}
+
+TYtTableDescription& TYtTablesData::GetModifTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) {
+ auto p = Tables.FindPtr(std::make_tuple(cluster, table, epoch.GetOrElse(0)));
+ YQL_ENSURE(p, "Table description is not defined: " << cluster << '.' << table << "[epoch=" << epoch.GetOrElse(0) << ']');
+ return *p;
+}
+
+TVector<std::pair<TString, TString>> TYtTablesData::GetAllEpochTables(ui32 epoch) const {
+ TVector<std::pair<TString, TString>> res;
+ res.reserve(Tables.size());
+ for (const auto& item: Tables) {
+ if (std::get<2>(item.first) == epoch) {
+ res.emplace_back(std::get<0>(item.first), std::get<1>(item.first));
+ }
+ }
+ return res;
+}
+
+TVector<std::pair<TString, TString>> TYtTablesData::GetAllZeroEpochTables() const {
+ return GetAllEpochTables(0U);
+}
+
+void TYtTablesData::ForEach(const std::function<void(const TString&, const TString&, ui32, const TYtTableDescription&)>& cb) const {
+ for (const auto& item: Tables) {
+ cb(std::get<0>(item.first), std::get<1>(item.first), std::get<2>(item.first), item.second);
+ }
+}
+
+void TYtTablesData::CleanupCompiledSQL() {
+ for (auto& item: Tables) {
+ item.second.CleanupCompiledSQL();
+ }
+}
+
+void TYtState::Reset() {
+ LoadEpochMetadata.Clear();
+ EpochDependencies.clear();
+ Configuration->ClearVersions();
+ TablesData = MakeIntrusive<TYtTablesData>();
+ AnonymousLabels.clear();
+ NodeHash.clear();
+ Checkpoints.clear();
+ NextEpochId = 1;
+}
+
+void TYtState::EnterEvaluation(ui64 id) {
+ bool res = ConfigurationEvalStates_.emplace(id, Configuration->GetState()).second;
+ YQL_ENSURE(res, "Duplicate evaluation state " << id);
+
+ res = EpochEvalStates_.emplace(id, NextEpochId).second;
+ YQL_ENSURE(res, "Duplicate evaluation state " << id);
+}
+
+void TYtState::LeaveEvaluation(ui64 id) {
+ {
+ auto it = ConfigurationEvalStates_.find(id);
+ YQL_ENSURE(it != ConfigurationEvalStates_.end());
+ Configuration->RestoreState(std::move(it->second));
+ ConfigurationEvalStates_.erase(it);
+ }
+
+ {
+ auto it = EpochEvalStates_.find(id);
+ YQL_ENSURE(it != EpochEvalStates_.end());
+ NextEpochId = it->second;
+ EpochEvalStates_.erase(it);
+ }
+}
+
+TDataProviderInitializer GetYtNativeDataProviderInitializer(IYtGateway::TPtr gateway) {
+ return [gateway] (
+ const TString& userName,
+ const TString& sessionId,
+ const TGatewaysConfig* gatewaysConfig,
+ const NKikimr::NMiniKQL::IFunctionRegistry* functionRegistry,
+ TIntrusivePtr<IRandomProvider> randomProvider,
+ TIntrusivePtr<TTypeAnnotationContext> typeCtx,
+ const TOperationProgressWriter& progressWriter,
+ const TYqlOperationOptions& operationOptions,
+ THiddenQueryAborter
+ ) {
+ Y_UNUSED(functionRegistry);
+ Y_UNUSED(randomProvider);
+ Y_UNUSED(progressWriter);
+ Y_UNUSED(operationOptions);
+ TDataProviderInfo info;
+ info.SupportsHidden = true;
+
+ auto ytState = MakeIntrusive<TYtState>();
+ ytState->SessionId = sessionId;
+ ytState->Gateway = gateway;
+ ytState->Types = typeCtx.Get();
+ ytState->DqIntegration_ = CreateYtDqIntegration(ytState.Get());
+
+ TStatWriter statWriter = [ytState](ui32 publicId, const TVector<TOperationStatistics::TEntry>& stat) {
+ with_lock(ytState->StatisticsMutex) {
+ for (size_t i = 0; i < stat.size(); ++i) {
+ ytState->Statistics[publicId].Entries.push_back(stat[i]);
+ }
+ }
+ };
+
+ if (gatewaysConfig) {
+ auto filter = [userName, ytState](const NYql::TAttr& attr) -> bool {
+ if (!attr.HasActivation()) {
+ return true;
+ }
+ if (NConfig::Allow(attr.GetActivation(), userName)) {
+ with_lock(ytState->StatisticsMutex) {
+ ytState->Statistics[Max<ui32>()].Entries.emplace_back(TStringBuilder() << "Activation:" << attr.GetName(), 0, 0, 0, 0, 1);
+ }
+ return true;
+ }
+ return false;
+ };
+
+ ytState->Configuration->Init(gatewaysConfig->GetYt(), filter, *typeCtx);
+ }
+
+ info.Names.insert({TString{YtProviderName}});
+ info.Source = CreateYtDataSource(ytState);
+ info.Sink = CreateYtDataSink(ytState);
+ info.SupportFullResultDataSink = true;
+ info.OpenSession = [gateway, statWriter](const TString& sessionId, const TString& username,
+ const TOperationProgressWriter& progressWriter, const TYqlOperationOptions& operationOptions,
+ TIntrusivePtr<IRandomProvider> randomProvider, TIntrusivePtr<ITimeProvider> timeProvider) {
+ gateway->OpenSession(
+ IYtGateway::TOpenSessionOptions(sessionId)
+ .UserName(username)
+ .ProgressWriter(progressWriter)
+ .OperationOptions(operationOptions)
+ .RandomProvider(randomProvider)
+ .TimeProvider(timeProvider)
+ .StatWriter(statWriter)
+ );
+ return NThreading::MakeFuture();
+ };
+
+ info.CleanupSession = [ytState, gateway](const TString& sessionId) {
+ gateway->CleanupSession(IYtGateway::TCleanupSessionOptions(sessionId));
+ };
+
+ info.CloseSession = [ytState, gateway](const TString& sessionId) {
+ gateway->CloseSession(IYtGateway::TCloseSessionOptions(sessionId));
+ // do manual cleanup; otherwise there may be dead nodes at program termination
+ // in setup with several providers
+ ytState->TablesData->CleanupCompiledSQL();
+ };
+
+ info.TokenResolver = [ytState, gateway](const TString& url, const TString& alias) -> TString {
+ TString cluster;
+ // assume it is not a YT link at all
+ if (!gateway->TryParseYtUrl(url, &cluster, nullptr) && !url.StartsWith("yt:") && alias != "yt") {
+ return {};
+ }
+
+ // todo: get token by cluster name from Auth when it will be implemented
+ if (auto token = ytState->Configuration->Auth.Get()) {
+ return *token;
+ }
+
+ if (cluster) {
+ if (auto p = ytState->Configuration->Tokens.FindPtr(cluster)) {
+ return *p;
+ }
+ }
+ return {};
+ };
+
+ return info;
+ };
+}
+
+namespace {
+
+using namespace NNodes;
+
+struct TYtDataSourceFunctions {
+ THashSet<TStringBuf> Names;
+
+ TYtDataSourceFunctions() {
+ Names.insert(TEpoch::CallableName());
+ Names.insert(TYtMeta::CallableName());
+ Names.insert(TYtStat::CallableName());
+ Names.insert(TYqlRowSpec::CallableName());
+ Names.insert(TYtTable::CallableName());
+ Names.insert(TYtRow::CallableName());
+ Names.insert(TYtRowRange::CallableName());
+ Names.insert(TYtKeyExact::CallableName());
+ Names.insert(TYtKeyRange::CallableName());
+ Names.insert(TYtPath::CallableName());
+ Names.insert(TYtSection::CallableName());
+ Names.insert(TYtReadTable::CallableName());
+ Names.insert(TYtReadTableScheme::CallableName());
+ Names.insert(TYtTableContent::CallableName());
+ Names.insert(TYtLength::CallableName());
+ Names.insert(TYtConfigure::CallableName());
+ Names.insert(TYtTablePath::CallableName());
+ Names.insert(TYtTableRecord::CallableName());
+ Names.insert(TYtTableIndex::CallableName());
+ Names.insert(TYtIsKeySwitch::CallableName());
+ Names.insert(TYtRowNumber::CallableName());
+ Names.insert(TYtStatOutTable::CallableName());
+ }
+};
+
+struct TYtDataSinkFunctions {
+ THashSet<TStringBuf> Names;
+
+ TYtDataSinkFunctions() {
+ Names.insert(TYtOutTable::CallableName());
+ Names.insert(TYtOutput::CallableName());
+ Names.insert(TYtSort::CallableName());
+ Names.insert(TYtCopy::CallableName());
+ Names.insert(TYtMerge::CallableName());
+ Names.insert(TYtMap::CallableName());
+ Names.insert(TYtReduce::CallableName());
+ Names.insert(TYtMapReduce::CallableName());
+ Names.insert(TYtWriteTable::CallableName());
+ Names.insert(TYtFill::CallableName());
+ Names.insert(TYtTouch::CallableName());
+ Names.insert(TYtDropTable::CallableName());
+ Names.insert(TCoCommit::CallableName());
+ Names.insert(TYtPublish::CallableName());
+ Names.insert(TYtEquiJoin::CallableName());
+ Names.insert(TYtStatOut::CallableName());
+ }
+};
+
+}
+
+const THashSet<TStringBuf>& YtDataSourceFunctions() {
+ return Default<TYtDataSourceFunctions>().Names;
+}
+
+const THashSet<TStringBuf>& YtDataSinkFunctions() {
+ return Default<TYtDataSinkFunctions>().Names;
+}
+
+bool TYtState::IsHybridEnabled() const {
+ return !OnlyNativeExecution && Types->PureResultDataSource == DqProviderName
+ && Configuration->HybridDqExecution.Get().GetOrElse(DefaultHybridDqExecution) && Types->HiddenMode == EHiddenMode::Disable;
+}
+
+bool TYtState::IsHybridEnabledForCluster(const std::string_view& cluster) const {
+ return Configuration->_EnableDq.Get(TString(cluster)).GetOrElse(true);
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_provider.h b/ydb/library/yql/providers/yt/provider/yql_yt_provider.h
new file mode 100644
index 0000000000..80f08db6c1
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_provider.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include "yql_yt_gateway.h"
+#include "yql_yt_table_desc.h"
+#include "yql_yt_table.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_yt_settings.h>
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/dq/integration/yql_dq_integration.h>
+#include <ydb/library/yql/core/yql_data_provider.h>
+#include <ydb/library/yql/core/yql_execution.h>
+#include <ydb/library/yql/ast/yql_constraint.h>
+
+#include <library/cpp/yson/writer.h>
+
+#include <util/generic/string.h>
+#include <util/generic/set.h>
+#include <util/generic/hash.h>
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/strbuf.h>
+#include <util/system/mutex.h>
+#include <util/str_stl.h>
+
+#include <utility>
+#include <tuple>
+#include <unordered_map>
+
+namespace NYql {
+
+struct TYtTableDescription: public TYtTableDescriptionBase {
+ TYtTableStatInfo::TPtr Stat;
+ TYtTableMetaInfo::TPtr Meta;
+ TYqlRowSpecInfo::TPtr RowSpec;
+ TYqlRowSpecInfo::TPtr QB2RowSpec;
+ TConstraintSet Constraints;
+ bool ConstraintsReady = false;
+ bool IsAnonymous = false;
+ bool IsReplaced = false;
+ TMaybe<bool> MonotonicKeys;
+ size_t WriteValidateCount = 0;
+ TMaybe<TString> Hash;
+
+ bool Fill(const TString& cluster, const TString& table, TExprContext& ctx, IModuleResolver* moduleResolver);
+ void ToYson(NYson::TYsonWriter& writer, const TString& cluster, const TString& table, const TString& view) const;
+ bool Validate(TPosition pos, TStringBuf cluster, TStringBuf tableName, bool withQB,
+ const THashMap<std::pair<TString, TString>, TString>& anonymousLabels, TExprContext& ctx) const;
+ void SetConstraintsReady();
+ bool FillViews(const TString& cluster, const TString& table, TExprContext& ctx, IModuleResolver* moduleResolver);
+};
+
+// Anonymous tables are kept by labels
+class TYtTablesData: public TThrRefBase {
+public:
+ using TPtr = TIntrusivePtr<TYtTablesData>;
+
+ const TYtTableDescription& GetTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) const;
+ const TYtTableDescription* FindTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch) const;
+ TYtTableDescription& GetOrAddTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch);
+ TYtTableDescription& GetModifTable(const TString& cluster, const TString& table, TMaybe<ui32> epoch);
+ TVector<std::pair<TString, TString>> GetAllEpochTables(ui32 epoch) const;
+ TVector<std::pair<TString, TString>> GetAllZeroEpochTables() const;
+ void CleanupCompiledSQL();
+ void ForEach(const std::function<void(const TString&, const TString&, ui32, const TYtTableDescription&)>& cb) const;
+private:
+ using TTableKey = std::tuple<TString, TString, ui32>; // cluster + table + epoch
+ THashMap<TTableKey, TYtTableDescription> Tables;
+};
+
+
+struct TYtState : public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtState>;
+
+ void Reset();
+ void EnterEvaluation(ui64 id);
+ void LeaveEvaluation(ui64 id);
+ bool IsHybridEnabled() const;
+ bool IsHybridEnabledForCluster(const std::string_view& cluster) const;
+
+ TString SessionId;
+ IYtGateway::TPtr Gateway;
+ TTypeAnnotationContext* Types = nullptr;
+ TMaybe<std::pair<ui32, size_t>> LoadEpochMetadata; // Epoch being committed, settings versions
+ THashMap<ui32, TSet<std::pair<TString, TString>>> EpochDependencies; // List of tables, which have to be updated after committing specific epoch
+ TYtVersionedConfiguration::TPtr Configuration = MakeIntrusive<TYtVersionedConfiguration>();
+ TYtTablesData::TPtr TablesData = MakeIntrusive<TYtTablesData>();
+ THashMap<std::pair<TString, TString>, TString> AnonymousLabels; // cluster + label -> name
+ std::unordered_map<ui64, TString> NodeHash; // unique id -> hash
+ THashMap<ui32, TOperationStatistics> Statistics; // public id -> stat
+ TMutex StatisticsMutex;
+ THashSet<std::pair<TString, TString>> Checkpoints; // Set of checkpoint tables
+ THolder<IDqIntegration> DqIntegration_;
+ ui32 NextEpochId = 1;
+ bool OnlyNativeExecution = false;
+private:
+ std::unordered_map<ui64, TYtVersionedConfiguration::TState> ConfigurationEvalStates_;
+ std::unordered_map<ui64, ui32> EpochEvalStates_;
+};
+
+
+TIntrusivePtr<IDataProvider> CreateYtDataSource(TYtState::TPtr state);
+TIntrusivePtr<IDataProvider> CreateYtDataSink(TYtState::TPtr state);
+
+TDataProviderInitializer GetYtNativeDataProviderInitializer(IYtGateway::TPtr gateway);
+
+const THashSet<TStringBuf>& YtDataSourceFunctions();
+const THashSet<TStringBuf>& YtDataSinkFunctions();
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
new file mode 100644
index 0000000000..31e64025f8
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.cpp
@@ -0,0 +1,84 @@
+#include "yql_yt_provider_impl.h"
+#include "yql_yt_op_settings.h"
+#include "yql_yt_table.h"
+#include "yql_yt_helpers.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+
+#include <util/string/builder.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+void ScanPlanDependencies(const TExprNode::TPtr& input, TExprNode::TListType& children) {
+ VisitExpr(input, [&children](const TExprNode::TPtr& node) {
+ if (TMaybeNode<TYtReadTable>(node)) {
+ children.push_back(node);
+ return false;
+ }
+ if (auto maybeOutput = TMaybeNode<TYtOutput>(node)) {
+ children.push_back(GetOutputOp(maybeOutput.Cast()).Ptr());
+ return false;
+ }
+ if (node->IsCallable("DqCnResult")) { // For TYtDqProcessWrite.
+ children.push_back(node->Child(0));
+ return false;
+ }
+ return true;
+ });
+}
+
+void ScanForUsedOutputTables(const TExprNode& input, TVector<TString>& usedNodeIds)
+{
+ VisitExpr(input, [&usedNodeIds](const TExprNode& node) {
+ if (auto maybeYtOutput = TMaybeNode<TYtOutput>(&node)) {
+
+ auto ytOutput = maybeYtOutput.Cast();
+
+ TString cluster = TString{GetOutputOp(ytOutput).DataSink().Cluster().Value()};
+ TString table = TString{GetOutTable(ytOutput).Cast<TYtOutTable>().Name().Value()};
+
+ if (!cluster.empty() && !table.empty()) {
+ usedNodeIds.push_back(MakeUsedNodeId(cluster, table));
+ }
+ return false;
+ }
+ return true;
+ });
+
+}
+
+TString MakeUsedNodeId(const TString& cluster, const TString& table)
+{
+ YQL_ENSURE(!cluster.empty());
+ YQL_ENSURE(!table.empty());
+
+ return cluster + "." + table;
+}
+
+TString MakeTableDisplayName(NNodes::TExprBase table, bool isOutput) {
+ TStringBuilder name;
+ if (table.Maybe<TYtTable>()) {
+ auto ytTable = table.Cast<TYtTable>();
+ name << ytTable.Cluster().Value() << ".";
+ if (NYql::HasSetting(ytTable.Settings().Ref(), EYtSettingType::Anonymous)) {
+ name << '@' << ytTable.Name().Value();
+ }
+ else {
+ name << '`' << ytTable.Name().Value() << '`';
+ }
+ auto epoch = isOutput ? ytTable.CommitEpoch() : ytTable.Epoch();
+ if (auto epochVal = TEpochInfo::Parse(epoch.Ref()).GetOrElse(0)) {
+ name << " #" << epochVal;
+ }
+ }
+ else {
+ name << "(tmp)";
+ }
+ return name;
+}
+
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.h b/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.h
new file mode 100644
index 0000000000..1ea4777465
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_provider_impl.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "yql_yt_provider.h"
+
+#include <ydb/library/yql/providers/common/transform/yql_visit.h>
+#include <ydb/library/yql/providers/common/transform/yql_exec.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h>
+
+#include <util/generic/ptr.h>
+
+namespace NYql {
+
+THolder<IGraphTransformer> CreateYtIODiscoveryTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtEpochTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtIntentDeterminationTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtLoadTableMetadataTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtLoadColumnarStatsTransformer(TYtState::TPtr state);
+
+THolder<TVisitorTransformerBase> CreateYtDataSourceTypeAnnotationTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtDataSourceConstraintTransformer(TYtState::TPtr state);
+THolder<TExecTransformerBase> CreateYtDataSourceExecTransformer(TYtState::TPtr state);
+
+THolder<TVisitorTransformerBase> CreateYtDataSinkTypeAnnotationTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtDataSinkConstraintTransformer(TYtState::TPtr state, bool subGraph);
+THolder<TExecTransformerBase> CreateYtDataSinkExecTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtDataSinkTrackableNodesCleanupTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtDataSinkFinalizingTransformer(TYtState::TPtr state);
+
+THolder<IGraphTransformer> CreateYtLogicalOptProposalTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtPhysicalOptProposalTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtPhysicalFinalizingTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateTYtPeepholeTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateTYtWideFlowTransformer(TYtState::TPtr state);
+THolder<IGraphTransformer> CreateYtDqHybridTransformer(TYtState::TPtr state, THolder<IGraphTransformer>&& finalizer);
+
+void ScanPlanDependencies(const TExprNode::TPtr& input, TExprNode::TListType& children);
+TString MakeTableDisplayName(NNodes::TExprBase table, bool isOutput);
+
+
+void ScanForUsedOutputTables(const TExprNode& input, TVector<TString>& usedNodeIds);
+TString MakeUsedNodeId(const TString& cluster, const TString& table);
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
new file mode 100644
index 0000000000..f1fc4004fa
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table.cpp
@@ -0,0 +1,2983 @@
+#include "yql_yt_table.h"
+#include "yql_yt_key.h"
+#include "yql_yt_helpers.h"
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/core/expr_nodes/yql_expr_nodes.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/utils/log/log.h>
+#include <ydb/library/yql/public/udf/tz/udf_tz.h>
+#include <ydb/library/yql/public/decimal/yql_decimal.h>
+#include <ydb/library/yql/public/decimal/yql_decimal_serialize.h>
+#include <ydb/library/yql/minikql/mkql_type_ops.h>
+#include <ydb/library/yql/utils/utf8.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <util/stream/output.h>
+#include <util/stream/str.h>
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/split.h>
+#include <util/generic/overloaded.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/xrange.h>
+#include <util/generic/singleton.h>
+#include <util/generic/utility.h>
+
+#include <limits>
+
+namespace NYql {
+
+using namespace NNodes;
+using namespace NKikimr;
+using namespace NKikimr::NUdf;
+using namespace std::string_view_literals;
+
+class TExprDataToYtNodeConverter {
+public:
+ using TConverter = std::function<NYT::TNode(const TExprNode&)>;
+
+ TExprDataToYtNodeConverter() {
+ Converters.emplace(TCoUint8::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((ui64)NYql::FromString<ui8>(*node.Child(0), EDataSlot::Uint8));
+ });
+ Converters.emplace(TCoInt8::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((i64)NYql::FromString<i8>(*node.Child(0), EDataSlot::Int8));
+ });
+ Converters.emplace(TCoUint16::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((ui64)NYql::FromString<ui16>(*node.Child(0), EDataSlot::Uint16));
+ });
+ Converters.emplace(TCoInt16::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((i64)NYql::FromString<i16>(*node.Child(0), EDataSlot::Int16));
+ });
+ Converters.emplace(TCoInt32::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((i64)NYql::FromString<i32>(*node.Child(0), EDataSlot::Int32));
+ });
+ Converters.emplace(TCoUint32::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((ui64)NYql::FromString<ui32>(*node.Child(0), EDataSlot::Uint32));
+ });
+ Converters.emplace(TCoInt64::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<i64>(*node.Child(0), EDataSlot::Int64));
+ });
+ Converters.emplace(TCoUint64::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<ui64>(*node.Child(0), EDataSlot::Uint64));
+ });
+ Converters.emplace(TCoString::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(node.Child(0)->Content());
+ });
+ Converters.emplace(TCoUtf8::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(node.Child(0)->Content());
+ });
+ Converters.emplace(TCoJson::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(node.Child(0)->Content());
+ });
+ Converters.emplace(TCoYson::CallableName(), [](const TExprNode& node) {
+ return NYT::NodeFromYsonString(TString{node.Child(0)->Content()});
+ });
+ Converters.emplace(TCoDecimal::CallableName(), [](const TExprNode& node) {
+ char data[sizeof(NDecimal::TInt128)];
+ const ui32 size = NDecimal::Serialize(
+ NDecimal::FromString(node.Child(0)->Content(),
+ ::FromString<ui8>(node.Child(1)->Content()),
+ ::FromString<ui8>(node.Child(2)->Content())),
+ data);
+ return NYT::TNode(TStringBuf(data, size));
+ });
+ Converters.emplace(TCoBool::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<bool>(*node.Child(0), EDataSlot::Bool));
+ });
+ Converters.emplace(TCoFloat::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((double)NYql::FromString<float>(*node.Child(0), EDataSlot::Float));
+ });
+ Converters.emplace(TCoDouble::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<double>(*node.Child(0), EDataSlot::Double));
+ });
+ Converters.emplace(TCoNull::CallableName(), [](const TExprNode& /*node*/) {
+ return NYT::TNode::CreateEntity();
+ });
+ Converters.emplace(TCoNothing::CallableName(), [](const TExprNode& /*node*/) {
+ return NYT::TNode::CreateEntity();
+ });
+ Converters.emplace(TCoDate::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((ui64)NYql::FromString<ui16>(*node.Child(0), EDataSlot::Date));
+ });
+ Converters.emplace(TCoDatetime::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode((ui64)NYql::FromString<ui32>(*node.Child(0), EDataSlot::Datetime));
+ });
+ Converters.emplace(TCoTimestamp::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<ui64>(*node.Child(0), EDataSlot::Timestamp));
+ });
+ Converters.emplace(TCoInterval::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(NYql::FromString<i64>(*node.Child(0), EDataSlot::Interval));
+ });
+ Converters.emplace(TCoTzDate::CallableName(), [](const TExprNode& node) {
+ TStringBuf tzName = node.Child(0)->Content();
+ TStringBuf valueStr;
+ GetNext(tzName, ',', valueStr);
+ TStringStream out;
+ NMiniKQL::SerializeTzDate(::FromString<ui16>(valueStr), NMiniKQL::GetTimezoneId(tzName), out);
+ return NYT::TNode(out.Str());
+ });
+ Converters.emplace(TCoTzDatetime::CallableName(), [](const TExprNode& node) {
+ TStringBuf tzName = node.Child(0)->Content();
+ TStringBuf valueStr;
+ GetNext(tzName, ',', valueStr);
+ TStringStream out;
+ NMiniKQL::SerializeTzDatetime(::FromString<ui32>(valueStr), NMiniKQL::GetTimezoneId(tzName), out);
+ return NYT::TNode(out.Str());
+ });
+ Converters.emplace(TCoTzTimestamp::CallableName(), [](const TExprNode& node) {
+ TStringBuf tzName = node.Child(0)->Content();
+ TStringBuf valueStr;
+ GetNext(tzName, ',', valueStr);
+ TStringStream out;
+ NMiniKQL::SerializeTzTimestamp(::FromString<ui64>(valueStr), NMiniKQL::GetTimezoneId(tzName), out);
+ return NYT::TNode(out.Str());
+ });
+ Converters.emplace(TCoUuid::CallableName(), [](const TExprNode& node) {
+ return NYT::TNode(node.Child(0)->Content());
+ });
+ }
+
+ NYT::TNode Convert(const TExprNode& node) const {
+ if (auto p = Converters.FindPtr(node.Content())) {
+ return (*p)(node);
+ }
+ return NYT::TNode();
+ }
+private:
+ THashMap<TStringBuf, TConverter> Converters;
+};
+
+// Converts ExprNode representation to YT table format
+NYT::TNode ExprNodeToYtNode(const TExprNode& node) {
+ return Default<TExprDataToYtNodeConverter>().Convert(node.IsCallable("Just") ? node.Head() : node);
+}
+
+TExprNode::TPtr YtNodeToExprNode(const NYT::TNode& node, TExprContext& ctx, TPositionHandle pos) {
+ switch (node.GetType()) {
+ case NYT::TNode::String:
+ return Build<TCoString>(ctx, pos)
+ .Literal()
+ .Value(node.AsString())
+ .Build()
+ .Done().Ptr();
+ case NYT::TNode::Int64:
+ return Build<TCoInt64>(ctx, pos)
+ .Literal()
+ .Value(ToString(node.AsInt64()))
+ .Build()
+ .Done().Ptr();
+ case NYT::TNode::Uint64:
+ return Build<TCoUint64>(ctx, pos)
+ .Literal()
+ .Value(ToString(node.AsUint64()))
+ .Build()
+ .Done().Ptr();
+ case NYT::TNode::Double:
+ return Build<TCoDouble>(ctx, pos)
+ .Literal()
+ .Value(ToString(node.AsDouble()))
+ .Build()
+ .Done().Ptr();
+ case NYT::TNode::Bool:
+ return Build<TCoBool>(ctx, pos)
+ .Literal()
+ .Value(ToString(node.AsBool()))
+ .Build()
+ .Done().Ptr();
+ case NYT::TNode::Null:
+ return Build<TCoNull>(ctx, pos)
+ .Done().Ptr();
+ case NYT::TNode::Undefined:
+ ythrow yexception() << "Cannot convert UNDEFINED TNode to expr node";
+ default:
+ return ctx.Builder(pos).Callable("Yson").Atom(0, NYT::NodeToYsonString(node)).Seal().Build();
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TYtTableStatInfo::Validate(const TExprNode& node, TExprContext& ctx) {
+ if (!EnsureCallable(node, ctx)) {
+ return false;
+ }
+ if (!node.IsCallable(TYtStat::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYtStat::CallableName()
+ << " callable, but got " << node.Content()));
+ return false;
+ }
+
+ for (auto& child: node.Children()) {
+ if (!EnsureTupleSize(*child, 2, ctx)) {
+ return false;
+ }
+ const TExprNode* name = child->Child(0);
+ const TExprNode* value = child->Child(1);
+ if (!EnsureAtom(*name, ctx)) {
+ return false;
+ }
+
+#define VALIDATE_FIELD(field) \
+ if (name->Content() == TStringBuf(#field)) { \
+ if (!EnsureAtom(*value, ctx)) { \
+ return false; \
+ } \
+ decltype(TYtTableStatInfo::field) _##field; \
+ if (!TryFromString(value->Content(), _##field)) { \
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), \
+ TStringBuilder() << "Bad value of '" #field "' attribute: " \
+ << value->Content())); \
+ return false; \
+ } \
+ }
+
+ if (name->Content() == "Id") {
+ if (!EnsureAtom(*value, ctx)) {
+ return false;
+ }
+ if (value->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()),
+ TStringBuilder() << "Empty value of 'Id' attribute"));
+ return false;
+ }
+ }
+ else
+ VALIDATE_FIELD(RecordsCount)
+ else
+ VALIDATE_FIELD(DataSize)
+ else
+ VALIDATE_FIELD(ChunkCount)
+ else
+ VALIDATE_FIELD(ModifyTime)
+ else
+ VALIDATE_FIELD(Revision)
+ else {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Unsupported table stat option: " << name->Content()));
+ return false;
+ }
+#undef VALIDATE_FIELD
+
+ }
+ return true;
+}
+
+void TYtTableStatInfo::Parse(TExprBase node) {
+ *this = {};
+ FromNode = node.Maybe<TExprBase>();
+ for (auto child: node.Cast<TYtStat>()) {
+ auto setting = child.Cast<TCoNameValueTuple>();
+#define HANDLE_FIELD(field) \
+ if (setting.Name().Value() == TStringBuf(#field)) { \
+ field = FromString(setting.Value().Cast<TCoAtom>().Value()); \
+ }
+
+ if (setting.Name().Value() == "Id") {
+ Id = TString{setting.Value().Cast<TCoAtom>().Value()};
+ }
+ else
+ HANDLE_FIELD(RecordsCount)
+ else
+ HANDLE_FIELD(DataSize)
+ else
+ HANDLE_FIELD(ChunkCount)
+ else
+ HANDLE_FIELD(ModifyTime)
+ else
+ HANDLE_FIELD(Revision)
+ else {
+ YQL_ENSURE(false, "Unexpected option " << setting.Name().Value());
+ }
+#undef HANDLE_FIELD
+ }
+}
+
+TExprBase TYtTableStatInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto statBuilder = Build<TYtStat>(ctx, pos);
+
+#define ADD_FIELD(field) \
+ .Add() \
+ .Name() \
+ .Value(TStringBuf(#field), TNodeFlags::Default) \
+ .Build() \
+ .Value<TCoAtom>() \
+ .Value(ToString(field)) \
+ .Build() \
+ .Build()
+
+ statBuilder
+ ADD_FIELD(Id)
+ ADD_FIELD(RecordsCount)
+ ADD_FIELD(DataSize)
+ ADD_FIELD(ChunkCount)
+ ADD_FIELD(ModifyTime)
+ ADD_FIELD(Revision)
+ ;
+
+#undef ADD_FIELD
+
+ return statBuilder.Done();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TYtTableMetaInfo::Validate(const TExprNode& node, TExprContext& ctx) {
+ if (!EnsureCallable(node, ctx)) {
+ return false;
+ }
+ if (!node.IsCallable(TYtMeta::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYtMeta::CallableName()
+ << " callable, but got " << node.Content()));
+ return false;
+ }
+
+ for (auto& child: node.Children()) {
+ if (!EnsureTupleSize(*child, 2, ctx)) {
+ return false;
+ }
+ const TExprNode* name = child->Child(0);
+ TExprNode* value = child->Child(1);
+ if (!EnsureAtom(*name, ctx)) {
+ return false;
+ }
+
+#define VALIDATE_FIELD(field) \
+ if (name->Content() == TStringBuf(#field)) { \
+ if (!EnsureAtom(*value, ctx)) { \
+ return false; \
+ } \
+ decltype(TYtTableMetaInfo::field) _##field; \
+ if (!TryFromString(value->Content(), _##field)) { \
+ ctx.AddError(TIssue(ctx.GetPosition(value->Pos()), \
+ TStringBuilder() << "Bad value of '" #field "' attribute: " \
+ << value->Content())); \
+ return false; \
+ } \
+ }
+
+ if (name->Content() == TStringBuf("Attrs")) {
+ if (!EnsureTuple(*value, ctx)) {
+ return false;
+ }
+ for (auto& item: value->Children()) {
+ if (!EnsureTupleSize(*item, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*item->Child(0), ctx) || !EnsureAtom(*item->Child(1), ctx)) {
+ return false;
+ }
+ }
+ }
+ else
+ VALIDATE_FIELD(CanWrite)
+ else
+ VALIDATE_FIELD(DoesExist)
+ else
+ VALIDATE_FIELD(YqlCompatibleScheme)
+ else
+ VALIDATE_FIELD(InferredScheme)
+ else
+ VALIDATE_FIELD(IsDynamic)
+ else
+ VALIDATE_FIELD(SqlView)
+ else
+ VALIDATE_FIELD(SqlViewSyntaxVersion)
+ else {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Unsupported table meta option: " << name->Content()));
+ return false;
+ }
+#undef VALIDATE_FIELD
+
+ }
+ return true;
+}
+
+void TYtTableMetaInfo::Parse(TExprBase node) {
+ *this = {};
+ FromNode = node.Maybe<TExprBase>();
+ for (auto child: node.Cast<TYtMeta>()) {
+ auto setting = child.Cast<TCoNameValueTuple>();
+#define HANDLE_FIELD(field) \
+ if (setting.Name().Value() == TStringBuf(#field)) { \
+ field = FromString(setting.Value().Cast<TCoAtom>().Value()); \
+ }
+
+ if (setting.Name().Value() == TStringBuf("Attrs")) {
+ for (auto item: setting.Value().Cast<TCoNameValueTupleList>()) {
+ Attrs.insert({TString{item.Name().Value()}, TString{item.Value().Cast<TCoAtom>().Value()}});
+ }
+ }
+ else if (setting.Name().Value() == TStringBuf("SqlView")) {
+ SqlView = TString{setting.Value().Cast<TCoAtom>().Value()};
+ }
+ else
+ HANDLE_FIELD(SqlViewSyntaxVersion)
+ else
+ HANDLE_FIELD(CanWrite)
+ else
+ HANDLE_FIELD(DoesExist)
+ else
+ HANDLE_FIELD(YqlCompatibleScheme)
+ else
+ HANDLE_FIELD(InferredScheme)
+ else
+ HANDLE_FIELD(IsDynamic)
+ else {
+ YQL_ENSURE(false, "Unexpected option " << setting.Name().Value());
+ }
+#undef HANDLE_FIELD
+
+ }
+}
+
+TExprBase TYtTableMetaInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto metaBuilder = Build<TYtMeta>(ctx, pos);
+
+#define ADD_BOOL_FIELD(field) \
+ .Add() \
+ .Name() \
+ .Value(TStringBuf(#field), TNodeFlags::Default) \
+ .Build() \
+ .Value<TCoAtom>() \
+ .Value(field ? TStringBuf("1") : TStringBuf("0"), TNodeFlags::Default) \
+ .Build() \
+ .Build()
+
+ metaBuilder
+ ADD_BOOL_FIELD(CanWrite)
+ ADD_BOOL_FIELD(DoesExist)
+ ADD_BOOL_FIELD(YqlCompatibleScheme)
+ ADD_BOOL_FIELD(InferredScheme)
+ ADD_BOOL_FIELD(IsDynamic)
+ ;
+
+#undef ADD_BOOL_FIELD
+
+ if (SqlView) {
+ metaBuilder
+ .Add()
+ .Name()
+ .Value(TStringBuf("SqlView"), TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(SqlView)
+ .Build()
+ .Build();
+
+ metaBuilder
+ .Add()
+ .Name()
+ .Value(TStringBuf("SqlViewSyntaxVersion"), TNodeFlags::Default)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(ToString(SqlViewSyntaxVersion), TNodeFlags::Default)
+ .Build()
+ .Build();
+ }
+
+ if (!Attrs.empty()) {
+ auto attrsBuilder = Build<TCoNameValueTupleList>(ctx, pos);
+ for (auto& attr: Attrs) {
+ attrsBuilder.Add()
+ .Name()
+ .Value(attr.first)
+ .Build()
+ .Value<TCoAtom>()
+ .Value(attr.second)
+ .Build()
+ .Build();
+ }
+ metaBuilder
+ .Add()
+ .Name()
+ .Value(TStringBuf("Attrs"), TNodeFlags::Default)
+ .Build()
+ .Value(attrsBuilder.Done())
+ .Build();
+ }
+
+ return metaBuilder.Done();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TEpochInfo::Validate(const TExprNode& node, TExprContext& ctx) {
+ if (!node.IsCallable(TEpoch::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TEpoch::CallableName()));
+ return false;
+ }
+ if (!EnsureArgsCount(node, 1, ctx)) {
+ return false;
+ }
+
+ auto epochValue = node.Child(TEpoch::idx_Value);
+ if (!EnsureAtom(*epochValue, ctx)) {
+ return false;
+ }
+
+ ui32 epoch = 0;
+ if (!TryFromString(epochValue->Content(), epoch)) {
+ ctx.AddError(TIssue(ctx.GetPosition(epochValue->Pos()), TStringBuilder() << "Bad epoch value: " << epochValue->Content()));
+ return false;
+ }
+ return true;
+}
+
+TMaybe<ui32> TEpochInfo::Parse(const TExprNode& node) {
+ if (auto maybeEpoch = TMaybeNode<TEpoch>(&node)) {
+ return FromString<ui32>(maybeEpoch.Cast().Value().Value());
+ }
+ return Nothing();
+}
+
+TExprBase TEpochInfo::ToExprNode(const TMaybe<ui32>& epoch, TExprContext& ctx, const TPositionHandle& pos) {
+ return epoch
+ ? Build<TEpoch>(ctx, pos).Value().Value(ToString(*epoch), TNodeFlags::Default).Build().Done().Cast<TExprBase>()
+ : Build<TCoVoid>(ctx, pos).Done().Cast<TExprBase>();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TYtTableBaseInfo::TYtTableBaseInfo(const TYtKey& key, TStringBuf cluster)
+ : Name(key.GetPath())
+ , Cluster(cluster)
+{
+}
+
+NYT::TNode TYtTableBaseInfo::GetCodecSpecNode(const NCommon::TStructMemberMapper& mapper) const {
+ NYT::TNode res = NYT::TNode::CreateMap();
+ if (RowSpec) {
+ RowSpec->FillCodecNode(res[YqlRowSpecAttribute], mapper);
+ }
+ if (Meta) {
+ if (auto p = Meta->Attrs.FindPtr(FORMAT_ATTR_NAME)) {
+ res[FORMAT_ATTR_NAME] = NYT::NodeFromYsonString(*p);
+ }
+ if (Meta->IsDynamic) {
+ res[YqlDynamicAttribute] = true;
+ }
+ }
+ return res;
+}
+
+NYT::TNode TYtTableBaseInfo::GetAttrSpecNode(ui64 nativeTypeCompatibility, bool rowSpecCompactForm) const {
+ NYT::TNode res = NYT::TNode::CreateMap();
+ if (RowSpec) {
+ RowSpec->FillAttrNode(res[YqlRowSpecAttribute], nativeTypeCompatibility, rowSpecCompactForm);
+ }
+ return res;
+}
+
+TYtTableBaseInfo::TPtr TYtTableBaseInfo::Parse(TExprBase node) {
+ if (node.Maybe<TYtOutTable>()) {
+ return MakeIntrusive<TYtOutTableInfo>(node);
+ } else if (auto out = node.Maybe<TYtOutput>()) {
+ auto tableWithCluster = GetOutTableWithCluster(node);
+ auto res = MakeIntrusive<TYtOutTableInfo>(tableWithCluster.first);
+ res->Cluster = tableWithCluster.second;
+ res->IsUnordered = IsUnorderedOutput(out.Cast());
+ return res;
+ } else if (node.Maybe<TYtTable>()) {
+ return MakeIntrusive<TYtTableInfo>(node);
+ } else {
+ ythrow yexception() << "Not a table node " << (node.Raw() ? TString{node.Ref().Content()}.Quote() : TStringBuf("\"null\""));
+ }
+}
+
+TYtTableMetaInfo::TPtr TYtTableBaseInfo::GetMeta(TExprBase node) {
+ TMaybeNode<TExprBase> meta;
+ if (auto outTable = node.Maybe<TYtOutTable>()) {
+ meta = outTable.Cast().Meta();
+ } else if (node.Maybe<TYtOutput>()) {
+ meta = GetOutTable(node).Cast<TYtOutTable>().Meta();
+ } else if (auto table = node.Maybe<TYtTable>()) {
+ meta = table.Cast().Meta();
+ } else {
+ ythrow yexception() << "Not a table node " << (node.Raw() ? TString{node.Ref().Content()}.Quote() : TStringBuf("\"null\""));
+ }
+
+ if (meta.Maybe<TCoVoid>()) {
+ return {};
+ }
+ return MakeIntrusive<TYtTableMetaInfo>(meta.Cast());
+}
+
+TYqlRowSpecInfo::TPtr TYtTableBaseInfo::GetRowSpec(TExprBase node) {
+ TMaybeNode<TExprBase> rowSpec;
+ if (auto outTable = node.Maybe<TYtOutTable>()) {
+ rowSpec = outTable.Cast().RowSpec();
+ } else if (auto out = node.Maybe<TYtOutput>()) {
+ rowSpec = GetOutTable(node).Cast<TYtOutTable>().RowSpec();
+ } else if (auto table = node.Maybe<TYtTable>()) {
+ rowSpec = table.Cast().RowSpec();
+ } else {
+ ythrow yexception() << "Not a table node " << (node.Raw() ? TString{node.Ref().Content()}.Quote() : TStringBuf("\"null\""));
+ }
+
+ if (rowSpec.Maybe<TCoVoid>()) {
+ return {};
+ }
+ return MakeIntrusive<TYqlRowSpecInfo>(rowSpec.Cast());
+}
+
+TStringBuf TYtTableBaseInfo::GetTableName(NNodes::TExprBase node) {
+ TMaybeNode<TYtTableBase> tableBase;
+ if (auto outTable = node.Maybe<TYtOutTable>()) {
+ tableBase = outTable.Cast();
+ } else if (node.Maybe<TYtOutput>()) {
+ tableBase = GetOutTable(node).Cast<TYtOutTable>();
+ } else if (auto table = node.Maybe<TYtTable>()) {
+ tableBase = table.Cast();
+ } else {
+ ythrow yexception() << "Not a table node " << (node.Raw() ? TString{node.Ref().Content()}.Quote() : TStringBuf("\"null\""));
+ }
+
+ return tableBase.Cast().Name().Value();
+}
+
+bool TYtTableBaseInfo::RequiresRemap() const {
+ return Meta->InferredScheme
+ // || !Meta->YqlCompatibleScheme -- Non compatuble schemas do not have RowSpec
+ || Meta->Attrs.contains(QB2Premapper)
+ || !RowSpec
+ || !RowSpec->StrictSchema
+ || !RowSpec->DefaultValues.empty();
+}
+
+bool TYtTableBaseInfo::HasSameScheme(const TTypeAnnotationNode& scheme) const {
+ if (!RowSpec) {
+ if (scheme.GetKind() != ETypeAnnotationKind::Struct) {
+ return false;
+ }
+ const TVector<const TItemExprType*>& items = scheme.Cast<TStructExprType>()->GetItems();
+ if (YAMR_FIELDS.size() != items.size()) {
+ return false;
+ }
+ for (size_t i: xrange(YAMR_FIELDS.size())) {
+ if (items[i]->GetName() != YAMR_FIELDS[i]
+ || items[i]->GetItemType()->GetKind() != ETypeAnnotationKind::Data
+ || items[i]->GetItemType()->Cast<TDataExprType>()->GetSlot() != EDataSlot::String)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ } else {
+ return IsSameAnnotation(*RowSpec->GetType(), scheme);
+ }
+}
+
+bool TYtTableBaseInfo::HasSamePhysicalScheme(const TYtTableBaseInfo& info) const {
+ if (bool(RowSpec) != bool(info.RowSpec)) {
+ return false;
+ }
+ if (RowSpec) {
+ if (!IsSameAnnotation(*RowSpec->GetType(), *info.RowSpec->GetType())) {
+ return false;
+ }
+ return RowSpec->GetAuxColumns() == info.RowSpec->GetAuxColumns();
+ }
+ else {
+ return true;
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TYtTableInfo::TYtTableInfo(const TYtKey& key, TStringBuf cluster)
+ : TYtTableBaseInfo(key, cluster)
+{
+}
+
+bool TYtTableInfo::Validate(const TExprNode& node, EYtSettingTypes accepted, TExprContext& ctx) {
+ if (!node.IsCallable(TYtTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYtTable::CallableName()));
+ return false;
+ }
+
+ if (!EnsureArgsCount(node, 8, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*node.Child(TYtTable::idx_Name), ctx)) {
+ return false;
+ }
+
+ if (node.Child(TYtTable::idx_Name)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(TYtTable::idx_Name)->Pos()), TStringBuilder() << "Expected non-empty table name"));
+ return false;
+ }
+
+#define VALIDATE_OPT_FIELD(idx, TFunc) \
+ if (!node.Child(idx)->IsCallable(TFunc::CallableName()) \
+ && !node.Child(idx)->IsCallable(TStringBuf("Void"))) { \
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(idx)->Pos()), TStringBuilder() \
+ << "Expected " << TFunc::CallableName() \
+ << " or Void")); \
+ return false; \
+ }
+
+ VALIDATE_OPT_FIELD(TYtTable::idx_RowSpec, TYqlRowSpec)
+ VALIDATE_OPT_FIELD(TYtTable::idx_Meta, TYtMeta)
+ VALIDATE_OPT_FIELD(TYtTable::idx_Stat, TYtStat)
+ VALIDATE_OPT_FIELD(TYtTable::idx_Epoch, TEpoch)
+ VALIDATE_OPT_FIELD(TYtTable::idx_CommitEpoch, TEpoch)
+
+#undef VALIDATE_OPT_FIELD
+
+ if (!EnsureTuple(*node.Child(TYtTable::idx_Settings), ctx)) {
+ return false;
+ }
+
+ if (!ValidateSettings(*node.Child(TYtTable::idx_Settings), accepted, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*node.Child(TYtTable::idx_Cluster), ctx)) {
+ return false;
+ }
+
+ if (node.Child(TYtTable::idx_Cluster)->Content().empty()) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(TYtTable::idx_Cluster)->Pos()), TStringBuilder() << "Expected non-empty cluster name"));
+ return false;
+ }
+
+ return true;
+}
+
+void TYtTableInfo::Parse(TExprBase node, bool useTypes) {
+ *this = {};
+ FromNode = node.Maybe<TExprBase>();
+ TYtTable table = node.Cast<TYtTable>();
+ Name = table.Name().Value();
+ if (table.RowSpec().Maybe<TYqlRowSpec>()) {
+ RowSpec = MakeIntrusive<TYqlRowSpecInfo>(table.RowSpec(), useTypes);
+ }
+ if (table.Meta().Maybe<TYtMeta>()) {
+ Meta = MakeIntrusive<TYtTableMetaInfo>(table.Meta());
+ }
+ if (table.Stat().Maybe<TYtStat>()) {
+ Stat = MakeIntrusive<TYtTableStatInfo>(table.Stat());
+ }
+ Epoch = TEpochInfo::Parse(table.Epoch().Ref());
+ CommitEpoch = TEpochInfo::Parse(table.CommitEpoch().Ref());
+ Settings = table.Settings();
+ Cluster = table.Cluster().Value();
+ IsTemp = IsAnonymous = NYql::HasSetting(table.Settings().Ref(), EYtSettingType::Anonymous);
+}
+
+TExprBase TYtTableInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto tableBuilder = Build<TYtTable>(ctx, pos);
+ YQL_ENSURE(!Name.empty());
+ tableBuilder.Name().Value(Name).Build();
+ if (RowSpec) {
+ tableBuilder.RowSpec(RowSpec->ToExprNode(ctx, pos));
+ } else {
+ tableBuilder.RowSpec<TCoVoid>().Build();
+ }
+ if (Meta) {
+ tableBuilder.Meta(Meta->ToExprNode(ctx, pos));
+ } else {
+ tableBuilder.Meta<TCoVoid>().Build();
+ }
+ if (Stat) {
+ tableBuilder.Stat(Stat->ToExprNode(ctx, pos));
+ } else {
+ tableBuilder.Stat<TCoVoid>().Build();
+ }
+ tableBuilder.Epoch(TEpochInfo::ToExprNode(Epoch, ctx, pos));
+ tableBuilder.CommitEpoch(TEpochInfo::ToExprNode(CommitEpoch, ctx, pos));
+ if (Settings) {
+ tableBuilder.Settings(Settings.Cast<TCoNameValueTupleList>());
+ } else {
+ tableBuilder.Settings().Build();
+ }
+ tableBuilder.Cluster().Value(Cluster).Build();
+
+ return tableBuilder.Done();
+}
+
+TStringBuf TYtTableInfo::GetTableLabel(NNodes::TExprBase node) {
+ if (auto ann = NYql::GetSetting(node.Cast<TYtTable>().Settings().Ref(), EYtSettingType::Anonymous)) {
+ if (ann->ChildrenSize() == 2) {
+ return ann->Child(1)->Content();
+ }
+ }
+ return node.Cast<TYtTable>().Name().Value();
+}
+
+bool TYtTableInfo::HasSubstAnonymousLabel(NNodes::TExprBase node) {
+ if (auto ann = NYql::GetSetting(node.Cast<TYtTable>().Settings().Ref(), EYtSettingType::Anonymous)) {
+ return ann->ChildrenSize() == 2;
+ }
+ return false;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+TYtOutTableInfo::TYtOutTableInfo(const TStructExprType* type, ui64 nativeYtTypeFlags) {
+ RowSpec = MakeIntrusive<TYqlRowSpecInfo>();
+ RowSpec->SetType(type, nativeYtTypeFlags);
+
+ Meta = MakeIntrusive<TYtTableMetaInfo>();
+ Meta->CanWrite = true;
+ Meta->DoesExist = true;
+ Meta->YqlCompatibleScheme = true;
+
+ IsTemp = true;
+}
+
+bool TYtOutTableInfo::Validate(const TExprNode& node, TExprContext& ctx) {
+ if (!node.IsCallable(TYtOutTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYtOutTable::CallableName()));
+ return false;
+ }
+ if (!EnsureArgsCount(node, 5, ctx)) {
+ return false;
+ }
+
+ if (!EnsureAtom(*node.Child(TYtOutTable::idx_Name), ctx)) {
+ return false;
+ }
+
+#define VALIDATE_OPT_FIELD(idx, TFunc) \
+ if (!node.Child(idx)->IsCallable(TFunc::CallableName()) \
+ && !node.Child(idx)->IsCallable(TStringBuf("Void"))) { \
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(idx)->Pos()), TStringBuilder() \
+ << "Expected " << TFunc::CallableName() \
+ << " or Void")); \
+ return false; \
+ }
+#define VALIDATE_REQ_FIELD(idx, TFunc) \
+ if (!node.Child(idx)->IsCallable(TFunc::CallableName())) { \
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(idx)->Pos()), TStringBuilder() \
+ << "Expected " << TFunc::CallableName())); \
+ return false; \
+ }
+
+ VALIDATE_REQ_FIELD(TYtOutTable::idx_RowSpec, TYqlRowSpec)
+ VALIDATE_REQ_FIELD(TYtOutTable::idx_Meta, TYtMeta)
+ VALIDATE_OPT_FIELD(TYtOutTable::idx_Stat, TYtStat)
+
+#undef VALIDATE_OPT_FIELD
+#undef VALIDATE_REQ_FIELD
+
+ if (!EnsureTuple(*node.Child(TYtOutTable::idx_Settings), ctx)) {
+ return false;
+ }
+
+ if (!ValidateSettings(*node.Child(TYtOutTable::idx_Settings), EYtSettingType::UniqueBy | EYtSettingType::OpHash, ctx)) {
+ return false;
+ }
+
+ return true;
+}
+
+void TYtOutTableInfo::Parse(TExprBase node) {
+ *this = {};
+ FromNode = node.Maybe<TExprBase>();
+ TYtOutTable table = node.Cast<TYtOutTable>();
+ Name = table.Name().Value();
+ RowSpec = MakeIntrusive<TYqlRowSpecInfo>(table.RowSpec());
+ Meta = MakeIntrusive<TYtTableMetaInfo>(table.Meta());
+ if (table.Stat().Maybe<TYtStat>()) {
+ Stat = MakeIntrusive<TYtTableStatInfo>(table.Stat());
+ }
+ Settings = table.Settings();
+}
+
+TExprBase TYtOutTableInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto tableBuilder = Build<TYtOutTable>(ctx, pos);
+ tableBuilder.Name().Value(Name).Build();
+ YQL_ENSURE(RowSpec);
+ tableBuilder.RowSpec(RowSpec->ToExprNode(ctx, pos));
+ YQL_ENSURE(Meta);
+ tableBuilder.Meta(Meta->ToExprNode(ctx, pos));
+ if (Stat) {
+ tableBuilder.Stat(Stat->ToExprNode(ctx, pos));
+ } else {
+ tableBuilder.Stat<TCoVoid>().Build();
+ }
+ if (Settings) {
+ tableBuilder.Settings(Settings.Cast<TCoNameValueTupleList>());
+ } else {
+ tableBuilder.Settings().Build();
+ }
+ return tableBuilder.Done();
+}
+
+TYtOutTableInfo& TYtOutTableInfo::SetUnique(const TDistinctConstraintNode* distinct, const TPositionHandle& pos, TExprContext& ctx) {
+ if (distinct) {
+ RowSpec->UniqueKeys = !RowSpec->SortMembers.empty() && RowSpec->SortMembers.size() == RowSpec->SortedBy.size()
+ && distinct->IsOrderBy(*RowSpec->MakeSortConstraint(ctx));
+ if (!Settings) {
+ Settings = Build<TCoNameValueTupleList>(ctx, pos).Done();
+ }
+ if (const auto columns = NYql::GetSettingAsColumnList(Settings.Ref(), EYtSettingType::UniqueBy)) {
+ YQL_ENSURE(distinct->ContainsCompleteSet(std::vector<std::string_view>(columns.cbegin(), columns.cend())));
+ } else if (const auto simple = distinct->OnlySimpleColumns(ctx)) {
+ const auto& sets = static_cast<const TDistinctConstraintNode*>(simple)->GetAllSets();
+ auto jt = sets.cbegin();
+ if (1U < sets.size()) {
+ TConstraintNode::TSetType sorted;
+ sorted.reserve(RowSpec->SortMembers.size());
+ std::for_each(RowSpec->SortMembers.cbegin(), RowSpec->SortMembers.cend(), [&sorted](const std::string_view& member) { sorted.insert_unique(TConstraintNode::TPathType(1U, member)); });
+ for (auto it = sets.cbegin(); sets.cend() != it; ++it) {
+ if (std::includes(sorted.cbegin(), sorted.cend(), it->cbegin(), it->cend())) {
+ jt = it;
+ break;
+ }
+ }
+ }
+ std::vector<std::string_view> uniques(jt->size());
+ std::transform(jt->cbegin(), jt->cend(), uniques.begin(), [&](const TConstraintNode::TPathType& path) { return path.front(); });
+ Settings = TExprBase(NYql::AddSetting(Settings.Ref(), EYtSettingType::UniqueBy, ToAtomList(uniques, pos, ctx), ctx));
+ }
+ }
+ return *this;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TYtRangesInfo::Validate(const TExprNode& node, TExprContext& ctx, const TYqlRowSpecInfo::TPtr& rowSpec) {
+ auto validatKey = [&rowSpec, &ctx] (TExprNode& key) {
+ if (!EnsureTupleMaxSize(key, (ui32)rowSpec->SortMembers.size(), ctx)) {
+ return false;
+ }
+ for (size_t i: xrange(key.ChildrenSize())) {
+ auto keyChild = key.Child(i);
+ bool isOptional = false;
+ const TDataExprType* columnType = nullptr;
+ if (!EnsureDataOrOptionalOfData(keyChild->Pos(), rowSpec->SortedByTypes[i], isOptional, columnType, ctx)) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyChild->Pos()), TStringBuilder()
+ << "Table column " << rowSpec->SortedBy[i] << " is not data or optional of data"));
+ return false;
+ }
+ if (IsNull(*keyChild)) {
+ if (!isOptional) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyChild->Pos()), TStringBuilder()
+ << "Column " << rowSpec->SortMembers[i] << " type "
+ << *rowSpec->SortedByTypes[i] << " cannot be compared with Null"));
+ return false;
+ }
+ } else if (keyChild->GetTypeAnn()->GetKind() == ETypeAnnotationKind::Optional) {
+ if (!IsSameAnnotation(*keyChild->GetTypeAnn(), *rowSpec->SortedByTypes[i])) {
+ ctx.AddError(TIssue(ctx.GetPosition(keyChild->Pos()), TStringBuilder()
+ << "Column " << rowSpec->SortMembers[i] << " type "
+ << *rowSpec->SortedByTypes[i] << " should be equal to compare type: "
+ << *keyChild->GetTypeAnn()));
+ return false;
+ }
+ } else {
+ auto keyType = keyChild->GetTypeAnn()->Cast<TDataExprType>();
+ if (keyType->GetSlot() != columnType->GetSlot()
+ && !GetSuperType(keyType->GetSlot(), columnType->GetSlot()))
+ {
+ ctx.AddError(TIssue(ctx.GetPosition(keyChild->Pos()), TStringBuilder()
+ << "Column " << rowSpec->SortMembers[i] << " type "
+ << *rowSpec->SortedByTypes[i] << " cannot be compared with "
+ << *keyChild->GetTypeAnn()));
+ return false;
+ }
+ }
+ }
+ return true;
+ };
+
+ for (auto& child: node.Children()) {
+ if (!TYtRangeItemBase::Match(child.Get())) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << "Expected one of YtRangeItemBase, but got " << child->Content()));
+
+ }
+ if (rowSpec) {
+ if (child->IsCallable(TYtKeyExact::CallableName())) {
+ if (!rowSpec->IsSorted()) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << TYtKeyExact::CallableName() << " cannot be used with unsorted table"));
+ return false;
+ }
+
+ if (!validatKey(*child->Child(TYtKeyExact::idx_Key))) {
+ return false;
+ }
+ }
+ else if (child->IsCallable(TYtKeyRange::CallableName())) {
+ if (!rowSpec->IsSorted()) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << TYtKeyRange::CallableName() << " cannot be used with unsorted table"));
+ return false;
+ }
+ if (!validatKey(*child->Child(TYtKeyRange::idx_Lower))) {
+ return false;
+ }
+ if (!validatKey(*child->Child(TYtKeyRange::idx_Upper))) {
+ return false;
+ }
+ }
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder()
+ << child->Content() << " cannot be used with YAMR table"));
+ return false;
+ }
+ }
+ return true;
+}
+
+void TYtRangesInfo::Parse(TExprBase node) {
+ *this = {};
+
+ for (auto item: node.Cast<TExprList>()) {
+ if (item.Maybe<TYtRow>()) {
+ Ranges.emplace_back(TRowSingle{FromString<ui64>(item.Cast<TYtRow>().Index().Literal().Value())});
+ } else if (item.Maybe<TYtRowRange>()) {
+ TYtRowRange rangeNode = item.Cast<TYtRowRange>();
+ TRowRange range;
+ if (!rangeNode.Lower().Maybe<TCoVoid>()) {
+ range.Lower = FromString<ui64>(rangeNode.Lower().Cast<TCoUint64>().Literal().Value());
+ }
+ if (!rangeNode.Upper().Maybe<TCoVoid>()) {
+ range.Upper = FromString<ui64>(rangeNode.Upper().Cast<TCoUint64>().Literal().Value());
+ }
+ Ranges.push_back(std::move(range));
+ } else if (item.Maybe<TYtKeyExact>()) {
+ TYtKeyExact keyNode = item.Cast<TYtKeyExact>();
+ Ranges.emplace_back(TKeySingle{TVector<NNodes::TExprBase>{keyNode.Key().begin(), keyNode.Key().end()}});
+ } else if (item.Maybe<TYtKeyRange>()) {
+ TYtKeyRange rangeNode = item.Cast<TYtKeyRange>();
+ TKeyRange range;
+ range.Lower.assign(rangeNode.Lower().begin(), rangeNode.Lower().end());
+ range.Upper.assign(rangeNode.Upper().begin(), rangeNode.Upper().end());
+ // if useKeyBound setting is missed, useKeyBoundApi should be set to false,
+ // irrespective of DEFAULT_USE_KEY_BOUND_API value
+ range.UseKeyBoundApi = false;
+ if (rangeNode.Flags()) {
+ for (auto atom: rangeNode.Flags().Cast()) {
+ if (atom.Value() == TStringBuf("excludeLower")) {
+ range.LowerInclude = false;
+ } else if (atom.Value() == TStringBuf("includeUpper")) {
+ range.UpperInclude = true;
+ } else if (atom.Value() == TStringBuf("useKeyBound")) {
+ range.UseKeyBoundApi = true;
+ }
+ }
+ }
+ Ranges.push_back(std::move(range));
+ }
+ }
+}
+
+void TYtRangesInfo::Parse(const TVector<NYT::TReadRange>& ranges, TExprContext& ctx, const TPositionHandle& pos) {
+ *this = {};
+
+ for (const NYT::TReadRange& item: ranges) {
+ YQL_ENSURE(!NYT::IsTrivial(item.Exact_) || !NYT::IsTrivial(item.LowerLimit_) || !NYT::IsTrivial(item.UpperLimit_), "Full scan range is not supported");
+
+ if (!NYT::IsTrivial(item.Exact_)) {
+ YQL_ENSURE(NYT::IsTrivial(item.LowerLimit_) && NYT::IsTrivial(item.UpperLimit_));
+ if (item.Exact_.RowIndex_) {
+ YQL_ENSURE(!item.Exact_.Key_ && !item.Exact_.Offset_);
+ Ranges.emplace_back(TRowSingle{(ui64)*item.Exact_.RowIndex_});
+ } else if (item.Exact_.Key_) {
+ YQL_ENSURE(!item.Exact_.Offset_);
+ TVector<TExprBase> key;
+ for (auto& keyPart: item.Exact_.Key_->Parts_) {
+ key.emplace_back(YtNodeToExprNode(keyPart, ctx, pos));
+ }
+ Ranges.emplace_back(TKeySingle{std::move(key)});
+ }
+ } else {
+ YQL_ENSURE(!NYT::IsTrivial(item.LowerLimit_) || !NYT::IsTrivial(item.UpperLimit_));
+ if (item.LowerLimit_.RowIndex_ || item.UpperLimit_.RowIndex_) {
+ TRowRange range;
+ if (item.LowerLimit_.RowIndex_) {
+ range.Lower.ConstructInPlace(*item.LowerLimit_.RowIndex_);
+ }
+ if (item.UpperLimit_.RowIndex_) {
+ range.Upper.ConstructInPlace(*item.UpperLimit_.RowIndex_);
+ }
+ Ranges.emplace_back(std::move(range));
+ } else if (item.LowerLimit_.Key_ || item.UpperLimit_.Key_) {
+ TKeyRange range;
+ range.UseKeyBoundApi = false;
+ if (item.LowerLimit_.Key_ && !item.LowerLimit_.Key_->Parts_.empty()) {
+ size_t count = item.LowerLimit_.Key_->Parts_.size();
+ if (item.LowerLimit_.Key_->Parts_.back().IsEntity() && !item.LowerLimit_.Key_->Parts_.back().HasAttributes()) {
+ range.LowerInclude = false;
+ --count;
+ }
+ for (size_t i = 0; i < count; ++i) {
+ range.Lower.emplace_back(YtNodeToExprNode(item.LowerLimit_.Key_->Parts_[i], ctx, pos));
+ }
+ }
+ if (item.UpperLimit_.Key_ && !item.UpperLimit_.Key_->Parts_.empty()) {
+ size_t count = item.UpperLimit_.Key_->Parts_.size();
+ if (item.UpperLimit_.Key_->Parts_.back().IsEntity()
+ && item.UpperLimit_.Key_->Parts_.back().HasAttributes()
+ && item.UpperLimit_.Key_->Parts_.back().GetAttributes().AsMap().Value("type", "") == "max") {
+ range.UpperInclude = true;
+ --count;
+ }
+ for (size_t i = 0; i < count; ++i) {
+ range.Upper.emplace_back(YtNodeToExprNode(item.UpperLimit_.Key_->Parts_[i], ctx, pos));
+ }
+ }
+ Ranges.emplace_back(std::move(range));
+ } else {
+ YQL_ENSURE(item.LowerLimit_.KeyBound_ || item.UpperLimit_.KeyBound_);
+ TKeyRange range;
+ range.UseKeyBoundApi = true;
+ if (item.LowerLimit_.KeyBound_) {
+ auto relation = item.LowerLimit_.KeyBound_->Relation();
+ YQL_ENSURE(relation == NYT::ERelation::Greater || relation == NYT::ERelation::GreaterOrEqual);
+ range.LowerInclude = relation == NYT::ERelation::GreaterOrEqual;
+ YQL_ENSURE(!item.LowerLimit_.KeyBound_->Key().Parts_.empty());
+ for (auto& key : item.LowerLimit_.KeyBound_->Key().Parts_) {
+ range.Lower.emplace_back(YtNodeToExprNode(key, ctx, pos));
+ }
+ }
+
+ if (item.UpperLimit_.KeyBound_) {
+ auto relation = item.UpperLimit_.KeyBound_->Relation();
+ YQL_ENSURE(relation == NYT::ERelation::Less || relation == NYT::ERelation::LessOrEqual);
+ range.UpperInclude = relation == NYT::ERelation::LessOrEqual;
+ YQL_ENSURE(!item.UpperLimit_.KeyBound_->Key().Parts_.empty());
+ for (auto& key : item.UpperLimit_.KeyBound_->Key().Parts_) {
+ range.Upper.emplace_back(YtNodeToExprNode(key, ctx, pos));
+ }
+ }
+ Ranges.emplace_back(std::move(range));
+ }
+ }
+ }
+}
+
+void TYtRangesInfo::AddRowRange(const TRowRange& range) {
+ Ranges.push_back(range);
+}
+
+void TYtRangesInfo::SetUseKeyBoundApi(bool useKeyBoundApi) {
+ for (auto& range: Ranges) {
+ if (auto* data = std::get_if<TKeyRange>(&range)) {
+ data->UseKeyBoundApi = useKeyBoundApi;
+ }
+ }
+}
+
+TExprBase TYtRangesInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const {
+ auto rangesBuilder = Build<TExprList>(ctx, pos);
+ for (auto& range: Ranges) {
+ std::visit(TOverloaded{
+ [&](const TRowSingle& row) {
+ rangesBuilder.Add<TYtRow>()
+ .Index()
+ .Literal()
+ .Value(ToString(row.Offset), TNodeFlags::Default)
+ .Build()
+ .Build()
+ .Build();
+ },
+ [&](const TRowRange& data) {
+ auto builder = rangesBuilder.Add<TYtRowRange>();
+ if (data.Lower) {
+ builder.Lower<TCoUint64>()
+ .Literal()
+ .Value(ToString(*data.Lower), TNodeFlags::Default)
+ .Build()
+ .Build();
+ } else {
+ builder.Lower<TCoVoid>().Build();
+ }
+ if (data.Upper) {
+ builder.Upper<TCoUint64>()
+ .Literal()
+ .Value(ToString(*data.Upper), TNodeFlags::Default)
+ .Build()
+ .Build();
+ } else {
+ builder.Upper<TCoVoid>().Build();
+ }
+ builder.Build();
+ },
+ [&](const TKeySingle& key) {
+ rangesBuilder.Add<TYtKeyExact>()
+ .Key()
+ .Add(key.Key)
+ .Build()
+ .Build();
+ },
+ [&](const TKeyRange& data) {
+ auto builder = rangesBuilder.Add<TYtKeyRange>();
+ builder.Lower().Add(data.Lower).Build();
+ builder.Upper().Add(data.Upper).Build();
+ if (!data.LowerInclude || data.UpperInclude) {
+ auto flagsBuilder = builder.Flags();
+ if (!data.LowerInclude) {
+ flagsBuilder.Add().Value("excludeLower", TNodeFlags::Default).Build();
+ }
+ if (data.UpperInclude) {
+ flagsBuilder.Add().Value("includeUpper", TNodeFlags::Default).Build();
+ }
+ if (data.UseKeyBoundApi) {
+ flagsBuilder.Add().Value("useKeyBound", TNodeFlags::Default).Build();
+ }
+ flagsBuilder.Build();
+ }
+ builder.Build();
+ }
+ }, range);
+ }
+ return rangesBuilder.Done();
+}
+
+void TYtRangesInfo::FillRichYPath(NYT::TRichYPath& path, size_t keyColumnsCount) const {
+ path.MutableRanges().ConstructInPlace();
+ for (auto& range: Ranges) {
+ std::visit(TOverloaded{
+ [&](const TRowSingle& row) {
+ path.AddRange(NYT::TReadRange().Exact(NYT::TReadLimit().RowIndex(row.Offset)));
+ },
+ [&](const TRowRange& data) {
+ NYT::TReadRange ytRange;
+ if (data.Lower) {
+ ytRange.LowerLimit(NYT::TReadLimit().RowIndex(*data.Lower));
+ }
+ if (data.Upper) {
+ ytRange.UpperLimit(NYT::TReadLimit().RowIndex(*data.Upper));
+ }
+ path.AddRange(ytRange);
+ },
+ [&](const TKeySingle& data) {
+ NYT::TKey key;
+ for (auto& node: data.Key) {
+ auto keyPart = ExprNodeToYtNode(node.Ref());
+ YQL_ENSURE(!keyPart.IsUndefined(), "Unsupported range node: " << node.Ref().Content());
+ key.Add(std::move(keyPart));
+ }
+ path.AddRange(NYT::TReadRange().Exact(NYT::TReadLimit().Key(key)));
+ },
+ [&](const TKeyRange& data) {
+ YQL_ENSURE(keyColumnsCount > 0);
+ YQL_ENSURE(data.Lower.size() <= keyColumnsCount);
+ YQL_ENSURE(data.Upper.size() <= keyColumnsCount);
+ NYT::TReadRange ytRange;
+ if (!data.Lower.empty()) {
+ NYT::TKey key;
+ for (auto& node: data.Lower) {
+ auto keyPart = ExprNodeToYtNode(node.Ref());
+ YQL_ENSURE(!keyPart.IsUndefined(), "Unsupported range node: " << node.Ref().Content());
+ key.Add(std::move(keyPart));
+ }
+ if (data.UseKeyBoundApi) {
+ NYT::TKeyBound lower(data.LowerInclude ? NYT::ERelation::GreaterOrEqual : NYT::ERelation::Greater, key);
+ ytRange.LowerLimit(NYT::TReadLimit().KeyBound(lower));
+ } else {
+ if (!data.LowerInclude) {
+ size_t toAddMaxs = keyColumnsCount - data.Lower.size();
+ for (size_t i = 0; i < toAddMaxs; ++i) {
+ auto entity = NYT::TNode::CreateEntity();
+ entity.Attributes().AsMap()[TStringBuf("type")] = TStringBuf("max");
+ key.Add(entity);
+ }
+ if (toAddMaxs == 0) {
+ key.Add(NYT::TNode::CreateEntity());
+ }
+ }
+ ytRange.LowerLimit(NYT::TReadLimit().Key(key));
+ }
+ }
+ if (!data.Upper.empty()) {
+ NYT::TKey key;
+ for (auto& node: data.Upper) {
+ auto keyPart = ExprNodeToYtNode(node.Ref());
+ YQL_ENSURE(!keyPart.IsUndefined(), "Unsupported range node: " << node.Ref().Content());
+ key.Add(std::move(keyPart));
+ }
+ if (data.UseKeyBoundApi) {
+ NYT::TKeyBound upper(data.UpperInclude ? NYT::ERelation::LessOrEqual : NYT::ERelation::Less, key);
+ ytRange.UpperLimit(NYT::TReadLimit().KeyBound(upper));
+ } else {
+ if (data.UpperInclude) {
+ auto entity = NYT::TNode::CreateEntity();
+ entity.Attributes().AsMap()[TStringBuf("type")] = TStringBuf("max");
+ key.Add(entity);
+ }
+ ytRange.UpperLimit(NYT::TReadLimit().Key(key));
+ }
+ }
+ path.AddRange(ytRange);
+ }
+ }, range);
+ }
+}
+
+TMaybe<ui64> TYtRangesInfo::GetUsedRows(ui64 tableRowCount) const {
+ ui64 rows = 0;
+ if (tableRowCount) {
+ for (auto& range: Ranges) {
+ if (std::holds_alternative<TRowSingle>(range)) {
+ ++rows;
+ } else if (const auto* data = std::get_if<TRowRange>(&range)) {
+ ui64 range = tableRowCount;
+ if (data->Upper) {
+ range = *data->Upper;
+ }
+ if (data->Lower) {
+ range -= Min(range, *data->Lower);
+ }
+ rows += range;
+ } else {
+ return Nothing();
+ }
+ }
+ }
+ return rows;
+}
+
+size_t TYtRangesInfo::GetRangesCount() const {
+ return Ranges.size();
+}
+
+size_t TYtRangesInfo::GetUsedKeyPrefixLength() const {
+ size_t used = 0;
+ for (auto& range: Ranges) {
+ if (const auto* key = std::get_if<TKeySingle>(&range)) {
+ used = std::max(used, key->Key.size());
+ } else if (const auto* keyRange = std::get_if<TKeyRange>(&range)) {
+ used = std::max(used, keyRange->Lower.size());
+ used = std::max(used, keyRange->Upper.size());
+ }
+ }
+ return used;
+}
+
+TVector<TYtRangesInfo::TRange> TYtRangesInfo::GetRanges() const {
+ return Ranges;
+}
+
+bool TYtRangesInfo::IsEmpty() const {
+ return Ranges.empty() || AllOf(Ranges, [](const TRange& r) {
+ if (const TRowRange* data = ::std::get_if<TRowRange>(&r)) {
+ return (data->Lower && data->Upper && *data->Lower >= *data->Upper) ||
+ (data->Upper && *data->Upper == 0ul);
+ }
+ return false;
+ });
+}
+
+namespace {
+
+template <typename TLayerType>
+NUdf::TUnboxedValuePod GetTzValue(TStringBuf atom) {
+ TStringBuf valueStr;
+ GetNext(atom, ',', valueStr);
+ return NUdf::TUnboxedValuePod(::FromString<TLayerType>(valueStr));
+}
+
+NUdf::TUnboxedValuePod GetDecimalValue(TCoDecimal decimal) {
+ return NUdf::TUnboxedValuePod(NDecimal::FromString(decimal.Literal().Value(),
+ ::FromString<ui8>(decimal.Precision().Value()),
+ ::FromString<ui8>(decimal.Scale().Value())));
+}
+
+NKikimr::NUdf::EDataSlot GetCompareType(TStringBuf type) {
+ using namespace NKikimr::NUdf;
+ auto cmpSlot = GetDataSlot(type);
+ switch (cmpSlot) {
+ case EDataSlot::Int8:
+ case EDataSlot::Uint8:
+ case EDataSlot::Int16:
+ case EDataSlot::Uint16:
+ case EDataSlot::Int32:
+ case EDataSlot::Uint32:
+ case EDataSlot::Int64:
+ cmpSlot = EDataSlot::Int64;
+ break;
+ case EDataSlot::Uint64:
+ cmpSlot = EDataSlot::Uint64;
+ break;
+ default:
+ break;
+ }
+ return cmpSlot;
+}
+
+int CompareDataNodes(NNodes::TExprBase left, NNodes::TExprBase right)
+{
+ using namespace NNodes;
+ using namespace NKikimr::NUdf;
+
+ if (left.Maybe<TCoNull>()) {
+ return right.Maybe<TCoNull>().IsValid() ? 0 : -1;
+ } else if (right.Maybe<TCoNull>()) {
+ return 1;
+ }
+
+ YQL_ENSURE(left.Maybe<TCoDataCtor>().IsValid());
+ YQL_ENSURE(right.Maybe<TCoDataCtor>().IsValid());
+
+ auto leftDataCtor = left.Cast<TCoDataCtor>();
+ auto rightDataCtor = right.Cast<TCoDataCtor>();
+
+ auto leftType = GetCompareType(left.Ref().Content());
+ auto rightType = GetCompareType(right.Ref().Content());
+ if (leftType == rightType) {
+ switch (leftType) {
+ case EDataSlot::Bool:
+ return NUdf::CompareValues(leftType,
+ TUnboxedValuePod(NYql::FromString<bool>(leftDataCtor.Literal().Ref(), leftType)),
+ TUnboxedValuePod(NYql::FromString<bool>(rightDataCtor.Literal().Ref(), leftType)));
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ case EDataSlot::Date:
+ case EDataSlot::Datetime:
+ return NUdf::CompareValues(leftType,
+ TUnboxedValuePod(NYql::FromString<i64>(leftDataCtor.Literal().Ref(), leftType)),
+ TUnboxedValuePod(NYql::FromString<i64>(rightDataCtor.Literal().Ref(), leftType)));
+ case EDataSlot::Uint64:
+ case EDataSlot::Timestamp:
+ return NUdf::CompareValues(leftType,
+ TUnboxedValuePod(NYql::FromString<ui64>(leftDataCtor.Literal().Ref(), leftType)),
+ TUnboxedValuePod(NYql::FromString<ui64>(rightDataCtor.Literal().Ref(), leftType)));
+ case EDataSlot::Float:
+ return NUdf::CompareValues(leftType,
+ TUnboxedValuePod(NYql::FromString<float>(leftDataCtor.Literal().Ref(), leftType)),
+ TUnboxedValuePod(NYql::FromString<float>(rightDataCtor.Literal().Ref(), leftType)));
+ case EDataSlot::Double:
+ return NUdf::CompareValues(leftType,
+ TUnboxedValuePod(NYql::FromString<double>(leftDataCtor.Literal().Ref(), leftType)),
+ TUnboxedValuePod(NYql::FromString<double>(rightDataCtor.Literal().Ref(), leftType)));
+ case EDataSlot::String:
+ case EDataSlot::Utf8:
+ case EDataSlot::Uuid:
+ return leftDataCtor.Literal().Value().compare(rightDataCtor.Literal().Value());
+ case EDataSlot::TzDate:
+ return NUdf::CompareValues(leftType,
+ GetTzValue<ui16>(leftDataCtor.Literal().Value()),
+ GetTzValue<ui16>(rightDataCtor.Literal().Value()));
+ case EDataSlot::TzDatetime:
+ return NUdf::CompareValues(leftType,
+ GetTzValue<ui32>(leftDataCtor.Literal().Value()),
+ GetTzValue<ui32>(rightDataCtor.Literal().Value()));
+ case EDataSlot::TzTimestamp:
+ return NUdf::CompareValues(leftType,
+ GetTzValue<ui64>(leftDataCtor.Literal().Value()),
+ GetTzValue<ui64>(rightDataCtor.Literal().Value()));
+ case EDataSlot::Decimal:
+ return NUdf::CompareValues(leftType,
+ GetDecimalValue(left.Cast<TCoDecimal>()),
+ GetDecimalValue(right.Cast<TCoDecimal>()));
+ default:
+ break;
+ }
+ }
+
+ YQL_LOG_CTX_THROW yexception() << "Cannot compare " << left.Ref().Content() << " and " << right.Ref().Content();
+}
+
+bool AdjacentDataNodes(NNodes::TExprBase left, NNodes::TExprBase right)
+{
+ using namespace NNodes;
+ using namespace NKikimr::NUdf;
+
+ YQL_ENSURE(left.Maybe<TCoDataCtor>().IsValid());
+ YQL_ENSURE(right.Maybe<TCoDataCtor>().IsValid());
+
+ auto leftLiteral = left.Cast<TCoDataCtor>().Literal();
+ auto rightLiteral = right.Cast<TCoDataCtor>().Literal();
+
+ auto leftType = GetCompareType(left.Ref().Content());
+ auto rightType = GetCompareType(right.Ref().Content());
+ if (leftType == rightType) {
+ switch (leftType) {
+ case EDataSlot::Bool:
+ return !FromString<bool>(leftLiteral.Ref(), leftType) && FromString<bool>(rightLiteral.Ref(), rightType);
+ case EDataSlot::Int64:
+ return FromString<i64>(leftLiteral.Ref(), leftType) + 1 == FromString<i64>(rightLiteral.Ref(), rightType);
+ case EDataSlot::Uint64:
+ return FromString<ui64>(leftLiteral.Ref(), leftType) + 1 == FromString<ui64>(rightLiteral.Ref(), rightType);
+ default:
+ return false;
+ }
+ }
+
+ YQL_LOG_CTX_THROW yexception() << "Cannot compare " << left.Ref().Content() << " and " << right.Ref().Content();
+}
+
+template <bool UpperBound>
+void ScaleDate(ui64& val, bool& includeBound, EDataSlot srcDataSlot, EDataSlot targetDataSlot) {
+ switch (srcDataSlot) {
+ case EDataSlot::Date:
+ switch (targetDataSlot) {
+ case EDataSlot::Datetime:
+ val *= 86400ull;
+ break;
+ case EDataSlot::Timestamp:
+ val *= 86400000000ull;
+ break;
+ default:
+ break;
+ }
+ break;
+ case EDataSlot::Datetime:
+ switch (targetDataSlot) {
+ case EDataSlot::Date:
+ if (val % 86400ull) {
+ includeBound = UpperBound;
+ }
+ val /= 86400ull;
+ break;
+ case EDataSlot::Timestamp:
+ val *= 1000000ull;
+ break;
+ default:
+ break;
+ }
+ break;
+ case EDataSlot::Timestamp:
+ switch (targetDataSlot) {
+ case EDataSlot::Date:
+ if (val % 86400000000ull) {
+ includeBound = UpperBound;
+ }
+ val /= 86400000000ull;
+ break;
+ case EDataSlot::Datetime:
+ if (val % 1000000ULL) {
+ includeBound = UpperBound;
+ }
+ val /= 1000000ULL;
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+bool AdjustLowerValue(TString& lowerValue, bool& lowerInclude, EDataSlot lowerDataSlot, EDataSlot targetDataSlot) {
+ if (EDataSlot::Interval == targetDataSlot
+ || (GetDataTypeInfo(targetDataSlot).Features & NUdf::SignedIntegralType))
+ {
+ // Target is signed integer
+ if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::FloatType) {
+ double dVal = FromString<double>(lowerValue);
+ i64 val = 0;
+ if (dVal <= static_cast<double>(std::numeric_limits<i64>::min())) {
+ if (dVal < static_cast<double>(std::numeric_limits<i64>::min())) {
+ lowerInclude = true;
+ }
+ val = std::numeric_limits<i64>::min();
+ }
+ else if (dVal == static_cast<double>(std::numeric_limits<i64>::max())) {
+ val = std::numeric_limits<i64>::max();
+ }
+ else if (dVal > static_cast<double>(std::numeric_limits<i64>::max())) {
+ return false;
+ }
+ else if (std::ceil(dVal) != dVal) {
+ lowerInclude = true;
+ val = static_cast<i64>(std::ceil(dVal));
+ }
+ else {
+ val = static_cast<i64>(dVal);
+ }
+
+ TMaybe<i64> valMin;
+ TMaybe<i64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMin = static_cast<i64>(std::numeric_limits<i8>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMin = static_cast<i64>(std::numeric_limits<i16>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMin = static_cast<i64>(std::numeric_limits<i32>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMin = static_cast<i64>(std::numeric_limits<i64>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && (val > *valMax || (!lowerInclude && val == *valMax))) {
+ return false;
+ }
+
+ if (valMin && val < *valMin) {
+ lowerInclude = true;
+ val = *valMin;
+ }
+
+ if (!lowerInclude) {
+ lowerInclude = true;
+ ++val;
+ }
+
+ lowerValue = ToString(val);
+ }
+ else if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::UnsignedIntegralType) {
+ ui64 val = FromString<ui64>(lowerValue);
+ TMaybe<ui64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMax = static_cast<ui64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMax = static_cast<ui64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMax = static_cast<ui64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMax = static_cast<ui64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && (val > *valMax || (!lowerInclude && val == *valMax))) {
+ return false;
+ }
+
+ if (!lowerInclude) {
+ lowerInclude = true;
+ ++val;
+ lowerValue = ToString(val);
+ }
+ }
+ else if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::SignedIntegralType) {
+ i64 val = FromString<i64>(lowerValue);
+
+ TMaybe<i64> valMin;
+ TMaybe<i64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMin = static_cast<i64>(std::numeric_limits<i8>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMin = static_cast<i64>(std::numeric_limits<i16>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMin = static_cast<i64>(std::numeric_limits<i32>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMin = static_cast<i64>(std::numeric_limits<i64>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && (val > *valMax || (!lowerInclude && val == *valMax))) {
+ return false;
+ }
+
+ if (valMin && val < *valMin) {
+ lowerInclude = true;
+ val = *valMin;
+ }
+
+ if (!lowerInclude) {
+ lowerInclude = true;
+ ++val;
+ }
+
+ lowerValue = ToString(val);
+ }
+ }
+ else if (GetDataTypeInfo(targetDataSlot).Features & (NUdf::UnsignedIntegralType | NUdf::DateType)) {
+ // Target is unsigned integer
+ ui64 val = 0;
+ if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::SignedIntegralType) {
+ if (FromString<i64>(lowerValue) < 0) {
+ lowerInclude = true;
+ val = 0;
+ }
+ else {
+ val = FromString<ui64>(lowerValue);
+ }
+ }
+ else if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::FloatType) {
+ double dVal = FromString<double>(lowerValue);
+
+ if (dVal > static_cast<double>(std::numeric_limits<ui64>::max())) {
+ return false;
+ }
+ else if (dVal == static_cast<double>(std::numeric_limits<ui64>::max())) {
+ val = std::numeric_limits<ui64>::max();
+ }
+ else if (dVal < 0. || std::ceil(dVal) != dVal) {
+ lowerInclude = true;
+ val = static_cast<ui64>(std::ceil(Max<double>(0., dVal)));
+ }
+ else {
+ val = static_cast<ui64>(dVal);
+ }
+ }
+ else if (GetDataTypeInfo(lowerDataSlot).Features & NUdf::UnsignedIntegralType) {
+ val = FromString<ui64>(lowerValue);
+ }
+ else if ((GetDataTypeInfo(lowerDataSlot).Features & NUdf::DateType) && (GetDataTypeInfo(targetDataSlot).Features & NUdf::DateType)
+ && lowerDataSlot != targetDataSlot) {
+
+ val = FromString<ui64>(lowerValue);
+ ScaleDate<false>(val, lowerInclude, lowerDataSlot, targetDataSlot);
+ }
+ else {
+ // Nothing to change for other types
+ return true;
+ }
+ TMaybe<ui64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Uint8:
+ valMax = static_cast<ui64>(std::numeric_limits<ui8>::max());
+ break;
+ case EDataSlot::Uint16:
+ valMax = static_cast<ui64>(std::numeric_limits<ui16>::max());
+ break;
+ case EDataSlot::Uint32:
+ valMax = static_cast<ui64>(std::numeric_limits<ui32>::max());
+ break;
+ case EDataSlot::Uint64:
+ valMax = static_cast<ui64>(std::numeric_limits<ui64>::max());
+ break;
+ case EDataSlot::Date:
+ valMax = static_cast<ui64>(NUdf::MAX_DATE);
+ break;
+ case EDataSlot::Datetime:
+ valMax = static_cast<ui64>(NUdf::MAX_DATETIME);
+ break;
+ case EDataSlot::Timestamp:
+ valMax = static_cast<ui64>(NUdf::MAX_TIMESTAMP);
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && (val > *valMax || (!lowerInclude && val == *valMax))) {
+ return false;
+ }
+
+ if (!lowerInclude) {
+ lowerInclude = true;
+ ++val;
+ }
+
+ lowerValue = ToString(val);
+ }
+ return true;
+}
+
+bool AdjustUpperValue(TString& upperValue, bool& upperInclude, EDataSlot upperDataSlot, EDataSlot targetDataSlot) {
+ if (EDataSlot::Interval == targetDataSlot
+ || (GetDataTypeInfo(targetDataSlot).Features & NUdf::SignedIntegralType))
+ {
+ // Target is signed integer
+ if (GetDataTypeInfo(upperDataSlot).Features & NUdf::FloatType) {
+ double dVal = FromString<double>(upperValue);
+ i64 val = 0;
+ if (dVal >= static_cast<double>(std::numeric_limits<i64>::max())) {
+ if (dVal > static_cast<double>(std::numeric_limits<i64>::max())) {
+ upperInclude = true;
+ }
+ val = std::numeric_limits<i64>::max();
+ }
+ else if (dVal < static_cast<double>(std::numeric_limits<i64>::min())) {
+ return false;
+ }
+ else if (dVal == static_cast<double>(std::numeric_limits<i64>::min())) {
+ val = std::numeric_limits<i64>::min();
+ }
+ else if (std::floor(dVal) != dVal) {
+ upperInclude = true;
+ val = static_cast<ui64>(std::floor(dVal));
+ }
+ else {
+ val = static_cast<i64>(dVal);
+ }
+
+ TMaybe<i64> valMin;
+ TMaybe<i64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMin = static_cast<i64>(std::numeric_limits<i8>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMin = static_cast<i64>(std::numeric_limits<i16>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMin = static_cast<i64>(std::numeric_limits<i32>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMin = static_cast<i64>(std::numeric_limits<i64>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMin && (val < *valMin || (!upperInclude && val == *valMin))) {
+ return false;
+ }
+
+ if (valMax && val > *valMax) {
+ upperInclude = true;
+ val = *valMax;
+ }
+ else if (upperInclude && (!valMax || val < *valMax)) {
+ upperInclude = false;
+ ++val;
+ }
+
+ upperValue = ToString(val);
+ }
+ else if (GetDataTypeInfo(upperDataSlot).Features & NUdf::UnsignedIntegralType) {
+ ui64 val = FromString<ui64>(upperValue);
+ TMaybe<ui64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMax = static_cast<ui64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMax = static_cast<ui64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMax = static_cast<ui64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMax = static_cast<ui64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && val > *valMax) {
+ upperInclude = true;
+ val = *valMax;
+ upperValue = ToString(val);
+ }
+ else if (upperInclude && (!valMax || val < *valMax)) {
+ upperInclude = false;
+ ++val;
+ upperValue = ToString(val);
+ }
+ }
+ else if (GetDataTypeInfo(upperDataSlot).Features & NUdf::SignedIntegralType) {
+ i64 val = FromString<i64>(upperValue);
+
+ TMaybe<i64> valMin;
+ TMaybe<i64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Int8:
+ valMin = static_cast<i64>(std::numeric_limits<i8>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i8>::max());
+ break;
+ case EDataSlot::Int16:
+ valMin = static_cast<i64>(std::numeric_limits<i16>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i16>::max());
+ break;
+ case EDataSlot::Int32:
+ valMin = static_cast<i64>(std::numeric_limits<i32>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i32>::max());
+ break;
+ case EDataSlot::Int64:
+ case EDataSlot::Interval:
+ valMin = static_cast<i64>(std::numeric_limits<i64>::min());
+ valMax = static_cast<i64>(std::numeric_limits<i64>::max());
+ break;
+ default:
+ break;
+ }
+
+ if (valMin && (val < *valMin || (!upperInclude && val == *valMin))) {
+ return false;
+ }
+
+ if (valMax && val > *valMax) {
+ upperInclude = true;
+ val = *valMax;
+ }
+ else if (upperInclude && (!valMax || val < *valMax)) {
+ upperInclude = false;
+ ++val;
+ }
+
+ upperValue = ToString(val);
+ }
+ }
+ else if (GetDataTypeInfo(targetDataSlot).Features & (NUdf::UnsignedIntegralType | NUdf::DateType)) {
+ // Target is unsigned integer
+ ui64 val = 0;
+ if (GetDataTypeInfo(upperDataSlot).Features & NUdf::SignedIntegralType) {
+ if (FromString<i64>(upperValue) < 0) {
+ return false;
+ }
+ else {
+ val = FromString<ui64>(upperValue);
+ }
+ }
+ else if (GetDataTypeInfo(upperDataSlot).Features & NUdf::FloatType) {
+ double dVal = FromString<double>(upperValue);
+ if (dVal < 0.) {
+ return false;
+ }
+ else if (dVal >= static_cast<double>(std::numeric_limits<ui64>::max())) {
+ if (dVal > static_cast<double>(std::numeric_limits<ui64>::max())) {
+ upperInclude = true;
+ }
+ val = std::numeric_limits<ui64>::max();
+ }
+ else if (std::floor(dVal) != dVal) {
+ upperInclude = true;
+ val = static_cast<ui64>(std::floor(dVal));
+ }
+ else {
+ val = static_cast<ui64>(dVal);
+ }
+ }
+ else if (GetDataTypeInfo(upperDataSlot).Features & NUdf::UnsignedIntegralType) {
+ val = FromString<ui64>(upperValue);
+ }
+ else if ((GetDataTypeInfo(upperDataSlot).Features & NUdf::DateType) && (GetDataTypeInfo(targetDataSlot).Features & NUdf::DateType)
+ && upperDataSlot != targetDataSlot) {
+
+ val = FromString<ui64>(upperValue);
+ ScaleDate<true>(val, upperInclude, upperDataSlot, targetDataSlot);
+ }
+ else {
+ // Nothing to change for other types
+ return true;
+ }
+
+ TMaybe<ui64> valMax;
+ switch (targetDataSlot) {
+ case EDataSlot::Uint8:
+ valMax = static_cast<ui64>(std::numeric_limits<ui8>::max());
+ break;
+ case EDataSlot::Uint16:
+ valMax = static_cast<ui64>(std::numeric_limits<ui16>::max());
+ break;
+ case EDataSlot::Uint32:
+ valMax = static_cast<ui64>(std::numeric_limits<ui32>::max());
+ break;
+ case EDataSlot::Uint64:
+ valMax = static_cast<ui64>(std::numeric_limits<ui64>::max());
+ break;
+ case EDataSlot::Date:
+ valMax = static_cast<ui64>(NUdf::MAX_DATE);
+ break;
+ case EDataSlot::Datetime:
+ valMax = static_cast<ui64>(NUdf::MAX_DATETIME);
+ break;
+ case EDataSlot::Timestamp:
+ valMax = static_cast<ui64>(NUdf::MAX_TIMESTAMP);
+ break;
+ default:
+ break;
+ }
+
+ if (valMax && val > *valMax) {
+ upperInclude = true;
+ val = *valMax;
+ }
+ else if (upperInclude && (!valMax || val < *valMax)) {
+ upperInclude = false;
+ ++val;
+ }
+
+ upperValue = ToString(val);
+ }
+ return true;
+}
+
+
+struct TKeyRangeInternal {
+ TVector<TExprBase> ExactPart;
+ TMaybeNode<TExprBase> LowerLeaf;
+ TMaybeNode<TExprBase> UpperLeaf;
+ bool LowerInclude = false;
+ bool UpperInclude = false;
+
+ bool IsFullScan() const {
+ return ExactPart.empty() && !LowerLeaf && !UpperLeaf;
+ }
+
+ bool IsExact() const {
+ return (!ExactPart.empty() && !LowerLeaf && !UpperLeaf)
+ || (LowerLeaf.Maybe<TCoNull>() && UpperLeaf.Maybe<TCoNull>())
+ || (LowerLeaf.IsValid() && UpperLeaf.IsValid() && LowerInclude
+ && ((UpperInclude && 0 == CompareDataNodes(LowerLeaf.Cast(), UpperLeaf.Cast()))
+ || (!UpperInclude && AdjacentDataNodes(LowerLeaf.Cast(), UpperLeaf.Cast())))
+ );
+ }
+
+ bool IsLowerInf() const {
+ return ExactPart.empty() && !LowerLeaf;
+ }
+
+ bool IsUpperInf() const {
+ return ExactPart.empty() && !UpperLeaf;
+ }
+
+ void NormalizeExact() {
+ if (LowerLeaf && UpperLeaf) {
+ ExactPart.push_back(LowerLeaf.Cast());
+ LowerLeaf = UpperLeaf = {};
+ }
+ }
+
+ TYtRangesInfo::TKeyRange ToKeyRange() const {
+ TYtRangesInfo::TKeyRange range;
+ YQL_ENSURE(LowerLeaf || UpperLeaf);
+ range.Lower.assign(ExactPart.begin(), ExactPart.end());
+ range.Upper.assign(ExactPart.begin(), ExactPart.end());
+ if (LowerLeaf) {
+ range.Lower.push_back(LowerLeaf.Cast());
+ }
+ if (UpperLeaf) {
+ range.Upper.push_back(UpperLeaf.Cast());
+ }
+ range.LowerInclude = LowerInclude || !LowerLeaf;
+ range.UpperInclude = (UpperInclude && UpperLeaf) || (!UpperLeaf && !ExactPart.empty());
+ return range;
+ }
+};
+
+template <bool leftLower, bool rightLower>
+bool CompareKeyRange(const TKeyRangeInternal& l, const TKeyRangeInternal& r) {
+ if (leftLower && l.IsLowerInf()) {
+ // Left lower is -inf, always less than right
+ return !rightLower || !r.IsLowerInf();
+ }
+ if (!leftLower && l.IsUpperInf()) {
+ // Left upper is +inf, always greater than right
+ return false;
+ }
+ if (rightLower && r.IsLowerInf()) {
+ // Right lower is -inf, always less than left
+ return false;
+ }
+ if (!rightLower && r.IsUpperInf()) {
+ // Right upper is +inf, always greater than left
+ return leftLower || !l.IsUpperInf();
+ }
+ auto& lBound = leftLower ? l.LowerLeaf : l.UpperLeaf;
+ auto& rBound = rightLower ? r.LowerLeaf : r.UpperLeaf;
+ int cmp = 0;
+ for (size_t i = 0; i < Min(l.ExactPart.size(), r.ExactPart.size()) && 0 == cmp; ++i) {
+ cmp = CompareDataNodes(l.ExactPart[i], r.ExactPart[i]);
+ }
+ if (0 == cmp) {
+ if (l.ExactPart.size() < r.ExactPart.size() && lBound) {
+ cmp = CompareDataNodes(lBound.Cast(), r.ExactPart[l.ExactPart.size()]);
+ }
+ else if (l.ExactPart.size() > r.ExactPart.size() && rBound) {
+ cmp = CompareDataNodes(l.ExactPart[r.ExactPart.size()], rBound.Cast());
+ }
+ else if (lBound && rBound) {
+ cmp = CompareDataNodes(lBound.Cast(), rBound.Cast());
+ }
+ }
+ if (cmp < 0) {
+ return true;
+ }
+ else if (cmp > 0) {
+ return false;
+ }
+
+ if (l.ExactPart.size() == r.ExactPart.size()) {
+ if (leftLower && !l.LowerLeaf) {
+ // Left lower is -inf, always less than right
+ return !rightLower || r.LowerLeaf;
+ }
+ if (!leftLower && !l.UpperLeaf) {
+ // Left upper is +inf, always greater than right
+ return false;
+ }
+ if (rightLower && !r.LowerLeaf) {
+ // Right lower is -inf, always less than left
+ return false;
+ }
+ if (!rightLower && !r.UpperLeaf) {
+ // Right upper is +inf, always greater than left
+ return leftLower || l.UpperLeaf;
+ }
+ }
+
+ auto lInclude = leftLower ? !l.LowerInclude : l.UpperInclude;
+ auto rInclude = rightLower ? !r.LowerInclude : r.UpperInclude;
+
+ if (rightLower) {
+ return lInclude == rInclude
+ ? (l.ExactPart.size() + lBound.IsValid()) < (r.ExactPart.size() + rBound.IsValid())
+ : lInclude < rInclude;
+ } else {
+ return lInclude == rInclude
+ ? (l.ExactPart.size() + lBound.IsValid()) > (r.ExactPart.size() + rBound.IsValid())
+ : lInclude > rInclude;
+ }
+}
+
+void DeduplicateRanges(TVector<TKeyRangeInternal>& ranges) {
+ std::stable_sort(ranges.begin(), ranges.end(), CompareKeyRange<true, true>);
+
+ size_t i = 0;
+ while (i + 1 < ranges.size()) {
+ TKeyRangeInternal& current = ranges[i];
+ TKeyRangeInternal& next = ranges[i + 1];
+ if (!CompareKeyRange<false, true>(current, next)) { // current.Upper >= next.Lower
+ if (CompareKeyRange<false, false>(current, next)) { // current.Upper < next.Upper
+ YQL_ENSURE(current.ExactPart.size() == next.ExactPart.size());
+ DoSwap(current.UpperLeaf, next.UpperLeaf);
+ DoSwap(current.UpperInclude, next.UpperInclude);
+ }
+ ranges.erase(ranges.begin() + i + 1);
+ if (ranges[i].IsFullScan()) {
+ ranges.clear();
+ return;
+ }
+ }
+ else {
+ ++i;
+ }
+ }
+}
+
+TVector<NNodes::TExprBase> ConvertBoundaryNode(const TExprNode& boundary, bool& isIncluded) {
+ isIncluded = false;
+ YQL_ENSURE(boundary.IsList());
+ TExprNodeList items = boundary.ChildrenList();
+ YQL_ENSURE(items.size() >= 2);
+
+ YQL_ENSURE(items.back()->IsCallable("Int32"));
+ isIncluded = FromString<i32>(items.back()->Head().Content()) != 0;
+ items.pop_back();
+
+ TVector<NNodes::TExprBase> result;
+ for (auto item : items) {
+ if (item->IsCallable("Nothing")) {
+ break;
+ }
+ YQL_ENSURE(item->IsCallable("Just"));
+ item = item->HeadPtr();
+
+ result.push_back(NNodes::TExprBase(std::move(item)));
+ YQL_ENSURE(result.back().Maybe<TCoDataCtor>() ||
+ result.back().Maybe<TCoNothing>() ||
+ result.back().Maybe<TCoJust>());
+ }
+
+ return result;
+}
+
+TExprBase RoundTz(const TExprBase& node, bool down, TExprContext& ctx) {
+ if (auto maybeTz = node.Maybe<TCoTzDateBase>()) {
+ TStringBuf tzName = maybeTz.Cast().Literal().Value();
+ TStringBuf value;
+ GetNext(tzName, ',', value);
+
+ const auto& names = NUdf::GetTimezones();
+ YQL_ENSURE(!names.empty());
+
+ TStringBuf targetName;
+ if (down) {
+ targetName = names.front();
+ } else {
+ for (auto it = names.rbegin(); it != names.rend(); ++it) {
+ if (!it->empty()) {
+ targetName = *it;
+ break;
+ }
+ }
+ }
+ YQL_ENSURE(!tzName.empty());
+ YQL_ENSURE(!targetName.empty());
+
+ if (tzName != targetName) {
+ return TExprBase(ctx.Builder(node.Pos())
+ .Callable(node.Ref().Content())
+ .Atom(0, TStringBuilder() << value << "," << targetName)
+ .Seal()
+ .Build());
+ }
+ }
+ return node;
+}
+
+TMaybe<TYtRangesInfo::TKeyRange> WidenTzKeys(const TYtRangesInfo::TKeySingle& single, TExprContext& ctx) {
+ if (AllOf(single.Key, [](const auto& key){ return !TCoTzDateBase::Match(key.Raw()); })) {
+ return {};
+ }
+ TYtRangesInfo::TKeyRange result;
+ result.LowerInclude = result.UpperInclude = true;
+ for (auto& key : single.Key) {
+ result.Lower.push_back(RoundTz(key, true, ctx));
+ result.Upper.push_back(RoundTz(key, false, ctx));
+ }
+ return result;
+}
+
+void WidenTzKeys(TYtRangesInfo::TKeyRange& range, TExprContext& ctx) {
+ for (auto& key : range.Lower) {
+ key = RoundTz(key, true, ctx);
+ }
+ for (auto& key : range.Upper) {
+ key = RoundTz(key, false, ctx);
+ }
+}
+
+} // unnamed
+
+TYtRangesInfo::TPtr TYtRangesInfo::ApplyLegacyKeyFilters(const TVector<TExprBase>& keyFilters,
+ const TYqlRowSpecInfo::TPtr& rowSpec, TExprContext& ctx)
+{
+ YQL_ENSURE(rowSpec && rowSpec->IsSorted());
+ TVector<TKeyRangeInternal> ranges;
+ for (auto andGrp: keyFilters) {
+ bool emptyMatch = false;
+ TKeyRangeInternal key;
+ size_t memberIndex = 0;
+ for (auto keyPredicates: andGrp.Cast<TCoNameValueTupleList>()) {
+ YQL_ENSURE(memberIndex < rowSpec->SortMembers.size() && rowSpec->SortMembers[memberIndex] == keyPredicates.Name().Value());
+ const TTypeAnnotationNode* columnType = rowSpec->SortedByTypes[memberIndex];
+ if (columnType->GetKind() == ETypeAnnotationKind::Optional) {
+ columnType = columnType->Cast<TOptionalExprType>()->GetItemType();
+ }
+ auto columnDataSlot = columnType->Cast<TDataExprType>()->GetSlot();
+ ++memberIndex;
+
+ key.NormalizeExact();
+ key.LowerLeaf = key.UpperLeaf = {};
+ key.LowerInclude = false;
+ key.UpperInclude = false;
+
+ auto isWithinRange = [&key](TExprBase value, bool includeBounds) -> int {
+ if (key.UpperLeaf) {
+ int cmp = CompareDataNodes(value, key.UpperLeaf.Cast());
+ if (cmp > 0 || (cmp == 0 && !key.UpperInclude && includeBounds)) {
+ return 1; // out of range: greater than upper bound
+ }
+ }
+ if (key.LowerLeaf) {
+ int cmp = CompareDataNodes(key.LowerLeaf.Cast(), value);
+ if (cmp > 0 || (cmp == 0 && !key.LowerInclude && includeBounds)) {
+ return -1; // out of range: less than lower bound
+ }
+ }
+ return 0;
+ };
+
+ for (auto cmp: keyPredicates.Value().Cast<TCoNameValueTupleList>()) {
+ auto operation = cmp.Name().Value();
+ TExprBase value = cmp.Value().Cast();
+ if (value.Maybe<TCoNothing>()) {
+ emptyMatch = true;
+ break;
+ }
+ if (value.Maybe<TCoNull>()) {
+ if ((key.LowerLeaf.IsValid() && !key.LowerLeaf.Maybe<TCoNull>())
+ || (key.UpperLeaf.IsValid() && !key.UpperLeaf.Maybe<TCoNull>()))
+ {
+ emptyMatch = true;
+ break;
+ }
+ key.LowerLeaf = key.UpperLeaf = value;
+ key.LowerInclude = key.UpperInclude = true;
+ }
+ else {
+ if (key.LowerLeaf.Maybe<TCoNull>() || key.UpperLeaf.Maybe<TCoNull>()) {
+ emptyMatch = true;
+ break;
+ }
+
+ if (value.Maybe<TCoJust>()) {
+ value = value.Cast<TCoJust>().Input();
+ }
+
+ if (operation == TStringBuf("<") || operation == TStringBuf("<=")) {
+ bool includeBound = (operation == TStringBuf("<="));
+ int cmp = isWithinRange(value, includeBound);
+ if (cmp < 0) /* lower than existing lower bound yields an empty match */ {
+ emptyMatch = true;
+ break;
+ }
+ else if (cmp == 0) {
+ key.UpperLeaf = value;
+ key.UpperInclude = includeBound;
+ }
+ }
+ else if (operation == TStringBuf(">") || operation == TStringBuf(">=")) {
+ bool includeBound = (operation == TStringBuf(">="));
+ int cmp = isWithinRange(value, includeBound);
+ if (cmp > 0) /* upper than existing upper bound yields an empty match */ {
+ emptyMatch = true;
+ break;
+ }
+ else if (cmp == 0) {
+ key.LowerLeaf = value;
+ key.LowerInclude = includeBound;
+ }
+ }
+ else if (operation == TStringBuf("==")) {
+ int cmp = isWithinRange(value, true);
+ if (cmp != 0) /* value out of the bounds */ {
+ emptyMatch = true;
+ break;
+ }
+ else {
+ key.UpperLeaf = key.LowerLeaf = value;
+ key.UpperInclude = key.LowerInclude = true;
+ }
+ }
+ else if (operation == TStringBuf("StartsWith")) {
+ int cmp = isWithinRange(value, true);
+ if (cmp != 0) /* value out of the bounds */ {
+ emptyMatch = true;
+ break;
+ }
+
+ key.LowerLeaf = value;
+ key.LowerInclude = true;
+ YQL_ENSURE(value.Ref().IsCallable({"String", "Utf8"}));
+ if (auto content = TString{key.LowerLeaf.Cast<TCoDataCtor>().Literal().Value()}; !content.empty()) {
+ std::optional<std::string> incremented;
+ if (columnDataSlot == EDataSlot::String) {
+ key.LowerLeaf = value = TExprBase(ctx.RenameNode(value.Ref(), "String"));
+ incremented = NextLexicographicString(content);
+ } else {
+ YQL_ENSURE(columnDataSlot == EDataSlot::Utf8);
+ if (IsUtf8(content)) {
+ incremented = NextValidUtf8(content);
+ } else {
+ emptyMatch = true;
+ break;
+ }
+ }
+
+ if (incremented) {
+ key.UpperLeaf = ctx.ChangeChildren(value.Ref(), {ctx.NewAtom(value.Ref().Head().Pos(), *incremented)});
+ key.UpperInclude = false;
+ }
+ }
+ }
+ }
+ }
+
+ if (!emptyMatch && key.LowerLeaf.Maybe<TCoDataCtor>()) {
+ auto lowerDataSlot = GetDataSlot(key.LowerLeaf.Cast().Ref().Content());
+ if (GetDataTypeInfo(lowerDataSlot).Features & (NUdf::NumericType | NUdf::DateType)) {
+ TString newLowerValue = TString{key.LowerLeaf.Cast<TCoDataCtor>().Literal().Value()};
+ if (!AdjustLowerValue(newLowerValue, key.LowerInclude, lowerDataSlot, columnDataSlot)) {
+ emptyMatch = true;
+ }
+ else if (columnDataSlot != lowerDataSlot
+ || newLowerValue != key.LowerLeaf.Cast<TCoDataCtor>().Literal().Value())
+ {
+ key.LowerLeaf = TExprBase(ctx.Builder(key.LowerLeaf.Cast().Pos())
+ .Callable(GetDataTypeInfo(columnDataSlot).Name)
+ .Atom(0, newLowerValue)
+ .Seal()
+ .Build());
+ }
+ }
+ }
+
+ if (!emptyMatch && key.UpperLeaf.Maybe<TCoDataCtor>()) {
+ auto upperDataSlot = GetDataSlot(key.UpperLeaf.Cast().Ref().Content());
+ if (GetDataTypeInfo(upperDataSlot).Features & (NUdf::NumericType | NUdf::DateType)) {
+ TString newUpperValue = TString{key.UpperLeaf.Cast<TCoDataCtor>().Literal().Value()};
+ if (!AdjustUpperValue(newUpperValue, key.UpperInclude, upperDataSlot, columnDataSlot)) {
+ emptyMatch = true;
+ }
+ else if (columnDataSlot != upperDataSlot
+ || newUpperValue != key.UpperLeaf.Cast<TCoDataCtor>().Literal().Value())
+ {
+ key.UpperLeaf = TExprBase(ctx.Builder(key.UpperLeaf.Cast().Pos())
+ .Callable(GetDataTypeInfo(columnDataSlot).Name)
+ .Atom(0, newUpperValue)
+ .Seal()
+ .Build());
+ }
+ }
+ }
+
+ if (emptyMatch) {
+ break;
+ }
+
+ if (!key.IsExact()) {
+ break;
+ }
+ }
+
+ if (!emptyMatch) {
+ if (key.IsFullScan()) {
+ return TYtRangesInfo::TPtr();
+ }
+
+ ranges.push_back(std::move(key));
+ }
+ }
+
+ TYtRangesInfo::TPtr rangesInfo = MakeIntrusive<TYtRangesInfo>();
+ if (!ranges.empty()) {
+ if (ranges.size() > 1) {
+ DeduplicateRanges(ranges);
+ if (ranges.empty()) {
+ return TYtRangesInfo::TPtr(); // Full scan
+ }
+ }
+ for (TKeyRangeInternal& range: ranges) {
+ if (range.IsExact()) {
+ range.NormalizeExact();
+ TKeySingle single{std::move(range.ExactPart)};
+ auto maybeKeyRange = WidenTzKeys(single, ctx);
+ if (maybeKeyRange) {
+ rangesInfo->Ranges.emplace_back(*maybeKeyRange);
+ } else {
+ rangesInfo->Ranges.emplace_back(std::move(single));
+ }
+ }
+ else {
+ TKeyRange r = range.ToKeyRange();
+ WidenTzKeys(r, ctx);
+ rangesInfo->Ranges.emplace_back(std::move(r));
+ }
+ }
+ }
+ return rangesInfo;
+}
+
+TYtRangesInfo::TPtr TYtRangesInfo::ApplyKeyFilter(const TExprNode& keyFilter) {
+ // keyFilter is List<Tuple<Left, Right>>, Left/Right are key boundaries
+ if (keyFilter.IsCallable("List")) {
+ return MakeEmptyRange();
+ }
+
+ YQL_ENSURE(keyFilter.IsCallable("AsList"));
+ auto res = MakeIntrusive<TYtRangesInfo>();
+ for (auto& interval : keyFilter.ChildrenList()) {
+ YQL_ENSURE(interval->IsList());
+ YQL_ENSURE(interval->ChildrenSize() == 2);
+
+ TKeyRange range;
+ range.Lower = ConvertBoundaryNode(interval->Head(), range.LowerInclude);
+ if (range.Lower.empty()) {
+ // just a convention
+ range.LowerInclude = true;
+ }
+ range.Upper = ConvertBoundaryNode(interval->Tail(), range.UpperInclude);
+
+ if (range.Lower.empty() && range.Upper.empty()) {
+ // full range
+ return {};
+ }
+
+ bool sameBounds = range.Lower.size() == range.Upper.size();
+ for (size_t i = 0; sameBounds && i < range.Lower.size(); ++i) {
+ sameBounds = range.Lower[i].Raw() == range.Upper[i].Raw();
+ }
+
+ if (sameBounds) {
+ YQL_ENSURE(range.LowerInclude && range.UpperInclude, "Not expecting empty regions here");
+ res->Ranges.emplace_back(TKeySingle{std::move(range.Lower)});
+ } else {
+ res->Ranges.emplace_back(std::move(range));
+ }
+ }
+
+ return res;
+}
+
+TYtRangesInfo::TPtr TYtRangesInfo::MakeEmptyRange() {
+ return MakeIntrusive<TYtRangesInfo>();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TYtColumnsInfo::Validate(TExprNode& node, TExprContext& ctx) {
+ if (!node.IsCallable(TCoVoid::CallableName())) {
+ if (!EnsureTuple(node, ctx)) {
+ return false;
+ }
+ THashSet<TStringBuf> columns;
+ THashSet<TStringBuf> renameFrom;
+ THashSet<TStringBuf> renameTo;
+
+ for (auto& child: node.Children()) {
+ TStringBuf name;
+ TStringBuf type;
+ TStringBuf originalName;
+
+ if (child->Type() == TExprNode::Atom) {
+ name = child->Content();
+ } else {
+ if (!EnsureTupleMinSize(*child, 2, ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*child->Child(0), ctx)) {
+ return false;
+ }
+ if (!EnsureAtom(*child->Child(1), ctx)) {
+ return false;
+ }
+
+ name = child->Child(0)->Content();
+ type = child->Child(1)->Content();
+
+ if (type == "weak") {
+ if (!EnsureTupleSize(*child, 2, ctx)) {
+ return false;
+ }
+ } else if (type == "rename") {
+ if (!EnsureTupleSize(*child, 3, ctx)) {
+ return false;
+ }
+ originalName = child->Child(2)->Content();
+ } else {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Unknown column type: " << type));
+ return false;
+ }
+ }
+
+ if (type != "rename") {
+ if (!columns.insert(name).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Duplicate column name: " << name));
+ return false;
+ }
+ } else {
+ if (!renameFrom.insert(originalName).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Duplicate original column name: " << originalName));
+ return false;
+ }
+ if (renameTo.find(originalName) != renameTo.end()) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Rename chain detected with original column name: " << originalName));
+ return false;
+ }
+
+ if (!renameTo.insert(name).second) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Duplicate target column name: " << name));
+ return false;
+ }
+ if (renameFrom.find(name) != renameFrom.end()) {
+ ctx.AddError(TIssue(ctx.GetPosition(child->Pos()), TStringBuilder() << "Rename chain detected with target column name: " << name));
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+void TYtColumnsInfo::Parse(NNodes::TExprBase node) {
+ Renames = {};
+ Columns = {};
+ Others = false;
+ if (node.Maybe<TExprList>()) {
+ for (auto child: node.Cast<TExprList>()) {
+ TColumn column;
+ if (child.Maybe<TCoAtom>()) {
+ column.Name = child.Cast<TCoAtom>().Value();
+ if (!Columns) {
+ Columns.ConstructInPlace();
+ }
+ Columns->emplace_back(column);
+ UpdateOthers(column.Name);
+ continue;
+ }
+ auto tuple = child.Cast<TCoAtomList>();
+ column.Name = tuple.Item(0).Value();
+ column.Type = tuple.Item(1).Value();
+ if (tuple.Ref().ChildrenSize() == 2) {
+ if (!Columns) {
+ Columns.ConstructInPlace();
+ }
+ Columns->emplace_back(column);
+ UpdateOthers(column.Name);
+ continue;
+ }
+
+ auto from = tuple.Item(2).Value();
+ if (!Renames) {
+ Renames.ConstructInPlace();
+ }
+ (*Renames)[from] = column.Name;
+ }
+
+ if (!Columns && !Renames) {
+ Columns.ConstructInPlace();
+ }
+ }
+}
+
+NNodes::TExprBase TYtColumnsInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos, const THashSet<TString>* filter) const {
+ if (!Columns && !Renames) {
+ return Build<TCoVoid>(ctx, pos).Done();
+ }
+
+ auto listBuilder = Build<TExprList>(ctx, pos);
+ if (Renames) {
+ for (auto& r: *Renames) {
+ listBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(r.second)
+ .Build()
+ .Add()
+ .Value("rename", TNodeFlags::Default)
+ .Build()
+ .Add()
+ .Value(r.first)
+ .Build()
+ .Build();
+ }
+ }
+
+ if (Columns) {
+ for (auto& c: *Columns) {
+ if (filter && !filter->contains(c.Name)) {
+ continue;
+ }
+ if (c.Type == "weak") {
+ listBuilder
+ .Add<TCoAtomList>()
+ .Add()
+ .Value(c.Name)
+ .Build()
+ .Add()
+ .Value(c.Type, TNodeFlags::Default)
+ .Build()
+ .Build();
+ } else {
+ listBuilder
+ .Add<TCoAtom>()
+ .Value(c.Name)
+ .Build();
+ }
+ }
+ }
+
+ return listBuilder.Done();
+}
+
+void TYtColumnsInfo::UpdateOthers(TStringBuf col) {
+ if (!Others) {
+ Others = (col == YqlOthersColumnName);
+ }
+}
+
+void TYtColumnsInfo::FillRichYPath(NYT::TRichYPath& path, bool withColumns) const {
+ if (Columns && withColumns) {
+ TVector<TString> columns;
+ for (auto& c: *Columns) {
+ columns.push_back(c.Name);
+ }
+ path.Columns(columns);
+ }
+
+ if (Renames) {
+ path.RenameColumns(*Renames);
+ }
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool TYtPathInfo::Validate(const TExprNode& node, TExprContext& ctx) {
+ if (!node.IsCallable(TYtPath::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), TStringBuilder() << "Expected " << TYtPath::CallableName()));
+ return false;
+ }
+ if (!EnsureArgsCount(node, 4, ctx)) {
+ return false;
+ }
+
+ if (!node.Child(TYtPath::idx_Table)->IsCallable(TYtTable::CallableName())
+ && !node.Child(TYtPath::idx_Table)->IsCallable(TYtOutput::CallableName())
+ && !node.Child(TYtPath::idx_Table)->IsCallable(TYtOutTable::CallableName())) {
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(TYtPath::idx_Table)->Pos()), TStringBuilder() << "Unexpected "
+ << node.Child(TYtPath::idx_Table)->Content()));
+ return false;
+ }
+
+ if (!TYtColumnsInfo::Validate(*node.Child(TYtPath::idx_Columns), ctx)) {
+ return false;
+ }
+
+ if (!node.Child(TYtPath::idx_Ranges)->IsCallable(TCoVoid::CallableName())) {
+ TYqlRowSpecInfo::TPtr rowSpec = TYtTableBaseInfo::GetRowSpec(TExprBase(node.ChildPtr(TYtPath::idx_Table)));
+ if (!TYtRangesInfo::Validate(*node.Child(TYtPath::idx_Ranges), ctx, rowSpec)) {
+ return false;
+ }
+ }
+
+ if (!node.Child(TYtPath::idx_Stat)->IsCallable(TYtStat::CallableName())
+ && !node.Child(TYtPath::idx_Stat)->IsCallable(TCoVoid::CallableName())) {
+
+ ctx.AddError(TIssue(ctx.GetPosition(node.Child(TYtPath::idx_Stat)->Pos()), TStringBuilder()
+ << "Expected " << TYtStat::CallableName()
+ << " or Void"));
+ return false;
+ }
+
+ return true;
+}
+
+void TYtPathInfo::Parse(TExprBase node) {
+ *this = {};
+ FromNode = node.Maybe<TExprBase>();
+ TYtPath path = node.Cast<TYtPath>();
+ Table = TYtTableBaseInfo::Parse(path.Table());
+
+ if (path.Columns().Maybe<TExprList>()) {
+ Columns = MakeIntrusive<TYtColumnsInfo>(path.Columns());
+ }
+
+ if (path.Ranges().Maybe<TExprList>()) {
+ Ranges = MakeIntrusive<TYtRangesInfo>(path.Ranges());
+ }
+
+ if (path.Stat().Maybe<TYtStat>()) {
+ Stat = MakeIntrusive<TYtTableStatInfo>(path.Stat().Ptr());
+ }
+}
+
+TExprBase TYtPathInfo::ToExprNode(TExprContext& ctx, const TPositionHandle& pos, NNodes::TExprBase table) const {
+ auto pathBuilder = Build<TYtPath>(ctx, pos);
+ pathBuilder.Table(table);
+ if (Columns) {
+ pathBuilder.Columns(Columns->ToExprNode(ctx, pos));
+ } else {
+ pathBuilder.Columns<TCoVoid>().Build();
+ }
+ if (Ranges) {
+ pathBuilder.Ranges(Ranges->ToExprNode(ctx, pos));
+ } else {
+ pathBuilder.Ranges<TCoVoid>().Build();
+ }
+ if (Stat) {
+ pathBuilder.Stat(Stat->ToExprNode(ctx, pos));
+ } else {
+ pathBuilder.Stat<TCoVoid>().Build();
+ }
+
+ return pathBuilder.Done();
+}
+
+void TYtPathInfo::FillRichYPath(NYT::TRichYPath& path) const {
+ if (Columns) {
+ // Should have the same criteria as in TYtPathInfo::GetCodecSpecNode()
+ bool useAllColumns = !Table->RowSpec; // Always use all columns for YAMR format
+ if (Table->RowSpec && !Table->RowSpec->StrictSchema && Columns->HasOthers()) {
+ useAllColumns = true;
+ }
+
+ Columns->FillRichYPath(path, /* withColumns = */ !useAllColumns);
+ }
+ if (Ranges) {
+ YQL_ENSURE(Table);
+ YQL_ENSURE(Table->RowSpec);
+ Ranges->FillRichYPath(path, Table->RowSpec->SortedBy.size());
+ }
+}
+
+IGraphTransformer::TStatus TYtPathInfo::GetType(const TTypeAnnotationNode*& filtered, TExprNode::TPtr& newFields, TExprContext& ctx, const TPositionHandle& pos) const {
+ YQL_ENSURE(Table, "TYtPathInfo::Parse() must be called");
+
+ filtered = nullptr;
+ newFields = {};
+ const TStructExprType* inputType = Table->FromNode.Ref().GetTypeAnn()->Cast<TListExprType>()->GetItemType()->Cast<TStructExprType>();
+ if (!Columns) {
+ filtered = inputType;
+ return IGraphTransformer::TStatus::Ok;
+ } else {
+ auto& renames = Columns->GetRenames();
+ if (renames) {
+ auto inputTypeItems = inputType->GetItems();
+ for (auto& r : *renames) {
+ auto from = r.first;
+ auto to = r.second;
+
+ auto idx = inputType->FindItem(from);
+ if (!idx) {
+ ctx.AddError(TIssue(ctx.GetPosition(pos), TStringBuilder() << "Unknown original member: " << from));
+ return IGraphTransformer::TStatus::Error;
+ }
+ inputTypeItems[*idx] = ctx.MakeType<TItemExprType>(to, inputTypeItems[*idx]->GetItemType());
+ }
+ YQL_ENSURE(!inputTypeItems.empty());
+ inputType = ctx.MakeType<TStructExprType>(inputTypeItems);
+ }
+
+ auto& columns = Columns->GetColumns();
+ if (!columns) {
+ filtered = inputType;
+ return IGraphTransformer::TStatus::Ok;
+ }
+
+ THashSet<TString> auxColumns;
+ if (Table->RowSpec) {
+ for (auto& aux: Table->RowSpec->GetAuxColumns()) {
+ auxColumns.insert(aux.first);
+ }
+ }
+ bool hasMissingColumns = false;
+ THashSet<TString> fieldNames;
+ for (auto& item: *columns) {
+ if (inputType->FindItem(item.Name) || item.Type == "weak") {
+ fieldNames.insert(item.Name);
+ } else if (!auxColumns.contains(item.Name)) {
+ hasMissingColumns = true;
+ }
+ }
+
+ if (hasMissingColumns) {
+ newFields = Columns->ToExprNode(ctx, pos, &fieldNames).Ptr();
+ return IGraphTransformer::TStatus::Repeat;
+ }
+
+ TVector<const TItemExprType*> items;
+ for (auto& item : inputType->GetItems()) {
+ if (fieldNames.contains(item->GetName())) {
+ items.push_back(item);
+ }
+ }
+ auto weakType = ctx.MakeType<TOptionalExprType>(ctx.MakeType<TDataExprType>(EDataSlot::Yson));
+ for (auto& item: *columns) {
+ if (!inputType->FindItem(item.Name) && item.Type == "weak") {
+ items.push_back(ctx.MakeType<TItemExprType>(item.Name, weakType));
+ }
+ }
+
+ auto itemType = ctx.MakeType<TStructExprType>(items);
+ if (!itemType->Validate(pos, ctx)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+ filtered = itemType;
+ return IGraphTransformer::TStatus::Ok;
+ }
+}
+
+const NCommon::TStructMemberMapper& TYtPathInfo::GetColumnMapper() {
+ if (Mapper_) {
+ return *Mapper_;
+ }
+ THashSet<TStringBuf> filterColumns;
+ THashMap<TString, TString> renames;
+ if (Columns && Columns->GetRenames()) {
+ renames = *Columns->GetRenames();
+ }
+
+ bool useAllColumns = true; // Should have the same criteria as in TYtPathInfo::FillRichYPath()
+ if (HasColumns()) {
+ useAllColumns = !Table->RowSpec; // Always use all columns for YAMR format
+ if (Table->RowSpec && !Table->RowSpec->StrictSchema && Columns->HasOthers()) {
+ useAllColumns = true;
+ }
+
+ if (!useAllColumns) {
+ for (auto& c: *Columns->GetColumns()) {
+ filterColumns.insert(c.Name);
+ }
+ for (auto& c: Table->RowSpec->SortMembers) {
+ auto column = c;
+ if (auto r = renames.find(c); r != renames.cend()) {
+ column = r->second;
+ }
+ filterColumns.insert(column);
+ }
+ }
+ }
+ if (useAllColumns && renames.empty()) {
+ Mapper_.ConstructInPlace();
+ return *Mapper_;
+ }
+
+ Mapper_ = [renames, filterColumns, useAllColumns, strict = Table->RowSpec->StrictSchema](TStringBuf name) -> TMaybe<TStringBuf> {
+ if (auto r = renames.find(name); r != renames.cend()) {
+ name = r->second;
+ }
+ if (strict && !useAllColumns && !filterColumns.contains(name)) {
+ return Nothing();
+ }
+ if (!strict && name == YqlOthersColumnName) {
+ return Nothing();
+ }
+ return MakeMaybe(name);
+ };
+ return *Mapper_;
+}
+
+NYT::TNode TYtPathInfo::GetCodecSpecNode() {
+ auto specNode = Table->GetCodecSpecNode(GetColumnMapper());
+ if (Table->RowSpec && !Table->RowSpec->StrictSchema && HasColumns()) {
+ auto list = NYT::TNode::CreateList();
+ for (auto& col: *Columns->GetColumns()) {
+ if (col.Type == "weak") {
+ list.Add(col.Name);
+ }
+ }
+ if (!list.AsList().empty()) {
+ specNode[YqlRowSpecAttribute][RowSpecAttrWeakFields] = list;
+ }
+ }
+
+ return specNode;
+}
+
+bool TYtPathInfo::RequiresRemap() const {
+ if (!Table->RequiresRemap()) {
+ return false;
+ }
+ const auto meta = Table->Meta;
+ const auto rowSpec = Table->RowSpec;
+ if (!meta->Attrs.contains(QB2Premapper) && rowSpec && rowSpec->DefaultValues.empty() && !rowSpec->StrictSchema) {
+ if (HasColumns() && !Columns->HasOthers()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+TString TYtPathInfo::GetCodecSpecStr() {
+ return NYT::NodeToCanonicalYsonString(GetCodecSpecNode(), NYson::EYsonFormat::Text);
+}
+
+ui64 TYtPathInfo::GetNativeYtTypeFlags() {
+ return Table->RowSpec ? Table->RowSpec->GetNativeYtTypeFlags(GetColumnMapper()) : 0;
+}
+
+TMaybe<NYT::TNode> TYtPathInfo::GetNativeYtType() {
+ return Table->RowSpec ? Table->RowSpec->GetNativeYtType(GetColumnMapper()) : Nothing();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+} // NYql
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_table.h b/ydb/library/yql/providers/yt/provider/yql_yt_table.h
new file mode 100644
index 0000000000..41b5b1aee9
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table.h
@@ -0,0 +1,352 @@
+#pragma once
+
+#include "yql_yt_op_settings.h"
+
+#include <ydb/library/yql/providers/yt/lib/row_spec/yql_row_spec.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+#include <ydb/library/yql/providers/common/schema/expr/yql_expr_schema.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/expr_nodes_gen/yql_expr_nodes_gen.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <yt/cpp/mapreduce/interface/node.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/vector.h>
+#include <util/generic/variant.h>
+#include <util/generic/maybe.h>
+#include <util/generic/hash_set.h>
+#include <util/string/cast.h>
+
+#include <utility>
+
+namespace NYql {
+
+class TYtKey;
+
+struct TYtTableStatInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtTableStatInfo>;
+
+ TYtTableStatInfo() {
+ }
+
+ TYtTableStatInfo(NNodes::TExprBase node) {
+ Parse(node);
+ }
+ TYtTableStatInfo(TExprNode::TPtr node)
+ : TYtTableStatInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const;
+
+ bool IsEmpty() const {
+ return 0 == RecordsCount;
+ }
+
+ NNodes::TMaybeNode<NNodes::TExprBase> FromNode;
+ TString Id;
+ ui64 RecordsCount = 0;
+ ui64 DataSize = 0;
+ ui64 ChunkCount = 0;
+ ui64 ModifyTime = 0;
+ ui64 Revision = 0;
+ ui64 TableRevision = 0; // Not serializable
+};
+
+struct TYtTableMetaInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtTableMetaInfo>;
+
+ TYtTableMetaInfo() {
+ }
+ TYtTableMetaInfo(NNodes::TExprBase node) {
+ Parse(node);
+ }
+ TYtTableMetaInfo(TExprNode::TPtr node)
+ : TYtTableMetaInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const;
+
+ NNodes::TMaybeNode<NNodes::TExprBase> FromNode;
+ bool CanWrite = true;
+ bool DoesExist = false;
+ bool YqlCompatibleScheme = false;
+ bool InferredScheme = false;
+ bool IsDynamic = false;
+ TString SqlView;
+ ui16 SqlViewSyntaxVersion = 0;
+
+ THashMap<TString, TString> Attrs;
+};
+
+struct TEpochInfo {
+ static bool Validate(const TExprNode& node, TExprContext& ctx);
+ static TMaybe<ui32> Parse(const TExprNode& node);
+ static NNodes::TExprBase ToExprNode(const TMaybe<ui32>& epoch, TExprContext& ctx, const TPositionHandle& pos);
+};
+
+struct TYtTableBaseInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtTableBaseInfo>;
+
+ TYtTableBaseInfo() = default;
+ TYtTableBaseInfo(const TYtKey& key, TStringBuf cluster);
+
+ virtual NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const = 0;
+
+ NYT::TNode GetCodecSpecNode(const NCommon::TStructMemberMapper& mapper = {}) const;
+ NYT::TNode GetAttrSpecNode(ui64 nativeTypeCompatibility, bool rowSpecCompactForm) const;
+
+ static TPtr Parse(NNodes::TExprBase node);
+ static TYtTableMetaInfo::TPtr GetMeta(NNodes::TExprBase node);
+ static TYqlRowSpecInfo::TPtr GetRowSpec(NNodes::TExprBase node);
+ static TStringBuf GetTableName(NNodes::TExprBase node);
+
+ bool RequiresRemap() const;
+ bool HasSameScheme(const TTypeAnnotationNode& scheme) const;
+ bool HasSamePhysicalScheme(const TYtTableBaseInfo& info) const;
+
+ NNodes::TMaybeNode<NNodes::TExprBase> FromNode;
+ TString Name;
+ TString Cluster;
+ TYqlRowSpecInfo::TPtr RowSpec;
+ TYtTableMetaInfo::TPtr Meta;
+ TYtTableStatInfo::TPtr Stat;
+ NNodes::TMaybeNode<NNodes::TExprBase> Settings;
+ bool IsTemp = false;
+ bool IsAnonymous = false;
+ bool IsUnordered = false;
+ TMaybe<ui32> Epoch; // Epoch, from which the table is read
+ TMaybe<ui32> CommitEpoch; // Epoch, in which the table modifications became accessible
+};
+
+struct TYtTableInfo: public TYtTableBaseInfo {
+ using TPtr = TIntrusivePtr<TYtTableInfo>;
+
+ TYtTableInfo() = default;
+ TYtTableInfo(const TYtKey& key, TStringBuf cluster);
+ TYtTableInfo(NNodes::TExprBase node, bool useTypes = true) {
+ Parse(node, useTypes);
+ }
+ TYtTableInfo(TExprNode::TPtr node)
+ : TYtTableInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static TStringBuf GetTableLabel(NNodes::TExprBase node);
+ static bool HasSubstAnonymousLabel(NNodes::TExprBase node);
+
+ static bool Validate(const TExprNode& node, EYtSettingTypes accepted, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node, bool useTypes = true);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const override;
+};
+
+struct TYtOutTableInfo: public TYtTableBaseInfo {
+ using TPtr = TIntrusivePtr<TYtOutTableInfo>;
+
+ TYtOutTableInfo() {
+ IsTemp = true;
+ }
+ TYtOutTableInfo(const TStructExprType* type, ui64 nativeYtTypeFlags);
+ TYtOutTableInfo(NNodes::TExprBase node) {
+ Parse(node);
+ IsTemp = true;
+ }
+ TYtOutTableInfo(TExprNode::TPtr node)
+ : TYtOutTableInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const override;
+
+ TYtOutTableInfo& SetUnique(const TDistinctConstraintNode* distinct, const TPositionHandle& pos, TExprContext& ctx);
+};
+
+struct TYtRangesInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtRangesInfo>;
+
+ struct TRowSingle {
+ ui64 Offset = Max<ui64>();
+ };
+ struct TRowRange {
+ TMaybe<ui64> Lower;
+ TMaybe<ui64> Upper;
+ };
+ struct TKeySingle {
+ TVector<NNodes::TExprBase> Key;
+ };
+ struct TKeyRange {
+ TVector<NNodes::TExprBase> Lower;
+ bool LowerInclude = true;
+ TVector<NNodes::TExprBase> Upper;
+ bool UpperInclude = false;
+ bool UseKeyBoundApi = DEFAULT_USE_KEY_BOUND_API;
+ };
+ using TRange = std::variant<TRowSingle, TRowRange, TKeySingle, TKeyRange>;
+
+ TYtRangesInfo() {
+ }
+ TYtRangesInfo(NNodes::TExprBase node) {
+ Parse(node);
+ }
+ TYtRangesInfo(TExprNode::TPtr node)
+ : TYtRangesInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx, const TYqlRowSpecInfo::TPtr& rowSpec = {});
+ void Parse(NNodes::TExprBase node);
+ void Parse(const TVector<NYT::TReadRange>& ranges, TExprContext& ctx, const TPositionHandle& pos = {});
+ void AddRowRange(const TRowRange& range);
+ void SetUseKeyBoundApi(bool useKeyBoundApi);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos) const;
+ void FillRichYPath(NYT::TRichYPath& path, size_t keyColumnsCount) const;
+ TMaybe<ui64> GetUsedRows(ui64 tableRowCount) const;
+ size_t GetRangesCount() const;
+ size_t GetUsedKeyPrefixLength() const;
+ bool IsEmpty() const;
+ TVector<TRange> GetRanges() const;
+ static TYtRangesInfo::TPtr ApplyLegacyKeyFilters(const TVector<NNodes::TExprBase>& keyFilters,
+ const TYqlRowSpecInfo::TPtr& rowSpec, TExprContext& ctx);
+ static TYtRangesInfo::TPtr ApplyKeyFilter(const TExprNode& keyFilter);
+ static TYtRangesInfo::TPtr MakeEmptyRange();
+
+private:
+ TVector<TRange> Ranges;
+};
+
+struct TYtColumnsInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtColumnsInfo>;
+
+ struct TColumn {
+ TString Name;
+ TString Type;
+ };
+
+ TYtColumnsInfo() = default;
+
+ TYtColumnsInfo(NNodes::TExprBase node)
+ {
+ Parse(node);
+ }
+
+ TYtColumnsInfo(TExprNode::TPtr node)
+ : TYtColumnsInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(TExprNode& node, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node);
+
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos, const THashSet<TString>* filter = nullptr) const;
+
+ bool HasOthers() const {
+ return Others;
+ }
+
+ void FillRichYPath(NYT::TRichYPath& path, bool withColumns) const;
+
+ bool HasColumns() const {
+ return Columns.Defined();
+ }
+
+ template <class TContainer>
+ void SetColumns(const TContainer& columns) {
+ Columns.ConstructInPlace();
+ Others = false;
+ for (auto& col: columns) {
+ Columns->emplace_back(TColumn{::ToString(col), TString()});
+ UpdateOthers(col);
+ }
+ }
+
+ void SetRenames(const THashMap<TString, TString>& renames) {
+ YQL_ENSURE(!Renames.Defined());
+ if (HasColumns()) {
+ for (auto& col : *Columns) {
+ auto r = renames.find(col.Name);
+ if (r != renames.end()) {
+ col.Name = r->second;
+ }
+ }
+ }
+ Renames.ConstructInPlace(renames);
+ }
+
+ const TMaybe<TVector<TColumn>>& GetColumns() const {
+ return Columns;
+ };
+
+ const TMaybe<THashMap<TString, TString>>& GetRenames() const {
+ return Renames;
+ };
+
+private:
+ void UpdateOthers(TStringBuf col);
+
+ TMaybe<THashMap<TString, TString>> Renames;
+ TMaybe<TVector<TColumn>> Columns;
+ bool Others = false;
+};
+
+// TODO: 1) additional fields, which missing in original table 2) field type converting
+struct TYtPathInfo: public TThrRefBase {
+ using TPtr = TIntrusivePtr<TYtPathInfo>;
+
+ TYtPathInfo() {
+ }
+ TYtPathInfo(NNodes::TExprBase node) {
+ Parse(node);
+ }
+ TYtPathInfo(TExprNode::TPtr node)
+ : TYtPathInfo(NNodes::TExprBase(node))
+ {
+ }
+
+ static bool Validate(const TExprNode& node, TExprContext& ctx);
+ void Parse(NNodes::TExprBase node);
+ NNodes::TExprBase ToExprNode(TExprContext& ctx, const TPositionHandle& pos, NNodes::TExprBase table) const;
+ void FillRichYPath(NYT::TRichYPath& path) const;
+ IGraphTransformer::TStatus GetType(const TTypeAnnotationNode*& filtered, TExprNode::TPtr& newFields,
+ TExprContext& ctx, const TPositionHandle& pos) const;
+
+ bool HasColumns() const {
+ return Columns && Columns->HasColumns();
+ }
+
+ template <class TContainer>
+ void SetColumns(const TContainer& columns) {
+ if (!Columns) {
+ Columns = MakeIntrusive<TYtColumnsInfo>();
+ }
+ Columns->SetColumns(columns);
+ }
+
+ NYT::TNode GetCodecSpecNode();
+ TString GetCodecSpecStr();
+ bool RequiresRemap() const;
+ ui64 GetNativeYtTypeFlags();
+ TMaybe<NYT::TNode> GetNativeYtType();
+
+ NNodes::TMaybeNode<NNodes::TExprBase> FromNode;
+ TYtTableBaseInfo::TPtr Table;
+ TYtColumnsInfo::TPtr Columns;
+ TYtRangesInfo::TPtr Ranges;
+ TYtTableStatInfo::TPtr Stat;
+private:
+ const NCommon::TStructMemberMapper& GetColumnMapper();
+
+ TMaybe<NCommon::TStructMemberMapper> Mapper_;
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
new file mode 100644
index 0000000000..3c72587a11
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.cpp
@@ -0,0 +1,366 @@
+#include "yql_yt_table_desc.h"
+
+#include <ydb/library/yql/providers/yt/common/yql_names.h>
+#include <ydb/library/yql/core/issue/protos/issue_id.pb.h>
+#include <ydb/library/yql/core/yql_expr_optimize.h>
+#include <ydb/library/yql/core/yql_expr_type_annotation.h>
+#include <ydb/library/yql/core/issue/yql_issue.h>
+#include <ydb/library/yql/sql/sql.h>
+#include <ydb/library/yql/utils/yql_panic.h>
+
+namespace NYql {
+
+namespace {
+
+const TString RAW_VIEW_SQL = "select * from self_raw";
+
+TExprNode::TPtr BuildProtoRemapper(const TMap<TString, TString>& protoFields, TExprContext& ctx) {
+ auto rowArg = ctx.NewArgument(TPosition(), TStringBuf("row"));
+ auto result = rowArg;
+ for (auto& x : protoFields) {
+ result = ctx.Builder(TPositionHandle())
+ .Callable("ReplaceMember")
+ .Add(0, result)
+ .Atom(1, x.first)
+ .Callable(2, "Apply")
+ .Callable(0, "Udf")
+ .Atom(0, "Protobuf.Parse")
+ .Callable(1, "Void")
+ .Seal()
+ .Callable(2, "Void")
+ .Seal()
+ .Atom(3, x.second)
+ .Seal()
+ .Callable(1, "Member")
+ .Add(0, result)
+ .Atom(1, x.first)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+
+ return ctx.NewLambda(TPosition(),
+ ctx.NewArguments(TPosition(), { rowArg }), std::move(result));
+}
+
+TExprNode::TPtr BuildUdfRemapper(const THashMap<TString, TString>& metaAttrs, TExprContext& ctx) {
+ return ctx.Builder(TPositionHandle())
+ .Lambda()
+ .Param(TStringBuf("row"))
+ .Callable(TStringBuf("Apply"))
+ .Callable(0, TStringBuf("Udf"))
+ .Atom(0, metaAttrs.at(YqlReadUdfAttribute))
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ if (auto runConfigValue = metaAttrs.FindPtr(YqlReadUdfRunConfigAttribute)) {
+ parent.Callable(1, TStringBuf("String"))
+ .Atom(0, *runConfigValue)
+ .Seal();
+ } else {
+ parent.Callable(1, TStringBuf("Void"))
+ .Seal();
+ }
+ return parent;
+ })
+ .Callable(2, TStringBuf("Void")) // User type
+ .Seal()
+ .Atom(3, metaAttrs.Value(YqlReadUdfTypeConfigAttribute, TString()))
+ .Seal()
+ .Arg(1, TStringBuf("row"))
+ .Seal()
+ .Seal()
+ .Build();
+}
+
+TExprNode::TPtr BuildIgnoreTypeV3Remapper(const TStructExprType* rowType, TExprContext& ctx) {
+ auto rowArg = ctx.NewArgument(TPosition(), TStringBuf("row"));
+ auto result = rowArg;
+ for (const TItemExprType* itemType : rowType->GetItems()) {
+ auto untaggedType = itemType->GetItemType();
+ while (untaggedType->GetKind() == ETypeAnnotationKind::Tagged) {
+ untaggedType = untaggedType->Cast<TTaggedExprType>()->GetBaseType();
+ }
+
+ auto unwrappedType = untaggedType;
+ if (unwrappedType->GetKind() == ETypeAnnotationKind::Optional) {
+ unwrappedType = unwrappedType->Cast<TOptionalExprType>()->GetItemType();
+ }
+
+ if (unwrappedType->GetKind() != ETypeAnnotationKind::Data) {
+
+ auto argumentsType = ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{untaggedType});
+
+ auto udfArgumentsType = ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{
+ argumentsType,
+ ctx.MakeType<TStructExprType>(TVector<const TItemExprType*>{}),
+ ctx.MakeType<TTupleExprType>(TTypeAnnotationNode::TListType{})
+ });
+
+ auto member = ctx.Builder(TPositionHandle())
+ .Callable("Member")
+ .Add(0, result)
+ .Atom(1, itemType->GetName())
+ .Seal()
+ .Build();
+
+ for (auto type = itemType->GetItemType(); type->GetKind() == ETypeAnnotationKind::Tagged; type = type->Cast<TTaggedExprType>()->GetBaseType()) {
+ member = ctx.Builder(TPositionHandle())
+ .Callable("Untag")
+ .Add(0, member)
+ .Atom(1, type->Cast<TTaggedExprType>()->GetTag())
+ .Seal()
+ .Build();
+ }
+
+ result = ctx.Builder(TPositionHandle())
+ .Callable("ReplaceMember")
+ .Add(0, result)
+ .Atom(1, itemType->GetName())
+ .Callable(2, "Apply")
+ .Callable(0, "Udf")
+ .Atom(0, "Yson2.Serialize")
+ .Callable(1, "Void")
+ .Seal()
+ .Callable(2, "Void")
+ .Seal()
+ .Seal()
+ .Callable(1, "Apply")
+ .Callable(0, "Udf")
+ .Atom(0, "Yson2.From")
+ .Callable(1, "Void")
+ .Seal()
+ .Add(2, ExpandType(TPositionHandle(), *udfArgumentsType, ctx))
+ .Seal()
+ .Add(1, member)
+ .Seal()
+ .Seal()
+ .Seal()
+ .Build();
+ }
+ }
+
+ if (result == rowArg) {
+ // No items to remap
+ return {};
+ }
+ return ctx.NewLambda(TPosition(),
+ ctx.NewArguments(TPosition(), { rowArg }), std::move(result));
+}
+
+TExprNode::TPtr CompileViewSql(const TString& provider, const TString& cluster, const TString& sql, ui16 syntaxVersion,
+ TExprContext& ctx, IModuleResolver* moduleResolver)
+{
+ NSQLTranslation::TTranslationSettings settings;
+ settings.Mode = NSQLTranslation::ESqlMode::LIMITED_VIEW;
+ settings.DefaultCluster = cluster.empty() ? "view" : cluster;
+ settings.ClusterMapping[settings.DefaultCluster] = cluster.empty() ? "data" : provider;
+ settings.SyntaxVersion = syntaxVersion;
+ settings.V0Behavior = NSQLTranslation::EV0Behavior::Disable;
+ NYql::TAstParseResult sqlRes = NSQLTranslation::SqlToYql(sql, settings);
+ ctx.IssueManager.RaiseIssues(sqlRes.Issues);
+ if (!sqlRes.IsOk()) {
+ return {};
+ }
+
+ TExprNode::TPtr exprRoot;
+ if (!CompileExpr(*sqlRes.Root, exprRoot, ctx, moduleResolver, false, Max<ui32>(), syntaxVersion)) {
+ return {};
+ }
+
+ bool hasError = false;
+ VisitExpr(*exprRoot, [&](const TExprNode& node) {
+ if (node.IsCallable("SecureParam")) {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), "SecureParam function can't be used in views"));
+ return false;
+ } else if (node.IsCallable("FuncCode") && node.ChildrenSize() > 0) {
+ if (!node.Head().IsCallable("String")) {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), "FuncCode should have constant function name in views"));
+ return false;
+ }
+
+ if (node.Head().Head().IsAtom() && node.Head().Head().Content() == "SecureParam") {
+ hasError = true;
+ ctx.AddError(TIssue(ctx.GetPosition(node.Pos()), "SecureParam function can't be used in views"));
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ if (hasError) {
+ return {};
+ }
+
+ return exprRoot;
+}
+
+} // unnamed
+
+
+bool TYtViewDescription::Fill(const TString& provider, const TString& cluster, const TString& sql, ui16 syntaxVersion, TExprContext& ctx,
+ IModuleResolver* moduleResolver)
+{
+ Sql = sql;
+ CompiledSql = CompileViewSql(provider, cluster, sql, syntaxVersion, ctx, moduleResolver);
+ return bool(CompiledSql);
+}
+
+void TYtViewDescription::CleanupCompiledSQL()
+{
+ CompiledSql.Reset();
+}
+
+bool TYtTableDescriptionBase::Fill(const TString& provider, const TString& cluster, const TString& table,
+ const TStructExprType* type, const TString& viewSql, ui16 syntaxVersion, const THashMap<TString, TString>& metaAttrs,
+ TExprContext& ctx, IModuleResolver* moduleResolver)
+{
+ // (1) row type
+ RawRowType = type;
+ YQL_ENSURE(RawRowType && RawRowType->GetKind() == ETypeAnnotationKind::Struct);
+ RowType = RawRowType;
+
+ bool onlyRawView = false;
+ if (TYtTableIntent::View == Intents) {
+ for (auto& view : Views) {
+ if (view.first == TStringBuf("raw")) {
+ onlyRawView = true;
+ } else {
+ onlyRawView = false;
+ break;
+ }
+ }
+ }
+
+ // (2) UDF remapper / proto fields
+ if (!onlyRawView) {
+ for (auto& x: metaAttrs) {
+ if (x.first.StartsWith(YqlProtoFieldPrefixAttribute)) {
+ auto fieldName = x.first.substr(YqlProtoFieldPrefixAttribute.size());
+ if (fieldName.empty()) {
+ ctx.AddError(TIssue(TPosition(),
+ TStringBuilder() << "Proto field name should not be empty. Table: "
+ << cluster << '.' << table));
+ return false;
+ }
+ if (type->FindItem(fieldName)) { // ignore nonexisting fields
+ ProtoFields.insert({fieldName, x.second});
+ } else {
+ ctx.AddWarning(YqlIssue(TPosition(), EYqlIssueCode::TIssuesIds_EIssueCode_YT_MISSING_PROTO_FIELD,
+ TStringBuilder() << "Proto field name for missing column " << fieldName << ". Table: "
+ << cluster << '.' << table));
+ }
+ }
+ }
+
+ const TString* udfName = metaAttrs.FindPtr(YqlReadUdfAttribute);
+ if (udfName && !ProtoFields.empty()) {
+ ctx.AddError(TIssue(TPosition(),
+ TStringBuilder() << "UDF remapper and proto fields cannot be declared simultaneously. Table: "
+ << cluster << '.' << table));
+ return false;
+ }
+
+ if (IgnoreTypeV3) {
+ UdfApplyLambda = BuildIgnoreTypeV3Remapper(RawRowType->Cast<TStructExprType>(), ctx);
+ }
+ TExprNode::TPtr lambda;
+ if (udfName) {
+ lambda = BuildUdfRemapper(metaAttrs, ctx);
+ } else if (!ProtoFields.empty()) {
+ lambda = BuildProtoRemapper(ProtoFields, ctx);
+ }
+ if (lambda) {
+ if (UdfApplyLambda) {
+ UdfApplyLambda = ctx.Builder(TPositionHandle())
+ .Lambda()
+ .Param("row")
+ .Apply(lambda)
+ .With(0)
+ .Apply(UdfApplyLambda)
+ .With(0, "row")
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal()
+ .Build();
+ } else {
+ UdfApplyLambda = lambda;
+ }
+ }
+ HasUdfApply = (bool)UdfApplyLambda;
+ }
+
+ // (3) views
+ if (!FillViews(provider, cluster, table, metaAttrs, ctx, moduleResolver)) {
+ return false;
+ }
+
+ if (viewSql) {
+ if (!View) {
+ if (!View.ConstructInPlace().Fill(provider, cluster, viewSql, syntaxVersion, ctx, moduleResolver)) {
+ ctx.AddError(TIssue(TPosition(),
+ TStringBuilder() << "Can't load sql view, table: " << cluster << '.' << table));
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool TYtTableDescriptionBase::FillViews(const TString& provider, const TString& cluster, const TString& table,
+ const THashMap<TString, TString>& metaAttrs, TExprContext& ctx, IModuleResolver* moduleResolver)
+{
+ for (auto& view: Views) {
+ TYtViewDescription& viewDesc = view.second;
+
+ if (!viewDesc.CompiledSql) {
+ TString viewSql;
+ ui16 syntaxVersion = 0;
+ if (view.first == "raw") {
+ viewSql = RAW_VIEW_SQL;
+ syntaxVersion = 1;
+ } else {
+ auto sql = metaAttrs.FindPtr(YqlViewPrefixAttribute + view.first);
+ if (!sql) {
+ ctx.AddError(TIssue(TPosition(),
+ TStringBuilder() << "View " << view.first
+ << " not found in table " << cluster << '.' << table
+ << " metadata").SetCode(TIssuesIds::YT_VIEW_NOT_FOUND, TSeverityIds::S_ERROR));
+ return false;
+ }
+
+ viewSql = *sql;
+ auto sqlSyntaxVersion = metaAttrs.FindPtr("_yql_syntax_version_" + view.first);
+ if (sqlSyntaxVersion) {
+ syntaxVersion = FromString<ui16>(*sqlSyntaxVersion);
+ }
+ }
+
+ if (!viewDesc.Fill(provider, cluster, viewSql, syntaxVersion, ctx, moduleResolver)) {
+ ctx.AddError(TIssue(TPosition(),
+ TStringBuilder() << "Can't load sql view " << viewSql.Quote()
+ << ", table: " << cluster << '.' << table
+ << ", view: " << view.first));
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void TYtTableDescriptionBase::CleanupCompiledSQL()
+{
+ UdfApplyLambda.Reset();
+ for (auto& view : Views) {
+ view.second.CleanupCompiledSQL();
+ }
+ if (View) {
+ View->CleanupCompiledSQL();
+ }
+}
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h
new file mode 100644
index 0000000000..85df388d27
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_table_desc.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <ydb/library/yql/ast/yql_expr.h>
+
+#include <util/system/types.h>
+#include <util/generic/flags.h>
+#include <util/generic/string.h>
+#include <util/generic/map.h>
+#include <util/generic/maybe.h>
+#include <util/generic/hash.h>
+#include <util/generic/ptr.h>
+
+#include <utility>
+
+namespace NYql {
+
+enum class TYtTableIntent: ui32 {
+ Read = 1 << 0,
+ View = 1 << 1, // Read via view
+ Override = 1 << 2,
+ Append = 1 << 3,
+ Create = 1 << 4, // Reserved. Not implemented yet
+ Drop = 1 << 5,
+ Flush = 1 << 6, // Untransactional write
+};
+
+Y_DECLARE_FLAGS(TYtTableIntents, TYtTableIntent);
+Y_DECLARE_OPERATORS_FOR_FLAGS(TYtTableIntents);
+
+inline bool HasReadIntents(TYtTableIntents intents) {
+ return intents & (TYtTableIntent::Read | TYtTableIntent::View);
+}
+
+inline bool HasModifyIntents(TYtTableIntents intents) {
+ return intents & (TYtTableIntent::Override | TYtTableIntent::Append | TYtTableIntent::Drop | TYtTableIntent::Flush);
+}
+
+inline bool HasExclusiveModifyIntents(TYtTableIntents intents) {
+ return intents & (TYtTableIntent::Override | TYtTableIntent::Drop | TYtTableIntent::Flush);
+}
+
+struct TYtViewDescription {
+ TString Sql;
+ ui16 SyntaxVersion = 0;
+ TExprNode::TPtr CompiledSql; // contains Read! to self/self_raw tables
+ const TTypeAnnotationNode* RowType = nullptr; // Filled only if scheme requested
+
+ bool Fill(const TString& provider, const TString& cluster, const TString& sql, ui16 syntaxVersion, TExprContext& ctx,
+ IModuleResolver* moduleResolver);
+ void CleanupCompiledSQL();
+};
+
+struct TYtTableDescriptionBase {
+ const TTypeAnnotationNode* RawRowType = nullptr;
+ const TTypeAnnotationNode* RowType = nullptr; // may be customized by UDF if scheme requested
+ TExprNode::TPtr UdfApplyLambda; // convert table row by UDF
+ bool HasUdfApply = false;
+ TMap<TString, TYtViewDescription> Views;
+ TMaybe<TYtViewDescription> View;
+ TMap<TString, TString> ProtoFields;
+ TYtTableIntents Intents;
+ ui32 InferSchemaRows = 0;
+ bool ForceInferSchema = false;
+ bool FailOnInvalidSchema = true;
+ bool HasWriteLock = false;
+ bool IgnoreTypeV3 = false;
+
+ bool Fill(const TString& provider, const TString& cluster, const TString& table, const TStructExprType* type,
+ const TString& viewSql, ui16 syntaxVersion, const THashMap<TString, TString>& metaAttrs, TExprContext& ctx,
+ IModuleResolver* moduleResolver);
+ void CleanupCompiledSQL();
+ bool FillViews(const TString& provider, const TString& cluster, const TString& table, const THashMap<TString, TString>& metaAttrs,
+ TExprContext& ctx, IModuleResolver* moduleResolver);
+};
+
+}
diff --git a/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp b/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
new file mode 100644
index 0000000000..6ae1d3c988
--- /dev/null
+++ b/ydb/library/yql/providers/yt/provider/yql_yt_wide_flow.cpp
@@ -0,0 +1,256 @@
+#include "yql_yt_provider_impl.h"
+
+#include <ydb/library/yql/providers/yt/expr_nodes/yql_yt_expr_nodes.h>
+#include <ydb/library/yql/ast/yql_expr.h>
+#include <ydb/library/yql/core/yql_graph_transformer.h>
+#include <ydb/library/yql/core/yql_opt_utils.h>
+#include <ydb/library/yql/providers/common/transform/yql_optimize.h>
+#include <ydb/library/yql/providers/yt/common/yql_configuration.h>
+
+namespace NYql {
+
+using namespace NNodes;
+
+namespace {
+
+TExprNode::TPtr MakeWideLambda(const TExprNode& lambda, ui32 limit, TExprContext& ctx) {
+ if (lambda.IsCallable("Void"))
+ return {};
+
+ if (const auto inStructType = dynamic_cast<const TStructExprType*>(GetSeqItemType(lambda.Head().Head().GetTypeAnn())), outStructType = dynamic_cast<const TStructExprType*>(GetSeqItemType(lambda.GetTypeAnn()));
+ inStructType && outStructType && limit > std::max(inStructType->GetSize(), outStructType->GetSize()) && 0U < std::min(inStructType->GetSize(), outStructType->GetSize())) {
+
+ return ctx.Builder(lambda.Pos())
+ .Lambda()
+ .Param("wide")
+ .Callable("ExpandMap")
+ .Apply(0, lambda)
+ .With(0)
+ .Callable("NarrowMap")
+ .Arg(0, "wide")
+ .Lambda(1)
+ .Params("fields", inStructType->GetSize())
+ .Callable("AsStruct")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : inStructType->GetItems()) {
+ parent.List(i)
+ .Atom(0, item->GetName())
+ .Arg(1, "fields", i)
+ .Seal();
+ ++i;
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Lambda(1)
+ .Param("item")
+ .Do([&](TExprNodeBuilder& lambda) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : outStructType->GetItems()) {
+ lambda.Callable(i++, "Member")
+ .Arg(0, "item")
+ .Atom(1, item->GetName())
+ .Seal();
+ }
+ return lambda;
+ })
+ .Seal()
+ .Seal()
+ .Seal().Build();
+ } else if (inStructType && limit > inStructType->GetSize() && 0U < inStructType->GetSize()) {
+ return ctx.Builder(lambda.Pos())
+ .Lambda()
+ .Param("wide")
+ .Apply(lambda)
+ .With(0)
+ .Callable("NarrowMap")
+ .Arg(0, "wide")
+ .Lambda(1)
+ .Params("fields", inStructType->GetSize())
+ .Callable("AsStruct")
+ .Do([&](TExprNodeBuilder& parent) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : inStructType->GetItems()) {
+ parent.List(i)
+ .Atom(0, item->GetName())
+ .Arg(1, "fields", i)
+ .Seal();
+ ++i;
+ }
+ return parent;
+ })
+ .Seal()
+ .Seal()
+ .Seal()
+ .Done()
+ .Seal()
+ .Seal().Build();
+ } else if (outStructType && limit > outStructType->GetSize() && 0U < outStructType->GetSize()) {
+ return ctx.Builder(lambda.Pos())
+ .Lambda()
+ .Param("flow")
+ .Callable("ExpandMap")
+ .Apply(0, lambda)
+ .With(0, "flow")
+ .Seal()
+ .Lambda(1)
+ .Param("item")
+ .Do([&](TExprNodeBuilder& lambda) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : outStructType->GetItems()) {
+ lambda.Callable(i++, "Member")
+ .Arg(0, "item")
+ .Atom(1, item->GetName())
+ .Seal();
+ }
+ return lambda;
+ })
+ .Seal()
+ .Seal()
+ .Seal().Build();
+ }
+
+ return {};
+}
+
+TExprNode::TPtr MakeWideLambdaNoArg(const TExprNode& lambda, ui32 limit, TExprContext& ctx) {
+ if (const auto outStructType = dynamic_cast<const TStructExprType*>(GetSeqItemType(lambda.GetTypeAnn()));
+ outStructType && limit > outStructType->GetSize() && 0U < outStructType->GetSize()) {
+ return ctx.Builder(lambda.Pos())
+ .Lambda()
+ .Callable("ExpandMap")
+ .Add(0, lambda.TailPtr())
+ .Lambda(1)
+ .Param("item")
+ .Do([&](TExprNodeBuilder& lambda) -> TExprNodeBuilder& {
+ ui32 i = 0U;
+ for (const auto& item : outStructType->GetItems()) {
+ lambda.Callable(i++, "Member")
+ .Arg(0, "item")
+ .Atom(1, item->GetName())
+ .Seal();
+ }
+ return lambda;
+ })
+ .Seal()
+ .Seal()
+ .Seal().Build();
+ }
+
+ return {};
+}
+
+class TYtWideFlowTransformer : public TOptimizeTransformerBase {
+public:
+ TYtWideFlowTransformer(TYtState::TPtr state)
+ : TOptimizeTransformerBase(state ? state->Types : nullptr, NLog::EComponent::ProviderYt, {})
+ , Limit_(state ? state->Configuration->WideFlowLimit.Get().GetOrElse(DEFAULT_WIDE_FLOW_LIMIT) : DEFAULT_WIDE_FLOW_LIMIT)
+ {
+ if (Limit_) {
+#define HNDL(name) "WideFlow-"#name, Hndl(&TYtWideFlowTransformer::name)
+ AddHandler(0, &TYtFill::Match, HNDL(OptimizeFill));
+ AddHandler(0, &TYtMap::Match, HNDL(OptimizeMap));
+ AddHandler(0, &TYtReduce::Match, HNDL(OptimizeReduce));
+ AddHandler(0, &TYtMapReduce::Match, HNDL(OptimizeMapReduce));
+#undef HNDL
+ }
+ }
+
+ TMaybeNode<TExprBase> OptimizeFill(TExprBase node, TExprContext& ctx) {
+ if (const auto fill = node.Cast<TYtFill>(); auto wideContent = MakeWideLambdaNoArg(fill.Content().Ref(), Limit_, ctx)) {
+ if (auto settings = UpdateSettingValue(fill.Settings().Ref(), EYtSettingType::Flow, ctx.NewAtom(fill.Pos(), ToString(Limit_), TNodeFlags::Default), ctx)) {
+ const auto wideFill = Build<TYtFill>(ctx, fill.Pos())
+ .InitFrom(fill)
+ .Content(std::move(wideContent))
+ .Settings(std::move(settings))
+ .Done();
+ return wideFill.Ptr();
+ }
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> OptimizeMap(TExprBase node, TExprContext& ctx) {
+ if (const auto map = node.Cast<TYtMap>(); auto wideMapper = MakeWideLambda(map.Mapper().Ref(), Limit_, ctx)) {
+ if (auto settings = UpdateSettingValue(map.Settings().Ref(), EYtSettingType::Flow, ctx.NewAtom(map.Pos(), ToString(Limit_), TNodeFlags::Default), ctx)) {
+ const auto wideMap = Build<TYtMap>(ctx, map.Pos())
+ .InitFrom(map)
+ .Mapper(std::move(wideMapper))
+ .Settings(std::move(settings))
+ .Done();
+ return wideMap.Ptr();
+ }
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> OptimizeReduce(TExprBase node, TExprContext& ctx) {
+ if (const auto reduce = node.Cast<TYtReduce>(); auto wideReducer = MakeWideLambda(reduce.Reducer().Ref(), Limit_, ctx)) {
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, reduce.Pos()).InitFrom(reduce.Settings())
+ .Add()
+ .Name().Build(ToString(EYtSettingType::ReduceInputType))
+ .Value(TExprBase(ExpandType(reduce.Pos(), GetSeqItemType(*reduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()), ctx)))
+ .Build();
+
+ if (auto settings = UpdateSettingValue(settingsBuilder.Done().Ref(), EYtSettingType::Flow, ctx.NewAtom(reduce.Pos(), ToString(Limit_), TNodeFlags::Default), ctx)) {
+ const auto wideReduce = Build<TYtReduce>(ctx, reduce.Pos())
+ .InitFrom(reduce)
+ .Reducer(std::move(wideReducer))
+ .Settings(std::move(settings))
+ .Done();
+ return wideReduce.Ptr();
+ }
+ }
+
+ return node;
+ }
+
+ TMaybeNode<TExprBase> OptimizeMapReduce(TExprBase node, TExprContext& ctx) {
+ const auto mapReduce = node.Cast<TYtMapReduce>();
+ if (auto wideMapper = MakeWideLambda(mapReduce.Mapper().Ref(), Limit_, ctx), wideReducer = MakeWideLambda(mapReduce.Reducer().Ref(), Limit_, ctx); wideMapper || wideReducer) {
+
+ auto settingsBuilder = Build<TCoNameValueTupleList>(ctx, mapReduce.Pos()).InitFrom(mapReduce.Settings())
+ .Add()
+ .Name().Build(ToString(EYtSettingType::ReduceInputType))
+ .Value(TExprBase(ExpandType(mapReduce.Pos(), GetSeqItemType(*mapReduce.Reducer().Args().Arg(0).Ref().GetTypeAnn()), ctx)))
+ .Build();
+
+ if (wideMapper) {
+ settingsBuilder
+ .Add()
+ .Name().Build(ToString(EYtSettingType::MapOutputType))
+ .Value(TExprBase(ExpandType(mapReduce.Pos(), GetSeqItemType(*mapReduce.Mapper().Ref().GetTypeAnn()), ctx)))
+ .Build();
+ }
+
+ if (auto settings = UpdateSettingValue(settingsBuilder.Done().Ref(), EYtSettingType::Flow, ctx.NewAtom(mapReduce.Pos(), ToString(Limit_), TNodeFlags::Default), ctx)) {
+ const auto wideMapReduce = Build<TYtMapReduce>(ctx, mapReduce.Pos())
+ .InitFrom(mapReduce)
+ .Mapper(wideMapper ? std::move(wideMapper) : mapReduce.Mapper().Ptr())
+ .Reducer(wideReducer ? std::move(wideReducer) : mapReduce.Reducer().Ptr())
+ .Settings(std::move(settings))
+ .Done();
+ return wideMapReduce.Ptr();
+ }
+ }
+
+ return node;
+ }
+private:
+ const ui16 Limit_;
+};
+
+}
+
+THolder<IGraphTransformer> CreateTYtWideFlowTransformer(TYtState::TPtr state) {
+ return MakeHolder<TYtWideFlowTransformer>(std::move(state));
+}
+
+} // namespace NYql
diff --git a/ydb/library/yql/providers/yt/ya.make b/ydb/library/yql/providers/yt/ya.make
new file mode 100644
index 0000000000..a43252062f
--- /dev/null
+++ b/ydb/library/yql/providers/yt/ya.make
@@ -0,0 +1,17 @@
+RECURSE(
+ codec
+ codec/codegen/ut
+ codec/ut
+ common
+ comp_nodes
+ comp_nodes/ut
+ comp_nodes/dq
+ expr_nodes
+ gateway
+ job
+ lib
+ mkql_dq
+ opt
+ provider
+ provider/ut
+)
diff --git a/ydb/library/yql/ya.make b/ydb/library/yql/ya.make
index 94a30b6b95..8a7e8bb3a5 100644
--- a/ydb/library/yql/ya.make
+++ b/ydb/library/yql/ya.make
@@ -26,3 +26,18 @@ RECURSE(
udfs
utils
)
+
+# TODO(max42): Recurse unconditionally as a final step of YT-19210.
+IF (NOT EXPORT_CMAKE)
+ RECURSE(
+ core/url_preprocessing
+ )
+ENDIF()
+
+#TODO(max42): Recurse unconditionally as a final step of YT-19210.
+IF (NOT EXPORT_CMAKE)
+ RECURSE_FOR_TESTS(
+ core/extract_predicate/ut
+ core/ut
+ )
+ENDIF()
diff --git a/yt/cpp/mapreduce/client/abortable_registry.cpp b/yt/cpp/mapreduce/client/abortable_registry.cpp
new file mode 100644
index 0000000000..283d39e049
--- /dev/null
+++ b/yt/cpp/mapreduce/client/abortable_registry.cpp
@@ -0,0 +1,125 @@
+#include "abortable_registry.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <util/generic/singleton.h>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransactionAbortable::TTransactionAbortable(const TClientContext& context, const TTransactionId& transactionId)
+ : Context_(context)
+ , TransactionId_(transactionId)
+{ }
+
+void TTransactionAbortable::Abort()
+{
+ AbortTransaction(nullptr, Context_, TransactionId_);
+}
+
+TString TTransactionAbortable::GetType() const
+{
+ return "transaction";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperationAbortable::TOperationAbortable(IClientRetryPolicyPtr clientRetryPolicy, TClientContext context, const TOperationId& operationId)
+ : ClientRetryPolicy_(std::move(clientRetryPolicy))
+ , Context_(std::move(context))
+ , OperationId_(operationId)
+{ }
+
+
+void TOperationAbortable::Abort()
+{
+ AbortOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, OperationId_);
+}
+
+TString TOperationAbortable::GetType() const
+{
+ return "operation";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAbortableRegistry::AbortAllAndBlockForever()
+{
+ auto guard = Guard(Lock_);
+
+ for (const auto& entry : ActiveAbortables_) {
+ const auto& id = entry.first;
+ const auto& abortable = entry.second;
+ try {
+ abortable->Abort();
+ } catch (std::exception& ex) {
+ YT_LOG_ERROR("Exception while aborting %v %v: %v",
+ abortable->GetType(),
+ id,
+ ex.what());
+ }
+ }
+
+ Running_ = false;
+}
+
+void TAbortableRegistry::Add(const TGUID& id, IAbortablePtr abortable)
+{
+ auto guard = Guard(Lock_);
+
+ if (!Running_) {
+ Sleep(TDuration::Max());
+ }
+
+ ActiveAbortables_[id] = abortable;
+}
+
+void TAbortableRegistry::Remove(const TGUID& id)
+{
+ auto guard = Guard(Lock_);
+
+ if (!Running_) {
+ Sleep(TDuration::Max());
+ }
+
+ ActiveAbortables_.erase(id);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TRegistryHolder
+{
+public:
+ TRegistryHolder()
+ : Registry_(::MakeIntrusive<TAbortableRegistry>())
+ { }
+
+ ::TIntrusivePtr<TAbortableRegistry> Get()
+ {
+ return Registry_;
+ }
+
+private:
+ ::TIntrusivePtr<TAbortableRegistry> Registry_;
+};
+
+} // namespace
+
+::TIntrusivePtr<TAbortableRegistry> TAbortableRegistry::Get()
+{
+ return Singleton<TRegistryHolder>()->Get();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/abortable_registry.h b/yt/cpp/mapreduce/client/abortable_registry.h
new file mode 100644
index 0000000000..119d685cad
--- /dev/null
+++ b/yt/cpp/mapreduce/client/abortable_registry.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <util/str_stl.h>
+#include <util/system/mutex.h>
+#include <util/generic/hash.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IAbortable
+ : public TThrRefBase
+{
+public:
+ virtual void Abort() = 0;
+ virtual TString GetType() const = 0;
+};
+
+using IAbortablePtr = ::TIntrusivePtr<IAbortable>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTransactionAbortable
+ : public IAbortable
+{
+public:
+ TTransactionAbortable(const TClientContext& context, const TTransactionId& transactionId);
+ void Abort() override;
+ TString GetType() const override;
+
+private:
+ TClientContext Context_;
+ TTransactionId TransactionId_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperationAbortable
+ : public IAbortable
+{
+public:
+ TOperationAbortable(IClientRetryPolicyPtr clientRetryPolicy, TClientContext context, const TOperationId& operationId);
+ void Abort() override;
+ TString GetType() const override;
+
+private:
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+ const TClientContext Context_;
+ const TOperationId OperationId_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortableRegistry
+ : public TThrRefBase
+{
+public:
+ TAbortableRegistry() = default;
+ static ::TIntrusivePtr<TAbortableRegistry> Get();
+
+ void AbortAllAndBlockForever();
+ void Add(const TGUID& id, IAbortablePtr abortable);
+ void Remove(const TGUID& id);
+
+private:
+ THashMap<TGUID, IAbortablePtr> ActiveAbortables_;
+ TMutex Lock_;
+ bool Running_ = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/batch_request_impl.cpp b/yt/cpp/mapreduce/client/batch_request_impl.cpp
new file mode 100644
index 0000000000..6afa5665f1
--- /dev/null
+++ b/yt/cpp/mapreduce/client/batch_request_impl.cpp
@@ -0,0 +1,198 @@
+#include "batch_request_impl.h"
+
+#include "lock.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/serialize.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+#include <yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h>
+
+#include <util/generic/guid.h>
+#include <util/string/builder.h>
+
+#include <exception>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+using ::NThreading::TFuture;
+using ::NThreading::TPromise;
+using ::NThreading::NewPromise;
+
+////////////////////////////////////////////////////////////////////
+
+TBatchRequest::TBatchRequest(const TTransactionId& defaultTransaction, ::TIntrusivePtr<TClient> client)
+ : DefaultTransaction_(defaultTransaction)
+ , Impl_(MakeIntrusive<TRawBatchRequest>(client->GetContext().Config))
+ , Client_(client)
+{ }
+
+TBatchRequest::TBatchRequest(TRawBatchRequest* impl, ::TIntrusivePtr<TClient> client)
+ : Impl_(impl)
+ , Client_(std::move(client))
+{ }
+
+TBatchRequest::~TBatchRequest() = default;
+
+IBatchRequestBase& TBatchRequest::WithTransaction(const TTransactionId& transactionId)
+{
+ if (!TmpWithTransaction_) {
+ TmpWithTransaction_.Reset(new TBatchRequest(Impl_.Get(), Client_));
+ }
+ TmpWithTransaction_->DefaultTransaction_ = transactionId;
+ return *TmpWithTransaction_;
+}
+
+TFuture<TNode> TBatchRequest::Get(
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ return Impl_->Get(DefaultTransaction_, path, options);
+}
+
+TFuture<void> TBatchRequest::Set(const TYPath& path, const TNode& node, const TSetOptions& options)
+{
+ return Impl_->Set(DefaultTransaction_, path, node, options);
+}
+
+TFuture<TNode::TListType> TBatchRequest::List(const TYPath& path, const TListOptions& options)
+{
+ return Impl_->List(DefaultTransaction_, path, options);
+}
+
+TFuture<bool> TBatchRequest::Exists(const TYPath& path, const TExistsOptions& options)
+{
+ return Impl_->Exists(DefaultTransaction_, path, options);
+}
+
+TFuture<ILockPtr> TBatchRequest::Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options)
+{
+ auto convert = [waitable=options.Waitable_, client=Client_] (TFuture<TNodeId> nodeIdFuture) -> ILockPtr {
+ return ::MakeIntrusive<TLock>(nodeIdFuture.GetValue(), client, waitable);
+ };
+ return Impl_->Lock(DefaultTransaction_, path, mode, options).Apply(convert);
+}
+
+::NThreading::TFuture<void> TBatchRequest::Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options = TUnlockOptions())
+{
+ return Impl_->Unlock(DefaultTransaction_, path, options);
+}
+
+TFuture<TLockId> TBatchRequest::Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options)
+{
+ return Impl_->Create(DefaultTransaction_, path, type, options);
+}
+
+TFuture<void> TBatchRequest::Remove(
+ const TYPath& path,
+ const TRemoveOptions& options)
+{
+ return Impl_->Remove(DefaultTransaction_, path, options);
+}
+
+TFuture<TNodeId> TBatchRequest::Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options)
+{
+ return Impl_->Move(DefaultTransaction_, sourcePath, destinationPath, options);
+}
+
+TFuture<TNodeId> TBatchRequest::Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options)
+{
+ return Impl_->Copy(DefaultTransaction_, sourcePath, destinationPath, options);
+}
+
+TFuture<TNodeId> TBatchRequest::Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options)
+{
+ return Impl_->Link(DefaultTransaction_, targetPath, linkPath, options);
+}
+
+TFuture<void> TBatchRequest::AbortOperation(const NYT::TOperationId& operationId)
+{
+ return Impl_->AbortOperation(operationId);
+}
+
+TFuture<void> TBatchRequest::CompleteOperation(const NYT::TOperationId& operationId)
+{
+ return Impl_->CompleteOperation(operationId);
+}
+
+TFuture<void> TBatchRequest::SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options)
+{
+ return Impl_->SuspendOperation(operationId, options);
+}
+
+TFuture<void> TBatchRequest::ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options)
+{
+ return Impl_->ResumeOperation(operationId, options);
+}
+
+TFuture<void> TBatchRequest::UpdateOperationParameters(
+ const NYT::TOperationId& operationId,
+ const NYT::TUpdateOperationParametersOptions& options)
+{
+ return Impl_->UpdateOperationParameters(operationId, options);
+}
+
+TFuture<TRichYPath> TBatchRequest::CanonizeYPath(const TRichYPath& path)
+{
+ return Impl_->CanonizeYPath(path);
+}
+
+TFuture<TVector<TTableColumnarStatistics>> TBatchRequest::GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const NYT::TGetTableColumnarStatisticsOptions& options)
+{
+ return Impl_->GetTableColumnarStatistics(DefaultTransaction_, paths, options);
+}
+
+TFuture<TCheckPermissionResponse> TBatchRequest::CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options)
+{
+ return Impl_->CheckPermission(user, permission, path, options);
+}
+
+void TBatchRequest::ExecuteBatch(const TExecuteBatchOptions& options)
+{
+ NYT::NDetail::ExecuteBatch(Client_->GetRetryPolicy()->CreatePolicyForGenericRequest(), Client_->GetContext(), *Impl_, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/batch_request_impl.h b/yt/cpp/mapreduce/client/batch_request_impl.h
new file mode 100644
index 0000000000..0a176417b3
--- /dev/null
+++ b/yt/cpp/mapreduce/client/batch_request_impl.h
@@ -0,0 +1,137 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/batch_request.h>
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/interface/node.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/deque.h>
+
+#include <exception>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TResponseInfo;
+class TClient;
+using TClientPtr = ::TIntrusivePtr<TClient>;
+
+namespace NRawClient {
+ class TRawBatchRequest;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBatchRequest
+ : public IBatchRequest
+{
+public:
+ TBatchRequest(const TTransactionId& defaultTransaction, ::TIntrusivePtr<TClient> client);
+
+ ~TBatchRequest();
+
+ virtual IBatchRequestBase& WithTransaction(const TTransactionId& transactionId) override;
+
+ virtual ::NThreading::TFuture<TLockId> Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options = TCreateOptions()) override;
+
+ virtual ::NThreading::TFuture<void> Remove(
+ const TYPath& path,
+ const TRemoveOptions& options = TRemoveOptions()) override;
+
+ virtual ::NThreading::TFuture<bool> Exists(
+ const TYPath& path,
+ const TExistsOptions& options = TExistsOptions()) override;
+
+ virtual ::NThreading::TFuture<TNode> Get(
+ const TYPath& path,
+ const TGetOptions& options = TGetOptions()) override;
+
+ virtual ::NThreading::TFuture<void> Set(
+ const TYPath& path,
+ const TNode& node,
+ const TSetOptions& options = TSetOptions()) override;
+
+ virtual ::NThreading::TFuture<TNode::TListType> List(
+ const TYPath& path,
+ const TListOptions& options = TListOptions()) override;
+
+ virtual ::NThreading::TFuture<TNodeId> Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options = TCopyOptions()) override;
+
+ virtual ::NThreading::TFuture<TNodeId> Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options = TMoveOptions()) override;
+
+ virtual ::NThreading::TFuture<TNodeId> Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options = TLinkOptions()) override;
+
+ virtual ::NThreading::TFuture<ILockPtr> Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options) override;
+
+ virtual ::NThreading::TFuture<void> Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options) override;
+
+ virtual ::NThreading::TFuture<void> AbortOperation(const TOperationId& operationId) override;
+
+ virtual ::NThreading::TFuture<void> CompleteOperation(const TOperationId& operationId) override;
+
+ ::NThreading::TFuture<void> SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options) override;
+
+ ::NThreading::TFuture<void> ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options) override;
+
+ virtual ::NThreading::TFuture<void> UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options) override;
+
+ virtual ::NThreading::TFuture<TRichYPath> CanonizeYPath(const TRichYPath& path) override;
+
+ virtual ::NThreading::TFuture<TVector<TTableColumnarStatistics>> GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options) override;
+
+ ::NThreading::TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options) override;
+
+ virtual void ExecuteBatch(const TExecuteBatchOptions& executeBatch) override;
+
+private:
+ TBatchRequest(NDetail::NRawClient::TRawBatchRequest* impl, ::TIntrusivePtr<TClient> client);
+
+private:
+ TTransactionId DefaultTransaction_;
+ ::TIntrusivePtr<NDetail::NRawClient::TRawBatchRequest> Impl_;
+ THolder<TBatchRequest> TmpWithTransaction_;
+ ::TIntrusivePtr<TClient> Client_;
+
+private:
+ friend class NYT::NDetail::TClient;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client.cpp b/yt/cpp/mapreduce/client/client.cpp
new file mode 100644
index 0000000000..ca979c5588
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client.cpp
@@ -0,0 +1,1361 @@
+#include "client.h"
+
+#include "batch_request_impl.h"
+#include "client_reader.h"
+#include "client_writer.h"
+#include "file_reader.h"
+#include "file_writer.h"
+#include "format_hints.h"
+#include "lock.h"
+#include "operation.h"
+#include "retry_transaction.h"
+#include "retryful_writer.h"
+#include "transaction.h"
+#include "transaction_pinger.h"
+#include "yt_poller.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/http.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/fluent.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+#include <yt/cpp/mapreduce/interface/skiff_row.h>
+
+#include <yt/cpp/mapreduce/io/yamr_table_reader.h>
+#include <yt/cpp/mapreduce/io/yamr_table_writer.h>
+#include <yt/cpp/mapreduce/io/node_table_reader.h>
+#include <yt/cpp/mapreduce/io/node_table_writer.h>
+#include <yt/cpp/mapreduce/io/proto_table_reader.h>
+#include <yt/cpp/mapreduce/io/proto_table_writer.h>
+#include <yt/cpp/mapreduce/io/skiff_row_table_reader.h>
+#include <yt/cpp/mapreduce/io/proto_helpers.h>
+
+#include <yt/cpp/mapreduce/library/table_schema/protobuf.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+#include <yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h>
+
+#include <library/cpp/json/json_reader.h>
+
+#include <util/generic/algorithm.h>
+#include <util/string/type.h>
+#include <util/system/env.h>
+
+#include <exception>
+
+using namespace NYT::NDetail::NRawClient;
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientBase::TClientBase(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ IClientRetryPolicyPtr retryPolicy)
+ : Context_(context)
+ , TransactionId_(transactionId)
+ , ClientRetryPolicy_(std::move(retryPolicy))
+{ }
+
+ITransactionPtr TClientBase::StartTransaction(
+ const TStartTransactionOptions& options)
+{
+ return MakeIntrusive<TTransaction>(GetParentClientImpl(), Context_, TransactionId_, options);
+}
+
+TNodeId TClientBase::Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options)
+{
+ return NRawClient::Create(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, type, options);
+}
+
+void TClientBase::Remove(
+ const TYPath& path,
+ const TRemoveOptions& options)
+{
+ return NRawClient::Remove(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+bool TClientBase::Exists(
+ const TYPath& path,
+ const TExistsOptions& options)
+{
+ return NRawClient::Exists(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+TNode TClientBase::Get(
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ return NRawClient::Get(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+void TClientBase::Set(
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options)
+{
+ NRawClient::Set(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, value, options);
+}
+
+void TClientBase::MultisetAttributes(
+ const TYPath& path, const TNode::TMapType& value, const TMultisetAttributesOptions& options)
+{
+ NRawClient::MultisetAttributes(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, value, options);
+}
+
+
+TNode::TListType TClientBase::List(
+ const TYPath& path,
+ const TListOptions& options)
+{
+ return NRawClient::List(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+TNodeId TClientBase::Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options)
+{
+ return NRawClient::Copy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, sourcePath, destinationPath, options);
+}
+
+TNodeId TClientBase::Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options)
+{
+ return NRawClient::Move(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, sourcePath, destinationPath, options);
+}
+
+TNodeId TClientBase::Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options)
+{
+ return NRawClient::Link(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, targetPath, linkPath, options);
+}
+
+void TClientBase::Concatenate(
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options)
+{
+ std::function<void(ITransactionPtr)> lambda = [&sourcePaths, &destinationPath, &options, this](ITransactionPtr transaction) {
+ if (!options.Append_ && !sourcePaths.empty() && !transaction->Exists(destinationPath.Path_)) {
+ auto typeNode = transaction->Get(CanonizeYPath(sourcePaths.front()).Path_ + "/@type");
+ auto type = FromString<ENodeType>(typeNode.AsString());
+ transaction->Create(destinationPath.Path_, type, TCreateOptions().IgnoreExisting(true));
+ }
+ NRawClient::Concatenate(this->Context_, transaction->GetId(), sourcePaths, destinationPath, options);
+ };
+ RetryTransactionWithPolicy(this, lambda, ClientRetryPolicy_->CreatePolicyForGenericRequest());
+}
+
+TRichYPath TClientBase::CanonizeYPath(const TRichYPath& path)
+{
+ return NRawClient::CanonizeYPath(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, path);
+}
+
+TVector<TTableColumnarStatistics> TClientBase::GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options)
+{
+ return NRawClient::GetTableColumnarStatistics(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ TransactionId_,
+ paths,
+ options);
+}
+
+TMultiTablePartitions TClientBase::GetTablePartitions(
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options)
+{
+ return NRawClient::GetTablePartitions(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ TransactionId_,
+ paths,
+ options);
+}
+
+TMaybe<TYPath> TClientBase::GetFileFromCache(
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options)
+{
+ return NRawClient::GetFileFromCache(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, md5Signature, cachePath, options);
+}
+
+TYPath TClientBase::PutFileToCache(
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options)
+{
+ return NRawClient::PutFileToCache(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, filePath, md5Signature, cachePath, options);
+}
+
+IFileReaderPtr TClientBase::CreateBlobTableReader(
+ const TYPath& path,
+ const TKey& key,
+ const TBlobTableReaderOptions& options)
+{
+ return new TBlobTableReader(
+ path,
+ key,
+ ClientRetryPolicy_,
+ GetTransactionPinger(),
+ Context_,
+ TransactionId_,
+ options);
+}
+
+IFileReaderPtr TClientBase::CreateFileReader(
+ const TRichYPath& path,
+ const TFileReaderOptions& options)
+{
+ return new TFileReader(
+ CanonizeYPath(path),
+ ClientRetryPolicy_,
+ GetTransactionPinger(),
+ Context_,
+ TransactionId_,
+ options);
+}
+
+IFileWriterPtr TClientBase::CreateFileWriter(
+ const TRichYPath& path,
+ const TFileWriterOptions& options)
+{
+ auto realPath = CanonizeYPath(path);
+ if (!NRawClient::Exists(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, realPath.Path_)) {
+ NRawClient::Create(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, realPath.Path_, NT_FILE,
+ TCreateOptions().IgnoreExisting(true));
+ }
+ return new TFileWriter(realPath, ClientRetryPolicy_, GetTransactionPinger(), Context_, TransactionId_, options);
+}
+
+TTableWriterPtr<::google::protobuf::Message> TClientBase::CreateTableWriter(
+ const TRichYPath& path, const ::google::protobuf::Descriptor& descriptor, const TTableWriterOptions& options)
+{
+ const Message* prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(&descriptor);
+ return new TTableWriter<::google::protobuf::Message>(CreateProtoWriter(path, options, prototype));
+}
+
+TRawTableReaderPtr TClientBase::CreateRawReader(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableReaderOptions& options)
+{
+ return CreateClientReader(path, format, options).Get();
+}
+
+TRawTableWriterPtr TClientBase::CreateRawWriter(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableWriterOptions& options)
+{
+ return ::MakeIntrusive<TRetryfulWriter>(
+ ClientRetryPolicy_,
+ GetTransactionPinger(),
+ Context_,
+ TransactionId_,
+ GetWriteTableCommand(Context_.Config->ApiVersion),
+ format,
+ CanonizeYPath(path),
+ options).Get();
+}
+
+IOperationPtr TClientBase::DoMap(
+ const TMapOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ mapper,
+ options
+ ] () {
+ ExecuteMap(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ mapper,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RawMap(
+ const TRawMapOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ mapper,
+ options
+ ] () {
+ ExecuteRawMap(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ mapper,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::DoReduce(
+ const TReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ reducer,
+ options
+ ] () {
+ ExecuteReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RawReduce(
+ const TRawReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ reducer,
+ options
+ ] () {
+ ExecuteRawReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::DoJoinReduce(
+ const TJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ reducer,
+ options
+ ] () {
+ ExecuteJoinReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RawJoinReduce(
+ const TRawJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ reducer,
+ options
+ ] () {
+ ExecuteRawJoinReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::DoMapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ ::TIntrusivePtr<IStructuredJob> reduceCombiner,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options
+ ] () {
+ ExecuteMapReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RawMapReduce(
+ const TRawMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ ::TIntrusivePtr<IRawJob> reduceCombiner,
+ ::TIntrusivePtr<IRawJob> reducer,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_=::TIntrusivePtr(this),
+ operation,
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options
+ ] () {
+ ExecuteRawMapReduce(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::Sort(
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ options
+ ] () {
+ ExecuteSort(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::Merge(
+ const TMergeOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ options
+ ] () {
+ ExecuteMerge(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::Erase(
+ const TEraseOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ options
+ ] () {
+ ExecuteErase(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RemoteCopy(
+ const TRemoteCopyOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ options
+ ] () {
+ ExecuteRemoteCopy(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::RunVanilla(
+ const TVanillaOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ auto operation = ::MakeIntrusive<TOperation>(GetParentClientImpl());
+ auto prepareOperation = [
+ this_ = ::TIntrusivePtr(this),
+ operation,
+ spec,
+ options
+ ] () {
+ ExecuteVanilla(
+ operation,
+ ::MakeIntrusive<TOperationPreparer>(this_->GetParentClientImpl(), this_->TransactionId_),
+ spec,
+ options);
+ };
+ return ProcessOperation(GetParentClientImpl(), std::move(prepareOperation), std::move(operation), options);
+}
+
+IOperationPtr TClientBase::AttachOperation(const TOperationId& operationId)
+{
+ auto operation = ::MakeIntrusive<TOperation>(operationId, GetParentClientImpl());
+ operation->GetBriefState(); // check that operation exists
+ return operation;
+}
+
+EOperationBriefState TClientBase::CheckOperation(const TOperationId& operationId)
+{
+ return NYT::NDetail::CheckOperation(ClientRetryPolicy_, Context_, operationId);
+}
+
+void TClientBase::AbortOperation(const TOperationId& operationId)
+{
+ NRawClient::AbortOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId);
+}
+
+void TClientBase::CompleteOperation(const TOperationId& operationId)
+{
+ NRawClient::CompleteOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId);
+}
+
+void TClientBase::WaitForOperation(const TOperationId& operationId)
+{
+ NYT::NDetail::WaitForOperation(ClientRetryPolicy_, Context_, operationId);
+}
+
+void TClientBase::AlterTable(
+ const TYPath& path,
+ const TAlterTableOptions& options)
+{
+ NRawClient::AlterTable(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+::TIntrusivePtr<TClientReader> TClientBase::CreateClientReader(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableReaderOptions& options,
+ bool useFormatFromTableAttributes)
+{
+ return ::MakeIntrusive<TClientReader>(
+ CanonizeYPath(path),
+ ClientRetryPolicy_,
+ GetTransactionPinger(),
+ Context_,
+ TransactionId_,
+ format,
+ options,
+ useFormatFromTableAttributes);
+}
+
+THolder<TClientWriter> TClientBase::CreateClientWriter(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableWriterOptions& options)
+{
+ auto realPath = CanonizeYPath(path);
+ if (!NRawClient::Exists(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, realPath.Path_)) {
+ NRawClient::Create(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, realPath.Path_, NT_TABLE,
+ TCreateOptions().IgnoreExisting(true));
+ }
+ return MakeHolder<TClientWriter>(
+ realPath,
+ ClientRetryPolicy_,
+ GetTransactionPinger(),
+ Context_,
+ TransactionId_,
+ format,
+ options
+ );
+}
+
+::TIntrusivePtr<INodeReaderImpl> TClientBase::CreateNodeReader(
+ const TRichYPath& path, const TTableReaderOptions& options)
+{
+ auto format = TFormat::YsonBinary();
+ ApplyFormatHints<TNode>(&format, options.FormatHints_);
+
+ // Skiff is disabled here because of large header problem (see https://st.yandex-team.ru/YT-6926).
+ // Revert this code to r3614168 when it is fixed.
+ return new TNodeTableReader(
+ CreateClientReader(path, format, options));
+}
+
+::TIntrusivePtr<IYaMRReaderImpl> TClientBase::CreateYaMRReader(
+ const TRichYPath& path, const TTableReaderOptions& options)
+{
+ return new TYaMRTableReader(
+ CreateClientReader(path, TFormat::YaMRLenval(), options, /* useFormatFromTableAttributes = */ true));
+}
+
+::TIntrusivePtr<IProtoReaderImpl> TClientBase::CreateProtoReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const Message* prototype)
+{
+ TVector<const ::google::protobuf::Descriptor*> descriptors;
+ descriptors.push_back(prototype->GetDescriptor());
+
+ if (Context_.Config->UseClientProtobuf) {
+ return new TProtoTableReader(
+ CreateClientReader(path, TFormat::YsonBinary(), options),
+ std::move(descriptors));
+ } else {
+ auto format = TFormat::Protobuf({prototype->GetDescriptor()}, Context_.Config->ProtobufFormatWithDescriptors);
+ return new TLenvalProtoTableReader(
+ CreateClientReader(path, format, options),
+ std::move(descriptors));
+ }
+}
+
+::TIntrusivePtr<ISkiffRowReaderImpl> TClientBase::CreateSkiffRowReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const ISkiffRowSkipperPtr& skipper,
+ const NSkiff::TSkiffSchemaPtr& schema)
+{
+ auto skiffOptions = TCreateSkiffSchemaOptions().HasRangeIndex(true);
+ auto resultSchema = NYT::NDetail::CreateSkiffSchema(TVector{schema}, skiffOptions);
+ return new TSkiffRowTableReader(
+ CreateClientReader(path, NYT::NDetail::CreateSkiffFormat(resultSchema), options),
+ resultSchema,
+ {skipper},
+ std::move(skiffOptions));
+}
+
+::TIntrusivePtr<INodeWriterImpl> TClientBase::CreateNodeWriter(
+ const TRichYPath& path, const TTableWriterOptions& options)
+{
+ auto format = TFormat::YsonBinary();
+ ApplyFormatHints<TNode>(&format, options.FormatHints_);
+
+ return new TNodeTableWriter(
+ CreateClientWriter(path, format, options));
+}
+
+::TIntrusivePtr<IYaMRWriterImpl> TClientBase::CreateYaMRWriter(
+ const TRichYPath& path, const TTableWriterOptions& options)
+{
+ auto format = TFormat::YaMRLenval();
+ ApplyFormatHints<TYaMRRow>(&format, options.FormatHints_);
+
+ return new TYaMRTableWriter(
+ CreateClientWriter(path, format, options));
+}
+
+::TIntrusivePtr<IProtoWriterImpl> TClientBase::CreateProtoWriter(
+ const TRichYPath& path,
+ const TTableWriterOptions& options,
+ const Message* prototype)
+{
+ TVector<const ::google::protobuf::Descriptor*> descriptors;
+ descriptors.push_back(prototype->GetDescriptor());
+
+ auto pathWithSchema = path;
+ if (options.InferSchema_.GetOrElse(Context_.Config->InferTableSchema) && !path.Schema_) {
+ pathWithSchema.Schema(CreateTableSchema(*prototype->GetDescriptor()));
+ }
+
+ if (Context_.Config->UseClientProtobuf) {
+ auto format = TFormat::YsonBinary();
+ ApplyFormatHints<TNode>(&format, options.FormatHints_);
+ return new TProtoTableWriter(
+ CreateClientWriter(pathWithSchema, format, options),
+ std::move(descriptors));
+ } else {
+ auto format = TFormat::Protobuf({prototype->GetDescriptor()}, Context_.Config->ProtobufFormatWithDescriptors);
+ ApplyFormatHints<::google::protobuf::Message>(&format, options.FormatHints_);
+ return new TLenvalProtoTableWriter(
+ CreateClientWriter(pathWithSchema, format, options),
+ std::move(descriptors));
+ }
+}
+
+TBatchRequestPtr TClientBase::CreateBatchRequest()
+{
+ return MakeIntrusive<TBatchRequest>(TransactionId_, GetParentClientImpl());
+}
+
+IClientPtr TClientBase::GetParentClient()
+{
+ return GetParentClientImpl();
+}
+
+const TClientContext& TClientBase::GetContext() const
+{
+ return Context_;
+}
+
+const IClientRetryPolicyPtr& TClientBase::GetRetryPolicy() const
+{
+ return ClientRetryPolicy_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransaction::TTransaction(
+ TClientPtr parentClient,
+ const TClientContext& context,
+ const TTransactionId& parentTransactionId,
+ const TStartTransactionOptions& options)
+ : TClientBase(context, parentTransactionId, parentClient->GetRetryPolicy())
+ , TransactionPinger_(parentClient->GetTransactionPinger())
+ , PingableTx_(
+ MakeHolder<TPingableTransaction>(
+ parentClient->GetRetryPolicy(),
+ context,
+ parentTransactionId,
+ TransactionPinger_->GetChildTxPinger(),
+ options))
+ , ParentClient_(parentClient)
+{
+ TransactionId_ = PingableTx_->GetId();
+}
+
+TTransaction::TTransaction(
+ TClientPtr parentClient,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TAttachTransactionOptions& options)
+ : TClientBase(context, transactionId, parentClient->GetRetryPolicy())
+ , TransactionPinger_(parentClient->GetTransactionPinger())
+ , PingableTx_(
+ new TPingableTransaction(
+ parentClient->GetRetryPolicy(),
+ context,
+ transactionId,
+ parentClient->GetTransactionPinger()->GetChildTxPinger(),
+ options))
+ , ParentClient_(parentClient)
+{ }
+
+const TTransactionId& TTransaction::GetId() const
+{
+ return TransactionId_;
+}
+
+ILockPtr TTransaction::Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options)
+{
+ auto lockId = NRawClient::Lock(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, mode, options);
+ return ::MakeIntrusive<TLock>(lockId, GetParentClientImpl(), options.Waitable_);
+}
+
+void TTransaction::Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options)
+{
+ NRawClient::Unlock(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_, path, options);
+}
+
+void TTransaction::Commit()
+{
+ PingableTx_->Commit();
+}
+
+void TTransaction::Abort()
+{
+ PingableTx_->Abort();
+}
+
+void TTransaction::Ping()
+{
+ PingTx(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, TransactionId_);
+}
+
+void TTransaction::Detach()
+{
+ PingableTx_->Detach();
+}
+
+ITransactionPingerPtr TTransaction::GetTransactionPinger()
+{
+ return TransactionPinger_;
+}
+
+TClientPtr TTransaction::GetParentClientImpl()
+{
+ return ParentClient_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClient::TClient(
+ const TClientContext& context,
+ const TTransactionId& globalId,
+ IClientRetryPolicyPtr retryPolicy)
+ : TClientBase(context, globalId, retryPolicy)
+ , TransactionPinger_(nullptr)
+{ }
+
+TClient::~TClient() = default;
+
+ITransactionPtr TClient::AttachTransaction(
+ const TTransactionId& transactionId,
+ const TAttachTransactionOptions& options)
+{
+ CheckShutdown();
+
+ return MakeIntrusive<TTransaction>(this, Context_, transactionId, options);
+}
+
+void TClient::MountTable(
+ const TYPath& path,
+ const TMountTableOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "mount_table");
+ SetTabletParams(header, path, options);
+ if (options.CellId_) {
+ header.AddParameter("cell_id", GetGuidAsString(*options.CellId_));
+ }
+ header.AddParameter("freeze", options.Freeze_);
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+}
+
+void TClient::UnmountTable(
+ const TYPath& path,
+ const TUnmountTableOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "unmount_table");
+ SetTabletParams(header, path, options);
+ header.AddParameter("force", options.Force_);
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+}
+
+void TClient::RemountTable(
+ const TYPath& path,
+ const TRemountTableOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "remount_table");
+ SetTabletParams(header, path, options);
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+}
+
+void TClient::FreezeTable(
+ const TYPath& path,
+ const TFreezeTableOptions& options)
+{
+ CheckShutdown();
+ NRawClient::FreezeTable(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, path, options);
+}
+
+void TClient::UnfreezeTable(
+ const TYPath& path,
+ const TUnfreezeTableOptions& options)
+{
+ CheckShutdown();
+ NRawClient::UnfreezeTable(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, path, options);
+}
+
+void TClient::ReshardTable(
+ const TYPath& path,
+ const TVector<TKey>& keys,
+ const TReshardTableOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "reshard_table");
+ SetTabletParams(header, path, options);
+ header.AddParameter("pivot_keys", BuildYsonNodeFluently().List(keys));
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+}
+
+void TClient::ReshardTable(
+ const TYPath& path,
+ i64 tabletCount,
+ const TReshardTableOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "reshard_table");
+ SetTabletParams(header, path, options);
+ header.AddParameter("tablet_count", tabletCount);
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+}
+
+void TClient::InsertRows(
+ const TYPath& path,
+ const TNode::TListType& rows,
+ const TInsertRowsOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("PUT", "insert_rows");
+ header.SetInputFormat(TFormat::YsonBinary());
+ // TODO: use corresponding raw request
+ header.MergeParameters(SerializeParametersForInsertRows(Context_.Config->Prefix, path, options));
+
+ auto body = NodeListToYsonString(rows);
+ TRequestConfig config;
+ config.IsHeavy = true;
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header, body, config);
+}
+
+void TClient::DeleteRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TDeleteRowsOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::DeleteRows(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, path, keys, options);
+}
+
+void TClient::TrimRows(
+ const TYPath& path,
+ i64 tabletIndex,
+ i64 rowCount,
+ const TTrimRowsOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("POST", "trim_rows");
+ header.AddParameter("trimmed_row_count", rowCount);
+ header.AddParameter("tablet_index", tabletIndex);
+ // TODO: use corresponding raw request
+ header.MergeParameters(NRawClient::SerializeParametersForTrimRows(Context_.Config->Prefix, path, options));
+
+ TRequestConfig config;
+ config.IsHeavy = true;
+ RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header, {}, config);
+}
+
+TNode::TListType TClient::LookupRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TLookupRowsOptions& options)
+{
+ CheckShutdown();
+
+ Y_UNUSED(options);
+ THttpHeader header("PUT", "lookup_rows");
+ header.AddPath(AddPathPrefix(path, Context_.Config->ApiVersion));
+ header.SetInputFormat(TFormat::YsonBinary());
+ header.SetOutputFormat(TFormat::YsonBinary());
+
+ header.MergeParameters(BuildYsonNodeFluently().BeginMap()
+ .DoIf(options.Timeout_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("timeout").Value(static_cast<i64>(options.Timeout_->MilliSeconds()));
+ })
+ .Item("keep_missing_rows").Value(options.KeepMissingRows_)
+ .DoIf(options.Versioned_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("versioned").Value(*options.Versioned_);
+ })
+ .DoIf(options.Columns_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("column_names").Value(*options.Columns_);
+ })
+ .EndMap());
+
+ auto body = NodeListToYsonString(keys);
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto result = RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header, body, config);
+ return NodeFromYsonString(result.Response, ::NYson::EYsonType::ListFragment).AsList();
+}
+
+TNode::TListType TClient::SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options)
+{
+ CheckShutdown();
+
+ THttpHeader header("GET", "select_rows");
+ header.SetInputFormat(TFormat::YsonBinary());
+ header.SetOutputFormat(TFormat::YsonBinary());
+
+ header.MergeParameters(BuildYsonNodeFluently().BeginMap()
+ .Item("query").Value(query)
+ .DoIf(options.Timeout_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("timeout").Value(static_cast<i64>(options.Timeout_->MilliSeconds()));
+ })
+ .DoIf(options.InputRowLimit_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("input_row_limit").Value(*options.InputRowLimit_);
+ })
+ .DoIf(options.OutputRowLimit_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("output_row_limit").Value(*options.OutputRowLimit_);
+ })
+ .Item("range_expansion_limit").Value(options.RangeExpansionLimit_)
+ .Item("fail_on_incomplete_result").Value(options.FailOnIncompleteResult_)
+ .Item("verbose_logging").Value(options.VerboseLogging_)
+ .Item("enable_code_cache").Value(options.EnableCodeCache_)
+ .EndMap());
+
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto result = RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header, {}, config);
+ return NodeFromYsonString(result.Response, ::NYson::EYsonType::ListFragment).AsList();
+}
+
+void TClient::AlterTableReplica(const TReplicaId& replicaId, const TAlterTableReplicaOptions& options)
+{
+ CheckShutdown();
+ NRawClient::AlterTableReplica(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, replicaId, options);
+}
+
+ui64 TClient::GenerateTimestamp()
+{
+ CheckShutdown();
+ THttpHeader header("GET", "generate_timestamp");
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto requestResult = RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header, {}, config);
+ return NodeFromYsonString(requestResult.Response).AsUint64();
+}
+
+TAuthorizationInfo TClient::WhoAmI()
+{
+ CheckShutdown();
+
+ THttpHeader header("GET", "auth/whoami", /* isApi = */ false);
+ auto requestResult = RetryRequestWithPolicy(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, header);
+ TAuthorizationInfo result;
+
+ NJson::TJsonValue jsonValue;
+ bool ok = NJson::ReadJsonTree(requestResult.Response, &jsonValue, /* throwOnError = */ true);
+ Y_VERIFY(ok);
+ result.Login = jsonValue["login"].GetString();
+ result.Realm = jsonValue["realm"].GetString();
+ return result;
+}
+
+TOperationAttributes TClient::GetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, options);
+}
+
+TListOperationsResult TClient::ListOperations(
+ const TListOperationsOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::ListOperations(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, options);
+}
+
+void TClient::UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::UpdateOperationParameters(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, options);
+}
+
+TJobAttributes TClient::GetJob(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetJob(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, jobId, options);
+}
+
+TListJobsResult TClient::ListJobs(
+ const TOperationId& operationId,
+ const TListJobsOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::ListJobs(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, options);
+}
+
+IFileReaderPtr TClient::GetJobInput(
+ const TJobId& jobId,
+ const TGetJobInputOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetJobInput(Context_, jobId, options);
+}
+
+IFileReaderPtr TClient::GetJobFailContext(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobFailContextOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetJobFailContext(Context_, operationId, jobId, options);
+}
+
+IFileReaderPtr TClient::GetJobStderr(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetJobStderr(Context_, operationId, jobId, options);
+}
+
+TNode::TListType TClient::SkyShareTable(
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::SkyShareTable(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ tablePaths,
+ options);
+}
+
+TCheckPermissionResponse TClient::CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::CheckPermission(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, user, permission, path, options);
+}
+
+TVector<TTabletInfo> TClient::GetTabletInfos(
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options)
+{
+ CheckShutdown();
+ return NRawClient::GetTabletInfos(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, path, tabletIndexes, options);
+}
+
+
+void TClient::SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options)
+{
+ CheckShutdown();
+ NRawClient::SuspendOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, options);
+}
+
+void TClient::ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options)
+{
+ CheckShutdown();
+ NRawClient::ResumeOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, operationId, options);
+}
+
+TYtPoller& TClient::GetYtPoller()
+{
+ auto g = Guard(YtPollerLock_);
+ if (!YtPoller_) {
+ CheckShutdown();
+ // We don't use current client and create new client because YtPoller_ might use
+ // this client during current client shutdown.
+ // That might lead to incrementing of current client refcount and double delete of current client object.
+ YtPoller_ = MakeHolder<TYtPoller>(Context_, ClientRetryPolicy_);
+ }
+ return *YtPoller_;
+}
+
+void TClient::Shutdown()
+{
+ auto g = Guard(YtPollerLock_);
+
+ if (!Shutdown_.exchange(true) && YtPoller_) {
+ YtPoller_->Stop();
+ }
+}
+
+ITransactionPingerPtr TClient::GetTransactionPinger()
+{
+ if (!TransactionPinger_) {
+ TransactionPinger_ = CreateTransactionPinger(Context_.Config);
+ }
+ return TransactionPinger_;
+}
+
+TClientPtr TClient::GetParentClientImpl()
+{
+ return this;
+}
+
+template <class TOptions>
+void TClient::SetTabletParams(
+ THttpHeader& header,
+ const TYPath& path,
+ const TOptions& options)
+{
+ header.AddPath(AddPathPrefix(path, Context_.Config->Prefix));
+ if (options.FirstTabletIndex_) {
+ header.AddParameter("first_tablet_index", *options.FirstTabletIndex_);
+ }
+ if (options.LastTabletIndex_) {
+ header.AddParameter("last_tablet_index", *options.LastTabletIndex_);
+ }
+}
+
+void TClient::CheckShutdown() const
+{
+ if (Shutdown_) {
+ ythrow TApiUsageError() << "Call client's methods after shutdown";
+ }
+}
+
+TClientPtr CreateClientImpl(
+ const TString& serverName,
+ const TCreateClientOptions& options)
+{
+ TClientContext context;
+ context.Config = options.Config_ ? options.Config_ : TConfig::Get();
+ context.TvmOnly = options.TvmOnly_;
+ context.UseTLS = options.UseTLS_;
+
+ context.ServerName = serverName;
+ if (serverName.find('.') == TString::npos &&
+ serverName.find(':') == TString::npos)
+ {
+ context.ServerName += ".yt.yandex.net";
+ }
+
+ if (serverName.find(':') == TString::npos) {
+ context.ServerName = CreateHostNameWithPort(context.ServerName, context);
+ }
+ if (options.TvmOnly_) {
+ context.ServerName = Format("tvm.%v", context.ServerName);
+ }
+
+ if (options.UseTLS_ || options.UseCoreHttpClient_) {
+ context.HttpClient = NHttpClient::CreateCoreHttpClient(options.UseTLS_, context.Config);
+ } else {
+ context.HttpClient = NHttpClient::CreateDefaultHttpClient();
+ }
+
+ context.Token = context.Config->Token;
+ if (options.Token_) {
+ context.Token = options.Token_;
+ } else if (options.TokenPath_) {
+ context.Token = TConfig::LoadTokenFromFile(options.TokenPath_);
+ } else if (options.ServiceTicketAuth_) {
+ context.ServiceTicketAuth = options.ServiceTicketAuth_;
+ }
+
+ context.ImpersonationUser = options.ImpersonationUser_;
+
+ if (context.Token) {
+ TConfig::ValidateToken(context.Token);
+ }
+
+ auto globalTxId = GetGuid(context.Config->GlobalTxId);
+
+ auto retryConfigProvider = options.RetryConfigProvider_;
+ if (!retryConfigProvider) {
+ retryConfigProvider = CreateDefaultRetryConfigProvider();
+ }
+ return new NDetail::TClient(context, globalTxId, CreateDefaultClientRetryPolicy(retryConfigProvider, context.Config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+IClientPtr CreateClient(
+ const TString& serverName,
+ const TCreateClientOptions& options)
+{
+ return NDetail::CreateClientImpl(serverName, options);
+}
+
+IClientPtr CreateClientFromEnv(const TCreateClientOptions& options)
+{
+ auto serverName = GetEnv("YT_PROXY");
+ if (!serverName) {
+ ythrow yexception() << "YT_PROXY is not set";
+ }
+
+ return NDetail::CreateClientImpl(serverName, options);
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client.h b/yt/cpp/mapreduce/client/client.h
new file mode 100644
index 0000000000..0f4df09d0b
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client.h
@@ -0,0 +1,506 @@
+#pragma once
+
+#include "client_reader.h"
+#include "client_writer.h"
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/interface/client.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYtPoller;
+
+class TClientBase;
+using TClientBasePtr = ::TIntrusivePtr<TClientBase>;
+
+class TClient;
+using TClientPtr = ::TIntrusivePtr<TClient>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientBase
+ : virtual public IClientBase
+{
+public:
+ TClientBase(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ IClientRetryPolicyPtr retryPolicy);
+
+ ITransactionPtr StartTransaction(
+ const TStartTransactionOptions& options) override;
+
+ // cypress
+
+ TNodeId Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options) override;
+
+ void Remove(
+ const TYPath& path,
+ const TRemoveOptions& options) override;
+
+ bool Exists(
+ const TYPath& path,
+ const TExistsOptions& options) override;
+
+ TNode Get(
+ const TYPath& path,
+ const TGetOptions& options) override;
+
+ void Set(
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options) override;
+
+ void MultisetAttributes(
+ const TYPath& path,
+ const TNode::TMapType& value,
+ const TMultisetAttributesOptions& options) override;
+
+ TNode::TListType List(
+ const TYPath& path,
+ const TListOptions& options) override;
+
+ TNodeId Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options) override;
+
+ TNodeId Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options) override;
+
+ TNodeId Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options) override;
+
+ void Concatenate(
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options) override;
+
+ TRichYPath CanonizeYPath(const TRichYPath& path) override;
+
+ TVector<TTableColumnarStatistics> GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options) override;
+
+ TMultiTablePartitions GetTablePartitions(
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options) override;
+
+ TMaybe<TYPath> GetFileFromCache(
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options = TGetFileFromCacheOptions()) override;
+
+ TYPath PutFileToCache(
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options = TPutFileToCacheOptions()) override;
+
+ IFileReaderPtr CreateFileReader(
+ const TRichYPath& path,
+ const TFileReaderOptions& options) override;
+
+ IFileWriterPtr CreateFileWriter(
+ const TRichYPath& path,
+ const TFileWriterOptions& options) override;
+
+ TTableWriterPtr<::google::protobuf::Message> CreateTableWriter(
+ const TRichYPath& path,
+ const ::google::protobuf::Descriptor& descriptor,
+ const TTableWriterOptions& options) override;
+
+ TRawTableReaderPtr CreateRawReader(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableReaderOptions& options) override;
+
+ TRawTableWriterPtr CreateRawWriter(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableWriterOptions& options) override;
+
+ IFileReaderPtr CreateBlobTableReader(
+ const TYPath& path,
+ const TKey& key,
+ const TBlobTableReaderOptions& options) override;
+
+ // operations
+
+ IOperationPtr DoMap(
+ const TMapOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ const TOperationOptions& options) override;
+
+ IOperationPtr RawMap(
+ const TRawMapOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ const TOperationOptions& options) override;
+
+ IOperationPtr DoReduce(
+ const TReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) override;
+
+ IOperationPtr RawReduce(
+ const TRawReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ const TOperationOptions& options) override;
+
+ IOperationPtr DoJoinReduce(
+ const TJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) override;
+
+ IOperationPtr RawJoinReduce(
+ const TRawJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ const TOperationOptions& options) override;
+
+ IOperationPtr DoMapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ ::TIntrusivePtr<IStructuredJob> reduceCombiner,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) override;
+
+ IOperationPtr RawMapReduce(
+ const TRawMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ ::TIntrusivePtr<IRawJob> reduceCombiner,
+ ::TIntrusivePtr<IRawJob> reducer,
+ const TOperationOptions& options) override;
+
+ IOperationPtr Sort(
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options) override;
+
+ IOperationPtr Merge(
+ const TMergeOperationSpec& spec,
+ const TOperationOptions& options) override;
+
+ IOperationPtr Erase(
+ const TEraseOperationSpec& spec,
+ const TOperationOptions& options) override;
+
+ IOperationPtr RemoteCopy(
+ const TRemoteCopyOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) override;
+
+ IOperationPtr RunVanilla(
+ const TVanillaOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) override;
+
+ IOperationPtr AttachOperation(const TOperationId& operationId) override;
+
+ EOperationBriefState CheckOperation(const TOperationId& operationId) override;
+
+ void AbortOperation(const TOperationId& operationId) override;
+
+ void CompleteOperation(const TOperationId& operationId) override;
+
+ void WaitForOperation(const TOperationId& operationId) override;
+
+ void AlterTable(
+ const TYPath& path,
+ const TAlterTableOptions& options) override;
+
+ TBatchRequestPtr CreateBatchRequest() override;
+
+ IClientPtr GetParentClient() override;
+
+ const TClientContext& GetContext() const;
+
+ const IClientRetryPolicyPtr& GetRetryPolicy() const;
+
+ virtual ITransactionPingerPtr GetTransactionPinger() = 0;
+
+protected:
+ virtual TClientPtr GetParentClientImpl() = 0;
+
+protected:
+ const TClientContext Context_;
+ TTransactionId TransactionId_;
+ IClientRetryPolicyPtr ClientRetryPolicy_;
+
+private:
+ ::TIntrusivePtr<TClientReader> CreateClientReader(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableReaderOptions& options,
+ bool useFormatFromTableAttributes = false);
+
+ THolder<TClientWriter> CreateClientWriter(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableWriterOptions& options);
+
+ ::TIntrusivePtr<INodeReaderImpl> CreateNodeReader(
+ const TRichYPath& path, const TTableReaderOptions& options) override;
+
+ ::TIntrusivePtr<IYaMRReaderImpl> CreateYaMRReader(
+ const TRichYPath& path, const TTableReaderOptions& options) override;
+
+ ::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const Message* prototype) override;
+
+ ::TIntrusivePtr<ISkiffRowReaderImpl> CreateSkiffRowReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const ISkiffRowSkipperPtr& skipper,
+ const NSkiff::TSkiffSchemaPtr& schema) override;
+
+ ::TIntrusivePtr<INodeWriterImpl> CreateNodeWriter(
+ const TRichYPath& path, const TTableWriterOptions& options) override;
+
+ ::TIntrusivePtr<IYaMRWriterImpl> CreateYaMRWriter(
+ const TRichYPath& path, const TTableWriterOptions& options) override;
+
+ ::TIntrusivePtr<IProtoWriterImpl> CreateProtoWriter(
+ const TRichYPath& path,
+ const TTableWriterOptions& options,
+ const Message* prototype) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTransaction
+ : public ITransaction
+ , public TClientBase
+{
+public:
+ //
+ // Start a new transaction.
+ TTransaction(
+ TClientPtr parentClient,
+ const TClientContext& context,
+ const TTransactionId& parentTransactionId,
+ const TStartTransactionOptions& options);
+
+ //
+ // Attach an existing transaction.
+ TTransaction(
+ TClientPtr parentClient,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TAttachTransactionOptions& options);
+
+ const TTransactionId& GetId() const override;
+
+ ILockPtr Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options) override;
+
+ void Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options) override;
+
+ void Commit() override;
+
+ void Abort() override;
+
+ void Ping() override;
+
+ void Detach() override;
+
+ ITransactionPingerPtr GetTransactionPinger() override;
+
+protected:
+ TClientPtr GetParentClientImpl() override;
+
+private:
+ ITransactionPingerPtr TransactionPinger_;
+ THolder<TPingableTransaction> PingableTx_;
+ TClientPtr ParentClient_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClient
+ : public IClient
+ , public TClientBase
+{
+public:
+ TClient(
+ const TClientContext& context,
+ const TTransactionId& globalId,
+ IClientRetryPolicyPtr retryPolicy);
+
+ ~TClient();
+
+ ITransactionPtr AttachTransaction(
+ const TTransactionId& transactionId,
+ const TAttachTransactionOptions& options) override;
+
+ void MountTable(
+ const TYPath& path,
+ const TMountTableOptions& options) override;
+
+ void UnmountTable(
+ const TYPath& path,
+ const TUnmountTableOptions& options) override;
+
+ void RemountTable(
+ const TYPath& path,
+ const TRemountTableOptions& options) override;
+
+ void FreezeTable(
+ const TYPath& path,
+ const TFreezeTableOptions& options) override;
+
+ void UnfreezeTable(
+ const TYPath& path,
+ const TUnfreezeTableOptions& options) override;
+
+ void ReshardTable(
+ const TYPath& path,
+ const TVector<TKey>& keys,
+ const TReshardTableOptions& options) override;
+
+ void ReshardTable(
+ const TYPath& path,
+ i64 tabletCount,
+ const TReshardTableOptions& options) override;
+
+ void InsertRows(
+ const TYPath& path,
+ const TNode::TListType& rows,
+ const TInsertRowsOptions& options) override;
+
+ void DeleteRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TDeleteRowsOptions& options) override;
+
+ void TrimRows(
+ const TYPath& path,
+ i64 tabletIndex,
+ i64 rowCount,
+ const TTrimRowsOptions& options) override;
+
+ TNode::TListType LookupRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TLookupRowsOptions& options) override;
+
+ TNode::TListType SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options) override;
+
+ void AlterTableReplica(
+ const TReplicaId& replicaId,
+ const TAlterTableReplicaOptions& alterTableReplicaOptions) override;
+
+ ui64 GenerateTimestamp() override;
+
+ TAuthorizationInfo WhoAmI() override;
+
+ TOperationAttributes GetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options) override;
+
+ TListOperationsResult ListOperations(
+ const TListOperationsOptions& options) override;
+
+ void UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options) override;
+
+ TJobAttributes GetJob(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options) override;
+
+ TListJobsResult ListJobs(
+ const TOperationId& operationId,
+ const TListJobsOptions& options = TListJobsOptions()) override;
+
+ IFileReaderPtr GetJobInput(
+ const TJobId& jobId,
+ const TGetJobInputOptions& options = TGetJobInputOptions()) override;
+
+ IFileReaderPtr GetJobFailContext(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobFailContextOptions& options = TGetJobFailContextOptions()) override;
+
+ IFileReaderPtr GetJobStderr(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& options = TGetJobStderrOptions()) override;
+
+ TNode::TListType SkyShareTable(
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options = TSkyShareTableOptions()) override;
+
+ TCheckPermissionResponse CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options) override;
+
+ TVector<TTabletInfo> GetTabletInfos(
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options) override;
+
+ void SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options) override;
+
+ void ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options) override;
+
+ void Shutdown() override;
+
+ ITransactionPingerPtr GetTransactionPinger() override;
+
+ // Helper methods
+ TYtPoller& GetYtPoller();
+
+protected:
+ TClientPtr GetParentClientImpl() override;
+
+private:
+ template <class TOptions>
+ void SetTabletParams(
+ THttpHeader& header,
+ const TYPath& path,
+ const TOptions& options);
+
+ void CheckShutdown() const;
+
+ ITransactionPingerPtr TransactionPinger_;
+
+ std::atomic<bool> Shutdown_ = false;
+ TMutex YtPollerLock_;
+ THolder<TYtPoller> YtPoller_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientPtr CreateClientImpl(
+ const TString& serverName,
+ const TCreateClientOptions& options = TCreateClientOptions());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client_reader.cpp b/yt/cpp/mapreduce/client/client_reader.cpp
new file mode 100644
index 0000000000..80759b12dc
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client_reader.cpp
@@ -0,0 +1,232 @@
+#include "client_reader.h"
+
+#include "structured_table_formats.h"
+#include "transaction.h"
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/io/helpers.h>
+#include <yt/cpp/mapreduce/io/yamr_table_reader.h>
+
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <library/cpp/yson/node/serialize.h>
+
+#include <util/random/random.h>
+#include <util/stream/file.h>
+#include <util/stream/str.h>
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+
+namespace NYT {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientReader::TClientReader(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFormat& format,
+ const TTableReaderOptions& options,
+ bool useFormatFromTableAttributes)
+ : Path_(path)
+ , ClientRetryPolicy_(std::move(clientRetryPolicy))
+ , Context_(context)
+ , ParentTransactionId_(transactionId)
+ , Format_(format)
+ , Options_(options)
+ , ReadTransaction_(nullptr)
+{
+ if (options.CreateTransaction_) {
+ Y_VERIFY(transactionPinger, "Internal error: transactionPinger is null");
+ ReadTransaction_ = MakeHolder<TPingableTransaction>(
+ ClientRetryPolicy_,
+ Context_,
+ transactionId,
+ transactionPinger->GetChildTxPinger(),
+ TStartTransactionOptions());
+ Path_.Path(Snapshot(
+ ClientRetryPolicy_,
+ Context_,
+ ReadTransaction_->GetId(),
+ path.Path_));
+ }
+
+ if (useFormatFromTableAttributes) {
+ auto transactionId2 = ReadTransaction_ ? ReadTransaction_->GetId() : ParentTransactionId_;
+ auto newFormat = GetTableFormat(ClientRetryPolicy_, Context_, transactionId2, Path_);
+ if (newFormat) {
+ Format_->Config = *newFormat;
+ }
+ }
+
+ TransformYPath();
+ CreateRequest();
+}
+
+bool TClientReader::Retry(
+ const TMaybe<ui32>& rangeIndex,
+ const TMaybe<ui64>& rowIndex)
+{
+ if (CurrentRequestRetryPolicy_) {
+ // TODO we should pass actual exception in Retry function
+ yexception genericError;
+ auto backoff = CurrentRequestRetryPolicy_->OnGenericError(genericError);
+ if (!backoff) {
+ return false;
+ }
+ }
+
+ try {
+ CreateRequest(rangeIndex, rowIndex);
+ return true;
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("Client reader retry failed: %v",
+ ex.what());
+
+ return false;
+ }
+}
+
+void TClientReader::ResetRetries()
+{
+ CurrentRequestRetryPolicy_ = nullptr;
+}
+
+size_t TClientReader::DoRead(void* buf, size_t len)
+{
+ return Input_->Read(buf, len);
+}
+
+void TClientReader::TransformYPath()
+{
+ for (auto& range : Path_.MutableRangesView()) {
+ auto& exact = range.Exact_;
+ if (IsTrivial(exact)) {
+ continue;
+ }
+
+ if (exact.RowIndex_) {
+ range.LowerLimit(TReadLimit().RowIndex(*exact.RowIndex_));
+ range.UpperLimit(TReadLimit().RowIndex(*exact.RowIndex_ + 1));
+ exact.RowIndex_.Clear();
+
+ } else if (exact.Key_) {
+ range.LowerLimit(TReadLimit().Key(*exact.Key_));
+
+ auto lastPart = TNode::CreateEntity();
+ lastPart.Attributes() = TNode()("type", "max");
+ exact.Key_->Parts_.push_back(lastPart);
+
+ range.UpperLimit(TReadLimit().Key(*exact.Key_));
+ exact.Key_.Clear();
+ }
+ }
+}
+
+void TClientReader::CreateRequest(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex)
+{
+ if (!CurrentRequestRetryPolicy_) {
+ CurrentRequestRetryPolicy_ = ClientRetryPolicy_->CreatePolicyForGenericRequest();
+ }
+ while (true) {
+ CurrentRequestRetryPolicy_->NotifyNewAttempt();
+
+ THttpHeader header("GET", GetReadTableCommand(Context_.Config->ApiVersion));
+ if (Context_.ServiceTicketAuth) {
+ header.SetServiceTicket(Context_.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(Context_.Token);
+ }
+ auto transactionId = (ReadTransaction_ ? ReadTransaction_->GetId() : ParentTransactionId_);
+ header.AddTransactionId(transactionId);
+
+ const auto& controlAttributes = Options_.ControlAttributes_;
+ header.AddParameter("control_attributes", TNode()
+ ("enable_row_index", controlAttributes.EnableRowIndex_)
+ ("enable_range_index", controlAttributes.EnableRangeIndex_));
+ header.SetOutputFormat(Format_);
+
+ header.SetResponseCompression(ToString(Context_.Config->AcceptEncoding));
+
+ if (rowIndex.Defined()) {
+ auto& ranges = Path_.MutableRanges();
+ if (ranges.Empty()) {
+ ranges.ConstructInPlace(TVector{TReadRange()});
+ } else {
+ if (rangeIndex.GetOrElse(0) >= ranges->size()) {
+ ythrow yexception()
+ << "range index " << rangeIndex.GetOrElse(0)
+ << " is out of range, input range count is " << ranges->size();
+ }
+ ranges->erase(ranges->begin(), ranges->begin() + rangeIndex.GetOrElse(0));
+ }
+ ranges->begin()->LowerLimit(TReadLimit().RowIndex(*rowIndex));
+ }
+
+ header.MergeParameters(FormIORequestParameters(Path_, Options_));
+
+ auto requestId = CreateGuidAsString();
+
+ try {
+ const auto proxyName = GetProxyForHeavyRequest(Context_);
+ Response_ = Context_.HttpClient->Request(GetFullUrl(proxyName, Context_, header), requestId, header);
+
+ Input_ = Response_->GetResponseStream();
+
+ YT_LOG_DEBUG("RSP %v - table stream", requestId);
+
+ return;
+ } catch (const TErrorResponse& e) {
+ LogRequestError(
+ requestId,
+ header,
+ e.what(),
+ CurrentRequestRetryPolicy_->GetAttemptDescription());
+
+ if (!IsRetriable(e)) {
+ throw;
+ }
+ auto backoff = CurrentRequestRetryPolicy_->OnRetriableError(e);
+ if (!backoff) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(*backoff);
+ } catch (const std::exception& e) {
+ LogRequestError(
+ requestId,
+ header,
+ e.what(),
+ CurrentRequestRetryPolicy_->GetAttemptDescription());
+
+ Response_.reset();
+ Input_ = nullptr;
+
+ auto backoff = CurrentRequestRetryPolicy_->OnGenericError(e);
+ if (!backoff) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(*backoff);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client_reader.h b/yt/cpp/mapreduce/client/client_reader.h
new file mode 100644
index 0000000000..22f5a0ebb0
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client_reader.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/http.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+
+namespace NYT {
+
+class TPingableTransaction;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientReader
+ : public TRawTableReader
+{
+public:
+ TClientReader(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFormat& format,
+ const TTableReaderOptions& options,
+ bool useFormatFromTableAttributes);
+
+ bool Retry(
+ const TMaybe<ui32>& rangeIndex,
+ const TMaybe<ui64>& rowIndex) override;
+
+ void ResetRetries() override;
+
+ bool HasRangeIndices() const override { return true; }
+
+protected:
+ size_t DoRead(void* buf, size_t len) override;
+
+private:
+ TRichYPath Path_;
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+ const TClientContext Context_;
+ TTransactionId ParentTransactionId_;
+ TMaybe<TFormat> Format_;
+ TTableReaderOptions Options_;
+
+ THolder<TPingableTransaction> ReadTransaction_;
+
+ NHttpClient::IHttpResponsePtr Response_;
+ IInputStream* Input_;
+
+ IRequestRetryPolicyPtr CurrentRequestRetryPolicy_;
+
+private:
+ void TransformYPath();
+ void CreateRequest(const TMaybe<ui32>& rangeIndex = Nothing(), const TMaybe<ui64>& rowIndex = Nothing());
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client_writer.cpp b/yt/cpp/mapreduce/client/client_writer.cpp
new file mode 100644
index 0000000000..357abd32eb
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client_writer.cpp
@@ -0,0 +1,69 @@
+#include "client_writer.h"
+
+#include "retryful_writer.h"
+#include "retryless_writer.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/common/fwd.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientWriter::TClientWriter(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TMaybe<TFormat>& format,
+ const TTableWriterOptions& options)
+ : BUFFER_SIZE(options.BufferSize_)
+{
+ if (options.SingleHttpRequest_) {
+ RawWriter_.Reset(new TRetrylessWriter(
+ context,
+ transactionId,
+ GetWriteTableCommand(context.Config->ApiVersion),
+ format,
+ path,
+ BUFFER_SIZE,
+ options));
+ } else {
+ RawWriter_.Reset(new TRetryfulWriter(
+ std::move(clientRetryPolicy),
+ std::move(transactionPinger),
+ context,
+ transactionId,
+ GetWriteTableCommand(context.Config->ApiVersion),
+ format,
+ path,
+ options));
+ }
+}
+
+size_t TClientWriter::GetStreamCount() const
+{
+ return 1;
+}
+
+IOutputStream* TClientWriter::GetStream(size_t tableIndex) const
+{
+ Y_UNUSED(tableIndex);
+ return RawWriter_.Get();
+}
+
+void TClientWriter::OnRowFinished(size_t)
+{
+ RawWriter_->NotifyRowEnd();
+}
+
+void TClientWriter::Abort()
+{
+ RawWriter_->Abort();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/client_writer.h b/yt/cpp/mapreduce/client/client_writer.h
new file mode 100644
index 0000000000..010a88a8ff
--- /dev/null
+++ b/yt/cpp/mapreduce/client/client_writer.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+struct TTableWriterOptions;
+class TRetryfulWriter;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientWriter
+ : public IProxyOutput
+{
+public:
+ TClientWriter(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TMaybe<TFormat>& format,
+ const TTableWriterOptions& options);
+
+ size_t GetStreamCount() const override;
+ IOutputStream* GetStream(size_t tableIndex) const override;
+ void OnRowFinished(size_t tableIndex) override;
+ void Abort() override;
+
+private:
+ ::TIntrusivePtr<TRawTableWriter> RawWriter_;
+
+ const size_t BUFFER_SIZE = 64 << 20;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/dummy_job_profiler.cpp b/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
new file mode 100644
index 0000000000..5a2f1e8d46
--- /dev/null
+++ b/yt/cpp/mapreduce/client/dummy_job_profiler.cpp
@@ -0,0 +1,26 @@
+#include "job_profiler.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDummyJobProfiler
+ : public IJobProfiler
+{
+ void Start() override
+ { }
+
+ void Stop() override
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IJobProfiler> CreateJobProfiler()
+{
+ return std::make_unique<TDummyJobProfiler>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/file_reader.cpp b/yt/cpp/mapreduce/client/file_reader.cpp
new file mode 100644
index 0000000000..fc21e0bc02
--- /dev/null
+++ b/yt/cpp/mapreduce/client/file_reader.cpp
@@ -0,0 +1,243 @@
+#include "file_reader.h"
+
+#include "transaction.h"
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/io/helpers.h>
+
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/http.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+namespace NYT {
+namespace NDetail {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TMaybe<ui64> GetEndOffset(const TFileReaderOptions& options) {
+ if (options.Length_) {
+ return options.Offset_.GetOrElse(0) + *options.Length_;
+ } else {
+ return Nothing();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStreamReaderBase::TStreamReaderBase(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId)
+ : Context_(context)
+ , ClientRetryPolicy_(std::move(clientRetryPolicy))
+ , ReadTransaction_(MakeHolder<TPingableTransaction>(
+ ClientRetryPolicy_,
+ context,
+ transactionId,
+ transactionPinger->GetChildTxPinger(),
+ TStartTransactionOptions()))
+{ }
+
+TStreamReaderBase::~TStreamReaderBase() = default;
+
+TYPath TStreamReaderBase::Snapshot(const TYPath& path)
+{
+ return NYT::Snapshot(ClientRetryPolicy_, Context_, ReadTransaction_->GetId(), path);
+}
+
+TString TStreamReaderBase::GetActiveRequestId() const
+{
+ if (Response_) {
+ return Response_->GetRequestId();;
+ } else {
+ return "<no-active-request>";
+ }
+}
+
+size_t TStreamReaderBase::DoRead(void* buf, size_t len)
+{
+ const int retryCount = Context_.Config->ReadRetryCount;
+ for (int attempt = 1; attempt <= retryCount; ++attempt) {
+ try {
+ if (!Input_) {
+ Response_ = Request(Context_, ReadTransaction_->GetId(), CurrentOffset_);
+ Input_ = Response_->GetResponseStream();
+ }
+ if (len == 0) {
+ return 0;
+ }
+ const size_t read = Input_->Read(buf, len);
+ CurrentOffset_ += read;
+ return read;
+ } catch (TErrorResponse& e) {
+ YT_LOG_ERROR("RSP %v - failed: %v (attempt %v of %v)",
+ GetActiveRequestId(),
+ e.what(),
+ attempt,
+ retryCount);
+
+ if (!IsRetriable(e) || attempt == retryCount) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(GetBackoffDuration(e, Context_.Config));
+ } catch (std::exception& e) {
+ YT_LOG_ERROR("RSP %v - failed: %v (attempt %v of %v)",
+ GetActiveRequestId(),
+ e.what(),
+ attempt,
+ retryCount);
+
+ // Invalidate connection.
+ Response_.reset();
+
+ if (attempt == retryCount) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(GetBackoffDuration(e, Context_.Config));
+ }
+ Input_ = nullptr;
+ }
+ Y_UNREACHABLE(); // we should either return or throw from loop above
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFileReader::TFileReader(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFileReaderOptions& options)
+ : TStreamReaderBase(std::move(clientRetryPolicy), std::move(transactionPinger), context, transactionId)
+ , FileReaderOptions_(options)
+ , Path_(path)
+ , StartOffset_(FileReaderOptions_.Offset_.GetOrElse(0))
+ , EndOffset_(GetEndOffset(FileReaderOptions_))
+{
+ Path_.Path_ = TStreamReaderBase::Snapshot(Path_.Path_);
+}
+
+NHttpClient::IHttpResponsePtr TFileReader::Request(const TClientContext& context, const TTransactionId& transactionId, ui64 readBytes)
+{
+ const ui64 currentOffset = StartOffset_ + readBytes;
+ TString hostName = GetProxyForHeavyRequest(context);
+
+ THttpHeader header("GET", GetReadFileCommand(context.Config->ApiVersion));
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+ header.AddTransactionId(transactionId);
+ header.SetOutputFormat(TMaybe<TFormat>()); // Binary format
+
+ if (EndOffset_) {
+ Y_VERIFY(*EndOffset_ >= currentOffset);
+ FileReaderOptions_.Length(*EndOffset_ - currentOffset);
+ }
+ FileReaderOptions_.Offset(currentOffset);
+ header.MergeParameters(FormIORequestParameters(Path_, FileReaderOptions_));
+
+ header.SetResponseCompression(ToString(context.Config->AcceptEncoding));
+
+ auto requestId = CreateGuidAsString();
+ NHttpClient::IHttpResponsePtr response;
+ try {
+ response = context.HttpClient->Request(GetFullUrl(hostName, context, header), requestId, header);
+ } catch (const std::exception& ex) {
+ LogRequestError(requestId, header, ex.what(), "");
+ throw;
+ }
+
+ YT_LOG_DEBUG("RSP %v - file stream",
+ requestId);
+
+ return response;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBlobTableReader::TBlobTableReader(
+ const TYPath& path,
+ const TKey& key,
+ IClientRetryPolicyPtr retryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TBlobTableReaderOptions& options)
+ : TStreamReaderBase(std::move(retryPolicy), std::move(transactionPinger), context, transactionId)
+ , Key_(key)
+ , Options_(options)
+{
+ Path_ = TStreamReaderBase::Snapshot(path);
+}
+
+NHttpClient::IHttpResponsePtr TBlobTableReader::Request(const TClientContext& context, const TTransactionId& transactionId, ui64 readBytes)
+{
+ TString hostName = GetProxyForHeavyRequest(context);
+
+ THttpHeader header("GET", "read_blob_table");
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+ header.AddTransactionId(transactionId);
+ header.SetOutputFormat(TMaybe<TFormat>()); // Binary format
+
+ const ui64 currentOffset = Options_.Offset_ + readBytes;
+ const i64 startPartIndex = currentOffset / Options_.PartSize_;
+ const ui64 skipBytes = currentOffset - Options_.PartSize_ * startPartIndex;
+ auto lowerLimitKey = Key_;
+ lowerLimitKey.Parts_.push_back(startPartIndex);
+ auto upperLimitKey = Key_;
+ upperLimitKey.Parts_.push_back(std::numeric_limits<i64>::max());
+ TNode params = PathToParamNode(TRichYPath(Path_).AddRange(TReadRange()
+ .LowerLimit(TReadLimit().Key(lowerLimitKey))
+ .UpperLimit(TReadLimit().Key(upperLimitKey))));
+ params["start_part_index"] = TNode(startPartIndex);
+ params["offset"] = skipBytes;
+ if (Options_.PartIndexColumnName_) {
+ params["part_index_column_name"] = *Options_.PartIndexColumnName_;
+ }
+ if (Options_.DataColumnName_) {
+ params["data_column_name"] = *Options_.DataColumnName_;
+ }
+ params["part_size"] = Options_.PartSize_;
+ header.MergeParameters(params);
+ header.SetResponseCompression(ToString(context.Config->AcceptEncoding));
+
+ auto requestId = CreateGuidAsString();
+ NHttpClient::IHttpResponsePtr response;
+ try {
+ response = context.HttpClient->Request(GetFullUrl(hostName, context, header), requestId, header);
+ } catch (const std::exception& ex) {
+ LogRequestError(requestId, header, ex.what(), "");
+ throw;
+ }
+
+ YT_LOG_DEBUG("RSP %v - blob table stream",
+ requestId);
+ return response;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/file_reader.h b/yt/cpp/mapreduce/client/file_reader.h
new file mode 100644
index 0000000000..d850008a31
--- /dev/null
+++ b/yt/cpp/mapreduce/client/file_reader.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+class IInputStream;
+
+namespace NYT {
+
+class THttpRequest;
+class TPingableTransaction;
+
+namespace NDetail {
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamReaderBase
+ : public IFileReader
+{
+public:
+ TStreamReaderBase(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId);
+
+ ~TStreamReaderBase();
+
+protected:
+ TYPath Snapshot(const TYPath& path);
+
+protected:
+ const TClientContext Context_;
+
+private:
+ size_t DoRead(void* buf, size_t len) override;
+ virtual NHttpClient::IHttpResponsePtr Request(const TClientContext& context, const TTransactionId& transactionId, ui64 readBytes) = 0;
+ TString GetActiveRequestId() const;
+
+private:
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+ TFileReaderOptions FileReaderOptions_;
+
+ NHttpClient::IHttpResponsePtr Response_;
+ IInputStream* Input_ = nullptr;
+
+ THolder<TPingableTransaction> ReadTransaction_;
+
+ ui64 CurrentOffset_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileReader
+ : public TStreamReaderBase
+{
+public:
+ TFileReader(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFileReaderOptions& options = TFileReaderOptions());
+
+private:
+ NHttpClient::IHttpResponsePtr Request(const TClientContext& context, const TTransactionId& transactionId, ui64 readBytes) override;
+
+private:
+ TFileReaderOptions FileReaderOptions_;
+
+ TRichYPath Path_;
+ const ui64 StartOffset_;
+ const TMaybe<ui64> EndOffset_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBlobTableReader
+ : public TStreamReaderBase
+{
+public:
+ TBlobTableReader(
+ const TYPath& path,
+ const TKey& key,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TBlobTableReaderOptions& options);
+
+private:
+ NHttpClient::IHttpResponsePtr Request(const TClientContext& context, const TTransactionId& transactionId, ui64 readBytes) override;
+
+private:
+ const TKey Key_;
+ const TBlobTableReaderOptions Options_;
+ TYPath Path_;
+};
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/file_writer.cpp b/yt/cpp/mapreduce/client/file_writer.cpp
new file mode 100644
index 0000000000..daf6461edd
--- /dev/null
+++ b/yt/cpp/mapreduce/client/file_writer.cpp
@@ -0,0 +1,60 @@
+#include "file_writer.h"
+
+#include <yt/cpp/mapreduce/io/helpers.h>
+#include <yt/cpp/mapreduce/interface/finish_or_die.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFileWriter::TFileWriter(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFileWriterOptions& options)
+ : RetryfulWriter_(
+ std::move(clientRetryPolicy),
+ std::move(transactionPinger),
+ context,
+ transactionId,
+ GetWriteFileCommand(context.Config->ApiVersion),
+ TMaybe<TFormat>(),
+ path,
+ options)
+{ }
+
+TFileWriter::~TFileWriter()
+{
+ NDetail::FinishOrDie(this, "TFileWriter");
+}
+
+void TFileWriter::DoWrite(const void* buf, size_t len)
+{
+ // If user tunes RetryBlockSize / DesiredChunkSize he expects
+ // us to send data exactly by RetryBlockSize. So behaviour of the writer is predictable.
+ //
+ // We want to avoid situation when size of sent data slightly exceeded DesiredChunkSize
+ // and server produced one chunk of desired size and one small chunk.
+ while (len > 0) {
+ const auto retryBlockRemainingSize = RetryfulWriter_.GetRetryBlockRemainingSize();
+ Y_VERIFY(retryBlockRemainingSize > 0);
+ const auto firstWriteLen = Min(len, retryBlockRemainingSize);
+ RetryfulWriter_.Write(buf, firstWriteLen);
+ RetryfulWriter_.NotifyRowEnd();
+ len -= firstWriteLen;
+ buf = static_cast<const char*>(buf) + firstWriteLen;
+ }
+}
+
+void TFileWriter::DoFinish()
+{
+ RetryfulWriter_.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/file_writer.h b/yt/cpp/mapreduce/client/file_writer.h
new file mode 100644
index 0000000000..f3b97b904e
--- /dev/null
+++ b/yt/cpp/mapreduce/client/file_writer.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "retryful_writer.h"
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileWriter
+ : public IFileWriter
+{
+public:
+ TFileWriter(
+ const TRichYPath& path,
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TFileWriterOptions& options = TFileWriterOptions());
+
+ ~TFileWriter() override;
+
+protected:
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFinish() override;
+
+private:
+ TRetryfulWriter RetryfulWriter_;
+ static const size_t BUFFER_SIZE = 64 << 20;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/format_hints.cpp b/yt/cpp/mapreduce/client/format_hints.cpp
new file mode 100644
index 0000000000..1f6eb173ad
--- /dev/null
+++ b/yt/cpp/mapreduce/client/format_hints.cpp
@@ -0,0 +1,84 @@
+#include "format_hints.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <util/string/builder.h>
+
+namespace NYT::NDetail {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void ApplyEnableTypeConversion(TFormat* format, const TFormatHints& formatHints)
+{
+ if (formatHints.EnableAllToStringConversion_) {
+ format->Config.Attributes()["enable_all_to_string_conversion"] = *formatHints.EnableAllToStringConversion_;
+ }
+ if (formatHints.EnableStringToAllConversion_) {
+ format->Config.Attributes()["enable_string_to_all_conversion"] = *formatHints.EnableStringToAllConversion_;
+ }
+ if (formatHints.EnableIntegralTypeConversion_) {
+ format->Config.Attributes()["enable_integral_type_conversion"] = *formatHints.EnableIntegralTypeConversion_;
+ }
+ if (formatHints.EnableIntegralToDoubleConversion_) {
+ format->Config.Attributes()["enable_integral_to_double_conversion"] = *formatHints.EnableIntegralToDoubleConversion_;
+ }
+ if (formatHints.EnableTypeConversion_) {
+ format->Config.Attributes()["enable_type_conversion"] = *formatHints.EnableTypeConversion_;
+ }
+}
+
+template <>
+void ApplyFormatHints<TNode>(TFormat* format, const TMaybe<TFormatHints>& formatHints)
+{
+ Y_VERIFY(format);
+ if (!formatHints) {
+ return;
+ }
+
+ ApplyEnableTypeConversion(format, *formatHints);
+
+ if (formatHints->SkipNullValuesForTNode_) {
+ Y_ENSURE_EX(
+ format->Config.AsString() == "yson",
+ TApiUsageError() << "SkipNullForTNode option must be used with yson format, actual format: " << format->Config.AsString());
+ format->Config.Attributes()["skip_null_values"] = formatHints->SkipNullValuesForTNode_;
+ }
+
+ if (formatHints->ComplexTypeMode_) {
+ Y_ENSURE_EX(
+ format->Config.AsString() == "yson",
+ TApiUsageError() << "ComplexTypeMode option must be used with yson format, actual format: "
+ << format->Config.AsString());
+ format->Config.Attributes()["complex_type_mode"] = ToString(*formatHints->ComplexTypeMode_);
+ }
+}
+
+template <>
+void ApplyFormatHints<TYaMRRow>(TFormat* format, const TMaybe<TFormatHints>& formatHints)
+{
+ Y_VERIFY(format);
+ if (!formatHints) {
+ return;
+ }
+
+ ythrow TApiUsageError() << "Yamr format currently has no supported format hints";
+}
+
+template <>
+void ApplyFormatHints<::google::protobuf::Message>(TFormat* format, const TMaybe<TFormatHints>& formatHints)
+{
+ Y_VERIFY(format);
+ if (!formatHints) {
+ return;
+ }
+
+ ythrow TApiUsageError() << "Protobuf format currently has no supported format hints";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/format_hints.h b/yt/cpp/mapreduce/client/format_hints.h
new file mode 100644
index 0000000000..f6576b1045
--- /dev/null
+++ b/yt/cpp/mapreduce/client/format_hints.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <util/generic/maybe.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TRow>
+void ApplyFormatHints(TFormat* format, const TMaybe<TFormatHints>& formatHints);
+
+template <>
+void ApplyFormatHints<TNode>(TFormat* format, const TMaybe<TFormatHints>& formatHints);
+
+template <>
+void ApplyFormatHints<TYaMRRow>(TFormat* format, const TMaybe<TFormatHints>& formatHints);
+
+template <>
+void ApplyFormatHints<::google::protobuf::Message>(TFormat* format, const TMaybe<TFormatHints>& formatHints);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/fwd.h b/yt/cpp/mapreduce/client/fwd.h
new file mode 100644
index 0000000000..d4449d4ac1
--- /dev/null
+++ b/yt/cpp/mapreduce/client/fwd.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <util/generic/ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPingableTransaction;
+
+class TClient;
+using TClientPtr = ::TIntrusivePtr<TClient>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/init.cpp b/yt/cpp/mapreduce/client/init.cpp
new file mode 100644
index 0000000000..c74598ba14
--- /dev/null
+++ b/yt/cpp/mapreduce/client/init.cpp
@@ -0,0 +1,280 @@
+#include "init.h"
+
+#include "abortable_registry.h"
+#include "job_profiler.h"
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/init.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <yt/cpp/mapreduce/interface/logging/logger.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/io/job_reader.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <library/cpp/sighandler/async_signals_handler.h>
+
+#include <util/folder/dirut.h>
+
+#include <util/generic/singleton.h>
+
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/type.h>
+
+#include <util/system/env.h>
+#include <util/system/thread.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void WriteVersionToLog()
+{
+ YT_LOG_INFO("Wrapper version: %v",
+ TProcessState::Get()->ClientVersion);
+}
+
+static TNode SecureVaultContents; // safe
+
+void InitializeSecureVault()
+{
+ SecureVaultContents = NodeFromYsonString(
+ GetEnv("YT_SECURE_VAULT", "{}"));
+}
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TNode& GetJobSecureVault()
+{
+ return SecureVaultContents;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbnormalTerminator
+{
+public:
+ TAbnormalTerminator() = default;
+
+ static void SetErrorTerminationHandler()
+ {
+ if (Instance().OldHandler_ != nullptr) {
+ return;
+ }
+
+ Instance().OldHandler_ = std::set_terminate(&TerminateHandler);
+
+ SetAsyncSignalFunction(SIGINT, SignalHandler);
+ SetAsyncSignalFunction(SIGTERM, SignalHandler);
+ }
+
+private:
+ static TAbnormalTerminator& Instance()
+ {
+ return *Singleton<TAbnormalTerminator>();
+ }
+
+ static void* Invoke(void* opaque)
+ {
+ (*reinterpret_cast<std::function<void()>*>(opaque))();
+ return nullptr;
+ }
+
+ static void TerminateWithTimeout(
+ const TDuration& timeout,
+ const std::function<void(void)>& exitFunction,
+ const TString& logMessage)
+ {
+ std::function<void()> threadFun = [=] {
+ YT_LOG_INFO("%v",
+ logMessage);
+ NDetail::TAbortableRegistry::Get()->AbortAllAndBlockForever();
+ };
+ TThread thread(TThread::TParams(Invoke, &threadFun).SetName("aborter"));
+ thread.Start();
+ thread.Detach();
+
+ Sleep(timeout);
+ exitFunction();
+ }
+
+ static void SignalHandler(int signalNumber)
+ {
+ TerminateWithTimeout(
+ TDuration::Seconds(5),
+ std::bind(_exit, -signalNumber),
+ ::TStringBuilder() << "Signal " << signalNumber << " received, aborting transactions. Waiting 5 seconds...");
+ }
+
+ static void TerminateHandler()
+ {
+ TerminateWithTimeout(
+ TDuration::Seconds(5),
+ [&] {
+ if (Instance().OldHandler_) {
+ Instance().OldHandler_();
+ } else {
+ abort();
+ }
+ },
+ ::TStringBuilder() << "Terminate called, aborting transactions. Waiting 5 seconds...");
+ }
+
+private:
+ std::terminate_handler OldHandler_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+EInitStatus& GetInitStatus()
+{
+ static EInitStatus initStatus = EInitStatus::NotInitialized;
+ return initStatus;
+}
+
+static void ElevateInitStatus(const EInitStatus newStatus) {
+ NDetail::GetInitStatus() = Max(NDetail::GetInitStatus(), newStatus);
+}
+
+void CommonInitialize(int argc, const char** argv)
+{
+ auto logLevelStr = to_lower(TConfig::Get()->LogLevel);
+ ILogger::ELevel logLevel;
+
+ if (!TryFromString(logLevelStr, logLevel)) {
+ Cerr << "Invalid log level: " << TConfig::Get()->LogLevel << Endl;
+ exit(1);
+ }
+
+ SetLogger(CreateStdErrLogger(logLevel));
+
+ TProcessState::Get()->SetCommandLine(argc, argv);
+}
+
+void NonJobInitialize(const TInitializeOptions& options)
+{
+ if (FromString<bool>(GetEnv("YT_CLEANUP_ON_TERMINATION", "0")) || options.CleanupOnTermination_) {
+ TAbnormalTerminator::SetErrorTerminationHandler();
+ }
+ if (options.WaitProxy_) {
+ NDetail::TWaitProxy::Get()->SetProxy(options.WaitProxy_);
+ }
+ WriteVersionToLog();
+}
+
+void ExecJob(int argc, const char** argv, const TInitializeOptions& options)
+{
+ // Now we are definitely in job.
+ // We take this setting from environment variable to be consistent with client code.
+ TConfig::Get()->UseClientProtobuf = IsTrue(GetEnv("YT_USE_CLIENT_PROTOBUF", ""));
+
+ auto execJobImpl = [&options](TString jobName, i64 outputTableCount, bool hasState) {
+ auto jobProfiler = CreateJobProfiler();
+ jobProfiler->Start();
+
+ InitializeSecureVault();
+
+ NDetail::OutputTableCount = static_cast<i64>(outputTableCount);
+
+ THolder<IInputStream> jobStateStream;
+ if (hasState) {
+ jobStateStream = MakeHolder<TIFStream>("jobstate");
+ } else {
+ jobStateStream = MakeHolder<TBufferStream>(0);
+ }
+
+ int ret = 1;
+ try {
+ ret = TJobFactory::Get()->GetJobFunction(jobName.data())(outputTableCount, *jobStateStream);
+ } catch (const TSystemError& ex) {
+ if (ex.Status() == EPIPE) {
+ // 32 == EPIPE, write number here so it's easier to grep this exit code in source files
+ exit(32);
+ }
+ throw;
+ }
+
+ jobProfiler->Stop();
+
+ if (options.JobOnExitFunction_) {
+ (*options.JobOnExitFunction_)();
+ }
+ exit(ret);
+ };
+
+ auto jobArguments = NodeFromYsonString(GetEnv("YT_JOB_ARGUMENTS", "#"));
+ if (jobArguments.HasValue()) {
+ execJobImpl(
+ jobArguments["job_name"].AsString(),
+ jobArguments["output_table_count"].AsInt64(),
+ jobArguments["has_state"].AsBool());
+ Y_UNREACHABLE();
+ }
+
+ TString jobType = argc >= 2 ? argv[1] : TString();
+ if (argc != 5 || jobType != "--yt-map" && jobType != "--yt-reduce") {
+ // We are inside job but probably using old API
+ // (i.e. both NYT::Initialize and NMR::Initialize are called).
+ WriteVersionToLog();
+ return;
+ }
+
+ TString jobName(argv[2]);
+ i64 outputTableCount = FromString<i64>(argv[3]);
+ int hasState = FromString<int>(argv[4]);
+ execJobImpl(jobName, outputTableCount, hasState);
+ Y_UNREACHABLE();
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+void JoblessInitialize(const TInitializeOptions& options)
+{
+ static const char* fakeArgv[] = {"unknown..."};
+ NDetail::CommonInitialize(1, fakeArgv);
+ NDetail::NonJobInitialize(options);
+ NDetail::ElevateInitStatus(NDetail::EInitStatus::JoblessInitialization);
+}
+
+void Initialize(int argc, const char* argv[], const TInitializeOptions& options)
+{
+ NDetail::CommonInitialize(argc, argv);
+
+ NDetail::ElevateInitStatus(NDetail::EInitStatus::FullInitialization);
+
+ const bool isInsideJob = !GetEnv("YT_JOB_ID").empty();
+ if (isInsideJob) {
+ NDetail::ExecJob(argc, argv, options);
+ } else {
+ NDetail::NonJobInitialize(options);
+ }
+}
+
+void Initialize(int argc, char* argv[], const TInitializeOptions& options)
+{
+ return Initialize(argc, const_cast<const char**>(argv), options);
+}
+
+void Initialize(const TInitializeOptions& options)
+{
+ static const char* fakeArgv[] = {"unknown..."};
+ Initialize(1, fakeArgv, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/init.h b/yt/cpp/mapreduce/client/init.h
new file mode 100644
index 0000000000..af2fc80e55
--- /dev/null
+++ b/yt/cpp/mapreduce/client/init.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/init.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class EInitStatus : int
+{
+ NotInitialized,
+ JoblessInitialization,
+ FullInitialization,
+};
+
+EInitStatus& GetInitStatus();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/job_profiler.h b/yt/cpp/mapreduce/client/job_profiler.h
new file mode 100644
index 0000000000..6532871380
--- /dev/null
+++ b/yt/cpp/mapreduce/client/job_profiler.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <memory>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IJobProfiler
+{
+ virtual ~IJobProfiler() = default;
+
+ //! Starts job profiling if corresponding options are set
+ //! in environment.
+ virtual void Start() = 0;
+
+ //! Stops profiling and sends profile to job proxy.
+ virtual void Stop() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IJobProfiler> CreateJobProfiler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/lock.cpp b/yt/cpp/mapreduce/client/lock.cpp
new file mode 100644
index 0000000000..88110f9266
--- /dev/null
+++ b/yt/cpp/mapreduce/client/lock.cpp
@@ -0,0 +1,105 @@
+#include "lock.h"
+
+#include "yt_poller.h"
+
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+
+#include <util/string/builder.h>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLockPollerItem
+ : public IYtPollerItem
+{
+public:
+ TLockPollerItem(const TLockId& lockId, ::NThreading::TPromise<void> acquired)
+ : LockStateYPath_("#" + GetGuidAsString(lockId) + "/@state")
+ , Acquired_(acquired)
+ { }
+
+ void PrepareRequest(TRawBatchRequest* batchRequest) override
+ {
+ LockState_ = batchRequest->Get(TTransactionId(), LockStateYPath_, TGetOptions());
+ }
+
+ EStatus OnRequestExecuted() override
+ {
+ try {
+ const auto& state = LockState_.GetValue().AsString();
+ if (state == "acquired") {
+ Acquired_.SetValue();
+ return PollBreak;
+ }
+ } catch (const TErrorResponse& e) {
+ if (!IsRetriable(e)) {
+ Acquired_.SetException(std::current_exception());
+ return PollBreak;
+ }
+ } catch (const std::exception& e) {
+ if (!IsRetriable(e)) {
+ Acquired_.SetException(std::current_exception());
+ return PollBreak;
+ }
+ }
+ return PollContinue;
+ }
+
+ void OnItemDiscarded() override
+ {
+ Acquired_.SetException(std::make_exception_ptr(yexception() << "Operation cancelled"));
+ }
+
+private:
+ const TString LockStateYPath_;
+ ::NThreading::TPromise<void> Acquired_;
+
+ ::NThreading::TFuture<TNode> LockState_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLock::TLock(const TLockId& lockId, TClientPtr client, bool waitable)
+ : LockId_(lockId)
+ , Client_(std::move(client))
+{
+ if (!waitable) {
+ Acquired_ = ::NThreading::MakeFuture();
+ }
+}
+
+const TLockId& TLock::GetId() const
+{
+ return LockId_;
+}
+
+TNodeId TLock::GetLockedNodeId() const
+{
+ auto nodeIdNode = Client_->Get(
+ ::TStringBuilder() << '#' << GetGuidAsString(LockId_) << "/@node_id",
+ TGetOptions());
+ return GetGuid(nodeIdNode.AsString());
+}
+
+const ::NThreading::TFuture<void>& TLock::GetAcquiredFuture() const
+{
+ if (!Acquired_) {
+ auto promise = ::NThreading::NewPromise<void>();
+ Client_->GetYtPoller().Watch(::MakeIntrusive<TLockPollerItem>(LockId_, promise));
+ Acquired_ = promise.GetFuture();
+ }
+ return *Acquired_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/lock.h b/yt/cpp/mapreduce/client/lock.h
new file mode 100644
index 0000000000..7e2c7a127d
--- /dev/null
+++ b/yt/cpp/mapreduce/client/lock.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "client.h"
+
+#include <yt/cpp/mapreduce/interface/client.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLock
+ : public ILock
+{
+public:
+ TLock(const TLockId& lockId, TClientPtr client, bool waitable);
+
+ virtual const TLockId& GetId() const override;
+ virtual TNodeId GetLockedNodeId() const override;
+ virtual const ::NThreading::TFuture<void>& GetAcquiredFuture() const override;
+
+private:
+ const TLockId LockId_;
+ mutable TMaybe<::NThreading::TFuture<void>> Acquired_;
+ TClientPtr Client_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/operation.cpp b/yt/cpp/mapreduce/client/operation.cpp
new file mode 100644
index 0000000000..fc1600c240
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation.cpp
@@ -0,0 +1,2981 @@
+#include "operation.h"
+
+#include "abortable_registry.h"
+#include "client.h"
+#include "operation_helpers.h"
+#include "operation_tracker.h"
+#include "transaction.h"
+#include "prepare_operation.h"
+#include "retry_heavy_write_request.h"
+#include "skiff.h"
+#include "structured_table_formats.h"
+#include "yt_poller.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/fluent.h>
+#include <yt/cpp/mapreduce/interface/format.h>
+#include <yt/cpp/mapreduce/interface/job_statistics.h>
+#include <yt/cpp/mapreduce/interface/protobuf_format.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/io/job_reader.h>
+#include <yt/cpp/mapreduce/io/job_writer.h>
+#include <yt/cpp/mapreduce/io/yamr_table_reader.h>
+#include <yt/cpp/mapreduce/io/yamr_table_writer.h>
+#include <yt/cpp/mapreduce/io/node_table_reader.h>
+#include <yt/cpp/mapreduce/io/node_table_writer.h>
+#include <yt/cpp/mapreduce/io/proto_table_reader.h>
+#include <yt/cpp/mapreduce/io/proto_table_writer.h>
+#include <yt/cpp/mapreduce/io/proto_helpers.h>
+#include <yt/cpp/mapreduce/io/skiff_table_reader.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <library/cpp/yson/node/serialize.h>
+
+#include <util/generic/hash_set.h>
+
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+
+#include <util/system/thread.h>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const ui64 DefaultExrtaTmpfsSize = 1024LL * 1024LL;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMapReduceOperationIo
+{
+ TVector<TRichYPath> Inputs;
+ TVector<TRichYPath> MapOutputs;
+ TVector<TRichYPath> Outputs;
+
+ TMaybe<TFormat> MapperInputFormat;
+ TMaybe<TFormat> MapperOutputFormat;
+
+ TMaybe<TFormat> ReduceCombinerInputFormat;
+ TMaybe<TFormat> ReduceCombinerOutputFormat;
+
+ TFormat ReducerInputFormat = TFormat::YsonBinary();
+ TFormat ReducerOutputFormat = TFormat::YsonBinary();
+
+ TVector<TSmallJobFile> MapperJobFiles;
+ TVector<TSmallJobFile> ReduceCombinerJobFiles;
+ TVector<TSmallJobFile> ReducerJobFiles;
+};
+
+template <typename T>
+void VerifyHasElements(const TVector<T>& paths, TStringBuf name)
+{
+ if (paths.empty()) {
+ ythrow TApiUsageError() << "no " << name << " table is specified";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVector<TSmallJobFile> CreateFormatConfig(
+ TMaybe<TSmallJobFile> inputConfig,
+ const TMaybe<TSmallJobFile>& outputConfig)
+{
+ TVector<TSmallJobFile> result;
+ if (inputConfig) {
+ result.push_back(std::move(*inputConfig));
+ }
+ if (outputConfig) {
+ result.push_back(std::move(*outputConfig));
+ }
+ return result;
+}
+
+template <typename T>
+ENodeReaderFormat NodeReaderFormatFromHintAndGlobalConfig(const TUserJobFormatHintsBase<T>& formatHints)
+{
+ auto result = TConfig::Get()->NodeReaderFormat;
+ if (formatHints.InputFormatHints_ && formatHints.InputFormatHints_->SkipNullValuesForTNode_) {
+ Y_ENSURE_EX(
+ result != ENodeReaderFormat::Skiff,
+ TApiUsageError() << "skiff format doesn't support SkipNullValuesForTNode format hint");
+ result = ENodeReaderFormat::Yson;
+ }
+ return result;
+}
+
+template <class TSpec>
+const TVector<TStructuredTablePath>& GetStructuredInputs(const TSpec& spec)
+{
+ if constexpr (std::is_same_v<TSpec, TVanillaTask>) {
+ static const TVector<TStructuredTablePath> empty;
+ return empty;
+ } else {
+ return spec.GetStructuredInputs();
+ }
+}
+
+template <class TSpec>
+const TVector<TStructuredTablePath>& GetStructuredOutputs(const TSpec& spec)
+{
+ return spec.GetStructuredOutputs();
+}
+
+template <class TSpec>
+const TMaybe<TFormatHints>& GetInputFormatHints(const TSpec& spec)
+{
+ if constexpr (std::is_same_v<TSpec, TVanillaTask>) {
+ static const TMaybe<TFormatHints> empty = Nothing();
+ return empty;
+ } else {
+ return spec.InputFormatHints_;
+ }
+}
+
+template <class TSpec>
+const TMaybe<TFormatHints>& GetOutputFormatHints(const TSpec& spec)
+{
+ return spec.OutputFormatHints_;
+}
+
+template <class TSpec>
+ENodeReaderFormat GetNodeReaderFormat(const TSpec& spec, bool allowSkiff)
+{
+ if constexpr (std::is_same<TSpec, TVanillaTask>::value) {
+ return ENodeReaderFormat::Yson;
+ } else {
+ return allowSkiff
+ ? NodeReaderFormatFromHintAndGlobalConfig(spec)
+ : ENodeReaderFormat::Yson;
+ }
+}
+
+static void SortColumnsToNames(const TSortColumns& sortColumns, THashSet<TString>* result)
+{
+ auto names = sortColumns.GetNames();
+ result->insert(names.begin(), names.end());
+}
+
+static THashSet<TString> SortColumnsToNames(const TSortColumns& sortColumns)
+{
+ THashSet<TString> columnNames;
+ SortColumnsToNames(sortColumns, &columnNames);
+ return columnNames;
+}
+
+THashSet<TString> GetColumnsUsedInOperation(const TJoinReduceOperationSpec& spec)
+{
+ return SortColumnsToNames(spec.JoinBy_);
+}
+
+THashSet<TString> GetColumnsUsedInOperation(const TReduceOperationSpec& spec) {
+ auto result = SortColumnsToNames(spec.SortBy_);
+ SortColumnsToNames(spec.ReduceBy_, &result);
+ if (spec.JoinBy_) {
+ SortColumnsToNames(*spec.JoinBy_, &result);
+ }
+ return result;
+}
+
+THashSet<TString> GetColumnsUsedInOperation(const TMapReduceOperationSpec& spec)
+{
+ auto result = SortColumnsToNames(spec.SortBy_);
+ SortColumnsToNames(spec.ReduceBy_, &result);
+ return result;
+}
+
+THashSet<TString> GetColumnsUsedInOperation(const TMapOperationSpec&)
+{
+ return THashSet<TString>();
+}
+
+THashSet<TString> GetColumnsUsedInOperation(const TVanillaTask&)
+{
+ return THashSet<TString>();
+}
+
+TStructuredJobTableList ApplyProtobufColumnFilters(
+ const TStructuredJobTableList& tableList,
+ const TOperationPreparer& preparer,
+ const THashSet<TString>& columnsUsedInOperations,
+ const TOperationOptions& options)
+{
+ bool hasInputQuery = options.Spec_.Defined() && options.Spec_->IsMap() && options.Spec_->HasKey("input_query");
+ if (hasInputQuery) {
+ return tableList;
+ }
+
+ auto isDynamic = BatchTransform(
+ CreateDefaultRequestRetryPolicy(preparer.GetContext().Config),
+ preparer.GetContext(),
+ tableList,
+ [&] (TRawBatchRequest& batch, const auto& table) {
+ return batch.Get(preparer.GetTransactionId(), table.RichYPath->Path_ + "/@dynamic", TGetOptions());
+ });
+
+ auto newTableList = tableList;
+ for (size_t tableIndex = 0; tableIndex < tableList.size(); ++tableIndex) {
+ if (isDynamic[tableIndex].AsBool()) {
+ continue;
+ }
+ auto& table = newTableList[tableIndex];
+ Y_VERIFY(table.RichYPath);
+ if (table.RichYPath->Columns_) {
+ continue;
+ }
+ if (!std::holds_alternative<TProtobufTableStructure>(table.Description)) {
+ continue;
+ }
+ const auto& descriptor = std::get<TProtobufTableStructure>(table.Description).Descriptor;
+ if (!descriptor) {
+ continue;
+ }
+ auto fromDescriptor = NDetail::InferColumnFilter(*descriptor);
+ if (!fromDescriptor) {
+ continue;
+ }
+ THashSet<TString> columns(fromDescriptor->begin(), fromDescriptor->end());
+ columns.insert(columnsUsedInOperations.begin(), columnsUsedInOperations.end());
+ table.RichYPath->Columns(TVector<TString>(columns.begin(), columns.end()));
+ }
+ return newTableList;
+}
+
+template <class TSpec>
+TSimpleOperationIo CreateSimpleOperationIo(
+ const IStructuredJob& structuredJob,
+ const TOperationPreparer& preparer,
+ const TSpec& spec,
+ const TOperationOptions& options,
+ bool allowSkiff)
+{
+ if (!std::holds_alternative<TVoidStructuredRowStream>(structuredJob.GetInputRowStreamDescription())) {
+ VerifyHasElements(GetStructuredInputs(spec), "input");
+ }
+
+ TUserJobFormatHints hints;
+ hints.InputFormatHints_ = GetInputFormatHints(spec);
+ hints.OutputFormatHints_ = GetOutputFormatHints(spec);
+ ENodeReaderFormat nodeReaderFormat = GetNodeReaderFormat(spec, allowSkiff);
+
+ return CreateSimpleOperationIoHelper(
+ structuredJob,
+ preparer,
+ options,
+ CanonizeStructuredTableList(preparer.GetContext(), GetStructuredInputs(spec)),
+ CanonizeStructuredTableList(preparer.GetContext(), GetStructuredOutputs(spec)),
+ hints,
+ nodeReaderFormat,
+ GetColumnsUsedInOperation(spec));
+}
+
+template <class T>
+TSimpleOperationIo CreateSimpleOperationIo(
+ const IJob& job,
+ const TOperationPreparer& preparer,
+ const TSimpleRawOperationIoSpec<T>& spec)
+{
+ auto getFormatOrDefault = [&] (const TMaybe<TFormat>& maybeFormat, const char* formatName) {
+ if (maybeFormat) {
+ return *maybeFormat;
+ } else if (spec.Format_) {
+ return *spec.Format_;
+ } else {
+ ythrow TApiUsageError() << "Neither " << formatName << "format nor default format is specified for raw operation";
+ }
+ };
+
+ auto inputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer.GetContext(), spec.GetInputs());
+ auto outputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer.GetContext(), spec.GetOutputs());
+
+ VerifyHasElements(inputs, "input");
+ VerifyHasElements(outputs, "output");
+
+ TUserJobFormatHints hints;
+
+ auto outputSchemas = PrepareOperation(
+ job,
+ TOperationPreparationContext(
+ inputs,
+ outputs,
+ preparer.GetContext(),
+ preparer.GetClientRetryPolicy(),
+ preparer.GetTransactionId()),
+ &inputs,
+ &outputs,
+ hints);
+
+ Y_VERIFY(outputs.size() == outputSchemas.size());
+ for (int i = 0; i < static_cast<int>(outputs.size()); ++i) {
+ if (!outputs[i].Schema_ && !outputSchemas[i].Columns().empty()) {
+ outputs[i].Schema_ = outputSchemas[i];
+ }
+ }
+
+ return TSimpleOperationIo {
+ inputs,
+ outputs,
+
+ getFormatOrDefault(spec.InputFormat_, "input"),
+ getFormatOrDefault(spec.OutputFormat_, "output"),
+
+ TVector<TSmallJobFile>{},
+ };
+}
+
+////////////////////////////////////////////////////////////////////
+
+TString GetJobStderrWithRetriesAndIgnoreErrors(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const size_t stderrTailSize,
+ const TGetJobStderrOptions& options = TGetJobStderrOptions())
+{
+ TString jobStderr;
+ try {
+ jobStderr = GetJobStderrWithRetries(
+ retryPolicy,
+ context,
+ operationId,
+ jobId,
+ options);
+ } catch (const TErrorResponse& e) {
+ YT_LOG_ERROR("Cannot get job stderr (OperationId: %v, JobId: %v, Error: %v)",
+ operationId,
+ jobId,
+ e.what());
+ }
+ if (jobStderr.size() > stderrTailSize) {
+ jobStderr = jobStderr.substr(jobStderr.size() - stderrTailSize, stderrTailSize);
+ }
+ return jobStderr;
+}
+
+TVector<TFailedJobInfo> GetFailedJobInfo(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TGetFailedJobInfoOptions& options)
+{
+ const auto listJobsResult = ListJobs(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ operationId,
+ TListJobsOptions()
+ .State(EJobState::Failed)
+ .Limit(options.MaxJobCount_));
+
+ const auto stderrTailSize = options.StderrTailSize_;
+
+ TVector<TFailedJobInfo> result;
+ for (const auto& job : listJobsResult.Jobs) {
+ auto& info = result.emplace_back();
+ Y_ENSURE(job.Id);
+ info.JobId = *job.Id;
+ info.Error = job.Error.GetOrElse(TYtError(TString("unknown error")));
+ if (job.StderrSize.GetOrElse(0) != 0) {
+ // There are cases when due to bad luck we cannot read stderr even if
+ // list_jobs reports that stderr_size > 0.
+ //
+ // Such errors don't have special error code
+ // so we ignore all errors and try our luck on other jobs.
+ info.Stderr = GetJobStderrWithRetriesAndIgnoreErrors(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ operationId,
+ *job.Id,
+ stderrTailSize);
+ }
+ }
+ return result;
+}
+
+struct TGetJobsStderrOptions
+{
+ using TSelf = TGetJobsStderrOptions;
+
+ // How many jobs to download. Which jobs will be chosen is undefined.
+ FLUENT_FIELD_DEFAULT(ui64, MaxJobCount, 10);
+
+ // How much of stderr should be downloaded.
+ FLUENT_FIELD_DEFAULT(ui64, StderrTailSize, 64 * 1024);
+};
+
+static TVector<TString> GetJobsStderr(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TGetJobsStderrOptions& options = TGetJobsStderrOptions())
+{
+ const auto listJobsResult = ListJobs(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ operationId,
+ TListJobsOptions().Limit(options.MaxJobCount_).WithStderr(true));
+ const auto stderrTailSize = options.StderrTailSize_;
+ TVector<TString> result;
+ for (const auto& job : listJobsResult.Jobs) {
+ result.push_back(
+ // There are cases when due to bad luck we cannot read stderr even if
+ // list_jobs reports that stderr_size > 0.
+ //
+ // Such errors don't have special error code
+ // so we ignore all errors and try our luck on other jobs.
+ GetJobStderrWithRetriesAndIgnoreErrors(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ operationId,
+ *job.Id,
+ stderrTailSize)
+ );
+ }
+ return result;
+}
+
+int CountIntermediateTables(const TStructuredJobTableList& tables)
+{
+ int result = 0;
+ for (const auto& table : tables) {
+ if (table.RichYPath) {
+ break;
+ }
+ ++result;
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSimpleOperationIo CreateSimpleOperationIoHelper(
+ const IStructuredJob& structuredJob,
+ const TOperationPreparer& preparer,
+ const TOperationOptions& options,
+ TStructuredJobTableList structuredInputs,
+ TStructuredJobTableList structuredOutputs,
+ TUserJobFormatHints hints,
+ ENodeReaderFormat nodeReaderFormat,
+ const THashSet<TString>& columnsUsedInOperations)
+{
+ auto intermediateInputTableCount = CountIntermediateTables(structuredInputs);
+ auto intermediateOutputTableCount = CountIntermediateTables(structuredOutputs);
+
+ auto jobSchemaInferenceResult = PrepareOperation(
+ structuredJob,
+ TOperationPreparationContext(
+ structuredInputs,
+ structuredOutputs,
+ preparer.GetContext(),
+ preparer.GetClientRetryPolicy(),
+ preparer.GetTransactionId()),
+ &structuredInputs,
+ &structuredOutputs,
+ hints);
+
+ TVector<TSmallJobFile> formatConfigList;
+ TFormatBuilder formatBuilder(preparer.GetClientRetryPolicy(), preparer.GetContext(), preparer.GetTransactionId(), options);
+
+ auto [inputFormat, inputFormatConfig] = formatBuilder.CreateFormat(
+ structuredJob,
+ EIODirection::Input,
+ structuredInputs,
+ hints.InputFormatHints_,
+ nodeReaderFormat,
+ /* allowFormatFromTableAttribute = */ true);
+
+ auto [outputFormat, outputFormatConfig] = formatBuilder.CreateFormat(
+ structuredJob,
+ EIODirection::Output,
+ structuredOutputs,
+ hints.OutputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute = */ false);
+
+ const bool inferOutputSchema = options.InferOutputSchema_.GetOrElse(preparer.GetContext().Config->InferTableSchema);
+
+ auto outputPaths = GetPathList(
+ TStructuredJobTableList(structuredOutputs.begin() + intermediateOutputTableCount, structuredOutputs.end()),
+ TVector<TTableSchema>(jobSchemaInferenceResult.begin() + intermediateOutputTableCount, jobSchemaInferenceResult.end()),
+ inferOutputSchema);
+
+ auto inputPaths = GetPathList(
+ ApplyProtobufColumnFilters(
+ TStructuredJobTableList(structuredInputs.begin() + intermediateInputTableCount, structuredInputs.end()),
+ preparer,
+ columnsUsedInOperations,
+ options),
+ /*schemaInferenceResult*/ Nothing(),
+ /*inferSchema*/ false);
+
+ return TSimpleOperationIo {
+ inputPaths,
+ outputPaths,
+
+ inputFormat,
+ outputFormat,
+
+ CreateFormatConfig(inputFormatConfig, outputFormatConfig)
+ };
+}
+
+EOperationBriefState CheckOperation(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId)
+{
+ auto attributes = GetOperation(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ operationId,
+ TGetOperationOptions().AttributeFilter(TOperationAttributeFilter()
+ .Add(EOperationAttribute::State)
+ .Add(EOperationAttribute::Result)));
+ Y_VERIFY(attributes.BriefState,
+ "get_operation for operation %s has not returned \"state\" field",
+ GetGuidAsString(operationId).Data());
+ if (*attributes.BriefState == EOperationBriefState::Completed) {
+ return EOperationBriefState::Completed;
+ } else if (*attributes.BriefState == EOperationBriefState::Aborted || *attributes.BriefState == EOperationBriefState::Failed) {
+ YT_LOG_ERROR("Operation %v %v (%v)",
+ operationId,
+ ToString(*attributes.BriefState),
+ ToString(TOperationExecutionTimeTracker::Get()->Finish(operationId)));
+
+ auto failedJobInfoList = GetFailedJobInfo(
+ clientRetryPolicy,
+ context,
+ operationId,
+ TGetFailedJobInfoOptions());
+
+ Y_VERIFY(attributes.Result && attributes.Result->Error);
+ ythrow TOperationFailedError(
+ *attributes.BriefState == EOperationBriefState::Aborted
+ ? TOperationFailedError::Aborted
+ : TOperationFailedError::Failed,
+ operationId,
+ *attributes.Result->Error,
+ failedJobInfoList);
+ }
+ return EOperationBriefState::InProgress;
+}
+
+void WaitForOperation(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId)
+{
+ const TDuration checkOperationStateInterval =
+ UseLocalModeOptimization(context, clientRetryPolicy)
+ ? Min(TDuration::MilliSeconds(100), context.Config->OperationTrackerPollPeriod)
+ : context.Config->OperationTrackerPollPeriod;
+
+ while (true) {
+ auto status = CheckOperation(clientRetryPolicy, context, operationId);
+ if (status == EOperationBriefState::Completed) {
+ YT_LOG_INFO("Operation %v completed (%v)",
+ operationId,
+ TOperationExecutionTimeTracker::Get()->Finish(operationId));
+ break;
+ }
+ TWaitProxy::Get()->Sleep(checkOperationStateInterval);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TNode BuildAutoMergeSpec(const TAutoMergeSpec& options)
+{
+ TNode result;
+ if (options.Mode_) {
+ result["mode"] = ToString(*options.Mode_);
+ }
+ if (options.MaxIntermediateChunkCount_) {
+ result["max_intermediate_chunk_count"] = *options.MaxIntermediateChunkCount_;
+ }
+ if (options.ChunkCountPerMergeJob_) {
+ result["chunk_count_per_merge_job"] = *options.ChunkCountPerMergeJob_;
+ }
+ if (options.ChunkSizeThreshold_) {
+ result["chunk_size_threshold"] = *options.ChunkSizeThreshold_;
+ }
+ return result;
+}
+
+TNode BuildJobProfilerSpec(const TJobProfilerSpec& profilerSpec)
+{
+ TNode result;
+ if (profilerSpec.ProfilingBinary_) {
+ result["binary"] = ToString(*profilerSpec.ProfilingBinary_);
+ }
+ if (profilerSpec.ProfilerType_) {
+ result["type"] = ToString(*profilerSpec.ProfilerType_);
+ }
+ if (profilerSpec.ProfilingProbability_) {
+ result["profiling_probability"] = *profilerSpec.ProfilingProbability_;
+ }
+ if (profilerSpec.SamplingFrequency_) {
+ result["sampling_frequency"] = *profilerSpec.SamplingFrequency_;
+ }
+
+ return result;
+}
+
+// Returns undefined node if resources doesn't contain any meaningful field
+TNode BuildSchedulerResourcesSpec(const TSchedulerResources& resources)
+{
+ TNode result;
+ if (resources.UserSlots().Defined()) {
+ result["user_slots"] = *resources.UserSlots();
+ }
+ if (resources.Cpu().Defined()) {
+ result["cpu"] = *resources.Cpu();
+ }
+ if (resources.Memory().Defined()) {
+ result["memory"] = *resources.Memory();
+ }
+ return result;
+}
+
+void BuildUserJobFluently(
+ const TJobPreparer& preparer,
+ const TMaybe<TFormat>& inputFormat,
+ const TMaybe<TFormat>& outputFormat,
+ TFluentMap fluent)
+{
+ const auto& userJobSpec = preparer.GetSpec();
+ TMaybe<i64> memoryLimit = userJobSpec.MemoryLimit_;
+ TMaybe<double> cpuLimit = userJobSpec.CpuLimit_;
+ TMaybe<ui16> portCount = userJobSpec.PortCount_;
+
+ // Use 1MB extra tmpfs size by default, it helps to detect job sandbox as tmp directory
+ // for standard python libraries. See YTADMINREQ-14505 for more details.
+ auto tmpfsSize = preparer.GetSpec().ExtraTmpfsSize_.GetOrElse(DefaultExrtaTmpfsSize);
+ if (preparer.ShouldMountSandbox()) {
+ tmpfsSize += preparer.GetTotalFileSize();
+ if (tmpfsSize == 0) {
+ // This can be a case for example when it is local mode and we don't upload binary.
+ // NOTE: YT doesn't like zero tmpfs size.
+ tmpfsSize = RoundUpFileSize(1);
+ }
+ memoryLimit = memoryLimit.GetOrElse(512ll << 20) + tmpfsSize;
+ }
+
+ fluent
+ .Item("file_paths").List(preparer.GetFiles())
+ .Item("command").Value(preparer.GetCommand())
+ .Item("class_name").Value(preparer.GetClassName())
+ .DoIf(!userJobSpec.Environment_.empty(), [&] (TFluentMap fluentMap) {
+ TNode environment;
+ for (const auto& item : userJobSpec.Environment_) {
+ environment[item.first] = item.second;
+ }
+ fluentMap.Item("environment").Value(environment);
+ })
+ .DoIf(userJobSpec.DiskSpaceLimit_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("disk_space_limit").Value(*userJobSpec.DiskSpaceLimit_);
+ })
+ .DoIf(inputFormat.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("input_format").Value(inputFormat->Config);
+ })
+ .DoIf(outputFormat.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("output_format").Value(outputFormat->Config);
+ })
+ .DoIf(memoryLimit.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("memory_limit").Value(*memoryLimit);
+ })
+ .DoIf(userJobSpec.MemoryReserveFactor_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("memory_reserve_factor").Value(*userJobSpec.MemoryReserveFactor_);
+ })
+ .DoIf(cpuLimit.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("cpu_limit").Value(*cpuLimit);
+ })
+ .DoIf(portCount.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("port_count").Value(*portCount);
+ })
+ .DoIf(userJobSpec.JobTimeLimit_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("job_time_limit").Value(userJobSpec.JobTimeLimit_->MilliSeconds());
+ })
+ .DoIf(userJobSpec.NetworkProject_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("network_project").Value(*userJobSpec.NetworkProject_);
+ })
+ .DoIf(preparer.ShouldMountSandbox(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("tmpfs_path").Value(".");
+ fluentMap.Item("tmpfs_size").Value(tmpfsSize);
+ fluentMap.Item("copy_files").Value(true);
+ })
+ .Item("profilers")
+ .BeginList()
+ .DoFor(userJobSpec.JobProfilers_, [&] (TFluentList list, const auto& jobProfiler) {
+ list.Item().Value(BuildJobProfilerSpec(jobProfiler));
+ })
+ .EndList();
+}
+
+template <typename T>
+void BuildCommonOperationPart(const TConfigPtr& config, const TOperationSpecBase<T>& baseSpec, const TOperationOptions& options, TFluentMap fluent)
+{
+ const TProcessState* properties = TProcessState::Get();
+ TString pool = config->Pool;
+
+ if (baseSpec.Pool_) {
+ pool = *baseSpec.Pool_;
+ }
+
+ fluent
+ .Item("started_by")
+ .BeginMap()
+ .Item("hostname").Value(properties->FqdnHostName)
+ .Item("pid").Value(properties->Pid)
+ .Item("user").Value(properties->UserName)
+ .Item("command").List(properties->CensoredCommandLine)
+ .Item("wrapper_version").Value(properties->ClientVersion)
+ .EndMap()
+ .DoIf(!pool.empty(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("pool").Value(pool);
+ })
+ .DoIf(baseSpec.Weight_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("weight").Value(*baseSpec.Weight_);
+ })
+ .DoIf(baseSpec.TimeLimit_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("time_limit").Value(baseSpec.TimeLimit_->MilliSeconds());
+ })
+ .DoIf(baseSpec.PoolTrees().Defined(), [&] (TFluentMap fluentMap) {
+ TNode poolTreesSpec = TNode::CreateList();
+ for (const auto& tree : *baseSpec.PoolTrees()) {
+ poolTreesSpec.Add(tree);
+ }
+ fluentMap.Item("pool_trees").Value(poolTreesSpec);
+ })
+ .DoIf(baseSpec.ResourceLimits().Defined(), [&] (TFluentMap fluentMap) {
+ auto resourceLimitsSpec = BuildSchedulerResourcesSpec(*baseSpec.ResourceLimits());
+ if (!resourceLimitsSpec.IsUndefined()) {
+ fluentMap.Item("resource_limits").Value(std::move(resourceLimitsSpec));
+ }
+ })
+ .DoIf(options.SecureVault_.Defined(), [&] (TFluentMap fluentMap) {
+ Y_ENSURE(options.SecureVault_->IsMap(),
+ "SecureVault must be a map node, got " << options.SecureVault_->GetType());
+ fluentMap.Item("secure_vault").Value(*options.SecureVault_);
+ })
+ .DoIf(baseSpec.Title_.Defined(), [&] (TFluentMap fluentMap) {
+ fluentMap.Item("title").Value(*baseSpec.Title_);
+ });
+}
+
+template <typename TSpec>
+void BuildCommonUserOperationPart(const TSpec& baseSpec, TNode* spec)
+{
+ if (baseSpec.MaxFailedJobCount_.Defined()) {
+ (*spec)["max_failed_job_count"] = *baseSpec.MaxFailedJobCount_;
+ }
+ if (baseSpec.FailOnJobRestart_.Defined()) {
+ (*spec)["fail_on_job_restart"] = *baseSpec.FailOnJobRestart_;
+ }
+ if (baseSpec.StderrTablePath_.Defined()) {
+ (*spec)["stderr_table_path"] = *baseSpec.StderrTablePath_;
+ }
+ if (baseSpec.CoreTablePath_.Defined()) {
+ (*spec)["core_table_path"] = *baseSpec.CoreTablePath_;
+ }
+ if (baseSpec.WaitingJobTimeout_.Defined()) {
+ (*spec)["waiting_job_timeout"] = baseSpec.WaitingJobTimeout_->MilliSeconds();
+ }
+}
+
+template <typename TSpec>
+void BuildJobCountOperationPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.JobCount_.Defined()) {
+ (*nodeSpec)["job_count"] = *spec.JobCount_;
+ }
+ if (spec.DataSizePerJob_.Defined()) {
+ (*nodeSpec)["data_size_per_job"] = *spec.DataSizePerJob_;
+ }
+}
+
+template <typename TSpec>
+void BuildPartitionCountOperationPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.PartitionCount_.Defined()) {
+ (*nodeSpec)["partition_count"] = *spec.PartitionCount_;
+ }
+ if (spec.PartitionDataSize_.Defined()) {
+ (*nodeSpec)["partition_data_size"] = *spec.PartitionDataSize_;
+ }
+}
+
+template <typename TSpec>
+void BuildDataSizePerSortJobPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.DataSizePerSortJob_.Defined()) {
+ (*nodeSpec)["data_size_per_sort_job"] = *spec.DataSizePerSortJob_;
+ }
+}
+
+template <typename TSpec>
+void BuildPartitionJobCountOperationPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.PartitionJobCount_.Defined()) {
+ (*nodeSpec)["partition_job_count"] = *spec.PartitionJobCount_;
+ }
+ if (spec.DataSizePerPartitionJob_.Defined()) {
+ (*nodeSpec)["data_size_per_partition_job"] = *spec.DataSizePerPartitionJob_;
+ }
+}
+
+template <typename TSpec>
+void BuildMapJobCountOperationPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.MapJobCount_.Defined()) {
+ (*nodeSpec)["map_job_count"] = *spec.MapJobCount_;
+ }
+ if (spec.DataSizePerMapJob_.Defined()) {
+ (*nodeSpec)["data_size_per_map_job"] = *spec.DataSizePerMapJob_;
+ }
+}
+
+template <typename TSpec>
+void BuildIntermediateDataPart(const TSpec& spec, TNode* nodeSpec)
+{
+ if (spec.IntermediateDataAccount_.Defined()) {
+ (*nodeSpec)["intermediate_data_account"] = *spec.IntermediateDataAccount_;
+ }
+ if (spec.IntermediateDataReplicationFactor_.Defined()) {
+ (*nodeSpec)["intermediate_data_replication_factor"] = *spec.IntermediateDataReplicationFactor_;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNode MergeSpec(TNode dst, TNode spec, const TOperationOptions& options)
+{
+ MergeNodes(dst["spec"], spec);
+ if (options.Spec_) {
+ MergeNodes(dst["spec"], *options.Spec_);
+ }
+ return dst;
+}
+
+template <typename TSpec>
+void CreateDebugOutputTables(const TSpec& spec, const TOperationPreparer& preparer)
+{
+ if (spec.StderrTablePath_.Defined()) {
+ NYT::NDetail::Create(
+ preparer.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ preparer.GetContext(),
+ TTransactionId(),
+ *spec.StderrTablePath_,
+ NT_TABLE,
+ TCreateOptions()
+ .IgnoreExisting(true)
+ .Recursive(true));
+ }
+ if (spec.CoreTablePath_.Defined()) {
+ NYT::NDetail::Create(
+ preparer.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ preparer.GetContext(),
+ TTransactionId(),
+ *spec.CoreTablePath_,
+ NT_TABLE,
+ TCreateOptions()
+ .IgnoreExisting(true)
+ .Recursive(true));
+ }
+}
+
+void CreateOutputTable(
+ const TOperationPreparer& preparer,
+ const TRichYPath& path)
+{
+ Y_ENSURE(path.Path_, "Output table is not set");
+ Create(
+ preparer.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ preparer.GetContext(), preparer.GetTransactionId(), path.Path_, NT_TABLE,
+ TCreateOptions()
+ .IgnoreExisting(true)
+ .Recursive(true));
+}
+
+void CreateOutputTables(
+ const TOperationPreparer& preparer,
+ const TVector<TRichYPath>& paths)
+{
+ for (auto& path : paths) {
+ CreateOutputTable(preparer, path);
+ }
+}
+
+void CheckInputTablesExist(
+ const TOperationPreparer& preparer,
+ const TVector<TRichYPath>& paths)
+{
+ Y_ENSURE(!paths.empty(), "Input tables are not set");
+ for (auto& path : paths) {
+ auto curTransactionId = path.TransactionId_.GetOrElse(preparer.GetTransactionId());
+ Y_ENSURE_EX(
+ Exists(
+ preparer.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ preparer.GetContext(),
+ curTransactionId,
+ path.Path_),
+ TApiUsageError() << "Input table '" << path.Path_ << "' doesn't exist");
+ }
+}
+
+void LogJob(const TOperationId& opId, const IJob* job, const char* type)
+{
+ if (job) {
+ YT_LOG_INFO("Operation %v; %v = %v",
+ opId,
+ type,
+ TJobFactory::Get()->GetJobName(job));
+ }
+}
+
+void LogYPaths(const TOperationId& opId, const TVector<TRichYPath>& paths, const char* type)
+{
+ for (size_t i = 0; i < paths.size(); ++i) {
+ YT_LOG_INFO("Operation %v; %v[%v] = %v",
+ opId,
+ type,
+ i,
+ paths[i].Path_);
+ }
+}
+
+void LogYPath(const TOperationId& opId, const TRichYPath& path, const char* type)
+{
+ YT_LOG_INFO("Operation %v; %v = %v",
+ opId,
+ type,
+ path.Path_);
+}
+
+TString AddModeToTitleIfDebug(const TString& title) {
+#ifndef NDEBUG
+ return title + " (debug build)";
+#else
+ return title;
+#endif
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void DoExecuteMap(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TSimpleOperationIo& operationIo,
+ TMapOperationSpecBase<T> spec,
+ const IJobPtr& mapper,
+ const TOperationOptions& options)
+{
+ if (options.CreateDebugOutputTables_) {
+ CreateDebugOutputTables(spec, *preparer);
+ }
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, operationIo.Inputs);
+ CreateOutputTables(*preparer, operationIo.Outputs);
+ }
+
+ TJobPreparer map(
+ *preparer,
+ spec.MapperSpec_,
+ *mapper,
+ operationIo.Outputs.size(),
+ operationIo.JobFiles,
+ options);
+
+ spec.Title_ = spec.Title_.GetOrElse(AddModeToTitleIfDebug(map.GetClassName()));
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("mapper").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ map,
+ operationIo.InputFormat,
+ operationIo.OutputFormat,
+ fluent);
+ })
+ .DoIf(spec.AutoMerge_.Defined(), [&] (TFluentMap fluent) {
+ auto autoMergeSpec = BuildAutoMergeSpec(*spec.AutoMerge_);
+ if (!autoMergeSpec.IsUndefined()) {
+ fluent.Item("auto_merge").Value(std::move(autoMergeSpec));
+ }
+ })
+ .Item("input_table_paths").List(operationIo.Inputs)
+ .Item("output_table_paths").List(operationIo.Outputs)
+ .DoIf(spec.Ordered_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("ordered").Value(spec.Ordered_.GetRef());
+ })
+ .Do(std::bind(BuildCommonOperationPart<T>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ specNode["spec"]["job_io"]["control_attributes"]["enable_row_index"] = TNode(true);
+ specNode["spec"]["job_io"]["control_attributes"]["enable_range_index"] = TNode(true);
+ if (!preparer->GetContext().Config->TableWriter.Empty()) {
+ specNode["spec"]["job_io"]["table_writer"] = preparer->GetContext().Config->TableWriter;
+ }
+
+ BuildCommonUserOperationPart(spec, &specNode["spec"]);
+ BuildJobCountOperationPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ operationIo,
+ mapper
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "map", spec);
+
+ LogJob(operationId, mapper.Get(), "mapper");
+ LogYPaths(operationId, operationIo.Inputs, "input");
+ LogYPaths(operationId, operationIo.Outputs, "output");
+
+ return operationId;
+ };
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteMap(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMapOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& mapper,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting map operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*mapper, *preparer, spec, options, /* allowSkiff = */ true);
+ DoExecuteMap(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ mapper,
+ options);
+}
+
+void ExecuteRawMap(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawMapOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& mapper,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting raw map operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*mapper, *preparer, spec);
+ DoExecuteMap(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ mapper,
+ options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void DoExecuteReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TSimpleOperationIo& operationIo,
+ TReduceOperationSpecBase<T> spec,
+ const IJobPtr& reducer,
+ const TOperationOptions& options)
+{
+ if (options.CreateDebugOutputTables_) {
+ CreateDebugOutputTables(spec, *preparer);
+ }
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, operationIo.Inputs);
+ CreateOutputTables(*preparer, operationIo.Outputs);
+ }
+
+ TJobPreparer reduce(
+ *preparer,
+ spec.ReducerSpec_,
+ *reducer,
+ operationIo.Outputs.size(),
+ operationIo.JobFiles,
+ options);
+
+ spec.Title_ = spec.Title_.GetOrElse(AddModeToTitleIfDebug(reduce.GetClassName()));
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("reducer").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ reduce,
+ operationIo.InputFormat,
+ operationIo.OutputFormat,
+ fluent);
+ })
+ .Item("sort_by").Value(spec.SortBy_)
+ .Item("reduce_by").Value(spec.ReduceBy_)
+ .DoIf(spec.JoinBy_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("join_by").Value(spec.JoinBy_.GetRef());
+ })
+ .DoIf(spec.EnableKeyGuarantee_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("enable_key_guarantee").Value(spec.EnableKeyGuarantee_.GetRef());
+ })
+ .Item("input_table_paths").List(operationIo.Inputs)
+ .Item("output_table_paths").List(operationIo.Outputs)
+ .Item("job_io").BeginMap()
+ .Item("control_attributes").BeginMap()
+ .Item("enable_key_switch").Value(true)
+ .Item("enable_row_index").Value(true)
+ .Item("enable_range_index").Value(true)
+ .EndMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&] (TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .EndMap()
+ .DoIf(spec.AutoMerge_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("auto_merge").Value(BuildAutoMergeSpec(*spec.AutoMerge_));
+ })
+ .Do(std::bind(BuildCommonOperationPart<T>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ BuildCommonUserOperationPart(spec, &specNode["spec"]);
+ BuildJobCountOperationPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ operationIo,
+ reducer
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "reduce", spec);
+
+ LogJob(operationId, reducer.Get(), "reducer");
+ LogYPaths(operationId, operationIo.Inputs, "input");
+ LogYPaths(operationId, operationIo.Outputs, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*reducer, *preparer, spec, options, /* allowSkiff = */ false);
+ DoExecuteReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ reducer,
+ options);
+}
+
+void ExecuteRawReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting raw reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*reducer, *preparer, spec);
+ DoExecuteReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ reducer,
+ options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void DoExecuteJoinReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TSimpleOperationIo& operationIo,
+ TJoinReduceOperationSpecBase<T> spec,
+ const IJobPtr& reducer,
+ const TOperationOptions& options)
+{
+ if (options.CreateDebugOutputTables_) {
+ CreateDebugOutputTables(spec, *preparer);
+ }
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, operationIo.Inputs);
+ CreateOutputTables(*preparer, operationIo.Outputs);
+ }
+
+ TJobPreparer reduce(
+ *preparer,
+ spec.ReducerSpec_,
+ *reducer,
+ operationIo.Outputs.size(),
+ operationIo.JobFiles,
+ options);
+
+ spec.Title_ = spec.Title_.GetOrElse(AddModeToTitleIfDebug(reduce.GetClassName()));
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("reducer").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ reduce,
+ operationIo.InputFormat,
+ operationIo.OutputFormat,
+ fluent);
+ })
+ .Item("join_by").Value(spec.JoinBy_)
+ .Item("input_table_paths").List(operationIo.Inputs)
+ .Item("output_table_paths").List(operationIo.Outputs)
+ .Item("job_io").BeginMap()
+ .Item("control_attributes").BeginMap()
+ .Item("enable_key_switch").Value(true)
+ .Item("enable_row_index").Value(true)
+ .Item("enable_range_index").Value(true)
+ .EndMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&] (TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .EndMap()
+ .Do(std::bind(BuildCommonOperationPart<T>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ BuildCommonUserOperationPart(spec, &specNode["spec"]);
+ BuildJobCountOperationPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ reducer,
+ operationIo
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "join_reduce", spec);
+
+ LogJob(operationId, reducer.Get(), "reducer");
+ LogYPaths(operationId, operationIo.Inputs, "input");
+ LogYPaths(operationId, operationIo.Outputs, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteJoinReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TJoinReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting join reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*reducer, *preparer, spec, options, /* allowSkiff = */ false);
+ return DoExecuteJoinReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ reducer,
+ options);
+}
+
+void ExecuteRawJoinReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawJoinReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting raw join reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto operationIo = CreateSimpleOperationIo(*reducer, *preparer, spec);
+ return DoExecuteJoinReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ reducer,
+ options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void DoExecuteMapReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMapReduceOperationIo& operationIo,
+ TMapReduceOperationSpecBase<T> spec,
+ const IJobPtr& mapper,
+ const IJobPtr& reduceCombiner,
+ const IJobPtr& reducer,
+ const TOperationOptions& options)
+{
+ TVector<TRichYPath> allOutputs;
+ allOutputs.insert(allOutputs.end(), operationIo.MapOutputs.begin(), operationIo.MapOutputs.end());
+ allOutputs.insert(allOutputs.end(), operationIo.Outputs.begin(), operationIo.Outputs.end());
+
+ if (options.CreateDebugOutputTables_) {
+ CreateDebugOutputTables(spec, *preparer);
+ }
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, operationIo.Inputs);
+ CreateOutputTables(*preparer, allOutputs);
+ }
+
+ TSortColumns sortBy = spec.SortBy_;
+ TSortColumns reduceBy = spec.ReduceBy_;
+
+ if (sortBy.Parts_.empty()) {
+ sortBy = reduceBy;
+ }
+
+ const bool hasMapper = mapper != nullptr;
+ const bool hasCombiner = reduceCombiner != nullptr;
+
+ TVector<TRichYPath> files;
+
+ TJobPreparer reduce(
+ *preparer,
+ spec.ReducerSpec_,
+ *reducer,
+ operationIo.Outputs.size(),
+ operationIo.ReducerJobFiles,
+ options);
+
+ TString title;
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .DoIf(hasMapper, [&] (TFluentMap fluent) {
+ TJobPreparer map(
+ *preparer,
+ spec.MapperSpec_,
+ *mapper,
+ 1 + operationIo.MapOutputs.size(),
+ operationIo.MapperJobFiles,
+ options);
+ fluent.Item("mapper").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ std::cref(map),
+ *operationIo.MapperInputFormat,
+ *operationIo.MapperOutputFormat,
+ fluent);
+ });
+
+ title = "mapper:" + map.GetClassName() + " ";
+ })
+ .DoIf(hasCombiner, [&] (TFluentMap fluent) {
+ TJobPreparer combine(
+ *preparer,
+ spec.ReduceCombinerSpec_,
+ *reduceCombiner,
+ size_t(1),
+ operationIo.ReduceCombinerJobFiles,
+ options);
+ fluent.Item("reduce_combiner").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ combine,
+ *operationIo.ReduceCombinerInputFormat,
+ *operationIo.ReduceCombinerOutputFormat,
+ fluent);
+ });
+ title += "combiner:" + combine.GetClassName() + " ";
+ })
+ .Item("reducer").DoMap([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ reduce,
+ operationIo.ReducerInputFormat,
+ operationIo.ReducerOutputFormat,
+ fluent);
+ })
+ .Item("sort_by").Value(sortBy)
+ .Item("reduce_by").Value(reduceBy)
+ .Item("input_table_paths").List(operationIo.Inputs)
+ .Item("output_table_paths").List(allOutputs)
+ .Item("mapper_output_table_count").Value(operationIo.MapOutputs.size())
+ .DoIf(spec.ForceReduceCombiners_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("force_reduce_combiners").Value(*spec.ForceReduceCombiners_);
+ })
+ .Item("map_job_io").BeginMap()
+ .Item("control_attributes").BeginMap()
+ .Item("enable_row_index").Value(true)
+ .Item("enable_range_index").Value(true)
+ .EndMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&] (TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .EndMap()
+ .Item("sort_job_io").BeginMap()
+ .Item("control_attributes").BeginMap()
+ .Item("enable_key_switch").Value(true)
+ .EndMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&] (TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .EndMap()
+ .Item("reduce_job_io").BeginMap()
+ .Item("control_attributes").BeginMap()
+ .Item("enable_key_switch").Value(true)
+ .EndMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&] (TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .EndMap()
+ .Do([&] (TFluentMap) {
+ spec.Title_ = spec.Title_.GetOrElse(AddModeToTitleIfDebug(title + "reducer:" + reduce.GetClassName()));
+ })
+ .Do(std::bind(BuildCommonOperationPart<T>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ if (spec.Ordered_) {
+ specNode["spec"]["ordered"] = *spec.Ordered_;
+ }
+
+ BuildCommonUserOperationPart(spec, &specNode["spec"]);
+ BuildMapJobCountOperationPart(spec, &specNode["spec"]);
+ BuildPartitionCountOperationPart(spec, &specNode["spec"]);
+ BuildIntermediateDataPart(spec, &specNode["spec"]);
+ BuildDataSizePerSortJobPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ mapper,
+ reduceCombiner,
+ reducer,
+ inputs=operationIo.Inputs,
+ allOutputs
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "map_reduce", spec);
+
+ LogJob(operationId, mapper.Get(), "mapper");
+ LogJob(operationId, reduceCombiner.Get(), "reduce_combiner");
+ LogJob(operationId, reducer.Get(), "reducer");
+ LogYPaths(operationId, inputs, "input");
+ LogYPaths(operationId, allOutputs, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteMapReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMapReduceOperationSpec& spec_,
+ const ::TIntrusivePtr<IStructuredJob>& mapper,
+ const ::TIntrusivePtr<IStructuredJob>& reduceCombiner,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting map-reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ TMapReduceOperationSpec spec = spec_;
+
+ TMapReduceOperationIo operationIo;
+ auto structuredInputs = CanonizeStructuredTableList(preparer->GetContext(), spec.GetStructuredInputs());
+ auto structuredMapOutputs = CanonizeStructuredTableList(preparer->GetContext(), spec.GetStructuredMapOutputs());
+ auto structuredOutputs = CanonizeStructuredTableList(preparer->GetContext(), spec.GetStructuredOutputs());
+
+ const bool inferOutputSchema = options.InferOutputSchema_.GetOrElse(preparer->GetContext().Config->InferTableSchema);
+
+ TVector<TTableSchema> currentInferenceResult;
+
+ auto fixSpec = [&] (const TFormat& format) {
+ if (format.IsYamredDsv()) {
+ spec.SortBy_.Parts_.clear();
+ spec.ReduceBy_.Parts_.clear();
+
+ const TYamredDsvAttributes attributes = format.GetYamredDsvAttributes();
+ for (auto& column : attributes.KeyColumnNames) {
+ spec.SortBy_.Parts_.push_back(column);
+ spec.ReduceBy_.Parts_.push_back(column);
+ }
+ for (const auto& column : attributes.SubkeyColumnNames) {
+ spec.SortBy_.Parts_.push_back(column);
+ }
+ }
+ };
+
+ VerifyHasElements(structuredInputs, "inputs");
+
+ TFormatBuilder formatBuilder(
+ preparer->GetClientRetryPolicy(),
+ preparer->GetContext(),
+ preparer->GetTransactionId(),
+ options);
+
+ if (mapper) {
+ auto mapperOutputDescription =
+ spec.GetIntermediateMapOutputDescription()
+ .GetOrElse(TUnspecifiedTableStructure());
+ TStructuredJobTableList mapperOutput = {
+ TStructuredJobTable::Intermediate(mapperOutputDescription),
+ };
+
+ for (const auto& table : structuredMapOutputs) {
+ mapperOutput.push_back(TStructuredJobTable{table.Description, table.RichYPath});
+ }
+
+ auto hints = spec.MapperFormatHints_;
+
+ auto mapperInferenceResult = PrepareOperation<TStructuredJobTableList>(
+ *mapper,
+ TOperationPreparationContext(
+ structuredInputs,
+ mapperOutput,
+ preparer->GetContext(),
+ preparer->GetClientRetryPolicy(),
+ preparer->GetTransactionId()),
+ &structuredInputs,
+ /* outputs */ nullptr,
+ hints);
+
+ auto nodeReaderFormat = NodeReaderFormatFromHintAndGlobalConfig(spec.MapperFormatHints_);
+
+ auto [inputFormat, inputFormatConfig] = formatBuilder.CreateFormat(
+ *mapper,
+ EIODirection::Input,
+ structuredInputs,
+ hints.InputFormatHints_,
+ nodeReaderFormat,
+ /* allowFormatFromTableAttribute */ true);
+
+ auto [outputFormat, outputFormatConfig] = formatBuilder.CreateFormat(
+ *mapper,
+ EIODirection::Output,
+ mapperOutput,
+ hints.OutputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute */ false);
+
+ operationIo.MapperJobFiles = CreateFormatConfig(inputFormatConfig, outputFormatConfig);
+ operationIo.MapperInputFormat = inputFormat;
+ operationIo.MapperOutputFormat = outputFormat;
+
+ Y_VERIFY(mapperInferenceResult.size() >= 1);
+ currentInferenceResult = TVector<TTableSchema>{mapperInferenceResult[0]};
+ // The first output as it corresponds to the intermediate data.
+ TVector<TTableSchema> additionalOutputsInferenceResult(mapperInferenceResult.begin() + 1, mapperInferenceResult.end());
+
+ operationIo.MapOutputs = GetPathList(
+ structuredMapOutputs,
+ additionalOutputsInferenceResult,
+ inferOutputSchema);
+ }
+
+ if (reduceCombiner) {
+ const bool isFirstStep = !mapper;
+ TStructuredJobTableList inputs;
+ if (isFirstStep) {
+ inputs = structuredInputs;
+ } else {
+ auto reduceCombinerIntermediateInput =
+ spec.GetIntermediateReduceCombinerInputDescription()
+ .GetOrElse(TUnspecifiedTableStructure());
+ inputs = {
+ TStructuredJobTable::Intermediate(reduceCombinerIntermediateInput),
+ };
+ }
+
+ auto reduceCombinerOutputDescription = spec.GetIntermediateReduceCombinerOutputDescription()
+ .GetOrElse(TUnspecifiedTableStructure());
+
+ TStructuredJobTableList outputs = {
+ TStructuredJobTable::Intermediate(reduceCombinerOutputDescription),
+ };
+
+ auto hints = spec.ReduceCombinerFormatHints_;
+
+ if (isFirstStep) {
+ currentInferenceResult = PrepareOperation<TStructuredJobTableList>(
+ *reduceCombiner,
+ TOperationPreparationContext(
+ inputs,
+ outputs,
+ preparer->GetContext(),
+ preparer->GetClientRetryPolicy(),
+ preparer->GetTransactionId()),
+ &inputs,
+ /* outputs */ nullptr,
+ hints);
+ } else {
+ currentInferenceResult = PrepareOperation<TStructuredJobTableList>(
+ *reduceCombiner,
+ TSpeculativeOperationPreparationContext(
+ currentInferenceResult,
+ inputs,
+ outputs),
+ /* inputs */ nullptr,
+ /* outputs */ nullptr,
+ hints);
+ }
+
+ auto [inputFormat, inputFormatConfig] = formatBuilder.CreateFormat(
+ *reduceCombiner,
+ EIODirection::Input,
+ inputs,
+ hints.InputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute = */ isFirstStep);
+
+ auto [outputFormat, outputFormatConfig] = formatBuilder.CreateFormat(
+ *reduceCombiner,
+ EIODirection::Output,
+ outputs,
+ hints.OutputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute = */ false);
+
+ operationIo.ReduceCombinerJobFiles = CreateFormatConfig(inputFormatConfig, outputFormatConfig);
+ operationIo.ReduceCombinerInputFormat = inputFormat;
+ operationIo.ReduceCombinerOutputFormat = outputFormat;
+
+ if (isFirstStep) {
+ fixSpec(*operationIo.ReduceCombinerInputFormat);
+ }
+ }
+
+ const bool isFirstStep = (!mapper && !reduceCombiner);
+ TStructuredJobTableList reducerInputs;
+ if (isFirstStep) {
+ reducerInputs = structuredInputs;
+ } else {
+ auto reducerInputDescription =
+ spec.GetIntermediateReducerInputDescription()
+ .GetOrElse(TUnspecifiedTableStructure());
+ reducerInputs = {
+ TStructuredJobTable::Intermediate(reducerInputDescription),
+ };
+ }
+
+ auto hints = spec.ReducerFormatHints_;
+
+ TVector<TTableSchema> reducerInferenceResult;
+ if (isFirstStep) {
+ reducerInferenceResult = PrepareOperation(
+ *reducer,
+ TOperationPreparationContext(
+ structuredInputs,
+ structuredOutputs,
+ preparer->GetContext(),
+ preparer->GetClientRetryPolicy(),
+ preparer->GetTransactionId()),
+ &structuredInputs,
+ &structuredOutputs,
+ hints);
+ } else {
+ reducerInferenceResult = PrepareOperation<TStructuredJobTableList>(
+ *reducer,
+ TSpeculativeOperationPreparationContext(
+ currentInferenceResult,
+ reducerInputs,
+ structuredOutputs),
+ /* inputs */ nullptr,
+ &structuredOutputs,
+ hints);
+ }
+
+ auto [inputFormat, inputFormatConfig] = formatBuilder.CreateFormat(
+ *reducer,
+ EIODirection::Input,
+ reducerInputs,
+ hints.InputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute = */ isFirstStep);
+
+ auto [outputFormat, outputFormatConfig] = formatBuilder.CreateFormat(
+ *reducer,
+ EIODirection::Output,
+ ToStructuredJobTableList(spec.GetStructuredOutputs()),
+ hints.OutputFormatHints_,
+ ENodeReaderFormat::Yson,
+ /* allowFormatFromTableAttribute = */ false);
+ operationIo.ReducerJobFiles = CreateFormatConfig(inputFormatConfig, outputFormatConfig);
+ operationIo.ReducerInputFormat = inputFormat;
+ operationIo.ReducerOutputFormat = outputFormat;
+
+ if (isFirstStep) {
+ fixSpec(operationIo.ReducerInputFormat);
+ }
+
+ operationIo.Inputs = GetPathList(
+ ApplyProtobufColumnFilters(
+ structuredInputs,
+ *preparer,
+ GetColumnsUsedInOperation(spec),
+ options),
+ /* jobSchemaInferenceResult */ Nothing(),
+ /* inferSchema */ false);
+
+ operationIo.Outputs = GetPathList(
+ structuredOutputs,
+ reducerInferenceResult,
+ inferOutputSchema);
+
+ VerifyHasElements(operationIo.Outputs, "outputs");
+
+ return DoExecuteMapReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options);
+}
+
+void ExecuteRawMapReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawMapReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& mapper,
+ const ::TIntrusivePtr<IRawJob>& reduceCombiner,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting raw map-reduce operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ TMapReduceOperationIo operationIo;
+ operationIo.Inputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.GetInputs());
+ operationIo.MapOutputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.GetMapOutputs());
+ operationIo.Outputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.GetOutputs());
+
+ VerifyHasElements(operationIo.Inputs, "inputs");
+ VerifyHasElements(operationIo.Outputs, "outputs");
+
+ auto getFormatOrDefault = [&] (const TMaybe<TFormat>& maybeFormat, const TMaybe<TFormat> stageDefaultFormat, const char* formatName) {
+ if (maybeFormat) {
+ return *maybeFormat;
+ } else if (stageDefaultFormat) {
+ return *stageDefaultFormat;
+ } else {
+ ythrow TApiUsageError() << "Cannot derive " << formatName;
+ }
+ };
+
+ if (mapper) {
+ operationIo.MapperInputFormat = getFormatOrDefault(spec.MapperInputFormat_, spec.MapperFormat_, "mapper input format");
+ operationIo.MapperOutputFormat = getFormatOrDefault(spec.MapperOutputFormat_, spec.MapperFormat_, "mapper output format");
+ }
+
+ if (reduceCombiner) {
+ operationIo.ReduceCombinerInputFormat = getFormatOrDefault(spec.ReduceCombinerInputFormat_, spec.ReduceCombinerFormat_, "reduce combiner input format");
+ operationIo.ReduceCombinerOutputFormat = getFormatOrDefault(spec.ReduceCombinerOutputFormat_, spec.ReduceCombinerFormat_, "reduce combiner output format");
+ }
+
+ operationIo.ReducerInputFormat = getFormatOrDefault(spec.ReducerInputFormat_, spec.ReducerFormat_, "reducer input format");
+ operationIo.ReducerOutputFormat = getFormatOrDefault(spec.ReducerOutputFormat_, spec.ReducerFormat_, "reducer output format");
+
+ return DoExecuteMapReduce(
+ operation,
+ preparer,
+ operationIo,
+ spec,
+ mapper,
+ reduceCombiner,
+ reducer,
+ options);
+}
+
+void ExecuteSort(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting sort operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto inputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.Inputs_);
+ auto output = CanonizeYPath(nullptr, preparer->GetContext(), spec.Output_);
+
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, inputs);
+ CreateOutputTable(*preparer, output);
+ }
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("input_table_paths").List(inputs)
+ .Item("output_table_path").Value(output)
+ .Item("sort_by").Value(spec.SortBy_)
+ .DoIf(spec.SchemaInferenceMode_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("schema_inference_mode").Value(ToString(*spec.SchemaInferenceMode_));
+ })
+ .Do(std::bind(BuildCommonOperationPart<TSortOperationSpec>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ BuildPartitionCountOperationPart(spec, &specNode["spec"]);
+ BuildPartitionJobCountOperationPart(spec, &specNode["spec"]);
+ BuildIntermediateDataPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ inputs,
+ output
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "sort", spec);
+
+ LogYPaths(operationId, inputs, "input");
+ LogYPath(operationId, output, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteMerge(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMergeOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting merge operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto inputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.Inputs_);
+ auto output = CanonizeYPath(nullptr, preparer->GetContext(), spec.Output_);
+
+ if (options.CreateOutputTables_) {
+ CheckInputTablesExist(*preparer, inputs);
+ CreateOutputTable(*preparer, output);
+ }
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("input_table_paths").List(inputs)
+ .Item("output_table_path").Value(output)
+ .Item("mode").Value(ToString(spec.Mode_))
+ .Item("combine_chunks").Value(spec.CombineChunks_)
+ .Item("force_transform").Value(spec.ForceTransform_)
+ .Item("merge_by").Value(spec.MergeBy_)
+ .DoIf(spec.SchemaInferenceMode_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("schema_inference_mode").Value(ToString(*spec.SchemaInferenceMode_));
+ })
+ .Do(std::bind(BuildCommonOperationPart<TMergeOperationSpec>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ BuildJobCountOperationPart(spec, &specNode["spec"]);
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ inputs,
+ output
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "merge", spec);
+
+ LogYPaths(operationId, inputs, "input");
+ LogYPath(operationId, output, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteErase(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TEraseOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting erase operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto tablePath = CanonizeYPath(nullptr, preparer->GetContext(), spec.TablePath_);
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("table_path").Value(tablePath)
+ .Item("combine_chunks").Value(spec.CombineChunks_)
+ .DoIf(spec.SchemaInferenceMode_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("schema_inference_mode").Value(ToString(*spec.SchemaInferenceMode_));
+ })
+ .Do(std::bind(BuildCommonOperationPart<TEraseOperationSpec>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options),
+ preparer,
+ tablePath
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "erase", spec);
+
+ LogYPath(operationId, tablePath, "table_path");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteRemoteCopy(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRemoteCopyOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting remote copy operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+ auto inputs = CanonizeYPaths(/* retryPolicy */ nullptr, preparer->GetContext(), spec.Inputs_);
+ auto output = CanonizeYPath(nullptr, preparer->GetContext(), spec.Output_);
+
+ if (options.CreateOutputTables_) {
+ CreateOutputTable(*preparer, output);
+ }
+
+ Y_ENSURE_EX(!spec.ClusterName_.empty(), TApiUsageError() << "ClusterName parameter is required");
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("cluster_name").Value(spec.ClusterName_)
+ .Item("input_table_paths").List(inputs)
+ .Item("output_table_path").Value(output)
+ .DoIf(spec.NetworkName_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("network_name").Value(*spec.NetworkName_);
+ })
+ .DoIf(spec.SchemaInferenceMode_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("schema_inference_mode").Value(ToString(*spec.SchemaInferenceMode_));
+ })
+ .Item("copy_attributes").Value(spec.CopyAttributes_)
+ .DoIf(!spec.AttributeKeys_.empty(), [&] (TFluentMap fluent) {
+ Y_ENSURE_EX(spec.CopyAttributes_, TApiUsageError() <<
+ "Specifying nonempty AttributeKeys in RemoteCopy "
+ "doesn't make sense without CopyAttributes == true");
+ fluent.Item("attribute_keys").List(spec.AttributeKeys_);
+ })
+ .Do(std::bind(BuildCommonOperationPart<TRemoteCopyOperationSpec>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ auto startOperation = [
+ operation=operation.Get(),
+ spec=MergeSpec(specNode, preparer->GetContext().Config->Spec, options),
+ preparer,
+ inputs,
+ output
+ ] () {
+ auto operationId = preparer->StartOperation(operation, "remote_copy", spec);
+
+ LogYPaths(operationId, inputs, "input");
+ LogYPath(operationId, output, "output");
+
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+void ExecuteVanilla(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TVanillaOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ YT_LOG_DEBUG("Starting vanilla operation (PreparationId: %v)",
+ preparer->GetPreparationId());
+
+ auto addTask = [&](TFluentMap fluent, const TVanillaTask& task) {
+ Y_VERIFY(task.Job_.Get());
+ if (std::holds_alternative<TVoidStructuredRowStream>(task.Job_->GetOutputRowStreamDescription())) {
+ Y_ENSURE_EX(task.Outputs_.empty(),
+ TApiUsageError() << "Vanilla task with void IVanillaJob doesn't expect output tables");
+ TJobPreparer jobPreparer(
+ *preparer,
+ task.Spec_,
+ *task.Job_,
+ /* outputTableCount */ 0,
+ /* smallFileList */ {},
+ options);
+ fluent
+ .Item(task.Name_).BeginMap()
+ .Item("job_count").Value(task.JobCount_)
+ .DoIf(task.NetworkProject_.Defined(), [&](TFluentMap fluent) {
+ fluent.Item("network_project").Value(*task.NetworkProject_);
+ })
+ .Do([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ std::cref(jobPreparer),
+ /* inputFormat */ Nothing(),
+ /* outputFormat */ Nothing(),
+ fluent);
+ })
+ .EndMap();
+ } else {
+ auto operationIo = CreateSimpleOperationIo(
+ *task.Job_,
+ *preparer,
+ task,
+ options,
+ false);
+ Y_ENSURE_EX(operationIo.Outputs.size() > 0,
+ TApiUsageError() << "Vanilla task with IVanillaJob that has table writer expects output tables");
+ if (options.CreateOutputTables_) {
+ CreateOutputTables(*preparer, operationIo.Outputs);
+ }
+ TJobPreparer jobPreparer(
+ *preparer,
+ task.Spec_,
+ *task.Job_,
+ operationIo.Outputs.size(),
+ operationIo.JobFiles,
+ options);
+ fluent
+ .Item(task.Name_).BeginMap()
+ .Item("job_count").Value(task.JobCount_)
+ .DoIf(task.NetworkProject_.Defined(), [&](TFluentMap fluent) {
+ fluent.Item("network_project").Value(*task.NetworkProject_);
+ })
+ .Do([&] (TFluentMap fluent) {
+ BuildUserJobFluently(
+ std::cref(jobPreparer),
+ /* inputFormat */ Nothing(),
+ operationIo.OutputFormat,
+ fluent);
+ })
+ .Item("output_table_paths").List(operationIo.Outputs)
+ .Item("job_io").BeginMap()
+ .DoIf(!preparer->GetContext().Config->TableWriter.Empty(), [&](TFluentMap fluent) {
+ fluent.Item("table_writer").Value(preparer->GetContext().Config->TableWriter);
+ })
+ .Item("control_attributes").BeginMap()
+ .Item("enable_row_index").Value(TNode(true))
+ .Item("enable_range_index").Value(TNode(true))
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }
+ };
+
+ if (options.CreateDebugOutputTables_) {
+ CreateDebugOutputTables(spec, *preparer);
+ }
+
+ TNode specNode = BuildYsonNodeFluently()
+ .BeginMap().Item("spec").BeginMap()
+ .Item("tasks").DoMapFor(spec.Tasks_, addTask)
+ .Do(std::bind(BuildCommonOperationPart<TVanillaOperationSpec>, preparer->GetContext().Config, spec, options, std::placeholders::_1))
+ .EndMap().EndMap();
+
+ BuildCommonUserOperationPart(spec, &specNode["spec"]);
+
+ auto startOperation = [operation=operation.Get(), spec=MergeSpec(std::move(specNode), preparer->GetContext().Config->Spec, options), preparer] () {
+ auto operationId = preparer->StartOperation(operation, "vanilla", spec, /* useStartOperationRequest */ true);
+ return operationId;
+ };
+
+ operation->SetDelayedStartFunction(std::move(startOperation));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperation::TOperationImpl
+ : public TThrRefBase
+{
+public:
+ TOperationImpl(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ TClientContext context,
+ const TMaybe<TOperationId>& operationId = {})
+ : ClientRetryPolicy_(clientRetryPolicy)
+ , Context_(std::move(context))
+ , Id_(operationId)
+ , PreparedPromise_(::NThreading::NewPromise<void>())
+ , StartedPromise_(::NThreading::NewPromise<void>())
+ {
+ if (Id_) {
+ PreparedPromise_.SetValue();
+ StartedPromise_.SetValue();
+ } else {
+ PreparedPromise_.GetFuture().Subscribe([this_=::TIntrusivePtr(this)] (const ::NThreading::TFuture<void>& preparedResult) {
+ try {
+ preparedResult.GetValue();
+ } catch (...) {
+ this_->StartedPromise_.SetException(std::current_exception());
+ return;
+ }
+ });
+ }
+ }
+
+ const TOperationId& GetId() const;
+ TString GetWebInterfaceUrl() const;
+
+ void OnPrepared();
+ void SetDelayedStartFunction(std::function<TOperationId()> start);
+ void Start();
+ bool IsStarted() const;
+ void OnPreparationException(std::exception_ptr e);
+
+ TString GetStatus();
+ void OnStatusUpdated(const TString& newStatus);
+
+ ::NThreading::TFuture<void> GetPreparedFuture();
+ ::NThreading::TFuture<void> GetStartedFuture();
+ ::NThreading::TFuture<void> Watch(TClientPtr client);
+
+ EOperationBriefState GetBriefState();
+ TMaybe<TYtError> GetError();
+ TJobStatistics GetJobStatistics();
+ TMaybe<TOperationBriefProgress> GetBriefProgress();
+ void AbortOperation();
+ void CompleteOperation();
+ void SuspendOperation(const TSuspendOperationOptions& options);
+ void ResumeOperation(const TResumeOperationOptions& options);
+ TOperationAttributes GetAttributes(const TGetOperationOptions& options);
+ void UpdateParameters(const TUpdateOperationParametersOptions& options);
+ TJobAttributes GetJob(const TJobId& jobId, const TGetJobOptions& options);
+ TListJobsResult ListJobs(const TListJobsOptions& options);
+
+ void AsyncFinishOperation(TOperationAttributes operationAttributes);
+ void FinishWithException(std::exception_ptr exception);
+ void UpdateBriefProgress(TMaybe<TOperationBriefProgress> briefProgress);
+ void AnalyzeUnrecognizedSpec(TNode unrecognizedSpec);
+
+ const TClientContext& GetContext() const;
+
+private:
+ void OnStarted(const TOperationId& operationId);
+
+ void UpdateAttributesAndCall(bool needJobStatistics, std::function<void(const TOperationAttributes&)> func);
+
+ void SyncFinishOperationImpl(const TOperationAttributes&);
+ static void* SyncFinishOperationProc(void* );
+
+ void ValidateOperationStarted() const;
+
+private:
+ IClientRetryPolicyPtr ClientRetryPolicy_;
+ const TClientContext Context_;
+ TMaybe<TOperationId> Id_;
+ TMutex Lock_;
+
+ ::NThreading::TPromise<void> PreparedPromise_;
+ ::NThreading::TPromise<void> StartedPromise_;
+ TMaybe<::NThreading::TPromise<void>> CompletePromise_;
+
+ std::function<TOperationId()> DelayedStartFunction_;
+ TString Status_;
+ TOperationAttributes Attributes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperationPollerItem
+ : public IYtPollerItem
+{
+public:
+ TOperationPollerItem(::TIntrusivePtr<TOperation::TOperationImpl> operationImpl)
+ : OperationImpl_(std::move(operationImpl))
+ { }
+
+ void PrepareRequest(TRawBatchRequest* batchRequest) override
+ {
+ auto filter = TOperationAttributeFilter()
+ .Add(EOperationAttribute::State)
+ .Add(EOperationAttribute::BriefProgress)
+ .Add(EOperationAttribute::Result);
+
+ if (!UnrecognizedSpecAnalyzed_) {
+ filter.Add(EOperationAttribute::UnrecognizedSpec);
+ }
+
+ OperationState_ = batchRequest->GetOperation(
+ OperationImpl_->GetId(),
+ TGetOperationOptions().AttributeFilter(filter));
+ }
+
+ EStatus OnRequestExecuted() override
+ {
+ try {
+ const auto& attributes = OperationState_.GetValue();
+ if (!UnrecognizedSpecAnalyzed_ && !attributes.UnrecognizedSpec.Empty()) {
+ OperationImpl_->AnalyzeUnrecognizedSpec(*attributes.UnrecognizedSpec);
+ UnrecognizedSpecAnalyzed_ = true;
+ }
+ Y_VERIFY(attributes.BriefState,
+ "get_operation for operation %s has not returned \"state\" field",
+ GetGuidAsString(OperationImpl_->GetId()).Data());
+ if (*attributes.BriefState != EOperationBriefState::InProgress) {
+ OperationImpl_->AsyncFinishOperation(attributes);
+ return PollBreak;
+ } else {
+ OperationImpl_->UpdateBriefProgress(attributes.BriefProgress);
+ }
+ } catch (const TErrorResponse& e) {
+ if (!IsRetriable(e)) {
+ OperationImpl_->FinishWithException(std::current_exception());
+ return PollBreak;
+ }
+ } catch (const std::exception& e) {
+ OperationImpl_->FinishWithException(std::current_exception());
+ return PollBreak;
+ }
+ return PollContinue;
+ }
+
+ void OnItemDiscarded() override {
+ OperationImpl_->FinishWithException(std::make_exception_ptr(yexception() << "Operation cancelled"));
+ }
+
+private:
+ ::TIntrusivePtr<TOperation::TOperationImpl> OperationImpl_;
+ ::NThreading::TFuture<TOperationAttributes> OperationState_;
+ bool UnrecognizedSpecAnalyzed_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TOperationId& TOperation::TOperationImpl::GetId() const
+{
+ ValidateOperationStarted();
+ return *Id_;
+}
+
+TString TOperation::TOperationImpl::GetWebInterfaceUrl() const
+{
+ ValidateOperationStarted();
+ return GetOperationWebInterfaceUrl(Context_.ServerName, *Id_);
+}
+
+void TOperation::TOperationImpl::OnPrepared()
+{
+ Y_VERIFY(!PreparedPromise_.HasException() && !PreparedPromise_.HasValue());
+ PreparedPromise_.SetValue();
+}
+
+void TOperation::TOperationImpl::SetDelayedStartFunction(std::function<TOperationId()> start)
+{
+ DelayedStartFunction_ = std::move(start);
+}
+
+void TOperation::TOperationImpl::Start()
+{
+ {
+ auto guard = Guard(Lock_);
+ if (Id_) {
+ ythrow TApiUsageError() << "Start() should not be called on running operations";
+ }
+ }
+ GetPreparedFuture().GetValueSync();
+
+ std::function<TOperationId()> startStuff;
+ {
+ auto guard = Guard(Lock_);
+ startStuff.swap(DelayedStartFunction_);
+ }
+ if (!startStuff) {
+ ythrow TApiUsageError() << "Seems that Start() was called multiple times. If not, contact yt@";
+ }
+
+ TOperationId operationId;
+ try {
+ operationId = startStuff();
+ } catch (...) {
+ auto exception = std::current_exception();
+ StartedPromise_.SetException(exception);
+ std::rethrow_exception(exception);
+ }
+ OnStarted(operationId);
+}
+
+bool TOperation::TOperationImpl::IsStarted() const {
+ auto guard = Guard(Lock_);
+ return bool(Id_);
+}
+
+void TOperation::TOperationImpl::OnPreparationException(std::exception_ptr e)
+{
+ Y_VERIFY(!PreparedPromise_.HasValue() && !PreparedPromise_.HasException());
+ PreparedPromise_.SetException(e);
+}
+
+TString TOperation::TOperationImpl::GetStatus()
+{
+ {
+ auto guard = Guard(Lock_);
+ if (!Id_) {
+ return Status_;
+ }
+ }
+ TMaybe<TString> state;
+ UpdateAttributesAndCall(false, [&] (const TOperationAttributes& attributes) {
+ state = attributes.State;
+ });
+
+ return "On YT cluster: " + state.GetOrElse("undefined state");
+}
+
+void TOperation::TOperationImpl::OnStatusUpdated(const TString& newStatus)
+{
+ auto guard = Guard(Lock_);
+ Status_ = newStatus;
+}
+
+::NThreading::TFuture<void> TOperation::TOperationImpl::GetPreparedFuture()
+{
+ return PreparedPromise_.GetFuture();
+}
+
+::NThreading::TFuture<void> TOperation::TOperationImpl::GetStartedFuture()
+{
+ return StartedPromise_.GetFuture();
+}
+
+::NThreading::TFuture<void> TOperation::TOperationImpl::Watch(TClientPtr client)
+{
+ {
+ auto guard = Guard(Lock_);
+ if (CompletePromise_) {
+ return *CompletePromise_;
+ }
+ CompletePromise_ = ::NThreading::NewPromise<void>();
+ }
+ GetStartedFuture().Subscribe([
+ this_=::TIntrusivePtr(this),
+ client=std::move(client)
+ ] (const ::NThreading::TFuture<void>& startedResult) {
+ try {
+ startedResult.GetValue();
+ } catch (...) {
+ this_->CompletePromise_->SetException(std::current_exception());
+ return;
+ }
+ client->GetYtPoller().Watch(::MakeIntrusive<TOperationPollerItem>(this_));
+ auto operationId = this_->GetId();
+ auto registry = TAbortableRegistry::Get();
+ registry->Add(
+ operationId,
+ ::MakeIntrusive<TOperationAbortable>(this_->ClientRetryPolicy_, this_->Context_, operationId));
+ // We have to own an IntrusivePtr to registry to prevent use-after-free
+ auto removeOperation = [registry, operationId] (const ::NThreading::TFuture<void>&) {
+ registry->Remove(operationId);
+ };
+ this_->CompletePromise_->GetFuture().Subscribe(removeOperation);
+ });
+
+ return *CompletePromise_;
+}
+
+EOperationBriefState TOperation::TOperationImpl::GetBriefState()
+{
+ ValidateOperationStarted();
+ EOperationBriefState result = EOperationBriefState::InProgress;
+ UpdateAttributesAndCall(false, [&] (const TOperationAttributes& attributes) {
+ Y_VERIFY(attributes.BriefState,
+ "get_operation for operation %s has not returned \"state\" field",
+ GetGuidAsString(*Id_).Data());
+ result = *attributes.BriefState;
+ });
+ return result;
+}
+
+TMaybe<TYtError> TOperation::TOperationImpl::GetError()
+{
+ ValidateOperationStarted();
+ TMaybe<TYtError> result;
+ UpdateAttributesAndCall(false, [&] (const TOperationAttributes& attributes) {
+ Y_VERIFY(attributes.Result);
+ result = attributes.Result->Error;
+ });
+ return result;
+}
+
+TJobStatistics TOperation::TOperationImpl::GetJobStatistics()
+{
+ ValidateOperationStarted();
+ TJobStatistics result;
+ UpdateAttributesAndCall(true, [&] (const TOperationAttributes& attributes) {
+ if (attributes.Progress) {
+ result = attributes.Progress->JobStatistics;
+ }
+ });
+ return result;
+}
+
+TMaybe<TOperationBriefProgress> TOperation::TOperationImpl::GetBriefProgress()
+{
+ ValidateOperationStarted();
+ {
+ auto g = Guard(Lock_);
+ if (CompletePromise_.Defined()) {
+ // Poller do this job for us
+ return Attributes_.BriefProgress;
+ }
+ }
+ TMaybe<TOperationBriefProgress> result;
+ UpdateAttributesAndCall(false, [&] (const TOperationAttributes& attributes) {
+ result = attributes.BriefProgress;
+ });
+ return result;
+}
+
+void TOperation::TOperationImpl::UpdateBriefProgress(TMaybe<TOperationBriefProgress> briefProgress)
+{
+ auto g = Guard(Lock_);
+ Attributes_.BriefProgress = std::move(briefProgress);
+}
+
+void TOperation::TOperationImpl::AnalyzeUnrecognizedSpec(TNode unrecognizedSpec)
+{
+ static const TVector<TVector<TString>> knownUnrecognizedSpecFieldPaths = {
+ {"mapper", "class_name"},
+ {"reducer", "class_name"},
+ {"reduce_combiner", "class_name"},
+ };
+
+ auto removeByPath = [] (TNode& node, auto pathBegin, auto pathEnd, auto& removeByPath) {
+ if (pathBegin == pathEnd) {
+ return;
+ }
+ if (!node.IsMap()) {
+ return;
+ }
+ auto* child = node.AsMap().FindPtr(*pathBegin);
+ if (!child) {
+ return;
+ }
+ removeByPath(*child, std::next(pathBegin), pathEnd, removeByPath);
+ if (std::next(pathBegin) == pathEnd || (child->IsMap() && child->Empty())) {
+ node.AsMap().erase(*pathBegin);
+ }
+ };
+
+ Y_VERIFY(unrecognizedSpec.IsMap());
+ for (const auto& knownFieldPath : knownUnrecognizedSpecFieldPaths) {
+ Y_VERIFY(!knownFieldPath.empty());
+ removeByPath(unrecognizedSpec, knownFieldPath.cbegin(), knownFieldPath.cend(), removeByPath);
+ }
+
+ if (!unrecognizedSpec.Empty()) {
+ YT_LOG_INFO(
+ "WARNING! Unrecognized spec for operation %s is not empty "
+ "(fields added by the YT API library are excluded): %s",
+ GetGuidAsString(*Id_).Data(),
+ NodeToYsonString(unrecognizedSpec).Data());
+ }
+}
+
+void TOperation::TOperationImpl::OnStarted(const TOperationId& operationId)
+{
+ auto guard = Guard(Lock_);
+ Y_VERIFY(!Id_,
+ "OnStarted() called with operationId = %s for operation with id %s",
+ GetGuidAsString(operationId).Data(),
+ GetGuidAsString(*Id_).Data());
+ Id_ = operationId;
+
+ Y_VERIFY(!StartedPromise_.HasValue() && !StartedPromise_.HasException());
+ StartedPromise_.SetValue();
+}
+
+void TOperation::TOperationImpl::UpdateAttributesAndCall(bool needJobStatistics, std::function<void(const TOperationAttributes&)> func)
+{
+ {
+ auto g = Guard(Lock_);
+ if (Attributes_.BriefState
+ && *Attributes_.BriefState != EOperationBriefState::InProgress
+ && (!needJobStatistics || Attributes_.Progress))
+ {
+ func(Attributes_);
+ return;
+ }
+ }
+
+ TOperationAttributes attributes = NDetail::GetOperation(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ *Id_,
+ TGetOperationOptions().AttributeFilter(TOperationAttributeFilter()
+ .Add(EOperationAttribute::Result)
+ .Add(EOperationAttribute::Progress)
+ .Add(EOperationAttribute::State)
+ .Add(EOperationAttribute::BriefProgress)));
+
+ func(attributes);
+
+ Y_ENSURE(attributes.BriefState);
+ if (*attributes.BriefState != EOperationBriefState::InProgress) {
+ auto g = Guard(Lock_);
+ Attributes_ = std::move(attributes);
+ }
+}
+
+void TOperation::TOperationImpl::FinishWithException(std::exception_ptr e)
+{
+ CompletePromise_->SetException(std::move(e));
+}
+
+void TOperation::TOperationImpl::AbortOperation()
+{
+ ValidateOperationStarted();
+ NYT::NDetail::AbortOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_);
+}
+
+void TOperation::TOperationImpl::CompleteOperation()
+{
+ ValidateOperationStarted();
+ NYT::NDetail::CompleteOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_);
+}
+
+void TOperation::TOperationImpl::SuspendOperation(const TSuspendOperationOptions& options)
+{
+ ValidateOperationStarted();
+ NYT::NDetail::SuspendOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, options);
+}
+
+void TOperation::TOperationImpl::ResumeOperation(const TResumeOperationOptions& options)
+{
+ ValidateOperationStarted();
+ NYT::NDetail::ResumeOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, options);
+}
+
+TOperationAttributes TOperation::TOperationImpl::GetAttributes(const TGetOperationOptions& options)
+{
+ ValidateOperationStarted();
+ return NYT::NDetail::GetOperation(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, options);
+}
+
+void TOperation::TOperationImpl::UpdateParameters(const TUpdateOperationParametersOptions& options)
+{
+ ValidateOperationStarted();
+ return NYT::NDetail::UpdateOperationParameters(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, options);
+}
+
+TJobAttributes TOperation::TOperationImpl::GetJob(const TJobId& jobId, const TGetJobOptions& options)
+{
+ ValidateOperationStarted();
+ return NYT::NDetail::GetJob(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, jobId, options);
+}
+
+TListJobsResult TOperation::TOperationImpl::ListJobs(const TListJobsOptions& options)
+{
+ ValidateOperationStarted();
+ return NYT::NDetail::ListJobs(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, *Id_, options);
+}
+
+struct TAsyncFinishOperationsArgs
+{
+ ::TIntrusivePtr<TOperation::TOperationImpl> OperationImpl;
+ TOperationAttributes OperationAttributes;
+};
+
+void TOperation::TOperationImpl::AsyncFinishOperation(TOperationAttributes operationAttributes)
+{
+ auto args = new TAsyncFinishOperationsArgs;
+ args->OperationImpl = this;
+ args->OperationAttributes = std::move(operationAttributes);
+
+ TThread thread(TThread::TParams(&TOperation::TOperationImpl::SyncFinishOperationProc, args).SetName("finish operation"));
+ thread.Start();
+ thread.Detach();
+}
+
+void* TOperation::TOperationImpl::SyncFinishOperationProc(void* pArgs)
+{
+ THolder<TAsyncFinishOperationsArgs> args(static_cast<TAsyncFinishOperationsArgs*>(pArgs));
+ args->OperationImpl->SyncFinishOperationImpl(args->OperationAttributes);
+ return nullptr;
+}
+
+void TOperation::TOperationImpl::SyncFinishOperationImpl(const TOperationAttributes& attributes)
+{
+ {
+ auto guard = Guard(Lock_);
+ Y_VERIFY(Id_);
+ }
+ Y_VERIFY(attributes.BriefState,
+ "get_operation for operation %s has not returned \"state\" field",
+ GetGuidAsString(*Id_).Data());
+ Y_VERIFY(*attributes.BriefState != EOperationBriefState::InProgress);
+
+ {
+ try {
+ // `attributes' that came from poller don't have JobStatistics
+ // so we call `GetJobStatistics' in order to get it from server
+ // and cache inside object.
+ GetJobStatistics();
+ } catch (const TErrorResponse& ) {
+ // But if for any reason we failed to get attributes
+ // we complete operation using what we have.
+ auto g = Guard(Lock_);
+ Attributes_ = attributes;
+ }
+ }
+
+ if (*attributes.BriefState == EOperationBriefState::Completed) {
+ CompletePromise_->SetValue();
+ } else if (*attributes.BriefState == EOperationBriefState::Aborted || *attributes.BriefState == EOperationBriefState::Failed) {
+ Y_VERIFY(attributes.Result && attributes.Result->Error);
+ const auto& error = *attributes.Result->Error;
+ YT_LOG_ERROR("Operation %v is `%v' with error: %v",
+ *Id_,
+ ToString(*attributes.BriefState),
+ error.FullDescription());
+
+ TString additionalExceptionText;
+ TVector<TFailedJobInfo> failedJobStderrInfo;
+ if (*attributes.BriefState == EOperationBriefState::Failed) {
+ try {
+ failedJobStderrInfo = NYT::NDetail::GetFailedJobInfo(ClientRetryPolicy_, Context_, *Id_, TGetFailedJobInfoOptions());
+ } catch (const std::exception& e) {
+ additionalExceptionText = "Cannot get job stderrs: ";
+ additionalExceptionText += e.what();
+ }
+ }
+ CompletePromise_->SetException(
+ std::make_exception_ptr(
+ TOperationFailedError(
+ *attributes.BriefState == EOperationBriefState::Failed
+ ? TOperationFailedError::Failed
+ : TOperationFailedError::Aborted,
+ *Id_,
+ error,
+ failedJobStderrInfo) << additionalExceptionText));
+ }
+}
+
+void TOperation::TOperationImpl::ValidateOperationStarted() const
+{
+ auto guard = Guard(Lock_);
+ if (!Id_) {
+ ythrow TApiUsageError() << "Operation is not started";
+ }
+}
+
+const TClientContext& TOperation::TOperationImpl::GetContext() const
+{
+ return Context_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperation::TOperation(TClientPtr client)
+ : Client_(std::move(client))
+ , Impl_(::MakeIntrusive<TOperationImpl>(Client_->GetRetryPolicy(), Client_->GetContext()))
+{
+}
+
+TOperation::TOperation(TOperationId id, TClientPtr client)
+ : Client_(std::move(client))
+ , Impl_(::MakeIntrusive<TOperationImpl>(Client_->GetRetryPolicy(), Client_->GetContext(), id))
+{
+}
+
+const TOperationId& TOperation::GetId() const
+{
+ return Impl_->GetId();
+}
+
+TString TOperation::GetWebInterfaceUrl() const
+{
+ return Impl_->GetWebInterfaceUrl();
+}
+
+void TOperation::OnPrepared()
+{
+ Impl_->OnPrepared();
+}
+
+void TOperation::SetDelayedStartFunction(std::function<TOperationId()> start)
+{
+ Impl_->SetDelayedStartFunction(std::move(start));
+}
+
+void TOperation::Start()
+{
+ Impl_->Start();
+}
+
+bool TOperation::IsStarted() const
+{
+ return Impl_->IsStarted();
+}
+
+void TOperation::OnPreparationException(std::exception_ptr e)
+{
+ Impl_->OnPreparationException(std::move(e));
+}
+
+TString TOperation::GetStatus() const
+{
+ return Impl_->GetStatus();
+}
+
+void TOperation::OnStatusUpdated(const TString& newStatus)
+{
+ Impl_->OnStatusUpdated(newStatus);
+}
+
+::NThreading::TFuture<void> TOperation::GetPreparedFuture()
+{
+ return Impl_->GetPreparedFuture();
+}
+
+::NThreading::TFuture<void> TOperation::GetStartedFuture()
+{
+ return Impl_->GetStartedFuture();
+}
+
+::NThreading::TFuture<void> TOperation::Watch()
+{
+ return Impl_->Watch(Client_);
+}
+
+TVector<TFailedJobInfo> TOperation::GetFailedJobInfo(const TGetFailedJobInfoOptions& options)
+{
+ return NYT::NDetail::GetFailedJobInfo(Client_->GetRetryPolicy(), Client_->GetContext(), GetId(), options);
+}
+
+EOperationBriefState TOperation::GetBriefState()
+{
+ return Impl_->GetBriefState();
+}
+
+TMaybe<TYtError> TOperation::GetError()
+{
+ return Impl_->GetError();
+}
+
+TJobStatistics TOperation::GetJobStatistics()
+{
+ return Impl_->GetJobStatistics();
+}
+
+TMaybe<TOperationBriefProgress> TOperation::GetBriefProgress()
+{
+ return Impl_->GetBriefProgress();
+}
+
+void TOperation::AbortOperation()
+{
+ Impl_->AbortOperation();
+}
+
+void TOperation::CompleteOperation()
+{
+ Impl_->CompleteOperation();
+}
+
+void TOperation::SuspendOperation(const TSuspendOperationOptions& options)
+{
+ Impl_->SuspendOperation(options);
+}
+
+void TOperation::ResumeOperation(const TResumeOperationOptions& options)
+{
+ Impl_->ResumeOperation(options);
+}
+
+TOperationAttributes TOperation::GetAttributes(const TGetOperationOptions& options)
+{
+ return Impl_->GetAttributes(options);
+}
+
+void TOperation::UpdateParameters(const TUpdateOperationParametersOptions& options)
+{
+ Impl_->UpdateParameters(options);
+}
+
+TJobAttributes TOperation::GetJob(const TJobId& jobId, const TGetJobOptions& options)
+{
+ return Impl_->GetJob(jobId, options);
+}
+
+TListJobsResult TOperation::ListJobs(const TListJobsOptions& options)
+{
+ return Impl_->ListJobs(options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAsyncPrepareAndStartOperationArgs
+{
+ std::function<void()> PrepareAndStart;
+};
+
+void* SyncPrepareAndStartOperation(void* pArgs)
+{
+ THolder<TAsyncPrepareAndStartOperationArgs> args(static_cast<TAsyncPrepareAndStartOperationArgs*>(pArgs));
+ args->PrepareAndStart();
+ return nullptr;
+}
+
+::TIntrusivePtr<TOperation> ProcessOperation(
+ NYT::NDetail::TClientPtr client,
+ std::function<void()> prepare,
+ ::TIntrusivePtr<TOperation> operation,
+ const TOperationOptions& options)
+{
+ auto prepareAndStart = [prepare = std::move(prepare), operation, mode = options.StartOperationMode_] () {
+ try {
+ prepare();
+ operation->OnPrepared();
+ } catch (...) {
+ operation->OnPreparationException(std::current_exception());
+ }
+ if (mode >= TOperationOptions::EStartOperationMode::AsyncStart) {
+ try {
+ operation->Start();
+ } catch (...) { }
+ }
+ };
+ if (options.StartOperationMode_ >= TOperationOptions::EStartOperationMode::SyncStart) {
+ prepareAndStart();
+ WaitIfRequired(operation, client, options);
+ } else {
+ auto args = new TAsyncPrepareAndStartOperationArgs;
+ args->PrepareAndStart = std::move(prepareAndStart);
+
+ TThread thread(TThread::TParams(SyncPrepareAndStartOperation, args).SetName("prepare and start operation"));
+ thread.Start();
+ thread.Detach();
+ }
+ return operation;
+}
+
+void WaitIfRequired(const TOperationPtr& operation, const TClientPtr& client, const TOperationOptions& options)
+{
+ auto retryPolicy = client->GetRetryPolicy();
+ auto context = client->GetContext();
+ if (options.StartOperationMode_ >= TOperationOptions::EStartOperationMode::SyncStart) {
+ operation->GetStartedFuture().GetValueSync();
+ }
+ if (options.StartOperationMode_ == TOperationOptions::EStartOperationMode::SyncWait) {
+ auto finishedFuture = operation->Watch();
+ TWaitProxy::Get()->WaitFuture(finishedFuture);
+ finishedFuture.GetValue();
+ if (context.Config->WriteStderrSuccessfulJobs) {
+ auto stderrs = GetJobsStderr(retryPolicy, context, operation->GetId());
+ for (const auto& jobStderr : stderrs) {
+ if (!jobStderr.empty()) {
+ Cerr << jobStderr << '\n';
+ }
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ResetUseClientProtobuf(const char* methodName)
+{
+ Cerr << "WARNING! OPTION `TConfig::UseClientProtobuf' IS RESET TO `true'; "
+ << "IT CAN DETERIORATE YOUR CODE PERFORMANCE!!! DON'T USE DEPRECATED METHOD `"
+ << "TOperationIOSpec::" << methodName << "' TO AVOID THIS RESET" << Endl;
+ // Give users some time to contemplate about usage of deprecated functions.
+ Cerr << "Sleeping for 5 seconds..." << Endl;
+ Sleep(TDuration::Seconds(5));
+ TConfig::Get()->UseClientProtobuf = true;
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+::TIntrusivePtr<INodeReaderImpl> CreateJobNodeReader(TRawTableReaderPtr rawTableReader)
+{
+ if (auto schema = NDetail::GetJobInputSkiffSchema()) {
+ return new NDetail::TSkiffTableReader(rawTableReader, schema);
+ } else {
+ return new TNodeTableReader(rawTableReader);
+ }
+}
+
+::TIntrusivePtr<IYaMRReaderImpl> CreateJobYaMRReader(TRawTableReaderPtr rawTableReader)
+{
+ return new TYaMRTableReader(rawTableReader);
+}
+
+::TIntrusivePtr<IProtoReaderImpl> CreateJobProtoReader(TRawTableReaderPtr rawTableReader)
+{
+ if (TConfig::Get()->UseClientProtobuf) {
+ return new TProtoTableReader(
+ rawTableReader,
+ GetJobInputDescriptors());
+ } else {
+ return new TLenvalProtoTableReader(
+ rawTableReader,
+ GetJobInputDescriptors());
+ }
+}
+
+::TIntrusivePtr<INodeWriterImpl> CreateJobNodeWriter(THolder<IProxyOutput> rawJobWriter)
+{
+ return new TNodeTableWriter(std::move(rawJobWriter));
+}
+
+::TIntrusivePtr<IYaMRWriterImpl> CreateJobYaMRWriter(THolder<IProxyOutput> rawJobWriter)
+{
+ return new TYaMRTableWriter(std::move(rawJobWriter));
+}
+
+::TIntrusivePtr<IProtoWriterImpl> CreateJobProtoWriter(THolder<IProxyOutput> rawJobWriter)
+{
+ if (TConfig::Get()->UseClientProtobuf) {
+ return new TProtoTableWriter(
+ std::move(rawJobWriter),
+ GetJobOutputDescriptors());
+ } else {
+ return new TLenvalProtoTableWriter(
+ std::move(rawJobWriter),
+ GetJobOutputDescriptors());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/operation.h b/yt/cpp/mapreduce/client/operation.h
new file mode 100644
index 0000000000..141161b0b7
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation.h
@@ -0,0 +1,203 @@
+#pragma once
+
+#include "fwd.h"
+#include "structured_table_formats.h"
+#include "operation_preparer.h"
+
+#include <yt/cpp/mapreduce/http/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/retry_policy.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/vector.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperation
+ : public IOperation
+{
+public:
+ class TOperationImpl;
+
+public:
+ explicit TOperation(TClientPtr client);
+ TOperation(TOperationId id, TClientPtr client);
+ virtual const TOperationId& GetId() const override;
+ virtual TString GetWebInterfaceUrl() const override;
+
+ void OnPrepared();
+ void SetDelayedStartFunction(std::function<TOperationId()> start);
+ virtual void Start() override;
+ void OnPreparationException(std::exception_ptr e);
+ virtual bool IsStarted() const override;
+
+ virtual TString GetStatus() const override;
+ void OnStatusUpdated(const TString& newStatus);
+
+ virtual ::NThreading::TFuture<void> GetPreparedFuture() override;
+ virtual ::NThreading::TFuture<void> GetStartedFuture() override;
+ virtual ::NThreading::TFuture<void> Watch() override;
+
+ virtual TVector<TFailedJobInfo> GetFailedJobInfo(const TGetFailedJobInfoOptions& options = TGetFailedJobInfoOptions()) override;
+ virtual EOperationBriefState GetBriefState() override;
+ virtual TMaybe<TYtError> GetError() override;
+ virtual TJobStatistics GetJobStatistics() override;
+ virtual TMaybe<TOperationBriefProgress> GetBriefProgress() override;
+ virtual void AbortOperation() override;
+ virtual void CompleteOperation() override;
+ virtual void SuspendOperation(const TSuspendOperationOptions& options) override;
+ virtual void ResumeOperation(const TResumeOperationOptions& options) override;
+ virtual TOperationAttributes GetAttributes(const TGetOperationOptions& options) override;
+ virtual void UpdateParameters(const TUpdateOperationParametersOptions& options) override;
+ virtual TJobAttributes GetJob(const TJobId& jobId, const TGetJobOptions& options) override;
+ virtual TListJobsResult ListJobs(const TListJobsOptions& options) override;
+
+private:
+ TClientPtr Client_;
+ ::TIntrusivePtr<TOperationImpl> Impl_;
+};
+
+using TOperationPtr = ::TIntrusivePtr<TOperation>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSimpleOperationIo
+{
+ TVector<TRichYPath> Inputs;
+ TVector<TRichYPath> Outputs;
+
+ TFormat InputFormat;
+ TFormat OutputFormat;
+
+ TVector<TSmallJobFile> JobFiles;
+};
+
+TSimpleOperationIo CreateSimpleOperationIoHelper(
+ const IStructuredJob& structuredJob,
+ const TOperationPreparer& preparer,
+ const TOperationOptions& options,
+ TStructuredJobTableList structuredInputs,
+ TStructuredJobTableList structuredOutputs,
+ TUserJobFormatHints hints,
+ ENodeReaderFormat nodeReaderFormat,
+ const THashSet<TString>& columnsUsedInOperations);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ExecuteMap(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMapOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& mapper,
+ const TOperationOptions& options);
+
+void ExecuteRawMap(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawMapOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& mapper,
+ const TOperationOptions& options);
+
+void ExecuteReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteRawReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteJoinReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TJoinReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteRawJoinReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawJoinReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteMapReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMapReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IStructuredJob>& mapper,
+ const ::TIntrusivePtr<IStructuredJob>& reduceCombiner,
+ const ::TIntrusivePtr<IStructuredJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteRawMapReduce(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRawMapReduceOperationSpec& spec,
+ const ::TIntrusivePtr<IRawJob>& mapper,
+ const ::TIntrusivePtr<IRawJob>& reduceCombiner,
+ const ::TIntrusivePtr<IRawJob>& reducer,
+ const TOperationOptions& options);
+
+void ExecuteSort(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options);
+
+void ExecuteMerge(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TMergeOperationSpec& spec,
+ const TOperationOptions& options);
+
+void ExecuteErase(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TEraseOperationSpec& spec,
+ const TOperationOptions& options);
+
+void ExecuteRemoteCopy(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TRemoteCopyOperationSpec& spec,
+ const TOperationOptions& options);
+
+void ExecuteVanilla(
+ const TOperationPtr& operation,
+ const TOperationPreparerPtr& preparer,
+ const TVanillaOperationSpec& spec,
+ const TOperationOptions& options);
+
+EOperationBriefState CheckOperation(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId);
+
+void WaitForOperation(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+::TIntrusivePtr<TOperation> ProcessOperation(
+ NYT::NDetail::TClientPtr client,
+ std::function<void()> prepare,
+ ::TIntrusivePtr<TOperation> operation,
+ const TOperationOptions& options);
+
+void WaitIfRequired(const TOperationPtr& operation, const TClientPtr& client, const TOperationOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/operation_helpers.cpp b/yt/cpp/mapreduce/client/operation_helpers.cpp
new file mode 100644
index 0000000000..abb2185662
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_helpers.cpp
@@ -0,0 +1,91 @@
+#include "operation_helpers.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <util/string/builder.h>
+
+#include <util/system/mutex.h>
+#include <util/system/rwlock.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui64 RoundUpFileSize(ui64 size)
+{
+ constexpr ui64 roundUpTo = 4ull << 10;
+ return (size + roundUpTo - 1) & ~(roundUpTo - 1);
+}
+
+bool UseLocalModeOptimization(const TClientContext& context, const IClientRetryPolicyPtr& clientRetryPolicy)
+{
+ if (!context.Config->EnableLocalModeOptimization) {
+ return false;
+ }
+
+ static THashMap<TString, bool> localModeMap;
+ static TRWMutex mutex;
+
+ {
+ TReadGuard guard(mutex);
+ auto it = localModeMap.find(context.ServerName);
+ if (it != localModeMap.end()) {
+ return it->second;
+ }
+ }
+
+ bool isLocalMode = false;
+ TString localModeAttr("//sys/@local_mode_fqdn");
+ // We don't want to pollute logs with errors about failed request,
+ // so we check if path exists before getting it.
+ if (NRawClient::Exists(clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ TTransactionId(),
+ localModeAttr,
+ TExistsOptions().ReadFrom(EMasterReadKind::Cache)))
+ {
+ auto fqdnNode = NRawClient::TryGet(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ TTransactionId(),
+ localModeAttr,
+ TGetOptions().ReadFrom(EMasterReadKind::Cache));
+ if (!fqdnNode.IsUndefined()) {
+ auto fqdn = fqdnNode.AsString();
+ isLocalMode = (fqdn == TProcessState::Get()->FqdnHostName);
+ YT_LOG_DEBUG("Checking local mode; LocalModeFqdn: %v FqdnHostName: %v IsLocalMode: %v",
+ fqdn,
+ TProcessState::Get()->FqdnHostName,
+ isLocalMode ? "true" : "false");
+ }
+ }
+
+ {
+ TWriteGuard guard(mutex);
+ localModeMap[context.ServerName] = isLocalMode;
+ }
+
+ return isLocalMode;
+}
+
+TString GetOperationWebInterfaceUrl(TStringBuf serverName, TOperationId operationId)
+{
+ serverName.ChopSuffix(":80");
+ serverName.ChopSuffix(".yt.yandex-team.ru");
+ serverName.ChopSuffix(".yt.yandex.net");
+ return ::TStringBuilder() << "https://yt.yandex-team.ru/" << serverName <<
+ "/operations/" << GetGuidAsString(operationId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/operation_helpers.h b/yt/cpp/mapreduce/client/operation_helpers.h
new file mode 100644
index 0000000000..7fd2ffb0de
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_helpers.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <yt/cpp/mapreduce/http/fwd.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui64 RoundUpFileSize(ui64 size);
+
+bool UseLocalModeOptimization(const TClientContext& context, const IClientRetryPolicyPtr& clientRetryPolicy);
+
+TString GetOperationWebInterfaceUrl(TStringBuf serverName, TOperationId operationId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/operation_preparer.cpp b/yt/cpp/mapreduce/client/operation_preparer.cpp
new file mode 100644
index 0000000000..e06fac4061
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_preparer.cpp
@@ -0,0 +1,881 @@
+#include "operation_preparer.h"
+
+#include "init.h"
+#include "file_writer.h"
+#include "operation.h"
+#include "operation_helpers.h"
+#include "operation_tracker.h"
+#include "transaction.h"
+#include "transaction_pinger.h"
+#include "yt_poller.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/digest/md5/md5.h>
+
+#include <util/folder/path.h>
+
+#include <util/string/builder.h>
+
+#include <util/system/execpath.h>
+
+namespace NYT::NDetail {
+
+using namespace NRawClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWaitOperationStartPollerItem
+ : public IYtPollerItem
+{
+public:
+ TWaitOperationStartPollerItem(TOperationId operationId, THolder<TPingableTransaction> transaction)
+ : OperationId_(operationId)
+ , Transaction_(std::move(transaction))
+ { }
+
+ void PrepareRequest(TRawBatchRequest* batchRequest) override
+ {
+ Future_ = batchRequest->GetOperation(
+ OperationId_,
+ TGetOperationOptions().AttributeFilter(
+ TOperationAttributeFilter().Add(EOperationAttribute::State)));
+ }
+
+ EStatus OnRequestExecuted() override
+ {
+ try {
+ auto attributes = Future_.GetValue();
+ Y_ENSURE(attributes.State.Defined());
+ bool operationHasLockedFiles =
+ *attributes.State != "starting" &&
+ *attributes.State != "pending" &&
+ *attributes.State != "orphaned" &&
+ *attributes.State != "waiting_for_agent" &&
+ *attributes.State != "initializing";
+ return operationHasLockedFiles ? EStatus::PollBreak : EStatus::PollContinue;
+ } catch (const TErrorResponse& e) {
+ YT_LOG_ERROR("get_operation request failed: %v (RequestId: %v)",
+ e.GetError().GetMessage(),
+ e.GetRequestId());
+ return IsRetriable(e) ? PollContinue : PollBreak;
+ } catch (const std::exception& e) {
+ YT_LOG_ERROR("%v", e.what());
+ return PollBreak;
+ }
+ }
+
+ void OnItemDiscarded() override {
+ }
+
+private:
+ TOperationId OperationId_;
+ THolder<TPingableTransaction> Transaction_;
+ ::NThreading::TFuture<TOperationAttributes> Future_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperationForwardingRequestRetryPolicy
+ : public IRequestRetryPolicy
+{
+public:
+ TOperationForwardingRequestRetryPolicy(const IRequestRetryPolicyPtr& underlying, const TOperationPtr& operation)
+ : Underlying_(underlying)
+ , Operation_(operation)
+ { }
+
+ void NotifyNewAttempt() override
+ {
+ Underlying_->NotifyNewAttempt();
+ }
+
+ TMaybe<TDuration> OnGenericError(const std::exception& e) override
+ {
+ UpdateOperationStatus(e.what());
+ return Underlying_->OnGenericError(e);
+ }
+
+ TMaybe<TDuration> OnRetriableError(const TErrorResponse& e) override
+ {
+ auto msg = e.GetError().ShortDescription();
+ UpdateOperationStatus(msg);
+ return Underlying_->OnRetriableError(e);
+ }
+
+ void OnIgnoredError(const TErrorResponse& e) override
+ {
+ Underlying_->OnIgnoredError(e);
+ }
+
+ TString GetAttemptDescription() const override
+ {
+ return Underlying_->GetAttemptDescription();
+ }
+
+private:
+ void UpdateOperationStatus(TStringBuf err)
+ {
+ Y_VERIFY(Operation_);
+ Operation_->OnStatusUpdated(
+ ::TStringBuilder() << "Retriable error during operation start: " << err);
+ }
+
+private:
+ IRequestRetryPolicyPtr Underlying_;
+ TOperationPtr Operation_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperationPreparer::TOperationPreparer(TClientPtr client, TTransactionId transactionId)
+ : Client_(std::move(client))
+ , TransactionId_(transactionId)
+ , FileTransaction_(MakeHolder<TPingableTransaction>(
+ Client_->GetRetryPolicy(),
+ Client_->GetContext(),
+ TransactionId_,
+ Client_->GetTransactionPinger()->GetChildTxPinger(),
+ TStartTransactionOptions()))
+ , ClientRetryPolicy_(Client_->GetRetryPolicy())
+ , PreparationId_(CreateGuidAsString())
+{ }
+
+const TClientContext& TOperationPreparer::GetContext() const
+{
+ return Client_->GetContext();
+}
+
+TTransactionId TOperationPreparer::GetTransactionId() const
+{
+ return TransactionId_;
+}
+
+TClientPtr TOperationPreparer::GetClient() const
+{
+ return Client_;
+}
+
+const TString& TOperationPreparer::GetPreparationId() const
+{
+ return PreparationId_;
+}
+
+const IClientRetryPolicyPtr& TOperationPreparer::GetClientRetryPolicy() const
+{
+ return ClientRetryPolicy_;
+}
+
+TOperationId TOperationPreparer::StartOperation(
+ TOperation* operation,
+ const TString& operationType,
+ const TNode& spec,
+ bool useStartOperationRequest)
+{
+ CheckValidity();
+
+ THttpHeader header("POST", (useStartOperationRequest ? "start_op" : operationType));
+ if (useStartOperationRequest) {
+ header.AddParameter("operation_type", operationType);
+ }
+ header.AddTransactionId(TransactionId_);
+ header.AddMutationId();
+
+ auto ysonSpec = NodeToYsonString(spec);
+ auto responseInfo = RetryRequestWithPolicy(
+ ::MakeIntrusive<TOperationForwardingRequestRetryPolicy>(
+ ClientRetryPolicy_->CreatePolicyForStartOperationRequest(),
+ TOperationPtr(operation)),
+ GetContext(),
+ header,
+ ysonSpec);
+ TOperationId operationId = ParseGuidFromResponse(responseInfo.Response);
+ YT_LOG_DEBUG("Operation started (OperationId: %v; PreparationId: %v)",
+ operationId,
+ GetPreparationId());
+
+ YT_LOG_INFO("Operation %v started (%v): %v",
+ operationId,
+ operationType,
+ GetOperationWebInterfaceUrl(GetContext().ServerName, operationId));
+
+ TOperationExecutionTimeTracker::Get()->Start(operationId);
+
+ Client_->GetYtPoller().Watch(
+ new TWaitOperationStartPollerItem(operationId, std::move(FileTransaction_)));
+
+ return operationId;
+}
+
+void TOperationPreparer::LockFiles(TVector<TRichYPath>* paths)
+{
+ CheckValidity();
+
+ TVector<::NThreading::TFuture<TLockId>> lockIdFutures;
+ lockIdFutures.reserve(paths->size());
+ TRawBatchRequest lockRequest(GetContext().Config);
+ for (const auto& path : *paths) {
+ lockIdFutures.push_back(lockRequest.Lock(
+ FileTransaction_->GetId(),
+ path.Path_,
+ ELockMode::LM_SNAPSHOT,
+ TLockOptions().Waitable(true)));
+ }
+ ExecuteBatch(ClientRetryPolicy_->CreatePolicyForGenericRequest(), GetContext(), lockRequest);
+
+ TVector<::NThreading::TFuture<TNode>> nodeIdFutures;
+ nodeIdFutures.reserve(paths->size());
+ TRawBatchRequest getNodeIdRequest(GetContext().Config);
+ for (const auto& lockIdFuture : lockIdFutures) {
+ nodeIdFutures.push_back(getNodeIdRequest.Get(
+ FileTransaction_->GetId(),
+ ::TStringBuilder() << '#' << GetGuidAsString(lockIdFuture.GetValue()) << "/@node_id",
+ TGetOptions()));
+ }
+ ExecuteBatch(ClientRetryPolicy_->CreatePolicyForGenericRequest(), GetContext(), getNodeIdRequest);
+
+ for (size_t i = 0; i != paths->size(); ++i) {
+ auto& richPath = (*paths)[i];
+ richPath.OriginalPath(richPath.Path_);
+ richPath.Path("#" + nodeIdFutures[i].GetValue().AsString());
+ YT_LOG_DEBUG("Locked file %v, new path is %v",
+ *richPath.OriginalPath_,
+ richPath.Path_);
+ }
+}
+
+void TOperationPreparer::CheckValidity() const
+{
+ Y_ENSURE(
+ FileTransaction_,
+ "File transaction is already moved, are you trying to use preparer for more than one operation?");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetryPolicyIgnoringLockConflicts
+ : public TAttemptLimitedRetryPolicy
+{
+public:
+ using TAttemptLimitedRetryPolicy::TAttemptLimitedRetryPolicy;
+ using TAttemptLimitedRetryPolicy::OnGenericError;
+
+ TMaybe<TDuration> OnRetriableError(const TErrorResponse& e) override
+ {
+ if (IsAttemptLimitExceeded()) {
+ return Nothing();
+ }
+ if (e.IsConcurrentTransactionLockConflict()) {
+ return GetBackoffDuration(Config_);
+ }
+ return TAttemptLimitedRetryPolicy::OnRetriableError(e);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileToUpload
+ : public IItemToUpload
+{
+public:
+ TFileToUpload(TString fileName, TMaybe<TString> md5)
+ : FileName_(std::move(fileName))
+ , MD5_(std::move(md5))
+ { }
+
+ TString CalculateMD5() const override
+ {
+ if (MD5_) {
+ return *MD5_;
+ }
+ constexpr size_t md5Size = 32;
+ TString result;
+ result.ReserveAndResize(md5Size);
+ MD5::File(FileName_.data(), result.Detach());
+ MD5_ = result;
+ return result;
+ }
+
+ THolder<IInputStream> CreateInputStream() const override
+ {
+ return MakeHolder<TFileInput>(FileName_);
+ }
+
+ TString GetDescription() const override
+ {
+ return FileName_;
+ }
+
+ ui64 GetDataSize() const override
+ {
+ return GetFileLength(FileName_);
+ }
+
+private:
+ TString FileName_;
+ mutable TMaybe<TString> MD5_;
+};
+
+class TDataToUpload
+ : public IItemToUpload
+{
+public:
+ TDataToUpload(TString data, TString description)
+ : Data_(std::move(data))
+ , Description_(std::move(description))
+ { }
+
+ TString CalculateMD5() const override
+ {
+ constexpr size_t md5Size = 32;
+ TString result;
+ result.ReserveAndResize(md5Size);
+ MD5::Data(reinterpret_cast<const unsigned char*>(Data_.data()), Data_.size(), result.Detach());
+ return result;
+ }
+
+ THolder<IInputStream> CreateInputStream() const override
+ {
+ return MakeHolder<TMemoryInput>(Data_.data(), Data_.size());
+ }
+
+ TString GetDescription() const override
+ {
+ return Description_;
+ }
+
+ ui64 GetDataSize() const override
+ {
+ return Data_.size();
+ }
+
+private:
+ TString Data_;
+ TString Description_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TString& GetPersistentExecPathMd5()
+{
+ static TString md5 = MD5::File(GetPersistentExecPath());
+ return md5;
+}
+
+static TMaybe<TSmallJobFile> GetJobState(const IJob& job)
+{
+ TString result;
+ {
+ TStringOutput output(result);
+ job.Save(output);
+ output.Finish();
+ }
+ if (result.empty()) {
+ return Nothing();
+ } else {
+ return TSmallJobFile{"jobstate", result};
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobPreparer::TJobPreparer(
+ TOperationPreparer& operationPreparer,
+ const TUserJobSpec& spec,
+ const IJob& job,
+ size_t outputTableCount,
+ const TVector<TSmallJobFile>& smallFileList,
+ const TOperationOptions& options)
+ : OperationPreparer_(operationPreparer)
+ , Spec_(spec)
+ , Options_(options)
+{
+
+ CreateStorage();
+ auto cypressFileList = CanonizeYPaths(/* retryPolicy */ nullptr, OperationPreparer_.GetContext(), spec.Files_);
+
+ for (const auto& file : cypressFileList) {
+ UseFileInCypress(file);
+ }
+ for (const auto& localFile : spec.GetLocalFiles()) {
+ UploadLocalFile(std::get<0>(localFile), std::get<1>(localFile));
+ }
+ auto jobStateSmallFile = GetJobState(job);
+ if (jobStateSmallFile) {
+ UploadSmallFile(*jobStateSmallFile);
+ }
+ for (const auto& smallFile : smallFileList) {
+ UploadSmallFile(smallFile);
+ }
+
+ if (auto commandJob = dynamic_cast<const ICommandJob*>(&job)) {
+ ClassName_ = TJobFactory::Get()->GetJobName(&job);
+ Command_ = commandJob->GetCommand();
+ } else {
+ PrepareJobBinary(job, outputTableCount, jobStateSmallFile.Defined());
+ }
+
+ operationPreparer.LockFiles(&CachedFiles_);
+}
+
+TVector<TRichYPath> TJobPreparer::GetFiles() const
+{
+ TVector<TRichYPath> allFiles = CypressFiles_;
+ allFiles.insert(allFiles.end(), CachedFiles_.begin(), CachedFiles_.end());
+ return allFiles;
+}
+
+const TString& TJobPreparer::GetClassName() const
+{
+ return ClassName_;
+}
+
+const TString& TJobPreparer::GetCommand() const
+{
+ return Command_;
+}
+
+const TUserJobSpec& TJobPreparer::GetSpec() const
+{
+ return Spec_;
+}
+
+bool TJobPreparer::ShouldMountSandbox() const
+{
+ return OperationPreparer_.GetContext().Config->MountSandboxInTmpfs || Options_.MountSandboxInTmpfs_;
+}
+
+ui64 TJobPreparer::GetTotalFileSize() const
+{
+ return TotalFileSize_;
+}
+
+TString TJobPreparer::GetFileStorage() const
+{
+ return Options_.FileStorage_ ?
+ *Options_.FileStorage_ :
+ OperationPreparer_.GetContext().Config->RemoteTempFilesDirectory;
+}
+
+TYPath TJobPreparer::GetCachePath() const
+{
+ return AddPathPrefix(
+ ::TStringBuilder() << GetFileStorage() << "/new_cache",
+ OperationPreparer_.GetContext().Config->Prefix);
+}
+
+void TJobPreparer::CreateStorage() const
+{
+ Create(
+ OperationPreparer_.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ OperationPreparer_.GetContext(),
+ Options_.FileStorageTransactionId_,
+ GetCachePath(),
+ NT_MAP,
+ TCreateOptions()
+ .IgnoreExisting(true)
+ .Recursive(true));
+}
+
+int TJobPreparer::GetFileCacheReplicationFactor() const
+{
+ if (IsLocalMode()) {
+ return 1;
+ } else {
+ return OperationPreparer_.GetContext().Config->FileCacheReplicationFactor;
+ }
+}
+
+void TJobPreparer::CreateFileInCypress(const TString& path) const
+{
+ auto attributes = TNode()("replication_factor", GetFileCacheReplicationFactor());
+ if (Options_.FileExpirationTimeout_) {
+ attributes["expiration_timeout"] = Options_.FileExpirationTimeout_->MilliSeconds();
+ }
+
+ Create(
+ OperationPreparer_.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ OperationPreparer_.GetContext(),
+ Options_.FileStorageTransactionId_,
+ path,
+ NT_FILE,
+ TCreateOptions()
+ .IgnoreExisting(true)
+ .Recursive(true)
+ .Attributes(attributes)
+ );
+}
+
+TString TJobPreparer::PutFileToCypressCache(
+ const TString& path,
+ const TString& md5Signature,
+ TTransactionId transactionId) const
+{
+ constexpr ui32 LockConflictRetryCount = 30;
+ auto retryPolicy = MakeIntrusive<TRetryPolicyIgnoringLockConflicts>(
+ LockConflictRetryCount,
+ OperationPreparer_.GetContext().Config);
+
+ auto putFileToCacheOptions = TPutFileToCacheOptions();
+ if (Options_.FileExpirationTimeout_) {
+ putFileToCacheOptions.PreserveExpirationTimeout(true);
+ }
+
+ auto cachePath = PutFileToCache(
+ retryPolicy,
+ OperationPreparer_.GetContext(),
+ transactionId,
+ path,
+ md5Signature,
+ GetCachePath(),
+ putFileToCacheOptions);
+
+ Remove(
+ OperationPreparer_.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ OperationPreparer_.GetContext(),
+ transactionId,
+ path,
+ TRemoveOptions().Force(true));
+
+ return cachePath;
+}
+
+TMaybe<TString> TJobPreparer::GetItemFromCypressCache(const TString& md5Signature, const TString& fileName) const
+{
+ constexpr ui32 LockConflictRetryCount = 30;
+ auto retryPolicy = MakeIntrusive<TRetryPolicyIgnoringLockConflicts>(
+ LockConflictRetryCount,
+ OperationPreparer_.GetContext().Config);
+ auto maybePath = GetFileFromCache(
+ retryPolicy,
+ OperationPreparer_.GetContext(),
+ TTransactionId(),
+ md5Signature,
+ GetCachePath(),
+ TGetFileFromCacheOptions());
+ if (maybePath) {
+ YT_LOG_DEBUG("File is already in cache (FileName: %v)",
+ fileName,
+ *maybePath);
+ }
+ return maybePath;
+}
+
+TDuration TJobPreparer::GetWaitForUploadTimeout(const IItemToUpload& itemToUpload) const
+{
+ const TDuration extraTime = OperationPreparer_.GetContext().Config->WaitLockPollInterval +
+ TDuration::MilliSeconds(100);
+ const double dataSizeGb = static_cast<double>(itemToUpload.GetDataSize()) / 1_GB;
+ return extraTime + dataSizeGb * OperationPreparer_.GetContext().Config->CacheLockTimeoutPerGb;
+}
+
+TString TJobPreparer::UploadToRandomPath(const IItemToUpload& itemToUpload) const
+{
+ TString uniquePath = AddPathPrefix(
+ ::TStringBuilder() << GetFileStorage() << "/cpp_" << CreateGuidAsString(),
+ OperationPreparer_.GetContext().Config->Prefix);
+ YT_LOG_INFO("Uploading file to random cypress path (FileName: %v; CypressPath: %v; PreparationId: %v)",
+ itemToUpload.GetDescription(),
+ uniquePath,
+ OperationPreparer_.GetPreparationId());
+
+ CreateFileInCypress(uniquePath);
+
+ {
+ TFileWriter writer(
+ uniquePath,
+ OperationPreparer_.GetClientRetryPolicy(),
+ OperationPreparer_.GetClient()->GetTransactionPinger(),
+ OperationPreparer_.GetContext(),
+ Options_.FileStorageTransactionId_,
+ TFileWriterOptions().ComputeMD5(true));
+ itemToUpload.CreateInputStream()->ReadAll(writer);
+ writer.Finish();
+ }
+ return uniquePath;
+}
+
+TMaybe<TString> TJobPreparer::TryUploadWithDeduplication(const IItemToUpload& itemToUpload) const
+{
+ const auto md5Signature = itemToUpload.CalculateMD5();
+
+ auto fileName = ::TStringBuilder() << GetFileStorage() << "/cpp_md5_" << md5Signature;
+ if (OperationPreparer_.GetContext().Config->CacheUploadDeduplicationMode == EUploadDeduplicationMode::Host) {
+ fileName << "_" << MD5::Data(TProcessState::Get()->FqdnHostName);
+ }
+ TString cypressPath = AddPathPrefix(fileName, OperationPreparer_.GetContext().Config->Prefix);
+
+ CreateFileInCypress(cypressPath);
+
+ auto uploadTx = MakeIntrusive<TTransaction>(
+ OperationPreparer_.GetClient(),
+ OperationPreparer_.GetContext(),
+ TTransactionId(),
+ TStartTransactionOptions());
+
+ ILockPtr lock;
+ try {
+ lock = uploadTx->Lock(cypressPath, ELockMode::LM_EXCLUSIVE, TLockOptions().Waitable(true));
+ } catch (const TErrorResponse& e) {
+ if (e.IsResolveError()) {
+ // If the node doesn't exist, it must be removed by concurrent uploading process.
+ // Let's try to find it in the cache.
+ return GetItemFromCypressCache(md5Signature, itemToUpload.GetDescription());
+ }
+ throw;
+ }
+
+ auto waitTimeout = GetWaitForUploadTimeout(itemToUpload);
+ YT_LOG_DEBUG("Waiting for the lock on file (FileName: %v; CypressPath: %v; LockTimeout: %v)",
+ itemToUpload.GetDescription(),
+ cypressPath,
+ waitTimeout);
+
+ if (!TWaitProxy::Get()->WaitFuture(lock->GetAcquiredFuture(), waitTimeout)) {
+ YT_LOG_DEBUG("Waiting for the lock timed out. Fallback to random path uploading (FileName: %v; CypressPath: %v)",
+ itemToUpload.GetDescription(),
+ cypressPath);
+ return Nothing();
+ }
+
+ YT_LOG_DEBUG("Exclusive lock successfully acquired (FileName: %v; CypressPath: %v)",
+ itemToUpload.GetDescription(),
+ cypressPath);
+
+ // Ensure that this process is the first to take a lock.
+ if (auto cachedItemPath = GetItemFromCypressCache(md5Signature, itemToUpload.GetDescription())) {
+ return *cachedItemPath;
+ }
+
+ YT_LOG_INFO("Uploading file to cypress (FileName: %v; CypressPath: %v; PreparationId: %v)",
+ itemToUpload.GetDescription(),
+ cypressPath,
+ OperationPreparer_.GetPreparationId());
+
+ {
+ auto writer = uploadTx->CreateFileWriter(cypressPath, TFileWriterOptions().ComputeMD5(true));
+ YT_VERIFY(writer);
+ itemToUpload.CreateInputStream()->ReadAll(*writer);
+ writer->Finish();
+ }
+
+ auto path = PutFileToCypressCache(cypressPath, md5Signature, uploadTx->GetId());
+
+ uploadTx->Commit();
+ return path;
+}
+
+TString TJobPreparer::UploadToCacheUsingApi(const IItemToUpload& itemToUpload) const
+{
+ auto md5Signature = itemToUpload.CalculateMD5();
+ Y_VERIFY(md5Signature.size() == 32);
+
+ if (auto cachedItemPath = GetItemFromCypressCache(md5Signature, itemToUpload.GetDescription())) {
+ return *cachedItemPath;
+ }
+
+ YT_LOG_INFO("File not found in cache; uploading to cypress (FileName: %v; PreparationId: %v)",
+ itemToUpload.GetDescription(),
+ OperationPreparer_.GetPreparationId());
+
+ if (OperationPreparer_.GetContext().Config->CacheUploadDeduplicationMode != EUploadDeduplicationMode::Disabled) {
+ if (auto path = TryUploadWithDeduplication(itemToUpload)) {
+ return *path;
+ }
+ }
+
+ auto path = UploadToRandomPath(itemToUpload);
+ return PutFileToCypressCache(path, md5Signature, Options_.FileStorageTransactionId_);
+}
+
+TString TJobPreparer::UploadToCache(const IItemToUpload& itemToUpload) const
+{
+ YT_LOG_INFO("Uploading file (FileName: %v; PreparationId: %v)",
+ itemToUpload.GetDescription(),
+ OperationPreparer_.GetPreparationId());
+
+ TString result;
+ switch (Options_.FileCacheMode_) {
+ case TOperationOptions::EFileCacheMode::ApiCommandBased:
+ Y_ENSURE_EX(Options_.FileStorageTransactionId_.IsEmpty(), TApiUsageError() <<
+ "Default cache mode (API command-based) doesn't allow non-default 'FileStorageTransactionId_'");
+ result = UploadToCacheUsingApi(itemToUpload);
+ break;
+ case TOperationOptions::EFileCacheMode::CachelessRandomPathUpload:
+ result = UploadToRandomPath(itemToUpload);
+ break;
+ default:
+ Y_FAIL("Unknown file cache mode: %d", static_cast<int>(Options_.FileCacheMode_));
+ }
+
+ YT_LOG_INFO("Complete uploading file (FileName: %v; PreparationId: %v)",
+ itemToUpload.GetDescription(),
+ OperationPreparer_.GetPreparationId());
+
+ return result;
+}
+
+void TJobPreparer::UseFileInCypress(const TRichYPath& file)
+{
+ if (!Exists(
+ OperationPreparer_.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ OperationPreparer_.GetContext(),
+ file.TransactionId_.GetOrElse(OperationPreparer_.GetTransactionId()),
+ file.Path_))
+ {
+ ythrow yexception() << "File " << file.Path_ << " does not exist";
+ }
+
+ if (ShouldMountSandbox()) {
+ auto size = Get(
+ OperationPreparer_.GetClientRetryPolicy()->CreatePolicyForGenericRequest(),
+ OperationPreparer_.GetContext(),
+ file.TransactionId_.GetOrElse(OperationPreparer_.GetTransactionId()),
+ file.Path_ + "/@uncompressed_data_size")
+ .AsInt64();
+
+ TotalFileSize_ += RoundUpFileSize(static_cast<ui64>(size));
+ }
+ CypressFiles_.push_back(file);
+}
+
+void TJobPreparer::UploadLocalFile(
+ const TLocalFilePath& localPath,
+ const TAddLocalFileOptions& options,
+ bool isApiFile)
+{
+ TFsPath fsPath(localPath);
+ fsPath.CheckExists();
+
+ TFileStat stat;
+ fsPath.Stat(stat);
+
+ bool isExecutable = stat.Mode & (S_IXUSR | S_IXGRP | S_IXOTH);
+ auto cachePath = UploadToCache(TFileToUpload(localPath, options.MD5CheckSum_));
+
+ TRichYPath cypressPath;
+ if (isApiFile) {
+ cypressPath = OperationPreparer_.GetContext().Config->ApiFilePathOptions;
+ }
+ cypressPath.Path(cachePath).FileName(options.PathInJob_.GetOrElse(fsPath.Basename()));
+ if (isExecutable) {
+ cypressPath.Executable(true);
+ }
+ if (options.BypassArtifactCache_) {
+ cypressPath.BypassArtifactCache(*options.BypassArtifactCache_);
+ }
+
+ if (ShouldMountSandbox()) {
+ TotalFileSize_ += RoundUpFileSize(stat.Size);
+ }
+
+ CachedFiles_.push_back(cypressPath);
+}
+
+void TJobPreparer::UploadBinary(const TJobBinaryConfig& jobBinary)
+{
+ if (std::holds_alternative<TJobBinaryLocalPath>(jobBinary)) {
+ auto binaryLocalPath = std::get<TJobBinaryLocalPath>(jobBinary);
+ auto opts = TAddLocalFileOptions().PathInJob("cppbinary");
+ if (binaryLocalPath.MD5CheckSum) {
+ opts.MD5CheckSum(*binaryLocalPath.MD5CheckSum);
+ }
+ UploadLocalFile(binaryLocalPath.Path, opts, /* isApiFile */ true);
+ } else if (std::holds_alternative<TJobBinaryCypressPath>(jobBinary)) {
+ auto binaryCypressPath = std::get<TJobBinaryCypressPath>(jobBinary);
+ TRichYPath ytPath = OperationPreparer_.GetContext().Config->ApiFilePathOptions;
+ ytPath.Path(binaryCypressPath.Path);
+ if (binaryCypressPath.TransactionId) {
+ ytPath.TransactionId(*binaryCypressPath.TransactionId);
+ }
+ UseFileInCypress(ytPath.FileName("cppbinary").Executable(true));
+ } else {
+ Y_FAIL("%s", (::TStringBuilder() << "Unexpected jobBinary tag: " << jobBinary.index()).data());
+ }
+}
+
+void TJobPreparer::UploadSmallFile(const TSmallJobFile& smallFile)
+{
+ auto cachePath = UploadToCache(TDataToUpload(smallFile.Data, smallFile.FileName + " [generated-file]"));
+ auto path = OperationPreparer_.GetContext().Config->ApiFilePathOptions;
+ CachedFiles_.push_back(path.Path(cachePath).FileName(smallFile.FileName));
+ if (ShouldMountSandbox()) {
+ TotalFileSize_ += RoundUpFileSize(smallFile.Data.size());
+ }
+}
+
+bool TJobPreparer::IsLocalMode() const
+{
+ return UseLocalModeOptimization(OperationPreparer_.GetContext(), OperationPreparer_.GetClientRetryPolicy());
+}
+
+void TJobPreparer::PrepareJobBinary(const IJob& job, int outputTableCount, bool hasState)
+{
+ auto jobBinary = TJobBinaryConfig();
+ if (!std::holds_alternative<TJobBinaryDefault>(Spec_.GetJobBinary())) {
+ jobBinary = Spec_.GetJobBinary();
+ }
+ TString binaryPathInsideJob;
+ if (std::holds_alternative<TJobBinaryDefault>(jobBinary)) {
+ if (GetInitStatus() != EInitStatus::FullInitialization) {
+ ythrow yexception() << "NYT::Initialize() must be called prior to any operation";
+ }
+
+ const bool isLocalMode = IsLocalMode();
+ const TMaybe<TString> md5 = !isLocalMode ? MakeMaybe(GetPersistentExecPathMd5()) : Nothing();
+ jobBinary = TJobBinaryLocalPath{GetPersistentExecPath(), md5};
+
+ if (isLocalMode) {
+ binaryPathInsideJob = GetExecPath();
+ }
+ } else if (std::holds_alternative<TJobBinaryLocalPath>(jobBinary)) {
+ const bool isLocalMode = IsLocalMode();
+ if (isLocalMode) {
+ binaryPathInsideJob = TFsPath(std::get<TJobBinaryLocalPath>(jobBinary).Path).RealPath();
+ }
+ }
+ Y_ASSERT(!std::holds_alternative<TJobBinaryDefault>(jobBinary));
+
+ // binaryPathInsideJob is only set when LocalModeOptimization option is on, so upload is not needed
+ if (!binaryPathInsideJob) {
+ binaryPathInsideJob = "./cppbinary";
+ UploadBinary(jobBinary);
+ }
+
+ TString jobCommandPrefix = Options_.JobCommandPrefix_;
+ if (!Spec_.JobCommandPrefix_.empty()) {
+ jobCommandPrefix = Spec_.JobCommandPrefix_;
+ }
+
+ TString jobCommandSuffix = Options_.JobCommandSuffix_;
+ if (!Spec_.JobCommandSuffix_.empty()) {
+ jobCommandSuffix = Spec_.JobCommandSuffix_;
+ }
+
+ ClassName_ = TJobFactory::Get()->GetJobName(&job);
+
+ auto jobArguments = TNode::CreateMap();
+ jobArguments["job_name"] = ClassName_;
+ jobArguments["output_table_count"] = static_cast<i64>(outputTableCount);
+ jobArguments["has_state"] = hasState;
+ Spec_.AddEnvironment("YT_JOB_ARGUMENTS", NodeToYsonString(jobArguments));
+
+ Command_ = ::TStringBuilder() <<
+ jobCommandPrefix <<
+ (OperationPreparer_.GetContext().Config->UseClientProtobuf ? "YT_USE_CLIENT_PROTOBUF=1" : "YT_USE_CLIENT_PROTOBUF=0") << " " <<
+ binaryPathInsideJob <<
+ jobCommandSuffix;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/operation_preparer.h b/yt/cpp/mapreduce/client/operation_preparer.h
new file mode 100644
index 0000000000..7ced54e3b5
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_preparer.h
@@ -0,0 +1,129 @@
+#pragma once
+
+#include "client.h"
+#include "structured_table_formats.h"
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperation;
+
+class TOperationPreparer
+ : public TThrRefBase
+{
+public:
+ TOperationPreparer(TClientPtr client, TTransactionId transactionId);
+
+ const TClientContext& GetContext() const;
+ TTransactionId GetTransactionId() const;
+ ITransactionPingerPtr GetTransactionPinger() const;
+ TClientPtr GetClient() const;
+
+ const TString& GetPreparationId() const;
+
+ void LockFiles(TVector<TRichYPath>* paths);
+
+ TOperationId StartOperation(
+ TOperation* operation,
+ const TString& operationType,
+ const TNode& spec,
+ bool useStartOperationRequest = false);
+
+ const IClientRetryPolicyPtr& GetClientRetryPolicy() const;
+
+private:
+ TClientPtr Client_;
+ TTransactionId TransactionId_;
+ THolder<TPingableTransaction> FileTransaction_;
+ IClientRetryPolicyPtr ClientRetryPolicy_;
+ const TString PreparationId_;
+
+private:
+ void CheckValidity() const;
+};
+
+using TOperationPreparerPtr = ::TIntrusivePtr<TOperationPreparer>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IItemToUpload
+{
+ virtual ~IItemToUpload() = default;
+
+ virtual TString CalculateMD5() const = 0;
+ virtual THolder<IInputStream> CreateInputStream() const = 0;
+ virtual TString GetDescription() const = 0;
+ virtual ui64 GetDataSize() const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJobPreparer
+ : private TNonCopyable
+{
+public:
+ TJobPreparer(
+ TOperationPreparer& operationPreparer,
+ const TUserJobSpec& spec,
+ const IJob& job,
+ size_t outputTableCount,
+ const TVector<TSmallJobFile>& smallFileList,
+ const TOperationOptions& options);
+
+ TVector<TRichYPath> GetFiles() const;
+ const TString& GetClassName() const;
+ const TString& GetCommand() const;
+ const TUserJobSpec& GetSpec() const;
+ bool ShouldMountSandbox() const;
+ ui64 GetTotalFileSize() const;
+
+private:
+ TOperationPreparer& OperationPreparer_;
+ TUserJobSpec Spec_;
+ TOperationOptions Options_;
+
+ TVector<TRichYPath> CypressFiles_;
+ TVector<TRichYPath> CachedFiles_;
+
+ TString ClassName_;
+ TString Command_;
+ ui64 TotalFileSize_ = 0;
+
+private:
+ TString GetFileStorage() const;
+ TYPath GetCachePath() const;
+
+ bool IsLocalMode() const;
+ int GetFileCacheReplicationFactor() const;
+
+ void CreateStorage() const;
+
+ void CreateFileInCypress(const TString& path) const;
+ TString PutFileToCypressCache(const TString& path, const TString& md5Signature, TTransactionId transactionId) const;
+ TMaybe<TString> GetItemFromCypressCache(const TString& md5Signature, const TString& fileName) const;
+
+ TDuration GetWaitForUploadTimeout(const IItemToUpload& itemToUpload) const;
+ TString UploadToRandomPath(const IItemToUpload& itemToUpload) const;
+ TString UploadToCacheUsingApi(const IItemToUpload& itemToUpload) const;
+ TMaybe<TString> TryUploadWithDeduplication(const IItemToUpload& itemToUpload) const;
+ TString UploadToCache(const IItemToUpload& itemToUpload) const;
+
+ void UseFileInCypress(const TRichYPath& file);
+
+ void UploadLocalFile(
+ const TLocalFilePath& localPath,
+ const TAddLocalFileOptions& options,
+ bool isApiFile = false);
+
+ void UploadBinary(const TJobBinaryConfig& jobBinary);
+ void UploadSmallFile(const TSmallJobFile& smallFile);
+
+ void PrepareJobBinary(const IJob& job, int outputTableCount, bool hasState);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/operation_tracker.cpp b/yt/cpp/mapreduce/client/operation_tracker.cpp
new file mode 100644
index 0000000000..56623e9927
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_tracker.cpp
@@ -0,0 +1,34 @@
+#include "operation_tracker.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TOperationExecutionTimeTracker::Start(const TOperationId& operationId) {
+ with_lock(Lock_) {
+ StartTimes_[operationId] = TInstant::Now();
+ }
+}
+
+TMaybe<TDuration> TOperationExecutionTimeTracker::Finish(const TOperationId& operationId) {
+ TDuration duration;
+ with_lock(Lock_) {
+ auto i = StartTimes_.find(operationId);
+ if (i == StartTimes_.end()) {
+ return Nothing();
+ }
+ duration = TInstant::Now() - i->second;
+ StartTimes_.erase(i);
+ }
+ return duration;
+}
+
+TOperationExecutionTimeTracker* TOperationExecutionTimeTracker::Get() {
+ return Singleton<TOperationExecutionTimeTracker>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/operation_tracker.h b/yt/cpp/mapreduce/client/operation_tracker.h
new file mode 100644
index 0000000000..9f1504ea91
--- /dev/null
+++ b/yt/cpp/mapreduce/client/operation_tracker.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/hash.h>
+#include <util/generic/maybe.h>
+#include <util/system/mutex.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperationExecutionTimeTracker {
+public:
+ void Start(const TOperationId& operationId);
+ TMaybe<TDuration> Finish(const TOperationId& operationId);
+ static TOperationExecutionTimeTracker* Get();
+
+private:
+ THashMap<TOperationId, TInstant> StartTimes_;
+ TMutex Lock_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/prepare_operation.cpp b/yt/cpp/mapreduce/client/prepare_operation.cpp
new file mode 100644
index 0000000000..7f772dc99a
--- /dev/null
+++ b/yt/cpp/mapreduce/client/prepare_operation.cpp
@@ -0,0 +1,286 @@
+#include "prepare_operation.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+
+#include <library/cpp/iterator/functools.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperationPreparationContext::TOperationPreparationContext(
+ const TStructuredJobTableList& structuredInputs,
+ const TStructuredJobTableList& structuredOutputs,
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& retryPolicy,
+ TTransactionId transactionId)
+ : Context_(context)
+ , RetryPolicy_(retryPolicy)
+ , TransactionId_(transactionId)
+ , InputSchemas_(structuredInputs.size())
+ , InputSchemasLoaded_(structuredInputs.size(), false)
+{
+ Inputs_.reserve(structuredInputs.size());
+ for (const auto& input : structuredInputs) {
+ Inputs_.push_back(input.RichYPath);
+ }
+ Outputs_.reserve(structuredOutputs.size());
+ for (const auto& output : structuredOutputs) {
+ Outputs_.push_back(output.RichYPath);
+ }
+}
+
+TOperationPreparationContext::TOperationPreparationContext(
+ TVector<TRichYPath> inputs,
+ TVector<TRichYPath> outputs,
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& retryPolicy,
+ TTransactionId transactionId)
+ : Context_(context)
+ , RetryPolicy_(retryPolicy)
+ , TransactionId_(transactionId)
+ , InputSchemas_(inputs.size())
+ , InputSchemasLoaded_(inputs.size(), false)
+{
+ Inputs_.reserve(inputs.size());
+ for (auto& input : inputs) {
+ Inputs_.push_back(std::move(input));
+ }
+ Outputs_.reserve(outputs.size());
+ for (const auto& output : outputs) {
+ Outputs_.push_back(std::move(output));
+ }
+}
+
+int TOperationPreparationContext::GetInputCount() const
+{
+ return static_cast<int>(Inputs_.size());
+}
+
+int TOperationPreparationContext::GetOutputCount() const
+{
+ return static_cast<int>(Outputs_.size());
+}
+
+const TVector<TTableSchema>& TOperationPreparationContext::GetInputSchemas() const
+{
+ TVector<::NThreading::TFuture<TNode>> schemaFutures;
+ NRawClient::TRawBatchRequest batch(Context_.Config);
+ for (int tableIndex = 0; tableIndex < static_cast<int>(InputSchemas_.size()); ++tableIndex) {
+ if (InputSchemasLoaded_[tableIndex]) {
+ schemaFutures.emplace_back();
+ continue;
+ }
+ Y_VERIFY(Inputs_[tableIndex]);
+ schemaFutures.push_back(batch.Get(TransactionId_, Inputs_[tableIndex]->Path_ + "/@schema", TGetOptions{}));
+ }
+
+ NRawClient::ExecuteBatch(
+ RetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ batch);
+
+ for (int tableIndex = 0; tableIndex < static_cast<int>(InputSchemas_.size()); ++tableIndex) {
+ if (schemaFutures[tableIndex].Initialized()) {
+ Deserialize(InputSchemas_[tableIndex], schemaFutures[tableIndex].ExtractValueSync());
+ }
+ }
+
+ return InputSchemas_;
+}
+
+const TTableSchema& TOperationPreparationContext::GetInputSchema(int index) const
+{
+ auto& schema = InputSchemas_[index];
+ if (!InputSchemasLoaded_[index]) {
+ Y_VERIFY(Inputs_[index]);
+ auto schemaNode = NRawClient::Get(
+ RetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ TransactionId_,
+ Inputs_[index]->Path_ + "/@schema");
+ Deserialize(schema, schemaNode);
+ }
+ return schema;
+}
+
+TMaybe<TYPath> TOperationPreparationContext::GetInputPath(int index) const
+{
+ Y_VERIFY(index < static_cast<int>(Inputs_.size()));
+ if (Inputs_[index]) {
+ return Inputs_[index]->Path_;
+ }
+ return Nothing();
+}
+
+TMaybe<TYPath> TOperationPreparationContext::GetOutputPath(int index) const
+{
+ Y_VERIFY(index < static_cast<int>(Outputs_.size()));
+ if (Outputs_[index]) {
+ return Outputs_[index]->Path_;
+ }
+ return Nothing();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSpeculativeOperationPreparationContext::TSpeculativeOperationPreparationContext(
+ const TVector<TTableSchema>& previousResult,
+ TStructuredJobTableList inputs,
+ TStructuredJobTableList outputs)
+ : InputSchemas_(previousResult)
+ , Inputs_(std::move(inputs))
+ , Outputs_(std::move(outputs))
+{
+ Y_VERIFY(Inputs_.size() == previousResult.size());
+}
+
+int TSpeculativeOperationPreparationContext::GetInputCount() const
+{
+ return static_cast<int>(Inputs_.size());
+}
+
+int TSpeculativeOperationPreparationContext::GetOutputCount() const
+{
+ return static_cast<int>(Outputs_.size());
+}
+
+const TVector<TTableSchema>& TSpeculativeOperationPreparationContext::GetInputSchemas() const
+{
+ return InputSchemas_;
+}
+
+const TTableSchema& TSpeculativeOperationPreparationContext::GetInputSchema(int index) const
+{
+ Y_VERIFY(index < static_cast<int>(InputSchemas_.size()));
+ return InputSchemas_[index];
+}
+
+TMaybe<TYPath> TSpeculativeOperationPreparationContext::GetInputPath(int index) const
+{
+ Y_VERIFY(index < static_cast<int>(Inputs_.size()));
+ if (Inputs_[index].RichYPath) {
+ return Inputs_[index].RichYPath->Path_;
+ }
+ return Nothing();
+}
+
+TMaybe<TYPath> TSpeculativeOperationPreparationContext::GetOutputPath(int index) const
+{
+ Y_VERIFY(index < static_cast<int>(Outputs_.size()));
+ if (Outputs_[index].RichYPath) {
+ return Outputs_[index].RichYPath->Path_;
+ }
+ return Nothing();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void FixInputTable(TRichYPath& table, int index, const TJobOperationPreparer& preparer)
+{
+ const auto& columnRenamings = preparer.GetInputColumnRenamings();
+ const auto& columnFilters = preparer.GetInputColumnFilters();
+
+ if (!columnRenamings[index].empty()) {
+ table.RenameColumns(columnRenamings[index]);
+ }
+ if (columnFilters[index]) {
+ table.Columns(*columnFilters[index]);
+ }
+}
+
+static void FixInputTable(TStructuredJobTable& table, int index, const TJobOperationPreparer& preparer)
+{
+ const auto& inputDescriptions = preparer.GetInputDescriptions();
+
+ if (inputDescriptions[index] && std::holds_alternative<TUnspecifiedTableStructure>(table.Description)) {
+ table.Description = *inputDescriptions[index];
+ }
+ if (table.RichYPath) {
+ FixInputTable(*table.RichYPath, index, preparer);
+ }
+}
+
+static void FixOutputTable(TRichYPath& /* table */, int /* index */, const TJobOperationPreparer& /* preparer */)
+{ }
+
+static void FixOutputTable(TStructuredJobTable& table, int index, const TJobOperationPreparer& preparer)
+{
+ const auto& outputDescriptions = preparer.GetOutputDescriptions();
+
+ if (outputDescriptions[index] && std::holds_alternative<TUnspecifiedTableStructure>(table.Description)) {
+ table.Description = *outputDescriptions[index];
+ }
+ if (table.RichYPath) {
+ FixOutputTable(*table.RichYPath, index, preparer);
+ }
+}
+
+template <typename TTables>
+TVector<TTableSchema> PrepareOperation(
+ const IJob& job,
+ const IOperationPreparationContext& context,
+ TTables* inputsPtr,
+ TTables* outputsPtr,
+ TUserJobFormatHints& hints)
+{
+ TJobOperationPreparer preparer(context);
+ job.PrepareOperation(context, preparer);
+ preparer.Finish();
+
+ if (inputsPtr) {
+ auto& inputs = *inputsPtr;
+ for (int i = 0; i < static_cast<int>(inputs.size()); ++i) {
+ FixInputTable(inputs[i], i, preparer);
+ }
+ }
+
+ if (outputsPtr) {
+ auto& outputs = *outputsPtr;
+ for (int i = 0; i < static_cast<int>(outputs.size()); ++i) {
+ FixOutputTable(outputs[i], i, preparer);
+ }
+ }
+
+ auto applyPatch = [](TMaybe<TFormatHints>& origin, const TMaybe<TFormatHints>& patch) {
+ if (origin) {
+ if (patch) {
+ origin->Merge(*patch);
+ }
+ } else {
+ origin = patch;
+ }
+ };
+
+ auto preparerHints = preparer.GetFormatHints();
+ applyPatch(preparerHints.InputFormatHints_, hints.InputFormatHints_);
+ applyPatch(preparerHints.OutputFormatHints_, hints.OutputFormatHints_);
+ hints = std::move(preparerHints);
+
+ return preparer.GetOutputSchemas();
+}
+
+template
+TVector<TTableSchema> PrepareOperation<TStructuredJobTableList>(
+ const IJob& job,
+ const IOperationPreparationContext& context,
+ TStructuredJobTableList* inputsPtr,
+ TStructuredJobTableList* outputsPtr,
+ TUserJobFormatHints& hints);
+
+template
+TVector<TTableSchema> PrepareOperation<TVector<TRichYPath>>(
+ const IJob& job,
+ const IOperationPreparationContext& context,
+ TVector<TRichYPath>* inputsPtr,
+ TVector<TRichYPath>* outputsPtr,
+ TUserJobFormatHints& hints);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/prepare_operation.h b/yt/cpp/mapreduce/client/prepare_operation.h
new file mode 100644
index 0000000000..3b64aa2856
--- /dev/null
+++ b/yt/cpp/mapreduce/client/prepare_operation.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include "structured_table_formats.h"
+
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOperationPreparationContext
+ : public IOperationPreparationContext
+{
+public:
+ TOperationPreparationContext(
+ const TStructuredJobTableList& structuredInputs,
+ const TStructuredJobTableList& structuredOutputs,
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& retryPolicy,
+ TTransactionId transactionId);
+
+ TOperationPreparationContext(
+ TVector<TRichYPath> inputs,
+ TVector<TRichYPath> outputs,
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& retryPolicy,
+ TTransactionId transactionId);
+
+ int GetInputCount() const override;
+ int GetOutputCount() const override;
+
+ const TVector<TTableSchema>& GetInputSchemas() const override;
+ const TTableSchema& GetInputSchema(int index) const override;
+
+ TMaybe<TYPath> GetInputPath(int index) const override;
+ TMaybe<TYPath> GetOutputPath(int index) const override;
+
+private:
+ TVector<TMaybe<TRichYPath>> Inputs_;
+ TVector<TMaybe<TRichYPath>> Outputs_;
+ const TClientContext& Context_;
+ const IClientRetryPolicyPtr RetryPolicy_;
+ TTransactionId TransactionId_;
+
+ mutable TVector<TTableSchema> InputSchemas_;
+ mutable TVector<bool> InputSchemasLoaded_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSpeculativeOperationPreparationContext
+ : public IOperationPreparationContext
+{
+public:
+ TSpeculativeOperationPreparationContext(
+ const TVector<TTableSchema>& previousResult,
+ TStructuredJobTableList inputs,
+ TStructuredJobTableList outputs);
+
+ int GetInputCount() const override;
+ int GetOutputCount() const override;
+
+ const TVector<TTableSchema>& GetInputSchemas() const override;
+ const TTableSchema& GetInputSchema(int index) const override;
+
+ TMaybe<TYPath> GetInputPath(int index) const override;
+ TMaybe<TYPath> GetOutputPath(int index) const override;
+
+private:
+ TVector<TTableSchema> InputSchemas_;
+ TStructuredJobTableList Inputs_;
+ TStructuredJobTableList Outputs_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TTables>
+TVector<TTableSchema> PrepareOperation(
+ const IJob& job,
+ const IOperationPreparationContext& context,
+ TTables* inputsPtr,
+ TTables* outputsPtr,
+ TUserJobFormatHints& hints);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobOperationPreparer GetOperationPreparer(
+ const IJob& job,
+ const IOperationPreparationContext& context);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/py_helpers.cpp b/yt/cpp/mapreduce/client/py_helpers.cpp
new file mode 100644
index 0000000000..3072449866
--- /dev/null
+++ b/yt/cpp/mapreduce/client/py_helpers.cpp
@@ -0,0 +1,112 @@
+#include "py_helpers.h"
+
+#include "client.h"
+#include "operation.h"
+#include "transaction.h"
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/fluent.h>
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/hash_set.h>
+
+namespace NYT {
+
+using namespace NDetail;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IStructuredJobPtr ConstructJob(const TString& jobName, const TString& state)
+{
+ auto node = TNode();
+ if (!state.empty()) {
+ node = NodeFromYsonString(state);
+ }
+ return TJobFactory::Get()->GetConstructingFunction(jobName.data())(node);
+}
+
+TString GetJobStateString(const IStructuredJob& job)
+{
+ TString result;
+ {
+ TStringOutput output(result);
+ job.Save(output);
+ output.Finish();
+ }
+ return result;
+}
+
+TStructuredJobTableList NodeToStructuredTablePaths(const TNode& node, const TOperationPreparer& preparer)
+{
+ int intermediateTableCount = 0;
+ TVector<TRichYPath> paths;
+ for (const auto& inputNode : node.AsList()) {
+ if (inputNode.IsNull()) {
+ ++intermediateTableCount;
+ } else {
+ paths.emplace_back(inputNode.AsString());
+ }
+ }
+ paths = NRawClient::CanonizeYPaths(/* retryPolicy */ nullptr, preparer.GetContext(), paths);
+ TStructuredJobTableList result(intermediateTableCount, TStructuredJobTable::Intermediate(TUnspecifiedTableStructure()));
+ for (const auto& path : paths) {
+ result.emplace_back(TStructuredJobTable{TUnspecifiedTableStructure(), path});
+ }
+ return result;
+}
+
+TString GetIOInfo(
+ const IStructuredJob& job,
+ const TCreateClientOptions& options,
+ const TString& cluster,
+ const TString& transactionId,
+ const TString& inputPaths,
+ const TString& outputPaths,
+ const TString& neededColumns)
+{
+ auto client = NDetail::CreateClientImpl(cluster, options);
+ TOperationPreparer preparer(client, GetGuid(transactionId));
+
+ auto structuredInputs = NodeToStructuredTablePaths(NodeFromYsonString(inputPaths), preparer);
+ auto structuredOutputs = NodeToStructuredTablePaths(NodeFromYsonString(outputPaths), preparer);
+
+ auto neededColumnsNode = NodeFromYsonString(neededColumns);
+ THashSet<TString> columnsUsedInOperations;
+ for (const auto& columnNode : neededColumnsNode.AsList()) {
+ columnsUsedInOperations.insert(columnNode.AsString());
+ }
+
+ auto operationIo = CreateSimpleOperationIoHelper(
+ job,
+ preparer,
+ TOperationOptions(),
+ std::move(structuredInputs),
+ std::move(structuredOutputs),
+ TUserJobFormatHints(),
+ ENodeReaderFormat::Yson,
+ columnsUsedInOperations);
+
+ return BuildYsonStringFluently().BeginMap()
+ .Item("input_format").Value(operationIo.InputFormat.Config)
+ .Item("output_format").Value(operationIo.OutputFormat.Config)
+ .Item("input_table_paths").List(operationIo.Inputs)
+ .Item("output_table_paths").List(operationIo.Outputs)
+ .Item("small_files").DoListFor(
+ operationIo.JobFiles.begin(),
+ operationIo.JobFiles.end(),
+ [] (TFluentList fluent, auto fileIt) {
+ fluent.Item().BeginMap()
+ .Item("file_name").Value(fileIt->FileName)
+ .Item("data").Value(fileIt->Data)
+ .EndMap();
+ })
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/py_helpers.h b/yt/cpp/mapreduce/client/py_helpers.h
new file mode 100644
index 0000000000..85aa0a93f3
--- /dev/null
+++ b/yt/cpp/mapreduce/client/py_helpers.h
@@ -0,0 +1,25 @@
+#include <yt/cpp/mapreduce/interface/client_method_options.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using IStructuredJobPtr = TIntrusiveConstPtr<IStructuredJob>;
+
+IStructuredJobPtr ConstructJob(const TString& jobName, const TString& state);
+
+TString GetJobStateString(const IStructuredJob& job);
+
+TString GetIOInfo(
+ const IStructuredJob& job,
+ const TCreateClientOptions& options,
+ const TString& cluster,
+ const TString& transactionId,
+ const TString& inputPaths,
+ const TString& outputPaths,
+ const TString& neededColumns);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp b/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
new file mode 100644
index 0000000000..b4e4975d7f
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retry_heavy_write_request.cpp
@@ -0,0 +1,87 @@
+#include "retry_heavy_write_request.h"
+
+#include "transaction.h"
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+namespace NYT {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RetryHeavyWriteRequest(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const ITransactionPingerPtr& transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ THttpHeader& header,
+ std::function<THolder<IInputStream>()> streamMaker)
+{
+ int retryCount = context.Config->RetryCount;
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+
+ for (int attempt = 0; attempt < retryCount; ++attempt) {
+ TPingableTransaction attemptTx(clientRetryPolicy, context, parentId, transactionPinger->GetChildTxPinger(), TStartTransactionOptions());
+
+ auto input = streamMaker();
+ TString requestId;
+
+ try {
+ auto hostName = GetProxyForHeavyRequest(context);
+ requestId = CreateGuidAsString();
+
+ header.AddTransactionId(attemptTx.GetId(), /* overwrite = */ true);
+ header.SetRequestCompression(ToString(context.Config->ContentEncoding));
+
+ auto request = context.HttpClient->StartRequest(GetFullUrl(hostName, context, header), requestId, header);
+ TransferData(input.Get(), request->GetStream());
+ request->Finish()->GetResponse();
+ } catch (TErrorResponse& e) {
+ YT_LOG_ERROR("RSP %v - attempt %v failed",
+ requestId,
+ attempt);
+
+ if (!IsRetriable(e) || attempt + 1 == retryCount) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(GetBackoffDuration(e, context.Config));
+ continue;
+
+ } catch (std::exception& e) {
+ YT_LOG_ERROR("RSP %v - %v - attempt %v failed",
+ requestId,
+ e.what(),
+ attempt);
+
+ if (attempt + 1 == retryCount) {
+ throw;
+ }
+ NDetail::TWaitProxy::Get()->Sleep(GetBackoffDuration(e, context.Config));
+ continue;
+ }
+
+ attemptTx.Commit();
+ return;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/retry_heavy_write_request.h b/yt/cpp/mapreduce/client/retry_heavy_write_request.h
new file mode 100644
index 0000000000..647cad302c
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retry_heavy_write_request.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+namespace NYT {
+
+///////////////////////////////////////////////////////////////////////////////
+
+void RetryHeavyWriteRequest(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const ITransactionPingerPtr& transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ THttpHeader& header,
+ std::function<THolder<IInputStream>()> streamMaker);
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/retry_transaction.h b/yt/cpp/mapreduce/client/retry_transaction.h
new file mode 100644
index 0000000000..5220c222b8
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retry_transaction.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/client/client.h>
+
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+namespace NYT::NDetail {
+
+template <typename TResult>
+TResult RetryTransactionWithPolicy(
+ const TClientBasePtr& client,
+ std::function<TResult(ITransactionPtr)> func,
+ IRequestRetryPolicyPtr retryPolicy)
+{
+ if (!retryPolicy) {
+ retryPolicy = CreateDefaultRequestRetryPolicy(client->GetContext().Config);
+ }
+
+ while (true) {
+ try {
+ retryPolicy->NotifyNewAttempt();
+ auto transaction = client->StartTransaction(TStartTransactionOptions());
+ if constexpr (std::is_same<TResult, void>::value) {
+ func(transaction);
+ transaction->Commit();
+ return;
+ } else {
+ auto result = func(transaction);
+ transaction->Commit();
+ return result;
+ }
+ } catch (const TErrorResponse& e) {
+ YT_LOG_ERROR("Retry failed %v - %v",
+ e.GetError().GetMessage(),
+ retryPolicy->GetAttemptDescription());
+
+ if (!IsRetriable(e)) {
+ throw;
+ }
+
+ auto maybeRetryTimeout = retryPolicy->OnRetriableError(e);
+ if (maybeRetryTimeout) {
+ TWaitProxy::Get()->Sleep(*maybeRetryTimeout);
+ } else {
+ throw;
+ }
+ } catch (const std::exception& e) {
+ YT_LOG_ERROR("Retry failed %v - %v",
+ e.what(),
+ retryPolicy->GetAttemptDescription());
+
+ if (!IsRetriable(e)) {
+ throw;
+ }
+
+ auto maybeRetryTimeout = retryPolicy->OnGenericError(e);
+ if (maybeRetryTimeout) {
+ TWaitProxy::Get()->Sleep(*maybeRetryTimeout);
+ } else {
+ throw;
+ }
+ }
+ }
+}
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/client/retryful_writer.cpp b/yt/cpp/mapreduce/client/retryful_writer.cpp
new file mode 100644
index 0000000000..12b2939ffa
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retryful_writer.cpp
@@ -0,0 +1,163 @@
+#include "retryful_writer.h"
+
+#include "retry_heavy_write_request.h"
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/finish_or_die.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <util/generic/size_literals.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRetryfulWriter::~TRetryfulWriter()
+{
+ NDetail::FinishOrDie(this, "TRetryfulWriter");
+}
+
+void TRetryfulWriter::CheckWriterState()
+{
+ switch (WriterState_) {
+ case Ok:
+ break;
+ case Completed:
+ ythrow TApiUsageError() << "Cannot use table writer that is finished";
+ case Error:
+ ythrow TApiUsageError() << "Cannot use table writer that finished with error";
+ }
+}
+
+void TRetryfulWriter::NotifyRowEnd()
+{
+ CheckWriterState();
+ if (Buffer_.Size() >= BufferSize_) {
+ FlushBuffer(false);
+ }
+}
+
+void TRetryfulWriter::DoWrite(const void* buf, size_t len)
+{
+ CheckWriterState();
+ while (Buffer_.Size() + len > Buffer_.Capacity()) {
+ Buffer_.Reserve(Buffer_.Capacity() * 2);
+ }
+ Buffer_.Append(static_cast<const char*>(buf), len);
+}
+
+void TRetryfulWriter::DoFinish()
+{
+ if (WriterState_ != Ok) {
+ return;
+ }
+ FlushBuffer(true);
+ if (Started_) {
+ FilledBuffers_.Stop();
+ Thread_.Join();
+ }
+ if (Exception_) {
+ WriterState_ = Error;
+ std::rethrow_exception(Exception_);
+ }
+ if (WriteTransaction_) {
+ WriteTransaction_->Commit();
+ }
+ WriterState_ = Completed;
+}
+
+void TRetryfulWriter::FlushBuffer(bool lastBlock)
+{
+ if (!Started_) {
+ if (lastBlock) {
+ try {
+ Send(Buffer_);
+ } catch (...) {
+ WriterState_ = Error;
+ throw;
+ }
+ return;
+ } else {
+ Started_ = true;
+ Thread_.Start();
+ }
+ }
+
+ auto emptyBuffer = EmptyBuffers_.Pop();
+ if (!emptyBuffer) {
+ WriterState_ = Error;
+ std::rethrow_exception(Exception_);
+ }
+ FilledBuffers_.Push(std::move(Buffer_));
+ Buffer_ = std::move(emptyBuffer.GetRef());
+}
+
+void TRetryfulWriter::Send(const TBuffer& buffer)
+{
+ THttpHeader header("PUT", Command_);
+ header.SetInputFormat(Format_);
+ header.MergeParameters(Parameters_);
+
+ auto streamMaker = [&buffer] () {
+ return MakeHolder<TBufferInput>(buffer);
+ };
+
+ auto transactionId = (WriteTransaction_ ? WriteTransaction_->GetId() : ParentTransactionId_);
+ RetryHeavyWriteRequest(ClientRetryPolicy_, TransactionPinger_, Context_, transactionId, header, streamMaker);
+
+ Parameters_ = SecondaryParameters_; // all blocks except the first one are appended
+}
+
+void TRetryfulWriter::SendThread()
+{
+ while (auto maybeBuffer = FilledBuffers_.Pop()) {
+ auto& buffer = maybeBuffer.GetRef();
+ try {
+ Send(buffer);
+ } catch (const std::exception&) {
+ Exception_ = std::current_exception();
+ EmptyBuffers_.Stop();
+ break;
+ }
+ buffer.Clear();
+ EmptyBuffers_.Push(std::move(buffer));
+ }
+}
+
+void* TRetryfulWriter::SendThread(void* opaque)
+{
+ static_cast<TRetryfulWriter*>(opaque)->SendThread();
+ return nullptr;
+}
+
+void TRetryfulWriter::Abort()
+{
+ if (Started_) {
+ FilledBuffers_.Stop();
+ Thread_.Join();
+ }
+ if (WriteTransaction_) {
+ WriteTransaction_->Abort();
+ }
+ WriterState_ = Completed;
+}
+
+size_t TRetryfulWriter::GetBufferSize(const TMaybe<TWriterOptions>& writerOptions)
+{
+ auto retryBlockSize = TMaybe<size_t>();
+ if (writerOptions) {
+ if (writerOptions->RetryBlockSize_) {
+ retryBlockSize = *writerOptions->RetryBlockSize_;
+ } else if (writerOptions->DesiredChunkSize_) {
+ retryBlockSize = *writerOptions->DesiredChunkSize_;
+ }
+ }
+ return retryBlockSize.GetOrElse(64_MB);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/retryful_writer.h b/yt/cpp/mapreduce/client/retryful_writer.h
new file mode 100644
index 0000000000..38e351977d
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retryful_writer.h
@@ -0,0 +1,130 @@
+#pragma once
+
+#include "transaction.h"
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/http/http.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/io/helpers.h>
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <library/cpp/threading/blocking_queue/blocking_queue.h>
+
+#include <util/stream/output.h>
+#include <util/generic/buffer.h>
+#include <util/stream/buffer.h>
+#include <util/system/thread.h>
+#include <util/system/event.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetryfulWriter
+ : public TRawTableWriter
+{
+public:
+ template <class TWriterOptions>
+ TRetryfulWriter(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ ITransactionPingerPtr transactionPinger,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ const TString& command,
+ const TMaybe<TFormat>& format,
+ const TRichYPath& path,
+ const TWriterOptions& options)
+ : ClientRetryPolicy_(std::move(clientRetryPolicy))
+ , TransactionPinger_(std::move(transactionPinger))
+ , Context_(context)
+ , Command_(command)
+ , Format_(format)
+ , BufferSize_(GetBufferSize(options.WriterOptions_))
+ , ParentTransactionId_(parentId)
+ , WriteTransaction_()
+ , FilledBuffers_(2)
+ , EmptyBuffers_(2)
+ , Buffer_(BufferSize_ * 2)
+ , Thread_(TThread::TParams{SendThread, this}.SetName("retryful_writer"))
+ {
+ Parameters_ = FormIORequestParameters(path, options);
+
+ auto secondaryPath = path;
+ secondaryPath.Append_ = true;
+ secondaryPath.Schema_.Clear();
+ secondaryPath.CompressionCodec_.Clear();
+ secondaryPath.ErasureCodec_.Clear();
+ secondaryPath.OptimizeFor_.Clear();
+ SecondaryParameters_ = FormIORequestParameters(secondaryPath, options);
+
+ if (options.CreateTransaction_) {
+ WriteTransaction_.ConstructInPlace(ClientRetryPolicy_, context, parentId, TransactionPinger_->GetChildTxPinger(), TStartTransactionOptions());
+ auto append = path.Append_.GetOrElse(false);
+ auto lockMode = (append ? LM_SHARED : LM_EXCLUSIVE);
+ NDetail::NRawClient::Lock(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, WriteTransaction_->GetId(), path.Path_, lockMode);
+ }
+
+ EmptyBuffers_.Push(TBuffer(BufferSize_ * 2));
+ }
+
+ ~TRetryfulWriter() override;
+ void NotifyRowEnd() override;
+ void Abort() override;
+
+ size_t GetRetryBlockRemainingSize() const
+ {
+ return (BufferSize_ > Buffer_.size()) ? (BufferSize_ - Buffer_.size()) : 0;
+ }
+
+protected:
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFinish() override;
+
+private:
+ static size_t GetBufferSize(const TMaybe<TWriterOptions>& writerOptions);
+
+private:
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+ const ITransactionPingerPtr TransactionPinger_;
+ const TClientContext Context_;
+ TString Command_;
+ TMaybe<TFormat> Format_;
+ const size_t BufferSize_;
+
+ TNode Parameters_;
+ TNode SecondaryParameters_;
+
+ TTransactionId ParentTransactionId_;
+ TMaybe<TPingableTransaction> WriteTransaction_;
+
+ ::NThreading::TBlockingQueue<TBuffer> FilledBuffers_;
+ ::NThreading::TBlockingQueue<TBuffer> EmptyBuffers_;
+
+ TBuffer Buffer_;
+
+ TThread Thread_;
+ bool Started_ = false;
+ std::exception_ptr Exception_ = nullptr;
+
+ enum EWriterState {
+ Ok,
+ Completed,
+ Error,
+ } WriterState_ = Ok;
+
+private:
+ void FlushBuffer(bool lastBlock);
+ void Send(const TBuffer& buffer);
+ void CheckWriterState();
+
+ void SendThread();
+ static void* SendThread(void* opaque);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/yt/cpp/mapreduce/client/retryless_writer.cpp b/yt/cpp/mapreduce/client/retryless_writer.cpp
new file mode 100644
index 0000000000..4c25c1a1dd
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retryless_writer.cpp
@@ -0,0 +1,45 @@
+#include "retryless_writer.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRetrylessWriter::~TRetrylessWriter()
+{
+ NDetail::FinishOrDie(this, "TRetrylessWriter");
+}
+
+void TRetrylessWriter::DoFinish()
+{
+ if (!Running_) {
+ return;
+ }
+ Running_ = false;
+
+ BufferedOutput_->Finish();
+ Request_->Finish()->GetResponse();
+}
+
+void TRetrylessWriter::DoWrite(const void* buf, size_t len)
+{
+ try {
+ BufferedOutput_->Write(buf, len);
+ } catch (...) {
+ Running_ = false;
+ throw;
+ }
+}
+
+void TRetrylessWriter::NotifyRowEnd()
+{ }
+
+void TRetrylessWriter::Abort()
+{
+ Running_ = false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/retryless_writer.h b/yt/cpp/mapreduce/client/retryless_writer.h
new file mode 100644
index 0000000000..baf49a258f
--- /dev/null
+++ b/yt/cpp/mapreduce/client/retryless_writer.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "transaction.h"
+
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/http.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/io/helpers.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <util/stream/buffered.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetrylessWriter
+ : public TRawTableWriter
+{
+public:
+ template <class TWriterOptions>
+ TRetrylessWriter(
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ const TString& command,
+ const TMaybe<TFormat>& format,
+ const TRichYPath& path,
+ size_t bufferSize,
+ const TWriterOptions& options)
+ {
+ THttpHeader header("PUT", command);
+ header.SetInputFormat(format);
+ header.MergeParameters(FormIORequestParameters(path, options));
+ header.AddTransactionId(parentId);
+ header.SetRequestCompression(ToString(context.Config->ContentEncoding));
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+
+ TString requestId = CreateGuidAsString();
+
+ auto hostName = GetProxyForHeavyRequest(context);
+ Request_ = context.HttpClient->StartRequest(GetFullUrl(hostName, context, header), requestId, header);
+ BufferedOutput_.Reset(new TBufferedOutput(Request_->GetStream(), bufferSize));
+ }
+
+ ~TRetrylessWriter() override;
+ void NotifyRowEnd() override;
+ void Abort() override;
+
+protected:
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFinish() override;
+
+private:
+ bool Running_ = true;
+ NHttpClient::IHttpRequestPtr Request_;
+ THolder<TBufferedOutput> BufferedOutput_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/skiff.cpp b/yt/cpp/mapreduce/client/skiff.cpp
new file mode 100644
index 0000000000..67a0f960ae
--- /dev/null
+++ b/yt/cpp/mapreduce/client/skiff.cpp
@@ -0,0 +1,396 @@
+#include "skiff.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/http/retry_request.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <yt/cpp/mapreduce/skiff/skiff_schema.h>
+
+#include <library/cpp/yson/consumer.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/string/cast.h>
+#include <util/stream/str.h>
+#include <util/stream/file.h>
+#include <util/folder/path.h>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NSkiff::TSkiffSchemaPtr ReadSkiffSchema(const TString& fileName)
+{
+ if (!TFsPath(fileName).Exists()) {
+ return nullptr;
+ }
+ TIFStream input(fileName);
+ NSkiff::TSkiffSchemaPtr schema;
+ Deserialize(schema, NodeFromYsonStream(&input));
+ return schema;
+}
+
+NSkiff::TSkiffSchemaPtr GetJobInputSkiffSchema()
+{
+ return ReadSkiffSchema("skiff_input");
+}
+
+NSkiff::EWireType ValueTypeToSkiffType(EValueType valueType)
+{
+ using NSkiff::EWireType;
+ switch (valueType) {
+ case VT_INT64:
+ case VT_INT32:
+ case VT_INT16:
+ case VT_INT8:
+ return EWireType::Int64;
+
+ case VT_UINT64:
+ case VT_UINT32:
+ case VT_UINT16:
+ case VT_UINT8:
+ return EWireType::Uint64;
+
+ case VT_DOUBLE:
+ case VT_FLOAT:
+ return EWireType::Double;
+
+ case VT_BOOLEAN:
+ return EWireType::Boolean;
+
+ case VT_STRING:
+ case VT_UTF8:
+ case VT_JSON:
+ return EWireType::String32;
+
+ case VT_ANY:
+ return EWireType::Yson32;
+
+ case VT_NULL:
+ case VT_VOID:
+ return EWireType::Nothing;
+
+ case VT_DATE:
+ case VT_DATETIME:
+ case VT_TIMESTAMP:
+ return EWireType::Uint64;
+
+ case VT_INTERVAL:
+ return EWireType::Int64;
+ };
+ ythrow yexception() << "Cannot convert EValueType '" << valueType << "' to NSkiff::EWireType";
+}
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TTableSchema& schema,
+ const TCreateSkiffSchemaOptions& options)
+{
+ using namespace NSkiff;
+
+ Y_ENSURE(schema.Strict(), "Cannot create Skiff schema for non-strict table schema");
+ TVector<TSkiffSchemaPtr> skiffColumns;
+ for (const auto& column: schema.Columns()) {
+ TSkiffSchemaPtr skiffColumn;
+ if (column.Type() == VT_ANY && *column.TypeV3() != *NTi::Optional(NTi::Yson())) {
+ // We ignore all complex types until YT-12717 is done.
+ return nullptr;
+ }
+ if (column.Required() || NTi::IsSingular(column.TypeV3()->GetTypeName())) {
+ skiffColumn = CreateSimpleTypeSchema(ValueTypeToSkiffType(column.Type()));
+ } else {
+ skiffColumn = CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(ValueTypeToSkiffType(column.Type()))});
+ }
+ if (options.RenameColumns_) {
+ auto maybeName = options.RenameColumns_->find(column.Name());
+ skiffColumn->SetName(maybeName == options.RenameColumns_->end() ? column.Name() : maybeName->second);
+ } else {
+ skiffColumn->SetName(column.Name());
+ }
+ skiffColumns.push_back(skiffColumn);
+ }
+
+ if (options.HasKeySwitch_) {
+ skiffColumns.push_back(
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$key_switch"));
+ }
+ if (options.HasRangeIndex_) {
+ skiffColumns.push_back(
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64)})
+ ->SetName("$range_index"));
+ }
+
+ skiffColumns.push_back(
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64)})
+ ->SetName("$row_index"));
+
+ return CreateTupleSchema(std::move(skiffColumns));
+}
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TNode& schemaNode,
+ const TCreateSkiffSchemaOptions& options)
+{
+ TTableSchema schema;
+ Deserialize(schema, schemaNode);
+ return CreateSkiffSchema(schema, options);
+}
+
+void Serialize(const NSkiff::TSkiffSchemaPtr& schema, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ if (schema->GetName().size() > 0) {
+ consumer->OnKeyedItem("name");
+ consumer->OnStringScalar(schema->GetName());
+ }
+ consumer->OnKeyedItem("wire_type");
+ consumer->OnStringScalar(ToString(schema->GetWireType()));
+ if (schema->GetChildren().size() > 0) {
+ consumer->OnKeyedItem("children");
+ consumer->OnBeginList();
+ for (const auto& child : schema->GetChildren()) {
+ consumer->OnListItem();
+ Serialize(child, consumer);
+ }
+ consumer->OnEndList();
+ }
+ consumer->OnEndMap();
+}
+
+void Deserialize(NSkiff::TSkiffSchemaPtr& schema, const TNode& node)
+{
+ using namespace NSkiff;
+
+ static auto createSchema = [](EWireType wireType, TVector<TSkiffSchemaPtr>&& children) -> TSkiffSchemaPtr {
+ switch (wireType) {
+ case EWireType::Tuple:
+ return CreateTupleSchema(std::move(children));
+ case EWireType::Variant8:
+ return CreateVariant8Schema(std::move(children));
+ case EWireType::Variant16:
+ return CreateVariant16Schema(std::move(children));
+ case EWireType::RepeatedVariant8:
+ return CreateRepeatedVariant8Schema(std::move(children));
+ case EWireType::RepeatedVariant16:
+ return CreateRepeatedVariant16Schema(std::move(children));
+ default:
+ return CreateSimpleTypeSchema(wireType);
+ }
+ };
+
+ const auto& map = node.AsMap();
+ const auto* wireTypePtr = map.FindPtr("wire_type");
+ Y_ENSURE(wireTypePtr, "'wire_type' is a required key");
+ auto wireType = FromString<NSkiff::EWireType>(wireTypePtr->AsString());
+
+ const auto* childrenPtr = map.FindPtr("children");
+ Y_ENSURE(NSkiff::IsSimpleType(wireType) || childrenPtr,
+ "'children' key is required for complex node '" << wireType << "'");
+ TVector<TSkiffSchemaPtr> children;
+ if (childrenPtr) {
+ for (const auto& childNode : childrenPtr->AsList()) {
+ TSkiffSchemaPtr childSchema;
+ Deserialize(childSchema, childNode);
+ children.push_back(std::move(childSchema));
+ }
+ }
+
+ schema = createSchema(wireType, std::move(children));
+
+ const auto* namePtr = map.FindPtr("name");
+ if (namePtr) {
+ schema->SetName(namePtr->AsString());
+ }
+}
+
+TFormat CreateSkiffFormat(const NSkiff::TSkiffSchemaPtr& schema) {
+ Y_ENSURE(schema->GetWireType() == NSkiff::EWireType::Variant16,
+ "Bad wire type for schema; expected 'variant16', got " << schema->GetWireType());
+
+ THashMap<
+ NSkiff::TSkiffSchemaPtr,
+ size_t,
+ NSkiff::TSkiffSchemaPtrHasher,
+ NSkiff::TSkiffSchemaPtrEqual> schemasMap;
+ size_t tableIndex = 0;
+ auto config = TNode("skiff");
+ config.Attributes()["table_skiff_schemas"] = TNode::CreateList();
+
+ for (const auto& schemaChild : schema->GetChildren()) {
+ auto [iter, inserted] = schemasMap.emplace(schemaChild, tableIndex);
+ size_t currentIndex;
+ if (inserted) {
+ currentIndex = tableIndex;
+ ++tableIndex;
+ } else {
+ currentIndex = iter->second;
+ }
+ config.Attributes()["table_skiff_schemas"].Add("$" + ToString(currentIndex));
+ }
+
+ config.Attributes()["skiff_schema_registry"] = TNode::CreateMap();
+
+ for (const auto& [tableSchema, index] : schemasMap) {
+ TNode node;
+ TNodeBuilder nodeBuilder(&node);
+ Serialize(tableSchema, &nodeBuilder);
+ config.Attributes()["skiff_schema_registry"][ToString(index)] = std::move(node);
+ }
+
+ return TFormat(config);
+}
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchemaIfNecessary(
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TTransactionId& transactionId,
+ ENodeReaderFormat nodeReaderFormat,
+ const TVector<TRichYPath>& tablePaths,
+ const TCreateSkiffSchemaOptions& options)
+{
+ if (nodeReaderFormat == ENodeReaderFormat::Yson) {
+ return nullptr;
+ }
+
+ for (const auto& path : tablePaths) {
+ if (path.Columns_) {
+ switch (nodeReaderFormat) {
+ case ENodeReaderFormat::Skiff:
+ ythrow TApiUsageError() << "Cannot use Skiff format with column selectors";
+ case ENodeReaderFormat::Auto:
+ return nullptr;
+ default:
+ Y_FAIL("Unexpected node reader format: %d", static_cast<int>(nodeReaderFormat));
+ }
+ }
+ }
+
+ auto nodes = NRawClient::BatchTransform(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ NRawClient::CanonizeYPaths(clientRetryPolicy->CreatePolicyForGenericRequest(), context, tablePaths),
+ [&] (TRawBatchRequest& batch, const TRichYPath& path) {
+ auto getOptions = TGetOptions()
+ .AttributeFilter(
+ TAttributeFilter()
+ .AddAttribute("schema")
+ .AddAttribute("dynamic")
+ .AddAttribute("type")
+ );
+ return batch.Get(transactionId, path.Path_, getOptions);
+ });
+
+ TVector<NSkiff::TSkiffSchemaPtr> schemas;
+ for (size_t tableIndex = 0; tableIndex < nodes.size(); ++tableIndex) {
+ const auto& tablePath = tablePaths[tableIndex].Path_;
+ const auto& attributes = nodes[tableIndex].GetAttributes();
+ Y_ENSURE_EX(attributes["type"] == TNode("table"),
+ TApiUsageError() << "Operation input path " << tablePath << " is not a table");
+ bool dynamic = attributes["dynamic"].AsBool();
+ bool strict = attributes["schema"].GetAttributes()["strict"].AsBool();
+ switch (nodeReaderFormat) {
+ case ENodeReaderFormat::Skiff:
+ Y_ENSURE_EX(strict,
+ TApiUsageError() << "Cannot use skiff format for table with non-strict schema '" << tablePath << "'");
+ Y_ENSURE_EX(!dynamic,
+ TApiUsageError() << "Cannot use skiff format for dynamic table '" << tablePath << "'");
+ break;
+ case ENodeReaderFormat::Auto:
+ if (dynamic || !strict) {
+ YT_LOG_DEBUG("Cannot use skiff format for table '%v' as it is dynamic or has non-strict schema",
+ tablePath);
+ return nullptr;
+ }
+ break;
+ default:
+ Y_FAIL("Unexpected node reader format: %d", static_cast<int>(nodeReaderFormat));
+ }
+
+ NSkiff::TSkiffSchemaPtr curSkiffSchema;
+ if (tablePaths[tableIndex].RenameColumns_) {
+ auto customOptions = options;
+ customOptions.RenameColumns(*tablePaths[tableIndex].RenameColumns_);
+ curSkiffSchema = CreateSkiffSchema(attributes["schema"], customOptions);
+ } else {
+ curSkiffSchema = CreateSkiffSchema(attributes["schema"], options);
+ }
+
+ if (!curSkiffSchema) {
+ return nullptr;
+ }
+ schemas.push_back(curSkiffSchema);
+ }
+ return NSkiff::CreateVariant16Schema(std::move(schemas));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TVector<NSkiff::TSkiffSchemaPtr>& tableSchemas,
+ const TCreateSkiffSchemaOptions& options
+) {
+ constexpr auto KEY_SWITCH_COLUMN = "$key_switch";
+ constexpr auto ROW_INDEX_COLUMN = "$row_index";
+ constexpr auto RANGE_INDEX_COLUMN = "$range_index";
+
+ TVector<NSkiff::TSkiffSchemaPtr> schemas;
+ schemas.reserve(tableSchemas.size());
+
+ for (const auto& tableSchema : tableSchemas) {
+ Y_ENSURE(tableSchema->GetWireType() == NSkiff::EWireType::Tuple,
+ "Expected 'tuple' wire type for table schema, got '" << tableSchema->GetWireType() << "'");
+
+ const auto& children = tableSchema->GetChildren();
+ NSkiff::TSkiffSchemaList columns;
+
+ columns.reserve(children.size() + 3);
+ if (options.HasKeySwitch_) {
+ columns.push_back(
+ CreateSimpleTypeSchema(NSkiff::EWireType::Boolean)->SetName(KEY_SWITCH_COLUMN));
+ }
+ columns.push_back(
+ NSkiff::CreateVariant8Schema({
+ CreateSimpleTypeSchema(NSkiff::EWireType::Nothing),
+ CreateSimpleTypeSchema(NSkiff::EWireType::Int64)})
+ ->SetName(ROW_INDEX_COLUMN));
+ if (options.HasRangeIndex_) {
+ columns.push_back(
+ NSkiff::CreateVariant8Schema({
+ CreateSimpleTypeSchema(NSkiff::EWireType::Nothing),
+ CreateSimpleTypeSchema(NSkiff::EWireType::Int64)})
+ ->SetName(RANGE_INDEX_COLUMN));
+ }
+ columns.insert(columns.end(), children.begin(), children.end());
+
+ schemas.push_back(NSkiff::CreateTupleSchema(columns));
+ }
+
+ return NSkiff::CreateVariant16Schema(schemas);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/skiff.h b/yt/cpp/mapreduce/client/skiff.h
new file mode 100644
index 0000000000..82d80a4967
--- /dev/null
+++ b/yt/cpp/mapreduce/client/skiff.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <yt/cpp/mapreduce/skiff/wire_type.h>
+#include <yt/cpp/mapreduce/skiff/skiff_schema.h>
+
+#include <util/generic/vector.h>
+
+namespace NYT::NYson {
+struct IYsonConsumer;
+} // namespace NYT::NYson
+
+namespace NYT {
+
+struct TClientContext;
+enum class ENodeReaderFormat : int;
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCreateSkiffSchemaOptions
+{
+ using TSelf = TCreateSkiffSchemaOptions;
+
+ FLUENT_FIELD_DEFAULT(bool, HasKeySwitch, false);
+ FLUENT_FIELD_DEFAULT(bool, HasRangeIndex, false);
+
+ using TRenameColumnsDescriptor = THashMap<TString, TString>;
+ FLUENT_FIELD_OPTION(TRenameColumnsDescriptor, RenameColumns);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TVector<NSkiff::TSkiffSchemaPtr>& tableSchemas,
+ const TCreateSkiffSchemaOptions& options);
+
+NSkiff::TSkiffSchemaPtr GetJobInputSkiffSchema();
+
+NSkiff::EWireType ValueTypeToSkiffType(EValueType valueType);
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TTableSchema& schema,
+ const TCreateSkiffSchemaOptions& options = TCreateSkiffSchemaOptions());
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchema(
+ const TNode& schemaNode,
+ const TCreateSkiffSchemaOptions& options = TCreateSkiffSchemaOptions());
+
+void Serialize(const NSkiff::TSkiffSchemaPtr& schema, NYson::IYsonConsumer* consumer);
+
+void Deserialize(NSkiff::TSkiffSchemaPtr& schema, const TNode& node);
+
+TFormat CreateSkiffFormat(const NSkiff::TSkiffSchemaPtr& schema);
+
+NSkiff::TSkiffSchemaPtr CreateSkiffSchemaIfNecessary(
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TTransactionId& transactionId,
+ ENodeReaderFormat nodeReaderFormat,
+ const TVector<TRichYPath>& tablePaths,
+ const TCreateSkiffSchemaOptions& options = TCreateSkiffSchemaOptions());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/structured_table_formats.cpp b/yt/cpp/mapreduce/client/structured_table_formats.cpp
new file mode 100644
index 0000000000..b6e82c6c15
--- /dev/null
+++ b/yt/cpp/mapreduce/client/structured_table_formats.cpp
@@ -0,0 +1,572 @@
+#include "structured_table_formats.h"
+
+#include "format_hints.h"
+#include "skiff.h"
+
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/io/yamr_table_reader.h>
+
+#include <yt/cpp/mapreduce/library/table_schema/protobuf.h>
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <library/cpp/type_info/type_info.h>
+#include <library/cpp/yson/writer.h>
+
+#include <memory>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMaybe<TNode> GetCommonTableFormat(
+ const TVector<TMaybe<TNode>>& formats)
+{
+ TMaybe<TNode> result;
+ bool start = true;
+ for (auto& format : formats) {
+ if (start) {
+ result = format;
+ start = false;
+ continue;
+ }
+
+ if (result.Defined() != format.Defined()) {
+ ythrow yexception() << "Different formats of input tables";
+ }
+
+ if (!result.Defined()) {
+ continue;
+ }
+
+ auto& resultAttrs = result.Get()->GetAttributes();
+ auto& formatAttrs = format.Get()->GetAttributes();
+
+ if (resultAttrs["key_column_names"] != formatAttrs["key_column_names"]) {
+ ythrow yexception() << "Different formats of input tables";
+ }
+
+ bool hasSubkeyColumns = resultAttrs.HasKey("subkey_column_names");
+ if (hasSubkeyColumns != formatAttrs.HasKey("subkey_column_names")) {
+ ythrow yexception() << "Different formats of input tables";
+ }
+
+ if (hasSubkeyColumns &&
+ resultAttrs["subkey_column_names"] != formatAttrs["subkey_column_names"])
+ {
+ ythrow yexception() << "Different formats of input tables";
+ }
+ }
+
+ return result;
+}
+
+TMaybe<TNode> GetTableFormat(
+ const IClientRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TRichYPath& path)
+{
+ auto formatPath = path.Path_ + "/@_format";
+ if (!NDetail::NRawClient::Exists(retryPolicy->CreatePolicyForGenericRequest(), context, transactionId, formatPath)) {
+ return TMaybe<TNode>();
+ }
+ TMaybe<TNode> format = NDetail::NRawClient::Get(retryPolicy->CreatePolicyForGenericRequest(), context, transactionId, formatPath);
+ if (format.Get()->AsString() != "yamred_dsv") {
+ return TMaybe<TNode>();
+ }
+ auto& formatAttrs = format.Get()->Attributes();
+ if (!formatAttrs.HasKey("key_column_names")) {
+ ythrow yexception() <<
+ "Table '" << path.Path_ << "': attribute 'key_column_names' is missing";
+ }
+ formatAttrs["has_subkey"] = "true";
+ formatAttrs["lenval"] = "true";
+ return format;
+}
+
+TMaybe<TNode> GetTableFormats(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& inputs)
+{
+ TVector<TMaybe<TNode>> formats;
+ for (auto& table : inputs) {
+ formats.push_back(GetTableFormat(clientRetryPolicy, context, transactionId, table));
+ }
+
+ return GetCommonTableFormat(formats);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NSkiff::TSkiffSchemaPtr TryCreateSkiffSchema(
+ const TClientContext& context,
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& tables,
+ const TOperationOptions& options,
+ ENodeReaderFormat nodeReaderFormat)
+{
+ bool hasInputQuery = options.Spec_.Defined() && options.Spec_->IsMap() && options.Spec_->HasKey("input_query");
+ if (hasInputQuery) {
+ Y_ENSURE_EX(nodeReaderFormat != ENodeReaderFormat::Skiff,
+ TApiUsageError() << "Cannot use Skiff format for operations with 'input_query' in spec");
+ return nullptr;
+ }
+ return CreateSkiffSchemaIfNecessary(
+ context,
+ clientRetryPolicy,
+ transactionId,
+ nodeReaderFormat,
+ tables,
+ TCreateSkiffSchemaOptions()
+ .HasKeySwitch(true)
+ .HasRangeIndex(true));
+}
+
+TString CreateSkiffConfig(const NSkiff::TSkiffSchemaPtr& schema)
+{
+ TString result;
+ TStringOutput stream(result);
+ ::NYson::TYsonWriter writer(&stream);
+ Serialize(schema, &writer);
+ return result;
+}
+
+TString CreateProtoConfig(const TVector<const ::google::protobuf::Descriptor*>& descriptorList)
+{
+ TString result;
+ TStringOutput messageTypeList(result);
+ for (const auto& descriptor : descriptorList) {
+ messageTypeList << descriptor->full_name() << Endl;
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGetTableStructureDescriptionStringImpl {
+ template<typename T>
+ TString operator()(const T& description) {
+ if constexpr (std::is_same_v<T, TUnspecifiedTableStructure>) {
+ return "Unspecified";
+ } else if constexpr (std::is_same_v<T, TProtobufTableStructure>) {
+ TString res;
+ TStringStream out(res);
+ if (description.Descriptor) {
+ out << description.Descriptor->full_name();
+ } else {
+ out << "<unknown>";
+ }
+ out << " protobuf message";
+ return res;
+ } else {
+ static_assert(TDependentFalse<T>, "Unknown type");
+ }
+ }
+};
+
+TString GetTableStructureDescriptionString(const TTableStructure& tableStructure)
+{
+ return std::visit(TGetTableStructureDescriptionStringImpl(), tableStructure);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString JobTablePathString(const TStructuredJobTable& jobTable)
+{
+ if (jobTable.RichYPath) {
+ return jobTable.RichYPath->Path_;
+ } else {
+ return "<intermediate-table>";
+ }
+}
+
+TStructuredJobTableList ToStructuredJobTableList(const TVector<TStructuredTablePath>& tableList)
+{
+ TStructuredJobTableList result;
+ for (const auto& table : tableList) {
+ result.push_back(TStructuredJobTable{table.Description, table.RichYPath});
+ }
+ return result;
+}
+
+TStructuredJobTableList CanonizeStructuredTableList(const TClientContext& context, const TVector<TStructuredTablePath>& tableList)
+{
+ TVector<TRichYPath> toCanonize;
+ toCanonize.reserve(tableList.size());
+ for (const auto& table : tableList) {
+ toCanonize.emplace_back(table.RichYPath);
+ }
+ const auto canonized = NRawClient::CanonizeYPaths(/* retryPolicy */ nullptr, context, toCanonize);
+ Y_VERIFY(canonized.size() == tableList.size());
+
+ TStructuredJobTableList result;
+ result.reserve(tableList.size());
+ for (size_t i = 0; i != canonized.size(); ++i) {
+ result.emplace_back(TStructuredJobTable{tableList[i].Description, canonized[i]});
+ }
+ return result;
+}
+
+TVector<TRichYPath> GetPathList(
+ const TStructuredJobTableList& tableList,
+ const TMaybe<TVector<TTableSchema>>& jobSchemaInferenceResult,
+ bool inferSchemaFromDescriptions)
+{
+ Y_VERIFY(!jobSchemaInferenceResult || tableList.size() == jobSchemaInferenceResult->size());
+
+ auto maybeInferSchema = [&] (const TStructuredJobTable& table, ui32 tableIndex) -> TMaybe<TTableSchema> {
+ if (jobSchemaInferenceResult && !jobSchemaInferenceResult->at(tableIndex).Empty()) {
+ return jobSchemaInferenceResult->at(tableIndex);
+ }
+ if (inferSchemaFromDescriptions) {
+ return GetTableSchema(table.Description);
+ }
+ return Nothing();
+ };
+
+ TVector<TRichYPath> result;
+ result.reserve(tableList.size());
+ for (size_t tableIndex = 0; tableIndex != tableList.size(); ++tableIndex) {
+ const auto& table = tableList[tableIndex];
+ Y_VERIFY(table.RichYPath, "Cannot get path for intermediate table");
+ auto richYPath = *table.RichYPath;
+ if (!richYPath.Schema_) {
+ if (auto schema = maybeInferSchema(table, tableIndex)) {
+ richYPath.Schema(std::move(*schema));
+ }
+ }
+
+ result.emplace_back(std::move(richYPath));
+ }
+ return result;
+}
+
+
+TStructuredRowStreamDescription GetJobStreamDescription(
+ const IStructuredJob& job,
+ EIODirection direction)
+{
+ switch (direction) {
+ case EIODirection::Input:
+ return job.GetInputRowStreamDescription();
+ case EIODirection::Output:
+ return job.GetOutputRowStreamDescription();
+ default:
+ Y_FAIL("unreachable");
+ }
+}
+
+TString GetSuffix(EIODirection direction)
+{
+ switch (direction) {
+ case EIODirection::Input:
+ return "_input";
+ case EIODirection::Output:
+ return "_output";
+ }
+ Y_FAIL("unreachable");
+}
+
+TString GetAddIOMethodName(EIODirection direction)
+{
+ switch (direction) {
+ case EIODirection::Input:
+ return "AddInput<>";
+ case EIODirection::Output:
+ return "AddOutput<>";
+ }
+ Y_FAIL("unreachable");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFormatBuilder::TFormatSwitcher
+{
+ template <typename T>
+ auto operator() (const T& /*t*/) {
+ if constexpr (std::is_same_v<T, TTNodeStructuredRowStream>) {
+ return &TFormatBuilder::CreateNodeFormat;
+ } else if constexpr (std::is_same_v<T, TTYaMRRowStructuredRowStream>) {
+ return &TFormatBuilder::CreateYamrFormat;
+ } else if constexpr (std::is_same_v<T, TProtobufStructuredRowStream>) {
+ return &TFormatBuilder::CreateProtobufFormat;
+ } else if constexpr (std::is_same_v<T, TVoidStructuredRowStream>) {
+ return &TFormatBuilder::CreateVoidFormat;
+ } else {
+ static_assert(TDependentFalse<T>, "unknown stream description");
+ }
+ }
+};
+
+TFormatBuilder::TFormatBuilder(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ TClientContext context,
+ TTransactionId transactionId,
+ TOperationOptions operationOptions)
+ : ClientRetryPolicy_(std::move(clientRetryPolicy))
+ , Context_(std::move(context))
+ , TransactionId_(transactionId)
+ , OperationOptions_(std::move(operationOptions))
+{ }
+
+std::pair <TFormat, TMaybe<TSmallJobFile>> TFormatBuilder::CreateFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe <TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute)
+{
+ auto jobStreamDescription = GetJobStreamDescription(job, direction);
+ auto method = std::visit(TFormatSwitcher(), jobStreamDescription);
+ return (this->*method)(
+ job,
+ direction,
+ structuredTableList,
+ formatHints,
+ nodeReaderFormat,
+ allowFormatFromTableAttribute);
+}
+
+std::pair<TFormat, TMaybe<TSmallJobFile>> TFormatBuilder::CreateVoidFormat(
+ const IStructuredJob& /*job*/,
+ const EIODirection& /*direction*/,
+ const TStructuredJobTableList& /*structuredTableList*/,
+ const TMaybe<TFormatHints>& /*formatHints*/,
+ ENodeReaderFormat /*nodeReaderFormat*/,
+ bool /*allowFormatFromTableAttribute*/)
+{
+ return {
+ TFormat(),
+ Nothing()
+ };
+}
+
+std::pair<TFormat, TMaybe<TSmallJobFile>> TFormatBuilder::CreateYamrFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& /*formatHints*/,
+ ENodeReaderFormat /*nodeReaderFormat*/,
+ bool allowFormatFromTableAttribute)
+{
+ for (const auto& table: structuredTableList) {
+ if (!std::holds_alternative<TUnspecifiedTableStructure>(table.Description)) {
+ ythrow TApiUsageError()
+ << "cannot use " << direction << " table '" << JobTablePathString(table)
+ << "' with job " << TJobFactory::Get()->GetJobName(&job) << "; "
+ << "table has unsupported structure description; check " << GetAddIOMethodName(direction) << " for this table";
+ }
+ }
+ TMaybe<TNode> formatFromTableAttributes;
+ if (allowFormatFromTableAttribute && OperationOptions_.UseTableFormats_) {
+ TVector<TRichYPath> tableList;
+ for (const auto& table: structuredTableList) {
+ Y_VERIFY(table.RichYPath, "Cannot use format from table for intermediate table");
+ tableList.push_back(*table.RichYPath);
+ }
+ formatFromTableAttributes = GetTableFormats(ClientRetryPolicy_, Context_, TransactionId_, tableList);
+ }
+ if (formatFromTableAttributes) {
+ return {
+ TFormat(*formatFromTableAttributes),
+ Nothing()
+ };
+ } else {
+ auto formatNode = TNode("yamr");
+ formatNode.Attributes() = TNode()
+ ("lenval", true)
+ ("has_subkey", true)
+ ("enable_table_index", true);
+ return {
+ TFormat(formatNode),
+ Nothing()
+ };
+ }
+}
+
+std::pair<TFormat, TMaybe<TSmallJobFile>> TFormatBuilder::CreateNodeFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool /*allowFormatFromTableAttribute*/)
+{
+ for (const auto& table: structuredTableList) {
+ if (!std::holds_alternative<TUnspecifiedTableStructure>(table.Description)) {
+ ythrow TApiUsageError()
+ << "cannot use " << direction << " table '" << JobTablePathString(table)
+ << "' with job " << TJobFactory::Get()->GetJobName(&job) << "; "
+ << "table has unsupported structure description; check AddInput<> / AddOutput<> for this table";
+ }
+ }
+ NSkiff::TSkiffSchemaPtr skiffSchema = nullptr;
+ if (nodeReaderFormat != ENodeReaderFormat::Yson) {
+ TVector<TRichYPath> tableList;
+ for (const auto& table: structuredTableList) {
+ Y_VERIFY(table.RichYPath, "Cannot use skiff with temporary tables");
+ tableList.emplace_back(*table.RichYPath);
+ }
+ skiffSchema = TryCreateSkiffSchema(
+ Context_,
+ ClientRetryPolicy_,
+ TransactionId_,
+ tableList,
+ OperationOptions_,
+ nodeReaderFormat);
+ }
+ if (skiffSchema) {
+ auto format = CreateSkiffFormat(skiffSchema);
+ NYT::NDetail::ApplyFormatHints<TNode>(&format, formatHints);
+ return {
+ CreateSkiffFormat(skiffSchema),
+ TSmallJobFile{
+ TString("skiff") + GetSuffix(direction),
+ CreateSkiffConfig(skiffSchema)
+ }
+ };
+ } else {
+ auto format = TFormat::YsonBinary();
+ NYT::NDetail::ApplyFormatHints<TNode>(&format, formatHints);
+ return {
+ format,
+ Nothing()
+ };
+ }
+}
+
+[[noreturn]] static void ThrowUnsupportedStructureDescription(
+ const EIODirection& direction,
+ const TStructuredJobTable& table,
+ const IStructuredJob& job)
+{
+ ythrow TApiUsageError()
+ << "cannot use " << direction << " table '" << JobTablePathString(table)
+ << "' with job " << TJobFactory::Get()->GetJobName(&job) << "; "
+ << "table has unsupported structure description; check " << GetAddIOMethodName(direction) << " for this table";
+}
+
+[[noreturn]] static void ThrowTypeDeriveFail(
+ const EIODirection& direction,
+ const IStructuredJob& job,
+ const TString& type)
+{
+ ythrow TApiUsageError()
+ << "Cannot derive exact " << type << " type for intermediate " << direction << " table for job "
+ << TJobFactory::Get()->GetJobName(&job)
+ << "; use one of TMapReduceOperationSpec::Hint* methods to specifiy intermediate table structure";
+}
+
+[[noreturn]] static void ThrowUnexpectedDifferentDescriptors(
+ const EIODirection& direction,
+ const TStructuredJobTable& table,
+ const IStructuredJob& job,
+ const TMaybe<TStringBuf> jobDescriptorName,
+ const TMaybe<TStringBuf> descriptorName)
+{
+ ythrow TApiUsageError()
+ << "Job " << TJobFactory::Get()->GetJobName(&job) << " expects "
+ << jobDescriptorName << " as " << direction << ", but table " << JobTablePathString(table)
+ << " is tagged with " << descriptorName;
+}
+
+std::pair<TFormat, TMaybe<TSmallJobFile>> TFormatBuilder::CreateProtobufFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& /*formatHints*/,
+ ENodeReaderFormat /*nodeReaderFormat*/,
+ bool /*allowFormatFromTableAttribute*/)
+{
+ if (Context_.Config->UseClientProtobuf) {
+ return {
+ TFormat::YsonBinary(),
+ TSmallJobFile{
+ TString("proto") + GetSuffix(direction),
+ CreateProtoConfig({}),
+ },
+ };
+ }
+ const ::google::protobuf::Descriptor* const jobDescriptor =
+ std::get<TProtobufStructuredRowStream>(GetJobStreamDescription(job, direction)).Descriptor;
+ Y_ENSURE(!structuredTableList.empty(),
+ "empty " << direction << " tables for job " << TJobFactory::Get()->GetJobName(&job));
+
+ TVector<const ::google::protobuf::Descriptor*> descriptorList;
+ for (const auto& table : structuredTableList) {
+ const ::google::protobuf::Descriptor* descriptor = nullptr;
+ if (std::holds_alternative<TProtobufTableStructure>(table.Description)) {
+ descriptor = std::get<TProtobufTableStructure>(table.Description).Descriptor;
+ } else if (table.RichYPath) {
+ ThrowUnsupportedStructureDescription(direction, table, job);
+ }
+ if (!descriptor) {
+ // It must be intermediate table, because there is no proper way to add such table to spec
+ // (AddInput requires to specify proper message).
+ Y_VERIFY(!table.RichYPath, "Descriptors for all tables except intermediate must be known");
+ if (jobDescriptor) {
+ descriptor = jobDescriptor;
+ } else {
+ ThrowTypeDeriveFail(direction, job, "protobuf");
+ }
+ }
+ if (jobDescriptor && descriptor != jobDescriptor) {
+ ThrowUnexpectedDifferentDescriptors(
+ direction,
+ table,
+ job,
+ jobDescriptor->full_name(),
+ descriptor->full_name());
+ }
+ descriptorList.push_back(descriptor);
+ }
+ Y_VERIFY(!descriptorList.empty(), "Messages for proto format are unknown (empty ProtoDescriptors)");
+ return {
+ TFormat::Protobuf(descriptorList, Context_.Config->ProtobufFormatWithDescriptors),
+ TSmallJobFile{
+ TString("proto") + GetSuffix(direction),
+ CreateProtoConfig(descriptorList)
+ },
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGetTableSchemaImpl
+{
+ template <typename T>
+ TMaybe<TTableSchema> operator() (const T& description) {
+ if constexpr (std::is_same_v<T, TUnspecifiedTableStructure>) {
+ return Nothing();
+ } else if constexpr (std::is_same_v<T, TProtobufTableStructure>) {
+ if (!description.Descriptor) {
+ return Nothing();
+ }
+ return CreateTableSchema(*description.Descriptor);
+ } else {
+ static_assert(TDependentFalse<T>, "unknown type");
+ }
+ }
+};
+
+TMaybe<TTableSchema> GetTableSchema(const TTableStructure& tableStructure)
+{
+ return std::visit(TGetTableSchemaImpl(), tableStructure);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/structured_table_formats.h b/yt/cpp/mapreduce/client/structured_table_formats.h
new file mode 100644
index 0000000000..27d980c587
--- /dev/null
+++ b/yt/cpp/mapreduce/client/structured_table_formats.h
@@ -0,0 +1,146 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <utility>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMaybe<TNode> GetCommonTableFormat(
+ const TVector<TMaybe<TNode>>& formats);
+
+TMaybe<TNode> GetTableFormat(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TRichYPath& path);
+
+TMaybe<TNode> GetTableFormats(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class EIODirection
+{
+ Input,
+ Output,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSmallJobFile
+{
+ TString FileName;
+ TString Data;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Table that is used while preparing operation formats. Can be real table or intermediate
+struct TStructuredJobTable
+{
+ TTableStructure Description;
+ // Might be null for intermediate tables in MapReduce operation
+ TMaybe<TRichYPath> RichYPath;
+
+ static TStructuredJobTable Intermediate(TTableStructure description)
+ {
+ return TStructuredJobTable{std::move(description), Nothing()};
+ }
+};
+using TStructuredJobTableList = TVector<TStructuredJobTable>;
+TString JobTablePathString(const TStructuredJobTable& jobTable);
+TStructuredJobTableList ToStructuredJobTableList(const TVector<TStructuredTablePath>& tableList);
+
+TStructuredJobTableList CanonizeStructuredTableList(const TClientContext& context, const TVector<TStructuredTablePath>& tableList);
+TVector<TRichYPath> GetPathList(
+ const TStructuredJobTableList& tableList,
+ const TMaybe<TVector<TTableSchema>>& schemaInferenceResult,
+ bool inferSchema);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFormatBuilder
+{
+private:
+ struct TFormatSwitcher;
+
+public:
+ TFormatBuilder(
+ IClientRetryPolicyPtr clientRetryPolicy,
+ TClientContext context,
+ TTransactionId transactionId,
+ TOperationOptions operationOptions);
+
+ std::pair<TFormat, TMaybe<TSmallJobFile>> CreateFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute);
+
+ std::pair<TFormat, TMaybe<TSmallJobFile>> CreateVoidFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute);
+
+ std::pair<TFormat, TMaybe<TSmallJobFile>> CreateYamrFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute);
+
+ std::pair<TFormat, TMaybe<TSmallJobFile>> CreateNodeFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute);
+
+ std::pair<TFormat, TMaybe<TSmallJobFile>> CreateProtobufFormat(
+ const IStructuredJob& job,
+ const EIODirection& direction,
+ const TStructuredJobTableList& structuredTableList,
+ const TMaybe<TFormatHints>& formatHints,
+ ENodeReaderFormat nodeReaderFormat,
+ bool allowFormatFromTableAttribute);
+
+private:
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+ const TClientContext Context_;
+ const TTransactionId TransactionId_;
+ const TOperationOptions OperationOptions_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMaybe<TTableSchema> GetTableSchema(const TTableStructure& tableStructure);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/transaction.cpp b/yt/cpp/mapreduce/client/transaction.cpp
new file mode 100644
index 0000000000..0aa1a7a1c3
--- /dev/null
+++ b/yt/cpp/mapreduce/client/transaction.cpp
@@ -0,0 +1,195 @@
+#include "transaction.h"
+
+#include "transaction_pinger.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/scope.h>
+
+#include <util/random/random.h>
+
+#include <util/string/builder.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPingableTransaction::TPingableTransaction(
+ const IClientRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ ITransactionPingerPtr transactionPinger,
+ const TStartTransactionOptions& options)
+ : ClientRetryPolicy_(retryPolicy)
+ , Context_(context)
+ , AbortableRegistry_(NDetail::TAbortableRegistry::Get())
+ , AbortOnTermination_(true)
+ , AutoPingable_(options.AutoPingable_)
+ , Pinger_(std::move(transactionPinger))
+{
+ auto transactionId = NDetail::NRawClient::StartTransaction(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ context,
+ parentId,
+ options);
+
+ auto actualTimeout = options.Timeout_.GetOrElse(Context_.Config->TxTimeout);
+ Init(context, transactionId, actualTimeout);
+}
+
+TPingableTransaction::TPingableTransaction(
+ const IClientRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ ITransactionPingerPtr transactionPinger,
+ const TAttachTransactionOptions& options)
+ : ClientRetryPolicy_(retryPolicy)
+ , Context_(context)
+ , AbortableRegistry_(NDetail::TAbortableRegistry::Get())
+ , AbortOnTermination_(options.AbortOnTermination_)
+ , AutoPingable_(options.AutoPingable_)
+ , Pinger_(std::move(transactionPinger))
+{
+ auto timeoutNode = NDetail::NRawClient::TryGet(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ context,
+ TTransactionId(),
+ "#" + GetGuidAsString(transactionId) + "/@timeout",
+ TGetOptions());
+ if (timeoutNode.IsUndefined()) {
+ throw yexception() << "Transaction " << GetGuidAsString(transactionId) << " does not exist";
+ }
+ auto timeout = TDuration::MilliSeconds(timeoutNode.AsInt64());
+ Init(context, transactionId, timeout);
+}
+
+void TPingableTransaction::Init(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ TDuration timeout)
+{
+ TransactionId_ = transactionId;
+
+ if (AbortOnTermination_) {
+ AbortableRegistry_->Add(
+ TransactionId_,
+ ::MakeIntrusive<NDetail::TTransactionAbortable>(context, TransactionId_));
+ }
+
+ if (AutoPingable_) {
+ // Compute 'MaxPingInterval_' and 'MinPingInterval_' such that 'pingInterval == (max + min) / 2'.
+ auto pingInterval = Context_.Config->PingInterval;
+ auto safeTimeout = timeout - TDuration::Seconds(5);
+ MaxPingInterval_ = Max(pingInterval, Min(safeTimeout, pingInterval * 1.5));
+ MinPingInterval_ = pingInterval - (MaxPingInterval_ - pingInterval);
+
+ Pinger_->RegisterTransaction(*this);
+ }
+}
+
+TPingableTransaction::~TPingableTransaction()
+{
+ try {
+ Stop(AbortOnTermination_ ? EStopAction::Abort : EStopAction::Detach);
+ } catch (...) {
+ }
+}
+
+const TTransactionId TPingableTransaction::GetId() const
+{
+ return TransactionId_;
+}
+
+const std::pair<TDuration, TDuration> TPingableTransaction::GetPingInterval() const {
+ return {MinPingInterval_, MaxPingInterval_};
+}
+
+const TClientContext TPingableTransaction::GetContext() const {
+ return Context_;
+}
+
+void TPingableTransaction::Commit()
+{
+ Stop(EStopAction::Commit);
+}
+
+void TPingableTransaction::Abort()
+{
+ Stop(EStopAction::Abort);
+}
+
+void TPingableTransaction::Detach()
+{
+ Stop(EStopAction::Detach);
+}
+
+void TPingableTransaction::Stop(EStopAction action)
+{
+ if (Finalized_) {
+ return;
+ }
+
+ Y_DEFER {
+ Finalized_ = true;
+ if (AutoPingable_ && Pinger_->HasTransaction(*this)) {
+ Pinger_->RemoveTransaction(*this);
+ }
+ };
+
+ switch (action) {
+ case EStopAction::Commit:
+ NDetail::NRawClient::CommitTransaction(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ TransactionId_);
+ break;
+ case EStopAction::Abort:
+ NDetail::NRawClient::AbortTransaction(
+ ClientRetryPolicy_->CreatePolicyForGenericRequest(),
+ Context_,
+ TransactionId_);
+ break;
+ case EStopAction::Detach:
+ // Do nothing.
+ break;
+ }
+
+ AbortableRegistry_->Remove(TransactionId_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPath Snapshot(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path)
+{
+ auto lockId = NDetail::NRawClient::Lock(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ transactionId,
+ path,
+ ELockMode::LM_SNAPSHOT);
+ auto lockedNodeId = NDetail::NRawClient::Get(
+ clientRetryPolicy->CreatePolicyForGenericRequest(),
+ context,
+ transactionId,
+ ::TStringBuilder() << '#' << GetGuidAsString(lockId) << "/@node_id");
+ return "#" + lockedNodeId.AsString();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/transaction.h b/yt/cpp/mapreduce/client/transaction.h
new file mode 100644
index 0000000000..559fca619e
--- /dev/null
+++ b/yt/cpp/mapreduce/client/transaction.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "abortable_registry.h"
+
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/system/thread.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPingableTransaction
+{
+public:
+ //
+ // Start a new transaction.
+ TPingableTransaction(
+ const IClientRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ ITransactionPingerPtr transactionPinger,
+ const TStartTransactionOptions& options);
+
+ //
+ // Attach to an existing transaction.
+ TPingableTransaction(
+ const IClientRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ ITransactionPingerPtr transactionPinger,
+ const TAttachTransactionOptions& options);
+
+ ~TPingableTransaction();
+
+ const TTransactionId GetId() const;
+
+ const std::pair<TDuration, TDuration> GetPingInterval() const;
+ const TClientContext GetContext() const;
+
+ void Commit();
+ void Abort();
+ void Detach();
+
+
+private:
+ enum class EStopAction
+ {
+ Detach,
+ Abort,
+ Commit,
+ };
+
+private:
+ IClientRetryPolicyPtr ClientRetryPolicy_;
+ TClientContext Context_;
+ TTransactionId TransactionId_;
+ TDuration MinPingInterval_;
+ TDuration MaxPingInterval_;
+
+ // We have to own an IntrusivePtr to registry to prevent use-after-free.
+ ::TIntrusivePtr<NDetail::TAbortableRegistry> AbortableRegistry_;
+
+ bool AbortOnTermination_;
+
+ bool AutoPingable_;
+ bool Finalized_ = false;
+ ITransactionPingerPtr Pinger_;
+
+private:
+ void Init(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ TDuration timeout);
+
+ void Stop(EStopAction action);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPath Snapshot(
+ const IClientRetryPolicyPtr& clientRetryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/transaction_pinger.cpp b/yt/cpp/mapreduce/client/transaction_pinger.cpp
new file mode 100644
index 0000000000..2b51e47f9f
--- /dev/null
+++ b/yt/cpp/mapreduce/client/transaction_pinger.cpp
@@ -0,0 +1,321 @@
+#include "transaction_pinger.h"
+
+#include "transaction.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#if defined(__x86_64__) || defined(__arm64__)
+ #include <yt/yt/core/concurrency/periodic_executor.h>
+ #include <yt/yt/core/concurrency/poller.h>
+ #include <yt/yt/core/concurrency/scheduler_api.h>
+ #include <yt/yt/core/concurrency/thread_pool_poller.h>
+ #include <yt/yt/core/concurrency/thread_pool.h>
+
+ #include <yt/yt/core/http/client.h>
+ #include <yt/yt/core/http/http.h>
+#endif // defined(__x86_64__) || defined(__arm64__)
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/datetime/base.h>
+#include <util/random/random.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#if defined(__x86_64__) || defined(__arm64__)
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckError(const TString& requestId, NHttp::IResponsePtr response)
+{
+ TErrorResponse errorResponse(static_cast<int>(response->GetStatusCode()), requestId);
+
+ if (const auto* ytError = response->GetHeaders()->Find("X-YT-Error")) {
+ errorResponse.ParseFromJsonError(*ytError);
+ }
+ if (errorResponse.IsOk()) {
+ return;
+ }
+
+ YT_LOG_ERROR("RSP %v - HTTP %v - %v",
+ requestId,
+ response->GetStatusCode(),
+ errorResponse.AsStrBuf());
+
+ ythrow errorResponse;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+void PingTx(NHttp::IClientPtr httpClient, const TPingableTransaction& tx)
+{
+ auto url = TString::Join("http://", tx.GetContext().ServerName, "/api/", tx.GetContext().Config->ApiVersion, "/ping_tx");
+ auto headers = New<NHttp::THeaders>();
+ auto requestId = CreateGuidAsString();
+
+ headers->Add("Host", url);
+ headers->Add("User-Agent", TProcessState::Get()->ClientVersion);
+
+ const auto& token = tx.GetContext().Token;
+ if (!token.empty()) {
+ headers->Add("Authorization", "OAuth " + token);
+ }
+
+ headers->Add("Transfer-Encoding", "chunked");
+ headers->Add("X-YT-Correlation-Id", requestId);
+ headers->Add("X-YT-Header-Format", "<format=text>yson");
+ headers->Add("Content-Encoding", "identity");
+ headers->Add("Accept-Encoding", "identity");
+
+ TNode node;
+ node["transaction_id"] = GetGuidAsString(tx.GetId());
+ auto strParams = NodeToYsonString(node);
+
+ YT_LOG_DEBUG("REQ %v - sending request (HostName: %v; Method POST %v; X-YT-Parameters (sent in body): %v)",
+ requestId,
+ tx.GetContext().ServerName,
+ url,
+ strParams
+ );
+
+ auto response = NConcurrency::WaitFor(httpClient->Post(url, TSharedRef::FromString(strParams), headers)).ValueOrThrow();
+ CheckError(requestId, response);
+
+ YT_LOG_DEBUG("RSP %v - received response %v bytes. (%v)",
+ requestId,
+ response->ReadAll().size(),
+ strParams);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSharedTransactionPinger
+ : public ITransactionPinger
+{
+public:
+ TSharedTransactionPinger(NHttp::IClientPtr httpClient, int poolThreadCount)
+ : PingerPool_(NConcurrency::CreateThreadPool(
+ poolThreadCount, "tx_pinger_pool"))
+ , HttpClient_(std::move(httpClient))
+ { }
+
+ ~TSharedTransactionPinger() override
+ {
+ PingerPool_->Shutdown();
+ }
+
+ ITransactionPingerPtr GetChildTxPinger() override
+ {
+ return this;
+ }
+
+ void RegisterTransaction(const TPingableTransaction& pingableTx) override
+ {
+ auto [minPingInterval, maxPingInterval] = pingableTx.GetPingInterval();
+ auto pingInterval = (minPingInterval + maxPingInterval) / 2;
+ double jitter = (maxPingInterval - pingInterval) / pingInterval;
+
+ auto opts = NConcurrency::TPeriodicExecutorOptions{pingInterval, pingInterval, jitter};
+ auto periodic = std::make_shared<NConcurrency::TPeriodicExecutorPtr>(nullptr);
+ // Have to use weak_ptr in order to break reference cycle
+ // This weak_ptr holds pointer to periodic, which will contain this lambda
+ // Also we consider that lifetime of this lambda is no longer than lifetime of pingableTx
+ // because every pingableTx have to call RemoveTransaction before it is destroyed
+ auto pingRoutine = BIND([this, &pingableTx, periodic = std::weak_ptr{periodic}] {
+ auto strong_ptr = periodic.lock();
+ YT_VERIFY(strong_ptr);
+ DoPingTransaction(pingableTx, *strong_ptr);
+ });
+ *periodic = New<NConcurrency::TPeriodicExecutor>(PingerPool_->GetInvoker(), pingRoutine, opts);
+ (*periodic)->Start();
+
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(!Transactions_.contains(pingableTx.GetId()));
+ Transactions_[pingableTx.GetId()] = std::move(periodic);
+ }
+
+ bool HasTransaction(const TPingableTransaction& pingableTx) override
+ {
+ auto guard = Guard(SpinLock_);
+ return Transactions_.contains(pingableTx.GetId());
+ }
+
+
+ void RemoveTransaction(const TPingableTransaction& pingableTx) override
+ {
+ std::shared_ptr<NConcurrency::TPeriodicExecutorPtr> periodic;
+ {
+ auto guard = Guard(SpinLock_);
+
+ auto it = Transactions_.find(pingableTx.GetId());
+
+ YT_VERIFY(it != Transactions_.end());
+
+ periodic = std::move(it->second);
+ Transactions_.erase(it);
+ }
+ NConcurrency::WaitUntilSet((*periodic)->Stop());
+ }
+
+private:
+ void DoPingTransaction(const TPingableTransaction& pingableTx,
+ NConcurrency::TPeriodicExecutorPtr periodic)
+ {
+ try {
+ PingTx(HttpClient_, pingableTx);
+ } catch (const std::exception& e) {
+ if (auto* errorResponse = dynamic_cast<const TErrorResponse*>(&e)) {
+ if (errorResponse->GetError().ContainsErrorCode(NYT::NClusterErrorCodes::NTransactionClient::NoSuchTransaction)) {
+ YT_UNUSED_FUTURE(periodic->Stop());
+ } else if (errorResponse->GetError().ContainsErrorCode(NYT::NClusterErrorCodes::Timeout)) {
+ periodic->ScheduleOutOfBand();
+ }
+ }
+ }
+ }
+
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ THashMap<TTransactionId, std::shared_ptr<NConcurrency::TPeriodicExecutorPtr>> Transactions_;
+
+ NConcurrency::IThreadPoolPtr PingerPool_;
+ NHttp::IClientPtr HttpClient_;
+};
+
+#endif // defined(__x86_64__) || defined(__arm64__)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThreadPerTransactionPinger
+ : public ITransactionPinger
+{
+public:
+ ~TThreadPerTransactionPinger() override
+ {
+ if (Running_) {
+ RemoveTransaction(*PingableTx_);
+ }
+ }
+
+ ITransactionPingerPtr GetChildTxPinger() override
+ {
+ return MakeIntrusive<TThreadPerTransactionPinger>();
+ }
+
+ void RegisterTransaction(const TPingableTransaction& pingableTx) override
+ {
+ YT_VERIFY(!Running_);
+ YT_VERIFY(PingableTx_ == nullptr);
+
+ PingableTx_ = &pingableTx;
+ Running_ = true;
+
+ PingerThread_ = MakeHolder<TThread>(
+ TThread::TParams{Pinger, this}.SetName("pingable_tx"));
+ PingerThread_->Start();
+ }
+
+ bool HasTransaction(const TPingableTransaction& pingableTx) override
+ {
+ return PingableTx_ == &pingableTx && Running_;
+ }
+
+ void RemoveTransaction(const TPingableTransaction& pingableTx) override
+ {
+ YT_VERIFY(HasTransaction(pingableTx));
+
+ Running_ = false;
+ if (PingerThread_) {
+ PingerThread_->Join();
+ }
+ }
+
+private:
+ static void* Pinger(void* opaque)
+ {
+ static_cast<TThreadPerTransactionPinger*>(opaque)->Pinger();
+ return nullptr;
+ }
+
+ void Pinger()
+ {
+ auto [minPingInterval, maxPingInterval] = PingableTx_->GetPingInterval();
+ while (Running_) {
+ TDuration waitTime = minPingInterval + (maxPingInterval - minPingInterval) * RandomNumber<float>();
+ try {
+ auto noRetryPolicy = MakeIntrusive<TAttemptLimitedRetryPolicy>(1u, PingableTx_->GetContext().Config);
+ NDetail::NRawClient::PingTx(noRetryPolicy, PingableTx_->GetContext(), PingableTx_->GetId());
+ } catch (const std::exception& e) {
+ if (auto* errorResponse = dynamic_cast<const TErrorResponse*>(&e)) {
+ if (errorResponse->GetError().ContainsErrorCode(NYT::NClusterErrorCodes::NTransactionClient::NoSuchTransaction)) {
+ break;
+ } else if (errorResponse->GetError().ContainsErrorCode(NYT::NClusterErrorCodes::Timeout)) {
+ waitTime = TDuration::MilliSeconds(0);
+ }
+ }
+ // Else do nothing, going to retry this error.
+ }
+
+ TInstant t = Now();
+ while (Running_ && Now() - t < waitTime) {
+ NDetail::TWaitProxy::Get()->Sleep(TDuration::MilliSeconds(100));
+ }
+ }
+ }
+
+private:
+ const TPingableTransaction* PingableTx_ = nullptr;
+
+ std::atomic<bool> Running_ = false;
+ THolder<TThread> PingerThread_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITransactionPingerPtr CreateTransactionPinger(const TConfigPtr& config)
+{
+ if (config->UseAsyncTxPinger) {
+// TODO(aleexfi): Remove it after YT-17689
+#if defined(__x86_64__) || defined(__arm64__)
+ YT_LOG_DEBUG("Using async transaction pinger");
+ auto httpClientConfig = NYT::New<NHttp::TClientConfig>();
+ httpClientConfig->MaxIdleConnections = 16;
+ auto httpPoller = NConcurrency::CreateThreadPoolPoller(
+ config->AsyncHttpClientThreads,
+ "tx_http_client_poller");
+ auto httpClient = NHttp::CreateClient(std::move(httpClientConfig), std::move(httpPoller));
+
+ return MakeIntrusive<TSharedTransactionPinger>(
+ std::move(httpClient),
+ config->AsyncTxPingerPoolThreads);
+#else
+ YT_LOG_WARNING("Async transaction pinger is not supported on your platform. Fallback to TThreadPerTransactionPinger...");
+#endif // defined(__x86_64__) || defined(__arm64__)
+ }
+ return MakeIntrusive<TThreadPerTransactionPinger>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/transaction_pinger.h b/yt/cpp/mapreduce/client/transaction_pinger.h
new file mode 100644
index 0000000000..98e8b5cb2f
--- /dev/null
+++ b/yt/cpp/mapreduce/client/transaction_pinger.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <util/generic/ptr.h>
+#include <util/system/thread.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPingableTransaction;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Each registered transaction must be removed from pinger
+// (using RemoveTransaction) before it is destroyed
+class ITransactionPinger
+ : public TThrRefBase
+{
+public:
+ virtual ~ITransactionPinger() = default;
+
+ virtual ITransactionPingerPtr GetChildTxPinger() = 0;
+
+ virtual void RegisterTransaction(const TPingableTransaction& pingableTx) = 0;
+
+ virtual bool HasTransaction(const TPingableTransaction& pingableTx) = 0;
+
+ virtual void RemoveTransaction(const TPingableTransaction& pingableTx) = 0;
+};
+
+ITransactionPingerPtr CreateTransactionPinger(const TConfigPtr& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/ya.make b/yt/cpp/mapreduce/client/ya.make
new file mode 100644
index 0000000000..a1b3b4da69
--- /dev/null
+++ b/yt/cpp/mapreduce/client/ya.make
@@ -0,0 +1,75 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ abortable_registry.cpp
+ batch_request_impl.cpp
+ client_reader.cpp
+ client_writer.cpp
+ client.cpp
+ file_reader.cpp
+ file_writer.cpp
+ format_hints.cpp
+ init.cpp
+ lock.cpp
+ operation_helpers.cpp
+ operation_preparer.cpp
+ operation_tracker.cpp
+ operation.cpp
+ prepare_operation.cpp
+ py_helpers.cpp
+ retry_heavy_write_request.cpp
+ retryful_writer.cpp
+ retryless_writer.cpp
+ skiff.cpp
+ structured_table_formats.cpp
+ transaction.cpp
+ transaction_pinger.cpp
+ yt_poller.cpp
+)
+
+PEERDIR(
+ library/cpp/digest/md5
+ library/cpp/sighandler
+ library/cpp/threading/blocking_queue
+ library/cpp/threading/future
+ library/cpp/type_info
+ library/cpp/yson
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/http
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/io
+ yt/cpp/mapreduce/library/table_schema
+ yt/cpp/mapreduce/raw_client
+)
+
+IF (ARCH_X86_64 OR OS_DARWIN)
+ PEERDIR(
+ yt/yt/core
+ yt/yt/core/http
+ )
+ELSE()
+ # Suppress yamaker's WBadIncl error on exotic platforms
+ PEERDIR(
+ yt/yt_proto/yt/core
+ )
+ENDIF()
+
+IF (BUILD_TYPE == "PROFILE")
+ PEERDIR(
+ yt/yt/library/ytprof
+ )
+
+ SRCS(
+ job_profiler.cpp
+ )
+ELSE()
+ SRCS(
+ dummy_job_profiler.cpp
+ )
+ENDIF()
+
+GENERATE_ENUM_SERIALIZATION(structured_table_formats.h)
+
+END()
diff --git a/yt/cpp/mapreduce/client/yt_poller.cpp b/yt/cpp/mapreduce/client/yt_poller.cpp
new file mode 100644
index 0000000000..e0bea1690e
--- /dev/null
+++ b/yt/cpp/mapreduce/client/yt_poller.cpp
@@ -0,0 +1,132 @@
+#include "yt_poller.h"
+
+#include <yt/cpp/mapreduce/raw_client/raw_batch_request.h>
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+#include <yt/cpp/mapreduce/common/debug_metrics.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+namespace NYT {
+namespace NDetail {
+
+using namespace NRawClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYtPoller::TYtPoller(
+ TClientContext context,
+ const IClientRetryPolicyPtr& retryPolicy)
+ : Context_(std::move(context))
+ , ClientRetryPolicy_(retryPolicy)
+ , WaiterThread_(&TYtPoller::WatchLoopProc, this)
+{
+ WaiterThread_.Start();
+}
+
+TYtPoller::~TYtPoller()
+{
+ Stop();
+}
+
+void TYtPoller::Watch(IYtPollerItemPtr item)
+{
+ auto g = Guard(Lock_);
+ Pending_.emplace_back(std::move(item));
+ HasData_.Signal();
+}
+
+
+void TYtPoller::Stop()
+{
+ {
+ auto g = Guard(Lock_);
+ if (!IsRunning_) {
+ return;
+ }
+ IsRunning_ = false;
+ HasData_.Signal();
+ }
+ WaiterThread_.Join();
+}
+
+void TYtPoller::DiscardQueuedItems()
+{
+ for (auto& item : Pending_) {
+ item->OnItemDiscarded();
+ }
+ for (auto& item : InProgress_) {
+ item->OnItemDiscarded();
+ }
+}
+
+void TYtPoller::WatchLoop()
+{
+ TInstant nextRequest = TInstant::Zero();
+ while (true) {
+ {
+ auto g = Guard(Lock_);
+ if (IsRunning_ && Pending_.empty() && InProgress_.empty()) {
+ TWaitProxy::Get()->WaitCondVar(HasData_, Lock_);
+ }
+
+ if (!IsRunning_) {
+ DiscardQueuedItems();
+ return;
+ }
+
+ {
+ auto ug = Unguard(Lock_); // allow adding new items into Pending_
+ TWaitProxy::Get()->SleepUntil(nextRequest);
+ nextRequest = TInstant::Now() + Context_.Config->WaitLockPollInterval;
+ }
+ if (!Pending_.empty()) {
+ InProgress_.splice(InProgress_.end(), Pending_);
+ }
+ Y_VERIFY(!InProgress_.empty());
+ }
+
+ TRawBatchRequest rawBatchRequest(Context_.Config);
+
+ for (auto& item : InProgress_) {
+ item->PrepareRequest(&rawBatchRequest);
+ }
+
+ try {
+ ExecuteBatch(ClientRetryPolicy_->CreatePolicyForGenericRequest(), Context_, rawBatchRequest);
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("Exception while executing batch request: %v", ex.what());
+ }
+
+ for (auto it = InProgress_.begin(); it != InProgress_.end();) {
+ auto& item = *it;
+
+ IYtPollerItem::EStatus status = item->OnRequestExecuted();
+
+ if (status == IYtPollerItem::PollBreak) {
+ it = InProgress_.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ IncDebugMetric(TStringBuf("yt_poller_top_loop_repeat_count"));
+ }
+}
+
+void* TYtPoller::WatchLoopProc(void* data)
+{
+ static_cast<TYtPoller*>(data)->WatchLoop();
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/client/yt_poller.h b/yt/cpp/mapreduce/client/yt_poller.h
new file mode 100644
index 0000000000..4f4e9eb7ab
--- /dev/null
+++ b/yt/cpp/mapreduce/client/yt_poller.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+
+#include <util/generic/list.h>
+#include <util/system/mutex.h>
+#include <util/system/thread.h>
+#include <util/system/condvar.h>
+
+namespace NYT {
+namespace NDetail {
+
+namespace NRawClient {
+ class TRawBatchRequest;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IYtPollerItem
+ : public TThrRefBase
+{
+public:
+ enum EStatus {
+ PollContinue,
+ PollBreak,
+ };
+
+public:
+ virtual ~IYtPollerItem() = default;
+
+ virtual void PrepareRequest(NRawClient::TRawBatchRequest* batchRequest) = 0;
+
+ // Should return PollContinue if poller should continue polling this item.
+ // Should return PollBreak if poller should stop polling this item.
+ virtual EStatus OnRequestExecuted() = 0;
+
+ virtual void OnItemDiscarded() = 0;
+
+};
+using IYtPollerItemPtr = ::TIntrusivePtr<IYtPollerItem>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYtPoller
+ : public TThrRefBase
+{
+public:
+ TYtPoller(TClientContext context, const IClientRetryPolicyPtr& retryPolicy);
+ ~TYtPoller();
+
+ void Watch(IYtPollerItemPtr item);
+
+ void Stop();
+
+private:
+ void DiscardQueuedItems();
+
+ void WatchLoop();
+ static void* WatchLoopProc(void*);
+
+private:
+ struct TItem;
+
+ const TClientContext Context_;
+ const IClientRetryPolicyPtr ClientRetryPolicy_;
+
+
+ TList<IYtPollerItemPtr> InProgress_;
+ TList<IYtPollerItemPtr> Pending_;
+
+ TThread WaiterThread_;
+ TMutex Lock_;
+ TCondVar HasData_;
+
+ bool IsRunning_ = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/debug_metrics.cpp b/yt/cpp/mapreduce/common/debug_metrics.cpp
new file mode 100644
index 0000000000..6235e55f7e
--- /dev/null
+++ b/yt/cpp/mapreduce/common/debug_metrics.cpp
@@ -0,0 +1,62 @@
+#include "debug_metrics.h"
+
+#include <util/generic/hash.h>
+#include <util/generic/singleton.h>
+
+#include <util/string/cast.h>
+#include <util/system/mutex.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDebugMetrics {
+public:
+ static TDebugMetrics& Get()
+ {
+ return *Singleton<TDebugMetrics>();
+ }
+
+ void Inc(TStringBuf name)
+ {
+ auto g = Guard(Lock_);
+ auto it = Metrics_.find(name);
+ if (it == Metrics_.end()) {
+ it = Metrics_.emplace(ToString(name), 0).first;
+ }
+ ++it->second;
+ }
+
+ ui64 Get(TStringBuf name) const
+ {
+ auto g = Guard(Lock_);
+ auto it = Metrics_.find(name);
+ if (it == Metrics_.end()) {
+ return 0;
+ } else {
+ return it->second;
+ }
+ }
+
+private:
+ TMutex Lock_;
+ THashMap<TString, ui64> Metrics_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IncDebugMetricImpl(TStringBuf name)
+{
+ TDebugMetrics::Get().Inc(name);
+}
+
+ui64 GetDebugMetric(TStringBuf name)
+{
+ return TDebugMetrics::Get().Get(name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/debug_metrics.h b/yt/cpp/mapreduce/common/debug_metrics.h
new file mode 100644
index 0000000000..6ebbc89f72
--- /dev/null
+++ b/yt/cpp/mapreduce/common/debug_metrics.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT {
+namespace NDetail {
+
+void IncDebugMetricImpl(TStringBuf name);
+
+// Helper functions that allows to track various events inside YT library, useful for testing.
+inline void IncDebugMetric(TStringBuf name)
+{
+ if (TConfig::Get()->EnableDebugMetrics) {
+ IncDebugMetricImpl(name);
+ }
+}
+ui64 GetDebugMetric(TStringBuf name);
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/fwd.h b/yt/cpp/mapreduce/common/fwd.h
new file mode 100644
index 0000000000..a195e727be
--- /dev/null
+++ b/yt/cpp/mapreduce/common/fwd.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <util/generic/fwd.h>
+
+namespace NYT {
+ class IRequestRetryPolicy;
+ using IRequestRetryPolicyPtr = ::TIntrusivePtr<IRequestRetryPolicy>;
+
+ class IClientRetryPolicy;
+ using IClientRetryPolicyPtr = ::TIntrusivePtr<IClientRetryPolicy>;
+}
diff --git a/yt/cpp/mapreduce/common/helpers.cpp b/yt/cpp/mapreduce/common/helpers.cpp
new file mode 100644
index 0000000000..95924d812c
--- /dev/null
+++ b/yt/cpp/mapreduce/common/helpers.cpp
@@ -0,0 +1,126 @@
+#include "helpers.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/interface/fluent.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+#include <library/cpp/yson/node/node_visitor.h>
+
+#include <library/cpp/yson/parser.h>
+#include <library/cpp/yson/writer.h>
+
+#include <library/cpp/json/json_reader.h>
+#include <library/cpp/json/json_value.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+#include <util/stream/str.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString NodeListToYsonString(const TNode::TListType& nodes)
+{
+ TStringStream stream;
+ ::NYson::TYsonWriter writer(&stream, NYson::EYsonFormat::Binary, ::NYson::EYsonType::ListFragment);
+ auto list = BuildYsonListFluently(&writer);
+ for (const auto& node : nodes) {
+ list.Item().Value(node);
+ }
+ return stream.Str();
+}
+
+TNode PathToNode(const TRichYPath& path)
+{
+ TNode result;
+ TNodeBuilder builder(&result);
+ Serialize(path, &builder);
+ return result;
+}
+
+TNode PathToParamNode(const TRichYPath& path)
+{
+ return TNode()("path", PathToNode(path));
+}
+
+TString AttributesToYsonString(const TNode& node)
+{
+ return BuildYsonStringFluently().BeginMap()
+ .Item("attributes").Value(node)
+ .EndMap();
+}
+
+TString AttributeFilterToYsonString(const TAttributeFilter& filter)
+{
+ return BuildYsonStringFluently().BeginMap()
+ .Item("attributes").Value(filter)
+ .EndMap();
+}
+
+TNode NodeFromTableSchema(const TTableSchema& schema)
+{
+ TNode result;
+ TNodeBuilder builder(&result);
+ Serialize(schema, &builder);
+ return result;
+}
+
+void MergeNodes(TNode& dst, const TNode& src)
+{
+ if (dst.IsMap() && src.IsMap()) {
+ auto& dstMap = dst.AsMap();
+ const auto& srcMap = src.AsMap();
+ for (const auto& srcItem : srcMap) {
+ const auto& key = srcItem.first;
+ auto dstItem = dstMap.find(key);
+ if (dstItem != dstMap.end()) {
+ MergeNodes(dstItem->second, srcItem.second);
+ } else {
+ dstMap[key] = srcItem.second;
+ }
+ }
+ } else {
+ if (dst.GetType() == src.GetType() && src.HasAttributes()) {
+ auto attributes = dst.GetAttributes();
+ MergeNodes(attributes, src.GetAttributes());
+ dst = src;
+ dst.Attributes() = attributes;
+ } else {
+ dst = src;
+ }
+ }
+}
+
+TYPath AddPathPrefix(const TYPath& path, const TString& pathPrefix)
+{
+ if (path.StartsWith("//") || path.StartsWith("#")) {
+ return path;
+ }
+ return pathPrefix + path;
+}
+
+TString GetWriteTableCommand(const TString& apiVersion)
+{
+ return apiVersion == "v2" ? "write" : "write_table";
+}
+
+TString GetReadTableCommand(const TString& apiVersion)
+{
+ return apiVersion == "v2" ? "read" : "read_table";
+}
+
+TString GetWriteFileCommand(const TString& apiVersion)
+{
+ return apiVersion == "v2" ? "upload" : "write_file";
+}
+
+TString GetReadFileCommand(const TString& apiVersion)
+{
+ return apiVersion == "v2" ? "download" : "read_file";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/helpers.h b/yt/cpp/mapreduce/common/helpers.h
new file mode 100644
index 0000000000..2174ba820b
--- /dev/null
+++ b/yt/cpp/mapreduce/common/helpers.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <library/cpp/yson/node/node_io.h> // backward compatibility
+
+#include <yt/cpp/mapreduce/interface/node.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <library/cpp/yson/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString NodeListToYsonString(const TNode::TListType& nodes);
+
+TNode PathToNode(const TRichYPath& path);
+TNode PathToParamNode(const TRichYPath& path);
+
+TString AttributesToYsonString(const TNode& attributes);
+
+TString AttributeFilterToYsonString(const TAttributeFilter& filter);
+
+TNode NodeFromTableSchema(const TTableSchema& schema);
+
+void MergeNodes(TNode& dst, const TNode& src);
+
+TYPath AddPathPrefix(const TYPath& path, const TString& pathPrefix);
+
+TString GetWriteTableCommand(const TString& apiVersion);
+TString GetReadTableCommand(const TString& apiVersion);
+TString GetWriteFileCommand(const TString& apiVersion);
+TString GetReadFileCommand(const TString& apiVersion);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/node_builder.h b/yt/cpp/mapreduce/common/node_builder.h
new file mode 100644
index 0000000000..c7f731cf09
--- /dev/null
+++ b/yt/cpp/mapreduce/common/node_builder.h
@@ -0,0 +1,4 @@
+#pragma once
+
+// Backward compatibility.
+#include <library/cpp/yson/node/node_builder.h>
diff --git a/yt/cpp/mapreduce/common/node_visitor.h b/yt/cpp/mapreduce/common/node_visitor.h
new file mode 100644
index 0000000000..a8bde52b5a
--- /dev/null
+++ b/yt/cpp/mapreduce/common/node_visitor.h
@@ -0,0 +1,4 @@
+#pragma once
+
+// Backward compatibility.
+#include <library/cpp/yson/node/node_visitor.h>
diff --git a/yt/cpp/mapreduce/common/retry_lib.cpp b/yt/cpp/mapreduce/common/retry_lib.cpp
new file mode 100644
index 0000000000..cf2c021eb4
--- /dev/null
+++ b/yt/cpp/mapreduce/common/retry_lib.cpp
@@ -0,0 +1,267 @@
+#include "retry_lib.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+#include <yt/cpp/mapreduce/interface/retry_policy.h>
+
+#include <util/string/builder.h>
+#include <util/generic/set.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttemptLimitedRetryPolicy::TAttemptLimitedRetryPolicy(ui32 attemptLimit, const TConfigPtr& config)
+ : Config_(config)
+ , AttemptLimit_(attemptLimit)
+{ }
+
+void TAttemptLimitedRetryPolicy::NotifyNewAttempt()
+{
+ ++Attempt_;
+}
+
+TMaybe<TDuration> TAttemptLimitedRetryPolicy::OnGenericError(const std::exception& e)
+{
+ if (IsAttemptLimitExceeded()) {
+ return Nothing();
+ }
+ return GetBackoffDuration(e, Config_);
+}
+
+TMaybe<TDuration> TAttemptLimitedRetryPolicy::OnRetriableError(const TErrorResponse& e)
+{
+ if (IsAttemptLimitExceeded()) {
+ return Nothing();
+ }
+ return GetBackoffDuration(e, Config_);
+}
+
+void TAttemptLimitedRetryPolicy::OnIgnoredError(const TErrorResponse& /*e*/)
+{
+ --Attempt_;
+}
+
+TString TAttemptLimitedRetryPolicy::GetAttemptDescription() const
+{
+ return ::TStringBuilder() << "attempt " << Attempt_ << " of " << AttemptLimit_;
+}
+
+bool TAttemptLimitedRetryPolicy::IsAttemptLimitExceeded() const
+{
+ return Attempt_ >= AttemptLimit_;
+}
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeLimitedRetryPolicy
+ : public IRequestRetryPolicy
+{
+public:
+ TTimeLimitedRetryPolicy(IRequestRetryPolicyPtr retryPolicy, TDuration timeout)
+ : RetryPolicy_(retryPolicy)
+ , Deadline_(TInstant::Now() + timeout)
+ , Timeout_(timeout)
+ { }
+ void NotifyNewAttempt() override
+ {
+ if (TInstant::Now() >= Deadline_) {
+ ythrow TRequestRetriesTimeout() << "retry timeout exceeded (timeout: " << Timeout_ << ")";
+ }
+ RetryPolicy_->NotifyNewAttempt();
+ }
+
+ TMaybe<TDuration> OnGenericError(const std::exception& e) override
+ {
+ return RetryPolicy_->OnGenericError(e);
+ }
+
+ TMaybe<TDuration> OnRetriableError(const TErrorResponse& e) override
+ {
+ return RetryPolicy_->OnRetriableError(e);
+ }
+
+ void OnIgnoredError(const TErrorResponse& e) override
+ {
+ return RetryPolicy_->OnIgnoredError(e);
+ }
+
+ TString GetAttemptDescription() const override
+ {
+ return RetryPolicy_->GetAttemptDescription();
+ }
+
+private:
+ const IRequestRetryPolicyPtr RetryPolicy_;
+ const TInstant Deadline_;
+ const TDuration Timeout_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDefaultClientRetryPolicy
+ : public IClientRetryPolicy
+{
+public:
+ explicit TDefaultClientRetryPolicy(IRetryConfigProviderPtr retryConfigProvider, const TConfigPtr& config)
+ : RetryConfigProvider_(std::move(retryConfigProvider))
+ , Config_(config)
+ { }
+
+ IRequestRetryPolicyPtr CreatePolicyForGenericRequest() override
+ {
+ return Wrap(CreateDefaultRequestRetryPolicy(Config_));
+ }
+
+ IRequestRetryPolicyPtr CreatePolicyForStartOperationRequest() override
+ {
+ return Wrap(MakeIntrusive<TAttemptLimitedRetryPolicy>(static_cast<ui32>(Config_->StartOperationRetryCount), Config_));
+ }
+
+ IRequestRetryPolicyPtr Wrap(IRequestRetryPolicyPtr basePolicy)
+ {
+ auto config = RetryConfigProvider_->CreateRetryConfig();
+ if (config.RetriesTimeLimit < TDuration::Max()) {
+ return ::MakeIntrusive<TTimeLimitedRetryPolicy>(std::move(basePolicy), config.RetriesTimeLimit);
+ }
+ return basePolicy;
+ }
+
+private:
+ IRetryConfigProviderPtr RetryConfigProvider_;
+ const TConfigPtr Config_;
+};
+
+class TDefaultRetryConfigProvider
+ : public IRetryConfigProvider
+{
+public:
+ TRetryConfig CreateRetryConfig() override
+ {
+ return {};
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRequestRetryPolicyPtr CreateDefaultRequestRetryPolicy(const TConfigPtr& config)
+{
+ return MakeIntrusive<TAttemptLimitedRetryPolicy>(static_cast<ui32>(config->RetryCount), config);
+}
+
+IClientRetryPolicyPtr CreateDefaultClientRetryPolicy(IRetryConfigProviderPtr retryConfigProvider, const TConfigPtr& config)
+{
+ return MakeIntrusive<TDefaultClientRetryPolicy>(std::move(retryConfigProvider), config);
+}
+IRetryConfigProviderPtr CreateDefaultRetryConfigProvider()
+{
+ return MakeIntrusive<TDefaultRetryConfigProvider>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static bool IsChunkError(int code)
+{
+ return code / 100 == 7;
+}
+
+// Check whether:
+// 1) codes contain at least one chunk error AND
+// 2) codes don't contain non-retriable chunk errors.
+static bool IsRetriableChunkError(const TSet<int>& codes)
+{
+ using namespace NClusterErrorCodes;
+ auto isChunkError = false;
+ for (auto code : codes) {
+ switch (code) {
+ case NChunkClient::SessionAlreadyExists:
+ case NChunkClient::ChunkAlreadyExists:
+ case NChunkClient::WindowError:
+ case NChunkClient::BlockContentMismatch:
+ case NChunkClient::InvalidBlockChecksum:
+ case NChunkClient::BlockOutOfRange:
+ case NChunkClient::MissingExtension:
+ case NChunkClient::NoSuchBlock:
+ case NChunkClient::NoSuchChunk:
+ case NChunkClient::NoSuchChunkList:
+ case NChunkClient::NoSuchChunkTree:
+ case NChunkClient::NoSuchChunkView:
+ case NChunkClient::NoSuchMedium:
+ return false;
+ default:
+ isChunkError |= IsChunkError(code);
+ break;
+ }
+ }
+ return isChunkError;
+}
+
+static TMaybe<TDuration> TryGetBackoffDuration(const TErrorResponse& errorResponse, const TConfigPtr& config)
+{
+ int httpCode = errorResponse.GetHttpCode();
+ if (httpCode / 100 != 4 && !errorResponse.IsFromTrailers()) {
+ return config->RetryInterval;
+ }
+
+ auto allCodes = errorResponse.GetError().GetAllErrorCodes();
+ using namespace NClusterErrorCodes;
+ if (httpCode == 429
+ || allCodes.count(NSecurityClient::RequestQueueSizeLimitExceeded)
+ || allCodes.count(NRpc::RequestQueueSizeLimitExceeded))
+ {
+ // request rate limit exceeded
+ return config->RateLimitExceededRetryInterval;
+ }
+ if (errorResponse.IsConcurrentOperationsLimitReached()) {
+ // limit for the number of concurrent operations exceeded
+ return config->StartOperationRetryInterval;
+ }
+ if (IsRetriableChunkError(allCodes)) {
+ // chunk client errors
+ return config->ChunkErrorsRetryInterval;
+ }
+ for (auto code : TVector<int>{
+ NRpc::TransportError,
+ NRpc::Unavailable,
+ NApi::RetriableArchiveError,
+ Canceled,
+ }) {
+ if (allCodes.contains(code)) {
+ return config->RetryInterval;
+ }
+ }
+ return Nothing();
+}
+
+TDuration GetBackoffDuration(const TErrorResponse& errorResponse, const TConfigPtr& config)
+{
+ return TryGetBackoffDuration(errorResponse, config).GetOrElse(config->RetryInterval);
+}
+
+bool IsRetriable(const TErrorResponse& errorResponse)
+{
+ // Retriability of an error doesn't depend on config, so just use global one.
+ return TryGetBackoffDuration(errorResponse, TConfig::Get()).Defined();
+}
+
+bool IsRetriable(const std::exception& ex)
+{
+ if (dynamic_cast<const TRequestRetriesTimeout*>(&ex)) {
+ return false;
+ }
+ return true;
+}
+
+TDuration GetBackoffDuration(const std::exception& /*error*/, const TConfigPtr& config)
+{
+ return GetBackoffDuration(config);
+}
+
+TDuration GetBackoffDuration(const TConfigPtr& config)
+{
+ return config->RetryInterval;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/retry_lib.h b/yt/cpp/mapreduce/common/retry_lib.h
new file mode 100644
index 0000000000..c6c061f614
--- /dev/null
+++ b/yt/cpp/mapreduce/common/retry_lib.h
@@ -0,0 +1,100 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// IRequestRetryPolicy class controls retries of single request.
+class IRequestRetryPolicy
+ : public virtual TThrRefBase
+{
+public:
+ // Helper function that returns text description of current attempt, e.g.
+ // "attempt 3 / 10"
+ // used in logs.
+ virtual TString GetAttemptDescription() const = 0;
+
+ // Library code calls this function before any request attempt.
+ virtual void NotifyNewAttempt() = 0;
+
+ // OnRetriableError is called whenever client gets YT error that can be retried (e.g. operation limit exceeded).
+ // OnGenericError is called whenever request failed due to generic error like network error.
+ //
+ // Both methods must return nothing if policy doesn't want to retry this error.
+ // Otherwise method should return backoff time.
+ virtual TMaybe<TDuration> OnRetriableError(const TErrorResponse& e) = 0;
+ virtual TMaybe<TDuration> OnGenericError(const std::exception& e) = 0;
+
+ // OnIgnoredError is called whenever client gets an error but is going to ignore it.
+ virtual void OnIgnoredError(const TErrorResponse& /*e*/) = 0;
+};
+using IRequestRetryPolicyPtr = ::TIntrusivePtr<IRequestRetryPolicy>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// IClientRetryPolicy controls creation of policies for individual requests.
+class IClientRetryPolicy
+ : public virtual TThrRefBase
+{
+public:
+ virtual IRequestRetryPolicyPtr CreatePolicyForGenericRequest() = 0;
+ virtual IRequestRetryPolicyPtr CreatePolicyForStartOperationRequest() = 0;
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttemptLimitedRetryPolicy
+ : public IRequestRetryPolicy
+{
+public:
+ explicit TAttemptLimitedRetryPolicy(ui32 attemptLimit, const TConfigPtr& config);
+
+ void NotifyNewAttempt() override;
+
+ TMaybe<TDuration> OnGenericError(const std::exception& e) override;
+ TMaybe<TDuration> OnRetriableError(const TErrorResponse& e) override;
+ void OnIgnoredError(const TErrorResponse& e) override;
+ TString GetAttemptDescription() const override;
+
+ bool IsAttemptLimitExceeded() const;
+
+protected:
+ const TConfigPtr Config_;
+
+private:
+ const ui32 AttemptLimit_;
+ ui32 Attempt_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRequestRetryPolicyPtr CreateDefaultRequestRetryPolicy(const TConfigPtr& config);
+IClientRetryPolicyPtr CreateDefaultClientRetryPolicy(IRetryConfigProviderPtr retryConfigProvider, const TConfigPtr& config);
+IRetryConfigProviderPtr CreateDefaultRetryConfigProvider();
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Check if error returned by YT can be retried
+bool IsRetriable(const TErrorResponse& errorResponse);
+bool IsRetriable(const std::exception& ex);
+
+// Get backoff duration for errors returned by YT.
+TDuration GetBackoffDuration(const TErrorResponse& errorResponse, const TConfigPtr& config);
+
+// Get backoff duration for errors that are not TErrorResponse.
+TDuration GetBackoffDuration(const std::exception& error, const TConfigPtr& config);
+TDuration GetBackoffDuration(const TConfigPtr& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/wait_proxy.cpp b/yt/cpp/mapreduce/common/wait_proxy.cpp
new file mode 100644
index 0000000000..3db034a098
--- /dev/null
+++ b/yt/cpp/mapreduce/common/wait_proxy.cpp
@@ -0,0 +1,118 @@
+#include "wait_proxy.h"
+
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/system/event.h>
+#include <util/system/condvar.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TDefaultWaitProxy::WaitFuture(const NThreading::TFuture<void>& future, TDuration timeout)
+{
+ return future.Wait(timeout);
+}
+
+bool TDefaultWaitProxy::WaitEvent(TSystemEvent& event, TDuration timeout)
+{
+ return event.WaitT(timeout);
+}
+
+bool TDefaultWaitProxy::WaitCondVar(TCondVar &condVar, TMutex &mutex, TDuration timeout)
+{
+ return condVar.WaitT(mutex, timeout);
+}
+
+void TDefaultWaitProxy::Sleep(TDuration timeout)
+{
+ ::Sleep(timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWaitProxy::TWaitProxy()
+ : Proxy_(::MakeIntrusive<TDefaultWaitProxy>())
+{ }
+
+TWaitProxy* TWaitProxy::Get()
+{
+ return Singleton<TWaitProxy>();
+}
+
+void TWaitProxy::SetProxy(::TIntrusivePtr<IWaitProxy> proxy)
+{
+ Proxy_ = std::move(proxy);
+}
+
+bool TWaitProxy::WaitFuture(const NThreading::TFuture<void>& future)
+{
+ return Proxy_->WaitFuture(future, TDuration::Max());
+}
+
+bool TWaitProxy::WaitFuture(const NThreading::TFuture<void>& future, TInstant deadLine)
+{
+ return Proxy_->WaitFuture(future, deadLine - TInstant::Now());
+}
+
+bool TWaitProxy::WaitFuture(const NThreading::TFuture<void>& future, TDuration timeout)
+{
+ return Proxy_->WaitFuture(future, timeout);
+}
+
+bool TWaitProxy::WaitEventD(TSystemEvent& event, TInstant deadLine)
+{
+ return Proxy_->WaitEvent(event, deadLine - TInstant::Now());
+}
+
+bool TWaitProxy::WaitEventT(TSystemEvent& event, TDuration timeout)
+{
+ return Proxy_->WaitEvent(event, timeout);
+}
+
+void TWaitProxy::WaitEventI(TSystemEvent& event)
+{
+ Proxy_->WaitEvent(event, TDuration::Max());
+}
+
+bool TWaitProxy::WaitEvent(TSystemEvent& event)
+{
+ return Proxy_->WaitEvent(event, TDuration::Max());
+}
+
+bool TWaitProxy::WaitCondVarD(TCondVar& condVar, TMutex& m, TInstant deadLine)
+{
+ return Proxy_->WaitCondVar(condVar, m, deadLine - TInstant::Now());
+}
+
+bool TWaitProxy::WaitCondVarT(TCondVar& condVar, TMutex& m, TDuration timeOut)
+{
+ return Proxy_->WaitCondVar(condVar, m, timeOut);
+}
+
+void TWaitProxy::WaitCondVarI(TCondVar& condVar, TMutex& m)
+{
+ Proxy_->WaitCondVar(condVar, m, TDuration::Max());
+}
+
+void TWaitProxy::WaitCondVar(TCondVar& condVar, TMutex& m)
+{
+ Proxy_->WaitCondVar(condVar, m, TDuration::Max());
+}
+
+void TWaitProxy::Sleep(TDuration timeout)
+{
+ Proxy_->Sleep(timeout);
+}
+
+void TWaitProxy::SleepUntil(TInstant instant)
+{
+ Proxy_->Sleep(instant - TInstant::Now());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/wait_proxy.h b/yt/cpp/mapreduce/common/wait_proxy.h
new file mode 100644
index 0000000000..e7c944cf24
--- /dev/null
+++ b/yt/cpp/mapreduce/common/wait_proxy.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/wait_proxy.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDefaultWaitProxy
+ : public IWaitProxy
+{
+public:
+ bool WaitFuture(const ::NThreading::TFuture<void>& future, TDuration timeout) override;
+ bool WaitEvent(TSystemEvent& event, TDuration timeout) override;
+ bool WaitCondVar(TCondVar& condVar, TMutex& mutex, TDuration timeout) override;
+ void Sleep(TDuration timeout) override;
+};
+
+class TWaitProxy {
+public:
+ TWaitProxy();
+
+ static TWaitProxy* Get();
+
+ // NB: Non thread-safe, should be called only in initialization code.
+ void SetProxy(::TIntrusivePtr<IWaitProxy> proxy);
+
+ bool WaitFuture(const ::NThreading::TFuture<void>& future);
+ bool WaitFuture(const ::NThreading::TFuture<void>& future, TInstant deadLine);
+ bool WaitFuture(const ::NThreading::TFuture<void>& future, TDuration timeout);
+
+ bool WaitEventD(TSystemEvent& event, TInstant deadLine);
+ bool WaitEventT(TSystemEvent& event, TDuration timeout);
+ void WaitEventI(TSystemEvent& event);
+ bool WaitEvent(TSystemEvent& event);
+
+ bool WaitCondVarD(TCondVar& condVar, TMutex& m, TInstant deadLine);
+ bool WaitCondVarT(TCondVar& condVar, TMutex& m, TDuration timeOut);
+ void WaitCondVarI(TCondVar& condVar, TMutex& m);
+ void WaitCondVar(TCondVar& condVar, TMutex& m);
+
+ void Sleep(TDuration timeout);
+ void SleepUntil(TInstant instant);
+
+private:
+ ::TIntrusivePtr<IWaitProxy> Proxy_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/common/ya.make b/yt/cpp/mapreduce/common/ya.make
new file mode 100644
index 0000000000..004708cb44
--- /dev/null
+++ b/yt/cpp/mapreduce/common/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ debug_metrics.cpp
+ helpers.cpp
+ retry_lib.cpp
+ wait_proxy.cpp
+)
+
+PEERDIR(
+ library/cpp/json
+ library/cpp/svnversion
+ library/cpp/threading/future
+ library/cpp/yson
+ library/cpp/yson/json
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/interface/logging
+)
+
+END()
diff --git a/yt/cpp/mapreduce/http/abortable_http_response.cpp b/yt/cpp/mapreduce/http/abortable_http_response.cpp
new file mode 100644
index 0000000000..9da9241d33
--- /dev/null
+++ b/yt/cpp/mapreduce/http/abortable_http_response.cpp
@@ -0,0 +1,223 @@
+#include "abortable_http_response.h"
+
+#include <util/system/mutex.h>
+#include <util/generic/singleton.h>
+#include <util/generic/hash_set.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortableHttpResponseRegistry {
+public:
+ TOutageId StartOutage(TString urlPattern, const TOutageOptions& options)
+ {
+ auto g = Guard(Lock_);
+ auto id = NextId_++;
+ IdToOutage.emplace(id, TOutageEntry{std::move(urlPattern), options.ResponseCount_, options.LengthLimit_});
+ return id;
+ }
+
+ void StopOutage(TOutageId id)
+ {
+ auto g = Guard(Lock_);
+ IdToOutage.erase(id);
+ }
+
+ void Add(IAbortableHttpResponse* response)
+ {
+ auto g = Guard(Lock_);
+ for (auto& [id, entry] : IdToOutage) {
+ if (entry.Counter > 0 && response->GetUrl().find(entry.Pattern) != TString::npos) {
+ response->SetLengthLimit(entry.LengthLimit);
+ entry.Counter -= 1;
+ }
+ }
+ ResponseList_.PushBack(response);
+ }
+
+ void Remove(IAbortableHttpResponse* response)
+ {
+ auto g = Guard(Lock_);
+ response->Unlink();
+ }
+
+ static TAbortableHttpResponseRegistry& Get()
+ {
+ return *Singleton<TAbortableHttpResponseRegistry>();
+ }
+
+ int AbortAll(const TString& urlPattern)
+ {
+ int result = 0;
+ for (auto& response : ResponseList_) {
+ if (!response.IsAborted() && response.GetUrl().find(urlPattern) != TString::npos) {
+ response.Abort();
+ ++result;
+ }
+ }
+ return result;
+ }
+
+private:
+ struct TOutageEntry
+ {
+ TString Pattern;
+ size_t Counter;
+ size_t LengthLimit;
+ };
+
+private:
+ TOutageId NextId_ = 0;
+ TIntrusiveList<IAbortableHttpResponse> ResponseList_;
+ THashMap<TOutageId, TOutageEntry> IdToOutage;
+ TMutex Lock_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortableHttpResponse::TOutage::TOutage(
+ TString urlPattern,
+ TAbortableHttpResponseRegistry& registry,
+ const TOutageOptions& options)
+ : UrlPattern_(std::move(urlPattern))
+ , Registry_(registry)
+ , Id_(registry.StartOutage(UrlPattern_, options))
+{ }
+
+TAbortableHttpResponse::TOutage::~TOutage()
+{
+ Stop();
+}
+
+void TAbortableHttpResponse::TOutage::Stop()
+{
+ if (!Stopped_) {
+ Registry_.StopOutage(Id_);
+ Stopped_ = true;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortableHttpResponseBase::TAbortableHttpResponseBase(const TString& url)
+ : Url_(url)
+{
+ TAbortableHttpResponseRegistry::Get().Add(this);
+}
+
+TAbortableHttpResponseBase::~TAbortableHttpResponseBase()
+{
+ TAbortableHttpResponseRegistry::Get().Remove(this);
+}
+
+void TAbortableHttpResponseBase::Abort()
+{
+ Aborted_ = true;
+}
+
+void TAbortableHttpResponseBase::SetLengthLimit(size_t limit)
+{
+ LengthLimit_ = limit;
+ if (LengthLimit_ == 0) {
+ Abort();
+ }
+}
+
+const TString& TAbortableHttpResponseBase::GetUrl() const
+{
+ return Url_;
+}
+
+bool TAbortableHttpResponseBase::IsAborted() const
+{
+ return Aborted_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortableHttpResponse::TAbortableHttpResponse(
+ IInputStream* socketStream,
+ const TString& requestId,
+ const TString& hostName,
+ const TString& url)
+ : THttpResponse(socketStream, requestId, hostName)
+ , TAbortableHttpResponseBase(url)
+{
+}
+
+size_t TAbortableHttpResponse::DoRead(void* buf, size_t len)
+{
+ if (Aborted_) {
+ ythrow TAbortedForTestPurpose() << "response was aborted";
+ }
+ len = std::min(len, LengthLimit_);
+ auto read = THttpResponse::DoRead(buf, len);
+ LengthLimit_ -= read;
+ if (LengthLimit_ == 0) {
+ Abort();
+ }
+ return read;
+}
+
+size_t TAbortableHttpResponse::DoSkip(size_t len)
+{
+ if (Aborted_) {
+ ythrow TAbortedForTestPurpose() << "response was aborted";
+ }
+ return THttpResponse::DoSkip(len);
+}
+
+int TAbortableHttpResponse::AbortAll(const TString& urlPattern)
+{
+ return TAbortableHttpResponseRegistry::Get().AbortAll(urlPattern);
+}
+
+TAbortableHttpResponse::TOutage TAbortableHttpResponse::StartOutage(
+ const TString& urlPattern,
+ const TOutageOptions& options)
+{
+ return TOutage(urlPattern, TAbortableHttpResponseRegistry::Get(), options);
+}
+
+TAbortableHttpResponse::TOutage TAbortableHttpResponse::StartOutage(
+ const TString& urlPattern,
+ size_t responseCount)
+{
+ return StartOutage(urlPattern, TOutageOptions().ResponseCount(responseCount));
+}
+
+TAbortableCoreHttpResponse::TAbortableCoreHttpResponse(
+ std::unique_ptr<IInputStream> stream,
+ const TString& url)
+ : TAbortableHttpResponseBase(url)
+ , Stream_(std::move(stream))
+{
+}
+
+size_t TAbortableCoreHttpResponse::DoRead(void* buf, size_t len)
+{
+ if (Aborted_) {
+ ythrow TAbortedForTestPurpose() << "response was aborted";
+ }
+ len = std::min(len, LengthLimit_);
+ auto read = Stream_->Read(buf, len);
+ LengthLimit_ -= read;
+ if (LengthLimit_ == 0) {
+ Abort();
+ }
+
+ return read;
+}
+
+size_t TAbortableCoreHttpResponse::DoSkip(size_t len)
+{
+ if (Aborted_) {
+ ythrow TAbortedForTestPurpose() << "response was aborted";
+ }
+ return Stream_->Skip(len);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/abortable_http_response.h b/yt/cpp/mapreduce/http/abortable_http_response.h
new file mode 100644
index 0000000000..d72bcfa0a6
--- /dev/null
+++ b/yt/cpp/mapreduce/http/abortable_http_response.h
@@ -0,0 +1,142 @@
+#pragma once
+
+#include "http.h"
+
+#include <util/generic/intrlist.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortableHttpResponseRegistry;
+
+using TOutageId = size_t;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortedForTestPurpose
+ : public yexception
+{ };
+
+struct TOutageOptions
+{
+ using TSelf = TOutageOptions;
+
+ /// @brief Number of responses to abort.
+ FLUENT_FIELD_DEFAULT(size_t, ResponseCount, std::numeric_limits<size_t>::max());
+
+ /// @brief Number of bytes to read before abortion. If zero, abort immediately.
+ FLUENT_FIELD_DEFAULT(size_t, LengthLimit, 0);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IAbortableHttpResponse
+ : public TIntrusiveListItem<IAbortableHttpResponse>
+{
+public:
+ virtual void Abort() = 0;
+ virtual const TString& GetUrl() const = 0;
+ virtual bool IsAborted() const = 0;
+ virtual void SetLengthLimit(size_t limit) = 0;
+
+ virtual ~IAbortableHttpResponse() = default;
+};
+
+class TAbortableHttpResponseBase
+ : public IAbortableHttpResponse
+{
+public:
+ TAbortableHttpResponseBase(const TString& url);
+ ~TAbortableHttpResponseBase();
+
+ void Abort() override;
+ const TString& GetUrl() const override;
+ bool IsAborted() const override;
+ void SetLengthLimit(size_t limit) override;
+
+protected:
+ TString Url_;
+ std::atomic<bool> Aborted_ = {false};
+ size_t LengthLimit_ = std::numeric_limits<size_t>::max();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Stream wrapper for @ref NYT::NHttpClient::TCoreHttpResponse with possibility to emulate errors.
+class TAbortableCoreHttpResponse
+ : public IInputStream
+ , public TAbortableHttpResponseBase
+{
+public:
+ TAbortableCoreHttpResponse(
+ std::unique_ptr<IInputStream> stream,
+ const TString& url);
+
+private:
+ size_t DoRead(void* buf, size_t len) override;
+ size_t DoSkip(size_t len) override;
+
+private:
+ std::unique_ptr<IInputStream> Stream_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Class extends @ref NYT::THttpResponse with possibility to emulate errors.
+class TAbortableHttpResponse
+ : public THttpResponse
+ , public TAbortableHttpResponseBase
+{
+public:
+ class TOutage
+ {
+ public:
+ TOutage(TString urlPattern, TAbortableHttpResponseRegistry& registry, const TOutageOptions& options);
+ TOutage(TOutage&&) = default;
+ TOutage(const TOutage&) = delete;
+ ~TOutage();
+
+ void Stop();
+
+ private:
+ TString UrlPattern_;
+ TAbortableHttpResponseRegistry& Registry_;
+ TOutageId Id_;
+ bool Stopped_ = false;
+ };
+
+public:
+ TAbortableHttpResponse(
+ IInputStream* socketStream,
+ const TString& requestId,
+ const TString& hostName,
+ const TString& url);
+
+ /// @brief Abort any responses which match `urlPattern` (i.e. contain it in url).
+ ///
+ /// @return number of aborted responses.
+ static int AbortAll(const TString& urlPattern);
+
+ /// @brief Start outage. Future responses which match `urlPattern` (i.e. contain it in url) will fail.
+ ///
+ /// @return outage object controlling the lifetime of outage (outage stops when object is destroyed)
+ [[nodiscard]] static TOutage StartOutage(
+ const TString& urlPattern,
+ const TOutageOptions& options = TOutageOptions());
+
+ /// @brief Start outage. Future `responseCount` responses which match `urlPattern` (i.e. contain it in url) will fail.
+ ///
+ /// @return outage object controlling the lifetime of outage (outage stops when object is destroyed)
+ [[nodiscard]] static TOutage StartOutage(
+ const TString& urlPattern,
+ size_t responseCount);
+
+private:
+ size_t DoRead(void* buf, size_t len) override;
+ size_t DoSkip(size_t len) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/context.cpp b/yt/cpp/mapreduce/http/context.cpp
new file mode 100644
index 0000000000..1c016263c5
--- /dev/null
+++ b/yt/cpp/mapreduce/http/context.cpp
@@ -0,0 +1,25 @@
+#include "context.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TClientContext& lhs, const TClientContext& rhs)
+{
+ return lhs.ServerName == rhs.ServerName &&
+ lhs.Token == rhs.Token &&
+ lhs.ImpersonationUser == rhs.ImpersonationUser &&
+ lhs.ServiceTicketAuth == rhs.ServiceTicketAuth &&
+ lhs.HttpClient == rhs.HttpClient &&
+ lhs.UseTLS == rhs.UseTLS &&
+ lhs.TvmOnly == rhs.TvmOnly;
+}
+
+bool operator!=(const TClientContext& lhs, const TClientContext& rhs)
+{
+ return !(rhs == lhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/context.h b/yt/cpp/mapreduce/http/context.h
new file mode 100644
index 0000000000..3926373e17
--- /dev/null
+++ b/yt/cpp/mapreduce/http/context.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/public.h>
+
+
+namespace NYT {
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct TClientContext
+{
+ TString ServerName;
+ TString Token;
+ TMaybe<TString> ImpersonationUser;
+ NAuth::IServiceTicketAuthPtrWrapperPtr ServiceTicketAuth;
+ NHttpClient::IHttpClientPtr HttpClient;
+ bool TvmOnly = false;
+ bool UseTLS = false;
+ TConfigPtr Config = TConfig::Get();
+};
+
+bool operator==(const TClientContext& lhs, const TClientContext& rhs);
+bool operator!=(const TClientContext& lhs, const TClientContext& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/core.h b/yt/cpp/mapreduce/http/core.h
new file mode 100644
index 0000000000..37c74d7551
--- /dev/null
+++ b/yt/cpp/mapreduce/http/core.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <yt/yt/core/http/public.h>
+
+#include <memory>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Wrapper for THeaderPtr which allows to hide NYT::IntrusivePtr from interfaces.
+struct THeadersPtrWrapper
+{
+ THeadersPtrWrapper(THeadersPtr ptr)
+ : Ptr(std::make_shared<THeadersPtr>(std::move(ptr)))
+ { }
+
+ THeadersPtr Get() {
+ return *Ptr;
+ }
+
+ std::shared_ptr<THeadersPtr> Ptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/cpp/mapreduce/http/fwd.h b/yt/cpp/mapreduce/http/fwd.h
new file mode 100644
index 0000000000..62891731f6
--- /dev/null
+++ b/yt/cpp/mapreduce/http/fwd.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <memory>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TClientContext;
+class THttpHeader;
+
+namespace NHttpClient {
+
+class IHttpClient;
+class IHttpRequest;
+class IHttpResponse;
+
+using IHttpClientPtr = std::shared_ptr<IHttpClient>;
+using IHttpResponsePtr = std::unique_ptr<IHttpResponse>;
+using IHttpRequestPtr = std::unique_ptr<IHttpRequest>;
+
+} // namespace NHttpClient
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/helpers.cpp b/yt/cpp/mapreduce/http/helpers.cpp
new file mode 100644
index 0000000000..233a565f20
--- /dev/null
+++ b/yt/cpp/mapreduce/http/helpers.cpp
@@ -0,0 +1,88 @@
+#include "helpers.h"
+
+#include "context.h"
+#include "requests.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+namespace NYT {
+
+///////////////////////////////////////////////////////////////////////////////
+
+TString CreateHostNameWithPort(const TString& hostName, const TClientContext& context)
+{
+ static constexpr int HttpProxyPort = 80;
+ static constexpr int HttpsProxyPort = 443;
+
+ static constexpr int TvmOnlyHttpProxyPort = 9026;
+ static constexpr int TvmOnlyHttpsProxyPort = 9443;
+
+ if (hostName.find(':') == TString::npos) {
+ int port;
+ if (context.TvmOnly) {
+ port = context.UseTLS
+ ? TvmOnlyHttpsProxyPort
+ : TvmOnlyHttpProxyPort;
+ } else {
+ port = context.UseTLS
+ ? HttpsProxyPort
+ : HttpProxyPort;
+ }
+ return Format("%v:%v", hostName, port);
+ }
+ return hostName;
+}
+
+TString GetFullUrl(const TString& hostName, const TClientContext& context, THttpHeader& header)
+{
+ Y_UNUSED(context);
+ return Format("http://%v%v", hostName, header.GetUrl());
+}
+
+static TString GetParametersDebugString(const THttpHeader& header)
+{
+ const auto& parameters = header.GetParameters();
+ if (parameters.Empty()) {
+ return "<empty>";
+ } else {
+ return NodeToYsonString(parameters);
+ }
+}
+
+TString TruncateForLogs(const TString& text, size_t maxSize)
+{
+ Y_VERIFY(maxSize > 10);
+ if (text.empty()) {
+ static TString empty = "empty";
+ return empty;
+ } else if (text.size() > maxSize) {
+ TStringStream out;
+ out << text.substr(0, maxSize) + "... (" << text.size() << " bytes total)";
+ return out.Str();
+ } else {
+ return text;
+ }
+}
+
+TString GetLoggedAttributes(const THttpHeader& header, const TString& url, bool includeParameters, size_t sizeLimit)
+{
+ const auto parametersDebugString = GetParametersDebugString(header);
+ TStringStream out;
+ out << "Method: " << url << "; "
+ << "X-YT-Parameters (sent in " << (includeParameters ? "header" : "body") << "): " << TruncateForLogs(parametersDebugString, sizeLimit);
+ return out.Str();
+}
+
+void LogRequest(const THttpHeader& header, const TString& url, bool includeParameters, const TString& requestId, const TString& hostName)
+{
+ YT_LOG_DEBUG("REQ %v - sending request (HostName: %v; %v)",
+ requestId,
+ hostName,
+ GetLoggedAttributes(header, url, includeParameters, Max<size_t>()));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/helpers.h b/yt/cpp/mapreduce/http/helpers.h
new file mode 100644
index 0000000000..0c510fa2e8
--- /dev/null
+++ b/yt/cpp/mapreduce/http/helpers.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "fwd.h"
+
+#include "http.h"
+
+#include <util/generic/fwd.h>
+
+namespace NYT {
+
+///////////////////////////////////////////////////////////////////////////////
+
+TString CreateHostNameWithPort(const TString& name, const TClientContext& context);
+
+TString GetFullUrl(const TString& hostName, const TClientContext& context, THttpHeader& header);
+
+TString TruncateForLogs(const TString& text, size_t maxSize);
+
+TString GetLoggedAttributes(const THttpHeader& header, const TString& url, bool includeParameters, size_t sizeLimit);
+
+void LogRequest(const THttpHeader& header, const TString& url, bool includeParameters, const TString& requestId, const TString& hostName);
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/host_manager.cpp b/yt/cpp/mapreduce/http/host_manager.cpp
new file mode 100644
index 0000000000..a239dde769
--- /dev/null
+++ b/yt/cpp/mapreduce/http/host_manager.cpp
@@ -0,0 +1,140 @@
+#include "host_manager.h"
+
+#include "context.h"
+#include "helpers.h"
+#include "http.h"
+#include "http_client.h"
+#include "requests.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <library/cpp/json/json_reader.h>
+
+#include <util/generic/guid.h>
+#include <util/generic/vector.h>
+#include <util/generic/singleton.h>
+#include <util/generic/ymath.h>
+
+#include <util/random/random.h>
+
+#include <util/string/vector.h>
+
+namespace NYT::NPrivate {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TVector<TString> ParseJsonStringArray(const TString& response)
+{
+ NJson::TJsonValue value;
+ TStringInput input(response);
+ NJson::ReadJsonTree(&input, &value);
+
+ const NJson::TJsonValue::TArray& array = value.GetArray();
+ TVector<TString> result;
+ result.reserve(array.size());
+ for (size_t i = 0; i < array.size(); ++i) {
+ result.push_back(array[i].GetString());
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THostManager::TClusterHostList
+{
+public:
+ explicit TClusterHostList(TVector<TString> hosts)
+ : Hosts_(std::move(hosts))
+ , Timestamp_(TInstant::Now())
+ { }
+
+ explicit TClusterHostList(std::exception_ptr error)
+ : Error_(std::move(error))
+ , Timestamp_(TInstant::Now())
+ { }
+
+ TString ChooseHostOrThrow() const
+ {
+ if (Error_) {
+ std::rethrow_exception(Error_);
+ }
+
+ if (Hosts_.empty()) {
+ ythrow yexception() << "fetched list of proxies is empty";
+ }
+
+ return Hosts_[RandomNumber<size_t>(Hosts_.size())];
+ }
+
+ TDuration GetAge() const
+ {
+ return TInstant::Now() - Timestamp_;
+ }
+
+private:
+ TVector<TString> Hosts_;
+ std::exception_ptr Error_;
+ TInstant Timestamp_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+THostManager& THostManager::Get()
+{
+ return *Singleton<THostManager>();
+}
+
+void THostManager::Reset()
+{
+ auto guard = Guard(Lock_);
+ ClusterHosts_.clear();
+}
+
+TString THostManager::GetProxyForHeavyRequest(const TClientContext& context)
+{
+ auto cluster = context.ServerName;
+ {
+ auto guard = Guard(Lock_);
+ auto it = ClusterHosts_.find(cluster);
+ if (it != ClusterHosts_.end() && it->second.GetAge() < context.Config->HostListUpdateInterval) {
+ return it->second.ChooseHostOrThrow();
+ }
+ }
+
+ auto hostList = GetHosts(context);
+ auto result = hostList.ChooseHostOrThrow();
+ {
+ auto guard = Guard(Lock_);
+ ClusterHosts_.emplace(cluster, std::move(hostList));
+ }
+ return result;
+}
+
+THostManager::TClusterHostList THostManager::GetHosts(const TClientContext& context)
+{
+ TString hostsEndpoint = context.Config->Hosts;
+ while (hostsEndpoint.StartsWith("/")) {
+ hostsEndpoint = hostsEndpoint.substr(1);
+ }
+ THttpHeader header("GET", hostsEndpoint, false);
+
+ try {
+ auto hostName = context.ServerName;
+ auto requestId = CreateGuidAsString();
+ // TODO: we need to set socket timeout here
+ auto response = context.HttpClient->Request(GetFullUrl(hostName, context, header), requestId, header);
+ auto hosts = ParseJsonStringArray(response->GetResponse());
+ for (auto& host : hosts) {
+ host = CreateHostNameWithPort(host, context);
+ }
+ return TClusterHostList(std::move(hosts));
+ } catch (const std::exception& e) {
+ return TClusterHostList(std::current_exception());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NPrivate
diff --git a/yt/cpp/mapreduce/http/host_manager.h b/yt/cpp/mapreduce/http/host_manager.h
new file mode 100644
index 0000000000..fdbb740566
--- /dev/null
+++ b/yt/cpp/mapreduce/http/host_manager.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <util/generic/string.h>
+#include <util/generic/hash.h>
+#include <util/system/spinlock.h>
+
+
+namespace NYT::NPrivate {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THostManager
+{
+public:
+ static THostManager& Get();
+
+ TString GetProxyForHeavyRequest(const TClientContext& context);
+
+ // For testing purposes only.
+ void Reset();
+
+private:
+ class TClusterHostList;
+
+private:
+ TAdaptiveLock Lock_;
+ THashMap<TString, TClusterHostList> ClusterHosts_;
+
+private:
+ static TClusterHostList GetHosts(const TClientContext& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NPrivate
diff --git a/yt/cpp/mapreduce/http/http.cpp b/yt/cpp/mapreduce/http/http.cpp
new file mode 100644
index 0000000000..d44b2638a0
--- /dev/null
+++ b/yt/cpp/mapreduce/http/http.cpp
@@ -0,0 +1,1014 @@
+#include "http.h"
+
+#include "abortable_http_response.h"
+#include "core.h"
+#include "helpers.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/yt/core/http/http.h>
+
+#include <library/cpp/json/json_writer.h>
+
+#include <library/cpp/string_utils/base64/base64.h>
+#include <library/cpp/string_utils/quote/quote.h>
+
+#include <util/generic/singleton.h>
+#include <util/generic/algorithm.h>
+
+#include <util/stream/mem.h>
+
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/escape.h>
+#include <util/string/printf.h>
+
+#include <util/system/byteorder.h>
+#include <util/system/getpid.h>
+
+#include <exception>
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpRequest::TRequestStream
+ : public IOutputStream
+{
+public:
+ TRequestStream(THttpRequest* httpRequest, const TSocket& s)
+ : HttpRequest_(httpRequest)
+ , SocketOutput_(s)
+ , HttpOutput_(static_cast<IOutputStream*>(&SocketOutput_))
+ {
+ HttpOutput_.EnableKeepAlive(true);
+ }
+
+private:
+ void DoWrite(const void* buf, size_t len) override
+ {
+ WrapWriteFunc([&] {
+ HttpOutput_.Write(buf, len);
+ });
+ }
+
+ void DoWriteV(const TPart* parts, size_t count) override
+ {
+ WrapWriteFunc([&] {
+ HttpOutput_.Write(parts, count);
+ });
+ }
+
+ void DoWriteC(char ch) override
+ {
+ WrapWriteFunc([&] {
+ HttpOutput_.Write(ch);
+ });
+ }
+
+ void DoFlush() override
+ {
+ WrapWriteFunc([&] {
+ HttpOutput_.Flush();
+ });
+ }
+
+ void DoFinish() override
+ {
+ WrapWriteFunc([&] {
+ HttpOutput_.Finish();
+ });
+ }
+
+ void WrapWriteFunc(std::function<void()> func)
+ {
+ CheckErrorState();
+ try {
+ func();
+ } catch (const std::exception&) {
+ HandleWriteException();
+ }
+ }
+
+ // In many cases http proxy stops reading request and resets connection
+ // if error has happend. This function tries to read error response
+ // in such cases.
+ void HandleWriteException() {
+ Y_VERIFY(WriteError_ == nullptr);
+ WriteError_ = std::current_exception();
+ Y_VERIFY(WriteError_ != nullptr);
+ try {
+ HttpRequest_->GetResponseStream();
+ } catch (const TErrorResponse &) {
+ throw;
+ } catch (...) {
+ }
+ std::rethrow_exception(WriteError_);
+ }
+
+ void CheckErrorState()
+ {
+ if (WriteError_) {
+ std::rethrow_exception(WriteError_);
+ }
+ }
+
+private:
+ THttpRequest* const HttpRequest_;
+ TSocketOutput SocketOutput_;
+ THttpOutput HttpOutput_;
+ std::exception_ptr WriteError_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+THttpHeader::THttpHeader(const TString& method, const TString& command, bool isApi)
+ : Method(method)
+ , Command(command)
+ , IsApi(isApi)
+{ }
+
+void THttpHeader::AddParameter(const TString& key, TNode value, bool overwrite)
+{
+ auto it = Parameters.find(key);
+ if (it == Parameters.end()) {
+ Parameters.emplace(key, std::move(value));
+ } else {
+ if (overwrite) {
+ it->second = std::move(value);
+ } else {
+ ythrow yexception() << "Duplicate key: " << key;
+ }
+ }
+}
+
+void THttpHeader::MergeParameters(const TNode& newParameters, bool overwrite)
+{
+ for (const auto& p : newParameters.AsMap()) {
+ AddParameter(p.first, p.second, overwrite);
+ }
+}
+
+void THttpHeader::RemoveParameter(const TString& key)
+{
+ Parameters.erase(key);
+}
+
+TNode THttpHeader::GetParameters() const
+{
+ return Parameters;
+}
+
+void THttpHeader::AddTransactionId(const TTransactionId& transactionId, bool overwrite)
+{
+ if (transactionId) {
+ AddParameter("transaction_id", GetGuidAsString(transactionId), overwrite);
+ } else {
+ RemoveParameter("transaction_id");
+ }
+}
+
+void THttpHeader::AddPath(const TString& path, bool overwrite)
+{
+ AddParameter("path", path, overwrite);
+}
+
+void THttpHeader::AddOperationId(const TOperationId& operationId, bool overwrite)
+{
+ AddParameter("operation_id", GetGuidAsString(operationId), overwrite);
+}
+
+void THttpHeader::AddMutationId()
+{
+ TGUID guid;
+
+ // Some users use `fork()' with yt wrapper
+ // (actually they use python + multiprocessing)
+ // and CreateGuid is not resistant to `fork()', so spice it a little bit.
+ //
+ // Check IGNIETFERRO-610
+ CreateGuid(&guid);
+ guid.dw[2] = GetPID() ^ MicroSeconds();
+
+ AddParameter("mutation_id", GetGuidAsString(guid), true);
+}
+
+bool THttpHeader::HasMutationId() const
+{
+ return Parameters.contains("mutation_id");
+}
+
+void THttpHeader::SetToken(const TString& token)
+{
+ Token = token;
+}
+
+void THttpHeader::SetImpersonationUser(const TString& impersonationUser)
+{
+ ImpersonationUser = impersonationUser;
+}
+
+void THttpHeader::SetServiceTicket(const TString& ticket)
+{
+ ServiceTicket = ticket;
+}
+
+void THttpHeader::SetInputFormat(const TMaybe<TFormat>& format)
+{
+ InputFormat = format;
+}
+
+void THttpHeader::SetOutputFormat(const TMaybe<TFormat>& format)
+{
+ OutputFormat = format;
+}
+
+TMaybe<TFormat> THttpHeader::GetOutputFormat() const
+{
+ return OutputFormat;
+}
+
+void THttpHeader::SetRequestCompression(const TString& compression)
+{
+ RequestCompression = compression;
+}
+
+void THttpHeader::SetResponseCompression(const TString& compression)
+{
+ ResponseCompression = compression;
+}
+
+TString THttpHeader::GetCommand() const
+{
+ return Command;
+}
+
+TString THttpHeader::GetUrl() const
+{
+ TStringStream url;
+
+ if (IsApi) {
+ url << "/api/" << TConfig::Get()->ApiVersion << "/" << Command;
+ } else {
+ url << "/" << Command;
+ }
+
+ return url.Str();
+}
+
+bool THttpHeader::ShouldAcceptFraming() const
+{
+ return TConfig::Get()->CommandsWithFraming.contains(Command);
+}
+
+TString THttpHeader::GetHeaderAsString(const TString& hostName, const TString& requestId, bool includeParameters) const
+{
+ TStringStream result;
+
+ result << Method << " " << GetUrl() << " HTTP/1.1\r\n";
+
+ GetHeader(hostName, requestId, includeParameters).Get()->WriteTo(&result);
+
+ if (ShouldAcceptFraming()) {
+ result << "X-YT-Accept-Framing: 1\r\n";
+ }
+
+ result << "\r\n";
+
+ return result.Str();
+}
+
+NHttp::THeadersPtrWrapper THttpHeader::GetHeader(const TString& hostName, const TString& requestId, bool includeParameters) const
+{
+ auto headers = New<NHttp::THeaders>();
+
+ headers->Add("Host", hostName);
+ headers->Add("User-Agent", TProcessState::Get()->ClientVersion);
+
+ if (!Token.empty()) {
+ headers->Add("Authorization", "OAuth " + Token);
+ }
+ if (!ServiceTicket.empty()) {
+ headers->Add("X-Ya-Service-Ticket", ServiceTicket);
+ }
+ if (!ImpersonationUser.empty()) {
+ headers->Add("X-Yt-User-Name", ImpersonationUser);
+ }
+
+ if (Method == "PUT" || Method == "POST") {
+ headers->Add("Transfer-Encoding", "chunked");
+ }
+
+ headers->Add("X-YT-Correlation-Id", requestId);
+ headers->Add("X-YT-Header-Format", "<format=text>yson");
+
+ headers->Add("Content-Encoding", RequestCompression);
+ headers->Add("Accept-Encoding", ResponseCompression);
+
+ auto printYTHeader = [&headers] (const char* headerName, const TString& value) {
+ static const size_t maxHttpHeaderSize = 64 << 10;
+ if (!value) {
+ return;
+ }
+ if (value.size() <= maxHttpHeaderSize) {
+ headers->Add(headerName, value);
+ return;
+ }
+
+ TString encoded;
+ Base64Encode(value, encoded);
+ auto ptr = encoded.data();
+ auto finish = encoded.data() + encoded.size();
+ size_t index = 0;
+ do {
+ auto end = Min(ptr + maxHttpHeaderSize, finish);
+ headers->Add(Format("%v%v", headerName, index++), TString(ptr, end));
+ ptr = end;
+ } while (ptr != finish);
+ };
+
+ if (InputFormat) {
+ printYTHeader("X-YT-Input-Format", NodeToYsonString(InputFormat->Config));
+ }
+ if (OutputFormat) {
+ printYTHeader("X-YT-Output-Format", NodeToYsonString(OutputFormat->Config));
+ }
+ if (includeParameters) {
+ printYTHeader("X-YT-Parameters", NodeToYsonString(Parameters));
+ }
+
+ return NHttp::THeadersPtrWrapper(std::move(headers));
+}
+
+const TString& THttpHeader::GetMethod() const
+{
+ return Method;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAddressCache* TAddressCache::Get()
+{
+ return Singleton<TAddressCache>();
+}
+
+bool ContainsAddressOfRequiredVersion(const TAddressCache::TAddressPtr& address)
+{
+ if (!TConfig::Get()->ForceIpV4 && !TConfig::Get()->ForceIpV6) {
+ return true;
+ }
+
+ for (auto i = address->Begin(); i != address->End(); ++i) {
+ const auto& addressInfo = *i;
+ if (TConfig::Get()->ForceIpV4 && addressInfo.ai_family == AF_INET) {
+ return true;
+ }
+ if (TConfig::Get()->ForceIpV6 && addressInfo.ai_family == AF_INET6) {
+ return true;
+ }
+ }
+ return false;
+}
+
+TAddressCache::TAddressPtr TAddressCache::Resolve(const TString& hostName)
+{
+ auto address = FindAddress(hostName);
+ if (address) {
+ return address;
+ }
+
+ TString host(hostName);
+ ui16 port = 80;
+
+ auto colon = hostName.find(':');
+ if (colon != TString::npos) {
+ port = FromString<ui16>(hostName.substr(colon + 1));
+ host = hostName.substr(0, colon);
+ }
+
+ auto retryPolicy = CreateDefaultRequestRetryPolicy(TConfig::Get());
+ auto error = yexception() << "can not resolve address of required version for host " << hostName;
+ while (true) {
+ address = new TNetworkAddress(host, port);
+ if (ContainsAddressOfRequiredVersion(address)) {
+ break;
+ }
+ retryPolicy->NotifyNewAttempt();
+ YT_LOG_DEBUG("Failed to resolve address of required version for host %v, retrying: %v",
+ hostName,
+ retryPolicy->GetAttemptDescription());
+ if (auto backoffDuration = retryPolicy->OnGenericError(error)) {
+ NDetail::TWaitProxy::Get()->Sleep(*backoffDuration);
+ } else {
+ ythrow error;
+ }
+ }
+
+ AddAddress(hostName, address);
+ return address;
+}
+
+TAddressCache::TAddressPtr TAddressCache::FindAddress(const TString& hostName) const
+{
+ TCacheEntry entry;
+ {
+ TReadGuard guard(Lock_);
+ auto it = Cache_.find(hostName);
+ if (it == Cache_.end()) {
+ return nullptr;
+ }
+ entry = it->second;
+ }
+
+ if (TInstant::Now() > entry.ExpirationTime) {
+ YT_LOG_DEBUG("Address resolution cache entry for host %v is expired, will retry resolution",
+ hostName);
+ return nullptr;
+ }
+
+ if (!ContainsAddressOfRequiredVersion(entry.Address)) {
+ YT_LOG_DEBUG("Address of required version not found for host %v, will retry resolution",
+ hostName);
+ return nullptr;
+ }
+
+ return entry.Address;
+}
+
+void TAddressCache::AddAddress(TString hostName, TAddressPtr address)
+{
+ auto entry = TCacheEntry{
+ .Address = std::move(address),
+ .ExpirationTime = TInstant::Now() + TConfig::Get()->AddressCacheExpirationTimeout,
+ };
+
+ {
+ TWriteGuard guard(Lock_);
+ Cache_.emplace(std::move(hostName), std::move(entry));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConnectionPool* TConnectionPool::Get()
+{
+ return Singleton<TConnectionPool>();
+}
+
+TConnectionPtr TConnectionPool::Connect(
+ const TString& hostName,
+ TDuration socketTimeout)
+{
+ Refresh();
+
+ if (socketTimeout == TDuration::Zero()) {
+ socketTimeout = TConfig::Get()->SocketTimeout;
+ }
+
+ {
+ auto guard = Guard(Lock_);
+ auto now = TInstant::Now();
+ auto range = Connections_.equal_range(hostName);
+ for (auto it = range.first; it != range.second; ++it) {
+ auto& connection = it->second;
+ if (connection->DeadLine < now) {
+ continue;
+ }
+ if (!AtomicCas(&connection->Busy, 1, 0)) {
+ continue;
+ }
+
+ connection->DeadLine = now + socketTimeout;
+ connection->Socket->SetSocketTimeout(socketTimeout.Seconds());
+ return connection;
+ }
+ }
+
+ TConnectionPtr connection(new TConnection);
+
+ auto networkAddress = TAddressCache::Get()->Resolve(hostName);
+ TSocketHolder socket(DoConnect(networkAddress));
+ SetNonBlock(socket, false);
+
+ connection->Socket.Reset(new TSocket(socket.Release()));
+
+ connection->DeadLine = TInstant::Now() + socketTimeout;
+ connection->Socket->SetSocketTimeout(socketTimeout.Seconds());
+
+ {
+ auto guard = Guard(Lock_);
+ static ui32 connectionId = 0;
+ connection->Id = ++connectionId;
+ Connections_.insert({hostName, connection});
+ }
+
+ YT_LOG_DEBUG("New connection to %v #%v opened",
+ hostName,
+ connection->Id);
+
+ return connection;
+}
+
+void TConnectionPool::Release(TConnectionPtr connection)
+{
+ auto socketTimeout = TConfig::Get()->SocketTimeout;
+ auto newDeadline = TInstant::Now() + socketTimeout;
+
+ {
+ auto guard = Guard(Lock_);
+ connection->DeadLine = newDeadline;
+ }
+
+ connection->Socket->SetSocketTimeout(socketTimeout.Seconds());
+ AtomicSet(connection->Busy, 0);
+
+ Refresh();
+}
+
+void TConnectionPool::Invalidate(
+ const TString& hostName,
+ TConnectionPtr connection)
+{
+ auto guard = Guard(Lock_);
+ auto range = Connections_.equal_range(hostName);
+ for (auto it = range.first; it != range.second; ++it) {
+ if (it->second == connection) {
+ YT_LOG_DEBUG("Closing connection #%v",
+ connection->Id);
+ Connections_.erase(it);
+ return;
+ }
+ }
+}
+
+void TConnectionPool::Refresh()
+{
+ auto guard = Guard(Lock_);
+
+ // simple, since we don't expect too many connections
+ using TItem = std::pair<TInstant, TConnectionMap::iterator>;
+ std::vector<TItem> sortedConnections;
+ for (auto it = Connections_.begin(); it != Connections_.end(); ++it) {
+ sortedConnections.emplace_back(it->second->DeadLine, it);
+ }
+
+ std::sort(
+ sortedConnections.begin(),
+ sortedConnections.end(),
+ [] (const TItem& a, const TItem& b) -> bool {
+ return a.first < b.first;
+ });
+
+ auto removeCount = static_cast<int>(Connections_.size()) - TConfig::Get()->ConnectionPoolSize;
+
+ const auto now = TInstant::Now();
+ for (const auto& item : sortedConnections) {
+ const auto& mapIterator = item.second;
+ auto connection = mapIterator->second;
+ if (AtomicGet(connection->Busy)) {
+ continue;
+ }
+
+ if (removeCount > 0) {
+ Connections_.erase(mapIterator);
+ YT_LOG_DEBUG("Closing connection #%v (too many opened connections)",
+ connection->Id);
+ --removeCount;
+ continue;
+ }
+
+ if (connection->DeadLine < now) {
+ Connections_.erase(mapIterator);
+ YT_LOG_DEBUG("Closing connection #%v (timeout)",
+ connection->Id);
+ }
+ }
+}
+
+SOCKET TConnectionPool::DoConnect(TAddressCache::TAddressPtr address)
+{
+ int lastError = 0;
+
+ for (auto i = address->Begin(); i != address->End(); ++i) {
+ struct addrinfo* info = &*i;
+
+ if (TConfig::Get()->ForceIpV4 && info->ai_family != AF_INET) {
+ continue;
+ }
+
+ if (TConfig::Get()->ForceIpV6 && info->ai_family != AF_INET6) {
+ continue;
+ }
+
+ TSocketHolder socket(
+ ::socket(info->ai_family, info->ai_socktype, info->ai_protocol));
+
+ if (socket.Closed()) {
+ lastError = LastSystemError();
+ continue;
+ }
+
+ SetNonBlock(socket, true);
+ if (TConfig::Get()->SocketPriority) {
+ SetSocketPriority(socket, *TConfig::Get()->SocketPriority);
+ }
+
+ if (connect(socket, info->ai_addr, info->ai_addrlen) == 0)
+ return socket.Release();
+
+ int err = LastSystemError();
+ if (err == EINPROGRESS || err == EAGAIN || err == EWOULDBLOCK) {
+ struct pollfd p = {
+ socket,
+ POLLOUT,
+ 0
+ };
+ const ssize_t n = PollD(&p, 1, TInstant::Now() + TConfig::Get()->ConnectTimeout);
+ if (n < 0) {
+ ythrow TSystemError(-(int)n) << "can not connect to " << info;
+ }
+ CheckedGetSockOpt(socket, SOL_SOCKET, SO_ERROR, err, "socket error");
+ if (!err)
+ return socket.Release();
+ }
+
+ lastError = err;
+ continue;
+ }
+
+ ythrow TSystemError(lastError) << "can not connect to " << *address;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TMaybe<TString> GetProxyName(const THttpInput& input)
+{
+ if (auto proxyHeader = input.Headers().FindHeader("X-YT-Proxy")) {
+ return proxyHeader->Value();
+ }
+ return Nothing();
+}
+
+THttpResponse::THttpResponse(
+ IInputStream* socketStream,
+ const TString& requestId,
+ const TString& hostName)
+ : HttpInput_(socketStream)
+ , RequestId_(requestId)
+ , HostName_(GetProxyName(HttpInput_).GetOrElse(hostName))
+ , Unframe_(HttpInput_.Headers().HasHeader("X-YT-Framing"))
+{
+ HttpCode_ = ParseHttpRetCode(HttpInput_.FirstLine());
+ if (HttpCode_ == 200 || HttpCode_ == 202) {
+ return;
+ }
+
+ ErrorResponse_ = TErrorResponse(HttpCode_, RequestId_);
+
+ auto logAndSetError = [&] (const TString& rawError) {
+ YT_LOG_ERROR("RSP %v - HTTP %v - %v",
+ RequestId_,
+ HttpCode_,
+ rawError.data());
+ ErrorResponse_->SetRawError(rawError);
+ };
+
+ switch (HttpCode_) {
+ case 429:
+ logAndSetError("request rate limit exceeded");
+ break;
+
+ case 500:
+ logAndSetError(::TStringBuilder() << "internal error in proxy " << HostName_);
+ break;
+
+ default: {
+ TStringStream httpHeaders;
+ httpHeaders << "HTTP headers (";
+ for (const auto& header : HttpInput_.Headers()) {
+ httpHeaders << header.Name() << ": " << header.Value() << "; ";
+ }
+ httpHeaders << ")";
+
+ auto errorString = Sprintf("RSP %s - HTTP %d - %s",
+ RequestId_.data(),
+ HttpCode_,
+ httpHeaders.Str().data());
+
+ YT_LOG_ERROR("%v",
+ errorString.data());
+
+ if (auto parsedResponse = ParseError(HttpInput_.Headers())) {
+ ErrorResponse_ = parsedResponse.GetRef();
+ } else {
+ ErrorResponse_->SetRawError(
+ errorString + " - X-YT-Error is missing in headers");
+ }
+ break;
+ }
+ }
+}
+
+const THttpHeaders& THttpResponse::Headers() const
+{
+ return HttpInput_.Headers();
+}
+
+void THttpResponse::CheckErrorResponse() const
+{
+ if (ErrorResponse_) {
+ throw *ErrorResponse_;
+ }
+}
+
+bool THttpResponse::IsExhausted() const
+{
+ return IsExhausted_;
+}
+
+int THttpResponse::GetHttpCode() const
+{
+ return HttpCode_;
+}
+
+const TString& THttpResponse::GetHostName() const
+{
+ return HostName_;
+}
+
+bool THttpResponse::IsKeepAlive() const
+{
+ return HttpInput_.IsKeepAlive();
+}
+
+TMaybe<TErrorResponse> THttpResponse::ParseError(const THttpHeaders& headers)
+{
+ for (const auto& header : headers) {
+ if (header.Name() == "X-YT-Error") {
+ TErrorResponse errorResponse(HttpCode_, RequestId_);
+ errorResponse.ParseFromJsonError(header.Value());
+ if (errorResponse.IsOk()) {
+ return Nothing();
+ }
+ return errorResponse;
+ }
+ }
+ return Nothing();
+}
+
+size_t THttpResponse::DoRead(void* buf, size_t len)
+{
+ size_t read;
+ if (Unframe_) {
+ read = UnframeRead(buf, len);
+ } else {
+ read = HttpInput_.Read(buf, len);
+ }
+ if (read == 0 && len != 0) {
+ // THttpInput MUST return defined (but may be empty)
+ // trailers when it is exhausted.
+ Y_VERIFY(HttpInput_.Trailers().Defined(),
+ "trailers MUST be defined for exhausted stream");
+ CheckTrailers(HttpInput_.Trailers().GetRef());
+ IsExhausted_ = true;
+ }
+ return read;
+}
+
+size_t THttpResponse::DoSkip(size_t len)
+{
+ size_t skipped;
+ if (Unframe_) {
+ skipped = UnframeSkip(len);
+ } else {
+ skipped = HttpInput_.Skip(len);
+ }
+ if (skipped == 0 && len != 0) {
+ // THttpInput MUST return defined (but may be empty)
+ // trailers when it is exhausted.
+ Y_VERIFY(HttpInput_.Trailers().Defined(),
+ "trailers MUST be defined for exhausted stream");
+ CheckTrailers(HttpInput_.Trailers().GetRef());
+ IsExhausted_ = true;
+ }
+ return skipped;
+}
+
+void THttpResponse::CheckTrailers(const THttpHeaders& trailers)
+{
+ if (auto errorResponse = ParseError(trailers)) {
+ errorResponse->SetIsFromTrailers(true);
+ YT_LOG_ERROR("RSP %v - %v",
+ RequestId_,
+ errorResponse.GetRef().what());
+ ythrow errorResponse.GetRef();
+ }
+}
+
+static ui32 ReadDataFrameSize(THttpInput* stream)
+{
+ ui32 littleEndianSize;
+ auto read = stream->Load(&littleEndianSize, sizeof(littleEndianSize));
+ if (read < sizeof(littleEndianSize)) {
+ ythrow yexception() << "Bad data frame header: " <<
+ "expected " << sizeof(littleEndianSize) << " bytes, got " << read;
+ }
+ return LittleToHost(littleEndianSize);
+}
+
+bool THttpResponse::RefreshFrameIfNecessary()
+{
+ while (RemainingFrameSize_ == 0) {
+ ui8 frameTypeByte;
+ auto read = HttpInput_.Read(&frameTypeByte, sizeof(frameTypeByte));
+ if (read == 0) {
+ return false;
+ }
+ auto frameType = static_cast<EFrameType>(frameTypeByte);
+ switch (frameType) {
+ case EFrameType::KeepAlive:
+ break;
+ case EFrameType::Data:
+ RemainingFrameSize_ = ReadDataFrameSize(&HttpInput_);
+ break;
+ default:
+ ythrow yexception() << "Bad frame type " << static_cast<int>(frameTypeByte);
+ }
+ }
+ return true;
+}
+
+size_t THttpResponse::UnframeRead(void* buf, size_t len)
+{
+ if (!RefreshFrameIfNecessary()) {
+ return 0;
+ }
+ auto read = HttpInput_.Read(buf, Min(len, RemainingFrameSize_));
+ RemainingFrameSize_ -= read;
+ return read;
+}
+
+size_t THttpResponse::UnframeSkip(size_t len)
+{
+ if (!RefreshFrameIfNecessary()) {
+ return 0;
+ }
+ auto skipped = HttpInput_.Skip(Min(len, RemainingFrameSize_));
+ RemainingFrameSize_ -= skipped;
+ return skipped;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THttpRequest::THttpRequest()
+{
+ RequestId = CreateGuidAsString();
+}
+
+THttpRequest::THttpRequest(const TString& requestId)
+ : RequestId(requestId)
+{ }
+
+THttpRequest::~THttpRequest()
+{
+ if (!Connection) {
+ return;
+ }
+
+ if (Input && Input->IsKeepAlive() && Input->IsExhausted()) {
+ // We should return to the pool only connections where HTTP response was fully read.
+ // Otherwise next reader might read our remaining data and misinterpret them (YT-6510).
+ TConnectionPool::Get()->Release(Connection);
+ } else {
+ TConnectionPool::Get()->Invalidate(HostName, Connection);
+ }
+}
+
+TString THttpRequest::GetRequestId() const
+{
+ return RequestId;
+}
+
+void THttpRequest::Connect(TString hostName, TDuration socketTimeout)
+{
+ HostName = std::move(hostName);
+ YT_LOG_DEBUG("REQ %v - requesting connection to %v from connection pool",
+ RequestId,
+ HostName);
+
+ StartTime_ = TInstant::Now();
+ Connection = TConnectionPool::Get()->Connect(HostName, socketTimeout);
+
+ YT_LOG_DEBUG("REQ %v - connection #%v",
+ RequestId,
+ Connection->Id);
+}
+
+IOutputStream* THttpRequest::StartRequestImpl(const THttpHeader& header, bool includeParameters)
+{
+ auto strHeader = header.GetHeaderAsString(HostName, RequestId, includeParameters);
+ Url_ = header.GetUrl();
+
+ LogRequest(header, Url_, includeParameters, RequestId, HostName);
+
+ LoggedAttributes_ = GetLoggedAttributes(header, Url_, includeParameters, 128);
+
+ auto outputFormat = header.GetOutputFormat();
+ if (outputFormat && outputFormat->IsTextYson()) {
+ LogResponse = true;
+ }
+
+ RequestStream_ = MakeHolder<TRequestStream>(this, *Connection->Socket.Get());
+
+ RequestStream_->Write(strHeader.data(), strHeader.size());
+ return RequestStream_.Get();
+}
+
+IOutputStream* THttpRequest::StartRequest(const THttpHeader& header)
+{
+ return StartRequestImpl(header, true);
+}
+
+void THttpRequest::FinishRequest()
+{
+ RequestStream_->Flush();
+ RequestStream_->Finish();
+}
+
+void THttpRequest::SmallRequest(const THttpHeader& header, TMaybe<TStringBuf> body)
+{
+ if (!body && (header.GetMethod() == "PUT" || header.GetMethod() == "POST")) {
+ const auto& parameters = header.GetParameters();
+ auto parametersStr = NodeToYsonString(parameters);
+ auto* output = StartRequestImpl(header, false);
+ output->Write(parametersStr);
+ FinishRequest();
+ } else {
+ auto* output = StartRequest(header);
+ if (body) {
+ output->Write(*body);
+ }
+ FinishRequest();
+ }
+}
+
+THttpResponse* THttpRequest::GetResponseStream()
+{
+ if (!Input) {
+ SocketInput.Reset(new TSocketInput(*Connection->Socket.Get()));
+ if (TConfig::Get()->UseAbortableResponse) {
+ Y_VERIFY(!Url_.empty());
+ Input.Reset(new TAbortableHttpResponse(SocketInput.Get(), RequestId, HostName, Url_));
+ } else {
+ Input.Reset(new THttpResponse(SocketInput.Get(), RequestId, HostName));
+ }
+ Input->CheckErrorResponse();
+ }
+ return Input.Get();
+}
+
+TString THttpRequest::GetResponse()
+{
+ TString result = GetResponseStream()->ReadAll();
+
+ TStringStream loggedAttributes;
+ loggedAttributes
+ << "Time: " << TInstant::Now() - StartTime_ << "; "
+ << "HostName: " << GetResponseStream()->GetHostName() << "; "
+ << LoggedAttributes_;
+
+ if (LogResponse) {
+ constexpr auto sizeLimit = 1 << 7;
+ YT_LOG_DEBUG("RSP %v - received response (Response: '%v'; %v)",
+ RequestId,
+ TruncateForLogs(result, sizeLimit),
+ loggedAttributes.Str());
+ } else {
+ YT_LOG_DEBUG("RSP %v - received response of %v bytes (%v)",
+ RequestId,
+ result.size(),
+ loggedAttributes.Str());
+ }
+ return result;
+}
+
+int THttpRequest::GetHttpCode() {
+ return GetResponseStream()->GetHttpCode();
+}
+
+void THttpRequest::InvalidateConnection()
+{
+ TConnectionPool::Get()->Invalidate(HostName, Connection);
+ Connection.Reset();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/http.h b/yt/cpp/mapreduce/http/http.h
new file mode 100644
index 0000000000..ee8783088d
--- /dev/null
+++ b/yt/cpp/mapreduce/http/http.h
@@ -0,0 +1,256 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/common.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/format.h>
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/interface/node.h>
+
+#include <library/cpp/deprecated/atomic/atomic.h>
+#include <library/cpp/http/io/stream.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/hash_multi_map.h>
+#include <util/generic/strbuf.h>
+#include <util/generic/guid.h>
+#include <util/network/socket.h>
+#include <util/stream/input.h>
+#include <util/system/mutex.h>
+#include <util/system/rwlock.h>
+#include <util/generic/ptr.h>
+
+namespace NYT {
+
+class TNode;
+
+namespace NHttp {
+
+struct THeadersPtrWrapper;
+
+} // NHttp
+
+///////////////////////////////////////////////////////////////////////////////
+
+enum class EFrameType
+{
+ Data = 0x01,
+ KeepAlive = 0x02,
+};
+
+
+class THttpHeader
+{
+public:
+ THttpHeader(const TString& method, const TString& command, bool isApi = true);
+
+ void AddParameter(const TString& key, TNode value, bool overwrite = false);
+ void RemoveParameter(const TString& key);
+ void MergeParameters(const TNode& parameters, bool overwrite = false);
+ TNode GetParameters() const;
+
+ void AddTransactionId(const TTransactionId& transactionId, bool overwrite = false);
+ void AddPath(const TString& path, bool overwrite = false);
+ void AddOperationId(const TOperationId& operationId, bool overwrite = false);
+ void AddMutationId();
+ bool HasMutationId() const;
+
+ void SetToken(const TString& token);
+ void SetImpersonationUser(const TString& impersonationUser);
+
+ void SetServiceTicket(const TString& ticket);
+
+ void SetInputFormat(const TMaybe<TFormat>& format);
+
+ void SetOutputFormat(const TMaybe<TFormat>& format);
+ TMaybe<TFormat> GetOutputFormat() const;
+
+ void SetRequestCompression(const TString& compression);
+ void SetResponseCompression(const TString& compression);
+
+ TString GetCommand() const;
+ TString GetUrl() const;
+ TString GetHeaderAsString(const TString& hostName, const TString& requestId, bool includeParameters = true) const;
+ NHttp::THeadersPtrWrapper GetHeader(const TString& hostName, const TString& requestId, bool includeParameters) const;
+
+ const TString& GetMethod() const;
+
+private:
+ bool ShouldAcceptFraming() const;
+
+private:
+ const TString Method;
+ const TString Command;
+ const bool IsApi;
+
+ TNode::TMapType Parameters;
+ TString ImpersonationUser;
+ TString Token;
+ TString ServiceTicket;
+ TNode Attributes;
+
+private:
+ TMaybe<TFormat> InputFormat = TFormat::YsonText();
+ TMaybe<TFormat> OutputFormat = TFormat::YsonText();
+
+ TString RequestCompression = "identity";
+ TString ResponseCompression = "identity";
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAddressCache
+{
+public:
+ using TAddressPtr = TAtomicSharedPtr<TNetworkAddress>;
+
+ static TAddressCache* Get();
+
+ TAddressPtr Resolve(const TString& hostName);
+
+private:
+ struct TCacheEntry {
+ TAddressPtr Address;
+ TInstant ExpirationTime;
+ };
+
+private:
+ TAddressPtr FindAddress(const TString& hostName) const;
+ void AddAddress(TString hostName, TAddressPtr address);
+
+private:
+ TRWMutex Lock_;
+ THashMap<TString, TCacheEntry> Cache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConnection
+{
+ THolder<TSocket> Socket;
+ TAtomic Busy = 1;
+ TInstant DeadLine;
+ ui32 Id;
+};
+
+using TConnectionPtr = TAtomicSharedPtr<TConnection>;
+
+class TConnectionPool
+{
+public:
+ using TConnectionMap = THashMultiMap<TString, TConnectionPtr>;
+
+ static TConnectionPool* Get();
+
+ TConnectionPtr Connect(const TString& hostName, TDuration socketTimeout);
+ void Release(TConnectionPtr connection);
+ void Invalidate(const TString& hostName, TConnectionPtr connection);
+
+private:
+ void Refresh();
+ static SOCKET DoConnect(TAddressCache::TAddressPtr address);
+
+private:
+ TMutex Lock_;
+ TConnectionMap Connections_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// Input stream that handles YT-specific header/trailer errors
+// and throws TErrorResponse if it finds any.
+class THttpResponse
+ : public IInputStream
+{
+public:
+ // 'requestId' and 'hostName' are provided for debug reasons
+ // (they will appear in some error messages).
+ THttpResponse(
+ IInputStream* socketStream,
+ const TString& requestId,
+ const TString& hostName);
+
+ const THttpHeaders& Headers() const;
+
+ void CheckErrorResponse() const;
+ bool IsExhausted() const;
+ int GetHttpCode() const;
+ const TString& GetHostName() const;
+ bool IsKeepAlive() const;
+
+protected:
+ size_t DoRead(void* buf, size_t len) override;
+ size_t DoSkip(size_t len) override;
+
+private:
+ void CheckTrailers(const THttpHeaders& trailers);
+ TMaybe<TErrorResponse> ParseError(const THttpHeaders& headers);
+ size_t UnframeRead(void* buf, size_t len);
+ size_t UnframeSkip(size_t len);
+ bool RefreshFrameIfNecessary();
+
+private:
+ THttpInput HttpInput_;
+ const TString RequestId_;
+ const TString HostName_;
+ int HttpCode_ = 0;
+ TMaybe<TErrorResponse> ErrorResponse_;
+ bool IsExhausted_ = false;
+ const bool Unframe_;
+ size_t RemainingFrameSize_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpRequest
+{
+public:
+ THttpRequest();
+ THttpRequest(const TString& requestId);
+ ~THttpRequest();
+
+ TString GetRequestId() const;
+
+ void Connect(TString hostName, TDuration socketTimeout = TDuration::Zero());
+
+ IOutputStream* StartRequest(const THttpHeader& header);
+ void FinishRequest();
+
+ void SmallRequest(const THttpHeader& header, TMaybe<TStringBuf> body);
+
+ THttpResponse* GetResponseStream();
+
+ TString GetResponse();
+
+ void InvalidateConnection();
+
+ int GetHttpCode();
+
+private:
+ IOutputStream* StartRequestImpl(const THttpHeader& header, bool includeParameters);
+
+private:
+ class TRequestStream;
+
+private:
+ TString HostName;
+ TString RequestId;
+ TString Url_;
+ TInstant StartTime_;
+ TString LoggedAttributes_;
+
+ TConnectionPtr Connection;
+
+ THolder<TRequestStream> RequestStream_;
+
+ THolder<TSocketInput> SocketInput;
+ THolder<THttpResponse> Input;
+
+ bool LogResponse = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/http_client.cpp b/yt/cpp/mapreduce/http/http_client.cpp
new file mode 100644
index 0000000000..a2af1182dc
--- /dev/null
+++ b/yt/cpp/mapreduce/http/http_client.cpp
@@ -0,0 +1,603 @@
+#include "http_client.h"
+
+#include "abortable_http_response.h"
+#include "core.h"
+#include "helpers.h"
+#include "http.h"
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <yt/yt/core/http/client.h>
+#include <yt/yt/core/http/config.h>
+#include <yt/yt/core/http/http.h>
+
+#include <yt/yt/core/https/client.h>
+#include <yt/yt/core/https/config.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+namespace NYT::NHttpClient {
+
+namespace {
+
+TString CreateHost(TStringBuf host, TStringBuf port)
+{
+ if (!port.empty()) {
+ return Format("%v:%v", host, port);
+ }
+
+ return TString(host);
+}
+
+TMaybe<TErrorResponse> GetErrorResponse(const TString& hostName, const TString& requestId, const NHttp::IResponsePtr& response)
+{
+ auto httpCode = response->GetStatusCode();
+ if (httpCode == NHttp::EStatusCode::OK || httpCode == NHttp::EStatusCode::Accepted) {
+ return {};
+ }
+
+ TErrorResponse errorResponse(static_cast<int>(httpCode), requestId);
+
+ auto logAndSetError = [&] (const TString& rawError) {
+ YT_LOG_ERROR("RSP %v - HTTP %v - %v",
+ requestId,
+ httpCode,
+ rawError.data());
+ errorResponse.SetRawError(rawError);
+ };
+
+ switch (httpCode) {
+ case NHttp::EStatusCode::TooManyRequests:
+ logAndSetError("request rate limit exceeded");
+ break;
+
+ case NHttp::EStatusCode::InternalServerError:
+ logAndSetError("internal error in proxy " + hostName);
+ break;
+
+ default: {
+ TStringStream httpHeaders;
+ httpHeaders << "HTTP headers (";
+ for (const auto& [headerName, headerValue] : response->GetHeaders()->Dump()) {
+ httpHeaders << headerName << ": " << headerValue << "; ";
+ }
+ httpHeaders << ")";
+
+ auto errorString = Sprintf("RSP %s - HTTP %d - %s",
+ requestId.data(),
+ static_cast<int>(httpCode),
+ httpHeaders.Str().data());
+
+ YT_LOG_ERROR("%v",
+ errorString.data());
+
+ if (auto errorHeader = response->GetHeaders()->Find("X-YT-Error")) {
+ errorResponse.ParseFromJsonError(*errorHeader);
+ if (errorResponse.IsOk()) {
+ return Nothing();
+ }
+ return errorResponse;
+ }
+
+ errorResponse.SetRawError(
+ errorString + " - X-YT-Error is missing in headers");
+ break;
+ }
+ }
+
+ return errorResponse;
+}
+
+void CheckErrorResponse(const TString& hostName, const TString& requestId, const NHttp::IResponsePtr& response)
+{
+ auto errorResponse = GetErrorResponse(hostName, requestId, response);
+ if (errorResponse) {
+ throw *errorResponse;
+ }
+}
+
+} // namespace
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TDefaultHttpResponse
+ : public IHttpResponse
+{
+public:
+ TDefaultHttpResponse(std::unique_ptr<THttpRequest> request)
+ : Request_(std::move(request))
+ { }
+
+ int GetStatusCode() override
+ {
+ return Request_->GetHttpCode();
+ }
+
+ IInputStream* GetResponseStream() override
+ {
+ return Request_->GetResponseStream();
+ }
+
+ TString GetResponse() override
+ {
+ return Request_->GetResponse();
+ }
+
+ TString GetRequestId() const override
+ {
+ return Request_->GetRequestId();
+ }
+
+private:
+ std::unique_ptr<THttpRequest> Request_;
+};
+
+class TDefaultHttpRequest
+ : public IHttpRequest
+{
+public:
+ TDefaultHttpRequest(std::unique_ptr<THttpRequest> request, IOutputStream* stream)
+ : Request_(std::move(request))
+ , Stream_(stream)
+ { }
+
+ IOutputStream* GetStream() override
+ {
+ return Stream_;
+ }
+
+ IHttpResponsePtr Finish() override
+ {
+ Request_->FinishRequest();
+ return std::make_unique<TDefaultHttpResponse>(std::move(Request_));
+ }
+
+private:
+ std::unique_ptr<THttpRequest> Request_;
+ IOutputStream* Stream_;
+};
+
+class TDefaultHttpClient
+ : public IHttpClient
+{
+public:
+ IHttpResponsePtr Request(const TString& url, const TString& requestId, const THttpConfig& config, const THttpHeader& header, TMaybe<TStringBuf> body) override
+ {
+ auto request = std::make_unique<THttpRequest>(requestId);
+
+ auto urlRef = NHttp::ParseUrl(url);
+
+ request->Connect(CreateHost(urlRef.Host, urlRef.PortStr), config.SocketTimeout);
+ request->SmallRequest(header, body);
+ return std::make_unique<TDefaultHttpResponse>(std::move(request));
+ }
+
+ IHttpRequestPtr StartRequest(const TString& url, const TString& requestId, const THttpConfig& config, const THttpHeader& header) override
+ {
+ auto request = std::make_unique<THttpRequest>(requestId);
+
+ auto urlRef = NHttp::ParseUrl(url);
+
+ request->Connect(CreateHost(urlRef.Host, urlRef.PortStr), config.SocketTimeout);
+ auto stream = request->StartRequest(header);
+ return std::make_unique<TDefaultHttpRequest>(std::move(request), stream);
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct TCoreRequestContext
+{
+ TString HostName;
+ TString Url;
+ TString RequestId;
+ bool LogResponse;
+ TInstant StartTime;
+ TString LoggedAttributes;
+};
+
+class TCoreHttpResponse
+ : public IHttpResponse
+{
+public:
+ TCoreHttpResponse(
+ TCoreRequestContext context,
+ NHttp::IResponsePtr response)
+ : Context_(std::move(context))
+ , Response_(std::move(response))
+ { }
+
+ int GetStatusCode() override
+ {
+ return static_cast<int>(Response_->GetStatusCode());
+ }
+
+ IInputStream* GetResponseStream() override
+ {
+ if (!Stream_) {
+ auto stream = std::make_unique<TWrappedStream>(
+ NConcurrency::CreateSyncAdapter(NConcurrency::CreateCopyingAdapter(Response_), NConcurrency::EWaitForStrategy::WaitFor),
+ Response_,
+ Context_.RequestId);
+ CheckErrorResponse(Context_.HostName, Context_.RequestId, Response_);
+
+ if (TConfig::Get()->UseAbortableResponse) {
+ Y_VERIFY(!Context_.Url.empty());
+ Stream_ = std::make_unique<TAbortableCoreHttpResponse>(std::move(stream), Context_.Url);
+ } else {
+ Stream_ = std::move(stream);
+ }
+ }
+
+ return Stream_.get();
+ }
+
+ TString GetResponse() override
+ {
+ auto result = GetResponseStream()->ReadAll();
+
+ TStringStream loggedAttributes;
+ loggedAttributes
+ << "Time: " << TInstant::Now() - Context_.StartTime << "; "
+ << "HostName: " << Context_.HostName << "; "
+ << Context_.LoggedAttributes;
+
+ if (Context_.LogResponse) {
+ constexpr auto sizeLimit = 1 << 7;
+ YT_LOG_DEBUG("RSP %v - received response (Response: '%v'; %v)",
+ Context_.RequestId,
+ TruncateForLogs(result, sizeLimit),
+ loggedAttributes.Str());
+ } else {
+ YT_LOG_DEBUG("RSP %v - received response of %v bytes (%v)",
+ Context_.RequestId,
+ result.size(),
+ loggedAttributes.Str());
+ }
+ return result;
+ }
+
+ TString GetRequestId() const override
+ {
+ return Context_.RequestId;
+ }
+
+private:
+ class TWrappedStream
+ : public IInputStream
+ {
+ public:
+ TWrappedStream(std::unique_ptr<IInputStream> underlying, NHttp::IResponsePtr response, TString requestId)
+ : Underlying_(std::move(underlying))
+ , Response_(std::move(response))
+ , RequestId_(std::move(requestId))
+ { }
+
+ protected:
+ size_t DoRead(void* buf, size_t len) override
+ {
+ size_t read = Underlying_->Read(buf, len);
+
+ if (read == 0 && len != 0) {
+ CheckTrailers(Response_->GetTrailers());
+ }
+ return read;
+ }
+
+ size_t DoSkip(size_t len) override
+ {
+ size_t skipped = Underlying_->Skip(len);
+ if (skipped == 0 && len != 0) {
+ CheckTrailers(Response_->GetTrailers());
+ }
+ return skipped;
+ }
+
+ private:
+ void CheckTrailers(const NHttp::THeadersPtr& trailers)
+ {
+ if (auto errorResponse = ParseError(trailers)) {
+ errorResponse->SetIsFromTrailers(true);
+ YT_LOG_ERROR("RSP %v - %v",
+ RequestId_,
+ errorResponse.GetRef().what());
+ ythrow errorResponse.GetRef();
+ }
+ }
+
+ TMaybe<TErrorResponse> ParseError(const NHttp::THeadersPtr& headers)
+ {
+ if (auto errorHeader = headers->Find("X-YT-Error")) {
+ TErrorResponse errorResponse(static_cast<int>(Response_->GetStatusCode()), RequestId_);
+ errorResponse.ParseFromJsonError(*errorHeader);
+ if (errorResponse.IsOk()) {
+ return Nothing();
+ }
+ return errorResponse;
+ }
+ return Nothing();
+ }
+
+ private:
+ std::unique_ptr<IInputStream> Underlying_;
+ NHttp::IResponsePtr Response_;
+ TString RequestId_;
+ };
+
+private:
+ TCoreRequestContext Context_;
+ NHttp::IResponsePtr Response_;
+ std::unique_ptr<IInputStream> Stream_;
+};
+
+class TCoreHttpRequest
+ : public IHttpRequest
+{
+public:
+ TCoreHttpRequest(TCoreRequestContext context, NHttp::IActiveRequestPtr activeRequest)
+ : Context_(std::move(context))
+ , ActiveRequest_(std::move(activeRequest))
+ , Stream_(NConcurrency::CreateBufferedSyncAdapter(ActiveRequest_->GetRequestStream()))
+ , WrappedStream_(this, Stream_.get())
+ { }
+
+ IOutputStream* GetStream() override
+ {
+ return &WrappedStream_;
+ }
+
+ IHttpResponsePtr Finish() override
+ {
+ WrappedStream_.Flush();
+ auto response = ActiveRequest_->Finish().Get().ValueOrThrow();
+ return std::make_unique<TCoreHttpResponse>(std::move(Context_), std::move(response));
+ }
+
+ IHttpResponsePtr FinishWithError()
+ {
+ auto response = ActiveRequest_->GetResponse();
+ return std::make_unique<TCoreHttpResponse>(std::move(Context_), std::move(response));
+ }
+
+private:
+ class TWrappedStream
+ : public IOutputStream
+ {
+ public:
+ TWrappedStream(TCoreHttpRequest* httpRequest, IOutputStream* underlying)
+ : HttpRequest_(httpRequest)
+ , Underlying_(underlying)
+ { }
+
+ private:
+ void DoWrite(const void* buf, size_t len) override
+ {
+ WrapWriteFunc([&] {
+ Underlying_->Write(buf, len);
+ });
+ }
+
+ void DoWriteV(const TPart* parts, size_t count) override
+ {
+ WrapWriteFunc([&] {
+ Underlying_->Write(parts, count);
+ });
+ }
+
+ void DoWriteC(char ch) override
+ {
+ WrapWriteFunc([&] {
+ Underlying_->Write(ch);
+ });
+ }
+
+ void DoFlush() override
+ {
+ WrapWriteFunc([&] {
+ Underlying_->Flush();
+ });
+ }
+
+ void DoFinish() override
+ {
+ WrapWriteFunc([&] {
+ Underlying_->Finish();
+ });
+ }
+
+ void WrapWriteFunc(std::function<void()> func)
+ {
+ CheckErrorState();
+ try {
+ func();
+ } catch (const std::exception&) {
+ HandleWriteException();
+ }
+ }
+
+ // In many cases http proxy stops reading request and resets connection
+ // if error has happend. This function tries to read error response
+ // in such cases.
+ void HandleWriteException() {
+ Y_VERIFY(WriteError_ == nullptr);
+ WriteError_ = std::current_exception();
+ Y_VERIFY(WriteError_ != nullptr);
+ try {
+ HttpRequest_->FinishWithError()->GetResponseStream();
+ } catch (const TErrorResponse &) {
+ throw;
+ } catch (...) {
+ }
+ std::rethrow_exception(WriteError_);
+ }
+
+ void CheckErrorState()
+ {
+ if (WriteError_) {
+ std::rethrow_exception(WriteError_);
+ }
+ }
+
+ private:
+ TCoreHttpRequest* const HttpRequest_;
+ IOutputStream* Underlying_;
+ std::exception_ptr WriteError_;
+ };
+
+private:
+ TCoreRequestContext Context_;
+ NHttp::IActiveRequestPtr ActiveRequest_;
+ std::unique_ptr<IOutputStream> Stream_;
+ TWrappedStream WrappedStream_;
+};
+
+class TCoreHttpClient
+ : public IHttpClient
+{
+public:
+ TCoreHttpClient(bool useTLS, const TConfigPtr& config)
+ : Poller_(NConcurrency::CreateThreadPoolPoller(1, "http_poller")) // TODO(nadya73): YT-18363: move threads count to config
+ {
+ if (useTLS) {
+ auto httpsConfig = NYT::New<NYT::NHttps::TClientConfig>();
+ httpsConfig->MaxIdleConnections = config->ConnectionPoolSize;
+ Client_ = NHttps::CreateClient(httpsConfig, Poller_);
+ } else {
+ auto httpConfig = NYT::New<NYT::NHttp::TClientConfig>();
+ httpConfig->MaxIdleConnections = config->ConnectionPoolSize;
+ Client_ = NHttp::CreateClient(httpConfig, Poller_);
+ }
+ }
+
+ IHttpResponsePtr Request(const TString& url, const TString& requestId, const THttpConfig& /*config*/, const THttpHeader& header, TMaybe<TStringBuf> body) override
+ {
+ TCoreRequestContext context = CreateContext(url, requestId, header);
+
+ // TODO(nadya73): YT-18363: pass socket timeouts from THttpConfig
+
+ NHttp::IResponsePtr response;
+
+ auto logRequest = [&](bool includeParameters) {
+ LogRequest(header, url, includeParameters, requestId, context.HostName);
+ context.LoggedAttributes = GetLoggedAttributes(header, url, includeParameters, 128);
+ };
+
+ if (!body && (header.GetMethod() == "PUT" || header.GetMethod() == "POST")) {
+ const auto& parameters = header.GetParameters();
+ auto parametersStr = NodeToYsonString(parameters);
+
+ bool includeParameters = false;
+ auto headers = header.GetHeader(context.HostName, requestId, includeParameters).Get();
+
+ logRequest(includeParameters);
+
+ auto activeRequest = StartRequestImpl(header.GetMethod(), url, headers);
+
+ activeRequest->GetRequestStream()->Write(TSharedRef::FromString(parametersStr)).Get().ThrowOnError();
+ response = activeRequest->Finish().Get().ValueOrThrow();
+ } else {
+ auto bodyRef = TSharedRef::FromString(TString(body ? *body : ""));
+ bool includeParameters = true;
+ auto headers = header.GetHeader(context.HostName, requestId, includeParameters).Get();
+
+ logRequest(includeParameters);
+
+ if (header.GetMethod() == "GET") {
+ response = RequestImpl(header.GetMethod(), url, headers, bodyRef);
+ } else {
+ auto activeRequest = StartRequestImpl(header.GetMethod(), url, headers);
+
+ auto request = std::make_unique<TCoreHttpRequest>(std::move(context), std::move(activeRequest));
+ if (body) {
+ request->GetStream()->Write(*body);
+ }
+ return request->Finish();
+ }
+ }
+
+ return std::make_unique<TCoreHttpResponse>(std::move(context), std::move(response));
+ }
+
+ IHttpRequestPtr StartRequest(const TString& url, const TString& requestId, const THttpConfig& /*config*/, const THttpHeader& header) override
+ {
+ TCoreRequestContext context = CreateContext(url, requestId, header);
+
+ LogRequest(header, url, true, requestId, context.HostName);
+ context.LoggedAttributes = GetLoggedAttributes(header, url, true, 128);
+
+ auto headers = header.GetHeader(context.HostName, requestId, true).Get();
+ auto activeRequest = StartRequestImpl(header.GetMethod(), url, headers);
+
+ return std::make_unique<TCoreHttpRequest>(std::move(context), std::move(activeRequest));
+ }
+
+private:
+ TCoreRequestContext CreateContext(const TString& url, const TString& requestId, const THttpHeader& header)
+ {
+ TCoreRequestContext context;
+ context.Url = url;
+ context.RequestId = requestId;
+
+ auto urlRef = NHttp::ParseUrl(url);
+ context.HostName = CreateHost(urlRef.Host, urlRef.PortStr);
+
+ context.LogResponse = false;
+ auto outputFormat = header.GetOutputFormat();
+ if (outputFormat && outputFormat->IsTextYson()) {
+ context.LogResponse = true;
+ }
+ context.StartTime = TInstant::Now();
+ return context;
+ }
+
+ NHttp::IResponsePtr RequestImpl(const TString& method, const TString& url, const NHttp::THeadersPtr& headers, const TSharedRef& body)
+ {
+ if (method == "GET") {
+ return Client_->Get(url, headers).Get().ValueOrThrow();
+ } else if (method == "POST") {
+ return Client_->Post(url, body, headers).Get().ValueOrThrow();
+ } else if (method == "PUT") {
+ return Client_->Put(url, body, headers).Get().ValueOrThrow();
+ } else {
+ YT_LOG_FATAL("Unsupported http method (Method: %v, Url: %v)",
+ method,
+ url);
+ }
+ }
+
+ NHttp::IActiveRequestPtr StartRequestImpl(const TString& method, const TString& url, const NHttp::THeadersPtr& headers)
+ {
+ if (method == "POST") {
+ return Client_->StartPost(url, headers).Get().ValueOrThrow();
+ } else if (method == "PUT") {
+ return Client_->StartPut(url, headers).Get().ValueOrThrow();
+ } else {
+ YT_LOG_FATAL("Unsupported http method (Method: %v, Url: %v)",
+ method,
+ url);
+ }
+ }
+
+ NConcurrency::IThreadPoolPollerPtr Poller_;
+ NHttp::IClientPtr Client_;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+IHttpClientPtr CreateDefaultHttpClient()
+{
+ return std::make_shared<TDefaultHttpClient>();
+}
+
+IHttpClientPtr CreateCoreHttpClient(bool useTLS, const TConfigPtr& config)
+{
+ return std::make_shared<TCoreHttpClient>(useTLS, config);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttpClient
diff --git a/yt/cpp/mapreduce/http/http_client.h b/yt/cpp/mapreduce/http/http_client.h
new file mode 100644
index 0000000000..859f0423cb
--- /dev/null
+++ b/yt/cpp/mapreduce/http/http_client.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+
+#include <util/stream/fwd.h>
+
+#include <memory>
+
+namespace NYT::NHttpClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct THttpConfig
+{
+ TDuration SocketTimeout = TDuration::Zero();
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class IHttpResponse
+{
+public:
+ virtual ~IHttpResponse() = default;
+
+ virtual int GetStatusCode() = 0;
+ virtual IInputStream* GetResponseStream() = 0;
+ virtual TString GetResponse() = 0;
+ virtual TString GetRequestId() const = 0;
+};
+
+class IHttpRequest
+{
+public:
+ virtual ~IHttpRequest() = default;
+
+ virtual IOutputStream* GetStream() = 0;
+ virtual IHttpResponsePtr Finish() = 0;
+};
+
+
+class IHttpClient
+{
+public:
+ virtual ~IHttpClient() = default;
+
+ virtual IHttpResponsePtr Request(const TString& url, const TString& requestId, const THttpConfig& config, const THttpHeader& header, TMaybe<TStringBuf> body = {}) = 0;
+
+ virtual IHttpResponsePtr Request(const TString& url, const TString& requestId, const THttpHeader& header, TMaybe<TStringBuf> body = {})
+ {
+ return Request(url, requestId, /*config*/ {}, header, body);
+ }
+
+ virtual IHttpRequestPtr StartRequest(const TString& url, const TString& requestId, const THttpConfig& config, const THttpHeader& header) = 0;
+
+ virtual IHttpRequestPtr StartRequest(const TString& url, const TString& requestId, const THttpHeader& header)
+ {
+ return StartRequest(url, requestId, /*config*/ {}, header);
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+IHttpClientPtr CreateDefaultHttpClient();
+
+IHttpClientPtr CreateCoreHttpClient(bool useTLS, const TConfigPtr& config);
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttpClient
diff --git a/yt/cpp/mapreduce/http/requests.cpp b/yt/cpp/mapreduce/http/requests.cpp
new file mode 100644
index 0000000000..7cf0f673bb
--- /dev/null
+++ b/yt/cpp/mapreduce/http/requests.cpp
@@ -0,0 +1,66 @@
+#include "requests.h"
+
+#include "context.h"
+#include "host_manager.h"
+#include "retry_request.h"
+
+#include <yt/cpp/mapreduce/client/transaction.h>
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/node_builder.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <util/stream/file.h>
+#include <util/string/builder.h>
+#include <util/generic/buffer.h>
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool ParseBoolFromResponse(const TString& response)
+{
+ return GetBool(NodeFromYsonString(response));
+}
+
+TGUID ParseGuidFromResponse(const TString& response)
+{
+ auto node = NodeFromYsonString(response);
+ return GetGuid(node.AsString());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetProxyForHeavyRequest(const TClientContext& context)
+{
+ if (!context.Config->UseHosts) {
+ return context.ServerName;
+ }
+
+ return NPrivate::THostManager::Get().GetProxyForHeavyRequest(context);
+}
+
+void LogRequestError(
+ const TString& requestId,
+ const THttpHeader& header,
+ const TString& message,
+ const TString& attemptDescription)
+{
+ YT_LOG_ERROR("RSP %v - %v - %v - %v - X-YT-Parameters: %v",
+ requestId,
+ header.GetUrl(),
+ message,
+ attemptDescription,
+ NodeToYsonString(header.GetParameters()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/requests.h b/yt/cpp/mapreduce/http/requests.h
new file mode 100644
index 0000000000..2c692475d1
--- /dev/null
+++ b/yt/cpp/mapreduce/http/requests.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "fwd.h"
+#include "http.h"
+
+#include <util/generic/maybe.h>
+#include <util/str_stl.h>
+
+namespace NYT {
+
+///////////////////////////////////////////////////////////////////////////////
+
+bool ParseBoolFromResponse(const TString& response);
+
+TGUID ParseGuidFromResponse(const TString& response);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetProxyForHeavyRequest(const TClientContext& context);
+
+void LogRequestError(
+ const TString& requestId,
+ const THttpHeader& header,
+ const TString& message,
+ const TString& attemptDescription);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/retry_request.cpp b/yt/cpp/mapreduce/http/retry_request.cpp
new file mode 100644
index 0000000000..ba116edcf7
--- /dev/null
+++ b/yt/cpp/mapreduce/http/retry_request.cpp
@@ -0,0 +1,149 @@
+#include "retry_request.h"
+
+#include "context.h"
+#include "helpers.h"
+#include "http_client.h"
+#include "requests.h"
+
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+namespace NYT {
+namespace NDetail {
+
+///////////////////////////////////////////////////////////////////////////////
+
+static TResponseInfo Request(
+ const TClientContext& context,
+ THttpHeader& header,
+ TMaybe<TStringBuf> body,
+ const TString& requestId,
+ const TRequestConfig& config)
+{
+ TString hostName;
+ if (config.IsHeavy) {
+ hostName = GetProxyForHeavyRequest(context);
+ } else {
+ hostName = context.ServerName;
+ }
+
+ auto url = GetFullUrl(hostName, context, header);
+
+ auto response = context.HttpClient->Request(url, requestId, config.HttpConfig, header, body);
+
+ TResponseInfo result;
+ result.RequestId = requestId;
+ result.Response = response->GetResponse();
+ result.HttpCode = response->GetStatusCode();
+ return result;
+}
+
+TResponseInfo RequestWithoutRetry(
+ const TClientContext& context,
+ THttpHeader& header,
+ TMaybe<TStringBuf> body,
+ const TRequestConfig& config)
+{
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+
+ if (context.ImpersonationUser) {
+ header.SetImpersonationUser(*context.ImpersonationUser);
+ }
+
+ if (header.HasMutationId()) {
+ header.RemoveParameter("retry");
+ header.AddMutationId();
+ }
+ auto requestId = CreateGuidAsString();
+ return Request(context, header, body, requestId, config);
+}
+
+
+TResponseInfo RetryRequestWithPolicy(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ THttpHeader& header,
+ TMaybe<TStringBuf> body,
+ const TRequestConfig& config)
+{
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+
+ if (context.ImpersonationUser) {
+ header.SetImpersonationUser(*context.ImpersonationUser);
+ }
+
+ bool useMutationId = header.HasMutationId();
+ bool retryWithSameMutationId = false;
+
+ if (!retryPolicy) {
+ retryPolicy = CreateDefaultRequestRetryPolicy(context.Config);
+ }
+
+ while (true) {
+ auto requestId = CreateGuidAsString();
+ try {
+ retryPolicy->NotifyNewAttempt();
+
+ if (useMutationId) {
+ if (retryWithSameMutationId) {
+ header.AddParameter("retry", true, /* overwrite = */ true);
+ } else {
+ header.RemoveParameter("retry");
+ header.AddMutationId();
+ }
+ }
+
+ return Request(context, header, body, requestId, config);
+ } catch (const TErrorResponse& e) {
+ LogRequestError(requestId, header, e.GetError().GetMessage(), retryPolicy->GetAttemptDescription());
+ retryWithSameMutationId = e.IsTransportError();
+
+ if (!IsRetriable(e)) {
+ throw;
+ }
+
+ auto maybeRetryTimeout = retryPolicy->OnRetriableError(e);
+ if (maybeRetryTimeout) {
+ TWaitProxy::Get()->Sleep(*maybeRetryTimeout);
+ } else {
+ throw;
+ }
+ } catch (const std::exception& e) {
+ LogRequestError(requestId, header, e.what(), retryPolicy->GetAttemptDescription());
+ retryWithSameMutationId = true;
+
+ if (!IsRetriable(e)) {
+ throw;
+ }
+
+ auto maybeRetryTimeout = retryPolicy->OnGenericError(e);
+ if (maybeRetryTimeout) {
+ TWaitProxy::Get()->Sleep(*maybeRetryTimeout);
+ } else {
+ throw;
+ }
+ }
+ }
+
+ Y_FAIL("Retries must have either succeeded or thrown an exception");
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/http/retry_request.h b/yt/cpp/mapreduce/http/retry_request.h
new file mode 100644
index 0000000000..2210e318f1
--- /dev/null
+++ b/yt/cpp/mapreduce/http/retry_request.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/http/http_client.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////
+
+struct TResponseInfo
+{
+ TString RequestId;
+ TString Response;
+ int HttpCode = 0;
+};
+
+////////////////////////////////////////////////////////////////////
+
+struct TRequestConfig
+{
+ NHttpClient::THttpConfig HttpConfig;
+ bool IsHeavy = false;
+};
+
+////////////////////////////////////////////////////////////////////
+
+// Retry request with given `header' and `body' using `retryPolicy'.
+// If `retryPolicy == nullptr' use default, currently `TAttemptLimitedRetryPolicy(TConfig::Get()->RetryCount)`.
+TResponseInfo RetryRequestWithPolicy(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ THttpHeader& header,
+ TMaybe<TStringBuf> body = {},
+ const TRequestConfig& config = TRequestConfig());
+
+TResponseInfo RequestWithoutRetry(
+ const TClientContext& context,
+ THttpHeader& header,
+ TMaybe<TStringBuf> body = {},
+ const TRequestConfig& config = TRequestConfig());
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/cpp/mapreduce/http/ya.make b/yt/cpp/mapreduce/http/ya.make
new file mode 100644
index 0000000000..ef81a4b64a
--- /dev/null
+++ b/yt/cpp/mapreduce/http/ya.make
@@ -0,0 +1,29 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ abortable_http_response.cpp
+ context.cpp
+ helpers.cpp
+ host_manager.cpp
+ http.cpp
+ http_client.cpp
+ requests.cpp
+ retry_request.cpp
+)
+
+PEERDIR(
+ library/cpp/deprecated/atomic
+ library/cpp/http/io
+ library/cpp/string_utils/base64
+ library/cpp/string_utils/quote
+ library/cpp/threading/cron
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/interface/logging
+ yt/yt/core/http
+ yt/yt/core/https
+)
+
+END()
diff --git a/yt/cpp/mapreduce/interface/batch_request.cpp b/yt/cpp/mapreduce/interface/batch_request.cpp
new file mode 100644
index 0000000000..fefdacb61a
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/batch_request.cpp
@@ -0,0 +1,15 @@
+#include "batch_request.h"
+#include "client.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IBatchRequestBase& IBatchRequest::WithTransaction(const ITransactionPtr& transaction)
+{
+ return WithTransaction(transaction->GetId());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/batch_request.h b/yt/cpp/mapreduce/interface/batch_request.h
new file mode 100644
index 0000000000..3ea28f76fd
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/batch_request.h
@@ -0,0 +1,222 @@
+#pragma once
+
+#include "fwd.h"
+
+#include "client_method_options.h"
+
+#include <library/cpp/threading/future/future.h>
+#include <util/generic/ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////
+
+/// Helper base of @ref NYT::IBatchRequest holding most of useful methods.
+class IBatchRequestBase
+ : public TThrRefBase
+{
+public:
+ virtual ~IBatchRequestBase() = default;
+
+ ///
+ /// @brief Create cypress node.
+ ///
+ /// @see NYT::ICypressClient::Create
+ virtual ::NThreading::TFuture<TNodeId> Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options = TCreateOptions()) = 0;
+
+ ///
+ /// @brief Remove cypress node.
+ ///
+ /// @see NYT::ICypressClient::Remove
+ virtual ::NThreading::TFuture<void> Remove(
+ const TYPath& path,
+ const TRemoveOptions& options = TRemoveOptions()) = 0;
+
+ ///
+ /// @brief Check wether cypress node exists.
+ ///
+ /// @see NYT::ICypressClient::Exists
+ virtual ::NThreading::TFuture<bool> Exists(
+ const TYPath& path,
+ const TExistsOptions& options = TExistsOptions()) = 0;
+
+ ///
+ /// @brief Get cypress node.
+ ///
+ /// @see NYT::ICypressClient::Get
+ virtual ::NThreading::TFuture<TNode> Get(
+ const TYPath& path,
+ const TGetOptions& options = TGetOptions()) = 0;
+
+ ///
+ /// @brief Set cypress node.
+ ///
+ /// @see NYT::ICypressClient::Set
+ virtual ::NThreading::TFuture<void> Set(
+ const TYPath& path,
+ const TNode& node,
+ const TSetOptions& options = TSetOptions()) = 0;
+
+ ///
+ /// @brief List cypress directory.
+ ///
+ /// @see NYT::ICypressClient::List
+ virtual ::NThreading::TFuture<TNode::TListType> List(
+ const TYPath& path,
+ const TListOptions& options = TListOptions()) = 0;
+
+ ///
+ /// @brief Copy cypress node.
+ ///
+ /// @see NYT::ICypressClient::Copy
+ virtual ::NThreading::TFuture<TNodeId> Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options = TCopyOptions()) = 0;
+
+ ///
+ /// @brief Move cypress node.
+ ///
+ /// @see NYT::ICypressClient::Move
+ virtual ::NThreading::TFuture<TNodeId> Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options = TMoveOptions()) = 0;
+
+ ///
+ /// @brief Create symbolic link.
+ ///
+ /// @see NYT::ICypressClient::Link.
+ virtual ::NThreading::TFuture<TNodeId> Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options = TLinkOptions()) = 0;
+
+ ///
+ /// @brief Lock cypress node.
+ ///
+ /// @see NYT::ICypressClient::Lock
+ virtual ::NThreading::TFuture<ILockPtr> Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options = TLockOptions()) = 0;
+
+ ///
+ /// @brief Unlock cypress node.
+ ///
+ /// @see NYT::ICypressClient::Unlock
+ virtual ::NThreading::TFuture<void> Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options = TUnlockOptions()) = 0;
+
+ ///
+ /// @brief Abort operation.
+ ///
+ /// @see NYT::IClient::AbortOperation
+ virtual ::NThreading::TFuture<void> AbortOperation(const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Force complete operation.
+ ///
+ /// @see NYT::IClient::CompleteOperation
+ virtual ::NThreading::TFuture<void> CompleteOperation(const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Suspend operation.
+ ///
+ /// @see NYT::IClient::SuspendOperation
+ virtual ::NThreading::TFuture<void> SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options = TSuspendOperationOptions()) = 0;
+
+ ///
+ /// @brief Resume operation.
+ ///
+ /// @see NYT::IClient::ResumeOperation
+ virtual ::NThreading::TFuture<void> ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options = TResumeOperationOptions()) = 0;
+
+ ///
+ /// @brief Update parameters of running operation.
+ ///
+ /// @see NYT::IClient::UpdateOperationParameters
+ virtual ::NThreading::TFuture<void> UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options = TUpdateOperationParametersOptions()) = 0;
+
+ ///
+ /// @brief Canonize cypress path
+ ///
+ /// @see NYT::ICypressClient::CanonizeYPath
+ virtual ::NThreading::TFuture<TRichYPath> CanonizeYPath(const TRichYPath& path) = 0;
+
+ ///
+ /// @brief Get table columnar statistic
+ ///
+ /// @see NYT::ICypressClient::GetTableColumnarStatistics
+ virtual ::NThreading::TFuture<TVector<TTableColumnarStatistics>> GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options = {}) = 0;
+
+ ///
+ /// @brief Check permission for given path.
+ ///
+ /// @see NYT::IClient::CheckPermission
+ virtual ::NThreading::TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options = TCheckPermissionOptions()) = 0;
+};
+
+///
+/// @brief Batch request object.
+///
+/// Allows to send multiple lightweight requests at once significantly
+/// reducing time of their execution.
+///
+/// Methods of this class accept same arguments as @ref NYT::IClient methods but
+/// return TFuture that is set after execution of @ref NYT::IBatchRequest::ExecuteBatch
+///
+/// @see [Example of usage](https://a.yandex-team.ru/arc/trunk/arcadia/yt/cpp/mapreduce/examples/tutorial/batch_request/main.cpp)
+class IBatchRequest
+ : public IBatchRequestBase
+{
+public:
+ ///
+ /// @brief Temporary override current transaction.
+ ///
+ /// Using WithTransaction user can temporary override default transaction.
+ /// Example of usage:
+ /// TBatchRequest batchRequest;
+ /// auto noTxResult = batchRequest.Get("//some/path");
+ /// auto txResult = batchRequest.WithTransaction(tx).Get("//some/path");
+ virtual IBatchRequestBase& WithTransaction(const TTransactionId& transactionId) = 0;
+ IBatchRequestBase& WithTransaction(const ITransactionPtr& transaction);
+
+ ///
+ /// @brief Executes all subrequests of batch request.
+ ///
+ /// After execution of this method all TFuture objects returned by subrequests will
+ /// be filled with either result or error.
+ ///
+ /// @note It is undefined in which order these requests are executed.
+ ///
+ /// @note This method doesn't throw if subrequest emits error.
+ /// Instead corresponding future is set with exception.
+ /// So it is always important to check TFuture status.
+ ///
+ /// Single TBatchRequest instance may be executed only once
+ /// and cannot be modified (filled with additional requests) after execution.
+ /// Exception is thrown on attempt to modify executed batch request
+ /// or execute it again.
+ virtual void ExecuteBatch(const TExecuteBatchOptions& options = TExecuteBatchOptions()) = 0;
+};
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/client.cpp b/yt/cpp/mapreduce/interface/client.cpp
new file mode 100644
index 0000000000..11d308b809
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/client.cpp
@@ -0,0 +1,19 @@
+#include "client.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ILock::Wait(TDuration timeout)
+{
+ return GetAcquiredFuture().GetValue(timeout);
+}
+
+void ITransaction::Detach()
+{
+ Y_FAIL("ITransaction::Detach() is not implemented");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/client.h b/yt/cpp/mapreduce/interface/client.h
new file mode 100644
index 0000000000..54f37c3ae0
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/client.h
@@ -0,0 +1,568 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/client.h
+///
+/// Main header of the C++ YT Wrapper.
+
+///
+/// @mainpage C++ library for working with YT
+///
+/// This library provides possibilities to work with YT as a [MapReduce](https://en.wikipedia.org/wiki/MapReduce) system. It allows:
+/// - to read/write tables and files
+/// - to run operations
+/// - to work with transactions.
+///
+/// This library provides only basic functions for working with dynamic tables.
+/// To access full powers of YT dynamic tables one should use
+/// [yt/client](https://a.yandex-team.ru/arc/trunk/arcadia/yt/19_4/yt/client) library.
+///
+/// Entry points to this library:
+/// - @ref NYT::Initialize() initialization function for this library;
+/// - @ref NYT::IClient main interface to work with YT cluster;
+/// - @ref NYT::CreateClient() function that creates client for particular cluster;
+/// - @ref NYT::IOperationClient ancestor of @ref NYT::IClient containing the set of methods to run operations.
+///
+/// Tutorial on how to use this library can be found [here](https://yt.yandex-team.ru/docs/api/c++/examples).
+
+#include "fwd.h"
+
+#include "client_method_options.h"
+#include "constants.h"
+#include "batch_request.h"
+#include "cypress.h"
+#include "init.h"
+#include "io.h"
+#include "node.h"
+#include "operation.h"
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/maybe.h>
+#include <util/system/compiler.h>
+
+/// Main namespace of YT client
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// OAuth info (returned by @ref NYT::IClient::WhoAmI).
+struct TAuthorizationInfo
+{
+ /// User's login.
+ TString Login;
+
+ /// Realm.
+ TString Realm;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Part of @ref NYT::TCheckPermissionResponse.
+///
+/// In case when 'Action == ESecurityAction::Deny' because of a 'deny' rule,
+/// the "denying" object name and id and "denied" subject name an id may be returned.
+struct TCheckPermissionResult
+{
+ /// Was the access granted or not.
+ ESecurityAction Action;
+
+ /// Id of the object whose ACL's "deny" rule forbids the access.
+ TMaybe<TGUID> ObjectId;
+
+ ///
+ /// @brief Name of the object whose ACL's "deny" rule forbids the access.
+ ///
+ /// Example is "node //tmp/x/y".
+ TMaybe<TString> ObjectName;
+
+ /// Id of the subject for whom the access was denied by a "deny" rule.
+ TMaybe<TGUID> SubjectId;
+
+ /// Name of the subject for whom the access was denied by a "deny" rule.
+ TMaybe<TString> SubjectName;
+};
+
+/// @brief Result of @ref NYT::IClient::CheckPermission command.
+///
+/// The base part of the response corresponds to the check result for the node itself.
+/// `Columns` vector contains check results for the columns (in the same order as in the request).
+struct TCheckPermissionResponse
+ : public TCheckPermissionResult
+{
+ /// @brief Results for the table columns access permissions.
+ ///
+ /// @see [Columnar ACL doc](https://yt.yandex-team.ru/docs/description/common/columnar_acl)
+ TVector<TCheckPermissionResult> Columns;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Interface representing a lock obtained from @ref NYT::ITransaction::Lock.
+///
+/// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#start-tx)
+class ILock
+ : public TThrRefBase
+{
+public:
+ virtual ~ILock() = default;
+
+ /// Get cypress node id of lock itself.
+ virtual const TLockId& GetId() const = 0;
+
+ /// Get cypress node id of locked object.
+ virtual TNodeId GetLockedNodeId() const = 0;
+
+ ///
+ /// @brief Get future that will be set once lock is in "acquired" state.
+ ///
+ /// Note that future might contain exception if some error occurred
+ /// e.g. lock transaction was aborted.
+ virtual const ::NThreading::TFuture<void>& GetAcquiredFuture() const = 0;
+
+ ///
+ /// @brief Wait until lock is in "acquired" state.
+ ///
+ /// Throws exception if timeout exceeded or some error occurred
+ /// e.g. lock transaction was aborted.
+ void Wait(TDuration timeout = TDuration::Max());
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Base class for @ref NYT::IClient and @ref NYT::ITransaction.
+///
+/// This class contains transactional commands.
+class IClientBase
+ : public TThrRefBase
+ , public ICypressClient
+ , public IIOClient
+ , public IOperationClient
+{
+public:
+ ///
+ /// @brief Start a [transaction] (https://yt.yandex-team.ru/docs/description/storage/transactions.html#master_transactions).
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#start-tx)
+ [[nodiscard]] virtual ITransactionPtr StartTransaction(
+ const TStartTransactionOptions& options = TStartTransactionOptions()) = 0;
+
+ ///
+ /// @brief Change properties of table.
+ ///
+ /// Allows to:
+ /// - switch table between dynamic/static mode
+ /// - or change table schema
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#alter-table)
+ virtual void AlterTable(
+ const TYPath& path,
+ const TAlterTableOptions& options = TAlterTableOptions()) = 0;
+
+ ///
+ /// @brief Create batch request object that allows to execute several light requests in parallel.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#execute-batch)
+ virtual TBatchRequestPtr CreateBatchRequest() = 0;
+
+ /// @brief Get root client outside of all transactions.
+ virtual IClientPtr GetParentClient() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+/// @brief Interface representing a master transaction.
+///
+/// @see [YT doc](https://yt.yandex-team.ru/docs/description/storage/transactions.html#master_transactions)
+class ITransaction
+ : virtual public IClientBase
+{
+public:
+ /// Get id of transaction.
+ virtual const TTransactionId& GetId() const = 0;
+
+ ///
+ /// @brief Try to lock given path.
+ ///
+ /// Lock will be held until transaction is commited/aborted or @ref NYT::ITransaction::Unlock method is called.
+ /// Lock modes:
+ /// - `LM_EXCLUSIVE`: if exclusive lock is taken no other transaction can take exclusive or shared lock.
+ /// - `LM_SHARED`: if shared lock is taken other transactions can take shared lock but not exclusive.
+ /// - `LM_SNAPSHOT`: snapshot lock always succeeds, when snapshot lock is taken current transaction snapshots object.
+ /// It will not see changes that occurred to it in other transactions.
+ ///
+ /// Exclusive/shared lock can be waitable or not.
+ /// If nonwaitable lock cannot be taken exception is thrown.
+ /// If waitable lock cannot be taken it is created in pending state and client can wait until it actually taken.
+ /// Check @ref NYT::TLockOptions::Waitable and @ref NYT::ILock::GetAcquiredFuture for more details.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#lock)
+ virtual ILockPtr Lock(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options = TLockOptions()) = 0;
+
+ ///
+ /// @brief Remove all the locks (including pending ones) for this transaction from a Cypress node at `path`.
+ ///
+ /// If the locked version of the node differs from the original one,
+ /// an error will be thrown.
+ ///
+ /// Command is successful even if the node has no locks.
+ /// Only explicit (created by @ref NYT::ITransaction::Lock) locks are removed.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#unlock)
+ virtual void Unlock(
+ const TYPath& path,
+ const TUnlockOptions& options = TUnlockOptions()) = 0;
+
+ ///
+ /// @brief Commit transaction.
+ ///
+ /// All changes that are made by transactions become visible globally or to parent transaction.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#commit)
+ virtual void Commit() = 0;
+
+ ///
+ /// @brief Abort transaction.
+ ///
+ /// All changes made by current transaction are lost.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#abort)
+ virtual void Abort() = 0;
+
+ /// @brief Explicitly ping transaction.
+ ///
+ /// User usually does not need this method (as transactions are pinged automatically,
+ /// see @ref NYT::TStartTransactionOptions::AutoPingable).
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#ping)
+ virtual void Ping() = 0;
+
+ ///
+ /// @brief Detach transaction.
+ ///
+ /// Stop any activities connected with it: pinging, aborting on crashes etc.
+ /// Forget about the transaction totally.
+ virtual void Detach();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Interface containing non-transactional commands.
+class IClient
+ : virtual public IClientBase
+{
+public:
+ ///
+ /// @brief Attach to existing master transaction.
+ ///
+ /// Returned object WILL NOT:
+ /// - ping transaction automatically (unless @ref NYT::TAttachTransactionOptions::AutoPing is set)
+ /// - abort it on program termination (unless @ref NYT::TAttachTransactionOptions::AbortOnTermination is set).
+ /// Otherwise returned object is similar to the object returned by @ref NYT::IClientBase::StartTransaction.
+ /// and it can see all the changes made inside the transaction.
+ [[nodiscard]] virtual ITransactionPtr AttachTransaction(
+ const TTransactionId& transactionId,
+ const TAttachTransactionOptions& options = TAttachTransactionOptions()) = 0;
+
+ ///
+ /// @brief Mount dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#mount-table)
+ virtual void MountTable(
+ const TYPath& path,
+ const TMountTableOptions& options = TMountTableOptions()) = 0;
+
+ ///
+ /// @brief Unmount dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#unmount-table)
+ virtual void UnmountTable(
+ const TYPath& path,
+ const TUnmountTableOptions& options = TUnmountTableOptions()) = 0;
+
+ ///
+ /// @brief Remount dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#remount-table)
+ virtual void RemountTable(
+ const TYPath& path,
+ const TRemountTableOptions& options = TRemountTableOptions()) = 0;
+
+ ///
+ /// @brief Switch dynamic table from `mounted' into `frozen' state.
+ ///
+ /// When table is in frozen state all its data is flushed to disk and writes are disabled.
+ ///
+ /// @note this function launches the process of switching, but doesn't wait until switching is accomplished.
+ /// Waiting has to be performed by user.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#freeze-table)
+ virtual void FreezeTable(
+ const TYPath& path,
+ const TFreezeTableOptions& options = TFreezeTableOptions()) = 0;
+
+ ///
+ /// @brief Switch dynamic table from `frozen` into `mounted` state.
+ ///
+ /// @note this function launches the process of switching, but doesn't wait until switching is accomplished.
+ /// Waiting has to be performed by user.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#unfreeze-table)
+ virtual void UnfreezeTable(
+ const TYPath& path,
+ const TUnfreezeTableOptions& options = TUnfreezeTableOptions()) = 0;
+
+ ///
+ /// @brief Reshard dynamic table (break it into tablets) by given pivot keys.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#reshard-table)
+ virtual void ReshardTable(
+ const TYPath& path,
+ const TVector<TKey>& pivotKeys,
+ const TReshardTableOptions& options = TReshardTableOptions()) = 0;
+
+ ///
+ /// @brief Reshard dynamic table, breaking it into given number of tablets.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#reshard-table)
+ virtual void ReshardTable(
+ const TYPath& path,
+ i64 tabletCount,
+ const TReshardTableOptions& options = TReshardTableOptions()) = 0;
+
+ ///
+ /// @brief Insert rows into dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#insert-rows)
+ virtual void InsertRows(
+ const TYPath& path,
+ const TNode::TListType& rows,
+ const TInsertRowsOptions& options = TInsertRowsOptions()) = 0;
+
+ ///
+ /// @brief Delete rows from dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#delete-rows)
+ virtual void DeleteRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TDeleteRowsOptions& options = TDeleteRowsOptions()) = 0;
+
+ ///
+ /// @brief Trim rows from the beginning of ordered dynamic table.
+ ///
+ /// Asynchronously removes `rowCount` rows from the beginning of ordered dynamic table.
+ /// Numeration of remaining rows *does not change*, e.g. after `trim(10)` and `trim(20)`
+ /// you get in total `20` deleted rows.
+ ///
+ /// @param path Path to ordered dynamic table.
+ /// @param tabletIndex Which tablet to trim.
+ /// @param rowCount How many trimmed rows will be in the table after command.
+ /// @param options Optional parameters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#trim-rows)
+ virtual void TrimRows(
+ const TYPath& path,
+ i64 tabletIndex,
+ i64 rowCount,
+ const TTrimRowsOptions& options = TTrimRowsOptions()) = 0;
+
+ ///
+ /// @brief Lookup rows with given keys from dynamic table.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#lookup-rows)
+ virtual TNode::TListType LookupRows(
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TLookupRowsOptions& options = TLookupRowsOptions()) = 0;
+
+ ///
+ /// @brief Select rows from dynamic table, using [SQL dialect](https://yt.yandex-team.ru/docs//description/dynamic_tables/dyn_query_language.html).
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#select-rows)
+ virtual TNode::TListType SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options = TSelectRowsOptions()) = 0;
+
+ ///
+ /// @brief Change properties of table replica.
+ ///
+ /// Allows to enable/disable replica and/or change its mode.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#alter-table-replica)
+ virtual void AlterTableReplica(
+ const TReplicaId& replicaId,
+ const TAlterTableReplicaOptions& alterTableReplicaOptions) = 0;
+
+ ///
+ /// @brief Generate a monotonously increasing master timestamp.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#generate-timestamp)
+ virtual ui64 GenerateTimestamp() = 0;
+
+ /// Return YT username of current client.
+ virtual TAuthorizationInfo WhoAmI() = 0;
+
+ ///
+ /// @brief Get operation attributes.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-operation)
+ virtual TOperationAttributes GetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options = TGetOperationOptions()) = 0;
+
+ ///
+ /// @brief List operations satisfying given filters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#list-operations)
+ virtual TListOperationsResult ListOperations(
+ const TListOperationsOptions& options = TListOperationsOptions()) = 0;
+
+ ///
+ /// @brief Update operation runtime parameters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#update-op-parameters)
+ virtual void UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options) = 0;
+
+ ///
+ /// @brief Get job attributes.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-job)
+ virtual TJobAttributes GetJob(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options = TGetJobOptions()) = 0;
+
+ ///
+ /// List attributes of jobs satisfying given filters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#list-jobs)
+ virtual TListJobsResult ListJobs(
+ const TOperationId& operationId,
+ const TListJobsOptions& options = TListJobsOptions()) = 0;
+
+ ///
+ /// @brief Get the input of a running or failed job.
+ ///
+ /// @ref NYT::TErrorResponse exception is thrown if job is missing.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-job-input)
+ virtual IFileReaderPtr GetJobInput(
+ const TJobId& jobId,
+ const TGetJobInputOptions& options = TGetJobInputOptions()) = 0;
+
+ ///
+ /// @brief Get fail context of a failed job.
+ ///
+ /// @ref NYT::TErrorResponse exception is thrown if it is missing.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-job-fail-context)
+ virtual IFileReaderPtr GetJobFailContext(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobFailContextOptions& options = TGetJobFailContextOptions()) = 0;
+
+ ///
+ /// @brief Get stderr of a running or failed job.
+ ///
+ /// @ref NYT::TErrorResponse exception is thrown if it is missing.
+ ///
+ /// @note YT doesn't store all job stderrs
+ ///
+ /// @note If job stderr exceeds few megabytes YT will store only head and tail of stderr.
+ ///
+ /// @see Description of `max_stderr_size` spec option [here](https://yt.yandex-team.ru/docs//description/mr/operations_options.html).
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-job-stderr)
+ virtual IFileReaderPtr GetJobStderr(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& options = TGetJobStderrOptions()) = 0;
+
+ ///
+ /// @brief Create one or several rbtorrents for files in a blob table.
+ ///
+ /// If specified, one torrent is created for each value of `KeyColumns` option.
+ /// Otherwise, a single torrent with all files of a table is created.
+ ///
+ /// @return list of nodes, each node has two fields
+ /// * `key`: list of key columns values. Empty if `KeyColumns` is not specified.
+ /// * `rbtorrent`: rbtorrent string (with `rbtorrent:` prefix)
+ ///
+ /// @see [More info.](https://docs.yandex-team.ru/docs/yt/description/storage/blobtables#sky_share)
+ virtual TNode::TListType SkyShareTable(
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options) = 0;
+
+ ///
+ /// @brief Check if `user` has `permission` to access a Cypress node at `path`.
+ ///
+ /// For tables access to columns specified in `options.Columns_` can be checked
+ /// (@see [the doc](https://yt.yandex-team.ru/docs/description/common/columnar_acl)).
+ ///
+ /// If access is denied (the returned result has `.Action == ESecurityAction::Deny`)
+ /// because of a `deny` rule, the "denying" object name and id
+ /// and "denied" subject name an id may be returned.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#check_permission)
+ virtual TCheckPermissionResponse CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options = TCheckPermissionOptions()) = 0;
+
+ /// @brief Get information about tablet
+ /// @see NYT::TTabletInfo
+ virtual TVector<TTabletInfo> GetTabletInfos(
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options = TGetTabletInfosOptions()) = 0;
+
+ ///
+ /// @brief Suspend operation.
+ ///
+ /// Jobs will be aborted.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#suspend_op)
+ virtual void SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options = TSuspendOperationOptions()) = 0;
+
+ /// @brief Resume previously suspended operation.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#resume_op)
+ virtual void ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options = TResumeOperationOptions()) = 0;
+
+ ///
+ /// @brief Synchronously terminates all client's background activities
+ ///
+ /// e.g. no callbacks will be executed after the function is completed
+ ///
+ /// @note It is safe to call Shutdown multiple times
+ ///
+ /// @note @ref NYT::TApiUsageError will be thrown if any client's method is called after shutdown
+ ///
+ virtual void Shutdown() = 0;
+};
+
+
+/// Create a client for particular MapReduce cluster.
+IClientPtr CreateClient(
+ const TString& serverName,
+ const TCreateClientOptions& options = TCreateClientOptions());
+
+
+/// Create a client for mapreduce cluster specified in `YT_PROXY` environment variable.
+IClientPtr CreateClientFromEnv(
+ const TCreateClientOptions& options = TCreateClientOptions());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/client_method_options.cpp b/yt/cpp/mapreduce/interface/client_method_options.cpp
new file mode 100644
index 0000000000..66f72bfe5f
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/client_method_options.cpp
@@ -0,0 +1,34 @@
+#include "client_method_options.h"
+
+#include "tvm.h"
+
+namespace NYT {
+
+template <typename T>
+static void MergeMaybe(TMaybe<T>& origin, const TMaybe<T>& patch)
+{
+ if (patch) {
+ origin = patch;
+ }
+}
+
+void TFormatHints::Merge(const TFormatHints& patch)
+{
+ if (patch.SkipNullValuesForTNode_) {
+ SkipNullValuesForTNode(true);
+ }
+ MergeMaybe(EnableStringToAllConversion_, patch.EnableStringToAllConversion_);
+ MergeMaybe(EnableAllToStringConversion_, patch.EnableAllToStringConversion_);
+ MergeMaybe(EnableIntegralTypeConversion_, patch.EnableIntegralTypeConversion_);
+ MergeMaybe(EnableIntegralToDoubleConversion_, patch.EnableIntegralToDoubleConversion_);
+ MergeMaybe(EnableTypeConversion_, patch.EnableTypeConversion_);
+ MergeMaybe(ComplexTypeMode_, patch.ComplexTypeMode_);
+}
+
+TCreateClientOptions& TCreateClientOptions::ServiceTicketAuth(const NAuth::IServiceTicketAuthPtrWrapper& wrapper)
+{
+ ServiceTicketAuth_ = std::make_shared<NAuth::IServiceTicketAuthPtrWrapper>(wrapper);
+ return *this;
+}
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/client_method_options.h b/yt/cpp/mapreduce/interface/client_method_options.h
new file mode 100644
index 0000000000..8074632353
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/client_method_options.h
@@ -0,0 +1,1452 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/client_method_options.h
+///
+/// Header containing options for @ref NYT::IClient methods.
+
+#include "common.h"
+#include "config.h"
+#include "format.h"
+#include "public.h"
+#include "retry_policy.h"
+
+#include <util/datetime/base.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Type of the cypress node.
+enum ENodeType : int
+{
+ NT_STRING /* "string_node" */,
+ NT_INT64 /* "int64_node" */,
+ NT_UINT64 /* "uint64_node" */,
+ NT_DOUBLE /* "double_node" */,
+ NT_BOOLEAN /* "boolean_node" */,
+ NT_MAP /* "map_node" */,
+ NT_LIST /* "list_node" */,
+ NT_FILE /* "file" */,
+ NT_TABLE /* "table" */,
+ NT_DOCUMENT /* "document" */,
+ NT_REPLICATED_TABLE /* "replicated_table" */,
+ NT_TABLE_REPLICA /* "table_replica" */,
+ NT_USER /* "user" */,
+ NT_SCHEDULER_POOL /* "scheduler_pool" */,
+ NT_LINK /* "link" */,
+};
+
+///
+/// @brief Mode of composite type representation in yson.
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/data_types#yson
+enum class EComplexTypeMode : int
+{
+ Named /* "named" */,
+ Positional /* "positional" */,
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Create
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#create
+struct TCreateOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TCreateOptions;
+ /// @endcond
+
+ /// Create missing parent directories if required.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ ///
+ /// @brief Do not raise error if node already exists.
+ ///
+ /// Node is not recreated.
+ /// Force and IgnoreExisting MUST NOT be used simultaneously.
+ FLUENT_FIELD_DEFAULT(bool, IgnoreExisting, false);
+
+ ///
+ /// @brief Recreate node if it exists.
+ ///
+ /// Force and IgnoreExisting MUST NOT be used simultaneously.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+
+ /// @brief Set node attributes.
+ FLUENT_FIELD_OPTION(TNode, Attributes);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Remove
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#remove
+struct TRemoveOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TRemoveOptions;
+ /// @endcond
+
+ ///
+ /// @brief Remove whole tree when removing composite cypress node (e.g. `map_node`).
+ ///
+ /// Without this option removing nonempty composite node will fail.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ /// @brief Do not fail if removing node doesn't exist.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+};
+
+/// Base class for options for operations that read from master.
+template <typename TDerived>
+struct TMasterReadOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Where to read from.
+ FLUENT_FIELD_OPTION(EMasterReadKind, ReadFrom);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Exists
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#exists
+struct TExistsOptions
+ : public TMasterReadOptions<TExistsOptions>
+{
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Get
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#get
+struct TGetOptions
+ : public TMasterReadOptions<TGetOptions>
+{
+ /// @brief Attributes that should be fetched with each node.
+ FLUENT_FIELD_OPTION(TAttributeFilter, AttributeFilter);
+
+ /// @brief Limit for the number of children node.
+ FLUENT_FIELD_OPTION(i64, MaxSize);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Set
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#set
+struct TSetOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSetOptions;
+ /// @endcond
+
+ /// Create missing parent directories if required.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ /// Allow setting any nodes, not only attribute and document ones.
+ FLUENT_FIELD_OPTION(bool, Force);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::MultisetAttributes
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#multiset_attributes
+struct TMultisetAttributesOptions
+{ };
+
+///
+/// @brief Options for @ref NYT::ICypressClient::List
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#list
+struct TListOptions
+ : public TMasterReadOptions<TListOptions>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TListOptions;
+ /// @endcond
+
+ /// Attributes that should be fetched for each node.
+ FLUENT_FIELD_OPTION(TAttributeFilter, AttributeFilter);
+
+ /// Limit for the number of children that will be fetched.
+ FLUENT_FIELD_OPTION(i64, MaxSize);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Copy
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#copy
+struct TCopyOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TCopyOptions;
+ /// @endcond
+
+ /// Create missing directories in destination path if required.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ /// Allows to use existing node as destination, it will be overwritten.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+
+ /// Whether to preserves account of source node.
+ FLUENT_FIELD_DEFAULT(bool, PreserveAccount, false);
+
+ /// Whether to preserve `expiration_time` attribute of source node.
+ FLUENT_FIELD_OPTION(bool, PreserveExpirationTime);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Move
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#move
+struct TMoveOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TMoveOptions;
+ /// @endcond
+
+ /// Create missing directories in destination path if required.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ /// Allows to use existing node as destination, it will be overwritten.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+
+ /// Whether to preserves account of source node.
+ FLUENT_FIELD_DEFAULT(bool, PreserveAccount, false);
+
+ /// Whether to preserve `expiration_time` attribute of source node.
+ FLUENT_FIELD_OPTION(bool, PreserveExpirationTime);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Link
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#link
+struct TLinkOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TLinkOptions;
+ /// @endcond
+
+ /// Create parent directories of destination if they don't exist.
+ FLUENT_FIELD_DEFAULT(bool, Recursive, false);
+
+ /// Do not raise error if link already exists.
+ FLUENT_FIELD_DEFAULT(bool, IgnoreExisting, false);
+
+ /// Force rewrite target node.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+
+ /// Attributes of created link.
+ FLUENT_FIELD_OPTION(TNode, Attributes);
+};
+
+///
+/// @brief Options for @ref NYT::ICypressClient::Concatenate
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#concatenate
+struct TConcatenateOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TConcatenateOptions;
+ /// @endcond
+
+ /// Whether we should append to destination or rewrite it.
+ FLUENT_FIELD_OPTION(bool, Append);
+};
+
+///
+/// @brief Options for @ref NYT::IIOClient::CreateBlobTableReader
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#read_blob_table
+struct TBlobTableReaderOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TBlobTableReaderOptions;
+ /// @endcond
+
+ /// Name of the part index column. By default it is "part_index".
+ FLUENT_FIELD_OPTION(TString, PartIndexColumnName);
+
+ /// Name of the data column. By default it is "data".
+ FLUENT_FIELD_OPTION(TString, DataColumnName);
+
+ ///
+ /// @brief Size of each part.
+ ///
+ /// All blob parts except the last part of the blob must be of this size
+ /// otherwise blob table reader emits error.
+ FLUENT_FIELD_DEFAULT(ui64, PartSize, 4 * 1024 * 1024);
+
+ /// @brief Offset from which to start reading
+ FLUENT_FIELD_DEFAULT(i64, Offset, 0);
+};
+
+///
+/// @brief Resource limits for operation (or pool)
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/scheduler/scheduler_and_pools#resursy
+/// @see NYT::TUpdateOperationParametersOptions
+struct TResourceLimits
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TResourceLimits;
+ /// @endcond
+
+ /// Number of slots for user jobs.
+ FLUENT_FIELD_OPTION(i64, UserSlots);
+
+ /// Number of cpu cores.
+ FLUENT_FIELD_OPTION(double, Cpu);
+
+ /// Network usage. Doesn't have precise physical unit.
+ FLUENT_FIELD_OPTION(i64, Network);
+
+ /// Memory in bytes.
+ FLUENT_FIELD_OPTION(i64, Memory);
+};
+
+///
+/// @brief Scheduling options for single pool tree.
+///
+/// @see NYT::TUpdateOperationParametersOptions
+struct TSchedulingOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSchedulingOptions;
+ /// @endcond
+
+ ///
+ /// @brief Pool to switch operation to.
+ ///
+ /// @note Switching is currently disabled on the server (will induce an exception).
+ FLUENT_FIELD_OPTION(TString, Pool);
+
+ /// @brief Operation weight.
+ FLUENT_FIELD_OPTION(double, Weight);
+
+ /// @brief Operation resource limits.
+ FLUENT_FIELD_OPTION(TResourceLimits, ResourceLimits);
+};
+
+///
+/// @brief Collection of scheduling options for multiple pool trees.
+///
+/// @see NYT::TUpdateOperationParametersOptions
+struct TSchedulingOptionsPerPoolTree
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSchedulingOptionsPerPoolTree;
+ /// @endcond
+
+ TSchedulingOptionsPerPoolTree(const THashMap<TString, TSchedulingOptions>& options = {})
+ : Options_(options)
+ { }
+
+ /// Add scheduling options for pool tree.
+ TSelf& Add(TStringBuf poolTreeName, const TSchedulingOptions& schedulingOptions)
+ {
+ Y_ENSURE(Options_.emplace(poolTreeName, schedulingOptions).second);
+ return *this;
+ }
+
+ THashMap<TString, TSchedulingOptions> Options_;
+};
+
+///
+/// @brief Options for @ref NYT::IOperation::SuspendOperation
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#suspend_op
+struct TSuspendOperationOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSuspendOperationOptions;
+ /// @endcond
+
+ ///
+ /// @brief Whether to abort already running jobs.
+ ///
+ /// By default running jobs are not aborted.
+ FLUENT_FIELD_OPTION(bool, AbortRunningJobs);
+};
+
+///
+/// @brief Options for @ref NYT::IOperation::ResumeOperation
+///
+/// @note They are empty for now but options might appear in the future.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#resume_op
+struct TResumeOperationOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TResumeOperationOptions;
+ /// @endcond
+};
+
+///
+/// @brief Options for @ref NYT::IOperation::UpdateParameters
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#update_op_parameters
+struct TUpdateOperationParametersOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TUpdateOperationParametersOptions;
+ /// @endcond
+
+ /// New owners of the operation.
+ FLUENT_VECTOR_FIELD(TString, Owner);
+
+ /// Pool to switch operation to (for all pool trees it is running in).
+ FLUENT_FIELD_OPTION(TString, Pool);
+
+ /// New operation weight (for all pool trees it is running in).
+ FLUENT_FIELD_OPTION(double, Weight);
+
+ /// Scheduling options for each pool tree the operation is running in.
+ FLUENT_FIELD_OPTION(TSchedulingOptionsPerPoolTree, SchedulingOptionsPerPoolTree);
+};
+
+///
+/// @brief Base class for many options related to IO.
+///
+/// @ref NYT::TFileWriterOptions
+/// @ref NYT::TFileReaderOptions
+/// @ref NYT::TTableReaderOptions
+/// @ref NYT::TTableWriterOptions
+template <class TDerived>
+struct TIOOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Advanced options for reader/writer.
+ ///
+ /// Readers/writers have many options not of all of them are supported by library.
+ /// If you need such unsupported option, you might use `Config` option until
+ /// option is supported.
+ ///
+ /// Example:
+ ///
+ /// TTableWriterOptions().Config(TNode()("max_row_weight", 64 << 20)))
+ ///
+ /// @note We encourage you to ask yt@ to add native C++ support of required options
+ /// and use `Config` only as temporary solution while native support is not ready.
+ FLUENT_FIELD_OPTION(TNode, Config);
+
+ ///
+ /// @brief Whether to create internal client transaction for reading / writing table.
+ ///
+ /// This is advanced option.
+ ///
+ /// If `CreateTransaction` is set to `false` reader/writer doesn't create internal transaction
+ /// and doesn't lock table. This option is overriden (effectively `false`) for writers by
+ /// @ref NYT::TTableWriterOptions::SingleHttpRequest
+ ///
+ /// WARNING: if `CreateTransaction` is `false`, read/write might become non-atomic.
+ /// Change ONLY if you are sure what you are doing!
+ FLUENT_FIELD_DEFAULT(bool, CreateTransaction, true);
+};
+
+/// @brief Options for reading file from YT.
+struct TFileReaderOptions
+ : public TIOOptions<TFileReaderOptions>
+{
+ ///
+ /// @brief Offset to start reading from.
+ ///
+ /// By default reading is started from the beginning of the file.
+ FLUENT_FIELD_OPTION(i64, Offset);
+
+ ///
+ /// @brief Maximum length to read.
+ ///
+ /// By default file is read until the end.
+ FLUENT_FIELD_OPTION(i64, Length);
+};
+
+/// @brief Options that control how server side of YT stores data.
+struct TWriterOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TWriterOptions;
+ /// @endcond
+
+ ///
+ /// @brief Whether to wait all replicas to be written.
+ ///
+ /// When set to true upload will be considered successful as soon as
+ /// @ref NYT::TWriterOptions::MinUploadReplicationFactor number of replicas are created.
+ FLUENT_FIELD_OPTION(bool, EnableEarlyFinish);
+
+ /// Number of replicas to be created.
+ FLUENT_FIELD_OPTION(ui64, UploadReplicationFactor);
+
+ ///
+ /// Min number of created replicas needed to consider upload successful.
+ ///
+ /// @see NYT::TWriterOptions::EnableEarlyFinish
+ FLUENT_FIELD_OPTION(ui64, MinUploadReplicationFactor);
+
+ ///
+ /// @brief Desired size of a chunk.
+ ///
+ /// @see @ref NYT::TWriterOptions::RetryBlockSize
+ FLUENT_FIELD_OPTION(ui64, DesiredChunkSize);
+
+ ///
+ /// @brief Size of data block accumulated in memory to provide retries.
+ ///
+ /// Data is accumulated in memory buffer so in case error occurs data could be resended.
+ ///
+ /// If `RetryBlockSize` is not set buffer size is set to `DesiredChunkSize`.
+ /// If niether `RetryBlockSize` nor `DesiredChunkSize` is set size of buffer is 64MB.
+ ///
+ /// @note Written chunks cannot be larger than size of this memory buffer.
+ ///
+ /// Since DesiredChunkSize is compared against data already compressed with compression codec
+ /// it makes sense to set `RetryBlockSize = DesiredChunkSize / ExpectedCompressionRatio`
+ ///
+ /// @see @ref NYT::TWriterOptions::DesiredChunkSize
+ /// @see @ref NYT::TTableWriterOptions::SingleHttpRequest
+ FLUENT_FIELD_OPTION(size_t, RetryBlockSize);
+};
+
+///
+/// @brief Options for writing file
+///
+/// @see NYT::IIOClient::CreateFileWriter
+struct TFileWriterOptions
+ : public TIOOptions<TFileWriterOptions>
+{
+ ///
+ /// @brief Whether to compute MD5 sum of written file.
+ ///
+ /// If ComputeMD5 is set to `true` and we are appending to an existing file
+ /// the `md5` attribute must be set (i.e. it was previously written only with `ComputeMD5 == true`).
+ FLUENT_FIELD_OPTION(bool, ComputeMD5);
+
+ ///
+ /// @brief Options to control how YT server side writes data.
+ ///
+ /// @see NYT::TWriterOptions
+ FLUENT_FIELD_OPTION(TWriterOptions, WriterOptions);
+};
+
+class TSkiffRowHints {
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TSkiffRowHints;
+ /// @endcond
+
+ ///
+ /// @brief Library doesn't interpret it, only pass it to CreateSkiffParser<...>() and GetSkiffSchema<...>() functions.
+ ///
+ /// You can set something in it to pass necessary information to CreateSkiffParser<...>() and GetSkiffSchema<...>() functions.
+ FLUENT_FIELD_OPTION(TNode, Attributes);
+};
+
+/// Options that control how C++ objects represent table rows when reading or writing a table.
+class TFormatHints
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TFormatHints;
+ /// @endcond
+
+ ///
+ /// @brief Whether to skip null values.
+ ///
+ /// When set to true TNode doesn't contain null column values
+ /// (e.g. corresponding keys will be missing instead of containing null value).
+ ///
+ /// Only meaningful for TNode representation.
+ ///
+ /// Useful for sparse tables which have many columns in schema
+ /// but only few columns are set in any row.
+ FLUENT_FIELD_DEFAULT(bool, SkipNullValuesForTNode, false);
+
+ ///
+ /// @brief Whether to convert string to numeric and boolean types (e.g. "42u" -> 42u, "false" -> %false)
+ /// when writing to schemaful table.
+ FLUENT_FIELD_OPTION(bool, EnableStringToAllConversion);
+
+ ///
+ /// @brief Whether to convert numeric and boolean types to string (e.g., 3.14 -> "3.14", %true -> "true")
+ /// when writing to schemaful table.
+ FLUENT_FIELD_OPTION(bool, EnableAllToStringConversion);
+
+ ///
+ /// @brief Whether to convert uint64 <-> int64 when writing to schemaful table.
+ ///
+ /// On overflow the corresponding error with be raised.
+ ///
+ /// This options is enabled by default.
+ FLUENT_FIELD_OPTION(bool, EnableIntegralTypeConversion);
+
+ /// Whether to convert uint64 and int64 to double (e.g. 42 -> 42.0) when writing to schemaful table.
+ FLUENT_FIELD_OPTION(bool, EnableIntegralToDoubleConversion);
+
+ /// Shortcut for enabling all type conversions.
+ FLUENT_FIELD_OPTION(bool, EnableTypeConversion);
+
+ ///
+ /// @brief Controls how complex types are represented in TNode or yson-strings.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/data_types#yson
+ FLUENT_FIELD_OPTION(EComplexTypeMode, ComplexTypeMode);
+
+ ///
+ /// @brief Allow to use any meta-information for creating skiff schema and parser for reading ISkiffRow.
+ FLUENT_FIELD_OPTION(TSkiffRowHints, SkiffRowHints);
+
+ ///
+ /// @brief Apply the patch to the fields.
+ ///
+ /// Non-default and non-empty values replace the default and empty ones.
+ void Merge(const TFormatHints& patch);
+};
+
+/// Options that control which control attributes (like row_index) are added to rows during read.
+class TControlAttributes
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TControlAttributes;
+ /// @endcond
+
+ ///
+ /// @brief Whether to add "row_index" attribute to rows read.
+ FLUENT_FIELD_DEFAULT(bool, EnableRowIndex, true);
+
+ ///
+ /// @brief Whether to add "range_index" attribute to rows read.
+ FLUENT_FIELD_DEFAULT(bool, EnableRangeIndex, true);
+};
+
+/// Options for @ref NYT::IClient::CreateTableReader
+struct TTableReaderOptions
+ : public TIOOptions<TTableReaderOptions>
+{
+ /// @deprecated Size of internal client buffer.
+ FLUENT_FIELD_DEFAULT(size_t, SizeLimit, 4 << 20);
+
+ ///
+ /// @brief Allows to fine tune format that is used for reading tables.
+ ///
+ /// Has no effect when used with raw-reader.
+ FLUENT_FIELD_OPTION(TFormatHints, FormatHints);
+
+ ///
+ /// @brief Allows to tune which attributes are added to rows while reading tables.
+ ///
+ FLUENT_FIELD_DEFAULT(TControlAttributes, ControlAttributes, TControlAttributes());
+};
+
+/// Options for @ref NYT::IClient::CreateTableWriter
+struct TTableWriterOptions
+ : public TIOOptions<TTableWriterOptions>
+{
+ ///
+ /// @brief Enable or disable retryful writing.
+ ///
+ /// If set to true no retry is made but we also make less requests to master.
+ /// If set to false writer can make up to `TConfig::RetryCount` attempts to send each block of data.
+ ///
+ /// @note Writers' methods might throw strange exceptions that might look like network error
+ /// when `SingleHttpRequest == true` and YT node encounters an error
+ /// (due to limitations of HTTP protocol YT node have no chance to report error
+ /// before it reads the whole input so it just drops the connection).
+ FLUENT_FIELD_DEFAULT(bool, SingleHttpRequest, false);
+
+ ///
+ /// @brief Allows to change the size of locally buffered rows before flushing to yt.
+ ///
+ /// Used only with @ref NYT::TTableWriterOptions::SingleHttpRequest
+ FLUENT_FIELD_DEFAULT(size_t, BufferSize, 64 << 20);
+
+ ///
+ /// @brief Allows to fine tune format that is used for writing tables.
+ ///
+ /// Has no effect when used with raw-writer.
+ FLUENT_FIELD_OPTION(TFormatHints, FormatHints);
+
+ /// @brief Try to infer schema of inexistent table from the type of written rows.
+ ///
+ /// @note Default values for this option may differ depending on the row type.
+ /// For protobuf it's currently false by default.
+ FLUENT_FIELD_OPTION(bool, InferSchema);
+
+ ///
+ /// @brief Options to control how YT server side writes data.
+ ///
+ /// @see NYT::TWriterOptions
+ FLUENT_FIELD_OPTION(TWriterOptions, WriterOptions);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::StartTransaction
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#start_tx
+struct TStartTransactionOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TStartTransactionOptions;
+ /// @endcond
+
+ FLUENT_FIELD_DEFAULT(bool, PingAncestors, false);
+
+ ///
+ /// @brief How long transaction lives after last ping.
+ ///
+ /// If server doesn't receive any pings for transaction for this time
+ /// transaction will be aborted. By default timeout is 15 seconds.
+ FLUENT_FIELD_OPTION(TDuration, Timeout);
+
+ ///
+ /// @brief Moment in the future when transaction is aborted.
+ FLUENT_FIELD_OPTION(TInstant, Deadline);
+
+ ///
+ /// @brief Whether to ping created transaction automatically.
+ ///
+ /// When set to true library creates a thread that pings transaction.
+ /// When set to false library doesn't ping transaction and it's user responsibility to ping it.
+ FLUENT_FIELD_DEFAULT(bool, AutoPingable, true);
+
+ ///
+ /// @brief Set the title attribute of transaction.
+ ///
+ /// If title was not specified
+ /// neither using this option nor using @ref NYT::TStartTransactionOptions::Attributes option
+ /// library will generate default title for transaction.
+ /// Such default title includes machine name, pid, user name and some other useful info.
+ FLUENT_FIELD_OPTION(TString, Title);
+
+ ///
+ /// @brief Set custom transaction attributes
+ ///
+ /// @note @ref NYT::TStartTransactionOptions::Title option overrides `"title"` attribute.
+ FLUENT_FIELD_OPTION(TNode, Attributes);
+};
+
+///
+/// @brief Options for attaching transaction.
+///
+/// @see NYT::IClient::AttachTransaction
+struct TAttachTransactionOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TAttachTransactionOptions;
+ /// @endcond
+
+ ///
+ /// @brief Ping transaction automatically.
+ ///
+ /// When set to |true| library creates a thread that pings transaction.
+ /// When set to |false| library doesn't ping transaction and
+ /// it's user responsibility to ping it.
+ FLUENT_FIELD_DEFAULT(bool, AutoPingable, false);
+
+ ///
+ /// @brief Abort transaction on program termination.
+ ///
+ /// Should the transaction be aborted on program termination
+ /// (either normal or by a signal or uncaught exception -- two latter
+ /// only if @ref TInitializeOptions::CleanupOnTermination is set).
+ FLUENT_FIELD_DEFAULT(bool, AbortOnTermination, false);
+};
+
+///
+/// @brief Type of the lock.
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locking_mode
+/// @see NYT::ITransaction::Lock
+enum ELockMode : int
+{
+ /// Exclusive lock.
+ LM_EXCLUSIVE /* "exclusive" */,
+
+ /// Shared lock.
+ LM_SHARED /* "shared" */,
+
+ /// Snapshot lock.
+ LM_SNAPSHOT /* "snapshot" */,
+};
+
+///
+/// @brief Options for locking cypress node
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locks
+/// @see NYT::ITransaction::Lock
+struct TLockOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TLockOptions;
+ /// @endcond
+
+ ///
+ /// @brief Whether to wait already locked node to be unlocked.
+ ///
+ /// If `Waitable' is set to true Lock method will create
+ /// waitable lock, that will be taken once other transactions
+ /// that hold lock to that node are commited / aborted.
+ ///
+ /// @note Lock method DOES NOT wait until lock is actually acquired.
+ /// Waiting should be done using corresponding methods of ILock.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locking_queue
+ FLUENT_FIELD_DEFAULT(bool, Waitable, false);
+
+ ///
+ /// @brief Also take attribute_key lock.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locks_compatibility
+ FLUENT_FIELD_OPTION(TString, AttributeKey);
+
+ ///
+ /// @brief Also take child_key lock.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locks_compatibility
+ FLUENT_FIELD_OPTION(TString, ChildKey);
+};
+
+///
+/// @brief Options for @ref NYT::ITransaction::Unlock
+///
+/// @note They are empty for now but options might appear in the future.
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/transactions#locks_compatibility
+struct TUnlockOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TUnlockOptions;
+ /// @endcond
+};
+
+/// Base class for options that deal with tablets.
+template <class TDerived>
+struct TTabletOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// Index of a first tablet to deal with.
+ FLUENT_FIELD_OPTION(i64, FirstTabletIndex);
+
+ /// Index of a last tablet to deal with.
+ FLUENT_FIELD_OPTION(i64, LastTabletIndex);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::MountTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#mount_table
+struct TMountTableOptions
+ : public TTabletOptions<TMountTableOptions>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TMountTableOptions;
+ /// @endcond
+
+ /// If specified table will be mounted to this cell.
+ FLUENT_FIELD_OPTION(TTabletCellId, CellId);
+
+ /// If set to true tablets will be mounted in freezed state.
+ FLUENT_FIELD_DEFAULT(bool, Freeze, false);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::UnmountTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#unmount_table
+struct TUnmountTableOptions
+ : public TTabletOptions<TUnmountTableOptions>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TUnmountTableOptions;
+ /// @endcond
+
+ /// Advanced option, don't use unless yt team told you so.
+ FLUENT_FIELD_DEFAULT(bool, Force, false);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::RemountTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#remount_table
+struct TRemountTableOptions
+ : public TTabletOptions<TRemountTableOptions>
+{ };
+
+///
+/// @brief Options for @ref NYT::IClient::ReshardTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#reshard_table
+struct TReshardTableOptions
+ : public TTabletOptions<TReshardTableOptions>
+{ };
+
+///
+/// @brief Options for @ref NYT::IClient::FreezeTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#freeze_table
+struct TFreezeTableOptions
+ : public TTabletOptions<TFreezeTableOptions>
+{ };
+
+///
+/// @brief Options for @ref NYT::IClient::UnfreezeTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#unfreeze_table
+struct TUnfreezeTableOptions
+ : public TTabletOptions<TUnfreezeTableOptions>
+{ };
+
+///
+/// @brief Options for @ref NYT::IClient::AlterTable
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#alter_table
+struct TAlterTableOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TAlterTableOptions;
+ /// @endcond
+
+ /// Change table schema.
+ FLUENT_FIELD_OPTION(TTableSchema, Schema);
+
+ /// Alter table between static and dynamic mode.
+ FLUENT_FIELD_OPTION(bool, Dynamic);
+
+ ///
+ /// @brief Changes id of upstream replica on metacluster.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/replicated_dynamic_tables
+ FLUENT_FIELD_OPTION(TReplicaId, UpstreamReplicaId);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::LookupRows
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#lookup_rows
+struct TLookupRowsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TLookupRowsOptions;
+ /// @endcond
+
+ /// Timeout for operation.
+ FLUENT_FIELD_OPTION(TDuration, Timeout);
+
+ /// Column names to return.
+ FLUENT_FIELD_OPTION(TColumnNames, Columns);
+
+ ///
+ /// @brief Whether to return rows that were not found in table.
+ ///
+ /// If set to true List returned by LookupRows method will have same
+ /// length as list of keys. If row is not found in table corresponding item in list
+ /// will have null value.
+ FLUENT_FIELD_DEFAULT(bool, KeepMissingRows, false);
+
+ /// If set to true returned values will have "timestamp" attribute.
+ FLUENT_FIELD_OPTION(bool, Versioned);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::SelectRows
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#select_rows
+struct TSelectRowsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSelectRowsOptions;
+ /// @endcond
+
+ /// Timeout for operation.
+ FLUENT_FIELD_OPTION(TDuration, Timeout);
+
+ ///
+ /// @brief Limitation for number of rows read by single node.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/dyn_query_language#ogranicheniya-na-slozhnost-zaprosa-(opcii)
+ FLUENT_FIELD_OPTION(i64, InputRowLimit);
+
+ ///
+ /// @brief Limitation for number of output rows on single cluster node.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/dyn_query_language#ogranicheniya-na-slozhnost-zaprosa-(opcii)
+ FLUENT_FIELD_OPTION(i64, OutputRowLimit);
+
+ ///
+ /// @brief Maximum row ranges derived from WHERE clause.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/dyn_query_language#ogranicheniya-na-slozhnost-zaprosa-(opcii)
+ FLUENT_FIELD_DEFAULT(ui64, RangeExpansionLimit, 1000);
+
+ ///
+ /// @brief Whether to fail if InputRowLimit or OutputRowLimit is exceeded.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/dyn_query_language#ogranicheniya-na-slozhnost-zaprosa-(opcii)
+ FLUENT_FIELD_DEFAULT(bool, FailOnIncompleteResult, true);
+
+ /// @brief Enable verbose logging on server side.
+ FLUENT_FIELD_DEFAULT(bool, VerboseLogging, false);
+
+ FLUENT_FIELD_DEFAULT(bool, EnableCodeCache, true);
+};
+
+/// Options for NYT::CreateClient;
+struct TCreateClientOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TCreateClientOptions;
+ /// @endcond
+
+ /// @brief Impersonated user name.
+ ///
+ /// If authenticated user is allowed to impersonate other YT users (e.g. yql_agent), this field may be used to override user name.
+ FLUENT_FIELD_OPTION(TString, ImpersonationUser);
+
+ /// @brief User token.
+ ///
+ /// @see NYT::TCreateClientOptions::TokenPath
+ FLUENT_FIELD(TString, Token);
+
+ /// @brief Path to the file where user token is stored.
+ ///
+ /// Token is looked in these places in following order:
+ /// - @ref NYT::TCreateClientOptions::Token
+ /// - @ref NYT::TCreateClientOptions::TokenPath
+ /// - `TConfig::Get()->Token` option.
+ /// - `YT_TOKEN` environment variable
+ /// - `YT_SECURE_VAULT_YT_TOKEN` environment variable
+ /// - File specified in `YT_TOKEN_PATH` environment variable
+ /// - `$HOME/.yt/token` file.
+ FLUENT_FIELD(TString, TokenPath);
+
+ /// @brief TVM service ticket producer.
+ ///
+ /// We store a wrapper of NYT::TIntrusivePtr here (not a NYT::TIntrusivePtr),
+ /// because otherwise other projects will have build problems
+ /// because of visibility of two different `TIntrusivePtr`-s (::TInstrusivePtr and NYT::TInstrusivePtr).
+ ///
+ /// @see NYT::NAuth::TServiceTicketClientAuth
+ /// {@
+ NAuth::IServiceTicketAuthPtrWrapperPtr ServiceTicketAuth_ = nullptr;
+ TSelf& ServiceTicketAuth(const NAuth::IServiceTicketAuthPtrWrapper& wrapper);
+ /// @}
+
+ /// @brief Use tvm-only endpoints in cluster connection.
+ FLUENT_FIELD_DEFAULT(bool, TvmOnly, false);
+
+ /// @brief Use HTTPs (use HTTP client from yt/yt/core always).
+ ///
+ /// @see UseCoreHttpClient
+ FLUENT_FIELD_DEFAULT(bool, UseTLS, false);
+
+ /// @brief Use HTTP client from yt/yt/core.
+ FLUENT_FIELD_DEFAULT(bool, UseCoreHttpClient, false);
+
+ ///
+ /// @brief RetryConfig provider allows to fine tune request retries.
+ ///
+ /// E.g. set total timeout for all retries.
+ FLUENT_FIELD_DEFAULT(IRetryConfigProviderPtr, RetryConfigProvider, nullptr);
+
+ /// @brief Override global config for the client.
+ ///
+ /// The config contains implementation parameters such as connection timeouts,
+ /// access token, api version and more.
+ /// @see NYT::TConfig
+ FLUENT_FIELD_DEFAULT(TConfigPtr, Config, nullptr);
+};
+
+///
+/// @brief Options for @ref NYT::IBatchRequest::ExecuteBatch
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#execute_batch
+struct TExecuteBatchOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TExecuteBatchOptions;
+ /// @endcond
+
+ ///
+ /// @brief How many requests will be executed in parallel on the cluster.
+ ///
+ /// This parameter could be used to avoid RequestLimitExceeded errors.
+ FLUENT_FIELD_OPTION(ui64, Concurrency);
+
+ ///
+ /// @brief Maximum size of batch sent in one request to server.
+ ///
+ /// Huge batches are executed using multiple requests.
+ /// BatchPartMaxSize is maximum size of single request that goes to server
+ /// If not specified it is set to `Concurrency * 5'
+ FLUENT_FIELD_OPTION(ui64, BatchPartMaxSize);
+};
+
+///
+/// @brief Durability mode.
+///
+/// @see NYT::TTabletTransactionOptions::TDurability
+/// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/sorted_dynamic_tables#sohrannost
+enum class EDurability
+{
+ /// Sync mode (default).
+ Sync /* "sync" */,
+
+ /// Async mode (might reduce latency of write requests, but less reliable).
+ Async /* "async" */,
+};
+
+///
+/// @brief Atomicity mode.
+///
+/// @see NYT::TTabletTransactionOptions::TDurability
+/// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/sorted_dynamic_tables#sohrannost
+enum class EAtomicity
+{
+ /// Transactions are non atomic (might reduce latency of write requests).
+ None /* "none" */,
+
+ /// Transactions are atomic (default).
+ Full /* "full" */,
+};
+
+///
+/// @brief Table replica mode.
+///
+/// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/replicated_dynamic_tables#atributy
+enum class ETableReplicaMode
+{
+ Sync /* "sync" */,
+ Async /* "async" */,
+};
+
+/// Base class for options dealing with io to dynamic tables.
+template <typename TDerived>
+struct TTabletTransactionOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Atomicity mode of operation
+ ///
+ /// Setting to NYT::EAtomicity::None allows to improve latency of operations
+ /// at the cost of weakening contracts.
+ ///
+ /// @note Use with care.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/sorted_dynamic_tables#oslablenie-garantij
+ FLUENT_FIELD_OPTION(EAtomicity, Atomicity);
+
+ ///
+ /// @brief Durability mode of operation
+ ///
+ /// Setting to NYT::EDurability::Async allows to improve latency of operations
+ /// at the cost of weakening contracts.
+ ///
+ /// @note Use with care.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/sorted_dynamic_tables#oslablenie-garantij
+ FLUENT_FIELD_OPTION(EDurability, Durability);
+};
+
+///
+/// @brief Options for NYT::IClient::InsertRows
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#insert_rows
+struct TInsertRowsOptions
+ : public TTabletTransactionOptions<TInsertRowsOptions>
+{
+ ///
+ /// @brief Whether to overwrite missing columns with nulls.
+ ///
+ /// By default all columns missing in input data are set to Null and overwrite currently stored value.
+ /// If `Update' is set to true currently stored value will not be overwritten for columns that are missing in input data.
+ FLUENT_FIELD_OPTION(bool, Update);
+
+ ///
+ /// @brief Whether to overwrite or aggregate aggregated columns.
+ ///
+ /// Used with aggregating columns.
+ /// By default value in aggregating column will be overwritten.
+ /// If `Aggregate' is set to true row will be considered as delta and it will be aggregated with currently stored value.
+ FLUENT_FIELD_OPTION(bool, Aggregate);
+
+ ///
+ /// @brief Whether to fail when inserting to table without sync replica.
+ ///
+ /// Used for insert operation for tables without sync replica.
+ /// https://yt.yandex-team.ru/docs/description/dynamic_tables/replicated_dynamic_tables#write
+ /// Default value is 'false'. So insertion into table without sync replicas fails.
+ FLUENT_FIELD_OPTION(bool, RequireSyncReplica);
+};
+
+///
+/// @brief Options for NYT::IClient::DeleteRows
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#delete_rows
+struct TDeleteRowsOptions
+ : public TTabletTransactionOptions<TDeleteRowsOptions>
+{
+ ///
+ /// @brief Whether to fail when deleting from table without sync replica.
+ ///
+ // Used for delete operation for tables without sync replica.
+ // https://yt.yandex-team.ru/docs/description/dynamic_tables/replicated_dynamic_tables#write
+ // Default value is 'false'. So deletion into table without sync replicas fails.
+ FLUENT_FIELD_OPTION(bool, RequireSyncReplica);
+};
+
+///
+/// @brief Options for NYT::IClient::TrimRows
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#trim_rows
+struct TTrimRowsOptions
+ : public TTabletTransactionOptions<TTrimRowsOptions>
+{ };
+
+/// @brief Options for NYT::IClient::AlterTableReplica
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#alter_table_replica
+/// @see https://yt.yandex-team.ru/docs/description/dynamic_tables/replicated_dynamic_tables
+struct TAlterTableReplicaOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TAlterTableReplicaOptions;
+ /// @endcond
+
+ ///
+ /// @brief Whether to enable or disable replica.
+ ///
+ /// Doesn't change state of replica if `Enabled' is not set.
+ FLUENT_FIELD_OPTION(bool, Enabled);
+
+ ///
+ /// @brief Change replica mode.
+ ///
+ /// Doesn't change replica mode if `Mode` is not set.
+ FLUENT_FIELD_OPTION(ETableReplicaMode, Mode);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetFileFromCache
+///
+/// @note They are empty for now but options might appear in the future.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#get_file_from_cache
+struct TGetFileFromCacheOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetFileFromCacheOptions;
+ /// @endcond
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetTableColumnarStatistics
+///
+/// @note They are empty for now but options might appear in the future.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#put_file_to_cache
+struct TPutFileToCacheOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TPutFileToCacheOptions;
+ /// @endcond
+
+ /// Whether to preserve `expiration_timeout` attribute of source node.
+ FLUENT_FIELD_OPTION(bool, PreserveExpirationTimeout);
+};
+
+///
+/// Type of permission used in ACL.
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/access_control
+enum class EPermission : int
+{
+ /// Applies to: all objects.
+ Read /* "read" */,
+
+ /// Applies to: all objects.
+ Write /* "write" */,
+
+ /// Applies to: accounts / pools.
+ Use /* "use" */,
+
+ /// Applies to: all objects.
+ Administer /* "administer" */,
+
+ /// Applies to: schemas.
+ Create /* "create" */,
+
+ /// Applies to: all objects.
+ Remove /* "remove" */,
+
+ /// Applies to: tables.
+ Mount /* "mount" */,
+
+ /// Applies to: operations.
+ Manage /* "manage" */,
+};
+
+/// Whether permission is granted or denied.
+enum class ESecurityAction : int
+{
+ /// Permission is granted.
+ Allow /* "allow" */,
+
+ /// Permission is denied.
+ Deny /* "deny" */,
+};
+
+///
+/// @brief Options for @ref NYT::IClient::CheckPermission
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#check_permission
+struct TCheckPermissionOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TCheckPermissionOptions;
+ /// @endcond
+
+ /// Columns to check permission to (for tables only).
+ FLUENT_VECTOR_FIELD(TString, Column);
+};
+
+///
+/// @brief Columnar statistics fetching mode.
+///
+/// @ref NYT::TGetTableColumnarStatisticsOptions::FetcherMode
+enum class EColumnarStatisticsFetcherMode
+{
+ /// Slow mode for fetching precise columnar statistics.
+ FromNodes /* "from_nodes" */,
+
+ ///
+ /// @brief Fast mode for fetching lightweight columnar statistics.
+ ///
+ /// Relative precision is 1 / 256.
+ ///
+ /// @note Might be unavailable for old tables in that case some upper bound is returned.
+ FromMaster /* "from_master" */,
+
+ /// Use lightweight columnar statistics (FromMaster) if available otherwise switch to slow but precise mode (FromNodes).
+ Fallback /* "fallback" */,
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetTableColumnarStatistics
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#get_table_columnar_statistics
+struct TGetTableColumnarStatisticsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetTableColumnarStatisticsOptions;
+ /// @endcond
+
+ ///
+ /// @brief Mode of statistics fetching.
+ ///
+ /// @ref NYT::EColumnarStatisticsFetcherMode
+ FLUENT_FIELD_OPTION(EColumnarStatisticsFetcherMode, FetcherMode);
+};
+
+///
+/// @brief Table partitioning mode.
+///
+/// @ref NYT::TGetTablePartitionsOptions::PartitionMode
+enum class ETablePartitionMode
+{
+ ///
+ /// @brief Ignores the order of input tables and their chunk and sorting orders.
+ ///
+ Unordered /* "unordered" */,
+
+ ///
+ /// @brief The order of table ranges inside each partition obey the order of input tables and their chunk orders.
+ ///
+ Ordered /* "ordered" */,
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetTablePartitions
+///
+struct TGetTablePartitionsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetTablePartitionsOptions;
+ /// @endcond
+
+ ///
+ /// @brief Table partitioning mode.
+ ///
+ /// @ref NYT::ETablePartitionMode
+ FLUENT_FIELD(ETablePartitionMode, PartitionMode);
+
+ ///
+ /// @brief Approximate data weight of each output partition.
+ ///
+ FLUENT_FIELD(i64, DataWeightPerPartition);
+
+ ///
+ /// @brief Maximum output partition count.
+ ///
+ /// Consider the situation when the `MaxPartitionCount` is given
+ /// and the total data weight exceeds `MaxPartitionCount * DataWeightPerPartition`.
+ /// If `AdjustDataWeightPerPartition` is |true|
+ /// `GetTablePartitions` will yield partitions exceeding the `DataWeightPerPartition`.
+ /// If `AdjustDataWeightPerPartition` is |false|
+ /// the partitioning will be aborted as soon as the output partition count exceeds this limit.
+ FLUENT_FIELD_OPTION(int, MaxPartitionCount);
+
+ ///
+ /// @brief Allow the data weight per partition to exceed `DataWeightPerPartition` when `MaxPartitionCount` is set.
+ ///
+ /// |True| by default.
+ FLUENT_FIELD_DEFAULT(bool, AdjustDataWeightPerPartition, true);
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetTabletInfos
+///
+/// @note They are empty for now but options might appear in the future.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#get_tablet_infos
+struct TGetTabletInfosOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetTabletInfosOptions;
+ /// @endcond
+};
+
+/// Options for @ref NYT::IClient::SkyShareTable
+struct TSkyShareTableOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSkyShareTableOptions;
+ /// @endcond
+
+ ///
+ /// @brief Key columns that are used to group files in a table into torrents.
+ ///
+ /// One torrent is created for each value of `KeyColumns` columns.
+ /// If not specified, all files go into single torrent.
+ FLUENT_FIELD_OPTION(TColumnNames, KeyColumns);
+
+ /// @brief Allow skynet manager to return fastbone links to skynet. See YT-11437
+ FLUENT_FIELD_OPTION(bool, EnableFastbone);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/common.cpp b/yt/cpp/mapreduce/interface/common.cpp
new file mode 100644
index 0000000000..f6d60127ce
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/common.cpp
@@ -0,0 +1,664 @@
+#include "common.h"
+
+#include "errors.h"
+#include "format.h"
+#include "serialize.h"
+#include "fluent.h"
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/type_info/type.h>
+
+#include <util/generic/xrange.h>
+
+namespace NYT {
+
+using ::google::protobuf::FieldDescriptor;
+using ::google::protobuf::Descriptor;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSortColumn::TSortColumn(TStringBuf name, ESortOrder sortOrder)
+ : Name_(name)
+ , SortOrder_(sortOrder)
+{ }
+
+TSortColumn::TSortColumn(const TString& name, ESortOrder sortOrder)
+ : TSortColumn(static_cast<TStringBuf>(name), sortOrder)
+{ }
+
+TSortColumn::TSortColumn(const char* name, ESortOrder sortOrder)
+ : TSortColumn(static_cast<TStringBuf>(name), sortOrder)
+{ }
+
+const TSortColumn& TSortColumn::EnsureAscending() const
+{
+ Y_ENSURE(SortOrder() == ESortOrder::SO_ASCENDING);
+ return *this;
+}
+
+TNode TSortColumn::ToNode() const
+{
+ return BuildYsonNodeFluently().Value(*this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Below lie backward compatibility methods.
+////////////////////////////////////////////////////////////////////////////////
+
+TSortColumn& TSortColumn::operator = (TStringBuf name)
+{
+ EnsureAscending();
+ Name_ = name;
+ return *this;
+}
+
+TSortColumn& TSortColumn::operator = (const TString& name)
+{
+ return (*this = static_cast<TStringBuf>(name));
+}
+
+TSortColumn& TSortColumn::operator = (const char* name)
+{
+ return (*this = static_cast<TStringBuf>(name));
+}
+
+bool TSortColumn::operator == (TStringBuf rhsName) const
+{
+ EnsureAscending();
+ return Name_ == rhsName;
+}
+
+bool TSortColumn::operator != (TStringBuf rhsName) const
+{
+ return !(*this == rhsName);
+}
+
+bool TSortColumn::operator == (const TString& rhsName) const
+{
+ return *this == static_cast<TStringBuf>(rhsName);
+}
+
+bool TSortColumn::operator != (const TString& rhsName) const
+{
+ return !(*this == rhsName);
+}
+
+bool TSortColumn::operator == (const char* rhsName) const
+{
+ return *this == static_cast<TStringBuf>(rhsName);
+}
+
+bool TSortColumn::operator != (const char* rhsName) const
+{
+ return !(*this == rhsName);
+}
+
+TSortColumn::operator TStringBuf() const
+{
+ EnsureAscending();
+ return Name_;
+}
+
+TSortColumn::operator TString() const
+{
+ return TString(static_cast<TStringBuf>(*this));
+}
+
+TSortColumn::operator std::string() const
+{
+ EnsureAscending();
+ return static_cast<std::string>(Name_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSortColumns::TSortColumns()
+{ }
+
+TSortColumns::TSortColumns(const TVector<TString>& names)
+{
+ Parts_.assign(names.begin(), names.end());
+}
+
+TSortColumns::TSortColumns(const TColumnNames& names)
+ : TSortColumns(names.Parts_)
+{ }
+
+TSortColumns::operator TColumnNames() const
+{
+ return TColumnNames(EnsureAscending().GetNames());
+}
+
+const TSortColumns& TSortColumns::EnsureAscending() const
+{
+ for (const auto& sortColumn : Parts_) {
+ sortColumn.EnsureAscending();
+ }
+ return *this;
+}
+
+TVector<TString> TSortColumns::GetNames() const
+{
+ TVector<TString> names;
+ names.reserve(Parts_.size());
+ for (const auto& sortColumn : Parts_) {
+ names.push_back(sortColumn.Name());
+ }
+ return names;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NTi::TTypePtr OldTypeToTypeV3(EValueType type)
+{
+ switch (type) {
+ case VT_INT64:
+ return NTi::Int64();
+ case VT_UINT64:
+ return NTi::Uint64();
+
+ case VT_DOUBLE:
+ return NTi::Double();
+
+ case VT_BOOLEAN:
+ return NTi::Bool();
+
+ case VT_STRING:
+ return NTi::String();
+
+ case VT_ANY:
+ return NTi::Yson();
+
+ case VT_INT8:
+ return NTi::Int8();
+ case VT_INT16:
+ return NTi::Int16();
+ case VT_INT32:
+ return NTi::Int32();
+
+ case VT_UINT8:
+ return NTi::Uint8();
+ case VT_UINT16:
+ return NTi::Uint16();
+ case VT_UINT32:
+ return NTi::Uint32();
+
+ case VT_UTF8:
+ return NTi::Utf8();
+
+ case VT_NULL:
+ return NTi::Null();
+
+ case VT_VOID:
+ return NTi::Void();
+
+ case VT_DATE:
+ return NTi::Date();
+ case VT_DATETIME:
+ return NTi::Datetime();
+ case VT_TIMESTAMP:
+ return NTi::Timestamp();
+ case VT_INTERVAL:
+ return NTi::Interval();
+
+ case VT_FLOAT:
+ return NTi::Float();
+ case VT_JSON:
+ return NTi::Json();
+ }
+}
+
+static std::pair<EValueType, bool> Simplify(const NTi::TTypePtr& type)
+{
+ using namespace NTi;
+ const auto typeName = type->GetTypeName();
+ switch (typeName) {
+ case ETypeName::Bool:
+ return {VT_BOOLEAN, true};
+
+ case ETypeName::Int8:
+ return {VT_INT8, true};
+ case ETypeName::Int16:
+ return {VT_INT16, true};
+ case ETypeName::Int32:
+ return {VT_INT32, true};
+ case ETypeName::Int64:
+ return {VT_INT64, true};
+
+ case ETypeName::Uint8:
+ return {VT_UINT8, true};
+ case ETypeName::Uint16:
+ return {VT_UINT16, true};
+ case ETypeName::Uint32:
+ return {VT_UINT32, true};
+ case ETypeName::Uint64:
+ return {VT_UINT64, true};
+
+ case ETypeName::Float:
+ return {VT_FLOAT, true};
+ case ETypeName::Double:
+ return {VT_DOUBLE, true};
+
+ case ETypeName::String:
+ return {VT_STRING, true};
+ case ETypeName::Utf8:
+ return {VT_UTF8, true};
+
+ case ETypeName::Date:
+ return {VT_DATE, true};
+ case ETypeName::Datetime:
+ return {VT_DATETIME, true};
+ case ETypeName::Timestamp:
+ return {VT_TIMESTAMP, true};
+ case ETypeName::Interval:
+ return {VT_INTERVAL, true};
+
+ case ETypeName::TzDate:
+ case ETypeName::TzDatetime:
+ case ETypeName::TzTimestamp:
+ break;
+
+ case ETypeName::Json:
+ return {VT_JSON, true};
+ case ETypeName::Decimal:
+ return {VT_STRING, true};
+ case ETypeName::Uuid:
+ break;
+ case ETypeName::Yson:
+ return {VT_ANY, true};
+
+ case ETypeName::Void:
+ return {VT_VOID, false};
+ case ETypeName::Null:
+ return {VT_NULL, false};
+
+ case ETypeName::Optional:
+ {
+ auto itemType = type->AsOptional()->GetItemType();
+ if (itemType->IsPrimitive()) {
+ auto simplified = Simplify(itemType->AsPrimitive());
+ if (simplified.second) {
+ simplified.second = false;
+ return simplified;
+ }
+ }
+ return {VT_ANY, false};
+ }
+ case ETypeName::List:
+ return {VT_ANY, true};
+ case ETypeName::Dict:
+ return {VT_ANY, true};
+ case ETypeName::Struct:
+ return {VT_ANY, true};
+ case ETypeName::Tuple:
+ return {VT_ANY, true};
+ case ETypeName::Variant:
+ return {VT_ANY, true};
+ case ETypeName::Tagged:
+ return Simplify(type->AsTagged()->GetItemType());
+ }
+ ythrow TApiUsageError() << "Unsupported type: " << typeName;
+}
+
+NTi::TTypePtr ToTypeV3(EValueType type, bool required)
+{
+ auto typeV3 = OldTypeToTypeV3(type);
+ if (!Simplify(typeV3).second) {
+ if (required) {
+ ythrow TApiUsageError() << "type: " << type << " cannot be required";
+ } else {
+ return typeV3;
+ }
+ }
+ if (required) {
+ return typeV3;
+ } else {
+ return NTi::Optional(typeV3);
+ }
+}
+
+TColumnSchema::TColumnSchema()
+ : TypeV3_(NTi::Optional(NTi::Int64()))
+{ }
+
+EValueType TColumnSchema::Type() const
+{
+ return Simplify(TypeV3_).first;
+}
+
+TColumnSchema& TColumnSchema::Type(EValueType type) &
+{
+ return Type(ToTypeV3(type, false));
+}
+
+TColumnSchema TColumnSchema::Type(EValueType type) &&
+{
+ return Type(ToTypeV3(type, false));
+}
+
+TColumnSchema& TColumnSchema::Type(const NTi::TTypePtr& type) &
+{
+ Y_VERIFY(type.Get(), "Cannot create column schema with nullptr type");
+ TypeV3_ = type;
+ return *this;
+}
+
+TColumnSchema TColumnSchema::Type(const NTi::TTypePtr& type) &&
+{
+ Y_VERIFY(type.Get(), "Cannot create column schema with nullptr type");
+ TypeV3_ = type;
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::TypeV3(const NTi::TTypePtr& type) &
+{
+ return Type(type);
+}
+
+TColumnSchema TColumnSchema::TypeV3(const NTi::TTypePtr& type) &&
+{
+ return Type(type);
+}
+
+NTi::TTypePtr TColumnSchema::TypeV3() const
+{
+ return TypeV3_;
+}
+
+bool TColumnSchema::Required() const
+{
+ return Simplify(TypeV3_).second;
+}
+
+TColumnSchema& TColumnSchema::Type(EValueType type, bool required) &
+{
+ return Type(ToTypeV3(type, required));
+}
+
+TColumnSchema TColumnSchema::Type(EValueType type, bool required) &&
+{
+ return Type(ToTypeV3(type, required));
+}
+
+bool operator==(const TColumnSchema& lhs, const TColumnSchema& rhs)
+{
+ return
+ lhs.Name() == rhs.Name() &&
+ NTi::NEq::TStrictlyEqual()(lhs.TypeV3(), rhs.TypeV3()) &&
+ lhs.SortOrder() == rhs.SortOrder() &&
+ lhs.Lock() == rhs.Lock() &&
+ lhs.Expression() == rhs.Expression() &&
+ lhs.Aggregate() == rhs.Aggregate() &&
+ lhs.Group() == rhs.Group();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TTableSchema::Empty() const
+{
+ return Columns_.empty();
+}
+
+TTableSchema& TTableSchema::AddColumn(const TString& name, EValueType type) &
+{
+ Columns_.push_back(TColumnSchema().Name(name).Type(type));
+ return *this;
+}
+
+TTableSchema TTableSchema::AddColumn(const TString& name, EValueType type) &&
+{
+ return std::move(AddColumn(name, type));
+}
+
+TTableSchema& TTableSchema::AddColumn(const TString& name, EValueType type, ESortOrder sortOrder) &
+{
+ Columns_.push_back(TColumnSchema().Name(name).Type(type).SortOrder(sortOrder));
+ return *this;
+}
+
+TTableSchema TTableSchema::AddColumn(const TString& name, EValueType type, ESortOrder sortOrder) &&
+{
+ return std::move(AddColumn(name, type, sortOrder));
+}
+
+TTableSchema& TTableSchema::AddColumn(const TString& name, const NTi::TTypePtr& type) &
+{
+ Columns_.push_back(TColumnSchema().Name(name).Type(type));
+ return *this;
+}
+
+TTableSchema TTableSchema::AddColumn(const TString& name, const NTi::TTypePtr& type) &&
+{
+ return std::move(AddColumn(name, type));
+}
+
+TTableSchema& TTableSchema::AddColumn(const TString& name, const NTi::TTypePtr& type, ESortOrder sortOrder) &
+{
+ Columns_.push_back(TColumnSchema().Name(name).Type(type).SortOrder(sortOrder));
+ return *this;
+}
+
+TTableSchema TTableSchema::AddColumn(const TString& name, const NTi::TTypePtr& type, ESortOrder sortOrder) &&
+{
+ return std::move(AddColumn(name, type, sortOrder));
+}
+
+TTableSchema& TTableSchema::SortBy(const TSortColumns& sortColumns) &
+{
+ Y_ENSURE(sortColumns.Parts_.size() <= Columns_.size());
+
+ THashMap<TString, ui64> sortColumnIndex;
+ for (auto i: xrange(sortColumns.Parts_.size())) {
+ Y_ENSURE(sortColumnIndex.emplace(sortColumns.Parts_[i].Name(), i).second,
+ "Key column name '" << sortColumns.Parts_[i].Name() << "' repeats in columns list");
+ }
+
+ TVector<TColumnSchema> newColumnsSorted(sortColumns.Parts_.size());
+ TVector<TColumnSchema> newColumnsUnsorted;
+ for (auto& column : Columns_) {
+ auto it = sortColumnIndex.find(column.Name());
+ if (it == sortColumnIndex.end()) {
+ column.ResetSortOrder();
+ newColumnsUnsorted.push_back(std::move(column));
+ } else {
+ auto index = it->second;
+ const auto& sortColumn = sortColumns.Parts_[index];
+ column.SortOrder(sortColumn.SortOrder());
+ newColumnsSorted[index] = std::move(column);
+ sortColumnIndex.erase(it);
+ }
+ }
+
+ Y_ENSURE(sortColumnIndex.empty(), "Column name '" << sortColumnIndex.begin()->first
+ << "' not found in table schema");
+
+ newColumnsSorted.insert(newColumnsSorted.end(), newColumnsUnsorted.begin(), newColumnsUnsorted.end());
+ Columns_ = std::move(newColumnsSorted);
+
+ return *this;
+}
+
+TTableSchema TTableSchema::SortBy(const TSortColumns& sortColumns) &&
+{
+ return std::move(SortBy(sortColumns));
+}
+
+TVector<TColumnSchema>& TTableSchema::MutableColumns()
+{
+ return Columns_;
+}
+
+TNode TTableSchema::ToNode() const
+{
+ TNode result;
+ TNodeBuilder builder(&result);
+ Serialize(*this, &builder);
+ return result;
+}
+
+TTableSchema TTableSchema::FromNode(const TNode& node)
+{
+ TTableSchema schema;
+ Deserialize(schema, node);
+ return schema;
+}
+
+bool operator==(const TTableSchema& lhs, const TTableSchema& rhs)
+{
+ return
+ lhs.Columns() == rhs.Columns() &&
+ lhs.Strict() == rhs.Strict() &&
+ lhs.UniqueKeys() == rhs.UniqueKeys();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyBound::TKeyBound(ERelation relation, TKey key)
+ : Relation_(relation)
+ , Key_(std::move(key))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableSchema CreateTableSchema(
+ const Descriptor& messageDescriptor,
+ const TSortColumns& sortColumns,
+ bool keepFieldsWithoutExtension)
+{
+ auto result = CreateTableSchema(messageDescriptor, keepFieldsWithoutExtension);
+ if (!sortColumns.Parts_.empty()) {
+ result.SortBy(sortColumns.Parts_);
+ }
+ return result;
+}
+
+TTableSchema CreateTableSchema(NTi::TTypePtr type)
+{
+ Y_VERIFY(type);
+ TTableSchema schema;
+ Deserialize(schema, NodeFromYsonString(NTi::NIo::AsYtSchema(type.Get())));
+ return schema;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsTrivial(const TReadLimit& readLimit)
+{
+ return !readLimit.Key_ && !readLimit.RowIndex_ && !readLimit.Offset_ && !readLimit.TabletIndex_ && !readLimit.KeyBound_;
+}
+
+EValueType NodeTypeToValueType(TNode::EType nodeType)
+{
+ switch (nodeType) {
+ case TNode::EType::Int64: return VT_INT64;
+ case TNode::EType::Uint64: return VT_UINT64;
+ case TNode::EType::String: return VT_STRING;
+ case TNode::EType::Double: return VT_DOUBLE;
+ case TNode::EType::Bool: return VT_BOOLEAN;
+ default:
+ ythrow yexception() << "Cannot convert TNode type " << nodeType << " to EValueType";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TVector<TReadRange>& GetRangesCompat(const TRichYPath& path)
+{
+ static const TVector<TReadRange> empty;
+
+ const auto& maybeRanges = path.GetRanges();
+ if (maybeRanges.Empty()) {
+ return empty;
+ } else if (maybeRanges->size() > 0) {
+ return *maybeRanges;
+ } else {
+ // If you see this exception, that means that caller of this function doesn't known what to do
+ // with RichYPath that has set range list, but the range list is empty.
+ //
+ // To avoid this exception caller must explicitly handle such case.
+ // NB. YT-17683
+ ythrow TApiUsageError() << "Unsupported RichYPath: explicitly empty range list";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(EValueType type)
+{
+ switch (type) {
+ case VT_INT8:
+ return "int8";
+ case VT_INT16:
+ return "int16";
+ case VT_INT32:
+ return "int32";
+ case VT_INT64:
+ return "int64";
+
+ case VT_UINT8:
+ return "uint8";
+ case VT_UINT16:
+ return "uint16";
+ case VT_UINT32:
+ return "uint32";
+ case VT_UINT64:
+ return "uint64";
+
+ case VT_DOUBLE:
+ return "double";
+
+ case VT_BOOLEAN:
+ return "boolean";
+
+ case VT_STRING:
+ return "string";
+ case VT_UTF8:
+ return "utf8";
+
+ case VT_ANY:
+ return "any";
+
+ case VT_NULL:
+ return "null";
+ case VT_VOID:
+ return "void";
+
+ case VT_DATE:
+ return "date";
+ case VT_DATETIME:
+ return "datetime";
+ case VT_TIMESTAMP:
+ return "timestamp";
+ case VT_INTERVAL:
+ return "interval";
+
+ case VT_FLOAT:
+ return "float";
+
+ case VT_JSON:
+ return "json";
+ }
+ ythrow yexception() << "Invalid value type " << static_cast<int>(type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
+
+template <>
+void Out<NYT::TSortColumn>(IOutputStream& os, const NYT::TSortColumn& sortColumn)
+{
+ if (sortColumn.SortOrder() == NYT::ESortOrder::SO_ASCENDING) {
+ os << sortColumn.Name();
+ } else {
+ os << NYT::BuildYsonStringFluently(NYson::EYsonFormat::Text).Value(sortColumn);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/common.h b/yt/cpp/mapreduce/interface/common.h
new file mode 100644
index 0000000000..b1754ade70
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/common.h
@@ -0,0 +1,1301 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/common.h
+///
+/// Header containing miscellaneous structs and classes used in library.
+
+#include "fwd.h"
+
+#include <library/cpp/type_info/type_info.h>
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/guid.h>
+#include <util/generic/map.h>
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+#include <util/system/type_name.h>
+#include <util/generic/vector.h>
+
+#include <google/protobuf/message.h>
+
+#include <initializer_list>
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @cond Doxygen_Suppress
+#define FLUENT_FIELD(type, name) \
+ type name##_; \
+ TSelf& name(const type& value) \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ static_assert(true)
+
+#define FLUENT_FIELD_ENCAPSULATED(type, name) \
+private: \
+ type name##_; \
+public: \
+ TSelf& name(const type& value) & \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ TSelf name(const type& value) && \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ const type& name() const & \
+ { \
+ return name##_; \
+ } \
+ type name() && \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+#define FLUENT_FIELD_OPTION(type, name) \
+ TMaybe<type> name##_; \
+ TSelf& name(const type& value) \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ static_assert(true)
+
+#define FLUENT_FIELD_OPTION_ENCAPSULATED(type, name) \
+private: \
+ TMaybe<type> name##_; \
+public: \
+ TSelf& name(const type& value) & \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ TSelf name(const type& value) && \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ TSelf& Reset##name() & \
+ { \
+ name##_ = Nothing(); \
+ return static_cast<TSelf&>(*this); \
+ } \
+ TSelf Reset##name() && \
+ { \
+ name##_ = Nothing(); \
+ return static_cast<TSelf&>(*this); \
+ } \
+ const TMaybe<type>& name() const& \
+ { \
+ return name##_; \
+ } \
+ TMaybe<type> name() && \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+#define FLUENT_FIELD_DEFAULT(type, name, defaultValue) \
+ type name##_ = defaultValue; \
+ TSelf& name(const type& value) \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ static_assert(true)
+
+#define FLUENT_FIELD_DEFAULT_ENCAPSULATED(type, name, defaultValue) \
+private: \
+ type name##_ = defaultValue; \
+public: \
+ TSelf& name(const type& value) & \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ TSelf name(const type& value) && \
+ { \
+ name##_ = value; \
+ return static_cast<TSelf&>(*this); \
+ } \
+ const type& name() const & \
+ { \
+ return name##_; \
+ } \
+ type name() && \
+ { \
+ return name##_; \
+ } \
+ static_assert(true)
+
+#define FLUENT_VECTOR_FIELD(type, name) \
+ TVector<type> name##s_; \
+ TSelf& Add##name(const type& value) \
+ { \
+ name##s_.push_back(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf& name##s(TVector<type> values) \
+ { \
+ name##s_ = std::move(values); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ static_assert(true)
+
+#define FLUENT_OPTIONAL_VECTOR_FIELD_ENCAPSULATED(type, name) \
+private: \
+ TMaybe<TVector<type>> name##s_; \
+public: \
+ const TMaybe<TVector<type>>& name##s() const & { \
+ return name##s_; \
+ } \
+ TMaybe<TVector<type>>& name##s() & { \
+ return name##s_; \
+ } \
+ TMaybe<TVector<type>> name##s() && { \
+ return std::move(name##s_); \
+ } \
+ TSelf& Add##name(const type& value) & \
+ { \
+ if (name##s_.Empty()) { \
+ name##s_.ConstructInPlace(); \
+ } \
+ name##s_->push_back(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf Add##name(const type& value) && \
+ { \
+ if (name##s_.Empty()) { \
+ name##s_.ConstructInPlace(); \
+ } \
+ name##s_->push_back(value); \
+ return static_cast<TSelf&&>(*this);\
+ } \
+ TSelf& name##s(TVector<type> values) & \
+ { \
+ name##s_ = std::move(values); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf name##s(TVector<type> values) && \
+ { \
+ name##s_ = std::move(values); \
+ return static_cast<TSelf&&>(*this);\
+ } \
+ TSelf& name##s(TNothing) & \
+ { \
+ name##s_ = Nothing(); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf name##s(TNothing) && \
+ { \
+ name##s_ = Nothing(); \
+ return static_cast<TSelf&&>(*this);\
+ } \
+ TSelf& Reset##name##s() & \
+ { \
+ name##s_ = Nothing(); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf Reset##name##s() && \
+ { \
+ name##s_ = Nothing(); \
+ return static_cast<TSelf&&>(*this);\
+ } \
+ static_assert(true)
+
+#define FLUENT_VECTOR_FIELD_ENCAPSULATED(type, name) \
+private: \
+ TVector<type> name##s_; \
+public: \
+ TSelf& Add##name(const type& value) & \
+ { \
+ name##s_.push_back(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf Add##name(const type& value) && \
+ { \
+ name##s_.push_back(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf& name##s(TVector<type> value) & \
+ { \
+ name##s_ = std::move(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ TSelf name##s(TVector<type> value) && \
+ { \
+ name##s_ = std::move(value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ const TVector<type>& name##s() const & \
+ { \
+ return name##s_; \
+ } \
+ TVector<type> name##s() && \
+ { \
+ return name##s_; \
+ } \
+ static_assert(true)
+
+#define FLUENT_MAP_FIELD(keytype, valuetype, name) \
+ TMap<keytype,valuetype> name##_; \
+ TSelf& Add##name(const keytype& key, const valuetype& value) \
+ { \
+ name##_.emplace(key, value); \
+ return static_cast<TSelf&>(*this);\
+ } \
+ static_assert(true)
+
+/// @endcond
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Convenience class that keeps sequence of items.
+///
+/// Designed to be used as function parameter.
+///
+/// Users of such function can then pass:
+/// - single item,
+/// - initializer list of items,
+/// - vector of items;
+/// as argument to this function.
+///
+/// Example:
+/// ```
+/// void Foo(const TOneOrMany<int>& arg);
+/// ...
+/// Foo(1); // ok
+/// Foo({1, 2, 3}); // ok
+/// ```
+template <class T, class TDerived>
+struct TOneOrMany
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = std::conditional_t<std::is_void_v<TDerived>, TOneOrMany, TDerived>;
+ /// @endcond
+
+ /// Initialize with empty sequence.
+ TOneOrMany() = default;
+
+ // Initialize from initializer list.
+ template<class U>
+ TOneOrMany(std::initializer_list<U> il)
+ {
+ Parts_.assign(il.begin(), il.end());
+ }
+
+ /// Put arguments to sequence
+ template <class U, class... TArgs>
+ requires std::is_convertible_v<U, T>
+ TOneOrMany(U&& arg, TArgs&&... args)
+ {
+ Add(arg, std::forward<TArgs>(args)...);
+ }
+
+ /// Initialize from vector.
+ TOneOrMany(TVector<T> args)
+ : Parts_(std::move(args))
+ { }
+
+ /// @brief Order is defined the same way as in TVector
+ bool operator==(const TOneOrMany& rhs) const
+ {
+ // N.B. We would like to make this method to be `= default`,
+ // but this breaks MSVC compiler for the cases when T doesn't
+ // support comparison.
+ return Parts_ == rhs.Parts_;
+ }
+
+ ///
+ /// @{
+ ///
+ /// @brief Add all arguments to sequence
+ template <class U, class... TArgs>
+ requires std::is_convertible_v<U, T>
+ TSelf& Add(U&& part, TArgs&&... args) &
+ {
+ Parts_.push_back(std::forward<U>(part));
+ if constexpr (sizeof...(args) > 0) {
+ [[maybe_unused]] int dummy[sizeof...(args)] = {(Parts_.push_back(std::forward<TArgs>(args)), 0) ... };
+ }
+ return static_cast<TSelf&>(*this);
+ }
+
+ template <class U, class... TArgs>
+ requires std::is_convertible_v<U, T>
+ TSelf Add(U&& part, TArgs&&... args) &&
+ {
+ return std::move(Add(std::forward<U>(part), std::forward<TArgs>(args)...));
+ }
+ /// @}
+
+ /// Content of sequence.
+ TVector<T> Parts_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Type of the value that can occur in YT table.
+///
+/// @ref NYT::TTableSchema
+/// https://yt.yandex-team.ru/docs/description/storage/data_types
+enum EValueType : int
+{
+ /// Int64, signed integer of 64 bits.
+ VT_INT64,
+
+ /// Uint64, unsigned integer of 64 bits.
+ VT_UINT64,
+
+ /// Double, floating point number of double precision (64 bits).
+ VT_DOUBLE,
+ /// Boolean, `true` or `false`.
+ VT_BOOLEAN,
+
+ /// String, arbitrary byte sequence.
+ VT_STRING,
+
+ /// Any, arbitrary yson document.
+ VT_ANY,
+
+ /// Int8, signed integer of 8 bits.
+ VT_INT8,
+ /// Int16, signed integer of 16 bits.
+ VT_INT16,
+ /// Int32, signed integer of 32 bits.
+ VT_INT32,
+
+ /// Uint8, unsigned integer of 8 bits.
+ VT_UINT8,
+ /// Uint16, unsigned integer of 16 bits.
+ VT_UINT16,
+ /// Uint32, unsigned integer of 32 bits.
+ VT_UINT32,
+
+ /// Utf8, byte sequence that is valid utf8.
+ VT_UTF8,
+
+ /// Null, absence of value (almost never used in schemas)
+ VT_NULL,
+ /// Void, absence of value (almost never used in schemas) the difference between null, and void is yql-specific.
+ VT_VOID,
+
+ /// Date, number of days since Unix epoch (unsigned)
+ VT_DATE,
+ /// Datetime, number of seconds since Unix epoch (unsigned)
+ VT_DATETIME,
+ /// Timestamp, number of milliseconds since Unix epoch (unsigned)
+ VT_TIMESTAMP,
+ /// Interval, difference between two timestamps (signed)
+ VT_INTERVAL,
+
+ /// Float, floating point number (32 bits)
+ VT_FLOAT,
+ /// Json, sequence of bytes that is valid json.
+ VT_JSON,
+};
+
+///
+/// @brief Sort order.
+///
+/// @ref NYT::TTableSchema
+enum ESortOrder : int
+{
+ /// Ascending sort order.
+ SO_ASCENDING /* "ascending" */,
+ /// Descending sort order.
+ SO_DESCENDING /* "descending" */,
+};
+
+///
+/// @brief Value of "optimize_for" attribute.
+///
+/// @ref NYT::TRichYPath
+enum EOptimizeForAttr : i8
+{
+ /// Optimize for scan
+ OF_SCAN_ATTR /* "scan" */,
+
+ /// Optimize for lookup
+ OF_LOOKUP_ATTR /* "lookup" */,
+};
+
+///
+/// @brief Value of "erasure_codec" attribute.
+///
+/// @ref NYT::TRichYPath
+enum EErasureCodecAttr : i8
+{
+ /// @cond Doxygen_Suppress
+ EC_NONE_ATTR /* "none" */,
+ EC_REED_SOLOMON_6_3_ATTR /* "reed_solomon_6_3" */,
+ EC_LRC_12_2_2_ATTR /* "lrc_12_2_2" */,
+ EC_ISA_LRC_12_2_2_ATTR /* "isa_lrc_12_2_2" */,
+ /// @endcond
+};
+
+///
+/// @brief Value of "schema_modification" attribute.
+///
+/// @ref NYT::TRichYPath
+enum ESchemaModificationAttr : i8
+{
+ SM_NONE_ATTR /* "none" */,
+ SM_UNVERSIONED_UPDATE /* "unversioned_update" */,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Table key column description.
+///
+/// The description includes column name and sort order.
+///
+/// @anchor TSortOrder_backward_compatibility
+/// @note
+/// Many functions that use `TSortOrder` as argument used to take `TString`
+/// (the only allowed sort order was "ascending" and user didn't have to specify it).
+/// @note
+/// This class is designed to provide backward compatibility for such code and therefore
+/// objects of this class can be constructed and assigned from TString-like objects only.
+///
+/// @see NYT::TSortOperationSpec
+class TSortColumn
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TSortColumn;
+ /// @endcond
+
+ /// Column name
+ FLUENT_FIELD_ENCAPSULATED(TString, Name);
+
+ /// Sort order
+ FLUENT_FIELD_DEFAULT_ENCAPSULATED(ESortOrder, SortOrder, ESortOrder::SO_ASCENDING);
+
+ ///
+ /// @{
+ ///
+ /// @brief Construct object from name and sort order
+ ///
+ /// Constructors are intentionally implicit so `TSortColumn` can be compatible with old code.
+ /// @ref TSortOrder_backward_compatibility
+ TSortColumn(TStringBuf name = {}, ESortOrder sortOrder = ESortOrder::SO_ASCENDING);
+ TSortColumn(const TString& name, ESortOrder sortOrder = ESortOrder::SO_ASCENDING);
+ TSortColumn(const char* name, ESortOrder sortOrder = ESortOrder::SO_ASCENDING);
+ /// @}
+
+ /// Check that sort order is ascending, throw exception otherwise.
+ const TSortColumn& EnsureAscending() const;
+
+ /// @brief Convert sort to yson representation as YT API expects it.
+ TNode ToNode() const;
+
+ /// @brief Comparison is default and checks both name and sort order.
+ bool operator == (const TSortColumn& rhs) const = default;
+
+ ///
+ /// @{
+ ///
+ /// @brief Assign object from column name, and set sort order to `ascending`.
+ ///
+ /// This is backward compatibility methods.
+ ///
+ /// @ref TSortOrder_backward_compatibility
+ TSortColumn& operator = (TStringBuf name);
+ TSortColumn& operator = (const TString& name);
+ TSortColumn& operator = (const char* name);
+ /// @}
+
+ bool operator == (const TStringBuf rhsName) const;
+ bool operator != (const TStringBuf rhsName) const;
+ bool operator == (const TString& rhsName) const;
+ bool operator != (const TString& rhsName) const;
+ bool operator == (const char* rhsName) const;
+ bool operator != (const char* rhsName) const;
+
+ // Intentionally implicit conversions.
+ operator TString() const;
+ operator TStringBuf() const;
+ operator std::string() const;
+
+ Y_SAVELOAD_DEFINE(Name_, SortOrder_);
+};
+
+///
+/// @brief List of @ref TSortColumn
+///
+/// Contains a bunch of helper methods such as constructing from single object.
+class TSortColumns
+ : public TOneOrMany<TSortColumn, TSortColumns>
+{
+public:
+ using TOneOrMany<TSortColumn, TSortColumns>::TOneOrMany;
+
+ /// Construct empty list.
+ TSortColumns();
+
+ ///
+ /// @{
+ ///
+ /// @brief Construct list of ascending sort order columns by their names.
+ ///
+ /// Required for backward compatibility.
+ ///
+ /// @ref TSortOrder_backward_compatibility
+ TSortColumns(const TVector<TString>& names);
+ TSortColumns(const TColumnNames& names);
+ /// @}
+
+
+ ///
+ /// @brief Implicit conversion to column list.
+ ///
+ /// If all columns has ascending sort order return list of their names.
+ /// Throw exception otherwise.
+ ///
+ /// Required for backward compatibility.
+ ///
+ /// @ref TSortOrder_backward_compatibility
+ operator TColumnNames() const;
+
+ /// Make sure that all columns are of ascending sort order.
+ const TSortColumns& EnsureAscending() const;
+
+ /// Get list of column names.
+ TVector<TString> GetNames() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Helper function to create new style type from old style one.
+NTi::TTypePtr ToTypeV3(EValueType type, bool required);
+
+///
+/// @brief Single column description
+///
+/// Each field describing column has setter and getter.
+///
+/// Example reading field:
+/// ```
+/// ... columnSchema.Name() ...
+/// ```
+///
+/// Example setting field:
+/// ```
+/// columnSchema.Name("my-column").Type(VT_INT64); // set name and type
+/// ```
+///
+/// @ref https://yt.yandex-team.ru/docs/description/storage/static_schema
+class TColumnSchema
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TColumnSchema;
+ /// @endcond
+
+ ///
+ /// @brief Construct empty column schemas
+ ///
+ /// @note
+ /// Such schema cannot be used in schema as it it doesn't have name.
+ TColumnSchema();
+
+ ///
+ /// @{
+ ///
+ /// @brief Copy and move constructors are default.
+ TColumnSchema(const TColumnSchema&) = default;
+ TColumnSchema& operator=(const TColumnSchema&) = default;
+ /// @}
+
+
+ FLUENT_FIELD_ENCAPSULATED(TString, Name);
+
+ ///
+ /// @brief Functions to work with type in old manner.
+ ///
+ /// @deprecated New code is recommended to work with types using @ref NTi::TTypePtr from type_info library.
+ TColumnSchema& Type(EValueType type) &;
+ TColumnSchema Type(EValueType type) &&;
+ EValueType Type() const;
+
+ /// @brief Set and get column type.
+ /// @{
+ TColumnSchema& Type(const NTi::TTypePtr& type) &;
+ TColumnSchema Type(const NTi::TTypePtr& type) &&;
+
+ TColumnSchema& TypeV3(const NTi::TTypePtr& type) &;
+ TColumnSchema TypeV3(const NTi::TTypePtr& type) &&;
+ NTi::TTypePtr TypeV3() const;
+ /// @}
+
+ ///
+ /// @brief Raw yson representation of column type
+ /// @deprecated Prefer to use `TypeV3` methods.
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TNode, RawTypeV3);
+
+ /// Column sort order
+ FLUENT_FIELD_OPTION_ENCAPSULATED(ESortOrder, SortOrder);
+
+ ///
+ /// @brief Lock group name
+ ///
+ /// @ref https://yt.yandex-team.ru/docs/description/dynamic_tables/sorted_dynamic_tables#blokirovka-stroki
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TString, Lock);
+
+ /// Expression defining column value
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TString, Expression);
+
+ /// Aggregating function name
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TString, Aggregate);
+
+ ///
+ /// @brief Storage group name
+ ///
+ /// @ref https://yt.yandex-team.ru/docs/description/storage/static_schema
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TString, Group);
+
+ ///
+ /// @brief Column requiredness.
+ ///
+ /// Required columns doesn't accept NULL values.
+ /// Usually if column is required it means that it has Optional<...> type
+ bool Required() const;
+
+ ///
+ /// @{
+ ///
+ /// @brief Set type in old-style manner
+ TColumnSchema& Type(EValueType type, bool required) &;
+ TColumnSchema Type(EValueType type, bool required) &&;
+ /// @}
+
+private:
+ friend void Deserialize(TColumnSchema& columnSchema, const TNode& node);
+ NTi::TTypePtr TypeV3_;
+ bool Required_ = false;
+};
+
+/// Equality check checks all fields of column schema.
+bool operator==(const TColumnSchema& lhs, const TColumnSchema& rhs);
+
+///
+/// @brief Description of table schema
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/static_schema
+class TTableSchema
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TTableSchema;
+ /// @endcond
+
+ /// Column schema
+ FLUENT_VECTOR_FIELD_ENCAPSULATED(TColumnSchema, Column);
+
+ ///
+ /// @brief Strictness of the schema
+ ///
+ /// Strict schemas are not allowed to have columns not described in schema.
+ /// Nonstrict schemas are allowed to have such columns, all such missing columns are assumed to have
+ FLUENT_FIELD_DEFAULT_ENCAPSULATED(bool, Strict, true);
+
+ ///
+ /// @brief Whether keys are unique
+ ///
+ /// This flag can be set only for schemas that have sorted columns.
+ /// If flag is set table cannot have multiple rows with same key.
+ FLUENT_FIELD_DEFAULT_ENCAPSULATED(bool, UniqueKeys, false);
+
+ /// Get modifiable column list
+ TVector<TColumnSchema>& MutableColumns();
+
+ /// Check if schema has any described column
+ [[nodiscard]] bool Empty() const;
+
+ /// Add column
+ TTableSchema& AddColumn(const TString& name, const NTi::TTypePtr& type, ESortOrder sortOrder) &;
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, const NTi::TTypePtr&, ESortOrder)&;
+ TTableSchema AddColumn(const TString& name, const NTi::TTypePtr& type, ESortOrder sortOrder) &&;
+
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, const NTi::TTypePtr&, ESortOrder)&;
+ TTableSchema& AddColumn(const TString& name, const NTi::TTypePtr& type) &;
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, const NTi::TTypePtr&, ESortOrder)&;
+ TTableSchema AddColumn(const TString& name, const NTi::TTypePtr& type) &&;
+
+ /// Add optional column of specified type
+ TTableSchema& AddColumn(const TString& name, EValueType type, ESortOrder sortOrder) &;
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, EValueType, ESortOrder)&;
+ TTableSchema AddColumn(const TString& name, EValueType type, ESortOrder sortOrder) &&;
+
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, EValueType, ESortOrder)&;
+ TTableSchema& AddColumn(const TString& name, EValueType type) &;
+ /// @copydoc NYT::TTableSchema::AddColumn(const TString&, EValueType, ESortOrder)&;
+ TTableSchema AddColumn(const TString& name, EValueType type) &&;
+
+ ///
+ /// @brief Make table schema sorted by specified columns
+ ///
+ /// Resets old key columns if any
+ TTableSchema& SortBy(const TSortColumns& columns) &;
+
+ /// @copydoc NYT::TTableSchema::SortBy(const TSortColumns&)&;
+ TTableSchema SortBy(const TSortColumns& columns) &&;
+
+ /// Get yson description of table schema
+ [[nodiscard]] TNode ToNode() const;
+
+ /// Parse schema from yson node
+ static NYT::TTableSchema FromNode(const TNode& node);
+
+ friend void Deserialize(TTableSchema& tableSchema, const TNode& node);
+};
+
+/// Check for equality of all columns and all schema attributes
+bool operator==(const TTableSchema& lhs, const TTableSchema& rhs);
+
+/// Create table schema by protobuf message descriptor
+TTableSchema CreateTableSchema(
+ const ::google::protobuf::Descriptor& messageDescriptor,
+ const TSortColumns& sortColumns = TSortColumns(),
+ bool keepFieldsWithoutExtension = true);
+
+/// Create table schema by protobuf message type
+template <class TProtoType, typename = std::enable_if_t<std::is_base_of_v<::google::protobuf::Message, TProtoType>>>
+inline TTableSchema CreateTableSchema(
+ const TSortColumns& sortColumns = TSortColumns(),
+ bool keepFieldsWithoutExtension = true)
+{
+ static_assert(
+ std::is_base_of_v<::google::protobuf::Message, TProtoType>,
+ "Template argument must be derived from ::google::protobuf::Message");
+
+ return CreateTableSchema(
+ *TProtoType::descriptor(),
+ sortColumns,
+ keepFieldsWithoutExtension);
+}
+
+///
+/// @brief Create strict table schema from `struct` type.
+///
+/// Names and types of columns are taken from struct member names and types.
+/// `Strict` flag is set to true, all other attribute of schema and columns
+/// are left with default values
+TTableSchema CreateTableSchema(NTi::TTypePtr type);
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Enumeration describing comparison operation used in key bound.
+///
+/// ERelation is a part of @ref NYT::TKeyBound that can be used as
+/// lower or upper key limit in @ref TReadLimit.
+///
+/// Relations `Less` and `LessOrEqual` are for upper limit and
+/// relations `Greater` and `GreaterOrEqual` are for lower limit.
+///
+/// It is a error to use relation in the limit of wrong kind.
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/ypath#rich_ypath
+enum class ERelation
+{
+ ///
+ /// @brief Relation "less"
+ ///
+ /// Specifies range of keys that are before specified key.
+ /// Can only be used in upper limit.
+ Less /* "<" */,
+
+ ///
+ /// @brief Relation "less or equal"
+ ///
+ /// Specifies range of keys that are before or equal specified key.
+ /// Can only be used in upper limit.
+ LessOrEqual /* "<=" */,
+
+ ///
+ /// @brief Relation "greater"
+ ///
+ /// Specifies range of keys that are after specified key.
+ /// Can only be used in lower limit.
+ Greater /* ">" */,
+
+ ///
+ /// @brief Relation "greater or equal"
+ ///
+ /// Specifies range of keys that are after or equal than specified key.
+ /// Can only be used in lower limit.
+ GreaterOrEqual /* ">=" */,
+};
+
+///
+/// @brief Key with relation specifying interval of keys in lower or upper limit of @ref NYT::TReadRange
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/ypath#rich_ypath
+struct TKeyBound
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TKeyBound;
+
+ explicit TKeyBound(ERelation relation = ERelation::Less, TKey key = TKey{});
+
+ FLUENT_FIELD_DEFAULT_ENCAPSULATED(ERelation, Relation, ERelation::Less);
+ FLUENT_FIELD_DEFAULT_ENCAPSULATED(TKey, Key, TKey{});
+ /// @endcond
+};
+
+///
+/// @brief Description of the read limit.
+///
+/// It is actually a variant and must store exactly one field.
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/ypath#rich_ypath
+struct TReadLimit
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TReadLimit;
+ /// @endcond
+
+ ///
+ /// @brief KeyBound specifies table key and whether to include it
+ ///
+ /// It can be used in lower or upper limit when reading tables.
+ FLUENT_FIELD_OPTION(TKeyBound, KeyBound);
+
+ ///
+ /// @brief Table key
+ ///
+ /// It can be used in exact, lower or upper limit when reading tables.
+ FLUENT_FIELD_OPTION(TKey, Key);
+
+ ///
+ /// @brief Row index
+ ///
+ /// It can be used in exact, lower or upper limit when reading tables.
+ FLUENT_FIELD_OPTION(i64, RowIndex);
+
+ ///
+ /// @brief File offset
+ ///
+ /// It can be used in lower or upper limit when reading files.
+ FLUENT_FIELD_OPTION(i64, Offset);
+
+ ///
+ /// @brief Tablet index
+ ///
+ /// It can be used in lower or upper limit in dynamic table operations
+ FLUENT_FIELD_OPTION(i64, TabletIndex);
+};
+
+///
+/// @brief Range of a table or a file
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/ypath#rich_ypath
+struct TReadRange
+{
+ using TSelf = TReadRange;
+
+ ///
+ /// @brief Lower limit of the range
+ ///
+ /// It is usually inclusive (except when @ref NYT::TKeyBound with relation @ref NYT::ERelation::Greater is used).
+ FLUENT_FIELD(TReadLimit, LowerLimit);
+
+ ///
+ /// @brief Lower limit of the range
+ ///
+ /// It is usually exclusive (except when @ref NYT::TKeyBound with relation @ref NYT::ERelation::LessOrEqual is used).
+ FLUENT_FIELD(TReadLimit, UpperLimit);
+
+ /// Exact key or row index.
+ FLUENT_FIELD(TReadLimit, Exact);
+
+ /// Create read range from row indexes.
+ static TReadRange FromRowIndices(i64 lowerLimit, i64 upperLimit)
+ {
+ return TReadRange()
+ .LowerLimit(TReadLimit().RowIndex(lowerLimit))
+ .UpperLimit(TReadLimit().RowIndex(upperLimit));
+ }
+
+ /// Create read range from keys.
+ static TReadRange FromKeys(const TKey& lowerKeyInclusive, const TKey& upperKeyExclusive)
+ {
+ return TReadRange()
+ .LowerLimit(TReadLimit().Key(lowerKeyInclusive))
+ .UpperLimit(TReadLimit().Key(upperKeyExclusive));
+ }
+};
+
+///
+/// @brief Path with additional attributes.
+///
+/// Allows to specify additional attributes for path used in some operations.
+///
+/// @see https://yt.yandex-team.ru/docs/description/common/ypath#rich_ypath
+struct TRichYPath
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TRichYPath;
+ /// @endcond
+
+ /// Path itself.
+ FLUENT_FIELD(TYPath, Path);
+
+ /// Specifies that path should be appended not overwritten
+ FLUENT_FIELD_OPTION(bool, Append);
+
+ /// @deprecated Deprecated attribute.
+ FLUENT_FIELD_OPTION(bool, PartiallySorted);
+
+ /// Specifies that path is expected to be sorted by these columns.
+ FLUENT_FIELD(TSortColumns, SortedBy);
+
+ /// Add range to read.
+ TRichYPath& AddRange(TReadRange range)
+ {
+ if (!Ranges_) {
+ Ranges_.ConstructInPlace();
+ }
+ Ranges_->push_back(std::move(range));
+ return *this;
+ }
+
+ TRichYPath& ResetRanges()
+ {
+ Ranges_.Clear();
+ return *this;
+ }
+
+ ///
+ /// @{
+ ///
+ /// Return ranges to read.
+ ///
+ /// NOTE: Nothing (in TMaybe) and empty TVector are different ranges.
+ /// Nothing represents universal range (reader reads all table rows).
+ /// Empty TVector represents empty range (reader returns empty set of rows).
+ const TMaybe<TVector<TReadRange>>& GetRanges() const
+ {
+ return Ranges_;
+ }
+
+ TMaybe<TVector<TReadRange>>& MutableRanges()
+ {
+ return Ranges_;
+ }
+
+ ///
+ /// @{
+ ///
+ /// Get range view, that is convenient way to iterate through all ranges.
+ TArrayRef<TReadRange> MutableRangesView()
+ {
+ if (Ranges_.Defined()) {
+ return TArrayRef(Ranges_->data(), Ranges_->size());
+ } else {
+ return {};
+ }
+ }
+
+ TArrayRef<const TReadRange> GetRangesView() const
+ {
+ if (Ranges_.Defined()) {
+ return TArrayRef(Ranges_->data(), Ranges_->size());
+ } else {
+ return {};
+ }
+ }
+ /// @}
+
+ /// @{
+ ///
+ /// Get range by index.
+ const TReadRange& GetRange(ssize_t i) const
+ {
+ return Ranges_.GetRef()[i];
+ }
+
+ TReadRange& MutableRange(ssize_t i)
+ {
+ return Ranges_.GetRef()[i];
+ }
+ /// @}
+
+ ///
+ /// @brief Specifies columns that should be read.
+ ///
+ /// If it's set to Nothing then all columns will be read.
+ /// If empty TColumnNames is specified then each read row will be empty.
+ FLUENT_FIELD_OPTION(TColumnNames, Columns);
+
+ FLUENT_FIELD_OPTION(bool, Teleport);
+ FLUENT_FIELD_OPTION(bool, Primary);
+ FLUENT_FIELD_OPTION(bool, Foreign);
+ FLUENT_FIELD_OPTION(i64, RowCountLimit);
+
+ FLUENT_FIELD_OPTION(TString, FileName);
+
+ /// Specifies original path to be shown in Web UI
+ FLUENT_FIELD_OPTION(TYPath, OriginalPath);
+
+ ///
+ /// @brief Specifies that this path points to executable file
+ ///
+ /// Used in operation specs.
+ FLUENT_FIELD_OPTION(bool, Executable);
+
+ ///
+ /// @brief Specify format to use when loading table.
+ ///
+ /// Used in operation specs.
+ FLUENT_FIELD_OPTION(TNode, Format);
+
+ /// @brief Specifies table schema that will be set on the path
+ FLUENT_FIELD_OPTION(TTableSchema, Schema);
+
+ /// Specifies compression codec that will be set on the path
+ FLUENT_FIELD_OPTION(TString, CompressionCodec);
+
+ /// Specifies erasure codec that will be set on the path
+ FLUENT_FIELD_OPTION(EErasureCodecAttr, ErasureCodec);
+
+ /// Specifies schema modification that will be set on the path
+ FLUENT_FIELD_OPTION(ESchemaModificationAttr, SchemaModification);
+
+ /// Specifies optimize_for attribute that will be set on the path
+ FLUENT_FIELD_OPTION(EOptimizeForAttr, OptimizeFor);
+
+ ///
+ /// @brief Do not put file used in operation into node cache
+ ///
+ /// If BypassArtifactCache == true, file will be loaded into the job's sandbox bypassing the cache on the YT node.
+ /// It helps jobs that use tmpfs to start faster,
+ /// because files will be loaded into tmpfs directly bypassing disk cache
+ FLUENT_FIELD_OPTION(bool, BypassArtifactCache);
+
+ ///
+ /// @brief Timestamp of dynamic table.
+ ///
+ /// NOTE: it is _not_ unix timestamp
+ /// (instead it's transaction timestamp, that is more complex structure).
+ FLUENT_FIELD_OPTION(i64, Timestamp);
+
+ ///
+ /// @brief Specify transaction that should be used to access this path.
+ ///
+ /// Allows to start cross-transactional operations.
+ FLUENT_FIELD_OPTION(TTransactionId, TransactionId);
+
+ using TRenameColumnsDescriptor = THashMap<TString, TString>;
+
+ /// Specifies columnar mapping which will be applied to columns before transfer to job.
+ FLUENT_FIELD_OPTION(TRenameColumnsDescriptor, RenameColumns);
+
+ /// Create empty path with no attributes
+ TRichYPath()
+ { }
+
+ ///
+ /// @{
+ ///
+ /// @brief Create path from string
+ TRichYPath(const char* path)
+ : Path_(path)
+ { }
+
+ TRichYPath(const TYPath& path)
+ : Path_(path)
+ { }
+ /// @}
+
+private:
+ TMaybe<TVector<TReadRange>> Ranges_;
+};
+
+///
+/// @ref Create copy of @ref NYT::TRichYPath with schema derived from proto message.
+///
+///
+template <typename TProtoType>
+TRichYPath WithSchema(const TRichYPath& path, const TSortColumns& sortBy = TSortColumns())
+{
+ static_assert(std::is_base_of_v<::google::protobuf::Message, TProtoType>, "TProtoType must be Protobuf message");
+
+ auto schemedPath = path;
+ if (!schemedPath.Schema_) {
+ schemedPath.Schema(CreateTableSchema<TProtoType>(sortBy));
+ }
+ return schemedPath;
+}
+
+///
+/// @brief Create copy of @ref NYT::TRichYPath with schema derived from TRowType if possible.
+///
+/// If TRowType is protobuf message schema is derived from it and set to returned path.
+/// Otherwise schema of original path is left unchanged (and probably unset).
+template <typename TRowType>
+TRichYPath MaybeWithSchema(const TRichYPath& path, const TSortColumns& sortBy = TSortColumns())
+{
+ if constexpr (std::is_base_of_v<::google::protobuf::Message, TRowType>) {
+ return WithSchema<TRowType>(path, sortBy);
+ } else {
+ return path;
+ }
+}
+
+///
+/// @brief Get the list of ranges related to path in compatibility mode.
+///
+/// - If path is missing ranges, empty list is returned.
+/// - If path has associated range list and the list is not empty, function returns this list.
+/// - If path has associated range list and this list is empty, exception is thrown.
+///
+/// Before YT-17683 RichYPath didn't support empty range list and empty range actualy meant universal range.
+/// This function emulates this old behavior.
+///
+/// @see https://st.yandex-team.ru/YT-17683
+const TVector<TReadRange>& GetRangesCompat(const TRichYPath& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Statistics about table columns.
+struct TTableColumnarStatistics
+{
+ /// Total data weight for all chunks for each of requested columns.
+ THashMap<TString, i64> ColumnDataWeight;
+
+ /// Total weight of all old chunks that don't keep columnar statistics.
+ i64 LegacyChunksDataWeight = 0;
+
+ /// Timestamps total weight (only for dynamic tables).
+ TMaybe<i64> TimestampTotalWeight;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Description of a partition.
+struct TMultiTablePartition
+{
+ struct TStatistics
+ {
+ i64 ChunkCount = 0;
+ i64 DataWeight = 0;
+ i64 RowCount = 0;
+ };
+
+ /// Ranges of input tables for this partition.
+ TVector<TRichYPath> TableRanges;
+
+ /// Aggregate statistics of all the table ranges in the partition.
+ TStatistics AggregateStatistics;
+};
+
+/// Table partitions from GetTablePartitions command.
+struct TMultiTablePartitions
+{
+ /// Disjoint partitions into which the input tables were divided.
+ TVector<TMultiTablePartition> Partitions;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Contains information about tablet
+///
+/// @see NYT::IClient::GetTabletInfos
+struct TTabletInfo
+{
+ ///
+ /// @brief Indicates the total number of rows added to the tablet (including trimmed ones).
+ ///
+ /// Currently only provided for ordered tablets.
+ i64 TotalRowCount = 0;
+
+ ///
+ /// @brief Contains the number of front rows that are trimmed and are not guaranteed to be accessible.
+ ///
+ /// Only makes sense for ordered tablet.
+ i64 TrimmedRowCount = 0;
+
+ ///
+ /// @brief Tablet cell barrier timestamp, which lags behind the current timestamp
+ ///
+ /// It is guaranteed that all transactions with commit timestamp not exceeding the barrier are fully committed;
+ /// e.g. all their added rows are visible (and are included in @ref NYT::TTabletInfo::TotalRowCount).
+ /// Mostly makes sense for ordered tablets.
+ ui64 BarrierTimestamp;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// List of attributes to retrieve in operations like @ref NYT::ICypressClient::Get
+struct TAttributeFilter
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TAttributeFilter;
+ /// @endcond
+
+ /// List of attributes.
+ FLUENT_VECTOR_FIELD(TString, Attribute);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Check if none of the fields of @ref NYT::TReadLimit is set.
+///
+/// @return true if any field of readLimit is set and false otherwise.
+bool IsTrivial(const TReadLimit& readLimit);
+
+/// Convert yson node type to table schema type
+EValueType NodeTypeToValueType(TNode::EType nodeType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Enumeration for specifying how reading from master is performed.
+///
+/// Used in operations like NYT::ICypressClient::Get
+enum class EMasterReadKind : int
+{
+ ///
+ /// @brief Reading from leader.
+ ///
+ /// Should almost never be used since it's expensive and for regular uses has no difference from
+ /// "follower" read.
+ Leader /* "leader" */,
+
+ /// @brief Reading from master follower (default).
+ Follower /* "follower" */,
+ Cache /* "cache" */,
+ MasterCache /* "master_cache" */,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @cond Doxygen_Suppress
+namespace NDetail {
+
+// MUST NOT BE USED BY CLIENTS
+// TODO: we should use default GENERATE_ENUM_SERIALIZATION
+TString ToString(EValueType type);
+
+} // namespace NDetail
+/// @endcond
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/common_ut.cpp b/yt/cpp/mapreduce/interface/common_ut.cpp
new file mode 100644
index 0000000000..3f19433816
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/common_ut.cpp
@@ -0,0 +1,303 @@
+#include "common_ut.h"
+
+#include "fluent.h"
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <util/generic/xrange.h>
+
+using namespace NYT;
+
+template <class T>
+TString SaveToString(const T& obj)
+{
+ TString s;
+ TStringOutput out(s);
+ ::Save(&out, obj);
+ return s;
+}
+
+template <class T>
+T LoadFromString(TStringBuf s)
+{
+ TMemoryInput in(s);
+ T obj;
+ ::Load(&in, obj);
+ return obj;
+}
+
+template <class T>
+T SaveLoad(const T& obj)
+{
+ return LoadFromString<T>(SaveToString(obj));
+}
+
+Y_UNIT_TEST_SUITE(Common)
+{
+ Y_UNIT_TEST(SortColumnsLegacy)
+ {
+ TSortColumns keys1("a", "b");
+ UNIT_ASSERT((keys1.Parts_ == TSortColumns{"a", "b"}));
+
+ keys1.Add("c", "d");
+ UNIT_ASSERT((keys1.Parts_ == TSortColumns{"a", "b", "c", "d"}));
+
+ auto keys2 = TSortColumns(keys1).Add("e", "f");
+ UNIT_ASSERT((keys1.Parts_ == TSortColumns{"a", "b", "c", "d"}));
+ UNIT_ASSERT((keys2.Parts_ == TSortColumns{"a", "b", "c", "d", "e", "f"}));
+
+ auto keys3 = TSortColumns(keys1).Add("e").Add("f").Add("g");
+ UNIT_ASSERT((keys1.Parts_ == TSortColumns{"a", "b", "c", "d"}));
+ UNIT_ASSERT((keys3.Parts_ == TSortColumns{"a", "b", "c", "d", "e", "f", "g"}));
+ }
+
+ Y_UNIT_TEST(SortColumn)
+ {
+ auto ascending = TSortColumn("a");
+ UNIT_ASSERT_VALUES_EQUAL(ascending.Name(), "a");
+ UNIT_ASSERT_VALUES_EQUAL(ascending.SortOrder(), ESortOrder::SO_ASCENDING);
+ UNIT_ASSERT_VALUES_EQUAL(ascending, TSortColumn("a", ESortOrder::SO_ASCENDING));
+ UNIT_ASSERT_VALUES_UNEQUAL(ascending, TSortColumn("a", ESortOrder::SO_DESCENDING));
+
+ UNIT_ASSERT_NO_EXCEPTION(ascending.EnsureAscending());
+ UNIT_ASSERT_VALUES_EQUAL(static_cast<TString>(ascending), "a");
+ UNIT_ASSERT_VALUES_EQUAL(ascending, "a");
+
+ auto another = ascending;
+ UNIT_ASSERT_NO_EXCEPTION(another = "another");
+ UNIT_ASSERT_VALUES_EQUAL(another.Name(), "another");
+ UNIT_ASSERT_VALUES_EQUAL(another.SortOrder(), ESortOrder::SO_ASCENDING);
+ UNIT_ASSERT_VALUES_EQUAL(another, TSortColumn("another", ESortOrder::SO_ASCENDING));
+ UNIT_ASSERT_VALUES_UNEQUAL(another, TSortColumn("another", ESortOrder::SO_DESCENDING));
+
+ auto ascendingNode = BuildYsonNodeFluently().Value(ascending);
+ UNIT_ASSERT_VALUES_EQUAL(ascendingNode, TNode("a"));
+
+ UNIT_ASSERT_VALUES_EQUAL(SaveLoad(ascending), ascending);
+ UNIT_ASSERT_VALUES_UNEQUAL(SaveToString(ascending), SaveToString(TString("a")));
+
+ auto descending = TSortColumn("a", ESortOrder::SO_DESCENDING);
+ UNIT_ASSERT_VALUES_EQUAL(descending.Name(), "a");
+ UNIT_ASSERT_VALUES_EQUAL(descending.SortOrder(), ESortOrder::SO_DESCENDING);
+ UNIT_ASSERT_VALUES_EQUAL(descending, TSortColumn("a", ESortOrder::SO_DESCENDING));
+ UNIT_ASSERT_VALUES_UNEQUAL(descending, TSortColumn("a", ESortOrder::SO_ASCENDING));
+
+ UNIT_ASSERT_EXCEPTION(descending.EnsureAscending(), yexception);
+ UNIT_ASSERT_EXCEPTION(static_cast<TString>(descending), yexception);
+ UNIT_ASSERT_EXCEPTION(descending == "a", yexception);
+ UNIT_ASSERT_EXCEPTION(descending = "a", yexception);
+
+ auto descendingNode = BuildYsonNodeFluently().Value(descending);
+ UNIT_ASSERT_VALUES_EQUAL(descendingNode, TNode()("name", "a")("sort_order", "descending"));
+
+ UNIT_ASSERT_VALUES_EQUAL(SaveLoad(descending), descending);
+ UNIT_ASSERT_VALUES_UNEQUAL(SaveToString(descending), SaveToString("a"));
+
+ UNIT_ASSERT_VALUES_EQUAL(ToString(TSortColumn("blah")), "blah");
+ UNIT_ASSERT_VALUES_EQUAL(ToString(TSortColumn("blah", ESortOrder::SO_DESCENDING)), "{\"name\"=\"blah\";\"sort_order\"=\"descending\"}");
+ }
+
+ Y_UNIT_TEST(SortColumns)
+ {
+ TSortColumns ascending("a", "b");
+ UNIT_ASSERT(ascending.Parts_ == (TSortColumns{"a", "b"}));
+ UNIT_ASSERT_NO_EXCEPTION(ascending.EnsureAscending());
+ UNIT_ASSERT_VALUES_EQUAL(static_cast<TColumnNames>(ascending).Parts_, (TVector<TString>{"a", "b"}));
+ UNIT_ASSERT_VALUES_EQUAL(ascending.GetNames(), (TVector<TString>{"a", "b"}));
+
+ auto mixed = ascending;
+ mixed.Add(TSortColumn("c", ESortOrder::SO_DESCENDING), "d");
+ UNIT_ASSERT((mixed.Parts_ != TVector<TSortColumn>{"a", "b", "c", "d"}));
+ UNIT_ASSERT((mixed.Parts_ == TVector<TSortColumn>{"a", "b", TSortColumn("c", ESortOrder::SO_DESCENDING), "d"}));
+ UNIT_ASSERT_VALUES_EQUAL(mixed.GetNames(), (TVector<TString>{"a", "b", "c", "d"}));
+ UNIT_ASSERT_EXCEPTION(mixed.EnsureAscending(), yexception);
+ UNIT_ASSERT_EXCEPTION(static_cast<TColumnNames>(mixed), yexception);
+ }
+
+ Y_UNIT_TEST(KeyBound)
+ {
+ auto keyBound = TKeyBound(ERelation::Greater, TKey(7, "a", TNode()("x", "y")));
+ UNIT_ASSERT_VALUES_EQUAL(keyBound.Relation(), ERelation::Greater);
+ UNIT_ASSERT_EQUAL(keyBound.Key(), TKey(7, "a", TNode()("x", "y")));
+
+ auto keyBound1 = TKeyBound().Relation(ERelation::Greater).Key(TKey(7, "a", TNode()("x", "y")));
+ auto expectedNode = TNode()
+ .Add(">")
+ .Add(TNode().Add(7).Add("a").Add(TNode()("x", "y")));
+
+ UNIT_ASSERT_VALUES_EQUAL(expectedNode, BuildYsonNodeFluently().Value(keyBound));
+ UNIT_ASSERT_VALUES_EQUAL(expectedNode, BuildYsonNodeFluently().Value(keyBound1));
+
+ keyBound.Relation(ERelation::LessOrEqual);
+ keyBound.Key(TKey("A", 7));
+ UNIT_ASSERT_VALUES_EQUAL(keyBound.Relation(), ERelation::LessOrEqual);
+ UNIT_ASSERT_EQUAL(keyBound.Key(), TKey("A", 7));
+
+ UNIT_ASSERT_VALUES_EQUAL(
+ BuildYsonNodeFluently().Value(keyBound),
+ TNode()
+ .Add("<=")
+ .Add(TNode().Add("A").Add(7)));
+ }
+
+ Y_UNIT_TEST(TTableSchema)
+ {
+ TTableSchema schema;
+ schema
+ .AddColumn(TColumnSchema().Name("a").Type(EValueType::VT_STRING).SortOrder(SO_ASCENDING))
+ .AddColumn(TColumnSchema().Name("b").Type(EValueType::VT_UINT64))
+ .AddColumn(TColumnSchema().Name("c").Type(EValueType::VT_INT64));
+ auto checkSortBy = [](TTableSchema schema, const TVector<TString>& columns) {
+ auto initialSchema = schema;
+ schema.SortBy(columns);
+ for (auto i: xrange(columns.size())) {
+ UNIT_ASSERT_VALUES_EQUAL(schema.Columns()[i].Name(), columns[i]);
+ UNIT_ASSERT_VALUES_EQUAL(schema.Columns()[i].SortOrder(), ESortOrder::SO_ASCENDING);
+ }
+ for (auto i: xrange(columns.size(), (size_t)initialSchema.Columns().size())) {
+ UNIT_ASSERT_VALUES_EQUAL(schema.Columns()[i].SortOrder(), Nothing());
+ }
+ UNIT_ASSERT_VALUES_EQUAL(initialSchema.Columns().size(), schema.Columns().size());
+ return schema;
+ };
+ auto newSchema = checkSortBy(schema, {"b"});
+ UNIT_ASSERT_VALUES_EQUAL(newSchema.Columns()[1].Name(), TString("a"));
+ UNIT_ASSERT_VALUES_EQUAL(newSchema.Columns()[2].Name(), TString("c"));
+ checkSortBy(schema, {"b", "c"});
+ checkSortBy(schema, {"c", "a"});
+ UNIT_ASSERT_EXCEPTION(checkSortBy(schema, {"b", "b"}), yexception);
+ UNIT_ASSERT_EXCEPTION(checkSortBy(schema, {"a", "junk"}), yexception);
+ }
+
+ Y_UNIT_TEST(TColumnSchema_TypeV3)
+ {
+ {
+ auto column = TColumnSchema().Type(NTi::Interval());
+ UNIT_ASSERT_VALUES_EQUAL(column.Required(), true);
+ UNIT_ASSERT_VALUES_EQUAL(column.Type(), VT_INTERVAL);
+ }
+ {
+ auto column = TColumnSchema().Type(NTi::Optional(NTi::Date()));
+ UNIT_ASSERT_VALUES_EQUAL(column.Required(), false);
+ UNIT_ASSERT_VALUES_EQUAL(column.Type(), VT_DATE);
+ }
+ {
+ auto column = TColumnSchema().Type(NTi::Null());
+ UNIT_ASSERT_VALUES_EQUAL(column.Required(), false);
+ UNIT_ASSERT_VALUES_EQUAL(column.Type(), VT_NULL);
+ }
+ {
+ auto column = TColumnSchema().Type(NTi::Optional(NTi::Null()));
+ UNIT_ASSERT_VALUES_EQUAL(column.Required(), false);
+ UNIT_ASSERT_VALUES_EQUAL(column.Type(), VT_ANY);
+ }
+ }
+
+ Y_UNIT_TEST(ToTypeV3)
+ {
+ UNIT_ASSERT_VALUES_EQUAL(*ToTypeV3(VT_INT32, true), *NTi::Int32());
+ UNIT_ASSERT_VALUES_EQUAL(*ToTypeV3(VT_UTF8, false), *NTi::Optional(NTi::Utf8()));
+ }
+
+ Y_UNIT_TEST(DeserializeColumn)
+ {
+ auto deserialize = [] (TStringBuf yson) {
+ auto node = NodeFromYsonString(yson);
+ TColumnSchema column;
+ Deserialize(column, node);
+ return column;
+ };
+
+ auto column = deserialize("{name=foo; type=int64; required=%false}");
+ UNIT_ASSERT_VALUES_EQUAL(column.Name(), "foo");
+ UNIT_ASSERT_VALUES_EQUAL(*column.TypeV3(), *NTi::Optional(NTi::Int64()));
+
+ column = deserialize("{name=bar; type=utf8; required=%true; type_v3=utf8}");
+ UNIT_ASSERT_VALUES_EQUAL(column.Name(), "bar");
+ UNIT_ASSERT_VALUES_EQUAL(*column.TypeV3(), *NTi::Utf8());
+ }
+
+ Y_UNIT_TEST(ColumnSchemaEquality)
+ {
+ auto base = TColumnSchema()
+ .Name("col")
+ .TypeV3(NTi::Optional(NTi::List(NTi::String())))
+ .SortOrder(ESortOrder::SO_ASCENDING)
+ .Lock("lock")
+ .Expression("x + 12")
+ .Aggregate("sum")
+ .Group("group");
+
+ auto other = base;
+ ASSERT_SERIALIZABLES_EQUAL(other, base);
+ other.Name("other");
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.TypeV3(NTi::List(NTi::String()));
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.ResetSortOrder();
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.Lock("lock1");
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.Expression("x + 13");
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.ResetAggregate();
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+
+ other = base;
+ other.Group("group1");
+ ASSERT_SERIALIZABLES_UNEQUAL(other, base);
+ }
+
+ Y_UNIT_TEST(TableSchemaEquality)
+ {
+ auto col1 = TColumnSchema()
+ .Name("col1")
+ .TypeV3(NTi::Optional(NTi::List(NTi::String())))
+ .SortOrder(ESortOrder::SO_ASCENDING);
+
+ auto col2 = TColumnSchema()
+ .Name("col2")
+ .TypeV3(NTi::Uint32());
+
+ auto schema = TTableSchema()
+ .AddColumn(col1)
+ .AddColumn(col2)
+ .Strict(true)
+ .UniqueKeys(true);
+
+ auto other = schema;
+ ASSERT_SERIALIZABLES_EQUAL(other, schema);
+
+ other.Strict(false);
+ ASSERT_SERIALIZABLES_UNEQUAL(other, schema);
+
+ other = schema;
+ other.MutableColumns()[0].TypeV3(NTi::List(NTi::String()));
+ ASSERT_SERIALIZABLES_UNEQUAL(other, schema);
+
+ other = schema;
+ other.MutableColumns().push_back(col1);
+ ASSERT_SERIALIZABLES_UNEQUAL(other, schema);
+
+ other = schema;
+ other.UniqueKeys(false);
+ ASSERT_SERIALIZABLES_UNEQUAL(other, schema);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/common_ut.h b/yt/cpp/mapreduce/interface/common_ut.h
new file mode 100644
index 0000000000..6f70f09bee
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/common_ut.h
@@ -0,0 +1 @@
+#pragma once
diff --git a/yt/cpp/mapreduce/interface/config.cpp b/yt/cpp/mapreduce/interface/config.cpp
new file mode 100644
index 0000000000..b474dc0844
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/config.cpp
@@ -0,0 +1,321 @@
+#include "config.h"
+
+#include "operation.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/json/json_reader.h>
+#include <library/cpp/svnversion/svnversion.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/yson/json/yson2json_adapter.h>
+
+#include <util/string/strip.h>
+#include <util/folder/dirut.h>
+#include <util/folder/path.h>
+#include <util/stream/file.h>
+#include <util/generic/singleton.h>
+#include <util/string/builder.h>
+#include <util/string/cast.h>
+#include <util/string/type.h>
+#include <util/system/hostname.h>
+#include <util/system/user.h>
+#include <util/system/env.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TConfig::GetBool(const char* var, bool defaultValue)
+{
+ TString val = GetEnv(var, "");
+ if (val.empty()) {
+ return defaultValue;
+ }
+ return IsTrue(val);
+}
+
+int TConfig::GetInt(const char* var, int defaultValue)
+{
+ int result = 0;
+ TString val = GetEnv(var, "");
+ if (val.empty()) {
+ return defaultValue;
+ }
+ try {
+ result = FromString<int>(val);
+ } catch (const yexception& e) {
+ ythrow yexception() << "Cannot parse " << var << '=' << val << " as integer: " << e.what();
+ }
+ return result;
+}
+
+TDuration TConfig::GetDuration(const char* var, TDuration defaultValue)
+{
+ return TDuration::Seconds(GetInt(var, defaultValue.Seconds()));
+}
+
+EEncoding TConfig::GetEncoding(const char* var)
+{
+ const TString encodingName = GetEnv(var, "identity");
+ EEncoding encoding;
+ if (TryFromString(encodingName, encoding)) {
+ return encoding;
+ } else {
+ ythrow yexception() << var << ": encoding '" << encodingName << "' is not supported";
+ }
+}
+
+ EUploadDeduplicationMode TConfig::GetUploadingDeduplicationMode(
+ const char* var,
+ EUploadDeduplicationMode defaultValue)
+{
+ const TString deduplicationMode = GetEnv(var, TEnumTraits<EUploadDeduplicationMode>::ToString(defaultValue));
+ return TEnumTraits<EUploadDeduplicationMode>::FromString(deduplicationMode);
+}
+
+void TConfig::ValidateToken(const TString& token)
+{
+ for (size_t i = 0; i < token.size(); ++i) {
+ ui8 ch = token[i];
+ if (ch < 0x21 || ch > 0x7e) {
+ ythrow yexception() << "Incorrect token character '" << ch << "' at position " << i;
+ }
+ }
+}
+
+TString TConfig::LoadTokenFromFile(const TString& tokenPath)
+{
+ TFsPath path(tokenPath);
+ return path.IsFile() ? Strip(TIFStream(path).ReadAll()) : TString();
+}
+
+TNode TConfig::LoadJsonSpec(const TString& strSpec)
+{
+ TNode spec;
+ TStringInput input(strSpec);
+ TNodeBuilder builder(&spec);
+ TYson2JsonCallbacksAdapter callbacks(&builder);
+
+ Y_ENSURE(NJson::ReadJson(&input, &callbacks), "Cannot parse json spec: " << strSpec);
+ Y_ENSURE(spec.IsMap(), "Json spec is not a map");
+
+ return spec;
+}
+
+TRichYPath TConfig::LoadApiFilePathOptions(const TString& ysonMap)
+{
+ TNode attributes;
+ try {
+ attributes = NodeFromYsonString(ysonMap);
+ } catch (const yexception& exc) {
+ ythrow yexception() << "Failed to parse YT_API_FILE_PATH_OPTIONS (it must be yson map): " << exc;
+ }
+ TNode pathNode = "";
+ pathNode.Attributes() = attributes;
+ TRichYPath path;
+ Deserialize(path, pathNode);
+ return path;
+}
+
+void TConfig::LoadToken()
+{
+ if (auto envToken = GetEnv("YT_TOKEN")) {
+ Token = envToken;
+ } else if (auto envToken = GetEnv("YT_SECURE_VAULT_YT_TOKEN")) {
+ // If this code runs inside an vanilla peration in YT
+ // it should not use regular environment variable `YT_TOKEN`
+ // because it would be visible in UI.
+ // Token should be passed via `secure_vault` parameter in operation spec.
+ Token = envToken;
+ } else if (auto tokenPath = GetEnv("YT_TOKEN_PATH")) {
+ Token = LoadTokenFromFile(tokenPath);
+ } else {
+ Token = LoadTokenFromFile(GetHomeDir() + "/.yt/token");
+ }
+ ValidateToken(Token);
+}
+
+void TConfig::LoadSpec()
+{
+ TString strSpec = GetEnv("YT_SPEC", "{}");
+ Spec = LoadJsonSpec(strSpec);
+
+ strSpec = GetEnv("YT_TABLE_WRITER", "{}");
+ TableWriter = LoadJsonSpec(strSpec);
+}
+
+void TConfig::LoadTimings()
+{
+ ConnectTimeout = GetDuration("YT_CONNECT_TIMEOUT",
+ TDuration::Seconds(10));
+
+ SocketTimeout = GetDuration("YT_SOCKET_TIMEOUT",
+ GetDuration("YT_SEND_RECEIVE_TIMEOUT", // common
+ TDuration::Seconds(60)));
+
+ AddressCacheExpirationTimeout = TDuration::Minutes(15);
+
+ CacheLockTimeoutPerGb = TDuration::MilliSeconds(1000.0 * 1_GB * 8 / 20_MB); // 20 Mbps = 20 MBps / 8.
+
+ TxTimeout = GetDuration("YT_TX_TIMEOUT",
+ TDuration::Seconds(120));
+
+ PingTimeout = GetDuration("YT_PING_TIMEOUT",
+ TDuration::Seconds(5));
+
+ PingInterval = GetDuration("YT_PING_INTERVAL",
+ TDuration::Seconds(5));
+
+ WaitLockPollInterval = TDuration::Seconds(5);
+
+ RetryInterval = GetDuration("YT_RETRY_INTERVAL",
+ TDuration::Seconds(3));
+
+ ChunkErrorsRetryInterval = GetDuration("YT_CHUNK_ERRORS_RETRY_INTERVAL",
+ TDuration::Seconds(60));
+
+ RateLimitExceededRetryInterval = GetDuration("YT_RATE_LIMIT_EXCEEDED_RETRY_INTERVAL",
+ TDuration::Seconds(60));
+
+ StartOperationRetryInterval = GetDuration("YT_START_OPERATION_RETRY_INTERVAL",
+ TDuration::Seconds(60));
+
+ HostListUpdateInterval = TDuration::Seconds(60);
+}
+
+void TConfig::Reset()
+{
+ Hosts = GetEnv("YT_HOSTS", "hosts");
+ Pool = GetEnv("YT_POOL");
+ Prefix = GetEnv("YT_PREFIX");
+ ApiVersion = GetEnv("YT_VERSION", "v3");
+ LogLevel = GetEnv("YT_LOG_LEVEL", "error");
+
+ ContentEncoding = GetEncoding("YT_CONTENT_ENCODING");
+ AcceptEncoding = GetEncoding("YT_ACCEPT_ENCODING");
+
+ GlobalTxId = GetEnv("YT_TRANSACTION", "");
+
+ UseAsyncTxPinger = false;
+ AsyncHttpClientThreads = 1;
+ AsyncTxPingerPoolThreads = 1;
+
+ ForceIpV4 = GetBool("YT_FORCE_IPV4");
+ ForceIpV6 = GetBool("YT_FORCE_IPV6");
+ UseHosts = GetBool("YT_USE_HOSTS", true);
+
+ LoadToken();
+ LoadSpec();
+ LoadTimings();
+
+ CacheUploadDeduplicationMode = GetUploadingDeduplicationMode("YT_UPLOAD_DEDUPLICATION", EUploadDeduplicationMode::Host);
+
+ RetryCount = Max(GetInt("YT_RETRY_COUNT", 10), 1);
+ ReadRetryCount = Max(GetInt("YT_READ_RETRY_COUNT", 30), 1);
+ StartOperationRetryCount = Max(GetInt("YT_START_OPERATION_RETRY_COUNT", 30), 1);
+
+ RemoteTempFilesDirectory = GetEnv("YT_FILE_STORAGE",
+ "//tmp/yt_wrapper/file_storage");
+ RemoteTempTablesDirectory = GetEnv("YT_TEMP_TABLES_STORAGE",
+ "//tmp/yt_wrapper/table_storage");
+ RemoteTempTablesDirectory = GetEnv("YT_TEMP_DIR",
+ RemoteTempTablesDirectory);
+
+ InferTableSchema = false;
+
+ UseClientProtobuf = GetBool("YT_USE_CLIENT_PROTOBUF", false);
+ NodeReaderFormat = ENodeReaderFormat::Auto;
+ ProtobufFormatWithDescriptors = true;
+
+ MountSandboxInTmpfs = GetBool("YT_MOUNT_SANDBOX_IN_TMPFS");
+
+ ApiFilePathOptions = LoadApiFilePathOptions(GetEnv("YT_API_FILE_PATH_OPTIONS", "{}"));
+
+ ConnectionPoolSize = GetInt("YT_CONNECTION_POOL_SIZE", 16);
+
+ TraceHttpRequestsMode = FromString<ETraceHttpRequestsMode>(to_lower(GetEnv("YT_TRACE_HTTP_REQUESTS", "never")));
+
+ CommandsWithFraming = {
+ "read_table",
+ "get_table_columnar_statistics",
+ "get_job_input",
+ "concatenate",
+ "partition_tables",
+ };
+}
+
+TConfig::TConfig()
+{
+ Reset();
+}
+
+TConfigPtr TConfig::Get()
+{
+ struct TConfigHolder
+ {
+ TConfigHolder()
+ : Config(::MakeIntrusive<TConfig>())
+ { }
+
+ TConfigPtr Config;
+ };
+
+ return Singleton<TConfigHolder>()->Config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProcessState::TProcessState()
+{
+ try {
+ FqdnHostName = ::FQDNHostName();
+ } catch (const yexception& e) {
+ try {
+ FqdnHostName = ::HostName();
+ } catch (const yexception& e) {
+ ythrow yexception() << "Cannot get fqdn and host name: " << e.what();
+ }
+ }
+
+ try {
+ UserName = ::GetUsername();
+ } catch (const yexception& e) {
+ ythrow yexception() << "Cannot get user name: " << e.what();
+ }
+
+ Pid = static_cast<int>(getpid());
+
+ if (!ClientVersion) {
+ ClientVersion = ::TStringBuilder() << "YT C++ native " << GetProgramCommitId();
+ }
+}
+
+static TString CensorString(TString input)
+{
+ static const TString prefix = "AQAD-";
+ if (input.find(prefix) == TString::npos) {
+ return input;
+ } else {
+ return TString(input.size(), '*');
+ }
+}
+
+void TProcessState::SetCommandLine(int argc, const char* argv[])
+{
+ for (int i = 0; i < argc; ++i) {
+ CommandLine.push_back(argv[i]);
+ CensoredCommandLine.push_back(CensorString(CommandLine.back()));
+ }
+}
+
+TProcessState* TProcessState::Get()
+{
+ return Singleton<TProcessState>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/config.h b/yt/cpp/mapreduce/interface/config.h
new file mode 100644
index 0000000000..c44ad25f1c
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/config.h
@@ -0,0 +1,228 @@
+#pragma once
+
+#include "fwd.h"
+#include "common.h"
+#include "node.h"
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <util/generic/maybe.h>
+#include <util/generic/string.h>
+#include <util/generic/hash_set.h>
+
+#include <util/datetime/base.h>
+
+namespace NYT {
+
+enum EEncoding : int
+{
+ E_IDENTITY /* "identity" */,
+ E_GZIP /* "gzip" */,
+ E_BROTLI /* "br" */,
+ E_Z_LZ4 /* "z-lz4" */,
+};
+
+enum class ENodeReaderFormat : int
+{
+ Yson, // Always use YSON format,
+ Skiff, // Always use Skiff format, throw exception if it's not possible (non-strict schema, dynamic table etc.)
+ Auto, // Use Skiff format if it's possible, YSON otherwise
+};
+
+enum class ETraceHttpRequestsMode
+{
+ // Never dump http requests.
+ Never /* "never" */,
+ // Dump failed http requests.
+ Error /* "error" */,
+ // Dump all http requests.
+ Always /* "always" */,
+};
+
+DEFINE_ENUM(EUploadDeduplicationMode,
+ // For each file only one process' thread from all possible hosts can upload it to the file cache at the same time.
+ // The others will wait for the uploading to finish and use already cached file.
+ ((Global) (0))
+
+ // For each file and each particular host only one process' thread can upload it to the file cache at the same time.
+ // The others will wait for the uploading to finish and use already cached file.
+ ((Host) (1))
+
+ // All processes' threads will upload a file to the cache concurrently.
+ ((Disabled) (2))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConfig
+ : public TThrRefBase
+{
+ TString Hosts;
+ TString Pool;
+ TString Token;
+ TString Prefix;
+ TString ApiVersion;
+ TString LogLevel;
+
+ // Compression for data that is sent to YT cluster.
+ EEncoding ContentEncoding;
+
+ // Compression for data that is read from YT cluster.
+ EEncoding AcceptEncoding;
+
+ TString GlobalTxId;
+
+ bool ForceIpV4;
+ bool ForceIpV6;
+ bool UseHosts;
+
+ TDuration HostListUpdateInterval;
+
+ TNode Spec;
+ TNode TableWriter;
+
+ TDuration ConnectTimeout;
+ TDuration SocketTimeout;
+ TDuration AddressCacheExpirationTimeout;
+ TDuration TxTimeout;
+ TDuration PingTimeout;
+ TDuration PingInterval;
+
+ bool UseAsyncTxPinger;
+ int AsyncHttpClientThreads;
+ int AsyncTxPingerPoolThreads;
+
+ // How often should we poll for lock state
+ TDuration WaitLockPollInterval;
+
+ TDuration RetryInterval;
+ TDuration ChunkErrorsRetryInterval;
+
+ TDuration RateLimitExceededRetryInterval;
+ TDuration StartOperationRetryInterval;
+
+ int RetryCount;
+ int ReadRetryCount;
+ int StartOperationRetryCount;
+
+ /// @brief Period for checking status of running operation.
+ TDuration OperationTrackerPollPeriod = TDuration::Seconds(5);
+
+ TString RemoteTempFilesDirectory;
+ TString RemoteTempTablesDirectory;
+
+ //
+ // Infer schemas for nonexstent tables from typed rows (e.g. protobuf)
+ // when writing from operation or client writer.
+ // This options can be overriden in TOperationOptions and TTableWriterOptions.
+ bool InferTableSchema;
+
+ bool UseClientProtobuf;
+ ENodeReaderFormat NodeReaderFormat;
+ bool ProtobufFormatWithDescriptors;
+
+ int ConnectionPoolSize;
+
+ /// Defines replication factor that is used for files that are uploaded to YT
+ /// to use them in operations.
+ int FileCacheReplicationFactor = 10;
+
+ /// @brief Used when waiting for other process which uploads the same file to the file cache.
+ ///
+ /// If CacheUploadDeduplicationMode is not Disabled, current process can wait for some other
+ /// process which is uploading the same file. This value is proportional to the timeout of waiting,
+ /// actual timeout computes as follows: fileSizeGb * CacheLockTimeoutPerGb.
+ /// Default timeout assumes that host has uploading speed equal to 20 Mb/s.
+ /// If timeout was reached, the file will be uploaded by current process without any other waits.
+ TDuration CacheLockTimeoutPerGb;
+
+ /// @brief Used to prevent concurrent uploading of the same file to the file cache.
+ /// NB: Each mode affects only users with the same mode enabled.
+ EUploadDeduplicationMode CacheUploadDeduplicationMode;
+
+ bool MountSandboxInTmpfs;
+
+ /// @brief Set upload options (e.g.) for files created by library.
+ ///
+ /// Path itself is always ignored but path options (e.g. `BypassArtifactCache`) are used when uploading system files:
+ /// cppbinary, job state, etc
+ TRichYPath ApiFilePathOptions;
+
+ // Testing options, should never be used in user programs.
+ bool UseAbortableResponse = false;
+ bool EnableDebugMetrics = false;
+
+ //
+ // There is optimization used with local YT that enables to skip binary upload and use real binary path.
+ // When EnableLocalModeOptimization is set to false this optimization is completely disabled.
+ bool EnableLocalModeOptimization = true;
+
+ //
+ // If you want see stderr even if you jobs not failed set this true.
+ bool WriteStderrSuccessfulJobs = false;
+
+ //
+ // This configuration is useful for debug.
+ // If set to ETraceHttpRequestsMode::Error library will dump all http error requests.
+ // If set to ETraceHttpRequestsMode::All library will dump all http requests.
+ // All tracing occurres as DEBUG level logging.
+ ETraceHttpRequestsMode TraceHttpRequestsMode = ETraceHttpRequestsMode::Never;
+
+ TString SkynetApiHost;
+
+ // Sets SO_PRIORITY option on the socket
+ TMaybe<int> SocketPriority;
+
+ // Framing settings
+ // (cf. https://yt.yandex-team.ru/docs/description/proxy/http_proxy_reference#framing).
+ THashSet<TString> CommandsWithFraming;
+
+ static bool GetBool(const char* var, bool defaultValue = false);
+ static int GetInt(const char* var, int defaultValue);
+ static TDuration GetDuration(const char* var, TDuration defaultValue);
+ static EEncoding GetEncoding(const char* var);
+ static EUploadDeduplicationMode GetUploadingDeduplicationMode(
+ const char* var,
+ EUploadDeduplicationMode defaultValue);
+
+ static void ValidateToken(const TString& token);
+ static TString LoadTokenFromFile(const TString& tokenPath);
+
+ static TNode LoadJsonSpec(const TString& strSpec);
+
+ static TRichYPath LoadApiFilePathOptions(const TString& ysonMap);
+
+ void LoadToken();
+ void LoadSpec();
+ void LoadTimings();
+
+ void Reset();
+
+ TConfig();
+
+ static TConfigPtr Get();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProcessState
+{
+ TString FqdnHostName;
+ TString UserName;
+ TVector<TString> CommandLine;
+
+ // Command line with everything that looks like tokens censored.
+ TVector<TString> CensoredCommandLine;
+ int Pid;
+ TString ClientVersion;
+
+ TProcessState();
+
+ void SetCommandLine(int argc, const char* argv[]);
+
+ static TProcessState* Get();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/config_ut.cpp b/yt/cpp/mapreduce/interface/config_ut.cpp
new file mode 100644
index 0000000000..e49ba02108
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/config_ut.cpp
@@ -0,0 +1,20 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+using namespace NYT;
+
+Y_UNIT_TEST_SUITE(ConfigSuite)
+{
+ Y_UNIT_TEST(TestReset) {
+ // very limited test, checks only one config field
+
+ auto origConfig = *TConfig::Get();
+ TConfig::Get()->Reset();
+ UNIT_ASSERT_VALUES_EQUAL(origConfig.Hosts, TConfig::Get()->Hosts);
+
+ TConfig::Get()->Hosts = "hosts/fb867";
+ TConfig::Get()->Reset();
+ UNIT_ASSERT_VALUES_EQUAL(origConfig.Hosts, TConfig::Get()->Hosts);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/constants.h b/yt/cpp/mapreduce/interface/constants.h
new file mode 100644
index 0000000000..4f70410814
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/constants.h
@@ -0,0 +1,19 @@
+#pragma once
+
+
+#include <util/system/defaults.h>
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+// Maximum number of input tables for operation.
+// If greater number of input tables are provided behaviour is undefined
+// (it might work ok or it might fail or it might work very slowly).
+constexpr size_t MaxInputTableCount = 1000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/cypress.cpp b/yt/cpp/mapreduce/interface/cypress.cpp
new file mode 100644
index 0000000000..53686effd2
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/cypress.cpp
@@ -0,0 +1,24 @@
+#include "cypress.h"
+
+#include "config.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ICypressClient::Concatenate(
+ const TVector<TYPath>& sourcePaths,
+ const TYPath& destinationPath,
+ const TConcatenateOptions& options)
+{
+ TVector<TRichYPath> richSourcePaths;
+ richSourcePaths.reserve(sourcePaths.size());
+ for (const auto& path : sourcePaths) {
+ richSourcePaths.emplace_back(path);
+ }
+ Concatenate(richSourcePaths, destinationPath, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/cypress.h b/yt/cpp/mapreduce/interface/cypress.h
new file mode 100644
index 0000000000..e05316ebc6
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/cypress.h
@@ -0,0 +1,252 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/cypress.h
+///
+/// Header containing interface to execute [Cypress](https://yt.yandex-team.ru/docs/description/common/cypress.html)-related commands.
+
+#include "fwd.h"
+
+#include "client_method_options.h"
+#include "common.h"
+#include "node.h"
+
+#include <util/generic/maybe.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Client interface to execute [Cypress](https://yt.yandex-team.ru/docs/description/common/cypress.html)-related commands.
+class ICypressClient
+{
+public:
+ virtual ~ICypressClient() = default;
+
+ ///
+ /// @brief Create Cypress node of given type.
+ ///
+ /// @param path Path in Cypress to the new object.
+ /// @param type New node type.
+ /// @param options Optional parameters.
+ ///
+ /// @return Id of the created node.
+ ///
+ /// @note All but the last components must exist unless @ref NYT::TCreateOptions::Recursive is `true`.
+ ///
+ /// @note The node itself must not exist unless @ref NYT::TCreateOptions::IgnoreExisting or @ref NYT::TCreateOptions::Force are `true`.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#create)
+ virtual TNodeId Create(
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options = TCreateOptions()) = 0;
+
+ ///
+ /// @brief Create table with schema inferred from the template argument.
+ ///
+ /// @tparam TRowType type of C++ representation of the row to be stored in the table.
+ /// @param path Path in Cypress to the new table.
+ /// @param sortColumns List of columns to mark as sorted in schema.
+ /// @param options Optional parameters.
+ ///
+ /// @return Id of the created node.
+ ///
+ /// @note If "schema" is passed in `options.Attributes` it has priority over the deduced schema (the latter is ignored).
+ template <typename TRowType>
+ TNodeId CreateTable(
+ const TYPath& path,
+ const TSortColumns& sortColumns = TSortColumns(),
+ const TCreateOptions& options = TCreateOptions());
+
+ ///
+ /// @brief Remove Cypress node.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#remove)
+ virtual void Remove(
+ const TYPath& path,
+ const TRemoveOptions& options = TRemoveOptions()) = 0;
+
+ ///
+ /// @brief Check if Cypress node exists.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#exists)
+ virtual bool Exists(
+ const TYPath& path,
+ const TExistsOptions& options = TExistsOptions()) = 0;
+
+ ///
+ /// @brief Get Cypress node contents.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get)
+ virtual TNode Get(
+ const TYPath& path,
+ const TGetOptions& options = TGetOptions()) = 0;
+
+ ///
+ /// @brief Set Cypress node contents.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#set)
+ virtual void Set(
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options = TSetOptions()) = 0;
+
+ ///
+ /// @brief Set multiple attributes for cypress path.
+ ///
+ /// @param path Path to root of the attributes to be set e.g. "//path/to/table/@";
+ /// it is important to make sure that path ends with "/@".
+ /// @param attributes Map with attributes
+ /// @param options Optional parameters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#multiset_attributes)
+ virtual void MultisetAttributes(
+ const TYPath& path,
+ const TNode::TMapType& attributes,
+ const TMultisetAttributesOptions& options = TMultisetAttributesOptions()) = 0;
+
+ ///
+ /// @brief List Cypress map or attribute node keys.
+ ///
+ /// @param path Path in the tree to the node in question.
+ /// @param options Optional parameters.
+ ///
+ /// @return List of keys with attributes (if they were required in @ref NYT::TListOptions::AttributeFilter).
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#list)
+ virtual TNode::TListType List(
+ const TYPath& path,
+ const TListOptions& options = TListOptions()) = 0;
+
+ ///
+ /// @brief Copy Cypress node.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#copy)
+ virtual TNodeId Copy(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options = TCopyOptions()) = 0;
+
+ ///
+ /// @brief Move Cypress node (equivalent to copy-then-remove).
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#move)
+ virtual TNodeId Move(
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options = TMoveOptions()) = 0;
+
+ ///
+ /// @brief Create link to Cypress node.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#link)
+ virtual TNodeId Link(
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options = TLinkOptions()) = 0;
+
+ ///
+ /// @brief Concatenate several tables into one.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#concatenate)
+ virtual void Concatenate(
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options = TConcatenateOptions()) = 0;
+
+ ///
+ /// @brief Concatenate several tables into one.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#concatenate)
+ virtual void Concatenate(
+ const TVector<TYPath>& sourcePaths,
+ const TYPath& destinationPath,
+ const TConcatenateOptions& options = TConcatenateOptions());
+
+ ///
+ /// @brief Canonize YPath, moving all the complex YPath features to attributes.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#parse-ypath)
+ virtual TRichYPath CanonizeYPath(const TRichYPath& path) = 0;
+
+ ///
+ /// @brief Get statistics for given sets of columns in given table ranges.
+ ///
+ /// @note Paths must contain column selectors.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-table-columnar-statistics)
+ virtual TVector<TTableColumnarStatistics> GetTableColumnarStatistics(
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options = {}) = 0;
+
+ ///
+ /// @brief Divide input tables into disjoint partitions.
+ ///
+ /// Resulted partitions are vectors of rich YPaths.
+ /// Each partition can be given to a separate worker for further independent processing.
+ ///
+ virtual TMultiTablePartitions GetTablePartitions(
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options) = 0;
+
+ ///
+ /// @brief Get file from file cache.
+ ///
+ /// @param md5Signature MD5 digest of the file.
+ /// @param cachePath Path to the file cache.
+ /// @param options Optional parameters.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#get-file-from-cache)
+ virtual TMaybe<TYPath> GetFileFromCache(
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options = TGetFileFromCacheOptions()) = 0;
+
+ ///
+ /// @brief Put file to file cache.
+ ///
+ /// @param filePath Path in Cypress to the file to cache.
+ /// @param md5Signature Expected MD5 digest of the file.
+ /// @param cachePath Path to the file cache.
+ /// @param options Optional parameters.
+ ///
+ /// @note The file in `filePath` must have been written with @ref NYT::TFileWriterOptions::ComputeMD5 set to `true`.
+ ///
+ /// @see [YT doc](https://yt.yandex-team.ru/docs/api/commands.html#put-file-to-cache)
+ virtual TYPath PutFileToCache(
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options = TPutFileToCacheOptions()) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TRowType>
+TNodeId ICypressClient::CreateTable(
+ const TYPath& path,
+ const TSortColumns& sortColumns,
+ const TCreateOptions& options)
+{
+ static_assert(
+ std::is_base_of_v<::google::protobuf::Message, TRowType>,
+ "TRowType must be inherited from google::protobuf::Message");
+
+ TCreateOptions actualOptions = options;
+ if (!actualOptions.Attributes_) {
+ actualOptions.Attributes_ = TNode::CreateMap();
+ }
+
+ if (!actualOptions.Attributes_->HasKey("schema")) {
+ actualOptions.Attributes_->AsMap().emplace(
+ "schema",
+ CreateTableSchema<TRowType>(sortColumns).ToNode());
+ }
+
+ return Create(path, ENodeType::NT_TABLE, actualOptions);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/error_codes.h b/yt/cpp/mapreduce/interface/error_codes.h
new file mode 100644
index 0000000000..d8d76e04fd
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/error_codes.h
@@ -0,0 +1,468 @@
+#pragma once
+
+//
+// generated by generate-error-codes.py
+//
+
+namespace NYT {
+namespace NClusterErrorCodes {
+
+
+
+// from ./core/misc/public.h
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int OK = 0;
+ constexpr int Generic = 1;
+ constexpr int Canceled = 2;
+ constexpr int Timeout = 3;
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+
+
+// from ./core/rpc/public.h
+namespace NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int TransportError = 100;
+ constexpr int ProtocolError = 101;
+ constexpr int NoSuchService = 102;
+ constexpr int NoSuchMethod = 103;
+ constexpr int Unavailable = 105;
+ constexpr int PoisonPill = 106;
+ constexpr int RequestQueueSizeLimitExceeded = 108;
+ constexpr int AuthenticationError = 109;
+ constexpr int InvalidCsrfToken = 110;
+ constexpr int InvalidCredentials = 111;
+ constexpr int StreamingNotSupported = 112;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NRpc
+
+
+
+// from ./core/bus/public.h
+namespace NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int TransportError = 100;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NBus
+
+
+
+// from ./client/scheduler/public.h
+namespace NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int NoSuchOperation = 200;
+ constexpr int InvalidOperationState = 201;
+ constexpr int TooManyOperations = 202;
+ constexpr int NoSuchJob = 203;
+ constexpr int OperationFailedOnJobRestart = 210;
+ constexpr int OperationFailedWithInconsistentLocking = 211;
+ constexpr int OperationControllerCrashed = 212;
+ constexpr int TestingError = 213;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NScheduler
+
+
+
+// from ./client/table_client/public.h
+namespace NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int SortOrderViolation = 301;
+ constexpr int InvalidDoubleValue = 302;
+ constexpr int IncomparableType = 303;
+ constexpr int UnhashableType = 304;
+ // E.g. name table with more than #MaxColumnId columns (may come from legacy chunks).
+ constexpr int CorruptedNameTable = 305;
+ constexpr int UniqueKeyViolation = 306;
+ constexpr int SchemaViolation = 307;
+ constexpr int RowWeightLimitExceeded = 308;
+ constexpr int InvalidColumnFilter = 309;
+ constexpr int InvalidColumnRenaming = 310;
+ constexpr int IncompatibleKeyColumns = 311;
+ constexpr int ReaderDeadlineExpired = 312;
+ constexpr int TimestampOutOfRange = 313;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NTableClient
+
+
+
+// from ./client/cypress_client/public.h
+namespace NCypressClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int SameTransactionLockConflict = 400;
+ constexpr int DescendantTransactionLockConflict = 401;
+ constexpr int ConcurrentTransactionLockConflict = 402;
+ constexpr int PendingLockConflict = 403;
+ constexpr int LockDestroyed = 404;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NCypressClient
+
+
+
+// from ./core/ytree/public.h
+namespace NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int ResolveError = 500;
+ constexpr int AlreadyExists = 501;
+ constexpr int MaxChildCountViolation = 502;
+ constexpr int MaxStringLengthViolation = 503;
+ constexpr int MaxAttributeSizeViolation = 504;
+ constexpr int MaxKeyLengthViolation = 505;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYTree
+
+
+
+// from ./client/hydra/public.h
+namespace NHydra {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int NoSuchSnapshot = 600;
+ constexpr int NoSuchChangelog = 601;
+ constexpr int InvalidEpoch = 602;
+ constexpr int InvalidVersion = 603;
+ constexpr int OutOfOrderMutations = 609;
+ constexpr int InvalidSnapshotVersion = 610;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NHydra
+
+
+
+// from ./client/chunk_client/public.h
+namespace NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int AllTargetNodesFailed = 700;
+ constexpr int SendBlocksFailed = 701;
+ constexpr int NoSuchSession = 702;
+ constexpr int SessionAlreadyExists = 703;
+ constexpr int ChunkAlreadyExists = 704;
+ constexpr int WindowError = 705;
+ constexpr int BlockContentMismatch = 706;
+ constexpr int NoSuchBlock = 707;
+ constexpr int NoSuchChunk = 708;
+ constexpr int NoLocationAvailable = 710;
+ constexpr int IOError = 711;
+ constexpr int MasterCommunicationFailed = 712;
+ constexpr int NoSuchChunkTree = 713;
+ constexpr int MasterNotConnected = 714;
+ constexpr int ChunkUnavailable = 716;
+ constexpr int NoSuchChunkList = 717;
+ constexpr int WriteThrottlingActive = 718;
+ constexpr int NoSuchMedium = 719;
+ constexpr int OptimisticLockFailure = 720;
+ constexpr int InvalidBlockChecksum = 721;
+ constexpr int BlockOutOfRange = 722;
+ constexpr int ObjectNotReplicated = 723;
+ constexpr int MissingExtension = 724;
+ constexpr int BandwidthThrottlingFailed = 725;
+ constexpr int ReaderTimeout = 726;
+ constexpr int NoSuchChunkView = 727;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NChunkClient
+
+
+
+// from ./client/election/public.h
+namespace NElection {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int InvalidState = 800;
+ constexpr int InvalidLeader = 801;
+ constexpr int InvalidEpoch = 802;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NElection
+
+
+
+// from ./client/security_client/public.h
+namespace NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int AuthenticationError = 900;
+ constexpr int AuthorizationError = 901;
+ constexpr int AccountLimitExceeded = 902;
+ constexpr int UserBanned = 903;
+ constexpr int RequestQueueSizeLimitExceeded = 904;
+ constexpr int NoSuchAccount = 905;
+ constexpr int SafeModeEnabled = 906;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NSecurityClient
+
+
+
+// from ./client/object_client/public.h
+namespace NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int PrerequisiteCheckFailed = 1000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NObjectClient
+
+
+
+// from ./server/lib/exec_agent/public.h
+namespace NExecAgent {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int ConfigCreationFailed = 1100;
+ constexpr int AbortByScheduler = 1101;
+ constexpr int ResourceOverdraft = 1102;
+ constexpr int WaitingJobTimeout = 1103;
+ constexpr int SlotNotFound = 1104;
+ constexpr int JobEnvironmentDisabled = 1105;
+ constexpr int JobProxyConnectionFailed = 1106;
+ constexpr int ArtifactCopyingFailed = 1107;
+ constexpr int NodeDirectoryPreparationFailed = 1108;
+ constexpr int SlotLocationDisabled = 1109;
+ constexpr int QuotaSettingFailed = 1110;
+ constexpr int RootVolumePreparationFailed = 1111;
+ constexpr int NotEnoughDiskSpace = 1112;
+ constexpr int ArtifactDownloadFailed = 1113;
+ constexpr int JobProxyPreparationTimeout = 1114;
+ constexpr int JobPreparationTimeout = 1115;
+ constexpr int JobProxyFailed = 1120;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NExecAgent
+
+
+
+// from ./ytlib/job_proxy/public.h
+namespace NJobProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int MemoryLimitExceeded = 1200;
+ constexpr int MemoryCheckFailed = 1201;
+ constexpr int JobTimeLimitExceeded = 1202;
+ constexpr int UnsupportedJobType = 1203;
+ constexpr int JobNotPrepared = 1204;
+ constexpr int UserJobFailed = 1205;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NJobProxy
+
+
+
+// from ./server/node/data_node/public.h
+namespace NDataNode {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int LocalChunkReaderFailed = 1300;
+ constexpr int LayerUnpackingFailed = 1301;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDataNode
+
+
+
+// from ./core/net/public.h
+namespace NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int Aborted = 1500;
+ constexpr int ResolveTimedOut = 1501;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NNet
+
+
+
+// from ./client/node_tracker_client/public.h
+namespace NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int NoSuchNode = 1600;
+ constexpr int InvalidState = 1601;
+ constexpr int NoSuchNetwork = 1602;
+ constexpr int NoSuchRack = 1603;
+ constexpr int NoSuchDataCenter = 1604;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NNodeTrackerClient
+
+
+
+// from ./client/tablet_client/public.h
+namespace NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int TransactionLockConflict = 1700;
+ constexpr int NoSuchTablet = 1701;
+ constexpr int TabletNotMounted = 1702;
+ constexpr int AllWritesDisabled = 1703;
+ constexpr int InvalidMountRevision = 1704;
+ constexpr int TableReplicaAlreadyExists = 1705;
+ constexpr int InvalidTabletState = 1706;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NTabletClient
+
+
+
+// from ./server/lib/shell/public.h
+namespace NShell {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int ShellExited = 1800;
+ constexpr int ShellManagerShutDown = 1801;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NShell
+
+
+
+// from ./client/api/public.h
+namespace NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int TooManyConcurrentRequests = 1900;
+ constexpr int JobArchiveUnavailable = 1910;
+ constexpr int RetriableArchiveError = 1911;
+ constexpr int NoSuchOperation = 1915;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NApi
+
+
+
+// from ./server/controller_agent/chunk_pools/public.h
+namespace NChunkPools {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int DataSliceLimitExceeded = 2000;
+ constexpr int MaxDataWeightPerJobExceeded = 2001;
+ constexpr int MaxPrimaryDataWeightPerJobExceeded = 2002;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NChunkPools
+
+
+
+// from ./client/api/rpc_proxy/public.h
+namespace NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int ProxyBanned = 2100;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NApi
+
+
+
+// from ./ytlib/controller_agent/public.h
+namespace NControllerAgent {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int AgentCallFailed = 4400;
+ constexpr int NoOnlineNodeToScheduleJob = 4410;
+ constexpr int MaterializationFailed = 4415;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NControllerAgent
+
+
+
+// from ./client/transaction_client/public.h
+namespace NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int NoSuchTransaction = 11000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NTransactionClient
+
+
+
+// from ./server/lib/containers/public.h
+namespace NContainers {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int FailedToStartContainer = 13000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NContainers
+
+
+
+// from ./ytlib/job_prober_client/public.h
+namespace NJobProberClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ constexpr int JobIsNotRunning = 17000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NJobProberClient
+
+} // namespace NClusterErrorCodes
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/error_ut.cpp b/yt/cpp/mapreduce/interface/error_ut.cpp
new file mode 100644
index 0000000000..03f2751b23
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/error_ut.cpp
@@ -0,0 +1,81 @@
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <library/cpp/json/json_reader.h>
+
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+using namespace NYT;
+
+template<>
+void Out<NYT::TNode>(IOutputStream& s, const NYT::TNode& node)
+{
+ s << "TNode:" << NodeToYsonString(node);
+}
+
+Y_UNIT_TEST_SUITE(ErrorSuite)
+{
+ Y_UNIT_TEST(TestParseJson)
+ {
+ // Scary real world error! Бу!
+ const char* jsonText =
+ R"""({)"""
+ R"""("code":500,)"""
+ R"""("message":"Error resolving path //home/user/link",)"""
+ R"""("attributes":{)"""
+ R"""("fid":18446484571700269066,)"""
+ R"""("method":"Create",)"""
+ R"""("tid":17558639495721339338,)"""
+ R"""("datetime":"2017-04-07T13:38:56.474819Z",)"""
+ R"""("pid":414529,)"""
+ R"""("host":"build01-01g.yt.yandex.net"},)"""
+ R"""("inner_errors":[{)"""
+ R"""("code":1,)"""
+ R"""("message":"Node //tt cannot have children",)"""
+ R"""("attributes":{)"""
+ R"""("fid":18446484571700269066,)"""
+ R"""("tid":17558639495721339338,)"""
+ R"""("datetime":"2017-04-07T13:38:56.474725Z",)"""
+ R"""("pid":414529,)"""
+ R"""("host":"build01-01g.yt.yandex.net"},)"""
+ R"""("inner_errors":[]}]})""";
+
+ NJson::TJsonValue jsonValue;
+ ReadJsonFastTree(jsonText, &jsonValue, /*throwOnError=*/ true);
+
+ TYtError error(jsonValue);
+ UNIT_ASSERT_VALUES_EQUAL(error.GetCode(), 500);
+ UNIT_ASSERT_VALUES_EQUAL(error.GetMessage(), R"""(Error resolving path //home/user/link)""");
+ UNIT_ASSERT_VALUES_EQUAL(error.InnerErrors().size(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(error.InnerErrors()[0].GetCode(), 1);
+
+ UNIT_ASSERT_VALUES_EQUAL(error.HasAttributes(), true);
+ UNIT_ASSERT_VALUES_EQUAL(error.GetAttributes().at("method"), TNode("Create"));
+
+ UNIT_ASSERT_VALUES_EQUAL(error.GetAllErrorCodes(), TSet<int>({500, 1}));
+ }
+
+ Y_UNIT_TEST(TestGetYsonText) {
+ const char* jsonText =
+ R"""({)"""
+ R"""("code":500,)"""
+ R"""("message":"outer error",)"""
+ R"""("attributes":{)"""
+ R"""("method":"Create",)"""
+ R"""("pid":414529},)"""
+ R"""("inner_errors":[{)"""
+ R"""("code":1,)"""
+ R"""("message":"inner error",)"""
+ R"""("attributes":{},)"""
+ R"""("inner_errors":[])"""
+ R"""(}]})""";
+ TYtError error;
+ error.ParseFrom(jsonText);
+ TString ysonText = error.GetYsonText();
+ TYtError error2(NodeFromYsonString(ysonText));
+ UNIT_ASSERT_EQUAL(
+ ysonText,
+ R"""({"code"=500;"message"="outer error";"attributes"={"method"="Create";"pid"=414529};"inner_errors"=[{"code"=1;"message"="inner error"}]})""");
+ UNIT_ASSERT_EQUAL(error2.GetYsonText(), ysonText);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/errors.cpp b/yt/cpp/mapreduce/interface/errors.cpp
new file mode 100644
index 0000000000..49a7c7cfc1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/errors.cpp
@@ -0,0 +1,437 @@
+#include "errors.h"
+
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node_visitor.h>
+
+#include <yt/cpp/mapreduce/interface/error_codes.h>
+
+#include <library/cpp/json/json_reader.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/string/builder.h>
+#include <util/stream/str.h>
+#include <util/generic/set.h>
+
+namespace NYT {
+
+using namespace NJson;
+
+////////////////////////////////////////////////////////////////////
+
+static void WriteErrorDescription(const TYtError& error, IOutputStream* out)
+{
+ (*out) << '\'' << error.GetMessage() << '\'';
+ const auto& innerErrorList = error.InnerErrors();
+ if (!innerErrorList.empty()) {
+ (*out) << " { ";
+ bool first = true;
+ for (const auto& innerError : innerErrorList) {
+ if (first) {
+ first = false;
+ } else {
+ (*out) << " ; ";
+ }
+ WriteErrorDescription(innerError, out);
+ }
+ (*out) << " }";
+ }
+}
+
+static void SerializeError(const TYtError& error, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ {
+ consumer->OnKeyedItem("code");
+ consumer->OnInt64Scalar(error.GetCode());
+
+ consumer->OnKeyedItem("message");
+ consumer->OnStringScalar(error.GetMessage());
+
+ if (!error.GetAttributes().empty()) {
+ consumer->OnKeyedItem("attributes");
+ consumer->OnBeginMap();
+ {
+ for (const auto& item : error.GetAttributes()) {
+ consumer->OnKeyedItem(item.first);
+ TNodeVisitor(consumer).Visit(item.second);
+ }
+ }
+ consumer->OnEndMap();
+ }
+
+ if (!error.InnerErrors().empty()) {
+ consumer->OnKeyedItem("inner_errors");
+ {
+ consumer->OnBeginList();
+ for (const auto& innerError : error.InnerErrors()) {
+ SerializeError(innerError, consumer);
+ }
+ consumer->OnEndList();
+ }
+ }
+ }
+ consumer->OnEndMap();
+}
+
+static TString DumpJobInfoForException(const TOperationId& operationId, const TVector<TFailedJobInfo>& failedJobInfoList)
+{
+ ::TStringBuilder output;
+ // Exceptions have limit to contain 65508 bytes of text, so we also limit stderr text
+ constexpr size_t MAX_SIZE = 65508 / 2;
+
+ size_t written = 0;
+ for (const auto& failedJobInfo : failedJobInfoList) {
+ if (written >= MAX_SIZE) {
+ break;
+ }
+ TStringStream nextChunk;
+ nextChunk << '\n';
+ nextChunk << "OperationId: " << GetGuidAsString(operationId) << " JobId: " << GetGuidAsString(failedJobInfo.JobId) << '\n';
+ nextChunk << "Error: " << failedJobInfo.Error.FullDescription() << '\n';
+ if (!failedJobInfo.Stderr.empty()) {
+ nextChunk << "Stderr: " << Endl;
+ size_t tmpWritten = written + nextChunk.Str().size();
+ if (tmpWritten >= MAX_SIZE) {
+ break;
+ }
+
+ if (tmpWritten + failedJobInfo.Stderr.size() > MAX_SIZE) {
+ nextChunk << failedJobInfo.Stderr.substr(failedJobInfo.Stderr.size() - (MAX_SIZE - tmpWritten));
+ } else {
+ nextChunk << failedJobInfo.Stderr;
+ }
+ }
+ written += nextChunk.Str().size();
+ output << nextChunk.Str();
+ }
+ return output;
+}
+
+////////////////////////////////////////////////////////////////////
+
+TYtError::TYtError()
+ : Code_(0)
+{ }
+
+TYtError::TYtError(const TString& message)
+ : Code_(NYT::NClusterErrorCodes::Generic)
+ , Message_(message)
+{ }
+
+TYtError::TYtError(int code, const TString& message)
+ : Code_(code)
+ , Message_(message)
+{ }
+
+TYtError::TYtError(const TJsonValue& value)
+{
+ const TJsonValue::TMapType& map = value.GetMap();
+ TJsonValue::TMapType::const_iterator it = map.find("message");
+ if (it != map.end()) {
+ Message_ = it->second.GetString();
+ }
+
+ it = map.find("code");
+ if (it != map.end()) {
+ Code_ = static_cast<int>(it->second.GetInteger());
+ } else {
+ Code_ = NYT::NClusterErrorCodes::Generic;
+ }
+
+ it = map.find("inner_errors");
+ if (it != map.end()) {
+ const TJsonValue::TArray& innerErrors = it->second.GetArray();
+ for (const auto& innerError : innerErrors) {
+ InnerErrors_.push_back(TYtError(innerError));
+ }
+ }
+
+ it = map.find("attributes");
+ if (it != map.end()) {
+ auto attributes = NYT::NodeFromJsonValue(it->second);
+ if (attributes.IsMap()) {
+ Attributes_ = std::move(attributes.AsMap());
+ }
+ }
+}
+
+TYtError::TYtError(const TNode& node)
+{
+ const auto& map = node.AsMap();
+ auto it = map.find("message");
+ if (it != map.end()) {
+ Message_ = it->second.AsString();
+ }
+
+ it = map.find("code");
+ if (it != map.end()) {
+ Code_ = static_cast<int>(it->second.AsInt64());
+ } else {
+ Code_ = NYT::NClusterErrorCodes::Generic;
+ }
+
+ it = map.find("inner_errors");
+ if (it != map.end()) {
+ const auto& innerErrors = it->second.AsList();
+ for (const auto& innerError : innerErrors) {
+ InnerErrors_.push_back(TYtError(innerError));
+ }
+ }
+
+ it = map.find("attributes");
+ if (it != map.end()) {
+ auto& attributes = it->second;
+ if (attributes.IsMap()) {
+ Attributes_ = std::move(attributes.AsMap());
+ }
+ }
+}
+
+int TYtError::GetCode() const
+{
+ return Code_;
+}
+
+const TString& TYtError::GetMessage() const
+{
+ return Message_;
+}
+
+const TVector<TYtError>& TYtError::InnerErrors() const
+{
+ return InnerErrors_;
+}
+
+void TYtError::ParseFrom(const TString& jsonError)
+{
+ TJsonValue value;
+ TStringInput input(jsonError);
+ ReadJsonTree(&input, &value);
+ *this = TYtError(value);
+}
+
+TSet<int> TYtError::GetAllErrorCodes() const
+{
+ TDeque<const TYtError*> queue = {this};
+ TSet<int> result;
+ while (!queue.empty()) {
+ const auto* current = queue.front();
+ queue.pop_front();
+ result.insert(current->Code_);
+ for (const auto& error : current->InnerErrors_) {
+ queue.push_back(&error);
+ }
+ }
+ return result;
+}
+
+bool TYtError::ContainsErrorCode(int code) const
+{
+ if (Code_ == code) {
+ return true;
+ }
+ for (const auto& error : InnerErrors_) {
+ if (error.ContainsErrorCode(code)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+bool TYtError::ContainsText(const TStringBuf& text) const
+{
+ if (Message_.Contains(text)) {
+ return true;
+ }
+ for (const auto& error : InnerErrors_) {
+ if (error.ContainsText(text)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TYtError::HasAttributes() const
+{
+ return !Attributes_.empty();
+}
+
+const TNode::TMapType& TYtError::GetAttributes() const
+{
+ return Attributes_;
+}
+
+TString TYtError::GetYsonText() const
+{
+ TStringStream out;
+ ::NYson::TYsonWriter writer(&out, NYson::EYsonFormat::Text);
+ SerializeError(*this, &writer);
+ return std::move(out.Str());
+}
+
+TString TYtError::ShortDescription() const
+{
+ TStringStream out;
+ WriteErrorDescription(*this, &out);
+ return std::move(out.Str());
+}
+
+TString TYtError::FullDescription() const
+{
+ TStringStream s;
+ WriteErrorDescription(*this, &s);
+ s << "; full error: " << GetYsonText();
+ return s.Str();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TErrorResponse::TErrorResponse(int httpCode, const TString& requestId)
+ : HttpCode_(httpCode)
+ , RequestId_(requestId)
+{ }
+
+bool TErrorResponse::IsOk() const
+{
+ return Error_.GetCode() == 0;
+}
+
+void TErrorResponse::SetRawError(const TString& message)
+{
+ Error_ = TYtError(message);
+ Setup();
+}
+
+void TErrorResponse::SetError(TYtError error)
+{
+ Error_ = std::move(error);
+ Setup();
+}
+
+void TErrorResponse::ParseFromJsonError(const TString& jsonError)
+{
+ Error_.ParseFrom(jsonError);
+ Setup();
+}
+
+void TErrorResponse::SetIsFromTrailers(bool isFromTrailers)
+{
+ IsFromTrailers_ = isFromTrailers;
+}
+
+int TErrorResponse::GetHttpCode() const
+{
+ return HttpCode_;
+}
+
+bool TErrorResponse::IsFromTrailers() const
+{
+ return IsFromTrailers_;
+}
+
+bool TErrorResponse::IsTransportError() const
+{
+ return HttpCode_ == 503;
+}
+
+TString TErrorResponse::GetRequestId() const
+{
+ return RequestId_;
+}
+
+const TYtError& TErrorResponse::GetError() const
+{
+ return Error_;
+}
+
+bool TErrorResponse::IsResolveError() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NYTree::ResolveError);
+}
+
+bool TErrorResponse::IsAccessDenied() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NSecurityClient::AuthorizationError);
+}
+
+bool TErrorResponse::IsConcurrentTransactionLockConflict() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NCypressClient::ConcurrentTransactionLockConflict);
+}
+
+bool TErrorResponse::IsRequestRateLimitExceeded() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NSecurityClient::RequestQueueSizeLimitExceeded);
+}
+
+bool TErrorResponse::IsRequestQueueSizeLimitExceeded() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NRpc::RequestQueueSizeLimitExceeded);
+}
+
+bool TErrorResponse::IsChunkUnavailable() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NChunkClient::ChunkUnavailable);
+}
+
+bool TErrorResponse::IsRequestTimedOut() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::Timeout);
+}
+
+bool TErrorResponse::IsNoSuchTransaction() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NTransactionClient::NoSuchTransaction);
+}
+
+bool TErrorResponse::IsConcurrentOperationsLimitReached() const
+{
+ return Error_.ContainsErrorCode(NClusterErrorCodes::NScheduler::TooManyOperations);
+}
+
+void TErrorResponse::Setup()
+{
+ TStringStream s;
+ *this << Error_.FullDescription();
+}
+
+////////////////////////////////////////////////////////////////////
+
+TOperationFailedError::TOperationFailedError(
+ EState state,
+ TOperationId id,
+ TYtError ytError,
+ TVector<TFailedJobInfo> failedJobInfo)
+ : State_(state)
+ , OperationId_(id)
+ , Error_(std::move(ytError))
+ , FailedJobInfo_(std::move(failedJobInfo))
+{
+ *this << Error_.FullDescription();
+ if (!FailedJobInfo_.empty()) {
+ *this << DumpJobInfoForException(OperationId_, FailedJobInfo_);
+ }
+}
+
+TOperationFailedError::EState TOperationFailedError::GetState() const
+{
+ return State_;
+}
+
+TOperationId TOperationFailedError::GetOperationId() const
+{
+ return OperationId_;
+}
+
+const TYtError& TOperationFailedError::GetError() const
+{
+ return Error_;
+}
+
+const TVector<TFailedJobInfo>& TOperationFailedError::GetFailedJobInfo() const
+{
+ return FailedJobInfo_;
+}
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/errors.h b/yt/cpp/mapreduce/interface/errors.h
new file mode 100644
index 0000000000..afad58ed72
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/errors.h
@@ -0,0 +1,290 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/errors.h
+///
+/// Errors and exceptions emitted by library.
+
+#include "fwd.h"
+#include "common.h"
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/generic/bt_exception.h>
+#include <util/generic/yexception.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+namespace NJson {
+ class TJsonValue;
+} // namespace NJson
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Error that is thrown when library detects invalid usage of API.
+///
+/// For example trying to start operations on empty table list.
+class TApiUsageError
+ : public TWithBackTrace<yexception>
+{ };
+
+///
+/// @brief Error that is thrown when request retries continues for too long.
+///
+/// @see NYT::TRetryConfig
+/// @see NYT::IRetryConfigProvider
+class TRequestRetriesTimeout
+ : public yexception
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Error returned by YT cluster.
+///
+/// An object of this class describe error that happened on YT server.
+/// Internally each error is a tree. Each node of the tree contains:
+/// - integer error code;
+/// - text description of error;
+/// - attributes describing error context.
+///
+/// To get text description of an error one should use
+/// @ref NYT::TYtError::ShortDescription or @ref NYT::TYtError::FullDescription
+///
+/// To distinguish between error kinds @ref NYT::TYtError::ContainsErrorCode should be used.
+///
+/// @see NYT::TErrorResponse
+/// @see NYT::TOperationFailedError
+class TYtError
+{
+public:
+ /// Constructs error with NYT::NClusterErrorCodes::OK code and empty message.
+ TYtError();
+
+ /// Constructs error with NYT::NClusterErrorCodes::Generic code and given message.
+ explicit TYtError(const TString& message);
+
+ /// Constructs error with given code and given message.
+ TYtError(int code, const TString& message);
+
+ /// Construct error from json representation.
+ TYtError(const ::NJson::TJsonValue& value);
+
+ /// Construct error from TNode representation.
+ TYtError(const TNode& value);
+
+ ///
+ /// @brief Check if error or any of inner errors has given error code.
+ ///
+ /// Use this method to distinguish kind of error.
+ bool ContainsErrorCode(int code) const;
+
+ ///
+ /// @brief Get short description of error.
+ ///
+ /// Short description contain text description of error and all inner errors.
+ /// It is human readable but misses some important information (error codes, error attributes).
+ ///
+ /// Usually it's better to use @ref NYT::TYtError::FullDescription to log errors.
+ TString ShortDescription() const;
+
+ ///
+ /// @brief Get full description of error.
+ ///
+ /// Full description contains readable short description
+ /// followed by text yson representation of error that contains error codes and attributes.
+ TString FullDescription() const;
+
+ ///
+ /// @brief Get error code of the topmost error.
+ ///
+ /// @warning Do not use this method to distinguish between error kinds
+ /// @ref NYT::TYtError::ContainsErrorCode should be used instead.
+ int GetCode() const;
+
+ ///
+ /// @brief Get error text of the topmost error.
+ ///
+ /// @warning This method should not be used to log errors
+ /// since text description of inner errors is going to be lost.
+ /// @ref NYT::TYtError::FullDescription should be used instead.
+ const TString& GetMessage() const;
+
+ ///
+ /// @brief Check if error or any of inner errors contains given text chunk.
+ ///
+ /// @warning @ref NYT::TYtError::ContainsErrorCode must be used instead of
+ /// this method when possible. If there is no suitable error code it's
+ /// better to ask yt@ to add one. This method should only be used as workaround.
+ bool ContainsText(const TStringBuf& text) const;
+
+ /// @brief Get inner errors.
+ const TVector<TYtError>& InnerErrors() const;
+
+ /// Parse error from json string.
+ void ParseFrom(const TString& jsonError);
+
+ /// Collect error codes from entire error tree.
+ TSet<int> GetAllErrorCodes() const;
+
+ /// Check if error has any attributes.
+ bool HasAttributes() const;
+
+ /// Get error attributes.
+ const TNode::TMapType& GetAttributes() const;
+
+ /// Get text yson representation of error
+ TString GetYsonText() const;
+
+private:
+ int Code_;
+ TString Message_;
+ TVector<TYtError> InnerErrors_;
+ TNode::TMapType Attributes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Generic error response returned by server.
+///
+/// TErrorResponse can be thrown from almost any client method when server responds with error.
+///
+class TErrorResponse
+ : public yexception
+{
+public:
+ TErrorResponse(int httpCode, const TString& requestId);
+ TErrorResponse(int httpCode, TYtError error);
+
+ /// Get error object returned by server.
+ const TYtError& GetError() const;
+
+ /// Get if (correlation-id) of request that was responded with error.
+ TString GetRequestId() const;
+
+ /// Get HTTP code of response.
+ int GetHttpCode() const;
+
+ /// Is error parsed from response trailers.
+ bool IsFromTrailers() const;
+
+ /// Check if error was caused by transport problems inside YT cluster.
+ bool IsTransportError() const;
+
+ /// Check if error was caused by failure to resolve cypress path.
+ bool IsResolveError() const;
+
+ /// Check if error was caused by lack of permissions to execute request.
+ bool IsAccessDenied() const;
+
+ /// Check if error was caused by failure to lock object because of another transaction is holding lock.
+ bool IsConcurrentTransactionLockConflict() const;
+
+ /// Check if error was caused by request quota limit exceeding.
+ bool IsRequestRateLimitExceeded() const;
+
+ // YT can't serve request because it is overloaded.
+ bool IsRequestQueueSizeLimitExceeded() const;
+
+ /// Check if error was caused by failure to get chunk. Such errors are almost always temporary.
+ bool IsChunkUnavailable() const;
+
+ /// Check if error was caused by internal YT timeout.
+ bool IsRequestTimedOut() const;
+
+ /// Check if error was caused by trying to work with transaction that was finished or never existed.
+ bool IsNoSuchTransaction() const;
+
+ // User reached their limit of concurrently running operations.
+ bool IsConcurrentOperationsLimitReached() const;
+
+ /// @deprecated This method must not be used.
+ bool IsOk() const;
+
+ void SetRawError(const TString& message);
+ void SetError(TYtError error);
+ void ParseFromJsonError(const TString& jsonError);
+ void SetIsFromTrailers(bool isFromTrailers);
+
+private:
+ void Setup();
+
+private:
+ int HttpCode_;
+ TString RequestId_;
+ TYtError Error_;
+ bool IsFromTrailers_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Info about failed jobs.
+///
+/// @see NYT::TOperationFailedError
+struct TFailedJobInfo
+{
+ /// Id of a job.
+ TJobId JobId;
+
+ /// Error describing job failure.
+ TYtError Error;
+
+ /// Stderr of job.
+ ///
+ /// @note YT doesn't store all job stderrs, check @ref NYT::IOperationClient::GetJobStderr
+ /// for list of limitations.
+ ///
+ /// @see NYT::IOperationClient::GetJobStderr
+ TString Stderr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Error that is thrown when operation watched by library fails.
+///
+/// This error is thrown from operation starting methods when they are started in sync mode (@ refNYT::TOperationOptions::Wait == true)
+/// or from future returned by NYT::IOperation::Watch.
+///
+/// @see NYT::IOperationClient
+class TOperationFailedError
+ : public yexception
+{
+public:
+ /// Final state of operation.
+ enum EState {
+ /// Operation was failed due to some error.
+ Failed,
+ /// Operation didn't experienced errors, but was aborted by user request or by YT.
+ Aborted,
+ };
+
+public:
+ TOperationFailedError(EState state, TOperationId id, TYtError ytError, TVector<TFailedJobInfo> failedJobInfo);
+
+ /// Get final state of operation.
+ EState GetState() const;
+
+ /// Get operation id.
+ TOperationId GetOperationId() const;
+
+ /// Return operation error.
+ const TYtError& GetError() const;
+
+ /// Return info about failed jobs (if any).
+ const TVector<TFailedJobInfo>& GetFailedJobInfo() const;
+
+private:
+ EState State_;
+ TOperationId OperationId_;
+ TYtError Error_;
+ TVector<TFailedJobInfo> FailedJobInfo_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/finish_or_die.h b/yt/cpp/mapreduce/interface/finish_or_die.h
new file mode 100644
index 0000000000..9d7dcece02
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/finish_or_die.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <util/system/yassert.h>
+
+#include <exception>
+
+/// @cond Doxygen_Suppress
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void FinishOrDie(T* pThis, const char* className) noexcept
+{
+ auto fail = [&] (const char* what) {
+ Y_FAIL(
+ "\n\n"
+ "Destructor of %s caught exception during Finish: %s.\n"
+ "Some data is probably has not been written.\n"
+ "In order to handle such exceptions consider explicitly call Finish() method.\n",
+ className,
+ what);
+ };
+
+ try {
+ pThis->Finish();
+ } catch (const std::exception& ex) {
+ if (!std::uncaught_exceptions()) {
+ fail(ex.what());
+ }
+ } catch (...) {
+ if (!std::uncaught_exceptions()) {
+ fail("<unknown exception>");
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
+/// @endcond
diff --git a/yt/cpp/mapreduce/interface/fluent.h b/yt/cpp/mapreduce/interface/fluent.h
new file mode 100644
index 0000000000..8ca6e86336
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/fluent.h
@@ -0,0 +1,678 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/fluent.h
+///
+/// Adapters for working with @ref NYson::IYsonConsumer in a structured way, with compile-time syntax checks.
+///
+/// The following documentation is copied verbatim from `yt/core/ytree/fluent.h`.
+///
+/// WHAT IS THIS
+///
+/// Fluent adapters encapsulate invocation of IYsonConsumer methods in a
+/// convenient structured manner. Key advantage of fluent-like code is that
+/// attempt of building syntactically incorrect YSON structure will result
+/// in a compile-time error.
+///
+/// Each fluent object is associated with a context that defines possible YSON
+/// tokens that may appear next. For example, TFluentMap is a fluent object
+/// that corresponds to a location within YSON map right before a key-value
+/// pair or the end of the map.
+///
+/// More precisely, each object that may be obtained by a sequence of fluent
+/// method calls has the full history of its enclosing YSON composite types in
+/// its single template argument hereinafter referred to as TParent. This allows
+/// us not to forget the original context after opening and closing the embedded
+/// composite structure.
+///
+/// It is possible to invoke a separate YSON building procedure by calling
+/// one of convenience Do* methods. There are two possibilities here: it is
+/// possible to delegate invocation context either as a fluent object (like
+/// TFluentMap, TFluentList, TFluentAttributes or TFluentAny) or as a raw
+/// IYsonConsumer*. The latter is discouraged since it is impossible to check
+/// if a given side-built YSON structure fits current fluent context.
+/// For example it is possible to call Do() method inside YSON map passing
+/// consumer to a procedure that will treat context like it is in a list.
+/// Passing typed fluent builder saves you from such a misbehaviour.
+///
+/// TFluentXxx corresponds to an internal class of TXxx
+/// without any history hidden in template argument. It allows you to
+/// write procedures of form:
+///
+/// void BuildSomeAttributesInYson(TFluentMap fluent) { ... }
+///
+/// without thinking about the exact way how this procedure is nested in other
+/// procedures.
+///
+/// An important notation: we will refer to a function whose first argument
+/// is TFluentXxx as TFuncXxx.
+///
+///
+/// BRIEF LIST OF AVAILABLE METHODS
+///
+/// Only the most popular methods are covered here. Refer to the code for the
+/// rest of them.
+///
+/// TAny:
+/// * Value(T value) -> TParent, serialize `value` using underlying consumer.
+/// T should be such that free function Serialize(NYson::IYsonConsumer*, const T&) is
+/// defined;
+/// * BeginMap() -> TFluentMap, open map;
+/// * BeginList() -> TFluentList, open list;
+/// * BeginAttributes() -> TFluentAttributes, open attributes;
+///
+/// * Do(TFuncAny func) -> TAny, delegate invocation to a separate procedure.
+/// * DoIf(bool condition, TFuncAny func) -> TAny, same as Do() but invoke
+/// `func` only if `condition` is true;
+/// * DoFor(TCollection collection, TFuncAny func) -> TAny, same as Do()
+/// but iterate over `collection` and pass each of its elements as a second
+/// argument to `func`. Instead of passing a collection you may it is possible
+/// to pass two iterators as an argument;
+///
+/// * DoMap(TFuncMap func) -> TAny, open a map, delegate invocation to a separate
+/// procedure and close map;
+/// * DoMapFor(TCollection collection, TFuncMap func) -> TAny, open a map, iterate
+/// over `collection` and pass each of its elements as a second argument to `func`
+/// and close map;
+/// * DoList(TFuncList func) -> TAny, same as DoMap();
+/// * DoListFor(TCollection collection, TFuncList func) -> TAny; same as DoMapFor().
+///
+///
+/// TFluentMap:
+/// * Item(TStringBuf key) -> TAny, open an element keyed with `key`;
+/// * EndMap() -> TParent, close map;
+/// * Do(TFuncMap func) -> TFluentMap, same as Do() for TAny;
+/// * DoIf(bool condition, TFuncMap func) -> TFluentMap, same as DoIf() for TAny;
+/// * DoFor(TCollection collection, TFuncMap func) -> TFluentMap, same as DoFor() for TAny.
+///
+///
+/// TFluentList:
+/// * Item() -> TAny, open an new list element;
+/// * EndList() -> TParent, close list;
+/// * Do(TFuncList func) -> TFluentList, same as Do() for TAny;
+/// * DoIf(bool condition, TFuncList func) -> TFluentList, same as DoIf() for TAny;
+/// * DoFor(TCollection collection, TListMap func) -> TFluentList, same as DoFor() for TAny.
+///
+///
+/// TFluentAttributes:
+/// * Item(TStringBuf key) -> TAny, open an element keyed with `key`.
+/// * EndAttributes() -> TParentWithoutAttributes, close attributes. Note that
+/// this method leads to a context that is forces not to have attributes,
+/// preventing us from putting attributes twice before an object.
+/// * Do(TFuncAttributes func) -> TFluentAttributes, same as Do() for TAny;
+/// * DoIf(bool condition, TFuncAttributes func) -> TFluentAttributes, same as DoIf()
+/// for TAny;
+/// * DoFor(TCollection collection, TListAttributes func) -> TFluentAttributes, same as DoFor()
+/// for TAny.
+///
+
+
+#include "common.h"
+#include "serialize.h"
+
+#include <library/cpp/yson/node/serialize.h>
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <library/cpp/yson/consumer.h>
+#include <library/cpp/yson/writer.h>
+
+#include <util/generic/noncopyable.h>
+#include <util/generic/ptr.h>
+#include <util/stream/str.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TFluentYsonUnwrapper
+{
+ using TUnwrapped = T;
+
+ static TUnwrapped Unwrap(T t)
+ {
+ return std::move(t);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFluentYsonVoid
+{ };
+
+template <>
+struct TFluentYsonUnwrapper<TFluentYsonVoid>
+{
+ using TUnwrapped = void;
+
+ static TUnwrapped Unwrap(TFluentYsonVoid)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// This class is actually a namespace for specific fluent adapter classes.
+class TFluentYsonBuilder
+ : private TNonCopyable
+{
+private:
+ template <class T>
+ static void WriteValue(NYT::NYson::IYsonConsumer* consumer, const T& value)
+ {
+ Serialize(value, consumer);
+ }
+
+public:
+ class TFluentAny;
+ template <class TParent> class TAny;
+ template <class TParent> class TToAttributes;
+ template <class TParent> class TAttributes;
+ template <class TParent> class TListType;
+ template <class TParent> class TMapType;
+
+ /// Base class for all fluent adapters.
+ template <class TParent>
+ class TFluentBase
+ {
+ public:
+ /// Implicit conversion to yson consumer
+ operator NYT::NYson::IYsonConsumer* () const
+ {
+ return Consumer;
+ }
+
+ protected:
+ /// @cond Doxygen_Suppress
+ NYT::NYson::IYsonConsumer* Consumer;
+ TParent Parent;
+
+ TFluentBase(NYT::NYson::IYsonConsumer* consumer, TParent parent)
+ : Consumer(consumer)
+ , Parent(std::move(parent))
+ { }
+
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ TUnwrappedParent GetUnwrappedParent()
+ {
+ return TFluentYsonUnwrapper<TParent>::Unwrap(std::move(Parent));
+ }
+ /// @endcond Doxygen_Suppress
+ };
+
+ /// Base class for fluent adapters for fragment of list, map or attributes.
+ template <template <class TParent> class TThis, class TParent>
+ class TFluentFragmentBase
+ : public TFluentBase<TParent>
+ {
+ public:
+ using TDeepThis = TThis<TParent>;
+ using TShallowThis = TThis<TFluentYsonVoid>;
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ explicit TFluentFragmentBase(NYT::NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentBase<TParent>(consumer, std::move(parent))
+ { }
+
+ /// Delegate invocation to a separate procedure.
+ template <class TFunc>
+ TDeepThis& Do(const TFunc& func)
+ {
+ func(TShallowThis(this->Consumer));
+ return *static_cast<TDeepThis*>(this);
+ }
+
+ /// Conditionally delegate invocation to a separate procedure.
+ template <class TFunc>
+ TDeepThis& DoIf(bool condition, const TFunc& func)
+ {
+ if (condition) {
+ func(TShallowThis(this->Consumer));
+ }
+ return *static_cast<TDeepThis*>(this);
+ }
+
+ /// Calls `func(*this, element)` for each `element` in range `[begin, end)`.
+ template <class TFunc, class TIterator>
+ TDeepThis& DoFor(const TIterator& begin, const TIterator& end, const TFunc& func)
+ {
+ for (auto current = begin; current != end; ++current) {
+ func(TShallowThis(this->Consumer), current);
+ }
+ return *static_cast<TDeepThis*>(this);
+ }
+
+ /// Calls `func(*this, element)` for each `element` in `collection`.
+ template <class TFunc, class TCollection>
+ TDeepThis& DoFor(const TCollection& collection, const TFunc& func)
+ {
+ for (const auto& item : collection) {
+ func(TShallowThis(this->Consumer), item);
+ }
+ return *static_cast<TDeepThis*>(this);
+ }
+
+ };
+
+ /// Fluent adapter of a value without attributes.
+ template <class TParent>
+ class TAnyWithoutAttributes
+ : public TFluentBase<TParent>
+ {
+ public:
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ TAnyWithoutAttributes(NYT::NYson::IYsonConsumer* consumer, TParent parent)
+ : TFluentBase<TParent>(consumer, std::move(parent))
+ { }
+
+ /// Pass `value` to underlying consumer.
+ template <class T>
+ TUnwrappedParent Value(const T& value)
+ {
+ WriteValue(this->Consumer, value);
+ return this->GetUnwrappedParent();
+ }
+
+ /// Call `OnEntity()` of underlying consumer.
+ TUnwrappedParent Entity()
+ {
+ this->Consumer->OnEntity();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Serialize `collection` to underlying consumer as a list.
+ template <class TCollection>
+ TUnwrappedParent List(const TCollection& collection)
+ {
+ this->Consumer->OnBeginList();
+ for (const auto& item : collection) {
+ this->Consumer->OnListItem();
+ WriteValue(this->Consumer, item);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Serialize maximum `maxSize` elements of `collection` to underlying consumer as a list.
+ template <class TCollection>
+ TUnwrappedParent ListLimited(const TCollection& collection, size_t maxSize)
+ {
+ this->Consumer->OnBeginAttributes();
+ this->Consumer->OnKeyedItem("count");
+ this->Consumer->OnInt64Scalar(collection.size());
+ this->Consumer->OnEndAttributes();
+ this->Consumer->OnBeginList();
+ size_t printedSize = 0;
+ for (const auto& item : collection) {
+ if (printedSize >= maxSize)
+ break;
+ this->Consumer->OnListItem();
+ WriteValue(this->Consumer, item);
+ ++printedSize;
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a list.
+ TListType<TParent> BeginList()
+ {
+ this->Consumer->OnBeginList();
+ return TListType<TParent>(this->Consumer, this->Parent);
+ }
+
+ /// Open a list, delegate invocation to `func`, then close the list.
+ template <class TFunc>
+ TUnwrappedParent DoList(const TFunc& func)
+ {
+ this->Consumer->OnBeginList();
+ func(TListType<TFluentYsonVoid>(this->Consumer));
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a list, call `func(*this, element)` for each `element` of range, then close the list.
+ template <class TFunc, class TIterator>
+ TUnwrappedParent DoListFor(const TIterator& begin, const TIterator& end, const TFunc& func)
+ {
+ this->Consumer->OnBeginList();
+ for (auto current = begin; current != end; ++current) {
+ func(TListType<TFluentYsonVoid>(this->Consumer), current);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a list, call `func(*this, element)` for each `element` of `collection`, then close the list.
+ template <class TFunc, class TCollection>
+ TUnwrappedParent DoListFor(const TCollection& collection, const TFunc& func)
+ {
+ this->Consumer->OnBeginList();
+ for (const auto& item : collection) {
+ func(TListType<TFluentYsonVoid>(this->Consumer), item);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a map.
+ TMapType<TParent> BeginMap()
+ {
+ this->Consumer->OnBeginMap();
+ return TMapType<TParent>(this->Consumer, this->Parent);
+ }
+
+ /// Open a map, delegate invocation to `func`, then close the map.
+ template <class TFunc>
+ TUnwrappedParent DoMap(const TFunc& func)
+ {
+ this->Consumer->OnBeginMap();
+ func(TMapType<TFluentYsonVoid>(this->Consumer));
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a map, call `func(*this, element)` for each `element` of range, then close the map.
+ template <class TFunc, class TIterator>
+ TUnwrappedParent DoMapFor(const TIterator& begin, const TIterator& end, const TFunc& func)
+ {
+ this->Consumer->OnBeginMap();
+ for (auto current = begin; current != end; ++current) {
+ func(TMapType<TFluentYsonVoid>(this->Consumer), current);
+ }
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+
+ /// Open a map, call `func(*this, element)` for each `element` of `collection`, then close the map.
+ template <class TFunc, class TCollection>
+ TUnwrappedParent DoMapFor(const TCollection& collection, const TFunc& func)
+ {
+ this->Consumer->OnBeginMap();
+ for (const auto& item : collection) {
+ func(TMapType<TFluentYsonVoid>(this->Consumer), item);
+ }
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ /// Fluent adapter of any value.
+ template <class TParent>
+ class TAny
+ : public TAnyWithoutAttributes<TParent>
+ {
+ public:
+ using TBase = TAnyWithoutAttributes<TParent>;
+
+ explicit TAny(NYT::NYson::IYsonConsumer* consumer, TParent parent)
+ : TBase(consumer, std::move(parent))
+ { }
+
+ /// Open attributes.
+ TAttributes<TBase> BeginAttributes()
+ {
+ this->Consumer->OnBeginAttributes();
+ return TAttributes<TBase>(
+ this->Consumer,
+ TBase(this->Consumer, this->Parent));
+ }
+ };
+
+ /// Fluent adapter of attributes fragment (the inside part of attributes).
+ template <class TParent = TFluentYsonVoid>
+ class TAttributes
+ : public TFluentFragmentBase<TAttributes, TParent>
+ {
+ public:
+ using TThis = TAttributes<TParent>;
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ explicit TAttributes(NYT::NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TAttributes, TParent>(consumer, std::move(parent))
+ { }
+
+ /// Pass attribute key to underlying consumer.
+ TAny<TThis> Item(const TStringBuf& key)
+ {
+ this->Consumer->OnKeyedItem(key);
+ return TAny<TThis>(this->Consumer, *this);
+ }
+
+ /// Pass attribute key to underlying consumer.
+ template <size_t Size>
+ TAny<TThis> Item(const char (&key)[Size])
+ {
+ return Item(TStringBuf(key, Size - 1));
+ }
+
+ //TODO: from TNode
+
+ /// Close the attributes.
+ TUnwrappedParent EndAttributes()
+ {
+ this->Consumer->OnEndAttributes();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ /// Fluent adapter of list fragment (the inside part of a list).
+ template <class TParent = TFluentYsonVoid>
+ class TListType
+ : public TFluentFragmentBase<TListType, TParent>
+ {
+ public:
+ using TThis = TListType<TParent>;
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ explicit TListType(NYT::NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TListType, TParent>(consumer, std::move(parent))
+ { }
+
+ /// Call `OnListItem()` of underlying consumer.
+ TAny<TThis> Item()
+ {
+ this->Consumer->OnListItem();
+ return TAny<TThis>(this->Consumer, *this);
+ }
+
+ // TODO: from TNode
+
+ /// Close the list.
+ TUnwrappedParent EndList()
+ {
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ /// Fluent adapter of map fragment (the inside part of a map).
+ template <class TParent = TFluentYsonVoid>
+ class TMapType
+ : public TFluentFragmentBase<TMapType, TParent>
+ {
+ public:
+ using TThis = TMapType<TParent>;
+ using TUnwrappedParent = typename TFluentYsonUnwrapper<TParent>::TUnwrapped;
+
+ explicit TMapType(NYT::NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TMapType, TParent>(consumer, std::move(parent))
+ { }
+
+ /// Pass map key to underlying consumer.
+ template <size_t Size>
+ TAny<TThis> Item(const char (&key)[Size])
+ {
+ return Item(TStringBuf(key, Size - 1));
+ }
+
+ /// Pass map key to underlying consumer.
+ TAny<TThis> Item(const TStringBuf& key)
+ {
+ this->Consumer->OnKeyedItem(key);
+ return TAny<TThis>(this->Consumer, *this);
+ }
+
+ // TODO: from TNode
+
+ /// Close the map.
+ TUnwrappedParent EndMap()
+ {
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Builder representing any value.
+using TFluentAny = TFluentYsonBuilder::TAny<TFluentYsonVoid>;
+
+/// Builder representing the inside of a list (list fragment).
+using TFluentList = TFluentYsonBuilder::TListType<TFluentYsonVoid>;
+
+/// Builder representing the inside of a map (map fragment).
+using TFluentMap = TFluentYsonBuilder::TMapType<TFluentYsonVoid>;
+
+/// Builder representing the inside of attributes.
+using TFluentAttributes = TFluentYsonBuilder::TAttributes<TFluentYsonVoid>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Create a fluent adapter to invoke methods of `consumer`.
+static inline TFluentAny BuildYsonFluently(NYT::NYson::IYsonConsumer* consumer)
+{
+ return TFluentAny(consumer, TFluentYsonVoid());
+}
+
+/// Create a fluent adapter to invoke methods of `consumer` describing the contents of a list.
+static inline TFluentList BuildYsonListFluently(NYT::NYson::IYsonConsumer* consumer)
+{
+ return TFluentList(consumer);
+}
+
+/// Create a fluent adapter to invoke methods of `consumer` describing the contents of a map.
+static inline TFluentMap BuildYsonMapFluently(NYT::NYson::IYsonConsumer* consumer)
+{
+ return TFluentMap(consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentYsonWriterState
+ : public TThrRefBase
+{
+public:
+ using TValue = TString;
+
+ explicit TFluentYsonWriterState(::NYson::EYsonFormat format)
+ : Writer(&Output, format)
+ { }
+
+ TString GetValue()
+ {
+ return Output.Str();
+ }
+
+ NYT::NYson::IYsonConsumer* GetConsumer()
+ {
+ return &Writer;
+ }
+
+private:
+ TStringStream Output;
+ ::NYson::TYsonWriter Writer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentYsonBuilderState
+ : public TThrRefBase
+{
+public:
+ using TValue = TNode;
+
+ explicit TFluentYsonBuilderState()
+ : Builder(&Node)
+ { }
+
+ TNode GetValue()
+ {
+ return std::move(Node);
+ }
+
+ NYT::NYson::IYsonConsumer* GetConsumer()
+ {
+ return &Builder;
+ }
+
+private:
+ TNode Node;
+ TNodeBuilder Builder;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState>
+class TFluentYsonHolder
+{
+public:
+ explicit TFluentYsonHolder(::TIntrusivePtr<TState> state)
+ : State(state)
+ { }
+
+ ::TIntrusivePtr<TState> GetState() const
+ {
+ return State;
+ }
+
+private:
+ ::TIntrusivePtr<TState> State;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState>
+struct TFluentYsonUnwrapper< TFluentYsonHolder<TState> >
+{
+ using TUnwrapped = typename TState::TValue;
+
+ static TUnwrapped Unwrap(const TFluentYsonHolder<TState>& holder)
+ {
+ return std::move(holder.GetState()->GetValue());
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState>
+TFluentYsonBuilder::TAny<TFluentYsonHolder<TState>>
+BuildYsonFluentlyWithState(::TIntrusivePtr<TState> state)
+{
+ return TFluentYsonBuilder::TAny<TFluentYsonHolder<TState>>(
+ state->GetConsumer(),
+ TFluentYsonHolder<TState>(state));
+}
+
+/// Create a fluent adapter returning a `TString` with corresponding YSON when construction is finished.
+inline TFluentYsonBuilder::TAny<TFluentYsonHolder<TFluentYsonWriterState>>
+BuildYsonStringFluently(::NYson::EYsonFormat format = ::NYson::EYsonFormat::Text)
+{
+ ::TIntrusivePtr<TFluentYsonWriterState> state(new TFluentYsonWriterState(format));
+ return BuildYsonFluentlyWithState(state);
+}
+
+/// Create a fluent adapter returning a @ref NYT::TNode when construction is finished.
+inline TFluentYsonBuilder::TAny<TFluentYsonHolder<TFluentYsonBuilderState>>
+BuildYsonNodeFluently()
+{
+ ::TIntrusivePtr<TFluentYsonBuilderState> state(new TFluentYsonBuilderState);
+ return BuildYsonFluentlyWithState(state);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/format.cpp b/yt/cpp/mapreduce/interface/format.cpp
new file mode 100644
index 0000000000..f8318310a4
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/format.cpp
@@ -0,0 +1,135 @@
+#include "format.h"
+#include "protobuf_format.h"
+
+#include "errors.h"
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/messagext.h>
+
+namespace NYT {
+
+TTableSchema CreateTableSchema(
+ const ::google::protobuf::Descriptor& messageDescriptor,
+ bool keepFieldsWithoutExtension)
+{
+ return NDetail::CreateTableSchemaImpl(messageDescriptor, keepFieldsWithoutExtension);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFormat::TFormat(const TNode& config)
+ : Config(config)
+{ }
+
+
+TFormat TFormat::Protobuf(
+ const TVector<const ::google::protobuf::Descriptor*>& descriptors,
+ bool withDescriptors)
+{
+ if (withDescriptors) {
+ return TFormat(NDetail::MakeProtoFormatConfigWithDescriptors(descriptors));
+ } else {
+ return TFormat(NDetail::MakeProtoFormatConfigWithTables(descriptors));
+ }
+}
+
+TFormat TFormat::YsonText()
+{
+ TNode config("yson");
+ config.Attributes()("format", "text");
+ return TFormat(config);
+}
+
+TFormat TFormat::YsonBinary()
+{
+ TNode config("yson");
+ config.Attributes()("format", "binary");
+ return TFormat(config);
+}
+
+TFormat TFormat::YaMRLenval()
+{
+ TNode config("yamr");
+ config.Attributes()("lenval", true)("has_subkey", true);
+ return TFormat(config);
+}
+
+TFormat TFormat::Json()
+{
+ return TFormat(TNode("json"));
+}
+
+bool TFormat::IsTextYson() const
+{
+ if (!Config.IsString() || Config.AsString() != "yson") {
+ return false;
+ }
+ if (!Config.HasAttributes()) {
+ return false;
+ }
+ const auto& attributes = Config.GetAttributes();
+ if (!attributes.HasKey("format") || attributes["format"] != TNode("text")) {
+ return false;
+ }
+ return true;
+}
+
+bool TFormat::IsProtobuf() const
+{
+ return Config.IsString() && Config.AsString() == "protobuf";
+}
+
+bool TFormat::IsYamredDsv() const
+{
+ return Config.IsString() && Config.AsString() == "yamred_dsv";
+}
+
+static TString FormatName(const TFormat& format)
+{
+ if (!format.Config.IsString()) {
+ Y_VERIFY(format.Config.IsUndefined());
+ return "<undefined>";
+ }
+ return format.Config.AsString();
+}
+
+TYamredDsvAttributes TFormat::GetYamredDsvAttributes() const
+{
+ if (!IsYamredDsv()) {
+ ythrow TApiUsageError() << "Cannot get yamred_dsv attributes for " << FormatName(*this) << " format";
+ }
+ TYamredDsvAttributes attributes;
+
+ const auto& nodeAttributes = Config.GetAttributes();
+ {
+ const auto& keyColumns = nodeAttributes["key_column_names"];
+ if (!keyColumns.IsList()) {
+ ythrow yexception() << "Ill-formed format: key_column_names is of non-list type: " << keyColumns.GetType();
+ }
+ for (auto& column : keyColumns.AsList()) {
+ if (!column.IsString()) {
+ ythrow yexception() << "Ill-formed format: key_column_names: " << column.GetType();
+ }
+ attributes.KeyColumnNames.push_back(column.AsString());
+ }
+ }
+
+ if (nodeAttributes.HasKey("subkey_column_names")) {
+ const auto& subkeyColumns = nodeAttributes["subkey_column_names"];
+ if (!subkeyColumns.IsList()) {
+ ythrow yexception() << "Ill-formed format: subkey_column_names is not a list: " << subkeyColumns.GetType();
+ }
+ for (const auto& column : subkeyColumns.AsList()) {
+ if (!column.IsString()) {
+ ythrow yexception() << "Ill-formed format: non-string inside subkey_key_column_names: " << column.GetType();
+ }
+ attributes.SubkeyColumnNames.push_back(column.AsString());
+ }
+ }
+
+ return attributes;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/format.h b/yt/cpp/mapreduce/interface/format.h
new file mode 100644
index 0000000000..e297576464
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/format.h
@@ -0,0 +1,122 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/format.h
+///
+/// Header containing class to work with raw [YT formats](https://yt.yandex-team.ru/docs/description/storage/formats.html).
+
+#include "node.h"
+
+#include <google/protobuf/descriptor.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @deprecated
+struct TYamredDsvAttributes
+{
+ /// Names of key columns.
+ TVector<TString> KeyColumnNames;
+
+ /// Names of subkey columns.
+ TVector<TString> SubkeyColumnNames;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Class representing YT data format.
+///
+/// Normally the user does not need to use it.
+/// However, the class is handy for "raw" operations and table reading and writing,
+/// e.g. @ref NYT::IOperationClient::RawMap and other raw operations,
+/// @ref NYT::IIOClient::CreateRawReader and @ref NYT::IIOClient::CreateRawWriter.
+/// Anyway, the static factory methods should be preferred to the constructor.
+///
+/// @see [YT doc](https://yt.yandex-team.ru/docs/description/storage/formats.html).
+struct TFormat
+{
+public:
+ /// Format representation understandable by YT.
+ TNode Config;
+
+public:
+ /// @brief Construct format from given YT format representation.
+ ///
+ /// @note Prefer using static factory methods (e.g. @ref NYT::TFormat::YsonBinary, @ref NYT::TFormat::YsonText, @ref NYT::TFormat::Protobuf).
+ explicit TFormat(const TNode& config = TNode());
+
+ /// @brief Create text YSON format.
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/description/storage/formats.html#YSON)
+ static TFormat YsonText();
+
+ /// @brief Create binary YSON format.
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/description/storage/formats.html#YSON)
+ static TFormat YsonBinary();
+
+ /// @brief Create YaMR format.
+ ///
+ /// @deprecated
+ static TFormat YaMRLenval();
+
+ /// @brief Create protobuf format from protobuf message descriptors.
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/api/c++/protobuf.html).
+ static TFormat Protobuf(
+ const TVector<const ::google::protobuf::Descriptor*>& descriptors,
+ bool withDescriptors = false);
+
+ /// @brief Create JSON format.
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/description/storage/formats.html#JSON)
+ static TFormat Json();
+
+ /// @brief Create protobuf format for the message specified in template parameter.
+ ///
+ /// `T` must be inherited from `Message`.
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/api/c++/protobuf.html).
+ template<typename T>
+ static inline TFormat Protobuf(bool withDescriptors = false);
+
+ /// @brief Is the format text YSON?
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/description/storage/formats.html#YSON)
+ bool IsTextYson() const;
+
+ /// @brief Is the format protobuf?
+ ///
+ /// @see [the doc](https://yt.yandex-team.ru/docs/api/c++/protobuf.html)
+ bool IsProtobuf() const;
+
+ /// @brief Is the format YaMR?
+ ///
+ /// @deprecated
+ bool IsYamredDsv() const;
+
+ /// @brief For YAMR format returns its attributes in structured way.
+ ///
+ /// @deprecated
+ TYamredDsvAttributes GetYamredDsvAttributes() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<typename T>
+TFormat TFormat::Protobuf(bool withDescriptors) {
+ return TFormat::Protobuf({T::descriptor()}, withDescriptors);
+}
+
+/// @brief Create table schema from protobuf message descriptor.
+///
+/// @param messageDescriptor Message descriptor
+/// @param keepFieldsWithoutExtension Add to schema fields without "column_name" or "key_column_name" extensions.
+TTableSchema CreateTableSchema(
+ const ::google::protobuf::Descriptor& messageDescriptor,
+ bool keepFieldsWithoutExtension);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/format_ut.cpp b/yt/cpp/mapreduce/interface/format_ut.cpp
new file mode 100644
index 0000000000..069c29087d
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/format_ut.cpp
@@ -0,0 +1,235 @@
+#include "common.h"
+#include "errors.h"
+#include "format.h"
+#include "common_ut.h"
+
+#include <yt/cpp/mapreduce/interface/proto3_ut.pb.h>
+#include <yt/cpp/mapreduce/interface/protobuf_table_schema_ut.pb.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYT;
+
+static TNode GetColumns(const TFormat& format, int tableIndex = 0)
+{
+ return format.Config.GetAttributes()["tables"][tableIndex]["columns"];
+}
+
+Y_UNIT_TEST_SUITE(ProtobufFormat)
+{
+ Y_UNIT_TEST(TIntegral)
+ {
+ const auto format = TFormat::Protobuf<NUnitTesting::TIntegral>();
+ auto columns = GetColumns(format);
+
+ struct TColumn
+ {
+ TString Name;
+ TString ProtoType;
+ int FieldNumber;
+ };
+
+ auto expected = TVector<TColumn>{
+ {"DoubleField", "double", 1},
+ {"FloatField", "float", 2},
+ {"Int32Field", "int32", 3},
+ {"Int64Field", "int64", 4},
+ {"Uint32Field", "uint32", 5},
+ {"Uint64Field", "uint64", 6},
+ {"Sint32Field", "sint32", 7},
+ {"Sint64Field", "sint64", 8},
+ {"Fixed32Field", "fixed32", 9},
+ {"Fixed64Field", "fixed64", 10},
+ {"Sfixed32Field", "sfixed32", 11},
+ {"Sfixed64Field", "sfixed64", 12},
+ {"BoolField", "bool", 13},
+ {"EnumField", "enum_string", 14},
+ };
+
+ UNIT_ASSERT_VALUES_EQUAL(columns.Size(), expected.size());
+ for (int i = 0; i < static_cast<int>(columns.Size()); ++i) {
+ UNIT_ASSERT_VALUES_EQUAL(columns[i]["name"], expected[i].Name);
+ UNIT_ASSERT_VALUES_EQUAL(columns[i]["proto_type"], expected[i].ProtoType);
+ UNIT_ASSERT_VALUES_EQUAL(columns[i]["field_number"], expected[i].FieldNumber);
+ }
+ }
+
+ Y_UNIT_TEST(TRowFieldSerializationOption)
+ {
+ const auto format = TFormat::Protobuf<NUnitTesting::TRowFieldSerializationOption>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["name"], "UrlRow_1");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["field_number"], 1);
+ const auto& fields = columns[0]["fields"];
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["name"], "Host");
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["proto_type"], "string");
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["field_number"], 1);
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["name"], "Path");
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["proto_type"], "string");
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["field_number"], 2);
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["name"], "HttpCode");
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["proto_type"], "sint32");
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["field_number"], 3);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["name"], "UrlRow_2");
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["proto_type"], "message");
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["field_number"], 2);
+ }
+
+ Y_UNIT_TEST(Packed)
+ {
+ const auto format = TFormat::Protobuf<NUnitTesting::TPacked>();
+ auto column = GetColumns(format)[0];
+
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "PackedListInt64");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["field_number"], 1);
+ UNIT_ASSERT_VALUES_EQUAL(column["packed"], true);
+ UNIT_ASSERT_VALUES_EQUAL(column["repeated"], true);
+ }
+
+ Y_UNIT_TEST(Cyclic)
+ {
+ UNIT_ASSERT_EXCEPTION(TFormat::Protobuf<NUnitTesting::TCyclic>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(TFormat::Protobuf<NUnitTesting::TCyclic::TA>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(TFormat::Protobuf<NUnitTesting::TCyclic::TB>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(TFormat::Protobuf<NUnitTesting::TCyclic::TC>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(TFormat::Protobuf<NUnitTesting::TCyclic::TD>(), TApiUsageError);
+
+ const auto format = TFormat::Protobuf<NUnitTesting::TCyclic::TE>();
+ auto column = GetColumns(format)[0];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "d");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "message");
+ UNIT_ASSERT_VALUES_EQUAL(column["field_number"], 1);
+ }
+
+ Y_UNIT_TEST(Map)
+ {
+ const auto format = TFormat::Protobuf<NUnitTesting::TWithMap>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns.Size(), 5);
+ {
+ const auto& column = columns[0];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapDefault");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "message");
+ }
+ {
+ const auto& column = columns[1];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapListOfStructsLegacy");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "message");
+ }
+ {
+ const auto& column = columns[2];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapListOfStructs");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "structured_message");
+ }
+ {
+ const auto& column = columns[3];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapOptionalDict");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "structured_message");
+ }
+ {
+ const auto& column = columns[4];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapDict");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "structured_message");
+ }
+ }
+
+ Y_UNIT_TEST(Oneof)
+ {
+ const auto format = TFormat::Protobuf<NUnitTesting::TWithOneof>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns.Size(), 4);
+ auto check = [] (const TNode& column, TStringBuf name, TStringBuf oneof2Name) {
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], name);
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 5);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["name"], "field");
+
+ const auto& oneof2 = column["fields"][1];
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["name"], oneof2Name);
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["proto_type"], "oneof");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][0]["name"], "y2");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][1]["name"], "z2");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][1]["proto_type"], "structured_message");
+ const auto& embeddedOneof = oneof2["fields"][1]["fields"][0];
+ UNIT_ASSERT_VALUES_EQUAL(embeddedOneof["name"], "Oneof");
+ UNIT_ASSERT_VALUES_EQUAL(embeddedOneof["fields"][0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(embeddedOneof["fields"][1]["name"], "y");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][2]["name"], "x2");
+
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][2]["name"], "x1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][3]["name"], "y1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][4]["name"], "z1");
+ };
+
+ check(columns[0], "DefaultSeparateFields", "variant_field_name");
+ check(columns[1], "NoDefault", "Oneof2");
+
+ {
+ const auto& column = columns[2];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "SerializationProtobuf");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 3);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["name"], "x1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["name"], "y1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][2]["name"], "z1");
+ }
+ {
+ const auto& column = columns[3];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "TopLevelOneof");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "oneof");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["name"], "MemberOfTopLevelOneof");
+ }
+ }
+}
+
+Y_UNIT_TEST_SUITE(Proto3)
+{
+ Y_UNIT_TEST(TWithOptional)
+ {
+ const auto format = TFormat::Protobuf<NTestingProto3::TWithOptional>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["field_number"], 1);
+ }
+
+ Y_UNIT_TEST(TWithOptionalMessage)
+ {
+ const auto format = TFormat::Protobuf<NTestingProto3::TWithOptionalMessage>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["field_number"], 1);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["fields"].Size(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["fields"][0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["fields"][0]["field_number"], 1);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/fwd.h b/yt/cpp/mapreduce/interface/fwd.h
new file mode 100644
index 0000000000..0434c03d8b
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/fwd.h
@@ -0,0 +1,397 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/fwd.h
+///
+/// Header containing mostly forward declarations of types.
+
+
+#include <util/generic/fwd.h>
+#include <util/system/types.h>
+
+#include <variant>
+
+/// @cond Doxygen_Suppress
+namespace google::protobuf {
+ class Message;
+}
+
+namespace NYT {
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // batch_request.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ class IBatchRequest;
+ using TBatchRequestPtr = ::TIntrusivePtr<IBatchRequest>;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // client.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ enum ELockMode : int;
+
+ struct TStartTransactionOptions;
+
+ struct TLockOptions;
+
+ template <class TDerived>
+ struct TTabletOptions;
+
+ struct TMountTableOptions;
+
+ struct TUnmountTableOptions;
+
+ struct TRemountTableOptions;
+
+ struct TReshardTableOptions;
+
+ struct TAlterTableOptions;
+
+ struct TLookupRowsOptions;
+
+ struct TSelectRowsOptions;
+
+ struct TCreateClientOptions;
+
+ struct TAlterTableReplicaOptions;
+
+ struct TGetFileFromCacheOptions;
+
+ struct TPutFileToCacheOptions;
+
+ struct TCheckPermissionResult;
+ struct TCheckPermissionResponse;
+ struct TCheckPermissionOptions;
+
+ struct TTabletInfo;
+
+ class ILock;
+ using ILockPtr = ::TIntrusivePtr<ILock>;
+
+ class ITransaction;
+ using ITransactionPtr = ::TIntrusivePtr<ITransaction>;
+
+ class ITransactionPinger;
+ using ITransactionPingerPtr = ::TIntrusivePtr<ITransactionPinger>;
+
+ struct IOperation;
+ using IOperationPtr = ::TIntrusivePtr<IOperation>;
+
+ class IClientBase;
+
+ class IClient;
+
+ using IClientPtr = ::TIntrusivePtr<IClient>;
+ using IClientBasePtr = ::TIntrusivePtr<IClientBase>;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // config.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ struct TConfig;
+ using TConfigPtr = ::TIntrusivePtr<TConfig>;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // cypress.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ enum ENodeType : int;
+
+ struct TCreateOptions;
+
+ struct TRemoveOptions;
+
+ struct TGetOptions;
+
+ struct TSetOptions;
+
+ struct TMultisetAttributesOptions;
+
+ struct TListOptions;
+
+ struct TCopyOptions;
+
+ struct TMoveOptions;
+
+ struct TLinkOptions;
+
+ struct TConcatenateOptions;
+
+ struct TInsertRowsOptions;
+
+ struct TDeleteRowsOptions;
+
+ struct TTrimRowsOptions;
+
+ class ICypressClient;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // errors.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ class TApiUsageError;
+
+ class TYtError;
+
+ class TErrorResponse;
+
+ struct TFailedJobInfo;
+
+ class TOperationFailedError;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // node.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ class TNode;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // common.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ using TTransactionId = TGUID;
+ using TNodeId = TGUID;
+ using TLockId = TGUID;
+ using TOperationId = TGUID;
+ using TTabletCellId = TGUID;
+ using TReplicaId = TGUID;
+ using TJobId = TGUID;
+
+ using TYPath = TString;
+ using TLocalFilePath = TString;
+
+ template <class T, class TDerived = void>
+ struct TOneOrMany;
+
+ // key column values
+ using TKey = TOneOrMany<TNode>;
+
+ class TSortColumn;
+
+ // column names
+ using TColumnNames = TOneOrMany<TString>;
+
+ // key column descriptors.
+ class TSortColumns;
+
+ enum EValueType : int;
+
+ enum ESortOrder : int;
+
+ enum EOptimizeForAttr : i8;
+
+ enum EErasureCodecAttr : i8;
+
+ enum ESchemaModificationAttr : i8;
+
+ enum class EMasterReadKind : int;
+
+ class TColumnSchema;
+
+ class TTableSchema;
+
+ enum class ERelation;
+
+ struct TKeyBound;
+
+ struct TReadLimit;
+
+ struct TReadRange;
+
+ struct TRichYPath;
+
+ struct TAttributeFilter;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // io.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ enum class EFormatType : int;
+
+ struct TFormat;
+
+ class IFileReader;
+
+ using IFileReaderPtr = ::TIntrusivePtr<IFileReader>;
+
+ class IFileWriter;
+
+ using IFileWriterPtr = ::TIntrusivePtr<IFileWriter>;
+
+ class IBlobTableReader;
+ using IBlobTableReaderPtr = ::TIntrusivePtr<IBlobTableReader>;
+
+ class TRawTableReader;
+
+ using TRawTableReaderPtr = ::TIntrusivePtr<TRawTableReader>;
+
+ class TRawTableWriter;
+
+ using TRawTableWriterPtr = ::TIntrusivePtr<TRawTableWriter>;
+
+ template <class T, class = void>
+ class TTableReader;
+
+ template <class T, class = void>
+ class TTableRangesReader;
+
+ template <typename T>
+ using TTableRangesReaderPtr = ::TIntrusivePtr<TTableRangesReader<T>>;
+
+ template <class T>
+ using TTableReaderPtr = ::TIntrusivePtr<TTableReader<T>>;
+
+ template <class T, class = void>
+ class TTableWriter;
+
+ template <class T>
+ using TTableWriterPtr = ::TIntrusivePtr<TTableWriter<T>>;
+
+ struct TYaMRRow;
+
+ using ::google::protobuf::Message;
+
+ class ISkiffRowParser;
+
+ using ISkiffRowParserPtr = ::TIntrusivePtr<ISkiffRowParser>;
+
+ class ISkiffRowSkipper;
+
+ using ISkiffRowSkipperPtr = ::TIntrusivePtr<ISkiffRowSkipper>;
+
+ namespace NDetail {
+
+ class TYdlGenericRowType;
+
+ } // namespace NDetail
+
+ template<class... TYdlRowTypes>
+ class TYdlOneOf;
+
+ template<class... TProtoRowTypes>
+ class TProtoOneOf;
+
+ template<class... TSkiffRowTypes>
+ class TSkiffRowOneOf;
+
+ using TYaMRReader = TTableReader<TYaMRRow>;
+ using TYaMRWriter = TTableWriter<TYaMRRow>;
+ using TNodeReader = TTableReader<TNode>;
+ using TNodeWriter = TTableWriter<TNode>;
+ using TMessageReader = TTableReader<Message>;
+ using TMessageWriter = TTableWriter<Message>;
+ using TYdlTableWriter = TTableWriter<NDetail::TYdlGenericRowType>;
+
+ template <class TDerived>
+ struct TIOOptions;
+
+ struct TFileReaderOptions;
+
+ struct TFileWriterOptions;
+
+ struct TTableReaderOptions;
+
+ class TSkiffRowHints;
+
+ struct TTableWriterOptions;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // job_statistics.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ class TJobStatistics;
+
+ template <typename T>
+ class TJobStatisticsEntry;
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // operation.h
+ ////////////////////////////////////////////////////////////////////////////////
+
+ class TFormatHints;
+
+ struct TUserJobSpec;
+
+ struct TMapOperationSpec;
+
+ struct TRawMapOperationSpec;
+
+ struct TReduceOperationSpec;
+
+ struct TMapReduceOperationSpec;
+
+ struct TJoinReduceOperationSpec;
+
+ struct TSortOperationSpec;
+
+ class IIOperationPreparationContext;
+
+ class IJob;
+ using IJobPtr = ::TIntrusivePtr<IJob>;
+
+ class IRawJob;
+ using IRawJobPtr = ::TIntrusivePtr<IRawJob>;
+
+ enum EMergeMode : int;
+
+ struct TMergeOperationSpec;
+
+ struct TEraseOperationSpec;
+
+ template <class TR, class TW>
+ class IMapper;
+
+ template <class TR, class TW>
+ class IReducer;
+
+ template <class TR, class TW>
+ class IAggregatorReducer;
+
+ struct TSuspendOperationOptions;
+
+ struct TResumeOperationOptions;
+
+ enum class EOperationBriefState : int;
+
+ struct TOperationAttributes;
+
+ struct TOperationOptions;
+
+ enum class EOperationAttribute : int;
+
+ struct TOperationAttributeFilter;
+
+ struct TGetOperationOptions;
+
+ struct TListOperationsOptions;
+
+ struct TGetJobOptions;
+
+ struct TListJobsOptions;
+
+ struct IOperationClient;
+
+ enum class EFinishedJobState : int;
+
+ enum class EJobType : int;
+ enum class EJobState : int;
+ enum class ETaskName : int;
+ class TTaskName;
+
+ struct TJobBinaryDefault;
+
+ struct TJobBinaryLocalPath;
+
+ struct TJobBinaryCypressPath;
+
+ using TJobBinaryConfig = std::variant<
+ TJobBinaryDefault,
+ TJobBinaryLocalPath,
+ TJobBinaryCypressPath>;
+
+ struct TRetryConfig;
+ class IRetryConfigProvider;
+ using IRetryConfigProviderPtr = ::TIntrusivePtr<IRetryConfigProvider>;
+}
+/// @endcond
diff --git a/yt/cpp/mapreduce/interface/init.h b/yt/cpp/mapreduce/interface/init.h
new file mode 100644
index 0000000000..302be268fc
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/init.h
@@ -0,0 +1,71 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/init.h
+///
+/// Initialization functions of YT Wrapper.
+
+#include <yt/cpp/mapreduce/interface/wait_proxy.h>
+
+#include <util/generic/fwd.h>
+
+#include <functional>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Options for @ref NYT::Initialize() and @ref NYT::JoblessInitialize() functions
+struct TInitializeOptions
+{
+ using TSelf = TInitializeOptions;
+
+ ///
+ /// @brief Override waiting functions for YT Wrapper.
+ ///
+ /// This options allows to override functions used by this library to wait something.
+ FLUENT_FIELD_DEFAULT(::TIntrusivePtr<IWaitProxy>, WaitProxy, nullptr);
+
+ ///
+ /// @brief Enable/disable cleanup when program execution terminates abnormally.
+ ///
+ /// When set to true, library will abort all active transactions and running operations when program
+ /// terminates on error or signal.
+ FLUENT_FIELD_DEFAULT(bool, CleanupOnTermination, false);
+
+ ///
+ /// @brief Set callback to be called before exit() in job mode.
+ ///
+ /// Provided function will be called just before exit() when program is started in job mode.
+ /// This might be useful for shutting down libraries that are used inside operations.
+ ///
+ /// NOTE: Keep in mind that inside job execution environment differs from client execution environment.
+ /// So JobOnExitFunction should not depend on argc/argv environment variables etc.
+ FLUENT_FIELD_OPTION(std::function<void()>, JobOnExitFunction);
+};
+
+///
+/// @brief Performs basic initialization (logging, termination handlers, etc).
+///
+/// This function never switches to job mode.
+void JoblessInitialize(const TInitializeOptions& options = TInitializeOptions());
+
+///
+/// @brief Performs basic initialization and switches to a job mode if required.
+///
+/// This function performs basic initialization (it sets up logging reads the config, etc) and checks if binary is launched
+/// on YT machine inside a job. If latter is true this function launches proper job and after job is done it calls exit().
+///
+/// This function must be called if application starts any operation.
+/// This function must be called immediately after entering main() function before any argument parsing is done.
+void Initialize(int argc, const char **argv, const TInitializeOptions &options = TInitializeOptions());
+
+/// Similar to @ref NYT::Initialize(int, const char**, const TInitializeOptions&)
+void Initialize(int argc, char **argv, const TInitializeOptions &options = TInitializeOptions());
+
+/// Similar to @ref NYT::Initialize(int, const char**, const TInitializeOptions&)
+void Initialize(const TInitializeOptions &options = TInitializeOptions());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/io-inl.h b/yt/cpp/mapreduce/interface/io-inl.h
new file mode 100644
index 0000000000..c35ebb7481
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/io-inl.h
@@ -0,0 +1,1015 @@
+#pragma once
+
+#ifndef IO_INL_H_
+#error "Direct inclusion of this file is not allowed, use io.h"
+#endif
+#undef IO_INL_H_
+
+#include "finish_or_die.h"
+
+#include <util/generic/typetraits.h>
+#include <util/generic/yexception.h>
+#include <util/stream/length.h>
+
+#include <util/system/mutex.h>
+#include <util/system/spinlock.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template<class T>
+struct TIsProtoOneOf
+ : std::false_type
+{ };
+
+template <class ...TProtoRowTypes>
+struct TIsProtoOneOf<TProtoOneOf<TProtoRowTypes...>>
+ : std::true_type
+{ };
+
+template <class T>
+struct TIsSkiffRowOneOf
+ : std::false_type
+{ };
+
+template <class ...TSkiffRowTypes>
+struct TIsSkiffRowOneOf<TSkiffRowOneOf<TSkiffRowTypes...>>
+ : std::true_type
+{ };
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TRowTraits;
+
+template <>
+struct TRowTraits<TNode>
+{
+ using TRowType = TNode;
+ using IReaderImpl = INodeReaderImpl;
+ using IWriterImpl = INodeWriterImpl;
+};
+
+template <>
+struct TRowTraits<TYaMRRow>
+{
+ using TRowType = TYaMRRow;
+ using IReaderImpl = IYaMRReaderImpl;
+ using IWriterImpl = IYaMRWriterImpl;
+};
+
+template <>
+struct TRowTraits<Message>
+{
+ using TRowType = Message;
+ using IReaderImpl = IProtoReaderImpl;
+ using IWriterImpl = IProtoWriterImpl;
+};
+
+template <class T>
+struct TRowTraits<T, std::enable_if_t<TIsBaseOf<Message, T>::Value>>
+{
+ using TRowType = T;
+ using IReaderImpl = IProtoReaderImpl;
+ using IWriterImpl = IProtoWriterImpl;
+};
+
+template <class T>
+struct TRowTraits<T, std::enable_if_t<TIsSkiffRow<T>::value>>
+{
+ using TRowType = T;
+ using IReaderImpl = ISkiffRowReaderImpl;
+};
+
+template <class... TSkiffRowTypes>
+struct TRowTraits<TSkiffRowOneOf<TSkiffRowTypes...>>
+{
+ using TRowType = TSkiffRowOneOf<TSkiffRowTypes...>;
+ using IReaderImpl = ISkiffRowReaderImpl;
+};
+
+template <class... TProtoRowTypes>
+struct TRowTraits<TProtoOneOf<TProtoRowTypes...>>
+{
+ using TRowType = TProtoOneOf<TProtoRowTypes...>;
+ using IReaderImpl = IProtoReaderImpl;
+ using IWriterImpl = IProtoWriterImpl;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IReaderImplBase
+ : public TThrRefBase
+{
+ virtual bool IsValid() const = 0;
+ virtual void Next() = 0;
+ virtual ui32 GetTableIndex() const = 0;
+ virtual ui32 GetRangeIndex() const = 0;
+ virtual ui64 GetRowIndex() const = 0;
+ virtual void NextKey() = 0;
+
+ // Not pure virtual because of clients that has already implemented this interface.
+ virtual TMaybe<size_t> GetReadByteCount() const;
+ virtual i64 GetTabletIndex() const;
+ virtual bool IsEndOfStream() const;
+ virtual bool IsRawReaderExhausted() const;
+};
+
+struct INodeReaderImpl
+ : public IReaderImplBase
+{
+ virtual const TNode& GetRow() const = 0;
+ virtual void MoveRow(TNode* row) = 0;
+};
+
+struct IYaMRReaderImpl
+ : public IReaderImplBase
+{
+ virtual const TYaMRRow& GetRow() const = 0;
+ virtual void MoveRow(TYaMRRow* row)
+ {
+ *row = GetRow();
+ }
+};
+
+struct IProtoReaderImpl
+ : public IReaderImplBase
+{
+ virtual void ReadRow(Message* row) = 0;
+};
+
+struct ISkiffRowReaderImpl
+ : public IReaderImplBase
+{
+ virtual void ReadRow(const ISkiffRowParserPtr& parser) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// We don't include <yt/cpp/mapreduce/interface/logging/yt_log.h> in this file
+// to avoid macro name clashes (specifically YT_LOG_DEBUG)
+void LogTableReaderStatistics(ui64 rowCount, TMaybe<size_t> byteCount);
+
+template <class T>
+class TTableReaderBase
+ : public TThrRefBase
+{
+public:
+ using TRowType = typename TRowTraits<T>::TRowType;
+ using IReaderImpl = typename TRowTraits<T>::IReaderImpl;
+
+ explicit TTableReaderBase(::TIntrusivePtr<IReaderImpl> reader)
+ : Reader_(reader)
+ { }
+
+ ~TTableReaderBase() override
+ {
+ NDetail::LogTableReaderStatistics(ReadRowCount_, Reader_->GetReadByteCount());
+ }
+
+ bool IsValid() const
+ {
+ return Reader_->IsValid();
+ }
+
+ void Next()
+ {
+ Reader_->Next();
+ ++ReadRowCount_;
+ RowState_ = ERowState::None;
+ }
+
+ bool IsEndOfStream()
+ {
+ return Reader_->IsEndOfStream();
+ }
+
+ bool IsRawReaderExhausted()
+ {
+ return Reader_->IsRawReaderExhausted();
+ }
+
+ ui32 GetTableIndex() const
+ {
+ return Reader_->GetTableIndex();
+ }
+
+ ui32 GetRangeIndex() const
+ {
+ return Reader_->GetRangeIndex();
+ }
+
+ ui64 GetRowIndex() const
+ {
+ return Reader_->GetRowIndex();
+ }
+
+ i64 GetTabletIndex() const
+ {
+ return Reader_->GetTabletIndex();
+ }
+
+protected:
+ template <typename TCacher, typename TCacheGetter>
+ const auto& DoGetRowCached(TCacher cacher, TCacheGetter cacheGetter) const
+ {
+ switch (RowState_) {
+ case ERowState::None:
+ cacher();
+ RowState_ = ERowState::Cached;
+ break;
+ case ERowState::Cached:
+ break;
+ case ERowState::MovedOut:
+ ythrow yexception() << "Row is already moved";
+ }
+ return *cacheGetter();
+ }
+
+ template <typename U, typename TMover, typename TCacheMover>
+ void DoMoveRowCached(U* result, TMover mover, TCacheMover cacheMover)
+ {
+ Y_VERIFY(result);
+ switch (RowState_) {
+ case ERowState::None:
+ mover(result);
+ break;
+ case ERowState::Cached:
+ cacheMover(result);
+ break;
+ case ERowState::MovedOut:
+ ythrow yexception() << "Row is already moved";
+ }
+ RowState_ = ERowState::MovedOut;
+ }
+
+private:
+ enum class ERowState
+ {
+ None,
+ Cached,
+ MovedOut,
+ };
+
+protected:
+ ::TIntrusivePtr<IReaderImpl> Reader_;
+
+private:
+ ui64 ReadRowCount_ = 0;
+ mutable ERowState RowState_ = ERowState::None;
+};
+
+template <class T>
+class TSimpleTableReader
+ : public TTableReaderBase<T>
+{
+public:
+ using TBase = TTableReaderBase<T>;
+ using typename TBase::TRowType;
+
+ using TBase::TBase;
+
+ const TRowType& GetRow() const
+ {
+ // Caching is implemented in underlying reader.
+ return TBase::DoGetRowCached(
+ /* cacher */ [&] {},
+ /* cacheGetter */ [&] {
+ return &Reader_->GetRow();
+ });
+ }
+
+ void MoveRow(TRowType* result)
+ {
+ // Caching is implemented in underlying reader.
+ TBase::DoMoveRowCached(
+ result,
+ /* mover */ [&] (TRowType* result) {
+ Reader_->MoveRow(result);
+ },
+ /* cacheMover */ [&] (TRowType* result) {
+ Reader_->MoveRow(result);
+ });
+ }
+
+ TRowType MoveRow()
+ {
+ TRowType result;
+ MoveRow(&result);
+ return result;
+ }
+
+private:
+ using TBase::Reader_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+template <>
+class TTableReader<TNode>
+ : public NDetail::TSimpleTableReader<TNode>
+{
+ using TSimpleTableReader<TNode>::TSimpleTableReader;
+};
+
+template <>
+class TTableReader<TYaMRRow>
+ : public NDetail::TSimpleTableReader<TYaMRRow>
+{
+ using TSimpleTableReader<TYaMRRow>::TSimpleTableReader;
+};
+
+template <>
+class TTableReader<Message>
+ : public NDetail::TTableReaderBase<Message>
+{
+public:
+ using TBase = NDetail::TTableReaderBase<Message>;
+
+ using TBase::TBase;
+
+ template <class U>
+ const U& GetRow() const
+ {
+ static_assert(TIsBaseOf<Message, U>::Value);
+
+ return TBase::DoGetRowCached(
+ /* cacher */ [&] {
+ CachedRow_.Reset(new U);
+ Reader_->ReadRow(CachedRow_.Get());
+ },
+ /* cacheGetter */ [&] {
+ auto result = dynamic_cast<const U*>(CachedRow_.Get());
+ Y_VERIFY(result);
+ return result;
+ });
+ }
+
+ template <class U>
+ void MoveRow(U* result)
+ {
+ static_assert(TIsBaseOf<Message, U>::Value);
+
+ TBase::DoMoveRowCached(
+ result,
+ /* mover */ [&] (U* result) {
+ Reader_->ReadRow(result);
+ },
+ /* cacheMover */ [&] (U* result) {
+ auto cast = dynamic_cast<U*>(CachedRow_.Get());
+ Y_VERIFY(cast);
+ result->Swap(cast);
+ });
+ }
+
+ template <class U>
+ U MoveRow()
+ {
+ static_assert(TIsBaseOf<Message, U>::Value);
+
+ U result;
+ MoveRow(&result);
+ return result;
+ }
+
+ ::TIntrusivePtr<IProtoReaderImpl> GetReaderImpl() const
+ {
+ return Reader_;
+ }
+
+private:
+ using TBase::Reader_;
+ mutable THolder<Message> CachedRow_;
+};
+
+template<class... TProtoRowTypes>
+class TTableReader<TProtoOneOf<TProtoRowTypes...>>
+ : public NDetail::TTableReaderBase<TProtoOneOf<TProtoRowTypes...>>
+{
+public:
+ using TBase = NDetail::TTableReaderBase<TProtoOneOf<TProtoRowTypes...>>;
+
+ using TBase::TBase;
+
+ template <class U>
+ const U& GetRow() const
+ {
+ AssertIsOneOf<U>();
+ return TBase::DoGetRowCached(
+ /* cacher */ [&] {
+ Reader_->ReadRow(&std::get<U>(CachedRows_));
+ CachedIndex_ = NDetail::TIndexInTuple<U, decltype(CachedRows_)>::Value;
+ },
+ /* cacheGetter */ [&] {
+ return &std::get<U>(CachedRows_);
+ });
+ }
+
+ template <class U>
+ void MoveRow(U* result)
+ {
+ AssertIsOneOf<U>();
+ return TBase::DoMoveRowCached(
+ result,
+ /* mover */ [&] (U* result) {
+ Reader_->ReadRow(result);
+ },
+ /* cacheMover */ [&] (U* result) {
+ Y_VERIFY((NDetail::TIndexInTuple<U, decltype(CachedRows_)>::Value) == CachedIndex_);
+ *result = std::move(std::get<U>(CachedRows_));
+ });
+ }
+
+ template <class U>
+ U MoveRow()
+ {
+ U result;
+ MoveRow(&result);
+ return result;
+ }
+
+ ::TIntrusivePtr<IProtoReaderImpl> GetReaderImpl() const
+ {
+ return Reader_;
+ }
+
+private:
+ using TBase::Reader_;
+ // std::variant could also be used here, but std::tuple leads to better performance
+ // because of deallocations that std::variant has to do
+ mutable std::tuple<TProtoRowTypes...> CachedRows_;
+ mutable int CachedIndex_;
+
+ template <class U>
+ static constexpr void AssertIsOneOf()
+ {
+ static_assert(
+ (std::is_same<U, TProtoRowTypes>::value || ...),
+ "Template parameter must be one of TProtoOneOf template parameter");
+ }
+};
+
+template <class T>
+class TTableReader<T, std::enable_if_t<TIsBaseOf<Message, T>::Value>>
+ : public TTableReader<TProtoOneOf<T>>
+{
+public:
+ using TRowType = T;
+ using TBase = TTableReader<TProtoOneOf<T>>;
+
+ using TBase::TBase;
+
+ const T& GetRow() const
+ {
+ return TBase::template GetRow<T>();
+ }
+
+ void MoveRow(T* result)
+ {
+ TBase::template MoveRow<T>(result);
+ }
+
+ T MoveRow()
+ {
+ return TBase::template MoveRow<T>();
+ }
+};
+
+template<class... TSkiffRowTypes>
+class TTableReader<TSkiffRowOneOf<TSkiffRowTypes...>>
+ : public NDetail::TTableReaderBase<TSkiffRowOneOf<TSkiffRowTypes...>>
+{
+public:
+ using TBase = NDetail::TTableReaderBase<TSkiffRowOneOf<TSkiffRowTypes...>>;
+
+ using TBase::TBase;
+
+ explicit TTableReader(::TIntrusivePtr<typename TBase::IReaderImpl> reader, const TMaybe<TSkiffRowHints>& hints)
+ : TBase(reader)
+ , Parsers_({(CreateSkiffParser<TSkiffRowTypes>(&std::get<TSkiffRowTypes>(CachedRows_), hints))...})
+ { }
+
+ template <class U>
+ const U& GetRow() const
+ {
+ AssertIsOneOf<U>();
+ return TBase::DoGetRowCached(
+ /* cacher */ [&] {
+ auto index = NDetail::TIndexInTuple<U, decltype(CachedRows_)>::Value;
+ Reader_->ReadRow(Parsers_[index]);
+ CachedIndex_ = index;
+ },
+ /* cacheGetter */ [&] {
+ return &std::get<U>(CachedRows_);
+ });
+ }
+
+ template <class U>
+ void MoveRow(U* result)
+ {
+ AssertIsOneOf<U>();
+ return TBase::DoMoveRowCached(
+ result,
+ /* mover */ [&] (U* result) {
+ auto index = NDetail::TIndexInTuple<U, decltype(CachedRows_)>::Value;
+ Reader_->ReadRow(Parsers_[index]);
+ *result = std::move(std::get<U>(CachedRows_));
+ },
+ /* cacheMover */ [&] (U* result) {
+ Y_VERIFY((NDetail::TIndexInTuple<U, decltype(CachedRows_)>::Value) == CachedIndex_);
+ *result = std::move(std::get<U>(CachedRows_));
+ });
+ }
+
+ template <class U>
+ U MoveRow()
+ {
+ U result;
+ MoveRow(&result);
+ return result;
+ }
+
+ ::TIntrusivePtr<ISkiffRowReaderImpl> GetReaderImpl() const
+ {
+ return Reader_;
+ }
+
+private:
+ using TBase::Reader_;
+ // std::variant could also be used here, but std::tuple leads to better performance
+ // because of deallocations that std::variant has to do
+ mutable std::tuple<TSkiffRowTypes...> CachedRows_;
+ mutable std::vector<ISkiffRowParserPtr> Parsers_;
+ mutable int CachedIndex_;
+
+ template <class U>
+ static constexpr void AssertIsOneOf()
+ {
+ static_assert(
+ (std::is_same<U, TSkiffRowTypes>::value || ...),
+ "Template parameter must be one of TSkiffRowOneOf template parameter");
+ }
+};
+
+template <class T>
+class TTableReader<T, std::enable_if_t<TIsSkiffRow<T>::value>>
+ : public TTableReader<TSkiffRowOneOf<T>>
+{
+public:
+ using TRowType = T;
+ using TBase = TTableReader<TSkiffRowOneOf<T>>;
+
+ using TBase::TBase;
+
+ const T& GetRow()
+ {
+ return TBase::template GetRow<T>();
+ }
+
+ void MoveRow(T* result)
+ {
+ TBase::template MoveRow<T>(result);
+ }
+
+ T MoveRow()
+ {
+ return TBase::template MoveRow<T>();
+ }
+};
+
+template <>
+inline TTableReaderPtr<TNode> IIOClient::CreateTableReader<TNode>(
+ const TRichYPath& path, const TTableReaderOptions& options)
+{
+ return new TTableReader<TNode>(CreateNodeReader(path, options));
+}
+
+template <>
+inline TTableReaderPtr<TYaMRRow> IIOClient::CreateTableReader<TYaMRRow>(
+ const TRichYPath& path, const TTableReaderOptions& options)
+{
+ return new TTableReader<TYaMRRow>(CreateYaMRReader(path, options));
+}
+
+template <class T, class = std::enable_if_t<TIsBaseOf<Message, T>::Value>>
+struct TReaderCreator
+{
+ static TTableReaderPtr<T> Create(::TIntrusivePtr<IProtoReaderImpl> reader)
+ {
+ return new TTableReader<T>(reader);
+ }
+};
+
+template <class T>
+inline TTableReaderPtr<T> IIOClient::CreateTableReader(
+ const TRichYPath& path, const TTableReaderOptions& options)
+{
+ if constexpr (TIsBaseOf<Message, T>::Value) {
+ TAutoPtr<T> prototype(new T);
+ return new TTableReader<T>(CreateProtoReader(path, options, prototype.Get()));
+ } else if constexpr (TIsSkiffRow<T>::value) {
+ const auto& hints = options.FormatHints_ ? options.FormatHints_->SkiffRowHints_ : Nothing();
+ auto schema = GetSkiffSchema<T>(hints);
+ auto skipper = CreateSkiffSkipper<T>(hints);
+ return new TTableReader<T>(CreateSkiffRowReader(path, options, skipper, schema), hints);
+ } else {
+ static_assert(TDependentFalse<T>, "Unsupported type for table reader");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+TTableReaderPtr<T> CreateTableReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options)
+{
+ return TReaderCreator<T>::Create(NDetail::CreateProtoReader(stream, options, T::descriptor()));
+}
+
+template <class... Ts>
+TTableReaderPtr<typename NDetail::TProtoOneOfUnique<Ts...>::TType> CreateProtoMultiTableReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options)
+{
+ return new TTableReader<typename NDetail::TProtoOneOfUnique<Ts...>::TType>(
+ NDetail::CreateProtoReader(stream, options, {Ts::descriptor()...}));
+}
+
+template <class T>
+TTableReaderPtr<T> CreateProtoMultiTableReader(
+ IInputStream* stream,
+ int tableCount,
+ const TTableReaderOptions& options)
+{
+ static_assert(TIsBaseOf<::google::protobuf::Message, T>::Value);
+ TVector<const ::google::protobuf::Descriptor*> descriptors(tableCount, T::descriptor());
+ return new TTableReader<T>(NDetail::CreateProtoReader(stream, options, std::move(descriptors)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TTableRangesReader<T>
+ : public TThrRefBase
+{
+public:
+ using TRowType = T;
+
+private:
+ using TReaderImpl = typename TRowTraits<TRowType>::IReaderImpl;
+
+public:
+ TTableRangesReader(::TIntrusivePtr<TReaderImpl> readerImpl)
+ : ReaderImpl_(readerImpl)
+ , Reader_(MakeIntrusive<TTableReader<TRowType>>(readerImpl))
+ , IsValid_(Reader_->IsValid())
+ { }
+
+ TTableReader<T>& GetRange()
+ {
+ return *Reader_;
+ }
+
+ bool IsValid() const
+ {
+ return IsValid_;
+ }
+
+ void Next()
+ {
+ ReaderImpl_->NextKey();
+ if ((IsValid_ = Reader_->IsValid())) {
+ Reader_->Next();
+ }
+ }
+
+private:
+ ::TIntrusivePtr<TReaderImpl> ReaderImpl_;
+ ::TIntrusivePtr<TTableReader<TRowType>> Reader_;
+ bool IsValid_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+struct IWriterImplBase
+ : public TThrRefBase
+{
+ virtual void AddRow(const T& row, size_t tableIndex) = 0;
+
+ virtual void AddRow(const T& row, size_t tableIndex, size_t /*rowWeight*/)
+ {
+ AddRow(row, tableIndex);
+ }
+
+ virtual void AddRow(T&& row, size_t tableIndex) = 0;
+
+ virtual void AddRow(T&& row, size_t tableIndex, size_t /*rowWeight*/)
+ {
+ AddRow(std::move(row), tableIndex);
+ }
+
+ virtual void AddRowBatch(const TVector<T>& rowBatch, size_t tableIndex, size_t rowBatchWeight = 0)
+ {
+ for (const auto& row : rowBatch) {
+ AddRow(row, tableIndex, rowBatchWeight / rowBatch.size());
+ }
+ }
+
+ virtual void AddRowBatch(TVector<T>&& rowBatch, size_t tableIndex, size_t rowBatchWeight = 0)
+ {
+ auto rowBatchSize = rowBatch.size();
+ for (auto&& row : std::move(rowBatch)) {
+ AddRow(std::move(row), tableIndex, rowBatchWeight / rowBatchSize);
+ }
+ }
+
+ virtual size_t GetTableCount() const = 0;
+ virtual void FinishTable(size_t tableIndex) = 0;
+ virtual void Abort()
+ { }
+};
+
+struct INodeWriterImpl
+ : public IWriterImplBase<TNode>
+{
+};
+
+struct IYaMRWriterImpl
+ : public IWriterImplBase<TYaMRRow>
+{
+};
+
+struct IProtoWriterImpl
+ : public IWriterImplBase<Message>
+{
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TTableWriterBase
+ : public TThrRefBase
+{
+public:
+ using TRowType = T;
+ using IWriterImpl = typename TRowTraits<T>::IWriterImpl;
+
+ explicit TTableWriterBase(::TIntrusivePtr<IWriterImpl> writer)
+ : Writer_(writer)
+ , Locks_(MakeAtomicShared<TVector<TAdaptiveLock>>(writer->GetTableCount()))
+ { }
+
+ ~TTableWriterBase() override
+ {
+ if (Locks_.RefCount() == 1) {
+ NDetail::FinishOrDie(this, "TTableWriterBase");
+ }
+ }
+
+ void Abort()
+ {
+ Writer_->Abort();
+ }
+
+ void AddRow(const T& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ DoAddRow<T>(row, tableIndex, rowWeight);
+ }
+
+ void AddRow(T&& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ DoAddRow<T>(std::move(row), tableIndex, rowWeight);
+ }
+
+ void AddRowBatch(const TVector<T>& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ DoAddRowBatch<T>(rowBatch, tableIndex, rowBatchWeight);
+ }
+
+ void AddRowBatch(TVector<T>&& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ DoAddRowBatch<T>(std::move(rowBatch), tableIndex, rowBatchWeight);
+ }
+
+ void Finish()
+ {
+ for (size_t i = 0; i < Locks_->size(); ++i) {
+ auto guard = Guard((*Locks_)[i]);
+ Writer_->FinishTable(i);
+ }
+ }
+
+protected:
+ template <class U>
+ void DoAddRow(const U& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ if (tableIndex >= Locks_->size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << Locks_->size() << ")";
+ }
+
+ auto guard = Guard((*Locks_)[tableIndex]);
+ Writer_->AddRow(row, tableIndex, rowWeight);
+ }
+
+ template <class U>
+ void DoAddRow(U&& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ if (tableIndex >= Locks_->size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << Locks_->size() << ")";
+ }
+
+ auto guard = Guard((*Locks_)[tableIndex]);
+ Writer_->AddRow(std::move(row), tableIndex, rowWeight);
+ }
+
+ template <class U>
+ void DoAddRowBatch(const TVector<U>& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ if (tableIndex >= Locks_->size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << Locks_->size() << ")";
+ }
+
+ auto guard = Guard((*Locks_)[tableIndex]);
+ Writer_->AddRowBatch(rowBatch, tableIndex, rowBatchWeight);
+ }
+
+ template <class U>
+ void DoAddRowBatch(TVector<U>&& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ if (tableIndex >= Locks_->size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << Locks_->size() << ")";
+ }
+
+ auto guard = Guard((*Locks_)[tableIndex]);
+ Writer_->AddRowBatch(std::move(rowBatch), tableIndex, rowBatchWeight);
+ }
+
+ ::TIntrusivePtr<IWriterImpl> GetWriterImpl()
+ {
+ return Writer_;
+ }
+
+private:
+ ::TIntrusivePtr<IWriterImpl> Writer_;
+ TAtomicSharedPtr<TVector<TAdaptiveLock>> Locks_;
+};
+
+template <>
+class TTableWriter<TNode>
+ : public TTableWriterBase<TNode>
+{
+public:
+ using TBase = TTableWriterBase<TNode>;
+
+ explicit TTableWriter(::TIntrusivePtr<IWriterImpl> writer)
+ : TBase(writer)
+ { }
+};
+
+template <>
+class TTableWriter<TYaMRRow>
+ : public TTableWriterBase<TYaMRRow>
+{
+public:
+ using TBase = TTableWriterBase<TYaMRRow>;
+
+ explicit TTableWriter(::TIntrusivePtr<IWriterImpl> writer)
+ : TBase(writer)
+ { }
+};
+
+template <>
+class TTableWriter<Message>
+ : public TTableWriterBase<Message>
+{
+public:
+ using TBase = TTableWriterBase<Message>;
+
+ explicit TTableWriter(::TIntrusivePtr<IWriterImpl> writer)
+ : TBase(writer)
+ { }
+
+ template <class U, std::enable_if_t<std::is_base_of<Message, U>::value>* = nullptr>
+ void AddRow(const U& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ TBase::AddRow(row, tableIndex, rowWeight);
+ }
+
+ template <class U, std::enable_if_t<std::is_base_of<Message, U>::value>* = nullptr>
+ void AddRowBatch(const TVector<U>& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ for (const auto& row : rowBatch) {
+ AddRow(row, tableIndex, rowBatchWeight / rowBatch.size());
+ }
+ }
+};
+
+template <class T>
+class TTableWriter<T, std::enable_if_t<TIsBaseOf<Message, T>::Value>>
+ : public TTableWriter<Message>
+{
+public:
+ using TRowType = T;
+ using TBase = TTableWriter<Message>;
+
+ explicit TTableWriter(::TIntrusivePtr<IWriterImpl> writer)
+ : TBase(writer)
+ { }
+
+ void AddRow(const T& row, size_t tableIndex = 0, size_t rowWeight = 0)
+ {
+ TBase::AddRow<T>(row, tableIndex, rowWeight);
+ }
+
+ void AddRowBatch(const TVector<T>& rowBatch, size_t tableIndex = 0, size_t rowBatchWeight = 0)
+ {
+ TBase::AddRowBatch<T>(rowBatch, tableIndex, rowBatchWeight);
+ }
+};
+
+template <>
+inline TTableWriterPtr<TNode> IIOClient::CreateTableWriter<TNode>(
+ const TRichYPath& path, const TTableWriterOptions& options)
+{
+ return new TTableWriter<TNode>(CreateNodeWriter(path, options));
+}
+
+template <>
+inline TTableWriterPtr<TYaMRRow> IIOClient::CreateTableWriter<TYaMRRow>(
+ const TRichYPath& path, const TTableWriterOptions& options)
+{
+ return new TTableWriter<TYaMRRow>(CreateYaMRWriter(path, options));
+}
+
+template <class T>
+inline TTableWriterPtr<T> IIOClient::CreateTableWriter(
+ const TRichYPath& path, const TTableWriterOptions& options)
+{
+ if constexpr (TIsBaseOf<Message, T>::Value) {
+ TAutoPtr<T> prototype(new T);
+ return new TTableWriter<T>(CreateProtoWriter(path, options, prototype.Get()));
+ } else {
+ static_assert(TDependentFalse<T>, "Unsupported type for table writer");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+TTableReaderPtr<T> CreateConcreteProtobufReader(TTableReader<Message>* reader)
+{
+ static_assert(std::is_base_of_v<Message, T>, "T must be a protobuf type (either Message or its descendant)");
+ Y_ENSURE(reader, "reader must be non-null");
+ return ::MakeIntrusive<TTableReader<T>>(reader->GetReaderImpl());
+}
+
+template <typename T>
+TTableReaderPtr<T> CreateConcreteProtobufReader(const TTableReaderPtr<Message>& reader)
+{
+ Y_ENSURE(reader, "reader must be non-null");
+ return CreateConcreteProtobufReader<T>(reader.Get());
+}
+
+template <typename T>
+TTableReaderPtr<Message> CreateGenericProtobufReader(TTableReader<T>* reader)
+{
+ static_assert(std::is_base_of_v<Message, T>, "T must be a protobuf type (either Message or its descendant)");
+ Y_ENSURE(reader, "reader must be non-null");
+ return ::MakeIntrusive<TTableReader<Message>>(reader->GetReaderImpl());
+}
+
+template <typename T>
+TTableReaderPtr<Message> CreateGenericProtobufReader(const TTableReaderPtr<T>& reader)
+{
+ Y_ENSURE(reader, "reader must be non-null");
+ return CreateGenericProtobufReader(reader.Get());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/io.cpp b/yt/cpp/mapreduce/interface/io.cpp
new file mode 100644
index 0000000000..f97629721a
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/io.cpp
@@ -0,0 +1,47 @@
+#include "io.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <util/string/cast.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMaybe<size_t> IReaderImplBase::GetReadByteCount() const
+{
+ return Nothing();
+}
+
+i64 IReaderImplBase::GetTabletIndex() const
+{
+ Y_FAIL("Unimplemented");
+}
+
+bool IReaderImplBase::IsEndOfStream() const
+{
+ Y_FAIL("Unimplemented");
+}
+
+bool IReaderImplBase::IsRawReaderExhausted() const
+{
+ Y_FAIL("Unimplemented");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+void LogTableReaderStatistics(ui64 rowCount, TMaybe<size_t> byteCount)
+{
+ TString byteCountStr = (byteCount ? ::ToString(*byteCount) : "<unknown>");
+ YT_LOG_DEBUG("Table reader has read %v rows, %v bytes",
+ rowCount,
+ byteCountStr);
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/io.h b/yt/cpp/mapreduce/interface/io.h
new file mode 100644
index 0000000000..e2b20a1802
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/io.h
@@ -0,0 +1,586 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/io.h
+///
+/// Header containing client interface for reading and writing tables and files.
+
+
+#include "fwd.h"
+
+#include "client_method_options.h"
+#include "common.h"
+#include "format.h"
+#include "node.h"
+#include "mpl.h"
+#include "skiff_row.h"
+
+#include <google/protobuf/message.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+#include <util/generic/yexception.h>
+#include <util/generic/maybe.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief "Marker" type to use for several protobuf types in @ref NYT::TTableReader.
+///
+/// @tparam Ts Possible types of rows to be read.
+template<class... TProtoRowTypes>
+class TProtoOneOf
+{
+public:
+ static_assert(
+ (TIsBaseOf<::google::protobuf::Message, TProtoRowTypes>::Value && ...),
+ "Template parameters can only be protobuf types");
+
+ TProtoOneOf() = delete;
+};
+
+///
+/// @brief "Marker" type to use for several skiff row types in @ref NYT::TTableReader.
+///
+/// @tparam Ts Possible types of rows to be read.
+template<class... TSkiffRowTypes>
+class TSkiffRowOneOf
+{
+public:
+ static_assert(
+ (TIsSkiffRow<TSkiffRowTypes>::value && ...),
+ "Template parameters can only be SkiffRow types");
+
+ TSkiffRowOneOf() = delete;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @cond Doxygen_Suppress
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTuple>
+struct TProtoOneOfFromTuple;
+
+template <class... Ts>
+struct TProtoOneOfFromTuple<std::tuple<Ts...>>
+{
+ using TType = TProtoOneOf<Ts...>;
+};
+
+template <class... Ts>
+struct TProtoOneOfUnique
+{
+ using TTuple = typename TUniqueTypes<std::tuple<>, std::tuple<Ts...>>::TType;
+ using TType = typename TProtoOneOfFromTuple<TTuple>::TType;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+/// @endcond
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct INodeReaderImpl;
+struct IYaMRReaderImpl;
+struct IProtoReaderImpl;
+struct ISkiffRowReaderImpl;
+struct INodeWriterImpl;
+struct IYaMRWriterImpl;
+struct IProtoWriterImpl;
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Class of exceptions connected to reading or writing tables or files.
+class TIOException
+ : public yexception
+{ };
+
+///////////////////////////////////////////////////////////////////////////////
+
+/// Interface representing YT file reader.
+class IFileReader
+ : public TThrRefBase
+ , public IInputStream
+{ };
+
+/// Interface representing YT file writer.
+class IFileWriter
+ : public TThrRefBase
+ , public IOutputStream
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Low-level interface to read YT table with retries.
+class TRawTableReader
+ : public TThrRefBase
+ , public IInputStream
+{
+public:
+ /// @brief Retry table read starting from the specified `rangeIndex` and `rowIndex`.
+ ///
+ /// @param rangeIndex Index of first range to read
+ /// @param rowIndex Index of first row to read; if `rowIndex == Nothing` entire request will be retried.
+ ///
+ /// @return `true` on successful request retry, `false` if no retry attempts are left (then `Retry()` shouldn't be called any more).
+ ///
+ /// `rowIndex` must be inside the range with index `rangeIndex` if the latter is specified.
+ ///
+ /// After successful retry the user should reset `rangeIndex` / `rowIndex` values and read new ones
+ /// from the stream.
+ virtual bool Retry(
+ const TMaybe<ui32>& rangeIndex,
+ const TMaybe<ui64>& rowIndex) = 0;
+
+ /// Resets retry attempt count to the initial value (then `Retry()` can be called again).
+ virtual void ResetRetries() = 0;
+
+ /// @brief May the input stream contain table ranges?
+ ///
+ /// In the case when it is `true` the `TRawTableReader` user is responsible
+ /// to track active range index in order to pass it to Retry().
+ virtual bool HasRangeIndices() const = 0;
+};
+
+/// @brief Low-level interface to write YT table.
+///
+/// Retries must be handled by implementation.
+class TRawTableWriter
+ : public TThrRefBase
+ , public IOutputStream
+{
+public:
+ /// @brief Call this method after complete row representation is written to the stream.
+ ///
+ /// When this method is called `TRowTableWriter` can check its buffer
+ /// and if it is full send data to YT.
+ /// @note `TRawTableWriter` never sends partial records to YT (due to retries).
+ virtual void NotifyRowEnd() = 0;
+
+ /// @brief Try to abort writing process as soon as possible (makes sense for multi-threaded writers).
+ ///
+ /// By default it does nothing, but implementations are welcome to override this method.
+ virtual void Abort()
+ { }
+};
+
+/// @brief Interface to deal with multiple raw output streams.
+class IProxyOutput
+{
+public:
+ virtual ~IProxyOutput()
+ { }
+
+ /// Get amount of managed streams.
+ virtual size_t GetStreamCount() const = 0;
+
+ /// Get stream corresponding to the specified table index.
+ virtual IOutputStream* GetStream(size_t tableIndex) const = 0;
+
+ /// This handler must be called right after the next row has been written.
+ virtual void OnRowFinished(size_t tableIndex) = 0;
+
+ /// @brief Try to abort writing process as soon as possible (makes sense for multi-threaded writers).
+ ///
+ /// By default it does nothing, but implementations are welcome to override this method.
+ virtual void Abort()
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Class template to read typed rows from YT tables.
+///
+/// @tparam T Row type.
+///
+/// Correct usage of this class usually looks like
+/// ```
+/// for (const auto& cursor : *reader) {
+/// const auto& row = cursor.GetRow();
+/// ...
+/// }
+/// ```
+/// or, more verbosely,
+/// ```
+/// for (; reader->IsValid(); reader->Next()) {
+/// const auto& row = reader->GetRow();
+/// ...
+/// }
+/// ```
+///
+/// @note Actual (partial) specializations of this template may look a bit different,
+/// e.g. @ref NYT::TTableReader::GetRow, @ref NYT::TTableReader::MoveRow may be method templates.
+template <class T, class>
+class TTableReader
+ : public TThrRefBase
+{
+public:
+ /// Get current row.
+ const T& GetRow() const;
+
+ /// Extract current row; further calls to `GetRow` and `MoveRow` will fail.
+ T MoveRow();
+
+ /// Extract current row to `result`; further calls to `GetRow` and `MoveRow` will fail.
+ void MoveRow(T* result);
+
+ /// Check whether all the rows were read.
+ bool IsValid() const;
+
+ /// Move the cursor to the next row.
+ void Next();
+
+ /// Get table index of the current row.
+ ui32 GetTableIndex() const;
+
+ /// Get range index of the current row (zero if it is unknown or read request contains no ranges)
+ ui32 GetRangeIndex() const;
+
+ /// Get current row index (zero if it unknown).
+ ui64 GetRowIndex() const;
+
+ /// Get current tablet index (for ordered dynamic tables).
+ i64 GetTabletIndex() const;
+
+ /// Returns `true` if job consumed all the input and `false` otherwise.
+ bool IsEndOfStream() const;
+
+ /// Returns `true` if job raw input stream was closed and `false` otherwise.
+ bool IsRawReaderExhausted() const;
+};
+
+/// @brief Iterator for use in range-based-for.
+///
+/// @note Idiomatic usage:
+/// ```
+/// for (const auto& cursor : *reader) {
+/// const auto& row = cursor.GetRow();
+/// ...
+/// }
+/// ```
+template <class T>
+class TTableReaderIterator
+{
+public:
+ /// Construct iterator from table reader (can be `nullptr`).
+ explicit TTableReaderIterator<T>(TTableReader<T>* reader)
+ {
+ if (reader && reader->IsValid()) {
+ Reader_ = reader;
+ } else {
+ Reader_ = nullptr;
+ }
+ }
+
+ /// Equality operator.
+ bool operator==(const TTableReaderIterator& it) const
+ {
+ return Reader_ == it.Reader_;
+ }
+
+ /// Inequality operator.
+ bool operator!=(const TTableReaderIterator& it) const
+ {
+ return Reader_ != it.Reader_;
+ }
+
+ /// Dereference operator.
+ TTableReader<T>& operator*()
+ {
+ return *Reader_;
+ }
+
+ /// Const dereference operator.
+ const TTableReader<T>& operator*() const
+ {
+ return *Reader_;
+ }
+
+ /// Preincrement operator.
+ TTableReaderIterator& operator++()
+ {
+ Reader_->Next();
+ if (!Reader_->IsValid()) {
+ Reader_ = nullptr;
+ }
+ return *this;
+ }
+
+private:
+ TTableReader<T>* Reader_;
+};
+
+/// @brief Function to facilitate range-based-for for @ref NYT::TTableReader.
+///
+/// @see @ref NYT::TTableReaderIterator
+template <class T>
+TTableReaderIterator<T> begin(TTableReader<T>& reader)
+{
+ return TTableReaderIterator<T>(&reader);
+}
+
+/// @brief Function to facilitate range-based-for for @ref NYT::TTableReader.
+///
+/// @see @ref NYT::TTableReaderIterator
+template <class T>
+TTableReaderIterator<T> end(TTableReader<T>&)
+{
+ return TTableReaderIterator<T>(nullptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Class to facilitate reading table rows sorted by key.
+///
+/// Each reader returned from @ref NYT::TTableRangesReader::GetRange represents
+/// a range of rows with the same key.
+///
+/// @note Idiomatic usage:
+/// ```
+/// for (; reader->IsValid(); reader->Next()) {
+/// auto& rangeReader = reader->GetRange();
+/// ...
+/// }
+/// ```
+template <class T, class>
+class TTableRangesReader
+ : public TThrRefBase
+{
+public:
+ /// Get reader for rows with the same key.
+ TTableReader<T>& GetRange();
+
+ /// Check whether all rows are read.
+ bool IsValid() const;
+
+ /// Move cursor to the next range.
+ void Next();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Class template to write typed rows to YT tables.
+template <class T, class>
+class TTableWriter
+ : public TThrRefBase
+{
+public:
+ /// @brief Submit a row for writing.
+ ///
+ /// The row may (and very probably will) *not* be written immediately.
+ void AddRow(const T& row);
+
+ /// Stop writing data as soon as possible (without flushing data, e.g. before aborting parent transaction).
+ void Finish();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Type representing YaMR table row.
+///
+/// @deprecated
+struct TYaMRRow
+{
+ /// Key column.
+ TStringBuf Key;
+
+ /// Subkey column.
+ TStringBuf SubKey;
+
+ /// Value column.
+ TStringBuf Value;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Interface for creating table and file readers and writer.
+class IIOClient
+{
+public:
+ virtual ~IIOClient() = default;
+
+ /// Create a reader for file at `path`.
+ virtual IFileReaderPtr CreateFileReader(
+ const TRichYPath& path,
+ const TFileReaderOptions& options = TFileReaderOptions()) = 0;
+
+ /// Create a writer for file at `path`.
+ virtual IFileWriterPtr CreateFileWriter(
+ const TRichYPath& path,
+ const TFileWriterOptions& options = TFileWriterOptions()) = 0;
+
+ /// Create a typed reader for table at `path`.
+ template <class T>
+ TTableReaderPtr<T> CreateTableReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options = TTableReaderOptions());
+
+ /// Create a typed writer for table at `path`.
+ template <class T>
+ TTableWriterPtr<T> CreateTableWriter(
+ const TRichYPath& path,
+ const TTableWriterOptions& options = TTableWriterOptions());
+
+ /// Create a writer to write protobuf messages with specified descriptor.
+ virtual TTableWriterPtr<::google::protobuf::Message> CreateTableWriter(
+ const TRichYPath& path,
+ const ::google::protobuf::Descriptor& descriptor,
+ const TTableWriterOptions& options = TTableWriterOptions()) = 0;
+
+ /// Create a reader to read a table using specified format.
+ virtual TRawTableReaderPtr CreateRawReader(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableReaderOptions& options = TTableReaderOptions()) = 0;
+
+ /// Create a reader to write a table using specified format.
+ virtual TRawTableWriterPtr CreateRawWriter(
+ const TRichYPath& path,
+ const TFormat& format,
+ const TTableWriterOptions& options = TTableWriterOptions()) = 0;
+
+ ///
+ /// @brief Create a reader for [blob table](https://docs.yandex-team.ru/docs/yt/description/storage/blobtables) at `path`.
+ ///
+ /// @param path Blob table path.
+ /// @param blobId Key identifying the blob.
+ /// @param options Optional parameters
+ ///
+ /// Blob table is a table that stores a number of blobs.
+ /// Blobs are sliced into parts of the same size (maybe except of last part).
+ /// Those parts are stored in the separate rows.
+ ///
+ /// Blob table have constraints on its schema.
+ /// - There must be columns that identify blob (blob id columns). That columns might be of any type.
+ /// - There must be a column of `int64` type that identify part inside the blob (this column is called `part index`).
+ /// - There must be a column of `string` type that stores actual data (this column is called `data column`).
+ virtual IFileReaderPtr CreateBlobTableReader(
+ const TYPath& path,
+ const TKey& blobId,
+ const TBlobTableReaderOptions& options = TBlobTableReaderOptions()) = 0;
+
+private:
+ virtual ::TIntrusivePtr<INodeReaderImpl> CreateNodeReader(
+ const TRichYPath& path, const TTableReaderOptions& options) = 0;
+
+ virtual ::TIntrusivePtr<IYaMRReaderImpl> CreateYaMRReader(
+ const TRichYPath& path, const TTableReaderOptions& options) = 0;
+
+ virtual ::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const ::google::protobuf::Message* prototype) = 0;
+
+ virtual ::TIntrusivePtr<ISkiffRowReaderImpl> CreateSkiffRowReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options,
+ const ISkiffRowSkipperPtr& skipper,
+ const NSkiff::TSkiffSchemaPtr& schema) = 0;
+
+ virtual ::TIntrusivePtr<INodeWriterImpl> CreateNodeWriter(
+ const TRichYPath& path, const TTableWriterOptions& options) = 0;
+
+ virtual ::TIntrusivePtr<IYaMRWriterImpl> CreateYaMRWriter(
+ const TRichYPath& path, const TTableWriterOptions& options) = 0;
+
+ virtual ::TIntrusivePtr<IProtoWriterImpl> CreateProtoWriter(
+ const TRichYPath& path,
+ const TTableWriterOptions& options,
+ const ::google::protobuf::Message* prototype) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Create a protobuf table reader from a stream.
+///
+/// @tparam T Protobuf message type to read (must be inherited from `Message`).
+///
+/// @param stream Input stream in YT protobuf format.
+template <typename T>
+TTableReaderPtr<T> CreateTableReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options = {});
+
+///
+/// @brief Create a protobuf multi table reader from a stream.
+///
+/// @tparam Ts Protobuf message types to read (must be inherited from `Message`).
+///
+/// @param stream Input stream in YT protobuf format.
+template <class... Ts>
+TTableReaderPtr<typename NDetail::TProtoOneOfUnique<Ts...>::TType> CreateProtoMultiTableReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options = {});
+
+///
+/// @brief Create a homogenous protobuf multi table reader from a stream.
+///
+/// @tparam T Protobuf message type to read (must be inherited from `Message`).
+///
+/// @param stream Input stream in YT protobuf format.
+/// @param tableCount Number of tables in input stream.
+template <class T>
+TTableReaderPtr<T> CreateProtoMultiTableReader(
+ IInputStream* stream,
+ int tableCount,
+ const TTableReaderOptions& options = {});
+
+/// Create a @ref NYT::TNode table reader from a stream.
+template <>
+TTableReaderPtr<TNode> CreateTableReader<TNode>(
+ IInputStream* stream, const TTableReaderOptions& options);
+
+/// Create a @ref NYT::TYaMRRow table reader from a stream.
+template <>
+TTableReaderPtr<TYaMRRow> CreateTableReader<TYaMRRow>(
+ IInputStream* stream, const TTableReaderOptions& options);
+
+namespace NDetail {
+
+/// Create a protobuf table reader from a stream.
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options,
+ const ::google::protobuf::Descriptor* descriptor);
+
+
+/// Create a protobuf table reader from a stream that can contain table switches.
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& options,
+ TVector<const ::google::protobuf::Descriptor*> descriptors);
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Convert generic protobuf table reader to a concrete one (for certain type `T`).
+template <typename T>
+TTableReaderPtr<T> CreateConcreteProtobufReader(TTableReader<Message>* reader);
+
+/// Convert generic protobuf table reader to a concrete one (for certain type `T`).
+template <typename T>
+TTableReaderPtr<T> CreateConcreteProtobufReader(const TTableReaderPtr<Message>& reader);
+
+/// Convert a concrete (for certain type `T`) protobuf table reader to a generic one.
+template <typename T>
+TTableReaderPtr<Message> CreateGenericProtobufReader(TTableReader<T>* reader);
+
+/// Convert a concrete (for certain type `T`) protobuf table reader to a generic one.
+template <typename T>
+TTableReaderPtr<Message> CreateGenericProtobufReader(const TTableReaderPtr<T>& reader);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define IO_INL_H_
+#include "io-inl.h"
+#undef IO_INL_H_
diff --git a/yt/cpp/mapreduce/interface/job_counters.cpp b/yt/cpp/mapreduce/interface/job_counters.cpp
new file mode 100644
index 0000000000..6d4a2a6fcb
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_counters.cpp
@@ -0,0 +1,164 @@
+#include "job_counters.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////
+
+namespace {
+ ui64 CountTotal(const TNode& data)
+ {
+ if (data.IsMap()) {
+ if (auto totalPtr = data.AsMap().FindPtr("total")) {
+ return data["total"].IntCast<ui64>();
+ } else {
+ ui64 total = 0;
+ for (const auto& keyVal: data.AsMap()) {
+ total += CountTotal(keyVal.second);
+ }
+ return total;
+ }
+ } else {
+ return data.IntCast<ui64>();
+ }
+ }
+
+ TNode GetNode(const TNode& data, const TStringBuf& key)
+ {
+ if (auto resPtr = data.AsMap().FindPtr(key)) {
+ return *resPtr;
+ }
+ return TNode();
+ }
+} // namespace
+
+////////////////////////////////////////////////////////////////////
+
+TJobCounter::TJobCounter(TNode data)
+ : Data_(std::move(data))
+{
+ if (Data_.HasValue()) {
+ Total_ = CountTotal(Data_);
+ }
+}
+
+TJobCounter::TJobCounter(ui64 total)
+ : Total_(total)
+{ }
+
+ui64 TJobCounter::GetTotal() const
+{
+ return Total_;
+}
+
+ui64 TJobCounter::GetValue(const TStringBuf key) const
+{
+ if (Data_.HasValue()) {
+ return CountTotal(Data_[key]);
+ }
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////
+
+TJobCounters::TJobCounters(const NYT::TNode& counters)
+ : Total_(0)
+{
+ if (!counters.IsMap()) {
+ ythrow yexception() << "TJobCounters must be initialized with Map type TNode";
+ }
+ auto abortedNode = GetNode(counters, "aborted");
+ if (abortedNode.HasValue()) {
+ Aborted_ = TJobCounter(GetNode(abortedNode, "total"));
+ AbortedScheduled_ = TJobCounter(GetNode(abortedNode, "scheduled"));
+ AbortedNonScheduled_ = TJobCounter(GetNode(abortedNode, "non_scheduled"));
+ }
+ auto completedNode = GetNode(counters, "completed");
+ if (completedNode.HasValue()) {
+ Completed_ = TJobCounter(GetNode(completedNode, "total"));
+ CompletedNonInterrupted_ = TJobCounter(GetNode(completedNode, "non-interrupted"));
+ CompletedInterrupted_ = TJobCounter(GetNode(completedNode, "interrupted"));
+ }
+ Lost_ = TJobCounter(GetNode(counters, "lost"));
+ Invalidated_ = TJobCounter(GetNode(counters, "invalidated"));
+ Failed_ = TJobCounter(GetNode(counters, "failed"));
+ Running_ = TJobCounter(GetNode(counters, "running"));
+ Suspended_ = TJobCounter(GetNode(counters, "suspended"));
+ Pending_ = TJobCounter(GetNode(counters, "pending"));
+ Blocked_ = TJobCounter(GetNode(counters, "blocked"));
+ Total_ = CountTotal(counters);
+}
+
+
+const TJobCounter& TJobCounters::GetAborted() const
+{
+ return Aborted_;
+}
+
+const TJobCounter& TJobCounters::GetAbortedScheduled() const
+{
+ return AbortedScheduled_;
+}
+
+const TJobCounter& TJobCounters::GetAbortedNonScheduled() const
+{
+ return AbortedNonScheduled_;
+}
+
+const TJobCounter& TJobCounters::GetCompleted() const
+{
+ return Completed_;
+}
+
+const TJobCounter& TJobCounters::GetCompletedNonInterrupted() const
+{
+ return CompletedNonInterrupted_;
+}
+
+const TJobCounter& TJobCounters::GetCompletedInterrupted() const
+{
+ return CompletedInterrupted_;
+}
+
+const TJobCounter& TJobCounters::GetLost() const
+{
+ return Lost_;
+}
+
+const TJobCounter& TJobCounters::GetInvalidated() const
+{
+ return Invalidated_;
+}
+
+const TJobCounter& TJobCounters::GetFailed() const
+{
+ return Failed_;
+}
+
+const TJobCounter& TJobCounters::GetRunning() const
+{
+ return Running_;
+}
+
+const TJobCounter& TJobCounters::GetSuspended() const
+{
+ return Suspended_;
+}
+
+const TJobCounter& TJobCounters::GetPending() const
+{
+ return Pending_;
+}
+
+const TJobCounter& TJobCounters::GetBlocked() const
+{
+ return Blocked_;
+}
+
+ui64 TJobCounters::GetTotal() const
+{
+ return Total_;
+}
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/job_counters.h b/yt/cpp/mapreduce/interface/job_counters.h
new file mode 100644
index 0000000000..9257cc1ec1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_counters.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/interface/node.h>
+
+namespace NYT {
+
+class TJobCounter
+{
+private:
+ TNode Data_;
+ ui64 Total_ = 0;
+
+public:
+ TJobCounter() = default;
+
+ TJobCounter(TNode data);
+ TJobCounter(ui64 total);
+
+ ui64 GetTotal() const;
+
+ ui64 GetValue(const TStringBuf key) const;
+};
+
+/// Class representing a collection of job counters.
+class TJobCounters
+{
+public:
+ ///
+ /// Construct empty counter.
+ TJobCounters() = default;
+
+ ///
+ /// Construct counter from counters node.
+ TJobCounters(const NYT::TNode& counters);
+
+ const TJobCounter& GetAborted() const;
+ const TJobCounter& GetAbortedScheduled() const;
+ const TJobCounter& GetAbortedNonScheduled() const;
+ const TJobCounter& GetCompleted() const;
+ const TJobCounter& GetCompletedNonInterrupted() const;
+ const TJobCounter& GetCompletedInterrupted() const;
+ const TJobCounter& GetLost() const;
+ const TJobCounter& GetInvalidated() const;
+ const TJobCounter& GetFailed() const;
+ const TJobCounter& GetRunning() const;
+ const TJobCounter& GetSuspended() const;
+ const TJobCounter& GetPending() const;
+ const TJobCounter& GetBlocked() const;
+
+ ui64 GetTotal() const;
+
+private:
+ ui64 Total_ = 0;
+
+ TJobCounter Aborted_;
+ TJobCounter AbortedScheduled_;
+ TJobCounter AbortedNonScheduled_;
+ TJobCounter Completed_;
+ TJobCounter CompletedNonInterrupted_;
+ TJobCounter CompletedInterrupted_;
+ TJobCounter Lost_;
+ TJobCounter Invalidated_;
+ TJobCounter Failed_;
+ TJobCounter Running_;
+ TJobCounter Suspended_;
+ TJobCounter Pending_;
+ TJobCounter Blocked_;
+};
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/job_counters_ut.cpp b/yt/cpp/mapreduce/interface/job_counters_ut.cpp
new file mode 100644
index 0000000000..56d3932b8f
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_counters_ut.cpp
@@ -0,0 +1,103 @@
+#include <yt/cpp/mapreduce/interface/job_counters.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYT;
+
+Y_UNIT_TEST_SUITE(JobCounters)
+{
+ Y_UNIT_TEST(Full)
+ {
+ const TString input = R"""(
+ {
+ "completed" = {
+ "total" = 6;
+ "non-interrupted" = 1;
+ "interrupted" = {
+ "whatever_interrupted" = 2;
+ "whatever_else_interrupted" = 3;
+ };
+ };
+ "aborted" = {
+ "non_scheduled" = {
+ "whatever_non_scheduled" = 4;
+ "whatever_else_non_scheduled" = 5;
+ };
+ "scheduled" = {
+ "whatever_scheduled" = 6;
+ "whatever_else_scheduled" = 7;
+ };
+ "total" = 22;
+ };
+ "lost" = 8;
+ "invalidated" = 9;
+ "failed" = 10;
+ "running" = 11;
+ "suspended" = 12;
+ "pending" = 13;
+ "blocked" = 14;
+ "total" = 105;
+ })""";
+
+ TJobCounters counters(NodeFromYsonString(input));
+
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetTotal(), 105);
+
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompleted().GetTotal(), 6);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedNonInterrupted().GetTotal(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedInterrupted().GetTotal(), 5);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAborted().GetTotal(), 22);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedNonScheduled().GetTotal(), 9);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedScheduled().GetTotal(), 13);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetLost().GetTotal(), 8);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetInvalidated().GetTotal(), 9);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetFailed().GetTotal(), 10);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetRunning().GetTotal(), 11);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetSuspended().GetTotal(), 12);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetPending().GetTotal(), 13);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetBlocked().GetTotal(), 14);
+
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedInterrupted().GetValue("whatever_interrupted"), 2);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedInterrupted().GetValue("whatever_else_interrupted"), 3);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedNonScheduled().GetValue("whatever_non_scheduled"), 4);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedNonScheduled().GetValue("whatever_else_non_scheduled"), 5);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedScheduled().GetValue("whatever_scheduled"), 6);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedScheduled().GetValue("whatever_else_scheduled"), 7);
+
+ UNIT_ASSERT_EXCEPTION(counters.GetCompletedInterrupted().GetValue("Nothingness"), yexception);
+ }
+
+ Y_UNIT_TEST(Empty)
+ {
+ const TString input = "{}";
+
+ TJobCounters counters(NodeFromYsonString(input));
+
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetTotal(), 0);
+
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompleted().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedNonInterrupted().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetCompletedInterrupted().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAborted().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedNonScheduled().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetAbortedScheduled().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetLost().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetInvalidated().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetFailed().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetRunning().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetSuspended().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetPending().GetTotal(), 0);
+ UNIT_ASSERT_VALUES_EQUAL(counters.GetBlocked().GetTotal(), 0);
+ }
+
+ Y_UNIT_TEST(Broken)
+ {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TJobCounters(TNode()), yexception, "TJobCounters");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TJobCounters(TNode(1)), yexception, "TJobCounters");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TJobCounters(TNode(1.0)), yexception, "TJobCounters");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(TJobCounters(TNode("Whatever")), yexception, "TJobCounters");
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/job_statistics.cpp b/yt/cpp/mapreduce/interface/job_statistics.cpp
new file mode 100644
index 0000000000..bd9791672d
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_statistics.cpp
@@ -0,0 +1,361 @@
+#include "job_statistics.h"
+
+#include "operation.h"
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/serialize.h>
+
+#include <library/cpp/yson/writer.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/ptr.h>
+#include <util/stream/file.h>
+#include <util/string/cast.h>
+#include <util/string/subst.h>
+#include <util/system/file.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////
+
+template <>
+i64 ConvertJobStatisticsEntry(i64 value)
+{
+ return value;
+}
+
+template <>
+TDuration ConvertJobStatisticsEntry(i64 value)
+{
+ return TDuration::MilliSeconds(value);
+}
+
+////////////////////////////////////////////////////////////////////
+
+static TTaskName JobTypeToTaskName(EJobType jobType)
+{
+ switch (jobType) {
+ case EJobType::PartitionMap:
+ return ETaskName::PartitionMap0;
+ case EJobType::Partition:
+ return ETaskName::Partition0;
+ default:
+ return ToString(jobType);
+ }
+}
+
+static TTaskName FixTaskName(TString taskName)
+{
+ if (taskName == "partition") {
+ return ETaskName::Partition0;
+ } else if (taskName == "partition_map") {
+ return ETaskName::PartitionMap0;
+ }
+ return taskName;
+}
+
+////////////////////////////////////////////////////////////////////
+
+class TJobStatistics::TData
+ : public TThrRefBase
+{
+public:
+ using TTaskName2Data = THashMap<TString, TJobStatistics::TDataEntry>;
+ using TState2TaskName2Data = THashMap<EJobState, TTaskName2Data>;
+ using TName2State2TaskName2Data = THashMap<TString, TState2TaskName2Data>;
+
+public:
+ TName2State2TaskName2Data Name2State2TaskName2Data;
+
+public:
+ TData() = default;
+
+ TData(const TNode& statisticsNode)
+ {
+ ParseNode(statisticsNode, TString(), &Name2State2TaskName2Data);
+ }
+
+ static void Aggregate(TJobStatistics::TDataEntry* result, const TJobStatistics::TDataEntry& other)
+ {
+ result->Max = Max(result->Max, other.Max);
+ result->Min = Min(result->Min, other.Min);
+ result->Sum += other.Sum;
+ result->Count += other.Count;
+ }
+
+ static void ParseNode(const TNode& node, TState2TaskName2Data* output)
+ {
+ auto getInt = [] (const TNode& theNode, TStringBuf key) {
+ const auto& nodeAsMap = theNode.AsMap();
+ auto it = nodeAsMap.find(key);
+ if (it == nodeAsMap.end()) {
+ ythrow yexception() << "Key '" << key << "' is not found";
+ }
+ const auto& valueNode = it->second;
+ if (!valueNode.IsInt64()) {
+ ythrow yexception() << "Key '" << key << "' is not of int64 type";
+ }
+ return valueNode.AsInt64();
+ };
+
+ for (const auto& [stateStr, taskName2DataNode] : node.AsMap()) {
+ EJobState state;
+ if (!TryFromString(stateStr, state)) {
+ continue;
+ }
+ for (const auto& [taskName, dataNode] : taskName2DataNode.AsMap()) {
+ auto fixedTaskName = FixTaskName(taskName);
+ auto& data = (*output)[state][fixedTaskName.Get()];
+ data.Max = getInt(dataNode, "max");
+ data.Min = getInt(dataNode, "min");
+ data.Sum = getInt(dataNode, "sum");
+ data.Count = getInt(dataNode, "count");
+ }
+ }
+ }
+
+ static void ParseNode(const TNode& node, const TString& curPath, TName2State2TaskName2Data* output)
+ {
+ Y_VERIFY(node.IsMap());
+
+ for (const auto& [key, value] : node.AsMap()) {
+ if (key == "$"sv) {
+ ParseNode(value, &(*output)[curPath]);
+ } else {
+ TString childPath = curPath;
+ if (!childPath.empty()) {
+ childPath.push_back('/');
+ }
+ if (key.find_first_of('/') != key.npos) {
+ TString keyCopy(key);
+ SubstGlobal(keyCopy, "/", "\\/");
+ childPath += keyCopy;
+ } else {
+ childPath += key;
+ }
+ ParseNode(value, childPath, output);
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+struct TJobStatistics::TFilter
+ : public TThrRefBase
+{
+ TVector<TTaskName> TaskNameFilter;
+ TVector<EJobState> JobStateFilter = {EJobState::Completed};
+};
+
+////////////////////////////////////////////////////////////////////
+
+const TString TJobStatistics::CustomStatisticsNamePrefix_ = "custom/";
+
+TJobStatistics::TJobStatistics()
+ : Data_(::MakeIntrusive<TData>())
+ , Filter_(::MakeIntrusive<TFilter>())
+{ }
+
+
+TJobStatistics::TJobStatistics(const NYT::TNode& statisticsNode)
+ : Data_(::MakeIntrusive<TData>(statisticsNode))
+ , Filter_(::MakeIntrusive<TFilter>())
+{ }
+
+TJobStatistics::TJobStatistics(::TIntrusivePtr<TData> data, ::TIntrusivePtr<TFilter> filter)
+ : Data_(data)
+ , Filter_(::MakeIntrusive<TFilter>(*filter))
+{ }
+
+TJobStatistics::TJobStatistics(const TJobStatistics& jobStatistics) = default;
+TJobStatistics::TJobStatistics(TJobStatistics&&) = default;
+
+TJobStatistics& TJobStatistics::operator=(const TJobStatistics& jobStatistics) = default;
+TJobStatistics& TJobStatistics::operator=(TJobStatistics&& jobStatistics) = default;
+
+TJobStatistics::~TJobStatistics() = default;
+
+TJobStatistics TJobStatistics::TaskName(TVector<TTaskName> taskNames) const
+{
+ auto newFilter = ::MakeIntrusive<TFilter>(*Filter_);
+ newFilter->TaskNameFilter = std::move(taskNames);
+ return TJobStatistics(Data_, std::move(newFilter));
+}
+
+TJobStatistics TJobStatistics::JobState(TVector<EJobState> jobStates) const
+{
+ auto newFilter = ::MakeIntrusive<TFilter>(*Filter_);
+ newFilter->JobStateFilter = std::move(jobStates);
+ return TJobStatistics(Data_, std::move(newFilter));
+}
+
+TJobStatistics TJobStatistics::JobType(TVector<EJobType> jobTypes) const
+{
+ TVector<TTaskName> taskNames;
+ for (auto jobType : jobTypes) {
+ taskNames.push_back(JobTypeToTaskName(jobType));
+ }
+ return TaskName(std::move(taskNames));
+}
+
+bool TJobStatistics::HasStatistics(TStringBuf name) const
+{
+ return Data_->Name2State2TaskName2Data.contains(name);
+}
+
+TJobStatisticsEntry<i64> TJobStatistics::GetStatistics(TStringBuf name) const
+{
+ return GetStatisticsAs<i64>(name);
+}
+
+TVector<TString> TJobStatistics::GetStatisticsNames() const
+{
+ TVector<TString> result;
+ result.reserve(Data_->Name2State2TaskName2Data.size());
+ for (const auto& entry : Data_->Name2State2TaskName2Data) {
+ result.push_back(entry.first);
+ }
+ return result;
+}
+
+bool TJobStatistics::HasCustomStatistics(TStringBuf name) const
+{
+ return HasStatistics(CustomStatisticsNamePrefix_ + name);
+}
+
+TJobStatisticsEntry<i64> TJobStatistics::GetCustomStatistics(TStringBuf name) const
+{
+ return GetCustomStatisticsAs<i64>(name);
+}
+
+TVector<TString> TJobStatistics::GetCustomStatisticsNames() const
+{
+ TVector<TString> result;
+ for (const auto& entry : Data_->Name2State2TaskName2Data) {
+ if (entry.first.StartsWith(CustomStatisticsNamePrefix_)) {
+ result.push_back(entry.first.substr(CustomStatisticsNamePrefix_.size()));
+ }
+ }
+ return result;
+}
+
+TMaybe<TJobStatistics::TDataEntry> TJobStatistics::GetStatisticsImpl(TStringBuf name) const
+{
+ auto name2State2TaskName2DataIt = Data_->Name2State2TaskName2Data.find(name);
+ Y_ENSURE(
+ name2State2TaskName2DataIt != Data_->Name2State2TaskName2Data.end(),
+ "Statistics '" << name << "' are missing");
+ const auto& state2TaskName2Data = name2State2TaskName2DataIt->second;
+
+ TMaybe<TDataEntry> result;
+ auto aggregate = [&] (const TDataEntry& data) {
+ if (result) {
+ TData::Aggregate(&result.GetRef(), data);
+ } else {
+ result = data;
+ }
+ };
+
+ auto aggregateTaskName2Data = [&] (const TData::TTaskName2Data& taskName2Data) {
+ if (Filter_->TaskNameFilter.empty()) {
+ for (const auto& [taskName, data] : taskName2Data) {
+ aggregate(data);
+ }
+ } else {
+ for (const auto& taskName : Filter_->TaskNameFilter) {
+ auto it = taskName2Data.find(taskName.Get());
+ if (it == taskName2Data.end()) {
+ continue;
+ }
+ const auto& data = it->second;
+ aggregate(data);
+ }
+ }
+ };
+
+ if (Filter_->JobStateFilter.empty()) {
+ for (const auto& [state, taskName2Data] : state2TaskName2Data) {
+ aggregateTaskName2Data(taskName2Data);
+ }
+ } else {
+ for (auto state : Filter_->JobStateFilter) {
+ auto it = state2TaskName2Data.find(state);
+ if (it == state2TaskName2Data.end()) {
+ continue;
+ }
+ const auto& taskName2Data = it->second;
+ aggregateTaskName2Data(taskName2Data);
+ }
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////
+
+namespace {
+
+constexpr int USER_STATISTICS_FILE_DESCRIPTOR = 5;
+constexpr char PATH_DELIMITER = '/';
+constexpr char ESCAPE = '\\';
+
+IOutputStream* GetStatisticsStream()
+{
+ static TFile file = Duplicate(USER_STATISTICS_FILE_DESCRIPTOR);
+ static TFileOutput stream(file);
+ return &stream;
+}
+
+template <typename T>
+void WriteCustomStatisticsAny(TStringBuf path, const T& value)
+{
+ ::NYson::TYsonWriter writer(GetStatisticsStream(), NYson::EYsonFormat::Binary, ::NYson::EYsonType::ListFragment);
+ int depth = 0;
+ size_t begin = 0;
+ size_t end = 0;
+ TVector<TString> items;
+ while (end <= path.size()) {
+ if (end + 1 < path.size() && path[end] == ESCAPE && path[end + 1] == PATH_DELIMITER) {
+ end += 2;
+ continue;
+ }
+ if (end == path.size() || path[end] == PATH_DELIMITER) {
+ writer.OnBeginMap();
+ items.emplace_back(path.data() + begin, end - begin);
+ SubstGlobal(items.back(), "\\/", "/");
+ writer.OnKeyedItem(TStringBuf(items.back()));
+ ++depth;
+ begin = end + 1;
+ }
+ ++end;
+ }
+ Serialize(value, &writer);
+ while (depth > 0) {
+ writer.OnEndMap();
+ --depth;
+ }
+}
+
+}
+
+////////////////////////////////////////////////////////////////////
+
+void WriteCustomStatistics(const TNode& statistics)
+{
+ ::NYson::TYsonWriter writer(GetStatisticsStream(), NYson::EYsonFormat::Binary, ::NYson::EYsonType::ListFragment);
+ Serialize(statistics, &writer);
+}
+
+void WriteCustomStatistics(TStringBuf path, i64 value)
+{
+ WriteCustomStatisticsAny(path, value);
+}
+
+void FlushCustomStatisticsStream() {
+ GetStatisticsStream()->Flush();
+}
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/job_statistics.h b/yt/cpp/mapreduce/interface/job_statistics.h
new file mode 100644
index 0000000000..8af751604f
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_statistics.h
@@ -0,0 +1,268 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/job_statistics.h
+///
+/// Header containing classes and utility functions to work with
+/// [job statistics](https://docs.yandex-team.ru/yt/problems/jobstatistics).
+
+#include "fwd.h"
+
+#include <library/cpp/yson/node/node.h>
+
+#include <util/system/defaults.h>
+#include <util/generic/maybe.h>
+#include <util/generic/ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Convert i64 representation of statistics to other type.
+///
+/// Library defines this template for types TDuration and i64.
+/// Users may define it for their types.
+///
+/// @see @ref NYT::TJobStatistics::GetStatisticsAs method.
+template <typename T>
+T ConvertJobStatisticsEntry(i64 value);
+
+////////////////////////////////////////////////////////////////////
+
+/// Class representing a collection of job statistics.
+class TJobStatistics
+{
+public:
+ ///
+ /// Construct empty statistics.
+ TJobStatistics();
+
+ ///
+ /// Construct statistics from statistics node.
+ TJobStatistics(const NYT::TNode& statistics);
+
+ TJobStatistics(const TJobStatistics& jobStatistics);
+ TJobStatistics(TJobStatistics&& jobStatistics);
+
+ TJobStatistics& operator=(const TJobStatistics& jobStatistics);
+ TJobStatistics& operator=(TJobStatistics&& jobStatistics);
+
+ ~TJobStatistics();
+
+ ///
+ /// @brief Filter statistics by task name.
+ ///
+ /// @param taskNames What task names to include (empty means all).
+ TJobStatistics TaskName(TVector<TTaskName> taskNames) const;
+
+ ///
+ /// @brief Filter statistics by job state.
+ ///
+ /// @param filter What job states to include (empty means all).
+ ///
+ /// @note Default statistics include only (successfully) completed jobs.
+ TJobStatistics JobState(TVector<EJobState> filter) const;
+
+ ///
+ /// @brief Filter statistics by job type.
+ ///
+ /// @param filter What job types to include (empty means all).
+ ///
+ /// @deprecated Use @ref TJobStatistics::TaskName instead.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/jobs#obshaya-shema
+ TJobStatistics JobType(TVector<EJobType> filter) const;
+
+ ///
+ /// @brief Check that given statistics exist.
+ ///
+ /// @param name Slash separated statistics name, e.g. "time/total" (like it appears in web interface).
+ bool HasStatistics(TStringBuf name) const;
+
+ ///
+ /// @brief Get statistics by name.
+ ///
+ /// @param name Slash separated statistics name, e.g. "time/total" (like it appears in web interface).
+ ///
+ /// @note If statistics is missing an exception is thrown. If because of filters
+ /// no fields remain the returned value is empty (all fields are `Nothing`).
+ ///
+ /// @note We don't use `TMaybe<TJobStatisticsEntry>` here;
+ /// instead, @ref NYT::TJobStatisticsEntry methods return `TMaybe<i64>`,
+ /// so user easier use `.GetOrElse`:
+ /// ```
+ /// jobStatistics.GetStatistics("some/statistics/name").Max().GetOrElse(0);
+ /// ```
+ TJobStatisticsEntry<i64> GetStatistics(TStringBuf name) const;
+
+ ///
+ /// @brief Get statistics by name.
+ ///
+ /// @param name Slash separated statistics name, e.g. "time/total" (like it appears in web interface).
+ ///
+ /// @note In order to use `GetStatisticsAs` method, @ref NYT::ConvertJobStatisticsEntry function must be defined
+ /// (the library defines it for `i64` and `TDuration`, user may define it for other types).
+ template <typename T>
+ TJobStatisticsEntry<T> GetStatisticsAs(TStringBuf name) const;
+
+ ///
+ /// Get (slash separated) names of statistics.
+ TVector<TString> GetStatisticsNames() const;
+
+ ///
+ /// @brief Check if given custom statistics exists.
+ ///
+ /// @param name Slash separated custom statistics name.
+ bool HasCustomStatistics(TStringBuf name) const;
+
+ ///
+ /// @brief Get custom statistics (those the user can write in job with @ref NYT::WriteCustomStatistics).
+ ///
+ /// @param name Slash separated custom statistics name.
+ TJobStatisticsEntry<i64> GetCustomStatistics(TStringBuf name) const;
+
+ ///
+ /// @brief Get custom statistics (those the user can write in job with @ref NYT::WriteCustomStatistics).
+ ///
+ /// @param name Slash separated custom statistics name.
+ template <typename T>
+ TJobStatisticsEntry<T> GetCustomStatisticsAs(TStringBuf name) const;
+
+ ///
+ /// Get names of all custom statistics.
+ TVector<TString> GetCustomStatisticsNames() const;
+
+private:
+ class TData;
+ struct TFilter;
+
+ struct TDataEntry {
+ i64 Max;
+ i64 Min;
+ i64 Sum;
+ i64 Count;
+ };
+
+ static const TString CustomStatisticsNamePrefix_;
+
+private:
+ TJobStatistics(::TIntrusivePtr<TData> data, ::TIntrusivePtr<TFilter> filter);
+
+ TMaybe<TDataEntry> GetStatisticsImpl(TStringBuf name) const;
+
+private:
+ ::TIntrusivePtr<TData> Data_;
+ ::TIntrusivePtr<TFilter> Filter_;
+
+private:
+ template<typename T>
+ friend class TJobStatisticsEntry;
+};
+
+////////////////////////////////////////////////////////////////////
+
+/// Class representing single statistic.
+template <typename T>
+class TJobStatisticsEntry
+{
+public:
+ TJobStatisticsEntry(TMaybe<TJobStatistics::TDataEntry> data)
+ : Data_(std::move(data))
+ { }
+
+ /// Sum of the statistic over all jobs.
+ TMaybe<T> Sum() const
+ {
+ if (Data_) {
+ return ConvertJobStatisticsEntry<T>(Data_->Sum);
+ }
+ return Nothing();
+ }
+
+ /// @brief Average of the statistic over all jobs.
+ ///
+ /// @note Only jobs that emitted statistics are taken into account.
+ TMaybe<T> Avg() const
+ {
+ if (Data_ && Data_->Count) {
+ return ConvertJobStatisticsEntry<T>(Data_->Sum / Data_->Count);
+ }
+ return Nothing();
+ }
+
+ /// @brief Number of jobs that emitted this statistic.
+ TMaybe<T> Count() const
+ {
+ if (Data_) {
+ return ConvertJobStatisticsEntry<T>(Data_->Count);
+ }
+ return Nothing();
+ }
+
+ /// @brief Maximum value of the statistic over all jobs.
+ TMaybe<T> Max() const
+ {
+ if (Data_) {
+ return ConvertJobStatisticsEntry<T>(Data_->Max);
+ }
+ return Nothing();
+ }
+
+ /// @brief Minimum value of the statistic over all jobs.
+ TMaybe<T> Min() const
+ {
+ if (Data_) {
+ return ConvertJobStatisticsEntry<T>(Data_->Min);
+ }
+ return Nothing();
+ }
+
+private:
+ TMaybe<TJobStatistics::TDataEntry> Data_;
+
+private:
+ friend class TJobStatistics;
+};
+
+////////////////////////////////////////////////////////////////////
+
+template <typename T>
+TJobStatisticsEntry<T> TJobStatistics::GetStatisticsAs(TStringBuf name) const
+{
+ return TJobStatisticsEntry<T>(GetStatisticsImpl(name));
+}
+
+template <typename T>
+TJobStatisticsEntry<T> TJobStatistics::GetCustomStatisticsAs(TStringBuf name) const
+{
+ return TJobStatisticsEntry<T>(GetStatisticsImpl(CustomStatisticsNamePrefix_ + name));
+}
+
+////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Write [custom statistics](https://yt.yandex-team.ru/docs/description/mr/jobs#user_stats).
+///
+/// @param path Slash-separated path (length must not exceed 512 bytes).
+/// @param value Value of the statistic.
+///
+/// @note The function must be called in job.
+/// Total number of statistics (with different paths) must not exceed 128.
+void WriteCustomStatistics(TStringBuf path, i64 value);
+
+///
+/// @brief Write several [custom statistics](https://yt.yandex-team.ru/docs/description/mr/jobs#user_stats) at once.
+///
+/// @param statistics A tree of map nodes with leaves of type `i64`.
+///
+/// @note The call is equivalent to calling @ref NYT::WriteCustomStatistics(TStringBuf, i64) for every path in the given map.
+void WriteCustomStatistics(const TNode& statistics);
+
+///
+/// @brief Flush [custom statistics stream](https://yt.yandex-team.ru/docs/description/mr/jobs#user_stats)
+///
+void FlushCustomStatisticsStream();
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/job_statistics_ut.cpp b/yt/cpp/mapreduce/interface/job_statistics_ut.cpp
new file mode 100644
index 0000000000..0cf53d771a
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/job_statistics_ut.cpp
@@ -0,0 +1,257 @@
+#include <yt/cpp/mapreduce/interface/job_statistics.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYT;
+
+Y_UNIT_TEST_SUITE(JobStatistics)
+{
+ Y_UNIT_TEST(Simple)
+ {
+ const TString input = R"""(
+ {
+ "data" = {
+ "output" = {
+ "0" = {
+ "uncompressed_data_size" = {
+ "$" = {
+ "completed" = {
+ "simple_sort" = {
+ "max" = 130;
+ "count" = 1;
+ "min" = 130;
+ "sum" = 130;
+ };
+ "map" = {
+ "max" = 42;
+ "count" = 1;
+ "min" = 42;
+ "sum" = 42;
+ };
+ };
+ "aborted" = {
+ "simple_sort" = {
+ "max" = 24;
+ "count" = 1;
+ "min" = 24;
+ "sum" = 24;
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ })""";
+
+ TJobStatistics stat(NodeFromYsonString(input));
+
+ UNIT_ASSERT(stat.HasStatistics("data/output/0/uncompressed_data_size"));
+ UNIT_ASSERT(!stat.HasStatistics("nonexistent-statistics"));
+ UNIT_ASSERT_EXCEPTION_CONTAINS(stat.GetStatistics("BLAH-BLAH"), yexception, "Statistics");
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatisticsNames(), TVector<TString>{"data/output/0/uncompressed_data_size"});
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Max(), 130);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Count(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Min(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Sum(), 172);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Avg(), 172 / 2);
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.JobState({EJobState::Aborted}).GetStatistics("data/output/0/uncompressed_data_size").Sum(), 24);
+ UNIT_ASSERT_VALUES_EQUAL(stat.JobType({EJobType::Map}).JobState({EJobState::Aborted}).GetStatistics("data/output/0/uncompressed_data_size").Sum(), TMaybe<i64>());
+ }
+
+ Y_UNIT_TEST(TestOtherTypes)
+ {
+ const TString input = R"""(
+ {
+ "time" = {
+ "exec" = {
+ "$" = {
+ "completed" = {
+ "map" = {
+ "max" = 2482468;
+ "count" = 38;
+ "min" = 578976;
+ "sum" = 47987270;
+ };
+ };
+ };
+ };
+ };
+ })""";
+
+ TJobStatistics stat(NodeFromYsonString(input));
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatisticsAs<TDuration>("time/exec").Max(), TDuration::MilliSeconds(2482468));
+ }
+
+ Y_UNIT_TEST(Custom)
+ {
+ const TString input = R"""(
+ {
+ "custom" = {
+ "some" = {
+ "path" = {
+ "$" = {
+ "completed" = {
+ "map" = {
+ "max" = -1;
+ "count" = 1;
+ "min" = -1;
+ "sum" = -1;
+ };
+ };
+ };
+ };
+ };
+ "another" = {
+ "path" = {
+ "$" = {
+ "completed" = {
+ "map" = {
+ "max" = 1001;
+ "count" = 2;
+ "min" = 1001;
+ "sum" = 2002;
+ };
+ };
+ };
+ };
+ };
+ };
+ })""";
+
+ TJobStatistics stat(NodeFromYsonString(input));
+
+ UNIT_ASSERT(stat.HasCustomStatistics("some/path"));
+ UNIT_ASSERT(!stat.HasCustomStatistics("nonexistent-statistics"));
+ UNIT_ASSERT_EXCEPTION_CONTAINS(stat.GetCustomStatistics("BLAH-BLAH"), yexception, "Statistics");
+
+ const auto names = stat.GetCustomStatisticsNames();
+ const THashSet<TString> expected = {"some/path", "another/path"};
+ UNIT_ASSERT_VALUES_EQUAL(THashSet<TString>(names.begin(), names.end()), expected);
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetCustomStatistics("some/path").Max(), -1);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetCustomStatistics("another/path").Avg(), 1001);
+ }
+
+ Y_UNIT_TEST(TaskNames)
+ {
+ const TString input = R"""(
+ {
+ "data" = {
+ "output" = {
+ "0" = {
+ "uncompressed_data_size" = {
+ "$" = {
+ "completed" = {
+ "partition_map" = {
+ "max" = 130;
+ "count" = 1;
+ "min" = 130;
+ "sum" = 130;
+ };
+ "partition(0)" = {
+ "max" = 42;
+ "count" = 1;
+ "min" = 42;
+ "sum" = 42;
+ };
+ };
+ "aborted" = {
+ "simple_sort" = {
+ "max" = 24;
+ "count" = 1;
+ "min" = 24;
+ "sum" = 24;
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ })""";
+
+ TJobStatistics stat(NodeFromYsonString(input));
+
+ UNIT_ASSERT(stat.HasStatistics("data/output/0/uncompressed_data_size"));
+ UNIT_ASSERT(!stat.HasStatistics("nonexistent-statistics"));
+ UNIT_ASSERT_EXCEPTION_CONTAINS(stat.GetStatistics("BLAH-BLAH"), yexception, "Statistics");
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatisticsNames(), TVector<TString>{"data/output/0/uncompressed_data_size"});
+
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Max(), 130);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Count(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Min(), 42);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Sum(), 172);
+ UNIT_ASSERT_VALUES_EQUAL(stat.GetStatistics("data/output/0/uncompressed_data_size").Avg(), 172 / 2);
+
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .JobState({EJobState::Aborted})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 24);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .JobType({EJobType::Partition})
+ .JobState({EJobState::Aborted})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ TMaybe<i64>());
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({"partition(0)"})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 42);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({"partition"})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ TMaybe<i64>());
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({"partition_map(0)"})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 130);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .JobType({EJobType::Partition})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 42);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .JobType({EJobType::PartitionMap})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 130);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({ETaskName::Partition0})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 42);
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({ETaskName::Partition1})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ TMaybe<i64>());
+ UNIT_ASSERT_VALUES_EQUAL(
+ stat
+ .TaskName({ETaskName::PartitionMap0})
+ .GetStatistics("data/output/0/uncompressed_data_size")
+ .Sum(),
+ 130);
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/logging/logger.cpp b/yt/cpp/mapreduce/interface/logging/logger.cpp
new file mode 100644
index 0000000000..bfa56b94f6
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/logger.cpp
@@ -0,0 +1,188 @@
+#include "logger.h"
+
+#include <util/datetime/base.h>
+
+#include <util/stream/file.h>
+#include <util/stream/format.h>
+#include <util/stream/printf.h>
+#include <util/stream/str.h>
+
+#include <util/system/mutex.h>
+#include <util/system/rwlock.h>
+#include <util/system/thread.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TStringBuf StripFileName(TStringBuf path) {
+ TStringBuf l, r;
+ if (path.TryRSplit('/', l, r) || path.TryRSplit('\\', l, r)) {
+ return r;
+ } else {
+ return path;
+ }
+}
+
+static char GetLogLevelCode(ILogger::ELevel level) {
+ switch (level) {
+ case ILogger::FATAL: return 'F';
+ case ILogger::ERROR: return 'E';
+ case ILogger::INFO: return 'I';
+ case ILogger::DEBUG: return 'D';
+ }
+ Y_UNREACHABLE();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullLogger
+ : public ILogger
+{
+public:
+ void Log(ELevel level, const TSourceLocation& sourceLocation, const char* format, va_list args) override
+ {
+ Y_UNUSED(level);
+ Y_UNUSED(sourceLocation);
+ Y_UNUSED(format);
+ Y_UNUSED(args);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLoggerBase
+ : public ILogger
+{
+public:
+ TLoggerBase(ELevel cutLevel)
+ : CutLevel_(cutLevel)
+ { }
+
+ virtual void OutputLine(const TString& line) = 0;
+
+ void Log(ELevel level, const TSourceLocation& sourceLocation, const char* format, va_list args) override
+ {
+ if (level > CutLevel_) {
+ return;
+ }
+
+ TStringStream stream;
+ stream << TInstant::Now().ToStringLocal()
+ << " " << GetLogLevelCode(level)
+ << " [" << Hex(TThread::CurrentThreadId(), HF_FULL) << "] ";
+ Printf(stream, format, args);
+ stream << " - " << StripFileName(sourceLocation.File) << ':' << sourceLocation.Line << Endl;
+
+ TGuard<TMutex> guard(Mutex_);
+ OutputLine(stream.Str());
+ }
+
+private:
+ ELevel CutLevel_;
+ TMutex Mutex_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStdErrLogger
+ : public TLoggerBase
+{
+public:
+ TStdErrLogger(ELevel cutLevel)
+ : TLoggerBase(cutLevel)
+ { }
+
+ void OutputLine(const TString& line) override
+ {
+ Cerr << line;
+ }
+};
+
+ILoggerPtr CreateStdErrLogger(ILogger::ELevel cutLevel)
+{
+ return new TStdErrLogger(cutLevel);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileLogger
+ : public TLoggerBase
+{
+public:
+ TFileLogger(ELevel cutLevel, const TString& path, bool append)
+ : TLoggerBase(cutLevel)
+ , Stream_(TFile(path, OpenAlways | WrOnly | Seq | (append ? ForAppend : EOpenMode())))
+ { }
+
+ void OutputLine(const TString& line) override
+ {
+ Stream_ << line;
+ }
+
+private:
+ TUnbufferedFileOutput Stream_;
+};
+
+ILoggerPtr CreateFileLogger(ILogger::ELevel cutLevel, const TString& path, bool append)
+{
+ return new TFileLogger(cutLevel, path, append);
+}
+////////////////////////////////////////////////////////////////////////////////
+
+class TBufferedFileLogger
+ : public TLoggerBase
+{
+public:
+ TBufferedFileLogger(ELevel cutLevel, const TString& path, bool append)
+ : TLoggerBase(cutLevel)
+ , Stream_(TFile(path, OpenAlways | WrOnly | Seq | (append ? ForAppend : EOpenMode())))
+ { }
+
+ void OutputLine(const TString& line) override
+ {
+ Stream_ << line;
+ }
+
+private:
+ TFileOutput Stream_;
+};
+
+ILoggerPtr CreateBufferedFileLogger(ILogger::ELevel cutLevel, const TString& path, bool append)
+{
+ return new TBufferedFileLogger(cutLevel, path, append);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TRWMutex LoggerMutex;
+static ILoggerPtr Logger;
+
+struct TLoggerInitializer
+{
+ TLoggerInitializer()
+ {
+ Logger = new TNullLogger;
+ }
+} LoggerInitializer;
+
+void SetLogger(ILoggerPtr logger)
+{
+ auto guard = TWriteGuard(LoggerMutex);
+ if (logger) {
+ Logger = logger;
+ } else {
+ Logger = new TNullLogger;
+ }
+}
+
+ILoggerPtr GetLogger()
+{
+ auto guard = TReadGuard(LoggerMutex);
+ return Logger;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
+
diff --git a/yt/cpp/mapreduce/interface/logging/logger.h b/yt/cpp/mapreduce/interface/logging/logger.h
new file mode 100644
index 0000000000..2b5aae87d1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/logger.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <util/generic/ptr.h>
+#include <util/generic/string.h>
+#include <util/system/compat.h>
+#include <util/system/src_location.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class ILogger
+ : public TThrRefBase
+{
+public:
+ enum ELevel
+ {
+ FATAL /* "fatal", "FATAL" */,
+ // We don't have such level as `warning', but we support it for compatibility with other APIs.
+ ERROR /* "error", "warning", "ERROR", "WARNING" */,
+ INFO /* "info", "INFO" */,
+ DEBUG /* "debug", "DEBUG" */
+ };
+
+ virtual void Log(ELevel level, const ::TSourceLocation& sourceLocation, const char* format, va_list args) = 0;
+};
+
+using ILoggerPtr = ::TIntrusivePtr<ILogger>;
+
+void SetLogger(ILoggerPtr logger);
+ILoggerPtr GetLogger();
+
+ILoggerPtr CreateStdErrLogger(ILogger::ELevel cutLevel);
+ILoggerPtr CreateFileLogger(ILogger::ELevel cutLevel, const TString& path, bool append = false);
+
+/**
+ * Create logger that writes to a file in a buffered manner.
+ * It should result in fewer system calls (useful if you expect a lot of log messages),
+ * but in case of a crash, you would lose some log messages that haven't been flushed yet.
+ */
+ILoggerPtr CreateBufferedFileLogger(ILogger::ELevel cutLevel, const TString& path, bool append = false);
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/logging/ya.make b/yt/cpp/mapreduce/interface/logging/ya.make
new file mode 100644
index 0000000000..8095bfe4ba
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ logger.cpp
+ yt_log.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/logging
+)
+
+GENERATE_ENUM_SERIALIZATION(logger.h)
+
+END()
diff --git a/yt/cpp/mapreduce/interface/logging/yt_log.cpp b/yt/cpp/mapreduce/interface/logging/yt_log.cpp
new file mode 100644
index 0000000000..9fa7b91580
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/yt_log.cpp
@@ -0,0 +1,126 @@
+#include "yt_log.h"
+
+#include "logger.h"
+
+#include <util/generic/guid.h>
+
+#include <util/system/mutex.h>
+
+namespace NYT {
+
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TLogManager
+ : public ILogManager
+{
+public:
+ static constexpr TStringBuf CategoryName = "Wrapper";
+
+public:
+ void RegisterStaticAnchor(
+ TLoggingAnchor* anchor,
+ ::TSourceLocation sourceLocation,
+ TStringBuf anchorMessage) override
+ {
+ if (anchor->Registered.exchange(true)) {
+ return;
+ }
+
+ anchor->Enabled.store(true);
+
+ auto guard = Guard(Mutex_);
+ anchor->SourceLocation = sourceLocation;
+ anchor->AnchorMessage = anchorMessage;
+ }
+
+ void UpdateAnchor(TLoggingAnchor* /*position*/) override
+ { }
+
+ void Enqueue(TLogEvent&& event) override
+ {
+ auto message = TString(event.MessageRef.ToStringBuf());
+ LogMessage(
+ ToImplLevel(event.Level),
+ ::TSourceLocation(event.SourceFile, event.SourceLine),
+ "%.*s",
+ event.MessageRef.size(),
+ event.MessageRef.begin());
+ }
+
+ const TLoggingCategory* GetCategory(TStringBuf categoryName) override
+ {
+ Y_VERIFY(categoryName == CategoryName);
+ return &Category_;
+ }
+
+ void UpdateCategory(TLoggingCategory* /*category*/) override
+ {
+ Y_FAIL();
+ }
+
+ bool GetAbortOnAlert() const override
+ {
+ return false;
+ }
+
+private:
+ static ILogger::ELevel ToImplLevel(ELogLevel level)
+ {
+ switch (level) {
+ case ELogLevel::Minimum:
+ case ELogLevel::Trace:
+ case ELogLevel::Debug:
+ return ILogger::ELevel::DEBUG;
+ case ELogLevel::Info:
+ return ILogger::ELevel::INFO;
+ case ELogLevel::Warning:
+ case ELogLevel::Error:
+ return ILogger::ELevel::ERROR;
+ case ELogLevel::Alert:
+ case ELogLevel::Fatal:
+ case ELogLevel::Maximum:
+ return ILogger::ELevel::FATAL;
+ }
+ }
+
+ static void LogMessage(ILogger::ELevel level, const ::TSourceLocation& sourceLocation, const char* format, ...)
+ {
+ va_list args;
+ va_start(args, format);
+ GetLogger()->Log(level, sourceLocation, format, args);
+ va_end(args);
+ }
+
+private:
+ ::TMutex Mutex_;
+ std::atomic<int> ActualVersion_{1};
+ const TLoggingCategory Category_{
+ .Name{CategoryName},
+ .MinPlainTextLevel{ELogLevel::Minimum},
+ .CurrentVersion{1},
+ .ActualVersion = &ActualVersion_,
+ };
+};
+
+TLogManager LogManager;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLogger Logger(&LogManager, TLogManager::CategoryName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TGUID& value, TStringBuf /*format*/)
+{
+ builder->AppendString(GetGuidAsString(value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/logging/yt_log.h b/yt/cpp/mapreduce/interface/logging/yt_log.h
new file mode 100644
index 0000000000..4cf93a6ba1
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/logging/yt_log.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <library/cpp/yt/logging/logger.h>
+
+struct TGUID;
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern NLogging::TLogger Logger;
+
+void FormatValue(TStringBuilderBase* builder, const TGUID& value, TStringBuf format);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/mpl.h b/yt/cpp/mapreduce/interface/mpl.h
new file mode 100644
index 0000000000..9865e28b6c
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/mpl.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "fwd.h"
+
+#include <tuple>
+#include <type_traits>
+
+namespace NYT {
+
+/// @cond Doxygen_Suppress
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TBase, class TDerived>
+struct TIsBaseOf
+{
+ static constexpr bool Value = std::is_base_of_v<TBase, TDerived> && !std::is_same_v<TBase, TDerived>;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T, class Tuple>
+struct TIndexInTuple;
+
+template <class T, class... Types>
+struct TIndexInTuple<T, std::tuple<T, Types...>>
+{
+ static constexpr int Value = 0;
+};
+
+template <class T>
+struct TIndexInTuple<T, std::tuple<>>
+{
+ static constexpr int Value = 0;
+};
+
+template <class T, class U, class... Types>
+struct TIndexInTuple<T, std::tuple<U, Types...>>
+{
+ static constexpr int Value = 1 + TIndexInTuple<T, std::tuple<Types...>>::Value;
+};
+
+template <class T, class TTuple>
+constexpr bool DoesTupleContainType = (TIndexInTuple<T, TTuple>::Value < std::tuple_size<TTuple>{});
+
+template <class TOut, class TIn = std::tuple<>>
+struct TUniqueTypes;
+
+template <class... TOut, class TInCar, class... TInCdr>
+struct TUniqueTypes<std::tuple<TOut...>, std::tuple<TInCar, TInCdr...>>
+{
+ using TType = std::conditional_t<
+ DoesTupleContainType<TInCar, std::tuple<TOut...>>,
+ typename TUniqueTypes<std::tuple<TOut...>, std::tuple<TInCdr...>>::TType,
+ typename TUniqueTypes<std::tuple<TOut..., TInCar>, std::tuple<TInCdr...>>::TType
+ >;
+};
+
+template <class TOut>
+struct TUniqueTypes<TOut, std::tuple<>>
+{
+ using TType = TOut;
+};
+
+} // namespace NDetail
+
+/// @endcond Doxygen_Suppress
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/yt/cpp/mapreduce/interface/node.h b/yt/cpp/mapreduce/interface/node.h
new file mode 100644
index 0000000000..fece1b36de
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/node.h
@@ -0,0 +1,7 @@
+#pragma once
+
+// Backward compatibility
+#include "fwd.h"
+#include <library/cpp/yson/node/node.h>
+
+
diff --git a/yt/cpp/mapreduce/interface/operation-inl.h b/yt/cpp/mapreduce/interface/operation-inl.h
new file mode 100644
index 0000000000..8d53cd446f
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/operation-inl.h
@@ -0,0 +1,928 @@
+#pragma once
+
+#ifndef OPERATION_INL_H_
+#error "Direct inclusion of this file is not allowed, use operation.h"
+#include "operation.h"
+#endif
+#undef OPERATION_INL_H_
+
+#include "errors.h"
+
+#include <util/generic/bt_exception.h>
+#include <util/generic/singleton.h>
+#include <util/system/type_name.h>
+
+#include <util/stream/file.h>
+#include <util/stream/buffer.h>
+#include <util/string/subst.h>
+
+#include <typeindex>
+
+namespace NYT {
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<class T>
+void Assign(TVector<T>& array, size_t idx, const T& value) {
+ array.resize(std::max(array.size(), idx + 1));
+ array[idx] = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TRow>
+TStructuredRowStreamDescription GetStructuredRowStreamDescription()
+{
+ if constexpr (std::is_same_v<TRow, NYT::TNode>) {
+ return TTNodeStructuredRowStream{};
+ } else if constexpr (std::is_same_v<TRow, NYT::TYaMRRow>) {
+ return TTYaMRRowStructuredRowStream{};
+ } else if constexpr (std::is_same_v<::google::protobuf::Message, TRow>) {
+ return TProtobufStructuredRowStream{nullptr};
+ } else if constexpr (TIsBaseOf<::google::protobuf::Message, TRow>::Value) {
+ return TProtobufStructuredRowStream{TRow::descriptor()};
+ } else if constexpr (TIsProtoOneOf<TRow>::value) {
+ return TProtobufStructuredRowStream{nullptr};
+ } else {
+ static_assert(TDependentFalse<TRow>, "Unknown row type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TRow>
+TStructuredTablePath Structured(TRichYPath richYPath)
+{
+ return TStructuredTablePath(std::move(richYPath), StructuredTableDescription<TRow>());
+}
+
+template <typename TRow>
+TTableStructure StructuredTableDescription()
+{
+ if constexpr (std::is_same_v<TRow, NYT::TNode>) {
+ return TUnspecifiedTableStructure{};
+ } else if constexpr (std::is_same_v<TRow, NYT::TYaMRRow>) {
+ return TUnspecifiedTableStructure{};
+ } else if constexpr (std::is_base_of_v<::google::protobuf::Message, TRow>) {
+ if constexpr (std::is_same_v<::google::protobuf::Message, TRow>) {
+ static_assert(TDependentFalse<TRow>, "Cannot use ::google::protobuf::Message as table descriptor");
+ } else {
+ return TProtobufTableStructure{TRow::descriptor()};
+ }
+ } else {
+ static_assert(TDependentFalse<TRow>, "Unknown row type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TDerived>
+TDerived& TRawOperationIoTableSpec<TDerived>::AddInput(const TRichYPath& path)
+{
+ Inputs_.push_back(path);
+ return static_cast<TDerived&>(*this);
+}
+
+template <typename TDerived>
+TDerived& TRawOperationIoTableSpec<TDerived>::SetInput(size_t tableIndex, const TRichYPath& path)
+{
+ NDetail::Assign(Inputs_, tableIndex, path);
+}
+
+template <typename TDerived>
+TDerived& TRawOperationIoTableSpec<TDerived>::AddOutput(const TRichYPath& path)
+{
+ Outputs_.push_back(path);
+ return static_cast<TDerived&>(*this);
+}
+
+template <typename TDerived>
+TDerived& TRawOperationIoTableSpec<TDerived>::SetOutput(size_t tableIndex, const TRichYPath& path)
+{
+ NDetail::Assign(Outputs_, tableIndex, path);
+}
+
+template <typename TDerived>
+const TVector<TRichYPath>& TRawOperationIoTableSpec<TDerived>::GetInputs() const
+{
+ return Inputs_;
+}
+
+template <typename TDerived>
+const TVector<TRichYPath>& TRawOperationIoTableSpec<TDerived>::GetOutputs() const
+{
+ return Outputs_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TDerived>
+TDerived& TRawMapReduceOperationIoSpec<TDerived>::AddMapOutput(const TRichYPath& path)
+{
+ MapOutputs_.push_back(path);
+ return static_cast<TDerived&>(*this);
+}
+
+template <typename TDerived>
+TDerived& TRawMapReduceOperationIoSpec<TDerived>::SetMapOutput(size_t tableIndex, const TRichYPath& path)
+{
+ NDetail::Assign(MapOutputs_, tableIndex, path);
+}
+
+template <typename TDerived>
+const TVector<TRichYPath>& TRawMapReduceOperationIoSpec<TDerived>::GetMapOutputs() const
+{
+ return MapOutputs_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+::TIntrusivePtr<INodeReaderImpl> CreateJobNodeReader(TRawTableReaderPtr rawTableReader);
+::TIntrusivePtr<IYaMRReaderImpl> CreateJobYaMRReader(TRawTableReaderPtr rawTableReader);
+::TIntrusivePtr<IProtoReaderImpl> CreateJobProtoReader(TRawTableReaderPtr rawTableReader);
+
+::TIntrusivePtr<INodeWriterImpl> CreateJobNodeWriter(THolder<IProxyOutput> rawTableWriter);
+::TIntrusivePtr<IYaMRWriterImpl> CreateJobYaMRWriter(THolder<IProxyOutput> rawTableWriter);
+::TIntrusivePtr<IProtoWriterImpl> CreateJobProtoWriter(THolder<IProxyOutput> rawTableWriter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+inline ::TIntrusivePtr<typename TRowTraits<T>::IReaderImpl> CreateJobReaderImpl(TRawTableReaderPtr rawTableReader);
+
+template <>
+inline ::TIntrusivePtr<INodeReaderImpl> CreateJobReaderImpl<TNode>(TRawTableReaderPtr rawTableReader)
+{
+ return CreateJobNodeReader(rawTableReader);
+}
+
+template <>
+inline ::TIntrusivePtr<IYaMRReaderImpl> CreateJobReaderImpl<TYaMRRow>(TRawTableReaderPtr rawTableReader)
+{
+ return CreateJobYaMRReader(rawTableReader);
+}
+
+template <>
+inline ::TIntrusivePtr<IProtoReaderImpl> CreateJobReaderImpl<Message>(TRawTableReaderPtr rawTableReader)
+{
+ return CreateJobProtoReader(rawTableReader);
+}
+
+template <class T>
+inline ::TIntrusivePtr<typename TRowTraits<T>::IReaderImpl> CreateJobReaderImpl(TRawTableReaderPtr rawTableReader)
+{
+ if constexpr (TIsBaseOf<Message, T>::Value || NDetail::TIsProtoOneOf<T>::value) {
+ return CreateJobProtoReader(rawTableReader);
+ } else {
+ static_assert(TDependentFalse<T>, "Unknown row type");
+ }
+}
+
+template <class T>
+inline TTableReaderPtr<T> CreateJobReader(TRawTableReaderPtr rawTableReader)
+{
+ return new TTableReader<T>(CreateJobReaderImpl<T>(rawTableReader));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TTableWriterPtr<T> CreateJobWriter(THolder<IProxyOutput> rawJobWriter);
+
+template <>
+inline TTableWriterPtr<TNode> CreateJobWriter<TNode>(THolder<IProxyOutput> rawJobWriter)
+{
+ return new TTableWriter<TNode>(CreateJobNodeWriter(std::move(rawJobWriter)));
+}
+
+template <>
+inline TTableWriterPtr<TYaMRRow> CreateJobWriter<TYaMRRow>(THolder<IProxyOutput> rawJobWriter)
+{
+ return new TTableWriter<TYaMRRow>(CreateJobYaMRWriter(std::move(rawJobWriter)));
+}
+
+template <>
+inline TTableWriterPtr<Message> CreateJobWriter<Message>(THolder<IProxyOutput> rawJobWriter)
+{
+ return new TTableWriter<Message>(CreateJobProtoWriter(std::move(rawJobWriter)));
+}
+
+template <class T, class = void>
+struct TProtoWriterCreator;
+
+template <class T>
+struct TProtoWriterCreator<T, std::enable_if_t<TIsBaseOf<Message, T>::Value>>
+{
+ static TTableWriterPtr<T> Create(::TIntrusivePtr<IProtoWriterImpl> writer)
+ {
+ return new TTableWriter<T>(writer);
+ }
+};
+
+template <class T>
+inline TTableWriterPtr<T> CreateJobWriter(THolder<IProxyOutput> rawJobWriter)
+{
+ if constexpr (TIsBaseOf<Message, T>::Value) {
+ return TProtoWriterCreator<T>::Create(CreateJobProtoWriter(std::move(rawJobWriter)));
+ } else {
+ static_assert(TDependentFalse<T>, "Unknown row type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TOperationInputSpecBase::AddInput(const TRichYPath& path)
+{
+ Inputs_.push_back(path);
+ StructuredInputs_.emplace_back(Structured<T>(path));
+}
+
+template <class T>
+void TOperationInputSpecBase::SetInput(size_t tableIndex, const TRichYPath& path)
+{
+ NDetail::Assign(Inputs_, tableIndex, path);
+ NDetail::Assign(StructuredInputs_, tableIndex, Structured<T>(path));
+}
+
+
+template <class T>
+void TOperationOutputSpecBase::AddOutput(const TRichYPath& path)
+{
+ Outputs_.push_back(path);
+ StructuredOutputs_.emplace_back(Structured<T>(path));
+}
+
+template <class T>
+void TOperationOutputSpecBase::SetOutput(size_t tableIndex, const TRichYPath& path)
+{
+ NDetail::Assign(Outputs_, tableIndex, path);
+ NDetail::Assign(StructuredOutputs_, tableIndex, Structured<T>(path));
+}
+
+template <class TDerived>
+template <class T>
+TDerived& TOperationIOSpec<TDerived>::AddInput(const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "input type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationInputSpecBase::AddInput<T>(path);
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class T>
+TDerived& TOperationIOSpec<TDerived>::SetInput(size_t tableIndex, const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "input type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationInputSpecBase::SetInput<T>(tableIndex, path);
+ return *static_cast<TDerived*>(this);
+}
+
+
+template <class TDerived>
+template <class T>
+TDerived& TOperationIOSpec<TDerived>::AddOutput(const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "output type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationOutputSpecBase::AddOutput<T>(path);
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class T>
+TDerived& TOperationIOSpec<TDerived>::SetOutput(size_t tableIndex, const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "output type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationOutputSpecBase::SetOutput<T>(tableIndex, path);
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+TDerived& TOperationIOSpec<TDerived>::AddStructuredInput(TStructuredTablePath path)
+{
+ TOperationInputSpecBase::AddStructuredInput(std::move(path));
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+TDerived& TOperationIOSpec<TDerived>::AddStructuredOutput(TStructuredTablePath path)
+{
+ TOperationOutputSpecBase::AddStructuredOutput(std::move(path));
+ return *static_cast<TDerived*>(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TVanillaTask& TVanillaTask::AddOutput(const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "output type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationOutputSpecBase::AddOutput<T>(path);
+ return *this;
+}
+
+template <class T>
+TVanillaTask& TVanillaTask::SetOutput(size_t tableIndex, const TRichYPath& path)
+{
+ static_assert(!std::is_same<T, Message>::value, "output type can't be Message, it can only be its strict subtype (see st.yandex-team.ru/YT-7609)");
+ TOperationOutputSpecBase::SetOutput<T>(tableIndex, path);
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+void ResetUseClientProtobuf(const char* methodName);
+
+} // namespace NDetail
+
+template <class TDerived>
+TDerived& TOperationIOSpec<TDerived>::AddProtobufInput_VerySlow_Deprecated(const TRichYPath& path)
+{
+ NDetail::ResetUseClientProtobuf("AddProtobufInput_VerySlow_Deprecated");
+ Inputs_.push_back(path);
+ StructuredInputs_.emplace_back(TStructuredTablePath(path, TProtobufTableStructure{nullptr}));
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+TDerived& TOperationIOSpec<TDerived>::AddProtobufOutput_VerySlow_Deprecated(const TRichYPath& path)
+{
+ NDetail::ResetUseClientProtobuf("AddProtobufOutput_VerySlow_Deprecated");
+ Outputs_.push_back(path);
+ StructuredOutputs_.emplace_back(TStructuredTablePath(path, TProtobufTableStructure{nullptr}));
+ return *static_cast<TDerived*>(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TRow>
+TJobOperationPreparer::TInputGroup& TJobOperationPreparer::TInputGroup::Description()
+{
+ for (auto i : Indices_) {
+ Preparer_.InputDescription<TRow>(i);
+ }
+ return *this;
+}
+
+template <typename TRow>
+TJobOperationPreparer::TOutputGroup& TJobOperationPreparer::TOutputGroup::Description(bool inferSchema)
+{
+ for (auto i : Indices_) {
+ Preparer_.OutputDescription<TRow>(i, inferSchema);
+ }
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TCont>
+TJobOperationPreparer::TInputGroup TJobOperationPreparer::BeginInputGroup(const TCont& indices)
+{
+ for (auto i : indices) {
+ ValidateInputTableIndex(i, TStringBuf("BeginInputGroup()"));
+ }
+ return TInputGroup(*this, TVector<int>(std::begin(indices), std::end(indices)));
+}
+
+template <typename TCont>
+TJobOperationPreparer::TOutputGroup TJobOperationPreparer::BeginOutputGroup(const TCont& indices)
+{
+ for (auto i : indices) {
+ ValidateOutputTableIndex(i, TStringBuf("BeginOutputGroup()"));
+ }
+ return TOutputGroup(*this, indices);
+}
+
+
+template <typename TRow>
+TJobOperationPreparer& TJobOperationPreparer::InputDescription(int tableIndex)
+{
+ ValidateMissingInputDescription(tableIndex);
+ InputTableDescriptions_[tableIndex] = StructuredTableDescription<TRow>();
+ return *this;
+}
+
+template <typename TRow>
+TJobOperationPreparer& TJobOperationPreparer::OutputDescription(int tableIndex, bool inferSchema)
+{
+ ValidateMissingOutputDescription(tableIndex);
+ OutputTableDescriptions_[tableIndex] = StructuredTableDescription<TRow>();
+ if (inferSchema && !OutputSchemas_[tableIndex]) {
+ OutputSchemas_[tableIndex] = CreateTableSchema<TRow>();
+ }
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TDerived>
+template <class TRow>
+TDerived& TIntermediateTablesHintSpec<TDerived>::HintMapOutput()
+{
+ IntermediateMapOutputDescription_ = StructuredTableDescription<TRow>();
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class TRow>
+TDerived& TIntermediateTablesHintSpec<TDerived>::AddMapOutput(const TRichYPath& path)
+{
+ MapOutputs_.push_back(path);
+ StructuredMapOutputs_.emplace_back(Structured<TRow>(path));
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class TRow>
+TDerived& TIntermediateTablesHintSpec<TDerived>::HintReduceCombinerInput()
+{
+ IntermediateReduceCombinerInputDescription_ = StructuredTableDescription<TRow>();
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class TRow>
+TDerived& TIntermediateTablesHintSpec<TDerived>::HintReduceCombinerOutput()
+{
+ IntermediateReduceCombinerOutputDescription_ = StructuredTableDescription<TRow>();
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+template <class TRow>
+TDerived& TIntermediateTablesHintSpec<TDerived>::HintReduceInput()
+{
+ IntermediateReducerInputDescription_ = StructuredTableDescription<TRow>();
+ return *static_cast<TDerived*>(this);
+}
+
+template <class TDerived>
+const TVector<TStructuredTablePath>& TIntermediateTablesHintSpec<TDerived>::GetStructuredMapOutputs() const
+{
+ return StructuredMapOutputs_;
+}
+
+template <class TDerived>
+const TMaybe<TTableStructure>& TIntermediateTablesHintSpec<TDerived>::GetIntermediateMapOutputDescription() const
+{
+ return IntermediateMapOutputDescription_;
+}
+
+template <class TDerived>
+const TMaybe<TTableStructure>& TIntermediateTablesHintSpec<TDerived>::GetIntermediateReduceCombinerInputDescription() const
+{
+ return IntermediateReduceCombinerInputDescription_;
+}
+
+template <class TDerived>
+const TMaybe<TTableStructure>& TIntermediateTablesHintSpec<TDerived>::GetIntermediateReduceCombinerOutputDescription() const
+{
+ return IntermediateReduceCombinerOutputDescription_;
+}
+
+template <class TDerived>
+const TMaybe<TTableStructure>& TIntermediateTablesHintSpec<TDerived>::GetIntermediateReducerInputDescription() const
+{
+ return IntermediateReducerInputDescription_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReducerContext
+{
+ bool Break = false;
+ static TReducerContext* Get() { return Singleton<TReducerContext>(); }
+};
+
+template <class TR, class TW>
+inline void IReducer<TR, TW>::Break()
+{
+ TReducerContext::Get()->Break = true;
+}
+
+template <typename TReader, typename TWriter>
+void FeedJobInput(
+ IMapper<TReader, TWriter>* mapper,
+ typename TRowTraits<typename TReader::TRowType>::IReaderImpl* readerImpl,
+ TWriter* writer)
+{
+ using TInputRow = typename TReader::TRowType;
+
+ auto reader = MakeIntrusive<TTableReader<TInputRow>>(readerImpl);
+ mapper->Do(reader.Get(), writer);
+}
+
+template <typename TReader, typename TWriter>
+void FeedJobInput(
+ IReducer<TReader, TWriter>* reducer,
+ typename TRowTraits<typename TReader::TRowType>::IReaderImpl* readerImpl,
+ TWriter* writer)
+{
+ using TInputRow = typename TReader::TRowType;
+
+ auto rangesReader = MakeIntrusive<TTableRangesReader<TInputRow>>(readerImpl);
+ for (; rangesReader->IsValid(); rangesReader->Next()) {
+ reducer->Do(&rangesReader->GetRange(), writer);
+ if (TReducerContext::Get()->Break) {
+ break;
+ }
+ }
+}
+
+template <typename TReader, typename TWriter>
+void FeedJobInput(
+ IAggregatorReducer<TReader, TWriter>* reducer,
+ typename TRowTraits<typename TReader::TRowType>::IReaderImpl* readerImpl,
+ TWriter* writer)
+{
+ using TInputRow = typename TReader::TRowType;
+
+ auto rangesReader = MakeIntrusive<TTableRangesReader<TInputRow>>(readerImpl);
+ reducer->Do(rangesReader.Get(), writer);
+}
+
+template <class TRawJob>
+int RunRawJob(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ TRawJobContext context(outputTableCount);
+
+ TRawJob job;
+ job.Load(jobStateStream);
+ job.Do(context);
+ return 0;
+}
+
+template <>
+inline int RunRawJob<TCommandRawJob>(size_t /* outputTableCount */, IInputStream& /* jobStateStream */)
+{
+ Y_FAIL();
+}
+
+template <class TVanillaJob>
+int RunVanillaJob(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ TVanillaJob job;
+ job.Load(jobStateStream);
+
+ if constexpr (std::is_base_of<IVanillaJob<>, TVanillaJob>::value) {
+ Y_VERIFY(outputTableCount == 0, "Void vanilla job expects zero 'outputTableCount'");
+ job.Do();
+ } else {
+ Y_VERIFY(outputTableCount, "Vanilla job with table writer expects nonzero 'outputTableCount'");
+ using TOutputRow = typename TVanillaJob::TWriter::TRowType;
+
+ THolder<IProxyOutput> rawJobWriter;
+ if (auto customWriter = job.CreateCustomRawJobWriter(outputTableCount)) {
+ rawJobWriter = std::move(customWriter);
+ } else {
+ rawJobWriter = CreateRawJobWriter(outputTableCount);
+ }
+ auto writer = CreateJobWriter<TOutputRow>(std::move(rawJobWriter));
+
+ job.Start(writer.Get());
+ job.Do(writer.Get());
+ job.Finish(writer.Get());
+
+ writer->Finish();
+ }
+ return 0;
+}
+
+template <>
+inline int RunVanillaJob<TCommandVanillaJob>(size_t /* outputTableCount */, IInputStream& /* jobStateStream */)
+{
+ Y_FAIL();
+}
+
+template <class TJob>
+ requires TIsBaseOf<IStructuredJob, TJob>::Value
+int RunJob(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ using TInputRow = typename TJob::TReader::TRowType;
+ using TOutputRow = typename TJob::TWriter::TRowType;
+
+ auto job = MakeIntrusive<TJob>();
+ job->Load(jobStateStream);
+
+ TRawTableReaderPtr rawJobReader;
+ if (auto customReader = job->CreateCustomRawJobReader(/*fd*/ 0)) {
+ rawJobReader = customReader;
+ } else {
+ rawJobReader = CreateRawJobReader(/*fd*/ 0);
+ }
+ auto readerImpl = CreateJobReaderImpl<TInputRow>(rawJobReader);
+
+ // Many users don't expect to have jobs with empty input so we skip such jobs.
+ if (!readerImpl->IsValid()) {
+ return 0;
+ }
+
+ THolder<IProxyOutput> rawJobWriter;
+ if (auto customWriter = job->CreateCustomRawJobWriter(outputTableCount)) {
+ rawJobWriter = std::move(customWriter);
+ } else {
+ rawJobWriter = CreateRawJobWriter(outputTableCount);
+ }
+ auto writer = CreateJobWriter<TOutputRow>(std::move(rawJobWriter));
+
+ job->Start(writer.Get());
+ FeedJobInput(job.Get(), readerImpl.Get(), writer.Get());
+ job->Finish(writer.Get());
+
+ writer->Finish();
+
+ return 0;
+}
+
+//
+// We leave RunMapJob/RunReduceJob/RunAggregatorReducer for backward compatibility,
+// some user use them already. :(
+
+template <class TMapper>
+int RunMapJob(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ return RunJob<TMapper>(outputTableCount, jobStateStream);
+}
+
+template <class TReducer>
+int RunReduceJob(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ return RunJob<TReducer>(outputTableCount, jobStateStream);
+}
+
+template <class TReducer>
+int RunAggregatorReducer(size_t outputTableCount, IInputStream& jobStateStream)
+{
+ return RunJob<TReducer>(outputTableCount, jobStateStream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T, typename = void>
+struct TIsConstructibleFromNode
+ : std::false_type
+{ };
+
+template <typename T>
+struct TIsConstructibleFromNode<T, std::void_t<decltype(T::FromNode(std::declval<TNode&>()))>>
+ : std::true_type
+{ };
+
+template <class TJob>
+::TIntrusivePtr<NYT::IStructuredJob> ConstructJobFromNode(const TNode& node)
+{
+ if constexpr (TIsConstructibleFromNode<TJob>::value) {
+ Y_ENSURE(node.GetType() != TNode::Undefined,
+ "job has FromNode method but constructor arguments were not provided");
+ return TJob::FromNode(node);
+ } else {
+ Y_ENSURE(node.GetType() == TNode::Undefined,
+ "constructor arguments provided but job does not contain FromNode method");
+ return MakeIntrusive<TJob>();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TJobFunction = int (*)(size_t, IInputStream&);
+using TConstructJobFunction = ::TIntrusivePtr<NYT::IStructuredJob> (*)(const TNode&);
+
+class TJobFactory
+{
+public:
+ static TJobFactory* Get()
+ {
+ return Singleton<TJobFactory>();
+ }
+
+ template <class TJob>
+ void RegisterJob(const char* name)
+ {
+ RegisterJobImpl<TJob>(name, RunJob<TJob>);
+ JobConstructors[name] = ConstructJobFromNode<TJob>;
+ }
+
+ template <class TRawJob>
+ void RegisterRawJob(const char* name)
+ {
+ RegisterJobImpl<TRawJob>(name, RunRawJob<TRawJob>);
+ }
+
+ template <class TVanillaJob>
+ void RegisterVanillaJob(const char* name)
+ {
+ RegisterJobImpl<TVanillaJob>(name, RunVanillaJob<TVanillaJob>);
+ }
+
+ TString GetJobName(const IJob* job)
+ {
+ const auto typeIndex = std::type_index(typeid(*job));
+ CheckJobRegistered(typeIndex);
+ return JobNames[typeIndex];
+ }
+
+ TJobFunction GetJobFunction(const char* name)
+ {
+ CheckNameRegistered(name);
+ return JobFunctions[name];
+ }
+
+ TConstructJobFunction GetConstructingFunction(const char* name)
+ {
+ CheckNameRegistered(name);
+ return JobConstructors[name];
+ }
+
+private:
+ TMap<std::type_index, TString> JobNames;
+ THashMap<TString, TJobFunction> JobFunctions;
+ THashMap<TString, TConstructJobFunction> JobConstructors;
+
+ template <typename TJob, typename TRunner>
+ void RegisterJobImpl(const char* name, TRunner runner) {
+ const auto typeIndex = std::type_index(typeid(TJob));
+ CheckNotRegistered(typeIndex, name);
+ JobNames[typeIndex] = name;
+ JobFunctions[name] = runner;
+ }
+
+ void CheckNotRegistered(const std::type_index& typeIndex, const char* name)
+ {
+ Y_ENSURE(!JobNames.contains(typeIndex),
+ "type_info '" << typeIndex.name() << "'"
+ "is already registered under name '" << JobNames[typeIndex] << "'");
+ Y_ENSURE(!JobFunctions.contains(name),
+ "job with name '" << name << "' is already registered");
+ }
+
+ void CheckJobRegistered(const std::type_index& typeIndex)
+ {
+ Y_ENSURE(JobNames.contains(typeIndex),
+ "type_info '" << typeIndex.name() << "' is not registered, use REGISTER_* macros");
+ }
+
+ void CheckNameRegistered(const char* name)
+ {
+ Y_ENSURE(JobFunctions.contains(name),
+ "job with name '" << name << "' is not registered, use REGISTER_* macros");
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TMapper>
+struct TMapperRegistrator
+{
+ TMapperRegistrator(const char* name)
+ {
+ static_assert(TMapper::JobType == IJob::EType::Mapper,
+ "REGISTER_MAPPER is not compatible with this job class");
+
+ NYT::TJobFactory::Get()->RegisterJob<TMapper>(name);
+ }
+};
+
+template <class TReducer>
+struct TReducerRegistrator
+{
+ TReducerRegistrator(const char* name)
+ {
+ static_assert(TReducer::JobType == IJob::EType::Reducer ||
+ TReducer::JobType == IJob::EType::ReducerAggregator,
+ "REGISTER_REDUCER is not compatible with this job class");
+
+ NYT::TJobFactory::Get()->RegisterJob<TReducer>(name);
+ }
+};
+
+template <class TRawJob>
+struct TRawJobRegistrator
+{
+ TRawJobRegistrator(const char* name)
+ {
+ static_assert(TRawJob::JobType == IJob::EType::RawJob,
+ "REGISTER_RAW_JOB is not compatible with this job class");
+ NYT::TJobFactory::Get()->RegisterRawJob<TRawJob>(name);
+ }
+};
+
+template <class TVanillaJob>
+struct TVanillaJobRegistrator
+{
+ TVanillaJobRegistrator(const char* name)
+ {
+ static_assert(TVanillaJob::JobType == IJob::EType::VanillaJob,
+ "REGISTER_VANILLA_JOB is not compatible with this job class");
+ NYT::TJobFactory::Get()->RegisterVanillaJob<TVanillaJob>(name);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TString YtRegistryTypeName(const TString& name) {
+ TString res = name;
+#ifdef _win_
+ SubstGlobal(res, "class ", "");
+#endif
+ return res;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define REGISTER_MAPPER(...) \
+static const NYT::TMapperRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(NYT::YtRegistryTypeName(TypeName<__VA_ARGS__>()).data());
+
+#define REGISTER_NAMED_MAPPER(name, ...) \
+static const NYT::TMapperRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(name);
+
+#define REGISTER_REDUCER(...) \
+static const NYT::TReducerRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(NYT::YtRegistryTypeName(TypeName<__VA_ARGS__>()).data());
+
+#define REGISTER_NAMED_REDUCER(name, ...) \
+static const NYT::TReducerRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(name);
+
+#define REGISTER_NAMED_RAW_JOB(name, ...) \
+static const NYT::TRawJobRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(name);
+
+#define REGISTER_RAW_JOB(...) \
+REGISTER_NAMED_RAW_JOB((NYT::YtRegistryTypeName(TypeName<__VA_ARGS__>()).data()), __VA_ARGS__)
+
+#define REGISTER_NAMED_VANILLA_JOB(name, ...) \
+static NYT::TVanillaJobRegistrator<__VA_ARGS__> \
+Y_GENERATE_UNIQUE_ID(TJobRegistrator)(name);
+
+#define REGISTER_VANILLA_JOB(...) \
+REGISTER_NAMED_VANILLA_JOB((NYT::YtRegistryTypeName(TypeName<__VA_ARGS__>()).data()), __VA_ARGS__)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IMapper<TReader, TWriter>::GetInputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TReader::TRowType>();
+}
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IMapper<TReader, TWriter>::GetOutputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TWriter::TRowType>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IReducer<TReader, TWriter>::GetInputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TReader::TRowType>();
+}
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IReducer<TReader, TWriter>::GetOutputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TWriter::TRowType>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IAggregatorReducer<TReader, TWriter>::GetInputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TReader::TRowType>();
+}
+
+template <typename TReader, typename TWriter>
+TStructuredRowStreamDescription IAggregatorReducer<TReader, TWriter>::GetOutputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TWriter::TRowType>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TWriter>
+TStructuredRowStreamDescription IVanillaJob<TWriter>::GetInputRowStreamDescription() const
+{
+ return TVoidStructuredRowStream();
+}
+
+template <typename TWriter>
+TStructuredRowStreamDescription IVanillaJob<TWriter>::GetOutputRowStreamDescription() const
+{
+ return NYT::NDetail::GetStructuredRowStreamDescription<typename TWriter::TRowType>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/operation.cpp b/yt/cpp/mapreduce/interface/operation.cpp
new file mode 100644
index 0000000000..706fc4caa4
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/operation.cpp
@@ -0,0 +1,663 @@
+#include "operation.h"
+
+#include <util/generic/iterator_range.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+ i64 OutputTableCount = -1;
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTaskName::TTaskName(TString taskName)
+ : TaskName_(std::move(taskName))
+{ }
+
+TTaskName::TTaskName(const char* taskName)
+ : TaskName_(taskName)
+{ }
+
+TTaskName::TTaskName(ETaskName taskName)
+ : TaskName_(ToString(taskName))
+{ }
+
+const TString& TTaskName::Get() const
+{
+ return TaskName_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCommandRawJob::TCommandRawJob(TStringBuf command)
+ : Command_(command)
+{ }
+
+const TString& TCommandRawJob::GetCommand() const
+{
+ return Command_;
+}
+
+void TCommandRawJob::Do(const TRawJobContext& /* jobContext */)
+{
+ Y_FAIL("TCommandRawJob::Do must not be called");
+}
+
+REGISTER_NAMED_RAW_JOB("NYT::TCommandRawJob", TCommandRawJob)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCommandVanillaJob::TCommandVanillaJob(TStringBuf command)
+ : Command_(command)
+{ }
+
+const TString& TCommandVanillaJob::GetCommand() const
+{
+ return Command_;
+}
+
+void TCommandVanillaJob::Do()
+{
+ Y_FAIL("TCommandVanillaJob::Do must not be called");
+}
+
+REGISTER_NAMED_VANILLA_JOB("NYT::TCommandVanillaJob", TCommandVanillaJob);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TUnspecifiedTableStructure&, const TUnspecifiedTableStructure&)
+{
+ return true;
+}
+
+bool operator==(const TProtobufTableStructure& lhs, const TProtobufTableStructure& rhs)
+{
+ return lhs.Descriptor == rhs.Descriptor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TVector<TStructuredTablePath>& TOperationInputSpecBase::GetStructuredInputs() const
+{
+ return StructuredInputs_;
+}
+
+const TVector<TStructuredTablePath>& TOperationOutputSpecBase::GetStructuredOutputs() const
+{
+ return StructuredOutputs_;
+}
+
+void TOperationInputSpecBase::AddStructuredInput(TStructuredTablePath path)
+{
+ Inputs_.push_back(path.RichYPath);
+ StructuredInputs_.push_back(std::move(path));
+}
+
+void TOperationOutputSpecBase::AddStructuredOutput(TStructuredTablePath path)
+{
+ Outputs_.push_back(path.RichYPath);
+ StructuredOutputs_.push_back(std::move(path));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVanillaTask& TVanillaTask::AddStructuredOutput(TStructuredTablePath path)
+{
+ TOperationOutputSpecBase::AddStructuredOutput(std::move(path));
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructuredRowStreamDescription IVanillaJob<void>::GetInputRowStreamDescription() const
+{
+ return TVoidStructuredRowStream();
+}
+
+TStructuredRowStreamDescription IVanillaJob<void>::GetOutputRowStreamDescription() const
+{
+ return TVoidStructuredRowStream();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRawJobContext::TRawJobContext(size_t outputTableCount)
+ : InputFile_(Duplicate(0))
+{
+ for (size_t i = 0; i != outputTableCount; ++i) {
+ OutputFileList_.emplace_back(Duplicate(3 * i + 1));
+ }
+}
+
+const TFile& TRawJobContext::GetInputFile() const
+{
+ return InputFile_;
+}
+
+const TVector<TFile>& TRawJobContext::GetOutputFileList() const
+{
+ return OutputFileList_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUserJobSpec& TUserJobSpec::AddLocalFile(
+ const TLocalFilePath& path,
+ const TAddLocalFileOptions& options)
+{
+ LocalFiles_.emplace_back(path, options);
+ return *this;
+}
+
+TUserJobSpec& TUserJobSpec::JobBinaryLocalPath(TString path, TMaybe<TString> md5)
+{
+ JobBinary_ = TJobBinaryLocalPath{path, md5};
+ return *this;
+}
+
+TUserJobSpec& TUserJobSpec::JobBinaryCypressPath(TString path, TMaybe<TTransactionId> transactionId)
+{
+ JobBinary_ = TJobBinaryCypressPath{path, transactionId};
+ return *this;
+}
+
+const TJobBinaryConfig& TUserJobSpec::GetJobBinary() const
+{
+ return JobBinary_;
+}
+
+TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> TUserJobSpec::GetLocalFiles() const
+{
+ return LocalFiles_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobOperationPreparer::TInputGroup::TInputGroup(TJobOperationPreparer& preparer, TVector<int> indices)
+ : Preparer_(preparer)
+ , Indices_(std::move(indices))
+{ }
+
+TJobOperationPreparer::TInputGroup& TJobOperationPreparer::TInputGroup::ColumnRenaming(const THashMap<TString, TString>& renaming)
+{
+ for (auto i : Indices_) {
+ Preparer_.InputColumnRenaming(i, renaming);
+ }
+ return *this;
+}
+
+TJobOperationPreparer::TInputGroup& TJobOperationPreparer::TInputGroup::ColumnFilter(const TVector<TString>& columns)
+{
+ for (auto i : Indices_) {
+ Preparer_.InputColumnFilter(i, columns);
+ }
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::TInputGroup::EndInputGroup()
+{
+ return Preparer_;
+}
+
+TJobOperationPreparer::TOutputGroup::TOutputGroup(TJobOperationPreparer& preparer, TVector<int> indices)
+ : Preparer_(preparer)
+ , Indices_(std::move(indices))
+{ }
+
+TJobOperationPreparer::TOutputGroup& TJobOperationPreparer::TOutputGroup::Schema(const TTableSchema &schema)
+{
+ for (auto i : Indices_) {
+ Preparer_.OutputSchema(i, schema);
+ }
+ return *this;
+}
+
+TJobOperationPreparer::TOutputGroup& TJobOperationPreparer::TOutputGroup::NoSchema()
+{
+ for (auto i : Indices_) {
+ Preparer_.NoOutputSchema(i);
+ }
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::TOutputGroup::EndOutputGroup()
+{
+ return Preparer_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobOperationPreparer::TJobOperationPreparer(const IOperationPreparationContext& context)
+ : Context_(context)
+ , OutputSchemas_(context.GetOutputCount())
+ , InputColumnRenamings_(context.GetInputCount())
+ , InputColumnFilters_(context.GetInputCount())
+ , InputTableDescriptions_(context.GetInputCount())
+ , OutputTableDescriptions_(context.GetOutputCount())
+{ }
+
+TJobOperationPreparer::TInputGroup TJobOperationPreparer::BeginInputGroup(int begin, int end)
+{
+ Y_ENSURE_EX(begin <= end, TApiUsageError()
+ << "BeginInputGroup(): begin must not exceed end, got " << begin << ", " << end);
+ TVector<int> indices;
+ for (int i = begin; i < end; ++i) {
+ ValidateInputTableIndex(i, TStringBuf("BeginInputGroup()"));
+ indices.push_back(i);
+ }
+ return TInputGroup(*this, std::move(indices));
+}
+
+
+TJobOperationPreparer::TOutputGroup TJobOperationPreparer::BeginOutputGroup(int begin, int end)
+{
+ Y_ENSURE_EX(begin <= end, TApiUsageError()
+ << "BeginOutputGroup(): begin must not exceed end, got " << begin << ", " << end);
+ TVector<int> indices;
+ for (int i = begin; i < end; ++i) {
+ ValidateOutputTableIndex(i, TStringBuf("BeginOutputGroup()"));
+ indices.push_back(i);
+ }
+ return TOutputGroup(*this, std::move(indices));
+}
+
+TJobOperationPreparer& TJobOperationPreparer::NodeOutput(int tableIndex)
+{
+ ValidateMissingOutputDescription(tableIndex);
+ OutputTableDescriptions_[tableIndex] = StructuredTableDescription<TNode>();
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::OutputSchema(int tableIndex, TTableSchema schema)
+{
+ ValidateMissingOutputSchema(tableIndex);
+ OutputSchemas_[tableIndex] = std::move(schema);
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::NoOutputSchema(int tableIndex)
+{
+ ValidateMissingOutputSchema(tableIndex);
+ OutputSchemas_[tableIndex] = EmptyNonstrictSchema();
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::InputColumnRenaming(
+ int tableIndex,
+ const THashMap<TString,TString>& renaming)
+{
+ ValidateInputTableIndex(tableIndex, TStringBuf("InputColumnRenaming()"));
+ InputColumnRenamings_[tableIndex] = renaming;
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::InputColumnFilter(int tableIndex, const TVector<TString>& columns)
+{
+ ValidateInputTableIndex(tableIndex, TStringBuf("InputColumnFilter()"));
+ InputColumnFilters_[tableIndex] = columns;
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::FormatHints(TUserJobFormatHints newFormatHints)
+{
+ FormatHints_ = newFormatHints;
+ return *this;
+}
+
+void TJobOperationPreparer::Finish()
+{
+ FinallyValidate();
+}
+
+TVector<TTableSchema> TJobOperationPreparer::GetOutputSchemas()
+{
+ TVector<TTableSchema> result;
+ result.reserve(OutputSchemas_.size());
+ for (auto& schema : OutputSchemas_) {
+ Y_VERIFY(schema.Defined());
+ result.push_back(std::move(*schema));
+ schema.Clear();
+ }
+ return result;
+}
+
+void TJobOperationPreparer::FinallyValidate() const
+{
+ TVector<int> illegallyMissingSchemaIndices;
+ for (int i = 0; i < static_cast<int>(OutputSchemas_.size()); ++i) {
+ if (!OutputSchemas_[i]) {
+ illegallyMissingSchemaIndices.push_back(i);
+ }
+ }
+ if (illegallyMissingSchemaIndices.empty()) {
+ return;
+ }
+ TApiUsageError error;
+ error << "Output table schemas are missing: ";
+ for (auto i : illegallyMissingSchemaIndices) {
+ error << "no. " << i;
+ if (auto path = Context_.GetInputPath(i)) {
+ error << "(" << *path << ")";
+ }
+ error << "; ";
+ }
+ ythrow std::move(error);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJobOperationPreparer::ValidateInputTableIndex(int tableIndex, TStringBuf message) const
+{
+ Y_ENSURE_EX(
+ 0 <= tableIndex && tableIndex < static_cast<int>(Context_.GetInputCount()),
+ TApiUsageError() <<
+ message << ": input table index " << tableIndex << " us out of range [0;" <<
+ OutputSchemas_.size() << ")");
+}
+
+void TJobOperationPreparer::ValidateOutputTableIndex(int tableIndex, TStringBuf message) const
+{
+ Y_ENSURE_EX(
+ 0 <= tableIndex && tableIndex < static_cast<int>(Context_.GetOutputCount()),
+ TApiUsageError() <<
+ message << ": output table index " << tableIndex << " us out of range [0;" <<
+ OutputSchemas_.size() << ")");
+}
+
+void TJobOperationPreparer::ValidateMissingOutputSchema(int tableIndex) const
+{
+ ValidateOutputTableIndex(tableIndex, "ValidateMissingOutputSchema()");
+ Y_ENSURE_EX(!OutputSchemas_[tableIndex],
+ TApiUsageError() <<
+ "Output table schema no. " << tableIndex << " " <<
+ "(" << Context_.GetOutputPath(tableIndex).GetOrElse("<unknown path>") << ") " <<
+ "is already set");
+}
+
+void TJobOperationPreparer::ValidateMissingInputDescription(int tableIndex) const
+{
+ ValidateInputTableIndex(tableIndex, "ValidateMissingInputDescription()");
+ Y_ENSURE_EX(!InputTableDescriptions_[tableIndex],
+ TApiUsageError() <<
+ "Description for input no. " << tableIndex << " " <<
+ "(" << Context_.GetOutputPath(tableIndex).GetOrElse("<unknown path>") << ") " <<
+ "is already set");
+}
+
+void TJobOperationPreparer::ValidateMissingOutputDescription(int tableIndex) const
+{
+ ValidateOutputTableIndex(tableIndex, "ValidateMissingOutputDescription()");
+ Y_ENSURE_EX(!OutputTableDescriptions_[tableIndex],
+ TApiUsageError() <<
+ "Description for output no. " << tableIndex << " " <<
+ "(" << Context_.GetOutputPath(tableIndex).GetOrElse("<unknown path>") << ") " <<
+ "is already set");
+}
+
+TTableSchema TJobOperationPreparer::EmptyNonstrictSchema() {
+ return TTableSchema().Strict(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TVector<THashMap<TString, TString>>& TJobOperationPreparer::GetInputColumnRenamings() const
+{
+ return InputColumnRenamings_;
+}
+
+const TVector<TMaybe<TVector<TString>>>& TJobOperationPreparer::GetInputColumnFilters() const
+{
+ return InputColumnFilters_;
+}
+
+const TVector<TMaybe<TTableStructure>>& TJobOperationPreparer::GetInputDescriptions() const
+{
+ return InputTableDescriptions_;
+}
+
+const TVector<TMaybe<TTableStructure>>& TJobOperationPreparer::GetOutputDescriptions() const
+{
+ return OutputTableDescriptions_;
+}
+
+const TUserJobFormatHints& TJobOperationPreparer::GetFormatHints() const
+{
+ return FormatHints_;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::InputFormatHints(TFormatHints hints)
+{
+ FormatHints_.InputFormatHints(hints);
+ return *this;
+}
+
+TJobOperationPreparer& TJobOperationPreparer::OutputFormatHints(TFormatHints hints)
+{
+ FormatHints_.OutputFormatHints(hints);
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IJob::PrepareOperation(const IOperationPreparationContext& context, TJobOperationPreparer& resultBuilder) const
+{
+ for (int i = 0; i < context.GetOutputCount(); ++i) {
+ resultBuilder.NoOutputSchema(i);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IOperationPtr IOperationClient::Map(
+ const TMapOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ const TOperationOptions& options)
+{
+ Y_VERIFY(mapper.Get());
+
+ return DoMap(
+ spec,
+ std::move(mapper),
+ options);
+}
+
+IOperationPtr IOperationClient::Map(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TMapOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ Y_ENSURE_EX(spec.Inputs_.empty(),
+ TApiUsageError() << "TMapOperationSpec::Inputs MUST be empty");
+ Y_ENSURE_EX(spec.Outputs_.empty(),
+ TApiUsageError() << "TMapOperationSpec::Outputs MUST be empty");
+
+ auto mapSpec = spec;
+ for (const auto& inputPath : input.Parts_) {
+ mapSpec.AddStructuredInput(inputPath);
+ }
+ for (const auto& outputPath : output.Parts_) {
+ mapSpec.AddStructuredOutput(outputPath);
+ }
+ return Map(mapSpec, std::move(mapper), options);
+}
+
+IOperationPtr IOperationClient::Reduce(
+ const TReduceOperationSpec& spec,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options)
+{
+ Y_VERIFY(reducer.Get());
+
+ return DoReduce(
+ spec,
+ std::move(reducer),
+ options);
+}
+
+IOperationPtr IOperationClient::Reduce(
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ const TReduceOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ Y_ENSURE_EX(spec.Inputs_.empty(),
+ TApiUsageError() << "TReduceOperationSpec::Inputs MUST be empty");
+ Y_ENSURE_EX(spec.Outputs_.empty(),
+ TApiUsageError() << "TReduceOperationSpec::Outputs MUST be empty");
+ Y_ENSURE_EX(spec.ReduceBy_.Parts_.empty(),
+ TApiUsageError() << "TReduceOperationSpec::ReduceBy MUST be empty");
+
+ auto reduceSpec = spec;
+ for (const auto& inputPath : input.Parts_) {
+ reduceSpec.AddStructuredInput(inputPath);
+ }
+ for (const auto& outputPath : output.Parts_) {
+ reduceSpec.AddStructuredOutput(outputPath);
+ }
+ reduceSpec.ReduceBy(reduceBy);
+ return Reduce(reduceSpec, std::move(reducer), options);
+}
+
+IOperationPtr IOperationClient::JoinReduce(
+ const TJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options)
+{
+ Y_VERIFY(reducer.Get());
+
+ return DoJoinReduce(
+ spec,
+ std::move(reducer),
+ options);
+}
+
+IOperationPtr IOperationClient::MapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options)
+{
+ Y_VERIFY(reducer.Get());
+
+ return DoMapReduce(
+ spec,
+ std::move(mapper),
+ nullptr,
+ std::move(reducer),
+ options);
+}
+
+IOperationPtr IOperationClient::MapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reduceCombiner,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options)
+{
+ Y_VERIFY(reducer.Get());
+
+ return DoMapReduce(
+ spec,
+ std::move(mapper),
+ std::move(reduceCombiner),
+ std::move(reducer),
+ options);
+}
+
+IOperationPtr IOperationClient::MapReduce(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ TMapReduceOperationSpec spec,
+ const TOperationOptions& options)
+{
+ Y_ENSURE_EX(spec.Inputs_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::Inputs MUST be empty");
+ Y_ENSURE_EX(spec.Outputs_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::Outputs MUST be empty");
+ Y_ENSURE_EX(spec.ReduceBy_.Parts_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::ReduceBy MUST be empty");
+
+ for (const auto& inputPath : input.Parts_) {
+ spec.AddStructuredInput(inputPath);
+ }
+ for (const auto& outputPath : output.Parts_) {
+ spec.AddStructuredOutput(outputPath);
+ }
+ spec.ReduceBy(reduceBy);
+ return MapReduce(spec, std::move(mapper), std::move(reducer), options);
+}
+
+IOperationPtr IOperationClient::MapReduce(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reduceCombiner,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ TMapReduceOperationSpec spec,
+ const TOperationOptions& options)
+{
+ Y_ENSURE_EX(spec.Inputs_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::Inputs MUST be empty");
+ Y_ENSURE_EX(spec.Outputs_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::Outputs MUST be empty");
+ Y_ENSURE_EX(spec.ReduceBy_.Parts_.empty(),
+ TApiUsageError() << "TMapReduceOperationSpec::ReduceBy MUST be empty");
+
+ for (const auto& inputPath : input.Parts_) {
+ spec.AddStructuredInput(inputPath);
+ }
+ for (const auto& outputPath : output.Parts_) {
+ spec.AddStructuredOutput(outputPath);
+ }
+ spec.ReduceBy(reduceBy);
+ return MapReduce(spec, std::move(mapper), std::move(reduceCombiner), std::move(reducer), options);
+}
+
+IOperationPtr IOperationClient::Sort(
+ const TOneOrMany<TRichYPath>& input,
+ const TRichYPath& output,
+ const TSortColumns& sortBy,
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options)
+{
+ Y_ENSURE_EX(spec.Inputs_.empty(),
+ TApiUsageError() << "TSortOperationSpec::Inputs MUST be empty");
+ Y_ENSURE_EX(spec.Output_.Path_.empty(),
+ TApiUsageError() << "TSortOperationSpec::Output MUST be empty");
+ Y_ENSURE_EX(spec.SortBy_.Parts_.empty(),
+ TApiUsageError() << "TSortOperationSpec::SortBy MUST be empty");
+
+ auto sortSpec = spec;
+ for (const auto& inputPath : input.Parts_) {
+ sortSpec.AddInput(inputPath);
+ }
+ sortSpec.Output(output);
+ sortSpec.SortBy(sortBy);
+ return Sort(sortSpec, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRawTableReaderPtr IStructuredJob::CreateCustomRawJobReader(int) const
+{
+ return nullptr;
+}
+
+THolder<IProxyOutput> IStructuredJob::CreateCustomRawJobWriter(size_t) const
+{
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/operation.h b/yt/cpp/mapreduce/interface/operation.h
new file mode 100644
index 0000000000..171a7e4af7
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/operation.h
@@ -0,0 +1,3494 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/operation.h
+///
+/// Header containing interface to run operations in YT
+/// and retrieve information about them.
+/// @see [the doc](https://yt.yandex-team.ru/docs/description/mr/map_reduce_overview.html).
+
+#include "client_method_options.h"
+#include "errors.h"
+#include "io.h"
+#include "job_statistics.h"
+#include "job_counters.h"
+
+#include <library/cpp/threading/future/future.h>
+#include <library/cpp/type_info/type_info.h>
+
+#include <util/datetime/base.h>
+#include <util/generic/variant.h>
+#include <util/generic/vector.h>
+#include <util/generic/maybe.h>
+#include <util/system/file.h>
+#include <util/system/types.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Tag class marking that the row type for table is not specified.
+struct TUnspecifiedTableStructure
+{ };
+
+/// Tag class marking that table rows have protobuf type.
+struct TProtobufTableStructure
+{
+ /// @brief Descriptor of the protobuf type of table rows.
+ ///
+ /// @note If table is tagged with @ref ::google::protobuf::Message instead of real proto class
+ /// this descriptor might be null.
+ const ::google::protobuf::Descriptor* Descriptor = nullptr;
+};
+
+
+/// Tag class to specify table row type.
+using TTableStructure = std::variant<
+ TUnspecifiedTableStructure,
+ TProtobufTableStructure
+>;
+
+bool operator==(const TUnspecifiedTableStructure&, const TUnspecifiedTableStructure&);
+bool operator==(const TProtobufTableStructure& lhs, const TProtobufTableStructure& rhs);
+
+/// Table path marked with @ref NYT::TTableStructure tag.
+struct TStructuredTablePath
+{
+ TStructuredTablePath(TRichYPath richYPath = TRichYPath(), TTableStructure description = TUnspecifiedTableStructure())
+ : RichYPath(std::move(richYPath))
+ , Description(std::move(description))
+ { }
+
+ TStructuredTablePath(TRichYPath richYPath, const ::google::protobuf::Descriptor* descriptor)
+ : RichYPath(std::move(richYPath))
+ , Description(TProtobufTableStructure({descriptor}))
+ { }
+
+ TStructuredTablePath(TYPath path)
+ : RichYPath(std::move(path))
+ , Description(TUnspecifiedTableStructure())
+ { }
+
+ TStructuredTablePath(const char* path)
+ : RichYPath(path)
+ , Description(TUnspecifiedTableStructure())
+ { }
+
+ TRichYPath RichYPath;
+ TTableStructure Description;
+};
+
+/// Create marked table path from row type.
+template <typename TRow>
+TStructuredTablePath Structured(TRichYPath richYPath);
+
+/// Create tag class from row type.
+template <typename TRow>
+TTableStructure StructuredTableDescription();
+
+///////////////////////////////////////////////////////////////////////////////
+
+/// Tag class marking that row stream is empty.
+struct TVoidStructuredRowStream
+{ };
+
+/// Tag class marking that row stream consists of `NYT::TNode`.
+struct TTNodeStructuredRowStream
+{ };
+
+/// Tag class marking that row stream consists of @ref NYT::TYaMRRow.
+struct TTYaMRRowStructuredRowStream
+{ };
+
+/// Tag class marking that row stream consists of protobuf rows of given type.
+struct TProtobufStructuredRowStream
+{
+ /// @brief Descriptor of the protobuf type of table rows.
+ ///
+ /// @note If `Descriptor` is nullptr, then row stream consists of multiple message types.
+ const ::google::protobuf::Descriptor* Descriptor = nullptr;
+};
+
+/// Tag class to specify type of rows in an operation row stream
+using TStructuredRowStreamDescription = std::variant<
+ TVoidStructuredRowStream,
+ TTNodeStructuredRowStream,
+ TTYaMRRowStructuredRowStream,
+ TProtobufStructuredRowStream
+>;
+
+///////////////////////////////////////////////////////////////////////////////
+
+/// Tag class marking that current binary should be used in operation.
+struct TJobBinaryDefault
+{ };
+
+/// Tag class marking that binary from specified local path should be used in operation.
+struct TJobBinaryLocalPath
+{
+ TString Path;
+ TMaybe<TString> MD5CheckSum;
+};
+
+/// Tag class marking that binary from specified Cypress path should be used in operation.
+struct TJobBinaryCypressPath
+{
+ TYPath Path;
+ TMaybe<TTransactionId> TransactionId;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+/// @cond Doxygen_Suppress
+namespace NDetail {
+ extern i64 OutputTableCount;
+} // namespace NDetail
+/// @endcond
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Auto merge mode.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/automerge
+enum class EAutoMergeMode
+{
+ /// Auto merge is disabled.
+ Disabled /* "disabled" */,
+
+ /// Mode that tries to achieve good chunk sizes and doesn't limit usage of chunk quota for intermediate chunks.
+ Relaxed /* "relaxed" */,
+
+ /// Mode that tries to optimize usage of chunk quota for intermediate chunks, operation might run slower.
+ Economy /* "economy" */,
+
+ ///
+ /// @brief Manual configuration of automerge parameters.
+ ///
+ /// @ref TAutoMergeSpec
+ Manual /* "manual" */,
+};
+
+///
+/// @brief Options for auto merge operation stage.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/automerge
+class TAutoMergeSpec
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TAutoMergeSpec;
+ /// @endcond
+
+ /// Mode of the auto merge.
+ FLUENT_FIELD_OPTION(EAutoMergeMode, Mode);
+
+ /// @brief Upper limit for number of intermediate chunks.
+ ///
+ /// Works only for Manual mode.
+ FLUENT_FIELD_OPTION(i64, MaxIntermediateChunkCount);
+
+ /// @brief Number of chunks limit to merge in one job.
+ ///
+ /// Works only for Manual mode.
+ FLUENT_FIELD_OPTION(i64, ChunkCountPerMergeJob);
+
+ /// @brief Automerge will not merge chunks that are larger than `DesiredChunkSize * (ChunkSizeThreshold / 100.)`
+ ///
+ /// Works only for Manual mode.
+ FLUENT_FIELD_OPTION(i64, ChunkSizeThreshold);
+};
+
+/// Base for operations with auto merge options.
+template <class TDerived>
+class TWithAutoMergeSpec
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Options for auto merge operation stage.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/automerge
+ FLUENT_FIELD_OPTION(TAutoMergeSpec, AutoMerge);
+};
+
+///
+/// @brief Resources controlled by scheduler and used by running operations.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/scheduler/scheduler_and_pools#resursy
+class TSchedulerResources
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TSchedulerResources;
+ /// @endcond
+
+ /// Each job consumes exactly one user slot.
+ FLUENT_FIELD_OPTION_ENCAPSULATED(i64, UserSlots);
+
+ /// Number of (virtual) cpu cores consumed by all jobs.
+ FLUENT_FIELD_OPTION_ENCAPSULATED(i64, Cpu);
+
+ /// Amount of memory in bytes.
+ FLUENT_FIELD_OPTION_ENCAPSULATED(i64, Memory);
+};
+
+/// Base for input format hints of a user job.
+template <class TDerived>
+class TUserJobInputFormatHintsBase
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Fine tune input format of the job.
+ FLUENT_FIELD_OPTION(TFormatHints, InputFormatHints);
+};
+
+/// Base for output format hints of a user job.
+template <class TDerived>
+class TUserJobOutputFormatHintsBase
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Fine tune output format of the job.
+ FLUENT_FIELD_OPTION(TFormatHints, OutputFormatHints);
+};
+
+/// Base for format hints of a user job.
+template <class TDerived>
+class TUserJobFormatHintsBase
+ : public TUserJobInputFormatHintsBase<TDerived>
+ , public TUserJobOutputFormatHintsBase<TDerived>
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+};
+
+/// User job format hints.
+class TUserJobFormatHints
+ : public TUserJobFormatHintsBase<TUserJobFormatHints>
+{ };
+
+/// Spec of input and output tables of a raw operation.
+template <class TDerived>
+class TRawOperationIoTableSpec
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// Add input table path to input path list.
+ TDerived& AddInput(const TRichYPath& path);
+
+ /// Set input table path no. `tableIndex`.
+ TDerived& SetInput(size_t tableIndex, const TRichYPath& path);
+
+ /// Add output table path to output path list.
+ TDerived& AddOutput(const TRichYPath& path);
+
+ /// Set output table path no. `tableIndex`.
+ TDerived& SetOutput(size_t tableIndex, const TRichYPath& path);
+
+ /// Get all input table paths.
+ const TVector<TRichYPath>& GetInputs() const;
+
+ /// Get all output table paths.
+ const TVector<TRichYPath>& GetOutputs() const;
+
+private:
+ TVector<TRichYPath> Inputs_;
+ TVector<TRichYPath> Outputs_;
+};
+
+/// Base spec for IO in "simple" raw operations (Map, Reduce etc.).
+template <class TDerived>
+struct TSimpleRawOperationIoSpec
+ : public TRawOperationIoTableSpec<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Describes format for both input and output.
+ ///
+ /// @note `Format' is overriden by `InputFormat' and `OutputFormat'.
+ FLUENT_FIELD_OPTION(TFormat, Format);
+
+ /// Describes input format.
+ FLUENT_FIELD_OPTION(TFormat, InputFormat);
+
+ /// Describes output format.
+ FLUENT_FIELD_OPTION(TFormat, OutputFormat);
+};
+
+/// Spec for IO in MapReduce operation.
+template <class TDerived>
+class TRawMapReduceOperationIoSpec
+ : public TRawOperationIoTableSpec<TDerived>
+{
+public:
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// @brief Describes format for both input and output of mapper.
+ ///
+ /// @note `MapperFormat' is overriden by `MapperInputFormat' and `MapperOutputFormat'.
+ FLUENT_FIELD_OPTION(TFormat, MapperFormat);
+
+ /// Describes mapper input format.
+ FLUENT_FIELD_OPTION(TFormat, MapperInputFormat);
+
+ /// Describes mapper output format.
+ FLUENT_FIELD_OPTION(TFormat, MapperOutputFormat);
+
+ /// @brief Describes format for both input and output of reduce combiner.
+ ///
+ /// @note `ReduceCombinerFormat' is overriden by `ReduceCombinerInputFormat' and `ReduceCombinerOutputFormat'.
+ FLUENT_FIELD_OPTION(TFormat, ReduceCombinerFormat);
+
+ /// Describes reduce combiner input format.
+ FLUENT_FIELD_OPTION(TFormat, ReduceCombinerInputFormat);
+
+ /// Describes reduce combiner output format.
+ FLUENT_FIELD_OPTION(TFormat, ReduceCombinerOutputFormat);
+
+ /// @brief Describes format for both input and output of reducer.
+ ///
+ /// @note `ReducerFormat' is overriden by `ReducerInputFormat' and `ReducerOutputFormat'.
+ FLUENT_FIELD_OPTION(TFormat, ReducerFormat);
+
+ /// Describes reducer input format.
+ FLUENT_FIELD_OPTION(TFormat, ReducerInputFormat);
+
+ /// Describes reducer output format.
+ FLUENT_FIELD_OPTION(TFormat, ReducerOutputFormat);
+
+ /// Add direct map output table path.
+ TDerived& AddMapOutput(const TRichYPath& path);
+
+ /// Set direct map output table path no. `tableIndex`.
+ TDerived& SetMapOutput(size_t tableIndex, const TRichYPath& path);
+
+ /// Get all direct map output table paths
+ const TVector<TRichYPath>& GetMapOutputs() const;
+
+private:
+ TVector<TRichYPath> MapOutputs_;
+};
+
+///
+/// @brief Base spec of operations with input tables.
+class TOperationInputSpecBase
+{
+public:
+ template <class T, class = void>
+ struct TFormatAdder;
+
+ ///
+ /// @brief Add input table path to input path list and specify type of rows.
+ template <class T>
+ void AddInput(const TRichYPath& path);
+
+ ///
+ /// @brief Add input table path as structured paths.
+ void AddStructuredInput(TStructuredTablePath path);
+
+ ///
+ /// @brief Set input table path and type.
+ template <class T>
+ void SetInput(size_t tableIndex, const TRichYPath& path);
+
+ ///
+ /// @brief All input paths.
+ TVector<TRichYPath> Inputs_;
+
+ ///
+ /// @brief Get all input structured paths.
+ const TVector<TStructuredTablePath>& GetStructuredInputs() const;
+
+private:
+ TVector<TStructuredTablePath> StructuredInputs_;
+ friend struct TOperationIOSpecBase;
+ template <class T>
+ friend struct TOperationIOSpec;
+};
+
+///
+/// @brief Base spec of operations with output tables.
+class TOperationOutputSpecBase
+{
+public:
+ template <class T, class = void>
+ struct TFormatAdder;
+
+ ///
+ /// @brief Add output table path to output path list and specify type of rows.
+ template <class T>
+ void AddOutput(const TRichYPath& path);
+
+ ///
+ /// @brief Add output table path as structured paths.
+ void AddStructuredOutput(TStructuredTablePath path);
+
+ ///
+ /// @brief Set output table path and type.
+ template <class T>
+ void SetOutput(size_t tableIndex, const TRichYPath& path);
+
+ ///
+ /// @brief All output paths.
+ TVector<TRichYPath> Outputs_;
+
+ ///
+ /// @brief Get all output structured paths.
+ const TVector<TStructuredTablePath>& GetStructuredOutputs() const;
+
+private:
+ TVector<TStructuredTablePath> StructuredOutputs_;
+ friend struct TOperationIOSpecBase;
+ template <class T>
+ friend struct TOperationIOSpec;
+};
+
+///
+/// @brief Base spec for operations with inputs and outputs.
+struct TOperationIOSpecBase
+ : public TOperationInputSpecBase
+ , public TOperationOutputSpecBase
+{ };
+
+///
+/// @brief Base spec for operations with inputs and outputs.
+template <class TDerived>
+struct TOperationIOSpec
+ : public TOperationIOSpecBase
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ template <class T>
+ TDerived& AddInput(const TRichYPath& path);
+
+ TDerived& AddStructuredInput(TStructuredTablePath path);
+
+ template <class T>
+ TDerived& SetInput(size_t tableIndex, const TRichYPath& path);
+
+ template <class T>
+ TDerived& AddOutput(const TRichYPath& path);
+
+ TDerived& AddStructuredOutput(TStructuredTablePath path);
+
+ template <class T>
+ TDerived& SetOutput(size_t tableIndex, const TRichYPath& path);
+
+
+ // DON'T USE THESE METHODS! They are left solely for backward compatibility.
+ // These methods are the only way to do equivalent of (Add/Set)(Input/Output)<Message>
+ // but please consider using (Add/Set)(Input/Output)<TConcreteMessage>
+ // (where TConcreteMessage is some descendant of Message)
+ // because they are faster and better (see https://st.yandex-team.ru/YT-6967)
+ TDerived& AddProtobufInput_VerySlow_Deprecated(const TRichYPath& path);
+ TDerived& AddProtobufOutput_VerySlow_Deprecated(const TRichYPath& path);
+};
+
+///
+/// @brief Base spec for all operations.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/operations_options
+template <class TDerived>
+struct TOperationSpecBase
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Limit on operation execution time.
+ ///
+ /// If operation doesn't finish in time it will be aborted.
+ FLUENT_FIELD_OPTION(TDuration, TimeLimit);
+
+ /// @brief Title to be shown in web interface.
+ FLUENT_FIELD_OPTION(TString, Title);
+
+ /// @brief Pool to be used for this operation.
+ FLUENT_FIELD_OPTION(TString, Pool);
+
+ /// @brief Weight of operation.
+ ///
+ /// Coefficient defining how much resources operation gets relative to its siblings in the same pool.
+ FLUENT_FIELD_OPTION(double, Weight);
+
+ /// @breif Pool tree list that operation will use.
+ FLUENT_OPTIONAL_VECTOR_FIELD_ENCAPSULATED(TString, PoolTree);
+
+ /// How much resources can be consumed by operation.
+ FLUENT_FIELD_OPTION_ENCAPSULATED(TSchedulerResources, ResourceLimits);
+};
+
+///
+/// @brief Base spec for all operations with user jobs.
+template <class TDerived>
+struct TUserOperationSpecBase
+ : TOperationSpecBase<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ /// How many jobs can fail before operation is failed.
+ FLUENT_FIELD_OPTION(ui64, MaxFailedJobCount);
+
+ /// On any unsuccessful job completion (i.e. abortion or failure) force the whole operation to fail.
+ FLUENT_FIELD_OPTION(bool, FailOnJobRestart);
+
+ ///
+ /// @brief Table to save whole stderr of operation.
+ ///
+ /// @see https://clubs.at.yandex-team.ru/yt/1045
+ FLUENT_FIELD_OPTION(TYPath, StderrTablePath);
+
+ ///
+ /// @brief Table to save coredumps of operation.
+ ///
+ /// @see https://clubs.at.yandex-team.ru/yt/1045
+ FLUENT_FIELD_OPTION(TYPath, CoreTablePath);
+
+ ///
+ /// @brief How long should the scheduler wait for the job to be started on a node.
+ ///
+ /// When you run huge jobs that require preemption of all the other jobs on
+ /// a node, the default timeout might be insufficient and your job may be
+ /// aborted with 'waiting_timeout' reason. This is especially problematic
+ /// when you are setting 'FailOnJobRestart' option.
+ ///
+ /// @note The value must be between 10 seconds and 10 minutes.
+ FLUENT_FIELD_OPTION(TDuration, WaitingJobTimeout);
+};
+
+///
+/// @brief Class to provide information on intermediate mapreduce stream protobuf types.
+///
+/// When using protobuf format it is important to know exact types of proto messages
+/// that are used in input/output.
+///
+/// Sometimes such messages cannot be derived from job class
+/// i.e. when job class uses `NYT::TTableReader<::google::protobuf::Message>`
+/// or `NYT::TTableWriter<::google::protobuf::Message>`.
+///
+/// When using such jobs user can provide exact message type using this class.
+///
+/// @note Only input/output that relate to intermediate tables can be hinted.
+/// Input to map and output of reduce is derived from `AddInput`/`AddOutput`.
+template <class TDerived>
+struct TIntermediateTablesHintSpec
+{
+ /// Specify intermediate map output type.
+ template <class T>
+ TDerived& HintMapOutput();
+
+ /// Specify reduce combiner input.
+ template <class T>
+ TDerived& HintReduceCombinerInput();
+
+ /// Specify reduce combiner output.
+ template <class T>
+ TDerived& HintReduceCombinerOutput();
+
+ /// Specify reducer input.
+ template <class T>
+ TDerived& HintReduceInput();
+
+ ///
+ /// @brief Add output of map stage.
+ ///
+ /// Mapper output table #0 is always intermediate table that is going to be reduced later.
+ /// Rows that mapper write to tables #1, #2, ... are saved in MapOutput tables.
+ template <class T>
+ TDerived& AddMapOutput(const TRichYPath& path);
+
+ TVector<TRichYPath> MapOutputs_;
+
+ const TVector<TStructuredTablePath>& GetStructuredMapOutputs() const;
+ const TMaybe<TTableStructure>& GetIntermediateMapOutputDescription() const;
+ const TMaybe<TTableStructure>& GetIntermediateReduceCombinerInputDescription() const;
+ const TMaybe<TTableStructure>& GetIntermediateReduceCombinerOutputDescription() const;
+ const TMaybe<TTableStructure>& GetIntermediateReducerInputDescription() const;
+
+private:
+ TVector<TStructuredTablePath> StructuredMapOutputs_;
+ TMaybe<TTableStructure> IntermediateMapOutputDescription_;
+ TMaybe<TTableStructure> IntermediateReduceCombinerInputDescription_;
+ TMaybe<TTableStructure> IntermediateReduceCombinerOutputDescription_;
+ TMaybe<TTableStructure> IntermediateReducerInputDescription_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAddLocalFileOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TAddLocalFileOptions;
+ /// @endcond
+
+ ///
+ /// @brief Path by which job will see the uploaded file.
+ ///
+ /// Defaults to basename of the local path.
+ FLUENT_FIELD_OPTION(TString, PathInJob);
+
+ ///
+ /// @brief MD5 checksum of uploaded file.
+ ///
+ /// If not specified it is computed by this library.
+ /// If this argument is provided, the user can some cpu and disk IO.
+ FLUENT_FIELD_OPTION(TString, MD5CheckSum);
+
+ ///
+ /// @brief Do not put file into node cache
+ ///
+ /// @see NYT::TRichYPath::BypassArtifactCache
+ FLUENT_FIELD_OPTION(bool, BypassArtifactCache);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// @brief Binary to run job profiler on.
+enum class EProfilingBinary
+{
+ /// Profile job proxy.
+ JobProxy /* "job_proxy" */,
+
+ /// Profile user job.
+ UserJob /* "user_job" */,
+};
+
+/// @brief Type of job profiler.
+enum class EProfilerType
+{
+ /// Profile CPU usage.
+ Cpu /* "cpu" */,
+
+ /// Profile memory usage.
+ Memory /* "memory" */,
+
+ /// Profiler peak memory usage.
+ PeakMemory /* "peak_memory" */,
+};
+
+/// @brief Specifies a job profiler.
+struct TJobProfilerSpec
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TJobProfilerSpec;
+ /// @endcond
+
+ /// @brief Binary to profile.
+ FLUENT_FIELD_OPTION(EProfilingBinary, ProfilingBinary);
+
+ /// @brief Type of the profiler.
+ FLUENT_FIELD_OPTION(EProfilerType, ProfilerType);
+
+ /// @brief Probabiliy of the job being selected for profiling.
+ FLUENT_FIELD_OPTION(double, ProfilingProbability);
+
+ /// @brief For sampling profilers, sets the number of samples per second.
+ FLUENT_FIELD_OPTION(int, SamplingFrequency);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Spec of user job.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/operations_options#user_script_options
+struct TUserJobSpec
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TUserJobSpec;
+ /// @endcond
+
+ ///
+ /// @brief Specify a local file to upload to Cypress and prepare for use in job.
+ TSelf& AddLocalFile(const TLocalFilePath& path, const TAddLocalFileOptions& options = TAddLocalFileOptions());
+
+ ///
+ /// @brief Get the list of all added local files.
+ TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> GetLocalFiles() const;
+
+ /// @brief Paths to files in Cypress to use in job.
+ FLUENT_VECTOR_FIELD(TRichYPath, File);
+
+ ///
+ /// @brief MemoryLimit specifies how much memory job process can use.
+ ///
+ /// @note
+ /// If job uses tmpfs (check @ref NYT::TOperationOptions::MountSandboxInTmpfs)
+ /// YT computes its memory usage as total of:
+ /// - memory usage of job process itself (including mapped files);
+ /// - total size of tmpfs used by this job.
+ ///
+ /// @note
+ /// When @ref NYT::TOperationOptions::MountSandboxInTmpfs is enabled library will compute
+ /// total size of all files used by this job and add this total size to MemoryLimit.
+ /// Thus you shouldn't include size of your files (e.g. binary file) into MemoryLimit.
+ ///
+ /// @note
+ /// Final memory memory_limit passed to YT is calculated as follows:
+ ///
+ /// @note
+ /// ```
+ /// memory_limit = MemoryLimit + <total-size-of-used-files> + ExtraTmpfsSize
+ /// ```
+ ///
+ /// @see NYT::TUserJobSpec::ExtraTmpfsSize
+ FLUENT_FIELD_OPTION(i64, MemoryLimit);
+
+ ///
+ /// @brief Size of data that is going to be written to tmpfs.
+ ///
+ /// This option should be used if job writes data to tmpfs.
+ ///
+ /// ExtraTmpfsSize should not include size of files specified with
+ /// @ref NYT::TUserJobSpec::AddLocalFile or @ref NYT::TUserJobSpec::AddFile
+ /// These files are copied to tmpfs automatically and their total size
+ /// is computed automatically.
+ ///
+ /// @see NYT::TOperationOptions::MountSandboxInTmpfs
+ /// @see NYT::TUserJobSpec::MemoryLimit
+ FLUENT_FIELD_OPTION(i64, ExtraTmpfsSize);
+
+ ///
+ /// @brief Maximum number of CPU cores for a single job to use.
+ FLUENT_FIELD_OPTION(double, CpuLimit);
+
+ ///
+ /// @brief Fraction of @ref NYT::TUserJobSpec::MemoryLimit that job gets at start.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/operations_options#memory_reserve_factor
+ FLUENT_FIELD_OPTION(double, MemoryReserveFactor);
+
+ ///
+ /// @brief Local path to executable to be used inside jobs.
+ ////
+ /// Provided executable must use C++ YT API library (this library)
+ /// and implement job class that is going to be used.
+ ///
+ /// This option might be useful if we want to start operation from nonlinux machines
+ /// (in that case we use `JobBinary` to provide path to the same program compiled for linux).
+ /// Other example of using this option is uploading executable to cypress in advance
+ /// and save the time required to upload current executable to cache.
+ /// `md5` argument can be used to save cpu time and disk IO when binary MD5 checksum is known.
+ /// When argument is not provided library will compute it itself.
+ TUserJobSpec& JobBinaryLocalPath(TString path, TMaybe<TString> md5 = Nothing());
+
+ ///
+ /// @brief Cypress path to executable to be used inside jobs.
+ TUserJobSpec& JobBinaryCypressPath(TString path, TMaybe<TTransactionId> transactionId = Nothing());
+
+ ///
+ /// @brief String that will be prepended to the command.
+ ///
+ /// This option overrides @ref NYT::TOperationOptions::JobCommandPrefix.
+ FLUENT_FIELD(TString, JobCommandPrefix);
+
+ ///
+ /// @brief String that will be appended to the command.
+ ///
+ /// This option overrides @ref NYT::TOperationOptions::JobCommandSuffix.
+ FLUENT_FIELD(TString, JobCommandSuffix);
+
+ ///
+ /// @brief Map of environment variables that will be set for jobs.
+ FLUENT_MAP_FIELD(TString, TString, Environment);
+
+ ///
+ /// @brief Limit for all files inside job sandbox (in bytes).
+ FLUENT_FIELD_OPTION(ui64, DiskSpaceLimit);
+
+ ///
+ /// @brief Number of ports reserved for the job (passed through environment in YT_PORT_0, YT_PORT_1, ...).
+ FLUENT_FIELD_OPTION(ui16, PortCount);
+
+ ///
+ /// @brief Network project used to isolate job network.
+ FLUENT_FIELD_OPTION(TString, NetworkProject);
+
+ ///
+ /// @brief Limit on job execution time.
+ ///
+ /// Jobs that exceed this limit will be considered failed.
+ FLUENT_FIELD_OPTION(TDuration, JobTimeLimit);
+
+ ///
+ /// @brief Get job binary config.
+ const TJobBinaryConfig& GetJobBinary() const;
+
+ ///
+ /// @brief List of profilers to run.
+ FLUENT_VECTOR_FIELD(TJobProfilerSpec, JobProfiler);
+
+private:
+ TVector<std::tuple<TLocalFilePath, TAddLocalFileOptions>> LocalFiles_;
+ TJobBinaryConfig JobBinary_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Spec of Map operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/map
+template <typename TDerived>
+struct TMapOperationSpecBase
+ : public TUserOperationSpecBase<TDerived>
+ , public TWithAutoMergeSpec<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Spec of mapper job.
+ FLUENT_FIELD(TUserJobSpec, MapperSpec);
+
+ ///
+ /// @brief Whether to guarantee the order of rows passed to mapper matches the order in the table.
+ ///
+ /// When `Ordered' is false (by default), there is no guaranties about order of reading rows.
+ /// In this case mapper might work slightly faster because row delivered from fast node can be processed YT waits
+ /// response from slow nodes.
+ /// When `Ordered' is true, rows will come in order in which they are stored in input tables.
+ FLUENT_FIELD_OPTION(bool, Ordered);
+
+ ///
+ /// @brief Recommended number of jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TMapOperationSpecBase::DataSizePerJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui32, JobCount);
+
+ ///
+ /// @brief Recommended of data size for each job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TMapOperationSpecBase::JobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerJob);
+};
+
+///
+/// @brief Spec of Map operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/map
+struct TMapOperationSpec
+ : public TMapOperationSpecBase<TMapOperationSpec>
+ , public TOperationIOSpec<TMapOperationSpec>
+ , public TUserJobFormatHintsBase<TMapOperationSpec>
+{ };
+
+///
+/// @brief Spec of raw Map operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/map
+struct TRawMapOperationSpec
+ : public TMapOperationSpecBase<TRawMapOperationSpec>
+ , public TSimpleRawOperationIoSpec<TRawMapOperationSpec>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Spec of Reduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+template <typename TDerived>
+struct TReduceOperationSpecBase
+ : public TUserOperationSpecBase<TDerived>
+ , public TWithAutoMergeSpec<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Spec of reduce job.
+ FLUENT_FIELD(TUserJobSpec, ReducerSpec);
+
+ ///
+ /// @brief Columns to sort rows by (must include `ReduceBy` as prefix).
+ FLUENT_FIELD(TSortColumns, SortBy);
+
+ ///
+ /// @brief Columns to group rows by.
+ FLUENT_FIELD(TSortColumns, ReduceBy);
+
+ ///
+ /// @brief Columns to join foreign tables by (must be prefix of `ReduceBy`).
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/reduce#foreign_tables
+ FLUENT_FIELD_OPTION(TSortColumns, JoinBy);
+
+ ///
+ /// @brief Guarantee to feed all rows with same `ReduceBy` columns to a single job (`true` by default).
+ FLUENT_FIELD_OPTION(bool, EnableKeyGuarantee);
+
+ ///
+ /// @brief Recommended number of jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TReduceOperationSpecBase::DataSizePerJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui32, JobCount);
+
+ ///
+ /// @brief Recommended of data size for each job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TReduceOperationSpecBase::JobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerJob);
+};
+
+///
+/// @brief Spec of Reduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+struct TReduceOperationSpec
+ : public TReduceOperationSpecBase<TReduceOperationSpec>
+ , public TOperationIOSpec<TReduceOperationSpec>
+ , public TUserJobFormatHintsBase<TReduceOperationSpec>
+{ };
+
+///
+/// @brief Spec of raw Reduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+struct TRawReduceOperationSpec
+ : public TReduceOperationSpecBase<TRawReduceOperationSpec>
+ , public TSimpleRawOperationIoSpec<TRawReduceOperationSpec>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Spec of JoinReduce operation.
+///
+/// @deprecated Instead the user should run a reduce operation
+/// with @ref NYT::TReduceOperationSpec::EnableKeyGuarantee set to `false`.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce#foreign_tables
+template <typename TDerived>
+struct TJoinReduceOperationSpecBase
+ : public TUserOperationSpecBase<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Spec of reduce job.
+ FLUENT_FIELD(TUserJobSpec, ReducerSpec);
+
+ ///
+ /// @brief Columns to join foreign tables by (must be prefix of `ReduceBy`).
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/reduce#foreign_tables
+ FLUENT_FIELD(TSortColumns, JoinBy);
+
+ ///
+ /// @brief Recommended number of jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TJoinReduceOperationSpecBase::DataSizePerJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui32, JobCount);
+
+ ///
+ /// @brief Recommended of data size for each job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TJoinReduceOperationSpecBase::JobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerJob);
+};
+
+///
+/// @brief Spec of JoinReduce operation.
+///
+/// @deprecated Instead the user should run a reduce operation
+/// with @ref NYT::TReduceOperationSpec::EnableKeyGuarantee set to `false`.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce#foreign_tables
+struct TJoinReduceOperationSpec
+ : public TJoinReduceOperationSpecBase<TJoinReduceOperationSpec>
+ , public TOperationIOSpec<TJoinReduceOperationSpec>
+ , public TUserJobFormatHintsBase<TJoinReduceOperationSpec>
+{ };
+
+///
+/// @brief Spec of raw JoinReduce operation.
+///
+/// @deprecated Instead the user should run a reduce operation
+/// with @ref NYT::TReduceOperationSpec::EnableKeyGuarantee set to `false`.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/reduce#foreign_tables
+struct TRawJoinReduceOperationSpec
+ : public TJoinReduceOperationSpecBase<TRawJoinReduceOperationSpec>
+ , public TSimpleRawOperationIoSpec<TRawJoinReduceOperationSpec>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Spec of MapReduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+template <typename TDerived>
+struct TMapReduceOperationSpecBase
+ : public TUserOperationSpecBase<TDerived>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TDerived;
+ /// @endcond
+
+ ///
+ /// @brief Spec of map job.
+ FLUENT_FIELD(TUserJobSpec, MapperSpec);
+
+ ///
+ /// @brief Spec of reduce job.
+ FLUENT_FIELD(TUserJobSpec, ReducerSpec);
+
+ ///
+ /// @brief Spec of reduce combiner.
+ FLUENT_FIELD(TUserJobSpec, ReduceCombinerSpec);
+
+ ///
+ /// @brief Columns to sort rows by (must include `ReduceBy` as prefix).
+ FLUENT_FIELD(TSortColumns, SortBy);
+
+ ///
+ /// @brief Columns to group rows by.
+ FLUENT_FIELD(TSortColumns, ReduceBy);
+
+ ///
+ /// @brief Recommended number of map jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TMapReduceOperationSpecBase::DataSizePerMapJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui32, MapJobCount);
+
+ ///
+ /// @brief Recommended of data size for each map job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TMapReduceOperationSpecBase::MapJobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerMapJob);
+
+ ///
+ /// @brief Recommended number of intermediate data partitions.
+ FLUENT_FIELD_OPTION(ui64, PartitionCount);
+
+ ///
+ /// @brief Recommended size of intermediate data partitions.
+ FLUENT_FIELD_OPTION(ui64, PartitionDataSize);
+
+ ///
+ /// @brief Account to use for intermediate data.
+ FLUENT_FIELD_OPTION(TString, IntermediateDataAccount);
+
+ ///
+ /// @brief Replication factor for intermediate data (1 by default).
+ FLUENT_FIELD_OPTION(ui64, IntermediateDataReplicationFactor);
+
+ ///
+ /// @brief Recommended size of data to be passed to a single reduce combiner.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerSortJob);
+
+ ///
+ /// @brief Whether to guarantee the order of rows passed to mapper matches the order in the table.
+ ///
+ /// @see @ref NYT::TMapOperationSpec::Ordered for more info.
+ FLUENT_FIELD_OPTION(bool, Ordered);
+
+ ///
+ /// @brief Guarantee to run reduce combiner before reducer.
+ FLUENT_FIELD_OPTION(bool, ForceReduceCombiners);
+};
+
+///
+/// @brief Spec of MapReduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+struct TMapReduceOperationSpec
+ : public TMapReduceOperationSpecBase<TMapReduceOperationSpec>
+ , public TOperationIOSpec<TMapReduceOperationSpec>
+ , public TIntermediateTablesHintSpec<TMapReduceOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TMapReduceOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Format hints for mapper.
+ FLUENT_FIELD_DEFAULT(TUserJobFormatHints, MapperFormatHints, TUserJobFormatHints());
+
+ ///
+ /// @brief Format hints for reducer.
+ FLUENT_FIELD_DEFAULT(TUserJobFormatHints, ReducerFormatHints, TUserJobFormatHints());
+
+ ///
+ /// @brief Format hints for reduce combiner.
+ FLUENT_FIELD_DEFAULT(TUserJobFormatHints, ReduceCombinerFormatHints, TUserJobFormatHints());
+};
+
+///
+/// @brief Spec of raw MapReduce operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+struct TRawMapReduceOperationSpec
+ : public TMapReduceOperationSpecBase<TRawMapReduceOperationSpec>
+ , public TRawMapReduceOperationIoSpec<TRawMapReduceOperationSpec>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Schema inference mode.
+///
+/// @see https://yt.yandex-team.ru/docs/description/storage/static_schema.html#schema_inference
+enum class ESchemaInferenceMode : int
+{
+ FromInput /* "from_input" */,
+ FromOutput /* "from_output" */,
+ Auto /* "auto" */,
+};
+
+///
+/// @brief Spec of Sort operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/sort
+struct TSortOperationSpec
+ : TOperationSpecBase<TSortOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TSortOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Paths to input tables.
+ FLUENT_VECTOR_FIELD(TRichYPath, Input);
+
+ ///
+ /// @brief Path to output table.
+ FLUENT_FIELD(TRichYPath, Output);
+
+ ///
+ /// @brief Columns to sort table by.
+ FLUENT_FIELD(TSortColumns, SortBy);
+
+ ///
+ /// @brief Recommended number of intermediate data partitions.
+ FLUENT_FIELD_OPTION(ui64, PartitionCount);
+
+ ///
+ /// @brief Recommended size of intermediate data partitions.
+ FLUENT_FIELD_OPTION(ui64, PartitionDataSize);
+
+ ///
+ /// @brief Recommended number of partition jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TSortOperationSpec::DataSizePerPartitionJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, PartitionJobCount);
+
+ ///
+ /// @brief Recommended of data size for each partition job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TSortOperationSpec::PartitionJobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerPartitionJob);
+
+ ///
+ /// @brief Inference mode for output table schema.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/static_schema.html#schema_inference
+ FLUENT_FIELD_OPTION(ESchemaInferenceMode, SchemaInferenceMode);
+
+ ///
+ /// @brief Account to use for intermediate data.
+ FLUENT_FIELD_OPTION(TString, IntermediateDataAccount);
+
+ ///
+ /// @brief Replication factor for intermediate data (1 by default).
+ FLUENT_FIELD_OPTION(ui64, IntermediateDataReplicationFactor);
+};
+
+
+///
+/// @brief Merge mode.
+enum EMergeMode : int
+{
+ MM_UNORDERED /* "unordered" */,
+ MM_ORDERED /* "ordered" */,
+ MM_SORTED /* "sorted" */,
+};
+
+///
+/// @brief Spec of Merge operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/merge
+struct TMergeOperationSpec
+ : TOperationSpecBase<TMergeOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TMergeOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Paths to input tables.
+ FLUENT_VECTOR_FIELD(TRichYPath, Input);
+
+ ///
+ /// @brief Path to output table.
+ FLUENT_FIELD(TRichYPath, Output);
+
+ ///
+ /// @brief Columns by which to merge (for @ref NYT::EMergeMode::MM_SORTED).
+ FLUENT_FIELD(TSortColumns, MergeBy);
+
+ ///
+ /// @brief Merge mode.
+ FLUENT_FIELD_DEFAULT(EMergeMode, Mode, MM_UNORDERED);
+
+ ///
+ /// @brief Combine output chunks to larger ones.
+ FLUENT_FIELD_DEFAULT(bool, CombineChunks, false);
+
+ ///
+ /// @brief Guarantee that all input chunks will be read.
+ FLUENT_FIELD_DEFAULT(bool, ForceTransform, false);
+
+ ///
+ /// @brief Recommended number of jobs to run.
+ ///
+ /// `JobCount' has higher priority than @ref NYT::TMergeOperationSpec::DataSizePerJob.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui32, JobCount);
+
+ ///
+ /// @brief Recommended of data size for each job.
+ ///
+ /// `DataSizePerJob` has lower priority that @ref NYT::TMergeOperationSpec::JobCount.
+ /// This option only provide a recommendation and may be ignored if conflicting with YT internal limits.
+ FLUENT_FIELD_OPTION(ui64, DataSizePerJob);
+
+ ///
+ /// @brief Inference mode for output table schema.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/static_schema.html#schema_inference
+ FLUENT_FIELD_OPTION(ESchemaInferenceMode, SchemaInferenceMode);
+};
+
+///
+/// @brief Spec of Erase operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/erase
+struct TEraseOperationSpec
+ : TOperationSpecBase<TEraseOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TEraseOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Which table (or row range) to erase.
+ FLUENT_FIELD(TRichYPath, TablePath);
+
+ ///
+ /// Combine output chunks to larger ones.
+ FLUENT_FIELD_DEFAULT(bool, CombineChunks, false);
+
+ ///
+ /// @brief Inference mode for output table schema.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/static_schema.html#schema_inference
+ FLUENT_FIELD_OPTION(ESchemaInferenceMode, SchemaInferenceMode);
+};
+
+///
+/// @brief Spec of RemoteCopy operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/remote_copy
+struct TRemoteCopyOperationSpec
+ : TOperationSpecBase<TRemoteCopyOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TRemoteCopyOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Source cluster name.
+ FLUENT_FIELD(TString, ClusterName);
+
+ ///
+ /// @brief Network to use for copy (all remote cluster nodes must have it configured).
+ FLUENT_FIELD_OPTION(TString, NetworkName);
+
+ ///
+ /// @brief Paths to input tables.
+ FLUENT_VECTOR_FIELD(TRichYPath, Input);
+
+ ///
+ /// @brief Path to output table.
+ FLUENT_FIELD(TRichYPath, Output);
+
+ ///
+ /// @brief Inference mode for output table schema.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/storage/static_schema.html#schema_inference
+ FLUENT_FIELD_OPTION(ESchemaInferenceMode, SchemaInferenceMode);
+
+ ///
+ /// @brief Copy user attributes from input to output table (allowed only for single input table).
+ FLUENT_FIELD_DEFAULT(bool, CopyAttributes, false);
+
+ ///
+ /// @brief Names of user attributes to copy from input to output table.
+ ///
+ /// @note To make this option make sense set @ref NYT::TRemoteCopyOperationSpec::CopyAttributes to `true`.
+ FLUENT_VECTOR_FIELD(TString, AttributeKey);
+
+private:
+
+ ///
+ /// @brief Config for remote cluster connection.
+ FLUENT_FIELD_OPTION(TNode, ClusterConnection);
+};
+
+class IVanillaJobBase;
+
+///
+/// @brief Task of Vanilla operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/vanilla
+struct TVanillaTask
+ : public TOperationOutputSpecBase
+ , public TUserJobOutputFormatHintsBase<TVanillaTask>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TVanillaTask;
+ /// @endcond
+
+ ///
+ /// @brief Add output table path and specify the task output type (i.e. TMyProtoMessage).
+ template <class T>
+ TSelf& AddOutput(const TRichYPath& path);
+
+ ///
+ /// @brief Add output table path as structured path.
+ TSelf& AddStructuredOutput(TStructuredTablePath path);
+
+ ///
+ /// @brief Set output table path and specify the task output type (i.e. TMyProtoMessage).
+ template <class T>
+ TSelf& SetOutput(size_t tableIndex, const TRichYPath& path);
+
+ ///
+ /// @brief Task name.
+ FLUENT_FIELD(TString, Name);
+
+ ///
+ /// @brief Job to be executed in this task.
+ FLUENT_FIELD(::TIntrusivePtr<IVanillaJobBase>, Job);
+
+ ///
+ /// @brief User job spec.
+ FLUENT_FIELD(TUserJobSpec, Spec);
+
+ ///
+ /// @brief Number of jobs to run and wait for successful completion.
+ ///
+ /// @note If @ref NYT::TUserOperationSpecBase::FailOnJobRestart is `false`, a failed job will be restarted
+ /// and will not count in this amount.
+ FLUENT_FIELD(ui64, JobCount);
+
+ ///
+ /// @brief Network project name.
+ FLUENT_FIELD(TMaybe<TString>, NetworkProject);
+
+};
+
+///
+/// @brief Spec of Vanilla operation.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/vanilla
+struct TVanillaOperationSpec
+ : TUserOperationSpecBase<TVanillaOperationSpec>
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TVanillaOperationSpec;
+ /// @endcond
+
+ ///
+ /// @brief Description of tasks to run in this operation.
+ FLUENT_VECTOR_FIELD(TVanillaTask, Task);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Options for @ref NYT::IOperationClient::Map and other operation start commands.
+struct TOperationOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TOperationOptions;
+ /// @endcond
+
+ ///
+ /// @brief Additional field to put to operation spec.
+ FLUENT_FIELD_OPTION(TNode, Spec);
+
+ ///
+ /// @brief Start operation mode.
+ enum class EStartOperationMode : int
+ {
+ ///
+ /// @brief Prepare operation asynchronously. Call IOperation::Start() to start operation.
+ AsyncPrepare,
+
+ ///
+ /// @brief Prepare and start operation asynchronously. Don't wait for operation completion.
+ AsyncStart,
+
+ ///
+ /// @brief Prepare and start operation synchronously. Don't wait for operation completion.
+ SyncStart,
+
+ ///
+ /// @brief Prepare, start and wait for operation completion synchronously.
+ SyncWait,
+ };
+
+ ///
+ /// @brief Start operation mode.
+ FLUENT_FIELD_DEFAULT(EStartOperationMode, StartOperationMode, EStartOperationMode::SyncWait);
+
+ ///
+ /// @brief Wait for operation finish synchronously.
+ ///
+ /// @deprecated Use StartOperationMode() instead.
+ TSelf& Wait(bool value) {
+ StartOperationMode_ = value ? EStartOperationMode::SyncWait : EStartOperationMode::SyncStart;
+ return static_cast<TSelf&>(*this);
+ }
+
+ ///
+ ///
+ /// @brief Use format from table attribute (for YAMR-like format).
+ ///
+ /// @deprecated
+ FLUENT_FIELD_DEFAULT(bool, UseTableFormats, false);
+
+ ///
+ /// @brief Prefix for bash command running the jobs.
+ ///
+ /// Can be overridden for the specific job type in the @ref NYT::TUserJobSpec.
+ FLUENT_FIELD(TString, JobCommandPrefix);
+
+ ///
+ /// @brief Suffix for bash command running the jobs.
+ ///
+ /// Can be overridden for the specific job type in the @ref NYT::TUserJobSpec.
+ FLUENT_FIELD(TString, JobCommandSuffix);
+
+ ///
+ /// @brief Put all files required by the job into tmpfs.
+ ///
+ /// This option can be set globally using @ref NYT::TConfig::MountSandboxInTmpfs.
+ /// @see https://yt.yandex-team.ru/docs/problems/woodpeckers
+ FLUENT_FIELD_DEFAULT(bool, MountSandboxInTmpfs, false);
+
+ ///
+ /// @brief Path to directory to store temporary files.
+ FLUENT_FIELD_OPTION(TString, FileStorage);
+
+ ///
+ /// @brief Expiration timeout for uploaded files.
+ FLUENT_FIELD_OPTION(TDuration, FileExpirationTimeout);
+
+ ///
+ /// @brief Info to be passed securely to the job.
+ FLUENT_FIELD_OPTION(TNode, SecureVault);
+
+ ///
+ /// @brief File cache mode.
+ enum class EFileCacheMode : int
+ {
+ ///
+ /// @brief Use YT API commands "get_file_from_cache" and "put_file_to_cache".
+ ApiCommandBased,
+
+ ///
+ /// @brief Upload files to random paths inside @ref NYT::TOperationOptions::FileStorage without caching.
+ CachelessRandomPathUpload,
+ };
+
+ ///
+ /// @brief File cache mode.
+ FLUENT_FIELD_DEFAULT(EFileCacheMode, FileCacheMode, EFileCacheMode::ApiCommandBased);
+
+ ///
+ /// @brief Id of transaction within which all Cypress file storage entries will be checked/created.
+ ///
+ /// By default, the root transaction is used.
+ ///
+ /// @note Set a specific transaction only if you
+ /// 1. specify non-default file storage path in @ref NYT::TOperationOptions::FileStorage or in @ref NYT::TConfig::RemoteTempFilesDirectory.
+ /// 2. use `CachelessRandomPathUpload` caching mode (@ref NYT::TOperationOptions::FileCacheMode).
+ FLUENT_FIELD(TTransactionId, FileStorageTransactionId);
+
+ ///
+ /// @brief Ensure stderr and core tables exist before starting operation.
+ ///
+ /// If set to `false`, it is user's responsibility to ensure these tables exist.
+ FLUENT_FIELD_DEFAULT(bool, CreateDebugOutputTables, true);
+
+ ///
+ /// @brief Ensure output tables exist before starting operation.
+ ///
+ /// If set to `false`, it is user's responsibility to ensure output tables exist.
+ FLUENT_FIELD_DEFAULT(bool, CreateOutputTables, true);
+
+ ///
+ /// @brief Try to infer schema of inexistent table from the type of written rows.
+ ///
+ /// @note Default values for this option may differ depending on the row type.
+ /// For protobuf it's currently `false` by default.
+ FLUENT_FIELD_OPTION(bool, InferOutputSchema);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Get operation secure vault (specified in @ref NYT::TOperationOptions::SecureVault) inside a job.
+const TNode& GetJobSecureVault();
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Context passed to @ref NYT::IRawJob::Do.
+class TRawJobContext
+{
+public:
+ explicit TRawJobContext(size_t outputTableCount);
+
+ ///
+ /// @brief Get file corresponding to input stream.
+ const TFile& GetInputFile() const;
+
+ ///
+ /// @brief Get files corresponding to output streams.
+ const TVector<TFile>& GetOutputFileList() const;
+
+private:
+ TFile InputFile_;
+ TVector<TFile> OutputFileList_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface for classes that can be Saved/Loaded (to be used with @ref Y_SAVELOAD_JOB).
+class ISerializableForJob
+{
+public:
+ virtual ~ISerializableForJob() = default;
+
+ ///
+ /// @brief Dump state to output stream to be restored in job.
+ virtual void Save(IOutputStream& stream) const = 0;
+
+ ///
+ /// @brief Load state from a stream.
+ virtual void Load(IInputStream& stream) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Provider of information about operation inputs/outputs during @ref NYT::IJob::PrepareOperation.
+class IOperationPreparationContext
+{
+public:
+ virtual ~IOperationPreparationContext() = default;
+
+ /// @brief Get the number of input tables.
+ virtual int GetInputCount() const = 0;
+
+ /// @brief Get the number of output tables.
+ virtual int GetOutputCount() const = 0;
+
+ /// @brief Get the schema of input table no. `index`.
+ virtual const TTableSchema& GetInputSchema(int index) const = 0;
+
+ /// @brief Get all the input table schemas.
+ virtual const TVector<TTableSchema>& GetInputSchemas() const = 0;
+
+ /// @brief Path to the input table if available (`Nothing()` for intermediate tables).
+ virtual TMaybe<TYPath> GetInputPath(int index) const = 0;
+
+ /// @brief Path to the output table if available (`Nothing()` for intermediate tables).
+ virtual TMaybe<TYPath> GetOutputPath(int index) const = 0;
+};
+
+///
+/// @brief Fluent builder class for @ref NYT::IJob::PrepareOperation.
+///
+/// @note Method calls are supposed to be chained.
+class TJobOperationPreparer
+{
+public:
+
+ ///
+ /// @brief Group of input tables that allows to specify properties on all of them at once.
+ ///
+ /// The instances are created with @ref NYT::TJobOperationPreparer::BeginInputGroup, not directly.
+ class TInputGroup
+ {
+ public:
+ TInputGroup(TJobOperationPreparer& preparer, TVector<int> indices);
+
+ /// @brief Specify the type of input rows.
+ template <typename TRow>
+ TInputGroup& Description();
+
+ /// @brief Specify renaming of input columns.
+ TInputGroup& ColumnRenaming(const THashMap<TString, TString>& renaming);
+
+ /// @brief Specify what input columns to send to job
+ ///
+ /// @note Filter is applied before renaming, so it must specify original column names.
+ TInputGroup& ColumnFilter(const TVector<TString>& columns);
+
+ /// @brief Finish describing the input group.
+ TJobOperationPreparer& EndInputGroup();
+
+ private:
+ TJobOperationPreparer& Preparer_;
+ TVector<int> Indices_;
+ };
+
+ ///
+ /// @brief Group of output tables that allows to specify properties on all of them at once.
+ ///
+ /// The instances are created with @ref NYT::TJobOperationPreparer::BeginOutputGroup, not directly.
+ class TOutputGroup
+ {
+ public:
+ TOutputGroup(TJobOperationPreparer& preparer, TVector<int> indices);
+
+ /// @brief Specify the type of output rows.
+ ///
+ /// @tparam TRow type of output rows from tables of this group.
+ /// @param inferSchema Infer schema from `TRow` and specify it for these output tables.
+ template <typename TRow>
+ TOutputGroup& Description(bool inferSchema = true);
+
+ /// @brief Specify schema for these tables.
+ TOutputGroup& Schema(const TTableSchema& schema);
+
+ /// @brief Specify that all the the tables in this group are unschematized.
+ ///
+ /// It is equivalent of `.Schema(TTableSchema().Strict(false)`.
+ TOutputGroup& NoSchema();
+
+ /// @brief Finish describing the output group.
+ TJobOperationPreparer& EndOutputGroup();
+
+ private:
+ TJobOperationPreparer& Preparer_;
+ TVector<int> Indices_;
+ };
+
+public:
+ explicit TJobOperationPreparer(const IOperationPreparationContext& context);
+
+ /// @brief Begin input group consisting of tables with indices `[begin, end)`.
+ ///
+ /// @param begin First index.
+ /// @param end Index after the last one.
+ TInputGroup BeginInputGroup(int begin, int end);
+
+ /// @brief Begin input group consisting of tables with indices from `indices`.
+ ///
+ /// @tparam TCont Container with integers. Must support `std::begin` and `std::end` functions.
+ /// @param indices Indices of tables to include in the group.
+ template <typename TCont>
+ TInputGroup BeginInputGroup(const TCont& indices);
+
+ /// @brief Begin output group consisting of tables with indices `[begin, end)`.
+ ///
+ /// @param begin First index.
+ /// @param end Index after the last one.
+ TOutputGroup BeginOutputGroup(int begin, int end);
+
+ /// @brief Begin input group consisting of tables with indices from `indices`.
+ ///
+ /// @tparam TCont Container with integers. Must support `std::begin` and `std::end` functions.
+ /// @param indices Indices of tables to include in the group.
+ template <typename TCont>
+ TOutputGroup BeginOutputGroup(const TCont& indices);
+
+ /// @brief Specify the schema for output table no `tableIndex`.
+ ///
+ /// @note All the output schemas must be specified either with this method, `NoOutputSchema` or `OutputDescription` with `inferSchema == true`
+ TJobOperationPreparer& OutputSchema(int tableIndex, TTableSchema schema);
+
+ /// @brief Mark the output table no. `tableIndex` as unschematized.
+ TJobOperationPreparer& NoOutputSchema(int tableIndex);
+
+ /// @brief Specify renaming of input columns for table no. `tableIndex`.
+ TJobOperationPreparer& InputColumnRenaming(int tableIndex, const THashMap<TString, TString>& renaming);
+
+ /// @brief Specify what input columns of table no. `tableIndex` to send to job
+ ///
+ /// @note Filter is applied before renaming, so it must specify original column names.
+ TJobOperationPreparer& InputColumnFilter(int tableIndex, const TVector<TString>& columns);
+
+ /// @brief Specify the type of input rows for table no. `tableIndex`.
+ ///
+ /// @tparam TRow type of input rows.
+ template <typename TRow>
+ TJobOperationPreparer& InputDescription(int tableIndex);
+
+ /// @brief Specify the type of output rows for table no. `tableIndex`.
+ ///
+ /// @tparam TRow type of output rows.
+ /// @param inferSchema Infer schema from `TRow` and specify it for the output tables.
+ template <typename TRow>
+ TJobOperationPreparer& OutputDescription(int tableIndex, bool inferSchema = true);
+
+ /// @brief Set type of output rows for table no. `tableIndex` to TNode
+ ///
+ /// @note Set schema via `OutputSchema` if needed
+ TJobOperationPreparer& NodeOutput(int tableIndex);
+
+ /// @brief Specify input format hints.
+ ///
+ /// These hints have lower priority than ones specified in spec.
+ TJobOperationPreparer& InputFormatHints(TFormatHints hints);
+
+ /// @brief Specify output format hints.
+ ///
+ /// These hints have lower priority than ones specified in spec.
+ TJobOperationPreparer& OutputFormatHints(TFormatHints hints);
+
+ /// @brief Specify format hints.
+ ///
+ /// These hints have lower priority than ones specified in spec.
+ TJobOperationPreparer& FormatHints(TUserJobFormatHints newFormatHints);
+
+ /// @name "Private" members
+ /// The following methods should not be used by clients in @ref NYT::IJob::PrepareOperation
+ ///@{
+
+ /// @brief Finish the building process.
+ void Finish();
+
+ /// @brief Get output table schemas as specified by the user.
+ TVector<TTableSchema> GetOutputSchemas();
+
+ /// @brief Get input column renamings as specified by the user.
+ const TVector<THashMap<TString, TString>>& GetInputColumnRenamings() const;
+
+ /// @brief Get input column filters as specified by the user.
+ const TVector<TMaybe<TVector<TString>>>& GetInputColumnFilters() const;
+
+ /// @brief Get input column descriptions as specified by the user.
+ const TVector<TMaybe<TTableStructure>>& GetInputDescriptions() const;
+
+ /// @brief Get output column descriptions as specified by the user.
+ const TVector<TMaybe<TTableStructure>>& GetOutputDescriptions() const;
+
+ /// @brief Get format hints as specified by the user.
+ const TUserJobFormatHints& GetFormatHints() const;
+
+ ///@}
+private:
+
+ /// @brief Validate that schema for output table no. `tableIndex` has not been set yet.
+ void ValidateMissingOutputSchema(int tableIndex) const;
+
+ /// @brief Validate that description for input table no. `tableIndex` has not been set yet.
+ void ValidateMissingInputDescription(int tableIndex) const;
+
+ /// @brief Validate that description for output table no. `tableIndex` has not been set yet.
+ void ValidateMissingOutputDescription(int tableIndex) const;
+
+ /// @brief Validate that `tableIndex` is in correct range for input table indices.
+ ///
+ /// @param message Message to add to the exception in case of violation.
+ void ValidateInputTableIndex(int tableIndex, TStringBuf message) const;
+
+ /// @brief Validate that `tableIndex` is in correct range for output table indices.
+ ///
+ /// @param message Message to add to the exception in case of violation.
+ void ValidateOutputTableIndex(int tableIndex, TStringBuf message) const;
+
+ /// @brief Validate that all the output schemas has been set.
+ void FinallyValidate() const;
+
+ static TTableSchema EmptyNonstrictSchema();
+
+private:
+ const IOperationPreparationContext& Context_;
+
+ TVector<TMaybe<TTableSchema>> OutputSchemas_;
+ TVector<THashMap<TString, TString>> InputColumnRenamings_;
+ TVector<TMaybe<TVector<TString>>> InputColumnFilters_;
+ TVector<TMaybe<TTableStructure>> InputTableDescriptions_;
+ TVector<TMaybe<TTableStructure>> OutputTableDescriptions_;
+ TUserJobFormatHints FormatHints_ = {};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface for all user jobs.
+class IJob
+ : public TThrRefBase
+{
+public:
+
+ ///
+ /// @brief Type of job.
+ enum EType
+ {
+ Mapper,
+ Reducer,
+ ReducerAggregator,
+ RawJob,
+ VanillaJob,
+ };
+
+ ///
+ /// @brief Save job state to stream to be restored on cluster nodes.
+ virtual void Save(IOutputStream& stream) const
+ {
+ Y_UNUSED(stream);
+ }
+
+ ///
+ /// @brief Restore job state from a stream.
+ virtual void Load(IInputStream& stream)
+ {
+ Y_UNUSED(stream);
+ }
+
+ ///
+ /// @brief Get operation secure vault (specified in @ref NYT::TOperationOptions::SecureVault) inside a job.
+ const TNode& SecureVault() const
+ {
+ return GetJobSecureVault();
+ }
+
+ ///
+ /// @brief Get number of output tables.
+ i64 GetOutputTableCount() const
+ {
+ Y_VERIFY(NDetail::OutputTableCount > 0);
+
+ return NDetail::OutputTableCount;
+ }
+
+ ///
+ /// @brief Method allowing user to control some properties of input and output tables and formats.
+ ///
+ /// User can override this method in their job class to:
+ /// - specify output table schemas.
+ /// The most natural way is usually through @ref NYT::TJobOperationPreparer::OutputDescription (especially for protobuf),
+ /// but you can use @ref NYT::TJobOperationPreparer::OutputSchema directly
+ /// - specify output row type (@ref NYT::TJobOperationPreparer::OutputDescription)
+ /// - specify input row type (@ref NYT::TJobOperationPreparer::InputDescription)
+ /// - specify input column filter and renaming (@ref NYT::TJobOperationPreparer::InputColumnFilter and @ref NYT::TJobOperationPreparer::InputColumnRenaming)
+ /// - specify format hints (@ref NYT::TJobOperationPreparer::InputFormatHints,
+ /// NYT::TJobOperationPreparer::OutputFormatHints and @ref NYT::TJobOperationPreparer::FormatHints)
+ /// - maybe something more, cf. the methods of @ref NYT::TJobOperationPreparer.
+ ///
+ /// If one has several similar tables, groups can be used.
+ /// Groups are delimited by @ref NYT::TJobOperationPreparer::BeginInputGroup /
+ /// @ref NYT::TJobOperationPreparer::TInputGroup::EndInputGroup and
+ /// @ref NYT::TJobOperationPreparer::BeginOutputGroup /
+ /// @ref NYT::TJobOperationPreparer::TOutputGroup::EndOutputGroup.
+ /// Example:
+ /// @code{.cpp}
+ /// preparer
+ /// .BeginInputGroup({1,2,4,8})
+ /// .ColumnRenaming({{"a", "b"}, {"c", "d"}})
+ /// .ColumnFilter({"a", "c"})
+ /// .EndInputGroup();
+ /// @endcode
+ ///
+ /// @note All the output table schemas must be set
+ /// (possibly as empty nonstrict using @ref NYT::TJobOperationPreparer::NoOutputSchema or
+ /// @ref NYT::TJobOperationPreparer::TOutputGroup::NoSchema).
+ /// By default all the output table schemas are marked as empty nonstrict.
+ virtual void PrepareOperation(const IOperationPreparationContext& context, TJobOperationPreparer& preparer) const;
+};
+
+///
+/// @brief Declare what fields of currently declared job class to save and restore on cluster node.
+#define Y_SAVELOAD_JOB(...) \
+ virtual void Save(IOutputStream& stream) const override { Save(&stream); } \
+ virtual void Load(IInputStream& stream) override { Load(&stream); } \
+ Y_PASS_VA_ARGS(Y_SAVELOAD_DEFINE(__VA_ARGS__))
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface for jobs with typed inputs and outputs.
+class IStructuredJob
+ : public IJob
+{
+public:
+ ///
+ /// @brief This methods are called when creating table reader and writer for the job.
+ ///
+ /// Override them if you want to implement custom input logic. (e.g. addtitional bufferization)
+ virtual TRawTableReaderPtr CreateCustomRawJobReader(int fd) const;
+ virtual THolder<IProxyOutput> CreateCustomRawJobWriter(size_t outputTableCount) const;
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const = 0;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Create default raw job reader.
+TRawTableReaderPtr CreateRawJobReader(int fd = 0);
+
+///
+/// @brief Create default raw job writer.
+THolder<IProxyOutput> CreateRawJobWriter(size_t outputTableCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Base interface for structured (typed) map jobs.
+class IMapperBase
+ : public IStructuredJob
+{ };
+
+///
+/// @brief Base interface for structured (typed) map jobs with given reader and writer.
+template <class TR, class TW>
+class IMapper
+ : public IMapperBase
+{
+public:
+ using TReader = TR;
+ using TWriter = TW;
+
+public:
+ /// Type of job implemented by this class.
+ static constexpr EType JobType = EType::Mapper;
+
+ ///
+ /// @brief This method is called before feeding input rows to mapper (before `Do` method).
+ virtual void Start(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ ///
+ /// @brief This method is called exactly once for the whole job input.
+ ///
+ /// Read input rows from `reader` and write output ones to `writer`.
+ virtual void Do(TReader* reader, TWriter* writer) = 0;
+
+ ///
+ /// @brief This method is called after feeding input rows to mapper (after `Do` method).
+ virtual void Finish(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const override;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Base interface for structured (typed) reduce jobs.
+///
+/// It is common base for @ref NYT::IReducer and @ref NYT::IAggregatorReducer.
+class IReducerBase
+ : public IStructuredJob
+{ };
+
+///
+/// @brief Base interface for structured (typed) reduce jobs with given reader and writer.
+template <class TR, class TW>
+class IReducer
+ : public IReducerBase
+{
+public:
+ using TReader = TR;
+ using TWriter = TW;
+
+public:
+ /// Type of job implemented by this class.
+ static constexpr EType JobType = EType::Reducer;
+
+public:
+
+ ///
+ /// @brief This method is called before feeding input rows to reducer (before `Do` method).
+ virtual void Start(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ ///
+ /// @brief This method is called exactly once for each range with same value of `ReduceBy` (or `JoinBy`) keys.
+ virtual void Do(TReader* reader, TWriter* writer) = 0;
+
+ ///
+ /// @brief This method is called after feeding input rows to reducer (after `Do` method).
+ virtual void Finish(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ ///
+ /// @brief Refuse to process the remaining row ranges and finish the job (successfully).
+ void Break();
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const override;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Base interface of jobs used inside reduce operations.
+///
+/// Unlike @ref NYT::IReducer jobs their `Do' method is called only once
+/// and takes whole range of records split by key boundaries.
+///
+/// Template argument `TR` must be @ref NYT::TTableRangesReader.
+template <class TR, class TW>
+class IAggregatorReducer
+ : public IReducerBase
+{
+public:
+ using TReader = TR;
+ using TWriter = TW;
+
+public:
+ /// Type of job implemented by this class.
+ static constexpr EType JobType = EType::ReducerAggregator;
+
+public:
+ ///
+ /// @brief This method is called before feeding input rows to reducer (before `Do` method).
+ virtual void Start(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ ///
+ /// @brief This method is called exactly once for the whole job input.
+ virtual void Do(TReader* reader, TWriter* writer) = 0;
+
+ ///
+ /// @brief This method is called after feeding input rows to reducer (after `Do` method).
+ virtual void Finish(TWriter* writer)
+ {
+ Y_UNUSED(writer);
+ }
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const override;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface for raw jobs (i.e. reading and writing byte streams).
+class IRawJob
+ : public IJob
+{
+public:
+ /// Type of job implemented by this class.
+ static constexpr EType JobType = EType::RawJob;
+
+ ///
+ /// @brief This method is called exactly once for the whole job input.
+ virtual void Do(const TRawJobContext& jobContext) = 0;
+};
+
+///
+/// @brief Interface of jobs that run the given bash command.
+class ICommandJob
+ : public IJob
+{
+public:
+ ///
+ /// @brief Get bash command to run.
+ ///
+ /// @note This method is called on the client side.
+ virtual const TString& GetCommand() const = 0;
+};
+
+///
+/// @brief Raw job executing given bash command.
+///
+/// @note The binary will not be uploaded.
+class TCommandRawJob
+ : public IRawJob
+ , public ICommandJob
+{
+public:
+ ///
+ /// @brief Create job with specified command.
+ ///
+ /// @param command Bash command to run.
+ explicit TCommandRawJob(TStringBuf command = {});
+
+ const TString& GetCommand() const override;
+ void Do(const TRawJobContext& jobContext) override;
+
+private:
+ TString Command_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Base interface for vanilla jobs.
+///
+/// @see https://yt.yandex-team.ru/docs/description/mr/vanilla
+class IVanillaJobBase
+ : public virtual IStructuredJob
+{
+public:
+ /// Type of job implemented by this class.
+ static constexpr EType JobType = EType::VanillaJob;
+};
+
+template <class TW = void>
+class IVanillaJob;
+
+///
+/// @brief Interface of vanilla job without outputs.
+template <>
+class IVanillaJob<void>
+ : public IVanillaJobBase
+{
+public:
+ ///
+ /// @brief This method is called exactly once for each vanilla job.
+ virtual void Do() = 0;
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const override;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const override;
+};
+
+///
+/// @brief Vanilla job executing given bash command.
+///
+/// @note The binary will not be uploaded.
+class TCommandVanillaJob
+ : public IVanillaJob<>
+ , public ICommandJob
+{
+public:
+ ///
+ /// @brief Create job with specified command.
+ ///
+ /// @param command Bash command to run.
+ explicit TCommandVanillaJob(TStringBuf command = {});
+
+ const TString& GetCommand() const override;
+ void Do() override;
+
+private:
+ TString Command_;
+};
+
+///
+/// @brief Interface for vanilla jobs with output tables.
+template <class TW>
+class IVanillaJob
+ : public IVanillaJobBase
+{
+public:
+ using TWriter = TW;
+
+public:
+ ///
+ /// @brief This method is called before `Do` method.
+ virtual void Start(TWriter* /* writer */)
+ { }
+
+ ///
+ /// @brief This method is called exactly once for each vanilla job.
+ ///
+ /// Write output rows to `writer`.
+ virtual void Do(TWriter* writer) = 0;
+
+ ///
+ /// @brief This method is called after `Do` method.
+ virtual void Finish(TWriter* /* writer */)
+ { }
+
+ virtual TStructuredRowStreamDescription GetInputRowStreamDescription() const override;
+ virtual TStructuredRowStreamDescription GetOutputRowStreamDescription() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Attributes to request for an operation.
+enum class EOperationAttribute : int
+{
+ Id /* "id" */,
+ Type /* "type" */,
+ State /* "state" */,
+ AuthenticatedUser /* "authenticated_user" */,
+ StartTime /* "start_time" */,
+ FinishTime /* "finish_time" */,
+ BriefProgress /* "brief_progress" */,
+ BriefSpec /* "brief_spec" */,
+ Suspended /* "suspended" */,
+ Result /* "result" */,
+ Progress /* "progress" */,
+ Events /* "events" */,
+ Spec /* "spec" */,
+ FullSpec /* "full_spec" */,
+ UnrecognizedSpec /* "unrecognized_spec" */,
+};
+
+///
+/// @brief Class describing which attributes to request in @ref NYT::IClient::GetOperation or @ref NYT::IClient::ListOperations.
+struct TOperationAttributeFilter
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TOperationAttributeFilter;
+ /// @endcond
+
+ TVector<EOperationAttribute> Attributes_;
+
+ ///
+ /// @brief Add attribute to the filter. Calls are supposed to be chained.
+ TSelf& Add(EOperationAttribute attribute)
+ {
+ Attributes_.push_back(attribute);
+ return *this;
+ }
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetOperation call.
+struct TGetOperationOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetOperationOptions;
+ /// @endcond
+
+ ///
+ /// @brief What attributes to request (if omitted, the default set of attributes will be requested).
+ FLUENT_FIELD_OPTION(TOperationAttributeFilter, AttributeFilter);
+};
+
+///
+/// @brief "Coarse-grained" state of an operation.
+enum class EOperationBriefState : int
+{
+ InProgress /* "in_progress" */,
+ Completed /* "completed" */,
+ Aborted /* "aborted" */,
+
+ /// Failed
+ Failed /* "failed" */,
+};
+
+///
+/// @brief Operation type.
+enum class EOperationType : int
+{
+ Map /* "map" */,
+ Merge /* "merge" */,
+ Erase /* "erase" */,
+ Sort /* "sort" */,
+ Reduce /* "reduce" */,
+ MapReduce /* "map_reduce" */,
+ RemoteCopy /* "remote_copy" */,
+ JoinReduce /* "join_reduce" */,
+ Vanilla /* "vanilla" */,
+};
+
+///
+/// @brief Operation progress.
+struct TOperationProgress
+{
+ ///
+ /// @brief Total job statistics.
+ TJobStatistics JobStatistics;
+
+ ///
+ /// @brief Job counter for various job states with hierarchy.
+ TJobCounters JobCounters;
+
+ ///
+ /// @brief Time when this progress was built on scheduler or CA.
+ TMaybe<TInstant> BuildTime;
+};
+
+///
+/// @brief Brief operation progress (numbers of jobs in these states).
+struct TOperationBriefProgress
+{
+ ui64 Aborted = 0;
+ ui64 Completed = 0;
+ ui64 Failed = 0;
+ ui64 Lost = 0;
+ ui64 Pending = 0;
+ ui64 Running = 0;
+ ui64 Total = 0;
+};
+
+///
+/// @brief Operation result.
+struct TOperationResult
+{
+ ///
+ /// @brief For a unsuccessfully finished operation: description of error.
+ TMaybe<TYtError> Error;
+};
+
+///
+/// @brief Operation event (change of state).
+struct TOperationEvent
+{
+ ///
+ /// @brief New state of operation.
+ TString State;
+
+ ///
+ /// @brief Time of state change.
+ TInstant Time;
+};
+
+///
+/// @brief Operation info.
+///
+/// A field may be `Nothing()` either if it was not requested (see @ref NYT::TGetOperationOptions::AttributeFilter)
+/// or it is not available (i.e. `FinishTime` for a running operation).
+/// @see https://yt.yandex-team.ru/docs/api/commands#get_operation
+struct TOperationAttributes
+{
+ ///
+ /// @brief Operation id.
+ TMaybe<TOperationId> Id;
+
+ ///
+ /// @brief Operation type.
+ TMaybe<EOperationType> Type;
+
+ ///
+ /// @brief Operation state.
+ TMaybe<TString> State;
+
+ ///
+ /// @brief "Coarse-grained" operation state.
+ TMaybe<EOperationBriefState> BriefState;
+
+ ///
+ /// @brief Name of user that started the operation.
+ TMaybe<TString> AuthenticatedUser;
+
+ ///
+ /// @brief Operation start time.
+ TMaybe<TInstant> StartTime;
+
+ ///
+ /// @brief Operation finish time (if the operation has finished).
+ TMaybe<TInstant> FinishTime;
+
+ ///
+ /// @brief Brief progress of the operation.
+ TMaybe<TOperationBriefProgress> BriefProgress;
+
+ ///
+ /// @brief Brief spec of operation (light-weight fields only).
+ TMaybe<TNode> BriefSpec;
+
+ ///
+ /// @brief Spec of the operation as provided by the user.
+ TMaybe<TNode> Spec;
+
+ ///
+ /// @brief Full spec of operation (all fields not specified by user are filled with default values).
+ TMaybe<TNode> FullSpec;
+
+ ///
+ /// @brief Fields not recognized by scheduler.
+ TMaybe<TNode> UnrecognizedSpec;
+
+ ///
+ /// @brief Is operation suspended.
+ TMaybe<bool> Suspended;
+
+ ///
+ /// @brief Operation result.
+ TMaybe<TOperationResult> Result;
+
+ ///
+ /// @brief Operation progress.
+ TMaybe<TOperationProgress> Progress;
+
+ ///
+ /// @brief List of operation events (changes of state).
+ TMaybe<TVector<TOperationEvent>> Events;
+
+ ///
+ /// @brief Map from alert name to its description.
+ TMaybe<THashMap<TString, TYtError>> Alerts;
+};
+
+///
+/// @brief Direction of cursor for paging, see @ref NYT::TListOperationsOptions::CursorDirection.
+enum class ECursorDirection
+{
+ Past /* "past" */,
+ Future /* "future" */,
+};
+
+///
+/// @brief Options of @ref NYT::IClient::ListOperations command.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#list_operations
+struct TListOperationsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TListOperationsOptions;
+ /// @endcond
+
+ ///
+ /// @name Time range specification
+ ///
+ /// List operations with start time in half-closed interval
+ /// `[CursorTime, ToTime)` if `CursorDirection == Future` or
+ /// `[FromTime, CursorTime)` if `CursorDirection == Past`.
+ ///@{
+
+ ///
+ /// @brief Search for operations with start time >= `FromTime`.
+ FLUENT_FIELD_OPTION(TInstant, FromTime);
+
+ ///
+ /// @brief Search for operations with start time < `ToTime`.
+ FLUENT_FIELD_OPTION(TInstant, ToTime);
+
+ ///
+ /// @brief Additional restriction on operation start time (useful for pagination).
+ ///
+ /// Search for operations with start time >= `CursorTime` if `CursorDirection == Future`
+ /// and with start time < `CursorTime` if `CursorDirection == Past`
+ FLUENT_FIELD_OPTION(TInstant, CursorTime);
+
+ ///
+ /// @brief Direction of pagination (see @ref NYT::TListOperationsOptions::CursorTime).
+ FLUENT_FIELD_OPTION(ECursorDirection, CursorDirection);
+
+ ///@}
+
+ ///
+ /// @name Filters
+ /// Choose operations satisfying given filters.
+ ///@{
+
+ ///
+ /// @brief Search for `Filter` as a substring in operation text factors
+ /// (e.g. title or input/output table paths).
+ FLUENT_FIELD_OPTION(TString, Filter);
+
+ ///
+ /// @brief Choose operations whose pools include `Pool`.
+ FLUENT_FIELD_OPTION(TString, Pool);
+
+ ///
+ /// @brief Choose operations with given @ref NYT::TOperationAttributes::AuthenticatedUser.
+ FLUENT_FIELD_OPTION(TString, User);
+
+ ///
+ /// @brief Choose operations with given @ref NYT::TOperationAttributes::State.
+ FLUENT_FIELD_OPTION(TString, State);
+
+ ///
+ /// @brief Choose operations with given @ref NYT::TOperationAttributes::Type.
+ FLUENT_FIELD_OPTION(EOperationType, Type);
+
+ ///
+ /// @brief Choose operations having (or not having) any failed jobs.
+ FLUENT_FIELD_OPTION(bool, WithFailedJobs);
+
+ ///@}
+
+ ///
+ /// @brief Search for operations in the archive in addition to Cypress.
+ FLUENT_FIELD_OPTION(bool, IncludeArchive);
+
+ ///
+ /// @brief Include the counters for different filter parameters in the response.
+ ///
+ /// Include number of operations for each pool, user, state, type
+ /// and the number of operations having failed jobs.
+ FLUENT_FIELD_OPTION(bool, IncludeCounters);
+
+ ///
+ /// @brief Return no more than `Limit` operations (current default and maximum value is 1000).
+ FLUENT_FIELD_OPTION(i64, Limit);
+};
+
+///
+/// @brief Response for @ref NYT::IClient::ListOperations command.
+struct TListOperationsResult
+{
+ ///
+ /// @brief Found operations' attributes.
+ TVector<TOperationAttributes> Operations;
+
+ ///
+ /// @name Counters for different filter.
+ ///
+ /// If counters were requested (@ref NYT::TListOperationsOptions::IncludeCounters is `true`)
+ /// the maps contain the number of operations found for each pool, user, state and type.
+ /// NOTE:
+ /// 1) Counters ignore CursorTime and CursorDirection,
+ /// they always are collected in the whole [FromTime, ToTime) interval.
+ /// 2) Each next counter in the sequence [pool, user, state, type, with_failed_jobs]
+ /// takes into account all the previous filters (i.e. if you set User filter to "some-user"
+ /// type counts describe only operations with user "some-user").
+ /// @{
+
+ ///
+ /// @brief Number of operations for each pool.
+ TMaybe<THashMap<TString, i64>> PoolCounts;
+
+ ///
+ /// @brief Number of operations for each user (subject to previous filters).
+ TMaybe<THashMap<TString, i64>> UserCounts;
+
+ ///
+ /// @brief Number of operations for each state (subject to previous filters).
+ TMaybe<THashMap<TString, i64>> StateCounts;
+
+ ///
+ /// @brief Number of operations for each type (subject to previous filters).
+ TMaybe<THashMap<EOperationType, i64>> TypeCounts;
+
+ ///
+ /// @brief Number of operations having failed jobs (subject to all previous filters).
+ TMaybe<i64> WithFailedJobsCount;
+
+ /// @}
+
+ ///
+ /// @brief Whether some operations were not returned due to @ref NYT::TListOperationsOptions::Limit.
+ ///
+ /// `Incomplete == true` means that not all operations satisfying filters
+ /// were returned (limit exceeded) and you need to repeat the request with new @ref NYT::TListOperationsOptions::CursorTime
+ /// (e.g. `CursorTime == *Operations.back().StartTime`, but don't forget to
+ /// remove the duplicates).
+ bool Incomplete;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Data source for @ref NYT::IClient::ListJobs command.
+enum class EListJobsDataSource : int
+{
+ Runtime /* "runtime" */,
+ Archive /* "archive" */,
+ Auto /* "auto" */,
+ Manual /* "manual" */,
+};
+
+///
+/// @brief Job type.
+enum class EJobType : int
+{
+ SchedulerFirst /* "scheduler_first" */,
+ Map /* "map" */,
+ PartitionMap /* "partition_map" */,
+ SortedMerge /* "sorted_merge" */,
+ OrderedMerge /* "ordered_merge" */,
+ UnorderedMerge /* "unordered_merge" */,
+ Partition /* "partition" */,
+ SimpleSort /* "simple_sort" */,
+ FinalSort /* "final_sort" */,
+ SortedReduce /* "sorted_reduce" */,
+ PartitionReduce /* "partition_reduce" */,
+ ReduceCombiner /* "reduce_combiner" */,
+ RemoteCopy /* "remote_copy" */,
+ IntermediateSort /* "intermediate_sort" */,
+ OrderedMap /* "ordered_map" */,
+ JoinReduce /* "join_reduce" */,
+ Vanilla /* "vanilla" */,
+ SchedulerUnknown /* "scheduler_unknown" */,
+ SchedulerLast /* "scheduler_last" */,
+ ReplicatorFirst /* "replicator_first" */,
+ ReplicateChunk /* "replicate_chunk" */,
+ RemoveChunk /* "remove_chunk" */,
+ RepairChunk /* "repair_chunk" */,
+ SealChunk /* "seal_chunk" */,
+ ReplicatorLast /* "replicator_last" */,
+};
+
+///
+/// @brief Well-known task names.
+enum class ETaskName : int
+{
+ Map /* "map" */,
+ PartitionMap0 /* "partition_map(0)" */,
+ SortedMerge /* "sorted_merge" */,
+ OrderedMerge /* "ordered_merge" */,
+ UnorderedMerge /* "unordered_merge" */,
+ Partition0 /* "partition(0)" */,
+ Partition1 /* "partition(1)" */,
+ Partition2 /* "partition(2)" */,
+ SimpleSort /* "simple_sort" */,
+ FinalSort /* "final_sort" */,
+ SortedReduce /* "sorted_reduce" */,
+ PartitionReduce /* "partition_reduce" */,
+ ReduceCombiner /* "reduce_combiner" */,
+ RemoteCopy /* "remote_copy" */,
+ IntermediateSort /* "intermediate_sort" */,
+ OrderedMap /* "ordered_map" */,
+ JoinReduce /* "join_reduce" */,
+};
+
+///
+/// @brief Task name (can either well-known or just a string).
+class TTaskName
+{
+public:
+
+ // Constructors are implicit by design.
+
+ ///
+ /// @brief Construct a custom task name.
+ TTaskName(TString taskName);
+
+ ///
+ /// @brief Construct a custom task name.
+ TTaskName(const char* taskName);
+
+ ///
+ /// @brief Construct a well-known task name.
+ TTaskName(ETaskName taskName);
+
+ const TString& Get() const;
+
+private:
+ TString TaskName_;
+};
+
+///
+/// @brief Job state.
+enum class EJobState : int
+{
+ None /* "none" */,
+ Waiting /* "waiting" */,
+ Running /* "running" */,
+ Aborting /* "aborting" */,
+ Completed /* "completed" */,
+ Failed /* "failed" */,
+ Aborted /* "aborted" */,
+ Lost /* "lost" */,
+};
+
+///
+/// @brief Job sort field.
+///
+/// @see @ref NYT::TListJobsOptions.
+enum class EJobSortField : int
+{
+ Type /* "type" */,
+ State /* "state" */,
+ StartTime /* "start_time" */,
+ FinishTime /* "finish_time" */,
+ Address /* "address" */,
+ Duration /* "duration" */,
+ Progress /* "progress" */,
+ Id /* "id" */,
+};
+
+///
+/// @brief Job sort direction.
+///
+/// @see @ref NYT::TListJobsOptions.
+enum class EJobSortDirection : int
+{
+ Ascending /* "ascending" */,
+ Descending /* "descending" */,
+};
+
+///
+/// @brief Options for @ref NYT::IClient::ListJobs.
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands.html#list_jobs
+struct TListJobsOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TListJobsOptions;
+ /// @endcond
+
+ ///
+ /// @name Filters
+ /// Return only jobs with given value of parameter (type, state, address and existence of stderr).
+ /// If a field is `Nothing()`, return jobs with all possible values of the corresponding parameter.
+ /// @{
+
+ ///
+ /// @brief Job type.
+ FLUENT_FIELD_OPTION(EJobType, Type);
+
+ ///
+ /// @brief Job state.
+ FLUENT_FIELD_OPTION(EJobState, State);
+
+ ///
+ /// @brief Address of the cluster node where job was running.
+ FLUENT_FIELD_OPTION(TString, Address);
+
+ ///
+ /// @brief Return only jobs whose stderr has been saved.
+ FLUENT_FIELD_OPTION(bool, WithStderr);
+
+ ///
+ /// @brief Return only jobs whose spec has been saved.
+ FLUENT_FIELD_OPTION(bool, WithSpec);
+
+ ///
+ /// @brief Return only jobs whose fail context has been saved.
+ FLUENT_FIELD_OPTION(bool, WithFailContext);
+
+ /// @}
+
+ ///
+ /// @name Sort options
+ /// @{
+
+ ///
+ /// @brief Sort by this field.
+ FLUENT_FIELD_OPTION(EJobSortField, SortField);
+
+ ///
+ /// @brief Sort order.
+ FLUENT_FIELD_OPTION(ESortOrder, SortOrder);
+
+ /// @}
+
+ ///
+ /// @brief Data source.
+ ///
+ /// Where to search for jobs: in scheduler and Cypress ('Runtime'), in archive ('Archive'),
+ /// automatically basing on operation presence in Cypress ('Auto') or choose manually (`Manual').
+ FLUENT_FIELD_OPTION(EListJobsDataSource, DataSource);
+
+ /// @deprecated
+ FLUENT_FIELD_OPTION(bool, IncludeCypress);
+
+ /// @deprecated
+ FLUENT_FIELD_OPTION(bool, IncludeControllerAgent);
+
+ /// @deprecated
+ FLUENT_FIELD_OPTION(bool, IncludeArchive);
+
+ ///
+ /// @brief Maximum number of jobs to return.
+ FLUENT_FIELD_OPTION(i64, Limit);
+
+ ///
+ /// @brief Number of jobs (in specified sort order) to skip.
+ ///
+ /// Together with @ref NYT::TListJobsOptions::Limit may be used for pagination.
+ FLUENT_FIELD_OPTION(i64, Offset);
+};
+
+///
+/// @brief Description of a core dump that happened in the job.
+struct TCoreInfo
+{
+ i64 ProcessId;
+ TString ExecutableName;
+ TMaybe<ui64> Size;
+ TMaybe<TYtError> Error;
+};
+
+///
+/// @brief Job attributes.
+///
+/// A field may be `Nothing()` if it is not available (i.e. `FinishTime` for a running job).
+///
+/// @see https://yt.yandex-team.ru/docs/api/commands#get_job
+struct TJobAttributes
+{
+ ///
+ /// @brief Job id.
+ TMaybe<TJobId> Id;
+
+ ///
+ /// @brief Job type
+ TMaybe<EJobType> Type;
+
+ ///
+ /// @brief Job state.
+ TMaybe<EJobState> State;
+
+ ///
+ /// @brief Address of a cluster node where job was running.
+ TMaybe<TString> Address;
+
+ ///
+ /// @brief The name of the task that job corresponds to.
+ TMaybe<TString> TaskName;
+
+ ///
+ /// @brief Job start time.
+ TMaybe<TInstant> StartTime;
+
+ ///
+ /// @brief Job finish time (for a finished job).
+ TMaybe<TInstant> FinishTime;
+
+ ///
+ /// @brief Estimated ratio of job's completed work.
+ TMaybe<double> Progress;
+
+ ///
+ /// @brief Size of saved job stderr.
+ TMaybe<i64> StderrSize;
+
+ ///
+ /// @brief Error for a unsuccessfully finished job.
+ TMaybe<TYtError> Error;
+
+ ///
+ /// @brief Job brief statistics.
+ TMaybe<TNode> BriefStatistics;
+
+ ///
+ /// @brief Job input paths (with ranges).
+ TMaybe<TVector<TRichYPath>> InputPaths;
+
+ ///
+ /// @brief Infos for core dumps produced by job.
+ TMaybe<TVector<TCoreInfo>> CoreInfos;
+};
+
+///
+/// @brief Response for @ref NYT::IOperation::ListJobs.
+struct TListJobsResult
+{
+ ///
+ /// @brief Jobs.
+ TVector<TJobAttributes> Jobs;
+
+ ///
+ /// @deprecated
+ TMaybe<i64> CypressJobCount;
+
+ ///
+ /// @brief Number of jobs retrieved from controller agent.
+ TMaybe<i64> ControllerAgentJobCount;
+
+ ///
+ /// @brief Number of jobs retrieved from archive.
+ TMaybe<i64> ArchiveJobCount;
+};
+
+////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Options for @ref NYT::IClient::GetJob.
+struct TGetJobOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetJobOptions;
+ /// @endcond
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetJobInput.
+struct TGetJobInputOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetJobInputOptions;
+ /// @endcond
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetJobFailContext.
+struct TGetJobFailContextOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetJobFailContextOptions;
+ /// @endcond
+};
+
+///
+/// @brief Options for @ref NYT::IClient::GetJobStderr.
+struct TGetJobStderrOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetJobStderrOptions;
+ /// @endcond
+};
+
+////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Options for @ref NYT::IOperation::GetFailedJobInfo.
+struct TGetFailedJobInfoOptions
+{
+ /// @cond Doxygen_Suppress
+ using TSelf = TGetFailedJobInfoOptions;
+ /// @endcond
+
+ ///
+ /// @brief How many jobs to download. Which jobs will be chosen is undefined.
+ FLUENT_FIELD_DEFAULT(ui64, MaxJobCount, 10);
+
+ ///
+ /// @brief How much of stderr tail should be downloaded.
+ FLUENT_FIELD_DEFAULT(ui64, StderrTailSize, 64 * 1024);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface representing an operation.
+struct IOperation
+ : public TThrRefBase
+{
+ virtual ~IOperation() = default;
+
+ ///
+ /// @brief Get operation id.
+ virtual const TOperationId& GetId() const = 0;
+
+ ///
+ /// @brief Get URL of the operation in YT Web UI.
+ virtual TString GetWebInterfaceUrl() const = 0;
+
+ ///
+ /// @brief Get last error for not started operations. Get state on YT cluster for started operations.
+ ///
+ /// For not started operations last error is an error that's being retried during operation
+ /// preparation/start (e.g. lock files, start operation request).
+ virtual TString GetStatus() const = 0;
+
+ ///
+ /// @brief Get preparation future.
+ ///
+ /// @return future that is set when operation is prepared.
+ virtual ::NThreading::TFuture<void> GetPreparedFuture() = 0;
+
+ ///
+ /// @brief Start operation synchronously.
+ ///
+ /// @note: Do NOT call this method twice.
+ ///
+ /// If operation is not prepared yet, Start() will block waiting for preparation finish.
+ /// Be ready to catch exception if operation preparation or start failed.
+ virtual void Start() = 0;
+
+ ///
+ /// @brief Is the operation started
+ ///
+ /// Returns true if the operation is started on the cluster
+ virtual bool IsStarted() const = 0;
+
+ ///
+ /// @brief Get start future.
+ ///
+ /// @return future that is set when operation is started.
+ virtual ::NThreading::TFuture<void> GetStartedFuture() = 0;
+
+ ///
+ /// @brief Start watching operation.
+ ///
+ /// @return future that is set when operation is complete.
+ ///
+ /// @note: the user should check value of returned future to ensure that operation completed successfully e.g.
+ /// @code{.cpp}
+ /// auto operationComplete = operation->Watch();
+ /// operationComplete.Wait();
+ /// operationComplete.GetValue(); /// will throw if operation completed with errors
+ /// @endcode
+ ///
+ /// If operation is completed successfully the returned future contains void value.
+ /// If operation is completed with error future contains @ref NYT::TOperationFailedError.
+ /// In rare cases when error occurred while waiting (e.g. YT become unavailable) future might contain other exception.
+ virtual ::NThreading::TFuture<void> Watch() = 0;
+
+ ///
+ /// @brief Get information about failed jobs.
+ ///
+ /// Can be called for operation in any stage.
+ /// Though user should keep in mind that this method always fetches info from cypress
+ /// and doesn't work when operation is archived. Successfully completed operations can be archived
+ /// quite quickly (in about ~30 seconds).
+ virtual TVector<TFailedJobInfo> GetFailedJobInfo(const TGetFailedJobInfoOptions& options = TGetFailedJobInfoOptions()) = 0;
+
+ ///
+ /// Get operation brief state.
+ virtual EOperationBriefState GetBriefState() = 0;
+
+ ///
+ /// @brief Get error (if operation has failed).
+ ///
+ /// @return `Nothing()` if operation is in 'Completed' or 'InProgress' state (or reason for failed / aborted operation).
+ virtual TMaybe<TYtError> GetError() = 0;
+
+ ///
+ /// Get job statistics.
+ virtual TJobStatistics GetJobStatistics() = 0;
+
+ ///
+ /// Get operation progress.
+ ///
+ /// @return `Nothing()` if operation has no running jobs yet, e.g. when it is in "materializing" or "pending" state.
+ virtual TMaybe<TOperationBriefProgress> GetBriefProgress() = 0;
+
+ ///
+ /// @brief Abort operation.
+ ///
+ /// Operation will be finished immediately.
+ /// All results of completed/running jobs will be lost.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#abort_op
+ virtual void AbortOperation() = 0;
+
+ ///
+ /// @brief Complete operation.
+ ///
+ /// Operation will be finished immediately.
+ /// All results of completed jobs will appear in output tables.
+ /// All results of running (not completed) jobs will be lost.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#complete_op
+ virtual void CompleteOperation() = 0;
+
+ ///
+ /// @brief Suspend operation.
+ ///
+ /// Jobs will not be aborted by default, c.f. @ref NYT::TSuspendOperationOptions.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#suspend_op
+ virtual void SuspendOperation(
+ const TSuspendOperationOptions& options = TSuspendOperationOptions()) = 0;
+
+ ///
+ /// @brief Resume previously suspended operation.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#resume_op
+ virtual void ResumeOperation(
+ const TResumeOperationOptions& options = TResumeOperationOptions()) = 0;
+
+ ///
+ /// @brief Get operation attributes.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#get_operation
+ virtual TOperationAttributes GetAttributes(
+ const TGetOperationOptions& options = TGetOperationOptions()) = 0;
+
+ ///
+ /// @brief Update operation runtime parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#update_op_parameters
+ virtual void UpdateParameters(
+ const TUpdateOperationParametersOptions& options = TUpdateOperationParametersOptions()) = 0;
+
+ ///
+ /// @brief Get job attributes.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#get_job
+ virtual TJobAttributes GetJob(
+ const TJobId& jobId,
+ const TGetJobOptions& options = TGetJobOptions()) = 0;
+
+ ///
+ /// List jobs satisfying given filters (see @ref NYT::TListJobsOptions).
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#list_jobs
+ virtual TListJobsResult ListJobs(
+ const TListJobsOptions& options = TListJobsOptions()) = 0;
+};
+
+///
+/// @brief Interface of client capable of managing operations.
+struct IOperationClient
+{
+ ///
+ /// @brief Run Map operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param mapper Instance of a job to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/map
+ IOperationPtr Map(
+ const TMapOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run Map operation.
+ ///
+ /// @param mapper Instance of a job to run.
+ /// @param input Input table(s)
+ /// @param output Output table(s)
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/map
+ IOperationPtr Map(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TMapOperationSpec& spec = TMapOperationSpec(),
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run raw Map operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param rawJob Instance of a raw mapper to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/map
+ virtual IOperationPtr RawMap(
+ const TRawMapOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> rawJob,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run Reduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param reducer Instance of a job to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+ IOperationPtr Reduce(
+ const TReduceOperationSpec& spec,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run Reduce operation.
+ ///
+ /// @param reducer Instance of a job to run.
+ /// @param input Input table(s)
+ /// @param output Output table(s)
+ /// @param reduceBy Columns to group rows by.
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+ IOperationPtr Reduce(
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ const TReduceOperationSpec& spec = TReduceOperationSpec(),
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run raw Reduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param rawJob Instance of a raw reducer to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/reduce
+ virtual IOperationPtr RawReduce(
+ const TRawReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> rawJob,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run JoinReduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param reducer Instance of a job to run.
+ /// @param options Optional parameters.
+ ///
+ /// @deprecated Use @ref NYT::IOperationClient::Reduce with @ref NYT::TReduceOperationSpec::EnableKeyGuarantee set to `false.
+ IOperationPtr JoinReduce(
+ const TJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run raw JoinReduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param rawJob Instance of a raw reducer to run.
+ /// @param options Optional parameters.
+ ///
+ /// @deprecated Use @ref NYT::IOperationClient::RawReduce with @ref NYT::TReduceOperationSpec::EnableKeyGuarantee set to `false.
+ virtual IOperationPtr RawJoinReduce(
+ const TRawJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> rawJob,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run MapReduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param mapper Instance of a map job to run (identity mapper if `nullptr`).
+ /// @param reducer Instance of a reduce job to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+ IOperationPtr MapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run MapReduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param mapper Instance of a map job to run (identity mapper if `nullptr`).
+ /// @param reducerCombiner Instance of a reduce combiner to run (identity reduce combiner if `nullptr`).
+ /// @param reducer Instance of a reduce job to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+ IOperationPtr MapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reduceCombiner,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run MapReduce operation.
+ ///
+ /// @param mapper Instance of mapper to run (identity mapper if `nullptr`).
+ /// @param reducer Instance of reducer to run.
+ /// @param input Input table(s)
+ /// @param output Output table(s)
+ /// @param reduceBy Columns to group rows by.
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+ IOperationPtr MapReduce(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ TMapReduceOperationSpec spec = TMapReduceOperationSpec(),
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run MapReduce operation.
+ ///
+ /// @param mapper Instance of mapper to run (identity mapper if `nullptr`).
+ /// @param reduceCombiner Instance of reduceCombiner to run (identity reduce combiner if `nullptr`).
+ /// @param reducer Instance of reducer to run.
+ /// @param input Input table(s)
+ /// @param output Output table(s)
+ /// @param reduceBy Columns to group rows by.
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+ IOperationPtr MapReduce(
+ ::TIntrusivePtr<IMapperBase> mapper,
+ ::TIntrusivePtr<IReducerBase> reduceCombiner,
+ ::TIntrusivePtr<IReducerBase> reducer,
+ const TOneOrMany<TStructuredTablePath>& input,
+ const TOneOrMany<TStructuredTablePath>& output,
+ const TSortColumns& reduceBy,
+ TMapReduceOperationSpec spec = TMapReduceOperationSpec(),
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run raw MapReduce operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param mapper Instance of a raw mapper to run (identity mapper if `nullptr`).
+ /// @param mapper Instance of a raw reduce combiner to run (identity reduce combiner if `nullptr`).
+ /// @param mapper Instance of a raw reducer to run.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/mapreduce
+ virtual IOperationPtr RawMapReduce(
+ const TRawMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IRawJob> mapper,
+ ::TIntrusivePtr<IRawJob> reduceCombiner,
+ ::TIntrusivePtr<IRawJob> reducer,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run Sort operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/sort
+ virtual IOperationPtr Sort(
+ const TSortOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run Sort operation.
+ ///
+ /// @param input Input table(s).
+ /// @param output Output table.
+ /// @param sortBy Columns to sort input rows by.
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/sort
+ IOperationPtr Sort(
+ const TOneOrMany<TRichYPath>& input,
+ const TRichYPath& output,
+ const TSortColumns& sortBy,
+ const TSortOperationSpec& spec = TSortOperationSpec(),
+ const TOperationOptions& options = TOperationOptions());
+
+ ///
+ /// @brief Run Merge operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/merge
+ virtual IOperationPtr Merge(
+ const TMergeOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run Erase operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/erase
+ virtual IOperationPtr Erase(
+ const TEraseOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run RemoteCopy operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/remote_copy
+ virtual IOperationPtr RemoteCopy(
+ const TRemoteCopyOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Run Vanilla operation.
+ ///
+ /// @param spec Operation spec.
+ /// @param options Optional parameters.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/description/mr/vanilla
+ virtual IOperationPtr RunVanilla(
+ const TVanillaOperationSpec& spec,
+ const TOperationOptions& options = TOperationOptions()) = 0;
+
+ ///
+ /// @brief Abort operation.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#abort_op
+ virtual void AbortOperation(
+ const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Complete operation.
+ ///
+ /// @see https://yt.yandex-team.ru/docs/api/commands#complete_op
+ virtual void CompleteOperation(
+ const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Wait for operation to finish.
+ virtual void WaitForOperation(
+ const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Check and return operation status.
+ ///
+ /// @note this function will never return @ref NYT::EOperationBriefState::Failed or @ref NYT::EOperationBriefState::Aborted status,
+ /// it will throw @ref NYT::TOperationFailedError instead.
+ virtual EOperationBriefState CheckOperation(
+ const TOperationId& operationId) = 0;
+
+ ///
+ /// @brief Create an operation object given operation id.
+ ///
+ /// @throw @ref NYT::TErrorResponse if the operation doesn't exist.
+ virtual IOperationPtr AttachOperation(const TOperationId& operationId) = 0;
+
+private:
+ virtual IOperationPtr DoMap(
+ const TMapOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ const TOperationOptions& options) = 0;
+
+ virtual IOperationPtr DoReduce(
+ const TReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) = 0;
+
+ virtual IOperationPtr DoJoinReduce(
+ const TJoinReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) = 0;
+
+ virtual IOperationPtr DoMapReduce(
+ const TMapReduceOperationSpec& spec,
+ ::TIntrusivePtr<IStructuredJob> mapper,
+ ::TIntrusivePtr<IStructuredJob> reduceCombiner,
+ ::TIntrusivePtr<IStructuredJob> reducer,
+ const TOperationOptions& options) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define OPERATION_INL_H_
+#include "operation-inl.h"
+#undef OPERATION_INL_H_
diff --git a/yt/cpp/mapreduce/interface/operation_ut.cpp b/yt/cpp/mapreduce/interface/operation_ut.cpp
new file mode 100644
index 0000000000..0fa62e1568
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/operation_ut.cpp
@@ -0,0 +1,269 @@
+#include <yt/cpp/mapreduce/interface/common_ut.h>
+#include <yt/cpp/mapreduce/interface/job_statistics.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/protobuf_table_schema_ut.pb.h>
+
+#include <yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYT;
+using namespace NYT::NUnitTesting;
+
+class TDummyInferenceContext
+ : public IOperationPreparationContext
+{
+public:
+ TDummyInferenceContext(int inputCount, int outputCount)
+ : InputCount_(inputCount)
+ , OutputCount_(outputCount)
+ , InputSchemas_(inputCount)
+ { }
+
+ int GetInputCount() const override
+ {
+ return InputCount_;
+ }
+
+ int GetOutputCount() const override
+ {
+ return OutputCount_;
+ }
+
+ const TVector<TTableSchema>& GetInputSchemas() const override
+ {
+ return InputSchemas_;
+ }
+
+ const TTableSchema& GetInputSchema(int index) const override
+ {
+ return InputSchemas_[index];
+ }
+
+ TMaybe<TYPath> GetInputPath(int) const override
+ {
+ return Nothing();
+ }
+
+ TMaybe<TYPath> GetOutputPath(int) const override
+ {
+ return Nothing();
+ }
+
+private:
+ int InputCount_;
+ int OutputCount_;
+ TVector<TTableSchema> InputSchemas_;
+};
+
+Y_UNIT_TEST_SUITE(PrepareOperation)
+{
+
+ Y_UNIT_TEST(BasicSchemas)
+ {
+ auto firstSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("some_column").Type(EValueType::VT_UINT64));
+ auto otherSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("other_column").Type(EValueType::VT_BOOLEAN));
+ auto thirdSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("third_column").Type(EValueType::VT_STRING));
+
+ TDummyInferenceContext context(3,7);
+ TJobOperationPreparer builder(context);
+
+ builder
+ .OutputSchema(1, firstSchema)
+ .BeginOutputGroup(TVector<int>{2, 5})
+ .Schema(otherSchema)
+ .EndOutputGroup()
+ .BeginOutputGroup(3, 5)
+ .Schema(thirdSchema)
+ .EndOutputGroup()
+ .BeginOutputGroup(TVector<int>{0, 6})
+ .Schema(thirdSchema)
+ .EndOutputGroup();
+
+ UNIT_ASSERT_EXCEPTION(builder.OutputSchema(1, otherSchema), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(builder.BeginOutputGroup(3, 5).Schema(otherSchema), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(builder.BeginOutputGroup(TVector<int>{3,6,7}).Schema(otherSchema), TApiUsageError);
+
+ builder.Finish();
+ auto result = builder.GetOutputSchemas();
+
+ ASSERT_SERIALIZABLES_EQUAL(result[0], thirdSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[1], firstSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[2], otherSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[3], thirdSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[4], thirdSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[5], otherSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[6], thirdSchema);
+ }
+
+ Y_UNIT_TEST(NoSchema)
+ {
+ auto schema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("some_column").Type(EValueType::VT_UINT64));
+
+ TDummyInferenceContext context(3,4);
+ TJobOperationPreparer builder(context);
+
+ builder
+ .OutputSchema(1, schema)
+ .NoOutputSchema(0)
+ .BeginOutputGroup(2, 4)
+ .Schema(schema)
+ .EndOutputGroup();
+
+ UNIT_ASSERT_EXCEPTION(builder.OutputSchema(0, schema), TApiUsageError);
+
+ builder.Finish();
+ auto result = builder.GetOutputSchemas();
+
+ UNIT_ASSERT(result[0].Empty());
+
+ ASSERT_SERIALIZABLES_EQUAL(result[1], schema);
+ ASSERT_SERIALIZABLES_EQUAL(result[2], schema);
+ ASSERT_SERIALIZABLES_EQUAL(result[3], schema);
+ }
+
+ Y_UNIT_TEST(Descriptions)
+ {
+ auto urlRowSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("Host").Type(NTi::Optional(NTi::String())))
+ .AddColumn(TColumnSchema().Name("Path").Type(NTi::Optional(NTi::String())))
+ .AddColumn(TColumnSchema().Name("HttpCode").Type(NTi::Optional(NTi::Int32())));
+
+ auto urlRowStruct = NTi::Struct({
+ {"Host", NTi::Optional(NTi::String())},
+ {"Path", NTi::Optional(NTi::String())},
+ {"HttpCode", NTi::Optional(NTi::Int32())},
+ });
+
+ auto rowFieldSerializationOptionSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(NTi::Optional(urlRowStruct)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(NTi::Optional(NTi::String())));
+
+ auto rowSerializedRepeatedFieldsSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("Ints").Type(NTi::List(NTi::Int64())))
+ .AddColumn(TColumnSchema().Name("UrlRows").Type(NTi::List(urlRowStruct)));
+
+ TDummyInferenceContext context(5,7);
+ TJobOperationPreparer builder(context);
+
+ builder
+ .InputDescription<TUrlRow>(0)
+ .BeginInputGroup(2, 3)
+ .Description<TUrlRow>()
+ .EndInputGroup()
+ .BeginInputGroup(TVector<int>{1, 4})
+ .Description<TRowSerializedRepeatedFields>()
+ .EndInputGroup()
+ .InputDescription<TUrlRow>(3);
+
+ UNIT_ASSERT_EXCEPTION(builder.InputDescription<TUrlRow>(0), TApiUsageError);
+
+ builder
+ .OutputDescription<TUrlRow>(0, false)
+ .OutputDescription<TRowFieldSerializationOption>(1)
+ .BeginOutputGroup(2, 4)
+ .Description<TUrlRow>()
+ .EndOutputGroup()
+ .BeginOutputGroup(TVector<int>{4,6})
+ .Description<TRowSerializedRepeatedFields>()
+ .EndOutputGroup()
+ .OutputDescription<TUrlRow>(5, false);
+
+ UNIT_ASSERT_EXCEPTION(builder.OutputDescription<TUrlRow>(0), TApiUsageError);
+ UNIT_ASSERT_NO_EXCEPTION(builder.OutputSchema(0, urlRowSchema));
+ UNIT_ASSERT_NO_EXCEPTION(builder.OutputSchema(5, urlRowSchema));
+ UNIT_ASSERT_EXCEPTION(builder.OutputSchema(1, urlRowSchema), TApiUsageError);
+
+ builder.Finish();
+ auto result = builder.GetOutputSchemas();
+
+ ASSERT_SERIALIZABLES_EQUAL(result[0], urlRowSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[1], rowFieldSerializationOptionSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[2], urlRowSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[3], urlRowSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[4], rowSerializedRepeatedFieldsSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[5], urlRowSchema);
+ ASSERT_SERIALIZABLES_EQUAL(result[6], rowSerializedRepeatedFieldsSchema);
+
+ auto expectedInputDescriptions = TVector<TMaybe<TTableStructure>>{
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TRowSerializedRepeatedFields::descriptor()}},
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TRowSerializedRepeatedFields::descriptor()}},
+ };
+ UNIT_ASSERT_EQUAL(expectedInputDescriptions, builder.GetInputDescriptions());
+
+ auto expectedOutputDescriptions = TVector<TMaybe<TTableStructure>>{
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TRowFieldSerializationOption::descriptor()}},
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TRowSerializedRepeatedFields::descriptor()}},
+ {TProtobufTableStructure{TUrlRow::descriptor()}},
+ {TProtobufTableStructure{TRowSerializedRepeatedFields::descriptor()}},
+ };
+ UNIT_ASSERT_EQUAL(expectedOutputDescriptions, builder.GetOutputDescriptions());
+ }
+
+ Y_UNIT_TEST(InputColumns)
+ {
+ TDummyInferenceContext context(5, 1);
+ TJobOperationPreparer builder(context);
+ builder
+ .InputColumnFilter(2, {"a", "b"})
+ .BeginInputGroup(0, 2)
+ .ColumnFilter({"b", "c"})
+ .ColumnRenaming({{"b", "B"}, {"c", "C"}})
+ .EndInputGroup()
+ .InputColumnRenaming(3, {{"a", "AAA"}})
+ .NoOutputSchema(0);
+ builder.Finish();
+
+ auto expectedRenamings = TVector<THashMap<TString, TString>>{
+ {{"b", "B"}, {"c", "C"}},
+ {{"b", "B"}, {"c", "C"}},
+ {},
+ {{"a", "AAA"}},
+ {},
+ };
+ UNIT_ASSERT_EQUAL(builder.GetInputColumnRenamings(), expectedRenamings);
+
+ auto expectedFilters = TVector<TMaybe<TVector<TString>>>{
+ {{"b", "c"}},
+ {{"b", "c"}},
+ {{"a", "b"}},
+ {},
+ {},
+ };
+ UNIT_ASSERT_EQUAL(builder.GetInputColumnFilters(), expectedFilters);
+ }
+
+ Y_UNIT_TEST(Bug_r7349102)
+ {
+ auto firstSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("some_column").Type(EValueType::VT_UINT64));
+ auto otherSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("other_column").Type(EValueType::VT_BOOLEAN));
+ auto thirdSchema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("third_column").Type(EValueType::VT_STRING));
+
+ TDummyInferenceContext context(3,1);
+ TJobOperationPreparer builder(context);
+
+ builder
+ .InputDescription<TUrlRow>(0)
+ .InputDescription<TUrlRow>(1)
+ .InputDescription<TUrlRow>(2)
+ .OutputDescription<TUrlRow>(0);
+
+ builder.Finish();
+ }
+
+} // Y_UNIT_TEST_SUITE(SchemaInference)
diff --git a/yt/cpp/mapreduce/interface/proto3_ut.proto b/yt/cpp/mapreduce/interface/proto3_ut.proto
new file mode 100644
index 0000000000..b24c13085b
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/proto3_ut.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+import "yt/yt_proto/yt/formats/extension.proto";
+
+package NYT.NTestingProto3;
+
+option (NYT.file_default_field_flags) = SERIALIZATION_YT;
+
+message TWithOptional
+{
+ optional int64 x = 1;
+}
+
+message TWithOptionalMessage
+{
+ optional TWithOptional x = 1;
+}
diff --git a/yt/cpp/mapreduce/interface/protobuf_file_options_ut.cpp b/yt/cpp/mapreduce/interface/protobuf_file_options_ut.cpp
new file mode 100644
index 0000000000..5ffa9564d7
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_file_options_ut.cpp
@@ -0,0 +1,271 @@
+#include "errors.h"
+#include "format.h"
+#include "common_ut.h"
+
+#include <yt/cpp/mapreduce/interface/protobuf_file_options_ut.pb.h>
+
+#include <yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NYT;
+
+Y_UNIT_TEST_SUITE(ProtobufFileOptions)
+{
+ NTi::TTypePtr GetUrlRowType(bool required)
+ {
+ static const NTi::TTypePtr structType = NTi::Struct({
+ {"Host", ToTypeV3(EValueType::VT_STRING, false)},
+ {"Path", ToTypeV3(EValueType::VT_STRING, false)},
+ {"HttpCode", ToTypeV3(EValueType::VT_INT32, false)}});
+ return required ? structType : NTi::TTypePtr(NTi::Optional(structType));
+ }
+
+ Y_UNIT_TEST(TRowFieldSerializationOption)
+ {
+ const auto schema = CreateTableSchema<NTestingFileOptions::TRowFieldSerializationOption>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(GetUrlRowType(false))));
+ }
+
+ Y_UNIT_TEST(TRowMixedSerializationOptions)
+ {
+ const auto schema = CreateTableSchema<NTestingFileOptions::TRowMixedSerializationOptions>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(GetUrlRowType(false))));
+ }
+
+ Y_UNIT_TEST(FieldSortOrder)
+ {
+ const auto schema = CreateTableSchema<NTestingFileOptions::TFieldSortOrder>();
+
+ auto asInProtoFile = NTi::Optional(NTi::Struct({
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ {"z", NTi::Optional(NTi::Bool())},
+ }));
+ auto byFieldNumber = NTi::Optional(NTi::Struct({
+ {"z", NTi::Optional(NTi::Bool())},
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ }));
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("EmbeddedDefault").Type(asInProtoFile))
+ .AddColumn(TColumnSchema().Name("EmbeddedAsInProtoFile").Type(asInProtoFile))
+ .AddColumn(TColumnSchema().Name("EmbeddedByFieldNumber").Type(byFieldNumber)));
+ }
+
+ Y_UNIT_TEST(Map)
+ {
+ const auto schema = CreateTableSchema<NTestingFileOptions::TWithMap>();
+
+ auto createKeyValueStruct = [] (NTi::TTypePtr key, NTi::TTypePtr value) {
+ return NTi::List(NTi::Struct({
+ {"key", NTi::Optional(key)},
+ {"value", NTi::Optional(value)},
+ }));
+ };
+
+ auto embedded = NTi::Struct({
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ });
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("MapDefault")
+ .Type(createKeyValueStruct(NTi::Int64(), embedded)))
+ .AddColumn(TColumnSchema()
+ .Name("MapDict")
+ .Type(NTi::Dict(NTi::Int64(), embedded))));
+ }
+
+ Y_UNIT_TEST(Oneof)
+ {
+ const auto schema = CreateTableSchema<NTestingFileOptions::TWithOneof>();
+
+ auto embedded = NTi::Struct({
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ });
+
+ auto defaultVariantType = NTi::Optional(NTi::Struct({
+ {"field", NTi::Optional(NTi::String())},
+ {"Oneof2", NTi::Optional(NTi::Variant(NTi::Struct({
+ {"y2", NTi::String()},
+ {"z2", embedded},
+ {"x2", NTi::Int64()},
+ })))},
+ {"x1", NTi::Optional(NTi::Int64())},
+ {"y1", NTi::Optional(NTi::String())},
+ {"z1", NTi::Optional(embedded)},
+ }));
+
+ auto noDefaultType = NTi::Optional(NTi::Struct({
+ {"field", NTi::Optional(NTi::String())},
+ {"y2", NTi::Optional(NTi::String())},
+ {"z2", NTi::Optional(embedded)},
+ {"x2", NTi::Optional(NTi::Int64())},
+ {"x1", NTi::Optional(NTi::Int64())},
+ {"y1", NTi::Optional(NTi::String())},
+ {"z1", NTi::Optional(embedded)},
+ }));
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("DefaultVariant")
+ .Type(defaultVariantType)
+ )
+ .AddColumn(TColumnSchema()
+ .Name("NoDefault")
+ .Type(noDefaultType)
+ )
+ .AddColumn(TColumnSchema()
+ .Name("SerializationProtobuf")
+ .Type(NTi::Optional(NTi::Struct({
+ {"x1", NTi::Optional(NTi::Int64())},
+ {"y1", NTi::Optional(NTi::String())},
+ {"z1", NTi::Optional(NTi::String())},
+ })))
+ )
+ .AddColumn(TColumnSchema()
+ .Name("MemberOfTopLevelOneof")
+ .Type(NTi::Optional(NTi::Int64()))
+ )
+ );
+ }
+}
+
+static TNode GetColumns(const TFormat& format, int tableIndex = 0)
+{
+ return format.Config.GetAttributes()["tables"][tableIndex]["columns"];
+}
+
+Y_UNIT_TEST_SUITE(ProtobufFormatFileOptions)
+{
+ Y_UNIT_TEST(TRowFieldSerializationOption)
+ {
+ const auto format = TFormat::Protobuf<NTestingFileOptions::TRowFieldSerializationOption>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["name"], "UrlRow_1");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["proto_type"], "message");
+ UNIT_ASSERT_VALUES_EQUAL(columns[0]["field_number"], 1);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["name"], "UrlRow_2");
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(columns[1]["field_number"], 2);
+ const auto& fields = columns[1]["fields"];
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["name"], "Host");
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["proto_type"], "string");
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["field_number"], 1);
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["name"], "Path");
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["proto_type"], "string");
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["field_number"], 2);
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["name"], "HttpCode");
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["proto_type"], "sint32");
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["field_number"], 3);
+ }
+
+ Y_UNIT_TEST(Map)
+ {
+ const auto format = TFormat::Protobuf<NTestingFileOptions::TWithMap>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns.Size(), 2);
+ {
+ const auto& column = columns[0];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapDefault");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "structured_message");
+ }
+ {
+ const auto& column = columns[1];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MapDict");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 2);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["proto_type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["proto_type"], "structured_message");
+ }
+ }
+
+ Y_UNIT_TEST(Oneof)
+ {
+ const auto format = TFormat::Protobuf<NTestingFileOptions::TWithOneof>();
+ auto columns = GetColumns(format);
+
+ UNIT_ASSERT_VALUES_EQUAL(columns.Size(), 4);
+
+ {
+ const auto& column = columns[0];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "DefaultVariant");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 5);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["name"], "field");
+
+ const auto& oneof2 = column["fields"][1];
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["name"], "Oneof2");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["proto_type"], "oneof");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][0]["name"], "y2");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][1]["name"], "z2");
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][1]["proto_type"], "structured_message");
+ const auto& embeddedFields = oneof2["fields"][1]["fields"];
+ UNIT_ASSERT_VALUES_EQUAL(embeddedFields[0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(embeddedFields[1]["name"], "y");
+
+ UNIT_ASSERT_VALUES_EQUAL(oneof2["fields"][2]["name"], "x2");
+
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][2]["name"], "x1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][3]["name"], "y1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][4]["name"], "z1");
+ };
+
+ {
+ const auto& column = columns[1];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "NoDefault");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ const auto& fields = column["fields"];
+ UNIT_ASSERT_VALUES_EQUAL(fields.Size(), 7);
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[0]["name"], "field");
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[1]["name"], "y2");
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["name"], "z2");
+ UNIT_ASSERT_VALUES_EQUAL(fields[2]["proto_type"], "structured_message");
+ const auto& embeddedFields = fields[2]["fields"];
+ UNIT_ASSERT_VALUES_EQUAL(embeddedFields[0]["name"], "x");
+ UNIT_ASSERT_VALUES_EQUAL(embeddedFields[1]["name"], "y");
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[3]["name"], "x2");
+
+ UNIT_ASSERT_VALUES_EQUAL(fields[4]["name"], "x1");
+ UNIT_ASSERT_VALUES_EQUAL(fields[5]["name"], "y1");
+ UNIT_ASSERT_VALUES_EQUAL(fields[6]["name"], "z1");
+ };
+
+ {
+ const auto& column = columns[2];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "SerializationProtobuf");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "structured_message");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"].Size(), 3);
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][0]["name"], "x1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][1]["name"], "y1");
+ UNIT_ASSERT_VALUES_EQUAL(column["fields"][2]["name"], "z1");
+ }
+ {
+ const auto& column = columns[3];
+ UNIT_ASSERT_VALUES_EQUAL(column["name"], "MemberOfTopLevelOneof");
+ UNIT_ASSERT_VALUES_EQUAL(column["proto_type"], "int64");
+ }
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/protobuf_file_options_ut.proto b/yt/cpp/mapreduce/interface/protobuf_file_options_ut.proto
new file mode 100644
index 0000000000..4804b2f60c
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_file_options_ut.proto
@@ -0,0 +1,142 @@
+import "yt/yt_proto/yt/formats/extension.proto";
+
+package NYT.NTestingFileOptions;
+
+option (NYT.file_default_field_flags) = SERIALIZATION_YT;
+option (NYT.file_default_field_flags) = MAP_AS_LIST_OF_STRUCTS;
+option (NYT.file_default_message_flags) = DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE;
+option (NYT.file_default_oneof_flags) = SEPARATE_FIELDS;
+
+message TUrlRow
+{
+ optional string Host = 1 [(NYT.column_name) = "Host"];
+ optional string Path = 2 [(NYT.column_name) = "Path"];
+ optional sint32 HttpCode = 3 [(NYT.column_name) = "HttpCode"];
+}
+
+message TRowFieldSerializationOption
+{
+ optional TUrlRow UrlRow_1 = 1 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+ optional TUrlRow UrlRow_2 = 2;
+}
+
+message TRowMixedSerializationOptions
+{
+ option (NYT.default_field_flags) = SERIALIZATION_PROTOBUF;
+ optional TUrlRow UrlRow_1 = 1;
+ optional TUrlRow UrlRow_2 = 2 [(NYT.flags) = SERIALIZATION_YT];
+}
+
+message TRowSerializedRepeatedFields
+{
+ repeated int64 Ints = 1;
+ repeated TUrlRow UrlRows = 2;
+}
+
+message TFieldSortOrder
+{
+ message TEmbeddedDefault {
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ message TEmbeddedAsInProtoFile {
+ option (NYT.message_flags) = DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE;
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ message TEmbeddedByFieldNumber {
+ option (NYT.message_flags) = SORT_FIELDS_BY_FIELD_NUMBER;
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional TEmbeddedDefault EmbeddedDefault = 1;
+ optional TEmbeddedAsInProtoFile EmbeddedAsInProtoFile = 2;
+ optional TEmbeddedByFieldNumber EmbeddedByFieldNumber = 3;
+}
+
+message TWithMap
+{
+ message TEmbedded {
+ optional int64 x = 1;
+ optional string y = 2;
+ }
+
+ map<int64, TEmbedded> MapDefault = 1;
+ map<int64, TEmbedded> MapDict = 5 [(NYT.flags) = MAP_AS_DICT];
+}
+
+message TWithOneof
+{
+ message TEmbedded
+ {
+ oneof Oneof {
+ int64 x = 1;
+ string y = 2;
+ }
+ }
+
+ message TDefaultVariant
+ {
+ option (NYT.default_oneof_flags) = VARIANT;
+ optional string field = 1;
+
+ oneof Oneof2
+ {
+ string y2 = 4;
+ TEmbedded z2 = 6;
+ int64 x2 = 2;
+ }
+
+ oneof Oneof1
+ {
+ option (NYT.oneof_flags) = SEPARATE_FIELDS;
+ int64 x1 = 10;
+ string y1 = 3;
+ TEmbedded z1 = 5;
+ }
+ }
+
+ message TNoDefault
+ {
+ optional string field = 1;
+
+ oneof Oneof2
+ {
+ string y2 = 4;
+ TEmbedded z2 = 6;
+ int64 x2 = 2;
+ }
+
+ oneof Oneof1
+ {
+ int64 x1 = 10;
+ string y1 = 3;
+ TEmbedded z1 = 5;
+ }
+ }
+
+ message TSerializationProtobuf
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_PROTOBUF;
+ oneof Oneof
+ {
+ int64 x1 = 2;
+ string y1 = 1;
+ TEmbedded z1 = 3;
+ }
+ }
+
+ optional TDefaultVariant DefaultVariant = 1;
+ optional TNoDefault NoDefault = 2;
+ optional TSerializationProtobuf SerializationProtobuf = 3;
+
+ oneof TopLevelOneof
+ {
+ int64 MemberOfTopLevelOneof = 4;
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/protobuf_format.cpp b/yt/cpp/mapreduce/interface/protobuf_format.cpp
new file mode 100644
index 0000000000..3d57ed2797
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_format.cpp
@@ -0,0 +1,1498 @@
+#include "protobuf_format.h"
+
+#include "errors.h"
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <google/protobuf/text_format.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/hash_set.h>
+#include <util/generic/stack.h>
+#include <util/generic/overloaded.h>
+
+#include <util/stream/output.h>
+#include <util/stream/file.h>
+
+namespace NYT::NDetail {
+
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::DescriptorProto;
+using ::google::protobuf::EnumDescriptor;
+using ::google::protobuf::EnumDescriptorProto;
+using ::google::protobuf::FieldDescriptor;
+using ::google::protobuf::FieldDescriptorProto;
+using ::google::protobuf::OneofDescriptor;
+using ::google::protobuf::Message;
+using ::google::protobuf::FileDescriptor;
+using ::google::protobuf::FileDescriptorProto;
+using ::google::protobuf::FileDescriptorSet;
+using ::google::protobuf::FieldOptions;
+using ::google::protobuf::FileOptions;
+using ::google::protobuf::OneofOptions;
+using ::google::protobuf::MessageOptions;
+
+using ::ToString;
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TOneofOption = std::variant<
+ EProtobufOneofMode>;
+
+using TFieldOption = std::variant<
+ EProtobufType,
+ EProtobufSerializationMode,
+ EProtobufListMode,
+ EProtobufMapMode,
+ EProtobufEnumWritingMode>;
+
+using TMessageOption = std::variant<
+ EProtobufFieldSortOrder>;
+
+struct TOtherColumns
+{ };
+
+using TValueTypeOrOtherColumns = std::variant<EValueType, TOtherColumns>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFieldOption FieldFlagToOption(EWrapperFieldFlag::Enum flag)
+{
+ using EFlag = EWrapperFieldFlag;
+ switch (flag) {
+ case EFlag::SERIALIZATION_PROTOBUF:
+ return EProtobufSerializationMode::Protobuf;
+ case EFlag::SERIALIZATION_YT:
+ return EProtobufSerializationMode::Yt;
+
+ case EFlag::ANY:
+ return EProtobufType::Any;
+ case EFlag::OTHER_COLUMNS:
+ return EProtobufType::OtherColumns;
+ case EFlag::ENUM_INT:
+ return EProtobufType::EnumInt;
+ case EFlag::ENUM_STRING:
+ return EProtobufType::EnumString;
+
+ case EFlag::OPTIONAL_LIST:
+ return EProtobufListMode::Optional;
+ case EFlag::REQUIRED_LIST:
+ return EProtobufListMode::Required;
+
+ case EFlag::MAP_AS_LIST_OF_STRUCTS_LEGACY:
+ return EProtobufMapMode::ListOfStructsLegacy;
+ case EFlag::MAP_AS_LIST_OF_STRUCTS:
+ return EProtobufMapMode::ListOfStructs;
+ case EFlag::MAP_AS_DICT:
+ return EProtobufMapMode::Dict;
+ case EFlag::MAP_AS_OPTIONAL_DICT:
+ return EProtobufMapMode::OptionalDict;
+ case EFlag::EMBEDDED:
+ return EProtobufSerializationMode::Embedded;
+
+ case EFlag::ENUM_SKIP_UNKNOWN_VALUES:
+ return EProtobufEnumWritingMode::SkipUnknownValues;
+ case EFlag::ENUM_CHECK_VALUES:
+ return EProtobufEnumWritingMode::CheckValues;
+ }
+ Y_FAIL();
+}
+
+TMessageOption MessageFlagToOption(EWrapperMessageFlag::Enum flag)
+{
+ using EFlag = EWrapperMessageFlag;
+ switch (flag) {
+ case EFlag::DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE:
+ return EProtobufFieldSortOrder::AsInProtoFile;
+ case EFlag::SORT_FIELDS_BY_FIELD_NUMBER:
+ return EProtobufFieldSortOrder::ByFieldNumber;
+ }
+ Y_FAIL();
+}
+
+TOneofOption OneofFlagToOption(EWrapperOneofFlag::Enum flag)
+{
+ using EFlag = EWrapperOneofFlag;
+ switch (flag) {
+ case EFlag::SEPARATE_FIELDS:
+ return EProtobufOneofMode::SeparateFields;
+ case EFlag::VARIANT:
+ return EProtobufOneofMode::Variant;
+ }
+ Y_FAIL();
+}
+
+EWrapperFieldFlag::Enum OptionToFieldFlag(TFieldOption option)
+{
+ using EFlag = EWrapperFieldFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (EProtobufType type)
+ {
+ switch (type) {
+ case EProtobufType::Any:
+ return EFlag::ANY;
+ case EProtobufType::OtherColumns:
+ return EFlag::OTHER_COLUMNS;
+ case EProtobufType::EnumInt:
+ return EFlag::ENUM_INT;
+ case EProtobufType::EnumString:
+ return EFlag::ENUM_STRING;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufSerializationMode serializationMode)
+ {
+ switch (serializationMode) {
+ case EProtobufSerializationMode::Yt:
+ return EFlag::SERIALIZATION_YT;
+ case EProtobufSerializationMode::Protobuf:
+ return EFlag::SERIALIZATION_PROTOBUF;
+ case EProtobufSerializationMode::Embedded:
+ return EFlag::EMBEDDED;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufListMode listMode)
+ {
+ switch (listMode) {
+ case EProtobufListMode::Optional:
+ return EFlag::OPTIONAL_LIST;
+ case EProtobufListMode::Required:
+ return EFlag::REQUIRED_LIST;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufMapMode mapMode)
+ {
+ switch (mapMode) {
+ case EProtobufMapMode::ListOfStructsLegacy:
+ return EFlag::MAP_AS_LIST_OF_STRUCTS_LEGACY;
+ case EProtobufMapMode::ListOfStructs:
+ return EFlag::MAP_AS_LIST_OF_STRUCTS;
+ case EProtobufMapMode::Dict:
+ return EFlag::MAP_AS_DICT;
+ case EProtobufMapMode::OptionalDict:
+ return EFlag::MAP_AS_OPTIONAL_DICT;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufEnumWritingMode enumWritingMode)
+ {
+ switch (enumWritingMode) {
+ case EProtobufEnumWritingMode::SkipUnknownValues:
+ return EFlag::ENUM_SKIP_UNKNOWN_VALUES;
+ case EProtobufEnumWritingMode::CheckValues:
+ return EFlag::ENUM_CHECK_VALUES;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return std::visit(TVisitor(), option);
+}
+
+EWrapperMessageFlag::Enum OptionToMessageFlag(TMessageOption option)
+{
+ using EFlag = EWrapperMessageFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (EProtobufFieldSortOrder sortOrder)
+ {
+ switch (sortOrder) {
+ case EProtobufFieldSortOrder::AsInProtoFile:
+ return EFlag::DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE;
+ case EProtobufFieldSortOrder::ByFieldNumber:
+ return EFlag::SORT_FIELDS_BY_FIELD_NUMBER;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return std::visit(TVisitor(), option);
+}
+
+EWrapperOneofFlag::Enum OptionToOneofFlag(TOneofOption option)
+{
+ using EFlag = EWrapperOneofFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (EProtobufOneofMode mode)
+ {
+ switch (mode) {
+ case EProtobufOneofMode::SeparateFields:
+ return EFlag::SEPARATE_FIELDS;
+ case EProtobufOneofMode::Variant:
+ return EFlag::VARIANT;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return std::visit(TVisitor(), option);
+}
+
+
+template <typename T, typename TOptionToFlag>
+void SetOption(TMaybe<T>& option, T newOption, TOptionToFlag optionToFlag)
+{
+ if (option) {
+ if (*option == newOption) {
+ ythrow yexception() << "Duplicate protobuf flag " << optionToFlag(newOption);
+ } else {
+ ythrow yexception() << "Incompatible protobuf flags " <<
+ optionToFlag(*option) << " and " << optionToFlag(newOption);
+ }
+ }
+ option = newOption;
+}
+
+class TParseProtobufFieldOptionsVisitor
+{
+public:
+ void operator() (EProtobufType type)
+ {
+ SetOption(Type, type);
+ }
+
+ void operator() (EProtobufSerializationMode serializationMode)
+ {
+ SetOption(SerializationMode, serializationMode);
+ }
+
+ void operator() (EProtobufListMode listMode)
+ {
+ SetOption(ListMode, listMode);
+ }
+
+ void operator() (EProtobufMapMode mapMode)
+ {
+ SetOption(MapMode, mapMode);
+ }
+
+ void operator() (EProtobufEnumWritingMode enumWritingMode)
+ {
+ SetOption(EnumWritingMode, enumWritingMode);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NDetail::SetOption(option, newOption, OptionToFieldFlag);
+ }
+
+public:
+ TMaybe<EProtobufType> Type;
+ TMaybe<EProtobufSerializationMode> SerializationMode;
+ TMaybe<EProtobufListMode> ListMode;
+ TMaybe<EProtobufMapMode> MapMode;
+ TMaybe<EProtobufEnumWritingMode> EnumWritingMode;
+};
+
+class TParseProtobufMessageOptionsVisitor
+{
+public:
+ void operator() (EProtobufFieldSortOrder fieldSortOrder)
+ {
+ SetOption(FieldSortOrder, fieldSortOrder);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NDetail::SetOption(option, newOption, OptionToMessageFlag);
+ }
+
+public:
+ TMaybe<EProtobufFieldSortOrder> FieldSortOrder;
+};
+
+class TParseProtobufOneofOptionsVisitor
+{
+public:
+ void operator() (EProtobufOneofMode mode)
+ {
+ SetOption(Mode, mode);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NDetail::SetOption(option, newOption, OptionToOneofFlag);
+ }
+
+public:
+ TMaybe<EProtobufOneofMode> Mode;
+};
+
+void ParseProtobufFieldOptions(
+ const ::google::protobuf::RepeatedField<EWrapperFieldFlag::Enum>& flags,
+ TProtobufFieldOptions* fieldOptions)
+{
+ TParseProtobufFieldOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, FieldFlagToOption(flag));
+ }
+ if (visitor.Type) {
+ fieldOptions->Type = *visitor.Type;
+ }
+ if (visitor.SerializationMode) {
+ fieldOptions->SerializationMode = *visitor.SerializationMode;
+ }
+ if (visitor.ListMode) {
+ fieldOptions->ListMode = *visitor.ListMode;
+ }
+ if (visitor.MapMode) {
+ fieldOptions->MapMode = *visitor.MapMode;
+ }
+}
+
+void ParseProtobufMessageOptions(
+ const ::google::protobuf::RepeatedField<EWrapperMessageFlag::Enum>& flags,
+ TProtobufMessageOptions* messageOptions)
+{
+ TParseProtobufMessageOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, MessageFlagToOption(flag));
+ }
+ if (visitor.FieldSortOrder) {
+ messageOptions->FieldSortOrder = *visitor.FieldSortOrder;
+ }
+}
+
+void ParseProtobufOneofOptions(
+ const ::google::protobuf::RepeatedField<EWrapperOneofFlag::Enum>& flags,
+ TProtobufOneofOptions* messageOptions)
+{
+ TParseProtobufOneofOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, OneofFlagToOption(flag));
+ }
+ if (visitor.Mode) {
+ messageOptions->Mode = *visitor.Mode;
+ }
+}
+
+TProtobufFieldOptions GetDefaultFieldOptions(
+ const Descriptor* descriptor,
+ TProtobufFieldOptions defaultFieldOptions = {})
+{
+ ParseProtobufFieldOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_field_flags),
+ &defaultFieldOptions);
+ ParseProtobufFieldOptions(
+ descriptor->options().GetRepeatedExtension(default_field_flags),
+ &defaultFieldOptions);
+ return defaultFieldOptions;
+}
+
+TProtobufOneofOptions GetDefaultOneofOptions(const Descriptor* descriptor)
+{
+ TProtobufOneofOptions defaultOneofOptions;
+ ParseProtobufOneofOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_oneof_flags),
+ &defaultOneofOptions);
+ ParseProtobufOneofOptions(
+ descriptor->options().GetRepeatedExtension(default_oneof_flags),
+ &defaultOneofOptions);
+ switch (defaultOneofOptions.Mode) {
+ case EProtobufOneofMode::Variant: {
+ auto defaultFieldOptions = GetDefaultFieldOptions(descriptor);
+ switch (defaultFieldOptions.SerializationMode) {
+ case EProtobufSerializationMode::Protobuf:
+ // For Protobuf serialization mode default is SeparateFields.
+ defaultOneofOptions.Mode = EProtobufOneofMode::SeparateFields;
+ return defaultOneofOptions;
+ case EProtobufSerializationMode::Yt:
+ case EProtobufSerializationMode::Embedded:
+ return defaultOneofOptions;
+ }
+ Y_FAIL();
+ }
+ case EProtobufOneofMode::SeparateFields:
+ return defaultOneofOptions;
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateProtobufType(const FieldDescriptor& fieldDescriptor, EProtobufType protobufType)
+{
+ const auto fieldType = fieldDescriptor.type();
+ auto ensureType = [&] (FieldDescriptor::Type expectedType) {
+ Y_ENSURE(fieldType == expectedType,
+ "Type of field " << fieldDescriptor.name() << "does not match specified field flag " <<
+ OptionToFieldFlag(protobufType) << ": "
+ "expected " << FieldDescriptor::TypeName(expectedType) << ", " <<
+ "got " << FieldDescriptor::TypeName(fieldType));
+ };
+ switch (protobufType) {
+ case EProtobufType::Any:
+ ensureType(FieldDescriptor::TYPE_BYTES);
+ return;
+ case EProtobufType::OtherColumns:
+ ensureType(FieldDescriptor::TYPE_BYTES);
+ return;
+ case EProtobufType::EnumInt:
+ ensureType(FieldDescriptor::TYPE_ENUM);
+ return;
+ case EProtobufType::EnumString:
+ ensureType(FieldDescriptor::TYPE_ENUM);
+ return;
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCycleChecker
+{
+private:
+ class TGuard
+ {
+ public:
+ TGuard(TCycleChecker* checker, const Descriptor* descriptor)
+ : Checker_(checker)
+ , Descriptor_(descriptor)
+ {
+ Checker_->ActiveVertices_.insert(Descriptor_);
+ Checker_->Stack_.push(Descriptor_);
+ }
+
+ ~TGuard()
+ {
+ Checker_->ActiveVertices_.erase(Descriptor_);
+ Checker_->Stack_.pop();
+ }
+
+ private:
+ TCycleChecker* Checker_;
+ const Descriptor* Descriptor_;
+ };
+
+public:
+ [[nodiscard]] TGuard Enter(const Descriptor* descriptor)
+ {
+ if (ActiveVertices_.contains(descriptor)) {
+ Y_VERIFY(!Stack_.empty());
+ ythrow TApiUsageError() << "Cyclic reference found for protobuf messages. " <<
+ "Consider removing " << EWrapperFieldFlag::SERIALIZATION_YT << " flag " <<
+ "somewhere on the cycle containing " <<
+ Stack_.top()->full_name() << " and " << descriptor->full_name();
+ }
+ return TGuard(this, descriptor);
+ }
+
+private:
+ THashSet<const Descriptor*> ActiveVertices_;
+ TStack<const Descriptor*> Stack_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtobufFieldOptions GetFieldOptions(
+ const FieldDescriptor* fieldDescriptor,
+ const TMaybe<TProtobufFieldOptions>& defaultFieldOptions)
+{
+ TProtobufFieldOptions options;
+ if (defaultFieldOptions) {
+ options = *defaultFieldOptions;
+ } else {
+ options = GetDefaultFieldOptions(fieldDescriptor->containing_type());
+ }
+ ParseProtobufFieldOptions(fieldDescriptor->options().GetRepeatedExtension(flags), &options);
+ return options;
+}
+
+TProtobufOneofOptions GetOneofOptions(
+ const OneofDescriptor* oneofDescriptor,
+ const TMaybe<TProtobufOneofOptions>& defaultOneofOptions)
+{
+ TProtobufOneofOptions options;
+ if (defaultOneofOptions) {
+ options = *defaultOneofOptions;
+ } else {
+ options = GetDefaultOneofOptions(oneofDescriptor->containing_type());
+ }
+ ParseProtobufOneofOptions(oneofDescriptor->options().GetRepeatedExtension(oneof_flags), &options);
+
+ if (oneofDescriptor->is_synthetic()) {
+ options.Mode = EProtobufOneofMode::SeparateFields;
+ }
+
+ auto variantFieldName = oneofDescriptor->options().GetExtension(variant_field_name);
+ switch (options.Mode) {
+ case EProtobufOneofMode::SeparateFields:
+ if (variantFieldName) {
+ ythrow TApiUsageError() << "\"variant_field_name\" requires (NYT.oneof_flags) = VARIANT";
+ }
+ break;
+ case EProtobufOneofMode::Variant:
+ if (variantFieldName) {
+ options.VariantFieldName = variantFieldName;
+ } else {
+ options.VariantFieldName = oneofDescriptor->name();
+ }
+ break;
+ }
+ return options;
+}
+
+
+TProtobufMessageOptions GetMessageOptions(const Descriptor* descriptor)
+{
+ TProtobufMessageOptions options;
+ ParseProtobufMessageOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_message_flags),
+ &options);
+ ParseProtobufMessageOptions(
+ descriptor->options().GetRepeatedExtension(message_flags),
+ &options);
+ return options;
+}
+
+TNode MakeEnumerationConfig(const ::google::protobuf::EnumDescriptor* enumDescriptor)
+{
+ auto config = TNode::CreateMap();
+ for (int i = 0; i < enumDescriptor->value_count(); ++i) {
+ config[enumDescriptor->value(i)->name()] = enumDescriptor->value(i)->number();
+ }
+ return config;
+}
+
+TString DeduceProtobufType(
+ const FieldDescriptor* fieldDescriptor,
+ const TProtobufFieldOptions& options)
+{
+ if (options.Type) {
+ ValidateProtobufType(*fieldDescriptor, *options.Type);
+ return ToString(*options.Type);
+ }
+ switch (fieldDescriptor->type()) {
+ case FieldDescriptor::TYPE_ENUM:
+ return ToString(EProtobufType::EnumString);
+ case FieldDescriptor::TYPE_MESSAGE:
+ switch (options.SerializationMode) {
+ case EProtobufSerializationMode::Protobuf:
+ return "message";
+ case EProtobufSerializationMode::Yt:
+ return "structured_message";
+ case EProtobufSerializationMode::Embedded:
+ return "embedded_message";
+ }
+ Y_FAIL();
+ default:
+ return fieldDescriptor->type_name();
+ }
+ Y_FAIL();
+}
+
+TString GetColumnName(const ::google::protobuf::FieldDescriptor& field)
+{
+ const auto& options = field.options();
+ const auto columnName = options.GetExtension(column_name);
+ if (!columnName.empty()) {
+ return columnName;
+ }
+ const auto keyColumnName = options.GetExtension(key_column_name);
+ if (!keyColumnName.empty()) {
+ return keyColumnName;
+ }
+ return field.name();
+}
+
+TNode MakeProtoFormatMessageFieldsConfig(
+ const Descriptor* descriptor,
+ TNode* enumerations,
+ TCycleChecker& cycleChecker);
+
+TNode MakeProtoFormatMessageFieldsConfig(
+ const Descriptor* descriptor,
+ TNode* enumerations,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions,
+ TCycleChecker& cycleChecker);
+
+TNode MakeMapFieldsConfig(
+ const FieldDescriptor* fieldDescriptor,
+ TNode* enumerations,
+ const TProtobufFieldOptions& fieldOptions,
+ TCycleChecker& cycleChecker)
+{
+ Y_VERIFY(fieldDescriptor->is_map());
+ auto message = fieldDescriptor->message_type();
+ switch (fieldOptions.MapMode) {
+ case EProtobufMapMode::ListOfStructsLegacy:
+ return MakeProtoFormatMessageFieldsConfig(
+ message,
+ enumerations,
+ cycleChecker);
+ case EProtobufMapMode::ListOfStructs:
+ case EProtobufMapMode::Dict:
+ case EProtobufMapMode::OptionalDict: {
+ TProtobufFieldOptions defaultFieldOptions;
+ defaultFieldOptions.SerializationMode = EProtobufSerializationMode::Yt;
+ return MakeProtoFormatMessageFieldsConfig(
+ message,
+ enumerations,
+ defaultFieldOptions,
+ TProtobufOneofOptions{},
+ cycleChecker);
+ }
+ }
+ Y_FAIL();
+}
+
+TNode MakeProtoFormatFieldConfig(
+ const FieldDescriptor* fieldDescriptor,
+ TNode* enumerations,
+ const TProtobufFieldOptions& defaultOptions,
+ TCycleChecker& cycleChecker)
+{
+ auto fieldConfig = TNode::CreateMap();
+ fieldConfig["field_number"] = fieldDescriptor->number();
+ fieldConfig["name"] = GetColumnName(*fieldDescriptor);
+
+ auto fieldOptions = GetFieldOptions(fieldDescriptor, defaultOptions);
+
+ Y_ENSURE(fieldOptions.SerializationMode != EProtobufSerializationMode::Embedded,
+ "EMBEDDED flag is currently supported only with "
+ "ProtobufFormatWithDescriptors config option set to true");
+
+ if (fieldDescriptor->is_repeated()) {
+ Y_ENSURE_EX(fieldOptions.SerializationMode == EProtobufSerializationMode::Yt,
+ TApiUsageError() << "Repeated field \"" << fieldDescriptor->full_name() << "\" " <<
+ "must have flag \"" << EWrapperFieldFlag::SERIALIZATION_YT << "\"");
+ }
+ fieldConfig["repeated"] = fieldDescriptor->is_repeated();
+ fieldConfig["packed"] = fieldDescriptor->is_packed();
+
+ fieldConfig["proto_type"] = DeduceProtobufType(fieldDescriptor, fieldOptions);
+
+ if (fieldDescriptor->type() == FieldDescriptor::TYPE_ENUM) {
+ auto* enumeration = fieldDescriptor->enum_type();
+ (*enumerations)[enumeration->full_name()] = MakeEnumerationConfig(enumeration);
+ fieldConfig["enumeration_name"] = enumeration->full_name();
+ }
+
+ if (fieldOptions.SerializationMode != EProtobufSerializationMode::Yt) {
+ return fieldConfig;
+ }
+
+ if (fieldDescriptor->is_map()) {
+ fieldConfig["fields"] = MakeMapFieldsConfig(fieldDescriptor, enumerations, fieldOptions, cycleChecker);
+ return fieldConfig;
+ }
+
+ if (fieldDescriptor->type() == FieldDescriptor::TYPE_MESSAGE) {
+ fieldConfig["fields"] = MakeProtoFormatMessageFieldsConfig(
+ fieldDescriptor->message_type(),
+ enumerations,
+ cycleChecker);
+ }
+
+ return fieldConfig;
+}
+
+void MakeProtoFormatOneofConfig(
+ const OneofDescriptor* oneofDescriptor,
+ TNode* enumerations,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions,
+ TCycleChecker& cycleChecker,
+ TNode* fields)
+{
+ auto addFields = [&] (TNode* fields) {
+ for (int i = 0; i < oneofDescriptor->field_count(); ++i) {
+ fields->Add(MakeProtoFormatFieldConfig(
+ oneofDescriptor->field(i),
+ enumerations,
+ defaultFieldOptions,
+ cycleChecker));
+ }
+ };
+
+ auto oneofOptions = GetOneofOptions(oneofDescriptor, defaultOneofOptions);
+ switch (oneofOptions.Mode) {
+ case EProtobufOneofMode::SeparateFields:
+ addFields(fields);
+ return;
+ case EProtobufOneofMode::Variant: {
+ auto oneofFields = TNode::CreateList();
+ addFields(&oneofFields);
+ auto oneofField = TNode()
+ ("proto_type", "oneof")
+ ("name", oneofOptions.VariantFieldName)
+ ("fields", std::move(oneofFields));
+ fields->Add(std::move(oneofField));
+ return;
+ }
+ }
+ Y_FAIL();
+}
+
+TNode MakeProtoFormatMessageFieldsConfig(
+ const Descriptor* descriptor,
+ TNode* enumerations,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions,
+ TCycleChecker& cycleChecker)
+{
+ auto fields = TNode::CreateList();
+ THashSet<const OneofDescriptor*> visitedOneofs;
+ auto guard = cycleChecker.Enter(descriptor);
+ for (int fieldIndex = 0; fieldIndex < descriptor->field_count(); ++fieldIndex) {
+ auto fieldDescriptor = descriptor->field(fieldIndex);
+ auto oneofDescriptor = fieldDescriptor->containing_oneof();
+ if (!oneofDescriptor) {
+ fields.Add(MakeProtoFormatFieldConfig(
+ fieldDescriptor,
+ enumerations,
+ defaultFieldOptions,
+ cycleChecker));
+ } else if (!visitedOneofs.contains(oneofDescriptor)) {
+ MakeProtoFormatOneofConfig(
+ oneofDescriptor,
+ enumerations,
+ defaultFieldOptions,
+ defaultOneofOptions,
+ cycleChecker,
+ &fields);
+ visitedOneofs.insert(oneofDescriptor);
+ }
+ }
+ return fields;
+}
+
+TNode MakeProtoFormatMessageFieldsConfig(
+ const Descriptor* descriptor,
+ TNode* enumerations,
+ TCycleChecker& cycleChecker)
+{
+ return MakeProtoFormatMessageFieldsConfig(
+ descriptor,
+ enumerations,
+ GetDefaultFieldOptions(descriptor),
+ GetDefaultOneofOptions(descriptor),
+ cycleChecker);
+}
+
+TNode MakeProtoFormatConfigWithTables(const TVector<const Descriptor*>& descriptors)
+{
+ TNode config("protobuf");
+ config.Attributes()
+ ("enumerations", TNode::CreateMap())
+ ("tables", TNode::CreateList());
+
+ auto& enumerations = config.Attributes()["enumerations"];
+
+ for (auto* descriptor : descriptors) {
+ TCycleChecker cycleChecker;
+ auto columns = MakeProtoFormatMessageFieldsConfig(descriptor, &enumerations, cycleChecker);
+ config.Attributes()["tables"].Add(
+ TNode()("columns", std::move(columns)));
+ }
+
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileDescriptorSetBuilder
+{
+public:
+ TFileDescriptorSetBuilder()
+ : ExtensionFile_(EWrapperFieldFlag::descriptor()->file())
+ { }
+
+ void AddDescriptor(const Descriptor* descriptor)
+ {
+ auto [it, inserted] = AllDescriptors_.insert(descriptor);
+ if (!inserted) {
+ return;
+ }
+
+ const auto* containingType = descriptor->containing_type();
+ while (containingType) {
+ AddDescriptor(containingType);
+ containingType = containingType->containing_type();
+ }
+ for (int i = 0; i < descriptor->field_count(); ++i) {
+ AddField(descriptor->field(i));
+ }
+ }
+
+ FileDescriptorSet Build()
+ {
+ THashSet<const FileDescriptor*> visitedFiles;
+ TVector<const FileDescriptor*> fileTopoOrder;
+ for (const auto* descriptor : AllDescriptors_) {
+ TraverseDependencies(descriptor->file(), visitedFiles, fileTopoOrder);
+ }
+
+ THashSet<TString> messageTypeNames;
+ THashSet<TString> enumTypeNames;
+ for (const auto* descriptor : AllDescriptors_) {
+ messageTypeNames.insert(descriptor->full_name());
+ }
+ for (const auto* enumDescriptor : EnumDescriptors_) {
+ enumTypeNames.insert(enumDescriptor->full_name());
+ }
+ FileDescriptorSet fileDescriptorSetProto;
+ for (const auto* file : fileTopoOrder) {
+ auto* fileProto = fileDescriptorSetProto.add_file();
+ file->CopyTo(fileProto);
+ Strip(fileProto, messageTypeNames, enumTypeNames);
+ }
+ return fileDescriptorSetProto;
+ }
+
+private:
+ void AddField(const FieldDescriptor* fieldDescriptor)
+ {
+ if (fieldDescriptor->message_type()) {
+ AddDescriptor(fieldDescriptor->message_type());
+ }
+ if (fieldDescriptor->enum_type()) {
+ AddEnumDescriptor(fieldDescriptor->enum_type());
+ }
+ }
+
+ void AddEnumDescriptor(const EnumDescriptor* enumDescriptor)
+ {
+ auto [it, inserted] = EnumDescriptors_.insert(enumDescriptor);
+ if (!inserted) {
+ return;
+ }
+ const auto* containingType = enumDescriptor->containing_type();
+ while (containingType) {
+ AddDescriptor(containingType);
+ containingType = containingType->containing_type();
+ }
+ }
+
+ void TraverseDependencies(
+ const FileDescriptor* current,
+ THashSet<const FileDescriptor*>& visited,
+ TVector<const FileDescriptor*>& topoOrder)
+ {
+ auto [it, inserted] = visited.insert(current);
+ if (!inserted) {
+ return;
+ }
+ for (int i = 0; i < current->dependency_count(); ++i) {
+ TraverseDependencies(current->dependency(i), visited, topoOrder);
+ }
+ topoOrder.push_back(current);
+ }
+
+ template <typename TOptions>
+ void StripUnknownOptions(TOptions* options)
+ {
+ std::vector<const FieldDescriptor*> fields;
+ auto reflection = options->GetReflection();
+ reflection->ListFields(*options, &fields);
+ for (auto field : fields) {
+ if (field->is_extension() && field->file() != ExtensionFile_) {
+ reflection->ClearField(options, field);
+ }
+ }
+ }
+
+ template <typename TRepeatedField, typename TPredicate>
+ void RemoveIf(TRepeatedField* repeatedField, TPredicate predicate)
+ {
+ repeatedField->erase(
+ std::remove_if(repeatedField->begin(), repeatedField->end(), predicate),
+ repeatedField->end());
+ }
+
+ void Strip(
+ const TString& containingTypePrefix,
+ DescriptorProto* messageProto,
+ const THashSet<TString>& messageTypeNames,
+ const THashSet<TString>& enumTypeNames)
+ {
+ const auto prefix = containingTypePrefix + messageProto->name() + '.';
+
+ RemoveIf(messageProto->mutable_nested_type(), [&] (const DescriptorProto& descriptorProto) {
+ return !messageTypeNames.contains(prefix + descriptorProto.name());
+ });
+ RemoveIf(messageProto->mutable_enum_type(), [&] (const EnumDescriptorProto& enumDescriptorProto) {
+ return !enumTypeNames.contains(prefix + enumDescriptorProto.name());
+ });
+
+ messageProto->clear_extension();
+ StripUnknownOptions(messageProto->mutable_options());
+ for (auto& fieldProto : *messageProto->mutable_field()) {
+ StripUnknownOptions(fieldProto.mutable_options());
+ }
+ for (auto& oneofProto : *messageProto->mutable_oneof_decl()) {
+ StripUnknownOptions(oneofProto.mutable_options());
+ }
+ for (auto& nestedTypeProto : *messageProto->mutable_nested_type()) {
+ Strip(prefix, &nestedTypeProto, messageTypeNames, enumTypeNames);
+ }
+ for (auto& enumProto : *messageProto->mutable_enum_type()) {
+ StripUnknownOptions(enumProto.mutable_options());
+ for (auto& enumValue : *enumProto.mutable_value()) {
+ StripUnknownOptions(enumValue.mutable_options());
+ }
+ }
+ }
+
+ void Strip(
+ FileDescriptorProto* fileProto,
+ const THashSet<TString>& messageTypeNames,
+ const THashSet<TString>& enumTypeNames)
+ {
+ const auto prefix = fileProto->package().Empty()
+ ? ""
+ : fileProto->package() + '.';
+
+ RemoveIf(fileProto->mutable_message_type(), [&] (const DescriptorProto& descriptorProto) {
+ return !messageTypeNames.contains(prefix + descriptorProto.name());
+ });
+ RemoveIf(fileProto->mutable_enum_type(), [&] (const EnumDescriptorProto& enumDescriptorProto) {
+ return !enumTypeNames.contains(prefix + enumDescriptorProto.name());
+ });
+
+ fileProto->clear_service();
+ fileProto->clear_extension();
+
+ StripUnknownOptions(fileProto->mutable_options());
+ for (auto& messageProto : *fileProto->mutable_message_type()) {
+ Strip(prefix, &messageProto, messageTypeNames, enumTypeNames);
+ }
+ for (auto& enumProto : *fileProto->mutable_enum_type()) {
+ StripUnknownOptions(enumProto.mutable_options());
+ for (auto& enumValue : *enumProto.mutable_value()) {
+ StripUnknownOptions(enumValue.mutable_options());
+ }
+ }
+ }
+
+private:
+ const FileDescriptor* const ExtensionFile_;
+ THashSet<const Descriptor*> AllDescriptors_;
+ THashSet<const EnumDescriptor*> EnumDescriptors_;
+};
+
+TNode MakeProtoFormatConfigWithDescriptors(const TVector<const Descriptor*>& descriptors)
+{
+ TFileDescriptorSetBuilder builder;
+ auto typeNames = TNode::CreateList();
+ for (const auto* descriptor : descriptors) {
+ builder.AddDescriptor(descriptor);
+ typeNames.Add(descriptor->full_name());
+ }
+
+ auto fileDescriptorSetText = builder.Build().ShortDebugString();
+ TNode config("protobuf");
+ config.Attributes()
+ ("file_descriptor_set_text", std::move(fileDescriptorSetText))
+ ("type_names", std::move(typeNames));
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TTypePtrOrOtherColumns = std::variant<NTi::TTypePtr, TOtherColumns>;
+
+struct TMember {
+ TString Name;
+ TTypePtrOrOtherColumns TypeOrOtherColumns;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TValueTypeOrOtherColumns GetScalarFieldType(
+ const FieldDescriptor& fieldDescriptor,
+ const TProtobufFieldOptions& options)
+{
+ if (options.Type) {
+ switch (*options.Type) {
+ case EProtobufType::EnumInt:
+ return EValueType::VT_INT64;
+ case EProtobufType::EnumString:
+ return EValueType::VT_STRING;
+ case EProtobufType::Any:
+ return EValueType::VT_ANY;
+ case EProtobufType::OtherColumns:
+ return TOtherColumns{};
+ }
+ Y_FAIL();
+ }
+
+ switch (fieldDescriptor.cpp_type()) {
+ case FieldDescriptor::CPPTYPE_INT32:
+ return EValueType::VT_INT32;
+ case FieldDescriptor::CPPTYPE_INT64:
+ return EValueType::VT_INT64;
+ case FieldDescriptor::CPPTYPE_UINT32:
+ return EValueType::VT_UINT32;
+ case FieldDescriptor::CPPTYPE_UINT64:
+ return EValueType::VT_UINT64;
+ case FieldDescriptor::CPPTYPE_FLOAT:
+ case FieldDescriptor::CPPTYPE_DOUBLE:
+ return EValueType::VT_DOUBLE;
+ case FieldDescriptor::CPPTYPE_BOOL:
+ return EValueType::VT_BOOLEAN;
+ case FieldDescriptor::CPPTYPE_STRING:
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ case FieldDescriptor::CPPTYPE_ENUM:
+ return EValueType::VT_STRING;
+ default:
+ ythrow yexception() <<
+ "Unexpected field type '" << fieldDescriptor.cpp_type_name() << "' " <<
+ "for field " << fieldDescriptor.name();
+ }
+}
+
+bool HasNameExtension(const FieldDescriptor& fieldDescriptor)
+{
+ const auto& options = fieldDescriptor.options();
+ return options.HasExtension(column_name) || options.HasExtension(key_column_name);
+}
+
+void SortFields(TVector<const FieldDescriptor*>& fieldDescriptors, EProtobufFieldSortOrder fieldSortOrder)
+{
+ switch (fieldSortOrder) {
+ case EProtobufFieldSortOrder::AsInProtoFile:
+ return;
+ case EProtobufFieldSortOrder::ByFieldNumber:
+ SortBy(fieldDescriptors, [] (const FieldDescriptor* fieldDescriptor) {
+ return fieldDescriptor->number();
+ });
+ return;
+ }
+ Y_FAIL();
+}
+
+NTi::TTypePtr CreateStruct(TStringBuf fieldName, TVector<TMember> members)
+{
+ TVector<NTi::TStructType::TOwnedMember> structMembers;
+ structMembers.reserve(members.size());
+ for (auto& member : members) {
+ std::visit(TOverloaded{
+ [&] (TOtherColumns) {
+ ythrow TApiUsageError() <<
+ "Could not deduce YT type for field " << member.Name << " of " <<
+ "embedded message field " << fieldName << " " <<
+ "(note that " << EWrapperFieldFlag::OTHER_COLUMNS << " fields " <<
+ "are not allowed inside embedded messages)";
+ },
+ [&] (NTi::TTypePtr& type) {
+ structMembers.emplace_back(std::move(member.Name), std::move(type));
+ },
+ }, member.TypeOrOtherColumns);
+ }
+ return NTi::Struct(std::move(structMembers));
+}
+
+TMaybe<TVector<TString>> InferColumnFilter(const ::google::protobuf::Descriptor& descriptor)
+{
+ auto isOtherColumns = [] (const ::google::protobuf::FieldDescriptor& field) {
+ return GetFieldOptions(&field).Type == EProtobufType::OtherColumns;
+ };
+
+ TVector<TString> result;
+ result.reserve(descriptor.field_count());
+ for (int i = 0; i < descriptor.field_count(); ++i) {
+ const auto& field = *descriptor.field(i);
+ if (isOtherColumns(field)) {
+ return {};
+ }
+ result.push_back(GetColumnName(field));
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableSchemaInferrer
+{
+public:
+ TTableSchemaInferrer(bool keepFieldsWithoutExtension)
+ : KeepFieldsWithoutExtension_(keepFieldsWithoutExtension)
+ { }
+
+ TTableSchema InferSchema(const Descriptor& messageDescriptor);
+
+private:
+ TTypePtrOrOtherColumns GetFieldType(
+ const FieldDescriptor& fieldDescriptor,
+ const TProtobufFieldOptions& defaultOptions);
+
+ void ProcessOneofField(
+ TStringBuf containingFieldName,
+ const OneofDescriptor& oneofDescriptor,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions,
+ EProtobufFieldSortOrder fieldSortOrder,
+ TVector<TMember>* members);
+
+ TVector<TMember> GetMessageMembers(
+ TStringBuf containingFieldName,
+ const Descriptor& fieldDescriptor,
+ TProtobufFieldOptions defaultFieldOptions,
+ std::optional<EProtobufFieldSortOrder> overrideFieldSortOrder = std::nullopt);
+
+ NTi::TTypePtr GetMessageType(
+ const FieldDescriptor& fieldDescriptor,
+ TProtobufFieldOptions defaultFieldOptions);
+
+ NTi::TTypePtr GetMapType(
+ const FieldDescriptor& fieldDescriptor,
+ const TProtobufFieldOptions& fieldOptions);
+
+private:
+ void GetMessageMembersImpl(
+ TStringBuf containingFieldName,
+ const Descriptor& fieldDescriptor,
+ TProtobufFieldOptions defaultFieldOptions,
+ std::optional<EProtobufFieldSortOrder> overrideFieldSortOrder,
+ TVector<TMember>* members);
+
+private:
+ const bool KeepFieldsWithoutExtension_;
+ TCycleChecker CycleChecker_;
+};
+
+void TTableSchemaInferrer::ProcessOneofField(
+ TStringBuf containingFieldName,
+ const OneofDescriptor& oneofDescriptor,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions,
+ EProtobufFieldSortOrder fieldSortOrder,
+ TVector<TMember>* members)
+{
+ auto oneofOptions = GetOneofOptions(&oneofDescriptor, defaultOneofOptions);
+
+ auto addFields = [&] (TVector<TMember>* members, bool removeOptionality) {
+ TVector<const FieldDescriptor*> fieldDescriptors;
+ for (int i = 0; i < oneofDescriptor.field_count(); ++i) {
+ fieldDescriptors.push_back(oneofDescriptor.field(i));
+ }
+ SortFields(fieldDescriptors, fieldSortOrder);
+ for (auto innerFieldDescriptor : fieldDescriptors) {
+ auto typeOrOtherColumns = GetFieldType(
+ *innerFieldDescriptor,
+ defaultFieldOptions);
+ if (auto* maybeType = std::get_if<NTi::TTypePtr>(&typeOrOtherColumns);
+ maybeType && removeOptionality && (*maybeType)->IsOptional())
+ {
+ typeOrOtherColumns = (*maybeType)->AsOptional()->GetItemType();
+ }
+ members->push_back(TMember{
+ GetColumnName(*innerFieldDescriptor),
+ std::move(typeOrOtherColumns),
+ });
+ }
+ };
+
+ switch (oneofOptions.Mode) {
+ case EProtobufOneofMode::SeparateFields:
+ addFields(members, /* removeOptionality */ false);
+ return;
+ case EProtobufOneofMode::Variant: {
+ TVector<TMember> variantMembers;
+ addFields(&variantMembers, /* removeOptionality */ true);
+ members->push_back(TMember{
+ oneofOptions.VariantFieldName,
+ NTi::Optional(
+ NTi::Variant(
+ CreateStruct(containingFieldName, std::move(variantMembers))
+ )
+ )
+ });
+ return;
+ }
+ }
+ Y_FAIL();
+}
+
+TVector<TMember> TTableSchemaInferrer::GetMessageMembers(
+ TStringBuf containingFieldName,
+ const Descriptor& messageDescriptor,
+ TProtobufFieldOptions defaultFieldOptions,
+ std::optional<EProtobufFieldSortOrder> overrideFieldSortOrder)
+{
+ TVector<TMember> members;
+ GetMessageMembersImpl(
+ containingFieldName,
+ messageDescriptor,
+ defaultFieldOptions,
+ overrideFieldSortOrder,
+ &members
+ );
+ return members;
+}
+
+void TTableSchemaInferrer::GetMessageMembersImpl(
+ TStringBuf containingFieldName,
+ const Descriptor& messageDescriptor,
+ TProtobufFieldOptions defaultFieldOptions,
+ std::optional<EProtobufFieldSortOrder> overrideFieldSortOrder,
+ TVector<TMember>* members)
+{
+ auto guard = CycleChecker_.Enter(&messageDescriptor);
+ defaultFieldOptions = GetDefaultFieldOptions(&messageDescriptor, defaultFieldOptions);
+ auto messageOptions = GetMessageOptions(&messageDescriptor);
+ auto defaultOneofOptions = GetDefaultOneofOptions(&messageDescriptor);
+
+ TVector<const FieldDescriptor*> fieldDescriptors;
+ fieldDescriptors.reserve(messageDescriptor.field_count());
+ for (int i = 0; i < messageDescriptor.field_count(); ++i) {
+ if (!KeepFieldsWithoutExtension_ && !HasNameExtension(*messageDescriptor.field(i))) {
+ continue;
+ }
+ fieldDescriptors.push_back(messageDescriptor.field(i));
+ }
+
+ auto fieldSortOrder = overrideFieldSortOrder.value_or(messageOptions.FieldSortOrder);
+ SortFields(fieldDescriptors, fieldSortOrder);
+
+ THashSet<const OneofDescriptor*> visitedOneofs;
+ for (const auto innerFieldDescriptor : fieldDescriptors) {
+ auto oneofDescriptor = innerFieldDescriptor->containing_oneof();
+ if (oneofDescriptor) {
+ if (visitedOneofs.contains(oneofDescriptor)) {
+ continue;
+ }
+ ProcessOneofField(
+ containingFieldName,
+ *oneofDescriptor,
+ defaultFieldOptions,
+ defaultOneofOptions,
+ messageOptions.FieldSortOrder,
+ members);
+ visitedOneofs.insert(oneofDescriptor);
+ continue;
+ }
+ auto fieldOptions = GetFieldOptions(innerFieldDescriptor, defaultFieldOptions);
+ if (fieldOptions.SerializationMode == EProtobufSerializationMode::Embedded) {
+ Y_ENSURE(innerFieldDescriptor->type() == FieldDescriptor::TYPE_MESSAGE,
+ "EMBEDDED column must have message type");
+ Y_ENSURE(innerFieldDescriptor->label() == FieldDescriptor::LABEL_REQUIRED,
+ "EMBEDDED column must be marked required");
+ GetMessageMembersImpl(
+ innerFieldDescriptor->full_name(),
+ *innerFieldDescriptor->message_type(),
+ defaultFieldOptions,
+ /*overrideFieldSortOrder*/ std::nullopt,
+ members);
+ } else {
+ auto typeOrOtherColumns = GetFieldType(
+ *innerFieldDescriptor,
+ defaultFieldOptions);
+ members->push_back(TMember{
+ GetColumnName(*innerFieldDescriptor),
+ std::move(typeOrOtherColumns),
+ });
+ }
+ }
+}
+
+NTi::TTypePtr TTableSchemaInferrer::GetMessageType(
+ const FieldDescriptor& fieldDescriptor,
+ TProtobufFieldOptions defaultFieldOptions)
+{
+ Y_VERIFY(fieldDescriptor.message_type());
+ const auto& messageDescriptor = *fieldDescriptor.message_type();
+ auto members = GetMessageMembers(
+ fieldDescriptor.full_name(),
+ messageDescriptor,
+ defaultFieldOptions);
+
+ return CreateStruct(fieldDescriptor.full_name(), std::move(members));
+}
+
+NTi::TTypePtr TTableSchemaInferrer::GetMapType(
+ const FieldDescriptor& fieldDescriptor,
+ const TProtobufFieldOptions& fieldOptions)
+{
+ Y_VERIFY(fieldDescriptor.is_map());
+ switch (fieldOptions.MapMode) {
+ case EProtobufMapMode::ListOfStructsLegacy:
+ case EProtobufMapMode::ListOfStructs: {
+ TProtobufFieldOptions embeddedOptions;
+ if (fieldOptions.MapMode == EProtobufMapMode::ListOfStructs) {
+ embeddedOptions.SerializationMode = EProtobufSerializationMode::Yt;
+ }
+ auto list = NTi::List(GetMessageType(fieldDescriptor, embeddedOptions));
+ switch (fieldOptions.ListMode) {
+ case EProtobufListMode::Required:
+ return list;
+ case EProtobufListMode::Optional:
+ return NTi::Optional(std::move(list));
+ }
+ Y_FAIL();
+ }
+ case EProtobufMapMode::Dict:
+ case EProtobufMapMode::OptionalDict: {
+ auto message = fieldDescriptor.message_type();
+ Y_VERIFY(message->field_count() == 2);
+ auto keyVariant = GetScalarFieldType(*message->field(0), TProtobufFieldOptions{});
+ Y_VERIFY(std::holds_alternative<EValueType>(keyVariant));
+ auto key = std::get<EValueType>(keyVariant);
+ TProtobufFieldOptions embeddedOptions;
+ embeddedOptions.SerializationMode = EProtobufSerializationMode::Yt;
+ auto valueVariant = GetFieldType(*message->field(1), embeddedOptions);
+ Y_VERIFY(std::holds_alternative<NTi::TTypePtr>(valueVariant));
+ auto value = std::get<NTi::TTypePtr>(valueVariant);
+ Y_VERIFY(value->IsOptional());
+ value = value->AsOptional()->GetItemType();
+ auto dict = NTi::Dict(ToTypeV3(key, true), value);
+ if (fieldOptions.MapMode == EProtobufMapMode::OptionalDict) {
+ return NTi::Optional(dict);
+ } else {
+ return dict;
+ }
+ }
+ }
+}
+
+TTypePtrOrOtherColumns TTableSchemaInferrer::GetFieldType(
+ const FieldDescriptor& fieldDescriptor,
+ const TProtobufFieldOptions& defaultOptions)
+{
+ auto fieldOptions = GetFieldOptions(&fieldDescriptor, defaultOptions);
+ if (fieldOptions.Type) {
+ ValidateProtobufType(fieldDescriptor, *fieldOptions.Type);
+ }
+
+ auto getScalarType = [&] {
+ auto valueTypeOrOtherColumns = GetScalarFieldType(fieldDescriptor, fieldOptions);
+ return std::visit(TOverloaded{
+ [] (TOtherColumns) -> TTypePtrOrOtherColumns {
+ return TOtherColumns{};
+ },
+ [] (EValueType valueType) -> TTypePtrOrOtherColumns {
+ return ToTypeV3(valueType, true);
+ }
+ }, valueTypeOrOtherColumns);
+ };
+
+ auto withFieldLabel = [&] (const TTypePtrOrOtherColumns& typeOrOtherColumns) -> TTypePtrOrOtherColumns {
+ switch (fieldDescriptor.label()) {
+ case FieldDescriptor::Label::LABEL_REPEATED: {
+ Y_ENSURE(fieldOptions.SerializationMode == EProtobufSerializationMode::Yt,
+ "Repeated fields are supported only for YT serialization mode, field \"" + fieldDescriptor.full_name() +
+ "\" has incorrect serialization mode");
+ auto* type = std::get_if<NTi::TTypePtr>(&typeOrOtherColumns);
+ Y_ENSURE(type, "OTHER_COLUMNS field can not be repeated");
+ switch (fieldOptions.ListMode) {
+ case EProtobufListMode::Required:
+ return NTi::TTypePtr(NTi::List(*type));
+ case EProtobufListMode::Optional:
+ return NTi::TTypePtr(NTi::Optional(NTi::List(*type)));
+ }
+ Y_FAIL();
+ }
+ case FieldDescriptor::Label::LABEL_OPTIONAL:
+ return std::visit(TOverloaded{
+ [] (TOtherColumns) -> TTypePtrOrOtherColumns {
+ return TOtherColumns{};
+ },
+ [] (NTi::TTypePtr type) -> TTypePtrOrOtherColumns {
+ return NTi::TTypePtr(NTi::Optional(std::move(type)));
+ }
+ }, typeOrOtherColumns);
+ case FieldDescriptor::LABEL_REQUIRED: {
+ auto* type = std::get_if<NTi::TTypePtr>(&typeOrOtherColumns);
+ Y_ENSURE(type, "OTHER_COLUMNS field can not be required");
+ return *type;
+ }
+ }
+ Y_FAIL();
+ };
+
+ switch (fieldOptions.SerializationMode) {
+ case EProtobufSerializationMode::Protobuf:
+ return withFieldLabel(getScalarType());
+ case EProtobufSerializationMode::Yt:
+ if (fieldDescriptor.type() == FieldDescriptor::TYPE_MESSAGE) {
+ if (fieldDescriptor.is_map()) {
+ return GetMapType(fieldDescriptor, fieldOptions);
+ } else {
+ return withFieldLabel(GetMessageType(fieldDescriptor, TProtobufFieldOptions{}));
+ }
+ } else {
+ return withFieldLabel(getScalarType());
+ }
+ case EProtobufSerializationMode::Embedded:
+ ythrow yexception() << "EMBEDDED field is not allowed for field "
+ << fieldDescriptor.full_name();
+ }
+ Y_FAIL();
+}
+
+TTableSchema TTableSchemaInferrer::InferSchema(const Descriptor& messageDescriptor)
+{
+ TTableSchema result;
+
+ auto defaultFieldOptions = GetDefaultFieldOptions(&messageDescriptor);
+ auto members = GetMessageMembers(
+ messageDescriptor.full_name(),
+ messageDescriptor,
+ defaultFieldOptions,
+ // Use special sort order for top level messages.
+ /*overrideFieldSortOrder*/ EProtobufFieldSortOrder::AsInProtoFile);
+
+ for (auto& member : members) {
+ std::visit(TOverloaded{
+ [&] (TOtherColumns) {
+ result.Strict(false);
+ },
+ [&] (NTi::TTypePtr& type) {
+ result.AddColumn(TColumnSchema()
+ .Name(std::move(member.Name))
+ .Type(std::move(type))
+ );
+ },
+ }, member.TypeOrOtherColumns);
+ }
+
+ return result;
+}
+
+TTableSchema CreateTableSchemaImpl(
+ const Descriptor& messageDescriptor,
+ bool keepFieldsWithoutExtension)
+{
+ TTableSchemaInferrer inferrer(keepFieldsWithoutExtension);
+ return inferrer.InferSchema(messageDescriptor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+void Out<NYT::EWrapperFieldFlag::Enum>(IOutputStream& stream, NYT::EWrapperFieldFlag::Enum value)
+{
+ stream << NYT::EWrapperFieldFlag_Enum_Name(value);
+}
+
+template <>
+void Out<NYT::EWrapperMessageFlag::Enum>(IOutputStream& stream, NYT::EWrapperMessageFlag::Enum value)
+{
+ stream << NYT::EWrapperMessageFlag_Enum_Name(value);
+}
+
+template <>
+void Out<NYT::EWrapperOneofFlag::Enum>(IOutputStream& stream, NYT::EWrapperOneofFlag::Enum value)
+{
+ stream << NYT::EWrapperOneofFlag_Enum_Name(value);
+}
diff --git a/yt/cpp/mapreduce/interface/protobuf_format.h b/yt/cpp/mapreduce/interface/protobuf_format.h
new file mode 100644
index 0000000000..aafbced386
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_format.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include "common.h"
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <util/generic/maybe.h>
+
+#include <google/protobuf/message.h>
+
+/// @cond Doxygen_Suppress
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class EProtobufType
+{
+ EnumInt /* "enum_int" */,
+ EnumString /* "enum_string" */,
+ Any /* "any" */,
+ OtherColumns /* "other_columns" */,
+};
+
+enum class EProtobufSerializationMode
+{
+ Protobuf,
+ Yt,
+ Embedded,
+};
+
+enum class EProtobufListMode
+{
+ Optional,
+ Required,
+};
+
+enum class EProtobufMapMode
+{
+ ListOfStructsLegacy,
+ ListOfStructs,
+ Dict,
+ OptionalDict,
+};
+
+enum class EProtobufFieldSortOrder
+{
+ AsInProtoFile,
+ ByFieldNumber,
+};
+
+enum class EProtobufOneofMode
+{
+ SeparateFields,
+ Variant,
+};
+
+enum class EProtobufEnumWritingMode
+{
+ SkipUnknownValues,
+ CheckValues,
+};
+
+struct TProtobufOneofOptions
+{
+ EProtobufOneofMode Mode = EProtobufOneofMode::Variant;
+ TString VariantFieldName;
+};
+
+struct TProtobufFieldOptions
+{
+ TMaybe<EProtobufType> Type;
+ EProtobufSerializationMode SerializationMode = EProtobufSerializationMode::Protobuf;
+ EProtobufListMode ListMode = EProtobufListMode::Required;
+ EProtobufMapMode MapMode = EProtobufMapMode::ListOfStructsLegacy;
+};
+
+struct TProtobufMessageOptions
+{
+ EProtobufFieldSortOrder FieldSortOrder = EProtobufFieldSortOrder::ByFieldNumber;
+};
+
+TString GetColumnName(const ::google::protobuf::FieldDescriptor& field);
+
+TProtobufFieldOptions GetFieldOptions(
+ const ::google::protobuf::FieldDescriptor* fieldDescriptor,
+ const TMaybe<TProtobufFieldOptions>& defaultFieldOptions = {});
+
+TProtobufOneofOptions GetOneofOptions(
+ const ::google::protobuf::OneofDescriptor* oneofDescriptor,
+ const TMaybe<TProtobufOneofOptions>& defaultOneofOptions = {});
+
+TProtobufMessageOptions GetMessageOptions(const ::google::protobuf::Descriptor* descriptor);
+
+TMaybe<TVector<TString>> InferColumnFilter(const ::google::protobuf::Descriptor& descriptor);
+
+TNode MakeProtoFormatConfigWithTables(const TVector<const ::google::protobuf::Descriptor*>& descriptors);
+TNode MakeProtoFormatConfigWithDescriptors(const TVector<const ::google::protobuf::Descriptor*>& descriptors);
+
+TTableSchema CreateTableSchemaImpl(
+ const ::google::protobuf::Descriptor& messageDescriptor,
+ bool keepFieldsWithoutExtension);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
+/// @endcond
diff --git a/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.cpp b/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.cpp
new file mode 100644
index 0000000000..19a3d5163f
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.cpp
@@ -0,0 +1,451 @@
+#include "common.h"
+#include "errors.h"
+#include "common_ut.h"
+#include "util/generic/fwd.h"
+
+#include <yt/cpp/mapreduce/interface/protobuf_table_schema_ut.pb.h>
+#include <yt/cpp/mapreduce/interface/proto3_ut.pb.h>
+
+#include <yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <algorithm>
+
+using namespace NYT;
+
+bool IsFieldPresent(const TTableSchema& schema, TStringBuf name)
+{
+ for (const auto& field : schema.Columns()) {
+ if (field.Name() == name) {
+ return true;
+ }
+ }
+ return false;
+}
+
+Y_UNIT_TEST_SUITE(ProtoSchemaTest_Simple)
+{
+ Y_UNIT_TEST(TIntegral)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TIntegral>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("DoubleField").Type(ToTypeV3(EValueType::VT_DOUBLE, false)))
+ .AddColumn(TColumnSchema().Name("FloatField").Type(ToTypeV3(EValueType::VT_DOUBLE, false)))
+ .AddColumn(TColumnSchema().Name("Int32Field").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("Int64Field").Type(ToTypeV3(EValueType::VT_INT64, false)))
+ .AddColumn(TColumnSchema().Name("Uint32Field").Type(ToTypeV3(EValueType::VT_UINT32, false)))
+ .AddColumn(TColumnSchema().Name("Uint64Field").Type(ToTypeV3(EValueType::VT_UINT64, false)))
+ .AddColumn(TColumnSchema().Name("Sint32Field").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("Sint64Field").Type(ToTypeV3(EValueType::VT_INT64, false)))
+ .AddColumn(TColumnSchema().Name("Fixed32Field").Type(ToTypeV3(EValueType::VT_UINT32, false)))
+ .AddColumn(TColumnSchema().Name("Fixed64Field").Type(ToTypeV3(EValueType::VT_UINT64, false)))
+ .AddColumn(TColumnSchema().Name("Sfixed32Field").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("Sfixed64Field").Type(ToTypeV3(EValueType::VT_INT64, false)))
+ .AddColumn(TColumnSchema().Name("BoolField").Type(ToTypeV3(EValueType::VT_BOOLEAN, false)))
+ .AddColumn(TColumnSchema().Name("EnumField").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(TOneOf)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TOneOf>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("DoubleField").Type(ToTypeV3(EValueType::VT_DOUBLE, false)))
+ .AddColumn(TColumnSchema().Name("Int32Field").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("BoolField").Type(ToTypeV3(EValueType::VT_BOOLEAN, false))));
+ }
+
+ Y_UNIT_TEST(TWithRequired)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TWithRequired>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("RequiredField").Type(ToTypeV3(EValueType::VT_STRING, true)))
+ .AddColumn(TColumnSchema().Name("NotRequiredField").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(TAggregated)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TAggregated>();
+
+ UNIT_ASSERT_VALUES_EQUAL(6, schema.Columns().size());
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("StringField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("BytesField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("NestedField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("NestedRepeatedField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("NestedOneOfField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("NestedRecursiveField").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(TAliased)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TAliased>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("key").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("subkey").Type(ToTypeV3(EValueType::VT_DOUBLE, false)))
+ .AddColumn(TColumnSchema().Name("Data").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(SortColumns)
+ {
+ const TSortColumns keys = {"key", "subkey"};
+
+ const auto schema = CreateTableSchema<NUnitTesting::TAliased>(keys);
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("key")
+ .Type(ToTypeV3(EValueType::VT_INT32, false))
+ .SortOrder(ESortOrder::SO_ASCENDING))
+ .AddColumn(TColumnSchema()
+ .Name("subkey")
+ .Type(ToTypeV3(EValueType::VT_DOUBLE, false))
+ .SortOrder(ESortOrder::SO_ASCENDING))
+ .AddColumn(TColumnSchema().Name("Data").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(SortColumnsReordered)
+ {
+ const TSortColumns keys = {"subkey"};
+
+ const auto schema = CreateTableSchema<NUnitTesting::TAliased>(keys);
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("subkey")
+ .Type(ToTypeV3(EValueType::VT_DOUBLE, false))
+ .SortOrder(ESortOrder::SO_ASCENDING))
+ .AddColumn(TColumnSchema().Name("key").Type(ToTypeV3(EValueType::VT_INT32, false)))
+ .AddColumn(TColumnSchema().Name("Data").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(SortColumnsInvalid)
+ {
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TAliased>({"subkey", "subkey"}), yexception);
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TAliased>({"key", "junk"}), yexception);
+ }
+
+ Y_UNIT_TEST(KeepFieldsWithoutExtensionTrue)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TAliased>({}, true);
+ UNIT_ASSERT(IsFieldPresent(schema, "key"));
+ UNIT_ASSERT(IsFieldPresent(schema, "subkey"));
+ UNIT_ASSERT(IsFieldPresent(schema, "Data"));
+ UNIT_ASSERT(schema.Strict());
+ }
+
+ Y_UNIT_TEST(KeepFieldsWithoutExtensionFalse)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TAliased>({}, false);
+ UNIT_ASSERT(IsFieldPresent(schema, "key"));
+ UNIT_ASSERT(IsFieldPresent(schema, "subkey"));
+ UNIT_ASSERT(!IsFieldPresent(schema, "Data"));
+ UNIT_ASSERT(schema.Strict());
+ }
+
+ Y_UNIT_TEST(ProtobufTypeOption)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TWithTypeOptions>({});
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .Strict(false)
+ .AddColumn(TColumnSchema().Name("ColorIntField").Type(ToTypeV3(EValueType::VT_INT64, false)))
+ .AddColumn(TColumnSchema().Name("ColorStringField").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("AnyField").Type(ToTypeV3(EValueType::VT_ANY, false)))
+ .AddColumn(TColumnSchema().Name("EmbeddedField").Type(
+ NTi::Optional(NTi::Struct({
+ {"ColorIntField", ToTypeV3(EValueType::VT_INT64, false)},
+ {"ColorStringField", ToTypeV3(EValueType::VT_STRING, false)},
+ {"AnyField", ToTypeV3(EValueType::VT_ANY, false)}}))))
+ .AddColumn(TColumnSchema().Name("RepeatedEnumIntField").Type(NTi::List(NTi::Int64()))));
+ }
+
+ Y_UNIT_TEST(ProtobufTypeOption_TypeMismatch)
+ {
+ UNIT_ASSERT_EXCEPTION(
+ CreateTableSchema<NUnitTesting::TWithTypeOptions_TypeMismatch_EnumInt>({}),
+ yexception);
+ UNIT_ASSERT_EXCEPTION(
+ CreateTableSchema<NUnitTesting::TWithTypeOptions_TypeMismatch_EnumString>({}),
+ yexception);
+ UNIT_ASSERT_EXCEPTION(
+ CreateTableSchema<NUnitTesting::TWithTypeOptions_TypeMismatch_Any>({}),
+ yexception);
+ UNIT_ASSERT_EXCEPTION(
+ CreateTableSchema<NUnitTesting::TWithTypeOptions_TypeMismatch_OtherColumns>({}),
+ yexception);
+ }
+}
+
+Y_UNIT_TEST_SUITE(ProtoSchemaTest_Complex)
+{
+ Y_UNIT_TEST(TRepeated)
+ {
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TRepeated>(), yexception);
+
+ const auto schema = CreateTableSchema<NUnitTesting::TRepeatedYtMode>();
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("Int32Field").Type(NTi::List(ToTypeV3(EValueType::VT_INT32, true)))));
+ }
+
+ Y_UNIT_TEST(TRepeatedOptionalList)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TOptionalList>();
+ auto type = NTi::Optional(NTi::List(NTi::Int64()));
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("OptionalListInt64").TypeV3(type)));
+ }
+
+ NTi::TTypePtr GetUrlRowType(bool required)
+ {
+ static const NTi::TTypePtr structType = NTi::Struct({
+ {"Host", ToTypeV3(EValueType::VT_STRING, false)},
+ {"Path", ToTypeV3(EValueType::VT_STRING, false)},
+ {"HttpCode", ToTypeV3(EValueType::VT_INT32, false)}});
+ return required ? structType : NTi::TTypePtr(NTi::Optional(structType));
+ }
+
+ Y_UNIT_TEST(TRowFieldSerializationOption)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TRowFieldSerializationOption>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(GetUrlRowType(false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(TRowMessageSerializationOption)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TRowMessageSerializationOption>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(GetUrlRowType(false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(GetUrlRowType(false))));
+ }
+
+ Y_UNIT_TEST(TRowMixedSerializationOptions)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TRowMixedSerializationOptions>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(GetUrlRowType(false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ NTi::TTypePtr GetUrlRowType_ColumnNames(bool required)
+ {
+ static const NTi::TTypePtr type = NTi::Struct({
+ {"Host_ColumnName", ToTypeV3(EValueType::VT_STRING, false)},
+ {"Path_KeyColumnName", ToTypeV3(EValueType::VT_STRING, false)},
+ {"HttpCode", ToTypeV3(EValueType::VT_INT32, false)},
+ });
+ return required ? type : NTi::TTypePtr(NTi::Optional(type));
+ }
+
+ Y_UNIT_TEST(TRowMixedSerializationOptions_ColumnNames)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TRowMixedSerializationOptions_ColumnNames>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("UrlRow_1").Type(GetUrlRowType_ColumnNames(false)))
+ .AddColumn(TColumnSchema().Name("UrlRow_2").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(NoOptionInheritance)
+ {
+ auto deepestEmbedded = NTi::Optional(NTi::Struct({{"x", ToTypeV3(EValueType::VT_INT64, false)}}));
+
+ const auto schema = CreateTableSchema<NUnitTesting::TNoOptionInheritance>();
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("EmbeddedYt_YtOption")
+ .Type(NTi::Optional(NTi::Struct({{"embedded", deepestEmbedded}}))))
+ .AddColumn(TColumnSchema().Name("EmbeddedYt_ProtobufOption").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("EmbeddedYt_NoOption").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema()
+ .Name("EmbeddedProtobuf_YtOption")
+ .Type(NTi::Optional(NTi::Struct({{"embedded", ToTypeV3(EValueType::VT_STRING, false)}}))))
+ .AddColumn(TColumnSchema().Name("EmbeddedProtobuf_ProtobufOption").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("EmbeddedProtobuf_NoOption").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema()
+ .Name("Embedded_YtOption")
+ .Type(NTi::Optional(NTi::Struct({{"embedded", ToTypeV3(EValueType::VT_STRING, false)}}))))
+ .AddColumn(TColumnSchema().Name("Embedded_ProtobufOption").Type(ToTypeV3(EValueType::VT_STRING, false)))
+ .AddColumn(TColumnSchema().Name("Embedded_NoOption").Type(ToTypeV3(EValueType::VT_STRING, false))));
+ }
+
+ Y_UNIT_TEST(Cyclic)
+ {
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TCyclic>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TCyclic::TA>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TCyclic::TB>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TCyclic::TC>(), TApiUsageError);
+ UNIT_ASSERT_EXCEPTION(CreateTableSchema<NUnitTesting::TCyclic::TD>(), TApiUsageError);
+
+ ASSERT_SERIALIZABLES_EQUAL(
+ TTableSchema().AddColumn(
+ TColumnSchema().Name("d").TypeV3(NTi::Optional(NTi::String()))),
+ CreateTableSchema<NUnitTesting::TCyclic::TE>());
+ }
+
+ Y_UNIT_TEST(FieldSortOrder)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TFieldSortOrder>();
+
+ auto byFieldNumber = NTi::Optional(NTi::Struct({
+ {"z", NTi::Optional(NTi::Bool())},
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ }));
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema().Name("EmbeddedDefault").Type(byFieldNumber))
+ .AddColumn(TColumnSchema()
+ .Name("EmbeddedAsInProtoFile")
+ .Type(NTi::Optional(NTi::Struct({
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ {"z", NTi::Optional(NTi::Bool())},
+ }))))
+ .AddColumn(TColumnSchema().Name("EmbeddedByFieldNumber").Type(byFieldNumber)));
+ }
+
+ Y_UNIT_TEST(Map)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TWithMap>();
+
+ auto createKeyValueStruct = [] (NTi::TTypePtr key, NTi::TTypePtr value) {
+ return NTi::List(NTi::Struct({
+ {"key", NTi::Optional(key)},
+ {"value", NTi::Optional(value)},
+ }));
+ };
+
+ auto embedded = NTi::Struct({
+ {"x", NTi::Optional(NTi::Int64())},
+ {"y", NTi::Optional(NTi::String())},
+ });
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("MapDefault")
+ .Type(createKeyValueStruct(NTi::Int64(), NTi::String())))
+ .AddColumn(TColumnSchema()
+ .Name("MapListOfStructsLegacy")
+ .Type(createKeyValueStruct(NTi::Int64(), NTi::String())))
+ .AddColumn(TColumnSchema()
+ .Name("MapListOfStructs")
+ .Type(createKeyValueStruct(NTi::Int64(), embedded)))
+ .AddColumn(TColumnSchema()
+ .Name("MapOptionalDict")
+ .Type(NTi::Optional(NTi::Dict(NTi::Int64(), embedded))))
+ .AddColumn(TColumnSchema()
+ .Name("MapDict")
+ .Type(NTi::Dict(NTi::Int64(), embedded))));
+ }
+
+ Y_UNIT_TEST(Oneof)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TWithOneof>();
+
+ auto embedded = NTi::Struct({
+ {"Oneof", NTi::Optional(NTi::Variant(NTi::Struct({
+ {"x", NTi::Int64()},
+ {"y", NTi::String()},
+ })))},
+ });
+
+ auto createType = [&] (TString oneof2Name) {
+ return NTi::Optional(NTi::Struct({
+ {"field", NTi::Optional(NTi::String())},
+ {oneof2Name, NTi::Optional(NTi::Variant(NTi::Struct({
+ {"x2", NTi::Int64()},
+ {"y2", NTi::String()},
+ {"z2", embedded},
+ })))},
+ {"y1", NTi::Optional(NTi::String())},
+ {"z1", NTi::Optional(embedded)},
+ {"x1", NTi::Optional(NTi::Int64())},
+ }));
+ };
+
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("DefaultSeparateFields")
+ .Type(createType("variant_field_name")))
+ .AddColumn(TColumnSchema()
+ .Name("NoDefault")
+ .Type(createType("Oneof2")))
+ .AddColumn(TColumnSchema()
+ .Name("SerializationProtobuf")
+ .Type(NTi::Optional(NTi::Struct({
+ {"y1", NTi::Optional(NTi::String())},
+ {"x1", NTi::Optional(NTi::Int64())},
+ {"z1", NTi::Optional(NTi::String())},
+ }))))
+ .AddColumn(TColumnSchema()
+ .Name("TopLevelOneof")
+ .Type(
+ NTi::Optional(
+ NTi::Variant(NTi::Struct({
+ {"MemberOfTopLevelOneof", NTi::Int64()}
+ }))
+ )
+ ))
+ );
+ }
+
+ Y_UNIT_TEST(Embedded)
+ {
+ const auto schema = CreateTableSchema<NUnitTesting::TEmbeddingMessage>();
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .Strict(false)
+ .AddColumn(TColumnSchema().Name("embedded2_num").Type(NTi::Optional(NTi::Uint64())))
+ .AddColumn(TColumnSchema().Name("embedded2_struct").Type(NTi::Optional(NTi::Struct({
+ {"float1", NTi::Optional(NTi::Double())},
+ {"string1", NTi::Optional(NTi::String())},
+ }))))
+ .AddColumn(TColumnSchema().Name("embedded2_repeated").Type(NTi::List(NTi::String())))
+ .AddColumn(TColumnSchema().Name("embedded_num").Type(NTi::Optional(NTi::Uint64())))
+ .AddColumn(TColumnSchema().Name("embedded_extra_field").Type(NTi::Optional(NTi::String())))
+ .AddColumn(TColumnSchema().Name("variant").Type(NTi::Optional(NTi::Variant(NTi::Struct({
+ {"str_variant", NTi::String()},
+ {"uint_variant", NTi::Uint64()},
+ })))))
+ .AddColumn(TColumnSchema().Name("num").Type(NTi::Optional(NTi::Uint64())))
+ .AddColumn(TColumnSchema().Name("extra_field").Type(NTi::Optional(NTi::String())))
+ );
+ }
+}
+
+Y_UNIT_TEST_SUITE(ProtoSchemaTest_Proto3)
+{
+ Y_UNIT_TEST(TWithOptional)
+ {
+ const auto schema = CreateTableSchema<NTestingProto3::TWithOptional>();
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("x").Type(NTi::Optional(NTi::Int64()))
+ )
+ );
+ }
+
+ Y_UNIT_TEST(TWithOptionalMessage)
+ {
+ const auto schema = CreateTableSchema<NTestingProto3::TWithOptionalMessage>();
+ ASSERT_SERIALIZABLES_EQUAL(schema, TTableSchema()
+ .AddColumn(TColumnSchema()
+ .Name("x").Type(
+ NTi::Optional(
+ NTi::Struct({{"x", NTi::Optional(NTi::Int64())}})
+ )
+ )
+ )
+ );
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.proto b/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.proto
new file mode 100644
index 0000000000..60bad6e650
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/protobuf_table_schema_ut.proto
@@ -0,0 +1,402 @@
+import "yt/yt_proto/yt/formats/extension.proto";
+
+package NYT.NUnitTesting;
+
+message TIntegral
+{
+ optional double DoubleField = 1;
+ optional float FloatField = 2;
+ optional int32 Int32Field = 3;
+ optional int64 Int64Field = 4;
+ optional uint32 Uint32Field = 5;
+ optional uint64 Uint64Field = 6;
+ optional sint32 Sint32Field = 7;
+ optional sint64 Sint64Field = 8;
+ optional fixed32 Fixed32Field = 9;
+ optional fixed64 Fixed64Field = 10;
+ optional sfixed32 Sfixed32Field = 11;
+ optional sfixed64 Sfixed64Field = 12;
+ optional bool BoolField = 13;
+ enum TriBool
+ {
+ TRI_FALSE = 0;
+ TRI_TRUE = 1;
+ TRI_UNDEF = -1;
+ }
+ optional TriBool EnumField = 14;
+}
+
+message TRepeated
+{
+ repeated int32 Int32Field = 1;
+}
+
+message TRepeatedYtMode
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ repeated int32 Int32Field = 1;
+}
+
+message TWithTypeOptions
+{
+ enum Color
+ {
+ WHITE = 0;
+ BLUE = 1;
+ RED = -1;
+ }
+
+ message TEmbedded
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional Color ColorIntField = 1 [(NYT.flags) = ENUM_INT];
+ optional Color ColorStringField = 2 [(NYT.flags) = ENUM_STRING];
+ optional bytes AnyField = 3 [(NYT.flags) = ANY];
+ }
+
+ optional Color ColorIntField = 1 [(NYT.flags) = ENUM_INT];
+ optional Color ColorStringField = 2 [(NYT.flags) = ENUM_STRING];
+ optional bytes AnyField = 3 [(NYT.flags) = ANY];
+ optional bytes OtherColumnsField = 4 [(NYT.flags) = OTHER_COLUMNS];
+ optional TEmbedded EmbeddedField = 5 [(NYT.flags) = SERIALIZATION_YT];
+ repeated Color RepeatedEnumIntField = 6 [(NYT.flags) = SERIALIZATION_YT, (NYT.flags) = ENUM_INT];
+}
+
+message TWithTypeOptions_TypeMismatch_EnumInt
+{
+ optional int64 EnumField = 1 [(NYT.flags) = ENUM_INT];
+}
+
+message TWithTypeOptions_TypeMismatch_EnumString
+{
+ optional string EnumField = 1 [(NYT.flags) = ENUM_STRING];
+}
+
+message TWithTypeOptions_TypeMismatch_Any
+{
+ optional string AnyField = 1 [(NYT.flags) = ANY];
+}
+
+message TWithTypeOptions_TypeMismatch_OtherColumns
+{
+ optional string OtherColumnsField = 1 [(NYT.flags) = OTHER_COLUMNS];
+}
+
+message TOneOf
+{
+ oneof Chooser
+ {
+ double DoubleField = 1;
+ int32 Int32Field = 2;
+ }
+ optional bool BoolField = 3;
+}
+
+message TWithRequired
+{
+ required string RequiredField = 1;
+ optional string NotRequiredField = 2;
+};
+
+message TAggregated
+{
+ optional string StringField = 1;
+ optional bytes BytesField = 2;
+ optional TIntegral NestedField = 3;
+ optional TRepeated NestedRepeatedField = 4;
+ optional TOneOf NestedOneOfField = 5;
+ optional TAggregated NestedRecursiveField = 6;
+}
+
+message TAliased
+{
+ optional int32 Key = 1 [(NYT.key_column_name) = "key"];
+ optional double Subkey = 2 [(NYT.key_column_name) = "subkey"];
+ optional TAggregated Data = 3;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TUrlRow
+{
+ optional string Host = 1 [(NYT.column_name) = "Host"];
+ optional string Path = 2 [(NYT.column_name) = "Path"];
+ optional sint32 HttpCode = 3 [(NYT.column_name) = "HttpCode"];
+}
+
+message TRowFieldSerializationOption
+{
+ optional TUrlRow UrlRow_1 = 1 [(NYT.flags) = SERIALIZATION_YT];
+ optional TUrlRow UrlRow_2 = 2;
+}
+
+message TRowMessageSerializationOption
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TUrlRow UrlRow_1 = 1;
+ optional TUrlRow UrlRow_2 = 2;
+}
+
+message TRowMixedSerializationOptions
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TUrlRow UrlRow_1 = 1;
+ optional TUrlRow UrlRow_2 = 2 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+}
+
+message TRowSerializedRepeatedFields
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ repeated int64 Ints = 1;
+ repeated TUrlRow UrlRows = 2;
+}
+
+message TUrlRowWithColumnNames
+{
+ optional string Host = 1 [(NYT.column_name) = "Host_ColumnName", (NYT.key_column_name) = "Host_KeyColumnName"];
+ optional string Path = 2 [(NYT.key_column_name) = "Path_KeyColumnName"];
+ optional sint32 HttpCode = 3;
+}
+
+message TRowMixedSerializationOptions_ColumnNames
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TUrlRowWithColumnNames UrlRow_1 = 1;
+ optional TUrlRowWithColumnNames UrlRow_2 = 2 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+}
+
+message TNoOptionInheritance
+{
+ message TDeepestEmbedded
+ {
+ optional int64 x = 1;
+ }
+
+ message TEmbedded
+ {
+ optional TDeepestEmbedded embedded = 1;
+ }
+
+ message TEmbeddedYt
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional TDeepestEmbedded embedded = 1;
+ }
+
+ message TEmbeddedProtobuf
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_PROTOBUF;
+
+ optional TDeepestEmbedded embedded = 1;
+ }
+
+ optional TEmbeddedYt EmbeddedYt_YtOption = 1 [(NYT.flags) = SERIALIZATION_YT];
+ optional TEmbeddedYt EmbeddedYt_ProtobufOption = 2 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+ optional TEmbeddedYt EmbeddedYt_NoOption = 3;
+ optional TEmbeddedProtobuf EmbeddedProtobuf_YtOption = 4 [(NYT.flags) = SERIALIZATION_YT];
+ optional TEmbeddedProtobuf EmbeddedProtobuf_ProtobufOption = 5 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+ optional TEmbeddedProtobuf EmbeddedProtobuf_NoOption = 6;
+ optional TEmbedded Embedded_YtOption = 7 [(NYT.flags) = SERIALIZATION_YT];
+ optional TEmbedded Embedded_ProtobufOption = 8 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+ optional TEmbedded Embedded_NoOption = 9;
+}
+
+message TOptionalList
+{
+ repeated int64 OptionalListInt64 = 1 [(NYT.flags) = OPTIONAL_LIST, (NYT.flags) = SERIALIZATION_YT];
+}
+
+message TPacked
+{
+ repeated int64 PackedListInt64 = 1 [(NYT.flags) = SERIALIZATION_YT, packed=true];
+}
+
+message TCyclic
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ message TA
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ repeated TB b = 1;
+ optional TC c = 2;
+ }
+
+ message TB
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TD d = 1;
+ }
+
+ message TC
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TD d = 1;
+ }
+
+ message TD
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TA a = 1;
+ }
+
+ message TE
+ {
+ optional TD d = 1 [(NYT.flags) = SERIALIZATION_PROTOBUF];
+ }
+
+ optional TA a = 1;
+}
+
+message TFieldSortOrder
+{
+ message TEmbeddedDefault {
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ message TEmbeddedAsInProtoFile {
+ option (NYT.message_flags) = DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE;
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ message TEmbeddedByFieldNumber {
+ option (NYT.message_flags) = SORT_FIELDS_BY_FIELD_NUMBER;
+ optional int64 x = 2;
+ optional string y = 12;
+ optional bool z = 1;
+ }
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional TEmbeddedDefault EmbeddedDefault = 1;
+ optional TEmbeddedAsInProtoFile EmbeddedAsInProtoFile = 2;
+ optional TEmbeddedByFieldNumber EmbeddedByFieldNumber = 3;
+}
+
+message TWithMap
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ message TEmbedded {
+ optional int64 x = 1;
+ optional string y = 2;
+ }
+
+ map<int64, TEmbedded> MapDefault = 1;
+ map<int64, TEmbedded> MapListOfStructsLegacy = 2 [(NYT.flags) = MAP_AS_LIST_OF_STRUCTS_LEGACY];
+ map<int64, TEmbedded> MapListOfStructs = 3 [(NYT.flags) = MAP_AS_LIST_OF_STRUCTS];
+ map<int64, TEmbedded> MapOptionalDict = 4 [(NYT.flags) = MAP_AS_OPTIONAL_DICT];
+ map<int64, TEmbedded> MapDict = 5 [(NYT.flags) = MAP_AS_DICT];
+}
+
+message TWithOneof
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ message TEmbedded
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ oneof Oneof {
+ int64 x = 1;
+ string y = 2;
+ }
+ }
+
+ message TDefaultSeparateFields
+ {
+ option (NYT.default_oneof_flags) = SEPARATE_FIELDS;
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional string field = 1;
+
+ oneof Oneof2
+ {
+ option (NYT.variant_field_name) = "variant_field_name";
+ option (NYT.oneof_flags) = VARIANT;
+ string y2 = 4;
+ TEmbedded z2 = 6;
+ int64 x2 = 2;
+ }
+
+ oneof Oneof1
+ {
+ int64 x1 = 10;
+ string y1 = 3;
+ TEmbedded z1 = 5;
+ }
+ }
+
+ message TNoDefault
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional string field = 1;
+
+ oneof Oneof2
+ {
+ string y2 = 4;
+ TEmbedded z2 = 6;
+ int64 x2 = 2;
+ }
+
+ oneof Oneof1
+ {
+ option (NYT.oneof_flags) = SEPARATE_FIELDS;
+ int64 x1 = 10;
+ string y1 = 3;
+ TEmbedded z1 = 5;
+ }
+ }
+
+ message TSerializationProtobuf
+ {
+ oneof Oneof
+ {
+ int64 x1 = 2;
+ string y1 = 1;
+ TEmbedded z1 = 3;
+ }
+ }
+
+ optional TDefaultSeparateFields DefaultSeparateFields = 1;
+ optional TNoDefault NoDefault = 2;
+ optional TSerializationProtobuf SerializationProtobuf = 3;
+
+ oneof TopLevelOneof
+ {
+ int64 MemberOfTopLevelOneof = 4;
+ }
+}
+
+message TEmbeddedStruct {
+ optional float float1 = 1;
+ optional string string1 = 2;
+}
+
+message TEmbedded2Message {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional uint64 embedded2_num = 10;
+ optional TEmbeddedStruct embedded2_struct = 17;
+ repeated string embedded2_repeated = 42;
+}
+
+message TEmbedded1Message {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ required TEmbedded2Message t2 = 1 [(NYT.flags) = EMBEDDED];
+ oneof variant {
+ string str_variant = 101;
+ uint64 uint_variant = 102;
+ }
+ optional uint64 embedded_num = 10; // make intensional field_num collision!
+ optional string embedded_extra_field = 11;
+}
+
+message TEmbeddingMessage {
+ optional bytes other_columns_field = 15 [(NYT.flags) = OTHER_COLUMNS];
+ required TEmbedded1Message t1 = 2 [(NYT.flags) = EMBEDDED];
+ optional uint64 num = 12;
+ optional string extra_field = 13;
+}
diff --git a/yt/cpp/mapreduce/interface/public.h b/yt/cpp/mapreduce/interface/public.h
new file mode 100644
index 0000000000..bdeda78795
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/public.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <memory>
+
+namespace NYT::NAuth {
+
+struct IServiceTicketAuthPtrWrapper;
+using IServiceTicketAuthPtrWrapperPtr = std::shared_ptr<IServiceTicketAuthPtrWrapper>;
+
+} // namespace NYT::NAuth
diff --git a/yt/cpp/mapreduce/interface/retry_policy.h b/yt/cpp/mapreduce/interface/retry_policy.h
new file mode 100644
index 0000000000..c198839079
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/retry_policy.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <util/datetime/base.h>
+#include <util/generic/ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// A configuration that controls retries of a single request.
+struct TRetryConfig
+{
+ ///
+ /// @brief How long retries of a single YT request can go on.
+ ///
+ /// If this limit is reached while retry count is not yet exceeded @ref TRequestRetriesTimeout exception is thrown.
+ TDuration RetriesTimeLimit = TDuration::Max();
+};
+
+/// The library uses this class to understand how to retry individual requests.
+class IRetryConfigProvider
+ : public virtual TThrRefBase
+{
+public:
+ ///
+ /// @brief Gets retry policy for single request.
+ ///
+ /// CreateRetryConfig is called before ANY request.
+ /// Returned config controls retries of this request.
+ ///
+ /// Must be thread safe since it can be used from different threads
+ /// to perform internal library requests (e.g. pings).
+ ///
+ /// Some methods (e.g. IClient::Map) involve multiple requests to YT and therefore
+ /// this method will be called several times during execution of single method.
+ ///
+ /// If user needs to limit overall retries inside long operation they might create
+ /// retry policy that knows about overall deadline
+ /// @ref NYT::TRetryConfig::RetriesTimeLimit taking into account that overall deadline.
+ /// (E.g. when deadline reached it returns zero limit for retries).
+ virtual TRetryConfig CreateRetryConfig() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/cpp/mapreduce/interface/serialize.cpp b/yt/cpp/mapreduce/interface/serialize.cpp
new file mode 100644
index 0000000000..ae05d9f50d
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/serialize.cpp
@@ -0,0 +1,553 @@
+#include "serialize.h"
+
+#include "common.h"
+#include "fluent.h"
+
+#include <library/cpp/yson/parser.h>
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/serialize.h>
+
+#include <library/cpp/type_info/type_io.h>
+
+#include <util/generic/string.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// const auto& nodeMap = node.AsMap();
+#define DESERIALIZE_ITEM(NAME, MEMBER) \
+ if (const auto* item = nodeMap.FindPtr(NAME)) { \
+ Deserialize(MEMBER, *item); \
+ }
+
+// const auto& attributesMap = node.GetAttributes().AsMap();
+#define DESERIALIZE_ATTR(NAME, MEMBER) \
+ if (const auto* attr = attributesMap.FindPtr(NAME)) { \
+ Deserialize(MEMBER, *attr); \
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TSortColumn& sortColumn, NYson::IYsonConsumer* consumer)
+{
+ if (sortColumn.SortOrder() == ESortOrder::SO_ASCENDING) {
+ Serialize(sortColumn.Name(), consumer);
+ } else {
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("name").Value(sortColumn.Name())
+ .Item("sort_order").Value(ToString(sortColumn.SortOrder()))
+ .EndMap();
+ }
+}
+
+void Deserialize(TSortColumn& sortColumn, const TNode& node)
+{
+ if (node.IsString()) {
+ sortColumn = TSortColumn(node.AsString());
+ } else if (node.IsMap()) {
+ const auto& name = node["name"].AsString();
+ const auto& sortOrderString = node["sort_order"].AsString();
+ sortColumn = TSortColumn(name, ::FromString<ESortOrder>(sortOrderString));
+ } else {
+ ythrow yexception() << "Expected sort column to be string or map, got " << node.GetType();
+ }
+}
+
+template <class T, class TDerived>
+void SerializeOneOrMany(const TOneOrMany<T, TDerived>& oneOrMany, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).List(oneOrMany.Parts_);
+}
+
+template <class T, class TDerived>
+void DeserializeOneOrMany(TOneOrMany<T, TDerived>& oneOrMany, const TNode& node)
+{
+ Deserialize(oneOrMany.Parts_, node);
+}
+
+void Serialize(const TKey& key, NYson::IYsonConsumer* consumer)
+{
+ SerializeOneOrMany(key, consumer);
+}
+
+void Deserialize(TKey& key, const TNode& node)
+{
+ DeserializeOneOrMany(key, node);
+}
+
+void Serialize(const TSortColumns& sortColumns, NYson::IYsonConsumer* consumer)
+{
+ SerializeOneOrMany(sortColumns, consumer);
+}
+
+void Deserialize(TSortColumns& sortColumns, const TNode& node)
+{
+ DeserializeOneOrMany(sortColumns, node);
+}
+
+void Serialize(const TColumnNames& columnNames, NYson::IYsonConsumer* consumer)
+{
+ SerializeOneOrMany(columnNames, consumer);
+}
+
+void Deserialize(TColumnNames& columnNames, const TNode& node)
+{
+ DeserializeOneOrMany(columnNames, node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Deserialize(EValueType& valueType, const TNode& node)
+{
+ const auto& nodeStr = node.AsString();
+ static const THashMap<TString, EValueType> str2ValueType = {
+ {"int8", VT_INT8},
+ {"int16", VT_INT16},
+ {"int32", VT_INT32},
+ {"int64", VT_INT64},
+
+ {"uint8", VT_UINT8},
+ {"uint16", VT_UINT16},
+ {"uint32", VT_UINT32},
+ {"uint64", VT_UINT64},
+
+ {"boolean", VT_BOOLEAN},
+ {"double", VT_DOUBLE},
+
+ {"string", VT_STRING},
+ {"utf8", VT_UTF8},
+
+ {"any", VT_ANY},
+
+ {"null", VT_NULL},
+ {"void", VT_VOID},
+
+ {"date", VT_DATE},
+ {"datetime", VT_DATETIME},
+ {"timestamp", VT_TIMESTAMP},
+ {"interval", VT_INTERVAL},
+ {"float", VT_FLOAT},
+ {"json", VT_JSON},
+ };
+
+ auto it = str2ValueType.find(nodeStr);
+ if (it == str2ValueType.end()) {
+ ythrow yexception() << "Invalid value type '" << nodeStr << "'";
+ }
+
+ valueType = it->second;
+}
+
+void Deserialize(ESortOrder& sortOrder, const TNode& node)
+{
+ sortOrder = FromString<ESortOrder>(node.AsString());
+}
+
+void Deserialize(EOptimizeForAttr& optimizeFor, const TNode& node)
+{
+ optimizeFor = FromString<EOptimizeForAttr>(node.AsString());
+}
+
+void Deserialize(EErasureCodecAttr& erasureCodec, const TNode& node)
+{
+ erasureCodec = FromString<EErasureCodecAttr>(node.AsString());
+}
+
+void Deserialize(ESchemaModificationAttr& schemaModification, const TNode& node)
+{
+ schemaModification = FromString<ESchemaModificationAttr>(node.AsString());
+}
+
+void Serialize(const TColumnSchema& columnSchema, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("name").Value(columnSchema.Name())
+ .DoIf(!columnSchema.RawTypeV3().Defined(),
+ [&] (TFluentMap fluent) {
+ fluent.Item("type").Value(NDetail::ToString(columnSchema.Type()));
+ fluent.Item("required").Value(columnSchema.Required());
+ if (columnSchema.Type() == VT_ANY
+ && *columnSchema.TypeV3() != *NTi::Optional(NTi::Yson()))
+ {
+ // A lot of user canonize serialized schema.
+ // To be backward compatible we only set type_v3 for new types.
+ fluent.Item("type_v3").Value(columnSchema.TypeV3());
+ }
+ }
+ )
+ .DoIf(columnSchema.RawTypeV3().Defined(), [&] (TFluentMap fluent) {
+ const auto& rawTypeV3 = *columnSchema.RawTypeV3();
+ fluent.Item("type_v3").Value(rawTypeV3);
+
+ // We going set old fields `type` and `required` to be compatible
+ // with old clusters that doesn't support type_v3 yet.
+
+ // if type is simple return its name otherwise return empty optional
+ auto isRequired = [](TStringBuf simpleType) {
+ return simpleType != "null" && simpleType != "void";
+ };
+ auto getSimple = [] (const TNode& typeV3) -> TMaybe<TString> {
+ static const THashMap<TString,TString> typeV3ToOld = {
+ {"bool", "boolean"},
+ {"yson", "any"},
+ };
+ TMaybe<TString> result;
+ if (typeV3.IsString()) {
+ result = typeV3.AsString();
+ } else if (typeV3.IsMap() && typeV3.Size() == 1) {
+ Y_VERIFY(typeV3["type_name"].IsString(), "invalid type is passed");
+ result = typeV3["type_name"].AsString();
+ }
+ if (result) {
+ auto it = typeV3ToOld.find(*result);
+ if (it != typeV3ToOld.end()) {
+ result = it->second;
+ }
+ }
+ return result;
+ };
+ auto simplify = [&](const TNode& typeV3) -> TMaybe<std::pair<TString, bool>> {
+ auto simple = getSimple(typeV3);
+ if (simple) {
+ return std::make_pair(*simple, isRequired(*simple));
+ }
+ if (typeV3.IsMap() && typeV3["type_name"] == "optional") {
+ auto simpleItem = getSimple(typeV3["item"]);
+ if (simpleItem && isRequired(*simpleItem)) {
+ return std::make_pair(*simpleItem, false);
+ }
+ }
+ return {};
+ };
+
+ auto simplified = simplify(rawTypeV3);
+
+ if (simplified) {
+ const auto& [simpleType, required] = *simplified;
+ fluent
+ .Item("type").Value(simpleType)
+ .Item("required").Value(required);
+ return;
+ }
+ })
+ .DoIf(columnSchema.SortOrder().Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("sort_order").Value(ToString(*columnSchema.SortOrder()));
+ })
+ .DoIf(columnSchema.Lock().Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("lock").Value(*columnSchema.Lock());
+ })
+ .DoIf(columnSchema.Expression().Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("expression").Value(*columnSchema.Expression());
+ })
+ .DoIf(columnSchema.Aggregate().Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("aggregate").Value(*columnSchema.Aggregate());
+ })
+ .DoIf(columnSchema.Group().Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("group").Value(*columnSchema.Group());
+ })
+ .EndMap();
+}
+
+void Deserialize(TColumnSchema& columnSchema, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("name", columnSchema.Name_);
+ DESERIALIZE_ITEM("type_v3", columnSchema.RawTypeV3_);
+ DESERIALIZE_ITEM("sort_order", columnSchema.SortOrder_);
+ DESERIALIZE_ITEM("lock", columnSchema.Lock_);
+ DESERIALIZE_ITEM("expression", columnSchema.Expression_);
+ DESERIALIZE_ITEM("aggregate", columnSchema.Aggregate_);
+ DESERIALIZE_ITEM("group", columnSchema.Group_);
+
+ if (nodeMap.contains("type_v3")) {
+ NTi::TTypePtr type;
+ DESERIALIZE_ITEM("type_v3", type);
+ columnSchema.Type(type);
+ } else {
+ EValueType oldType = VT_INT64;
+ bool required = false;
+ DESERIALIZE_ITEM("type", oldType);
+ DESERIALIZE_ITEM("required", required);
+ columnSchema.Type(ToTypeV3(oldType, required));
+ }
+}
+
+void Serialize(const TTableSchema& tableSchema, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginAttributes()
+ .Item("strict").Value(tableSchema.Strict())
+ .Item("unique_keys").Value(tableSchema.UniqueKeys())
+ .EndAttributes()
+ .List(tableSchema.Columns());
+}
+
+void Deserialize(TTableSchema& tableSchema, const TNode& node)
+{
+ const auto& attributesMap = node.GetAttributes().AsMap();
+ DESERIALIZE_ATTR("strict", tableSchema.Strict_);
+ DESERIALIZE_ATTR("unique_keys", tableSchema.UniqueKeys_);
+ Deserialize(tableSchema.Columns_, node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TKeyBound& keyBound, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginList()
+ .Item().Value(ToString(keyBound.Relation()))
+ .Item().Value(keyBound.Key())
+ .EndList();
+}
+
+void Deserialize(TKeyBound& keyBound, const TNode& node)
+{
+ const auto& nodeList = node.AsList();
+ Y_ENSURE(nodeList.size() == 2);
+
+ const auto& relationNode = nodeList[0];
+ keyBound.Relation(::FromString<ERelation>(relationNode.AsString()));
+
+ const auto& keyNode = nodeList[1];
+ TKey key;
+ Deserialize(key, keyNode);
+ keyBound.Key(std::move(key));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TReadLimit& readLimit, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginMap()
+ .DoIf(readLimit.KeyBound_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("key_bound").Value(*readLimit.KeyBound_);
+ })
+ .DoIf(readLimit.Key_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("key").Value(*readLimit.Key_);
+ })
+ .DoIf(readLimit.RowIndex_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("row_index").Value(*readLimit.RowIndex_);
+ })
+ .DoIf(readLimit.Offset_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("offset").Value(*readLimit.Offset_);
+ })
+ .DoIf(readLimit.TabletIndex_.Defined(), [&] (TFluentMap fluent) {
+ fluent.Item("tablet_index").Value(*readLimit.TabletIndex_);
+ })
+ .EndMap();
+}
+
+void Deserialize(TReadLimit& readLimit, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("key_bound", readLimit.KeyBound_);
+ DESERIALIZE_ITEM("key", readLimit.Key_);
+ DESERIALIZE_ITEM("row_index", readLimit.RowIndex_);
+ DESERIALIZE_ITEM("offset", readLimit.Offset_);
+ DESERIALIZE_ITEM("tablet_index", readLimit.TabletIndex_);
+}
+
+void Serialize(const TReadRange& readRange, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginMap()
+ .DoIf(!IsTrivial(readRange.LowerLimit_), [&] (TFluentMap fluent) {
+ fluent.Item("lower_limit").Value(readRange.LowerLimit_);
+ })
+ .DoIf(!IsTrivial(readRange.UpperLimit_), [&] (TFluentMap fluent) {
+ fluent.Item("upper_limit").Value(readRange.UpperLimit_);
+ })
+ .DoIf(!IsTrivial(readRange.Exact_), [&] (TFluentMap fluent) {
+ fluent.Item("exact").Value(readRange.Exact_);
+ })
+ .EndMap();
+}
+
+void Deserialize(TReadRange& readRange, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("lower_limit", readRange.LowerLimit_);
+ DESERIALIZE_ITEM("upper_limit", readRange.UpperLimit_);
+ DESERIALIZE_ITEM("exact", readRange.Exact_);
+}
+
+void Serialize(const THashMap<TString, TString>& renameColumns, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .DoMapFor(renameColumns, [] (TFluentMap fluent, const auto& item) {
+ fluent.Item(item.first).Value(item.second);
+ });
+}
+
+void Serialize(const TRichYPath& path, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginAttributes()
+ .DoIf(path.GetRanges().Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("ranges").List(*path.GetRanges());
+ })
+ .DoIf(path.Columns_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("columns").Value(*path.Columns_);
+ })
+ .DoIf(path.Append_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("append").Value(*path.Append_);
+ })
+ .DoIf(path.PartiallySorted_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("partially_sorted").Value(*path.PartiallySorted_);
+ })
+ .DoIf(!path.SortedBy_.Parts_.empty(), [&] (TFluentAttributes fluent) {
+ fluent.Item("sorted_by").Value(path.SortedBy_);
+ })
+ .DoIf(path.Teleport_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("teleport").Value(*path.Teleport_);
+ })
+ .DoIf(path.Primary_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("primary").Value(*path.Primary_);
+ })
+ .DoIf(path.Foreign_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("foreign").Value(*path.Foreign_);
+ })
+ .DoIf(path.RowCountLimit_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("row_count_limit").Value(*path.RowCountLimit_);
+ })
+ .DoIf(path.FileName_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("file_name").Value(*path.FileName_);
+ })
+ .DoIf(path.OriginalPath_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("original_path").Value(*path.OriginalPath_);
+ })
+ .DoIf(path.Executable_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("executable").Value(*path.Executable_);
+ })
+ .DoIf(path.Format_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("format").Value(*path.Format_);
+ })
+ .DoIf(path.Schema_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("schema").Value(*path.Schema_);
+ })
+ .DoIf(path.Timestamp_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("timestamp").Value(*path.Timestamp_);
+ })
+ .DoIf(path.CompressionCodec_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("compression_codec").Value(*path.CompressionCodec_);
+ })
+ .DoIf(path.ErasureCodec_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("erasure_codec").Value(ToString(*path.ErasureCodec_));
+ })
+ .DoIf(path.SchemaModification_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("schema_modification").Value(ToString(*path.SchemaModification_));
+ })
+ .DoIf(path.OptimizeFor_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("optimize_for").Value(ToString(*path.OptimizeFor_));
+ })
+ .DoIf(path.TransactionId_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("transaction_id").Value(GetGuidAsString(*path.TransactionId_));
+ })
+ .DoIf(path.RenameColumns_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("rename_columns").Value(*path.RenameColumns_);
+ })
+ .DoIf(path.BypassArtifactCache_.Defined(), [&] (TFluentAttributes fluent) {
+ fluent.Item("bypass_artifact_cache").Value(*path.BypassArtifactCache_);
+ })
+ .EndAttributes()
+ .Value(path.Path_);
+}
+
+void Deserialize(TRichYPath& path, const TNode& node)
+{
+ path = {};
+
+ const auto& attributesMap = node.GetAttributes().AsMap();
+ DESERIALIZE_ATTR("ranges", path.MutableRanges());
+ DESERIALIZE_ATTR("columns", path.Columns_);
+ DESERIALIZE_ATTR("append", path.Append_);
+ DESERIALIZE_ATTR("partially_sorted", path.PartiallySorted_);
+ DESERIALIZE_ATTR("sorted_by", path.SortedBy_);
+ DESERIALIZE_ATTR("teleport", path.Teleport_);
+ DESERIALIZE_ATTR("primary", path.Primary_);
+ DESERIALIZE_ATTR("foreign", path.Foreign_);
+ DESERIALIZE_ATTR("row_count_limit", path.RowCountLimit_);
+ DESERIALIZE_ATTR("file_name", path.FileName_);
+ DESERIALIZE_ATTR("original_path", path.OriginalPath_);
+ DESERIALIZE_ATTR("executable", path.Executable_);
+ DESERIALIZE_ATTR("format", path.Format_);
+ DESERIALIZE_ATTR("schema", path.Schema_);
+ DESERIALIZE_ATTR("timestamp", path.Timestamp_);
+ DESERIALIZE_ATTR("compression_codec", path.CompressionCodec_);
+ DESERIALIZE_ATTR("erasure_codec", path.ErasureCodec_);
+ DESERIALIZE_ATTR("schema_modification", path.SchemaModification_);
+ DESERIALIZE_ATTR("optimize_for", path.OptimizeFor_);
+ DESERIALIZE_ATTR("transaction_id", path.TransactionId_);
+ DESERIALIZE_ATTR("rename_columns", path.RenameColumns_);
+ DESERIALIZE_ATTR("bypass_artifact_cache", path.BypassArtifactCache_);
+ Deserialize(path.Path_, node);
+}
+
+void Serialize(const TAttributeFilter& filter, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).List(filter.Attributes_);
+}
+
+void Deserialize(TTableColumnarStatistics& statistics, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("column_data_weights", statistics.ColumnDataWeight);
+ DESERIALIZE_ITEM("legacy_chunks_data_weight", statistics.LegacyChunksDataWeight);
+ DESERIALIZE_ITEM("timestamp_total_weight", statistics.TimestampTotalWeight);
+}
+
+void Deserialize(TMultiTablePartition::TStatistics& statistics, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("chunk_count", statistics.ChunkCount);
+ DESERIALIZE_ITEM("data_weight", statistics.DataWeight);
+ DESERIALIZE_ITEM("row_count", statistics.RowCount);
+}
+
+void Deserialize(TMultiTablePartition& partition, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("table_ranges", partition.TableRanges);
+ DESERIALIZE_ITEM("aggregate_statistics", partition.AggregateStatistics);
+}
+
+void Deserialize(TMultiTablePartitions& partitions, const TNode& node)
+{
+ const auto& nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("partitions", partitions.Partitions);
+}
+
+void Serialize(const TGUID& value, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).Value(GetGuidAsString(value));
+}
+
+void Deserialize(TGUID& value, const TNode& node)
+{
+ value = GetGuid(node.AsString());
+}
+
+void Deserialize(TTabletInfo& value, const TNode& node)
+{
+ auto nodeMap = node.AsMap();
+ DESERIALIZE_ITEM("total_row_count", value.TotalRowCount)
+ DESERIALIZE_ITEM("trimmed_row_count", value.TrimmedRowCount)
+ DESERIALIZE_ITEM("barrier_timestamp", value.BarrierTimestamp)
+}
+
+void Serialize(const NTi::TTypePtr& type, NYson::IYsonConsumer* consumer)
+{
+ auto yson = NTi::NIo::SerializeYson(type.Get());
+ ::NYson::ParseYsonStringBuffer(yson, consumer);
+}
+
+void Deserialize(NTi::TTypePtr& type, const TNode& node)
+{
+ auto yson = NodeToYsonString(node, NYson::EYsonFormat::Binary);
+ type = NTi::NIo::DeserializeYson(*NTi::HeapFactory(), yson);
+}
+
+#undef DESERIALIZE_ITEM
+#undef DESERIALIZE_ATTR
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/serialize.h b/yt/cpp/mapreduce/interface/serialize.h
new file mode 100644
index 0000000000..223dd446ba
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/serialize.h
@@ -0,0 +1,90 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/serialize.h
+///
+/// Header containing declaration of functions for serializing to/from YSON.
+
+#include "common.h"
+
+#include <library/cpp/type_info/fwd.h>
+
+namespace NYT::NYson {
+struct IYsonConsumer;
+} // namespace NYT::NYson
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void Deserialize(TMaybe<T>& value, const TNode& node)
+{
+ value.ConstructInPlace();
+ Deserialize(value.GetRef(), node);
+}
+
+template <class T>
+void Deserialize(TVector<T>& value, const TNode& node)
+{
+ for (const auto& element : node.AsList()) {
+ value.emplace_back();
+ Deserialize(value.back(), element);
+ }
+}
+
+template <class T>
+void Deserialize(THashMap<TString, T>& value, const TNode& node)
+{
+ for (const auto& item : node.AsMap()) {
+ Deserialize(value[item.first], item.second);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TKey& key, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TKey& key, const TNode& node);
+
+void Serialize(const TSortColumns& sortColumns, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TSortColumns& sortColumns, const TNode& node);
+
+void Serialize(const TColumnNames& columnNames, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TColumnNames& columnNames, const TNode& node);
+
+void Serialize(const TSortColumn& sortColumn, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TSortColumn& sortColumn, const TNode& node);
+
+void Serialize(const TKeyBound& keyBound, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TKeyBound& keyBound, const TNode& node);
+
+void Serialize(const TReadLimit& readLimit, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TReadLimit& readLimit, const TNode& node);
+
+void Serialize(const TReadRange& readRange, NYT::NYson::IYsonConsumer* consumer);
+
+void Serialize(const TRichYPath& path, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TRichYPath& path, const TNode& node);
+
+void Serialize(const TAttributeFilter& filter, NYT::NYson::IYsonConsumer* consumer);
+
+void Serialize(const TColumnSchema& columnSchema, NYT::NYson::IYsonConsumer* consumer);
+void Serialize(const TTableSchema& tableSchema, NYT::NYson::IYsonConsumer* consumer);
+
+void Deserialize(EValueType& valueType, const TNode& node);
+void Deserialize(TTableSchema& tableSchema, const TNode& node);
+void Deserialize(TColumnSchema& columnSchema, const TNode& node);
+void Deserialize(TTableColumnarStatistics& statistics, const TNode& node);
+void Deserialize(TMultiTablePartition& partition, const TNode& node);
+void Deserialize(TMultiTablePartitions& partitions, const TNode& node);
+void Deserialize(TTabletInfo& tabletInfos, const TNode& node);
+
+void Serialize(const TGUID& path, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(TGUID& value, const TNode& node);
+
+void Serialize(const NTi::TTypePtr& type, NYT::NYson::IYsonConsumer* consumer);
+void Deserialize(NTi::TTypePtr& type, const TNode& node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/serialize_ut.cpp b/yt/cpp/mapreduce/interface/serialize_ut.cpp
new file mode 100644
index 0000000000..59d4501ee8
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/serialize_ut.cpp
@@ -0,0 +1,49 @@
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/generic/serialized_enum.h>
+
+using namespace NYT;
+
+Y_UNIT_TEST_SUITE(Serialization)
+{
+ Y_UNIT_TEST(TableSchema)
+ {
+ auto schema = TTableSchema()
+ .AddColumn(TColumnSchema().Name("a").Type(EValueType::VT_STRING).SortOrder(SO_ASCENDING))
+ .AddColumn(TColumnSchema().Name("b").Type(EValueType::VT_UINT64))
+ .AddColumn(TColumnSchema().Name("c").Type(EValueType::VT_INT64, true));
+
+ auto schemaNode = schema.ToNode();
+ UNIT_ASSERT(schemaNode.IsList());
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode.Size(), 3);
+
+
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[0]["name"], "a");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[0]["type"], "string");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[0]["required"], false);
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[0]["sort_order"], "ascending");
+
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[1]["name"], "b");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[1]["type"], "uint64");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[1]["required"], false);
+
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[2]["name"], "c");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[2]["type"], "int64");
+ UNIT_ASSERT_VALUES_EQUAL(schemaNode[2]["required"], true);
+ }
+
+ Y_UNIT_TEST(ValueTypeSerialization)
+ {
+ for (const auto value : GetEnumAllValues<EValueType>()) {
+ TNode serialized = NYT::NDetail::ToString(value);
+ EValueType deserialized;
+ Deserialize(deserialized, serialized);
+ UNIT_ASSERT_VALUES_EQUAL(value, deserialized);
+ }
+ }
+}
diff --git a/yt/cpp/mapreduce/interface/skiff_row.cpp b/yt/cpp/mapreduce/interface/skiff_row.cpp
new file mode 100644
index 0000000000..7838bdaee9
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/skiff_row.cpp
@@ -0,0 +1 @@
+#include "skiff_row.h"
diff --git a/yt/cpp/mapreduce/interface/skiff_row.h b/yt/cpp/mapreduce/interface/skiff_row.h
new file mode 100644
index 0000000000..5dd335cb65
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/skiff_row.h
@@ -0,0 +1,127 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/skiff_row.h
+/// Header containing interfaces that you need to define for using TSkiffRowTableReader
+/// What you need to do for your struct type TMyType:
+/// 1. Write `true` specialization TIsSkiffRow<TMyType>;
+/// 2. Write specialization GetSkiffSchema<TMyType>();
+/// 3. Write your own parser derived from ISkiffRowParser and write specialization GetSkiffParser<TMyType>() which returns this parser.
+
+#include "fwd.h"
+
+#include <yt/cpp/mapreduce/skiff/skiff_schema.h>
+
+#include <yt/cpp/mapreduce/interface/format.h>
+
+#include <library/cpp/skiff/skiff.h>
+
+#include <util/generic/maybe.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Need to write `true_type` specialization for your row type `T`.
+/// And implement two functions: `GetSkiffSchema` and `CreateSkiffParser`.
+///
+/// Example:
+///
+/// template <>
+/// struct TIsSkiffRow<T>
+/// : std::true_type
+/// { };
+///
+template<class T>
+struct TIsSkiffRow
+ : std::false_type
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Return skiff schema for row type `T`.
+/// Need to write its specialization.
+template <typename T>
+NSkiff::TSkiffSchemaPtr GetSkiffSchema(const TMaybe<TSkiffRowHints>& /*hints*/)
+{
+ static_assert(TDependentFalse<T>, "Unimplemented `GetSkiffSchema` method");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Allow to parse rows as user's structs from stream (TCheckedInDebugSkiffParser).
+/// Need to write derived class for your own row type.
+///
+/// Example:
+///
+/// class TMySkiffRowParser : public ISkiffRowParser
+/// {
+/// public:
+/// TMySkiffRowParser(TMySkiffRow* row)
+/// : Row_(row)
+/// {}
+///
+/// void Parse(NSkiff::TCheckedInDebugSkiffParser* parser)
+/// . {
+/// Row_->SomeInt64Field = parser->ParseInt64();
+/// }
+///
+/// private:
+/// TMySkiffRow* Row_;
+/// }
+///
+class ISkiffRowParser
+ : public TThrRefBase
+{
+public:
+ //! Read one row from parser
+ virtual void Parse(NSkiff::TCheckedInDebugSkiffParser* /*parser*/) = 0;
+};
+
+//! Creates a parser for row type `T`.
+template <typename T>
+ISkiffRowParserPtr CreateSkiffParser(T* /*row*/, const TMaybe<TSkiffRowHints>& /*hints*/)
+{
+ static_assert(TDependentFalse<T>, "Unimplemented `CreateSkiffParser` function");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Allow to skip row content without getting row.
+/// By default row will be parsed using your parser derived from ISkiffRowParser.
+/// If you want, you can write more optimal skipper, but it isn't required.
+class ISkiffRowSkipper
+ : public TThrRefBase
+{
+public:
+ virtual void SkipRow(NSkiff::TCheckedInDebugSkiffParser* /*parser*/) = 0;
+};
+
+//! Default ISkiffRowSkipper implementation.
+template <typename T>
+class TSkiffRowSkipper : public ISkiffRowSkipper {
+public:
+ explicit TSkiffRowSkipper(const TMaybe<TSkiffRowHints>& hints)
+ : Parser_(CreateSkiffParser<T>(&Row_, hints))
+ { }
+
+ void SkipRow(NSkiff::TCheckedInDebugSkiffParser* parser) {
+ Parser_->Parse(parser);
+ }
+
+private:
+ T Row_;
+ ISkiffRowParserPtr Parser_;
+};
+
+//! Creates a skipper for row type 'T'.
+/// You don't need to write its specialization.
+template <typename T>
+ISkiffRowSkipperPtr CreateSkiffSkipper(const TMaybe<TSkiffRowHints>& hints)
+{
+ return ::MakeIntrusive<TSkiffRowSkipper<T>>(hints);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/tvm.cpp b/yt/cpp/mapreduce/interface/tvm.cpp
new file mode 100644
index 0000000000..bfa3f0304e
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/tvm.cpp
@@ -0,0 +1 @@
+#include "tvm.h"
diff --git a/yt/cpp/mapreduce/interface/tvm.h b/yt/cpp/mapreduce/interface/tvm.h
new file mode 100644
index 0000000000..d8d16d841b
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/tvm.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <yt/yt/library/tvm/tvm_base.h>
+
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+
+namespace NYT::NAuth {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// This wrapper is required because NYT::NAuth::IServiceTicketAuthPtr is NYT::TIntrusivePtr,
+/// and, if we used this pointer in interfaces of `mapreduce/yt` client, a lot of users of this library
+/// could get unexpected build errors that `TIntrusivePtr` is ambigious
+/// (from `::` namespace and from `::NYT::` namespace).
+/// So we use this wrapper in our interfaces to avoid such problems for users.
+struct IServiceTicketAuthPtrWrapper
+{
+ //
+ /// Construct wrapper from NYT::TIntrusivePtr
+ ///
+ /// This constructor is implicit so users can transparently pass NYT::TIntrusivePtr to the functions of
+ /// mapreduce/yt client.
+ template <class T, class = typename std::enable_if_t<std::is_convertible_v<T*, IServiceTicketAuth*>>>
+ IServiceTicketAuthPtrWrapper(const TIntrusivePtr<T> ptr)
+ : Ptr(ptr)
+ {
+ }
+
+ /// Wrapped pointer
+ NYT::TIntrusivePtr<IServiceTicketAuth> Ptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NAuth
diff --git a/yt/cpp/mapreduce/interface/ut/ya.make b/yt/cpp/mapreduce/interface/ut/ya.make
new file mode 100644
index 0000000000..0219e6430c
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/ut/ya.make
@@ -0,0 +1,25 @@
+UNITTEST_FOR(yt/cpp/mapreduce/interface)
+
+SRCS(
+ common_ut.cpp
+ config_ut.cpp
+ error_ut.cpp
+ format_ut.cpp
+ job_counters_ut.cpp
+ job_statistics_ut.cpp
+ operation_ut.cpp
+ proto3_ut.proto
+ protobuf_table_schema_ut.cpp
+ protobuf_file_options_ut.cpp
+ protobuf_table_schema_ut.proto
+ protobuf_file_options_ut.proto
+ serialize_ut.cpp
+)
+
+PEERDIR(
+ contrib/libs/protobuf
+ library/cpp/testing/unittest
+ yt/yt_proto/yt/formats
+)
+
+END()
diff --git a/yt/cpp/mapreduce/interface/wait_proxy.h b/yt/cpp/mapreduce/interface/wait_proxy.h
new file mode 100644
index 0000000000..f7d8e0638e
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/wait_proxy.h
@@ -0,0 +1,54 @@
+#pragma once
+
+///
+/// @file yt/cpp/mapreduce/interface/serialize.h
+///
+/// Header containing interface to enable customizable waiting.
+
+#include <yt/cpp/mapreduce/interface/common.h>
+
+#include <util/datetime/base.h>
+
+namespace NThreading {
+template <typename T>
+class TFuture;
+}
+
+class TSystemEvent;
+class TCondVar;
+class TMutex;
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+///
+/// @brief Interface to facilitate customizable waiting.
+///
+/// All the waiting functions in the library are obliged to use the methods of a wait proxy instead of direct function calls.
+class IWaitProxy
+ : public TThrRefBase
+{
+public:
+ virtual ~IWaitProxy() = default;
+
+ ///
+ /// @brief Wait for the future setting with timeout.
+ virtual bool WaitFuture(const ::NThreading::TFuture<void>& future, TDuration timeout) = 0;
+
+ ///
+ /// @brief Wait for a system event with timeout.
+ virtual bool WaitEvent(TSystemEvent& event, TDuration timeout) = 0;
+
+ ///
+ /// @brief Wait for the notification on the condition variable with timeout.
+ virtual bool WaitCondVar(TCondVar& condVar, TMutex& mutex, TDuration timeout) = 0;
+
+ ///
+ /// @brief Sleep in the current thread for (approximately) specified amount of time.
+ virtual void Sleep(TDuration timeout) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/interface/ya.make b/yt/cpp/mapreduce/interface/ya.make
new file mode 100644
index 0000000000..0e94f14633
--- /dev/null
+++ b/yt/cpp/mapreduce/interface/ya.make
@@ -0,0 +1,46 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ batch_request.cpp
+ client.cpp
+ client_method_options.cpp
+ common.cpp
+ config.cpp
+ cypress.cpp
+ errors.cpp
+ format.cpp
+ job_counters.cpp
+ job_statistics.cpp
+ io.cpp
+ operation.cpp
+ protobuf_format.cpp
+ serialize.cpp
+ skiff_row.cpp
+ tvm.cpp
+)
+
+PEERDIR(
+ contrib/libs/protobuf
+ library/cpp/type_info
+ library/cpp/threading/future
+ library/cpp/yson/node
+ yt/cpp/mapreduce/interface/logging
+ yt/yt_proto/yt/formats
+ yt/yt/library/tvm
+)
+
+GENERATE_ENUM_SERIALIZATION(client_method_options.h)
+GENERATE_ENUM_SERIALIZATION(client.h)
+GENERATE_ENUM_SERIALIZATION(common.h)
+GENERATE_ENUM_SERIALIZATION(config.h)
+GENERATE_ENUM_SERIALIZATION(cypress.h)
+GENERATE_ENUM_SERIALIZATION(job_counters.h)
+GENERATE_ENUM_SERIALIZATION(job_statistics.h)
+GENERATE_ENUM_SERIALIZATION(operation.h)
+GENERATE_ENUM_SERIALIZATION(protobuf_format.h)
+
+END()
+
+RECURSE_FOR_TESTS(ut)
diff --git a/yt/cpp/mapreduce/io/counting_raw_reader.cpp b/yt/cpp/mapreduce/io/counting_raw_reader.cpp
new file mode 100644
index 0000000000..6a918bdddb
--- /dev/null
+++ b/yt/cpp/mapreduce/io/counting_raw_reader.cpp
@@ -0,0 +1,38 @@
+#include "counting_raw_reader.h"
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TCountingRawTableReader::Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex)
+{
+ return Reader_->Retry(rangeIndex, rowIndex);
+}
+
+void TCountingRawTableReader::ResetRetries()
+{
+ Reader_->ResetRetries();
+}
+
+bool TCountingRawTableReader::HasRangeIndices() const
+{
+ return Reader_->HasRangeIndices();
+}
+
+size_t TCountingRawTableReader::GetReadByteCount() const
+{
+ return ReadByteCount_;
+}
+
+size_t TCountingRawTableReader::DoRead(void* buf, size_t len)
+{
+ auto readLen = Reader_->Read(buf, len);
+ ReadByteCount_ += readLen;
+ return readLen;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/counting_raw_reader.h b/yt/cpp/mapreduce/io/counting_raw_reader.h
new file mode 100644
index 0000000000..3b6705c5e4
--- /dev/null
+++ b/yt/cpp/mapreduce/io/counting_raw_reader.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+namespace NDetail {
+
+class TCountingRawTableReader
+ final : public TRawTableReader
+{
+public:
+ TCountingRawTableReader(::TIntrusivePtr<TRawTableReader> reader)
+ : Reader_(std::move(reader))
+ { }
+
+ bool Retry(const TMaybe<ui32>& rangeIndex, const TMaybe<ui64>& rowIndex) override;
+ void ResetRetries() override;
+ bool HasRangeIndices() const override;
+
+ size_t GetReadByteCount() const;
+
+protected:
+ size_t DoRead(void* buf, size_t len) override;
+
+private:
+ ::TIntrusivePtr<TRawTableReader> Reader_;
+ size_t ReadByteCount_ = 0;
+};
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/helpers.h b/yt/cpp/mapreduce/io/helpers.h
new file mode 100644
index 0000000000..5dbbf20906
--- /dev/null
+++ b/yt/cpp/mapreduce/io/helpers.h
@@ -0,0 +1,130 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+struct TIOOptionsTraits;
+
+template <>
+struct TIOOptionsTraits<TFileReaderOptions>
+{
+ static constexpr const char* const ConfigName = "file_reader";
+};
+template <>
+struct TIOOptionsTraits<TFileWriterOptions>
+{
+ static constexpr const char* const ConfigName = "file_writer";
+};
+template <>
+struct TIOOptionsTraits<TTableReaderOptions>
+{
+ static constexpr const char* const ConfigName = "table_reader";
+};
+template <>
+struct TIOOptionsTraits<TTableWriterOptions>
+{
+ static constexpr const char* const ConfigName = "table_writer";
+};
+
+template <class TOptions>
+TNode FormIORequestParameters(
+ const TRichYPath& path,
+ const TOptions& options)
+{
+ auto params = PathToParamNode(path);
+ if (options.Config_) {
+ params[TIOOptionsTraits<TOptions>::ConfigName] = *options.Config_;
+ }
+ return params;
+}
+
+template <>
+inline TNode FormIORequestParameters(
+ const TRichYPath& path,
+ const TFileReaderOptions& options)
+{
+ auto params = PathToParamNode(path);
+ if (options.Config_) {
+ params[TIOOptionsTraits<TTableReaderOptions>::ConfigName] = *options.Config_;
+ }
+ if (options.Offset_) {
+ params["offset"] = *options.Offset_;
+ }
+ if (options.Length_) {
+ params["length"] = *options.Length_;
+ }
+ return params;
+}
+
+static void AddWriterOptionsToNode(const TWriterOptions& options, TNode* node)
+{
+ if (options.EnableEarlyFinish_) {
+ (*node)["enable_early_finish"] = *options.EnableEarlyFinish_;
+ }
+ if (options.UploadReplicationFactor_) {
+ (*node)["upload_replication_factor"] = *options.UploadReplicationFactor_;
+ }
+ if (options.MinUploadReplicationFactor_) {
+ (*node)["min_upload_replication_factor"] = *options.MinUploadReplicationFactor_;
+ }
+ if (options.DesiredChunkSize_) {
+ (*node)["desired_chunk_size"] = *options.DesiredChunkSize_;
+ }
+}
+
+template <>
+inline TNode FormIORequestParameters(
+ const TRichYPath& path,
+ const TFileWriterOptions& options)
+{
+ auto params = PathToParamNode(path);
+ TNode fileWriter = TNode::CreateMap();
+ if (options.Config_) {
+ fileWriter = *options.Config_;
+ }
+ if (options.WriterOptions_) {
+ AddWriterOptionsToNode(*options.WriterOptions_, &fileWriter);
+ }
+ if (fileWriter.Empty()) {
+ AddWriterOptionsToNode(
+ TWriterOptions()
+ .EnableEarlyFinish(true)
+ .UploadReplicationFactor(3)
+ .MinUploadReplicationFactor(2),
+ &fileWriter);
+ }
+ params[TIOOptionsTraits<TFileWriterOptions>::ConfigName] = fileWriter;
+ if (options.ComputeMD5_) {
+ params["compute_md5"] = *options.ComputeMD5_;
+ }
+ return params;
+}
+
+template <>
+inline TNode FormIORequestParameters(
+ const TRichYPath& path,
+ const TTableWriterOptions& options)
+{
+ auto params = PathToParamNode(path);
+ auto tableWriter = TConfig::Get()->TableWriter;
+ if (options.Config_) {
+ MergeNodes(tableWriter, *options.Config_);
+ }
+ if (options.WriterOptions_) {
+ AddWriterOptionsToNode(*options.WriterOptions_, &tableWriter);
+ }
+ if (!tableWriter.Empty()) {
+ params[TIOOptionsTraits<TTableWriterOptions>::ConfigName] = std::move(tableWriter);
+ }
+ return params;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/yt/cpp/mapreduce/io/job_reader.cpp b/yt/cpp/mapreduce/io/job_reader.cpp
new file mode 100644
index 0000000000..39056f00e2
--- /dev/null
+++ b/yt/cpp/mapreduce/io/job_reader.cpp
@@ -0,0 +1,46 @@
+#include "job_reader.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobReader::TJobReader(int fd)
+ : TJobReader(Duplicate(fd))
+{ }
+
+TJobReader::TJobReader(const TFile& file)
+ : FdFile_(file)
+ , FdInput_(FdFile_)
+ , BufferedInput_(&FdInput_, BUFFER_SIZE)
+{ }
+
+bool TJobReader::Retry(const TMaybe<ui32>& /*rangeIndex*/, const TMaybe<ui64>& /*rowIndex*/)
+{
+ return false;
+}
+
+void TJobReader::ResetRetries()
+{ }
+
+bool TJobReader::HasRangeIndices() const
+{
+ return true;
+}
+
+size_t TJobReader::DoRead(void* buf, size_t len)
+{
+ return BufferedInput_.Read(buf, len);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRawTableReaderPtr CreateRawJobReader(int fd)
+{
+ return ::MakeIntrusive<TJobReader>(fd);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/job_reader.h b/yt/cpp/mapreduce/io/job_reader.h
new file mode 100644
index 0000000000..ce62ec180f
--- /dev/null
+++ b/yt/cpp/mapreduce/io/job_reader.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <util/stream/buffered.h>
+#include <util/stream/file.h>
+#include <util/system/file.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJobReader
+ : public TRawTableReader
+{
+public:
+ explicit TJobReader(int fd);
+ explicit TJobReader(const TFile& file);
+
+ virtual bool Retry( const TMaybe<ui32>& /*rangeIndex*/, const TMaybe<ui64>& /*rowIndex*/) override;
+ virtual void ResetRetries() override;
+ virtual bool HasRangeIndices() const override;
+
+protected:
+ size_t DoRead(void* buf, size_t len) override;
+
+private:
+ TFile FdFile_;
+ TUnbufferedFileInput FdInput_;
+ TBufferedInput BufferedInput_;
+
+ static const size_t BUFFER_SIZE = 64 << 10;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/job_writer.cpp b/yt/cpp/mapreduce/io/job_writer.cpp
new file mode 100644
index 0000000000..d08bb0a665
--- /dev/null
+++ b/yt/cpp/mapreduce/io/job_writer.cpp
@@ -0,0 +1,68 @@
+#include "job_writer.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <util/system/file.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobWriter::TStream::TStream(int fd)
+ : TStream(Duplicate(fd))
+{ }
+
+TJobWriter::TStream::TStream(const TFile& file)
+ : FdFile(file)
+ , FdOutput(FdFile)
+ , BufferedOutput(&FdOutput, BUFFER_SIZE)
+{ }
+
+TJobWriter::TStream::~TStream()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobWriter::TJobWriter(size_t outputTableCount)
+{
+ for (size_t i = 0; i < outputTableCount; ++i) {
+ Streams_.emplace_back(MakeHolder<TStream>(int(i * 3 + 1)));
+ }
+}
+
+TJobWriter::TJobWriter(const TVector<TFile>& fileList)
+{
+ for (const auto& f : fileList) {
+ Streams_.emplace_back(MakeHolder<TStream>(f));
+ }
+}
+
+size_t TJobWriter::GetStreamCount() const
+{
+ return Streams_.size();
+}
+
+IOutputStream* TJobWriter::GetStream(size_t tableIndex) const
+{
+ if (tableIndex >= Streams_.size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << Streams_.size() << ")";
+ }
+ return &Streams_[tableIndex]->BufferedOutput;
+}
+
+void TJobWriter::OnRowFinished(size_t)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+THolder<IProxyOutput> CreateRawJobWriter(size_t outputTableCount)
+{
+ return ::MakeHolder<TJobWriter>(outputTableCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/job_writer.h b/yt/cpp/mapreduce/io/job_writer.h
new file mode 100644
index 0000000000..9b24650640
--- /dev/null
+++ b/yt/cpp/mapreduce/io/job_writer.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <util/generic/vector.h>
+#include <util/generic/ptr.h>
+#include <util/stream/file.h>
+#include <util/stream/buffered.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJobWriter
+ : public IProxyOutput
+{
+public:
+ explicit TJobWriter(size_t outputTableCount);
+ explicit TJobWriter(const TVector<TFile>& fileList);
+
+ size_t GetStreamCount() const override;
+ IOutputStream* GetStream(size_t tableIndex) const override;
+ void OnRowFinished(size_t tableIndex) override;
+
+private:
+ struct TStream {
+ TFile FdFile;
+ TUnbufferedFileOutput FdOutput;
+ TBufferedOutput BufferedOutput;
+
+ explicit TStream(int fd);
+ explicit TStream(const TFile& file);
+ ~TStream();
+
+ static const size_t BUFFER_SIZE = 1 << 20;
+ };
+
+ TVector<THolder<TStream>> Streams_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/lenval_table_reader.cpp b/yt/cpp/mapreduce/io/lenval_table_reader.cpp
new file mode 100644
index 0000000000..98274c7996
--- /dev/null
+++ b/yt/cpp/mapreduce/io/lenval_table_reader.cpp
@@ -0,0 +1,198 @@
+#include "lenval_table_reader.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <util/string/printf.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const i32 CONTROL_ATTR_TABLE_INDEX = -1;
+const i32 CONTROL_ATTR_KEY_SWITCH = -2;
+const i32 CONTROL_ATTR_RANGE_INDEX = -3;
+const i32 CONTROL_ATTR_ROW_INDEX = -4;
+const i32 CONTROL_ATTR_END_OF_STREAM = -5;
+const i32 CONTROL_ATTR_TABLET_INDEX = -6;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLenvalTableReader::TLenvalTableReader(::TIntrusivePtr<TRawTableReader> input)
+ : Input_(std::move(input))
+{
+ TLenvalTableReader::Next();
+}
+
+TLenvalTableReader::~TLenvalTableReader()
+{ }
+
+void TLenvalTableReader::CheckValidity() const
+{
+ if (!IsValid()) {
+ ythrow yexception() << "Iterator is not valid";
+ }
+}
+
+bool TLenvalTableReader::IsValid() const
+{
+ return Valid_;
+}
+
+void TLenvalTableReader::Next()
+{
+ if (!RowTaken_) {
+ SkipRow();
+ }
+
+ CheckValidity();
+
+ if (RowIndex_) {
+ ++*RowIndex_;
+ }
+
+ while (true) {
+ try {
+ i32 value = 0;
+ if (!ReadInteger(&value, true)) {
+ return;
+ }
+
+ while (value < 0 && !IsEndOfStream_) {
+ switch (value) {
+ case CONTROL_ATTR_KEY_SWITCH:
+ if (!AtStart_) {
+ Valid_ = false;
+ return;
+ } else {
+ ReadInteger(&value);
+ }
+ break;
+
+ case CONTROL_ATTR_TABLE_INDEX: {
+ ui32 tmp = 0;
+ ReadInteger(&tmp);
+ TableIndex_ = tmp;
+ ReadInteger(&value);
+ break;
+ }
+ case CONTROL_ATTR_ROW_INDEX: {
+ ui64 tmp = 0;
+ ReadInteger(&tmp);
+ RowIndex_ = tmp;
+ ReadInteger(&value);
+ break;
+ }
+ case CONTROL_ATTR_RANGE_INDEX: {
+ ui32 tmp = 0;
+ ReadInteger(&tmp);
+ RangeIndex_ = tmp;
+ ReadInteger(&value);
+ break;
+ }
+ case CONTROL_ATTR_TABLET_INDEX: {
+ ui64 tmp = 0;
+ ReadInteger(&tmp);
+ TabletIndex_ = tmp;
+ ReadInteger(&value);
+ break;
+ }
+ case CONTROL_ATTR_END_OF_STREAM: {
+ IsEndOfStream_ = true;
+ break;
+ }
+ default:
+ ythrow yexception() <<
+ Sprintf("Invalid control integer %d in lenval stream", value);
+ }
+ }
+
+ Length_ = static_cast<ui32>(value);
+ RowTaken_ = false;
+ AtStart_ = false;
+ } catch (const std::exception& e) {
+ if (!PrepareRetry()) {
+ throw;
+ }
+ continue;
+ }
+ break;
+ }
+}
+
+bool TLenvalTableReader::Retry()
+{
+ if (PrepareRetry()) {
+ RowTaken_ = true;
+ Next();
+ return true;
+ }
+ return false;
+}
+
+void TLenvalTableReader::NextKey()
+{
+ while (Valid_) {
+ Next();
+ }
+
+ if (Finished_) {
+ return;
+ }
+
+ Valid_ = true;
+
+ if (RowIndex_) {
+ --*RowIndex_;
+ }
+
+ RowTaken_ = true;
+}
+
+ui32 TLenvalTableReader::GetTableIndex() const
+{
+ CheckValidity();
+ return TableIndex_;
+}
+
+ui32 TLenvalTableReader::GetRangeIndex() const
+{
+ CheckValidity();
+ return RangeIndex_.GetOrElse(0);
+}
+
+ui64 TLenvalTableReader::GetRowIndex() const
+{
+ CheckValidity();
+ return RowIndex_.GetOrElse(0UL);
+}
+
+TMaybe<size_t> TLenvalTableReader::GetReadByteCount() const
+{
+ return Input_.GetReadByteCount();
+}
+
+bool TLenvalTableReader::IsEndOfStream() const
+{
+ return IsEndOfStream_;
+}
+
+bool TLenvalTableReader::IsRawReaderExhausted() const
+{
+ return Finished_;
+}
+
+bool TLenvalTableReader::PrepareRetry()
+{
+ if (Input_.Retry(RangeIndex_, RowIndex_)) {
+ RowIndex_.Clear();
+ RangeIndex_.Clear();
+ return true;
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/lenval_table_reader.h b/yt/cpp/mapreduce/io/lenval_table_reader.h
new file mode 100644
index 0000000000..990fe0b756
--- /dev/null
+++ b/yt/cpp/mapreduce/io/lenval_table_reader.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "counting_raw_reader.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLenvalTableReader
+{
+public:
+ explicit TLenvalTableReader(::TIntrusivePtr<TRawTableReader> input);
+ virtual ~TLenvalTableReader();
+
+protected:
+ bool IsValid() const;
+ void Next();
+ ui32 GetTableIndex() const;
+ ui32 GetRangeIndex() const;
+ ui64 GetRowIndex() const;
+ void NextKey();
+ TMaybe<size_t> GetReadByteCount() const;
+ bool IsEndOfStream() const;
+ bool IsRawReaderExhausted() const;
+
+ void CheckValidity() const;
+
+ bool Retry();
+
+ template <class T>
+ bool ReadInteger(T* result, bool acceptEndOfStream = false)
+ {
+ size_t count = Input_.Load(result, sizeof(T));
+ if (acceptEndOfStream && count == 0) {
+ Finished_ = true;
+ Valid_ = false;
+ return false;
+ }
+ Y_ENSURE(count == sizeof(T), "Premature end of stream");
+ return true;
+ }
+
+ virtual void SkipRow() = 0;
+
+protected:
+ NDetail::TCountingRawTableReader Input_;
+
+ bool Valid_ = true;
+ bool Finished_ = false;
+ ui32 TableIndex_ = 0;
+ TMaybe<ui64> RowIndex_;
+ TMaybe<ui32> RangeIndex_;
+ TMaybe<ui64> TabletIndex_;
+ bool IsEndOfStream_ = false;
+ bool AtStart_ = true;
+ bool RowTaken_ = true;
+ ui32 Length_ = 0;
+
+private:
+ bool PrepareRetry();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/node_table_reader.cpp b/yt/cpp/mapreduce/io/node_table_reader.cpp
new file mode 100644
index 0000000000..d39e1398a5
--- /dev/null
+++ b/yt/cpp/mapreduce/io/node_table_reader.cpp
@@ -0,0 +1,375 @@
+#include "node_table_reader.h"
+
+#include <yt/cpp/mapreduce/common/node_builder.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/parser.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRowBuilder
+ : public ::NYson::TYsonConsumerBase
+{
+public:
+ explicit TRowBuilder(TMaybe<TRowElement>* resultRow);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnBeginList() override;
+ void OnEntity() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+ void Finalize();
+
+private:
+ THolder<TNodeBuilder> Builder_;
+ TRowElement Row_;
+ int Depth_ = 0;
+ bool Started_ = false;
+ TMaybe<TRowElement>* ResultRow_;
+
+ void SaveResultRow();
+};
+
+TRowBuilder::TRowBuilder(TMaybe<TRowElement>* resultRow)
+ : ResultRow_(resultRow)
+{ }
+
+void TRowBuilder::OnStringScalar(TStringBuf value)
+{
+ Row_.Size += sizeof(TNode) + sizeof(TString) + value.size();
+ Builder_->OnStringScalar(value);
+}
+
+void TRowBuilder::OnInt64Scalar(i64 value)
+{
+ Row_.Size += sizeof(TNode);
+ Builder_->OnInt64Scalar(value);
+}
+
+void TRowBuilder::OnUint64Scalar(ui64 value)
+{
+ Row_.Size += sizeof(TNode);
+ Builder_->OnUint64Scalar(value);
+}
+
+void TRowBuilder::OnDoubleScalar(double value)
+{
+ Row_.Size += sizeof(TNode);
+ Builder_->OnDoubleScalar(value);
+}
+
+void TRowBuilder::OnBooleanScalar(bool value)
+{
+ Row_.Size += sizeof(TNode);
+ Builder_->OnBooleanScalar(value);
+}
+
+void TRowBuilder::OnBeginList()
+{
+ ++Depth_;
+ Builder_->OnBeginList();
+}
+
+void TRowBuilder::OnEntity()
+{
+ Row_.Size += sizeof(TNode);
+ Builder_->OnEntity();
+}
+
+void TRowBuilder::OnListItem()
+{
+ if (Depth_ == 0) {
+ SaveResultRow();
+ } else {
+ Builder_->OnListItem();
+ }
+}
+
+void TRowBuilder::OnEndList()
+{
+ --Depth_;
+ Builder_->OnEndList();
+}
+
+void TRowBuilder::OnBeginMap()
+{
+ ++Depth_;
+ Builder_->OnBeginMap();
+}
+
+void TRowBuilder::OnKeyedItem(TStringBuf key)
+{
+ Row_.Size += sizeof(TString) + key.size();
+ Builder_->OnKeyedItem(key);
+}
+
+void TRowBuilder::OnEndMap()
+{
+ --Depth_;
+ Builder_->OnEndMap();
+}
+
+void TRowBuilder::OnBeginAttributes()
+{
+ ++Depth_;
+ Builder_->OnBeginAttributes();
+}
+
+void TRowBuilder::OnEndAttributes()
+{
+ --Depth_;
+ Builder_->OnEndAttributes();
+}
+
+void TRowBuilder::SaveResultRow()
+{
+ if (!Started_) {
+ Started_ = true;
+ } else {
+ *ResultRow_ = std::move(Row_);
+ }
+ Row_.Reset();
+ Builder_.Reset(new TNodeBuilder(&Row_.Node));
+}
+
+void TRowBuilder::Finalize()
+{
+ if (Started_) {
+ *ResultRow_ = std::move(Row_);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNodeTableReader::TNodeTableReader(::TIntrusivePtr<TRawTableReader> input)
+ : Input_(std::move(input))
+{
+ PrepareParsing();
+ Next();
+}
+
+TNodeTableReader::~TNodeTableReader()
+{
+}
+
+void TNodeTableReader::ParseListFragmentItem() {
+ if (!Parser_->Parse()) {
+ Builder_->Finalize();
+ IsLast_ = true;
+ }
+}
+
+const TNode& TNodeTableReader::GetRow() const
+{
+ CheckValidity();
+ if (!Row_) {
+ ythrow yexception() << "Row is moved";
+ }
+ return Row_->Node;
+}
+
+void TNodeTableReader::MoveRow(TNode* result)
+{
+ CheckValidity();
+ if (!Row_) {
+ ythrow yexception() << "Row is moved";
+ }
+ *result = std::move(Row_->Node);
+ Row_.Clear();
+}
+
+bool TNodeTableReader::IsValid() const
+{
+ return Valid_;
+}
+
+void TNodeTableReader::Next()
+{
+ try {
+ NextImpl();
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("TNodeTableReader::Next failed: %v", ex.what());
+ throw;
+ }
+}
+
+void TNodeTableReader::NextImpl()
+{
+ CheckValidity();
+
+ if (RowIndex_) {
+ ++*RowIndex_;
+ }
+
+ // At the begin of stream parser doesn't return a finished row.
+ ParseFirstListFragmentItem();
+
+ while (true) {
+ if (IsLast_) {
+ Finished_ = true;
+ Valid_ = false;
+ break;
+ }
+
+ try {
+ ParseListFragmentItem();
+ } catch (std::exception& ex) {
+ NeedParseFirst_ = true;
+ OnStreamError(std::current_exception(), ex.what());
+ ParseFirstListFragmentItem();
+ continue;
+ }
+
+ Row_ = std::move(*NextRow_);
+ if (!Row_) {
+ throw yexception() << "No row in NextRow_";
+ }
+
+ // We successfully parsed one more row from the stream,
+ // so reset retry count to their initial value.
+ Input_.ResetRetries();
+
+ if (!Row_->Node.IsNull()) {
+ AtStart_ = false;
+ break;
+ }
+
+ for (auto& entry : Row_->Node.GetAttributes().AsMap()) {
+ if (entry.first == "key_switch") {
+ if (!AtStart_) {
+ Valid_ = false;
+ }
+ } else if (entry.first == "table_index") {
+ TableIndex_ = static_cast<ui32>(entry.second.AsInt64());
+ } else if (entry.first == "row_index") {
+ RowIndex_ = static_cast<ui64>(entry.second.AsInt64());
+ } else if (entry.first == "range_index") {
+ RangeIndex_ = static_cast<ui32>(entry.second.AsInt64());
+ } else if (entry.first == "tablet_index") {
+ TabletIndex_ = entry.second.AsInt64();
+ } else if (entry.first == "end_of_stream") {
+ IsEndOfStream_ = true;
+ }
+ }
+
+ if (!Valid_) {
+ break;
+ }
+ }
+}
+
+void TNodeTableReader::ParseFirstListFragmentItem()
+{
+ while (NeedParseFirst_) {
+ try {
+ ParseListFragmentItem();
+ NeedParseFirst_ = false;
+ break;
+ } catch (std::exception& ex) {
+ OnStreamError(std::current_exception(), ex.what());
+ }
+ }
+}
+
+ui32 TNodeTableReader::GetTableIndex() const
+{
+ CheckValidity();
+ return TableIndex_;
+}
+
+ui32 TNodeTableReader::GetRangeIndex() const
+{
+ CheckValidity();
+ return RangeIndex_.GetOrElse(0);
+}
+
+ui64 TNodeTableReader::GetRowIndex() const
+{
+ CheckValidity();
+ return RowIndex_.GetOrElse(0UL);
+}
+
+i64 TNodeTableReader::GetTabletIndex() const
+{
+ CheckValidity();
+ return TabletIndex_.GetOrElse(0L);
+}
+
+void TNodeTableReader::NextKey()
+{
+ while (Valid_) {
+ Next();
+ }
+
+ if (Finished_) {
+ return;
+ }
+
+ Valid_ = true;
+
+ if (RowIndex_) {
+ --*RowIndex_;
+ }
+}
+
+TMaybe<size_t> TNodeTableReader::GetReadByteCount() const
+{
+ return Input_.GetReadByteCount();
+}
+
+bool TNodeTableReader::IsEndOfStream() const
+{
+ return IsEndOfStream_;
+}
+
+bool TNodeTableReader::IsRawReaderExhausted() const
+{
+ return Finished_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TNodeTableReader::PrepareParsing()
+{
+ NextRow_.Clear();
+ Builder_.Reset(new TRowBuilder(&NextRow_));
+ Parser_.Reset(new ::NYson::TYsonListParser(Builder_.Get(), &Input_));
+}
+
+void TNodeTableReader::OnStreamError(std::exception_ptr exception, TString error)
+{
+ YT_LOG_ERROR("Read error: %v", error);
+ Exception_ = exception;
+ if (Input_.Retry(RangeIndex_, RowIndex_)) {
+ RowIndex_.Clear();
+ RangeIndex_.Clear();
+ PrepareParsing();
+ } else {
+ std::rethrow_exception(Exception_);
+ }
+}
+
+void TNodeTableReader::CheckValidity() const
+{
+ if (!Valid_) {
+ ythrow yexception() << "Iterator is not valid";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/node_table_reader.h b/yt/cpp/mapreduce/io/node_table_reader.h
new file mode 100644
index 0000000000..4fe839eeb6
--- /dev/null
+++ b/yt/cpp/mapreduce/io/node_table_reader.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include "counting_raw_reader.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <library/cpp/yson/public.h>
+
+#include <util/stream/input.h>
+#include <util/generic/buffer.h>
+#include <util/system/event.h>
+#include <util/system/thread.h>
+
+#include <atomic>
+
+namespace NYT {
+
+class TRawTableReader;
+class TRowBuilder;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRowElement
+{
+ TNode Node;
+ size_t Size = 0;
+
+ void Reset()
+ {
+ Node = TNode();
+ Size = 0;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNodeTableReader
+ : public INodeReaderImpl
+{
+public:
+ explicit TNodeTableReader(::TIntrusivePtr<TRawTableReader> input);
+ ~TNodeTableReader() override;
+
+ const TNode& GetRow() const override;
+ void MoveRow(TNode* result) override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ i64 GetTabletIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsEndOfStream() const override;
+ bool IsRawReaderExhausted() const override;
+
+private:
+ void NextImpl();
+ void OnStreamError(std::exception_ptr exception, TString error);
+ void CheckValidity() const;
+ void PrepareParsing();
+ void ParseListFragmentItem();
+ void ParseFirstListFragmentItem();
+
+private:
+ NDetail::TCountingRawTableReader Input_;
+
+ bool Valid_ = true;
+ bool Finished_ = false;
+ ui32 TableIndex_ = 0;
+ TMaybe<ui64> RowIndex_;
+ TMaybe<ui32> RangeIndex_;
+ TMaybe<i64> TabletIndex_;
+ bool IsEndOfStream_ = false;
+ bool AtStart_ = true;
+
+ TMaybe<TRowElement> Row_;
+ TMaybe<TRowElement> NextRow_;
+
+ THolder<TRowBuilder> Builder_;
+ THolder<::NYson::TYsonListParser> Parser_;
+
+ std::exception_ptr Exception_;
+ bool NeedParseFirst_ = true;
+ bool IsLast_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/node_table_writer.cpp b/yt/cpp/mapreduce/io/node_table_writer.cpp
new file mode 100644
index 0000000000..dcb5a0f5b5
--- /dev/null
+++ b/yt/cpp/mapreduce/io/node_table_writer.cpp
@@ -0,0 +1,72 @@
+#include "node_table_writer.h"
+
+#include <yt/cpp/mapreduce/common/node_visitor.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/writer.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNodeTableWriter::TNodeTableWriter(THolder<IProxyOutput> output, NYson::EYsonFormat format)
+ : Output_(std::move(output))
+{
+ for (size_t i = 0; i < Output_->GetStreamCount(); ++i) {
+ Writers_.push_back(
+ MakeHolder<::NYson::TYsonWriter>(Output_->GetStream(i), format, NYT::NYson::EYsonType::ListFragment));
+ }
+}
+
+TNodeTableWriter::~TNodeTableWriter()
+{ }
+
+size_t TNodeTableWriter::GetTableCount() const
+{
+ return Output_->GetStreamCount();
+}
+
+void TNodeTableWriter::FinishTable(size_t tableIndex) {
+ Output_->GetStream(tableIndex)->Finish();
+}
+
+void TNodeTableWriter::AddRow(const TNode& row, size_t tableIndex)
+{
+ if (row.HasAttributes()) {
+ ythrow TIOException() << "Row cannot have attributes";
+ }
+
+ static const TNode emptyMap = TNode::CreateMap();
+ const TNode* outRow = &emptyMap;
+ if (row.GetType() != TNode::Undefined) {
+ if (!row.IsMap()) {
+ ythrow TIOException() << "Row should be a map node";
+ } else {
+ outRow = &row;
+ }
+ }
+
+ auto* writer = Writers_[tableIndex].Get();
+ writer->OnListItem();
+
+ TNodeVisitor visitor(writer);
+ visitor.Visit(*outRow);
+
+ Output_->OnRowFinished(tableIndex);
+}
+
+void TNodeTableWriter::AddRow(TNode&& row, size_t tableIndex) {
+ AddRow(row, tableIndex);
+}
+
+void TNodeTableWriter::Abort()
+{
+ Output_->Abort();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/node_table_writer.h b/yt/cpp/mapreduce/io/node_table_writer.h
new file mode 100644
index 0000000000..4bf8cb2fe7
--- /dev/null
+++ b/yt/cpp/mapreduce/io/node_table_writer.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <library/cpp/yson/public.h>
+
+namespace NYT {
+
+class IProxyOutput;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNodeTableWriter
+ : public INodeWriterImpl
+{
+public:
+ explicit TNodeTableWriter(THolder<IProxyOutput> output, ::NYson::EYsonFormat format = ::NYson::EYsonFormat::Binary);
+ ~TNodeTableWriter() override;
+
+ void AddRow(const TNode& row, size_t tableIndex) override;
+ void AddRow(TNode&& row, size_t tableIndex) override;
+
+ size_t GetTableCount() const override;
+ void FinishTable(size_t) override;
+ void Abort() override;
+
+private:
+ THolder<IProxyOutput> Output_;
+ TVector<THolder<::NYson::TYsonWriter>> Writers_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_helpers.cpp b/yt/cpp/mapreduce/io/proto_helpers.cpp
new file mode 100644
index 0000000000..2ffbfd8d89
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_helpers.cpp
@@ -0,0 +1,101 @@
+#include "proto_helpers.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+#include <yt/cpp/mapreduce/interface/fluent.h>
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/messagext.h>
+#include <google/protobuf/io/coded_stream.h>
+
+#include <util/stream/str.h>
+#include <util/stream/file.h>
+#include <util/folder/path.h>
+
+namespace NYT {
+
+using ::google::protobuf::Message;
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::DescriptorPool;
+
+using ::google::protobuf::io::CodedInputStream;
+using ::google::protobuf::io::TCopyingInputStreamAdaptor;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TVector<const Descriptor*> GetJobDescriptors(const TString& fileName)
+{
+ TVector<const Descriptor*> descriptors;
+ if (!TFsPath(fileName).Exists()) {
+ ythrow TIOException() <<
+ "Cannot load '" << fileName << "' file";
+ }
+
+ TIFStream input(fileName);
+ TString line;
+ while (input.ReadLine(line)) {
+ const auto* pool = DescriptorPool::generated_pool();
+ const auto* descriptor = pool->FindMessageTypeByName(line);
+ descriptors.push_back(descriptor);
+ }
+
+ return descriptors;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVector<const Descriptor*> GetJobInputDescriptors()
+{
+ return GetJobDescriptors("proto_input");
+}
+
+TVector<const Descriptor*> GetJobOutputDescriptors()
+{
+ return GetJobDescriptors("proto_output");
+}
+
+void ValidateProtoDescriptor(
+ const Message& row,
+ size_t tableIndex,
+ const TVector<const Descriptor*>& descriptors,
+ bool isRead)
+{
+ const char* direction = isRead ? "input" : "output";
+
+ if (tableIndex >= descriptors.size()) {
+ ythrow TIOException() <<
+ "Table index " << tableIndex <<
+ " is out of range [0, " << descriptors.size() <<
+ ") in " << direction;
+ }
+
+ if (row.GetDescriptor() != descriptors[tableIndex]) {
+ ythrow TIOException() <<
+ "Invalid row of type " << row.GetDescriptor()->full_name() <<
+ " at index " << tableIndex <<
+ ", row of type " << descriptors[tableIndex]->full_name() <<
+ " expected in " << direction;
+ }
+}
+
+void ParseFromArcadiaStream(IInputStream* stream, Message& row, ui32 length)
+{
+ TLengthLimitedInput input(stream, length);
+ TCopyingInputStreamAdaptor adaptor(&input);
+ CodedInputStream codedStream(&adaptor);
+ codedStream.SetTotalBytesLimit(length + 1);
+ bool parsedOk = row.ParseFromCodedStream(&codedStream);
+ Y_ENSURE(parsedOk, "Failed to parse protobuf message");
+
+ Y_ENSURE(input.Left() == 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_helpers.h b/yt/cpp/mapreduce/io/proto_helpers.h
new file mode 100644
index 0000000000..9d1ec0027c
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_helpers.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/node.h>
+
+namespace google {
+namespace protobuf {
+
+class Message;
+class Descriptor;
+
+}
+}
+
+class IInputStream;
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVector<const ::google::protobuf::Descriptor*> GetJobInputDescriptors();
+TVector<const ::google::protobuf::Descriptor*> GetJobOutputDescriptors();
+
+void ValidateProtoDescriptor(
+ const ::google::protobuf::Message& row,
+ size_t tableIndex,
+ const TVector<const ::google::protobuf::Descriptor*>& descriptors,
+ bool isRead);
+
+void ParseFromArcadiaStream(
+ IInputStream* stream,
+ ::google::protobuf::Message& row,
+ ui32 size);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_table_reader.cpp b/yt/cpp/mapreduce/io/proto_table_reader.cpp
new file mode 100644
index 0000000000..28a4bc8719
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_table_reader.cpp
@@ -0,0 +1,305 @@
+#include "proto_table_reader.h"
+
+#include "node_table_reader.h"
+
+#include "proto_helpers.h"
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <util/string/escape.h>
+#include <util/string/printf.h>
+
+namespace NYT {
+
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::FieldDescriptor;
+using ::google::protobuf::EnumValueDescriptor;
+
+const TString& GetFieldColumnName(const FieldDescriptor* fieldDesc) {
+ const auto& columnName = fieldDesc->options().GetExtension(column_name);
+ if (!columnName.empty()) {
+ return columnName;
+ }
+ const auto& keyColumnName = fieldDesc->options().GetExtension(key_column_name);
+ if (!keyColumnName.empty()) {
+ return keyColumnName;
+ }
+ return fieldDesc->name();
+}
+
+void ReadMessageFromNode(const TNode& node, Message* row)
+{
+ auto* descriptor = row->GetDescriptor();
+ auto* reflection = row->GetReflection();
+
+ int count = descriptor->field_count();
+ for (int i = 0; i < count; ++i) {
+ auto* fieldDesc = descriptor->field(i);
+
+ const auto& columnName = GetFieldColumnName(fieldDesc);
+
+ const auto& nodeMap = node.AsMap();
+ auto it = nodeMap.find(columnName);
+ if (it == nodeMap.end()) {
+ continue; // no such column
+ }
+ auto actualType = it->second.GetType();
+ if (actualType == TNode::Null) {
+ continue; // null field
+ }
+
+ auto checkType = [&columnName] (TNode::EType expected, TNode::EType actual) {
+ if (expected != actual) {
+ ythrow TNode::TTypeError() << "expected node type " << expected
+ << ", actual " << actual << " for node " << columnName.data();
+ }
+ };
+
+ switch (fieldDesc->type()) {
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_BYTES:
+ checkType(TNode::String, actualType);
+ reflection->SetString(row, fieldDesc, it->second.AsString());
+ break;
+ case FieldDescriptor::TYPE_INT64:
+ case FieldDescriptor::TYPE_SINT64:
+ case FieldDescriptor::TYPE_SFIXED64:
+ checkType(TNode::Int64, actualType);
+ reflection->SetInt64(row, fieldDesc, it->second.AsInt64());
+ break;
+ case FieldDescriptor::TYPE_INT32:
+ case FieldDescriptor::TYPE_SINT32:
+ case FieldDescriptor::TYPE_SFIXED32:
+ checkType(TNode::Int64, actualType);
+ reflection->SetInt32(row, fieldDesc, it->second.AsInt64());
+ break;
+ case FieldDescriptor::TYPE_UINT64:
+ case FieldDescriptor::TYPE_FIXED64:
+ checkType(TNode::Uint64, actualType);
+ reflection->SetUInt64(row, fieldDesc, it->second.AsUint64());
+ break;
+ case FieldDescriptor::TYPE_UINT32:
+ case FieldDescriptor::TYPE_FIXED32:
+ checkType(TNode::Uint64, actualType);
+ reflection->SetUInt32(row, fieldDesc, it->second.AsUint64());
+ break;
+ case FieldDescriptor::TYPE_DOUBLE:
+ checkType(TNode::Double, actualType);
+ reflection->SetDouble(row, fieldDesc, it->second.AsDouble());
+ break;
+ case FieldDescriptor::TYPE_FLOAT:
+ checkType(TNode::Double, actualType);
+ reflection->SetFloat(row, fieldDesc, it->second.AsDouble());
+ break;
+ case FieldDescriptor::TYPE_BOOL:
+ checkType(TNode::Bool, actualType);
+ reflection->SetBool(row, fieldDesc, it->second.AsBool());
+ break;
+ case FieldDescriptor::TYPE_ENUM: {
+ TNode::EType columnType = TNode::String;
+ for (const auto& flag : fieldDesc->options().GetRepeatedExtension(flags)) {
+ if (flag == EWrapperFieldFlag::ENUM_INT) {
+ columnType = TNode::Int64;
+ break;
+ }
+ }
+ checkType(columnType, actualType);
+
+ const EnumValueDescriptor* valueDesc = nullptr;
+ TString stringValue;
+ if (columnType == TNode::String) {
+ const auto& value = it->second.AsString();
+ valueDesc = fieldDesc->enum_type()->FindValueByName(value);
+ stringValue = value;
+ } else if (columnType == TNode::Int64) {
+ const auto& value = it->second.AsInt64();
+ valueDesc = fieldDesc->enum_type()->FindValueByNumber(value);
+ stringValue = ToString(value);
+ } else {
+ Y_FAIL();
+ }
+
+ if (valueDesc == nullptr) {
+ ythrow yexception() << "Failed to parse value '" << EscapeC(stringValue) << "' as " << fieldDesc->enum_type()->full_name();
+ }
+
+ reflection->SetEnum(row, fieldDesc, valueDesc);
+
+ break;
+ }
+ case FieldDescriptor::TYPE_MESSAGE: {
+ checkType(TNode::String, actualType);
+ Message* message = reflection->MutableMessage(row, fieldDesc);
+ if (!message->ParseFromArray(it->second.AsString().data(), it->second.AsString().size())) {
+ ythrow yexception() << "Failed to parse protobuf message";
+ }
+ break;
+ }
+ default:
+ ythrow yexception() << "Incorrect protobuf type";
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtoTableReader::TProtoTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ TVector<const Descriptor*>&& descriptors)
+ : NodeReader_(new TNodeTableReader(std::move(input)))
+ , Descriptors_(std::move(descriptors))
+{ }
+
+TProtoTableReader::~TProtoTableReader()
+{ }
+
+void TProtoTableReader::ReadRow(Message* row)
+{
+ const auto& node = NodeReader_->GetRow();
+ ReadMessageFromNode(node, row);
+}
+
+bool TProtoTableReader::IsValid() const
+{
+ return NodeReader_->IsValid();
+}
+
+void TProtoTableReader::Next()
+{
+ NodeReader_->Next();
+}
+
+ui32 TProtoTableReader::GetTableIndex() const
+{
+ return NodeReader_->GetTableIndex();
+}
+
+ui32 TProtoTableReader::GetRangeIndex() const
+{
+ return NodeReader_->GetRangeIndex();
+}
+
+ui64 TProtoTableReader::GetRowIndex() const
+{
+ return NodeReader_->GetRowIndex();
+}
+
+void TProtoTableReader::NextKey()
+{
+ NodeReader_->NextKey();
+}
+
+TMaybe<size_t> TProtoTableReader::GetReadByteCount() const
+{
+ return NodeReader_->GetReadByteCount();
+}
+
+bool TProtoTableReader::IsEndOfStream() const
+{
+ return NodeReader_->IsEndOfStream();
+}
+
+bool TProtoTableReader::IsRawReaderExhausted() const
+{
+ return NodeReader_->IsRawReaderExhausted();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLenvalProtoTableReader::TLenvalProtoTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ TVector<const Descriptor*>&& descriptors)
+ : TLenvalTableReader(std::move(input))
+ , Descriptors_(std::move(descriptors))
+{ }
+
+TLenvalProtoTableReader::~TLenvalProtoTableReader()
+{ }
+
+void TLenvalProtoTableReader::ReadRow(Message* row)
+{
+ ValidateProtoDescriptor(*row, GetTableIndex(), Descriptors_, true);
+
+ while (true) {
+ try {
+ ParseFromArcadiaStream(&Input_, *row, Length_);
+ RowTaken_ = true;
+
+ // We successfully parsed one more row from the stream,
+ // so reset retry count to their initial value.
+ Input_.ResetRetries();
+
+ break;
+ } catch (const std::exception& ) {
+ if (!TLenvalTableReader::Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+bool TLenvalProtoTableReader::IsValid() const
+{
+ return TLenvalTableReader::IsValid();
+}
+
+void TLenvalProtoTableReader::Next()
+{
+ TLenvalTableReader::Next();
+}
+
+ui32 TLenvalProtoTableReader::GetTableIndex() const
+{
+ return TLenvalTableReader::GetTableIndex();
+}
+
+ui32 TLenvalProtoTableReader::GetRangeIndex() const
+{
+ return TLenvalTableReader::GetRangeIndex();
+}
+
+ui64 TLenvalProtoTableReader::GetRowIndex() const
+{
+ return TLenvalTableReader::GetRowIndex();
+}
+
+void TLenvalProtoTableReader::NextKey()
+{
+ TLenvalTableReader::NextKey();
+}
+
+TMaybe<size_t> TLenvalProtoTableReader::GetReadByteCount() const
+{
+ return TLenvalTableReader::GetReadByteCount();
+}
+
+bool TLenvalProtoTableReader::IsEndOfStream() const
+{
+ return TLenvalTableReader::IsEndOfStream();
+}
+
+bool TLenvalProtoTableReader::IsRawReaderExhausted() const
+{
+ return TLenvalTableReader::IsRawReaderExhausted();
+}
+
+void TLenvalProtoTableReader::SkipRow()
+{
+ while (true) {
+ try {
+ size_t skipped = Input_.Skip(Length_);
+ if (skipped != Length_) {
+ ythrow yexception() << "Premature end of stream";
+ }
+ break;
+ } catch (const std::exception& ) {
+ if (!TLenvalTableReader::Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_table_reader.h b/yt/cpp/mapreduce/io/proto_table_reader.h
new file mode 100644
index 0000000000..05a528b9c6
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_table_reader.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "lenval_table_reader.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+class TRawTableReader;
+class TNodeTableReader;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtoTableReader
+ : public IProtoReaderImpl
+{
+public:
+ explicit TProtoTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ TVector<const ::google::protobuf::Descriptor*>&& descriptors);
+ ~TProtoTableReader() override;
+
+ void ReadRow(Message* row) override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsEndOfStream() const override;
+ bool IsRawReaderExhausted() const override;
+
+private:
+ THolder<TNodeTableReader> NodeReader_;
+ TVector<const ::google::protobuf::Descriptor*> Descriptors_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLenvalProtoTableReader
+ : public IProtoReaderImpl
+ , public TLenvalTableReader
+{
+public:
+ explicit TLenvalProtoTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ TVector<const ::google::protobuf::Descriptor*>&& descriptors);
+ ~TLenvalProtoTableReader() override;
+
+ void ReadRow(Message* row) override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsEndOfStream() const override;
+ bool IsRawReaderExhausted() const override;
+
+protected:
+ void SkipRow() override;
+
+private:
+ TVector<const ::google::protobuf::Descriptor*> Descriptors_;
+};
+
+// Sometime useful outside mapreduce/yt
+void ReadMessageFromNode(const TNode& node, Message* row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_table_writer.cpp b/yt/cpp/mapreduce/io/proto_table_writer.cpp
new file mode 100644
index 0000000000..1ce7811625
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_table_writer.cpp
@@ -0,0 +1,184 @@
+#include "proto_table_writer.h"
+
+#include "node_table_writer.h"
+#include "proto_helpers.h"
+
+#include <yt/cpp/mapreduce/common/node_builder.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <google/protobuf/unknown_field_set.h>
+
+namespace NYT {
+
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::FieldDescriptor;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNode MakeNodeFromMessage(const Message& row)
+{
+ TNode node;
+ TNodeBuilder builder(&node);
+ builder.OnBeginMap();
+
+ auto* descriptor = row.GetDescriptor();
+ auto* reflection = row.GetReflection();
+
+ int count = descriptor->field_count();
+ for (int i = 0; i < count; ++i) {
+ auto* fieldDesc = descriptor->field(i);
+ if (fieldDesc->is_repeated()) {
+ Y_ENSURE(reflection->FieldSize(row, fieldDesc) == 0, "Storing repeated protobuf fields is not supported yet");
+ continue;
+ } else if (!reflection->HasField(row, fieldDesc)) {
+ continue;
+ }
+
+ TString columnName = fieldDesc->options().GetExtension(column_name);
+ if (columnName.empty()) {
+ const auto& keyColumnName = fieldDesc->options().GetExtension(key_column_name);
+ columnName = keyColumnName.empty() ? fieldDesc->name() : keyColumnName;
+ }
+
+ builder.OnKeyedItem(columnName);
+
+ switch (fieldDesc->type()) {
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_BYTES:
+ builder.OnStringScalar(reflection->GetString(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_INT64:
+ case FieldDescriptor::TYPE_SINT64:
+ case FieldDescriptor::TYPE_SFIXED64:
+ builder.OnInt64Scalar(reflection->GetInt64(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_INT32:
+ case FieldDescriptor::TYPE_SINT32:
+ case FieldDescriptor::TYPE_SFIXED32:
+ builder.OnInt64Scalar(reflection->GetInt32(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_UINT64:
+ case FieldDescriptor::TYPE_FIXED64:
+ builder.OnUint64Scalar(reflection->GetUInt64(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_UINT32:
+ case FieldDescriptor::TYPE_FIXED32:
+ builder.OnUint64Scalar(reflection->GetUInt32(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_DOUBLE:
+ builder.OnDoubleScalar(reflection->GetDouble(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_FLOAT:
+ builder.OnDoubleScalar(reflection->GetFloat(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_BOOL:
+ builder.OnBooleanScalar(reflection->GetBool(row, fieldDesc));
+ break;
+ case FieldDescriptor::TYPE_ENUM:
+ builder.OnStringScalar(reflection->GetEnum(row, fieldDesc)->name());
+ break;
+ case FieldDescriptor::TYPE_MESSAGE:
+ builder.OnStringScalar(reflection->GetMessage(row, fieldDesc).SerializeAsString());
+ break;
+ default:
+ ythrow yexception() << "Invalid field type for column: " << columnName;
+ break;
+ }
+ }
+
+ builder.OnEndMap();
+ return node;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtoTableWriter::TProtoTableWriter(
+ THolder<IProxyOutput> output,
+ TVector<const Descriptor*>&& descriptors)
+ : NodeWriter_(new TNodeTableWriter(std::move(output)))
+ , Descriptors_(std::move(descriptors))
+{ }
+
+TProtoTableWriter::~TProtoTableWriter()
+{ }
+
+size_t TProtoTableWriter::GetTableCount() const
+{
+ return NodeWriter_->GetTableCount();
+}
+
+void TProtoTableWriter::FinishTable(size_t tableIndex)
+{
+ NodeWriter_->FinishTable(tableIndex);
+}
+
+void TProtoTableWriter::AddRow(const Message& row, size_t tableIndex)
+{
+ NodeWriter_->AddRow(MakeNodeFromMessage(row), tableIndex);
+}
+
+void TProtoTableWriter::AddRow(Message&& row, size_t tableIndex)
+{
+ TProtoTableWriter::AddRow(row, tableIndex);
+}
+
+
+void TProtoTableWriter::Abort()
+{
+ NodeWriter_->Abort();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLenvalProtoTableWriter::TLenvalProtoTableWriter(
+ THolder<IProxyOutput> output,
+ TVector<const Descriptor*>&& descriptors)
+ : Output_(std::move(output))
+ , Descriptors_(std::move(descriptors))
+{ }
+
+TLenvalProtoTableWriter::~TLenvalProtoTableWriter()
+{ }
+
+size_t TLenvalProtoTableWriter::GetTableCount() const
+{
+ return Output_->GetStreamCount();
+}
+
+void TLenvalProtoTableWriter::FinishTable(size_t tableIndex)
+{
+ Output_->GetStream(tableIndex)->Finish();
+}
+
+void TLenvalProtoTableWriter::AddRow(const Message& row, size_t tableIndex)
+{
+ ValidateProtoDescriptor(row, tableIndex, Descriptors_, false);
+
+ Y_VERIFY(row.GetReflection()->GetUnknownFields(row).empty(),
+ "Message has unknown fields. This probably means bug in client code.\n"
+ "Message: %s", row.DebugString().data());
+
+ auto* stream = Output_->GetStream(tableIndex);
+ i32 size = row.ByteSize();
+ stream->Write(&size, sizeof(size));
+ bool serializedOk = row.SerializeToArcadiaStream(stream);
+ Y_ENSURE(serializedOk, "Failed to serialize protobuf message");
+ Output_->OnRowFinished(tableIndex);
+}
+
+void TLenvalProtoTableWriter::AddRow(Message&& row, size_t tableIndex)
+{
+ TLenvalProtoTableWriter::AddRow(row, tableIndex);
+}
+
+void TLenvalProtoTableWriter::Abort()
+{
+ Output_->Abort();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/proto_table_writer.h b/yt/cpp/mapreduce/io/proto_table_writer.h
new file mode 100644
index 0000000000..a6df69e6ae
--- /dev/null
+++ b/yt/cpp/mapreduce/io/proto_table_writer.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+class IProxyOutput;
+class TNodeTableWriter;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtoTableWriter
+ : public IProtoWriterImpl
+{
+public:
+ TProtoTableWriter(
+ THolder<IProxyOutput> output,
+ TVector<const ::google::protobuf::Descriptor*>&& descriptors);
+ ~TProtoTableWriter() override;
+
+ void AddRow(const Message& row, size_t tableIndex) override;
+ void AddRow(Message&& row, size_t tableIndex) override;
+
+ size_t GetTableCount() const override;
+ void FinishTable(size_t) override;
+ void Abort() override;
+
+private:
+ THolder<TNodeTableWriter> NodeWriter_;
+ TVector<const ::google::protobuf::Descriptor*> Descriptors_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLenvalProtoTableWriter
+ : public IProtoWriterImpl
+{
+public:
+ TLenvalProtoTableWriter(
+ THolder<IProxyOutput> output,
+ TVector<const ::google::protobuf::Descriptor*>&& descriptors);
+ ~TLenvalProtoTableWriter() override;
+
+ void AddRow(const Message& row, size_t tableIndex) override;
+ void AddRow(Message&& row, size_t tableIndex) override;
+
+ size_t GetTableCount() const override;
+ void FinishTable(size_t) override;
+ void Abort() override;
+
+private:
+ THolder<IProxyOutput> Output_;
+ TVector<const ::google::protobuf::Descriptor*> Descriptors_;
+};
+
+// Sometime useful outside mapreduce/yt
+TNode MakeNodeFromMessage(const ::google::protobuf::Message& row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp b/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
new file mode 100644
index 0000000000..8da3b2da31
--- /dev/null
+++ b/yt/cpp/mapreduce/io/skiff_row_table_reader.cpp
@@ -0,0 +1,232 @@
+#include "skiff_row_table_reader.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/interface/skiff_row.h>
+
+#include <library/cpp/skiff/skiff.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSkiffRowTableReader::TSkiffRowTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ const NSkiff::TSkiffSchemaPtr& schema,
+ TVector<ISkiffRowSkipperPtr>&& skippers,
+ NDetail::TCreateSkiffSchemaOptions&& options)
+ : Input_(std::move(input))
+ , BufferedInput_(&Input_)
+ , Parser_({schema, &BufferedInput_})
+ , Skippers_(std::move(skippers))
+ , Options_(std::move(options))
+{
+ Next();
+}
+
+TSkiffRowTableReader::~TSkiffRowTableReader()
+{ }
+
+bool TSkiffRowTableReader::Retry()
+{
+ if (PrepareRetry()) {
+ RowTaken_ = true;
+ Next();
+ return true;
+ }
+ return false;
+}
+
+bool TSkiffRowTableReader::PrepareRetry()
+{
+ if (Input_.Retry(RangeIndex_, RowIndex_)) {
+ RowIndex_.Clear();
+ RangeIndex_.Clear();
+ BufferedInput_ = TBufferedInput(&Input_);
+ Parser_.emplace(&BufferedInput_);
+ return true;
+ }
+ return false;
+}
+
+void TSkiffRowTableReader::ReadRow(const ISkiffRowParserPtr& parser)
+{
+ while (true) {
+ try {
+ parser->Parse(&Parser_.value());
+ RowTaken_ = true;
+
+ // We successfully parsed one more row from the stream,
+ // so reset retry count to their initial value.
+ Input_.ResetRetries();
+
+ break;
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("Read error during parsing: %v", ex.what());
+
+ if (!Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+bool TSkiffRowTableReader::IsValid() const
+{
+ return Valid_;
+}
+
+void TSkiffRowTableReader::SkipRow()
+{
+ CheckValidity();
+ while (true) {
+ try {
+ Skippers_[TableIndex_]->SkipRow(&Parser_.value());
+
+ break;
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("Read error during skipping row: %v", ex.what());
+
+ if (!Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+void TSkiffRowTableReader::CheckValidity() const {
+ if (!IsValid()) {
+ ythrow yexception() << "Iterator is not valid";
+ }
+}
+
+void TSkiffRowTableReader::Next()
+{
+ if (!RowTaken_) {
+ SkipRow();
+ }
+
+ CheckValidity();
+
+ if (Y_UNLIKELY(Finished_ || !Parser_->HasMoreData())) {
+ Finished_ = true;
+ Valid_ = false;
+ return;
+ }
+
+ if (AfterKeySwitch_) {
+ AfterKeySwitch_ = false;
+ return;
+ }
+
+ if (RowIndex_) {
+ ++*RowIndex_;
+ }
+
+ while (true) {
+ try {
+ auto tag = Parser_->ParseVariant16Tag();
+ if (tag == NSkiff::EndOfSequenceTag<ui16>()) {
+ IsEndOfStream_ = true;
+ break;
+ } else {
+ TableIndex_ = tag;
+ }
+
+ if (TableIndex_ >= Skippers_.size()) {
+ ythrow TIOException() <<
+ "Table index " << TableIndex_ <<
+ " is out of range [0, " << Skippers_.size() <<
+ ") in read";
+ }
+
+ if (Options_.HasKeySwitch_) {
+ auto keySwitch = Parser_->ParseBoolean();
+ if (keySwitch) {
+ AfterKeySwitch_ = true;
+ Valid_ = false;
+ }
+ }
+
+ auto tagRowIndex = Parser_->ParseVariant8Tag();
+ if (tagRowIndex == 1) {
+ RowIndex_ = Parser_->ParseInt64();
+ } else {
+ Y_ENSURE(tagRowIndex == 0, "Tag for row_index was expected to be 0 or 1, got " << tagRowIndex);
+ }
+
+ if (Options_.HasRangeIndex_) {
+ auto tagRangeIndex = Parser_->ParseVariant8Tag();
+ if (tagRangeIndex == 1) {
+ RangeIndex_ = Parser_->ParseInt64();
+ } else {
+ Y_ENSURE(tagRangeIndex == 0, "Tag for range_index was expected to be 0 or 1, got " << tagRangeIndex);
+ }
+ }
+
+ break;
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR("Read error: %v", ex.what());
+
+ if (!PrepareRetry()) {
+ throw;
+ }
+ }
+ }
+
+ RowTaken_ = false;
+}
+
+ui32 TSkiffRowTableReader::GetTableIndex() const
+{
+ CheckValidity();
+ return TableIndex_;
+}
+
+ui32 TSkiffRowTableReader::GetRangeIndex() const
+{
+ CheckValidity();
+ return RangeIndex_.GetOrElse(0);
+}
+
+ui64 TSkiffRowTableReader::GetRowIndex() const
+{
+ CheckValidity();
+ return RowIndex_.GetOrElse(0ULL);
+}
+
+void TSkiffRowTableReader::NextKey() {
+ while (Valid_) {
+ Next();
+ }
+
+ if (Finished_) {
+ return;
+ }
+
+ Valid_ = true;
+
+ if (RowIndex_) {
+ --*RowIndex_;
+ }
+
+ RowTaken_ = true;
+}
+
+TMaybe<size_t> TSkiffRowTableReader::GetReadByteCount() const {
+ return Input_.GetReadByteCount();
+}
+
+bool TSkiffRowTableReader::IsEndOfStream() const {
+ return IsEndOfStream_;
+}
+
+bool TSkiffRowTableReader::IsRawReaderExhausted() const {
+ return Finished_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/skiff_row_table_reader.h b/yt/cpp/mapreduce/io/skiff_row_table_reader.h
new file mode 100644
index 0000000000..368968266c
--- /dev/null
+++ b/yt/cpp/mapreduce/io/skiff_row_table_reader.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "counting_raw_reader.h"
+
+#include <yt/cpp/mapreduce/client/skiff.h>
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/cpp/mapreduce/skiff/unchecked_parser.h>
+
+#include <util/stream/buffered.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffRowTableReader
+ : public ISkiffRowReaderImpl
+{
+public:
+ explicit TSkiffRowTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ const NSkiff::TSkiffSchemaPtr& schema,
+ TVector<ISkiffRowSkipperPtr>&& skippers,
+ NDetail::TCreateSkiffSchemaOptions&& options);
+
+ ~TSkiffRowTableReader() override;
+
+ void ReadRow(const ISkiffRowParserPtr& parser) override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsEndOfStream() const override;
+ bool IsRawReaderExhausted() const override;
+
+private:
+ bool Retry();
+ void SkipRow();
+ void CheckValidity() const;
+ bool PrepareRetry();
+
+private:
+ NDetail::TCountingRawTableReader Input_;
+ TBufferedInput BufferedInput_;
+ std::optional<NSkiff::TCheckedInDebugSkiffParser> Parser_;
+ TVector<ISkiffRowSkipperPtr> Skippers_;
+ NDetail::TCreateSkiffSchemaOptions Options_;
+
+ bool RowTaken_ = true;
+ bool Valid_ = true;
+ bool Finished_ = false;
+ bool AfterKeySwitch_ = false;
+ bool IsEndOfStream_ = false;
+
+ TMaybe<ui64> RowIndex_;
+ TMaybe<ui32> RangeIndex_;
+ ui32 TableIndex_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/skiff_table_reader.cpp b/yt/cpp/mapreduce/io/skiff_table_reader.cpp
new file mode 100644
index 0000000000..51c20609f0
--- /dev/null
+++ b/yt/cpp/mapreduce/io/skiff_table_reader.cpp
@@ -0,0 +1,293 @@
+#include "skiff_table_reader.h"
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <yt/cpp/mapreduce/skiff/wire_type.h>
+#include <yt/cpp/mapreduce/skiff/skiff_schema.h>
+
+#include <util/string/cast.h>
+
+namespace NYT {
+namespace NDetail {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum EColumnType : i8
+{
+ Dense,
+ KeySwitch,
+ RangeIndex,
+ RowIndex
+};
+
+struct TSkiffColumnSchema
+{
+ EColumnType Type;
+ bool Required;
+ NSkiff::EWireType WireType;
+ TString Name;
+
+ TSkiffColumnSchema(EColumnType type, bool required, NSkiff::EWireType wireType, const TString& name)
+ : Type(type)
+ , Required(required)
+ , WireType(wireType)
+ , Name(name)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+struct TSkiffTableReader::TSkiffTableSchema
+{
+ TVector<TSkiffColumnSchema> Columns;
+};
+
+TSkiffTableReader::TSkiffTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ const NSkiff::TSkiffSchemaPtr& schema)
+ : Input_(std::move(input))
+ , BufferedInput_(&Input_)
+ , Parser_(&BufferedInput_)
+ , Schemas_(CreateSkiffTableSchemas(schema))
+{
+ Next();
+}
+
+TSkiffTableReader::~TSkiffTableReader() = default;
+
+const TNode& TSkiffTableReader::GetRow() const
+{
+ EnsureValidity();
+ Y_ENSURE(!Row_.IsUndefined(), "Row is moved");
+ return Row_;
+}
+
+void TSkiffTableReader::MoveRow(TNode* result)
+{
+ EnsureValidity();
+ Y_ENSURE(!Row_.IsUndefined(), "Row is moved");
+ *result = std::move(Row_);
+ Row_ = TNode();
+}
+
+bool TSkiffTableReader::IsValid() const
+{
+ return Valid_;
+}
+
+void TSkiffTableReader::Next()
+{
+ EnsureValidity();
+ if (Y_UNLIKELY(Finished_ || !Parser_->HasMoreData())) {
+ Finished_ = true;
+ Valid_ = false;
+ return;
+ }
+
+ if (AfterKeySwitch_) {
+ AfterKeySwitch_ = false;
+ return;
+ }
+
+ while (true) {
+ try {
+ ReadRow();
+ break;
+ } catch (const std::exception& exception) {
+ YT_LOG_ERROR("Read error: %v", exception.what());
+ if (!Input_.Retry(RangeIndex_, RowIndex_)) {
+ throw;
+ }
+ BufferedInput_ = TBufferedInput(&Input_);
+ Parser_.emplace(NSkiff::TUncheckedSkiffParser(&BufferedInput_));
+ RangeIndex_.Clear();
+ RowIndex_.Clear();
+ }
+ }
+}
+
+ui32 TSkiffTableReader::GetTableIndex() const
+{
+ EnsureValidity();
+ return TableIndex_;
+}
+
+ui32 TSkiffTableReader::GetRangeIndex() const
+{
+ EnsureValidity();
+ return RangeIndex_.GetOrElse(0);
+}
+
+ui64 TSkiffTableReader::GetRowIndex() const
+{
+ EnsureValidity();
+ return RowIndex_.GetOrElse(0ULL);
+}
+
+void TSkiffTableReader::NextKey()
+{
+ while (Valid_) {
+ Next();
+ }
+
+ if (Finished_) {
+ return;
+ }
+
+ Valid_ = true;
+}
+
+TMaybe<size_t> TSkiffTableReader::GetReadByteCount() const
+{
+ return Input_.GetReadByteCount();
+}
+
+bool TSkiffTableReader::IsRawReaderExhausted() const
+{
+ return Finished_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVector<TSkiffTableReader::TSkiffTableSchema> TSkiffTableReader::CreateSkiffTableSchemas(
+ const NSkiff::TSkiffSchemaPtr& schema)
+{
+ using NSkiff::EWireType;
+
+ constexpr auto keySwitchColumnName = "$key_switch";
+ constexpr auto rangeIndexColumnName = "$range_index";
+ constexpr auto rowIndexColumnName = "$row_index";
+
+ static const THashMap<TString, TSkiffColumnSchema> specialColumns = {
+ {keySwitchColumnName, {EColumnType::KeySwitch, true, EWireType::Boolean, keySwitchColumnName}},
+ {rangeIndexColumnName, {EColumnType::RangeIndex, false, EWireType::Int64, rangeIndexColumnName}},
+ {rowIndexColumnName, {EColumnType::RowIndex, false, EWireType::Int64, rowIndexColumnName}},
+ };
+
+ Y_ENSURE(schema->GetWireType() == EWireType::Variant16,
+ "Expected 'variant16' wire type for schema, got '" << schema->GetWireType() << "'");
+ TVector<TSkiffTableSchema> result;
+ for (const auto& tableSchema : schema->GetChildren()) {
+ Y_ENSURE(tableSchema->GetWireType() == EWireType::Tuple,
+ "Expected 'tuple' wire type for table schema, got '" << tableSchema->GetWireType() << "'");
+ TVector<TSkiffColumnSchema> columns;
+ for (const auto& columnSchema : tableSchema->GetChildren()) {
+ if (columnSchema->GetName().StartsWith("$")) {
+ auto iter = specialColumns.find(columnSchema->GetName());
+ Y_ENSURE(iter != specialColumns.end(), "Unknown special column: " << columnSchema->GetName());
+ columns.push_back(iter->second);
+ } else {
+ auto wireType = columnSchema->GetWireType();
+ bool required = true;
+ if (wireType == EWireType::Variant8) {
+ const auto& children = columnSchema->GetChildren();
+ Y_ENSURE(
+ children.size() == 2 && children[0]->GetWireType() == EWireType::Nothing &&
+ NSkiff::IsSimpleType(children[1]->GetWireType()),
+ "Expected schema of form 'variant8<nothing, simple-type>', got "
+ << NSkiff::GetShortDebugString(columnSchema));
+ wireType = children[1]->GetWireType();
+ required = false;
+ }
+ Y_ENSURE(NSkiff::IsSimpleType(wireType),
+ "Expected column schema to be of simple type, got " << NSkiff::GetShortDebugString(columnSchema));
+ columns.emplace_back(
+ EColumnType::Dense,
+ required,
+ wireType,
+ columnSchema->GetName());
+ }
+ }
+ result.push_back({std::move(columns)});
+ }
+ return result;
+}
+
+void TSkiffTableReader::ReadRow()
+{
+ if (Row_.IsUndefined()) {
+ Row_ = TNode::CreateMap();
+ } else {
+ Row_.AsMap().clear();
+ }
+
+ if (RowIndex_) {
+ ++*RowIndex_;
+ }
+
+ TableIndex_ = Parser_->ParseVariant16Tag();
+ Y_ENSURE(TableIndex_ < Schemas_.size(), "Table index out of range: " << TableIndex_ << " >= " << Schemas_.size());
+ const auto& tableSchema = Schemas_[TableIndex_];
+
+ auto parse = [&](NSkiff::EWireType wireType) -> TNode {
+ switch (wireType) {
+ case NSkiff::EWireType::Int64:
+ return Parser_->ParseInt64();
+ case NSkiff::EWireType::Uint64:
+ return Parser_->ParseUint64();
+ case NSkiff::EWireType::Boolean:
+ return Parser_->ParseBoolean();
+ case NSkiff::EWireType::Double:
+ return Parser_->ParseDouble();
+ case NSkiff::EWireType::String32:
+ return Parser_->ParseString32();
+ case NSkiff::EWireType::Yson32:
+ return NodeFromYsonString(Parser_->ParseYson32());
+ case NSkiff::EWireType::Nothing:
+ return TNode::CreateEntity();
+ default:
+ Y_FAIL("Bad column wire type: '%s'", ::ToString(wireType).data());
+ }
+ };
+
+ for (const auto& columnSchema : tableSchema.Columns) {
+ if (!columnSchema.Required) {
+ auto tag = Parser_->ParseVariant8Tag();
+ if (tag == 0) {
+ if (columnSchema.Type == EColumnType::Dense) {
+ Row_[columnSchema.Name] = TNode::CreateEntity();
+ }
+ continue;
+ }
+ Y_ENSURE(tag == 1, "Tag for 'variant8<nothing," << columnSchema.WireType
+ << ">' expected to be 0 or 1, got " << tag);
+ }
+ auto value = parse(columnSchema.WireType);
+ switch (columnSchema.Type) {
+ case EColumnType::Dense:
+ Row_[columnSchema.Name] = std::move(value);
+ break;
+ case EColumnType::KeySwitch:
+ if (value.AsBool()) {
+ AfterKeySwitch_ = true;
+ Valid_ = false;
+ }
+ break;
+ case EColumnType::RangeIndex:
+ RangeIndex_ = value.AsInt64();
+ break;
+ case EColumnType::RowIndex:
+ RowIndex_ = value.AsInt64();
+ break;
+ default:
+ Y_FAIL("Bad column type: %d", static_cast<int>(columnSchema.Type));
+ }
+ }
+
+ // We successfully parsed one more row from the stream,
+ // so reset retry count to their initial value.
+ Input_.ResetRetries();
+}
+
+void TSkiffTableReader::EnsureValidity() const
+{
+ Y_ENSURE(Valid_, "Iterator is not valid");
+}
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/skiff_table_reader.h b/yt/cpp/mapreduce/io/skiff_table_reader.h
new file mode 100644
index 0000000000..95ece5f9c7
--- /dev/null
+++ b/yt/cpp/mapreduce/io/skiff_table_reader.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "counting_raw_reader.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+#include <yt/cpp/mapreduce/skiff/wire_type.h>
+#include <yt/cpp/mapreduce/skiff/unchecked_parser.h>
+
+#include <util/stream/buffered.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffTableReader
+ : public INodeReaderImpl
+{
+public:
+ TSkiffTableReader(
+ ::TIntrusivePtr<TRawTableReader> input,
+ const std::shared_ptr<NSkiff::TSkiffSchema>& schema);
+ ~TSkiffTableReader() override;
+
+ virtual const TNode& GetRow() const override;
+ virtual void MoveRow(TNode* row) override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsRawReaderExhausted() const override;
+
+private:
+ struct TSkiffTableSchema;
+
+private:
+ void EnsureValidity() const;
+ void ReadRow();
+ static TVector<TSkiffTableSchema> CreateSkiffTableSchemas(const std::shared_ptr<NSkiff::TSkiffSchema>& schema);
+
+private:
+ NDetail::TCountingRawTableReader Input_;
+ TBufferedInput BufferedInput_;
+ std::optional<NSkiff::TUncheckedSkiffParser> Parser_;
+ TVector<TSkiffTableSchema> Schemas_;
+
+ TNode Row_;
+
+ bool Valid_ = true;
+ bool AfterKeySwitch_ = false;
+ bool Finished_ = false;
+ TMaybe<ui64> RangeIndex_;
+ TMaybe<ui64> RowIndex_;
+ ui32 TableIndex_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/stream_raw_reader.cpp b/yt/cpp/mapreduce/io/stream_raw_reader.cpp
new file mode 100644
index 0000000000..ec19b67d0b
--- /dev/null
+++ b/yt/cpp/mapreduce/io/stream_raw_reader.cpp
@@ -0,0 +1,59 @@
+#include "stream_table_reader.h"
+
+#include "node_table_reader.h"
+#include "proto_table_reader.h"
+#include "skiff_table_reader.h"
+#include "yamr_table_reader.h"
+
+#include <util/system/env.h>
+#include <util/string/type.h>
+
+namespace NYT {
+
+template <>
+TTableReaderPtr<TNode> CreateTableReader<TNode>(
+ IInputStream* stream, const TTableReaderOptions& /*options*/)
+{
+ auto impl = ::MakeIntrusive<TNodeTableReader>(
+ ::MakeIntrusive<NDetail::TInputStreamProxy>(stream));
+ return new TTableReader<TNode>(impl);
+}
+
+template <>
+TTableReaderPtr<TYaMRRow> CreateTableReader<TYaMRRow>(
+ IInputStream* stream, const TTableReaderOptions& /*options*/)
+{
+ auto impl = ::MakeIntrusive<TYaMRTableReader>(
+ ::MakeIntrusive<NDetail::TInputStreamProxy>(stream));
+ return new TTableReader<TYaMRRow>(impl);
+}
+
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& /* options */,
+ const ::google::protobuf::Descriptor* descriptor)
+{
+ return new TLenvalProtoTableReader(
+ ::MakeIntrusive<TInputStreamProxy>(stream),
+ {descriptor});
+}
+
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& /* options */,
+ TVector<const ::google::protobuf::Descriptor*> descriptors)
+{
+ return new TLenvalProtoTableReader(
+ ::MakeIntrusive<TInputStreamProxy>(stream),
+ std::move(descriptors));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/stream_table_reader.h b/yt/cpp/mapreduce/io/stream_table_reader.h
new file mode 100644
index 0000000000..d799c63cf4
--- /dev/null
+++ b/yt/cpp/mapreduce/io/stream_table_reader.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInputStreamProxy
+ : public TRawTableReader
+{
+public:
+ TInputStreamProxy(IInputStream* stream)
+ : Stream_(stream)
+ { }
+
+ bool Retry(const TMaybe<ui32>& /* rangeIndex */, const TMaybe<ui64>& /* rowIndex */) override
+ {
+ return false;
+ }
+
+ void ResetRetries() override
+ { }
+
+ bool HasRangeIndices() const override
+ {
+ return false;
+ }
+
+protected:
+ size_t DoRead(void* buf, size_t len) override
+ {
+ return Stream_->Read(buf, len);
+ }
+
+private:
+ IInputStream* Stream_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& /* options */,
+ const ::google::protobuf::Descriptor* descriptor);
+
+::TIntrusivePtr<IProtoReaderImpl> CreateProtoReader(
+ IInputStream* stream,
+ const TTableReaderOptions& /* options */,
+ TVector<const ::google::protobuf::Descriptor*> descriptors);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+template <>
+TTableReaderPtr<TNode> CreateTableReader<TNode>(
+ IInputStream* stream, const TTableReaderOptions& options);
+
+template <>
+TTableReaderPtr<TYaMRRow> CreateTableReader<TYaMRRow>(
+ IInputStream* stream, const TTableReaderOptions& /*options*/);
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/ya.make b/yt/cpp/mapreduce/io/ya.make
new file mode 100644
index 0000000000..d355e86850
--- /dev/null
+++ b/yt/cpp/mapreduce/io/ya.make
@@ -0,0 +1,33 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ counting_raw_reader.cpp
+ job_reader.cpp
+ job_writer.cpp
+ lenval_table_reader.cpp
+ node_table_reader.cpp
+ node_table_writer.cpp
+ proto_helpers.cpp
+ proto_table_reader.cpp
+ proto_table_writer.cpp
+ skiff_row_table_reader.cpp
+ skiff_table_reader.cpp
+ stream_raw_reader.cpp
+ yamr_table_reader.cpp
+ yamr_table_writer.cpp
+)
+
+PEERDIR(
+ contrib/libs/protobuf
+ library/cpp/yson
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/interface/logging
+ yt/yt_proto/yt/formats
+ library/cpp/yson/node
+ yt/cpp/mapreduce/skiff
+)
+
+END()
diff --git a/yt/cpp/mapreduce/io/yamr_table_reader.cpp b/yt/cpp/mapreduce/io/yamr_table_reader.cpp
new file mode 100644
index 0000000000..6204738e10
--- /dev/null
+++ b/yt/cpp/mapreduce/io/yamr_table_reader.cpp
@@ -0,0 +1,145 @@
+#include "yamr_table_reader.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/raw_client/raw_requests.h>
+
+////////////////////////////////////////////////////////////////////
+
+static void CheckedSkip(IInputStream* input, size_t byteCount)
+{
+ size_t skipped = input->Skip(byteCount);
+ Y_ENSURE(skipped == byteCount, "Premature end of YaMR stream");
+}
+
+////////////////////////////////////////////////////////////////////
+
+namespace NYT {
+
+using namespace NYT::NDetail::NRawClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYaMRTableReader::TYaMRTableReader(::TIntrusivePtr<TRawTableReader> input)
+ : TLenvalTableReader(std::move(input))
+{ }
+
+TYaMRTableReader::~TYaMRTableReader()
+{ }
+
+const TYaMRRow& TYaMRTableReader::GetRow() const
+{
+ CheckValidity();
+ if (!RowTaken_) {
+ const_cast<TYaMRTableReader*>(this)->ReadRow();
+ }
+ return Row_;
+}
+
+bool TYaMRTableReader::IsValid() const
+{
+ return Valid_;
+}
+
+void TYaMRTableReader::Next()
+{
+ TLenvalTableReader::Next();
+}
+
+void TYaMRTableReader::NextKey()
+{
+ TLenvalTableReader::NextKey();
+}
+
+ui32 TYaMRTableReader::GetTableIndex() const
+{
+ return TLenvalTableReader::GetTableIndex();
+}
+
+ui32 TYaMRTableReader::GetRangeIndex() const
+{
+ return TLenvalTableReader::GetRangeIndex();
+}
+
+ui64 TYaMRTableReader::GetRowIndex() const
+{
+ return TLenvalTableReader::GetRowIndex();
+}
+
+TMaybe<size_t> TYaMRTableReader::GetReadByteCount() const
+{
+ return TLenvalTableReader::GetReadByteCount();
+}
+
+bool TYaMRTableReader::IsEndOfStream() const
+{
+ return TLenvalTableReader::IsEndOfStream();
+}
+
+bool TYaMRTableReader::IsRawReaderExhausted() const
+{
+ return TLenvalTableReader::IsRawReaderExhausted();
+}
+
+void TYaMRTableReader::ReadField(TString* result, i32 length)
+{
+ result->resize(length);
+ size_t count = Input_.Load(result->begin(), length);
+ Y_ENSURE(count == static_cast<size_t>(length), "Premature end of YaMR stream");
+}
+
+void TYaMRTableReader::ReadRow()
+{
+ while (true) {
+ try {
+ i32 value = static_cast<i32>(Length_);
+ ReadField(&Key_, value);
+ Row_.Key = Key_;
+
+ ReadInteger(&value);
+ ReadField(&SubKey_, value);
+ Row_.SubKey = SubKey_;
+
+ ReadInteger(&value);
+ ReadField(&Value_, value);
+ Row_.Value = Value_;
+
+ RowTaken_ = true;
+
+ // We successfully parsed one more row from the stream,
+ // so reset retry count to their initial value.
+ Input_.ResetRetries();
+
+ break;
+ } catch (const std::exception& ) {
+ if (!TLenvalTableReader::Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+void TYaMRTableReader::SkipRow()
+{
+ while (true) {
+ try {
+ i32 value = static_cast<i32>(Length_);
+ CheckedSkip(&Input_, value);
+
+ ReadInteger(&value);
+ CheckedSkip(&Input_, value);
+
+ ReadInteger(&value);
+ CheckedSkip(&Input_, value);
+ break;
+ } catch (const std::exception& ) {
+ if (!TLenvalTableReader::Retry()) {
+ throw;
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/yamr_table_reader.h b/yt/cpp/mapreduce/io/yamr_table_reader.h
new file mode 100644
index 0000000000..39fdecfa71
--- /dev/null
+++ b/yt/cpp/mapreduce/io/yamr_table_reader.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "lenval_table_reader.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+class TRawTableReader;
+struct TClientContext;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYaMRTableReader
+ : public IYaMRReaderImpl
+ , public TLenvalTableReader
+{
+public:
+ explicit TYaMRTableReader(::TIntrusivePtr<TRawTableReader> input);
+ ~TYaMRTableReader() override;
+
+ const TYaMRRow& GetRow() const override;
+
+ bool IsValid() const override;
+ void Next() override;
+ ui32 GetTableIndex() const override;
+ ui32 GetRangeIndex() const override;
+ ui64 GetRowIndex() const override;
+ void NextKey() override;
+ TMaybe<size_t> GetReadByteCount() const override;
+ bool IsEndOfStream() const override;
+ bool IsRawReaderExhausted() const override;
+
+private:
+ void ReadField(TString* result, i32 length);
+
+ void ReadRow();
+ void SkipRow() override;
+
+ TYaMRRow Row_;
+ TString Key_;
+ TString SubKey_;
+ TString Value_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/yamr_table_writer.cpp b/yt/cpp/mapreduce/io/yamr_table_writer.cpp
new file mode 100644
index 0000000000..cce7ceb0f0
--- /dev/null
+++ b/yt/cpp/mapreduce/io/yamr_table_writer.cpp
@@ -0,0 +1,53 @@
+#include "yamr_table_writer.h"
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYaMRTableWriter::TYaMRTableWriter(THolder<IProxyOutput> output)
+ : Output_(std::move(output))
+{ }
+
+TYaMRTableWriter::~TYaMRTableWriter()
+{ }
+
+size_t TYaMRTableWriter::GetTableCount() const
+{
+ return Output_->GetStreamCount();
+}
+
+void TYaMRTableWriter::FinishTable(size_t tableIndex) {
+ Output_->GetStream(tableIndex)->Finish();
+}
+
+void TYaMRTableWriter::AddRow(const TYaMRRow& row, size_t tableIndex)
+{
+ auto* stream = Output_->GetStream(tableIndex);
+
+ auto writeField = [&stream] (const TStringBuf& field) {
+ i32 length = static_cast<i32>(field.length());
+ stream->Write(&length, sizeof(length));
+ stream->Write(field.data(), field.length());
+ };
+
+ writeField(row.Key);
+ writeField(row.SubKey);
+ writeField(row.Value);
+
+ Output_->OnRowFinished(tableIndex);
+}
+
+void TYaMRTableWriter::AddRow(TYaMRRow&& row, size_t tableIndex) {
+ TYaMRTableWriter::AddRow(row, tableIndex);
+}
+
+void TYaMRTableWriter::Abort()
+{
+ Output_->Abort();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/io/yamr_table_writer.h b/yt/cpp/mapreduce/io/yamr_table_writer.h
new file mode 100644
index 0000000000..cf88eaf287
--- /dev/null
+++ b/yt/cpp/mapreduce/io/yamr_table_writer.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/io.h>
+
+namespace NYT {
+
+class IProxyOutput;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYaMRTableWriter
+ : public IYaMRWriterImpl
+{
+public:
+ explicit TYaMRTableWriter(THolder<IProxyOutput> output);
+ ~TYaMRTableWriter() override;
+
+ void AddRow(const TYaMRRow& row, size_t tableIndex) override;
+ void AddRow(TYaMRRow&& row, size_t tableIndex) override;
+
+ size_t GetTableCount() const override;
+ void FinishTable(size_t) override;
+ void Abort() override;
+
+private:
+ THolder<IProxyOutput> Output_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/library/table_schema/protobuf.cpp b/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
new file mode 100644
index 0000000000..888da828e7
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/protobuf.cpp
@@ -0,0 +1 @@
+#include "protobuf.h"
diff --git a/yt/cpp/mapreduce/library/table_schema/protobuf.h b/yt/cpp/mapreduce/library/table_schema/protobuf.h
new file mode 100644
index 0000000000..e29e096745
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/protobuf.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/common.h>
diff --git a/yt/cpp/mapreduce/library/table_schema/ya.make b/yt/cpp/mapreduce/library/table_schema/ya.make
new file mode 100644
index 0000000000..4aebad72dd
--- /dev/null
+++ b/yt/cpp/mapreduce/library/table_schema/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ protobuf.h
+ protobuf.cpp
+)
+
+PEERDIR(
+ yt/cpp/mapreduce/interface
+)
+
+END()
diff --git a/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp b/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
new file mode 100644
index 0000000000..be81f5a21a
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/raw_batch_request.cpp
@@ -0,0 +1,687 @@
+#include "raw_batch_request.h"
+
+#include "raw_requests.h"
+#include "rpc_parameters_serialization.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/errors.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <library/cpp/yson/node/node.h>
+
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <util/generic/guid.h>
+#include <util/string/builder.h>
+
+#include <exception>
+
+namespace NYT::NDetail::NRawClient {
+
+using NThreading::TFuture;
+using NThreading::TPromise;
+using NThreading::NewPromise;
+
+////////////////////////////////////////////////////////////////////
+
+static TString RequestInfo(const TNode& request)
+{
+ return ::TStringBuilder()
+ << request["command"].AsString() << ' ' << NodeToYsonString(request["parameters"]);
+}
+
+static void EnsureNothing(const TMaybe<TNode>& node)
+{
+ Y_ENSURE(!node, "Internal error: expected to have no response, but got response of type " << node->GetType());
+}
+
+static void EnsureSomething(const TMaybe<TNode>& node)
+{
+ Y_ENSURE(node, "Internal error: expected to have response of any type, but got no response.");
+}
+
+static void EnsureType(const TNode& node, TNode::EType type)
+{
+ Y_ENSURE(node.GetType() == type, "Internal error: unexpected response type. "
+ << "Expected: " << type << ", actual: " << node.GetType());
+}
+
+static void EnsureType(const TMaybe<TNode>& node, TNode::EType type)
+{
+ Y_ENSURE(node, "Internal error: expected to have response of type " << type << ", but got no response.");
+ EnsureType(*node, type);
+}
+
+////////////////////////////////////////////////////////////////////
+
+template <typename TReturnType>
+class TResponseParserBase
+ : public TRawBatchRequest::IResponseItemParser
+{
+public:
+ using TFutureResult = TFuture<TReturnType>;
+
+public:
+ TResponseParserBase()
+ : Result(NewPromise<TReturnType>())
+ { }
+
+ void SetException(std::exception_ptr e) override
+ {
+ Result.SetException(std::move(e));
+ }
+
+ TFuture<TReturnType> GetFuture()
+ {
+ return Result.GetFuture();
+ }
+
+protected:
+ TPromise<TReturnType> Result;
+};
+
+////////////////////////////////////////////////////////////////////
+
+
+class TGetResponseParser
+ : public TResponseParserBase<TNode>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureSomething(node);
+ Result.SetValue(std::move(*node));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TVoidResponseParser
+ : public TResponseParserBase<void>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureNothing(node);
+ Result.SetValue();
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TListResponseParser
+ : public TResponseParserBase<TNode::TListType>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::List);
+ Result.SetValue(std::move(node->AsList()));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TExistsResponseParser
+ : public TResponseParserBase<bool>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::Bool);
+ Result.SetValue(std::move(node->AsBool()));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TGuidResponseParser
+ : public TResponseParserBase<TGUID>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::String);
+ Result.SetValue(GetGuid(node->AsString()));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TCanonizeYPathResponseParser
+ : public TResponseParserBase<TRichYPath>
+{
+public:
+ explicit TCanonizeYPathResponseParser(TString pathPrefix, const TRichYPath& original)
+ : OriginalNode_(PathToNode(original))
+ , PathPrefix_(std::move(pathPrefix))
+ { }
+
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::String);
+
+ for (const auto& item : OriginalNode_.GetAttributes().AsMap()) {
+ node->Attributes()[item.first] = item.second;
+ }
+ TRichYPath result;
+ Deserialize(result, *node);
+ result.Path_ = AddPathPrefix(result.Path_, PathPrefix_);
+ Result.SetValue(result);
+ }
+
+private:
+ TNode OriginalNode_;
+ TString PathPrefix_;
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TGetOperationResponseParser
+ : public TResponseParserBase<TOperationAttributes>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::Map);
+ Result.SetValue(ParseOperationAttributes(*node));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TTableColumnarStatisticsParser
+ : public TResponseParserBase<TVector<TTableColumnarStatistics>>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::Map);
+ TVector<TTableColumnarStatistics> statistics;
+ Deserialize(statistics, *node);
+ Result.SetValue(std::move(statistics));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TTablePartitionsParser
+ : public TResponseParserBase<TMultiTablePartitions>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::Map);
+ TMultiTablePartitions partitions;
+ Deserialize(partitions, *node);
+ Result.SetValue(std::move(partitions));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetFileFromCacheParser
+ : public TResponseParserBase<TMaybe<TYPath>>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::String);
+ if (node->AsString().empty()) {
+ Result.SetValue(Nothing());
+ } else {
+ Result.SetValue(node->AsString());
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TYPathParser
+ : public TResponseParserBase<TYPath>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::String);
+ Result.SetValue(node->AsString());
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+class TCheckPermissionParser
+ : public TResponseParserBase<TCheckPermissionResponse>
+{
+public:
+ void SetResponse(TMaybe<TNode> node) override
+ {
+ EnsureType(node, TNode::Map);
+ Result.SetValue(ParseCheckPermissionResponse(*node));
+ }
+};
+
+////////////////////////////////////////////////////////////////////
+
+TRawBatchRequest::TBatchItem::TBatchItem(TNode parameters, ::TIntrusivePtr<IResponseItemParser> responseParser)
+ : Parameters(std::move(parameters))
+ , ResponseParser(std::move(responseParser))
+ , NextTry()
+{ }
+
+TRawBatchRequest::TBatchItem::TBatchItem(const TBatchItem& batchItem, TInstant nextTry)
+ : Parameters(batchItem.Parameters)
+ , ResponseParser(batchItem.ResponseParser)
+ , NextTry(nextTry)
+{ }
+
+////////////////////////////////////////////////////////////////////
+
+TRawBatchRequest::TRawBatchRequest(const TConfigPtr& config)
+ : Config_(config)
+{ }
+
+TRawBatchRequest::~TRawBatchRequest() = default;
+
+bool TRawBatchRequest::IsExecuted() const
+{
+ return Executed_;
+}
+
+void TRawBatchRequest::MarkExecuted()
+{
+ Executed_ = true;
+}
+
+template <typename TResponseParser>
+typename TResponseParser::TFutureResult TRawBatchRequest::AddRequest(
+ const TString& command,
+ TNode parameters,
+ TMaybe<TNode> input)
+{
+ return AddRequest(command, parameters, input, MakeIntrusive<TResponseParser>());
+}
+
+template <typename TResponseParser>
+typename TResponseParser::TFutureResult TRawBatchRequest::AddRequest(
+ const TString& command,
+ TNode parameters,
+ TMaybe<TNode> input,
+ ::TIntrusivePtr<TResponseParser> parser)
+{
+ Y_ENSURE(!Executed_, "Cannot add request: batch request is already executed");
+ TNode request;
+ request["command"] = command;
+ request["parameters"] = std::move(parameters);
+ if (input) {
+ request["input"] = std::move(*input);
+ }
+ BatchItemList_.emplace_back(std::move(request), parser);
+ return parser->GetFuture();
+}
+
+void TRawBatchRequest::AddRequest(TBatchItem batchItem)
+{
+ Y_ENSURE(!Executed_, "Cannot add request: batch request is already executed");
+ BatchItemList_.push_back(batchItem);
+}
+
+TFuture<TNodeId> TRawBatchRequest::Create(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options)
+{
+ return AddRequest<TGuidResponseParser>(
+ "create",
+ SerializeParamsForCreate(transaction, Config_->Prefix, path, type, options),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::Remove(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TRemoveOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "remove",
+ SerializeParamsForRemove(transaction, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<bool> TRawBatchRequest::Exists(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TExistsOptions& options)
+{
+ return AddRequest<TExistsResponseParser>(
+ "exists",
+ SerializeParamsForExists(transaction, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<TNode> TRawBatchRequest::Get(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ return AddRequest<TGetResponseParser>(
+ "get",
+ SerializeParamsForGet(transaction, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::Set(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TNode& node,
+ const TSetOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "set",
+ SerializeParamsForSet(transaction, Config_->Prefix, path, options),
+ node);
+}
+
+TFuture<TNode::TListType> TRawBatchRequest::List(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TListOptions& options)
+{
+ return AddRequest<TListResponseParser>(
+ "list",
+ SerializeParamsForList(transaction, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<TNodeId> TRawBatchRequest::Copy(
+ const TTransactionId& transaction,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options)
+{
+ return AddRequest<TGuidResponseParser>(
+ "copy",
+ SerializeParamsForCopy(transaction, Config_->Prefix, sourcePath, destinationPath, options),
+ Nothing());
+}
+
+TFuture<TNodeId> TRawBatchRequest::Move(
+ const TTransactionId& transaction,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options)
+{
+ return AddRequest<TGuidResponseParser>(
+ "move",
+ SerializeParamsForMove(transaction, Config_->Prefix, sourcePath, destinationPath, options),
+ Nothing());
+}
+
+TFuture<TNodeId> TRawBatchRequest::Link(
+ const TTransactionId& transaction,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options)
+{
+ return AddRequest<TGuidResponseParser>(
+ "link",
+ SerializeParamsForLink(transaction, Config_->Prefix, targetPath, linkPath, options),
+ Nothing());
+}
+
+TFuture<TLockId> TRawBatchRequest::Lock(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options)
+{
+ return AddRequest<TGuidResponseParser>(
+ "lock",
+ SerializeParamsForLock(transaction, Config_->Prefix, path, mode, options),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::Unlock(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TUnlockOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "unlock",
+ SerializeParamsForUnlock(transaction, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<TMaybe<TYPath>> TRawBatchRequest::GetFileFromCache(
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options)
+{
+ return AddRequest<TGetFileFromCacheParser>(
+ "get_file_from_cache",
+ SerializeParamsForGetFileFromCache(transactionId, md5Signature, cachePath, options),
+ Nothing());
+}
+
+TFuture<TYPath> TRawBatchRequest::PutFileToCache(
+ const TTransactionId& transactionId,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options)
+{
+ return AddRequest<TYPathParser>(
+ "put_file_to_cache",
+ SerializeParamsForPutFileToCache(transactionId, Config_->Prefix, filePath, md5Signature, cachePath, options),
+ Nothing());
+}
+
+TFuture<TCheckPermissionResponse> TRawBatchRequest::CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options)
+{
+ return AddRequest<TCheckPermissionParser>(
+ "check_permission",
+ SerializeParamsForCheckPermission(user, permission, Config_->Prefix, path, options),
+ Nothing());
+}
+
+TFuture<TOperationAttributes> TRawBatchRequest::GetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options)
+{
+ return AddRequest<TGetOperationResponseParser>(
+ "get_operation",
+ SerializeParamsForGetOperation(operationId, options),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::AbortOperation(const TOperationId& operationId)
+{
+ return AddRequest<TVoidResponseParser>(
+ "abort_op",
+ SerializeParamsForAbortOperation(operationId),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::CompleteOperation(const TOperationId& operationId)
+{
+ return AddRequest<TVoidResponseParser>(
+ "complete_op",
+ SerializeParamsForCompleteOperation(operationId),
+ Nothing());
+}
+TFuture<void> TRawBatchRequest::SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "suspend_operation",
+ SerializeParamsForSuspendOperation(operationId, options),
+ Nothing());
+}
+TFuture<void> TRawBatchRequest::ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "resume_operation",
+ SerializeParamsForResumeOperation(operationId, options),
+ Nothing());
+}
+
+TFuture<void> TRawBatchRequest::UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options)
+{
+ return AddRequest<TVoidResponseParser>(
+ "update_op_parameters",
+ SerializeParamsForUpdateOperationParameters(operationId, options),
+ Nothing());
+}
+
+TFuture<TRichYPath> TRawBatchRequest::CanonizeYPath(const TRichYPath& path)
+{
+ if (path.Path_.find_first_of("<>{}[]") != TString::npos) {
+ return AddRequest<TCanonizeYPathResponseParser>(
+ "parse_ypath",
+ SerializeParamsForParseYPath(path),
+ Nothing(),
+ MakeIntrusive<TCanonizeYPathResponseParser>(Config_->Prefix, path));
+ } else {
+ TRichYPath result = path;
+ result.Path_ = AddPathPrefix(result.Path_, Config_->Prefix);
+ return NThreading::MakeFuture(result);
+ }
+}
+
+TFuture<TVector<TTableColumnarStatistics>> TRawBatchRequest::GetTableColumnarStatistics(
+ const TTransactionId& transaction,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options)
+{
+ return AddRequest<TTableColumnarStatisticsParser>(
+ "get_table_columnar_statistics",
+ SerializeParamsForGetTableColumnarStatistics(transaction, paths, options),
+ Nothing());
+}
+
+TFuture<TMultiTablePartitions> TRawBatchRequest::GetTablePartitions(
+ const TTransactionId& transaction,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options)
+{
+ return AddRequest<TTablePartitionsParser>(
+ "partition_tables",
+ SerializeParamsForGetTablePartitions(transaction, paths, options),
+ Nothing());
+}
+
+void TRawBatchRequest::FillParameterList(size_t maxSize, TNode* result, TInstant* nextTry) const
+{
+ Y_VERIFY(result);
+ Y_VERIFY(nextTry);
+
+ *nextTry = TInstant();
+ maxSize = Min(maxSize, BatchItemList_.size());
+ *result = TNode::CreateList();
+ for (size_t i = 0; i < maxSize; ++i) {
+ YT_LOG_DEBUG("ExecuteBatch preparing: %v",
+ RequestInfo(BatchItemList_[i].Parameters));
+
+ result->Add(BatchItemList_[i].Parameters);
+ if (BatchItemList_[i].NextTry > *nextTry) {
+ *nextTry = BatchItemList_[i].NextTry;
+ }
+ }
+}
+
+void TRawBatchRequest::ParseResponse(
+ const TResponseInfo& requestResult,
+ const IRequestRetryPolicyPtr& retryPolicy,
+ TRawBatchRequest* retryBatch,
+ TInstant now)
+{
+ TNode node = NodeFromYsonString(requestResult.Response);
+ return ParseResponse(node, requestResult.RequestId, retryPolicy, retryBatch, now);
+}
+
+void TRawBatchRequest::ParseResponse(
+ TNode node,
+ const TString& requestId,
+ const IRequestRetryPolicyPtr& retryPolicy,
+ TRawBatchRequest* retryBatch,
+ TInstant now)
+{
+ Y_VERIFY(retryBatch);
+
+ EnsureType(node, TNode::List);
+ auto& responseList = node.AsList();
+ const auto size = responseList.size();
+ Y_ENSURE(size <= BatchItemList_.size(),
+ "Size of server response exceeds size of batch request;"
+ " size of batch: " << BatchItemList_.size() <<
+ " size of server response: " << size << '.');
+
+ for (size_t i = 0; i != size; ++i) {
+ try {
+ EnsureType(responseList[i], TNode::Map);
+ auto& responseNode = responseList[i].AsMap();
+ const auto outputIt = responseNode.find("output");
+ if (outputIt != responseNode.end()) {
+ BatchItemList_[i].ResponseParser->SetResponse(std::move(outputIt->second));
+ } else {
+ const auto errorIt = responseNode.find("error");
+ if (errorIt == responseNode.end()) {
+ BatchItemList_[i].ResponseParser->SetResponse(Nothing());
+ } else {
+ TErrorResponse error(400, requestId);
+ error.SetError(TYtError(errorIt->second));
+ if (auto curInterval = IsRetriable(error) ? retryPolicy->OnRetriableError(error) : Nothing()) {
+ YT_LOG_INFO(
+ "Batch subrequest (%s) failed, will retry, error: %s",
+ RequestInfo(BatchItemList_[i].Parameters),
+ error.what());
+ retryBatch->AddRequest(TBatchItem(BatchItemList_[i], now + *curInterval));
+ } else {
+ YT_LOG_ERROR(
+ "Batch subrequest (%s) failed, error: %s",
+ RequestInfo(BatchItemList_[i].Parameters),
+ error.what());
+ BatchItemList_[i].ResponseParser->SetException(std::make_exception_ptr(error));
+ }
+ }
+ }
+ } catch (const std::exception& e) {
+ // We don't expect other exceptions, so we don't catch (...)
+ BatchItemList_[i].ResponseParser->SetException(std::current_exception());
+ }
+ }
+ BatchItemList_.erase(BatchItemList_.begin(), BatchItemList_.begin() + size);
+}
+
+void TRawBatchRequest::SetErrorResult(std::exception_ptr e) const
+{
+ for (const auto& batchItem : BatchItemList_) {
+ batchItem.ResponseParser->SetException(e);
+ }
+}
+
+size_t TRawBatchRequest::BatchSize() const
+{
+ return BatchItemList_.size();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail::NRawClient
diff --git a/yt/cpp/mapreduce/raw_client/raw_batch_request.h b/yt/cpp/mapreduce/raw_client/raw_batch_request.h
new file mode 100644
index 0000000000..7ed5bebf5e
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/raw_batch_request.h
@@ -0,0 +1,190 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+
+#include <yt/cpp/mapreduce/interface/batch_request.h>
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/interface/node.h>
+#include <yt/cpp/mapreduce/interface/retry_policy.h>
+
+#include <yt/cpp/mapreduce/http/requests.h>
+
+#include <library/cpp/threading/future/future.h>
+
+#include <util/generic/ptr.h>
+#include <util/generic/deque.h>
+
+#include <exception>
+
+namespace NYT::NDetail {
+ struct TResponseInfo;
+}
+
+namespace NYT::NDetail::NRawClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRawBatchRequest
+ : public TThrRefBase
+{
+public:
+ struct IResponseItemParser
+ : public TThrRefBase
+ {
+ ~IResponseItemParser() = default;
+
+ virtual void SetResponse(TMaybe<TNode> node) = 0;
+ virtual void SetException(std::exception_ptr e) = 0;
+ };
+
+public:
+ TRawBatchRequest(const TConfigPtr& config);
+ ~TRawBatchRequest();
+
+ bool IsExecuted() const;
+ void MarkExecuted();
+
+ void FillParameterList(size_t maxSize, TNode* result, TInstant* nextTry) const;
+
+ size_t BatchSize() const;
+
+ void ParseResponse(
+ const TResponseInfo& requestResult,
+ const IRequestRetryPolicyPtr& retryPolicy,
+ TRawBatchRequest* retryBatch,
+ TInstant now = TInstant::Now());
+ void ParseResponse(
+ TNode response,
+ const TString& requestId,
+ const IRequestRetryPolicyPtr& retryPolicy,
+ TRawBatchRequest* retryBatch,
+ TInstant now = TInstant::Now());
+ void SetErrorResult(std::exception_ptr e) const;
+
+ ::NThreading::TFuture<TNodeId> Create(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options);
+ ::NThreading::TFuture<void> Remove(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TRemoveOptions& options);
+ ::NThreading::TFuture<bool> Exists(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TExistsOptions& options);
+ ::NThreading::TFuture<TNode> Get(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TGetOptions& options);
+ ::NThreading::TFuture<void> Set(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options);
+ ::NThreading::TFuture<TNode::TListType> List(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TListOptions& options);
+ ::NThreading::TFuture<TNodeId> Copy(
+ const TTransactionId& transaction,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options);
+ ::NThreading::TFuture<TNodeId> Move(
+ const TTransactionId& transaction,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options);
+ ::NThreading::TFuture<TNodeId> Link(
+ const TTransactionId& transaction,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options);
+ ::NThreading::TFuture<TLockId> Lock(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options);
+ ::NThreading::TFuture<void> Unlock(
+ const TTransactionId& transaction,
+ const TYPath& path,
+ const TUnlockOptions& options);
+ ::NThreading::TFuture<TMaybe<TYPath>> GetFileFromCache(
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options);
+ ::NThreading::TFuture<TYPath> PutFileToCache(
+ const TTransactionId& transactionId,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options);
+ ::NThreading::TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options);
+ ::NThreading::TFuture<TOperationAttributes> GetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options);
+ ::NThreading::TFuture<void> AbortOperation(const TOperationId& operationId);
+ ::NThreading::TFuture<void> CompleteOperation(const TOperationId& operationId);
+ ::NThreading::TFuture<void> SuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options);
+ ::NThreading::TFuture<void> ResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options);
+ ::NThreading::TFuture<void> UpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options);
+ ::NThreading::TFuture<TRichYPath> CanonizeYPath(const TRichYPath& path);
+ ::NThreading::TFuture<TVector<TTableColumnarStatistics>> GetTableColumnarStatistics(
+ const TTransactionId& transaction,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options);
+ ::NThreading::TFuture<TMultiTablePartitions> GetTablePartitions(
+ const TTransactionId& transaction,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options);
+
+private:
+ struct TBatchItem {
+ TNode Parameters;
+ ::TIntrusivePtr<IResponseItemParser> ResponseParser;
+ TInstant NextTry;
+
+ TBatchItem(TNode parameters, ::TIntrusivePtr<IResponseItemParser> responseParser);
+
+ TBatchItem(const TBatchItem& batchItem, TInstant nextTry);
+ };
+
+private:
+ template <typename TResponseParser>
+ typename TResponseParser::TFutureResult AddRequest(
+ const TString& command,
+ TNode parameters,
+ TMaybe<TNode> input);
+
+ template <typename TResponseParser>
+ typename TResponseParser::TFutureResult AddRequest(
+ const TString& command,
+ TNode parameters,
+ TMaybe<TNode> input,
+ ::TIntrusivePtr<TResponseParser> parser);
+
+ void AddRequest(TBatchItem batchItem);
+
+private:
+ TConfigPtr Config_;
+
+ TDeque<TBatchItem> BatchItemList_;
+ bool Executed_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail::NRawClient
diff --git a/yt/cpp/mapreduce/raw_client/raw_requests.cpp b/yt/cpp/mapreduce/raw_client/raw_requests.cpp
new file mode 100644
index 0000000000..26120759fd
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/raw_requests.cpp
@@ -0,0 +1,1027 @@
+#include "raw_requests.h"
+
+#include "raw_batch_request.h"
+#include "rpc_parameters_serialization.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+#include <yt/cpp/mapreduce/common/retry_lib.h>
+#include <yt/cpp/mapreduce/common/wait_proxy.h>
+
+#include <yt/cpp/mapreduce/http/fwd.h>
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/http/helpers.h>
+#include <yt/cpp/mapreduce/http/http_client.h>
+#include <yt/cpp/mapreduce/http/retry_request.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/client.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+#include <yt/cpp/mapreduce/interface/tvm.h>
+
+#include <yt/cpp/mapreduce/interface/logging/yt_log.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/guid.h>
+#include <util/generic/scope.h>
+
+namespace NYT::NDetail::NRawClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+void ExecuteBatch(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ TRawBatchRequest& batchRequest,
+ const TExecuteBatchOptions& options)
+{
+ if (batchRequest.IsExecuted()) {
+ ythrow yexception() << "Cannot execute batch request since it is already executed";
+ }
+ Y_DEFER {
+ batchRequest.MarkExecuted();
+ };
+
+ const auto concurrency = options.Concurrency_.GetOrElse(50);
+ const auto batchPartMaxSize = options.BatchPartMaxSize_.GetOrElse(concurrency * 5);
+
+ if (!retryPolicy) {
+ retryPolicy = CreateDefaultRequestRetryPolicy(context.Config);
+ }
+
+ while (batchRequest.BatchSize()) {
+ TRawBatchRequest retryBatch(context.Config);
+
+ while (batchRequest.BatchSize()) {
+ auto parameters = TNode::CreateMap();
+ TInstant nextTry;
+ batchRequest.FillParameterList(batchPartMaxSize, &parameters["requests"], &nextTry);
+ if (nextTry) {
+ SleepUntil(nextTry);
+ }
+ parameters["concurrency"] = concurrency;
+ auto body = NodeToYsonString(parameters);
+ THttpHeader header("POST", "execute_batch");
+ header.AddMutationId();
+ NDetail::TResponseInfo result;
+ try {
+ result = RetryRequestWithPolicy(retryPolicy, context, header, body);
+ } catch (const std::exception& e) {
+ batchRequest.SetErrorResult(std::current_exception());
+ retryBatch.SetErrorResult(std::current_exception());
+ throw;
+ }
+ batchRequest.ParseResponse(std::move(result), retryPolicy.Get(), &retryBatch);
+ }
+
+ batchRequest = std::move(retryBatch);
+ }
+}
+
+TNode Get(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ THttpHeader header("GET", "get");
+ header.MergeParameters(SerializeParamsForGet(transactionId, context.Config->Prefix, path, options));
+ return NodeFromYsonString(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+TNode TryGet(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ try {
+ return Get(retryPolicy, context, transactionId, path, options);
+ } catch (const TErrorResponse& error) {
+ if (!error.IsResolveError()) {
+ throw;
+ }
+ return TNode();
+ }
+}
+
+void Set(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options)
+{
+ THttpHeader header("PUT", "set");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForSet(transactionId, context.Config->Prefix, path, options));
+ auto body = NodeToYsonString(value);
+ RetryRequestWithPolicy(retryPolicy, context, header, body);
+}
+
+void MultisetAttributes(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TNode::TMapType& value,
+ const TMultisetAttributesOptions& options)
+{
+ THttpHeader header("PUT", "api/v4/multiset_attributes", false);
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForMultisetAttributes(transactionId, context.Config->Prefix, path, options));
+
+ auto body = NodeToYsonString(value);
+ RetryRequestWithPolicy(retryPolicy, context, header, body);
+}
+
+bool Exists(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TExistsOptions& options)
+{
+ THttpHeader header("GET", "exists");
+ header.MergeParameters(SerializeParamsForExists(transactionId, context.Config->Prefix, path, options));
+ return ParseBoolFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+TNodeId Create(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const ENodeType& type,
+ const TCreateOptions& options)
+{
+ THttpHeader header("POST", "create");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForCreate(transactionId, context.Config->Prefix, path, type, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+TNodeId Copy(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options)
+{
+ THttpHeader header("POST", "copy");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForCopy(transactionId, context.Config->Prefix, sourcePath, destinationPath, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+TNodeId Move(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options)
+{
+ THttpHeader header("POST", "move");
+ header.AddMutationId();
+ header.MergeParameters(NRawClient::SerializeParamsForMove(transactionId, context.Config->Prefix, sourcePath, destinationPath, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+void Remove(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TRemoveOptions& options)
+{
+ THttpHeader header("POST", "remove");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForRemove(transactionId, context.Config->Prefix, path, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+TNode::TListType List(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TListOptions& options)
+{
+ THttpHeader header("GET", "list");
+
+ TYPath updatedPath = AddPathPrefix(path, context.Config->Prefix);
+ // Translate "//" to "/"
+ // Translate "//some/constom/prefix/from/config/" to "//some/constom/prefix/from/config"
+ if (path.empty() && updatedPath.EndsWith('/')) {
+ updatedPath.pop_back();
+ }
+ header.MergeParameters(SerializeParamsForList(transactionId, context.Config->Prefix, updatedPath, options));
+ auto result = RetryRequestWithPolicy(retryPolicy, context, header);
+ return NodeFromYsonString(result.Response).AsList();
+}
+
+TNodeId Link(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options)
+{
+ THttpHeader header("POST", "link");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForLink(transactionId, context.Config->Prefix, targetPath, linkPath, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+TLockId Lock(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options)
+{
+ THttpHeader header("POST", "lock");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForLock(transactionId, context.Config->Prefix, path, mode, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+void Unlock(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TUnlockOptions& options)
+{
+ THttpHeader header("POST", "unlock");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForUnlock(transactionId, context.Config->Prefix, path, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void Concatenate(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options)
+{
+ THttpHeader header("POST", "concatenate");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForConcatenate(transactionId, context.Config->Prefix, sourcePaths, destinationPath, options));
+ RequestWithoutRetry(context, header);
+}
+
+void PingTx(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId)
+{
+ THttpHeader header("POST", "ping_tx");
+ header.MergeParameters(SerializeParamsForPingTx(transactionId));
+ TRequestConfig requestConfig;
+ requestConfig.HttpConfig = NHttpClient::THttpConfig{
+ .SocketTimeout = context.Config->PingTimeout
+ };
+ RetryRequestWithPolicy(retryPolicy, context, header, {}, requestConfig);
+}
+
+TOperationAttributes ParseOperationAttributes(const TNode& node)
+{
+ const auto& mapNode = node.AsMap();
+ TOperationAttributes result;
+
+ if (auto idNode = mapNode.FindPtr("id")) {
+ result.Id = GetGuid(idNode->AsString());
+ }
+
+ if (auto typeNode = mapNode.FindPtr("type")) {
+ result.Type = FromString<EOperationType>(typeNode->AsString());
+ } else if (auto operationTypeNode = mapNode.FindPtr("operation_type")) {
+ // COMPAT(levysotsky): "operation_type" is a deprecated synonim for "type".
+ // This branch should be removed when all clusters are updated.
+ result.Type = FromString<EOperationType>(operationTypeNode->AsString());
+ }
+
+ if (auto stateNode = mapNode.FindPtr("state")) {
+ result.State = stateNode->AsString();
+ // We don't use FromString here, because OS_IN_PROGRESS unites many states: "initializing", "running", etc.
+ if (*result.State == "completed") {
+ result.BriefState = EOperationBriefState::Completed;
+ } else if (*result.State == "aborted") {
+ result.BriefState = EOperationBriefState::Aborted;
+ } else if (*result.State == "failed") {
+ result.BriefState = EOperationBriefState::Failed;
+ } else {
+ result.BriefState = EOperationBriefState::InProgress;
+ }
+ }
+ if (auto authenticatedUserNode = mapNode.FindPtr("authenticated_user")) {
+ result.AuthenticatedUser = authenticatedUserNode->AsString();
+ }
+ if (auto startTimeNode = mapNode.FindPtr("start_time")) {
+ result.StartTime = TInstant::ParseIso8601(startTimeNode->AsString());
+ }
+ if (auto finishTimeNode = mapNode.FindPtr("finish_time")) {
+ result.FinishTime = TInstant::ParseIso8601(finishTimeNode->AsString());
+ }
+ auto briefProgressNode = mapNode.FindPtr("brief_progress");
+ if (briefProgressNode && briefProgressNode->HasKey("jobs")) {
+ result.BriefProgress.ConstructInPlace();
+ static auto load = [] (const TNode& item) {
+ // Backward compatibility with old YT versions
+ return item.IsInt64() ? item.AsInt64() : item["total"].AsInt64();
+ };
+ const auto& jobs = (*briefProgressNode)["jobs"];
+ result.BriefProgress->Aborted = load(jobs["aborted"]);
+ result.BriefProgress->Completed = load(jobs["completed"]);
+ result.BriefProgress->Running = jobs["running"].AsInt64();
+ result.BriefProgress->Total = jobs["total"].AsInt64();
+ result.BriefProgress->Failed = jobs["failed"].AsInt64();
+ result.BriefProgress->Lost = jobs["lost"].AsInt64();
+ result.BriefProgress->Pending = jobs["pending"].AsInt64();
+ }
+ if (auto briefSpecNode = mapNode.FindPtr("brief_spec")) {
+ result.BriefSpec = *briefSpecNode;
+ }
+ if (auto specNode = mapNode.FindPtr("spec")) {
+ result.Spec = *specNode;
+ }
+ if (auto fullSpecNode = mapNode.FindPtr("full_spec")) {
+ result.FullSpec = *fullSpecNode;
+ }
+ if (auto unrecognizedSpecNode = mapNode.FindPtr("unrecognized_spec")) {
+ result.UnrecognizedSpec = *unrecognizedSpecNode;
+ }
+ if (auto suspendedNode = mapNode.FindPtr("suspended")) {
+ result.Suspended = suspendedNode->AsBool();
+ }
+ if (auto resultNode = mapNode.FindPtr("result")) {
+ result.Result.ConstructInPlace();
+ auto error = TYtError((*resultNode)["error"]);
+ if (error.GetCode() != 0) {
+ result.Result->Error = std::move(error);
+ }
+ }
+ if (auto progressNode = mapNode.FindPtr("progress")) {
+ const auto& progressMap = progressNode->AsMap();
+ TMaybe<TInstant> buildTime;
+ if (auto buildTimeNode = progressMap.FindPtr("build_time")) {
+ buildTime = TInstant::ParseIso8601(buildTimeNode->AsString());
+ }
+ TJobStatistics jobStatistics;
+ if (auto jobStatisticsNode = progressMap.FindPtr("job_statistics")) {
+ jobStatistics = TJobStatistics(*jobStatisticsNode);
+ }
+ TJobCounters jobCounters;
+ if (auto jobCountersNode = progressMap.FindPtr("total_job_counter")) {
+ jobCounters = TJobCounters(*jobCountersNode);
+ }
+ result.Progress = TOperationProgress{
+ .JobStatistics = std::move(jobStatistics),
+ .JobCounters = std::move(jobCounters),
+ .BuildTime = buildTime,
+ };
+ }
+ if (auto eventsNode = mapNode.FindPtr("events")) {
+ result.Events.ConstructInPlace().reserve(eventsNode->Size());
+ for (const auto& eventNode : eventsNode->AsList()) {
+ result.Events->push_back(TOperationEvent{
+ eventNode["state"].AsString(),
+ TInstant::ParseIso8601(eventNode["time"].AsString()),
+ });
+ }
+ }
+ if (auto alertsNode = mapNode.FindPtr("alerts")) {
+ result.Alerts.ConstructInPlace();
+ for (const auto& [alertType, alertError] : alertsNode->AsMap()) {
+ result.Alerts->emplace(alertType, TYtError(alertError));
+ }
+ }
+
+ return result;
+}
+
+TOperationAttributes GetOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TGetOperationOptions& options)
+{
+ THttpHeader header("GET", "get_operation");
+ header.MergeParameters(SerializeParamsForGetOperation(operationId, options));
+ auto result = RetryRequestWithPolicy(retryPolicy, context, header);
+ return ParseOperationAttributes(NodeFromYsonString(result.Response));
+}
+
+void AbortOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId)
+{
+ THttpHeader header("POST", "abort_op");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForAbortOperation(operationId));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void CompleteOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId)
+{
+ THttpHeader header("POST", "complete_op");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForCompleteOperation(operationId));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void SuspendOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options)
+{
+ THttpHeader header("POST", "suspend_op");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForSuspendOperation(operationId, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void ResumeOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options)
+{
+ THttpHeader header("POST", "resume_op");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForResumeOperation(operationId, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+template <typename TKey>
+static THashMap<TKey, i64> GetCounts(const TNode& countsNode)
+{
+ THashMap<TKey, i64> counts;
+ for (const auto& entry : countsNode.AsMap()) {
+ counts.emplace(FromString<TKey>(entry.first), entry.second.AsInt64());
+ }
+ return counts;
+}
+
+TListOperationsResult ListOperations(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TListOperationsOptions& options)
+{
+ THttpHeader header("GET", "list_operations");
+ header.MergeParameters(SerializeParamsForListOperations(options));
+ auto responseInfo = RetryRequestWithPolicy(retryPolicy, context, header);
+ auto resultNode = NodeFromYsonString(responseInfo.Response);
+
+ TListOperationsResult result;
+ for (const auto& operationNode : resultNode["operations"].AsList()) {
+ result.Operations.push_back(ParseOperationAttributes(operationNode));
+ }
+
+ if (resultNode.HasKey("pool_counts")) {
+ result.PoolCounts = GetCounts<TString>(resultNode["pool_counts"]);
+ }
+ if (resultNode.HasKey("user_counts")) {
+ result.UserCounts = GetCounts<TString>(resultNode["user_counts"]);
+ }
+ if (resultNode.HasKey("type_counts")) {
+ result.TypeCounts = GetCounts<EOperationType>(resultNode["type_counts"]);
+ }
+ if (resultNode.HasKey("state_counts")) {
+ result.StateCounts = GetCounts<TString>(resultNode["state_counts"]);
+ }
+ if (resultNode.HasKey("failed_jobs_count")) {
+ result.WithFailedJobsCount = resultNode["failed_jobs_count"].AsInt64();
+ }
+
+ result.Incomplete = resultNode["incomplete"].AsBool();
+
+ return result;
+}
+
+void UpdateOperationParameters(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options)
+{
+ THttpHeader header("POST", "update_op_parameters");
+ header.MergeParameters(SerializeParamsForUpdateOperationParameters(operationId, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+TJobAttributes ParseJobAttributes(const TNode& node)
+{
+ const auto& mapNode = node.AsMap();
+ TJobAttributes result;
+
+ // Currently "get_job" returns "job_id" field and "list_jobs" returns "id" field.
+ auto idNode = mapNode.FindPtr("id");
+ if (!idNode) {
+ idNode = mapNode.FindPtr("job_id");
+ }
+ if (idNode) {
+ result.Id = GetGuid(idNode->AsString());
+ }
+
+ if (auto typeNode = mapNode.FindPtr("type")) {
+ result.Type = FromString<EJobType>(typeNode->AsString());
+ }
+ if (auto stateNode = mapNode.FindPtr("state")) {
+ result.State = FromString<EJobState>(stateNode->AsString());
+ }
+ if (auto addressNode = mapNode.FindPtr("address")) {
+ result.Address = addressNode->AsString();
+ }
+ if (auto taskNameNode = mapNode.FindPtr("task_name")) {
+ result.TaskName = taskNameNode->AsString();
+ }
+ if (auto startTimeNode = mapNode.FindPtr("start_time")) {
+ result.StartTime = TInstant::ParseIso8601(startTimeNode->AsString());
+ }
+ if (auto finishTimeNode = mapNode.FindPtr("finish_time")) {
+ result.FinishTime = TInstant::ParseIso8601(finishTimeNode->AsString());
+ }
+ if (auto progressNode = mapNode.FindPtr("progress")) {
+ result.Progress = progressNode->AsDouble();
+ }
+ if (auto stderrSizeNode = mapNode.FindPtr("stderr_size")) {
+ result.StderrSize = stderrSizeNode->AsUint64();
+ }
+ if (auto errorNode = mapNode.FindPtr("error")) {
+ result.Error.ConstructInPlace(*errorNode);
+ }
+ if (auto briefStatisticsNode = mapNode.FindPtr("brief_statistics")) {
+ result.BriefStatistics = *briefStatisticsNode;
+ }
+ if (auto inputPathsNode = mapNode.FindPtr("input_paths")) {
+ const auto& inputPathNodesList = inputPathsNode->AsList();
+ result.InputPaths.ConstructInPlace();
+ result.InputPaths->reserve(inputPathNodesList.size());
+ for (const auto& inputPathNode : inputPathNodesList) {
+ TRichYPath path;
+ Deserialize(path, inputPathNode);
+ result.InputPaths->push_back(std::move(path));
+ }
+ }
+ if (auto coreInfosNode = mapNode.FindPtr("core_infos")) {
+ const auto& coreInfoNodesList = coreInfosNode->AsList();
+ result.CoreInfos.ConstructInPlace();
+ result.CoreInfos->reserve(coreInfoNodesList.size());
+ for (const auto& coreInfoNode : coreInfoNodesList) {
+ TCoreInfo coreInfo;
+ coreInfo.ProcessId = coreInfoNode["process_id"].AsInt64();
+ coreInfo.ExecutableName = coreInfoNode["executable_name"].AsString();
+ if (coreInfoNode.HasKey("size")) {
+ coreInfo.Size = coreInfoNode["size"].AsUint64();
+ }
+ if (coreInfoNode.HasKey("error")) {
+ coreInfo.Error.ConstructInPlace(coreInfoNode["error"]);
+ }
+ result.CoreInfos->push_back(std::move(coreInfo));
+ }
+ }
+ return result;
+}
+
+TJobAttributes GetJob(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options)
+{
+ THttpHeader header("GET", "get_job");
+ header.MergeParameters(SerializeParamsForGetJob(operationId, jobId, options));
+ auto responseInfo = RetryRequestWithPolicy(retryPolicy, context, header);
+ auto resultNode = NodeFromYsonString(responseInfo.Response);
+ return ParseJobAttributes(resultNode);
+}
+
+TListJobsResult ListJobs(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TListJobsOptions& options)
+{
+ THttpHeader header("GET", "list_jobs");
+ header.MergeParameters(SerializeParamsForListJobs(operationId, options));
+ auto responseInfo = RetryRequestWithPolicy(retryPolicy, context, header);
+ auto resultNode = NodeFromYsonString(responseInfo.Response);
+
+ TListJobsResult result;
+
+ const auto& jobNodesList = resultNode["jobs"].AsList();
+ result.Jobs.reserve(jobNodesList.size());
+ for (const auto& jobNode : jobNodesList) {
+ result.Jobs.push_back(ParseJobAttributes(jobNode));
+ }
+
+ if (resultNode.HasKey("cypress_job_count") && !resultNode["cypress_job_count"].IsNull()) {
+ result.CypressJobCount = resultNode["cypress_job_count"].AsInt64();
+ }
+ if (resultNode.HasKey("controller_agent_job_count") && !resultNode["controller_agent_job_count"].IsNull()) {
+ result.ControllerAgentJobCount = resultNode["scheduler_job_count"].AsInt64();
+ }
+ if (resultNode.HasKey("archive_job_count") && !resultNode["archive_job_count"].IsNull()) {
+ result.ArchiveJobCount = resultNode["archive_job_count"].AsInt64();
+ }
+
+ return result;
+}
+
+class TResponseReader
+ : public IFileReader
+{
+public:
+ TResponseReader(const TClientContext& context, THttpHeader header)
+ {
+ if (context.ServiceTicketAuth) {
+ header.SetServiceTicket(context.ServiceTicketAuth->Ptr->IssueServiceTicket());
+ } else {
+ header.SetToken(context.Token);
+ }
+
+ auto hostName = GetProxyForHeavyRequest(context);
+ auto requestId = CreateGuidAsString();
+
+ Response_ = context.HttpClient->Request(GetFullUrl(hostName, context, header), requestId, header);
+ ResponseStream_ = Response_->GetResponseStream();
+ }
+
+private:
+ size_t DoRead(void* buf, size_t len) override
+ {
+ return ResponseStream_->Read(buf, len);
+ }
+
+ size_t DoSkip(size_t len) override
+ {
+ return ResponseStream_->Skip(len);
+ }
+
+private:
+ THttpRequest Request_;
+ NHttpClient::IHttpResponsePtr Response_;
+ IInputStream* ResponseStream_;
+};
+
+IFileReaderPtr GetJobInput(
+ const TClientContext& context,
+ const TJobId& jobId,
+ const TGetJobInputOptions& /* options */)
+{
+ THttpHeader header("GET", "get_job_input");
+ header.AddParameter("job_id", GetGuidAsString(jobId));
+ return new TResponseReader(context, std::move(header));
+}
+
+IFileReaderPtr GetJobFailContext(
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobFailContextOptions& /* options */)
+{
+ THttpHeader header("GET", "get_job_fail_context");
+ header.AddOperationId(operationId);
+ header.AddParameter("job_id", GetGuidAsString(jobId));
+ return new TResponseReader(context, std::move(header));
+}
+
+TString GetJobStderrWithRetries(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& /* options */)
+{
+ THttpHeader header("GET", "get_job_stderr");
+ header.AddOperationId(operationId);
+ header.AddParameter("job_id", GetGuidAsString(jobId));
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto responseInfo = RetryRequestWithPolicy(retryPolicy, context, header, {}, config);
+ return responseInfo.Response;
+}
+
+IFileReaderPtr GetJobStderr(
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& /* options */)
+{
+ THttpHeader header("GET", "get_job_stderr");
+ header.AddOperationId(operationId);
+ header.AddParameter("job_id", GetGuidAsString(jobId));
+ return new TResponseReader(context, std::move(header));
+}
+
+TMaybe<TYPath> GetFileFromCache(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options)
+{
+ THttpHeader header("GET", "get_file_from_cache");
+ header.MergeParameters(SerializeParamsForGetFileFromCache(transactionId, md5Signature, cachePath, options));
+ auto responseInfo = RetryRequestWithPolicy(retryPolicy, context, header);
+ auto path = NodeFromYsonString(responseInfo.Response).AsString();
+ return path.empty() ? Nothing() : TMaybe<TYPath>(path);
+}
+
+TYPath PutFileToCache(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options)
+{
+ THttpHeader header("POST", "put_file_to_cache");
+ header.MergeParameters(SerializeParamsForPutFileToCache(transactionId, context.Config->Prefix, filePath, md5Signature, cachePath, options));
+ auto result = RetryRequestWithPolicy(retryPolicy, context, header);
+ return NodeFromYsonString(result.Response).AsString();
+}
+
+TNode::TListType SkyShareTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options)
+{
+ THttpHeader header("POST", "api/v1/share", /*IsApi*/ false);
+
+ auto proxyName = context.ServerName.substr(0, context.ServerName.find('.'));
+
+ auto host = context.Config->SkynetApiHost;
+ if (host == "") {
+ host = "skynet." + proxyName + ".yt.yandex.net";
+ }
+
+ header.MergeParameters(SerializeParamsForSkyShareTable(proxyName, context.Config->Prefix, tablePaths, options));
+ TClientContext skyApiHost({ .ServerName = host, .HttpClient = NHttpClient::CreateDefaultHttpClient() });
+ TResponseInfo response = {};
+
+ // As documented at https://wiki.yandex-team.ru/yt/userdoc/blob_tables/#shag3.sozdajomrazdachu
+ // first request returns HTTP status code 202 (Accepted). And we need retrying until we have 200 (OK).
+ while (response.HttpCode != 200) {
+ response = RetryRequestWithPolicy(retryPolicy, skyApiHost, header, "");
+ TWaitProxy::Get()->Sleep(TDuration::Seconds(5));
+ }
+
+ if (options.KeyColumns_) {
+ return NodeFromJsonString(response.Response)["torrents"].AsList();
+ } else {
+ TNode torrent;
+
+ torrent["key"] = TNode::CreateList();
+ torrent["rbtorrent"] = response.Response;
+
+ return TNode::TListType{ torrent };
+ }
+}
+
+TCheckPermissionResponse ParseCheckPermissionResponse(const TNode& node)
+{
+ auto parseSingleResult = [] (const TNode::TMapType& node) {
+ TCheckPermissionResult result;
+ result.Action = ::FromString<ESecurityAction>(node.at("action").AsString());
+ if (auto objectId = node.FindPtr("object_id")) {
+ result.ObjectId = GetGuid(objectId->AsString());
+ }
+ if (auto objectName = node.FindPtr("object_name")) {
+ result.ObjectName = objectName->AsString();
+ }
+ if (auto subjectId = node.FindPtr("subject_id")) {
+ result.SubjectId = GetGuid(subjectId->AsString());
+ }
+ if (auto subjectName = node.FindPtr("subject_name")) {
+ result.SubjectName = subjectName->AsString();
+ }
+ return result;
+ };
+
+ const auto& mapNode = node.AsMap();
+ TCheckPermissionResponse result;
+ static_cast<TCheckPermissionResult&>(result) = parseSingleResult(mapNode);
+ if (auto columns = mapNode.FindPtr("columns")) {
+ result.Columns.reserve(columns->AsList().size());
+ for (const auto& columnNode : columns->AsList()) {
+ result.Columns.push_back(parseSingleResult(columnNode.AsMap()));
+ }
+ }
+ return result;
+}
+
+TCheckPermissionResponse CheckPermission(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options)
+{
+ THttpHeader header("GET", "check_permission");
+ header.MergeParameters(SerializeParamsForCheckPermission(user, permission, context.Config->Prefix, path, options));
+ auto response = RetryRequestWithPolicy(retryPolicy, context, header);
+ return ParseCheckPermissionResponse(NodeFromYsonString(response.Response));
+}
+
+TVector<TTabletInfo> GetTabletInfos(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options)
+{
+ THttpHeader header("POST", "api/v4/get_tablet_infos", false);
+ header.MergeParameters(SerializeParamsForGetTabletInfos(context.Config->Prefix, path, tabletIndexes, options));
+ auto response = RetryRequestWithPolicy(retryPolicy, context, header);
+ TVector<TTabletInfo> result;
+ Deserialize(result, *NodeFromYsonString(response.Response).AsMap().FindPtr("tablets"));
+ return result;
+}
+
+TVector<TTableColumnarStatistics> GetTableColumnarStatistics(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options)
+{
+ THttpHeader header("GET", "get_table_columnar_statistics");
+ header.MergeParameters(SerializeParamsForGetTableColumnarStatistics(transactionId, paths, options));
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto requestResult = RetryRequestWithPolicy(retryPolicy, context, header, {}, config);
+ auto response = NodeFromYsonString(requestResult.Response);
+ TVector<TTableColumnarStatistics> result;
+ Deserialize(result, response);
+ return result;
+}
+
+TMultiTablePartitions GetTablePartitions(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options)
+{
+ THttpHeader header("GET", "partition_tables");
+ header.MergeParameters(SerializeParamsForGetTablePartitions(transactionId, paths, options));
+ TRequestConfig config;
+ config.IsHeavy = true;
+ auto requestResult = RetryRequestWithPolicy(retryPolicy, context, header, {}, config);
+ auto response = NodeFromYsonString(requestResult.Response);
+ TMultiTablePartitions result;
+ Deserialize(result, response);
+ return result;
+}
+
+TRichYPath CanonizeYPath(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TRichYPath& path)
+{
+ return CanonizeYPaths(retryPolicy, context, {path}).front();
+}
+
+TVector<TRichYPath> CanonizeYPaths(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TVector<TRichYPath>& paths)
+{
+ TRawBatchRequest batch(context.Config);
+ TVector<NThreading::TFuture<TRichYPath>> futures;
+ futures.reserve(paths.size());
+ for (int i = 0; i < static_cast<int>(paths.size()); ++i) {
+ futures.push_back(batch.CanonizeYPath(paths[i]));
+ }
+ ExecuteBatch(retryPolicy, context, batch, TExecuteBatchOptions{});
+ TVector<TRichYPath> result;
+ result.reserve(futures.size());
+ for (auto& future : futures) {
+ result.push_back(future.ExtractValueSync());
+ }
+ return result;
+}
+
+void AlterTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TAlterTableOptions& options)
+{
+ THttpHeader header("POST", "alter_table");
+ header.AddMutationId();
+ header.MergeParameters(SerializeParamsForAlterTable(transactionId, context.Config->Prefix, path, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void AlterTableReplica(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TReplicaId& replicaId,
+ const TAlterTableReplicaOptions& options)
+{
+ THttpHeader header("POST", "alter_table_replica");
+ header.AddMutationId();
+ header.MergeParameters(NRawClient::SerializeParamsForAlterTableReplica(replicaId, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void DeleteRows(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TDeleteRowsOptions& options)
+{
+ THttpHeader header("PUT", "delete_rows");
+ header.SetInputFormat(TFormat::YsonBinary());
+ header.MergeParameters(NRawClient::SerializeParametersForDeleteRows(context.Config->Prefix, path, options));
+
+ auto body = NodeListToYsonString(keys);
+ TRequestConfig requestConfig;
+ requestConfig.IsHeavy = true;
+ RetryRequestWithPolicy(retryPolicy, context, header, body, requestConfig);
+}
+
+void FreezeTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TFreezeTableOptions& options)
+{
+ THttpHeader header("POST", "freeze_table");
+ header.MergeParameters(SerializeParamsForFreezeTable(context.Config->Prefix, path, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void UnfreezeTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TUnfreezeTableOptions& options)
+{
+ THttpHeader header("POST", "unfreeze_table");
+ header.MergeParameters(SerializeParamsForUnfreezeTable(context.Config->Prefix, path, options));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void AbortTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId)
+{
+ THttpHeader header("POST", "abort_tx");
+ header.AddMutationId();
+ header.MergeParameters(NRawClient::SerializeParamsForAbortTransaction(transactionId));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+void CommitTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId)
+{
+ THttpHeader header("POST", "commit_tx");
+ header.AddMutationId();
+ header.MergeParameters(NRawClient::SerializeParamsForCommitTransaction(transactionId));
+ RetryRequestWithPolicy(retryPolicy, context, header);
+}
+
+TTransactionId StartTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& parentTransactionId,
+ const TStartTransactionOptions& options)
+{
+ THttpHeader header("POST", "start_tx");
+ header.AddMutationId();
+ header.MergeParameters(NRawClient::SerializeParamsForStartTransaction(parentTransactionId, context.Config->TxTimeout, options));
+ return ParseGuidFromResponse(RetryRequestWithPolicy(retryPolicy, context, header).Response);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail::NRawClient
diff --git a/yt/cpp/mapreduce/raw_client/raw_requests.h b/yt/cpp/mapreduce/raw_client/raw_requests.h
new file mode 100644
index 0000000000..05fcbade76
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/raw_requests.h
@@ -0,0 +1,397 @@
+#pragma once
+
+#include "raw_batch_request.h"
+
+#include <yt/cpp/mapreduce/common/fwd.h>
+#include <yt/cpp/mapreduce/http/context.h>
+#include <yt/cpp/mapreduce/interface/client_method_options.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IRequestRetryPolicy;
+struct TClientContext;
+struct TExecuteBatchOptions;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail::NRawClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperationAttributes ParseOperationAttributes(const TNode& node);
+
+TCheckPermissionResponse ParseCheckPermissionResponse(const TNode& node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// marks `batchRequest' as executed
+void ExecuteBatch(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ TRawBatchRequest& batchRequest,
+ const TExecuteBatchOptions& options = TExecuteBatchOptions());
+
+//
+// Cypress
+//
+
+TNode Get(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TGetOptions& options = TGetOptions());
+
+TNode TryGet(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TGetOptions& options);
+
+void Set(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TNode& value,
+ const TSetOptions& options = TSetOptions());
+
+void MultisetAttributes(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TNode::TMapType& value,
+ const TMultisetAttributesOptions& options = TMultisetAttributesOptions());
+
+bool Exists(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TExistsOptions& options = TExistsOptions());
+
+TNodeId Create(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const ENodeType& type,
+ const TCreateOptions& options = TCreateOptions());
+
+TNodeId Copy(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options = TCopyOptions());
+
+TNodeId Move(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options = TMoveOptions());
+
+void Remove(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TRemoveOptions& options = TRemoveOptions());
+
+TNode::TListType List(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TListOptions& options = TListOptions());
+
+TNodeId Link(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options = TLinkOptions());
+
+TLockId Lock(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options = TLockOptions());
+
+void Unlock(
+ IRequestRetryPolicyPtr retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TUnlockOptions& options = TUnlockOptions());
+
+void Concatenate(
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options = TConcatenateOptions());
+
+//
+// Transactions
+//
+
+void PingTx(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId);
+
+//
+// Operations
+//
+
+TOperationAttributes GetOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TGetOperationOptions& options = TGetOperationOptions());
+
+void AbortOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId);
+
+void CompleteOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId);
+
+void SuspendOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options = TSuspendOperationOptions());
+
+void ResumeOperation(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options = TResumeOperationOptions());
+
+TListOperationsResult ListOperations(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TListOperationsOptions& options = TListOperationsOptions());
+
+void UpdateOperationParameters(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options = TUpdateOperationParametersOptions());
+
+//
+// Jobs
+//
+
+TJobAttributes GetJob(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options = TGetJobOptions());
+
+TListJobsResult ListJobs(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TListJobsOptions& options = TListJobsOptions());
+
+::TIntrusivePtr<IFileReader> GetJobInput(
+ const TClientContext& context,
+ const TJobId& jobId,
+ const TGetJobInputOptions& options = TGetJobInputOptions());
+
+::TIntrusivePtr<IFileReader> GetJobFailContext(
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobFailContextOptions& options = TGetJobFailContextOptions());
+
+TString GetJobStderrWithRetries(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& /* options */ = TGetJobStderrOptions());
+
+::TIntrusivePtr<IFileReader> GetJobStderr(
+ const TClientContext& context,
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobStderrOptions& options = TGetJobStderrOptions());
+
+//
+// File cache
+//
+
+TMaybe<TYPath> GetFileFromCache(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions& options = TGetFileFromCacheOptions());
+
+TYPath PutFileToCache(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options = TPutFileToCacheOptions());
+
+//
+// SkyShare
+//
+
+TNode::TListType SkyShareTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options);
+
+//
+// Misc
+//
+
+TCheckPermissionResponse CheckPermission(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TString& user,
+ EPermission permission,
+ const TYPath& path,
+ const TCheckPermissionOptions& options = TCheckPermissionOptions());
+
+TVector<TTabletInfo> GetTabletInfos(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options);
+
+TVector<TTableColumnarStatistics> GetTableColumnarStatistics(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options);
+
+TMultiTablePartitions GetTablePartitions(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options);
+
+TRichYPath CanonizeYPath(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TRichYPath& path);
+
+TVector<TRichYPath> CanonizeYPaths(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TVector<TRichYPath>& paths);
+
+//
+// Tables
+//
+
+void AlterTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId,
+ const TYPath& path,
+ const TAlterTableOptions& options);
+
+void AlterTableReplica(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TReplicaId& replicaId,
+ const TAlterTableReplicaOptions& options);
+
+void DeleteRows(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TNode::TListType& keys,
+ const TDeleteRowsOptions& options);
+
+void FreezeTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TFreezeTableOptions& options);
+
+void UnfreezeTable(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TYPath& path,
+ const TUnfreezeTableOptions& options);
+
+
+// Transactions
+void AbortTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId);
+
+void CommitTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& transactionId);
+
+TTransactionId StartTransaction(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TTransactionId& parentId,
+ const TStartTransactionOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<typename TSrc, typename TBatchAdder>
+auto BatchTransform(
+ const IRequestRetryPolicyPtr& retryPolicy,
+ const TClientContext& context,
+ const TSrc& src,
+ TBatchAdder batchAdder,
+ const TExecuteBatchOptions& executeBatchOptions = {})
+{
+ TRawBatchRequest batch(context.Config);
+ using TFuture = decltype(batchAdder(batch, *std::begin(src)));
+ TVector<TFuture> futures;
+ for (const auto& el : src) {
+ futures.push_back(batchAdder(batch, el));
+ }
+ ExecuteBatch(retryPolicy, context, batch, executeBatchOptions);
+ using TDst = decltype(futures[0].ExtractValueSync());
+ TVector<TDst> result;
+ result.reserve(std::size(src));
+ for (auto& future : futures) {
+ result.push_back(future.ExtractValueSync());
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail::NRawClient
+} // namespace NYT
diff --git a/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp b/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
new file mode 100644
index 0000000000..1936266d0d
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.cpp
@@ -0,0 +1,873 @@
+#include "rpc_parameters_serialization.h"
+
+#include <yt/cpp/mapreduce/common/helpers.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+#include <yt/cpp/mapreduce/interface/client_method_options.h>
+#include <yt/cpp/mapreduce/interface/operation.h>
+#include <yt/cpp/mapreduce/interface/serialize.h>
+
+#include <library/cpp/yson/node/node.h>
+#include <library/cpp/yson/node/node_io.h>
+#include <library/cpp/yson/node/node_builder.h>
+
+#include <util/generic/guid.h>
+#include <util/string/cast.h>
+
+namespace NYT::NDetail::NRawClient {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////
+
+static void SetTransactionIdParam(TNode* node, const TTransactionId& transactionId)
+{
+ if (transactionId != TTransactionId()) {
+ (*node)["transaction_id"] = GetGuidAsString(transactionId);
+ }
+}
+
+static void SetOperationIdParam(TNode* node, const TOperationId& operationId)
+{
+ (*node)["operation_id"] = GetGuidAsString(operationId);
+}
+
+static void SetPathParam(TNode* node, const TString& pathPrefix, const TYPath& path)
+{
+ (*node)["path"] = AddPathPrefix(path, pathPrefix);
+}
+
+static TNode SerializeAttributeFilter(const TAttributeFilter& attributeFilter)
+{
+ TNode result = TNode::CreateList();
+ for (const auto& attribute : attributeFilter.Attributes_) {
+ result.Add(attribute);
+ }
+ return result;
+}
+
+static TNode SerializeAttributeFilter(const TOperationAttributeFilter& attributeFilter)
+{
+ TNode result = TNode::CreateList();
+ for (const auto& attribute : attributeFilter.Attributes_) {
+ result.Add(ToString(attribute));
+ }
+ return result;
+}
+
+template <typename TOptions>
+static void SetFirstLastTabletIndex(TNode* node, const TOptions& options)
+{
+ if (options.FirstTabletIndex_) {
+ (*node)["first_tablet_index"] = *options.FirstTabletIndex_;
+ }
+ if (options.LastTabletIndex_) {
+ (*node)["last_tablet_index"] = *options.LastTabletIndex_;
+ }
+}
+
+static TString GetDefaultTransactionTitle()
+{
+ const auto processState = TProcessState::Get();
+ TStringStream res;
+
+ res << "User transaction. Created by: " << processState->UserName << " on " << processState->FqdnHostName
+ << " client: " << processState->ClientVersion << " pid: " << processState->Pid;
+ if (!processState->CommandLine.empty()) {
+ res << " program: " << processState->CommandLine[0];
+ } else {
+ res << " command line is unknown probably NYT::Initialize was never called";
+ }
+
+#ifndef NDEBUG
+ res << " build: debug";
+#endif
+
+ return res.Str();
+}
+
+template <typename T>
+void SerializeMasterReadOptions(TNode* node, const TMasterReadOptions<T>& options)
+{
+ if (options.ReadFrom_) {
+ (*node)["read_from"] = ToString(*options.ReadFrom_);
+ }
+}
+
+////////////////////////////////////////////////////////////////////
+
+TNode SerializeParamsForCreate(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ result["recursive"] = options.Recursive_;
+ result["type"] = ToString(type);
+ result["ignore_existing"] = options.IgnoreExisting_;
+ result["force"] = options.Force_;
+ if (options.Attributes_) {
+ result["attributes"] = *options.Attributes_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForRemove(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TRemoveOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ result["recursive"] = options.Recursive_;
+ result["force"] = options.Force_;
+ return result;
+}
+
+TNode SerializeParamsForExists(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TExistsOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ SerializeMasterReadOptions(&result, options);
+ return result;
+}
+
+TNode SerializeParamsForGet(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TGetOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ SerializeMasterReadOptions(&result, options);
+ if (options.AttributeFilter_) {
+ result["attributes"] = SerializeAttributeFilter(*options.AttributeFilter_);
+ }
+ if (options.MaxSize_) {
+ result["max_size"] = *options.MaxSize_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForSet(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TSetOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ result["recursive"] = options.Recursive_;
+ if (options.Force_) {
+ result["force"] = *options.Force_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForMultisetAttributes(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ [[maybe_unused]] const TMultisetAttributesOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ return result;
+}
+
+TNode SerializeParamsForList(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TListOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ SerializeMasterReadOptions(&result, options);
+ if (options.MaxSize_) {
+ result["max_size"] = *options.MaxSize_;
+ }
+ if (options.AttributeFilter_) {
+ result["attributes"] = SerializeAttributeFilter(*options.AttributeFilter_);
+ }
+ return result;
+}
+
+TNode SerializeParamsForCopy(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ result["source_path"] = AddPathPrefix(sourcePath, pathPrefix);
+ result["destination_path"] = AddPathPrefix(destinationPath, pathPrefix);
+ result["recursive"] = options.Recursive_;
+ result["force"] = options.Force_;
+ result["preserve_account"] = options.PreserveAccount_;
+ if (options.PreserveExpirationTime_) {
+ result["preserve_expiration_time"] = *options.PreserveExpirationTime_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForMove(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ result["source_path"] = AddPathPrefix(sourcePath, pathPrefix);
+ result["destination_path"] = AddPathPrefix(destinationPath, pathPrefix);
+ result["recursive"] = options.Recursive_;
+ result["force"] = options.Force_;
+ result["preserve_account"] = options.PreserveAccount_;
+ if (options.PreserveExpirationTime_) {
+ result["preserve_expiration_time"] = *options.PreserveExpirationTime_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForLink(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ result["target_path"] = AddPathPrefix(targetPath, pathPrefix);
+ result["link_path"] = AddPathPrefix(linkPath, pathPrefix);
+ result["recursive"] = options.Recursive_;
+ result["ignore_existing"] = options.IgnoreExisting_;
+ result["force"] = options.Force_;
+ if (options.Attributes_) {
+ result["attributes"] = *options.Attributes_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForLock(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ result["mode"] = ToString(mode);
+ result["waitable"] = options.Waitable_;
+ if (options.AttributeKey_) {
+ result["attribute_key"] = *options.AttributeKey_;
+ }
+ if (options.ChildKey_) {
+ result["child_key"] = *options.ChildKey_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForUnlock(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TUnlockOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ Y_UNUSED(options);
+ return result;
+}
+
+TNode SerializeParamsForConcatenate(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ {
+ auto actualDestination = destinationPath;
+ actualDestination.Path(AddPathPrefix(actualDestination.Path_, pathPrefix));
+ if (options.Append_) {
+ actualDestination.Append(*options.Append_);
+ }
+ result["destination_path"] = PathToNode(actualDestination);
+ }
+ auto& sourcePathsNode = result["source_paths"];
+ for (const auto& path : sourcePaths) {
+ auto actualSource = path;
+ actualSource.Path(AddPathPrefix(actualSource.Path_, pathPrefix));
+ sourcePathsNode.Add(PathToNode(actualSource));
+ }
+ return result;
+}
+
+TNode SerializeParamsForPingTx(
+ const TTransactionId& transactionId)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ return result;
+}
+
+TNode SerializeParamsForListOperations(
+ const TListOperationsOptions& options)
+{
+ TNode result = TNode::CreateMap();
+ if (options.FromTime_) {
+ result["from_time"] = ToString(*options.FromTime_);
+ }
+ if (options.ToTime_) {
+ result["to_time"] = ToString(*options.ToTime_);
+ }
+ if (options.CursorTime_) {
+ result["cursor_time"] = ToString(*options.CursorTime_);
+ }
+ if (options.CursorDirection_) {
+ result["cursor_direction"] = ToString(*options.CursorDirection_);
+ }
+ if (options.Pool_) {
+ result["pool"] = *options.Pool_;
+ }
+ if (options.Filter_) {
+ result["filter"] = *options.Filter_;
+ }
+ if (options.User_) {
+ result["user"] = *options.User_;
+ }
+ if (options.State_) {
+ result["state"] = *options.State_;
+ }
+ if (options.Type_) {
+ result["type"] = ToString(*options.Type_);
+ }
+ if (options.WithFailedJobs_) {
+ result["with_failed_jobs"] = *options.WithFailedJobs_;
+ }
+ if (options.IncludeCounters_) {
+ result["include_counters"] = *options.IncludeCounters_;
+ }
+ if (options.IncludeArchive_) {
+ result["include_archive"] = *options.IncludeArchive_;
+ }
+ if (options.Limit_) {
+ result["limit"] = *options.Limit_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForGetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ if (options.AttributeFilter_) {
+ result["attributes"] = SerializeAttributeFilter(*options.AttributeFilter_);
+ }
+ return result;
+}
+
+TNode SerializeParamsForAbortOperation(const TOperationId& operationId)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ return result;
+}
+
+TNode SerializeParamsForCompleteOperation(const TOperationId& operationId)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ return result;
+}
+
+TNode SerializeParamsForSuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ if (options.AbortRunningJobs_) {
+ result["abort_running_jobs"] = *options.AbortRunningJobs_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ Y_UNUSED(options);
+ return result;
+}
+
+TNode SerializeParamsForUpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ TNode& parameters = result["parameters"];
+ if (options.Pool_) {
+ parameters["pool"] = *options.Pool_;
+ }
+ if (options.Weight_) {
+ parameters["weight"] = *options.Weight_;
+ }
+ if (!options.Owners_.empty()) {
+ parameters["owners"] = TNode::CreateList();
+ for (const auto& owner : options.Owners_) {
+ parameters["owners"].Add(owner);
+ }
+ }
+ if (options.SchedulingOptionsPerPoolTree_) {
+ parameters["scheduling_options_per_pool_tree"] = TNode::CreateMap();
+ for (const auto& entry : options.SchedulingOptionsPerPoolTree_->Options_) {
+ auto schedulingOptionsNode = TNode::CreateMap();
+ const auto& schedulingOptions = entry.second;
+ if (schedulingOptions.Pool_) {
+ schedulingOptionsNode["pool"] = *schedulingOptions.Pool_;
+ }
+ if (schedulingOptions.Weight_) {
+ schedulingOptionsNode["weight"] = *schedulingOptions.Weight_;
+ }
+ if (schedulingOptions.ResourceLimits_) {
+ auto resourceLimitsNode = TNode::CreateMap();
+ const auto& resourceLimits = *schedulingOptions.ResourceLimits_;
+ if (resourceLimits.UserSlots_) {
+ resourceLimitsNode["user_slots"] = *resourceLimits.UserSlots_;
+ }
+ if (resourceLimits.Memory_) {
+ resourceLimitsNode["memory"] = *resourceLimits.Memory_;
+ }
+ if (resourceLimits.Cpu_) {
+ resourceLimitsNode["cpu"] = *resourceLimits.Cpu_;
+ }
+ if (resourceLimits.Network_) {
+ resourceLimitsNode["network"] = *resourceLimits.Network_;
+ }
+ schedulingOptionsNode["resource_limits"] = std::move(resourceLimitsNode);
+ }
+ parameters["scheduling_options_per_pool_tree"][entry.first] = std::move(schedulingOptionsNode);
+ }
+ }
+ return result;
+}
+
+TNode SerializeParamsForGetJob(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& /* options */)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+ result["job_id"] = GetGuidAsString(jobId);
+ return result;
+}
+
+TNode SerializeParamsForListJobs(
+ const TOperationId& operationId,
+ const TListJobsOptions& options)
+{
+ TNode result;
+ SetOperationIdParam(&result, operationId);
+
+ if (options.Type_) {
+ result["type"] = ToString(*options.Type_);
+ }
+ if (options.State_) {
+ result["state"] = ToString(*options.State_);
+ }
+ if (options.Address_) {
+ result["address"] = *options.Address_;
+ }
+ if (options.WithStderr_) {
+ result["with_stderr"] = *options.WithStderr_;
+ }
+ if (options.WithSpec_) {
+ result["with_spec"] = *options.WithSpec_;
+ }
+ if (options.WithFailContext_) {
+ result["with_fail_context"] = *options.WithFailContext_;
+ }
+
+ if (options.SortField_) {
+ result["sort_field"] = ToString(*options.SortField_);
+ }
+ if (options.SortOrder_) {
+ result["sort_order"] = ToString(*options.SortOrder_);
+ }
+
+ if (options.Offset_) {
+ result["offset"] = *options.Offset_;
+ }
+ if (options.Limit_) {
+ result["limit"] = *options.Limit_;
+ }
+
+ if (options.IncludeCypress_) {
+ result["include_cypress"] = *options.IncludeCypress_;
+ }
+ if (options.IncludeArchive_) {
+ result["include_archive"] = *options.IncludeArchive_;
+ }
+ if (options.IncludeControllerAgent_) {
+ result["include_controller_agent"] = *options.IncludeControllerAgent_;
+ }
+ return result;
+}
+
+TNode SerializeParametersForInsertRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TInsertRowsOptions& options)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ if (options.Aggregate_) {
+ result["aggregate"] = *options.Aggregate_;
+ }
+ if (options.Update_) {
+ result["update"] = *options.Update_;
+ }
+ if (options.Atomicity_) {
+ result["atomicity"] = ToString(*options.Atomicity_);
+ }
+ if (options.Durability_) {
+ result["durability"] = ToString(*options.Durability_);
+ }
+ if (options.RequireSyncReplica_) {
+ result["require_sync_replica"] = *options.RequireSyncReplica_;
+ }
+ return result;
+}
+
+TNode SerializeParametersForDeleteRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TDeleteRowsOptions& options)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ if (options.Atomicity_) {
+ result["atomicity"] = ToString(*options.Atomicity_);
+ }
+ if (options.Durability_) {
+ result["durability"] = ToString(*options.Durability_);
+ }
+ if (options.RequireSyncReplica_) {
+ result["require_sync_replica"] = *options.RequireSyncReplica_;
+ }
+ return result;
+}
+
+TNode SerializeParametersForTrimRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TTrimRowsOptions& /* options*/)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ return result;
+}
+
+TNode SerializeParamsForParseYPath(const TRichYPath& path)
+{
+ TNode result;
+ result["path"] = PathToNode(path);
+ return result;
+}
+
+TNode SerializeParamsForEnableTableReplica(
+ const TReplicaId& replicaId)
+{
+ TNode result;
+ result["replica_id"] = GetGuidAsString(replicaId);
+ return result;
+}
+
+TNode SerializeParamsForDisableTableReplica(
+ const TReplicaId& replicaId)
+{
+ TNode result;
+ result["replica_id"] = GetGuidAsString(replicaId);
+ return result;
+}
+
+TNode SerializeParamsForAlterTableReplica(const TReplicaId& replicaId, const TAlterTableReplicaOptions& options)
+{
+ TNode result;
+ result["replica_id"] = GetGuidAsString(replicaId);
+ if (options.Enabled_) {
+ result["enabled"] = *options.Enabled_;
+ }
+ if (options.Mode_) {
+ result["mode"] = ToString(*options.Mode_);
+ }
+ return result;
+}
+
+TNode SerializeParamsForFreezeTable(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TFreezeTableOptions& options)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ SetFirstLastTabletIndex(&result, options);
+ return result;
+}
+
+TNode SerializeParamsForUnfreezeTable(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TUnfreezeTableOptions& options)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ SetFirstLastTabletIndex(&result, options);
+ return result;
+}
+
+TNode SerializeParamsForAlterTable(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TAlterTableOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, path);
+ if (options.Dynamic_) {
+ result["dynamic"] = *options.Dynamic_;
+ }
+ if (options.Schema_) {
+ TNode schema;
+ {
+ TNodeBuilder builder(&schema);
+ Serialize(*options.Schema_, &builder);
+ }
+ result["schema"] = schema;
+ }
+ if (options.UpstreamReplicaId_) {
+ result["upstream_replica_id"] = GetGuidAsString(*options.UpstreamReplicaId_);
+ }
+ return result;
+}
+
+TNode SerializeParamsForGetTableColumnarStatistics(
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ for (const auto& path : paths) {
+ result["paths"].Add(PathToNode(path));
+ }
+ if (options.FetcherMode_) {
+ result["fetcher_mode"] = ToString(*options.FetcherMode_);
+ }
+ return result;
+}
+
+TNode SerializeParamsForGetTablePartitions(
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ for (const auto& path : paths) {
+ result["paths"].Add(PathToNode(path));
+ }
+ result["partition_mode"] = ToString(options.PartitionMode_);
+ result["data_weight_per_partition"] = options.DataWeightPerPartition_;
+ if (options.MaxPartitionCount_) {
+ result["max_partition_count"] = *options.MaxPartitionCount_;
+ }
+ result["adjust_data_weight_per_partition"] = options.AdjustDataWeightPerPartition_;
+ return result;
+}
+
+TNode SerializeParamsForGetFileFromCache(
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions&)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ result["md5"] = md5Signature;
+ result["cache_path"] = cachePath;
+ return result;
+}
+
+TNode SerializeParamsForPutFileToCache(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ SetPathParam(&result, pathPrefix, filePath);
+ result["md5"] = md5Signature;
+ result["cache_path"] = cachePath;
+ if (options.PreserveExpirationTimeout_) {
+ result["preserve_expiration_timeout"] = *options.PreserveExpirationTimeout_;
+ }
+ return result;
+}
+
+TNode SerializeParamsForSkyShareTable(
+ const TString& serverName,
+ const TString& pathPrefix,
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options)
+{
+ TNode result;
+
+ if (tablePaths.size() == 1) {
+ SetPathParam(&result, pathPrefix, tablePaths[0]);
+ } else {
+ auto pathList = TNode::CreateList();
+ for (const auto& p : tablePaths) {
+ pathList.Add(AddPathPrefix(p, pathPrefix));
+ }
+ result["paths"] = pathList;
+ }
+ result["cluster"] = serverName;
+
+ if (options.KeyColumns_) {
+ auto keyColumnsList = TNode::CreateList();
+ for (const auto& s : options.KeyColumns_->Parts_) {
+ if (s.empty()) {
+ continue;
+ }
+ keyColumnsList.Add(s);
+ }
+ result["key_columns"] = keyColumnsList;
+ }
+
+ if (options.EnableFastbone_) {
+ result["enable_fastbone"] = *options.EnableFastbone_;
+ }
+
+ return result;
+}
+
+TNode SerializeParamsForCheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TCheckPermissionOptions& options)
+{
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ result["path"] = path;
+ result["user"] = user;
+ result["permission"] = ToString(permission);
+ if (!options.Columns_.empty()) {
+ result["columns"] = TNode::CreateList();
+ result["columns"].AsList().assign(options.Columns_.begin(), options.Columns_.end());
+ }
+ return result;
+}
+
+TNode SerializeParamsForGetTabletInfos(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options)
+{
+ Y_UNUSED(options);
+ TNode result;
+ SetPathParam(&result, pathPrefix, path);
+ result["tablet_indexes"] = TNode::CreateList();
+ result["tablet_indexes"].AsList().assign(tabletIndexes.begin(), tabletIndexes.end());
+ return result;
+}
+
+TNode SerializeParamsForAbortTransaction(const TTransactionId& transactionId)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ return result;
+}
+
+TNode SerializeParamsForCommitTransaction(const TTransactionId& transactionId)
+{
+ TNode result;
+ SetTransactionIdParam(&result, transactionId);
+ return result;
+}
+
+TNode SerializeParamsForStartTransaction(
+ const TTransactionId& parentTransactionId,
+ TDuration txTimeout,
+ const TStartTransactionOptions& options)
+{
+ TNode result;
+
+ SetTransactionIdParam(&result, parentTransactionId);
+ result["timeout"] = static_cast<i64>((options.Timeout_.GetOrElse(txTimeout).MilliSeconds()));
+ if (options.Deadline_) {
+ result["deadline"] = ToString(options.Deadline_);
+ }
+
+ if (options.PingAncestors_) {
+ result["ping_ancestor_transactions"] = true;
+ }
+
+ if (options.Attributes_ && !options.Attributes_->IsMap()) {
+ ythrow TApiUsageError() << "Attributes must be a Map node";
+ }
+
+ auto attributes = options.Attributes_.GetOrElse(TNode::CreateMap());
+ if (options.Title_) {
+ attributes["title"] = *options.Title_;
+ } else if (!attributes.HasKey("title")) {
+ attributes["title"] = GetDefaultTransactionTitle();
+ }
+ result["attributes"] = attributes;
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail::NRawClient
diff --git a/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h b/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h
new file mode 100644
index 0000000000..a60e3ea369
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/rpc_parameters_serialization.h
@@ -0,0 +1,231 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/fwd.h>
+#include <yt/cpp/mapreduce/interface/client_method_options.h>
+
+namespace NYT::NDetail::NRawClient {
+
+////////////////////////////////////////////////////////////////////
+
+TNode SerializeParamsForCreate(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ ENodeType type,
+ const TCreateOptions& options);
+
+TNode SerializeParamsForRemove(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TRemoveOptions& options);
+
+TNode SerializeParamsForExists(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TExistsOptions& options);
+
+TNode SerializeParamsForGet(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TGetOptions& options);
+
+TNode SerializeParamsForSet(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TSetOptions& options);
+
+TNode SerializeParamsForMultisetAttributes(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TMultisetAttributesOptions& options);
+
+TNode SerializeParamsForList(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TListOptions& options);
+
+TNode SerializeParamsForCopy(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TCopyOptions& options);
+
+TNode SerializeParamsForMove(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& sourcePath,
+ const TYPath& destinationPath,
+ const TMoveOptions& options);
+
+TNode SerializeParamsForLink(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& targetPath,
+ const TYPath& linkPath,
+ const TLinkOptions& options);
+
+TNode SerializeParamsForLock(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ ELockMode mode,
+ const TLockOptions& options);
+
+TNode SerializeParamsForUnlock(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TUnlockOptions& options);
+
+TNode SerializeParamsForConcatenate(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TVector<TRichYPath>& sourcePaths,
+ const TRichYPath& destinationPath,
+ const TConcatenateOptions& options);
+
+TNode SerializeParamsForPingTx(
+ const TTransactionId& transactionId);
+
+TNode SerializeParamsForGetOperation(
+ const TOperationId& operationId,
+ const TGetOperationOptions& options);
+
+TNode SerializeParamsForAbortOperation(
+ const TOperationId& operationId);
+
+TNode SerializeParamsForCompleteOperation(
+ const TOperationId& operationId);
+
+TNode SerializeParamsForSuspendOperation(
+ const TOperationId& operationId,
+ const TSuspendOperationOptions& options);
+
+TNode SerializeParamsForResumeOperation(
+ const TOperationId& operationId,
+ const TResumeOperationOptions& options);
+
+TNode SerializeParamsForListOperations(
+ const TListOperationsOptions& options);
+
+TNode SerializeParamsForUpdateOperationParameters(
+ const TOperationId& operationId,
+ const TUpdateOperationParametersOptions& options);
+
+TNode SerializeParamsForGetJob(
+ const TOperationId& operationId,
+ const TJobId& jobId,
+ const TGetJobOptions& options);
+
+TNode SerializeParamsForListJobs(
+ const TOperationId& operationId,
+ const TListJobsOptions& options);
+
+TNode SerializeParametersForInsertRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TInsertRowsOptions& options);
+
+TNode SerializeParametersForDeleteRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TDeleteRowsOptions& options);
+
+TNode SerializeParametersForTrimRows(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TTrimRowsOptions& options);
+
+TNode SerializeParamsForParseYPath(
+ const TRichYPath& path);
+
+TNode SerializeParamsForEnableTableReplica(
+ const TReplicaId& replicaId);
+
+TNode SerializeParamsForDisableTableReplica(
+ const TReplicaId& replicaId);
+
+TNode SerializeParamsForAlterTableReplica(
+ const TReplicaId& replicaId,
+ const TAlterTableReplicaOptions& options);
+
+TNode SerializeParamsForFreezeTable(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TFreezeTableOptions& options);
+
+TNode SerializeParamsForUnfreezeTable(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TUnfreezeTableOptions& options);
+
+TNode SerializeParamsForAlterTable(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TAlterTableOptions& options);
+
+TNode SerializeParamsForGetTableColumnarStatistics(
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTableColumnarStatisticsOptions& options);
+
+TNode SerializeParamsForGetTablePartitions(
+ const TTransactionId& transactionId,
+ const TVector<TRichYPath>& paths,
+ const TGetTablePartitionsOptions& options);
+
+TNode SerializeParamsForGetFileFromCache(
+ const TTransactionId& transactionId,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TGetFileFromCacheOptions&);
+
+TNode SerializeParamsForPutFileToCache(
+ const TTransactionId& transactionId,
+ const TString& pathPrefix,
+ const TYPath& filePath,
+ const TString& md5Signature,
+ const TYPath& cachePath,
+ const TPutFileToCacheOptions& options);
+
+TNode SerializeParamsForSkyShareTable(
+ const TString& serverName,
+ const TString& pathPrefix,
+ const std::vector<TYPath>& tablePaths,
+ const TSkyShareTableOptions& options);
+
+TNode SerializeParamsForCheckPermission(
+ const TString& user,
+ EPermission permission,
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TCheckPermissionOptions& options);
+
+TNode SerializeParamsForGetTabletInfos(
+ const TString& pathPrefix,
+ const TYPath& path,
+ const TVector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options);
+
+TNode SerializeParamsForAbortTransaction(
+ const TTransactionId& transactionId);
+
+TNode SerializeParamsForCommitTransaction(
+ const TTransactionId& transactionId);
+
+TNode SerializeParamsForStartTransaction(
+ const TTransactionId& parentTransactionId,
+ TDuration txTimeout,
+ const TStartTransactionOptions& options);
+
+////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail::NRawClient
diff --git a/yt/cpp/mapreduce/raw_client/ya.make b/yt/cpp/mapreduce/raw_client/ya.make
new file mode 100644
index 0000000000..0d03aae80c
--- /dev/null
+++ b/yt/cpp/mapreduce/raw_client/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ raw_batch_request.cpp
+ raw_requests.cpp
+ rpc_parameters_serialization.cpp
+)
+
+PEERDIR(
+ yt/cpp/mapreduce/common
+ yt/cpp/mapreduce/http
+ yt/cpp/mapreduce/interface
+ yt/cpp/mapreduce/interface/logging
+ library/cpp/yson/node
+)
+
+END()
diff --git a/yt/cpp/mapreduce/skiff/skiff_schema.h b/yt/cpp/mapreduce/skiff/skiff_schema.h
new file mode 100644
index 0000000000..e8c97de8e8
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/skiff_schema.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include <library/cpp/skiff/skiff_schema.h>
diff --git a/yt/cpp/mapreduce/skiff/unchecked_parser.h b/yt/cpp/mapreduce/skiff/unchecked_parser.h
new file mode 100644
index 0000000000..8fd9f90b0b
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/unchecked_parser.h
@@ -0,0 +1 @@
+#include <library/cpp/skiff/skiff.h>
diff --git a/yt/cpp/mapreduce/skiff/wire_type.h b/yt/cpp/mapreduce/skiff/wire_type.h
new file mode 100644
index 0000000000..96d19c06d3
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/wire_type.h
@@ -0,0 +1 @@
+#include <library/cpp/skiff/public.h>
diff --git a/yt/cpp/mapreduce/skiff/ya.make b/yt/cpp/mapreduce/skiff/ya.make
new file mode 100644
index 0000000000..95d91ecd47
--- /dev/null
+++ b/yt/cpp/mapreduce/skiff/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/skiff
+)
+
+END()
diff --git a/yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h b/yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h
new file mode 100644
index 0000000000..37d9d501cd
--- /dev/null
+++ b/yt/cpp/mapreduce/tests/yt_unittest_lib/yt_unittest_lib.h
@@ -0,0 +1,194 @@
+#pragma once
+
+#include <yt/cpp/mapreduce/interface/logging/logger.h>
+#include <yt/cpp/mapreduce/interface/client.h>
+
+#include <yt/cpp/mapreduce/interface/config.h>
+
+#include <library/cpp/yson/node/node_io.h>
+
+#include <util/generic/bt_exception.h>
+
+#include <util/datetime/base.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<>
+void Out<NYT::TNode>(IOutputStream& s, const NYT::TNode& node);
+
+template<>
+void Out<TGUID>(IOutputStream& s, const TGUID& guid);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT {
+namespace NTesting {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IClientPtr CreateTestClient(TString proxy = "", const TCreateClientOptions& options = {});
+
+// Create map node by unique path in Cypress and return that path.
+TYPath CreateTestDirectory(const IClientBasePtr& client);
+
+TString GenerateRandomData(size_t size, ui64 seed = 42);
+
+TVector<TNode> ReadTable(const IClientBasePtr& client, const TString& tablePath);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO: should be removed, usages should be replaced with TConfigSaverGuard
+class TZeroWaitLockPollIntervalGuard
+{
+public:
+ TZeroWaitLockPollIntervalGuard();
+
+ ~TZeroWaitLockPollIntervalGuard();
+
+private:
+ TDuration OldWaitLockPollInterval_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConfigSaverGuard
+{
+public:
+ TConfigSaverGuard();
+ ~TConfigSaverGuard();
+
+private:
+ TConfig Config_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDebugMetricDiff
+{
+public:
+ TDebugMetricDiff(TString name);
+ ui64 GetTotal() const;
+
+private:
+ TString Name_;
+ ui64 InitialValue_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TOwningYaMRRow
+{
+ TString Key;
+ TString SubKey;
+ TString Value;
+
+ TOwningYaMRRow(const TYaMRRow& row = {});
+ TOwningYaMRRow(TString key, TString subKey, TString value);
+
+ operator TYaMRRow() const;
+};
+
+bool operator == (const TOwningYaMRRow& row1, const TOwningYaMRRow& row2);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestFixture
+{
+public:
+ explicit TTestFixture(const TCreateClientOptions& options = {});
+ ~TTestFixture();
+
+ // Return precreated client.
+ IClientPtr GetClient() const;
+
+ // Return newly created client. Useful for cases:
+ // - when we want to have multiple clients objects;
+ // - when we want to control to control destruction of client object;
+ IClientPtr CreateClient(const TCreateClientOptions& options = {}) const;
+
+ IClientPtr CreateClientForUser(const TString& user, TCreateClientOptions options = {});
+
+ TYPath GetWorkingDir() const;
+
+private:
+ TConfigSaverGuard ConfigGuard_;
+ IClientPtr Client_;
+ TYPath WorkingDir_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTabletFixture
+ : public TTestFixture
+{
+public:
+ TTabletFixture();
+
+private:
+ void WaitForTabletCell();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Compares only columns and only "name" and "type" fields of columns.
+bool AreSchemasEqual(const TTableSchema& lhs, const TTableSchema& rhs);
+
+class TWaitFailedException
+ : public TWithBackTrace<yexception>
+{ };
+
+void WaitForPredicate(const std::function<bool()>& predicate, TDuration timeout = TDuration::Seconds(60));
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Redirects all the LOG_* calls with the corresponding level to `stream`.
+// Moreover, the LOG_* calls are delegated to `oldLogger`.
+class TStreamTeeLogger
+ : public ILogger
+{
+public:
+ TStreamTeeLogger(ELevel cutLevel, IOutputStream* stream, ILoggerPtr oldLogger);
+ void Log(ELevel level, const ::TSourceLocation& sourceLocation, const char* format, va_list args) override;
+
+private:
+ ILoggerPtr OldLogger_;
+ IOutputStream* Stream_;
+ ELevel Level_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+TString ToYson(const T& x)
+{
+ TNode result;
+ TNodeBuilder builder(&result);
+ Serialize(x, &builder);
+ return NodeToYsonString(result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NTesting
+} // namespace NYT
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+void Out<NYT::NTesting::TOwningYaMRRow>(IOutputStream& out, const NYT::NTesting::TOwningYaMRRow& row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// for UNITTEST()
+#define ASSERT_SERIALIZABLES_EQUAL(a, b) \
+ UNIT_ASSERT_EQUAL_C(a, b, NYT::NTesting::ToYson(a) << " != " << NYT::NTesting::ToYson(b))
+
+#define ASSERT_SERIALIZABLES_UNEQUAL(a, b) \
+ UNIT_ASSERT_UNEQUAL_C(a, b, NYT::NTesting::ToYson(a) << " == " << NYT::NTesting::ToYson(b))
+
+// for GTEST()
+#define ASSERT_SERIALIZABLES_EQ(a, b) \
+ ASSERT_EQ(a, b) << NYT::NTesting::ToYson(a) << " != " << NYT::NTesting::ToYson(b)
+
+#define ASSERT_SERIALIZABLES_NE(a, b) \
+ ASSERT_NE(a, b) << NYT::NTesting::ToYson(a) << " == " << NYT::NTesting::ToYson(b)
diff --git a/yt/yt/client/api/client.cpp b/yt/yt/client/api/client.cpp
new file mode 100644
index 0000000000..f5965b0311
--- /dev/null
+++ b/yt/yt/client/api/client.cpp
@@ -0,0 +1,573 @@
+#include "client.h"
+#include "transaction.h"
+#include "private.h"
+
+#include <yt/yt/client/job_tracker_client/helpers.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <contrib/libs/pfr/include/pfr/tuple_size.hpp>
+
+namespace NYT::NApi {
+
+using namespace NConcurrency;
+using namespace NThreading;
+using namespace NYTree;
+using namespace NJobTrackerClient;
+
+static const auto& Logger = ApiLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+EWorkloadCategory FromUserWorkloadCategory(EUserWorkloadCategory category)
+{
+ switch (category) {
+ case EUserWorkloadCategory::Realtime:
+ return EWorkloadCategory::UserRealtime;
+ case EUserWorkloadCategory::Interactive:
+ return EWorkloadCategory::UserInteractive;
+ case EUserWorkloadCategory::Batch:
+ return EWorkloadCategory::UserBatch;
+ default:
+ YT_ABORT();
+ }
+}
+
+} // namespace
+
+TUserWorkloadDescriptor::operator TWorkloadDescriptor() const
+{
+ TWorkloadDescriptor result;
+ result.Category = FromUserWorkloadCategory(Category);
+ result.Band = Band;
+ return result;
+}
+
+struct TSerializableUserWorkloadDescriptor
+ : public TYsonStructLite
+ , public TUserWorkloadDescriptor
+{
+ REGISTER_YSON_STRUCT_LITE(TSerializableUserWorkloadDescriptor);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.BaseClassParameter("category", &TThis::Category);
+ registrar.BaseClassParameter("band", &TThis::Band)
+ .Optional();
+ }
+
+public:
+ static TThis Wrap(const TUserWorkloadDescriptor& source)
+ {
+ TThis result = Create();
+ result.Band = source.Band;
+ result.Category = source.Category;
+ return result;
+ }
+
+ TUserWorkloadDescriptor Unwrap()
+ {
+ TUserWorkloadDescriptor result;
+ result.Band = Band;
+ result.Category = Category;
+ return result;
+ }
+};
+
+void Serialize(const TUserWorkloadDescriptor& workloadDescriptor, NYson::IYsonConsumer* consumer)
+{
+ NYTree::Serialize(TSerializableUserWorkloadDescriptor::Wrap(workloadDescriptor), consumer);
+}
+
+void Deserialize(TUserWorkloadDescriptor& workloadDescriptor, INodePtr node)
+{
+ auto serializableWorkloadDescriptor = TSerializableUserWorkloadDescriptor::Create();
+ NYTree::Deserialize(serializableWorkloadDescriptor, node);
+ workloadDescriptor = serializableWorkloadDescriptor.Unwrap();
+}
+
+void Deserialize(TUserWorkloadDescriptor& workloadDescriptor, NYson::TYsonPullParserCursor* cursor)
+{
+ auto serializableWorkloadDescriptor = TSerializableUserWorkloadDescriptor::Create();
+ NYTree::Deserialize(serializableWorkloadDescriptor, cursor);
+ workloadDescriptor = serializableWorkloadDescriptor.Unwrap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+NRpc::TMutationId TMutatingOptions::GetOrGenerateMutationId() const
+{
+ if (Retry && !MutationId) {
+ THROW_ERROR_EXCEPTION("Cannot execute retry without mutation id");
+ }
+ return MutationId ? MutationId : NRpc::GenerateMutationId();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJournalWriterPerformanceCounters::TJournalWriterPerformanceCounters(const NProfiling::TProfiler& profiler)
+{
+#define XX(name) \
+ name ## Timer = profiler.Timer("/" + CamelCaseToUnderscoreCase(#name) + "_time");
+
+ XX(GetBasicAttributes)
+ XX(BeginUpload)
+ XX(GetExtendedAttributes)
+ XX(GetUploadParameters)
+ XX(EndUpload)
+ XX(OpenSession)
+ XX(CreateChunk)
+ XX(AllocateWriteTargets)
+ XX(StartNodeSession)
+ XX(ConfirmChunk)
+ XX(AttachChunk)
+ XX(SealChunk)
+
+#undef XX
+
+ WriteQuorumLag = profiler.Timer("/write_quorum_lag");
+ MaxReplicaLag = profiler.Timer("/max_replica_lag");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TError TCheckPermissionResult::ToError(
+ const TString& user,
+ EPermission permission,
+ const std::optional<TString>& column) const
+{
+ switch (Action) {
+ case NSecurityClient::ESecurityAction::Allow:
+ return TError();
+
+ case NSecurityClient::ESecurityAction::Deny: {
+ TError error;
+ if (ObjectName && SubjectName) {
+ error = TError(
+ NSecurityClient::EErrorCode::AuthorizationError,
+ "Access denied for user %Qv: %Qlv permission is denied for %Qv by ACE at %v",
+ user,
+ permission,
+ *SubjectName,
+ *ObjectName);
+ } else {
+ error = TError(
+ NSecurityClient::EErrorCode::AuthorizationError,
+ "Access denied for user %Qv: %Qlv permission is not allowed by any matching ACE",
+ user,
+ permission);
+ }
+ error.MutableAttributes()->Set("user", user);
+ error.MutableAttributes()->Set("permission", permission);
+ if (ObjectId) {
+ error.MutableAttributes()->Set("denied_by", ObjectId);
+ }
+ if (SubjectId) {
+ error.MutableAttributes()->Set("denied_for", SubjectId);
+ }
+ if (column) {
+ error.MutableAttributes()->Set("column", *column);
+ }
+ return error;
+ }
+
+ default:
+ YT_ABORT();
+ }
+}
+
+TError TCheckPermissionByAclResult::ToError(const TString &user, EPermission permission) const
+{
+ switch (Action) {
+ case NSecurityClient::ESecurityAction::Allow:
+ return TError();
+
+ case NSecurityClient::ESecurityAction::Deny: {
+ TError error;
+ if (SubjectName) {
+ error = TError(
+ NSecurityClient::EErrorCode::AuthorizationError,
+ "Access denied for user %Qv: %Qlv permission is denied for %Qv by ACL",
+ user,
+ permission,
+ *SubjectName);
+ } else {
+ error = TError(
+ NSecurityClient::EErrorCode::AuthorizationError,
+ "Access denied for user %Qv: %Qlv permission is not allowed by any matching ACE",
+ user,
+ permission);
+ }
+ error.MutableAttributes()->Set("user", user);
+ error.MutableAttributes()->Set("permission", permission);
+ if (SubjectId) {
+ error.MutableAttributes()->Set("denied_for", SubjectId);
+ }
+ return error;
+ }
+
+ default:
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TMultiTablePartition& partition, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("table_ranges").Value(partition.TableRanges)
+ .Item("aggregate_statistics").Value(partition.AggregateStatistics)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TMultiTablePartitions& partitions, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("partitions").Value(partitions.Partitions)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(
+ const TOperation& operation,
+ NYson::IYsonConsumer* consumer,
+ bool needType,
+ bool needOperationType,
+ bool idWithAttributes)
+{
+ auto fillItems = [&] (TFluentMap fluent) {
+ fluent
+ .OptionalItem("id", operation.Id)
+ .OptionalItem("state", operation.State)
+ .DoIf(operation.Type.operator bool(), [&] (TFluentMap fluent) {
+ if (needType) {
+ fluent.Item("type").Value(operation.Type);
+ }
+ if (needOperationType) {
+ fluent.Item("operation_type").Value(operation.Type);
+ }
+ })
+ .OptionalItem("authenticated_user", operation.AuthenticatedUser)
+ .OptionalItem("start_time", operation.StartTime)
+ .OptionalItem("finish_time", operation.FinishTime)
+ .OptionalItem("brief_progress", operation.BriefProgress)
+ .OptionalItem("progress", operation.Progress)
+ .OptionalItem("brief_spec", operation.BriefSpec)
+ .OptionalItem("full_spec", operation.FullSpec)
+ .OptionalItem("spec", operation.Spec)
+ .OptionalItem("provided_spec", operation.ProvidedSpec)
+ .OptionalItem("experiment_assignments", operation.ExperimentAssignments)
+ .OptionalItem("experiment_assignment_names", operation.ExperimentAssignmentNames)
+ .OptionalItem("unrecognized_spec", operation.UnrecognizedSpec)
+ .OptionalItem("runtime_parameters", operation.RuntimeParameters)
+ .OptionalItem("suspended", operation.Suspended)
+ .OptionalItem("result", operation.Result)
+ .OptionalItem("events", operation.Events)
+ .OptionalItem("slot_index_per_pool_tree", operation.SlotIndexPerPoolTree)
+ .OptionalItem("alerts", operation.Alerts)
+ .OptionalItem("alert_events", operation.AlertEvents)
+ .OptionalItem("task_names", operation.TaskNames)
+ .OptionalItem("controller_features", operation.ControllerFeatures)
+ .DoIf(operation.OtherAttributes.operator bool(), [&] (TFluentMap fluent) {
+ for (const auto& [key, value] : operation.OtherAttributes->ListPairs()) {
+ fluent.Item(key).Value(value);
+ }
+ });
+ };
+
+ if (idWithAttributes) {
+ if (!operation.Id) {
+ THROW_ERROR_EXCEPTION(
+ "Cannot serialize operation in id-with-attributes format "
+ "as \"id\" attribute is missing from attribute filter");
+ }
+ BuildYsonFluently(consumer)
+ .BeginAttributes()
+ .Do(fillItems)
+ .EndAttributes()
+ .Value(*operation.Id);
+ } else {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Do(fillItems)
+ .EndMap();
+ }
+}
+
+void Deserialize(TOperation& operation, NYTree::IAttributeDictionaryPtr attributes, bool clone)
+{
+ if (clone) {
+ attributes = attributes->Clone();
+ }
+
+ auto setField = [&] (auto& field, const TString& name) {
+ using T = std::remove_reference_t<decltype(field)>;
+ if constexpr (std::is_same_v<T, NYson::TYsonString>) {
+ if (auto value = attributes->FindYson(name)) {
+ field = std::move(value);
+ attributes->Remove(name);
+ } else {
+ field = {};
+ }
+ } else {
+ using TValue = typename TOptionalTraits<T>::TValue;
+ if (auto value = attributes->FindAndRemove<TValue>(name)) {
+ field = std::move(value);
+ } else {
+ field.reset();
+ }
+ }
+ };
+
+ setField(operation.Id, "id");
+ setField(operation.Type, "type");
+ setField(operation.State, "state");
+ setField(operation.StartTime, "start_time");
+ setField(operation.FinishTime, "finish_time");
+ setField(operation.AuthenticatedUser, "authenticated_user");
+ setField(operation.BriefSpec, "brief_spec");
+ setField(operation.Spec, "spec");
+ setField(operation.ProvidedSpec, "provided_spec");
+ setField(operation.ExperimentAssignments, "experiment_assignments");
+ setField(operation.ExperimentAssignmentNames, "experiment_assignment_names");
+ setField(operation.FullSpec, "full_spec");
+ setField(operation.UnrecognizedSpec, "unrecognized_spec");
+ setField(operation.BriefProgress, "brief_progress");
+ setField(operation.Progress, "progress");
+ setField(operation.RuntimeParameters, "runtime_parameters");
+ setField(operation.Suspended, "suspended");
+ setField(operation.Events, "events");
+ setField(operation.Result, "result");
+ setField(operation.SlotIndexPerPoolTree, "slot_index_per_pool_tree");
+ setField(operation.Alerts, "alerts");
+ setField(operation.AlertEvents, "alert_events");
+ setField(operation.TaskNames, "task_names");
+ setField(operation.ControllerFeatures, "controller_features");
+
+ operation.OtherAttributes = std::move(attributes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TQuery& query, NYson::IYsonConsumer* consumer)
+{
+ static_assert(pfr::tuple_size<TQuery>::value == 13);
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .OptionalItem("id", query.Id)
+ .OptionalItem("engine", query.Engine)
+ .OptionalItem("query", query.Query)
+ .OptionalItem("start_time", query.StartTime)
+ .OptionalItem("finish_time", query.FinishTime)
+ .OptionalItem("settings", query.Settings)
+ .OptionalItem("user", query.User)
+ .OptionalItem("state", query.State)
+ .OptionalItem("result_count", query.ResultCount)
+ .OptionalItem("progress", query.Progress)
+ .OptionalItem("annotations", query.Annotations)
+ .OptionalItem("error", query.Error)
+ .DoIf(static_cast<bool>(query.OtherAttributes), [&] (TFluentMap fluent) {
+ for (const auto& [key, value] : query.OtherAttributes->ListPairs()) {
+ fluent.Item(key).Value(value);
+ }
+ })
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TQueryResult& queryResult, NYson::IYsonConsumer* consumer)
+{
+ static_assert(pfr::tuple_size<TQueryResult>::value == 5);
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("id").Value(queryResult.Id)
+ .Item("result_index").Value(queryResult.ResultIndex)
+ .DoIf(!queryResult.Error.IsOK(), [&] (TFluentMap fluent) {
+ fluent
+ .Item("error").Value(queryResult.Error);
+ })
+ .OptionalItem("schema", queryResult.Schema)
+ .Item("data_statistics").Value(queryResult.DataStatistics)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<EJobState> TJob::GetState() const
+{
+ if (ArchiveState && ControllerState) {
+ if (IsJobInProgress(*ArchiveState)) {
+ return ControllerState;
+ } else {
+ return ArchiveState;
+ }
+ } else if (ArchiveState) {
+ return ArchiveState;
+ } else if (ControllerState) {
+ return ControllerState;
+ }
+ return std::nullopt;
+}
+
+// Tries to find "abort_reason" attribute in the error and parse it as |EAbortReason|.
+// Returns |std::nullopt| if the attribute is not found or any of two parsings is unsuccessful.
+static std::optional<NScheduler::EAbortReason> TryGetJobAbortReasonFromError(const NYson::TYsonString& errorYson)
+{
+ if (!errorYson) {
+ return std::nullopt;
+ }
+
+ TError error;
+ try {
+ error = ConvertTo<TError>(errorYson);
+ } catch (const std::exception& exception) {
+ return std::nullopt;
+ }
+
+ if (auto yson = error.Attributes().FindYson("abort_reason")) {
+ try {
+ return ConvertTo<NScheduler::EAbortReason>(yson);
+ } catch (const std::exception& exception) {
+ return std::nullopt;
+ }
+ }
+
+ return std::nullopt;
+}
+
+void Serialize(const TJob& job, NYson::IYsonConsumer* consumer, TStringBuf idKey)
+{
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .OptionalItem(idKey, job.Id)
+ .OptionalItem("operation_id", job.OperationId)
+ .OptionalItem("type", job.Type)
+ .OptionalItem("state", job.GetState())
+ .OptionalItem("controller_state", job.ControllerState)
+ .OptionalItem("archive_state", job.ArchiveState)
+ .OptionalItem("address", job.Address)
+ .OptionalItem("start_time", job.StartTime)
+ .OptionalItem("finish_time", job.FinishTime)
+ .OptionalItem("has_spec", job.HasSpec)
+ .OptionalItem("job_competition_id", job.JobCompetitionId)
+ .OptionalItem("probing_job_competition_id", job.ProbingJobCompetitionId)
+ .OptionalItem("has_competitors", job.HasCompetitors)
+ .OptionalItem("has_probing_competitors", job.HasProbingCompetitors)
+ .OptionalItem("progress", job.Progress)
+ .OptionalItem("stderr_size", job.StderrSize)
+ .OptionalItem("fail_context_size", job.FailContextSize)
+ .OptionalItem("error", job.Error)
+ .OptionalItem("abort_reason", TryGetJobAbortReasonFromError(job.Error))
+ .OptionalItem("brief_statistics", job.BriefStatistics)
+ .OptionalItem("input_paths", job.InputPaths)
+ .OptionalItem("core_infos", job.CoreInfos)
+ .OptionalItem("events", job.Events)
+ .OptionalItem("statistics", job.Statistics)
+ .OptionalItem("exec_attributes", job.ExecAttributes)
+ .OptionalItem("task_name", job.TaskName)
+ .OptionalItem("pool_tree", job.PoolTree)
+ .OptionalItem("pool", job.Pool)
+ .OptionalItem("monitoring_descriptor", job.MonitoringDescriptor)
+ .OptionalItem("is_stale", job.IsStale)
+ .OptionalItem("job_cookie", job.JobCookie)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<ITransactionPtr> StartAlienTransaction(
+ const ITransactionPtr& localTransaction,
+ const IClientPtr& alienClient,
+ const TAlienTransactionStartOptions& options)
+{
+ YT_VERIFY(localTransaction->GetType() == NTransactionClient::ETransactionType::Tablet);
+
+ if (localTransaction->GetConnection()->IsSameCluster(alienClient->GetConnection())) {
+ return MakeFuture(localTransaction);
+ }
+
+ return alienClient->StartTransaction(
+ NTransactionClient::ETransactionType::Tablet,
+ TTransactionStartOptions{
+ .Id = localTransaction->GetId(),
+ .Atomicity = options.Atomicity,
+ .Durability = options.Durability,
+ .StartTimestamp = options.StartTimestamp
+ }).Apply(BIND([=] (const ITransactionPtr& alienTransaction) {
+ localTransaction->RegisterAlienTransaction(alienTransaction);
+ return alienTransaction;
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TCheckClusterLivenessOptions::IsCheckTrivial() const
+{
+ return !CheckCypressRoot && !CheckSecondaryMasterCells && !CheckTabletCellBundle;
+}
+
+bool TCheckClusterLivenessOptions::operator==(const TCheckClusterLivenessOptions& other) const
+{
+ return
+ CheckCypressRoot == other.CheckCypressRoot &&
+ CheckSecondaryMasterCells == other.CheckSecondaryMasterCells &&
+ CheckTabletCellBundle == other.CheckTabletCellBundle;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: After the cluster name is actually set, the value never changes. Thus, it is safe to return TStringBuf.
+std::optional<TStringBuf> TClusterAwareClientBase::GetClusterName(bool fetchIfNull)
+{
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ if (ClusterName_) {
+ return ClusterName_;
+ }
+ }
+
+ auto clusterName = GetConnection()->GetClusterName();
+ if (fetchIfNull && !clusterName) {
+ clusterName = FetchClusterNameFromMasterCache();
+ }
+
+ if (!clusterName) {
+ return {};
+ }
+
+ auto guard = WriterGuard(SpinLock_);
+ if (!ClusterName_) {
+ ClusterName_ = clusterName;
+ }
+
+ return ClusterName_;
+}
+
+std::optional<TString> TClusterAwareClientBase::FetchClusterNameFromMasterCache()
+{
+ TGetNodeOptions options;
+ options.ReadFrom = EMasterChannelKind::MasterCache;
+ auto clusterNameYsonOrError = WaitFor(GetNode(ClusterNamePath, options));
+ if (!clusterNameYsonOrError.IsOK()) {
+ YT_LOG_WARNING(clusterNameYsonOrError, "Could not fetch cluster name from from master cache (Path: %v)",
+ ClusterNamePath);
+ return {};
+ }
+ return ConvertTo<TString>(clusterNameYsonOrError.Value());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/client.h b/yt/yt/client/api/client.h
new file mode 100644
index 0000000000..04e30bd072
--- /dev/null
+++ b/yt/yt/client/api/client.h
@@ -0,0 +1,2543 @@
+#pragma once
+
+#include "public.h"
+#include "connection.h"
+
+#include <yt/yt/client/cypress_client/public.h>
+
+#include <yt/yt/client/chaos_client/replication_card.h>
+
+#include <yt/yt/client/job_tracker_client/public.h>
+
+#include <yt/yt/client/journal_client/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/client/query_client/query_statistics.h>
+
+#include <yt/yt/client/query_tracker_client/public.h>
+
+#include <yt/yt/client/scheduler/operation_id_or_alias.h>
+
+#include <yt/yt/client/security_client/public.h>
+
+#include <yt/yt/client/node_tracker_client/public.h>
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt/client/table_client/config.h>
+#include <yt/yt/client/table_client/chunk_stripe_statistics.h>
+#include <yt/yt/client/table_client/row_base.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/queue_client/queue_rowset.h>
+
+#include <yt/yt/client/table_client/columnar_statistics.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/client/chunk_client/config.h>
+#include <yt/yt/client/chunk_client/data_statistics.h>
+
+#include <yt/yt/client/hive/timestamp_map.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/client/driver/private.h>
+
+#include <yt/yt/client/ypath/public.h>
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/ytree/permission.h>
+#include <yt/yt/core/ytree/attribute_filter.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/library/erasure/public.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TUserWorkloadDescriptor
+{
+ EUserWorkloadCategory Category = EUserWorkloadCategory::Interactive;
+ int Band = 0;
+
+ operator TWorkloadDescriptor() const;
+};
+
+void Serialize(const TUserWorkloadDescriptor& workloadDescriptor, NYson::IYsonConsumer* consumer);
+void Deserialize(TUserWorkloadDescriptor& workloadDescriptor, NYTree::INodePtr node);
+void Deserialize(TUserWorkloadDescriptor& workloadDescriptor, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTimeoutOptions
+{
+ std::optional<TDuration> Timeout;
+};
+
+struct TDetailedProfilingInfo
+ : public TRefCounted
+{
+ bool EnableDetailedTableProfiling = false;
+ NYPath::TYPath TablePath;
+ TDuration MountCacheWaitTime;
+
+ int WastedSubrequestCount = 0;
+
+ std::vector<TErrorCode> RetryReasons;
+};
+
+DEFINE_REFCOUNTED_TYPE(TDetailedProfilingInfo)
+
+struct TMultiplexingBandOptions
+{
+ NRpc::EMultiplexingBand MultiplexingBand = NRpc::EMultiplexingBand::Default;
+};
+
+struct TTabletRangeOptions
+{
+ std::optional<int> FirstTabletIndex;
+ std::optional<int> LastTabletIndex;
+};
+
+struct TTransactionalOptions
+{
+ //! Ignored when queried via transaction.
+ NObjectClient::TTransactionId TransactionId;
+ bool Ping = false;
+ bool PingAncestors = false;
+ //! For internal use only.
+ //! Setting it to |true| may result in loss of consistency.
+ bool SuppressTransactionCoordinatorSync = false;
+ //! For internal use only.
+ //! Setting it to |true| may result in loss of consistency .
+ bool SuppressUpstreamSync = false;
+};
+
+struct TSuppressableAccessTrackingOptions
+{
+ bool SuppressAccessTracking = false;
+ bool SuppressModificationTracking = false;
+ bool SuppressExpirationTimeoutRenewal = false;
+};
+
+struct TMutatingOptions
+{
+ NRpc::TMutationId MutationId;
+ bool Retry = false;
+
+ NRpc::TMutationId GetOrGenerateMutationId() const;
+};
+
+struct TMasterReadOptions
+{
+ EMasterChannelKind ReadFrom = EMasterChannelKind::Follower;
+ bool DisablePerUserCache = false;
+ TDuration ExpireAfterSuccessfulUpdateTime = TDuration::Seconds(15);
+ TDuration ExpireAfterFailedUpdateTime = TDuration::Seconds(15);
+ std::optional<int> CacheStickyGroupSize;
+ bool EnableClientCacheStickiness = false;
+
+ // When staleness bound is non-zero, master cache is allowed to
+ // return successful expired response, with staleness not exceeding the bound.
+ // This allows non-blocking master cache responses, with async on-demand updates.
+ TDuration SuccessStalenessBound;
+};
+
+struct TSerializableMasterReadOptions
+ : public TMasterReadOptions
+ , public NYTree::TYsonStruct
+{
+ REGISTER_YSON_STRUCT(TSerializableMasterReadOptions);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.BaseClassParameter("read_from", &TThis::ReadFrom)
+ .Default(TMasterReadOptions{}.ReadFrom);
+
+ registrar.BaseClassParameter("disable_per_user_cache", &TThis::DisablePerUserCache)
+ .Default(TMasterReadOptions{}.DisablePerUserCache);
+
+ registrar.BaseClassParameter("expire_after_successful_update_time", &TThis::ExpireAfterSuccessfulUpdateTime)
+ .Default(TMasterReadOptions{}.ExpireAfterSuccessfulUpdateTime);
+
+ registrar.BaseClassParameter("expire_after_failed_update_time", &TThis::ExpireAfterFailedUpdateTime)
+ .Default(TMasterReadOptions{}.ExpireAfterFailedUpdateTime);
+
+ registrar.BaseClassParameter("cache_sticky_group_size", &TThis::CacheStickyGroupSize)
+ .Default(TMasterReadOptions{}.CacheStickyGroupSize);
+
+ registrar.BaseClassParameter("enable_client_cache_stickiness", &TThis::EnableClientCacheStickiness)
+ .Default(TMasterReadOptions{}.EnableClientCacheStickiness);
+
+ registrar.BaseClassParameter("success_staleness_bound", &TThis::SuccessStalenessBound)
+ .Default(TMasterReadOptions{}.SuccessStalenessBound);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableMasterReadOptions)
+
+struct TPrerequisiteRevisionConfig
+ : public NYTree::TYsonStruct
+{
+ NYTree::TYPath Path;
+ NHydra::TRevision Revision;
+
+ REGISTER_YSON_STRUCT(TPrerequisiteRevisionConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("path", &TThis::Path);
+ registrar.Parameter("revision", &TThis::Revision);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TPrerequisiteRevisionConfig)
+
+struct TPrerequisiteOptions
+{
+ std::vector<NTransactionClient::TTransactionId> PrerequisiteTransactionIds;
+ std::vector<TPrerequisiteRevisionConfigPtr> PrerequisiteRevisions;
+};
+
+struct TSyncReplicaCacheOptions
+{
+ std::optional<TDuration> CachedSyncReplicasTimeout;
+};
+
+struct TMountTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{
+ NTabletClient::TTabletCellId CellId = NTabletClient::NullTabletCellId;
+ std::vector<NTabletClient::TTabletCellId> TargetCellIds;
+ bool Freeze = false;
+};
+
+struct TUnmountTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{
+ bool Force = false;
+};
+
+struct TRemountTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{ };
+
+struct TFreezeTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{ };
+
+struct TUnfreezeTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{ };
+
+struct TReshardTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{
+ std::optional<bool> Uniform;
+ std::optional<bool> EnableSlicing;
+ std::optional<double> SlicingAccuracy;
+};
+
+struct TReshardTableAutomaticOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTabletRangeOptions
+{
+ bool KeepActions = false;
+};
+
+struct TAlterTableOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TTransactionalOptions
+{
+ std::optional<NTableClient::TTableSchema> Schema;
+ std::optional<NTableClient::TMasterTableSchemaId> SchemaId;
+ std::optional<bool> Dynamic;
+ std::optional<NTabletClient::TTableReplicaId> UpstreamReplicaId;
+ std::optional<NTableClient::ETableSchemaModification> SchemaModification;
+ std::optional<NChaosClient::TReplicationProgress> ReplicationProgress;
+};
+
+struct TTrimTableOptions
+ : public TTimeoutOptions
+{ };
+
+struct TAlterTableReplicaOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{
+ std::optional<bool> Enabled;
+ std::optional<NTabletClient::ETableReplicaMode> Mode;
+ std::optional<bool> PreserveTimestamps;
+ std::optional<NTransactionClient::EAtomicity> Atomicity;
+ std::optional<bool> EnableReplicatedTableTracker;
+};
+
+struct TGetTablePivotKeysOptions
+ : public TTimeoutOptions
+{
+ bool RepresentKeyAsList = false;
+};
+
+struct TGetInSyncReplicasOptions
+ : public TTimeoutOptions
+ , public TSyncReplicaCacheOptions
+{
+ NTransactionClient::TTimestamp Timestamp = NTransactionClient::NullTimestamp;
+};
+
+struct TGetTabletInfosOptions
+ : public TTimeoutOptions
+{
+ bool RequestErrors = false;
+};
+
+struct TTabletInfo
+{
+ struct TTableReplicaInfo
+ {
+ NTabletClient::TTableReplicaId ReplicaId;
+ NTransactionClient::TTimestamp LastReplicationTimestamp;
+ NTabletClient::ETableReplicaMode Mode;
+ i64 CurrentReplicationRowIndex;
+ i64 CommittedReplicationRowIndex;
+ TError ReplicationError;
+ };
+
+ //! Currently only provided for ordered tablets.
+ //! Indicates the total number of rows added to the tablet (including trimmed ones).
+ // TODO(babenko): implement for sorted tablets
+ i64 TotalRowCount = 0;
+
+ //! Only makes sense for ordered tablet.
+ //! Contains the number of front rows that are trimmed and are not guaranteed to be accessible.
+ i64 TrimmedRowCount = 0;
+
+ //! Only makes sense for replicated tablets.
+ //! Contains the number of rows that are yet to be committed.
+ i64 DelayedLocklessRowCount = 0;
+
+ //! Mostly makes sense for ordered tablets.
+ //! Contains the barrier timestamp of the tablet cell containing the tablet, which lags behind the current timestamp.
+ //! It is guaranteed that all transactions with commit timestamp not exceeding the barrier
+ //! are fully committed; e.g. all their added rows are visible (and are included in TTabletInfo::TotalRowCount).
+ NTransactionClient::TTimestamp BarrierTimestamp;
+
+ //! Contains maximum timestamp of committed transactions.
+ NTransactionClient::TTimestamp LastWriteTimestamp;
+
+ //! Only makes sense for replicated tablets.
+ std::optional<std::vector<TTableReplicaInfo>> TableReplicaInfos;
+
+ //! Set if `RequestErrors` is present in command parameters.
+ std::vector<TError> TabletErrors;
+};
+
+struct TGetTabletErrorsOptions
+ : public TTimeoutOptions
+{
+ std::optional<i64> Limit;
+};
+
+struct TGetTabletErrorsResult
+{
+ bool Incomplete = false;
+ THashMap<NTabletClient::TTabletId, std::vector<TError>> TabletErrors;
+ THashMap<NTabletClient::TTableReplicaId, std::vector<TError>> ReplicationErrors;
+};
+
+struct TBalanceTabletCellsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{
+ bool KeepActions = false;
+};
+
+struct TGetReplicationCardOptions
+ : public TTimeoutOptions
+ , public NChaosClient::TReplicationCardFetchOptions
+{
+ bool BypassCache = false;
+};
+
+struct TUpdateChaosTableReplicaProgressOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{
+ NChaosClient::TReplicationProgress Progress;
+};
+
+struct TAlterReplicationCardOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{
+ NTabletClient::TReplicatedTableOptionsPtr ReplicatedTableOptions;
+ std::optional<bool> EnableReplicatedTableTracker;
+ std::optional<NChaosClient::TReplicationCardCollocationId> ReplicationCardCollocationId;
+};
+
+struct TAddMemberOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TRemoveMemberOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TCheckPermissionOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+ , public TTransactionalOptions
+ , public TPrerequisiteOptions
+{
+ std::optional<std::vector<TString>> Columns;
+ std::optional<bool> Vital;
+};
+
+struct TCheckPermissionResult
+{
+ TError ToError(
+ const TString& user,
+ NYTree::EPermission permission,
+ const std::optional<TString>& columns = {}) const;
+
+ NSecurityClient::ESecurityAction Action;
+ NObjectClient::TObjectId ObjectId;
+ std::optional<TString> ObjectName;
+ NSecurityClient::TSubjectId SubjectId;
+ std::optional<TString> SubjectName;
+};
+
+struct TCheckPermissionResponse
+ : public TCheckPermissionResult
+{
+ std::optional<std::vector<TCheckPermissionResult>> Columns;
+};
+
+struct TCheckPermissionByAclOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+ , public TPrerequisiteOptions
+{
+ bool IgnoreMissingSubjects = false;
+};
+
+struct TCheckPermissionByAclResult
+{
+ TError ToError(const TString& user, NYTree::EPermission permission) const;
+
+ NSecurityClient::ESecurityAction Action;
+ NSecurityClient::TSubjectId SubjectId;
+ std::optional<TString> SubjectName;
+ std::vector<TString> MissingSubjects;
+};
+
+struct TTransferAccountResourcesOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TTransferPoolResourcesOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TTransactionStartOptions
+ : public TMutatingOptions
+{
+ std::optional<TDuration> Timeout;
+
+ //! If not null then the transaction must use this externally provided id.
+ //! Only applicable to tablet transactions.
+ NTransactionClient::TTransactionId Id;
+
+ NTransactionClient::TTransactionId ParentId;
+ std::vector<NTransactionClient::TTransactionId> PrerequisiteTransactionIds;
+
+ std::optional<TInstant> Deadline;
+
+ bool AutoAbort = true;
+ bool Sticky = false;
+
+ std::optional<TDuration> PingPeriod;
+ bool Ping = true;
+ bool PingAncestors = true;
+
+ NYTree::IAttributeDictionaryPtr Attributes;
+
+ NTransactionClient::EAtomicity Atomicity = NTransactionClient::EAtomicity::Full;
+ NTransactionClient::EDurability Durability = NTransactionClient::EDurability::Sync;
+
+ //! If not null then the transaction must use this externally provided start timestamp.
+ //! Only applicable to tablet transactions.
+ NTransactionClient::TTimestamp StartTimestamp = NTransactionClient::NullTimestamp;
+
+ //! For master transactions only; disables generating start timestamp.
+ bool SuppressStartTimestampGeneration = false;
+
+ //! Only for master transactions.
+ //! Indicates the master cell the transaction will be initially started at and controlled by
+ //! (chosen automatically by default).
+ NObjectClient::TCellTag CoordinatorMasterCellTag = NObjectClient::InvalidCellTag;
+
+ //! Only for master transactions.
+ //! Indicates the cells the transaction will be replicated to at the start. None by default,
+ //! but usually the transaction will be able to be replicated at a later time on demand.
+ std::optional<NObjectClient::TCellTagList> ReplicateToMasterCellTags;
+
+ //! Only for master transactions.
+ //! By default, all master transactions are Cypress expect for some
+ //! system ones (e.g. store flusher transactions).
+ bool StartCypressTransaction = true;
+};
+
+struct TTransactionAttachOptions
+{
+ bool AutoAbort = false;
+ std::optional<TDuration> PingPeriod;
+ bool Ping = true;
+ bool PingAncestors = false;
+
+ //! If non-empty, assumes that the transaction is sticky and specifies address of the transaction manager.
+ //! Throws if the transaction is not sticky actually.
+ //! Only supported by RPC proxy client for now. Ignored by other clients.
+ TString StickyAddress;
+};
+
+struct TTransactionCommitOptions
+ : public TMutatingOptions
+ , public TPrerequisiteOptions
+ , public TTransactionalOptions
+{
+ //! If not null, then this particular cell will be the coordinator.
+ NObjectClient::TCellId CoordinatorCellId;
+
+ //! If |true| then two-phase-commit protocol is executed regardless of the number of participants.
+ bool Force2PC = false;
+
+ //! Eager: coordinator is committed first, success is reported immediately; the participants are committed afterwards.
+ //! Lazy: all the participants must successfully commit before coordinator commits; only after this success is reported.
+ ETransactionCoordinatorCommitMode CoordinatorCommitMode = ETransactionCoordinatorCommitMode::Eager;
+
+ //! Early: coordinator is prepared first and committed first.
+ //! Late: coordinator is prepared last and after commit timestamp generation; prepare and commit
+ //! are executed in single mutation.
+ ETransactionCoordinatorPrepareMode CoordinatorPrepareMode = ETransactionCoordinatorPrepareMode::Early;
+
+ //! At non-coordinating participants, Transaction Manager will synchronize with
+ //! these cells before running prepare.
+ std::vector<NObjectClient::TCellId> CellIdsToSyncWithBeforePrepare;
+
+ //! If |true| then all participants will use the commit timestamp provided by the coordinator.
+ //! If |false| then the participants will use individual commit timestamps based on their cell tag.
+ bool InheritCommitTimestamp = true;
+
+ //! If |true| then the coordinator will generate a non-null prepare timestamp (which is a lower bound for
+ //! the upcoming commit timestamp) and send it to all the participants.
+ //! If |false| then no prepare timestamp is generated and null value is provided to the participants.
+ //! The latter is useful for async replication that does not involve any local write operations
+ //! and also relies on ETransactionCoordinatorCommitMode::Lazy transactions whose commit may be delayed
+ //! for an arbitrary period of time in case of replica failure.
+ bool GeneratePrepareTimestamp = true;
+
+ //! If non-null then the coordinator will fail the commit if generated commit timestamp
+ //! exceeds |MaxAllowedCommitTimestamp|.
+ NTransactionClient::TTimestamp MaxAllowedCommitTimestamp = NTransactionClient::NullTimestamp;
+
+ //! Cell ids of additional 2PC participants.
+ //! Used to implement cross-cluster commit via RPC proxy.
+ std::vector<NObjectClient::TCellId> AdditionalParticipantCellIds;
+};
+
+struct TTransactionPingOptions
+{
+ bool EnableRetries = false;
+};
+
+struct TTransactionCommitResult
+{
+ //! NullTimestamp for all cases when CommitTimestamps are empty.
+ //! NullTimestamp when the primary cell did not participate in to transaction.
+ NHiveClient::TTimestamp PrimaryCommitTimestamp = NHiveClient::NullTimestamp;
+ //! Empty for non-atomic transactions (timestamps are fake).
+ //! Empty for empty tablet transactions (since the commit is essentially no-op).
+ //! May contain multiple items for cross-cluster commit.
+ NHiveClient::TTimestampMap CommitTimestamps;
+};
+
+struct TTransactionAbortOptions
+ : public TMutatingOptions
+ , public TPrerequisiteOptions
+ , public TTransactionalOptions
+{
+ bool Force = false;
+};
+
+struct TTabletReadOptionsBase
+ : public TSyncReplicaCacheOptions
+{
+ NHydra::EPeerKind ReadFrom = NHydra::EPeerKind::Leader;
+ std::optional<TDuration> RpcHedgingDelay;
+
+ //! Ignored when queried via transaction.
+ NTransactionClient::TTimestamp Timestamp = NTransactionClient::SyncLastCommittedTimestamp;
+
+ NTransactionClient::TTimestamp RetentionTimestamp = NTransactionClient::NullTimestamp;
+
+ EReplicaConsistency ReplicaConsistency = EReplicaConsistency::None;
+};
+
+struct TTabletReadOptions
+ : public TTimeoutOptions
+ , public TTabletReadOptionsBase
+{ };
+
+struct TLookupRequestOptions
+{
+ NTableClient::TColumnFilter ColumnFilter;
+ bool KeepMissingRows = false;
+ bool EnablePartialResult = false;
+ std::optional<bool> UseLookupCache;
+ TDetailedProfilingInfoPtr DetailedProfilingInfo;
+ NTableClient::TTableSchemaPtr FallbackTableSchema;
+ NTabletClient::TTableReplicaId FallbackReplicaId;
+};
+
+struct TLookupRowsOptionsBase
+ : public TTabletReadOptions
+ , public TLookupRequestOptions
+ , public TMultiplexingBandOptions
+{ };
+
+struct TLookupRowsOptions
+ : public TLookupRowsOptionsBase
+{ };
+
+struct TVersionedLookupRowsOptions
+ : public TLookupRowsOptionsBase
+{
+ NTableClient::TRetentionConfigPtr RetentionConfig;
+};
+
+struct TMultiLookupSubrequest
+{
+ NYPath::TYPath Path;
+ NTableClient::TNameTablePtr NameTable;
+ TSharedRange<NTableClient::TLegacyKey> Keys;
+
+ // NB: Other options from TLookupRowsOptions that are absent from TLookupRequestOptions are
+ // common and included in TMultiLookupOptions.
+ TLookupRequestOptions Options;
+};
+
+struct TMultiLookupOptions
+ : public TTimeoutOptions
+ , public TTabletReadOptionsBase
+ , public TMultiplexingBandOptions
+{ };
+
+struct TSelectRowsOptionsBase
+ : public TTabletReadOptions
+ , public TSuppressableAccessTrackingOptions
+{
+ //! Limits range expanding.
+ ui64 RangeExpansionLimit = 200000;
+ //! Limits maximum parallel subqueries.
+ int MaxSubqueries = std::numeric_limits<int>::max();
+ //! Path in Cypress with UDFs.
+ std::optional<TString> UdfRegistryPath;
+ //! If |true| then logging is more verbose.
+ bool VerboseLogging = false;
+};
+
+struct TSelectRowsOptions
+ : public TSelectRowsOptionsBase
+{
+ //! If null then connection defaults are used.
+ std::optional<i64> InputRowLimit;
+ //! If null then connection defaults are used.
+ std::optional<i64> OutputRowLimit;
+ //! Combine independent joins in one.
+ bool UseMultijoin = true;
+ //! Allow queries without any condition on key columns.
+ bool AllowFullScan = true;
+ //! Allow queries with join condition which implies foreign query with IN operator.
+ bool AllowJoinWithoutIndex = false;
+ //! Execution pool.
+ std::optional<TString> ExecutionPool;
+ //! If |true| then incomplete result would lead to a failure.
+ bool FailOnIncompleteResult = true;
+ //! Enables generated code caching.
+ bool EnableCodeCache = true;
+ //! Used to prioritize requests.
+ TUserWorkloadDescriptor WorkloadDescriptor;
+ //! Memory limit per execution node.
+ size_t MemoryLimitPerNode = std::numeric_limits<size_t>::max();
+ //! Info on detailed profiling.
+ TDetailedProfilingInfoPtr DetailedProfilingInfo;
+ //! YSON map with placeholder values for parameterized queries.
+ NYson::TYsonString PlaceholderValues;
+ //! Expected schemas for tables in a query (used for replica fallback in replicated tables).
+ using TExpectedTableSchemas = THashMap<NYPath::TYPath, NTableClient::TTableSchemaPtr>;
+ TExpectedTableSchemas ExpectedTableSchemas;
+};
+
+struct TExplainQueryOptions
+ : public TSelectRowsOptionsBase
+{
+ bool VerboseOutput = false;
+};
+
+struct TPullRowsOptions
+ : public TTabletReadOptions
+{
+ NChaosClient::TReplicaId UpstreamReplicaId;
+ THashMap<NTabletClient::TTabletId, i64> StartReplicationRowIndexes;
+ i64 TabletRowsPerRead = 1000;
+ bool OrderRowsByTimestamp = false;
+
+ NChaosClient::TReplicationProgress ReplicationProgress;
+ NTransactionClient::TTimestamp UpperTimestamp = NTransactionClient::NullTimestamp;
+ NTableClient::TTableSchemaPtr TableSchema;
+};
+
+struct TPullRowsResult
+{
+ THashMap<NTabletClient::TTabletId, i64> EndReplicationRowIndexes;
+ i64 RowCount = 0;
+ i64 DataWeight = 0;
+ NChaosClient::TReplicationProgress ReplicationProgress;
+ ITypeErasedRowsetPtr Rowset;
+ bool Versioned = true;
+};
+
+struct TGetNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMasterReadOptions
+ , public TSuppressableAccessTrackingOptions
+ , public TPrerequisiteOptions
+{
+ // NB(eshcherbin): Used in profiling Orchid.
+ NYTree::IAttributeDictionaryPtr Options;
+ NYTree::TAttributeFilter Attributes;
+ std::optional<i64> MaxSize;
+};
+
+struct TSetNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TSuppressableAccessTrackingOptions
+ , public TPrerequisiteOptions
+{
+ bool Recursive = false;
+ bool Force = false;
+};
+
+struct TMultisetAttributesNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TSuppressableAccessTrackingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TRemoveNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{
+ bool Recursive = true;
+ bool Force = false;
+};
+
+struct TListNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMasterReadOptions
+ , public TSuppressableAccessTrackingOptions
+ , public TPrerequisiteOptions
+{
+ NYTree::TAttributeFilter Attributes;
+ std::optional<i64> MaxSize;
+};
+
+struct TCreateObjectOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{
+ bool IgnoreExisting = false;
+ bool Sync = true;
+ NYTree::IAttributeDictionaryPtr Attributes;
+};
+
+struct TCreateNodeOptions
+ : public TCreateObjectOptions
+ , public TTransactionalOptions
+{
+ bool Recursive = false;
+ bool LockExisting = false;
+ bool Force = false;
+ bool IgnoreTypeMismatch = false;
+};
+
+struct TLockNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{
+ bool Waitable = false;
+ std::optional<TString> ChildKey;
+ std::optional<TString> AttributeKey;
+};
+
+struct TLockNodeResult
+{
+ NCypressClient::TLockId LockId;
+ NCypressClient::TNodeId NodeId;
+ NHydra::TRevision Revision = NHydra::NullRevision;
+};
+
+struct TUnlockNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TCopyNodeOptionsBase
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{
+ bool Recursive = false;
+ bool Force = false;
+ bool PreserveAccount = false;
+ bool PreserveCreationTime = false;
+ bool PreserveModificationTime = false;
+ bool PreserveOwner = false;
+ bool PreserveExpirationTime = false;
+ bool PreserveExpirationTimeout = false;
+ bool PreserveAcl = false;
+ bool PessimisticQuotaCheck = true;
+};
+
+struct TCopyNodeOptions
+ : public TCopyNodeOptionsBase
+{
+ bool IgnoreExisting = false;
+ bool LockExisting = false;
+};
+
+struct TMoveNodeOptions
+ : public TCopyNodeOptionsBase
+{
+ TMoveNodeOptions()
+ {
+ // COMPAT(babenko): YT-11903, consider dropping this override
+ PreserveCreationTime = true;
+ }
+};
+
+struct TLinkNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{
+ //! Attributes of a newly created link node.
+ NYTree::IAttributeDictionaryPtr Attributes;
+ bool Recursive = false;
+ bool IgnoreExisting = false;
+ bool LockExisting = false;
+ bool Force = false;
+};
+
+struct TConcatenateNodesOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+{
+ NChunkClient::TFetcherConfigPtr ChunkMetaFetcherConfig;
+
+ bool UniqualizeChunks = false;
+};
+
+struct TNodeExistsOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+ , public TTransactionalOptions
+ , public TSuppressableAccessTrackingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TExternalizeNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+{ };
+
+struct TInternalizeNodeOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+{ };
+
+struct TFileReaderOptions
+ : public TTransactionalOptions
+ , public TSuppressableAccessTrackingOptions
+{
+ std::optional<i64> Offset;
+ std::optional<i64> Length;
+ TFileReaderConfigPtr Config;
+};
+
+struct TFileWriterOptions
+ : public TTransactionalOptions
+ , public TPrerequisiteOptions
+{
+ bool ComputeMD5 = false;
+ TFileWriterConfigPtr Config;
+};
+
+struct TGetFileFromCacheOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+ , public TTransactionalOptions
+{
+ NYPath::TYPath CachePath;
+};
+
+struct TPutFileToCacheOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+ , public TTransactionalOptions
+{
+ NYPath::TYPath CachePath;
+ bool PreserveExpirationTimeout = false;
+ int RetryCount = 10;
+};
+
+struct TJournalReaderOptions
+ : public TTransactionalOptions
+ , public TSuppressableAccessTrackingOptions
+{
+ std::optional<i64> FirstRowIndex;
+ std::optional<i64> RowCount;
+ TJournalReaderConfigPtr Config;
+};
+
+struct TJournalWriterPerformanceCounters
+{
+ TJournalWriterPerformanceCounters() = default;
+ explicit TJournalWriterPerformanceCounters(const NProfiling::TProfiler& profiler);
+
+ NProfiling::TEventTimer GetBasicAttributesTimer;
+ NProfiling::TEventTimer BeginUploadTimer;
+ NProfiling::TEventTimer GetExtendedAttributesTimer;
+ NProfiling::TEventTimer GetUploadParametersTimer;
+ NProfiling::TEventTimer EndUploadTimer;
+ NProfiling::TEventTimer OpenSessionTimer;
+ NProfiling::TEventTimer CreateChunkTimer;
+ NProfiling::TEventTimer AllocateWriteTargetsTimer;
+ NProfiling::TEventTimer StartNodeSessionTimer;
+ NProfiling::TEventTimer ConfirmChunkTimer;
+ NProfiling::TEventTimer AttachChunkTimer;
+ NProfiling::TEventTimer SealChunkTimer;
+
+ NProfiling::TEventTimer WriteQuorumLag;
+ NProfiling::TEventTimer MaxReplicaLag;
+};
+
+struct TJournalWriterOptions
+ : public TTransactionalOptions
+ , public TPrerequisiteOptions
+{
+ TJournalWriterConfigPtr Config;
+ bool EnableMultiplexing = true;
+ // TODO(babenko): enable by default
+ bool EnableChunkPreallocation = false;
+
+ i64 ReplicaLagLimit = NJournalClient::DefaultReplicaLagLimit;
+
+ TJournalWriterPerformanceCounters Counters;
+};
+
+struct TTruncateJournalOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+ , public TPrerequisiteOptions
+{ };
+
+struct TTableReaderOptions
+ : public TTransactionalOptions
+{
+ bool Unordered = false;
+ bool OmitInaccessibleColumns = false;
+ bool EnableTableIndex = false;
+ bool EnableRowIndex = false;
+ bool EnableRangeIndex = false;
+ bool EnableTabletIndex = false;
+ NTableClient::TTableReaderConfigPtr Config;
+};
+
+struct TTableWriterOptions
+ : public TTransactionalOptions
+{
+ bool ValidateAnyIsValidYson = false;
+
+ NTableClient::TTableWriterConfigPtr Config;
+};
+
+struct TPullQueueOptions
+ : public TSelectRowsOptions
+{
+ // COMPAT(achulkov2): Remove this once we drop support for legacy PullQueue via SelectRows.
+ bool UseNativeTabletNodeApi = true;
+};
+
+struct TPullConsumerOptions
+ : public TPullQueueOptions
+{ };
+
+struct TRegisterQueueConsumerOptions
+ : public TTimeoutOptions
+{
+ std::optional<std::vector<int>> Partitions;
+};
+
+struct TUnregisterQueueConsumerOptions
+ : public TTimeoutOptions
+{ };
+
+struct TListQueueConsumerRegistrationsOptions
+ : public TTimeoutOptions
+{ };
+
+struct TGetColumnarStatisticsOptions
+ : public TTransactionalOptions
+ , public TTimeoutOptions
+{
+ NChunkClient::TFetchChunkSpecConfigPtr FetchChunkSpecConfig;
+ NChunkClient::TFetcherConfigPtr FetcherConfig;
+ NTableClient::EColumnarStatisticsFetcherMode FetcherMode = NTableClient::EColumnarStatisticsFetcherMode::FromNodes;
+ bool EnableEarlyFinish = true;
+};
+
+struct TPartitionTablesOptions
+ : public TTransactionalOptions
+ , public TTimeoutOptions
+{
+ NChunkClient::TFetchChunkSpecConfigPtr FetchChunkSpecConfig;
+ NChunkClient::TFetcherConfigPtr FetcherConfig;
+ NChunkClient::TChunkSliceFetcherConfigPtr ChunkSliceFetcherConfig;
+ NTableClient::ETablePartitionMode PartitionMode = NTableClient::ETablePartitionMode::Unordered;
+ i64 DataWeightPerPartition;
+ std::optional<int> MaxPartitionCount;
+ bool AdjustDataWeightPerPartition = true;
+ bool EnableKeyGuarantee = false;
+};
+
+struct TMultiTablePartition
+{
+ //! Table ranges are indexed by table index.
+ std::vector<NYPath::TRichYPath> TableRanges;
+
+ //! Aggregate statistics of all the table ranges in the partition.
+ NTableClient::TChunkStripeStatistics AggregateStatistics;
+};
+
+void Serialize(const TMultiTablePartition& partitions, NYson::IYsonConsumer* consumer);
+
+struct TMultiTablePartitions
+{
+ std::vector<TMultiTablePartition> Partitions;
+};
+
+void Serialize(const TMultiTablePartitions& partitions, NYson::IYsonConsumer* consumer);
+
+struct TLocateSkynetShareOptions
+ : public TTimeoutOptions
+{
+ NChunkClient::TFetchChunkSpecConfigPtr Config;
+};
+
+struct TStartOperationOptions
+ : public TTimeoutOptions
+ , public TTransactionalOptions
+ , public TMutatingOptions
+{ };
+
+struct TAbortOperationOptions
+ : public TTimeoutOptions
+{
+ std::optional<TString> AbortMessage;
+};
+
+struct TSuspendOperationOptions
+ : public TTimeoutOptions
+{
+ bool AbortRunningJobs = false;
+};
+
+struct TResumeOperationOptions
+ : public TTimeoutOptions
+{ };
+
+struct TCompleteOperationOptions
+ : public TTimeoutOptions
+{ };
+
+struct TUpdateOperationParametersOptions
+ : public TTimeoutOptions
+{ };
+
+struct TDumpJobContextOptions
+ : public TTimeoutOptions
+{ };
+
+//! Source to fetch job spec from. Useful in tests.
+DEFINE_BIT_ENUM_WITH_UNDERLYING_TYPE(EJobSpecSource, ui16,
+ //! Job spec is fetched from exec node.
+ ((Node) (1))
+
+ //! Job spec is fetched from job archive.
+ ((Archive) (2))
+
+ //! Job spec is fetched from any available source.
+ ((Auto) (0xFFFF))
+);
+
+struct TGetJobInputOptions
+ : public TTimeoutOptions
+{
+ //! Where job spec should be retrieved from.
+ EJobSpecSource JobSpecSource = EJobSpecSource::Auto;
+};
+
+struct TGetJobInputPathsOptions
+ : public TTimeoutOptions
+{
+ //! Where job spec should be retrieved from.
+ EJobSpecSource JobSpecSource = EJobSpecSource::Auto;
+};
+
+struct TGetJobSpecOptions
+ : public TTimeoutOptions
+{
+ //! Where job spec should be retrieved from.
+ EJobSpecSource JobSpecSource = EJobSpecSource::Auto;
+
+ bool OmitNodeDirectory = false;
+ bool OmitInputTableSpecs = false;
+ bool OmitOutputTableSpecs = false;
+};
+
+
+struct TGetJobStderrOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{ };
+
+struct TGetJobFailContextOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{ };
+
+DEFINE_ENUM(EOperationSortDirection,
+ ((None) (0))
+ ((Past) (1))
+ ((Future) (2))
+);
+
+struct TListOperationsAccessFilter
+ : NYTree::TYsonStruct
+{
+ TString Subject;
+ NYTree::EPermissionSet Permissions;
+
+ // This parameter cannot be set from YSON, it must be computed.
+ THashSet<TString> SubjectTransitiveClosure;
+
+ REGISTER_YSON_STRUCT(TListOperationsAccessFilter);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("subject", &TThis::Subject);
+ registrar.Parameter("permissions", &TThis::Permissions);
+ }
+};
+
+DECLARE_REFCOUNTED_TYPE(TListOperationsAccessFilter)
+DEFINE_REFCOUNTED_TYPE(TListOperationsAccessFilter)
+
+struct TListOperationsOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{
+ std::optional<TInstant> FromTime;
+ std::optional<TInstant> ToTime;
+ std::optional<TInstant> CursorTime;
+ EOperationSortDirection CursorDirection = EOperationSortDirection::Past;
+ std::optional<TString> UserFilter;
+
+ TListOperationsAccessFilterPtr AccessFilter;
+
+ std::optional<NScheduler::EOperationState> StateFilter;
+ std::optional<NScheduler::EOperationType> TypeFilter;
+ std::optional<TString> SubstrFilter;
+ std::optional<TString> PoolTree;
+ std::optional<TString> Pool;
+ std::optional<bool> WithFailedJobs;
+ bool IncludeArchive = false;
+ bool IncludeCounters = true;
+ ui64 Limit = 100;
+
+ std::optional<THashSet<TString>> Attributes;
+
+ // TODO(ignat): Remove this mode when UI migrate to list_operations without enabled UI mode.
+ // See st/YTFRONT-1360.
+ bool EnableUIMode = false;
+
+ TDuration ArchiveFetchingTimeout = TDuration::Seconds(3);
+
+ TListOperationsOptions()
+ {
+ ReadFrom = EMasterChannelKind::Cache;
+ }
+};
+
+struct TPollJobShellResponse
+{
+ NYson::TYsonString Result;
+ // YT-14507: Logging context is required for SOC audit.
+ NYson::TYsonString LoggingContext;
+};
+
+DEFINE_ENUM(EJobSortField,
+ ((None) (0))
+ ((Type) (1))
+ ((State) (2))
+ ((StartTime) (3))
+ ((FinishTime) (4))
+ ((Address) (5))
+ ((Duration) (6))
+ ((Progress) (7))
+ ((Id) (8))
+);
+
+DEFINE_ENUM(EJobSortDirection,
+ ((Ascending) (0))
+ ((Descending) (1))
+);
+
+DEFINE_ENUM(EDataSource,
+ ((Archive) (0))
+ ((Runtime) (1))
+ ((Auto) (2))
+ // Should be used only in tests.
+ ((Manual) (3))
+);
+
+struct TListJobsOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{
+ NJobTrackerClient::TJobId JobCompetitionId;
+ std::optional<NJobTrackerClient::EJobType> Type;
+ std::optional<NJobTrackerClient::EJobState> State;
+ std::optional<TString> Address;
+ std::optional<bool> WithStderr;
+ std::optional<bool> WithFailContext;
+ std::optional<bool> WithSpec;
+ std::optional<bool> WithCompetitors;
+ std::optional<TString> TaskName;
+
+ TDuration RunningJobsLookbehindPeriod = TDuration::Max();
+
+ EJobSortField SortField = EJobSortField::None;
+ EJobSortDirection SortOrder = EJobSortDirection::Ascending;
+
+ i64 Limit = 1000;
+ i64 Offset = 0;
+
+ // All options below are deprecated.
+ bool IncludeCypress = false;
+ bool IncludeControllerAgent = false;
+ bool IncludeArchive = false;
+ EDataSource DataSource = EDataSource::Auto;
+};
+
+struct TAbandonJobOptions
+ : public TTimeoutOptions
+{ };
+
+struct TPollJobShellOptions
+ : public TTimeoutOptions
+{ };
+
+struct TAbortJobOptions
+ : public TTimeoutOptions
+{
+ std::optional<TDuration> InterruptTimeout;
+};
+
+struct TGetOperationOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{
+ std::optional<THashSet<TString>> Attributes;
+ TDuration ArchiveTimeout = TDuration::Seconds(5);
+ TDuration MaximumCypressProgressAge = TDuration::Minutes(2);
+ bool IncludeRuntime = false;
+};
+
+struct TGetJobOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{
+ std::optional<THashSet<TString>> Attributes;
+};
+
+struct TSelectRowsResult
+{
+ IUnversionedRowsetPtr Rowset;
+ NQueryClient::TQueryStatistics Statistics;
+};
+
+struct TGetClusterMetaOptions
+ : public TTimeoutOptions
+ , public TMasterReadOptions
+{
+ bool PopulateNodeDirectory = false;
+ bool PopulateClusterDirectory = false;
+ bool PopulateMediumDirectory = false;
+ bool PopulateCellDirectory = false;
+ bool PopulateMasterCacheNodeAddresses = false;
+ bool PopulateTimestampProviderAddresses = false;
+ bool PopulateFeatures = false;
+};
+
+struct TClusterMeta
+{
+ std::shared_ptr<NNodeTrackerClient::NProto::TNodeDirectory> NodeDirectory;
+ std::shared_ptr<NHiveClient::NProto::TClusterDirectory> ClusterDirectory;
+ std::shared_ptr<NChunkClient::NProto::TMediumDirectory> MediumDirectory;
+ std::vector<TString> MasterCacheNodeAddresses;
+ std::vector<TString> TimestampProviderAddresses;
+ NYTree::IMapNodePtr Features;
+};
+
+struct TOperation
+{
+ std::optional<NScheduler::TOperationId> Id;
+
+ std::optional<NScheduler::EOperationType> Type;
+ std::optional<NScheduler::EOperationState> State;
+
+ std::optional<TInstant> StartTime;
+ std::optional<TInstant> FinishTime;
+
+ std::optional<TString> AuthenticatedUser;
+
+ NYson::TYsonString BriefSpec;
+ NYson::TYsonString Spec;
+ NYson::TYsonString ProvidedSpec;
+ NYson::TYsonString ExperimentAssignments;
+ NYson::TYsonString ExperimentAssignmentNames;
+ NYson::TYsonString FullSpec;
+ NYson::TYsonString UnrecognizedSpec;
+
+ NYson::TYsonString BriefProgress;
+ NYson::TYsonString Progress;
+
+ NYson::TYsonString RuntimeParameters;
+
+ std::optional<bool> Suspended;
+
+ NYson::TYsonString Events;
+ NYson::TYsonString Result;
+
+ NYson::TYsonString SlotIndexPerPoolTree;
+ NYson::TYsonString Alerts;
+ NYson::TYsonString AlertEvents;
+
+ NYson::TYsonString TaskNames;
+
+ NYson::TYsonString ControllerFeatures;
+
+ NYTree::IAttributeDictionaryPtr OtherAttributes;
+};
+
+void Serialize(
+ const TOperation& operation,
+ NYson::IYsonConsumer* consumer,
+ bool needType = true,
+ bool needOperationType = false,
+ bool idWithAttributes = false);
+
+void Deserialize(TOperation& operation, NYTree::IAttributeDictionaryPtr attriubutes, bool clone = true);
+
+struct TListOperationsResult
+{
+ std::vector<TOperation> Operations;
+ std::optional<THashMap<TString, i64>> PoolTreeCounts;
+ std::optional<THashMap<TString, i64>> PoolCounts;
+ std::optional<THashMap<TString, i64>> UserCounts;
+ std::optional<TEnumIndexedVector<NScheduler::EOperationState, i64>> StateCounts;
+ std::optional<TEnumIndexedVector<NScheduler::EOperationType, i64>> TypeCounts;
+ std::optional<i64> FailedJobsCount;
+ bool Incomplete = false;
+};
+
+struct TJob
+{
+ NJobTrackerClient::TJobId Id;
+ NJobTrackerClient::TJobId OperationId;
+ std::optional<NJobTrackerClient::EJobType> Type;
+ std::optional<NJobTrackerClient::EJobState> ControllerState;
+ std::optional<NJobTrackerClient::EJobState> ArchiveState;
+ std::optional<TInstant> StartTime;
+ std::optional<TInstant> FinishTime;
+ std::optional<TString> Address;
+ std::optional<double> Progress;
+ std::optional<ui64> StderrSize;
+ std::optional<ui64> FailContextSize;
+ std::optional<bool> HasSpec;
+ std::optional<bool> HasCompetitors;
+ std::optional<bool> HasProbingCompetitors;
+ NJobTrackerClient::TJobId JobCompetitionId;
+ NJobTrackerClient::TJobId ProbingJobCompetitionId;
+ NYson::TYsonString Error;
+ NYson::TYsonString BriefStatistics;
+ NYson::TYsonString Statistics;
+ NYson::TYsonString InputPaths;
+ NYson::TYsonString CoreInfos;
+ NYson::TYsonString Events;
+ NYson::TYsonString ExecAttributes;
+ std::optional<TString> TaskName;
+ std::optional<TString> PoolTree;
+ std::optional<TString> Pool;
+ std::optional<TString> MonitoringDescriptor;
+ std::optional<ui64> JobCookie;
+
+ std::optional<bool> IsStale;
+
+ std::optional<NJobTrackerClient::EJobState> GetState() const;
+};
+
+void Serialize(const TJob& job, NYson::IYsonConsumer* consumer, TStringBuf idKey);
+
+struct TListJobsStatistics
+{
+ TEnumIndexedVector<NJobTrackerClient::EJobState, i64> StateCounts;
+ TEnumIndexedVector<NJobTrackerClient::EJobType, i64> TypeCounts;
+};
+
+struct TListJobsResult
+{
+ std::vector<TJob> Jobs;
+ std::optional<int> CypressJobCount;
+ std::optional<int> ControllerAgentJobCount;
+ std::optional<int> ArchiveJobCount;
+
+ TListJobsStatistics Statistics;
+
+ std::vector<TError> Errors;
+};
+
+struct TQuery
+{
+ NQueryTrackerClient::TQueryId Id;
+ std::optional<NQueryTrackerClient::EQueryEngine> Engine;
+ std::optional<TString> Query;
+ std::optional<TInstant> StartTime;
+ std::optional<TInstant> FinishTime;
+ NYson::TYsonString Settings;
+ std::optional<TString> User;
+ std::optional<NQueryTrackerClient::EQueryState> State;
+ std::optional<i64> ResultCount;
+ NYson::TYsonString Progress;
+ std::optional<TError> Error;
+ NYson::TYsonString Annotations;
+ NYTree::IAttributeDictionaryPtr OtherAttributes;
+};
+
+void Serialize(const TQuery& query, NYson::IYsonConsumer* consumer);
+
+struct TQueryResult
+{
+ NQueryTrackerClient::TQueryId Id;
+ i64 ResultIndex;
+ TError Error;
+ NTableClient::TTableSchemaPtr Schema;
+ NChunkClient::NProto::TDataStatistics DataStatistics;
+};
+
+void Serialize(const TQueryResult& queryResult, NYson::IYsonConsumer* consumer);
+
+struct TListQueueConsumerRegistrationsResult
+{
+ NYPath::TRichYPath QueuePath;
+ NYPath::TRichYPath ConsumerPath;
+ bool Vital;
+ std::optional<std::vector<int>> Partitions;
+};
+
+struct TGetFileFromCacheResult
+{
+ NYPath::TYPath Path;
+};
+
+struct TPutFileToCacheResult
+{
+ NYPath::TYPath Path;
+};
+
+struct TCheckClusterLivenessOptions
+ : public TTimeoutOptions
+{
+ //! Checks cypress root availability.
+ bool CheckCypressRoot = false;
+ //! Checks secondary master cells generic availability.
+ bool CheckSecondaryMasterCells = false;
+ //! Unless null checks tablet cell bundle health.
+ std::optional<TString> CheckTabletCellBundle;
+
+ bool IsCheckTrivial() const;
+
+ // NB: For testing purposes.
+ bool operator==(const TCheckClusterLivenessOptions& other) const;
+};
+
+struct TBuildSnapshotOptions
+ : public TTimeoutOptions
+{
+ //! Refers either to masters or to tablet cells.
+ //! If null then the primary one is assumed.
+ NHydra::TCellId CellId;
+ bool SetReadOnly = false;
+ bool WaitForSnapshotCompletion = true;
+};
+
+struct TBuildMasterSnapshotsOptions
+ : public TTimeoutOptions
+{
+ bool SetReadOnly = false;
+ bool WaitForSnapshotCompletion = true;
+ bool Retry = true;
+};
+
+struct TSwitchLeaderOptions
+ : public TTimeoutOptions
+{ };
+
+struct TResetStateHashOptions
+ : public TTimeoutOptions
+{
+ //! If not set, random number is used.
+ std::optional<ui64> NewStateHash;
+};
+
+struct TGCCollectOptions
+ : public TTimeoutOptions
+{
+ //! Refers to master cell.
+ //! If null then the primary one is assumed.
+ NHydra::TCellId CellId;
+};
+
+struct TKillProcessOptions
+ : public TTimeoutOptions
+{
+ int ExitCode = 42;
+};
+
+struct TWriteCoreDumpOptions
+ : public TTimeoutOptions
+{ };
+
+struct TWriteLogBarrierOptions
+ : public TTimeoutOptions
+{
+ TString Category;
+};
+
+struct TWriteOperationControllerCoreDumpOptions
+ : public TTimeoutOptions
+{ };
+
+struct THealExecNodeOptions
+ : public TTimeoutOptions
+{
+ std::vector<TString> Locations;
+ std::vector<TString> AlertTypesToReset;
+ bool ForceReset = false;
+};
+
+struct TSuspendCoordinatorOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TResumeCoordinatorOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TMigrateReplicationCardsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{
+ NObjectClient::TCellId DestinationCellId;
+ std::vector<NChaosClient::TReplicationCardId> ReplicationCardIds;
+};
+
+struct TSuspendChaosCellsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TResumeChaosCellsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TSuspendTabletCellsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TResumeTabletCellsOptions
+ : public TTimeoutOptions
+ , public TMutatingOptions
+{ };
+
+struct TDisableChunkLocationsOptions
+ : public TTimeoutOptions
+{ };
+
+struct TDisableChunkLocationsResult
+{
+ std::vector<TGuid> LocationUuids;
+};
+
+struct TDestroyChunkLocationsOptions
+ : public TTimeoutOptions
+{ };
+
+struct TDestroyChunkLocationsResult
+{
+ std::vector<TGuid> LocationUuids;
+};
+
+struct TResurrectChunkLocationsOptions
+ : public TTimeoutOptions
+{ };
+
+struct TResurrectChunkLocationsResult
+{
+ std::vector<TGuid> LocationUuids;
+};
+
+using TCellIdToSnapshotIdMap = THashMap<NHydra::TCellId, int>;
+
+struct TTableBackupManifest
+ : public NYTree::TYsonStruct
+{
+ NYTree::TYPath SourcePath;
+ NYTree::TYPath DestinationPath;
+ NTabletClient::EOrderedTableBackupMode OrderedMode;
+
+ REGISTER_YSON_STRUCT(TTableBackupManifest);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("source_path", &TThis::SourcePath);
+ registrar.Parameter("destination_path", &TThis::DestinationPath);
+ registrar.Parameter("ordered_mode", &TThis::OrderedMode)
+ .Default(NTabletClient::EOrderedTableBackupMode::Exact);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableBackupManifest)
+
+struct TBackupManifest
+ : public NYTree::TYsonStruct
+{
+ THashMap<TString, std::vector<TTableBackupManifestPtr>> Clusters;
+
+ REGISTER_YSON_STRUCT(TBackupManifest);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("clusters", &TThis::Clusters);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBackupManifest)
+
+struct TCreateTableBackupOptions
+ : public TTimeoutOptions
+{
+ TDuration CheckpointTimestampDelay = TDuration::Zero();
+ TDuration CheckpointCheckPeriod = TDuration::Zero();
+ TDuration CheckpointCheckTimeout = TDuration::Zero();
+
+ bool Force = false;
+};
+
+struct TRestoreTableBackupOptions
+ : public TTimeoutOptions
+{
+ bool Force = false;
+ bool Mount = false;
+ bool EnableReplicas = false;
+};
+
+struct TQueryTrackerOptions
+{
+ TString QueryTrackerStage = "production";
+};
+
+struct TStartQueryOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ NYTree::INodePtr Settings;
+ bool Draft = false;
+ NYTree::IMapNodePtr Annotations;
+};
+
+struct TAbortQueryOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ std::optional<TString> AbortMessage;
+};
+
+struct TGetQueryResultOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{ };
+
+struct TReadQueryResultOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ std::optional<std::vector<TString>> Columns;
+ std::optional<i64> LowerRowIndex;
+ std::optional<i64> UpperRowIndex;
+};
+
+struct TGetQueryOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ NYTree::TAttributeFilter Attributes;
+ NTransactionClient::TTimestamp Timestamp = NTransactionClient::NullTimestamp;
+};
+
+struct TListQueriesOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ std::optional<TInstant> FromTime;
+ std::optional<TInstant> ToTime;
+ std::optional<TInstant> CursorTime;
+ EOperationSortDirection CursorDirection = EOperationSortDirection::Past;
+ std::optional<TString> UserFilter;
+
+ std::optional<NQueryTrackerClient::EQueryState> StateFilter;
+ std::optional<NQueryTrackerClient::EQueryEngine> EngineFilter;
+ std::optional<TString> SubstrFilter;
+ ui64 Limit = 100;
+
+ NYTree::TAttributeFilter Attributes;
+};
+
+struct TListQueriesResult
+{
+ std::vector<TQuery> Queries;
+ bool Incomplete = false;
+ NTransactionClient::TTimestamp Timestamp;
+};
+
+struct TAlterQueryOptions
+ : public TTimeoutOptions
+ , public TQueryTrackerOptions
+{
+ NYTree::IMapNodePtr Annotations;
+};
+
+struct TSetUserPasswordOptions
+ : public TTimeoutOptions
+{ };
+
+struct TIssueTokenOptions
+ : public TTimeoutOptions
+{ };
+
+struct TIssueTokenResult
+{
+ TString Token;
+};
+
+struct TRevokeTokenOptions
+ : public TTimeoutOptions
+{ };
+
+struct TListUserTokensOptions
+ : public TTimeoutOptions
+{ };
+
+struct TListUserTokensResult
+{
+ // Tokens are SHA256-encoded.
+ std::vector<TString> Tokens;
+};
+
+struct TAddMaintenanceOptions
+ : public TTimeoutOptions
+{ };
+
+struct TMaintenanceFilter
+{
+ struct TByUser
+ {
+ struct TAll
+ { };
+
+ struct TMine
+ { };
+ };
+
+ // Empty means no filtering by id.
+ std::vector<TMaintenanceId> Ids;
+ std::optional<EMaintenanceType> Type;
+ std::variant<TByUser::TAll, TByUser::TMine, TString> User = {};
+};
+
+struct TRemoveMaintenanceOptions
+ : public TTimeoutOptions
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides a basic set of functions that can be invoked
+//! both standalone and inside transaction.
+/*
+ * This interface contains methods shared by IClient and ITransaction.
+ *
+ * Thread affinity: single
+ */
+struct IClientBase
+ : public virtual TRefCounted
+{
+ virtual IConnectionPtr GetConnection() = 0;
+
+ // Transactions
+ virtual TFuture<ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options = {}) = 0;
+
+ // Tables
+ virtual TFuture<IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options = {}) = 0;
+
+ virtual TFuture<IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options = {}) = 0;
+
+ virtual TFuture<TSelectRowsResult> SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> ExplainQuery(
+ const TString& query,
+ const TExplainQueryOptions& options = TExplainQueryOptions()) = 0;
+
+ virtual TFuture<TPullRowsResult> PullRows(
+ const NYPath::TYPath& path,
+ const TPullRowsOptions& options = {}) = 0;
+
+ virtual TFuture<ITableReaderPtr> CreateTableReader(
+ const NYPath::TRichYPath& path,
+ const TTableReaderOptions& options = {}) = 0;
+
+ virtual TFuture<ITableWriterPtr> CreateTableWriter(
+ const NYPath::TRichYPath& path,
+ const TTableWriterOptions& options = {}) = 0;
+
+ // Cypress
+ virtual TFuture<NYson::TYsonString> GetNode(
+ const NYPath::TYPath& path,
+ const TGetNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> SetNode(
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> MultisetAttributesNode(
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> RemoveNode(
+ const NYPath::TYPath& path,
+ const TRemoveNodeOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> ListNode(
+ const NYPath::TYPath& path,
+ const TListNodeOptions& options = {}) = 0;
+
+ virtual TFuture<NCypressClient::TNodeId> CreateNode(
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options = {}) = 0;
+
+ virtual TFuture<TLockNodeResult> LockNode(
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> UnlockNode(
+ const NYPath::TYPath& path,
+ const TUnlockNodeOptions& options = {}) = 0;
+
+ virtual TFuture<NCypressClient::TNodeId> CopyNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TCopyNodeOptions& options = {}) = 0;
+
+ virtual TFuture<NCypressClient::TNodeId> MoveNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TMoveNodeOptions& options = {}) = 0;
+
+ virtual TFuture<NCypressClient::TNodeId> LinkNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TLinkNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> ConcatenateNodes(
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options = {}) = 0;
+
+ virtual TFuture<bool> NodeExists(
+ const NYPath::TYPath& path,
+ const TNodeExistsOptions& options = {}) = 0;
+
+ virtual TFuture<void> ExternalizeNode(
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> InternalizeNode(
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options = {}) = 0;
+
+
+ // Objects
+ virtual TFuture<NObjectClient::TObjectId> CreateObject(
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options = {}) = 0;
+
+
+ // Files
+ virtual TFuture<IFileReaderPtr> CreateFileReader(
+ const NYPath::TYPath& path,
+ const TFileReaderOptions& options = {}) = 0;
+
+ virtual IFileWriterPtr CreateFileWriter(
+ const NYPath::TRichYPath& path,
+ const TFileWriterOptions& options = {}) = 0;
+
+ // Journals
+ virtual IJournalReaderPtr CreateJournalReader(
+ const NYPath::TYPath& path,
+ const TJournalReaderOptions& options = {}) = 0;
+
+ virtual IJournalWriterPtr CreateJournalWriter(
+ const NYPath::TYPath& path,
+ const TJournalWriterOptions& options = {}) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IClientBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A central entry point for all interactions with the YT cluster.
+/*!
+ * In contrast to IConnection, each IClient represents an authenticated entity.
+ * The needed username is passed to #IConnection::CreateClient via options.
+ * Note that YT API has no built-in authentication mechanisms so it must be wrapped
+ * with appropriate logic.
+ *
+ * Most methods accept |TransactionId| as a part of their options.
+ * A similar effect can be achieved by issuing requests via ITransaction.
+ */
+struct IClient
+ : public virtual IClientBase
+{
+ //! Terminates all channels.
+ //! Aborts all pending uncommitted transactions.
+ virtual void Terminate() = 0;
+
+ virtual const NTabletClient::ITableMountCachePtr& GetTableMountCache() = 0;
+ virtual const NChaosClient::IReplicationCardCachePtr& GetReplicationCardCache() = 0;
+ virtual const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() = 0;
+
+ virtual std::optional<TStringBuf> GetClusterName(bool fetchIfNull = true) = 0;
+
+ // Transactions
+ virtual ITransactionPtr AttachTransaction(
+ NTransactionClient::TTransactionId transactionId,
+ const TTransactionAttachOptions& options = {}) = 0;
+
+ // Tables
+ virtual TFuture<void> MountTable(
+ const NYPath::TYPath& path,
+ const TMountTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> UnmountTable(
+ const NYPath::TYPath& path,
+ const TUnmountTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> RemountTable(
+ const NYPath::TYPath& path,
+ const TRemountTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> FreezeTable(
+ const NYPath::TYPath& path,
+ const TFreezeTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> UnfreezeTable(
+ const NYPath::TYPath& path,
+ const TUnfreezeTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ const std::vector<NTableClient::TLegacyOwningKey>& pivotKeys,
+ const TReshardTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ int tabletCount,
+ const TReshardTableOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<NTabletClient::TTabletActionId>> ReshardTableAutomatic(
+ const NYPath::TYPath& path,
+ const TReshardTableAutomaticOptions& options = {}) = 0;
+
+ virtual TFuture<void> TrimTable(
+ const NYPath::TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const TTrimTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> AlterTable(
+ const NYPath::TYPath& path,
+ const TAlterTableOptions& options = {}) = 0;
+
+ virtual TFuture<void> AlterTableReplica(
+ NTabletClient::TTableReplicaId replicaId,
+ const TAlterTableReplicaOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> GetTablePivotKeys(
+ const NYPath::TYPath& path,
+ const TGetTablePivotKeysOptions& options = {}) = 0;
+
+ virtual TFuture<void> CreateTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TCreateTableBackupOptions& options = {}) = 0;
+
+ virtual TFuture<void> RestoreTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TRestoreTableBackupOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const NTableClient::TNameTablePtr& nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TGetInSyncReplicasOptions& options = {}) = 0;
+
+ //! Same as above but returns the list of replicas that are in sync w.r.t. all of the table keys.
+ virtual TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const TGetInSyncReplicasOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<TTabletInfo>> GetTabletInfos(
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options = {}) = 0;
+
+ virtual TFuture<TGetTabletErrorsResult> GetTabletErrors(
+ const NYPath::TYPath& path,
+ const TGetTabletErrorsOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<NTabletClient::TTabletActionId>> BalanceTabletCells(
+ const TString& tabletCellBundle,
+ const std::vector<NYPath::TYPath>& movableTables,
+ const TBalanceTabletCellsOptions& options = {}) = 0;
+
+ virtual TFuture<NChaosClient::TReplicationCardPtr> GetReplicationCard(
+ const NChaosClient::TReplicationCardId replicationCardId,
+ const TGetReplicationCardOptions& options = {}) = 0;
+
+ virtual TFuture<void> UpdateChaosTableReplicaProgress(
+ NChaosClient::TReplicaId replicaId,
+ const TUpdateChaosTableReplicaProgressOptions& options = {}) = 0;
+
+ virtual TFuture<void> AlterReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options = {}) = 0;
+
+
+ virtual TFuture<TSkynetSharePartsLocationsPtr> LocateSkynetShare(
+ const NYPath::TRichYPath& path,
+ const TLocateSkynetShareOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<NTableClient::TColumnarStatistics>> GetColumnarStatistics(
+ const std::vector<NYPath::TRichYPath>& path,
+ const TGetColumnarStatisticsOptions& options = {}) = 0;
+
+ virtual TFuture<TMultiTablePartitions> PartitionTables(
+ const std::vector<NYPath::TRichYPath>& paths,
+ const TPartitionTablesOptions& options) = 0;
+
+ // Queues
+ //! Reads a batch of rows from a given partition of a given queue, starting at (at least) the given offset.
+ //! Requires the user to have read-access to the specified queue.
+ virtual TFuture<NQueueClient::IQueueRowsetPtr> PullQueue(
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options = {}) = 0;
+
+ //! Same as PullQueue, but requires user to have read-access to the consumer and the consumer being registered for the given queue.
+ virtual TFuture<NQueueClient::IQueueRowsetPtr> PullConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options = {}) = 0;
+
+ virtual TFuture<void> RegisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options = {}) = 0;
+
+ virtual TFuture<void> UnregisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<TListQueueConsumerRegistrationsResult>> ListQueueConsumerRegistrations(
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options = {}) = 0;
+
+ // Journals
+ virtual TFuture<void> TruncateJournal(
+ const NYPath::TYPath& path,
+ i64 rowCount,
+ const TTruncateJournalOptions& options = {}) = 0;
+
+ // Files
+ virtual TFuture<TGetFileFromCacheResult> GetFileFromCache(
+ const TString& md5,
+ const TGetFileFromCacheOptions& options = {}) = 0;
+
+ virtual TFuture<TPutFileToCacheResult> PutFileToCache(
+ const NYPath::TYPath& path,
+ const TString& expectedMD5,
+ const TPutFileToCacheOptions& options = {}) = 0;
+
+ // Security
+ virtual TFuture<void> AddMember(
+ const TString& group,
+ const TString& member,
+ const TAddMemberOptions& options = {}) = 0;
+
+ virtual TFuture<void> RemoveMember(
+ const TString& group,
+ const TString& member,
+ const TRemoveMemberOptions& options = {}) = 0;
+
+ virtual TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ const NYPath::TYPath& path,
+ NYTree::EPermission permission,
+ const TCheckPermissionOptions& options = {}) = 0;
+
+ virtual TFuture<TCheckPermissionByAclResult> CheckPermissionByAcl(
+ const std::optional<TString>& user,
+ NYTree::EPermission permission,
+ NYTree::INodePtr acl,
+ const TCheckPermissionByAclOptions& options = {}) = 0;
+
+ virtual TFuture<void> TransferAccountResources(
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options = {}) = 0;
+
+ virtual TFuture<void> TransferPoolResources(
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options = {}) = 0;
+
+ // Scheduler
+ virtual TFuture<NScheduler::TOperationId> StartOperation(
+ NScheduler::EOperationType type,
+ const NYson::TYsonString& spec,
+ const TStartOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> AbortOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TAbortOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> SuspendOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TSuspendOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> ResumeOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TResumeOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> CompleteOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TCompleteOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> UpdateOperationParameters(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NYson::TYsonString& parameters,
+ const TUpdateOperationParametersOptions& options = {}) = 0;
+
+ virtual TFuture<TOperation> GetOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TGetOperationOptions& options = {}) = 0;
+
+ virtual TFuture<void> DumpJobContext(
+ NJobTrackerClient::TJobId jobId,
+ const NYPath::TYPath& path,
+ const TDumpJobContextOptions& options = {}) = 0;
+
+ virtual TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> GetJobInput(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> GetJobInputPaths(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputPathsOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> GetJobSpec(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobSpecOptions& options = {}) = 0;
+
+ virtual TFuture<TSharedRef> GetJobStderr(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobStderrOptions& options = {}) = 0;
+
+ virtual TFuture<TSharedRef> GetJobFailContext(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobFailContextOptions& options = {}) = 0;
+
+ virtual TFuture<TListOperationsResult> ListOperations(
+ const TListOperationsOptions& options = {}) = 0;
+
+ virtual TFuture<TListJobsResult> ListJobs(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TListJobsOptions& options = {}) = 0;
+
+ virtual TFuture<NYson::TYsonString> GetJob(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobOptions& options = {}) = 0;
+
+ virtual TFuture<void> AbandonJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbandonJobOptions& options = {}) = 0;
+
+ virtual TFuture<TPollJobShellResponse> PollJobShell(
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const NYson::TYsonString& parameters,
+ const TPollJobShellOptions& options = {}) = 0;
+
+ virtual TFuture<void> AbortJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbortJobOptions& options = {}) = 0;
+
+ // Metadata
+ virtual TFuture<TClusterMeta> GetClusterMeta(
+ const TGetClusterMetaOptions& options = {}) = 0;
+
+ virtual TFuture<void> CheckClusterLiveness(
+ const TCheckClusterLivenessOptions& options = {}) = 0;
+
+ // Administration
+ virtual TFuture<int> BuildSnapshot(
+ const TBuildSnapshotOptions& options = {}) = 0;
+
+ virtual TFuture<TCellIdToSnapshotIdMap> BuildMasterSnapshots(
+ const TBuildMasterSnapshotsOptions& options = {}) = 0;
+
+ virtual TFuture<void> SwitchLeader(
+ NHydra::TCellId cellId,
+ const TString& newLeaderAddress,
+ const TSwitchLeaderOptions& options = {}) = 0;
+
+ virtual TFuture<void> ResetStateHash(
+ NHydra::TCellId cellId,
+ const TResetStateHashOptions& options = {}) = 0;
+
+ virtual TFuture<void> GCCollect(
+ const TGCCollectOptions& options = {}) = 0;
+
+ virtual TFuture<void> KillProcess(
+ const TString& address,
+ const TKillProcessOptions& options = {}) = 0;
+
+ virtual TFuture<TString> WriteCoreDump(
+ const TString& address,
+ const TWriteCoreDumpOptions& options = {}) = 0;
+
+ virtual TFuture<TGuid> WriteLogBarrier(
+ const TString& address,
+ const TWriteLogBarrierOptions& options) = 0;
+
+ virtual TFuture<TString> WriteOperationControllerCoreDump(
+ NJobTrackerClient::TOperationId operationId,
+ const TWriteOperationControllerCoreDumpOptions& options = {}) = 0;
+
+ virtual TFuture<void> HealExecNode(
+ const TString& address,
+ const THealExecNodeOptions& options = {}) = 0;
+
+ virtual TFuture<void> SuspendCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TSuspendCoordinatorOptions& options = {}) = 0;
+
+ virtual TFuture<void> ResumeCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TResumeCoordinatorOptions& options = {}) = 0;
+
+ virtual TFuture<void> MigrateReplicationCards(
+ NObjectClient::TCellId chaosCellId,
+ const TMigrateReplicationCardsOptions& options = {}) = 0;
+
+ virtual TFuture<void> SuspendChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& options = {}) = 0;
+
+ virtual TFuture<void> ResumeChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeChaosCellsOptions& options = {}) = 0;
+
+ virtual TFuture<void> SuspendTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendTabletCellsOptions& options = {}) = 0;
+
+ virtual TFuture<void> ResumeTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeTabletCellsOptions& options = {}) = 0;
+
+ virtual TFuture<TMaintenanceId> AddMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options = {}) = 0;
+
+ virtual TFuture<TMaintenanceCounts> RemoveMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options = {}) = 0;
+
+ virtual TFuture<TDisableChunkLocationsResult> DisableChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& options = {}) = 0;
+
+ virtual TFuture<TDestroyChunkLocationsResult> DestroyChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& options = {}) = 0;
+
+ virtual TFuture<TResurrectChunkLocationsResult> ResurrectChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& options = {}) = 0;
+
+ // Query tracker
+
+ virtual TFuture<NQueryTrackerClient::TQueryId> StartQuery(
+ NQueryTrackerClient::EQueryEngine engine,
+ const TString& query,
+ const TStartQueryOptions& options = {}) = 0;
+
+ virtual TFuture<void> AbortQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAbortQueryOptions& options = {}) = 0;
+
+ virtual TFuture<TQueryResult> GetQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex = 0,
+ const TGetQueryResultOptions& options = {}) = 0;
+
+ virtual TFuture<IUnversionedRowsetPtr> ReadQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex = 0,
+ const TReadQueryResultOptions& options = {}) = 0;
+
+ virtual TFuture<TQuery> GetQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TGetQueryOptions& options = {}) = 0;
+
+ virtual TFuture<TListQueriesResult> ListQueries(const TListQueriesOptions& options = {}) = 0;
+
+ virtual TFuture<void> AlterQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAlterQueryOptions& options = {}) = 0;
+
+ // Authentication
+
+ // Methods below correspond to simple authentication scheme
+ // and are intended to be used on clusters without third-party tokens (e.g. Yandex blackbox).
+ virtual TFuture<void> SetUserPassword(
+ const TString& user,
+ const TString& currentPasswordSha256,
+ const TString& newPasswordSha256,
+ const TSetUserPasswordOptions& options) = 0;
+
+ virtual TFuture<TIssueTokenResult> IssueToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TIssueTokenOptions& options) = 0;
+
+ virtual TFuture<void> RevokeToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TString& tokenSha256,
+ const TRevokeTokenOptions& options) = 0;
+
+ virtual TFuture<TListUserTokensResult> ListUserTokens(
+ const TString& user,
+ const TString& passwordSha256,
+ const TListUserTokensOptions& options) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClusterAwareClientBase
+ : public virtual IClient
+{
+public:
+ //! Returns and caches the cluster name corresponding to this client.
+ //! If available, the name is taken from the client's connection configuration.
+ //! Otherwise, if fetchIfNull is set to true, the first call to this method
+ //! will fetch the cluster name from Cypress via master caches.
+ //!
+ //! NB: Descendants of this class should be able to perform GetNode calls,
+ //! so this cannot be used directly in tablet transactions.
+ //! Use the transaction's parent client instead.
+ std::optional<TStringBuf> GetClusterName(bool fetchIfNull) override;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ std::optional<TString> ClusterName_;
+
+ std::optional<TString> FetchClusterNameFromMasterCache();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A subset of #TTransactionStartOptions tailored for the case of alien
+//! transactions.
+struct TAlienTransactionStartOptions
+{
+ NTransactionClient::EAtomicity Atomicity = NTransactionClient::EAtomicity::Full;
+ NTransactionClient::EDurability Durability = NTransactionClient::EDurability::Sync;
+
+ NTransactionClient::TTimestamp StartTimestamp = NTransactionClient::NullTimestamp;
+};
+
+//! A helper for starting an alien transaction at #alienClient.
+/*!
+ * Internally, invokes #IClient::StartTransaction and #ITransaction::RegisterAlienTransaction
+ * but also takes care of the following issues:
+ * 1) Alien and local transaction ids must be same;
+ * 2) If #alienClient and #localTransaction have matching clusters then no
+ * new transaction must be created.
+ */
+TFuture<ITransactionPtr> StartAlienTransaction(
+ const ITransactionPtr& localTransaction,
+ const IClientPtr& alienClient,
+ const TAlienTransactionStartOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/client_cache.cpp b/yt/yt/client/api/client_cache.cpp
new file mode 100644
index 0000000000..2af50c294b
--- /dev/null
+++ b/yt/yt/client/api/client_cache.cpp
@@ -0,0 +1,45 @@
+#include "client_cache.h"
+#include "connection.h"
+
+namespace NYT::NApi {
+
+using namespace NRpc;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCachedClient::TCachedClient(
+ const TAuthenticationIdentity& identity,
+ IClientPtr client)
+ : TSyncCacheValueBase(identity)
+ , Client_(std::move(client))
+{ }
+
+const IClientPtr& TCachedClient::GetClient()
+{
+ return Client_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientCache::TClientCache(
+ TSlruCacheConfigPtr config,
+ IConnectionPtr connection)
+ : TSyncSlruCacheBase<TAuthenticationIdentity, TCachedClient>(std::move(config))
+ , Connection_(std::move(connection))
+{ }
+
+IClientPtr TClientCache::Get(
+ const TAuthenticationIdentity& identity,
+ const TClientOptions& options)
+{
+ auto cachedClient = Find(identity);
+ if (!cachedClient) {
+ cachedClient = New<TCachedClient>(identity, Connection_->CreateClient(options));
+ TryInsert(cachedClient, &cachedClient);
+ }
+ return cachedClient->GetClient();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/client_cache.h b/yt/yt/client/api/client_cache.h
new file mode 100644
index 0000000000..bc7d455b6a
--- /dev/null
+++ b/yt/yt/client/api/client_cache.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/misc/sync_cache.h>
+
+#include <yt/yt/core/rpc/authentication_identity.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCachedClient
+ : public TSyncCacheValueBase<NRpc::TAuthenticationIdentity, TCachedClient>
+{
+public:
+ TCachedClient(
+ const NRpc::TAuthenticationIdentity& identity,
+ IClientPtr client);
+
+ const IClientPtr& GetClient();
+
+private:
+ const IClientPtr Client_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An SLRU-cache based class for keeping a cache of clients for different users.
+/*
+ * For NApi::NNative::IClient equivalent see ytlib/api/native/client_cache.h.
+ *
+ * Cache is completely thread-safe.
+ */
+class TClientCache
+ : public TSyncSlruCacheBase<NRpc::TAuthenticationIdentity, TCachedClient>
+{
+public:
+ TClientCache(
+ TSlruCacheConfigPtr config,
+ IConnectionPtr connection);
+
+ IClientPtr Get(
+ const NRpc::TAuthenticationIdentity& identity,
+ const TClientOptions& options);
+
+private:
+ // TODO(max42): shouldn't this be TWeakPtr?
+ const IConnectionPtr Connection_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/config.cpp b/yt/yt/client/api/config.cpp
new file mode 100644
index 0000000000..d8cdabecd3
--- /dev/null
+++ b/yt/yt/client/api/config.cpp
@@ -0,0 +1,190 @@
+#include "config.h"
+
+#include <yt/yt/core/misc/backoff_strategy_config.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTableMountCacheConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("on_error_retry_count", &TThis::OnErrorRetryCount)
+ .GreaterThanOrEqual(0)
+ .Default(5);
+ registrar.Parameter("on_error_retry_slack_period", &TThis::OnErrorSlackPeriod)
+ .GreaterThan(TDuration::Zero())
+ .Default(TDuration::Seconds(1));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TConnectionConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("connection_type", &TThis::ConnectionType)
+ .Default(EConnectionType::Native);
+ registrar.Parameter("cluster_name", &TThis::ClusterName)
+ .Default();
+ registrar.Parameter("table_mount_cache", &TThis::TableMountCache)
+ .DefaultNew();
+ registrar.Parameter("replication_card_cache", &TThis::ReplicationCardCache)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TConnectionDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("table_mount_cache", &TThis::TableMountCache)
+ .DefaultNew();
+ registrar.Parameter("tablet_write_backoff", &TThis::TabletWriteBackoff)
+ .DefaultNew();
+
+ registrar.Preprocessor([] (TThis* config) {
+ // TODO(gritukan): Enable tablet retries by default one day.
+ config->TabletWriteBackoff = New<TSerializableExponentialBackoffOptions>();
+ config->TabletWriteBackoff->RetryCount = 1;
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPersistentQueuePollerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_prefetch_row_count", &TThis::MaxPrefetchRowCount)
+ .GreaterThan(0)
+ .Default(1024);
+ registrar.Parameter("max_prefetch_data_weight", &TThis::MaxPrefetchDataWeight)
+ .GreaterThan(0)
+ .Default((i64) 16 * 1024 * 1024);
+ registrar.Parameter("max_rows_per_fetch", &TThis::MaxRowsPerFetch)
+ .GreaterThan(0)
+ .Default(512);
+ registrar.Parameter("max_rows_per_poll", &TThis::MaxRowsPerPoll)
+ .GreaterThan(0)
+ .Default(1);
+ registrar.Parameter("max_fetched_untrimmed_row_count", &TThis::MaxFetchedUntrimmedRowCount)
+ .GreaterThan(0)
+ .Default(40000);
+ registrar.Parameter("untrimmed_data_rows_low", &TThis::UntrimmedDataRowsLow)
+ .Default(0);
+ registrar.Parameter("untrimmed_data_rows_high", &TThis::UntrimmedDataRowsHigh)
+ .Default(std::numeric_limits<i64>::max());
+ registrar.Parameter("data_poll_period", &TThis::DataPollPeriod)
+ .Default(TDuration::Seconds(1));
+ registrar.Parameter("state_trim_period", &TThis::StateTrimPeriod)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("backoff_time", &TThis::BackoffTime)
+ .Default(TDuration::Seconds(5));
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->UntrimmedDataRowsLow > config->UntrimmedDataRowsHigh) {
+ THROW_ERROR_EXCEPTION("\"untrimmed_data_rows_low\" must not exceed \"untrimmed_data_rows_high\"");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJournalChunkWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_batch_row_count", &TThis::MaxBatchRowCount)
+ .Default(256);
+ registrar.Parameter("max_batch_data_size", &TThis::MaxBatchDataSize)
+ .Default(16_MB);
+ registrar.Parameter("max_batch_delay", &TThis::MaxBatchDelay)
+ .Default(TDuration::MilliSeconds(5));
+
+ registrar.Parameter("max_flush_row_count", &TThis::MaxFlushRowCount)
+ .Default(100'000);
+ registrar.Parameter("max_flush_data_size", &TThis::MaxFlushDataSize)
+ .Default(100_MB);
+
+ registrar.Parameter("prefer_local_host", &TThis::PreferLocalHost)
+ .Default(true);
+
+ registrar.Parameter("node_rpc_timeout", &TThis::NodeRpcTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("node_ping_period", &TThis::NodePingPeriod)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("node_ban_timeout", &TThis::NodeBanTimeout)
+ .Default(TDuration::Seconds(60));
+
+ registrar.Parameter("node_channel", &TThis::NodeChannel)
+ .DefaultNew();
+
+ registrar.Parameter("replica_failure_probability", &TThis::ReplicaFailureProbability)
+ .Default(0.0)
+ .InRange(0.0, 1.0);
+ registrar.Parameter("replica_row_limits", &TThis::ReplicaRowLimits)
+ .Default();
+ registrar.Parameter("replica_fake_timeout_delay", &TThis::ReplicaFakeTimeoutDelay)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MaxBatchRowCount > config->MaxFlushRowCount) {
+ THROW_ERROR_EXCEPTION("\"max_batch_row_count\" cannot be greater than \"max_flush_row_count\"")
+ << TErrorAttribute("max_batch_row_count", config->MaxBatchRowCount)
+ << TErrorAttribute("max_flush_row_count", config->MaxFlushRowCount);
+ }
+ if (config->MaxBatchDataSize > config->MaxFlushDataSize) {
+ THROW_ERROR_EXCEPTION("\"max_batch_data_size\" cannot be greater than \"max_flush_data_size\"")
+ << TErrorAttribute("max_batch_data_size", config->MaxBatchDataSize)
+ << TErrorAttribute("max_flush_data_size", config->MaxFlushDataSize);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJournalWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_chunk_row_count", &TThis::MaxChunkRowCount)
+ .GreaterThan(0)
+ .Default(1'000'000);
+ registrar.Parameter("max_chunk_data_size", &TThis::MaxChunkDataSize)
+ .GreaterThan(0)
+ .Default(10_GB);
+ registrar.Parameter("max_chunk_session_duration", &TThis::MaxChunkSessionDuration)
+ .Default(TDuration::Hours(60));
+
+ registrar.Parameter("open_session_backoff_time", &TThis::OpenSessionBackoffTime)
+ .Default(TDuration::Seconds(10));
+
+ registrar.Parameter("prerequisite_transaction_probe_period", &TThis::PrerequisiteTransactionProbePeriod)
+ .Default(TDuration::Seconds(60));
+
+ registrar.Parameter("dont_close", &TThis::DontClose)
+ .Default(false);
+ registrar.Parameter("dont_seal", &TThis::DontSeal)
+ .Default(false);
+ registrar.Parameter("dont_preallocate", &TThis::DontPreallocate)
+ .Default(false);
+ registrar.Parameter("open_delay", &TThis::OpenDelay)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJournalChunkWriterOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("replication_factor", &TThis::ReplicationFactor)
+ .Default(3);
+ registrar.Parameter("erasure_codec", &TThis::ErasureCodec)
+ .Default(NErasure::ECodec::None);
+
+ registrar.Parameter("read_quorum", &TThis::ReadQuorum)
+ .Default(2);
+ registrar.Parameter("write_quorum", &TThis::WriteQuorum)
+ .Default(2);
+
+ registrar.Parameter("replica_lag_limit", &TThis::ReplicaLagLimit)
+ .Default(NJournalClient::DefaultReplicaLagLimit);
+
+ registrar.Parameter("enable_multiplexing", &TThis::EnableMultiplexing)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/config.h b/yt/yt/client/api/config.h
new file mode 100644
index 0000000000..5bc5506299
--- /dev/null
+++ b/yt/yt/client/api/config.h
@@ -0,0 +1,256 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/journal_client/config.h>
+
+#include <yt/yt/client/tablet_client/config.h>
+
+#include <yt/yt/client/chaos_client/config.h>
+
+#include <yt/yt/client/chunk_client/config.h>
+
+#include <yt/yt/client/file_client/config.h>
+
+#include <yt/yt/library/erasure/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/core/misc/backoff_strategy_api.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EConnectionType,
+ (Native)
+ (Rpc)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableMountCacheConfig
+ : public NTabletClient::TTableMountCacheConfig
+{
+public:
+ int OnErrorRetryCount;
+ TDuration OnErrorSlackPeriod;
+
+ REGISTER_YSON_STRUCT(TTableMountCacheConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableMountCacheConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConnectionConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ EConnectionType ConnectionType;
+ std::optional<TString> ClusterName;
+ TTableMountCacheConfigPtr TableMountCache;
+ NChaosClient::TReplicationCardCacheConfigPtr ReplicationCardCache;
+
+ REGISTER_YSON_STRUCT(TConnectionConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnectionConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConnectionDynamicConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ NTabletClient::TTableMountCacheDynamicConfigPtr TableMountCache;
+
+ TSerializableExponentialBackoffOptionsPtr TabletWriteBackoff;
+
+ REGISTER_YSON_STRUCT(TConnectionDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnectionDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPersistentQueuePollerConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Try to keep at most this many prefetched rows in memory. This limit is approximate.
+ i64 MaxPrefetchRowCount;
+
+ //! Try to keep at most this much prefetched data in memory. This limit is approximate.
+ i64 MaxPrefetchDataWeight;
+
+ //! The limit for the number of rows to be requested in a single background fetch request.
+ i64 MaxRowsPerFetch;
+
+ //! The limit for the number of rows to be returned by #TPersistentQueuePoller::Poll call.
+ i64 MaxRowsPerPoll;
+
+ //! The limit on maximum number of consumed but not yet trimmed row indexes. No new rows are fetched when the limit is reached.
+ i64 MaxFetchedUntrimmedRowCount;
+
+ //! When trimming data table, keep the number of consumed but untrimmed rows about this level.
+ i64 UntrimmedDataRowsLow;
+
+ //! When more than this many of consumed but untrimmed rows appear in data table, trim the front ones
+ //! in accordance to #UntrimmedDataRowsLow.
+ i64 UntrimmedDataRowsHigh;
+
+ //! How often the data table is to be polled.
+ TDuration DataPollPeriod;
+
+ //! How often the state table is to be trimmed.
+ TDuration StateTrimPeriod;
+
+ //! For how long to backoff when a state conflict is detected.
+ TDuration BackoffTime;
+
+ REGISTER_YSON_STRUCT(TPersistentQueuePollerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TPersistentQueuePollerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileReaderConfig
+ : public virtual NChunkClient::TMultiChunkReaderConfig
+{
+ REGISTER_YSON_STRUCT(TFileReaderConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFileReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileWriterConfig
+ : public NChunkClient::TMultiChunkWriterConfig
+ , public NFileClient::TFileChunkWriterConfig
+{
+ REGISTER_YSON_STRUCT(TFileWriterConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFileWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalReaderConfig
+ : public NJournalClient::TChunkReaderConfig
+ , public TWorkloadConfig
+{
+ REGISTER_YSON_STRUCT(TJournalReaderConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TJournalReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalChunkWriterConfig
+ : public virtual TWorkloadConfig
+{
+public:
+ int MaxBatchRowCount;
+ i64 MaxBatchDataSize;
+ TDuration MaxBatchDelay;
+
+ int MaxFlushRowCount;
+ i64 MaxFlushDataSize;
+
+ bool PreferLocalHost;
+
+ TDuration NodeRpcTimeout;
+ TDuration NodePingPeriod;
+ TDuration NodeBanTimeout;
+
+ NRpc::TRetryingChannelConfigPtr NodeChannel;
+
+ // For testing purposes only.
+ double ReplicaFailureProbability;
+
+ //! After writing #ReplicaRowLimits[index] rows to replica #index
+ //! request will fail with timeout after #ReplicaFakeTimeoutDelay
+ //! but rows will be actually written.
+ std::optional<std::vector<int>> ReplicaRowLimits;
+ TDuration ReplicaFakeTimeoutDelay;
+
+ REGISTER_YSON_STRUCT(TJournalChunkWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJournalChunkWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalWriterConfig
+ : public TJournalChunkWriterConfig
+{
+public:
+ int MaxChunkRowCount;
+ i64 MaxChunkDataSize;
+ TDuration MaxChunkSessionDuration;
+
+ TDuration OpenSessionBackoffTime;
+
+ TDuration PrerequisiteTransactionProbePeriod;
+
+ // For testing purposes only.
+ bool DontClose;
+ bool DontSeal;
+ bool DontPreallocate;
+
+ std::optional<TDuration> OpenDelay;
+
+ REGISTER_YSON_STRUCT(TJournalWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJournalWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalChunkWriterOptions
+ : public NYTree::TYsonStruct
+{
+public:
+ int ReplicationFactor;
+ NErasure::ECodec ErasureCodec;
+
+ int ReadQuorum;
+ int WriteQuorum;
+
+ int ReplicaLagLimit;
+
+ bool EnableMultiplexing;
+
+ REGISTER_YSON_STRUCT(TJournalChunkWriterOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJournalChunkWriterOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/connection.h b/yt/yt/client/api/connection.h
new file mode 100644
index 0000000000..8c59c70319
--- /dev/null
+++ b/yt/yt/client/api/connection.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/chunk_client/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/client/security_client/public.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/client/hive/public.h>
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/rpc/authentication_identity.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConnectionOptions
+{
+ //! If non-null, suppresses creation of a per-connection thread pool.
+ IInvokerPtr ConnectionInvoker;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTransactionParticipantOptions
+{
+ TDuration RpcTimeout = TDuration::Seconds(5);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents an established connection with a YT cluster.
+/*
+ * IConnection instance caches most of the stuff needed for fast interaction
+ * with the cluster (e.g. connection channels, mount info etc).
+ *
+ * Thread affinity: any
+ */
+struct IConnection
+ : public virtual TRefCounted
+{
+ virtual TClusterTag GetClusterTag() const = 0;
+ virtual const TString& GetLoggingTag() const = 0;
+ virtual const TString& GetClusterId() const = 0;
+ virtual const std::optional<TString>& GetClusterName() const = 0;
+ virtual IInvokerPtr GetInvoker() = 0;
+
+ // TODO(gritukan): Fix alien transaction creation for RPC proxy connection
+ // and eliminate this method.
+ virtual bool IsSameCluster(const IConnectionPtr& other) const = 0;
+
+ virtual IClientPtr CreateClient(const TClientOptions& options = {}) = 0;
+ virtual NHiveClient::ITransactionParticipantPtr CreateTransactionParticipant(
+ NHiveClient::TCellId cellId,
+ const TTransactionParticipantOptions& options = TTransactionParticipantOptions()) = 0;
+
+ virtual void ClearMetadataCaches() = 0;
+ virtual void Terminate() = 0;
+
+ //! Returns a YSON-serialized connection config.
+ virtual NYson::TYsonString GetConfigYson() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/delegating_client.cpp b/yt/yt/client/api/delegating_client.cpp
new file mode 100644
index 0000000000..f379b27044
--- /dev/null
+++ b/yt/yt/client/api/delegating_client.cpp
@@ -0,0 +1,993 @@
+#include "delegating_client.h"
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDelegatingClient::TDelegatingClient(IClientPtr underlying)
+ : Underlying_(std::move(underlying))
+{ }
+
+IConnectionPtr TDelegatingClient::GetConnection()
+{
+ return Underlying_->GetConnection();
+}
+
+std::optional<TStringBuf> TDelegatingClient::GetClusterName(bool fetchIfNull)
+{
+ return Underlying_->GetClusterName(fetchIfNull);
+}
+
+TFuture<ITransactionPtr> TDelegatingClient::StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options)
+{
+ return Underlying_->StartTransaction(type, options);
+}
+
+TFuture<IUnversionedRowsetPtr> TDelegatingClient::LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options)
+{
+ return Underlying_->LookupRows(path, std::move(nameTable), keys, options);
+}
+
+TFuture<IVersionedRowsetPtr> TDelegatingClient::VersionedLookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options)
+{
+ return Underlying_->VersionedLookupRows(path, std::move(nameTable), keys, options);
+}
+
+TFuture<std::vector<IUnversionedRowsetPtr>> TDelegatingClient::MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options)
+{
+ return Underlying_->MultiLookup(subrequests, options);
+}
+
+TFuture<TSelectRowsResult> TDelegatingClient::SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options)
+{
+ return Underlying_->SelectRows(query, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::ExplainQuery(
+ const TString& query,
+ const TExplainQueryOptions& options)
+{
+ return Underlying_->ExplainQuery(query, options);
+}
+
+TFuture<TPullRowsResult> TDelegatingClient::PullRows(
+ const NYPath::TYPath& path,
+ const TPullRowsOptions& options)
+{
+ return Underlying_->PullRows(path, options);
+}
+
+TFuture<ITableReaderPtr> TDelegatingClient::CreateTableReader(
+ const NYPath::TRichYPath& path,
+ const TTableReaderOptions& options)
+{
+ return Underlying_->CreateTableReader(path, options);
+}
+
+TFuture<ITableWriterPtr> TDelegatingClient::CreateTableWriter(
+ const NYPath::TRichYPath& path,
+ const TTableWriterOptions& options)
+{
+ return Underlying_->CreateTableWriter(path, options);
+}
+
+TFuture<NQueueClient::IQueueRowsetPtr> TDelegatingClient::PullQueue(
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options)
+{
+ return Underlying_->PullQueue(queuePath, offset, partitionIndex, rowBatchReadOptions, options);
+}
+
+TFuture<NQueueClient::IQueueRowsetPtr> TDelegatingClient::PullConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options)
+{
+ return Underlying_->PullConsumer(consumerPath, queuePath, offset, partitionIndex, rowBatchReadOptions, options);
+}
+
+TFuture<void> TDelegatingClient::RegisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options)
+{
+ return Underlying_->RegisterQueueConsumer(queuePath, consumerPath, vital, options);
+}
+
+TFuture<void> TDelegatingClient::UnregisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options)
+{
+ return Underlying_->UnregisterQueueConsumer(queuePath, consumerPath, options);
+}
+
+TFuture<std::vector<TListQueueConsumerRegistrationsResult>> TDelegatingClient::ListQueueConsumerRegistrations(
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options)
+{
+ return Underlying_->ListQueueConsumerRegistrations(queuePath, consumerPath, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::GetNode(
+ const NYPath::TYPath& path,
+ const TGetNodeOptions& options)
+{
+ return Underlying_->GetNode(path, options);
+}
+
+TFuture<void> TDelegatingClient::SetNode(
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options)
+{
+ return Underlying_->SetNode(path, value, options);
+}
+
+TFuture<void> TDelegatingClient::MultisetAttributesNode(
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options)
+{
+ return Underlying_->MultisetAttributesNode(path, attributes, options);
+}
+
+TFuture<void> TDelegatingClient::RemoveNode(
+ const NYPath::TYPath& path,
+ const TRemoveNodeOptions& options)
+{
+ return Underlying_->RemoveNode(path, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::ListNode(
+ const NYPath::TYPath& path,
+ const TListNodeOptions& options)
+{
+ return Underlying_->ListNode(path, options);
+}
+
+TFuture<NCypressClient::TNodeId> TDelegatingClient::CreateNode(
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options)
+{
+ return Underlying_->CreateNode(path, type, options);
+}
+
+TFuture<TLockNodeResult> TDelegatingClient::LockNode(
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options)
+{
+ return Underlying_->LockNode(path, mode, options);
+}
+
+TFuture<void> TDelegatingClient::UnlockNode(
+ const NYPath::TYPath& path,
+ const TUnlockNodeOptions& options)
+{
+ return Underlying_->UnlockNode(path, options);
+}
+
+TFuture<NCypressClient::TNodeId> TDelegatingClient::CopyNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TCopyNodeOptions& options)
+{
+ return Underlying_->CopyNode(srcPath, dstPath, options);
+}
+
+TFuture<NCypressClient::TNodeId> TDelegatingClient::MoveNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TMoveNodeOptions& options)
+{
+ return Underlying_->MoveNode(srcPath, dstPath, options);
+}
+
+TFuture<NCypressClient::TNodeId> TDelegatingClient::LinkNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TLinkNodeOptions& options)
+{
+ return Underlying_->LinkNode(srcPath, dstPath, options);
+}
+
+TFuture<void> TDelegatingClient::ConcatenateNodes(
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options)
+{
+ return Underlying_->ConcatenateNodes(srcPaths, dstPath, options);
+}
+
+TFuture<bool> TDelegatingClient::NodeExists(
+ const NYPath::TYPath& path,
+ const TNodeExistsOptions& options)
+{
+ return Underlying_->NodeExists(path, options);
+}
+
+TFuture<void> TDelegatingClient::ExternalizeNode(
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options)
+{
+ return Underlying_->ExternalizeNode(path, cellTag, options);
+}
+
+TFuture<void> TDelegatingClient::InternalizeNode(
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options)
+{
+ return Underlying_->InternalizeNode(path, options);
+}
+
+TFuture<NObjectClient::TObjectId> TDelegatingClient::CreateObject(
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options)
+{
+ return Underlying_->CreateObject(type, options);
+}
+
+TFuture<IFileReaderPtr> TDelegatingClient::CreateFileReader(
+ const NYPath::TYPath& path,
+ const TFileReaderOptions& options)
+{
+ return Underlying_->CreateFileReader(path, options);
+}
+
+IFileWriterPtr TDelegatingClient::CreateFileWriter(
+ const NYPath::TRichYPath& path,
+ const TFileWriterOptions& options)
+{
+ return Underlying_->CreateFileWriter(path, options);
+}
+
+IJournalReaderPtr TDelegatingClient::CreateJournalReader(
+ const NYPath::TYPath& path,
+ const TJournalReaderOptions& options)
+{
+ return Underlying_->CreateJournalReader(path, options);
+}
+
+IJournalWriterPtr TDelegatingClient::CreateJournalWriter(
+ const NYPath::TYPath& path,
+ const TJournalWriterOptions& options)
+{
+ return Underlying_->CreateJournalWriter(path, options);
+}
+
+void TDelegatingClient::Terminate()
+{
+ return Underlying_->Terminate();
+}
+
+const NTabletClient::ITableMountCachePtr& TDelegatingClient::GetTableMountCache()
+{
+ return Underlying_->GetTableMountCache();
+}
+const NChaosClient::IReplicationCardCachePtr& TDelegatingClient::GetReplicationCardCache()
+{
+ return Underlying_->GetReplicationCardCache();
+}
+const NTransactionClient::ITimestampProviderPtr& TDelegatingClient::GetTimestampProvider() {
+ return Underlying_->GetTimestampProvider();
+}
+
+ITransactionPtr TDelegatingClient::AttachTransaction(
+ NTransactionClient::TTransactionId transactionId,
+ const TTransactionAttachOptions& options)
+{
+ return Underlying_->AttachTransaction(transactionId, options);
+}
+
+TFuture<void> TDelegatingClient::MountTable(
+ const NYPath::TYPath& path,
+ const TMountTableOptions& options)
+{
+ return Underlying_->MountTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::UnmountTable(
+ const NYPath::TYPath& path,
+ const TUnmountTableOptions& options)
+{
+ return Underlying_->UnmountTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::RemountTable(
+ const NYPath::TYPath& path,
+ const TRemountTableOptions& options)
+{
+ return Underlying_->RemountTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::FreezeTable(
+ const NYPath::TYPath& path,
+ const TFreezeTableOptions& options)
+{
+ return Underlying_->FreezeTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::UnfreezeTable(
+ const NYPath::TYPath& path,
+ const TUnfreezeTableOptions& options)
+{
+ return Underlying_->UnfreezeTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::ReshardTable(
+ const NYPath::TYPath& path,
+ const std::vector<NTableClient::TLegacyOwningKey>& pivotKeys,
+ const TReshardTableOptions& options)
+{
+ return Underlying_->ReshardTable(path, pivotKeys, options);
+}
+
+TFuture<void> TDelegatingClient::ReshardTable(
+ const NYPath::TYPath& path,
+ int tabletCount,
+ const TReshardTableOptions& options)
+{
+ return Underlying_->ReshardTable(path, tabletCount, options);
+}
+
+TFuture<std::vector<NTabletClient::TTabletActionId>> TDelegatingClient::ReshardTableAutomatic(
+ const NYPath::TYPath& path,
+ const TReshardTableAutomaticOptions& options)
+{
+ return Underlying_->ReshardTableAutomatic(path, options);
+}
+
+TFuture<void> TDelegatingClient::TrimTable(
+ const NYPath::TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const TTrimTableOptions& options)
+{
+ return Underlying_->TrimTable(path, tabletIndex, trimmedRowCount, options);
+}
+
+TFuture<void> TDelegatingClient::AlterTable(
+ const NYPath::TYPath& path,
+ const TAlterTableOptions& options)
+{
+ return Underlying_->AlterTable(path, options);
+}
+
+TFuture<void> TDelegatingClient::AlterTableReplica(
+ NTabletClient::TTableReplicaId replicaId,
+ const TAlterTableReplicaOptions& options)
+{
+ return Underlying_->AlterTableReplica(replicaId, options);
+}
+
+TFuture<void> TDelegatingClient::AlterReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options)
+{
+ return Underlying_->AlterReplicationCard(replicationCardId, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::GetTablePivotKeys(
+ const NYPath::TYPath& path,
+ const TGetTablePivotKeysOptions& options)
+{
+ return Underlying_->GetTablePivotKeys(path, options);
+}
+
+TFuture<void> TDelegatingClient::CreateTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TCreateTableBackupOptions& options)
+{
+ return Underlying_->CreateTableBackup(manifest, options);
+}
+
+TFuture<void> TDelegatingClient::RestoreTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TRestoreTableBackupOptions& options)
+{
+ return Underlying_->RestoreTableBackup(manifest, options);
+}
+
+TFuture<std::vector<NTabletClient::TTableReplicaId>> TDelegatingClient::GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const NTableClient::TNameTablePtr& nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TGetInSyncReplicasOptions& options)
+{
+ return Underlying_->GetInSyncReplicas(path, nameTable, keys, options);
+}
+
+TFuture<std::vector<NTabletClient::TTableReplicaId>> TDelegatingClient::GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const TGetInSyncReplicasOptions& options)
+{
+ return Underlying_->GetInSyncReplicas(path, options);
+}
+
+TFuture<std::vector<TTabletInfo>> TDelegatingClient::GetTabletInfos(
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options)
+{
+ return Underlying_->GetTabletInfos(path, tabletIndexes, options);
+}
+
+TFuture<TGetTabletErrorsResult> TDelegatingClient::GetTabletErrors(
+ const NYPath::TYPath& path,
+ const TGetTabletErrorsOptions& options)
+{
+ return Underlying_->GetTabletErrors(path, options);
+}
+
+TFuture<std::vector<NTabletClient::TTabletActionId>> TDelegatingClient::BalanceTabletCells(
+ const TString& tabletCellBundle,
+ const std::vector<NYPath::TYPath>& movableTables,
+ const TBalanceTabletCellsOptions& options)
+{
+ return Underlying_->BalanceTabletCells(tabletCellBundle, movableTables, options);
+}
+
+TFuture<NChaosClient::TReplicationCardPtr> TDelegatingClient::GetReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TGetReplicationCardOptions& options)
+{
+ return Underlying_->GetReplicationCard(replicationCardId, options);
+}
+
+TFuture<void> TDelegatingClient::UpdateChaosTableReplicaProgress(
+ NChaosClient::TReplicaId replicaId,
+ const TUpdateChaosTableReplicaProgressOptions& options)
+{
+ return Underlying_->UpdateChaosTableReplicaProgress(replicaId, options);
+}
+
+TFuture<TSkynetSharePartsLocationsPtr> TDelegatingClient::LocateSkynetShare(
+ const NYPath::TRichYPath& path,
+ const TLocateSkynetShareOptions& options)
+{
+ return Underlying_->LocateSkynetShare(path, options);
+}
+
+TFuture<std::vector<NTableClient::TColumnarStatistics>> TDelegatingClient::GetColumnarStatistics(
+ const std::vector<NYPath::TRichYPath>& path,
+ const TGetColumnarStatisticsOptions& options)
+{
+ return Underlying_->GetColumnarStatistics(path, options);
+}
+
+TFuture<TMultiTablePartitions> TDelegatingClient::PartitionTables(
+ const std::vector<NYPath::TRichYPath>& paths,
+ const TPartitionTablesOptions& options)
+{
+ return Underlying_->PartitionTables(paths, options);
+}
+
+TFuture<void> TDelegatingClient::TruncateJournal(
+ const NYPath::TYPath& path,
+ i64 rowCount,
+ const TTruncateJournalOptions& options)
+{
+ return Underlying_->TruncateJournal(path, rowCount, options);
+}
+
+TFuture<TGetFileFromCacheResult> TDelegatingClient::GetFileFromCache(
+ const TString& md5,
+ const TGetFileFromCacheOptions& options)
+{
+ return Underlying_->GetFileFromCache(md5, options);
+}
+
+TFuture<TPutFileToCacheResult> TDelegatingClient::PutFileToCache(
+ const NYPath::TYPath& path,
+ const TString& expectedMD5,
+ const TPutFileToCacheOptions& options)
+{
+ return Underlying_->PutFileToCache(path, expectedMD5, options);
+}
+
+TFuture<void> TDelegatingClient::AddMember(
+ const TString& group,
+ const TString& member,
+ const TAddMemberOptions& options)
+{
+ return Underlying_->AddMember(group, member, options);
+}
+
+TFuture<void> TDelegatingClient::RemoveMember(
+ const TString& group,
+ const TString& member,
+ const TRemoveMemberOptions& options)
+{
+ return Underlying_->RemoveMember(group, member, options);
+}
+
+TFuture<TCheckPermissionResponse> TDelegatingClient::CheckPermission(
+ const TString& user,
+ const NYPath::TYPath& path,
+ NYTree::EPermission permission,
+ const TCheckPermissionOptions& options)
+{
+ return Underlying_->CheckPermission(user, path, permission, options);
+}
+
+TFuture<TCheckPermissionByAclResult> TDelegatingClient::CheckPermissionByAcl(
+ const std::optional<TString>& user,
+ NYTree::EPermission permission,
+ NYTree::INodePtr acl,
+ const TCheckPermissionByAclOptions& options)
+{
+ return Underlying_->CheckPermissionByAcl(user, permission, std::move(acl), options);
+}
+
+TFuture<void> TDelegatingClient::TransferAccountResources(
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options)
+{
+ return Underlying_->TransferAccountResources(
+ srcAccount, dstAccount, std::move(resourceDelta), options
+ );
+}
+
+TFuture<void> TDelegatingClient::TransferPoolResources(
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options)
+{
+ return Underlying_->TransferPoolResources(
+ srcPool, dstPool, poolTree, std::move(resourceDelta), options
+ );
+}
+
+TFuture<NScheduler::TOperationId> TDelegatingClient::StartOperation(
+ NScheduler::EOperationType type,
+ const NYson::TYsonString& spec,
+ const TStartOperationOptions& options)
+{
+ return Underlying_->StartOperation(type, spec, options);
+}
+
+TFuture<void> TDelegatingClient::AbortOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TAbortOperationOptions& options)
+{
+ return Underlying_->AbortOperation(operationIdOrAlias, options);
+}
+
+TFuture<void> TDelegatingClient::SuspendOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TSuspendOperationOptions& options)
+{
+ return Underlying_->SuspendOperation(operationIdOrAlias, options);
+}
+
+TFuture<void> TDelegatingClient::ResumeOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TResumeOperationOptions& options)
+{
+ return Underlying_->ResumeOperation(operationIdOrAlias, options);
+}
+
+TFuture<void> TDelegatingClient::CompleteOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TCompleteOperationOptions& options)
+{
+ return Underlying_->CompleteOperation(operationIdOrAlias, options);
+}
+
+TFuture<void> TDelegatingClient::UpdateOperationParameters(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NYson::TYsonString& parameters,
+ const TUpdateOperationParametersOptions& options)
+{
+ return Underlying_->UpdateOperationParameters(operationIdOrAlias, parameters, options);
+}
+
+TFuture<TOperation> TDelegatingClient::GetOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TGetOperationOptions& options)
+{
+ return Underlying_->GetOperation(operationIdOrAlias, options);
+}
+
+TFuture<void> TDelegatingClient::DumpJobContext(
+ NJobTrackerClient::TJobId jobId,
+ const NYPath::TYPath& path,
+ const TDumpJobContextOptions& options)
+{
+ return Underlying_->DumpJobContext(jobId, path, options);
+}
+
+TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> TDelegatingClient::GetJobInput(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputOptions& options)
+{
+ return Underlying_->GetJobInput(jobId, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::GetJobInputPaths(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputPathsOptions& options)
+{
+ return Underlying_->GetJobInputPaths(jobId, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::GetJobSpec(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobSpecOptions& options)
+{
+ return Underlying_->GetJobSpec(jobId, options);
+}
+
+TFuture<TSharedRef> TDelegatingClient::GetJobStderr(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobStderrOptions& options)
+{
+ return Underlying_->GetJobStderr(operationIdOrAlias, jobId, options);
+}
+
+TFuture<TSharedRef> TDelegatingClient::GetJobFailContext(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobFailContextOptions& options)
+{
+ return Underlying_->GetJobFailContext(operationIdOrAlias, jobId, options);
+}
+
+TFuture<TListOperationsResult> TDelegatingClient::ListOperations(
+ const TListOperationsOptions& options)
+{
+ return Underlying_->ListOperations(options);
+}
+
+TFuture<TListJobsResult> TDelegatingClient::ListJobs(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TListJobsOptions& options)
+{
+ return Underlying_->ListJobs(operationIdOrAlias, options);
+}
+
+TFuture<NYson::TYsonString> TDelegatingClient::GetJob(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobOptions& options)
+{
+ return Underlying_->GetJob(operationIdOrAlias, jobId, options);
+}
+
+TFuture<void> TDelegatingClient::AbandonJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbandonJobOptions& options)
+{
+ return Underlying_->AbandonJob(jobId, options);
+}
+
+TFuture<TPollJobShellResponse> TDelegatingClient::PollJobShell(
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const NYson::TYsonString& parameters,
+ const TPollJobShellOptions& options)
+{
+ return Underlying_->PollJobShell(jobId, shellName, parameters, options);
+}
+
+TFuture<void> TDelegatingClient::AbortJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbortJobOptions& options)
+{
+ return Underlying_->AbortJob(jobId, options);
+}
+
+TFuture<TClusterMeta> TDelegatingClient::GetClusterMeta(
+ const TGetClusterMetaOptions& options)
+{
+ return Underlying_->GetClusterMeta(options);
+}
+
+TFuture<void> TDelegatingClient::CheckClusterLiveness(
+ const TCheckClusterLivenessOptions& options)
+{
+ return Underlying_->CheckClusterLiveness(options);
+}
+
+TFuture<int> TDelegatingClient::BuildSnapshot(
+ const TBuildSnapshotOptions& options)
+{
+ return Underlying_->BuildSnapshot(options);
+}
+
+TFuture<TCellIdToSnapshotIdMap> TDelegatingClient::BuildMasterSnapshots(
+ const TBuildMasterSnapshotsOptions& options)
+{
+ return Underlying_->BuildMasterSnapshots(options);
+}
+
+TFuture<void> TDelegatingClient::SwitchLeader(
+ NHydra::TCellId cellId,
+ const TString& newLeaderAddress,
+ const TSwitchLeaderOptions& options)
+{
+ return Underlying_->SwitchLeader(cellId, newLeaderAddress, options);
+}
+
+TFuture<void> TDelegatingClient::ResetStateHash(
+ NHydra::TCellId cellId,
+ const TResetStateHashOptions& options)
+{
+ return Underlying_->ResetStateHash(cellId, options);
+}
+
+TFuture<void> TDelegatingClient::GCCollect(
+ const TGCCollectOptions& options)
+{
+ return Underlying_->GCCollect(options);
+}
+
+TFuture<void> TDelegatingClient::KillProcess(
+ const TString& address,
+ const TKillProcessOptions& options)
+{
+ return Underlying_->KillProcess(address, options);
+}
+
+TFuture<TString> TDelegatingClient::WriteCoreDump(
+ const TString& address,
+ const TWriteCoreDumpOptions& options)
+{
+ return Underlying_->WriteCoreDump(address, options);
+}
+
+TFuture<TGuid> TDelegatingClient::WriteLogBarrier(
+ const TString& address,
+ const TWriteLogBarrierOptions& options)
+{
+ return Underlying_->WriteLogBarrier(address, options);
+}
+
+TFuture<TString> TDelegatingClient::WriteOperationControllerCoreDump(
+ NJobTrackerClient::TOperationId operationId,
+ const TWriteOperationControllerCoreDumpOptions& options)
+{
+ return Underlying_->WriteOperationControllerCoreDump(operationId, options);
+}
+
+TFuture<void> TDelegatingClient::HealExecNode(
+ const TString& address,
+ const THealExecNodeOptions& options)
+{
+ return Underlying_->HealExecNode(address, options);
+}
+
+TFuture<void> TDelegatingClient::SuspendCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TSuspendCoordinatorOptions& options)
+{
+ return Underlying_->SuspendCoordinator(coordinatorCellId, options);
+}
+
+TFuture<void> TDelegatingClient::ResumeCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TResumeCoordinatorOptions& options)
+{
+ return Underlying_->ResumeCoordinator(coordinatorCellId, options);
+}
+
+TFuture<void> TDelegatingClient::MigrateReplicationCards(
+ NObjectClient::TCellId chaosCellId,
+ const TMigrateReplicationCardsOptions& options)
+{
+ return Underlying_->MigrateReplicationCards(chaosCellId, options);
+}
+
+TFuture<void> TDelegatingClient::SuspendChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& options)
+{
+ return Underlying_->SuspendChaosCells(cellIds, options);
+}
+
+TFuture<void> TDelegatingClient::ResumeChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeChaosCellsOptions& options)
+{
+ return Underlying_->ResumeChaosCells(cellIds, options);
+}
+
+TFuture<void> TDelegatingClient::SuspendTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendTabletCellsOptions& options)
+{
+ return Underlying_->SuspendTabletCells(cellIds, options);
+}
+
+TFuture<void> TDelegatingClient::ResumeTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeTabletCellsOptions& options)
+{
+ return Underlying_->ResumeTabletCells(cellIds, options);
+}
+
+TFuture<TMaintenanceId> TDelegatingClient::AddMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options)
+{
+ return Underlying_->AddMaintenance(component, address, type, comment, options);
+}
+
+TFuture<TMaintenanceCounts> TDelegatingClient::RemoveMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options)
+{
+ return Underlying_->RemoveMaintenance(component, address, filter, options);
+}
+
+TFuture<TDisableChunkLocationsResult> TDelegatingClient::DisableChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& options)
+{
+ return Underlying_->DisableChunkLocations(nodeAddress, locationUuids, options);
+}
+
+TFuture<TDestroyChunkLocationsResult> TDelegatingClient::DestroyChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& options)
+{
+ return Underlying_->DestroyChunkLocations(nodeAddress, locationUuids, options);
+}
+
+TFuture<TResurrectChunkLocationsResult> TDelegatingClient::ResurrectChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& options)
+{
+ return Underlying_->ResurrectChunkLocations(nodeAddress, locationUuids, options);
+}
+
+TFuture<void> TDelegatingClient::SetUserPassword(
+ const TString& user,
+ const TString& currentPasswordSha256,
+ const TString& newPasswordSha256,
+ const TSetUserPasswordOptions& options)
+{
+ return Underlying_->SetUserPassword(
+ user,
+ currentPasswordSha256,
+ newPasswordSha256,
+ options);
+}
+
+TFuture<TIssueTokenResult> TDelegatingClient::IssueToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TIssueTokenOptions& options)
+{
+ return Underlying_->IssueToken(
+ user,
+ passwordSha256,
+ options);
+}
+
+TFuture<void> TDelegatingClient::RevokeToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TString& tokenSha256,
+ const TRevokeTokenOptions& options)
+{
+ return Underlying_->RevokeToken(
+ user,
+ passwordSha256,
+ tokenSha256,
+ options);
+}
+
+TFuture<TListUserTokensResult> TDelegatingClient::ListUserTokens(
+ const TString& user,
+ const TString& passwordSha256,
+ const TListUserTokensOptions& options)
+{
+ return Underlying_->ListUserTokens(
+ user,
+ passwordSha256,
+ options);
+}
+
+TFuture<NQueryTrackerClient::TQueryId> TDelegatingClient::StartQuery(
+ NQueryTrackerClient::EQueryEngine engine,
+ const TString& query,
+ const TStartQueryOptions& options)
+{
+ return Underlying_->StartQuery(engine, query, options);
+}
+
+TFuture<void> TDelegatingClient::AbortQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAbortQueryOptions& options)
+{
+ return Underlying_->AbortQuery(queryId, options);
+}
+
+TFuture<TQueryResult> TDelegatingClient::GetQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TGetQueryResultOptions& options)
+{
+ return Underlying_->GetQueryResult(queryId, resultIndex, options);
+}
+
+TFuture<IUnversionedRowsetPtr> TDelegatingClient::ReadQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TReadQueryResultOptions& options)
+{
+ return Underlying_->ReadQueryResult(queryId, resultIndex, options);
+}
+
+TFuture<TQuery> TDelegatingClient::GetQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TGetQueryOptions& options)
+{
+ return Underlying_->GetQuery(queryId, options);
+}
+
+TFuture<TListQueriesResult> TDelegatingClient::ListQueries(const TListQueriesOptions& options)
+{
+ return Underlying_->ListQueries(options);
+}
+
+TFuture<void> TDelegatingClient::AlterQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAlterQueryOptions& options)
+{
+ return Underlying_->AlterQuery(queryId, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Method below ensures that delegating client contains implementations for all
+// methods of IClient. Tthis reduces the number of PR iterations you need to
+// find that some out-of-yt/yt implementation of IClient does not compile.
+void InstantiateDelegatingClient()
+{
+ auto delegatingClient = New<TDelegatingClient>(/*client*/ nullptr);
+ Y_UNUSED(delegatingClient);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/delegating_client.h b/yt/yt/client/api/delegating_client.h
new file mode 100644
index 0000000000..b26d7e8721
--- /dev/null
+++ b/yt/yt/client/api/delegating_client.h
@@ -0,0 +1,613 @@
+#pragma once
+
+#include "client.h"
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A simple base class that implements IClient and delegates
+//! all calls to an underlying instance.
+class TDelegatingClient
+ : public IClient
+{
+public:
+ explicit TDelegatingClient(IClientPtr underlying);
+
+ // IClientBase methods
+
+ IConnectionPtr GetConnection() override;
+ std::optional<TStringBuf> GetClusterName(bool fetchIfNull = true) override;
+
+ // Transactions
+ TFuture<ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options = {}) override;
+
+ // Tables
+ TFuture<IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options = {}) override;
+
+ TFuture<IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options = {}) override;
+
+ TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options = {}) override;
+
+ TFuture<TSelectRowsResult> SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options = {}) override;
+
+ TFuture<NYson::TYsonString> ExplainQuery(
+ const TString& query,
+ const TExplainQueryOptions& options = {}) override;
+
+ TFuture<TPullRowsResult> PullRows(
+ const NYPath::TYPath& path,
+ const TPullRowsOptions& options = {}) override;
+
+ TFuture<ITableReaderPtr> CreateTableReader(
+ const NYPath::TRichYPath& path,
+ const TTableReaderOptions& options = {}) override;
+
+ TFuture<ITableWriterPtr> CreateTableWriter(
+ const NYPath::TRichYPath& path,
+ const TTableWriterOptions& options = {}) override;
+
+ // Queues
+ TFuture<NQueueClient::IQueueRowsetPtr> PullQueue(
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options = {}) override;
+
+ TFuture<NQueueClient::IQueueRowsetPtr> PullConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options = {}) override;
+
+ TFuture<void> RegisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options = {}) override;
+
+ TFuture<void> UnregisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options = {}) override;
+
+ TFuture<std::vector<TListQueueConsumerRegistrationsResult>> ListQueueConsumerRegistrations(
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options = {}) override;
+
+ // Cypress
+ TFuture<NYson::TYsonString> GetNode(
+ const NYPath::TYPath& path,
+ const TGetNodeOptions& options = {}) override;
+
+ TFuture<void> SetNode(
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options = {}) override;
+
+ TFuture<void> MultisetAttributesNode(
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options = {}) override;
+
+ TFuture<void> RemoveNode(
+ const NYPath::TYPath& path,
+ const TRemoveNodeOptions& options = {}) override;
+
+ TFuture<NYson::TYsonString> ListNode(
+ const NYPath::TYPath& path,
+ const TListNodeOptions& options = {}) override;
+
+ TFuture<NCypressClient::TNodeId> CreateNode(
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options = {}) override;
+
+ TFuture<TLockNodeResult> LockNode(
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options = {}) override;
+
+ TFuture<void> UnlockNode(
+ const NYPath::TYPath& path,
+ const TUnlockNodeOptions& options = {}) override;
+
+ TFuture<NCypressClient::TNodeId> CopyNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TCopyNodeOptions& options = {}) override;
+
+ TFuture<NCypressClient::TNodeId> MoveNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TMoveNodeOptions& options = {}) override;
+
+ TFuture<NCypressClient::TNodeId> LinkNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TLinkNodeOptions& options = {}) override;
+
+ TFuture<void> ConcatenateNodes(
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options = {}) override;
+
+ TFuture<bool> NodeExists(
+ const NYPath::TYPath& path,
+ const TNodeExistsOptions& options = {}) override;
+
+ TFuture<void> ExternalizeNode(
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options = {}) override;
+
+ TFuture<void> InternalizeNode(
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options = {}) override;
+
+ // Objects
+ TFuture<NObjectClient::TObjectId> CreateObject(
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options = {}) override;
+
+
+ // Files
+ TFuture<IFileReaderPtr> CreateFileReader(
+ const NYPath::TYPath& path,
+ const TFileReaderOptions& options = {}) override;
+
+ IFileWriterPtr CreateFileWriter(
+ const NYPath::TRichYPath& path,
+ const TFileWriterOptions& options = {}) override;
+
+ // Journals
+ IJournalReaderPtr CreateJournalReader(
+ const NYPath::TYPath& path,
+ const TJournalReaderOptions& options = {}) override;
+
+ IJournalWriterPtr CreateJournalWriter(
+ const NYPath::TYPath& path,
+ const TJournalWriterOptions& options = {}) override;
+
+ // IClient methods
+
+ void Terminate() override;
+
+ const NTabletClient::ITableMountCachePtr& GetTableMountCache() override;
+
+ const NChaosClient::IReplicationCardCachePtr& GetReplicationCardCache() override;
+
+ const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() override;
+
+ // Transactions
+ ITransactionPtr AttachTransaction(
+ NTransactionClient::TTransactionId transactionId,
+ const TTransactionAttachOptions& options = {}) override;
+
+ // Tables
+ TFuture<void> MountTable(
+ const NYPath::TYPath& path,
+ const TMountTableOptions& options = {}) override;
+
+ TFuture<void> UnmountTable(
+ const NYPath::TYPath& path,
+ const TUnmountTableOptions& options = {}) override;
+
+ TFuture<void> RemountTable(
+ const NYPath::TYPath& path,
+ const TRemountTableOptions& options = {}) override;
+
+ TFuture<void> FreezeTable(
+ const NYPath::TYPath& path,
+ const TFreezeTableOptions& options = {}) override;
+
+ TFuture<void> UnfreezeTable(
+ const NYPath::TYPath& path,
+ const TUnfreezeTableOptions& options = {}) override;
+
+ TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ const std::vector<NTableClient::TLegacyOwningKey>& pivotKeys,
+ const TReshardTableOptions& options = {}) override;
+
+ TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ int tabletCount,
+ const TReshardTableOptions& options = {}) override;
+
+ TFuture<std::vector<NTabletClient::TTabletActionId>> ReshardTableAutomatic(
+ const NYPath::TYPath& path,
+ const TReshardTableAutomaticOptions& options = {}) override;
+
+ TFuture<void> TrimTable(
+ const NYPath::TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const TTrimTableOptions& options = {}) override;
+
+ TFuture<void> AlterTable(
+ const NYPath::TYPath& path,
+ const TAlterTableOptions& options = {}) override;
+
+ TFuture<void> AlterTableReplica(
+ NTabletClient::TTableReplicaId replicaId,
+ const TAlterTableReplicaOptions& options = {}) override;
+
+ TFuture<void> AlterReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options = {}) override;
+
+ TFuture<NYson::TYsonString> GetTablePivotKeys(
+ const NYPath::TYPath& path,
+ const TGetTablePivotKeysOptions& options = {}) override;
+
+ TFuture<void> CreateTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TCreateTableBackupOptions& options = {}) override;
+
+ TFuture<void> RestoreTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TRestoreTableBackupOptions& options = {}) override;
+
+ TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const NTableClient::TNameTablePtr& nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TGetInSyncReplicasOptions& options = {}) override;
+
+ TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const TGetInSyncReplicasOptions& options = {}) override;
+
+ TFuture<std::vector<TTabletInfo>> GetTabletInfos(
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options = {}) override;
+
+ TFuture<TGetTabletErrorsResult> GetTabletErrors(
+ const NYPath::TYPath& path,
+ const TGetTabletErrorsOptions& options = {}) override;
+
+ TFuture<std::vector<NTabletClient::TTabletActionId>> BalanceTabletCells(
+ const TString& tabletCellBundle,
+ const std::vector<NYPath::TYPath>& movableTables,
+ const TBalanceTabletCellsOptions& options = {}) override;
+
+ TFuture<NChaosClient::TReplicationCardPtr> GetReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TGetReplicationCardOptions& options = {}) override;
+
+ TFuture<void> UpdateChaosTableReplicaProgress(
+ NChaosClient::TReplicaId replicaId,
+ const TUpdateChaosTableReplicaProgressOptions& options = {}) override;
+
+ TFuture<TSkynetSharePartsLocationsPtr> LocateSkynetShare(
+ const NYPath::TRichYPath& path,
+ const TLocateSkynetShareOptions& options = {}) override;
+
+ TFuture<std::vector<NTableClient::TColumnarStatistics>> GetColumnarStatistics(
+ const std::vector<NYPath::TRichYPath>& path,
+ const TGetColumnarStatisticsOptions& options = {}) override;
+
+ TFuture<TMultiTablePartitions> PartitionTables(
+ const std::vector<NYPath::TRichYPath>& paths,
+ const TPartitionTablesOptions& options) override;
+
+ // Journals
+ TFuture<void> TruncateJournal(
+ const NYPath::TYPath& path,
+ i64 rowCount,
+ const TTruncateJournalOptions& options = {}) override;
+
+ // Files
+ TFuture<TGetFileFromCacheResult> GetFileFromCache(
+ const TString& md5,
+ const TGetFileFromCacheOptions& options = {}) override;
+
+ TFuture<TPutFileToCacheResult> PutFileToCache(
+ const NYPath::TYPath& path,
+ const TString& expectedMD5,
+ const TPutFileToCacheOptions& options = {}) override;
+
+ // Security
+ TFuture<void> AddMember(
+ const TString& group,
+ const TString& member,
+ const TAddMemberOptions& options = {}) override;
+
+ TFuture<void> RemoveMember(
+ const TString& group,
+ const TString& member,
+ const TRemoveMemberOptions& options = {}) override;
+
+ TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ const NYPath::TYPath& path,
+ NYTree::EPermission permission,
+ const TCheckPermissionOptions& options = {}) override;
+
+ TFuture<TCheckPermissionByAclResult> CheckPermissionByAcl(
+ const std::optional<TString>& user,
+ NYTree::EPermission permission,
+ NYTree::INodePtr acl,
+ const TCheckPermissionByAclOptions& options = {}) override;
+
+ TFuture<void> TransferAccountResources(
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options = {}) override;
+
+ // Scheduler
+ TFuture<void> TransferPoolResources(
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options = {}) override;
+
+ TFuture<NScheduler::TOperationId> StartOperation(
+ NScheduler::EOperationType type,
+ const NYson::TYsonString& spec,
+ const TStartOperationOptions& options = {}) override;
+
+ TFuture<void> AbortOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TAbortOperationOptions& options = {}) override;
+
+ TFuture<void> SuspendOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TSuspendOperationOptions& options = {}) override;
+
+ TFuture<void> ResumeOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TResumeOperationOptions& options = {}) override;
+
+ TFuture<void> CompleteOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TCompleteOperationOptions& options = {}) override;
+
+ TFuture<void> UpdateOperationParameters(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NYson::TYsonString& parameters,
+ const TUpdateOperationParametersOptions& options = {}) override;
+
+ TFuture<TOperation> GetOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TGetOperationOptions& options = {}) override;
+
+ TFuture<void> DumpJobContext(
+ NJobTrackerClient::TJobId jobId,
+ const NYPath::TYPath& path,
+ const TDumpJobContextOptions& options = {}) override;
+
+ TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> GetJobInput(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputOptions& options = {}) override;
+
+ TFuture<NYson::TYsonString> GetJobInputPaths(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputPathsOptions& options = {}) override;
+
+ TFuture<NYson::TYsonString> GetJobSpec(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobSpecOptions& options = {}) override;
+
+ TFuture<TSharedRef> GetJobStderr(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobStderrOptions& options = {}) override;
+
+ TFuture<TSharedRef> GetJobFailContext(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobFailContextOptions& options = {}) override;
+
+ TFuture<TListOperationsResult> ListOperations(
+ const TListOperationsOptions& options = {}) override;
+
+ TFuture<TListJobsResult> ListJobs(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TListJobsOptions& options = {}
+ ) override;
+
+ TFuture<NYson::TYsonString> GetJob(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobOptions& options = {}) override;
+
+ TFuture<void> AbandonJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbandonJobOptions& options = {}) override;
+
+ TFuture<TPollJobShellResponse> PollJobShell(
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const NYson::TYsonString& parameters,
+ const TPollJobShellOptions& options = {}) override;
+
+ TFuture<void> AbortJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbortJobOptions& options = {}) override;
+
+ // Metadata
+ TFuture<TClusterMeta> GetClusterMeta(
+ const TGetClusterMetaOptions& options = {}) override;
+
+ TFuture<void> CheckClusterLiveness(
+ const TCheckClusterLivenessOptions& options = {}) override;
+
+ // Administration
+ TFuture<int> BuildSnapshot(
+ const TBuildSnapshotOptions& options = {}) override;
+
+ TFuture<TCellIdToSnapshotIdMap> BuildMasterSnapshots(
+ const TBuildMasterSnapshotsOptions& options = {}) override;
+
+ TFuture<void> SwitchLeader(
+ NHydra::TCellId cellId,
+ const TString& newLeaderAddress,
+ const TSwitchLeaderOptions& options = {}) override;
+
+ TFuture<void> ResetStateHash(
+ NHydra::TCellId cellId,
+ const TResetStateHashOptions& options = {}) override;
+
+ TFuture<void> GCCollect(
+ const TGCCollectOptions& options = {}) override;
+
+ TFuture<void> KillProcess(
+ const TString& address,
+ const TKillProcessOptions& options = {}) override;
+
+ TFuture<TString> WriteCoreDump(
+ const TString& address,
+ const TWriteCoreDumpOptions& options = {}) override;
+
+ TFuture<TGuid> WriteLogBarrier(
+ const TString& address,
+ const TWriteLogBarrierOptions& options = {}) override;
+
+ TFuture<TString> WriteOperationControllerCoreDump(
+ NJobTrackerClient::TOperationId operationId,
+ const TWriteOperationControllerCoreDumpOptions& options = {}) override;
+
+ TFuture<void> HealExecNode(
+ const TString& address,
+ const THealExecNodeOptions& options = {}) override;
+
+ TFuture<void> SuspendCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TSuspendCoordinatorOptions& options = {}) override;
+
+ TFuture<void> ResumeCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TResumeCoordinatorOptions& options = {}) override;
+
+ TFuture<void> MigrateReplicationCards(
+ NObjectClient::TCellId chaosCellId,
+ const TMigrateReplicationCardsOptions& options = {}) override;
+
+ TFuture<void> SuspendChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& options = {}) override;
+
+ TFuture<void> ResumeChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeChaosCellsOptions& options = {}) override;
+
+ TFuture<void> SuspendTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendTabletCellsOptions& options = {}) override;
+
+ TFuture<void> ResumeTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeTabletCellsOptions& options = {}) override;
+
+ TFuture<TMaintenanceId> AddMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options = {}) override;
+
+ TFuture<TMaintenanceCounts> RemoveMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options = {}) override;
+
+ TFuture<TDisableChunkLocationsResult> DisableChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& options = {}) override;
+
+ TFuture<TDestroyChunkLocationsResult> DestroyChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& options = {}) override;
+
+ TFuture<TResurrectChunkLocationsResult> ResurrectChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& options = {}) override;
+
+ TFuture<void> SetUserPassword(
+ const TString& user,
+ const TString& currentPasswordSha256,
+ const TString& newPasswordSha256,
+ const TSetUserPasswordOptions& options) override;
+
+ TFuture<TIssueTokenResult> IssueToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TIssueTokenOptions& options) override;
+
+ TFuture<void> RevokeToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TString& tokenSha256,
+ const TRevokeTokenOptions& options) override;
+
+ TFuture<TListUserTokensResult> ListUserTokens(
+ const TString& user,
+ const TString& passwordSha256,
+ const TListUserTokensOptions& options) override;
+
+ // Query tracker
+
+ TFuture<NQueryTrackerClient::TQueryId> StartQuery(
+ NQueryTrackerClient::EQueryEngine engine,
+ const TString& query,
+ const TStartQueryOptions& options) override;
+
+ TFuture<void> AbortQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAbortQueryOptions& options) override;
+
+ TFuture<TQueryResult> GetQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TGetQueryResultOptions& options) override;
+
+ TFuture<IUnversionedRowsetPtr> ReadQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TReadQueryResultOptions& options) override;
+
+ TFuture<TQuery> GetQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TGetQueryOptions& options) override;
+
+ TFuture<TListQueriesResult> ListQueries(const TListQueriesOptions& options) override;
+
+ TFuture<void> AlterQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAlterQueryOptions& options) override;
+
+protected:
+ const IClientPtr Underlying_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/file_reader.h b/yt/yt/client/api/file_reader.h
new file mode 100644
index 0000000000..bf4855fef3
--- /dev/null
+++ b/yt/yt/client/api/file_reader.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/hydra/public.h>
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IFileReader
+ : public NConcurrency::IAsyncZeroCopyInputStream
+{
+ //! Returns ID of file node.
+ virtual NObjectClient::TObjectId GetId() const = 0;
+
+ //! Returns revision of file node.
+ virtual NHydra::TRevision GetRevision() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFileReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/file_writer.h b/yt/yt/client/api/file_writer.h
new file mode 100644
index 0000000000..1e8742fcfe
--- /dev/null
+++ b/yt/yt/client/api/file_writer.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IFileWriter
+ : public virtual TRefCounted
+{
+ //! Opens the writer.
+ //! No other method can be called prior to the success of this one.
+ virtual TFuture<void> Open() = 0;
+
+ //! Writes the next portion of file data.
+ /*!
+ * #data must remain alive until this asynchronous operation completes.
+ */
+ virtual TFuture<void> Write(const TSharedRef& data) = 0;
+
+ //! Closes the writer.
+ //! No other method can be called after this one.
+ virtual TFuture<void> Close() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFileWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/helpers.cpp b/yt/yt/client/api/helpers.cpp
new file mode 100644
index 0000000000..ab2b06d7db
--- /dev/null
+++ b/yt/yt/client/api/helpers.cpp
@@ -0,0 +1,24 @@
+#include "helpers.h"
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateMaintenanceComment(const TString& comment)
+{
+ constexpr int MaxMaintenanceCommentLength = 512;
+
+ if (comment.size() > MaxMaintenanceCommentLength) {
+ THROW_ERROR_EXCEPTION("Maintenance comment is too long")
+ << TErrorAttribute("comment_length", comment.size())
+ << TErrorAttribute("max_comment_length", MaxMaintenanceCommentLength);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/helpers.h b/yt/yt/client/api/helpers.h
new file mode 100644
index 0000000000..d9eff87d4d
--- /dev/null
+++ b/yt/yt/client/api/helpers.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateMaintenanceComment(const TString& comment);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/internal_client.cpp b/yt/yt/client/api/internal_client.cpp
new file mode 100644
index 0000000000..2fb7da7506
--- /dev/null
+++ b/yt/yt/client/api/internal_client.cpp
@@ -0,0 +1,27 @@
+#include "internal_client.h"
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSerializableHunkDescriptor::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("chunk_id", &TThis::ChunkId);
+ registrar.BaseClassParameter("erasure_codec", &TThis::ErasureCodec)
+ .Optional();
+ registrar.BaseClassParameter("block_index", &TThis::BlockIndex);
+ registrar.BaseClassParameter("block_offset", &TThis::BlockOffset);
+ registrar.BaseClassParameter("block_size", &TThis::BlockSize)
+ .Optional();
+ registrar.BaseClassParameter("length", &TThis::Length);
+}
+
+TSerializableHunkDescriptor::TSerializableHunkDescriptor(const THunkDescriptor& descriptor)
+ : THunkDescriptor(descriptor)
+{
+ ::NYT::NYTree::TYsonStructRegistry::Get()->InitializeStruct(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/internal_client.h b/yt/yt/client/api/internal_client.h
new file mode 100644
index 0000000000..0c569021b9
--- /dev/null
+++ b/yt/yt/client/api/internal_client.h
@@ -0,0 +1,136 @@
+#pragma once
+
+#include "client.h"
+
+#include <yt/yt/client/chunk_client/public.h>
+
+#include <yt/yt/library/erasure/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THunkDescriptor
+{
+ NChunkClient::TChunkId ChunkId;
+ NErasure::ECodec ErasureCodec = NErasure::ECodec::None;
+ int BlockIndex = -1;
+ i64 BlockOffset = -1;
+ i64 Length = -1;
+ std::optional<i64> BlockSize;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializableHunkDescriptor
+ : public THunkDescriptor
+ , public NYTree::TYsonStruct
+{
+public:
+ TSerializableHunkDescriptor(const THunkDescriptor& descriptor);
+
+ REGISTER_YSON_STRUCT(TSerializableHunkDescriptor);
+
+ static void Register(TRegistrar registrar);
+};
+
+using TSerializableHunkDescriptorPtr = TIntrusivePtr<TSerializableHunkDescriptor>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReadHunksOptions
+ : public TTimeoutOptions
+{
+ NChunkClient::TChunkFragmentReaderConfigPtr Config;
+
+ //! If false, chunk fragment is returned as is.
+ bool ParseHeader;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWriteHunksOptions
+ : public TTimeoutOptions
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLockHunkStoreOptions
+ : public TTimeoutOptions
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TUnlockHunkStoreOptions
+ : public TTimeoutOptions
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGetOrderedTabletSafeTrimRowCountOptions
+ : public TTimeoutOptions
+{ };
+
+struct TGetOrderedTabletSafeTrimRowCountRequest
+{
+ NYTree::TYPath Path;
+ int TabletIndex;
+ NTransactionClient::TTimestamp Timestamp;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides a set of private APIs.
+/*!
+ * Only native clients are expected to implement this.
+ */
+struct IInternalClient
+ : public virtual TRefCounted
+{
+ virtual TFuture<std::vector<TSharedRef>> ReadHunks(
+ const std::vector<THunkDescriptor>& descriptors,
+ const TReadHunksOptions& options = {}) = 0;
+
+ virtual TFuture<std::vector<THunkDescriptor>> WriteHunks(
+ const NYTree::TYPath& path,
+ int tabletIndex,
+ const std::vector<TSharedRef>& payloads,
+ const TWriteHunksOptions& options = {}) = 0;
+
+ virtual TFuture<void> LockHunkStore(
+ const NYTree::TYPath& path,
+ int tabletIndex,
+ NTabletClient::TStoreId storeId,
+ NTabletClient::TTabletId lockerTabletId,
+ const TLockHunkStoreOptions& options = {}) = 0;
+ virtual TFuture<void> UnlockHunkStore(
+ const NYTree::TYPath& path,
+ int tabletIndex,
+ NTabletClient::TStoreId storeId,
+ NTabletClient::TTabletId lockerTabletId,
+ const TUnlockHunkStoreOptions& options = {}) = 0;
+
+ //! Same as NApi::IClient::PullQueue, but without authentication.
+ //! This is used inside methods like NApi::IClient::PullConsumer, which perform their own authentication
+ //! and allow reading from a queue without having read permissions for the underlying dynamic table.
+ virtual TFuture<NQueueClient::IQueueRowsetPtr> PullQueueUnauthenticated(
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options = {}) = 0;
+
+ //! For each request, finds and returns some index bounds in the given ordered tablet based on the given timestamp.
+ //! See response definition for details.
+ virtual TFuture<std::vector<TErrorOr<i64>>> GetOrderedTabletSafeTrimRowCount(
+ const std::vector<TGetOrderedTabletSafeTrimRowCountRequest>& requests,
+ const TGetOrderedTabletSafeTrimRowCountOptions& options = {}) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IInternalClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/journal_reader.h b/yt/yt/client/api/journal_reader.h
new file mode 100644
index 0000000000..f8b763b9a5
--- /dev/null
+++ b/yt/yt/client/api/journal_reader.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IJournalReader
+ : public virtual TRefCounted
+{
+ //! Opens the reader.
+ //! No other method can be called prior to the success of this one.
+ virtual TFuture<void> Open() = 0;
+
+ //! Reads another portion of the journal.
+ //! Each row is passed in its own TSharedRef.
+ //! When no more rows remain, an empty vector is returned.
+ //! One must not call #Read again before the previous call is complete.
+ virtual TFuture<std::vector<TSharedRef>> Read() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IJournalReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/journal_writer.h b/yt/yt/client/api/journal_writer.h
new file mode 100644
index 0000000000..bdfab47345
--- /dev/null
+++ b/yt/yt/client/api/journal_writer.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IJournalWriter
+ : public virtual TRefCounted
+{
+ //! Opens the writer.
+ //! No other method can be called prior to the success of this one.
+ virtual TFuture<void> Open() = 0;
+
+ //! Writes another portion of rows into the journal.
+ //! The result is set when the rows are successfully flushed by an appropriate number
+ //! of replicas.
+ virtual TFuture<void> Write(TRange<TSharedRef> rows) = 0;
+
+ //! Closes the writer.
+ //! No other method can be called after this one.
+ virtual TFuture<void> Close() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IJournalWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/operation_archive_schema.cpp b/yt/yt/client/api/operation_archive_schema.cpp
new file mode 100644
index 0000000000..86b3f6aa1d
--- /dev/null
+++ b/yt/yt/client/api/operation_archive_schema.cpp
@@ -0,0 +1,127 @@
+#include "operation_archive_schema.h"
+
+namespace NYT::NApi {
+
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOrderedByIdTableDescriptor::TOrderedByIdTableDescriptor()
+ : NameTable(New<TNameTable>())
+ , Index(NameTable)
+{ }
+
+const TOrderedByIdTableDescriptor& TOrderedByIdTableDescriptor::Get()
+{
+ static const TOrderedByIdTableDescriptor descriptor;
+ return descriptor;
+}
+
+TOrderedByIdTableDescriptor::TIndex::TIndex(const TNameTablePtr& nameTable)
+ : IdHash(nameTable->RegisterName("id_hash"))
+ , IdHi(nameTable->RegisterName("id_hi"))
+ , IdLo(nameTable->RegisterName("id_lo"))
+ , State(nameTable->RegisterName("state"))
+ , AuthenticatedUser(nameTable->RegisterName("authenticated_user"))
+ , OperationType(nameTable->RegisterName("operation_type"))
+ , Progress(nameTable->RegisterName("progress"))
+ , Spec(nameTable->RegisterName("spec"))
+ , BriefProgress(nameTable->RegisterName("brief_progress"))
+ , BriefSpec(nameTable->RegisterName("brief_spec"))
+ , StartTime(nameTable->RegisterName("start_time"))
+ , FinishTime(nameTable->RegisterName("finish_time"))
+ , FilterFactors(nameTable->RegisterName("filter_factors"))
+ , Result(nameTable->RegisterName("result"))
+ , Events(nameTable->RegisterName("events"))
+ , Alerts(nameTable->RegisterName("alerts"))
+ , SlotIndex(nameTable->RegisterName("slot_index"))
+ , UnrecognizedSpec(nameTable->RegisterName("unrecognized_spec"))
+ , FullSpec(nameTable->RegisterName("full_spec"))
+ , RuntimeParameters(nameTable->RegisterName("runtime_parameters"))
+ , SlotIndexPerPoolTree(nameTable->RegisterName("slot_index_per_pool_tree"))
+ , TaskNames(nameTable->RegisterName("task_names"))
+ , ExperimentAssignments(nameTable->RegisterName("experiment_assignments"))
+ , ExperimentAssignmentNames(nameTable->RegisterName("experiment_assignment_names"))
+ , ControllerFeatures(nameTable->RegisterName("controller_features"))
+ , AlertEvents(nameTable->RegisterName("alert_events"))
+ , ProvidedSpec(nameTable->RegisterName("provided_spec"))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOrderedByStartTimeTableDescriptor::TOrderedByStartTimeTableDescriptor()
+ : NameTable(New<TNameTable>())
+ , Index(NameTable)
+{ }
+
+const TOrderedByStartTimeTableDescriptor& TOrderedByStartTimeTableDescriptor::Get()
+{
+ static const TOrderedByStartTimeTableDescriptor descriptor;
+ return descriptor;
+}
+
+TOrderedByStartTimeTableDescriptor::TIndex::TIndex(const TNameTablePtr& nameTable)
+ : StartTime(nameTable->RegisterName("start_time"))
+ , IdHi(nameTable->RegisterName("id_hi"))
+ , IdLo(nameTable->RegisterName("id_lo"))
+ , OperationType(nameTable->RegisterName("operation_type"))
+ , State(nameTable->RegisterName("state"))
+ , AuthenticatedUser(nameTable->RegisterName("authenticated_user"))
+ , FilterFactors(nameTable->RegisterName("filter_factors"))
+ , Pool(nameTable->RegisterName("pool"))
+ , Pools(nameTable->RegisterName("pools"))
+ , HasFailedJobs(nameTable->RegisterName("has_failed_jobs"))
+ , Acl(nameTable->RegisterName("acl"))
+ , PoolTreeToPool(nameTable->RegisterName("pool_tree_to_pool"))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJobTableDescriptor::TJobTableDescriptor()
+ : NameTable(New<TNameTable>())
+ , Index(NameTable)
+{ }
+
+const TJobTableDescriptor& TJobTableDescriptor::Get()
+{
+ static const TJobTableDescriptor descriptor;
+ return descriptor;
+}
+
+TJobTableDescriptor::TIndex::TIndex(const TNameTablePtr& nameTable)
+ : OperationIdHi(nameTable->RegisterName("operation_id_hi"))
+ , OperationIdLo(nameTable->RegisterName("operation_id_lo"))
+ , JobIdHi(nameTable->RegisterName("job_id_hi"))
+ , JobIdLo(nameTable->RegisterName("job_id_lo"))
+ , Type(nameTable->RegisterName("type"))
+ , State(nameTable->RegisterName("state"))
+ , TransientState(nameTable->RegisterName("transient_state"))
+ , StartTime(nameTable->RegisterName("start_time"))
+ , FinishTime(nameTable->RegisterName("finish_time"))
+ , UpdateTime(nameTable->RegisterName("update_time"))
+ , Address(nameTable->RegisterName("address"))
+ , Error(nameTable->RegisterName("error"))
+ , Statistics(nameTable->RegisterName("statistics"))
+ , BriefStatistics(nameTable->RegisterName("brief_statistics"))
+ , StatisticsLz4(nameTable->RegisterName("statistics_lz4"))
+ , Events(nameTable->RegisterName("events"))
+ , StderrSize(nameTable->RegisterName("stderr_size"))
+ , HasSpec(nameTable->RegisterName("has_spec"))
+ , HasFailContext(nameTable->RegisterName("has_fail_context"))
+ , FailContextSize(nameTable->RegisterName("fail_context_size"))
+ , CoreInfos(nameTable->RegisterName("core_infos"))
+ , JobCompetitionId(nameTable->RegisterName("job_competition_id"))
+ , ProbingJobCompetitionId(nameTable->RegisterName("probing_job_competition_id"))
+ , HasCompetitors(nameTable->RegisterName("has_competitors"))
+ , HasProbingCompetitors(nameTable->RegisterName("has_probing_competitors"))
+ , ExecAttributes(nameTable->RegisterName("exec_attributes"))
+ , TaskName(nameTable->RegisterName("task_name"))
+ , PoolTree(nameTable->RegisterName("pool_tree"))
+ , MonitoringDescriptor(nameTable->RegisterName("monitoring_descriptor"))
+ , JobCookie(nameTable->RegisterName("job_cookie"))
+ , ControllerState(nameTable->RegisterName("controller_state"))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/operation_archive_schema.h b/yt/yt/client/api/operation_archive_schema.h
new file mode 100644
index 0000000000..ed5abd9ce3
--- /dev/null
+++ b/yt/yt/client/api/operation_archive_schema.h
@@ -0,0 +1,133 @@
+#pragma once
+
+#include <yt/yt/client/table_client/name_table.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TOrderedByIdTableDescriptor
+{
+ TOrderedByIdTableDescriptor();
+
+ static const TOrderedByIdTableDescriptor& Get();
+
+ struct TIndex
+ {
+ explicit TIndex(const NTableClient::TNameTablePtr& nameTable);
+
+ const int IdHash;
+ const int IdHi;
+ const int IdLo;
+ const int State;
+ const int AuthenticatedUser;
+ const int OperationType;
+ const int Progress;
+ const int Spec;
+ const int BriefProgress;
+ const int BriefSpec;
+ const int StartTime;
+ const int FinishTime;
+ const int FilterFactors;
+ const int Result;
+ const int Events;
+ const int Alerts;
+ const int SlotIndex; // TODO(renadeen): delete this column when version with this comment will be on every cluster
+ const int UnrecognizedSpec;
+ const int FullSpec;
+ const int RuntimeParameters;
+ const int SlotIndexPerPoolTree;
+ const int TaskNames;
+ const int ExperimentAssignments;
+ const int ExperimentAssignmentNames;
+ const int ControllerFeatures;
+ const int AlertEvents;
+ const int ProvidedSpec;
+ };
+
+ const NTableClient::TNameTablePtr NameTable;
+ const TIndex Index;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TOrderedByStartTimeTableDescriptor
+{
+ TOrderedByStartTimeTableDescriptor();
+
+ static const TOrderedByStartTimeTableDescriptor& Get();
+
+ struct TIndex
+ {
+ explicit TIndex(const NTableClient::TNameTablePtr& nameTable);
+
+ const int StartTime;
+ const int IdHi;
+ const int IdLo;
+ const int OperationType;
+ const int State;
+ const int AuthenticatedUser;
+ const int FilterFactors;
+ const int Pool;
+ const int Pools;
+ const int HasFailedJobs;
+ const int Acl;
+ const int PoolTreeToPool;
+ };
+
+ const NTableClient::TNameTablePtr NameTable;
+ const TIndex Index;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TJobTableDescriptor
+{
+ TJobTableDescriptor();
+
+ static const TJobTableDescriptor& Get();
+
+ struct TIndex
+ {
+ explicit TIndex(const NTableClient::TNameTablePtr& nameTable);
+
+ const int OperationIdHi;
+ const int OperationIdLo;
+ const int JobIdHi;
+ const int JobIdLo;
+ const int Type;
+ const int State;
+ const int TransientState;
+ const int StartTime;
+ const int FinishTime;
+ const int UpdateTime;
+ const int Address;
+ const int Error;
+ const int Statistics;
+ const int BriefStatistics;
+ const int StatisticsLz4;
+ const int Events;
+ const int StderrSize;
+ const int HasSpec;
+ const int HasFailContext;
+ const int FailContextSize;
+ const int CoreInfos;
+ const int JobCompetitionId;
+ const int ProbingJobCompetitionId;
+ const int HasCompetitors;
+ const int HasProbingCompetitors;
+ const int ExecAttributes;
+ const int TaskName;
+ const int PoolTree;
+ const int MonitoringDescriptor;
+ const int JobCookie;
+ const int ControllerState;
+ };
+
+ const NTableClient::TNameTablePtr NameTable;
+ const TIndex Index;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NApi
diff --git a/yt/yt/client/api/persistent_queue.cpp b/yt/yt/client/api/persistent_queue.cpp
new file mode 100644
index 0000000000..fa3274b616
--- /dev/null
+++ b/yt/yt/client/api/persistent_queue.cpp
@@ -0,0 +1,1054 @@
+#include "persistent_queue.h"
+#include "client.h"
+#include "transaction.h"
+#include "config.h"
+#include "private.h"
+
+#include <yt/yt/client/table_client/column_sort_schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+namespace NYT::NApi {
+
+using namespace NYPath;
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NTableClient;
+using namespace NObjectClient;
+using namespace NTransactionClient;
+using namespace NApi;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPersistentQueuePollerBufferTag
+{ };
+
+namespace {
+
+DEFINE_ENUM(ERowState,
+ ((Consumed) (0))
+ ((ConsumedAndTrimmed) (1))
+);
+
+struct TStateTableRow
+{
+ int TabletIndex;
+ i64 RowIndex;
+ ERowState State;
+};
+
+struct TStateTable
+{
+ static const TString TabletIndexColumnName;
+ static const TString RowIndexColumnName;
+ static const TString StateColumnName;
+};
+
+const TString TStateTable::TabletIndexColumnName("tablet_index");
+const TString TStateTable::RowIndexColumnName("row_index");
+const TString TStateTable::StateColumnName("state");
+
+std::vector<int> PrepareTabletIndexes(std::vector<int> tabletIndexes)
+{
+ std::sort(tabletIndexes.begin(), tabletIndexes.end());
+ tabletIndexes.erase(std::unique(tabletIndexes.begin(), tabletIndexes.end()), tabletIndexes.end());
+ return tabletIndexes;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPersistentQueuePoller::TImpl
+ : public TRefCounted
+{
+public:
+ TImpl(
+ TPersistentQueuePollerConfigPtr config,
+ IClientPtr client,
+ const TYPath& dataTablePath,
+ const TYPath& stateTablePath,
+ const std::vector<int>& tabletIndexes)
+ : Config_(std::move(config))
+ , Client_(std::move(client))
+ , DataTablePath_(dataTablePath)
+ , StateTablePath_(stateTablePath)
+ , TabletIndexes_(PrepareTabletIndexes(tabletIndexes))
+ , PollerId_(TGuid::Create())
+ , Logger(ApiLogger.WithTag("PollerId: %v", PollerId_))
+ , Invoker_(Client_->GetConnection()->GetInvoker())
+ {
+ YT_VERIFY(Config_);
+
+ RecreateState(false);
+
+ YT_LOG_INFO("Persistent queue poller initialized (DataTablePath: %v, StateTablePath: %v, TabletIndexes: %v)",
+ DataTablePath_,
+ StateTablePath_,
+ TabletIndexes_);
+
+ for (int tabletIndex : TabletIndexes_) {
+ auto executor = New<TPeriodicExecutor>(
+ Invoker_,
+ BIND(&TImpl::FetchTablet, MakeWeak(this), tabletIndex),
+ Config_->DataPollPeriod);
+ PollExecutors_.push_back(executor);
+ executor->Start();
+ }
+
+ {
+ TrimExecutor_ = New<TPeriodicExecutor>(
+ Invoker_,
+ BIND(&TImpl::Trim, MakeWeak(this)),
+ Config_->StateTrimPeriod);
+ TrimExecutor_->Start();
+ }
+ }
+
+ TFuture<IPersistentQueueRowsetPtr> Poll()
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto promise = NewPromise<IPersistentQueueRowsetPtr>();
+ auto state = GetState();
+ auto guard = Guard(state->SpinLock);
+ state->Promises.push_back(promise);
+ TryFulfillPromises(state, &guard);
+ return promise;
+ }
+
+private:
+ struct TBatch
+ {
+ IUnversionedRowsetPtr Rowset;
+ int RowCount;
+ i64 DataWeight;
+ int TabletIndex;
+ i64 RowsetStartRowIndex;
+ i64 BeginRowIndex;
+ i64 EndRowIndex;
+ };
+
+ struct TTablet
+ {
+ THashSet<i64> ConsumedRowIndexes;
+ i64 MaxConsumedRowIndex = std::numeric_limits<i64>::min();
+ i64 FetchRowIndex = std::numeric_limits<i64>::max();
+ i64 LastTrimmedRowIndex = std::numeric_limits<i64>::min();
+ };
+
+ struct TState
+ : public TRefCounted
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock);
+ std::deque<TPromise<IPersistentQueueRowsetPtr>> Promises;
+ std::deque<TBatch> Batches;
+ int BatchesRowCount = 0;
+ i64 BatchesDataWeight = 0;
+ THashMap<int, TTablet> TabletMap;
+ std::atomic<bool> Failed = {false};
+ };
+
+ using TStatePtr = TIntrusivePtr<TState>;
+
+ class TPolledRowset
+ : public IPersistentQueueRowset
+ {
+ public:
+ TPolledRowset(
+ TIntrusivePtr<TImpl> owner,
+ TStatePtr state,
+ TBatch batch)
+ : Owner_(std::move(owner))
+ , State_(std::move(state))
+ , Batch_(std::move(batch))
+ { }
+
+ ~TPolledRowset()
+ {
+ if (!Committed_) {
+ Owner_->ReclaimBatch(State_, std::move(Batch_));
+ }
+ }
+
+ const TTableSchemaPtr& GetSchema() const override
+ {
+ return Batch_.Rowset->GetSchema();
+ }
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return Batch_.Rowset->GetNameTable();
+ }
+
+ TSharedRange<TUnversionedRow> GetRows() const override
+ {
+ return Batch_.Rowset->GetRows().Slice(
+ Batch_.BeginRowIndex - Batch_.RowsetStartRowIndex,
+ Batch_.EndRowIndex - Batch_.RowsetStartRowIndex);
+ }
+
+ TFuture<void> Confirm(const ITransactionPtr& transaction) override
+ {
+ transaction->SubscribeCommitted(BIND(&TPolledRowset::OnCommitted, MakeStrong(this)));
+ return Owner_->ConfirmBatch(State_, Batch_, transaction);
+ }
+
+ private:
+ const TIntrusivePtr<TImpl> Owner_;
+ const TStatePtr State_;
+ const TBatch Batch_;
+
+ bool Committed_ = false;
+
+
+ void OnCommitted()
+ {
+ Owner_->OnBatchCommitted(Batch_);
+ Committed_ = true;
+ }
+ };
+
+
+ const TPersistentQueuePollerConfigPtr Config_;
+ const IClientPtr Client_;
+ const NYPath::TYPath DataTablePath_;
+ const NYPath::TYPath StateTablePath_;
+ const std::vector<int> TabletIndexes_;
+
+ const TGuid PollerId_;
+ const NLogging::TLogger Logger;
+ const IInvokerPtr Invoker_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TStatePtr State_;
+
+ std::vector<TPeriodicExecutorPtr> PollExecutors_;
+ TPeriodicExecutorPtr TrimExecutor_;
+
+
+ TStatePtr GetState()
+ {
+ auto guard = Guard(SpinLock_);
+ return State_;
+ }
+
+
+ std::vector<TStateTableRow> ReadStateTable(const IClientBasePtr& client)
+ {
+ // TODO(babenko): escaping
+ auto query = Format(
+ "[%v], [%v], [%v] from [%v] where [%v] in (%v)",
+ TStateTable::TabletIndexColumnName,
+ TStateTable::RowIndexColumnName,
+ TStateTable::StateColumnName,
+ StateTablePath_,
+ TStateTable::TabletIndexColumnName,
+ JoinToString(TabletIndexes_));
+ auto result = WaitFor(client->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ const auto& schema = rowset->GetSchema();
+ auto tabletIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::TabletIndexColumnName);
+ auto rowIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::RowIndexColumnName);
+ auto stateColumnId = schema->GetColumnIndexOrThrow(TStateTable::StateColumnName);
+
+ std::vector<TStateTableRow> rows;
+
+ for (auto row : rowset->GetRows()) {
+ TStateTableRow stateRow;
+
+ YT_ASSERT(row[tabletIndexColumnId].Type == EValueType::Int64);
+ stateRow.TabletIndex = static_cast<int>(row[tabletIndexColumnId].Data.Int64);
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ stateRow.RowIndex = row[rowIndexColumnId].Data.Int64;
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ stateRow.State = ERowState(row[stateColumnId].Data.Int64);
+
+ rows.push_back(stateRow);
+ }
+
+ return rows;
+ }
+
+
+ void DoLoadState(const TStatePtr& state)
+ {
+ YT_LOG_INFO("Loading queue poller state for initialization");
+
+ auto stateRows = ReadStateTable(Client_);
+
+ auto guard = Guard(state->SpinLock);
+
+ for (auto& [tabletId, tabletState] : state->TabletMap) {
+ tabletState.FetchRowIndex = 0;
+ tabletState.LastTrimmedRowIndex = -1;
+ }
+
+ for (const auto& row : stateRows) {
+ auto& tabletState = GetOrCrash(state->TabletMap, row.TabletIndex);
+
+ tabletState.ConsumedRowIndexes.insert(row.RowIndex);
+ tabletState.MaxConsumedRowIndex = std::max(tabletState.MaxConsumedRowIndex, row.RowIndex);
+
+ if (row.State == ERowState::ConsumedAndTrimmed) {
+ tabletState.FetchRowIndex = row.RowIndex;
+ tabletState.LastTrimmedRowIndex = std::max(tabletState.LastTrimmedRowIndex, row.RowIndex);
+ }
+ }
+
+ for (auto& [tabletId, tablet] : state->TabletMap) {
+ while (tablet.ConsumedRowIndexes.find(tablet.FetchRowIndex) != tablet.ConsumedRowIndexes.end()) {
+ YT_VERIFY(tablet.ConsumedRowIndexes.erase(tablet.FetchRowIndex) == 1);
+ ++tablet.FetchRowIndex;
+ }
+ }
+
+ for (const auto& [tabletIndex, tablet] : state->TabletMap) {
+ YT_LOG_DEBUG("Tablet state collected (TabletIndex: %v, ConsumedRowIndexes: %v, FetchRowIndex: %v)",
+ tabletIndex,
+ tablet.ConsumedRowIndexes,
+ tablet.FetchRowIndex);
+ }
+
+ YT_LOG_INFO("Queue poller state loaded");
+ }
+
+ void LoadState(const TStatePtr& state)
+ {
+ try {
+ DoLoadState(state);
+ } catch (const std::exception& ex) {
+ OnStateFailed(state);
+ YT_LOG_ERROR(ex, "Error loading queue poller state");
+ }
+ }
+
+ void RecreateState(bool backoff)
+ {
+ auto state = New<TState>();
+ for (int tabletIndex : TabletIndexes_) {
+ YT_VERIFY(state->TabletMap.emplace(tabletIndex, TTablet()).second);
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (State_) {
+ state->Promises = std::move(State_->Promises);
+ }
+ State_ = state;
+ }
+
+ TDelayedExecutor::Submit(
+ BIND(&TImpl::LoadState, MakeStrong(this), state),
+ backoff ? Config_->BackoffTime : TDuration::Zero());
+ }
+
+
+ void DoFetchTablet(int tabletIndex)
+ {
+ auto state = GetState();
+ if (state->Failed) {
+ return;
+ }
+
+ auto& tablet = GetOrCrash(state->TabletMap, tabletIndex);
+
+ auto rowLimit = Config_->MaxRowsPerFetch;
+ {
+ auto guard = Guard(state->SpinLock);
+ if (tablet.FetchRowIndex > tablet.LastTrimmedRowIndex + Config_->MaxFetchedUntrimmedRowCount) {
+ YT_LOG_INFO("Number of fetched but trimmed rows exceeds the limit; fetching new rows suspended (TabletIndex: %v, RowCount: %v, Limit: %v)",
+ tabletIndex,
+ tablet.FetchRowIndex - tablet.LastTrimmedRowIndex,
+ Config_->MaxFetchedUntrimmedRowCount);
+ return;
+ }
+ if (state->BatchesDataWeight > Config_->MaxPrefetchDataWeight) {
+ return;
+ }
+ rowLimit = std::min(rowLimit, Config_->MaxPrefetchRowCount - state->BatchesRowCount);
+ }
+
+ if (rowLimit <= 0) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Started fetching data (TabletIndex: %v, FetchRowIndex: %v, RowLimit: %v)",
+ tabletIndex,
+ tablet.FetchRowIndex,
+ rowLimit);
+
+ // TODO(babenko): escaping
+ auto query = Format(
+ "* from [%v] where [%v] = %v and [%v] between %v and %v order by [%v] limit %v",
+ DataTablePath_,
+ TabletIndexColumnName,
+ tabletIndex,
+ RowIndexColumnName,
+ tablet.FetchRowIndex,
+ tablet.FetchRowIndex + rowLimit - 1,
+ RowIndexColumnName,
+ rowLimit);
+ auto result = WaitFor(Client_->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ const auto& schema = rowset->GetSchema();
+ auto rows = rowset->GetRows();
+
+ YT_LOG_DEBUG("Finished fetching data (TabletIndex: %v, RowCount: %v)",
+ tabletIndex,
+ rows.Size());
+
+ if (rows.Empty()) {
+ return;
+ }
+
+ auto rowIndexColumnId = schema->GetColumnIndexOrThrow(RowIndexColumnName);
+
+ std::vector<TBatch> batches;
+ i64 currentRowIndex = tablet.FetchRowIndex;
+ i64 batchBeginRowIndex = -1;
+
+
+ auto beginBatch = [&] () {
+ YT_VERIFY(batchBeginRowIndex < 0);
+ batchBeginRowIndex = currentRowIndex;
+ };
+
+ auto endBatch = [&] () {
+ if (batchBeginRowIndex < 0) {
+ return;
+ }
+
+ i64 batchEndRowIndex = currentRowIndex;
+ YT_VERIFY(batchBeginRowIndex < batchEndRowIndex);
+
+ TBatch batch;
+ batch.TabletIndex = tabletIndex;
+ batch.Rowset = rowset;
+ batch.RowCount = static_cast<int>(batchEndRowIndex - batchBeginRowIndex);
+ batch.RowsetStartRowIndex = tablet.FetchRowIndex;
+ batch.BeginRowIndex = batchBeginRowIndex;
+ batch.EndRowIndex = batchEndRowIndex;
+ batch.DataWeight = 0;
+ for (i64 index = batchBeginRowIndex; index < batchEndRowIndex; ++index) {
+ batch.DataWeight += GetDataWeight(rows[index - tablet.FetchRowIndex]);
+ }
+ batches.emplace_back(std::move(batch));
+
+ YT_LOG_DEBUG("Rows fetched (TabletIndex: %v, RowIndexes: %v-%v, DataWeight: %v)",
+ tabletIndex,
+ batchBeginRowIndex,
+ batchEndRowIndex - 1,
+ batch.DataWeight);
+
+ batchBeginRowIndex = -1;
+ };
+
+ for (auto row : rows) {
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ auto queryRowIndex = row[rowIndexColumnId].Data.Int64;
+ if (queryRowIndex != currentRowIndex) {
+ OnStateFailed(state);
+ THROW_ERROR_EXCEPTION("Fetched row index mismatch: expected %v, got %v",
+ currentRowIndex,
+ queryRowIndex);
+ }
+
+ if (tablet.ConsumedRowIndexes.find(currentRowIndex) == tablet.ConsumedRowIndexes.end()) {
+ if (batchBeginRowIndex >= 0 && currentRowIndex - batchBeginRowIndex >= Config_->MaxRowsPerPoll) {
+ endBatch();
+ }
+ if (batchBeginRowIndex < 0) {
+ beginBatch();
+ }
+ } else {
+ endBatch();
+ }
+
+ ++currentRowIndex;
+ }
+
+ endBatch();
+
+ {
+ auto guard = Guard(state->SpinLock);
+
+ for (const auto& batch : batches) {
+ state->Batches.push_back(batch);
+ state->BatchesRowCount += batch.RowCount;
+ state->BatchesDataWeight += batch.DataWeight;
+ }
+
+ tablet.FetchRowIndex += rows.Size();
+ if (tablet.FetchRowIndex > tablet.MaxConsumedRowIndex) {
+ // No need to keep them anymore.
+ tablet.ConsumedRowIndexes.clear();
+ }
+
+ TryFulfillPromises(state, &guard);
+ }
+ }
+
+ void FetchTablet(int tabletIndex)
+ {
+ try {
+ DoFetchTablet(tabletIndex);
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error fetching queue data (TabletIndex: %v)",
+ tabletIndex);
+ }
+ }
+
+
+ void TryFulfillPromises(const TStatePtr& state, TGuard<NThreading::TSpinLock>* guard)
+ {
+ if (state->Failed) {
+ return;
+ }
+
+ std::vector<std::tuple<TBatch, TPromise<IPersistentQueueRowsetPtr>>> toFulfill;
+ while (!state->Batches.empty() && !state->Promises.empty()) {
+ const auto& batch = state->Batches.front();
+ const auto& promise = state->Promises.front();
+ toFulfill.push_back(std::make_tuple(batch, promise));
+ state->Batches.pop_front();
+ state->Promises.pop_front();
+ state->BatchesRowCount -= batch.RowCount;
+ state->BatchesDataWeight -= batch.DataWeight;
+ }
+
+ guard->Release();
+
+ for (auto& tuple : toFulfill) {
+ const auto& rowset = std::get<0>(tuple);
+ auto& promise = std::get<1>(tuple);
+ YT_LOG_DEBUG("Rows offered (TabletIndex: %v, RowIndexes: %v-%v)",
+ rowset.TabletIndex,
+ rowset.BeginRowIndex,
+ rowset.EndRowIndex - 1);
+ promise.Set(New<TPolledRowset>(this, state, rowset));
+ }
+ }
+
+ void ReclaimBatch(
+ const TStatePtr& state,
+ TBatch batch)
+ {
+ auto guard = Guard(state->SpinLock);
+
+ if (State_ != state) {
+ return;
+ }
+
+ State_->BatchesRowCount += batch.RowCount;
+ State_->BatchesDataWeight += batch.DataWeight;
+ State_->Batches.emplace_back(std::move(batch));
+
+ YT_LOG_DEBUG("Rows reclaimed (TabletIndex: %v RowIndexes: %v-%v)",
+ batch.TabletIndex,
+ batch.BeginRowIndex,
+ batch.EndRowIndex - 1);
+
+ TryFulfillPromises(state, &guard);
+ }
+
+
+ TFuture<void> ConfirmBatch(
+ const TStatePtr& state,
+ const TBatch& batch,
+ const ITransactionPtr& transaction)
+ {
+ return BIND(&TImpl::DoConfirmBatch, MakeStrong(this))
+ .AsyncVia(Invoker_)
+ .Run(state, batch, transaction);
+ }
+
+ void DoConfirmBatch(
+ const TStatePtr& state,
+ const TBatch& batch,
+ const ITransactionPtr& transaction)
+ {
+ try {
+ // Check that none of the dequeued rows were consumed in another transaction.
+ {
+ // TODO(babenko): escaping
+ auto query = Format("[%v] from [%v] where [%v] = %v and [%v] between %v and %v",
+ TStateTable::RowIndexColumnName,
+ StateTablePath_,
+ TStateTable::TabletIndexColumnName,
+ batch.TabletIndex,
+ TStateTable::RowIndexColumnName,
+ batch.BeginRowIndex,
+ batch.EndRowIndex - 1);
+ auto result = WaitFor(transaction->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ auto rows = rowset->GetRows();
+ if (!rows.Empty()) {
+ std::vector<i64> rowIndexes;
+ for (auto row : rows) {
+ i64 rowIndex;
+ FromUnversionedRow(
+ row,
+ &rowIndex);
+ rowIndexes.push_back(rowIndex);
+ }
+ OnStateFailed(state);
+ THROW_ERROR_EXCEPTION("Some of the offered rows were already consumed")
+ << TErrorAttribute("consumed_row_indexes", rowIndexes);
+ }
+ }
+
+ // Check that none of the dequeued rows were trimmed.
+ {
+ // TODO(babenko): escaping
+ auto query = Format("[%v] from [%v] where [%v] = %v and [%v] = %v order by [%v] limit 1",
+ TStateTable::RowIndexColumnName,
+ StateTablePath_,
+ TStateTable::TabletIndexColumnName,
+ batch.TabletIndex,
+ TStateTable::StateColumnName,
+ static_cast<int>(ERowState::ConsumedAndTrimmed),
+ TStateTable::RowIndexColumnName);
+ auto result = WaitFor(transaction->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ const auto& schema = rowset->GetSchema();
+ auto rows = rowset->GetRows();
+ if (!rows.Empty()) {
+ YT_VERIFY(rows.Size() == 1);
+ auto row = rows[0];
+
+ auto rowIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::RowIndexColumnName);
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ auto rowIndex = row[rowIndexColumnId].Data.Int64;
+
+ if (rowIndex >= batch.BeginRowIndex) {
+ OnStateFailed(state);
+ THROW_ERROR_EXCEPTION("Some of the offered rows were already trimmed")
+ << TErrorAttribute("trimmed_row_index", rowIndex);
+ }
+ }
+ }
+
+ // Mark rows as consumed in state table.
+ {
+ auto nameTable = New<TNameTable>();
+ auto tabletIndexColumnId = nameTable->RegisterName(TStateTable::TabletIndexColumnName);
+ auto rowIndexColumnId = nameTable->RegisterName(TStateTable::RowIndexColumnName);
+ auto stateColumnId = nameTable->RegisterName(TStateTable::StateColumnName);
+
+ auto rowBuffer = New<TRowBuffer>(TPersistentQueuePollerBufferTag());
+ std::vector<TUnversionedRow> rows;
+ for (i64 rowIndex = batch.BeginRowIndex; rowIndex < batch.EndRowIndex; ++rowIndex) {
+ auto row = rowBuffer->AllocateUnversioned(3);
+ row[0] = MakeUnversionedInt64Value(batch.TabletIndex, tabletIndexColumnId);
+ row[1] = MakeUnversionedInt64Value(rowIndex, rowIndexColumnId);
+ row[2] = MakeUnversionedInt64Value(static_cast<int>(ERowState::Consumed), stateColumnId);
+ rows.push_back(row);
+ }
+ transaction->WriteRows(
+ StateTablePath_,
+ std::move(nameTable),
+ MakeSharedRange(std::move(rows), std::move(rowBuffer)));
+ }
+
+ YT_LOG_DEBUG("Rows processing confirmed (TabletIndex: %v, RowIndexes: %v-%v, TransactionId: %v)",
+ batch.TabletIndex,
+ batch.BeginRowIndex,
+ batch.EndRowIndex - 1,
+ transaction->GetId());
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error confirming persistent queue rows",
+ batch.TabletIndex,
+ batch.BeginRowIndex,
+ batch.EndRowIndex - 1)
+ << TErrorAttribute("poller_id", PollerId_)
+ << TErrorAttribute("transaction_id", transaction->GetId())
+ << TErrorAttribute("tablet_index", batch.TabletIndex)
+ << TErrorAttribute("begin_row_index", batch.BeginRowIndex)
+ << TErrorAttribute("end_row_index", batch.EndRowIndex)
+ << TErrorAttribute("data_table_path", DataTablePath_)
+ << TErrorAttribute("state_table_path", StateTablePath_)
+ << ex;
+ }
+ }
+
+
+ void OnBatchCommitted(const TBatch& batch)
+ {
+ YT_LOG_DEBUG("Rows processing committed (TabletIndex: %v RowIndexes: %v-%v)",
+ batch.TabletIndex,
+ batch.BeginRowIndex,
+ batch.EndRowIndex - 1);
+ }
+
+
+ void GuardedTrim()
+ {
+ // NB: Not actually needed, just for a backoff.
+ auto state = GetState();
+ if (state->Failed) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Getting tablet infos");
+
+ auto asyncTabletInfos = Client_->GetTabletInfos(
+ DataTablePath_,
+ TabletIndexes_);
+ auto tabletInfos = WaitFor(asyncTabletInfos)
+ .ValueOrThrow();
+
+ THashMap<int, const TTabletInfo*> tabletIndexToInfo;
+ YT_VERIFY(TabletIndexes_.size() == tabletInfos.size());
+ for (size_t index = 0; index < TabletIndexes_.size(); ++index) {
+ YT_VERIFY(tabletIndexToInfo.emplace(TabletIndexes_[index], &tabletInfos[index]).second);
+ }
+
+ YT_LOG_DEBUG("Tablet infos received");
+
+ YT_LOG_DEBUG("Starting state trim transaction");
+
+ auto transaction = WaitFor(Client_->StartTransaction(ETransactionType::Tablet))
+ .ValueOrThrow();
+
+ YT_LOG_DEBUG("State trim transaction started (TransactionId: %v)",
+ transaction->GetId());
+
+ YT_LOG_DEBUG("Loading queue poller state for trim");
+
+ auto stateRows = ReadStateTable(transaction);
+
+ YT_LOG_DEBUG("Queue poller state loaded");
+
+ struct TTabletStatistics
+ {
+ i64 LastTrimmedRowIndex = -1;
+ THashSet<i64> ConsumedRowIndexes;
+ i64 TrimmedRowCountRequest = -1;
+ };
+
+ THashMap<int, TTabletStatistics> tabletStatisticsMap;
+
+ for (const auto& row : stateRows) {
+ auto& tablet = tabletStatisticsMap[row.TabletIndex];
+ if (row.State == ERowState::ConsumedAndTrimmed) {
+ tablet.LastTrimmedRowIndex = std::max(tablet.LastTrimmedRowIndex, row.RowIndex);
+ }
+ YT_VERIFY(tablet.ConsumedRowIndexes.insert(row.RowIndex).second);
+ }
+
+ {
+ auto guard = Guard(state->SpinLock);
+ for (const auto& [tabletIndex, statistics] : tabletStatisticsMap) {
+ auto& tablet = GetOrCrash(state->TabletMap, tabletIndex);
+ tablet.LastTrimmedRowIndex = statistics.LastTrimmedRowIndex;
+ }
+ }
+
+ {
+ auto nameTable = New<TNameTable>();
+ auto tabletIndexColumnId = nameTable->RegisterName(TStateTable::TabletIndexColumnName);
+ auto rowIndexColumnId = nameTable->RegisterName(TStateTable::RowIndexColumnName);
+ auto stateColumnId = nameTable->RegisterName(TStateTable::StateColumnName);
+
+ for (auto& [tabletIndex, statistics] : tabletStatisticsMap) {
+ i64 stateTrimRowIndex = statistics.LastTrimmedRowIndex;
+ while (statistics.ConsumedRowIndexes.find(stateTrimRowIndex + 1) != statistics.ConsumedRowIndexes.end()) {
+ ++stateTrimRowIndex;
+ }
+
+ if (stateTrimRowIndex > statistics.LastTrimmedRowIndex) {
+ auto rowBuffer = New<TRowBuffer>(TPersistentQueuePollerBufferTag());
+
+ std::vector<TUnversionedRow> deleteKeys;
+ for (i64 rowIndex = statistics.LastTrimmedRowIndex; rowIndex < stateTrimRowIndex; ++rowIndex) {
+ auto key = rowBuffer->AllocateUnversioned(2);
+ key[0] = MakeUnversionedInt64Value(tabletIndex, tabletIndexColumnId);
+ key[1] = MakeUnversionedInt64Value(rowIndex, rowIndexColumnId);
+ deleteKeys.push_back(key);
+ }
+ transaction->DeleteRows(
+ StateTablePath_,
+ nameTable,
+ MakeSharedRange(std::move(deleteKeys), rowBuffer));
+
+ std::vector<TUnversionedRow> writeRows;
+ {
+ auto row = rowBuffer->AllocateUnversioned(3);
+ row[0] = MakeUnversionedInt64Value(tabletIndex, tabletIndexColumnId);
+ row[1] = MakeUnversionedInt64Value(stateTrimRowIndex, rowIndexColumnId);
+ row[2] = MakeUnversionedInt64Value(static_cast<int>(ERowState::ConsumedAndTrimmed), stateColumnId);
+ writeRows.push_back(row);
+ }
+ transaction->WriteRows(
+ StateTablePath_,
+ nameTable,
+ MakeSharedRange(std::move(writeRows), std::move(rowBuffer)));
+
+ YT_LOG_DEBUG("Tablet state update scheduled (TabletIndex: %v, TrimRowIndex: %v)",
+ tabletIndex,
+ stateTrimRowIndex);
+ }
+
+ const auto& tabletInfo = GetOrCrash(tabletIndexToInfo, tabletIndex);
+ if (stateTrimRowIndex - tabletInfo->TrimmedRowCount >= Config_->UntrimmedDataRowsHigh) {
+ statistics.TrimmedRowCountRequest = stateTrimRowIndex - Config_->UntrimmedDataRowsLow;
+ YT_LOG_DEBUG("Tablet data trim scheduled (TabletIndex: %v, TrimmedRowCount: %v)",
+ tabletIndex,
+ statistics.TrimmedRowCountRequest);
+ }
+ }
+ }
+
+ YT_LOG_DEBUG("Committing state trim transaction");
+
+ WaitFor(transaction->Commit())
+ .ThrowOnError();
+
+ YT_LOG_DEBUG("State trim transaction committed");
+
+ std::vector<TFuture<void>> dataTrimAsyncResults;
+ for (const auto& [tabletIndex, statistics] : tabletStatisticsMap) {
+ if (statistics.TrimmedRowCountRequest > 0) {
+ dataTrimAsyncResults.push_back(Client_->TrimTable(
+ DataTablePath_,
+ tabletIndex,
+ statistics.TrimmedRowCountRequest));
+ }
+ }
+
+ if (!dataTrimAsyncResults.empty()) {
+ WaitFor(AllSucceeded(dataTrimAsyncResults))
+ .ThrowOnError();
+
+ YT_LOG_DEBUG("Tablet data trim completed");
+ }
+ }
+
+ void Trim()
+ {
+ try {
+ GuardedTrim();
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error trimming queue poller");
+ }
+ }
+
+
+ void OnStateFailed(const TStatePtr& state)
+ {
+ bool expected = false;
+ if (state->Failed.compare_exchange_strong(expected, true)) {
+ RecreateState(true);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPersistentQueuePoller::TPersistentQueuePoller(
+ TPersistentQueuePollerConfigPtr config,
+ IClientPtr client,
+ const TYPath& dataTablePath,
+ const TYPath& stateTablePath,
+ const std::vector<int>& tabletIndexes)
+ : Impl_(New<TImpl>(
+ std::move(config),
+ std::move(client),
+ dataTablePath,
+ stateTablePath,
+ tabletIndexes))
+{ }
+
+TPersistentQueuePoller::~TPersistentQueuePoller()
+{ }
+
+TFuture<IPersistentQueueRowsetPtr> TPersistentQueuePoller::Poll()
+{
+ return Impl_->Poll();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> CreatePersistentQueueStateTable(
+ const IClientBasePtr& client,
+ const TYPath& path)
+{
+ TTableSchema schema({
+ TColumnSchema(TStateTable::TabletIndexColumnName, EValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending),
+ TColumnSchema(TStateTable::RowIndexColumnName, EValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending),
+ TColumnSchema(TStateTable::StateColumnName, EValueType::Int64)
+ });
+
+ auto attributes = CreateEphemeralAttributes();
+ attributes->Set("dynamic", true);
+ attributes->Set("schema", schema);
+
+ TCreateNodeOptions options;
+ options.Attributes = std::move(attributes);
+ return client->CreateNode(path, EObjectType::Table, options).As<void>();
+}
+
+TFuture<THashMap<int, TPersistentQueueTabletState>> ReadPersistentQueueTabletsState(
+ const IClientBasePtr& client,
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes)
+{
+ return
+ BIND([=] {
+ // TODO(babenko): escaping
+ auto query = Format(
+ "[%v], [%v], [%v] from [%v] where [%v] in (%v)",
+ TStateTable::TabletIndexColumnName,
+ TStateTable::RowIndexColumnName,
+ TStateTable::StateColumnName,
+ path,
+ TStateTable::TabletIndexColumnName,
+ JoinToString(tabletIndexes));
+ auto result = WaitFor(client->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ const auto& schema = rowset->GetSchema();
+ auto tabletIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::TabletIndexColumnName);
+ auto rowIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::RowIndexColumnName);
+ auto stateColumnId = schema->GetColumnIndexOrThrow(TStateTable::StateColumnName);
+
+ THashMap<int, TPersistentQueueTabletState> tabletMap;
+
+ for (auto row : rowset->GetRows()) {
+ YT_ASSERT(row[tabletIndexColumnId].Type == EValueType::Int64);
+ int tabletIndex = static_cast<int>(row[tabletIndexColumnId].Data.Int64);
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ i64 rowIndex = row[rowIndexColumnId].Data.Int64;
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ auto state = ERowState(row[stateColumnId].Data.Int64);
+
+ auto& tabletState = tabletMap[tabletIndex];
+ if (state == ERowState::ConsumedAndTrimmed) {
+ tabletState.FirstUntrimmedRowIndex = std::max(tabletState.FirstUntrimmedRowIndex, rowIndex + 1);
+ }
+ }
+
+ for (auto& [tabetId, tabletState] : tabletMap) {
+ tabletState.ConsumedRowCount = tabletState.FirstUntrimmedRowIndex;
+ }
+
+ for (auto row : rowset->GetRows()) {
+ YT_ASSERT(row[tabletIndexColumnId].Type == EValueType::Int64);
+ int tabletIndex = static_cast<int>(row[tabletIndexColumnId].Data.Int64);
+
+ YT_ASSERT(row[rowIndexColumnId].Type == EValueType::Int64);
+ i64 rowIndex = row[rowIndexColumnId].Data.Int64;
+
+ auto& tabletState = tabletMap[tabletIndex];
+ if (rowIndex >= tabletState.FirstUntrimmedRowIndex) {
+ ++tabletState.ConsumedRowCount;
+ }
+ }
+
+ return tabletMap;
+ })
+ .AsyncVia(client->GetConnection()->GetInvoker())
+ .Run();
+}
+
+TFuture<void> UpdatePersistentQueueTabletsState(
+ const IClientBasePtr& client,
+ const NYPath::TYPath& path,
+ const THashMap<int, TPersistentQueueTabletUpdate>& tabletMap)
+{
+ return
+ BIND([=] {
+ auto transaction = WaitFor(client->StartTransaction(ETransactionType::Tablet))
+ .ValueOrThrow();
+
+ // TODO(babenko): escaping
+ auto query = Format(
+ "[%v], [%v], [%v] from [%v] where [%v] in (%v)",
+ TStateTable::TabletIndexColumnName,
+ TStateTable::RowIndexColumnName,
+ TStateTable::StateColumnName,
+ path,
+ TStateTable::TabletIndexColumnName,
+ JoinToString(GetKeys(tabletMap)));
+ auto result = WaitFor(client->SelectRows(query))
+ .ValueOrThrow();
+ const auto& rowset = result.Rowset;
+ const auto& schema = rowset->GetSchema();
+ auto rowsetTabletIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::TabletIndexColumnName);
+ auto rowsetRowIndexColumnId = schema->GetColumnIndexOrThrow(TStateTable::RowIndexColumnName);
+
+ auto nameTable = New<TNameTable>();
+ auto nameTableTabletIndexColumnId = nameTable->RegisterName(TStateTable::TabletIndexColumnName);
+ auto nameTableRowIndexColumnId = nameTable->RegisterName(TStateTable::RowIndexColumnName);
+ auto nameTableStateColumnId = nameTable->RegisterName(TStateTable::StateColumnName);
+
+ auto rowBuffer = New<TRowBuffer>(TPersistentQueuePollerBufferTag());
+ std::vector<TRowModification> modifications;
+
+ for (auto rowsetRow : rowset->GetRows()) {
+ YT_ASSERT(rowsetRow[rowsetTabletIndexColumnId].Type == EValueType::Int64);
+ int tabletIndex = static_cast<int>(rowsetRow[rowsetTabletIndexColumnId].Data.Int64);
+
+ YT_ASSERT(rowsetRow[rowsetRowIndexColumnId].Type == EValueType::Int64);
+ i64 rowIndex = rowsetRow[rowsetRowIndexColumnId].Data.Int64;
+
+ auto rowToDelete = rowBuffer->AllocateUnversioned(2);
+ rowToDelete[0] = MakeUnversionedInt64Value(tabletIndex, nameTableTabletIndexColumnId);
+ rowToDelete[1] = MakeUnversionedInt64Value(rowIndex, nameTableRowIndexColumnId);
+ modifications.push_back(TRowModification{
+ ERowModificationType::Delete,
+ rowToDelete.ToTypeErasedRow(),
+ TLockMask()
+ });
+ }
+
+ for (const auto& [tabletIndex, tabletUpdate] : tabletMap) {
+ YT_ASSERT(tabletUpdate.FirstUnconsumedRowIndex >= 0);
+ if (tabletUpdate.FirstUnconsumedRowIndex > 0) {
+ auto rowToWrite = rowBuffer->AllocateUnversioned(3);
+ rowToWrite[0] = MakeUnversionedInt64Value(tabletIndex, nameTableTabletIndexColumnId);
+ rowToWrite[1] = MakeUnversionedInt64Value(tabletUpdate.FirstUnconsumedRowIndex - 1, nameTableRowIndexColumnId);
+ rowToWrite[2] = MakeUnversionedInt64Value(static_cast<i64>(ERowState::ConsumedAndTrimmed), nameTableStateColumnId);
+ modifications.push_back(TRowModification{
+ ERowModificationType::Write,
+ rowToWrite.ToTypeErasedRow(),
+ TLockMask()
+ });
+ }
+ }
+
+ transaction->ModifyRows(
+ path,
+ nameTable,
+ MakeSharedRange(std::move(modifications), std::move(rowBuffer)));
+
+ WaitFor(transaction->Commit())
+ .ThrowOnError();
+ })
+ .AsyncVia(client->GetConnection()->GetInvoker())
+ .Run();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/persistent_queue.h b/yt/yt/client/api/persistent_queue.h
new file mode 100644
index 0000000000..ac3b63dc84
--- /dev/null
+++ b/yt/yt/client/api/persistent_queue.h
@@ -0,0 +1,137 @@
+#pragma once
+
+#include "rowset.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents a rowset extracted from a persistent queue via
+//! TPersistentQueue::Poll.
+struct IPersistentQueueRowset
+ : public IUnversionedRowset
+{
+ //! Confirms that the rowset has been successfully processed
+ //! within #transaction and must not be consumed again.
+ virtual TFuture<void> Confirm(const ITransactionPtr& transaction) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPersistentQueueRowset)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables polling and consuming a subset of tablets of an ordered dynamic table.
+/*!
+ * The best practices for using TPersistentQueuePoller are as follows:
+ *
+ * - Create as many as |N * K| tablets within the queue, where |N| is the number of
+ * consumer processes and |K| is a small constant allowing for tuning |N| afterwards.
+ *
+ * - Within each consumer process, create a single instance of TPersistentQueuePoller
+ * and assign it a unique subset of |K| tablets. Assigning the same tablet to multiple
+ * instances will not lead to any data corruption but will cause lock conflicts and
+ * performance degradation.
+ *
+ * - Within each consumer process, spawn a number of worker fibers, possibly
+ * within a thread pool. Each fiber must do the following:
+ * - poll the queue (via #TPersistentQueuePoller::Poll); wait until rows arrive;
+ * - start a transaction
+ * - process the data within the transaction; make any writes necessary;
+ * - mark the dequeued rows as consumed (via IPersistentQueueRowset::Confirm)
+ * - commit the transaction
+ *
+ * Thread affinity: any
+ */
+class TPersistentQueuePoller
+ : public TRefCounted
+{
+public:
+ //! Constructs a poller.
+ /*
+ * \param config poller configuration
+ * \param dataTablePath points to an ordered table with queue data
+ * \param stateTablePath points to a sorted per-consumer table holding the state of the consumer
+ * \param tabletIndexes contains the indexes of the set of tablets to be polled
+ */
+ TPersistentQueuePoller(
+ TPersistentQueuePollerConfigPtr config,
+ IClientPtr client,
+ const NYPath::TYPath& dataTablePath,
+ const NYPath::TYPath& stateTablePath,
+ const std::vector<int>& tabletIndexes);
+
+ ~TPersistentQueuePoller();
+
+ //! Polls the tablets of the queue.
+ /*!
+ * When unconsumed rows become available, the returned future gets
+ * populated with the queue rows. At most #TPersistentQueuePollerConfig::MaxRowsPerPoll
+ * rows are returned.
+ *
+ * This does not constitute a dequeue operation yet,
+ * however as long the the returned IPersistentQueueRowset instance is alive,
+ * the client is assumed to be holding a (transient) lock for these rows.
+ *
+ * Is is assumed that upon receiving IPersistentQueueRowset the client initiates
+ * a transaction to process these rows, carries out all the required updates within this
+ * transaction and marks the rows are dequeued by calling IPersistentQueueRowset::Confirm.
+ * When this transaction commits, these rows are persistently marked as consumed.
+ *
+ * Under any circumstances, it is guaranteed that any queued row is processed at most once
+ * by a consumer transaction that was able to commit successfully.
+ */
+ TFuture<IPersistentQueueRowsetPtr> Poll();
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+
+};
+
+DEFINE_REFCOUNTED_TYPE(TPersistentQueuePoller)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an empty table holding the state of a consumer.
+TFuture<void> CreatePersistentQueueStateTable(
+ const IClientBasePtr& client,
+ const NYPath::TYPath& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPersistentQueueTabletState
+{
+ //! Index of the first (minimum) untrimmed row in this particular tablet.
+ //! This row may already be consumed.
+ i64 FirstUntrimmedRowIndex = 0;
+
+ //! The total number of rows consumed in this tablet.
+ i64 ConsumedRowCount = 0;
+};
+
+//! Reads the state of tablets with certain #tabletIndexes.
+//! The resulting hashtable maps tablet indexes to tablet states.
+TFuture<THashMap<int, TPersistentQueueTabletState>> ReadPersistentQueueTabletsState(
+ const IClientBasePtr& client,
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes);
+
+struct TPersistentQueueTabletUpdate
+{
+ //! Index of the first (minimum) unconsumed row in this particular tablet.
+ i64 FirstUnconsumedRowIndex = 0;
+};
+
+//! Updates the poller state for certain tablets; in particular this enables rewinding or
+//! skipping some rows in these tablets.
+TFuture<void> UpdatePersistentQueueTabletsState(
+ const IClientBasePtr& client,
+ const NYPath::TYPath& path,
+ const THashMap<int, TPersistentQueueTabletUpdate>& tabletMap);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/private.h b/yt/yt/client/api/private.h
new file mode 100644
index 0000000000..60a8f6a4fa
--- /dev/null
+++ b/yt/yt/client/api/private.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger ApiLogger("Api");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/public.cpp b/yt/yt/client/api/public.cpp
new file mode 100644
index 0000000000..7bf0377af1
--- /dev/null
+++ b/yt/yt/client/api/public.cpp
@@ -0,0 +1,10 @@
+#include "public.h"
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/public.h b/yt/yt/client/api/public.h
new file mode 100644
index 0000000000..1a21f9b790
--- /dev/null
+++ b/yt/yt/client/api/public.h
@@ -0,0 +1,213 @@
+#pragma once
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/library/auth/authentication_options.h>
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TClusterTag = NObjectClient::TCellTag;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Keep in sync with NRpcProxy::NProto::EMasterReadKind.
+// On cache miss request is redirected to next level cache:
+// Local cache -> (node) cache -> master cache
+DEFINE_ENUM(EMasterChannelKind,
+ ((Leader) (0))
+ ((Follower) (1))
+ // Use local (per-connection) cache.
+ ((LocalCache) (4))
+ // Use cache located on nodes.
+ ((Cache) (2))
+ // Use cache located on masters (if caching on masters is enabled).
+ ((MasterCache) (3))
+);
+
+DEFINE_ENUM(EUserWorkloadCategory,
+ (Batch)
+ (Interactive)
+ (Realtime)
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((TooManyConcurrentRequests) (1900))
+ ((JobArchiveUnavailable) (1910))
+ ((RetriableArchiveError) (1911))
+ ((NoSuchOperation) (1915))
+ ((NoSuchJob) (1916))
+ ((UncertainOperationControllerState) (1917))
+ ((NoSuchAttribute) (1920))
+ ((FormatDisabled) (1925))
+ ((ClusterLivenessCheckFailed) (1926))
+);
+
+DEFINE_ENUM(ERowModificationType,
+ ((Write) (0))
+ ((Delete) (1))
+ ((VersionedWrite) (2))
+ ((WriteAndLock) (3))
+);
+
+DEFINE_ENUM(EReplicaConsistency,
+ ((None) (0))
+ ((Sync) (1))
+);
+
+DEFINE_ENUM(ETransactionCoordinatorCommitMode,
+ // Success is reported when phase 2 starts (all participants have prepared but not yet committed).
+ ((Eager) (0))
+ // Success is reported when transaction is finished (all participants have committed).
+ ((Lazy) (1))
+);
+
+DEFINE_ENUM(ETransactionCoordinatorPrepareMode,
+ // Coordinator is prepared just like every other participant.
+ ((Early) (0))
+ // Coordinator is prepared after other participants prepared and
+ // after commit timestamp generation. In this mode prepare and commit
+ // for coordinator are executed in the same mutation, so no preparation
+ // locks are required.
+ ((Late) (1))
+);
+
+DEFINE_ENUM(EProxyType,
+ ((Http) (1))
+ ((Rpc) (2))
+ ((Grpc) (3))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+struct IRowset;
+template <class TRow>
+using IRowsetPtr = TIntrusivePtr<IRowset<TRow>>;
+
+using IUnversionedRowset = IRowset<NTableClient::TUnversionedRow>;
+using IVersionedRowset = IRowset<NTableClient::TVersionedRow>;
+using ITypeErasedRowset = IRowset<NTableClient::TTypeErasedRow>;
+
+DECLARE_REFCOUNTED_TYPE(IUnversionedRowset)
+DECLARE_REFCOUNTED_TYPE(IVersionedRowset)
+DECLARE_REFCOUNTED_TYPE(ITypeErasedRowset)
+DECLARE_REFCOUNTED_STRUCT(IPersistentQueueRowset)
+DECLARE_REFCOUNTED_STRUCT(TSkynetSharePartsLocations)
+
+struct TConnectionOptions;
+
+using TClientOptions = NAuth::TAuthenticationOptions;
+
+struct TTransactionParticipantOptions;
+
+struct TTimeoutOptions;
+struct TTransactionalOptions;
+struct TPrerequisiteOptions;
+struct TMasterReadOptions;
+struct TMutatingOptions;
+struct TReshardTableOptions;
+struct TSuppressableAccessTrackingOptions;
+struct TTabletRangeOptions;
+
+struct TGetFileFromCacheResult;
+struct TPutFileToCacheResult;
+
+DECLARE_REFCOUNTED_STRUCT(IConnection)
+DECLARE_REFCOUNTED_STRUCT(IClientBase)
+DECLARE_REFCOUNTED_STRUCT(IClient)
+DECLARE_REFCOUNTED_STRUCT(IInternalClient)
+DECLARE_REFCOUNTED_STRUCT(ITransaction)
+DECLARE_REFCOUNTED_STRUCT(IStickyTransactionPool)
+
+DECLARE_REFCOUNTED_STRUCT(ITableReader)
+DECLARE_REFCOUNTED_STRUCT(ITableWriter)
+
+DECLARE_REFCOUNTED_STRUCT(IFileReader)
+DECLARE_REFCOUNTED_STRUCT(IFileWriter)
+
+DECLARE_REFCOUNTED_STRUCT(IJournalReader)
+DECLARE_REFCOUNTED_STRUCT(IJournalWriter)
+
+DECLARE_REFCOUNTED_CLASS(TPersistentQueuePoller)
+
+DECLARE_REFCOUNTED_CLASS(TTableMountCacheConfig)
+DECLARE_REFCOUNTED_CLASS(TConnectionConfig)
+DECLARE_REFCOUNTED_CLASS(TConnectionDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TPersistentQueuePollerConfig)
+
+DECLARE_REFCOUNTED_CLASS(TFileReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TFileWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TJournalReaderConfig)
+
+DECLARE_REFCOUNTED_CLASS(TJournalChunkWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TJournalWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TJournalChunkWriterOptions)
+
+DECLARE_REFCOUNTED_STRUCT(TSerializableMasterReadOptions)
+
+DECLARE_REFCOUNTED_STRUCT(TPrerequisiteRevisionConfig)
+
+DECLARE_REFCOUNTED_STRUCT(TDetailedProfilingInfo)
+
+DECLARE_REFCOUNTED_STRUCT(TSchedulingOptions)
+
+DECLARE_REFCOUNTED_CLASS(TJobInputReader)
+
+DECLARE_REFCOUNTED_CLASS(TClientCache)
+
+DECLARE_REFCOUNTED_STRUCT(TTableBackupManifest)
+DECLARE_REFCOUNTED_STRUCT(TBackupManifest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const TString ClusterNamePath("//sys/@cluster_name");
+inline const TString RpcProxiesPath("//sys/rpc_proxies");
+inline const TString GrpcProxiesPath("//sys/grpc_proxies");
+inline const TString AliveNodeName("alive");
+inline const TString BannedAttributeName("banned");
+inline const TString RoleAttributeName("role");
+inline const TString AddressesAttributeName("addresses");
+inline const TString BalancersAttributeName("balancers");
+inline const TString DefaultRpcProxyRole("default");
+inline const TString DefaultHttpProxyRole("data");
+inline const TString JournalPayloadKey("payload");
+inline const TString HunkPayloadKey("payload");
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMaintenanceType,
+ // 0 is reserved for None.
+ ((Ban) (1))
+ ((Decommission) (2))
+ ((DisableSchedulerJobs) (3))
+ ((DisableWriteSessions) (4))
+ ((DisableTabletCells) (5))
+ ((PendingRestart) (6))
+);
+
+DEFINE_ENUM(EMaintenanceComponent,
+ ((ClusterNode) (1))
+ ((HttpProxy) (2))
+ ((RpcProxy) (3))
+ ((Host) (4))
+);
+
+using TMaintenanceId = TGuid;
+
+using TMaintenanceCounts = TEnumIndexedVector<EMaintenanceType, int>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/rowset.cpp b/yt/yt/client/api/rowset.cpp
new file mode 100644
index 0000000000..c60d60f0c1
--- /dev/null
+++ b/yt/yt/client/api/rowset.cpp
@@ -0,0 +1,197 @@
+#include "rowset.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NApi {
+
+using namespace NTabletClient;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+class TRowsetBase
+ : public IRowset<TRow>
+{
+public:
+ explicit TRowsetBase(TTableSchemaPtr schema)
+ : Schema_(std::move(schema))
+ , NameTableInitialized_(false)
+ { }
+
+ explicit TRowsetBase(TNameTablePtr nameTable)
+ : NameTableInitialized_(true)
+ , NameTable_(std::move(nameTable))
+ { }
+
+ const TTableSchemaPtr& GetSchema() const override
+ {
+ return Schema_;
+ }
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ // Fast path.
+ if (NameTableInitialized_.load()) {
+ return NameTable_;
+ }
+
+ // Slow path.
+ auto guard = Guard(NameTableLock_);
+ if (!NameTable_) {
+ NameTable_ = TNameTable::FromSchema(*Schema_);
+ }
+ NameTableInitialized_ = true;
+ return NameTable_;
+ }
+
+private:
+ const TTableSchemaPtr Schema_;
+
+ mutable std::atomic<bool> NameTableInitialized_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, NameTableLock_);
+ mutable TNameTablePtr NameTable_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+class TRowset
+ : public TRowsetBase<TRow>
+{
+public:
+ TRowset(
+ TTableSchemaPtr schema,
+ TSharedRange<TRow> rows)
+ : TRowsetBase<TRow>(std::move(schema))
+ , Rows_(std::move(rows))
+ { }
+
+ TRowset(
+ TNameTablePtr nameTable,
+ TSharedRange<TRow> rows)
+ : TRowsetBase<TRow>(std::move(nameTable))
+ , Rows_(std::move(rows))
+ { }
+
+ TSharedRange<TRow> GetRows() const override
+ {
+ return Rows_;
+ }
+
+private:
+ const TSharedRange<TRow> Rows_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TRowset<TUnversionedRow>)
+DEFINE_REFCOUNTED_TYPE(TRowset<TVersionedRow>)
+
+template <class TRow>
+IRowsetPtr<TRow> CreateRowset(
+ NTableClient::TTableSchemaPtr schema,
+ TSharedRange<TRow> rows)
+{
+ return New<TRowset<TRow>>(std::move(schema), std::move(rows));
+}
+
+template
+IUnversionedRowsetPtr CreateRowset<TUnversionedRow>(
+ NTableClient::TTableSchemaPtr schema,
+ TSharedRange<TUnversionedRow> rows);
+template
+IVersionedRowsetPtr CreateRowset<TVersionedRow>(
+ NTableClient::TTableSchemaPtr schema,
+ TSharedRange<TVersionedRow> rows);
+template
+ITypeErasedRowsetPtr CreateRowset<TTypeErasedRow>(
+ NTableClient::TTableSchemaPtr schema,
+ TSharedRange<TTypeErasedRow> rows);
+
+template <class TRow>
+IRowsetPtr<TRow> CreateRowset(
+ TNameTablePtr nameTable,
+ TSharedRange<TRow> rows)
+{
+ return New<TRowset<TRow>>(std::move(nameTable), std::move(rows));
+}
+
+template
+IUnversionedRowsetPtr CreateRowset<TUnversionedRow>(
+ TNameTablePtr nameTable,
+ TSharedRange<TUnversionedRow> rows);
+template
+IVersionedRowsetPtr CreateRowset<TVersionedRow>(
+ TNameTablePtr nameTable,
+ TSharedRange<TVersionedRow> rows);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulRowsetWriter
+ : public TRowsetBase<TUnversionedRow>
+ , public IUnversionedRowsetWriter
+{
+public:
+ using TRowsetBase::TRowsetBase;
+
+ TSharedRange<TUnversionedRow> GetRows() const override
+ {
+ return MakeSharedRange(Rows_, RowBuffer_);
+ }
+
+ TFuture<IUnversionedRowsetPtr> GetResult() const
+ {
+ return Result_.ToFuture();
+ }
+
+ TFuture<void> Close() override
+ {
+ Result_.Set(IUnversionedRowsetPtr(this));
+ Result_.Reset();
+ return VoidFuture;
+ }
+
+ bool Write(TRange<TUnversionedRow> rows) override
+ {
+ for (auto row : rows) {
+ Rows_.push_back(RowBuffer_->CaptureRow(row));
+ }
+ return true;
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return VoidFuture;
+ }
+
+private:
+ const TTableSchemaPtr Schema_;
+ const TNameTablePtr NameTable_;
+
+ TPromise<IUnversionedRowsetPtr> Result_ = NewPromise<IUnversionedRowsetPtr>();
+
+ struct TSchemafulRowsetWriterBufferTag
+ { };
+
+ const TRowBufferPtr RowBuffer_ = New<TRowBuffer>(TSchemafulRowsetWriterBufferTag());
+ std::vector<TUnversionedRow> Rows_;
+
+};
+
+std::tuple<IUnversionedRowsetWriterPtr, TFuture<IUnversionedRowsetPtr>> CreateSchemafulRowsetWriter(TTableSchemaPtr schema)
+{
+ auto writer = New<TSchemafulRowsetWriter>(std::move(schema));
+ return std::make_tuple(writer, writer->GetResult());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/rowset.h b/yt/yt/client/api/rowset.h
new file mode 100644
index 0000000000..f70dfc438c
--- /dev/null
+++ b/yt/yt/client/api/rowset.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/shared_range.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+struct IRowset
+ : public virtual TRefCounted
+{
+ virtual const NTableClient::TTableSchemaPtr& GetSchema() const = 0;
+ virtual const NTableClient::TNameTablePtr& GetNameTable() const = 0;
+
+ virtual TSharedRange<TRow> GetRows() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IUnversionedRowset)
+DEFINE_REFCOUNTED_TYPE(IVersionedRowset)
+DEFINE_REFCOUNTED_TYPE(ITypeErasedRowset)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+IRowsetPtr<TRow> CreateRowset(
+ NTableClient::TTableSchemaPtr schema,
+ TSharedRange<TRow> rows);
+
+template <class TRow>
+IRowsetPtr<TRow> CreateRowset(
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<TRow> rows);
+
+std::tuple<NTableClient::IUnversionedRowsetWriterPtr, TFuture<IUnversionedRowsetPtr>>
+ CreateSchemafulRowsetWriter(NTableClient::TTableSchemaPtr schema);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/rpc_proxy/address_helpers.cpp b/yt/yt/client/api/rpc_proxy/address_helpers.cpp
new file mode 100644
index 0000000000..753e95d8a5
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/address_helpers.cpp
@@ -0,0 +1,72 @@
+#include "address_helpers.h"
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/local_address.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const EAddressType DefaultAddressType(EAddressType::InternalRpc);
+const TString DefaultNetworkName("default");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAddressMap GetLocalAddresses(const NNodeTrackerClient::TNetworkAddressList& addresses, int port)
+{
+ // Append port number.
+ TAddressMap result;
+ result.reserve(addresses.size());
+ for (const auto& [networkName, networkAddress] : addresses) {
+ YT_VERIFY(result.emplace(networkName, BuildServiceAddress(networkAddress, port)).second);
+ }
+
+ // Add default address.
+ result.emplace(DefaultNetworkName, BuildServiceAddress(GetLocalHostName(), port));
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TString> GetAddressOrNull(
+ const TProxyAddressMap& addresses,
+ EAddressType type,
+ const TString& network)
+{
+ auto typeAddressesIt = addresses.find(type);
+ if (typeAddressesIt != addresses.end()) {
+ auto it = typeAddressesIt->second.find(network);
+ if (it != typeAddressesIt->second.end()) {
+ return it->second;
+ }
+ }
+ return std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<std::vector<TString>> GetBalancersOrNull(
+ const TBalancersMap& balancers,
+ const TString& role,
+ EAddressType addressType,
+ const TString& network)
+{
+ auto roleBalancersIt = balancers.find(role);
+ if (roleBalancersIt != balancers.end()) {
+ auto roleTypeBalancersIt = roleBalancersIt->second.find(addressType);
+ if (roleTypeBalancersIt != roleBalancersIt->second.end()) {
+ auto it = roleTypeBalancersIt->second.find(network);
+ if (it != roleTypeBalancersIt->second.end()) {
+ return it->second;
+ }
+ }
+ }
+ return std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/address_helpers.h b/yt/yt/client/api/rpc_proxy/address_helpers.h
new file mode 100644
index 0000000000..c2f31764c6
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/address_helpers.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Network -> host:port.
+using TAddressMap = THashMap<TString, TString>;
+
+// Address type (e.g. RPC, HTTP) -> network -> host:port.
+using TProxyAddressMap = THashMap<EAddressType, TAddressMap>;
+
+extern const EAddressType DefaultAddressType;
+extern const TString DefaultNetworkName;
+
+// Network -> [host:port].
+using TNetworkAddressesMap = THashMap<TString, std::vector<TString>>;
+
+// Address type (e.g. RPC, HTTP) -> network -> [host:port].
+using TAddressTypeAddressesMap = THashMap<EAddressType, TNetworkAddressesMap>;
+
+// Role -> address type -> network -> host:port.
+using TBalancersMap = THashMap<TString, TAddressTypeAddressesMap>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAddressMap GetLocalAddresses(
+ const NNodeTrackerClient::TNetworkAddressList& addresses,
+ int port);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TString> GetAddressOrNull(
+ const TProxyAddressMap& addresses,
+ EAddressType addressType,
+ const TString& network);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<std::vector<TString>> GetBalancersOrNull(
+ const TBalancersMap& balancers,
+ const TString& role,
+ EAddressType addressType,
+ const TString& network);
+
+///////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/api_service_proxy.h b/yt/yt/client/api/rpc_proxy/api_service_proxy.h
new file mode 100644
index 0000000000..44010e35b3
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/api_service_proxy.h
@@ -0,0 +1,174 @@
+#pragma once
+
+#include "public.h"
+#include "protocol_version.h"
+
+#include <yt/yt_proto/yt/client/api/rpc_proxy/proto/api_service.pb.h>
+
+#include <yt/yt/core/rpc/client.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TApiServiceProxy
+ : public NRpc::TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TApiServiceProxy, ApiService,
+ .SetFeaturesType<ERpcProxyFeature>()
+ .SetProtocolVersion({
+ YTRpcProxyProtocolVersionMajor,
+ YTRpcProxyClientProtocolVersionMinor
+ }));
+
+ // Transaction server
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GenerateTimestamps);
+
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, StartTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PingTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CommitTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, FlushTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AbortTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AttachTransaction);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, DetachTransaction);
+
+ // Cypress server
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ExistsNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, SetNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, MultisetAttributesNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, RemoveNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ListNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CreateNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, LockNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, UnlockNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CopyNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, MoveNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, LinkNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ConcatenateNodes);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ExternalizeNode);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, InternalizeNode);
+
+ // Tablet server
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, MountTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, UnmountTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, RemountTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, FreezeTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, UnfreezeTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ReshardTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ReshardTableAutomatic);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, TrimTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AlterTable);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AlterTableReplica);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetTablePivotKeys);
+
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, LookupRows);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, VersionedLookupRows);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, MultiLookup);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, SelectRows);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ExplainQuery);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PullRows);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetInSyncReplicas);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetTabletInfos);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetTabletErrors);
+
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, BalanceTabletCells);
+
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ModifyRows);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, BatchModifyRows);
+
+ // Chaos
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AlterReplicationCard);
+
+ // Queues
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PullQueue);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PullConsumer);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, RegisterQueueConsumer);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, UnregisterQueueConsumer);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ListQueueConsumerRegistrations);
+
+ // Scheduler pools
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, TransferPoolResources);
+
+ // Operations
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, StartOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AbortOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, SuspendOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ResumeOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CompleteOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, UpdateOperationParameters);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetOperation);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ListOperations);
+
+ // Jobs
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ListJobs);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJob);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, DumpJobContext);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJobInput,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJobInputPaths);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJobSpec);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJobStderr);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetJobFailContext);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AbandonJob);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PollJobShell);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AbortJob);
+
+ // Files
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ReadFile,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, WriteFile,
+ .SetStreamingEnabled(true));
+
+ // Journals
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ReadJournal,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, WriteJournal,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, TruncateJournal);
+
+ // Tables
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ReadTable,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, WriteTable,
+ .SetStreamingEnabled(true));
+
+ // File caching
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetFileFromCache);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PutFileToCache);
+
+ // Object server
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CreateObject);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetTableMountInfo);
+
+ // Administration
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, BuildSnapshot);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GCCollect);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, SuspendCoordinator);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ResumeCoordinator);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, MigrateReplicationCards);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, SuspendChaosCells);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ResumeChaosCells);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AddMaintenance);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, RemoveMaintenance);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, DisableChunkLocations);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, DestroyChunkLocations);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, ResurrectChunkLocations);
+
+ // Security
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, AddMember);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, RemoveMember);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CheckPermission);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CheckPermissionByAcl);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, TransferAccountResources);
+
+ // Metadata
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, GetColumnarStatistics);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, PartitionTables);
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, CheckClusterLiveness);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/client_base.cpp b/yt/yt/client/api/rpc_proxy/client_base.cpp
new file mode 100644
index 0000000000..c7243150fe
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/client_base.cpp
@@ -0,0 +1,1020 @@
+#include "client_base.h"
+
+#include "api_service_proxy.h"
+#include "config.h"
+#include "file_reader.h"
+#include "file_writer.h"
+#include "helpers.h"
+#include "journal_reader.h"
+#include "journal_writer.h"
+#include "private.h"
+#include "table_reader.h"
+#include "table_writer.h"
+#include "transaction.h"
+
+#include <yt/yt/client/api/file_reader.h>
+#include <yt/yt/client/api/file_writer.h>
+#include <yt/yt/client/api/journal_reader.h>
+#include <yt/yt/client/api/journal_writer.h>
+#include <yt/yt/client/api/rowset.h>
+
+#include <yt/yt/client/chaos_client/replication_card_serialization.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_base.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/library/auth/credentials_injecting_channel.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/ytree/attribute_filter.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYPath;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NObjectClient;
+using namespace NTransactionClient;
+using namespace NYTree;
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IConnectionPtr TClientBase::GetConnection()
+{
+ return GetRpcProxyConnection();
+}
+
+TApiServiceProxy TClientBase::CreateApiServiceProxy(NRpc::IChannelPtr channel)
+{
+ if (!channel) {
+ channel = GetRetryingChannel();
+ }
+ TApiServiceProxy proxy(channel);
+ const auto& config = GetRpcProxyConnection()->GetConfig();
+ proxy.SetDefaultTimeout(config->RpcTimeout);
+ proxy.SetDefaultRequestCodec(config->RequestCodec);
+ proxy.SetDefaultResponseCodec(config->ResponseCodec);
+ proxy.SetDefaultEnableLegacyRpcCodecs(config->EnableLegacyRpcCodecs);
+
+ NRpc::TStreamingParameters streamingParameters;
+ streamingParameters.ReadTimeout = config->DefaultStreamingStallTimeout;
+ streamingParameters.WriteTimeout = config->DefaultStreamingStallTimeout;
+ proxy.DefaultClientAttachmentsStreamingParameters() = streamingParameters;
+ proxy.DefaultServerAttachmentsStreamingParameters() = streamingParameters;
+
+ return proxy;
+}
+
+void TClientBase::InitStreamingRequest(NRpc::TClientRequest& request)
+{
+ auto connection = GetRpcProxyConnection();
+ const auto& config = connection->GetConfig();
+ request.SetTimeout(config->DefaultTotalStreamingTimeout);
+}
+
+TFuture<ITransactionPtr> TClientBase::StartTransaction(
+ ETransactionType type,
+ const TTransactionStartOptions& options)
+{
+ // Keep some stuff to reuse it in the transaction.
+ auto connection = GetRpcProxyConnection();
+ auto client = GetRpcProxyClient();
+ bool sticky = (type == ETransactionType::Tablet) || options.Sticky;
+ // Based on this flag the sticky channel will be wrapped into retrying after the first non-retrying call.
+ bool wrapStickyChannelOnResponse = false;
+ NRpc::IChannelPtr channel;
+ if (sticky) {
+ channel = CreateNonRetryingStickyChannel();
+ if (options.Id == NullTransactionId) {
+ channel = WrapStickyChannelIntoRetrying(std::move(channel));
+ } else {
+ wrapStickyChannelOnResponse = true;
+ }
+ } else {
+ channel = GetRetryingChannel();
+ }
+
+ const auto& config = connection->GetConfig();
+ auto timeout = options.Timeout.value_or(config->DefaultTransactionTimeout);
+ auto pingPeriod = options.PingPeriod.value_or(config->DefaultPingPeriod);
+
+ auto proxy = CreateApiServiceProxy(channel);
+
+ auto req = proxy.StartTransaction();
+ req->SetTimeout(config->RpcTimeout);
+
+ req->set_type(static_cast<NProto::ETransactionType>(type));
+ req->set_timeout(ToProto<i64>(timeout));
+ if (options.Deadline) {
+ req->set_deadline(ToProto<ui64>(*options.Deadline));
+ }
+ if (options.Id) {
+ ToProto(req->mutable_id(), options.Id);
+ }
+ if (options.ParentId) {
+ ToProto(req->mutable_parent_id(), options.ParentId);
+ }
+ ToProto(req->mutable_prerequisite_transaction_ids(), options.PrerequisiteTransactionIds);
+ // XXX(sandello): Better? Remove these fields from the protocol at all?
+ // COMPAT(kiselyovp): remove auto_abort from the protocol
+ req->set_auto_abort(false);
+ req->set_sticky(sticky);
+ req->set_ping(options.Ping);
+ req->set_ping_ancestors(options.PingAncestors);
+ req->set_atomicity(static_cast<NProto::EAtomicity>(options.Atomicity));
+ req->set_durability(static_cast<NProto::EDurability>(options.Durability));
+ if (options.Attributes) {
+ ToProto(req->mutable_attributes(), *options.Attributes);
+ }
+ if (options.StartTimestamp != NullTimestamp) {
+ req->set_start_timestamp(options.StartTimestamp);
+ }
+
+ return req->Invoke().Apply(BIND(
+ [
+ =,
+ this,
+ this_ = MakeStrong(this),
+ connection = std::move(connection),
+ client = std::move(client),
+ channel = std::move(channel)
+ ]
+ (const TApiServiceProxy::TRspStartTransactionPtr& rsp) mutable {
+ std::optional<TStickyTransactionParameters> stickyParameters;
+ if (sticky) {
+ stickyParameters.emplace().ProxyAddress = rsp->GetAddress();
+ }
+ auto transactionId = FromProto<TTransactionId>(rsp->id());
+ auto startTimestamp = FromProto<TTimestamp>(rsp->start_timestamp());
+ if (wrapStickyChannelOnResponse) {
+ // The first call within the sticky channel has been non-retrying,
+ // so wrap the channel into retrying now for future use.
+ channel = WrapStickyChannelIntoRetrying(std::move(channel));
+ }
+ return CreateTransaction(
+ std::move(connection),
+ std::move(client),
+ std::move(channel),
+ transactionId,
+ startTimestamp,
+ type,
+ options.Atomicity,
+ options.Durability,
+ timeout,
+ options.PingAncestors,
+ pingPeriod,
+ std::move(stickyParameters),
+ rsp->sequence_number_source_id(),
+ "Transaction started");
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CYPRESS
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<bool> TClientBase::NodeExists(
+ const TYPath& path,
+ const TNodeExistsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ExistsNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspExistsNodePtr& rsp) {
+ return rsp->exists();
+ }));
+}
+
+TFuture<TYsonString> TClientBase::GetNode(
+ const TYPath& path,
+ const TGetNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ // COMPAT(max42): after 22.3 is everywhere, drop legacy field.
+ if (options.Attributes) {
+ ToProto(req->mutable_legacy_attributes()->mutable_keys(), options.Attributes.Keys);
+ ToProto(req->mutable_attributes(), options.Attributes);
+ } else {
+ req->mutable_legacy_attributes()->set_all(true);
+ }
+
+ if (options.MaxSize) {
+ req->set_max_size(*options.MaxSize);
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+ if (options.Options) {
+ ToProto(req->mutable_options(), *options.Options);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetNodePtr& rsp) {
+ return TYsonString(rsp->value());
+ }));
+}
+
+TFuture<TYsonString> TClientBase::ListNode(
+ const TYPath& path,
+ const TListNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ListNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ // COMPAT(max42): after 22.3 is everywhere, drop legacy field.
+ if (options.Attributes) {
+ ToProto(req->mutable_legacy_attributes()->mutable_keys(), options.Attributes.Keys);
+ ToProto(req->mutable_attributes(), options.Attributes);
+ } else {
+ req->mutable_legacy_attributes()->set_all(true);
+ }
+
+ if (options.MaxSize) {
+ req->set_max_size(*options.MaxSize);
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspListNodePtr& rsp) {
+ return TYsonString(rsp->value());
+ }));
+}
+
+TFuture<NCypressClient::TNodeId> TClientBase::CreateNode(
+ const TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CreateNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_type(ToProto<int>(type));
+
+ if (options.Attributes) {
+ ToProto(req->mutable_attributes(), *options.Attributes);
+ }
+ req->set_recursive(options.Recursive);
+ req->set_force(options.Force);
+ req->set_ignore_existing(options.IgnoreExisting);
+ req->set_lock_existing(options.LockExisting);
+ req->set_ignore_type_mismatch(options.IgnoreTypeMismatch);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspCreateNodePtr& rsp) {
+ return FromProto<NCypressClient::TNodeId>(rsp->node_id());
+ }));
+}
+
+TFuture<void> TClientBase::RemoveNode(
+ const TYPath& path,
+ const TRemoveNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.RemoveNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ req->set_recursive(options.Recursive);
+ req->set_force(options.Force);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClientBase::SetNode(
+ const TYPath& path,
+ const TYsonString& value,
+ const TSetNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.SetNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_value(value.ToString());
+ req->set_recursive(options.Recursive);
+ req->set_force(options.Force);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClientBase::MultisetAttributesNode(
+ const TYPath& path,
+ const IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.MultisetAttributesNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ auto children = attributes->GetChildren();
+ std::sort(children.begin(), children.end());
+ for (const auto& [attribute, value] : children) {
+ auto* protoSubrequest = req->add_subrequests();
+ protoSubrequest->set_attribute(attribute);
+ protoSubrequest->set_value(ConvertToYsonString(value).ToString());
+ }
+
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TLockNodeResult> TClientBase::LockNode(
+ const TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.LockNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_mode(ToProto<int>(mode));
+
+ req->set_waitable(options.Waitable);
+ if (options.ChildKey) {
+ req->set_child_key(*options.ChildKey);
+ }
+ if (options.AttributeKey) {
+ req->set_attribute_key(*options.AttributeKey);
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspLockNodePtr& rsp) {
+ TLockNodeResult result;
+ FromProto(&result.NodeId, rsp->node_id());
+ FromProto(&result.LockId, rsp->lock_id());
+ FromProto(&result.Revision, rsp->revision());
+ return result;
+ }));
+}
+
+TFuture<void> TClientBase::UnlockNode(
+ const TYPath& path,
+ const TUnlockNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.UnlockNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<NCypressClient::TNodeId> TClientBase::CopyNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TCopyNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CopyNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_src_path(srcPath);
+ req->set_dst_path(dstPath);
+
+ req->set_recursive(options.Recursive);
+ req->set_ignore_existing(options.IgnoreExisting);
+ req->set_lock_existing(options.LockExisting);
+ req->set_force(options.Force);
+ req->set_preserve_account(options.PreserveAccount);
+ req->set_preserve_creation_time(options.PreserveCreationTime);
+ req->set_preserve_modification_time(options.PreserveModificationTime);
+ req->set_preserve_expiration_time(options.PreserveExpirationTime);
+ req->set_preserve_expiration_timeout(options.PreserveExpirationTimeout);
+ req->set_preserve_owner(options.PreserveOwner);
+ req->set_preserve_acl(options.PreserveAcl);
+ req->set_pessimistic_quota_check(options.PessimisticQuotaCheck);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspCopyNodePtr& rsp) {
+ return FromProto<NCypressClient::TNodeId>(rsp->node_id());
+ }));
+}
+
+TFuture<NCypressClient::TNodeId> TClientBase::MoveNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TMoveNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.MoveNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_src_path(srcPath);
+ req->set_dst_path(dstPath);
+
+ req->set_recursive(options.Recursive);
+ req->set_force(options.Force);
+ req->set_preserve_account(options.PreserveAccount);
+ req->set_preserve_creation_time(options.PreserveCreationTime);
+ req->set_preserve_modification_time(options.PreserveModificationTime);
+ req->set_preserve_expiration_time(options.PreserveExpirationTime);
+ req->set_preserve_expiration_timeout(options.PreserveExpirationTimeout);
+ req->set_preserve_owner(options.PreserveOwner);
+ req->set_pessimistic_quota_check(options.PessimisticQuotaCheck);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspMoveNodePtr& rsp) {
+ return FromProto<NCypressClient::TNodeId>(rsp->node_id());
+ }));
+}
+
+TFuture<NCypressClient::TNodeId> TClientBase::LinkNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TLinkNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.LinkNode();
+ SetTimeoutOptions(*req, options);
+
+ req->set_src_path(srcPath);
+ req->set_dst_path(dstPath);
+
+ req->set_recursive(options.Recursive);
+ req->set_force(options.Force);
+ req->set_ignore_existing(options.IgnoreExisting);
+ req->set_lock_existing(options.LockExisting);
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspLinkNodePtr& rsp) {
+ return FromProto<NCypressClient::TNodeId>(rsp->node_id());
+ }));
+}
+
+TFuture<void> TClientBase::ConcatenateNodes(
+ const std::vector<TRichYPath>& srcPaths,
+ const TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ConcatenateNodes();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_src_paths(), srcPaths);
+ ToProto(req->mutable_dst_path(), dstPath);
+ ToProto(req->mutable_transactional_options(), options);
+ // TODO(babenko)
+ // ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClientBase::ExternalizeNode(
+ const TYPath& path,
+ TCellTag cellTag,
+ const TExternalizeNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ExternalizeNode();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_path(), path);
+ req->set_cell_tag(ToProto<int>(cellTag));
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClientBase::InternalizeNode(
+ const TYPath& path,
+ const TInternalizeNodeOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.InternalizeNode();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_path(), path);
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<NObjectClient::TObjectId> TClientBase::CreateObject(
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.CreateObject();
+
+ req->set_type(ToProto<int>(type));
+ req->set_ignore_existing(options.IgnoreExisting);
+ req->set_sync(options.Sync);
+ if (options.Attributes) {
+ ToProto(req->mutable_attributes(), *options.Attributes);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspCreateObjectPtr& rsp) {
+ return FromProto<NObjectClient::TObjectId>(rsp->object_id());
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<IFileReaderPtr> TClientBase::CreateFileReader(
+ const TYPath& path,
+ const TFileReaderOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.ReadFile();
+ InitStreamingRequest(*req);
+
+ req->set_path(path);
+ if (options.Offset) {
+ req->set_offset(*options.Offset);
+ }
+ if (options.Length) {
+ req->set_length(*options.Length);
+ }
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+
+ return NRpcProxy::CreateFileReader(std::move(req));
+}
+
+IFileWriterPtr TClientBase::CreateFileWriter(
+ const TRichYPath& path,
+ const TFileWriterOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.WriteFile();
+ InitStreamingRequest(*req);
+
+ ToProto(req->mutable_path(), path);
+
+ req->set_compute_md5(options.ComputeMD5);
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return NRpcProxy::CreateFileWriter(std::move(req));
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+IJournalReaderPtr TClientBase::CreateJournalReader(
+ const TYPath& path,
+ const TJournalReaderOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.ReadJournal();
+ InitStreamingRequest(*req);
+
+ req->set_path(path);
+
+ if (options.FirstRowIndex) {
+ req->set_first_row_index(*options.FirstRowIndex);
+ }
+ if (options.RowCount) {
+ req->set_row_count(*options.RowCount);
+ }
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+
+ return NRpcProxy::CreateJournalReader(std::move(req));
+}
+
+IJournalWriterPtr TClientBase::CreateJournalWriter(
+ const TYPath& path,
+ const TJournalWriterOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.WriteJournal();
+ InitStreamingRequest(*req);
+
+ req->set_path(path);
+
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ req->set_enable_multiplexing(options.EnableMultiplexing);
+ req->set_enable_chunk_preallocation(options.EnableChunkPreallocation);
+ req->set_replica_lag_limit(options.ReplicaLagLimit);
+ // TODO(kiselyovp) profiler is ignored
+
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return NRpcProxy::CreateJournalWriter(std::move(req));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<ITableReaderPtr> TClientBase::CreateTableReader(
+ const TRichYPath& path,
+ const TTableReaderOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.ReadTable();
+ InitStreamingRequest(*req);
+
+ ToProto(req->mutable_path(), path);
+
+ req->set_unordered(options.Unordered);
+ req->set_omit_inaccessible_columns(options.OmitInaccessibleColumns);
+ req->set_enable_table_index(options.EnableTableIndex);
+ req->set_enable_row_index(options.EnableRowIndex);
+ req->set_enable_range_index(options.EnableRangeIndex);
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+
+ return NRpcProxy::CreateTableReader(std::move(req));
+}
+
+TFuture<ITableWriterPtr> TClientBase::CreateTableWriter(
+ const TRichYPath& path,
+ const TTableWriterOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.WriteTable();
+ InitStreamingRequest(*req);
+
+ ToProto(req->mutable_path(), path);
+
+ if (options.Config) {
+ req->set_config(ConvertToYsonString(*options.Config).ToString());
+ }
+
+ ToProto(req->mutable_transactional_options(), options);
+
+ return NRpcProxy::CreateTableWriter(std::move(req));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<IUnversionedRowsetPtr> TClientBase::LookupRows(
+ const TYPath& path,
+ TNameTablePtr nameTable,
+ const TSharedRange<TLegacyKey>& keys,
+ const TLookupRowsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.LookupRows();
+ req->SetResponseHeavy(true);
+ req->SetTimeout(options.Timeout.value_or(GetRpcProxyConnection()->GetConfig()->DefaultLookupRowsTimeout));
+
+ req->set_path(path);
+ req->Attachments() = SerializeRowset(nameTable, keys, req->mutable_rowset_descriptor());
+
+ if (!options.ColumnFilter.IsUniversal()) {
+ for (auto id : options.ColumnFilter.GetIndexes()) {
+ req->add_columns(TString(nameTable->GetName(id)));
+ }
+ }
+ req->set_timestamp(options.Timestamp);
+ req->set_retention_timestamp(options.RetentionTimestamp);
+ req->set_keep_missing_rows(options.KeepMissingRows);
+ req->set_enable_partial_result(options.EnablePartialResult);
+ req->set_replica_consistency(static_cast<NProto::EReplicaConsistency>(options.ReplicaConsistency));
+ if (options.UseLookupCache) {
+ req->set_use_lookup_cache(*options.UseLookupCache);
+ }
+
+ req->SetMultiplexingBand(options.MultiplexingBand);
+ req->set_multiplexing_band(static_cast<NProto::EMultiplexingBand>(options.MultiplexingBand));
+
+ ToProto(req->mutable_tablet_read_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspLookupRowsPtr& rsp) {
+ return DeserializeRowset<TUnversionedRow>(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()));
+ }));
+}
+
+TFuture<IVersionedRowsetPtr> TClientBase::VersionedLookupRows(
+ const TYPath& path,
+ TNameTablePtr nameTable,
+ const TSharedRange<TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.VersionedLookupRows();
+ req->SetResponseHeavy(true);
+ req->SetTimeout(options.Timeout.value_or(GetRpcProxyConnection()->GetConfig()->DefaultLookupRowsTimeout));
+
+ req->set_path(path);
+ req->Attachments() = SerializeRowset(nameTable, keys, req->mutable_rowset_descriptor());
+
+ if (!options.ColumnFilter.IsUniversal()) {
+ for (auto id : options.ColumnFilter.GetIndexes()) {
+ req->add_columns(TString(nameTable->GetName(id)));
+ }
+ }
+ req->set_timestamp(options.Timestamp);
+ req->set_keep_missing_rows(options.KeepMissingRows);
+ req->set_enable_partial_result(options.EnablePartialResult);
+ req->set_replica_consistency(static_cast<NProto::EReplicaConsistency>(options.ReplicaConsistency));
+ if (options.UseLookupCache) {
+ req->set_use_lookup_cache(*options.UseLookupCache);
+ }
+
+ req->SetMultiplexingBand(options.MultiplexingBand);
+ req->set_multiplexing_band(static_cast<NProto::EMultiplexingBand>(options.MultiplexingBand));
+ if (options.RetentionConfig) {
+ ToProto(req->mutable_retention_config(), *options.RetentionConfig);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspVersionedLookupRowsPtr& rsp) {
+ return DeserializeRowset<TVersionedRow>(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()));
+ }));
+}
+
+TFuture<std::vector<IUnversionedRowsetPtr>> TClientBase::MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.MultiLookup();
+ req->SetResponseHeavy(true);
+ req->SetTimeout(options.Timeout.value_or(GetRpcProxyConnection()->GetConfig()->DefaultLookupRowsTimeout));
+ req->SetMultiplexingBand(options.MultiplexingBand);
+
+ for (const auto& subrequest : subrequests) {
+ auto* protoSubrequest = req->add_subrequests();
+
+ protoSubrequest->set_path(subrequest.Path);
+
+ const auto& subrequestOptions = subrequest.Options;
+ if (!subrequestOptions.ColumnFilter.IsUniversal()) {
+ for (auto id : subrequestOptions.ColumnFilter.GetIndexes()) {
+ protoSubrequest->add_columns(TString(subrequest.NameTable->GetName(id)));
+ }
+ }
+ protoSubrequest->set_keep_missing_rows(subrequestOptions.KeepMissingRows);
+ protoSubrequest->set_enable_partial_result(subrequestOptions.EnablePartialResult);
+ if (subrequestOptions.UseLookupCache) {
+ protoSubrequest->set_use_lookup_cache(*subrequestOptions.UseLookupCache);
+ }
+
+ auto rowset = SerializeRowset(
+ subrequest.NameTable,
+ subrequest.Keys,
+ protoSubrequest->mutable_rowset_descriptor());
+ protoSubrequest->set_attachment_count(rowset.size());
+ req->Attachments().insert(req->Attachments().end(), rowset.begin(), rowset.end());
+ }
+
+ req->set_replica_consistency(static_cast<NProto::EReplicaConsistency>(options.ReplicaConsistency));
+ req->set_timestamp(options.Timestamp);
+ req->set_retention_timestamp(options.RetentionTimestamp);
+ req->set_multiplexing_band(static_cast<NProto::EMultiplexingBand>(options.MultiplexingBand));
+ ToProto(req->mutable_tablet_read_options(), options);
+
+ return req->Invoke().Apply(BIND([subrequestCount = std::ssize(subrequests)] (const TApiServiceProxy::TRspMultiLookupPtr& rsp) {
+ YT_VERIFY(subrequestCount == rsp->subresponses_size());
+
+ std::vector<IUnversionedRowsetPtr> result;
+ result.reserve(subrequestCount);
+
+ int beginAttachmentIndex = 0;
+ for (const auto& subresponse : rsp->subresponses()) {
+ int endAttachmentIndex = beginAttachmentIndex + subresponse.attachment_count();
+ YT_VERIFY(endAttachmentIndex <= std::ssize(rsp->Attachments()));
+
+ std::vector<TSharedRef> subresponseAttachments{
+ rsp->Attachments().begin() + beginAttachmentIndex,
+ rsp->Attachments().begin() + endAttachmentIndex};
+ result.push_back(DeserializeRowset<TUnversionedRow>(
+ subresponse.rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(std::move(subresponseAttachments))));
+
+ beginAttachmentIndex = endAttachmentIndex;
+ }
+ YT_VERIFY(beginAttachmentIndex == std::ssize(rsp->Attachments()));
+
+ return result;
+ }));
+}
+
+template<class TRequest>
+void FillRequestBySelectRowsOptionsBase(const TSelectRowsOptionsBase& options, TRequest request)
+{
+ request->set_timestamp(options.Timestamp);
+ if (options.UdfRegistryPath) {
+ request->set_udf_registry_path(*options.UdfRegistryPath);
+ }
+}
+
+TFuture<TSelectRowsResult> TClientBase::SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.SelectRows();
+ req->SetResponseHeavy(true);
+ req->set_query(query);
+
+ FillRequestBySelectRowsOptionsBase(options, req);
+ // TODO(ifsmirnov): retention timestamp in explain_query.
+ req->set_retention_timestamp(options.RetentionTimestamp);
+ // TODO(lukyan): Move to FillRequestBySelectRowsOptionsBase
+ req->SetTimeout(options.Timeout.value_or(GetRpcProxyConnection()->GetConfig()->DefaultSelectRowsTimeout));
+
+ if (options.InputRowLimit) {
+ req->set_input_row_limit(*options.InputRowLimit);
+ }
+ if (options.OutputRowLimit) {
+ req->set_output_row_limit(*options.OutputRowLimit);
+ }
+ req->set_range_expansion_limit(options.RangeExpansionLimit);
+ req->set_max_subqueries(options.MaxSubqueries);
+ req->set_allow_full_scan(options.AllowFullScan);
+ req->set_allow_join_without_index(options.AllowJoinWithoutIndex);
+
+ if (options.ExecutionPool) {
+ req->set_execution_pool(*options.ExecutionPool);
+ }
+ if (options.PlaceholderValues) {
+ req->set_placeholder_values(options.PlaceholderValues.ToString());
+ }
+ req->set_fail_on_incomplete_result(options.FailOnIncompleteResult);
+ req->set_verbose_logging(options.VerboseLogging);
+ req->set_enable_code_cache(options.EnableCodeCache);
+ req->set_memory_limit_per_node(options.MemoryLimitPerNode);
+ ToProto(req->mutable_suppressable_access_tracking_options(), options);
+ req->set_replica_consistency(static_cast<NProto::EReplicaConsistency>(options.ReplicaConsistency));
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspSelectRowsPtr& rsp) {
+ TSelectRowsResult result;
+ result.Rowset = DeserializeRowset<TUnversionedRow>(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()));
+ FromProto(&result.Statistics, rsp->statistics());
+ return result;
+ }));
+}
+
+TFuture<TYsonString> TClientBase::ExplainQuery(
+ const TString& query,
+ const TExplainQueryOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ExplainQuery();
+ req->set_query(query);
+ FillRequestBySelectRowsOptionsBase(options, req);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspExplainQueryPtr& rsp) {
+ return TYsonString(rsp->value());
+ }));
+}
+
+TFuture<TPullRowsResult> TClientBase::PullRows(
+ const TYPath& path,
+ const TPullRowsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PullRows();
+ req->SetResponseHeavy(true);
+ req->set_path(path);
+ ToProto(req->mutable_upstream_replica_id(), options.UpstreamReplicaId);
+ req->set_order_rows_by_timestamp(options.OrderRowsByTimestamp);
+ req->set_tablet_rows_per_read(options.TabletRowsPerRead);
+ ToProto(req->mutable_replication_progress(), options.ReplicationProgress);
+ if (options.UpperTimestamp != NullTimestamp) {
+ req->set_upper_timestamp(options.UpperTimestamp);
+ }
+ for (auto [tabletId, rowIndex] : options.StartReplicationRowIndexes) {
+ auto *protoReplicationRowIndex = req->add_start_replication_row_indexes();
+ ToProto(protoReplicationRowIndex->mutable_tablet_id(), tabletId);
+ protoReplicationRowIndex->set_row_index(rowIndex);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPullRowsPtr& rsp) {
+ TPullRowsResult result;
+ result.RowCount = rsp->row_count();
+ result.DataWeight = rsp->data_weight();
+ result.Versioned = rsp->versioned();
+ FromProto(&result.ReplicationProgress, rsp->replication_progress());
+
+ for (auto protoReplicationRowIndex : rsp->end_replication_row_indexes()) {
+ auto tabletId = FromProto<TTabletId>(protoReplicationRowIndex.tablet_id());
+ int rowIndex = protoReplicationRowIndex.row_index();
+ if (result.EndReplicationRowIndexes.contains(tabletId)) {
+ THROW_ERROR_EXCEPTION("Duplicate tablet id in end replication row indexes")
+ << TErrorAttribute("tablet_id", tabletId);
+ }
+ InsertOrCrash(result.EndReplicationRowIndexes, std::make_pair(tabletId, rowIndex));
+ }
+
+ result.Rowset = DeserializeRowset(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()),
+ rsp->versioned());
+
+ return result;
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/client_base.h b/yt/yt/client/api/rpc_proxy/client_base.h
new file mode 100644
index 0000000000..f8df2a67a8
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/client_base.h
@@ -0,0 +1,192 @@
+#pragma once
+
+#include "private.h"
+#include "helpers.h"
+#include "connection_impl.h"
+#include "api_service_proxy.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/driver/private.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRpcProxyClientBufferTag
+{ };
+
+class TClientBase
+ : public virtual NApi::IClientBase
+{
+protected:
+ // Returns a bound RPC proxy connection for this interface.
+ virtual TConnectionPtr GetRpcProxyConnection() = 0;
+ // Returns a bound RPC proxy client for this interface.
+ virtual TClientPtr GetRpcProxyClient() = 0;
+ // Returns an RPC channel to use for API calls.
+ virtual NRpc::IChannelPtr GetRetryingChannel() const = 0;
+ // Returns an RPC channel to use for API calls within single transaction.
+ // The channel is non-retrying, so should be wrapped into retrying channel on demand.
+ virtual NRpc::IChannelPtr CreateNonRetryingStickyChannel() const = 0;
+ // Wraps the underlying sticky channel into retrying channel.
+ // Stickiness affects retries policy.
+ virtual NRpc::IChannelPtr WrapStickyChannelIntoRetrying(NRpc::IChannelPtr underlying) const = 0;
+
+ friend class TTransaction;
+
+public:
+ NApi::IConnectionPtr GetConnection() override;
+
+ // Helpers for accessing low-lepel API service proxy.
+ // Prefer using interface methods unless you know exactly what you are doing.
+ TApiServiceProxy CreateApiServiceProxy(NRpc::IChannelPtr channel = {});
+ void InitStreamingRequest(NRpc::TClientRequest& request);
+
+ // Transactions.
+ TFuture<NApi::ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const NApi::TTransactionStartOptions& options) override;
+
+ // Tables.
+ TFuture<NApi::IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const NApi::TLookupRowsOptions& options) override;
+
+ TFuture<NApi::IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const NApi::TVersionedLookupRowsOptions& options) override;
+
+ TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options = {}) override;
+
+ TFuture<NApi::TSelectRowsResult> SelectRows(
+ const TString& query,
+ const NApi::TSelectRowsOptions& options) override;
+
+ TFuture<NYson::TYsonString> ExplainQuery(
+ const TString& query,
+ const NApi::TExplainQueryOptions& options) override;
+
+ virtual TFuture<TPullRowsResult> PullRows(
+ const NYPath::TYPath& path,
+ const NApi::TPullRowsOptions& options) override;
+
+ // TODO(babenko): batch read and batch write.
+
+ // Cypress.
+ TFuture<bool> NodeExists(
+ const NYPath::TYPath& path,
+ const NApi::TNodeExistsOptions& options) override;
+
+ TFuture<NYson::TYsonString> GetNode(
+ const NYPath::TYPath& path,
+ const NApi::TGetNodeOptions& options) override;
+
+ TFuture<void> SetNode(
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const NApi::TSetNodeOptions& options) override;
+
+ TFuture<void> MultisetAttributesNode(
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const NApi::TMultisetAttributesNodeOptions& options) override;
+
+ TFuture<void> RemoveNode(
+ const NYPath::TYPath& path,
+ const NApi::TRemoveNodeOptions& options) override;
+
+ TFuture<NYson::TYsonString> ListNode(
+ const NYPath::TYPath& path,
+ const NApi::TListNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> CreateNode(
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const NApi::TCreateNodeOptions& options) override;
+
+ TFuture<NApi::TLockNodeResult> LockNode(
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const NApi::TLockNodeOptions& options) override;
+
+ TFuture<void> UnlockNode(
+ const NYPath::TYPath& path,
+ const NApi::TUnlockNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> CopyNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TCopyNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> MoveNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TMoveNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> LinkNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TLinkNodeOptions& options) override;
+
+ TFuture<void> ConcatenateNodes(
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const NApi::TConcatenateNodesOptions& options) override;
+
+ TFuture<void> ExternalizeNode(
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options = {}) override;
+
+ TFuture<void> InternalizeNode(
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options = {}) override;
+
+ // Objects.
+ TFuture<NObjectClient::TObjectId> CreateObject(
+ NObjectClient::EObjectType type,
+ const NApi::TCreateObjectOptions& options) override;
+
+ //! NB: Readers and writers returned by methods below are NOT thread-safe.
+ // Files.
+ TFuture<NApi::IFileReaderPtr> CreateFileReader(
+ const NYPath::TYPath& path,
+ const NApi::TFileReaderOptions& options) override;
+
+ NApi::IFileWriterPtr CreateFileWriter(
+ const NYPath::TRichYPath& path,
+ const NApi::TFileWriterOptions& options) override;
+
+ // Journals.
+ NApi::IJournalReaderPtr CreateJournalReader(
+ const NYPath::TYPath& path,
+ const NApi::TJournalReaderOptions& options) override;
+
+ NApi::IJournalWriterPtr CreateJournalWriter(
+ const NYPath::TYPath& path,
+ const NApi::TJournalWriterOptions& options) override;
+
+ // Tables.
+ TFuture<NApi::ITableReaderPtr> CreateTableReader(
+ const NYPath::TRichYPath& path,
+ const NApi::TTableReaderOptions& options) override;
+
+ TFuture<NApi::ITableWriterPtr> CreateTableWriter(
+ const NYPath::TRichYPath& path,
+ const NApi::TTableWriterOptions& options) override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/client_impl.cpp b/yt/yt/client/api/rpc_proxy/client_impl.cpp
new file mode 100644
index 0000000000..9b6b2e70ea
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/client_impl.cpp
@@ -0,0 +1,2039 @@
+#include "client_impl.h"
+
+#include "config.h"
+#include "helpers.h"
+#include "private.h"
+#include "table_mount_cache.h"
+#include "timestamp_provider.h"
+#include "transaction.h"
+
+#include <yt/yt/client/api/helpers.h>
+#include <yt/yt/client/api/rowset.h>
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/client/chaos_client/replication_card_serialization.h>
+
+#include <yt/yt/client/transaction_client/remote_timestamp_provider.h>
+
+#include <yt/yt/client/scheduler/operation_id_or_alias.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_base.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/library/auth/credentials_injecting_channel.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/rpc/dynamic_channel_pool.h>
+#include <yt/yt/core/rpc/retrying_channel.h>
+#include <yt/yt/core/rpc/stream.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <util/generic/cast.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+using namespace NAuth;
+using namespace NChaosClient;
+using namespace NChunkClient;
+using namespace NObjectClient;
+using namespace NRpc;
+using namespace NScheduler;
+using namespace NQueueClient;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NTransactionClient;
+using namespace NYPath;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateCredentialsInjectingChannel(
+ IChannelPtr underlying,
+ const TClientOptions& options)
+{
+ if (options.Token) {
+ return CreateTokenInjectingChannel(
+ underlying,
+ options);
+ } else if (options.SessionId || options.SslSessionId) {
+ return CreateCookieInjectingChannel(
+ underlying,
+ options);
+ } else if (options.ServiceTicketAuth) {
+ return CreateServiceTicketInjectingChannel(
+ underlying,
+ options);
+ } else {
+ return CreateUserInjectingChannel(underlying, options);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClient::TClient(
+ TConnectionPtr connection,
+ const TClientOptions& clientOptions)
+ : Connection_(std::move(connection))
+ , RetryingChannel_(MaybeCreateRetryingChannel(
+ CreateCredentialsInjectingChannel(
+ Connection_->CreateChannel(false),
+ clientOptions),
+ /*retryProxyBanned*/ true))
+ , ClientOptions_(clientOptions)
+ , TableMountCache_(BIND(
+ &CreateTableMountCache,
+ Connection_->GetConfig()->TableMountCache,
+ RetryingChannel_,
+ RpcProxyClientLogger,
+ Connection_->GetConfig()->RpcTimeout))
+ , TimestampProvider_(BIND(&TClient::CreateTimestampProvider, Unretained(this)))
+{ }
+
+const ITableMountCachePtr& TClient::GetTableMountCache()
+{
+ return TableMountCache_.Value();
+}
+
+const IReplicationCardCachePtr& TClient::GetReplicationCardCache()
+{
+ YT_UNIMPLEMENTED();
+}
+
+const ITimestampProviderPtr& TClient::GetTimestampProvider()
+{
+ return TimestampProvider_.Value();
+}
+
+void TClient::Terminate()
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr TClient::MaybeCreateRetryingChannel(NRpc::IChannelPtr channel, bool retryProxyBanned) const
+{
+ const auto& config = Connection_->GetConfig();
+ if (config->EnableRetries) {
+ return NRpc::CreateRetryingChannel(
+ config->RetryingChannel,
+ std::move(channel),
+ BIND([=] (const TError& error) {
+ return IsRetriableError(error, retryProxyBanned);
+ }));
+ } else {
+ return channel;
+ }
+}
+
+IChannelPtr TClient::CreateNonRetryingChannelByAddress(const TString& address) const
+{
+ return CreateCredentialsInjectingChannel(
+ Connection_->CreateChannelByAddress(address),
+ ClientOptions_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConnectionPtr TClient::GetRpcProxyConnection()
+{
+ return Connection_;
+}
+
+TClientPtr TClient::GetRpcProxyClient()
+{
+ return this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr TClient::GetRetryingChannel() const
+{
+ return RetryingChannel_;
+}
+
+IChannelPtr TClient::CreateNonRetryingStickyChannel() const
+{
+ return CreateCredentialsInjectingChannel(
+ Connection_->CreateChannel(true),
+ ClientOptions_);
+}
+
+IChannelPtr TClient::WrapStickyChannelIntoRetrying(IChannelPtr underlying) const
+{
+ return MaybeCreateRetryingChannel(
+ std::move(underlying),
+ /*retryProxyBanned*/ false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr TClient::CreateTimestampProvider() const
+{
+ return NRpcProxy::CreateTimestampProvider(
+ RetryingChannel_,
+ Connection_->GetConfig()->RpcTimeout,
+ Connection_->GetConfig()->TimestampProviderLatestTimestampUpdatePeriod,
+ Connection_->GetConfig()->ClockClusterTag);
+}
+
+ITransactionPtr TClient::AttachTransaction(
+ TTransactionId transactionId,
+ const TTransactionAttachOptions& options)
+{
+ auto connection = GetRpcProxyConnection();
+ auto client = GetRpcProxyClient();
+
+ auto channel = options.StickyAddress
+ ? WrapStickyChannelIntoRetrying(CreateNonRetryingChannelByAddress(options.StickyAddress))
+ : GetRetryingChannel();
+
+ auto proxy = CreateApiServiceProxy(channel);
+
+ auto req = proxy.AttachTransaction();
+ ToProto(req->mutable_transaction_id(), transactionId);
+ // COMPAT(kiselyovp): remove auto_abort from the protocol
+ req->set_auto_abort(false);
+ if (options.PingPeriod) {
+ req->set_ping_period(options.PingPeriod->GetValue());
+ }
+ req->set_ping(options.Ping);
+ req->set_ping_ancestors(options.PingAncestors);
+
+ auto rsp = NConcurrency::WaitFor(req->Invoke())
+ .ValueOrThrow();
+
+ auto transactionType = static_cast<ETransactionType>(rsp->type());
+ auto startTimestamp = static_cast<TTimestamp>(rsp->start_timestamp());
+ auto atomicity = static_cast<EAtomicity>(rsp->atomicity());
+ auto durability = static_cast<EDurability>(rsp->durability());
+ auto timeout = TDuration::FromValue(NYT::FromProto<i64>(rsp->timeout()));
+
+ if (options.StickyAddress && transactionType != ETransactionType::Tablet) {
+ THROW_ERROR_EXCEPTION("Sticky address is supported for tablet transactions only");
+ }
+
+ std::optional<TStickyTransactionParameters> stickyParameters;
+ if (options.StickyAddress || transactionType == ETransactionType::Tablet) {
+ stickyParameters.emplace();
+ if (options.StickyAddress) {
+ stickyParameters->ProxyAddress = options.StickyAddress;
+ } else {
+ stickyParameters->ProxyAddress = rsp->GetAddress();
+ }
+ }
+
+ return CreateTransaction(
+ std::move(connection),
+ std::move(client),
+ std::move(channel),
+ transactionId,
+ startTimestamp,
+ transactionType,
+ atomicity,
+ durability,
+ timeout,
+ options.PingAncestors,
+ options.PingPeriod,
+ std::move(stickyParameters),
+ rsp->sequence_number_source_id(),
+ "Transaction attached");
+}
+
+TFuture<void> TClient::MountTable(
+ const TYPath& path,
+ const TMountTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.MountTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ NYT::ToProto(req->mutable_cell_id(), options.CellId);
+ if (!options.TargetCellIds.empty()) {
+ NYT::ToProto(req->mutable_target_cell_ids(), options.TargetCellIds);
+ }
+ req->set_freeze(options.Freeze);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::UnmountTable(
+ const TYPath& path,
+ const TUnmountTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.UnmountTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ req->set_force(options.Force);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::RemountTable(
+ const TYPath& path,
+ const TRemountTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.RemountTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::FreezeTable(
+ const TYPath& path,
+ const TFreezeTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.FreezeTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::UnfreezeTable(
+ const TYPath& path,
+ const TUnfreezeTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.UnfreezeTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::ReshardTable(
+ const TYPath& path,
+ const std::vector<TLegacyOwningKey>& pivotKeys,
+ const TReshardTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ReshardTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ auto writer = CreateWireProtocolWriter();
+ // XXX(sandello): This is ugly and inefficient.
+ std::vector<TUnversionedRow> keys;
+ for (const auto& pivotKey : pivotKeys) {
+ keys.push_back(pivotKey);
+ }
+ writer->WriteRowset(MakeRange(keys));
+ req->Attachments() = writer->Finish();
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::ReshardTable(
+ const TYPath& path,
+ int tabletCount,
+ const TReshardTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ReshardTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_tablet_count(tabletCount);
+ if (options.Uniform) {
+ req->set_uniform(*options.Uniform);
+ }
+ if (options.EnableSlicing) {
+ req->set_enable_slicing(*options.EnableSlicing);
+ }
+ if (options.SlicingAccuracy) {
+ req->set_slicing_accuracy(*options.SlicingAccuracy);
+ }
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<std::vector<TTabletActionId>> TClient::ReshardTableAutomatic(
+ const TYPath& path,
+ const TReshardTableAutomaticOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ReshardTableAutomatic();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_keep_actions(options.KeepActions);
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_tablet_range_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspReshardTableAutomaticPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ return FromProto<std::vector<TTabletActionId>>(rsp->tablet_actions());
+ }));
+}
+
+TFuture<void> TClient::TrimTable(
+ const TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const TTrimTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.TrimTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_tablet_index(tabletIndex);
+ req->set_trimmed_row_count(trimmedRowCount);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::AlterTable(
+ const TYPath& path,
+ const TAlterTableOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AlterTable();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ if (options.Schema) {
+ req->set_schema(ConvertToYsonString(*options.Schema).ToString());
+ }
+ if (options.SchemaId) {
+ ToProto(req->mutable_schema_id(), *options.SchemaId);
+ }
+ if (options.Dynamic) {
+ req->set_dynamic(*options.Dynamic);
+ }
+ if (options.UpstreamReplicaId) {
+ ToProto(req->mutable_upstream_replica_id(), *options.UpstreamReplicaId);
+ }
+ if (options.SchemaModification) {
+ req->set_schema_modification(static_cast<NProto::ETableSchemaModification>(*options.SchemaModification));
+ }
+ if (options.ReplicationProgress) {
+ ToProto(req->mutable_replication_progress(), *options.ReplicationProgress);
+ }
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::AlterTableReplica(
+ TTableReplicaId replicaId,
+ const TAlterTableReplicaOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AlterTableReplica();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_replica_id(), replicaId);
+
+ if (options.Enabled) {
+ req->set_enabled(*options.Enabled);
+ }
+ if (options.Mode) {
+ req->set_mode(static_cast<NProto::ETableReplicaMode>(*options.Mode));
+ }
+ if (options.PreserveTimestamps) {
+ req->set_preserve_timestamps(*options.PreserveTimestamps);
+ }
+ if (options.Atomicity) {
+ req->set_atomicity(static_cast<NProto::EAtomicity>(*options.Atomicity));
+ }
+ if (options.EnableReplicatedTableTracker) {
+ req->set_enable_replicated_table_tracker(*options.EnableReplicatedTableTracker);
+ }
+
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TYsonString> TClient::GetTablePivotKeys(
+ const TYPath& path,
+ const TGetTablePivotKeysOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetTablePivotKeys();
+ req->set_represent_key_as_list(options.RepresentKeyAsList);
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetTablePivotKeysPtr& rsp) {
+ return TYsonString(rsp->value());
+ }));
+}
+
+TFuture<void> TClient::CreateTableBackup(
+ const TBackupManifestPtr& /*manifest*/,
+ const TCreateTableBackupOptions& /*options*/)
+{
+ ThrowUnimplemented("CreateTableBackup");
+}
+
+TFuture<void> TClient::RestoreTableBackup(
+ const TBackupManifestPtr& /*manifest*/,
+ const TRestoreTableBackupOptions& /*options*/)
+{
+ ThrowUnimplemented("RestoreTableBackup");
+}
+
+TFuture<std::vector<TTableReplicaId>> TClient::GetInSyncReplicas(
+ const TYPath& path,
+ const TNameTablePtr& nameTable,
+ const TSharedRange<TLegacyKey>& keys,
+ const TGetInSyncReplicasOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetInSyncReplicas();
+ SetTimeoutOptions(*req, options);
+
+ if (options.Timestamp) {
+ req->set_timestamp(options.Timestamp);
+ }
+
+ if (options.CachedSyncReplicasTimeout) {
+ req->set_cached_sync_replicas_timeout(NYT::ToProto<i64>(*options.CachedSyncReplicasTimeout));
+ }
+
+ req->set_path(path);
+ req->Attachments() = SerializeRowset(nameTable, keys, req->mutable_rowset_descriptor());
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspGetInSyncReplicasPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ return FromProto<std::vector<TTableReplicaId>>(rsp->replica_ids());
+ }));
+}
+
+TFuture<std::vector<TTableReplicaId>> TClient::GetInSyncReplicas(
+ const TYPath& path,
+ const TGetInSyncReplicasOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetInSyncReplicas();
+ SetTimeoutOptions(*req, options);
+
+ if (options.Timestamp) {
+ req->set_timestamp(options.Timestamp);
+ }
+
+ if (options.CachedSyncReplicasTimeout) {
+ req->set_cached_sync_replicas_timeout(NYT::ToProto<i64>(*options.CachedSyncReplicasTimeout));
+ }
+
+ req->set_path(path);
+ req->RequireServerFeature(ERpcProxyFeature::GetInSyncWithoutKeys);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspGetInSyncReplicasPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ return FromProto<std::vector<TTableReplicaId>>(rsp->replica_ids());
+ }));
+}
+
+TFuture<std::vector<TTabletInfo>> TClient::GetTabletInfos(
+ const TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetTabletInfos();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ ToProto(req->mutable_tablet_indexes(), tabletIndexes);
+ req->set_request_errors(options.RequestErrors);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspGetTabletInfosPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ std::vector<TTabletInfo> tabletInfos;
+ tabletInfos.reserve(rsp->tablets_size());
+ for (const auto& protoTabletInfo : rsp->tablets()) {
+ auto& tabletInfo = tabletInfos.emplace_back();
+ tabletInfo.TotalRowCount = protoTabletInfo.total_row_count();
+ tabletInfo.TrimmedRowCount = protoTabletInfo.trimmed_row_count();
+ tabletInfo.DelayedLocklessRowCount = protoTabletInfo.delayed_lockless_row_count();
+ tabletInfo.BarrierTimestamp = protoTabletInfo.barrier_timestamp();
+ tabletInfo.LastWriteTimestamp = protoTabletInfo.last_write_timestamp();
+ tabletInfo.TableReplicaInfos = protoTabletInfo.replicas().empty()
+ ? std::nullopt
+ : std::make_optional(std::vector<TTabletInfo::TTableReplicaInfo>());
+ FromProto(&tabletInfo.TabletErrors, protoTabletInfo.tablet_errors());
+
+ for (const auto& protoReplicaInfo : protoTabletInfo.replicas()) {
+ auto& currentReplica = tabletInfo.TableReplicaInfos->emplace_back();
+ currentReplica.ReplicaId = FromProto<TGuid>(protoReplicaInfo.replica_id());
+ currentReplica.LastReplicationTimestamp = protoReplicaInfo.last_replication_timestamp();
+ currentReplica.Mode = CheckedEnumCast<ETableReplicaMode>(protoReplicaInfo.mode());
+ currentReplica.CurrentReplicationRowIndex = protoReplicaInfo.current_replication_row_index();
+ currentReplica.CommittedReplicationRowIndex = protoReplicaInfo.committed_replication_row_index();
+ currentReplica.ReplicationError = FromProto<TError>(protoReplicaInfo.replication_error());
+ }
+ }
+ return tabletInfos;
+ }));
+}
+
+TFuture<TGetTabletErrorsResult> TClient::GetTabletErrors(
+ const TYPath& path,
+ const TGetTabletErrorsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetTabletErrors();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ if (options.Limit) {
+ req->set_limit(*options.Limit);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspGetTabletErrorsPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ TGetTabletErrorsResult tabletErrors;
+ if (rsp->has_incomplete() && rsp->incomplete()) {
+ tabletErrors.Incomplete = rsp->incomplete();
+ }
+
+ for (i64 index = 0; index != rsp->tablet_ids_size(); ++index) {
+ std::vector<TError> errors;
+ for (const auto& protoError : rsp->tablet_errors(index).errors()) {
+ errors.push_back(FromProto<TError>(protoError));
+ }
+ tabletErrors.TabletErrors[FromProto<TTabletId>(rsp->tablet_ids(index))] = std::move(errors);
+ }
+
+ for (i64 index = 0; index != rsp->replica_ids_size(); ++index) {
+ std::vector<TError> errors;
+ for (const auto& protoError : rsp->replication_errors(index).errors()) {
+ errors.push_back(FromProto<TError>(protoError));
+ }
+ tabletErrors.ReplicationErrors[FromProto<TTableReplicaId>(rsp->replica_ids(index))] = std::move(errors);
+ }
+ return tabletErrors;
+ }));
+}
+
+TFuture<std::vector<TTabletActionId>> TClient::BalanceTabletCells(
+ const TString& tabletCellBundle,
+ const std::vector<TYPath>& movableTables,
+ const TBalanceTabletCellsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.BalanceTabletCells();
+ SetTimeoutOptions(*req, options);
+
+ req->set_bundle(tabletCellBundle);
+ req->set_keep_actions(options.KeepActions);
+ ToProto(req->mutable_movable_tables(), movableTables);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspBalanceTabletCellsPtr>& rspOrError) {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ return FromProto<std::vector<TTabletActionId>>(rsp->tablet_actions());
+ }));
+}
+
+TFuture<NChaosClient::TReplicationCardPtr> TClient::GetReplicationCard(
+ NChaosClient::TReplicationCardId /*replicationCardId*/,
+ const TGetReplicationCardOptions& /*options*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TFuture<void> TClient::UpdateChaosTableReplicaProgress(
+ NChaosClient::TReplicaId /*replicaId*/,
+ const TUpdateChaosTableReplicaProgressOptions& /*options*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TFuture<void> TClient::AlterReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AlterReplicationCard();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_replication_card_id(), replicationCardId);
+
+ if (options.ReplicatedTableOptions) {
+ req->set_replicated_table_options(ConvertToYsonString(options.ReplicatedTableOptions).ToString());
+ }
+ if (options.EnableReplicatedTableTracker) {
+ req->set_enable_replicated_table_tracker(*options.EnableReplicatedTableTracker);
+ }
+ if (options.ReplicationCardCollocationId) {
+ ToProto(req->mutable_replication_card_collocation_id(), *options.ReplicationCardCollocationId);
+ }
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<IQueueRowsetPtr> TClient::PullQueue(
+ const TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PullQueue();
+ req->SetResponseHeavy(true);
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_queue_path(), queuePath);
+ req->set_offset(offset);
+ req->set_partition_index(partitionIndex);
+ ToProto(req->mutable_row_batch_read_options(), rowBatchReadOptions);
+ req->set_use_native_tablet_node_api(options.UseNativeTabletNodeApi);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPullQueuePtr& rsp) -> IQueueRowsetPtr {
+ auto rowset = DeserializeRowset<TUnversionedRow>(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()));
+ return CreateQueueRowset(rowset, rsp->start_offset());
+ }));
+}
+
+TFuture<IQueueRowsetPtr> TClient::PullConsumer(
+ const TRichYPath& consumerPath,
+ const TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PullConsumer();
+ req->SetResponseHeavy(true);
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_consumer_path(), consumerPath);
+ ToProto(req->mutable_queue_path(), queuePath);
+ req->set_offset(offset);
+ req->set_partition_index(partitionIndex);
+ ToProto(req->mutable_row_batch_read_options(), rowBatchReadOptions);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPullConsumerPtr& rsp) -> IQueueRowsetPtr {
+ auto rowset = DeserializeRowset<TUnversionedRow>(
+ rsp->rowset_descriptor(),
+ MergeRefsToRef<TRpcProxyClientBufferTag>(rsp->Attachments()));
+ return CreateQueueRowset(rowset, rsp->start_offset());
+ }));
+}
+
+TFuture<void> TClient::RegisterQueueConsumer(
+ const TRichYPath& queuePath,
+ const TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.RegisterQueueConsumer();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_queue_path(), queuePath);
+ ToProto(req->mutable_consumer_path(), consumerPath);
+ req->set_vital(vital);
+ if (options.Partitions) {
+ ToProto(req->mutable_partitions()->mutable_items(), *options.Partitions);
+ }
+
+ return req->Invoke().AsVoid();
+}
+
+TFuture<void> TClient::UnregisterQueueConsumer(
+ const TRichYPath& queuePath,
+ const TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.UnregisterQueueConsumer();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_queue_path(), queuePath);
+ ToProto(req->mutable_consumer_path(), consumerPath);
+
+ return req->Invoke().AsVoid();
+}
+
+TFuture<std::vector<TListQueueConsumerRegistrationsResult>> TClient::ListQueueConsumerRegistrations(
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ListQueueConsumerRegistrations();
+ SetTimeoutOptions(*req, options);
+
+ if (queuePath) {
+ ToProto(req->mutable_queue_path(), *queuePath);
+ }
+ if (consumerPath) {
+ ToProto(req->mutable_consumer_path(), *consumerPath);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspListQueueConsumerRegistrationsPtr& rsp) {
+ std::vector<TListQueueConsumerRegistrationsResult> result;
+ for (const auto& registration : rsp->registrations()) {
+ std::optional<std::vector<int>> partitions;
+ if (registration.has_partitions()) {
+ partitions = FromProto<std::vector<int>>(registration.partitions().items());
+ }
+ result.push_back({
+ .QueuePath = FromProto<TRichYPath>(registration.queue_path()),
+ .ConsumerPath = FromProto<TRichYPath>(registration.consumer_path()),
+ .Vital = registration.vital(),
+ .Partitions = std::move(partitions),
+ });
+ }
+ return result;
+ }));
+}
+
+TFuture<void> TClient::AddMember(
+ const TString& group,
+ const TString& member,
+ const TAddMemberOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AddMember();
+ SetTimeoutOptions(*req, options);
+
+ req->set_group(group);
+ req->set_member(member);
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::RemoveMember(
+ const TString& group,
+ const TString& member,
+ const TRemoveMemberOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.RemoveMember();
+ SetTimeoutOptions(*req, options);
+
+ req->set_group(group);
+ req->set_member(member);
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TCheckPermissionResponse> TClient::CheckPermission(
+ const TString& user,
+ const TYPath& path,
+ EPermission permission,
+ const TCheckPermissionOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CheckPermission();
+ SetTimeoutOptions(*req, options);
+
+ req->set_user(user);
+ req->set_path(path);
+ req->set_permission(static_cast<int>(permission));
+ if (options.Columns) {
+ auto* protoColumns = req->mutable_columns();
+ ToProto(protoColumns->mutable_items(), *options.Columns);
+ }
+ if (options.Vital) {
+ req->set_vital(*options.Vital);
+ }
+
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_transactional_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspCheckPermissionPtr& rsp) {
+ TCheckPermissionResponse response;
+ static_cast<TCheckPermissionResult&>(response) = FromProto<TCheckPermissionResult>(rsp->result());
+ if (rsp->has_columns()) {
+ response.Columns = FromProto<std::vector<TCheckPermissionResult>>(rsp->columns().items());
+ }
+ return response;
+ }));
+}
+
+TFuture<TCheckPermissionByAclResult> TClient::CheckPermissionByAcl(
+ const std::optional<TString>& user,
+ EPermission permission,
+ INodePtr acl,
+ const TCheckPermissionByAclOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CheckPermissionByAcl();
+ SetTimeoutOptions(*req, options);
+
+ if (user) {
+ req->set_user(*user);
+ }
+ req->set_permission(static_cast<int>(permission));
+ req->set_acl(ConvertToYsonString(acl).ToString());
+ req->set_ignore_missing_subjects(options.IgnoreMissingSubjects);
+
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspCheckPermissionByAclPtr& rsp) {
+ return FromProto<TCheckPermissionByAclResult>(rsp->result());
+ }));
+}
+
+TFuture<void> TClient::TransferAccountResources(
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.TransferAccountResources();
+ SetTimeoutOptions(*req, options);
+
+ req->set_src_account(srcAccount);
+ req->set_dst_account(dstAccount);
+ req->set_resource_delta(ConvertToYsonString(resourceDelta).ToString());
+
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::TransferPoolResources(
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.TransferPoolResources();
+ SetTimeoutOptions(*req, options);
+
+ req->set_src_pool(srcPool);
+ req->set_dst_pool(dstPool);
+ req->set_pool_tree(poolTree);
+ req->set_resource_delta(ConvertToYsonString(resourceDelta).ToString());
+
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<NScheduler::TOperationId> TClient::StartOperation(
+ NScheduler::EOperationType type,
+ const TYsonString& spec,
+ const TStartOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.StartOperation();
+ SetTimeoutOptions(*req, options);
+
+ req->set_type(NProto::ConvertOperationTypeToProto(type));
+ req->set_spec(spec.ToString());
+
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspStartOperationPtr& rsp) {
+ return FromProto<TOperationId>(rsp->operation_id());
+ }));
+}
+
+TFuture<void> TClient::AbortOperation(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TAbortOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AbortOperation();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ if (options.AbortMessage) {
+ req->set_abort_message(*options.AbortMessage);
+ }
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::SuspendOperation(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TSuspendOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.SuspendOperation();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+ req->set_abort_running_jobs(options.AbortRunningJobs);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::ResumeOperation(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TResumeOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ResumeOperation();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::CompleteOperation(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TCompleteOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CompleteOperation();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::UpdateOperationParameters(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TYsonString& parameters,
+ const TUpdateOperationParametersOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.UpdateOperationParameters();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ req->set_parameters(parameters.ToString());
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TOperation> TClient::GetOperation(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TGetOperationOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetOperation();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ ToProto(req->mutable_master_read_options(), options);
+ // COMPAT(max42): after 22.3 is everywhere, drop legacy field.
+ if (options.Attributes) {
+ ToProto(req->mutable_legacy_attributes(), *options.Attributes);
+ ToProto(req->mutable_attributes()->mutable_keys(), *options.Attributes);
+ }
+
+ req->set_include_runtime(options.IncludeRuntime);
+ req->set_maximum_cypress_progress_age(ToProto<i64>(options.MaximumCypressProgressAge));
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetOperationPtr& rsp) {
+ auto attributes = ConvertToAttributes(TYsonStringBuf(rsp->meta()));
+ TOperation operation;
+ Deserialize(operation, std::move(attributes), /*clone*/ false);
+ return operation;
+ }));
+}
+
+TFuture<void> TClient::DumpJobContext(
+ NJobTrackerClient::TJobId jobId,
+ const TYPath& path,
+ const TDumpJobContextOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.DumpJobContext();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+ req->set_path(path);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> TClient::GetJobInput(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+ auto req = proxy.GetJobInput();
+ if (options.Timeout) {
+ SetTimeoutOptions(*req, options);
+ } else {
+ InitStreamingRequest(*req);
+ }
+
+ ToProto(req->mutable_job_id(), jobId);
+ req->set_job_spec_source(static_cast<NProto::EJobSpecSource>(options.JobSpecSource));
+
+ return CreateRpcClientInputStream(std::move(req));
+}
+
+TFuture<TYsonString> TClient::GetJobInputPaths(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputPathsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetJobInputPaths();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+ req->set_job_spec_source(static_cast<NProto::EJobSpecSource>(options.JobSpecSource));
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetJobInputPathsPtr& rsp) {
+ return TYsonString(rsp->paths());
+ }));
+}
+
+TFuture<TYsonString> TClient::GetJobSpec(
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobSpecOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetJobSpec();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+ req->set_omit_node_directory(options.OmitNodeDirectory);
+ req->set_omit_input_table_specs(options.OmitInputTableSpecs);
+ req->set_omit_output_table_specs(options.OmitOutputTableSpecs);
+ req->set_job_spec_source(static_cast<NProto::EJobSpecSource>(options.JobSpecSource));
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetJobSpecPtr& rsp) {
+ return TYsonString(rsp->job_spec());
+ }));
+}
+
+TFuture<TSharedRef> TClient::GetJobStderr(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobStderrOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetJobStderr();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+ ToProto(req->mutable_job_id(), jobId);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetJobStderrPtr& rsp) {
+ YT_VERIFY(rsp->Attachments().size() == 1);
+ return rsp->Attachments().front();
+ }));
+}
+
+TFuture<TSharedRef> TClient::GetJobFailContext(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobFailContextOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetJobFailContext();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+ ToProto(req->mutable_job_id(), jobId);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetJobFailContextPtr& rsp) {
+ YT_VERIFY(rsp->Attachments().size() == 1);
+ return rsp->Attachments().front();
+ }));
+}
+
+TFuture<TListOperationsResult> TClient::ListOperations(
+ const TListOperationsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ListOperations();
+ SetTimeoutOptions(*req, options);
+
+ if (options.FromTime) {
+ req->set_from_time(NYT::ToProto<i64>(*options.FromTime));
+ }
+ if (options.ToTime) {
+ req->set_to_time(NYT::ToProto<i64>(*options.ToTime));
+ }
+ if (options.CursorTime) {
+ req->set_cursor_time(NYT::ToProto<i64>(*options.CursorTime));
+ }
+ req->set_cursor_direction(static_cast<NProto::EOperationSortDirection>(options.CursorDirection));
+ if (options.UserFilter) {
+ req->set_user_filter(*options.UserFilter);
+ }
+
+ if (options.AccessFilter) {
+ req->set_access_filter(ConvertToYsonString(options.AccessFilter).ToString());
+ }
+
+ if (options.StateFilter) {
+ req->set_state_filter(NProto::ConvertOperationStateToProto(*options.StateFilter));
+ }
+ if (options.TypeFilter) {
+ req->set_type_filter(NProto::ConvertOperationTypeToProto(*options.TypeFilter));
+ }
+ if (options.SubstrFilter) {
+ req->set_substr_filter(*options.SubstrFilter);
+ }
+ if (options.Pool) {
+ req->set_pool(*options.Pool);
+ }
+ if (options.PoolTree) {
+ req->set_pool_tree(*options.PoolTree);
+ }
+ if (options.WithFailedJobs) {
+ req->set_with_failed_jobs(*options.WithFailedJobs);
+ }
+ if (options.ArchiveFetchingTimeout) {
+ req->set_archive_fetching_timeout(NYT::ToProto<i64>(options.ArchiveFetchingTimeout));
+ }
+ req->set_include_archive(options.IncludeArchive);
+ req->set_include_counters(options.IncludeCounters);
+ req->set_limit(options.Limit);
+
+ // COMPAT(max42): after 22.3 is everywhere, drop legacy field.
+ if (options.Attributes) {
+ ToProto(req->mutable_legacy_attributes()->mutable_keys(), *options.Attributes);
+ ToProto(req->mutable_attributes()->mutable_keys(), *options.Attributes);
+ } else {
+ req->mutable_legacy_attributes()->set_all(true);
+ }
+
+ req->set_enable_ui_mode(options.EnableUIMode);
+
+ ToProto(req->mutable_master_read_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspListOperationsPtr& rsp) {
+ return FromProto<TListOperationsResult>(rsp->result());
+ }));
+}
+
+TFuture<TListJobsResult> TClient::ListJobs(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ const TListJobsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ListJobs();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+
+ if (options.Type) {
+ req->set_type(NProto::ConvertJobTypeToProto(*options.Type));
+ }
+ if (options.State) {
+ req->set_state(NProto::ConvertJobStateToProto(*options.State));
+ }
+ if (options.Address) {
+ req->set_address(*options.Address);
+ }
+ if (options.WithStderr) {
+ req->set_with_stderr(*options.WithStderr);
+ }
+ if (options.WithFailContext) {
+ req->set_with_fail_context(*options.WithFailContext);
+ }
+ if (options.WithSpec) {
+ req->set_with_spec(*options.WithSpec);
+ }
+ if (options.JobCompetitionId) {
+ ToProto(req->mutable_job_competition_id(), options.JobCompetitionId);
+ }
+ if (options.WithCompetitors) {
+ req->set_with_competitors(*options.WithCompetitors);
+ }
+ if (options.TaskName) {
+ req->set_task_name(*options.TaskName);
+ }
+
+ req->set_sort_field(static_cast<NProto::EJobSortField>(options.SortField));
+ req->set_sort_order(static_cast<NProto::EJobSortDirection>(options.SortOrder));
+
+ req->set_limit(options.Limit);
+ req->set_offset(options.Offset);
+
+ req->set_include_cypress(options.IncludeCypress);
+ req->set_include_controller_agent(options.IncludeControllerAgent);
+ req->set_include_archive(options.IncludeArchive);
+
+ req->set_data_source(static_cast<NProto::EDataSource>(options.DataSource));
+ req->set_running_jobs_lookbehind_period(NYT::ToProto<i64>(options.RunningJobsLookbehindPeriod));
+
+ ToProto(req->mutable_master_read_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspListJobsPtr& rsp) {
+ return FromProto<TListJobsResult>(rsp->result());
+ }));
+}
+
+TFuture<TYsonString> TClient::GetJob(
+ const TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetJob();
+ SetTimeoutOptions(*req, options);
+
+ NScheduler::ToProto(req, operationIdOrAlias);
+ ToProto(req->mutable_job_id(), jobId);
+
+ // COMPAT(max42): after 22.3 is everywhere, drop legacy field.
+ if (options.Attributes) {
+ ToProto(req->mutable_legacy_attributes()->mutable_keys(), *options.Attributes);
+ ToProto(req->mutable_attributes()->mutable_keys(), *options.Attributes);
+ } else {
+ req->mutable_legacy_attributes()->set_all(true);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetJobPtr& rsp) {
+ return TYsonString(rsp->info());
+ }));
+}
+
+TFuture<void> TClient::AbandonJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbandonJobOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AbandonJob();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TPollJobShellResponse> TClient::PollJobShell(
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const TYsonString& parameters,
+ const TPollJobShellOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PollJobShell();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+ req->set_parameters(parameters.ToString());
+ if (shellName) {
+ req->set_shell_name(*shellName);
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPollJobShellPtr& rsp) {
+ return TPollJobShellResponse {
+ .Result = TYsonString(rsp->result()),
+ };
+ }));
+}
+
+TFuture<void> TClient::AbortJob(
+ NJobTrackerClient::TJobId jobId,
+ const TAbortJobOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AbortJob();
+ SetTimeoutOptions(*req, options);
+
+ ToProto(req->mutable_job_id(), jobId);
+ if (options.InterruptTimeout) {
+ req->set_interrupt_timeout(NYT::ToProto<i64>(*options.InterruptTimeout));
+ }
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TGetFileFromCacheResult> TClient::GetFileFromCache(
+ const TString& md5,
+ const TGetFileFromCacheOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetFileFromCache();
+ SetTimeoutOptions(*req, options);
+ ToProto(req->mutable_transactional_options(), options);
+
+ req->set_md5(md5);
+ req->set_cache_path(options.CachePath);
+
+ ToProto(req->mutable_master_read_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetFileFromCachePtr& rsp) {
+ return FromProto<TGetFileFromCacheResult>(rsp->result());
+ }));
+}
+
+TFuture<TPutFileToCacheResult> TClient::PutFileToCache(
+ const TYPath& path,
+ const TString& expectedMD5,
+ const TPutFileToCacheOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PutFileToCache();
+ SetTimeoutOptions(*req, options);
+ ToProto(req->mutable_transactional_options(), options);
+
+ req->set_path(path);
+ req->set_md5(expectedMD5);
+ req->set_cache_path(options.CachePath);
+ req->set_preserve_expiration_timeout(options.PreserveExpirationTimeout);
+
+ ToProto(req->mutable_prerequisite_options(), options);
+ ToProto(req->mutable_master_read_options(), options);
+ ToProto(req->mutable_mutating_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPutFileToCachePtr& rsp) {
+ return FromProto<TPutFileToCacheResult>(rsp->result());
+ }));
+}
+
+TFuture<TClusterMeta> TClient::GetClusterMeta(
+ const TGetClusterMetaOptions& /*options*/)
+{
+ ThrowUnimplemented("GetClusterMeta");
+}
+
+TFuture<void> TClient::CheckClusterLiveness(
+ const TCheckClusterLivenessOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.CheckClusterLiveness();
+ SetTimeoutOptions(*req, options);
+
+ req->set_check_cypress_root(options.CheckCypressRoot);
+ req->set_check_secondary_master_cells(options.CheckSecondaryMasterCells);
+ if (auto bundleName = options.CheckTabletCellBundle) {
+ req->set_check_tablet_cell_bundle(*bundleName);
+ }
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<TSkynetSharePartsLocationsPtr> TClient::LocateSkynetShare(
+ const TRichYPath&,
+ const TLocateSkynetShareOptions& /*options*/)
+{
+ ThrowUnimplemented("LocateSkynetShare");
+}
+
+TFuture<std::vector<TColumnarStatistics>> TClient::GetColumnarStatistics(
+ const std::vector<TRichYPath>& path,
+ const TGetColumnarStatisticsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GetColumnarStatistics();
+ SetTimeoutOptions(*req, options);
+
+ for (const auto& subPath: path) {
+ req->add_paths(ConvertToYsonString(subPath).ToString());
+ }
+
+ req->set_fetcher_mode(static_cast<NProto::EColumnarStatisticsFetcherMode>(options.FetcherMode));
+
+ ToProto(req->mutable_fetch_chunk_spec_config(), options.FetchChunkSpecConfig);
+
+ ToProto(req->mutable_fetcher_config(), options.FetcherConfig);
+
+ req->set_enable_early_finish(options.EnableEarlyFinish);
+
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGetColumnarStatisticsPtr& rsp) {
+ return NYT::FromProto<std::vector<TColumnarStatistics>>(rsp->statistics());
+ }));
+}
+
+TFuture<NApi::TMultiTablePartitions> TClient::PartitionTables(
+ const std::vector<TRichYPath>& paths,
+ const TPartitionTablesOptions& options)
+{
+ if (options.PartitionMode == NTableClient::ETablePartitionMode::Sorted) {
+ THROW_ERROR_EXCEPTION("Sorted partitioning is not supported yet");
+ }
+
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.PartitionTables();
+ SetTimeoutOptions(*req, options);
+
+ for (const auto& path : paths) {
+ req->add_paths(ToString(path));
+ }
+
+ ToProto(req->mutable_fetch_chunk_spec_config(), options.FetchChunkSpecConfig);
+
+ ToProto(req->mutable_fetcher_config(), options.FetcherConfig);
+
+ req->mutable_chunk_slice_fetcher_config()->set_max_slices_per_fetch(
+ ToProto<i64>(options.ChunkSliceFetcherConfig->MaxSlicesPerFetch));
+
+ req->set_partition_mode(static_cast<NProto::EPartitionTablesMode>(options.PartitionMode));
+
+ req->set_data_weight_per_partition(options.DataWeightPerPartition);
+
+ if (options.MaxPartitionCount) {
+ req->set_max_partition_count(*options.MaxPartitionCount);
+ }
+
+ req->set_adjust_data_weight_per_partition(options.AdjustDataWeightPerPartition);
+
+ req->set_enable_key_guarantee(options.EnableKeyGuarantee);
+
+ ToProto(req->mutable_transactional_options(), options);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspPartitionTablesPtr& rsp) {
+ return FromProto<TMultiTablePartitions>(*rsp);
+ }));
+}
+
+TFuture<void> TClient::TruncateJournal(
+ const TYPath& path,
+ i64 rowCount,
+ const TTruncateJournalOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.TruncateJournal();
+ SetTimeoutOptions(*req, options);
+
+ req->set_path(path);
+ req->set_row_count(rowCount);
+ ToProto(req->mutable_mutating_options(), options);
+ ToProto(req->mutable_prerequisite_options(), options);
+
+ return req->Invoke().As<void>();
+}
+
+
+TFuture<int> TClient::BuildSnapshot(const TBuildSnapshotOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.BuildSnapshot();
+ if (options.CellId) {
+ ToProto(req->mutable_cell_id(), options.CellId);
+ }
+ req->set_set_read_only(options.SetReadOnly);
+ req->set_wait_for_snapshot_completion(options.WaitForSnapshotCompletion);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspBuildSnapshotPtr>& rspOrError) -> int {
+ const auto& rsp = rspOrError.ValueOrThrow();
+ return rsp->snapshot_id();
+ }));
+}
+
+TFuture<TCellIdToSnapshotIdMap> TClient::BuildMasterSnapshots(const TBuildMasterSnapshotsOptions& /*options*/)
+{
+ ThrowUnimplemented("BuildMasterSnapshots");
+}
+
+TFuture<void> TClient::SwitchLeader(
+ NHydra::TCellId /*cellId*/,
+ const TString& /*newLeaderAddress*/,
+ const TSwitchLeaderOptions& /*options*/)
+{
+ ThrowUnimplemented("SwitchLeader");
+}
+
+TFuture<void> TClient::ResetStateHash(
+ NHydra::TCellId /*cellId*/,
+ const TResetStateHashOptions& /*options*/)
+{
+ ThrowUnimplemented("ResetStateHash");
+}
+
+TFuture<void> TClient::GCCollect(const TGCCollectOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.GCCollect();
+ if (options.CellId) {
+ ToProto(req->mutable_cell_id(), options.CellId);
+ }
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::KillProcess(
+ const TString& /*address*/,
+ const TKillProcessOptions& /*options*/)
+{
+ ThrowUnimplemented("KillProcess");
+}
+
+TFuture<TString> TClient::WriteCoreDump(
+ const TString& /*address*/,
+ const TWriteCoreDumpOptions& /*options*/)
+{
+ ThrowUnimplemented("WriteCoreDump");
+}
+
+TFuture<TGuid> TClient::WriteLogBarrier(
+ const TString& /*address*/,
+ const TWriteLogBarrierOptions& /*options*/)
+{
+ ThrowUnimplemented("WriteLogBarrier");
+}
+
+TFuture<TString> TClient::WriteOperationControllerCoreDump(
+ TOperationId /*operationId*/,
+ const TWriteOperationControllerCoreDumpOptions& /*options*/)
+{
+ ThrowUnimplemented("WriteOperationControllerCoreDump");
+}
+
+TFuture<void> TClient::HealExecNode(
+ const TString& /*address*/,
+ const THealExecNodeOptions& /*options*/)
+{
+ ThrowUnimplemented("HealExecNode");
+}
+
+TFuture<void> TClient::SuspendCoordinator(
+ TCellId coordinatorCellId,
+ const TSuspendCoordinatorOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.SuspendCoordinator();
+ ToProto(req->mutable_coordinator_cell_id(), coordinatorCellId);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::ResumeCoordinator(
+ TCellId coordinatorCellId,
+ const TResumeCoordinatorOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ResumeCoordinator();
+ ToProto(req->mutable_coordinator_cell_id(), coordinatorCellId);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::MigrateReplicationCards(
+ TCellId chaosCellId,
+ const TMigrateReplicationCardsOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.MigrateReplicationCards();
+ ToProto(req->mutable_chaos_cell_id(), chaosCellId);
+ ToProto(req->mutable_replication_card_ids(), options.ReplicationCardIds);
+ if (options.DestinationCellId) {
+ ToProto(req->mutable_destination_cell_id(), options.DestinationCellId);
+ }
+
+ return req->Invoke().As<void>();
+}
+
+namespace {
+
+NProto::EMaintenanceComponent ConvertMaintenanceComponentToProto(EMaintenanceComponent component)
+{
+ switch (component) {
+ case EMaintenanceComponent::ClusterNode:
+ return NProto::EMaintenanceComponent::MC_CLUSTER_NODE;
+ case EMaintenanceComponent::HttpProxy:
+ return NProto::EMaintenanceComponent::MC_HTTP_PROXY;
+ case EMaintenanceComponent::RpcProxy:
+ return NProto::EMaintenanceComponent::MC_RPC_PROXY;
+ case EMaintenanceComponent::Host:
+ return NProto::EMaintenanceComponent::MC_HOST;
+ default:
+ THROW_ERROR_EXCEPTION("Invalid maintenance component %Qv", component);
+ }
+}
+
+NProto::EMaintenanceType ConvertMaintenanceTypeToProto(EMaintenanceType type)
+{
+ switch (type) {
+ case EMaintenanceType::Ban:
+ return NProto::EMaintenanceType::MT_BAN;
+ case EMaintenanceType::Decommission:
+ return NProto::EMaintenanceType::MT_DECOMMISSION;
+ case EMaintenanceType::DisableSchedulerJobs:
+ return NProto::EMaintenanceType::MT_DISABLE_SCHEDULER_JOBS;
+ case EMaintenanceType::DisableWriteSessions:
+ return NProto::EMaintenanceType::MT_DISABLE_WRITE_SESSIONS;
+ case EMaintenanceType::DisableTabletCells:
+ return NProto::EMaintenanceType::MT_DISABLE_TABLET_CELLS;
+ case EMaintenanceType::PendingRestart:
+ return NProto::EMaintenanceType::MT_PENDING_RESTART;
+ default:
+ THROW_ERROR_EXCEPTION("Invalid maintenance type %Qv", type);
+ }
+}
+
+} // namespace
+
+TFuture<TMaintenanceId> TClient::AddMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options)
+{
+ ValidateMaintenanceComment(comment);
+
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.AddMaintenance();
+ req->set_component(ConvertMaintenanceComponentToProto(component));
+ req->set_address(address);
+ req->set_type(ConvertMaintenanceTypeToProto(type));
+ req->set_comment(comment);
+ SetTimeoutOptions(*req, options);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspAddMaintenancePtr>& rsp) {
+ return FromProto<TMaintenanceId>(rsp.ValueOrThrow()->id());
+ }));
+}
+
+TFuture<TMaintenanceCounts> TClient::RemoveMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.RemoveMaintenance();
+ req->set_component(ConvertMaintenanceComponentToProto(component));
+ req->set_address(address);
+
+ ToProto(req->mutable_ids(), filter.Ids);
+
+ if (filter.Type) {
+ req->set_type(ConvertMaintenanceTypeToProto(*filter.Type));
+ }
+
+ using TByUser = TMaintenanceFilter::TByUser;
+ Visit(filter.User,
+ [] (TByUser::TAll) {},
+ [&] (TByUser::TMine) {
+ req->set_mine(true);
+ },
+ [&] (const TString& user) {
+ req->set_user(user);
+ });
+
+ SetTimeoutOptions(*req, options);
+
+ return req->Invoke().Apply(BIND([] (const TErrorOr<TApiServiceProxy::TRspRemoveMaintenancePtr>& rsp) {
+ auto rspValue = rsp.ValueOrThrow();
+
+ const auto& protoCounts = rspValue->removed_maintenance_counts();
+ TMaintenanceCounts counts;
+
+ if (!rspValue->use_map_instead_of_fields()) {
+ counts[EMaintenanceType::Ban] = rspValue->ban();
+ counts[EMaintenanceType::Decommission] = rspValue->decommission();
+ counts[EMaintenanceType::DisableSchedulerJobs] = rspValue->disable_scheduler_jobs();
+ counts[EMaintenanceType::DisableWriteSessions] = rspValue->disable_write_sessions();
+ counts[EMaintenanceType::DisableTabletCells] = rspValue->disable_tablet_cells();
+ counts[EMaintenanceType::PendingRestart] = rspValue->pending_restart();
+ } else {
+ for (auto type : TEnumTraits<EMaintenanceType>::GetDomainValues()) {
+ auto it = protoCounts.find(ConvertMaintenanceTypeToProto(type));
+ counts[type] = it == protoCounts.end() ? 0 : it->second;
+ }
+ }
+
+ return counts;
+ }));
+}
+
+TFuture<void> TClient::SuspendChaosCells(
+ const std::vector<TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.SuspendChaosCells();
+ ToProto(req->mutable_cell_ids(), cellIds);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::ResumeChaosCells(
+ const std::vector<TCellId>& cellIds,
+ const TResumeChaosCellsOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ResumeChaosCells();
+ ToProto(req->mutable_cell_ids(), cellIds);
+
+ return req->Invoke().As<void>();
+}
+
+TFuture<void> TClient::SuspendTabletCells(
+ const std::vector<TCellId>& /*cellIds*/,
+ const TSuspendTabletCellsOptions& /*options*/)
+{
+ ThrowUnimplemented("SuspendTabletCells");
+}
+
+TFuture<void> TClient::ResumeTabletCells(
+ const std::vector<TCellId>& /*cellIds*/,
+ const TResumeTabletCellsOptions& /*options*/)
+{
+ ThrowUnimplemented("ResumeTabletCells");
+}
+
+TFuture<TDisableChunkLocationsResult> TClient::DisableChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.DisableChunkLocations();
+ ToProto(req->mutable_node_address(), nodeAddress);
+ ToProto(req->mutable_location_uuids(), locationUuids);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspDisableChunkLocationsPtr& rsp) {
+ auto locationUuids = FromProto<std::vector<TGuid>>(rsp->location_uuids());
+ return TDisableChunkLocationsResult{
+ .LocationUuids = locationUuids
+ };
+ }));
+}
+
+TFuture<TDestroyChunkLocationsResult> TClient::DestroyChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.DestroyChunkLocations();
+ ToProto(req->mutable_node_address(), nodeAddress);
+ ToProto(req->mutable_location_uuids(), locationUuids);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspDestroyChunkLocationsPtr& rsp) {
+ auto locationUuids = FromProto<std::vector<TGuid>>(rsp->location_uuids());
+ return TDestroyChunkLocationsResult{
+ .LocationUuids = locationUuids
+ };
+ }));
+}
+
+TFuture<TResurrectChunkLocationsResult> TClient::ResurrectChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& /*options*/)
+{
+ auto proxy = CreateApiServiceProxy();
+
+ auto req = proxy.ResurrectChunkLocations();
+ ToProto(req->mutable_node_address(), nodeAddress);
+ ToProto(req->mutable_location_uuids(), locationUuids);
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspResurrectChunkLocationsPtr& rsp) {
+ auto locationUuids = FromProto<std::vector<TGuid>>(rsp->location_uuids());
+ return TResurrectChunkLocationsResult{
+ .LocationUuids = locationUuids
+ };
+ }));
+}
+
+TFuture<NQueryTrackerClient::TQueryId> TClient::StartQuery(
+ NQueryTrackerClient::EQueryEngine /*engine*/,
+ const TString& /*query*/,
+ const TStartQueryOptions& /*options*/)
+{
+ ThrowUnimplemented("StartQuery");
+}
+
+TFuture<void> TClient::AbortQuery(
+ NQueryTrackerClient::TQueryId /*queryId*/,
+ const TAbortQueryOptions& /*options*/)
+{
+ ThrowUnimplemented("AbortQuery");
+}
+
+TFuture<TQueryResult> TClient::GetQueryResult(
+ NQueryTrackerClient::TQueryId /*queryId*/,
+ i64 /*resultIndex*/,
+ const TGetQueryResultOptions& /*options*/)
+{
+ ThrowUnimplemented("GetQueryResult");
+}
+
+TFuture<IUnversionedRowsetPtr> TClient::ReadQueryResult(
+ NQueryTrackerClient::TQueryId /*queryId*/,
+ i64 /*resultIndex*/,
+ const TReadQueryResultOptions& /*options*/)
+{
+ ThrowUnimplemented("AbortQuery");
+}
+
+TFuture<TQuery> TClient::GetQuery(
+ NQueryTrackerClient::TQueryId /*queryId*/,
+ const TGetQueryOptions& /*options*/)
+{
+ ThrowUnimplemented("GetQuery");
+}
+
+TFuture<TListQueriesResult> TClient::ListQueries(
+ const TListQueriesOptions& /*options*/)
+{
+ ThrowUnimplemented("ListQueries");
+}
+
+TFuture<void> TClient::AlterQuery(
+ NQueryTrackerClient::TQueryId /*queryId*/,
+ const TAlterQueryOptions& /*options*/)
+{
+ ThrowUnimplemented("AlterQuery");
+}
+
+TFuture<void> TClient::SetUserPassword(
+ const TString& /*user*/,
+ const TString& /*currentPasswordSha256*/,
+ const TString& /*newPasswordSha256*/,
+ const TSetUserPasswordOptions& /*options*/)
+{
+ ThrowUnimplemented("SetUserPassword");
+}
+
+TFuture<TIssueTokenResult> TClient::IssueToken(
+ const TString& /*user*/,
+ const TString& /*passwordSha256*/,
+ const TIssueTokenOptions& /*options*/)
+{
+ ThrowUnimplemented("IssueToken");
+}
+
+TFuture<void> TClient::RevokeToken(
+ const TString& /*user*/,
+ const TString& /*passwordSha256*/,
+ const TString& /*tokenSha256*/,
+ const TRevokeTokenOptions& /*options*/)
+{
+ ThrowUnimplemented("RevokeToken");
+}
+
+TFuture<TListUserTokensResult> TClient::ListUserTokens(
+ const TString& /*user*/,
+ const TString& /*passwordSha256*/,
+ const TListUserTokensOptions& /*options*/)
+{
+ ThrowUnimplemented("ListUserTokens");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/client_impl.h b/yt/yt/client/api/rpc_proxy/client_impl.h
new file mode 100644
index 0000000000..4393fabdb5
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/client_impl.h
@@ -0,0 +1,499 @@
+#pragma once
+
+#include "client_base.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClient
+ : public virtual NApi::IClient
+ , public TClientBase
+ , public NApi::TClusterAwareClientBase
+{
+public:
+ TClient(
+ TConnectionPtr connection,
+ const TClientOptions& options);
+
+ void Terminate() override;
+ const NTabletClient::ITableMountCachePtr& GetTableMountCache() override;
+ const NChaosClient::IReplicationCardCachePtr& GetReplicationCardCache() override;
+ const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() override;
+
+ // Transactions.
+ NApi::ITransactionPtr AttachTransaction(
+ NTransactionClient::TTransactionId transactionId,
+ const NApi::TTransactionAttachOptions& options) override;
+
+ // Tables.
+ TFuture<void> MountTable(
+ const NYPath::TYPath& path,
+ const NApi::TMountTableOptions& options) override;
+
+ TFuture<void> UnmountTable(
+ const NYPath::TYPath& path,
+ const NApi::TUnmountTableOptions& options) override;
+
+ TFuture<void> RemountTable(
+ const NYPath::TYPath& path,
+ const NApi::TRemountTableOptions& options) override;
+
+ TFuture<void> FreezeTable(
+ const NYPath::TYPath& path,
+ const NApi::TFreezeTableOptions& options) override;
+
+ TFuture<void> UnfreezeTable(
+ const NYPath::TYPath& path,
+ const NApi::TUnfreezeTableOptions& options) override;
+
+ TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ const std::vector<NTableClient::TLegacyOwningKey>& pivotKeys,
+ const NApi::TReshardTableOptions& options) override;
+
+ TFuture<void> ReshardTable(
+ const NYPath::TYPath& path,
+ int tabletCount,
+ const NApi::TReshardTableOptions& options) override;
+
+ TFuture<std::vector<NTabletClient::TTabletActionId>> ReshardTableAutomatic(
+ const NYPath::TYPath& path,
+ const NApi::TReshardTableAutomaticOptions& options) override;
+
+ TFuture<void> TrimTable(
+ const NYPath::TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const NApi::TTrimTableOptions& options) override;
+
+ TFuture<void> AlterTable(
+ const NYPath::TYPath& path,
+ const NApi::TAlterTableOptions& options) override;
+
+ TFuture<void> AlterTableReplica(
+ NTabletClient::TTableReplicaId replicaId,
+ const NApi::TAlterTableReplicaOptions& options) override;
+
+ TFuture<NYson::TYsonString> GetTablePivotKeys(
+ const NYPath::TYPath& path,
+ const TGetTablePivotKeysOptions& options) override;
+
+ TFuture<void> CreateTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TCreateTableBackupOptions& options) override;
+
+ TFuture<void> RestoreTableBackup(
+ const TBackupManifestPtr& manifest,
+ const TRestoreTableBackupOptions& options) override;
+
+ TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const NTableClient::TNameTablePtr& nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const NApi::TGetInSyncReplicasOptions& options) override;
+
+ TFuture<std::vector<NTabletClient::TTableReplicaId>> GetInSyncReplicas(
+ const NYPath::TYPath& path,
+ const NApi::TGetInSyncReplicasOptions& options) override;
+
+ TFuture<std::vector<NApi::TTabletInfo>> GetTabletInfos(
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const NApi::TGetTabletInfosOptions& options) override;
+
+ TFuture<TGetTabletErrorsResult> GetTabletErrors(
+ const NYPath::TYPath& path,
+ const NApi::TGetTabletErrorsOptions& options) override;
+
+ TFuture<std::vector<NTabletClient::TTabletActionId>> BalanceTabletCells(
+ const TString& tabletCellBundle,
+ const std::vector<NYPath::TYPath>& movableTables,
+ const NApi::TBalanceTabletCellsOptions& options) override;
+
+ TFuture<NChaosClient::TReplicationCardPtr> GetReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TGetReplicationCardOptions& options = {}) override;
+
+ TFuture<void> UpdateChaosTableReplicaProgress(
+ NChaosClient::TReplicaId replicaId,
+ const TUpdateChaosTableReplicaProgressOptions& options = {}) override;
+
+ TFuture<void> AlterReplicationCard(
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options = {}) override;
+
+ // Queues.
+ TFuture<NQueueClient::IQueueRowsetPtr> PullQueue(
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options = {}) override;
+
+ TFuture<NQueueClient::IQueueRowsetPtr> PullConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options = {}) override;
+
+ TFuture<void> RegisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options = {}) override;
+
+ TFuture<void> UnregisterQueueConsumer(
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options = {}) override;
+
+ TFuture<std::vector<TListQueueConsumerRegistrationsResult>> ListQueueConsumerRegistrations(
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options = {}) override;
+
+ // Files.
+ TFuture<NApi::TGetFileFromCacheResult> GetFileFromCache(
+ const TString& md5,
+ const NApi::TGetFileFromCacheOptions& options) override;
+
+ TFuture<NApi::TPutFileToCacheResult> PutFileToCache(
+ const NYPath::TYPath& path,
+ const TString& expectedMD5,
+ const NApi::TPutFileToCacheOptions& options) override;
+
+ // Security.
+ TFuture<void> AddMember(
+ const TString& group,
+ const TString& member,
+ const NApi::TAddMemberOptions& options) override;
+
+ TFuture<void> RemoveMember(
+ const TString& group,
+ const TString& member,
+ const NApi::TRemoveMemberOptions& options) override;
+
+ TFuture<TCheckPermissionResponse> CheckPermission(
+ const TString& user,
+ const NYPath::TYPath& path,
+ NYTree::EPermission permission,
+ const NApi::TCheckPermissionOptions& options) override;
+
+ TFuture<TCheckPermissionByAclResult> CheckPermissionByAcl(
+ const std::optional<TString>& user,
+ NYTree::EPermission permission,
+ NYTree::INodePtr acl,
+ const NApi::TCheckPermissionByAclOptions& options) override;
+
+ TFuture<void> TransferAccountResources(
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options) override;
+
+ // Scheduler pools.
+ virtual TFuture<void> TransferPoolResources(
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options) override;
+
+ // Scheduler.
+ TFuture<NScheduler::TOperationId> StartOperation(
+ NScheduler::EOperationType type,
+ const NYson::TYsonString& spec,
+ const NApi::TStartOperationOptions& options) override;
+
+ TFuture<void> AbortOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TAbortOperationOptions& options) override;
+
+ TFuture<void> SuspendOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TSuspendOperationOptions& options) override;
+
+ TFuture<void> ResumeOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TResumeOperationOptions& options) override;
+
+ TFuture<void> CompleteOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TCompleteOperationOptions& options) override;
+
+ TFuture<void> UpdateOperationParameters(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NYson::TYsonString& parameters,
+ const NApi::TUpdateOperationParametersOptions& options) override;
+
+ TFuture<TOperation> GetOperation(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TGetOperationOptions& options) override;
+
+ TFuture<void> DumpJobContext(
+ NJobTrackerClient::TJobId jobId,
+ const NYPath::TYPath& path,
+ const NApi::TDumpJobContextOptions& options) override;
+
+ TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> GetJobInput(
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobInputOptions& options) override;
+
+ TFuture<NYson::TYsonString> GetJobInputPaths(
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobInputPathsOptions& options) override;
+
+ TFuture<NYson::TYsonString> GetJobSpec(
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobSpecOptions& options) override;
+
+ TFuture<TSharedRef> GetJobStderr(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobStderrOptions& options) override;
+
+ TFuture<TSharedRef> GetJobFailContext(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobFailContextOptions& options) override;
+
+ TFuture<NApi::TListOperationsResult> ListOperations(
+ const NApi::TListOperationsOptions& options) override;
+
+ TFuture<NApi::TListJobsResult> ListJobs(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NApi::TListJobsOptions&) override;
+
+ TFuture<NYson::TYsonString> GetJob(
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TGetJobOptions& options) override;
+
+ TFuture<void> AbandonJob(
+ NJobTrackerClient::TJobId job_id,
+ const NApi::TAbandonJobOptions& options) override;
+
+ TFuture<TPollJobShellResponse> PollJobShell(
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const NYson::TYsonString& parameters,
+ const NApi::TPollJobShellOptions& options) override;
+
+ TFuture<void> AbortJob(
+ NJobTrackerClient::TJobId jobId,
+ const NApi::TAbortJobOptions& options) override;
+
+ // Metadata.
+ TFuture<NApi::TClusterMeta> GetClusterMeta(
+ const NApi::TGetClusterMetaOptions&) override;
+
+ TFuture<void> CheckClusterLiveness(
+ const TCheckClusterLivenessOptions&) override;
+
+ TFuture<NApi::TSkynetSharePartsLocationsPtr> LocateSkynetShare(
+ const NYPath::TRichYPath&,
+ const NApi::TLocateSkynetShareOptions&) override;
+
+ TFuture<std::vector<NTableClient::TColumnarStatistics>> GetColumnarStatistics(
+ const std::vector<NYPath::TRichYPath>& path,
+ const NApi::TGetColumnarStatisticsOptions& options) override;
+
+ TFuture<TMultiTablePartitions> PartitionTables(
+ const std::vector<NYPath::TRichYPath>& paths,
+ const NApi::TPartitionTablesOptions& options) override;
+
+ TFuture<void> TruncateJournal(
+ const NYPath::TYPath& path,
+ i64 rowCount,
+ const NApi::TTruncateJournalOptions& options) override;
+
+ // Administration.
+ TFuture<int> BuildSnapshot(
+ const NApi::TBuildSnapshotOptions& options) override;
+
+ TFuture<TCellIdToSnapshotIdMap> BuildMasterSnapshots(
+ const TBuildMasterSnapshotsOptions& options) override;
+
+ TFuture<void> SwitchLeader(
+ NHydra::TCellId cellId,
+ const TString& newLeaderAddress,
+ const TSwitchLeaderOptions& options) override;
+
+ TFuture<void> ResetStateHash(
+ NHydra::TCellId cellId,
+ const TResetStateHashOptions& options) override;
+
+ TFuture<void> GCCollect(
+ const NApi::TGCCollectOptions& options) override;
+
+ TFuture<void> KillProcess(
+ const TString& address,
+ const NApi::TKillProcessOptions& options) override;
+
+ TFuture<TString> WriteCoreDump(
+ const TString& address,
+ const NApi::TWriteCoreDumpOptions& options) override;
+
+ TFuture<TGuid> WriteLogBarrier(
+ const TString& address,
+ const TWriteLogBarrierOptions& options) override;
+
+ TFuture<TString> WriteOperationControllerCoreDump(
+ NJobTrackerClient::TOperationId operationId,
+ const NApi::TWriteOperationControllerCoreDumpOptions& options) override;
+
+ TFuture<void> HealExecNode(
+ const TString& address,
+ const THealExecNodeOptions& options) override;
+
+ TFuture<void> SuspendCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TSuspendCoordinatorOptions& options) override;
+
+ TFuture<void> ResumeCoordinator(
+ NObjectClient::TCellId coordinatorCellId,
+ const TResumeCoordinatorOptions& options) override;
+
+ TFuture<void> MigrateReplicationCards(
+ NObjectClient::TCellId chaosCellId,
+ const TMigrateReplicationCardsOptions& options) override;
+
+ TFuture<void> SuspendChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& options) override;
+
+ TFuture<void> ResumeChaosCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeChaosCellsOptions& options) override;
+
+ TFuture<void> SuspendTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendTabletCellsOptions& options) override;
+
+ TFuture<void> ResumeTabletCells(
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeTabletCellsOptions& options) override;
+
+ TFuture<TMaintenanceId> AddMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options) override;
+
+ TFuture<TMaintenanceCounts> RemoveMaintenance(
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options) override;
+
+ TFuture<TDisableChunkLocationsResult> DisableChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& options) override;
+
+ TFuture<TDestroyChunkLocationsResult> DestroyChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& options) override;
+
+ TFuture<TResurrectChunkLocationsResult> ResurrectChunkLocations(
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& options) override;
+
+ // Query tracker
+
+ TFuture<NQueryTrackerClient::TQueryId> StartQuery(
+ NQueryTrackerClient::EQueryEngine engine,
+ const TString& query,
+ const TStartQueryOptions& options) override;
+
+ TFuture<void> AbortQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAbortQueryOptions& options) override;
+
+ TFuture<TQueryResult> GetQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex = 0,
+ const TGetQueryResultOptions& options = {}) override;
+
+ TFuture<IUnversionedRowsetPtr> ReadQueryResult(
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex = 0,
+ const TReadQueryResultOptions& options = {}) override;
+
+ TFuture<TQuery> GetQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TGetQueryOptions& options = {}) override;
+
+ TFuture<TListQueriesResult> ListQueries(
+ const TListQueriesOptions& options = {}) override;
+
+ TFuture<void> AlterQuery(
+ NQueryTrackerClient::TQueryId queryId,
+ const TAlterQueryOptions& options = {}) override;
+
+ // Authentication
+
+ virtual TFuture<void> SetUserPassword(
+ const TString& user,
+ const TString& currentPasswordSha256,
+ const TString& newPasswordSha256,
+ const TSetUserPasswordOptions& options) override;
+
+ TFuture<TIssueTokenResult> IssueToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TIssueTokenOptions& options) override;
+
+ TFuture<void> RevokeToken(
+ const TString& user,
+ const TString& passwordSha256,
+ const TString& tokenSha256,
+ const TRevokeTokenOptions& options) override;
+
+ TFuture<TListUserTokensResult> ListUserTokens(
+ const TString& user,
+ const TString& passwordSha256,
+ const TListUserTokensOptions& options) override;
+
+private:
+ const TConnectionPtr Connection_;
+ const NRpc::TDynamicChannelPoolPtr ChannelPool_;
+ const NRpc::IChannelPtr RetryingChannel_;
+ const TClientOptions ClientOptions_;
+
+ TLazyIntrusivePtr<NTabletClient::ITableMountCache> TableMountCache_;
+
+ TLazyIntrusivePtr<NTransactionClient::ITimestampProvider> TimestampProvider_;
+
+ NTransactionClient::ITimestampProviderPtr CreateTimestampProvider() const;
+
+ NRpc::IChannelPtr MaybeCreateRetryingChannel(NRpc::IChannelPtr channel, bool retryProxyBanned) const;
+ // Returns an RPC channel to use for API calls to the particular address (e.g.: AttachTransaction).
+ // The channel is non-retrying, so should be wrapped into retrying channel on demand.
+ NRpc::IChannelPtr CreateNonRetryingChannelByAddress(const TString& address) const;
+
+ TConnectionPtr GetRpcProxyConnection() override;
+ TClientPtr GetRpcProxyClient() override;
+
+ NRpc::IChannelPtr GetRetryingChannel() const override;
+ NRpc::IChannelPtr CreateNonRetryingStickyChannel() const override;
+ NRpc::IChannelPtr WrapStickyChannelIntoRetrying(NRpc::IChannelPtr underlying) const override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/config.cpp b/yt/yt/client/api/rpc_proxy/config.cpp
new file mode 100644
index 0000000000..2ce120929c
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/config.cpp
@@ -0,0 +1,125 @@
+#include "config.h"
+
+#include "address_helpers.h"
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/bus/tcp/config.h>
+
+#include <yt/yt/core/http/config.h>
+
+#include <yt/yt/core/rpc/config.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TConnectionConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("cluster_url", &TThis::ClusterUrl)
+ .Default();
+ registrar.Parameter("cluster_tag", &TThis::ClusterTag)
+ .Optional();
+ registrar.Parameter("proxy_role", &TThis::ProxyRole)
+ .Optional();
+ registrar.Parameter("proxy_address_type", &TThis::ProxyAddressType)
+ .Optional();
+ registrar.Parameter("proxy_network_name", &TThis::ProxyNetworkName)
+ .Optional();
+ registrar.Parameter("proxy_addresses", &TThis::ProxyAddresses)
+ .Alias("addresses")
+ .Optional();
+ registrar.Parameter("proxy_endpoints", &TThis::ProxyEndpoints)
+ .Optional();
+ registrar.Parameter("proxy_unix_domain_socket", &TThis::ProxyUnixDomainSocket)
+ .Optional();
+
+ registrar.Parameter("dynamic_channel_pool", &TThis::DynamicChannelPool)
+ .DefaultNew();
+
+ registrar.Parameter("ping_period", &TThis::PingPeriod)
+ .Default(TDuration::Seconds(3));
+
+ registrar.Parameter("proxy_list_update_period", &TThis::ProxyListUpdatePeriod)
+ .Default(TDuration::Minutes(5));
+ registrar.Parameter("proxy_list_retry_period", &TThis::ProxyListRetryPeriod)
+ .Default(TDuration::Seconds(1));
+ registrar.Parameter("max_proxy_list_retry_period", &TThis::MaxProxyListRetryPeriod)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("max_proxy_list_update_attempts", &TThis::MaxProxyListUpdateAttempts)
+ .Default(3);
+
+ registrar.Parameter("rpc_timeout", &TThis::RpcTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("rpc_acknowledgement_timeout", &TThis::RpcAcknowledgementTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("timestamp_provider_latest_timestamp_update_period", &TThis::TimestampProviderLatestTimestampUpdatePeriod)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("default_transaction_timeout", &TThis::DefaultTransactionTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("default_lookup_rows_timeout", &TThis::DefaultLookupRowsTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("default_select_rows_timeout", &TThis::DefaultSelectRowsTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("default_total_streaming_timeout", &TThis::DefaultTotalStreamingTimeout)
+ .Default(TDuration::Minutes(15));
+ registrar.Parameter("default_streaming_stall_timeout", &TThis::DefaultStreamingStallTimeout)
+ .Default(TDuration::Minutes(1));
+
+ registrar.Parameter("default_ping_period", &TThis::DefaultPingPeriod)
+ .Default(TDuration::Seconds(5));
+
+ registrar.Parameter("bus_client", &TThis::BusClient)
+ .DefaultNew();
+ registrar.Parameter("idle_channel_ttl", &TThis::IdleChannelTtl)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("http_client", &TThis::HttpClient)
+ .DefaultNew();
+
+ registrar.Parameter("request_codec", &TThis::RequestCodec)
+ .Default(NCompression::ECodec::None);
+ registrar.Parameter("response_codec", &TThis::ResponseCodec)
+ .Default(NCompression::ECodec::None);
+ // COMPAT(kiselyovp): legacy RPC codecs
+ registrar.Parameter("enable_legacy_rpc_codecs", &TThis::EnableLegacyRpcCodecs)
+ .Default(true);
+
+ registrar.Parameter("enable_retries", &TThis::EnableRetries)
+ .Default(false);
+ registrar.Parameter("retrying_channel", &TThis::RetryingChannel)
+ .DefaultNew();
+
+ registrar.Parameter("modify_rows_batch_capacity", &TThis::ModifyRowsBatchCapacity)
+ .GreaterThanOrEqual(0)
+ .Default(0);
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->DynamicChannelPool->MaxPeerCount = 100;
+ });
+
+ registrar.Parameter("clock_cluster_tag", &TThis::ClockClusterTag)
+ .Default(NObjectClient::InvalidCellTag);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (!config->ProxyEndpoints && !config->ClusterUrl && !config->ProxyAddresses && !config->ProxyUnixDomainSocket) {
+ THROW_ERROR_EXCEPTION("Either \"endpoints\" or \"cluster_url\" or \"proxy_addresses\" or \"proxy_unix_domain_socket\" must be specified");
+ }
+ if (config->ProxyEndpoints && config->ProxyRole) {
+ THROW_ERROR_EXCEPTION("\"proxy_role\" is not supported by Service Discovery");
+ }
+ if (config->ProxyAddresses && config->ProxyAddresses->empty()) {
+ THROW_ERROR_EXCEPTION("\"proxy_addresses\" must not be empty");
+ }
+
+ if (!config->ClusterName && config->ClusterUrl) {
+ config->ClusterName = InferYTClusterFromClusterUrl(*config->ClusterUrl);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/config.h b/yt/yt/client/api/rpc_proxy/config.h
new file mode 100644
index 0000000000..4f4c7e548d
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/config.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/tcp/config.h>
+
+#include <yt/yt/core/http/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/library/re2/public.h>
+
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/config.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConnectionConfig
+ : public NApi::TConnectionConfig
+{
+public:
+ std::optional<TString> ClusterUrl;
+ std::optional<TClusterTag> ClusterTag;
+ std::optional<TString> ProxyRole;
+ std::optional<EAddressType> ProxyAddressType;
+ std::optional<TString> ProxyNetworkName;
+ std::optional<std::vector<TString>> ProxyAddresses;
+ NRpc::TServiceDiscoveryEndpointsConfigPtr ProxyEndpoints;
+ std::optional<TString> ProxyUnixDomainSocket;
+
+ NRpc::TDynamicChannelPoolConfigPtr DynamicChannelPool;
+
+ TDuration PingPeriod;
+ TDuration ProxyListUpdatePeriod;
+ TDuration ProxyListRetryPeriod;
+ TDuration MaxProxyListRetryPeriod;
+ int MaxProxyListUpdateAttempts;
+
+ TDuration RpcTimeout;
+ std::optional<TDuration> RpcAcknowledgementTimeout;
+
+ TDuration TimestampProviderLatestTimestampUpdatePeriod;
+
+ TDuration DefaultTransactionTimeout;
+ TDuration DefaultLookupRowsTimeout;
+ TDuration DefaultSelectRowsTimeout;
+ TDuration DefaultTotalStreamingTimeout;
+ TDuration DefaultStreamingStallTimeout;
+ TDuration DefaultPingPeriod;
+
+ NBus::TBusConfigPtr BusClient;
+ TDuration IdleChannelTtl;
+
+ NHttp::TClientConfigPtr HttpClient;
+
+ NCompression::ECodec RequestCodec;
+ NCompression::ECodec ResponseCodec;
+
+ bool EnableLegacyRpcCodecs;
+
+ bool EnableRetries;
+ NRpc::TRetryingChannelConfigPtr RetryingChannel;
+
+ i64 ModifyRowsBatchCapacity;
+
+ NObjectClient::TCellTag ClockClusterTag;
+
+ REGISTER_YSON_STRUCT(TConnectionConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnectionConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/connection.cpp b/yt/yt/client/api/rpc_proxy/connection.cpp
new file mode 100644
index 0000000000..678840e77c
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/connection.cpp
@@ -0,0 +1,18 @@
+#include "connection.h"
+#include "connection_impl.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::IConnectionPtr CreateConnection(
+ TConnectionConfigPtr config,
+ TConnectionOptions options)
+{
+ return New<TConnection>(std::move(config), std::move(options));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/connection.h b/yt/yt/client/api/rpc_proxy/connection.h
new file mode 100644
index 0000000000..b89ec650c4
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/connection.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/connection.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConnectionOptions
+ : public NApi::TConnectionOptions
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::IConnectionPtr CreateConnection(
+ TConnectionConfigPtr config,
+ TConnectionOptions options = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/connection_impl.cpp b/yt/yt/client/api/rpc_proxy/connection_impl.cpp
new file mode 100644
index 0000000000..310af6a14e
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/connection_impl.cpp
@@ -0,0 +1,506 @@
+#include "connection_impl.h"
+#include "discovery_service_proxy.h"
+#include "connection_impl.h"
+#include "client_impl.h"
+#include "config.h"
+#include "helpers.h"
+#include "private.h"
+
+#include <yt/yt/core/net/local_address.h>
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/bus/tcp/dispatcher.h>
+
+#include <yt/yt/core/http/client.h>
+#include <yt/yt/core/http/http.h>
+#include <yt/yt/core/http/helpers.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/rpc/bus/channel.h>
+#include <yt/yt/core/rpc/roaming_channel.h>
+#include <yt/yt/core/rpc/caching_channel_factory.h>
+#include <yt/yt/core/rpc/dynamic_channel_pool.h>
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/service_discovery/service_discovery.h>
+
+#include <yt/yt/build/ya_version.h>
+
+#include <util/system/env.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NBus;
+using namespace NRpc;
+using namespace NNet;
+using namespace NHttp;
+using namespace NYson;
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NServiceDiscovery;
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashMap<TString, TString> ParseProxyUrlAliasingRules(TString envConfig)
+{
+ if (envConfig.empty()) {
+ return {};
+ }
+ return ConvertTo<THashMap<TString, TString>>(TYsonString(envConfig));
+}
+
+void ApplyProxyUrlAliasingRules(TString& url)
+{
+ static const auto rules = ParseProxyUrlAliasingRules(GetEnv("YT_PROXY_URL_ALIASING_CONFIG"));
+ if (auto ruleIt = rules.find(url); ruleIt != rules.end()) {
+ url = ruleIt->second;
+ }
+}
+
+TString NormalizeHttpProxyUrl(TString url)
+{
+ const TStringBuf CanonicalPrefix = "http://";
+ const TStringBuf CanonicalSuffix = ".yt.yandex.net";
+
+ ApplyProxyUrlAliasingRules(url);
+
+ if (url.find('.') == TString::npos &&
+ url.find(':') == TString::npos &&
+ url.find("localhost") == TString::npos)
+ {
+ url.append(CanonicalSuffix);
+ }
+
+ if (!url.StartsWith(CanonicalPrefix)) {
+ url.prepend(CanonicalPrefix);
+ }
+
+ return url;
+}
+
+namespace {
+
+TString MakeConnectionLoggingTag(const TConnectionConfigPtr& config, TGuid connectionId)
+{
+ TStringBuilder builder;
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+ if (config->ClusterUrl) {
+ delimitedBuilder->AppendFormat("ClusterUrl: %v", *config->ClusterUrl);
+ }
+ if (config->ProxyRole) {
+ delimitedBuilder->AppendFormat("ProxyRole: %v", *config->ProxyRole);
+ }
+ delimitedBuilder->AppendFormat("ConnectionId: %v", connectionId);
+ return builder.Flush();
+}
+
+TString MakeEndpointDescription(const TConnectionConfigPtr& config, TGuid connectionId)
+{
+ return Format("Rpc{%v}", MakeConnectionLoggingTag(config, connectionId));
+}
+
+IAttributeDictionaryPtr MakeEndpointAttributes(const TConnectionConfigPtr& config, TGuid connectionId)
+{
+ return ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("rpc_proxy").Value(true)
+ .DoIf(config->ClusterUrl.has_value(), [&] (auto fluent) {
+ fluent
+ .Item("cluster_url").Value(*config->ClusterUrl);
+ })
+ .DoIf(config->ProxyRole.has_value(), [&] (auto fluent) {
+ fluent
+ .Item("proxy_role").Value(*config->ProxyRole);
+ })
+ .Item("connection_id").Value(connectionId)
+ .EndMap());
+}
+
+TString MakeConnectionClusterId(const TConnectionConfigPtr& config)
+{
+ if (config->ClusterName) {
+ return Format("Rpc(Name=%v)", *config->ClusterName);
+ } else if (config->ClusterUrl) {
+ return Format("Rpc(Url=%v)", *config->ClusterUrl);
+ } else {
+ return Format("Rpc(ProxyAddresses=%v)", config->ProxyAddresses);
+ }
+}
+
+class TProxyChannelProvider
+ : public IRoamingChannelProvider
+{
+public:
+ TProxyChannelProvider(
+ TConnectionConfigPtr config,
+ TGuid connectionId,
+ TDynamicChannelPoolPtr pool,
+ bool sticky)
+ : Pool_(std::move(pool))
+ , Sticky_(sticky)
+ , EndpointDescription_(MakeEndpointDescription(config, connectionId))
+ , EndpointAttributes_(MakeEndpointAttributes(config, connectionId))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription_;
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ TFuture<IChannelPtr> GetChannel() override
+ {
+ if (Sticky_) {
+ auto guard = Guard(SpinLock_);
+ if (!Channel_) {
+ Channel_ = Pool_->GetRandomChannel();
+ }
+ return Channel_;
+ } else {
+ return Pool_->GetRandomChannel();
+ }
+ }
+
+ void Terminate(const TError& /*error*/) override
+ { }
+
+ TFuture<IChannelPtr> GetChannel(const TString& /*serviceName*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& /*request*/) override
+ {
+ return GetChannel();
+ }
+
+private:
+ const TDynamicChannelPoolPtr Pool_;
+ const bool Sticky_;
+ const TGuid ConnectionId_;
+
+ const TString EndpointDescription_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TFuture<IChannelPtr> Channel_;
+};
+
+TConnectionConfigPtr GetPostprocessedConfig(TConnectionConfigPtr config)
+{
+ config->Postprocess();
+ return config;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConnection::TConnection(TConnectionConfigPtr config, TConnectionOptions options)
+ : Config_(GetPostprocessedConfig(std::move(config)))
+ , ConnectionId_(TGuid::Create())
+ , LoggingTag_(MakeConnectionLoggingTag(Config_, ConnectionId_))
+ , ClusterId_(MakeConnectionClusterId(Config_))
+ , Logger(RpcProxyClientLogger.WithRawTag(LoggingTag_))
+ , ChannelFactory_(Config_->ProxyUnixDomainSocket
+ ? NRpc::NBus::CreateUdsBusChannelFactory(Config_->BusClient)
+ : NRpc::NBus::CreateTcpBusChannelFactory(Config_->BusClient))
+ , CachingChannelFactory_(CreateCachingChannelFactory(
+ ChannelFactory_,
+ Config_->IdleChannelTtl))
+ , ChannelPool_(New<TDynamicChannelPool>(
+ Config_->DynamicChannelPool,
+ ChannelFactory_,
+ MakeEndpointDescription(Config_, ConnectionId_),
+ MakeEndpointAttributes(Config_, ConnectionId_),
+ TApiServiceProxy::GetDescriptor().ServiceName,
+ TDiscoverRequestHook()))
+{
+ if (options.ConnectionInvoker) {
+ ConnectionInvoker_ = options.ConnectionInvoker;
+ } else {
+ ActionQueue_ = New<TActionQueue>("RpcProxyConn");
+ ConnectionInvoker_ = ActionQueue_->GetInvoker();
+ }
+
+ UpdateProxyListExecutor_ = New<TPeriodicExecutor>(
+ GetInvoker(),
+ BIND(&TConnection::OnProxyListUpdate, MakeWeak(this)),
+ TPeriodicExecutorOptions::WithJitter(Config_->ProxyListUpdatePeriod));
+
+ if (Config_->ProxyEndpoints) {
+ ServiceDiscovery_ = NRpc::TDispatcher::Get()->GetServiceDiscovery();
+ if (!ServiceDiscovery_) {
+ ChannelPool_->SetPeerDiscoveryError(TError("No Service Discovery is configured"));
+ return;
+ }
+ }
+
+ if (Config_->ProxyAddresses) {
+ ChannelPool_->SetPeers(*Config_->ProxyAddresses);
+ }
+
+ if (Config_->ProxyUnixDomainSocket) {
+ ChannelPool_->SetPeers({*Config_->ProxyUnixDomainSocket});
+ }
+}
+
+TConnection::~TConnection()
+{
+ RunNoExcept([&] {
+ Terminate();
+ });
+}
+
+IChannelPtr TConnection::CreateChannel(bool sticky)
+{
+ auto provider = New<TProxyChannelProvider>(
+ Config_,
+ ConnectionId_,
+ ChannelPool_,
+ sticky);
+ return CreateRoamingChannel(std::move(provider));
+}
+
+IChannelPtr TConnection::CreateChannelByAddress(const TString& address)
+{
+ return CachingChannelFactory_->CreateChannel(address);
+}
+
+TClusterTag TConnection::GetClusterTag() const
+{
+ THROW_ERROR_EXCEPTION_UNLESS(Config_->ClusterTag,
+ "Cluster tag is not specified in connection config; please set \"cluster_tag\"");
+ return *Config_->ClusterTag;
+}
+
+const TString& TConnection::GetLoggingTag() const
+{
+ return LoggingTag_;
+}
+
+const TString& TConnection::GetClusterId() const
+{
+ return ClusterId_;
+}
+
+const std::optional<TString>& TConnection::GetClusterName() const
+{
+ return Config_->ClusterName;
+}
+
+bool TConnection::IsSameCluster(const IConnectionPtr& other) const
+{
+ // NB: Cluster tag is not defined for RPC proxy connection
+ // so we use some best-effort logic here.
+ return GetClusterId() == other->GetClusterId();
+}
+
+IInvokerPtr TConnection::GetInvoker()
+{
+ return ConnectionInvoker_;
+}
+
+NApi::IClientPtr TConnection::CreateClient(const TClientOptions& options)
+{
+ if (options.Token) {
+ DiscoveryToken_.Store(*options.Token);
+ }
+
+ if (Config_->ClusterUrl || Config_->ProxyEndpoints) {
+ UpdateProxyListExecutor_->Start();
+ }
+
+ return New<TClient>(this, options);
+}
+
+NHiveClient::ITransactionParticipantPtr TConnection::CreateTransactionParticipant(
+ NHiveClient::TCellId /*cellId*/,
+ const TTransactionParticipantOptions& /*options*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void TConnection::ClearMetadataCaches()
+{ }
+
+void TConnection::Terminate()
+{
+ YT_LOG_DEBUG("Terminating connection");
+ ChannelPool_->Terminate(TError("Connection terminated"));
+ YT_UNUSED_FUTURE(UpdateProxyListExecutor_->Stop());
+}
+
+const TConnectionConfigPtr& TConnection::GetConfig()
+{
+ return Config_;
+}
+
+std::vector<TString> TConnection::DiscoverProxiesViaHttp()
+{
+ auto correlationId = TGuid::Create();
+
+ try {
+ YT_LOG_DEBUG("Updating proxy list via HTTP (CorrelationId: %v)", correlationId);
+
+ auto poller = TTcpDispatcher::Get()->GetXferPoller();
+ auto client = NHttp::CreateClient(Config_->HttpClient, std::move(poller));
+ auto headers = New<THeaders>();
+ SetUserAgent(headers, GetRpcUserAgent());
+ if (auto token = DiscoveryToken_.Load()) {
+ headers->Add("Authorization", "OAuth " + token);
+ }
+ headers->Add("X-YT-Correlation-Id", ToString(correlationId));
+ headers->Add("X-YT-Header-Format", "<format=text>yson");
+ headers->Add(
+ "X-YT-Parameters",
+ BuildYsonStringFluently(EYsonFormat::Text)
+ .BeginMap()
+ .Item("output_format")
+ .BeginAttributes()
+ .Item("format").Value("text")
+ .EndAttributes()
+ .Value("yson")
+ .OptionalItem("role", Config_->ProxyRole)
+ .OptionalItem("address_type", Config_->ProxyAddressType)
+ .OptionalItem("network_name", Config_->ProxyNetworkName)
+ .EndMap().ToString());
+
+ auto url = NormalizeHttpProxyUrl(*Config_->ClusterUrl) + "/api/v4/discover_proxies";
+ auto rsp = WaitFor(client->Get(url, headers))
+ .ValueOrThrow();
+
+ if (rsp->GetStatusCode() != EStatusCode::OK) {
+ THROW_ERROR_EXCEPTION("HTTP proxy discovery request returned an error")
+ << TErrorAttribute("correlation_id", correlationId)
+ << TErrorAttribute("status_code", rsp->GetStatusCode())
+ << ParseYTError(rsp);
+ }
+
+ auto body = rsp->ReadAll();
+ YT_LOG_DEBUG("Received proxy list via HTTP (CorrelationId: %v)", correlationId);
+
+ auto node = ConvertTo<INodePtr>(TYsonString(ToString(body)));
+ node = node->AsMap()->FindChild("proxies");
+ return ConvertTo<std::vector<TString>>(node);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error discovering RPC proxies via HTTP")
+ << TErrorAttribute("correlation_id", correlationId)
+ << ex;
+ }
+}
+
+std::vector<TString> TConnection::DiscoverProxiesViaServiceDiscovery()
+{
+ YT_LOG_DEBUG("Updating proxy list via Service Discovery");
+
+ if (!ServiceDiscovery_) {
+ THROW_ERROR_EXCEPTION("No service discovery configured");
+ }
+
+ std::vector<TFuture<TEndpointSet>> asyncEndpointSets;
+ for (const auto& cluster : Config_->ProxyEndpoints->Clusters) {
+ asyncEndpointSets.push_back(ServiceDiscovery_->ResolveEndpoints(
+ cluster,
+ Config_->ProxyEndpoints->EndpointSetId));
+ }
+
+ auto endpointSets = WaitFor(AllSet(asyncEndpointSets))
+ .ValueOrThrow();
+
+ std::vector<TString> allAddresses;
+ std::vector<TError> errors;
+ for (int i = 0; i < std::ssize(endpointSets); ++i) {
+ if (!endpointSets[i].IsOK()) {
+ errors.push_back(endpointSets[i]);
+ YT_LOG_WARNING(
+ endpointSets[i],
+ "Could not resolve endpoints from cluster (Cluster: %v, EndpointSetId: %v)",
+ Config_->ProxyEndpoints->Clusters[i],
+ Config_->ProxyEndpoints->EndpointSetId);
+ continue;
+ }
+
+ auto addresses = AddressesFromEndpointSet(endpointSets[i].Value());
+ allAddresses.insert(allAddresses.end(), addresses.begin(), addresses.end());
+ }
+
+ if (errors.size() == endpointSets.size()) {
+ THROW_ERROR_EXCEPTION("Error discovering RPC proxies via Service Discovery") << errors;
+ }
+
+ return allAddresses;
+}
+
+void TConnection::OnProxyListUpdate()
+{
+ auto attributes = CreateEphemeralAttributes();
+ if (Config_->ProxyEndpoints) {
+ attributes->Set("endpoint_set_cluster", Config_->ProxyEndpoints->Cluster);
+ attributes->Set("endpoint_set_id", Config_->ProxyEndpoints->EndpointSetId);
+ } else if (Config_->ClusterUrl) {
+ attributes->Set("cluster_url", Config_->ClusterUrl);
+ } else {
+ YT_ABORT();
+ }
+ attributes->Set("proxy_role", Config_->ProxyRole.value_or(DefaultRpcProxyRole));
+
+ auto backoff = Config_->ProxyListRetryPeriod;
+ for (int attempt = 0;; ++attempt) {
+ try {
+ std::vector<TString> proxies;
+ if (Config_->ProxyEndpoints) {
+ proxies = DiscoverProxiesViaServiceDiscovery();
+ } else if (Config_->ClusterUrl) {
+ proxies = DiscoverProxiesViaHttp();
+ } else {
+ YT_ABORT();
+ }
+
+ if (proxies.empty()) {
+ THROW_ERROR_EXCEPTION("Proxy list is empty");
+ }
+
+ ChannelPool_->SetPeers(proxies);
+
+ break;
+ } catch (const std::exception& ex) {
+ if (attempt > Config_->MaxProxyListUpdateAttempts) {
+ ChannelPool_->SetPeerDiscoveryError(TError(ex) << *attributes);
+ }
+
+ YT_LOG_WARNING(ex, "Error updating proxy list (Attempt: %v, Backoff: %v)",
+ attempt,
+ backoff);
+
+ TDelayedExecutor::WaitForDuration(backoff);
+
+ if (backoff < Config_->MaxProxyListRetryPeriod) {
+ backoff *= 1.2;
+ }
+
+ if (attempt > Config_->MaxProxyListUpdateAttempts) {
+ attempt = 0;
+ }
+ }
+ }
+}
+
+NYson::TYsonString TConnection::GetConfigYson() const
+{
+ return ConvertToYsonString(Config_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/connection_impl.h b/yt/yt/client/api/rpc_proxy/connection_impl.h
new file mode 100644
index 0000000000..5f2c618cfe
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/connection_impl.h
@@ -0,0 +1,90 @@
+#pragma once
+
+#include "connection.h"
+
+#include <yt/yt/client/api/sticky_transaction_pool.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+// TODO(prime@): Create HTTP endpoint for discovery that works without authentication.
+#include <yt/yt/core/misc/atomic_object.h>
+
+#include <yt/yt/core/service_discovery/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConnection
+ : public NApi::IConnection
+{
+public:
+ TConnection(TConnectionConfigPtr config, TConnectionOptions options);
+ ~TConnection();
+
+ NRpc::IChannelPtr CreateChannel(bool sticky);
+ NRpc::IChannelPtr CreateChannelByAddress(const TString& address);
+
+ const TConnectionConfigPtr& GetConfig();
+
+ // IConnection implementation.
+ TClusterTag GetClusterTag() const override;
+ const TString& GetLoggingTag() const override;
+ const TString& GetClusterId() const override;
+ const std::optional<TString>& GetClusterName() const override;
+
+ bool IsSameCluster(const IConnectionPtr& other) const override;
+
+ IInvokerPtr GetInvoker() override;
+
+ NApi::IClientPtr CreateClient(const NApi::TClientOptions& options) override;
+ NHiveClient::ITransactionParticipantPtr CreateTransactionParticipant(
+ NHiveClient::TCellId cellId,
+ const NApi::TTransactionParticipantOptions& options) override;
+
+ void ClearMetadataCaches() override;
+
+ void Terminate() override;
+
+ NYson::TYsonString GetConfigYson() const override;
+
+private:
+ friend class TClient;
+ friend class TTransaction;
+ friend class TTimestampProvider;
+
+ const TConnectionConfigPtr Config_;
+
+ const TGuid ConnectionId_;
+ const TString LoggingTag_;
+ const TString ClusterId_;
+ const NLogging::TLogger Logger;
+ const NRpc::IChannelFactoryPtr ChannelFactory_;
+ const NRpc::IChannelFactoryPtr CachingChannelFactory_;
+ const NRpc::TDynamicChannelPoolPtr ChannelPool_;
+
+ NConcurrency::TActionQueuePtr ActionQueue_;
+ IInvokerPtr ConnectionInvoker_;
+
+ NConcurrency::TPeriodicExecutorPtr UpdateProxyListExecutor_;
+
+ // TODO(prime@): Create HTTP endpoint for discovery that works without authentication.
+ TAtomicObject<TString> DiscoveryToken_;
+
+ NServiceDiscovery::IServiceDiscoveryPtr ServiceDiscovery_;
+
+ std::vector<TString> DiscoverProxiesViaHttp();
+ std::vector<TString> DiscoverProxiesViaServiceDiscovery();
+
+ void OnProxyListUpdate();
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/discovery_service_proxy.h b/yt/yt/client/api/rpc_proxy/discovery_service_proxy.h
new file mode 100644
index 0000000000..8a524be756
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/discovery_service_proxy.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "public.h"
+
+#include "protocol_version.h"
+
+#include <yt/yt_proto/yt/client/api/rpc_proxy/proto/discovery_service.pb.h>
+
+#include <yt/yt/core/rpc/client.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDiscoveryServiceProxy
+ : public NRpc::TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TDiscoveryServiceProxy, DiscoveryService,
+ .SetProtocolVersion(NRpc::TProtocolVersion{0, 0}));
+
+ DEFINE_RPC_PROXY_METHOD(NRpcProxy::NProto, DiscoverProxies,
+ .SetMultiplexingBand(NRpc::EMultiplexingBand::Control));
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/file_reader.cpp b/yt/yt/client/api/rpc_proxy/file_reader.cpp
new file mode 100644
index 0000000000..24c55ff521
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/file_reader.cpp
@@ -0,0 +1,71 @@
+#include "file_reader.h"
+
+#include <yt/yt/client/api/file_reader.h>
+
+#include <yt/yt/client/hydra/public.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+using namespace NObjectClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileReader
+ : public IFileReader
+{
+public:
+ TFileReader(
+ IAsyncZeroCopyInputStreamPtr underlying,
+ TObjectId id,
+ NHydra::TRevision revision)
+ : Underlying_(std::move(underlying))
+ , Id_(id)
+ , Revision_(revision)
+ {
+ YT_VERIFY(Underlying_);
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ return Underlying_->Read();
+ }
+
+ NObjectClient::TObjectId GetId() const override
+ {
+ return Id_;
+ }
+
+ NHydra::TRevision GetRevision() const override
+ {
+ return Revision_;
+ }
+
+private:
+ const IAsyncZeroCopyInputStreamPtr Underlying_;
+ const TObjectId Id_;
+ const NHydra::TRevision Revision_;
+};
+
+TFuture<IFileReaderPtr> CreateFileReader(
+ TApiServiceProxy::TReqReadFilePtr request)
+{
+ return NRpc::CreateRpcClientInputStream(std::move(request))
+ .Apply(BIND([=] (const IAsyncZeroCopyInputStreamPtr& inputStream) {
+ return inputStream->Read().Apply(BIND([=] (const TSharedRef& metaRef) {
+ NApi::NRpcProxy::NProto::TReadFileMeta meta;
+ if (!TryDeserializeProto(&meta, metaRef)) {
+ THROW_ERROR_EXCEPTION("Failed to deserialize file stream header");
+ }
+
+ return New<TFileReader>(inputStream, FromProto<TObjectId>(meta.id()), meta.revision());
+ })).As<IFileReaderPtr>();
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/file_reader.h b/yt/yt/client/api/rpc_proxy/file_reader.h
new file mode 100644
index 0000000000..f91af5e27d
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/file_reader.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<IFileReaderPtr> CreateFileReader(
+ TApiServiceProxy::TReqReadFilePtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/file_writer.cpp b/yt/yt/client/api/rpc_proxy/file_writer.cpp
new file mode 100644
index 0000000000..33318dff3a
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/file_writer.cpp
@@ -0,0 +1,94 @@
+#include "file_writer.h"
+
+#include <yt/yt/client/api/file_writer.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileWriter
+ : public IFileWriter
+{
+public:
+ TFileWriter(
+ TApiServiceProxy::TReqWriteFilePtr request)
+ : Request_(std::move(request))
+ {
+ YT_VERIFY(Request_);
+ }
+
+ TFuture<void> Open() override
+ {
+ ValidateNotClosed();
+
+ if (!OpenResult_) {
+ OpenResult_ = NRpc::CreateRpcClientOutputStream(Request_)
+ .Apply(BIND([=, this, this_ = MakeStrong(this)] (const IAsyncZeroCopyOutputStreamPtr& outputStream) {
+ Underlying_ = outputStream;
+ })).As<void>();
+ }
+
+ return OpenResult_;
+ }
+
+ TFuture<void> Write(const TSharedRef& data) override
+ {
+ ValidateOpened();
+ ValidateNotClosed();
+
+ if (!data) {
+ return VoidFuture;
+ }
+
+ // Returned future might be set instantly, and the user can modify #data right after that.
+ struct TTag { };
+ auto dataCopy = TSharedMutableRef::MakeCopy<TTag>(data);
+ return Underlying_->Write(dataCopy);
+ }
+
+ TFuture<void> Close() override
+ {
+ ValidateOpened();
+ ValidateNotClosed();
+
+ Closed_ = true;
+ return Underlying_->Close();
+ }
+
+private:
+ const TApiServiceProxy::TReqWriteFilePtr Request_;
+
+ IAsyncZeroCopyOutputStreamPtr Underlying_;
+ TFuture<void> OpenResult_;
+ bool Closed_ = false;
+
+ void ValidateOpened()
+ {
+ if (!OpenResult_ || !OpenResult_.IsSet()) {
+ THROW_ERROR_EXCEPTION("Cannot write into an unopened file writer");
+ }
+ OpenResult_.Get().ThrowOnError();
+ }
+
+ void ValidateNotClosed()
+ {
+ if (Closed_) {
+ THROW_ERROR_EXCEPTION("File writer is closed");
+ }
+ }
+};
+
+IFileWriterPtr CreateFileWriter(
+ TApiServiceProxy::TReqWriteFilePtr request)
+{
+ return New<TFileWriter>(std::move(request));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/file_writer.h b/yt/yt/client/api/rpc_proxy/file_writer.h
new file mode 100644
index 0000000000..19d9b67739
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/file_writer.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFileWriterPtr CreateFileWriter(
+ TApiServiceProxy::TReqWriteFilePtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/helpers.cpp b/yt/yt/client/api/rpc_proxy/helpers.cpp
new file mode 100644
index 0000000000..ad2603610b
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/helpers.cpp
@@ -0,0 +1,1844 @@
+#include "helpers.h"
+
+#include <yt/yt/client/api/rowset.h>
+
+#include <yt/yt/client/table_client/column_sort_schema.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_base.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ThrowUnimplemented(const TString& method)
+{
+ THROW_ERROR_EXCEPTION("%Qv method is not implemented in RPC proxy",
+ method);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+////////////////////////////////////////////////////////////////////////////////
+// OPTIONS
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TTransactionalOptions* proto,
+ const NApi::TTransactionalOptions& options)
+{
+ if (options.TransactionId) {
+ ToProto(proto->mutable_transaction_id(), options.TransactionId);
+ }
+ proto->set_ping(options.Ping);
+ proto->set_ping_ancestors(options.PingAncestors);
+ proto->set_suppress_transaction_coordinator_sync(options.SuppressTransactionCoordinatorSync);
+ proto->set_suppress_upstream_sync(options.SuppressUpstreamSync);
+}
+
+void ToProto(
+ NProto::TPrerequisiteOptions* proto,
+ const NApi::TPrerequisiteOptions& options)
+{
+ for (const auto& item : options.PrerequisiteTransactionIds) {
+ auto* protoItem = proto->add_transactions();
+ ToProto(protoItem->mutable_transaction_id(), item);
+ }
+ for (const auto& item : options.PrerequisiteRevisions) {
+ auto* protoItem = proto->add_revisions();
+ protoItem->set_path(item->Path);
+ protoItem->set_revision(item->Revision);
+ }
+}
+
+void ToProto(
+ NProto::TMasterReadOptions* proto,
+ const NApi::TMasterReadOptions& options)
+{
+ proto->set_read_from(static_cast<NProto::EMasterReadKind>(options.ReadFrom));
+ proto->set_disable_per_user_cache(options.DisablePerUserCache);
+ proto->set_expire_after_successful_update_time(NYT::ToProto<i64>(options.ExpireAfterSuccessfulUpdateTime));
+ proto->set_expire_after_failed_update_time(NYT::ToProto<i64>(options.ExpireAfterFailedUpdateTime));
+ proto->set_success_staleness_bound(NYT::ToProto<i64>(options.SuccessStalenessBound));
+ if (options.CacheStickyGroupSize) {
+ proto->set_cache_sticky_group_size(*options.CacheStickyGroupSize);
+ }
+}
+
+void ToProto(
+ NProto::TMutatingOptions* proto,
+ const NApi::TMutatingOptions& options)
+{
+ ToProto(proto->mutable_mutation_id(), options.GetOrGenerateMutationId());
+ proto->set_retry(options.Retry);
+}
+
+void ToProto(
+ NProto::TSuppressableAccessTrackingOptions* proto,
+ const NApi::TSuppressableAccessTrackingOptions& options)
+{
+ proto->set_suppress_access_tracking(options.SuppressAccessTracking);
+ proto->set_suppress_modification_tracking(options.SuppressModificationTracking);
+ proto->set_suppress_expiration_timeout_renewal(options.SuppressExpirationTimeoutRenewal);
+}
+
+void ToProto(
+ NProto::TTabletRangeOptions* proto,
+ const NApi::TTabletRangeOptions& options)
+{
+ if (options.FirstTabletIndex) {
+ proto->set_first_tablet_index(*options.FirstTabletIndex);
+ }
+ if (options.LastTabletIndex) {
+ proto->set_last_tablet_index(*options.LastTabletIndex);
+ }
+}
+
+void ToProto(
+ NProto::TTabletReadOptions* protoOptions,
+ const NApi::TTabletReadOptionsBase& options)
+{
+ protoOptions->set_read_from(static_cast<NProto::ETabletReadKind>(options.ReadFrom));
+ if (options.CachedSyncReplicasTimeout) {
+ protoOptions->set_cached_sync_replicas_timeout(NYT::ToProto<i64>(*options.CachedSyncReplicasTimeout));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CONFIGS
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TRetentionConfig* protoConfig,
+ const NTableClient::TRetentionConfig& config)
+{
+ protoConfig->set_min_data_versions(config.MinDataVersions);
+ protoConfig->set_max_data_versions(config.MaxDataVersions);
+ protoConfig->set_min_data_ttl(config.MinDataTtl.GetValue());
+ protoConfig->set_max_data_ttl(config.MaxDataTtl.GetValue());
+ protoConfig->set_ignore_major_timestamp(config.IgnoreMajorTimestamp);
+}
+
+void FromProto(
+ NTableClient::TRetentionConfig* config,
+ const NProto::TRetentionConfig& protoConfig)
+{
+ config->MinDataVersions = protoConfig.min_data_versions();
+ config->MaxDataVersions = protoConfig.max_data_versions();
+ config->MinDataTtl = TDuration::FromValue(protoConfig.min_data_ttl());
+ config->MaxDataTtl = TDuration::FromValue(protoConfig.max_data_ttl());
+ config->IgnoreMajorTimestamp = protoConfig.ignore_major_timestamp();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// RESULTS
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TGetFileFromCacheResult* proto,
+ const NApi::TGetFileFromCacheResult& result)
+{
+ proto->set_path(result.Path);
+}
+
+void FromProto(
+ NApi::TGetFileFromCacheResult* result,
+ const NProto::TGetFileFromCacheResult& proto)
+{
+ result->Path = proto.path();
+}
+
+void ToProto(
+ NProto::TPutFileToCacheResult* proto,
+ const NApi::TPutFileToCacheResult& result)
+{
+ proto->set_path(result.Path);
+}
+
+void FromProto(
+ NApi::TPutFileToCacheResult* result,
+ const NProto::TPutFileToCacheResult& proto)
+{
+ result->Path = proto.path();
+}
+
+void ToProto(
+ NProto::TCheckPermissionResult* proto,
+ const NApi::TCheckPermissionResult& result)
+{
+ proto->Clear();
+
+ proto->set_action(static_cast<NProto::ESecurityAction>(result.Action));
+
+ ToProto(proto->mutable_object_id(), result.ObjectId);
+ if (result.ObjectName) {
+ proto->set_object_name(*result.ObjectName);
+ }
+
+ ToProto(proto->mutable_subject_id(), result.SubjectId);
+ if (result.SubjectName) {
+ proto->set_subject_name(*result.SubjectName);
+ }
+}
+
+void FromProto(
+ NApi::TCheckPermissionResult* result,
+ const NProto::TCheckPermissionResult& proto)
+{
+ result->Action = static_cast<NSecurityClient::ESecurityAction>(proto.action());
+
+ FromProto(&result->ObjectId, proto.object_id());
+ if (proto.has_object_name()) {
+ result->ObjectName = proto.object_name();
+ } else {
+ result->ObjectName.reset();
+ }
+
+ FromProto(&result->SubjectId, proto.subject_id());
+ if (proto.has_subject_name()) {
+ result->SubjectName = proto.subject_name();
+ } else {
+ result->SubjectName.reset();
+ }
+}
+
+void ToProto(
+ NProto::TCheckPermissionByAclResult* proto,
+ const NApi::TCheckPermissionByAclResult& result)
+{
+ proto->Clear();
+
+ proto->set_action(static_cast<NProto::ESecurityAction>(result.Action));
+
+ ToProto(proto->mutable_subject_id(), result.SubjectId);
+ if (result.SubjectName) {
+ proto->set_subject_name(*result.SubjectName);
+ }
+
+ NYT::ToProto(proto->mutable_missing_subjects(), result.MissingSubjects);
+}
+
+void FromProto(
+ NApi::TCheckPermissionByAclResult* result,
+ const NProto::TCheckPermissionByAclResult& proto)
+{
+ result->Action = static_cast<NSecurityClient::ESecurityAction>(proto.action());
+
+ FromProto(&result->SubjectId, proto.subject_id());
+ if (proto.has_subject_name()) {
+ result->SubjectName = proto.subject_name();
+ } else {
+ result->SubjectName.reset();
+ }
+
+ NYT::FromProto(&result->MissingSubjects, proto.missing_subjects());
+}
+
+void ToProto(
+ NProto::TListOperationsResult* proto,
+ const NApi::TListOperationsResult& result)
+{
+ proto->Clear();
+ NYT::ToProto(proto->mutable_operations(), result.Operations);
+
+ if (result.PoolTreeCounts) {
+ auto* poolTreeCounts = proto->mutable_pool_tree_counts()->mutable_entries();
+ for (const auto& entry: *result.PoolTreeCounts) {
+ (*poolTreeCounts)[entry.first] = entry.second;
+ }
+ }
+ if (result.PoolCounts) {
+ for (const auto& entry: *result.PoolCounts) {
+ auto* newPoolCount = proto->mutable_pool_counts()->add_entries();
+ newPoolCount->set_pool(entry.first);
+ newPoolCount->set_count(entry.second);
+ }
+ }
+ if (result.UserCounts) {
+ for (const auto& entry: *result.UserCounts) {
+ auto* newUserCount = proto->mutable_user_counts()->add_entries();
+ newUserCount->set_user(entry.first);
+ newUserCount->set_count(entry.second);
+ }
+ }
+
+ if (result.StateCounts) {
+ for (const auto& state: TEnumTraits<NScheduler::EOperationState>::GetDomainValues()) {
+ if ((*result.StateCounts)[state] != 0) {
+ auto* newStateCount = proto->mutable_state_counts()->add_entries();
+ newStateCount->set_state(ConvertOperationStateToProto(state));
+ newStateCount->set_count((*result.StateCounts)[state]);
+ }
+ }
+ }
+ if (result.TypeCounts) {
+ for (const auto& type: TEnumTraits<NScheduler::EOperationType>::GetDomainValues()) {
+ if ((*result.TypeCounts)[type] != 0) {
+ auto* newTypeCount = proto->mutable_type_counts()->add_entries();
+ newTypeCount->set_type(ConvertOperationTypeToProto(type));
+ newTypeCount->set_count((*result.TypeCounts)[type]);
+ }
+ }
+ }
+
+ if (result.FailedJobsCount) {
+ proto->set_failed_jobs_count(*result.FailedJobsCount);
+ }
+ proto->set_incomplete(result.Incomplete);
+}
+
+void FromProto(
+ NApi::TListOperationsResult* result,
+ const NProto::TListOperationsResult& proto)
+{
+ NYT::FromProto(&result->Operations, proto.operations());
+
+ if (proto.has_pool_tree_counts()) {
+ result->PoolTreeCounts.emplace();
+ for (const auto& [poolTree, count]: proto.pool_tree_counts().entries()) {
+ YT_VERIFY((*result->PoolTreeCounts)[poolTree] == 0);
+ (*result->PoolTreeCounts)[poolTree] = count;
+ }
+ } else {
+ result->PoolTreeCounts.reset();
+ }
+
+ if (proto.has_pool_counts()) {
+ result->PoolCounts.emplace();
+ for (const auto& poolCount: proto.pool_counts().entries()) {
+ auto pool = poolCount.pool();
+ YT_VERIFY((*result->PoolCounts)[pool] == 0);
+ (*result->PoolCounts)[pool] = poolCount.count();
+ }
+ } else {
+ result->PoolCounts.reset();
+ }
+ if (proto.has_user_counts()) {
+ result->UserCounts.emplace();
+ for (const auto& userCount: proto.user_counts().entries()) {
+ auto user = userCount.user();
+ YT_VERIFY((*result->UserCounts)[user] == 0);
+ (*result->UserCounts)[user] = userCount.count();
+ }
+ } else {
+ result->UserCounts.reset();
+ }
+
+ if (proto.has_state_counts()) {
+ result->StateCounts.emplace();
+ std::fill(result->StateCounts->begin(), result->StateCounts->end(), 0);
+ for (const auto& stateCount: proto.state_counts().entries()) {
+ auto state = ConvertOperationStateFromProto(stateCount.state());
+ YT_VERIFY(result->StateCounts->IsDomainValue(state));
+ YT_VERIFY((*result->StateCounts)[state] == 0);
+ (*result->StateCounts)[state] = stateCount.count();
+ }
+ } else {
+ result->StateCounts.reset();
+ }
+ if (proto.has_type_counts()) {
+ result->TypeCounts.emplace();
+ std::fill(result->TypeCounts->begin(), result->TypeCounts->end(), 0);
+ for (const auto& typeCount: proto.type_counts().entries()) {
+ auto type = ConvertOperationTypeFromProto(typeCount.type());
+ YT_VERIFY(result->TypeCounts->IsDomainValue(type));
+ YT_VERIFY((*result->TypeCounts)[type] == 0);
+ (*result->TypeCounts)[type] = typeCount.count();
+ }
+ } else {
+ result->TypeCounts.reset();
+ }
+
+ if (proto.has_failed_jobs_count()) {
+ result->FailedJobsCount = proto.failed_jobs_count();
+ } else {
+ result->FailedJobsCount.reset();
+ }
+ result->Incomplete = proto.incomplete();
+}
+
+void ToProto(
+ NProto::TListJobsResult* proto,
+ const NApi::TListJobsResult& result)
+{
+ proto->Clear();
+ NYT::ToProto(proto->mutable_jobs(), result.Jobs);
+
+ if (result.CypressJobCount) {
+ proto->set_cypress_job_count(*result.CypressJobCount);
+ }
+ if (result.ControllerAgentJobCount) {
+ proto->set_controller_agent_job_count(*result.ControllerAgentJobCount);
+ }
+ if (result.ArchiveJobCount) {
+ proto->set_archive_job_count(*result.ArchiveJobCount);
+ }
+
+ ToProto(proto->mutable_statistics(), result.Statistics);
+ NYT::ToProto(proto->mutable_errors(), result.Errors);
+}
+
+void FromProto(
+ NApi::TListJobsResult* result,
+ const NProto::TListJobsResult& proto)
+{
+ NYT::FromProto(&result->Jobs, proto.jobs());
+
+ if (proto.has_cypress_job_count()) {
+ result->CypressJobCount = proto.cypress_job_count();
+ } else {
+ result->CypressJobCount.reset();
+ }
+ if (proto.has_controller_agent_job_count()) {
+ result->ControllerAgentJobCount = proto.controller_agent_job_count();
+ } else {
+ result->ControllerAgentJobCount.reset();
+ }
+ if (proto.has_archive_job_count()) {
+ result->ArchiveJobCount = proto.archive_job_count();
+ } else {
+ result->ArchiveJobCount.reset();
+ }
+
+ FromProto(&result->Statistics, proto.statistics());
+ NYT::FromProto(&result->Errors, proto.errors());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// MISC
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TColumnSchema* protoSchema, const NTableClient::TColumnSchema& schema)
+{
+ protoSchema->set_stable_name(schema.StableName().Get());
+ protoSchema->set_name(schema.Name());
+ protoSchema->set_type(NYT::ToProto<int>(GetPhysicalType(schema.CastToV1Type())));
+ auto typeV3Yson = ConvertToYsonString(TTypeV3LogicalTypeWrapper{schema.LogicalType()});
+ protoSchema->set_type_v3(typeV3Yson.ToString());
+ if (schema.Lock()) {
+ protoSchema->set_lock(*schema.Lock());
+ } else {
+ protoSchema->clear_lock();
+ }
+ if (schema.Expression()) {
+ protoSchema->set_expression(*schema.Expression());
+ } else {
+ protoSchema->clear_expression();
+ }
+ if (schema.Aggregate()) {
+ protoSchema->set_aggregate(*schema.Aggregate());
+ } else {
+ protoSchema->clear_aggregate();
+ }
+ if (schema.SortOrder()) {
+ protoSchema->set_sort_order(NYT::ToProto<int>(*schema.SortOrder()));
+ } else {
+ protoSchema->clear_sort_order();
+ }
+ if (schema.Group()) {
+ protoSchema->set_group(*schema.Group());
+ } else {
+ protoSchema->clear_group();
+ }
+ if (schema.Required()) {
+ protoSchema->set_required(schema.Required());
+ } else {
+ protoSchema->clear_required();
+ }
+ if (schema.MaxInlineHunkSize()) {
+ protoSchema->set_max_inline_hunk_size(*schema.MaxInlineHunkSize());
+ } else {
+ protoSchema->clear_max_inline_hunk_size();
+ }
+}
+
+void FromProto(NTableClient::TColumnSchema* schema, const NProto::TColumnSchema& protoSchema)
+{
+ schema->SetName(protoSchema.name());
+ schema->SetStableName(
+ protoSchema.has_stable_name()
+ ? TStableName(protoSchema.stable_name())
+ : TStableName(protoSchema.name()));
+
+ auto physicalType = CheckedEnumCast<EValueType>(protoSchema.type());
+
+ TLogicalTypePtr columnType;
+ if (protoSchema.has_type_v3()) {
+ columnType = ConvertTo<TTypeV3LogicalTypeWrapper>(TYsonStringBuf(protoSchema.type_v3())).LogicalType;
+ auto [v1Type, v1Required] = CastToV1Type(columnType);
+ if (protoSchema.has_required() && protoSchema.required() != v1Required) {
+ THROW_ERROR_EXCEPTION("Fields \"type_v3\" and \"required\" do not match")
+ << TErrorAttribute("type_v3", ToString(*columnType))
+ << TErrorAttribute("required", protoSchema.required());
+ }
+ if (protoSchema.has_logical_type() && v1Type != NYT::FromProto<ESimpleLogicalValueType>(protoSchema.logical_type())) {
+ THROW_ERROR_EXCEPTION("Fields \"type_v3\" and \"logical_type\" do not match")
+ << TErrorAttribute("type_v3", ToString(*columnType))
+ << TErrorAttribute("logical_type", NYT::FromProto<ESimpleLogicalValueType>(protoSchema.logical_type()));
+ }
+ if (protoSchema.has_type() && GetPhysicalType(v1Type) != physicalType) {
+ THROW_ERROR_EXCEPTION("Fields \"type_v3\" and \"logical_type\" do not match")
+ << TErrorAttribute("type_v3", ToString(*columnType))
+ << TErrorAttribute("type", protoSchema.type());
+ }
+ } else if (protoSchema.has_logical_type()) {
+ auto logicalType = CheckedEnumCast<ESimpleLogicalValueType>(protoSchema.logical_type());
+ columnType = MakeLogicalType(logicalType, protoSchema.required());
+ if (protoSchema.has_type() && GetPhysicalType(logicalType) != physicalType) {
+ THROW_ERROR_EXCEPTION("Fields \"logical_type\" and \"type\" do not match")
+ << TErrorAttribute("logical_type", ToString(*columnType))
+ << TErrorAttribute("type", protoSchema.type());
+ }
+ } else if (protoSchema.has_type()) {
+ columnType = MakeLogicalType(GetLogicalType(physicalType), protoSchema.required());
+ }
+
+ if (!columnType) {
+ THROW_ERROR_EXCEPTION("Type is not specified");
+ }
+
+ schema->SetLogicalType(std::move(columnType));
+ schema->SetLock(protoSchema.has_lock() ? std::make_optional(protoSchema.lock()) : std::nullopt);
+ schema->SetExpression(protoSchema.has_expression() ? std::make_optional(protoSchema.expression()) : std::nullopt);
+ schema->SetAggregate(protoSchema.has_aggregate() ? std::make_optional(protoSchema.aggregate()) : std::nullopt);
+ schema->SetSortOrder(protoSchema.has_sort_order() ? std::make_optional(ESortOrder(protoSchema.sort_order())) : std::nullopt);
+ schema->SetGroup(protoSchema.has_group() ? std::make_optional(protoSchema.group()) : std::nullopt);
+ schema->SetMaxInlineHunkSize(protoSchema.has_max_inline_hunk_size() ? std::make_optional(protoSchema.max_inline_hunk_size()) : std::nullopt);
+}
+
+void ToProto(NProto::TTableSchema* protoSchema, const NTableClient::TTableSchema& schema)
+{
+ using NYT::ToProto;
+
+ ToProto(protoSchema->mutable_columns(), schema.Columns());
+ protoSchema->set_strict(schema.GetStrict());
+ protoSchema->set_unique_keys(schema.GetUniqueKeys());
+}
+
+void FromProto(NTableClient::TTableSchema* schema, const NProto::TTableSchema& protoSchema)
+{
+ using NYT::FromProto;
+
+ *schema = NTableClient::TTableSchema(
+ FromProto<std::vector<NTableClient::TColumnSchema>>(protoSchema.columns()),
+ protoSchema.strict(),
+ protoSchema.unique_keys());
+}
+
+void ToProto(NProto::TTableSchema* protoSchema, const NTableClient::TTableSchemaPtr& schema)
+{
+ ToProto(protoSchema, *schema);
+}
+
+void FromProto(NTableClient::TTableSchemaPtr* schema, const NProto::TTableSchema& protoSchema)
+{
+ *schema = New<NTableClient::TTableSchema>();
+ FromProto(schema->Get(), protoSchema);
+}
+
+void ToProto(NProto::TTabletInfo* protoTabletInfo, const NTabletClient::TTabletInfo& tabletInfo)
+{
+ ToProto(protoTabletInfo->mutable_tablet_id(), tabletInfo.TabletId);
+ protoTabletInfo->set_mount_revision(tabletInfo.MountRevision);
+ protoTabletInfo->set_state(static_cast<i32>(tabletInfo.State));
+ ToProto(protoTabletInfo->mutable_pivot_key(), tabletInfo.PivotKey);
+ if (tabletInfo.CellId) {
+ ToProto(protoTabletInfo->mutable_cell_id(), tabletInfo.CellId);
+ }
+}
+
+void FromProto(NTabletClient::TTabletInfo* tabletInfo, const NProto::TTabletInfo& protoTabletInfo)
+{
+ using NYT::FromProto;
+
+ tabletInfo->TabletId =
+ FromProto<TTabletId>(protoTabletInfo.tablet_id());
+ tabletInfo->MountRevision = protoTabletInfo.mount_revision();
+ tabletInfo->State = CheckedEnumCast<ETabletState>(protoTabletInfo.state());
+ tabletInfo->PivotKey = FromProto<NTableClient::TLegacyOwningKey>(protoTabletInfo.pivot_key());
+ if (protoTabletInfo.has_cell_id()) {
+ tabletInfo->CellId = FromProto<TTabletCellId>(protoTabletInfo.cell_id());
+ }
+}
+
+void ToProto(
+ NProto::TQueryStatistics* protoStatistics,
+ const NQueryClient::TQueryStatistics& statistics)
+{
+ protoStatistics->set_rows_read(statistics.RowsRead);
+ protoStatistics->set_data_weight_read(statistics.DataWeightRead);
+ protoStatistics->set_rows_written(statistics.RowsWritten);
+ protoStatistics->set_sync_time(statistics.SyncTime.GetValue());
+ protoStatistics->set_async_time(statistics.AsyncTime.GetValue());
+ protoStatistics->set_execute_time(statistics.ExecuteTime.GetValue());
+ protoStatistics->set_read_time(statistics.ReadTime.GetValue());
+ protoStatistics->set_write_time(statistics.WriteTime.GetValue());
+ protoStatistics->set_codegen_time(statistics.CodegenTime.GetValue());
+ protoStatistics->set_wait_on_ready_event_time(statistics.WaitOnReadyEventTime.GetValue());
+ protoStatistics->set_incomplete_input(statistics.IncompleteInput);
+ protoStatistics->set_incomplete_output(statistics.IncompleteOutput);
+ protoStatistics->set_memory_usage(statistics.MemoryUsage);
+
+ NYT::ToProto(protoStatistics->mutable_inner_statistics(), statistics.InnerStatistics);
+}
+
+void FromProto(
+ NQueryClient::TQueryStatistics* statistics,
+ const NProto::TQueryStatistics& protoStatistics)
+{
+ statistics->RowsRead = protoStatistics.rows_read();
+ statistics->DataWeightRead = protoStatistics.data_weight_read();
+ statistics->RowsWritten = protoStatistics.rows_written();
+ statistics->SyncTime = TDuration::FromValue(protoStatistics.sync_time());
+ statistics->AsyncTime = TDuration::FromValue(protoStatistics.async_time());
+ statistics->ExecuteTime = TDuration::FromValue(protoStatistics.execute_time());
+ statistics->ReadTime = TDuration::FromValue(protoStatistics.read_time());
+ statistics->WriteTime = TDuration::FromValue(protoStatistics.write_time());
+ statistics->CodegenTime = TDuration::FromValue(protoStatistics.codegen_time());
+ statistics->WaitOnReadyEventTime = TDuration::FromValue(protoStatistics.wait_on_ready_event_time());
+ statistics->IncompleteInput = protoStatistics.incomplete_input();
+ statistics->IncompleteOutput = protoStatistics.incomplete_output();
+ statistics->MemoryUsage = protoStatistics.memory_usage();
+
+ NYT::FromProto(&statistics->InnerStatistics, protoStatistics.inner_statistics());
+}
+
+void ToProto(NProto::TOperation* protoOperation, const NApi::TOperation& operation)
+{
+ protoOperation->Clear();
+
+ if (operation.Id) {
+ ToProto(protoOperation->mutable_id(), *operation.Id);
+ }
+ if (operation.Type) {
+ protoOperation->set_type(ConvertOperationTypeToProto(*operation.Type));
+ }
+ if (operation.State) {
+ protoOperation->set_state(ConvertOperationStateToProto(*operation.State));
+ }
+
+ if (operation.StartTime) {
+ protoOperation->set_start_time(NYT::ToProto<i64>(*operation.StartTime));
+ }
+ if (operation.FinishTime) {
+ protoOperation->set_finish_time(NYT::ToProto<i64>(*operation.FinishTime));
+ }
+
+ if (operation.AuthenticatedUser) {
+ protoOperation->set_authenticated_user(*operation.AuthenticatedUser);
+ }
+
+ if (operation.BriefSpec) {
+ protoOperation->set_brief_spec(operation.BriefSpec.ToString());
+ }
+ if (operation.Spec) {
+ protoOperation->set_spec(operation.Spec.ToString());
+ }
+ if (operation.ProvidedSpec) {
+ protoOperation->set_provided_spec(operation.ProvidedSpec.ToString());
+ }
+ if (operation.ExperimentAssignments) {
+ protoOperation->set_experiment_assignments(operation.ExperimentAssignments.ToString());
+ }
+ if (operation.ExperimentAssignmentNames) {
+ protoOperation->set_experiment_assignment_names(operation.ExperimentAssignmentNames.ToString());
+ }
+ if (operation.FullSpec) {
+ protoOperation->set_full_spec(operation.FullSpec.ToString());
+ }
+ if (operation.UnrecognizedSpec) {
+ protoOperation->set_unrecognized_spec(operation.UnrecognizedSpec.ToString());
+ }
+
+ if (operation.BriefProgress) {
+ protoOperation->set_brief_progress(operation.BriefProgress.ToString());
+ }
+ if (operation.Progress) {
+ protoOperation->set_progress(operation.Progress.ToString());
+ }
+
+ if (operation.RuntimeParameters) {
+ protoOperation->set_runtime_parameters(operation.RuntimeParameters.ToString());
+ }
+
+ if (operation.Suspended) {
+ protoOperation->set_suspended(*operation.Suspended);
+ }
+
+ if (operation.Events) {
+ protoOperation->set_events(operation.Events.ToString());
+ }
+ if (operation.Result) {
+ protoOperation->set_result(operation.Result.ToString());
+ }
+
+ if (operation.SlotIndexPerPoolTree) {
+ protoOperation->set_slot_index_per_pool_tree(operation.SlotIndexPerPoolTree.ToString());
+ }
+
+ if (operation.TaskNames) {
+ protoOperation->set_task_names(operation.TaskNames.ToString());
+ }
+
+ if (operation.Alerts) {
+ protoOperation->set_alerts(operation.Alerts.ToString());
+ }
+ if (operation.AlertEvents) {
+ protoOperation->set_alert_events(operation.AlertEvents.ToString());
+ }
+
+ if (operation.ControllerFeatures) {
+ protoOperation->set_controller_features(operation.ControllerFeatures.ToString());
+ }
+
+ if (operation.OtherAttributes) {
+ protoOperation->set_other_attributes(ConvertToYsonString(operation.OtherAttributes).ToString());
+ }
+}
+
+void FromProto(NApi::TOperation* operation, const NProto::TOperation& protoOperation)
+{
+ if (protoOperation.has_id()) {
+ operation->Id = NYT::FromProto<NScheduler::TOperationId>(protoOperation.id());
+ } else {
+ operation->Id.reset();
+ }
+ if (protoOperation.has_type()) {
+ operation->Type = ConvertOperationTypeFromProto(protoOperation.type());
+ } else {
+ operation->Type.reset();
+ }
+ if (protoOperation.has_state()) {
+ operation->State = ConvertOperationStateFromProto(protoOperation.state());
+ } else {
+ operation->State.reset();
+ }
+
+ if (protoOperation.has_start_time()) {
+ operation->StartTime = TInstant::FromValue(protoOperation.start_time());
+ } else {
+ operation->StartTime.reset();
+ }
+ if (protoOperation.has_finish_time()) {
+ operation->FinishTime = TInstant::FromValue(protoOperation.finish_time());
+ } else {
+ operation->FinishTime.reset();
+ }
+
+ if (protoOperation.has_authenticated_user()) {
+ operation->AuthenticatedUser = protoOperation.authenticated_user();
+ } else {
+ operation->AuthenticatedUser.reset();
+ }
+
+ if (protoOperation.has_brief_spec()) {
+ operation->BriefSpec = TYsonString(protoOperation.brief_spec());
+ } else {
+ operation->BriefSpec = TYsonString();
+ }
+ if (protoOperation.has_spec()) {
+ operation->Spec = TYsonString(protoOperation.spec());
+ } else {
+ operation->Spec = TYsonString();
+ }
+ if (protoOperation.has_provided_spec()) {
+ operation->ProvidedSpec = TYsonString(protoOperation.provided_spec());
+ } else {
+ operation->ProvidedSpec = TYsonString();
+ }
+ if (protoOperation.has_full_spec()) {
+ operation->FullSpec = TYsonString(protoOperation.full_spec());
+ } else {
+ operation->FullSpec = TYsonString();
+ }
+ if (protoOperation.has_unrecognized_spec()) {
+ operation->UnrecognizedSpec = TYsonString(protoOperation.unrecognized_spec());
+ } else {
+ operation->UnrecognizedSpec = TYsonString();
+ }
+
+ if (protoOperation.has_experiment_assignments()) {
+ operation->ExperimentAssignments = TYsonString(protoOperation.experiment_assignments());
+ } else {
+ operation->ExperimentAssignments = TYsonString();
+ }
+ if (protoOperation.has_experiment_assignment_names()) {
+ operation->ExperimentAssignmentNames = TYsonString(protoOperation.experiment_assignment_names());
+ } else {
+ operation->ExperimentAssignmentNames = TYsonString();
+ }
+
+ if (protoOperation.has_brief_progress()) {
+ operation->BriefProgress = TYsonString(protoOperation.brief_progress());
+ } else {
+ operation->BriefProgress = TYsonString();
+ }
+ if (protoOperation.has_progress()) {
+ operation->Progress = TYsonString(protoOperation.progress());
+ } else {
+ operation->Progress = TYsonString();
+ }
+
+ if (protoOperation.has_runtime_parameters()) {
+ operation->RuntimeParameters = TYsonString(protoOperation.runtime_parameters());
+ } else {
+ operation->RuntimeParameters = TYsonString();
+ }
+
+ if (protoOperation.has_suspended()) {
+ operation->Suspended = protoOperation.suspended();
+ } else {
+ operation->Suspended.reset();
+ }
+
+ if (protoOperation.has_events()) {
+ operation->Events = TYsonString(protoOperation.events());
+ } else {
+ operation->Events = TYsonString();
+ }
+ if (protoOperation.has_result()) {
+ operation->Result = TYsonString(protoOperation.result());
+ } else {
+ operation->Result = TYsonString();
+ }
+
+ if (protoOperation.has_slot_index_per_pool_tree()) {
+ operation->SlotIndexPerPoolTree = TYsonString(protoOperation.slot_index_per_pool_tree());
+ } else {
+ operation->SlotIndexPerPoolTree = TYsonString();
+ }
+
+ if (protoOperation.has_task_names()) {
+ operation->TaskNames = TYsonString(protoOperation.task_names());
+ } else {
+ operation->TaskNames = TYsonString();
+ }
+
+ if (protoOperation.has_alerts()) {
+ operation->Alerts = TYsonString(protoOperation.alerts());
+ } else {
+ operation->Alerts = TYsonString();
+ }
+ if (protoOperation.has_alert_events()) {
+ operation->AlertEvents = TYsonString(protoOperation.alert_events());
+ } else {
+ operation->AlertEvents = TYsonString();
+ }
+
+ if (protoOperation.has_controller_features()) {
+ operation->ControllerFeatures = TYsonString(protoOperation.controller_features());
+ } else {
+ operation->ControllerFeatures = TYsonString();
+ }
+
+ if (protoOperation.has_other_attributes()) {
+ operation->OtherAttributes = ConvertToAttributes(TYsonStringBuf(protoOperation.other_attributes()));
+ } else {
+ operation->OtherAttributes = {};
+ }
+}
+
+void ToProto(NProto::TJob* protoJob, const NApi::TJob& job)
+{
+ protoJob->Clear();
+
+ if (job.Id) {
+ ToProto(protoJob->mutable_id(), job.Id);
+ }
+ if (job.OperationId) {
+ ToProto(protoJob->mutable_operation_id(), job.OperationId);
+ }
+ if (job.Type) {
+ protoJob->set_type(ConvertJobTypeToProto(*job.Type));
+ }
+ if (auto state = job.GetState()) {
+ protoJob->set_state(ConvertJobStateToProto(*state));
+ }
+ if (job.ControllerState) {
+ protoJob->set_controller_state(
+ ConvertJobStateToProto(*job.ControllerState));
+ }
+ if (job.ArchiveState) {
+ protoJob->set_archive_state(ConvertJobStateToProto(*job.ArchiveState));
+ }
+
+ if (job.StartTime) {
+ protoJob->set_start_time(NYT::ToProto<i64>(*job.StartTime));
+ }
+ if (job.FinishTime) {
+ protoJob->set_finish_time(NYT::ToProto<i64>(*job.FinishTime));
+ }
+
+ if (job.Address) {
+ protoJob->set_address(*job.Address);
+ }
+ if (job.Progress) {
+ protoJob->set_progress(*job.Progress);
+ }
+ if (job.StderrSize) {
+ protoJob->set_stderr_size(*job.StderrSize);
+ }
+ if (job.FailContextSize) {
+ protoJob->set_fail_context_size(*job.FailContextSize);
+ }
+ if (job.HasSpec) {
+ protoJob->set_has_spec(*job.HasSpec);
+ }
+
+ if (job.Error) {
+ protoJob->set_error(job.Error.ToString());
+ }
+ if (job.BriefStatistics) {
+ protoJob->set_brief_statistics(job.BriefStatistics.ToString());
+ }
+ if (job.InputPaths) {
+ protoJob->set_input_paths(job.InputPaths.ToString());
+ }
+ if (job.CoreInfos) {
+ protoJob->set_core_infos(job.CoreInfos.ToString());
+ }
+ if (job.JobCompetitionId) {
+ ToProto(protoJob->mutable_job_competition_id(), job.JobCompetitionId);
+ }
+ if (job.ProbingJobCompetitionId) {
+ ToProto(protoJob->mutable_probing_job_competition_id(), job.ProbingJobCompetitionId);
+ }
+ if (job.HasCompetitors) {
+ protoJob->set_has_competitors(*job.HasCompetitors);
+ }
+ if (job.HasProbingCompetitors) {
+ protoJob->set_has_probing_competitors(*job.HasProbingCompetitors);
+ }
+ if (job.IsStale) {
+ protoJob->set_is_stale(*job.IsStale);
+ }
+ if (job.ExecAttributes) {
+ protoJob->set_exec_attributes(job.ExecAttributes.ToString());
+ }
+ if (job.TaskName) {
+ protoJob->set_task_name(*job.TaskName);
+ }
+ if (job.PoolTree) {
+ protoJob->set_pool_tree(*job.PoolTree);
+ }
+ if (job.Pool) {
+ protoJob->set_pool(*job.Pool);
+ }
+ if (job.JobCookie) {
+ protoJob->set_job_cookie(*job.JobCookie);
+ }
+}
+
+void FromProto(NApi::TJob* job, const NProto::TJob& protoJob)
+{
+ if (protoJob.has_id()) {
+ FromProto(&job->Id, protoJob.id());
+ } else {
+ job->Id = {};
+ }
+ if (protoJob.has_operation_id()) {
+ FromProto(&job->OperationId, protoJob.operation_id());
+ } else {
+ job->OperationId = {};
+ }
+ if (protoJob.has_type()) {
+ job->Type = ConvertJobTypeFromProto(protoJob.type());
+ } else {
+ job->Type.reset();
+ }
+ if (protoJob.has_controller_state()) {
+ job->ControllerState = ConvertJobStateFromProto(protoJob.controller_state());
+ } else {
+ job->ControllerState.reset();
+ }
+ if (protoJob.has_archive_state()) {
+ job->ArchiveState = ConvertJobStateFromProto(protoJob.archive_state());
+ } else {
+ job->ArchiveState.reset();
+ }
+ if (protoJob.has_start_time()) {
+ job->StartTime = TInstant::FromValue(protoJob.start_time());
+ } else {
+ job->StartTime.reset();
+ }
+ if (protoJob.has_finish_time()) {
+ job->FinishTime = TInstant::FromValue(protoJob.finish_time());
+ } else {
+ job->FinishTime.reset();
+ }
+ if (protoJob.has_address()) {
+ job->Address = protoJob.address();
+ } else {
+ job->Address.reset();
+ }
+ if (protoJob.has_progress()) {
+ job->Progress = protoJob.progress();
+ } else {
+ job->Progress.reset();
+ }
+ if (protoJob.has_stderr_size()) {
+ job->StderrSize = protoJob.stderr_size();
+ } else {
+ job->StderrSize.reset();
+ }
+ if (protoJob.has_fail_context_size()) {
+ job->FailContextSize = protoJob.fail_context_size();
+ } else {
+ job->FailContextSize.reset();
+ }
+ if (protoJob.has_has_spec()) {
+ job->HasSpec = protoJob.has_spec();
+ } else {
+ job->HasSpec = false;
+ }
+ if (protoJob.has_error()) {
+ job->Error = TYsonString(protoJob.error());
+ } else {
+ job->Error = TYsonString();
+ }
+ if (protoJob.has_brief_statistics()) {
+ job->BriefStatistics = TYsonString(protoJob.brief_statistics());
+ } else {
+ job->BriefStatistics = TYsonString();
+ }
+ if (protoJob.has_input_paths()) {
+ job->InputPaths = TYsonString(protoJob.input_paths());
+ } else {
+ job->InputPaths = TYsonString();
+ }
+ if (protoJob.has_core_infos()) {
+ job->CoreInfos = TYsonString(protoJob.core_infos());
+ } else {
+ job->CoreInfos = TYsonString();
+ }
+ if (protoJob.has_job_competition_id()) {
+ FromProto(&job->JobCompetitionId, protoJob.job_competition_id());
+ } else {
+ job->JobCompetitionId = {};
+ }
+ if (protoJob.has_probing_job_competition_id()) {
+ FromProto(&job->ProbingJobCompetitionId, protoJob.probing_job_competition_id());
+ } else {
+ job->ProbingJobCompetitionId = {};
+ }
+ if (protoJob.has_has_competitors()) {
+ job->HasCompetitors = protoJob.has_competitors();
+ } else {
+ job->HasCompetitors = false;
+ }
+ if (protoJob.has_has_probing_competitors()) {
+ job->HasProbingCompetitors = protoJob.has_probing_competitors();
+ } else {
+ job->HasProbingCompetitors = false;
+ }
+ if (protoJob.has_is_stale()) {
+ job->IsStale = protoJob.is_stale();
+ } else {
+ job->IsStale.reset();
+ }
+ if (protoJob.has_exec_attributes()) {
+ job->ExecAttributes = TYsonString(protoJob.exec_attributes());
+ } else {
+ job->ExecAttributes = TYsonString();
+ }
+ if (protoJob.has_task_name()) {
+ job->TaskName = protoJob.task_name();
+ } else {
+ job->TaskName.reset();
+ }
+ if (protoJob.has_pool_tree()) {
+ job->PoolTree = protoJob.pool_tree();
+ } else {
+ job->PoolTree.reset();
+ }
+ if (protoJob.has_pool()) {
+ job->Pool = protoJob.pool();
+ } else {
+ job->Pool.reset();
+ }
+ if (protoJob.has_job_cookie()) {
+ job->JobCookie = protoJob.job_cookie();
+ } else {
+ job->JobCookie.reset();
+ }
+}
+
+void ToProto(
+ NProto::TListJobsStatistics* protoStatistics,
+ const NApi::TListJobsStatistics& statistics)
+{
+ protoStatistics->mutable_state_counts()->clear_entries();
+ for (const auto& state: TEnumTraits<NJobTrackerClient::EJobState>::GetDomainValues()) {
+ if (statistics.StateCounts[state] != 0) {
+ auto* newStateCount = protoStatistics->mutable_state_counts()->add_entries();
+ newStateCount->set_state(ConvertJobStateToProto(state));
+ newStateCount->set_count(statistics.StateCounts[state]);
+ }
+ }
+
+ protoStatistics->mutable_type_counts()->clear_entries();
+ for (const auto& type: TEnumTraits<NJobTrackerClient::EJobType>::GetDomainValues()) {
+ if (statistics.TypeCounts[type] != 0) {
+ auto* newTypeCount = protoStatistics->mutable_type_counts()->add_entries();
+ newTypeCount->set_type(ConvertJobTypeToProto(type));
+ newTypeCount->set_count(statistics.TypeCounts[type]);
+ }
+ }
+}
+
+void FromProto(
+ NApi::TListJobsStatistics* statistics,
+ const NProto::TListJobsStatistics& protoStatistics)
+{
+ std::fill(statistics->StateCounts.begin(), statistics->StateCounts.end(), 0);
+ for (const auto& stateCount: protoStatistics.state_counts().entries()) {
+ auto state = ConvertJobStateFromProto(stateCount.state());
+ YT_VERIFY(statistics->StateCounts.IsDomainValue(state));
+ YT_VERIFY(statistics->StateCounts[state] == 0);
+ statistics->StateCounts[state] = stateCount.count();
+ }
+
+ std::fill(statistics->TypeCounts.begin(), statistics->TypeCounts.end(), 0);
+ for (const auto& typeCount: protoStatistics.type_counts().entries()) {
+ auto type = ConvertJobTypeFromProto(typeCount.type());
+ YT_VERIFY(statistics->TypeCounts.IsDomainValue(type));
+ YT_VERIFY(statistics->TypeCounts[type] == 0);
+ statistics->TypeCounts[type] = typeCount.count();
+ }
+}
+
+void ToProto(
+ NProto::TFetchChunkSpecConfig* protoFetchChunkSpecConfig,
+ const NChunkClient::TFetchChunkSpecConfigPtr& fetchChunkSpecConfig)
+{
+ protoFetchChunkSpecConfig->set_max_chunk_per_fetch(
+ fetchChunkSpecConfig->MaxChunksPerFetch);
+ protoFetchChunkSpecConfig->set_max_chunk_per_locate_request(
+ fetchChunkSpecConfig->MaxChunksPerLocateRequest);
+}
+
+void FromProto(
+ const NChunkClient::TFetchChunkSpecConfigPtr& fetchChunkSpecConfig,
+ const NProto::TFetchChunkSpecConfig& protoFetchChunkSpecConfig)
+{
+ NYT::FromProto(
+ &fetchChunkSpecConfig->MaxChunksPerFetch,
+ protoFetchChunkSpecConfig.max_chunk_per_fetch());
+ NYT::FromProto(
+ &fetchChunkSpecConfig->MaxChunksPerLocateRequest,
+ protoFetchChunkSpecConfig.max_chunk_per_locate_request());
+}
+
+void ToProto(
+ NProto::TFetcherConfig* protoFetcherConfig,
+ const NChunkClient::TFetcherConfigPtr& fetcherConfig)
+{
+ protoFetcherConfig->set_node_rpc_timeout(
+ NYT::ToProto<i64>(fetcherConfig->NodeRpcTimeout));
+}
+
+void FromProto(
+ const NChunkClient::TFetcherConfigPtr& fetcherConfig,
+ const NProto::TFetcherConfig& protoFetcherConfig)
+{
+ fetcherConfig->NodeRpcTimeout = TDuration::FromValue(protoFetcherConfig.node_rpc_timeout());
+}
+
+void ToProto(
+ NProto::TColumnarStatistics* protoStatistics,
+ const NTableClient::TColumnarStatistics& statistics)
+{
+ protoStatistics->Clear();
+
+ NYT::ToProto(protoStatistics->mutable_column_data_weights(), statistics.ColumnDataWeights);
+ if (statistics.TimestampTotalWeight) {
+ protoStatistics->set_timestamp_total_weight(*statistics.TimestampTotalWeight);
+ }
+ protoStatistics->set_legacy_chunk_data_weight(statistics.LegacyChunkDataWeight);
+}
+
+void FromProto(
+ NTableClient::TColumnarStatistics* statistics,
+ const NProto::TColumnarStatistics& protoStatistics)
+{
+ NYT::FromProto(&statistics->ColumnDataWeights, protoStatistics.column_data_weights());
+ if (protoStatistics.has_timestamp_total_weight()) {
+ statistics->TimestampTotalWeight = protoStatistics.timestamp_total_weight();
+ } else {
+ statistics->TimestampTotalWeight.reset();
+ }
+ statistics->LegacyChunkDataWeight = protoStatistics.legacy_chunk_data_weight();
+}
+
+void ToProto(
+ NProto::TMultiTablePartition* protoMultiTablePartition,
+ const NApi::TMultiTablePartition& multiTablePartition)
+{
+ protoMultiTablePartition->Clear();
+
+ for (const auto& range : multiTablePartition.TableRanges) {
+ protoMultiTablePartition->add_table_ranges(ToString(range));
+ }
+
+ auto aggregateStatistics = protoMultiTablePartition->mutable_aggregate_statistics();
+ aggregateStatistics->set_chunk_count(multiTablePartition.AggregateStatistics.ChunkCount);
+ aggregateStatistics->set_data_weight(multiTablePartition.AggregateStatistics.DataWeight);
+ aggregateStatistics->set_row_count(multiTablePartition.AggregateStatistics.RowCount);
+}
+
+void FromProto(
+ NApi::TMultiTablePartition* multiTablePartition,
+ const NProto::TMultiTablePartition& protoMultiTablePartition)
+{
+ for (const auto& range : protoMultiTablePartition.table_ranges()) {
+ multiTablePartition->TableRanges.emplace_back(NYPath::TRichYPath::Parse(range));
+ }
+
+ if (protoMultiTablePartition.has_aggregate_statistics()) {
+ const auto& aggregateStatistics = protoMultiTablePartition.aggregate_statistics();
+ multiTablePartition->AggregateStatistics.ChunkCount = aggregateStatistics.chunk_count();
+ multiTablePartition->AggregateStatistics.DataWeight = aggregateStatistics.data_weight();
+ multiTablePartition->AggregateStatistics.RowCount = aggregateStatistics.row_count();
+ }
+}
+
+void FromProto(
+ NApi::TMultiTablePartitions* multiTablePartitions,
+ const NProto::TRspPartitionTables& protoRspPartitionTables)
+{
+ NYT::FromProto(
+ &multiTablePartitions->Partitions,
+ protoRspPartitionTables.partitions());
+}
+
+void ToProto(
+ NProto::TRowBatchReadOptions* proto,
+ const NQueueClient::TQueueRowBatchReadOptions& result)
+{
+ proto->set_max_row_count(result.MaxRowCount);
+ proto->set_max_data_weight(result.MaxDataWeight);
+ if (result.DataWeightPerRowHint) {
+ proto->set_data_weight_per_row_hint(*result.DataWeightPerRowHint);
+ }
+}
+
+void FromProto(
+ NQueueClient::TQueueRowBatchReadOptions* result,
+ const NProto::TRowBatchReadOptions& proto)
+{
+ result->MaxRowCount = proto.max_row_count();
+ result->MaxDataWeight = proto.max_data_weight();
+ if (proto.has_data_weight_per_row_hint()) {
+ result->DataWeightPerRowHint = proto.data_weight_per_row_hint();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ENUMS
+////////////////////////////////////////////////////////////////////////////////
+
+NProto::EOperationType ConvertOperationTypeToProto(
+ NScheduler::EOperationType operationType)
+{
+ switch (operationType) {
+ case NScheduler::EOperationType::Map:
+ return NProto::EOperationType::OT_MAP;
+ case NScheduler::EOperationType::Merge:
+ return NProto::EOperationType::OT_MERGE;
+ case NScheduler::EOperationType::Erase:
+ return NProto::EOperationType::OT_ERASE;
+ case NScheduler::EOperationType::Sort:
+ return NProto::EOperationType::OT_SORT;
+ case NScheduler::EOperationType::Reduce:
+ return NProto::EOperationType::OT_REDUCE;
+ case NScheduler::EOperationType::MapReduce:
+ return NProto::EOperationType::OT_MAP_REDUCE;
+ case NScheduler::EOperationType::RemoteCopy:
+ return NProto::EOperationType::OT_REMOTE_COPY;
+ case NScheduler::EOperationType::JoinReduce:
+ return NProto::EOperationType::OT_JOIN_REDUCE;
+ case NScheduler::EOperationType::Vanilla:
+ return NProto::EOperationType::OT_VANILLA;
+ }
+ YT_ABORT();
+}
+
+NScheduler::EOperationType ConvertOperationTypeFromProto(
+ NProto::EOperationType proto)
+{
+ switch (proto) {
+ case NProto::EOperationType::OT_MAP:
+ return NScheduler::EOperationType::Map;
+ case NProto::EOperationType::OT_MERGE:
+ return NScheduler::EOperationType::Merge;
+ case NProto::EOperationType::OT_ERASE:
+ return NScheduler::EOperationType::Erase;
+ case NProto::EOperationType::OT_SORT:
+ return NScheduler::EOperationType::Sort;
+ case NProto::EOperationType::OT_REDUCE:
+ return NScheduler::EOperationType::Reduce;
+ case NProto::EOperationType::OT_MAP_REDUCE:
+ return NScheduler::EOperationType::MapReduce;
+ case NProto::EOperationType::OT_REMOTE_COPY:
+ return NScheduler::EOperationType::RemoteCopy;
+ case NProto::EOperationType::OT_JOIN_REDUCE:
+ return NScheduler::EOperationType::JoinReduce;
+ case NProto::EOperationType::OT_VANILLA:
+ return NScheduler::EOperationType::Vanilla;
+ case NProto::EOperationType::OT_UNKNOWN:
+ THROW_ERROR_EXCEPTION("Protobuf contains unknown value for operation type");
+ }
+ YT_ABORT();
+}
+
+NProto::EOperationState ConvertOperationStateToProto(
+ NScheduler::EOperationState operationState)
+{
+ switch (operationState) {
+ case NScheduler::EOperationState::None:
+ return NProto::EOperationState::OS_NONE;
+ case NScheduler::EOperationState::Starting:
+ return NProto::EOperationState::OS_STARTING;
+ case NScheduler::EOperationState::Orphaned:
+ return NProto::EOperationState::OS_ORPHANED;
+ case NScheduler::EOperationState::WaitingForAgent:
+ return NProto::EOperationState::OS_WAITING_FOR_AGENT;
+ case NScheduler::EOperationState::Initializing:
+ return NProto::EOperationState::OS_INITIALIZING;
+ case NScheduler::EOperationState::Preparing:
+ return NProto::EOperationState::OS_PREPARING;
+ case NScheduler::EOperationState::Materializing:
+ return NProto::EOperationState::OS_MATERIALIZING;
+ case NScheduler::EOperationState::Reviving:
+ return NProto::EOperationState::OS_REVIVING;
+ case NScheduler::EOperationState::RevivingJobs:
+ return NProto::EOperationState::OS_REVIVING_JOBS;
+ case NScheduler::EOperationState::Pending:
+ return NProto::EOperationState::OS_PENDING;
+ case NScheduler::EOperationState::Running:
+ return NProto::EOperationState::OS_RUNNING;
+ case NScheduler::EOperationState::Completing:
+ return NProto::EOperationState::OS_COMPLETING;
+ case NScheduler::EOperationState::Completed:
+ return NProto::EOperationState::OS_COMPLETED;
+ case NScheduler::EOperationState::Aborting:
+ return NProto::EOperationState::OS_ABORTING;
+ case NScheduler::EOperationState::Aborted:
+ return NProto::EOperationState::OS_ABORTED;
+ case NScheduler::EOperationState::Failing:
+ return NProto::EOperationState::OS_FAILING;
+ case NScheduler::EOperationState::Failed:
+ return NProto::EOperationState::OS_FAILED;
+ case NScheduler::EOperationState::ReviveInitializing:
+ return NProto::EOperationState::OS_REVIVE_INITIALIZING;
+ }
+ YT_ABORT();
+}
+
+NScheduler::EOperationState ConvertOperationStateFromProto(
+ NProto::EOperationState proto)
+{
+ switch (proto) {
+ case NProto::EOperationState::OS_NONE:
+ return NScheduler::EOperationState::None;
+ case NProto::EOperationState::OS_STARTING:
+ return NScheduler::EOperationState::Starting;
+ case NProto::EOperationState::OS_ORPHANED:
+ return NScheduler::EOperationState::Orphaned;
+ case NProto::EOperationState::OS_WAITING_FOR_AGENT:
+ return NScheduler::EOperationState::WaitingForAgent;
+ case NProto::EOperationState::OS_INITIALIZING:
+ return NScheduler::EOperationState::Initializing;
+ case NProto::EOperationState::OS_PREPARING:
+ return NScheduler::EOperationState::Preparing;
+ case NProto::EOperationState::OS_MATERIALIZING:
+ return NScheduler::EOperationState::Materializing;
+ case NProto::EOperationState::OS_REVIVING:
+ return NScheduler::EOperationState::Reviving;
+ case NProto::EOperationState::OS_REVIVING_JOBS:
+ return NScheduler::EOperationState::RevivingJobs;
+ case NProto::EOperationState::OS_PENDING:
+ return NScheduler::EOperationState::Pending;
+ case NProto::EOperationState::OS_RUNNING:
+ return NScheduler::EOperationState::Running;
+ case NProto::EOperationState::OS_COMPLETING:
+ return NScheduler::EOperationState::Completing;
+ case NProto::EOperationState::OS_COMPLETED:
+ return NScheduler::EOperationState::Completed;
+ case NProto::EOperationState::OS_ABORTING:
+ return NScheduler::EOperationState::Aborting;
+ case NProto::EOperationState::OS_ABORTED:
+ return NScheduler::EOperationState::Aborted;
+ case NProto::EOperationState::OS_FAILING:
+ return NScheduler::EOperationState::Failing;
+ case NProto::EOperationState::OS_FAILED:
+ return NScheduler::EOperationState::Failed;
+ case NProto::EOperationState::OS_REVIVE_INITIALIZING:
+ return NScheduler::EOperationState::ReviveInitializing;
+ case NProto::EOperationState::OS_UNKNOWN:
+ THROW_ERROR_EXCEPTION("Protobuf contains unknown value for operation state");
+ }
+ YT_ABORT();
+}
+
+NProto::EJobType ConvertJobTypeToProto(
+ NJobTrackerClient::EJobType jobType)
+{
+ switch (jobType) {
+ case NJobTrackerClient::EJobType::Map:
+ return NProto::EJobType::JT_MAP;
+ case NJobTrackerClient::EJobType::PartitionMap:
+ return NProto::EJobType::JT_PARTITION_MAP;
+ case NJobTrackerClient::EJobType::SortedMerge:
+ return NProto::EJobType::JT_SORTED_MERGE;
+ case NJobTrackerClient::EJobType::OrderedMerge:
+ return NProto::EJobType::JT_ORDERED_MERGE;
+ case NJobTrackerClient::EJobType::UnorderedMerge:
+ return NProto::EJobType::JT_UNORDERED_MERGE;
+ case NJobTrackerClient::EJobType::Partition:
+ return NProto::EJobType::JT_PARTITION;
+ case NJobTrackerClient::EJobType::SimpleSort:
+ return NProto::EJobType::JT_SIMPLE_SORT;
+ case NJobTrackerClient::EJobType::FinalSort:
+ return NProto::EJobType::JT_FINAL_SORT;
+ case NJobTrackerClient::EJobType::SortedReduce:
+ return NProto::EJobType::JT_SORTED_REDUCE;
+ case NJobTrackerClient::EJobType::PartitionReduce:
+ return NProto::EJobType::JT_PARTITION_REDUCE;
+ case NJobTrackerClient::EJobType::ReduceCombiner:
+ return NProto::EJobType::JT_REDUCE_COMBINER;
+ case NJobTrackerClient::EJobType::RemoteCopy:
+ return NProto::EJobType::JT_REMOTE_COPY;
+ case NJobTrackerClient::EJobType::IntermediateSort:
+ return NProto::EJobType::JT_INTERMEDIATE_SORT;
+ case NJobTrackerClient::EJobType::OrderedMap:
+ return NProto::EJobType::JT_ORDERED_MAP;
+ case NJobTrackerClient::EJobType::JoinReduce:
+ return NProto::EJobType::JT_JOIN_REDUCE;
+ case NJobTrackerClient::EJobType::Vanilla:
+ return NProto::EJobType::JT_VANILLA;
+ case NJobTrackerClient::EJobType::SchedulerUnknown:
+ return NProto::EJobType::JT_SCHEDULER_UNKNOWN;
+ case NJobTrackerClient::EJobType::ReplicateChunk:
+ return NProto::EJobType::JT_REPLICATE_CHUNK;
+ case NJobTrackerClient::EJobType::RemoveChunk:
+ return NProto::EJobType::JT_REMOVE_CHUNK;
+ case NJobTrackerClient::EJobType::RepairChunk:
+ return NProto::EJobType::JT_REPAIR_CHUNK;
+ case NJobTrackerClient::EJobType::SealChunk:
+ return NProto::EJobType::JT_SEAL_CHUNK;
+ case NJobTrackerClient::EJobType::MergeChunks:
+ return NProto::EJobType::JT_MERGE_CHUNKS;
+ case NJobTrackerClient::EJobType::AutotomizeChunk:
+ return NProto::EJobType::JT_AUTOTOMIZE_CHUNK;
+ case NJobTrackerClient::EJobType::ShallowMerge:
+ return NProto::EJobType::JT_SHALLOW_MERGE;
+ case NJobTrackerClient::EJobType::ReincarnateChunk:
+ return NProto::EJobType::JT_REINCARNATE_CHUNK;
+ }
+ YT_ABORT();
+}
+
+NJobTrackerClient::EJobType ConvertJobTypeFromProto(
+ NProto::EJobType proto)
+{
+ switch (proto) {
+ case NProto::EJobType::JT_MAP:
+ return NJobTrackerClient::EJobType::Map;
+ case NProto::EJobType::JT_PARTITION_MAP:
+ return NJobTrackerClient::EJobType::PartitionMap;
+ case NProto::EJobType::JT_SORTED_MERGE:
+ return NJobTrackerClient::EJobType::SortedMerge;
+ case NProto::EJobType::JT_ORDERED_MERGE:
+ return NJobTrackerClient::EJobType::OrderedMerge;
+ case NProto::EJobType::JT_UNORDERED_MERGE:
+ return NJobTrackerClient::EJobType::UnorderedMerge;
+ case NProto::EJobType::JT_PARTITION:
+ return NJobTrackerClient::EJobType::Partition;
+ case NProto::EJobType::JT_SIMPLE_SORT:
+ return NJobTrackerClient::EJobType::SimpleSort;
+ case NProto::EJobType::JT_FINAL_SORT:
+ return NJobTrackerClient::EJobType::FinalSort;
+ case NProto::EJobType::JT_SORTED_REDUCE:
+ return NJobTrackerClient::EJobType::SortedReduce;
+ case NProto::EJobType::JT_PARTITION_REDUCE:
+ return NJobTrackerClient::EJobType::PartitionReduce;
+ case NProto::EJobType::JT_REDUCE_COMBINER:
+ return NJobTrackerClient::EJobType::ReduceCombiner;
+ case NProto::EJobType::JT_REMOTE_COPY:
+ return NJobTrackerClient::EJobType::RemoteCopy;
+ case NProto::EJobType::JT_INTERMEDIATE_SORT:
+ return NJobTrackerClient::EJobType::IntermediateSort;
+ case NProto::EJobType::JT_ORDERED_MAP:
+ return NJobTrackerClient::EJobType::OrderedMap;
+ case NProto::EJobType::JT_JOIN_REDUCE:
+ return NJobTrackerClient::EJobType::JoinReduce;
+ case NProto::EJobType::JT_VANILLA:
+ return NJobTrackerClient::EJobType::Vanilla;
+ case NProto::EJobType::JT_SCHEDULER_UNKNOWN:
+ return NJobTrackerClient::EJobType::SchedulerUnknown;
+ case NProto::EJobType::JT_REPLICATE_CHUNK:
+ return NJobTrackerClient::EJobType::ReplicateChunk;
+ case NProto::EJobType::JT_REMOVE_CHUNK:
+ return NJobTrackerClient::EJobType::RemoveChunk;
+ case NProto::EJobType::JT_REPAIR_CHUNK:
+ return NJobTrackerClient::EJobType::RepairChunk;
+ case NProto::EJobType::JT_SEAL_CHUNK:
+ return NJobTrackerClient::EJobType::SealChunk;
+ case NProto::EJobType::JT_MERGE_CHUNKS:
+ return NJobTrackerClient::EJobType::MergeChunks;
+ case NProto::EJobType::JT_AUTOTOMIZE_CHUNK:
+ return NJobTrackerClient::EJobType::AutotomizeChunk;
+ case NProto::EJobType::JT_SHALLOW_MERGE:
+ return NJobTrackerClient::EJobType::ShallowMerge;
+ case NProto::EJobType::JT_REINCARNATE_CHUNK:
+ return NJobTrackerClient::EJobType::ReincarnateChunk;
+ case NProto::EJobType::JT_UNKNOWN:
+ THROW_ERROR_EXCEPTION("Protobuf contains unknown value for job type");
+ }
+ YT_ABORT();
+}
+
+NProto::EJobState ConvertJobStateToProto(
+ NJobTrackerClient::EJobState jobState)
+{
+ switch (jobState) {
+ case NJobTrackerClient::EJobState::Waiting:
+ return NProto::EJobState::JS_WAITING;
+ case NJobTrackerClient::EJobState::Running:
+ return NProto::EJobState::JS_RUNNING;
+ case NJobTrackerClient::EJobState::Aborting:
+ return NProto::EJobState::JS_ABORTING;
+ case NJobTrackerClient::EJobState::Completed:
+ return NProto::EJobState::JS_COMPLETED;
+ case NJobTrackerClient::EJobState::Failed:
+ return NProto::EJobState::JS_FAILED;
+ case NJobTrackerClient::EJobState::Aborted:
+ return NProto::EJobState::JS_ABORTED;
+ case NJobTrackerClient::EJobState::Lost:
+ return NProto::EJobState::JS_LOST;
+ case NJobTrackerClient::EJobState::None:
+ return NProto::EJobState::JS_NONE;
+ }
+ YT_ABORT();
+}
+
+NJobTrackerClient::EJobState ConvertJobStateFromProto(
+ NProto::EJobState proto)
+{
+ switch (proto) {
+ case NProto::EJobState::JS_WAITING:
+ return NJobTrackerClient::EJobState::Waiting;
+ case NProto::EJobState::JS_RUNNING:
+ return NJobTrackerClient::EJobState::Running;
+ case NProto::EJobState::JS_ABORTING:
+ return NJobTrackerClient::EJobState::Aborting;
+ case NProto::EJobState::JS_COMPLETED:
+ return NJobTrackerClient::EJobState::Completed;
+ case NProto::EJobState::JS_FAILED:
+ return NJobTrackerClient::EJobState::Failed;
+ case NProto::EJobState::JS_ABORTED:
+ return NJobTrackerClient::EJobState::Aborted;
+ case NProto::EJobState::JS_LOST:
+ return NJobTrackerClient::EJobState::Lost;
+ case NProto::EJobState::JS_NONE:
+ return NJobTrackerClient::EJobState::None;
+ case NProto::EJobState::JS_UNKNOWN:
+ THROW_ERROR_EXCEPTION("Protobuf contains unknown value for job state");
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsRetriableError(const TError& error, bool retryProxyBanned)
+{
+ if (error.FindMatching(NRpcProxy::EErrorCode::ProxyBanned) ||
+ error.FindMatching(NRpc::EErrorCode::PeerBanned))
+ {
+ return retryProxyBanned;
+ }
+
+ //! Retriable error codes are based on the ones used in http client.
+ return
+ error.FindMatching(NRpc::EErrorCode::RequestQueueSizeLimitExceeded) ||
+ error.FindMatching(NRpc::EErrorCode::TransportError) ||
+ error.FindMatching(NRpc::EErrorCode::Unavailable) ||
+ error.FindMatching(NRpc::EErrorCode::TransientFailure) ||
+ error.FindMatching(NSecurityClient::EErrorCode::RequestQueueSizeLimitExceeded);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetTimeoutOptions(
+ NRpc::TClientRequest& request,
+ const TTimeoutOptions& options)
+{
+ request.SetTimeout(options.Timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ROWSETS
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+struct TRowsetTraits;
+
+template <>
+struct TRowsetTraits<TUnversionedRow>
+{
+ static constexpr NProto::ERowsetKind Kind = NProto::RK_UNVERSIONED;
+};
+
+template <>
+struct TRowsetTraits<TVersionedRow>
+{
+ static constexpr NProto::ERowsetKind Kind = NProto::RK_VERSIONED;
+};
+
+void ValidateRowsetDescriptor(
+ const NProto::TRowsetDescriptor& descriptor,
+ int expectedVersion,
+ NProto::ERowsetKind expectedKind,
+ NProto::ERowsetFormat expectedFormat)
+{
+ if (descriptor.wire_format_version() != expectedVersion) {
+ THROW_ERROR_EXCEPTION(
+ "Incompatible rowset wire format version: expected %v, got %v",
+ expectedVersion,
+ descriptor.wire_format_version());
+ }
+
+ if (descriptor.rowset_kind() != expectedKind) {
+ THROW_ERROR_EXCEPTION(
+ "Incompatible rowset kind: expected %Qv, got %Qv",
+ NProto::ERowsetKind_Name(expectedKind),
+ NProto::ERowsetKind_Name(descriptor.rowset_kind()));
+ }
+ if (descriptor.rowset_format() != expectedFormat) {
+ THROW_ERROR_EXCEPTION(
+ "Incompatible rowset format: expected %Qv, got %Qv",
+ NProto::ERowsetFormat_Name(expectedFormat),
+ NProto::ERowsetFormat_Name(descriptor.rowset_format()));
+ }
+}
+
+std::vector<TSharedRef> SerializeRowset(
+ const NTableClient::TNameTablePtr& nameTable,
+ TRange<NTableClient::TUnversionedRow> rows,
+ NProto::TRowsetDescriptor* descriptor)
+{
+ descriptor->Clear();
+ descriptor->set_wire_format_version(NApi::NRpcProxy::CurrentWireFormatVersion);
+ descriptor->set_rowset_kind(NProto::RK_UNVERSIONED);
+ for (int id = 0; id < nameTable->GetSize(); ++id) {
+ auto* entry = descriptor->add_name_table_entries();
+ entry->set_name(TString(nameTable->GetName(id)));
+ }
+
+ auto writer = CreateWireProtocolWriter();
+ writer->WriteUnversionedRowset(rows);
+ return writer->Finish();
+}
+
+template <class TRow>
+std::vector<TSharedRef> SerializeRowset(
+ const TTableSchema& schema,
+ TRange<TRow> rows,
+ NProto::TRowsetDescriptor* descriptor)
+{
+ descriptor->Clear();
+ descriptor->set_wire_format_version(NApi::NRpcProxy::CurrentWireFormatVersion);
+ descriptor->set_rowset_kind(TRowsetTraits<TRow>::Kind);
+ ToProto(descriptor->mutable_schema(), schema);
+
+ // COMPAT(babenko)
+ for (const auto& column : schema.Columns()) {
+ auto* entry = descriptor->add_name_table_entries();
+ entry->set_name(column.Name());
+ // we save physical type for backward compatibility
+ // COMPAT(babenko)
+ entry->set_type(ToProto<int>(column.GetWireType()));
+ // COMPAT(babenko)
+ entry->set_logical_type(ToProto<int>(column.CastToV1Type()));
+ }
+
+ auto writer = CreateWireProtocolWriter();
+ writer->WriteRowset(rows);
+ return writer->Finish();
+}
+
+// Instantiate templates.
+template std::vector<TSharedRef> SerializeRowset(
+ const TTableSchema& schema,
+ TRange<TUnversionedRow> rows,
+ NProto::TRowsetDescriptor* descriptor);
+template std::vector<TSharedRef> SerializeRowset(
+ const TTableSchema& schema,
+ TRange<TVersionedRow> rows,
+ NProto::TRowsetDescriptor* descriptor);
+
+TTableSchemaPtr DeserializeRowsetSchema(
+ const NProto::TRowsetDescriptor& descriptor)
+{
+ if (descriptor.has_schema()) {
+ return NYT::FromProto<TTableSchemaPtr>(descriptor.schema());
+ }
+
+ // COMPAT(babenko)
+ std::vector<TColumnSchema> columns;
+ columns.resize(descriptor.name_table_entries_size());
+ for (int i = 0; i < descriptor.name_table_entries_size(); ++i) {
+ const auto& entry = descriptor.name_table_entries(i);
+ if (entry.has_name()) {
+ columns[i].SetName(entry.name());
+ columns[i].SetStableName(TStableName(entry.name()));
+ }
+ if (entry.has_logical_type()) {
+ auto simpleLogicalType = CheckedEnumCast<NTableClient::ESimpleLogicalValueType>(entry.logical_type());
+ columns[i].SetLogicalType(OptionalLogicalType(SimpleLogicalType(simpleLogicalType)));
+ } else if (entry.has_type()) {
+ auto simpleLogicalType = CheckedEnumCast<NTableClient::ESimpleLogicalValueType>(entry.type());
+ columns[i].SetLogicalType(OptionalLogicalType(SimpleLogicalType(simpleLogicalType)));
+ }
+ }
+
+ auto schema = New<TTableSchema>(std::move(columns));
+ ValidateColumnUniqueness(*schema);
+ return schema;
+}
+
+namespace {
+
+template <class TRow>
+auto ReadRows(IWireProtocolReader* reader, const TTableSchema& schema);
+
+template <>
+auto ReadRows<TUnversionedRow>(IWireProtocolReader* reader, const TTableSchema& /*schema*/)
+{
+ return reader->ReadUnversionedRowset(true);
+}
+
+template <>
+auto ReadRows<TVersionedRow>(IWireProtocolReader* reader, const TTableSchema& schema)
+{
+ auto schemaData = IWireProtocolReader::GetSchemaData(schema, TColumnFilter());
+ return reader->ReadVersionedRowset(schemaData, true);
+}
+
+} // namespace
+
+template <class TRow>
+TIntrusivePtr<NApi::IRowset<TRow>> DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data)
+{
+ if (descriptor.rowset_format() != NApi::NRpcProxy::NProto::RF_YT_WIRE) {
+ THROW_ERROR_EXCEPTION("Unsupported rowset format %Qv",
+ NApi::NRpcProxy::NProto::ERowsetFormat_Name(descriptor.rowset_format()));
+ }
+
+ ValidateRowsetDescriptor(
+ descriptor,
+ NApi::NRpcProxy::CurrentWireFormatVersion,
+ TRowsetTraits<TRow>::Kind,
+ NApi::NRpcProxy::NProto::RF_YT_WIRE);
+
+ struct TDeserializedRowsetTag { };
+ auto reader = CreateWireProtocolReader(data, New<TRowBuffer>(TDeserializedRowsetTag()));
+
+ auto schema = DeserializeRowsetSchema(descriptor);
+ auto rows = ReadRows<TRow>(reader.get(), *schema);
+ return NApi::CreateRowset(std::move(schema), std::move(rows));
+}
+
+// Instantiate templates.
+template NApi::IUnversionedRowsetPtr DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data);
+template NApi::IVersionedRowsetPtr DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TSharedRef> SerializeRowset(
+ const NTableClient::TTableSchema& schema,
+ TRange<TTypeErasedRow> rows,
+ NProto::TRowsetDescriptor* descriptor,
+ bool versioned)
+{
+ if (versioned) {
+ return SerializeRowset(
+ schema,
+ ReinterpretCastRange<TVersionedRow>(rows),
+ descriptor);
+ } else {
+ return SerializeRowset(
+ schema,
+ ReinterpretCastRange<TUnversionedRow>(rows),
+ descriptor);
+ }
+}
+
+TIntrusivePtr<NApi::IRowset<TTypeErasedRow>> DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data,
+ bool versioned)
+{
+ if (versioned) {
+ auto rowset = DeserializeRowset<TVersionedRow>(descriptor, data);
+ // TODO(savrus): Get refcounted schema from rowset.
+ auto schema = DeserializeRowsetSchema(descriptor);
+ return CreateRowset(
+ std::move(schema),
+ ReinterpretCastRange<TTypeErasedRow>(rowset->GetRows()));
+ } else {
+ auto rowset = DeserializeRowset<TUnversionedRow>(descriptor, data);
+ // TODO(savrus): Get refcounted schema from rowset.
+ auto schema = DeserializeRowsetSchema(descriptor);
+ return CreateRowset(
+ std::move(schema),
+ ReinterpretCastRange<TTypeErasedRow>(rowset->GetRows()));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SortByRegexes(std::vector<TString>& values, const std::vector<NRe2::TRe2Ptr>& regexes)
+{
+ auto valueToRank = [&] (const TString& value) -> size_t {
+ for (size_t index = 0; index < regexes.size(); ++index) {
+ if (NRe2::TRe2::FullMatch(NRe2::StringPiece(value), *regexes[index])) {
+ return index;
+ }
+ }
+ return regexes.size();
+ };
+ std::stable_sort(values.begin(), values.end(), [&] (const auto& lhs, const auto& rhs) {
+ return valueToRank(lhs) < valueToRank(rhs);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/helpers.h b/yt/yt/client/api/rpc_proxy/helpers.h
new file mode 100644
index 0000000000..8c240cac5f
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/helpers.h
@@ -0,0 +1,280 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <yt/yt/library/re2/re2.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt_proto/yt/client/api/rpc_proxy/proto/api_service.pb.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetTimeoutOptions(
+ NRpc::TClientRequest& request,
+ const NApi::TTimeoutOptions& options);
+
+[[noreturn]] void ThrowUnimplemented(const TString& method);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+void ToProto(
+ NProto::TTransactionalOptions* proto,
+ const NApi::TTransactionalOptions& options);
+
+void ToProto(
+ NProto::TPrerequisiteOptions* proto,
+ const NApi::TPrerequisiteOptions& options);
+
+void ToProto(
+ NProto::TMasterReadOptions* proto,
+ const NApi::TMasterReadOptions& options);
+
+void ToProto(
+ NProto::TMutatingOptions* proto,
+ const NApi::TMutatingOptions& options);
+
+void ToProto(
+ NProto::TSuppressableAccessTrackingOptions* proto,
+ const NApi::TSuppressableAccessTrackingOptions& options);
+
+void ToProto(
+ NProto::TTabletRangeOptions* proto,
+ const NApi::TTabletRangeOptions& options);
+
+void ToProto(
+ NProto::TRetentionConfig* protoConfig,
+ const NTableClient::TRetentionConfig& config);
+
+void FromProto(
+ NTableClient::TRetentionConfig* config,
+ const NProto::TRetentionConfig& protoConfig);
+
+void ToProto(
+ NProto::TGetFileFromCacheResult* proto,
+ const NApi::TGetFileFromCacheResult& result);
+
+void FromProto(
+ NApi::TGetFileFromCacheResult* result,
+ const NProto::TGetFileFromCacheResult& proto);
+
+void ToProto(
+ NProto::TPutFileToCacheResult* proto,
+ const NApi::TPutFileToCacheResult& result);
+
+void FromProto(
+ NApi::TPutFileToCacheResult* result,
+ const NProto::TPutFileToCacheResult& proto);
+
+void ToProto(
+ NProto::TCheckPermissionResult* proto,
+ const NApi::TCheckPermissionResult& result);
+
+void FromProto(
+ NApi::TCheckPermissionResult* result,
+ const NProto::TCheckPermissionResult& proto);
+
+void ToProto(
+ NProto::TCheckPermissionByAclResult* proto,
+ const NApi::TCheckPermissionByAclResult& result);
+
+void FromProto(
+ NApi::TCheckPermissionByAclResult* result,
+ const NProto::TCheckPermissionByAclResult& proto);
+
+void ToProto(
+ NProto::TListOperationsResult* proto,
+ const NApi::TListOperationsResult& result);
+
+void FromProto(
+ NApi::TListOperationsResult* result,
+ const NProto::TListOperationsResult& proto);
+
+void ToProto(
+ NProto::TListJobsResult* proto,
+ const NApi::TListJobsResult& result);
+
+void FromProto(
+ NApi::TListJobsResult* result,
+ const NProto::TListJobsResult& proto);
+
+void ToProto(NProto::TColumnSchema* protoSchema, const NTableClient::TColumnSchema& schema);
+void FromProto(NTableClient::TColumnSchema* schema, const NProto::TColumnSchema& protoSchema);
+
+void ToProto(NProto::TTableSchema* protoSchema, const NTableClient::TTableSchema& schema);
+void FromProto(NTableClient::TTableSchema* schema, const NProto::TTableSchema& protoSchema);
+
+void ToProto(NProto::TTableSchema* protoSchema, const NTableClient::TTableSchemaPtr& schema);
+void FromProto(NTableClient::TTableSchemaPtr* schema, const NProto::TTableSchema& protoSchema);
+
+// Doesn't fill cell_config_version.
+void ToProto(
+ NProto::TTabletInfo* protoTabletInfo,
+ const NTabletClient::TTabletInfo& tabletInfo);
+// Doesn't fill TableId, UpdateTime and Owners.
+void FromProto(
+ NTabletClient::TTabletInfo* tabletInfo,
+ const NProto::TTabletInfo& protoTabletInfo);
+
+void ToProto(
+ NProto::TTabletReadOptions* protoOptions,
+ const NApi::TTabletReadOptionsBase& options);
+
+void ToProto(
+ NProto::TQueryStatistics* protoStatistics,
+ const NQueryClient::TQueryStatistics& statistics);
+
+void FromProto(
+ NQueryClient::TQueryStatistics* statistics,
+ const NProto::TQueryStatistics& protoStatistics);
+
+void ToProto(
+ NProto::TOperation* protoOperation,
+ const NApi::TOperation& operation);
+
+void FromProto(
+ NApi::TOperation* operation,
+ const NProto::TOperation& protoOperation);
+
+void ToProto(
+ NProto::TJob* protoJob,
+ const NApi::TJob& job);
+
+void FromProto(
+ NApi::TJob* job,
+ const NProto::TJob& protoJob);
+
+void ToProto(
+ NProto::TListJobsStatistics* protoStatistics,
+ const NApi::TListJobsStatistics& statistics);
+
+void FromProto(
+ NApi::TListJobsStatistics* statistics,
+ const NProto::TListJobsStatistics& protoStatistics);
+
+void ToProto(
+ NProto::TFetchChunkSpecConfig* protoFetchChunkSpecConfig,
+ const NChunkClient::TFetchChunkSpecConfigPtr& fetchChunkSpecConfig);
+
+void FromProto(
+ const NChunkClient::TFetchChunkSpecConfigPtr& fetchChunkSpecConfig,
+ const NProto::TFetchChunkSpecConfig& protoFetchChunkSpecConfig);
+
+void ToProto(
+ NProto::TFetcherConfig* protoFetcherConfig,
+ const NChunkClient::TFetcherConfigPtr& fetcherConfig);
+
+void FromProto(
+ const NChunkClient::TFetcherConfigPtr& fetcherConfig,
+ const NProto::TFetcherConfig& protoFetcherConfig);
+
+void ToProto(
+ NProto::TColumnarStatistics* protoStatistics,
+ const NTableClient::TColumnarStatistics& statistics);
+
+void FromProto(
+ NTableClient::TColumnarStatistics* statistics,
+ const NProto::TColumnarStatistics& protoStatistics);
+
+void ToProto(
+ NProto::TMultiTablePartition* protoMultiTablePartition,
+ const NApi::TMultiTablePartition& multiTablePartition);
+
+void FromProto(
+ NApi::TMultiTablePartition* multiTablePartition,
+ const NProto::TMultiTablePartition& protoMultiTablePartition);
+
+void FromProto(
+ NApi::TMultiTablePartitions* multiTablePartitions,
+ const NProto::TRspPartitionTables& protoRspPartitionTables);
+
+void ToProto(
+ NProto::TRowBatchReadOptions* proto,
+ const NQueueClient::TQueueRowBatchReadOptions& result);
+
+void FromProto(
+ NQueueClient::TQueueRowBatchReadOptions* result,
+ const NProto::TRowBatchReadOptions& proto);
+
+NProto::EOperationType ConvertOperationTypeToProto(
+ NScheduler::EOperationType operationType);
+
+NScheduler::EOperationType ConvertOperationTypeFromProto(
+ NProto::EOperationType proto);
+
+NProto::EOperationState ConvertOperationStateToProto(
+ NScheduler::EOperationState operationState);
+
+NScheduler::EOperationState ConvertOperationStateFromProto(
+ NProto::EOperationState proto);
+
+NProto::EJobType ConvertJobTypeToProto(
+ NJobTrackerClient::EJobType jobType);
+
+NJobTrackerClient::EJobType ConvertJobTypeFromProto(
+ NProto::EJobType proto);
+
+NProto::EJobState ConvertJobStateToProto(
+ NJobTrackerClient::EJobState jobState);
+
+NJobTrackerClient::EJobState ConvertJobStateFromProto(
+ NProto::EJobState proto);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsRetriableError(const TError& error, bool retryProxyBanned = true);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateRowsetDescriptor(
+ const NProto::TRowsetDescriptor& descriptor,
+ int expectedVersion,
+ NProto::ERowsetKind expectedKind,
+ NProto::ERowsetFormat expectedFormat);
+
+std::vector<TSharedRef> SerializeRowset(
+ const NTableClient::TNameTablePtr& nameTable,
+ TRange<NTableClient::TUnversionedRow> rows,
+ NProto::TRowsetDescriptor* descriptor);
+
+template <class TRow>
+std::vector<TSharedRef> SerializeRowset(
+ const NTableClient::TTableSchema& schema,
+ TRange<TRow> rows,
+ NProto::TRowsetDescriptor* descriptor);
+
+template <class TRow>
+TIntrusivePtr<NApi::IRowset<TRow>> DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data);
+
+std::vector<TSharedRef> SerializeRowset(
+ const NTableClient::TTableSchema& schema,
+ TRange<NTableClient::TTypeErasedRow> rows,
+ NProto::TRowsetDescriptor* descriptor,
+ bool versioned);
+
+TIntrusivePtr<NApi::IRowset<NTableClient::TTypeErasedRow>> DeserializeRowset(
+ const NProto::TRowsetDescriptor& descriptor,
+ const TSharedRef& data,
+ bool versioned);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Invokes std::stable_sort reordering addresses by the index of the first regex they match;
+//! addresses not matching any regex are placed at the very end.
+void SortByRegexes(std::vector<TString>& values, const std::vector<NRe2::TRe2Ptr>& regexes);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/journal_reader.cpp b/yt/yt/client/api/rpc_proxy/journal_reader.cpp
new file mode 100644
index 0000000000..b5f82d4126
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/journal_reader.cpp
@@ -0,0 +1,73 @@
+#include "journal_reader.h"
+
+#include <yt/yt/client/api/journal_reader.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalReader
+ : public IJournalReader
+{
+public:
+ explicit TJournalReader(
+ TApiServiceProxy::TReqReadJournalPtr request)
+ : Request_(std::move(request))
+ {
+ YT_VERIFY(Request_);
+ }
+
+ TFuture<void> Open() override
+ {
+ if (!OpenResult_) {
+ OpenResult_ = NRpc::CreateRpcClientInputStream(Request_)
+ .Apply(BIND([=, this, this_ = MakeStrong(this)] (const IAsyncZeroCopyInputStreamPtr& inputStream) {
+ Underlying_ = inputStream;
+ }));
+ }
+
+ return OpenResult_;
+ }
+
+ TFuture<std::vector<TSharedRef>> Read() override
+ {
+ ValidateOpened();
+
+ return Underlying_->Read().Apply(BIND ([] (const TSharedRef& packedRows) {
+ std::vector<TSharedRef> rows;
+ if (packedRows) {
+ UnpackRefs(packedRows, &rows);
+ }
+ return rows;
+ }));
+ }
+
+private:
+ const TApiServiceProxy::TReqReadJournalPtr Request_;
+
+ IAsyncZeroCopyInputStreamPtr Underlying_;
+ TFuture<void> OpenResult_;
+
+ void ValidateOpened()
+ {
+ if (!OpenResult_ || !OpenResult_.IsSet()) {
+ THROW_ERROR_EXCEPTION("Cannot read from an unopened journal reader");
+ }
+ OpenResult_.Get().ThrowOnError();
+ }
+};
+
+IJournalReaderPtr CreateJournalReader(
+ TApiServiceProxy::TReqReadJournalPtr request)
+{
+ return New<TJournalReader>(std::move(request));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/journal_reader.h b/yt/yt/client/api/rpc_proxy/journal_reader.h
new file mode 100644
index 0000000000..a90369ba62
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/journal_reader.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IJournalReaderPtr CreateJournalReader(
+ TApiServiceProxy::TReqReadJournalPtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/journal_writer.cpp b/yt/yt/client/api/rpc_proxy/journal_writer.cpp
new file mode 100644
index 0000000000..5b66ab93f7
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/journal_writer.cpp
@@ -0,0 +1,91 @@
+#include "journal_writer.h"
+
+#include <yt/yt/client/api/journal_writer.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJournalWriter
+ : public IJournalWriter
+{
+public:
+ TJournalWriter(
+ TApiServiceProxy::TReqWriteJournalPtr request)
+ : Request_(std::move(request))
+ {
+ YT_VERIFY(Request_);
+ }
+
+ TFuture<void> Open() override
+ {
+ ValidateNotClosed();
+
+ if (!OpenResult_) {
+ OpenResult_ = NRpc::CreateRpcClientOutputStream(Request_, true)
+ .Apply(BIND([=, this, this_ = MakeStrong(this)] (const IAsyncZeroCopyOutputStreamPtr& outputStream) {
+ Underlying_ = outputStream;
+ })).As<void>();
+ }
+
+ return OpenResult_;
+ }
+
+ TFuture<void> Write(TRange<TSharedRef> rows) override
+ {
+ ValidateOpened();
+ ValidateNotClosed();
+
+ if (rows.Empty()) {
+ return VoidFuture;
+ }
+
+ return Underlying_->Write(PackRefs(rows));
+ }
+
+ TFuture<void> Close() override
+ {
+ ValidateOpened();
+ ValidateNotClosed();
+
+ Closed_ = true;
+ return Underlying_->Close();
+ }
+
+private:
+ const TApiServiceProxy::TReqWriteJournalPtr Request_;
+
+ IAsyncZeroCopyOutputStreamPtr Underlying_;
+ TFuture<void> OpenResult_;
+ bool Closed_ = false;
+
+ void ValidateOpened()
+ {
+ if (!OpenResult_ || !OpenResult_.IsSet()) {
+ THROW_ERROR_EXCEPTION("Cannot write into an unopened journal writer");
+ }
+ OpenResult_.Get().ThrowOnError();
+ }
+
+ void ValidateNotClosed()
+ {
+ if (Closed_) {
+ THROW_ERROR_EXCEPTION("Journal writer is closed");
+ }
+ }
+};
+
+IJournalWriterPtr CreateJournalWriter(
+ TApiServiceProxy::TReqWriteJournalPtr request)
+{
+ return New<TJournalWriter>(std::move(request));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/journal_writer.h b/yt/yt/client/api/rpc_proxy/journal_writer.h
new file mode 100644
index 0000000000..cc9cf9dda5
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/journal_writer.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IJournalWriterPtr CreateJournalWriter(
+ TApiServiceProxy::TReqWriteJournalPtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/private.h b/yt/yt/client/api/rpc_proxy/private.h
new file mode 100644
index 0000000000..3d6a588d7c
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/private.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Parameters specific to sticky transactions.
+// The separate structure wrapped in |std::optional| helps to differentiate
+// between sticky and non-sticky transactions.
+struct TStickyTransactionParameters
+{
+ // Empty if not supported.
+ TString ProxyAddress;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TConnection)
+DECLARE_REFCOUNTED_CLASS(TClientBase)
+DECLARE_REFCOUNTED_CLASS(TClient)
+DECLARE_REFCOUNTED_CLASS(TTransaction)
+
+inline const NLogging::TLogger RpcProxyClientLogger("RpcProxyClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashMap<TString, TString> ParseProxyUrlAliasingRules(TString envConfig);
+void ApplyProxyUrlAliasingRules(TString& url);
+TString NormalizeHttpProxyUrl(TString url);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/protocol_version.h b/yt/yt/client/api/rpc_proxy/protocol_version.h
new file mode 100644
index 0000000000..8b75926232
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/protocol_version.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "protocol_version_variables.h"
diff --git a/yt/yt/client/api/rpc_proxy/protocol_version_variables.h.in b/yt/yt/client/api/rpc_proxy/protocol_version_variables.h.in
new file mode 100644
index 0000000000..f22e31d560
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/protocol_version_variables.h.in
@@ -0,0 +1,10 @@
+// We could inline this variables in .cpp file but `ymake' has disappointing bug:
+// bug https://st.yandex-team.ru/DEVTOOLS-4546.
+//
+// And compilation breaks if .cpp.in file includes any autogenerated file like .pb.h
+static constexpr int YTRpcProxyProtocolVersionMajor = @YT_RPC_PROXY_PROTOCOL_VERSION_MAJOR@;
+static constexpr int YTRpcProxyClientProtocolVersionMinor = @YT_RPC_PROXY_CLIENT_PROTOCOL_VERSION_MINOR@;
+static constexpr int YTRpcProxyServerProtocolVersionMinor = @YT_RPC_PROXY_SERVER_PROTOCOL_VERSION_MINOR@;
+
+// Feature versions
+static constexpr int YTRpcModifyRowsStrongLocksVersion = @YT_RPC_MODIFY_ROWS_STRONG_LOCKS_VERSION@;
diff --git a/yt/yt/client/api/rpc_proxy/public.cpp b/yt/yt/client/api/rpc_proxy/public.cpp
new file mode 100644
index 0000000000..8bc9c11a57
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/public.cpp
@@ -0,0 +1,12 @@
+#include "public.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString ApiServiceName = "ApiService";
+const TString DiscoveryServiceName = "DiscoveryService";
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/public.h b/yt/yt/client/api/rpc_proxy/public.h
new file mode 100644
index 0000000000..33f580a444
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/public.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <yt/yt/client/api/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConnectionOptions;
+
+DECLARE_REFCOUNTED_STRUCT(IRowStreamEncoder)
+DECLARE_REFCOUNTED_STRUCT(IRowStreamDecoder)
+
+DECLARE_REFCOUNTED_CLASS(TConnectionConfig)
+
+extern const TString ApiServiceName;
+extern const TString DiscoveryServiceName;
+
+constexpr int CurrentWireFormatVersion = 1;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// COMPAT(babenko): get rid of this in favor of NRpc::EErrorCode::PeerBanned
+YT_DEFINE_ERROR_ENUM(
+ ((ProxyBanned) (2100))
+);
+
+DEFINE_ENUM(ERpcProxyFeature,
+ ((GetInSyncWithoutKeys)(0))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EAddressType,
+ ((InternalRpc) (0))
+ ((MonitoringHttp) (1))
+ ((TvmOnlyInternalRpc) (2))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/row_stream.cpp b/yt/yt/client/api/rpc_proxy/row_stream.cpp
new file mode 100644
index 0000000000..4fac08b20a
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/row_stream.cpp
@@ -0,0 +1,138 @@
+#include "row_stream.h"
+
+#include <library/cpp/yt/memory/ref.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::tuple<TSharedRef, TMutableRef> SerializeRowStreamBlockEnvelope(
+ i64 payloadSize,
+ const NApi::NRpcProxy::NProto::TRowsetDescriptor& descriptor,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics* statistics)
+{
+ /*
+ Block with statistics:
+ count: i32 = 2
+ length-of-part-1: i64
+ part-1:
+ count: i32 = 2
+ length-of-part-1-1: i64
+ part-1-1: descriptor
+ length-of-part-1-2: i64
+ part-1-2: payload
+ length-of-part-2: i64
+ part-2: statistics
+
+ Block without statistics:
+ count: i32 = 2
+ length-of-part-1: i64
+ part-1: descriptor
+ length-of-part-2: i64
+ part-2: payload
+ */
+
+ i64 totalSize = 0;
+ totalSize += sizeof (i32); // partCount
+ totalSize += sizeof (i64) * 2; // partLength * 2
+ totalSize += descriptor.ByteSizeLong(); // descriptor
+ totalSize += payloadSize;
+ if (statistics) {
+ totalSize += sizeof (i32); // partCount
+ totalSize += sizeof (i64) * 2; // partLength * 2
+ totalSize += statistics->ByteSize();
+ }
+
+ struct TSerializedRowStreamBlockTag { };
+ auto block = TSharedMutableRef::Allocate<TSerializedRowStreamBlockTag>(totalSize, {.InitializeStorage = false});
+
+ char* current = block.Begin();
+
+ auto writeInt32 = [&] (i32 value) {
+ *reinterpret_cast<i32*>(current) = value;
+ current += sizeof (i32);
+ };
+
+ auto writeInt64 = [&] (i64 value) {
+ *reinterpret_cast<i64*>(current) = value;
+ current += sizeof (i64);
+ };
+
+ TMutableRef payloadRef;
+ auto skipPayload = [&] {
+ payloadRef = TMutableRef(current, current + payloadSize);
+ current += payloadSize;
+ };
+
+ auto writeProto = [&] (const auto& proto) {
+ current = reinterpret_cast<char*>(proto.SerializeWithCachedSizesToArray(reinterpret_cast<ui8*>(current)));
+ };
+
+ if (statistics) {
+ writeInt32(2); // partCount
+ writeInt64(sizeof (i32) + 2 * sizeof (i64) + descriptor.ByteSizeLong() + payloadSize); // partLength
+ }
+
+ writeInt32(2); // partCount
+ writeInt64(descriptor.ByteSizeLong()); // partLength
+ writeProto(descriptor);
+ writeInt64(payloadSize); // partLength
+ skipPayload();
+
+ if (statistics) {
+ writeInt64(statistics->ByteSize()); // partLength
+ writeProto(*statistics);
+ }
+
+ YT_VERIFY(current == block.End());
+
+ return std::make_tuple(std::move(block), payloadRef);
+}
+
+TSharedRef DeserializeRowStreamBlockEnvelope(
+ const TSharedRef& block,
+ NApi::NRpcProxy::NProto::TRowsetDescriptor* descriptor,
+ NApi::NRpcProxy::NProto::TRowsetStatistics* statistics)
+{
+ TSharedRef rowsRef;
+ if (statistics) {
+ auto parts = UnpackRefs(block);
+ if (parts.size() != 2) {
+ THROW_ERROR_EXCEPTION(
+ "Error deserializing row stream: expected %v packed refs, got %v",
+ 2,
+ parts.size());
+ }
+
+ rowsRef = parts[0];
+ const auto& statisticsRef = parts[1];
+ if (!TryDeserializeProto(statistics, statisticsRef)) {
+ THROW_ERROR_EXCEPTION("Error deserializing rowset statistics");
+ }
+ } else {
+ rowsRef = block;
+ }
+
+ auto parts = UnpackRefs(rowsRef);
+ if (parts.size() != 2) {
+ THROW_ERROR_EXCEPTION(
+ "Error deserializing row stream: expected %v packed refs, got %v",
+ 2,
+ parts.size());
+ }
+
+ const auto& descriptorRef = parts[0];
+ const auto& payloadRef = parts[1];
+
+ if (!TryDeserializeProto(descriptor, descriptorRef)) {
+ THROW_ERROR_EXCEPTION("Error deserializing rowset descriptor");
+ }
+
+ return payloadRef;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/row_stream.h b/yt/yt/client/api/rpc_proxy/row_stream.h
new file mode 100644
index 0000000000..6b3d449894
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/row_stream.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt_proto/yt/client/api/rpc_proxy/proto/api_service.pb.h>
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <library/cpp/yt/memory/ref.h>
+#include <yt/yt/core/misc/range.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IRowStreamEncoder
+ : public virtual TRefCounted
+{
+ virtual TSharedRef Encode(
+ const NTableClient::IUnversionedRowBatchPtr& batch,
+ const NProto::TRowsetStatistics* statistics) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRowStreamEncoder)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IRowStreamDecoder
+ : public virtual TRefCounted
+{
+ virtual NTableClient::IUnversionedRowBatchPtr Decode(
+ const TSharedRef& payloadRef,
+ const NProto::TRowsetDescriptor& descriptor) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRowStreamDecoder)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper for formatting row stream blocks.
+/*!
+ * Serializes #descriptor and (if given) #statistics.
+ *
+ * \returns a tuple consisting of
+ * 1) the whole row stream block
+ * 2) the ref where payload (of size #payloadSize) must be placed
+ */
+std::tuple<TSharedRef, TMutableRef> SerializeRowStreamBlockEnvelope(
+ i64 payloadSize,
+ const NApi::NRpcProxy::NProto::TRowsetDescriptor& descriptor,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics* statistics);
+
+//! A helper for parsing row stream blocks.
+/*!
+ * \returns the paylod slice
+ */
+TSharedRef DeserializeRowStreamBlockEnvelope(
+ const TSharedRef& block,
+ NApi::NRpcProxy::NProto::TRowsetDescriptor* descriptor,
+ NApi::NRpcProxy::NProto::TRowsetStatistics* statistics);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_mount_cache.cpp b/yt/yt/client/api/rpc_proxy/table_mount_cache.cpp
new file mode 100644
index 0000000000..1c540db44a
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_mount_cache.cpp
@@ -0,0 +1,138 @@
+#include "table_mount_cache.h"
+#include "api_service_proxy.h"
+#include "helpers.h"
+
+#include <yt/yt/client/api/config.h>
+
+#include <yt/yt/client/tablet_client/config.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache_detail.h>
+
+#include <yt/yt/client/table_client/helpers.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NRpc;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NYPath;
+
+using NYT::FromProto;
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TTableMountCache
+ : public TTableMountCacheBase
+{
+public:
+ TTableMountCache(
+ TTableMountCacheConfigPtr config,
+ IChannelPtr channel,
+ const NLogging::TLogger& logger,
+ TDuration timeout)
+ : TTableMountCacheBase(std::move(config), logger)
+ , Channel_(std::move(channel))
+ , Timeout_(timeout)
+ { }
+
+private:
+ TFuture<TTableMountInfoPtr> DoGet(const NYPath::TYPath& path, bool /*isPeriodicUpdate*/) noexcept override
+ {
+ YT_LOG_DEBUG("Requesting table mount info (Path: %v)",
+ path);
+
+ TApiServiceProxy proxy(Channel_);
+ proxy.SetDefaultTimeout(Timeout_);
+
+ auto req = proxy.GetTableMountInfo();
+ req->set_path(path);
+
+ return req->Invoke().Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TApiServiceProxy::TRspGetTableMountInfoPtr& rsp) {
+ auto tableInfo = New<TTableMountInfo>();
+ tableInfo->Path = path;
+ auto tableId = FromProto<NObjectClient::TObjectId>(rsp->table_id());
+ tableInfo->TableId = tableId;
+
+ auto primarySchema = NYT::FromProto<NTableClient::TTableSchemaPtr>(rsp->schema());
+ tableInfo->Schemas[ETableSchemaKind::Primary] = primarySchema;
+ tableInfo->Schemas[ETableSchemaKind::Write] = primarySchema->ToWrite();
+ tableInfo->Schemas[ETableSchemaKind::VersionedWrite] = primarySchema->ToVersionedWrite();
+ tableInfo->Schemas[ETableSchemaKind::Delete] = primarySchema->ToDelete();
+ tableInfo->Schemas[ETableSchemaKind::Query] = primarySchema->ToQuery();
+ tableInfo->Schemas[ETableSchemaKind::Lookup] = primarySchema->ToLookup();
+ tableInfo->Schemas[ETableSchemaKind::PrimaryWithTabletIndex] = primarySchema->WithTabletIndex();
+
+ tableInfo->UpstreamReplicaId = FromProto<TTableReplicaId>(rsp->upstream_replica_id());
+ tableInfo->Dynamic = rsp->dynamic();
+ tableInfo->NeedKeyEvaluation = primarySchema->HasComputedColumns();
+
+ for (const auto& protoTabletInfo : rsp->tablets()) {
+ auto tabletInfo = New<NTabletClient::TTabletInfo>();
+ FromProto(tabletInfo.Get(), protoTabletInfo);
+ tabletInfo->TableId = tableId;
+ tabletInfo->UpdateTime = Now();
+ tabletInfo->Owners.push_back(MakeWeak(tableInfo));
+
+ tabletInfo = TabletInfoCache_.Insert(std::move(tabletInfo));
+ tableInfo->Tablets.push_back(tabletInfo);
+ if (tabletInfo->State == ETabletState::Mounted) {
+ tableInfo->MountedTablets.push_back(tabletInfo);
+ }
+ }
+
+ for (const auto& protoReplicaInfo : rsp->replicas()) {
+ auto replicaInfo = New<TTableReplicaInfo>();
+ replicaInfo->ReplicaId = FromProto<TTableReplicaId>(protoReplicaInfo.replica_id());
+ replicaInfo->ClusterName = protoReplicaInfo.cluster_name();
+ replicaInfo->ReplicaPath = protoReplicaInfo.replica_path();
+ replicaInfo->Mode = ETableReplicaMode(protoReplicaInfo.mode());
+ tableInfo->Replicas.push_back(replicaInfo);
+ }
+
+ if (tableInfo->IsSorted()) {
+ tableInfo->LowerCapBound = MinKey();
+ tableInfo->UpperCapBound = MaxKey();
+ } else {
+ tableInfo->LowerCapBound = MakeUnversionedOwningRow(static_cast<int>(0));
+ tableInfo->UpperCapBound = MakeUnversionedOwningRow(static_cast<int>(tableInfo->Tablets.size()));
+ }
+
+ YT_LOG_DEBUG("Table mount info received (Path: %v, TableId: %v, TabletCount: %v, Dynamic: %v)",
+ path,
+ tableInfo->TableId,
+ tableInfo->Tablets.size(),
+ tableInfo->Dynamic);
+
+ return tableInfo;
+ }));
+ }
+
+private:
+ const IChannelPtr Channel_;
+ const TDuration Timeout_;
+
+ void InvalidateTable(const TTableMountInfoPtr& tableInfo) override
+ {
+ InvalidateValue(tableInfo->Path, tableInfo);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITableMountCachePtr CreateTableMountCache(
+ TTableMountCacheConfigPtr config,
+ IChannelPtr channel,
+ const NLogging::TLogger& logger,
+ TDuration timeout)
+{
+ return New<TTableMountCache>(
+ std::move(config),
+ std::move(channel),
+ logger,
+ timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_mount_cache.h b/yt/yt/client/api/rpc_proxy/table_mount_cache.h
new file mode 100644
index 0000000000..d312583fdd
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_mount_cache.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTabletClient::ITableMountCachePtr CreateTableMountCache(
+ TTableMountCacheConfigPtr config,
+ NRpc::IChannelPtr channel,
+ const NLogging::TLogger& logger,
+ TDuration timeout);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_reader.cpp b/yt/yt/client/api/rpc_proxy/table_reader.cpp
new file mode 100644
index 0000000000..2bb1410874
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_reader.cpp
@@ -0,0 +1,257 @@
+#include "table_reader.h"
+#include "helpers.h"
+#include "row_stream.h"
+#include "wire_row_stream.h"
+
+#include <yt/yt/client/api/rowset.h>
+#include <yt/yt/client/api/table_reader.h>
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/data_statistics.pb.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableReader
+ : public ITableReader
+{
+public:
+ TTableReader(
+ IAsyncZeroCopyInputStreamPtr underlying,
+ i64 startRowIndex,
+ const std::vector<TString>& omittedInaccessibleColumns,
+ TTableSchemaPtr schema,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics& statistics)
+ : Underlying_ (std::move(underlying))
+ , StartRowIndex_(startRowIndex)
+ , TableSchema_(std::move(schema))
+ , OmittedInaccessibleColumns_(omittedInaccessibleColumns)
+ , Decoder_(CreateWireRowStreamDecoder(NameTable_))
+ {
+ YT_VERIFY(Underlying_);
+
+ ApplyReaderStatistics(statistics);
+
+ RowsWithStatisticsFuture_ = GetRowsWithStatistics();
+ ReadyEvent_.TrySetFrom(RowsWithStatisticsFuture_);
+ }
+
+ i64 GetStartRowIndex() const override
+ {
+ return StartRowIndex_;
+ }
+
+ i64 GetTotalRowCount() const override
+ {
+ return TotalRowCount_;
+ }
+
+ NChunkClient::NProto::TDataStatistics GetDataStatistics() const override
+ {
+ auto dataStatistics = DataStatistics_;
+ dataStatistics.set_row_count(RowCount_);
+ dataStatistics.set_data_weight(DataWeight_);
+
+ return dataStatistics;
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return ReadyEvent_;
+ }
+
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& options) override
+ {
+ StoredRows_.clear();
+
+ if (!ReadyEvent_.IsSet() || !ReadyEvent_.Get().IsOK()) {
+ return CreateEmptyUnversionedRowBatch();
+ }
+
+ if (!Finished_) {
+ ReadyEvent_ = NewPromise<void>();
+ }
+
+ std::vector<TUnversionedRow> rows;
+ rows.reserve(options.MaxRowsPerRead);
+ i64 dataWeight = 0;
+
+ while (RowsWithStatisticsFuture_ &&
+ RowsWithStatisticsFuture_.IsSet() &&
+ RowsWithStatisticsFuture_.Get().IsOK() &&
+ !Finished_ &&
+ std::ssize(rows) < options.MaxRowsPerRead &&
+ dataWeight < options.MaxDataWeightPerRead)
+ {
+ const auto& currentRows = RowsWithStatisticsFuture_.Get().Value().Rows;
+ const auto& currentStatistics = RowsWithStatisticsFuture_.Get().Value().Statistics;
+
+ if (currentRows.Empty()) {
+ ReadyEvent_.Set();
+ Finished_ = true;
+ ApplyReaderStatistics(currentStatistics);
+ continue;
+ }
+
+ while (CurrentRowsOffset_ < std::ssize(currentRows) &&
+ std::ssize(rows) < options.MaxRowsPerRead &&
+ dataWeight < options.MaxDataWeightPerRead)
+ {
+ auto row = currentRows[CurrentRowsOffset_++];
+ rows.push_back(row);
+ dataWeight += GetDataWeight(row);
+ }
+
+ StoredRows_.push_back(currentRows);
+ ApplyReaderStatistics(currentStatistics);
+
+ if (CurrentRowsOffset_ == std::ssize(currentRows)) {
+ RowsWithStatisticsFuture_ = GetRowsWithStatistics();
+ CurrentRowsOffset_ = 0;
+ }
+ }
+
+ RowCount_ += rows.size();
+ DataWeight_ += dataWeight;
+
+ ReadyEvent_.TrySetFrom(RowsWithStatisticsFuture_);
+ return rows.empty()
+ ? nullptr
+ : CreateBatchFromUnversionedRows(MakeSharedRange(std::move(rows), MakeStrong(this)));
+ }
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return NameTable_;
+ }
+
+ const TTableSchemaPtr& GetTableSchema() const override
+ {
+ return TableSchema_;
+ }
+
+ const std::vector<TString>& GetOmittedInaccessibleColumns() const override
+ {
+ return OmittedInaccessibleColumns_;
+ }
+
+private:
+ struct TRowsWithStatistics
+ {
+ TSharedRange<TUnversionedRow> Rows;
+ NApi::NRpcProxy::NProto::TRowsetStatistics Statistics;
+ };
+
+ const IAsyncZeroCopyInputStreamPtr Underlying_;
+ const i64 StartRowIndex_;
+ const TTableSchemaPtr TableSchema_;
+ const std::vector<TString> OmittedInaccessibleColumns_;
+
+ const TNameTablePtr NameTable_ = New<TNameTable>();
+ const IRowStreamDecoderPtr Decoder_;
+
+ NChunkClient::NProto::TDataStatistics DataStatistics_;
+ i64 TotalRowCount_;
+
+ i64 RowCount_ = 0;
+ i64 DataWeight_ = 0;
+
+ TNameTableToSchemaIdMapping IdMapping_;
+
+ TPromise<void> ReadyEvent_ = NewPromise<void>();
+
+ std::vector<TSharedRange<TUnversionedRow>> StoredRows_;
+ TFuture<TRowsWithStatistics> RowsWithStatisticsFuture_;
+ i64 CurrentRowsOffset_ = 0;
+
+ bool Finished_ = false;
+
+ void ApplyReaderStatistics(const NApi::NRpcProxy::NProto::TRowsetStatistics& statistics)
+ {
+ TotalRowCount_ = statistics.total_row_count();
+ DataStatistics_ = statistics.data_statistics();
+ }
+
+ TFuture<TRowsWithStatistics> GetRowsWithStatistics()
+ {
+ return Underlying_->Read()
+ .Apply(BIND([this, weakThis = MakeWeak(this)] (const TSharedRef& block) {
+ auto this_ = weakThis.Lock();
+ if (!this_) {
+ THROW_ERROR_EXCEPTION(NYT::EErrorCode::Canceled, "Reader destroyed");
+ }
+
+ NApi::NRpcProxy::NProto::TRowsetDescriptor descriptor;
+ NApi::NRpcProxy::NProto::TRowsetStatistics statistics;
+ auto payloadRef = DeserializeRowStreamBlockEnvelope(block, &descriptor, &statistics);
+
+ ValidateRowsetDescriptor(
+ descriptor,
+ NApi::NRpcProxy::CurrentWireFormatVersion,
+ NApi::NRpcProxy::NProto::RK_UNVERSIONED,
+ NApi::NRpcProxy::NProto::ERowsetFormat::RF_YT_WIRE);
+
+ auto decoder = GetOrCreateDecoder(descriptor.rowset_format());
+ auto batch = decoder->Decode(payloadRef, descriptor);
+ auto rows = batch->MaterializeRows();
+ auto rowsWithStatistics = TRowsWithStatistics{
+ std::move(rows),
+ std::move(statistics)
+ };
+
+ if (rowsWithStatistics.Rows.Empty()) {
+ return ExpectEndOfStream(Underlying_).Apply(BIND([=] () {
+ return std::move(rowsWithStatistics);
+ }));
+ }
+ return MakeFuture(std::move(rowsWithStatistics));
+ }));
+ }
+
+ IRowStreamDecoderPtr GetOrCreateDecoder(NApi::NRpcProxy::NProto::ERowsetFormat format)
+ {
+ if (format != NApi::NRpcProxy::NProto::RF_YT_WIRE) {
+ THROW_ERROR_EXCEPTION("Unsupported rowset format %Qv",
+ NApi::NRpcProxy::NProto::ERowsetFormat_Name(format));
+ }
+ return Decoder_;
+ }
+};
+
+TFuture<ITableReaderPtr> CreateTableReader(TApiServiceProxy::TReqReadTablePtr request)
+{
+ return NRpc::CreateRpcClientInputStream(std::move(request))
+ .Apply(BIND([=] (const IAsyncZeroCopyInputStreamPtr& inputStream) {
+ return inputStream->Read().Apply(BIND([=] (const TSharedRef& metaRef) {
+ NApi::NRpcProxy::NProto::TRspReadTableMeta meta;
+ if (!TryDeserializeProto(&meta, metaRef)) {
+ THROW_ERROR_EXCEPTION("Failed to deserialize table reader meta information");
+ }
+
+ i64 startRowIndex = meta.start_row_index();
+ auto omittedInaccessibleColumns = FromProto<std::vector<TString>>(
+ meta.omitted_inaccessible_columns());
+ auto schema = NYT::FromProto<TTableSchemaPtr>(meta.schema());
+ return New<TTableReader>(
+ inputStream,
+ startRowIndex,
+ std::move(omittedInaccessibleColumns),
+ std::move(schema),
+ meta.statistics());
+ })).As<ITableReaderPtr>();
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_reader.h b/yt/yt/client/api/rpc_proxy/table_reader.h
new file mode 100644
index 0000000000..cc84ef2ab0
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_reader.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<ITableReaderPtr> CreateTableReader(
+ TApiServiceProxy::TReqReadTablePtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_writer.cpp b/yt/yt/client/api/rpc_proxy/table_writer.cpp
new file mode 100644
index 0000000000..40e696bf05
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_writer.cpp
@@ -0,0 +1,106 @@
+#include "table_writer.h"
+#include "helpers.h"
+#include "row_stream.h"
+#include "wire_row_stream.h"
+
+#include <yt/yt/client/api/table_writer.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/rpc/stream.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableWriter
+ : public ITableWriter
+{
+public:
+ TTableWriter(
+ IAsyncZeroCopyOutputStreamPtr underlying,
+ TTableSchemaPtr schema)
+ : Underlying_(std::move(underlying))
+ , Schema_(std::move(schema))
+ , Encoder_(CreateWireRowStreamEncoder(NameTable_))
+ {
+ YT_VERIFY(Underlying_);
+ NameTable_->SetEnableColumnNameValidation();
+ }
+
+ bool Write(TRange<TUnversionedRow> rows) override
+ {
+ YT_VERIFY(!Closed_);
+ YT_VERIFY(ReadyEvent_.IsSet() && ReadyEvent_.Get().IsOK());
+
+ auto batch = CreateBatchFromUnversionedRows(TSharedRange<TUnversionedRow>(rows, nullptr));
+
+ auto block = Encoder_->Encode(batch, nullptr);
+
+ ReadyEvent_ = NewPromise<void>();
+ ReadyEvent_.TrySetFrom(Underlying_->Write(std::move(block)));
+
+ return ReadyEvent_.IsSet() && ReadyEvent_.Get().IsOK();
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return ReadyEvent_;
+ }
+
+ TFuture<void> Close() override
+ {
+ YT_VERIFY(!Closed_);
+ Closed_ = true;
+
+ return Underlying_->Close();
+ }
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return NameTable_;
+ }
+
+ const TTableSchemaPtr& GetSchema() const override
+ {
+ return Schema_;
+ }
+
+private:
+ const IAsyncZeroCopyOutputStreamPtr Underlying_;
+ const TTableSchemaPtr Schema_;
+
+ const TNameTablePtr NameTable_ = New<TNameTable>();
+ const IRowStreamEncoderPtr Encoder_;
+
+ TPromise<void> ReadyEvent_ = MakePromise<void>(TError());
+ bool Closed_ = false;
+};
+
+TFuture<ITableWriterPtr> CreateTableWriter(
+ TApiServiceProxy::TReqWriteTablePtr request)
+{
+ auto schema = New<TTableSchema>();
+ return NRpc::CreateRpcClientOutputStream(
+ std::move(request),
+ BIND ([=] (const TSharedRef& metaRef) {
+ NApi::NRpcProxy::NProto::TWriteTableMeta meta;
+ if (!TryDeserializeProto(&meta, metaRef)) {
+ THROW_ERROR_EXCEPTION("Failed to deserialize schema for table writer");
+ }
+
+ FromProto(schema.Get(), meta.schema());
+ }))
+ .Apply(BIND([=] (const IAsyncZeroCopyOutputStreamPtr& outputStream) {
+ return New<TTableWriter>(outputStream, std::move(schema));
+ })).As<ITableWriterPtr>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/table_writer.h b/yt/yt/client/api/rpc_proxy/table_writer.h
new file mode 100644
index 0000000000..ce67e7ed0d
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/table_writer.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "api_service_proxy.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<ITableWriterPtr> CreateTableWriter(
+ TApiServiceProxy::TReqWriteTablePtr request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/timestamp_provider.cpp b/yt/yt/client/api/rpc_proxy/timestamp_provider.cpp
new file mode 100644
index 0000000000..faec95465e
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/timestamp_provider.cpp
@@ -0,0 +1,69 @@
+#include "timestamp_provider.h"
+#include "api_service_proxy.h"
+#include "connection_impl.h"
+
+#include <yt/yt/client/transaction_client/timestamp_provider_base.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NTransactionClient;
+using namespace NObjectClient;
+using namespace NRpc;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimestampProvider
+ : public TTimestampProviderBase
+{
+public:
+ TTimestampProvider(
+ IChannelPtr channel,
+ TDuration rpcTimeout,
+ TDuration latestTimestampUpdatePeriod,
+ TCellTag clockClusterTag)
+ : TTimestampProviderBase(latestTimestampUpdatePeriod)
+ , Channel_(std::move(channel))
+ , RpcTimeout_(rpcTimeout)
+ , ClockClusterTag_(clockClusterTag)
+ { }
+
+private:
+ const IChannelPtr Channel_;
+ const TDuration RpcTimeout_;
+ const TCellTag ClockClusterTag_;
+
+ TFuture<NTransactionClient::TTimestamp> DoGenerateTimestamps(int count) override
+ {
+ TApiServiceProxy proxy(Channel_);
+
+ auto req = proxy.GenerateTimestamps();
+ req->SetTimeout(RpcTimeout_);
+ req->set_count(count);
+ if (ClockClusterTag_ != InvalidCellTag) {
+ req->set_clock_cluster_tag(ToProto<int>(ClockClusterTag_));
+ }
+
+ return req->Invoke().Apply(BIND([] (const TApiServiceProxy::TRspGenerateTimestampsPtr& rsp) {
+ return rsp->timestamp();
+ }));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTransactionClient::ITimestampProviderPtr CreateTimestampProvider(
+ IChannelPtr channel,
+ TDuration rpcTimeout,
+ TDuration latestTimestampUpdatePeriod,
+ TCellTag clockClusterTag)
+{
+ return New<TTimestampProvider>(
+ std::move(channel),
+ rpcTimeout,
+ latestTimestampUpdatePeriod,
+ clockClusterTag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/timestamp_provider.h b/yt/yt/client/api/rpc_proxy/timestamp_provider.h
new file mode 100644
index 0000000000..b3abf287b5
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/timestamp_provider.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTransactionClient::ITimestampProviderPtr CreateTimestampProvider(
+ NRpc::IChannelPtr channel,
+ TDuration rpcTimeout,
+ TDuration latestTimestampUpdatePeriod,
+ NObjectClient::TCellTag clockClusterTag = NObjectClient::InvalidCellTag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/transaction.cpp b/yt/yt/client/api/rpc_proxy/transaction.cpp
new file mode 100644
index 0000000000..9e0c7423a1
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/transaction.cpp
@@ -0,0 +1,45 @@
+#include "transaction.h"
+
+#include "transaction_impl.h"
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::ITransactionPtr CreateTransaction(
+ TConnectionPtr connection,
+ TClientPtr client,
+ NRpc::IChannelPtr channel,
+ NTransactionClient::TTransactionId id,
+ NTransactionClient::TTimestamp startTimestamp,
+ NTransactionClient::ETransactionType type,
+ NTransactionClient::EAtomicity atomicity,
+ NTransactionClient::EDurability durability,
+ TDuration timeout,
+ bool pingAncestors,
+ std::optional<TDuration> pingPeriod,
+ std::optional<TStickyTransactionParameters> stickyParameters,
+ i64 sequenceNumberSourceId,
+ TStringBuf capitalizedCreationReason)
+{
+ return New<TTransaction>(
+ std::move(connection),
+ std::move(client),
+ std::move(channel),
+ id,
+ startTimestamp,
+ type,
+ atomicity,
+ durability,
+ timeout,
+ pingAncestors,
+ pingPeriod,
+ std::move(stickyParameters),
+ sequenceNumberSourceId,
+ capitalizedCreationReason);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/transaction.h b/yt/yt/client/api/rpc_proxy/transaction.h
new file mode 100644
index 0000000000..bb75aedc27
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/transaction.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/client/api/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::ITransactionPtr CreateTransaction(
+ TConnectionPtr connection,
+ TClientPtr client,
+ NRpc::IChannelPtr channel,
+ NTransactionClient::TTransactionId id,
+ NTransactionClient::TTimestamp startTimestamp,
+ NTransactionClient::ETransactionType type,
+ NTransactionClient::EAtomicity atomicity,
+ NTransactionClient::EDurability durability,
+ TDuration timeout,
+ bool pingAncestors,
+ std::optional<TDuration> pingPeriod,
+ std::optional<TStickyTransactionParameters> stickyParameters,
+ i64 sequenceNumberSourceId,
+ TStringBuf capitalizedCreationReason);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/transaction_impl-inl.h b/yt/yt/client/api/rpc_proxy/transaction_impl-inl.h
new file mode 100644
index 0000000000..d175aacbbd
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/transaction_impl-inl.h
@@ -0,0 +1,30 @@
+#ifndef TRANSACTION_IMPL_INL_H_
+#error "Direct inclusion of this file is not allowed, include transaction_impl.h"
+// For the sake of sane code completion.
+#include "transaction_impl.h"
+#endif
+#undef TRANSACTION_IMPL_INL_H_
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T TTransaction::PatchTransactionId(const T& options)
+{
+ auto copiedOptions = options;
+ copiedOptions.TransactionId = Id_;
+ return copiedOptions;
+}
+
+template <class T>
+T TTransaction::PatchTransactionTimestamp(const T& options)
+{
+ auto copiedOptions = options;
+ copiedOptions.Timestamp = StartTimestamp_;
+ return copiedOptions;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/transaction_impl.cpp b/yt/yt/client/api/rpc_proxy/transaction_impl.cpp
new file mode 100644
index 0000000000..20dadc0449
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/transaction_impl.cpp
@@ -0,0 +1,1075 @@
+#include "transaction_impl.h"
+#include "client_impl.h"
+#include "helpers.h"
+#include "config.h"
+#include "private.h"
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/api/transaction.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NConcurrency;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NTransactionClient;
+using namespace NObjectClient;
+using namespace NTableClient;
+using namespace NCypressClient;
+using namespace NApi;
+using namespace NYTree;
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransaction::TTransaction(
+ TConnectionPtr connection,
+ TClientPtr client,
+ NRpc::IChannelPtr channel,
+ TTransactionId id,
+ TTimestamp startTimestamp,
+ ETransactionType type,
+ EAtomicity atomicity,
+ EDurability durability,
+ TDuration timeout,
+ bool pingAncestors,
+ std::optional<TDuration> pingPeriod,
+ std::optional<TStickyTransactionParameters> stickyParameters,
+ i64 sequenceNumberSourceId,
+ TStringBuf capitalizedCreationReason)
+ : Connection_(std::move(connection))
+ , Client_(std::move(client))
+ , Channel_(std::move(channel))
+ , Id_(id)
+ , StartTimestamp_(startTimestamp)
+ , Type_(type)
+ , Atomicity_(atomicity)
+ , Durability_(durability)
+ , Timeout_(timeout)
+ , PingAncestors_(pingAncestors)
+ , PingPeriod_(pingPeriod)
+ , StickyProxyAddress_(stickyParameters ? std::move(stickyParameters->ProxyAddress) : TString())
+ , SequenceNumberSourceId_(sequenceNumberSourceId)
+ , Logger(RpcProxyClientLogger.WithTag("TransactionId: %v, %v",
+ Id_,
+ Connection_->GetLoggingTag()))
+ , Proxy_(Channel_)
+{
+ const auto& config = Connection_->GetConfig();
+ Proxy_.SetDefaultTimeout(config->RpcTimeout);
+ Proxy_.SetDefaultRequestCodec(config->RequestCodec);
+ Proxy_.SetDefaultResponseCodec(config->ResponseCodec);
+ Proxy_.SetDefaultEnableLegacyRpcCodecs(config->EnableLegacyRpcCodecs);
+
+ YT_LOG_DEBUG("%v (Type: %v, StartTimestamp: %v, Atomicity: %v, "
+ "Durability: %v, Timeout: %v, PingAncestors: %v, PingPeriod: %v, Sticky: %v, StickyProxyAddress: %v)",
+ capitalizedCreationReason,
+ GetType(),
+ GetStartTimestamp(),
+ GetAtomicity(),
+ GetDurability(),
+ GetTimeout(),
+ PingAncestors_,
+ PingPeriod_,
+ /*sticky*/ stickyParameters.has_value(),
+ StickyProxyAddress_);
+
+ // TODO(babenko): don't run periodic pings if client explicitly disables them in options
+ RunPeriodicPings();
+}
+
+IConnectionPtr TTransaction::GetConnection()
+{
+ return Connection_;
+}
+
+IClientPtr TTransaction::GetClient() const
+{
+ return Client_;
+}
+
+TTransactionId TTransaction::GetId() const
+{
+ return Id_;
+}
+
+TTimestamp TTransaction::GetStartTimestamp() const
+{
+ return StartTimestamp_;
+}
+
+ETransactionType TTransaction::GetType() const
+{
+ return Type_;
+}
+
+EAtomicity TTransaction::GetAtomicity() const
+{
+ return Atomicity_;
+}
+
+EDurability TTransaction::GetDurability() const
+{
+ return Durability_;
+}
+
+TDuration TTransaction::GetTimeout() const
+{
+ return Timeout_;
+}
+
+void TTransaction::RegisterAlienTransaction(const ITransactionPtr& transaction)
+{
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ != ETransactionState::Active) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Transaction %v is in %Qlv state",
+ GetId(),
+ State_);
+ }
+
+ if (GetType() != ETransactionType::Tablet) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::MalformedAlienTransaction,
+ "Transaction %v is of type %Qlv and hence does not allow alien transactions",
+ GetId(),
+ GetType());
+ }
+
+ if (GetId() != transaction->GetId()) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::MalformedAlienTransaction,
+ "Transaction id mismatch: native %v, alien %v",
+ GetId(),
+ transaction->GetId());
+ }
+
+ AlienTransactions_.push_back(transaction);
+ }
+
+ YT_LOG_DEBUG("Alien transaction registered (AlienConnection: {%v})",
+ transaction->GetConnection()->GetLoggingTag());
+}
+
+TFuture<void> TTransaction::Ping(const NApi::TTransactionPingOptions& /*options*/)
+{
+ return SendPing();
+}
+
+void TTransaction::Detach()
+{
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ == ETransactionState::Detached) {
+ return;
+ }
+
+ State_ = ETransactionState::Detached;
+ }
+
+ YT_LOG_DEBUG("Transaction detached");
+
+ auto req = Proxy_.DetachTransaction();
+ ToProto(req->mutable_transaction_id(), GetId());
+ // Fire-and-forget.
+ YT_UNUSED_FUTURE(req->Invoke());
+}
+
+void TTransaction::SubscribeCommitted(const TCommittedHandler& handler)
+{
+ Committed_.Subscribe(handler);
+}
+
+void TTransaction::UnsubscribeCommitted(const TCommittedHandler& handler)
+{
+ Committed_.Unsubscribe(handler);
+}
+
+void TTransaction::SubscribeAborted(const TAbortedHandler& handler)
+{
+ Aborted_.Subscribe(handler);
+}
+
+void TTransaction::UnsubscribeAborted(const TAbortedHandler& handler)
+{
+ Aborted_.Unsubscribe(handler);
+}
+
+TFuture<TTransactionFlushResult> TTransaction::Flush()
+{
+ std::vector<TFuture<void>> futures;
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ != ETransactionState::Active) {
+ return MakeFuture<TTransactionFlushResult>(TError(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Transaction %v is in %Qlv state",
+ GetId(),
+ State_));
+ }
+
+ if (!AlienTransactions_.empty()) {
+ return MakeFuture<TTransactionFlushResult>(TError(
+ NTransactionClient::EErrorCode::AlienTransactionsForbidden,
+ "Cannot flush transaction %v since it has %v alien transaction(s)",
+ GetId(),
+ AlienTransactions_.size()));
+ }
+
+ State_ = ETransactionState::Flushing;
+ futures = FlushModifyRowsRequests();
+ }
+
+ YT_LOG_DEBUG("Flushing transaction");
+
+ return AllSucceeded(futures)
+ .Apply(
+ BIND([=, this, this_ = MakeStrong(this)] {
+ auto req = Proxy_.FlushTransaction();
+ ToProto(req->mutable_transaction_id(), GetId());
+ return req->Invoke();
+ }))
+ .Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TApiServiceProxy::TErrorOrRspFlushTransactionPtr& rspOrError) -> TErrorOr<TTransactionFlushResult> {
+ {
+ auto guard = Guard(SpinLock_);
+ if (rspOrError.IsOK() && State_ == ETransactionState::Flushing) {
+ State_ = ETransactionState::Flushed;
+ } else if (!rspOrError.IsOK()) {
+ YT_LOG_DEBUG(rspOrError, "Error flushing transaction");
+ YT_UNUSED_FUTURE(DoAbort(&guard));
+ THROW_ERROR_EXCEPTION("Error flushing transaction %v",
+ GetId())
+ << rspOrError;
+ }
+ }
+
+ const auto& rsp = rspOrError.Value();
+ TTransactionFlushResult result{
+ .ParticipantCellIds = FromProto<std::vector<TCellId>>(rsp->participant_cell_ids())
+ };
+
+ YT_LOG_DEBUG("Transaction flushed (ParticipantCellIds: %v)",
+ result.ParticipantCellIds);
+
+ return result;
+ }));
+}
+
+TFuture<TTransactionCommitResult> TTransaction::Commit(const TTransactionCommitOptions& options)
+{
+ std::vector<TFuture<void>> futures;
+ std::vector<NApi::ITransactionPtr> alienTransactions;
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ != ETransactionState::Active) {
+ return MakeFuture<TTransactionCommitResult>(TError(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Transaction %v is in %Qlv state",
+ GetId(),
+ State_));
+ }
+
+ State_ = ETransactionState::Committing;
+ futures = FlushModifyRowsRequests();
+ alienTransactions = std::move(AlienTransactions_);
+ }
+
+ YT_LOG_DEBUG("Committing transaction (AlienTransactionCount: %v)",
+ alienTransactions.size());
+
+ for (const auto& transaction : alienTransactions) {
+ futures.push_back(
+ transaction->Flush().Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TErrorOr<TTransactionFlushResult>& resultOrError) {
+ THROW_ERROR_EXCEPTION_IF_FAILED(resultOrError, "Error flushing alien transaction");
+
+ const auto& result = resultOrError.Value();
+
+ YT_LOG_DEBUG("Alien transaction flushed (ParticipantCellIds: %v, AlienConnection: {%v})",
+ result.ParticipantCellIds,
+ transaction->GetConnection()->GetLoggingTag());
+
+ for (auto cellId : result.ParticipantCellIds) {
+ AdditionalParticipantCellIds_.insert(cellId);
+ }
+ })));
+ }
+
+ return AllSucceeded(futures)
+ .Apply(
+ BIND([=, this, this_ = MakeStrong(this)] {
+ auto req = Proxy_.CommitTransaction();
+ ToProto(req->mutable_transaction_id(), GetId());
+ ToProto(req->mutable_additional_participant_cell_ids(), AdditionalParticipantCellIds_);
+ ToProto(req->mutable_prerequisite_options(), options);
+ return req->Invoke();
+ }))
+ .Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TErrorOr<TApiServiceProxy::TRspCommitTransactionPtr>& rspOrError) {
+ {
+ auto guard = Guard(SpinLock_);
+ if (rspOrError.IsOK() && State_ == ETransactionState::Committing) {
+ State_ = ETransactionState::Committed;
+ } else if (!rspOrError.IsOK()) {
+ YT_UNUSED_FUTURE(DoAbort(&guard));
+ THROW_ERROR_EXCEPTION("Error committing transaction %v",
+ GetId())
+ << rspOrError;
+ }
+ }
+
+ for (const auto& transaction : alienTransactions) {
+ transaction->Detach();
+ }
+
+ const auto& rsp = rspOrError.Value();
+ TTransactionCommitResult result{
+ .PrimaryCommitTimestamp = rsp->primary_commit_timestamp(),
+ .CommitTimestamps = FromProto<NHiveClient::TTimestampMap>(rsp->commit_timestamps())
+ };
+
+ YT_LOG_DEBUG("Transaction committed (CommitTimestamps: %v)",
+ result.CommitTimestamps);
+
+ Committed_.Fire();
+
+ return result;
+ }));
+}
+
+TFuture<void> TTransaction::Abort(const TTransactionAbortOptions& options)
+{
+ auto guard = Guard(SpinLock_);
+
+ if (State_ == ETransactionState::Committed || State_ == ETransactionState::Detached) {
+ return MakeFuture<void>(TError(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Cannot abort since transaction %v is in %Qlv state",
+ GetId(),
+ State_));
+ }
+
+ return DoAbort(&guard, options);
+}
+
+void TTransaction::ModifyRows(
+ const TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TRowModification> modifications,
+ const TModifyRowsOptions& options)
+{
+ ValidateTabletTransactionId(GetId());
+
+ for (const auto& modification : modifications) {
+ // TODO(sandello): handle versioned rows
+ YT_VERIFY(
+ modification.Type == ERowModificationType::Write ||
+ modification.Type == ERowModificationType::Delete ||
+ modification.Type == ERowModificationType::WriteAndLock);
+ }
+
+ auto reqSequenceNumber = ModifyRowsRequestSequenceCounter_.fetch_add(1);
+
+ auto req = Proxy_.ModifyRows();
+ req->set_sequence_number(reqSequenceNumber);
+ req->set_sequence_number_source_id(SequenceNumberSourceId_);
+ ToProto(req->mutable_transaction_id(), GetId());
+ req->set_path(path);
+ req->set_require_sync_replica(options.RequireSyncReplica);
+ ToProto(req->mutable_upstream_replica_id(), options.UpstreamReplicaId);
+ req->set_allow_missing_key_columns(options.AllowMissingKeyColumns);
+
+ std::vector<TUnversionedRow> rows;
+ rows.reserve(modifications.Size());
+
+ bool usedStrongLocks = false;
+ for (const auto& modification : modifications) {
+ auto mask = modification.Locks;
+ for (int index = 0; index < TLegacyLockMask::MaxCount; ++index) {
+ if (mask.Get(index) > MaxOldLockType) {
+ THROW_ERROR_EXCEPTION("New locks are not supported in RPC client yet")
+ << TErrorAttribute("lock_index", index)
+ << TErrorAttribute("lock_type", mask.Get(index));
+ }
+ usedStrongLocks |= mask.Get(index) == ELockType::SharedStrong;
+ }
+ }
+
+ if (usedStrongLocks) {
+ req->Header().set_protocol_version_minor(YTRpcModifyRowsStrongLocksVersion);
+ }
+
+ for (const auto& modification : modifications) {
+ rows.emplace_back(modification.Row);
+ req->add_row_modification_types(static_cast<NProto::ERowModificationType>(modification.Type));
+ if (usedStrongLocks) {
+ auto locks = modification.Locks;
+ YT_VERIFY(!locks.HasNewLocks());
+ req->add_row_locks(locks.ToLegacyMask().GetBitmap());
+ } else {
+ TLegacyLockBitmap bitmap = 0;
+ for (int index = 0; index < TLegacyLockMask::MaxCount; ++index) {
+ if (modification.Locks.Get(index) == ELockType::SharedWeak) {
+ bitmap |= 1u << index;
+ }
+ }
+ req->add_row_read_locks(bitmap);
+ }
+ }
+
+ req->Attachments() = SerializeRowset(
+ nameTable,
+ MakeRange(rows),
+ req->mutable_rowset_descriptor());
+
+ TFuture<void> future;
+ const auto& config = Connection_->GetConfig();
+ if (config->ModifyRowsBatchCapacity == 0) {
+ ValidateActive();
+ future = req->Invoke().As<void>();
+ } else {
+ YT_LOG_DEBUG("Pushing a subrequest into a batch modify rows request (SubrequestAttachmentCount: 1+%v)",
+ req->Attachments().size());
+
+ auto reqBody = SerializeProtoToRef(*req);
+
+ {
+ auto guard = Guard(SpinLock_);
+
+ DoValidateActive();
+
+ if (!BatchModifyRowsRequest_) {
+ BatchModifyRowsRequest_ = Proxy_.BatchModifyRows();
+ ToProto(BatchModifyRowsRequest_->mutable_transaction_id(), GetId());
+ }
+
+ BatchModifyRowsRequest_->Attachments().push_back(reqBody);
+ BatchModifyRowsRequest_->Attachments().insert(
+ BatchModifyRowsRequest_->Attachments().end(),
+ req->Attachments().begin(),
+ req->Attachments().end());
+ BatchModifyRowsRequest_->add_part_counts(req->Attachments().size());
+
+ if (BatchModifyRowsRequest_->part_counts_size() == config->ModifyRowsBatchCapacity) {
+ future = InvokeBatchModifyRowsRequest();
+ }
+ }
+ }
+
+ if (future) {
+ future
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this)](const TError& error) {
+ if (!error.IsOK()) {
+ YT_LOG_DEBUG(error, "Error sending row modifications");
+ YT_UNUSED_FUTURE(Abort());
+ }
+ }));
+
+ {
+ auto guard = Guard(SpinLock_);
+ BatchModifyRowsFutures_.push_back(std::move(future));
+ }
+ }
+}
+
+TFuture<ITransactionPtr> TTransaction::StartTransaction(
+ ETransactionType type,
+ const TTransactionStartOptions& options)
+{
+ ValidateActive();
+ return Client_->StartTransaction(
+ type,
+ PatchTransactionId(options));
+}
+
+TFuture<IUnversionedRowsetPtr> TTransaction::LookupRows(
+ const TYPath& path,
+ TNameTablePtr nameTable,
+ const TSharedRange<TLegacyKey>& keys,
+ const TLookupRowsOptions& options)
+{
+ ValidateActive();
+ return Client_->LookupRows(
+ path,
+ std::move(nameTable),
+ keys,
+ PatchTransactionTimestamp(options));
+}
+
+TFuture<IVersionedRowsetPtr> TTransaction::VersionedLookupRows(
+ const TYPath& path,
+ TNameTablePtr nameTable,
+ const TSharedRange<TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options)
+{
+ ValidateActive();
+ return Client_->VersionedLookupRows(
+ path,
+ std::move(nameTable),
+ keys,
+ PatchTransactionTimestamp(options));
+}
+
+TFuture<std::vector<IUnversionedRowsetPtr>> TTransaction::MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options)
+{
+ ValidateActive();
+ return Client_->MultiLookup(
+ subrequests,
+ PatchTransactionTimestamp(options));
+}
+
+TFuture<TSelectRowsResult> TTransaction::SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options)
+{
+ ValidateActive();
+ return Client_->SelectRows(
+ query,
+ PatchTransactionTimestamp(options));
+}
+
+TFuture<NYson::TYsonString> TTransaction::ExplainQuery(
+ const TString& query,
+ const TExplainQueryOptions& options)
+{
+ ValidateActive();
+ return Client_->ExplainQuery(
+ query,
+ PatchTransactionTimestamp(options));
+}
+
+TFuture<TPullRowsResult> TTransaction::PullRows(
+ const TYPath& path,
+ const TPullRowsOptions& options)
+{
+ ValidateActive();
+ return Client_->PullRows(
+ path,
+ options);
+}
+
+TFuture<ITableReaderPtr> TTransaction::CreateTableReader(
+ const TRichYPath& path,
+ const NApi::TTableReaderOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateTableReader(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<ITableWriterPtr> TTransaction::CreateTableWriter(
+ const TRichYPath& path,
+ const NApi::TTableWriterOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateTableWriter(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<NYson::TYsonString> TTransaction::GetNode(
+ const TYPath& path,
+ const TGetNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->GetNode(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::SetNode(
+ const TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->SetNode(
+ path,
+ value,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::MultisetAttributesNode(
+ const TYPath& path,
+ const IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->MultisetAttributesNode(
+ path,
+ attributes,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::RemoveNode(
+ const TYPath& path,
+ const TRemoveNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->RemoveNode(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<NYson::TYsonString> TTransaction::ListNode(
+ const TYPath& path,
+ const TListNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->ListNode(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<TNodeId> TTransaction::CreateNode(
+ const TYPath& path,
+ EObjectType type,
+ const TCreateNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateNode(
+ path,
+ type,
+ PatchTransactionId(options));
+}
+
+TFuture<TLockNodeResult> TTransaction::LockNode(
+ const TYPath& path,
+ ELockMode mode,
+ const TLockNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->LockNode(
+ path,
+ mode,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::UnlockNode(
+ const NYPath::TYPath& path,
+ const NApi::TUnlockNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->UnlockNode(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<TNodeId> TTransaction::CopyNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TCopyNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->CopyNode(
+ srcPath,
+ dstPath,
+ PatchTransactionId(options));
+}
+
+TFuture<TNodeId> TTransaction::MoveNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TMoveNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->MoveNode(
+ srcPath,
+ dstPath,
+ PatchTransactionId(options));
+}
+
+TFuture<TNodeId> TTransaction::LinkNode(
+ const TYPath& srcPath,
+ const TYPath& dstPath,
+ const TLinkNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->LinkNode(
+ srcPath,
+ dstPath,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::ConcatenateNodes(
+ const std::vector<TRichYPath>& srcPaths,
+ const TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options)
+{
+ ValidateActive();
+ return Client_->ConcatenateNodes(
+ srcPaths,
+ dstPath,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::ExternalizeNode(
+ const TYPath& path,
+ TCellTag cellTag,
+ const TExternalizeNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->ExternalizeNode(
+ path,
+ cellTag,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::InternalizeNode(
+ const TYPath& path,
+ const TInternalizeNodeOptions& options)
+{
+ ValidateActive();
+ return Client_->InternalizeNode(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<bool> TTransaction::NodeExists(
+ const TYPath& path,
+ const TNodeExistsOptions& options)
+{
+ ValidateActive();
+ return Client_->NodeExists(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<TObjectId> TTransaction::CreateObject(
+ EObjectType type,
+ const TCreateObjectOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateObject(type, options);
+}
+
+TFuture<IFileReaderPtr> TTransaction::CreateFileReader(
+ const TYPath& path,
+ const TFileReaderOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateFileReader(
+ path,
+ PatchTransactionId(options));
+}
+
+IFileWriterPtr TTransaction::CreateFileWriter(
+ const TRichYPath& path,
+ const TFileWriterOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateFileWriter(
+ path,
+ PatchTransactionId(options));
+}
+
+IJournalReaderPtr TTransaction::CreateJournalReader(
+ const TYPath& path,
+ const TJournalReaderOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateJournalReader(
+ path,
+ PatchTransactionId(options));
+}
+
+IJournalWriterPtr TTransaction::CreateJournalWriter(
+ const TYPath& path,
+ const TJournalWriterOptions& options)
+{
+ ValidateActive();
+ return Client_->CreateJournalWriter(
+ path,
+ PatchTransactionId(options));
+}
+
+TFuture<void> TTransaction::DoAbort(
+ TGuard<NThreading::TSpinLock>* guard,
+ const TTransactionAbortOptions& /*options*/)
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (State_ == ETransactionState::Aborting || State_ == ETransactionState::Aborted) {
+ return AbortPromise_.ToFuture();
+ }
+
+ YT_LOG_DEBUG("Aborting transaction");
+
+ State_ = ETransactionState::Aborting;
+
+ auto alienTransactions = AlienTransactions_;
+
+ guard->Release();
+
+ auto req = Proxy_.AbortTransaction();
+ ToProto(req->mutable_transaction_id(), GetId());
+
+ AbortPromise_.TrySetFrom(req->Invoke().Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TApiServiceProxy::TErrorOrRspAbortTransactionPtr& rspOrError) {
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ != ETransactionState::Aborting) {
+ YT_LOG_DEBUG(rspOrError, "Transaction is no longer aborting, abort response ignored");
+ return;
+ }
+
+ if (rspOrError.IsOK()) {
+ YT_LOG_DEBUG("Transaction aborted");
+ } else if (rspOrError.FindMatching(NTransactionClient::EErrorCode::NoSuchTransaction)) {
+ YT_LOG_DEBUG("Transaction has expired or was already aborted, ignored");
+ } else {
+ YT_LOG_DEBUG(rspOrError, "Error aborting transaction, considered detached");
+ State_ = ETransactionState::Detached;
+ THROW_ERROR_EXCEPTION("Error aborting transaction %v",
+ GetId())
+ << rspOrError;
+ }
+
+ State_ = ETransactionState::Aborted;
+ }
+
+ Aborted_.Fire(TError("Transaction aborted by user request"));
+ })));
+
+ for (const auto& transaction : alienTransactions) {
+ YT_UNUSED_FUTURE(transaction->Abort());
+ }
+
+ return AbortPromise_.ToFuture();
+}
+
+TFuture<void> TTransaction::SendPing()
+{
+ YT_LOG_DEBUG("Pinging transaction");
+
+ auto req = Proxy_.PingTransaction();
+ ToProto(req->mutable_transaction_id(), GetId());
+ req->set_ping_ancestors(PingAncestors_);
+
+ return req->Invoke().Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const TApiServiceProxy::TErrorOrRspPingTransactionPtr& rspOrError) {
+ if (rspOrError.IsOK()) {
+ YT_LOG_DEBUG("Transaction pinged");
+ } else if (rspOrError.FindMatching(NTransactionClient::EErrorCode::NoSuchTransaction)) {
+ // Hard error.
+ YT_LOG_DEBUG("Transaction has expired or was aborted");
+
+ bool fireAborted = false;
+ {
+ auto guard = Guard(SpinLock_);
+ if (State_ != ETransactionState::Committed &&
+ State_ != ETransactionState::Flushed &&
+ State_ != ETransactionState::FlushedModifications &&
+ State_ != ETransactionState::Aborted &&
+ State_ != ETransactionState::Detached)
+ {
+ State_ = ETransactionState::Aborted;
+ fireAborted = true;
+ }
+ }
+
+ auto error = TError(
+ NTransactionClient::EErrorCode::NoSuchTransaction,
+ "Transaction %v has expired or was aborted",
+ GetId());
+
+ if (fireAborted) {
+ AbortPromise_.TrySet();
+ Aborted_.Fire(error);
+ }
+
+ THROW_ERROR(error);
+ } else {
+ // Soft error.
+ YT_LOG_DEBUG(rspOrError, "Error pinging transaction");
+ THROW_ERROR_EXCEPTION("Error pinging transaction %v",
+ GetId())
+ << rspOrError;
+ }
+ }));
+}
+
+void TTransaction::RunPeriodicPings()
+{
+ if (!PingPeriod_) {
+ return;
+ }
+
+ if (!IsPingableState()) {
+ return;
+ }
+
+ SendPing().Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TError& error) {
+ if (!IsPingableState()) {
+ return;
+ }
+
+ if (error.FindMatching(NYT::EErrorCode::Timeout)) {
+ RunPeriodicPings();
+ return;
+ }
+
+ YT_LOG_DEBUG("Transaction ping scheduled");
+
+ TDelayedExecutor::Submit(
+ BIND(&TTransaction::RunPeriodicPings, MakeWeak(this)),
+ *PingPeriod_);
+ }));
+}
+
+bool TTransaction::IsPingableState()
+{
+ auto guard = Guard(SpinLock_);
+ return
+ State_ == ETransactionState::Active ||
+ State_ == ETransactionState::Flushing ||
+ State_ == ETransactionState::Flushed ||
+ State_ == ETransactionState::FlushingModifications ||
+ State_ == ETransactionState::FlushedModifications ||
+ State_ == ETransactionState::Committing;
+}
+
+void TTransaction::ValidateActive()
+{
+ auto guard = Guard(SpinLock_);
+ DoValidateActive();
+}
+
+void TTransaction::DoValidateActive()
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ if (State_ != ETransactionState::Active) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Transaction %v is not active",
+ GetId());
+ }
+}
+
+TApiServiceProxy::TReqBatchModifyRowsPtr TTransaction::CreateBatchModifyRowsRequest()
+{
+ auto req = Proxy_.BatchModifyRows();
+ ToProto(req->mutable_transaction_id(), GetId());
+ return req;
+}
+
+TFuture<void> TTransaction::InvokeBatchModifyRowsRequest()
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ YT_VERIFY(BatchModifyRowsRequest_);
+
+ TApiServiceProxy::TReqBatchModifyRowsPtr batchRequest;
+ batchRequest.Swap(BatchModifyRowsRequest_);
+ if (batchRequest->part_counts_size() == 0) {
+ return VoidFuture;
+ }
+
+ YT_LOG_DEBUG("Invoking a batch modify rows request (Subrequests: %v)",
+ batchRequest->part_counts_size());
+
+ return batchRequest->Invoke().As<void>();
+}
+
+std::vector<TFuture<void>> TTransaction::FlushModifyRowsRequests()
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ auto futures = std::move(BatchModifyRowsFutures_);
+ if (BatchModifyRowsRequest_) {
+ futures.push_back(InvokeBatchModifyRowsRequest());
+ }
+ return futures;
+}
+
+TTransactionStartOptions TTransaction::PatchTransactionId(const TTransactionStartOptions& options)
+{
+ auto copiedOptions = options;
+ copiedOptions.ParentId = GetId();
+ return copiedOptions;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& TTransaction::GetStickyProxyAddress() const
+{
+ return StickyProxyAddress_;
+}
+
+TFuture<void> TTransaction::FlushModifications()
+{
+ std::vector<TFuture<void>> futures;
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (State_ != ETransactionState::Active) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Transaction %v is in %Qlv state",
+ GetId(),
+ State_);
+ }
+
+ if (!AlienTransactions_.empty()) {
+ return MakeFuture<void>(TError(
+ NTransactionClient::EErrorCode::AlienTransactionsForbidden,
+ "Cannot flush transaction %v modifications since it has %v alien transaction(s)",
+ GetId(),
+ AlienTransactions_.size()));
+ }
+
+ State_ = ETransactionState::FlushingModifications;
+ futures = FlushModifyRowsRequests();
+ }
+
+ YT_LOG_DEBUG("Flushing transaction modifications");
+
+ return AllSucceeded(futures)
+ .Apply(BIND([this, this_ = MakeStrong(this)] (const TError& rspOrError) {
+ {
+ auto guard = Guard(SpinLock_);
+ if (rspOrError.IsOK() && State_ == ETransactionState::FlushingModifications) {
+ State_ = ETransactionState::FlushedModifications;
+ } else if (!rspOrError.IsOK()) {
+ YT_LOG_DEBUG(rspOrError, "Error flushing transaction modifications");
+ YT_UNUSED_FUTURE(DoAbort(&guard));
+ THROW_ERROR_EXCEPTION("Error flushing transaction %v modifications",
+ GetId())
+ << rspOrError;
+ }
+ }
+
+ YT_LOG_DEBUG("Transaction modifications flushed");
+
+ ModificationsFlushed_.Fire();
+
+ return TError();
+ }));
+}
+
+void TTransaction::SubscribeModificationsFlushed(const TModificationsFlushedHandler& handler)
+{
+ ModificationsFlushed_.Subscribe(handler);
+}
+
+void TTransaction::UnsubscribeModificationsFlushed(const TModificationsFlushedHandler& handler)
+{
+ ModificationsFlushed_.Unsubscribe(handler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/rpc_proxy/transaction_impl.h b/yt/yt/client/api/rpc_proxy/transaction_impl.h
new file mode 100644
index 0000000000..415b0b4686
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/transaction_impl.h
@@ -0,0 +1,292 @@
+#pragma once
+
+#include "client_base.h"
+
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETransactionState,
+ (Active)
+ (Committing)
+ (Committed)
+ (Flushing)
+ (Flushed)
+ (FlushingModifications)
+ (FlushedModifications)
+ (Aborting)
+ (Aborted)
+ (Detached)
+);
+
+class TTransaction
+ : public NApi::ITransaction
+{
+public:
+ TTransaction(
+ TConnectionPtr connection,
+ TClientPtr client,
+ NRpc::IChannelPtr channel,
+ NTransactionClient::TTransactionId id,
+ NTransactionClient::TTimestamp startTimestamp,
+ NTransactionClient::ETransactionType type,
+ NTransactionClient::EAtomicity atomicity,
+ NTransactionClient::EDurability durability,
+ TDuration timeout,
+ bool pingAncestors,
+ std::optional<TDuration> pingPeriod,
+ std::optional<TStickyTransactionParameters> stickyParameters,
+ i64 sequenceNumberSourceId,
+ TStringBuf capitalizedCreationReason);
+
+ // ITransaction implementation.
+ NApi::IConnectionPtr GetConnection() override;
+ NApi::IClientPtr GetClient() const override;
+
+ NTransactionClient::ETransactionType GetType() const override;
+ NTransactionClient::TTransactionId GetId() const override;
+ NTransactionClient::TTimestamp GetStartTimestamp() const override;
+ NTransactionClient::EAtomicity GetAtomicity() const override;
+ NTransactionClient::EDurability GetDurability() const override;
+ TDuration GetTimeout() const override;
+
+ TFuture<void> Ping(const NApi::TTransactionPingOptions& options = {}) override;
+ TFuture<NApi::TTransactionFlushResult> Flush() override;
+ TFuture<NApi::TTransactionCommitResult> Commit(const NApi::TTransactionCommitOptions&) override;
+ TFuture<void> Abort(const NApi::TTransactionAbortOptions& options = {}) override;
+ void Detach() override;
+ void RegisterAlienTransaction(const ITransactionPtr& transaction) override;
+
+ void SubscribeCommitted(const TCommittedHandler& handler) override;
+ void UnsubscribeCommitted(const TCommittedHandler& handler) override;
+
+ void SubscribeAborted(const TAbortedHandler& handler) override;
+ void UnsubscribeAborted(const TAbortedHandler& handler) override;
+
+ void ModifyRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NApi::TRowModification> modifications,
+ const NApi::TModifyRowsOptions& options) override;
+
+ // IClientBase implementation.
+ TFuture<NApi::ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const NApi::TTransactionStartOptions& options) override;
+
+ TFuture<NApi::IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const NApi::TLookupRowsOptions& options) override;
+
+ TFuture<NApi::IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const NApi::TVersionedLookupRowsOptions& options) override;
+
+ TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options) override;
+
+ TFuture<NApi::TSelectRowsResult> SelectRows(
+ const TString& query,
+ const NApi::TSelectRowsOptions& options) override;
+
+ TFuture<NYson::TYsonString> ExplainQuery(
+ const TString& query,
+ const NApi::TExplainQueryOptions& options) override;
+
+ TFuture<NApi::TPullRowsResult> PullRows(
+ const NYPath::TYPath& path,
+ const NApi::TPullRowsOptions& options) override;
+
+ TFuture<ITableReaderPtr> CreateTableReader(
+ const NYPath::TRichYPath& path,
+ const NApi::TTableReaderOptions& options) override;
+
+ TFuture<ITableWriterPtr> CreateTableWriter(
+ const NYPath::TRichYPath& path,
+ const NApi::TTableWriterOptions& options) override;
+
+ TFuture<NYson::TYsonString> GetNode(
+ const NYPath::TYPath& path,
+ const NApi::TGetNodeOptions& options) override;
+
+ TFuture<void> SetNode(
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const NApi::TSetNodeOptions& options) override;
+
+ TFuture<void> MultisetAttributesNode(
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const NApi::TMultisetAttributesNodeOptions& options) override;
+
+ TFuture<void> RemoveNode(
+ const NYPath::TYPath& path,
+ const NApi::TRemoveNodeOptions& options) override;
+
+ TFuture<NYson::TYsonString> ListNode(
+ const NYPath::TYPath& path,
+ const NApi::TListNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> CreateNode(
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const NApi::TCreateNodeOptions& options) override;
+
+ TFuture<NApi::TLockNodeResult> LockNode(
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const NApi::TLockNodeOptions& options) override;
+
+ TFuture<void> UnlockNode(
+ const NYPath::TYPath& path,
+ const NApi::TUnlockNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> CopyNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TCopyNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> MoveNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TMoveNodeOptions& options) override;
+
+ TFuture<NCypressClient::TNodeId> LinkNode(
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const NApi::TLinkNodeOptions& options) override;
+
+ TFuture<void> ConcatenateNodes(
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const NApi::TConcatenateNodesOptions& options) override;
+
+ TFuture<void> ExternalizeNode(
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options) override;
+
+ TFuture<void> InternalizeNode(
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options) override;
+
+ TFuture<bool> NodeExists(
+ const NYPath::TYPath& path,
+ const NApi::TNodeExistsOptions& options) override;
+
+ TFuture<NObjectClient::TObjectId> CreateObject(
+ NObjectClient::EObjectType type,
+ const NApi::TCreateObjectOptions& options) override;
+
+ TFuture<NApi::IFileReaderPtr> CreateFileReader(
+ const NYPath::TYPath& path,
+ const NApi::TFileReaderOptions& options) override;
+
+ NApi::IFileWriterPtr CreateFileWriter(
+ const NYPath::TRichYPath& path,
+ const NApi::TFileWriterOptions& options) override;
+
+ NApi::IJournalReaderPtr CreateJournalReader(
+ const NYPath::TYPath& path,
+ const NApi::TJournalReaderOptions& options) override;
+
+ NApi::IJournalWriterPtr CreateJournalWriter(
+ const NYPath::TYPath& path,
+ const NApi::TJournalWriterOptions& options) override;
+
+ // Custom methods.
+
+ //! Returns proxy address this transaction is sticking to.
+ //! Empty for non-sticky transactions (e.g.: master) or
+ //! if address resolution is not supported by the implementation.
+ const TString& GetStickyProxyAddress() const;
+
+ //! Flushes all modifications to RPC proxy.
+ //!
+ //! In contrast to #Flush does not send #FlushTransaction request to RPC proxy and does not change
+ //! the state of the transaction within RPC proxy allowing to work with the transaction after the call
+ //! from a different transaction client.
+ //!
+ //! Useful for sending modifications to the same transaction from several different sources.
+ TFuture<void> FlushModifications();
+
+ using TModificationsFlushedHandlerSignature = void();
+ using TModificationsFlushedHandler = TCallback<TModificationsFlushedHandlerSignature>;
+ void SubscribeModificationsFlushed(const TModificationsFlushedHandler& handler);
+ void UnsubscribeModificationsFlushed(const TModificationsFlushedHandler& handler);
+
+private:
+ const TConnectionPtr Connection_;
+ const TClientPtr Client_;
+ const NRpc::IChannelPtr Channel_;
+ const NTransactionClient::TTransactionId Id_;
+ const NTransactionClient::TTimestamp StartTimestamp_;
+ const NTransactionClient::ETransactionType Type_;
+ const NTransactionClient::EAtomicity Atomicity_;
+ const NTransactionClient::EDurability Durability_;
+ const TDuration Timeout_;
+ const bool PingAncestors_;
+ const std::optional<TDuration> PingPeriod_;
+ const TString StickyProxyAddress_;
+ const i64 SequenceNumberSourceId_;
+
+ const NLogging::TLogger Logger;
+
+ TApiServiceProxy Proxy_;
+
+ std::atomic<i64> ModifyRowsRequestSequenceCounter_ = 0;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ ETransactionState State_ = ETransactionState::Active;
+ const TPromise<void> AbortPromise_ = NewPromise<void>();
+ std::vector<NApi::ITransactionPtr> AlienTransactions_;
+
+ THashSet<NObjectClient::TCellId> AdditionalParticipantCellIds_;
+
+ TApiServiceProxy::TReqBatchModifyRowsPtr BatchModifyRowsRequest_;
+ std::vector<TFuture<void>> BatchModifyRowsFutures_;
+
+ TSingleShotCallbackList<TCommittedHandlerSignature> Committed_;
+ TSingleShotCallbackList<TAbortedHandlerSignature> Aborted_;
+ TSingleShotCallbackList<TModificationsFlushedHandlerSignature> ModificationsFlushed_;
+
+ TFuture<void> SendPing();
+ void RunPeriodicPings();
+ bool IsPingableState();
+
+ TFuture<void> DoAbort(
+ TGuard<NThreading::TSpinLock>* guard,
+ const TTransactionAbortOptions& options = {});
+
+ void ValidateActive();
+ void DoValidateActive();
+
+ TApiServiceProxy::TReqBatchModifyRowsPtr CreateBatchModifyRowsRequest();
+ TFuture<void> InvokeBatchModifyRowsRequest();
+ std::vector<TFuture<void>> FlushModifyRowsRequests();
+
+ template <class T>
+ T PatchTransactionId(const T& options);
+ NApi::TTransactionStartOptions PatchTransactionId(const NApi::TTransactionStartOptions& options);
+ template <class T>
+ T PatchTransactionTimestamp(const T& options);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
+#define TRANSACTION_IMPL_INL_H_
+#include "transaction_impl-inl.h"
+#undef TRANSACTION_IMPL_INL_H_
diff --git a/yt/yt/client/api/rpc_proxy/wire_row_stream.cpp b/yt/yt/client/api/rpc_proxy/wire_row_stream.cpp
new file mode 100644
index 0000000000..b3ea4244f5
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/wire_row_stream.cpp
@@ -0,0 +1,141 @@
+#include "wire_row_stream.h"
+#include "row_stream.h"
+#include "helpers.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/row_batch.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/wire_protocol.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+
+#include <yt/yt/core/misc/range.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireRowStreamEncoder
+ : public IRowStreamEncoder
+{
+public:
+ explicit TWireRowStreamEncoder(TNameTablePtr nameTable)
+ : NameTable_(std::move(nameTable))
+ { }
+
+ TSharedRef Encode(
+ const IUnversionedRowBatchPtr& batch,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics* statistics) override
+ {
+ YT_VERIFY(NameTableSize_ <= NameTable_->GetSize());
+
+ NProto::TRowsetDescriptor descriptor;
+ descriptor.set_wire_format_version(NApi::NRpcProxy::CurrentWireFormatVersion);
+ descriptor.set_rowset_kind(NApi::NRpcProxy::NProto::RK_UNVERSIONED);
+ descriptor.set_rowset_format(NApi::NRpcProxy::NProto::RF_YT_WIRE);
+ for (int id = NameTableSize_; id < NameTable_->GetSize(); ++id) {
+ auto* entry = descriptor.add_name_table_entries();
+ entry->set_name(TString(NameTable_->GetName(id)));
+ }
+ NameTableSize_ += descriptor.name_table_entries_size();
+
+ auto writer = CreateWireProtocolWriter();
+ auto rows = batch->MaterializeRows();
+ writer->WriteUnversionedRowset(rows);
+ auto rowRefs = writer->Finish();
+
+ auto [block, payloadRef] = SerializeRowStreamBlockEnvelope(
+ GetByteSize(rowRefs),
+ descriptor,
+ statistics);
+
+ MergeRefsToRef(rowRefs, payloadRef);
+
+ return block;
+ }
+
+private:
+ const TNameTablePtr NameTable_;
+
+ int NameTableSize_ = 0;
+};
+
+IRowStreamEncoderPtr CreateWireRowStreamEncoder(TNameTablePtr nameTable)
+{
+ return New<TWireRowStreamEncoder>(std::move(nameTable));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireRowStreamDecoder
+ : public IRowStreamDecoder
+{
+public:
+ explicit TWireRowStreamDecoder(TNameTablePtr nameTable)
+ : NameTable_(std::move(nameTable))
+ {
+ Descriptor_.set_wire_format_version(NApi::NRpcProxy::CurrentWireFormatVersion);
+ Descriptor_.set_rowset_kind(NApi::NRpcProxy::NProto::RK_UNVERSIONED);
+ }
+
+ IUnversionedRowBatchPtr Decode(
+ const TSharedRef& payloadRef,
+ const NProto::TRowsetDescriptor& descriptorDelta) override
+ {
+ struct TWireRowStreamDecoderTag { };
+ auto reader = CreateWireProtocolReader(payloadRef, New<TRowBuffer>(TWireRowStreamDecoderTag()));
+ auto rows = reader->ReadUnversionedRowset(true);
+
+ auto oldNameTableSize = Descriptor_.name_table_entries_size();
+ YT_VERIFY(oldNameTableSize <= NameTable_->GetSize());
+
+ Descriptor_.MergeFrom(descriptorDelta);
+ auto newNameTableSize = Descriptor_.name_table_entries_size();
+
+ IdMapping_.resize(newNameTableSize);
+ for (int id = oldNameTableSize; id < newNameTableSize; ++id) {
+ const auto& name = Descriptor_.name_table_entries(id).name();
+ auto mappedId = NameTable_->GetIdOrRegisterName(name);
+ IdMapping_[id] = mappedId;
+ HasNontrivialIdMapping_ |= (id != mappedId);
+ }
+
+ if (HasNontrivialIdMapping_) {
+ for (auto row : rows) {
+ auto mutableRow = TMutableUnversionedRow(row.ToTypeErasedRow());
+ for (auto& value : mutableRow) {
+ auto newId = ApplyIdMapping(value, &IdMapping_);
+ if (newId < 0 || newId >= NameTable_->GetSize()) {
+ THROW_ERROR_EXCEPTION("Id mapping returned an invalid value %v for id %v: "
+ "expected a value in [0, %v) range",
+ newId,
+ value.Id,
+ NameTable_->GetSize());
+ }
+ value.Id = newId;
+ }
+ }
+ }
+
+ return CreateBatchFromUnversionedRows(std::move(rows));
+ }
+
+private:
+ const TNameTablePtr NameTable_;
+
+ NApi::NRpcProxy::NProto::TRowsetDescriptor Descriptor_;
+ TNameTableToSchemaIdMapping IdMapping_;
+ bool HasNontrivialIdMapping_ = false;
+};
+
+IRowStreamDecoderPtr CreateWireRowStreamDecoder(TNameTablePtr nameTable)
+{
+ return New<TWireRowStreamDecoder>(std::move(nameTable));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
+
diff --git a/yt/yt/client/api/rpc_proxy/wire_row_stream.h b/yt/yt/client/api/rpc_proxy/wire_row_stream.h
new file mode 100644
index 0000000000..528a15fdeb
--- /dev/null
+++ b/yt/yt/client/api/rpc_proxy/wire_row_stream.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+namespace NYT::NApi::NRpcProxy {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRowStreamEncoderPtr CreateWireRowStreamEncoder(NTableClient::TNameTablePtr nameTable);
+IRowStreamDecoderPtr CreateWireRowStreamDecoder(NTableClient::TNameTablePtr nameTable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/api/skynet.cpp b/yt/yt/client/api/skynet.cpp
new file mode 100644
index 0000000000..19038c76b8
--- /dev/null
+++ b/yt/yt/client/api/skynet.cpp
@@ -0,0 +1,49 @@
+#include "skynet.h"
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NApi {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NChunkClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TSkynetSharePartsLocations& skynetPartsLocations, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("nodes").Value(skynetPartsLocations.NodeDirectory)
+ .Item("chunk_specs").DoListFor(
+ skynetPartsLocations.ChunkSpecs,
+ [&] (TFluentList fluent, const NChunkClient::NProto::TChunkSpec& spec) {
+ fluent
+ .Item()
+ .BeginMap()
+ .Item("chunk_id").Value(FromProto<TChunkId>(spec.chunk_id()))
+ .Item("row_index").Value(spec.table_row_index())
+ .Item("row_count").Value(spec.row_count_override())
+ .Item("range_index").Value(spec.range_index())
+ .DoIf(spec.has_lower_limit(), [&] (TFluentMap fluent) {
+ fluent.Item("lower_limit").Value(FromProto<TLegacyReadLimit>(spec.lower_limit()));
+ })
+ .DoIf(spec.has_upper_limit(), [&] (TFluentMap fluent) {
+ fluent.Item("upper_limit").Value(FromProto<TLegacyReadLimit>(spec.upper_limit()));
+ })
+ .Item("replicas").DoListFor(spec.replicas(),
+ [] (TFluentList fluent, ui32 packedReplica) {
+ TChunkReplica replica;
+ FromProto(&replica, packedReplica);
+ fluent.Item().Value(replica.GetNodeId());
+ })
+ .EndMap();
+ })
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/skynet.h b/yt/yt/client/api/skynet.h
new file mode 100644
index 0000000000..4e81d291ce
--- /dev/null
+++ b/yt/yt/client/api/skynet.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/chunk_spec.pb.h>
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSkynetSharePartsLocations
+ : public TRefCounted
+{
+ NNodeTrackerClient::TNodeDirectoryPtr NodeDirectory = New<NNodeTrackerClient::TNodeDirectory>();
+ std::vector<NChunkClient::NProto::TChunkSpec> ChunkSpecs;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSkynetSharePartsLocations)
+
+void Serialize(
+ const TSkynetSharePartsLocations& skynetPartsLocations,
+ NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/sticky_transaction_pool.cpp b/yt/yt/client/api/sticky_transaction_pool.cpp
new file mode 100644
index 0000000000..0b2f264157
--- /dev/null
+++ b/yt/yt/client/api/sticky_transaction_pool.cpp
@@ -0,0 +1,191 @@
+#include "sticky_transaction_pool.h"
+
+#include "transaction.h"
+
+#include <yt/yt/core/concurrency/lease_manager.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NTransactionClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITransactionPtr IStickyTransactionPool::GetTransactionAndRenewLeaseOrThrow(
+ TTransactionId transactionId)
+{
+ auto transaction = FindTransactionAndRenewLease(transactionId);
+ if (!transaction) {
+ THROW_ERROR_EXCEPTION(
+ NTransactionClient::EErrorCode::NoSuchTransaction,
+ "Sticky transaction %v is not found, "
+ "this usually means that you use tablet transactions within HTTP API; "
+ "consider using RPC API instead",
+ transactionId);
+ }
+
+ return transaction;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStickyTransactionPool
+ : public IStickyTransactionPool
+{
+public:
+ explicit TStickyTransactionPool(const NLogging::TLogger& logger)
+ : Logger(logger)
+ { }
+
+ ITransactionPtr RegisterTransaction(ITransactionPtr transaction) override
+ {
+ auto transactionId = transaction->GetId();
+ TStickyTransactionEntry entry{
+ transaction,
+ NConcurrency::TLeaseManager::CreateLease(
+ transaction->GetTimeout(),
+ BIND(&TStickyTransactionPool::OnStickyTransactionLeaseExpired, MakeWeak(this), transactionId, MakeWeak(transaction)))
+ };
+
+ bool inserted = false;
+ {
+ auto guard = WriterGuard(StickyTransactionLock_);
+ inserted = IdToStickyTransactionEntry_.emplace(transactionId, entry).second;
+ }
+
+ if (!inserted) {
+ NConcurrency::TLeaseManager::CloseLease(entry.Lease);
+ THROW_ERROR_EXCEPTION(NTransactionClient::EErrorCode::InvalidTransactionState,
+ "Failed to register duplicate sticky transaction %v",
+ transactionId);
+ }
+
+ transaction->SubscribeCommitted(
+ BIND(&TStickyTransactionPool::OnStickyTransactionCommitted, MakeWeak(this), transactionId));
+ transaction->SubscribeAborted(
+ BIND(&TStickyTransactionPool::OnStickyTransactionAborted, MakeWeak(this), transactionId));
+
+ YT_LOG_DEBUG("Sticky transaction registered (TransactionId: %v)",
+ transactionId);
+
+ return transaction;
+ }
+
+ void UnregisterTransaction(TTransactionId transactionId) override
+ {
+ TStickyTransactionEntry entry;
+ {
+ auto guard = WriterGuard(StickyTransactionLock_);
+ auto it = IdToStickyTransactionEntry_.find(transactionId);
+ if (it == IdToStickyTransactionEntry_.end()) {
+ return;
+ }
+ entry = std::move(it->second);
+ IdToStickyTransactionEntry_.erase(it);
+ }
+
+ YT_LOG_DEBUG("Sticky transaction unregistered (TransactionId: %v)",
+ transactionId);
+ }
+
+ ITransactionPtr FindTransactionAndRenewLease(TTransactionId transactionId) override
+ {
+ ITransactionPtr transaction;
+ NConcurrency::TLease lease;
+ {
+ auto guard = ReaderGuard(StickyTransactionLock_);
+ auto it = IdToStickyTransactionEntry_.find(transactionId);
+ if (it == IdToStickyTransactionEntry_.end()) {
+ return nullptr;
+ }
+ const auto& entry = it->second;
+ transaction = entry.Transaction;
+ lease = entry.Lease;
+ }
+ NConcurrency::TLeaseManager::RenewLease(lease);
+ YT_LOG_DEBUG("Sticky transaction lease renewed (TransactionId: %v)",
+ transactionId);
+ return transaction;
+ }
+
+private:
+ const NLogging::TLogger Logger;
+
+ struct TStickyTransactionEntry
+ {
+ ITransactionPtr Transaction;
+ NConcurrency::TLease Lease;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, StickyTransactionLock_);
+ THashMap<TTransactionId, TStickyTransactionEntry> IdToStickyTransactionEntry_;
+
+ void OnStickyTransactionLeaseExpired(TTransactionId transactionId, TWeakPtr<ITransaction> weakTransaction)
+ {
+ auto transaction = weakTransaction.Lock();
+ if (!transaction) {
+ return;
+ }
+
+ {
+ auto guard = WriterGuard(StickyTransactionLock_);
+ auto it = IdToStickyTransactionEntry_.find(transactionId);
+ if (it == IdToStickyTransactionEntry_.end()) {
+ return;
+ }
+ if (it->second.Transaction != transaction) {
+ return;
+ }
+ IdToStickyTransactionEntry_.erase(it);
+ }
+
+ YT_LOG_DEBUG("Sticky transaction lease expired (TransactionId: %v)",
+ transactionId);
+
+ YT_UNUSED_FUTURE(transaction->Abort());
+ }
+
+ void OnStickyTransactionCommitted(TTransactionId transactionId)
+ {
+ OnStickyTransactionFinished(transactionId);
+ }
+
+ void OnStickyTransactionAborted(TTransactionId transactionId, const TError& /*error*/)
+ {
+ OnStickyTransactionFinished(transactionId);
+ }
+
+ void OnStickyTransactionFinished(TTransactionId transactionId)
+ {
+ NConcurrency::TLease lease;
+ {
+ auto guard = WriterGuard(StickyTransactionLock_);
+ auto it = IdToStickyTransactionEntry_.find(transactionId);
+ if (it == IdToStickyTransactionEntry_.end()) {
+ return;
+ }
+ lease = it->second.Lease;
+ IdToStickyTransactionEntry_.erase(it);
+ }
+
+ YT_LOG_DEBUG("Sticky transaction unregistered (TransactionId: %v)",
+ transactionId);
+
+ NConcurrency::TLeaseManager::CloseLease(lease);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IStickyTransactionPoolPtr CreateStickyTransactionPool(
+ const NLogging::TLogger& logger)
+{
+ return New<TStickyTransactionPool>(logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/sticky_transaction_pool.h b/yt/yt/client/api/sticky_transaction_pool.h
new file mode 100644
index 0000000000..0e50472006
--- /dev/null
+++ b/yt/yt/client/api/sticky_transaction_pool.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "client.h"
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IStickyTransactionPool
+ : public virtual TRefCounted
+{
+ //! Registers transaction in the pool.
+ virtual ITransactionPtr RegisterTransaction(ITransactionPtr transaction) = 0;
+
+ //! Unregisters transaction from the pool.
+ virtual void UnregisterTransaction(NTransactionClient::TTransactionId transactionId) = 0;
+
+ //! Finds a transaction by id and renews its lease. Returns |nullptr| if transaction is not found.
+ virtual ITransactionPtr FindTransactionAndRenewLease(NTransactionClient::TTransactionId transactionId) = 0;
+
+ //! Finds a transaction by id and renews its lease. Throws if transaction is not found.
+ ITransactionPtr GetTransactionAndRenewLeaseOrThrow(NTransactionClient::TTransactionId transactionId);
+};
+
+DEFINE_REFCOUNTED_TYPE(IStickyTransactionPool)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IStickyTransactionPoolPtr CreateStickyTransactionPool(const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/table_reader.h b/yt/yt/client/api/table_reader.h
new file mode 100644
index 0000000000..d2123db691
--- /dev/null
+++ b/yt/yt/client/api/table_reader.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/unversioned_reader.h>
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/data_statistics.pb.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(babenko): consider joining with NTableClient::IRowBatchReader
+struct ITableReader
+ : public virtual TRefCounted
+{
+ //! Returns the starting row index within the table.
+ virtual i64 GetStartRowIndex() const = 0;
+
+ //! Returns the total (approximate) number of rows readable.
+ virtual i64 GetTotalRowCount() const = 0;
+
+ //! Returns various data statistics.
+ virtual NChunkClient::NProto::TDataStatistics GetDataStatistics() const = 0;
+
+ //! Returns an asynchronous flag enabling to wait until data is available.
+ virtual TFuture<void> GetReadyEvent() = 0;
+
+ //! Attempts to read a bunch of #rows. If true is returned but #rows is empty
+ //! the rows are not immediately available and the client must invoke
+ //! #GetReadyEvent and wait. False is returned if the end of table was reached.
+ virtual NTableClient::IUnversionedRowBatchPtr Read(const NTableClient::TRowBatchReadOptions& options = {}) = 0;
+
+ //! Returns the name table used for constructing rows.
+ virtual const NTableClient::TNameTablePtr& GetNameTable() const = 0;
+
+ //! Returns schema of the table.
+ virtual const NTableClient::TTableSchemaPtr& GetTableSchema() const = 0;
+
+ //! Returns the names of columns that are not accessible according to columnar ACL
+ //! and were omitted. See #TTableReaderOptions::OmitInaccessibleColumns.
+ virtual const std::vector<TString>& GetOmittedInaccessibleColumns() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITableReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/table_writer.h b/yt/yt/client/api/table_writer.h
new file mode 100644
index 0000000000..a9aff96356
--- /dev/null
+++ b/yt/yt/client/api/table_writer.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITableWriter
+ : public virtual TRefCounted
+{
+ //! Attempts to write a bunch of #rows. If false is returned then the rows
+ //! are not accepted and the client must invoke #GetReadyEvent and wait.
+ virtual bool Write(TRange<NTableClient::TUnversionedRow> rows) = 0;
+
+ //! Returns an asynchronous flag enabling to wait until data is written.
+ virtual TFuture<void> GetReadyEvent() = 0;
+
+ //! Closes the writer. Must be the last call to the writer.
+ virtual TFuture<void> Close() = 0;
+
+ //! Returns the name table to be used for constructing rows.
+ virtual const NTableClient::TNameTablePtr& GetNameTable() const = 0;
+
+ //! Returns the schema to be used for constructing rows.
+ virtual const NTableClient::TTableSchemaPtr& GetSchema() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITableWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/transaction-inl.h b/yt/yt/client/api/transaction-inl.h
new file mode 100644
index 0000000000..c2ceb5f388
--- /dev/null
+++ b/yt/yt/client/api/transaction-inl.h
@@ -0,0 +1,42 @@
+#ifndef TRANSACTION_INL_H_
+#error "Direct inclusion of this file is not allowed, include transaction.h"
+// For the sake of sane code completion.
+#include "transaction.h"
+#endif
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TDerivedTransaction>
+TDerivedTransaction* ITransaction::As()
+{
+ auto* derived = dynamic_cast<TDerivedTransaction*>(this);
+ YT_VERIFY(derived);
+ return derived;
+}
+
+template <class TDerivedTransaction>
+TDerivedTransaction* ITransaction::TryAs()
+{
+ return dynamic_cast<TDerivedTransaction*>(this);
+}
+
+template <class TDerivedTransaction>
+const TDerivedTransaction* ITransaction::As() const
+{
+ const auto* derived = dynamic_cast<const TDerivedTransaction*>(this);
+ YT_VERIFY(derived);
+ return derived;
+}
+
+template <class TDerivedTransaction>
+const TDerivedTransaction* ITransaction::TryAs() const
+{
+ return dynamic_cast<const TDerivedTransaction*>(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
diff --git a/yt/yt/client/api/transaction.cpp b/yt/yt/client/api/transaction.cpp
new file mode 100644
index 0000000000..590cca2097
--- /dev/null
+++ b/yt/yt/client/api/transaction.cpp
@@ -0,0 +1,164 @@
+#include "transaction.h"
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/queue_client/consumer_client.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NQueueClient;
+using namespace NConcurrency;
+
+/////////////////////////////////////////////////////////////////////////////
+
+void ITransaction::WriteRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TUnversionedRow> rows,
+ const TModifyRowsOptions& options)
+{
+ std::vector<TRowModification> modifications;
+ modifications.reserve(rows.Size());
+
+ for (auto row : rows) {
+ modifications.push_back({ERowModificationType::Write, row.ToTypeErasedRow(), TLockMask()});
+ }
+
+ ModifyRows(
+ path,
+ std::move(nameTable),
+ MakeSharedRange(std::move(modifications), std::move(rows.ReleaseHolder())),
+ options);
+}
+
+void ITransaction::WriteRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TVersionedRow> rows,
+ const TModifyRowsOptions& options)
+{
+ std::vector<TRowModification> modifications;
+ modifications.reserve(rows.Size());
+
+ for (auto row : rows) {
+ modifications.push_back({ERowModificationType::VersionedWrite, row.ToTypeErasedRow(), TLockMask()});
+ }
+
+ ModifyRows(
+ path,
+ std::move(nameTable),
+ MakeSharedRange(std::move(modifications), std::move(rows.ReleaseHolder())),
+ options);
+}
+
+void ITransaction::DeleteRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TLegacyKey> keys,
+ const TModifyRowsOptions& options)
+{
+ std::vector<TRowModification> modifications;
+ modifications.reserve(keys.Size());
+ for (auto key : keys) {
+ modifications.push_back({ERowModificationType::Delete, key.ToTypeErasedRow(), TLockMask()});
+ }
+
+ ModifyRows(
+ path,
+ std::move(nameTable),
+ MakeSharedRange(std::move(modifications), std::move(keys.ReleaseHolder())),
+ options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ITransaction::LockRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TLegacyKey> keys,
+ TLockMask lockMask)
+{
+ std::vector<TRowModification> modifications;
+ modifications.reserve(keys.Size());
+
+ for (auto key : keys) {
+ TRowModification modification;
+ modification.Type = ERowModificationType::WriteAndLock;
+ modification.Row = key.ToTypeErasedRow();
+ modification.Locks = lockMask;
+ modifications.push_back(modification);
+ }
+
+ ModifyRows(
+ path,
+ std::move(nameTable),
+ MakeSharedRange(std::move(modifications), std::move(keys)),
+ TModifyRowsOptions());
+}
+
+void ITransaction::LockRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TLegacyKey> keys,
+ ELockType lockType)
+{
+ TLockMask lockMask;
+ lockMask.Set(PrimaryLockIndex, lockType);
+ LockRows(path, nameTable, keys, lockMask);
+}
+
+void ITransaction::LockRows(
+ const NYPath::TYPath& path,
+ TNameTablePtr nameTable,
+ TSharedRange<TLegacyKey> keys,
+ const std::vector<TString>& locks,
+ ELockType lockType)
+{
+ const auto& tableMountCache = GetClient()->GetTableMountCache();
+ auto tableInfo = WaitFor(tableMountCache->GetTableInfo(path))
+ .ValueOrThrow();
+
+ auto lockMask = GetLockMask(
+ *tableInfo->Schemas[ETableSchemaKind::Write],
+ GetAtomicity() == NTransactionClient::EAtomicity::Full,
+ locks,
+ lockType);
+
+ LockRows(path, nameTable, keys, lockMask);
+}
+
+void ITransaction::AdvanceConsumer(
+ const NYPath::TYPath& path,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset)
+{
+ THROW_ERROR_EXCEPTION_IF(newOffset < 0, "Queue consumer offset %v cannot be negative", newOffset);
+
+ auto consumerClient = CreateBigRTConsumerClient(GetClient(), path);
+ consumerClient->Advance(MakeStrong(this), partitionIndex, oldOffset, newOffset);
+}
+
+void ITransaction::AdvanceConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset)
+{
+ THROW_ERROR_EXCEPTION_IF(newOffset < 0, "Queue consumer offset %v cannot be negative", newOffset);
+
+ // TODO(achulkov2): Support consumers from any cluster.
+ auto subConsumerClient = CreateSubConsumerClient(GetClient(), consumerPath.GetPath(), queuePath);
+ return subConsumerClient->Advance(MakeStrong(this), partitionIndex, oldOffset, newOffset);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/api/transaction.h b/yt/yt/client/api/transaction.h
new file mode 100644
index 0000000000..1e1a33d6d5
--- /dev/null
+++ b/yt/yt/client/api/transaction.h
@@ -0,0 +1,193 @@
+#pragma once
+
+#include "client.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/versioned_row.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/core/actions/signal.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTransactionFlushResult
+{
+ std::vector<NElection::TCellId> ParticipantCellIds;
+};
+
+//! Either a write or delete.
+struct TRowModification
+{
+ //! Discriminates between writes and deletes.
+ ERowModificationType Type;
+ //! Either a row (for write; versioned or unversioned) or a key (for delete; always unversioned).
+ NTableClient::TTypeErasedRow Row;
+ //! Locks.
+ NTableClient::TLockMask Locks;
+};
+
+struct TModifyRowsOptions
+{
+ //! If this happens to be a modification of a replicated table,
+ //! controls if at least one sync replica is required.
+ bool RequireSyncReplica = true;
+
+ //! For chaos replicated tables indicates if it is necessary to explore other replicas.
+ bool TopmostTransaction = true;
+
+ //! For chaos replicas pass replication card to ensure that all data is sent using same meta info.
+ NChaosClient::TReplicationCardPtr ReplicationCard;
+
+ //! For writes to replicas, this is the id of the replica at the upstream cluster.
+ NTabletClient::TTableReplicaId UpstreamReplicaId;
+
+ //! Modifications are sent asynchronously. Sequential numbering is
+ //! required to restore their order.
+ //!
+ //! This parameter is only used by native client (in particular within RPC proxy server).
+ std::optional<i64> SequenceNumber;
+
+ //! Modifications can be sent from several sources in case of several clients
+ //! attached to the same transaction.
+ //!
+ //! Modifications within one source will be serialized by this source sequence numbers.
+ //! Modifications from different sources will be serialized arbitrarily, that is why
+ //! different sources must send independent modifications.
+ //!
+ //! If sequence number is missing, source id is ignored.
+ //!
+ //! This parameter is only used by native client (in particular within RPC proxy server).
+ i64 SequenceNumberSourceId = 0;
+
+ //! If set treat missing key columns as null.
+ bool AllowMissingKeyColumns = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents a client-controlled transaction.
+/*
+ * Transactions are created by calling IClientBase::Transaction.
+ *
+ * For some table operations (e.g. #WriteRows), the transaction instance
+ * buffers all modifications and flushes them during #Commit. This, in
+ * particular, explains why these methods return |void|.
+ *
+ * Thread affinity: any
+ */
+struct ITransaction
+ : public virtual IClientBase
+{
+ virtual IClientPtr GetClient() const = 0;
+ virtual NTransactionClient::ETransactionType GetType() const = 0;
+ virtual NTransactionClient::TTransactionId GetId() const = 0;
+ virtual NTransactionClient::TTimestamp GetStartTimestamp() const = 0;
+ virtual NTransactionClient::EAtomicity GetAtomicity() const = 0;
+ virtual NTransactionClient::EDurability GetDurability() const = 0;
+ virtual TDuration GetTimeout() const = 0;
+
+ virtual TFuture<void> Ping(const NApi::TTransactionPingOptions& options = {}) = 0;
+ virtual TFuture<TTransactionCommitResult> Commit(const TTransactionCommitOptions& options = TTransactionCommitOptions()) = 0;
+ virtual TFuture<void> Abort(const TTransactionAbortOptions& options = TTransactionAbortOptions()) = 0;
+ virtual void Detach() = 0;
+ virtual TFuture<TTransactionFlushResult> Flush() = 0;
+ virtual void RegisterAlienTransaction(const ITransactionPtr& transaction) = 0;
+
+ using TCommittedHandlerSignature = void();
+ using TCommittedHandler = TCallback<TCommittedHandlerSignature>;
+ DECLARE_INTERFACE_SIGNAL(TCommittedHandlerSignature, Committed);
+
+ using TAbortedHandlerSignature = void(const TError& error);
+ using TAbortedHandler = TCallback<TAbortedHandlerSignature>;
+ DECLARE_INTERFACE_SIGNAL(TAbortedHandlerSignature, Aborted);
+
+ // Verified dynamic casts to a more specific interface.
+
+ template <class TDerivedTransaction>
+ TDerivedTransaction* As();
+ template <class TDerivedTransaction>
+ TDerivedTransaction* TryAs();
+
+ template <class TDerivedTransaction>
+ const TDerivedTransaction* As() const;
+ template <class TDerivedTransaction>
+ const TDerivedTransaction* TryAs() const;
+
+ // Tables.
+
+ void WriteRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TUnversionedRow> rows,
+ const TModifyRowsOptions& options = TModifyRowsOptions());
+
+ void WriteRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TVersionedRow> rows,
+ const TModifyRowsOptions& options = TModifyRowsOptions());
+
+ void DeleteRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TLegacyKey> keys,
+ const TModifyRowsOptions& options = TModifyRowsOptions());
+
+ void LockRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TLegacyKey> keys,
+ NTableClient::TLockMask lockMask);
+
+ void LockRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TLegacyKey> keys,
+ NTableClient::ELockType lockType = NTableClient::ELockType::SharedStrong);
+
+ void LockRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<NTableClient::TLegacyKey> keys,
+ const std::vector<TString>& locks,
+ NTableClient::ELockType lockType = NTableClient::ELockType::SharedStrong);
+
+ virtual void ModifyRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<TRowModification> modifications,
+ const TModifyRowsOptions& options = TModifyRowsOptions()) = 0;
+
+ // Consumers.
+
+ //! Advance the consumer's offset for the given partition, setting it to a new value.
+ //!
+ //! If oldOffset is specified, the current offset is read inside this transaction and compared with oldOffset.
+ //! If they are equal, the new offset is written, otherwise an exception is thrown.
+ void AdvanceConsumer(
+ const NYPath::TYPath& path,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset);
+
+ void AdvanceConsumer(
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset);
+};
+
+DEFINE_REFCOUNTED_TYPE(ITransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
+
+
+#define TRANSACTION_INL_H_
+#include "transaction-inl.h"
+#undef TRANSACTION_INL_H_
diff --git a/yt/yt/client/arrow/arrow_row_stream_decoder.cpp b/yt/yt/client/arrow/arrow_row_stream_decoder.cpp
new file mode 100644
index 0000000000..99277ef173
--- /dev/null
+++ b/yt/yt/client/arrow/arrow_row_stream_decoder.cpp
@@ -0,0 +1,20 @@
+#include "arrow_row_stream_decoder.h"
+
+namespace NYT::NArrow {
+
+using namespace NApi::NRpcProxy;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRowStreamDecoderPtr CreateArrowRowStreamDecoder(
+ TTableSchemaPtr /*schema*/,
+ TNameTablePtr /*nameTable*/)
+{
+ THROW_ERROR_EXCEPTION("Arrow decoder is not implemented yet");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow
+
diff --git a/yt/yt/client/arrow/arrow_row_stream_decoder.h b/yt/yt/client/arrow/arrow_row_stream_decoder.h
new file mode 100644
index 0000000000..be8a7cab29
--- /dev/null
+++ b/yt/yt/client/arrow/arrow_row_stream_decoder.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rpc_proxy/public.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+namespace NYT::NArrow {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::NRpcProxy::IRowStreamDecoderPtr CreateArrowRowStreamDecoder(
+ NTableClient::TTableSchemaPtr schema,
+ NTableClient::TNameTablePtr nameTable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow
diff --git a/yt/yt/client/arrow/arrow_row_stream_encoder.cpp b/yt/yt/client/arrow/arrow_row_stream_encoder.cpp
new file mode 100644
index 0000000000..673f3727ed
--- /dev/null
+++ b/yt/yt/client/arrow/arrow_row_stream_encoder.cpp
@@ -0,0 +1,1061 @@
+#include "arrow_row_stream_encoder.h"
+
+#include <yt/yt/client/arrow/fbs/Message.fbs.h>
+#include <yt/yt/client/arrow/fbs/Schema.fbs.h>
+
+#include <yt/yt/client/api/rpc_proxy/row_stream.h>
+#include <yt/yt/client/api/rpc_proxy/wire_row_stream.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_batch.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/columnar.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/range.h>
+
+#include <util/system/align.h>
+
+namespace NYT::NArrow {
+
+using namespace NApi::NRpcProxy;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = ArrowLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+using TBatchColumn = IUnversionedColumnarRowBatch::TColumn;
+using TBodyWriter = std::function<void(TMutableRef)>;
+
+constexpr i64 ArrowAlignment = 8;
+
+flatbuffers::Offset<flatbuffers::String> SerializeString(
+ flatbuffers::FlatBufferBuilder* flatbufBuilder,
+ const TString& str)
+{
+ return flatbufBuilder->CreateString(str.data(), str.length());
+}
+
+std::tuple<org::apache::arrow::flatbuf::Type, flatbuffers::Offset<void>> SerializeColumnType(
+ flatbuffers::FlatBufferBuilder* flatbufBuilder,
+ const TColumnSchema& schema)
+{
+ auto simpleType = CastToV1Type(schema.LogicalType()).first;
+ switch (simpleType) {
+ case ESimpleLogicalValueType::Null:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_Null,
+ org::apache::arrow::flatbuf::CreateNull(*flatbufBuilder)
+ .Union());
+
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint64:
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Uint32:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_Int,
+ org::apache::arrow::flatbuf::CreateInt(
+ *flatbufBuilder,
+ GetIntegralTypeBitWidth(simpleType),
+ IsIntegralTypeSigned(simpleType)).Union());
+
+ case ESimpleLogicalValueType::Double:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_FloatingPoint,
+ org::apache::arrow::flatbuf::CreateFloatingPoint(
+ *flatbufBuilder,
+ org::apache::arrow::flatbuf::Precision_DOUBLE)
+ .Union());
+
+ case ESimpleLogicalValueType::Boolean:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_Bool,
+ org::apache::arrow::flatbuf::CreateBool(*flatbufBuilder)
+ .Union());
+
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Any:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_Binary,
+ org::apache::arrow::flatbuf::CreateBinary(*flatbufBuilder)
+ .Union());
+
+ case ESimpleLogicalValueType::Utf8:
+ return std::make_tuple(
+ org::apache::arrow::flatbuf::Type_Utf8,
+ org::apache::arrow::flatbuf::CreateUtf8(*flatbufBuilder)
+ .Union());
+
+ // TODO(babenko): the following types are not supported:
+ // Date
+ // Datetime
+ // Interval
+ // Timestamp
+
+ default:
+ THROW_ERROR_EXCEPTION("Column %v has type %Qlv that is not currently supported by Arrow encoder",
+ schema.GetDiagnosticNameString(),
+ simpleType);
+ }
+}
+
+bool IsRleButNotDictionaryEncodedStringLikeColumn(const TBatchColumn& column)
+{
+ auto simpleType = CastToV1Type(column.Type).first;
+ return
+ IsStringLikeType(simpleType) &&
+ column.Rle &&
+ !column.Rle->ValueColumn->Dictionary;
+}
+
+bool IsRleAndDictionaryEncodedColumn(const TBatchColumn& column)
+{
+ return
+ column.Rle &&
+ column.Rle->ValueColumn->Dictionary;
+}
+
+bool IsDictionaryEncodedColumn(const TBatchColumn& column)
+{
+ return
+ column.Dictionary ||
+ IsRleAndDictionaryEncodedColumn(column) ||
+ IsRleButNotDictionaryEncodedStringLikeColumn(column);
+}
+
+struct TTypedBatchColumn
+{
+ const TBatchColumn* Column;
+ TLogicalTypePtr Type;
+};
+
+struct TRecordBatchBodyPart
+{
+ i64 Size;
+ TBodyWriter Writer;
+};
+
+struct TRecordBatchSerializationContext final
+{
+ explicit TRecordBatchSerializationContext(flatbuffers::FlatBufferBuilder* flatbufBuilder)
+ : FlatbufBuilder(flatbufBuilder)
+ { }
+
+ void AddFieldNode(i64 length, i64 nullCount)
+ {
+ FieldNodes.emplace_back(length, nullCount);
+ }
+
+ void AddBuffer(i64 size, TBodyWriter writer)
+ {
+ YT_LOG_DEBUG("Buffer registered (Offset: %v, Size: %v)",
+ CurrentBodyOffset,
+ size);
+
+ Buffers.emplace_back(CurrentBodyOffset, size);
+ CurrentBodyOffset += AlignUp<i64>(size, ArrowAlignment);
+ Parts.push_back(TRecordBatchBodyPart{size, std::move(writer)});
+ }
+
+ flatbuffers::FlatBufferBuilder* const FlatbufBuilder;
+
+ i64 CurrentBodyOffset = 0;
+ std::vector<org::apache::arrow::flatbuf::FieldNode> FieldNodes;
+ std::vector<org::apache::arrow::flatbuf::Buffer> Buffers;
+ std::vector<TRecordBatchBodyPart> Parts;
+};
+
+template <class T>
+TMutableRange<T> GetTypedValues(TMutableRef ref)
+{
+ return MakeMutableRange(
+ reinterpret_cast<T*>(ref.Begin()),
+ reinterpret_cast<T*>(ref.End()));
+}
+
+void SerializeColumnPrologue(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ if (column->NullBitmap ||
+ column->Rle && column->Rle->ValueColumn->NullBitmap)
+ {
+ if (column->Rle) {
+ const auto* valueColumn = column->Rle->ValueColumn;
+ auto rleIndexes = column->GetTypedValues<ui64>();
+
+ context->AddFieldNode(
+ column->ValueCount,
+ CountOnesInRleBitmap(
+ valueColumn->NullBitmap->Data,
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount));
+
+ context->AddBuffer(
+ GetBitmapByteSize(column->ValueCount),
+ [=] (TMutableRef dstRef) {
+ BuildValidityBitmapFromRleNullBitmap(
+ valueColumn->NullBitmap->Data,
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ dstRef);
+ });
+ } else {
+ context->AddFieldNode(
+ column->ValueCount,
+ CountOnesInBitmap(
+ column->NullBitmap->Data,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount));
+
+ context->AddBuffer(
+ GetBitmapByteSize(column->ValueCount),
+ [=] (TMutableRef dstRef) {
+ CopyBitmapRangeToBitmapNegated(
+ column->NullBitmap->Data,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ dstRef);
+ });
+ }
+ } else {
+ context->AddFieldNode(
+ column->ValueCount,
+ 0);
+
+ context->AddBuffer(
+ 0,
+ [=] (TMutableRef /*dstRef*/) { });
+ }
+}
+
+void SerializeRleButNotDictionaryEncodedStringLikeColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(column->Values->BitWidth == 64);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(!column->Values->ZigZagEncoded);
+
+ YT_LOG_DEBUG("Adding RLE but not dictionary-encoded string-like column (ColumnId: %v, StartIndex: %v, ValueCount: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount);
+
+ SerializeColumnPrologue(typedColumn, context);
+
+ auto rleIndexes = column->GetTypedValues<ui64>();
+
+ context->AddBuffer(
+ sizeof (ui32) * column->ValueCount,
+ [=] (TMutableRef dstRef) {
+ BuildIotaDictionaryIndexesFromRleIndexes(
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ GetTypedValues<ui32>(dstRef));
+ });
+}
+
+void SerializeDictionaryColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(column->Dictionary->ZeroMeansNull);
+ YT_VERIFY(column->Values->BitWidth == 32);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(!column->Values->ZigZagEncoded);
+
+ YT_LOG_DEBUG("Adding dictionary column (ColumnId: %v, StartIndex: %v, ValueCount: %v, Rle: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount,
+ column->Rle.has_value());
+
+ auto relevantDictionaryIndexes = column->GetRelevantTypedValues<ui32>();
+
+ context->AddFieldNode(
+ column->ValueCount,
+ CountNullsInDictionaryIndexesWithZeroNull(relevantDictionaryIndexes));
+
+ context->AddBuffer(
+ GetBitmapByteSize(column->ValueCount),
+ [=] (TMutableRef dstRef) {
+ BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ relevantDictionaryIndexes,
+ dstRef);
+ });
+
+ context->AddBuffer(
+ sizeof (ui32) * column->ValueCount,
+ [=] (TMutableRef dstRef) {
+ BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull(
+ relevantDictionaryIndexes,
+ GetTypedValues<ui32>(dstRef));
+ });
+}
+
+void SerializeRleDictionaryColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(column->Values->BitWidth == 64);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(!column->Values->ZigZagEncoded);
+ YT_VERIFY(column->Rle->ValueColumn->Dictionary->ZeroMeansNull);
+ YT_VERIFY(column->Rle->ValueColumn->Values->BitWidth == 32);
+ YT_VERIFY(column->Rle->ValueColumn->Values->BaseValue == 0);
+ YT_VERIFY(!column->Rle->ValueColumn->Values->ZigZagEncoded);
+
+ YT_LOG_DEBUG("Adding dictionary column (ColumnId: %v, StartIndex: %v, ValueCount: %v, Rle: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount,
+ column->Rle.has_value());
+
+ auto dictionaryIndexes = column->Rle->ValueColumn->GetTypedValues<ui32>();
+ auto rleIndexes = column->GetTypedValues<ui64>();
+
+ context->AddFieldNode(
+ column->ValueCount,
+ CountNullsInRleDictionaryIndexesWithZeroNull(
+ dictionaryIndexes,
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount));
+
+ context->AddBuffer(
+ GetBitmapByteSize(column->ValueCount),
+ [=] (TMutableRef dstRef) {
+ BuildValidityBitmapFromRleDictionaryIndexesWithZeroNull(
+ dictionaryIndexes,
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ dstRef);
+ });
+
+ context->AddBuffer(
+ sizeof (ui32) * column->ValueCount,
+ [=] (TMutableRef dstRef) {
+ BuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNull(
+ dictionaryIndexes,
+ rleIndexes,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ GetTypedValues<ui32>(dstRef));
+ });
+}
+
+void SerializeIntegerColumn(
+ const TTypedBatchColumn& typedColumn,
+ ESimpleLogicalValueType simpleType,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+
+ YT_LOG_DEBUG("Adding integer column (ColumnId: %v, StartIndex: %v, ValueCount: %v, Rle: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount,
+ column->Rle.has_value());
+
+ SerializeColumnPrologue(typedColumn, context);
+
+ context->AddBuffer(
+ column->ValueCount * GetIntegralTypeByteSize(simpleType),
+ [=] (TMutableRef dstRef) {
+ const auto* valueColumn = column->Rle
+ ? column->Rle->ValueColumn
+ : column;
+ auto values = valueColumn->GetTypedValues<ui64>();
+
+ auto rleIndexes = column->Rle
+ ? column->GetTypedValues<ui64>()
+ : TRange<ui64>();
+
+ switch (simpleType) {
+ #define XX(cppType, ytType) \
+ case ESimpleLogicalValueType::ytType: { \
+ auto dstValues = GetTypedValues<cppType>(dstRef); \
+ auto* currentOutput = dstValues.Begin(); \
+ DecodeIntegerVector( \
+ column->StartIndex, \
+ column->StartIndex + column->ValueCount, \
+ valueColumn->Values->BaseValue, \
+ valueColumn->Values->ZigZagEncoded, \
+ TRange<ui32>(), \
+ rleIndexes, \
+ [&] (auto index) { \
+ return values[index]; \
+ }, \
+ [&] (auto value) { \
+ *currentOutput++ = value; \
+ }); \
+ break; \
+ }
+
+ XX( i8, Int8)
+ XX( i16, Int16)
+ XX( i32, Int32)
+ XX( i64, Int64)
+ XX( ui8, Uint8)
+ XX(ui16, Uint16)
+ XX(ui32, Uint32)
+ XX(ui64, Uint64)
+
+ #undef XX
+
+ default:
+ THROW_ERROR_EXCEPTION("Integer column %v has unexpected type %Qlv",
+ typedColumn.Column->Id,
+ simpleType);
+ }
+ });
+}
+
+void SerializeDoubleColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(column->Values->BitWidth == 64);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(!column->Values->ZigZagEncoded);
+
+ YT_LOG_DEBUG("Adding double column (ColumnId: %v, StartIndex: %v, ValueCount: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount,
+ column->Rle.has_value());
+
+ SerializeColumnPrologue(typedColumn, context);
+
+ context->AddBuffer(
+ column->ValueCount * sizeof(double),
+ [=] (TMutableRef dstRef) {
+ auto relevantValues = column->GetRelevantTypedValues<double>();
+ ::memcpy(
+ dstRef.Begin(),
+ relevantValues.Begin(),
+ column->ValueCount * sizeof(double));
+ });
+}
+
+void SerializeStringLikeColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(column->Values->BitWidth == 32);
+ YT_VERIFY(column->Values->ZigZagEncoded);
+ YT_VERIFY(column->Strings);
+ YT_VERIFY(column->Strings->AvgLength);
+ YT_VERIFY(!column->Rle);
+
+ auto startIndex = column->StartIndex;
+ auto endIndex = startIndex + column->ValueCount;
+ auto stringData = column->Strings->Data;
+ auto avgLength = *column->Strings->AvgLength;
+
+ auto offsets = column->GetTypedValues<ui32>();
+ auto startOffset = DecodeStringOffset(offsets, avgLength, startIndex);
+ auto endOffset = DecodeStringOffset(offsets, avgLength, endIndex);
+ auto stringsSize = endOffset - startOffset;
+
+ YT_LOG_DEBUG("Adding string-like column (ColumnId: %v, StartIndex: %v, ValueCount: %v, StartOffset: %v, EndOffset: %v, StringsSize: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount,
+ startOffset,
+ endOffset,
+ stringsSize);
+
+ SerializeColumnPrologue(typedColumn, context);
+
+ context->AddBuffer(
+ sizeof(i32) * (column->ValueCount + 1),
+ [=] (TMutableRef dstRef) {
+ DecodeStringOffsets(
+ offsets,
+ avgLength,
+ startIndex,
+ endIndex,
+ GetTypedValues<ui32>(dstRef));
+ });
+
+ context->AddBuffer(
+ stringsSize,
+ [=] (TMutableRef dstRef) {
+ ::memcpy(
+ dstRef.Begin(),
+ stringData.Begin() + startOffset,
+ stringsSize);
+ });
+}
+
+void SerializeBooleanColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+ YT_VERIFY(column->Values);
+ YT_VERIFY(!column->Values->ZigZagEncoded);
+ YT_VERIFY(column->Values->BaseValue == 0);
+ YT_VERIFY(column->Values->BitWidth == 1);
+
+ YT_LOG_DEBUG("Adding boolean column (ColumnId: %v, StartIndex: %v, ValueCount: %v)",
+ column->Id,
+ column->StartIndex,
+ column->ValueCount);
+
+ SerializeColumnPrologue(typedColumn, context);
+
+ context->AddBuffer(
+ GetBitmapByteSize(column->ValueCount),
+ [=] (TMutableRef dstRef) {
+ CopyBitmapRangeToBitmap(
+ column->Values->Data,
+ column->StartIndex,
+ column->StartIndex + column->ValueCount,
+ dstRef);
+ });
+}
+
+void SerializeColumn(
+ const TTypedBatchColumn& typedColumn,
+ TRecordBatchSerializationContext* context)
+{
+ const auto* column = typedColumn.Column;
+
+ if (IsRleButNotDictionaryEncodedStringLikeColumn(*typedColumn.Column)) {
+ SerializeRleButNotDictionaryEncodedStringLikeColumn(typedColumn, context);
+ return;
+ }
+
+ if (column->Dictionary) {
+ SerializeDictionaryColumn(typedColumn, context);
+ return;
+ }
+
+ if (column->Rle && column->Rle->ValueColumn->Dictionary) {
+ SerializeRleDictionaryColumn(typedColumn, context);
+ return;
+ }
+
+ auto simpleType = CastToV1Type(typedColumn.Type).first;
+ if (IsIntegralType(simpleType)) {
+ SerializeIntegerColumn(typedColumn, simpleType, context);
+ } else if (simpleType == ESimpleLogicalValueType::Double) {
+ SerializeDoubleColumn(typedColumn, context);
+ } else if (IsStringLikeType(simpleType)) {
+ SerializeStringLikeColumn(typedColumn, context);
+ } else if (simpleType == ESimpleLogicalValueType::Boolean) {
+ SerializeBooleanColumn(typedColumn, context);
+ } else if (simpleType == ESimpleLogicalValueType::Null) {
+ // No buffers are allocated for null columns.
+ } else {
+ THROW_ERROR_EXCEPTION("Column %v has unexpected type %Qlv",
+ typedColumn.Column->Id,
+ simpleType);
+ }
+}
+
+auto SerializeRecordBatch(
+ flatbuffers::FlatBufferBuilder* flatbufBuilder,
+ int length,
+ TRange<TTypedBatchColumn> typedColumns)
+{
+ auto context = New<TRecordBatchSerializationContext>(flatbufBuilder);
+
+ for (const auto& typedColumn : typedColumns) {
+ SerializeColumn(typedColumn, context.Get());
+ }
+
+ auto fieldNodesOffset = flatbufBuilder->CreateVectorOfStructs(context->FieldNodes);
+
+ auto buffersOffset = flatbufBuilder->CreateVectorOfStructs(context->Buffers);
+
+ auto recordBatchOffset = org::apache::arrow::flatbuf::CreateRecordBatch(
+ *flatbufBuilder,
+ length,
+ fieldNodesOffset,
+ buffersOffset);
+
+ auto totalSize = context->CurrentBodyOffset;
+
+ return std::make_tuple(
+ recordBatchOffset,
+ totalSize,
+ [context = std::move(context)] (TMutableRef dstRef) {
+ char* current = dstRef.Begin();
+ for (const auto& part : context->Parts) {
+ part.Writer(TMutableRef(current, current + part.Size));
+ current += AlignUp<i64>(part.Size, ArrowAlignment);
+ }
+ YT_VERIFY(current == dstRef.End());
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TArrowRowStreamEncoder)
+
+class TArrowRowStreamEncoder
+ : public IRowStreamEncoder
+{
+public:
+ TArrowRowStreamEncoder(
+ TTableSchemaPtr schema,
+ TNameTablePtr nameTable,
+ IRowStreamEncoderPtr fallbackEncoder)
+ : Schema_(std::move(schema))
+ , NameTable_(std::move(nameTable))
+ , FallbackEncoder_(std::move(fallbackEncoder))
+ {
+ YT_LOG_DEBUG("Row stream encoder created (Schema: %v)",
+ *Schema_);
+ }
+
+ const TTableSchemaPtr& GetSchema()
+ {
+ return Schema_;
+ }
+
+ const TNameTablePtr& GetNameTable()
+ {
+ return NameTable_;
+ }
+
+ bool IsFirstBatch()
+ {
+ return FirstBatch_;
+ }
+
+ std::vector<IUnversionedColumnarRowBatch::TDictionaryId>& ArrowDictionaryIds()
+ {
+ return ArrowDictionaryIds_;
+ }
+
+ TSharedRef Encode(
+ const IUnversionedRowBatchPtr& batch,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics* statistics) override;
+
+private:
+ const TTableSchemaPtr Schema_;
+ const TNameTablePtr NameTable_;
+ const IRowStreamEncoderPtr FallbackEncoder_;
+
+ bool FirstBatch_ = true;
+ std::vector<IUnversionedColumnarRowBatch::TDictionaryId> ArrowDictionaryIds_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TArrowRowStreamEncoder)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TArrowRowStreamBlockEncoder
+{
+public:
+ TArrowRowStreamBlockEncoder(
+ TArrowRowStreamEncoderPtr streamEncoder,
+ IUnversionedColumnarRowBatchPtr batch)
+ : StreamEncoder_(std::move(streamEncoder))
+ , Batch_(std::move(batch))
+ {
+ PrepareColumns();
+ if (IsSchemaMessageNeeded()) {
+ if (!StreamEncoder_->IsFirstBatch()) {
+ RegisterEosMarker();
+ }
+ ResetArrowDictionaries();
+ PrepareSchema();
+ }
+ PrepareDictionaryBatches();
+ PrepareRecordBatch();
+ }
+
+ i64 GetPayloadSize() const
+ {
+ i64 size = 0;
+ for (const auto& message : Messages_) {
+ size += sizeof (ui32); // continuation indicator
+ size += sizeof (ui32); // metadata size
+ if (message.FlatbufBuilder) {
+ size += AlignUp<i64>(message.FlatbufBuilder->GetSize(), ArrowAlignment); // metadata message
+ size += AlignUp<i64>(message.BodySize, ArrowAlignment); // body
+ }
+ }
+ return size;
+ }
+
+ void WritePayload(TMutableRef payloadRef)
+ {
+ YT_LOG_DEBUG("Started writing payload (Size: %v)",
+ payloadRef.Size());
+ char* current = payloadRef.Begin();
+ for (const auto& message : Messages_) {
+ // Continuation indicator
+ *reinterpret_cast<ui32*>(current) = 0xFFFFFFFF;
+ current += sizeof(ui32);
+
+ if (message.FlatbufBuilder) {
+ auto metadataSize = message.FlatbufBuilder->GetSize();
+ auto* metadataPtr = message.FlatbufBuilder->GetBufferPointer();
+
+ // Metadata size
+ *reinterpret_cast<ui32*>(current) = AlignUp<i64>(metadataSize, ArrowAlignment);
+ current += sizeof(ui32);
+
+ // Metadata message
+ ::memcpy(current, metadataPtr, metadataSize);
+ current += AlignUp<i64>(metadataSize, ArrowAlignment);
+
+ // Body
+ if (message.BodyWriter) {
+ message.BodyWriter(TMutableRef(current, current + message.BodySize));
+ current += AlignUp<i64>(message.BodySize, ArrowAlignment);
+ } else {
+ YT_VERIFY(message.BodySize == 0);
+ }
+ } else {
+ // EOS marker
+ *reinterpret_cast<ui32*>(current) = 0;
+ current += sizeof(ui32);
+ }
+ }
+ YT_VERIFY(current == payloadRef.End());
+ YT_LOG_DEBUG("Finished writing payload");
+ }
+
+private:
+ const TArrowRowStreamEncoderPtr StreamEncoder_;
+ const IUnversionedColumnarRowBatchPtr Batch_;
+
+ std::vector<TTypedBatchColumn> TypedColumns_;
+
+ struct TMessage
+ {
+ std::optional<flatbuffers::FlatBufferBuilder> FlatbufBuilder;
+ i64 BodySize;
+ TBodyWriter BodyWriter;
+ };
+
+ std::vector<TMessage> Messages_;
+
+
+ void RegisterEosMarker()
+ {
+ YT_LOG_DEBUG("EOS marker registered");
+
+ Messages_.push_back(TMessage{
+ std::nullopt,
+ 0,
+ TBodyWriter()
+ });
+ }
+
+ void RegisterMessage(
+ org::apache::arrow::flatbuf::MessageHeader type,
+ flatbuffers::FlatBufferBuilder&& flatbufBuilder,
+ i64 bodySize = 0,
+ std::function<void(TMutableRef)> bodyWriter = nullptr)
+ {
+ YT_LOG_DEBUG("Message registered (Type: %v, MessageSize: %v, BodySize: %v)",
+ org::apache::arrow::flatbuf::EnumNamesMessageHeader()[type],
+ flatbufBuilder.GetSize(),
+ bodySize);
+
+ YT_VERIFY((bodySize % ArrowAlignment) == 0);
+ Messages_.push_back(TMessage{
+ std::move(flatbufBuilder),
+ bodySize,
+ std::move(bodyWriter)
+ });
+ }
+
+ const TColumnSchema& GetColumnSchema(const TBatchColumn& column)
+ {
+ YT_VERIFY(column.Id >= 0);
+ auto name = StreamEncoder_->GetNameTable()->GetName(column.Id);
+ return StreamEncoder_->GetSchema()->GetColumn(name);
+ }
+
+ void PrepareColumns()
+ {
+ auto batchColumns = Batch_->MaterializeColumns();
+ TypedColumns_.reserve(batchColumns.Size());
+ for (const auto* column : batchColumns) {
+ TypedColumns_.push_back(TTypedBatchColumn{
+ column,
+ GetColumnSchema(*column).LogicalType()
+ });
+ }
+ }
+
+ bool IsSchemaMessageNeeded()
+ {
+ if (StreamEncoder_->IsFirstBatch()) {
+ return true;
+ }
+
+ YT_VERIFY(StreamEncoder_->ArrowDictionaryIds().size() == TypedColumns_.size());
+
+ bool result = StreamEncoder_->IsFirstBatch();
+ for (int index = 0; index < std::ssize(TypedColumns_); ++index) {
+ bool currentDictionary = IsDictionaryEncodedColumn(*TypedColumns_[index].Column);
+ bool previousDictionary = StreamEncoder_->ArrowDictionaryIds()[index] != IUnversionedColumnarRowBatch::NullDictionaryId;
+ if (currentDictionary != previousDictionary) {
+ result = true;
+ }
+ }
+ return result;
+ }
+
+ void ResetArrowDictionaries()
+ {
+ StreamEncoder_->ArrowDictionaryIds().assign(TypedColumns_.size(), IUnversionedColumnarRowBatch::NullDictionaryId);
+ }
+
+
+ void PrepareSchema()
+ {
+ flatbuffers::FlatBufferBuilder flatbufBuilder;
+
+ int arrowDictionaryIdCounter = 0;
+ std::vector<flatbuffers::Offset<org::apache::arrow::flatbuf::Field>> fieldOffsets;
+
+ for (const auto& typedColumn : TypedColumns_) {
+ const auto& columnSchema = GetColumnSchema(*typedColumn.Column);
+
+ auto nameOffset = SerializeString(&flatbufBuilder, columnSchema.Name());
+
+ auto [typeType, typeOffset] = SerializeColumnType(&flatbufBuilder, columnSchema);
+
+ flatbuffers::Offset<org::apache::arrow::flatbuf::DictionaryEncoding> dictionaryEncodingOffset;
+ if (IsDictionaryEncodedColumn(*typedColumn.Column)) {
+ dictionaryEncodingOffset = org::apache::arrow::flatbuf::CreateDictionaryEncoding(
+ flatbufBuilder,
+ arrowDictionaryIdCounter++);
+ }
+
+ auto fieldOffset = org::apache::arrow::flatbuf::CreateField(
+ flatbufBuilder,
+ nameOffset,
+ columnSchema.LogicalType()->IsNullable(),
+ typeType,
+ typeOffset,
+ dictionaryEncodingOffset);
+
+ fieldOffsets.push_back(fieldOffset);
+ }
+
+ auto fieldsOffset = flatbufBuilder.CreateVector(fieldOffsets);
+
+ auto schemaOffset = org::apache::arrow::flatbuf::CreateSchema(
+ flatbufBuilder,
+ org::apache::arrow::flatbuf::Endianness_Little,
+ fieldsOffset);
+
+ auto messageOffset = org::apache::arrow::flatbuf::CreateMessage(
+ flatbufBuilder,
+ org::apache::arrow::flatbuf::MetadataVersion_V4,
+ org::apache::arrow::flatbuf::MessageHeader_Schema,
+ schemaOffset.Union(),
+ 0);
+
+ flatbufBuilder.Finish(messageOffset);
+
+ RegisterMessage(
+ org::apache::arrow::flatbuf::MessageHeader_Schema,
+ std::move(flatbufBuilder));
+ }
+
+ void PrepareDictionaryBatches()
+ {
+ int arrowDictionaryIdCounter = 0;
+ auto prepareDictionaryBatch = [&] (
+ int columnIndex,
+ IUnversionedColumnarRowBatch::TDictionaryId ytDictionaryId,
+ const TBatchColumn* dictionaryColumn)
+ {
+ int arrowDictionaryId = arrowDictionaryIdCounter++;
+ const auto& typedColumn = TypedColumns_[columnIndex];
+ auto previousYTDictionaryId = StreamEncoder_->ArrowDictionaryIds()[columnIndex];
+ if (ytDictionaryId == previousYTDictionaryId) {
+ YT_LOG_DEBUG("Reusing previous dictionary (ColumnId: %v, YTDictionaryId: %v, ArrowDictionaryId: %v)",
+ typedColumn.Column->Id,
+ ytDictionaryId,
+ arrowDictionaryId);
+ } else {
+ YT_LOG_DEBUG("Sending new dictionary (ColumnId: %v, YTDictionaryId: %v, ArrowDictionaryId: %v)",
+ typedColumn.Column->Id,
+ ytDictionaryId,
+ arrowDictionaryId);
+ PrepareDictionaryBatch(
+ TTypedBatchColumn{dictionaryColumn, typedColumn.Type},
+ arrowDictionaryId);
+ StreamEncoder_->ArrowDictionaryIds()[columnIndex] = ytDictionaryId;
+ }
+ };
+
+ for (int columnIndex = 0; columnIndex < std::ssize(TypedColumns_); ++columnIndex) {
+ const auto& typedColumn = TypedColumns_[columnIndex];
+ if (typedColumn.Column->Dictionary) {
+ YT_LOG_DEBUG("Adding dictionary batch for dictionary-encoded column (ColumnId: %v)",
+ typedColumn.Column->Id);
+ prepareDictionaryBatch(
+ columnIndex,
+ typedColumn.Column->Dictionary->DictionaryId,
+ typedColumn.Column->Dictionary->ValueColumn);
+ } else if (IsRleButNotDictionaryEncodedStringLikeColumn(*typedColumn.Column)) {
+ YT_LOG_DEBUG("Adding dictionary batch for RLE but not dictionary-encoded string-like column (ColumnId: %v)",
+ typedColumn.Column->Id);
+ prepareDictionaryBatch(
+ columnIndex,
+ IUnversionedColumnarRowBatch::GenerateDictionaryId(), // any unique one will do
+ typedColumn.Column->Rle->ValueColumn);
+ } else if (IsRleAndDictionaryEncodedColumn(*typedColumn.Column)) {
+ YT_LOG_DEBUG("Adding dictionary batch for RLE and dictionary-encoded column (ColumnId: %v)",
+ typedColumn.Column->Id);
+ prepareDictionaryBatch(
+ columnIndex,
+ typedColumn.Column->Rle->ValueColumn->Dictionary->DictionaryId,
+ typedColumn.Column->Rle->ValueColumn->Dictionary->ValueColumn);
+ }
+ }
+ }
+
+ void PrepareDictionaryBatch(
+ const TTypedBatchColumn& typedColumn,
+ int arrowDictionaryId)
+ {
+ flatbuffers::FlatBufferBuilder flatbufBuilder;
+
+ auto [recordBatchOffset, bodySize, bodyWriter] = SerializeRecordBatch(
+ &flatbufBuilder,
+ typedColumn.Column->ValueCount,
+ MakeRange({typedColumn}));
+
+ auto dictionaryBatchOffset = org::apache::arrow::flatbuf::CreateDictionaryBatch(
+ flatbufBuilder,
+ arrowDictionaryId,
+ recordBatchOffset);
+
+ auto messageOffset = org::apache::arrow::flatbuf::CreateMessage(
+ flatbufBuilder,
+ org::apache::arrow::flatbuf::MetadataVersion_V4,
+ org::apache::arrow::flatbuf::MessageHeader_DictionaryBatch,
+ dictionaryBatchOffset.Union(),
+ bodySize);
+
+ flatbufBuilder.Finish(messageOffset);
+
+ RegisterMessage(
+ org::apache::arrow::flatbuf::MessageHeader_DictionaryBatch,
+ std::move(flatbufBuilder),
+ bodySize,
+ std::move(bodyWriter));
+ }
+
+ void PrepareRecordBatch()
+ {
+ flatbuffers::FlatBufferBuilder flatbufBuilder;
+
+ auto [recordBatchOffset, bodySize, bodyWriter] = SerializeRecordBatch(
+ &flatbufBuilder,
+ Batch_->GetRowCount(),
+ TypedColumns_);
+
+ auto messageOffset = org::apache::arrow::flatbuf::CreateMessage(
+ flatbufBuilder,
+ org::apache::arrow::flatbuf::MetadataVersion_V4,
+ org::apache::arrow::flatbuf::MessageHeader_RecordBatch,
+ recordBatchOffset.Union(),
+ bodySize);
+
+ flatbufBuilder.Finish(messageOffset);
+
+ RegisterMessage(
+ org::apache::arrow::flatbuf::MessageHeader_RecordBatch,
+ std::move(flatbufBuilder),
+ bodySize,
+ std::move(bodyWriter));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef TArrowRowStreamEncoder::Encode(
+ const IUnversionedRowBatchPtr& batch,
+ const NApi::NRpcProxy::NProto::TRowsetStatistics* statistics)
+{
+ auto columnarBatch = batch->TryAsColumnar();
+ if (!columnarBatch) {
+ YT_LOG_DEBUG("Encoding non-columnar batch; running fallback");
+ return FallbackEncoder_->Encode(batch, statistics);
+ }
+
+ YT_LOG_DEBUG("Encoding columnar batch (RowCount: %v)",
+ batch->GetRowCount());
+
+ NApi::NRpcProxy::NProto::TRowsetDescriptor descriptor;
+ descriptor.set_wire_format_version(NApi::NRpcProxy::CurrentWireFormatVersion);
+ descriptor.set_rowset_kind(NApi::NRpcProxy::NProto::RK_UNVERSIONED);
+ descriptor.set_rowset_format(NApi::NRpcProxy::NProto::RF_ARROW);
+
+ TArrowRowStreamBlockEncoder blockEncoder(this, std::move(columnarBatch));
+
+ auto [block, payloadRef] = SerializeRowStreamBlockEnvelope(
+ blockEncoder.GetPayloadSize(),
+ descriptor,
+ statistics);
+
+ blockEncoder.WritePayload(payloadRef);
+
+ FirstBatch_ = false;
+
+ return block;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+IRowStreamEncoderPtr CreateArrowRowStreamEncoder(
+ TTableSchemaPtr schema,
+ TNameTablePtr nameTable,
+ IRowStreamEncoderPtr fallbackEncoder)
+{
+ return New<TArrowRowStreamEncoder>(
+ std::move(schema),
+ std::move(nameTable),
+ std::move(fallbackEncoder));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow
diff --git a/yt/yt/client/arrow/arrow_row_stream_encoder.h b/yt/yt/client/arrow/arrow_row_stream_encoder.h
new file mode 100644
index 0000000000..6131b9c397
--- /dev/null
+++ b/yt/yt/client/arrow/arrow_row_stream_encoder.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rpc_proxy/public.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+namespace NYT::NArrow {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::NRpcProxy::IRowStreamEncoderPtr CreateArrowRowStreamEncoder(
+ NTableClient::TTableSchemaPtr schema,
+ NTableClient::TNameTablePtr nameTable,
+ NApi::NRpcProxy::IRowStreamEncoderPtr fallbackEncoder);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow
diff --git a/yt/yt/client/arrow/fbs/Message.fbs b/yt/yt/client/arrow/fbs/Message.fbs
new file mode 100644
index 0000000000..ed43187a6c
--- /dev/null
+++ b/yt/yt/client/arrow/fbs/Message.fbs
@@ -0,0 +1,140 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+include "Schema.fbs";
+include "SparseTensor.fbs";
+include "Tensor.fbs";
+
+namespace org.apache.arrow.flatbuf;
+
+/// ----------------------------------------------------------------------
+/// Data structures for describing a table row batch (a collection of
+/// equal-length Arrow arrays)
+
+/// Metadata about a field at some level of a nested type tree (but not
+/// its children).
+///
+/// For example, a List<Int16> with values [[1, 2, 3], null, [4], [5, 6], null]
+/// would have {length: 5, null_count: 2} for its List node, and {length: 6,
+/// null_count: 0} for its Int16 node, as separate FieldNode structs
+struct FieldNode {
+ /// The number of value slots in the Arrow array at this level of a nested
+ /// tree
+ length: long;
+
+ /// The number of observed nulls. Fields with null_count == 0 may choose not
+ /// to write their physical validity bitmap out as a materialized buffer,
+ /// instead setting the length of the bitmap buffer to 0.
+ null_count: long;
+}
+
+enum CompressionType:byte {
+ // LZ4 frame format, for portability, as provided by lz4frame.h or wrappers
+ // thereof. Not to be confused with "raw" (also called "block") format
+ // provided by lz4.h
+ LZ4_FRAME,
+
+ // Zstandard
+ ZSTD
+}
+
+/// Provided for forward compatibility in case we need to support different
+/// strategies for compressing the IPC message body (like whole-body
+/// compression rather than buffer-level) in the future
+enum BodyCompressionMethod:byte {
+ /// Each constituent buffer is first compressed with the indicated
+ /// compressor, and then written with the uncompressed length in the first 8
+ /// bytes as a 64-bit little-endian signed integer followed by the compressed
+ /// buffer bytes (and then padding as required by the protocol). The
+ /// uncompressed length may be set to -1 to indicate that the data that
+ /// follows is not compressed, which can be useful for cases where
+ /// compression does not yield appreciable savings.
+ BUFFER
+}
+
+/// Optional compression for the memory buffers constituting IPC message
+/// bodies. Intended for use with RecordBatch but could be used for other
+/// message types
+table BodyCompression {
+ /// Compressor library
+ codec: CompressionType = LZ4_FRAME;
+
+ /// Indicates the way the record batch body was compressed
+ method: BodyCompressionMethod = BUFFER;
+}
+
+/// A data header describing the shared memory layout of a "record" or "row"
+/// batch. Some systems call this a "row batch" internally and others a "record
+/// batch".
+table RecordBatch {
+ /// number of records / rows. The arrays in the batch should all have this
+ /// length
+ length: long;
+
+ /// Nodes correspond to the pre-ordered flattened logical schema
+ nodes: [FieldNode];
+
+ /// Buffers correspond to the pre-ordered flattened buffer tree
+ ///
+ /// The number of buffers appended to this list depends on the schema. For
+ /// example, most primitive arrays will have 2 buffers, 1 for the validity
+ /// bitmap and 1 for the values. For struct arrays, there will only be a
+ /// single buffer for the validity (nulls) bitmap
+ buffers: [Buffer];
+
+ /// Optional compression of the message body
+ compression: BodyCompression;
+}
+
+/// For sending dictionary encoding information. Any Field can be
+/// dictionary-encoded, but in this case none of its children may be
+/// dictionary-encoded.
+/// There is one vector / column per dictionary, but that vector / column
+/// may be spread across multiple dictionary batches by using the isDelta
+/// flag
+
+table DictionaryBatch {
+ id: long;
+ data: RecordBatch;
+
+ /// If isDelta is true the values in the dictionary are to be appended to a
+ /// dictionary with the indicated id. If isDelta is false this dictionary
+ /// should replace the existing dictionary.
+ isDelta: bool = false;
+}
+
+/// ----------------------------------------------------------------------
+/// The root Message type
+
+/// This union enables us to easily send different message types without
+/// redundant storage, and in the future we can easily add new message types.
+///
+/// Arrow implementations do not need to implement all of the message types,
+/// which may include experimental metadata types. For maximum compatibility,
+/// it is best to send data using RecordBatch
+union MessageHeader {
+ Schema, DictionaryBatch, RecordBatch, Tensor, SparseTensor
+}
+
+table Message {
+ version: org.apache.arrow.flatbuf.MetadataVersion;
+ header: MessageHeader;
+ bodyLength: long;
+ custom_metadata: [ KeyValue ];
+}
+
+root_type Message; \ No newline at end of file
diff --git a/yt/yt/client/arrow/fbs/Schema.fbs b/yt/yt/client/arrow/fbs/Schema.fbs
new file mode 100644
index 0000000000..48c6d8789e
--- /dev/null
+++ b/yt/yt/client/arrow/fbs/Schema.fbs
@@ -0,0 +1,353 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/// Logical types, vector layouts, and schemas
+
+namespace org.apache.arrow.flatbuf;
+
+enum MetadataVersion:short {
+ /// 0.1.0
+ V1,
+
+ /// 0.2.0
+ V2,
+
+ /// 0.3.0 -> 0.7.1
+ V3,
+
+ /// >= 0.8.0
+ V4,
+}
+
+/// These are stored in the flatbuffer in the Type union below
+
+table Null {
+}
+
+/// A Struct_ in the flatbuffer metadata is the same as an Arrow Struct
+/// (according to the physical memory layout). We used Struct_ here as
+/// Struct is a reserved word in Flatbuffers
+table Struct_ {
+}
+
+table List {
+}
+
+/// Same as List, but with 64-bit offsets, allowing to represent
+/// extremely large data values.
+table LargeList {
+}
+
+table FixedSizeList {
+ /// Number of list items per value
+ listSize: int;
+}
+
+/// A Map is a logical nested type that is represented as
+///
+/// List<entries: Struct<key: K, value: V>>
+///
+/// In this layout, the keys and values are each respectively contiguous. We do
+/// not constrain the key and value types, so the application is responsible
+/// for ensuring that the keys are hashable and unique. Whether the keys are sorted
+/// may be set in the metadata for this field.
+///
+/// In a field with Map type, the field has a child Struct field, which then
+/// has two children: key type and the second the value type. The names of the
+/// child fields may be respectively "entries", "key", and "value", but this is
+/// not enforced.
+///
+/// Map
+/// - child[0] entries: Struct
+/// - child[0] key: K
+/// - child[1] value: V
+///
+/// Neither the "entries" field nor the "key" field may be nullable.
+///
+/// The metadata is structured so that Arrow systems without special handling
+/// for Map can make Map an alias for List. The "layout" attribute for the Map
+/// field must have the same contents as a List.
+table Map {
+ /// Set to true if the keys within each value are sorted
+ keysSorted: bool;
+}
+
+enum UnionMode:short { Sparse, Dense }
+
+/// A union is a complex type with children in Field
+/// By default ids in the type vector refer to the offsets in the children
+/// optionally typeIds provides an indirection between the child offset and the type id
+/// for each child typeIds[offset] is the id used in the type vector
+table Union {
+ mode: UnionMode;
+ typeIds: [ int ]; // optional, describes typeid of each child.
+}
+
+table Int {
+ bitWidth: int; // restricted to 8, 16, 32, and 64 in v1
+ is_signed: bool;
+}
+
+enum Precision:short {HALF, SINGLE, DOUBLE}
+
+table FloatingPoint {
+ precision: Precision;
+}
+
+/// Unicode with UTF-8 encoding
+table Utf8 {
+}
+
+/// Opaque binary data
+table Binary {
+}
+
+/// Same as Utf8, but with 64-bit offsets, allowing to represent
+/// extremely large data values.
+table LargeUtf8 {
+}
+
+/// Same as Binary, but with 64-bit offsets, allowing to represent
+/// extremely large data values.
+table LargeBinary {
+}
+
+table FixedSizeBinary {
+ /// Number of bytes per value
+ byteWidth: int;
+}
+
+table Bool {
+}
+
+table Decimal {
+ /// Total number of decimal digits
+ precision: int;
+ /// Number of digits after the decimal point "."
+ scale: int;
+}
+
+enum DateUnit: short {
+ DAY,
+ MILLISECOND
+}
+
+/// Date is either a 32-bit or 64-bit type representing elapsed time since UNIX
+/// epoch (1970-01-01), stored in either of two units:
+///
+/// * Milliseconds (64 bits) indicating UNIX time elapsed since the epoch (no
+/// leap seconds), where the values are evenly divisible by 86400000
+/// * Days (32 bits) since the UNIX epoch
+table Date {
+ unit: DateUnit = MILLISECOND;
+}
+
+enum TimeUnit: short { SECOND, MILLISECOND, MICROSECOND, NANOSECOND }
+
+/// Time type. The physical storage type depends on the unit
+/// - SECOND and MILLISECOND: 32 bits
+/// - MICROSECOND and NANOSECOND: 64 bits
+table Time {
+ unit: TimeUnit = MILLISECOND;
+ bitWidth: int = 32;
+}
+
+/// Time elapsed from the Unix epoch, 00:00:00.000 on 1 January 1970, excluding
+/// leap seconds, as a 64-bit integer. Note that UNIX time does not include
+/// leap seconds.
+///
+/// The Timestamp metadata supports both "time zone naive" and "time zone
+/// aware" timestamps. Read about the timezone attribute for more detail
+table Timestamp {
+ unit: TimeUnit;
+
+ /// The time zone is a string indicating the name of a time zone, one of:
+ ///
+ /// * As used in the Olson time zone database (the "tz database" or
+ /// "tzdata"), such as "America/New_York"
+ /// * An absolute time zone offset of the form +XX:XX or -XX:XX, such as +07:30
+ ///
+ /// Whether a timezone string is present indicates different semantics about
+ /// the data:
+ ///
+ /// * If the time zone is null or equal to an empty string, the data is "time
+ /// zone naive" and shall be displayed *as is* to the user, not localized
+ /// to the locale of the user. This data can be though of as UTC but
+ /// without having "UTC" as the time zone, it is not considered to be
+ /// localized to any time zone
+ ///
+ /// * If the time zone is set to a valid value, values can be displayed as
+ /// "localized" to that time zone, even though the underlying 64-bit
+ /// integers are identical to the same data stored in UTC. Converting
+ /// between time zones is a metadata-only operation and does not change the
+ /// underlying values
+ timezone: string;
+}
+
+enum IntervalUnit: short { YEAR_MONTH, DAY_TIME}
+// A "calendar" interval which models types that don't necessarily
+// have a precise duration without the context of a base timestamp (e.g.
+// days can differ in length during day light savings time transitions).
+// YEAR_MONTH - Indicates the number of elapsed whole months, stored as
+// 4-byte integers.
+// DAY_TIME - Indicates the number of elapsed days and milliseconds,
+// stored as 2 contiguous 32-bit integers (8-bytes in total). Support
+// of this IntervalUnit is not required for full arrow compatibility.
+table Interval {
+ unit: IntervalUnit;
+}
+
+// An absolute length of time unrelated to any calendar artifacts.
+//
+// For the purposes of Arrow Implementations, adding this value to a Timestamp
+// ("t1") naively (i.e. simply summing the two number) is acceptable even
+// though in some cases the resulting Timestamp (t2) would not account for
+// leap-seconds during the elapsed time between "t1" and "t2". Similarly,
+// representing the difference between two Unix timestamp is acceptable, but
+// would yield a value that is possibly a few seconds off from the true elapsed
+// time.
+//
+// The resolution defaults to millisecond, but can be any of the other
+// supported TimeUnit values as with Timestamp and Time types. This type is
+// always represented as an 8-byte integer.
+table Duration {
+ unit: TimeUnit = MILLISECOND;
+}
+
+/// ----------------------------------------------------------------------
+/// Top-level Type value, enabling extensible type-specific metadata. We can
+/// add new logical types to Type without breaking backwards compatibility
+
+union Type {
+ Null,
+ Int,
+ FloatingPoint,
+ Binary,
+ Utf8,
+ Bool,
+ Decimal,
+ Date,
+ Time,
+ Timestamp,
+ Interval,
+ List,
+ Struct_,
+ Union,
+ FixedSizeBinary,
+ FixedSizeList,
+ Map,
+ Duration,
+ LargeBinary,
+ LargeUtf8,
+ LargeList,
+}
+
+/// ----------------------------------------------------------------------
+/// user defined key value pairs to add custom metadata to arrow
+/// key namespacing is the responsibility of the user
+
+table KeyValue {
+ key: string;
+ value: string;
+}
+
+/// ----------------------------------------------------------------------
+/// Dictionary encoding metadata
+/// Maintained for forwards compatibility, in the future
+/// Dictionaries might be explicit maps between integers and values
+/// allowing for non-contiguous index values
+enum DictionaryKind : short { DenseArray }
+table DictionaryEncoding {
+ /// The known dictionary id in the application where this data is used. In
+ /// the file or streaming formats, the dictionary ids are found in the
+ /// DictionaryBatch messages
+ id: long;
+
+ /// The dictionary indices are constrained to be positive integers. If this
+ /// field is null, the indices must be signed int32
+ indexType: Int;
+
+ /// By default, dictionaries are not ordered, or the order does not have
+ /// semantic meaning. In some statistical, applications, dictionary-encoding
+ /// is used to represent ordered categorical data, and we provide a way to
+ /// preserve that metadata here
+ isOrdered: bool;
+
+ dictionaryKind: DictionaryKind;
+}
+
+/// ----------------------------------------------------------------------
+/// A field represents a named column in a record / row batch or child of a
+/// nested type.
+
+table Field {
+ /// Name is not required, in i.e. a List
+ name: string;
+
+ /// Whether or not this field can contain nulls. Should be true in general.
+ nullable: bool;
+
+ /// This is the type of the decoded value if the field is dictionary encoded.
+ type: Type;
+
+ /// Present only if the field is dictionary encoded.
+ dictionary: DictionaryEncoding;
+
+ /// children apply only to nested data types like Struct, List and Union. For
+ /// primitive types children will have length 0.
+ children: [ Field ];
+
+ /// User-defined metadata
+ custom_metadata: [ KeyValue ];
+}
+
+/// ----------------------------------------------------------------------
+/// Endianness of the platform producing the data
+
+enum Endianness:short { Little, Big }
+
+/// ----------------------------------------------------------------------
+/// A Buffer represents a single contiguous memory segment
+struct Buffer {
+ /// The relative offset into the shared memory page where the bytes for this
+ /// buffer starts
+ offset: long;
+
+ /// The absolute length (in bytes) of the memory buffer. The memory is found
+ /// from offset (inclusive) to offset + length (non-inclusive). When building
+ /// messages using the encapsulated IPC message, padding bytes may be written
+ /// after a buffer, but such padding bytes do not need to be accounted for in
+ /// the size here.
+ length: long;
+}
+
+/// ----------------------------------------------------------------------
+/// A Schema describes the columns in a row batch
+
+table Schema {
+
+ /// endianness of the buffer
+ /// it is Little Endian by default
+ /// if endianness doesn't match the underlying system then the vectors need to be converted
+ endianness: Endianness=Little;
+
+ fields: [Field];
+ // User-defined metadata
+ custom_metadata: [ KeyValue ];
+}
+
+root_type Schema; \ No newline at end of file
diff --git a/yt/yt/client/arrow/fbs/SparseTensor.fbs b/yt/yt/client/arrow/fbs/SparseTensor.fbs
new file mode 100644
index 0000000000..38ae498a8e
--- /dev/null
+++ b/yt/yt/client/arrow/fbs/SparseTensor.fbs
@@ -0,0 +1,218 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/// EXPERIMENTAL: Metadata for n-dimensional sparse arrays, aka "sparse tensors".
+/// Arrow implementations in general are not required to implement this type
+
+include "Tensor.fbs";
+
+namespace org.apache.arrow.flatbuf;
+
+/// ----------------------------------------------------------------------
+/// EXPERIMENTAL: Data structures for sparse tensors
+
+/// Coordinate (COO) format of sparse tensor index.
+///
+/// COO's index list are represented as a NxM matrix,
+/// where N is the number of non-zero values,
+/// and M is the number of dimensions of a sparse tensor.
+///
+/// indicesBuffer stores the location and size of the data of this indices
+/// matrix. The value type and the stride of the indices matrix is
+/// specified in indicesType and indicesStrides fields.
+///
+/// For example, let X be a 2x3x4x5 tensor, and it has the following
+/// 6 non-zero values:
+///
+/// X[0, 1, 2, 0] := 1
+/// X[1, 1, 2, 3] := 2
+/// X[0, 2, 1, 0] := 3
+/// X[0, 1, 3, 0] := 4
+/// X[0, 1, 2, 1] := 5
+/// X[1, 2, 0, 4] := 6
+///
+/// In COO format, the index matrix of X is the following 4x6 matrix:
+///
+/// [[0, 0, 0, 0, 1, 1],
+/// [1, 1, 1, 2, 1, 2],
+/// [2, 2, 3, 1, 2, 0],
+/// [0, 1, 0, 0, 3, 4]]
+///
+/// Note that the indices are sorted in lexicographical order.
+table SparseTensorIndexCOO {
+ /// The type of values in indicesBuffer
+ indicesType: Int (required);
+
+ /// Non-negative byte offsets to advance one value cell along each dimension
+ /// If omitted, default to row-major order (C-like).
+ indicesStrides: [long];
+
+ /// The location and size of the indices matrix's data
+ indicesBuffer: Buffer (required);
+}
+
+enum SparseMatrixCompressedAxis: short { Row, Column }
+
+/// Compressed Sparse format, that is matrix-specific.
+table SparseMatrixIndexCSX {
+ /// Which axis, row or column, is compressed
+ compressedAxis: SparseMatrixCompressedAxis;
+
+ /// The type of values in indptrBuffer
+ indptrType: Int (required);
+
+ /// indptrBuffer stores the location and size of indptr array that
+ /// represents the range of the rows.
+ /// The i-th row spans from indptr[i] to indptr[i+1] in the data.
+ /// The length of this array is 1 + (the number of rows), and the type
+ /// of index value is long.
+ ///
+ /// For example, let X be the following 6x4 matrix:
+ ///
+ /// X := [[0, 1, 2, 0],
+ /// [0, 0, 3, 0],
+ /// [0, 4, 0, 5],
+ /// [0, 0, 0, 0],
+ /// [6, 0, 7, 8],
+ /// [0, 9, 0, 0]].
+ ///
+ /// The array of non-zero values in X is:
+ ///
+ /// values(X) = [1, 2, 3, 4, 5, 6, 7, 8, 9].
+ ///
+ /// And the indptr of X is:
+ ///
+ /// indptr(X) = [0, 2, 3, 5, 5, 8, 10].
+ indptrBuffer: Buffer (required);
+
+ /// The type of values in indicesBuffer
+ indicesType: Int (required);
+
+ /// indicesBuffer stores the location and size of the array that
+ /// contains the column indices of the corresponding non-zero values.
+ /// The type of index value is long.
+ ///
+ /// For example, the indices of the above X is:
+ ///
+ /// indices(X) = [1, 2, 2, 1, 3, 0, 2, 3, 1].
+ ///
+ /// Note that the indices are sorted in lexicographical order for each row.
+ indicesBuffer: Buffer (required);
+}
+
+/// Compressed Sparse Fiber (CSF) sparse tensor index.
+table SparseTensorIndexCSF {
+ /// CSF is a generalization of compressed sparse row (CSR) index.
+ /// See [smith2017knl]: http://shaden.io/pub-files/smith2017knl.pdf
+ ///
+ /// CSF index recursively compresses each dimension of a tensor into a set
+ /// of prefix trees. Each path from a root to leaf forms one tensor
+ /// non-zero index. CSF is implemented with two arrays of buffers and one
+ /// arrays of integers.
+ ///
+ /// For example, let X be a 2x3x4x5 tensor and let it have the following
+ /// 8 non-zero values:
+ ///
+ /// X[0, 0, 0, 1] := 1
+ /// X[0, 0, 0, 2] := 2
+ /// X[0, 1, 0, 0] := 3
+ /// X[0, 1, 0, 2] := 4
+ /// X[0, 1, 1, 0] := 5
+ /// X[1, 1, 1, 0] := 6
+ /// X[1, 1, 1, 1] := 7
+ /// X[1, 1, 1, 2] := 8
+ ///
+ /// As a prefix tree this would be represented as:
+ ///
+ /// 0 1
+ /// / \ |
+ /// 0 1 1
+ /// / / \ |
+ /// 0 0 1 1
+ /// /| /| | /| |
+ /// 1 2 0 2 0 0 1 2
+
+ /// The type of values in indptrBuffers
+ indptrType: Int (required);
+
+ /// indptrBuffers stores the sparsity structure.
+ /// Each two consecutive dimensions in a tensor correspond to a buffer in
+ /// indptrBuffers. A pair of consecutive values at indptrBuffers[dim][i]
+ /// and indptrBuffers[dim][i + 1] signify a range of nodes in
+ /// indicesBuffers[dim + 1] who are children of indicesBuffers[dim][i] node.
+ ///
+ /// For example, the indptrBuffers for the above X is:
+ ///
+ /// indptrBuffer(X) = [
+ /// [0, 2, 3],
+ /// [0, 1, 3, 4],
+ /// [0, 2, 4, 5, 8]
+ /// ].
+ ///
+ indptrBuffers: [Buffer] (required);
+
+ /// The type of values in indicesBuffers
+ indicesType: Int (required);
+
+ /// indicesBuffers stores values of nodes.
+ /// Each tensor dimension corresponds to a buffer in indicesBuffers.
+ /// For example, the indicesBuffers for the above X is:
+ ///
+ /// indicesBuffer(X) = [
+ /// [0, 1],
+ /// [0, 1, 1],
+ /// [0, 0, 1, 1],
+ /// [1, 2, 0, 2, 0, 0, 1, 2]
+ /// ].
+ ///
+ indicesBuffers: [Buffer] (required);
+
+ /// axisOrder stores the sequence in which dimensions were traversed to
+ /// produce the prefix tree.
+ /// For example, the axisOrder for the above X is:
+ ///
+ /// axisOrder(X) = [0, 1, 2, 3].
+ ///
+ axisOrder: [int] (required);
+}
+
+union SparseTensorIndex {
+ SparseTensorIndexCOO,
+ SparseMatrixIndexCSX,
+ SparseTensorIndexCSF
+}
+
+table SparseTensor {
+ /// The type of data contained in a value cell.
+ /// Currently only fixed-width value types are supported,
+ /// no strings or nested types.
+ type: Type (required);
+
+ /// The dimensions of the tensor, optionally named.
+ shape: [TensorDim] (required);
+
+ /// The number of non-zero values in a sparse tensor.
+ non_zero_length: long;
+
+ /// Sparse tensor index
+ sparseIndex: SparseTensorIndex (required);
+
+ /// The location and size of the tensor's data
+ data: Buffer (required);
+}
+
+root_type SparseTensor; \ No newline at end of file
diff --git a/yt/yt/client/arrow/fbs/Tensor.fbs b/yt/yt/client/arrow/fbs/Tensor.fbs
new file mode 100644
index 0000000000..3384196057
--- /dev/null
+++ b/yt/yt/client/arrow/fbs/Tensor.fbs
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/// EXPERIMENTAL: Metadata for n-dimensional arrays, aka "tensors" or
+/// "ndarrays". Arrow implementations in general are not required to implement
+/// this type
+
+include "Schema.fbs";
+
+namespace org.apache.arrow.flatbuf;
+
+/// ----------------------------------------------------------------------
+/// Data structures for dense tensors
+
+/// Shape data for a single axis in a tensor
+table TensorDim {
+ /// Length of dimension
+ size: long;
+
+ /// Name of the dimension, optional
+ name: string;
+}
+
+table Tensor {
+ /// The type of data contained in a value cell. Currently only fixed-width
+ /// value types are supported, no strings or nested types
+ type: Type (required);
+
+ /// The dimensions of the tensor, optionally named
+ shape: [TensorDim] (required);
+
+ /// Non-negative byte offsets to advance one value cell along each dimension
+ /// If omitted, default to row-major order (C-like).
+ strides: [long];
+
+ /// The location and size of the tensor's data
+ data: Buffer (required);
+}
+
+root_type Tensor; \ No newline at end of file
diff --git a/yt/yt/client/arrow/public.cpp b/yt/yt/client/arrow/public.cpp
new file mode 100644
index 0000000000..2470d419c2
--- /dev/null
+++ b/yt/yt/client/arrow/public.cpp
@@ -0,0 +1,10 @@
+#include "public.h"
+
+namespace NYT::NArrow {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow
+
diff --git a/yt/yt/client/arrow/public.h b/yt/yt/client/arrow/public.h
new file mode 100644
index 0000000000..8830264345
--- /dev/null
+++ b/yt/yt/client/arrow/public.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NArrow {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger ArrowLogger("Arrow");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NArrow \ No newline at end of file
diff --git a/yt/yt/client/arrow/ya.make b/yt/yt/client/arrow/ya.make
new file mode 100644
index 0000000000..9969677fd5
--- /dev/null
+++ b/yt/yt/client/arrow/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ fbs/Message.fbs
+ fbs/Schema.fbs
+ fbs/Tensor.fbs
+ fbs/SparseTensor.fbs
+ arrow_row_stream_encoder.cpp
+ arrow_row_stream_decoder.cpp
+ public.cpp
+)
+
+PEERDIR(
+ yt/yt/client
+ contrib/libs/flatbuffers
+)
+
+END()
diff --git a/yt/yt/client/chaos_client/config.h b/yt/yt/client/chaos_client/config.h
new file mode 100644
index 0000000000..bc284e856d
--- /dev/null
+++ b/yt/yt/client/chaos_client/config.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/cache_config.h>
+
+#include <yt/yt/core/rpc/config.h>
+
+namespace NYT::NChaosClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplicationCardCacheConfig
+ : public TAsyncExpiringCacheConfig
+ , public NRpc::TBalancingChannelConfig
+ , public NRpc::TRetryingChannelConfig
+{
+ REGISTER_YSON_STRUCT(TReplicationCardCacheConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TReplicationCardCacheConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
+
diff --git a/yt/yt/client/chaos_client/helpers.cpp b/yt/yt/client/chaos_client/helpers.cpp
new file mode 100644
index 0000000000..d6ddbb0c01
--- /dev/null
+++ b/yt/yt/client/chaos_client/helpers.cpp
@@ -0,0 +1,95 @@
+#include "public.h"
+#include "replication_card_serialization.h"
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NChaosClient {
+
+using namespace NObjectClient;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReplicationCardId MakeReplicationCardId(TObjectId randomId)
+{
+ return MakeId(
+ EObjectType::ReplicationCard,
+ CellTagFromId(randomId),
+ CounterFromId(randomId),
+ HashFromId(randomId) & 0xffff0000);
+}
+
+TReplicaId MakeReplicaId(TReplicationCardId replicationCardId, TReplicaIdIndex index)
+{
+ return MakeId(
+ EObjectType::ChaosTableReplica,
+ CellTagFromId(replicationCardId),
+ CounterFromId(replicationCardId),
+ HashFromId(replicationCardId) | index);
+}
+
+TReplicationCardId ReplicationCardIdFromReplicaId(TReplicaId replicaId)
+{
+ return MakeId(
+ EObjectType::ReplicationCard,
+ CellTagFromId(replicaId),
+ CounterFromId(replicaId),
+ HashFromId(replicaId) & 0xffff0000);
+}
+
+TReplicationCardId ReplicationCardIdFromUpstreamReplicaIdOrNull(TReplicaId upstreamReplicaId)
+{
+ return IsChaosTableReplicaType(TypeFromId(upstreamReplicaId))
+ ? ReplicationCardIdFromReplicaId(upstreamReplicaId)
+ : TReplicationCardId();
+}
+
+TReplicationCardId MakeReplicationCardCollocationId(TObjectId randomId)
+{
+ return MakeId(
+ EObjectType::ReplicationCardCollocation,
+ CellTagFromId(randomId),
+ CounterFromId(randomId),
+ HashFromId(randomId) & 0xffff0000);
+}
+
+TCellTag GetSiblingChaosCellTag(TCellTag cellTag)
+{
+ return TCellTag(cellTag.Underlying() ^ 1);
+}
+
+bool IsOrderedTabletReplicationProgress(const TReplicationProgress& progress)
+{
+ const auto& segments = progress.Segments;
+ const auto& upper = progress.UpperKey;
+
+ if (segments.size() != 1) {
+ return false;
+ }
+
+ if (segments[0].LowerKey.GetCount() != 0 &&
+ (segments[0].LowerKey.GetCount() != 1 || segments[0].LowerKey[0].Type != EValueType::Int64))
+ {
+ return false;
+ }
+
+ if (upper.GetCount() != 1 || (upper[0].Type != EValueType::Int64 && upper[0].Type != EValueType::Max)) {
+ return false;
+ }
+
+ return true;
+}
+
+void ValidateOrderedTabletReplicationProgress(const TReplicationProgress& progress)
+{
+ if (!IsOrderedTabletReplicationProgress(progress)) {
+ THROW_ERROR_EXCEPTION("Invalid replication progress for ordered table")
+ << TErrorAttribute("replication_progress", progress);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chaos_client/public.h b/yt/yt/client/chaos_client/public.h
new file mode 100644
index 0000000000..73baf9edd7
--- /dev/null
+++ b/yt/yt/client/chaos_client/public.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/hydra/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+namespace NYT::NChaosClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TReplicationCardId = NObjectClient::TObjectId;
+using TReplicationCardCollocationId = NObjectClient::TObjectId;
+using TReplicaId = NObjectClient::TObjectId;
+using TReplicationEra = ui64;
+using TReplicaIdIndex = ui16;
+
+constexpr TReplicationEra InvalidReplicationEra = static_cast<TReplicationEra>(-1);
+constexpr TReplicationEra InitialReplicationEra = 0;
+
+constexpr int MaxReplicasPerReplicationCard = 128;
+
+DECLARE_REFCOUNTED_STRUCT(TReplicationCard)
+
+DECLARE_REFCOUNTED_STRUCT(IReplicationCardCache)
+DECLARE_REFCOUNTED_CLASS(TReplicationCardCacheConfig)
+
+struct TReplicationProgress;
+struct TReplicaHistoryItem;
+struct TReplicaInfo;
+struct TReplicationCardFecthOptions;
+
+YT_DEFINE_ERROR_ENUM(
+ ((ReplicationCardNotKnown) (3200))
+ ((ReplicationCardMigrated) (3201))
+ ((ChaosCellSuspended) (3202))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/chaos_client/replication_card.cpp b/yt/yt/client/chaos_client/replication_card.cpp
new file mode 100644
index 0000000000..6e32041b2e
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card.cpp
@@ -0,0 +1,590 @@
+#include "replication_card.h"
+
+#include <yt/yt/core/misc/guid.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <util/digest/multi.h>
+
+#include <algorithm>
+
+namespace NYT::NChaosClient {
+
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NTransactionClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReplicationCardFetchOptions::operator size_t() const
+{
+ return MultiHash(
+ IncludeCoordinators,
+ IncludeProgress,
+ IncludeHistory);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCardFetchOptions& options, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{IncludeCoordinators: %v, IncludeProgress: %v, IncludeHistory: %v}",
+ options.IncludeCoordinators,
+ options.IncludeProgress,
+ options.IncludeHistory);
+}
+
+TString ToString(const TReplicationCardFetchOptions& options)
+{
+ return ToStringViaBuilder(options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationProgress& replicationProgress, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{Segments: %v, UpperKey: %v}",
+ MakeFormattableView(replicationProgress.Segments, [] (auto* builder, const auto& segment) {
+ builder->AppendFormat("<%v, %x>", segment.LowerKey, segment.Timestamp);
+ }),
+ replicationProgress.UpperKey);
+}
+
+TString ToString(const TReplicationProgress& replicationProgress)
+{
+ return ToStringViaBuilder(replicationProgress);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TReplicaHistoryItem& replicaHistoryItem, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{Era: %v, Timestamp: %v, Mode: %v, State: %v}",
+ replicaHistoryItem.Era,
+ replicaHistoryItem.Timestamp,
+ replicaHistoryItem.Mode,
+ replicaHistoryItem.State);
+}
+
+TString ToString(const TReplicaHistoryItem& replicaHistoryItem)
+{
+ return ToStringViaBuilder(replicaHistoryItem);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TReplicaInfo& replicaInfo, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{ClusterName: %v, ReplicaPath: %v, ContentType: %v, Mode: %v, State: %v, Progress: %v, History: %v}",
+ replicaInfo.ClusterName,
+ replicaInfo.ReplicaPath,
+ replicaInfo.ContentType,
+ replicaInfo.Mode,
+ replicaInfo.State,
+ replicaInfo.ReplicationProgress,
+ replicaInfo.History);
+}
+
+TString ToString(const TReplicaInfo& replicaInfo)
+{
+ return ToStringViaBuilder(replicaInfo);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCard& replicationCard, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{Era: %v, Replicas: %v, CoordinatorCellIds: %v, TableId: %v, TablePath: %v, TableClusterName: %v, CurrentTimestamp: %v, CollocationId: %v}",
+ replicationCard.Era,
+ replicationCard.Replicas,
+ replicationCard.CoordinatorCellIds,
+ replicationCard.TableId,
+ replicationCard.TablePath,
+ replicationCard.TableClusterName,
+ replicationCard.CurrentTimestamp,
+ replicationCard.ReplicationCardCollocationId);
+}
+
+TString ToString(const TReplicationCard& replicationCard)
+{
+ return ToStringViaBuilder(replicationCard);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TReplicationProgress::TSegment::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, LowerKey);
+ Persist(context, Timestamp);
+}
+
+void TReplicationProgress::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Segments);
+ Persist(context, UpperKey);
+}
+
+void TReplicaHistoryItem::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Era);
+ Persist(context, Timestamp);
+ Persist(context, Mode);
+ Persist(context, State);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int TReplicaInfo::FindHistoryItemIndex(TTimestamp timestamp) const
+{
+ auto it = std::upper_bound(
+ History.begin(),
+ History.end(),
+ timestamp,
+ [] (TTimestamp lhs, const TReplicaHistoryItem& rhs) {
+ return lhs < rhs.Timestamp;
+ });
+ return std::distance(History.begin(), it) - 1;
+}
+
+TReplicaInfo* TReplicationCard::FindReplica(TReplicaId replicaId)
+{
+ auto it = Replicas.find(replicaId);
+ return it == Replicas.end() ? nullptr : &it->second;
+}
+
+TReplicaInfo* TReplicationCard::GetReplicaOrThrow(TReplicaId replicaId, TReplicationCardId replicationCardId)
+{
+ auto* replicaInfo = FindReplica(replicaId);
+ if (!replicaInfo) {
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Replication card %v does not contain replica %v",
+ replicationCardId,
+ replicaId);
+ }
+ return replicaInfo;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsReplicaSync(ETableReplicaMode mode)
+{
+ return mode == ETableReplicaMode::Sync || mode == ETableReplicaMode::SyncToAsync;
+}
+
+bool IsReplicaAsync(ETableReplicaMode mode)
+{
+ return mode == ETableReplicaMode::Async || mode == ETableReplicaMode::AsyncToSync;
+}
+
+bool IsReplicaEnabled(ETableReplicaState state)
+{
+ return state == ETableReplicaState::Enabled || state == ETableReplicaState::Disabling;
+}
+
+bool IsReplicaDisabled(ETableReplicaState state)
+{
+ return state == ETableReplicaState::Disabled || state == ETableReplicaState::Enabling;
+}
+
+bool IsReplicaReallySync(ETableReplicaMode mode, ETableReplicaState state)
+{
+ return IsReplicaSync(mode) && IsReplicaEnabled(state);
+}
+
+ETableReplicaMode GetTargetReplicaMode(ETableReplicaMode mode)
+{
+ return (mode == ETableReplicaMode::Sync || mode == ETableReplicaMode::AsyncToSync)
+ ? ETableReplicaMode::Sync
+ : ETableReplicaMode::Async;
+}
+
+ETableReplicaState GetTargetReplicaState(ETableReplicaState state)
+{
+ return (state == ETableReplicaState::Enabled || state == ETableReplicaState::Enabling)
+ ? ETableReplicaState::Enabled
+ : ETableReplicaState::Disabled;
+}
+
+void UpdateReplicationProgress(TReplicationProgress* progress, const TReplicationProgress& update)
+{
+ std::vector<TReplicationProgress::TSegment> segments;
+ auto progressIt = progress->Segments.begin();
+ auto progressEnd = progress->Segments.end();
+ auto updateIt = update.Segments.begin();
+ auto updateEnd = update.Segments.end();
+ auto progressTimestamp = NullTimestamp;
+ auto updateTimestamp = NullTimestamp;
+
+ auto append = [&] (TUnversionedOwningRow key) {
+ auto timestamp = std::max(progressTimestamp, updateTimestamp);
+ if (segments.empty() || segments.back().Timestamp != timestamp) {
+ segments.push_back({std::move(key), timestamp});
+ }
+ };
+
+ bool upper = false;
+ auto processUpperKey = [&] (const TUnversionedOwningRow& key) {
+ if (upper || updateIt != updateEnd) {
+ return;
+ }
+
+ auto cmpResult = CompareRows(key, update.UpperKey);
+ if (cmpResult >= 0) {
+ updateTimestamp = NullTimestamp;
+ upper = true;
+ }
+ if (cmpResult > 0) {
+ append(update.UpperKey);
+ }
+ };
+
+ while (progressIt < progressEnd || updateIt < updateEnd) {
+ int cmpResult;
+ if (updateIt == updateEnd) {
+ cmpResult = -1;
+ } else if (progressIt == progressEnd) {
+ cmpResult = 1;
+ } else {
+ cmpResult = CompareRows(progressIt->LowerKey, updateIt->LowerKey);
+ }
+
+ if (cmpResult < 0) {
+ if (updateIt == updateEnd) {
+ processUpperKey(progressIt->LowerKey);
+ }
+ progressTimestamp = progressIt->Timestamp;
+ append(std::move(progressIt->LowerKey));
+ ++progressIt;
+ } else if (cmpResult > 0) {
+ updateTimestamp = updateIt->Timestamp;
+ append(updateIt->LowerKey);
+ ++updateIt;
+ } else {
+ updateTimestamp = updateIt->Timestamp;
+ progressTimestamp = progressIt->Timestamp;
+ append(std::move(progressIt->LowerKey));
+ ++progressIt;
+ ++updateIt;
+ }
+ }
+
+ processUpperKey(progress->UpperKey);
+ progress->Segments = std::move(segments);
+}
+
+bool IsReplicationProgressGreaterOrEqual(const TReplicationProgress& progress, const TReplicationProgress& other)
+{
+ auto progressIt = progress.Segments.begin();
+ auto otherIt = std::upper_bound(
+ other.Segments.begin(),
+ other.Segments.end(),
+ progressIt->LowerKey,
+ [] (const auto& lhs, const auto& rhs) {
+ return CompareRows(lhs, rhs.LowerKey) < 0;
+ });
+
+ auto progressEnd = progress.Segments.end();
+ auto otherEnd = other.Segments.end();
+ auto progressTimestamp = MaxTimestamp;
+ auto otherTimestamp = otherIt == other.Segments.begin()
+ ? NullTimestamp
+ : (otherIt - 1)->Timestamp;
+
+ while (progressIt < progressEnd || otherIt < otherEnd) {
+ int cmpResult;
+ if (otherIt == otherEnd) {
+ if (CompareRows(progressIt->LowerKey, other.UpperKey) >= 0) {
+ return true;
+ }
+ cmpResult = -1;
+ } else if (progressIt == progressEnd) {
+ if (CompareRows(progress.UpperKey, otherIt->LowerKey) <= 0) {
+ return true;
+ }
+ cmpResult = 1;
+ } else {
+ cmpResult = CompareRows(progressIt->LowerKey, otherIt->LowerKey);
+ }
+
+ if (cmpResult < 0) {
+ progressTimestamp = progressIt->Timestamp;
+ ++progressIt;
+ } else if (cmpResult > 0) {
+ otherTimestamp = otherIt->Timestamp;
+ ++otherIt;
+ } else {
+ progressTimestamp = progressIt->Timestamp;
+ otherTimestamp = otherIt->Timestamp;
+ ++progressIt;
+ ++otherIt;
+ }
+
+ if (progressTimestamp < otherTimestamp) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool IsReplicationProgressEqual(const TReplicationProgress& progress, const TReplicationProgress& other)
+{
+ if (progress.Segments.size() != other.Segments.size() || progress.UpperKey != other.UpperKey) {
+ return false;
+ }
+ for (int index = 0; index < std::ssize(progress.Segments); ++index) {
+ const auto& segment = progress.Segments[index];
+ const auto& otherSegment = other.Segments[index];
+ if (segment.LowerKey != otherSegment.LowerKey || segment.Timestamp != otherSegment.Timestamp) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool IsReplicationProgressGreaterOrEqual(const TReplicationProgress& progress, TTimestamp timestamp)
+{
+ for (const auto& segment : progress.Segments) {
+ if (segment.Timestamp < timestamp) {
+ return false;
+ }
+ }
+ return true;
+}
+
+TReplicationProgress ExtractReplicationProgress(
+ const TReplicationProgress& progress,
+ TLegacyKey lower,
+ TLegacyKey upper)
+{
+ TReplicationProgress extracted;
+ extracted.UpperKey = TUnversionedOwningRow(upper);
+
+ auto it = std::upper_bound(
+ progress.Segments.begin(),
+ progress.Segments.end(),
+ lower,
+ [] (const auto& lhs, const auto& rhs) {
+ return CompareRows(lhs, rhs.LowerKey) < 0;
+ });
+
+ YT_VERIFY(it != progress.Segments.begin());
+ --it;
+
+ extracted.Segments.push_back({TUnversionedOwningRow(lower), it->Timestamp});
+
+ for (++it; it < progress.Segments.end() && it->LowerKey < upper; ++it) {
+ extracted.Segments.push_back(*it);
+ }
+
+ return extracted;
+}
+
+TReplicationProgress AdvanceReplicationProgress(const TReplicationProgress& progress, TTimestamp timestamp)
+{
+ TReplicationProgress result;
+ result.UpperKey = progress.UpperKey;
+
+ for (const auto& segment : progress.Segments) {
+ if (segment.Timestamp > timestamp) {
+ result.Segments.push_back(segment);
+ } else if (result.Segments.empty() || result.Segments.back().Timestamp > timestamp) {
+ result.Segments.push_back({segment.LowerKey, timestamp});
+ }
+ }
+
+ return result;
+}
+
+TReplicationProgress LimitReplicationProgressByTimestamp(const TReplicationProgress& progress, TTimestamp timestamp)
+{
+ TReplicationProgress result;
+ result.UpperKey = progress.UpperKey;
+
+ for (const auto& segment : progress.Segments) {
+ if (segment.Timestamp < timestamp) {
+ result.Segments.push_back(segment);
+ } else if (result.Segments.empty() || result.Segments.back().Timestamp < timestamp) {
+ result.Segments.push_back({segment.LowerKey, timestamp});
+ }
+ }
+
+ return result;
+}
+
+void CanonizeReplicationProgress(TReplicationProgress* progress)
+{
+ int current = 0;
+ for (int index = 1; index < std::ssize(progress->Segments); ++index) {
+ if (progress->Segments[current].Timestamp != progress->Segments[index].Timestamp) {
+ ++current;
+ if (current != index) {
+ progress->Segments[current] = std::move(progress->Segments[index]);
+ }
+ }
+ }
+ progress->Segments.resize(current + 1);
+}
+
+TTimestamp GetReplicationProgressMinTimestamp(const TReplicationProgress& progress)
+{
+ auto minTimestamp = MaxTimestamp;
+ for (const auto& segment : progress.Segments) {
+ minTimestamp = std::min(segment.Timestamp, minTimestamp);
+ }
+ return minTimestamp;
+}
+
+TTimestamp GetReplicationProgressMaxTimestamp(const TReplicationProgress& progress)
+{
+ auto maxTimestamp = MinTimestamp;
+ for (const auto& segment : progress.Segments) {
+ maxTimestamp = std::max(segment.Timestamp, maxTimestamp);
+ }
+ return maxTimestamp;
+}
+
+std::optional<TTimestamp> FindReplicationProgressTimestampForKey(
+ const TReplicationProgress& progress,
+ TUnversionedValueRange key)
+{
+ if (CompareValueRanges(progress.UpperKey.Elements(), key) <= 0 ||
+ CompareValueRanges(progress.Segments[0].LowerKey.Elements(), key) > 0)
+ {
+ return {};
+ }
+
+ auto it = std::upper_bound(
+ progress.Segments.begin(),
+ progress.Segments.end(),
+ key,
+ [&] (const auto& /*key*/, const auto& segment) {
+ return CompareValueRanges(key, segment.LowerKey.Elements()) < 0;
+ });
+ YT_VERIFY(it > progress.Segments.begin());
+
+ return (it - 1)->Timestamp;
+}
+
+TTimestamp GetReplicationProgressTimestampForKeyOrThrow(
+ const TReplicationProgress& progress,
+ TUnversionedRow key)
+{
+ auto timestamp = FindReplicationProgressTimestampForKey(progress, key.Elements());
+ if (!timestamp) {
+ THROW_ERROR_EXCEPTION("Key %v is out or replication progress range", key);
+ }
+ return *timestamp;
+}
+
+TTimestamp GetReplicationProgressMinTimestamp(
+ const TReplicationProgress& progress,
+ TLegacyKey lower,
+ TLegacyKey upper)
+{
+ auto it = std::upper_bound(
+ progress.Segments.begin(),
+ progress.Segments.end(),
+ lower,
+ [] (const auto& lhs, const auto& rhs) {
+ return CompareRows(lhs, rhs.LowerKey) < 0;
+ });
+
+ YT_VERIFY(it != progress.Segments.begin());
+ --it;
+
+ auto minTimestamp = MaxTimestamp;
+ for (; it < progress.Segments.end() && it->LowerKey < upper; ++it) {
+ minTimestamp = std::min(it->Timestamp, minTimestamp);
+ }
+ return minTimestamp;
+}
+
+TReplicationProgress GatherReplicationProgress(
+ std::vector<TReplicationProgress> progresses,
+ const std::vector<TUnversionedRow>& pivotKeys,
+ TUnversionedRow upperKey)
+{
+ YT_VERIFY(progresses.size() == pivotKeys.size());
+ YT_VERIFY(!pivotKeys.empty() && pivotKeys.back() < upperKey);
+
+ TReplicationProgress progress;
+ for (int index = 0; index < std::ssize(progresses); ++index) {
+ auto& segments = progresses[index].Segments;
+ auto lowerKey = pivotKeys[index];
+ if (segments.empty()) {
+ progress.Segments.push_back({TUnversionedOwningRow(lowerKey), MinTimestamp});
+ } else {
+ YT_VERIFY(lowerKey == segments[0].LowerKey);
+ progress.Segments.insert(
+ progress.Segments.end(),
+ std::make_move_iterator(segments.begin()),
+ std::make_move_iterator(segments.end()));
+ }
+ }
+
+ YT_VERIFY(upperKey > progress.Segments.back().LowerKey);
+ progress.UpperKey = TUnversionedOwningRow(upperKey);
+ CanonizeReplicationProgress(&progress);
+ return progress;
+}
+
+std::vector<TReplicationProgress> ScatterReplicationProgress(
+ TReplicationProgress progress,
+ const std::vector<TUnversionedRow>& pivotKeys,
+ TUnversionedRow upperKey)
+{
+ YT_VERIFY(!pivotKeys.empty() && !progress.Segments.empty());
+ YT_VERIFY(pivotKeys[0] >= progress.Segments[0].LowerKey);
+ YT_VERIFY(progress.UpperKey.Get() >= upperKey);
+ YT_VERIFY(pivotKeys.back() < upperKey);
+
+ std::vector<TReplicationProgress> result;
+ auto& segments = progress.Segments;
+ int segmentIndex = 0;
+ int pivotIndex = 0;
+ auto previousTimestamp = MaxTimestamp;
+
+ while (pivotIndex < std::ssize(pivotKeys)) {
+ auto& pivotKey = pivotKeys[pivotIndex];
+ auto cmpResult = segmentIndex < std::ssize(segments)
+ ? CompareRows(pivotKey, segments[segmentIndex].LowerKey.Get())
+ : -1;
+
+ if (cmpResult <= 0) {
+ if (!result.empty()) {
+ result.back().UpperKey = TUnversionedOwningRow(pivotKey);
+ }
+
+ result.emplace_back();
+ YT_VERIFY(cmpResult == 0 || segmentIndex > 0);
+ auto timestamp = cmpResult == 0
+ ? segments[segmentIndex].Timestamp
+ : previousTimestamp;
+ result.back().Segments.push_back({TUnversionedOwningRow(pivotKey), timestamp});
+
+ ++pivotIndex;
+ if (cmpResult == 0) {
+ previousTimestamp = segments[segmentIndex].Timestamp;
+ ++segmentIndex;
+ }
+ } else {
+ previousTimestamp = segments[segmentIndex].Timestamp;
+ if (!result.empty()) {
+ result.back().Segments.push_back(std::move(segments[segmentIndex]));
+ }
+ ++segmentIndex;
+ }
+ }
+
+ for (; segmentIndex < std::ssize(segments) && segments[segmentIndex].LowerKey < upperKey; ++segmentIndex) {
+ result.back().Segments.push_back(std::move(segments[segmentIndex]));
+ }
+
+ result.back().UpperKey = TUnversionedOwningRow(upperKey);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chaos_client/replication_card.h b/yt/yt/client/chaos_client/replication_card.h
new file mode 100644
index 0000000000..364f4f6dd6
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card.h
@@ -0,0 +1,160 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/chaos_client/public.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+namespace NYT::NChaosClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReplicationProgress
+{
+ struct TSegment
+ {
+ NTableClient::TUnversionedOwningRow LowerKey;
+ NTransactionClient::TTimestamp Timestamp;
+
+ void Persist(const TStreamPersistenceContext& context);
+ };
+
+ std::vector<TSegment> Segments;
+ NTableClient::TUnversionedOwningRow UpperKey;
+
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+struct TReplicaHistoryItem
+{
+ NChaosClient::TReplicationEra Era;
+ NTransactionClient::TTimestamp Timestamp;
+ NTabletClient::ETableReplicaMode Mode;
+ NTabletClient::ETableReplicaState State;
+
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+struct TReplicaInfo
+{
+ TString ClusterName;
+ NYPath::TYPath ReplicaPath;
+ NTabletClient::ETableReplicaContentType ContentType;
+ NTabletClient::ETableReplicaMode Mode;
+ NTabletClient::ETableReplicaState State;
+ TReplicationProgress ReplicationProgress;
+ std::vector<TReplicaHistoryItem> History;
+ bool EnableReplicatedTableTracker = true;
+
+ //! Returns index of history item corresponding to timestamp, -1 if none.
+ int FindHistoryItemIndex(NTransactionClient::TTimestamp timestamp) const;
+};
+
+struct TReplicationCard
+ : public TRefCounted
+{
+ THashMap<TReplicaId, TReplicaInfo> Replicas;
+ std::vector<NObjectClient::TCellId> CoordinatorCellIds;
+ TReplicationEra Era = InvalidReplicationEra;
+ NTableClient::TTableId TableId;
+ NYPath::TYPath TablePath;
+ TString TableClusterName;
+ NTransactionClient::TTimestamp CurrentTimestamp = NTransactionClient::NullTimestamp;
+ NTabletClient::TReplicatedTableOptionsPtr ReplicatedTableOptions;
+ TReplicationCardCollocationId ReplicationCardCollocationId;
+
+ //! Returns pointer to replica with a given id, nullptr if none.
+ TReplicaInfo* FindReplica(TReplicaId replicaId);
+ TReplicaInfo* GetReplicaOrThrow(TReplicaId replicaId, TReplicationCardId replicationCardId);
+};
+
+DEFINE_REFCOUNTED_TYPE(TReplicationCard)
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct TReplicationCardFetchOptions
+{
+ bool IncludeCoordinators = false;
+ bool IncludeProgress = false;
+ bool IncludeHistory = false;
+ bool IncludeReplicatedTableOptions = false;
+
+ operator size_t() const;
+ bool operator == (const TReplicationCardFetchOptions& other) const = default;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCardFetchOptions& options, TStringBuf /*spec*/);
+TString ToString(const TReplicationCardFetchOptions& options);
+
+///////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationProgress& replicationProgress, TStringBuf /*spec*/);
+TString ToString(const TReplicationProgress& replicationProgress);
+
+void FormatValue(TStringBuilderBase* builder, const TReplicaHistoryItem& replicaHistoryItem, TStringBuf /*spec*/);
+TString ToString(const TReplicaHistoryItem& replicaHistoryItem);
+
+void FormatValue(TStringBuilderBase* builder, const TReplicaInfo& replicaInfo, TStringBuf /*spec*/);
+TString ToString(const TReplicaInfo& replicaInfo);
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCard& replicationCard, TStringBuf /*spec*/);
+TString ToString(const TReplicationCard& replicationCard);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsReplicaSync(NTabletClient::ETableReplicaMode mode);
+bool IsReplicaAsync(NTabletClient::ETableReplicaMode mode);
+bool IsReplicaEnabled(NTabletClient::ETableReplicaState state);
+bool IsReplicaDisabled(NTabletClient::ETableReplicaState state);
+bool IsReplicaReallySync(NTabletClient::ETableReplicaMode mode, NTabletClient::ETableReplicaState state);
+NTabletClient::ETableReplicaMode GetTargetReplicaMode(NTabletClient::ETableReplicaMode mode);
+NTabletClient::ETableReplicaState GetTargetReplicaState(NTabletClient::ETableReplicaState state);
+
+void UpdateReplicationProgress(TReplicationProgress* progress, const TReplicationProgress& update);
+
+bool IsReplicationProgressEqual(const TReplicationProgress& progress, const TReplicationProgress& other);
+bool IsReplicationProgressGreaterOrEqual(const TReplicationProgress& progress, const TReplicationProgress& other);
+bool IsReplicationProgressGreaterOrEqual(const TReplicationProgress& progress, NTransactionClient::TTimestamp timestamp);
+
+TReplicationProgress ExtractReplicationProgress(
+ const TReplicationProgress& progress,
+ NTableClient::TLegacyKey lower,
+ NTableClient::TLegacyKey upper);
+TReplicationProgress AdvanceReplicationProgress(const TReplicationProgress& progress, NTransactionClient::TTimestamp timestamp);
+TReplicationProgress LimitReplicationProgressByTimestamp(const TReplicationProgress& progress, NTransactionClient::TTimestamp timestamp);
+void CanonizeReplicationProgress(TReplicationProgress* progress);
+
+NTransactionClient::TTimestamp GetReplicationProgressMinTimestamp(const TReplicationProgress& progress);
+NTransactionClient::TTimestamp GetReplicationProgressMaxTimestamp(const TReplicationProgress& progress);
+NTransactionClient::TTimestamp GetReplicationProgressMinTimestamp(
+ const TReplicationProgress& progress,
+ NTableClient::TLegacyKey lower,
+ NTableClient::TLegacyKey upper);
+
+std::optional<NTransactionClient::TTimestamp> FindReplicationProgressTimestampForKey(
+ const TReplicationProgress& progress,
+ NTableClient::TUnversionedValueRange key);
+NTransactionClient::TTimestamp GetReplicationProgressTimestampForKeyOrThrow(
+ const TReplicationProgress& progress,
+ NTableClient::TUnversionedRow key);
+
+// Gathers replication progresses into a single one.
+// Pivot key should match first segment lower keys of a corresponding progress.
+// If source progress is empty it is is considered to span corresponding pivot keys range and have MinTimestamp timestamp,
+TReplicationProgress GatherReplicationProgress(
+ std::vector<TReplicationProgress> progresses,
+ const std::vector<NTableClient::TUnversionedRow>& pivotKeys,
+ NTableClient::TUnversionedRow upperKey);
+
+// Splits replication progress into ranges [pivotKeys[0]: pivotKeys[1]], ..., [pivotKeys[-1]: upperKey].
+std::vector<TReplicationProgress> ScatterReplicationProgress(
+ TReplicationProgress progress,
+ const std::vector<NTableClient::TUnversionedRow>& pivotKeys,
+ NTableClient::TUnversionedRow upperKey);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chaos_client/replication_card_cache.cpp b/yt/yt/client/chaos_client/replication_card_cache.cpp
new file mode 100644
index 0000000000..28f660ec6e
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card_cache.cpp
@@ -0,0 +1,33 @@
+#include "replication_card_cache.h"
+
+#include <library/cpp/yt/string/format.h>
+#include <library/cpp/yt/string/guid.h>
+
+#include <util/digest/multi.h>
+
+namespace NYT::NChaosClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+TReplicationCardCacheKey::operator size_t() const
+{
+ return MultiHash(
+ CardId,
+ FetchOptions);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCardCacheKey& key, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{CardId: %v, FetchOptions: %v}",
+ key.CardId,
+ key.FetchOptions);
+}
+
+TString ToString(const TReplicationCardCacheKey& key)
+{
+ return ToStringViaBuilder(key);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chaos_client/replication_card_cache.h b/yt/yt/client/chaos_client/replication_card_cache.h
new file mode 100644
index 0000000000..e7b5872067
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card_cache.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "public.h"
+#include "replication_card.h"
+
+namespace NYT::NChaosClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReplicationCardCacheKey
+{
+ TReplicationCardId CardId;
+ TReplicationCardFetchOptions FetchOptions;
+ TReplicationEra RefreshEra = InvalidReplicationEra;
+
+ operator size_t() const;
+ bool operator == (const TReplicationCardCacheKey& other) const = default;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TReplicationCardCacheKey& key, TStringBuf /*spec*/);
+TString ToString(const TReplicationCardCacheKey& key);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IReplicationCardCache
+ : public virtual TRefCounted
+{
+ virtual TFuture<TReplicationCardPtr> GetReplicationCard(const TReplicationCardCacheKey& key) = 0;
+ virtual void ForceRefresh(const TReplicationCardCacheKey& key, const TReplicationCardPtr& replicationCard) = 0;
+ virtual void Clear() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IReplicationCardCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
+
diff --git a/yt/yt/client/chaos_client/replication_card_serialization.cpp b/yt/yt/client/chaos_client/replication_card_serialization.cpp
new file mode 100644
index 0000000000..cb2ca23516
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card_serialization.cpp
@@ -0,0 +1,465 @@
+#include "replication_card_serialization.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/tablet_client/config.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/collection_helpers.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NChaosClient {
+
+using namespace NTransactionClient;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NYTree;
+using namespace NYson;
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializableSegment
+ : public TYsonStruct
+{
+ TUnversionedOwningRow LowerKey;
+ TTimestamp Timestamp;
+
+ REGISTER_YSON_STRUCT(TSerializableSegment);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("lower_key", &TThis::LowerKey)
+ .Default();
+ registrar.Parameter("timestamp", &TThis::Timestamp)
+ .Default();
+ }
+};
+
+DECLARE_REFCOUNTED_STRUCT(TSerializableReplicationProgress)
+
+struct TSerializableReplicationProgress
+ : public TYsonStruct
+{
+ std::vector<TIntrusivePtr<TSerializableSegment>> Segments;
+ TUnversionedOwningRow UpperKey;
+
+ REGISTER_YSON_STRUCT(TSerializableReplicationProgress);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("segments", &TThis::Segments)
+ .Default();
+ registrar.Parameter("upper_key", &TThis::UpperKey)
+ .Default();
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableReplicationProgress)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TSerializableReplicaInfo)
+
+struct TSerializableReplicaInfo
+ : public TYsonStruct
+{
+ TString ClusterName;
+ NYPath::TYPath ReplicaPath;
+ NTabletClient::ETableReplicaContentType ContentType;
+ NTabletClient::ETableReplicaMode Mode;
+ NTabletClient::ETableReplicaState State;
+ TReplicationProgress ReplicationProgress;
+ bool EnableReplicatedTableTracker;
+
+ REGISTER_YSON_STRUCT(TSerializableReplicaInfo);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("cluster_name", &TThis::ClusterName)
+ .NonEmpty();
+ registrar.Parameter("replica_path", &TThis::ReplicaPath)
+ .NonEmpty();
+ registrar.Parameter("content_type", &TThis::ContentType);
+ registrar.Parameter("mode", &TThis::Mode)
+ .Default(ETableReplicaMode::Async);
+ registrar.Parameter("state", &TThis::State)
+ .Default(ETableReplicaState::Disabled);
+ registrar.Parameter("replication_progress", &TThis::ReplicationProgress)
+ .Default();
+ registrar.Parameter("enable_replicated_table_tracker", &TThis::EnableReplicatedTableTracker)
+ .Default(false);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableReplicaInfo)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TSerializableReplicationCard)
+
+struct TSerializableReplicationCard
+ : public NYTree::TYsonStruct
+{
+ THashMap<TString, TReplicaInfo> Replicas;
+ std::vector<NObjectClient::TCellId> CoordinatorCellIds;
+ TReplicationEra Era;
+ TTableId TableId;
+ TYPath TablePath;
+ TString TableClusterName;
+ NTransactionClient::TTimestamp CurrentTimestamp;
+ NTabletClient::TReplicatedTableOptionsPtr ReplicatedTableOptions;
+ TReplicationCardCollocationId ReplicationCardCollocationId;
+
+ REGISTER_YSON_STRUCT(TSerializableReplicationCard);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("replicas", &TThis::Replicas);
+ registrar.Parameter("coordinator_cell_ids", &TThis::CoordinatorCellIds)
+ .Default();
+ registrar.Parameter("era", &TThis::Era)
+ .Default(0);
+ registrar.Parameter("table_id", &TThis::TableId)
+ .Default();
+ registrar.Parameter("table_path", &TThis::TablePath)
+ .Default();
+ registrar.Parameter("table_cluster_name", &TThis::TableClusterName)
+ .Default();
+ registrar.Parameter("current_timestamp", &TThis::CurrentTimestamp)
+ .Default();
+ registrar.Parameter("replicated_table_options", &TThis::ReplicatedTableOptions)
+ .Default();
+ registrar.Parameter("replication_card_collocation_id", &TThis::ReplicationCardCollocationId)
+ .Default();
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableReplicationCard)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void DeserializeImpl(TReplicationProgress& replicationProgress, TSerializableReplicationProgressPtr serializable)
+{
+ replicationProgress.UpperKey = std::move(serializable->UpperKey);
+ replicationProgress.Segments.reserve(serializable->Segments.size());
+
+ for (auto& segment : serializable->Segments) {
+ replicationProgress.Segments.push_back({
+ .LowerKey = std::move(segment->LowerKey),
+ .Timestamp = segment->Timestamp
+ });
+ }
+}
+
+void DeserializeImpl(TReplicaInfo& replicaInfo, TSerializableReplicaInfoPtr serializable)
+{
+ replicaInfo.ClusterName = serializable->ClusterName;
+ replicaInfo.ReplicaPath = serializable->ReplicaPath;
+ replicaInfo.ContentType = serializable->ContentType;
+ replicaInfo.Mode = serializable->Mode;
+ replicaInfo.State = serializable->State;
+ replicaInfo.ReplicationProgress = std::move(serializable->ReplicationProgress);
+}
+
+void DeserializeImpl(TReplicationCard& replicationCard, TSerializableReplicationCardPtr serializable)
+{
+ replicationCard.Replicas.clear();
+ for (const auto& [replicaId, replicaInfo] : serializable->Replicas) {
+ EmplaceOrCrash(replicationCard.Replicas, TReplicaId::FromString(replicaId), replicaInfo);
+ }
+ replicationCard.CoordinatorCellIds = std::move(serializable->CoordinatorCellIds);
+ replicationCard.Era = serializable->Era;
+ replicationCard.TableId = serializable->TableId;
+ replicationCard.TablePath = serializable->TablePath;
+ replicationCard.TableClusterName = serializable->TableClusterName;
+ replicationCard.CurrentTimestamp = serializable->CurrentTimestamp;
+ replicationCard.ReplicatedTableOptions = serializable->ReplicatedTableOptions;
+ replicationCard.ReplicationCardCollocationId = serializable->ReplicationCardCollocationId;
+}
+
+void Deserialize(TReplicationProgress& replicationProgress, INodePtr node)
+{
+ DeserializeImpl(replicationProgress, ConvertTo<TSerializableReplicationProgressPtr>(node));
+}
+
+void Deserialize(TReplicaInfo& replicaInfo, INodePtr node)
+{
+ DeserializeImpl(replicaInfo, ConvertTo<TSerializableReplicaInfoPtr>(node));
+}
+
+void Deserialize(TReplicationCard& replicationCard, INodePtr node)
+{
+ DeserializeImpl(replicationCard, ConvertTo<TSerializableReplicationCardPtr>(node));
+}
+
+void Deserialize(TReplicationProgress& replicationProgress, TYsonPullParserCursor* cursor)
+{
+ DeserializeImpl(replicationProgress, ExtractTo<TSerializableReplicationProgressPtr>(cursor));
+}
+
+void Deserialize(TReplicaInfo& replicaInfo, TYsonPullParserCursor* cursor)
+{
+ DeserializeImpl(replicaInfo, ExtractTo<TSerializableReplicaInfoPtr>(cursor));
+}
+
+void Deserialize(TReplicationCard& replicationCard, TYsonPullParserCursor* cursor)
+{
+ DeserializeImpl(replicationCard, ExtractTo<TSerializableReplicationCardPtr>(cursor));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TReplicationProgress& replicationProgress, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("segments").DoListFor(replicationProgress.Segments, [] (auto fluent, const auto& segment) {
+ fluent
+ .Item().BeginMap()
+ .Item("lower_key").Value(segment.LowerKey ? segment.LowerKey : EmptyKey())
+ .Item("timestamp").Value(segment.Timestamp)
+ .EndMap();
+ })
+ .Item("upper_key").Value(replicationProgress.UpperKey ? replicationProgress.UpperKey : EmptyKey())
+ .EndMap();
+}
+
+void Serialize(const TReplicaHistoryItem& replicaHistoryItem, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("era").Value(replicaHistoryItem.Era)
+ .Item("timestamp").Value(replicaHistoryItem.Timestamp)
+ .Item("mode").Value(replicaHistoryItem.Mode)
+ .Item("state").Value(replicaHistoryItem.State)
+ .EndMap();
+}
+
+void Serialize(
+ const TReplicaInfo& replicaInfo,
+ TFluentMap fluent,
+ const TReplicationCardFetchOptions& options)
+{
+ fluent
+ .Item("cluster_name").Value(replicaInfo.ClusterName)
+ .Item("replica_path").Value(replicaInfo.ReplicaPath)
+ .Item("content_type").Value(replicaInfo.ContentType)
+ .Item("mode").Value(replicaInfo.Mode)
+ .Item("state").Value(replicaInfo.State)
+ .Item("enable_replicated_table_tracker").Value(replicaInfo.EnableReplicatedTableTracker)
+ .DoIf(options.IncludeProgress, [&] (auto fluent) {
+ fluent
+ .Item("replication_progress").Value(replicaInfo.ReplicationProgress);
+ })
+ .DoIf(options.IncludeHistory, [&] (auto fluent) {
+ fluent
+ .Item("history").Value(replicaInfo.History);
+ });
+}
+
+void Serialize(
+ const TReplicaInfo& replicaInfo,
+ IYsonConsumer* consumer,
+ const TReplicationCardFetchOptions& options)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Do([&] (auto fluent) {
+ Serialize(replicaInfo, fluent, options);
+ })
+ .EndMap();
+}
+
+void Serialize(
+ const TReplicationCard& replicationCard,
+ IYsonConsumer* consumer,
+ const TReplicationCardFetchOptions& options)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Do([&] (auto fluent) {
+ Serialize(replicationCard, fluent, options);
+ })
+ .EndMap();
+}
+
+void Serialize(
+ const TReplicationCard& replicationCard,
+ TFluentMap fluent,
+ const TReplicationCardFetchOptions& options)
+{
+ fluent
+ .Item("replicas").DoMapFor(replicationCard.Replicas, [&] (auto fluent, const auto& pair) {
+ fluent
+ .Item(ToString(pair.first)).Do([&] (auto fluent) {
+ Serialize(pair.second, fluent.GetConsumer(), options);
+ });
+ })
+ .DoIf(options.IncludeCoordinators, [&] (auto fluent) {
+ fluent
+ .Item("coordinator_cell_ids").Value(replicationCard.CoordinatorCellIds);
+ })
+ .DoIf(options.IncludeReplicatedTableOptions && replicationCard.ReplicatedTableOptions, [&] (auto fluent) {
+ fluent
+ .Item("replicated_table_options").Value(replicationCard.ReplicatedTableOptions);
+ })
+ .Item("era").Value(replicationCard.Era)
+ .Item("table_id").Value(replicationCard.TableId)
+ .Item("table_path").Value(replicationCard.TablePath)
+ .Item("table_cluster_name").Value(replicationCard.TableClusterName)
+ .Item("current_timestamp").Value(replicationCard.CurrentTimestamp)
+ .Item("replication_card_collocation_id").Value(replicationCard.ReplicationCardCollocationId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NChaosClient::NProto::TReplicationProgress::TSegment* protoSegment, const TReplicationProgress::TSegment& segment)
+{
+ ToProto(protoSegment->mutable_lower_key(), segment.LowerKey);
+ protoSegment->set_timestamp(segment.Timestamp);
+}
+
+void FromProto(TReplicationProgress::TSegment* segment, const NChaosClient::NProto::TReplicationProgress::TSegment& protoSegment)
+{
+ segment->LowerKey = FromProto<TUnversionedOwningRow>(protoSegment.lower_key());
+ segment->Timestamp = protoSegment.timestamp();
+}
+
+void ToProto(NChaosClient::NProto::TReplicationProgress* protoReplicationProgress, const TReplicationProgress& replicationProgress)
+{
+ ToProto(protoReplicationProgress->mutable_segments(), replicationProgress.Segments);
+ ToProto(protoReplicationProgress->mutable_upper_key(), replicationProgress.UpperKey);
+}
+
+void FromProto(TReplicationProgress* replicationProgress, const NChaosClient::NProto::TReplicationProgress& protoReplicationProgress)
+{
+ FromProto(&replicationProgress->Segments, protoReplicationProgress.segments());
+ FromProto(&replicationProgress->UpperKey, protoReplicationProgress.upper_key());
+}
+
+void ToProto(NChaosClient::NProto::TReplicaHistoryItem* protoHistoryItem, const TReplicaHistoryItem& historyItem)
+{
+ protoHistoryItem->set_era(historyItem.Era);
+ protoHistoryItem->set_timestamp(ToProto<ui64>(historyItem.Timestamp));
+ protoHistoryItem->set_mode(ToProto<i32>(historyItem.Mode));
+ protoHistoryItem->set_state(ToProto<i32>(historyItem.State));
+}
+
+void FromProto(TReplicaHistoryItem* historyItem, const NChaosClient::NProto::TReplicaHistoryItem& protoHistoryItem)
+{
+ historyItem->Era = protoHistoryItem.era();
+ historyItem->Timestamp = FromProto<TTimestamp>(protoHistoryItem.timestamp());
+ historyItem->Mode = FromProto<ETableReplicaMode>(protoHistoryItem.mode());
+ historyItem->State = FromProto<ETableReplicaState>(protoHistoryItem.state());
+}
+
+void ToProto(
+ NChaosClient::NProto::TReplicaInfo* protoReplicaInfo,
+ const TReplicaInfo& replicaInfo,
+ const TReplicationCardFetchOptions& options)
+{
+ protoReplicaInfo->set_cluster_name(replicaInfo.ClusterName);
+ protoReplicaInfo->set_replica_path(replicaInfo.ReplicaPath);
+ protoReplicaInfo->set_content_type(ToProto<i32>(replicaInfo.ContentType));
+ protoReplicaInfo->set_mode(ToProto<i32>(replicaInfo.Mode));
+ protoReplicaInfo->set_state(ToProto<i32>(replicaInfo.State));
+ protoReplicaInfo->set_enable_replicated_table_tracker(replicaInfo.EnableReplicatedTableTracker);
+ if (options.IncludeProgress) {
+ ToProto(protoReplicaInfo->mutable_progress(), replicaInfo.ReplicationProgress);
+ }
+ if (options.IncludeHistory) {
+ ToProto(protoReplicaInfo->mutable_history(), replicaInfo.History);
+ }
+}
+
+void FromProto(TReplicaInfo* replicaInfo, const NChaosClient::NProto::TReplicaInfo& protoReplicaInfo)
+{
+ replicaInfo->ClusterName = protoReplicaInfo.cluster_name();
+ replicaInfo->ReplicaPath = protoReplicaInfo.replica_path();
+ replicaInfo->ContentType = FromProto<ETableReplicaContentType>(protoReplicaInfo.content_type());
+ replicaInfo->Mode = FromProto<ETableReplicaMode>(protoReplicaInfo.mode());
+ replicaInfo->State = FromProto<ETableReplicaState>(protoReplicaInfo.state());
+ if (protoReplicaInfo.has_progress()) {
+ FromProto(&replicaInfo->ReplicationProgress, protoReplicaInfo.progress());
+ }
+ FromProto(&replicaInfo->History, protoReplicaInfo.history());
+ if (protoReplicaInfo.has_enable_replicated_table_tracker()) {
+ replicaInfo->EnableReplicatedTableTracker = protoReplicaInfo.enable_replicated_table_tracker();
+ }
+}
+
+void ToProto(
+ NChaosClient::NProto::TReplicationCard* protoReplicationCard,
+ const TReplicationCard& replicationCard,
+ const TReplicationCardFetchOptions& options)
+{
+ for (const auto& [replicaId, replicaInfo] : SortHashMapByKeys(replicationCard.Replicas)) {
+ auto* protoReplicaEntry = protoReplicationCard->add_replicas();
+ ToProto(protoReplicaEntry->mutable_id(), replicaId);
+ ToProto(protoReplicaEntry->mutable_info(), replicaInfo, options);
+ }
+ if (options.IncludeCoordinators) {
+ ToProto(protoReplicationCard->mutable_coordinator_cell_ids(), replicationCard.CoordinatorCellIds);
+ }
+ if (options.IncludeReplicatedTableOptions && replicationCard.ReplicatedTableOptions) {
+ protoReplicationCard->set_replicated_table_options(ConvertToYsonString(replicationCard.ReplicatedTableOptions).ToString());
+ }
+ protoReplicationCard->set_era(replicationCard.Era);
+ ToProto(protoReplicationCard->mutable_table_id(), replicationCard.TableId);
+ protoReplicationCard->set_table_path(replicationCard.TablePath);
+ protoReplicationCard->set_table_cluster_name(replicationCard.TableClusterName);
+ protoReplicationCard->set_current_timestamp(replicationCard.CurrentTimestamp);
+ ToProto(protoReplicationCard->mutable_replication_card_collocation_id(), replicationCard.ReplicationCardCollocationId);
+}
+
+void FromProto(TReplicationCard* replicationCard, const NChaosClient::NProto::TReplicationCard& protoReplicationCard)
+{
+ for (const auto& protoEntry : protoReplicationCard.replicas()) {
+ auto replicaId = FromProto<TReplicaId>(protoEntry.id());
+ auto& replicaInfo = EmplaceOrCrash(replicationCard->Replicas, replicaId, TReplicaInfo())->second;
+ FromProto(&replicaInfo, protoEntry.info());
+ }
+ FromProto(&replicationCard->CoordinatorCellIds, protoReplicationCard.coordinator_cell_ids());
+ replicationCard->Era = protoReplicationCard.era();
+ replicationCard->TableId = FromProto<TTableId>(protoReplicationCard.table_id());
+ replicationCard->TablePath = protoReplicationCard.table_path();
+ replicationCard->TableClusterName = protoReplicationCard.table_cluster_name();
+ replicationCard->CurrentTimestamp = protoReplicationCard.current_timestamp();
+ if (protoReplicationCard.has_replicated_table_options()) {
+ replicationCard->ReplicatedTableOptions = ConvertTo<TReplicatedTableOptionsPtr>(TYsonString(protoReplicationCard.replicated_table_options()));
+ }
+ if (protoReplicationCard.has_replication_card_collocation_id()) {
+ FromProto(&replicationCard->ReplicationCardCollocationId, protoReplicationCard.replication_card_collocation_id());
+ }
+}
+
+void ToProto(
+ NChaosClient::NProto::TReplicationCardFetchOptions* protoOptions,
+ const TReplicationCardFetchOptions& options)
+{
+ protoOptions->set_include_coordinators(options.IncludeCoordinators);
+ protoOptions->set_include_progress(options.IncludeProgress);
+ protoOptions->set_include_history(options.IncludeHistory);
+ protoOptions->set_include_replicated_table_options(options.IncludeReplicatedTableOptions);
+}
+
+void FromProto(
+ TReplicationCardFetchOptions* options,
+ const NChaosClient::NProto::TReplicationCardFetchOptions& protoOptions)
+{
+ options->IncludeCoordinators = protoOptions.include_coordinators();
+ options->IncludeProgress = protoOptions.include_progress();
+ options->IncludeHistory = protoOptions.include_history();
+ options->IncludeReplicatedTableOptions = protoOptions.include_replicated_table_options();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chaos_client/replication_card_serialization.h b/yt/yt/client/chaos_client/replication_card_serialization.h
new file mode 100644
index 0000000000..7e69b2c43a
--- /dev/null
+++ b/yt/yt/client/chaos_client/replication_card_serialization.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "replication_card.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt_proto/yt/client/chaos_client/proto/replication_card.pb.h>
+
+namespace NYT::NChaosClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TReplicationProgress& replicationProgress, NYson::IYsonConsumer* consumer);
+void Serialize(const TReplicaHistoryItem& replicaHistoryItem, NYson::IYsonConsumer* consumer);
+void Serialize(
+ const TReplicaInfo& replicaInfo,
+ NYTree::TFluentMap fluent,
+ const TReplicationCardFetchOptions& options = {});
+void Serialize(
+ const TReplicaInfo& replicaInfo,
+ NYson::IYsonConsumer* consumer,
+ const TReplicationCardFetchOptions& options = {});
+void Serialize(
+ const TReplicationCard& replicationCard,
+ NYson::IYsonConsumer* consumer,
+ const TReplicationCardFetchOptions& options = {});
+void Serialize(
+ const TReplicationCard& replicationCard,
+ NYTree::TFluentMap fluent,
+ const TReplicationCardFetchOptions& options = {});
+
+void Deserialize(TReplicationProgress& replicationProgress, NYTree::INodePtr node);
+void Deserialize(TReplicaInfo& replicaInfo, NYTree::INodePtr node);
+void Deserialize(TReplicationCard& replicationCard, NYTree::INodePtr node);
+
+void Deserialize(TReplicationProgress& replicationProgress, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(TReplicaInfo& replicaInfo, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(TReplicationCard& replicationCard, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NChaosClient::NProto::TReplicationProgress* protoReplicationProgress,
+ const TReplicationProgress& replicationProgress);
+void FromProto(
+ TReplicationProgress* replicationProgress,
+ const NChaosClient::NProto::TReplicationProgress& protoReplicationProgress);
+
+void ToProto(
+ NChaosClient::NProto::TReplicaInfo* protoReplicaInfo,
+ const TReplicaInfo& replicaInfo,
+ const TReplicationCardFetchOptions& options = {});
+void FromProto(
+ TReplicaInfo* replicaInfo,
+ const NChaosClient::NProto::TReplicaInfo& protoReplicaInfo);
+
+void ToProto(
+ NChaosClient::NProto::TReplicationCard* protoReplicationCard,
+ const TReplicationCard& replicationCard,
+ const TReplicationCardFetchOptions& options = {});
+void FromProto(
+ TReplicationCard* replicationCard,
+ const NChaosClient::NProto::TReplicationCard& protoReplicationCard);
+
+void ToProto(
+ NChaosClient::NProto::TReplicationCardFetchOptions* protoOptions,
+ const TReplicationCardFetchOptions& options);
+void FromProto(
+ TReplicationCardFetchOptions* options,
+ const NChaosClient::NProto::TReplicationCardFetchOptions& protoOptions);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/chunk_client/chunk_replica-inl.h b/yt/yt/client/chunk_client/chunk_replica-inl.h
new file mode 100644
index 0000000000..08903dfbb0
--- /dev/null
+++ b/yt/yt/client/chunk_client/chunk_replica-inl.h
@@ -0,0 +1,290 @@
+#ifndef CHUNK_REPLICA_INL_H_
+#error "Direct inclusion of this file is not allowed, include chunk_replica.h"
+// For the sake of sane code completion.
+#include "chunk_replica.h"
+#endif
+
+#include <yt/yt/client/object_client/helpers.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static_assert(
+ ChunkReplicaIndexBound <= (1LL << 5),
+ "Replica index must fit into 5 bits.");
+static_assert(
+ MediumIndexBound <= (1LL << 7),
+ "Medium index must fit into 7 bits.");
+
+Y_FORCE_INLINE TChunkReplicaWithMedium::TChunkReplicaWithMedium()
+ : TChunkReplicaWithMedium(NNodeTrackerClient::InvalidNodeId)
+{ }
+
+Y_FORCE_INLINE TChunkReplicaWithMedium::TChunkReplicaWithMedium(ui64 value)
+ : Value(value)
+{ }
+
+Y_FORCE_INLINE TChunkReplicaWithMedium::TChunkReplicaWithMedium(
+ NNodeTrackerClient::TNodeId nodeId,
+ int replicaIndex,
+ int mediumIndex)
+ : Value(
+ static_cast<ui64>(nodeId) |
+ (static_cast<ui64>(replicaIndex) << 24) |
+ (static_cast<ui64>(mediumIndex) << 29))
+{
+ YT_ASSERT(nodeId >= 0 && nodeId <= static_cast<int>(NNodeTrackerClient::MaxNodeId));
+ YT_ASSERT(replicaIndex >= 0 && replicaIndex < ChunkReplicaIndexBound);
+ YT_ASSERT(mediumIndex >= 0 && mediumIndex < MediumIndexBound);
+}
+
+Y_FORCE_INLINE TChunkReplicaWithMedium::TChunkReplicaWithMedium(
+ TChunkReplica replica)
+ : TChunkReplicaWithMedium(
+ replica.GetNodeId(),
+ replica.GetReplicaIndex(),
+ GenericMediumIndex)
+{ }
+
+Y_FORCE_INLINE NNodeTrackerClient::TNodeId TChunkReplicaWithMedium::GetNodeId() const
+{
+ return Value & 0x00ffffff;
+}
+
+Y_FORCE_INLINE int TChunkReplicaWithMedium::GetReplicaIndex() const
+{
+ return (Value & 0x1f000000) >> 24;
+}
+
+Y_FORCE_INLINE int TChunkReplicaWithMedium::GetMediumIndex() const
+{
+ return Value >> 29;
+}
+
+Y_FORCE_INLINE TChunkReplica TChunkReplicaWithMedium::ToChunkReplica() const
+{
+ return TChunkReplica(GetNodeId(), GetReplicaIndex());
+}
+
+Y_FORCE_INLINE void ToProto(ui64* value, TChunkReplicaWithMedium replica)
+{
+ *value = replica.Value;
+}
+
+Y_FORCE_INLINE void FromProto(TChunkReplicaWithMedium* replica, ui64 value)
+{
+ replica->Value = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TChunkReplicaWithLocation::TChunkReplicaWithLocation()
+ : TChunkReplicaWithMedium()
+ , ChunkLocationUuid_(InvalidChunkLocationUuid)
+{ }
+
+Y_FORCE_INLINE TChunkReplicaWithLocation::TChunkReplicaWithLocation(
+ TChunkReplicaWithMedium replica,
+ TChunkLocationUuid locationUuid)
+ : TChunkReplicaWithMedium(replica)
+ , ChunkLocationUuid_(locationUuid)
+{ }
+
+Y_FORCE_INLINE TChunkReplicaWithLocation::TChunkReplicaWithLocation(
+ NNodeTrackerClient::TNodeId nodeId,
+ int replicaIndex,
+ int mediumIndex,
+ TChunkLocationUuid locationUuid)
+ : TChunkReplicaWithMedium(nodeId, replicaIndex, mediumIndex)
+ , ChunkLocationUuid_(locationUuid)
+{ }
+
+Y_FORCE_INLINE TChunkLocationUuid TChunkReplicaWithLocation::GetChunkLocationUuid() const
+{
+ return ChunkLocationUuid_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TChunkReplica::TChunkReplica()
+ : TChunkReplica(NNodeTrackerClient::InvalidNodeId)
+{ }
+
+Y_FORCE_INLINE TChunkReplica::TChunkReplica(ui32 value)
+ : Value(value)
+{ }
+
+Y_FORCE_INLINE TChunkReplica::TChunkReplica(int nodeId, int replicaIndex)
+ : Value(static_cast<ui64>(nodeId) | (static_cast<ui64>(replicaIndex) << 24))
+{
+ YT_ASSERT(nodeId >= 0 && nodeId <= static_cast<int>(NNodeTrackerClient::MaxNodeId));
+ YT_ASSERT(replicaIndex >= 0 && replicaIndex < ChunkReplicaIndexBound);
+}
+
+Y_FORCE_INLINE TChunkReplica::TChunkReplica(const TChunkReplicaWithMedium& replica)
+ : Value(static_cast<ui64>(replica.GetNodeId()) | (static_cast<ui64>(replica.GetReplicaIndex()) << 24))
+{ }
+
+Y_FORCE_INLINE NNodeTrackerClient::TNodeId TChunkReplica::GetNodeId() const
+{
+ return Value & 0x00ffffff;
+}
+
+Y_FORCE_INLINE int TChunkReplica::GetReplicaIndex() const
+{
+ return (Value & 0x1f000000) >> 24;
+}
+
+Y_FORCE_INLINE void ToProto(ui32* value, TChunkReplica replica)
+{
+ *value = replica.Value;
+}
+
+Y_FORCE_INLINE void FromProto(TChunkReplica* replica, ui32 value)
+{
+ replica->Value = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TChunkIdWithIndex::TChunkIdWithIndex()
+ : ReplicaIndex(GenericChunkReplicaIndex)
+{ }
+
+Y_FORCE_INLINE TChunkIdWithIndex::TChunkIdWithIndex(TChunkId id, int replicaIndex)
+ : Id(id)
+ , ReplicaIndex(replicaIndex)
+{ }
+
+Y_FORCE_INLINE bool operator==(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs)
+{
+ return lhs.Id == rhs.Id && lhs.ReplicaIndex == rhs.ReplicaIndex;
+}
+
+Y_FORCE_INLINE bool operator!=(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs)
+{
+ return !(lhs == rhs);
+}
+
+Y_FORCE_INLINE bool operator<(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs)
+{
+ if (lhs.Id == rhs.Id) {
+ return lhs.ReplicaIndex < rhs.ReplicaIndex;
+ }
+ return lhs.Id < rhs.Id;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TChunkIdWithIndexes::TChunkIdWithIndexes()
+ : TChunkIdWithIndex()
+ , MediumIndex(DefaultStoreMediumIndex)
+{ }
+
+Y_FORCE_INLINE TChunkIdWithIndexes::TChunkIdWithIndexes(const TChunkIdWithIndex& chunkIdWithIndex, int mediumIndex)
+ : TChunkIdWithIndex(chunkIdWithIndex)
+ , MediumIndex(mediumIndex)
+{ }
+
+Y_FORCE_INLINE TChunkIdWithIndexes::TChunkIdWithIndexes(TChunkId id, int replicaIndex, int mediumIndex)
+ : TChunkIdWithIndex(id, replicaIndex)
+ , MediumIndex(mediumIndex)
+{ }
+
+Y_FORCE_INLINE bool operator==(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs)
+{
+ return static_cast<const TChunkIdWithIndex&>(lhs) == static_cast<const TChunkIdWithIndex&>(rhs) &&
+ lhs.MediumIndex == rhs.MediumIndex;
+}
+
+Y_FORCE_INLINE bool operator!=(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs)
+{
+ return !(lhs == rhs);
+}
+
+Y_FORCE_INLINE bool operator<(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs)
+{
+ const auto& lhs_ = static_cast<const TChunkIdWithIndex&>(lhs);
+ const auto& rhs_ = static_cast<const TChunkIdWithIndex&>(rhs);
+ if (lhs_ == rhs_) {
+ return lhs.MediumIndex < rhs.MediumIndex;
+ }
+ return lhs_ < rhs_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool IsArtifactChunkId(TChunkId id)
+{
+ return NObjectClient::TypeFromId(id) == NObjectClient::EObjectType::Artifact;
+}
+
+inline bool IsPhysicalChunkType(NObjectClient::EObjectType type)
+{
+ return
+ type == NObjectClient::EObjectType::Chunk ||
+ type == NObjectClient::EObjectType::ErasureChunk ||
+ type == NObjectClient::EObjectType::JournalChunk ||
+ type == NObjectClient::EObjectType::ErasureJournalChunk;
+}
+
+inline bool IsJournalChunkType(NObjectClient::EObjectType type)
+{
+ return
+ type == NObjectClient::EObjectType::JournalChunk ||
+ type == NObjectClient::EObjectType::ErasureJournalChunk;
+}
+
+inline bool IsJournalChunkId(TChunkId id)
+{
+ return IsJournalChunkType(NObjectClient::TypeFromId(id));
+}
+
+inline bool IsBlobChunkType(NObjectClient::EObjectType type)
+{
+ return
+ type == NObjectClient::EObjectType::Chunk ||
+ type == NObjectClient::EObjectType::ErasureChunk;
+}
+
+inline bool IsBlobChunkId(TChunkId id)
+{
+ return IsBlobChunkType(NObjectClient::TypeFromId(id));
+}
+
+inline bool IsErasureChunkType(NObjectClient::EObjectType type)
+{
+ return
+ type == NObjectClient::EObjectType::ErasureChunk ||
+ type == NObjectClient::EObjectType::ErasureJournalChunk;
+}
+
+inline bool IsErasureChunkId(TChunkId id)
+{
+ return IsErasureChunkType(NObjectClient::TypeFromId(id));
+}
+
+inline bool IsErasureChunkPartId(TChunkId id)
+{
+ auto type = NObjectClient::TypeFromId(id);
+ return
+ type >= NObjectClient::MinErasureChunkPartType && type <= NObjectClient::MaxErasureChunkPartType ||
+ type >= NObjectClient::MinErasureJournalChunkPartType && type <= NObjectClient::MaxErasureJournalChunkPartType;
+}
+
+inline bool IsRegularChunkType(NObjectClient::EObjectType type)
+{
+ return
+ type == NObjectClient::EObjectType::Chunk ||
+ type == NObjectClient::EObjectType::JournalChunk;
+}
+
+inline bool IsRegularChunkId(TChunkId id)
+{
+ return IsRegularChunkType(NObjectClient::TypeFromId(id));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/chunk_replica.cpp b/yt/yt/client/chunk_client/chunk_replica.cpp
new file mode 100644
index 0000000000..8c21a83e5b
--- /dev/null
+++ b/yt/yt/client/chunk_client/chunk_replica.cpp
@@ -0,0 +1,260 @@
+#include "chunk_replica.h"
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <yt/yt/library/erasure/public.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/confirm_chunk_replica_info.pb.h>
+
+namespace NYT::NChunkClient {
+
+using namespace NNodeTrackerClient;
+using namespace NObjectClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkIdWithIndex::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ Save(context, Id);
+ Save(context, ReplicaIndex);
+}
+
+void TChunkIdWithIndex::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+
+ Load(context, Id);
+ Load(context, ReplicaIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkIdWithIndexes::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ Save(context, Id);
+ Save(context, ReplicaIndex);
+ Save(context, MediumIndex);
+}
+
+void TChunkIdWithIndexes::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+
+ Load(context, Id);
+ Load(context, ReplicaIndex);
+ Load(context, MediumIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TConfirmChunkReplicaInfo* value, TChunkReplicaWithLocation replica)
+{
+ using NYT::ToProto;
+
+ value->set_replica(replica.Value);
+ ToProto(value->mutable_location_uuid(), replica.ChunkLocationUuid_);
+}
+
+void FromProto(TChunkReplicaWithLocation* replica, NProto::TConfirmChunkReplicaInfo value)
+{
+ using NYT::FromProto;
+
+ replica->Value = value.replica();
+ replica->ChunkLocationUuid_ = FromProto<TGuid>(value.location_uuid());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplicaWithLocation replica, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", replica.GetNodeId());
+ if (replica.GetReplicaIndex() != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", replica.GetReplicaIndex());
+ }
+ builder->AppendFormat("@%v", replica.GetChunkLocationUuid());
+}
+
+TString ToString(TChunkReplicaWithLocation replica)
+{
+ return ToStringViaBuilder(replica);
+}
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplicaWithMedium replica, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", replica.GetNodeId());
+ if (replica.GetReplicaIndex() != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", replica.GetReplicaIndex());
+ }
+ if (replica.GetMediumIndex() == AllMediaIndex) {
+ builder->AppendString("@all");
+ } else if (replica.GetMediumIndex() != GenericMediumIndex) {
+ builder->AppendFormat("@%v", replica.GetMediumIndex());
+ }
+}
+
+TString ToString(TChunkReplicaWithMedium replica)
+{
+ return ToStringViaBuilder(replica);
+}
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplica replica, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", replica.GetNodeId());
+ if (replica.GetReplicaIndex() != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", replica.GetReplicaIndex());
+ }
+}
+
+TString ToString(TChunkReplica replica)
+{
+ return ToStringViaBuilder(replica);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TChunkIdWithIndex& id, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", id.Id);
+ if (id.ReplicaIndex != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", id.ReplicaIndex);
+ }
+}
+
+TString ToString(const TChunkIdWithIndex& id)
+{
+ return ToStringViaBuilder(id);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TChunkIdWithIndexes& id, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", id.Id);
+ if (id.ReplicaIndex != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", id.ReplicaIndex);
+ }
+ if (id.MediumIndex == AllMediaIndex) {
+ builder->AppendString("@all");
+ } else if (id.MediumIndex != GenericMediumIndex) {
+ builder->AppendFormat("@%v", id.MediumIndex);
+ }
+}
+
+TString ToString(const TChunkIdWithIndexes& id)
+{
+ return ToStringViaBuilder(id);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChunkReplicaAddressFormatter::TChunkReplicaAddressFormatter(TNodeDirectoryPtr nodeDirectory)
+ : NodeDirectory_(std::move(nodeDirectory))
+{ }
+
+void TChunkReplicaAddressFormatter::operator()(TStringBuilderBase* builder, TChunkReplicaWithMedium replica) const
+{
+ if (const auto* descriptor = NodeDirectory_->FindDescriptor(replica.GetNodeId())) {
+ builder->AppendFormat("%v", *descriptor);
+ } else {
+ builder->AppendFormat("<unresolved-%v>", replica.GetNodeId());
+ }
+ if (replica.GetReplicaIndex() != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", replica.GetReplicaIndex());
+ }
+ if (replica.GetMediumIndex() == AllMediaIndex) {
+ builder->AppendString("@all");
+ } else if (replica.GetMediumIndex() != GenericMediumIndex) {
+ builder->AppendFormat("@%v", replica.GetMediumIndex());
+ }
+}
+
+void TChunkReplicaAddressFormatter::operator()(TStringBuilderBase* builder, TChunkReplica replica) const
+{
+ const auto* descriptor = NodeDirectory_->FindDescriptor(replica.GetNodeId());
+ if (descriptor) {
+ builder->AppendFormat("%v", descriptor->GetDefaultAddress());
+ } else {
+ builder->AppendFormat("<unresolved-%v>", replica.GetNodeId());
+ }
+ if (replica.GetReplicaIndex() != GenericChunkReplicaIndex) {
+ builder->AppendFormat("/%v", replica.GetReplicaIndex());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+EObjectType BaseErasurePartTypeFromPartId(TChunkId id)
+{
+ auto type = TypeFromId(id);
+ if (type >= MinErasureChunkPartType && type <= MaxErasureChunkPartType) {
+ return EObjectType::ErasureChunkPart_0;
+ } else if (type >= MinErasureJournalChunkPartType && type <= MaxErasureJournalChunkPartType) {
+ return EObjectType::ErasureJournalChunkPart_0;
+ } else {
+ YT_ABORT();
+ }
+}
+
+EObjectType BaseErasurePartTypeFromWholeId(TChunkId id)
+{
+ switch (TypeFromId(id)) {
+ case EObjectType::ErasureChunk: return EObjectType::ErasureChunkPart_0;
+ case EObjectType::ErasureJournalChunk: return EObjectType::ErasureJournalChunkPart_0;
+ default: YT_ABORT();
+ }
+}
+
+EObjectType WholeErasureTypeFromPartId(TChunkId id)
+{
+ auto type = TypeFromId(id);
+ if (type >= MinErasureChunkPartType && type <= MaxErasureChunkPartType) {
+ return EObjectType::ErasureChunk;
+ } else if (type >= MinErasureJournalChunkPartType && type <= MaxErasureJournalChunkPartType) {
+ return EObjectType::ErasureJournalChunk;
+ } else {
+ YT_ABORT();
+ }
+}
+
+} // namespace
+
+TChunkId ErasurePartIdFromChunkId(TChunkId id, int index)
+{
+ return ReplaceTypeInId(id, static_cast<EObjectType>(static_cast<int>(BaseErasurePartTypeFromWholeId(id)) + index));
+}
+
+TChunkId ErasureChunkIdFromPartId(TChunkId id)
+{
+ return ReplaceTypeInId(id, WholeErasureTypeFromPartId(id));
+}
+
+int ReplicaIndexFromErasurePartId(TChunkId id)
+{
+ int index = static_cast<int>(TypeFromId(id)) - static_cast<int>(BaseErasurePartTypeFromPartId(id));
+ YT_VERIFY(index >= 0 && index < ChunkReplicaIndexBound);
+ return index;
+}
+
+TChunkId EncodeChunkId(const TChunkIdWithIndex& idWithIndex)
+{
+ return IsErasureChunkId(idWithIndex.Id)
+ ? ErasurePartIdFromChunkId(idWithIndex.Id, idWithIndex.ReplicaIndex)
+ : idWithIndex.Id;
+}
+
+TChunkIdWithIndex DecodeChunkId(TChunkId id)
+{
+ return IsErasureChunkPartId(id)
+ ? TChunkIdWithIndex(ErasureChunkIdFromPartId(id), ReplicaIndexFromErasurePartId(id))
+ : TChunkIdWithIndex(id, GenericChunkReplicaIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/chunk_replica.h b/yt/yt/client/chunk_client/chunk_replica.h
new file mode 100644
index 0000000000..c666254e51
--- /dev/null
+++ b/yt/yt/client/chunk_client/chunk_replica.h
@@ -0,0 +1,260 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/node_tracker_client/public.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TConfirmChunkReplicaInfo;
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(ui64* value, TChunkReplicaWithMedium replica);
+void FromProto(TChunkReplicaWithMedium* replica, ui64 value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A compact representation of |(nodeId, replicaIndex, mediumIndex)| triplet.
+class TChunkReplicaWithMedium
+{
+public:
+ TChunkReplicaWithMedium();
+ TChunkReplicaWithMedium(
+ NNodeTrackerClient::TNodeId nodeId,
+ int replicaIndex,
+ int mediumIndex);
+ // NB: Will be assigned to generic medium.
+ explicit TChunkReplicaWithMedium(TChunkReplica replica);
+
+ NNodeTrackerClient::TNodeId GetNodeId() const;
+ int GetReplicaIndex() const;
+ int GetMediumIndex() const;
+
+ TChunkReplica ToChunkReplica() const;
+
+private:
+ /*!
+ * Bits:
+ * 0-23: node id
+ * 24-28: replica index (5 bits)
+ * 29-37: medium index (7 bits)
+ */
+ ui64 Value;
+
+ explicit TChunkReplicaWithMedium(ui64 value);
+
+ friend void ToProto(ui64* value, TChunkReplicaWithMedium replica);
+ friend void FromProto(TChunkReplicaWithMedium* replica, ui64 value);
+ friend void ToProto(NProto::TConfirmChunkReplicaInfo* value, TChunkReplicaWithLocation replica);
+ friend void FromProto(TChunkReplicaWithLocation* replica, NProto::TConfirmChunkReplicaInfo value);
+};
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplicaWithMedium replica, TStringBuf spec);
+TString ToString(TChunkReplicaWithMedium replica);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TConfirmChunkReplicaInfo* value, TChunkReplicaWithLocation replica);
+void FromProto(TChunkReplicaWithLocation* replica, NProto::TConfirmChunkReplicaInfo value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// COMPAT(kvk1920): Remove GetMediumIndex().
+class TChunkReplicaWithLocation
+ : public TChunkReplicaWithMedium
+{
+public:
+ TChunkReplicaWithLocation();
+ TChunkReplicaWithLocation(TChunkReplicaWithMedium replica, TChunkLocationUuid locationUuid);
+ TChunkReplicaWithLocation(
+ NNodeTrackerClient::TNodeId nodeId,
+ int replicaIndex,
+ int mediumIndex,
+ TChunkLocationUuid locationUuid);
+
+ TChunkLocationUuid GetChunkLocationUuid() const;
+
+ friend void ToProto(NProto::TConfirmChunkReplicaInfo* value, TChunkReplicaWithLocation replica);
+ friend void FromProto(TChunkReplicaWithLocation* replica, NProto::TConfirmChunkReplicaInfo value);
+
+private:
+ TChunkLocationUuid ChunkLocationUuid_;
+};
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplicaWithLocation replica, TStringBuf spec);
+TString ToString(TChunkReplicaWithLocation replica);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkReplica
+{
+public:
+ TChunkReplica();
+ TChunkReplica(int nodeId, int replicaIndex);
+ TChunkReplica(const TChunkReplicaWithMedium& replica);
+
+ NNodeTrackerClient::TNodeId GetNodeId() const;
+ int GetReplicaIndex() const;
+
+private:
+ /*!
+ * Bits:
+ * 0-23: node id
+ * 24-28: replica index (5 bits)
+ */
+ ui32 Value;
+
+ explicit TChunkReplica(ui32 value);
+
+ friend void ToProto(ui32* value, TChunkReplica replica);
+ friend void FromProto(TChunkReplica* replica, ui32 value);
+};
+
+void FormatValue(TStringBuilderBase* builder, TChunkReplica replica, TStringBuf spec);
+TString ToString(TChunkReplica replica);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TChunkIdWithIndex
+{
+ TChunkIdWithIndex();
+ TChunkIdWithIndex(TChunkId id, int replicaIndex);
+
+ TChunkId Id;
+ int ReplicaIndex;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TChunkIdWithIndexes
+ : public TChunkIdWithIndex
+{
+ TChunkIdWithIndexes();
+ TChunkIdWithIndexes(const TChunkIdWithIndex& chunkIdWithIndex, int mediumIndex);
+ TChunkIdWithIndexes(TChunkId id, int replicaIndex, int mediumIndex);
+
+ int MediumIndex;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs);
+bool operator!=(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs);
+bool operator<(const TChunkIdWithIndex& lhs, const TChunkIdWithIndex& rhs);
+
+TString ToString(const TChunkIdWithIndex& id);
+
+bool operator==(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs);
+bool operator!=(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs);
+bool operator<(const TChunkIdWithIndexes& lhs, const TChunkIdWithIndexes& rhs);
+
+TString ToString(const TChunkIdWithIndexes& id);
+
+//! Returns |true| iff this is an artifact chunk.
+bool IsArtifactChunkId(TChunkId id);
+
+//! Returns |true| iff this is a chunk or any type (journal or blob, replicated or erasure-coded).
+bool IsPhysicalChunkType(NObjectClient::EObjectType type);
+
+//! Returns |true| iff this is a journal chunk type.
+bool IsJournalChunkType(NObjectClient::EObjectType type);
+
+//! Returns |true| iff this is a journal chunk.
+bool IsJournalChunkId(TChunkId id);
+
+//! Returns |true| iff this is a blob chunk (regular or erasure).
+bool IsBlobChunkType(NObjectClient::EObjectType type);
+
+//! Returns |true| iff this is a blob chunk (regular or erasure).
+bool IsBlobChunkId(TChunkId id);
+
+//! Returns |true| iff this is an erasure chunk.
+bool IsErasureChunkType(NObjectClient::EObjectType type);
+
+//! Returns |true| iff this is an erasure chunk.
+bool IsErasureChunkId(TChunkId id);
+
+//! Returns |true| iff this is an erasure chunk part.
+bool IsErasureChunkPartId(TChunkId id);
+
+//! Returns |true| iff this is a regular (not erasure) chunk.
+inline bool IsRegularChunkType(NObjectClient::EObjectType type);
+
+//! Returns |true| iff this is a regular (not erasure) chunk.
+inline bool IsRegularChunkId(TChunkId id);
+
+//! Returns id for a part of a given erasure chunk.
+TChunkId ErasurePartIdFromChunkId(TChunkId id, int index);
+
+//! Returns the whole chunk id for a given erasure chunk part id.
+TChunkId ErasureChunkIdFromPartId(TChunkId id);
+
+//! Returns part index for a given erasure chunk part id.
+int ReplicaIndexFromErasurePartId(TChunkId id);
+
+//! For usual chunks, preserves the id.
+//! For erasure chunks, constructs the part id using the given replica index.
+TChunkId EncodeChunkId(const TChunkIdWithIndex& idWithIndex);
+
+//! For regular chunks, preserves the id and returns #GenericChunkReplicaIndex.
+//! For erasure chunk parts, constructs the whole chunk id and extracts part index.
+TChunkIdWithIndex DecodeChunkId(TChunkId id);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkReplicaAddressFormatter
+{
+public:
+ explicit TChunkReplicaAddressFormatter(NNodeTrackerClient::TNodeDirectoryPtr nodeDirectory);
+
+ void operator()(TStringBuilderBase* builder, TChunkReplicaWithMedium replica) const;
+
+ void operator()(TStringBuilderBase* builder, TChunkReplica replica) const;
+
+private:
+ NNodeTrackerClient::TNodeDirectoryPtr NodeDirectory_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
+
+//! A hasher for TChunkIdWithIndex.
+template <>
+struct THash<NYT::NChunkClient::TChunkIdWithIndex>
+{
+ inline size_t operator()(const NYT::NChunkClient::TChunkIdWithIndex& value) const
+ {
+ return THash<NYT::NChunkClient::TChunkId>()(value.Id) * 497 + value.ReplicaIndex;
+ }
+};
+
+//! A hasher for TChunkIdWithIndexes.
+template <>
+struct THash<NYT::NChunkClient::TChunkIdWithIndexes>
+{
+ inline size_t operator()(const NYT::NChunkClient::TChunkIdWithIndexes& value) const
+ {
+ return THash<NYT::NChunkClient::TChunkId>()(value.Id) * 497 +
+ value.ReplicaIndex + value.MediumIndex * 8;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define CHUNK_REPLICA_INL_H_
+#include "chunk_replica-inl.h"
+#undef CHUNK_REPLICA_INL_H_
diff --git a/yt/yt/client/chunk_client/config.cpp b/yt/yt/client/chunk_client/config.cpp
new file mode 100644
index 0000000000..2e6a3d9ec4
--- /dev/null
+++ b/yt/yt/client/chunk_client/config.cpp
@@ -0,0 +1,434 @@
+#include "config.h"
+
+#include <yt/yt/core/rpc/config.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFetchChunkSpecConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_chunks_per_fetch", &TThis::MaxChunksPerFetch)
+ .GreaterThan(0)
+ .Default(100000);
+ registrar.Parameter("max_chunks_per_locate_request", &TThis::MaxChunksPerLocateRequest)
+ .GreaterThan(0)
+ .Default(10000);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFetcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("node_rpc_timeout", &TThis::NodeRpcTimeout)
+ .Default(TDuration::Seconds(30));
+
+ registrar.Parameter("node_ban_duration", &TThis::NodeBanDuration)
+ .Default(TDuration::Seconds(5));
+
+ registrar.Parameter("backoff_time", &TThis::BackoffTime)
+ .Default(TDuration::MilliSeconds(100));
+
+ registrar.Parameter("max_chunks_per_node_fetch", &TThis::MaxChunksPerNodeFetch)
+ .Default(500);
+
+ registrar.Parameter("node_directory_synchronization_timeout", &TThis::NodeDirectorySynchronizationTimeout)
+ .Default(TDuration::Minutes(5));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBlockReordererConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_block_reordering", &TThis::EnableBlockReordering)
+ .Default(false);
+
+ registrar.Parameter("shuffle_blocks", &TThis::ShuffleBlocks)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkSliceFetcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_slices_per_fetch", &TThis::MaxSlicesPerFetch)
+ .GreaterThan(0)
+ .Default(10'000);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TEncodingWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("encode_window_size", &TThis::EncodeWindowSize)
+ .Default(16_MB)
+ .GreaterThan(0);
+ registrar.Parameter("default_compression_ratio", &TThis::DefaultCompressionRatio)
+ .Default(0.2);
+ registrar.Parameter("verify_compression", &TThis::VerifyCompression)
+ .Default(true);
+ registrar.Parameter("compute_checksum", &TThis::ComputeChecksum)
+ .Default(true);
+ registrar.Parameter("compression_concurrency", &TThis::CompressionConcurrency)
+ .Default(1)
+ .GreaterThan(0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TReplicationReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("block_rpc_timeout", &TThis::BlockRpcTimeout)
+ .Default(TDuration::Seconds(120));
+ registrar.Parameter("block_rpc_hedging_delay", &TThis::BlockRpcHedgingDelay)
+ .Default();
+ registrar.Parameter("lookup_rpc_hedging_delay", &TThis::LookupRpcHedgingDelay)
+ .Default();
+ registrar.Parameter("cancel_primary_block_rpc_request_on_hedging", &TThis::CancelPrimaryBlockRpcRequestOnHedging)
+ .Default(false);
+ registrar.Parameter("cancel_primary_lookup_rpc_request_on_hedging", &TThis::CancelPrimaryLookupRpcRequestOnHedging)
+ .Default(false);
+ registrar.Parameter("lookup_rpc_timeout", &TThis::LookupRpcTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("meta_rpc_timeout", &TThis::MetaRpcTimeout)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("meta_rpc_hedging_delay", &TThis::MetaRpcHedgingDelay)
+ .Default();
+ registrar.Parameter("probe_rpc_timeout", &TThis::ProbeRpcTimeout)
+ .Default(TDuration::Seconds(5));
+ registrar.Parameter("probe_peer_count", &TThis::ProbePeerCount)
+ .Default(3)
+ .GreaterThan(0);
+ registrar.Parameter("retry_count", &TThis::RetryCount)
+ .Default(20);
+ registrar.Parameter("fail_on_no_seeds", &TThis::FailOnNoSeeds)
+ .Default(false);
+ registrar.Parameter("min_backoff_time", &TThis::MinBackoffTime)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("max_backoff_time", &TThis::MaxBackoffTime)
+ .Default(TDuration::Seconds(60));
+ registrar.Parameter("backoff_time_multiplier", &TThis::BackoffTimeMultiplier)
+ .GreaterThan(1)
+ .Default(1.5);
+ registrar.Parameter("pass_count", &TThis::PassCount)
+ .Default(500);
+ registrar.Parameter("fetch_from_peers", &TThis::FetchFromPeers)
+ .Default(true);
+ registrar.Parameter("peer_expiration_timeout", &TThis::PeerExpirationTimeout)
+ .Default(TDuration::Seconds(300));
+ registrar.Parameter("populate_cache", &TThis::PopulateCache)
+ .Default(true);
+ registrar.Parameter("prefer_local_host", &TThis::PreferLocalHost)
+ .Default(false);
+ registrar.Parameter("prefer_local_rack", &TThis::PreferLocalRack)
+ .Default(false);
+ registrar.Parameter("prefer_local_data_center", &TThis::PreferLocalDataCenter)
+ .Default(true);
+ registrar.Parameter("max_ban_count", &TThis::MaxBanCount)
+ .Default(5);
+ registrar.Parameter("disk_queue_size_factor", &TThis::DiskQueueSizeFactor)
+ .Default(1.0);
+ registrar.Parameter("net_queue_size_factor", &TThis::NetQueueSizeFactor)
+ .Default(0.5);
+ registrar.Parameter("enable_workload_fifo_scheduling", &TThis::EnableWorkloadFifoScheduling)
+ .Default(true);
+ registrar.Parameter("retry_timeout", &TThis::RetryTimeout)
+ .Default(TDuration::Minutes(3));
+ registrar.Parameter("session_timeout", &TThis::SessionTimeout)
+ .Default(TDuration::Minutes(20));
+ registrar.Parameter("lookup_request_pass_count", &TThis::LookupRequestPassCount)
+ .GreaterThan(0)
+ .Default(10);
+ registrar.Parameter("lookup_request_retry_count", &TThis::LookupRequestRetryCount)
+ .GreaterThan(0)
+ .Default(5);
+ registrar.Parameter("use_async_block_cache", &TThis::UseAsyncBlockCache)
+ .Default(false);
+ registrar.Parameter("use_block_cache", &TThis::UseBlockCache)
+ .Default(true);
+ registrar.Parameter("suspicious_node_grace_period", &TThis::SuspiciousNodeGracePeriod)
+ .Default();
+ registrar.Parameter("prolonged_discard_seeds_delay", &TThis::ProlongedDiscardSeedsDelay)
+ .Default(TDuration::Minutes(1));
+ registrar.Parameter("enable_chunk_meta_cache", &TThis::EnableChunkMetaCache)
+ .Default(true);
+ registrar.Parameter("ban_peers_permanently", &TThis::BanPeersPermanently)
+ .Default(true);
+ registrar.Parameter("enable_local_throttling", &TThis::EnableLocalThrottling)
+ .Default(false);
+ registrar.Parameter("chunk_meta_cache_failure_probability", &TThis::ChunkMetaCacheFailureProbability)
+ .Default();
+ registrar.Parameter("use_direct_io", &TThis::UseDirectIO)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ // Seems unreasonable to make backoff greater than half of total session timeout.
+ config->MaxBackoffTime = std::min(config->MaxBackoffTime, config->SessionTimeout / 2);
+ config->RetryTimeout = std::min(config->RetryTimeout, config->SessionTimeout);
+
+ // Rpc timeout should not exceed session timeout.
+ config->BlockRpcTimeout = std::min(config->BlockRpcTimeout, config->RetryTimeout);
+ config->LookupRpcTimeout = std::min(config->LookupRpcTimeout, config->RetryTimeout);
+ config->MetaRpcTimeout = std::min(config->MetaRpcTimeout, config->RetryTimeout);
+ config->ProbeRpcTimeout = std::min(config->ProbeRpcTimeout, config->RetryTimeout);
+
+ // These are supposed to be not greater than PassCount and RetryCount.
+ config->LookupRequestPassCount = std::min(config->LookupRequestPassCount, config->PassCount);
+ config->LookupRequestRetryCount = std::min(config->LookupRequestRetryCount, config->RetryCount);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBlockFetcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("window_size", &TThis::WindowSize)
+ .Default(20_MB)
+ .GreaterThan(0);
+ registrar.Parameter("group_size", &TThis::GroupSize)
+ .Default(15_MB)
+ .GreaterThan(0);
+
+ registrar.Parameter("use_uncompressed_block_cache", &TThis::UseUncompressedBlockCache)
+ .Default(true);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->GroupSize > config->WindowSize) {
+ THROW_ERROR_EXCEPTION("\"group_size\" cannot be larger than \"window_size\"");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TErasureReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_auto_repair", &TThis::EnableAutoRepair)
+ .Default(true);
+ registrar.Parameter("replication_reader_speed_limit_per_sec", &TThis::ReplicationReaderSpeedLimitPerSec)
+ .Default(5_MB);
+ registrar.Parameter("slow_reader_expiration_timeout", &TThis::SlowReaderExpirationTimeout)
+ .Default(TDuration::Minutes(2));
+ registrar.Parameter("replication_reader_timeout", &TThis::ReplicationReaderTimeout)
+ .Default(TDuration::Seconds(60));
+ registrar.Parameter("replication_reader_failure_timeout", &TThis::ReplicationReaderFailureTimeout)
+ .Default(TDuration::Minutes(10));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TMultiChunkReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_buffer_size", &TThis::MaxBufferSize)
+ .GreaterThan(0L)
+ .LessThanOrEqual(10_GB)
+ .Default(100_MB);
+ registrar.Parameter("max_parallel_readers", &TThis::MaxParallelReaders)
+ .GreaterThanOrEqual(1)
+ .LessThanOrEqual(1000)
+ .Default(512);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MaxBufferSize < 2 * config->WindowSize) {
+ THROW_ERROR_EXCEPTION("\"max_buffer_size\" cannot be less than twice \"window_size\"");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TReplicationWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("send_window_size", &TThis::SendWindowSize)
+ .Default(32_MB)
+ .GreaterThan(0);
+ registrar.Parameter("group_size", &TThis::GroupSize)
+ .Default(10_MB)
+ .GreaterThan(0);
+ registrar.Parameter("node_channel", &TThis::NodeChannel)
+ .DefaultNew();
+ registrar.Parameter("node_rpc_timeout", &TThis::NodeRpcTimeout)
+ .Default(TDuration::Seconds(300));
+ registrar.Parameter("upload_replication_factor", &TThis::UploadReplicationFactor)
+ .GreaterThanOrEqual(1)
+ .Default(2);
+ registrar.Parameter("ban_failed_nodes", &TThis::BanFailedNodes)
+ .Default(true);
+ registrar.Parameter("min_upload_replication_factor", &TThis::MinUploadReplicationFactor)
+ .Default(2)
+ .GreaterThanOrEqual(1);
+ registrar.Parameter("direct_upload_node_count", &TThis::DirectUploadNodeCount)
+ .Default();
+ registrar.Parameter("prefer_local_host", &TThis::PreferLocalHost)
+ .Default(true);
+ registrar.Parameter("node_ping_interval", &TThis::NodePingPeriod)
+ .Default(TDuration::Seconds(10));
+ registrar.Parameter("populate_cache", &TThis::PopulateCache)
+ .Default(false);
+ registrar.Parameter("sync_on_close", &TThis::SyncOnClose)
+ .Default(true);
+ registrar.Parameter("enable_direct_io", &TThis::EnableDirectIO)
+ .Default(false);
+ registrar.Parameter("enable_early_finish", &TThis::EnableEarlyFinish)
+ .Default(false);
+ registrar.Parameter("allocate_write_targets_backoff_time", &TThis::AllocateWriteTargetsBackoffTime)
+ .Default(TDuration::Seconds(5));
+ registrar.Parameter("allocate_write_targets_retry_count", &TThis::AllocateWriteTargetsRetryCount)
+ .Default(10);
+
+ registrar.Parameter("testing_delay", &TThis::TestingDelay)
+ .Default();
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->NodeChannel->RetryBackoffTime = TDuration::Seconds(10);
+ config->NodeChannel->RetryAttempts = 100;
+ });
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (!config->DirectUploadNodeCount) {
+ return;
+ }
+
+ if (*config->DirectUploadNodeCount < 1) {
+ THROW_ERROR_EXCEPTION("\"direct_upload_node_count\" cannot be less that 1");
+ }
+ });
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->SendWindowSize < config->GroupSize) {
+ THROW_ERROR_EXCEPTION("\"send_window_size\" cannot be less than \"group_size\"");
+ }
+ });
+}
+
+int TReplicationWriterConfig::GetDirectUploadNodeCount()
+{
+ auto replicationFactor = std::min(MinUploadReplicationFactor, UploadReplicationFactor);
+ if (DirectUploadNodeCount) {
+ return std::min(*DirectUploadNodeCount, replicationFactor);
+ }
+
+ return std::max(static_cast<int>(std::sqrt(replicationFactor)), 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TErasureWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_erasure_target_node_reallocation", &TThis::EnableErasureTargetNodeReallocation)
+ .Default(false);
+
+ registrar.Parameter("erasure_window_size", &TThis::ErasureWindowSize)
+ .Default(8_MB)
+ .GreaterThan(0);
+
+ registrar.Parameter("writer_window_size", &TThis::WriterWindowSize)
+ .Default(64_MB)
+ .GreaterThan(0);
+ registrar.Parameter("writer_group_size", &TThis::WriterGroupSize)
+ .Default(16_MB)
+ .GreaterThan(0);
+
+ registrar.Parameter("desired_segment_part_size", &TThis::DesiredSegmentPartSize)
+ .Default(0)
+ .GreaterThanOrEqual(0);
+
+ registrar.Parameter("erasure_store_original_block_checksums", &TThis::ErasureStoreOriginalBlockChecksums)
+ .Default(false);
+
+ registrar.Parameter("enable_striped_erasure", &TThis::EnableStripedErasure)
+ .Default(false);
+
+ registrar.Parameter("erasure_stripe_size", &TThis::ErasureStripeSize)
+ .Default()
+ .GreaterThan(0);
+
+ registrar.Parameter("use_effective_erasure_codecs", &TThis::UseEffectiveErasureCodecs)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TMultiChunkWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("desired_chunk_size", &TThis::DesiredChunkSize)
+ .GreaterThan(0)
+ .Default(2_GB);
+
+ registrar.Parameter("desired_chunk_weight", &TThis::DesiredChunkWeight)
+ .GreaterThan(0)
+ .Default(100_GB);
+
+ registrar.Parameter("max_meta_size", &TThis::MaxMetaSize)
+ .GreaterThan(0)
+ .LessThanOrEqual(64_MB)
+ .Default(30_MB);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TEncodingWriterOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("compression_codec", &TThis::CompressionCodec)
+ .Default(NCompression::ECodec::None);
+ registrar.Parameter("chunks_eden", &TThis::ChunksEden)
+ .Default(false);
+ registrar.Parameter("set_chunk_creation_time", &TThis::SetChunkCreationTime)
+ .Default(true)
+ .DontSerializeDefault();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkFragmentReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("peer_info_expiration_timeout", &TThis::PeerInfoExpirationTimeout)
+ .Default(TDuration::Minutes(30));
+
+ registrar.Parameter("seeds_expiration_timeout", &TThis::SeedsExpirationTimeout)
+ .Default(TDuration::Seconds(3));
+
+ registrar.Parameter("periodic_update_delay", &TThis::PeriodicUpdateDelay)
+ .GreaterThan(TDuration::Zero())
+ .Default(TDuration::Seconds(10));
+
+ registrar.Parameter("net_queue_size_factor", &TThis::NetQueueSizeFactor)
+ .Default(0.5);
+ registrar.Parameter("disk_queue_size_factor", &TThis::DiskQueueSizeFactor)
+ .Default(1.0);
+
+ registrar.Parameter("probe_chunk_set_rpc_timeout", &TThis::ProbeChunkSetRpcTimeout)
+ .Default(TDuration::Seconds(5));
+ registrar.Parameter("get_chunk_fragment_set_rpc_timeout", &TThis::GetChunkFragmentSetRpcTimeout)
+ .Default(TDuration::Seconds(15));
+
+ registrar.Parameter("fragment_read_hedging_delay", &TThis::FragmentReadHedgingDelay)
+ .Default();
+
+ registrar.Parameter("retry_count_limit", &TThis::RetryCountLimit)
+ .GreaterThanOrEqual(1)
+ .Default(10);
+ registrar.Parameter("retry_backoff_time", &TThis::RetryBackoffTime)
+ .Default(TDuration::MilliSeconds(10));
+ registrar.Parameter("read_time_limit", &TThis::ReadTimeLimit)
+ .Default(TDuration::Seconds(15));
+
+ registrar.Parameter("chunk_info_cache_expiration_timeout", &TThis::ChunkInfoCacheExpirationTimeout)
+ .Default(TDuration::Seconds(30));
+
+ registrar.Parameter("suspicious_node_grace_period", &TThis::SuspiciousNodeGracePeriod)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("use_direct_io", &TThis::UseDirectIO)
+ .Default(false)
+ .DontSerializeDefault();
+
+ registrar.Parameter("max_inflight_fragment_length", &TThis::MaxInflightFragmentLength)
+ .Default(16_MB);
+ registrar.Parameter("max_inflight_fragment_count", &TThis::MaxInflightFragmentCount)
+ .Default(8192);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/config.h b/yt/yt/client/chunk_client/config.h
new file mode 100644
index 0000000000..6d66e569ef
--- /dev/null
+++ b/yt/yt/client/chunk_client/config.h
@@ -0,0 +1,500 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/misc/config.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFetchChunkSpecConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ int MaxChunksPerFetch;
+ int MaxChunksPerLocateRequest;
+
+ REGISTER_YSON_STRUCT(TFetchChunkSpecConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFetchChunkSpecConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFetcherConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ TDuration NodeRpcTimeout;
+
+ //! If node throttled fetch request, it becomes banned for this period of time.
+ TDuration NodeBanDuration;
+
+ //! Time to sleep before next fetching round if no requests were performed.
+ TDuration BackoffTime;
+
+ int MaxChunksPerNodeFetch;
+
+ //! Timeout when waiting for all replicas to appear in the given node directory.
+ TDuration NodeDirectorySynchronizationTimeout;
+
+ REGISTER_YSON_STRUCT(TFetcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFetcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBlockReordererConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ bool EnableBlockReordering;
+
+ //! Instead of grouping blocks by column groups, shuffle them.
+ //! Used only for testing purposes.
+ bool ShuffleBlocks;
+
+ REGISTER_YSON_STRUCT(TBlockReordererConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBlockReordererConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkSliceFetcherConfig
+ : public TFetcherConfig
+{
+public:
+ int MaxSlicesPerFetch;
+
+ REGISTER_YSON_STRUCT(TChunkSliceFetcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkSliceFetcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEncodingWriterConfig
+ : public virtual TWorkloadConfig
+ , public virtual TBlockReordererConfig
+{
+public:
+ i64 EncodeWindowSize;
+ double DefaultCompressionRatio;
+ bool VerifyCompression;
+ bool ComputeChecksum;
+ int CompressionConcurrency;
+
+ REGISTER_YSON_STRUCT(TEncodingWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TEncodingWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplicationReaderConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Timeout for a block request.
+ TDuration BlockRpcTimeout;
+
+ //! Delay before sending a hedged block request. If null then hedging is disabled.
+ //! NB: Hedging policy may be overridden via hedging manager.
+ std::optional<TDuration> BlockRpcHedgingDelay;
+
+ //! Same as above but for a LookupRows rpc.
+ std::optional<TDuration> LookupRpcHedgingDelay;
+
+ //! Whether to cancel the primary block request when backup one is sent.
+ bool CancelPrimaryBlockRpcRequestOnHedging;
+
+ //! Same as above but for a LookupRows rpc.
+ bool CancelPrimaryLookupRpcRequestOnHedging;
+
+ //! Timeout for a lookup request.
+ TDuration LookupRpcTimeout;
+
+ //! Timeout for a meta request.
+ TDuration MetaRpcTimeout;
+
+ //! Delay before sending for a hedged meta request. If null then hedging is disabled.
+ //! NB: Hedging policy may be overridden via hedging manager.
+ std::optional<TDuration> MetaRpcHedgingDelay;
+
+ //! Timeout for a queue size probing request.
+ TDuration ProbeRpcTimeout;
+
+ //! Maximum number of peers to poll for queue length each round.
+ int ProbePeerCount;
+
+ //! Maximum number of attempts to fetch new seeds.
+ int RetryCount;
+
+ //! Fail read session immediately if master reports no seeds for chunk.
+ bool FailOnNoSeeds;
+
+ //! Time to wait before making another pass with same seeds.
+ //! Increases exponentially with every pass, from MinPassBackoffTime to MaxPassBackoffTime.
+ TDuration MinBackoffTime;
+ TDuration MaxBackoffTime;
+ double BackoffTimeMultiplier;
+
+ //! Maximum number of passes with same seeds.
+ int PassCount;
+
+ //! Enable fetching blocks from peers suggested by seeds.
+ bool FetchFromPeers;
+
+ //! Timeout after which a node forgets about the peer.
+ //! Only makes sense if the reader is equipped with peer descriptor.
+ TDuration PeerExpirationTimeout;
+
+ //! If |true| then fetched blocks are cached by the node.
+ bool PopulateCache;
+
+ //! If |true| then local data center replicas are unconditionally preferred to remote replicas.
+ bool PreferLocalDataCenter;
+
+ //! If |true| then local rack replicas are unconditionally preferred to remote replicas.
+ bool PreferLocalRack;
+
+ //! If |true| then local host replicas are unconditionally preferred to any other replicas.
+ bool PreferLocalHost;
+
+ //! If peer ban counter exceeds #MaxBanCount, peer is banned forever.
+ int MaxBanCount;
+
+ //! Factors to calculate peer load as linear combination of disk queue and net queue.
+ double NetQueueSizeFactor;
+ double DiskQueueSizeFactor;
+
+ //! If |true|, then workload descriptors are annotated with the read session start time
+ //! and are thus scheduled in FIFO order.
+ bool EnableWorkloadFifoScheduling;
+
+ //! Total retry timeout, helps when we are doing too many passes.
+ TDuration RetryTimeout;
+
+ //! Total session timeout (for ReadBlocks and GetMeta calls).
+ TDuration SessionTimeout;
+
+ //! Maximum number of passes within single retry for lookup request.
+ int LookupRequestPassCount;
+
+ //! Maximum number of retries for lookup request.
+ int LookupRequestRetryCount;
+
+ //! If |true| block cache will be accessed via asynchronous interface, if |false|
+ //! synchronous interface will be used.
+ bool UseAsyncBlockCache;
+
+ //! If |true| replication reader will try to fetch blocks from local block cache.
+ bool UseBlockCache;
+
+ //! Will locate new replicas from master
+ //! if node was suspicious for at least the period (unless null).
+ std::optional<TDuration> SuspiciousNodeGracePeriod;
+
+ //! Is used to increase interval between Locates
+ //! that are called for discarding seeds that are suspicious.
+ TDuration ProlongedDiscardSeedsDelay;
+
+ //! If |true| GetMeta() will be performed via provided ChunkMetaCache.
+ //! If ChunkMetaCache is nullptr or partition tag is specified, this option has no effect.
+ bool EnableChunkMetaCache;
+
+ //! If |true| reader will retain a set of peers that will be banned for every session.
+ bool BanPeersPermanently;
+
+ //! For testing purposes.
+ //! If |true| network throttlers will be applied even in case of requests to local host.
+ bool EnableLocalThrottling;
+
+ //! For testing purposes.
+ //! Unless null, reader will simulate failure of accessing chunk meta cache with such probability.
+ std::optional<double> ChunkMetaCacheFailureProbability;
+
+ //! Will open and read with DirectIO (unless already opened w/o DirectIO or disabled via location config).
+ // NB: Right now is used only with data node lookup + chunk index.
+ bool UseDirectIO;
+
+ REGISTER_YSON_STRUCT(TReplicationReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TReplicationReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBlockFetcherConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Prefetch window size (in bytes).
+ i64 WindowSize;
+
+ //! Maximum amount of data to be transferred via a single RPC request.
+ i64 GroupSize;
+
+ //! If |True| block fetcher will try to fetch block from local uncompressed block cache.
+ bool UseUncompressedBlockCache;
+
+ REGISTER_YSON_STRUCT(TBlockFetcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBlockFetcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TErasureReaderConfig
+ : public TReplicationReaderConfig
+ , public virtual TBlockFetcherConfig
+{
+public:
+ bool EnableAutoRepair;
+ double ReplicationReaderSpeedLimitPerSec;
+ TDuration SlowReaderExpirationTimeout;
+ TDuration ReplicationReaderTimeout;
+ TDuration ReplicationReaderFailureTimeout;
+
+ REGISTER_YSON_STRUCT(TErasureReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TErasureReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMultiChunkReaderConfig
+ : public virtual TErasureReaderConfig
+ , public virtual TBlockFetcherConfig
+ , public virtual TFetchChunkSpecConfig
+ , public virtual TWorkloadConfig
+{
+public:
+ i64 MaxBufferSize;
+ int MaxParallelReaders;
+
+ REGISTER_YSON_STRUCT(TMultiChunkReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TMultiChunkReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplicationWriterConfig
+ : public virtual TWorkloadConfig
+ , public virtual TBlockReordererConfig
+{
+public:
+ //! Maximum window size (in bytes).
+ i64 SendWindowSize;
+
+ //! Maximum group size (in bytes).
+ i64 GroupSize;
+
+ //! RPC requests timeout.
+ /*!
+ * This timeout is especially useful for |PutBlocks| calls to ensure that
+ * uploading is not stalled.
+ */
+ TDuration NodeRpcTimeout;
+
+ NRpc::TRetryingChannelConfigPtr NodeChannel;
+
+ int UploadReplicationFactor;
+
+ int MinUploadReplicationFactor;
+
+ std::optional<int> DirectUploadNodeCount;
+
+ bool PreferLocalHost;
+
+ bool BanFailedNodes;
+
+ //! Interval between consecutive pings to Data Nodes.
+ TDuration NodePingPeriod;
+
+ //! If |true| then written blocks are cached by the node.
+ bool PopulateCache;
+
+ //! If |true| then the chunk is fsynced to disk upon closing.
+ bool SyncOnClose;
+
+ bool EnableDirectIO;
+
+ //! If |true| then the chunk is finished as soon as MinUploadReplicationFactor chunks are written.
+ bool EnableEarlyFinish;
+
+ TDuration AllocateWriteTargetsBackoffTime;
+
+ int AllocateWriteTargetsRetryCount;
+
+ std::optional<TDuration> TestingDelay;
+
+ int GetDirectUploadNodeCount();
+
+ REGISTER_YSON_STRUCT(TReplicationWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TReplicationWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TErasureWriterConfig
+ : public virtual TBlockReordererConfig
+{
+public:
+ i64 WriterWindowSize;
+ i64 WriterGroupSize;
+
+ // TODO(gritukan): Drop.
+ std::optional<i64> ErasureStripeSize;
+ i64 ErasureWindowSize;
+ bool ErasureStoreOriginalBlockChecksums;
+
+ i64 DesiredSegmentPartSize;
+
+ bool EnableErasureTargetNodeReallocation;
+
+ bool EnableStripedErasure;
+
+ bool UseEffectiveErasureCodecs;
+
+ REGISTER_YSON_STRUCT(TErasureWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TErasureWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMultiChunkWriterConfig
+ : public TReplicationWriterConfig
+ , public TErasureWriterConfig
+{
+public:
+ i64 DesiredChunkSize;
+ i64 DesiredChunkWeight;
+ i64 MaxMetaSize;
+
+ REGISTER_YSON_STRUCT(TMultiChunkWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TMultiChunkWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMemoryTrackedWriterOptions
+ : public NYTree::TYsonStruct
+{
+public:
+ IMemoryUsageTrackerPtr MemoryTracker;
+
+ IMemoryReferenceTrackerPtr MemoryReferenceTracker;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEncodingWriterOptions
+ : public virtual TMemoryTrackedWriterOptions
+{
+public:
+ NCompression::ECodec CompressionCodec;
+ bool ChunksEden;
+ bool SetChunkCreationTime;
+
+ REGISTER_YSON_STRUCT(TEncodingWriterOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TEncodingWriterOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkFragmentReaderConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Expiration timeout of corresponding sync expiring cache.
+ TDuration PeerInfoExpirationTimeout;
+
+ //! Minimal delay between sequential chunk replica locations.
+ TDuration SeedsExpirationTimeout;
+
+ //! Delay between background cache updates.
+ TDuration PeriodicUpdateDelay;
+
+ //! Factors to calculate peer load as linear combination of disk queue and net queue.
+ double NetQueueSizeFactor;
+ double DiskQueueSizeFactor;
+
+ //! RPC timeouts of ProbeChunkSet and GetChunkFragmentSet.
+ TDuration ProbeChunkSetRpcTimeout;
+ TDuration GetChunkFragmentSetRpcTimeout;
+
+ //! Delay before sending a hedged request. If null then hedging is disabled.
+ //! NB: This option may be overridden via hedging manager.
+ std::optional<TDuration> FragmentReadHedgingDelay;
+
+ //! Limit on retry count.
+ int RetryCountLimit;
+ //! Time between retries.
+ TDuration RetryBackoffTime;
+ //! Maximum time to serve fragments read request.
+ TDuration ReadTimeLimit;
+
+ //! Chunk that was not accessed for the time by user
+ //! will stop being accessed within periodic updates and then will be evicted via expiring cache logic.
+ TDuration ChunkInfoCacheExpirationTimeout;
+
+ //! Will locate new replicas from master
+ //! if node was suspicious for at least the period (unless null).
+ std::optional<TDuration> SuspiciousNodeGracePeriod;
+
+ //! Will open and read with DirectIO (unless already opened w/o DirectIO or disabled via location config).
+ bool UseDirectIO;
+
+ //! Upper bound on length of simultaneously requested fragments withing a reading session.
+ i64 MaxInflightFragmentLength;
+
+ //! Upper bound on count of simultaneously requested fragments withing a reading session.
+ i64 MaxInflightFragmentCount;
+
+ REGISTER_YSON_STRUCT(TChunkFragmentReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkFragmentReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/data_statistics.cpp b/yt/yt/client/chunk_client/data_statistics.cpp
new file mode 100644
index 0000000000..6e72d1df97
--- /dev/null
+++ b/yt/yt/client/chunk_client/data_statistics.cpp
@@ -0,0 +1,217 @@
+#include "data_statistics.h"
+
+#include <yt/yt/client/chunk_client/data_statistics.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NChunkClient {
+
+using namespace NCompression;
+using namespace NYTree;
+using namespace NYson;
+
+using ::ToString;
+using ::FromString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool HasInvalidDataWeight(const TDataStatistics& statistics)
+{
+ return statistics.has_data_weight() && statistics.data_weight() == -1;
+}
+
+bool HasInvalidUnmergedRowCount(const TDataStatistics& statistics)
+{
+ return statistics.has_unmerged_row_count() && statistics.unmerged_row_count() == -1;
+}
+
+bool HasInvalidUnmergedDataWeight(const TDataStatistics& statistics)
+{
+ return statistics.has_unmerged_data_weight() && statistics.unmerged_data_weight() == -1;
+}
+
+TDataStatistics& operator += (TDataStatistics& lhs, const TDataStatistics& rhs)
+{
+ lhs.set_uncompressed_data_size(lhs.uncompressed_data_size() + rhs.uncompressed_data_size());
+ lhs.set_compressed_data_size(lhs.compressed_data_size() + rhs.compressed_data_size());
+ lhs.set_chunk_count(lhs.chunk_count() + rhs.chunk_count());
+ lhs.set_row_count(lhs.row_count() + rhs.row_count());
+ lhs.set_regular_disk_space(lhs.regular_disk_space() + rhs.regular_disk_space());
+ lhs.set_erasure_disk_space(lhs.erasure_disk_space() + rhs.erasure_disk_space());
+
+ if (HasInvalidDataWeight(lhs) || HasInvalidDataWeight(rhs)) {
+ lhs.set_data_weight(-1);
+ } else {
+ lhs.set_data_weight(lhs.data_weight() + rhs.data_weight());
+ }
+
+ if (HasInvalidUnmergedRowCount(lhs) || HasInvalidUnmergedRowCount(rhs)) {
+ lhs.set_unmerged_row_count(-1);
+ } else {
+ lhs.set_unmerged_row_count(lhs.unmerged_row_count() + rhs.unmerged_row_count());
+ }
+
+ if (HasInvalidUnmergedDataWeight(lhs) || HasInvalidUnmergedDataWeight(rhs)) {
+ lhs.set_unmerged_data_weight(-1);
+ } else {
+ lhs.set_unmerged_data_weight(lhs.unmerged_data_weight() + rhs.unmerged_data_weight());
+ }
+
+ return lhs;
+}
+
+TDataStatistics operator + (const TDataStatistics& lhs, const TDataStatistics& rhs)
+{
+ auto result = lhs;
+ result += rhs;
+ return result;
+}
+
+bool operator == (const TDataStatistics& lhs, const TDataStatistics& rhs)
+{
+ return
+ lhs.uncompressed_data_size() == rhs.uncompressed_data_size() &&
+ lhs.compressed_data_size() == rhs.compressed_data_size() &&
+ lhs.row_count() == rhs.row_count() &&
+ lhs.chunk_count() == rhs.chunk_count() &&
+ lhs.regular_disk_space() == rhs.regular_disk_space() &&
+ lhs.erasure_disk_space() == rhs.erasure_disk_space() &&
+ (HasInvalidDataWeight(lhs) || HasInvalidDataWeight(rhs) || lhs.data_weight() == rhs.data_weight()) &&
+ (
+ HasInvalidUnmergedRowCount(lhs) ||
+ HasInvalidUnmergedRowCount(rhs) ||
+ lhs.unmerged_row_count() == rhs.unmerged_row_count()) &&
+ (
+ HasInvalidUnmergedDataWeight(lhs) ||
+ HasInvalidUnmergedDataWeight(rhs) ||
+ lhs.unmerged_data_weight() == rhs.unmerged_data_weight());
+}
+
+bool operator != (const TDataStatistics& lhs, const TDataStatistics& rhs)
+{
+ return !(lhs == rhs);
+}
+
+void Serialize(const TDataStatistics& statistics, NYson::IYsonConsumer* consumer)
+{
+ // TODO(max42): replace all Item with OptionalItem in order to expose only meaningful
+ // fields in each particular context. This would require fixing some tests or using
+ // manually constructed data statistics with some fields being explicitly set to zero
+ // as a neutral element.
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("chunk_count").Value(statistics.chunk_count())
+ .Item("row_count").Value(statistics.row_count())
+ .Item("uncompressed_data_size").Value(statistics.uncompressed_data_size())
+ .Item("compressed_data_size").Value(statistics.compressed_data_size())
+ .Item("data_weight").Value(statistics.data_weight())
+ .Item("regular_disk_space").Value(statistics.regular_disk_space())
+ .Item("erasure_disk_space").Value(statistics.erasure_disk_space())
+ .Item("unmerged_row_count").Value(statistics.unmerged_row_count())
+ .Item("unmerged_data_weight").Value(statistics.unmerged_data_weight())
+ .EndMap();
+}
+
+void SetDataStatisticsField(TDataStatistics& statistics, TStringBuf key, i64 value)
+{
+ if (key == "chunk_count") {
+ statistics.set_chunk_count(value);
+ } else if (key == "row_count") {
+ statistics.set_row_count(value);
+ } else if (key == "uncompressed_data_size") {
+ statistics.set_uncompressed_data_size(value);
+ } else if (key == "compressed_data_size") {
+ statistics.set_compressed_data_size(value);
+ } else if (key == "data_weight") {
+ statistics.set_data_weight(value);
+ } else if (key == "regular_disk_space") {
+ statistics.set_regular_disk_space(value);
+ } else if (key == "erasure_disk_space") {
+ statistics.set_erasure_disk_space(value);
+ } else if (key == "unmerged_row_count") {
+ statistics.set_unmerged_row_count(value);
+ } else if (key == "unmerged_data_weight") {
+ statistics.set_unmerged_data_weight(value);
+ } // Else we have a strange situation on our hands but we intentionally ignore it.
+}
+
+void FormatValue(TStringBuilderBase* builder, const TDataStatistics& statistics, TStringBuf /*spec*/)
+{
+ builder->AppendFormat(
+ "{UncompressedDataSize: %v, CompressedDataSize: %v, DataWeight: %v, RowCount: %v, "
+ "ChunkCount: %v, RegularDiskSpace: %v, ErasureDiskSpace: %v, "
+ "UnmergedRowCount: %v, UnmergedDataWeight: %v}",
+ statistics.uncompressed_data_size(),
+ statistics.compressed_data_size(),
+ statistics.data_weight(),
+ statistics.row_count(),
+ statistics.chunk_count(),
+ statistics.regular_disk_space(),
+ statistics.erasure_disk_space(),
+ statistics.unmerged_row_count(),
+ statistics.unmerged_data_weight());
+}
+
+void FormatValue(TStringBuilderBase* builder, const TDataStatistics* statistics, TStringBuf spec)
+{
+ if (statistics) {
+ FormatValue(builder, *statistics, spec);
+ } else {
+ FormatValue(builder, std::nullopt, spec);
+ }
+}
+
+TString ToString(const TDataStatistics& statistics)
+{
+ return ToStringViaBuilder(statistics);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCodecStatistics& TCodecStatistics::Append(const TCodecDuration& codecTime)
+{
+ return Append(std::make_pair(codecTime.Codec, codecTime.CpuDuration));
+}
+
+TCodecStatistics& TCodecStatistics::Append(const std::pair<ECodec, TDuration>& codecTime)
+{
+ CodecToDuration_[codecTime.first] += codecTime.second;
+ TotalDuration_ += codecTime.second;
+ return *this;
+}
+
+TCodecStatistics& TCodecStatistics::operator+=(const TCodecStatistics& other)
+{
+ for (const auto& pair : other.CodecToDuration_) {
+ Append(pair);
+ }
+ return *this;
+}
+
+TDuration TCodecStatistics::GetTotalDuration() const
+{
+ return TotalDuration_;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TCodecStatistics& statistics, TStringBuf /* spec */)
+{
+ FormatKeyValueRange(builder, statistics.CodecToDuration(), TDefaultFormatter());
+}
+
+TString ToString(const TCodecStatistics& statistics)
+{
+ return ToStringViaBuilder(statistics);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/data_statistics.h b/yt/yt/client/chunk_client/data_statistics.h
new file mode 100644
index 0000000000..4fc0b210c8
--- /dev/null
+++ b/yt/yt/client/chunk_client/data_statistics.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/data_statistics.pb.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/misc/property.h>
+
+#include <library/cpp/yt/small_containers/compact_flat_map.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+bool HasInvalidDataWeight(const TDataStatistics& statistics);
+
+TDataStatistics& operator += (TDataStatistics& lhs, const TDataStatistics& rhs);
+TDataStatistics operator + (const TDataStatistics& lhs, const TDataStatistics& rhs);
+
+bool operator == (const TDataStatistics& lhs, const TDataStatistics& rhs);
+bool operator != (const TDataStatistics& lhs, const TDataStatistics& rhs);
+
+void Serialize(const TDataStatistics& statistics, NYson::IYsonConsumer* consumer);
+
+void SetDataStatisticsField(TDataStatistics& statistics, TStringBuf key, i64 value);
+
+void FormatValue(TStringBuilderBase* builder, const TDataStatistics& statistics, TStringBuf spec);
+void FormatValue(TStringBuilderBase* builder, const TDataStatistics* statistics, TStringBuf spec);
+TString ToString(const TDataStatistics& statistics);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCodecDuration
+{
+ NCompression::ECodec Codec;
+ TDuration CpuDuration;
+};
+
+class TCodecStatistics
+{
+public:
+ using TCodecToDuration = TCompactFlatMap<
+ NCompression::ECodec,
+ TDuration,
+ 1>;
+ DEFINE_BYREF_RO_PROPERTY(TCodecToDuration, CodecToDuration);
+
+public:
+ TCodecStatistics& Append(const TCodecDuration& codecTime);
+
+ TCodecStatistics& operator+=(const TCodecStatistics& other);
+
+ TDuration GetTotalDuration() const;
+
+private:
+ TDuration TotalDuration_;
+
+ TCodecStatistics& Append(const std::pair<NCompression::ECodec, TDuration>& codecTime);
+};
+
+void FormatValue(TStringBuilderBase* builder, const TCodecStatistics& statistics, TStringBuf spec);
+TString ToString(const TCodecStatistics& statistics);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
+
diff --git a/yt/yt/client/chunk_client/helpers.cpp b/yt/yt/client/chunk_client/helpers.cpp
new file mode 100644
index 0000000000..bbc97fcd54
--- /dev/null
+++ b/yt/yt/client/chunk_client/helpers.cpp
@@ -0,0 +1,48 @@
+#include "helpers.h"
+
+#include "read_limit.h"
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/chunk_spec.pb.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NChunkClient {
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintTo(const TReadRange& readRange, std::ostream* os)
+{
+ *os << ToString(readRange);
+}
+
+NObjectClient::TObjectId GetObjectIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec)
+{
+ return FromProto<NObjectClient::TObjectId>(chunkSpec.chunk_id());
+}
+
+NObjectClient::TCellId GetCellIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec)
+{
+ return FromProto<NObjectClient::TCellId>(chunkSpec.cell_id());
+}
+
+NObjectClient::TObjectId GetTabletIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec)
+{
+ return FromProto<NTabletClient::TTabletId>(chunkSpec.tablet_id());
+}
+
+void SetTabletId(NProto::TChunkSpec* chunkSpec, NTabletClient::TTabletId tabletId)
+{
+ ToProto(chunkSpec->mutable_tablet_id(), tabletId);
+}
+
+void SetObjectId(NProto::TChunkSpec* chunkSpec, NObjectClient::TObjectId objectId)
+{
+ ToProto(chunkSpec->mutable_chunk_id(), objectId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/helpers.h b/yt/yt/client/chunk_client/helpers.h
new file mode 100644
index 0000000000..6bbefd8b51
--- /dev/null
+++ b/yt/yt/client/chunk_client/helpers.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/tablet_client/public.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintTo(const TReadRange& readRange, std::ostream* os);
+
+NObjectClient::TObjectId GetObjectIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec);
+NObjectClient::TCellId GetCellIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec);
+NTabletClient::TTabletId GetTabletIdFromChunkSpec(const NProto::TChunkSpec& chunkSpec);
+
+void SetTabletId(NProto::TChunkSpec* chunkSpec, NTabletClient::TTabletId tabletId);
+void SetObjectId(NProto::TChunkSpec* chunkSpec, NObjectClient::TObjectId objectId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/public.cpp b/yt/yt/client/chunk_client/public.cpp
new file mode 100644
index 0000000000..f897ffa00d
--- /dev/null
+++ b/yt/yt/client/chunk_client/public.cpp
@@ -0,0 +1,23 @@
+#include "public.h"
+
+#include <yt/yt/client/misc/workload.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TChunkId NullChunkId = NObjectClient::NullObjectId;
+const TChunkViewId NullChunkViewId = NObjectClient::NullObjectId;
+const TChunkListId NullChunkListId = NObjectClient::NullObjectId;
+const TChunkTreeId NullChunkTreeId = NObjectClient::NullObjectId;
+
+const TString DefaultStoreAccountName("sys");
+const TString DefaultStoreMediumName("default");
+const TString DefaultCacheMediumName("cache");
+const TString DefaultSlotsMediumName("default");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/public.h b/yt/yt/client/chunk_client/public.h
new file mode 100644
index 0000000000..3b6f1143ad
--- /dev/null
+++ b/yt/yt/client/chunk_client/public.h
@@ -0,0 +1,201 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+#include <library/cpp/yt/small_containers/compact_flat_map.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TChunkInfo;
+class TChunkSpec;
+class TChunkMeta;
+class TBlocksExt;
+class TMiscExt;
+
+class TDataStatistics;
+
+class TLegacyReadRange;
+
+class TMediumDirectory;
+
+} // namespace NProto
+
+YT_DEFINE_ERROR_ENUM(
+ ((AllTargetNodesFailed) (700))
+ ((SendBlocksFailed) (701))
+ ((NoSuchSession) (702))
+ ((SessionAlreadyExists) (703))
+ ((ChunkAlreadyExists) (704))
+ ((WindowError) (705))
+ ((BlockContentMismatch) (706))
+ ((NoSuchBlock) (707))
+ ((NoSuchChunk) (708))
+ ((NoLocationAvailable) (710))
+ ((IOError) (711))
+ ((MasterCommunicationFailed) (712))
+ ((NoSuchChunkTree) (713))
+ ((NoSuchChunkList) (717))
+ ((MasterNotConnected) (714))
+ ((ChunkUnavailable) (716))
+ ((WriteThrottlingActive) (718))
+ ((NoSuchMedium) (719))
+ ((OptimisticLockFailure) (720))
+ ((InvalidBlockChecksum) (721))
+ ((MalformedReadRequest) (722))
+ ((MissingExtension) (724))
+ ((ReaderThrottlingFailed) (725))
+ ((ReaderTimeout) (726))
+ ((NoSuchChunkView) (727))
+ ((IncorrectChunkFileChecksum) (728))
+ ((BrokenChunkFileMeta) (729))
+ ((IncorrectLayerFileSize) (730))
+ ((NoSpaceLeftOnDevice) (731))
+ ((ConcurrentChunkUpdate) (732))
+ ((InvalidInputChunk) (733))
+ ((UnsupportedChunkFeature) (734))
+ ((IncompatibleChunkMetas) (735))
+ ((AutoRepairFailed) (736))
+ ((ChunkBlockFetchFailed) (737))
+ ((ChunkMetaFetchFailed) (738))
+ ((RowsLookupFailed) (739))
+ ((BlockChecksumMismatch) (740))
+ ((NoChunkSeedsKnown) (741))
+ ((NoChunkSeedsGiven) (742))
+ ((ChunkIsLost) (743))
+ ((ChunkReadSessionSlow) (744))
+ ((NodeProbeFailed) (745))
+ ((UnrecoverableRepairError) (747))
+ ((MissingJournalChunkRecord) (748))
+ ((LocationDiskFailed) (749))
+ ((LocationCrashed) (750))
+ ((LocationDiskWaitingReplacement) (751))
+ ((ChunkMetaCacheFetchFailed) (752))
+);
+
+using TChunkId = NObjectClient::TObjectId;
+extern const TChunkId NullChunkId;
+
+using TChunkViewId = NObjectClient::TObjectId;
+extern const TChunkViewId NullChunkViewId;
+
+using TChunkListId = NObjectClient::TObjectId;
+extern const TChunkListId NullChunkListId;
+
+using TChunkTreeId = NObjectClient::TObjectId;
+extern const TChunkTreeId NullChunkTreeId;
+
+using TChunkLocationUuid = TGuid;
+constexpr auto EmptyChunkLocationUuid = TChunkLocationUuid(0, 0);
+constexpr auto InvalidChunkLocationUuid = TChunkLocationUuid(-1, -1);
+
+constexpr int MinReplicationFactor = 1;
+constexpr int MaxReplicationFactor = 20;
+constexpr int DefaultReplicationFactor = 3;
+
+constexpr int MaxMediumCount = 120; // leave some room for sentinels
+
+template <typename T>
+using TMediumMap = THashMap<int, T>;
+template <typename T>
+using TCompactMediumMap = TCompactFlatMap<int, T, 4>;
+
+//! Used as an expected upper bound in TCompactVector.
+/*
+ * Maximum regular number of replicas is 16 (for LRC codec).
+ * Additional +8 enables some flexibility during balancing.
+ */
+constexpr int TypicalReplicaCount = 24;
+constexpr int GenericChunkReplicaIndex = 16; // no specific replica; the default one for non-erasure chunks
+
+//! Valid indexes are in range |[0, ChunkReplicaIndexBound)|.
+constexpr int ChunkReplicaIndexBound = 32;
+
+constexpr int GenericMediumIndex = 126; // internal sentinel meaning "no specific medium"
+constexpr int AllMediaIndex = 127; // passed to various APIs to indicate that any medium is OK
+constexpr int DefaultStoreMediumIndex = 0;
+constexpr int DefaultSlotsMediumIndex = 0;
+
+//! Valid indexes (including sentinels) are in range |[0, MediumIndexBound)|.
+constexpr int MediumIndexBound = AllMediaIndex + 1;
+
+class TChunkReplicaWithMedium;
+using TChunkReplicaWithMediumList = TCompactVector<TChunkReplicaWithMedium, TypicalReplicaCount>;
+
+class TChunkReplicaWithLocation;
+using TChunkReplicaWithLocationList = TCompactVector<TChunkReplicaWithLocation, TypicalReplicaCount>;
+
+class TChunkReplica;
+using TChunkReplicaList = TCompactVector<TChunkReplica, TypicalReplicaCount>;
+
+extern const TString DefaultStoreAccountName;
+extern const TString DefaultStoreMediumName;
+extern const TString DefaultCacheMediumName;
+extern const TString DefaultSlotsMediumName;
+
+DECLARE_REFCOUNTED_STRUCT(IReaderBase)
+
+DECLARE_REFCOUNTED_CLASS(TFetchChunkSpecConfig)
+DECLARE_REFCOUNTED_CLASS(TFetcherConfig)
+DECLARE_REFCOUNTED_CLASS(TChunkSliceFetcherConfig)
+DECLARE_REFCOUNTED_CLASS(TEncodingWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TErasureReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TMultiChunkReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TBlockFetcherConfig)
+DECLARE_REFCOUNTED_CLASS(TReplicationReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TReplicationWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TErasureWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TMultiChunkWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TEncodingWriterOptions)
+DECLARE_REFCOUNTED_CLASS(TBlockReordererConfig)
+DECLARE_REFCOUNTED_CLASS(TChunkFragmentReaderConfig)
+
+struct TCodecDuration;
+class TCodecStatistics;
+
+class TLegacyReadLimit;
+class TLegacyReadRange;
+class TReadLimit;
+class TReadRange;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EChunkAvailabilityPolicy,
+ ((DataPartsAvailable) (0))
+ ((AllPartsAvailable) (1))
+ ((Repairable) (2))
+);
+
+// Keep in sync with NChunkServer::ETableChunkFormat.
+DEFINE_ENUM(EChunkFormat,
+ // Sentinels.
+ ((Unknown) (-1))
+
+ // File chunks.
+ ((FileDefault) (1))
+
+ // Table chunks.
+ ((TableUnversionedSchemaful) (3))
+ ((TableUnversionedSchemalessHorizontal) (4))
+ ((TableUnversionedColumnar) (6))
+ ((TableVersionedSimple) (2))
+ ((TableVersionedColumnar) (5))
+ ((TableVersionedIndexed) (8))
+ ((TableVersionedSlim) (9))
+
+ // Journal chunks.
+ ((JournalDefault) (0))
+
+ // Hunk chunks.
+ ((HunkDefault) (7))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/read_limit.cpp b/yt/yt/client/chunk_client/read_limit.cpp
new file mode 100644
index 0000000000..01fdbcb31b
--- /dev/null
+++ b/yt/yt/client/chunk_client/read_limit.cpp
@@ -0,0 +1,1028 @@
+#include "read_limit.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/node.h>
+
+namespace NYT::NChunkClient {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLegacyReadLimit::TLegacyReadLimit(const NProto::TReadLimit& protoLimit)
+{
+ InitCopy(protoLimit);
+}
+
+TLegacyReadLimit::TLegacyReadLimit(NProto::TReadLimit&& protoLimit)
+{
+ InitMove(std::move(protoLimit));
+}
+
+TLegacyReadLimit::TLegacyReadLimit(const std::unique_ptr<NProto::TReadLimit>& protoLimit)
+{
+ if (protoLimit) {
+ InitCopy(*protoLimit);
+ }
+}
+
+TLegacyReadLimit::TLegacyReadLimit(const TLegacyOwningKey& key)
+{
+ SetLegacyKey(key);
+}
+
+TLegacyReadLimit::TLegacyReadLimit(TLegacyOwningKey&& key)
+{
+ SetLegacyKey(std::move(key));
+}
+
+TLegacyReadLimit& TLegacyReadLimit::operator= (const NProto::TReadLimit& protoLimit)
+{
+ InitCopy(protoLimit);
+ return *this;
+}
+
+TLegacyReadLimit& TLegacyReadLimit::operator= (NProto::TReadLimit&& protoLimit)
+{
+ InitMove(std::move(protoLimit));
+ return *this;
+}
+
+TLegacyReadLimit TLegacyReadLimit::GetSuccessor() const
+{
+ TLegacyReadLimit result;
+ if (HasLegacyKey()) {
+ auto key = GetLegacyKey();
+ result.SetLegacyKey(GetKeyPrefixSuccessor(key, key.GetCount()));
+ }
+ if (HasRowIndex()) {
+ result.SetRowIndex(GetRowIndex() + 1);
+ }
+ if (HasChunkIndex()) {
+ result.SetChunkIndex(GetChunkIndex() + 1);
+ }
+ if (HasTabletIndex()) {
+ // We use tabletIndex in ordered dynamic tables, where indexing is over pairs (tabletIndex, rowIndex).
+ result.SetTabletIndex(GetTabletIndex());
+ }
+ return result;
+}
+
+const NProto::TReadLimit& TLegacyReadLimit::AsProto() const
+{
+ return ReadLimit_;
+}
+
+const TLegacyOwningKey& TLegacyReadLimit::GetLegacyKey() const
+{
+ YT_ASSERT(HasLegacyKey());
+ return Key_;
+}
+
+bool TLegacyReadLimit::HasLegacyKey() const
+{
+ return ReadLimit_.has_legacy_key();
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetLegacyKey(const TLegacyOwningKey& key)
+{
+ Key_ = key;
+ ToProto(ReadLimit_.mutable_legacy_key(), Key_);
+ return *this;
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetLegacyKey(TLegacyOwningKey&& key)
+{
+ swap(Key_, key);
+ ToProto(ReadLimit_.mutable_legacy_key(), Key_);
+ return *this;
+}
+
+i64 TLegacyReadLimit::GetRowIndex() const
+{
+ YT_ASSERT(HasRowIndex());
+ return ReadLimit_.row_index();
+}
+
+bool TLegacyReadLimit::HasRowIndex() const
+{
+ return ReadLimit_.has_row_index();
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetRowIndex(i64 rowIndex)
+{
+ ReadLimit_.set_row_index(rowIndex);
+ return *this;
+}
+
+i64 TLegacyReadLimit::GetOffset() const
+{
+ YT_ASSERT(HasOffset());
+ return ReadLimit_.offset();
+}
+
+bool TLegacyReadLimit::HasOffset() const
+{
+ return ReadLimit_.has_offset();
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetOffset(i64 offset)
+{
+ ReadLimit_.set_offset(offset);
+ return *this;
+}
+
+i64 TLegacyReadLimit::GetChunkIndex() const
+{
+ YT_ASSERT(HasChunkIndex());
+ return ReadLimit_.chunk_index();
+}
+
+bool TLegacyReadLimit::HasChunkIndex() const
+{
+ return ReadLimit_.has_chunk_index();
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetChunkIndex(i64 chunkIndex)
+{
+ ReadLimit_.set_chunk_index(chunkIndex);
+ return *this;
+}
+
+i32 TLegacyReadLimit::GetTabletIndex() const
+{
+ YT_ASSERT(HasTabletIndex());
+ return ReadLimit_.tablet_index();
+}
+
+bool TLegacyReadLimit::HasTabletIndex() const
+{
+ return ReadLimit_.has_tablet_index();
+}
+
+TLegacyReadLimit& TLegacyReadLimit::SetTabletIndex(i32 tabletIndex)
+{
+ ReadLimit_.set_tablet_index(tabletIndex);
+ return *this;
+}
+
+bool TLegacyReadLimit::IsTrivial() const
+{
+ return NChunkClient::IsTrivial(ReadLimit_);
+}
+
+void TLegacyReadLimit::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist(context, ReadLimit_);
+ Persist(context, Key_);
+}
+
+void TLegacyReadLimit::MergeLowerLegacyKey(const TLegacyOwningKey& key)
+{
+ if (!HasLegacyKey() || GetLegacyKey() < key) {
+ SetLegacyKey(key);
+ }
+}
+
+void TLegacyReadLimit::MergeUpperLegacyKey(const TLegacyOwningKey& key)
+{
+ if (!HasLegacyKey() || GetLegacyKey() > key) {
+ SetLegacyKey(key);
+ }
+}
+
+void TLegacyReadLimit::MergeLowerRowIndex(i64 rowIndex)
+{
+ if (!HasRowIndex() || GetRowIndex() < rowIndex) {
+ SetRowIndex(rowIndex);
+ }
+}
+
+void TLegacyReadLimit::MergeUpperRowIndex(i64 rowIndex)
+{
+ if (!HasRowIndex() || GetRowIndex() > rowIndex) {
+ SetRowIndex(rowIndex);
+ }
+}
+
+void TLegacyReadLimit::InitKey()
+{
+ if (ReadLimit_.has_legacy_key()) {
+ FromProto(&Key_, ReadLimit_.legacy_key());
+ }
+}
+
+void TLegacyReadLimit::InitCopy(const NProto::TReadLimit& readLimit)
+{
+ ReadLimit_.CopyFrom(readLimit);
+ InitKey();
+}
+
+void TLegacyReadLimit::InitMove(NProto::TReadLimit&& readLimit)
+{
+ ReadLimit_.Swap(&readLimit);
+ InitKey();
+}
+
+size_t TLegacyReadLimit::SpaceUsed() const
+{
+ return
+ sizeof(*this) +
+ ReadLimit_.SpaceUsed() - sizeof(ReadLimit_) +
+ Key_.GetSpaceUsed() - sizeof(Key_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TLegacyReadLimit& limit)
+{
+ using ::ToString;
+
+ TStringBuilder builder;
+ builder.AppendChar('{');
+
+ bool firstToken = true;
+ auto append = [&] (const char* label, TStringBuf value) {
+ if (!firstToken) {
+ builder.AppendString(", ");
+ }
+ firstToken = false;
+ builder.AppendString(label);
+ builder.AppendString(": ");
+ builder.AppendString(value);
+ };
+
+ if (limit.HasLegacyKey()) {
+ append("Key", ToString(limit.GetLegacyKey()));
+ }
+
+ if (limit.HasRowIndex()) {
+ append("RowIndex", ToString(limit.GetRowIndex()));
+ }
+
+ if (limit.HasOffset()) {
+ append("Offset", ToString(limit.GetOffset()));
+ }
+
+ if (limit.HasChunkIndex()) {
+ append("ChunkIndex", ToString(limit.GetChunkIndex()));
+ }
+
+ if (limit.HasTabletIndex()) {
+ append("TabletIndex", ToString(limit.GetTabletIndex()));
+ }
+
+ builder.AppendChar('}');
+ return builder.Flush();
+}
+
+bool IsTrivial(const TLegacyReadLimit& limit)
+{
+ return limit.IsTrivial();
+}
+
+bool IsTrivial(const NProto::TReadLimit& limit)
+{
+ return
+ !limit.has_row_index() &&
+ !limit.has_legacy_key() &&
+ !limit.has_offset() &&
+ !limit.has_chunk_index() &&
+ !limit.has_tablet_index();
+}
+
+void ToProto(NProto::TReadLimit* protoReadLimit, const TLegacyReadLimit& readLimit)
+{
+ protoReadLimit->CopyFrom(readLimit.AsProto());
+}
+
+void FromProto(TLegacyReadLimit* readLimit, const NProto::TReadLimit& protoReadLimit)
+{
+ *readLimit = protoReadLimit;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TLegacyReadLimit& readLimit, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .DoIf(readLimit.HasLegacyKey(), [&] (TFluentMap fluent) {
+ fluent.Item("key").Value(readLimit.GetLegacyKey());
+ })
+ .DoIf(readLimit.HasRowIndex(), [&] (TFluentMap fluent) {
+ fluent.Item("row_index").Value(readLimit.GetRowIndex());
+ })
+ .DoIf(readLimit.HasOffset(), [&] (TFluentMap fluent) {
+ fluent.Item("offset").Value(readLimit.GetOffset());
+ })
+ .DoIf(readLimit.HasChunkIndex(), [&] (TFluentMap fluent) {
+ fluent.Item("chunk_index").Value(readLimit.GetChunkIndex());
+ })
+ .DoIf(readLimit.HasTabletIndex(), [&] (TFluentMap fluent) {
+ fluent.Item("tablet_index").Value(readLimit.GetTabletIndex());
+ })
+ .EndMap();
+}
+
+namespace {
+
+template <class T>
+std::optional<T> FindReadLimitComponent(const IAttributeDictionaryPtr& attributes, const TString& key)
+{
+ try {
+ return attributes->Find<T>(key);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing %Qv component of a read limit",
+ key)
+ << ex;
+ }
+}
+
+} // namespace
+
+void Deserialize(TLegacyReadLimit& readLimit, INodePtr node)
+{
+ if (node->GetType() != NYTree::ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Error parsing read limit: expected %Qlv, actual %Qlv",
+ NYTree::ENodeType::Map,
+ node->GetType());
+ }
+
+ readLimit = TLegacyReadLimit();
+ auto attributes = ConvertToAttributes(node);
+
+ auto optionalKey = FindReadLimitComponent<TLegacyOwningKey>(attributes, "key");
+ if (optionalKey) {
+ readLimit.SetLegacyKey(*optionalKey);
+ }
+
+ auto optionalRowIndex = FindReadLimitComponent<i64>(attributes, "row_index");
+ if (optionalRowIndex) {
+ readLimit.SetRowIndex(*optionalRowIndex);
+ }
+
+ auto optionalOffset = FindReadLimitComponent<i64>(attributes, "offset");
+ if (optionalOffset) {
+ readLimit.SetOffset(*optionalOffset);
+ }
+
+ auto optionalChunkIndex = FindReadLimitComponent<i64>(attributes, "chunk_index");
+ if (optionalChunkIndex) {
+ readLimit.SetChunkIndex(*optionalChunkIndex);
+ }
+
+ auto optionalTabletIndex = FindReadLimitComponent<i32>(attributes, "tablet_index");
+ if (optionalTabletIndex) {
+ readLimit.SetTabletIndex(*optionalTabletIndex);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLegacyReadRange::TLegacyReadRange(const TLegacyReadLimit& exact)
+ : LowerLimit_(exact)
+ , UpperLimit_(exact.GetSuccessor())
+{ }
+
+TLegacyReadRange::TLegacyReadRange(const TLegacyReadLimit& lowerLimit, const TLegacyReadLimit& upperLimit)
+ : LowerLimit_(lowerLimit)
+ , UpperLimit_(upperLimit)
+{ }
+
+TLegacyReadRange::TLegacyReadRange(const NProto::TReadRange& range)
+{
+ InitCopy(range);
+}
+
+TLegacyReadRange::TLegacyReadRange(NProto::TReadRange&& range)
+{
+ InitMove(std::move(range));
+}
+
+TLegacyReadRange& TLegacyReadRange::operator= (const NProto::TReadRange& range)
+{
+ InitCopy(range);
+ return *this;
+}
+
+TLegacyReadRange& TLegacyReadRange::operator= (NProto::TReadRange&& range)
+{
+ InitMove(std::move(range));
+ return *this;
+}
+
+void TLegacyReadRange::InitCopy(const NProto::TReadRange& range)
+{
+ LowerLimit_ = range.has_lower_limit() ? TLegacyReadLimit(range.lower_limit()) : TLegacyReadLimit();
+ UpperLimit_ = range.has_upper_limit() ? TLegacyReadLimit(range.upper_limit()) : TLegacyReadLimit();
+}
+
+void TLegacyReadRange::InitMove(NProto::TReadRange&& range)
+{
+ LowerLimit_ = range.has_lower_limit() ? TLegacyReadLimit(range.lower_limit()) : TLegacyReadLimit();
+ UpperLimit_ = range.has_upper_limit() ? TLegacyReadLimit(range.upper_limit()) : TLegacyReadLimit();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TLegacyReadRange& range)
+{
+ return Format("[<%v> : <%v>]", range.LowerLimit(), range.UpperLimit());
+}
+
+void ToProto(NProto::TReadRange* protoReadRange, const TLegacyReadRange& readRange)
+{
+ if (!readRange.LowerLimit().IsTrivial()) {
+ ToProto(protoReadRange->mutable_lower_limit(), readRange.LowerLimit());
+ }
+ if (!readRange.UpperLimit().IsTrivial()) {
+ ToProto(protoReadRange->mutable_upper_limit(), readRange.UpperLimit());
+ }
+}
+
+void FromProto(TLegacyReadRange* readRange, const NProto::TReadRange& protoReadRange)
+{
+ *readRange = TLegacyReadRange(protoReadRange);
+}
+
+void Serialize(const TLegacyReadRange& readRange, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .DoIf(!readRange.LowerLimit().IsTrivial(), [&] (TFluentMap fluent) {
+ fluent.Item("lower_limit").Value(readRange.LowerLimit());
+ })
+ .DoIf(!readRange.UpperLimit().IsTrivial(), [&] (TFluentMap fluent) {
+ fluent.Item("upper_limit").Value(readRange.UpperLimit());
+ })
+ .EndMap();
+
+}
+
+namespace {
+
+template <class T>
+std::optional<T> FindReadRangeComponent(const IAttributeDictionaryPtr& attributes, const TString& key)
+{
+ try {
+ return attributes->Find<T>(key);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing %Qv component of a read range",
+ key)
+ << ex;
+ }
+}
+
+} // namespace
+
+void Deserialize(TLegacyReadRange& readRange, NYTree::INodePtr node)
+{
+ if (node->GetType() != NYTree::ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Error parsing read range: expected %Qlv, actual %Qlv",
+ NYTree::ENodeType::Map,
+ node->GetType());
+ }
+
+ readRange = TLegacyReadRange();
+ auto attributes = ConvertToAttributes(node);
+ auto optionalExact = FindReadRangeComponent<TLegacyReadLimit>(attributes, "exact");
+ auto optionalLowerLimit = FindReadRangeComponent<TLegacyReadLimit>(attributes, "lower_limit");
+ auto optionalUpperLimit = FindReadRangeComponent<TLegacyReadLimit>(attributes, "upper_limit");
+
+ if (optionalExact) {
+ if (optionalLowerLimit || optionalUpperLimit) {
+ THROW_ERROR_EXCEPTION("\"lower_limit\" and \"upper_limit\" attributes cannot be specified "
+ "together with \"exact\" attribute");
+ }
+ readRange = TLegacyReadRange(*optionalExact);
+ }
+
+ if (optionalLowerLimit) {
+ readRange.LowerLimit() = *optionalLowerLimit;
+ }
+
+ if (optionalUpperLimit) {
+ readRange.UpperLimit() = *optionalUpperLimit;
+ }
+}
+
+void TLegacyReadRange::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist(context, LowerLimit_);
+ Persist(context, UpperLimit_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadLimit::TReadLimit(const TKeyBound& keyBound)
+ : KeyBound_(keyBound.ToOwning())
+{ }
+
+TReadLimit::TReadLimit(TOwningKeyBound keyBound)
+ : KeyBound_(std::move(keyBound))
+{ }
+
+TReadLimit::TReadLimit(
+ const NProto::TReadLimit& readLimit,
+ bool isUpper,
+ int keyLength)
+{
+ if (readLimit.has_key_bound_prefix()) {
+ NTableClient::FromProto(&KeyBound_.Prefix, readLimit.key_bound_prefix());
+ KeyBound_.IsUpper = isUpper;
+ KeyBound_.IsInclusive = readLimit.key_bound_is_inclusive();
+ } else if (readLimit.has_legacy_key()) {
+ YT_VERIFY(keyLength > 0);
+ TUnversionedOwningRow legacyKey;
+ FromProto(&legacyKey, readLimit.legacy_key());
+ KeyBound_ = KeyBoundFromLegacyRow(legacyKey, isUpper, keyLength);
+ }
+
+ if (readLimit.has_row_index()) {
+ RowIndex_ = readLimit.row_index();
+ }
+ if (readLimit.has_offset()) {
+ Offset_ = readLimit.offset();
+ }
+ if (readLimit.has_chunk_index()) {
+ ChunkIndex_ = readLimit.chunk_index();
+ }
+ if (readLimit.has_tablet_index()) {
+ TabletIndex_ = readLimit.tablet_index();
+ }
+}
+
+bool TReadLimit::IsTrivial() const
+{
+ return GetSelectorCount() == 0;
+}
+
+bool TReadLimit::HasIndependentSelectors() const
+{
+ if (GetSelectorCount() >= 3) {
+ return true;
+ }
+ if (GetSelectorCount() == 2) {
+ if (RowIndex_ && TabletIndex_) {
+ // Row index and tablet index are not independent.
+ return false;
+ } else {
+ return true;
+ }
+ }
+ // Having single or no selector at all means that there are no independent selectors.
+ return false;
+}
+
+int TReadLimit::GetSelectorCount() const
+{
+ int selectorCount = 0;
+
+ if (KeyBound_) {
+ ++selectorCount;
+ }
+
+ if (RowIndex_) {
+ ++selectorCount;
+ }
+
+ if (Offset_) {
+ ++selectorCount;
+ }
+
+ if (ChunkIndex_) {
+ ++selectorCount;
+ }
+
+ if (TabletIndex_) {
+ ++selectorCount;
+ }
+
+ return selectorCount;
+}
+
+TReadLimit TReadLimit::ToExactUpperCounterpart() const
+{
+ YT_VERIFY(!HasIndependentSelectors());
+
+ TReadLimit result = *this;
+
+ if (KeyBound_) {
+ YT_VERIFY(!KeyBound_.IsUpper);
+ // We are either a special >[] empty key bound, or a lower part of some
+ // key bound from an exact read limit, in which case we must be inclusive.
+ if (!KeyBound_.IsEmpty()) {
+ YT_VERIFY(KeyBound_.IsInclusive);
+ }
+ result.KeyBound_ = result.KeyBound_.Invert().ToggleInclusiveness();
+ }
+
+ // Ordered dyntables obey pretty tricky logic. All rows are ordered by
+ // (tablet_index, row_index) tuple lexicographically. Exact selector is
+ // transformed to lower+upper limits like following:
+ // - {tablet_index = 42} -> {tablet_index = 42}..{tablet_index = 43}
+ // - {tablet_index = 42; row_index = 123} ->
+ // {tablet_index = 42; row_index = 123}..{tablet_index = 42; row_index = 124}
+ // Thus, both for static and ordered dynamic tables we should increment (only)
+ // row index if it is present. Addititonally, for ordered dynamic tables,
+ // if row index is missing, we must increment tablet index if it is present.
+
+ if (RowIndex_) {
+ ++*result.RowIndex_;
+ } else if (TabletIndex_) {
+ ++*result.TabletIndex_;
+ }
+
+ if (Offset_) {
+ ++*result.Offset_;
+ }
+
+ if (ChunkIndex_) {
+ ++*result.ChunkIndex_;
+ }
+
+ return result;
+}
+
+TReadLimit TReadLimit::Invert() const
+{
+ YT_VERIFY(!HasIndependentSelectors());
+
+ auto result = *this;
+
+ if (KeyBound_) {
+ result.KeyBound() = result.KeyBound().Invert();
+ }
+
+ return result;
+}
+
+bool TReadLimit::operator == (const TReadLimit& other) const
+{
+ return
+ KeyBound_ == other.KeyBound_ &&
+ RowIndex_ == other.RowIndex_ &&
+ Offset_ == other.Offset_ &&
+ ChunkIndex_ == other.ChunkIndex_ &&
+ TabletIndex_ == other.TabletIndex_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TReadLimit& readLimit)
+{
+ using ::ToString;
+
+ TStringBuilder builder;
+ builder.AppendChar('{');
+
+ bool firstToken = true;
+ auto append = [&] (const char* label, TStringBuf value) {
+ if (!firstToken) {
+ builder.AppendString(", ");
+ }
+ firstToken = false;
+ builder.AppendString(label);
+ builder.AppendString(": ");
+ builder.AppendString(value);
+ };
+
+ if (readLimit.KeyBound()) {
+ append("Key", ToString(readLimit.KeyBound()));
+ }
+
+ if (readLimit.GetRowIndex()) {
+ append("RowIndex", ToString(readLimit.GetRowIndex()));
+ }
+
+ if (readLimit.GetOffset()) {
+ append("Offset", ToString(readLimit.GetOffset()));
+ }
+
+ if (readLimit.GetChunkIndex()) {
+ append("ChunkIndex", ToString(readLimit.GetChunkIndex()));
+ }
+
+ if (readLimit.GetTabletIndex()) {
+ append("TabletIndex", ToString(readLimit.GetTabletIndex()));
+ }
+
+ builder.AppendChar('}');
+ return builder.Flush();
+}
+
+void ToProto(NProto::TReadLimit* protoReadLimit, const TReadLimit& readLimit)
+{
+ if (readLimit.KeyBound()) {
+ ToProto(protoReadLimit->mutable_key_bound_prefix(), readLimit.KeyBound().Prefix);
+ protoReadLimit->set_key_bound_is_inclusive(readLimit.KeyBound().IsInclusive);
+ ToProto(protoReadLimit->mutable_legacy_key(), KeyBoundToLegacyRow(readLimit.KeyBound()));
+ }
+
+ if (readLimit.GetRowIndex()) {
+ protoReadLimit->set_row_index(*readLimit.GetRowIndex());
+ }
+ if (readLimit.GetOffset()) {
+ protoReadLimit->set_offset(*readLimit.GetOffset());
+ }
+ if (readLimit.GetChunkIndex()) {
+ protoReadLimit->set_chunk_index(*readLimit.GetChunkIndex());
+ }
+ if (readLimit.GetTabletIndex()) {
+ protoReadLimit->set_tablet_index(*readLimit.GetTabletIndex());
+ }
+}
+
+void FromProto(TReadLimit* readLimit, const NProto::TReadLimit& protoReadLimit, bool isUpper, int keyLength)
+{
+ // Formally speaking two exceptions in this method could be YT_VERIFY, but let's
+ // try to be more tolerant to possible bugs. After all, this code is used in
+ // master a lot.
+
+ auto validateKeyLengthIsPresent = [=] {
+ if (keyLength == 0) {
+ THROW_ERROR_EXCEPTION(
+ "Read limit contains key, but key length is not provided");
+ }
+ };
+
+ readLimit->KeyBound().IsUpper = isUpper;
+ if (protoReadLimit.has_key_bound_prefix()) {
+ validateKeyLengthIsPresent();
+
+ FromProto(&readLimit->KeyBound().Prefix, protoReadLimit.key_bound_prefix());
+ readLimit->KeyBound().IsInclusive = protoReadLimit.key_bound_is_inclusive();
+
+ if (readLimit->KeyBound().Prefix.GetCount() > keyLength) {
+ THROW_ERROR_EXCEPTION(
+ "Invalid key bound prefix length; expected no more than %v, actual %v",
+ keyLength,
+ readLimit->KeyBound().Prefix.GetCount());
+ }
+ } else if (protoReadLimit.has_legacy_key()) {
+ validateKeyLengthIsPresent();
+
+ TLegacyOwningKey legacyKey;
+ FromProto(&legacyKey, protoReadLimit.legacy_key());
+ readLimit->KeyBound() = KeyBoundFromLegacyRow(legacyKey, isUpper, keyLength);
+ }
+
+ if (protoReadLimit.has_row_index()) {
+ readLimit->SetRowIndex(protoReadLimit.row_index());
+ }
+ if (protoReadLimit.has_offset()) {
+ readLimit->SetOffset(protoReadLimit.offset());
+ }
+ if (protoReadLimit.has_chunk_index()) {
+ readLimit->SetChunkIndex(protoReadLimit.chunk_index());
+ }
+ if (protoReadLimit.has_tablet_index()) {
+ readLimit->SetTabletIndex(protoReadLimit.tablet_index());
+ }
+}
+
+void Serialize(const TReadLimit& readLimit, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .DoIf(static_cast<bool>(readLimit.KeyBound()), [&] (TFluentMap fluent) {
+ fluent
+ .Item("key_bound").Value(readLimit.KeyBound())
+ // COMPAT(max42): it is important to serialize also a key in order to keep
+ // backward compatibility with old clusters. Note that modern version of code
+ // should ignore "key" selector if "key_bound" is present.
+ .Item("key").Value(KeyBoundToLegacyRow(readLimit.KeyBound()));
+ })
+ .OptionalItem("row_index", readLimit.GetRowIndex())
+ .OptionalItem("offset", readLimit.GetOffset())
+ .OptionalItem("chunk_index", readLimit.GetChunkIndex())
+ .OptionalItem("tablet_index", readLimit.GetTabletIndex())
+ .EndMap();
+}
+
+void Deserialize(TReadLimit& readLimit, const INodePtr& node)
+{
+ if (node->GetType() != NYTree::ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Error parsing read limit: expected %Qlv node, actual %Qlv node",
+ NYTree::ENodeType::Map,
+ node->GetType());
+ }
+
+ readLimit = TReadLimit();
+ auto attributes = ConvertToAttributes(node);
+
+ if (auto optionalKeyBound = FindReadLimitComponent<TOwningKeyBound>(attributes, "key_bound")) {
+ readLimit.KeyBound() = *optionalKeyBound;
+ }
+
+ if (auto optionalRowIndex = FindReadLimitComponent<i64>(attributes, "row_index")) {
+ readLimit.SetRowIndex(*optionalRowIndex);
+ }
+
+ if (auto optionalOffset = FindReadLimitComponent<i64>(attributes, "offset")) {
+ readLimit.SetOffset(*optionalOffset);
+ }
+
+ if (auto optionalChunkIndex = FindReadLimitComponent<i64>(attributes, "chunk_index")) {
+ readLimit.SetChunkIndex(*optionalChunkIndex);
+ }
+
+ if (auto optionalTabletIndex = FindReadLimitComponent<i32>(attributes, "tablet_index")) {
+ readLimit.SetTabletIndex(*optionalTabletIndex);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadRange::TReadRange(TReadLimit lowerLimit, TReadLimit upperLimit)
+ : LowerLimit_(std::move(lowerLimit))
+ , UpperLimit_(std::move(upperLimit))
+{
+ if (LowerLimit_.KeyBound()) {
+ YT_VERIFY(!LowerLimit_.KeyBound().IsUpper);
+ }
+ if (UpperLimit_.KeyBound()) {
+ YT_VERIFY(UpperLimit_.KeyBound().IsUpper);
+ }
+}
+
+TReadRange::TReadRange(
+ const NProto::TReadRange& range,
+ int keyLength)
+{
+ if (range.has_lower_limit()) {
+ LowerLimit_ = TReadLimit(range.lower_limit(), /* isUpper */false, keyLength);
+ }
+ if (range.has_upper_limit()) {
+ UpperLimit_ = TReadLimit(range.upper_limit(), /* isUpper */true, keyLength);
+ }
+}
+
+bool TReadRange::operator == (const TReadRange& other) const
+{
+ return LowerLimit_ == other.LowerLimit_ && UpperLimit_ == other.UpperLimit_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TReadRange& readRange)
+{
+ return Format("[<%v> : <%v>]", readRange.LowerLimit(), readRange.UpperLimit());
+}
+
+void ToProto(NProto::TReadRange* protoReadRange, const TReadRange& readRange)
+{
+ if (!readRange.LowerLimit().IsTrivial()) {
+ if (readRange.LowerLimit().KeyBound()) {
+ YT_VERIFY(!readRange.LowerLimit().KeyBound().IsUpper);
+ }
+ ToProto(protoReadRange->mutable_lower_limit(), readRange.LowerLimit());
+ }
+ if (!readRange.UpperLimit().IsTrivial()) {
+ if (readRange.UpperLimit().KeyBound()) {
+ YT_VERIFY(readRange.UpperLimit().KeyBound().IsUpper);
+ }
+ ToProto(protoReadRange->mutable_upper_limit(), readRange.UpperLimit());
+ }
+}
+
+void FromProto(TReadRange* readRange, const NProto::TReadRange& protoReadRange, int keyLength)
+{
+ if (protoReadRange.has_lower_limit()) {
+ FromProto(&readRange->LowerLimit(), protoReadRange.lower_limit(), /* isUpper */ false, keyLength);
+ }
+ if (protoReadRange.has_upper_limit()) {
+ FromProto(&readRange->UpperLimit(), protoReadRange.upper_limit(), /* isUpper */ true, keyLength);
+ }
+}
+
+void Serialize(const TReadRange& readLimit, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("lower_limit").Value(readLimit.LowerLimit())
+ .Item("upper_limit").Value(readLimit.UpperLimit())
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(NProto::TReadLimit, /*key*/4, TUnversionedOwningRow)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadLimit ReadLimitFromLegacyReadLimit(const TLegacyReadLimit& legacyReadLimit, bool isUpper, int keyLength)
+{
+ TReadLimit result;
+ if (legacyReadLimit.HasRowIndex()) {
+ result.SetRowIndex(legacyReadLimit.GetRowIndex());
+ }
+ if (legacyReadLimit.HasChunkIndex()) {
+ result.SetChunkIndex(legacyReadLimit.GetChunkIndex());
+ }
+ if (legacyReadLimit.HasOffset()) {
+ result.SetOffset(legacyReadLimit.GetOffset());
+ }
+ if (legacyReadLimit.HasTabletIndex()) {
+ result.SetTabletIndex(legacyReadLimit.GetTabletIndex());
+ }
+ if (legacyReadLimit.HasLegacyKey()) {
+ result.KeyBound() = KeyBoundFromLegacyRow(legacyReadLimit.GetLegacyKey(), isUpper, keyLength);
+ }
+
+ return result;
+}
+
+TReadLimit ReadLimitFromLegacyReadLimitKeyless(const TLegacyReadLimit& legacyReadLimit)
+{
+ YT_VERIFY(!legacyReadLimit.HasLegacyKey());
+
+ TReadLimit result;
+ if (legacyReadLimit.HasRowIndex()) {
+ result.SetRowIndex(legacyReadLimit.GetRowIndex());
+ }
+ if (legacyReadLimit.HasChunkIndex()) {
+ result.SetChunkIndex(legacyReadLimit.GetChunkIndex());
+ }
+ if (legacyReadLimit.HasOffset()) {
+ result.SetOffset(legacyReadLimit.GetOffset());
+ }
+ if (legacyReadLimit.HasTabletIndex()) {
+ result.SetTabletIndex(legacyReadLimit.GetTabletIndex());
+ }
+
+ return result;
+}
+
+TLegacyReadLimit ReadLimitToLegacyReadLimit(const TReadLimit& readLimit)
+{
+ TLegacyReadLimit result;
+ if (const auto& rowIndex = readLimit.GetRowIndex()) {
+ result.SetRowIndex(*rowIndex);
+ }
+ if (const auto& chunkIndex = readLimit.GetChunkIndex()) {
+ result.SetChunkIndex(*chunkIndex);
+ }
+ if (const auto& offset = readLimit.GetOffset()) {
+ result.SetOffset(*offset);
+ }
+ if (const auto& tabletIndex = readLimit.GetTabletIndex()) {
+ result.SetTabletIndex(*tabletIndex);
+ }
+ if (readLimit.KeyBound()) {
+ result.SetLegacyKey(KeyBoundToLegacyRow(readLimit.KeyBound()));
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadRange ReadRangeFromLegacyReadRange(const TLegacyReadRange& legacyReadRange, int keyLength)
+{
+ TReadRange result;
+
+ result.LowerLimit() = ReadLimitFromLegacyReadLimit(legacyReadRange.LowerLimit(), /* isUpper */ false, keyLength);
+ result.UpperLimit() = ReadLimitFromLegacyReadLimit(legacyReadRange.UpperLimit(), /* isUpper */ true, keyLength);
+
+ return result;
+}
+
+TReadRange ReadRangeFromLegacyReadRangeKeyless(const TLegacyReadRange& legacyReadRange)
+{
+ TReadRange result;
+
+ result.LowerLimit() = ReadLimitFromLegacyReadLimitKeyless(legacyReadRange.LowerLimit());
+ result.UpperLimit() = ReadLimitFromLegacyReadLimitKeyless(legacyReadRange.UpperLimit());
+
+ return result;
+}
+
+TLegacyReadRange ReadRangeToLegacyReadRange(const TReadRange& readRange)
+{
+ TLegacyReadRange result;
+
+ result.LowerLimit() = ReadLimitToLegacyReadLimit(readRange.LowerLimit());
+ result.UpperLimit() = ReadLimitToLegacyReadLimit(readRange.UpperLimit());
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/read_limit.h b/yt/yt/client/chunk_client/read_limit.h
new file mode 100644
index 0000000000..a1f2662323
--- /dev/null
+++ b/yt/yt/client/chunk_client/read_limit.h
@@ -0,0 +1,248 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/read_limit.pb.h>
+
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/public.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLegacyReadLimit
+{
+public:
+ TLegacyReadLimit() = default;
+
+ explicit TLegacyReadLimit(const NProto::TReadLimit& readLimit);
+ explicit TLegacyReadLimit(NProto::TReadLimit&& readLimit);
+ explicit TLegacyReadLimit(const std::unique_ptr<NProto::TReadLimit>& protoLimit);
+
+ explicit TLegacyReadLimit(const NTableClient::TLegacyOwningKey& key);
+ explicit TLegacyReadLimit(NTableClient::TLegacyOwningKey&& key);
+
+ TLegacyReadLimit& operator= (const NProto::TReadLimit& protoLimit);
+ TLegacyReadLimit& operator= (NProto::TReadLimit&& protoLimit);
+
+ TLegacyReadLimit GetSuccessor() const;
+
+ const NProto::TReadLimit& AsProto() const;
+
+ const NTableClient::TLegacyOwningKey& GetLegacyKey() const;
+ bool HasLegacyKey() const;
+ TLegacyReadLimit& SetLegacyKey(const NTableClient::TLegacyOwningKey& key);
+ TLegacyReadLimit& SetLegacyKey(NTableClient::TLegacyOwningKey&& key);
+
+ i64 GetRowIndex() const;
+ bool HasRowIndex() const;
+ TLegacyReadLimit& SetRowIndex(i64 rowIndex);
+
+ i64 GetOffset() const;
+ bool HasOffset() const;
+ TLegacyReadLimit& SetOffset(i64 offset);
+
+ i64 GetChunkIndex() const;
+ bool HasChunkIndex() const;
+ TLegacyReadLimit& SetChunkIndex(i64 chunkIndex);
+
+ i32 GetTabletIndex() const;
+ bool HasTabletIndex() const;
+ TLegacyReadLimit& SetTabletIndex(i32 tabletIndex);
+
+ bool IsTrivial() const;
+
+ void MergeLowerLegacyKey(const NTableClient::TLegacyOwningKey& key);
+ void MergeUpperLegacyKey(const NTableClient::TLegacyOwningKey& key);
+
+ void MergeLowerRowIndex(i64 rowIndex);
+ void MergeUpperRowIndex(i64 rowIndex);
+
+ void Persist(const TStreamPersistenceContext& context);
+
+ size_t SpaceUsed() const;
+
+private:
+ NProto::TReadLimit ReadLimit_;
+ NTableClient::TLegacyOwningKey Key_;
+
+ void InitKey();
+ void InitCopy(const NProto::TReadLimit& readLimit);
+ void InitMove(NProto::TReadLimit&& readLimit);
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TLegacyReadLimit& limit);
+
+bool IsTrivial(const TLegacyReadLimit& limit);
+bool IsTrivial(const NProto::TReadLimit& limit);
+
+void ToProto(NProto::TReadLimit* protoReadLimit, const TLegacyReadLimit& readLimit);
+void FromProto(TLegacyReadLimit* readLimit, const NProto::TReadLimit& protoReadLimit);
+
+void Serialize(const TLegacyReadLimit& readLimit, NYson::IYsonConsumer* consumer);
+void Deserialize(TLegacyReadLimit& readLimit, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLegacyReadRange
+{
+public:
+ TLegacyReadRange() = default;
+ TLegacyReadRange(const TLegacyReadLimit& lowerLimit, const TLegacyReadLimit& upperLimit);
+ explicit TLegacyReadRange(const TLegacyReadLimit& exact);
+
+ explicit TLegacyReadRange(const NProto::TReadRange& range);
+ explicit TLegacyReadRange(NProto::TReadRange&& range);
+ TLegacyReadRange& operator= (const NProto::TReadRange& range);
+ TLegacyReadRange& operator= (NProto::TReadRange&& range);
+
+ DEFINE_BYREF_RW_PROPERTY(TLegacyReadLimit, LowerLimit);
+ DEFINE_BYREF_RW_PROPERTY(TLegacyReadLimit, UpperLimit);
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ void InitCopy(const NProto::TReadRange& range);
+ void InitMove(NProto::TReadRange&& range);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TLegacyReadRange& range);
+
+void ToProto(NProto::TReadRange* protoReadRange, const TLegacyReadRange& readRange);
+void FromProto(TLegacyReadRange* readRange, const NProto::TReadRange& protoReadRange);
+
+void Serialize(const TLegacyReadRange& readRange, NYson::IYsonConsumer* consumer);
+void Deserialize(TLegacyReadRange& readRange, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadLimit
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(NTableClient::TOwningKeyBound, KeyBound);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<i64>, RowIndex);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<i64>, Offset);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<i64>, ChunkIndex);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<i32>, TabletIndex);
+
+public:
+ TReadLimit() = default;
+ explicit TReadLimit(const NTableClient::TKeyBound& keyBound);
+ explicit TReadLimit(NTableClient::TOwningKeyBound keyBound);
+ TReadLimit(
+ const NProto::TReadLimit& readLimit,
+ bool isUpper,
+ int keyLength = 0);
+
+ //! Returns true if no selectors are specified.
+ bool IsTrivial() const;
+
+ //! Returns true if this read limit contains more than one "independent" selector.
+ //! By "independent" we mean following: "row_index" and "key" are independent, but
+ //! "tablet_index" and "row_index" are not since they are used for ordered dyntables
+ //! and tuple (tablet_index, row_index) defines a read limit for them.
+ //!
+ //! Rationale: this method allows checking if read limit is suitable for "exact" clause
+ //! or for specifying read ranges for Erase operation.
+ //!
+ //! Equivalent way of thinking: if readLimit.HasIndependentSelectors() == true,
+ //! then {upper_limit = readLimit} and {lower_limit = readLimit} always define
+ //! complementary row sets.
+ bool HasIndependentSelectors() const;
+
+ //! Return number of specified selectors.
+ int GetSelectorCount() const;
+
+ //! Given read limit with exactly one selector, either increment single integer selector, or
+ //! invert key bound selector. This method is used to transform "exact" YPath read limit
+ //! into a pair of lower and upper read limit.
+ TReadLimit ToExactUpperCounterpart() const;
+
+ //! Return inverted read limit, i.e. integer selectors remain as is and key bound is inverted.
+ //! NB: this method makes YT_VERIFY that read limit contains exactly one selector. Otherwise
+ //! semantics of such method is weird.
+ TReadLimit Invert() const;
+
+ bool operator == (const TReadLimit& other) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TReadLimit& readLimit);
+
+void ToProto(NProto::TReadLimit* protoReadLimit, const TReadLimit& readLimit);
+//! If protoReadLimit contains key, it is transformed into new key bound by
+//! calling KeyBoundFromLegacyKey using *keyLength. In this case, if keyLength
+//! is std::nullopt, exception is thrown.
+void FromProto(TReadLimit* readLimit, const NProto::TReadLimit& protoReadLimit, bool isUpper, int keyLength);
+
+void Serialize(const TReadLimit& readLimit, NYson::IYsonConsumer* consumer);
+//! This method deserializes modern read limit representation, recognizing only key bound, but not legacy key.
+void Deserialize(TReadLimit& readLimit, const NYTree::INodePtr& node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadRange
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(TReadLimit, LowerLimit);
+ DEFINE_BYREF_RW_PROPERTY(TReadLimit, UpperLimit);
+
+public:
+ TReadRange() = default;
+ TReadRange(TReadLimit lowerLimit, TReadLimit upperLimit);
+ TReadRange(
+ const NProto::TReadRange& range,
+ int keyLength = 0);
+
+ bool operator == (const TReadRange& other) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TReadRange& readRange);
+
+void ToProto(NProto::TReadRange* protoReadRange, const TReadRange& readRange);
+//! See comment for FromProto(TReadLimit*, const NProto::TReadLimit&, bool, std::optional<int>).
+void FromProto(TReadRange* readRange, const NProto::TReadRange& protoReadRange, int keyLength);
+
+void Serialize(const TReadRange& readLimit, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Interop functions.
+
+//! Transform legacy read limit to new read limit possibly transforming legacy key into
+//! key bound by calling KeyBoundFromLegacyKey.
+TReadLimit ReadLimitFromLegacyReadLimit(const TLegacyReadLimit& legacyReadLimit, bool isUpper, int keyLength);
+//! Transform legacy read limit without legacy key into new read limit (merely copying all integer fields).
+TReadLimit ReadLimitFromLegacyReadLimitKeyless(const TLegacyReadLimit& legacyReadLimit);
+
+//! Transform new read limit into legacy read limit.
+TLegacyReadLimit ReadLimitToLegacyReadLimit(const TReadLimit& readLimit);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Transform legacy read range to new read range possibly transforming legacy keys into
+//! key bounds by calling KeyBoundFromLegacyKey.
+TReadRange ReadRangeFromLegacyReadRange(const TLegacyReadRange& legacyReadRange, int keyLength);
+//! Transform legacy read range without legacy keys into new read range (merely copying all integer fields).
+TReadRange ReadRangeFromLegacyReadRangeKeyless(const TLegacyReadRange& legacyReadRange);
+
+//! Transform new read range into legacy read range.
+TLegacyReadRange ReadRangeToLegacyReadRange(const TReadRange& readRange);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/reader_base.h b/yt/yt/client/chunk_client/reader_base.h
new file mode 100644
index 0000000000..baa163fb8f
--- /dev/null
+++ b/yt/yt/client/chunk_client/reader_base.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "data_statistics.h"
+#include "ready_event_reader_base.h"
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IReaderBase
+ : public virtual IReadyEventReaderBase
+{
+ virtual NProto::TDataStatistics GetDataStatistics() const = 0;
+ virtual TCodecStatistics GetDecompressionStatistics() const = 0;
+
+ virtual bool IsFetchingCompleted() const = 0;
+ virtual std::vector<TChunkId> GetFailedChunkIds() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IReaderBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/ready_event_reader_base.cpp b/yt/yt/client/chunk_client/ready_event_reader_base.cpp
new file mode 100644
index 0000000000..6f7e217916
--- /dev/null
+++ b/yt/yt/client/chunk_client/ready_event_reader_base.cpp
@@ -0,0 +1,39 @@
+#include "ready_event_reader_base.h"
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TReadyEventReaderBase::GetReadyEvent() const
+{
+ WaitTimer_.StartIfNotActive();
+ if (ReadyEvent_.IsSet()) {
+ WaitTimer_.Stop();
+ }
+ return ReadyEvent_;
+}
+
+const TFuture<void>& TReadyEventReaderBase::ReadyEvent() const
+{
+ return ReadyEvent_;
+}
+
+void TReadyEventReaderBase::SetReadyEvent(TFuture<void> readyEvent)
+{
+ // We could use TTimingGuard here but we try to not prolong
+ // reader lifetime for such insignificant business as timing.
+ ReadyEvent_ = readyEvent.Apply(BIND([weakThis = MakeWeak(this)] {
+ if (auto strongThis = weakThis.Lock()) {
+ strongThis->WaitTimer_.Stop();
+ }
+ }));
+}
+
+TDuration TReadyEventReaderBase::GetWaitTime() const
+{
+ return WaitTimer_.GetElapsedTime();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/ready_event_reader_base.h b/yt/yt/client/chunk_client/ready_event_reader_base.h
new file mode 100644
index 0000000000..ffaf6b1cf7
--- /dev/null
+++ b/yt/yt/client/chunk_client/ready_event_reader_base.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IReadyEventReaderBase
+ : public virtual TRefCounted
+{
+ virtual TFuture<void> GetReadyEvent() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IReadyEventReaderBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadyEventReaderBase
+ : public virtual NChunkClient::IReadyEventReaderBase
+{
+protected:
+ //! Return ready event without starting wait timer. Intended for internal use in subclasses.
+ const TFuture<void>& ReadyEvent() const;
+
+ //! Set ready event. Ready event is wrapped with a callback which
+ //! stops wait timer when ready event is ready.
+ void SetReadyEvent(TFuture<void> readyEvent);
+
+ //! Return how much time caller spent waiting on ready event.
+ TDuration GetWaitTime() const;
+
+private:
+ TFuture<void> ReadyEvent_ = VoidFuture;
+
+ //! This timer is started when GetReadyEvent() is invoked and stopped when ready event is set.
+ //! In other words, it shows how much time caller spends waiting on ready event.
+ mutable NProfiling::TWallTimer WaitTimer_ = NProfiling::TWallTimer(false /* start */);
+
+ //! Return ready event and start wait timer if it is not already active.
+ //! This method is intended for external use, but not for accessing ready event from subclasses, thus private.
+ TFuture<void> GetReadyEvent() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/chunk_client/writer_base.h b/yt/yt/client/chunk_client/writer_base.h
new file mode 100644
index 0000000000..e4cd0b00c3
--- /dev/null
+++ b/yt/yt/client/chunk_client/writer_base.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NChunkClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! The purpose of this interface is to be a virtual base for TMultiChunkSequentialWriter
+//! and some specific writers, e.g. IVersionedWriter, to mix them up.
+struct IWriterBase
+ : public virtual TRefCounted
+{
+ //! Returns an asynchronous flag enabling to wait until data is written.
+ virtual TFuture<void> GetReadyEvent() = 0;
+
+ //! Closes the writer. Must be the last call to the writer.
+ virtual TFuture<void> Close() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/complex_types/check_type_compatibility.cpp b/yt/yt/client/complex_types/check_type_compatibility.cpp
new file mode 100644
index 0000000000..49a744198f
--- /dev/null
+++ b/yt/yt/client/complex_types/check_type_compatibility.cpp
@@ -0,0 +1,444 @@
+#include "check_type_compatibility.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+
+using namespace NYT::NTableClient;
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TCompatibilityPair = std::pair<ESchemaCompatibility, TError>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TCompatibilityPair CheckTypeCompatibilityImpl(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor);
+
+static const TCompatibilityPair& MinCompatibility(const TCompatibilityPair& lhs, const TCompatibilityPair& rhs)
+{
+ if (lhs.first <= rhs.first) {
+ return lhs;
+ } else {
+ return rhs;
+ }
+}
+
+static TCompatibilityPair CreateResultPair(
+ ESchemaCompatibility compatibility,
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor)
+{
+ if (compatibility == ESchemaCompatibility::FullyCompatible) {
+ return {compatibility, TError()};
+ }
+ return {
+ compatibility,
+ TError(
+ "Type of %Qv field is modified in non backward compatible manner",
+ oldDescriptor.GetDescription())
+ << TErrorAttribute("old_type", ToString(*oldDescriptor.GetType()))
+ << TErrorAttribute("new_type", ToString(*newDescriptor.GetType()))
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ESimpleTypeClass,
+ (Int)
+ (Uint)
+ (Floating)
+ (String)
+);
+
+template <ESimpleTypeClass typeClass>
+static int GetSimpleTypeRank(ESimpleLogicalValueType type)
+{
+ if constexpr (typeClass == ESimpleTypeClass::Int) {
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ return 8;
+ case ESimpleLogicalValueType::Int16:
+ return 16;
+ case ESimpleLogicalValueType::Int32:
+ return 32;
+ case ESimpleLogicalValueType::Int64:
+ return 64;
+ default:
+ return -1;
+ }
+ } else if constexpr (typeClass == ESimpleTypeClass::Uint) {
+ switch (type) {
+ case ESimpleLogicalValueType::Uint8:
+ return 8;
+ case ESimpleLogicalValueType::Uint16:
+ return 16;
+ case ESimpleLogicalValueType::Uint32:
+ return 32;
+ case ESimpleLogicalValueType::Uint64:
+ return 64;
+ default:
+ return -1;
+ }
+ } else if constexpr (typeClass == ESimpleTypeClass::Floating) {
+ switch (type) {
+ case ESimpleLogicalValueType::Float:
+ return 32;
+ case ESimpleLogicalValueType::Double:
+ return 64;
+ default:
+ return -1;
+ }
+ } else if constexpr (typeClass == ESimpleTypeClass::String) {
+ switch (type) {
+ case ESimpleLogicalValueType::Utf8:
+ return 1;
+ case ESimpleLogicalValueType::String:
+ return 2;
+ default:
+ return -1;
+ }
+ } else {
+ // Poor man static_assert(false).
+ static_assert(typeClass == ESimpleTypeClass::Int);
+ }
+}
+
+template <ESimpleTypeClass typeClass>
+static constexpr ESchemaCompatibility CheckCompatibilityUsingClass(ESimpleLogicalValueType oldType, ESimpleLogicalValueType newType)
+{
+ int oldRank = GetSimpleTypeRank<typeClass>(oldType);
+ if (oldRank <= 0) {
+ THROW_ERROR_EXCEPTION("Internal error; unexpected rank %v of type %Qlv",
+ oldRank,
+ oldType);
+ }
+ int newRank = GetSimpleTypeRank<typeClass>(newType);
+ if (newRank < 0) {
+ return ESchemaCompatibility::Incompatible;
+ }
+ if (newRank >= oldRank) {
+ return ESchemaCompatibility::FullyCompatible;
+ } else {
+ return ESchemaCompatibility::RequireValidation;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TCompatibilityPair CheckTypeCompatibilitySimple(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor)
+{
+ auto oldElement = oldDescriptor.GetType()->AsSimpleTypeRef().GetElement();
+ auto newElement = newDescriptor.GetType()->AsSimpleTypeRef().GetElement();
+
+ if (newElement == ESimpleLogicalValueType::Any) {
+ // N.B. for historical reasons a bunch of types could be cast to Any type.
+ ESchemaCompatibility result;
+ switch (oldElement) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Utf8:
+
+ case ESimpleLogicalValueType::Boolean:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ case ESimpleLogicalValueType::Interval:
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+
+ case ESimpleLogicalValueType::Any:
+ result = ESchemaCompatibility::FullyCompatible;
+ break;
+
+ default:
+ result = ESchemaCompatibility::Incompatible;
+ break;
+ }
+ return CreateResultPair(
+ result,
+ oldDescriptor,
+ newDescriptor);
+ }
+
+ switch (oldElement) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+ return CreateResultPair(
+ CheckCompatibilityUsingClass<ESimpleTypeClass::Int>(oldElement, newElement),
+ oldDescriptor,
+ newDescriptor);
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+ return CreateResultPair(
+ CheckCompatibilityUsingClass<ESimpleTypeClass::Uint>(oldElement, newElement),
+ oldDescriptor,
+ newDescriptor);
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ return CreateResultPair(
+ CheckCompatibilityUsingClass<ESimpleTypeClass::Floating>(oldElement, newElement),
+ oldDescriptor,
+ newDescriptor);
+
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::String:
+ return CreateResultPair(
+ CheckCompatibilityUsingClass<ESimpleTypeClass::String>(oldElement, newElement),
+ oldDescriptor,
+ newDescriptor);
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ case ESimpleLogicalValueType::Boolean:
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ case ESimpleLogicalValueType::Interval:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::Uuid:
+ case ESimpleLogicalValueType::Any: {
+ const auto compatibility =
+ oldElement == newElement ? ESchemaCompatibility::FullyCompatible : ESchemaCompatibility::Incompatible;
+ return CreateResultPair(compatibility, oldDescriptor, newDescriptor);
+ }
+ }
+ THROW_ERROR_EXCEPTION("Internal error: unknown logical type: %Qlv",
+ oldElement);
+}
+
+TCompatibilityPair CheckElementsCompatibility(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor,
+ bool allowNewElements)
+{
+ const auto oldSize = std::ssize(oldDescriptor.GetType()->GetElements());
+ const auto newSize = std::ssize(newDescriptor.GetType()->GetElements());
+ if (oldSize > newSize) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Some elements of %Qv are removed",
+ oldDescriptor.GetDescription()),
+ };
+ }
+ if (!allowNewElements && oldSize != newSize) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Added new elements to tuple %Qv",
+ oldDescriptor.GetDescription()),
+ };
+ }
+ auto result = std::pair(ESchemaCompatibility::FullyCompatible, TError());
+ for (int i = 0; result.first != ESchemaCompatibility::Incompatible && i < oldSize; ++i) {
+ result = MinCompatibility(
+ result,
+ CheckTypeCompatibilityImpl(oldDescriptor.Element(i), newDescriptor.Element(i)));
+ }
+ return result;
+}
+
+TCompatibilityPair CheckFieldsCompatibility(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor,
+ bool checkNewFieldsNullability)
+{
+ const auto& oldFields = oldDescriptor.GetType()->GetFields();
+ const auto& newFields = newDescriptor.GetType()->GetFields();
+ if (oldFields.size() > newFields.size()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Some members of %Qv are removed",
+ oldDescriptor.GetDescription()),
+ };
+ }
+
+ int fieldIndex = 0;
+ //
+ auto result = std::pair(ESchemaCompatibility::FullyCompatible, TError());
+ for (; fieldIndex < std::ssize(oldFields) && result.first != ESchemaCompatibility::Incompatible; ++fieldIndex) {
+ const auto& oldName = oldFields[fieldIndex].Name;
+ const auto& newName = newFields[fieldIndex].Name;
+ if (oldName != newName) {
+ result = {
+ ESchemaCompatibility::Incompatible,
+ TError(
+ "Member name mismatch in %Qv: old name %Qv, new name %Qv",
+ oldDescriptor.GetDescription(),
+ oldName,
+ newName)
+ };
+ } else {
+ const auto& oldFieldDescriptor = oldDescriptor.Field(fieldIndex);
+ const auto& newFieldDescriptor = newDescriptor.Field(fieldIndex);
+ auto currentCompatibility = CheckTypeCompatibilityImpl(oldFieldDescriptor, newFieldDescriptor);
+ result = MinCompatibility(result, currentCompatibility);
+ }
+ }
+
+ // All added fields must be nullable
+ if (checkNewFieldsNullability) {
+ for (; fieldIndex < std::ssize(newFields) && result.first != ESchemaCompatibility::Incompatible; ++fieldIndex) {
+ const auto& newFieldDescriptor = newDescriptor.Field(fieldIndex);
+ if (!newFieldDescriptor.GetType()->IsNullable()) {
+ result = {
+ ESchemaCompatibility::Incompatible,
+ TError(
+ "Newly added member %Qv is not optional",
+ newFieldDescriptor.GetDescription())
+ << TErrorAttribute("new_type", ToString(*newFieldDescriptor.GetType()))
+ };
+ }
+ }
+ }
+ return result;
+}
+
+TCompatibilityPair CheckDictTypeCompatibility(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor)
+{
+ auto keyCompatibility = CheckTypeCompatibilityImpl(
+ oldDescriptor.DictKey(),
+ newDescriptor.DictKey());
+ if (keyCompatibility.first == ESchemaCompatibility::Incompatible) {
+ return keyCompatibility;
+ }
+ const auto valueCompatibility = CheckTypeCompatibilityImpl(
+ oldDescriptor.DictValue(),
+ newDescriptor.DictValue());
+ return MinCompatibility(keyCompatibility, valueCompatibility);
+}
+
+TCompatibilityPair CheckDecimalTypeCompatibility(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor)
+{
+ if (*oldDescriptor.GetType() == *newDescriptor.GetType()) {
+ return {ESchemaCompatibility::FullyCompatible, {}};
+ } else {
+ return CreateResultPair(ESchemaCompatibility::Incompatible, oldDescriptor, newDescriptor);
+ }
+}
+
+// Returns pair:
+// 1. Inner element that is neither optional nor tagged.
+// 2. How many times this element is wrapped into Optional type.
+static std::pair<TComplexTypeFieldDescriptor, int> UnwrapOptionalAndTagged(const TComplexTypeFieldDescriptor& descriptor)
+{
+ int nesting = 0;
+ auto current = descriptor;
+ while (true) {
+ const auto metatype = current.GetType()->GetMetatype();
+ if (metatype == ELogicalMetatype::Optional) {
+ ++nesting;
+ current = current.OptionalElement();
+ } else if (metatype == ELogicalMetatype::Tagged) {
+ current = current.TaggedElement();
+ } else {
+ break;
+ }
+ }
+ return {current, nesting};
+}
+
+static TCompatibilityPair CheckTypeCompatibilityImpl(
+ const TComplexTypeFieldDescriptor& oldDescriptor,
+ const TComplexTypeFieldDescriptor& newDescriptor)
+{
+ const auto oldMetatype = oldDescriptor.GetType()->GetMetatype();
+ const auto newMetatype = newDescriptor.GetType()->GetMetatype();
+
+ if (oldMetatype == ELogicalMetatype::Optional ||
+ oldMetatype == ELogicalMetatype::Tagged ||
+ newMetatype == ELogicalMetatype::Optional ||
+ newMetatype == ELogicalMetatype::Tagged)
+ {
+ const auto [oldElement, oldNesting] = UnwrapOptionalAndTagged(oldDescriptor);
+ const auto [newElement, newNesting] = UnwrapOptionalAndTagged(newDescriptor);
+
+ if (oldNesting == newNesting || oldNesting == 0 && newNesting == 1) {
+ return CheckTypeCompatibilityImpl(oldElement, newElement);
+ } else if (oldNesting == 1 && newNesting == 0) {
+ auto elementCompatibility = CheckTypeCompatibilityImpl(oldElement, newElement);
+ return MinCompatibility(
+ elementCompatibility,
+ CreateResultPair(ESchemaCompatibility::RequireValidation, oldElement, newElement));
+ } else {
+ return CreateResultPair(ESchemaCompatibility::Incompatible, oldElement, newElement);
+ }
+ }
+
+ if (oldMetatype != newMetatype) {
+ return CreateResultPair(ESchemaCompatibility::Incompatible, oldDescriptor, newDescriptor);
+ }
+
+ switch (oldMetatype) {
+ case ELogicalMetatype::Simple:
+ return CheckTypeCompatibilitySimple(oldDescriptor, newDescriptor);
+ case ELogicalMetatype::Optional:
+ case ELogicalMetatype::Tagged:
+ // Optional and Tagged cases were checked earlier in this function.
+ THROW_ERROR_EXCEPTION("Internal error; unexpected optional or tagged");
+ case ELogicalMetatype::List:
+ return CheckTypeCompatibilityImpl(oldDescriptor.ListElement(), newDescriptor.ListElement());
+ case ELogicalMetatype::VariantStruct:
+ return CheckFieldsCompatibility(
+ oldDescriptor,
+ newDescriptor,
+ /*checkNewFieldsNullability*/ false);
+ case ELogicalMetatype::Struct:
+ return CheckFieldsCompatibility(
+ oldDescriptor,
+ newDescriptor,
+ /*checkNewFieldsNullability*/ true);
+ case ELogicalMetatype::Tuple:
+ return CheckElementsCompatibility(
+ oldDescriptor,
+ newDescriptor,
+ /*allowNewElements*/ false);
+ case ELogicalMetatype::VariantTuple:
+ return CheckElementsCompatibility(
+ oldDescriptor,
+ newDescriptor,
+ /*allowNewElement*/ true);
+ case ELogicalMetatype::Dict:
+ return CheckDictTypeCompatibility(oldDescriptor, newDescriptor);
+ case ELogicalMetatype::Decimal:
+ return CheckDecimalTypeCompatibility(oldDescriptor, newDescriptor);
+ }
+ THROW_ERROR_EXCEPTION("Internal error; unexpected metatype: %Qlv", oldMetatype);
+}
+
+TCompatibilityPair CheckTypeCompatibility(
+ const NYT::NTableClient::TLogicalTypePtr& oldType,
+ const NYT::NTableClient::TLogicalTypePtr& newType)
+{
+ return CheckTypeCompatibilityImpl(TComplexTypeFieldDescriptor(oldType), TComplexTypeFieldDescriptor(newType));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/check_type_compatibility.h b/yt/yt/client/complex_types/check_type_compatibility.h
new file mode 100644
index 0000000000..086f1d9134
--- /dev/null
+++ b/yt/yt/client/complex_types/check_type_compatibility.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Returned value is pair with elements
+// 1. Compatibility of types.
+// 2. If types are NOT FullyCompatible, error contains description of incompatibility.
+std::pair<NTableClient::ESchemaCompatibility, TError> CheckTypeCompatibility(
+ const NYT::NTableClient::TLogicalTypePtr& oldType,
+ const NYT::NTableClient::TLogicalTypePtr& newType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/check_yson_token-inl.h b/yt/yt/client/complex_types/check_yson_token-inl.h
new file mode 100644
index 0000000000..4f6f83da4d
--- /dev/null
+++ b/yt/yt/client/complex_types/check_yson_token-inl.h
@@ -0,0 +1,25 @@
+#ifndef CHECK_YSON_TOKEN_INL_H_
+#error "Direct inclusion of this file is not allowed, include check_yson.h"
+// For the sake of sane code completion.
+#include "check_yson_token-inl.h"
+#endif
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const NYson::TYsonPullParserCursor& cursor,
+ NYson::EYsonItemType expected)
+{
+ if (expected != cursor->GetType()) {
+ ThrowUnexpectedYsonTokenException(descriptor, cursor, {expected});
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/check_yson_token.cpp b/yt/yt/client/complex_types/check_yson_token.cpp
new file mode 100644
index 0000000000..6109f31d95
--- /dev/null
+++ b/yt/yt/client/complex_types/check_yson_token.cpp
@@ -0,0 +1,22 @@
+#include "check_yson_token.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+
+namespace NYT::NComplexTypes {
+
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ThrowUnexpectedYsonTokenException(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TYsonPullParserCursor& cursor,
+ const std::vector<EYsonItemType>& expected)
+{
+ ThrowUnexpectedYsonTokenException(descriptor.GetDescription(), cursor, expected);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/check_yson_token.h b/yt/yt/client/complex_types/check_yson_token.h
new file mode 100644
index 0000000000..153acc298a
--- /dev/null
+++ b/yt/yt/client/complex_types/check_yson_token.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <yt/yt/client/table_client/public.h>
+#include <yt/yt/core/yson/public.h>
+
+#include <vector>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const NYson::TYsonPullParserCursor& cursor,
+ NYson::EYsonItemType expected);
+
+void ThrowUnexpectedYsonTokenException(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const NYson::TYsonPullParserCursor& cursor,
+ const std::vector<NYson::EYsonItemType>& expected);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
+
+#define CHECK_YSON_TOKEN_INL_H_
+#include "check_yson_token-inl.h"
+#undef CHECK_YSON_TOKEN_INL_H_
diff --git a/yt/yt/client/complex_types/infinite_entity.cpp b/yt/yt/client/complex_types/infinite_entity.cpp
new file mode 100644
index 0000000000..8534dd9892
--- /dev/null
+++ b/yt/yt/client/complex_types/infinite_entity.cpp
@@ -0,0 +1,39 @@
+#include "infinite_entity.h"
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInfiniteEntity::TInfiniteEntity()
+ : Stream_(TStringBuf("#;#;#;#;#;#;#;#;"))
+ , Parser_(&Stream_, NYson::EYsonType::ListFragment)
+ , Cursor_(&Parser_)
+{
+ YT_VERIFY(Cursor_.TryConsumeFragmentStart());
+}
+
+NYson::TYsonPullParserCursor* TInfiniteEntity::GetCursor()
+{
+ return &Cursor_;
+}
+
+TInfiniteEntity::TRingBufferStream::TRingBufferStream(TStringBuf buffer)
+ : Buffer_(buffer)
+ , Pointer_(Buffer_.data())
+{ }
+
+size_t TInfiniteEntity::TRingBufferStream::DoNext(const void** ptr, size_t len)
+{
+ const auto end = Buffer_.data() + Buffer_.size();
+ auto result = Min<size_t>(len, end - Pointer_);
+ *ptr = Pointer_;
+ Pointer_ += result;
+ if (Pointer_ == end) {
+ Pointer_ = Buffer_.data();
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/infinite_entity.h b/yt/yt/client/complex_types/infinite_entity.h
new file mode 100644
index 0000000000..fcf1dd0782
--- /dev/null
+++ b/yt/yt/client/complex_types/infinite_entity.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInfiniteEntity
+{
+public:
+ TInfiniteEntity();
+
+ NYson::TYsonPullParserCursor* GetCursor();
+
+private:
+ class TRingBufferStream
+ : public IZeroCopyInput
+ {
+ public:
+ explicit TRingBufferStream(TStringBuf buffer);
+
+ private:
+ size_t DoNext(const void** ptr, size_t len) override;
+
+ private:
+ const TStringBuf Buffer_;
+ const char* Pointer_;
+ };
+
+private:
+ TRingBufferStream Stream_;
+ NYson::TYsonPullParser Parser_;
+ NYson::TYsonPullParserCursor Cursor_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes \ No newline at end of file
diff --git a/yt/yt/client/complex_types/public.h b/yt/yt/client/complex_types/public.h
new file mode 100644
index 0000000000..fa9173f00e
--- /dev/null
+++ b/yt/yt/client/complex_types/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename... TArgs>
+using TComplexTypeYsonScanner = std::function<void(NYson::TYsonPullParserCursor*, TArgs...)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes \ No newline at end of file
diff --git a/yt/yt/client/complex_types/scanner_factory.h b/yt/yt/client/complex_types/scanner_factory.h
new file mode 100644
index 0000000000..647a67855d
--- /dev/null
+++ b/yt/yt/client/complex_types/scanner_factory.h
@@ -0,0 +1,231 @@
+#pragma once
+
+#include "check_yson_token.h"
+#include "infinite_entity.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/core/yson/pull_parser.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Helper class to generate efficient scanners of complex values.
+template <typename... TArgs>
+class TScannerFactory
+{
+public:
+ using TScanner = TComplexTypeYsonScanner<TArgs...>;
+
+ template <typename TApplier, typename TElement>
+ static TScanner CreateOptionalScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ TElement element)
+ {
+ using namespace NYson;
+ bool isElementNullable = descriptor.GetType()->AsOptionalTypeRef().IsElementNullable();
+
+ if (isElementNullable) {
+ return [
+ element=std::move(element),
+ descriptor=std::move(descriptor),
+ applier=std::move(applier)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ const auto ysonType = (*cursor)->GetType();
+ if (ysonType == EYsonItemType::EntityValue) {
+ applier.OnEmptyOptional(args...);
+ cursor->Next();
+ } else if (ysonType != EYsonItemType::BeginList) {
+ ThrowUnexpectedYsonTokenException(descriptor, *cursor, {EYsonItemType::EntityValue, EYsonItemType::BeginList});
+ } else {
+ cursor->Next();
+ applier.OnFilledOptional(element, cursor, args...);
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ }
+ };
+ } else {
+ return [
+ element=std::move(element),
+ descriptor=std::move(descriptor),
+ applier=std::move(applier)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ auto ysonType = (*cursor)->GetType();
+ if (ysonType == EYsonItemType::EntityValue) {
+ applier.OnEmptyOptional(args...);
+ cursor->Next();
+ } else {
+ applier.OnFilledOptional(element, cursor, args...);
+ }
+ };
+ }
+ }
+
+ template <typename TApplier, typename TElement>
+ static TScanner CreateListScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ TElement element)
+ {
+ using namespace NYson;
+ return [
+ element=std::move(element),
+ descriptor=std::move(descriptor),
+ applier=std::move(applier)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ applier.OnListBegin(args...);
+ while ((*cursor)->GetType() != EYsonItemType::EndList) {
+ applier.OnListItem(element, cursor, args...);
+ }
+ applier.OnListEnd(args...);
+
+ // We are on list end token since we exited while loop
+ cursor->Next();
+ };
+ }
+
+ template <typename TApplier, typename TElement>
+ static TScanner CreateTupleScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ std::vector<TElement> elements)
+ {
+ using namespace NYson;
+ return [
+ elements=std::move(elements),
+ descriptor=std::move(descriptor),
+ applier=std::move(applier)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ applier.OnTupleBegin(args...);
+ for (const auto& scanner : elements) {
+ applier.OnTupleItem(scanner, cursor, args...);
+ }
+ applier.OnTupleEnd(args...);
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+ }
+
+ template <typename TApplier, typename TField>
+ static TScanner CreateStructScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ std::vector<TField> fields)
+ {
+ using namespace NYson;
+ return [
+ fields=std::move(fields),
+ descriptor=std::move(descriptor),
+ applier=std::move(applier)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ applier.OnStructBegin(args...);
+ for (auto it = fields.begin(), end = fields.end(); it != end; ++it) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::EndList) {
+ TInfiniteEntity infiniteEntity;
+ auto entityCursor = infiniteEntity.GetCursor();
+ do {
+ applier.OnStructField(*it, entityCursor, args...);
+ ++it;
+ } while (it != end);
+ break;
+ }
+ applier.OnStructField(*it, cursor, args...);
+ }
+ applier.OnStructEnd(args...);
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+ }
+
+ template <typename TApplier, typename TAlternatives>
+ static TScanner CreateVariantScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ std::vector<TAlternatives> alternatives)
+ {
+ using namespace NYson;
+ return [
+ descriptor=std::move(descriptor),
+ applier=std::move(applier),
+ alternatives=std::move(alternatives)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::Int64Value);
+ auto tag = cursor->GetCurrent().UncheckedAsInt64();
+ if (tag < 0) {
+ THROW_ERROR_EXCEPTION(
+ "Error while parsing %Qv: variant tag (%v) is negative",
+ descriptor.GetDescription(),
+ tag);
+ }
+ if (tag >= std::ssize(alternatives)) {
+ THROW_ERROR_EXCEPTION(
+ "Error while parsing %Qv: variant tag (%v) exceeds variant alternative count (%v)",
+ descriptor.GetDescription(),
+ tag,
+ alternatives.size());
+ }
+ cursor->Next();
+
+ applier.OnVariantAlternative(alternatives[tag], cursor, args...);
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+ }
+
+ template <typename TApplier, typename TElement>
+ static TScanner CreateDictScanner(
+ NTableClient::TComplexTypeFieldDescriptor descriptor,
+ TApplier applier,
+ TElement key,
+ TElement value)
+ {
+ using namespace NYson;
+ return [
+ descriptor=std::move(descriptor),
+ applier=std::move(applier),
+ key=std::move(key),
+ value=std::move(value)
+ ] (TYsonPullParserCursor* cursor, TArgs... args) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ applier.OnDictBegin(args...);
+ while ((*cursor)->GetType() != EYsonItemType::EndList) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+
+ applier.OnKey(key, cursor, args...);
+
+ applier.OnValue(value, cursor, args...);
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ }
+ applier.OnDictEnd(args...);
+
+ // Skip end list token
+ cursor->Next();
+ };
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT:NComplexTypes
diff --git a/yt/yt/client/complex_types/time_text.cpp b/yt/yt/client/complex_types/time_text.cpp
new file mode 100644
index 0000000000..6bff1d9571
--- /dev/null
+++ b/yt/yt/client/complex_types/time_text.cpp
@@ -0,0 +1,62 @@
+#include "time_text.h"
+
+namespace NYT::NComplexTypes {
+
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void ValidateLength(TStringBuf data, ESimpleLogicalValueType valueType)
+{
+ size_t minLength;
+ size_t maxLength;
+ switch (valueType) {
+ case ESimpleLogicalValueType::Date:
+ minLength = maxLength = DateLength;
+ break;
+ case ESimpleLogicalValueType::Datetime:
+ minLength = maxLength = DateTimeLength;
+ break;
+ case ESimpleLogicalValueType::Timestamp:
+ minLength = DateTimeLength;
+ maxLength = TimestampLength;
+ break;
+ default:
+ YT_ABORT();
+ }
+ if (minLength > data.size() || data.size() > maxLength) {
+ THROW_ERROR_EXCEPTION(
+ "Invalid date string length. Expected: [%v..%v], got: %v",
+ minLength,
+ maxLength,
+ data.size());
+ }
+}
+
+ui64 BinaryTimeFromText(TStringBuf data, ESimpleLogicalValueType valueType)
+{
+ // ISO 8601 allows omitting dates' "suffix". E.g 2021-09, 2021-09-30, 2021-09-30T23:59 are valid ISO 8601 dates.
+ // So we have to validate date's length to make sure we respect ValueType_.
+ ValidateLength(data, valueType);
+ TInstant instant;
+ try {
+ instant = TInstant::ParseIso8601(data);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Could not parse date %Qv", data)
+ << ex;
+ }
+ switch (valueType) {
+ case ESimpleLogicalValueType::Date:
+ return instant.Days();
+ case ESimpleLogicalValueType::Datetime:
+ return instant.Seconds();
+ case ESimpleLogicalValueType::Timestamp:
+ return instant.MicroSeconds();
+ default:
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/time_text.h b/yt/yt/client/complex_types/time_text.h
new file mode 100644
index 0000000000..64073963a8
--- /dev/null
+++ b/yt/yt/client/complex_types/time_text.h
@@ -0,0 +1,15 @@
+#include <yt/yt/client/table_client/row_base.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr static auto DateLength = std::char_traits<char>::length("YYYY-MM-DD");
+constexpr static auto DateTimeLength = std::char_traits<char>::length("YYYY-MM-DDThh:mm:ssZ");
+constexpr static auto TimestampLength = std::char_traits<char>::length("YYYY-MM-DDThh:mm:ss.123456Z");
+
+ui64 BinaryTimeFromText(TStringBuf data, NTableClient::ESimpleLogicalValueType valueType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/uuid_text.cpp b/yt/yt/client/complex_types/uuid_text.cpp
new file mode 100644
index 0000000000..c3e5a0298f
--- /dev/null
+++ b/yt/yt/client/complex_types/uuid_text.cpp
@@ -0,0 +1,125 @@
+#include "uuid_text.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/system/byteorder.h>
+#include <util/system/unaligned_mem.h>
+
+#include <util/string/hex.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void CheckUuidSize(TStringBuf bytes)
+{
+ if (bytes.size() != UuidBinarySize) {
+ THROW_ERROR_EXCEPTION(
+ "Invalid binary UUID length: got %v, expected: %v",
+ bytes.size(),
+ UuidBinarySize);
+ }
+}
+
+void TextYqlUuidToBytes(TStringBuf uuid, char* ptr)
+{
+ // Opposite to TextYqlUuidFromBytes(). Check that function first.
+ if (uuid.size() != UuidYqlTextSize) {
+ THROW_ERROR_EXCEPTION(
+ "Invalid text yql UUID length: got %v, expected: %v",
+ uuid.size(),
+ UuidYqlTextSize);
+ }
+ auto parseByte = [] (const char* ptr) {
+ try {
+ return Char2Digit(*ptr) * 16 + Char2Digit(*(ptr + 1));
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Could not parse hex byte") << ex;
+ }
+ };
+ auto verifyDash = [] (const char* ptr) {
+ if (*ptr != '-') {
+ THROW_ERROR_EXCEPTION("Unexpected character: %Qv, expected: %Qv", *ptr, '-');
+ }
+ };
+
+ const char* input = uuid.Data();
+ for (int i = 3; i >= 0; --i) {
+ *ptr++ = parseByte(input + 2 * i);
+ }
+ verifyDash(input + 8);
+ for (int i = 5; i >= 4; --i) {
+ *ptr++ = parseByte(input + 1 + 2 * i);
+ }
+ verifyDash(input + 13);
+ for (int i = 7; i >= 6; --i) {
+ *ptr++ = parseByte(input + 2 + 2 * i);
+ }
+ verifyDash(input + 18);
+ for (int i = 8; i <= 9; ++i) {
+ *ptr++ = parseByte(input + 3 + 2 * i);
+ }
+ verifyDash(input + 23);
+ for (int i = 10; i < 16; ++i) {
+ *ptr++ = parseByte(input + 4 + 2 * i);
+ }
+}
+
+char* TextYqlUuidFromBytes(TStringBuf bytes, char* ptr)
+{
+ // Inspired by https://a.yandex-team.ru/arc/trunk/arcadia/yql/minikql/mkql_type_ops.cpp?rev=6853830#L327
+ CheckUuidSize(bytes);
+ static const char* HexDigits = "0123456789abcdef";
+
+ auto writeByte = [&ptr] (ui8 x) {
+ *ptr++ = HexDigits[x >> 4];
+ *ptr++ = HexDigits[x & 0b1111];
+ };
+
+ for (int i = 3; i >= 0; --i) {
+ writeByte(bytes[i]);
+ }
+ *ptr++ = '-';
+ for (int i = 5; i >= 4; --i) {
+ writeByte(bytes[i]);
+ }
+ *ptr++ = '-';
+ for (int i = 7; i >= 6; --i) {
+ writeByte(bytes[i]);
+ }
+ *ptr++ = '-';
+ for (int i = 8; i <= 9; ++i) {
+ writeByte(bytes[i]);
+ }
+ *ptr++ = '-';
+ for (int i = 10; i < 16; ++i) {
+ writeByte(bytes[i]);
+ }
+ return ptr;
+}
+
+void GuidToBytes(TGuid guid, char* ptr)
+{
+ auto low = LittleToHost(guid.Parts64[0]);
+ auto high = LittleToHost(guid.Parts64[1]);
+
+ WriteUnaligned<ui64>(ptr, HostToInet(high));
+ WriteUnaligned<ui64>(ptr + sizeof(ui64), HostToInet(low));
+}
+
+TGuid GuidFromBytes(TStringBuf bytes)
+{
+ // Doing the same way as
+ // https://a.yandex-team.ru/arc/trunk/arcadia/yt/yt/client/formats/skiff_yson_converter-inl.h?rev=r8579019#L124
+
+ CheckUuidSize(bytes);
+ const char* ptr = bytes.Data();
+ auto low = InetToHost(ReadUnaligned<ui64>(ptr + sizeof(ui64)));
+ auto high = InetToHost(ReadUnaligned<ui64>(ptr));
+
+ return TGuid(HostToLittle(low), HostToLittle(high));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/uuid_text.h b/yt/yt/client/complex_types/uuid_text.h
new file mode 100644
index 0000000000..55006737f2
--- /dev/null
+++ b/yt/yt/client/complex_types/uuid_text.h
@@ -0,0 +1,19 @@
+#include <yt/yt/core/misc/guid.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr size_t UuidYqlTextSize = 36;
+constexpr size_t UuidYtTextSize = 35;
+constexpr size_t UuidBinarySize = 16;
+
+void TextYqlUuidToBytes(TStringBuf uuid, char* ptr);
+char* TextYqlUuidFromBytes(TStringBuf bytes, char* ptr);
+
+void GuidToBytes(TGuid guid, char* ptr);
+TGuid GuidFromBytes(TStringBuf bytes);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/yson_format_conversion.cpp b/yt/yt/client/complex_types/yson_format_conversion.cpp
new file mode 100644
index 0000000000..b4132ead42
--- /dev/null
+++ b/yt/yt/client/complex_types/yson_format_conversion.cpp
@@ -0,0 +1,1267 @@
+#include "yson_format_conversion.h"
+
+#include "uuid_text.h"
+#include "time_text.h"
+#include "scanner_factory.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/unversioned_value.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/misc/collection_helpers.h>
+#include <yt/yt/core/misc/blob_output.h>
+#include <yt/yt/core/misc/guid.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/stream/buffer.h>
+#include <util/stream/mem.h>
+
+#include <variant>
+
+namespace NYT::NComplexTypes {
+
+using namespace NTableClient;
+using namespace NYson;
+using namespace NFormats;
+using namespace NDecimal;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EConverterType,
+ (ToServer)
+ (ToClient)
+);
+
+struct TYsonConverterCreatorConfig
+{
+ TYsonConverterConfig Config;
+ EConverterType ConverterType;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsTimeType(const TLogicalTypePtr& type)
+{
+ if (type->GetMetatype() != ELogicalMetatype::Simple) {
+ return false;
+ }
+ switch (type->AsSimpleTypeRef().GetElement()) {
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsUuidType(const TLogicalTypePtr& type)
+{
+ return type->GetMetatype() == ELogicalMetatype::Simple &&
+ type->AsSimpleTypeRef().GetElement() == ESimpleLogicalValueType::Uuid;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIsTransformForTypeNeededCache
+{
+public:
+ TIsTransformForTypeNeededCache(
+ const TLogicalTypePtr& logicalType,
+ const TYsonConverterCreatorConfig& config)
+ {
+ CheckAndCacheTriviality(logicalType, config);
+ }
+
+ bool IsTrivial(const TLogicalTypePtr& logicalType) const
+ {
+ return GetOrCrash(Cache_, logicalType.Get());
+ }
+
+private:
+ bool CheckAndCacheTriviality(const TLogicalTypePtr& logicalType, const TYsonConverterCreatorConfig& config)
+ {
+ auto& result = Cache_[logicalType.Get()];
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ if (IsTimeType(logicalType)) {
+ return result = (config.Config.TimeMode == ETimeMode::Binary);
+ } else if (IsUuidType(logicalType)) {
+ return result = (config.Config.UuidMode == EUuidMode::Binary);
+ } else {
+ return result = true;
+ }
+
+ case ELogicalMetatype::Decimal:
+ return result = (config.Config.DecimalMode == EDecimalMode::Binary);
+
+ case ELogicalMetatype::Optional:
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Tagged:
+ return result = CheckAndCacheTriviality(logicalType->GetElement(), config);
+
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple: {
+ result = true;
+ for (const auto& element : logicalType->GetElements()) {
+ if (!CheckAndCacheTriviality(element, config)) {
+ result = false;
+ // no break here, we want to cache all elements
+ }
+ }
+ return result;
+ }
+
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct: {
+ result = (config.Config.ComplexTypeMode == EComplexTypeMode::Positional);
+ for (const auto& field : logicalType->GetFields()) {
+ if (!CheckAndCacheTriviality(field.Type, config)) {
+ result = false;
+ // no break here, we want to cache all elements
+ }
+ }
+ return result;
+ }
+
+ case ELogicalMetatype::Dict:
+ result = (config.Config.StringKeyedDictMode == EDictMode::Positional);
+ if (!CheckAndCacheTriviality(logicalType->AsDictTypeRef().GetKey(), config)) {
+ result = false;
+ }
+ if (!CheckAndCacheTriviality(logicalType->AsDictTypeRef().GetValue(), config)) {
+ result = false;
+ }
+ return result;
+ }
+ YT_ABORT();
+ }
+
+private:
+ THashMap<void*, bool> Cache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IdRecoder(TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+{
+ cursor->TransferComplexValue(consumer);
+}
+
+using TYsonCursorConverter = std::function<void(TYsonPullParserCursor*, IYsonConsumer*)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckValueType(EValueType actualType, EValueType expectedType)
+{
+ if (actualType != expectedType) {
+ THROW_ERROR_EXCEPTION(
+ "Unexpected value type: %Qv, expected: %Qv",
+ actualType,
+ expectedType);
+ }
+}
+
+void CheckYsonItemType(EYsonItemType actualType, EYsonItemType expectedType)
+{
+ if (actualType != expectedType) {
+ THROW_ERROR_EXCEPTION(
+ "Unexpected yson token: %Qv, expected: %Qv",
+ actualType,
+ expectedType);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecimalRawServerToClientConverter
+{
+public:
+ TDecimalRawServerToClientConverter(int precision, int scale)
+ : Precision_(precision)
+ , Scale_(scale)
+ { }
+
+ void operator () (TUnversionedValue value, IYsonConsumer* consumer)
+ {
+ CheckValueType(value.Type, EValueType::String);
+ auto data = value.AsStringBuf();
+ auto converted = TDecimal::BinaryToText(data, Precision_, Scale_, Buffer_.data(), Buffer_.size());
+ consumer->OnStringScalar(converted);
+ }
+
+private:
+ const int Precision_;
+ const int Scale_;
+ std::array<char, TDecimal::MaxTextSize> Buffer_;
+};
+
+class TDecimalRawClientToServerConverter
+{
+public:
+ TDecimalRawClientToServerConverter(int precision, int scale)
+ : Precision_(precision)
+ , Scale_(scale)
+ { }
+
+ // This operator should be called only after previous result is consumed.
+ // So use-after-free won't occur.
+ TUnversionedValue operator () (TUnversionedValue value)
+ {
+ CheckValueType(value.Type, EValueType::String);
+ auto data = value.AsStringBuf();
+ auto converted = TDecimal::TextToBinary(data, Precision_, Scale_, Buffer_.data(), Buffer_.size());
+ return MakeUnversionedStringValue(converted);
+ }
+
+private:
+ const int Precision_;
+ const int Scale_;
+ std::array<char, TDecimal::MaxBinarySize> Buffer_;
+};
+
+template <EConverterType ConverterType>
+class TDecimalCursorConverter
+{
+public:
+ TDecimalCursorConverter(int precision, int scale)
+ : Precision_(precision)
+ , Scale_(scale)
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ CheckYsonItemType((*cursor)->GetType(), EYsonItemType::StringValue);
+ auto data = (*cursor)->UncheckedAsString();
+ TStringBuf converted;
+ if constexpr (ConverterType == EConverterType::ToClient) {
+ converted = TDecimal::BinaryToText(data, Precision_, Scale_, Buffer_.data(), Buffer_.size());
+ } else {
+ static_assert(ConverterType == EConverterType::ToServer);
+ converted = TDecimal::TextToBinary(data, Precision_, Scale_, Buffer_.data(), Buffer_.size());
+ }
+ consumer->OnStringScalar(converted);
+ cursor->Next();
+ }
+
+private:
+ const int Precision_;
+ const int Scale_;
+
+ static_assert(ConverterType == EConverterType::ToClient || ConverterType == EConverterType::ToServer);
+ constexpr static auto Size_ = (ConverterType == EConverterType::ToClient)
+ ? TDecimal::MaxTextSize
+ : TDecimal::MaxBinarySize;
+ std::array<char, Size_> Buffer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeServerToClientConverter
+{
+public:
+ TTimeServerToClientConverter(ESimpleLogicalValueType valueType)
+ : ConvertedWriter_(Converted_)
+ , ValueType_(valueType)
+ {
+ Converted_.reserve(TimestampLength);
+ }
+
+ TTimeServerToClientConverter(const TTimeServerToClientConverter& other)
+ : ConvertedWriter_(Converted_)
+ , ValueType_(other.ValueType_)
+ {
+ Converted_.reserve(TimestampLength);
+ }
+
+ void operator () (TUnversionedValue value, IYsonConsumer* consumer)
+ {
+ CheckValueType(value.Type, EValueType::Uint64);
+ Convert(value.Data.Uint64, consumer);
+ }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ CheckYsonItemType((*cursor)->GetType(), EYsonItemType::Uint64Value);
+ Convert((*cursor)->UncheckedAsUint64(), consumer);
+ cursor->Next();
+ }
+
+private:
+ void Convert(ui64 data, IYsonConsumer* consumer)
+ {
+ Converted_.clear();
+ switch (ValueType_) {
+ case ESimpleLogicalValueType::Date:
+ ConvertedWriter_ << TInstant::Days(data);
+ Converted_.resize(DateLength);
+ break;
+ case ESimpleLogicalValueType::Datetime:
+ ConvertedWriter_ << TInstant::Seconds(data);
+ Converted_.resize(DateTimeLength);
+ Converted_.back() = 'Z';
+ break;
+ case ESimpleLogicalValueType::Timestamp:
+ ConvertedWriter_ << TInstant::MicroSeconds(data);
+ break;
+ default:
+ YT_ABORT();
+ }
+ consumer->OnStringScalar(Converted_);
+ }
+
+private:
+ TString Converted_;
+ TStringOutput ConvertedWriter_;
+ const ESimpleLogicalValueType ValueType_;
+};
+
+class TTimeClientToServerConverter
+{
+public:
+ TTimeClientToServerConverter(ESimpleLogicalValueType valueType)
+ : ValueType_(valueType)
+ { }
+
+ TUnversionedValue operator () (TUnversionedValue value)
+ {
+ CheckValueType(value.Type, EValueType::String);
+ auto data = value.AsStringBuf();
+ return MakeUnversionedUint64Value(BinaryTimeFromText(data, ValueType_));
+ }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ CheckYsonItemType((*cursor)->GetType(), EYsonItemType::StringValue);
+ auto data = (*cursor)->UncheckedAsString();
+ consumer->OnUint64Scalar(BinaryTimeFromText(data, ValueType_));
+ cursor->Next();
+ }
+
+private:
+ const ESimpleLogicalValueType ValueType_;
+};
+
+std::variant<TYsonServerToClientConverter, TYsonClientToServerConverter> CreateDecimalRawConverter(
+ const TLogicalTypePtr& type,
+ const TYsonConverterCreatorConfig& config)
+{
+ YT_VERIFY(config.Config.DecimalMode != EDecimalMode::Binary);
+
+ const auto& decimalTypeRef = type->AsDecimalTypeRef();
+ auto precision = decimalTypeRef.GetPrecision();
+ auto scale = decimalTypeRef.GetScale();
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TDecimalRawServerToClientConverter(precision, scale);
+ case EConverterType::ToServer:
+ return TDecimalRawClientToServerConverter(precision, scale);
+ }
+ YT_ABORT();
+}
+
+TYsonCursorConverter CreateDecimalConverter(
+ const TYsonConverterCreatorConfig& config,
+ int precision,
+ int scale)
+{
+ YT_VERIFY(config.Config.DecimalMode != EDecimalMode::Binary);
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TDecimalCursorConverter<EConverterType::ToClient>(precision, scale);
+ case EConverterType::ToServer:
+ return TDecimalCursorConverter<EConverterType::ToServer>(precision, scale);
+ }
+ YT_ABORT();
+}
+
+std::variant<TYsonServerToClientConverter, TYsonClientToServerConverter> CreateTimeRawConverter(
+ const TLogicalTypePtr& type,
+ const TYsonConverterCreatorConfig& config)
+{
+ YT_VERIFY(config.Config.TimeMode != ETimeMode::Binary);
+ const auto& simpleType = type->AsSimpleTypeRef().GetElement();
+
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TTimeServerToClientConverter(simpleType);
+ case EConverterType::ToServer:
+ return TTimeClientToServerConverter(simpleType);
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUuidServerToClientConverter
+{
+public:
+ TUuidServerToClientConverter(EUuidMode uuidMode)
+ : UuidMode_(uuidMode)
+ { }
+
+ void operator () (TUnversionedValue value, IYsonConsumer* consumer)
+ {
+ CheckValueType(value.Type, EValueType::String);
+ Convert(value.AsStringBuf(), consumer);
+ }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ CheckYsonItemType((*cursor)->GetType(), EYsonItemType::StringValue);
+ Convert((*cursor)->UncheckedAsString(), consumer);
+ cursor->Next();
+ }
+
+private:
+ void Convert(TStringBuf data, IYsonConsumer* consumer)
+ {
+ char* end;
+ switch (UuidMode_) {
+ case EUuidMode::TextYql:
+ end = TextYqlUuidFromBytes(data, Buffer_.data());
+ break;
+ case EUuidMode::TextYt:
+ end = WriteGuidToBuffer(Buffer_.data(), GuidFromBytes(data));
+ break;
+ default:
+ // binary uuid should not be converted
+ YT_ABORT();
+ }
+ consumer->OnStringScalar(TStringBuf(Buffer_.data(), end));
+ }
+
+private:
+ const EUuidMode UuidMode_;
+ std::array<char, std::max(UuidYtTextSize, UuidYqlTextSize)> Buffer_;
+};
+
+class TUuidClientToServerConverter
+{
+public:
+ TUuidClientToServerConverter(EUuidMode uuidMode)
+ : UuidMode_(uuidMode)
+ { }
+
+ TUnversionedValue operator () (TUnversionedValue value)
+ {
+ CheckValueType(value.Type, EValueType::String);
+ auto data = value.AsStringBuf();
+ return MakeUnversionedStringValue(Convert(data));
+ }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ CheckYsonItemType((*cursor)->GetType(), EYsonItemType::StringValue);
+ auto data = (*cursor)->UncheckedAsString();
+ consumer->OnStringScalar(Convert(data));
+ cursor->Next();
+ }
+
+private:
+ TStringBuf Convert(TStringBuf data)
+ {
+ switch (UuidMode_) {
+ case EUuidMode::TextYql:
+ TextYqlUuidToBytes(data, Buffer_.data());
+ break;
+ case EUuidMode::TextYt:
+ GuidToBytes(TGuid::FromString(data), Buffer_.data());
+ break;
+ default:
+ // binary uuid should not be converted
+ YT_ABORT();
+ }
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+
+private:
+ const EUuidMode UuidMode_;
+ std::array<char, UuidBinarySize> Buffer_;
+};
+
+std::variant<TYsonServerToClientConverter, TYsonClientToServerConverter> CreateUuidRawConverter(
+ const TYsonConverterCreatorConfig& config)
+{
+ YT_VERIFY(config.Config.UuidMode != EUuidMode::Binary);
+
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TUuidServerToClientConverter(config.Config.UuidMode);
+ case EConverterType::ToServer:
+ return TUuidClientToServerConverter(config.Config.UuidMode);
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TStructFieldInfo
+{
+ TYsonCursorConverter Converter;
+ TString FieldName;
+ bool IsNullable = false;
+};
+
+
+template <bool IsElementNullable>
+class TOptionalHandler
+{
+public:
+ void OnEmptyOptional(IYsonConsumer* consumer) const
+ {
+ consumer->OnEntity();
+ }
+
+ void OnFilledOptional(const TYsonCursorConverter& recoder, TYsonPullParserCursor* cursor, IYsonConsumer* consumer) const
+ {
+ if constexpr (IsElementNullable) {
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ recoder(cursor, consumer);
+ consumer->OnEndList();
+ } else {
+ recoder(cursor, consumer);
+ }
+ }
+};
+
+class TListHandler
+{
+public:
+ Y_FORCE_INLINE void OnListBegin(IYsonConsumer* consumer) const
+ {
+ consumer->OnBeginList();
+ }
+
+ Y_FORCE_INLINE void OnListItem(
+ const TYsonCursorConverter& recoder,
+ TYsonPullParserCursor* cursor,
+ IYsonConsumer* consumer) const
+ {
+ consumer->OnListItem();
+ recoder(cursor, consumer);
+ }
+
+ Y_FORCE_INLINE void OnListEnd(IYsonConsumer* consumer) const
+ {
+ consumer->OnEndList();
+ }
+};
+
+class TTupleApplier
+{
+public:
+ Y_FORCE_INLINE void OnTupleBegin(IYsonConsumer* consumer) const
+ {
+ consumer->OnBeginList();
+ }
+
+ Y_FORCE_INLINE void
+ OnTupleItem(const TYsonCursorConverter& recoder, TYsonPullParserCursor* cursor, IYsonConsumer* consumer) const
+ {
+ consumer->OnListItem();
+ recoder(cursor, consumer);
+ }
+
+ Y_FORCE_INLINE void OnTupleEnd(IYsonConsumer* consumer) const
+ {
+ consumer->OnEndList();
+ }
+};
+
+template <bool SkipNullValues>
+class TStructApplier
+{
+public:
+ Y_FORCE_INLINE void OnStructBegin(IYsonConsumer* consumer) const
+ {
+ consumer->OnBeginMap();
+ }
+
+ Y_FORCE_INLINE void OnStructEnd(IYsonConsumer* consumer) const
+ {
+ consumer->OnEndMap();
+ }
+
+ Y_FORCE_INLINE void OnStructField(
+ const TStructFieldInfo& field,
+ TYsonPullParserCursor* cursor,
+ IYsonConsumer* consumer) const
+ {
+ if constexpr (SkipNullValues) {
+ if (field.IsNullable && (*cursor)->GetType() == EYsonItemType::EntityValue) {
+ cursor->Next();
+ return;
+ }
+ }
+ consumer->OnKeyedItem(field.FieldName);
+ field.Converter(cursor, consumer);
+ }
+};
+
+class TVariantTupleApplier
+{
+public:
+ Y_FORCE_INLINE void OnVariantAlternative(
+ const std::pair<int, TYsonCursorConverter>& alternative,
+ TYsonPullParserCursor* cursor,
+ IYsonConsumer* consumer) const
+ {
+ consumer->OnBeginList();
+
+ consumer->OnListItem();
+ consumer->OnInt64Scalar(alternative.first);
+
+ consumer->OnListItem();
+ alternative.second(cursor, consumer);
+
+ consumer->OnEndList();
+ }
+};
+
+class TVariantStructApplier
+{
+public:
+ Y_FORCE_INLINE void OnVariantAlternative(
+ const std::pair<TString, TYsonCursorConverter>& alternative,
+ TYsonPullParserCursor* cursor,
+ IYsonConsumer* consumer) const
+ {
+ consumer->OnBeginList();
+ consumer->OnListItem();
+
+ consumer->OnStringScalar(alternative.first);
+
+ consumer->OnListItem();
+ alternative.second(cursor, consumer);
+
+ consumer->OnEndList();
+ }
+};
+
+template <EDictMode mode>
+class TDictApplier
+{
+public:
+ Y_FORCE_INLINE void OnDictBegin(IYsonConsumer* consumer) const
+ {
+ if constexpr (mode == EDictMode::Positional) {
+ consumer->OnBeginList();
+ } else if constexpr (mode == EDictMode::Named) {
+ consumer->OnBeginMap();
+ } else {
+ // Not compilable.
+ static_assert(mode == EDictMode::Positional);
+ }
+ }
+
+ Y_FORCE_INLINE void
+ OnKey(const TYsonCursorConverter& keyRecoder, TYsonPullParserCursor* cursor, IYsonConsumer* consumer) const
+ {
+ if constexpr (mode == EDictMode::Positional) {
+ consumer->OnListItem();
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ keyRecoder(cursor, consumer);
+ } else if constexpr (mode == EDictMode::Named) {
+ const auto& item = *cursor;
+
+ // Named representation of dict supported only for string keys.
+ YT_ASSERT(item->GetType() == EYsonItemType::StringValue);
+
+ consumer->OnKeyedItem(item->UncheckedAsString());
+ cursor->Next();
+ } else {
+ // Not compilable.
+ static_assert(mode == EDictMode::Positional);
+ }
+ }
+
+ Y_FORCE_INLINE void
+ OnValue(const TYsonCursorConverter& valueRecoder, TYsonPullParserCursor* cursor, IYsonConsumer* consumer) const
+ {
+ if constexpr (mode == EDictMode::Positional) {
+ consumer->OnListItem();
+ valueRecoder(cursor, consumer);
+ consumer->OnEndList();
+ } if constexpr (mode == EDictMode::Named) {
+ valueRecoder(cursor, consumer);
+ } else {
+ // Not compilable.
+ static_assert(mode == EDictMode::Positional);
+ }
+ }
+
+ Y_FORCE_INLINE void OnDictEnd(IYsonConsumer* consumer) const
+ {
+ if constexpr (mode == EDictMode::Positional) {
+ consumer->OnEndList();
+ } if constexpr (mode == EDictMode::Named) {
+ consumer->OnEndMap();
+ } else {
+ // Not compilable.
+ static_assert(mode == EDictMode::Positional);
+ }
+ }
+};
+
+class TNamedToPositionalDictConverter
+{
+public:
+ TNamedToPositionalDictConverter(TComplexTypeFieldDescriptor descriptor, TYsonCursorConverter valueConverter)
+ : Descriptor_(std::move(descriptor))
+ , ValueConverter_(std::move(valueConverter))
+ {
+ }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ EnsureYsonToken(Descriptor_, *cursor, EYsonItemType::BeginMap);
+ cursor->Next();
+
+ consumer->OnBeginList();
+ while ((*cursor)->GetType() != EYsonItemType::EndMap) {
+ EnsureYsonToken(Descriptor_, *cursor, EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+
+ consumer->OnListItem();
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ consumer->OnStringScalar(key);
+
+ cursor->Next();
+
+ consumer->OnListItem();
+ ValueConverter_(cursor, consumer);
+ consumer->OnEndList();
+
+ }
+
+ // Skip map end token.
+ cursor->Next();
+ consumer->OnEndList();
+ }
+
+private:
+ TComplexTypeFieldDescriptor Descriptor_;
+ TYsonCursorConverter ValueConverter_;
+};
+
+class TNamedToPositionalStructConverter
+{
+public:
+ TNamedToPositionalStructConverter(TComplexTypeFieldDescriptor descriptor, std::vector<TStructFieldInfo> fields)
+ : BufferOutput_(Buffer_)
+ , YsonWriter_(&BufferOutput_, EYsonType::ListFragment)
+ , Descriptor_(std::move(descriptor))
+ {
+ PositionTable_.reserve(fields.size());
+ for (int i = 0; i < std::ssize(fields); ++i) {
+ auto& field = fields[i];
+ FieldMap_.emplace(
+ field.FieldName,
+ TFieldMapEntry{std::move(field.Converter), i});
+
+ PositionTable_.emplace_back();
+ PositionTable_.back().IsNullable = field.IsNullable;
+ PositionTable_.back().FieldName = std::move(field.FieldName);
+ }
+ }
+
+ // NB. to wrap this object into std::function we must be able to copy it.
+ TNamedToPositionalStructConverter(const TNamedToPositionalStructConverter& other)
+ : FieldMap_(other.FieldMap_)
+ , PositionTable_(other.PositionTable_)
+ , Buffer_(other.Buffer_)
+ , BufferOutput_(Buffer_)
+ , YsonWriter_(&BufferOutput_, EYsonType::ListFragment)
+ , Descriptor_(other.Descriptor_)
+ , CurrentGeneration_(other.CurrentGeneration_)
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, IYsonConsumer* consumer)
+ {
+ IncrementGeneration();
+
+ EnsureYsonToken(Descriptor_, *cursor, EYsonItemType::BeginMap);
+ cursor->Next();
+ Buffer_.Clear();
+
+ YT_ASSERT(YsonWriter_.GetDepth() == 0);
+
+ while ((*cursor)->GetType() != EYsonItemType::EndMap) {
+ EnsureYsonToken(Descriptor_, *cursor, EYsonItemType::StringValue);
+ auto fieldName = (*cursor)->UncheckedAsString();
+ auto it = FieldMap_.find(fieldName);
+ if (it == FieldMap_.end()) {
+ THROW_ERROR_EXCEPTION(
+ "Unknown field %Qv while parsing %Qv",
+ fieldName,
+ Descriptor_.GetDescription());
+ }
+ cursor->Next();
+
+ auto& positionEntry = PositionTable_[it->second.Position];
+ if (positionEntry.Generation == CurrentGeneration_) {
+ THROW_ERROR_EXCEPTION(
+ "Multiple occurrences of field %Qv while parsing %Qv",
+ it->first, // NB. it's not safe to use fieldName since we moved cursor
+ Descriptor_.GetDescription());
+ }
+
+ auto offset = Buffer_.Size();
+ it->second.Converter(cursor, &YsonWriter_);
+ YsonWriter_.Flush();
+
+ positionEntry.Offset = offset;
+ positionEntry.Size = Buffer_.size() - offset;
+ positionEntry.Generation = CurrentGeneration_;
+ }
+
+ // Skip map end token.
+ cursor->Next();
+
+ consumer->OnBeginList();
+ for (const auto& positionEntry : PositionTable_) {
+ if (positionEntry.Generation == CurrentGeneration_) {
+ auto yson = TStringBuf(Buffer_.Data() + positionEntry.Offset, positionEntry.Size);
+ consumer->OnRaw(yson, EYsonType::ListFragment);
+ } else if (positionEntry.IsNullable) {
+ consumer->OnRaw("#;", EYsonType::ListFragment);
+ } else {
+ THROW_ERROR_EXCEPTION("Field %Qv is missing while parsing %Qv",
+ positionEntry.FieldName,
+ Descriptor_.GetDescription());
+ }
+ }
+ consumer->OnEndList();
+ }
+private:
+ void IncrementGeneration()
+ {
+ if (++CurrentGeneration_ == 0) {
+ for (auto& entry : PositionTable_) {
+ entry.Generation = 0;
+ }
+ CurrentGeneration_ = 1;
+ }
+ }
+
+private:
+ struct TFieldMapEntry
+ {
+ TYsonCursorConverter Converter;
+ int Position = 0;
+ };
+
+ struct TPositionTableEntry {
+ size_t Offset = 0;
+ size_t Size = 0;
+ ui16 Generation = 0;
+ bool IsNullable = false;
+ TString FieldName;
+ };
+
+ THashMap<TString, TFieldMapEntry> FieldMap_;
+ std::vector<TPositionTableEntry> PositionTable_;
+ TBuffer Buffer_;
+ TBufferOutput BufferOutput_;
+ TBufferedBinaryYsonWriter YsonWriter_;
+ TComplexTypeFieldDescriptor Descriptor_;
+ ui16 CurrentGeneration_ = 0;
+};
+
+class TClientToServerComplexValueConverterWrapper
+{
+public:
+ TClientToServerComplexValueConverterWrapper(TYsonCursorConverter converter)
+ : Converter_(std::move(converter))
+ , ConvertedWriter_(&ConvertedBuffer_)
+ { }
+
+ TClientToServerComplexValueConverterWrapper(const TClientToServerComplexValueConverterWrapper& other)
+ : Converter_(other.Converter_)
+ , ConvertedWriter_(&ConvertedBuffer_)
+ { }
+
+ // This operator should be called only after previous result is consumed.
+ // So use-after-free won't occur.
+ TUnversionedValue operator () (TUnversionedValue value)
+ {
+ TMemoryInput in(value.Data.String, value.Length);
+ TYsonPullParser parser(&in, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+
+ ConvertedBuffer_.Clear();
+ Converter_(&cursor, &ConvertedWriter_);
+ ConvertedWriter_.Flush();
+ return MakeUnversionedCompositeValue(ConvertedBuffer_.Blob().ToStringBuf());
+ }
+
+private:
+ TYsonCursorConverter Converter_;
+ TBlobOutput ConvertedBuffer_;
+ TBufferedBinaryYsonWriter ConvertedWriter_;
+};
+
+TYsonCursorConverter CreateNamedToPositionalVariantStructConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ std::vector<std::pair<TString, TYsonCursorConverter>> fieldConverters)
+{
+ THashMap<TString, std::pair<int, TYsonCursorConverter>> typeMap;
+ int fieldIndex = 0;
+ for (auto& [fieldName, converter] : fieldConverters) {
+ typeMap.emplace(std::move(fieldName), std::pair(fieldIndex, std::move(converter)));
+ ++fieldIndex;
+ }
+
+ return [
+ descriptor=descriptor,
+ typeMap=std::move(typeMap)
+ ] (TYsonPullParserCursor* cursor, IYsonConsumer* ysonConsumer) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::StringValue);
+ auto fieldName = (*cursor)->UncheckedAsString();
+ auto it = typeMap.find(fieldName);
+ if (it == typeMap.end()) {
+ THROW_ERROR_EXCEPTION(
+ "Unknown variant field %Qv while parsing %Qv",
+ fieldName,
+ descriptor.GetDescription());
+ }
+ cursor->Next();
+
+ const auto& [variantIndex, converter] = it->second;
+ ysonConsumer->OnBeginList();
+
+ ysonConsumer->OnListItem();
+ ysonConsumer->OnInt64Scalar(variantIndex);
+
+ ysonConsumer->OnListItem();
+ converter(cursor, ysonConsumer);
+
+ ysonConsumer->OnEndList();
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+}
+
+TYsonCursorConverter CreateStructFieldsConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ std::vector<TStructFieldInfo> fieldInfos,
+ const TYsonConverterCreatorConfig& config)
+{
+ YT_VERIFY(config.Config.ComplexTypeMode == EComplexTypeMode::Positional);
+ return [
+ descriptor=descriptor,
+ fieldInfos=std::move(fieldInfos)
+ ] (TYsonPullParserCursor* cursor, IYsonConsumer* ysonConsumer) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+ ysonConsumer->OnBeginList();
+
+ for (const auto& fieldInfo : fieldInfos) {
+ if ((*cursor)->GetType() == EYsonItemType::EndList) {
+ break;
+ }
+ ysonConsumer->OnListItem();
+ fieldInfo.Converter(cursor, ysonConsumer);
+ }
+ ysonConsumer->OnEndList();
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+}
+
+TYsonCursorConverter CreateVariantStructFieldsConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ std::vector<std::pair<TString, TYsonCursorConverter>> elementConverters,
+ const TYsonConverterCreatorConfig& config)
+{
+ YT_VERIFY(config.Config.ComplexTypeMode == EComplexTypeMode::Positional);
+ return [
+ descriptor=descriptor,
+ alternatives=std::move(elementConverters)
+ ] (TYsonPullParserCursor* cursor, IYsonConsumer* ysonConsumer) {
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::Int64Value);
+ auto tag = cursor->GetCurrent().UncheckedAsInt64();
+ if (tag < 0 || tag >= std::ssize(alternatives)) {
+ THROW_ERROR_EXCEPTION(
+ "Error while parsing %Qv: variant tag (%v) is out of range [0, %v)",
+ descriptor.GetDescription(),
+ tag,
+ alternatives.size());
+ }
+ ysonConsumer->OnBeginList();
+
+ ysonConsumer->OnListItem();
+ ysonConsumer->OnInt64Scalar(tag);
+
+ cursor->Next();
+ ysonConsumer->OnListItem();
+ alternatives[tag].second(cursor, ysonConsumer);
+
+ ysonConsumer->OnEndList();
+
+ EnsureYsonToken(descriptor, *cursor, EYsonItemType::EndList);
+ cursor->Next();
+ };
+}
+
+using TYsonConsumerScannerFactory = TScannerFactory<IYsonConsumer*>;
+
+TYsonCursorConverter CreateYsonConverterImpl(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TIsTransformForTypeNeededCache& cache,
+ const TYsonConverterCreatorConfig& config)
+{
+ const auto& type = descriptor.GetType();
+ if (cache.IsTrivial(type)) {
+ return IdRecoder;
+ }
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple: {
+ const auto& simpleValueType = type->AsSimpleTypeRef().GetElement();
+ if (IsTimeType(type)) {
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TTimeServerToClientConverter(simpleValueType);
+ case EConverterType::ToServer:
+ return TTimeClientToServerConverter(simpleValueType);
+ }
+ YT_ABORT();
+ }
+ if (IsUuidType(type)) {
+ switch (config.ConverterType) {
+ case EConverterType::ToClient:
+ return TUuidServerToClientConverter(config.Config.UuidMode);
+ case EConverterType::ToServer:
+ return TUuidClientToServerConverter(config.Config.UuidMode);
+ }
+ YT_ABORT();
+ }
+ [[fallthrough]]; // AUTOGENERATED_FALLTHROUGH_FIXME
+ }
+ case ELogicalMetatype::Decimal: {
+ const auto& decimalTypeRef = type->AsDecimalTypeRef();
+ auto precision = decimalTypeRef.GetPrecision();
+ auto scale = decimalTypeRef.GetScale();
+ return CreateDecimalConverter(config, precision, scale);
+ }
+ case ELogicalMetatype::Optional: {
+ auto elementConverter = CreateYsonConverterImpl(descriptor.OptionalElement(), cache, config);
+ if (type->AsOptionalTypeRef().IsElementNullable()) {
+ return TYsonConsumerScannerFactory::CreateOptionalScanner(
+ descriptor,
+ TOptionalHandler<true>(),
+ elementConverter);
+ } else {
+ return TYsonConsumerScannerFactory::CreateOptionalScanner(
+ descriptor,
+ TOptionalHandler<false>(),
+ elementConverter);
+ };
+ }
+ case ELogicalMetatype::List: {
+ auto elementConverter = CreateYsonConverterImpl(descriptor.ListElement(), cache, config);
+ return TYsonConsumerScannerFactory::CreateListScanner(descriptor, TListHandler(), elementConverter);
+ }
+ case ELogicalMetatype::Tuple: {
+ std::vector<TYsonCursorConverter> elementConverters;
+ const auto size = type->GetElements().size();
+ for (size_t i = 0; i != size; ++i) {
+ elementConverters.push_back(CreateYsonConverterImpl(descriptor.TupleElement(i), cache, config));
+ }
+ return TYsonConsumerScannerFactory::CreateTupleScanner(
+ descriptor, TTupleApplier(), std::move(elementConverters));
+ }
+ case ELogicalMetatype::Struct: {
+ const auto& fields = type->GetFields();
+ std::vector<TStructFieldInfo> fieldInfos;
+ for (size_t i = 0; i != fields.size(); ++i) {
+ fieldInfos.emplace_back();
+ fieldInfos.back().FieldName = fields[i].Name;
+ fieldInfos.back().Converter = CreateYsonConverterImpl(descriptor.StructField(i), cache, config);
+ fieldInfos.back().IsNullable = fields[i].Type->IsNullable();
+ }
+ if (config.Config.ComplexTypeMode == EComplexTypeMode::Positional) {
+ return CreateStructFieldsConverter(descriptor, fieldInfos, config);
+ } else {
+ YT_VERIFY(config.Config.ComplexTypeMode == EComplexTypeMode::Named);
+ if (config.ConverterType == EConverterType::ToServer) {
+ return TNamedToPositionalStructConverter(descriptor, std::move(fieldInfos));
+ } else {
+ YT_VERIFY(config.ConverterType == EConverterType::ToClient);
+ if (config.Config.SkipNullValues) {
+ return TYsonConsumerScannerFactory::CreateStructScanner(
+ descriptor, TStructApplier<true>(), std::move(fieldInfos));
+ } else {
+ return TYsonConsumerScannerFactory::CreateStructScanner(
+ descriptor, TStructApplier<false>(), std::move(fieldInfos));
+ }
+ }
+ }
+ }
+ case ELogicalMetatype::VariantTuple: {
+ std::vector<std::pair<int,TYsonCursorConverter>> elementConverters;
+ const auto size = type->GetElements().size();
+ for (size_t i = 0; i != size; ++i) {
+ elementConverters.emplace_back(i, CreateYsonConverterImpl(descriptor.VariantTupleElement(i), cache, config));
+ }
+ return TYsonConsumerScannerFactory::CreateVariantScanner(
+ descriptor, TVariantTupleApplier(), std::move(elementConverters));
+ }
+ case ELogicalMetatype::VariantStruct: {
+ std::vector<std::pair<TString, TYsonCursorConverter>> elementConverters;
+ const auto& fields = type->GetFields();
+ for (size_t i = 0; i != fields.size(); ++i) {
+ elementConverters.emplace_back(
+ fields[i].Name,
+ CreateYsonConverterImpl(descriptor.VariantStructField(i), cache, config));
+ }
+ if (config.Config.ComplexTypeMode == EComplexTypeMode::Positional) {
+ return CreateVariantStructFieldsConverter(descriptor, std::move(elementConverters), config);
+ } else {
+ YT_VERIFY(config.Config.ComplexTypeMode == EComplexTypeMode::Named);
+ if (config.ConverterType == EConverterType::ToServer) {
+ return CreateNamedToPositionalVariantStructConverter(descriptor, std::move(elementConverters));
+ } else {
+ YT_VERIFY(config.ConverterType == EConverterType::ToClient);
+ return TYsonConsumerScannerFactory::CreateVariantScanner(
+ descriptor, TVariantStructApplier(), std::move(elementConverters));
+ }
+ }
+ }
+ case ELogicalMetatype::Dict: {
+ auto keyConverter = CreateYsonConverterImpl(descriptor.DictKey(), cache, config);
+ auto valueConverter = CreateYsonConverterImpl(descriptor.DictValue(), cache, config);
+
+ if (config.Config.StringKeyedDictMode == EDictMode::Named) {
+ auto keyType = descriptor.DictKey().GetType();
+ if (keyType->GetMetatype() == ELogicalMetatype::Simple
+ && keyType->AsSimpleTypeRef().GetElement() == ESimpleLogicalValueType::String) {
+ if (config.ConverterType == EConverterType::ToClient) {
+ return TYsonConsumerScannerFactory::CreateDictScanner(
+ descriptor,
+ TDictApplier<EDictMode::Named>(),
+ keyConverter,
+ valueConverter);
+ } else {
+ YT_VERIFY(config.ConverterType == EConverterType::ToServer);
+ return TNamedToPositionalDictConverter(descriptor, std::move(valueConverter));
+ }
+ }
+ }
+
+ return TYsonConsumerScannerFactory::CreateDictScanner(
+ descriptor,
+ TDictApplier<EDictMode::Positional>(),
+ keyConverter,
+ valueConverter);
+ }
+ case ELogicalMetatype::Tagged:
+ return CreateYsonConverterImpl(descriptor.TaggedElement(), cache, config);
+ }
+ YT_ABORT();
+}
+
+std::variant<TYsonServerToClientConverter, TYsonClientToServerConverter> CreateYsonConverterForConfig(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TIsTransformForTypeNeededCache& cache,
+ const TYsonConverterCreatorConfig& config)
+{
+ const auto& type = descriptor.GetType();
+ const auto metatype = type->GetMetatype();
+
+ if (metatype == ELogicalMetatype::Decimal) {
+ return CreateDecimalRawConverter(type, config);
+ }
+ if (metatype == ELogicalMetatype::Optional && type->GetElement()->GetMetatype() == ELogicalMetatype::Decimal) {
+ return CreateDecimalRawConverter(type->GetElement(), config);
+ }
+
+ if (IsTimeType(type)) {
+ return CreateTimeRawConverter(type, config);
+ }
+ if (metatype == ELogicalMetatype::Optional && IsTimeType(type->GetElement())) {
+ return CreateTimeRawConverter(type->GetElement(), config);
+ }
+
+ if (IsUuidType(type)) {
+ return CreateUuidRawConverter(config);
+ }
+ if (metatype == ELogicalMetatype::Optional && IsUuidType(type->GetElement())) {
+ return CreateUuidRawConverter(config);
+ }
+
+ auto converter = CreateYsonConverterImpl(descriptor, cache, config);
+ if (config.ConverterType == EConverterType::ToClient) {
+ return [
+ converter=std::move(converter)
+ ] (TUnversionedValue value, IYsonConsumer* consumer) {
+ TMemoryInput in(value.Data.String, value.Length);
+ TYsonPullParser parser(&in, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+ converter(&cursor, consumer);
+ };
+ } else {
+ YT_VERIFY(config.ConverterType == EConverterType::ToServer);
+ return TClientToServerComplexValueConverterWrapper(std::move(converter));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonServerToClientConverter CreateYsonServerToClientConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TYsonConverterConfig& config)
+{
+ TYsonConverterCreatorConfig creatorConfig{config, EConverterType::ToClient};
+ TIsTransformForTypeNeededCache cache(descriptor.GetType(), creatorConfig);
+ if (cache.IsTrivial(descriptor.GetType())) {
+ return {};
+ }
+
+ auto converterVariant = CreateYsonConverterForConfig(descriptor, cache, creatorConfig);
+ YT_VERIFY(std::holds_alternative<TYsonServerToClientConverter>(converterVariant));
+ return std::get<TYsonServerToClientConverter>(converterVariant);
+}
+
+TYsonClientToServerConverter CreateYsonClientToServerConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TYsonConverterConfig& config)
+{
+ TYsonConverterCreatorConfig creatorConfig{config, EConverterType::ToServer};
+ TIsTransformForTypeNeededCache cache(descriptor.GetType(), creatorConfig);
+ if (cache.IsTrivial(descriptor.GetType())) {
+ return {};
+ }
+
+ auto converterVariant = CreateYsonConverterForConfig(descriptor, cache, creatorConfig);
+ YT_VERIFY(std::holds_alternative<TYsonClientToServerConverter>(converterVariant));
+ return std::get<TYsonClientToServerConverter>(converterVariant);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/complex_types/yson_format_conversion.h b/yt/yt/client/complex_types/yson_format_conversion.h
new file mode 100644
index 0000000000..7bac5cc6a0
--- /dev/null
+++ b/yt/yt/client/complex_types/yson_format_conversion.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/formats/public.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// There are two modes of representing complex types in yson: named and positional.
+// They are similar for all types but Struct and VariantStruct.
+//
+// Consider Struct({{"field0", type0}, {"field1", type1}, ...})
+// Its positional representation might be:
+// [v0; v1; ...] (v0 is of type0, v1 is of type1)
+//
+// Named representation of the same structure:
+// {field0=v0; field1=v1}
+//
+//
+// Consider Struct({{"field0", type0}, {"field1", type1}, ...})
+// Its positional representation might be:
+// [1, v1]
+//
+// Named representation of the same structure:
+// [field1; v1]
+//
+// Functions in this file create convertors between these two representations.
+
+using TYsonClientToServerConverter = std::function<NTableClient::TUnversionedValue(NTableClient::TUnversionedValue value)>;
+using TYsonServerToClientConverter = std::function<void(NTableClient::TUnversionedValue value, NYson::IYsonConsumer* consumer)>;
+
+struct TYsonConverterConfig
+{
+ NFormats::EComplexTypeMode ComplexTypeMode = NFormats::EComplexTypeMode::Named;
+ NFormats::EDictMode StringKeyedDictMode = NFormats::EDictMode::Positional;
+ NFormats::EDecimalMode DecimalMode = NFormats::EDecimalMode::Binary;
+ NFormats::ETimeMode TimeMode = NFormats::ETimeMode::Binary;
+ NFormats::EUuidMode UuidMode = NFormats::EUuidMode::Binary;
+
+ // When SkipNullValues is true converters doesn't write
+ // structure fields that have `#` value (i.e. they are Null).
+ bool SkipNullValues = false;
+};
+
+TYsonServerToClientConverter CreateYsonServerToClientConverter(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const TYsonConverterConfig& config);
+
+TYsonClientToServerConverter CreateYsonClientToServerConverter(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const TYsonConverterConfig& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/cypress_client/public.cpp b/yt/yt/client/cypress_client/public.cpp
new file mode 100644
index 0000000000..147aed65b7
--- /dev/null
+++ b/yt/yt/client/cypress_client/public.cpp
@@ -0,0 +1,12 @@
+#include "public.h"
+
+namespace NYT::NCypressClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLockId NullLockId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCypressClient
+
diff --git a/yt/yt/client/cypress_client/public.h b/yt/yt/client/cypress_client/public.h
new file mode 100644
index 0000000000..05e70d4984
--- /dev/null
+++ b/yt/yt/client/cypress_client/public.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NCypressClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NObjectClient::TObjectId;
+using NObjectClient::TTransactionId;
+using NObjectClient::NullObjectId;
+using NObjectClient::NullTransactionId;
+using NObjectClient::EObjectType;
+using NObjectClient::TVersionedObjectId;
+
+using TNodeId = TObjectId;
+using TLockId = TObjectId;
+using TCypressShardId = TObjectId;
+using TVersionedNodeId = TVersionedObjectId;
+
+extern const TLockId NullLockId;
+
+// NB: The order is from weakest to strongest.
+DEFINE_ENUM(ELockMode,
+ ((None) (0))
+ ((Snapshot) (1))
+ ((Shared) (2))
+ ((Exclusive) (3))
+);
+
+DEFINE_ENUM(ELockState,
+ ((Pending) (0))
+ ((Acquired) (1))
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((SameTransactionLockConflict) (400))
+ ((DescendantTransactionLockConflict) (401))
+ ((ConcurrentTransactionLockConflict) (402))
+ ((PendingLockConflict) (403))
+ ((LockDestroyed) (404))
+ ((TooManyLocksOnTransaction) (405))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCypressClient
diff --git a/yt/yt/client/driver/admin_commands.cpp b/yt/yt/client/driver/admin_commands.cpp
new file mode 100644
index 0000000000..d4b0e460f0
--- /dev/null
+++ b/yt/yt/client/driver/admin_commands.cpp
@@ -0,0 +1,440 @@
+#include "admin_commands.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NChaosClient;
+using namespace NObjectClient;
+
+using NApi::TMaintenanceId;
+using NApi::TMaintenanceFilter;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBuildSnapshotCommand::TBuildSnapshotCommand()
+{
+ RegisterParameter("cell_id", Options.CellId);
+ RegisterParameter("set_read_only", Options.SetReadOnly)
+ .Optional();
+ RegisterParameter("wait_for_snapshot_completion", Options.WaitForSnapshotCompletion)
+ .Optional();
+}
+
+void TBuildSnapshotCommand::DoExecute(ICommandContextPtr context)
+{
+ auto snapshotId = WaitFor(context->GetClient()->BuildSnapshot(Options))
+ .ValueOrThrow();
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("snapshot_id").Value(snapshotId)
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBuildMasterSnapshotsCommand::TBuildMasterSnapshotsCommand()
+{
+ RegisterParameter("set_read_only", Options.SetReadOnly)
+ .Optional();
+ RegisterParameter("wait_for_snapshot_completion", Options.WaitForSnapshotCompletion)
+ .Optional();
+ RegisterParameter("retry", Options.Retry)
+ .Optional();
+}
+
+void TBuildMasterSnapshotsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto cellIdToSnapshotId = WaitFor(context->GetClient()->BuildMasterSnapshots(Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .DoListFor(cellIdToSnapshotId, [=] (TFluentList fluent, const auto& pair) {
+ fluent
+ .Item().BeginMap()
+ .Item("cell_id").Value(pair.first)
+ .Item("snapshot_id").Value(pair.second)
+ .EndMap();
+ })
+ );
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSwitchLeaderCommand::TSwitchLeaderCommand()
+{
+ RegisterParameter("cell_id", CellId_);
+ RegisterParameter("new_leader_address", NewLeaderAddress_);
+}
+
+void TSwitchLeaderCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SwitchLeader(CellId_, NewLeaderAddress_))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResetStateHashCommand::TResetStateHashCommand()
+{
+ RegisterParameter("cell_id", CellId_);
+ RegisterParameter("new_state_hash", Options.NewStateHash)
+ .Optional();
+}
+
+void TResetStateHashCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->ResetStateHash(CellId_, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THealExecNodeCommand::THealExecNodeCommand()
+{
+ RegisterParameter("address", Address_);
+ RegisterParameter("locations", Options.Locations)
+ .Optional();
+ RegisterParameter("alert_types_to_reset", Options.AlertTypesToReset)
+ .Optional();
+ RegisterParameter("force_reset", Options.ForceReset)
+ .Optional();
+}
+
+void THealExecNodeCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->HealExecNode(Address_, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSuspendCoordinatorCommand::TSuspendCoordinatorCommand()
+{
+ RegisterParameter("coordinator_cell_id", CoordinatorCellId_);
+}
+
+void TSuspendCoordinatorCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SuspendCoordinator(CoordinatorCellId_))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResumeCoordinatorCommand::TResumeCoordinatorCommand()
+{
+ RegisterParameter("coordinator_cell_id", CoordinatorCellId_);
+}
+
+void TResumeCoordinatorCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->ResumeCoordinator(CoordinatorCellId_))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMigrateReplicationCardsCommand::TMigrateReplicationCardsCommand()
+{
+ RegisterParameter("chaos_cell_id", ChaosCellId_);
+ RegisterParameter("destination_cell_id", Options.DestinationCellId)
+ .Optional();
+ RegisterParameter("replication_card_ids", Options.ReplicationCardIds)
+ .Optional();
+}
+
+void TMigrateReplicationCardsCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->MigrateReplicationCards(ChaosCellId_, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSuspendChaosCellsCommand::TSuspendChaosCellsCommand()
+{
+ RegisterParameter("cell_ids", CellIds_);
+}
+
+void TSuspendChaosCellsCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SuspendChaosCells(CellIds_))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResumeChaosCellsCommand::TResumeChaosCellsCommand()
+{
+ RegisterParameter("cell_ids", CellIds_);
+}
+
+void TResumeChaosCellsCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->ResumeChaosCells(CellIds_))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSuspendTabletCellsCommand::TSuspendTabletCellsCommand()
+{
+ RegisterParameter("cell_ids", CellIds_);
+}
+
+void TSuspendTabletCellsCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SuspendTabletCells(CellIds_, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResumeTabletCellsCommand::TResumeTabletCellsCommand()
+{
+ RegisterParameter("cell_ids", CellIds_);
+}
+
+void TResumeTabletCellsCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->ResumeTabletCells(CellIds_, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAddMaintenanceCommand::TAddMaintenanceCommand()
+{
+ RegisterParameter("component", Component_);
+ RegisterParameter("address", Address_);
+ RegisterParameter("type", Type_);
+ RegisterParameter("comment", Comment_);
+}
+
+void TAddMaintenanceCommand::DoExecute(ICommandContextPtr context)
+{
+ auto id = WaitFor(context->GetClient()->AddMaintenance(
+ Component_,
+ Address_,
+ Type_,
+ Comment_,
+ Options))
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "id", id);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRemoveMaintenanceCommand::TRemoveMaintenanceCommand()
+{
+ RegisterParameter("component", Component_);
+ RegisterParameter("address", Address_);
+
+ RegisterParameter("id", Id_)
+ .Default();
+
+ RegisterParameter("ids", Ids_)
+ .Default();
+
+ RegisterParameter("user", User_)
+ .Default();
+ RegisterParameter("mine", Mine_)
+ .Optional();
+
+ RegisterParameter("type", Type_)
+ .Optional();
+
+ RegisterParameter("all", All_)
+ .Optional();
+
+ RegisterPostprocessor([&] {
+ THROW_ERROR_EXCEPTION_IF(Id_ && Ids_,
+ "At most one of {\"id\", \"ids\"} can be specified at the same time");
+
+ THROW_ERROR_EXCEPTION_IF(Ids_ && Ids_->empty(),
+ "\"ids\" must not be empty if specified");
+
+ THROW_ERROR_EXCEPTION_IF(!Id_ && !Ids_ && !User_ && !Mine_ && !Type_ && !All_,
+ "\"all\" must be specified explicitly");
+
+ THROW_ERROR_EXCEPTION_IF(Mine_ && User_,
+ "Cannot specify both \"user\" and \"mine\"");
+
+ THROW_ERROR_EXCEPTION_IF(All_ && (User_ || Mine_ || Type_ || Id_ || Ids_),
+ "\"all\" cannot be used with other options");
+ });
+}
+
+namespace {
+
+TStringBuf MaintenanceTypeToString(NApi::EMaintenanceType type)
+{
+ switch (type) {
+ case NApi::EMaintenanceType::Ban:
+ return "ban";
+ case NApi::EMaintenanceType::Decommission:
+ return "decommission";
+ case NApi::EMaintenanceType::DisableSchedulerJobs:
+ return "disable_scheduler_jobs";
+ case NApi::EMaintenanceType::DisableWriteSessions:
+ return "disable_write_sessions";
+ case NApi::EMaintenanceType::DisableTabletCells:
+ return "disable_tablet_cells";
+ case NApi::EMaintenanceType::PendingRestart:
+ return "pending_restart";
+ default:
+ YT_ABORT();
+ }
+}
+
+} // namespace
+
+void TRemoveMaintenanceCommand::DoExecute(ICommandContextPtr context)
+{
+ TMaintenanceFilter filter;
+
+ if (Id_) {
+ filter.Ids = {*Id_};
+ } else if (Ids_) {
+ filter.Ids = *Ids_;
+ }
+
+ if (Mine_) {
+ filter.User = TMaintenanceFilter::TByUser::TMine{};
+ } else if (User_) {
+ filter.User = *User_;
+ } else {
+ filter.User = TMaintenanceFilter::TByUser::TAll{};
+ }
+
+ if (Type_) {
+ filter.Type = *Type_;
+ }
+
+ auto removedMaintenanceCounts = WaitFor(context->GetClient()->RemoveMaintenance(
+ Component_,
+ Address_,
+ filter,
+ Options))
+ .ValueOrThrow();
+
+ ProduceOutput(context, [&] (NYson::IYsonConsumer* consumer) {
+ auto fluent = BuildYsonFluently(consumer)
+ .BeginMap();
+ for (auto type : TEnumTraits<NApi::EMaintenanceType>::GetDomainValues()) {
+ if (removedMaintenanceCounts[type] > 0) {
+ fluent = fluent.Item(MaintenanceTypeToString(type)).Value(removedMaintenanceCounts[type]);
+ }
+ }
+ fluent.EndMap();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDisableChunkLocationsCommand::TDisableChunkLocationsCommand()
+{
+ RegisterParameter("node_address", NodeAddress_);
+ RegisterParameter("location_uuids", LocationUuids_)
+ .Default();
+}
+
+void TDisableChunkLocationsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->DisableChunkLocations(NodeAddress_, LocationUuids_, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("location_uuids")
+ .BeginList()
+ .DoFor(result.LocationUuids, [&] (TFluentList fluent, const auto& uuid) {
+ fluent.Item().Value(uuid);
+ })
+ .EndList()
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDestroyChunkLocationsCommand::TDestroyChunkLocationsCommand()
+{
+ RegisterParameter("node_address", NodeAddress_);
+ RegisterParameter("location_uuids", LocationUuids_)
+ .Default();
+}
+
+void TDestroyChunkLocationsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->DestroyChunkLocations(NodeAddress_, LocationUuids_, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("location_uuids")
+ .BeginList()
+ .DoFor(result.LocationUuids, [&] (TFluentList fluent, const auto& uuid) {
+ fluent.Item().Value(uuid);
+ })
+ .EndList()
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResurrectChunkLocationsCommand::TResurrectChunkLocationsCommand()
+{
+ RegisterParameter("node_address", NodeAddress_);
+ RegisterParameter("location_uuids", LocationUuids_)
+ .Default();
+}
+
+void TResurrectChunkLocationsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->ResurrectChunkLocations(NodeAddress_, LocationUuids_, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("location_uuids")
+ .BeginList()
+ .DoFor(result.LocationUuids, [&] (TFluentList fluent, const auto& uuid) {
+ fluent.Item().Value(uuid);
+ })
+ .EndList()
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/admin_commands.h b/yt/yt/client/driver/admin_commands.h
new file mode 100644
index 0000000000..9e1ade1f31
--- /dev/null
+++ b/yt/yt/client/driver/admin_commands.h
@@ -0,0 +1,257 @@
+#pragma once
+
+#include "command.h"
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBuildSnapshotCommand
+ : public TTypedCommand<NApi::TBuildSnapshotOptions>
+{
+public:
+ TBuildSnapshotCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBuildMasterSnapshotsCommand
+ : public TTypedCommand<NApi::TBuildMasterSnapshotsOptions>
+{
+public:
+ TBuildMasterSnapshotsCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSwitchLeaderCommand
+ : public TTypedCommand<NApi::TSwitchLeaderOptions>
+{
+public:
+ TSwitchLeaderCommand();
+
+private:
+ NHydra::TCellId CellId_;
+ TString NewLeaderAddress_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResetStateHashCommand
+ : public TTypedCommand<NApi::TResetStateHashOptions>
+{
+public:
+ TResetStateHashCommand();
+
+private:
+ NHydra::TCellId CellId_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THealExecNodeCommand
+ : public TTypedCommand<NApi::THealExecNodeOptions>
+{
+public:
+ THealExecNodeCommand();
+
+private:
+ TString Address_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendCoordinatorCommand
+ : public TTypedCommand<NApi::TSuspendCoordinatorOptions>
+{
+public:
+ TSuspendCoordinatorCommand();
+
+private:
+ NObjectClient::TCellId CoordinatorCellId_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResumeCoordinatorCommand
+ : public TTypedCommand<NApi::TResumeCoordinatorOptions>
+{
+public:
+ TResumeCoordinatorCommand();
+
+private:
+ NObjectClient::TCellId CoordinatorCellId_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMigrateReplicationCardsCommand
+ : public TTypedCommand<NApi::TMigrateReplicationCardsOptions>
+{
+public:
+ TMigrateReplicationCardsCommand();
+
+private:
+ NObjectClient::TCellId ChaosCellId_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendChaosCellsCommand
+ : public TTypedCommand<NApi::TSuspendChaosCellsOptions>
+{
+public:
+ TSuspendChaosCellsCommand();
+
+private:
+ std::vector<NObjectClient::TCellId> CellIds_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResumeChaosCellsCommand
+ : public TTypedCommand<NApi::TResumeChaosCellsOptions>
+{
+public:
+ TResumeChaosCellsCommand();
+
+private:
+ std::vector<NObjectClient::TCellId> CellIds_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendTabletCellsCommand
+ : public TTypedCommand<NApi::TSuspendTabletCellsOptions>
+{
+public:
+ TSuspendTabletCellsCommand();
+
+private:
+ std::vector<NObjectClient::TCellId> CellIds_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResumeTabletCellsCommand
+ : public TTypedCommand<NApi::TResumeTabletCellsOptions>
+{
+public:
+ TResumeTabletCellsCommand();
+
+private:
+ std::vector<NObjectClient::TCellId> CellIds_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAddMaintenanceCommand
+ : public TTypedCommand<NApi::TAddMaintenanceOptions>
+{
+public:
+ TAddMaintenanceCommand();
+
+private:
+ NApi::EMaintenanceComponent Component_;
+ TString Address_;
+ NApi::EMaintenanceType Type_;
+ TString Comment_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoveMaintenanceCommand
+ : public TTypedCommand<NApi::TRemoveMaintenanceOptions>
+{
+public:
+ TRemoveMaintenanceCommand();
+
+private:
+ NApi::EMaintenanceComponent Component_;
+ TString Address_;
+ bool Mine_ = false;
+ bool All_ = false;
+ std::optional<TString> User_;
+ std::optional<NApi::TMaintenanceId> Id_;
+ std::optional<std::vector<NApi::TMaintenanceId>> Ids_;
+ std::optional<NApi::EMaintenanceType> Type_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDisableChunkLocationsCommand
+ : public TTypedCommand<NApi::TDisableChunkLocationsOptions>
+{
+public:
+ TDisableChunkLocationsCommand();
+
+private:
+ TString NodeAddress_;
+ std::vector<TGuid> LocationUuids_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDestroyChunkLocationsCommand
+ : public TTypedCommand<NApi::TDestroyChunkLocationsOptions>
+{
+public:
+ TDestroyChunkLocationsCommand();
+
+private:
+ TString NodeAddress_;
+ std::vector<TGuid> LocationUuids_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResurrectChunkLocationsCommand
+ : public TTypedCommand<NApi::TResurrectChunkLocationsOptions>
+{
+public:
+ TResurrectChunkLocationsCommand();
+
+private:
+ TString NodeAddress_;
+ std::vector<TGuid> LocationUuids_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/authentication_commands.cpp b/yt/yt/client/driver/authentication_commands.cpp
new file mode 100644
index 0000000000..170dfdcbf9
--- /dev/null
+++ b/yt/yt/client/driver/authentication_commands.cpp
@@ -0,0 +1,97 @@
+#include "authentication_commands.h"
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NDriver {
+
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSetUserPasswordCommand::TSetUserPasswordCommand()
+{
+ RegisterParameter("user", User_);
+ RegisterParameter("current_password_sha256", CurrentPasswordSha256_)
+ .Default();
+ RegisterParameter("new_password_sha256", NewPasswordSha256_);
+}
+
+void TSetUserPasswordCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SetUserPassword(
+ User_,
+ CurrentPasswordSha256_,
+ NewPasswordSha256_,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TIssueTokenCommand::TIssueTokenCommand()
+{
+ RegisterParameter("user", User_);
+ RegisterParameter("password_sha256", PasswordSha256_)
+ .Default();
+}
+
+void TIssueTokenCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->IssueToken(
+ User_,
+ PasswordSha256_,
+ Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(ConvertToYsonString(result.Token));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRevokeTokenCommand::TRevokeTokenCommand()
+{
+ RegisterParameter("user", User_);
+ RegisterParameter("password_sha256", PasswordSha256_)
+ .Default();
+ RegisterParameter("token_sha256", TokenSha256_);
+}
+
+void TRevokeTokenCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->RevokeToken(
+ User_,
+ PasswordSha256_,
+ TokenSha256_,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListUserTokensCommand::TListUserTokensCommand()
+{
+ RegisterParameter("user", User_);
+ RegisterParameter("password_sha256", PasswordSha256_)
+ .Default();
+}
+
+void TListUserTokensCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->ListUserTokens(
+ User_,
+ PasswordSha256_,
+ Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(ConvertToYsonString(result.Tokens));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/authentication_commands.h b/yt/yt/client/driver/authentication_commands.h
new file mode 100644
index 0000000000..7181d7ce8c
--- /dev/null
+++ b/yt/yt/client/driver/authentication_commands.h
@@ -0,0 +1,73 @@
+#include "command.h"
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSetUserPasswordCommand
+ : public TTypedCommand<NApi::TSetUserPasswordOptions>
+{
+public:
+ TSetUserPasswordCommand();
+
+private:
+ TString User_;
+
+ TString CurrentPasswordSha256_;
+ TString NewPasswordSha256_;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIssueTokenCommand
+ : public TTypedCommand<NApi::TIssueTokenOptions>
+{
+public:
+ TIssueTokenCommand();
+
+private:
+ TString User_;
+
+ TString PasswordSha256_;
+
+ void DoExecute(ICommandContextPtr context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRevokeTokenCommand
+ : public TTypedCommand<NApi::TRevokeTokenOptions>
+{
+public:
+ TRevokeTokenCommand();
+
+private:
+ TString User_;
+
+ TString PasswordSha256_;
+ TString TokenSha256_;
+
+ void DoExecute(ICommandContextPtr context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListUserTokensCommand
+ : public TTypedCommand<NApi::TListUserTokensOptions>
+{
+public:
+ TListUserTokensCommand();
+
+private:
+ TString User_;
+
+ TString PasswordSha256_;
+
+ void DoExecute(ICommandContextPtr context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/chaos_commands.cpp b/yt/yt/client/driver/chaos_commands.cpp
new file mode 100644
index 0000000000..5a3c72358f
--- /dev/null
+++ b/yt/yt/client/driver/chaos_commands.cpp
@@ -0,0 +1,58 @@
+#include "chaos_commands.h"
+
+#include <yt/yt/client/chaos_client/replication_card_serialization.h>
+
+#include <yt/yt/client/tablet_client/config.h>
+
+namespace NYT::NDriver {
+
+using namespace NApi;
+using namespace NChaosClient;
+using namespace NConcurrency;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUpdateChaosTableReplicaProgressCommand::TUpdateChaosTableReplicaProgressCommand()
+{
+ RegisterParameter("replica_id", ReplicaId);
+ RegisterParameter("progress", Options.Progress);
+}
+
+void TUpdateChaosTableReplicaProgressCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+
+ auto future = client->UpdateChaosTableReplicaProgress(ReplicaId, Options);
+ WaitFor(future)
+ .ThrowOnError();
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAlterReplicationCardCommand::TAlterReplicationCardCommand()
+{
+ RegisterParameter("replication_card_id", ReplicationCardId);
+ RegisterParameter("replicated_table_options", Options.ReplicatedTableOptions)
+ .Optional();
+ RegisterParameter("enable_replicated_table_tracker", Options.EnableReplicatedTableTracker)
+ .Optional();
+ RegisterParameter("replication_card_collocation_id", Options.ReplicationCardCollocationId)
+ .Optional();
+}
+
+void TAlterReplicationCardCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->AlterReplicationCard(
+ ReplicationCardId,
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/chaos_commands.h b/yt/yt/client/driver/chaos_commands.h
new file mode 100644
index 0000000000..ed763e84d8
--- /dev/null
+++ b/yt/yt/client/driver/chaos_commands.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/chaos_client/replication_card.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUpdateChaosTableReplicaProgressCommand
+ : public TTypedCommand<NApi::TUpdateChaosTableReplicaProgressOptions>
+{
+public:
+ TUpdateChaosTableReplicaProgressCommand();
+
+private:
+ NChaosClient::TReplicaId ReplicaId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAlterReplicationCardCommand
+ : public TTypedCommand<NApi::TAlterReplicationCardOptions>
+{
+public:
+ TAlterReplicationCardCommand();
+
+private:
+ NChaosClient::TReplicationCardId ReplicationCardId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/command-inl.h b/yt/yt/client/driver/command-inl.h
new file mode 100644
index 0000000000..564a328662
--- /dev/null
+++ b/yt/yt/client/driver/command-inl.h
@@ -0,0 +1,272 @@
+#ifndef COMMAND_INL_H
+#error "Direct inclusion of this file is not allowed, include command.h"
+// For the sake of sane code completion.
+#include "command.h"
+#endif
+
+#include "private.h"
+#include "driver.h"
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void ProduceSingleOutputValue(
+ ICommandContextPtr context,
+ TStringBuf name,
+ const T& value)
+{
+ ProduceSingleOutput(context, name, [&] (NYson::IYsonConsumer* consumer) {
+ NYTree::BuildYsonFluently(consumer)
+ .Value(value);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TTransactionalCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTransactionalOptions&>>
+>::TTransactionalCommandBase()
+{
+ this->RegisterParameter("transaction_id", this->Options.TransactionId)
+ .Optional();
+ this->RegisterParameter("ping", this->Options.Ping)
+ .Optional();
+ this->RegisterParameter("ping_ancestor_transactions", this->Options.PingAncestors)
+ .Optional();
+ this->RegisterParameter("suppress_transaction_coordinator_sync", this->Options.SuppressTransactionCoordinatorSync)
+ .Optional();
+ this->RegisterParameter("suppress_upstream_sync", this->Options.SuppressUpstreamSync)
+ .Optional();
+}
+
+template <class TOptions>
+NApi::ITransactionPtr TTransactionalCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTransactionalOptions&>>
+>::AttachTransaction(
+ ICommandContextPtr context,
+ bool required)
+{
+ auto transactionId = this->Options.TransactionId;
+ if (!transactionId) {
+ if (required) {
+ THROW_ERROR_EXCEPTION("Transaction is required");
+ }
+ return nullptr;
+ }
+
+ const auto& transactionPool = context->GetDriver()->GetStickyTransactionPool();
+
+ if (!NTransactionClient::IsMasterTransactionId(transactionId)) {
+ return transactionPool->GetTransactionAndRenewLeaseOrThrow(transactionId);
+ }
+
+ auto transaction = transactionPool->FindTransactionAndRenewLease(transactionId);
+ if (!transaction) {
+ NApi::TTransactionAttachOptions options;
+ options.Ping = this->Options.Ping;
+ options.PingAncestors = this->Options.PingAncestors;
+ transaction = context->GetClient()->AttachTransaction(transactionId, options);
+ }
+
+ return transaction;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TMutatingCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TMutatingOptions&>>
+>::TMutatingCommandBase()
+{
+ this->RegisterParameter("mutation_id", this->Options.MutationId)
+ .Optional();
+ this->RegisterParameter("retry", this->Options.Retry)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TReadOnlyMasterCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TMasterReadOptions&>>
+>::TReadOnlyMasterCommandBase()
+{
+ this->RegisterParameter("read_from", this->Options.ReadFrom)
+ .Optional();
+ this->RegisterParameter("disable_per_user_cache", this->Options.DisablePerUserCache)
+ .Optional();
+ this->RegisterParameter("expire_after_successful_update_time", this->Options.ExpireAfterSuccessfulUpdateTime)
+ .Optional();
+ this->RegisterParameter("expire_after_failed_update_time", this->Options.ExpireAfterFailedUpdateTime)
+ .Optional();
+ this->RegisterParameter("success_staleness_bound", this->Options.SuccessStalenessBound)
+ .Optional();
+ this->RegisterParameter("cache_sticky_group_size", this->Options.CacheStickyGroupSize)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TReadOnlyTabletCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTabletReadOptions&>>
+>::TReadOnlyTabletCommandBase()
+{
+ this->RegisterParameter("read_from", this->Options.ReadFrom)
+ .Optional();
+ this->RegisterParameter("rpc_hedging_delay", this->Options.RpcHedgingDelay)
+ .Optional();
+ this->RegisterParameter("timestamp", this->Options.Timestamp)
+ .Optional();
+ this->RegisterParameter("retention_timestamp", this->Options.RetentionTimestamp)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TSuppressableAccessTrackingCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TSuppressableAccessTrackingOptions&>>
+>::TSuppressableAccessTrackingCommandBase()
+{
+ this->RegisterParameter("suppress_access_tracking", this->Options.SuppressAccessTracking)
+ .Optional();
+ this->RegisterParameter("suppress_modification_tracking", this->Options.SuppressModificationTracking)
+ .Optional();
+ this->RegisterParameter("suppress_expiration_timeout_renewal", this->Options.SuppressExpirationTimeoutRenewal)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TPrerequisiteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TPrerequisiteOptions&>>
+>::TPrerequisiteCommandBase()
+{
+ this->RegisterParameter("prerequisite_transaction_ids", this->Options.PrerequisiteTransactionIds)
+ .Optional();
+ this->RegisterParameter("prerequisite_revisions", this->Options.PrerequisiteRevisions)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TTimeoutCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTimeoutOptions&>>
+>::TTimeoutCommandBase()
+{
+ this->RegisterParameter("timeout", this->Options.Timeout)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TTabletReadCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletTransactionOptions&>>
+>::TTabletReadCommandBase()
+{
+ this->RegisterParameter("transaction_id", this->Options.TransactionId)
+ .Optional();
+}
+
+template <class TOptions>
+NApi::IClientBasePtr TTabletReadCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletTransactionOptions&>>
+>::GetClientBase(ICommandContextPtr context)
+{
+ if (auto transactionId = this->Options.TransactionId) {
+ const auto& transactionPool = context->GetDriver()->GetStickyTransactionPool();
+ return transactionPool->GetTransactionAndRenewLeaseOrThrow(transactionId);
+ } else {
+ return context->GetClient();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TTabletWriteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletWriteOptions&>>
+>::TTabletWriteCommandBase()
+{
+ this->RegisterParameter("atomicity", this->Options.Atomicity)
+ .Default();
+ this->RegisterParameter("durability", this->Options.Durability)
+ .Default();
+}
+
+template <class TOptions>
+NApi::ITransactionPtr TTabletWriteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletWriteOptions&>>
+>::GetTransaction(ICommandContextPtr context)
+{
+ if (auto transactionId = this->Options.TransactionId) {
+ const auto& transactionPool = context->GetDriver()->GetStickyTransactionPool();
+ return transactionPool->GetTransactionAndRenewLeaseOrThrow(transactionId);
+ } else {
+ NApi::TTransactionStartOptions options;
+ options.Atomicity = this->Options.Atomicity;
+ options.Durability = this->Options.Durability;
+ const auto& client = context->GetClient();
+ return NConcurrency::WaitFor(client->StartTransaction(NTransactionClient::ETransactionType::Tablet, options))
+ .ValueOrThrow();
+ }
+}
+
+template <class TOptions>
+bool TTabletWriteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletWriteOptions&>>
+>::ShouldCommitTransaction()
+{
+ return !this->Options.TransactionId;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+TSelectRowsCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TSelectRowsOptionsBase&>>
+>::TSelectRowsCommandBase()
+{
+ this->RegisterParameter("range_expansion_limit", this->Options.RangeExpansionLimit)
+ .Optional();
+ this->RegisterParameter("max_subqueries", this->Options.MaxSubqueries)
+ .GreaterThan(0)
+ .Optional();
+ this->RegisterParameter("udf_registry_path", this->Options.UdfRegistryPath)
+ .Default();
+ this->RegisterParameter("verbose_logging", this->Options.VerboseLogging)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/command.cpp b/yt/yt/client/driver/command.cpp
new file mode 100644
index 0000000000..7271e545ae
--- /dev/null
+++ b/yt/yt/client/driver/command.cpp
@@ -0,0 +1,119 @@
+#include "command.h"
+#include "config.h"
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+namespace NYT::NDriver {
+
+using namespace NYPath;
+using namespace NYson;
+using namespace NYTree;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+void ProduceOutput(
+ ICommandContextPtr context,
+ const std::function<void(IYsonConsumer*)>& producer)
+{
+ TStringStream stream;
+ TYsonWriter writer(&stream, EYsonFormat::Binary);
+ producer(&writer);
+ writer.Flush();
+ context->ProduceOutputValue(TYsonString(stream.Str()));
+}
+
+void ProduceEmptyOutput(ICommandContextPtr context)
+{
+ switch (context->GetConfig()->ApiVersion) {
+ case ApiVersion3:
+ break;
+ default:
+ context->ProduceOutputValue(NYTree::BuildYsonStringFluently().BeginMap().EndMap());
+ break;
+ }
+}
+
+void ProduceSingleOutput(
+ ICommandContextPtr context,
+ TStringBuf name,
+ const std::function<void(IYsonConsumer*)>& producer)
+{
+ switch (context->GetConfig()->ApiVersion) {
+ case ApiVersion3:
+ ProduceOutput(context, producer);
+ break;
+ default:
+ ProduceOutput(context, [&] (IYsonConsumer* consumer) {
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item(name).Do([&] (auto fluent) {
+ producer(fluent.GetConsumer());
+ })
+ .EndMap();
+ });
+ break;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCommandBase::TCommandBase()
+{
+ SetUnrecognizedStrategy(NYTree::EUnrecognizedStrategy::Keep);
+}
+
+void TCommandBase::Execute(ICommandContextPtr context)
+{
+ const auto& request = context->Request();
+ Logger.AddTag("RequestId: %" PRIx64 ", User: %v",
+ request.Id,
+ request.AuthenticatedUser);
+ Deserialize(*this, request.Parameters);
+
+ if (!HasResponseParameters()) {
+ ProduceResponseParameters(context, /* producer */ {});
+ }
+
+ DoExecute(context);
+}
+
+bool TCommandBase::HasResponseParameters() const
+{
+ return false;
+}
+
+void TCommandBase::ProduceResponseParameters(
+ ICommandContextPtr context,
+ const std::function<void(IYsonConsumer*)>& producer)
+{
+ if (producer) {
+ YT_VERIFY(HasResponseParameters());
+ producer(context->Request().ResponseParametersConsumer);
+ }
+ if (context->Request().ResponseParametersFinishedCallback) {
+ context->Request().ResponseParametersFinishedCallback();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Keep sync with yt/ytlib/scheduler/helpers.cpp.
+TYPath GetNewOperationPath(TGuid operationId)
+{
+ int hashByte = operationId.Parts32[0] & 0xff;
+ return
+ "//sys/operations/" +
+ Format("%02x", hashByte) +
+ "/" +
+ ToYPathLiteral(ToString(operationId));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/command.h b/yt/yt/client/driver/command.h
new file mode 100644
index 0000000000..669f688fe4
--- /dev/null
+++ b/yt/yt/client/driver/command.h
@@ -0,0 +1,321 @@
+#pragma once
+
+#include "private.h"
+#include "driver.h"
+
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/transaction.h>
+#include <yt/yt/client/api/sticky_transaction_pool.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICommand
+{
+ virtual ~ICommand() = default;
+ virtual void Execute(ICommandContextPtr context) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICommandContext
+ : public virtual TRefCounted
+{
+ virtual const TDriverConfigPtr& GetConfig() const = 0;
+ virtual const NApi::IClientPtr& GetClient() const = 0;
+ virtual const NApi::IClientPtr& GetRootClient() const = 0;
+ virtual NApi::IInternalClientPtr GetInternalClientOrThrow() const = 0;
+ virtual const IDriverPtr& GetDriver() const = 0;
+
+ virtual const TDriverRequest& Request() const = 0;
+
+ virtual const NFormats::TFormat& GetInputFormat() = 0;
+ virtual const NFormats::TFormat& GetOutputFormat() = 0;
+
+ virtual void ProduceOutputValue(const NYson::TYsonString& yson) = 0;
+ virtual NYson::TYsonString ConsumeInputValue() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ICommandContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Calls |context->ProduceOutputValue| with |TYsonString| created by the producer.
+void ProduceOutput(
+ ICommandContextPtr context,
+ const std::function<void(NYson::IYsonConsumer*)>& producer);
+
+//! Produces either nothing (v3) or empty map (>=v4).
+void ProduceEmptyOutput(ICommandContextPtr context);
+
+//! Run |producer| (v3) or open a map with key |name| and run |producer| then close map (>=v4).
+void ProduceSingleOutput(
+ ICommandContextPtr context,
+ TStringBuf name,
+ const std::function<void(NYson::IYsonConsumer*)>& producer);
+
+//! Produces either |value| (v3) or map {|name|=|value|} (>=v4).
+template <typename T>
+void ProduceSingleOutputValue(
+ ICommandContextPtr context,
+ TStringBuf name,
+ const T& value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCommandBase
+ : public virtual NYTree::TYsonSerializableLite
+ , public ICommand
+{
+protected:
+ NLogging::TLogger Logger = DriverLogger;
+
+ virtual void DoExecute(ICommandContextPtr context) = 0;
+ virtual bool HasResponseParameters() const;
+
+ TCommandBase();
+
+ void ProduceResponseParameters(
+ ICommandContextPtr context,
+ const std::function<void(NYson::IYsonConsumer*)>& producer);
+
+public:
+ void Execute(ICommandContextPtr context) override;
+};
+
+template <class TOptions>
+class TTypedCommandBase
+ : public TCommandBase
+{
+protected:
+ TOptions Options;
+
+};
+
+template <class TOptions, class = void>
+class TTransactionalCommandBase
+{ };
+
+template <class TOptions>
+class TTransactionalCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTransactionalOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TTransactionalCommandBase();
+ NApi::ITransactionPtr AttachTransaction(
+ ICommandContextPtr context,
+ bool required);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TMutatingCommandBase
+{ };
+
+template <class TOptions>
+class TMutatingCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TMutatingOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TMutatingCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TReadOnlyMasterCommandBase
+{ };
+
+template <class TOptions>
+class TReadOnlyMasterCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TMasterReadOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TReadOnlyMasterCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TReadOnlyTabletCommandBase
+{ };
+
+template <class TOptions>
+class TReadOnlyTabletCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTabletReadOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TReadOnlyTabletCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TSuppressableAccessTrackingCommandBase
+{ };
+
+template <class TOptions>
+class TSuppressableAccessTrackingCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TSuppressableAccessTrackingOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TSuppressableAccessTrackingCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TPrerequisiteCommandBase
+{ };
+
+template <class TOptions>
+class TPrerequisiteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TPrerequisiteOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TPrerequisiteCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions, class = void>
+class TTimeoutCommandBase
+{ };
+
+template <class TOptions>
+class TTimeoutCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TTimeoutOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TTimeoutCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTabletTransactionOptions
+{
+ NTransactionClient::TTransactionId TransactionId;
+};
+
+template <class TOptions, class = void>
+class TTabletReadCommandBase
+{ };
+
+template <class TOptions>
+class TTabletReadCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletTransactionOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TTabletReadCommandBase();
+ NApi::IClientBasePtr GetClientBase(ICommandContextPtr context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTabletWriteOptions
+ : public TTabletTransactionOptions
+{
+ NTransactionClient::EAtomicity Atomicity;
+ NTransactionClient::EDurability Durability;
+};
+
+struct TInsertRowsOptions
+ : public TTabletWriteOptions
+ , public NApi::TModifyRowsOptions
+{ };
+
+struct TDeleteRowsOptions
+ : public TTabletWriteOptions
+ , public NApi::TModifyRowsOptions
+{ };
+
+struct TLockRowsOptions
+ : public TTabletTransactionOptions
+{ };
+
+template <class TOptions, class = void>
+class TTabletWriteCommandBase
+{ };
+
+template <class TOptions>
+class TTabletWriteCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, TTabletWriteOptions&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TTabletWriteCommandBase();
+ NApi::ITransactionPtr GetTransaction(ICommandContextPtr context);
+ bool ShouldCommitTransaction();
+};
+
+template <class TOptions, class = void>
+class TSelectRowsCommandBase
+{ };
+
+template <class TOptions>
+class TSelectRowsCommandBase<
+ TOptions,
+ typename std::enable_if_t<std::is_convertible_v<TOptions&, NApi::TSelectRowsOptionsBase&>>
+>
+ : public virtual TTypedCommandBase<TOptions>
+{
+protected:
+ TSelectRowsCommandBase();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+class TTypedCommand
+ : public virtual TTypedCommandBase<TOptions>
+ , public TTransactionalCommandBase<TOptions>
+ , public TTabletReadCommandBase<TOptions>
+ , public TTabletWriteCommandBase<TOptions>
+ , public TMutatingCommandBase<TOptions>
+ , public TReadOnlyMasterCommandBase<TOptions>
+ , public TReadOnlyTabletCommandBase<TOptions>
+ , public TSuppressableAccessTrackingCommandBase<TOptions>
+ , public TPrerequisiteCommandBase<TOptions>
+ , public TTimeoutCommandBase<TOptions>
+ , public TSelectRowsCommandBase<TOptions>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
+#define COMMAND_INL_H
+#include "command-inl.h"
+#undef COMMAND_INL_H
diff --git a/yt/yt/client/driver/config.cpp b/yt/yt/client/driver/config.cpp
new file mode 100644
index 0000000000..14cf20a9e5
--- /dev/null
+++ b/yt/yt/client/driver/config.cpp
@@ -0,0 +1,76 @@
+#include "config.h"
+
+#include <yt/yt/client/api/config.h>
+
+#include <yt/yt/client/chunk_client/config.h>
+
+#include <yt/yt/client/table_client/config.h>
+
+#include <yt/yt/core/misc/cache_config.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDriverConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("file_reader", &TThis::FileReader)
+ .DefaultNew();
+ registrar.Parameter("file_writer", &TThis::FileWriter)
+ .DefaultNew();
+ registrar.Parameter("table_reader", &TThis::TableReader)
+ .DefaultNew();
+ registrar.Parameter("table_writer", &TThis::TableWriter)
+ .DefaultNew();
+ registrar.Parameter("journal_reader", &TThis::JournalReader)
+ .DefaultNew();
+ registrar.Parameter("journal_writer", &TThis::JournalWriter)
+ .DefaultNew();
+ registrar.Parameter("fetcher", &TThis::Fetcher)
+ .DefaultNew();
+ registrar.Parameter("chunk_fragment_reader", &TThis::ChunkFragmentReader)
+ .DefaultNew();
+
+ registrar.Parameter("read_buffer_row_count", &TThis::ReadBufferRowCount)
+ .Default(10'000);
+ registrar.Parameter("read_buffer_size", &TThis::ReadBufferSize)
+ .Default(1_MB);
+ registrar.Parameter("write_buffer_size", &TThis::WriteBufferSize)
+ .Default(1_MB);
+
+ registrar.Parameter("client_cache", &TThis::ClientCache)
+ .DefaultNew();
+
+ registrar.Parameter("api_version", &TThis::ApiVersion)
+ .Default(ApiVersion3)
+ .GreaterThanOrEqual(ApiVersion3)
+ .LessThanOrEqual(ApiVersion4);
+
+ registrar.Parameter("token", &TThis::Token)
+ .Optional();
+
+ registrar.Parameter("proxy_discovery_cache", &TThis::ProxyDiscoveryCache)
+ .DefaultNew();
+
+ registrar.Parameter("enable_internal_commands", &TThis::EnableInternalCommands)
+ .Default(false);
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->ClientCache->Capacity = 1024_KB;
+ config->ProxyDiscoveryCache->RefreshTime = TDuration::Seconds(15);
+ config->ProxyDiscoveryCache->ExpireAfterSuccessfulUpdateTime = TDuration::Seconds(15);
+ config->ProxyDiscoveryCache->ExpireAfterFailedUpdateTime = TDuration::Seconds(15);
+ });
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->ApiVersion != ApiVersion3 && config->ApiVersion != ApiVersion4) {
+ THROW_ERROR_EXCEPTION("Unsupported API version %v",
+ config->ApiVersion);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/config.h b/yt/yt/client/driver/config.h
new file mode 100644
index 0000000000..01585b0332
--- /dev/null
+++ b/yt/yt/client/driver/config.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/chunk_client/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int ApiVersion3 = 3;
+constexpr int ApiVersion4 = 4;
+
+class TDriverConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NApi::TFileReaderConfigPtr FileReader;
+ NApi::TFileWriterConfigPtr FileWriter;
+ NTableClient::TTableReaderConfigPtr TableReader;
+ NTableClient::TTableWriterConfigPtr TableWriter;
+ NApi::TJournalReaderConfigPtr JournalReader;
+ NApi::TJournalWriterConfigPtr JournalWriter;
+ NChunkClient::TFetcherConfigPtr Fetcher;
+ NChunkClient::TChunkFragmentReaderConfigPtr ChunkFragmentReader;
+ int ApiVersion;
+
+ i64 ReadBufferRowCount;
+ i64 ReadBufferSize;
+ i64 WriteBufferSize;
+
+ TSlruCacheConfigPtr ClientCache;
+
+ std::optional<TString> Token;
+
+ TAsyncExpiringCacheConfigPtr ProxyDiscoveryCache;
+
+ bool EnableInternalCommands;
+
+ REGISTER_YSON_STRUCT(TDriverConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDriverConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/cypress_commands.cpp b/yt/yt/client/driver/cypress_commands.cpp
new file mode 100644
index 0000000000..c41e328421
--- /dev/null
+++ b/yt/yt/client/driver/cypress_commands.cpp
@@ -0,0 +1,491 @@
+#include "cypress_commands.h"
+#include "config.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NObjectClient;
+using namespace NApi;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetCommand::TGetCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+ // TODO(babenko): rename to "limit"
+ RegisterParameter("max_size", Options.MaxSize)
+ .Optional();
+ RegisterParameter("return_only_value", ShouldReturnOnlyValue)
+ .Default(false);
+}
+
+void TGetCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Options = IAttributeDictionary::FromMap(GetUnrecognized());
+
+ auto asyncResult = context->GetClient()->GetNode(Path.GetPath(), Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ // XXX(levysotsky): Python client doesn't want to parse the returned |map_node|.
+ if (ShouldReturnOnlyValue) {
+ context->ProduceOutputValue(result);
+ } else {
+ ProduceSingleOutputValue(context, "value", result);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSetCommand::TSetCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+}
+
+void TSetCommand::DoExecute(ICommandContextPtr context)
+{
+ auto value = context->ConsumeInputValue();
+
+ auto asyncResult = context->GetClient()->SetNode(Path.GetPath(), value, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMultisetAttributesCommand::TMultisetAttributesCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TMultisetAttributesCommand::DoExecute(ICommandContextPtr context)
+{
+ auto attributes = ConvertTo<IMapNodePtr>(context->ConsumeInputValue());
+
+ auto asyncResult = context->GetClient()->MultisetAttributesNode(Path.GetPath(), attributes, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRemoveCommand::TRemoveCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+}
+
+void TRemoveCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->RemoveNode(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListCommand::TListCommand()
+{
+ RegisterParameter("path", Path);
+ // NB: default value is an empty filter in contrast to GetCommand, for which it is the universal filter.
+ // Refer to YT-5543 for details.
+ RegisterParameter("attributes", Options.Attributes)
+ .Default(TAttributeFilter({}, {}));
+ // TODO(babenko): rename to "limit"
+ RegisterParameter("max_size", Options.MaxSize)
+ .Optional();
+ RegisterParameter("return_only_value", ShouldReturnOnlyValue)
+ .Default(false);
+}
+
+void TListCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->ListNode(Path.GetPath(), Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ // XXX(levysotsky): Python client doesn't want to parse the returned |map_node|.
+ if (ShouldReturnOnlyValue) {
+ context->ProduceOutputValue(result);
+ } else {
+ ProduceSingleOutputValue(context, "value", result);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCreateCommand::Register(TRegistrar registrar)
+{
+ registrar.Parameter("type", &TThis::Type);
+}
+
+void TCreateCommand::Execute(ICommandContextPtr context)
+{
+ // For historical reasons, we handle both CreateNode and CreateObject requests
+ // in a single command. Here we route the request to an appropriate backend command.
+ Deserialize(*this, context->Request().Parameters);
+ auto backend = IsVersionedType(Type)
+ ? std::unique_ptr<ICommand>(new TCreateNodeCommand())
+ : std::unique_ptr<ICommand>(new TCreateObjectCommand());
+ backend->Execute(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCreateNodeCommand::TCreateNodeCommand()
+{
+ RegisterParameter("path", Path)
+ .Optional();
+ RegisterParameter("type", Type);
+ RegisterParameter("attributes", Attributes)
+ .Optional();
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("ignore_existing", Options.IgnoreExisting)
+ .Optional();
+ RegisterParameter("lock_existing", Options.LockExisting)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+ RegisterParameter("ignore_type_mismatch", Options.IgnoreTypeMismatch)
+ .Optional();
+}
+
+void TCreateNodeCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Attributes = Attributes
+ ? ConvertToAttributes(Attributes)
+ : CreateEphemeralAttributes();
+
+ auto asyncNodeId = context->GetClient()->CreateNode(Path.GetPath(), Type, Options);
+ auto nodeId = WaitFor(asyncNodeId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "node_id", nodeId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCreateObjectCommand::TCreateObjectCommand()
+{
+ RegisterParameter("type", Type);
+ RegisterParameter("ignore_existing", Options.IgnoreExisting)
+ .Optional();
+ RegisterParameter("sync", Options.Sync)
+ .Optional();
+ RegisterParameter("attributes", Attributes)
+ .Optional();
+}
+
+void TCreateObjectCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Attributes = Attributes
+ ? ConvertToAttributes(Attributes)
+ : CreateEphemeralAttributes();
+
+ auto asyncObjectId = context->GetClient()->CreateObject(
+ Type,
+ Options);
+ auto objectId = WaitFor(asyncObjectId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "object_id", objectId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLockCommand::TLockCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("mode", Mode)
+ .Default(NCypressClient::ELockMode::Exclusive);
+ RegisterParameter("waitable", Options.Waitable)
+ .Optional();
+ RegisterParameter("child_key", Options.ChildKey)
+ .Optional();
+ RegisterParameter("attribute_key", Options.AttributeKey)
+ .Optional();
+
+ RegisterPostprocessor([&] () {
+ if (Mode != NCypressClient::ELockMode::Shared) {
+ if (Options.ChildKey) {
+ THROW_ERROR_EXCEPTION("\"child_key\" can only be specified for shared locks");
+ }
+ if (Options.AttributeKey) {
+ THROW_ERROR_EXCEPTION("\"attribute_key\" can only be specified for shared locks");
+ }
+ }
+ if (Options.ChildKey && Options.AttributeKey) {
+ THROW_ERROR_EXCEPTION("Cannot specify both \"child_key\" and \"attribute_key\"");
+ }
+ });
+}
+
+void TLockCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncLockResult = context->GetClient()->LockNode(Path.GetPath(), Mode, Options);
+ auto lockResult = WaitFor(asyncLockResult)
+ .ValueOrThrow();
+
+ switch (context->GetConfig()->ApiVersion) {
+ case ApiVersion3:
+ ProduceSingleOutputValue(context, "lock_id", lockResult.LockId);
+ break;
+ default:
+ ProduceOutput(context, [&](NYson::IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("lock_id").Value(lockResult.LockId)
+ .Item("node_id").Value(lockResult.NodeId)
+ .Item("revision").Value(lockResult.Revision)
+ .EndMap();
+ });
+ break;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnlockCommand::TUnlockCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TUnlockCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncUnlockResult = context->GetClient()->UnlockNode(Path.GetPath(), Options);
+ WaitFor(asyncUnlockResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCopyCommand::TCopyCommand()
+{
+ RegisterParameter("source_path", SourcePath);
+ RegisterParameter("destination_path", DestinationPath);
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("ignore_existing", Options.IgnoreExisting)
+ .Optional();
+ RegisterParameter("lock_existing", Options.LockExisting)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+ RegisterParameter("preserve_account", Options.PreserveAccount)
+ .Optional();
+ RegisterParameter("preserve_creation_time", Options.PreserveCreationTime)
+ .Optional();
+ RegisterParameter("preserve_modification_time", Options.PreserveModificationTime)
+ // COMPAT(babenko)
+ .Alias("preserve_modifcation_time")
+ .Optional();
+ RegisterParameter("preserve_expiration_time", Options.PreserveExpirationTime)
+ .Optional();
+ RegisterParameter("preserve_expiration_timeout", Options.PreserveExpirationTimeout)
+ .Optional();
+ RegisterParameter("preserve_owner", Options.PreserveOwner)
+ .Optional();
+ RegisterParameter("preserve_acl", Options.PreserveAcl)
+ .Optional();
+ RegisterParameter("pessimistic_quota_check", Options.PessimisticQuotaCheck)
+ .Optional();
+}
+
+void TCopyCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncNodeId = context->GetClient()->CopyNode(
+ SourcePath.GetPath(),
+ DestinationPath.GetPath(),
+ Options);
+ auto nodeId = WaitFor(asyncNodeId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "node_id", nodeId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMoveCommand::TMoveCommand()
+{
+ RegisterParameter("source_path", SourcePath);
+ RegisterParameter("destination_path", DestinationPath);
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+ RegisterParameter("preserve_account", Options.PreserveAccount)
+ .Optional();
+ RegisterParameter("preserve_creation_time", Options.PreserveCreationTime)
+ .Optional();
+ RegisterParameter("preserve_modification_time", Options.PreserveModificationTime)
+ // COMPAT(babenko)
+ .Alias("preserve_modifcation_time")
+ .Optional();
+ RegisterParameter("preserve_expiration_time", Options.PreserveExpirationTime)
+ .Optional();
+ RegisterParameter("preserve_expiration_timeout", Options.PreserveExpirationTimeout)
+ .Optional();
+ RegisterParameter("preserve_owner", Options.PreserveOwner)
+ .Optional();
+ RegisterParameter("pessimistic_quota_check", Options.PessimisticQuotaCheck)
+ .Optional();
+}
+
+void TMoveCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncNodeId = context->GetClient()->MoveNode(
+ SourcePath.GetPath(),
+ DestinationPath.GetPath(),
+ Options);
+ auto nodeId = WaitFor(asyncNodeId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "node_id", nodeId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TExistsCommand::TExistsCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TExistsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->NodeExists(Path.GetPath(), Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "value", result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLinkCommand::TLinkCommand()
+{
+ RegisterParameter("link_path", LinkPath);
+ RegisterParameter("target_path", TargetPath);
+ RegisterParameter("attributes", Attributes)
+ .Optional();
+ RegisterParameter("recursive", Options.Recursive)
+ .Optional();
+ RegisterParameter("ignore_existing", Options.IgnoreExisting)
+ .Optional();
+ RegisterParameter("lock_existing", Options.LockExisting)
+ .Optional();
+ RegisterParameter("force", Options.Force)
+ .Optional();
+}
+
+void TLinkCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Attributes = Attributes
+ ? ConvertToAttributes(Attributes)
+ : CreateEphemeralAttributes();
+
+ auto asyncNodeId = context->GetClient()->LinkNode(
+ TargetPath.GetPath(),
+ LinkPath.GetPath(),
+ Options);
+ auto nodeId = WaitFor(asyncNodeId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "node_id", nodeId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConcatenateCommand::TConcatenateCommand()
+{
+ RegisterParameter("source_paths", SourcePaths);
+ RegisterParameter("destination_path", DestinationPath);
+
+ RegisterParameter("uniqualize_chunks", Options.UniqualizeChunks)
+ .Default(false);
+}
+
+void TConcatenateCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.ChunkMetaFetcherConfig = context->GetConfig()->Fetcher;
+
+ auto asyncResult = context->GetClient()->ConcatenateNodes(
+ SourcePaths,
+ DestinationPath,
+ Options);
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TExternalizeCommand::TExternalizeCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("cell_tag", CellTag);
+}
+
+void TExternalizeCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->ExternalizeNode(
+ Path,
+ CellTag,
+ Options);
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInternalizeCommand::TInternalizeCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TInternalizeCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->InternalizeNode(
+ Path,
+ Options);
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/cypress_commands.h b/yt/yt/client/driver/cypress_commands.h
new file mode 100644
index 0000000000..52bcd36097
--- /dev/null
+++ b/yt/yt/client/driver/cypress_commands.h
@@ -0,0 +1,266 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/cypress_client/public.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetCommand
+ : public TTypedCommand<NApi::TGetNodeOptions>
+{
+public:
+ TGetCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ bool ShouldReturnOnlyValue;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSetCommand
+ : public TTypedCommand<NApi::TSetNodeOptions>
+{
+public:
+ TSetCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMultisetAttributesCommand
+ : public TTypedCommand<NApi::TMultisetAttributesNodeOptions>
+{
+public:
+ TMultisetAttributesCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoveCommand
+ : public TTypedCommand<NApi::TRemoveNodeOptions>
+{
+public:
+ TRemoveCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListCommand
+ : public TTypedCommand<NApi::TListNodeOptions>
+{
+public:
+ TListCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ bool ShouldReturnOnlyValue;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCreateCommand
+ : public NYTree::TYsonStructLite
+{
+public:
+ void Execute(ICommandContextPtr context);
+
+ REGISTER_YSON_STRUCT_LITE(TCreateCommand);
+
+ static void Register(TRegistrar);
+
+private:
+ NObjectClient::EObjectType Type;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCreateNodeCommand
+ : public TTypedCommand<NApi::TCreateNodeOptions>
+{
+public:
+ TCreateNodeCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NObjectClient::EObjectType Type;
+ NYTree::INodePtr Attributes;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCreateObjectCommand
+ : public TTypedCommand<NApi::TCreateObjectOptions>
+{
+public:
+ TCreateObjectCommand();
+
+private:
+ NObjectClient::EObjectType Type;
+ NYTree::INodePtr Attributes;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLockCommand
+ : public TTypedCommand<NApi::TLockNodeOptions>
+{
+public:
+ TLockCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NCypressClient::ELockMode Mode;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnlockCommand
+ : public TTypedCommand<NApi::TUnlockNodeOptions>
+{
+public:
+ TUnlockCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCopyCommand
+ : public TTypedCommand<NApi::TCopyNodeOptions>
+{
+public:
+ TCopyCommand();
+
+private:
+ NYPath::TRichYPath SourcePath;
+ NYPath::TRichYPath DestinationPath;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMoveCommand
+ : public TTypedCommand<NApi::TMoveNodeOptions>
+{
+public:
+ TMoveCommand();
+
+private:
+ NYPath::TRichYPath SourcePath;
+ NYPath::TRichYPath DestinationPath;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TExistsCommand
+ : public TTypedCommand<NApi::TNodeExistsOptions>
+{
+public:
+ TExistsCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLinkCommand
+ : public TTypedCommand<NApi::TLinkNodeOptions>
+{
+public:
+ TLinkCommand();
+
+private:
+ NYPath::TRichYPath LinkPath;
+ NYPath::TRichYPath TargetPath;
+ NYTree::INodePtr Attributes;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConcatenateCommand
+ : public TTypedCommand<NApi::TConcatenateNodesOptions>
+{
+public:
+ TConcatenateCommand();
+
+private:
+ std::vector<NYPath::TRichYPath> SourcePaths;
+ NYPath::TRichYPath DestinationPath;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TExternalizeCommand
+ : public TTypedCommand<NApi::TExternalizeNodeOptions>
+{
+public:
+ TExternalizeCommand();
+
+private:
+ NYPath::TYPath Path;
+ NObjectClient::TCellTag CellTag;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInternalizeCommand
+ : public TTypedCommand<NApi::TInternalizeNodeOptions>
+{
+public:
+ TInternalizeCommand();
+
+private:
+ NYPath::TYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/driver.cpp b/yt/yt/client/driver/driver.cpp
new file mode 100644
index 0000000000..3329cb2672
--- /dev/null
+++ b/yt/yt/client/driver/driver.cpp
@@ -0,0 +1,682 @@
+#include "driver.h"
+
+#include "authentication_commands.h"
+#include "chaos_commands.h"
+#include "command.h"
+#include "config.h"
+#include "cypress_commands.h"
+#include "admin_commands.h"
+#include "etc_commands.h"
+#include "file_commands.h"
+#include "journal_commands.h"
+#include "queue_commands.h"
+#include "scheduler_commands.h"
+#include "table_commands.h"
+#include "transaction_commands.h"
+#include "internal_commands.h"
+#include "proxy_discovery_cache.h"
+#include "query_commands.h"
+
+#include <yt/yt/client/api/transaction.h>
+#include <yt/yt/client/api/connection.h>
+#include <yt/yt/client/api/sticky_transaction_pool.h>
+#include <yt/yt/client/api/client_cache.h>
+
+#include <yt/yt/client/api/rpc_proxy/connection_impl.h>
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt/core/yson/null_consumer.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/library/tvm/tvm_base.h>
+
+namespace NYT::NDriver {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NRpc;
+using namespace NElection;
+using namespace NTransactionClient;
+using namespace NChunkClient;
+using namespace NScheduler;
+using namespace NFormats;
+using namespace NSecurityClient;
+using namespace NConcurrency;
+using namespace NHydra;
+using namespace NHiveClient;
+using namespace NTabletClient;
+using namespace NApi;
+using namespace NNodeTrackerClient;
+using namespace NTracing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = DriverLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TCommandDescriptor& descriptor, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("name").Value(descriptor.CommandName)
+ .Item("input_type").Value(descriptor.InputType)
+ .Item("output_type").Value(descriptor.OutputType)
+ .Item("is_volatile").Value(descriptor.Volatile)
+ .Item("is_heavy").Value(descriptor.Heavy)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDriverRequest::TDriverRequest()
+ : ResponseParametersConsumer(GetNullYsonConsumer())
+{ }
+
+TDriverRequest::TDriverRequest(THolderPtr holder)
+ : ResponseParametersConsumer(GetNullYsonConsumer())
+ , Holder_(std::move(holder))
+{ }
+
+void TDriverRequest::Reset()
+{
+ Holder_.Reset();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCommandDescriptor IDriver::GetCommandDescriptor(const TString& commandName) const
+{
+ auto descriptor = FindCommandDescriptor(commandName);
+ YT_VERIFY(descriptor);
+ return *descriptor;
+}
+
+TCommandDescriptor IDriver::GetCommandDescriptorOrThrow(const TString& commandName) const
+{
+ auto descriptor = FindCommandDescriptor(commandName);
+ if (!descriptor) {
+ THROW_ERROR_EXCEPTION("Unknown command %Qv", commandName);
+ }
+ return *descriptor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+class TDriver
+ : public IDriver
+{
+public:
+ TDriver(TDriverConfigPtr config, IConnectionPtr connection)
+ : Config_(std::move(config))
+ , Connection_(std::move(connection))
+ , ClientCache_(New<TClientCache>(Config_->ClientCache, Connection_))
+ , RootClient_(ClientCache_->Get(
+ GetRootAuthenticationIdentity(),
+ TClientOptions::FromAuthenticationIdentity(GetRootAuthenticationIdentity())))
+ , ProxyDiscoveryCache_(CreateProxyDiscoveryCache(
+ Config_->ProxyDiscoveryCache,
+ RootClient_))
+ , StickyTransactionPool_(CreateStickyTransactionPool(Logger))
+ {
+ // Register all commands.
+#define REGISTER(command, name, inDataType, outDataType, isVolatile, isHeavy, version) \
+ if (version == Config_->ApiVersion) { \
+ RegisterCommand<command>( \
+ TCommandDescriptor{name, EDataType::inDataType, EDataType::outDataType, isVolatile, isHeavy}); \
+ }
+#define REGISTER_ALL(command, name, inDataType, outDataType, isVolatile, isHeavy) \
+ RegisterCommand<command>( \
+ TCommandDescriptor{name, EDataType::inDataType, EDataType::outDataType, isVolatile, isHeavy}); \
+
+ REGISTER (TStartTransactionCommand, "start_tx", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TPingTransactionCommand, "ping_tx", Null, Null, true, false, ApiVersion3);
+ REGISTER (TCommitTransactionCommand, "commit_tx", Null, Null, true, false, ApiVersion3);
+ REGISTER (TAbortTransactionCommand, "abort_tx", Null, Null, true, false, ApiVersion3);
+
+ REGISTER (TStartTransactionCommand, "start_transaction", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TPingTransactionCommand, "ping_transaction", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TCommitTransactionCommand, "commit_transaction", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TAbortTransactionCommand, "abort_transaction", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER_ALL(TGenerateTimestampCommand, "generate_timestamp", Null, Structured, false, false);
+
+ REGISTER_ALL(TCreateCommand, "create", Null, Structured, true, false);
+ REGISTER_ALL(TGetCommand, "get", Null, Structured, false, false);
+ REGISTER_ALL(TListCommand, "list", Null, Structured, false, false);
+ REGISTER_ALL(TLockCommand, "lock", Null, Structured, true, false);
+
+ REGISTER (TUnlockCommand, "unlock", Null, Null, true, false, ApiVersion3);
+ REGISTER (TUnlockCommand, "unlock", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER_ALL(TCopyCommand, "copy", Null, Structured, true, false);
+ REGISTER_ALL(TMoveCommand, "move", Null, Structured, true, false);
+ REGISTER_ALL(TLinkCommand, "link", Null, Structured, true, false);
+ REGISTER_ALL(TExistsCommand, "exists", Null, Structured, false, false);
+
+ REGISTER (TConcatenateCommand, "concatenate", Null, Null, true, false, ApiVersion3);
+ REGISTER (TRemoveCommand, "remove", Null, Null, true, false, ApiVersion3);
+ REGISTER (TSetCommand, "set", Structured, Null, true, false, ApiVersion3);
+
+ REGISTER (TConcatenateCommand, "concatenate", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TRemoveCommand, "remove", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TSetCommand, "set", Structured, Structured, true, false, ApiVersion4);
+ REGISTER (TMultisetAttributesCommand, "multiset_attributes", Structured, Structured, true, false, ApiVersion4);
+ REGISTER (TExternalizeCommand, "externalize", Null, Null, true, false, ApiVersion4);
+ REGISTER (TInternalizeCommand, "internalize", Null, Null, true, false, ApiVersion4);
+
+ REGISTER (TWriteFileCommand, "write_file", Binary, Null, true, true, ApiVersion3);
+ REGISTER (TWriteFileCommand, "write_file", Binary, Structured, true, true, ApiVersion4);
+ REGISTER_ALL(TReadFileCommand, "read_file", Null, Binary, false, true );
+
+ REGISTER_ALL(TGetFileFromCacheCommand, "get_file_from_cache", Null, Structured, false, false);
+ REGISTER_ALL(TPutFileToCacheCommand, "put_file_to_cache", Null, Structured, true, false);
+
+ REGISTER (TWriteTableCommand, "write_table", Tabular, Null, true, true , ApiVersion3);
+ REGISTER (TWriteTableCommand, "write_table", Tabular, Structured, true, true , ApiVersion4);
+ REGISTER_ALL(TGetTableColumnarStatisticsCommand, "get_table_columnar_statistics", Null, Structured, false, false);
+ REGISTER_ALL(TReadTableCommand, "read_table", Null, Tabular, false, true );
+ REGISTER_ALL(TReadBlobTableCommand, "read_blob_table", Null, Binary, false, true );
+ REGISTER_ALL(TLocateSkynetShareCommand, "locate_skynet_share", Null, Structured, false, true );
+
+ REGISTER_ALL(TPartitionTablesCommand, "partition_tables", Null, Structured, false, false);
+
+ REGISTER (TInsertRowsCommand, "insert_rows", Tabular, Null, true, true , ApiVersion3);
+ REGISTER (TLockRowsCommand, "lock_rows", Tabular, Null, true, true , ApiVersion3);
+ REGISTER (TDeleteRowsCommand, "delete_rows", Tabular, Null, true, true , ApiVersion3);
+ REGISTER (TTrimRowsCommand, "trim_rows", Null, Null, true, true , ApiVersion3);
+
+ REGISTER (TInsertRowsCommand, "insert_rows", Tabular, Structured, true, true , ApiVersion4);
+ REGISTER (TLockRowsCommand, "lock_rows", Tabular, Structured, true, true , ApiVersion4);
+ REGISTER (TDeleteRowsCommand, "delete_rows", Tabular, Structured, true, true , ApiVersion4);
+ REGISTER (TTrimRowsCommand, "trim_rows", Null, Structured, true, true , ApiVersion4);
+
+ REGISTER_ALL(TExplainQueryCommand, "explain_query", Null, Structured, false, true );
+ REGISTER_ALL(TSelectRowsCommand, "select_rows", Null, Tabular, false, true );
+ REGISTER_ALL(TLookupRowsCommand, "lookup_rows", Tabular, Tabular, false, true );
+ REGISTER_ALL(TPullRowsCommand, "pull_rows", Null, Tabular, false, true );
+
+ REGISTER (TEnableTableReplicaCommand, "enable_table_replica", Null, Null, true, false, ApiVersion3);
+ REGISTER (TDisableTableReplicaCommand, "disable_table_replica", Null, Null, true, false, ApiVersion3);
+ REGISTER (TAlterTableReplicaCommand, "alter_table_replica", Null, Null, true, false, ApiVersion3);
+
+ REGISTER (TEnableTableReplicaCommand, "enable_table_replica", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TDisableTableReplicaCommand, "disable_table_replica", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TAlterTableReplicaCommand, "alter_table_replica", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER_ALL(TGetInSyncReplicasCommand, "get_in_sync_replicas", Tabular, Structured, false, true );
+
+ REGISTER (TMountTableCommand, "mount_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TUnmountTableCommand, "unmount_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TRemountTableCommand, "remount_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TFreezeTableCommand, "freeze_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TUnfreezeTableCommand, "unfreeze_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TReshardTableCommand, "reshard_table", Null, Null, true, false, ApiVersion3);
+ REGISTER (TAlterTableCommand, "alter_table", Null, Null, true, false, ApiVersion3);
+
+ REGISTER (TMountTableCommand, "mount_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TUnmountTableCommand, "unmount_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TRemountTableCommand, "remount_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TFreezeTableCommand, "freeze_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TUnfreezeTableCommand, "unfreeze_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TReshardTableCommand, "reshard_table", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TAlterTableCommand, "alter_table", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER (TGetTablePivotKeysCommand, "get_table_pivot_keys", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TGetTabletInfosCommand, "get_tablet_infos", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TGetTabletErrorsCommand, "get_tablet_errors", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER (TCreateTableBackupCommand, "create_table_backup", Null, Null, true, false, ApiVersion3);
+ REGISTER (TRestoreTableBackupCommand, "restore_table_backup", Null, Null, true, false, ApiVersion3);
+ REGISTER (TCreateTableBackupCommand, "create_table_backup", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TRestoreTableBackupCommand, "restore_table_backup", Null, Structured, true, false, ApiVersion4);
+
+
+ REGISTER_ALL(TReshardTableAutomaticCommand, "reshard_table_automatic", Null, Structured, true, false);
+ REGISTER_ALL(TBalanceTabletCellsCommand, "balance_tablet_cells", Null, Structured, true, false);
+
+ REGISTER (TUpdateChaosTableReplicaProgressCommand, "update_replication_progress", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TAlterReplicationCardCommand, "alter_replication_card", Null, Structured, false, false, ApiVersion4);
+
+ REGISTER (TMergeCommand, "merge", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TEraseCommand, "erase", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TMapCommand, "map", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TSortCommand, "sort", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TReduceCommand, "reduce", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TJoinReduceCommand, "join_reduce", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TMapReduceCommand, "map_reduce", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TRemoteCopyCommand, "remote_copy", Null, Structured, true, false, ApiVersion3);
+
+ REGISTER (TStartOperationCommand, "start_op", Null, Structured, true, false, ApiVersion3);
+ REGISTER (TAbortOperationCommand, "abort_op", Null, Null, true, false, ApiVersion3);
+ REGISTER (TSuspendOperationCommand, "suspend_op", Null, Null, true, false, ApiVersion3);
+ REGISTER (TResumeOperationCommand, "resume_op", Null, Null, true, false, ApiVersion3);
+ REGISTER (TCompleteOperationCommand, "complete_op", Null, Null, true, false, ApiVersion3);
+ REGISTER (TUpdateOperationParametersCommand, "update_op_parameters", Null, Null, true, false, ApiVersion3);
+
+ REGISTER (TStartOperationCommand, "start_operation", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TAbortOperationCommand, "abort_operation", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TSuspendOperationCommand, "suspend_operation", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TResumeOperationCommand, "resume_operation", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TCompleteOperationCommand, "complete_operation", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TUpdateOperationParametersCommand, "update_operation_parameters", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER_ALL(TParseYPathCommand, "parse_ypath", Null, Structured, false, false);
+
+ REGISTER (TAddMemberCommand, "add_member", Null, Null, true, false, ApiVersion3);
+ REGISTER (TRemoveMemberCommand, "remove_member", Null, Null, true, false, ApiVersion3);
+
+ REGISTER (TAddMemberCommand, "add_member", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TRemoveMemberCommand, "remove_member", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER_ALL(TCheckPermissionCommand, "check_permission", Null, Structured, false, false);
+ REGISTER_ALL(TCheckPermissionByAclCommand, "check_permission_by_acl", Null, Structured, false, false);
+
+ REGISTER (TTransferAccountResourcesCommand, "transfer_account_resources", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TTransferPoolResourcesCommand, "transfer_pool_resources", Null, Structured, true, false, ApiVersion4);
+
+ REGISTER (TWriteJournalCommand, "write_journal", Tabular, Null, true, true , ApiVersion3);
+ REGISTER (TWriteJournalCommand, "write_journal", Tabular, Structured, true, true , ApiVersion4);
+ REGISTER_ALL(TReadJournalCommand, "read_journal", Null, Tabular, false, true );
+ REGISTER (TTruncateJournalCommand, "truncate_journal", Null, Null, true, false, ApiVersion4);
+
+ REGISTER_ALL(TGetJobInputCommand, "get_job_input", Null, Binary, false, true );
+ REGISTER_ALL(TGetJobInputPathsCommand, "get_job_input_paths", Null, Structured, false, true );
+ REGISTER_ALL(TGetJobStderrCommand, "get_job_stderr", Null, Binary, false, true );
+ REGISTER_ALL(TGetJobFailContextCommand, "get_job_fail_context", Null, Binary, false, true );
+ REGISTER_ALL(TGetJobSpecCommand, "get_job_spec", Null, Structured, false, true );
+ REGISTER_ALL(TListOperationsCommand, "list_operations", Null, Structured, false, false);
+ REGISTER_ALL(TListJobsCommand, "list_jobs", Null, Structured, false, false);
+ REGISTER_ALL(TGetJobCommand, "get_job", Null, Structured, false, false);
+ REGISTER_ALL(TPollJobShellCommand, "poll_job_shell", Null, Structured, true, false);
+ REGISTER_ALL(TGetOperationCommand, "get_operation", Null, Structured, false, false);
+
+ REGISTER (TDumpJobContextCommand, "dump_job_context", Null, Null, true, false, ApiVersion3);
+ REGISTER (TAbandonJobCommand, "abandon_job", Null, Null, false, false, ApiVersion3);
+ REGISTER (TAbortJobCommand, "abort_job", Null, Null, false, false, ApiVersion3);
+
+ REGISTER (TDumpJobContextCommand, "dump_job_context", Null, Structured, true, false, ApiVersion4);
+ REGISTER (TAbandonJobCommand, "abandon_job", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TAbortJobCommand, "abort_job", Null, Structured, false, false, ApiVersion4);
+
+ REGISTER_ALL(TGetVersionCommand, "get_version", Null, Structured, false, false);
+ REGISTER_ALL(TGetSupportedFeaturesCommand, "get_supported_features", Null, Structured, false, false);
+
+ REGISTER_ALL(TExecuteBatchCommand, "execute_batch", Null, Structured, true, false);
+
+ REGISTER (TDiscoverProxiesCommand, "discover_proxies", Null, Structured, false, false, ApiVersion4);
+
+ REGISTER_ALL(TBuildSnapshotCommand, "build_snapshot", Null, Structured, true, false);
+ REGISTER_ALL(TBuildMasterSnapshotsCommand, "build_master_snapshots", Null, Structured, true, false);
+ REGISTER_ALL(TSwitchLeaderCommand, "switch_leader", Null, Structured, true, false);
+ REGISTER_ALL(TResetStateHashCommand, "reset_state_hash", Null, Structured, true, false);
+ REGISTER_ALL(THealExecNodeCommand, "heal_exec_node", Null, Structured, true, false);
+
+ REGISTER_ALL(TSuspendCoordinatorCommand, "suspend_coordinator", Null, Structured, false, false);
+ REGISTER_ALL(TResumeCoordinatorCommand, "resume_coordinator", Null, Structured, false, false);
+ REGISTER_ALL(TMigrateReplicationCardsCommand, "migrate_replication_cards", Null, Structured, false, false);
+ REGISTER_ALL(TSuspendChaosCellsCommand, "suspend_chaos_cells", Null, Structured, false, false);
+ REGISTER_ALL(TResumeChaosCellsCommand, "resume_chaos_cells", Null, Structured, false, false);
+
+ REGISTER_ALL(TSuspendTabletCellsCommand, "suspend_tablet_cells", Null, Structured, false, false);
+ REGISTER_ALL(TResumeTabletCellsCommand, "resume_tablet_cells", Null, Structured, false, false);
+
+ REGISTER_ALL(TAddMaintenanceCommand, "add_maintenance", Null, Structured, true, false);
+ REGISTER_ALL(TRemoveMaintenanceCommand, "remove_maintenance", Null, Structured, true, false);
+ REGISTER_ALL(TDisableChunkLocationsCommand, "disable_chunk_locations", Null, Structured, false, false);
+ REGISTER_ALL(TDestroyChunkLocationsCommand, "destroy_chunk_locations", Null, Structured, false, false);
+ REGISTER_ALL(TResurrectChunkLocationsCommand, "resurrect_chunk_locations", Null, Structured, false, false);
+
+ REGISTER_ALL(TSetUserPasswordCommand, "set_user_password", Null, Structured, false, false);
+ REGISTER_ALL(TIssueTokenCommand, "issue_token", Null, Structured, false, false);
+ REGISTER_ALL(TRevokeTokenCommand, "revoke_token", Null, Structured, false, false);
+ REGISTER_ALL(TListUserTokensCommand, "list_user_tokens", Null, Structured, false, false);
+
+ REGISTER (TRegisterQueueConsumerCommand, "register_queue_consumer", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TUnregisterQueueConsumerCommand, "unregister_queue_consumer", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TListQueueConsumerRegistrationsCommand, "list_queue_consumer_registrations", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TPullQueueCommand, "pull_queue", Null, Tabular, false, true, ApiVersion4);
+ REGISTER (TPullConsumerCommand, "pull_consumer", Null, Tabular, false, true, ApiVersion4);
+ REGISTER (TAdvanceConsumerCommand, "advance_consumer", Null, Structured, true, true, ApiVersion4);
+
+ REGISTER (TStartQueryCommand, "start_query", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TAbortQueryCommand, "abort_query", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TGetQueryCommand, "get_query", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TListQueriesCommand, "list_queries", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TGetQueryResultCommand, "get_query_result", Null, Structured, false, false, ApiVersion4);
+ REGISTER (TReadQueryResultCommand, "read_query_result", Null, Tabular, false, true, ApiVersion4);
+ REGISTER (TAlterQueryCommand, "alter_query", Null, Tabular, false, false, ApiVersion4);
+
+ if (Config_->EnableInternalCommands) {
+ REGISTER_ALL(TReadHunksCommand, "read_hunks", Null, Structured, false, true );
+ REGISTER_ALL(TWriteHunksCommand, "write_hunks", Null, Structured, false, true );
+ REGISTER_ALL(TLockHunkStoreCommand, "lock_hunk_store", Null, Structured, false, true );
+ REGISTER_ALL(TUnlockHunkStoreCommand, "unlock_hunk_store", Null, Structured, false, true );
+ REGISTER_ALL(TGetConnectionConfigCommand, "get_connection_config", Null, Structured, false, false);
+ }
+
+#undef REGISTER
+#undef REGISTER_ALL
+ }
+
+ TFuture<void> Execute(const TDriverRequest& request) override
+ {
+ auto traceContext = CreateTraceContextFromCurrent("Driver");
+ TTraceContextGuard guard(std::move(traceContext));
+
+ auto it = CommandNameToEntry_.find(request.CommandName);
+ if (it == CommandNameToEntry_.end()) {
+ return MakeFuture(TError(
+ "Unknown command %Qv",
+ request.CommandName));
+ }
+
+ const auto& entry = it->second;
+ TAuthenticationIdentity identity(
+ request.AuthenticatedUser,
+ request.UserTag.value_or(""));
+
+ YT_VERIFY(entry.Descriptor.InputType == EDataType::Null || request.InputStream);
+ YT_VERIFY(entry.Descriptor.OutputType == EDataType::Null || request.OutputStream);
+
+ YT_LOG_DEBUG("Command received (RequestId: %" PRIx64 ", Command: %v, User: %v)",
+ request.Id,
+ request.CommandName,
+ identity.User);
+
+ auto options = TClientOptions::FromAuthenticationIdentity(identity);
+ options.Token = request.UserToken;
+ options.ServiceTicketAuth = request.ServiceTicket
+ ? std::make_optional(New<NAuth::TServiceTicketFixedAuth>(*request.ServiceTicket))
+ : std::nullopt;
+
+ auto client = ClientCache_->Get(identity, options);
+
+ auto context = New<TCommandContext>(
+ this,
+ std::move(client),
+ RootClient_,
+ Config_,
+ entry.Descriptor,
+ request);
+
+ return BIND(&TDriver::DoExecute, entry.Execute, context)
+ .AsyncVia(Connection_->GetInvoker())
+ .Run();
+ }
+
+ std::optional<TCommandDescriptor> FindCommandDescriptor(const TString& commandName) const override
+ {
+ auto it = CommandNameToEntry_.find(commandName);
+ return it == CommandNameToEntry_.end() ? std::nullopt : std::make_optional(it->second.Descriptor);
+ }
+
+ const std::vector<TCommandDescriptor> GetCommandDescriptors() const override
+ {
+ std::vector<TCommandDescriptor> result;
+ result.reserve(CommandNameToEntry_.size());
+ for (const auto& [name, entry] : CommandNameToEntry_) {
+ result.push_back(entry.Descriptor);
+ }
+ return result;
+ }
+
+ void ClearMetadataCaches() override
+ {
+ ClientCache_->Clear();
+ Connection_->ClearMetadataCaches();
+ }
+
+ IStickyTransactionPoolPtr GetStickyTransactionPool() override
+ {
+ return StickyTransactionPool_;
+ }
+
+ IProxyDiscoveryCachePtr GetProxyDiscoveryCache() override
+ {
+ return ProxyDiscoveryCache_;
+ }
+
+ IConnectionPtr GetConnection() override
+ {
+ return Connection_;
+ }
+
+ void Terminate() override
+ {
+ // TODO(ignat): find and eliminate reference loop.
+ // Reset of the connection should be sufficient to release this connection.
+ // But there is some reference loop and it does not work.
+
+ ClearMetadataCaches();
+
+ // Release the connection with entire thread pools.
+ if (Connection_) {
+ Connection_->Terminate();
+ ClientCache_.Reset();
+ ProxyDiscoveryCache_.Reset();
+ Connection_.Reset();
+ }
+ }
+
+private:
+ const TDriverConfigPtr Config_;
+ IConnectionPtr Connection_;
+ TClientCachePtr ClientCache_;
+ const IClientPtr RootClient_;
+ IProxyDiscoveryCachePtr ProxyDiscoveryCache_;
+
+ class TCommandContext;
+ typedef TIntrusivePtr<TCommandContext> TCommandContextPtr;
+ typedef TCallback<void(ICommandContextPtr)> TExecuteCallback;
+
+ const IStickyTransactionPoolPtr StickyTransactionPool_;
+
+ struct TCommandEntry
+ {
+ TCommandDescriptor Descriptor;
+ TExecuteCallback Execute;
+ };
+
+ THashMap<TString, TCommandEntry> CommandNameToEntry_;
+
+
+ template <class TCommand>
+ void RegisterCommand(const TCommandDescriptor& descriptor)
+ {
+ TCommandEntry entry;
+ entry.Descriptor = descriptor;
+ entry.Execute = BIND_NO_PROPAGATE([] (ICommandContextPtr context) {
+ if constexpr (std::is_convertible<TCommand*, TYsonStructLite*>::value) {
+ TCommand command = TCommand::Create();
+ command.Execute(context);
+ } else {
+ TCommand command;
+ command.Execute(context);
+ }
+ });
+ YT_VERIFY(CommandNameToEntry_.emplace(descriptor.CommandName, entry).second);
+ }
+
+ static void DoExecute(TExecuteCallback executeCallback, TCommandContextPtr context)
+ {
+ const auto& request = context->Request();
+
+ auto Logger = DriverLogger;
+ if (request.LoggingTags) {
+ Logger.WithRawTag(*request.LoggingTags);
+ }
+
+ NTracing::TChildTraceContextGuard commandSpan(ConcatToString(TStringBuf("Driver:"), request.CommandName));
+ NTracing::AnnotateTraceContext([&] (const auto& traceContext) {
+ traceContext->AddTag("user", request.AuthenticatedUser);
+ traceContext->AddTag("request_id", request.Id);
+ });
+
+ YT_LOG_DEBUG("Command started (RequestId: %" PRIx64 ", Command: %v, User: %v)",
+ request.Id,
+ request.CommandName,
+ request.AuthenticatedUser);
+
+ TError result;
+ try {
+ executeCallback.Run(context);
+ } catch (const std::exception& ex) {
+ result = TError(ex);
+ }
+
+ if (result.IsOK()) {
+ YT_LOG_DEBUG("Command completed (RequestId: %" PRIx64 ", Command: %v, User: %v)",
+ request.Id,
+ request.CommandName,
+ request.AuthenticatedUser);
+ } else {
+ YT_LOG_DEBUG(result, "Command failed (RequestId: %" PRIx64 ", Command: %v, User: %v)",
+ request.Id,
+ request.CommandName,
+ request.AuthenticatedUser);
+ }
+
+ context->Finish();
+
+ THROW_ERROR_EXCEPTION_IF_FAILED(result);
+ }
+
+ class TCommandContext
+ : public ICommandContext
+ {
+ public:
+ TCommandContext(
+ IDriverPtr driver,
+ IClientPtr client,
+ IClientPtr rootClient,
+ TDriverConfigPtr config,
+ TCommandDescriptor descriptor,
+ const TDriverRequest& request)
+ : Driver_(std::move(driver))
+ , Client_(std::move(client))
+ , RootClient_(std::move(rootClient))
+ , Config_(std::move(config))
+ , Descriptor_(std::move(descriptor))
+ , Request_(std::move(request))
+ { }
+
+ const TDriverConfigPtr& GetConfig() const override
+ {
+ return Config_;
+ }
+
+ const IClientPtr& GetClient() const override
+ {
+ return Client_;
+ }
+
+ const IClientPtr& GetRootClient() const override
+ {
+ return RootClient_;
+ }
+
+ IInternalClientPtr GetInternalClientOrThrow() const override
+ {
+ auto internalClient = DynamicPointerCast<IInternalClient>(Client_);
+ if (!internalClient) {
+ THROW_ERROR_EXCEPTION("Client does not support internal API");
+ }
+ return internalClient;
+ }
+
+ const IDriverPtr& GetDriver() const override
+ {
+ return Driver_;
+ }
+
+ const TDriverRequest& Request() const override
+ {
+ return Request_;
+ }
+
+ const TFormat& GetInputFormat() override
+ {
+ if (!InputFormat_) {
+ InputFormat_ = ConvertTo<TFormat>(Request_.Parameters->GetChildOrThrow("input_format"));
+ }
+ return *InputFormat_;
+ }
+
+ const TFormat& GetOutputFormat() override
+ {
+ if (!OutputFormat_) {
+ OutputFormat_ = ConvertTo<TFormat>(Request_.Parameters->GetChildOrThrow("output_format"));
+ }
+ return *OutputFormat_;
+ }
+
+ TYsonString ConsumeInputValue() override
+ {
+ YT_VERIFY(Request_.InputStream);
+ auto syncInputStream = CreateSyncAdapter(Request_.InputStream);
+
+ auto producer = CreateProducerForFormat(
+ GetInputFormat(),
+ Descriptor_.InputType,
+ syncInputStream.get());
+
+ return ConvertToYsonString(producer);
+ }
+
+ void ProduceOutputValue(const TYsonString& yson) override
+ {
+ YT_VERIFY(Request_.OutputStream);
+ auto syncOutputStream = CreateBufferedSyncAdapter(Request_.OutputStream);
+
+ auto consumer = CreateConsumerForFormat(
+ GetOutputFormat(),
+ Descriptor_.OutputType,
+ syncOutputStream.get());
+
+ Serialize(yson, consumer.get());
+
+ consumer->Flush();
+ syncOutputStream->Flush();
+ }
+
+ void Finish()
+ {
+ Request_.Reset();
+ }
+
+ private:
+ const IDriverPtr Driver_;
+ const IClientPtr Client_;
+ const IClientPtr RootClient_;
+ const TDriverConfigPtr Config_;
+ const TCommandDescriptor Descriptor_;
+
+ TDriverRequest Request_;
+
+ std::optional<TFormat> InputFormat_;
+ std::optional<TFormat> OutputFormat_;
+ };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDriverPtr CreateDriver(
+ IConnectionPtr connection,
+ TDriverConfigPtr config)
+{
+ YT_VERIFY(connection);
+ YT_VERIFY(config);
+
+ return New<TDriver>(
+ std::move(config),
+ std::move(connection));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/driver.h b/yt/yt/client/driver/driver.h
new file mode 100644
index 0000000000..00a40969fb
--- /dev/null
+++ b/yt/yt/client/driver/driver.h
@@ -0,0 +1,167 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/formats/format.h>
+
+#include <yt/yt/client/security_client/public.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An instance of driver request.
+struct TDriverRequest
+{
+ TDriverRequest();
+ explicit TDriverRequest(TRefCountedPtr holder);
+
+ //! Request identifier to be logged.
+ std::variant<ui64, TGuid> Id = static_cast<ui64>(0);
+
+ //! Command name to execute.
+ TString CommandName;
+
+ //! Stream used for reading command input.
+ //! The stream must stay alive for the duration of #IDriver::Execute.
+ NConcurrency::IAsyncInputStreamPtr InputStream;
+
+ //! Stream where the command output is written.
+ //! The stream must stay alive for the duration of #IDriver::Execute.
+ NConcurrency::IFlushableAsyncOutputStreamPtr OutputStream;
+
+ //! A map containing command parameters.
+ NYTree::IMapNodePtr Parameters;
+
+ //! Name of the user issuing the request.
+ TString AuthenticatedUser = NSecurityClient::RootUserName;
+
+ //! Provides an additional annotation to differentiate between
+ //! various clients that authenticate via the same effective user.
+ std::optional<TString> UserTag;
+
+ //! Filled in the context of http proxy.
+ std::optional<NNet::TNetworkAddress> UserRemoteAddress;
+
+ //! User token.
+ std::optional<TString> UserToken;
+
+ //! TVM service ticket.
+ std::optional<TString> ServiceTicket;
+
+ //! Additional logging tags.
+ std::optional<TString> LoggingTags;
+
+ //! Provides means to return arbitrary structured data from any command.
+ //! Must be filled before writing data to output stream.
+ NYson::IYsonConsumer* ResponseParametersConsumer;
+
+ //! Invoked after driver is done producing response parameters and
+ //! before first write to output stream.
+ std::function<void()> ResponseParametersFinishedCallback;
+
+ void Reset();
+
+private:
+ using THolderPtr = TRefCountedPtr;
+ THolderPtr Holder_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Command meta-descriptor.
+/*!
+ * Contains various meta-information describing a given command type.
+ */
+struct TCommandDescriptor
+{
+ //! Name of the command.
+ TString CommandName;
+
+ //! Type of data expected by the command at #TDriverRequest::InputStream.
+ NFormats::EDataType InputType;
+
+ //! Type of data written by the command to #TDriverRequest::OutputStream.
+ NFormats::EDataType OutputType;
+
+ //! Whether the command affects the state of the cluster.
+ bool Volatile;
+
+ //! Whether the execution of a command is lengthy and/or causes a heavy load.
+ bool Heavy;
+};
+
+void Serialize(const TCommandDescriptor& descriptor, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An instance of command execution engine.
+/*!
+ * Each driver instance maintains a collection of cached connections to
+ * various YT subsystems (e.g. masters, scheduler).
+ *
+ * IDriver instances are thread-safe and reentrant.
+ */
+struct IDriver
+ : public virtual TRefCounted
+{
+ //! Asynchronously executes a given request.
+ virtual TFuture<void> Execute(const TDriverRequest& request) = 0;
+
+ //! Returns a descriptor for the command with a given name or
+ //! Null if no command with this name is registered.
+ virtual std::optional<TCommandDescriptor> FindCommandDescriptor(const TString& commandName) const = 0;
+
+ //! Returns a descriptor for then command with a given name.
+ //! Fails if no command with this name is registered.
+ TCommandDescriptor GetCommandDescriptor(const TString& commandName) const;
+
+ //! Returns a descriptor for then command with a given name.
+ //! Throws if no command with this name is registered.
+ TCommandDescriptor GetCommandDescriptorOrThrow(const TString& commandName) const;
+
+ //! Returns the list of descriptors for all supported commands.
+ virtual const std::vector<TCommandDescriptor> GetCommandDescriptors() const = 0;
+
+ virtual void ClearMetadataCaches() = 0;
+
+ //! Returns the pool of sticky transactions stored in the driver.
+ virtual NApi::IStickyTransactionPoolPtr GetStickyTransactionPool() = 0;
+
+ //! Returns the cache for proxy discovery requests.
+ virtual IProxyDiscoveryCachePtr GetProxyDiscoveryCache() = 0;
+
+ //! Returns the underlying connection.
+ virtual NApi::IConnectionPtr GetConnection() = 0;
+
+ //! Terminates the underlying connection.
+ virtual void Terminate() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IDriver)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDriverPtr CreateDriver(
+ NApi::IConnectionPtr connection,
+ TDriverConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/etc_commands.cpp b/yt/yt/client/driver/etc_commands.cpp
new file mode 100644
index 0000000000..67e6a1b38f
--- /dev/null
+++ b/yt/yt/client/driver/etc_commands.cpp
@@ -0,0 +1,436 @@
+#include "etc_commands.h"
+#include "proxy_discovery_cache.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/client/api/rpc_proxy/public.h>
+
+#include <yt/yt/build/build.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/yson/async_writer.h>
+
+namespace NYT::NDriver {
+
+using namespace NYPath;
+using namespace NYTree;
+using namespace NYson;
+using namespace NSecurityClient;
+using namespace NObjectClient;
+using namespace NConcurrency;
+using namespace NFormats;
+using namespace NApi;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAddMemberCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->AddMember(
+ Group,
+ Member,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRemoveMemberCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->RemoveMember(
+ Group,
+ Member,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TParseYPathCommand::TParseYPathCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TParseYPathCommand::DoExecute(ICommandContextPtr context)
+{
+ auto richPath = TRichYPath::Parse(Path);
+ ProduceSingleOutputValue(context, "path", richPath);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGetVersionCommand::DoExecute(ICommandContextPtr context)
+{
+ ProduceSingleOutputValue(context, "version", GetVersion());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGetSupportedFeaturesCommand::DoExecute(ICommandContextPtr context)
+{
+ TGetClusterMetaOptions options;
+ options.PopulateFeatures = true;
+ options.ReadFrom = EMasterChannelKind::Cache;
+ options.DisablePerUserCache = true;
+
+ auto meta = WaitFor(context->GetClient()->GetClusterMeta(options))
+ .ValueOrThrow();
+ if (!meta.Features) {
+ THROW_ERROR_EXCEPTION("Feature querying is not supported by current master version");
+ }
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("features").Value(meta.Features)
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCheckPermissionCommand::TCheckPermissionCommand()
+{
+ RegisterParameter("user", User);
+ RegisterParameter("permission", Permission);
+ RegisterParameter("path", Path);
+ RegisterParameter("columns", Options.Columns)
+ .Optional();
+ RegisterParameter("vital", Options.Vital)
+ .Optional();
+}
+
+void TCheckPermissionCommand::DoExecute(ICommandContextPtr context)
+{
+ auto response =
+ WaitFor(context->GetClient()->CheckPermission(
+ User,
+ Path.GetPath(),
+ Permission,
+ Options))
+ .ValueOrThrow();
+
+ auto produceResult = [] (auto fluent, const auto& result) {
+ fluent
+ .Item("action").Value(result.Action)
+ .OptionalItem("object_id", result.ObjectId)
+ .OptionalItem("object_name", result.ObjectName)
+ .OptionalItem("subject_id", result.SubjectId)
+ .OptionalItem("subject_name", result.SubjectName);
+ };
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Do([&] (auto fluent) { produceResult(fluent, response); })
+ .DoIf(response.Columns.has_value(), [&] (auto fluent) {
+ fluent
+ .Item("columns").DoListFor(*response.Columns, [&] (auto fluent, const auto& result) {
+ fluent
+ .Item().BeginMap()
+ .Do([&] (auto fluent) { produceResult(fluent, result); })
+ .EndMap();
+ });
+ })
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCheckPermissionByAclCommand::TCheckPermissionByAclCommand()
+{
+ RegisterParameter("user", User);
+ RegisterParameter("permission", Permission);
+ RegisterParameter("acl", Acl);
+}
+
+void TCheckPermissionByAclCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result =
+ WaitFor(context->GetClient()->CheckPermissionByAcl(
+ User,
+ Permission,
+ Acl,
+ Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("action").Value(result.Action)
+ .OptionalItem("subject_id", result.SubjectId)
+ .OptionalItem("subject_name", result.SubjectName)
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransferAccountResourcesCommand::TTransferAccountResourcesCommand()
+{
+ RegisterParameter("source_account", SourceAccount);
+ RegisterParameter("destination_account", DestinationAccount);
+ RegisterParameter("resource_delta", ResourceDelta);
+}
+
+void TTransferAccountResourcesCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->TransferAccountResources(
+ SourceAccount,
+ DestinationAccount,
+ ResourceDelta,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransferPoolResourcesCommand::TTransferPoolResourcesCommand()
+{
+ RegisterParameter("source_pool", SourcePool);
+ RegisterParameter("destination_pool", DestinationPool);
+ RegisterParameter("pool_tree", PoolTree);
+ RegisterParameter("resource_delta", ResourceDelta);
+}
+
+void TTransferPoolResourcesCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->TransferPoolResources(
+ SourcePool,
+ DestinationPool,
+ PoolTree,
+ ResourceDelta,
+ Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TExecuteBatchCommandRequest::Register(TRegistrar registrar)
+{
+ registrar.Parameter("command", &TThis::Command);
+ registrar.Parameter("parameters", &TThis::Parameters);
+ registrar.Parameter("input", &TThis::Input)
+ .Default();
+}
+
+class TExecuteBatchCommand::TRequestExecutor
+ : public TRefCounted
+{
+public:
+ TRequestExecutor(
+ ICommandContextPtr context,
+ int requestIndex,
+ TRequestPtr request,
+ NRpc::TMutationId mutationId,
+ bool retry)
+ : Context_(std::move(context))
+ , RequestIndex_(requestIndex)
+ , Request_(std::move(request))
+ , MutationId_(mutationId)
+ , Retry_(retry)
+ , SyncInput_(Input_)
+ , AsyncInput_(CreateAsyncAdapter(
+ &SyncInput_,
+ Context_->GetClient()->GetConnection()->GetInvoker()))
+ , SyncOutput_(Output_)
+ , AsyncOutput_(CreateAsyncAdapter(
+ &SyncOutput_,
+ Context_->GetClient()->GetConnection()->GetInvoker()))
+ { }
+
+ TFuture<TYsonString> Run()
+ {
+ try {
+ auto driver = Context_->GetDriver();
+ Descriptor_ = driver->GetCommandDescriptorOrThrow(Request_->Command);
+
+ if (Descriptor_.InputType != EDataType::Null &&
+ Descriptor_.InputType != EDataType::Structured &&
+ Descriptor_.InputType != EDataType::Tabular)
+ {
+ THROW_ERROR_EXCEPTION("Command %Qv cannot be part of a batch since it has inappropriate input type %Qlv",
+ Request_->Command,
+ Descriptor_.InputType);
+ }
+
+ if (Descriptor_.OutputType != EDataType::Null &&
+ Descriptor_.OutputType != EDataType::Structured)
+ {
+ THROW_ERROR_EXCEPTION("Command %Qv cannot be part of a batch since it has inappropriate output type %Qlv",
+ Request_->Command,
+ Descriptor_.OutputType);
+ }
+
+ TDriverRequest driverRequest;
+ driverRequest.Id = Context_->Request().Id;
+ driverRequest.CommandName = Request_->Command;
+ auto parameters = IAttributeDictionary::FromMap(Request_->Parameters);
+ if (Descriptor_.InputType == EDataType::Structured) {
+ if (!Request_->Input) {
+ THROW_ERROR_EXCEPTION("Command %Qv requires input",
+ Descriptor_.CommandName);
+ }
+ Input_ = ConvertToYsonString(Request_->Input).ToString();
+ parameters->Set("input_format", TFormat(EFormatType::Yson));
+ driverRequest.InputStream = AsyncInput_;
+ } else if (Descriptor_.InputType == EDataType::Tabular) {
+ if (!Request_->Input) {
+ THROW_ERROR_EXCEPTION("Command %Qv requires input",
+ Descriptor_.CommandName);
+ }
+ Input_ = Request_->Input->AsString()->GetValue();
+ driverRequest.InputStream = AsyncInput_;
+ }
+ if (Descriptor_.OutputType == EDataType::Structured) {
+ parameters->Set("output_format", TFormat(EFormatType::Yson));
+ driverRequest.OutputStream = AsyncOutput_;
+ }
+ if (Descriptor_.Volatile) {
+ parameters->Set("mutation_id", MutationId_);
+ parameters->Set("retry", Retry_);
+ }
+ driverRequest.Parameters = parameters->ToMap();
+ driverRequest.AuthenticatedUser = Context_->Request().AuthenticatedUser;
+ driverRequest.UserTag = Context_->Request().UserTag;
+ driverRequest.LoggingTags = Format("SubrequestIndex: %v", RequestIndex_);
+
+ return driver->Execute(driverRequest).Apply(
+ BIND(&TRequestExecutor::OnResponse, MakeStrong(this)));
+ } catch (const std::exception& ex) {
+ return MakeFuture<TYsonString>(ex);
+ }
+ }
+
+private:
+ const ICommandContextPtr Context_;
+ const int RequestIndex_;
+ const TRequestPtr Request_;
+ const NRpc::TMutationId MutationId_;
+ const bool Retry_;
+
+ TCommandDescriptor Descriptor_;
+
+ TString Input_;
+ TStringInput SyncInput_;
+ IAsyncInputStreamPtr AsyncInput_;
+
+ TString Output_;
+ TStringOutput SyncOutput_;
+ IFlushableAsyncOutputStreamPtr AsyncOutput_;
+
+ TYsonString OnResponse(const TError& error)
+ {
+ return BuildYsonStringFluently()
+ .BeginMap()
+ .DoIf(!error.IsOK(), [&] (TFluentMap fluent) {
+ fluent
+ .Item("error").Value(error);
+ })
+ .DoIf(error.IsOK() && Descriptor_.OutputType == EDataType::Structured, [&] (TFluentMap fluent) {
+ fluent
+ .Item("output").Value(TYsonStringBuf(Output_));
+ })
+ .EndMap();
+ }
+};
+
+TExecuteBatchCommand::TExecuteBatchCommand()
+{
+ RegisterParameter("concurrency", Options.Concurrency)
+ .Default(50)
+ .GreaterThan(0);
+ RegisterParameter("requests", Requests);
+}
+
+void TExecuteBatchCommand::DoExecute(ICommandContextPtr context)
+{
+ auto mutationId = Options.GetOrGenerateMutationId();
+
+ std::vector<TCallback<TFuture<TYsonString>()>> callbacks;
+ for (int requestIndex = 0; requestIndex < std::ssize(Requests); ++requestIndex) {
+ auto executor = New<TRequestExecutor>(
+ context,
+ requestIndex,
+ Requests[requestIndex],
+ mutationId,
+ Options.Retry);
+ mutationId = NRpc::GenerateNextBatchMutationId(mutationId);
+ callbacks.push_back(BIND(&TRequestExecutor::Run, executor));
+ }
+
+ auto results = WaitFor(RunWithBoundedConcurrency(callbacks, Options.Concurrency))
+ .ValueOrThrow();
+
+ ProduceSingleOutput(context, "results", [&] (NYson::IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .DoListFor(results, [&] (TFluentList fluent, const TErrorOr<TYsonString>& result) {
+ fluent.Item().Value(result.ValueOrThrow());
+ });
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDiscoverProxiesCommand::TDiscoverProxiesCommand()
+{
+ RegisterParameter("type", Type)
+ .Default(EProxyType::Rpc);
+ RegisterParameter("role", Role)
+ .Default(DefaultRpcProxyRole);
+ RegisterParameter("address_type", AddressType)
+ .Default(NApi::NRpcProxy::DefaultAddressType);
+ RegisterParameter("network_name", NetworkName)
+ .Default(NApi::NRpcProxy::DefaultNetworkName);
+ RegisterParameter("ignore_balancers", IgnoreBalancers)
+ .Default(false);
+}
+
+void TDiscoverProxiesCommand::DoExecute(ICommandContextPtr context)
+{
+ TProxyDiscoveryRequest request{
+ .Type = Type,
+ .Role = Role,
+ .AddressType = AddressType,
+ .NetworkName = NetworkName,
+ .IgnoreBalancers = IgnoreBalancers
+ };
+
+ const auto& proxyDiscoveryCache = context->GetDriver()->GetProxyDiscoveryCache();
+ auto response = WaitFor(proxyDiscoveryCache->Discover(request))
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "proxies", response.Addresses);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBalanceTabletCellsCommand::TBalanceTabletCellsCommand()
+{
+ RegisterParameter("bundle", TabletCellBundle);
+ RegisterParameter("tables", MovableTables)
+ .Optional();
+ RegisterParameter("keep_actions", Options.KeepActions)
+ .Default(false);
+}
+
+void TBalanceTabletCellsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->BalanceTabletCells(
+ TabletCellBundle,
+ MovableTables,
+ Options);
+ auto tabletActions = WaitFor(asyncResult)
+ .ValueOrThrow();
+ context->ProduceOutputValue(BuildYsonStringFluently().List(tabletActions));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/etc_commands.h b/yt/yt/client/driver/etc_commands.h
new file mode 100644
index 0000000000..ad00e94211
--- /dev/null
+++ b/yt/yt/client/driver/etc_commands.h
@@ -0,0 +1,223 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/api/rpc_proxy/public.h>
+
+#include <yt/yt/core/ytree/permission.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+class TUpdateMembershipCommand
+ : public TTypedCommand<TOptions>
+{
+protected:
+ TString Group;
+ TString Member;
+
+ TUpdateMembershipCommand()
+ {
+ this->RegisterParameter("group", Group);
+ this->RegisterParameter("member", Member);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAddMemberCommand
+ : public TUpdateMembershipCommand<NApi::TAddMemberOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoveMemberCommand
+ : public TUpdateMembershipCommand<NApi::TRemoveMemberOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TParseYPathCommand
+ : public TCommandBase
+{
+public:
+ TParseYPathCommand();
+
+private:
+ TString Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetVersionCommand
+ : public TCommandBase
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetSupportedFeaturesCommand
+ : public TCommandBase
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCheckPermissionCommand
+ : public TTypedCommand<NApi::TCheckPermissionOptions>
+{
+public:
+ TCheckPermissionCommand();
+
+private:
+ TString User;
+ NYPath::TRichYPath Path;
+ NYTree::EPermission Permission;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCheckPermissionByAclCommand
+ : public TTypedCommand<NApi::TCheckPermissionByAclOptions>
+{
+public:
+ TCheckPermissionByAclCommand();
+
+private:
+ std::optional<TString> User;
+ NYTree::EPermission Permission;
+ NYTree::INodePtr Acl;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTransferAccountResourcesCommand
+ : public TTypedCommand<NApi::TTransferAccountResourcesOptions>
+{
+public:
+ TTransferAccountResourcesCommand();
+
+private:
+ TString SourceAccount;
+ TString DestinationAccount;
+ NYTree::INodePtr ResourceDelta;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTransferPoolResourcesCommand
+ : public TTypedCommand<NApi::TTransferPoolResourcesOptions>
+{
+public:
+ TTransferPoolResourcesCommand();
+
+private:
+ TString SourcePool;
+ TString DestinationPool;
+ TString PoolTree;
+ NYTree::INodePtr ResourceDelta;
+
+ virtual void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TExecuteBatchOptions
+ : public NApi::TMutatingOptions
+{
+ int Concurrency;
+};
+
+class TExecuteBatchCommandRequest
+ : public NYTree::TYsonStruct
+{
+public:
+ TString Command;
+ NYTree::IMapNodePtr Parameters;
+ NYTree::INodePtr Input;
+
+ REGISTER_YSON_STRUCT(TExecuteBatchCommandRequest);
+
+ static void Register(TRegistrar registrar);
+};
+
+class TExecuteBatchCommand
+ : public TTypedCommand<TExecuteBatchOptions>
+{
+public:
+ TExecuteBatchCommand();
+
+private:
+ using TRequestPtr = TIntrusivePtr<TExecuteBatchCommandRequest>;
+
+ std::vector<TRequestPtr> Requests;
+
+ class TRequestExecutor;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDiscoverProxiesOptions
+ : public NApi::TTimeoutOptions
+{ };
+
+class TDiscoverProxiesCommand
+ : public TTypedCommand<TDiscoverProxiesOptions>
+{
+public:
+ TDiscoverProxiesCommand();
+
+private:
+ NApi::EProxyType Type;
+ TString Role;
+ NApi::NRpcProxy::EAddressType AddressType;
+ TString NetworkName;
+ bool IgnoreBalancers;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBalanceTabletCellsCommand
+ : public TTypedCommand<NApi::TBalanceTabletCellsOptions>
+{
+public:
+ TBalanceTabletCellsCommand();
+
+private:
+ TString TabletCellBundle;
+ std::vector<NYPath::TYPath> MovableTables;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/file_commands.cpp b/yt/yt/client/driver/file_commands.cpp
new file mode 100644
index 0000000000..996b8b5431
--- /dev/null
+++ b/yt/yt/client/driver/file_commands.cpp
@@ -0,0 +1,171 @@
+#include "file_commands.h"
+#include "config.h"
+#include "helpers.h"
+
+#include <yt/yt/client/api/config.h>
+#include <yt/yt/client/api/file_reader.h>
+#include <yt/yt/client/api/file_writer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NApi;
+using namespace NConcurrency;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadFileCommand::TReadFileCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("offset", Options.Offset)
+ .Optional();
+ RegisterParameter("length", Options.Length)
+ .Optional();
+ RegisterParameter("file_reader", FileReader)
+ .Default(nullptr);
+ RegisterParameter("etag", Etag)
+ .Alias("etag_revision")
+ .Default();
+}
+
+void TReadFileCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->FileReader,
+ FileReader);
+
+ PutMethodInfoInTraceContext("read_file");
+
+ auto reader = WaitFor(context->GetClient()->CreateFileReader(Path.GetPath(), Options))
+ .ValueOrThrow();
+
+ ProduceResponseParameters(context, [&] (NYson::IYsonConsumer* consumer) {
+ BuildYsonMapFragmentFluently(consumer)
+ .Item("id").Value(reader->GetId())
+ .Item("revision").Value(reader->GetRevision());
+ });
+
+ if (!Etag.empty()) {
+ if (auto etag = ParseEtag(Etag);
+ etag.IsOK() &&
+ etag.Value().Id == reader->GetId() && etag.Value().Revision == reader->GetRevision())
+ {
+ return;
+ }
+
+ // COMPAT(shakurov)
+ NHydra::TRevision etagRevision;
+ if (TryFromString(Etag, etagRevision) && etagRevision == reader->GetRevision()) {
+ return;
+ }
+ }
+
+ auto output = context->Request().OutputStream;
+
+ while (true) {
+ auto block = WaitFor(reader->Read())
+ .ValueOrThrow();
+
+ if (!block)
+ break;
+
+ WaitFor(output->Write(block))
+ .ThrowOnError();
+ }
+}
+
+bool TReadFileCommand::HasResponseParameters() const
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWriteFileCommand::TWriteFileCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("file_writer", FileWriter)
+ .Default();
+ RegisterParameter("compute_md5", ComputeMD5)
+ .Default(false);
+}
+
+void TWriteFileCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->FileWriter,
+ FileWriter);
+ Options.ComputeMD5 = ComputeMD5;
+
+ PutMethodInfoInTraceContext("write_file");
+
+ auto writer = context->GetClient()->CreateFileWriter(Path, Options);
+
+ WaitFor(writer->Open())
+ .ThrowOnError();
+
+ struct TWriteBufferTag { };
+
+ auto buffer = TSharedMutableRef::Allocate<TWriteBufferTag>(context->GetConfig()->WriteBufferSize, {.InitializeStorage = false});
+
+ auto input = context->Request().InputStream;
+
+ while (true) {
+ auto bytesRead = WaitFor(input->Read(buffer))
+ .ValueOrThrow();
+
+ if (bytesRead == 0)
+ break;
+
+ WaitFor(writer->Write(buffer.Slice(0, bytesRead)))
+ .ThrowOnError();
+ }
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetFileFromCacheCommand::TGetFileFromCacheCommand()
+{
+ RegisterParameter("md5", MD5);
+ RegisterParameter("cache_path", Options.CachePath);
+}
+
+void TGetFileFromCacheCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->GetFileFromCache(MD5, Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .Value(result.Path));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPutFileToCacheCommand::TPutFileToCacheCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("md5", MD5);
+ RegisterParameter("cache_path", Options.CachePath);
+ RegisterParameter("preserve_expiration_timeout", Options.PreserveExpirationTimeout)
+ .Optional();
+}
+
+void TPutFileToCacheCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->PutFileToCache(Path, MD5, Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .Value(result.Path));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/file_commands.h b/yt/yt/client/driver/file_commands.h
new file mode 100644
index 0000000000..a422b9bd39
--- /dev/null
+++ b/yt/yt/client/driver/file_commands.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadFileCommand
+ : public TTypedCommand<NApi::TFileReaderOptions>
+{
+public:
+ TReadFileCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr FileReader;
+ TString Etag;
+
+ void DoExecute(ICommandContextPtr context) override;
+ bool HasResponseParameters() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteFileCommand
+ : public TTypedCommand<NApi::TFileWriterOptions>
+{
+public:
+ TWriteFileCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr FileWriter;
+ bool ComputeMD5;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetFileFromCacheCommand
+ : public TTypedCommand<NApi::TGetFileFromCacheOptions>
+{
+public:
+ TGetFileFromCacheCommand();
+
+private:
+ TString MD5;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPutFileToCacheCommand
+ : public TTypedCommand<NApi::TPutFileToCacheOptions>
+{
+public:
+ TPutFileToCacheCommand();
+
+private:
+ NYPath::TYPath Path;
+ TString MD5;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/helpers.cpp b/yt/yt/client/driver/helpers.cpp
new file mode 100644
index 0000000000..007fba71e8
--- /dev/null
+++ b/yt/yt/client/driver/helpers.cpp
@@ -0,0 +1,66 @@
+#include "helpers.h"
+
+#include <yt/yt/client/misc/io_tags.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/guid.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+namespace NYT::NDriver {
+
+using namespace NObjectClient;
+using namespace NTracing;
+
+///////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TEtag& lhs, const TEtag& rhs)
+{
+ return lhs.Id == rhs.Id && lhs.Revision == rhs.Revision;
+}
+
+TErrorOr<TEtag> ParseEtag(TStringBuf etagString)
+{
+ static const TErrorOr<TEtag> parseError(TError("Failed to parse etag"));
+
+ TStringBuf idString;
+ TStringBuf revisionString;
+ if (!etagString.TrySplit(':', idString, revisionString)) {
+ return parseError;
+ }
+
+ TEtag result;
+
+ if (!TObjectId::FromString(idString, &result.Id)) {
+ return parseError;
+ }
+
+ if (!TryFromString(revisionString, result.Revision)) {
+ return parseError;
+ }
+
+ return result;
+}
+
+TString ToString(const TEtag& Etag)
+{
+ return Format("%v:%v", Etag.Id, Etag.Revision);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PutMethodInfoInTraceContext(const TStringBuf& methodName)
+{
+ if (auto* traceContext = TryGetCurrentTraceContext()) {
+ auto baggage = traceContext->UnpackOrCreateBaggage();
+ AddTagToBaggage(baggage, EAggregateIOTag::ApiMethod, methodName);
+ AddTagToBaggage(baggage, EAggregateIOTag::ProxyKind, "http");
+ traceContext->PackBaggage(baggage);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/helpers.h b/yt/yt/client/driver/helpers.h
new file mode 100644
index 0000000000..b10b9bd543
--- /dev/null
+++ b/yt/yt/client/driver/helpers.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/object_client/public.h>
+#include <yt/yt/client/hydra/public.h>
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NDriver {
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct TEtag
+{
+ NObjectClient::TObjectId Id = NObjectClient::NullObjectId;
+ NHydra::TRevision Revision = NHydra::NullRevision;
+};
+
+bool operator==(const TEtag& lhs, const TEtag& rhs);
+
+TErrorOr<TEtag> ParseEtag(TStringBuf etagString);
+TString ToString(const TEtag& Etag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PutMethodInfoInTraceContext(const TStringBuf& methodName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/internal_commands.cpp b/yt/yt/client/driver/internal_commands.cpp
new file mode 100644
index 0000000000..bdc4499ade
--- /dev/null
+++ b/yt/yt/client/driver/internal_commands.cpp
@@ -0,0 +1,142 @@
+#include "internal_commands.h"
+
+#include "config.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NConcurrency;
+using namespace NApi;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadHunksCommand::TReadHunksCommand()
+{
+ RegisterParameter("descriptors", Descriptors);
+
+ RegisterParameter("parse_header", ParseHeader)
+ .Default(true);
+}
+
+void TReadHunksCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->ChunkFragmentReader,
+ ChunkFragmentReader);
+ Options.ParseHeader = ParseHeader;
+
+ std::vector<THunkDescriptor> descriptors;
+ descriptors.reserve(Descriptors.size());
+ for (const auto& descriptor : Descriptors) {
+ descriptors.push_back(*descriptor);
+ }
+
+ auto internalClient = context->GetInternalClientOrThrow();
+ auto responses = WaitFor(internalClient->ReadHunks(descriptors, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("hunks").DoListFor(responses, [&] (auto fluent, const TSharedRef& response) {
+ fluent
+ .Item().BeginMap()
+ .Item(HunkPayloadKey).Value(TStringBuf(response.Begin(), response.End()))
+ .EndMap();
+ })
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWriteHunksCommand::TWriteHunksCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("tablet_index", TabletIndex);
+ RegisterParameter("payloads", Payloads);
+}
+
+void TWriteHunksCommand::DoExecute(ICommandContextPtr context)
+{
+ auto internalClient = context->GetInternalClientOrThrow();
+
+ std::vector<TSharedRef> payloads;
+ payloads.reserve(Payloads.size());
+ for (const auto& payload : Payloads) {
+ payloads.push_back(TSharedRef::FromString(payload));
+ }
+
+ auto descriptors = WaitFor(internalClient->WriteHunks(Path, TabletIndex, payloads))
+ .ValueOrThrow();
+
+ std::vector<NApi::TSerializableHunkDescriptorPtr> serializableDescriptors;
+ serializableDescriptors.reserve(descriptors.size());
+ for (const auto& descriptor : descriptors) {
+ serializableDescriptors.push_back(New<NApi::TSerializableHunkDescriptor>(descriptor));
+ }
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .Value(serializableDescriptors));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLockHunkStoreCommand::TLockHunkStoreCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("tablet_index", TabletIndex);
+ RegisterParameter("store_id", StoreId);
+ RegisterParameter("locker_tablet_id", LockerTabletId);
+}
+
+void TLockHunkStoreCommand::DoExecute(ICommandContextPtr context)
+{
+ auto internalClient = context->GetInternalClientOrThrow();
+
+ WaitFor(internalClient->LockHunkStore(
+ Path,
+ TabletIndex,
+ StoreId,
+ LockerTabletId))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnlockHunkStoreCommand::TUnlockHunkStoreCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("tablet_index", TabletIndex);
+ RegisterParameter("store_id", StoreId);
+ RegisterParameter("locker_tablet_id", LockerTabletId);
+}
+
+void TUnlockHunkStoreCommand::DoExecute(ICommandContextPtr context)
+{
+ auto internalClient = context->GetInternalClientOrThrow();
+
+ WaitFor(internalClient->UnlockHunkStore(
+ Path,
+ TabletIndex,
+ StoreId,
+ LockerTabletId))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGetConnectionConfigCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetRootClient();
+
+ context->ProduceOutputValue(client->GetConnection()->GetConfigYson());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/internal_commands.h b/yt/yt/client/driver/internal_commands.h
new file mode 100644
index 0000000000..2d3f83bfde
--- /dev/null
+++ b/yt/yt/client/driver/internal_commands.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/api/internal_client.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadHunksCommand
+ : public TTypedCommand<NApi::TReadHunksOptions>
+{
+public:
+ TReadHunksCommand();
+
+private:
+ std::vector<NApi::TSerializableHunkDescriptorPtr> Descriptors;
+ NYTree::INodePtr ChunkFragmentReader;
+
+ bool ParseHeader;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteHunksCommand
+ : public TTypedCommand<NApi::TWriteHunksOptions>
+{
+public:
+ TWriteHunksCommand();
+
+private:
+ NYTree::TYPath Path;
+ int TabletIndex;
+ std::vector<TString> Payloads;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLockHunkStoreCommand
+ : public TTypedCommand<NApi::TLockHunkStoreOptions>
+{
+public:
+ TLockHunkStoreCommand();
+
+private:
+ NYTree::TYPath Path;
+ int TabletIndex;
+ NTabletClient::TStoreId StoreId;
+ NTabletClient::TTabletId LockerTabletId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnlockHunkStoreCommand
+ : public TTypedCommand<NApi::TUnlockHunkStoreOptions>
+{
+public:
+ TUnlockHunkStoreCommand();
+
+private:
+ NYTree::TYPath Path;
+ int TabletIndex;
+ NTabletClient::TStoreId StoreId;
+ NTabletClient::TTabletId LockerTabletId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGetConnectionConfigCommandOptions
+{ };
+
+class TGetConnectionConfigCommand
+ : public TTypedCommand<TGetConnectionConfigCommandOptions>
+{
+public:
+ TGetConnectionConfigCommand() = default;
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/journal_commands.cpp b/yt/yt/client/driver/journal_commands.cpp
new file mode 100644
index 0000000000..a3a4406e8d
--- /dev/null
+++ b/yt/yt/client/driver/journal_commands.cpp
@@ -0,0 +1,343 @@
+#include "journal_commands.h"
+#include "config.h"
+
+#include <yt/yt/client/api/config.h>
+#include <yt/yt/client/api/journal_reader.h>
+#include <yt/yt/client/api/journal_writer.h>
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/client/formats/format.h>
+#include <yt/yt/client/formats/parser.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NFormats;
+using namespace NConcurrency;
+using namespace NChunkClient;
+using namespace NApi;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadJournalCommand::TReadJournalCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("journal_reader", JournalReader)
+ .Default();
+}
+
+void TReadJournalCommand::DoExecute(ICommandContextPtr context)
+{
+ auto checkLimit = [] (const TReadLimit& limit) {
+ if (limit.KeyBound()) {
+ THROW_ERROR_EXCEPTION("Reading key range is not supported in journals");
+ }
+ if (limit.GetChunkIndex()) {
+ THROW_ERROR_EXCEPTION("Reading chunk index range is not supported in journals");
+ }
+ if (limit.GetOffset()) {
+ THROW_ERROR_EXCEPTION("Reading offset range is not supported in journals");
+ }
+ };
+
+ if (Path.GetNewRanges().size() > 1) {
+ THROW_ERROR_EXCEPTION("Reading multiple ranges is not supported in journals");
+ }
+
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->JournalReader,
+ JournalReader);
+
+ if (Path.GetNewRanges().size() == 1) {
+ auto range = Path.GetNewRanges()[0];
+
+ checkLimit(range.LowerLimit());
+ checkLimit(range.UpperLimit());
+
+ Options.FirstRowIndex = range.LowerLimit().GetRowIndex().value_or(0);
+
+ if (auto upperRowIndex = range.UpperLimit().GetRowIndex()) {
+ Options.RowCount = *upperRowIndex - *Options.FirstRowIndex;
+ }
+ }
+
+ auto reader = context->GetClient()->CreateJournalReader(
+ Path.GetPath(),
+ Options);
+
+ WaitFor(reader->Open())
+ .ThrowOnError();
+
+ auto output = context->Request().OutputStream;
+
+ // TODO(babenko): provide custom allocation tag
+ TBlobOutput buffer;
+ auto flushBuffer = [&] () {
+ WaitFor(output->Write(buffer.Flush()))
+ .ThrowOnError();
+ };
+
+ auto format = context->GetOutputFormat();
+ auto consumer = CreateConsumerForFormat(format, EDataType::Tabular, &buffer);
+
+ while (true) {
+ auto rowsOrError = WaitFor(reader->Read());
+ const auto& rows = rowsOrError.ValueOrThrow();
+
+ if (rows.empty())
+ break;
+
+ for (auto row : rows) {
+ BuildYsonListFragmentFluently(consumer.get())
+ .Item().BeginMap()
+ .Item(JournalPayloadKey).Value(TStringBuf(row.Begin(), row.Size()))
+ .EndMap();
+ }
+
+ if (std::ssize(buffer) > context->GetConfig()->ReadBufferSize) {
+ flushBuffer();
+ }
+ }
+
+ consumer->Flush();
+
+ if (buffer.Size() > 0) {
+ flushBuffer();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EJournalConsumerState,
+ (Root)
+ (AtItem)
+ (InsideMap)
+ (AtData)
+);
+
+class TJournalConsumer
+ : public TYsonConsumerBase
+{
+public:
+ explicit TJournalConsumer(IJournalWriterPtr writer)
+ : Writer_(std::move(writer))
+ { }
+
+ void Flush()
+ {
+ if (BufferedRows_.empty()) {
+ return;
+ }
+
+ WaitFor(Writer_->Write(BufferedRows_))
+ .ThrowOnError();
+
+ BufferedRows_.clear();
+ BufferedByteSize_ = 0;
+ }
+
+private:
+ const IJournalWriterPtr Writer_;
+
+ EJournalConsumerState State_ = EJournalConsumerState::Root;
+
+ std::vector<TSharedRef> BufferedRows_;
+ i64 BufferedByteSize_ = 0;
+ static constexpr i64 MaxBufferedSize = 64_KB;
+
+
+ void OnStringScalar(TStringBuf value) override
+ {
+ if (State_ != EJournalConsumerState::AtData) {
+ ThrowMalformedPayload();
+ }
+
+ auto row = TSharedRef::FromString(TString(value));
+ BufferedByteSize_ += row.Size();
+ BufferedRows_.push_back(std::move(row));
+
+ State_ = EJournalConsumerState::InsideMap;
+ }
+
+ void OnInt64Scalar(i64 /*value*/) override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnUint64Scalar(ui64 /*value*/) override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnDoubleScalar(double /*value*/) override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnBooleanScalar(bool /*value*/) override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnEntity() override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnBeginList() override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnListItem() override
+ {
+ if (State_ != EJournalConsumerState::Root) {
+ ThrowMalformedPayload();
+ }
+ State_ = EJournalConsumerState::AtItem;
+ }
+
+ void OnEndList() override
+ {
+ YT_ABORT();
+ }
+
+ void OnBeginMap() override
+ {
+ if (State_ != EJournalConsumerState::AtItem) {
+ ThrowMalformedPayload();
+ }
+ State_ = EJournalConsumerState::InsideMap;
+ }
+
+ void OnKeyedItem(TStringBuf key) override
+ {
+ if (State_ != EJournalConsumerState::InsideMap) {
+ ThrowMalformedPayload();
+ }
+ if (key != JournalPayloadKey) {
+ ThrowMalformedPayload();
+ }
+ State_ = EJournalConsumerState::AtData;
+ }
+
+ void OnEndMap() override
+ {
+ if (State_ != EJournalConsumerState::InsideMap) {
+ ThrowMalformedPayload();
+ }
+ State_ = EJournalConsumerState::Root;
+ }
+
+ void OnBeginAttributes() override
+ {
+ ThrowMalformedPayload();
+ }
+
+ void OnEndAttributes() override
+ {
+ YT_ABORT();
+ }
+
+
+ void ThrowMalformedPayload()
+ {
+ THROW_ERROR_EXCEPTION("Malformed journal payload");
+ }
+
+ void MaybeFlush()
+ {
+ if (BufferedByteSize_ >= MaxBufferedSize) {
+ Flush();
+ }
+ }
+};
+
+TWriteJournalCommand::TWriteJournalCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("journal_writer", JournalWriter)
+ .Default();
+ RegisterParameter("enable_chunk_preallocation", Options.EnableChunkPreallocation)
+ .Optional();
+ RegisterParameter("replica_lag_limit", Options.ReplicaLagLimit)
+ .Optional();
+}
+
+void TWriteJournalCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->JournalWriter,
+ JournalWriter);
+
+ auto writer = context->GetClient()->CreateJournalWriter(
+ Path.GetPath(),
+ Options);
+
+ WaitFor(writer->Open())
+ .ThrowOnError();
+
+ TJournalConsumer consumer(writer);
+
+ auto format = context->GetInputFormat();
+ auto parser = CreateParserForFormat(format, EDataType::Tabular, &consumer);
+
+ struct TWriteBufferTag { };
+
+ auto buffer = TSharedMutableRef::Allocate<TWriteBufferTag>(context->GetConfig()->WriteBufferSize, {.InitializeStorage = false});
+
+ auto input = context->Request().InputStream;
+
+ while (true) {
+ auto bytesRead = WaitFor(input->Read(buffer))
+ .ValueOrThrow();
+
+ if (bytesRead == 0)
+ break;
+
+ parser->Read(TStringBuf(buffer.Begin(), bytesRead));
+ }
+
+ parser->Finish();
+
+ consumer.Flush();
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTruncateJournalCommand::TTruncateJournalCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("row_count", RowCount);
+}
+
+void TTruncateJournalCommand::DoExecute(NYT::NDriver::ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->TruncateJournal(
+ Path,
+ RowCount,
+ Options);
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/journal_commands.h b/yt/yt/client/driver/journal_commands.h
new file mode 100644
index 0000000000..8730d1873d
--- /dev/null
+++ b/yt/yt/client/driver/journal_commands.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/formats/format.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadJournalCommand
+ : public TTypedCommand<NApi::TJournalReaderOptions>
+{
+public:
+ TReadJournalCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr JournalReader;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteJournalCommand
+ : public TTypedCommand<NApi::TJournalWriterOptions>
+{
+public:
+ TWriteJournalCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr JournalWriter;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTruncateJournalCommand
+ : public TTypedCommand<NApi::TTruncateJournalOptions>
+{
+public:
+ TTruncateJournalCommand();
+
+private:
+ NYPath::TYPath Path;
+ i64 RowCount;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/private.h b/yt/yt/client/driver/private.h
new file mode 100644
index 0000000000..d7fa243b11
--- /dev/null
+++ b/yt/yt/client/driver/private.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICommand;
+DECLARE_REFCOUNTED_STRUCT(ICommandContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger DriverLogger("Driver");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/proxy_discovery_cache.cpp b/yt/yt/client/driver/proxy_discovery_cache.cpp
new file mode 100644
index 0000000000..1a58eeaa5d
--- /dev/null
+++ b/yt/yt/client/driver/proxy_discovery_cache.cpp
@@ -0,0 +1,204 @@
+#include "proxy_discovery_cache.h"
+
+#include "private.h"
+
+#include <iterator>
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/api/rpc_proxy/address_helpers.h>
+
+#include <yt/yt/core/misc/async_expiring_cache.h>
+
+#include <util/digest/multi.h>
+
+namespace NYT::NDriver {
+
+using namespace NYPath;
+using namespace NYTree;
+using namespace NYson;
+using namespace NApi;
+using namespace NConcurrency;
+using namespace NApi::NRpcProxy;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TProxyDiscoveryRequest::operator==(const TProxyDiscoveryRequest& other) const
+{
+ return
+ Type == other.Type &&
+ Role == other.Role &&
+ AddressType == other.AddressType &&
+ NetworkName == other.NetworkName &&
+ IgnoreBalancers == other.IgnoreBalancers;
+}
+
+bool TProxyDiscoveryRequest::operator!=(const TProxyDiscoveryRequest& other) const
+{
+ return !(*this == other);
+}
+
+TProxyDiscoveryRequest::operator size_t() const
+{
+ return MultiHash(
+ Type,
+ Role,
+ AddressType,
+ NetworkName,
+ IgnoreBalancers);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TProxyDiscoveryRequest& request, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{Type: %v, Role: %v, AddressType: %v, NetworkName: %v, IgnoreBalancers: %v}",
+ request.Type,
+ request.Role,
+ request.AddressType,
+ request.NetworkName,
+ request.IgnoreBalancers);
+}
+
+TString ToString(const TProxyDiscoveryRequest& request)
+{
+ return ToStringViaBuilder(request);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProxyDiscoveryCache
+ : public TAsyncExpiringCache<TProxyDiscoveryRequest, TProxyDiscoveryResponse>
+ , public IProxyDiscoveryCache
+{
+public:
+ TProxyDiscoveryCache(
+ TAsyncExpiringCacheConfigPtr config,
+ IClientPtr client)
+ : TAsyncExpiringCache(
+ std::move(config),
+ DriverLogger.WithTag("Cache: ProxyDiscovery"))
+ , Client_(std::move(client))
+ { }
+
+ TFuture<TProxyDiscoveryResponse> Discover(
+ const TProxyDiscoveryRequest& request) override
+ {
+ return Get(request);
+ }
+
+private:
+ const IClientPtr Client_;
+
+ TFuture<TProxyDiscoveryResponse> DoGet(
+ const TProxyDiscoveryRequest& request,
+ bool /*isPeriodicUpdate*/) noexcept override
+ {
+ return GetResponseByBalancers(request).Apply(
+ BIND([=, this, this_ = MakeStrong(this)] (const std::optional<TProxyDiscoveryResponse>& response) {
+ if (response) {
+ return MakeFuture<TProxyDiscoveryResponse>(std::move(*response));
+ }
+ return GetResponseByAddresses(request);
+ }).AsyncVia(Client_->GetConnection()->GetInvoker()));
+ }
+
+ TFuture<std::optional<TProxyDiscoveryResponse>> GetResponseByBalancers(const TProxyDiscoveryRequest& request)
+ {
+ if (request.IgnoreBalancers) {
+ return MakeFuture<std::optional<TProxyDiscoveryResponse>>(std::nullopt);
+ }
+
+ TGetNodeOptions options;
+ options.ReadFrom = EMasterChannelKind::LocalCache;
+ options.Attributes = {BalancersAttributeName};
+
+ auto path = GetProxyRegistryPath(request.Type) + "/@";
+ return Client_->GetNode(path, options).Apply(
+ BIND([=] (const TYsonString& yson) -> std::optional<TProxyDiscoveryResponse> {
+ auto attributes = ConvertTo<IMapNodePtr>(yson);
+
+ auto balancers = attributes->GetChildValueOrDefault<TBalancersMap>(BalancersAttributeName, {});
+
+ auto responseBalancers = GetBalancersOrNull(balancers, request.Role, request.AddressType, request.NetworkName);
+
+ if (!responseBalancers) {
+ return std::nullopt;
+ }
+
+ TProxyDiscoveryResponse response;
+ std::move(responseBalancers->begin(), responseBalancers->end(), std::back_inserter(response.Addresses));
+ return response;
+ }).AsyncVia(Client_->GetConnection()->GetInvoker()));
+ }
+
+ TFuture<TProxyDiscoveryResponse> GetResponseByAddresses(const TProxyDiscoveryRequest& request)
+ {
+ TGetNodeOptions options;
+ options.ReadFrom = EMasterChannelKind::LocalCache;
+ options.SuppressUpstreamSync = true;
+ options.SuppressTransactionCoordinatorSync = true;
+ options.Attributes = {BannedAttributeName, RoleAttributeName, AddressesAttributeName};
+
+ auto path = GetProxyRegistryPath(request.Type);
+ return Client_->GetNode(path, options).Apply(BIND([=] (const TYsonString& yson) {
+ TProxyDiscoveryResponse response;
+
+ for (const auto& [proxyAddress, proxyNode] : ConvertTo<THashMap<TString, IMapNodePtr>>(yson)) {
+ if (!proxyNode->FindChild(AliveNodeName)) {
+ continue;
+ }
+
+ if (proxyNode->Attributes().Get(BannedAttributeName, false)) {
+ continue;
+ }
+
+ if (proxyNode->Attributes().Get<TString>(RoleAttributeName, DefaultRpcProxyRole) != request.Role) {
+ continue;
+ }
+
+ auto addresses = proxyNode->Attributes().Get<TProxyAddressMap>(AddressesAttributeName, {});
+ auto address = GetAddressOrNull(addresses, request.AddressType, request.NetworkName);
+
+ if (address) {
+ response.Addresses.push_back(*address);
+ } else {
+ // COMPAT(verytable): Drop it after all rpc proxies migrate to 22.3.
+ if (!proxyNode->Attributes().Contains(AddressesAttributeName)) {
+ response.Addresses.push_back(proxyAddress);
+ }
+ }
+ }
+ return response;
+ }).AsyncVia(Client_->GetConnection()->GetInvoker()));
+ }
+
+
+ static TYPath GetProxyRegistryPath(EProxyType type)
+ {
+ switch (type) {
+ case EProxyType::Rpc:
+ return RpcProxiesPath;
+ case EProxyType::Grpc:
+ return GrpcProxiesPath;
+ default:
+ THROW_ERROR_EXCEPTION("Proxy type %Qlv is not supported",
+ type);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IProxyDiscoveryCachePtr CreateProxyDiscoveryCache(
+ TAsyncExpiringCacheConfigPtr config,
+ IClientPtr client)
+{
+ return New<TProxyDiscoveryCache>(
+ std::move(config),
+ std::move(client));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/proxy_discovery_cache.h b/yt/yt/client/driver/proxy_discovery_cache.h
new file mode 100644
index 0000000000..f99b75cefa
--- /dev/null
+++ b/yt/yt/client/driver/proxy_discovery_cache.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/api/rpc_proxy/address_helpers.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProxyDiscoveryRequest
+{
+ NApi::EProxyType Type;
+ TString Role = NApi::DefaultRpcProxyRole;
+ NApi::NRpcProxy::EAddressType AddressType = NApi::NRpcProxy::DefaultAddressType;
+ TString NetworkName = NApi::NRpcProxy::DefaultNetworkName;
+ bool IgnoreBalancers = false;
+
+ bool operator==(const TProxyDiscoveryRequest& other) const;
+ bool operator!=(const TProxyDiscoveryRequest& other) const;
+
+ operator size_t() const;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TProxyDiscoveryRequest& request, TStringBuf spec);
+TString ToString(const TProxyDiscoveryRequest& request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProxyDiscoveryResponse
+{
+ std::vector<TString> Addresses;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IProxyDiscoveryCache
+ : public virtual TRefCounted
+{
+ virtual TFuture<TProxyDiscoveryResponse> Discover(
+ const TProxyDiscoveryRequest& request) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IProxyDiscoveryCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IProxyDiscoveryCachePtr CreateProxyDiscoveryCache(
+ TAsyncExpiringCacheConfigPtr config,
+ NApi::IClientPtr client);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/public.h b/yt/yt/client/driver/public.h
new file mode 100644
index 0000000000..6f4b5618da
--- /dev/null
+++ b/yt/yt/client/driver/public.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(IDriver)
+DECLARE_REFCOUNTED_STRUCT(IProxyDiscoveryCache)
+
+DECLARE_REFCOUNTED_CLASS(TDriverConfig)
+
+struct TCommandDescriptor;
+struct TDriverRequest;
+struct TEtag;
+struct TProxyDiscoveryRequest;
+struct TProxyDiscoveryResponse;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/query_commands.cpp b/yt/yt/client/driver/query_commands.cpp
new file mode 100644
index 0000000000..756c273175
--- /dev/null
+++ b/yt/yt/client/driver/query_commands.cpp
@@ -0,0 +1,198 @@
+#include "query_commands.h"
+
+#include <yt/yt/client/api/rowset.h>
+
+#include <yt/yt/client/formats/config.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NDriver {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NFormats;
+
+//////////////////////////////////////////////////////////////////////////////
+
+TStartQueryCommand::TStartQueryCommand()
+{
+ RegisterParameter("engine", Engine);
+ RegisterParameter("query", Query);
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Optional();
+ RegisterParameter("settings", Options.Settings)
+ .Optional();
+ RegisterParameter("draft", Options.Draft)
+ .Optional();
+ RegisterParameter("annotations", Options.Annotations)
+ .Optional();
+}
+
+void TStartQueryCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->StartQuery(Engine, Query, Options);
+ auto queryId = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("query_id").Value(queryId)
+ .EndMap());
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TAbortQueryCommand::TAbortQueryCommand()
+{
+ RegisterParameter("query_id", QueryId);
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Optional();
+}
+
+void TAbortQueryCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->AbortQuery(QueryId, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+ ProduceEmptyOutput(context);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TGetQueryResultCommand::TGetQueryResultCommand()
+{
+ RegisterParameter("query_id", QueryId);
+ RegisterParameter("result_index", ResultIndex)
+ .Default(0);
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Optional();
+}
+
+void TGetQueryResultCommand::DoExecute(ICommandContextPtr context)
+{
+ auto queryResult = WaitFor(context->GetClient()->GetQueryResult(QueryId, ResultIndex, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(ConvertToYsonString(queryResult));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TReadQueryResultCommand::TReadQueryResultCommand()
+{
+ RegisterParameter("query_id", QueryId);
+ RegisterParameter("result_index", ResultIndex)
+ .Default(0);
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Optional();
+ RegisterParameter("columns", Options.Columns)
+ .Default();
+ RegisterParameter("lower_row_index", Options.LowerRowIndex)
+ .Default();
+ RegisterParameter("upper_row_index", Options.UpperRowIndex)
+ .Default();
+}
+
+void TReadQueryResultCommand::DoExecute(ICommandContextPtr context)
+{
+ auto rowset = WaitFor(context->GetClient()->ReadQueryResult(QueryId, ResultIndex, Options))
+ .ValueOrThrow();
+
+ auto writer = CreateStaticTableWriterForFormat(
+ context->GetOutputFormat(),
+ rowset->GetNameTable(),
+ {rowset->GetSchema()},
+ context->Request().OutputStream,
+ /*enableContextSaving*/ false,
+ New<TControlAttributesConfig>(),
+ /*keyColumnCount*/ 0);
+
+ writer->Write(rowset->GetRows());
+ WaitFor(writer->Close())
+ .ThrowOnError();
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TGetQueryCommand::TGetQueryCommand()
+{
+ RegisterParameter("query_id", QueryId);
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Optional();
+}
+
+void TGetQueryCommand::DoExecute(ICommandContextPtr context)
+{
+ auto query = WaitFor(context->GetClient()->GetQuery(QueryId, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(ConvertToYsonString(query));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TListQueriesCommand::TListQueriesCommand()
+{
+ RegisterParameter("stage", Options.QueryTrackerStage)
+ .Default("production");
+ RegisterParameter("from_time", Options.FromTime)
+ .Optional();
+ RegisterParameter("to_time", Options.ToTime)
+ .Optional();
+ RegisterParameter("cursor_time", Options.CursorTime)
+ .Optional();
+ RegisterParameter("cursor_direction", Options.CursorDirection)
+ .Optional();
+ RegisterParameter("user", Options.UserFilter)
+ .Optional();
+ RegisterParameter("state", Options.StateFilter)
+ .Optional();
+ RegisterParameter("engine", Options.EngineFilter)
+ .Optional();
+ RegisterParameter("filter", Options.SubstrFilter)
+ .Optional();
+ RegisterParameter("limit", Options.Limit)
+ .Optional();
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+}
+
+void TListQueriesCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->ListQueries(Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("queries").Value(result.Queries)
+ .Item("incomplete").Value(result.Incomplete)
+ .Item("timestamp").Value(result.Timestamp)
+ .EndMap());
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+TAlterQueryCommand::TAlterQueryCommand()
+{
+ RegisterParameter("query_id", QueryId);
+ RegisterParameter("annotations", Options.Annotations)
+ .Optional();
+}
+
+void TAlterQueryCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->AlterQuery(QueryId, Options))
+ .ThrowOnError();
+ ProduceEmptyOutput(context);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/query_commands.h b/yt/yt/client/driver/query_commands.h
new file mode 100644
index 0000000000..dfffcfd059
--- /dev/null
+++ b/yt/yt/client/driver/query_commands.h
@@ -0,0 +1,112 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/client/query_tracker_client/public.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////
+
+class TStartQueryCommand
+ : public TTypedCommand<NApi::TStartQueryOptions>
+{
+public:
+ TStartQueryCommand();
+
+private:
+ NQueryTrackerClient::EQueryEngine Engine;
+ TString Query;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TAbortQueryCommand
+ : public TTypedCommand<NApi::TAbortQueryOptions>
+{
+public:
+ TAbortQueryCommand();
+
+private:
+ NQueryTrackerClient::TQueryId QueryId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TGetQueryResultCommand
+ : public TTypedCommand<NApi::TGetQueryResultOptions>
+{
+public:
+ TGetQueryResultCommand();
+
+private:
+ NQueryTrackerClient::TQueryId QueryId;
+ i64 ResultIndex;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TReadQueryResultCommand
+ : public TTypedCommand<NApi::TReadQueryResultOptions>
+{
+public:
+ TReadQueryResultCommand();
+
+private:
+ NQueryTrackerClient::TQueryId QueryId;
+ i64 ResultIndex;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TGetQueryCommand
+ : public TTypedCommand<NApi::TGetQueryOptions>
+{
+public:
+ TGetQueryCommand();
+
+private:
+ NQueryTrackerClient::TQueryId QueryId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TListQueriesCommand
+ : public TTypedCommand<NApi::TListQueriesOptions>
+{
+public:
+ TListQueriesCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+class TAlterQueryCommand
+ : public TTypedCommand<NApi::TAlterQueryOptions>
+{
+public:
+ TAlterQueryCommand();
+
+private:
+ NQueryTrackerClient::TQueryId QueryId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/queue_commands.cpp b/yt/yt/client/driver/queue_commands.cpp
new file mode 100644
index 0000000000..9c001d3bb8
--- /dev/null
+++ b/yt/yt/client/driver/queue_commands.cpp
@@ -0,0 +1,200 @@
+#include "queue_commands.h"
+#include "config.h"
+
+#include <yt/yt/client/api/config.h>
+
+namespace NYT::NDriver {
+
+using namespace NConcurrency;
+using namespace NApi;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRegisterQueueConsumerCommand::TRegisterQueueConsumerCommand()
+{
+ RegisterParameter("queue_path", QueuePath);
+ RegisterParameter("consumer_path", ConsumerPath);
+ RegisterParameter("vital", Vital);
+ RegisterParameter("partitions", Partitions)
+ .Default();
+}
+
+void TRegisterQueueConsumerCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Partitions = Partitions;
+
+ auto client = context->GetClient();
+ auto asyncResult = client->RegisterQueueConsumer(
+ QueuePath,
+ ConsumerPath,
+ Vital,
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnregisterQueueConsumerCommand::TUnregisterQueueConsumerCommand()
+{
+ RegisterParameter("queue_path", QueuePath);
+ RegisterParameter("consumer_path", ConsumerPath);
+}
+
+void TUnregisterQueueConsumerCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->UnregisterQueueConsumer(
+ QueuePath,
+ ConsumerPath,
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListQueueConsumerRegistrationsCommand::TListQueueConsumerRegistrationsCommand()
+{
+ RegisterParameter("queue_path", QueuePath)
+ .Default();
+ RegisterParameter("consumer_path", ConsumerPath)
+ .Default();
+}
+
+void TListQueueConsumerRegistrationsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->ListQueueConsumerRegistrations(
+ QueuePath,
+ ConsumerPath,
+ Options);
+ auto registrations = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ ProduceOutput(context, [&](NYson::IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .DoListFor(registrations, [=] (TFluentList fluent, const TListQueueConsumerRegistrationsResult& registration) {
+ fluent
+ .Item().BeginMap()
+ .Item("queue_path").Value(registration.QueuePath)
+ .Item("consumer_path").Value(registration.ConsumerPath)
+ .Item("vital").Value(registration.Vital)
+ .Item("partitions").Value(registration.Partitions)
+ .EndMap();
+ });
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPullQueueCommand::TPullQueueCommand()
+{
+ RegisterParameter("queue_path", QueuePath);
+ RegisterParameter("offset", Offset);
+ RegisterParameter("partition_index", PartitionIndex);
+
+ RegisterParameter("max_row_count", RowBatchReadOptions.MaxRowCount)
+ .Optional();
+ RegisterParameter("max_data_weight", RowBatchReadOptions.MaxDataWeight)
+ .Optional();
+ RegisterParameter("data_weight_per_row_hint", RowBatchReadOptions.DataWeightPerRowHint)
+ .Optional();
+}
+
+void TPullQueueCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+
+ auto result = WaitFor(client->PullQueue(
+ QueuePath,
+ Offset,
+ PartitionIndex,
+ RowBatchReadOptions,
+ Options))
+ .ValueOrThrow();
+
+ auto format = context->GetOutputFormat();
+ auto output = context->Request().OutputStream;
+ auto writer = CreateSchemafulWriterForFormat(format, result->GetSchema(), output);
+
+ writer->Write(result->GetRows());
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPullConsumerCommand::TPullConsumerCommand()
+{
+ RegisterParameter("consumer_path", ConsumerPath);
+ RegisterParameter("queue_path", QueuePath);
+ RegisterParameter("offset", Offset);
+ RegisterParameter("partition_index", PartitionIndex);
+
+ RegisterParameter("max_row_count", RowBatchReadOptions.MaxRowCount)
+ .Optional();
+ RegisterParameter("max_data_weight", RowBatchReadOptions.MaxDataWeight)
+ .Optional();
+ RegisterParameter("data_weight_per_row_hint", RowBatchReadOptions.DataWeightPerRowHint)
+ .Optional();
+}
+
+void TPullConsumerCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+
+ auto result = WaitFor(client->PullConsumer(
+ ConsumerPath,
+ QueuePath,
+ Offset,
+ PartitionIndex,
+ RowBatchReadOptions,
+ Options))
+ .ValueOrThrow();
+
+ auto format = context->GetOutputFormat();
+ auto output = context->Request().OutputStream;
+ auto writer = CreateSchemafulWriterForFormat(format, result->GetSchema(), output);
+
+ writer->Write(result->GetRows());
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAdvanceConsumerCommand::TAdvanceConsumerCommand()
+{
+ RegisterParameter("consumer_path", ConsumerPath);
+ RegisterParameter("queue_path", QueuePath);
+ RegisterParameter("partition_index", PartitionIndex);
+ RegisterParameter("old_offset", OldOffset)
+ .Optional();
+ RegisterParameter("new_offset", NewOffset);
+}
+
+void TAdvanceConsumerCommand::DoExecute(ICommandContextPtr context)
+{
+ auto transaction = GetTransaction(context);
+
+ transaction->AdvanceConsumer(ConsumerPath, QueuePath, PartitionIndex, OldOffset, NewOffset);
+
+ if (ShouldCommitTransaction()) {
+ WaitFor(transaction->Commit())
+ .ThrowOnError();
+ }
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/queue_commands.h b/yt/yt/client/driver/queue_commands.h
new file mode 100644
index 0000000000..0cc85c6444
--- /dev/null
+++ b/yt/yt/client/driver/queue_commands.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRegisterQueueConsumerCommand
+ : public TTypedCommand<NApi::TRegisterQueueConsumerOptions>
+{
+public:
+ TRegisterQueueConsumerCommand();
+
+private:
+ NYPath::TRichYPath QueuePath;
+ NYPath::TRichYPath ConsumerPath;
+ bool Vital;
+ std::optional<std::vector<int>> Partitions;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnregisterQueueConsumerCommand
+ : public TTypedCommand<NApi::TUnregisterQueueConsumerOptions>
+{
+public:
+ TUnregisterQueueConsumerCommand();
+
+private:
+ NYPath::TRichYPath QueuePath;
+ NYPath::TRichYPath ConsumerPath;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListQueueConsumerRegistrationsCommand
+ : public TTypedCommand<NApi::TListQueueConsumerRegistrationsOptions>
+{
+public:
+ TListQueueConsumerRegistrationsCommand();
+
+private:
+ std::optional<NYPath::TRichYPath> QueuePath;
+ std::optional<NYPath::TRichYPath> ConsumerPath;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPullQueueCommand
+ : public TTypedCommand<NApi::TPullQueueOptions>
+{
+public:
+ TPullQueueCommand();
+
+private:
+ NYPath::TRichYPath QueuePath;
+ i64 Offset;
+ int PartitionIndex;
+ NQueueClient::TQueueRowBatchReadOptions RowBatchReadOptions;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPullConsumerCommand
+ : public TTypedCommand<NApi::TPullConsumerOptions>
+{
+public:
+ TPullConsumerCommand();
+
+private:
+ NYPath::TRichYPath ConsumerPath;
+ NYPath::TRichYPath QueuePath;
+ i64 Offset;
+ int PartitionIndex;
+ NQueueClient::TQueueRowBatchReadOptions RowBatchReadOptions;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAdvanceConsumerOptions
+ : public TTabletWriteOptions
+{ };
+
+class TAdvanceConsumerCommand
+ : public TTypedCommand<TAdvanceConsumerOptions>
+{
+public:
+ TAdvanceConsumerCommand();
+
+private:
+ NYPath::TRichYPath ConsumerPath;
+ NYPath::TRichYPath QueuePath;
+ int PartitionIndex;
+ std::optional<i64> OldOffset;
+ i64 NewOffset;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/scheduler_commands.cpp b/yt/yt/client/driver/scheduler_commands.cpp
new file mode 100644
index 0000000000..fd871a3b6d
--- /dev/null
+++ b/yt/yt/client/driver/scheduler_commands.cpp
@@ -0,0 +1,611 @@
+#include "scheduler_commands.h"
+#include "driver.h"
+
+#include <yt/yt/client/api/file_reader.h>
+#include <yt/yt/client/api/rowset.h>
+
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/logging/fluent_log.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NApi;
+using namespace NConcurrency;
+using namespace NScheduler;
+using namespace NTableClient;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static inline const NLogging::TLogger JobShellStructuredLogger("JobShell");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDumpJobContextCommand::TDumpJobContextCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("path", Path);
+}
+
+void TDumpJobContextCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->DumpJobContext(JobId, Path))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobInputCommand::TGetJobInputCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("job_spec_source", Options.JobSpecSource)
+ .Default(EJobSpecSource::Auto);
+}
+
+void TGetJobInputCommand::DoExecute(ICommandContextPtr context)
+{
+ auto jobInputReader = WaitFor(context->GetClient()->GetJobInput(JobId, Options))
+ .ValueOrThrow();
+
+ auto output = context->Request().OutputStream;
+ PipeInputToOutput(jobInputReader, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobInputPathsCommand::TGetJobInputPathsCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("job_spec_source", Options.JobSpecSource)
+ .Default(EJobSpecSource::Auto);
+}
+
+void TGetJobInputPathsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto inputPaths = WaitFor(context->GetClient()->GetJobInputPaths(JobId, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(std::move(inputPaths));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobSpecCommand::TGetJobSpecCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("job_spec_source", Options.JobSpecSource)
+ .Default(EJobSpecSource::Auto);
+ RegisterParameter("omit_node_directory", Options.OmitNodeDirectory)
+ .Default(true);
+ RegisterParameter("omit_input_table_specs", Options.OmitInputTableSpecs)
+ .Default(false);
+ RegisterParameter("omit_output_table_specs", Options.OmitOutputTableSpecs)
+ .Default(false);
+}
+
+void TGetJobSpecCommand::DoExecute(ICommandContextPtr context)
+{
+ auto jobSpec = WaitFor(context->GetClient()->GetJobSpec(JobId, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(std::move(jobSpec));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobStderrCommand::TGetJobStderrCommand()
+{
+ RegisterParameter("job_id", JobId);
+}
+
+void TGetJobStderrCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->GetJobStderr(OperationIdOrAlias, JobId, Options))
+ .ValueOrThrow();
+
+ auto output = context->Request().OutputStream;
+ WaitFor(output->Write(result))
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobFailContextCommand::TGetJobFailContextCommand()
+{
+ RegisterParameter("job_id", JobId);
+}
+
+void TGetJobFailContextCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->GetJobFailContext(OperationIdOrAlias, JobId, Options))
+ .ValueOrThrow();
+
+ auto output = context->Request().OutputStream;
+ WaitFor(output->Write(result))
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListOperationsCommand::TListOperationsCommand()
+{
+ RegisterParameter("from_time", Options.FromTime)
+ .Optional();
+ RegisterParameter("to_time", Options.ToTime)
+ .Optional();
+ RegisterParameter("cursor_time", Options.CursorTime)
+ .Optional();
+ RegisterParameter("cursor_direction", Options.CursorDirection)
+ .Optional();
+ RegisterParameter("user", Options.UserFilter)
+ .Optional();
+ RegisterParameter("access", Options.AccessFilter)
+ .Optional();
+ RegisterParameter("state", Options.StateFilter)
+ .Optional();
+ RegisterParameter("type", Options.TypeFilter)
+ .Optional();
+ RegisterParameter("filter", Options.SubstrFilter)
+ .Optional();
+ RegisterParameter("pool_tree", Options.PoolTree)
+ .Optional();
+ RegisterParameter("pool", Options.Pool)
+ .Optional();
+ RegisterParameter("with_failed_jobs", Options.WithFailedJobs)
+ .Optional();
+ RegisterParameter("include_archive", Options.IncludeArchive)
+ .Optional();
+ RegisterParameter("include_counters", Options.IncludeCounters)
+ .Optional();
+ RegisterParameter("limit", Options.Limit)
+ .Optional();
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+ RegisterParameter("enable_ui_mode", EnableUIMode)
+ .Optional();
+ RegisterParameter("archive_fetching_timeout", Options.ArchiveFetchingTimeout)
+ .Optional();
+}
+
+void TListOperationsCommand::BuildOperations(const TListOperationsResult& result, TFluentMap fluent)
+{
+ bool needType = !Options.Attributes || Options.Attributes->contains("type");
+ // COMPAT(levysotsky): "operation_type" is a deprecated synonym for "type".
+ bool needOperationType = !Options.Attributes || Options.Attributes->contains("operation_type");
+
+ if (EnableUIMode) {
+ fluent
+ .Item("operations")
+ .BeginAttributes()
+ .Item("incomplete").Value(result.Incomplete)
+ .EndAttributes()
+ .DoListFor(result.Operations, [&] (TFluentList fluent, const TOperation& operation) {
+ fluent.Item().Do([&] (TFluentAny fluent) {
+ Serialize(operation, fluent.GetConsumer(), needType, needOperationType, /* idWithAttributes */ true);
+ });
+ });
+ } else {
+ fluent
+ .Item("operations")
+ .DoListFor(result.Operations, [&] (TFluentList fluent, const TOperation& operation) {
+ fluent.Item().Do([&] (TFluentAny fluent) {
+ Serialize(operation, fluent.GetConsumer(), needType, needOperationType);
+ });
+ })
+ .Item("incomplete").Value(result.Incomplete);
+ }
+}
+
+void TListOperationsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->ListOperations(Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Do(std::bind(&TListOperationsCommand::BuildOperations, this, result, std::placeholders::_1))
+ .DoIf(result.PoolTreeCounts.operator bool(), [&] (TFluentMap fluent) {
+ fluent.Item("pool_tree_counts").BeginMap()
+ .DoFor(*result.PoolTreeCounts, [] (TFluentMap fluent, const auto& item) {
+ fluent.Item(item.first).Value(item.second);
+ })
+ .EndMap();
+ })
+ .DoIf(result.PoolCounts.operator bool(), [&] (TFluentMap fluent) {
+ fluent.Item("pool_counts").BeginMap()
+ .DoFor(*result.PoolCounts, [] (TFluentMap fluent, const auto& item) {
+ fluent.Item(item.first).Value(item.second);
+ })
+ .EndMap();
+ })
+ .DoIf(result.UserCounts.operator bool(), [&] (TFluentMap fluent) {
+ fluent.Item("user_counts").BeginMap()
+ .DoFor(*result.UserCounts, [] (TFluentMap fluent, const auto& item) {
+ fluent.Item(item.first).Value(item.second);
+ })
+ .EndMap();
+ })
+ .DoIf(result.StateCounts.operator bool(), [&] (TFluentMap fluent) {
+ fluent.Item("state_counts").BeginMap()
+ .DoFor(TEnumTraits<EOperationState>::GetDomainValues(), [&result] (TFluentMap fluent, const EOperationState& item) {
+ i64 count = (*result.StateCounts)[item];
+ if (count) {
+ fluent.Item(FormatEnum(item)).Value(count);
+ }
+ })
+ .EndMap();
+ })
+ .DoIf(result.TypeCounts.operator bool(), [&] (TFluentMap fluent) {
+ fluent.Item("type_counts").BeginMap()
+ .DoFor(TEnumTraits<EOperationType>::GetDomainValues(), [&result] (TFluentMap fluent, const EOperationType& item) {
+ i64 count = (*result.TypeCounts)[item];
+ if (count) {
+ fluent.Item(FormatEnum(item)).Value(count);
+ }
+ })
+ .EndMap();
+ })
+ .OptionalItem("failed_jobs_count", result.FailedJobsCount)
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListJobsCommand::TListJobsCommand()
+{
+ RegisterParameter("type", Options.Type)
+ .Alias("job_type")
+ .Optional();
+ RegisterParameter("state", Options.State)
+ .Alias("job_state")
+ .Optional();
+ RegisterParameter("address", Options.Address)
+ .Optional();
+ RegisterParameter("with_stderr", Options.WithStderr)
+ .Optional();
+ RegisterParameter("with_spec", Options.WithSpec)
+ .Optional();
+ RegisterParameter("with_fail_context", Options.WithFailContext)
+ .Optional();
+ RegisterParameter("with_competitors", Options.WithCompetitors)
+ .Optional();
+ RegisterParameter("job_competition_id", Options.JobCompetitionId)
+ .Optional();
+ RegisterParameter("task_name", Options.TaskName)
+ .Optional();
+
+ RegisterParameter("sort_field", Options.SortField)
+ .Optional();
+ RegisterParameter("sort_order", Options.SortOrder)
+ .Optional();
+
+ RegisterParameter("limit", Options.Limit)
+ .Optional();
+ RegisterParameter("offset", Options.Offset)
+ .Optional();
+
+ RegisterParameter("data_source", Options.DataSource)
+ .Optional();
+
+ RegisterParameter("include_cypress", Options.IncludeCypress)
+ .Optional();
+ RegisterParameter("include_controller_agent", Options.IncludeControllerAgent)
+ .Alias("include_runtime")
+ .Alias("include_scheduler")
+ .Optional();
+ RegisterParameter("include_archive", Options.IncludeArchive)
+ .Optional();
+
+ RegisterParameter("running_jobs_lookbehind_period", Options.RunningJobsLookbehindPeriod)
+ .Optional();
+}
+
+void TListJobsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto result = WaitFor(context->GetClient()->ListJobs(OperationIdOrAlias, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("jobs").DoListFor(result.Jobs, [] (TFluentList fluent, const TJob& job) {
+ fluent
+ .Item().Do([&] (TFluentAny innerFluent) {
+ Serialize(job, innerFluent.GetConsumer(), TStringBuf("id"));
+ });
+ })
+ .Item("cypress_job_count").Value(result.CypressJobCount)
+ // COMPAT(asaitgalin): Remove it in favor of controller_agent_job_count
+ .Item("scheduler_job_count").Value(result.ControllerAgentJobCount)
+ .Item("controller_agent_job_count").Value(result.ControllerAgentJobCount)
+ .Item("archive_job_count").Value(result.ArchiveJobCount)
+ .Item("type_counts").DoMapFor(TEnumTraits<NJobTrackerClient::EJobType>::GetDomainValues(), [&] (TFluentMap fluent, const auto& item) {
+ i64 count = result.Statistics.TypeCounts[item];
+ if (count) {
+ fluent.Item(FormatEnum(item)).Value(count);
+ }
+ })
+ .Item("state_counts").DoMapFor(TEnumTraits<NJobTrackerClient::EJobState>::GetDomainValues(), [&] (TFluentMap fluent, const auto& item) {
+ i64 count = result.Statistics.StateCounts[item];
+ if (count) {
+ fluent.Item(FormatEnum(item)).Value(count);
+ }
+ })
+ .Item("errors").Value(result.Errors)
+ .EndMap());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetJobCommand::TGetJobCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+}
+
+void TGetJobCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->GetJob(OperationIdOrAlias, JobId, Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbandonJobCommand::TAbandonJobCommand()
+{
+ RegisterParameter("job_id", JobId);
+}
+
+void TAbandonJobCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->AbandonJob(JobId))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPollJobShellCommand::TPollJobShellCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("parameters", Parameters);
+ RegisterParameter("shell_name", ShellName)
+ .Default();
+
+ RegisterPostprocessor([&] {
+ // Compatibility with initial job shell protocol.
+ if (Parameters->GetType() == NYTree::ENodeType::String) {
+ Parameters = NYTree::ConvertToNode(NYson::TYsonString(Parameters->AsString()->GetValue()));
+ }
+ });
+}
+
+void TPollJobShellCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResponse = context->GetClient()->PollJobShell(
+ JobId,
+ ShellName,
+ ConvertToYsonString(Parameters),
+ Options);
+ auto response = WaitFor(asyncResponse)
+ .ValueOrThrow();
+
+ if (response.LoggingContext) {
+ LogStructuredEventFluently(JobShellStructuredLogger, NLogging::ELogLevel::Info)
+ .Do([&] (TFluentMap fluent) {
+ fluent.GetConsumer()->OnRaw(response.LoggingContext);
+ })
+ .Item("user").Value(context->Request().AuthenticatedUser)
+ .DoIf(static_cast<bool>(context->Request().UserRemoteAddress), [&] (TFluentMap fluent) {
+ fluent.Item("remote_address").Value(ToString(context->Request().UserRemoteAddress));
+ });
+ }
+
+ ProduceSingleOutputValue(context, "result", response.Result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortJobCommand::TAbortJobCommand()
+{
+ RegisterParameter("job_id", JobId);
+ RegisterParameter("interrupt_timeout", Options.InterruptTimeout)
+ .Optional();
+}
+
+void TAbortJobCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->AbortJob(JobId, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStartOperationCommand::TStartOperationCommand(std::optional<NScheduler::EOperationType> operationType)
+{
+ RegisterParameter("spec", Spec);
+ if (operationType) {
+ OperationType = *operationType;
+ } else {
+ RegisterParameter("operation_type", OperationType);
+ }
+}
+
+void TStartOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncOperationId = context->GetClient()->StartOperation(
+ OperationType,
+ ConvertToYsonString(Spec),
+ Options);
+
+ auto operationId = WaitFor(asyncOperationId)
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "operation_id", operationId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMapCommand::TMapCommand()
+ : TStartOperationCommand(EOperationType::Map)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMergeCommand::TMergeCommand()
+ : TStartOperationCommand(EOperationType::Merge)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSortCommand::TSortCommand()
+ : TStartOperationCommand(EOperationType::Sort)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEraseCommand::TEraseCommand()
+ : TStartOperationCommand(EOperationType::Erase)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReduceCommand::TReduceCommand()
+ : TStartOperationCommand(EOperationType::Reduce)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJoinReduceCommand::TJoinReduceCommand()
+ : TStartOperationCommand(EOperationType::JoinReduce)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMapReduceCommand::TMapReduceCommand()
+ : TStartOperationCommand(EOperationType::MapReduce)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRemoteCopyCommand::TRemoteCopyCommand()
+ : TStartOperationCommand(EOperationType::RemoteCopy)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortOperationCommand::TAbortOperationCommand()
+{
+ RegisterParameter("abort_message", Options.AbortMessage)
+ .Optional();
+}
+
+void TAbortOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->AbortOperation(OperationIdOrAlias, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSuspendOperationCommand::TSuspendOperationCommand()
+{
+ RegisterParameter("abort_running_jobs", Options.AbortRunningJobs)
+ .Optional();
+}
+
+void TSuspendOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->SuspendOperation(OperationIdOrAlias, Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TResumeOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->ResumeOperation(OperationIdOrAlias))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCompleteOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ WaitFor(context->GetClient()->CompleteOperation(OperationIdOrAlias))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUpdateOperationParametersCommand::TUpdateOperationParametersCommand()
+{
+ RegisterParameter("parameters", Parameters);
+}
+
+void TUpdateOperationParametersCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->UpdateOperationParameters(
+ OperationIdOrAlias,
+ ConvertToYsonString(Parameters),
+ Options);
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetOperationCommand::TGetOperationCommand()
+{
+ RegisterParameter("attributes", Options.Attributes)
+ .Optional();
+ RegisterParameter("include_runtime", Options.IncludeRuntime)
+ .Alias("include_scheduler")
+ .Optional();
+ RegisterParameter("maximum_cypress_progress_age", Options.MaximumCypressProgressAge)
+ .Optional();
+}
+
+void TGetOperationCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->GetOperation(OperationIdOrAlias, Options);
+ auto operation = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .Do([&] (TFluentAny fluent) {
+ Serialize(
+ operation,
+ fluent.GetConsumer(),
+ /* needType */ true,
+ /* needOperationType */ true);
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/scheduler_commands.h b/yt/yt/client/driver/scheduler_commands.h
new file mode 100644
index 0000000000..450e45be14
--- /dev/null
+++ b/yt/yt/client/driver/scheduler_commands.h
@@ -0,0 +1,382 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/job_tracker_client/public.h>
+
+#include <yt/yt/client/scheduler/operation_id_or_alias.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+class TSimpleOperationCommandBase
+ : public virtual TTypedCommandBase<TOptions>
+{
+private:
+ NScheduler::TOperationId OperationId;
+ std::optional<TString> OperationAlias;
+
+protected:
+ // Is calculated by two fields above.
+ NScheduler::TOperationIdOrAlias OperationIdOrAlias;
+
+public:
+ TSimpleOperationCommandBase()
+ {
+ this->RegisterParameter("operation_id", OperationId)
+ .Default();
+ this->RegisterParameter("operation_alias", OperationAlias)
+ .Default();
+
+ this->RegisterPostprocessor([&] {
+ if (!OperationId.IsEmpty() && OperationAlias.operator bool() ||
+ OperationId.IsEmpty() && !OperationAlias.operator bool())
+ {
+ THROW_ERROR_EXCEPTION("Exactly one of \"operation_id\" and \"operation_alias\" should be set")
+ << TErrorAttribute("operation_id", OperationId)
+ << TErrorAttribute("operation_alias", OperationAlias);
+ }
+
+ if (OperationId) {
+ OperationIdOrAlias = OperationId;
+ } else {
+ OperationIdOrAlias = *OperationAlias;
+ }
+ });
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDumpJobContextCommand
+ : public TTypedCommand<NApi::TDumpJobContextOptions>
+{
+public:
+ TDumpJobContextCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+ NYPath::TYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobInputCommand
+ : public TTypedCommand<NApi::TGetJobInputOptions>
+{
+public:
+ TGetJobInputCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobInputPathsCommand
+ : public TTypedCommand<NApi::TGetJobInputPathsOptions>
+{
+public:
+ TGetJobInputPathsCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobSpecCommand
+ : public TTypedCommand<NApi::TGetJobSpecOptions>
+{
+public:
+ TGetJobSpecCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobStderrCommand
+ : public TSimpleOperationCommandBase<NApi::TGetJobStderrOptions>
+{
+public:
+ TGetJobStderrCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobFailContextCommand
+ : public TSimpleOperationCommandBase<NApi::TGetJobFailContextOptions>
+{
+public:
+ TGetJobFailContextCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListOperationsCommand
+ : public TTypedCommand<NApi::TListOperationsOptions>
+{
+public:
+ TListOperationsCommand();
+
+private:
+ bool EnableUIMode = false;
+
+ void BuildOperations(const NApi::TListOperationsResult& result, NYTree::TFluentMap fluent);
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListJobsCommand
+ : public TSimpleOperationCommandBase<NApi::TListJobsOptions>
+{
+public:
+ TListJobsCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetJobCommand
+ : public TSimpleOperationCommandBase<NApi::TGetJobOptions>
+{
+public:
+ TGetJobCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbandonJobCommand
+ : public TTypedCommand<NApi::TAbandonJobOptions>
+{
+public:
+ TAbandonJobCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPollJobShellCommand
+ : public TTypedCommand<NApi::TPollJobShellOptions>
+{
+public:
+ TPollJobShellCommand();
+
+private:
+ NJobTrackerClient::TJobId JobId;
+ NYTree::INodePtr Parameters;
+ std::optional<TString> ShellName;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortJobCommand
+ : public TTypedCommand<NApi::TAbortJobOptions>
+{
+private:
+ NJobTrackerClient::TJobId JobId;
+
+public:
+ TAbortJobCommand();
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStartOperationCommand
+ : public TTypedCommand<NApi::TStartOperationOptions>
+{
+public:
+ explicit TStartOperationCommand(
+ std::optional<NScheduler::EOperationType> operationType = std::optional<NScheduler::EOperationType>());
+
+private:
+ NYTree::INodePtr Spec;
+ NScheduler::EOperationType OperationType;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMapCommand
+ : public TStartOperationCommand
+{
+public:
+ TMapCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMergeCommand
+ : public TStartOperationCommand
+{
+public:
+ TMergeCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSortCommand
+ : public TStartOperationCommand
+{
+public:
+ TSortCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEraseCommand
+ : public TStartOperationCommand
+{
+public:
+ TEraseCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReduceCommand
+ : public TStartOperationCommand
+{
+public:
+ TReduceCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJoinReduceCommand
+ : public TStartOperationCommand
+{
+public:
+ TJoinReduceCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMapReduceCommand
+ : public TStartOperationCommand
+{
+public:
+ TMapReduceCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteCopyCommand
+ : public TStartOperationCommand
+{
+public:
+ TRemoteCopyCommand();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortOperationCommand
+ : public TSimpleOperationCommandBase<NApi::TAbortOperationOptions>
+{
+public:
+ TAbortOperationCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendOperationCommand
+ : public TSimpleOperationCommandBase<NApi::TSuspendOperationOptions>
+{
+public:
+ TSuspendOperationCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResumeOperationCommand
+ : public TSimpleOperationCommandBase<NApi::TResumeOperationOptions>
+{
+public:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompleteOperationCommand
+ : public TSimpleOperationCommandBase<NApi::TCompleteOperationOptions>
+{
+public:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUpdateOperationParametersCommand
+ : public TSimpleOperationCommandBase<NApi::TUpdateOperationParametersOptions>
+{
+public:
+ TUpdateOperationParametersCommand();
+
+private:
+ NYTree::INodePtr Parameters;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetOperationCommand
+ : public TSimpleOperationCommandBase<NApi::TGetOperationOptions>
+{
+public:
+ TGetOperationCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/table_commands.cpp b/yt/yt/client/driver/table_commands.cpp
new file mode 100644
index 0000000000..3d5654ba42
--- /dev/null
+++ b/yt/yt/client/driver/table_commands.cpp
@@ -0,0 +1,1378 @@
+#include "table_commands.h"
+#include "config.h"
+#include "helpers.h"
+
+#include <yt/yt/client/api/rowset.h>
+#include <yt/yt/client/api/skynet.h>
+
+#include <yt/yt/client/chaos_client/replication_card_serialization.h>
+
+#include <yt/yt/client/table_client/adapters.h>
+#include <yt/yt/client/table_client/blob_reader.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/table_consumer.h>
+#include <yt/yt/client/table_client/table_output.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/versioned_writer.h>
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/formats/config.h>
+#include <yt/yt/client/formats/parser.h>
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NDriver {
+
+using namespace NApi;
+using namespace NChunkClient;
+using namespace NConcurrency;
+using namespace NFormats;
+using namespace NHiveClient;
+using namespace NQueryClient;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NTransactionClient;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTracing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NLogging::TLogger WithCommandTag(
+ const NLogging::TLogger& logger,
+ const ICommandContextPtr& context)
+{
+ return logger.WithTag("Command: %v",
+ context->Request().CommandName);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadTableCommand::TReadTableCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("table_reader", TableReader)
+ .Default();
+ RegisterParameter("control_attributes", ControlAttributes)
+ .DefaultNew();
+ RegisterParameter("unordered", Unordered)
+ .Default(false);
+ RegisterParameter("start_row_index_only", StartRowIndexOnly)
+ .Default(false);
+ RegisterParameter("omit_inaccessible_columns", Options.OmitInaccessibleColumns)
+ .Default(false);
+}
+
+void TReadTableCommand::DoExecute(ICommandContextPtr context)
+{
+ YT_LOG_DEBUG("Executing \"read_table\" command (Path: %v, Unordered: %v, StartRowIndexOnly: %v, "
+ "OmitInaccessibleColumns: %v)",
+ Path,
+ Unordered,
+ StartRowIndexOnly,
+ Options.OmitInaccessibleColumns);
+ Options.Ping = true;
+ Options.EnableTableIndex = ControlAttributes->EnableTableIndex;
+ Options.EnableRowIndex = ControlAttributes->EnableRowIndex;
+ Options.EnableRangeIndex = ControlAttributes->EnableRangeIndex;
+ Options.EnableTabletIndex = ControlAttributes->EnableTabletIndex;
+ Options.Config = UpdateYsonStruct(
+ context->GetConfig()->TableReader,
+ TableReader);
+
+ if (StartRowIndexOnly) {
+ Options.Config->WindowSize = 1;
+ Options.Config->GroupSize = 1;
+ }
+
+ PutMethodInfoInTraceContext("read_table");
+
+ auto reader = WaitFor(context->GetClient()->CreateTableReader(
+ Path,
+ Options))
+ .ValueOrThrow();
+
+ ProduceResponseParameters(context, [&] (IYsonConsumer* consumer) {
+ BuildYsonMapFragmentFluently(consumer)
+ .Item("approximate_row_count").Value(reader->GetTotalRowCount())
+ .Item("omitted_inaccessible_columns").Value(reader->GetOmittedInaccessibleColumns())
+ .DoIf(reader->GetTotalRowCount() > 0, [&](auto fluent) {
+ fluent
+ .Item("start_row_index").Value(reader->GetStartRowIndex());
+ });
+ });
+
+ if (StartRowIndexOnly) {
+ return;
+ }
+
+ auto writer = CreateStaticTableWriterForFormat(
+ context->GetOutputFormat(),
+ reader->GetNameTable(),
+ {reader->GetTableSchema()},
+ context->Request().OutputStream,
+ false,
+ ControlAttributes,
+ 0);
+
+ auto finally = Finally([&] () {
+ auto dataStatistics = reader->GetDataStatistics();
+ YT_LOG_DEBUG("Command statistics (RowCount: %v, WrittenSize: %v, "
+ "ReadUncompressedDataSize: %v, ReadCompressedDataSize: %v, "
+ "OmittedInaccessibleColumns: %v)",
+ dataStatistics.row_count(),
+ writer->GetWrittenSize(),
+ dataStatistics.uncompressed_data_size(),
+ dataStatistics.compressed_data_size(),
+ reader->GetOmittedInaccessibleColumns());
+ });
+
+ TPipeReaderToWriterOptions options;
+ options.BufferRowCount = context->GetConfig()->ReadBufferRowCount;
+ PipeReaderToWriter(
+ reader,
+ writer,
+ options);
+}
+
+bool TReadTableCommand::HasResponseParameters() const
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadBlobTableCommand::TReadBlobTableCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("part_size", PartSize);
+ RegisterParameter("table_reader", TableReader)
+ .Default();
+ RegisterParameter("part_index_column_name", PartIndexColumnName)
+ .Default();
+ RegisterParameter("data_column_name", DataColumnName)
+ .Default();
+ RegisterParameter("start_part_index", StartPartIndex)
+ .Default(0);
+ RegisterParameter("offset", Offset)
+ .Default(0);
+}
+
+void TReadBlobTableCommand::DoExecute(ICommandContextPtr context)
+{
+ if (Offset < 0) {
+ THROW_ERROR_EXCEPTION("Offset must be nonnegative");
+ }
+
+ if (PartSize <= 0) {
+ THROW_ERROR_EXCEPTION("Part size must be positive");
+ }
+ Options.Ping = true;
+
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableReader,
+ TableReader);
+
+ Options.Config = config;
+
+ auto reader = WaitFor(context->GetClient()->CreateTableReader(
+ Path,
+ Options))
+ .ValueOrThrow();
+
+ auto input = CreateBlobTableReader(
+ std::move(reader),
+ PartIndexColumnName,
+ DataColumnName,
+ StartPartIndex,
+ Offset,
+ PartSize);
+
+ auto output = context->Request().OutputStream;
+
+ // TODO(ignat): implement proper Pipe* function.
+ while (true) {
+ auto block = WaitFor(input->Read())
+ .ValueOrThrow();
+
+ if (!block)
+ break;
+
+ WaitFor(output->Write(block))
+ .ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLocateSkynetShareCommand::TLocateSkynetShareCommand()
+{
+ RegisterParameter("path", Path);
+}
+
+void TLocateSkynetShareCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Config = context->GetConfig()->TableReader;
+
+ auto asyncSkynetPartsLocations = context->GetClient()->LocateSkynetShare(
+ Path,
+ Options);
+
+ auto skynetPartsLocations = WaitFor(asyncSkynetPartsLocations);
+
+ auto format = context->GetOutputFormat();
+ auto syncOutputStream = CreateBufferedSyncAdapter(context->Request().OutputStream);
+
+ auto consumer = CreateConsumerForFormat(
+ format,
+ EDataType::Structured,
+ syncOutputStream.get());
+
+ Serialize(*skynetPartsLocations.ValueOrThrow(), consumer.get());
+ consumer->Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWriteTableCommand::TWriteTableCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("table_writer", TableWriter)
+ .Default();
+ RegisterParameter("max_row_buffer_size", MaxRowBufferSize)
+ .Default(1_MB);
+}
+
+void TWriteTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto transaction = AttachTransaction(context, false);
+
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableWriter,
+ TableWriter);
+
+ // XXX(babenko): temporary workaround; this is how it actually works but not how it is intended to be.
+ Options.PingAncestors = true;
+ Options.Config = config;
+
+ PutMethodInfoInTraceContext("write_table");
+
+ auto apiWriter = WaitFor(context->GetClient()->CreateTableWriter(
+ Path,
+ Options))
+ .ValueOrThrow();
+
+ auto schemalessWriter = CreateSchemalessFromApiWriterAdapter(std::move(apiWriter));
+
+ TWritingValueConsumer valueConsumer(
+ schemalessWriter,
+ ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes()),
+ MaxRowBufferSize);
+
+ TTableOutput output(CreateParserForFormat(
+ context->GetInputFormat(),
+ &valueConsumer));
+
+ PipeInputToOutput(context->Request().InputStream, &output, MaxRowBufferSize);
+
+ WaitFor(valueConsumer.Flush())
+ .ThrowOnError();
+
+ WaitFor(schemalessWriter->Close())
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetTableColumnarStatisticsCommand::TGetTableColumnarStatisticsCommand()
+{
+ RegisterParameter("paths", Paths);
+ RegisterParameter("fetcher_mode", FetcherMode)
+ .Default(EColumnarStatisticsFetcherMode::FromNodes);
+ RegisterParameter("max_chunks_per_node_fetch", MaxChunksPerNodeFetch)
+ .Default();
+ RegisterParameter("enable_early_finish", EnableEarlyFinish)
+ .Default(true);
+}
+
+void TGetTableColumnarStatisticsCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.FetchChunkSpecConfig = context->GetConfig()->TableReader;
+ Options.FetcherConfig = context->GetConfig()->Fetcher;
+ Options.EnableEarlyFinish = EnableEarlyFinish;
+
+ if (MaxChunksPerNodeFetch) {
+ Options.FetcherConfig = CloneYsonStruct(Options.FetcherConfig);
+ Options.FetcherConfig->MaxChunksPerNodeFetch = *MaxChunksPerNodeFetch;
+ }
+
+ Options.FetcherMode = FetcherMode;
+
+ auto transaction = AttachTransaction(context, false);
+
+ std::vector<TFuture<NYson::TYsonString>> asyncSchemaYsons;
+ for (int index = 0; index < std::ssize(Paths); ++index) {
+ if (Paths[index].GetColumns()) {
+ continue;
+ }
+ TGetNodeOptions options;
+ static_cast<TTransactionalOptions&>(options) = Options;
+ static_cast<TTimeoutOptions&>(options) = Options;
+ asyncSchemaYsons.push_back(context->GetClient()->GetNode(Paths[index].GetPath() + "/@schema", options));
+ }
+
+ if (!asyncSchemaYsons.empty()) {
+ YT_LOG_DEBUG("Fetching schemas for tables without column selectors (TableCount: %v)", asyncSchemaYsons.size());
+ auto allSchemas = WaitFor(AllSucceeded(asyncSchemaYsons))
+ .ValueOrThrow();
+ for (int pathIndex = 0, missingColumnIndex = 0; pathIndex < std::ssize(Paths); ++pathIndex) {
+ if (!Paths[pathIndex].GetColumns()) {
+ auto columnNames = ConvertTo<TTableSchema>(allSchemas[missingColumnIndex]).GetColumnNames();
+ if (columnNames.empty()) {
+ THROW_ERROR_EXCEPTION("Table %Qv does not have schema and column selector is not specified", Paths[pathIndex]);
+ }
+ Paths[pathIndex].SetColumns(columnNames);
+ ++missingColumnIndex;
+ }
+ }
+ }
+
+ YT_LOG_DEBUG("Starting fetching columnar statistics");
+
+ auto allStatisticsOrError = WaitFor(context->GetClient()->GetColumnarStatistics(Paths, Options));
+
+ YT_LOG_DEBUG("Finished fetching columnar statistics");
+
+ auto allStatistics = allStatisticsOrError.ValueOrThrow();
+
+ YT_VERIFY(allStatistics.size() == Paths.size());
+ for (int index = 0; index < std::ssize(allStatistics); ++index) {
+ YT_VERIFY(Paths[index].GetColumns()->size() == allStatistics[index].ColumnDataWeights.size());
+ }
+
+ ProduceOutput(context, [&] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .DoList([&] (TFluentList fluent) {
+ for (int index = 0; index < std::ssize(Paths); ++index) {
+ const auto& columns = Paths[index].GetColumns();
+ const auto& statistics = allStatistics[index];
+ fluent
+ .Item()
+ .BeginMap()
+ .Item("column_data_weights").DoMap([&] (TFluentMap fluent) {
+ for (int index = 0; index < std::ssize(*columns); ++index) {
+ fluent
+ .Item((*columns)[index]).Value(statistics.ColumnDataWeights[index]);
+ }
+ })
+ .OptionalItem("timestamp_total_weight", statistics.TimestampTotalWeight)
+ .Item("legacy_chunks_data_weight").Value(statistics.LegacyChunkDataWeight)
+ .EndMap();
+ }
+ });
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPartitionTablesCommand::TPartitionTablesCommand()
+{
+ RegisterParameter("paths", Paths);
+ RegisterParameter("partition_mode", PartitionMode)
+ .Default(ETablePartitionMode::Unordered);
+ RegisterParameter("data_weight_per_partition", DataWeightPerPartition);
+ RegisterParameter("max_partition_count", MaxPartitionCount)
+ .Default();
+ RegisterParameter("enable_key_guarantee", EnableKeyGuarantee)
+ .Default(false);
+ RegisterParameter("adjust_data_weight_per_partition", AdjustDataWeightPerPartition)
+ .Default(true);
+}
+
+void TPartitionTablesCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.FetchChunkSpecConfig = context->GetConfig()->TableReader;
+ Options.FetcherConfig = context->GetConfig()->Fetcher;
+ Options.ChunkSliceFetcherConfig = New<TChunkSliceFetcherConfig>();
+
+ Options.PartitionMode = PartitionMode;
+ Options.DataWeightPerPartition = DataWeightPerPartition;
+ Options.MaxPartitionCount = MaxPartitionCount;
+ Options.EnableKeyGuarantee = EnableKeyGuarantee;
+ Options.AdjustDataWeightPerPartition = AdjustDataWeightPerPartition;
+
+ auto partitions = WaitFor(context->GetClient()->PartitionTables(Paths, Options))
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(ConvertToYsonString(partitions));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMountTableCommand::TMountTableCommand()
+{
+ RegisterParameter("cell_id", Options.CellId)
+ .Optional();
+ RegisterParameter("freeze", Options.Freeze)
+ .Optional();
+ RegisterParameter("target_cell_ids", Options.TargetCellIds)
+ .Optional();
+}
+
+void TMountTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->MountTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnmountTableCommand::TUnmountTableCommand()
+{
+ RegisterParameter("force", Options.Force)
+ .Optional();
+}
+
+void TUnmountTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->UnmountTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRemountTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->RemountTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFreezeTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->FreezeTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TUnfreezeTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->UnfreezeTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReshardTableCommand::TReshardTableCommand()
+{
+ RegisterParameter("pivot_keys", PivotKeys)
+ .Default();
+ RegisterParameter("tablet_count", TabletCount)
+ .Default()
+ .GreaterThan(0);
+ RegisterParameter("uniform", Options.Uniform)
+ .Default();
+ RegisterParameter("enable_slicing", Options.EnableSlicing)
+ .Default();
+ RegisterParameter("slicing_accuracy", Options.SlicingAccuracy)
+ .Default()
+ .GreaterThan(0);
+
+ RegisterPostprocessor([&] () {
+ if (PivotKeys && TabletCount) {
+ THROW_ERROR_EXCEPTION("Cannot specify both \"pivot_keys\" and \"tablet_count\"");
+ }
+ if (!PivotKeys && !TabletCount) {
+ THROW_ERROR_EXCEPTION("Must specify either \"pivot_keys\" or \"tablet_count\"");
+ }
+ if (Options.Uniform && PivotKeys) {
+ THROW_ERROR_EXCEPTION("\"uniform\" can be specified only with \"tablet_count\"");
+ }
+ if (Options.EnableSlicing && PivotKeys) {
+ THROW_ERROR_EXCEPTION("\"enable_slicing\" can be specified only with \"tablet_count\"");
+ }
+ if (Options.EnableSlicing && Options.Uniform) {
+ THROW_ERROR_EXCEPTION("Cannot specify both \"enable_slicing\" and \"uniform\"");
+ }
+ if (Options.SlicingAccuracy && !Options.EnableSlicing) {
+ THROW_ERROR_EXCEPTION("\"slicing_accuracy\" can be specified only with \"enable_slicing\"");
+ }
+ });
+}
+
+void TReshardTableCommand::DoExecute(ICommandContextPtr context)
+{
+ TFuture<void> asyncResult;
+ if (PivotKeys) {
+ asyncResult = context->GetClient()->ReshardTable(
+ Path.GetPath(),
+ *PivotKeys,
+ Options);
+ } else {
+ asyncResult = context->GetClient()->ReshardTable(
+ Path.GetPath(),
+ *TabletCount,
+ Options);
+ }
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReshardTableAutomaticCommand::TReshardTableAutomaticCommand()
+{
+ RegisterParameter("keep_actions", Options.KeepActions)
+ .Default(false);
+}
+
+void TReshardTableAutomaticCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->ReshardTableAutomatic(
+ Path.GetPath(),
+ Options);
+ auto tabletActions = WaitFor(asyncResult)
+ .ValueOrThrow();
+ context->ProduceOutputValue(BuildYsonStringFluently().List(tabletActions));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAlterTableCommand::TAlterTableCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("schema", Options.Schema)
+ .Optional();
+ RegisterParameter("schema_id", Options.SchemaId)
+ .Optional();
+ RegisterParameter("dynamic", Options.Dynamic)
+ .Optional();
+ RegisterParameter("upstream_replica_id", Options.UpstreamReplicaId)
+ .Optional();
+ RegisterParameter("schema_modification", Options.SchemaModification)
+ .Optional();
+ RegisterParameter("replication_progress", Options.ReplicationProgress)
+ .Optional();
+}
+
+void TAlterTableCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->AlterTable(
+ Path.GetPath(),
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSelectRowsCommand::TSelectRowsCommand()
+{
+ RegisterParameter("query", Query);
+ RegisterParameter("input_row_limit", Options.InputRowLimit)
+ .Optional();
+ RegisterParameter("output_row_limit", Options.OutputRowLimit)
+ .Optional();
+ RegisterParameter("use_multijoin", Options.UseMultijoin)
+ .Optional();
+ RegisterParameter("allow_full_scan", Options.AllowFullScan)
+ .Optional();
+ RegisterParameter("allow_join_without_index", Options.AllowJoinWithoutIndex)
+ .Optional();
+ RegisterParameter("execution_pool", Options.ExecutionPool)
+ .Optional();
+ RegisterParameter("fail_on_incomplete_result", Options.FailOnIncompleteResult)
+ .Optional();
+ RegisterParameter("enable_code_cache", Options.EnableCodeCache)
+ .Optional();
+ RegisterParameter("workload_descriptor", Options.WorkloadDescriptor)
+ .Optional();
+ RegisterParameter("enable_statistics", EnableStatistics)
+ .Optional();
+ RegisterParameter("replica_consistency", Options.ReplicaConsistency)
+ .Optional();
+ RegisterParameter("placeholder_values", PlaceholderValues)
+ .Optional();
+}
+
+bool TSelectRowsCommand::HasResponseParameters() const
+{
+ return true;
+}
+
+void TSelectRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto clientBase = GetClientBase(context);
+
+ if (PlaceholderValues) {
+ Options.PlaceholderValues = ConvertToYsonString(PlaceholderValues);
+ }
+
+ auto result = WaitFor(clientBase->SelectRows(Query, Options))
+ .ValueOrThrow();
+
+ const auto& rowset = result.Rowset;
+ const auto& statistics = result.Statistics;
+
+ YT_LOG_INFO("Query result statistics (%v)", statistics);
+
+ if (EnableStatistics) {
+ ProduceResponseParameters(context, [&] (NYson::IYsonConsumer* consumer) {
+ Serialize(statistics, consumer);
+ });
+ }
+
+ auto format = context->GetOutputFormat();
+ auto output = context->Request().OutputStream;
+ auto writer = CreateSchemafulWriterForFormat(format, rowset->GetSchema(), output);
+
+ writer->Write(rowset->GetRows());
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TExplainQueryCommand::TExplainQueryCommand()
+{
+ RegisterParameter("query", Query);
+ RegisterParameter("verbose_output", Options.VerboseOutput)
+ .Optional();
+}
+
+void TExplainQueryCommand::DoExecute(ICommandContextPtr context)
+{
+ auto clientBase = GetClientBase(context);
+ auto result = WaitFor(clientBase->ExplainQuery(Query, Options))
+ .ValueOrThrow();
+ context->ProduceOutputValue(result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static std::vector<TUnversionedRow> ParseRows(
+ ICommandContextPtr context,
+ TBuildingValueConsumer* valueConsumer)
+{
+ TTableOutput output(CreateParserForFormat(
+ context->GetInputFormat(),
+ valueConsumer));
+
+ PipeInputToOutput(context->Request().InputStream, &output, 64_KB);
+ return valueConsumer->GetRows();
+}
+
+TInsertRowsCommand::TInsertRowsCommand()
+{
+ RegisterParameter("require_sync_replica", Options.RequireSyncReplica)
+ .Optional();
+ RegisterParameter("sequence_number", Options.SequenceNumber)
+ .Optional();
+ RegisterParameter("table_writer", TableWriter)
+ .Default();
+ RegisterParameter("path", Path);
+ RegisterParameter("update", Update)
+ .Default(false);
+ RegisterParameter("aggregate", Aggregate)
+ .Default(false);
+ RegisterParameter("allow_missing_key_columns", Options.AllowMissingKeyColumns)
+ .Default(false);
+}
+
+void TInsertRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableWriter,
+ TableWriter);
+
+ auto tableMountCache = context->GetClient()->GetTableMountCache();
+ auto tableInfo = WaitFor(tableMountCache->GetTableInfo(Path.GetPath()))
+ .ValueOrThrow();
+
+ tableInfo->ValidateDynamic();
+
+ if (!tableInfo->IsSorted() && Update) {
+ THROW_ERROR_EXCEPTION("Cannot use \"update\" mode for ordered tables");
+ }
+
+ struct TInsertRowsBufferTag
+ { };
+
+ auto insertRowsFormatConfig = ConvertTo<TInsertRowsFormatConfigPtr>(context->GetInputFormat().Attributes());
+ auto typeConversionConfig = ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes());
+ // Parse input data.
+ TBuildingValueConsumer valueConsumer(
+ tableInfo->Schemas[ETableSchemaKind::Write],
+ WithCommandTag(Logger, context),
+ insertRowsFormatConfig->EnableNullToYsonEntityConversion,
+ typeConversionConfig);
+ valueConsumer.SetAggregate(Aggregate);
+ valueConsumer.SetTreatMissingAsNull(!Update);
+ valueConsumer.SetAllowMissingKeyColumns(Options.AllowMissingKeyColumns);
+
+ auto rows = ParseRows(context, &valueConsumer);
+ auto rowBuffer = New<TRowBuffer>(TInsertRowsBufferTag());
+ auto capturedRows = rowBuffer->CaptureRows(rows);
+ auto rowRange = MakeSharedRange(
+ std::vector<TUnversionedRow>(capturedRows.begin(), capturedRows.end()),
+ std::move(rowBuffer));
+
+ // Run writes.
+ auto transaction = GetTransaction(context);
+
+ transaction->WriteRows(
+ Path.GetPath(),
+ valueConsumer.GetNameTable(),
+ std::move(rowRange),
+ Options);
+
+ if (ShouldCommitTransaction()) {
+ WaitFor(transaction->Commit())
+ .ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLookupRowsCommand::TLookupRowsCommand()
+{
+ RegisterParameter("table_writer", TableWriter)
+ .Default();
+ RegisterParameter("path", Path);
+ RegisterParameter("column_names", ColumnNames)
+ .Default();
+ RegisterParameter("versioned", Versioned)
+ .Default(false);
+ RegisterParameter("retention_config", RetentionConfig)
+ .Optional();
+ RegisterParameter("keep_missing_rows", Options.KeepMissingRows)
+ .Optional();
+ RegisterParameter("enable_partial_result", Options.EnablePartialResult)
+ .Optional();
+ RegisterParameter("use_lookup_cache", Options.UseLookupCache)
+ .Optional();
+ RegisterParameter("cached_sync_replicas_timeout", Options.CachedSyncReplicasTimeout)
+ .Optional();
+ RegisterParameter("replica_consistency", Options.ReplicaConsistency)
+ .Optional();
+}
+
+void TLookupRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto tableMountCache = context->GetClient()->GetTableMountCache();
+ auto asyncTableInfo = tableMountCache->GetTableInfo(Path.GetPath());
+ auto tableInfo = WaitFor(asyncTableInfo)
+ .ValueOrThrow();
+
+ tableInfo->ValidateDynamic();
+
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableWriter,
+ TableWriter);
+
+ if (Path.GetColumns()) {
+ THROW_ERROR_EXCEPTION("Columns cannot be specified with table path, use \"column_names\" instead")
+ << TErrorAttribute("rich_ypath", Path);
+ }
+ if (Path.HasNontrivialRanges()) {
+ THROW_ERROR_EXCEPTION("Ranges cannot be specified")
+ << TErrorAttribute("rich_ypath", Path);
+ }
+
+ struct TLookupRowsBufferTag
+ { };
+
+ // Parse input data.
+ TBuildingValueConsumer valueConsumer(
+ tableInfo->Schemas[ETableSchemaKind::Lookup],
+ WithCommandTag(Logger, context),
+ /*convertNullToEntity*/ false,
+ ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes()));
+ auto keys = ParseRows(context, &valueConsumer);
+ auto rowBuffer = New<TRowBuffer>(TLookupRowsBufferTag());
+ auto capturedKeys = rowBuffer->CaptureRows(keys);
+ auto mutableKeyRange = MakeSharedRange(std::move(capturedKeys), std::move(rowBuffer));
+ auto keyRange = TSharedRange<TUnversionedRow>(
+ static_cast<const TUnversionedRow*>(mutableKeyRange.Begin()),
+ static_cast<const TUnversionedRow*>(mutableKeyRange.End()),
+ mutableKeyRange.GetHolder());
+ auto nameTable = valueConsumer.GetNameTable();
+
+ if (ColumnNames) {
+ TColumnFilter::TIndexes columnFilterIndexes;
+ for (const auto& name : *ColumnNames) {
+ auto optionalIndex = nameTable->FindId(name);
+ if (!optionalIndex) {
+ if (!tableInfo->Schemas[ETableSchemaKind::Primary]->FindColumn(name)) {
+ THROW_ERROR_EXCEPTION("No such column %Qv",
+ name);
+ }
+ optionalIndex = nameTable->GetIdOrRegisterName(name);
+ }
+ columnFilterIndexes.push_back(*optionalIndex);
+ }
+ Options.ColumnFilter = TColumnFilter(std::move(columnFilterIndexes));
+ }
+
+ // Run lookup.
+ auto format = context->GetOutputFormat();
+ auto output = context->Request().OutputStream;
+
+ auto clientBase = GetClientBase(context);
+
+ if (Versioned) {
+ TVersionedLookupRowsOptions versionedOptions;
+ versionedOptions.ColumnFilter = Options.ColumnFilter;
+ versionedOptions.KeepMissingRows = Options.KeepMissingRows;
+ versionedOptions.EnablePartialResult = Options.EnablePartialResult;
+ versionedOptions.UseLookupCache = Options.UseLookupCache;
+ versionedOptions.Timestamp = Options.Timestamp;
+ versionedOptions.CachedSyncReplicasTimeout = Options.CachedSyncReplicasTimeout;
+ versionedOptions.RetentionConfig = RetentionConfig;
+ versionedOptions.ReplicaConsistency = Options.ReplicaConsistency;
+ auto asyncRowset = clientBase->VersionedLookupRows(Path.GetPath(), std::move(nameTable), std::move(keyRange), versionedOptions);
+ auto rowset = WaitFor(asyncRowset)
+ .ValueOrThrow();
+ auto writer = CreateVersionedWriterForFormat(format, rowset->GetSchema(), output);
+ writer->Write(rowset->GetRows());
+ WaitFor(writer->Close())
+ .ThrowOnError();
+ } else {
+ auto asyncRowset = clientBase->LookupRows(Path.GetPath(), std::move(nameTable), std::move(keyRange), Options);
+ auto rowset = WaitFor(asyncRowset)
+ .ValueOrThrow();
+
+ auto writer = CreateSchemafulWriterForFormat(format, rowset->GetSchema(), output);
+ writer->Write(rowset->GetRows());
+ WaitFor(writer->Close())
+ .ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPullRowsCommand::TPullRowsCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("upstream_replica_id", Options.UpstreamReplicaId);
+ RegisterParameter("replication_progress", Options.ReplicationProgress);
+ RegisterParameter("upper_timestamp", Options.UpperTimestamp)
+ .Optional();
+ RegisterParameter("order_rows_by_timestamp", Options.OrderRowsByTimestamp)
+ .Default(false);
+}
+
+void TPullRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ if (Path.HasNontrivialRanges()) {
+ THROW_ERROR_EXCEPTION("Ranges cannot be specified")
+ << TErrorAttribute("rich_ypath", Path);
+ }
+
+ auto format = context->GetOutputFormat();
+ auto output = context->Request().OutputStream;
+
+ auto client = context->GetClient();
+ auto pullRowsFuture = client->PullRows(Path.GetPath(), Options);
+ auto pullResult = WaitFor(pullRowsFuture)
+ .ValueOrThrow();
+
+ ProduceResponseParameters(context, [&] (IYsonConsumer* consumer) {
+ BuildYsonMapFragmentFluently(consumer)
+ .Item("replication_progress").Value(pullResult.ReplicationProgress)
+ .Item("end_replication_row_indexes")
+ .DoMapFor(pullResult.EndReplicationRowIndexes, [] (auto fluent, const auto& pair) {
+ fluent
+ .Item(ToString(pair.first)).Value(pair.second);
+ });
+ });
+
+ if (pullResult.Versioned) {
+ auto writer = CreateVersionedWriterForFormat(format, pullResult.Rowset->GetSchema(), output);
+ writer->Write(ReinterpretCastRange<TVersionedRow>(pullResult.Rowset->GetRows()));
+ WaitFor(writer->Close())
+ .ThrowOnError();
+ } else {
+ auto writer = CreateSchemafulWriterForFormat(format, pullResult.Rowset->GetSchema(), output);
+ writer->Write(ReinterpretCastRange<TUnversionedRow>(pullResult.Rowset->GetRows()));
+ WaitFor(writer->Close())
+ .ThrowOnError();
+ }
+}
+
+bool TPullRowsCommand::HasResponseParameters() const
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetInSyncReplicasCommand::TGetInSyncReplicasCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("timestamp", Options.Timestamp);
+ RegisterParameter("all_keys", AllKeys)
+ .Default(false);
+ RegisterParameter("cached_sync_replicas_timeout", Options.CachedSyncReplicasTimeout)
+ .Optional();
+}
+
+void TGetInSyncReplicasCommand::DoExecute(ICommandContextPtr context)
+{
+ auto tableMountCache = context->GetClient()->GetTableMountCache();
+ auto asyncTableInfo = tableMountCache->GetTableInfo(Path.GetPath());
+ auto tableInfo = WaitFor(asyncTableInfo)
+ .ValueOrThrow();
+
+ tableInfo->ValidateDynamic();
+ if (!tableInfo->IsChaosReplica() && !tableInfo->IsChaosReplicated()) {
+ tableInfo->ValidateReplicated();
+ }
+
+ TFuture<std::vector<NTabletClient::TTableReplicaId>> asyncReplicas;
+ if (AllKeys) {
+ asyncReplicas = context->GetClient()->GetInSyncReplicas(
+ Path.GetPath(),
+ Options);
+ } else {
+ struct TInSyncBufferTag
+ { };
+
+ // Parse input data.
+ TBuildingValueConsumer valueConsumer(
+ tableInfo->Schemas[ETableSchemaKind::Lookup],
+ WithCommandTag(Logger, context),
+ /*convertNullToEntity*/ false,
+ ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes()));
+ auto keys = ParseRows(context, &valueConsumer);
+ auto rowBuffer = New<TRowBuffer>(TInSyncBufferTag());
+ auto capturedKeys = rowBuffer->CaptureRows(keys);
+ auto mutableKeyRange = MakeSharedRange(std::move(capturedKeys), std::move(rowBuffer));
+ auto keyRange = TSharedRange<TUnversionedRow>(
+ static_cast<const TUnversionedRow*>(mutableKeyRange.Begin()),
+ static_cast<const TUnversionedRow*>(mutableKeyRange.End()),
+ mutableKeyRange.GetHolder());
+ auto nameTable = valueConsumer.GetNameTable();
+ asyncReplicas = context->GetClient()->GetInSyncReplicas(
+ Path.GetPath(),
+ std::move(nameTable),
+ std::move(keyRange),
+ Options);
+ }
+
+ auto replicas = WaitFor(asyncReplicas)
+ .ValueOrThrow();
+
+ context->ProduceOutputValue(BuildYsonStringFluently()
+ .List(replicas));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDeleteRowsCommand::TDeleteRowsCommand()
+{
+ RegisterParameter("sequence_number", Options.SequenceNumber)
+ .Optional();
+ RegisterParameter("require_sync_replica", Options.RequireSyncReplica)
+ .Optional();
+ RegisterParameter("table_writer", TableWriter)
+ .Default();
+ RegisterParameter("path", Path);
+}
+
+void TDeleteRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableWriter,
+ TableWriter);
+
+ auto tableMountCache = context->GetClient()->GetTableMountCache();
+ auto asyncTableInfo = tableMountCache->GetTableInfo(Path.GetPath());
+ auto tableInfo = WaitFor(asyncTableInfo)
+ .ValueOrThrow();
+
+ tableInfo->ValidateDynamic();
+ tableInfo->ValidateSorted();
+
+ struct TDeleteRowsBufferTag
+ { };
+
+ // Parse input data.
+ TBuildingValueConsumer valueConsumer(
+ tableInfo->Schemas[ETableSchemaKind::Delete],
+ WithCommandTag(Logger, context),
+ /*convertNullToEntity*/ false,
+ ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes()));
+ auto keys = ParseRows(context, &valueConsumer);
+ auto rowBuffer = New<TRowBuffer>(TDeleteRowsBufferTag());
+ auto capturedKeys = rowBuffer->CaptureRows(keys);
+ auto keyRange = MakeSharedRange(
+ std::vector<TLegacyKey>(capturedKeys.begin(), capturedKeys.end()),
+ std::move(rowBuffer));
+
+ // Run deletes.
+ auto transaction = GetTransaction(context);
+
+ transaction->DeleteRows(
+ Path.GetPath(),
+ valueConsumer.GetNameTable(),
+ std::move(keyRange),
+ Options);
+
+ if (ShouldCommitTransaction()) {
+ WaitFor(transaction->Commit())
+ .ThrowOnError();
+ }
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLockRowsCommand::TLockRowsCommand()
+{
+ RegisterParameter("table_writer", TableWriter)
+ .Default();
+ RegisterParameter("path", Path);
+ RegisterParameter("locks", Locks);
+ RegisterParameter("lock_type", LockType)
+ .Default(NTableClient::ELockType::SharedStrong);
+}
+
+void TLockRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto config = UpdateYsonStruct(
+ context->GetConfig()->TableWriter,
+ TableWriter);
+
+ auto tableMountCache = context->GetClient()->GetTableMountCache();
+ auto asyncTableInfo = tableMountCache->GetTableInfo(Path.GetPath());
+ auto tableInfo = WaitFor(asyncTableInfo)
+ .ValueOrThrow();
+
+ tableInfo->ValidateDynamic();
+
+ auto transactionPool = context->GetDriver()->GetStickyTransactionPool();
+ auto transaction = transactionPool->GetTransactionAndRenewLeaseOrThrow(Options.TransactionId);
+
+ struct TLockRowsBufferTag
+ { };
+
+ // Parse input data.
+ TBuildingValueConsumer valueConsumer(
+ tableInfo->Schemas[ETableSchemaKind::Write],
+ WithCommandTag(Logger, context),
+ /*convertNullToEntity*/ false,
+ ConvertTo<TTypeConversionConfigPtr>(context->GetInputFormat().Attributes()));
+ auto keys = ParseRows(context, &valueConsumer);
+ auto rowBuffer = New<TRowBuffer>(TLockRowsBufferTag());
+ auto capturedKeys = rowBuffer->CaptureRows(keys);
+ auto keyRange = MakeSharedRange(
+ std::vector<TLegacyKey>(capturedKeys.begin(), capturedKeys.end()),
+ std::move(rowBuffer));
+
+ // Run locks.
+ transaction->LockRows(
+ Path.GetPath(),
+ valueConsumer.GetNameTable(),
+ std::move(keyRange),
+ Locks,
+ LockType);
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTrimRowsCommand::TTrimRowsCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("tablet_index", TabletIndex);
+ RegisterParameter("trimmed_row_count", TrimmedRowCount);
+}
+
+void TTrimRowsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->TrimTable(
+ Path.GetPath(),
+ TabletIndex,
+ TrimmedRowCount,
+ Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEnableTableReplicaCommand::TEnableTableReplicaCommand()
+{
+ RegisterParameter("replica_id", ReplicaId);
+ Options.Enabled = true;
+}
+
+void TEnableTableReplicaCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->AlterTableReplica(ReplicaId, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDisableTableReplicaCommand::TDisableTableReplicaCommand()
+{
+ RegisterParameter("replica_id", ReplicaId);
+ Options.Enabled = false;
+}
+
+void TDisableTableReplicaCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->AlterTableReplica(ReplicaId, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAlterTableReplicaCommand::TAlterTableReplicaCommand()
+{
+ RegisterParameter("replica_id", ReplicaId);
+ RegisterParameter("enabled", Options.Enabled)
+ .Optional();
+ RegisterParameter("mode", Options.Mode)
+ .Optional();
+ RegisterParameter("preserve_timestamps", Options.PreserveTimestamps)
+ .Optional();
+ RegisterParameter("atomicity", Options.Atomicity)
+ .Optional();
+ RegisterParameter("enable_replicated_table_tracker", Options.EnableReplicatedTableTracker)
+ .Optional();
+}
+
+void TAlterTableReplicaCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncResult = client->AlterTableReplica(ReplicaId, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetTablePivotKeysCommand::TGetTablePivotKeysCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("represent_key_as_list", Options.RepresentKeyAsList)
+ .Default(false);
+}
+
+void TGetTablePivotKeysCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->GetTablePivotKeys(Path, Options);
+ auto result = WaitFor(asyncResult)
+ .ValueOrThrow();
+ context->ProduceOutputValue(result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCreateTableBackupCommand::TCreateTableBackupCommand()
+{
+ RegisterParameter("manifest", Manifest);
+ RegisterParameter("checkpoint_timestamp_delay", Options.CheckpointTimestampDelay)
+ .Default(TDuration::Seconds(5));
+ RegisterParameter("checkpoint_check_period", Options.CheckpointCheckPeriod)
+ .Default(TDuration::Seconds(1));
+ RegisterParameter("checkpoint_check_timeout", Options.CheckpointCheckTimeout)
+ .Default(TDuration::Seconds(10));
+ RegisterParameter("force", Options.Force)
+ .Default(false);
+}
+
+void TCreateTableBackupCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->CreateTableBackup(Manifest, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRestoreTableBackupCommand::TRestoreTableBackupCommand()
+{
+ RegisterParameter("manifest", Manifest);
+ RegisterParameter("force", Options.Force)
+ .Default(false);
+ RegisterParameter("mount", Options.Mount)
+ .Default(false);
+ RegisterParameter("enable_replicas", Options.EnableReplicas)
+ .Default(false);
+}
+
+void TRestoreTableBackupCommand::DoExecute(ICommandContextPtr context)
+{
+ auto asyncResult = context->GetClient()->RestoreTableBackup(Manifest, Options);
+ WaitFor(asyncResult)
+ .ThrowOnError();
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetTabletInfosCommand::TGetTabletInfosCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("tablet_indexes", TabletIndexes);
+ RegisterParameter("request_errors", Options.RequestErrors)
+ .Default(false);
+}
+
+void TGetTabletInfosCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncTablets = client->GetTabletInfos(Path, TabletIndexes, Options);
+ auto tablets = WaitFor(asyncTablets)
+ .ValueOrThrow();
+
+ auto addErrors = [] (const std::vector<TError>& errors, TFluentMap fluent) {
+ fluent
+ .Item("tablet_errors").DoListFor(errors, [] (TFluentList fluent, const auto& error) {
+ fluent
+ .Item().Value(error);
+ });
+ };
+
+ ProduceOutput(context, [&] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("tablets").DoListFor(tablets, [&] (auto fluent, const auto& tablet) {
+ fluent
+ .Item().BeginMap()
+ .Item("total_row_count").Value(tablet.TotalRowCount)
+ .Item("trimmed_row_count").Value(tablet.TrimmedRowCount)
+ .Item("delayed_lockless_row_count").Value(tablet.DelayedLocklessRowCount)
+ .Item("barrier_timestamp").Value(tablet.BarrierTimestamp)
+ .Item("last_write_timestamp").Value(tablet.LastWriteTimestamp)
+ .DoIf(Options.RequestErrors, BIND(addErrors, tablet.TabletErrors))
+ .DoIf(tablet.TableReplicaInfos.has_value(), [&] (TFluentMap fluent) {
+ fluent
+ .Item("replica_infos").DoListFor(
+ *tablet.TableReplicaInfos,
+ [&] (TFluentList fluent, const auto& replicaInfo) {
+ fluent
+ .Item()
+ .BeginMap()
+ .Item("replica_id").Value(replicaInfo.ReplicaId)
+ .Item("last_replication_timestamp").Value(replicaInfo.LastReplicationTimestamp)
+ .Item("mode").Value(replicaInfo.Mode)
+ .Item("current_replication_row_index").Value(replicaInfo.CurrentReplicationRowIndex)
+ .Item("committed_replication_row_index").Value(replicaInfo.CommittedReplicationRowIndex)
+ .DoIf(Options.RequestErrors, [&] (TFluentMap fluent) {
+ fluent
+ .Item("replication_error").Value(replicaInfo.ReplicationError);
+ })
+ .EndMap();
+ });
+ })
+ .EndMap();
+ })
+ .EndMap();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGetTabletErrorsCommand::TGetTabletErrorsCommand()
+{
+ RegisterParameter("path", Path);
+ RegisterParameter("limit", Options.Limit)
+ .Default()
+ .GreaterThan(0);
+}
+
+void TGetTabletErrorsCommand::DoExecute(ICommandContextPtr context)
+{
+ auto client = context->GetClient();
+ auto asyncErrors = client->GetTabletErrors(Path, Options);
+ auto errors = WaitFor(asyncErrors)
+ .ValueOrThrow();
+
+ ProduceOutput(context, [&] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("tablet_errors").DoMapFor(errors.TabletErrors, [] (auto fluent, const auto& pair) {
+ fluent
+ .Item(ToString(pair.first)).DoListFor(pair.second, [] (auto fluent, const auto& error) {
+ fluent.Item().Value(error);
+ });
+ })
+ .Item("replication_errors").DoMapFor(errors.ReplicationErrors, [] (auto fluent, const auto& pair) {
+ fluent
+ .Item(ToString(pair.first)).DoListFor(pair.second, [] (auto fluent, const auto& error) {
+ fluent.Item().Value(error);
+ });
+ })
+ .DoIf(errors.Incomplete, [&] (TFluentMap fluent) {
+ fluent
+ .Item("incomplete").Value(errors.Incomplete);
+ })
+ .EndMap();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/table_commands.h b/yt/yt/client/driver/table_commands.h
new file mode 100644
index 0000000000..2c5d0304d4
--- /dev/null
+++ b/yt/yt/client/driver/table_commands.h
@@ -0,0 +1,518 @@
+#pragma once
+
+#include "command.h"
+
+#include <yt/yt/client/formats/format.h>
+
+#include <yt/yt/client/table_client/config.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadTableCommand
+ : public TTypedCommand<NApi::TTableReaderOptions>
+{
+public:
+ TReadTableCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr TableReader;
+ NFormats::TControlAttributesConfigPtr ControlAttributes;
+ bool Unordered;
+ bool StartRowIndexOnly;
+
+ void DoExecute(ICommandContextPtr context) override;
+ bool HasResponseParameters() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadBlobTableCommand
+ : public TTypedCommand<NApi::TTableReaderOptions>
+{
+public:
+ TReadBlobTableCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr TableReader;
+
+ std::optional<TString> PartIndexColumnName;
+ std::optional<TString> DataColumnName;
+
+ i64 StartPartIndex;
+ i64 Offset;
+ i64 PartSize;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLocateSkynetShareCommand
+ : public TTypedCommand<NApi::TLocateSkynetShareOptions>
+{
+public:
+ TLocateSkynetShareCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteTableCommand
+ : public TTypedCommand<NApi::TTableWriterOptions>
+{
+public:
+ TWriteTableCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ NYTree::INodePtr TableWriter;
+ i64 MaxRowBufferSize;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetTableColumnarStatisticsCommand
+ : public TTypedCommand<NApi::TGetColumnarStatisticsOptions>
+{
+public:
+ TGetTableColumnarStatisticsCommand();
+
+private:
+ std::vector<NYPath::TRichYPath> Paths;
+ NTableClient::EColumnarStatisticsFetcherMode FetcherMode;
+ std::optional<int> MaxChunksPerNodeFetch;
+ bool EnableEarlyFinish;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPartitionTablesCommand
+ : public TTypedCommand<NApi::TPartitionTablesOptions>
+{
+public:
+ TPartitionTablesCommand();
+
+private:
+ std::vector<NYPath::TRichYPath> Paths;
+ NTableClient::ETablePartitionMode PartitionMode;
+ i64 DataWeightPerPartition;
+ std::optional<int> MaxPartitionCount;
+ bool EnableKeyGuarantee;
+
+ //! Treat the #DataWeightPerPartition as a hint and not as a maximum limit.
+ //! Consider the situation when the #MaxPartitionCount is given
+ //! and the total data weight exceeds #MaxPartitionCount * #DataWeightPerPartition.
+ //! If #AdjustDataWeightPerPartition is |true|
+ //! the #partition_tables command will yield partitions exceeding the #DataWeightPerPartition.
+ //! If #AdjustDataWeightPerPartition is |false|
+ //! the #partition_tables command will throw an exception.
+ bool AdjustDataWeightPerPartition;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOptions>
+class TTabletCommandBase
+ : public TTypedCommand<TOptions>
+{
+protected:
+ NYPath::TRichYPath Path;
+
+ TTabletCommandBase()
+ {
+ this->RegisterParameter("path", Path);
+ this->RegisterParameter("first_tablet_index", this->Options.FirstTabletIndex)
+ .Default();
+ this->RegisterParameter("last_tablet_index", this->Options.LastTabletIndex)
+ .Default();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMountTableCommand
+ : public TTabletCommandBase<NApi::TMountTableOptions>
+{
+public:
+ TMountTableCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnmountTableCommand
+ : public TTabletCommandBase<NApi::TUnmountTableOptions>
+{
+public:
+ TUnmountTableCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemountTableCommand
+ : public TTabletCommandBase<NApi::TRemountTableOptions>
+{
+public:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFreezeTableCommand
+ : public TTabletCommandBase<NApi::TFreezeTableOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnfreezeTableCommand
+ : public TTabletCommandBase<NApi::TUnfreezeTableOptions>
+{
+public:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReshardTableCommand
+ : public TTabletCommandBase<NApi::TReshardTableOptions>
+{
+public:
+ TReshardTableCommand();
+
+private:
+ std::optional<std::vector<NTableClient::TLegacyOwningKey>> PivotKeys;
+ std::optional<int> TabletCount;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReshardTableAutomaticCommand
+ : public TTabletCommandBase<NApi::TReshardTableAutomaticOptions>
+{
+public:
+ TReshardTableAutomaticCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAlterTableCommand
+ : public TTypedCommand<NApi::TAlterTableOptions>
+{
+public:
+ TAlterTableCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSelectRowsOptions
+ : public NApi::TSelectRowsOptions
+ , public TTabletTransactionOptions
+{ };
+
+class TSelectRowsCommand
+ : public TTypedCommand<TSelectRowsOptions>
+{
+public:
+ TSelectRowsCommand();
+
+private:
+ TString Query;
+ NYTree::IMapNodePtr PlaceholderValues;
+ bool EnableStatistics = false;
+
+ void DoExecute(ICommandContextPtr context) override;
+ bool HasResponseParameters() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TExplainQueryOptions
+ : public NApi::TExplainQueryOptions
+ , public TTabletTransactionOptions
+{ };
+
+class TExplainQueryCommand
+ : public TTypedCommand<TExplainQueryOptions>
+{
+public:
+ TExplainQueryCommand();
+
+private:
+ TString Query;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInsertRowsCommand
+ : public TTypedCommand<TInsertRowsOptions>
+{
+public:
+ TInsertRowsCommand();
+
+private:
+ NYTree::INodePtr TableWriter;
+ NYPath::TRichYPath Path;
+ bool Update;
+ bool Aggregate;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLookupRowsOptions
+ : public NApi::TLookupRowsOptions
+ , public TTabletTransactionOptions
+{ };
+
+class TLookupRowsCommand
+ : public TTypedCommand<TLookupRowsOptions>
+{
+public:
+ TLookupRowsCommand();
+
+private:
+ NYTree::INodePtr TableWriter;
+ NYPath::TRichYPath Path;
+ std::optional<std::vector<TString>> ColumnNames;
+ bool Versioned;
+ NTableClient::TRetentionConfigPtr RetentionConfig;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPullRowsOptions
+ : public NApi::TPullRowsOptions
+{ };
+
+class TPullRowsCommand
+ : public TTypedCommand<TPullRowsOptions>
+{
+public:
+ TPullRowsCommand();
+
+private:
+ NYPath::TRichYPath Path;
+
+ virtual void DoExecute(ICommandContextPtr context) override;
+ bool HasResponseParameters() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetInSyncReplicasCommand
+ : public TTypedCommand<NApi::TGetInSyncReplicasOptions>
+{
+public:
+ TGetInSyncReplicasCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ bool AllKeys;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDeleteRowsCommand
+ : public TTypedCommand<TDeleteRowsOptions>
+{
+public:
+ TDeleteRowsCommand();
+
+private:
+ NYTree::INodePtr TableWriter;
+ NYPath::TRichYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLockRowsCommand
+ : public TTypedCommand<TLockRowsOptions>
+{
+public:
+ TLockRowsCommand();
+
+private:
+ NYTree::INodePtr TableWriter;
+ NYPath::TRichYPath Path;
+ std::vector<TString> Locks;
+ NTableClient::ELockType LockType;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTrimRowsCommand
+ : public TTypedCommand<NApi::TTrimTableOptions>
+{
+public:
+ TTrimRowsCommand();
+
+private:
+ NYPath::TRichYPath Path;
+ int TabletIndex;
+ i64 TrimmedRowCount;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEnableTableReplicaCommand
+ : public TTypedCommand<NApi::TAlterTableReplicaOptions>
+{
+public:
+ TEnableTableReplicaCommand();
+
+private:
+ NTabletClient::TTableReplicaId ReplicaId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDisableTableReplicaCommand
+ : public TTypedCommand<NApi::TAlterTableReplicaOptions>
+{
+public:
+ TDisableTableReplicaCommand();
+
+private:
+ NTabletClient::TTableReplicaId ReplicaId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAlterTableReplicaCommand
+ : public TTypedCommand<NApi::TAlterTableReplicaOptions>
+{
+public:
+ TAlterTableReplicaCommand();
+
+private:
+ NTabletClient::TTableReplicaId ReplicaId;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetTabletInfosCommand
+ : public TTypedCommand<NApi::TGetTabletInfosOptions>
+{
+public:
+ TGetTabletInfosCommand();
+
+private:
+ NYPath::TYPath Path;
+ std::vector<int> TabletIndexes;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetTabletErrorsCommand
+ : public TTypedCommand<NApi::TGetTabletErrorsOptions>
+{
+public:
+ TGetTabletErrorsCommand();
+
+private:
+ NYPath::TYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGetTablePivotKeysCommand
+ : public TTypedCommand<NApi::TGetTablePivotKeysOptions>
+{
+public:
+ TGetTablePivotKeysCommand();
+
+private:
+ NYPath::TYPath Path;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCreateTableBackupCommand
+ : public TTypedCommand<NApi::TCreateTableBackupOptions>
+{
+public:
+ TCreateTableBackupCommand();
+
+private:
+ NApi::TBackupManifestPtr Manifest;
+
+ virtual void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRestoreTableBackupCommand
+ : public TTypedCommand<NApi::TRestoreTableBackupOptions>
+{
+public:
+ TRestoreTableBackupCommand();
+
+private:
+ NApi::TBackupManifestPtr Manifest;
+
+ virtual void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/transaction_commands.cpp b/yt/yt/client/driver/transaction_commands.cpp
new file mode 100644
index 0000000000..112bd50943
--- /dev/null
+++ b/yt/yt/client/driver/transaction_commands.cpp
@@ -0,0 +1,141 @@
+#include "config.h"
+#include "transaction_commands.h"
+
+#include <yt/yt/client/transaction_client/timestamp_provider.h>
+
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NDriver {
+
+using namespace NYTree;
+using namespace NTransactionClient;
+using namespace NCypressClient;
+using namespace NObjectClient;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStartTransactionCommand::TStartTransactionCommand()
+{
+ RegisterParameter("type", Type)
+ .Default(NTransactionClient::ETransactionType::Master);
+ RegisterParameter("attributes", Attributes)
+ .Default(nullptr);
+ RegisterParameter("sticky", Options.Sticky)
+ .Optional();
+ RegisterParameter("timeout", Options.Timeout)
+ .Optional();
+ RegisterParameter("transaction_id_override", Options.Id)
+ .Optional();
+ RegisterParameter("start_timestamp_override", Options.StartTimestamp)
+ .Optional();
+ RegisterParameter("transaction_id", Options.ParentId)
+ .Optional();
+ RegisterParameter("ping_ancestor_transactions", Options.PingAncestors)
+ .Optional();
+ RegisterParameter("prerequisite_transaction_ids", Options.PrerequisiteTransactionIds)
+ .Optional();
+ RegisterParameter("deadline", Options.Deadline)
+ .Optional();
+ RegisterParameter("atomicity", Options.Atomicity)
+ .Optional();
+ RegisterParameter("durability", Options.Durability)
+ .Optional();
+ RegisterParameter("suppress_start_timestamp_generation", Options.SuppressStartTimestampGeneration)
+ .Optional();
+ RegisterParameter("coordinator_master_cell_tag", Options.CoordinatorMasterCellTag)
+ .Optional();
+ RegisterParameter("replicate_to_master_cell_tags", Options.ReplicateToMasterCellTags)
+ .Optional();
+ RegisterParameter("start_cypress_transaction", Options.StartCypressTransaction)
+ .Optional();
+}
+
+void TStartTransactionCommand::DoExecute(ICommandContextPtr context)
+{
+ Options.Ping = true;
+ Options.AutoAbort = false;
+
+ if (Attributes) {
+ Options.Attributes = ConvertToAttributes(Attributes);
+ }
+
+ if (Type != ETransactionType::Master) {
+ Options.Sticky = true;
+ }
+
+ auto transaction = WaitFor(context->GetClient()->StartTransaction(Type, Options))
+ .ValueOrThrow();
+
+ if (Options.Sticky) {
+ context->GetDriver()->GetStickyTransactionPool()->RegisterTransaction(transaction);
+ } else {
+ transaction->Detach();
+ }
+
+ ProduceSingleOutputValue(context, "transaction_id", transaction->GetId());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPingTransactionCommand::DoExecute(ICommandContextPtr context)
+{
+ // Specially for evvers@ :)
+ if (!Options.TransactionId) {
+ return;
+ }
+
+ auto transaction = AttachTransaction(context, true);
+ WaitFor(transaction->Ping())
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCommitTransactionCommand::DoExecute(ICommandContextPtr context)
+{
+ auto transaction = AttachTransaction(context, true);
+ WaitFor(transaction->Commit(Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAbortTransactionCommand::TAbortTransactionCommand()
+{
+ RegisterParameter("force", Options.Force)
+ .Optional();
+}
+
+void TAbortTransactionCommand::DoExecute(ICommandContextPtr context)
+{
+ auto transaction = AttachTransaction(context, true);
+ WaitFor(transaction->Abort(Options))
+ .ThrowOnError();
+
+ ProduceEmptyOutput(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGenerateTimestampCommand::DoExecute(ICommandContextPtr context)
+{
+ auto timestampProvider = context->GetClient()->GetTimestampProvider();
+ auto timestamp = WaitFor(timestampProvider->GenerateTimestamps())
+ .ValueOrThrow();
+
+ ProduceSingleOutputValue(context, "timestamp", timestamp);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/driver/transaction_commands.h b/yt/yt/client/driver/transaction_commands.h
new file mode 100644
index 0000000000..62c61a3fc9
--- /dev/null
+++ b/yt/yt/client/driver/transaction_commands.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "command.h"
+
+namespace NYT::NDriver {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStartTransactionCommand
+ : public TTypedCommand<NApi::TTransactionStartOptions>
+{
+public:
+ TStartTransactionCommand();
+
+private:
+ NTransactionClient::ETransactionType Type;
+ NYTree::INodePtr Attributes;
+
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPingTransactionCommand
+ : public TTypedCommand<NApi::TTransactionalOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCommitTransactionCommand
+ : public TTypedCommand<NApi::TTransactionCommitOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAbortTransactionCommand
+ : public TTypedCommand<NApi::TTransactionAbortOptions>
+{
+public:
+ TAbortTransactionCommand();
+
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGenerateTimestampOptions
+{ };
+
+class TGenerateTimestampCommand
+ : public TTypedCommand<TGenerateTimestampOptions>
+{
+private:
+ void DoExecute(ICommandContextPtr context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDriver
+
diff --git a/yt/yt/client/driver/ya.make b/yt/yt/client/driver/ya.make
new file mode 100644
index 0000000000..d08c35855e
--- /dev/null
+++ b/yt/yt/client/driver/ya.make
@@ -0,0 +1,31 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ admin_commands.cpp
+ authentication_commands.cpp
+ chaos_commands.cpp
+ command.cpp
+ config.cpp
+ cypress_commands.cpp
+ driver.cpp
+ etc_commands.cpp
+ file_commands.cpp
+ helpers.cpp
+ journal_commands.cpp
+ proxy_discovery_cache.cpp
+ query_commands.cpp
+ queue_commands.cpp
+ scheduler_commands.cpp
+ table_commands.cpp
+ transaction_commands.cpp
+ internal_commands.cpp
+)
+
+PEERDIR(
+ yt/yt/client
+ yt/yt/client/formats
+)
+
+END()
diff --git a/yt/yt/client/election/public.cpp b/yt/yt/client/election/public.cpp
new file mode 100644
index 0000000000..0b1201670b
--- /dev/null
+++ b/yt/yt/client/election/public.cpp
@@ -0,0 +1,12 @@
+#include "public.h"
+
+namespace NYT::NElection {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TCellId NullCellId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NElection
+
diff --git a/yt/yt/client/election/public.h b/yt/yt/client/election/public.h
new file mode 100644
index 0000000000..b48fc712db
--- /dev/null
+++ b/yt/yt/client/election/public.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <yt/yt/core/misc/error_code.h>
+
+#include <library/cpp/yt/misc/enum.h>
+#include <library/cpp/yt/misc/guid.h>
+
+namespace NYT::NElection {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TEpochId = TGuid;
+using TPeerPriority = std::pair<i64, i64>;
+
+using TPeerId = int;
+constexpr TPeerId InvalidPeerId = -1;
+
+using TCellId = TGuid;
+extern const TCellId NullCellId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((InvalidState) (800))
+ ((InvalidLeader) (801))
+ ((InvalidEpoch) (802))
+);
+
+DEFINE_ENUM(EPeerState,
+ (Stopped)
+ (Voting)
+ (Leading)
+ (Following)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NElection
diff --git a/yt/yt/client/federated/client.cpp b/yt/yt/client/federated/client.cpp
new file mode 100644
index 0000000000..0726292b9f
--- /dev/null
+++ b/yt/yt/client/federated/client.cpp
@@ -0,0 +1,714 @@
+#include "client.h"
+
+#include "config.h"
+#include "private.h"
+
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/client/misc/method_helpers.h>
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/local_address.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+#include <yt/yt/core/rpc/helpers.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+using namespace NYT::NApi;
+
+const auto& Logger = FederatedClientLogger;
+
+DECLARE_REFCOUNTED_CLASS(TClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TString> GetDataCenterByClient(const IClientPtr& client)
+{
+ TListNodeOptions options;
+ options.MaxSize = 1;
+
+ auto items = NConcurrency::WaitFor(client->ListNode(RpcProxiesPath, options))
+ .ValueOrThrow();
+ auto itemsList = NYTree::ConvertTo<NYTree::IListNodePtr>(items);
+ if (!itemsList->GetChildCount()) {
+ return std::nullopt;
+ }
+ auto host = itemsList->GetChildren()[0];
+ return NNet::InferYPClusterFromHostName(host->GetValue<TString>());
+}
+
+class TTransaction
+ : public ITransaction
+{
+public:
+ TTransaction(TClientPtr client, int clientIndex, ITransactionPtr underlying);
+
+ TFuture<ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options = {}) override;
+
+ TFuture<IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options = {}) override;
+
+ TFuture<TSelectRowsResult> SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options = {}) override;
+
+ void ModifyRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<TRowModification> modifications,
+ const TModifyRowsOptions& options) override;
+
+ TFuture<TTransactionFlushResult> Flush() override;
+
+ TFuture<void> Ping(const NApi::TTransactionPingOptions& options = {}) override;
+
+ TFuture<TTransactionCommitResult> Commit(const TTransactionCommitOptions& options = TTransactionCommitOptions()) override;
+
+ TFuture<void> Abort(const TTransactionAbortOptions& options = TTransactionAbortOptions()) override;
+
+ TFuture<IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath&, NTableClient::TNameTablePtr,
+ const TSharedRange<NTableClient::TUnversionedRow>&,
+ const TVersionedLookupRowsOptions&) override;
+
+ TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>&,
+ const TMultiLookupOptions&) override;
+
+ TFuture<NYson::TYsonString> ExplainQuery(const TString&, const TExplainQueryOptions&) override;
+
+ TFuture<NYson::TYsonString> GetNode(const NYPath::TYPath&, const TGetNodeOptions&) override;
+
+ TFuture<NYson::TYsonString> ListNode(const NYPath::TYPath&, const TListNodeOptions&) override;
+
+ TFuture<bool> NodeExists(const NYPath::TYPath&, const TNodeExistsOptions&) override;
+
+ TFuture<TPullRowsResult> PullRows(const NYPath::TYPath&, const TPullRowsOptions&) override;
+
+ IClientPtr GetClient() const override
+ {
+ return Underlying_->GetClient();
+ }
+
+ NTransactionClient::ETransactionType GetType() const override
+ {
+ return Underlying_->GetType();
+ }
+
+ NTransactionClient::TTransactionId GetId() const override
+ {
+ return Underlying_->GetId();
+ }
+
+ NTransactionClient::TTimestamp GetStartTimestamp() const override
+ {
+ return Underlying_->GetStartTimestamp();
+ }
+
+ virtual NTransactionClient::EAtomicity GetAtomicity() const override
+ {
+ return Underlying_->GetAtomicity();
+ }
+
+ virtual NTransactionClient::EDurability GetDurability() const override
+ {
+ return Underlying_->GetDurability();
+ }
+
+ virtual TDuration GetTimeout() const override
+ {
+ return Underlying_->GetTimeout();
+ }
+
+ void Detach() override
+ {
+ return Underlying_->Detach();
+ }
+
+ void RegisterAlienTransaction(const ITransactionPtr& transaction) override
+ {
+ return Underlying_->RegisterAlienTransaction(transaction);
+ }
+
+ IConnectionPtr GetConnection() override
+ {
+ return Underlying_->GetConnection();
+ }
+
+ void SubscribeCommitted(const TCommittedHandler& handler) override
+ {
+ Underlying_->SubscribeCommitted(handler);
+ }
+
+ void UnsubscribeCommitted(const TCommittedHandler& handler) override
+ {
+ Underlying_->UnsubscribeCommitted(handler);
+ }
+
+ void SubscribeAborted(const TAbortedHandler& handler) override
+ {
+ Underlying_->SubscribeAborted(handler);
+ }
+
+ void UnsubscribeAborted(const TAbortedHandler& handler) override
+ {
+ Underlying_->UnsubscribeAborted(handler);
+ }
+
+ UNIMPLEMENTED_METHOD(TFuture<void>, SetNode, (const NYPath::TYPath&, const NYson::TYsonString&, const TSetNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, MultisetAttributesNode, (const NYPath::TYPath&, const NYTree::IMapNodePtr&, const TMultisetAttributesNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RemoveNode, (const NYPath::TYPath&, const TRemoveNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, CreateNode, (const NYPath::TYPath&, NObjectClient::EObjectType, const TCreateNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TLockNodeResult>, LockNode, (const NYPath::TYPath&, NCypressClient::ELockMode, const TLockNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UnlockNode, (const NYPath::TYPath&, const TUnlockNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, CopyNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TCopyNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, MoveNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TMoveNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, LinkNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TLinkNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ConcatenateNodes, (const std::vector<NYPath::TRichYPath>&, const NYPath::TRichYPath&, const TConcatenateNodesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ExternalizeNode, (const NYPath::TYPath&, NObjectClient::TCellTag, const TExternalizeNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, InternalizeNode, (const NYPath::TYPath&, const TInternalizeNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NObjectClient::TObjectId>, CreateObject, (NObjectClient::EObjectType, const TCreateObjectOptions&));
+
+ UNIMPLEMENTED_METHOD(TFuture<ITableReaderPtr>, CreateTableReader, (const NYPath::TRichYPath&, const TTableReaderOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<IFileReaderPtr>, CreateFileReader, (const NYPath::TYPath&, const TFileReaderOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<ITableWriterPtr>, CreateTableWriter, (const NYPath::TRichYPath&, const TTableWriterOptions&));
+ UNIMPLEMENTED_METHOD(IFileWriterPtr, CreateFileWriter, (const NYPath::TRichYPath&, const TFileWriterOptions&));
+ UNIMPLEMENTED_METHOD(IJournalReaderPtr, CreateJournalReader, (const NYPath::TYPath&, const TJournalReaderOptions&));
+ UNIMPLEMENTED_METHOD(IJournalWriterPtr, CreateJournalWriter, (const NYPath::TYPath&, const TJournalWriterOptions&));
+
+private:
+ const TClientPtr Client_;
+ const int ClientIndex_;
+ const ITransactionPtr Underlying_;
+
+ void OnResult(const TErrorOr<void>& error);
+};
+
+DECLARE_REFCOUNTED_TYPE(TTransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TClientDescription)
+
+struct TClientDescription final
+{
+ TClientDescription(IClientPtr client, int priority)
+ : Client(std::move(client))
+ , Priority(priority)
+ { }
+
+ IClientPtr Client;
+ int Priority;
+ std::atomic<bool> HasErrors{false};
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientDescription)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClient
+ : public IClient
+{
+public:
+ TClient(const std::vector<IClientPtr>& underlyingClients, TFederationConfigPtr config);
+
+ TFuture<IUnversionedRowsetPtr> LookupRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options = {}) override;
+ TFuture<TSelectRowsResult> SelectRows(
+ const TString& query,
+ const TSelectRowsOptions& options = {}) override;
+ TFuture<std::vector<IUnversionedRowsetPtr>> MultiLookup(
+ const std::vector<TMultiLookupSubrequest>&,
+ const TMultiLookupOptions&) override;
+ TFuture<IVersionedRowsetPtr> VersionedLookupRows(
+ const NYPath::TYPath&, NTableClient::TNameTablePtr,
+ const TSharedRange<NTableClient::TUnversionedRow>&,
+ const TVersionedLookupRowsOptions&) override;
+ TFuture<TPullRowsResult> PullRows(const NYPath::TYPath&, const TPullRowsOptions&) override;
+
+ TFuture<ITransactionPtr> StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const NApi::TTransactionStartOptions& options) override;
+
+ TFuture<NYson::TYsonString> ExplainQuery(const TString&, const TExplainQueryOptions&) override;
+
+ TFuture<NYson::TYsonString> GetNode(const NYPath::TYPath&, const TGetNodeOptions&) override;
+ TFuture<NYson::TYsonString> ListNode(const NYPath::TYPath&, const TListNodeOptions&) override;
+ TFuture<bool> NodeExists(const NYPath::TYPath&, const TNodeExistsOptions&) override;
+
+ const NTabletClient::ITableMountCachePtr& GetTableMountCache() override;
+ TFuture<std::vector<TTabletInfo>> GetTabletInfos(const NYPath::TYPath&, const std::vector<int>&, const TGetTabletInfosOptions&) override;
+
+ const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() override;
+
+ ITransactionPtr AttachTransaction(NTransactionClient::TTransactionId, const TTransactionAttachOptions&) override;
+
+ IConnectionPtr GetConnection() override
+ {
+ auto [client, _] = GetActiveClient();
+ return client->GetConnection();
+ }
+
+ std::optional<TStringBuf> GetClusterName(bool fetchIfNull) override
+ {
+ auto [client, _] = GetActiveClient();
+ return client->GetClusterName(fetchIfNull);
+ }
+
+ void Terminate() override
+ { }
+
+ // IClientBase Unsupported methods.
+ UNIMPLEMENTED_METHOD(TFuture<void>, SetNode, (const NYPath::TYPath&, const NYson::TYsonString&, const TSetNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, MultisetAttributesNode, (const NYPath::TYPath&, const NYTree::IMapNodePtr&, const TMultisetAttributesNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RemoveNode, (const NYPath::TYPath&, const TRemoveNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, CreateNode, (const NYPath::TYPath&, NObjectClient::EObjectType, const TCreateNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TLockNodeResult>, LockNode, (const NYPath::TYPath&, NCypressClient::ELockMode, const TLockNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UnlockNode, (const NYPath::TYPath&, const TUnlockNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, CopyNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TCopyNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, MoveNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TMoveNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NCypressClient::TNodeId>, LinkNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TLinkNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ConcatenateNodes, (const std::vector<NYPath::TRichYPath>&, const NYPath::TRichYPath&, const TConcatenateNodesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ExternalizeNode, (const NYPath::TYPath&, NObjectClient::TCellTag, const TExternalizeNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, InternalizeNode, (const NYPath::TYPath&, const TInternalizeNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NObjectClient::TObjectId>, CreateObject, (NObjectClient::EObjectType, const TCreateObjectOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TQueryResult>, GetQueryResult, (NQueryTrackerClient::TQueryId, i64, const TGetQueryResultOptions&));
+
+ // IClient unsupported methods.
+ UNIMPLEMENTED_METHOD(TFuture<void>, RegisterQueueConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, bool, const TRegisterQueueConsumerOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UnregisterQueueConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, const TUnregisterQueueConsumerOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<TListQueueConsumerRegistrationsResult>>, ListQueueConsumerRegistrations, (const std::optional<NYPath::TRichYPath>&, const std::optional<NYPath::TRichYPath>&, const TListQueueConsumerRegistrationsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullQueue, (const NYPath::TRichYPath&, i64, int, const NQueueClient::TQueueRowBatchReadOptions&, const TPullQueueOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, i64, int, const NQueueClient::TQueueRowBatchReadOptions&, const TPullConsumerOptions&));
+ UNIMPLEMENTED_METHOD(const NChaosClient::IReplicationCardCachePtr&, GetReplicationCardCache, ());
+ UNIMPLEMENTED_METHOD(TFuture<void>, MountTable, (const NYPath::TYPath&, const TMountTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UnmountTable, (const NYPath::TYPath&, const TUnmountTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RemountTable, (const NYPath::TYPath&, const TRemountTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, FreezeTable, (const NYPath::TYPath&, const TFreezeTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UnfreezeTable, (const NYPath::TYPath&, const TUnfreezeTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ReshardTable, (const NYPath::TYPath&, const std::vector<NTableClient::TUnversionedOwningRow>&, const TReshardTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ReshardTable, (const NYPath::TYPath&, int, const TReshardTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, ReshardTableAutomatic, (const NYPath::TYPath&, const TReshardTableAutomaticOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, TrimTable, (const NYPath::TYPath&, int, i64, const TTrimTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AlterTable, (const NYPath::TYPath&, const TAlterTableOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AlterTableReplica, (NTabletClient::TTableReplicaId, const TAlterTableReplicaOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AlterReplicationCard, (NChaosClient::TReplicationCardId, const TAlterReplicationCardOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (const NYPath::TYPath&, const NTableClient::TNameTablePtr&, const TSharedRange<NTableClient::TUnversionedRow>&, const TGetInSyncReplicasOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (const NYPath::TYPath&, const TGetInSyncReplicasOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TGetTabletErrorsResult>, GetTabletErrors, (const NYPath::TYPath&, const TGetTabletErrorsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, BalanceTabletCells, (const TString&, const std::vector<NYPath::TYPath>&, const TBalanceTabletCellsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TSkynetSharePartsLocationsPtr>, LocateSkynetShare, (const NYPath::TRichYPath&, const TLocateSkynetShareOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<std::vector<NTableClient::TColumnarStatistics>>, GetColumnarStatistics, (const std::vector<NYPath::TRichYPath>&, const TGetColumnarStatisticsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TMultiTablePartitions>, PartitionTables, (const std::vector<NYPath::TRichYPath>&, const TPartitionTablesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NYson::TYsonString>, GetTablePivotKeys, (const NYPath::TYPath&, const TGetTablePivotKeysOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, CreateTableBackup, (const TBackupManifestPtr&, const TCreateTableBackupOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RestoreTableBackup, (const TBackupManifestPtr&, const TRestoreTableBackupOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, TruncateJournal, (const NYPath::TYPath&, i64, const TTruncateJournalOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TGetFileFromCacheResult>, GetFileFromCache, (const TString&, const TGetFileFromCacheOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TPutFileToCacheResult>, PutFileToCache, (const NYPath::TYPath&, const TString&, const TPutFileToCacheOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AddMember, (const TString&, const TString&, const TAddMemberOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RemoveMember, (const TString&, const TString&, const TRemoveMemberOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TCheckPermissionResponse>, CheckPermission, (const TString&, const NYPath::TYPath&, NYTree::EPermission, const TCheckPermissionOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TCheckPermissionByAclResult>, CheckPermissionByAcl, (const std::optional<TString>&, NYTree::EPermission, NYTree::INodePtr, const TCheckPermissionByAclOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, TransferAccountResources, (const TString&, const TString&, NYTree::INodePtr, const TTransferAccountResourcesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, TransferPoolResources, (const TString&, const TString&, const TString&, NYTree::INodePtr, const TTransferPoolResourcesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NScheduler::TOperationId>, StartOperation, (NScheduler::EOperationType, const NYson::TYsonString&, const TStartOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AbortOperation, (const NScheduler::TOperationIdOrAlias&, const TAbortOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SuspendOperation, (const NScheduler::TOperationIdOrAlias&, const TSuspendOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ResumeOperation, (const NScheduler::TOperationIdOrAlias&, const TResumeOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, CompleteOperation, (const NScheduler::TOperationIdOrAlias&, const TCompleteOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UpdateOperationParameters, (const NScheduler::TOperationIdOrAlias&, const NYson::TYsonString&, const TUpdateOperationParametersOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TOperation>, GetOperation, (const NScheduler::TOperationIdOrAlias&, const TGetOperationOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, DumpJobContext, (NJobTrackerClient::TJobId, const NYPath::TYPath&, const TDumpJobContextOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr>, GetJobInput, (NJobTrackerClient::TJobId, const TGetJobInputOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NYson::TYsonString>, GetJobInputPaths, (NJobTrackerClient::TJobId, const TGetJobInputPathsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NYson::TYsonString>, GetJobSpec, (NJobTrackerClient::TJobId, const TGetJobSpecOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TSharedRef>, GetJobStderr, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobStderrOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TSharedRef>, GetJobFailContext, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobFailContextOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TListOperationsResult>, ListOperations, (const TListOperationsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TListJobsResult>, ListJobs, (const NScheduler::TOperationIdOrAlias&, const TListJobsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NYson::TYsonString>, GetJob, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AbandonJob, (NJobTrackerClient::TJobId, const TAbandonJobOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TPollJobShellResponse>, PollJobShell, (NJobTrackerClient::TJobId, const std::optional<TString>&, const NYson::TYsonString&, const TPollJobShellOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AbortJob, (NJobTrackerClient::TJobId, const TAbortJobOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TClusterMeta>, GetClusterMeta, (const TGetClusterMetaOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, CheckClusterLiveness, (const TCheckClusterLivenessOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<int>, BuildSnapshot, (const TBuildSnapshotOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TCellIdToSnapshotIdMap>, BuildMasterSnapshots, (const TBuildMasterSnapshotsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SwitchLeader, (NObjectClient::TCellId, const TString&, const TSwitchLeaderOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ResetStateHash, (NObjectClient::TCellId, const TResetStateHashOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, GCCollect, (const TGCCollectOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, KillProcess, (const TString&, const TKillProcessOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TString>, WriteCoreDump, (const TString&, const TWriteCoreDumpOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TGuid>, WriteLogBarrier, (const TString&, const TWriteLogBarrierOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TString>, WriteOperationControllerCoreDump, (NJobTrackerClient::TOperationId, const TWriteOperationControllerCoreDumpOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, HealExecNode, (const TString&, const THealExecNodeOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SuspendCoordinator, (NObjectClient::TCellId, const TSuspendCoordinatorOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ResumeCoordinator, (NObjectClient::TCellId, const TResumeCoordinatorOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, MigrateReplicationCards, (NObjectClient::TCellId, const TMigrateReplicationCardsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SuspendChaosCells, (const std::vector<NObjectClient::TCellId>&, const TSuspendChaosCellsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ResumeChaosCells, (const std::vector<NObjectClient::TCellId>&, const TResumeChaosCellsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SuspendTabletCells, (const std::vector<NObjectClient::TCellId>&, const TSuspendTabletCellsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, ResumeTabletCells, (const std::vector<NObjectClient::TCellId>&, const TResumeTabletCellsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NChaosClient::TReplicationCardPtr>, GetReplicationCard, (NChaosClient::TReplicationCardId, const TGetReplicationCardOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, UpdateChaosTableReplicaProgress, (NChaosClient::TReplicaId, const TUpdateChaosTableReplicaProgressOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TMaintenanceId>, AddMaintenance, (EMaintenanceComponent, const TString&, EMaintenanceType, const TString&, const TAddMaintenanceOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TMaintenanceCounts>, RemoveMaintenance, (EMaintenanceComponent, const TString&, const TMaintenanceFilter&, const TRemoveMaintenanceOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TDisableChunkLocationsResult>, DisableChunkLocations, (const TString&, const std::vector<TGuid>&, const TDisableChunkLocationsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TDestroyChunkLocationsResult>, DestroyChunkLocations, (const TString&, const std::vector<TGuid>&, const TDestroyChunkLocationsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TResurrectChunkLocationsResult>, ResurrectChunkLocations, (const TString&, const std::vector<TGuid>&, const TResurrectChunkLocationsOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, SetUserPassword, (const TString&, const TString&, const TString&, const TSetUserPasswordOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TIssueTokenResult>, IssueToken, (const TString&, const TString&, const TIssueTokenOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, RevokeToken, (const TString&, const TString&, const TString&, const TRevokeTokenOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TListUserTokensResult>, ListUserTokens, (const TString&, const TString&, const TListUserTokensOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<NQueryTrackerClient::TQueryId>, StartQuery, (NQueryTrackerClient::EQueryEngine, const TString&, const TStartQueryOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AbortQuery, (NQueryTrackerClient::TQueryId, const TAbortQueryOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<IUnversionedRowsetPtr>, ReadQueryResult, (NQueryTrackerClient::TQueryId, i64, const TReadQueryResultOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TQuery>, GetQuery, (NQueryTrackerClient::TQueryId, const TGetQueryOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<TListQueriesResult>, ListQueries, (const TListQueriesOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<void>, AlterQuery, (NQueryTrackerClient::TQueryId, const TAlterQueryOptions&));
+
+ UNIMPLEMENTED_METHOD(TFuture<ITableReaderPtr>, CreateTableReader, (const NYPath::TRichYPath&, const TTableReaderOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<ITableWriterPtr>, CreateTableWriter, (const NYPath::TRichYPath&, const TTableWriterOptions&));
+ UNIMPLEMENTED_METHOD(TFuture<IFileReaderPtr>, CreateFileReader, (const NYPath::TYPath&, const TFileReaderOptions&));
+ UNIMPLEMENTED_METHOD(IFileWriterPtr, CreateFileWriter, (const NYPath::TRichYPath&, const TFileWriterOptions&));
+ UNIMPLEMENTED_METHOD(IJournalReaderPtr, CreateJournalReader, (const NYPath::TYPath&, const TJournalReaderOptions&));
+ UNIMPLEMENTED_METHOD(IJournalWriterPtr, CreateJournalWriter, (const NYPath::TYPath&, const TJournalWriterOptions&));
+
+ friend class TTransaction;
+
+private:
+ struct TActiveClientInfo
+ {
+ IClientPtr Client;
+ int ClientIndex;
+ };
+
+ template <class T>
+ TFuture<T> DoCall(int retryAttemptCount, const TCallback<TFuture<T>(const IClientPtr&, int)>& callee);
+ void HandleError(const TErrorOr<void>& error, int clientIndex);
+
+ void UpdateActiveClient();
+ TActiveClientInfo GetActiveClient();
+
+ void CheckClustersHealth();
+
+private:
+ const TFederationConfigPtr Config_;
+ const NConcurrency::TPeriodicExecutorPtr Executor_;
+
+ std::vector<TClientDescriptionPtr> UnderlyingClients_;
+ IClientPtr ActiveClient_;
+ std::atomic<int> ActiveClientIndex_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, Lock_);
+};
+
+DECLARE_REFCOUNTED_TYPE(TTransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransaction::TTransaction(TClientPtr client, int clientIndex, ITransactionPtr underlying)
+ : Client_(std::move(client))
+ , ClientIndex_(clientIndex)
+ , Underlying_(std::move(underlying))
+{ }
+
+void TTransaction::OnResult(const TErrorOr<void>& error)
+{
+ if (!error.IsOK()) {
+ Client_->HandleError(error, ClientIndex_);
+ }
+}
+
+#define TRANSACTION_METHOD_IMPL(ResultType, MethodName, Args) \
+TFuture<ResultType> TTransaction::MethodName(Y_METHOD_USED_ARGS_DECLARATION(Args)) \
+{ \
+ auto future = Underlying_->MethodName(Y_PASS_METHOD_USED_ARGS(Args)); \
+ future.Subscribe(BIND(&TTransaction::OnResult, MakeStrong(this))); \
+ return future; \
+} Y_SEMICOLON_GUARD
+
+TRANSACTION_METHOD_IMPL(IUnversionedRowsetPtr, LookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TUnversionedRow>&, const TLookupRowsOptions&));
+TRANSACTION_METHOD_IMPL(TSelectRowsResult, SelectRows, (const TString&, const TSelectRowsOptions&));
+TRANSACTION_METHOD_IMPL(void, Ping, (const NApi::TTransactionPingOptions&));
+TRANSACTION_METHOD_IMPL(TTransactionCommitResult, Commit, (const TTransactionCommitOptions&));
+TRANSACTION_METHOD_IMPL(void, Abort, (const TTransactionAbortOptions&));
+TRANSACTION_METHOD_IMPL(IVersionedRowsetPtr, VersionedLookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TUnversionedRow>&, const TVersionedLookupRowsOptions&));
+TRANSACTION_METHOD_IMPL(std::vector<IUnversionedRowsetPtr>, MultiLookup, (const std::vector<TMultiLookupSubrequest>&, const TMultiLookupOptions&));
+TRANSACTION_METHOD_IMPL(TPullRowsResult, PullRows, (const NYPath::TYPath&, const TPullRowsOptions&));
+TRANSACTION_METHOD_IMPL(NYson::TYsonString, ExplainQuery, (const TString&, const TExplainQueryOptions&));
+TRANSACTION_METHOD_IMPL(NYson::TYsonString, GetNode, (const NYPath::TYPath&, const TGetNodeOptions&));
+TRANSACTION_METHOD_IMPL(NYson::TYsonString, ListNode, (const NYPath::TYPath&, const TListNodeOptions&));
+TRANSACTION_METHOD_IMPL(bool, NodeExists, (const NYPath::TYPath&, const TNodeExistsOptions&));
+
+void TTransaction::ModifyRows(
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<TRowModification> modifications,
+ const TModifyRowsOptions& options)
+{
+ Underlying_->ModifyRows(path, nameTable, modifications, options);
+}
+
+TFuture<TTransactionFlushResult> TTransaction::Flush()
+{
+ auto future = Underlying_->Flush();
+ future.Subscribe(BIND(&TTransaction::OnResult, MakeStrong(this)));
+ return future;
+}
+
+TFuture<ITransactionPtr> TTransaction::StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options)
+{
+ return Underlying_->StartTransaction(type, options).ApplyUnique(BIND(
+ [this, this_ = MakeStrong(this)] (TErrorOr<ITransactionPtr>&& result) -> TErrorOr<ITransactionPtr> {
+ if (!result.IsOK()) {
+ Client_->HandleError(result, ClientIndex_);
+ return result;
+ } else {
+ return {New<TTransaction>(Client_, ClientIndex_, result.Value())};
+ }
+ }
+ ));
+}
+
+DEFINE_REFCOUNTED_TYPE(TTransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClient::TClient(const std::vector<IClientPtr>& underlyingClients, TFederationConfigPtr config)
+ : Config_(std::move(config))
+ , Executor_(New<NConcurrency::TPeriodicExecutor>(
+ NRpc::TDispatcher::Get()->GetLightInvoker(),
+ BIND(&TClient::CheckClustersHealth, MakeWeak(this)),
+ Config_->ClusterHealthCheckPeriod))
+{
+ YT_VERIFY(!underlyingClients.empty());
+
+ UnderlyingClients_.reserve(underlyingClients.size());
+ const auto& localDatacenter = NNet::GetLocalYPCluster();
+ for (const auto& client : underlyingClients) {
+ int priority = GetDataCenterByClient(client) == localDatacenter ? 1 : 0;
+ UnderlyingClients_.push_back(New<TClientDescription>(client, priority));
+ }
+ std::stable_sort(UnderlyingClients_.begin(), UnderlyingClients_.end(), [](const auto& lhs, const auto& rhs) {
+ return lhs->Priority > rhs->Priority;
+ });
+
+ ActiveClient_ = UnderlyingClients_[0]->Client;
+ ActiveClientIndex_ = 0;
+
+ Executor_->Start();
+}
+
+void TClient::CheckClustersHealth()
+{
+ TCheckClusterLivenessOptions options;
+ options.CheckCypressRoot = true;
+ options.CheckTabletCellBundle = Config_->BundleName;
+
+ int activeClientIndex = ActiveClientIndex_.load();
+ std::optional<int> betterClientIndex;
+
+ std::vector<TFuture<void>> checks;
+ checks.reserve(UnderlyingClients_.size());
+
+ for (const auto& clientDescription : UnderlyingClients_) {
+ checks.emplace_back(clientDescription->Client->CheckClusterLiveness(options));
+ }
+
+ for (int index = 0; index < std::ssize(checks); ++index) {
+ const auto& check = checks[index];
+ bool hasErrors = !NConcurrency::WaitFor(check).IsOK();
+ UnderlyingClients_[index]->HasErrors = hasErrors;
+ if (!betterClientIndex && !hasErrors && index < activeClientIndex) {
+ betterClientIndex = index;
+ }
+ }
+
+ if (betterClientIndex && ActiveClientIndex_ == activeClientIndex) {
+ int newClientIndex = *betterClientIndex;
+ auto guard = NThreading::WriterGuard(Lock_);
+ ActiveClient_ = UnderlyingClients_[newClientIndex]->Client;
+ ActiveClientIndex_ = newClientIndex;
+ return;
+ }
+
+ // If active cluster is not healthy, try changing it.
+ if (UnderlyingClients_[activeClientIndex]->HasErrors) {
+ auto guard = NThreading::WriterGuard(Lock_);
+ // Check that active client wasn't changed.
+ if (ActiveClientIndex_ == activeClientIndex && UnderlyingClients_[activeClientIndex]->HasErrors) {
+ UpdateActiveClient();
+ }
+ }
+}
+
+template <class T>
+TFuture<T> TClient::DoCall(int retryAttemptCount, const TCallback<TFuture<T>(const IClientPtr&, int)>& callee)
+{
+ auto [client, clientIndex] = GetActiveClient();
+ return callee(client, clientIndex).ApplyUnique(BIND(
+ [
+ this,
+ this_ = MakeStrong(this),
+ retryAttemptCount,
+ callee,
+ clientIndex = clientIndex
+ ] (TErrorOr<T>&& result) {
+ if (!result.IsOK()) {
+ HandleError(result, clientIndex);
+ if (retryAttemptCount > 1) {
+ return DoCall<T>(retryAttemptCount - 1, callee);
+ }
+ }
+ return MakeFuture(std::move(result));
+ }));
+}
+
+TFuture<ITransactionPtr> TClient::StartTransaction(
+ NTransactionClient::ETransactionType type,
+ const NApi::TTransactionStartOptions& options)
+{
+ auto callee = BIND([this_ = MakeStrong(this), type, options] (const IClientPtr& client, int clientIndex) {
+ return client->StartTransaction(type, options).ApplyUnique(BIND(
+ [this_, clientIndex] (ITransactionPtr&& transaction) -> ITransactionPtr {
+ return New<TTransaction>(std::move(this_), clientIndex, std::move(transaction));
+ }));
+ });
+
+ return DoCall<ITransactionPtr>(Config_->ClusterRetryAttempts, callee);
+}
+
+#define CLIENT_METHOD_IMPL(ResultType, MethodName, Args) \
+TFuture<ResultType> TClient::MethodName(Y_METHOD_USED_ARGS_DECLARATION(Args)) \
+{ \
+ auto callee = BIND([Y_PASS_METHOD_USED_ARGS(Args)] (const IClientPtr& client, int /*clientIndex*/) { \
+ return client->MethodName(Y_PASS_METHOD_USED_ARGS(Args)); \
+ }); \
+ return DoCall<ResultType>(Config_->ClusterRetryAttempts, callee); \
+} Y_SEMICOLON_GUARD
+
+CLIENT_METHOD_IMPL(IUnversionedRowsetPtr, LookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TLegacyKey>&, const TLookupRowsOptions&));
+CLIENT_METHOD_IMPL(TSelectRowsResult, SelectRows, (const TString&, const TSelectRowsOptions&));
+CLIENT_METHOD_IMPL(std::vector<IUnversionedRowsetPtr>, MultiLookup, (const std::vector<TMultiLookupSubrequest>&, const TMultiLookupOptions&));
+CLIENT_METHOD_IMPL(IVersionedRowsetPtr, VersionedLookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TUnversionedRow>&, const TVersionedLookupRowsOptions&));
+CLIENT_METHOD_IMPL(TPullRowsResult, PullRows, (const NYPath::TYPath&, const TPullRowsOptions&));
+CLIENT_METHOD_IMPL(NYson::TYsonString, ExplainQuery, (const TString&, const TExplainQueryOptions&));
+CLIENT_METHOD_IMPL(NYson::TYsonString, GetNode, (const NYPath::TYPath&, const TGetNodeOptions&));
+CLIENT_METHOD_IMPL(NYson::TYsonString, ListNode, (const NYPath::TYPath&, const TListNodeOptions&));
+CLIENT_METHOD_IMPL(bool, NodeExists, (const NYPath::TYPath&, const TNodeExistsOptions&));
+CLIENT_METHOD_IMPL(std::vector<TTabletInfo>, GetTabletInfos, (const NYPath::TYPath&, const std::vector<int>&, const TGetTabletInfosOptions&));
+
+const NTabletClient::ITableMountCachePtr& TClient::GetTableMountCache()
+{
+ auto [client, _] = GetActiveClient();
+ return client->GetTableMountCache();
+}
+
+const NTransactionClient::ITimestampProviderPtr& TClient::GetTimestampProvider()
+{
+ auto [client, _] = GetActiveClient();
+ return client->GetTimestampProvider();
+}
+
+ITransactionPtr TClient::AttachTransaction(
+ NTransactionClient::TTransactionId transactionId,
+ const TTransactionAttachOptions& options)
+{
+ auto transactionClusterTag = NObjectClient::CellTagFromId(transactionId);
+ for (const auto& clientDescription : UnderlyingClients_) {
+ const auto& client = clientDescription->Client;
+ auto clientClusterTag = client->GetConnection()->GetClusterTag();
+ if (clientClusterTag == transactionClusterTag) {
+ return client->AttachTransaction(transactionId, options);
+ }
+ }
+ THROW_ERROR_EXCEPTION("No client is known for transaction %v", transactionId);
+}
+
+void TClient::HandleError(const TErrorOr<void>& error, int clientIndex)
+{
+ if (!NRpc::IsChannelFailureError(error)) {
+ return;
+ }
+
+ UnderlyingClients_[clientIndex]->HasErrors = true;
+ if (ActiveClientIndex_ != clientIndex) {
+ return;
+ }
+
+ auto guard = WriterGuard(Lock_);
+ if (ActiveClientIndex_ != clientIndex) {
+ return;
+ }
+
+ UpdateActiveClient();
+}
+
+void TClient::UpdateActiveClient()
+{
+ VERIFY_WRITER_SPINLOCK_AFFINITY(Lock_);
+
+ int activeClientIndex = ActiveClientIndex_.load();
+
+ for (int index = 0; index < std::ssize(UnderlyingClients_); ++index) {
+ const auto& clientDescription = UnderlyingClients_[index];
+ if (!clientDescription->HasErrors) {
+ if (activeClientIndex != index) {
+ YT_LOG_DEBUG("Active client was changed (PreviousClientIndex: %v, NewClientIndex: %v)",
+ activeClientIndex,
+ index);
+ }
+
+ ActiveClient_ = clientDescription->Client;
+ ActiveClientIndex_ = index;
+ break;
+ }
+ }
+}
+
+TClient::TActiveClientInfo TClient::GetActiveClient()
+{
+ auto guard = ReaderGuard(Lock_);
+ YT_LOG_TRACE("Request will be send to the active client (ClientIndex: %v)",
+ ActiveClientIndex_.load());
+ return {ActiveClient_, ActiveClientIndex_.load()};
+}
+
+DEFINE_REFCOUNTED_TYPE(TClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+NApi::IClientPtr CreateClient(const std::vector<NApi::IClientPtr>& clients, TFederationConfigPtr config)
+{
+ return New<TClient>(clients, std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/client.h b/yt/yt/client/federated/client.h
new file mode 100644
index 0000000000..4502c43068
--- /dev/null
+++ b/yt/yt/client/federated/client.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <vector>
+
+//! Federated client is a wrapper for several YT-clients with ability
+//! to retry asynchronously the same request with different underlying-clients.
+//! Each YT-client typically corresponds to a different YT-cluster.
+//! In case of errors (for example, cluster unavailability) federated client tries
+//! to retry request via another client (cluster).
+//!
+//! Client in the same datacenter is more prior than other.
+//!
+//! Federated client implements IClient interface, but does not support
+//! the most of mutable methods (except modifications inside transactions).
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates federated client with given underlying clients.
+NApi::IClientPtr CreateClient(const std::vector<NApi::IClientPtr>& clients, TFederationConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/config.cpp b/yt/yt/client/federated/config.cpp
new file mode 100644
index 0000000000..d16649672c
--- /dev/null
+++ b/yt/yt/client/federated/config.cpp
@@ -0,0 +1,35 @@
+#include "config.h"
+
+#include <yt/yt/client/api/rpc_proxy/config.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFederationConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("bundle_name", &TThis::BundleName)
+ .Default();
+
+ registrar.Parameter("cluster_health_check_period", &TThis::ClusterHealthCheckPeriod)
+ .GreaterThan(TDuration::Zero())
+ .Default(TDuration::Seconds(60));
+
+ registrar.Parameter("cluster_retry_attempts", &TThis::ClusterRetryAttempts)
+ .GreaterThanOrEqual(0)
+ .Default(3);
+}
+
+void TConnectionConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("rpc_proxy_connections", &TThis::RpcProxyConnections);
+
+ registrar.Postprocessor([] (TThis* config) {
+ THROW_ERROR_EXCEPTION_IF(config->RpcProxyConnections.empty(),
+ "At least one `rpc_proxy_connections` must be specified");
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/config.h b/yt/yt/client/federated/config.h
new file mode 100644
index 0000000000..c3a67aef58
--- /dev/null
+++ b/yt/yt/client/federated/config.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rpc_proxy/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <vector>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFederationConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Bundle name which liveness should be checked on the background.
+ std::optional<TString> BundleName;
+
+ //! How often cluster liveness should be checked on the background.
+ TDuration ClusterHealthCheckPeriod;
+
+ //! Maximum number of retry attempts to make.
+ int ClusterRetryAttempts;
+
+ REGISTER_YSON_STRUCT(TFederationConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFederationConfig)
+
+class TConnectionConfig
+ : public TFederationConfig
+{
+public:
+ //! The RPC connection config for participants clusters.
+ std::vector<NApi::NRpcProxy::TConnectionConfigPtr> RpcProxyConnections;
+
+ REGISTER_YSON_STRUCT(TConnectionConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnectionConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/connection.cpp b/yt/yt/client/federated/connection.cpp
new file mode 100644
index 0000000000..ec0d6bfdfd
--- /dev/null
+++ b/yt/yt/client/federated/connection.cpp
@@ -0,0 +1,137 @@
+#include "connection.h"
+
+#include "config.h"
+#include "client.h"
+
+#include <yt/yt/client/api/rpc_proxy/config.h>
+
+#include <yt/yt/client/misc/method_helpers.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/yt/string/guid.h>
+#include <library/cpp/yt/string/string_builder.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TString MakeConnectionLoggingTag(const std::vector<NApi::IConnectionPtr>& connections, TGuid connectionId)
+{
+ TStringBuilder builder;
+ builder.AppendString("Clusters: (");
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder, "; ");
+ for (const auto& connection : connections) {
+ delimitedBuilder->AppendString(connection->GetLoggingTag());
+ }
+ builder.AppendString("), ");
+ builder.AppendFormat("ConnectionId: %v", connectionId);
+ return builder.Flush();
+}
+
+class TConnection
+ : public NApi::IConnection
+{
+public:
+ TConnection(
+ std::vector<NApi::IConnectionPtr> connections,
+ NConcurrency::TActionQueuePtr actionQueue,
+ TFederationConfigPtr config)
+ : Config_(std::move(config))
+ , Connections_(std::move(connections))
+ , ActionQueue_(std::move(actionQueue))
+ , ConnectionId_(TGuid::Create())
+ , LoggingTag_(MakeConnectionLoggingTag(Connections_, ConnectionId_))
+ {
+ YT_VERIFY(!Connections_.empty());
+ }
+
+ const TString& GetLoggingTag() const override
+ {
+ return LoggingTag_;
+ }
+
+ IInvokerPtr GetInvoker() override
+ {
+ return Connections_[0]->GetInvoker();
+ }
+
+ NApi::IClientPtr CreateClient(const NApi::TClientOptions& options = {}) override
+ {
+ std::vector<NApi::IClientPtr> clients;
+ clients.reserve(Connections_.size());
+ for (auto& connection : Connections_) {
+ clients.push_back(connection->CreateClient(options));
+ }
+ return NFederated::CreateClient(clients, Config_);
+ }
+
+ void ClearMetadataCaches() override
+ {
+ // TODO(bulatman) What about exceptions?
+ for (auto& connection : Connections_) {
+ connection->ClearMetadataCaches();
+ }
+ }
+
+ void Terminate() override
+ {
+ // TODO(bulatman) What about exceptions?
+ for (auto& connection : Connections_) {
+ connection->Terminate();
+ }
+ }
+
+ //! Returns a YSON-serialized connection config.
+ NYson::TYsonString GetConfigYson() const override
+ {
+ return NYson::ConvertToYsonString(Config_);
+ }
+
+ UNIMPLEMENTED_CONST_METHOD(NApi::TClusterTag, GetClusterTag, ());
+ UNIMPLEMENTED_CONST_METHOD(const TString&, GetClusterId, ());
+ UNIMPLEMENTED_CONST_METHOD(const std::optional<TString>&, GetClusterName, ());
+ UNIMPLEMENTED_CONST_METHOD(bool, IsSameCluster, (const NApi::IConnectionPtr&));
+ UNIMPLEMENTED_METHOD(
+ NHiveClient::ITransactionParticipantPtr,
+ CreateTransactionParticipant,
+ (NHiveClient::TCellId, const NApi::TTransactionParticipantOptions&));
+
+private:
+ const TFederationConfigPtr Config_;
+ const std::vector<NApi::IConnectionPtr> Connections_;
+ const NConcurrency::TActionQueuePtr ActionQueue_;
+ const TGuid ConnectionId_;
+ const TString LoggingTag_;
+};
+
+} // namespace
+
+NApi::IConnectionPtr CreateConnection(std::vector<NApi::IConnectionPtr> connections, TFederationConfigPtr config)
+{
+ return New<TConnection>(std::move(connections), nullptr, std::move(config));
+}
+
+NApi::IConnectionPtr CreateConnection(TConnectionConfigPtr config, NApi::NRpcProxy::TConnectionOptions options)
+{
+ NConcurrency::TActionQueuePtr actionQueue;
+ if (!options.ConnectionInvoker) {
+ actionQueue = New<NConcurrency::TActionQueue>("FederatedConn");
+ options.ConnectionInvoker = actionQueue->GetInvoker();
+ }
+ std::vector<NApi::IConnectionPtr> connections;
+ connections.reserve(config->RpcProxyConnections.size());
+ for (const auto& rpcProxyConfig : config->RpcProxyConnections) {
+ connections.push_back(NApi::NRpcProxy::CreateConnection(rpcProxyConfig, options));
+ }
+
+ return New<TConnection>(std::move(connections), std::move(actionQueue), std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/connection.h b/yt/yt/client/federated/connection.h
new file mode 100644
index 0000000000..ce7b51f5ee
--- /dev/null
+++ b/yt/yt/client/federated/connection.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rpc_proxy/connection.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates federated connection with given underlying |connections|.
+//! Federated connection is a wrapper for several connections with ability
+//! to create federated client with clients created by |connections| with the same |TAuthenticationOptions|.
+NApi::IConnectionPtr CreateConnection(
+ std::vector<NApi::IConnectionPtr> connections,
+ TFederationConfigPtr config);
+
+//! Creates federated connection with given |config| and |options|.
+NApi::IConnectionPtr CreateConnection(
+ TConnectionConfigPtr config,
+ NApi::NRpcProxy::TConnectionOptions options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/private.h b/yt/yt/client/federated/private.h
new file mode 100644
index 0000000000..6762c79ff2
--- /dev/null
+++ b/yt/yt/client/federated/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger FederatedClientLogger("FederatedClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/public.h b/yt/yt/client/federated/public.h
new file mode 100644
index 0000000000..37b85a1ae4
--- /dev/null
+++ b/yt/yt/client/federated/public.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NClient::NFederated {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TFederationConfig)
+DECLARE_REFCOUNTED_CLASS(TConnectionConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/unittests/client_ut.cpp b/yt/yt/client/federated/unittests/client_ut.cpp
new file mode 100644
index 0000000000..10e14a5b71
--- /dev/null
+++ b/yt/yt/client/federated/unittests/client_ut.cpp
@@ -0,0 +1,480 @@
+#include <yt/yt/client/federated/client.h>
+#include <yt/yt/client/federated/config.h>
+
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/unittests/mock/client.h>
+#include <yt/yt/client/unittests/mock/connection.h>
+#include <yt/yt/client/unittests/mock/transaction.h>
+
+#include <yt/yt/core/net/local_address.h>
+
+#include <util/datetime/base.h>
+
+
+namespace NYT::NClient::NFederated {
+namespace {
+
+using namespace NYT::NApi;
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::StrictMock;
+
+using TStrictMockClient = StrictMock<NApi::TMockClient>;
+using TStrictMockConnection = StrictMock<NApi::TMockConnection>;
+using TStrictMockTransaction = StrictMock<NApi::TMockTransaction>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTestDataStorage
+{
+ TTestDataStorage()
+ {
+ LookupResult1 = [&]() {
+ auto rowBuffer = NYT::New<NTableClient::TRowBuffer>();
+ std::vector<NTableClient::TUnversionedRow> rows;
+ for (ui64 key : {10, 11}) {
+ auto row = rowBuffer->AllocateUnversioned(2);
+ row[0] = rowBuffer->CaptureValue(NTableClient::MakeUnversionedUint64Value(key, TableSchema->GetColumnIndex(KeyColumn)));
+ row[1] = rowBuffer->CaptureValue(NTableClient::MakeUnversionedUint64Value(key + 10, TableSchema->GetColumnIndex(ValueColumn)));
+ rows.push_back(NTableClient::TUnversionedRow{row});
+ }
+ return NApi::CreateRowset(TableSchema, MakeSharedRange(rows, std::move(rowBuffer)));
+ } ();
+
+ LookupResult2 = [&]() {
+ auto rowBuffer = NYT::New<NTableClient::TRowBuffer>();
+ std::vector<NTableClient::TUnversionedRow> rows;
+ for (ui64 key : {12, 13}) {
+ auto row = rowBuffer->AllocateUnversioned(2);
+ row[0] = rowBuffer->CaptureValue(NTableClient::MakeUnversionedUint64Value(key, TableSchema->GetColumnIndex(KeyColumn)));
+ row[1] = rowBuffer->CaptureValue(NTableClient::MakeUnversionedUint64Value(key + 10, TableSchema->GetColumnIndex(ValueColumn)));
+ rows.push_back(NTableClient::TUnversionedRow{row});
+ }
+ return NApi::CreateRowset(TableSchema, MakeSharedRange(rows, std::move(rowBuffer)));
+ } ();
+
+ NameTable = NYT::New<NTableClient::TNameTable>();
+
+ i32 keyField = NameTable->GetIdOrRegisterName(KeyColumn);
+ auto keys = [&]() {
+ auto rowBuffer = NYT::New<NTableClient::TRowBuffer>();
+ std::vector<NTableClient::TUnversionedRow> keysVector;
+ for (ui64 key : {10, 11}) {
+ NTableClient::TUnversionedRowBuilder builder;
+ builder.AddValue(NTableClient::MakeUnversionedUint64Value(key, keyField));
+ keysVector.push_back(rowBuffer->CaptureRow(builder.GetRow()));
+ }
+ return NYT::MakeSharedRange(std::move(keysVector), std::move(rowBuffer));
+ } ();
+ }
+
+ const NYPath::TYPath Path = "/test/table";
+ const TString KeyColumn = "key";
+ const TString ValueColumn = "value";
+
+ const NTableClient::TColumnSchema KeyColumnSchema = NTableClient::TColumnSchema(KeyColumn, NTableClient::EValueType::Uint64);
+ const NTableClient::TColumnSchema ValueColumnSchema = NTableClient::TColumnSchema(ValueColumn, NTableClient::EValueType::Uint64);
+ NTableClient::TTableSchemaPtr TableSchema = New<NTableClient::TTableSchema>(std::vector{KeyColumnSchema, ValueColumnSchema});
+
+ IUnversionedRowsetPtr LookupResult1;
+ IUnversionedRowsetPtr LookupResult2;
+ NTableClient::TNameTablePtr NameTable;
+ TSharedRange<NTableClient::TUnversionedRow> Keys;
+};
+
+TEST(TFederatedClientTest, Basic)
+{
+ TTestDataStorage data;
+
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString listResult1(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult1)));
+
+ NYson::TYsonString listResult2(TStringBuf(R"(["b-rpc-proxy-b.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult2)));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("a-rpc-proxy.vla.yp-c.yandex.net");
+
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillOnce(Return(VoidFuture))
+ .WillRepeatedly(Return(MakeFuture(TError("Failure"))));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ // Creation of federated client.
+ std::vector<IClientPtr> clients = {mockClientSas, mockClientVla};
+ auto config = New<TFederationConfig>();
+ config->ClusterHealthCheckPeriod = TDuration::Seconds(5);
+ config->ClusterRetryAttempts = 1;
+ auto federatedClient = CreateClient(clients, config);
+
+ // 1. `vla` client should be used as closest cluster.
+ // 2. error from `vla` cluster should be received.
+ // 3. `sas` client should be used as other cluster.
+
+ EXPECT_CALL(*mockClientVla, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult1)))
+ .WillOnce(Return(MakeFuture<IUnversionedRowsetPtr>(TError(NRpc::EErrorCode::Unavailable, "Failure"))));
+
+ EXPECT_CALL(*mockClientSas, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult2)));
+
+ // Wait for the first execution of CheckClustersHealth.
+ Sleep(TDuration::Seconds(2));
+
+ // From `vla`.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+ ASSERT_EQ("[0#10u, 1#20u]", actualFirstRow);
+ }
+
+ // Error from `vla`.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ ASSERT_ANY_THROW(result.Get().ValueOrThrow());
+ }
+
+ // From `sas`.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+
+ ASSERT_EQ("[0#12u, 1#22u]", actualFirstRow);
+ }
+}
+
+TEST(TFederatedClientTest, CheckHealth)
+{
+ TTestDataStorage data;
+
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString listResult1(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult1)));
+
+ NYson::TYsonString listResult2(TStringBuf(R"(["b-rpc-proxy-b.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult2)));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("a-rpc-proxy.vla.yp-c.yandex.net");
+
+ std::vector<IClientPtr> clients = {mockClientSas, mockClientVla};
+ auto config = New<TFederationConfig>();
+ config->ClusterHealthCheckPeriod = TDuration::Seconds(5);
+ config->ClusterRetryAttempts = 1;
+ config->BundleName = "my_bundle";
+
+ TCheckClusterLivenessOptions checkLivenessOptions;
+ checkLivenessOptions.CheckCypressRoot = true;
+ checkLivenessOptions.CheckTabletCellBundle = config->BundleName;
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(checkLivenessOptions))
+ .WillOnce(Return(VoidFuture))
+ .WillOnce(Return(MakeFuture(TError("Failure"))))
+ .WillOnce(Return(VoidFuture));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(checkLivenessOptions))
+ .WillRepeatedly(Return(VoidFuture));
+
+ auto federatedClient = CreateClient(clients, config);
+
+ EXPECT_CALL(*mockClientVla, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult1)))
+ .WillOnce(Return(MakeFuture(data.LookupResult1)));
+
+ EXPECT_CALL(*mockClientSas, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult2)));
+
+ // From `vla`.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+ ASSERT_EQ("[0#10u, 1#20u]", actualFirstRow);
+ }
+
+ // Wait for the check of cluster liveness.
+ Sleep(TDuration::Seconds(6));
+
+ // From `sas` because `vla` was marked as unhealthy after CheckClustersHealth.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+
+ ASSERT_EQ("[0#12u, 1#22u]", actualFirstRow);
+ }
+
+ // Wait for the next check of cluster liveness, `vla` cluster will become current again.
+ Sleep(TDuration::Seconds(5));
+
+ // From `vla` because it became ok again.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+ ASSERT_EQ("[0#10u, 1#20u]", actualFirstRow);
+ }
+}
+
+TEST(TFederatedClientTest, Transactions)
+{
+ TTestDataStorage data;
+
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString listResult1(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult1)));
+
+ NYson::TYsonString listResult2(TStringBuf(R"(["b-rpc-proxy-b.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult2)));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("a-rpc-proxy.vla.yp-c.yandex.net");
+
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillOnce(Return(VoidFuture))
+ .WillRepeatedly(Return(MakeFuture(TError("Failure"))));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ // Creation of federated client.
+ std::vector<IClientPtr> clients = {mockClientSas, mockClientVla};
+ auto config = New<TFederationConfig>();
+ config->ClusterHealthCheckPeriod = TDuration::Seconds(5);
+ config->ClusterRetryAttempts = 1;
+ auto federatedClient = CreateClient(clients, config);
+
+ auto mockTransactionVla = New<TStrictMockTransaction>();
+
+ EXPECT_CALL(*mockClientVla, StartTransaction(_, _))
+ .WillOnce(Return(MakeFuture(NApi::ITransactionPtr{mockTransactionVla})));
+
+ EXPECT_CALL(*mockTransactionVla, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult1)))
+ .WillOnce(Return(MakeFuture<IUnversionedRowsetPtr>(TError(NRpc::EErrorCode::Unavailable, "Failure"))));
+
+ // Wait for the first check of clusters healths.
+ Sleep(TDuration::Seconds(2));
+
+ auto transaction = federatedClient->StartTransaction(NTransactionClient::ETransactionType::Tablet).Get().Value();
+
+ // From `vla`.
+ {
+ auto result = transaction->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+ ASSERT_EQ("[0#10u, 1#20u]", actualFirstRow);
+ }
+
+ // Error from `vla`.
+ {
+ auto result = transaction->LookupRows(data.Path, data.NameTable, data.Keys);
+ ASSERT_ANY_THROW(result.Get().ValueOrThrow());
+ }
+
+ auto mockTransactionSas = New<TStrictMockTransaction>();
+ EXPECT_CALL(*mockClientSas, StartTransaction(_, _))
+ .WillOnce(Return(MakeFuture(NApi::ITransactionPtr{mockTransactionSas})));
+
+ EXPECT_CALL(*mockTransactionSas, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult2)));
+
+ // Creating next transaction in `sas`.
+ {
+ transaction = federatedClient->StartTransaction(NTransactionClient::ETransactionType::Tablet).Get().Value();
+
+ auto result = transaction->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+
+ ASSERT_EQ("[0#12u, 1#22u]", actualFirstRow);
+ }
+}
+
+TEST(TFederatedClientTest, RetryWithoutTransaction)
+{
+ TTestDataStorage data;
+
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString listResult1(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult1)));
+
+ NYson::TYsonString listResult2(TStringBuf(R"(["b-rpc-proxy-b.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult2)));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("a-rpc-proxy.vla.yp-c.yandex.net");
+
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillOnce(Return(VoidFuture))
+ .WillOnce(Return(VoidFuture))
+ .WillRepeatedly(Return(MakeFuture(TError("Failure"))));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ // Creation of federated client.
+ std::vector<IClientPtr> clients = {mockClientSas, mockClientVla};
+ auto config = New<TFederationConfig>();
+ config->ClusterHealthCheckPeriod = TDuration::Seconds(5);
+ auto federatedClient = CreateClient(clients, config);
+
+ // 1. `vla` client should be used as closest cluster.
+ // 2. error from `vla` cluster should be received.
+ // 3. `sas` client should be used as other cluster.
+
+ EXPECT_CALL(*mockClientVla, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture<IUnversionedRowsetPtr>(TError(NRpc::EErrorCode::Unavailable, "Failure"))));
+
+ EXPECT_CALL(*mockClientSas, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult2)));
+
+ // Wait for the first execution of CheckClustersHealth.
+ Sleep(TDuration::Seconds(2));
+
+ // Go to `vla`, getting error, retry via `sas` and getting response from `sas`.
+ {
+ auto result = federatedClient->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+
+ ASSERT_EQ("[0#12u, 1#22u]", actualFirstRow);
+ }
+
+ // Wait for the next check, `vla` is current cluster again.
+ Sleep(TDuration::Seconds(5));
+
+ auto mockTransactionVla = New<TStrictMockTransaction>();
+ EXPECT_CALL(*mockClientVla, StartTransaction(_, _))
+ .WillOnce(Return(MakeFuture<NApi::ITransactionPtr>(TError(NRpc::EErrorCode::Unavailable, "Failure"))));
+
+ auto mockTransactionSas = New<TStrictMockTransaction>();
+ EXPECT_CALL(*mockClientSas, StartTransaction(_, _))
+ .WillOnce(Return(MakeFuture(NApi::ITransactionPtr{mockTransactionSas})));
+ EXPECT_CALL(*mockTransactionSas, LookupRows(data.Path, _, _, _))
+ .WillOnce(Return(MakeFuture(data.LookupResult2)));
+
+
+ // Try to start transaction in `vla`, getting error, retry via `sas`, creating transaction and getting response from `sas`.
+ {
+ auto transaction = federatedClient->StartTransaction(NTransactionClient::ETransactionType::Tablet).Get().Value();
+
+ auto result = transaction->LookupRows(data.Path, data.NameTable, data.Keys);
+ auto rows = result.Get().Value()->GetRows();
+
+ ASSERT_EQ(2u, rows.Size());
+ auto actualFirstRow = ToString(rows[0]);
+
+ ASSERT_EQ("[0#12u, 1#22u]", actualFirstRow);
+ }
+}
+
+TEST(TFederatedClientTest, AttachTransaction)
+{
+ TTestDataStorage data;
+
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString listResult1(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult1)));
+
+ NYson::TYsonString listResult2(TStringBuf(R"(["b-rpc-proxy-b.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillOnce(Return(MakeFuture(listResult2)));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("b-rpc-proxy.vla.yp-c.yandex.net");
+
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(MakeFuture(TError("Failure"))));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ auto mockConnectionSas = New<TStrictMockConnection>();
+ EXPECT_CALL(*mockConnectionSas, GetClusterTag())
+ .WillRepeatedly(Return(NObjectClient::TCellTag(123)));
+ EXPECT_CALL(*mockClientSas, GetConnection())
+ .WillOnce(Return(mockConnectionSas));
+
+ auto mockConnectionVla = New<TStrictMockConnection>();
+ EXPECT_CALL(*mockConnectionVla, GetClusterTag())
+ .WillRepeatedly(Return(NObjectClient::TCellTag(456)));
+ EXPECT_CALL(*mockClientVla, GetConnection())
+ .WillOnce(Return(mockConnectionVla));
+
+ // Creation of federated client.
+ std::vector<IClientPtr> clients = {mockClientSas, mockClientVla};
+ auto config = New<TFederationConfig>();
+ config->ClusterHealthCheckPeriod = TDuration::Seconds(5);
+ auto federatedClient = CreateClient(clients, config);
+
+ auto mockTransactionSas = New<TStrictMockTransaction>();
+ auto transactionId = TGuid(0, 123 << 16, 0, 0);
+ EXPECT_CALL(*mockTransactionSas, GetId())
+ .WillRepeatedly(Return(transactionId));
+
+ EXPECT_CALL(*mockClientSas, AttachTransaction(transactionId, _))
+ .WillOnce(Return(mockTransactionSas));
+
+ auto transaction = federatedClient->AttachTransaction(transactionId);
+ ASSERT_EQ(transaction->GetId(), transactionId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/unittests/connection_ut.cpp b/yt/yt/client/federated/unittests/connection_ut.cpp
new file mode 100644
index 0000000000..7f7473d019
--- /dev/null
+++ b/yt/yt/client/federated/unittests/connection_ut.cpp
@@ -0,0 +1,75 @@
+#include <yt/yt/client/federated/connection.h>
+#include <yt/yt/client/federated/config.h>
+
+#include <yt/yt/client/unittests/mock/client.h>
+#include <yt/yt/client/unittests/mock/connection.h>
+
+#include <yt/yt/core/net/local_address.h>
+
+namespace NYT::NClient::NFederated {
+namespace {
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::ReturnRefOfCopy;
+using ::testing::StrictMock;
+
+using TStrictMockClient = StrictMock<NApi::TMockClient>;
+using TStrictMockConnection = StrictMock<NApi::TMockConnection>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFederatedConnectionTest, CreateClient)
+{
+ auto config = New<TFederationConfig>();
+ config->BundleName = "my_bundle";
+
+ auto mockConnectionSas = New<TStrictMockConnection>();
+ auto mockConnectionVla = New<TStrictMockConnection>();
+ auto mockClientSas = New<TStrictMockClient>();
+ auto mockClientVla = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ // To identify best (closest) cluster.
+ NYson::TYsonString nodesYsonSas(TStringBuf(R"(["a-rpc-proxy-a.sas.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientSas, ListNode("//sys/rpc_proxies", _))
+ .WillRepeatedly(Return(MakeFuture(nodesYsonSas)));
+
+ NYson::TYsonString nodesYsonVla(TStringBuf(R"(["a-rpc-proxy-a.vla.yp-c.yandex.net:9013"])"));
+ EXPECT_CALL(*mockClientVla, ListNode("//sys/rpc_proxies", _))
+ .WillRepeatedly(Return(MakeFuture(nodesYsonVla)));
+
+ EXPECT_CALL(*mockClientSas, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+ EXPECT_CALL(*mockClientVla, CheckClusterLiveness(_))
+ .WillRepeatedly(Return(VoidFuture));
+
+ NApi::TClientOptions clientOptions;
+ EXPECT_CALL(*mockConnectionSas, CreateClient(::testing::Ref(clientOptions)))
+ .WillOnce(Return(mockClientSas));
+ EXPECT_CALL(*mockConnectionVla, CreateClient(::testing::Ref(clientOptions)))
+ .WillOnce(Return(mockClientVla));
+
+ EXPECT_CALL(*mockConnectionSas, GetLoggingTag())
+ .WillOnce(ReturnRefOfCopy(TString("sas")));
+ EXPECT_CALL(*mockConnectionVla, GetLoggingTag())
+ .WillOnce(ReturnRefOfCopy(TString("vla")));
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("a-rpc-proxy.sas.yp-c.yandex.net");
+
+ auto connection = CreateConnection({mockConnectionSas, mockConnectionVla}, config);
+ EXPECT_THAT(connection->GetLoggingTag(), testing::HasSubstr("Clusters: (sas; vla)"));
+ auto client = connection->CreateClient(clientOptions);
+ auto nodes = client->ListNode("//sys/rpc_proxies").Get().ValueOrThrow();
+ EXPECT_EQ(nodesYsonSas, nodes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NClient::NFederated
diff --git a/yt/yt/client/federated/unittests/ya.make b/yt/yt/client/federated/unittests/ya.make
new file mode 100644
index 0000000000..5cb97978d0
--- /dev/null
+++ b/yt/yt/client/federated/unittests/ya.make
@@ -0,0 +1,21 @@
+GTEST(unittester-federated-client)
+
+SRCS(
+ client_ut.cpp
+ connection_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/iterator
+ library/cpp/testing/common
+ library/cpp/testing/hook
+
+ yt/yt/library/profiling/solomon
+
+ yt/yt/core
+
+ yt/yt/client/federated
+ yt/yt/client/unittests/mock
+)
+
+END()
diff --git a/yt/yt/client/federated/ya.make b/yt/yt/client/federated/ya.make
new file mode 100644
index 0000000000..efea22071c
--- /dev/null
+++ b/yt/yt/client/federated/ya.make
@@ -0,0 +1,17 @@
+LIBRARY()
+
+SRCS(
+ client.cpp
+ config.cpp
+ connection.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/string
+ yt/yt/core
+ yt/yt/client
+)
+
+END()
+
+RECURSE_FOR_TESTS(unittests)
diff --git a/yt/yt/client/file_client/config.h b/yt/yt/client/file_client/config.h
new file mode 100644
index 0000000000..03821e4966
--- /dev/null
+++ b/yt/yt/client/file_client/config.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/chunk_client/config.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NFileClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileChunkWriterConfig
+ : public virtual NChunkClient::TEncodingWriterConfig
+{
+public:
+ i64 BlockSize;
+
+ REGISTER_YSON_STRUCT(TFileChunkWriterConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("block_size", &TThis::BlockSize)
+ .Default(16_MB)
+ .GreaterThan(0);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFileChunkWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFileClient
diff --git a/yt/yt/client/file_client/public.h b/yt/yt/client/file_client/public.h
new file mode 100644
index 0000000000..f999e52908
--- /dev/null
+++ b/yt/yt/client/file_client/public.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NFileClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TFileChunkWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFileClient
diff --git a/yt/yt/client/formats/config.cpp b/yt/yt/client/formats/config.cpp
new file mode 100644
index 0000000000..f6c6a8c8a6
--- /dev/null
+++ b/yt/yt/client/formats/config.cpp
@@ -0,0 +1,355 @@
+#include "config.h"
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TControlAttributesConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_key_switch", &TThis::EnableKeySwitch)
+ .Default(false);
+
+ registrar.Parameter("enable_end_of_stream", &TThis::EnableEndOfStream)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("format", &TThis::Format)
+ .Default(NYson::EYsonFormat::Binary);
+ registrar.Parameter("complex_type_mode", &TThis::ComplexTypeMode)
+ .Default(EComplexTypeMode::Named);
+ registrar.Parameter("string_keyed_dict_mode", &TThis::StringKeyedDictMode)
+ .Default(EDictMode::Positional);
+ registrar.Parameter("decimal_mode", &TThis::DecimalMode)
+ .Default(EDecimalMode::Binary);
+ registrar.Parameter("time_mode", &TThis::TimeMode)
+ .Default(ETimeMode::Binary);
+ registrar.Parameter("uuid_mode", &TThis::UuidMode)
+ .Default(EUuidMode::Binary);
+ registrar.Parameter("skip_null_values", &TThis::SkipNullValues)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYamrFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("has_subkey", &TThis::HasSubkey)
+ .Default(false);
+ registrar.Parameter("key", &TThis::Key)
+ .Default("key");
+ registrar.Parameter("subkey", &TThis::Subkey)
+ .Default("subkey");
+ registrar.Parameter("value", &TThis::Value)
+ .Default("value");
+ registrar.BaseClassParameter("lenval", &TThis::Lenval)
+ .Default(false);
+ registrar.BaseClassParameter("fs", &TThis::FieldSeparator)
+ .Default('\t');
+ registrar.BaseClassParameter("rs", &TThis::RecordSeparator)
+ .Default('\n');
+ registrar.BaseClassParameter("enable_table_index", &TThis::EnableTableIndex)
+ .Default(false);
+ registrar.BaseClassParameter("enable_escaping", &TThis::EnableEscaping)
+ .Default(false);
+ registrar.BaseClassParameter("escaping_symbol", &TThis::EscapingSymbol)
+ .Default('\\');
+ registrar.BaseClassParameter("enable_eom", &TThis::EnableEom)
+ .Default(false);
+
+ registrar.Preprocessor([] (TThis* config) {
+ if (config->EnableEom && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("EOM marker is not supported in YAMR text mode");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDsvFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("record_separator", &TThis::RecordSeparator)
+ .Default('\n');
+ registrar.BaseClassParameter("key_value_separator", &TThis::KeyValueSeparator)
+ .Default('=');
+ registrar.BaseClassParameter("field_separator", &TThis::FieldSeparator)
+ .Default('\t');
+ registrar.BaseClassParameter("line_prefix", &TThis::LinePrefix)
+ .Default();
+ registrar.BaseClassParameter("enable_escaping", &TThis::EnableEscaping)
+ .Default(true);
+ registrar.BaseClassParameter("escaping_symbol", &TThis::EscapingSymbol)
+ .Default('\\');
+ registrar.BaseClassParameter("enable_table_index", &TThis::EnableTableIndex)
+ .Default(false);
+ registrar.Parameter("table_index_column", &TThis::TableIndexColumn)
+ .Default("@table_index")
+ .NonEmpty();
+ registrar.Parameter("skip_unsupported_types", &TThis::SkipUnsupportedTypes)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYamredDsvFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("record_separator", &TThis::RecordSeparator)
+ .Default('\n');
+ registrar.BaseClassParameter("key_value_separator", &TThis::KeyValueSeparator)
+ .Default('=');
+ registrar.BaseClassParameter("field_separator", &TThis::FieldSeparator)
+ .Default('\t');
+ registrar.BaseClassParameter("line_prefix", &TThis::LinePrefix)
+ .Default();
+ registrar.BaseClassParameter("enable_escaping", &TThis::EnableEscaping)
+ .Default(true);
+ registrar.BaseClassParameter("escaping_symbol", &TThis::EscapingSymbol)
+ .Default('\\');
+ registrar.BaseClassParameter("enable_table_index", &TThis::EnableTableIndex)
+ .Default(false);
+ registrar.BaseClassParameter("has_subkey", &TThis::HasSubkey)
+ .Default(false);
+ registrar.BaseClassParameter("lenval", &TThis::Lenval)
+ .Default(false);
+ registrar.Parameter("key_column_names", &TThis::KeyColumnNames);
+ registrar.Parameter("subkey_column_names", &TThis::SubkeyColumnNames)
+ .Default();
+ registrar.Parameter("yamr_keys_separator", &TThis::YamrKeysSeparator)
+ .Default(' ');
+ registrar.BaseClassParameter("enable_eom", &TThis::EnableEom)
+ .Default(false);
+ registrar.Parameter("skip_unsupported_types_in_value", &TThis::SkipUnsupportedTypesInValue)
+ .Default(false);
+
+ registrar.Preprocessor([] (TThis* config) {
+ if (config->EnableEom && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("EOM marker is not supported in YAMR text mode");
+ }
+ });
+
+ registrar.Postprocessor([] (TThis* config) {
+ THashSet<TString> names;
+
+ for (const auto& name : config->KeyColumnNames) {
+ if (!names.insert(name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column %Qv found in \"key_column_names\"",
+ name);
+ }
+ }
+
+ for (const auto& name : config->SubkeyColumnNames) {
+ if (!names.insert(name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column %Qv found in \"subkey_column_names\"",
+ name);
+ }
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::vector<TString>& TSchemafulDsvFormatConfig::GetColumnsOrThrow() const
+{
+ if (!Columns) {
+ THROW_ERROR_EXCEPTION("Missing \"columns\" attribute in schemaful DSV format");
+ }
+ return *Columns;
+}
+
+void TSchemafulDsvFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("record_separator", &TThis::RecordSeparator)
+ .Default('\n');
+ registrar.BaseClassParameter("field_separator", &TThis::FieldSeparator)
+ .Default('\t');
+
+ registrar.BaseClassParameter("enable_table_index", &TThis::EnableTableIndex)
+ .Default(false);
+
+ registrar.BaseClassParameter("enable_escaping", &TThis::EnableEscaping)
+ .Default(true);
+ registrar.BaseClassParameter("escaping_symbol", &TThis::EscapingSymbol)
+ .Default('\\');
+
+ registrar.Parameter("columns", &TThis::Columns)
+ .Default();
+
+ registrar.Parameter("missing_value_mode", &TThis::MissingValueMode)
+ .Default(EMissingSchemafulDsvValueMode::Fail);
+
+ registrar.Parameter("missing_value_sentinel", &TThis::MissingValueSentinel)
+ .Default("");
+
+ registrar.Parameter("enable_column_names_header", &TThis::EnableColumnNamesHeader)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->Columns) {
+ THashSet<TString> names;
+ for (const auto& name : *config->Columns) {
+ if (!names.insert(name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column name %Qv in schemaful DSV configuration",
+ name);
+ }
+ }
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProtobufTypeConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("proto_type", &TThis::ProtoType);
+ registrar.Parameter("fields", &TThis::Fields)
+ .Default();
+ registrar.Parameter("enumeration_name", &TThis::EnumerationName)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProtobufColumnConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("name", &TThis::Name)
+ .NonEmpty();
+ registrar.Parameter("field_number", &TThis::FieldNumber)
+ .Optional();
+ registrar.Parameter("repeated", &TThis::Repeated)
+ .Default(false);
+ registrar.Parameter("packed", &TThis::Packed)
+ .Default(false);
+
+ registrar.Parameter("type", &TThis::Type)
+ .Default();
+
+ registrar.Parameter("proto_type", &TThis::ProtoType)
+ .Default();
+ registrar.Parameter("fields", &TThis::Fields)
+ .Default();
+ registrar.Parameter("enumeration_name", &TThis::EnumerationName)
+ .Default();
+
+ registrar.Parameter("enum_writing_mode", &TThis::EnumWritingMode)
+ .Default(EProtobufEnumWritingMode::CheckValues);
+
+ registrar.Postprocessor([] (TThis* config) {
+ config->CustomPostprocess();
+ });
+}
+
+void TProtobufColumnConfig::CustomPostprocess()
+{
+ if (Packed && !Repeated) {
+ THROW_ERROR_EXCEPTION("Field %Qv is marked \"packed\" but is not marked \"repeated\"",
+ Name);
+ }
+
+ if (!Type) {
+ Type = New<TProtobufTypeConfig>();
+ if (!ProtoType) {
+ THROW_ERROR_EXCEPTION("One of \"type\" and \"proto_type\" must be specified");
+ }
+ Type->ProtoType = *ProtoType;
+ Type->Fields = std::move(Fields);
+ Type->EnumerationName = EnumerationName;
+ }
+
+ if (!FieldNumber && Type->ProtoType != EProtobufType::Oneof) {
+ THROW_ERROR_EXCEPTION("\"field_number\" is required for type %Qlv",
+ Type->ProtoType);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TProtobufTableConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("columns", &TThis::Columns);
+
+ registrar.Postprocessor([] (TThis* config) {
+ bool hasOtherColumns = false;
+ for (const auto& column: config->Columns) {
+ if (column->ProtoType == EProtobufType::OtherColumns) {
+ if (hasOtherColumns) {
+ THROW_ERROR_EXCEPTION("Multiple \"other_columns\" in protobuf config are not allowed");
+ }
+ hasOtherColumns = true;
+ }
+ }
+ });
+}
+
+void TProtobufFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("file_descriptor_set", &TThis::FileDescriptorSet)
+ .Default();
+ registrar.Parameter("file_indices", &TThis::FileIndices)
+ .Default();
+ registrar.Parameter("message_indices", &TThis::MessageIndices)
+ .Default();
+ registrar.Parameter("nested_messages_mode", &TThis::NestedMessagesMode)
+ .Default(ENestedMessagesMode::Protobuf);
+ registrar.Parameter("enums_as_strings", &TThis::EnumsAsStrings)
+ .Default();
+
+ registrar.Parameter("tables", &TThis::Tables)
+ .Default();
+ registrar.Parameter("enumerations", &TThis::Enumerations)
+ .Default();
+
+ registrar.Parameter("file_descriptor_set_text", &TThis::FileDescriptorSetText)
+ .Default();
+ registrar.Parameter("type_names", &TThis::TypeNames)
+ .Default();
+
+ registrar.Parameter("complex_type_mode", &TThis::ComplexTypeMode)
+ .Default(EComplexTypeMode::Named);
+ registrar.Parameter("decimal_mode", &TThis::DecimalMode)
+ .Default(EDecimalMode::Binary);
+ registrar.Parameter("time_mode", &TThis::TimeMode)
+ .Default(ETimeMode::Binary);
+ registrar.Parameter("uuid_mode", &TThis::UuidMode)
+ .Default(EUuidMode::Binary);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TWebJsonFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_selected_column_count", &TThis::MaxSelectedColumnCount)
+ .Default(50)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("field_weight_limit", &TThis::FieldWeightLimit)
+ .Default(1_KB)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("string_weight_limit", &TThis::StringWeightLimit)
+ .Default(200)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("max_all_column_names_count", &TThis::MaxAllColumnNamesCount)
+ .Default(2000)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("column_names", &TThis::ColumnNames)
+ .Default();
+ registrar.Parameter("value_format", &TThis::ValueFormat)
+ .Default(EWebJsonValueFormat::Schemaless);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSkiffFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("skiff_schema_registry", &TThis::SkiffSchemaRegistry)
+ .Default();
+ registrar.Parameter("table_skiff_schemas", &TThis::TableSkiffSchemas);
+
+ registrar.Parameter("override_intermediate_table_schema", &TThis::OverrideIntermediateTableSchema)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/config.h b/yt/yt/client/formats/config.h
new file mode 100644
index 0000000000..a371050ae6
--- /dev/null
+++ b/yt/yt/client/formats/config.h
@@ -0,0 +1,419 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/config.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TControlAttributesConfig
+ : public NTableClient::TChunkReaderOptions
+{
+public:
+ bool EnableKeySwitch;
+
+ bool EnableEndOfStream;
+
+ REGISTER_YSON_STRUCT(TControlAttributesConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TControlAttributesConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonFormatConfig
+ : public NTableClient::TTypeConversionConfig
+{
+public:
+ NYson::EYsonFormat Format;
+ EComplexTypeMode ComplexTypeMode;
+ EDictMode StringKeyedDictMode;
+ EDecimalMode DecimalMode;
+ ETimeMode TimeMode;
+ EUuidMode UuidMode;
+
+ //! Only works for tabular data.
+ bool SkipNullValues;
+
+ REGISTER_YSON_STRUCT(TYsonFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TYsonFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+// Readers for Yamr and Dsv share lots of methods and functionality //
+// and dependency diagram has the following shape: //
+// //
+// TTableFormatConfigBase --------------------------. //
+// / \ \ //
+// / \ \ //
+// TYamrFormatConfigBase TDsvFormatConfigBase \ //
+// / \ / \ \ //
+// / \ / \ \ //
+// TYamrFormatConfig TYamredDsvFormatConfig TDsvFormatConfig TSchemafulDsvFormatConfig //
+// //
+// All fields are declared in Base classes, all parameters are //
+// registered in derived classes. //
+
+class TTableFormatConfigBase
+ : public NTableClient::TTypeConversionConfig
+{
+public:
+ char RecordSeparator;
+ char FieldSeparator;
+
+ // Escaping rules (EscapingSymbol is '\\')
+ // * '\0' ---> "\0"
+ // * '\n' ---> "\n"
+ // * '\t' ---> "\t"
+ // * 'X' ---> "\X" if X not in ['\0', '\n', '\t']
+ bool EnableEscaping;
+ char EscapingSymbol;
+
+ bool EnableTableIndex;
+
+ REGISTER_YSON_STRUCT(TTableFormatConfigBase);
+
+ static void Register(TRegistrar )
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableFormatConfigBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYamrFormatConfigBase
+ : public virtual TTableFormatConfigBase
+{
+public:
+ bool HasSubkey;
+ bool Lenval;
+ bool EnableEom;
+
+ REGISTER_YSON_STRUCT(TYamrFormatConfigBase);
+
+ static void Register(TRegistrar )
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TYamrFormatConfigBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDsvFormatConfigBase
+ : public virtual TTableFormatConfigBase
+{
+public:
+ char KeyValueSeparator;
+
+ // Only supported for tabular data
+ std::optional<TString> LinePrefix;
+
+ REGISTER_YSON_STRUCT(TDsvFormatConfigBase);
+
+ static void Register(TRegistrar )
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TDsvFormatConfigBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYamrFormatConfig
+ : public TYamrFormatConfigBase
+{
+public:
+ TString Key;
+ TString Subkey;
+ TString Value;
+
+ REGISTER_YSON_STRUCT(TYamrFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TYamrFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDsvFormatConfig
+ : public TDsvFormatConfigBase
+{
+public:
+
+ TString TableIndexColumn;
+ bool SkipUnsupportedTypes = false;
+
+ REGISTER_YSON_STRUCT(TDsvFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDsvFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYamredDsvFormatConfig
+ : public TYamrFormatConfigBase
+ , public TDsvFormatConfigBase
+{
+public:
+ char YamrKeysSeparator;
+
+ std::vector<TString> KeyColumnNames;
+ std::vector<TString> SubkeyColumnNames;
+
+ bool SkipUnsupportedTypesInValue = false;
+
+ REGISTER_YSON_STRUCT(TYamredDsvFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TYamredDsvFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMissingSchemafulDsvValueMode,
+ (SkipRow)
+ (Fail)
+ (PrintSentinel)
+);
+
+class TSchemafulDsvFormatConfig
+ : public TTableFormatConfigBase
+{
+public:
+ std::optional<std::vector<TString>> Columns;
+
+ EMissingSchemafulDsvValueMode MissingValueMode;
+ TString MissingValueSentinel;
+
+ std::optional<bool> EnableColumnNamesHeader;
+
+ const std::vector<TString>& GetColumnsOrThrow() const;
+
+ REGISTER_YSON_STRUCT(TSchemafulDsvFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSchemafulDsvFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EProtobufType,
+ (Double)
+ (Float)
+
+ (Int64)
+ (Uint64)
+ (Sint64)
+ (Fixed64)
+ (Sfixed64)
+
+ (Int32)
+ (Uint32)
+ (Sint32)
+ (Fixed32)
+ (Sfixed32)
+
+ (Bool)
+ (String)
+ (Bytes)
+
+ (EnumInt)
+ (EnumString)
+
+ // Same as 'bytes'.
+ (Message)
+
+ // Protobuf type must be message.
+ // It corresponds to struct type.
+ (StructuredMessage)
+
+ // Protobuf type must be message.
+ // It corresponds to a set of table columns.
+ (EmbeddedMessage)
+
+ // Corresponds to variant struct type.
+ (Oneof)
+
+ // Protobuf type must be string.
+ // Maps to any scalar type (not necessarily "any" type) in table row.
+ (Any)
+
+ // Protobuf type must be string containing valid YSON map.
+ // Each entry (|key|, |value|) of this map will correspond
+ // a separate |TUnversionedValue| under name |key|.
+ // NOTE: Not allowed inside complex types.
+ (OtherColumns)
+);
+
+DEFINE_ENUM(EProtobufEnumWritingMode,
+ (CheckValues)
+ (SkipUnknownValues)
+);
+
+class TProtobufTypeConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ EProtobufType ProtoType;
+ std::vector<TProtobufColumnConfigPtr> Fields;
+ std::optional<TString> EnumerationName;
+
+ REGISTER_YSON_STRUCT(TProtobufTypeConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufTypeConfig)
+
+class TProtobufColumnConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString Name;
+ std::optional<ui64> FieldNumber;
+ bool Repeated;
+ bool Packed;
+
+ TProtobufTypeConfigPtr Type;
+
+ std::optional<EProtobufType> ProtoType;
+ std::vector<TProtobufColumnConfigPtr> Fields;
+ std::optional<TString> EnumerationName;
+ EProtobufEnumWritingMode EnumWritingMode;
+
+ REGISTER_YSON_STRUCT(TProtobufColumnConfig);
+
+ static void Register(TRegistrar registrar);
+public:
+ void CustomPostprocess();
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufColumnConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufTableConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::vector<TProtobufColumnConfigPtr> Columns;
+
+ REGISTER_YSON_STRUCT(TProtobufTableConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufTableConfig)
+
+DEFINE_ENUM(ENestedMessagesMode,
+ (Protobuf)
+ (Yson)
+);
+
+class TProtobufFormatConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString FileDescriptorSet; // deprecated
+ std::vector<int> FileIndices; // deprecated
+ std::vector<int> MessageIndices; // deprecated
+ bool EnumsAsStrings; // deprecated
+ ENestedMessagesMode NestedMessagesMode; // deprecated
+
+ std::vector<TProtobufTableConfigPtr> Tables;
+ NYTree::IMapNodePtr Enumerations;
+
+ std::optional<TString> FileDescriptorSetText;
+ std::vector<TString> TypeNames;
+
+ EComplexTypeMode ComplexTypeMode;
+ EDecimalMode DecimalMode;
+ ETimeMode TimeMode;
+ EUuidMode UuidMode;
+
+ REGISTER_YSON_STRUCT(TProtobufFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EWebJsonValueFormat,
+ // Values are stringified and (de-)serialized together with their types in form
+ // |"column_name": {"$type": "double", "$value": "3.141592"}|.
+ // Strings with length exceeding |FieldWeightLimit| are truncated and meta-attribute
+ // "$incomplete" with value |true| is added to the representation, e.g.
+ // |"column_name": {"$type": "string", "$incomplete": true, "$value": "Some very long st"}|
+ (Schemaless)
+
+ // Values are stringified and (de-)serialized in form
+ // |<column_name>: [ <value>, <stringified-type-index>]|, e.g. |"column_name": ["3.141592", "3"]|.
+ // Type indices point to type registry stored under "yql_type_registry" key.
+ // Non-UTF-8 strings are Base64-encoded and enclosed in a map with "b64" and "val" keys:
+ // | "column_name": {"val": "aqw==", "b64": true} |.
+ // Strings and lists can be truncated, in which case they are enclosed in a map with "inc" and "val" keys:
+ // | "column_name": {"val": ["12", "13"], "inc": true} |
+ // Wrapping in an additional map can occur on any depth.
+ // Both "inc" and "b64" keys may appear in such maps.
+ (Yql)
+);
+
+class TWebJsonFormatConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ int MaxSelectedColumnCount;
+ int FieldWeightLimit;
+ int StringWeightLimit;
+ int MaxAllColumnNamesCount;
+ std::optional<std::vector<TString>> ColumnNames;
+ EWebJsonValueFormat ValueFormat;
+
+ // Intentionally do not reveal following options to user.
+ bool SkipSystemColumns = true;
+
+ REGISTER_YSON_STRUCT(TWebJsonFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TWebJsonFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffFormatConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NYTree::IMapNodePtr SkiffSchemaRegistry;
+ NYTree::IListNodePtr TableSkiffSchemas;
+
+ // This is temporary configuration until we support schema on mapreduce operations fully.
+ std::optional<NTableClient::TTableSchema> OverrideIntermediateTableSchema;
+
+ REGISTER_YSON_STRUCT(TSkiffFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSkiffFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/dsv_parser.cpp b/yt/yt/client/formats/dsv_parser.cpp
new file mode 100644
index 0000000000..c2ccb2babf
--- /dev/null
+++ b/yt/yt/client/formats/dsv_parser.cpp
@@ -0,0 +1,236 @@
+#include "dsv_parser.h"
+
+#include "format.h"
+#include "escape.h"
+#include "parser.h"
+
+namespace NYT::NFormats {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EDsvParserState,
+ (InsidePrefix)
+ (InsideKey)
+ (InsideValue)
+);
+
+class TDsvParser
+ : public IParser
+{
+public:
+ TDsvParser(
+ IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config,
+ bool wrapWithmap);
+
+ void Read(TStringBuf data) override;
+ void Finish() override;
+
+private:
+ IYsonConsumer* Consumer;
+ TDsvFormatConfigPtr Config;
+ bool WrapWithMap; // This is actually used to represent "embedded" semantics.
+ char LastCharacter; // This is used to verify record separator presence.
+
+ TEscapeTable KeyEscapeTable_;
+ TEscapeTable ValueEscapeTable_;
+
+ bool NewRecordStarted;
+ bool ExpectingEscapedChar;
+
+ int RecordCount;
+ int FieldCount;
+
+ TString CurrentToken;
+
+ const char* Consume(const char* begin, const char* end);
+
+ void StartRecordIfNeeded();
+ void FinishRecord();
+
+ void ValidatePrefix(const TString& prefix) const;
+
+ using EState = EDsvParserState;
+ EState State;
+
+ EState GetStartState() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDsvParser::TDsvParser(
+ IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config,
+ bool wrapWithMap)
+ : Consumer(consumer)
+ , Config(config)
+ , WrapWithMap(wrapWithMap)
+ , LastCharacter(Config->RecordSeparator)
+ , NewRecordStarted(!wrapWithMap)
+ , ExpectingEscapedChar(false)
+ , RecordCount(1)
+ , FieldCount(1)
+ , State(GetStartState())
+{
+ ConfigureEscapeTables(config, false /* addCarriageReturn */, &KeyEscapeTable_, &ValueEscapeTable_);
+}
+
+void TDsvParser::Read(TStringBuf data)
+{
+ auto current = data.begin();
+ while (current != data.end()) {
+ current = Consume(current, data.end());
+ }
+}
+
+void TDsvParser::Finish()
+{
+ if (ExpectingEscapedChar) {
+ THROW_ERROR_EXCEPTION("Incomplete escape sequence in DSV");
+ }
+ if (WrapWithMap && LastCharacter != Config->RecordSeparator) {
+ THROW_ERROR_EXCEPTION("Expected record to be terminated with record separator");
+ }
+ if (!CurrentToken.empty()) {
+ StartRecordIfNeeded();
+ }
+ if (State == EState::InsideValue) {
+ Consumer->OnStringScalar(CurrentToken);
+ }
+ if (State == EState::InsidePrefix && !CurrentToken.empty()) {
+ ValidatePrefix(CurrentToken);
+ }
+ CurrentToken.clear();
+ FinishRecord();
+}
+
+const char* TDsvParser::Consume(const char* begin, const char* end)
+{
+ if (end - begin > 0) {
+ LastCharacter = *(end - 1);
+ }
+ // Process escaping symbols.
+ if (Config->EnableEscaping && !ExpectingEscapedChar && *begin == Config->EscapingSymbol) {
+ ExpectingEscapedChar = true;
+ return begin + 1;
+ }
+ if (ExpectingEscapedChar) {
+ CurrentToken.append(EscapeBackward[static_cast<ui8>(*begin)]);
+ ExpectingEscapedChar = false;
+ return begin + 1;
+ }
+
+ // Read until first stop symbol.
+ auto next = State == EState::InsideKey
+ ? KeyEscapeTable_.FindNext(begin, end)
+ : ValueEscapeTable_.FindNext(begin, end);
+ CurrentToken.append(begin, next);
+ if (next == end || (Config->EnableEscaping && *next == Config->EscapingSymbol)) {
+ return next;
+ }
+
+ if (*next == '\0') {
+ THROW_ERROR_EXCEPTION("Unescaped \\0 symbol in DSV")
+ << TErrorAttribute("record_index", RecordCount)
+ << TErrorAttribute("field_index", FieldCount);
+ }
+
+ // Here, we have finished reading prefix, key or value
+ if (State == EState::InsidePrefix) {
+ StartRecordIfNeeded();
+ ValidatePrefix(CurrentToken);
+ State = EState::InsideKey;
+ } else if (State == EState::InsideKey) {
+ StartRecordIfNeeded();
+ if (*next == Config->KeyValueSeparator) {
+ Consumer->OnKeyedItem(CurrentToken);
+ State = EState::InsideValue;
+ }
+ } else if (State == EState::InsideValue) {
+ Consumer->OnStringScalar(CurrentToken);
+ State = EState::InsideKey;
+ FieldCount += 1;
+ } else {
+ YT_ABORT();
+ }
+
+ CurrentToken.clear();
+ if (*next == Config->RecordSeparator) {
+ FinishRecord();
+ }
+ return next + 1;
+}
+
+TDsvParser::EState TDsvParser::GetStartState() const
+{
+ return Config->LinePrefix ? EState::InsidePrefix : EState::InsideKey;
+}
+
+void TDsvParser::StartRecordIfNeeded()
+{
+ if (!NewRecordStarted) {
+ Consumer->OnListItem();
+ Consumer->OnBeginMap();
+ NewRecordStarted = true;
+ }
+}
+
+void TDsvParser::FinishRecord()
+{
+ if (WrapWithMap && NewRecordStarted) {
+ Consumer->OnEndMap();
+ NewRecordStarted = false;
+ }
+ State = GetStartState();
+
+ RecordCount += 1;
+ FieldCount = 1;
+}
+
+void TDsvParser::ValidatePrefix(const TString& prefix) const
+{
+ if (prefix != *Config->LinePrefix) {
+ // TODO(babenko): provide position
+ THROW_ERROR_EXCEPTION("Malformed line prefix in DSV: expected %Qv, found %Qv",
+ *Config->LinePrefix,
+ prefix)
+ << TErrorAttribute("record_index", RecordCount)
+ << TErrorAttribute("field_index", FieldCount);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseDsv(
+ IInputStream* input,
+ IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForDsv(consumer, config);
+ Parse(input, parser.get());
+}
+
+void ParseDsv(
+ TStringBuf data,
+ IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForDsv(consumer, config);
+ parser->Read(data);
+ parser->Finish();
+}
+
+std::unique_ptr<IParser> CreateParserForDsv(
+ IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config,
+ bool wrapWithMap)
+{
+ return std::unique_ptr<IParser>(new TDsvParser(consumer, config, wrapWithMap));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/dsv_parser.h b/yt/yt/client/formats/dsv_parser.h
new file mode 100644
index 0000000000..5a156d5db5
--- /dev/null
+++ b/yt/yt/client/formats/dsv_parser.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * \param wrapWithMap If True then the parser wraps values with calls to
+ * #IYsonConsumer::OnBeginMap and #IYsonConsumer::OnEndMap.
+ */
+std::unique_ptr<IParser> CreateParserForDsv(
+ NYson::IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config = New<TDsvFormatConfig>(),
+ bool wrapWithMap = true);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseDsv(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config = New<TDsvFormatConfig>());
+
+void ParseDsv(
+ TStringBuf data,
+ NYson::IYsonConsumer* consumer,
+ TDsvFormatConfigPtr config = New<TDsvFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/dsv_writer.cpp b/yt/yt/client/formats/dsv_writer.cpp
new file mode 100644
index 0000000000..934b82ed26
--- /dev/null
+++ b/yt/yt/client/formats/dsv_writer.cpp
@@ -0,0 +1,278 @@
+#include "dsv_writer.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/format.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDsvWriterBase::TDsvWriterBase(
+ TDsvFormatConfigPtr config)
+ : Config_(config)
+{
+ YT_VERIFY(Config_);
+ ConfigureEscapeTables(config, true /* addCarriageReturn */, &KeyEscapeTable_, &ValueEscapeTable_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForDsv
+ : public TSchemalessFormatWriterBase
+ , public TDsvWriterBase
+{
+public:
+ TSchemalessWriterForDsv(
+ TNameTablePtr nameTable,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ IAsyncOutputStreamPtr output,
+ TDsvFormatConfigPtr config = New<TDsvFormatConfig>())
+ : TSchemalessFormatWriterBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ 0 /* keyColumnCount */)
+ , TDsvWriterBase(config)
+ { }
+
+private:
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ auto* output = GetOutputStream();
+ for (const auto& row : rows) {
+ bool firstValue = true;
+
+ if (Config_->LinePrefix) {
+ output->Write(*Config_->LinePrefix);
+ firstValue = false;
+ }
+
+ for (const auto* value = row.Begin(); value != row.End(); ++value) {
+ if (value->Type == EValueType::Null) {
+ continue;
+ }
+
+ if (IsRangeIndexColumnId(value->Id) ||
+ IsRowIndexColumnId(value->Id) ||
+ (IsTableIndexColumnId(value->Id) && !Config_->EnableTableIndex))
+ {
+ continue;
+ }
+
+ if (!firstValue) {
+ output->Write(Config_->FieldSeparator);
+ }
+ firstValue = false;
+
+ if (IsTableIndexColumnId(value->Id)) {
+ WriteTableIndexValue(*value);
+ } else {
+ WriteValue(*value);
+ }
+ }
+
+ output->Write(Config_->RecordSeparator);
+ TryFlushBuffer(false);
+ }
+ TryFlushBuffer(true);
+ }
+
+ void WriteValue(const TUnversionedValue& value)
+ {
+ if (Config_->SkipUnsupportedTypes && IsAnyOrComposite(value.Type)) {
+ return;
+ }
+ auto* output = GetOutputStream();
+ EscapeAndWrite(NameTableReader_->GetName(value.Id), output, KeyEscapeTable_);
+ output->Write(Config_->KeyValueSeparator);
+ WriteUnversionedValue(value, output, ValueEscapeTable_);
+ }
+
+ void WriteTableIndexValue(const TUnversionedValue& value)
+ {
+ auto* output = GetOutputStream();
+ EscapeAndWrite(Config_->TableIndexColumn, output, KeyEscapeTable_);
+ output->Write(Config_->KeyValueSeparator);
+ output->Write(::ToString(value.Data.Int64));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDsvNodeConsumer::TDsvNodeConsumer(
+ IOutputStream* stream,
+ TDsvFormatConfigPtr config)
+ : TDsvWriterBase(config)
+ , Stream_(stream)
+{ }
+
+void TDsvNodeConsumer::OnStringScalar(TStringBuf value)
+{
+ EscapeAndWrite(value, Stream_, ValueEscapeTable_);
+}
+
+void TDsvNodeConsumer::OnInt64Scalar(i64 value)
+{
+ Stream_->Write(::ToString(value));
+}
+
+void TDsvNodeConsumer::OnUint64Scalar(ui64 value)
+{
+ Stream_->Write(::ToString(value));
+}
+
+void TDsvNodeConsumer::OnDoubleScalar(double value)
+{
+ Stream_->Write(::ToString(value));
+}
+
+void TDsvNodeConsumer::OnBooleanScalar(bool value)
+{
+ Stream_->Write(FormatBool(value));
+}
+
+void TDsvNodeConsumer::OnEntity()
+{
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Entities are not supported by DSV");
+}
+
+void TDsvNodeConsumer::OnBeginList()
+{
+ if (AllowBeginList_) {
+ AllowBeginList_ = false;
+ } else {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Embedded lists are not supported by DSV");
+ }
+}
+
+void TDsvNodeConsumer::OnListItem()
+{
+ AllowBeginMap_ = true;
+ if (BeforeFirstListItem_) {
+ BeforeFirstListItem_ = false;
+ } else {
+ // Not first item.
+ Stream_->Write(Config_->RecordSeparator);
+ }
+}
+
+void TDsvNodeConsumer::OnEndList()
+{
+ Stream_->Write(Config_->RecordSeparator);
+}
+
+void TDsvNodeConsumer::OnBeginMap()
+{
+ if (AllowBeginMap_) {
+ AllowBeginList_ = false;
+ AllowBeginMap_ = false;
+ BeforeFirstMapItem_ = true;
+ } else {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Embedded maps are not supported by DSV");
+ }
+}
+
+void TDsvNodeConsumer::OnKeyedItem(TStringBuf key)
+{
+ YT_ASSERT(!AllowBeginMap_);
+ YT_ASSERT(!AllowBeginList_);
+
+ if (BeforeFirstMapItem_) {
+ BeforeFirstMapItem_ = false;
+ } else {
+ Stream_->Write(Config_->FieldSeparator);
+ }
+
+ EscapeAndWrite(key, Stream_, KeyEscapeTable_);
+ Stream_->Write(Config_->KeyValueSeparator);
+}
+
+void TDsvNodeConsumer::OnEndMap()
+{
+ YT_ASSERT(!AllowBeginMap_);
+ YT_ASSERT(!AllowBeginList_);
+}
+
+void TDsvNodeConsumer::OnBeginAttributes()
+{
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Embedded attributes are not supported by DSV");
+}
+
+void TDsvNodeConsumer::OnEndAttributes()
+{
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForDsv(
+ TDsvFormatConfigPtr config,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int /* keyColumnCount */)
+{
+ if (controlAttributesConfig->EnableKeySwitch) {
+ THROW_ERROR_EXCEPTION("Key switches are not supported in DSV format");
+ }
+
+ if (controlAttributesConfig->EnableRangeIndex) {
+ THROW_ERROR_EXCEPTION("Range indices are not supported in DSV format");
+ }
+
+ if (controlAttributesConfig->EnableRowIndex) {
+ THROW_ERROR_EXCEPTION("Row indices are not supported in DSV format");
+ }
+
+ if (controlAttributesConfig->EnableTabletIndex) {
+ THROW_ERROR_EXCEPTION("Tablet indices are not supported in DSV format");
+ }
+
+ if (controlAttributesConfig->EnableEndOfStream) {
+ THROW_ERROR_EXCEPTION("End of stream attribute is not supported in DSV format");
+ }
+
+ return New<TSchemalessWriterForDsv>(
+ nameTable,
+ enableContextSaving,
+ controlAttributesConfig,
+ output,
+ config);
+}
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForDsv(
+ const IAttributeDictionary& attributes,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = ConvertTo<TDsvFormatConfigPtr>(&attributes);
+ return CreateSchemalessWriterForDsv(
+ config,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ } catch (const std::exception& exc) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for DSV format") << exc;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/dsv_writer.h b/yt/yt/client/formats/dsv_writer.h
new file mode 100644
index 0000000000..5d1c5de674
--- /dev/null
+++ b/yt/yt/client/formats/dsv_writer.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "config.h"
+#include "escape.h"
+#include "helpers.h"
+#include "public.h"
+#include "schemaless_writer_adapter.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDsvWriterBase
+{
+public:
+ explicit TDsvWriterBase(TDsvFormatConfigPtr config);
+
+protected:
+ const TDsvFormatConfigPtr Config_;
+ TEscapeTable KeyEscapeTable_;
+ TEscapeTable ValueEscapeTable_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// YsonNode is written as follows:
+// * Each element of list is ended with RecordSeparator
+// * Items in map are separated with FieldSeparator
+// * Key and Values in map are separated with KeyValueSeparator
+class TDsvNodeConsumer
+ : public TDsvWriterBase
+ , public TFormatsConsumerBase
+{
+public:
+ explicit TDsvNodeConsumer(
+ IOutputStream* stream,
+ TDsvFormatConfigPtr config = New<TDsvFormatConfig>());
+
+ // IYsonConsumer overrides.
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+private:
+ IOutputStream* const Stream_;
+
+ bool AllowBeginList_ = true;
+ bool AllowBeginMap_ = true;
+
+ bool BeforeFirstMapItem_ = true;
+ bool BeforeFirstListItem_ = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForDsv(
+ TDsvFormatConfigPtr config,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int /* keyColumnCount */);
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForDsv(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int /* keyColumnCount */);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/escape.cpp b/yt/yt/client/formats/escape.cpp
new file mode 100644
index 0000000000..50b1bf85e5
--- /dev/null
+++ b/yt/yt/client/formats/escape.cpp
@@ -0,0 +1,249 @@
+#include "escape.h"
+
+#ifdef YT_USE_SSE42
+ #include <util/system/cpu_id.h>
+#endif
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+#ifdef YT_USE_SSE42
+
+const char _m128i_shift_right[31] = {
+ 0, 1, 2, 3, 4, 5, 6, 7,
+ 8, 9, 10, 11, 12, 13, 14, 15,
+ -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1
+};
+
+// This method performs an "aligned" load of |p| into 128-bit register.
+// If |p| is not aligned then the returned value will contain a (byte-)prefix
+// of memory region pointed by |p| truncated at the first 16-byte boundary.
+// The length of the result is stored into |length|.
+//
+// Note that real motivation for this method is to avoid accidental page faults
+// with direct unaligned reads. I. e., if you have 4 bytes at the end of a page
+// then unaligned read will read 16 - 4 = 12 bytes from the next page causing
+// a page fault; if the next page is unmapped this will incur a segmentation
+// fault and terminate the process.
+YT_ATTRIBUTE_NO_SANITIZE_ADDRESS inline __m128i AlignedPrefixLoad(
+ const void* p,
+ int* length) Y_NO_SANITIZE("memory")
+{
+ int offset = (size_t)p & 15; *length = 16 - offset;
+
+ if (offset) {
+ // Load and shift to the right.
+ // (Kudos to glibc authors for fast implementation).
+ return _mm_shuffle_epi8(
+ _mm_load_si128((__m128i*)((char*)p - offset)),
+ _mm_loadu_si128((__m128i*)(_m128i_shift_right + offset)));
+ } else {
+ // Just load.
+ return _mm_load_si128((__m128i*)p);
+ }
+}
+
+YT_ATTRIBUTE_NO_SANITIZE_ADDRESS inline const char* FindNextSymbol(
+ const char* begin,
+ const char* end,
+ __m128i symbols,
+ int count) Y_NO_SANITIZE("memory")
+{
+ const char* current = begin;
+ int length = end - begin;
+ int result, result2, tmp;
+
+ __m128i value = AlignedPrefixLoad(current, &tmp);
+ tmp = Min(tmp, length);
+
+ do {
+ // In short, PCMPxSTRx instruction takes two 128-bit registers with
+ // packed bytes and performs string comparison on them with user-defined
+ // strategy. As the result PCMPxSTRx produces a lot of stuff, i. e.
+ // match bit-mask, LSB or MSB of that bit-mask, and a few flags.
+ //
+ // See http://software.intel.com/sites/default/files/m/0/3/c/d/4/18187-d9156103.pdf
+ //
+ // In our case we are doing the following:
+ // - _SIDD_UBYTE_OPS - matching unsigned bytes,
+ // - _SIDD_CMP_EQUAL_ANY - comparing any byte from %xmm0 with any byte of %xmm1,
+ // - _SIDD_MASKED_POSITIVE_POLARITY - are interested only in proper bytes with positive matches,
+ // - _SIDD_LEAST_SIGNIFICANT - are interested in the index of least significant match position.
+ //
+ // In human terms this means "find position of first occurrence of
+ // any byte from %xmm0 in %xmm1".
+ //
+ // XXX(sandello): These intrinsics compile down to a single
+ // "pcmpestri $0x20,%xmm0,%xmm1" instruction, because |result| is written
+ // to %ecx and |result2| is (simultaneously) written to CFlag.
+ // We are interested in CFlag because it is cheaper to check.
+ result = _mm_cmpestri(
+ symbols,
+ count,
+ value,
+ tmp,
+ _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY |
+ _SIDD_MASKED_POSITIVE_POLARITY | _SIDD_LEAST_SIGNIFICANT);
+ result2 = _mm_cmpestrc(
+ symbols,
+ count,
+ value,
+ tmp,
+ _SIDD_UBYTE_OPS | _SIDD_CMP_EQUAL_ANY |
+ _SIDD_MASKED_POSITIVE_POLARITY | _SIDD_LEAST_SIGNIFICANT);
+
+ if (result2) {
+ return current + result;
+ } else {
+ current += tmp;
+ length -= tmp;
+ }
+
+ if (length > 0) {
+ // We may load more bytes than needed (but within memory page, due to 16-byte alignment)
+ // but subsequent call to _mm_cmpestri compares only appropriate bytes.
+ value = _mm_load_si128((__m128i*)current);
+ tmp = Min(16, length);
+ } else {
+ break;
+ }
+ } while (true);
+
+ YT_VERIFY(current == end);
+ return current;
+}
+
+#endif
+
+static inline const char* FindNextSymbol(
+ const char* begin,
+ const char* end,
+ const bool* bitmap)
+{
+ // XXX(sandello): Manual loop unrolling saves about 8% CPU.
+ const char* current = begin;
+#define DO_1 if (bitmap[static_cast<ui8>(*current)]) { return current; } ++current;
+#define DO_4 DO_1 DO_1 DO_1 DO_1
+#define DO_16 DO_4 DO_4 DO_4 DO_4
+ while (current + 16 < end) { DO_16; }
+ while (current + 4 < end) { DO_4; }
+ while (current < end) { DO_1; }
+#undef DO_1
+#undef DO_4
+#undef DO_16
+ YT_ASSERT(current == end);
+ return current;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEscapeTable::TEscapeTable()
+{ }
+
+void TEscapeTable::FillStops(const std::vector<char>& stopSymbols)
+{
+ YT_VERIFY(stopSymbols.size() <= 16);
+
+#ifdef YT_USE_SSE42
+ if (NX86::CachedHaveSSE42()) {
+ char storage[16] = {0};
+
+ SymbolCount = stopSymbols.size();
+ for (int i = 0; i < SymbolCount; ++i) {
+ storage[i] = stopSymbols[i]; // :) C-style!
+ }
+
+ Symbols = _mm_setr_epi8(
+ storage[0], storage[1], storage[2], storage[3],
+ storage[4], storage[5], storage[6], storage[7],
+ storage[8], storage[9], storage[10], storage[11],
+ storage[12], storage[13], storage[14], storage[15]);
+ } else
+#endif
+ {
+ for (int i = 0; i < 256; ++i) {
+ Bitmap[i] = false;
+ }
+
+ for (char stopSymbol : stopSymbols) {
+ Bitmap[static_cast<ui8>(stopSymbol)] = true;
+ }
+ }
+}
+
+const char* TEscapeTable::FindNext(const char* begin, const char* end) const
+{
+ if (begin == end) {
+ return end;
+ }
+#ifdef YT_USE_SSE42
+ if (NX86::CachedHaveSSE42()) {
+ return FindNextSymbol(begin, end, Symbols, SymbolCount);
+ } else
+#endif
+ {
+ return FindNextSymbol(begin, end, Bitmap);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void EscapeAndWrite(
+ TStringBuf string,
+ IOutputStream* stream,
+ const TEscapeTable& escapeTable)
+{
+ if (escapeTable.EscapingSymbol) {
+ auto* begin = string.begin();
+ auto* end = string.end();
+ auto* next = begin;
+ for (; begin != end; begin = next) {
+ next = escapeTable.FindNext(begin, end);
+
+ stream->Write(begin, next - begin);
+ if (next != end) {
+ stream->Write(escapeTable.EscapingSymbol);
+ stream->Write(EscapeForward[static_cast<ui8>(*next)]);
+ ++next;
+ }
+ }
+ } else {
+ stream->Write(string);
+ }
+}
+
+TString Escape(
+ TStringBuf string,
+ const TEscapeTable& escapeTable)
+{
+ if (escapeTable.EscapingSymbol) {
+ TString result;
+ // In worst case result length will be twice the original length.
+ result.reserve(2 * string.length());
+ auto* begin = string.begin();
+ auto* end = string.end();
+ auto* next = begin;
+ for (; begin != end; begin = next) {
+ next = escapeTable.FindNext(begin, end);
+ result.append(begin, next);
+ if (next != end) {
+ result.append(escapeTable.EscapingSymbol);
+ result.append(EscapeForward[static_cast<ui8>(*next)]);
+ ++next;
+ }
+ }
+ return result;
+ } else {
+ return TString(string);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/escape.h b/yt/yt/client/formats/escape.h
new file mode 100644
index 0000000000..979ff2689d
--- /dev/null
+++ b/yt/yt/client/formats/escape.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include "public.h"
+
+#include <string>
+#include <vector>
+
+#ifdef YT_USE_SSE42
+# include <nmmintrin.h>
+#endif
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr char EscapeForward[257] =
+ "\x30\x01\x02\x03\x04\x05\x06\x07\x08\x74\x6e\x0b\x0c\x72\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
+ "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
+ "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
+ "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
+ "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
+ "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
+ "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
+ "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
+ "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
+ "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
+ "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
+ "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
+ "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
+ "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
+
+constexpr char EscapeBackward[257] =
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
+ "\x00\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
+ "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
+ "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
+ "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x0a\x6f"
+ "\x70\x71\x0d\x73\x09\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
+ "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
+ "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
+ "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
+ "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
+ "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
+ "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
+ "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
+ "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEscapeTable
+{
+public:
+ TEscapeTable();
+
+ void FillStops(const std::vector<char>& v);
+
+ const char* FindNext(const char* begin, const char* end) const;
+
+ //! Value of \0 means that escaping is not enabled.
+ char EscapingSymbol = '\0';
+
+private:
+
+#ifdef YT_USE_SSE42
+# ifdef _MSC_VER
+# define DECL_PREFIX __declspec(align(16))
+# define DECL_SUFFIX
+# else
+# define DECL_PREFIX
+# define DECL_SUFFIX __attribute__((aligned(16)))
+# endif
+ DECL_PREFIX __m128i Symbols DECL_SUFFIX;
+ int SymbolCount;
+#endif
+ bool Bitmap[256];
+
+};
+
+void EscapeAndWrite(
+ TStringBuf string,
+ IOutputStream* stream,
+ const TEscapeTable& escapeTable);
+
+TString Escape(
+ TStringBuf string,
+ const TEscapeTable& escapeTable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/format.cpp b/yt/yt/client/formats/format.cpp
new file mode 100644
index 0000000000..291f3ed71f
--- /dev/null
+++ b/yt/yt/client/formats/format.cpp
@@ -0,0 +1,660 @@
+#include "format.h"
+#include "parser.h"
+#include "dsv_parser.h"
+#include "dsv_writer.h"
+#include "protobuf_parser.h"
+#include "protobuf_writer.h"
+#include "schemaful_dsv_parser.h"
+#include "schemaful_dsv_writer.h"
+#include "schemaful_writer.h"
+#include "web_json_writer.h"
+#include "schemaless_writer_adapter.h"
+#include "skiff_parser.h"
+#include "skiff_writer.h"
+#include "versioned_writer.h"
+#include "yamred_dsv_parser.h"
+#include "yamred_dsv_writer.h"
+#include "yamr_parser.h"
+#include "yamr_writer.h"
+#include "yson_parser.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/client/table_client/table_consumer.h>
+
+#include <yt/yt/library/skiff_ext/schema_match.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/yson/forwarding_consumer.h>
+
+#include <yt/yt/core/json/json_parser.h>
+#include <yt/yt/core/json/json_writer.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NJson;
+using namespace NTableClient;
+using namespace NSkiffExt;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFormat::TFormat()
+ : Type_(EFormatType::Null)
+ , Attributes_(CreateEphemeralAttributes())
+{ }
+
+TFormat::TFormat(EFormatType type, const IAttributeDictionary* attributes)
+ : Type_(type)
+ , Attributes_(attributes ? attributes->Clone() : CreateEphemeralAttributes())
+{ }
+
+TFormat::TFormat(const TFormat& other)
+ : Type_(other.Type_)
+ , Attributes_(other.Attributes_->Clone())
+{ }
+
+TFormat& TFormat::operator=(const TFormat& other)
+{
+ if (this != &other) {
+ Type_ = other.Type_;
+ Attributes_ = other.Attributes_ ? other.Attributes_->Clone() : nullptr;
+ }
+ return *this;
+}
+
+const IAttributeDictionary& TFormat::Attributes() const
+{
+ return *Attributes_;
+}
+
+void Serialize(const TFormat& value, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginAttributes()
+ .Items(value.Attributes())
+ .EndAttributes()
+ .Value(value.GetType());
+}
+
+void Deserialize(TFormat& value, INodePtr node)
+{
+ if (node->GetType() != ENodeType::String) {
+ THROW_ERROR_EXCEPTION("Format name must be a string");
+ }
+
+ auto typeStr = node->GetValue<TString>();
+ EFormatType type;
+ try {
+ type = ParseEnum<EFormatType>(typeStr);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Invalid format name %Qv",
+ typeStr);
+ }
+
+ value = TFormat(type, &node->Attributes());
+}
+
+void Deserialize(TFormat& value, NYson::TYsonPullParserCursor* cursor)
+{
+ Deserialize(value, ExtractTo<INodePtr>(cursor));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+EYsonType DataTypeToYsonType(EDataType dataType)
+{
+ switch (dataType) {
+ case EDataType::Structured:
+ return EYsonType::Node;
+ case EDataType::Tabular:
+ return EYsonType::ListFragment;
+ default:
+ THROW_ERROR_EXCEPTION("Data type %Qlv is not supported by YSON",
+ dataType);
+ }
+}
+
+std::unique_ptr<IFlushableYsonConsumer> CreateConsumerForYson(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IZeroCopyOutput* output)
+{
+ auto config = ConvertTo<TYsonFormatConfigPtr>(&attributes);
+ return CreateYsonWriter(
+ output,
+ config->Format,
+ DataTypeToYsonType(dataType),
+ config->Format == EYsonFormat::Binary);
+}
+
+std::unique_ptr<IFlushableYsonConsumer> CreateConsumerForJson(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IOutputStream* output)
+{
+ auto config = ConvertTo<TJsonFormatConfigPtr>(&attributes);
+ return CreateJsonConsumer(output, DataTypeToYsonType(dataType), config);
+}
+
+std::unique_ptr<IFlushableYsonConsumer> CreateConsumerForDsv(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IOutputStream* output)
+{
+ auto config = ConvertTo<TDsvFormatConfigPtr>(&attributes);
+ switch (dataType) {
+ case EDataType::Structured:
+ return std::unique_ptr<IFlushableYsonConsumer>(new TDsvNodeConsumer(output, config));
+
+ case EDataType::Tabular:
+ case EDataType::Binary:
+ case EDataType::Null:
+ THROW_ERROR_EXCEPTION("Data type %Qlv is not supported by DSV",
+ dataType);
+
+ default:
+ YT_ABORT();
+ };
+}
+
+class TTableParserAdapter
+ : public IParser
+{
+public:
+ TTableParserAdapter(
+ const TFormat& format,
+ std::vector<IValueConsumer*> valueConsumers,
+ int tableIndex)
+ : TableConsumer_(new TTableConsumer(
+ TYsonConverterConfig{
+ .ComplexTypeMode = format.Attributes().Get("complex_type_mode", EComplexTypeMode::Named),
+ .StringKeyedDictMode = format.Attributes().Get("string_keyed_dict_mode", EDictMode::Positional),
+ .DecimalMode = format.Attributes().Get("decimal_mode", EDecimalMode::Binary),
+ .TimeMode = format.Attributes().Get("time_mode", ETimeMode::Binary),
+ .UuidMode = format.Attributes().Get("uuid_mode", EUuidMode::Binary),
+ },
+ valueConsumers,
+ tableIndex))
+ , Parser_(CreateParserForFormat(
+ format,
+ EDataType::Tabular,
+ TableConsumer_.get()))
+ { }
+
+ void Read(TStringBuf data) override
+ {
+ Parser_->Read(data);
+ }
+
+ void Finish() override
+ {
+ Parser_->Finish();
+ }
+
+private:
+ const std::unique_ptr<IYsonConsumer> TableConsumer_;
+ const std::unique_ptr<IParser> Parser_;
+};
+
+} // namespace
+
+std::unique_ptr<IFlushableYsonConsumer> CreateConsumerForFormat(
+ const TFormat& format,
+ EDataType dataType,
+ IZeroCopyOutput* output)
+{
+ switch (format.GetType()) {
+ case EFormatType::Yson:
+ return CreateConsumerForYson(dataType, format.Attributes(), output);
+ case EFormatType::Json:
+ return CreateConsumerForJson(dataType, format.Attributes(), output);
+ case EFormatType::Dsv:
+ return CreateConsumerForDsv(dataType, format.Attributes(), output);
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported output format %Qlv",
+ format.GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TWriter, class TConsumerAdapter>
+TIntrusivePtr<TWriter> CreateAdaptedWriterForYson(
+ const IAttributeDictionary& attributes,
+ TTableSchemaPtr schema,
+ IAsyncOutputStreamPtr output)
+{
+ auto config = ConvertTo<TYsonFormatConfigPtr>(&attributes);
+ return New<TConsumerAdapter>(std::move(output), std::move(schema), [=] (IZeroCopyOutput* buffer) {
+ if (config->Format == EYsonFormat::Binary) {
+ return std::unique_ptr<IFlushableYsonConsumer>(new TBufferedBinaryYsonWriter(
+ buffer,
+ EYsonType::ListFragment,
+ true));
+ } else {
+ return std::unique_ptr<IFlushableYsonConsumer>(new TYsonWriter(
+ buffer,
+ config->Format,
+ EYsonType::ListFragment));
+ }
+ });
+}
+
+template <class TWriter, class TConsumerAdapter>
+TIntrusivePtr<TWriter> CreateAdaptedWriterForJson(
+ const IAttributeDictionary& attributes,
+ TTableSchemaPtr schema,
+ IAsyncOutputStreamPtr output)
+{
+ auto config = ConvertTo<TJsonFormatConfigPtr>(&attributes);
+ return New<TConsumerAdapter>(std::move(output), std::move(schema), [&] (IOutputStream* buffer) {
+ return CreateJsonConsumer(buffer, EYsonType::ListFragment, config);
+ });
+}
+
+IUnversionedRowsetWriterPtr CreateSchemafulWriterForFormat(
+ const TFormat& format,
+ TTableSchemaPtr schema,
+ IAsyncOutputStreamPtr output)
+{
+ switch (format.GetType()) {
+ case EFormatType::Yson:
+ return CreateAdaptedWriterForYson<IUnversionedRowsetWriter, TSchemafulWriter>(format.Attributes(), std::move(schema), std::move(output));
+ case EFormatType::Json:
+ return CreateAdaptedWriterForJson<IUnversionedRowsetWriter, TSchemafulWriter>(format.Attributes(), std::move(schema), std::move(output));
+ case EFormatType::SchemafulDsv:
+ return CreateSchemafulWriterForSchemafulDsv(format.Attributes(), std::move(schema), std::move(output));
+ case EFormatType::WebJson: {
+ auto webJsonFormatConfig = ConvertTo<TWebJsonFormatConfigPtr>(&format.Attributes());
+ webJsonFormatConfig->SkipSystemColumns = false;
+
+ return CreateWriterForWebJson(
+ std::move(webJsonFormatConfig),
+ TNameTable::FromSchema(*schema),
+ {schema},
+ std::move(output));
+ }
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported output format %Qlv",
+ format.GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedWriterPtr CreateVersionedWriterForFormat(
+ const TFormat& format,
+ NTableClient::TTableSchemaPtr schema,
+ NConcurrency::IAsyncOutputStreamPtr output)
+{
+ switch (format.GetType()) {
+ case EFormatType::Yson:
+ return CreateAdaptedWriterForYson<IVersionedWriter, TVersionedWriter>(format.Attributes(), std::move(schema), std::move(output));
+ case EFormatType::Json:
+ return CreateAdaptedWriterForJson<IVersionedWriter, TVersionedWriter>(format.Attributes(), std::move(schema), std::move(output));
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported output format %Qlv", format.GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateStaticTableWriterForFormat(
+ const TFormat& format,
+ TNameTablePtr nameTable,
+ const std::vector<TTableSchemaPtr>& tableSchemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ switch (format.GetType()) {
+ case EFormatType::Dsv:
+ return CreateSchemalessWriterForDsv(
+ format.Attributes(),
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ case EFormatType::Yamr:
+ return CreateSchemalessWriterForYamr(
+ format.Attributes(),
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ case EFormatType::YamredDsv:
+ return CreateSchemalessWriterForYamredDsv(
+ format.Attributes(),
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ case EFormatType::SchemafulDsv:
+ return CreateSchemalessWriterForSchemafulDsv(
+ format.Attributes(),
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ case EFormatType::Protobuf:
+ return CreateWriterForProtobuf(
+ format.Attributes(),
+ tableSchemas,
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ case EFormatType::WebJson:
+ return CreateWriterForWebJson(
+ format.Attributes(),
+ nameTable,
+ tableSchemas,
+ std::move(output));
+ case EFormatType::Skiff:
+ return CreateWriterForSkiff(
+ format.Attributes(),
+ nameTable,
+ tableSchemas,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ default:
+ auto adapter = New<TSchemalessWriterAdapter>(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ adapter->Init(tableSchemas, format);
+ return adapter;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonProducer CreateProducerForDsv(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IInputStream* input)
+{
+ if (dataType != EDataType::Tabular) {
+ THROW_ERROR_EXCEPTION("DSV is supported only for tabular data");
+ }
+ auto config = ConvertTo<TDsvFormatConfigPtr>(&attributes);
+ return BIND([=] (IYsonConsumer* consumer) {
+ ParseDsv(input, consumer, config);
+ });
+}
+
+TYsonProducer CreateProducerForYamr(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IInputStream* input)
+{
+ if (dataType != EDataType::Tabular) {
+ THROW_ERROR_EXCEPTION("YAMR is supported only for tabular data");
+ }
+ auto config = ConvertTo<TYamrFormatConfigPtr>(&attributes);
+ return BIND([=] (IYsonConsumer* consumer) {
+ ParseYamr(input, consumer, config);
+ });
+}
+
+TYsonProducer CreateProducerForYamredDsv(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IInputStream* input)
+{
+ if (dataType != EDataType::Tabular) {
+ THROW_ERROR_EXCEPTION("Yamred DSV is supported only for tabular data");
+ }
+ auto config = ConvertTo<TYamredDsvFormatConfigPtr>(&attributes);
+ return BIND([=] (IYsonConsumer* consumer) {
+ ParseYamredDsv(input, consumer, config);
+ });
+}
+
+TYsonProducer CreateProducerForSchemafulDsv(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IInputStream* input)
+{
+ if (dataType != EDataType::Tabular) {
+ THROW_ERROR_EXCEPTION("Schemaful DSV is supported only for tabular data");
+ }
+ auto config = ConvertTo<TSchemafulDsvFormatConfigPtr>(&attributes);
+ return BIND([=] (IYsonConsumer* consumer) {
+ ParseSchemafulDsv(input, consumer, config);
+ });
+}
+
+TYsonProducer CreateProducerForJson(
+ EDataType dataType,
+ const IAttributeDictionary& attributes,
+ IInputStream* input)
+{
+ auto ysonType = DataTypeToYsonType(dataType);
+ auto config = ConvertTo<TJsonFormatConfigPtr>(&attributes);
+ return BIND([=] (IYsonConsumer* consumer) {
+ ParseJson(input, consumer, config, ysonType);
+ });
+}
+
+TYsonProducer CreateProducerForYson(EDataType dataType, IInputStream* input)
+{
+ auto ysonType = DataTypeToYsonType(dataType);
+ return ConvertToProducer(TYsonInput(input, ysonType));
+}
+
+TYsonProducer CreateProducerForFormat(const TFormat& format, EDataType dataType, IInputStream* input)
+{
+ switch (format.GetType()) {
+ case EFormatType::Yson:
+ return CreateProducerForYson(dataType, input);
+ case EFormatType::Json:
+ return CreateProducerForJson(dataType, format.Attributes(), input);
+ case EFormatType::Dsv:
+ return CreateProducerForDsv(dataType, format.Attributes(), input);
+ case EFormatType::Yamr:
+ return CreateProducerForYamr(dataType, format.Attributes(), input);
+ case EFormatType::YamredDsv:
+ return CreateProducerForYamredDsv(dataType, format.Attributes(), input);
+ case EFormatType::SchemafulDsv:
+ return CreateProducerForSchemafulDsv(dataType, format.Attributes(), input);
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported input format %Qlv",
+ format.GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template<class TBase>
+struct TParserAdapter
+ : public TBase
+ , public IParser
+{
+public:
+ template<class... TArgs>
+ TParserAdapter(TArgs&&... args)
+ : TBase(std::forward<TArgs>(args)...)
+ { }
+
+ void Read(TStringBuf data) override
+ {
+ TBase::Read(data);
+ }
+
+ void Finish() override
+ {
+ TBase::Finish();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForFormat(const TFormat& format, EDataType dataType, IYsonConsumer* consumer)
+{
+ switch (format.GetType()) {
+ case EFormatType::Yson:
+ return CreateParserForYson(consumer, DataTypeToYsonType(dataType));
+ case EFormatType::Json: {
+ auto config = ConvertTo<TJsonFormatConfigPtr>(&format.Attributes());
+ return std::unique_ptr<IParser>(new TParserAdapter<TJsonParser>(consumer, config, DataTypeToYsonType(dataType)));
+ }
+ case EFormatType::Dsv: {
+ auto config = ConvertTo<TDsvFormatConfigPtr>(&format.Attributes());
+ return CreateParserForDsv(consumer, config);
+ }
+ case EFormatType::Yamr: {
+ auto config = ConvertTo<TYamrFormatConfigPtr>(&format.Attributes());
+ return CreateParserForYamr(consumer, config);
+ }
+ case EFormatType::YamredDsv: {
+ auto config = ConvertTo<TYamredDsvFormatConfigPtr>(&format.Attributes());
+ return CreateParserForYamredDsv(consumer, config);
+ }
+ case EFormatType::SchemafulDsv: {
+ auto config = ConvertTo<TSchemafulDsvFormatConfigPtr>(&format.Attributes());
+ return CreateParserForSchemafulDsv(consumer, config);
+ }
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported input format %Qlv",
+ format.GetType());
+ }
+}
+
+std::vector<std::unique_ptr<IParser>> CreateParsersForFormat(
+ const TFormat& format,
+ const std::vector<IValueConsumer*>& valueConsumers)
+{
+ std::vector<std::unique_ptr<IParser>> parsers;
+
+ auto parserCount = std::ssize(valueConsumers);
+ parsers.reserve(parserCount);
+
+ switch (format.GetType()) {
+ case EFormatType::Protobuf: {
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(&format.Attributes());
+ // TODO(max42): implementation of CreateParserForProtobuf clones config
+ // on each call, so this loop works in quadratic time. Fix that.
+ for (int tableIndex = 0; tableIndex < parserCount; ++tableIndex) {
+ parsers.emplace_back(CreateParserForProtobuf(valueConsumers[tableIndex], config, tableIndex));
+ }
+ break;
+ }
+ case EFormatType::Skiff: {
+ auto config = ConvertTo<TSkiffFormatConfigPtr>(&format.Attributes());
+ auto skiffSchemas = ParseSkiffSchemas(config->SkiffSchemaRegistry, config->TableSkiffSchemas);
+ for (int tableIndex = 0; tableIndex < parserCount; ++tableIndex) {
+ parsers.emplace_back(CreateParserForSkiff(valueConsumers[tableIndex], skiffSchemas, config, tableIndex));
+ }
+ break;
+ }
+ default:
+ for (int tableIndex = 0; tableIndex < parserCount; ++tableIndex) {
+ parsers.emplace_back(std::make_unique<TTableParserAdapter>(format, valueConsumers, tableIndex));
+ }
+ break;
+ }
+
+ return parsers;
+}
+
+std::unique_ptr<IParser> CreateParserForFormat(
+ const TFormat& format,
+ IValueConsumer* valueConsumer)
+{
+ auto parsers = CreateParsersForFormat(format, {valueConsumer});
+ return std::move(parsers.front());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ConfigureEscapeTable(const TSchemafulDsvFormatConfigPtr& config, TEscapeTable* escapeTable)
+{
+ std::vector<char> stopSymbols = {config->RecordSeparator, config->FieldSeparator};
+ if (config->EnableEscaping) {
+ stopSymbols.push_back(config->EscapingSymbol);
+ escapeTable->EscapingSymbol = config->EscapingSymbol;
+ }
+ escapeTable->FillStops(stopSymbols);
+}
+
+void ConfigureEscapeTables(
+ const TDsvFormatConfigBasePtr& config,
+ bool addCarriageReturn,
+ TEscapeTable* keyEscapeTable,
+ TEscapeTable* valueEscapeTable)
+{
+ std::vector<char> stopSymbols = {config->RecordSeparator, config->FieldSeparator, '\0'};
+
+ if (config->EnableEscaping) {
+ stopSymbols.push_back(config->EscapingSymbol);
+ keyEscapeTable->EscapingSymbol = valueEscapeTable->EscapingSymbol = config->EscapingSymbol;
+ }
+
+ if (addCarriageReturn) {
+ stopSymbols.push_back('\r');
+ }
+
+ valueEscapeTable->FillStops(stopSymbols);
+
+ stopSymbols.push_back(config->KeyValueSeparator);
+ keyEscapeTable->FillStops(stopSymbols);
+}
+
+void ConfigureEscapeTables(
+ const TYamrFormatConfigBasePtr& config,
+ bool enableKeyEscaping,
+ bool enableValueEscaping,
+ bool escapingForWriter,
+ TEscapeTable* keyEscapeTable,
+ TEscapeTable* valueEscapeTable)
+{
+ std::vector<char> valueStopSymbols = {config->RecordSeparator};
+ std::vector<char> keyStopSymbols = {config->RecordSeparator, config->FieldSeparator};
+
+ if (enableKeyEscaping) {
+ if (escapingForWriter) {
+ keyStopSymbols.push_back('\0');
+ keyStopSymbols.push_back('\r');
+ }
+ keyStopSymbols.push_back(config->EscapingSymbol);
+ keyEscapeTable->EscapingSymbol = config->EscapingSymbol;
+ }
+
+ if (enableValueEscaping) {
+ if (escapingForWriter) {
+ valueStopSymbols.push_back('\0');
+ valueStopSymbols.push_back('\r');
+ }
+ valueStopSymbols.push_back(config->EscapingSymbol);
+ valueEscapeTable->EscapingSymbol = config->EscapingSymbol;
+ }
+
+ keyEscapeTable->FillStops(keyStopSymbols);
+ valueEscapeTable->FillStops(valueStopSymbols);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/format.h b/yt/yt/client/formats/format.h
new file mode 100644
index 0000000000..23c7741d1d
--- /dev/null
+++ b/yt/yt/client/formats/format.h
@@ -0,0 +1,131 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/attributes.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFormat
+{
+public:
+ TFormat();
+ TFormat(const TFormat& other);
+ TFormat(EFormatType type, const NYTree::IAttributeDictionary* attributes = nullptr);
+
+ TFormat& operator = (const TFormat& other);
+
+ DEFINE_BYVAL_RO_PROPERTY(EFormatType, Type);
+
+ const NYTree::IAttributeDictionary& Attributes() const;
+
+private:
+ NYTree::IAttributeDictionaryPtr Attributes_;
+
+};
+
+void Serialize(const TFormat& value, NYson::IYsonConsumer* consumer);
+void Deserialize(TFormat& value, NYTree::INodePtr node);
+void Deserialize(TFormat& value, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISchemalessFormatWriter
+ : public NTableClient::IUnversionedRowsetWriter
+{
+ virtual TBlob GetContext() const = 0;
+
+ virtual i64 GetWrittenSize() const = 0;
+
+ [[nodiscard]] virtual TFuture<void> Flush() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISchemalessFormatWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This function historically creates format for reading dynamic tables.
+// It slightly differs from format for static tables. :(
+NTableClient::IUnversionedRowsetWriterPtr CreateSchemafulWriterForFormat(
+ const TFormat& Format,
+ NTableClient::TTableSchemaPtr schema,
+ NConcurrency::IAsyncOutputStreamPtr output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTableClient::IVersionedWriterPtr CreateVersionedWriterForFormat(
+ const TFormat& Format,
+ NTableClient::TTableSchemaPtr schema,
+ NConcurrency::IAsyncOutputStreamPtr output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateStaticTableWriterForFormat(
+ const TFormat& format,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& tableSchemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<NYson::IFlushableYsonConsumer> CreateConsumerForFormat(
+ const TFormat& format,
+ EDataType dataType,
+ IZeroCopyOutput* output);
+
+NYson::TYsonProducer CreateProducerForFormat(
+ const TFormat& format,
+ EDataType dataType,
+ IInputStream* input);
+
+std::unique_ptr<IParser> CreateParserForFormat(
+ const TFormat& format,
+ EDataType dataType,
+ NYson::IYsonConsumer* consumer);
+
+//! Create own parser for each value consumer.
+std::vector<std::unique_ptr<IParser>> CreateParsersForFormat(
+ const TFormat& format,
+ const std::vector<NTableClient::IValueConsumer*>& valueConsumers);
+
+//! Create parser for value consumer. Helper for previous method in singular case.
+std::unique_ptr<IParser> CreateParserForFormat(
+ const TFormat& format,
+ NTableClient::IValueConsumer* valueConsumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ConfigureEscapeTable(const TSchemafulDsvFormatConfigPtr& config, TEscapeTable* escapeTable);
+
+void ConfigureEscapeTables(
+ const TDsvFormatConfigBasePtr& config,
+ bool addCarriageReturn,
+ TEscapeTable* keyEscapeTable,
+ TEscapeTable* valueEscapeTable);
+
+void ConfigureEscapeTables(
+ const TYamrFormatConfigBasePtr& config,
+ bool enableKeyEscaping,
+ bool enableValueEscaping,
+ bool escapingForWriter,
+ TEscapeTable* keyEscapeTable,
+ TEscapeTable* valueEscapeTable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/helpers.cpp b/yt/yt/client/formats/helpers.cpp
new file mode 100644
index 0000000000..9609b447fb
--- /dev/null
+++ b/yt/yt/client/formats/helpers.cpp
@@ -0,0 +1,104 @@
+#include "helpers.h"
+
+#include "escape.h"
+#include "format.h"
+
+#include <yt/yt/client/table_client/schema.h>
+
+namespace NYT::NFormats {
+
+using namespace NTableClient;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFormatsConsumerBase::TFormatsConsumerBase()
+ : Parser(this)
+{ }
+
+void TFormatsConsumerBase::OnRaw(TStringBuf yson, EYsonType type)
+{
+ Parser.Parse(yson, type);
+}
+
+void TFormatsConsumerBase::Flush()
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void WriteInt(T value, IOutputStream* output)
+{
+ char buf[64];
+ char* end = buf + 64;
+ char* start = WriteDecIntToBufferBackwards(end, value);
+ output->Write(start, end - start);
+}
+
+void WriteDouble(double value, IOutputStream* output)
+{
+ char buf[64];
+ char* begin = buf;
+ auto length = FloatToString(value, buf, sizeof(buf));
+ if (std::find(begin, begin + length, '.') == begin + length &&
+ std::find(begin, begin + length, 'e') == begin + length)
+ {
+ begin[length++] = '.';
+ }
+ output->Write(begin, length);
+}
+
+void WriteUnversionedValue(const TUnversionedValue& value, IOutputStream* output, const TEscapeTable& escapeTable)
+{
+ switch (value.Type) {
+ case EValueType::Null:
+ return;
+ case EValueType::Int64:
+ WriteInt(value.Data.Int64, output);
+ return;
+ case EValueType::Uint64:
+ WriteInt(value.Data.Uint64, output);
+ return;
+ case EValueType::Double:
+ WriteDouble(value.Data.Double, output);
+ return;
+ case EValueType::Boolean:
+ output->Write(FormatBool(value.Data.Boolean));
+ return;
+ case EValueType::String:
+ EscapeAndWrite(value.AsStringBuf(), output, escapeTable);
+ return;
+
+ case EValueType::Any:
+ case EValueType::Composite:
+
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+ }
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Values of type %Qlv are not supported by the chosen format", value.Type)
+ << TErrorAttribute("value", ToString(value));
+}
+
+bool IsTrivialIntermediateSchema(const NTableClient::TTableSchema& schema)
+{
+ // Here we make expected objects.
+ // It might be not that efficient, but we don't want this code to be fast, we want it to be reliable.
+ // Creating expected objects checks that all attributes have their default values
+ // (including future attributes that might be introduced)
+
+ TKeyColumns columnNames;
+ for (const auto& column : schema.Columns()) {
+ columnNames.push_back(column.Name());
+ }
+
+ // Columns are ok, we check other schema attributes.
+ auto expectedSchema = TTableSchema::FromKeyColumns(columnNames);
+ return schema == *expectedSchema;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/helpers.h b/yt/yt/client/formats/helpers.h
new file mode 100644
index 0000000000..526a95db0f
--- /dev/null
+++ b/yt/yt/client/formats/helpers.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/parser.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFormatsConsumerBase
+ : public virtual NYson::IFlushableYsonConsumer
+{
+public:
+ TFormatsConsumerBase();
+
+ // This method has standard implementation for YAMR, DSV and YAMRED DSV formats.
+ void OnRaw(TStringBuf yson, NYson::EYsonType type) override;
+
+ void Flush() override;
+
+private:
+ NYson::TStatelessYsonParser Parser;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WriteUnversionedValue(const NTableClient::TUnversionedValue& value, IOutputStream* output, const TEscapeTable& escapeTable);
+
+bool IsTrivialIntermediateSchema(const NTableClient::TTableSchema& schema);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/lenval_control_constants.h b/yt/yt/client/formats/lenval_control_constants.h
new file mode 100644
index 0000000000..ccbbc71fe4
--- /dev/null
+++ b/yt/yt/client/formats/lenval_control_constants.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr ui32 LenvalTableIndexMarker = static_cast<ui32>(-1);
+constexpr ui32 LenvalKeySwitch = static_cast<ui32>(-2);
+constexpr ui32 LenvalRangeIndexMarker = static_cast<ui32>(-3);
+constexpr ui32 LenvalRowIndexMarker = static_cast<ui32>(-4);
+constexpr ui32 LenvalEndOfStream = static_cast<ui32>(-5);
+constexpr ui32 LenvalTabletIndexMarker = static_cast<ui32>(-6);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/parser.cpp b/yt/yt/client/formats/parser.cpp
new file mode 100644
index 0000000000..ab90721d4e
--- /dev/null
+++ b/yt/yt/client/formats/parser.cpp
@@ -0,0 +1,28 @@
+#include "parser.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <array>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const size_t ParseBufferSize = 1 << 16;
+
+void Parse(IInputStream* input, IParser* parser)
+{
+ std::array<char, ParseBufferSize> buffer;
+ while (true) {
+ size_t bytesRead = input->Read(buffer.data(), ParseBufferSize);
+ if (bytesRead == 0) {
+ break;
+ }
+ parser->Read(TStringBuf(buffer.data(), bytesRead));
+ }
+ parser->Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/parser.h b/yt/yt/client/formats/parser.h
new file mode 100644
index 0000000000..6993e7e0b9
--- /dev/null
+++ b/yt/yt/client/formats/parser.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IParser
+ : public TNonCopyable
+{
+ virtual ~IParser()
+ { }
+
+ virtual void Read(TStringBuf data) = 0;
+ virtual void Finish() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Parse(IInputStream* input, IParser* parser);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/private.h b/yt/yt/client/formats/private.h
new file mode 100644
index 0000000000..805168dd30
--- /dev/null
+++ b/yt/yt/client/formats/private.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TProtobufParserType)
+DECLARE_REFCOUNTED_CLASS(TProtobufWriterType)
+
+class TProtobufParserFieldDescription;
+class TProtobufWriterFieldDescription;
+
+DECLARE_REFCOUNTED_CLASS(TProtobufParserFormatDescription)
+DECLARE_REFCOUNTED_CLASS(TProtobufWriterFormatDescription)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf.cpp b/yt/yt/client/formats/protobuf.cpp
new file mode 100644
index 0000000000..e4c29652fa
--- /dev/null
+++ b/yt/yt/client/formats/protobuf.cpp
@@ -0,0 +1,1633 @@
+#include "protobuf.h"
+
+#include "protobuf_options.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/json/json_writer.h>
+
+#include <yt/yt/core/misc/string_builder.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <google/protobuf/io/tokenizer.h>
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/wire_format_lite.h>
+
+#include <stack>
+
+namespace NYT::NFormats {
+
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::EnumDescriptor;
+using ::google::protobuf::OneofDescriptor;
+using ::google::protobuf::FieldDescriptor;
+using ::google::protobuf::FileDescriptor;
+using ::google::protobuf::Message;
+using ::google::protobuf::DescriptorPool;
+using ::google::protobuf::internal::WireFormatLite;
+
+using namespace NYT::NTableClient;
+using namespace NYT::NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEnumerationDescription::TEnumerationDescription(const TString& name)
+ : Name_(name)
+{ }
+
+const TString& TEnumerationDescription::GetEnumerationName() const
+{
+ return Name_;
+}
+
+const TString& TEnumerationDescription::GetValueName(i32 value) const
+{
+ if (auto valueName = TryGetValueName(value)) {
+ return *valueName;
+ }
+ THROW_ERROR_EXCEPTION("Invalid value for enum")
+ << TErrorAttribute("enum_name", GetEnumerationName())
+ << TErrorAttribute("value", value);
+}
+
+const TString* TEnumerationDescription::TryGetValueName(i32 value) const
+{
+ auto it = ValueToName_.find(value);
+ if (it == ValueToName_.end()) {
+ return nullptr;
+ }
+ return &it->second;
+}
+
+i32 TEnumerationDescription::GetValue(TStringBuf valueName) const
+{
+ if (auto value = TryGetValue(valueName)) {
+ return *value;
+ }
+ THROW_ERROR_EXCEPTION("Invalid value for enum")
+ << TErrorAttribute("enum_name", GetEnumerationName())
+ << TErrorAttribute("value", valueName);
+}
+
+std::optional<i32> TEnumerationDescription::TryGetValue(TStringBuf valueName) const
+{
+ auto it = NameToValue_.find(valueName);
+ if (it == NameToValue_.end()) {
+ return std::nullopt;
+ }
+ return it->second;
+}
+
+void TEnumerationDescription::Add(TString name, i32 value)
+{
+ if (NameToValue_.find(name) != NameToValue_.end()) {
+ THROW_ERROR_EXCEPTION("Enumeration %v already has value %v",
+ Name_,
+ name);
+ }
+ if (ValueToName_.find(value) != ValueToName_.end()) {
+ THROW_ERROR_EXCEPTION("Enumeration %v already has value %v",
+ Name_,
+ value);
+ }
+ NameToValue_.emplace(name, value);
+ ValueToName_.emplace(value, std::move(name));
+}
+
+static IMapNodePtr ConvertToEnumMap(const EnumDescriptor* enumDescriptor)
+{
+ auto node = BuildYsonNodeFluently().DoMap([&] (TFluentMap fluent) {
+ for (int i = 0; i < enumDescriptor->value_count(); ++i) {
+ auto valueDescriptor = enumDescriptor->value(i);
+ YT_VERIFY(valueDescriptor);
+ fluent.Item(valueDescriptor->name()).Value(valueDescriptor->number());
+ }
+ });
+
+ return node->AsMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TParserErrorCollector
+ : public ::google::protobuf::io::ErrorCollector
+{
+public:
+ void AddError(
+ int line,
+ ::google::protobuf::io::ColumnNumber column,
+ const TString& message) override
+ {
+ if (std::ssize(Errors_) < ErrorCountLimit) {
+ Errors_.push_back(TError("%v", message)
+ << TErrorAttribute("line_number", line)
+ << TErrorAttribute("column_number", column));
+ }
+ }
+
+ std::vector<TError> GetErrors() const
+ {
+ return Errors_;
+ }
+
+private:
+ static constexpr int ErrorCountLimit = 100;
+ std::vector<TError> Errors_;
+};
+
+class TDescriptorPoolErrorCollector
+ : public DescriptorPool::ErrorCollector
+{
+public:
+ void AddError(
+ const TString& fileName,
+ const TString& elementName,
+ const Message* /*descriptor*/,
+ DescriptorPool::ErrorCollector::ErrorLocation /*location*/,
+ const TString& message) override
+ {
+ if (std::ssize(Errors_) < ErrorCountLimit) {
+ Errors_.push_back(TError("%v", message)
+ << TErrorAttribute("file_name", fileName)
+ << TErrorAttribute("element_name", elementName));
+ }
+ }
+
+ void ThrowOnError()
+ {
+ if (!Errors_.empty()) {
+ THROW_ERROR_EXCEPTION("Error while building protobuf descriptors")
+ << Errors_;
+ }
+ }
+
+private:
+ static constexpr int ErrorCountLimit = 100;
+ std::vector<TError> Errors_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TEnumerationDescription CreateEnumerationMap(
+ const TString& enumName,
+ const IMapNodePtr& enumerationConfig)
+{
+ TEnumerationDescription result(enumName);
+ for (const auto& enumValue : enumerationConfig->GetChildren()) {
+ const auto& name = enumValue.first;
+ const auto& valueNode = enumValue.second;
+ switch (valueNode->GetType()) {
+ case ENodeType::Uint64:
+ result.Add(name, valueNode->GetValue<ui64>());
+ break;
+ case ENodeType::Int64:
+ result.Add(name, valueNode->GetValue<i64>());
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Invalid specification of %Qv enumeration: expected type \"int64\" or \"uint64\", actual type %Qlv",
+ enumName,
+ valueNode->GetType());
+ }
+ }
+ return result;
+}
+
+static std::optional<WireFormatLite::FieldType> ConvertFromInternalProtobufType(EProtobufType type)
+{
+ auto fieldType = [=] () -> std::optional<::google::protobuf::FieldDescriptor::Type>{
+ using namespace ::google::protobuf;
+ switch (type) {
+ case EProtobufType::Double:
+ return FieldDescriptor::TYPE_DOUBLE;
+ case EProtobufType::Float:
+ return FieldDescriptor::TYPE_FLOAT;
+
+ case EProtobufType::Int64:
+ return FieldDescriptor::TYPE_INT64;
+ case EProtobufType::Uint64:
+ return FieldDescriptor::TYPE_UINT64;
+ case EProtobufType::Sint64:
+ return FieldDescriptor::TYPE_SINT64;
+ case EProtobufType::Fixed64:
+ return FieldDescriptor::TYPE_FIXED64;
+ case EProtobufType::Sfixed64:
+ return FieldDescriptor::TYPE_SFIXED64;
+
+ case EProtobufType::Int32:
+ return FieldDescriptor::TYPE_INT32;
+ case EProtobufType::Uint32:
+ return FieldDescriptor::TYPE_UINT32;
+ case EProtobufType::Sint32:
+ return FieldDescriptor::TYPE_SINT32;
+ case EProtobufType::Fixed32:
+ return FieldDescriptor::TYPE_FIXED32;
+ case EProtobufType::Sfixed32:
+ return FieldDescriptor::TYPE_SFIXED32;
+
+ case EProtobufType::Bool:
+ return FieldDescriptor::TYPE_BOOL;
+ case EProtobufType::String:
+ return FieldDescriptor::TYPE_STRING;
+ case EProtobufType::Bytes:
+ return FieldDescriptor::TYPE_BYTES;
+
+ case EProtobufType::EnumInt:
+ case EProtobufType::EnumString:
+ return FieldDescriptor::TYPE_ENUM;
+
+ case EProtobufType::Message:
+ case EProtobufType::StructuredMessage:
+ case EProtobufType::EmbeddedMessage:
+ return FieldDescriptor::TYPE_MESSAGE;
+
+ case EProtobufType::Any:
+ case EProtobufType::OtherColumns:
+ return FieldDescriptor::TYPE_BYTES;
+
+ case EProtobufType::Oneof:
+ return std::nullopt;
+ }
+ YT_ABORT();
+ }();
+ if (fieldType) {
+ return static_cast<WireFormatLite::FieldType>(*fieldType);
+ }
+ return std::nullopt;
+}
+
+static std::optional<EProtobufType> ConvertToInternalProtobufType(
+ ::google::protobuf::FieldDescriptor::Type type,
+ bool enumAsString)
+{
+ using namespace ::google::protobuf;
+ switch (type) {
+ case FieldDescriptor::TYPE_DOUBLE:
+ return EProtobufType::Double;
+ case FieldDescriptor::TYPE_FLOAT:
+ return EProtobufType::Float;
+ case FieldDescriptor::TYPE_INT64:
+ return EProtobufType::Int64;
+ case FieldDescriptor::TYPE_UINT64:
+ return EProtobufType::Uint64;
+ case FieldDescriptor::TYPE_INT32:
+ return EProtobufType::Int32;
+ case FieldDescriptor::TYPE_FIXED64:
+ return EProtobufType::Fixed64;
+ case FieldDescriptor::TYPE_FIXED32:
+ return EProtobufType::Fixed32;
+ case FieldDescriptor::TYPE_BOOL:
+ return EProtobufType::Bool;
+ case FieldDescriptor::TYPE_STRING:
+ return EProtobufType::String;
+ case FieldDescriptor::TYPE_GROUP:
+ return std::nullopt;
+ case FieldDescriptor::TYPE_MESSAGE:
+ return EProtobufType::Message;
+ case FieldDescriptor::TYPE_BYTES:
+ return EProtobufType::Bytes;
+ case FieldDescriptor::TYPE_UINT32:
+ return EProtobufType::Uint32;
+ case FieldDescriptor::TYPE_ENUM:
+ return enumAsString ? EProtobufType::EnumString : EProtobufType::EnumInt;
+ case FieldDescriptor::TYPE_SFIXED32:
+ return EProtobufType::Sfixed32;
+ case FieldDescriptor::TYPE_SFIXED64:
+ return EProtobufType::Sfixed64;
+ case FieldDescriptor::TYPE_SINT32:
+ return EProtobufType::Sint32;
+ case FieldDescriptor::TYPE_SINT64:
+ return EProtobufType::Sint64;
+ }
+ // TODO(levysotsky): Throw exception here.
+ return std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui32 TProtobufTag::GetFieldNumber() const
+{
+ return WireFormatLite::GetTagFieldNumber(WireTag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+DEFINE_ENUM(EIntegerSignednessKind,
+ (SignedInteger)
+ (UnsignedInteger)
+ (Other)
+);
+
+} // namespace
+
+void ValidateSimpleType(
+ EProtobufType protobufType,
+ NTableClient::ESimpleLogicalValueType logicalType)
+{
+ if (logicalType == ESimpleLogicalValueType::Any) {
+ return;
+ }
+
+ using EKind = EIntegerSignednessKind;
+
+ auto getLogicalTypeKind = [] (ESimpleLogicalValueType type) {
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+ return EKind::SignedInteger;
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+ return EKind::UnsignedInteger;
+ default:
+ return EKind::Other;
+ }
+ };
+
+ auto getProtobufTypeKind = [] (EProtobufType type) {
+ switch (type) {
+ case EProtobufType::Fixed64:
+ case EProtobufType::Uint64:
+ case EProtobufType::Uint32:
+ case EProtobufType::Fixed32:
+ return EKind::UnsignedInteger;
+
+ case EProtobufType::Int64:
+ case EProtobufType::Sint64:
+ case EProtobufType::Sfixed64:
+ case EProtobufType::Sfixed32:
+ case EProtobufType::Sint32:
+ case EProtobufType::Int32:
+ case EProtobufType::EnumInt:
+ return EKind::SignedInteger;
+
+ default:
+ return EKind::Other;
+ }
+ };
+
+ auto throwMismatchError = [&] (TStringBuf message) {
+ THROW_ERROR_EXCEPTION("Simple logical type %Qlv and protobuf type %Qlv mismatch: %v",
+ logicalType,
+ protobufType,
+ message);
+ };
+
+ auto validateLogicalType = [&] (auto... expectedTypes) {
+ if ((... && (logicalType != expectedTypes))) {
+ auto typeNameList = std::vector<TString>{FormatEnum(expectedTypes)...};
+ throwMismatchError(Format("expected logical type to be one of %v", typeNameList));
+ }
+ };
+
+ switch (protobufType) {
+ case EProtobufType::String:
+ validateLogicalType(ESimpleLogicalValueType::String, ESimpleLogicalValueType::Utf8);
+ return;
+
+ case EProtobufType::Bytes:
+ case EProtobufType::Message:
+ validateLogicalType(ESimpleLogicalValueType::String);
+ return;
+
+ case EProtobufType::Fixed64:
+ case EProtobufType::Uint64:
+ case EProtobufType::Uint32:
+ case EProtobufType::Fixed32:
+ case EProtobufType::Int64:
+ case EProtobufType::Sint64:
+ case EProtobufType::Sfixed64:
+ case EProtobufType::Sfixed32:
+ case EProtobufType::Sint32:
+ case EProtobufType::Int32: {
+ auto logicalTypeKind = getLogicalTypeKind(logicalType);
+ if (logicalTypeKind == EIntegerSignednessKind::Other) {
+ throwMismatchError("integer protobuf type can match only integer type in schema");
+ }
+ if (logicalTypeKind != getProtobufTypeKind(protobufType)) {
+ throwMismatchError("signedness of both types must be the same");
+ }
+ return;
+ }
+
+ case EProtobufType::Float:
+ validateLogicalType(ESimpleLogicalValueType::Float, ESimpleLogicalValueType::Double);
+ return;
+
+ case EProtobufType::Double:
+ validateLogicalType(ESimpleLogicalValueType::Double);
+ return;
+
+ case EProtobufType::Bool:
+ validateLogicalType(ESimpleLogicalValueType::Boolean);
+ return;
+
+ case EProtobufType::EnumInt:
+ if (getLogicalTypeKind(logicalType) != EKind::SignedInteger) {
+ throwMismatchError("logical type must be signed integer type");
+ }
+ return;
+ case EProtobufType::EnumString:
+ if (logicalType != ESimpleLogicalValueType::String &&
+ getLogicalTypeKind(logicalType) != EKind::SignedInteger)
+ {
+ throwMismatchError("logical type must be either signed integer type or string");
+ }
+ return;
+
+ case EProtobufType::Any:
+ // No check here: every type would be OK.
+ return;
+
+ case EProtobufType::StructuredMessage:
+ case EProtobufType::EmbeddedMessage:
+ case EProtobufType::OtherColumns:
+ case EProtobufType::Oneof:
+ throwMismatchError("protobuf type cannot match any simple type");
+ }
+ YT_ABORT();
+}
+
+static bool CanBePacked(EProtobufType type)
+{
+ auto wireFieldType = ConvertFromInternalProtobufType(type);
+ if (!wireFieldType) {
+ return false;
+ }
+ auto wireType = WireFormatLite::WireTypeForFieldType(*wireFieldType);
+ return
+ wireType == WireFormatLite::WireType::WIRETYPE_FIXED32 ||
+ wireType == WireFormatLite::WireType::WIRETYPE_FIXED64 ||
+ wireType == WireFormatLite::WireType::WIRETYPE_VARINT;
+}
+
+[[noreturn]] void ThrowSchemaMismatch(
+ TStringBuf message,
+ const TComplexTypeFieldDescriptor& descriptor,
+ const TProtobufTypeConfigPtr& protoTypeConfig)
+{
+ THROW_ERROR_EXCEPTION("Table schema and protobuf format config mismatch at %v: %v",
+ descriptor.GetDescription(),
+ message)
+ << TErrorAttribute("type_in_schema", ToString(*descriptor.GetType()))
+ << TErrorAttribute("protobuf_type", protoTypeConfig);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TType>
+void TProtobufFormatDescriptionBase<TType>::DoInit(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas)
+{
+ const bool tablesSpecified = !config->Tables.empty();
+ const bool fileDescriptorSetSpecified = !config->FileDescriptorSet.empty();
+ const bool fileDescriptorSetStructuredSpecified = config->FileDescriptorSetText.has_value();
+ if (tablesSpecified + fileDescriptorSetSpecified + fileDescriptorSetStructuredSpecified != 1) {
+ THROW_ERROR_EXCEPTION(R"(Exactly one of "tables", "file_descriptor_set" and "file_descriptor_set_text" )"
+ "must be specified in protobuf format");
+ }
+ if (tablesSpecified) {
+ InitFromProtobufSchema(config, schemas);
+ } else if (fileDescriptorSetSpecified) {
+ InitFromFileDescriptorsLegacy(config);
+ } else {
+ YT_VERIFY(fileDescriptorSetStructuredSpecified);
+ InitFromFileDescriptors(config, schemas);
+ }
+}
+
+static std::vector<const FileDescriptor*> BuildDescriptorPool(
+ const ::google::protobuf::FileDescriptorSet& fileDescriptorSet,
+ DescriptorPool* descriptorPool)
+{
+ std::vector<const FileDescriptor*> fileDescriptors;
+ TDescriptorPoolErrorCollector errorCollector;
+
+ for (const auto& fileDescriptorProto : fileDescriptorSet.file()) {
+ auto* file = descriptorPool->BuildFileCollectingErrors(
+ fileDescriptorProto,
+ &errorCollector);
+ errorCollector.ThrowOnError();
+ if (!file) {
+ THROW_ERROR_EXCEPTION("Unknown error building file %v",
+ fileDescriptorProto.name());
+ }
+
+ fileDescriptors.push_back(file);
+ }
+
+ return fileDescriptors;
+}
+
+static EProtobufType SpecialToProtobufType(ESpecialProtobufType specialType)
+{
+ switch (specialType) {
+ case ESpecialProtobufType::Any:
+ return EProtobufType::Any;
+ case ESpecialProtobufType::EnumInt:
+ return EProtobufType::EnumInt;
+ case ESpecialProtobufType::EnumString:
+ return EProtobufType::EnumString;
+ case ESpecialProtobufType::OtherColumns:
+ return EProtobufType::OtherColumns;
+ }
+ Y_FAIL();
+}
+
+class TCycleChecker
+{
+private:
+ class TGuard
+ {
+ public:
+ TGuard(TCycleChecker* checker, const Descriptor* descriptor)
+ : Checker_(checker)
+ , Descriptor_(descriptor)
+ {
+ Checker_->ActiveVertices_.insert(Descriptor_);
+ Checker_->Stack_.push(Descriptor_);
+ }
+
+ ~TGuard()
+ {
+ Checker_->ActiveVertices_.erase(Descriptor_);
+ Checker_->Stack_.pop();
+ }
+
+ private:
+ TCycleChecker* Checker_;
+ const Descriptor* Descriptor_;
+ };
+
+public:
+ [[nodiscard]] TGuard Enter(const Descriptor* descriptor)
+ {
+ if (ActiveVertices_.contains(descriptor)) {
+ Y_VERIFY(!Stack_.empty());
+ THROW_ERROR_EXCEPTION("Cyclic reference found for protobuf messages. "
+ "Consider removing %Qv flag somewhere on the cycle containing %Qv and %Qv",
+ EWrapperFieldFlag_Enum_Name(EWrapperFieldFlag::SERIALIZATION_YT),
+ Stack_.top()->full_name(),
+ descriptor->full_name());
+ }
+ return TGuard(this, descriptor);
+ }
+
+private:
+ THashSet<const Descriptor*> ActiveVertices_;
+ std::stack<const Descriptor*> Stack_;
+};
+
+class TProtobufTypeConfigBuilder
+{
+public:
+ TProtobufTypeConfigBuilder(bool enumsAsStrings)
+ : EnumsAsStrings_(enumsAsStrings)
+ , Enumerations_(GetEphemeralNodeFactory()->CreateMap())
+ { }
+
+ TProtobufTypeConfigPtr GetOrCreateTypeConfig(const Descriptor* descriptor)
+ {
+ if (auto it = DescriptorToTypeConfig_.find(descriptor); it != DescriptorToTypeConfig_.end()) {
+ return it->second;
+ }
+ auto typeConfig = CreateTypeConfig(descriptor, GetDefaultFieldOptions(descriptor));
+ DescriptorToTypeConfig_.emplace(descriptor, typeConfig);
+ return typeConfig;
+ }
+
+ IMapNodePtr GetEnumerations() const
+ {
+ return Enumerations_;
+ }
+
+private:
+ const bool EnumsAsStrings_;
+ const IMapNodePtr Enumerations_;
+ THashMap<const Descriptor*, TProtobufTypeConfigPtr> DescriptorToTypeConfig_;
+ TCycleChecker CycleChecker_;
+
+private:
+ TProtobufTypeConfigPtr CreateMapConfig(
+ const FieldDescriptor* fieldDescriptor,
+ const TProtobufFieldOptions& fieldOptions)
+ {
+ Y_VERIFY(fieldDescriptor->is_map());
+ const auto* descriptor = fieldDescriptor->message_type();
+ switch (fieldOptions.MapMode) {
+ case EProtobufMapMode::ListOfStructsLegacy:
+ return GetOrCreateTypeConfig(descriptor);
+ case EProtobufMapMode::ListOfStructs:
+ case EProtobufMapMode::Dict:
+ case EProtobufMapMode::OptionalDict:
+ return CreateTypeConfig(
+ descriptor,
+ TProtobufFieldOptions{.SerializationMode = EProtobufSerializationMode::Yt});
+ }
+ Y_FAIL();
+ }
+
+ TProtobufTypeConfigPtr CreateFieldTypeConfig(
+ const FieldDescriptor* fieldDescriptor,
+ const TProtobufFieldOptions& fieldOptions)
+ {
+ if (fieldDescriptor->type() == FieldDescriptor::TYPE_MESSAGE) {
+ switch (fieldOptions.SerializationMode) {
+ case EProtobufSerializationMode::Yt:
+ if (fieldDescriptor->is_map()) {
+ return CreateMapConfig(fieldDescriptor, fieldOptions);
+ } else {
+ return GetOrCreateTypeConfig(fieldDescriptor->message_type());
+ }
+ case EProtobufSerializationMode::Embedded: {
+ auto config = GetOrCreateTypeConfig(fieldDescriptor->message_type());
+ config->ProtoType = EProtobufType::EmbeddedMessage;
+ return config;
+ }
+ case EProtobufSerializationMode::Protobuf:
+ break;
+ // EProtobufSerializationMode::Protobuf handled later
+ }
+ }
+
+ auto typeConfig = New<TProtobufTypeConfig>();
+
+ EProtobufType type;
+ if (fieldOptions.Type) {
+ ValidateProtobufType(fieldDescriptor, *fieldOptions.Type);
+ type = SpecialToProtobufType(*fieldOptions.Type);
+ } else {
+ auto optionalType = ConvertToInternalProtobufType(fieldDescriptor->type(), EnumsAsStrings_);
+ if (!optionalType) {
+ return nullptr;
+ }
+ type = *optionalType;
+ }
+
+ typeConfig->ProtoType = type;
+
+ if (type == EProtobufType::EnumString || type == EProtobufType::EnumInt) {
+ auto enumDescriptor = fieldDescriptor->enum_type();
+ YT_VERIFY(enumDescriptor);
+ const auto& enumName = enumDescriptor->full_name();
+ auto child = Enumerations_->FindChild(enumName);
+ if (!child) {
+ Enumerations_->AddChild(enumName, ConvertToEnumMap(enumDescriptor));
+ }
+ typeConfig->EnumerationName = enumName;
+ }
+
+ return typeConfig;
+ }
+
+ TProtobufColumnConfigPtr CreateFieldConfig(
+ const FieldDescriptor* fieldDescriptor,
+ const TProtobufFieldOptions& defaultOptions)
+ {
+ auto fieldOptions = GetFieldOptions(fieldDescriptor, defaultOptions);
+ auto typeConfig = CreateFieldTypeConfig(fieldDescriptor, fieldOptions);
+ if (!typeConfig) {
+ return nullptr;
+ }
+
+ auto fieldConfig = New<TProtobufColumnConfig>();
+ fieldConfig->Name = GetColumnName(fieldDescriptor);
+ fieldConfig->FieldNumber = fieldDescriptor->number();
+ if (fieldDescriptor->is_repeated() && fieldOptions.SerializationMode != EProtobufSerializationMode::Yt) {
+ THROW_ERROR_EXCEPTION("Repeated field %Qv must have flag %Qv",
+ fieldDescriptor->full_name(),
+ EWrapperFieldFlag_Enum_Name(EWrapperFieldFlag::SERIALIZATION_YT));
+ }
+ fieldConfig->Repeated = fieldDescriptor->is_repeated();
+ fieldConfig->Packed = fieldDescriptor->is_packed();
+ fieldConfig->Type = std::move(typeConfig);
+ fieldConfig->EnumWritingMode = fieldOptions.EnumWritingMode;
+ fieldConfig->Postprocess();
+ return fieldConfig;
+ }
+
+ std::vector<TProtobufColumnConfigPtr> ProcessOneof(
+ const OneofDescriptor* oneofDescriptor,
+ const TProtobufFieldOptions& defaultFieldOptions,
+ const TProtobufOneofOptions& defaultOneofOptions)
+ {
+ std::vector<TProtobufColumnConfigPtr> fieldConfigs;
+ for (int i = 0; i < oneofDescriptor->field_count(); ++i) {
+ auto fieldConfig = CreateFieldConfig(oneofDescriptor->field(i), defaultFieldOptions);
+ if (!fieldConfig) {
+ continue;
+ }
+ fieldConfigs.push_back(std::move(fieldConfig));
+ }
+ if (fieldConfigs.empty()) {
+ THROW_ERROR_EXCEPTION("Parsing of oneof field %Qv resulted in zero variants",
+ oneofDescriptor->full_name());
+ }
+
+ auto oneofOptions = GetOneofOptions(oneofDescriptor, defaultOneofOptions);
+ switch (oneofOptions.Mode) {
+ case EProtobufOneofMode::SeparateFields:
+ return fieldConfigs;
+ case EProtobufOneofMode::Variant: {
+ auto field = New<TProtobufColumnConfig>();
+ field->Name = oneofOptions.VariantFieldName;
+ field->Type = New<TProtobufTypeConfig>();
+ field->Type->ProtoType = EProtobufType::Oneof;
+ field->Type->Fields = std::move(fieldConfigs);
+ return {field};
+ }
+ }
+ Y_FAIL();
+ }
+
+ // NB: This function does not caches the created config!
+ TProtobufTypeConfigPtr CreateTypeConfig(
+ const Descriptor* descriptor,
+ const TProtobufFieldOptions& defaultFieldOptions)
+ {
+ auto typeConfig = New<TProtobufTypeConfig>();
+ typeConfig->ProtoType = EProtobufType::StructuredMessage;
+ auto& fields = typeConfig->Fields;
+
+ auto defaultOneofOptions = GetDefaultOneofOptions(descriptor);
+
+ THashSet<const OneofDescriptor*> visitedOneofs;
+ auto guard = CycleChecker_.Enter(descriptor);
+ for (int fieldIndex = 0; fieldIndex < descriptor->field_count(); ++fieldIndex) {
+ auto fieldDescriptor = descriptor->field(fieldIndex);
+ auto oneofDescriptor = fieldDescriptor->containing_oneof();
+ if (!oneofDescriptor) {
+ auto fieldConfig = CreateFieldConfig(fieldDescriptor, defaultFieldOptions);
+ if (!fieldConfig) {
+ continue;
+ }
+ fields.push_back(std::move(fieldConfig));
+ } else if (!visitedOneofs.contains(oneofDescriptor)) {
+ auto addedFields = ProcessOneof(oneofDescriptor, defaultFieldOptions, defaultOneofOptions);
+ fields.insert(fields.end(), addedFields.begin(), addedFields.end());
+ visitedOneofs.insert(oneofDescriptor);
+ }
+ }
+
+ return typeConfig;
+ }
+};
+
+static TProtobufFormatConfigPtr CreateConfigWithTypes(
+ std::vector<const Descriptor*> descriptors,
+ bool enumsAsStrings = true)
+{
+ auto config = New<TProtobufFormatConfig>();
+ TProtobufTypeConfigBuilder builder(enumsAsStrings);
+ for (const auto* descriptor : descriptors) {
+ auto typeConfig = builder.GetOrCreateTypeConfig(descriptor);
+ auto tableConfig = New<TProtobufTableConfig>();
+ for (const auto& field : typeConfig->Fields) {
+ tableConfig->Columns.push_back(field);
+ }
+ config->Tables.push_back(std::move(tableConfig));
+ }
+ config->Enumerations = builder.GetEnumerations();
+ return config;
+}
+
+template <typename TType>
+void TProtobufFormatDescriptionBase<TType>::InitFromFileDescriptorsLegacy(
+ const TProtobufFormatConfigPtr& config)
+{
+ if (config->FileIndices.empty()) {
+ THROW_ERROR_EXCEPTION(R"("file_indices" attribute must be non empty in protobuf format)");
+ }
+ if (config->MessageIndices.size() != config->FileIndices.size()) {
+ THROW_ERROR_EXCEPTION(R"("message_indices" and "file_indices" must be of same size in protobuf format)");
+ }
+
+ ::google::protobuf::FileDescriptorSet fileDescriptorSet;
+ if (!fileDescriptorSet.ParseFromString(config->FileDescriptorSet)) {
+ THROW_ERROR_EXCEPTION(R"(Error parsing "file_descriptor_set" in protobuf config)");
+ }
+
+ DescriptorPool descriptorPool;
+ auto fileDescriptors = BuildDescriptorPool(fileDescriptorSet, &descriptorPool);
+
+ std::vector<const Descriptor*> messageDescriptors;
+ for (size_t i = 0; i < config->FileIndices.size(); ++i) {
+ if (config->FileIndices[i] >= static_cast<int>(fileDescriptors.size())) {
+ THROW_ERROR_EXCEPTION("File index is out of bound")
+ << TErrorAttribute("file_index", config->FileIndices[i])
+ << TErrorAttribute("file_count", fileDescriptors.size());
+ }
+ auto* fileDescriptor = fileDescriptors[config->FileIndices[i]];
+
+ if (config->MessageIndices[i] >= fileDescriptor->message_type_count()) {
+ THROW_ERROR_EXCEPTION("Message index is out of bound")
+ << TErrorAttribute("message_index", config->MessageIndices[i])
+ << TErrorAttribute("message_count", fileDescriptor->message_type_count());
+ }
+ messageDescriptors.push_back(fileDescriptor->message_type(config->MessageIndices[i]));
+ }
+
+ auto configWithTypes = CreateConfigWithTypes(messageDescriptors, config->EnumsAsStrings);
+ std::vector<TTableSchemaPtr> schemas(messageDescriptors.size(), New<NTableClient::TTableSchema>());
+ InitFromProtobufSchema(configWithTypes, schemas);
+}
+
+template <typename TType>
+void TProtobufFormatDescriptionBase<TType>::InitFromFileDescriptors(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas)
+{
+ if (config->TypeNames.empty()) {
+ THROW_ERROR_EXCEPTION(R"("type_names" attribute must be non empty if "file_descriptor_set_text" )"
+ "is specified");
+ }
+ YT_VERIFY(config->FileDescriptorSetText);
+
+ ::google::protobuf::FileDescriptorSet fileDescriptorSet;
+ ::google::protobuf::TextFormat::Parser parser;
+ // parser.AllowUnknownExtension(true);
+ TParserErrorCollector errorCollector;
+ parser.RecordErrorsTo(&errorCollector);
+ if (!parser.ParseFromString(*config->FileDescriptorSetText, &fileDescriptorSet)) {
+ THROW_ERROR_EXCEPTION(R"(Error parsing "file_descriptor_set_text" in protobuf config)")
+ << errorCollector.GetErrors();
+ }
+
+ DescriptorPool descriptorPool;
+ auto fileDescriptors = BuildDescriptorPool(fileDescriptorSet, &descriptorPool);
+
+ std::vector<const Descriptor*> messageDescriptors;
+ messageDescriptors.reserve(config->TypeNames.size());
+ for (const auto& typeName : config->TypeNames) {
+ auto descriptor = descriptorPool.FindMessageTypeByName(typeName);
+ if (!descriptor) {
+ THROW_ERROR_EXCEPTION("Message type %Qv not found in file descriptor set",
+ typeName);
+ }
+ messageDescriptors.push_back(descriptor);
+ }
+
+ auto configWithTypes = CreateConfigWithTypes(messageDescriptors);
+ InitFromProtobufSchema(configWithTypes, schemas);
+}
+
+template<>
+void TProtobufFormatDescriptionBase<TProtobufWriterType>::InitEmbeddedColumn(
+ int& fieldIndex,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ TProtobufTypeBuilder<TProtobufWriterType>& typeBuilder,
+ TTypePtr tableType,
+ TProtobufColumnConfigPtr columnConfig,
+ TTypePtr parent,
+ int parentEmbeddingIndex)
+{
+ auto embeddingIndex = tableType->AddEmbedding(parentEmbeddingIndex, columnConfig);
+
+ for (auto& fieldConfig: columnConfig->Type->Fields) {
+ InitColumn(fieldIndex, tableSchema, typeBuilder, tableType, fieldConfig, parent, embeddingIndex);
+ }
+}
+
+template<>
+void TProtobufFormatDescriptionBase<TProtobufParserType>::InitEmbeddedColumn(
+ int& fieldIndex,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ TProtobufTypeBuilder<TProtobufParserType>& typeBuilder,
+ TTypePtr tableType,
+ TProtobufColumnConfigPtr columnConfig,
+ TTypePtr parent,
+ int parentEmbeddingIndex)
+{
+ auto child = typeBuilder.CreateField(
+ fieldIndex,
+ columnConfig,
+ std::nullopt,
+ /* allowOtherColumns */ true,
+ true);
+
+ auto childPtr = child.get();
+
+ parent->AddChild(
+ std::nullopt,
+ std::move(child), //KMP
+ fieldIndex);
+
+ for (auto& fieldConfig: columnConfig->Type->Fields) {
+ InitColumn(fieldIndex, tableSchema, typeBuilder, tableType, fieldConfig, childPtr->Type, parentEmbeddingIndex);
+ }
+}
+
+template<typename TType>
+void TProtobufFormatDescriptionBase<TType>::InitColumn(
+ int& fieldIndex,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ TProtobufTypeBuilder<TType>& typeBuilder,
+ TTypePtr tableType,
+ TProtobufColumnConfigPtr columnConfig,
+ TTypePtr parent, // not used for Writer
+ int parentEmbeddingIndex) // not used for Parser
+{
+ if (columnConfig->Type->ProtoType == EProtobufType::EmbeddedMessage) {
+ if (columnConfig->Repeated) {
+ THROW_ERROR_EXCEPTION("Protobuf field %Qv of type %Qlv can not be repeated",
+ columnConfig->Name,
+ EProtobufType::EmbeddedMessage);
+ }
+
+ InitEmbeddedColumn(fieldIndex, tableSchema, typeBuilder, tableType, columnConfig, parent, parentEmbeddingIndex);
+ return;
+ }
+
+ auto columnSchema = tableSchema->FindColumn(columnConfig->Name);
+ TLogicalTypePtr logicalType = columnSchema ? columnSchema->LogicalType() : nullptr;
+ if (columnConfig->ProtoType == EProtobufType::OtherColumns) {
+ if (columnConfig->Repeated) {
+ THROW_ERROR_EXCEPTION("Protobuf field %Qv of type %Qlv can not be repeated",
+ columnConfig->Name,
+ EProtobufType::OtherColumns);
+ }
+ if (logicalType) {
+ THROW_ERROR_EXCEPTION("Protobuf field %Qv of type %Qlv should not match actual column in schema",
+ columnConfig->Name,
+ EProtobufType::OtherColumns);
+ }
+ }
+
+ std::optional<TComplexTypeFieldDescriptor> maybeDescriptor;
+ if (logicalType) {
+ YT_VERIFY(columnSchema);
+ maybeDescriptor = TComplexTypeFieldDescriptor(*columnSchema);
+ }
+
+ bool needSchema = columnConfig->Repeated
+ || columnConfig->ProtoType == EProtobufType::StructuredMessage
+ || columnConfig->ProtoType == EProtobufType::Oneof;
+ if (!logicalType && needSchema) {
+ if (columnConfig->FieldNumber) {
+ // Ignore missing column to facilitate schema evolution.
+ tableType->IgnoreChild(maybeDescriptor, *columnConfig->FieldNumber);
+ return;
+ }
+ THROW_ERROR_EXCEPTION(
+ "Field %Qv of type %Qlv requires a corresponding schematized column",
+ columnConfig->Name,
+ columnConfig->ProtoType);
+ }
+
+ parent->AddChild(
+ maybeDescriptor,
+ typeBuilder.CreateField(
+ fieldIndex,
+ columnConfig,
+ maybeDescriptor,
+ true),
+ fieldIndex,
+ parentEmbeddingIndex);
+ ++fieldIndex;
+}
+
+template <typename TType>
+void TProtobufFormatDescriptionBase<TType>::InitFromProtobufSchema(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas)
+{
+ if (config->Enumerations) {
+ const auto& enumerationConfigMap = config->Enumerations;
+ for (const auto& [name, field] : enumerationConfigMap->GetChildren()) {
+ if (field->GetType() != ENodeType::Map) {
+ THROW_ERROR_EXCEPTION(R"(Invalid enumeration specification type: expected "map", found %Qlv)",
+ field->GetType());
+ }
+ const auto& enumerationConfig = field->AsMap();
+ EnumerationDescriptionMap_.emplace(name, CreateEnumerationMap(name, enumerationConfig));
+ }
+ }
+
+ const auto& tableConfigs = config->Tables;
+ if (tableConfigs.size() < schemas.size()) {
+ THROW_ERROR_EXCEPTION("Number of schemas is greater than number of tables in protobuf config: %v > %v",
+ schemas.size(),
+ tableConfigs.size());
+ }
+
+ auto typeBuilder = TProtobufTypeBuilder<TType>(EnumerationDescriptionMap_);
+
+ for (size_t tableIndex = 0; tableIndex != schemas.size(); ++tableIndex) {
+ auto tableType = New<TType>();
+ tableType->ProtoType = EProtobufType::StructuredMessage;
+
+ int fieldIndex = 0;
+ const auto& tableConfig = tableConfigs[tableIndex];
+ const auto& tableSchema = schemas[tableIndex];
+
+ for (const auto& columnConfig : tableConfig->Columns) {
+ InitColumn(fieldIndex, tableSchema, typeBuilder, tableType, columnConfig, tableType, -1);
+ }
+
+ AddTable(std::move(tableType));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TType>
+TProtobufTypeBuilder<TType>::TProtobufTypeBuilder(const THashMap<TString, TEnumerationDescription>& enumerations)
+ : Enumerations_(enumerations)
+{ }
+
+template <typename TType>
+typename TProtobufTypeBuilder<TType>::TFieldPtr TProtobufTypeBuilder<TType>::CreateField(
+ int structFieldIndex,
+ const TProtobufColumnConfigPtr& columnConfig,
+ std::optional<TComplexTypeFieldDescriptor> maybeDescriptor,
+ bool allowOtherColumns,
+ bool allowEmbedded)
+{
+ const auto& typeConfig = columnConfig->Type;
+ if (!allowEmbedded && typeConfig->ProtoType == EProtobufType::EmbeddedMessage) {
+ THROW_ERROR_EXCEPTION("embedded_message inside of structured_message is not allowed");
+ }
+
+ if (!allowOtherColumns && columnConfig->ProtoType == EProtobufType::OtherColumns) {
+ YT_VERIFY(maybeDescriptor);
+ ThrowSchemaMismatch(
+ "protobuf field of type \"other_columns\" is not allowed inside complex types",
+ *maybeDescriptor,
+ typeConfig);
+ }
+
+ if (columnConfig->Packed && !CanBePacked(typeConfig->ProtoType)) {
+ YT_VERIFY(maybeDescriptor);
+ ThrowSchemaMismatch(
+ Format("packed protobuf field must have primitive numeric type, got %Qlv", typeConfig->ProtoType),
+ *maybeDescriptor,
+ typeConfig);
+ }
+
+ bool optional;
+ if (maybeDescriptor) {
+ if (maybeDescriptor->GetType()->GetMetatype() == ELogicalMetatype::Optional) {
+ maybeDescriptor = maybeDescriptor->OptionalElement();
+ optional = true;
+ } else {
+ optional = false;
+ }
+ } else {
+ optional = true;
+ }
+
+ if (columnConfig->Repeated) {
+ YT_VERIFY(maybeDescriptor);
+ if (maybeDescriptor->GetType()->GetMetatype() == ELogicalMetatype::Dict) {
+ // Do nothing, this case will be processed in the following switch.
+ } else if (maybeDescriptor->GetType()->GetMetatype() == ELogicalMetatype::List) {
+ maybeDescriptor = maybeDescriptor->ListElement();
+ } else {
+ ThrowSchemaMismatch(
+ Format("repeated field must correspond to list or dict, got %Qlv", maybeDescriptor->GetType()->GetMetatype()),
+ *maybeDescriptor,
+ typeConfig);
+ }
+ }
+
+ auto field = std::make_unique<TField>();
+
+ field->Name = columnConfig->Name;
+ field->Repeated = columnConfig->Repeated;
+ field->Packed = columnConfig->Packed;
+ field->StructFieldIndex = structFieldIndex;
+ field->Type = FindOrCreateType(
+ typeConfig,
+ maybeDescriptor,
+ optional,
+ columnConfig->Repeated);
+
+ if constexpr (IsWriter) {
+ field->EnumWritingMode = columnConfig->EnumWritingMode;
+ }
+
+ if (auto wireFieldType = ConvertFromInternalProtobufType(field->Type->ProtoType)) {
+ InitTag(*field, *wireFieldType, columnConfig);
+ }
+
+ return field;
+}
+
+template <typename TType>
+typename TProtobufTypeBuilder<TType>::TTypePtr TProtobufTypeBuilder<TType>::FindOrCreateType(
+ const TProtobufTypeConfigPtr& typeConfig,
+ std::optional<TComplexTypeFieldDescriptor> maybeDescriptor,
+ bool optional,
+ bool repeated)
+{
+ // XXX(levysotsky): Currently we always create new types.
+ // We may want to deduplicate them too.
+ auto type = New<TType>();
+
+ type->ProtoType = typeConfig->ProtoType;
+ type->Optional = optional;
+
+ if (typeConfig->ProtoType == EProtobufType::EnumString && !typeConfig->EnumerationName) {
+ THROW_ERROR_EXCEPTION("Invalid format config: missing \"enumeration_name\" for %Qlv type",
+ EProtobufType::EnumString);
+ }
+ if (typeConfig->EnumerationName) {
+ auto it = Enumerations_.find(*typeConfig->EnumerationName);
+ if (it == Enumerations_.end()) {
+ THROW_ERROR_EXCEPTION("Invalid format config: cannot find enumeration with name %Qv",
+ *typeConfig->EnumerationName);
+ }
+ type->EnumerationDescription = &it->second;
+ }
+
+ if (!maybeDescriptor) {
+ return type;
+ }
+
+ auto descriptor = std::move(*maybeDescriptor);
+
+ const auto& logicalType = descriptor.GetType();
+ YT_VERIFY(logicalType);
+
+ // list<optional<any>> is allowed.
+ if (repeated &&
+ typeConfig->ProtoType == EProtobufType::Any &&
+ logicalType->GetMetatype() == ELogicalMetatype::Optional &&
+ logicalType->GetElement()->GetMetatype() == ELogicalMetatype::Simple &&
+ logicalType->GetElement()->AsSimpleTypeRef().GetElement() == ESimpleLogicalValueType::Any)
+ {
+ return type;
+ }
+
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Struct:
+ if (typeConfig->ProtoType != EProtobufType::StructuredMessage) {
+ ThrowSchemaMismatch(
+ Format("expected \"structured_message\" protobuf type, got %Qlv", typeConfig->ProtoType),
+ descriptor,
+ typeConfig);
+ }
+ VisitStruct(type, typeConfig, descriptor);
+ return type;
+ case ELogicalMetatype::VariantStruct:
+ if (typeConfig->ProtoType != EProtobufType::Oneof) {
+ ThrowSchemaMismatch(
+ Format("expected \"oneof\" protobuf type, got %Qlv", typeConfig->ProtoType),
+ descriptor,
+ typeConfig);
+ }
+ VisitStruct(type, typeConfig, descriptor);
+ return type;
+ case ELogicalMetatype::Dict:
+ YT_VERIFY(repeated);
+ VisitDict(type, typeConfig, descriptor);
+ return type;
+ case ELogicalMetatype::Simple:
+ ValidateSimpleType(type->ProtoType, logicalType->AsSimpleTypeRef().GetElement());
+ return type;
+ default:
+ ThrowSchemaMismatch(
+ Format("unexpected logical metatype %Qlv", logicalType->GetMetatype()),
+ descriptor,
+ typeConfig);
+ }
+ YT_ABORT();
+}
+
+template <typename TType>
+void TProtobufTypeBuilder<TType>:: VisitStruct(
+ const TTypePtr& type,
+ const TProtobufTypeConfigPtr& typeConfig,
+ TComplexTypeFieldDescriptor descriptor)
+{
+ YT_VERIFY(
+ type->ProtoType == EProtobufType::StructuredMessage ||
+ type->ProtoType == EProtobufType::Oneof);
+
+ const auto isOneof = (type->ProtoType == EProtobufType::Oneof);
+
+ THashMap<TStringBuf, TProtobufColumnConfigPtr> nameToConfig;
+ for (const auto& config : typeConfig->Fields) {
+ auto inserted = nameToConfig.emplace(config->Name, config).second;
+ if (!inserted) {
+ ThrowSchemaMismatch(
+ Format("multiple fields with same name (%Qv) are forbidden", config->Name),
+ descriptor,
+ typeConfig);
+ }
+ }
+
+ auto processFieldMissingFromConfig = [&] (const TStructField& structField) {
+ if (isOneof) {
+ // It is OK, we will ignore this alternative when writing.
+ return;
+ }
+ if (structField.Type->GetMetatype() != ELogicalMetatype::Optional && !AreNonOptionalMissingFieldsAllowed) {
+ ThrowSchemaMismatch(
+ Format("non-optional field %Qv in schema is missing from protobuf config", structField.Name),
+ descriptor,
+ typeConfig);
+ }
+ };
+
+ const auto& structFields = descriptor.GetType()->GetFields();
+ if (!isOneof) {
+ type->StructFieldCount = static_cast<int>(structFields.size());
+ }
+ for (int fieldIndex = 0; fieldIndex != static_cast<int>(structFields.size()); ++fieldIndex) {
+ const auto& structField = structFields[fieldIndex];
+ auto configIt = nameToConfig.find(structField.Name);
+ if (configIt == nameToConfig.end()) {
+ processFieldMissingFromConfig(structField);
+ continue;
+ }
+ auto childDescriptor = isOneof
+ ? descriptor.VariantStructField(fieldIndex)
+ : descriptor.StructField(fieldIndex);
+ if (isOneof && childDescriptor.GetType()->IsNullable()) {
+ THROW_ERROR_EXCEPTION("Optional variant field %Qv can not match oneof field in protobuf format",
+ childDescriptor.GetDescription());
+ }
+ type->AddChild(
+ descriptor,
+ CreateField(fieldIndex, configIt->second, childDescriptor),
+ fieldIndex);
+ nameToConfig.erase(configIt);
+ }
+
+ for (const auto& [name, childConfig] : nameToConfig) {
+ if (childConfig->FieldNumber) {
+ type->IgnoreChild(descriptor, *childConfig->FieldNumber);
+ }
+ }
+}
+
+template <typename TType>
+void TProtobufTypeBuilder<TType>::VisitDict(
+ const TTypePtr& type,
+ const TProtobufTypeConfigPtr& typeConfig,
+ TComplexTypeFieldDescriptor descriptor)
+{
+ if (type->ProtoType != EProtobufType::StructuredMessage) {
+ ThrowSchemaMismatch(
+ Format("expected protobuf field of type %Qlv to match %Qlv type in schema",
+ EProtobufType::StructuredMessage,
+ ELogicalMetatype::Dict),
+ descriptor,
+ typeConfig);
+ }
+ const auto& fields = typeConfig->Fields;
+ if (fields.size() != 2 ||
+ fields[0]->Name != "key" ||
+ fields[1]->Name != "value")
+ {
+ ThrowSchemaMismatch(
+ Format(
+ "expected protobuf message with exactly fields \"key\" and \"value\" "
+ "to match %Qlv type in schema",
+ ELogicalMetatype::Dict),
+ descriptor,
+ typeConfig);
+ }
+ type->StructFieldCount = 2;
+ const auto& keyConfig = fields[0];
+ const auto& valueConfig = fields[1];
+ type->AddChild(
+ descriptor,
+ CreateField(0, keyConfig, descriptor.DictKey()),
+ /* fieldIndex */ 0);
+ type->AddChild(
+ descriptor,
+ CreateField(1, valueConfig, descriptor.DictValue()),
+ /* fieldIndex */ 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+static T& ResizeAndGetElement(std::vector<T>& vector, int index, const T& fill = {})
+{
+ if (index >= static_cast<int>(vector.size())) {
+ vector.resize(index + 1, fill);
+ }
+ return vector[index];
+}
+
+void InitTag(
+ TProtobufTag& field,
+ WireFormatLite::FieldType wireFieldType,
+ const TProtobufColumnConfigPtr& columnConfig)
+{
+ YT_VERIFY(columnConfig->FieldNumber);
+ field.TagSize = WireFormatLite::TagSize(*columnConfig->FieldNumber, wireFieldType);
+ WireFormatLite::WireType wireType;
+ if (columnConfig->Packed) {
+ wireType = WireFormatLite::WireType::WIRETYPE_LENGTH_DELIMITED;
+ } else {
+ wireType = WireFormatLite::WireTypeForFieldType(wireFieldType);
+ }
+ field.WireTag = WireFormatLite::MakeTag(*columnConfig->FieldNumber, wireType);
+}
+
+int TProtobufWriterType::AddEmbedding(
+ int parentEmbeddingIndex,
+ const TProtobufColumnConfigPtr& embeddingConfig)
+{
+ int myEmbeddingIndex = std::ssize(Embeddings);
+ auto& embedding = Embeddings.emplace_back();
+
+ embedding.ParentEmbeddingIndex = parentEmbeddingIndex;
+
+ auto wireFieldType = static_cast<WireFormatLite::FieldType>(::google::protobuf::FieldDescriptor::TYPE_MESSAGE);
+ InitTag(embedding, wireFieldType, embeddingConfig);
+
+ return myEmbeddingIndex;
+}
+
+void TProtobufWriterType::AddChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& /* descriptor */,
+ std::unique_ptr<TProtobufWriterFieldDescription> child,
+ std::optional<int> fieldIndex,
+ std::optional<int> parentEmbeddingIndex)
+{
+ if (parentEmbeddingIndex.has_value()) {
+ child->ParentEmbeddingIndex = parentEmbeddingIndex.value();
+ }
+ if (ProtoType == EProtobufType::Oneof) {
+ YT_VERIFY(fieldIndex);
+ ResizeAndGetElement(AlternativeToChildIndex_, *fieldIndex, InvalidChildIndex) = Children.size();
+ }
+ Children.push_back(std::move(child));
+}
+
+void TProtobufWriterType::IgnoreChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& /* descriptor */,
+ int /* fieldNumber */)
+{ }
+
+const TProtobufWriterFieldDescription* TProtobufWriterType::FindAlternative(int alternativeIndex) const
+{
+ if (alternativeIndex >= static_cast<int>(AlternativeToChildIndex_.size())) {
+ return nullptr;
+ }
+ if (AlternativeToChildIndex_[alternativeIndex] == InvalidChildIndex) {
+ return nullptr;
+ }
+ return Children[AlternativeToChildIndex_[alternativeIndex]].get();
+}
+
+void TProtobufWriterFormatDescription::AddTable(TProtobufWriterTypePtr tableType)
+{
+ auto& table = Tables_.emplace_back();
+ table.Type = std::move(tableType);
+ for (const auto& column : table.Type->Children) {
+ auto [it, inserted] = table.Columns.emplace(column->Name, column.get());
+ if (!inserted) {
+ THROW_ERROR_EXCEPTION("Multiple fields with same column name %Qv are forbidden in protobuf format",
+ column->Name)
+ << TErrorAttribute("table_index", std::ssize(Tables_) - 1);
+ }
+ if (column->Type->ProtoType == EProtobufType::OtherColumns) {
+ table.OtherColumnsField = column.get();
+ }
+ }
+ table.Embeddings = table.Type->Embeddings;
+}
+
+const TProtobufWriterFormatDescription::TTableDescription&
+TProtobufWriterFormatDescription::GetTableDescription(int tableIndex) const
+{
+ if (Y_UNLIKELY(tableIndex >= std::ssize(Tables_))) {
+ THROW_ERROR_EXCEPTION("Table with index %v is missing in format description",
+ tableIndex);
+ }
+ return Tables_[tableIndex];
+}
+
+const TProtobufWriterFieldDescription* TProtobufWriterFormatDescription::FindField(
+ int tableIndex,
+ int fieldIndex,
+ const TNameTablePtr& nameTable) const
+{
+ const auto& table = GetTableDescription(tableIndex);
+ auto currentSize = std::ssize(table.FieldIndexToDescription);
+ if (currentSize <= fieldIndex) {
+ table.FieldIndexToDescription.reserve(fieldIndex + 1);
+ for (int i = currentSize; i <= fieldIndex; ++i) {
+ YT_VERIFY(std::ssize(table.FieldIndexToDescription) == i);
+ auto fieldName = nameTable->GetName(i);
+ auto it = table.Columns.find(fieldName);
+ if (it == table.Columns.end()) {
+ table.FieldIndexToDescription.push_back(nullptr);
+ } else {
+ table.FieldIndexToDescription.emplace_back(it->second);
+ }
+ }
+ }
+ return table.FieldIndexToDescription[fieldIndex];
+}
+
+void TProtobufWriterFormatDescription::Init(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas)
+{
+ DoInit(config, schemas);
+}
+
+int TProtobufWriterFormatDescription::GetTableCount() const
+{
+ return std::ssize(Tables_);
+}
+
+const TProtobufWriterFieldDescription* TProtobufWriterFormatDescription::FindOtherColumnsField(int tableIndex) const
+{
+ return GetTableDescription(tableIndex).OtherColumnsField;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int TProtobufParserType::AddEmbedding(
+ int parentEmbeddingIndex,
+ const TProtobufColumnConfigPtr& embeddingConfig)
+{
+ (void) parentEmbeddingIndex;
+ (void) embeddingConfig;
+ return 0;
+}
+
+void TProtobufParserType::AddChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ std::unique_ptr<TProtobufParserFieldDescription> child,
+ std::optional<int> fieldIndex,
+ std::optional<int> embeddingIndex)
+{
+ Y_UNUSED(embeddingIndex);
+ if (child->Type->ProtoType == EProtobufType::Oneof) {
+ if (ProtoType == EProtobufType::Oneof) {
+ YT_VERIFY(descriptor);
+ THROW_ERROR_EXCEPTION("Invalid protobuf format: oneof group %Qv cannot have a oneof child %Qv",
+ descriptor->GetDescription(),
+ child->Name);
+ }
+ YT_VERIFY(!child->Type->Field);
+ child->Type->Field = child.get();
+ for (auto& grandChild : child->Type->Children) {
+ grandChild->StructFieldIndex = child->StructFieldIndex;
+ AddChild(descriptor, std::move(grandChild), /* fieldIndex */ {});
+ }
+ for (auto fieldNumber : child->Type->IgnoredChildFieldNumbers_) {
+ IgnoreChild(descriptor, fieldNumber);
+ }
+ child->Type->Children.clear();
+ child->Type->IgnoredChildFieldNumbers_.clear();
+ // YT_VERIFY(!child->Type->ContainingType);
+ // child->Type->ContainingType = this;
+ OneofDescriptions_.push_back(std::move(child));
+ } else if (ProtoType == EProtobufType::Oneof) {
+ YT_VERIFY(fieldIndex);
+ child->AlternativeIndex = *fieldIndex;
+ YT_VERIFY(!child->ContainingOneof);
+ child->ContainingOneof = this;
+ Children.push_back(std::move(child));
+ } else {
+ SetChildIndex(descriptor, child->GetFieldNumber(), Children.size());
+ Children.push_back(std::move(child));
+ }
+}
+
+void TProtobufParserType::IgnoreChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ int fieldNumber)
+{
+ SetChildIndex(descriptor, fieldNumber, IgnoredChildIndex);
+ IgnoredChildFieldNumbers_.push_back(fieldNumber);
+}
+
+void TProtobufParserType::SetChildIndex(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ int fieldNumber,
+ int childIndex,
+ TFieldNumberToChildIndex* store)
+{
+ if (store == nullptr) {
+ store = &FieldNumberToChildIndex_;
+ }
+ bool isNew = false;
+ if (fieldNumber < MaxFieldNumberVectorSize) {
+ auto& childIndexRef = ResizeAndGetElement(store->FieldNumberToChildIndexVector, fieldNumber, InvalidChildIndex);
+ isNew = (childIndexRef == InvalidChildIndex);
+ childIndexRef = childIndex;
+ } else {
+ isNew = store->FieldNumberToChildIndexMap.emplace(fieldNumber, childIndex).second;
+ }
+ if (!isNew) {
+ THROW_ERROR_EXCEPTION("Invalid protobuf format: duplicate field number %v (child of %Qv)",
+ fieldNumber,
+ descriptor ? descriptor->GetDescription() : "<root>");
+ }
+}
+
+void TProtobufParserType::SetEmbeddedChildIndex(
+ int fieldNumber,
+ int childIndex)
+{
+ SetChildIndex(std::nullopt, fieldNumber, childIndex, &FieldNumberToEmbeddedChildIndex_);
+}
+
+std::optional<int> TProtobufParserType::FieldNumberToChildIndex(int fieldNumber, const TFieldNumberToChildIndex* store) const
+{
+ if (store == nullptr) {
+ store = &FieldNumberToChildIndex_;
+ }
+ int index;
+ if (fieldNumber < std::ssize(store->FieldNumberToChildIndexVector)) {
+ index = store->FieldNumberToChildIndexVector[fieldNumber];
+ if (Y_UNLIKELY(index == InvalidChildIndex)) {
+ THROW_ERROR_EXCEPTION("Unexpected field number %v",
+ fieldNumber);
+ }
+ } else {
+ auto it = store->FieldNumberToChildIndexMap.find(fieldNumber);
+ if (Y_UNLIKELY(it == store->FieldNumberToChildIndexMap.end())) {
+ THROW_ERROR_EXCEPTION("Unexpected field number %v",
+ fieldNumber);
+ }
+ index = it->second;
+ }
+ if (index == IgnoredChildIndex) {
+ return {};
+ }
+ return index;
+}
+
+std::optional<int> TProtobufParserType::FieldNumberToEmbeddedChildIndex(int fieldNumber) const
+{
+ return FieldNumberToChildIndex(fieldNumber, &FieldNumberToEmbeddedChildIndex_);
+}
+
+void TProtobufParserFormatDescription::Init(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas)
+{
+ DoInit(config, schemas);
+}
+
+const TProtobufParserTypePtr& TProtobufParserFormatDescription::GetTableType() const
+{
+ return TableType_;
+}
+
+static int Process(
+ std::vector<std::pair<ui16, TProtobufParserFieldDescription*>>& ids,
+ int globalChildIndex,
+ const TNameTablePtr& nameTable,
+ const TProtobufParserTypePtr parent,
+ const std::unique_ptr<TProtobufParserFieldDescription>& child)
+{
+ if (child->Type->ProtoType == EProtobufType::EmbeddedMessage) {
+ for (const auto& grandChild: child->Type->Children) {
+ globalChildIndex = Process(ids, globalChildIndex, nameTable, child->Type, grandChild);
+ }
+ } else {
+ parent->SetEmbeddedChildIndex(child->GetFieldNumber(), globalChildIndex++);
+ auto name = child->Name;
+ if (child->IsOneofAlternative()) {
+ YT_VERIFY(child->ContainingOneof && child->ContainingOneof->Field);
+ name = child->ContainingOneof->Field->Name;
+ }
+ ids.emplace_back(static_cast<ui16>(nameTable->GetIdOrRegisterName(name)), child.get());
+ }
+ return globalChildIndex;
+}
+
+std::vector<std::pair<ui16, TProtobufParserFieldDescription*>> TProtobufParserFormatDescription::CreateRootChildColumnIds(const TNameTablePtr& nameTable) const
+{
+ std::vector<std::pair<ui16, TProtobufParserFieldDescription*>> ids;
+ int globalChildIndex = 0;
+
+ for (const auto& child: TableType_->Children) {
+ globalChildIndex = Process(ids, globalChildIndex, nameTable, TableType_, child);
+ }
+
+ return ids;
+}
+
+void TProtobufParserFormatDescription::AddTable(TProtobufParserTypePtr tableType)
+{
+ YT_VERIFY(!TableType_);
+ TableType_ = std::move(tableType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf.h b/yt/yt/client/formats/protobuf.h
new file mode 100644
index 0000000000..accf1e8b71
--- /dev/null
+++ b/yt/yt/client/formats/protobuf.h
@@ -0,0 +1,384 @@
+#pragma once
+
+#include "config.h"
+#include "private.h"
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEnumerationDescription
+{
+public:
+ explicit TEnumerationDescription(const TString& name);
+
+ const TString& GetEnumerationName() const;
+
+ const TString& GetValueName(i32 value) const;
+ const TString* TryGetValueName(i32 value) const;
+
+ i32 GetValue(TStringBuf name) const;
+ std::optional<i32> TryGetValue(TStringBuf name) const;
+
+ void Add(TString name, i32 value);
+
+private:
+ THashMap<TString, i32> NameToValue_;
+ THashMap<i32, TString> ValueToName_;
+ TString Name_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufTag
+{
+ ui64 WireTag = 0;
+ size_t TagSize = 0;
+ // Extracts field number from |WireTag|.
+ ui32 GetFieldNumber() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufFieldDescriptionBase
+ : public TProtobufTag
+{
+ TString Name;
+
+ // Index of field inside struct (for fields corresponding to struct fields in schema).
+ int StructFieldIndex = 0;
+
+ bool Repeated = false;
+
+ // Is a repeated field packed (i.e. it is encoded as `<tag> <length> <value1> ... <valueK>`)?
+ bool Packed = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufTypeBase
+ : public TRefCounted
+{
+ EProtobufType ProtoType = EProtobufType::Int64;
+
+ // Is the corresponding type in schema optional?
+ bool Optional = true;
+
+ // Number of fields in struct in schema (only for |Type == StructuredMessage|).
+ int StructFieldCount = 0;
+
+ const TEnumerationDescription* EnumerationDescription = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufWriterEmbeddingDescription
+ : public TProtobufTag
+{
+ static const int InvalidIndex = -1;
+ // Index of wrapping proto message
+ int ParentEmbeddingIndex = InvalidIndex;
+};
+
+class TProtobufWriterFieldDescription
+ : public TProtobufFieldDescriptionBase
+{
+public:
+ // Index of wrapping proto message
+ int ParentEmbeddingIndex = TProtobufWriterEmbeddingDescription::InvalidIndex;
+
+ // Whether to fail on writing unknown value to the enumeration.
+ EProtobufEnumWritingMode EnumWritingMode = EProtobufEnumWritingMode::CheckValues;
+
+ TProtobufWriterTypePtr Type;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufWriterType
+ : public TProtobufTypeBase
+{
+public:
+ int AddEmbedding(
+ int parentParentEmbeddingIndex,
+ const TProtobufColumnConfigPtr& embeddingConfig);
+
+ void AddChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ std::unique_ptr<TProtobufWriterFieldDescription> childType,
+ std::optional<int> fieldIndex,
+ std::optional<int> parentParentEmbeddingIndex = std::nullopt);
+
+ void IgnoreChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ int fieldNumber);
+
+ const TProtobufWriterFieldDescription* FindAlternative(int alternativeIndex) const;
+
+public:
+ std::vector<std::unique_ptr<TProtobufWriterFieldDescription>> Children;
+ std::vector<TProtobufWriterEmbeddingDescription> Embeddings;
+
+private:
+ std::vector<int> AlternativeToChildIndex_;
+ static constexpr int InvalidChildIndex = -1;
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufWriterType)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufParserType
+ : public TProtobufTypeBase
+{
+private:
+ struct TFieldNumberToChildIndex
+ {
+ std::vector<int> FieldNumberToChildIndexVector;
+ THashMap<int, int> FieldNumberToChildIndexMap;
+ };
+
+public:
+ int AddEmbedding(
+ int parentParentEmbeddingIndex,
+ const TProtobufColumnConfigPtr& embeddingConfig);
+
+ void AddChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ std::unique_ptr<TProtobufParserFieldDescription> childType,
+ std::optional<int> fieldIndex,
+ std::optional<int> parentParentEmbeddingIndex = std::nullopt);
+
+ void IgnoreChild(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ int fieldNumber);
+
+ // Returns std::nullopt iff the field is ignored (missing from table schema).
+ // Throws an exception iff the field number is unknown.
+ std::optional<int> FieldNumberToChildIndex(int fieldNumber, const TFieldNumberToChildIndex* store = nullptr) const;
+ std::optional<int> FieldNumberToEmbeddedChildIndex(int fieldNumber) const;
+
+ void SetEmbeddedChildIndex(
+ int fieldNumber,
+ int childIndex);
+private:
+ void SetChildIndex(
+ const std::optional<NTableClient::TComplexTypeFieldDescriptor>& descriptor,
+ int fieldNumber,
+ int childIndex,
+ TFieldNumberToChildIndex* store = nullptr);
+
+public:
+ // For oneof types -- corresponding oneof description.
+ const TProtobufParserFieldDescription* Field = nullptr;
+
+ std::vector<std::unique_ptr<TProtobufParserFieldDescription>> Children;
+
+private:
+ static constexpr int InvalidChildIndex = -1;
+ static constexpr int IgnoredChildIndex = -2;
+ static constexpr int MaxFieldNumberVectorSize = 256;
+
+ std::vector<int> IgnoredChildFieldNumbers_;
+ std::vector<std::unique_ptr<TProtobufParserFieldDescription>> OneofDescriptions_;
+
+ TFieldNumberToChildIndex FieldNumberToChildIndex_;
+ TFieldNumberToChildIndex FieldNumberToEmbeddedChildIndex_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufParserType)
+
+
+class TProtobufParserFieldDescription
+ : public TProtobufFieldDescriptionBase
+{
+public:
+ bool IsOneofAlternative() const
+ {
+ return AlternativeIndex.has_value();
+ }
+
+public:
+ TProtobufParserTypePtr Type;
+
+ // For oneof members -- index of alternative.
+ std::optional<int> AlternativeIndex;
+ // For oneof members -- containing oneof type.
+ const TProtobufParserType* ContainingOneof;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TType>
+class TProtobufTypeBuilder
+{
+public:
+ static constexpr bool IsWriter = std::is_same_v<TType, TProtobufWriterType>;
+ static constexpr bool AreNonOptionalMissingFieldsAllowed = IsWriter;
+
+ using TTypePtr = NYT::TIntrusivePtr<TType>;
+ using TField = std::conditional_t<IsWriter, TProtobufWriterFieldDescription, TProtobufParserFieldDescription>;
+ using TFieldPtr = std::unique_ptr<TField>;
+
+ TProtobufTypeBuilder(const THashMap<TString, TEnumerationDescription>& enumerations);
+
+ TFieldPtr CreateField(
+ int structFieldIndex,
+ const TProtobufColumnConfigPtr& columnConfig,
+ std::optional<NTableClient::TComplexTypeFieldDescriptor> maybeDescriptor,
+ bool allowOtherColumns = false,
+ bool allowEmbedded = false);
+
+private:
+ const THashMap<TString, TEnumerationDescription>& Enumerations_;
+
+private:
+ // Traverse type config, matching it with type descriptor from schema.
+ //
+ // Matching of the type config and type descriptor is performed by the following rules:
+ // * Field of simple type matches simple type T "naturally"
+ // * Repeated field matches List<T> iff corresponding non-repeated field matches T and T is not Optional<...>
+ // (List<Optional<Any>> is allowed as an exception)
+ // * Non-repeated field matches Optional<T> iff it matches T and T is not Optional<...>
+ // * StructuredMessage field matches Struct<Name1: Type1, ..., NameN: TypeN> iff
+ // - the field has subfields whose names are in set {Name1, ..., NameN}
+ // - the subfield with name NameK matches TypeK
+ // - if |NonOptionalMissingFieldsAllowed()| is |false|,
+ // for each name NameK missing from subfields TypeK is Optional<...>
+ TTypePtr FindOrCreateType(
+ const TProtobufTypeConfigPtr& typeConfig,
+ std::optional<NTableClient::TComplexTypeFieldDescriptor> maybeDescriptor,
+ bool optional,
+ bool repeated);
+
+ void VisitStruct(
+ const TTypePtr& type,
+ const TProtobufTypeConfigPtr& typeConfig,
+ NTableClient::TComplexTypeFieldDescriptor descriptor);
+
+ void VisitDict(
+ const TTypePtr& type,
+ const TProtobufTypeConfigPtr& typeConfig,
+ NTableClient::TComplexTypeFieldDescriptor descriptor);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TType>
+class TProtobufFormatDescriptionBase
+ : public TRefCounted
+{
+ static constexpr bool IsWriter = std::is_same_v<TType, TProtobufWriterType>;
+ using TTypePtr = NYT::TIntrusivePtr<TType>;
+
+protected:
+ void DoInit(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas);
+
+private:
+ THashMap<TString, TEnumerationDescription> EnumerationDescriptionMap_;
+
+private:
+ virtual void AddTable(NYT::TIntrusivePtr<TType> tableType) = 0;
+
+ void InitFromFileDescriptorsLegacy(const TProtobufFormatConfigPtr& config);
+
+ void InitFromFileDescriptors(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas);
+
+ void InitEmbeddedColumn(
+ int& fieldIndex,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ TProtobufTypeBuilder<TType>& typeBuilder,
+ TTypePtr tableType,
+ TProtobufColumnConfigPtr columnConfig,
+ TTypePtr parent,
+ int parentParentEmbeddingIndex);
+
+ void InitColumn(
+ int& fieldIndex,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ TProtobufTypeBuilder<TType>& typeBuilder,
+ TTypePtr tableType,
+ TProtobufColumnConfigPtr columnConfig,
+ TTypePtr parent,
+ int parentParentEmbeddingIndex);
+
+ void InitFromProtobufSchema(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufWriterFormatDescription
+ : public TProtobufFormatDescriptionBase<TProtobufWriterType>
+{
+public:
+ void Init(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas);
+
+ const TProtobufWriterFieldDescription* FindField(
+ int tableIndex,
+ int fieldIndex,
+ const NTableClient::TNameTablePtr& nameTable) const;
+
+ int GetTableCount() const;
+
+ const TProtobufWriterFieldDescription* FindOtherColumnsField(int tableIndex) const;
+
+private:
+ void AddTable(TProtobufWriterTypePtr tableType) override;
+
+private:
+ struct TTableDescription
+ {
+ TProtobufWriterTypePtr Type;
+ THashMap<TString, const TProtobufWriterFieldDescription*> Columns;
+ std::vector<TProtobufWriterEmbeddingDescription> Embeddings;
+
+ // Cached data.
+ mutable std::vector<const TProtobufWriterFieldDescription*> FieldIndexToDescription;
+ mutable const TProtobufWriterFieldDescription* OtherColumnsField = nullptr;
+ };
+
+public:
+ const TTableDescription& GetTableDescription(int tableIndex) const;
+
+private:
+ std::vector<TTableDescription> Tables_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufWriterFormatDescription)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufParserFormatDescription
+ : public TProtobufFormatDescriptionBase<TProtobufParserType>
+{
+public:
+ void Init(
+ const TProtobufFormatConfigPtr& config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas);
+
+ const TProtobufParserTypePtr& GetTableType() const;
+ std::vector<std::pair<ui16, TProtobufParserFieldDescription*>> CreateRootChildColumnIds(const NTableClient::TNameTablePtr& nameTable) const;
+
+private:
+ void AddTable(TProtobufParserTypePtr tableType) override;
+
+private:
+ TProtobufParserTypePtr TableType_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TProtobufParserFormatDescription)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf_options.cpp b/yt/yt/client/formats/protobuf_options.cpp
new file mode 100644
index 0000000000..5e9543d844
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_options.cpp
@@ -0,0 +1,503 @@
+#include "protobuf_options.h"
+
+#include <util/generic/variant.h>
+
+namespace NYT::NFormats {
+
+using ::google::protobuf::Descriptor;
+using ::google::protobuf::OneofDescriptor;
+using ::google::protobuf::FieldDescriptor;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TOneofOption = std::variant<
+ EProtobufOneofMode>;
+
+using TFieldOption = std::variant<
+ ESpecialProtobufType,
+ EProtobufSerializationMode,
+ EProtobufListMode,
+ EProtobufMapMode,
+ EProtobufEnumWritingMode>;
+
+using TMessageOption = std::variant<
+ EProtobufFieldSortOrder>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFieldOption FieldFlagToOption(EWrapperFieldFlag::Enum flag)
+{
+ using EFlag = EWrapperFieldFlag;
+ switch (flag) {
+ case EFlag::SERIALIZATION_PROTOBUF:
+ return EProtobufSerializationMode::Protobuf;
+ case EFlag::SERIALIZATION_YT:
+ return EProtobufSerializationMode::Yt;
+ case EFlag::EMBEDDED:
+ return EProtobufSerializationMode::Embedded;
+
+ case EFlag::ANY:
+ return ESpecialProtobufType::Any;
+ case EFlag::OTHER_COLUMNS:
+ return ESpecialProtobufType::OtherColumns;
+ case EFlag::ENUM_INT:
+ return ESpecialProtobufType::EnumInt;
+ case EFlag::ENUM_STRING:
+ return ESpecialProtobufType::EnumString;
+
+ case EFlag::OPTIONAL_LIST:
+ return EProtobufListMode::Optional;
+ case EFlag::REQUIRED_LIST:
+ return EProtobufListMode::Required;
+
+ case EFlag::MAP_AS_LIST_OF_STRUCTS_LEGACY:
+ return EProtobufMapMode::ListOfStructsLegacy;
+ case EFlag::MAP_AS_LIST_OF_STRUCTS:
+ return EProtobufMapMode::ListOfStructs;
+ case EFlag::MAP_AS_DICT:
+ return EProtobufMapMode::Dict;
+ case EFlag::MAP_AS_OPTIONAL_DICT:
+ return EProtobufMapMode::OptionalDict;
+
+ case EFlag::ENUM_SKIP_UNKNOWN_VALUES:
+ return EProtobufEnumWritingMode::SkipUnknownValues;
+ case EFlag::ENUM_CHECK_VALUES:
+ return EProtobufEnumWritingMode::CheckValues;
+ }
+ Y_FAIL();
+}
+
+TMessageOption MessageFlagToOption(EWrapperMessageFlag::Enum flag)
+{
+ using EFlag = EWrapperMessageFlag;
+ switch (flag) {
+ case EFlag::DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE:
+ return EProtobufFieldSortOrder::AsInProtoFile;
+ case EFlag::SORT_FIELDS_BY_FIELD_NUMBER:
+ return EProtobufFieldSortOrder::ByFieldNumber;
+ }
+ Y_FAIL();
+}
+
+TOneofOption OneofFlagToOption(EWrapperOneofFlag::Enum flag)
+{
+ using EFlag = EWrapperOneofFlag;
+ switch (flag) {
+ case EFlag::SEPARATE_FIELDS:
+ return EProtobufOneofMode::SeparateFields;
+ case EFlag::VARIANT:
+ return EProtobufOneofMode::Variant;
+ }
+ Y_FAIL();
+}
+
+TString OptionToFieldFlagName(TFieldOption option)
+{
+ using EFlag = EWrapperFieldFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (ESpecialProtobufType type)
+ {
+ switch (type) {
+ case ESpecialProtobufType::Any:
+ return EFlag::ANY;
+ case ESpecialProtobufType::OtherColumns:
+ return EFlag::OTHER_COLUMNS;
+ case ESpecialProtobufType::EnumInt:
+ return EFlag::ENUM_INT;
+ case ESpecialProtobufType::EnumString:
+ return EFlag::ENUM_STRING;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufSerializationMode serializationMode)
+ {
+ switch (serializationMode) {
+ case EProtobufSerializationMode::Yt:
+ return EFlag::SERIALIZATION_YT;
+ case EProtobufSerializationMode::Protobuf:
+ return EFlag::SERIALIZATION_PROTOBUF;
+ case EProtobufSerializationMode::Embedded:
+ return EFlag::EMBEDDED;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufListMode listMode)
+ {
+ switch (listMode) {
+ case EProtobufListMode::Optional:
+ return EFlag::OPTIONAL_LIST;
+ case EProtobufListMode::Required:
+ return EFlag::REQUIRED_LIST;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufMapMode mapMode)
+ {
+ switch (mapMode) {
+ case EProtobufMapMode::ListOfStructsLegacy:
+ return EFlag::MAP_AS_LIST_OF_STRUCTS_LEGACY;
+ case EProtobufMapMode::ListOfStructs:
+ return EFlag::MAP_AS_LIST_OF_STRUCTS;
+ case EProtobufMapMode::Dict:
+ return EFlag::MAP_AS_DICT;
+ case EProtobufMapMode::OptionalDict:
+ return EFlag::MAP_AS_OPTIONAL_DICT;
+ }
+ Y_FAIL();
+ }
+ EFlag::Enum operator() (EProtobufEnumWritingMode enumWritingMode)
+ {
+ switch (enumWritingMode) {
+ case EProtobufEnumWritingMode::SkipUnknownValues:
+ return EFlag::ENUM_SKIP_UNKNOWN_VALUES;
+ case EProtobufEnumWritingMode::CheckValues:
+ return EFlag::ENUM_CHECK_VALUES;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return EWrapperFieldFlag_Enum_Name(std::visit(TVisitor(), option));
+}
+
+TString OptionToMessageFlagName(TMessageOption option)
+{
+ using EFlag = EWrapperMessageFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (EProtobufFieldSortOrder sortOrder)
+ {
+ switch (sortOrder) {
+ case EProtobufFieldSortOrder::AsInProtoFile:
+ return EFlag::DEPRECATED_SORT_FIELDS_AS_IN_PROTO_FILE;
+ case EProtobufFieldSortOrder::ByFieldNumber:
+ return EFlag::SORT_FIELDS_BY_FIELD_NUMBER;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return EWrapperMessageFlag_Enum_Name(std::visit(TVisitor(), option));
+}
+
+TString OptionToOneofFlagName(TOneofOption option)
+{
+ using EFlag = EWrapperOneofFlag;
+ struct TVisitor
+ {
+ EFlag::Enum operator() (EProtobufOneofMode mode)
+ {
+ switch (mode) {
+ case EProtobufOneofMode::SeparateFields:
+ return EFlag::SEPARATE_FIELDS;
+ case EProtobufOneofMode::Variant:
+ return EFlag::VARIANT;
+ }
+ Y_FAIL();
+ }
+ };
+
+ return EWrapperOneofFlag_Enum_Name(std::visit(TVisitor(), option));
+}
+
+
+template <typename T, typename TOptionToFlagName>
+void SetOption(TMaybe<T>& option, T newOption, TOptionToFlagName optionToFlagName)
+{
+ if (option) {
+ if (*option == newOption) {
+ ythrow yexception() << "Duplicate protobuf flag " << optionToFlagName(newOption);
+ } else {
+ ythrow yexception() << "Incompatible protobuf flags " <<
+ optionToFlagName(*option) << " and " << optionToFlagName(newOption);
+ }
+ }
+ option = newOption;
+}
+
+class TParseProtobufFieldOptionsVisitor
+{
+public:
+ void operator() (ESpecialProtobufType type)
+ {
+ SetOption(Type, type);
+ }
+
+ void operator() (EProtobufSerializationMode serializationMode)
+ {
+ SetOption(SerializationMode, serializationMode);
+ }
+
+ void operator() (EProtobufListMode listMode)
+ {
+ SetOption(ListMode, listMode);
+ }
+
+ void operator() (EProtobufMapMode mapMode)
+ {
+ SetOption(MapMode, mapMode);
+ }
+
+ void operator() (EProtobufEnumWritingMode enumWritingMode)
+ {
+ SetOption(EnumWritingMode, enumWritingMode);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NFormats::SetOption(option, newOption, OptionToFieldFlagName);
+ }
+
+public:
+ TMaybe<ESpecialProtobufType> Type;
+ TMaybe<EProtobufSerializationMode> SerializationMode;
+ TMaybe<EProtobufListMode> ListMode;
+ TMaybe<EProtobufMapMode> MapMode;
+ TMaybe<EProtobufEnumWritingMode> EnumWritingMode;
+};
+
+class TParseProtobufMessageOptionsVisitor
+{
+public:
+ void operator() (EProtobufFieldSortOrder fieldSortOrder)
+ {
+ SetOption(FieldSortOrder, fieldSortOrder);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NFormats::SetOption(option, newOption, OptionToMessageFlagName);
+ }
+
+public:
+ TMaybe<EProtobufFieldSortOrder> FieldSortOrder;
+};
+
+class TParseProtobufOneofOptionsVisitor
+{
+public:
+ void operator() (EProtobufOneofMode mode)
+ {
+ SetOption(Mode, mode);
+ }
+
+ template <typename T>
+ void SetOption(TMaybe<T>& option, T newOption)
+ {
+ NYT::NFormats::SetOption(option, newOption, OptionToOneofFlagName);
+ }
+
+public:
+ TMaybe<EProtobufOneofMode> Mode;
+};
+
+void ParseProtobufFieldOptions(
+ const ::google::protobuf::RepeatedField<EWrapperFieldFlag::Enum>& flags,
+ TProtobufFieldOptions* fieldOptions)
+{
+ TParseProtobufFieldOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, FieldFlagToOption(flag));
+ }
+ if (visitor.Type) {
+ fieldOptions->Type = *visitor.Type;
+ }
+ if (visitor.SerializationMode) {
+ fieldOptions->SerializationMode = *visitor.SerializationMode;
+ }
+ if (visitor.ListMode) {
+ fieldOptions->ListMode = *visitor.ListMode;
+ }
+ if (visitor.MapMode) {
+ fieldOptions->MapMode = *visitor.MapMode;
+ }
+ if (visitor.EnumWritingMode) {
+ fieldOptions->EnumWritingMode = *visitor.EnumWritingMode;
+ }
+}
+
+void ParseProtobufMessageOptions(
+ const ::google::protobuf::RepeatedField<EWrapperMessageFlag::Enum>& flags,
+ TProtobufMessageOptions* messageOptions)
+{
+ TParseProtobufMessageOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, MessageFlagToOption(flag));
+ }
+ if (visitor.FieldSortOrder) {
+ messageOptions->FieldSortOrder = *visitor.FieldSortOrder;
+ }
+}
+
+void ParseProtobufOneofOptions(
+ const ::google::protobuf::RepeatedField<EWrapperOneofFlag::Enum>& flags,
+ TProtobufOneofOptions* messageOptions)
+{
+ TParseProtobufOneofOptionsVisitor visitor;
+ for (auto flag : flags) {
+ std::visit(visitor, OneofFlagToOption(flag));
+ }
+ if (visitor.Mode) {
+ messageOptions->Mode = *visitor.Mode;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtobufFieldOptions GetDefaultFieldOptions(
+ const Descriptor* descriptor,
+ TProtobufFieldOptions defaultFieldOptions)
+{
+ ParseProtobufFieldOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_field_flags),
+ &defaultFieldOptions);
+ ParseProtobufFieldOptions(
+ descriptor->options().GetRepeatedExtension(default_field_flags),
+ &defaultFieldOptions);
+ return defaultFieldOptions;
+}
+
+TProtobufOneofOptions GetDefaultOneofOptions(const Descriptor* descriptor)
+{
+ TProtobufOneofOptions defaultOneofOptions;
+ ParseProtobufOneofOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_oneof_flags),
+ &defaultOneofOptions);
+ ParseProtobufOneofOptions(
+ descriptor->options().GetRepeatedExtension(default_oneof_flags),
+ &defaultOneofOptions);
+ switch (defaultOneofOptions.Mode) {
+ case EProtobufOneofMode::Variant: {
+ auto defaultFieldOptions = GetDefaultFieldOptions(descriptor);
+ switch (defaultFieldOptions.SerializationMode) {
+ case EProtobufSerializationMode::Protobuf:
+ // For Protobuf serialization mode default is SeparateFields.
+ defaultOneofOptions.Mode = EProtobufOneofMode::SeparateFields;
+ return defaultOneofOptions;
+ case EProtobufSerializationMode::Yt:
+ case EProtobufSerializationMode::Embedded:
+ return defaultOneofOptions;
+ }
+ Y_FAIL();
+ }
+ case EProtobufOneofMode::SeparateFields:
+ return defaultOneofOptions;
+ }
+ Y_FAIL();
+}
+
+TProtobufFieldOptions GetFieldOptions(
+ const FieldDescriptor* fieldDescriptor,
+ const TMaybe<TProtobufFieldOptions>& defaultFieldOptions)
+{
+ TProtobufFieldOptions options;
+ if (defaultFieldOptions) {
+ options = *defaultFieldOptions;
+ } else {
+ options = GetDefaultFieldOptions(fieldDescriptor->containing_type());
+ }
+ ParseProtobufFieldOptions(fieldDescriptor->options().GetRepeatedExtension(flags), &options);
+ return options;
+}
+
+TProtobufOneofOptions GetOneofOptions(
+ const OneofDescriptor* oneofDescriptor,
+ const TMaybe<TProtobufOneofOptions>& defaultOneofOptions)
+{
+ TProtobufOneofOptions options;
+ if (defaultOneofOptions) {
+ options = *defaultOneofOptions;
+ } else {
+ options = GetDefaultOneofOptions(oneofDescriptor->containing_type());
+ }
+ ParseProtobufOneofOptions(oneofDescriptor->options().GetRepeatedExtension(oneof_flags), &options);
+
+ if (oneofDescriptor->is_synthetic()) {
+ options.Mode = EProtobufOneofMode::SeparateFields;
+ }
+
+ auto variantFieldName = oneofDescriptor->options().GetExtension(variant_field_name);
+ switch (options.Mode) {
+ case EProtobufOneofMode::SeparateFields:
+ if (variantFieldName) {
+ ythrow yexception() << "\"variant_field_name\" requires (NYT.oneof_flags) = VARIANT";
+ }
+ break;
+ case EProtobufOneofMode::Variant:
+ if (variantFieldName) {
+ options.VariantFieldName = variantFieldName;
+ } else {
+ options.VariantFieldName = oneofDescriptor->name();
+ }
+ break;
+ }
+ return options;
+}
+
+TProtobufMessageOptions GetMessageOptions(const Descriptor* descriptor)
+{
+ TProtobufMessageOptions options;
+ ParseProtobufMessageOptions(
+ descriptor->file()->options().GetRepeatedExtension(file_default_message_flags),
+ &options);
+ ParseProtobufMessageOptions(
+ descriptor->options().GetRepeatedExtension(message_flags),
+ &options);
+ return options;
+}
+
+TString GetColumnName(const FieldDescriptor* field)
+{
+ const auto& options = field->options();
+ const auto columnName = options.GetExtension(column_name);
+ if (!columnName.empty()) {
+ return columnName;
+ }
+ const auto keyColumnName = options.GetExtension(key_column_name);
+ if (!keyColumnName.empty()) {
+ return keyColumnName;
+ }
+ return field->name();
+}
+
+void ValidateProtobufType(const FieldDescriptor* fieldDescriptor, ESpecialProtobufType protobufType)
+{
+ const auto fieldType = fieldDescriptor->type();
+ auto ensureType = [&] (FieldDescriptor::Type expectedType) {
+ Y_ENSURE(fieldType == expectedType,
+ "Type of field " << fieldDescriptor->full_name() << "does not match specified field flag " <<
+ OptionToFieldFlagName(protobufType) << ": "
+ "expected " << FieldDescriptor::TypeName(expectedType) << ", " <<
+ "got " << FieldDescriptor::TypeName(fieldType));
+ };
+ switch (protobufType) {
+ case ESpecialProtobufType::Any:
+ ensureType(FieldDescriptor::TYPE_BYTES);
+ return;
+ case ESpecialProtobufType::OtherColumns:
+ ensureType(FieldDescriptor::TYPE_BYTES);
+ return;
+ case ESpecialProtobufType::EnumInt:
+ ensureType(FieldDescriptor::TYPE_ENUM);
+ return;
+ case ESpecialProtobufType::EnumString:
+ ensureType(FieldDescriptor::TYPE_ENUM);
+ return;
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf_options.h b/yt/yt/client/formats/protobuf_options.h
new file mode 100644
index 0000000000..03caaadaff
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_options.h
@@ -0,0 +1,99 @@
+#pragma once
+
+#include "config.h"
+
+#include <yt/yt_proto/yt/formats/extension.pb.h>
+
+#include <google/protobuf/message.h>
+
+#include <util/generic/maybe.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class ESpecialProtobufType
+{
+ EnumInt /* "enum_int" */,
+ EnumString /* "enum_string" */,
+ Any /* "any" */,
+ OtherColumns /* "other_columns" */,
+};
+
+enum class EProtobufSerializationMode
+{
+ Protobuf,
+ Yt,
+ Embedded,
+};
+
+enum class EProtobufListMode
+{
+ Optional,
+ Required,
+};
+
+enum class EProtobufMapMode
+{
+ ListOfStructsLegacy,
+ ListOfStructs,
+ Dict,
+ OptionalDict,
+};
+
+enum class EProtobufFieldSortOrder
+{
+ AsInProtoFile,
+ ByFieldNumber,
+};
+
+enum class EProtobufOneofMode
+{
+ SeparateFields,
+ Variant,
+};
+
+struct TProtobufOneofOptions
+{
+ EProtobufOneofMode Mode = EProtobufOneofMode::Variant;
+ TString VariantFieldName;
+};
+
+struct TProtobufFieldOptions
+{
+ TMaybe<ESpecialProtobufType> Type;
+ EProtobufSerializationMode SerializationMode = EProtobufSerializationMode::Protobuf;
+ EProtobufListMode ListMode = EProtobufListMode::Required;
+ EProtobufMapMode MapMode = EProtobufMapMode::ListOfStructsLegacy;
+ EProtobufEnumWritingMode EnumWritingMode = EProtobufEnumWritingMode::CheckValues;
+};
+
+struct TProtobufMessageOptions
+{
+ EProtobufFieldSortOrder FieldSortOrder = EProtobufFieldSortOrder::ByFieldNumber;
+};
+
+TProtobufFieldOptions GetDefaultFieldOptions(
+ const ::google::protobuf::Descriptor* descriptor,
+ TProtobufFieldOptions defaultFieldOptions = {});
+
+TProtobufOneofOptions GetDefaultOneofOptions(
+ const ::google::protobuf::Descriptor* descriptor);
+
+TProtobufFieldOptions GetFieldOptions(
+ const ::google::protobuf::FieldDescriptor* fieldDescriptor,
+ const TMaybe<TProtobufFieldOptions>& defaultFieldOptions = {});
+
+TProtobufOneofOptions GetOneofOptions(
+ const ::google::protobuf::OneofDescriptor* oneofDescriptor,
+ const TMaybe<TProtobufOneofOptions>& defaultOneofOptions = {});
+
+TString GetColumnName(const ::google::protobuf::FieldDescriptor* field);
+
+void ValidateProtobufType(
+ const ::google::protobuf::FieldDescriptor* fieldDescriptor,
+ ESpecialProtobufType protobufType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf_parser.cpp b/yt/yt/client/formats/protobuf_parser.cpp
new file mode 100644
index 0000000000..abaef0cb22
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_parser.cpp
@@ -0,0 +1,734 @@
+#include "protobuf_parser.h"
+
+#include "protobuf.h"
+#include "parser.h"
+#include "yson_map_to_unversioned_value.h"
+
+#include <yt/yt/client/table_client/helpers.h>
+#include <yt/yt/client/table_client/table_consumer.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/table_client/value_consumer.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <util/generic/buffer.h>
+#include <util/generic/scope.h>
+
+#include <util/string/escape.h>
+
+#include <google/protobuf/wire_format_lite.h>
+
+namespace NYT::NFormats {
+
+using namespace NYson;
+using namespace NTableClient;
+using namespace NComplexTypes;
+
+using ::google::protobuf::internal::WireFormatLite;
+
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRowParser
+{
+public:
+ explicit TRowParser(TStringBuf strbuf)
+ : Begin_(strbuf.data())
+ , End_(strbuf.data() + strbuf.size())
+ , Current_(strbuf.data())
+ { }
+
+ ui32 ReadVarUint32()
+ {
+ ui32 result;
+ Current_ += ::NYT::ReadVarUint32(Current_, End_, &result);
+ return result;
+ }
+
+ ui64 ReadVarUint64()
+ {
+ ui64 result;
+ Current_ += ::NYT::ReadVarUint64(Current_, End_, &result);
+ return result;
+ }
+
+ i64 ReadVarSint64()
+ {
+ i64 value;
+ Current_ += ReadVarInt64(Current_, End_, &value);
+ return value;
+ }
+
+ i32 ReadVarSint32()
+ {
+ i32 value;
+ Current_ += ReadVarInt32(Current_, End_, &value);
+ return value;
+ }
+
+ template <typename T>
+ T ReadFixed()
+ {
+ if (Current_ + sizeof(T) > End_) {
+ THROW_ERROR_EXCEPTION("Cannot read value of %v bytes, message exhausted",
+ sizeof(T));
+ }
+ T result = *reinterpret_cast<const T*>(Current_);
+ Current_ += sizeof(T);
+ return result;
+ }
+
+ TStringBuf ReadLengthDelimited()
+ {
+ ui32 length = ReadVarUint32();
+
+ ValidateLength(length);
+
+ auto result = TStringBuf(Current_, length);
+ Current_ += length;
+ return result;
+ }
+
+ void Skip(WireFormatLite::WireType wireType)
+ {
+ switch (wireType) {
+ case WireFormatLite::WIRETYPE_VARINT:
+ ReadVarUint64();
+ return;
+ case WireFormatLite::WIRETYPE_FIXED64:
+ ReadFixed<ui64>();
+ return;
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED:
+ ReadLengthDelimited();
+ return;
+ case WireFormatLite::WIRETYPE_START_GROUP:
+ case WireFormatLite::WIRETYPE_END_GROUP:
+ THROW_ERROR_EXCEPTION("Unexpected wire type %v", static_cast<int>(wireType));
+ case WireFormatLite::WIRETYPE_FIXED32:
+ ReadFixed<ui32>();
+ return;
+ }
+ YT_ABORT();
+ }
+
+ bool IsExhausted() const
+ {
+ return Current_ >= End_;
+ }
+
+ std::vector<TErrorAttribute> GetContextErrorAttributes() const
+ {
+ constexpr int contextMargin = 50;
+
+ auto contextBegin = Begin_ + contextMargin > Current_ ? Begin_ : Current_ - contextMargin;
+ auto contextEnd = Current_ + contextMargin > End_ ? End_ : Current_ + contextMargin;
+ size_t contextPos = Current_ - contextBegin;
+
+ TString contextString;
+ return {
+ TErrorAttribute("context", EscapeC(TStringBuf(contextBegin, contextEnd), contextString)),
+ TErrorAttribute("context_pos", contextPos)
+ };
+ }
+
+private:
+ void ValidateLength(ui32 length) const
+ {
+ if (Current_ + length > End_) {
+ THROW_ERROR_EXCEPTION("Broken protobuf message: field with length %v is out of message bounds",
+ length)
+ << GetContextErrorAttributes();
+ }
+ }
+
+private:
+ const char* Begin_;
+ const char* End_;
+ const char* Current_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// This struct can represent either
+// 1) a real column; |ChildIndex| means index in protobuf config; OR
+// 2) protobuf binary representation (inside EValueType::String)
+// for a structured message field;
+// in this case |ChildIndex| means the index of a field inside the message; OR
+// 3) unversioned-value or protobuf representation for a repeated field;
+// meaning of |ChildIndex| is as in (1) or (2).
+struct TField
+{
+ TUnversionedValue Value;
+ int ChildIndex;
+};
+
+class TCountingSorter
+{
+public:
+ // Sort a vector of |TField| by |ChildIndex| using counting sort.
+ // |ChildIndex| must be in range [0, |rangeSize|).
+ void Sort(std::vector<TField>* elements, int rangeSize)
+ {
+ if (elements->size() <= 1) {
+ return;
+ }
+ Counts_.assign(rangeSize, 0);
+ for (const auto& element : *elements) {
+ ++Counts_[element.ChildIndex];
+ }
+ for (int i = 1; i < static_cast<int>(Counts_.size()); ++i) {
+ Counts_[i] += Counts_[i - 1];
+ }
+ Result_.resize(elements->size());
+ for (auto it = elements->rbegin(); it != elements->rend(); ++it) {
+ auto childIndex = it->ChildIndex;
+ Result_[Counts_[childIndex] - 1] = std::move(*it);
+ --Counts_[childIndex];
+ }
+ std::swap(Result_, *elements);
+ }
+
+private:
+ std::vector<TField> Result_;
+ std::vector<int> Counts_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+int ComputeDepth(const TProtobufParserTypePtr& type)
+{
+ int depth = 0;
+ for (const auto& child : type->Children) {
+ depth = std::max(depth, ComputeDepth(child->Type) + 1);
+ }
+ return depth;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EProtobufParserState,
+ (InsideLength)
+ (InsideData)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufParser
+ : public IParser
+{
+public:
+ using EState = EProtobufParserState;
+
+ // NB(levysotsky): We expect the description to have only one table,
+ // the |tableIndex| parameter is for debugging purposes only.
+ TProtobufParser(
+ IValueConsumer* valueConsumer,
+ TProtobufParserFormatDescriptionPtr description,
+ int tableIndex,
+ const TYsonConverterConfig& config)
+ : ValueConsumer_(valueConsumer)
+ , Description_(std::move(description))
+ , TableIndex_(tableIndex)
+ , RootChildColumnIds_(Description_->CreateRootChildColumnIds(ValueConsumer_->GetNameTable()))
+ , RootChildOutputFlags_(RootChildColumnIds_.size()) //Description_->GetTableType()->Children.size())
+ // NB. We use ColumnConsumer_ to generate yson representation of complex types we don't want additional
+ // conversions so we use Positional mode.
+ // At the same time we use OtherColumnsConsumer_ to feed yson passed by users.
+ // This YSON should be in format specified on the format config.
+ , ColumnConsumer_(
+ TYsonConverterConfig{
+ .ComplexTypeMode = EComplexTypeMode::Positional,
+ },
+ valueConsumer)
+ , OtherColumnsConsumer_(config, valueConsumer)
+ {
+ FieldVectors_.resize(ComputeDepth(Description_->GetTableType()) + 1);
+ }
+
+ void Read(TStringBuf data) override
+ {
+ auto current = data.begin();
+ while (current != data.end()) {
+ current = Consume(current, data.end());
+ }
+ }
+
+ void Finish() override
+ {
+ if (State_ != EState::InsideLength || ExpectedBytes_ != sizeof(ui32)) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream");
+ }
+ }
+
+private:
+ const char* Consume(const char* begin, const char* end)
+ {
+ switch (State_) {
+ case EState::InsideLength:
+ return ConsumeLength(begin, end);
+ case EState::InsideData:
+ return ConsumeData(begin, end);
+ }
+ YT_ABORT();
+ }
+
+ const char* ConsumeInt32(const char* begin, const char* end)
+ {
+ const char* current = begin;
+ for (; ExpectedBytes_ != 0 && current != end; ++current, --ExpectedBytes_) {
+ Length_.Bytes[sizeof(ui32) - ExpectedBytes_] = *current;
+ }
+ return current;
+ }
+
+ const char* ConsumeLength(const char* begin, const char* end)
+ {
+ const char* next = ConsumeInt32(begin, end);
+ if (ExpectedBytes_ != 0) {
+ return next;
+ }
+
+ State_ = EState::InsideData;
+ ExpectedBytes_ = Length_.Value;
+
+ return ConsumeData(next, end);
+ }
+
+ const char* ConsumeData(const char* begin, const char* end)
+ {
+ const char* current = begin + ExpectedBytes_;
+ if (current > end) {
+ Data_.append(begin, end);
+ ExpectedBytes_ -= (end - begin);
+ return end;
+ }
+
+ if (Data_.empty()) {
+ OutputRow(TStringBuf(begin, current));
+ } else {
+ Data_.append(begin, current);
+ OutputRow(Data_);
+ }
+
+ State_ = EState::InsideLength;
+ ExpectedBytes_ = sizeof(ui32);
+ Data_.clear();
+ return current;
+ }
+
+ void OutputRow(TStringBuf buffer)
+ {
+ ValueConsumer_->OnBeginRow();
+ const auto& tableType = Description_->GetTableType();
+ RootChildOutputFlags_.assign(RootChildColumnIds_.size(), false);
+ ProcessStructuredMessage(buffer, tableType, /* depth */ 0);
+ ValueConsumer_->OnEndRow();
+ }
+
+ auto EnterChild(const TProtobufParserFieldDescription& child)
+ {
+ if (child.IsOneofAlternative()) {
+ Path_.push_back(child.ContainingOneof->Field);
+ }
+ Path_.push_back(&child);
+ return Finally([&] {
+ Path_.pop_back();
+ if (child.IsOneofAlternative()) {
+ Path_.pop_back();
+ }
+ });
+ }
+
+ void ProcessStructuredMessageEmbedded(TStringBuf buffer, const TProtobufParserTypePtr& type, int depth)
+ {
+ auto& fields = FieldVectors_[depth];
+
+ TRowParser rowParser(buffer);
+ try {
+ while (!rowParser.IsExhausted()) {
+ ui32 wireTag = rowParser.ReadVarUint32();
+ auto fieldNumber = WireFormatLite::GetTagFieldNumber(wireTag);
+ auto maybeChildIndex = type->FieldNumberToChildIndex(fieldNumber);
+ if (!maybeChildIndex) {
+ rowParser.Skip(WireFormatLite::GetTagWireType(wireTag));
+ continue;
+ }
+ auto childIndex = *maybeChildIndex;
+ const auto& childDescription = *type->Children[childIndex];
+ auto guard = EnterChild(childDescription);
+ if (Y_UNLIKELY(wireTag != childDescription.WireTag)) {
+ THROW_ERROR_EXCEPTION("Expected wire tag for field %Qv to be %v, got %v",
+ GetPathString(),
+ childDescription.WireTag,
+ wireTag)
+ << TErrorAttribute("field_number", fieldNumber);
+ }
+
+ int embeddedChildIndex = childIndex;
+ if (depth == 0 && childDescription.Type->ProtoType != EProtobufType::EmbeddedMessage) {
+ auto maybeEmbeddedChildIndex = type->FieldNumberToEmbeddedChildIndex(fieldNumber);
+ Y_VERIFY(maybeEmbeddedChildIndex);
+ embeddedChildIndex = *maybeEmbeddedChildIndex;
+ }
+
+ if (childDescription.Packed) {
+ auto elementsParser = TRowParser(rowParser.ReadLengthDelimited());
+ while (!elementsParser.IsExhausted()) {
+ ReadAndProcessUnversionedValue(elementsParser, embeddedChildIndex, childDescription, depth, &fields);
+ }
+ } else {
+ ReadAndProcessUnversionedValue(rowParser, embeddedChildIndex, childDescription, depth, &fields);
+ }
+ }
+ } catch (const std::exception& exception) {
+ THROW_ERROR_EXCEPTION(exception)
+ << TErrorAttribute("table_index", TableIndex_)
+ << rowParser.GetContextErrorAttributes();
+ }
+ }
+
+ void ProcessStructuredMessage(TStringBuf buffer, const TProtobufParserTypePtr& type, int depth)
+ {
+ auto& fields = FieldVectors_[depth];
+ fields.clear();
+
+ ProcessStructuredMessageEmbedded(buffer, type, depth);
+ int childrenCount = depth == 0 ? std::ssize(RootChildColumnIds_) : std::ssize(type->Children);
+ CountingSorter_.Sort(&fields, childrenCount);
+ OutputChildren(fields, type, depth);
+ }
+
+ Y_FORCE_INLINE void OutputChild(
+ std::vector<TField>::const_iterator begin,
+ std::vector<TField>::const_iterator end,
+ const TProtobufParserFieldDescription& childDescription,
+ int depth)
+ {
+ if (childDescription.Repeated) {
+ ColumnConsumer_.OnBeginList();
+ for (auto it = begin; it != end; ++it) {
+ ColumnConsumer_.OnListItem();
+ OutputValue(it->Value, childDescription, depth);
+ }
+ ColumnConsumer_.OnEndList();
+ } else {
+ if (Y_UNLIKELY(std::distance(begin, end) > 1)) {
+ THROW_ERROR_EXCEPTION("Error parsing protobuf: found %v entries for non-repeated field %Qv",
+ std::distance(begin, end),
+ GetPathString())
+ << TErrorAttribute("table_index", TableIndex_);
+ }
+ OutputValue(begin->Value, childDescription, depth);
+ }
+ }
+
+ void OutputChildren(
+ const std::vector<TField>& fields,
+ const TProtobufParserTypePtr& type,
+ int depth)
+ {
+ const auto inRoot = (depth == 0);
+
+ auto skipElements = [&](int count) {
+ if (inRoot) {
+ return;
+ }
+ YT_VERIFY(count >= 0);
+ for (int i = 0; i < count; ++i) {
+ ColumnConsumer_.OnEntity();
+ }
+ };
+
+ auto fieldIt = fields.cbegin();
+ int childrenCount = inRoot ? std::ssize(RootChildColumnIds_) : std::ssize(type->Children);
+ auto lastOutputStructFieldIndex = -1;
+ for (int childIndex = 0; childIndex < childrenCount; ++childIndex) {
+ const auto& childDescription = inRoot ? *RootChildColumnIds_[childIndex].second : *type->Children[childIndex];
+ auto guard = EnterChild(childDescription);
+
+ auto fieldRangeBegin = fieldIt;
+ while (fieldIt != fields.cend() && fieldIt->ChildIndex == childIndex) {
+ ++fieldIt;
+ }
+ int structFieldIndex = childDescription.StructFieldIndex;
+ if (fieldRangeBegin != fieldIt || (childDescription.Repeated && !childDescription.Type->Optional)) {
+ if (Y_UNLIKELY(
+ childDescription.IsOneofAlternative() &&
+ lastOutputStructFieldIndex == structFieldIndex))
+ {
+ const auto* oneof = childDescription.ContainingOneof;
+ YT_VERIFY(oneof);
+ THROW_ERROR_EXCEPTION(
+ "Error parsing protobuf: multiple entries for oneof field %Qv; the second one is %Qv",
+ GetPathString(/* offset */ 1),
+ GetPathString())
+ << TErrorAttribute("table_index", TableIndex_);
+ }
+ skipElements(structFieldIndex - lastOutputStructFieldIndex - 1);
+ if (inRoot) {
+ ColumnConsumer_.SetColumnIndex(RootChildColumnIds_[childIndex].first);
+ RootChildOutputFlags_[childIndex] = true;
+ }
+ OutputChild(fieldRangeBegin, fieldIt, childDescription, depth);
+ lastOutputStructFieldIndex = structFieldIndex;
+ } else {
+ auto isStructFieldPresentOrLegallyMissing = [&] () {
+ if (ShouldOutputValueImmediately(inRoot, childDescription)) {
+ if (RootChildOutputFlags_[childIndex]) {
+ // The value is already output.
+ return true;
+ }
+ }
+ if (childDescription.Type->ProtoType == EProtobufType::EmbeddedMessage) {
+ // The value is already parsed and processed
+ return true;
+ }
+ if (!childDescription.IsOneofAlternative()) {
+ return childDescription.Type->Optional;
+ }
+ if (childDescription.ContainingOneof->Optional) {
+ return true;
+ }
+ if (lastOutputStructFieldIndex == structFieldIndex) {
+ // It is not missing.
+ return true;
+ }
+ if (childIndex + 1 == (inRoot ? std::ssize(RootChildColumnIds_) : std::ssize(type->Children))) {
+ return false;
+ }
+ const auto& nextChildDescription = inRoot ? *RootChildColumnIds_[childIndex + 1].second : *type->Children[childIndex + 1];
+ // The current alternative is missing, but the next one corresponds to the same field,
+ // so the check is deferred to the next alternative.
+ return structFieldIndex == nextChildDescription.StructFieldIndex;
+ };
+ if (Y_UNLIKELY(!isStructFieldPresentOrLegallyMissing())) {
+ int offset = 0;
+ if (childDescription.IsOneofAlternative()) {
+ offset = 1;
+ }
+ THROW_ERROR_EXCEPTION("Error parsing protobuf: required field %Qv is missing",
+ GetPathString(offset));
+ }
+ }
+ }
+ skipElements(type->StructFieldCount - lastOutputStructFieldIndex - 1);
+ }
+
+ Y_FORCE_INLINE void OutputValue(
+ TUnversionedValue value,
+ const TProtobufParserFieldDescription& description,
+ int depth)
+ {
+ const auto inRoot = (depth == 0);
+
+ if (description.IsOneofAlternative()) {
+ ColumnConsumer_.OnBeginList();
+ ColumnConsumer_.OnListItem();
+ ColumnConsumer_.OnInt64Scalar(*description.AlternativeIndex);
+ ColumnConsumer_.OnListItem();
+ }
+ switch (description.Type->ProtoType) {
+ case EProtobufType::StructuredMessage:
+ YT_VERIFY(value.Type == EValueType::String);
+ ColumnConsumer_.OnBeginList();
+ ProcessStructuredMessage(value.AsStringBuf(), description.Type, depth + 1);
+ ColumnConsumer_.OnEndList();
+ break;
+ case EProtobufType::OtherColumns:
+ UnversionedValueToYson(value, &OtherColumnsConsumer_);
+ break;
+ case EProtobufType::Any:
+ UnversionedValueToYson(value, &ColumnConsumer_);
+ break;
+ default:
+ if (ShouldOutputValueImmediately(inRoot, description)) {
+ ValueConsumer_->OnValue(value);
+ } else {
+ UnversionedValueToYson(value, &ColumnConsumer_);
+ }
+ break;
+ }
+ if (description.IsOneofAlternative()) {
+ ColumnConsumer_.OnEndList();
+ }
+ }
+
+ // Reads unversioned value depending on the field type.
+ // If |depth == 0| and the field is not repeated we process it according to type.
+ // Otherwise, we append it to |fields| vector.
+ Y_FORCE_INLINE void ReadAndProcessUnversionedValue(
+ TRowParser& rowParser,
+ int childIndex,
+ const TProtobufParserFieldDescription& description,
+ int depth,
+ std::vector<TField>* fields)
+ {
+ if (description.Type->ProtoType == EProtobufType::EmbeddedMessage) {
+ ProcessStructuredMessageEmbedded(rowParser.ReadLengthDelimited(), description.Type, depth); // NOT depth + 1
+ return;
+ }
+
+ const auto inRoot = (depth == 0);
+ const auto id = inRoot ? RootChildColumnIds_[childIndex].first : static_cast<ui16>(0);
+ auto value = [&] {
+ switch (description.Type->ProtoType) {
+ case EProtobufType::StructuredMessage:
+ return MakeUnversionedStringValue(rowParser.ReadLengthDelimited(), id);
+ case EProtobufType::OtherColumns:
+ return MakeUnversionedAnyValue(rowParser.ReadLengthDelimited(), id);
+ case EProtobufType::Any:
+ return MakeUnversionedAnyValue(rowParser.ReadLengthDelimited(), id);
+ case EProtobufType::String:
+ case EProtobufType::Message:
+ case EProtobufType::Bytes:
+ return MakeUnversionedStringValue(rowParser.ReadLengthDelimited(), id);
+ case EProtobufType::Uint64:
+ return MakeUnversionedUint64Value(rowParser.ReadVarUint64(), id);
+ case EProtobufType::Uint32:
+ return MakeUnversionedUint64Value(rowParser.ReadVarUint32(), id);
+ case EProtobufType::Int64:
+ // Value is *not* zigzag encoded, so we use Uint64 intentionally.
+ return MakeUnversionedInt64Value(static_cast<i64>(rowParser.ReadVarUint64()), id);
+ case EProtobufType::EnumInt:
+ case EProtobufType::Int32:
+ // Value is *not* zigzag encoded, so we use Uint64 intentionally.
+ return MakeUnversionedInt64Value(static_cast<i64>(rowParser.ReadVarUint64()), id);
+ case EProtobufType::Sint64:
+ return MakeUnversionedInt64Value(rowParser.ReadVarSint64(), id);
+ case EProtobufType::Sint32:
+ return MakeUnversionedInt64Value(rowParser.ReadVarSint32(), id);
+ case EProtobufType::Fixed64:
+ return MakeUnversionedUint64Value(rowParser.ReadFixed<ui64>(), id);
+ case EProtobufType::Fixed32:
+ return MakeUnversionedUint64Value(rowParser.ReadFixed<ui32>(), id);
+ case EProtobufType::Sfixed64:
+ return MakeUnversionedInt64Value(rowParser.ReadFixed<i64>(), id);
+ case EProtobufType::Sfixed32:
+ return MakeUnversionedInt64Value(rowParser.ReadFixed<i32>(), id);
+ case EProtobufType::Double:
+ return MakeUnversionedDoubleValue(rowParser.ReadFixed<double>(), id);
+ case EProtobufType::Float:
+ return MakeUnversionedDoubleValue(rowParser.ReadFixed<float>(), id);
+ case EProtobufType::Bool:
+ return MakeUnversionedBooleanValue(static_cast<bool>(rowParser.ReadVarUint64()), id);
+ case EProtobufType::EnumString: {
+ auto enumValue = static_cast<i32>(rowParser.ReadVarUint64());
+ YT_VERIFY(description.Type->EnumerationDescription);
+ const auto& enumString = description.Type->EnumerationDescription->GetValueName(enumValue);
+ return MakeUnversionedStringValue(enumString, id);
+ }
+ case EProtobufType::Oneof:
+ THROW_ERROR_EXCEPTION("Oneof inside oneof is not supported in protobuf format; offending field %Qv",
+ GetPathString())
+ << TErrorAttribute("table_index", TableIndex_);
+ case EProtobufType::EmbeddedMessage:
+ Y_FAIL();
+ }
+ YT_ABORT();
+ }();
+ if (ShouldOutputValueImmediately(inRoot, description)) {
+ ColumnConsumer_.SetColumnIndex(id);
+ RootChildOutputFlags_[childIndex] = true;
+ OutputValue(value, description, depth);
+ } else {
+ fields->push_back({value, childIndex});
+ }
+ }
+
+ Y_FORCE_INLINE static bool ShouldOutputValueImmediately(bool inRoot, const TProtobufParserFieldDescription& description)
+ {
+ return inRoot && !description.Repeated && !description.IsOneofAlternative();
+ }
+
+ TString GetPathString(int offset = 0)
+ {
+ TStringStream stream;
+ stream << "<root>";
+ YT_VERIFY(std::ssize(Path_) >= offset);
+ for (int i = 0; i < std::ssize(Path_) - offset; ++i) {
+ stream << '.' << Path_[i]->Name;
+ }
+ return stream.Str();
+ }
+
+private:
+ IValueConsumer* const ValueConsumer_;
+
+ TProtobufParserFormatDescriptionPtr Description_;
+ int TableIndex_;
+
+ std::vector<std::pair<ui16, TProtobufParserFieldDescription*>> RootChildColumnIds_;
+ std::vector<bool> RootChildOutputFlags_;
+
+ TYsonToUnversionedValueConverter ColumnConsumer_;
+ TYsonMapToUnversionedValueConverter OtherColumnsConsumer_;
+
+ std::vector<const TProtobufParserFieldDescription*> Path_;
+
+ std::vector<std::vector<TField>> FieldVectors_;
+ TCountingSorter CountingSorter_;
+
+ EState State_ = EState::InsideLength;
+ union
+ {
+ ui32 Value;
+ char Bytes[sizeof(ui32)];
+ } Length_;
+ ui32 ExpectedBytes_ = sizeof(ui32);
+
+ TString Data_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForProtobuf(
+ IValueConsumer* consumer,
+ TProtobufFormatConfigPtr config,
+ int tableIndex)
+{
+ if (!config->Tables.empty()) {
+ // Retain only one table config, as we have only one schema here.
+ config = NYTree::CloneYsonStruct(config);
+ if (tableIndex >= std::ssize(config->Tables)) {
+ THROW_ERROR_EXCEPTION("Protobuf format does not have table with index %v",
+ tableIndex);
+ }
+ config->Tables = {config->Tables[tableIndex]};
+ } else if (!config->TypeNames.empty()) {
+ // Retain only one type name, as we have only one schema here.
+ config = NYTree::CloneYsonStruct(config);
+ if (tableIndex >= std::ssize(config->TypeNames)) {
+ THROW_ERROR_EXCEPTION("Protobuf format does not have table with index %v",
+ tableIndex);
+ }
+ config->TypeNames = {config->TypeNames[tableIndex]};
+ }
+ auto formatDescription = New<TProtobufParserFormatDescription>();
+ formatDescription->Init(config, {consumer->GetSchema()});
+ TYsonConverterConfig ysonConfig{
+ .ComplexTypeMode = config->ComplexTypeMode,
+ .DecimalMode = config->DecimalMode,
+ .TimeMode = config->TimeMode,
+ .UuidMode = config->UuidMode,
+ };
+ return std::make_unique<TProtobufParser>(
+ consumer,
+ formatDescription,
+ tableIndex,
+ ysonConfig);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/protobuf_parser.h b/yt/yt/client/formats/protobuf_parser.h
new file mode 100644
index 0000000000..1ac356069f
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_parser.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForProtobuf(
+ NTableClient::IValueConsumer* consumer,
+ TProtobufFormatConfigPtr config,
+ int tableIndex);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf_writer.cpp b/yt/yt/client/formats/protobuf_writer.cpp
new file mode 100644
index 0000000000..f4321cd68a
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_writer.cpp
@@ -0,0 +1,1078 @@
+#include "protobuf_writer.h"
+
+#include "lenval_control_constants.h"
+#include "protobuf.h"
+#include "schemaless_writer_adapter.h"
+#include "unversioned_value_yson_writer.h"
+
+#include <yt/yt/client/table_client/helpers.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/misc/zerocopy_output_writer.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/stream/buffer.h>
+#include <util/stream/mem.h>
+
+#include <google/protobuf/wire_format_lite.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NComplexTypes;
+
+using ::google::protobuf::internal::WireFormatLite;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZeroCopyWriterWithGapsBase
+{
+public:
+ TZeroCopyWriterWithGapsBase(TBlob& blob)
+ : Blob_(blob)
+ , InitialSize_(blob.Size())
+ { }
+
+protected:
+ TBlob& Blob_;
+ ui64 InitialSize_;
+};
+
+// Same as `TZeroCopyOutputStreamWriter` but also allows leaving small "gaps"
+// of fixed size in the output blob to be filled afterwards.
+//
+// Example usage:
+// ```
+// auto gap = writer->CreateGap(sizeof(ui64));
+// auto writtenSizeBefore = writer->GetTotalWrittenSize();
+// ... // Write something to the writer.
+// auto writtenSizeAfter = writer->GetTotalWrittenSize();
+// ui64 size = writtenSizeAfter - writtenSizeBefore;
+// memcpy(writer->GetGapPointer(gap), &size, sizeof(size));
+// ```
+class TZeroCopyWriterWithGaps
+ : public TZeroCopyWriterWithGapsBase
+ , public TZeroCopyOutputStreamWriter
+{
+public:
+ static constexpr ui64 MaxGapSize = 16;
+
+ using TGapPosition = ui64;
+
+ // NOTE: We need base class to initialize `InitialSize_` before `TZeroCopyOutputStreamWriter`.
+ TZeroCopyWriterWithGaps(TBlobOutput* blobOutput)
+ : TZeroCopyWriterWithGapsBase(blobOutput->Blob())
+ , TZeroCopyOutputStreamWriter(blobOutput)
+ { }
+
+ TGapPosition CreateGap(ui64 size)
+ {
+ auto position = InitialSize_ + GetTotalWrittenSize();
+ if (size <= RemainingBytes()) {
+ Advance(size);
+ } else {
+ char Buffer[MaxGapSize];
+ YT_VERIFY(size <= MaxGapSize);
+ Write(Buffer, size);
+ }
+ return position;
+ }
+
+ char* GetGapPointer(TGapPosition gap)
+ {
+ return Blob_.Begin() + gap;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This class is responsible for writing "other columns" field in protobuf format.
+//
+// |OnBeginRow|, |OnValue|, |OnEndRow|, |GetProtobufSize| and |WriteProtoField|
+// methods can be called even if there is no "other columns" field in current table descriptor,
+// in which case they will be effectively no-op.
+class TOtherColumnsWriter
+{
+public:
+ TOtherColumnsWriter(
+ const std::vector<TTableSchemaPtr>& schemas,
+ const TNameTablePtr& nameTable,
+ const TProtobufWriterFormatDescriptionPtr& description,
+ const TYsonConverterConfig& config)
+ : NameTableReader_(nameTable)
+ , Description_(description)
+ , TableIndexToConverter_(description->GetTableCount())
+ , Writer_(
+ &OutputStream_,
+ EYsonFormat::Binary,
+ EYsonType::Node,
+ /* enableRaw */ true)
+ {
+ for (int tableIndex = 0; tableIndex < description->GetTableCount(); ++tableIndex) {
+ if (auto fieldDescription = description->FindOtherColumnsField(tableIndex)) {
+ TableIndexToConverter_[tableIndex].emplace(
+ nameTable,
+ schemas[tableIndex],
+ config);
+ }
+ }
+
+ try {
+ RowIndexColumnId_ = nameTable->GetIdOrRegisterName(RowIndexColumnName);
+ RangeIndexColumnId_ = nameTable->GetIdOrRegisterName(RangeIndexColumnName);
+ TableIndexColumnId_ = nameTable->GetIdOrRegisterName(TableIndexColumnName);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to add system columns to name table for protobuf writer")
+ << ex;
+ }
+ }
+
+ void SetTableIndex(i64 tableIndex)
+ {
+ YT_VERIFY(!InsideRow_);
+ FieldDescription_ = Description_->FindOtherColumnsField(tableIndex);
+ if (FieldDescription_) {
+ Converter_ = &*TableIndexToConverter_[tableIndex];
+ } else {
+ Converter_ = nullptr;
+ }
+ }
+
+ bool IsEnabled() const
+ {
+ return FieldDescription_ != nullptr;
+ }
+
+ void OnBeginRow()
+ {
+ if (!IsEnabled()) {
+ return;
+ }
+
+ YT_VERIFY(!InsideRow_);
+ OutputStream_.Clear();
+ Writer_.OnBeginMap();
+ InsideRow_ = true;
+ }
+
+ void OnValue(TUnversionedValue value)
+ {
+ if (!IsEnabled()) {
+ return;
+ }
+
+ if (IsSystemColumnId(value.Id)) {
+ return;
+ }
+
+ YT_VERIFY(InsideRow_);
+ Writer_.OnKeyedItem(NameTableReader_.GetName(value.Id));
+ Converter_->WriteValue(value, &Writer_);
+ }
+
+ void OnEndRow()
+ {
+ if (!IsEnabled()) {
+ return;
+ }
+
+ YT_VERIFY(InsideRow_);
+ Writer_.OnEndMap();
+ InsideRow_ = false;
+ }
+
+ i64 GetProtobufSize() const
+ {
+ if (!IsEnabled()) {
+ return 0;
+ }
+
+ YT_VERIFY(!InsideRow_);
+ auto length = GetYsonString().size();
+ return FieldDescription_->TagSize + WireFormatLite::UInt32Size(length) + length;
+ }
+
+ void WriteProtoField(TZeroCopyOutputStreamWriter* writer) const
+ {
+ if (!IsEnabled()) {
+ return;
+ }
+
+ YT_VERIFY(!InsideRow_);
+ WriteVarUint32(writer, FieldDescription_->WireTag);
+ auto buffer = GetYsonString();
+ WriteVarUint32(writer, buffer.size());
+ writer->Write(buffer.begin(), buffer.size());
+ }
+
+private:
+ TStringBuf GetYsonString() const
+ {
+ return OutputStream_.Blob().ToStringBuf();
+ }
+
+ bool IsSystemColumnId(int id) const
+ {
+ return
+ TableIndexColumnId_ == id ||
+ RangeIndexColumnId_ == id ||
+ RowIndexColumnId_ == id;
+ }
+
+private:
+ const TNameTableReader NameTableReader_;
+
+ TProtobufWriterFormatDescriptionPtr Description_;
+
+ const TProtobufWriterFieldDescription* FieldDescription_ = nullptr;
+ std::vector<std::optional<TUnversionedValueYsonWriter>> TableIndexToConverter_;
+ TUnversionedValueYsonWriter* Converter_ = nullptr;
+
+ TBlobOutput OutputStream_;
+ TYsonWriter Writer_;
+
+ bool InsideRow_ = false;
+
+ int RowIndexColumnId_ = -1;
+ int RangeIndexColumnId_ = -1;
+ int TableIndexColumnId_ = -1;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEnumVisitor
+{
+public:
+ Y_FORCE_INLINE void OnInt64(i64 value)
+ {
+ InRange = TryIntegralCast<i32>(value, &EnumValue);
+ }
+
+ Y_FORCE_INLINE void OnUint64(ui64 value)
+ {
+ InRange = TryIntegralCast<i32>(value, &EnumValue);
+ }
+
+ Y_FORCE_INLINE void OnString(TStringBuf value, const TProtobufWriterTypePtr& type)
+ {
+ if (Y_UNLIKELY(!type->EnumerationDescription)) {
+ THROW_ERROR_EXCEPTION("Enumeration description not found");
+ }
+ if (SuppressUnknownValueError) {
+ if (auto enumValue = type->EnumerationDescription->TryGetValue(value)) {
+ EnumValue = *enumValue;
+ } else {
+ InRange = false;
+ }
+ } else {
+ EnumValue = type->EnumerationDescription->GetValue(value);
+ }
+ }
+
+public:
+ bool SuppressUnknownValueError = false;
+ bool InRange = true;
+ i32 EnumValue;
+};
+
+template <typename TValueExtractor>
+Y_FORCE_INLINE void WriteProtobufEnum(
+ TZeroCopyOutputStreamWriter* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ const TValueExtractor& extractor)
+{
+ const auto& type = fieldDescription.Type;
+
+ TEnumVisitor visitor;
+
+ if ((fieldDescription.Repeated || type->Optional) &&
+ fieldDescription.EnumWritingMode == EProtobufEnumWritingMode::SkipUnknownValues) {
+ visitor.SuppressUnknownValueError = true;
+ }
+
+ extractor.ExtractEnum(&visitor, type);
+
+ auto getEnumerationName = [&] () {
+ return type->EnumerationDescription
+ ? type->EnumerationDescription->GetEnumerationName()
+ : "<unknown>";
+ };
+
+ if (Y_UNLIKELY(!visitor.InRange)) {
+ if (visitor.SuppressUnknownValueError) {
+ return;
+ } else {
+ THROW_ERROR_EXCEPTION("Value out of range for protobuf enumeration %Qv",
+ getEnumerationName());
+ }
+ }
+ if (!fieldDescription.Packed) {
+ WriteVarUint32(writer, fieldDescription.WireTag);
+ }
+ WriteVarUint64(writer, static_cast<ui64>(visitor.EnumValue)); // No zigzag int32.
+}
+
+template <typename TValueExtractor>
+Y_FORCE_INLINE void WriteProtobufField(
+ TZeroCopyOutputStreamWriter* writer,
+ const TProtobufWriterTypePtr& type,
+ const TValueExtractor& extractor)
+{
+ switch (type->ProtoType) {
+ case EProtobufType::String:
+ case EProtobufType::Bytes:
+ case EProtobufType::Message: {
+ auto stringBuf = extractor.ExtractString();
+ WriteVarUint32(writer, stringBuf.size());
+ writer->Write(stringBuf.data(), stringBuf.size());
+ return;
+ }
+ case EProtobufType::Uint64:
+ WriteVarUint64(writer, extractor.ExtractUint64());
+ return;
+ case EProtobufType::Uint32:
+ WriteVarUint32(writer, extractor.ExtractUint64());
+ return;
+ case EProtobufType::Int64:
+ WriteVarUint64(writer, extractor.ExtractInt64()); // no zigzag
+ return;
+ case EProtobufType::Int32:
+ WriteVarUint64(writer, extractor.ExtractInt64()); // no zigzag
+ return;
+ case EProtobufType::Sint64:
+ WriteVarInt64(writer, extractor.ExtractInt64()); // zigzag
+ return;
+ case EProtobufType::Sint32:
+ WriteVarInt32(writer, extractor.ExtractInt64()); // zigzag
+ return;
+ case EProtobufType::Fixed64:
+ WritePod(*writer, extractor.ExtractUint64());
+ return;
+ case EProtobufType::Fixed32:
+ WritePod(*writer, static_cast<ui32>(extractor.ExtractUint64()));
+ return;
+ case EProtobufType::Sfixed64:
+ WritePod(*writer, extractor.ExtractInt64());
+ return;
+ case EProtobufType::Sfixed32:
+ WritePod(*writer, static_cast<i32>(extractor.ExtractInt64()));
+ return;
+ case EProtobufType::Double:
+ WritePod(*writer, extractor.ExtractDouble());
+ return;
+ case EProtobufType::Float:
+ WritePod(*writer, static_cast<float>(extractor.ExtractDouble()));
+ return;
+ case EProtobufType::Bool:
+ WritePod(*writer, static_cast<ui8>(extractor.ExtractBoolean()));
+ return;
+ case EProtobufType::EnumInt:
+ case EProtobufType::EnumString:
+ // WriteProtobufEnum must be used for writing enums
+ YT_ABORT();
+ case EProtobufType::Any:
+ case EProtobufType::OtherColumns:
+ case EProtobufType::StructuredMessage:
+ case EProtobufType::EmbeddedMessage:
+ case EProtobufType::Oneof:
+ THROW_ERROR_EXCEPTION("Wrong protobuf type %Qlv",
+ type->ProtoType);
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateYsonCursorType(const TYsonPullParserCursor* cursor, EYsonItemType expected)
+{
+ auto actual = cursor->GetCurrent().GetType();
+ if (Y_UNLIKELY(actual != expected)) {
+ THROW_ERROR_EXCEPTION("Protobuf writing error: bad YSON item, expected %Qlv, actual %Qlv",
+ expected,
+ actual);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonValueExtractor
+{
+public:
+ explicit TYsonValueExtractor(TYsonPullParser* parser)
+ : Parser_(parser)
+ { }
+
+ void ExtractEnum(TEnumVisitor* visitor, const TProtobufWriterTypePtr& type) const
+ {
+ auto item = Parser_->Next();
+ switch (item.GetType()) {
+ case EYsonItemType::Int64Value:
+ visitor->OnInt64(item.UncheckedAsInt64());
+ return;
+ case EYsonItemType::Uint64Value:
+ visitor->OnUint64(item.UncheckedAsUint64());
+ return;
+ case EYsonItemType::StringValue:
+ visitor->OnString(item.UncheckedAsString(), type);
+ return;
+ default:
+ const auto* enumDescription = type->EnumerationDescription;
+ THROW_ERROR_EXCEPTION("Cannot parse protobuf enumeration %Qv from YSON value of type %Qlv",
+ enumDescription ? enumDescription->GetEnumerationName() : "<unknown>",
+ item.GetType());
+ }
+ }
+
+ i64 ExtractInt64() const
+ {
+ return Parser_->ParseInt64();
+ }
+
+ ui64 ExtractUint64() const
+ {
+ return Parser_->ParseUint64();
+ }
+
+ TStringBuf ExtractString() const
+ {
+ return Parser_->ParseString();
+ }
+
+ bool ExtractBoolean() const
+ {
+ return Parser_->ParseBoolean();
+ }
+
+ double ExtractDouble() const
+ {
+ return Parser_->ParseDouble();
+ }
+
+private:
+ TYsonPullParser* const Parser_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateUnversionedValueType(const TUnversionedValue& value, EValueType type)
+{
+ if (Y_UNLIKELY(value.Type != type)) {
+ THROW_ERROR_EXCEPTION("Invalid protobuf storage type: expected %Qlv, got %Qlv",
+ type,
+ value.Type);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This class actually doesn't change the `TUnversionedValue` passed to it,
+// but is named consistently with more tricky `TYsonValueExtractor`.
+class TUnversionedValueExtractor
+{
+public:
+ explicit TUnversionedValueExtractor(TUnversionedValue value)
+ : Value_(value)
+ { }
+
+ void ExtractEnum(TEnumVisitor* visitor, const TProtobufWriterTypePtr& type) const
+ {
+ switch (Value_.Type) {
+ case EValueType::Int64:
+ visitor->OnInt64(Value_.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ visitor->OnUint64(Value_.Data.Uint64);
+ return;
+ case EValueType::String:
+ visitor->OnString(Value_.AsStringBuf(), type);
+ return;
+ default:
+ const auto* enumDescription = type->EnumerationDescription;
+ THROW_ERROR_EXCEPTION("Cannot parse protobuf enumeration %Qv from unverioned value of type %Qlv",
+ enumDescription ? enumDescription->GetEnumerationName() : "<unknown>",
+ Value_.Type);
+ }
+ }
+
+ i64 ExtractInt64() const
+ {
+ ValidateUnversionedValueType(Value_, EValueType::Int64);
+ return Value_.Data.Int64;
+ }
+
+ ui64 ExtractUint64() const
+ {
+ ValidateUnversionedValueType(Value_, EValueType::Uint64);
+ return Value_.Data.Uint64;
+ }
+
+ TStringBuf ExtractString() const
+ {
+ ValidateUnversionedValueType(Value_, EValueType::String);
+ return {Value_.Data.String, Value_.Length};
+ }
+
+ bool ExtractBoolean() const
+ {
+ ValidateUnversionedValueType(Value_, EValueType::Boolean);
+ return Value_.Data.Boolean;
+ }
+
+ double ExtractDouble() const
+ {
+ ValidateUnversionedValueType(Value_, EValueType::Double);
+ return Value_.Data.Double;
+ }
+
+private:
+ const TUnversionedValue Value_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Write varint representation occupying exactly `size` bytes.
+// If `value` is too small, `0x80` bytes will be added in due amount.
+int WriteVarUint64WithPadding(char* output, ui64 value, int size)
+{
+ for (int i = 0; i < size - 1; ++i) {
+ *output++ = static_cast<ui8>(value | 0x80);
+ value >>= 7;
+ }
+ *output++ = static_cast<ui8>(value);
+ YT_VERIFY(value < 0x80);
+ return size;
+}
+
+static bool MatchesCompositeType(const TProtobufWriterFieldDescription& field)
+{
+ return field.Repeated ||
+ field.Type->ProtoType == EProtobufType::StructuredMessage ||
+ field.Type->ProtoType == EProtobufType::Oneof;
+}
+
+static bool MatchesEnumerateType(const TProtobufWriterFieldDescription& field)
+{
+ return field.Type->ProtoType == EProtobufType::EnumInt ||
+ field.Type->ProtoType == EProtobufType::EnumString;
+}
+
+class TWriterImpl
+{
+private:
+ using TMessageSize = ui32;
+ std::vector<TBlobOutput> EmbeddedBuffers;
+ TProtobufWriterFormatDescriptionPtr FormatDescription;
+
+public:
+ TWriterImpl(
+ const std::vector<TTableSchemaPtr>& schemas,
+ const TNameTablePtr& nameTable,
+ const TProtobufWriterFormatDescriptionPtr& description,
+ const TYsonConverterConfig& config)
+ : EmbeddedBuffers(description->GetTableDescription(0).Embeddings.size())
+ , FormatDescription(description)
+ , OtherColumnsWriter_(schemas, nameTable, description, config)
+ { }
+
+ void SetTableIndex(i64 tableIndex)
+ {
+ OtherColumnsWriter_.SetTableIndex(tableIndex);
+ }
+
+ Y_FORCE_INLINE void OnBeginRow(TZeroCopyWriterWithGaps* writer)
+ {
+ Writer_ = writer;
+ MessageSizeGapPosition_ = writer->CreateGap(sizeof(TMessageSize));
+ OtherColumnsWriter_.OnBeginRow();
+ TotalWrittenSizeBefore_ = writer->GetTotalWrittenSize();
+ }
+
+ // It's quite likely that this value can be bounded by `WireFormatLite::UInt64Size(10 * ysonLength)`,
+ // but currently we return just maximum size of a varint representation of a 32-bit number.
+ static Y_FORCE_INLINE int GetMaxVarIntSizeOfProtobufSizeOfComplexType()
+ {
+ return MaxVarUint32Size;
+ }
+
+ static Y_FORCE_INLINE ui64 GetMaxBinaryYsonSize(TUnversionedValue value)
+ {
+ switch (value.Type) {
+ case EValueType::Uint64:
+ return 1 + MaxVarUint64Size;
+ case EValueType::Int64:
+ return 1 + MaxVarInt64Size;
+ case EValueType::Double:
+ return 1 + sizeof(double);
+ case EValueType::Boolean:
+ return 1;
+ case EValueType::String:
+ return 1 + MaxVarInt32Size + value.Length;
+ case EValueType::Any:
+ case EValueType::Composite:
+ return value.Length;
+ case EValueType::Null:
+ return 1;
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+ }
+ ThrowUnexpectedValueType(value.Type);
+ }
+
+ Y_FORCE_INLINE void OnValue(TUnversionedValue value, const TProtobufWriterFieldDescription& fieldDescription)
+ {
+ if (fieldDescription.ParentEmbeddingIndex != TProtobufWriterEmbeddingDescription::InvalidIndex) {
+ TZeroCopyWriterWithGaps writer(&EmbeddedBuffers[fieldDescription.ParentEmbeddingIndex]);
+ DoOnValue(&writer, value, fieldDescription);
+ } else {
+ DoOnValue(Writer_, value, fieldDescription);
+ }
+ }
+
+ Y_FORCE_INLINE void DoOnValue(TZeroCopyWriterWithGaps* writer, TUnversionedValue value, const TProtobufWriterFieldDescription& fieldDescription)
+ {
+ if (MatchesCompositeType(fieldDescription)) {
+ ValidateUnversionedValueType(value, EValueType::Composite);
+ TMemoryInput input(value.Data.String, value.Length);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto maxVarIntSize = GetMaxVarIntSizeOfProtobufSizeOfComplexType();
+ Traverse(writer, fieldDescription, &parser, maxVarIntSize);
+ } else if (Y_UNLIKELY(MatchesEnumerateType(fieldDescription))) {
+ WriteProtobufEnum(writer,
+ fieldDescription,
+ TUnversionedValueExtractor(value));
+ } else {
+ WriteVarUint32(writer, fieldDescription.WireTag);
+ if (fieldDescription.Type->ProtoType == EProtobufType::Any) {
+ auto maxYsonSize = GetMaxBinaryYsonSize(value);
+ WriteWithSizePrefix(writer, WireFormatLite::UInt64Size(maxYsonSize), [&] {
+ TCheckedInDebugYsonTokenWriter tokenWriter(writer);
+ UnversionedValueToYson(value, &tokenWriter);
+ });
+ } else {
+ WriteProtobufField(writer, fieldDescription.Type, TUnversionedValueExtractor(value));
+ }
+ }
+ }
+
+ Y_FORCE_INLINE void OnUnknownValue(TUnversionedValue value)
+ {
+ OtherColumnsWriter_.OnValue(value);
+ }
+
+ Y_FORCE_INLINE void OnEndRow()
+ {
+ TZeroCopyWriterWithGaps* writer = Writer_;
+ auto& embeddings = FormatDescription->GetTableDescription(0).Embeddings;
+
+ int parentEmbeddingIndex = 0;
+
+ std::function<int(int)> EmitMessage = [&](int parentEmbeddingIndex) {
+ auto& embeddingDescription = embeddings[parentEmbeddingIndex];
+ auto& blob = EmbeddedBuffers[parentEmbeddingIndex];
+
+ int myParentEmbeddingIndex = parentEmbeddingIndex;
+
+ WriteVarUint32(writer, embeddingDescription.WireTag);
+
+ WriteWithSizePrefix(writer, sizeof(TMessageSize), [&] {
+
+ writer->Write(blob.Begin(), blob.Size());
+ blob.Clear();
+ parentEmbeddingIndex++;
+ while (parentEmbeddingIndex < std::ssize(embeddings) && embeddings[parentEmbeddingIndex].ParentEmbeddingIndex == myParentEmbeddingIndex) {
+ parentEmbeddingIndex = EmitMessage(parentEmbeddingIndex);
+ }
+ });
+ return parentEmbeddingIndex;
+ };
+
+ while (parentEmbeddingIndex < std::ssize(embeddings)) {
+ Y_VERIFY(embeddings[parentEmbeddingIndex].ParentEmbeddingIndex == TProtobufWriterEmbeddingDescription::InvalidIndex);
+ parentEmbeddingIndex = EmitMessage(parentEmbeddingIndex);
+ }
+
+ OtherColumnsWriter_.OnEndRow();
+ OtherColumnsWriter_.WriteProtoField(writer);
+ writer->UndoRemaining();
+ auto totalWrittenSizeAfter = writer->GetTotalWrittenSize();
+ auto messageSize = totalWrittenSizeAfter - TotalWrittenSizeBefore_;
+ if (messageSize >= std::numeric_limits<TMessageSize>::max()) {
+ THROW_ERROR_EXCEPTION("Too large protobuf message: limit is %v, actual size is %v",
+ std::numeric_limits<TMessageSize>::max(),
+ messageSize);
+ }
+ auto messageSizeCast = static_cast<TMessageSize>(messageSize);
+ memcpy(writer->GetGapPointer(MessageSizeGapPosition_), &messageSizeCast, sizeof(messageSizeCast));
+ }
+
+private:
+ void Traverse(
+ TZeroCopyWriterWithGaps* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ TYsonPullParser* parser,
+ int maxVarIntSize)
+ {
+ if (fieldDescription.Repeated) {
+ if (fieldDescription.Type->Optional && parser->IsEntity()) {
+ parser->ParseEntity();
+ return;
+ }
+ if (fieldDescription.Packed) {
+ TraversePackedRepeated(writer, fieldDescription, parser, maxVarIntSize);
+ } else {
+ parser->ParseBeginList();
+ while (!parser->IsEndList()) {
+ TraverseNonRepeated(writer, fieldDescription, parser, maxVarIntSize);
+ }
+ parser->ParseEndList();
+ }
+ } else {
+ TraverseNonRepeated(writer, fieldDescription, parser, maxVarIntSize);
+ }
+ }
+
+ void TraverseOneof(
+ TZeroCopyWriterWithGaps* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ TYsonPullParser* parser,
+ int maxVarIntSize)
+ {
+ if (fieldDescription.Type->Optional) {
+ if (!parser->ParseOptionalBeginList()) {
+ return;
+ }
+ } else {
+ parser->ParseBeginList();
+ }
+
+ auto alternativeIndex = parser->ParseInt64();
+ auto alternative = fieldDescription.Type->FindAlternative(alternativeIndex);
+ if (alternative) {
+ Traverse(writer, *alternative, parser, maxVarIntSize);
+ } else {
+ parser->SkipComplexValue();
+ }
+ parser->ParseEndList();
+ }
+
+ template <typename TFun>
+ Y_FORCE_INLINE void WriteWithSizePrefix(TZeroCopyWriterWithGaps* writer, int maxVarIntSize, TFun writerFun)
+ {
+ auto gap = writer->CreateGap(maxVarIntSize);
+ auto totalWrittenSizeBefore = writer->GetTotalWrittenSize();
+
+ writerFun();
+
+ auto totalWrittenSizeAfter = writer->GetTotalWrittenSize();
+ auto messageSize = totalWrittenSizeAfter - totalWrittenSizeBefore;
+ WriteVarUint64WithPadding(writer->GetGapPointer(gap), messageSize, maxVarIntSize);
+ }
+
+ Y_FORCE_INLINE void TraversePackedRepeated(
+ TZeroCopyWriterWithGaps* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ TYsonPullParser* parser,
+ int maxVarIntSize)
+ {
+ parser->ParseBeginList();
+ if (!parser->IsEndList()) {
+ WriteVarUint32(writer, fieldDescription.WireTag);
+ WriteWithSizePrefix(writer, maxVarIntSize, [&] {
+ while (!parser->IsEndList()) {
+ if (Y_UNLIKELY(MatchesEnumerateType(fieldDescription))) {
+ WriteProtobufEnum(writer,
+ fieldDescription,
+ TYsonValueExtractor(parser));
+ } else {
+ WriteProtobufField(writer, fieldDescription.Type, TYsonValueExtractor(parser));
+ }
+ }
+ });
+ }
+ parser->ParseEndList();
+ }
+
+ Y_FORCE_INLINE void TraverseNonRepeated(
+ TZeroCopyWriterWithGaps* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ TYsonPullParser* parser,
+ int maxVarIntSize)
+ {
+ if (fieldDescription.Type->ProtoType == EProtobufType::Oneof) {
+ TraverseOneof(writer, fieldDescription, parser, maxVarIntSize);
+ return;
+ }
+ if (fieldDescription.Type->Optional && parser->IsEntity()) {
+ parser->ParseEntity();
+ if (fieldDescription.Type->ProtoType == EProtobufType::Any) {
+ WriteVarUint32(writer, fieldDescription.WireTag);
+ WriteWithSizePrefix(writer, 1, [&] {
+ TCheckedInDebugYsonTokenWriter ysonWriter(writer);
+ ysonWriter.WriteEntity();
+ });
+ }
+ return;
+ }
+ if (MatchesEnumerateType(fieldDescription)) {
+ WriteProtobufEnum(writer,
+ fieldDescription,
+ TYsonValueExtractor(parser));
+ return;
+ }
+
+ WriteVarUint32(writer, fieldDescription.WireTag);
+ switch (fieldDescription.Type->ProtoType) {
+ case EProtobufType::StructuredMessage:
+ WriteWithSizePrefix(writer, maxVarIntSize, [&] {
+ TraverseStruct(writer, fieldDescription, parser, maxVarIntSize);
+ });
+ return;
+ case EProtobufType::Any:
+ WriteWithSizePrefix(writer, maxVarIntSize, [&] {
+ TCheckedInDebugYsonTokenWriter ysonWriter(writer);
+ parser->TransferComplexValue(&ysonWriter);
+ });
+ return;
+ case EProtobufType::Oneof:
+ YT_ABORT();
+ default:
+ WriteProtobufField(writer, fieldDescription.Type, TYsonValueExtractor(parser));
+ return;
+ }
+ YT_ABORT();
+ }
+
+ Y_FORCE_INLINE void TraverseStruct(
+ TZeroCopyWriterWithGaps* writer,
+ const TProtobufWriterFieldDescription& fieldDescription,
+ TYsonPullParser* parser,
+ int maxVarIntSize)
+ {
+ parser->ParseBeginList();
+ auto childIterator = fieldDescription.Type->Children.cbegin();
+ int elementIndex = 0;
+ while (!parser->IsEndList()) {
+ if (childIterator == fieldDescription.Type->Children.cend() || (*childIterator)->StructFieldIndex != elementIndex) {
+ parser->SkipComplexValue();
+ ++elementIndex;
+ continue;
+ }
+ const auto& child = **childIterator;
+ if (child.Repeated) {
+ Traverse(writer, child, parser, maxVarIntSize);
+ } else {
+ TraverseNonRepeated(writer, child, parser, maxVarIntSize);
+ }
+ ++childIterator;
+ ++elementIndex;
+ }
+ parser->ParseEndList();
+ }
+
+private:
+ TOtherColumnsWriter OtherColumnsWriter_;
+ TZeroCopyWriterWithGaps* Writer_;
+ TZeroCopyWriterWithGaps::TGapPosition MessageSizeGapPosition_;
+ ui64 TotalWrittenSizeBefore_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForProtobuf
+ : public TSchemalessFormatWriterBase
+{
+public:
+ TSchemalessWriterForProtobuf(
+ const std::vector<TTableSchemaPtr>& schemas,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount,
+ TProtobufWriterFormatDescriptionPtr description,
+ const TYsonConverterConfig& ysonConfig)
+ : TSchemalessFormatWriterBase(
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount)
+ , Description_(description)
+ , WriterImpl_(schemas, nameTable, description, ysonConfig)
+ , StreamWriter_(GetOutputStream())
+ {
+ WriterImpl_.SetTableIndex(CurrentTableIndex_);
+ }
+
+private:
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ int rowCount = static_cast<int>(rows.Size());
+ for (int index = 0; index < rowCount; ++index) {
+ auto row = rows[index];
+
+ if (CheckKeySwitch(row, index + 1 == rowCount)) {
+ WritePod(*StreamWriter_, LenvalKeySwitch);
+ }
+
+ WriteControlAttributes(row);
+
+ WriterImpl_.OnBeginRow(&*StreamWriter_);
+ for (const auto& value : row) {
+ const auto* fieldDescription = Description_->FindField(
+ CurrentTableIndex_,
+ value.Id,
+ NameTable_);
+
+ if (!fieldDescription) {
+ WriterImpl_.OnUnknownValue(value);
+ continue;
+ }
+
+ if (value.Type == EValueType::Null) {
+ continue;
+ }
+
+ try {
+ WriterImpl_.OnValue(value, *fieldDescription);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error writing value of field %Qv",
+ fieldDescription->Name)
+ << ex;
+ }
+ }
+ WriterImpl_.OnEndRow();
+ TryFlushBufferAndUpdateWriter(false);
+ }
+ TryFlushBufferAndUpdateWriter(true);
+ }
+
+ void TryFlushBufferAndUpdateWriter(bool force)
+ {
+ TryFlushBuffer(force);
+ // |StreamWriter_| could have been reset in |FlushWriter()|.
+ if (!StreamWriter_) {
+ StreamWriter_.emplace(GetOutputStream());
+ }
+ }
+
+ void FlushWriter() override
+ {
+ // Reset |StreamWriter_| to ensure it will never touch the
+ // underlying |TBlobOutput| as it will be |Flush()|-ed soon.
+ StreamWriter_.reset();
+ TSchemalessFormatWriterBase::FlushWriter();
+ }
+
+ void WriteTableIndex(i64 tableIndex) override
+ {
+ CurrentTableIndex_ = tableIndex;
+ WriterImpl_.SetTableIndex(tableIndex);
+
+ WritePod(*StreamWriter_, static_cast<ui32>(LenvalTableIndexMarker));
+ WritePod(*StreamWriter_, static_cast<ui32>(tableIndex));
+ }
+
+ void WriteTabletIndex(i64 tabletIndex) override
+ {
+ WritePod(*StreamWriter_, static_cast<ui32>(LenvalTabletIndexMarker));
+ WritePod(*StreamWriter_, static_cast<ui64>(tabletIndex));
+ }
+
+ void WriteRangeIndex(i64 rangeIndex) override
+ {
+ WritePod(*StreamWriter_, static_cast<ui32>(LenvalRangeIndexMarker));
+ WritePod(*StreamWriter_, static_cast<ui32>(rangeIndex));
+ }
+
+ void WriteRowIndex(i64 rowIndex) override
+ {
+ WritePod(*StreamWriter_, static_cast<ui32>(LenvalRowIndexMarker));
+ WritePod(*StreamWriter_, static_cast<ui64>(rowIndex));
+ }
+
+ void WriteEndOfStream() override
+ {
+ WritePod(*StreamWriter_, static_cast<ui32>(LenvalEndOfStream));
+ }
+
+private:
+ const TProtobufWriterFormatDescriptionPtr Description_;
+ TWriterImpl WriterImpl_;
+
+ // Use optional to be able to destruct underlying object when switching output streams.
+ std::optional<TZeroCopyWriterWithGaps> StreamWriter_;
+
+ int CurrentTableIndex_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForProtobuf(
+ TProtobufFormatConfigPtr config,
+ const std::vector<TTableSchemaPtr>& schemas,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ auto description = New<TProtobufWriterFormatDescription>();
+ description->Init(config, schemas);
+ TYsonConverterConfig ysonConfig{
+ .ComplexTypeMode = config->ComplexTypeMode,
+ .DecimalMode = config->DecimalMode,
+ .TimeMode = config->TimeMode,
+ .UuidMode = config->UuidMode,
+ };
+
+ return New<TSchemalessWriterForProtobuf>(
+ schemas,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount,
+ std::move(description),
+ ysonConfig);
+}
+
+ISchemalessFormatWriterPtr CreateWriterForProtobuf(
+ const IAttributeDictionary& attributes,
+ const std::vector<TTableSchemaPtr>& schemas,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(&attributes);
+ return CreateWriterForProtobuf(
+ config,
+ schemas,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for protobuf format") << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/protobuf_writer.h b/yt/yt/client/formats/protobuf_writer.h
new file mode 100644
index 0000000000..a6f7936405
--- /dev/null
+++ b/yt/yt/client/formats/protobuf_writer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForProtobuf(
+ TProtobufFormatConfigPtr config,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ISchemalessFormatWriterPtr CreateWriterForProtobuf(
+ const NYTree::IAttributeDictionary& attributes,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/public.h b/yt/yt/client/formats/public.h
new file mode 100644
index 0000000000..4087a81d44
--- /dev/null
+++ b/yt/yt/client/formats/public.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((InvalidFormat) (2800))
+);
+
+DEFINE_ENUM(EComplexTypeMode,
+ (Positional)
+ (Named)
+);
+
+DEFINE_ENUM(EDictMode,
+ (Positional)
+ (Named)
+);
+
+DEFINE_ENUM(EDecimalMode,
+ (Text)
+ (Binary)
+);
+
+DEFINE_ENUM(ETimeMode,
+ (Text)
+ (Binary)
+);
+
+DEFINE_ENUM(EUuidMode,
+ (TextYql)
+ (TextYt)
+ (Binary)
+);
+
+//! Type of data that can be read or written by a driver command.
+DEFINE_ENUM(EDataType,
+ (Null)
+ (Binary)
+ (Structured)
+ (Tabular)
+);
+
+DEFINE_ENUM(EFormatType,
+ (Null)
+ (Yson)
+ (Json)
+ (Dsv)
+ (Yamr)
+ (YamredDsv)
+ (SchemafulDsv)
+ (Protobuf)
+ (WebJson)
+ (Skiff)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TYsonFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TTableFormatConfigBase)
+DECLARE_REFCOUNTED_CLASS(TYamrFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TYamrFormatConfigBase)
+DECLARE_REFCOUNTED_CLASS(TDsvFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TDsvFormatConfigBase)
+DECLARE_REFCOUNTED_CLASS(TYamredDsvFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TSchemafulDsvFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TProtobufTypeConfig)
+DECLARE_REFCOUNTED_CLASS(TProtobufColumnConfig)
+DECLARE_REFCOUNTED_CLASS(TProtobufTableConfig)
+DECLARE_REFCOUNTED_CLASS(TProtobufFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TWebJsonFormatConfig)
+DECLARE_REFCOUNTED_CLASS(TSkiffFormatConfig)
+
+DECLARE_REFCOUNTED_STRUCT(IYamrConsumer)
+
+DECLARE_REFCOUNTED_STRUCT(ISchemalessFormatWriter)
+
+DECLARE_REFCOUNTED_CLASS(TControlAttributesConfig)
+
+struct IParser;
+
+class TEscapeTable;
+
+class TFormat;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaful_dsv_parser.cpp b/yt/yt/client/formats/schemaful_dsv_parser.cpp
new file mode 100644
index 0000000000..8fb0bda433
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_dsv_parser.cpp
@@ -0,0 +1,198 @@
+#include "schemaful_dsv_parser.h"
+
+#include "parser.h"
+#include "escape.h"
+#include "format.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+namespace NYT::NFormats {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulDsvParser
+ : public IParser
+{
+public:
+ TSchemafulDsvParser(
+ IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config);
+
+ void Read(TStringBuf data) override;
+ void Finish() override;
+
+private:
+ IYsonConsumer* Consumer_;
+ TSchemafulDsvFormatConfigPtr Config_;
+ const std::vector<TString>& Columns_;
+
+ TEscapeTable EscapeTable_;
+
+ bool NewRecordStarted_ = false;
+ bool ExpectingEscapedChar_ = false;
+
+ int RowIndex_ = 0;
+ int FieldIndex_ = 0;
+
+ int TableIndex_ = 0;
+
+ TString CurrentToken_;
+
+ const char* Consume(const char* begin, const char* end);
+ void SwitchTable(int newTableIndex);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemafulDsvParser::TSchemafulDsvParser(
+ IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config)
+ : Consumer_(consumer)
+ , Config_(config)
+ , Columns_(Config_->GetColumnsOrThrow())
+{
+ ConfigureEscapeTable(Config_, &EscapeTable_);
+}
+
+void TSchemafulDsvParser::Read(TStringBuf data)
+{
+ auto current = data.begin();
+ while (current != data.end()) {
+ current = Consume(current, data.end());
+ }
+}
+
+const char* TSchemafulDsvParser::Consume(const char* begin, const char* end)
+{
+ // Process escaping symbols.
+ if (Config_->EnableEscaping && !ExpectingEscapedChar_ && *begin == Config_->EscapingSymbol) {
+ ExpectingEscapedChar_ = true;
+ return begin + 1;
+ }
+ if (ExpectingEscapedChar_) {
+ CurrentToken_.append(EscapeBackward[static_cast<ui8>(*begin)]);
+ ExpectingEscapedChar_ = false;
+ return begin + 1;
+ }
+
+ // Process common case.
+ auto next = EscapeTable_.FindNext(begin, end);
+ CurrentToken_.append(begin, next);
+ if (next == end || *next == Config_->EscapingSymbol) {
+ return next;
+ }
+
+ YT_VERIFY(*next == Config_->FieldSeparator ||
+ *next == Config_->RecordSeparator);
+
+ if (!NewRecordStarted_) {
+ NewRecordStarted_ = true;
+
+ if (Config_->EnableTableIndex) {
+ SwitchTable(FromString<int>(CurrentToken_));
+ }
+
+ Consumer_->OnListItem();
+ Consumer_->OnBeginMap();
+
+ if (Config_->EnableTableIndex) {
+ CurrentToken_.clear();
+ return next + 1;
+ }
+ }
+
+ if (FieldIndex_ == std::ssize(Columns_)) {
+ THROW_ERROR_EXCEPTION("Too many fields in row: expected %v but found more",
+ std::ssize(Columns_));
+ }
+
+ Consumer_->OnKeyedItem(Columns_[FieldIndex_++]);
+
+ if (Config_->MissingValueMode == EMissingSchemafulDsvValueMode::PrintSentinel &&
+ CurrentToken_ == Config_->MissingValueSentinel)
+ {
+ Consumer_->OnEntity();
+ } else {
+ Consumer_->OnStringScalar(CurrentToken_);
+ }
+
+ CurrentToken_.clear();
+
+ if (*next == Config_->RecordSeparator) {
+ if (FieldIndex_ != std::ssize(Columns_)) {
+ THROW_ERROR_EXCEPTION("Row %v is incomplete: expected %v fields but found %v",
+ RowIndex_,
+ std::ssize(Columns_),
+ FieldIndex_);
+ }
+ Consumer_->OnEndMap();
+ NewRecordStarted_ = false;
+ FieldIndex_ = 0;
+
+ RowIndex_ += 1;
+ }
+ return next + 1;
+}
+
+void TSchemafulDsvParser::SwitchTable(int newTableIndex)
+{
+ static const TString key = FormatEnum(NTableClient::EControlAttribute(
+ NTableClient::EControlAttribute::TableIndex));
+ if (newTableIndex != TableIndex_) {
+ TableIndex_ = newTableIndex;
+
+ Consumer_->OnListItem();
+ Consumer_->OnBeginAttributes();
+ Consumer_->OnKeyedItem(key);
+ Consumer_->OnInt64Scalar(TableIndex_);
+ Consumer_->OnEndAttributes();
+ Consumer_->OnEntity();
+ }
+}
+
+void TSchemafulDsvParser::Finish()
+{
+ if (NewRecordStarted_ || !CurrentToken_.empty() || ExpectingEscapedChar_) {
+ THROW_ERROR_EXCEPTION("Row %v is not finished", RowIndex_);
+ }
+ CurrentToken_.clear();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseSchemafulDsv(
+ IInputStream* input,
+ IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForSchemafulDsv(consumer, config);
+ Parse(input, parser.get());
+}
+
+void ParseSchemafulDsv(
+ TStringBuf data,
+ IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForSchemafulDsv(consumer, config);
+ parser->Read(data);
+ parser->Finish();
+}
+
+std::unique_ptr<IParser> CreateParserForSchemafulDsv(
+ IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config)
+{
+ if (config->EnableColumnNamesHeader) {
+ THROW_ERROR_EXCEPTION("Parameter %Qv must not be specified for schemaful DSV parser",
+ "enable_column_names_header");
+ }
+ return std::unique_ptr<IParser>(new TSchemafulDsvParser(consumer, config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaful_dsv_parser.h b/yt/yt/client/formats/schemaful_dsv_parser.h
new file mode 100644
index 0000000000..cc01a9b399
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_dsv_parser.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForSchemafulDsv(
+ NYson::IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config = New<TSchemafulDsvFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseSchemafulDsv(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config = New<TSchemafulDsvFormatConfig>());
+
+void ParseSchemafulDsv(
+ TStringBuf data,
+ NYson::IYsonConsumer* consumer,
+ TSchemafulDsvFormatConfigPtr config = New<TSchemafulDsvFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/schemaful_dsv_writer.cpp b/yt/yt/client/formats/schemaful_dsv_writer.cpp
new file mode 100644
index 0000000000..17b9210ff9
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_dsv_writer.cpp
@@ -0,0 +1,397 @@
+#include "schemaful_dsv_writer.h"
+
+#include "escape.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/format.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <limits>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This class contains methods common for TSchemafulWriterForSchemafulDsv and TSchemalessWriterForSchemafulDsv.
+class TSchemafulDsvWriterBase
+{
+protected:
+ TSchemafulDsvFormatConfigPtr Config_;
+
+ // This array indicates on which position should each
+ // column stay in the resulting row.
+ std::vector<int> IdToIndexInRow_;
+
+ // This array contains TUnversionedValue's reordered
+ // according to the desired order.
+ std::vector<const TUnversionedValue*> CurrentRowValues_;
+
+ TEscapeTable EscapeTable_;
+
+ TSchemafulDsvWriterBase(TSchemafulDsvFormatConfigPtr config, std::vector<int> idToIndexInRow)
+ : Config_(config)
+ , IdToIndexInRow_(idToIndexInRow)
+ {
+ ConfigureEscapeTable(Config_, &EscapeTable_);
+ if (!IdToIndexInRow_.empty()) {
+ CurrentRowValues_.resize(
+ *std::max_element(IdToIndexInRow_.begin(), IdToIndexInRow_.end()) + 1);
+ }
+ YT_VERIFY(Config_->Columns);
+ }
+
+ int FindMissingValueIndex() const
+ {
+ for (int valueIndex = 0; valueIndex < static_cast<int>(CurrentRowValues_.size()); ++valueIndex) {
+ const auto* value = CurrentRowValues_[valueIndex];
+ if (!value || value->Type == EValueType::Null) {
+ return valueIndex;
+ }
+ }
+ return -1;
+ }
+
+ template <class TFunction>
+ void WriteColumnNamesHeader(TFunction writeCallback)
+ {
+ if (Config_->EnableColumnNamesHeader && *Config_->EnableColumnNamesHeader) {
+ const auto& columns = *Config_->Columns;
+ for (size_t index = 0; index < columns.size(); ++index) {
+ writeCallback(
+ columns[index],
+ (index + 1 == columns.size()) ? Config_->RecordSeparator : Config_->FieldSeparator);
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForSchemafulDsv
+ : public TSchemalessFormatWriterBase
+ , public TSchemafulDsvWriterBase
+{
+public:
+ TSchemalessWriterForSchemafulDsv(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ TSchemafulDsvFormatConfigPtr config,
+ std::vector<int> idToIndexInRow)
+ : TSchemalessFormatWriterBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ 0 /* keyColumnCount */)
+ , TSchemafulDsvWriterBase(
+ config,
+ idToIndexInRow)
+ , TableIndexColumnId_(Config_->EnableTableIndex && controlAttributesConfig->EnableTableIndex
+ ? nameTable->GetId(TableIndexColumnName)
+ : -1)
+ {
+ BlobOutput_ = GetOutputStream();
+ WriteColumnNamesHeader([this](TStringBuf buf, char c) {
+ WriteRaw(buf);
+ WriteRaw(c);
+ });
+ }
+
+private:
+ TBlobOutput* BlobOutput_;
+ const int TableIndexColumnId_;
+
+ // ISchemalessFormatWriter overrides.
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ for (const auto& row : rows) {
+ CurrentRowValues_.assign(CurrentRowValues_.size(), nullptr);
+ for (auto item = row.Begin(); item != row.End(); ++item) {
+ if (item->Id < IdToIndexInRow_.size() && IdToIndexInRow_[item->Id] != -1) {
+ CurrentRowValues_[IdToIndexInRow_[item->Id]] = item;
+ }
+ }
+
+ if (Config_->EnableTableIndex && ControlAttributesConfig_->EnableTableIndex &&
+ !CurrentRowValues_[IdToIndexInRow_[TableIndexColumnId_]])
+ {
+ THROW_ERROR_EXCEPTION("Table index column is missing");
+ }
+
+ int missingValueIndex = FindMissingValueIndex();
+ if (missingValueIndex != -1) {
+ if (Config_->MissingValueMode == EMissingSchemafulDsvValueMode::SkipRow) {
+ continue;
+ } else if (Config_->MissingValueMode == EMissingSchemafulDsvValueMode::Fail) {
+ THROW_ERROR_EXCEPTION("Column %Qv is in schema but missing", (*Config_->Columns)[missingValueIndex]);
+ }
+ }
+
+ bool firstValue = true;
+ for (const auto* item : CurrentRowValues_) {
+ if (!firstValue) {
+ WriteRaw(Config_->FieldSeparator);
+ } else {
+ firstValue = false;
+ }
+ if (!item || item->Type == EValueType::Null) {
+ // If we got here, MissingValueMode is PrintSentinel.
+ WriteRaw(Config_->MissingValueSentinel);
+ } else {
+ WriteUnversionedValue(*item, BlobOutput_, EscapeTable_);
+ }
+ }
+ WriteRaw(Config_->RecordSeparator);
+ TryFlushBuffer(false);
+ }
+ TryFlushBuffer(true);
+ }
+
+ void WriteRaw(TStringBuf str)
+ {
+ BlobOutput_->Write(str.begin(), str.length());
+ }
+
+ void WriteRaw(char ch)
+ {
+ BlobOutput_->Write(ch);
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulWriterForSchemafulDsv
+ : public IUnversionedRowsetWriter
+ , public TSchemafulDsvWriterBase
+{
+public:
+ TSchemafulWriterForSchemafulDsv(
+ IAsyncOutputStreamPtr stream,
+ TSchemafulDsvFormatConfigPtr config,
+ std::vector<int> IdToIndexInRow)
+ : TSchemafulDsvWriterBase(
+ config,
+ IdToIndexInRow)
+ , Output_(CreateBufferedSyncAdapter(stream))
+ {
+ WriteColumnNamesHeader([this](TStringBuf buf, char c) {
+ Output_->Write(buf);
+ Output_->Write(c);
+ });
+ }
+
+ TFuture<void> Close() override
+ {
+ DoFlushBuffer();
+ return VoidFuture;
+ }
+
+ bool Write(TRange<TUnversionedRow> rows) override
+ {
+ for (const auto& row : rows) {
+ if (!row) {
+ THROW_ERROR_EXCEPTION("Empty rows are not supported by schemaful dsv writer");
+ }
+
+ CurrentRowValues_.assign(CurrentRowValues_.size(), nullptr);
+ for (auto item = row.Begin(); item != row.End(); ++item) {
+ YT_ASSERT(item->Id >= 0 && item->Id < IdToIndexInRow_.size());
+ if (IdToIndexInRow_[item->Id] != -1) {
+ CurrentRowValues_[IdToIndexInRow_[item->Id]] = item;
+ }
+ }
+
+ int missingValueIndex = FindMissingValueIndex();
+ if (missingValueIndex != -1) {
+ if (Config_->MissingValueMode == EMissingSchemafulDsvValueMode::SkipRow) {
+ continue;
+ } else if (Config_->MissingValueMode == EMissingSchemafulDsvValueMode::Fail) {
+ THROW_ERROR_EXCEPTION("Column %Qv is in schema but missing", (*Config_->Columns)[missingValueIndex]);
+ }
+ }
+
+ bool firstValue = true;
+ for (const auto* item : CurrentRowValues_) {
+ if (!firstValue) {
+ Output_->Write(Config_->FieldSeparator);
+ } else {
+ firstValue = false;
+ }
+ if (!item || item->Type == EValueType::Null) {
+ // If we got here, MissingValueMode is PrintSentinel.
+ Output_->Write(Config_->MissingValueSentinel);
+ } else {
+ WriteUnversionedValue(*item, Output_.get(), EscapeTable_);
+ }
+ }
+ Output_->Write(Config_->RecordSeparator);
+ }
+ DoFlushBuffer();
+
+ return true;
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return Result_;
+ }
+
+private:
+ std::unique_ptr<IOutputStream> Output_;
+
+ void DoFlushBuffer()
+ {
+ Output_->Flush();
+ }
+
+ TFuture<void> Result_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateDuplicateColumns(const std::vector<TString>& columns)
+{
+ THashSet<TString> names;
+ for (const auto& name : columns) {
+ if (!names.insert(name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column name %Qv in schemaful DSV config",
+ name);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForSchemafulDsv(
+ TSchemafulDsvFormatConfigPtr config,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int /* keyColumnCount */)
+{
+ if (controlAttributesConfig->EnableKeySwitch) {
+ THROW_ERROR_EXCEPTION("Key switches are not supported in schemaful DSV format");
+ }
+
+ if (controlAttributesConfig->EnableRangeIndex) {
+ THROW_ERROR_EXCEPTION("Range indices are not supported in schemaful DSV format");
+ }
+
+ if (controlAttributesConfig->EnableRowIndex) {
+ THROW_ERROR_EXCEPTION("Row indices are not supported in schemaful DSV format");
+ }
+
+ if (controlAttributesConfig->EnableTabletIndex) {
+ THROW_ERROR_EXCEPTION("Tablet indices are not supported in schemaful DSV format");
+ }
+
+ if (!config->Columns) {
+ THROW_ERROR_EXCEPTION("Config must contain columns for schemaful DSV schemaless writer");
+ }
+
+ std::vector<int> idToIndexInRow;
+ auto columns = *config->Columns;
+
+ if (config->EnableTableIndex && controlAttributesConfig->EnableTableIndex) {
+ columns.insert(columns.begin(), TableIndexColumnName);
+ }
+
+ ValidateDuplicateColumns(columns);
+
+ try {
+ for (int columnIndex = 0; columnIndex < static_cast<int>(columns.size()); ++columnIndex) {
+ nameTable->GetIdOrRegisterName(columns[columnIndex]);
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to add columns to name table for schemaful DSV format")
+ << ex;
+ }
+
+ idToIndexInRow.resize(nameTable->GetSize(), -1);
+ for (int columnIndex = 0; columnIndex < static_cast<int>(columns.size()); ++columnIndex) {
+ idToIndexInRow[nameTable->GetId(columns[columnIndex])] = columnIndex;
+ }
+
+ return New<TSchemalessWriterForSchemafulDsv>(
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ config,
+ idToIndexInRow);
+}
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForSchemafulDsv(
+ const IAttributeDictionary& attributes,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = ConvertTo<TSchemafulDsvFormatConfigPtr>(&attributes);
+ return CreateSchemalessWriterForSchemafulDsv(
+ config,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for schemaful DSV format") << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedRowsetWriterPtr CreateSchemafulWriterForSchemafulDsv(
+ TSchemafulDsvFormatConfigPtr config,
+ TTableSchemaPtr schema,
+ IAsyncOutputStreamPtr stream)
+{
+ std::vector<int> idToIndexInRow(schema->GetColumnCount(), -1);
+ if (config->Columns) {
+ ValidateDuplicateColumns(*config->Columns);
+ for (int columnIndex = 0; columnIndex < static_cast<int>(config->Columns->size()); ++columnIndex) {
+ idToIndexInRow[schema->GetColumnIndexOrThrow((*config->Columns)[columnIndex])] = columnIndex;
+ }
+ } else {
+ std::iota(idToIndexInRow.begin(), idToIndexInRow.end(), 0);
+ }
+
+ return New<TSchemafulWriterForSchemafulDsv>(
+ std::move(stream),
+ std::move(config),
+ idToIndexInRow);
+}
+
+IUnversionedRowsetWriterPtr CreateSchemafulWriterForSchemafulDsv(
+ const IAttributeDictionary& attributes,
+ TTableSchemaPtr schema,
+ IAsyncOutputStreamPtr stream)
+{
+ auto config = ConvertTo<TSchemafulDsvFormatConfigPtr>(&attributes);
+ return CreateSchemafulWriterForSchemafulDsv(
+ std::move(config),
+ std::move(schema),
+ std::move(stream));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaful_dsv_writer.h b/yt/yt/client/formats/schemaful_dsv_writer.h
new file mode 100644
index 0000000000..c420f9e7ea
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_dsv_writer.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "helpers.h"
+#include "schemaless_writer_adapter.h"
+
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/blob_output.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForSchemafulDsv(
+ TSchemafulDsvFormatConfigPtr config,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForSchemafulDsv(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTableClient::IUnversionedRowsetWriterPtr CreateSchemafulWriterForSchemafulDsv(
+ TSchemafulDsvFormatConfigPtr config,
+ NTableClient::TTableSchemaPtr schema,
+ NConcurrency::IAsyncOutputStreamPtr stream);
+
+NTableClient::IUnversionedRowsetWriterPtr CreateSchemafulWriterForSchemafulDsv(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TTableSchemaPtr schema,
+ NConcurrency::IAsyncOutputStreamPtr stream);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/schemaful_writer.cpp b/yt/yt/client/formats/schemaful_writer.cpp
new file mode 100644
index 0000000000..c7f72d5544
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_writer.cpp
@@ -0,0 +1,119 @@
+#include "schemaful_writer.h"
+#include "config.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemafulWriter::TSchemafulWriter(
+ NConcurrency::IAsyncOutputStreamPtr stream,
+ NTableClient::TTableSchemaPtr schema,
+ const std::function<std::unique_ptr<IFlushableYsonConsumer>(IZeroCopyOutput*)>& consumerBuilder)
+ : Stream_(std::move(stream))
+ , Schema_(std::move(schema))
+ , Consumer_(consumerBuilder(&Buffer_))
+{
+ auto nameTable = TNameTable::FromSchema(*Schema_);
+
+ for (const auto& column : Schema_->Columns()) {
+ if (IsV3Composite(column.LogicalType())) {
+ auto id = nameTable->GetIdOrThrow(column.Name());
+ TComplexTypeFieldDescriptor descriptor(column.Name(), column.LogicalType());
+ auto converter = CreateYsonServerToClientConverter(descriptor, /*config*/ {});
+ if (converter) {
+ ColumnConverters_.emplace(id, std::move(converter));
+ }
+ }
+ }
+}
+
+TFuture<void> TSchemafulWriter::Close()
+{
+ return Result_;
+}
+
+bool TSchemafulWriter::Write(TRange<TUnversionedRow> rows)
+{
+ Buffer_.Clear();
+
+ int columnCount = Schema_->GetColumnCount();
+ for (auto row : rows) {
+ if (!row) {
+ Consumer_->OnEntity();
+ continue;
+ }
+
+ YT_ASSERT(static_cast<int>(row.GetCount()) >= columnCount);
+ Consumer_->OnBeginMap();
+ for (int index = 0; index < columnCount; ++index) {
+ const auto& value = row[index];
+
+ const auto& column = Schema_->Columns()[index];
+ Consumer_->OnKeyedItem(column.Name());
+
+ switch (value.Type) {
+ case EValueType::Int64:
+ Consumer_->OnInt64Scalar(value.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ Consumer_->OnUint64Scalar(value.Data.Uint64);
+ break;
+ case EValueType::Double:
+ Consumer_->OnDoubleScalar(value.Data.Double);
+ break;
+ case EValueType::Boolean:
+ Consumer_->OnBooleanScalar(value.Data.Boolean);
+ break;
+ case EValueType::String:
+ Consumer_->OnStringScalar(value.AsStringBuf());
+ break;
+ case EValueType::Null:
+ Consumer_->OnEntity();
+ break;
+ case EValueType::Any:
+ Consumer_->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ break;
+
+ case EValueType::Composite: {
+ if (auto it = ColumnConverters_.find(value.Id); it != ColumnConverters_.end()) {
+ it->second(value, Consumer_.get());
+ } else {
+ Consumer_->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ }
+ break;
+ }
+
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ ThrowUnexpectedValueType(value.Type);
+ }
+ }
+ Consumer_->OnEndMap();
+ }
+
+ Consumer_->Flush();
+ auto buffer = Buffer_.Flush();
+ Result_ = Stream_->Write(buffer);
+ return Result_.IsSet() && Result_.Get().IsOK();
+}
+
+TFuture<void> TSchemafulWriter::GetReadyEvent()
+{
+ return Result_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaful_writer.h b/yt/yt/client/formats/schemaful_writer.h
new file mode 100644
index 0000000000..2d4848431f
--- /dev/null
+++ b/yt/yt/client/formats/schemaful_writer.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/complex_types/yson_format_conversion.h>
+
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulWriter
+ : public NTableClient::IUnversionedRowsetWriter
+{
+public:
+ TSchemafulWriter(
+ NConcurrency::IAsyncOutputStreamPtr stream,
+ NTableClient::TTableSchemaPtr schema,
+ const std::function<std::unique_ptr<NYson::IFlushableYsonConsumer>(IZeroCopyOutput*)>& consumerBuilder);
+
+ TFuture<void> Close() override;
+
+ bool Write(TRange<NTableClient::TUnversionedRow> rows) override;
+
+ TFuture<void> GetReadyEvent() override;
+
+private:
+ const NConcurrency::IAsyncOutputStreamPtr Stream_;
+ const NTableClient::TTableSchemaPtr Schema_;
+
+ TBlobOutput Buffer_;
+
+ TFuture<void> Result_;
+
+ const std::unique_ptr<NYson::IFlushableYsonConsumer> Consumer_;
+
+ THashMap<int, NComplexTypes::TYsonServerToClientConverter> ColumnConverters_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaless_writer_adapter.cpp b/yt/yt/client/formats/schemaless_writer_adapter.cpp
new file mode 100644
index 0000000000..bc9a3063a5
--- /dev/null
+++ b/yt/yt/client/formats/schemaless_writer_adapter.cpp
@@ -0,0 +1,440 @@
+#include "schemaless_writer_adapter.h"
+#include "config.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NFormats {
+
+using namespace NTableClient;
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NYTree;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const i64 ContextBufferSize = static_cast<i64>(128 * 7) * 1024;
+static const i64 ContextBufferCapacity = static_cast<i64>(1024) * 1024;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemalessFormatWriterBase::TSchemalessFormatWriterBase(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+ : NameTable_(nameTable)
+ , Output_(output)
+ , EnableContextSaving_(enableContextSaving)
+ , ControlAttributesConfig_(controlAttributesConfig)
+ , KeyColumnCount_(keyColumnCount)
+ , NameTableReader_(std::make_unique<TNameTableReader>(NameTable_))
+{
+ CurrentBuffer_.Reserve(ContextBufferCapacity);
+
+ EnableRowControlAttributes_ = ControlAttributesConfig_->EnableTableIndex ||
+ ControlAttributesConfig_->EnableRangeIndex ||
+ ControlAttributesConfig_->EnableRowIndex ||
+ ControlAttributesConfig_->EnableTabletIndex;
+
+ try {
+ RowIndexId_ = NameTable_->GetIdOrRegisterName(RowIndexColumnName);
+ RangeIndexId_ = NameTable_->GetIdOrRegisterName(RangeIndexColumnName);
+ TableIndexId_ = NameTable_->GetIdOrRegisterName(TableIndexColumnName);
+ TabletIndexId_ = NameTable_->GetIdOrRegisterName(TabletIndexColumnName);
+ } catch (const std::exception& ex) {
+ Error_ = TError("Failed to add system columns to name table for a format writer") << ex;
+ }
+}
+
+TFuture<void> TSchemalessFormatWriterBase::GetReadyEvent()
+{
+ return MakeFuture(Error_);
+}
+
+TFuture<void> TSchemalessFormatWriterBase::Close()
+{
+ if (Closed_) {
+ return MakeFuture(Error_);
+ }
+
+ try {
+ if (ControlAttributesConfig_->EnableEndOfStream) {
+ WriteEndOfStream();
+ }
+ DoFlushBuffer();
+ } catch (const std::exception& ex) {
+ Error_ = TError(ex);
+ }
+
+ Closed_ = true;
+
+ return MakeFuture(Error_);
+}
+
+TBlobOutput* TSchemalessFormatWriterBase::GetOutputStream()
+{
+ return &CurrentBuffer_;
+}
+
+TBlob TSchemalessFormatWriterBase::GetContext() const
+{
+ TBlob result;
+ result.Append(PreviousBuffer_);
+ result.Append(TRef::FromBlob(CurrentBuffer_.Blob()));
+ return result;
+}
+
+void TSchemalessFormatWriterBase::TryFlushBuffer(bool force)
+{
+ if (CurrentBuffer_.Size() > ContextBufferSize || (!EnableContextSaving_ && force)) {
+ DoFlushBuffer();
+ }
+}
+
+i64 TSchemalessFormatWriterBase::GetWrittenSize() const
+{
+ return WrittenSize_;
+}
+
+void TSchemalessFormatWriterBase::FlushWriter()
+{ }
+
+void TSchemalessFormatWriterBase::DoFlushBuffer()
+{
+ FlushWriter();
+
+ if (CurrentBuffer_.Size() == 0) {
+ return;
+ }
+
+ WrittenSize_ += CurrentBuffer_.Size();
+
+ PreviousBuffer_ = CurrentBuffer_.Flush();
+ WaitFor(Output_->Write(PreviousBuffer_))
+ .ThrowOnError();
+
+ CurrentBuffer_.Reserve(ContextBufferCapacity);
+}
+
+bool TSchemalessFormatWriterBase::Write(TRange<TUnversionedRow> rows)
+{
+ if (!Error_.IsOK()) {
+ return false;
+ }
+
+ try {
+ DoWrite(rows);
+ } catch (const std::exception& ex) {
+ Error_ = TError(ex);
+ return false;
+ }
+
+ return true;
+}
+
+bool TSchemalessFormatWriterBase::CheckKeySwitch(TUnversionedRow row, bool isLastRow)
+{
+ if (!ControlAttributesConfig_->EnableKeySwitch) {
+ return false;
+ }
+
+ bool needKeySwitch = false;
+ try {
+ needKeySwitch = CurrentKey_ && CompareRows(row, CurrentKey_, KeyColumnCount_);
+ CurrentKey_ = row;
+ } catch (const std::exception& ex) {
+ // COMPAT(psushin): composite values are not comparable anymore.
+ THROW_ERROR_EXCEPTION("Cannot inject key switch into output stream") << ex;
+ }
+
+ if (isLastRow && CurrentKey_) {
+ // After processing last row we create a copy of CurrentKey.
+ LastKey_ = GetKeyPrefix(CurrentKey_, KeyColumnCount_);
+ CurrentKey_ = LastKey_;
+ }
+
+ return needKeySwitch;
+}
+
+bool TSchemalessFormatWriterBase::IsSystemColumnId(int id) const
+{
+ return IsTableIndexColumnId(id) ||
+ IsRangeIndexColumnId(id) ||
+ IsRowIndexColumnId(id) ||
+ IsTabletIndexColumnId(id);
+}
+
+bool TSchemalessFormatWriterBase::IsTableIndexColumnId(int id) const
+{
+ return id == TableIndexId_;
+}
+
+bool TSchemalessFormatWriterBase::IsRowIndexColumnId(int id) const
+{
+ return id == RowIndexId_;
+}
+
+bool TSchemalessFormatWriterBase::IsRangeIndexColumnId(int id) const
+{
+ return id == RangeIndexId_;
+}
+
+bool TSchemalessFormatWriterBase::IsTabletIndexColumnId(int id) const
+{
+ return id == TabletIndexId_;
+}
+
+int TSchemalessFormatWriterBase::GetRangeIndexColumnId() const
+{
+ return RangeIndexId_;
+}
+
+int TSchemalessFormatWriterBase::GetRowIndexColumnId() const
+{
+ return RowIndexId_;
+}
+
+int TSchemalessFormatWriterBase::GetTableIndexColumnId() const
+{
+ return TableIndexId_;
+}
+
+int TSchemalessFormatWriterBase::GetTabletIndexColumnId() const
+{
+ return TabletIndexId_;
+}
+
+void TSchemalessFormatWriterBase::WriteControlAttributes(TUnversionedRow row)
+{
+ if (!EnableRowControlAttributes_) {
+ return;
+ }
+
+ ++RowIndex_;
+
+ std::optional<i64> tableIndex;
+ std::optional<i64> rangeIndex;
+ std::optional<i64> rowIndex;
+ std::optional<i64> tabletIndex;
+
+ for (auto* it = row.Begin(); it != row.End(); ++it) {
+ if (it->Id == TableIndexId_) {
+ tableIndex = it->Data.Int64;
+ } else if (it->Id == RowIndexId_) {
+ rowIndex = it->Data.Int64;
+ } else if (it->Id == RangeIndexId_) {
+ rangeIndex = it->Data.Int64;
+ } else if (it->Id == TabletIndexId_) {
+ tabletIndex = it->Data.Int64;
+ }
+ }
+
+ bool needRowIndex = false;
+ if (tableIndex && *tableIndex != TableIndex_) {
+ if (ControlAttributesConfig_->EnableTableIndex) {
+ WriteTableIndex(*tableIndex);
+ }
+ TableIndex_ = *tableIndex;
+ needRowIndex = true;
+ }
+
+ if (rangeIndex && *rangeIndex != RangeIndex_) {
+ if (ControlAttributesConfig_->EnableRangeIndex) {
+ WriteRangeIndex(*rangeIndex);
+ }
+ RangeIndex_ = *rangeIndex;
+ needRowIndex = true;
+ }
+
+ if (tabletIndex && *tabletIndex != TabletIndex_) {
+ if (ControlAttributesConfig_->EnableTabletIndex) {
+ WriteTabletIndex(*tabletIndex);
+ }
+ TabletIndex_ = *tabletIndex;
+ needRowIndex = true;
+ }
+
+ if (rowIndex) {
+ needRowIndex = needRowIndex || (*rowIndex != RowIndex_);
+ RowIndex_ = *rowIndex;
+ if (ControlAttributesConfig_->EnableRowIndex && needRowIndex) {
+ WriteRowIndex(*rowIndex);
+ }
+ }
+}
+
+void TSchemalessFormatWriterBase::WriteTableIndex(i64 /* tableIndex */)
+{ }
+
+void TSchemalessFormatWriterBase::WriteRangeIndex(i64 /* rangeIndex */)
+{ }
+
+void TSchemalessFormatWriterBase::WriteRowIndex(i64 /* rowIndex */)
+{ }
+
+void TSchemalessFormatWriterBase::WriteTabletIndex(i64 /* tabletIndex */)
+{ }
+
+void TSchemalessFormatWriterBase::WriteEndOfStream()
+{ }
+
+bool TSchemalessFormatWriterBase::HasError() const
+{
+ return !Error_.IsOK();
+}
+
+const TError& TSchemalessFormatWriterBase::GetError() const
+{
+ return Error_;
+}
+
+void TSchemalessFormatWriterBase::RegisterError(const TError& error)
+{
+ Error_ = error;
+}
+
+TFuture<void> TSchemalessFormatWriterBase::Flush()
+{
+ TryFlushBuffer(/*force*/ true);
+ return MakeFuture(Error_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemalessWriterAdapter::TSchemalessWriterAdapter(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+ : TSchemalessFormatWriterBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount)
+{ }
+
+// CreateConsumerForFormat may throw an exception if there is no consumer for the given format,
+// so we set Consumer_ inside Init function rather than inside the constructor.
+void TSchemalessWriterAdapter::Init(const std::vector<NTableClient::TTableSchemaPtr>& tableSchemas, const TFormat& format)
+{
+ // This is generic code for those formats, that support skipping nulls.
+ // See #TYsonFormatConfig and #TJsonFormatConfig.
+ SkipNullValues_ = format.Attributes().Get("skip_null_values", false);
+ TYsonConverterConfig config{
+ .ComplexTypeMode = format.Attributes().Get("complex_type_mode", EComplexTypeMode::Named),
+ .StringKeyedDictMode = format.Attributes().Get("string_keyed_dict_mode", EDictMode::Positional),
+ .DecimalMode = format.Attributes().Get("decimal_mode", EDecimalMode::Binary),
+ .TimeMode = format.Attributes().Get("time_mode", ETimeMode::Binary),
+ .UuidMode = format.Attributes().Get("uuid_mode", EUuidMode::Binary),
+ .SkipNullValues = SkipNullValues_,
+ };
+
+ Consumer_ = CreateConsumerForFormat(format, EDataType::Tabular, GetOutputStream());
+
+ ValueWriters_.reserve(tableSchemas.size());
+ for (const auto& schema : tableSchemas) {
+ ValueWriters_.emplace_back(NameTable_, schema, config);
+ }
+}
+
+void TSchemalessWriterAdapter::DoWrite(TRange<TUnversionedRow> rows)
+{
+ int count = static_cast<int>(rows.Size());
+ for (int index = 0; index < count; ++index) {
+ if (CheckKeySwitch(rows[index], index + 1 == count /* isLastRow */)) {
+ WriteControlAttribute(EControlAttribute::KeySwitch, true);
+ }
+
+ ConsumeRow(rows[index]);
+ FlushWriter();
+ TryFlushBuffer(false);
+ }
+
+ TryFlushBuffer(true);
+}
+
+void TSchemalessWriterAdapter::FlushWriter()
+{
+ Consumer_->Flush();
+}
+
+template <class T>
+void TSchemalessWriterAdapter::WriteControlAttribute(
+ EControlAttribute controlAttribute,
+ T value)
+{
+ BuildYsonListFragmentFluently(Consumer_.get())
+ .Item()
+ .BeginAttributes()
+ .Item(FormatEnum(controlAttribute)).Value(value)
+ .EndAttributes()
+ .Entity();
+}
+
+void TSchemalessWriterAdapter::WriteTableIndex(i64 tableIndex)
+{
+ CurrentTableIndex_ = tableIndex;
+ WriteControlAttribute(EControlAttribute::TableIndex, tableIndex);
+}
+
+void TSchemalessWriterAdapter::WriteRowIndex(i64 rowIndex)
+{
+ WriteControlAttribute(EControlAttribute::RowIndex, rowIndex);
+}
+
+void TSchemalessWriterAdapter::WriteRangeIndex(i64 rangeIndex)
+{
+ WriteControlAttribute(EControlAttribute::RangeIndex, rangeIndex);
+}
+
+void TSchemalessWriterAdapter::WriteTabletIndex(i64 tabletIndex)
+{
+ WriteControlAttribute(EControlAttribute::TabletIndex, tabletIndex);
+}
+
+void TSchemalessWriterAdapter::WriteEndOfStream()
+{
+ WriteControlAttribute(EControlAttribute::EndOfStream, true);
+}
+
+void TSchemalessWriterAdapter::ConsumeRow(TUnversionedRow row)
+{
+ WriteControlAttributes(row);
+
+ Consumer_->OnListItem();
+ Consumer_->OnBeginMap();
+ for (auto* it = row.Begin(); it != row.End(); ++it) {
+ auto& value = *it;
+
+ if (IsSystemColumnId(value.Id)) {
+ continue;
+ }
+
+ if (value.Type == EValueType::Null && SkipNullValues_) {
+ continue;
+ }
+
+ Consumer_->OnKeyedItem(NameTableReader_->GetName(value.Id));
+ ValueWriters_[CurrentTableIndex_].WriteValue(value, Consumer_.get());
+ }
+ Consumer_->OnEndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/schemaless_writer_adapter.h b/yt/yt/client/formats/schemaless_writer_adapter.h
new file mode 100644
index 0000000000..9eb65c1e58
--- /dev/null
+++ b/yt/yt/client/formats/schemaless_writer_adapter.h
@@ -0,0 +1,157 @@
+#pragma once
+
+#include "public.h"
+#include "format.h"
+#include "helpers.h"
+#include "unversioned_value_yson_writer.h"
+
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <memory>
+#include <limits>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessFormatWriterBase
+ : public ISchemalessFormatWriter
+{
+public:
+ bool Write(TRange<NTableClient::TUnversionedRow> rows) override;
+
+ TFuture<void> GetReadyEvent() override;
+
+ TFuture<void> Close() override;
+
+ TBlob GetContext() const override;
+
+ i64 GetWrittenSize() const override;
+
+ TFuture<void> Flush() override;
+
+protected:
+ const NTableClient::TNameTablePtr NameTable_;
+ const NConcurrency::IAsyncOutputStreamPtr Output_;
+ const bool EnableContextSaving_;
+ const TControlAttributesConfigPtr ControlAttributesConfig_;
+ const int KeyColumnCount_;
+
+ const std::unique_ptr<NTableClient::TNameTableReader> NameTableReader_;
+
+ NTableClient::TLegacyOwningKey LastKey_;
+ NTableClient::TLegacyKey CurrentKey_;
+
+ TSchemalessFormatWriterBase(
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ TBlobOutput* GetOutputStream();
+
+ void TryFlushBuffer(bool force);
+ virtual void FlushWriter();
+
+ virtual void DoWrite(TRange<NTableClient::TUnversionedRow> rows) = 0;
+
+ bool CheckKeySwitch(NTableClient::TUnversionedRow row, bool isLastRow);
+
+ bool IsSystemColumnId(int id) const;
+ bool IsTableIndexColumnId(int id) const;
+ bool IsRangeIndexColumnId(int id) const;
+ bool IsRowIndexColumnId(int id) const;
+ bool IsTabletIndexColumnId(int id) const;
+
+ int GetRangeIndexColumnId() const;
+ int GetRowIndexColumnId() const;
+ int GetTableIndexColumnId() const;
+ int GetTabletIndexColumnId() const;
+
+ // This is suitable only for switch-based control attributes,
+ // e.g. in such formats as YAMR or YSON.
+ void WriteControlAttributes(NTableClient::TUnversionedRow row);
+ virtual void WriteTableIndex(i64 tableIndex);
+ virtual void WriteRangeIndex(i64 rangeIndex);
+ virtual void WriteRowIndex(i64 rowIndex);
+ virtual void WriteTabletIndex(i64 tabletIndex);
+ virtual void WriteEndOfStream();
+
+ bool HasError() const;
+ const TError& GetError() const;
+ void RegisterError(const TError& error);
+
+private:
+ TBlobOutput CurrentBuffer_;
+ TSharedRef PreviousBuffer_;
+
+ int RowIndexId_ = -1;
+ int RangeIndexId_ = -1;
+ int TableIndexId_ = -1;
+ int TabletIndexId_ = -1;
+
+ i64 RangeIndex_ = std::numeric_limits<i64>::min();
+ i64 TableIndex_ = std::numeric_limits<i64>::min();
+ i64 RowIndex_ = std::numeric_limits<i64>::min();
+ i64 TabletIndex_ = std::numeric_limits<i64>::min();
+
+ bool EnableRowControlAttributes_;
+
+ TError Error_;
+
+ i64 WrittenSize_ = 0;
+
+ bool Closed_ = false;
+
+ void DoFlushBuffer();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterAdapter
+ : public TSchemalessFormatWriterBase
+{
+public:
+ TSchemalessWriterAdapter(
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ void Init(const std::vector<NTableClient::TTableSchemaPtr>& tableSchemas, const TFormat& format);
+
+private:
+ template <class T>
+ void WriteControlAttribute(
+ NTableClient::EControlAttribute controlAttribute,
+ T value);
+
+ void ConsumeRow(NTableClient::TUnversionedRow row);
+
+ void DoWrite(TRange<NTableClient::TUnversionedRow> rows) override;
+ void FlushWriter() override;
+
+ void WriteTableIndex(i64 tableIndex) override;
+ void WriteRangeIndex(i64 rangeIndex) override;
+ void WriteRowIndex(i64 rowIndex) override;
+ void WriteTabletIndex(i64 tabletIndex) override;
+ void WriteEndOfStream() override;
+
+private:
+ std::vector<TUnversionedValueYsonWriter> ValueWriters_;
+ std::unique_ptr<NYson::IFlushableYsonConsumer> Consumer_;
+ i64 CurrentTableIndex_ = 0;
+ bool SkipNullValues_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_parser.cpp b/yt/yt/client/formats/skiff_parser.cpp
new file mode 100644
index 0000000000..8b2d71238b
--- /dev/null
+++ b/yt/yt/client/formats/skiff_parser.cpp
@@ -0,0 +1,631 @@
+#include "skiff_parser.h"
+#include "skiff_yson_converter.h"
+
+#include "helpers.h"
+#include "parser.h"
+#include "yson_map_to_unversioned_value.h"
+
+#include <yt/yt/library/decimal/decimal.h>
+#include <yt/yt/library/skiff_ext/schema_match.h>
+#include <yt/yt/library/skiff_ext/parser.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/table_consumer.h>
+#include <yt/yt/client/table_client/value_consumer.h>
+
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <util/generic/strbuf.h>
+#include <util/stream/zerocopy.h>
+#include <util/stream/buffer.h>
+
+namespace NYT::NFormats {
+
+using namespace NTableClient;
+using namespace NSkiff;
+using namespace NSkiffExt;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TSkiffToUnversionedValueConverter = std::function<void(TCheckedInDebugSkiffParser*, IValueConsumer*)>;
+
+template <bool IsNullable, typename TFunction>
+class TPrimitiveTypeConverter
+{
+public:
+ explicit TPrimitiveTypeConverter(ui32 columnId, TFunction function = {})
+ : ColumnId_(columnId)
+ , Function_(function)
+ { }
+
+ void operator() (TCheckedInDebugSkiffParser* parser, IValueConsumer* valueConsumer)
+ {
+ if constexpr (IsNullable) {
+ ui8 tag = parser->ParseVariant8Tag();
+ if (tag == 0) {
+ valueConsumer->OnValue(MakeUnversionedNullValue(ColumnId_));
+ return;
+ } else if (tag > 1) {
+ const auto name = valueConsumer->GetNameTable()->GetName(ColumnId_);
+ THROW_ERROR_EXCEPTION(
+ "Found bad variant8 tag %Qv when parsing optional field %Qv",
+ tag,
+ name);
+ }
+ }
+
+ auto value = Function_(parser);
+ using TValueType = std::decay_t<decltype(value)>;
+
+ if constexpr (std::is_same_v<TValueType, TStringBuf>) {
+ valueConsumer->OnValue(MakeUnversionedStringValue(value, ColumnId_));
+ } else if constexpr (
+ std::is_same_v<TValueType, i8> ||
+ std::is_same_v<TValueType, i16> ||
+ std::is_same_v<TValueType, i32> ||
+ std::is_same_v<TValueType, i64>)
+ {
+ valueConsumer->OnValue(MakeUnversionedInt64Value(value, ColumnId_));
+ } else if constexpr (
+ std::is_same_v<TValueType, ui8> ||
+ std::is_same_v<TValueType, ui16> ||
+ std::is_same_v<TValueType, ui32> ||
+ std::is_same_v<TValueType, ui64>)
+ {
+ valueConsumer->OnValue(MakeUnversionedUint64Value(value, ColumnId_));
+ } else if constexpr (std::is_same_v<TValueType, bool>) {
+ valueConsumer->OnValue(MakeUnversionedBooleanValue(value, ColumnId_));
+ } else if constexpr (std::is_same_v<TValueType, double>) {
+ valueConsumer->OnValue(MakeUnversionedDoubleValue(value, ColumnId_));
+ } else if constexpr (std::is_same_v<TValueType, std::nullptr_t>) {
+ valueConsumer->OnValue(MakeUnversionedNullValue(ColumnId_));
+ } else {
+ static_assert(std::is_same_v<TValueType, TStringBuf>);
+ }
+ }
+
+private:
+ ui32 ColumnId_;
+ TFunction Function_;
+};
+
+template <bool IsNullable, typename TFunction>
+TPrimitiveTypeConverter<IsNullable, TFunction> CreatePrimitiveTypeConverter(ui32 columnId, TFunction function)
+{
+ return TPrimitiveTypeConverter<IsNullable, TFunction>(columnId, function);
+}
+
+template<bool isNullable>
+class TYson32TypeConverterImpl
+{
+public:
+ explicit TYson32TypeConverterImpl(ui16 columnId, TYsonToUnversionedValueConverter* ysonConverter)
+ : ColumnId_(columnId)
+ , YsonConverter_(ysonConverter)
+ {}
+
+ void operator()(TCheckedInDebugSkiffParser* parser, IValueConsumer* valueConsumer)
+ {
+ if constexpr (isNullable) {
+ ui8 tag = parser->ParseVariant8Tag();
+ if (tag == 0) {
+ valueConsumer->OnValue(MakeUnversionedNullValue(ColumnId_));
+ return;
+ } else if (tag > 1) {
+ const auto name = valueConsumer->GetNameTable()->GetName(ColumnId_);
+ THROW_ERROR_EXCEPTION(
+ "Found bad variant8 tag %Qv when parsing optional field %Qv",
+ tag,
+ name);
+ }
+ }
+ YT_VERIFY(YsonConverter_);
+ auto ysonString = parser->ParseYson32();
+ YsonConverter_->SetColumnIndex(ColumnId_);
+ {
+ auto consumer = YsonConverter_->SwitchToTable(0);
+ YT_VERIFY(consumer == valueConsumer);
+ }
+ ParseYsonStringBuffer(ysonString, NYson::EYsonType::Node, YsonConverter_);
+ }
+
+private:
+ const ui16 ColumnId_;
+ TYsonToUnversionedValueConverter* YsonConverter_ = nullptr;
+};
+
+TSkiffToUnversionedValueConverter CreatePrimitiveTypeConverter(
+ EWireType wireType,
+ bool required,
+ int columnId,
+ TYsonToUnversionedValueConverter* ysonConverter)
+{
+ switch (wireType) {
+#define CASE(x) \
+ case ((x)): \
+ do { \
+ if (required) { \
+ return TPrimitiveTypeConverter<false, TSimpleSkiffParser<(x)>>(columnId); \
+ } else { \
+ return TPrimitiveTypeConverter<true, TSimpleSkiffParser<(x)>>(columnId); \
+ } \
+ } while (0)
+ CASE(EWireType::Int8);
+ CASE(EWireType::Int16);
+ CASE(EWireType::Int32);
+ CASE(EWireType::Int64);
+ CASE(EWireType::Uint8);
+ CASE(EWireType::Uint16);
+ CASE(EWireType::Uint32);
+ CASE(EWireType::Uint64);
+ CASE(EWireType::Boolean);
+ CASE(EWireType::Double);
+ CASE(EWireType::String32);
+ CASE(EWireType::Nothing);
+#undef CASE
+ case EWireType::Yson32:
+ if (required) {
+ return TYson32TypeConverterImpl<false>(columnId, ysonConverter);
+ } else {
+ return TYson32TypeConverterImpl<true>(columnId, ysonConverter);
+ }
+ default:
+ break;
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSkiffToUnversionedValueConverter CreateSimpleValueConverter(
+ ESimpleLogicalValueType columnType,
+ const TFieldDescription& fieldDescription,
+ ui16 columnId,
+ TYsonToUnversionedValueConverter* ysonConverter)
+{
+ EWireType wireType = fieldDescription.ValidatedSimplify();
+ bool required = fieldDescription.IsRequired();
+ switch (columnType) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+
+ case ESimpleLogicalValueType::Interval:
+ CheckWireType(
+ wireType,
+ {EWireType::Int8, EWireType::Int16, EWireType::Int32, EWireType::Int64, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ CheckWireType(
+ wireType,
+ {EWireType::Uint8, EWireType::Uint16, EWireType::Uint32, EWireType::Uint64, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::Utf8:
+ CheckWireType(wireType, {EWireType::String32, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ CheckWireType(wireType, {EWireType::Double, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::Boolean:
+ CheckWireType(wireType, {EWireType::Boolean, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::Any:
+ CheckWireType(
+ wireType,
+ {
+ EWireType::Int8,
+ EWireType::Int16,
+ EWireType::Int32,
+ EWireType::Int64,
+
+ EWireType::Uint8,
+ EWireType::Uint16,
+ EWireType::Uint32,
+ EWireType::Uint64,
+
+ EWireType::Double,
+ EWireType::Boolean,
+ EWireType::String32,
+ EWireType::Nothing,
+ EWireType::Yson32
+ });
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ CheckWireType(wireType, {EWireType::Nothing, EWireType::Yson32});
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+ case ESimpleLogicalValueType::Uuid:
+ CheckWireType(wireType, {EWireType::Uint128, EWireType::String32, EWireType::Yson32});
+ if (wireType == EWireType::Uint128) {
+ if (fieldDescription.IsNullable()) {
+ return CreatePrimitiveTypeConverter<true>(columnId, TUuidParser());
+ } else {
+ return CreatePrimitiveTypeConverter<false>(columnId, TUuidParser());
+ }
+ } else {
+ return CreatePrimitiveTypeConverter(wireType, required, columnId, ysonConverter);
+ }
+ }
+}
+
+TSkiffToUnversionedValueConverter CreateDecimalValueConverter(
+ const TFieldDescription& fieldDescription,
+ ui16 columnId,
+ const TDecimalLogicalType& denullifiedType,
+ TYsonToUnversionedValueConverter* ysonConverter)
+{
+const auto precision = denullifiedType.GetPrecision();
+ const auto wireType = fieldDescription.ValidatedSimplify();
+ switch (wireType) {
+#define CASE(x) \
+ case x: \
+ do { \
+ if (fieldDescription.IsNullable()) { \
+ return CreatePrimitiveTypeConverter<true>(columnId, TDecimalSkiffParser<x>(precision)); \
+ } else { \
+ return CreatePrimitiveTypeConverter<false>(columnId, TDecimalSkiffParser<x>(precision)); \
+ } \
+ } while (0)
+ CASE(EWireType::Int32);
+ CASE(EWireType::Int64);
+ CASE(EWireType::Int128);
+#undef CASE
+ case EWireType::Yson32:
+ return CreatePrimitiveTypeConverter(wireType, fieldDescription.IsRequired(), columnId, ysonConverter);
+ default:
+ CheckSkiffWireTypeForDecimal(precision, wireType);
+ YT_ABORT();
+ }
+}
+
+class TComplexValueConverter
+{
+public:
+ TComplexValueConverter(TSkiffToYsonConverter converter, ui16 columnId)
+ : Converter_(std::move(converter))
+ , ColumnId_(columnId)
+ { }
+
+ void operator() (TCheckedInDebugSkiffParser* parser, IValueConsumer* valueConsumer)
+ {
+ Buffer_.Clear();
+ {
+ TBufferOutput out(Buffer_);
+ NYson::TCheckedInDebugYsonTokenWriter ysonTokenWriter(&out);
+ Converter_(parser, &ysonTokenWriter);
+ ysonTokenWriter.Finish();
+ }
+ auto value = TStringBuf(Buffer_.Data(), Buffer_.Size());
+ constexpr TStringBuf entity = "#";
+ if (value == entity) {
+ valueConsumer->OnValue(MakeUnversionedNullValue(ColumnId_));
+ } else {
+ valueConsumer->OnValue(MakeUnversionedCompositeValue(value, ColumnId_));
+ }
+ }
+
+private:
+ const TSkiffToYsonConverter Converter_;
+ const ui16 ColumnId_;
+ TBuffer Buffer_;
+};
+
+TSkiffToUnversionedValueConverter CreateComplexValueConverter(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ ui16 columnId,
+ bool sparseColumn)
+{
+ TSkiffToYsonConverterConfig config;
+ config.AllowOmitTopLevelOptional = sparseColumn;
+ auto converter = CreateSkiffToYsonConverter(descriptor, skiffSchema, config);
+ return TComplexValueConverter(converter, columnId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffParserImpl
+{
+
+public:
+ TSkiffParserImpl(
+ std::shared_ptr<TSkiffSchema> skiffSchema,
+ const TTableSchemaPtr& tableSchema,
+ IValueConsumer* valueConsumer)
+ : SkiffSchemaList_({std::move(skiffSchema)})
+ , ValueConsumer_(valueConsumer)
+ , YsonToUnversionedValueConverter_(TYsonConverterConfig(), ValueConsumer_)
+ , OtherColumnsConsumer_(TYsonConverterConfig(), ValueConsumer_)
+ {
+ THashMap<TString, const TColumnSchema*> columnSchemas;
+ for (const auto& column : tableSchema->Columns()) {
+ columnSchemas[column.Name()] = &column;
+ }
+
+ auto genericTableDescriptions = CreateTableDescriptionList(
+ SkiffSchemaList_, RangeIndexColumnName, RowIndexColumnName);
+
+ for (int tableIndex = 0; tableIndex < std::ssize(genericTableDescriptions); ++tableIndex) {
+ const auto& genericTableDescription = genericTableDescriptions[tableIndex];
+ auto& parserTableDescription = TableDescriptions_.emplace_back();
+ parserTableDescription.HasOtherColumns = genericTableDescription.HasOtherColumns;
+ for (const auto& fieldDescription : genericTableDescription.DenseFieldDescriptionList) {
+ const auto columnId = ValueConsumer_->GetNameTable()->GetIdOrRegisterName(fieldDescription.Name());
+ auto columnSchema = columnSchemas.FindPtr(fieldDescription.Name());
+ TSkiffToUnversionedValueConverter converter;
+ try {
+ converter = CreateSkiffToUnversionedValueConverter(
+ columnId,
+ columnSchema == nullptr ? nullptr : *columnSchema,
+ fieldDescription,
+ /*sparseColumn*/ false);
+ } catch (const std::exception& ex) {
+ auto logicalType = OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any));
+ if (columnSchema && *columnSchema) {
+ logicalType = (*columnSchema)->LogicalType();
+ }
+ THROW_ERROR_EXCEPTION("Cannot create Skiff parser for table #%v",
+ tableIndex)
+ << TErrorAttribute("logical_type", logicalType)
+ << ex;
+ }
+ parserTableDescription.DenseFieldConverters.emplace_back(converter);
+ }
+
+ for (const auto& fieldDescription : genericTableDescription.SparseFieldDescriptionList) {
+ const auto columnId = ValueConsumer_->GetNameTable()->GetIdOrRegisterName(fieldDescription.Name());
+ TSkiffToUnversionedValueConverter converter;
+ auto columnSchema = columnSchemas.FindPtr(fieldDescription.Name());
+ try {
+ converter = CreateSkiffToUnversionedValueConverter(
+ columnId,
+ columnSchema == nullptr ? nullptr : *columnSchema,
+ fieldDescription,
+ /*sparseColumn*/ true);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Cannot create Skiff parser for table #%v",
+ tableIndex)
+ << ex;
+ }
+ parserTableDescription.SparseFieldConverters.emplace_back(converter);
+ }
+ }
+ }
+
+ void DoParse(IZeroCopyInput* stream)
+ {
+ Parser_ = std::make_unique<TCheckedInDebugSkiffParser>(CreateVariant16Schema(SkiffSchemaList_), stream);
+
+ while (Parser_->HasMoreData()) {
+ auto tag = Parser_->ParseVariant16Tag();
+ if (tag > 0) {
+ THROW_ERROR_EXCEPTION("Unknown table index variant16 tag %v",
+ tag);
+ }
+ ValueConsumer_->OnBeginRow();
+
+ for (const auto& converter : TableDescriptions_[tag].DenseFieldConverters) {
+ converter(Parser_.get(), ValueConsumer_);
+ }
+
+ if (!TableDescriptions_[tag].SparseFieldConverters.empty()) {
+ for (auto sparseFieldIdx = Parser_->ParseVariant16Tag();
+ sparseFieldIdx != EndOfSequenceTag<ui16>();
+ sparseFieldIdx = Parser_->ParseVariant16Tag()) {
+ if (sparseFieldIdx >= TableDescriptions_[tag].SparseFieldConverters.size()) {
+ THROW_ERROR_EXCEPTION("Bad sparse field index %Qv, total sparse field count %Qv",
+ sparseFieldIdx,
+ TableDescriptions_[tag].SparseFieldConverters.size());
+ }
+
+ const auto& converter = TableDescriptions_[tag].SparseFieldConverters[sparseFieldIdx];
+ converter(Parser_.get(), ValueConsumer_);
+ }
+ }
+
+ if (TableDescriptions_[tag].HasOtherColumns) {
+ auto buf = Parser_->ParseYson32();
+ ParseYsonStringBuffer(
+ buf,
+ NYson::EYsonType::Node,
+ &OtherColumnsConsumer_);
+ }
+
+ ValueConsumer_->OnEndRow();
+ }
+ }
+
+ ui64 GetReadBytesCount()
+ {
+ return Parser_->GetReadBytesCount();
+ }
+
+private:
+ TSkiffToUnversionedValueConverter CreateSkiffToUnversionedValueConverter(
+ int columnId,
+ const TColumnSchema* columnSchema,
+ const TFieldDescription& skiffField,
+ bool sparseColumn)
+ {
+ const auto columnType = columnSchema ?
+ columnSchema->LogicalType() :
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any));
+
+ const auto denullifiedLogicalType = DenullifyLogicalType(columnType);
+
+ try {
+ switch (denullifiedLogicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CreateSimpleValueConverter(
+ denullifiedLogicalType->AsSimpleTypeRef().GetElement(),
+ skiffField,
+ columnId,
+ &YsonToUnversionedValueConverter_);
+ case ELogicalMetatype::Decimal:
+ return CreateDecimalValueConverter(
+ skiffField,
+ columnId,
+ denullifiedLogicalType->AsDecimalTypeRef(),
+ &YsonToUnversionedValueConverter_);
+ case ELogicalMetatype::Optional:
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::Dict:
+ return CreateComplexValueConverter(
+ TComplexTypeFieldDescriptor(skiffField.Name(), columnType),
+ skiffField.Schema(),
+ columnId,
+ /*sparseColumn*/ sparseColumn);
+ case ELogicalMetatype::Tagged:
+ // denullified type should not contain tagged type
+ break;
+ }
+ YT_ABORT();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Cannot create Skiff parser for column %Qv",
+ skiffField.Name())
+ << ex;
+ }
+ }
+
+private:
+ struct TTableDescription
+ {
+ std::vector<TSkiffToUnversionedValueConverter> DenseFieldConverters;
+ std::vector<TSkiffToUnversionedValueConverter> SparseFieldConverters;
+ bool HasOtherColumns = false;
+ };
+
+ const TSkiffSchemaList SkiffSchemaList_;
+
+ IValueConsumer* const ValueConsumer_;
+
+ TYsonToUnversionedValueConverter YsonToUnversionedValueConverter_;
+ TYsonMapToUnversionedValueConverter OtherColumnsConsumer_;
+
+ std::unique_ptr<TCheckedInDebugSkiffParser> Parser_;
+ std::vector<TTableDescription> TableDescriptions_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffPushParser
+ : public IParser
+{
+public:
+ TSkiffPushParser(
+ std::shared_ptr<TSkiffSchema> skiffSchema,
+ const TTableSchemaPtr& tableSchema,
+ IValueConsumer* consumer)
+ : ParserImpl_(std::make_unique<TSkiffParserImpl>(
+ std::move(skiffSchema),
+ tableSchema,
+ consumer))
+ , ParserCoroPipe_(
+ BIND([this] (IZeroCopyInput* stream) {
+ ParserImpl_->DoParse(stream);
+ }))
+ {}
+
+ void Read(TStringBuf data) override
+ {
+ if (!data.empty()) {
+ ParserCoroPipe_.Feed(data);
+ }
+ }
+
+ void Finish() override
+ {
+ ParserCoroPipe_.Finish();
+ }
+
+private:
+ std::unique_ptr<TSkiffParserImpl> ParserImpl_;
+ TCoroPipe ParserCoroPipe_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace // anonymous
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ std::shared_ptr<TSkiffSchema> skiffSchema,
+ const TTableSchemaPtr& tableSchema,
+ IValueConsumer* consumer)
+{
+ auto tableDescriptionList = CreateTableDescriptionList({skiffSchema}, RangeIndexColumnName, RowIndexColumnName);
+ if (tableDescriptionList.size() != 1) {
+ THROW_ERROR_EXCEPTION("Expected to have single table, actual table description count %Qv",
+ tableDescriptionList.size());
+ }
+ return std::make_unique<TSkiffPushParser>(
+ std::move(skiffSchema),
+ tableSchema,
+ consumer);
+}
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ IValueConsumer* consumer,
+ const std::vector<std::shared_ptr<TSkiffSchema>>& skiffSchemas,
+ const TSkiffFormatConfigPtr& config,
+ int tableIndex)
+{
+ if (tableIndex >= static_cast<int>(skiffSchemas.size())) {
+ THROW_ERROR_EXCEPTION("Skiff format config does not describe table #%v",
+ tableIndex);
+ }
+ if (tableIndex == 0 && config->OverrideIntermediateTableSchema) {
+ if (!IsTrivialIntermediateSchema(*consumer->GetSchema())) {
+ THROW_ERROR_EXCEPTION("Cannot use \"override_intermediate_table_schema\" since output table #0 has nontrivial schema")
+ << TErrorAttribute("schema", *consumer->GetSchema());
+ }
+ return CreateParserForSkiff(
+ skiffSchemas[tableIndex],
+ New<TTableSchema>(*config->OverrideIntermediateTableSchema),
+ consumer);
+ } else {
+ return CreateParserForSkiff(
+ skiffSchemas[tableIndex],
+ consumer);
+ }
+}
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ std::shared_ptr<TSkiffSchema> skiffSchema,
+ IValueConsumer* consumer)
+{
+ return CreateParserForSkiff(std::move(skiffSchema), consumer->GetSchema(), consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_parser.h b/yt/yt/client/formats/skiff_parser.h
new file mode 100644
index 0000000000..35cea6666e
--- /dev/null
+++ b/yt/yt/client/formats/skiff_parser.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <library/cpp/skiff/skiff.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ std::shared_ptr<NSkiff::TSkiffSchema> skiffSchema,
+ NTableClient::IValueConsumer* consumer);
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ std::shared_ptr<NSkiff::TSkiffSchema> skiffSchema,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ NTableClient::IValueConsumer* consumer);
+
+std::unique_ptr<IParser> CreateParserForSkiff(
+ NTableClient::IValueConsumer* consumer,
+ const std::vector<std::shared_ptr<NSkiff::TSkiffSchema>>& skiffSchemas,
+ const TSkiffFormatConfigPtr& config,
+ int tableIndex);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_writer.cpp b/yt/yt/client/formats/skiff_writer.cpp
new file mode 100644
index 0000000000..1eaf09ce8d
--- /dev/null
+++ b/yt/yt/client/formats/skiff_writer.cpp
@@ -0,0 +1,1112 @@
+#include "skiff_writer.h"
+
+#include "public.h"
+#include "schemaless_writer_adapter.h"
+#include "skiff_yson_converter.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/library/skiff_ext/schema_match.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/stream/buffer.h>
+
+#include <functional>
+#include <utility>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NSkiff;
+using namespace NSkiffExt;
+using namespace NTableClient;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr int MissingSystemColumn = -1;
+static constexpr int MissingRowRangeIndexTag = 2;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ESkiffWriterColumnType,
+ (Unknown)
+ (Dense)
+ (Sparse)
+ (Skip)
+ (RangeIndex)
+ (RowIndex)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+void ResizeToContainIndex(std::vector<T>* vec, size_t index)
+{
+ if (vec->size() < index + 1) {
+ vec->resize(index + 1);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIndexedSchemas
+{
+public:
+ explicit TIndexedSchemas(const std::vector<TTableSchemaPtr>& tableSchemas)
+ {
+ for (size_t tableIndex = 0; tableIndex < tableSchemas.size(); ++tableIndex) {
+ const auto& columns = tableSchemas[tableIndex]->Columns();
+ for (const auto& column : columns) {
+ Columns_[std::pair<int,TString>(tableIndex, column.Name())] = column;
+ }
+ }
+ }
+
+ const TColumnSchema* GetColumnSchema(int tableIndex, TStringBuf columnName) const
+ {
+ auto it = Columns_.find(std::pair<int,TString>(tableIndex, columnName));
+ if (it == Columns_.end()) {
+ return nullptr;
+ } else {
+ return &it->second;
+ }
+ }
+
+private:
+ // (TableIndex, ColumnName) -> ColumnSchema
+ THashMap<std::pair<int, TString>, TColumnSchema> Columns_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWriteContext
+{
+ TNameTablePtr NameTable;
+ TUnversionedValueYsonWriter* UnversionedValueYsonConverter = nullptr;
+ TBuffer* TmpBuffer = nullptr;
+};
+
+using TUnversionedValueToSkiffConverter = std::function<void(const TUnversionedValue&, TCheckedInDebugSkiffWriter*, TWriteContext*)>;
+
+template <EWireType wireType>
+constexpr EValueType WireTypeToValueType()
+{
+ if constexpr (wireType == EWireType::Int8 ||
+ wireType == EWireType::Int16 ||
+ wireType == EWireType::Int32 ||
+ wireType == EWireType::Int64)
+ {
+ return EValueType::Int64;
+ } else if constexpr (
+ wireType == EWireType::Uint8 ||
+ wireType == EWireType::Uint16 ||
+ wireType == EWireType::Uint32 ||
+ wireType == EWireType::Uint64)
+ {
+ return EValueType::Uint64;
+ } else if constexpr (wireType == EWireType::Double) {
+ return EValueType::Double;
+ } else if constexpr (wireType == EWireType::Boolean) {
+ return EValueType::Boolean;
+ } else if constexpr (wireType == EWireType::String32) {
+ return EValueType::String;
+ } else if constexpr (wireType == EWireType::Nothing) {
+ return EValueType::Null;
+ } else {
+ // Not compilable.
+ static_assert(wireType == EWireType::Int64, "Bad wireType");
+ }
+}
+
+template<EWireType wireType, bool isOptional>
+void ConvertSimpleValueImpl(const TUnversionedValue& value, TCheckedInDebugSkiffWriter* writer, TWriteContext* context)
+{
+ if constexpr (isOptional) {
+ if (value.Type == EValueType::Null) {
+ writer->WriteVariant8Tag(0);
+ return;
+ } else {
+ writer->WriteVariant8Tag(1);
+ }
+ }
+
+ if constexpr (wireType != EWireType::Yson32) {
+ constexpr auto expectedValueType = WireTypeToValueType<wireType>();
+ if (value.Type != expectedValueType) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow,
+ "Unexpected type of %Qv column: Skiff format expected %Qlv, actual table type %Qlv",
+ context->NameTable->GetName(value.Id),
+ expectedValueType,
+ value.Type);
+ }
+ }
+
+ if constexpr (wireType == EWireType::Int8) {
+ CheckIntSize<wireType>(value.Data.Int64);
+ writer->WriteInt8(value.Data.Int64);
+ } else if constexpr (wireType == EWireType::Int16) {
+ CheckIntSize<wireType>(value.Data.Int64);
+ writer->WriteInt16(value.Data.Int64);
+ } else if constexpr (wireType == EWireType::Int32) {
+ CheckIntSize<wireType>(value.Data.Int64);
+ writer->WriteInt32(value.Data.Int64);
+ } else if constexpr (wireType == EWireType::Int64) {
+ writer->WriteInt64(value.Data.Int64);
+
+ } else if constexpr (wireType == EWireType::Uint8) {
+ CheckIntSize<wireType>(value.Data.Uint64);
+ writer->WriteUint8(value.Data.Uint64);
+ } else if constexpr (wireType == EWireType::Uint16) {
+ CheckIntSize<wireType>(value.Data.Uint64);
+ writer->WriteUint16(value.Data.Uint64);
+ } else if constexpr (wireType == EWireType::Uint32) {
+ CheckIntSize<wireType>(value.Data.Uint64);
+ writer->WriteUint32(value.Data.Uint64);
+ } else if constexpr (wireType == EWireType::Uint64) {
+ writer->WriteUint64(value.Data.Uint64);
+
+ } else if constexpr (wireType == EWireType::Boolean) {
+ writer->WriteBoolean(value.Data.Boolean);
+ } else if constexpr (wireType == EWireType::Double) {
+ writer->WriteDouble(value.Data.Double);
+ } else if constexpr (wireType == EWireType::String32) {
+ writer->WriteString32(value.AsStringBuf());
+ } else if constexpr (wireType == EWireType::Yson32) {
+ context->TmpBuffer->Clear();
+ {
+ TBufferOutput out(*context->TmpBuffer);
+ NYson::TYsonWriter ysonWriter(&out);
+ context->UnversionedValueYsonConverter->WriteValue(value, &ysonWriter);
+ }
+ writer->WriteYson32(TStringBuf(context->TmpBuffer->data(), context->TmpBuffer->size()));
+ } else if constexpr (wireType == EWireType::Nothing) {
+ // Do nothing.
+ } else {
+ // Not compilable.
+ static_assert(wireType == EWireType::Int64, "Bad wireType");
+ }
+}
+
+template <EValueType ExpectedValueType, bool isOptional, typename TFunc>
+class TPrimitiveConverterWrapper
+{
+public:
+ explicit TPrimitiveConverterWrapper(TFunc function)
+ : Function_(function)
+ { }
+
+ void operator()(const TUnversionedValue& value, TCheckedInDebugSkiffWriter* writer, TWriteContext* context)
+ {
+ if constexpr (isOptional) {
+ if (value.Type == EValueType::Null) {
+ writer->WriteVariant8Tag(0);
+ return;
+ } else {
+ writer->WriteVariant8Tag(1);
+ }
+ }
+ if (value.Type != ExpectedValueType) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow,
+ "Unexpected type of %Qv column: Skiff format expected %Qlv, actual table type %Qlv",
+ context->NameTable->GetName(value.Id),
+ ExpectedValueType,
+ value.Type);
+ }
+ if constexpr (ExpectedValueType == EValueType::String) {
+ Function_(value.AsStringBuf(), writer);
+ } else {
+ // TODO(ermolovd) support other types and use this class instead of ConvertSimpleValueImpl
+ // poor man's static assert false
+ static_assert(ExpectedValueType == EValueType::String);
+ }
+ }
+
+private:
+ TFunc Function_;
+};
+
+class TRowAndRangeIndexWriter
+{
+public:
+ template <ERowRangeIndexMode Mode>
+ void WriteRowIndex(const TUnversionedValue& value, TCheckedInDebugSkiffWriter* writer, TWriteContext* /*context*/)
+ {
+ if (value.Type == EValueType::Int64) {
+ const auto currentRowIndex = value.Data.Int64;
+ if (RowIndex_ + 1 == currentRowIndex) {
+ writer->WriteVariant8Tag(0);
+ } else {
+ writer->WriteVariant8Tag(1);
+ writer->WriteInt64(value.Data.Int64);
+ }
+ RowIndex_ = currentRowIndex;
+ } else if (value.Type == EValueType::Null) {
+ if constexpr (Mode == ERowRangeIndexMode::Incremental) {
+ THROW_ERROR_EXCEPTION("Row index requested but reader did not return it");
+ } else {
+ static_assert(Mode == ERowRangeIndexMode::IncrementalWithError);
+ writer->WriteVariant8Tag(MissingRowRangeIndexTag);
+ }
+ RowIndex_ = Undefined;
+ }
+ }
+
+ template <ERowRangeIndexMode Mode>
+ void WriteRangeIndex(const TUnversionedValue& value, TCheckedInDebugSkiffWriter* writer, TWriteContext* /*context*/)
+ {
+ if (value.Type == EValueType::Int64) {
+ const auto currentRangeIndex = value.Data.Int64;
+ if (RangeIndex_ == currentRangeIndex) {
+ writer->WriteVariant8Tag(0);
+ } else {
+ writer->WriteVariant8Tag(1);
+ writer->WriteInt64(currentRangeIndex);
+ }
+ RangeIndex_ = currentRangeIndex;
+ } else if (value.Type == EValueType::Null) {
+ if constexpr (Mode == ERowRangeIndexMode::Incremental) {
+ THROW_ERROR_EXCEPTION("Range index requested but reader did not return it");
+ } else {
+ static_assert(Mode == ERowRangeIndexMode::IncrementalWithError);
+ writer->WriteVariant8Tag(MissingRowRangeIndexTag);
+ }
+ }
+ }
+
+ Y_FORCE_INLINE void PrepareTableIndex(i64 tableIndex)
+ {
+ if (TableIndex_ != tableIndex) {
+ TableIndex_ = tableIndex;
+ RowIndex_ = Undefined;
+ RangeIndex_ = Undefined;
+ }
+ }
+
+ Y_FORCE_INLINE void PrepareRangeIndex(i64 rangeIndex)
+ {
+ if (rangeIndex != RangeIndex_) {
+ RangeIndex_ = Undefined;
+ RowIndex_ = Undefined;
+ }
+ }
+
+ Y_FORCE_INLINE void ResetRangeIndex()
+ {
+ PrepareRangeIndex(Undefined);
+ }
+
+ Y_FORCE_INLINE void ResetRowIndex()
+ {
+ RowIndex_ = Undefined;
+ }
+
+private:
+ static constexpr i64 Undefined = -2;
+
+ i64 TableIndex_ = Undefined;
+ i64 RangeIndex_ = Undefined;
+ i64 RowIndex_ = Undefined;
+};
+
+TUnversionedValueToSkiffConverter CreateMissingCompositeValueConverter(TString name) {
+ return [name=std::move(name)] (const TUnversionedValue& value, TCheckedInDebugSkiffWriter* writer, TWriteContext*) {
+ if (value.Type != EValueType::Null) {
+ THROW_ERROR_EXCEPTION("Cannot represent nonnull value of column %Qv absent in schema as composite Skiff value",
+ name);
+ }
+ writer->WriteVariant8Tag(0);
+ };
+}
+
+template <EValueType ExpectedValueType, typename TFunction>
+TUnversionedValueToSkiffConverter CreatePrimitiveValueConverter(
+ bool required,
+ TFunction function)
+{
+ if (required) {
+ return TPrimitiveConverterWrapper<ExpectedValueType, false, TFunction>(std::move(function));
+ } else {
+ return TPrimitiveConverterWrapper<ExpectedValueType, true, TFunction>(std::move(function));
+ }
+}
+
+TUnversionedValueToSkiffConverter CreatePrimitiveValueConverter(EWireType wireType, bool required)
+{
+ switch (wireType) {
+#define CASE(t) \
+ case t: \
+ return required ? ConvertSimpleValueImpl<t, false> : ConvertSimpleValueImpl<t, true>;
+ CASE(EWireType::Int8)
+ CASE(EWireType::Int16)
+ CASE(EWireType::Int32)
+ CASE(EWireType::Int64)
+ CASE(EWireType::Uint8)
+ CASE(EWireType::Uint16)
+ CASE(EWireType::Uint32)
+ CASE(EWireType::Uint64)
+ CASE(EWireType::Double)
+ CASE(EWireType::Boolean)
+ CASE(EWireType::String32)
+ CASE(EWireType::Yson32)
+#undef CASE
+ case EWireType::Nothing:
+ // TODO(ermolovd): we should use `isOptional` instead of `required` (with corresponding condition inversion).
+ YT_VERIFY(required);
+ return ConvertSimpleValueImpl<EWireType::Nothing, false>;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+TUnversionedValueToSkiffConverter CreateSimpleValueConverter(
+ EWireType wireType,
+ bool required,
+ ESimpleLogicalValueType logicalType)
+{
+ switch (logicalType) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+
+ case ESimpleLogicalValueType::Interval:
+ CheckWireType(wireType, {EWireType::Int8, EWireType::Int16, EWireType::Int32, EWireType::Int64, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ CheckWireType(wireType, {EWireType::Uint8, EWireType::Uint16, EWireType::Uint32, EWireType::Uint64, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ CheckWireType(wireType, {EWireType::Double, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Boolean:
+ CheckWireType(wireType, {EWireType::Boolean, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::String:
+ CheckWireType(wireType, {EWireType::String32, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Any:
+ CheckWireType(wireType, {
+ EWireType::Int8,
+ EWireType::Int16,
+ EWireType::Int32,
+ EWireType::Int64,
+
+ EWireType::Uint8,
+ EWireType::Uint16,
+ EWireType::Uint32,
+ EWireType::Uint64,
+
+ EWireType::String32,
+ EWireType::Boolean,
+ EWireType::Double,
+ EWireType::Nothing,
+ EWireType::Yson32
+ });
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ CheckWireType(wireType, {EWireType::Nothing, EWireType::Yson32});
+ return CreatePrimitiveValueConverter(wireType, required);
+
+ case ESimpleLogicalValueType::Uuid:
+ CheckWireType(wireType, {EWireType::Uint128, EWireType::String32, EWireType::Yson32});
+ if (wireType == EWireType::Uint128) {
+ return CreatePrimitiveValueConverter<EValueType::String>(required, TUuidWriter());
+ } else {
+ return CreatePrimitiveValueConverter(wireType, required);
+ }
+ }
+}
+
+TUnversionedValueToSkiffConverter CreateComplexValueConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ bool isSparse)
+{
+ TYsonToSkiffConverterConfig config;
+ config.AllowOmitTopLevelOptional = isSparse;
+ auto ysonToSkiff = CreateYsonToSkiffConverter(descriptor, skiffSchema, config);
+ return [ysonToSkiff=ysonToSkiff] (const TUnversionedValue& value, TCheckedInDebugSkiffWriter* skiffWriter, TWriteContext* /*context*/) {
+ TMemoryInput input;
+ if (value.Type == EValueType::Any || value.Type == EValueType::Composite) {
+ // NB. value.Type might be EValueType::Any if user has used override_intermediate_table_schema
+ input.Reset(value.Data.String, value.Length);
+ } else if (value.Type == EValueType::Null) {
+ static const TStringBuf empty = "#";
+ input.Reset(empty.Data(), empty.Size());
+ } else {
+ THROW_ERROR_EXCEPTION("Internal error; unexpected value type: expected %Qlv or %Qlv, actual %Qlv",
+ EValueType::Composite,
+ EValueType::Null,
+ value.Type);
+ }
+ NYson::TYsonPullParser parser(&input, NYson::EYsonType::Node);
+ NYson::TYsonPullParserCursor cursor(&parser);
+ ysonToSkiff(&cursor, skiffWriter);
+ };
+}
+
+TUnversionedValueToSkiffConverter CreateDecimalValueConverter(
+ const TFieldDescription& field,
+ const TDecimalLogicalType& logicalType)
+{
+ bool isRequired = field.IsRequired();
+ int precision = logicalType.GetPrecision();
+ auto wireType = field.ValidatedSimplify();
+ switch (wireType) {
+ case EWireType::Int32:
+ return CreatePrimitiveValueConverter<EValueType::String>(
+ isRequired,
+ TDecimalSkiffWriter<EWireType::Int32>(precision));
+ case EWireType::Int64:
+ return CreatePrimitiveValueConverter<EValueType::String>(
+ isRequired,
+ TDecimalSkiffWriter<EWireType::Int64>(precision));
+ case EWireType::Int128:
+ return CreatePrimitiveValueConverter<EValueType::String>(
+ isRequired,
+ TDecimalSkiffWriter<EWireType::Int128>(precision));
+ case EWireType::Yson32:
+ return CreatePrimitiveValueConverter(wireType, isRequired);
+ default:
+ CheckSkiffWireTypeForDecimal(precision, wireType);
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSkiffEncodingInfo
+{
+ ESkiffWriterColumnType EncodingPart = ESkiffWriterColumnType::Unknown;
+
+ // Converter is set only for sparse part.
+ TUnversionedValueToSkiffConverter Converter;
+
+ // FieldIndex is index of field inside Skiff tuple for dense part of the row
+ // and variant tag for sparse part of the row.
+ ui32 FieldIndex = 0;
+
+ TSkiffEncodingInfo() = default;
+
+ static TSkiffEncodingInfo Skip()
+ {
+ TSkiffEncodingInfo result;
+ result.EncodingPart = ESkiffWriterColumnType::Skip;
+ return result;
+ }
+
+ static TSkiffEncodingInfo RangeIndex(ui32 fieldIndex)
+ {
+ TSkiffEncodingInfo result;
+ result.EncodingPart = ESkiffWriterColumnType::RangeIndex;
+ result.FieldIndex = fieldIndex;
+ return result;
+ }
+
+ static TSkiffEncodingInfo RowIndex(ui32 fieldIndex)
+ {
+ TSkiffEncodingInfo result;
+ result.EncodingPart = ESkiffWriterColumnType::RowIndex;
+ result.FieldIndex = fieldIndex;
+ return result;
+ }
+
+ static TSkiffEncodingInfo Dense(ui32 fieldIndex)
+ {
+ TSkiffEncodingInfo result;
+ result.EncodingPart = ESkiffWriterColumnType::Dense;
+ result.FieldIndex = fieldIndex;
+ return result;
+ }
+
+ static TSkiffEncodingInfo Sparse(TUnversionedValueToSkiffConverter converter, ui32 fieldIndex)
+ {
+ TSkiffEncodingInfo result;
+ result.EncodingPart = ESkiffWriterColumnType::Sparse;
+ result.Converter = std::move(converter);
+ result.FieldIndex = fieldIndex;
+ return result;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+struct TSparseFieldInfo
+{
+ const TUnversionedValueToSkiffConverter* Converter;
+ ui32 SparseFieldTag;
+ ui32 ValueIndex;
+
+ TSparseFieldInfo(const TUnversionedValueToSkiffConverter* converter, ui32 sparseFieldTag, ui32 valueIndex)
+ : Converter(converter)
+ , SparseFieldTag(sparseFieldTag)
+ , ValueIndex(valueIndex)
+ { }
+};
+
+struct TDenseFieldWriterInfo
+{
+ TUnversionedValueToSkiffConverter Converter;
+ ui16 ColumnId;
+
+ TDenseFieldWriterInfo(TUnversionedValueToSkiffConverter converter, ui16 columnId)
+ : Converter(std::move(converter))
+ , ColumnId(columnId)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSkiffWriterTableDescription
+{
+ std::vector<TSkiffEncodingInfo> KnownFields;
+ std::vector<TDenseFieldWriterInfo> DenseFieldInfos;
+ int KeySwitchFieldIndex = -1;
+ int RangeIndexFieldIndex = -1;
+ int RowIndexFieldIndex = -1;
+ ERowRangeIndexMode RangeIndexMode = ERowRangeIndexMode::Incremental;
+ ERowRangeIndexMode RowIndexMode = ERowRangeIndexMode::Incremental;
+ bool HasSparseColumns = false;
+ bool HasOtherColumns = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSkiffWriter
+ : public TSchemalessFormatWriterBase
+{
+public:
+ TSkiffWriter(
+ TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+ : TSchemalessFormatWriterBase(
+ std::move(nameTable),
+ std::move(output),
+ enableContextSaving,
+ std::move(controlAttributesConfig),
+ keyColumnCount)
+ { }
+
+ void Init(const std::vector<TTableSchemaPtr>& schemas, const std::vector<std::shared_ptr<TSkiffSchema>>& tableSkiffSchemas)
+ {
+ GetError().ThrowOnError();
+
+ for (const auto& schema : schemas) {
+ UnversionedValueToYsonConverter_.emplace_back(NameTable_, schema, TYsonConverterConfig());
+ }
+
+ std::shared_ptr<TSkiffSchema> streamSchema;
+ if (ControlAttributesConfig_->EnableEndOfStream) {
+ streamSchema = CreateRepeatedVariant16Schema(tableSkiffSchemas);
+ } else {
+ streamSchema = CreateVariant16Schema(tableSkiffSchemas);
+ }
+ SkiffWriter_.emplace(std::move(streamSchema), GetOutputStream());
+
+ auto indexedSchemas = TIndexedSchemas(schemas);
+
+ auto tableDescriptionList = CreateTableDescriptionList(tableSkiffSchemas, RangeIndexColumnName, RowIndexColumnName);
+ for (const auto& commonTableDescription : tableDescriptionList) {
+ auto tableIndex = TableDescriptionList_.size();
+ TableDescriptionList_.emplace_back();
+ auto& writerTableDescription = TableDescriptionList_.back();
+ writerTableDescription.HasOtherColumns = commonTableDescription.HasOtherColumns;
+ writerTableDescription.HasSparseColumns = !commonTableDescription.SparseFieldDescriptionList.empty();
+ writerTableDescription.KeySwitchFieldIndex = MissingSystemColumn;
+
+ writerTableDescription.RowIndexFieldIndex = MissingSystemColumn;
+ writerTableDescription.RowIndexMode = commonTableDescription.RowIndexMode;
+
+ writerTableDescription.RangeIndexFieldIndex = MissingSystemColumn;
+ writerTableDescription.RangeIndexMode = commonTableDescription.RangeIndexMode;
+
+ auto& knownFields = writerTableDescription.KnownFields;
+
+ const auto& denseFieldDescriptionList = commonTableDescription.DenseFieldDescriptionList;
+
+ auto& denseFieldWriterInfos = writerTableDescription.DenseFieldInfos;
+
+ auto createComplexValueConverter = [&] (const TFieldDescription& skiffField, bool isSparse) -> TUnversionedValueToSkiffConverter {
+ auto columnSchema = indexedSchemas.GetColumnSchema(tableIndex, skiffField.Name());
+
+ // NB: we don't create complex value converter for simple types
+ // (column is missing in schema or has simple type).
+ // 1. Complex value converter expects unversioned values of type ANY
+ // and simple types have other types.
+ // 2. For historical reasons we don't check Skiff schema that strictly for simple types,
+ // e.g we allow column to be optional in table schema and be required in Skiff schema
+ // (runtime check is used in such cases).
+ if (!columnSchema) {
+ if (!skiffField.Simplify() && !skiffField.IsRequired()) {
+ // NB. Special case, column is described in Skiff schema as non required complex field
+ // but is missing in schema.
+ // We expect it to be missing in whole table and return corresponding converter.
+ return CreateMissingCompositeValueConverter(skiffField.Name());
+ }
+ }
+
+ TLogicalTypePtr logicalType = OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any));
+ if (columnSchema) {
+ logicalType = columnSchema->LogicalType();
+ }
+ auto denullifiedLogicalType = DenullifyLogicalType(logicalType);
+ try {
+ switch (denullifiedLogicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CreateSimpleValueConverter(
+ skiffField.ValidatedSimplify(),
+ skiffField.IsRequired(),
+ denullifiedLogicalType->AsSimpleTypeRef().GetElement());
+ case ELogicalMetatype::Decimal:
+ return CreateDecimalValueConverter(skiffField, denullifiedLogicalType->AsDecimalTypeRef());
+ case ELogicalMetatype::Optional:
+ // NB. It's complex optional because we denullified type
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::Dict: {
+ auto descriptor = TComplexTypeFieldDescriptor(skiffField.Name(), columnSchema->LogicalType());
+ return CreateComplexValueConverter(std::move(descriptor), skiffField.Schema(), isSparse);
+ }
+ case ELogicalMetatype::Tagged:
+ // Don't expect tagged type in denullified logical type
+ break;
+ }
+ YT_ABORT();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Cannot create Skiff writer for column %Qv",
+ skiffField.Name())
+ << TErrorAttribute("logical_type", logicalType)
+ << ex;
+ }
+ };
+
+ size_t nextDenseIndex = 0;
+ for (size_t i = 0; i < denseFieldDescriptionList.size(); ++i) {
+ const auto& denseField = denseFieldDescriptionList[i];
+ const auto id = NameTable_->GetIdOrRegisterName(denseField.Name());
+ ResizeToContainIndex(&knownFields, id);
+ YT_VERIFY(knownFields[id].EncodingPart == ESkiffWriterColumnType::Unknown);
+
+ if (denseField.Schema()->GetWireType() == EWireType::Nothing) {
+ knownFields[id] = TSkiffEncodingInfo::Skip();
+ continue;
+ }
+
+ TUnversionedValueToSkiffConverter converter;
+ try {
+ if (denseField.Name() == RowIndexColumnName) {
+ writerTableDescription.RowIndexFieldIndex = nextDenseIndex;
+ knownFields[id] = TSkiffEncodingInfo::RowIndex(nextDenseIndex);
+
+ auto method =
+ commonTableDescription.RowIndexMode == ERowRangeIndexMode::Incremental
+ ? (&TRowAndRangeIndexWriter::WriteRowIndex<ERowRangeIndexMode::Incremental>)
+ : (&TRowAndRangeIndexWriter::WriteRowIndex<ERowRangeIndexMode::IncrementalWithError>);
+
+ converter = std::bind(
+ method,
+ &RowAndRangeIndexWriter_,
+ std::placeholders::_1,
+ std::placeholders::_2,
+ std::placeholders::_3);
+ } else if (denseField.Name() == RangeIndexColumnName) {
+ writerTableDescription.RangeIndexFieldIndex = nextDenseIndex;
+ knownFields[id] = TSkiffEncodingInfo::RangeIndex(nextDenseIndex);
+
+ auto method =
+ commonTableDescription.RangeIndexMode == ERowRangeIndexMode::Incremental
+ ? (&TRowAndRangeIndexWriter::WriteRangeIndex<ERowRangeIndexMode::Incremental>)
+ : (&TRowAndRangeIndexWriter::WriteRangeIndex<ERowRangeIndexMode::IncrementalWithError>);
+
+ converter = std::bind(
+ method,
+ &RowAndRangeIndexWriter_,
+ std::placeholders::_1,
+ std::placeholders::_2,
+ std::placeholders::_3);
+ } else {
+ if (denseField.Name() == KeySwitchColumnName) {
+ writerTableDescription.KeySwitchFieldIndex = nextDenseIndex;
+ }
+ knownFields[id] = TSkiffEncodingInfo::Dense(nextDenseIndex);
+ converter = createComplexValueConverter(denseField, /*sparse*/ false);
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Cannot create Skiff writer for table #%v",
+ tableIndex)
+ << ex;
+ }
+ denseFieldWriterInfos.emplace_back(converter, id);
+ ++nextDenseIndex;
+ }
+
+ const auto& sparseFieldDescriptionList = commonTableDescription.SparseFieldDescriptionList;
+ for (size_t i = 0; i < sparseFieldDescriptionList.size(); ++i) {
+ const auto& sparseField = sparseFieldDescriptionList[i];
+ auto id = NameTable_->GetIdOrRegisterName(sparseField.Name());
+ ResizeToContainIndex(&knownFields, id);
+ YT_VERIFY(knownFields[id].EncodingPart == ESkiffWriterColumnType::Unknown);
+
+ try {
+ auto converter = createComplexValueConverter(sparseField, /*sparse*/ true);
+ knownFields[id] = TSkiffEncodingInfo::Sparse(converter, i);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Cannot create Skiff writer for table #%v",
+ tableIndex)
+ << ex;
+ }
+ }
+
+ const auto systemColumnMaxId = Max(GetTableIndexColumnId(), GetRangeIndexColumnId(), GetRowIndexColumnId());
+ ResizeToContainIndex(&knownFields, systemColumnMaxId);
+ knownFields[GetTableIndexColumnId()] = TSkiffEncodingInfo::Skip();
+ if (writerTableDescription.RangeIndexFieldIndex == MissingSystemColumn) {
+ knownFields[GetRangeIndexColumnId()] = TSkiffEncodingInfo::Skip();
+ }
+ if (writerTableDescription.RowIndexFieldIndex == MissingSystemColumn) {
+ knownFields[GetRowIndexColumnId()] = TSkiffEncodingInfo::Skip();
+ }
+ }
+ }
+
+private:
+ std::vector<TErrorAttribute> GetRowPositionErrorAttributes() const
+ {
+ if (CurrentRow_ == nullptr) {
+ return {};
+ }
+
+ i64 tableIndex = 0;
+ std::optional<i64> rowIndex;
+
+ // We don't use tableIndex / rowIndex from DoWrite function because sometimes we want
+ // to throw error before DoWrite knows table index / row index.
+ // To keep things simple we always recompute table index / row index by ourselves.
+ for (const auto& value : *CurrentRow_) {
+ if (value.Id == GetTableIndexColumnId()) {
+ YT_VERIFY(value.Type == EValueType::Int64);
+ tableIndex = value.Data.Int64;
+ } else if (value.Id == GetRowIndexColumnId()) {
+ YT_VERIFY(value.Type == EValueType::Int64);
+ rowIndex = value.Data.Int64;
+ }
+ }
+
+ std::vector<TErrorAttribute> result = {
+ TErrorAttribute("table_index", tableIndex),
+ };
+ if (rowIndex) {
+ result.emplace_back("row_index", *rowIndex);
+ }
+ return result;
+ }
+
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ const auto rowCount = rows.Size();
+ TWriteContext writeContext;
+ writeContext.NameTable = NameTable_;
+ writeContext.TmpBuffer = &YsonBuffer_;
+
+ for (size_t rowIndexInBatch = 0; rowIndexInBatch < rowCount; ++rowIndexInBatch) {
+ auto row = rows[rowIndexInBatch];
+ CurrentRow_ = &row;
+ auto finallyGuard = Finally([&] {
+ CurrentRow_ = nullptr;
+ });
+
+ const auto valueCount = row.GetCount();
+ ui32 tableIndex = 0;
+ for (const auto& value : row) {
+ if (value.Id == GetTableIndexColumnId()) {
+ tableIndex = value.Data.Int64;
+ break;
+ }
+ }
+ if (tableIndex >= TableDescriptionList_.size()) {
+ THROW_ERROR_EXCEPTION("Table #%v is not described by Skiff schema",
+ tableIndex)
+ << GetRowPositionErrorAttributes();
+ }
+ YT_VERIFY(tableIndex < UnversionedValueToYsonConverter_.size());
+ writeContext.UnversionedValueYsonConverter = &UnversionedValueToYsonConverter_[tableIndex];
+
+ const auto& knownFields = TableDescriptionList_[tableIndex].KnownFields;
+ const auto& denseFields = TableDescriptionList_[tableIndex].DenseFieldInfos;
+ const auto hasOtherColumns = TableDescriptionList_[tableIndex].HasOtherColumns;
+ const auto hasSparseColumns = TableDescriptionList_[tableIndex].HasSparseColumns;
+ const auto keySwitchFieldIndex = TableDescriptionList_[tableIndex].KeySwitchFieldIndex;
+ const auto rowIndexFieldIndex = TableDescriptionList_[tableIndex].RowIndexFieldIndex;
+ const auto rangeIndexFieldIndex = TableDescriptionList_[tableIndex].RangeIndexFieldIndex;
+
+ const bool isLastRowInBatch = rowIndexInBatch + 1 == rowCount;
+
+ constexpr ui16 missingColumnPlaceholder = -1;
+ constexpr ui16 keySwitchColumnPlaceholder = -2;
+ DenseIndexes_.assign(denseFields.size(), missingColumnPlaceholder);
+ SparseFields_.clear();
+ OtherValueIndexes_.clear();
+
+ if (keySwitchFieldIndex != MissingSystemColumn) {
+ DenseIndexes_[keySwitchFieldIndex] = keySwitchColumnPlaceholder;
+ }
+
+ ui16 rowIndexValueId = missingColumnPlaceholder;
+ ui16 rangeIndexValueId = missingColumnPlaceholder;
+
+ for (ui32 valueIndex = 0; valueIndex < valueCount; ++valueIndex) {
+ const auto& value = row[valueIndex];
+
+ const auto columnId = value.Id;
+ static const TSkiffEncodingInfo unknownField = TSkiffEncodingInfo();
+ const auto& encodingInfo = columnId < knownFields.size() ? knownFields[columnId] : unknownField;
+ switch (encodingInfo.EncodingPart) {
+ case ESkiffWriterColumnType::Dense:
+ DenseIndexes_[encodingInfo.FieldIndex] = valueIndex;
+ break;
+ case ESkiffWriterColumnType::Sparse:
+ SparseFields_.emplace_back(
+ &encodingInfo.Converter,
+ encodingInfo.FieldIndex,
+ valueIndex);
+ break;
+ case ESkiffWriterColumnType::Skip:
+ break;
+ case ESkiffWriterColumnType::RowIndex:
+ rowIndexValueId = valueIndex;
+ break;
+ case ESkiffWriterColumnType::RangeIndex:
+ rangeIndexValueId = valueIndex;
+ break;
+ case ESkiffWriterColumnType::Unknown:
+ if (!hasOtherColumns) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Column %Qv is not described by Skiff schema and there is no %Qv column",
+ NameTable_->GetName(columnId),
+ OtherColumnsName)
+ << GetRowPositionErrorAttributes();
+ }
+ OtherValueIndexes_.emplace_back(valueIndex);
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+ if (rowIndexFieldIndex != MissingSystemColumn || rangeIndexFieldIndex != MissingSystemColumn) {
+ RowAndRangeIndexWriter_.PrepareTableIndex(tableIndex);
+ if (rangeIndexFieldIndex != MissingSystemColumn) {
+ DenseIndexes_[rangeIndexFieldIndex] = rangeIndexValueId;
+ }
+ if (rangeIndexValueId != missingColumnPlaceholder) {
+ YT_VERIFY(row[rangeIndexValueId].Type == EValueType::Int64);
+ const auto rangeIndex = row[rangeIndexValueId].Data.Int64;
+ RowAndRangeIndexWriter_.PrepareRangeIndex(rangeIndex);
+ } else if (rangeIndexFieldIndex != MissingSystemColumn) {
+ RowAndRangeIndexWriter_.ResetRangeIndex();
+ }
+
+ if (rowIndexFieldIndex != MissingSystemColumn) {
+ DenseIndexes_[rowIndexFieldIndex] = rowIndexValueId;
+ if (rowIndexValueId == missingColumnPlaceholder) {
+ RowAndRangeIndexWriter_.ResetRowIndex();
+ }
+ }
+ }
+
+ SkiffWriter_->WriteVariant16Tag(tableIndex);
+ for (size_t idx = 0; idx < denseFields.size(); ++idx) {
+ const auto& fieldInfo = denseFields[idx];
+ const auto valueIndex = DenseIndexes_[idx];
+
+ switch (valueIndex) {
+ case missingColumnPlaceholder:
+ fieldInfo.Converter(
+ MakeUnversionedSentinelValue(EValueType::Null, fieldInfo.ColumnId),
+ &*SkiffWriter_,
+ &writeContext);
+ break;
+ case keySwitchColumnPlaceholder:
+ SkiffWriter_->WriteBoolean(CheckKeySwitch(row, isLastRowInBatch));
+ break;
+ default: {
+ const auto& value = row[valueIndex];
+ fieldInfo.Converter(
+ value,
+ &*SkiffWriter_,
+ &writeContext);
+ break;
+ }
+ }
+ }
+
+ if (hasSparseColumns) {
+ for (const auto& fieldInfo : SparseFields_) {
+ const auto& value = row[fieldInfo.ValueIndex];
+ if (value.Type != EValueType::Null) {
+ SkiffWriter_->WriteVariant16Tag(fieldInfo.SparseFieldTag);
+ (*fieldInfo.Converter)(value, &*SkiffWriter_, &writeContext);
+ }
+ }
+ SkiffWriter_->WriteVariant16Tag(EndOfSequenceTag<ui16>());
+ }
+ if (hasOtherColumns) {
+ YsonBuffer_.Clear();
+ TBufferOutput out(YsonBuffer_);
+ NYson::TYsonWriter writer(
+ &out,
+ NYson::EYsonFormat::Binary,
+ NYson::EYsonType::Node,
+ /* enableRaw */ true);
+ writer.OnBeginMap();
+ for (const auto otherValueIndex : OtherValueIndexes_) {
+ const auto& value = row[otherValueIndex];
+ writer.OnKeyedItem(NameTable_->GetName(value.Id));
+ writeContext.UnversionedValueYsonConverter->WriteValue(value, &writer);
+ }
+ writer.OnEndMap();
+ SkiffWriter_->WriteYson32(TStringBuf(YsonBuffer_.Data(), YsonBuffer_.Size()));
+ }
+ SkiffWriter_->Flush();
+ TryFlushBuffer(false);
+ }
+ Flush();
+ }
+
+ TFuture<void> Flush() override
+ {
+ SkiffWriter_->Flush();
+ return TSchemalessFormatWriterBase::Flush();
+ }
+
+ TFuture<void> Close() override
+ {
+ // NB(gritukan): You can't move it into WriteEndOfStream, since it
+ // will be called between SkiffWriter and buffer flushes leading to
+ // their inconsistency.
+ if (ControlAttributesConfig_->EnableEndOfStream) {
+ SkiffWriter_->WriteVariant16Tag(EndOfSequenceTag<ui16>());
+ }
+
+ SkiffWriter_->Flush();
+ return TSchemalessFormatWriterBase::Close();
+ }
+
+private:
+ std::optional<NSkiff::TCheckedInDebugSkiffWriter> SkiffWriter_;
+
+ std::vector<ui16> DenseIndexes_;
+ std::vector<TSparseFieldInfo> SparseFields_;
+ std::vector<ui16> OtherValueIndexes_;
+
+ // Table #i is described by element with index i.
+ std::vector<TSkiffWriterTableDescription> TableDescriptionList_;
+
+ std::vector<TUnversionedValueYsonWriter> UnversionedValueToYsonConverter_;
+
+ // Buffer that we are going to reuse in order to reduce memory allocations.
+ TBuffer YsonBuffer_;
+
+ TRowAndRangeIndexWriter RowAndRangeIndexWriter_;
+
+ const TUnversionedRow* CurrentRow_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForSkiff(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = NYTree::ConvertTo<TSkiffFormatConfigPtr>(attributes);
+ auto skiffSchemas = ParseSkiffSchemas(config->SkiffSchemaRegistry, config->TableSkiffSchemas);
+
+ auto copySchemas = schemas;
+ if (config->OverrideIntermediateTableSchema) {
+ Y_VERIFY(!schemas.empty());
+ if (!IsTrivialIntermediateSchema(*schemas[0])) {
+ THROW_ERROR_EXCEPTION("Cannot use \"override_intermediate_table_schema\" since input table #0 has nontrivial schema")
+ << TErrorAttribute("schema", *schemas[0]);
+ }
+ copySchemas[0] = New<TTableSchema>(*config->OverrideIntermediateTableSchema);
+ }
+
+ return CreateWriterForSkiff(
+ skiffSchemas,
+ std::move(nameTable),
+ copySchemas,
+ std::move(output),
+ enableContextSaving,
+ std::move(controlAttributesConfig),
+ keyColumnCount);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for Skiff format") << ex;
+ }
+}
+
+ISchemalessFormatWriterPtr CreateWriterForSkiff(
+ const std::vector<std::shared_ptr<TSkiffSchema>>& tableSkiffSchemas,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ auto result = New<TSkiffWriter>(
+ std::move(nameTable),
+ std::move(output),
+ enableContextSaving,
+ std::move(controlAttributesConfig),
+ keyColumnCount);
+ result->Init(schemas, tableSkiffSchemas);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_writer.h b/yt/yt/client/formats/skiff_writer.h
new file mode 100644
index 0000000000..9cd8f66268
--- /dev/null
+++ b/yt/yt/client/formats/skiff_writer.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/library/skiff_ext/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForSkiff(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ISchemalessFormatWriterPtr CreateWriterForSkiff(
+ const std::vector<std::shared_ptr<NSkiff::TSkiffSchema>>& tableSkiffSchemas,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormat
diff --git a/yt/yt/client/formats/skiff_yson_converter-inl.h b/yt/yt/client/formats/skiff_yson_converter-inl.h
new file mode 100644
index 0000000000..2c667b35d6
--- /dev/null
+++ b/yt/yt/client/formats/skiff_yson_converter-inl.h
@@ -0,0 +1,157 @@
+#ifndef SKIFF_YSON_CONVERTER_INL_H_
+#error "Direct inclusion of this file is not allowed; include skiff_yson_converter.h"
+#endif
+
+#include <util/system/byteorder.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType wireType>
+Y_FORCE_INLINE auto TSimpleSkiffParser<wireType>::operator () (NSkiff::TCheckedInDebugSkiffParser* parser) const
+{
+ using namespace NSkiff;
+
+ if constexpr (wireType == EWireType::Int8) {
+ return parser->ParseInt8();
+ } else if constexpr (wireType == EWireType::Int16) {
+ return parser->ParseInt16();
+ } else if constexpr (wireType == EWireType::Int32) {
+ return parser->ParseInt32();
+ } else if constexpr (wireType == EWireType::Int64) {
+ return parser->ParseInt64();
+ } else if constexpr (wireType == EWireType::Uint8) {
+ return parser->ParseUint8();
+ } else if constexpr (wireType == EWireType::Uint16) {
+ return parser->ParseUint16();
+ } else if constexpr (wireType == EWireType::Uint32) {
+ return parser->ParseUint32();
+ } else if constexpr (wireType == EWireType::Uint64) {
+ return parser->ParseUint64();
+ } else if constexpr (wireType == EWireType::Boolean) {
+ return parser->ParseBoolean();
+ } else if constexpr (wireType == EWireType::Double) {
+ return parser->ParseDouble();
+ } else if constexpr (wireType == EWireType::String32) {
+ return parser->ParseString32();
+ } else if constexpr (wireType == EWireType::Nothing) {
+ return nullptr;
+ } else {
+ static_assert(wireType == EWireType::Int64);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType SkiffWireType>
+TDecimalSkiffParser<SkiffWireType>::TDecimalSkiffParser(int precision)
+ : Precision_(precision)
+{
+ CheckSkiffWireTypeForDecimal(precision, SkiffWireType);
+}
+
+template <NSkiff::EWireType SkiffWireType>
+Y_FORCE_INLINE TStringBuf TDecimalSkiffParser<SkiffWireType>::operator() (NSkiff::TCheckedInDebugSkiffParser* parser) const
+{
+ using namespace NSkiff;
+ using namespace NDecimal;
+
+ if constexpr (SkiffWireType == EWireType::Int32) {
+ auto value = parser->ParseInt32();
+ return TDecimal::WriteBinary32(Precision_, value, Buffer_, sizeof(Buffer_));
+ } else if constexpr (SkiffWireType == EWireType::Int64) {
+ auto value = parser->ParseInt64();
+ return TDecimal::WriteBinary64(Precision_, value, Buffer_, sizeof(Buffer_));
+ } else if constexpr (SkiffWireType == EWireType::Int128) {
+ const auto skiffValue = parser->ParseInt128();
+ return TDecimal::WriteBinary128(
+ Precision_,
+ TDecimal::TValue128{skiffValue.Low, skiffValue.High},
+ Buffer_,
+ sizeof(Buffer_));
+ } else {
+ static_assert(SkiffWireType == EWireType::Int128);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType SkiffWireType>
+TDecimalSkiffWriter<SkiffWireType>::TDecimalSkiffWriter(int precision)
+ : Precision_(precision)
+{
+ CheckSkiffWireTypeForDecimal(precision, SkiffWireType);
+}
+
+template <NSkiff::EWireType SkiffWireType>
+void TDecimalSkiffWriter<SkiffWireType>::operator()(TStringBuf value, NSkiff::TCheckedInDebugSkiffWriter* writer) const
+{
+ using namespace NSkiff;
+ using namespace NDecimal;
+
+ if constexpr (SkiffWireType == EWireType::Int32) {
+ auto intValue = TDecimal::ParseBinary32(Precision_, value);
+ writer->WriteInt32(intValue);
+ } else if constexpr (SkiffWireType == EWireType::Int64) {
+ auto intValue = TDecimal::ParseBinary64(Precision_, value);
+ writer->WriteInt64(intValue);
+ } else if constexpr (SkiffWireType == EWireType::Int128) {
+ auto intValue = TDecimal::ParseBinary128(Precision_, value);
+ writer->WriteInt128(TInt128{intValue.Low, intValue.High});
+ } else {
+ // poor man's static_assert(false)
+ static_assert(SkiffWireType == EWireType::Int128);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStringBuf TUuidParser::operator()(NSkiff::TCheckedInDebugSkiffParser* parser) const
+{
+ static_assert(sizeof(Buffer_) == 16);
+
+ auto value = parser->ParseUint128();
+ Buffer_[0] = HostToInet(value.High);
+ Buffer_[1] = HostToInet(value.Low);
+ return TStringBuf(reinterpret_cast<const char*>(Buffer_), sizeof(Buffer_));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TUuidWriter::operator()(TStringBuf value, NSkiff::TCheckedInDebugSkiffWriter* writer) const
+{
+ constexpr size_t ExpectedSize = 16;
+ if (value.size() != ExpectedSize) {
+ THROW_ERROR_EXCEPTION("Invalid size of UUID value: expected %v, actual %v",
+ ExpectedSize,
+ value.size());
+ }
+ const ui64* array = reinterpret_cast<const ui64*>(value.Data());
+ writer->WriteUint128(NSkiff::TUint128{InetToHost(array[1]), InetToHost(array[0])});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType wireType, typename TValueType>
+void CheckIntSize(TValueType value)
+{
+ using TIntType = typename NSkiff::TUnderlyingIntegerType<wireType>::TValue;
+ bool ok;
+ if constexpr (std::is_same_v<TValueType, i64>) {
+ ok = std::numeric_limits<TIntType>::min() <= value && value <= std::numeric_limits<TIntType>::max();
+ } else if constexpr (std::is_same_v<TValueType, ui64>) {
+ ok = value <= std::numeric_limits<TIntType>::max();
+ } else {
+ static_assert(std::is_same_v<TIntType, i64>, "We expect either i64 or ui64 here");
+ }
+ if (!ok) {
+ THROW_ERROR_EXCEPTION("Value %v is out of range for possible values for skiff type %Qlv",
+ value,
+ wireType);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_yson_converter.cpp b/yt/yt/client/formats/skiff_yson_converter.cpp
new file mode 100644
index 0000000000..171bfd9a9a
--- /dev/null
+++ b/yt/yt/client/formats/skiff_yson_converter.cpp
@@ -0,0 +1,1899 @@
+#include "skiff_yson_converter.h"
+
+#include <yt/yt/client/complex_types/check_yson_token.h>
+#include <yt/yt/client/table_client/logical_type.h>
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <util/stream/zerocopy.h>
+#include <util/stream/mem.h>
+
+namespace NYT::NFormats {
+
+using namespace NSkiff;
+using namespace NYson;
+using namespace NTableClient;
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TTypePair = std::pair<TComplexTypeFieldDescriptor, std::shared_ptr<TSkiffSchema>>;
+
+struct TConverterCreationContext;
+
+TYsonToSkiffConverter CreateYsonToSkiffConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config);
+
+TSkiffToYsonConverter CreateSkiffToYsonConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConverterCreationContext
+{
+ int NestingLevel = 0;
+};
+
+std::vector<TErrorAttribute> SkiffYsonErrorAttributes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ return {
+ TErrorAttribute("complex_type_field", descriptor.GetDescription()),
+ TErrorAttribute("logical_type_dbg", ToString(*descriptor.GetType())),
+ TErrorAttribute("skiff_schema_dbg", GetShortDebugString(skiffSchema)),
+ };
+}
+
+std::shared_ptr<TSkiffSchema> GetOptionalChild(const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ if (skiffSchema->GetWireType() != EWireType::Variant8) {
+ return nullptr;
+ }
+ auto children = skiffSchema->GetChildren();
+ if (children.size() != 2) {
+ return nullptr;
+ }
+ if (children[0]->GetWireType() != EWireType::Nothing) {
+ return nullptr;
+ }
+ return children[1];
+}
+
+struct TSkiffStructField
+{
+ TString Name;
+ std::shared_ptr<TSkiffSchema> Type;
+};
+
+template<EWireType wireType>
+constexpr EYsonItemType WireTypeToYsonItemType()
+{
+ if constexpr (
+ wireType == EWireType::Int8 ||
+ wireType == EWireType::Int16 ||
+ wireType == EWireType::Int32 ||
+ wireType == EWireType::Int64)
+ {
+ return EYsonItemType::Int64Value;
+ } else if constexpr (
+ wireType == EWireType::Uint8 ||
+ wireType == EWireType::Uint16 ||
+ wireType == EWireType::Uint32 ||
+ wireType == EWireType::Uint64)
+ {
+ return EYsonItemType::Uint64Value;
+ } else if constexpr (wireType == EWireType::Double) {
+ return EYsonItemType::DoubleValue;
+ } else if constexpr (wireType == EWireType::Boolean) {
+ return EYsonItemType::BooleanValue;
+ } else if constexpr (wireType == EWireType::String32) {
+ return EYsonItemType::StringValue;
+ } else if constexpr (wireType == EWireType::Nothing) {
+ return EYsonItemType::EntityValue;
+ } else {
+ static_assert(wireType == EWireType::Int64);
+ }
+}
+
+struct TOptionalTypesMatch
+{
+ TTypePair InnerTypes;
+ int LogicalNesting = 0;
+ int SkiffNesting = 0;
+};
+
+[[noreturn]] void ThrowBadWireType(EWireType expected, EWireType actual)
+{
+ THROW_ERROR_EXCEPTION("Bad Skiff wire type: expected %Qlv, actual %Qlv",
+ expected,
+ actual);
+}
+
+[[noreturn]] void RethrowCannotMatchField(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const std::exception& ex)
+{
+ THROW_ERROR_EXCEPTION("Cannot match field %Qv to Skiff schema",
+ descriptor.GetDescription())
+ << SkiffYsonErrorAttributes(descriptor, skiffSchema)
+ << ex;
+}
+
+template <typename... Args>
+[[noreturn]] void ThrowYsonToSkiffConversionError(const TComplexTypeFieldDescriptor& descriptor, const Args&... args)
+{
+ THROW_ERROR_EXCEPTION("Yson to Skiff conversion error while converting %Qv field",
+ descriptor.GetDescription())
+ << TError(args...);
+}
+
+[[noreturn]] void ThrowBadYsonToken(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::vector<EYsonItemType>& expected,
+ const EYsonItemType actual)
+{
+ TStringStream expectationString;
+ if (expected.size() > 1) {
+ expectationString << "one of ";
+ bool first = true;
+ for (const auto& itemType : expected) {
+ if (!first) {
+ expectationString << ", ";
+ }
+ first = false;
+ expectationString << Format("%Qlv", itemType);
+ }
+ } else {
+ YT_VERIFY(expected.size() == 1);
+ expectationString << Format("%Qlv", expected[0]);
+ }
+
+ ThrowYsonToSkiffConversionError(descriptor, "Bad yson token type, expected %v actual: %Qlv",
+ expectationString.Str(),
+ actual);
+}
+
+template <typename... Args>
+[[noreturn]] void ThrowSkiffToYsonConversionError(const TComplexTypeFieldDescriptor& descriptor, const Args&... args)
+{
+ THROW_ERROR_EXCEPTION("Skiff to Yson conversion error while converting %Qv field",
+ descriptor.GetDescription())
+ << TError(args...);
+}
+
+TOptionalTypesMatch MatchOptionalTypes(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ bool allowOmitOptional)
+{
+ // NB. Here we have a problem:
+ // variant_tuple<null, T> has exactly the same Skiff representation as optional<T>.
+ // We need to perform nontrivial analysis of schemas in order to align these schemas.
+ //
+ // When reading this code you can keep in mind following examples
+ // 1. logical type: optional<variant_tuple<null, T>>
+ // Skiff type: variant8<nothing, variant8<nothing, T>>
+ // Outer Skiff `variant8` encodes logical outer `optional` type
+ // 2. logical type: optional<variant_tuple<null, T>>
+ // Skiff type: variant8<nothing, T>
+ // Outer `variant8` Skiff type encodes logical inner `variant_tuple` type when allowOmitOptional is true.
+
+ // When logical type is either optional<T> or variant_tuple<null, T> this function returns descriptor of T.
+ // Otherwise it returns std::nullopt.
+ const auto& getLogicalOptionalLikeChild = [] (const TComplexTypeFieldDescriptor& descriptor) -> std::optional<TComplexTypeFieldDescriptor> {
+ const auto& type = descriptor.GetType();
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Optional:
+ return descriptor.OptionalElement();
+ case ELogicalMetatype::VariantTuple: {
+ const auto& variantType = type->AsVariantTupleTypeRef();
+ if (variantType.GetElements().size() == 2
+ && *variantType.GetElements()[0] == *SimpleLogicalType(ESimpleLogicalValueType::Null))
+ {
+ return descriptor.VariantTupleElement(1);
+ } else {
+ return {};
+ }
+ }
+ default:
+ return {};
+ }
+ };
+
+ try {
+
+ // First of all we compute strict and relaxed depths of optional chain.
+ // Strict depth is the depth of chain where each element is optional<T>.
+ // Relaxed depth is the depth of chain where each element is optional<T> or variant<null, T>
+
+ int logicalNestingRelaxed = 0;
+ int logicalNestingStrict = 0;
+ {
+ auto innerDescriptor = descriptor;
+ while (auto element = getLogicalOptionalLikeChild(innerDescriptor)) {
+ if (innerDescriptor.GetType()->GetMetatype() == ELogicalMetatype::Optional &&
+ logicalNestingStrict == logicalNestingRelaxed) {
+ ++logicalNestingStrict;
+ }
+ ++logicalNestingRelaxed;
+ innerDescriptor = *element;
+ }
+ }
+ YT_VERIFY(logicalNestingStrict);
+ YT_VERIFY(logicalNestingRelaxed >= logicalNestingStrict);
+
+ // Then we compute depth of corresponding Skiff chain of variant<nothing, T>
+ // This chain should match to logical relaxed chain.
+ int skiffNestingRelaxed = 0;
+ {
+ std::shared_ptr<TSkiffSchema> innerSkiffSchema = skiffSchema;
+ while (auto child = GetOptionalChild(innerSkiffSchema)) {
+ ++skiffNestingRelaxed;
+ innerSkiffSchema = child;
+ }
+ }
+
+ if (logicalNestingRelaxed != skiffNestingRelaxed &&
+ !(allowOmitOptional && logicalNestingRelaxed == skiffNestingRelaxed + 1))
+ {
+ THROW_ERROR_EXCEPTION("Optional nesting mismatch: logical type nesting %v, Skiff nesting %v",
+ logicalNestingRelaxed,
+ skiffNestingRelaxed);
+ }
+
+ // NB. We only allow to omit outer optional of the column so in order to match lengths of inner chains must be
+ // equal. Based on this assertion we compute lengths of Skiff chain corresponding to length of
+ auto skiffNestingStrict = skiffNestingRelaxed - (logicalNestingRelaxed - logicalNestingStrict);
+ YT_VERIFY(skiffNestingRelaxed >= 0);
+
+ // We descend over strict chains once again to get matching inner types.
+ auto innerDescriptor = descriptor;
+ for (int i = 0; i < logicalNestingStrict; ++i) {
+ YT_VERIFY(innerDescriptor.GetType()->GetMetatype() == ELogicalMetatype::Optional);
+ innerDescriptor = innerDescriptor.OptionalElement();
+ }
+ YT_VERIFY(innerDescriptor.GetType()->GetMetatype() != ELogicalMetatype::Optional);
+
+ auto innerSkiffSchema = skiffSchema;
+ for (int i = 0; i < skiffNestingStrict; ++i) {
+ innerSkiffSchema = GetOptionalChild(innerSkiffSchema);
+ YT_VERIFY(innerSkiffSchema);
+ }
+
+ return {{std::move(innerDescriptor), innerSkiffSchema}, logicalNestingStrict, skiffNestingStrict};
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+TTypePair MatchListTypes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::RepeatedVariant8) {
+ ThrowBadWireType(EWireType::RepeatedVariant8, skiffSchema->GetWireType());
+ }
+ if (skiffSchema->GetChildren().size() != 1) {
+ THROW_ERROR_EXCEPTION(
+ "%Qlv has too many children: expected %v, actual %v",
+ skiffSchema->GetWireType(),
+ 1,
+ skiffSchema->GetChildren().size());
+ }
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+ return {descriptor.ListElement(), skiffSchema->GetChildren()[0]};
+}
+
+std::vector<std::optional<TTypePair>> MatchStructTypes(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ bool allowUnknownSkiffFields)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::Tuple) {
+ ThrowBadWireType(EWireType::Tuple, skiffSchema->GetWireType());
+ }
+
+ THashMap<TString, int> skiffNameToIndex;
+ std::vector<TSkiffStructField> skiffFields;
+ {
+ const auto& children = skiffSchema->GetChildren();
+ for (size_t i = 0; i < children.size(); ++i) {
+ const auto& child = children[i];
+ const auto& name = child->GetName();
+ if (child->GetName().empty()) {
+ THROW_ERROR_EXCEPTION("%Qv child #%v has empty name",
+ EWireType::Tuple,
+ i);
+ }
+ skiffFields.push_back({child->GetName(), child});
+ if (skiffNameToIndex.find(name) != skiffNameToIndex.end()) {
+ THROW_ERROR_EXCEPTION("%Qv has multiple children with name %Qv",
+ EWireType::Tuple,
+ name);
+ }
+ skiffNameToIndex[name] = i;
+ }
+ }
+
+ std::vector<std::optional<TTypePair>> result;
+ auto addEmptyResult = [&result, &skiffFields, allowUnknownSkiffFields] (int index) {
+ if (!allowUnknownSkiffFields) {
+ THROW_ERROR_EXCEPTION("Skiff %Qv child %Qv is not found in logical type",
+ EWireType::Tuple,
+ skiffFields[index].Name);
+ }
+ if (!GetOptionalChild(skiffFields[index].Type)) {
+ THROW_ERROR_EXCEPTION("Non optional Skiff field %Qv is missing corresponding logical struct field",
+ skiffFields[index].Name);
+ }
+ result.emplace_back(std::nullopt);
+
+ };
+
+ ssize_t nextSkiffFieldIndex = 0;
+ const auto& fields = descriptor.GetType()->AsStructTypeRef().GetFields();
+ for (size_t i = 0; i < fields.size(); ++i) {
+ auto logicalField = fields[i];
+ auto skiffIndexIt = skiffNameToIndex.find(logicalField.Name);
+ if (skiffIndexIt == skiffNameToIndex.end()) {
+ result.emplace_back(TTypePair{descriptor.StructField(i), nullptr});
+ continue;
+ }
+ const auto skiffFieldIndex = skiffIndexIt->second;
+ if (skiffFieldIndex < nextSkiffFieldIndex) {
+ THROW_ERROR_EXCEPTION("%Qv child %Qv is out of order",
+ EWireType::Tuple,
+ logicalField.Name);
+ }
+
+ for (; nextSkiffFieldIndex < skiffFieldIndex; ++nextSkiffFieldIndex) {
+ addEmptyResult(nextSkiffFieldIndex);
+ }
+ auto skiffFieldSchema = skiffFields[nextSkiffFieldIndex].Type;
+ ++nextSkiffFieldIndex;
+
+ result.emplace_back(TTypePair{descriptor.StructField(i), skiffFieldSchema});
+ }
+
+ for (; nextSkiffFieldIndex < std::ssize(skiffFields); ++nextSkiffFieldIndex) {
+ addEmptyResult(nextSkiffFieldIndex);
+ }
+
+ return result;
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+std::vector<TTypePair> MatchTupleTypes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::Tuple) {
+ ThrowBadWireType(EWireType::Tuple, skiffSchema->GetWireType());
+ }
+
+ const auto& elements = descriptor.GetType()->AsTupleTypeRef().GetElements();
+ const auto& children = skiffSchema->GetChildren();
+
+ if (children.size() != elements.size()) {
+ THROW_ERROR_EXCEPTION("Tuple element counts do not match: logical type elements %v, Skiff elements %v",
+ elements.size(),
+ children.size());
+ }
+
+ std::vector<TTypePair> result;
+ for (size_t i = 0; i < elements.size(); ++i) {
+ result.emplace_back(descriptor.TupleElement(i), children[i]);
+ }
+
+ return result;
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+std::vector<TTypePair> MatchVariantTupleTypes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::Variant8 && skiffSchema->GetWireType() != EWireType::Variant16) {
+ ThrowBadWireType(EWireType::Tuple, skiffSchema->GetWireType());
+ }
+
+ const auto& elements = descriptor.GetType()->AsVariantTupleTypeRef().GetElements();
+ const auto& children = skiffSchema->GetChildren();
+
+ if (children.size() != elements.size()) {
+ THROW_ERROR_EXCEPTION("Variant element counts do not match: logical type elements %v, Skiff elements %v",
+ elements.size(),
+ children.size());
+ }
+
+ std::vector<TTypePair> result;
+ for (size_t i = 0; i < elements.size(); ++i) {
+ result.emplace_back(descriptor.VariantTupleElement(i), children[i]);
+ }
+
+ return result;
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+std::vector<TTypePair> MatchVariantStructTypes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::Variant8 && skiffSchema->GetWireType() != EWireType::Variant16) {
+ ThrowBadWireType(EWireType::Variant8, skiffSchema->GetWireType());
+ }
+
+ const auto& fields = descriptor.GetType()->AsVariantStructTypeRef().GetFields();
+ const auto& children = skiffSchema->GetChildren();
+
+ if (children.size() != fields.size()) {
+ THROW_ERROR_EXCEPTION("Variant element counts do not match: logical type elements %v, Skiff elements %v",
+ fields.size(),
+ children.size());
+ }
+
+ std::vector<TTypePair> result;
+ for (size_t i = 0; i < fields.size(); ++i) {
+ if (fields[i].Name != children[i]->GetName()) {
+ THROW_ERROR_EXCEPTION("Skiff %v child #%v expected to be %Qv but %Qv found",
+ skiffSchema->GetWireType(),
+ i,
+ fields[i].Name,
+ children[i]->GetName());
+ }
+ result.emplace_back(descriptor.VariantStructField(i), children[i]);
+ }
+
+ return result;
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+std::pair<TTypePair, TTypePair> MatchDictTypes(const TComplexTypeFieldDescriptor& descriptor, const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ if (skiffSchema->GetWireType() != EWireType::RepeatedVariant8) {
+ ThrowBadWireType(EWireType::RepeatedVariant8, skiffSchema->GetWireType());
+ }
+
+ if (skiffSchema->GetChildren().size() != 1) {
+ THROW_ERROR_EXCEPTION("%Qlv has unexpected children count: expected %v, actual %v",
+ EWireType::RepeatedVariant8,
+ 1,
+ skiffSchema->GetChildren().size());
+ }
+
+ auto tupleSchema = skiffSchema->GetChildren()[0];
+ if (tupleSchema->GetWireType() != EWireType::Tuple) {
+ THROW_ERROR_EXCEPTION("%Qlv has unexpected wire type: expected %Qlv, actual %Qlv",
+ EWireType::RepeatedVariant8,
+ EWireType::Tuple,
+ tupleSchema->GetWireType());
+ }
+
+ if (tupleSchema->GetChildren().size() != 2) {
+ THROW_ERROR_EXCEPTION("%Qlv has unexpected children count: expected %v, found %v",
+ EWireType::Tuple,
+ 1,
+ skiffSchema->GetChildren().size());
+ }
+ return {
+ {descriptor.DictKey(), tupleSchema->GetChildren()[0]},
+ {descriptor.DictValue(), tupleSchema->GetChildren()[1]}
+ };
+ } catch (const std::exception & ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EYsonItemType ExpectedTokenType, typename TFunction>
+class TPrimitiveTypeYsonToSkiffConverter
+{
+public:
+ explicit TPrimitiveTypeYsonToSkiffConverter(TComplexTypeFieldDescriptor descriptor, TFunction function)
+ : Descriptor_(std::move(descriptor))
+ , Function_(std::move(function))
+ { }
+
+ void operator()(TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer)
+ {
+ NComplexTypes::EnsureYsonToken(Descriptor_, *cursor, ExpectedTokenType);
+ if constexpr (ExpectedTokenType == EYsonItemType::StringValue) {
+ const auto& value = cursor->GetCurrent().UncheckedAsString();
+ Function_(value, writer);
+ } else {
+ // poor man's static_assert(false)
+ static_assert(ExpectedTokenType == EYsonItemType::StringValue);
+ }
+ cursor->Next();
+ }
+
+private:
+ const TComplexTypeFieldDescriptor Descriptor_;
+ const TFunction Function_;
+};
+
+
+template <EWireType wireType>
+class TSimpleYsonToSkiffConverter
+{
+public:
+ explicit TSimpleYsonToSkiffConverter(TComplexTypeFieldDescriptor descriptor)
+ : Descriptor_(std::move(descriptor))
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer)
+ {
+ if constexpr (wireType == EWireType::Yson32) {
+ TmpString_.clear();
+ {
+ TStringOutput output(TmpString_);
+ TBufferedBinaryYsonWriter ysonWriter(&output);
+ cursor->TransferComplexValue(&ysonWriter);
+ ysonWriter.Flush();
+ }
+ writer->WriteYson32(TmpString_);
+ } else {
+ constexpr auto expectedValueType = WireTypeToYsonItemType<wireType>();
+ auto ysonItem = cursor->GetCurrent();
+ if (ysonItem.GetType() != expectedValueType) {
+ ThrowYsonToSkiffConversionError(Descriptor_, "Unexpected yson type: expected %Qlv, found %Qlv",
+ expectedValueType,
+ ysonItem.GetType());
+ }
+
+ if constexpr (wireType == EWireType::Int8) {
+ const auto value = ysonItem.UncheckedAsInt64();
+ CheckIntSize<wireType>(value);
+ writer->WriteInt8(value);
+ } else if constexpr (wireType == EWireType::Int16) {
+ const auto value = ysonItem.UncheckedAsInt64();
+ CheckIntSize<wireType>(value);
+ writer->WriteInt16(value);
+ } else if constexpr (wireType == EWireType::Int32) {
+ const auto value = ysonItem.UncheckedAsInt64();
+ CheckIntSize<wireType>(value);
+ writer->WriteInt32(value);
+ } else if constexpr (wireType == EWireType::Int64) {
+ writer->WriteInt64(ysonItem.UncheckedAsInt64());
+
+ } else if constexpr (wireType == EWireType::Uint8) {
+ auto value = ysonItem.UncheckedAsUint64();
+ CheckIntSize<wireType>(value);
+ writer->WriteUint8(value);
+ } else if constexpr (wireType == EWireType::Uint16) {
+ auto value = ysonItem.UncheckedAsUint64();
+ CheckIntSize<wireType>(value);
+ writer->WriteUint16(value);
+ } else if constexpr (wireType == EWireType::Uint32) {
+ auto value = ysonItem.UncheckedAsUint64();
+ CheckIntSize<wireType>(value);
+ writer->WriteUint32(value);
+ } else if constexpr (wireType == EWireType::Uint64) {
+ writer->WriteUint64(ysonItem.UncheckedAsUint64());
+
+ } else if constexpr (wireType == EWireType::Boolean) {
+ writer->WriteBoolean(ysonItem.UncheckedAsBoolean());
+ } else if constexpr (wireType == EWireType::Double) {
+ writer->WriteDouble(ysonItem.UncheckedAsDouble());
+ } else if constexpr (wireType == EWireType::String32) {
+ writer->WriteString32(ysonItem.UncheckedAsString());
+ } else if constexpr (wireType == EWireType::Nothing) {
+ // do nothing
+ } else {
+ static_assert(wireType == EWireType::Int64);
+ }
+ cursor->Next();
+ }
+ }
+
+private:
+ TComplexTypeFieldDescriptor Descriptor_;
+ TString TmpString_;
+};
+
+template <EYsonItemType ExpectedTokenType, typename TFunction>
+inline TPrimitiveTypeYsonToSkiffConverter<ExpectedTokenType, TFunction> CreatePrimitiveTypeYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ TFunction function)
+{
+ return TPrimitiveTypeYsonToSkiffConverter<ExpectedTokenType, TFunction>(descriptor, function);
+}
+
+TYsonToSkiffConverter CreatePrimitiveTypeYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ EWireType wireType)
+{
+ switch (wireType) {
+#define CASE(x) case x: return TSimpleYsonToSkiffConverter<x>(std::move(descriptor));
+ CASE(EWireType::Int8)
+ CASE(EWireType::Int16)
+ CASE(EWireType::Int32)
+ CASE(EWireType::Int64)
+ CASE(EWireType::Uint8)
+ CASE(EWireType::Uint16)
+ CASE(EWireType::Uint32)
+ CASE(EWireType::Uint64)
+ CASE(EWireType::Boolean)
+ CASE(EWireType::Double)
+ CASE(EWireType::String32)
+ CASE(EWireType::Yson32)
+ CASE(EWireType::Nothing)
+#undef CASE
+ default:
+ YT_ABORT();
+ }
+}
+
+TYsonToSkiffConverter CreateSimpleYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ try {
+ const auto wireType = skiffSchema->GetWireType();
+ const auto logicalType = descriptor.GetType()->AsSimpleTypeRef().GetElement();
+ switch (logicalType) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+
+ case ESimpleLogicalValueType::Interval:
+ CheckWireType(wireType, {EWireType::Int8, EWireType::Int16, EWireType::Int32, EWireType::Int64});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ CheckWireType(wireType, {EWireType::Uint8, EWireType::Uint16, EWireType::Uint32, EWireType::Uint64});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ CheckWireType(wireType, {EWireType::Double});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Boolean:
+ CheckWireType(wireType, {EWireType::Boolean});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::String:
+ CheckWireType(wireType, {EWireType::String32});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Any:
+ CheckWireType(wireType, {EWireType::Yson32});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ CheckWireType(wireType, {EWireType::Nothing});
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+
+ case ESimpleLogicalValueType::Uuid:
+ CheckWireType(wireType, {EWireType::Uint128, EWireType::String32});
+ if (wireType == EWireType::Uint128) {
+ return CreatePrimitiveTypeYsonToSkiffConverter<EYsonItemType::StringValue>(
+ std::move(descriptor),
+ TUuidWriter());
+ } else {
+ return CreatePrimitiveTypeYsonToSkiffConverter(std::move(descriptor), wireType);
+ }
+ }
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+TYsonToSkiffConverter CreateDecimalYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ const auto& logicalType = descriptor.GetType();
+ const int precision = logicalType->AsDecimalTypeRef().GetPrecision();
+ const auto wireType = skiffSchema->GetWireType();
+ switch (wireType) {
+ case EWireType::Int32:
+ return CreatePrimitiveTypeYsonToSkiffConverter<EYsonItemType::StringValue>(
+ std::move(descriptor),
+ TDecimalSkiffWriter<EWireType::Int32>(precision));
+ case EWireType::Int64:
+ return CreatePrimitiveTypeYsonToSkiffConverter<EYsonItemType::StringValue>(
+ std::move(descriptor),
+ TDecimalSkiffWriter<EWireType::Int64>(precision));
+ case EWireType::Int128:
+ return CreatePrimitiveTypeYsonToSkiffConverter<EYsonItemType::StringValue>(
+ std::move(descriptor),
+ TDecimalSkiffWriter<EWireType::Int128>(precision));
+ default:
+ CheckSkiffWireTypeForDecimal(precision, wireType);
+ YT_ABORT();
+ }
+}
+
+class TOptionalYsonToSkiffConverterImpl
+{
+public:
+ TOptionalYsonToSkiffConverterImpl(
+ TYsonToSkiffConverter innerConverter,
+ TComplexTypeFieldDescriptor descriptor,
+ int ysonOptionalLevel,
+ int skiffOptionalLevel)
+ : InnerConverter_(std::move(innerConverter))
+ , Descriptor_(std::move(descriptor))
+ , OuterExpectFilledLevel_(ysonOptionalLevel > 1 ? ysonOptionalLevel - skiffOptionalLevel : 0)
+ , OuterTranslateLevel_(ysonOptionalLevel - 1)
+ , InnerOptionalTranslate_(skiffOptionalLevel > 0)
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer)
+ {
+ auto throwValueExpectedToBeNonempty = [&] {
+ ThrowYsonToSkiffConversionError(Descriptor_, "\"#\" found while value expected to be nonempty");
+ };
+
+ int outerOptionalsFound = 0;
+ for (; outerOptionalsFound < OuterExpectFilledLevel_; ++outerOptionalsFound) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::BeginList) {
+ cursor->Next();
+ } else if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ throwValueExpectedToBeNonempty();
+ } else {
+ ThrowBadYsonToken(
+ Descriptor_,
+ {EYsonItemType::BeginList},
+ cursor->GetCurrent().GetType());
+ }
+ }
+
+ for (; outerOptionalsFound < OuterTranslateLevel_; ++outerOptionalsFound) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::BeginList) {
+ writer->WriteVariant8Tag(1);
+ cursor->Next();
+ } else if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ writer->WriteVariant8Tag(0);
+ cursor->Next();
+ goto skip_end_list_tokens;
+ } else {
+ ThrowBadYsonToken(
+ Descriptor_,
+ {EYsonItemType::BeginList, EYsonItemType::EntityValue},
+ cursor->GetCurrent().GetType());
+ }
+ }
+
+ if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ if (InnerOptionalTranslate_) {
+ writer->WriteVariant8Tag(0);
+ } else {
+ throwValueExpectedToBeNonempty();
+ }
+ cursor->Next();
+ } else {
+ if (InnerOptionalTranslate_) {
+ writer->WriteVariant8Tag(1);
+ }
+ InnerConverter_(cursor, writer);
+ }
+
+skip_end_list_tokens:
+ for (int i = 0; i < outerOptionalsFound; ++i) {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(Descriptor_, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ }
+ }
+
+private:
+ const TYsonToSkiffConverter InnerConverter_;
+ const TComplexTypeFieldDescriptor Descriptor_;
+
+ // Max level of outer yson optional we expect to be filled.
+ const int OuterExpectFilledLevel_;
+ // Max level of outer optional we want to translate to yson.
+ const int OuterTranslateLevel_;
+
+ // If true we translate inner yson optional into Skiff optional
+ // if false we expect inner yson optional to be filled.
+ const bool InnerOptionalTranslate_;
+};
+
+class TOptionalNullYsonToSkiffConverterImpl
+{
+public:
+ TOptionalNullYsonToSkiffConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ int ysonOptionalLevel,
+ int skiffOptionalLevel)
+ : Descriptor_(std::move(descriptor))
+ , OuterExpectFilledLevel_(ysonOptionalLevel - skiffOptionalLevel)
+ , OuterTranslateLevel_(ysonOptionalLevel)
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer)
+ {
+ auto throwValueExpectedToBeNonempty = [&] {
+ ThrowYsonToSkiffConversionError(Descriptor_, "\"#\" found while value expected to be nonempty");
+ };
+
+ int outerOptionalsFound = 0;
+ for (; outerOptionalsFound < OuterExpectFilledLevel_; ++outerOptionalsFound) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::BeginList) {
+ cursor->Next();
+ } else if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ throwValueExpectedToBeNonempty();
+ } else {
+ ThrowBadYsonToken(
+ Descriptor_,
+ {EYsonItemType::BeginList},
+ cursor->GetCurrent().GetType());
+ }
+ }
+
+ for (; outerOptionalsFound < OuterTranslateLevel_; ++outerOptionalsFound) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::BeginList) {
+ writer->WriteVariant8Tag(1);
+ cursor->Next();
+ } else if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ writer->WriteVariant8Tag(0);
+ cursor->Next();
+ goto skip_end_list_tokens;
+ } else {
+ ThrowBadYsonToken(
+ Descriptor_,
+ {EYsonItemType::BeginList, EYsonItemType::EntityValue},
+ cursor->GetCurrent().GetType());
+ }
+ }
+
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EntityValue) {
+ ThrowBadYsonToken(
+ Descriptor_,
+ {EYsonItemType::EntityValue},
+ cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+
+skip_end_list_tokens:
+ for (int i = 0; i < outerOptionalsFound; ++i) {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(Descriptor_, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ }
+ }
+
+private:
+ const TComplexTypeFieldDescriptor Descriptor_;
+
+ // How many levels of yson optional we expect to be filled.
+ const int OuterExpectFilledLevel_;
+ // How many levels of outer optional we want to translate to yson
+ const int OuterTranslateLevel_;
+};
+
+TYsonToSkiffConverter CreateOptionalYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ const bool allowOmitOptional = config.AllowOmitTopLevelOptional && context.NestingLevel == 0;
+ auto match = MatchOptionalTypes(
+ descriptor,
+ skiffSchema,
+ allowOmitOptional);
+
+ if (match.InnerTypes.first.GetType()->IsNullable()) {
+ return TOptionalNullYsonToSkiffConverterImpl(
+ descriptor,
+ match.LogicalNesting,
+ match.SkiffNesting);
+ } else {
+ auto innerConverter = CreateYsonToSkiffConverterImpl(
+ std::move(match.InnerTypes.first),
+ match.InnerTypes.second,
+ context,
+ config);
+
+ return TOptionalYsonToSkiffConverterImpl(
+ innerConverter,
+ std::move(descriptor),
+ match.LogicalNesting,
+ match.SkiffNesting);
+ }
+}
+
+TYsonToSkiffConverter CreateListYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ auto match = MatchListTypes(descriptor, skiffSchema);
+ auto innerConverter = CreateYsonToSkiffConverterImpl(
+ std::move(match.first),
+ match.second,
+ context,
+ config);
+ return [innerConverter = innerConverter, descriptor = std::move(descriptor)] (
+ TYsonPullParserCursor* cursor,
+ TCheckedInDebugSkiffWriter* writer)
+ {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+
+ while (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ writer->WriteVariant8Tag(0);
+ innerConverter(cursor, writer);
+ }
+ writer->WriteVariant8Tag(EndOfSequenceTag<ui8>());
+ cursor->Next();
+ };
+}
+
+class TInfiniteEntity
+{
+public:
+ TInfiniteEntity()
+ : Stream_(TStringBuf("#;#;#;#;#;#;#;#;"))
+ , Parser_(&Stream_, EYsonType::ListFragment)
+ , Cursor_(&Parser_)
+ {
+ YT_VERIFY(Cursor_.TryConsumeFragmentStart());
+ }
+
+ TYsonPullParserCursor* GetCursor()
+ {
+ return &Cursor_;
+ }
+
+private:
+ class TRingBufferStream
+ : public IZeroCopyInput
+ {
+ public:
+ explicit TRingBufferStream(TStringBuf buffer)
+ : Buffer_(buffer)
+ , Pointer_(Buffer_.data())
+ { }
+
+ private:
+ size_t DoNext(const void** ptr, size_t len) override
+ {
+ const auto end = Buffer_.data() + Buffer_.size();
+ auto result = Min<size_t>(len, end - Pointer_);
+ *ptr = Pointer_;
+ Pointer_ += result;
+ if (Pointer_ == end) {
+ Pointer_ = Buffer_.data();
+ }
+ return result;
+ }
+
+ private:
+ const TStringBuf Buffer_;
+ const char* Pointer_;
+ };
+
+private:
+ TRingBufferStream Stream_;
+ TYsonPullParser Parser_;
+ TYsonPullParserCursor Cursor_;
+};
+
+TYsonToSkiffConverter CreateStructYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ TYsonToSkiffConverter skipYsonValue = [](TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* /*writer*/) {
+ cursor->SkipComplexValue();
+ };
+
+ TYsonToSkiffConverter writeNullUnknownField = [](TYsonPullParserCursor*, TCheckedInDebugSkiffWriter* writer) {
+ writer->WriteVariant8Tag(0);
+ };
+
+ auto fieldMatchList = MatchStructTypes(descriptor, skiffSchema, /*allowUnknownSkiffFields*/ true);
+ std::vector<TYsonToSkiffConverter> converterList;
+ for (const auto& match : fieldMatchList) {
+ if (match) {
+ auto [fieldDescriptor, fieldSkiffSchema] = *match;
+ if (fieldSkiffSchema) {
+ auto converter = CreateYsonToSkiffConverterImpl(fieldDescriptor, fieldSkiffSchema, context, config);
+ converterList.emplace_back(converter);
+ } else {
+ converterList.emplace_back(skipYsonValue);
+ }
+ } else {
+ converterList.emplace_back(writeNullUnknownField);
+ }
+ }
+
+ return [converterList = std::move(converterList), descriptor = std::move(descriptor)]
+ (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer) {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ for (auto it = converterList.begin(), end = converterList.end(); it != end; ++it) {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::EndList) {
+ TInfiniteEntity infiniteEntity;
+ auto entityCursor = infiniteEntity.GetCursor();
+ do {
+ const auto& converter = *it;
+ converter(entityCursor, writer);
+ ++it;
+ } while (it != end);
+ break;
+ }
+ const auto& converter = *it;
+ converter(cursor, writer);
+ }
+
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ };
+}
+
+TYsonToSkiffConverter CreateTupleYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ TYsonToSkiffConverter skipYsonValue = [](TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* /*writer*/) {
+ cursor->SkipComplexValue();
+ };
+
+ auto tupleMatch = MatchTupleTypes(descriptor, skiffSchema);
+ const auto& children = skiffSchema->GetChildren();
+ std::vector<TYsonToSkiffConverter> converterList;
+ for (int i = 0; i < std::ssize(tupleMatch); ++i) {
+ if (children[i]->GetWireType() == EWireType::Nothing) {
+ converterList.emplace_back(skipYsonValue);
+ } else {
+ const auto&[descriptor, skiffSchema] = tupleMatch[i];
+ auto converter = CreateYsonToSkiffConverterImpl(descriptor, skiffSchema, context, config);
+ converterList.emplace_back(converter);
+ }
+ }
+
+ return [converterList = std::move(converterList), descriptor = std::move(descriptor)]
+ (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer) {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ for (const auto& converter : converterList) {
+ converter(cursor, writer);
+ }
+
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ };
+}
+
+template <EWireType wireType>
+class TVariantYsonToSkiffConverterImpl
+{
+public:
+ TVariantYsonToSkiffConverterImpl(std::vector<TYsonToSkiffConverter> converterList, TComplexTypeFieldDescriptor descriptor)
+ : ConverterList_(std::move(converterList))
+ , Descriptor_(std::move(descriptor))
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer)
+ {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(Descriptor_, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ if (cursor->GetCurrent().GetType() != EYsonItemType::Int64Value) {
+ ThrowBadYsonToken(Descriptor_, {EYsonItemType::Int64Value}, cursor->GetCurrent().GetType());
+ }
+ auto tag = cursor->GetCurrent().UncheckedAsInt64();
+ cursor->Next();
+ if (tag >= std::ssize(ConverterList_)) {
+ ThrowYsonToSkiffConversionError(Descriptor_, "variant tag (%v) exceeds %v children count (%v)",
+ tag,
+ wireType,
+ ConverterList_.size());
+ }
+ if constexpr (wireType == EWireType::Variant8) {
+ writer->WriteVariant8Tag(tag);
+ } else {
+ static_assert(wireType == EWireType::Variant16);
+ writer->WriteVariant16Tag(tag);
+ }
+ ConverterList_[tag](cursor, writer);
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(Descriptor_, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ }
+
+private:
+ const std::vector<TYsonToSkiffConverter> ConverterList_;
+ const TComplexTypeFieldDescriptor Descriptor_;
+};
+
+TYsonToSkiffConverter CreateVariantYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ std::vector<TTypePair> variantMatch;
+ if (descriptor.GetType()->GetMetatype() == ELogicalMetatype::VariantStruct) {
+ variantMatch = MatchVariantStructTypes(descriptor, skiffSchema);
+ } else {
+ YT_VERIFY(descriptor.GetType()->GetMetatype() == ELogicalMetatype::VariantTuple);
+ variantMatch = MatchVariantTupleTypes(descriptor, skiffSchema);
+ }
+
+ std::vector<TYsonToSkiffConverter> converterList;
+ for (const auto&[descriptor, skiffSchema] : variantMatch) {
+ auto converter = CreateYsonToSkiffConverterImpl(descriptor, skiffSchema, context, config);
+ converterList.emplace_back(converter);
+ }
+
+ if (skiffSchema->GetWireType() == EWireType::Variant8) {
+ return TVariantYsonToSkiffConverterImpl<EWireType::Variant8>(std::move(converterList), std::move(descriptor));
+ } else if (skiffSchema->GetWireType() == EWireType::Variant16) {
+ return TVariantYsonToSkiffConverterImpl<EWireType::Variant16>(std::move(converterList), std::move(descriptor));
+ }
+ YT_ABORT();
+}
+
+TYsonToSkiffConverter CreateDictYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ const auto [keyMatch, valueMatch] = MatchDictTypes(descriptor, skiffSchema);
+ auto keyConverter = CreateYsonToSkiffConverterImpl(keyMatch.first, keyMatch.second, context, config);
+ auto valueConverter = CreateYsonToSkiffConverterImpl(valueMatch.first, valueMatch.second, context, config);
+
+ return [
+ keyConverter = std::move(keyConverter),
+ valueConverter = std::move(valueConverter),
+ descriptor = std::move(descriptor)
+ ] (TYsonPullParserCursor* cursor, TCheckedInDebugSkiffWriter* writer) {
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ while (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ writer->WriteVariant8Tag(0);
+ if (cursor->GetCurrent().GetType() != EYsonItemType::BeginList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::BeginList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ keyConverter(cursor, writer);
+ valueConverter(cursor, writer);
+ if (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ ThrowBadYsonToken(descriptor, {EYsonItemType::EndList}, cursor->GetCurrent().GetType());
+ }
+ cursor->Next();
+ }
+ writer->WriteVariant8Tag(EndOfSequenceTag<ui8>());
+ cursor->Next(); // Skip EYsonItemType::EndList.
+ };
+}
+
+TYsonToSkiffConverter CreateYsonToSkiffConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TYsonToSkiffConverterConfig& config)
+{
+ TConverterCreationContext innerContext = context;
+ ++innerContext.NestingLevel;
+ const auto& logicalType = descriptor.GetType();
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CreateSimpleYsonToSkiffConverter(std::move(descriptor), skiffSchema);
+ case ELogicalMetatype::Decimal:
+ return CreateDecimalYsonToSkiffConverter(std::move(descriptor), skiffSchema);
+ case ELogicalMetatype::Optional:
+ return CreateOptionalYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::List:
+ return CreateListYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Struct:
+ return CreateStructYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Tuple:
+ return CreateTupleYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::VariantStruct:
+ return CreateVariantYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Dict:
+ return CreateDictYsonToSkiffConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Tagged:
+ // We have detagged our type previously.
+ YT_ABORT();
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TFunction>
+class TPrimitiveTypeSkiffToYsonConverter
+{
+public:
+ explicit TPrimitiveTypeSkiffToYsonConverter(TFunction function = {})
+ : Function_(std::move(function))
+ { }
+
+ void operator() (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer)
+ {
+ auto value = Function_(parser);
+
+ using TValueType = std::decay_t<decltype(value)>;
+
+ if constexpr (std::is_same_v<TValueType, TStringBuf>) {
+ writer->WriteBinaryString(value);
+ } else if constexpr (
+ std::is_same_v<TValueType, i8> ||
+ std::is_same_v<TValueType, i16> ||
+ std::is_same_v<TValueType, i32> ||
+ std::is_same_v<TValueType, i64>)
+ {
+ writer->WriteBinaryInt64(value);
+ } else if constexpr (
+ std::is_same_v<TValueType, ui8> ||
+ std::is_same_v<TValueType, ui16> ||
+ std::is_same_v<TValueType, ui32> ||
+ std::is_same_v<TValueType, ui64>)
+ {
+ writer->WriteBinaryUint64(value);
+ } else if constexpr (std::is_same_v<TValueType, bool>) {
+ writer->WriteBinaryBoolean(value);
+ } else if constexpr (std::is_same_v<TValueType, double>) {
+ writer->WriteBinaryDouble(value);
+ } else if constexpr (std::is_same_v<TValueType, std::nullptr_t>) {
+ writer->WriteEntity();
+ } else {
+ static_assert(std::is_same_v<TValueType, TStringBuf>);
+ }
+ }
+
+private:
+ TFunction Function_;
+};
+
+class TYson32SkiffToYsonConverter
+{
+public:
+ Y_FORCE_INLINE void operator () (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer)
+ {
+ TMemoryInput inputStream(parser->ParseYson32());
+ TYsonPullParser pullParser(&inputStream, EYsonType::Node);
+ TYsonPullParserCursor(&pullParser).TransferComplexValue(writer);
+ }
+};
+
+TSkiffToYsonConverter CreatePrimitiveTypeSkiffToYsonConverter(EWireType wireType)
+{
+ switch (wireType) {
+#define CASE(x) \
+ case x: \
+ return TPrimitiveTypeSkiffToYsonConverter(TSimpleSkiffParser<x>());
+ CASE(EWireType::Int8)
+ CASE(EWireType::Int16)
+ CASE(EWireType::Int32)
+ CASE(EWireType::Int64)
+
+ CASE(EWireType::Uint8)
+ CASE(EWireType::Uint16)
+ CASE(EWireType::Uint32)
+ CASE(EWireType::Uint64)
+
+ CASE(EWireType::Boolean)
+ CASE(EWireType::Double)
+ CASE(EWireType::String32)
+ CASE(EWireType::Nothing)
+#undef CASE
+ case EWireType::Yson32:
+ return TYson32SkiffToYsonConverter();
+ default:
+ YT_ABORT();
+ }
+}
+
+TSkiffToYsonConverter CreateSimpleSkiffToYsonConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& /*context*/,
+ const TSkiffToYsonConverterConfig& /*config*/)
+{
+ try {
+ const auto& logicalType = descriptor.GetType()->AsSimpleTypeRef();
+ auto valueType = logicalType.GetElement();
+ auto wireType = skiffSchema->GetWireType();
+ switch (valueType) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+
+ case ESimpleLogicalValueType::Interval:
+ CheckWireType(wireType, {EWireType::Int8, EWireType::Int16, EWireType::Int32, EWireType::Int64});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ CheckWireType(wireType, {EWireType::Uint8, EWireType::Uint16, EWireType::Uint32, EWireType::Uint64});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Boolean:
+ CheckWireType(wireType, {EWireType::Boolean});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ CheckWireType(wireType, {EWireType::Double});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Json:
+ CheckWireType(wireType, {EWireType::String32});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Any:
+ CheckWireType(wireType, {EWireType::Yson32});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ CheckWireType(wireType, {EWireType::Nothing});
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+
+ case ESimpleLogicalValueType::Uuid:
+ CheckWireType(wireType, {EWireType::Uint128, EWireType::String32});
+ if (wireType == EWireType::Uint128) {
+ return TPrimitiveTypeSkiffToYsonConverter(TUuidParser());
+ } else {
+ return CreatePrimitiveTypeSkiffToYsonConverter(wireType);
+ }
+ }
+ YT_ABORT();
+ } catch (const std::exception& ex) {
+ RethrowCannotMatchField(descriptor, skiffSchema, ex);
+ }
+}
+
+class TOptionalSkiffToYsonConverterImpl
+{
+public:
+ TOptionalSkiffToYsonConverterImpl(
+ TSkiffToYsonConverter innerConverter,
+ TComplexTypeFieldDescriptor descriptor,
+ int ysonNesting,
+ int skiffNesting)
+ : InnerConverter_(std::move(innerConverter))
+ , Descriptor_(std::move(descriptor))
+ , OuterFill_(ysonNesting > 1 ? ysonNesting - skiffNesting : 0)
+ , OuterTranslate_(ysonNesting - OuterFill_ - 1)
+ , InnerTranslate_(skiffNesting > 0)
+ {
+ YT_VERIFY(skiffNesting >= 0);
+ YT_VERIFY(ysonNesting > 0);
+
+ YT_VERIFY(skiffNesting <= ysonNesting);
+ YT_VERIFY(ysonNesting <= skiffNesting + 1);
+ }
+
+ void operator () (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer)
+ {
+ for (int i = 0; i < OuterFill_; ++i) {
+ writer->WriteBeginList();
+ }
+
+ int outerOptionalsFilled = 0;
+ for (; outerOptionalsFilled < OuterTranslate_; ++outerOptionalsFilled) {
+ auto tag = parser->ParseVariant8Tag();
+ if (tag == 0) {
+ writer->WriteEntity();
+ goto write_list_ends;
+ } else if (tag == 1) {
+ writer->WriteBeginList();
+ } else {
+ ThrowUnexpectedVariant8Tag(tag);
+ }
+ }
+
+ if (InnerTranslate_) {
+ auto tag = parser->ParseVariant8Tag();
+ if (tag == 0) {
+ writer->WriteEntity();
+ goto write_list_ends;
+ } else if (tag != 1) {
+ ThrowUnexpectedVariant8Tag(tag);
+ }
+ }
+ InnerConverter_(parser, writer);
+
+write_list_ends:
+ const int toClose = outerOptionalsFilled + OuterFill_;
+ for (int i = 0; i < toClose; ++i) {
+ writer->WriteEndList();
+ }
+ }
+
+private:
+ void ThrowUnexpectedVariant8Tag(ui8 tag) const
+ {
+ ThrowSkiffToYsonConversionError(Descriptor_, "Unexpected %lv tag, expected %Qv or %Qv got %Qv",
+ EWireType::Variant8,
+ 0,
+ 1,
+ tag);
+ }
+
+private:
+ const TSkiffToYsonConverter InnerConverter_;
+ const TComplexTypeFieldDescriptor Descriptor_;
+
+ // How many levels of yson optional we set unconditionally.
+ const int OuterFill_;
+
+ // How many levels of Skiff optionals we translate to yson outer optionals (which are encoded as list).
+ const int OuterTranslate_;
+
+ const bool InnerTranslate_;
+};
+
+class TOptionalNullSkiffToYsonConverterImpl
+{
+public:
+ TOptionalNullSkiffToYsonConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ int ysonNesting,
+ int skiffNesting)
+ : Descriptor_(std::move(descriptor))
+ , OuterFill_(ysonNesting > 1 ? ysonNesting - skiffNesting : 0)
+ , OuterTranslate_(ysonNesting - OuterFill_)
+ {
+ YT_VERIFY(skiffNesting >= 0);
+ YT_VERIFY(ysonNesting > 0);
+
+ YT_VERIFY(skiffNesting <= ysonNesting);
+ YT_VERIFY(ysonNesting <= skiffNesting + 1);
+ }
+
+ void operator () (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer)
+ {
+ for (int i = 0; i < OuterFill_; ++i) {
+ writer->WriteBeginList();
+ }
+
+ int outerOptionalsFilled = 0;
+ for (; outerOptionalsFilled < OuterTranslate_; ++outerOptionalsFilled) {
+ auto tag = parser->ParseVariant8Tag();
+ if (tag == 0) {
+ writer->WriteEntity();
+ goto write_list_ends;
+ } else if (tag == 1) {
+ writer->WriteBeginList();
+ } else {
+ ThrowUnexpectedVariant8Tag(tag);
+ }
+ }
+ writer->WriteEntity();
+
+write_list_ends:
+ const int toClose = outerOptionalsFilled + OuterFill_;
+ for (int i = 0; i < toClose; ++i) {
+ writer->WriteEndList();
+ }
+ }
+
+private:
+ void ThrowUnexpectedVariant8Tag(ui8 tag) const
+ {
+ ThrowSkiffToYsonConversionError(Descriptor_, "Unexpected %lv tag, expected %Qv or %Qv got %Qv",
+ EWireType::Variant8,
+ 0,
+ 1,
+ tag);
+ }
+
+private:
+ const TComplexTypeFieldDescriptor Descriptor_;
+
+ // How many levels of yson optional we set unconditionally.
+ const int OuterFill_;
+
+ // How many levels of Skiff optionals we translate to yson outer optionals (which are encoded as list).
+ const int OuterTranslate_;
+};
+
+TSkiffToYsonConverter CreateOptionalSkiffToYsonConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ const bool allowOmitOptional = config.AllowOmitTopLevelOptional && context.NestingLevel == 0;
+ auto match = MatchOptionalTypes(descriptor, skiffSchema, allowOmitOptional);
+ if (match.LogicalNesting != match.SkiffNesting) {
+ if (!config.AllowOmitTopLevelOptional || context.NestingLevel > 0) {
+ RethrowCannotMatchField(descriptor, skiffSchema, TErrorException()
+ <<= TError("Optional nesting mismatch"));
+ }
+ }
+
+ if (match.InnerTypes.first.GetType()->IsNullable()) {
+ return TOptionalNullSkiffToYsonConverterImpl(std::move(descriptor), match.LogicalNesting, match.SkiffNesting);
+ } else {
+ auto innerConverter = CreateSkiffToYsonConverterImpl(
+ std::move(match.InnerTypes.first),
+ match.InnerTypes.second,
+ context,
+ config);
+ return TOptionalSkiffToYsonConverterImpl(
+ innerConverter, std::move(descriptor), match.LogicalNesting, match.SkiffNesting);
+ }
+}
+
+TSkiffToYsonConverter CreateListSkiffToYsonConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ auto match = MatchListTypes(descriptor, skiffSchema);
+ auto innerConverter = CreateSkiffToYsonConverterImpl(std::move(match.first), match.second, context, config);
+
+ return [innerConverter = innerConverter, descriptor=std::move(descriptor)](TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer) {
+ writer->WriteBeginList();
+ while (true) {
+ auto tag = parser->ParseVariant8Tag();
+ if (tag == EndOfSequenceTag<ui8>()) {
+ break;
+ } else if (tag != 0) {
+ ThrowSkiffToYsonConversionError(descriptor, "Unexpected %lv tag, expected %Qv or %Qv got %Qv",
+ EWireType::RepeatedVariant8,
+ 0,
+ EndOfSequenceTag<ui8>(),
+ tag);
+ }
+ innerConverter(parser, writer);
+ writer->WriteItemSeparator();
+ }
+ writer->WriteEndList();
+ };
+}
+
+TSkiffToYsonConverter CreateStructSkiffToYsonConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ const auto insertEntity = [](TCheckedInDebugSkiffParser* /*parser*/, TCheckedInDebugYsonTokenWriter* writer) {
+ writer->WriteEntity();
+ };
+
+ auto structMatch = MatchStructTypes(descriptor, skiffSchema, /*allowUnknownSkiffFields*/false);
+ std::vector<TSkiffToYsonConverter> converterList;
+ for (const auto& match : structMatch) {
+ const auto& [fieldDescriptor, fieldSkiffSchema] = *match;
+ if (fieldSkiffSchema) {
+ converterList.emplace_back(CreateSkiffToYsonConverterImpl(fieldDescriptor, fieldSkiffSchema, context, config));
+ } else if (fieldDescriptor.GetType()->GetMetatype() == ELogicalMetatype::Optional) {
+ converterList.emplace_back(insertEntity);
+ } else {
+ RethrowCannotMatchField(
+ descriptor,
+ skiffSchema,
+ TErrorException() <<= TError(
+ "Non optional struct field %Qv is missing in Skiff schema",
+ fieldDescriptor.GetDescription()));
+ }
+ }
+
+ return [converterList = converterList](TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer) {
+ writer->WriteBeginList();
+ for (const auto& converter : converterList) {
+ converter(parser, writer);
+ writer->WriteItemSeparator();
+ }
+ writer->WriteEndList();
+ };
+}
+
+TSkiffToYsonConverter CreateTupleSkiffToYsonConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ auto tupleMatch = MatchTupleTypes(descriptor, skiffSchema);
+ std::vector<TSkiffToYsonConverter> converterList;
+ for (const auto& [fieldDescriptor, fieldSkiffSchema] : tupleMatch) {
+ converterList.emplace_back(CreateSkiffToYsonConverterImpl(fieldDescriptor, fieldSkiffSchema, context, config));
+ }
+ return [converterList = converterList](TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer) {
+ writer->WriteBeginList();
+ for (const auto& converter : converterList) {
+ converter(parser, writer);
+ writer->WriteItemSeparator();
+ }
+ writer->WriteEndList();
+ };
+}
+
+template <EWireType wireType>
+class TVariantSkiffToYsonConverterImpl
+{
+public:
+ TVariantSkiffToYsonConverterImpl(std::vector<TSkiffToYsonConverter> converterList, TComplexTypeFieldDescriptor descriptor)
+ : ConverterList_(std::move(converterList))
+ , Descriptor_(std::move(descriptor))
+ { }
+
+ void operator () (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer)
+ {
+ int tag;
+ if constexpr (wireType == EWireType::Variant8) {
+ tag = parser->ParseVariant8Tag();
+ } else {
+ static_assert(wireType == EWireType::Variant16);
+ tag = parser->ParseVariant16Tag();
+ }
+
+ if (tag >= std::ssize(ConverterList_)) {
+ ThrowSkiffToYsonConversionError(Descriptor_, "Variant tag (%v) exceeds %v children count (%v)",
+ tag,
+ wireType,
+ ConverterList_.size());
+ }
+ writer->WriteBeginList();
+ writer->WriteBinaryInt64(tag);
+ writer->WriteItemSeparator();
+ ConverterList_[tag](parser, writer);
+ writer->WriteItemSeparator();
+ writer->WriteEndList();
+ }
+
+private:
+ const std::vector<TSkiffToYsonConverter> ConverterList_;
+ const TComplexTypeFieldDescriptor Descriptor_;
+};
+
+TSkiffToYsonConverter CreateVariantSkiffToYsonConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ std::vector<TTypePair> variantMatch;
+ if (descriptor.GetType()->GetMetatype() == ELogicalMetatype::VariantStruct) {
+ variantMatch = MatchVariantStructTypes(descriptor, skiffSchema);
+ } else {
+ YT_VERIFY(descriptor.GetType()->GetMetatype() == ELogicalMetatype::VariantTuple);
+ variantMatch = MatchVariantTupleTypes(descriptor, skiffSchema);
+ }
+
+ std::vector<TSkiffToYsonConverter> converterList;
+ for (const auto& [fieldDescriptor, fieldSkiffSchema] : variantMatch) {
+ converterList.emplace_back(CreateSkiffToYsonConverterImpl(fieldDescriptor, fieldSkiffSchema, context, config));
+ }
+
+ if (skiffSchema->GetWireType() == EWireType::Variant8) {
+ return TVariantSkiffToYsonConverterImpl<EWireType::Variant8>(std::move(converterList), std::move(descriptor));
+ } else {
+ YT_VERIFY(skiffSchema->GetWireType() == EWireType::Variant16);
+ return TVariantSkiffToYsonConverterImpl<EWireType::Variant16>(std::move(converterList), std::move(descriptor));
+ }
+}
+
+TSkiffToYsonConverter CreateDictSkiffToYsonConverter(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ auto [keyMatch, valueMatch] = MatchDictTypes(descriptor, skiffSchema);
+ auto keyConverter = CreateSkiffToYsonConverterImpl(std::move(keyMatch.first), keyMatch.second, context, config);
+ auto valueConverter = CreateSkiffToYsonConverterImpl(std::move(valueMatch.first), valueMatch.second, context, config);
+
+ return [
+ keyConverter = std::move(keyConverter),
+ valueConverter = std::move(valueConverter),
+ descriptor=std::move(descriptor)
+ ] (TCheckedInDebugSkiffParser* parser, TCheckedInDebugYsonTokenWriter* writer) {
+ writer->WriteBeginList();
+ while (true) {
+ auto tag = parser->ParseVariant8Tag();
+ if (tag == EndOfSequenceTag<ui8>()) {
+ break;
+ } else if (tag != 0) {
+ ThrowSkiffToYsonConversionError(descriptor, "Unexpected %lv tag, expected %Qv or %Qv got %Qv",
+ EWireType::RepeatedVariant8,
+ 0,
+ EndOfSequenceTag<ui8>(),
+ tag);
+ }
+ writer->WriteBeginList();
+ {
+ keyConverter(parser, writer);
+ writer->WriteItemSeparator();
+ valueConverter(parser, writer);
+ writer->WriteItemSeparator();
+ }
+ writer->WriteEndList();
+ writer->WriteItemSeparator();
+ }
+ writer->WriteEndList();
+ };
+}
+
+TSkiffToYsonConverter CreateDecimalSkiffToYsonConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema)
+{
+ const auto& logicalType = descriptor.GetType();
+ int precision = logicalType->AsDecimalTypeRef().GetPrecision();
+ auto wireType = skiffSchema->GetWireType();
+ switch (wireType) {
+ case EWireType::Int32:
+ return TPrimitiveTypeSkiffToYsonConverter(TDecimalSkiffParser<EWireType::Int32>(precision));
+ case EWireType::Int64:
+ return TPrimitiveTypeSkiffToYsonConverter(TDecimalSkiffParser<EWireType::Int64>(precision));
+ case EWireType::Int128:
+ return TPrimitiveTypeSkiffToYsonConverter(TDecimalSkiffParser<EWireType::Int128>(precision));
+ default:
+ CheckSkiffWireTypeForDecimal(precision, wireType);
+ // Previous call must throw
+ YT_ABORT();
+ }
+}
+
+TSkiffToYsonConverter CreateSkiffToYsonConverterImpl(
+ TComplexTypeFieldDescriptor descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TConverterCreationContext& context,
+ const TSkiffToYsonConverterConfig& config)
+{
+ TConverterCreationContext innerContext = context;
+ ++innerContext.NestingLevel;
+ const auto& logicalType = descriptor.GetType();
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CreateSimpleSkiffToYsonConverter(descriptor, skiffSchema, innerContext, config);
+ case ELogicalMetatype::Decimal:
+ return CreateDecimalSkiffToYsonConverter(descriptor, skiffSchema);
+ case ELogicalMetatype::Optional:
+ return CreateOptionalSkiffToYsonConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::List:
+ return CreateListSkiffToYsonConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Struct:
+ return CreateStructSkiffToYsonConverter(descriptor, skiffSchema, innerContext, config);
+ case ELogicalMetatype::Tuple:
+ return CreateTupleSkiffToYsonConverter(descriptor, skiffSchema, innerContext, config);
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::VariantTuple:
+ return CreateVariantSkiffToYsonConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Dict:
+ return CreateDictSkiffToYsonConverter(std::move(descriptor), skiffSchema, innerContext, config);
+ case ELogicalMetatype::Tagged:
+ // We have detagged our type previously.
+ break;
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonToSkiffConverter CreateYsonToSkiffConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TYsonToSkiffConverterConfig& config)
+{
+ TConverterCreationContext context;
+ // CreateYsonToSkiffConverterImpl will increment NestingLevel to 0 for the top level element.
+ context.NestingLevel = -1;
+ return CreateYsonToSkiffConverterImpl(descriptor.Detag(), skiffSchema, context, config);
+}
+
+TSkiffToYsonConverter CreateSkiffToYsonConverter(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ const TSkiffToYsonConverterConfig& config)
+{
+ TConverterCreationContext context;
+ context.NestingLevel = -1;
+ return CreateSkiffToYsonConverterImpl(descriptor.Detag(), skiffSchema, context, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckSkiffWireTypeForDecimal(int precision, NSkiff::EWireType wireType)
+{
+ using namespace NSkiff;
+
+ const auto decimalBinarySize = NDecimal::TDecimal::GetValueBinarySize(precision);
+ int skiffBinarySize = 0;
+ if (wireType == NSkiff::EWireType::Int32) {
+ skiffBinarySize = sizeof(i32);
+ } else if (wireType == NSkiff::EWireType::Int64) {
+ skiffBinarySize = sizeof(i64);
+ } else if (wireType == NSkiff::EWireType::Int128) {
+ skiffBinarySize = 2 * sizeof(i64);
+ }
+
+ if (decimalBinarySize != skiffBinarySize) {
+ THROW_ERROR_EXCEPTION("Skiff type %v cannot represent type Decimal<%v, ?>",
+ wireType,
+ precision);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckWireType(EWireType wireType, const std::initializer_list<EWireType>& allowed)
+{
+ if (std::find(allowed.begin(), allowed.end(), wireType) == allowed.end()) {
+ THROW_ERROR_EXCEPTION("Unexpected wire type %Qlv",
+ wireType);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/skiff_yson_converter.h b/yt/yt/client/formats/skiff_yson_converter.h
new file mode 100644
index 0000000000..233b106729
--- /dev/null
+++ b/yt/yt/client/formats/skiff_yson_converter.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include <yt/yt/client/table_client/public.h>
+#include <yt/yt/client/table_client/row_base.h>
+
+#include <yt/yt/library/decimal/decimal.h>
+#include <yt/yt/core/yson/public.h>
+
+#include <library/cpp/skiff/skiff.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TYsonToSkiffConverter = std::function<void(NYson::TYsonPullParserCursor*, NSkiff::TCheckedInDebugSkiffWriter*)>;
+using TSkiffToYsonConverter = std::function<void(NSkiff::TCheckedInDebugSkiffParser*, NYson::TCheckedInDebugYsonTokenWriter*)>;
+
+struct TYsonToSkiffConverterConfig
+{
+ // Usually skiffSchema MUST match descriptor.LogicalType.
+ // But when AllowOmitTopLevelOptional is set to true and descriptor.LogicalType is Optional<SomeInnerType>
+ // skiffSchema CAN match SomeInnerType. In that case returned converter will throw error when it gets empty value.
+ //
+ // Useful for sparse fields.
+ bool AllowOmitTopLevelOptional = false;
+};
+
+TYsonToSkiffConverter CreateYsonToSkiffConverter(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<NSkiff::TSkiffSchema>& skiffSchema,
+ const TYsonToSkiffConverterConfig& config = {});
+
+struct TSkiffToYsonConverterConfig
+{
+ // Similar to TYsonToSkiffConverterConfig::AllowOmitTopLevelOptional.
+ bool AllowOmitTopLevelOptional = false;
+};
+
+TSkiffToYsonConverter CreateSkiffToYsonConverter(
+ const NTableClient::TComplexTypeFieldDescriptor& descriptor,
+ const std::shared_ptr<NSkiff::TSkiffSchema>& skiffSchema,
+ const TSkiffToYsonConverterConfig& config = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType wireType>
+struct TSimpleSkiffParser
+{
+ Y_FORCE_INLINE auto operator () (NSkiff::TCheckedInDebugSkiffParser* parser) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType SkiffWireType>
+class TDecimalSkiffParser
+{
+public:
+ explicit TDecimalSkiffParser(int precision);
+ Y_FORCE_INLINE TStringBuf operator() (NSkiff::TCheckedInDebugSkiffParser* parser) const;
+
+private:
+ const int Precision_;
+ mutable char Buffer_[NDecimal::TDecimal::MaxBinarySize];
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NSkiff::EWireType SkiffWireType>
+class TDecimalSkiffWriter
+{
+public:
+ explicit TDecimalSkiffWriter(int precision);
+
+ void operator() (TStringBuf value, NSkiff::TCheckedInDebugSkiffWriter* writer) const;
+
+private:
+ const int Precision_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUuidParser
+{
+public:
+ Y_FORCE_INLINE TStringBuf operator() (NSkiff::TCheckedInDebugSkiffParser* parser) const;
+
+private:
+ mutable ui64 Buffer_[2];
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUuidWriter
+{
+public:
+ Y_FORCE_INLINE void operator() (TStringBuf value, NSkiff::TCheckedInDebugSkiffWriter* writer) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckSkiffWireTypeForDecimal(int precision, NSkiff::EWireType wireType);
+void CheckWireType(NSkiff::EWireType wireType, const std::initializer_list<NSkiff::EWireType>& expected);
+
+template <NSkiff::EWireType wireType, typename TValueType>
+Y_FORCE_INLINE void CheckIntSize(TValueType value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::Formats
+
+#define SKIFF_YSON_CONVERTER_INL_H_
+#include "skiff_yson_converter-inl.h"
+#undef SKIFF_YSON_CONVERTER_INL_H_
diff --git a/yt/yt/client/formats/unversioned_value_yson_writer.cpp b/yt/yt/client/formats/unversioned_value_yson_writer.cpp
new file mode 100644
index 0000000000..c89dd48373
--- /dev/null
+++ b/yt/yt/client/formats/unversioned_value_yson_writer.cpp
@@ -0,0 +1,84 @@
+#include "unversioned_value_yson_writer.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_value.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+namespace NYT::NFormats {
+
+using namespace NYson;
+using namespace NTableClient;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedValueYsonWriter::TUnversionedValueYsonWriter(
+ const TNameTablePtr& nameTable,
+ const TTableSchemaPtr& tableSchema,
+ const TYsonConverterConfig& config)
+{
+ if (config.ComplexTypeMode == EComplexTypeMode::Positional &&
+ config.DecimalMode == EDecimalMode::Binary &&
+ config.TimeMode == ETimeMode::Binary &&
+ config.UuidMode == EUuidMode::Binary) {
+ return;
+ }
+
+ const auto& columns = tableSchema->Columns();
+ for (size_t i = 0; i != columns.size(); ++i) {
+ const auto& column = columns[i];
+ auto id = nameTable->GetIdOrRegisterName(column.Name());
+ TComplexTypeFieldDescriptor descriptor(column.Name(), column.LogicalType());
+ auto converter = CreateYsonServerToClientConverter(descriptor, config);
+ if (converter) {
+ ColumnConverters_.emplace(id, std::move(converter));
+ }
+ }
+}
+
+void TUnversionedValueYsonWriter::WriteValue(const TUnversionedValue& value, IYsonConsumer* consumer)
+{
+ if (value.Type == EValueType::Null) {
+ consumer->OnEntity();
+ return;
+ }
+ auto it = ColumnConverters_.find(value.Id);
+ if (it != ColumnConverters_.end()) {
+ it->second(value, consumer);
+ return;
+ }
+ switch (value.Type) {
+ case EValueType::Int64:
+ consumer->OnInt64Scalar(value.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ consumer->OnUint64Scalar(value.Data.Uint64);
+ return;
+ case EValueType::Double:
+ consumer->OnDoubleScalar(value.Data.Double);
+ return;
+ case EValueType::Boolean:
+ consumer->OnBooleanScalar(value.Data.Boolean);
+ return;
+ case EValueType::String:
+ consumer->OnStringScalar(value.AsStringBuf());
+ return;
+ case EValueType::Any:
+ case EValueType::Composite:
+ consumer->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ return;
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::TheBottom:
+ case EValueType::Max:
+ break;
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/unversioned_value_yson_writer.h b/yt/yt/client/formats/unversioned_value_yson_writer.h
new file mode 100644
index 0000000000..1b6f671298
--- /dev/null
+++ b/yt/yt/client/formats/unversioned_value_yson_writer.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/client/complex_types/yson_format_conversion.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnversionedValueYsonWriter
+{
+public:
+ TUnversionedValueYsonWriter(
+ const NTableClient::TNameTablePtr& nameTable,
+ const NTableClient::TTableSchemaPtr& tableSchema,
+ const NComplexTypes::TYsonConverterConfig& config);
+
+ void WriteValue(const NTableClient::TUnversionedValue& value, NYson::IYsonConsumer* consumer);
+
+private:
+ THashMap<int, NComplexTypes::TYsonServerToClientConverter> ColumnConverters_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats \ No newline at end of file
diff --git a/yt/yt/client/formats/versioned_writer.cpp b/yt/yt/client/formats/versioned_writer.cpp
new file mode 100644
index 0000000000..056c86d5e1
--- /dev/null
+++ b/yt/yt/client/formats/versioned_writer.cpp
@@ -0,0 +1,138 @@
+#include "versioned_writer.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVersionedWriter::TVersionedWriter(
+ NConcurrency::IAsyncOutputStreamPtr stream,
+ NTableClient::TTableSchemaPtr schema,
+ const std::function<std::unique_ptr<IFlushableYsonConsumer>(IZeroCopyOutput*)>& consumerBuilder)
+ : Stream_(std::move(stream))
+ , Schema_(std::move(schema))
+ , Consumer_(consumerBuilder(&Buffer_))
+{ }
+
+TFuture<void> TVersionedWriter::Close()
+{
+ return Result_;
+}
+
+bool TVersionedWriter::Write(TRange<TVersionedRow> rows)
+{
+ Buffer_.Clear();
+
+ auto consumeUnversionedData = [&] (const TUnversionedValue& value) {
+ switch (value.Type) {
+ case EValueType::Int64:
+ Consumer_->OnInt64Scalar(value.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ Consumer_->OnUint64Scalar(value.Data.Uint64);
+ return;
+ case EValueType::Double:
+ Consumer_->OnDoubleScalar(value.Data.Double);
+ return;
+ case EValueType::Boolean:
+ Consumer_->OnBooleanScalar(value.Data.Boolean);
+ return;
+ case EValueType::String:
+ Consumer_->OnStringScalar(value.AsStringBuf());
+ return;
+ case EValueType::Null:
+ Consumer_->OnEntity();
+ return;
+ case EValueType::Any:
+ Consumer_->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ return;
+ case EValueType::Composite:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+ }
+ ThrowUnexpectedValueType(value.Type);
+ };
+
+ for (auto row : rows) {
+ if (!row) {
+ Consumer_->OnEntity();
+ continue;
+ }
+
+ Consumer_->OnBeginAttributes();
+ {
+ Consumer_->OnKeyedItem("write_timestamps");
+ Consumer_->OnBeginList();
+ for (auto timestamp : row.WriteTimestamps()) {
+ Consumer_->OnListItem();
+ Consumer_->OnUint64Scalar(timestamp);
+ }
+ Consumer_->OnEndList();
+ }
+ {
+ Consumer_->OnKeyedItem("delete_timestamps");
+ Consumer_->OnBeginList();
+ for (auto timestamp : row.DeleteTimestamps()) {
+ Consumer_->OnListItem();
+ Consumer_->OnUint64Scalar(timestamp);
+ }
+ Consumer_->OnEndList();
+ }
+ Consumer_->OnEndAttributes();
+
+ Consumer_->OnBeginMap();
+ for (const auto& value : row.Keys()) {
+ const auto& column = Schema_->Columns()[value.Id];
+ Consumer_->OnKeyedItem(column.Name());
+ consumeUnversionedData(value);
+ }
+ for (auto valuesBeginIt = row.BeginValues(), valuesEndIt = row.EndValues(); valuesBeginIt != valuesEndIt; /**/) {
+ auto columnBeginIt = valuesBeginIt;
+ auto columnEndIt = columnBeginIt;
+ while (columnEndIt < valuesEndIt && columnEndIt->Id == columnBeginIt->Id) {
+ ++columnEndIt;
+ }
+
+ const auto& column = Schema_->Columns()[columnBeginIt->Id];
+ Consumer_->OnKeyedItem(column.Name());
+ Consumer_->OnBeginList();
+ while (columnBeginIt != columnEndIt) {
+ Consumer_->OnListItem();
+ Consumer_->OnBeginAttributes();
+ Consumer_->OnKeyedItem("timestamp");
+ Consumer_->OnUint64Scalar(columnBeginIt->Timestamp);
+ Consumer_->OnKeyedItem("aggregate");
+ Consumer_->OnBooleanScalar(Any(columnBeginIt->Flags & EValueFlags::Aggregate));
+ Consumer_->OnEndAttributes();
+ consumeUnversionedData(*columnBeginIt);
+ ++columnBeginIt;
+ }
+ Consumer_->OnEndList();
+
+ valuesBeginIt = columnEndIt;
+ }
+ Consumer_->OnEndMap();
+ }
+
+ Consumer_->Flush();
+ auto buffer = Buffer_.Flush();
+ Result_ = Stream_->Write(buffer);
+ return Result_.IsSet() && Result_.Get().IsOK();
+}
+
+TFuture<void> TVersionedWriter::GetReadyEvent()
+{
+ return Result_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/versioned_writer.h b/yt/yt/client/formats/versioned_writer.h
new file mode 100644
index 0000000000..215f4e6537
--- /dev/null
+++ b/yt/yt/client/formats/versioned_writer.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/versioned_writer.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVersionedWriter
+ : public NTableClient::IVersionedWriter
+{
+public:
+ TVersionedWriter(
+ NConcurrency::IAsyncOutputStreamPtr stream,
+ NTableClient::TTableSchemaPtr schema,
+ const std::function<std::unique_ptr<NYson::IFlushableYsonConsumer>(IZeroCopyOutput*)>& consumerBuilder);
+
+ virtual TFuture<void> Close() override;
+
+ virtual bool Write(TRange<NTableClient::TVersionedRow> rows) override;
+
+ virtual TFuture<void> GetReadyEvent() override;
+
+private:
+ const NConcurrency::IAsyncOutputStreamPtr Stream_;
+ const NTableClient::TTableSchemaPtr Schema_;
+
+ TBlobOutput Buffer_;
+ TFuture<void> Result_;
+
+ const std::unique_ptr<NYson::IFlushableYsonConsumer> Consumer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/web_json_writer.cpp b/yt/yt/client/formats/web_json_writer.cpp
new file mode 100644
index 0000000000..f76ce0e16e
--- /dev/null
+++ b/yt/yt/client/formats/web_json_writer.cpp
@@ -0,0 +1,772 @@
+#include "web_json_writer.h"
+
+#include "config.h"
+#include "format.h"
+#include "public.h"
+#include "schemaless_writer_adapter.h"
+#include "yql_yson_converter.h"
+
+#include <yt/yt/client/complex_types/yson_format_conversion.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/yson/format.h>
+
+#include <yt/yt/core/json/json_writer.h>
+#include <yt/yt/core/json/config.h>
+#include <yt/yt/core/json/helpers.h>
+
+#include <yt/yt/core/misc/utf8_decoder.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/stream/length.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NComplexTypes;
+using namespace NYTree;
+using namespace NYson;
+using namespace NJson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto ContextBufferCapacity = 1_MB;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWebJsonColumnFilter
+{
+public:
+ TWebJsonColumnFilter(int maxSelectedColumnCount, std::optional<THashSet<TString>> names)
+ : MaxSelectedColumnCount_(maxSelectedColumnCount)
+ , Names_(std::move(names))
+ { }
+
+ bool Accept(ui16 columnId, TStringBuf columnName)
+ {
+ if (Names_) {
+ return AcceptByNames(columnName);
+ }
+ return AcceptByMaxCount(columnId);
+ }
+
+private:
+ const int MaxSelectedColumnCount_;
+ std::optional<THashSet<TString>> Names_;
+
+ THashSet<ui16> AcceptedColumnIds_;
+
+ bool AcceptByNames(TStringBuf columnName)
+ {
+ return Names_->contains(columnName);
+ }
+
+ bool AcceptByMaxCount(ui16 columnId)
+ {
+ if (std::ssize(AcceptedColumnIds_) < MaxSelectedColumnCount_) {
+ AcceptedColumnIds_.insert(columnId);
+ return true;
+ }
+ return AcceptedColumnIds_.contains(columnId);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWebJsonColumnFilter CreateWebJsonColumnFilter(const TWebJsonFormatConfigPtr& webJsonConfig)
+{
+ std::optional<THashSet<TString>> columnNames;
+ if (webJsonConfig->ColumnNames) {
+ columnNames.emplace();
+ for (const auto& columnName : *webJsonConfig->ColumnNames) {
+ if (!columnNames->insert(columnName).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column name %Qv in \"column_names\" parameter of web_json format config",
+ columnName);
+ }
+ }
+ }
+ return TWebJsonColumnFilter(webJsonConfig->MaxSelectedColumnCount, std::move(columnNames));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStringBuf GetSimpleYqlTypeName(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Float:
+ return TStringBuf("Float");
+ case ESimpleLogicalValueType::Double:
+ return TStringBuf("Double");
+ case ESimpleLogicalValueType::Boolean:
+ return TStringBuf("Boolean");
+ case ESimpleLogicalValueType::String:
+ return TStringBuf("String");
+ case ESimpleLogicalValueType::Utf8:
+ return TStringBuf("Utf8");
+ case ESimpleLogicalValueType::Any:
+ return TStringBuf("Yson");
+ case ESimpleLogicalValueType::Json:
+ return TStringBuf("Json");
+ case ESimpleLogicalValueType::Int8:
+ return TStringBuf("Int8");
+ case ESimpleLogicalValueType::Int16:
+ return TStringBuf("Int16");
+ case ESimpleLogicalValueType::Int32:
+ return TStringBuf("Int32");
+ case ESimpleLogicalValueType::Int64:
+ return TStringBuf("Int64");
+ case ESimpleLogicalValueType::Uint8:
+ return TStringBuf("Uint8");
+ case ESimpleLogicalValueType::Uint16:
+ return TStringBuf("Uint16");
+ case ESimpleLogicalValueType::Uint32:
+ return TStringBuf("Uint32");
+ case ESimpleLogicalValueType::Uint64:
+ return TStringBuf("Uint64");
+ case ESimpleLogicalValueType::Date:
+ return TStringBuf("Date");
+ case ESimpleLogicalValueType::Datetime:
+ return TStringBuf("Datetime");
+ case ESimpleLogicalValueType::Timestamp:
+ return TStringBuf("Timestamp");
+ case ESimpleLogicalValueType::Interval:
+ return TStringBuf("Interval");
+ case ESimpleLogicalValueType::Uuid:
+ return TStringBuf("Uuid");
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ // This case must have been processed earlier.
+ YT_ABORT();
+ }
+ YT_ABORT();
+}
+
+void SerializeAsYqlType(TFluentAny fluent, const TLogicalTypePtr& type)
+{
+ auto serializeStruct = [] (TFluentList fluentList, const TStructLogicalTypeBase& structType) {
+ fluentList
+ .Item().Value("StructType")
+ .Item().DoListFor(structType.GetFields(), [] (TFluentList innerFluentList, const TStructField& field) {
+ innerFluentList
+ .Item()
+ .BeginList()
+ .Item().Value(field.Name)
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, field.Type);
+ })
+ .EndList();
+ });
+ };
+
+ auto serializeTuple = [] (TFluentList fluentList, const TTupleLogicalTypeBase& tupleType) {
+ fluentList
+ .Item().Value("TupleType")
+ .Item().DoListFor(tupleType.GetElements(), [&] (TFluentList innerFluentList, const TLogicalTypePtr& element) {
+ innerFluentList
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, element);
+ });
+ });
+ };
+
+ auto build = [&] (TFluentList fluentList){
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ if (type->AsSimpleTypeRef().GetElement() == ESimpleLogicalValueType::Null) {
+ fluentList
+ .Item().Value("NullType");
+ } else if (type->AsSimpleTypeRef().GetElement() == ESimpleLogicalValueType::Void) {
+ fluentList
+ .Item().Value("VoidType");
+ } else {
+ fluentList
+ .Item().Value("DataType")
+ .Item().Value(GetSimpleYqlTypeName(type->AsSimpleTypeRef().GetElement()));
+ }
+ return;
+ case ELogicalMetatype::Decimal:
+ fluentList
+ .Item().Value("DataType")
+ .Item().Value("Decimal")
+ .Item().Value(ToString(type->AsDecimalTypeRef().GetPrecision()))
+ .Item().Value(ToString(type->AsDecimalTypeRef().GetScale()));
+ return;
+ case ELogicalMetatype::Optional:
+ fluentList
+ .Item().Value("OptionalType")
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type->AsOptionalTypeRef().GetElement());
+ });
+ return;
+ case ELogicalMetatype::List:
+ fluentList
+ .Item().Value("ListType")
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type->AsListTypeRef().GetElement());
+ });
+ return;
+ case ELogicalMetatype::Struct:
+ serializeStruct(fluentList, type->AsStructTypeRef());
+ return;
+ case ELogicalMetatype::Tuple:
+ serializeTuple(fluentList, type->AsTupleTypeRef());
+ return;
+ case ELogicalMetatype::VariantStruct:
+ fluentList
+ .Item().Value("VariantType")
+ .Item().DoList([&] (TFluentList fluentList) {
+ serializeStruct(fluentList, type->AsVariantStructTypeRef());
+ });
+ return;
+ case ELogicalMetatype::VariantTuple:
+ fluentList
+ .Item().Value("VariantType")
+ .Item().DoList([&] (TFluentList fluentList) {
+ serializeTuple(fluentList, type->AsVariantTupleTypeRef());
+ });
+ return;
+ case ELogicalMetatype::Dict:
+ fluentList
+ .Item().Value("DictType")
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type->AsDictTypeRef().GetKey());
+ })
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type->AsDictTypeRef().GetValue());
+ });
+ return;
+ case ELogicalMetatype::Tagged:
+ fluentList
+ .Item().Value("TaggedType")
+ .Item().Value(type->AsTaggedTypeRef().GetTag())
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type->AsTaggedTypeRef().GetElement());
+ });
+ return;
+ }
+ YT_ABORT();
+ };
+
+ fluent.DoList(build);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYqlValueWriter
+{
+public:
+ TYqlValueWriter(
+ const TWebJsonFormatConfigPtr& config,
+ const TNameTablePtr& /*nameTable*/,
+ const std::vector<TTableSchemaPtr>& schemas,
+ IJsonWriter* consumer)
+ : Consumer_(consumer)
+ , TableIndexToColumnIdToTypeIndex_(schemas.size())
+ {
+ YT_VERIFY(config->ValueFormat == EWebJsonValueFormat::Yql);
+ auto converterConfig = CreateYqlJsonConsumerConfig(config);
+
+ for (auto valueType : TEnumTraits<EValueType>::GetDomainValues()) {
+ if (IsValueType(valueType) || valueType == EValueType::Null || valueType == EValueType::Composite) {
+ TLogicalTypePtr logicalType = valueType == EValueType::Composite
+ ? SimpleLogicalType(ESimpleLogicalValueType::Any)
+ : SimpleLogicalType(GetLogicalType(valueType));
+ Types_.push_back(logicalType);
+ Converters_.push_back(CreateUnversionedValueToYqlConverter(Types_.back(), converterConfig, Consumer_));
+ ValueTypeToTypeIndex_[valueType] = static_cast<int>(Types_.size()) - 1;
+ } else {
+ ValueTypeToTypeIndex_[valueType] = UnknownTypeIndex;
+ }
+ }
+
+ for (int tableIndex = 0; tableIndex != static_cast<int>(schemas.size()); ++tableIndex) {
+ const auto& schema = schemas[tableIndex];
+ for (const auto& column : schema->Columns()) {
+ Types_.push_back(column.LogicalType());
+ Converters_.push_back(
+ CreateUnversionedValueToYqlConverter(column.LogicalType(), converterConfig, Consumer_));
+ auto [it, inserted] = TableIndexAndColumnNameToTypeIndex_.emplace(
+ std::make_pair(tableIndex, column.Name()),
+ static_cast<int>(Types_.size()) - 1);
+ YT_VERIFY(inserted);
+ }
+ }
+ }
+
+ void WriteValue(int tableIndex, TStringBuf columnName, TUnversionedValue value)
+ {
+ Consumer_->OnBeginList();
+
+ Consumer_->OnListItem();
+ auto typeIndex = GetTypeIndex(tableIndex, value.Id, columnName, value.Type);
+ Converters_[typeIndex](value);
+
+ Consumer_->OnListItem();
+ Consumer_->OnStringScalar(ToString(typeIndex));
+
+ Consumer_->OnEndList();
+ }
+
+ void WriteMetaInfo()
+ {
+ Consumer_->OnKeyedItem("yql_type_registry");
+ BuildYsonFluently(Consumer_)
+ .DoListFor(Types_, [&] (TFluentList fluentList, const TLogicalTypePtr& type) {
+ fluentList
+ .Item().Do([&] (TFluentAny innerFluent) {
+ SerializeAsYqlType(innerFluent, type);
+ });
+ });
+ }
+
+private:
+ static constexpr int UnknownTypeIndex = -1;
+ static constexpr int UnschematizedTypeIndex = -2;
+
+ IJsonWriter* Consumer_;
+ std::vector<TUnversionedValueToYqlConverter> Converters_;
+ std::vector<TLogicalTypePtr> Types_;
+ std::vector<std::vector<int>> TableIndexToColumnIdToTypeIndex_;
+ THashMap<std::pair<int, TString>, int> TableIndexAndColumnNameToTypeIndex_;
+ TEnumIndexedVector<EValueType, int> ValueTypeToTypeIndex_;
+
+private:
+ int GetTypeIndex(int tableIndex, ui16 columnId, TStringBuf columnName, EValueType valueType)
+ {
+ YT_VERIFY(0 <= tableIndex && tableIndex < static_cast<int>(TableIndexToColumnIdToTypeIndex_.size()));
+ auto& columnIdToTypeIndex = TableIndexToColumnIdToTypeIndex_[tableIndex];
+ if (columnId >= columnIdToTypeIndex.size()) {
+ columnIdToTypeIndex.resize(columnId + 1, UnknownTypeIndex);
+ }
+
+ auto typeIndex = columnIdToTypeIndex[columnId];
+ if (typeIndex == UnschematizedTypeIndex) {
+ typeIndex = ValueTypeToTypeIndex_[valueType];
+ } else if (typeIndex == UnknownTypeIndex) {
+ auto it = TableIndexAndColumnNameToTypeIndex_.find(std::make_pair(tableIndex, columnName));
+ if (it == TableIndexAndColumnNameToTypeIndex_.end()) {
+ typeIndex = ValueTypeToTypeIndex_[valueType];
+ columnIdToTypeIndex[columnId] = UnschematizedTypeIndex;
+ } else {
+ typeIndex = columnIdToTypeIndex[columnId] = it->second;
+ }
+ }
+
+ YT_VERIFY(typeIndex != UnknownTypeIndex && typeIndex != UnschematizedTypeIndex);
+ return typeIndex;
+ }
+
+ static TYqlConverterConfigPtr CreateYqlJsonConsumerConfig(const TWebJsonFormatConfigPtr& formatConfig)
+ {
+ auto config = New<TYqlConverterConfig>();
+ config->FieldWeightLimit = formatConfig->FieldWeightLimit;
+ config->StringWeightLimit = formatConfig->StringWeightLimit;
+ return config;
+ }
+};
+
+class TSchemalessValueWriter
+{
+public:
+ TSchemalessValueWriter(
+ const TWebJsonFormatConfigPtr& config,
+ const TNameTablePtr& nameTable,
+ const std::vector<TTableSchemaPtr>& schemas,
+ IJsonWriter* writer)
+ : FieldWeightLimit_(config->FieldWeightLimit)
+ , BlobYsonWriter_(&TmpBlob_, EYsonType::Node)
+ {
+ YT_VERIFY(config->ValueFormat == EWebJsonValueFormat::Schemaless);
+
+ auto jsonFormatConfig = New<TJsonFormatConfig>();
+ jsonFormatConfig->Stringify = true;
+ jsonFormatConfig->AnnotateWithTypes = true;
+ Consumer_ = CreateJsonConsumer(writer, EYsonType::Node, std::move(jsonFormatConfig));
+
+ for (int tableIndex = 0; tableIndex != std::ssize(schemas); ++tableIndex) {
+ for (const auto& column : schemas[tableIndex]->Columns()) {
+ if (!IsV3Composite(column.LogicalType())) {
+ continue;
+ }
+ auto columnId = nameTable->GetIdOrRegisterName(column.Name());
+ auto descriptor = TComplexTypeFieldDescriptor(column);
+ auto converter = CreateYsonServerToClientConverter(descriptor, { });
+ if (converter) {
+ YsonConverters_.emplace(std::pair{tableIndex, columnId}, std::move(converter));
+ }
+ }
+ }
+ }
+
+ void WriteValue(int tableIndex, TStringBuf /*columnName*/, TUnversionedValue value)
+ {
+ switch (value.Type) {
+ case EValueType::Any:
+ case EValueType::Composite: {
+ const auto data = value.AsStringBuf();
+ auto key = std::pair<int,int>(tableIndex, value.Id);
+ auto it = YsonConverters_.find(key);
+ if (it == YsonConverters_.end()) {
+ Consumer_->OnNodeWeightLimited(data, FieldWeightLimit_);
+ } else {
+ TmpBlob_.Clear();
+ it->second(value, &BlobYsonWriter_);
+ BlobYsonWriter_.Flush();
+
+ Consumer_->OnNodeWeightLimited(TStringBuf(TmpBlob_.Begin(), TmpBlob_.Size()), FieldWeightLimit_);
+ }
+ return;
+ }
+ case EValueType::String:
+ Consumer_->OnStringScalarWeightLimited(
+ value.AsStringBuf(),
+ FieldWeightLimit_);
+ return;
+ case EValueType::Int64:
+ Consumer_->OnInt64Scalar(value.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ Consumer_->OnUint64Scalar(value.Data.Uint64);
+ return;
+ case EValueType::Double:
+ Consumer_->OnDoubleScalar(value.Data.Double);
+ return;
+ case EValueType::Boolean:
+ Consumer_->OnBooleanScalar(value.Data.Boolean);
+ return;
+ case EValueType::Null:
+ Consumer_->OnEntity();
+ return;
+ case EValueType::TheBottom:
+ case EValueType::Min:
+ case EValueType::Max:
+ break;
+ }
+ ThrowUnexpectedValueType(value.Type);
+ }
+
+ void WriteMetaInfo()
+ { }
+
+private:
+ int FieldWeightLimit_;
+ std::unique_ptr<IJsonConsumer> Consumer_;
+
+ // Map <tableIndex,columnId> -> YsonConverter
+ THashMap<std::pair<int, int>, TYsonServerToClientConverter> YsonConverters_;
+ TBlobOutput TmpBlob_;
+ TBufferedBinaryYsonWriter BlobYsonWriter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TValueWriter>
+class TWriterForWebJson
+ : public ISchemalessFormatWriter
+{
+public:
+ TWriterForWebJson(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ TWebJsonColumnFilter columnFilter,
+ const std::vector<TTableSchemaPtr>& schemas,
+ TWebJsonFormatConfigPtr config);
+
+ bool Write(TRange<TUnversionedRow> rows) override;
+ TFuture<void> GetReadyEvent() override;
+ TBlob GetContext() const override;
+ i64 GetWrittenSize() const override;
+ TFuture<void> Close() override;
+ TFuture<void> Flush() override;
+
+private:
+ const TWebJsonFormatConfigPtr Config_;
+ const TNameTablePtr NameTable_;
+ const TNameTableReader NameTableReader_;
+
+ std::unique_ptr<IOutputStream> UnderlyingOutput_;
+ TCountingOutput Output_;
+
+ std::unique_ptr<IJsonWriter> ResponseBuilder_;
+
+ TWebJsonColumnFilter ColumnFilter_;
+ THashMap<ui16, TString> AllColumnIdToName_;
+
+ TValueWriter ValueWriter_;
+
+ TUtf8Transcoder Utf8Transcoder_;
+
+ bool IncompleteAllColumnNames_ = false;
+ bool IncompleteColumns_ = false;
+
+ TError Error_;
+ int TableIndexId_ = -1;
+
+private:
+ bool TryRegisterColumn(ui16 columnId, TStringBuf columnName);
+ bool SkipSystemColumn(TStringBuf columnName) const;
+
+ void DoFlush(bool force);
+ void DoWrite(TRange<TUnversionedRow> rows);
+ void DoClose();
+};
+
+template <typename TValueWriter>
+TWriterForWebJson<TValueWriter>::TWriterForWebJson(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ TWebJsonColumnFilter columnFilter,
+ const std::vector<TTableSchemaPtr>& schemas,
+ TWebJsonFormatConfigPtr config)
+ : Config_(std::move(config))
+ , NameTable_(std::move(nameTable))
+ , NameTableReader_(NameTable_)
+ , UnderlyingOutput_(CreateBufferedSyncAdapter(
+ std::move(output),
+ EWaitForStrategy::WaitFor,
+ ContextBufferCapacity))
+ , Output_(UnderlyingOutput_.get())
+ , ResponseBuilder_(CreateJsonWriter(&Output_))
+ , ColumnFilter_(std::move(columnFilter))
+ , ValueWriter_(Config_, NameTable_, schemas, ResponseBuilder_.get())
+{
+ ResponseBuilder_->OnBeginMap();
+ ResponseBuilder_->OnKeyedItem("rows");
+ ResponseBuilder_->OnBeginList();
+ TableIndexId_ = NameTable_->GetIdOrRegisterName(TableIndexColumnName);
+}
+
+template <typename TValueWriter>
+bool TWriterForWebJson<TValueWriter>::Write(TRange<TUnversionedRow> rows)
+{
+ if (!Error_.IsOK()) {
+ return false;
+ }
+
+ try {
+ DoWrite(rows);
+ } catch (const std::exception& ex) {
+ Error_ = TError(ex);
+ return false;
+ }
+
+ return true;
+}
+
+template <typename TValueWriter>
+TFuture<void> TWriterForWebJson<TValueWriter>::GetReadyEvent()
+{
+ return MakeFuture(Error_);
+}
+
+template <typename TValueWriter>
+TBlob TWriterForWebJson<TValueWriter>::GetContext() const
+{
+ return TBlob();
+}
+
+template <typename TValueWriter>
+i64 TWriterForWebJson<TValueWriter>::GetWrittenSize() const
+{
+ return static_cast<i64>(Output_.Counter());
+}
+
+template <typename TValueWriter>
+TFuture<void> TWriterForWebJson<TValueWriter>::Close()
+{
+ try {
+ DoClose();
+ } catch (const std::exception& exception) {
+ Error_ = TError(exception);
+ }
+
+ return GetReadyEvent();
+}
+
+template <typename TValueWriter>
+TFuture<void> TWriterForWebJson<TValueWriter>::Flush()
+{
+ DoFlush(/*force*/ true);
+
+ return GetReadyEvent();
+}
+
+template <typename TValueWriter>
+bool TWriterForWebJson<TValueWriter>::TryRegisterColumn(ui16 columnId, TStringBuf columnName)
+{
+ if (SkipSystemColumn(columnName)) {
+ return false;
+ }
+
+ if (std::ssize(AllColumnIdToName_) < Config_->MaxAllColumnNamesCount) {
+ AllColumnIdToName_[columnId] = columnName;
+ } else if (!AllColumnIdToName_.contains(columnId)) {
+ IncompleteAllColumnNames_ = true;
+ }
+
+ const auto result = ColumnFilter_.Accept(columnId, columnName);
+ if (!result) {
+ IncompleteColumns_ = true;
+ }
+
+ return result;
+}
+
+template <typename TValueWriter>
+bool TWriterForWebJson<TValueWriter>::SkipSystemColumn(TStringBuf columnName) const
+{
+ if (!columnName.StartsWith(SystemColumnNamePrefix)) {
+ return false;
+ }
+ return Config_->SkipSystemColumns;
+}
+
+template <typename TValueWriter>
+void TWriterForWebJson<TValueWriter>::DoFlush(bool force)
+{
+ ResponseBuilder_->Flush();
+ if (force) {
+ UnderlyingOutput_->Flush();
+ }
+}
+
+template <typename TValueWriter>
+void TWriterForWebJson<TValueWriter>::DoWrite(TRange<TUnversionedRow> rows)
+{
+ for (auto row : rows) {
+ if (!row) {
+ continue;
+ }
+
+ ResponseBuilder_->OnListItem();
+ ResponseBuilder_->OnBeginMap();
+
+ for (auto value : row) {
+ auto columnName = NameTableReader_.FindName(value.Id);
+ if (!columnName) {
+ continue;
+ }
+
+ if (!TryRegisterColumn(value.Id, columnName)) {
+ continue;
+ }
+
+ int tableIndex = 0;
+ for (auto val : row) {
+ if (val.Id == TableIndexId_) {
+ tableIndex = val.Data.Int64;
+ break;
+ }
+ }
+
+ if (IsSpecialJsonKey(columnName)) {
+ ResponseBuilder_->OnKeyedItem(Utf8Transcoder_.Encode(TString("$") + columnName));
+ } else {
+ ResponseBuilder_->OnKeyedItem(Utf8Transcoder_.Encode(columnName));
+ }
+ ValueWriter_.WriteValue(tableIndex, columnName, value);
+ }
+
+ ResponseBuilder_->OnEndMap();
+
+ DoFlush(false);
+ }
+
+ DoFlush(true);
+}
+
+template <typename TValueWriter>
+void TWriterForWebJson<TValueWriter>::DoClose()
+{
+ if (Error_.IsOK()) {
+ ResponseBuilder_->OnEndList();
+
+ ResponseBuilder_->OnKeyedItem("incomplete_columns");
+ // TODO(levysotsky): Maybe we don't need stringification here?
+ ResponseBuilder_->OnStringScalar(IncompleteColumns_ ? "true" : "false");
+
+ ResponseBuilder_->OnKeyedItem("incomplete_all_column_names");
+ // TODO(levysotsky): Maybe we don't need stringification here?
+ ResponseBuilder_->OnStringScalar(IncompleteAllColumnNames_ ? "true" : "false");
+
+ ResponseBuilder_->OnKeyedItem("all_column_names");
+ ResponseBuilder_->OnBeginList();
+
+ std::vector<TStringBuf> allColumnNamesSorted;
+ allColumnNamesSorted.reserve(AllColumnIdToName_.size());
+ for (const auto& columnIdToName : AllColumnIdToName_) {
+ allColumnNamesSorted.push_back(columnIdToName.second);
+ }
+ std::sort(allColumnNamesSorted.begin(), allColumnNamesSorted.end());
+
+ for (const auto columnName : allColumnNamesSorted) {
+ ResponseBuilder_->OnListItem();
+ ResponseBuilder_->OnStringScalar(Utf8Transcoder_.Encode(columnName));
+ }
+
+ ResponseBuilder_->OnEndList();
+
+ ValueWriter_.WriteMetaInfo();
+
+ ResponseBuilder_->OnEndMap();
+
+ DoFlush(true);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForWebJson(
+ TWebJsonFormatConfigPtr config,
+ TNameTablePtr nameTable,
+ const std::vector<TTableSchemaPtr>& schemas,
+ IAsyncOutputStreamPtr output)
+{
+ switch (config->ValueFormat) {
+ case EWebJsonValueFormat::Schemaless:
+ return New<TWriterForWebJson<TSchemalessValueWriter>>(
+ std::move(nameTable),
+ std::move(output),
+ CreateWebJsonColumnFilter(config),
+ schemas,
+ std::move(config));
+ case EWebJsonValueFormat::Yql:
+ return New<TWriterForWebJson<TYqlValueWriter>>(
+ std::move(nameTable),
+ std::move(output),
+ CreateWebJsonColumnFilter(config),
+ schemas,
+ std::move(config));
+
+ }
+ YT_ABORT();
+}
+
+ISchemalessFormatWriterPtr CreateWriterForWebJson(
+ const NYTree::IAttributeDictionary& attributes,
+ TNameTablePtr nameTable,
+ const std::vector<TTableSchemaPtr>& schemas,
+ IAsyncOutputStreamPtr output)
+{
+ try {
+ return CreateWriterForWebJson(
+ ConvertTo<TWebJsonFormatConfigPtr>(&attributes),
+ std::move(nameTable),
+ schemas,
+ std::move(output));
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for web JSON format") << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/web_json_writer.h b/yt/yt/client/formats/web_json_writer.h
new file mode 100644
index 0000000000..25d476a98f
--- /dev/null
+++ b/yt/yt/client/formats/web_json_writer.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "helpers.h"
+#include "schemaless_writer_adapter.h"
+
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/blob_output.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateWriterForWebJson(
+ TWebJsonFormatConfigPtr config,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output);
+
+ISchemalessFormatWriterPtr CreateWriterForWebJson(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ const std::vector<NTableClient::TTableSchemaPtr>& schemas,
+ NConcurrency::IAsyncOutputStreamPtr output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/ya.make b/yt/yt/client/formats/ya.make
new file mode 100644
index 0000000000..18eb0e8384
--- /dev/null
+++ b/yt/yt/client/formats/ya.make
@@ -0,0 +1,45 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ config.cpp
+ dsv_parser.cpp
+ dsv_writer.cpp
+ escape.cpp
+ format.cpp
+ helpers.cpp
+ parser.cpp
+ protobuf.cpp
+ protobuf_options.cpp
+ protobuf_parser.cpp
+ protobuf_writer.cpp
+ schemaful_dsv_parser.cpp
+ schemaful_dsv_writer.cpp
+ schemaful_writer.cpp
+ schemaless_writer_adapter.cpp
+ web_json_writer.cpp
+ skiff_parser.cpp
+ skiff_yson_converter.cpp
+ skiff_writer.cpp
+ unversioned_value_yson_writer.cpp
+ versioned_writer.cpp
+ yamr_parser.cpp
+ yamr_parser_base.cpp
+ yamr_writer.cpp
+ yamr_writer_base.cpp
+ yamred_dsv_parser.cpp
+ yamred_dsv_writer.cpp
+ yson_parser.cpp
+ yson_map_to_unversioned_value.cpp
+ yql_yson_converter.cpp
+)
+
+PEERDIR(
+ yt/yt/client
+ yt/yt/library/skiff_ext
+ yt/yt_proto/yt/formats
+ library/cpp/string_utils/base64
+)
+
+END()
diff --git a/yt/yt/client/formats/yamr_parser.cpp b/yt/yt/client/formats/yamr_parser.cpp
new file mode 100644
index 0000000000..0465741743
--- /dev/null
+++ b/yt/yt/client/formats/yamr_parser.cpp
@@ -0,0 +1,98 @@
+#include "yamr_parser.h"
+#include "yamr_parser_base.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TYamrParserConsumer
+ : public TYamrConsumerBase
+{
+public:
+ TYamrParserConsumer(NYson::IYsonConsumer* consumer, TYamrFormatConfigPtr config)
+ : TYamrConsumerBase(consumer)
+ , Config(config)
+ { }
+
+ void ConsumeKey(TStringBuf key) override
+ {
+ Consumer->OnListItem();
+ Consumer->OnBeginMap();
+ Consumer->OnKeyedItem(Config->Key);
+ Consumer->OnStringScalar(key);
+ }
+
+ void ConsumeSubkey(TStringBuf subkey) override
+ {
+ Consumer->OnKeyedItem(Config->Subkey);
+ Consumer->OnStringScalar(subkey);
+ }
+
+ void ConsumeValue(TStringBuf value) override
+ {
+ Consumer->OnKeyedItem(Config->Value);
+ Consumer->OnStringScalar(value);
+ Consumer->OnEndMap();
+ }
+
+private:
+ TYamrFormatConfigPtr Config;
+
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYamr(
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config)
+{
+ if (!config) {
+ config = New<TYamrFormatConfig>();
+ }
+
+ auto parserConsumer = New<TYamrParserConsumer>(consumer, config);
+
+ return config->Lenval
+ ? std::unique_ptr<IParser>(
+ new TYamrLenvalBaseParser(
+ parserConsumer,
+ config->HasSubkey,
+ config->EnableEom))
+ : std::unique_ptr<IParser>(
+ new TYamrDelimitedBaseParser(
+ parserConsumer,
+ config,
+ config->EnableEscaping /* enableKeyEscaping */,
+ config->EnableEscaping /* enableValueEscaping */));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYamr(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config)
+{
+ auto parser = CreateParserForYamr(consumer, config);
+ Parse(input, parser.get());
+}
+
+void ParseYamr(
+ TStringBuf data,
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config)
+{
+ auto parser = CreateParserForYamr(consumer, config);
+ parser->Read(data);
+ parser->Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_parser.h b/yt/yt/client/formats/yamr_parser.h
new file mode 100644
index 0000000000..10b5185d8e
--- /dev/null
+++ b/yt/yt/client/formats/yamr_parser.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYamr(
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config = New<TYamrFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYamr(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config = New<TYamrFormatConfig>());
+
+void ParseYamr(
+ TStringBuf data,
+ NYson::IYsonConsumer* consumer,
+ TYamrFormatConfigPtr config = New<TYamrFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_parser_base.cpp b/yt/yt/client/formats/yamr_parser_base.cpp
new file mode 100644
index 0000000000..3a7a7d8833
--- /dev/null
+++ b/yt/yt/client/formats/yamr_parser_base.cpp
@@ -0,0 +1,453 @@
+#include "yamr_parser_base.h"
+
+#include "format.h"
+#include "config.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <util/string/escape.h>
+
+namespace NYT::NFormats {
+
+using namespace NYTree;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYamrConsumerBase::TYamrConsumerBase(NYson::IYsonConsumer* consumer)
+ : Consumer(consumer)
+{ }
+
+void TYamrConsumerBase::SwitchTable(i64 tableIndex)
+{
+ static const TString Key = FormatEnum(EControlAttribute::TableIndex);
+ Consumer->OnListItem();
+ Consumer->OnBeginAttributes();
+ Consumer->OnKeyedItem(Key);
+ Consumer->OnInt64Scalar(tableIndex);
+ Consumer->OnEndAttributes();
+ Consumer->OnEntity();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYamrDelimitedBaseParser::TYamrDelimitedBaseParser(
+ IYamrConsumerPtr consumer,
+ const TYamrFormatConfigBasePtr& config,
+ bool enableKeyEscaping,
+ bool enableValueEscaping)
+ : Consumer(consumer)
+ , Config_(config)
+ , State(EState::InsideKey)
+ , ExpectingEscapedChar(false)
+ , Offset(0)
+ , Record(1)
+ , BufferPosition(0)
+{
+ ConfigureEscapeTables(
+ config,
+ enableKeyEscaping,
+ enableValueEscaping,
+ false /* escapingForWriter */,
+ &KeyEscapeTable_,
+ &ValueEscapeTable_);
+}
+
+void TYamrDelimitedBaseParser::Read(TStringBuf data)
+{
+ auto current = data.begin();
+ auto end = data.end();
+ while (current != end) {
+ current = Consume(current, end);
+ }
+}
+
+void TYamrDelimitedBaseParser::Finish()
+{
+ if (ExpectingEscapedChar) {
+ ThrowIncorrectFormat();
+ }
+ if (State == EState::InsideKey && !CurrentToken.empty()) {
+ ThrowIncorrectFormat();
+ }
+ if (State == EState::InsideSubkey) {
+ ProcessSubkey(CurrentToken);
+ ProcessValue("");
+ }
+ if (State == EState::InsideValue) {
+ ProcessValue(CurrentToken);
+ }
+}
+
+TString TYamrDelimitedBaseParser::GetContext() const
+{
+ TString result;
+ const char* last = ContextBuffer + BufferPosition;
+ if (Offset >= ContextBufferSize) {
+ result.append(last, ContextBuffer + ContextBufferSize);
+ }
+ result.append(ContextBuffer, last);
+ return result;
+}
+
+IAttributeDictionaryPtr TYamrDelimitedBaseParser::GetDebugInfo() const
+{
+ auto result = CreateEphemeralAttributes();
+ result->Set("context", GetContext());
+ result->Set("offset", Offset);
+ result->Set("record", Record);
+ result->Set("state", State);
+ return result;
+}
+
+void TYamrDelimitedBaseParser::ProcessTableSwitch(TStringBuf tableIndex)
+{
+ YT_ASSERT(!ExpectingEscapedChar);
+ YT_ASSERT(State == EState::InsideKey);
+ i64 value;
+ try {
+ value = FromString<i64>(tableIndex);
+ } catch (const std::exception& ex) {
+ TString tableIndexString(tableIndex);
+ if (tableIndex.size() > ContextBufferSize) {
+ tableIndexString = TString(tableIndex.SubStr(0, ContextBufferSize)) + "...truncated...";
+ }
+ THROW_ERROR_EXCEPTION("YAMR line %Qv cannot be parsed as a table switch; did you forget a record separator?",
+ tableIndexString)
+ << *GetDebugInfo();
+ }
+ Consumer->SwitchTable(value);
+}
+
+void TYamrDelimitedBaseParser::ProcessKey(TStringBuf key)
+{
+ YT_ASSERT(!ExpectingEscapedChar);
+ YT_ASSERT(State == EState::InsideKey);
+ Consumer->ConsumeKey(key);
+ State = Config_->HasSubkey ? EState::InsideSubkey : EState::InsideValue;
+}
+
+void TYamrDelimitedBaseParser::ProcessSubkey(TStringBuf subkey)
+{
+ YT_ASSERT(!ExpectingEscapedChar);
+ YT_ASSERT(State == EState::InsideSubkey);
+ Consumer->ConsumeSubkey(subkey);
+ State = EState::InsideValue;
+}
+
+void TYamrDelimitedBaseParser::ProcessSubkeyBadFormat(TStringBuf subkey)
+{
+ YT_ASSERT(!ExpectingEscapedChar);
+ YT_ASSERT(State == EState::InsideSubkey);
+ Consumer->ConsumeSubkey(subkey);
+ Consumer->ConsumeValue("");
+ State = EState::InsideKey;
+}
+
+void TYamrDelimitedBaseParser::ProcessValue(TStringBuf value)
+{
+ YT_ASSERT(!ExpectingEscapedChar);
+ YT_ASSERT(State == EState::InsideValue);
+ Consumer->ConsumeValue(value);
+ State = EState::InsideKey;
+ Record += 1;
+}
+
+const char* TYamrDelimitedBaseParser::ProcessToken(
+ void (TYamrDelimitedBaseParser::*processor)(TStringBuf value),
+ const char* begin,
+ const char* next)
+{
+ if (CurrentToken.empty()) {
+ (this->*processor)(TStringBuf(begin, next));
+ } else {
+ CurrentToken.append(begin, next);
+ (this->*processor)(CurrentToken);
+ CurrentToken.clear();
+ }
+
+ OnRangeConsumed(next, next + 1);
+ return next + 1;
+}
+
+const char* TYamrDelimitedBaseParser::FindNext(const char* begin, const char* end, const TEscapeTable& escapeTable)
+{
+ const char* next = escapeTable.FindNext(begin, end);
+ OnRangeConsumed(begin, next);
+ return next;
+}
+
+const char* TYamrDelimitedBaseParser::Consume(const char* begin, const char* end)
+{
+ if (ExpectingEscapedChar) {
+ // Read and unescape.
+ CurrentToken.append(EscapeBackward[static_cast<ui8>(*begin)]);
+ ExpectingEscapedChar = false;
+ OnRangeConsumed(begin, begin + 1);
+ return begin + 1;
+ }
+
+ YT_ASSERT(!ExpectingEscapedChar);
+
+ const char* next = FindNext(begin, end, State == EState::InsideValue ? ValueEscapeTable_ : KeyEscapeTable_);
+ if (next == end) {
+ CurrentToken.append(begin, next);
+ if (CurrentToken.length() > MaxRowWeightLimit) {
+ THROW_ERROR_EXCEPTION(
+ "YAMR token length limit exceeded: %v > %v",
+ CurrentToken.length(),
+ MaxRowWeightLimit)
+ << *GetDebugInfo();
+ }
+ return end;
+ }
+
+ if (*next == Config_->EscapingSymbol) {
+ CurrentToken.append(begin, next);
+ OnRangeConsumed(next, next + 1);
+ ExpectingEscapedChar = true;
+ return next + 1;
+ }
+
+ switch (State) {
+ case EState::InsideKey:
+ if (*next == Config_->RecordSeparator) {
+ return ProcessToken(&TYamrDelimitedBaseParser::ProcessTableSwitch, begin, next);
+ }
+
+ if (*next == Config_->FieldSeparator) {
+ return ProcessToken(&TYamrDelimitedBaseParser::ProcessKey, begin, next);
+ }
+ break;
+
+ case EState::InsideSubkey:
+ if (*next == Config_->FieldSeparator) {
+ return ProcessToken(&TYamrDelimitedBaseParser::ProcessSubkey, begin, next);
+ }
+
+ if (*next == Config_->RecordSeparator) {
+ // See yamr_parser_ut.cpp: IncompleteRows() for details.
+ return ProcessToken(&TYamrDelimitedBaseParser::ProcessSubkeyBadFormat, begin, next);
+ }
+ break;
+
+ case EState::InsideValue:
+ if (*next == Config_->RecordSeparator) {
+ return ProcessToken(&TYamrDelimitedBaseParser::ProcessValue, begin, next);
+ }
+ break;
+ }
+
+ ThrowIncorrectFormat();
+
+ // To suppress warnings.
+ YT_ABORT();
+}
+
+void TYamrDelimitedBaseParser::ThrowIncorrectFormat() const
+{
+ THROW_ERROR_EXCEPTION("Unexpected symbol in YAMR row: expected %Qv, found %Qv",
+ EscapeC(Config_->FieldSeparator),
+ EscapeC(Config_->RecordSeparator))
+ << *GetDebugInfo();
+}
+
+void TYamrDelimitedBaseParser::OnRangeConsumed(const char* begin, const char* end)
+{
+ Offset += end - begin;
+ auto current = std::max(begin, end - ContextBufferSize);
+ for ( ; current < end; ++current) {
+ AppendToContextBuffer(*current);
+ }
+}
+
+void TYamrDelimitedBaseParser::AppendToContextBuffer(char symbol)
+{
+ ContextBuffer[BufferPosition] = symbol;
+ ++BufferPosition;
+ if (BufferPosition >= ContextBufferSize) {
+ BufferPosition -= ContextBufferSize;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYamrLenvalBaseParser::TYamrLenvalBaseParser(
+ IYamrConsumerPtr consumer,
+ bool enableSubkey,
+ bool enableEom)
+ : Consumer(consumer)
+ , EnableSubkey(enableSubkey)
+ , EnableEom(enableEom)
+{ }
+
+void TYamrLenvalBaseParser::Read(TStringBuf data)
+{
+ auto current = data.begin();
+ while (current != data.end()) {
+ current = Consume(current, data.end());
+ }
+}
+
+void TYamrLenvalBaseParser::Finish()
+{
+ if (EnableEom && !MetEom) {
+ THROW_ERROR_EXCEPTION("Missing EOM marker in the stream");
+ }
+
+ if (State == EState::InsideValue && !ReadingLength && BytesToRead == 0) {
+ Consumer->ConsumeValue(CurrentToken);
+ return;
+ }
+
+ if (!(State == EState::InsideKey && ReadingLength && BytesToRead == 4)) {
+ THROW_ERROR_EXCEPTION("Premature end of stream");
+ }
+}
+
+const char* TYamrLenvalBaseParser::Consume(const char* begin, const char* end)
+{
+ if (MetEom) {
+ THROW_ERROR_EXCEPTION("Garbage after EOM marker");
+ }
+ if (ReadingLength) {
+ return ConsumeLength(begin, end);
+ } else {
+ return ConsumeData(begin, end);
+ }
+}
+
+const char* TYamrLenvalBaseParser::ConsumeInt(const char* begin, const char* end, int length)
+{
+ const char* current = begin;
+ while (BytesToRead != 0 && current != end) {
+ Union.Bytes[length - BytesToRead] = *current;
+ ++current;
+ --BytesToRead;
+ }
+ return current;
+}
+
+const char* TYamrLenvalBaseParser::ConsumeLength(const char* begin, const char* end)
+{
+ YT_ASSERT(ReadingLength);
+ const char* next = ConsumeInt(begin, end, 4);
+
+ if (BytesToRead == 0) {
+ ReadingLength = false;
+ BytesToRead = static_cast<ui32>(Union.Value);
+ }
+
+ if (BytesToRead == static_cast<ui32>(-1)) {
+ if (State == EState::InsideKey) {
+ BytesToRead = 4;
+ State = EState::InsideTableSwitch;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected table switch instruction");
+ }
+ } else if (BytesToRead == static_cast<ui32>(-5)) {
+ if (!EnableEom) {
+ THROW_ERROR_EXCEPTION("Unexpected EOM marker");
+ }
+ if (State == EState::InsideKey) {
+ BytesToRead = 8;
+ State = EState::InsideEom;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected EOM marker");
+ }
+ }
+
+ if (BytesToRead > MaxRowWeightLimit) {
+ THROW_ERROR_EXCEPTION(
+ "YAMR lenval length limit exceeded: %v > %v",
+ BytesToRead,
+ MaxRowWeightLimit);
+ }
+
+ return next;
+}
+
+const char* TYamrLenvalBaseParser::ConsumeData(const char* begin, const char* end)
+{
+ if (State == EState::InsideTableSwitch) {
+ YT_ASSERT(CurrentToken.empty());
+ const char* next = ConsumeInt(begin, end, 4);
+
+ if (BytesToRead == 0) {
+ Consumer->SwitchTable(static_cast<ui32>(Union.Value));
+ State = EState::InsideKey;
+ ReadingLength = true;
+ BytesToRead = 4;
+ }
+
+ return next;
+ } else if (State == EState::InsideEom) {
+ const char* next = ConsumeInt(begin, end, 8);
+
+ if (BytesToRead == 0) {
+ MetEom = true;
+ if (Union.Value != RowCount) {
+ THROW_ERROR_EXCEPTION("Row count mismatch")
+ << TErrorAttribute("eom_marker_row_count", Union.Value)
+ << TErrorAttribute("actual_row_count", RowCount);
+ }
+ State = EState::InsideKey;
+ ReadingLength = true;
+ BytesToRead = 4;
+ }
+
+ return next;
+ }
+
+ // Consume ordinary string tokens.
+ TStringBuf data;
+ const char* current = begin + BytesToRead;
+
+ if (current > end) {
+ CurrentToken.append(begin, end);
+ BytesToRead -= (end - begin);
+ YT_ASSERT(BytesToRead > 0);
+ return end;
+ }
+
+ if (CurrentToken.empty()) {
+ data = TStringBuf(begin, current);
+ } else {
+ CurrentToken.append(begin, current);
+ data = CurrentToken;
+ }
+
+ switch (State) {
+ case EState::InsideKey:
+ ++RowCount;
+ Consumer->ConsumeKey(data);
+ State = EnableSubkey ? EState::InsideSubkey : EState::InsideValue;
+ break;
+ case EState::InsideSubkey:
+ Consumer->ConsumeSubkey(data);
+ State = EState::InsideValue;
+ break;
+ case EState::InsideValue:
+ Consumer->ConsumeValue(data);
+ State = EState::InsideKey;
+ break;
+ default:
+ YT_ABORT();
+ }
+
+ CurrentToken.clear();
+ ReadingLength = true;
+ BytesToRead = 4;
+
+ return current;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_parser_base.h b/yt/yt/client/formats/yamr_parser_base.h
new file mode 100644
index 0000000000..240a2855a8
--- /dev/null
+++ b/yt/yt/client/formats/yamr_parser_base.h
@@ -0,0 +1,160 @@
+#pragma once
+
+#include "parser.h"
+
+#include "escape.h"
+
+#include <yt/yt/core/ytree/attributes.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IYamrConsumer
+ : public virtual TRefCounted
+{
+ virtual void ConsumeKey(TStringBuf key) = 0;
+ virtual void ConsumeSubkey(TStringBuf subkey) = 0;
+ virtual void ConsumeValue(TStringBuf value) = 0;
+ virtual void SwitchTable(i64 tableIndex) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IYamrConsumer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYamrConsumerBase
+ : public IYamrConsumer
+{
+public:
+ explicit TYamrConsumerBase(NYson::IYsonConsumer* consumer);
+ virtual void SwitchTable(i64 tableIndex) override;
+
+protected:
+ NYson::IYsonConsumer* Consumer;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EYamrDelimitedBaseParserState,
+ (InsideKey)
+ (InsideSubkey)
+ (InsideValue)
+);
+
+class TYamrDelimitedBaseParser
+ : public IParser
+{
+public:
+ TYamrDelimitedBaseParser(
+ IYamrConsumerPtr consumer,
+ const TYamrFormatConfigBasePtr& config,
+ bool enableKeyEscaping,
+ bool enableValueEscaping);
+
+ virtual void Read(TStringBuf data) override;
+ virtual void Finish() override;
+
+private:
+ using EState = EYamrDelimitedBaseParserState;
+
+ const char* Consume(const char* begin, const char* end);
+
+ void ProcessKey(TStringBuf key);
+ void ProcessSubkey(TStringBuf subkey);
+ void ProcessSubkeyBadFormat(TStringBuf subkey);
+ void ProcessValue(TStringBuf value);
+ void ProcessTableSwitch(TStringBuf tableIndex);
+
+ const char* ProcessToken(
+ void (TYamrDelimitedBaseParser::*processor)(TStringBuf value),
+ const char* begin,
+ const char* next);
+
+ const char* FindNext(const char* begin, const char* end, const TEscapeTable& escapeTable);
+
+ void ThrowIncorrectFormat() const;
+
+ void OnRangeConsumed(const char* begin, const char* end);
+ void AppendToContextBuffer(char symbol);
+
+ TString GetContext() const;
+ NYTree::IAttributeDictionaryPtr GetDebugInfo() const;
+
+ IYamrConsumerPtr Consumer;
+
+ TYamrFormatConfigBasePtr Config_;
+
+ EState State;
+
+ bool ExpectingEscapedChar;
+
+ TString CurrentToken;
+
+ // Diagnostic Info
+ i64 Offset;
+ i64 Record;
+ i32 BufferPosition;
+
+ static const int ContextBufferSize = 64;
+ char ContextBuffer[ContextBufferSize];
+
+ TEscapeTable KeyEscapeTable_;
+ TEscapeTable ValueEscapeTable_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EYamrLenvalBaseParserState,
+ (InsideTableSwitch)
+ (InsideKey)
+ (InsideSubkey)
+ (InsideValue)
+ (InsideEom)
+);
+
+class TYamrLenvalBaseParser
+ : public IParser
+{
+public:
+ TYamrLenvalBaseParser(
+ IYamrConsumerPtr consumer,
+ bool enableSubkey,
+ bool enableEom);
+
+ virtual void Read(TStringBuf data) override;
+ virtual void Finish() override;
+
+private:
+ using EState = EYamrLenvalBaseParserState;
+
+ const char* Consume(const char* begin, const char* end);
+ const char* ConsumeInt(const char* begin, const char* end, int length);
+ const char* ConsumeLength(const char* begin, const char* end);
+ const char* ConsumeData(const char* begin, const char* end);
+
+ IYamrConsumerPtr Consumer;
+
+ bool EnableSubkey;
+
+ TString CurrentToken;
+
+ union {
+ ui64 Value;
+ char Bytes[8];
+ } Union;
+
+ bool ReadingLength = true;
+ ui32 BytesToRead = 4;
+
+ EState State = EState::InsideKey;
+
+ bool EnableEom;
+ bool MetEom = false;
+ ui64 RowCount = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_writer.cpp b/yt/yt/client/formats/yamr_writer.cpp
new file mode 100644
index 0000000000..4408b35568
--- /dev/null
+++ b/yt/yt/client/formats/yamr_writer.cpp
@@ -0,0 +1,211 @@
+#include "yamr_writer.h"
+
+#include "escape.h"
+#include "helpers.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/format.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForYamr
+ : public TSchemalessWriterForYamrBase
+{
+public:
+ TSchemalessWriterForYamr(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount,
+ TYamrFormatConfigPtr config = New<TYamrFormatConfig>())
+ : TSchemalessWriterForYamrBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount,
+ config)
+ {
+ ConfigureEscapeTables(
+ config,
+ config->EnableEscaping /* enableKeyEscaping */,
+ config->EnableEscaping /* enableValueEscaping */,
+ true /* escapingForWriter */,
+ &KeyEscapeTable_,
+ &ValueEscapeTable_);
+
+ try {
+ KeyId_ = nameTable->GetIdOrRegisterName(config->Key);
+ SubkeyId_ = Config_->HasSubkey ? nameTable->GetIdOrRegisterName(config->Subkey) : -1;
+ ValueId_ = nameTable->GetIdOrRegisterName(config->Value);
+ } catch (const std::exception& ex) {
+ auto error = TError("Failed to add columns to name table for YAMR format")
+ << ex;
+ RegisterError(error);
+ }
+ }
+
+private:
+ TEscapeTable KeyEscapeTable_;
+ TEscapeTable ValueEscapeTable_;
+
+ int KeyId_;
+ int SubkeyId_;
+ int ValueId_;
+
+ // ISchemalessFormatWriter override.
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ TableIndexWasWritten_ = false;
+
+ auto* stream = GetOutputStream();
+ // This nasty line is needed to use Config as TYamrFormatConfigPtr
+ // without extra serializing/deserializing.
+ TYamrFormatConfigPtr config(static_cast<TYamrFormatConfig*>(Config_.Get()));
+
+ int rowCount = static_cast<int>(rows.Size());
+ for (int index = 0; index < rowCount; index++) {
+ auto row = rows[index];
+ if (CheckKeySwitch(row, index + 1 == rowCount /* isLastRow */)) {
+ YT_VERIFY(config->Lenval);
+ WritePod(*stream, static_cast<ui32>(-2));
+ }
+
+ WriteControlAttributes(row);
+
+ std::optional<TStringBuf> key;
+ std::optional<TStringBuf> subkey;
+ std::optional<TStringBuf> value;
+
+ for (const auto* item = row.Begin(); item != row.End(); ++item) {
+ if (item->Id == KeyId_) {
+ ValidateColumnType(item, TStringBuf("key"));
+ key = item->AsStringBuf();
+ } else if (item->Id == SubkeyId_) {
+ if (item->Type != EValueType::Null) {
+ ValidateColumnType(item, TStringBuf("subkey"));
+ subkey = item->AsStringBuf();
+ }
+ } else if (item->Id == ValueId_) {
+ ValidateColumnType(item, TStringBuf("value"));
+ value = item->AsStringBuf();
+ } else {
+ // Ignore unknown columns.
+ continue;
+ }
+ }
+
+ if (!key) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Missing key column %Qv in YAMR record",
+ config->Key);
+ }
+
+ if (!subkey) {
+ subkey = "";
+ }
+
+ if (!value) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Missing value column %Qv in YAMR record",
+ config->Value);
+ }
+
+ if (!config->Lenval) {
+ EscapeAndWrite(*key, stream, KeyEscapeTable_);
+ stream->Write(config->FieldSeparator);
+ if (config->HasSubkey) {
+ EscapeAndWrite(*subkey, stream, KeyEscapeTable_);
+ stream->Write(config->FieldSeparator);
+ }
+ EscapeAndWrite(*value, stream, ValueEscapeTable_);
+ stream->Write(config->RecordSeparator);
+ } else {
+ WriteInLenvalMode(*key);
+ if (config->HasSubkey) {
+ WriteInLenvalMode(*subkey);
+ }
+ WriteInLenvalMode(*value);
+ }
+
+ TryFlushBuffer(false);
+ }
+
+ TryFlushBuffer(true);
+ }
+
+ void ValidateColumnType(const TUnversionedValue* value, TStringBuf columnName)
+ {
+ if (value->Type != EValueType::String) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Wrong type %Qlv of column %Qv in YAMR record",
+ value->Type,
+ columnName);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamr(
+ TYamrFormatConfigPtr config,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ if (controlAttributesConfig->EnableKeySwitch && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("Key switches are not supported in text YAMR format");
+ }
+
+ if (controlAttributesConfig->EnableRangeIndex && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("Range indices are not supported in text YAMR format");
+ }
+
+ if (controlAttributesConfig->EnableEndOfStream) {
+ THROW_ERROR_EXCEPTION("End of stream control attribute is not supported in YAMR format");
+ }
+
+ return New<TSchemalessWriterForYamr>(
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount,
+ config);
+}
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamr(
+ const IAttributeDictionary& attributes,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = ConvertTo<TYamrFormatConfigPtr>(&attributes);
+ return CreateSchemalessWriterForYamr(
+ config,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ } catch (const std::exception& exc) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for YAMR format") << exc;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_writer.h b/yt/yt/client/formats/yamr_writer.h
new file mode 100644
index 0000000000..d381b1d4bd
--- /dev/null
+++ b/yt/yt/client/formats/yamr_writer.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "helpers.h"
+#include "yamr_writer_base.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamr(
+ TYamrFormatConfigPtr config,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamr(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_writer_base.cpp b/yt/yt/client/formats/yamr_writer_base.cpp
new file mode 100644
index 0000000000..1dc73de0d8
--- /dev/null
+++ b/yt/yt/client/formats/yamr_writer_base.cpp
@@ -0,0 +1,90 @@
+#include "yamr_writer_base.h"
+#include "yamr_writer.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/format.h>
+
+namespace NYT::NFormats {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemalessWriterForYamrBase::TSchemalessWriterForYamrBase(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount,
+ TYamrFormatConfigBasePtr config)
+ : TSchemalessFormatWriterBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount)
+ , Config_(config)
+{ }
+
+void TSchemalessWriterForYamrBase::WriteInLenvalMode(TStringBuf value)
+{
+ auto* stream = GetOutputStream();
+ WritePod(*stream, static_cast<ui32>(value.size()));
+ stream->Write(value);
+}
+
+void TSchemalessWriterForYamrBase::WriteTableIndex(i64 tableIndex)
+{
+ TableIndexWasWritten_ = true;
+ CurrentTableIndex_ = tableIndex;
+
+ auto* stream = GetOutputStream();
+
+ if (!Config_->EnableTableIndex) {
+ // Silently ignore table switches.
+ return;
+ }
+
+ if (Config_->Lenval) {
+ WritePod(*stream, static_cast<ui32>(-1));
+ WritePod(*stream, static_cast<ui32>(tableIndex));
+ } else {
+ stream->Write(ToString(tableIndex));
+ stream->Write(Config_->RecordSeparator);
+ }
+}
+
+void TSchemalessWriterForYamrBase::WriteRangeIndex(i64 rangeIndex)
+{
+ YT_VERIFY(Config_->Lenval);
+
+ auto* stream = GetOutputStream();
+ WritePod(*stream, static_cast<ui32>(-3));
+ WritePod(*stream, static_cast<ui32>(rangeIndex));
+}
+
+void TSchemalessWriterForYamrBase::WriteRowIndex(i64 rowIndex)
+{
+ auto* stream = GetOutputStream();
+ if (Config_->Lenval) {
+ WritePod(*stream, static_cast<ui32>(-4));
+ WritePod(*stream, static_cast<ui64>(rowIndex));
+ } else {
+ if (!TableIndexWasWritten_) {
+ stream->Write(ToString(CurrentTableIndex_));
+ stream->Write(Config_->RecordSeparator);
+ }
+ stream->Write(ToString(rowIndex));
+ stream->Write(Config_->RecordSeparator);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamr_writer_base.h b/yt/yt/client/formats/yamr_writer_base.h
new file mode 100644
index 0000000000..d8483f6636
--- /dev/null
+++ b/yt/yt/client/formats/yamr_writer_base.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "helpers.h"
+#include "escape.h"
+#include "schemaless_writer_adapter.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForYamrBase
+ : public TSchemalessFormatWriterBase
+{
+public:
+ TSchemalessWriterForYamrBase(
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount,
+ TYamrFormatConfigBasePtr config);
+
+protected:
+ TYamrFormatConfigBasePtr Config_;
+ bool TableIndexWasWritten_ = false;
+ int CurrentTableIndex_ = 0;
+
+ void WriteInLenvalMode(TStringBuf value);
+
+ void WriteTableIndex(i64 tableIndex) override;
+ void WriteRangeIndex(i64 rangeIndex) override;
+ void WriteRowIndex(i64 rowIndex) override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSchemalessWriterForYamrBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yamred_dsv_parser.cpp b/yt/yt/client/formats/yamred_dsv_parser.cpp
new file mode 100644
index 0000000000..476e760ea6
--- /dev/null
+++ b/yt/yt/client/formats/yamred_dsv_parser.cpp
@@ -0,0 +1,130 @@
+#include "yamred_dsv_parser.h"
+#include "dsv_parser.h"
+#include "yamr_parser_base.h"
+
+namespace NYT::NFormats {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TYamredDsvParserConsumer
+ : public TYamrConsumerBase
+{
+public:
+ TYamredDsvParserConsumer(IYsonConsumer* consumer, TYamredDsvFormatConfigPtr config)
+ : TYamrConsumerBase(consumer)
+ , Config(config)
+ , DsvParser(CreateParserForDsv(consumer, ConvertTo<TDsvFormatConfigPtr>(Config), /*wrapWithMap*/ false))
+ { }
+
+ void ConsumeKey(TStringBuf key) override
+ {
+ Consumer->OnListItem();
+ Consumer->OnBeginMap();
+ ConsumeFields(Config->KeyColumnNames, key);
+ }
+
+ void ConsumeSubkey(TStringBuf subkey) override
+ {
+ ConsumeFields(Config->SubkeyColumnNames, subkey);
+ }
+
+ void ConsumeValue(TStringBuf value) override
+ {
+ DsvParser->Read(value);
+ DsvParser->Finish();
+ Consumer->OnEndMap();
+ }
+
+private:
+ TYamredDsvFormatConfigPtr Config;
+ std::unique_ptr<IParser> DsvParser;
+
+ void ConsumeFields(
+ const std::vector<TString>& fieldNames,
+ TStringBuf wholeField)
+ {
+ static const char* emptyString = "";
+ char delimiter = Config->YamrKeysSeparator;
+
+ std::vector<TStringBuf> fields;
+ if (wholeField.length() == 0) {
+ fields = std::vector<TStringBuf>(1, TStringBuf(emptyString));
+ } else {
+ size_t position = 0;
+ while (position != TStringBuf::npos) {
+ size_t newPosition = (fields.size() + 1 == fieldNames.size())
+ ? TStringBuf::npos
+ : wholeField.find(delimiter, position);
+ fields.push_back(wholeField.substr(position, newPosition));
+ position = (newPosition == TStringBuf::npos) ? TStringBuf::npos : newPosition + 1;
+ }
+ }
+
+ if (fields.size() != fieldNames.size()) {
+ THROW_ERROR_EXCEPTION("Invalid number of key fields in YAMRed DSV: expected %v, actual %v",
+ fieldNames.size(),
+ fields.size());
+ }
+
+ for (int i = 0; i < static_cast<int>(fields.size()); ++i) {
+ Consumer->OnKeyedItem(fieldNames[i]);
+ Consumer->OnStringScalar(fields[i]);
+ }
+ }
+
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYamredDsv(
+ IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config)
+{
+ auto parserConsumer = New<TYamredDsvParserConsumer>(consumer, config);
+
+ return config->Lenval
+ ? std::unique_ptr<IParser>(
+ new TYamrLenvalBaseParser(
+ parserConsumer,
+ config->HasSubkey,
+ config->EnableEom))
+ : std::unique_ptr<IParser>(
+ new TYamrDelimitedBaseParser(
+ parserConsumer,
+ config,
+ config->EnableEscaping /* enableKeyEscaping */,
+ false /* enableValueEscaping */));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYamredDsv(
+ IInputStream* input,
+ IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForYamredDsv(consumer, config);
+ Parse(input, parser.get());
+}
+
+void ParseYamredDsv(
+ TStringBuf data,
+ IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config)
+{
+ auto parser = CreateParserForYamredDsv(consumer, config);
+ parser->Read(data);
+ parser->Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/yamred_dsv_parser.h b/yt/yt/client/formats/yamred_dsv_parser.h
new file mode 100644
index 0000000000..e260ee6b30
--- /dev/null
+++ b/yt/yt/client/formats/yamred_dsv_parser.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYamredDsv(
+ NYson::IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config = nullptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYamredDsv(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config = nullptr);
+
+void ParseYamredDsv(
+ TStringBuf data,
+ NYson::IYsonConsumer* consumer,
+ TYamredDsvFormatConfigPtr config = nullptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/yamred_dsv_writer.cpp b/yt/yt/client/formats/yamred_dsv_writer.cpp
new file mode 100644
index 0000000000..956e771732
--- /dev/null
+++ b/yt/yt/client/formats/yamred_dsv_writer.cpp
@@ -0,0 +1,251 @@
+#include "yamred_dsv_writer.h"
+
+#include "escape.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+
+namespace NYT::NFormats {
+
+using namespace NYTree;
+using namespace NTableClient;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessWriterForYamredDsv
+ : public TSchemalessWriterForYamrBase
+{
+public:
+ TSchemalessWriterForYamredDsv(
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount,
+ TYamredDsvFormatConfigPtr config = New<TYamredDsvFormatConfig>())
+ : TSchemalessWriterForYamrBase(
+ nameTable,
+ std::move(output),
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount,
+ config)
+ , Config_(config)
+ {
+ ConfigureEscapeTables(config, true /* addCarriageReturn */, &KeyEscapeTable_, &ValueEscapeTable_);
+ try {
+ // We register column names in order to have correct size of NameTable_ in DoWrite method.
+ for (const auto& columnName : config->KeyColumnNames) {
+ KeyColumnIds_.push_back(nameTable->GetIdOrRegisterName(columnName));
+ }
+
+ for (const auto& columnName : config->SubkeyColumnNames) {
+ SubkeyColumnIds_.push_back(nameTable->GetIdOrRegisterName(columnName));
+ }
+ } catch (const std::exception& ex) {
+ auto error = TError("Failed to add columns to name table for YAMRed DSV format")
+ << ex;
+ RegisterError(error);
+ }
+
+ UpdateEscapedColumnNames();
+ }
+
+private:
+ const TYamredDsvFormatConfigPtr Config_;
+
+ std::vector<const TUnversionedValue*> RowValues_;
+
+ std::vector<int> KeyColumnIds_;
+ std::vector<int> SubkeyColumnIds_;
+ std::vector<TString> EscapedColumnNames_;
+
+ // In lenval mode key, subkey and value are first written into
+ // this buffer in order to calculate their length.
+ TBlobOutput LenvalBuffer_;
+
+ // We capture size of name table at the beginning of #WriteRows
+ // and refer to the captured value, since name table may change asynchronously.
+ int NameTableSize_ = 0;
+
+ TEscapeTable KeyEscapeTable_;
+ TEscapeTable ValueEscapeTable_;
+
+
+ // ISchemalessFormatWriter implementation
+ void DoWrite(TRange<TUnversionedRow> rows) override
+ {
+ TableIndexWasWritten_ = false;
+
+ auto* stream = GetOutputStream();
+
+ UpdateEscapedColumnNames();
+ RowValues_.resize(NameTableSize_);
+ // Invariant: at the beginning of each loop iteration RowValues contains
+ // nullptr in each element.
+ int rowCount = static_cast<int>(rows.Size());
+ for (int index = 0; index < rowCount; index++) {
+ auto row = rows[index];
+ if (CheckKeySwitch(row, index + 1 == rowCount /* isLastRow */)) {
+ YT_VERIFY(!Config_->Lenval);
+ WritePod(*stream, static_cast<ui32>(-2));
+ }
+
+ WriteControlAttributes(row);
+
+ for (const auto* item = row.Begin(); item != row.End(); ++item) {
+ if (IsSystemColumnId(item->Id) || item->Type == EValueType::Null) {
+ // Ignore null values and system columns.
+ continue;
+ }
+ YT_VERIFY(item->Id < NameTableSize_);
+ RowValues_[item->Id] = item;
+ }
+
+ WriteYamrKey(KeyColumnIds_);
+ if (Config_->HasSubkey) {
+ WriteYamrKey(SubkeyColumnIds_);
+ } else {
+ // Due to YAMRed DSV format logic, when there is no subkey, but still some
+ // columns are marked as subkey columns, we should explicitly remove them
+ // from the row (i. e. don't print as a rest of values in YAMR value column).
+ for (int id : SubkeyColumnIds_)
+ RowValues_[id] = nullptr;
+ }
+ WriteYamrValue();
+ TryFlushBuffer(false);
+ }
+ TryFlushBuffer(true);
+ }
+
+ void WriteYamrKey(const std::vector<int>& columnIds)
+ {
+ auto* stream = Config_->Lenval ? &LenvalBuffer_ : GetOutputStream();
+
+ bool firstColumn = true;
+ for (int id : columnIds) {
+ if (!firstColumn) {
+ stream->Write(Config_->YamrKeysSeparator);
+ } else {
+ firstColumn = false;
+ }
+ if (!RowValues_[id]) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::FormatCannotRepresentRow, "Key column %Qv is missing",
+ NameTableReader_->GetName(id));
+ }
+ WriteUnversionedValue(*RowValues_[id], stream, ValueEscapeTable_);
+ RowValues_[id] = nullptr;
+ }
+
+ if (Config_->Lenval) {
+ WritePod(*GetOutputStream(), static_cast<ui32>(LenvalBuffer_.Size()));
+ GetOutputStream()->Write(LenvalBuffer_.Begin(), LenvalBuffer_.Size());
+ LenvalBuffer_.Clear();
+ } else {
+ GetOutputStream()->Write(Config_->FieldSeparator);
+ }
+ }
+
+ void WriteYamrValue()
+ {
+ auto* stream = Config_->Lenval ? &LenvalBuffer_ : GetOutputStream();
+
+ bool firstColumn = true;
+ for (int id = 0; id < NameTableSize_; ++id) {
+ const auto* value = RowValues_[id];
+ if (!value) {
+ continue;
+ }
+ bool skip = Config_->SkipUnsupportedTypesInValue && IsAnyOrComposite(value->Type);
+ if (!skip) {
+ if (!firstColumn) {
+ stream->Write(Config_->FieldSeparator);
+ } else {
+ firstColumn = false;
+ }
+ stream->Write(EscapedColumnNames_[id]);
+ stream->Write(Config_->KeyValueSeparator);
+ WriteUnversionedValue(*RowValues_[id], stream, ValueEscapeTable_);
+ }
+ RowValues_[id] = nullptr;
+ }
+
+ if (Config_->Lenval) {
+ WritePod(*GetOutputStream(), static_cast<ui32>(LenvalBuffer_.Size()));
+ GetOutputStream()->Write(LenvalBuffer_.Begin(), LenvalBuffer_.Size());
+ LenvalBuffer_.Clear();
+ } else {
+ GetOutputStream()->Write(Config_->RecordSeparator);
+ }
+ }
+
+ void UpdateEscapedColumnNames()
+ {
+ // We store escaped column names in order to not re-escape them each time we write a column name.
+ NameTableSize_ = NameTableReader_->GetSize();
+ EscapedColumnNames_.reserve(NameTableSize_);
+ for (int columnIndex = EscapedColumnNames_.size(); columnIndex < NameTableSize_; ++columnIndex) {
+ EscapedColumnNames_.emplace_back(Escape(NameTableReader_->GetName(columnIndex), KeyEscapeTable_));
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamredDsv(
+ TYamredDsvFormatConfigPtr config,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ if (controlAttributesConfig->EnableKeySwitch && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("Key switches are not supported in text YAMRed DSV format");
+ }
+
+ if (controlAttributesConfig->EnableRangeIndex && !config->Lenval) {
+ THROW_ERROR_EXCEPTION("Range indices are not supported in text YAMRed DSV format");
+ }
+
+ if (controlAttributesConfig->EnableEndOfStream) {
+ THROW_ERROR_EXCEPTION("End of stream control attribute is not supported in YAMRed DSV format");
+ }
+
+ return New<TSchemalessWriterForYamredDsv>(
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount,
+ config);
+}
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamredDsv(
+ const IAttributeDictionary& attributes,
+ TNameTablePtr nameTable,
+ IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount)
+{
+ try {
+ auto config = ConvertTo<TYamredDsvFormatConfigPtr>(&attributes);
+ return CreateSchemalessWriterForYamredDsv(
+ config,
+ nameTable,
+ output,
+ enableContextSaving,
+ controlAttributesConfig,
+ keyColumnCount);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::InvalidFormat, "Failed to parse config for YAMRed DSV format") << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/yamred_dsv_writer.h b/yt/yt/client/formats/yamred_dsv_writer.h
new file mode 100644
index 0000000000..6d9050abe3
--- /dev/null
+++ b/yt/yt/client/formats/yamred_dsv_writer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "helpers.h"
+#include "yamr_writer_base.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamredDsv(
+ TYamredDsvFormatConfigPtr config,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+ISchemalessFormatWriterPtr CreateSchemalessWriterForYamredDsv(
+ const NYTree::IAttributeDictionary& attributes,
+ NTableClient::TNameTablePtr nameTable,
+ NConcurrency::IAsyncOutputStreamPtr output,
+ bool enableContextSaving,
+ TControlAttributesConfigPtr controlAttributesConfig,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
+
diff --git a/yt/yt/client/formats/yql_yson_converter.cpp b/yt/yt/client/formats/yql_yson_converter.cpp
new file mode 100644
index 0000000000..dc57a1a69a
--- /dev/null
+++ b/yt/yt/client/formats/yql_yson_converter.cpp
@@ -0,0 +1,855 @@
+#include "yql_yson_converter.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <yt/yt/core/json/json_writer.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <library/cpp/string_utils/base64/base64.h>
+
+#include <util/charset/utf8.h>
+
+#include <util/string/cast.h>
+
+#include <util/stream/buffer.h>
+
+namespace NYT::NFormats {
+
+using namespace NJson;
+using namespace NTableClient;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TStringBuf TruncateUtf8(TStringBuf string, i64 limit)
+{
+ auto begin = reinterpret_cast<const unsigned char*>(string.cbegin());
+ auto end = reinterpret_cast<const unsigned char*>(string.cend());
+ auto it = begin + std::max<i64>(0, limit);
+ if (it < end) {
+ wchar32 rune;
+ size_t runeLength;
+ while (it >= begin && SafeReadUTF8Char(rune, runeLength, it, end) != RECODE_OK) {
+ --it;
+ }
+ }
+ return string.Trunc(it - begin);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYqlJsonWriter
+ : public TRefCounted
+{
+public:
+ explicit TYqlJsonWriter(NJson::IJsonWriter* underlying);
+
+ void OnInt64Scalar(i64 value);
+ void OnUint64Scalar(ui64 value);
+ void OnDoubleScalar(double value);
+ void OnBooleanScalar(bool value);
+ void OnEntity();
+
+ void OnBeginList();
+ void OnListItem();
+ void OnEndList();
+
+ void OnBeginMap();
+ void OnKeyedItem(TStringBuf key);
+ void OnEndMap();
+
+ void OnStringScalarNoWeightLimit(TStringBuf value);
+ void OnStringScalarWeightLimited(TStringBuf value, i64 limit);
+ void TransferYsonWeightLimited(const std::function<void(NYson::TCheckedInDebugYsonTokenWriter*)>& callback, i64 limit);
+
+ ui64 GetWrittenByteCount() const;
+
+private:
+ NJson::IJsonWriter* const Underlying_;
+ TBuffer Buffer_;
+ std::unique_ptr<NJson::IJsonConsumer> AnyWriter_;
+
+public:
+ static constexpr TStringBuf KeyValue = "val";
+ static constexpr TStringBuf KeyIncomplete = "inc";
+ static constexpr TStringBuf KeyBase64 = "b64";
+
+private:
+ void OnStringScalarImpl(TStringBuf value, bool incomplete = false, bool base64 = false);
+ template <typename TFun>
+ void WriteWithWrapping(TFun fun, bool incomplete, bool base64, bool forceMap = false);
+};
+
+DECLARE_REFCOUNTED_CLASS(TYqlJsonWriter)
+DEFINE_REFCOUNTED_TYPE(TYqlJsonWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYqlJsonWriter::TYqlJsonWriter(IJsonWriter* underlying)
+ : Underlying_(underlying)
+{
+ auto config = New<TJsonFormatConfig>();
+ config->Stringify = true;
+ config->AnnotateWithTypes = true;
+ config->EncodeUtf8 = true;
+ AnyWriter_ = CreateJsonConsumer(Underlying_, EYsonType::Node, std::move(config));
+}
+
+void TYqlJsonWriter::OnInt64Scalar(i64 value)
+{
+ Underlying_->OnStringScalar(ToString(value));
+}
+
+void TYqlJsonWriter::OnUint64Scalar(ui64 value)
+{
+ Underlying_->OnStringScalar(ToString(value));
+}
+
+void TYqlJsonWriter::OnDoubleScalar(double value)
+{
+ Underlying_->OnStringScalar(::FloatToString(value));
+}
+
+void TYqlJsonWriter::OnBooleanScalar(bool value)
+{
+ Underlying_->OnBooleanScalar(value);
+}
+
+void TYqlJsonWriter::TYqlJsonWriter::OnEntity()
+{
+ Underlying_->OnEntity();
+}
+
+void TYqlJsonWriter::OnBeginList()
+{
+ Underlying_->OnBeginList();
+}
+
+void TYqlJsonWriter::OnListItem()
+{
+ Underlying_->OnListItem();
+}
+
+void TYqlJsonWriter::OnEndList()
+{
+ Underlying_->OnEndList();
+}
+
+void TYqlJsonWriter::OnBeginMap()
+{
+ Underlying_->OnBeginMap();
+}
+
+void TYqlJsonWriter::OnKeyedItem(TStringBuf key)
+{
+ Underlying_->OnKeyedItem(key);
+}
+
+void TYqlJsonWriter::OnEndMap()
+{
+ Underlying_->OnEndMap();
+}
+
+void TYqlJsonWriter::OnStringScalarNoWeightLimit(TStringBuf value)
+{
+ bool base64 = !IsUtf(value);
+ OnStringScalarImpl(value, false, base64);
+}
+
+void TYqlJsonWriter::OnStringScalarWeightLimited(TStringBuf value, i64 limit)
+{
+ TStringBuf valueToWrite = value;
+ auto incomplete = false;
+ auto base64 = false;
+ if (IsUtf(valueToWrite)) {
+ if (static_cast<i64>(valueToWrite.Size()) > limit) {
+ valueToWrite = TruncateUtf8(valueToWrite, limit);
+ incomplete = true;
+ }
+ } else {
+ base64 = true;
+ auto maxEncodedSize = Base64EncodeBufSize(valueToWrite.Size());
+ if (static_cast<i64>(maxEncodedSize) > limit) {
+ auto truncatedLen = (limit - 1) / 4 * 3;
+ incomplete = (truncatedLen < static_cast<i64>(valueToWrite.Size()));
+ valueToWrite.Trunc(truncatedLen);
+ }
+ Buffer_.Resize(Base64EncodeBufSize(valueToWrite.Size()));
+ valueToWrite = Base64Encode(valueToWrite, Buffer_.Begin());
+ }
+ OnStringScalarImpl(valueToWrite, incomplete, base64);
+}
+
+void TYqlJsonWriter::TransferYsonWeightLimited(
+ const std::function<void(TCheckedInDebugYsonTokenWriter*)>& callback,
+ i64 limit)
+{
+ Buffer_.Clear();
+ {
+ TBufferOutput output(Buffer_);
+ TCheckedInDebugYsonTokenWriter writer(&output);
+ callback(&writer);
+ }
+ auto yson = TStringBuf(Buffer_.Begin(), Buffer_.End());
+ if (static_cast<i64>(yson.Size()) > limit) {
+ OnStringScalarImpl("", /* incomplete */ true, /* base64 */ false);
+ } else {
+ WriteWithWrapping(
+ [&] {
+ AnyWriter_->OnRaw(yson, EYsonType::Node);
+ },
+ /* incomplete */ false,
+ /* base64 */ false,
+ /* forceMap */ true);
+ }
+}
+
+void TYqlJsonWriter::OnStringScalarImpl(
+ TStringBuf value,
+ bool incomplete,
+ bool base64)
+{
+ WriteWithWrapping(
+ [&] {
+ Underlying_->OnStringScalar(value);
+ },
+ incomplete,
+ base64);
+}
+
+template <typename TFun>
+void TYqlJsonWriter::WriteWithWrapping(
+ TFun fun,
+ bool incomplete,
+ bool base64,
+ bool forceMap)
+{
+ auto needMap = (incomplete || base64 || forceMap);
+ if (needMap) {
+ Underlying_->OnBeginMap();
+ }
+ if (incomplete) {
+ Underlying_->OnKeyedItem(KeyIncomplete);
+ Underlying_->OnBooleanScalar(incomplete);
+ }
+ if (base64) {
+ Underlying_->OnKeyedItem(KeyBase64);
+ Underlying_->OnBooleanScalar(base64);
+ }
+
+ if (needMap) {
+ Underlying_->OnKeyedItem(KeyValue);
+ }
+ fun();
+
+ if (needMap) {
+ Underlying_->OnEndMap();
+ }
+}
+
+ui64 TYqlJsonWriter::GetWrittenByteCount() const
+{
+ return Underlying_->GetWrittenByteCount();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TWeightLimitedYsonToYqlConverter = std::function<
+ void(NYson::TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)>;
+using TWeightLimitedUnversionedValueToYqlConverter = std::function<
+ void(TUnversionedValue value, TYqlJsonWriter* consumer, i64 totalLimit)>;
+
+static TWeightLimitedYsonToYqlConverter CreateWeightLimitedYsonToYqlConverter(
+ const TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config);
+
+static void EnsureYsonItemTypeEqual(const TYsonItem& item, EYsonItemType type)
+{
+ if (Y_UNLIKELY(item.GetType() != type)) {
+ THROW_ERROR_EXCEPTION("YSON item type mismatch: expected %Qlv, got %Qlv",
+ type,
+ item.GetType());
+ }
+}
+
+static void EnsureYsonItemTypeNotEqual(const TYsonItem& item, EYsonItemType type)
+{
+ if (Y_UNLIKELY(item.GetType() == type)) {
+ THROW_ERROR_EXCEPTION("Unexpected YSON item type %Qlv",
+ type);
+ }
+}
+
+template <EValueType PhysicalType>
+class TSimpleYsonToYqlConverter
+{
+public:
+ explicit TSimpleYsonToYqlConverter(TYqlConverterConfigPtr config)
+ : Config_(std::move(config))
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ auto getStringWeightLimit = [&] {
+ auto bytesLeft = totalLimit - static_cast<i64>(consumer->GetWrittenByteCount());
+ if (Config_->StringWeightLimit) {
+ bytesLeft = std::min(bytesLeft, *Config_->StringWeightLimit);
+ }
+ return bytesLeft;
+ };
+
+ const auto& item = cursor->GetCurrent();
+ if constexpr (PhysicalType == EValueType::Int64) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::Int64Value);
+ consumer->OnInt64Scalar(item.UncheckedAsInt64());
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::Uint64) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::Uint64Value);
+ consumer->OnUint64Scalar(item.UncheckedAsUint64());
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::String) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::StringValue);
+ consumer->OnStringScalarWeightLimited(
+ item.UncheckedAsString(),
+ getStringWeightLimit());
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::Double) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::DoubleValue);
+ consumer->OnDoubleScalar(item.UncheckedAsDouble());
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::Boolean) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::BooleanValue);
+ consumer->OnBooleanScalar(item.UncheckedAsBoolean());
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::Null) {
+ EnsureYsonItemTypeEqual(item, EYsonItemType::EntityValue);
+ consumer->OnEntity();
+ cursor->Next();
+ } else if constexpr (PhysicalType == EValueType::Any || PhysicalType == EValueType::Composite) {
+ consumer->TransferYsonWeightLimited(
+ [cursor] (TCheckedInDebugYsonTokenWriter* writer) {
+ cursor->TransferComplexValue(writer);
+ },
+ getStringWeightLimit());
+ } else {
+ // Silly assert instead of forbidden |static_assert(false)|.
+ static_assert(PhysicalType == EValueType::Int64, "Unexpected physical type");
+ }
+ }
+
+private:
+ TYqlConverterConfigPtr Config_;
+};
+
+TWeightLimitedYsonToYqlConverter CreateSimpleTypeYsonToYqlConverter(
+ EValueType physicalType,
+ TYqlConverterConfigPtr config)
+{
+ switch (physicalType) {
+ case EValueType::Int64:
+ return TSimpleYsonToYqlConverter<EValueType::Int64>(std::move(config));
+ case EValueType::Uint64:
+ return TSimpleYsonToYqlConverter<EValueType::Uint64>(std::move(config));
+ case EValueType::String:
+ return TSimpleYsonToYqlConverter<EValueType::String>(std::move(config));
+ case EValueType::Double:
+ return TSimpleYsonToYqlConverter<EValueType::Double>(std::move(config));
+ case EValueType::Boolean:
+ return TSimpleYsonToYqlConverter<EValueType::Boolean>(std::move(config));
+ case EValueType::Null:
+ return TSimpleYsonToYqlConverter<EValueType::Null>(std::move(config));
+ case EValueType::Any:
+ return TSimpleYsonToYqlConverter<EValueType::Any>(std::move(config));
+ case EValueType::Composite:
+ return TSimpleYsonToYqlConverter<EValueType::Composite>(std::move(config));
+
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+ }
+ ThrowUnexpectedValueType(physicalType);
+}
+
+class TDecimalYsonToYqlConverter
+{
+public:
+ TDecimalYsonToYqlConverter(int precision, int scale)
+ : Precision_(precision)
+ , Scale_(scale)
+ { }
+
+ void operator () (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 /*totalLimit*/) const
+ {
+ const auto& item = cursor->GetCurrent();
+ EnsureYsonItemTypeEqual(item, EYsonItemType::StringValue);
+
+ char buffer[NDecimal::TDecimal::MaxTextSize];
+ const auto binaryDecimal = item.UncheckedAsString();
+ const auto textDecimal = NDecimal::TDecimal::BinaryToText(binaryDecimal, Precision_, Scale_, buffer, sizeof(buffer));
+ consumer->OnStringScalarNoWeightLimit(textDecimal);
+
+ cursor->Next();
+ }
+
+private:
+ const int Precision_;
+ const int Scale_;
+};
+
+class TListYsonToYqlConverter
+{
+public:
+ TListYsonToYqlConverter(const TListLogicalType& type, TYqlConverterConfigPtr config)
+ : ElementConverter_(CreateWeightLimitedYsonToYqlConverter(type.GetElement(), std::move(config)))
+ { }
+
+ void operator() (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::BeginList);
+ cursor->Next();
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem(TYqlJsonWriter::KeyValue);
+ consumer->OnBeginList();
+ auto incomplete = false;
+ while (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ if (static_cast<i64>(consumer->GetWrittenByteCount()) >= totalLimit) {
+ incomplete = true;
+ break;
+ }
+ consumer->OnListItem();
+ ElementConverter_(cursor, consumer, totalLimit);
+ }
+ if (incomplete) {
+ while (cursor->GetCurrent().GetType() != EYsonItemType::EndList) {
+ cursor->SkipComplexValue();
+ }
+ }
+ consumer->OnEndList();
+ if (incomplete) {
+ consumer->OnKeyedItem(TYqlJsonWriter::KeyIncomplete);
+ consumer->OnBooleanScalar(true);
+ }
+ consumer->OnEndMap();
+ cursor->Next();
+ }
+
+private:
+ const TWeightLimitedYsonToYqlConverter ElementConverter_;
+};
+
+void ConvertSequence(
+ TYsonPullParserCursor* cursor,
+ TYqlJsonWriter* consumer,
+ const std::vector<TWeightLimitedYsonToYqlConverter>& converters,
+ i64 totalLimit)
+{
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::BeginList);
+ cursor->Next();
+
+ consumer->OnBeginList();
+ for (const auto& converter : converters) {
+ EnsureYsonItemTypeNotEqual(cursor->GetCurrent(), EYsonItemType::EndList);
+ converter(cursor, consumer, totalLimit);
+ }
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::EndList);
+ consumer->OnEndList();
+
+ cursor->Next();
+}
+
+class TStructYsonToYqlConverter
+{
+public:
+ explicit TStructYsonToYqlConverter(const TStructLogicalType& type, TYqlConverterConfigPtr config)
+ {
+ for (const auto& field : type.GetFields()) {
+ FieldConverters_.push_back(CreateWeightLimitedYsonToYqlConverter(field.Type, config));
+ }
+ }
+
+ void operator() (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ ConvertSequence(cursor, consumer, FieldConverters_, totalLimit);
+ }
+
+private:
+ std::vector<TWeightLimitedYsonToYqlConverter> FieldConverters_;
+};
+
+class TTupleYsonToYqlConverter
+{
+public:
+ TTupleYsonToYqlConverter(const TTupleLogicalType& type, TYqlConverterConfigPtr config)
+ {
+ for (const auto& element : type.GetElements()) {
+ ElementConverters_.push_back(CreateWeightLimitedYsonToYqlConverter(element, config));
+ }
+ }
+
+ void operator() (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ ConvertSequence(cursor, consumer, ElementConverters_, totalLimit);
+ }
+
+private:
+ std::vector<TWeightLimitedYsonToYqlConverter> ElementConverters_;
+};
+
+class TOptionalYsonToYqlConverter
+{
+public:
+ TOptionalYsonToYqlConverter(const TOptionalLogicalType& type, TYqlConverterConfigPtr config)
+ : IsElementNullable_(type.GetElement()->IsNullable())
+ , ElementConverter_(CreateWeightLimitedYsonToYqlConverter(type.GetElement(), std::move(config)))
+ { }
+
+ void operator() (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ if (cursor->GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ consumer->OnEntity();
+ cursor->Next();
+ return;
+ }
+
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ if (IsElementNullable_) {
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::BeginList);
+ cursor->Next();
+ EnsureYsonItemTypeNotEqual(cursor->GetCurrent(), EYsonItemType::EndList);
+ ElementConverter_(cursor, consumer, totalLimit);
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::EndList);
+ cursor->Next();
+ } else {
+ ElementConverter_(cursor, consumer, totalLimit);
+ }
+ consumer->OnEndList();
+ }
+
+private:
+ const bool IsElementNullable_;
+ const TWeightLimitedYsonToYqlConverter ElementConverter_;
+};
+
+class TVariantYsonToYqlConverter
+{
+public:
+ TVariantYsonToYqlConverter(const TVariantTupleLogicalType& type, TYqlConverterConfigPtr config)
+ {
+ for (const auto& element : type.GetElements()) {
+ ElementConverters_.push_back(CreateWeightLimitedYsonToYqlConverter(element, config));
+ }
+ }
+
+ TVariantYsonToYqlConverter(const TVariantStructLogicalType& type, TYqlConverterConfigPtr config)
+ {
+ for (const auto& field : type.GetFields()) {
+ ElementConverters_.push_back(CreateWeightLimitedYsonToYqlConverter(field.Type, config));
+ }
+ }
+
+ void operator() (TYsonPullParserCursor* cursor, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::BeginList);
+ cursor->Next();
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::Int64Value);
+ const auto alternativeIndex = cursor->GetCurrent().UncheckedAsInt64();
+ if (Y_UNLIKELY(!(0 <= alternativeIndex && alternativeIndex < static_cast<int>(ElementConverters_.size())))) {
+ THROW_ERROR_EXCEPTION("Alternative index is out of bounds: expected it to be in [%v, %v), got %v",
+ 0,
+ ElementConverters_.size(),
+ alternativeIndex);
+ }
+ cursor->Next();
+ consumer->OnBeginList();
+
+ consumer->OnListItem();
+ consumer->OnInt64Scalar(alternativeIndex);
+
+ consumer->OnListItem();
+ ElementConverters_[alternativeIndex](cursor, consumer, totalLimit);
+
+ EnsureYsonItemTypeEqual(cursor->GetCurrent(), EYsonItemType::EndList);
+ consumer->OnEndList();
+
+ cursor->Next();
+ }
+
+private:
+ std::vector<TWeightLimitedYsonToYqlConverter> ElementConverters_;
+};
+
+static TWeightLimitedYsonToYqlConverter CreateWeightLimitedYsonToYqlConverter(
+ const TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config)
+{
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CreateSimpleTypeYsonToYqlConverter(
+ GetPhysicalType(logicalType->AsSimpleTypeRef().GetElement()),
+ std::move(config));
+ case ELogicalMetatype::Decimal:
+ return TDecimalYsonToYqlConverter(
+ logicalType->AsDecimalTypeRef().GetPrecision(),
+ logicalType->AsDecimalTypeRef().GetScale());
+ case ELogicalMetatype::List:
+ return TListYsonToYqlConverter(logicalType->AsListTypeRef(), std::move(config));
+ case ELogicalMetatype::Struct:
+ return TStructYsonToYqlConverter(logicalType->AsStructTypeRef(), std::move(config));
+ case ELogicalMetatype::Optional:
+ return TOptionalYsonToYqlConverter(logicalType->AsOptionalTypeRef(), std::move(config));
+ case ELogicalMetatype::Tuple:
+ return TTupleYsonToYqlConverter(logicalType->AsTupleTypeRef(), std::move(config));
+ case ELogicalMetatype::VariantStruct:
+ return TVariantYsonToYqlConverter(logicalType->AsVariantStructTypeRef(), std::move(config));
+ case ELogicalMetatype::VariantTuple:
+ return TVariantYsonToYqlConverter(logicalType->AsVariantTupleTypeRef(), std::move(config));
+ case ELogicalMetatype::Dict: {
+ // Converter is identical to list<tuple<Key, Value>>.
+ auto listOfTuples = ListLogicalType(
+ TupleLogicalType({
+ logicalType->AsDictTypeRef().GetKey(),
+ logicalType->AsDictTypeRef().GetValue()}));
+ return TListYsonToYqlConverter(
+ listOfTuples->AsListTypeRef(),
+ std::move(config));
+ }
+ case ELogicalMetatype::Tagged:
+ return CreateWeightLimitedYsonToYqlConverter(
+ logicalType->AsTaggedTypeRef().GetElement(),
+ std::move(config));
+ }
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EValueType Type, bool Required>
+class TSimpleUnversionedValueToYqlConverter
+{
+public:
+ void operator () (TUnversionedValue value, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ if constexpr (!Required) {
+ if (value.Type == EValueType::Null) {
+ consumer->OnEntity();
+ return;
+ }
+ consumer->OnBeginList();
+ }
+
+ if (Y_UNLIKELY(Type != EValueType::Any && value.Type != Type)) {
+ THROW_ERROR_EXCEPTION("Bad value type: expected %Qlv, got %Qlv",
+ Type,
+ value.Type);
+ }
+ if constexpr (Type == EValueType::Int64) {
+ consumer->OnInt64Scalar(value.Data.Int64);
+ } else if constexpr (Type == EValueType::Uint64) {
+ consumer->OnUint64Scalar(value.Data.Uint64);
+ } else if constexpr (Type == EValueType::String) {
+ auto bytesLeft = totalLimit - static_cast<i64>(consumer->GetWrittenByteCount());
+ consumer->OnStringScalarWeightLimited(
+ value.AsStringBuf(),
+ bytesLeft);
+ } else if constexpr (Type == EValueType::Double) {
+ consumer->OnDoubleScalar(value.Data.Double);
+ } else if constexpr (Type == EValueType::Boolean) {
+ consumer->OnBooleanScalar(value.Data.Boolean);
+ } else if constexpr (Type == EValueType::Any || Type == EValueType::Composite) {
+ auto bytesLeft = totalLimit - static_cast<i64>(consumer->GetWrittenByteCount());
+ consumer->TransferYsonWeightLimited(
+ [value] (TCheckedInDebugYsonTokenWriter* tokenWriter) {
+ UnversionedValueToYson(value, tokenWriter);
+ },
+ bytesLeft);
+ } else if constexpr (Type == EValueType::Null) {
+ consumer->OnEntity();
+ } else {
+ // Silly assert instead of uncompilable |static_assert(false)|.
+ static_assert(Type == EValueType::Int64, "Unexpected value type");
+ }
+
+ if constexpr (!Required) {
+ consumer->OnEndList();
+ }
+ }
+};
+
+class TDecimalUnversionedValueToYqlConverter
+{
+public:
+ TDecimalUnversionedValueToYqlConverter(int precision, int scale, bool isNullable)
+ : Precision_(precision)
+ , Scale_(scale)
+ , IsNullable_(isNullable)
+ { }
+
+ void operator () (TUnversionedValue value, TYqlJsonWriter* consumer, i64 /*totalLimit*/) const
+ {
+ if (IsNullable_) {
+ if (value.Type == EValueType::Null) {
+ consumer->OnEntity();
+ return;
+ }
+ consumer->OnBeginList();
+ }
+
+ char buffer[NDecimal::TDecimal::MaxTextSize];
+ const auto binaryDecimal = value.AsStringBuf();
+ const auto textDecimal = NDecimal::TDecimal::BinaryToText(binaryDecimal, Precision_, Scale_, buffer, sizeof(buffer));
+ consumer->OnStringScalarNoWeightLimit(textDecimal);
+
+ if (IsNullable_) {
+ consumer->OnEndList();
+ }
+ }
+
+private:
+ const int Precision_;
+ const int Scale_;
+ const bool IsNullable_;
+};
+
+class TComplexUnversionedValueToYqlConverter
+{
+public:
+ TComplexUnversionedValueToYqlConverter(const TLogicalTypePtr& logicalType, TYqlConverterConfigPtr config)
+ : Type_(logicalType)
+ , Converter_(CreateWeightLimitedYsonToYqlConverter(logicalType, std::move(config)))
+ , IsNullable_(logicalType->IsNullable())
+ { }
+
+ void operator () (TUnversionedValue value, TYqlJsonWriter* consumer, i64 totalLimit)
+ {
+ if (value.Type == EValueType::Null) {
+ if (Y_UNLIKELY(!IsNullable_)) {
+ THROW_ERROR_EXCEPTION("Unexpected value type %Qlv for non-nullable type %Qv",
+ EValueType::Null,
+ NTableClient::ToString(*Type_));
+ }
+ consumer->OnEntity();
+ return;
+ }
+ if (Y_UNLIKELY(value.Type != EValueType::Composite)) {
+ THROW_ERROR_EXCEPTION("Bad value type: expected %Qlv, got %Qlv",
+ EValueType::Composite,
+ value.Type);
+ }
+ TMemoryInput input(value.Data.String, value.Length);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+ Converter_(&cursor, consumer, totalLimit);
+ }
+
+private:
+ const TLogicalTypePtr Type_;
+ const TWeightLimitedYsonToYqlConverter Converter_;
+ const bool IsNullable_;
+};
+
+static TWeightLimitedUnversionedValueToYqlConverter CreateSimpleUnversionedValueToYqlConverter(
+ EValueType type,
+ bool isRequired)
+{
+ switch (type) {
+#define CASE(type) \
+ case type: \
+ if (isRequired) { \
+ return TSimpleUnversionedValueToYqlConverter<type, true>{}; \
+ } else { \
+ return TSimpleUnversionedValueToYqlConverter<type, false>{}; \
+ }
+
+ CASE(EValueType::Int64)
+ CASE(EValueType::Uint64)
+ CASE(EValueType::String)
+ CASE(EValueType::Double)
+ CASE(EValueType::Boolean)
+ CASE(EValueType::Any)
+ CASE(EValueType::Composite)
+ CASE(EValueType::Null)
+#undef CASE
+
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+ }
+ ThrowUnexpectedValueType(type);
+}
+
+static TWeightLimitedUnversionedValueToYqlConverter CreateWeightLimitedUnversionedValueToYqlConverter(
+ const TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config)
+{
+ auto denullifiedLogicalType = DenullifyLogicalType(logicalType);
+ switch (denullifiedLogicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple: {
+ auto[simpleType, isRequired] = CastToV1Type(logicalType);
+ auto physicalType = GetPhysicalType(simpleType);
+ return CreateSimpleUnversionedValueToYqlConverter(physicalType, isRequired);
+ }
+ case ELogicalMetatype::Decimal: {
+ const int precision = denullifiedLogicalType->AsDecimalTypeRef().GetPrecision();
+ const int scale = denullifiedLogicalType->AsDecimalTypeRef().GetScale();
+ const int isNullable = logicalType->IsNullable();
+ return TDecimalUnversionedValueToYqlConverter(precision, scale, isNullable);
+ }
+ case ELogicalMetatype::Optional:
+ // NB. It's complex optional because we called DenullifyLogicalType
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::Dict:
+ return TComplexUnversionedValueToYqlConverter(logicalType, std::move(config));
+ case ELogicalMetatype::Tagged:
+ // DenullifyLogicalType should have cleaned type of tagged types.
+ Y_FAIL();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonToYqlConverter CreateYsonToYqlConverter(
+ const TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config,
+ IJsonWriter* consumer)
+{
+ auto weightLimitedConverter = CreateWeightLimitedYsonToYqlConverter(logicalType, config);
+ return [=, consumer = New<TYqlJsonWriter>(consumer)] (TYsonPullParserCursor* cursor) mutable {
+ auto totalLimit = config->FieldWeightLimit
+ ? static_cast<i64>(consumer->GetWrittenByteCount()) + *config->FieldWeightLimit
+ : std::numeric_limits<i64>::max();
+ weightLimitedConverter(cursor, consumer.Get(), totalLimit);
+ };
+}
+
+TUnversionedValueToYqlConverter CreateUnversionedValueToYqlConverter(
+ const TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config,
+ IJsonWriter* consumer)
+{
+ auto weightLimitedConverter = CreateWeightLimitedUnversionedValueToYqlConverter(logicalType, config);
+ return [=, consumer = New<TYqlJsonWriter>(consumer)] (TUnversionedValue value) mutable {
+ auto totalLimit = config->FieldWeightLimit
+ ? static_cast<i64>(consumer->GetWrittenByteCount()) + *config->FieldWeightLimit
+ : std::numeric_limits<i64>::max();
+ weightLimitedConverter(value, consumer.Get(), totalLimit);
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yql_yson_converter.h b/yt/yt/client/formats/yql_yson_converter.h
new file mode 100644
index 0000000000..5caafea963
--- /dev/null
+++ b/yt/yt/client/formats/yql_yson_converter.h
@@ -0,0 +1,41 @@
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/json/public.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <util/generic/buffer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TYqlConverterConfig
+ : public TRefCounted
+{
+ std::optional<i64> StringWeightLimit;
+ std::optional<i64> FieldWeightLimit;
+};
+DECLARE_REFCOUNTED_STRUCT(TYqlConverterConfig)
+DEFINE_REFCOUNTED_TYPE(TYqlConverterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TYsonToYqlConverter = std::function<void(NYson::TYsonPullParserCursor*)>;
+using TUnversionedValueToYqlConverter = std::function<void(NTableClient::TUnversionedValue)>;
+
+// Created converters throw exceptions on schema incompliance.
+TYsonToYqlConverter CreateYsonToYqlConverter(
+ const NTableClient::TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config,
+ NJson::IJsonWriter* writer);
+TUnversionedValueToYqlConverter CreateUnversionedValueToYqlConverter(
+ const NTableClient::TLogicalTypePtr& logicalType,
+ TYqlConverterConfigPtr config,
+ NJson::IJsonWriter* writer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yson_map_to_unversioned_value.cpp b/yt/yt/client/formats/yson_map_to_unversioned_value.cpp
new file mode 100644
index 0000000000..fced3477f5
--- /dev/null
+++ b/yt/yt/client/formats/yson_map_to_unversioned_value.cpp
@@ -0,0 +1,181 @@
+#include "yson_map_to_unversioned_value.h"
+
+namespace NYT::NFormats {
+
+using namespace NTableClient;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonMapToUnversionedValueConverter::TYsonMapToUnversionedValueConverter(
+ const NComplexTypes::TYsonConverterConfig& config,
+ IValueConsumer* consumer)
+ : Consumer_(consumer)
+ , AllowUnknownColumns_(consumer->GetAllowUnknownColumns())
+ , NameTable_(consumer->GetNameTable())
+ , ColumnConsumer_(config, this)
+{ }
+
+void TYsonMapToUnversionedValueConverter::Reset()
+{
+ YT_VERIFY(!InsideValue_);
+}
+
+void TYsonMapToUnversionedValueConverter::OnStringScalar(TStringBuf value)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnStringScalar(value);
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnInt64Scalar(i64 value)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnInt64Scalar(value);
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnUint64Scalar(ui64 value)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnUint64Scalar(value);
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnDoubleScalar(double value)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnDoubleScalar(value);
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnBooleanScalar(bool value)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnBooleanScalar(value);
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnEntity()
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnEntity();
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnBeginList()
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnBeginList();
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnListItem()
+{
+ YT_VERIFY(InsideValue_); // Should crash on BeginList()
+ ColumnConsumer_.OnListItem();
+}
+
+void TYsonMapToUnversionedValueConverter::OnBeginMap()
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnBeginMap();
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnKeyedItem(TStringBuf name)
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnKeyedItem(name);
+ } else {
+ InsideValue_ = true;
+ if (AllowUnknownColumns_) {
+ ColumnConsumer_.SetColumnIndex(NameTable_->GetIdOrRegisterName(name));
+ } else {
+ auto id = NameTable_->FindId(name);
+ if (!id) {
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::SchemaViolation, "No column %Qv in table schema",
+ name);
+ }
+ ColumnConsumer_.SetColumnIndex(*id);
+ }
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnEndMap()
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnEndMap();
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnBeginAttributes()
+{
+ if (Y_LIKELY(InsideValue_)) {
+ ColumnConsumer_.OnBeginAttributes();
+ } else {
+ THROW_ERROR_EXCEPTION("YSON map without attributes expected");
+ }
+}
+
+void TYsonMapToUnversionedValueConverter::OnEndList()
+{
+ YT_VERIFY(InsideValue_); // Should throw on BeginList().
+ ColumnConsumer_.OnEndList();
+}
+
+void TYsonMapToUnversionedValueConverter::OnEndAttributes()
+{
+ YT_VERIFY(InsideValue_); // Should throw on BeginAttributes()
+ ColumnConsumer_.OnEndAttributes();
+}
+
+const TNameTablePtr& TYsonMapToUnversionedValueConverter::GetNameTable() const
+{
+ return Consumer_->GetNameTable();
+}
+
+bool TYsonMapToUnversionedValueConverter::GetAllowUnknownColumns() const
+{
+ YT_ABORT();
+}
+
+void TYsonMapToUnversionedValueConverter::OnBeginRow()
+{
+ YT_ABORT();
+}
+
+void TYsonMapToUnversionedValueConverter::OnValue(const TUnversionedValue& value)
+{
+ InsideValue_ = false;
+ Consumer_->OnValue(value);
+}
+
+void TYsonMapToUnversionedValueConverter::OnEndRow()
+{
+ YT_ABORT();
+}
+
+const NTableClient::TTableSchemaPtr& TYsonMapToUnversionedValueConverter::GetSchema() const
+{
+ return Consumer_->GetSchema();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yson_map_to_unversioned_value.h b/yt/yt/client/formats/yson_map_to_unversioned_value.h
new file mode 100644
index 0000000000..1e53ad6c88
--- /dev/null
+++ b/yt/yt/client/formats/yson_map_to_unversioned_value.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/public.h>
+#include <yt/yt/client/table_client/table_consumer.h>
+#include <yt/yt/client/table_client/value_consumer.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonMapToUnversionedValueConverter
+ : public NYson::TYsonConsumerBase
+ , private NTableClient::IValueConsumer
+{
+public:
+ TYsonMapToUnversionedValueConverter(
+ const NComplexTypes::TYsonConverterConfig& config,
+ NTableClient::IValueConsumer* valueConsumer);
+
+ void Reset();
+ virtual void OnStringScalar(TStringBuf value) override;
+ virtual void OnInt64Scalar(i64 value) override;
+ virtual void OnUint64Scalar(ui64 value) override;
+ virtual void OnDoubleScalar(double value) override;
+ virtual void OnBooleanScalar(bool value) override;
+ virtual void OnEntity() override;
+ virtual void OnBeginList() override;
+ virtual void OnListItem() override;
+ virtual void OnBeginMap() override;
+ virtual void OnKeyedItem(TStringBuf name) override;
+ virtual void OnEndMap() override;
+ virtual void OnBeginAttributes() override;
+ virtual void OnEndList() override;
+ virtual void OnEndAttributes() override;
+
+private:
+ virtual const NTableClient::TNameTablePtr& GetNameTable() const override;
+ virtual bool GetAllowUnknownColumns() const override;
+ virtual void OnBeginRow() override;
+ virtual void OnValue(const NTableClient::TUnversionedValue& value) override;
+ virtual void OnEndRow() override;
+ virtual const NTableClient::TTableSchemaPtr& GetSchema() const override;
+
+private:
+ NTableClient::IValueConsumer* const Consumer_;
+ const bool AllowUnknownColumns_;
+ const NTableClient::TNameTablePtr NameTable_;
+
+ NTableClient::TYsonToUnversionedValueConverter ColumnConsumer_;
+ bool InsideValue_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yson_parser.cpp b/yt/yt/client/formats/yson_parser.cpp
new file mode 100644
index 0000000000..193b7caf31
--- /dev/null
+++ b/yt/yt/client/formats/yson_parser.cpp
@@ -0,0 +1,56 @@
+#include "yson_parser.h"
+#include "parser.h"
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/core/yson/parser.h>
+
+namespace NYT::NFormats {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Wrapper around YSON parser that implements IParser interface.
+class TYsonParserAdapter
+ : public IParser
+{
+public:
+ TYsonParserAdapter(
+ IYsonConsumer* consumer,
+ EYsonType type,
+ bool enableLinePositionInfo)
+ : Parser(consumer, type, {
+ .EnableLinePositionInfo=enableLinePositionInfo,
+ .MemoryLimit=NTableClient::MaxRowWeightLimit
+ })
+ { }
+
+ void Read(TStringBuf data) override
+ {
+ Parser.Read(data);
+ }
+
+ void Finish() override
+ {
+ Parser.Finish();
+ }
+
+private:
+ TYsonParser Parser;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYson(
+ IYsonConsumer* consumer,
+ EYsonType type,
+ bool enableLinePositionInfo)
+{
+ return std::unique_ptr<IParser>(new TYsonParserAdapter(consumer, type, enableLinePositionInfo));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/formats/yson_parser.h b/yt/yt/client/formats/yson_parser.h
new file mode 100644
index 0000000000..a6e4880b30
--- /dev/null
+++ b/yt/yt/client/formats/yson_parser.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NFormats {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IParser> CreateParserForYson(
+ NYson::IYsonConsumer* consumer,
+ NYson::EYsonType type = NYson::EYsonType::Node,
+ bool enableLinePositionInfo = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/hedging/cache.cpp b/yt/yt/client/hedging/cache.cpp
new file mode 100644
index 0000000000..cf725dbf1f
--- /dev/null
+++ b/yt/yt/client/hedging/cache.cpp
@@ -0,0 +1,93 @@
+#include "cache.h"
+#include "options.h"
+#include "rpc.h"
+
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConfig MakeClusterConfig(const TClustersConfig& clustersConfig, TStringBuf clusterUrl)
+{
+ auto [cluster, proxyRole] = ExtractClusterAndProxyRole(clusterUrl);
+ auto it = clustersConfig.GetClusterConfigs().find(cluster);
+ TConfig config = (it != clustersConfig.GetClusterConfigs().end()) ?
+ it->second : clustersConfig.GetDefaultConfig();
+ config.SetClusterName(ToString(cluster));
+ if (!proxyRole.empty()) {
+ config.SetProxyRole(ToString(proxyRole));
+ }
+ return config;
+}
+
+namespace {
+
+class TClientsCache
+ : public IClientsCache
+{
+public:
+ TClientsCache(const TClustersConfig& config, const NApi::TClientOptions& options)
+ : ClustersConfig_(config)
+ , Options_(options)
+ {}
+
+ NApi::IClientPtr GetClient(TStringBuf clusterUrl) override
+ {
+ {
+ auto guard = ReaderGuard(Lock_);
+ auto clientIt = Clients_.find(clusterUrl);
+ if (clientIt != Clients_.end()) {
+ return clientIt->second;
+ }
+ }
+
+ auto client = CreateClient(MakeClusterConfig(ClustersConfig_, clusterUrl), Options_);
+
+ auto guard = WriterGuard(Lock_);
+ return Clients_.try_emplace(clusterUrl, client).first->second;
+ }
+
+private:
+ TClustersConfig ClustersConfig_;
+ NApi::TClientOptions Options_;
+ NThreading::TReaderWriterSpinLock Lock_;
+ THashMap<TString, NApi::IClientPtr> Clients_;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+IClientsCachePtr CreateClientsCache(const TClustersConfig& config, const NApi::TClientOptions& options)
+{
+ return New<TClientsCache>(config, options);
+}
+
+IClientsCachePtr CreateClientsCache(const TConfig& config, const NApi::TClientOptions& options)
+{
+ TClustersConfig clustersConfig;
+ *clustersConfig.MutableDefaultConfig() = config;
+ return CreateClientsCache(clustersConfig, options);
+}
+
+IClientsCachePtr CreateClientsCache(const TConfig& config)
+{
+ return CreateClientsCache(config, GetClientOpsFromEnvStatic());
+}
+
+IClientsCachePtr CreateClientsCache(const NApi::TClientOptions& options)
+{
+ return CreateClientsCache(TClustersConfig{}, options);
+}
+
+IClientsCachePtr CreateClientsCache()
+{
+ return CreateClientsCache(GetClientOpsFromEnvStatic());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/cache.h b/yt/yt/client/hedging/cache.h
new file mode 100644
index 0000000000..e957cecc7e
--- /dev/null
+++ b/yt/yt/client/hedging/cache.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+class TConfig;
+class TClustersConfig;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Cache of clients per cluster.
+class IClientsCache
+ : public TRefCounted
+{
+public:
+ virtual NApi::IClientPtr GetClient(TStringBuf clusterUrl) = 0;
+};
+
+DECLARE_REFCOUNTED_TYPE(IClientsCache)
+DEFINE_REFCOUNTED_TYPE(IClientsCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates clients cache which explicitly given config. Server name is always overwritten with requested.
+IClientsCachePtr CreateClientsCache(const TClustersConfig& config, const NApi::TClientOptions& options);
+
+//! Creates clients cache which shares same config (except server name).
+IClientsCachePtr CreateClientsCache(const TConfig& config, const NApi::TClientOptions& options);
+
+//! Shortcut to use client options from env.
+IClientsCachePtr CreateClientsCache(const TConfig& config);
+
+//! Shortcut to create cache with custom options and proxy role.
+IClientsCachePtr CreateClientsCache(const NApi::TClientOptions& options);
+
+//! Shortcut to create cache with default config.
+IClientsCachePtr CreateClientsCache();
+
+
+//! Helper function to create one cluster config from cluster url and clusters config
+TConfig MakeClusterConfig(const TClustersConfig& config, TStringBuf clusterUrl);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/config.cpp b/yt/yt/client/hedging/config.cpp
new file mode 100644
index 0000000000..a0b009f1b3
--- /dev/null
+++ b/yt/yt/client/hedging/config.cpp
@@ -0,0 +1,67 @@
+#include "config.h"
+
+#include "counter.h"
+#include "rpc.h"
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TClientConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("connection", &TThis::Connection);
+ registrar.Parameter("initial_penalty", &TThis::InitialPenalty);
+}
+
+THedgingClientOptions::TClientOptions::TClientOptions(
+ NApi::IClientPtr client,
+ TString clusterName,
+ TDuration initialPenalty,
+ TCounterPtr counter)
+ : Client(std::move(client))
+ , ClusterName(std::move(clusterName))
+ , InitialPenalty(initialPenalty)
+ , Counter(std::move(counter))
+{ }
+
+THedgingClientOptions::TClientOptions::TClientOptions(
+ NApi::IClientPtr client,
+ TDuration initialPenalty,
+ TCounterPtr counter)
+ : TClientOptions(client, "default", initialPenalty, counter)
+{ }
+
+void THedgingClientOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("client_configs", &TThis::ClientConfigs)
+ .Default();
+ registrar.Parameter("ban_penalty", &TThis::BanPenalty)
+ .Default(TDuration::MilliSeconds(1));
+ registrar.Parameter("ban_duration", &TThis::BanDuration)
+ .Default(TDuration::MilliSeconds(50));
+ registrar.Parameter("tags", &TThis::Tags)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ NProfiling::TTagSet counterTagSet;
+
+ for (const auto& [tagName, tagValue] : config->Tags) {
+ counterTagSet.AddTag(NProfiling::TTag(tagName, tagValue));
+ }
+
+ config->Clients.reserve(config->Clients.size());
+ for (const auto& client : config->ClientConfigs) {
+ THROW_ERROR_EXCEPTION_UNLESS(client->Connection->ClusterUrl, "\"cluster_url\" must be set");
+ auto clusterUrl = client->Connection->ClusterUrl.value();
+ config->Clients.emplace_back(
+ CreateClient(client->Connection),
+ clusterUrl,
+ client->InitialPenalty,
+ New<TCounter>(counterTagSet.WithTag(NProfiling::TTag("yt_cluster", clusterUrl))));
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/config.h b/yt/yt/client/hedging/config.h
new file mode 100644
index 0000000000..b8e7c5dc2f
--- /dev/null
+++ b/yt/yt/client/hedging/config.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rpc_proxy/config.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <util/generic/vector.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TClientConfig
+ : public virtual NYTree::TYsonStruct
+{
+ NApi::NRpcProxy::TConnectionConfigPtr Connection;
+ TDuration InitialPenalty;
+
+ REGISTER_YSON_STRUCT(TClientConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! The options for hedging client.
+struct THedgingClientOptions
+ : public virtual NYTree::TYsonStructLite
+{
+ std::vector<TClientConfigPtr> ClientConfigs;
+
+ struct TClientOptions
+ {
+ TClientOptions(
+ NApi::IClientPtr client,
+ TString clusterName,
+ TDuration initialPenalty,
+ TCounterPtr counter = {});
+
+ TClientOptions(
+ NApi::IClientPtr client,
+ TDuration initialPenalty,
+ TCounterPtr counter = {});
+
+ NApi::IClientPtr Client;
+ TString ClusterName;
+ TDuration InitialPenalty;
+ TCounterPtr Counter;
+ };
+
+ TDuration BanPenalty;
+ TDuration BanDuration;
+ THashMap<TString, TString> Tags;
+
+ // This parameter is set on postprocessor.
+ TVector<TClientOptions> Clients;
+
+ REGISTER_YSON_STRUCT(THedgingClientOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/counter.cpp b/yt/yt/client/hedging/counter.cpp
new file mode 100644
index 0000000000..29eed83000
--- /dev/null
+++ b/yt/yt/client/hedging/counter.cpp
@@ -0,0 +1,51 @@
+#include "counter.h"
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto HedgingClientProfiler = NProfiling::TRegistry{"/hedging_client"}.WithHot();
+static const auto LagPenaltyProviderProfiler = NProfiling::TRegistry{"/lag_penalty_provider"}.WithHot();
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCounter::TCounter(const NProfiling::TRegistry& registry)
+ : SuccessRequestCount(registry.Counter("/requests_success"))
+ , CancelRequestCount(registry.Counter("/requests_cancel"))
+ , ErrorRequestCount(registry.Counter("/requests_error"))
+ , EffectivePenalty(registry.TimeGauge("/effective_penalty"))
+ , ExternalPenalty(registry.TimeGauge("/external_penalty"))
+ , RequestDuration(registry.Histogram("/request_duration", TDuration::MilliSeconds(1), TDuration::MilliSeconds(70)))
+{
+}
+
+TCounter::TCounter(const TString& clusterName)
+ : TCounter(HedgingClientProfiler.WithTag("yt_cluster", clusterName))
+{
+}
+
+TCounter::TCounter(const NProfiling::TTagSet& tagSet)
+ : TCounter(HedgingClientProfiler.WithTags(tagSet))
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLagPenaltyProviderCounters::TLagPenaltyProviderCounters(const NProfiling::TRegistry& registry, const TVector<TString>& clusters)
+ : SuccessRequestCount(registry.Counter("/update_success"))
+ , ErrorRequestCount(registry.Counter("/update_error"))
+ , TotalTabletsCount(registry.Gauge("/tablets_total"))
+{
+ for (const auto& cluster : clusters) {
+ LagTabletsCount.emplace(cluster, registry.WithTag("yt_cluster", cluster).Gauge("/tablets_with_lag"));
+ }
+}
+
+TLagPenaltyProviderCounters::TLagPenaltyProviderCounters(const TString& tablePath, const TVector<TString>& clusterNames)
+ : TLagPenaltyProviderCounters(LagPenaltyProviderProfiler.WithTag("table", tablePath), clusterNames)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/counter.h b/yt/yt/client/hedging/counter.h
new file mode 100644
index 0000000000..1502df2475
--- /dev/null
+++ b/yt/yt/client/hedging/counter.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// ! Counters which will be collected from yt-client.
+struct TCounter final
+{
+ explicit TCounter(const TString& clusterName);
+ explicit TCounter(const NProfiling::TTagSet& tagSet);
+ explicit TCounter(const NProfiling::TRegistry& registry);
+
+ NProfiling::TCounter SuccessRequestCount;
+ NProfiling::TCounter CancelRequestCount;
+ NProfiling::TCounter ErrorRequestCount;
+ NProfiling::TTimeGauge EffectivePenalty;
+ NProfiling::TTimeGauge ExternalPenalty;
+ NProfiling::TEventTimer RequestDuration;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCounter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// ! Counters for TReplicationLagPenaltyProvider.
+struct TLagPenaltyProviderCounters final
+{
+ TLagPenaltyProviderCounters(const NProfiling::TRegistry& registry, const TVector<TString>& clusters);
+ TLagPenaltyProviderCounters(const TString& tablePath, const TVector<TString>& replicaClusters);
+
+ NProfiling::TCounter SuccessRequestCount;
+ NProfiling::TCounter ErrorRequestCount;
+ THashMap<TString, NProfiling::TGauge> LagTabletsCount; // cluster -> # of tablets
+ NProfiling::TGauge TotalTabletsCount;
+};
+
+DEFINE_REFCOUNTED_TYPE(TLagPenaltyProviderCounters)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/hedging.cpp b/yt/yt/client/hedging/hedging.cpp
new file mode 100644
index 0000000000..25bb480f69
--- /dev/null
+++ b/yt/yt/client/hedging/hedging.cpp
@@ -0,0 +1,283 @@
+#include "hedging.h"
+
+#include "cache.h"
+#include "counter.h"
+#include "logger.h"
+#include "rpc.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/misc/method_helpers.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <library/cpp/iterator/enumerate.h>
+#include <library/cpp/iterator/zip.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/va_args.h>
+
+#include <util/system/compiler.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+using namespace NYT;
+using namespace NApi;
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TClientBuilder = std::function<NApi::IClientPtr(const TConfig&)>;
+
+#define RETRYABLE_METHOD(ReturnType, MethodName, Args) \
+ ReturnType MethodName(Y_METHOD_USED_ARGS_DECLARATION(Args)) override { \
+ return Executor_->DoWithHedging<typename ReturnType::TValueType>(BIND([=] (IClientPtr client) { \
+ return client->MethodName(Y_PASS_METHOD_USED_ARGS(Args)); \
+ })); \
+ } Y_SEMICOLON_GUARD
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THedgingClient
+ : public IClient
+{
+public:
+ THedgingClient(const THedgingClientOptions& options, const IPenaltyProviderPtr& penaltyProvider)
+ : Executor_(New<THedgingExecutor>(options, penaltyProvider))
+ { }
+
+ // IClientBase methods.
+ // Supported methods.
+ IConnectionPtr GetConnection() override
+ {
+ return Executor_->GetConnection();
+ }
+
+ std::optional<TStringBuf> GetClusterName(bool fetchIfNull = true) override
+ {
+ Y_UNUSED(fetchIfNull);
+ return {};
+ }
+
+ RETRYABLE_METHOD(TFuture<IUnversionedRowsetPtr>, LookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TUnversionedRow>&, const TLookupRowsOptions&));
+ RETRYABLE_METHOD(TFuture<IVersionedRowsetPtr>, VersionedLookupRows, (const NYPath::TYPath&, NTableClient::TNameTablePtr, const TSharedRange<NTableClient::TUnversionedRow>&, const TVersionedLookupRowsOptions&));
+ RETRYABLE_METHOD(TFuture<TSelectRowsResult>, SelectRows, (const TString&, const TSelectRowsOptions&));
+ RETRYABLE_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullQueue, (const NYPath::TRichYPath&, i64, int, const NQueueClient::TQueueRowBatchReadOptions&, const TPullQueueOptions&));
+ RETRYABLE_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, i64, int, const NQueueClient::TQueueRowBatchReadOptions&, const TPullConsumerOptions&));
+ RETRYABLE_METHOD(TFuture<void>, RegisterQueueConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, bool, const TRegisterQueueConsumerOptions&));
+ RETRYABLE_METHOD(TFuture<void>, UnregisterQueueConsumer, (const NYPath::TRichYPath&, const NYPath::TRichYPath&, const TUnregisterQueueConsumerOptions&));
+ RETRYABLE_METHOD(TFuture<std::vector<TListQueueConsumerRegistrationsResult>>, ListQueueConsumerRegistrations, (const std::optional<NYPath::TRichYPath>&, const std::optional<NYPath::TRichYPath>&, const TListQueueConsumerRegistrationsOptions&));
+ RETRYABLE_METHOD(TFuture<NYson::TYsonString>, ExplainQuery, (const TString&, const TExplainQueryOptions&));
+ RETRYABLE_METHOD(TFuture<ITableReaderPtr>, CreateTableReader, (const NYPath::TRichYPath&, const TTableReaderOptions&));
+ RETRYABLE_METHOD(TFuture<NYson::TYsonString>, GetNode, (const NYPath::TYPath&, const TGetNodeOptions&));
+ RETRYABLE_METHOD(TFuture<NYson::TYsonString>, ListNode, (const NYPath::TYPath&, const TListNodeOptions&));
+ RETRYABLE_METHOD(TFuture<bool>, NodeExists, (const NYPath::TYPath&, const TNodeExistsOptions&));
+ RETRYABLE_METHOD(TFuture<IFileReaderPtr>, CreateFileReader, (const NYPath::TYPath&, const TFileReaderOptions&));
+ RETRYABLE_METHOD(TFuture<std::vector<IUnversionedRowsetPtr>>, MultiLookup, (const std::vector<TMultiLookupSubrequest>&, const TMultiLookupOptions&));
+
+ // Unsupported methods.
+ UNSUPPORTED_METHOD(TFuture<ITransactionPtr>, StartTransaction, (NTransactionClient::ETransactionType, const TTransactionStartOptions&));
+ UNSUPPORTED_METHOD(TFuture<ITableWriterPtr>, CreateTableWriter, (const NYPath::TRichYPath&, const TTableWriterOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SetNode, (const NYPath::TYPath&, const NYson::TYsonString&, const TSetNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, MultisetAttributesNode, (const NYPath::TYPath&, const NYTree::IMapNodePtr&, const TMultisetAttributesNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, RemoveNode, (const NYPath::TYPath&, const TRemoveNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<NCypressClient::TNodeId>, CreateNode, (const NYPath::TYPath&, NObjectClient::EObjectType, const TCreateNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<TLockNodeResult>, LockNode, (const NYPath::TYPath&, NCypressClient::ELockMode, const TLockNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, UnlockNode, (const NYPath::TYPath&, const TUnlockNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<NCypressClient::TNodeId>, CopyNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TCopyNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<NCypressClient::TNodeId>, MoveNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TMoveNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<NCypressClient::TNodeId>, LinkNode, (const NYPath::TYPath&, const NYPath::TYPath&, const TLinkNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ConcatenateNodes, (const std::vector<NYPath::TRichYPath>&, const NYPath::TRichYPath&, const TConcatenateNodesOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ExternalizeNode, (const NYPath::TYPath&, NObjectClient::TCellTag, const TExternalizeNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, InternalizeNode, (const NYPath::TYPath&, const TInternalizeNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<NObjectClient::TObjectId>, CreateObject, (NObjectClient::EObjectType, const TCreateObjectOptions&));
+ UNSUPPORTED_METHOD(IFileWriterPtr, CreateFileWriter, (const NYPath::TRichYPath&, const TFileWriterOptions&));
+ UNSUPPORTED_METHOD(IJournalReaderPtr, CreateJournalReader, (const NYPath::TYPath&, const TJournalReaderOptions&));
+ UNSUPPORTED_METHOD(IJournalWriterPtr, CreateJournalWriter, (const NYPath::TYPath&, const TJournalWriterOptions&));
+
+ // IClient methods.
+ // Unsupported methods.
+ UNSUPPORTED_METHOD(void, Terminate, ());
+ UNSUPPORTED_METHOD(const NTabletClient::ITableMountCachePtr&, GetTableMountCache, ());
+ UNSUPPORTED_METHOD(const NChaosClient::IReplicationCardCachePtr&, GetReplicationCardCache, ());
+ UNSUPPORTED_METHOD(const NTransactionClient::ITimestampProviderPtr&, GetTimestampProvider, ());
+ UNSUPPORTED_METHOD(ITransactionPtr, AttachTransaction, (NTransactionClient::TTransactionId, const TTransactionAttachOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, MountTable, (const NYPath::TYPath&, const TMountTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, UnmountTable, (const NYPath::TYPath&, const TUnmountTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, RemountTable, (const NYPath::TYPath&, const TRemountTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, FreezeTable, (const NYPath::TYPath&, const TFreezeTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, UnfreezeTable, (const NYPath::TYPath&, const TUnfreezeTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ReshardTable, (const NYPath::TYPath&, const std::vector<NTableClient::TUnversionedOwningRow>&, const TReshardTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ReshardTable, (const NYPath::TYPath&, int, const TReshardTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, ReshardTableAutomatic, (const NYPath::TYPath&, const TReshardTableAutomaticOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, TrimTable, (const NYPath::TYPath&, int, i64, const TTrimTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AlterTable, (const NYPath::TYPath&, const TAlterTableOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AlterTableReplica, (NTabletClient::TTableReplicaId, const TAlterTableReplicaOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AlterReplicationCard, (NChaosClient::TReplicationCardId, const TAlterReplicationCardOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (const NYPath::TYPath&, const NTableClient::TNameTablePtr&, const TSharedRange<NTableClient::TUnversionedRow>&, const TGetInSyncReplicasOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (const NYPath::TYPath&, const TGetInSyncReplicasOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<TTabletInfo>>, GetTabletInfos, (const NYPath::TYPath&, const std::vector<int>&, const TGetTabletInfosOptions&));
+ UNSUPPORTED_METHOD(TFuture<TGetTabletErrorsResult>, GetTabletErrors, (const NYPath::TYPath&, const TGetTabletErrorsOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, BalanceTabletCells, (const TString&, const std::vector<NYPath::TYPath>&, const TBalanceTabletCellsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TSkynetSharePartsLocationsPtr>, LocateSkynetShare, (const NYPath::TRichYPath&, const TLocateSkynetShareOptions&));
+ UNSUPPORTED_METHOD(TFuture<std::vector<NTableClient::TColumnarStatistics>>, GetColumnarStatistics, (const std::vector<NYPath::TRichYPath>&, const TGetColumnarStatisticsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TMultiTablePartitions>, PartitionTables, (const std::vector<NYPath::TRichYPath>&, const TPartitionTablesOptions&));
+ UNSUPPORTED_METHOD(TFuture<NYson::TYsonString>, GetTablePivotKeys, (const NYPath::TYPath&, const TGetTablePivotKeysOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, CreateTableBackup, (const TBackupManifestPtr&, const TCreateTableBackupOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, RestoreTableBackup, (const TBackupManifestPtr&, const TRestoreTableBackupOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, TruncateJournal, (const NYPath::TYPath&, i64, const TTruncateJournalOptions&));
+ UNSUPPORTED_METHOD(TFuture<TGetFileFromCacheResult>, GetFileFromCache, (const TString&, const TGetFileFromCacheOptions&));
+ UNSUPPORTED_METHOD(TFuture<TPutFileToCacheResult>, PutFileToCache, (const NYPath::TYPath&, const TString&, const TPutFileToCacheOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AddMember, (const TString&, const TString&, const TAddMemberOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, RemoveMember, (const TString&, const TString&, const TRemoveMemberOptions&));
+ UNSUPPORTED_METHOD(TFuture<TCheckPermissionResponse>, CheckPermission, (const TString&, const NYPath::TYPath&, NYTree::EPermission, const TCheckPermissionOptions&));
+ UNSUPPORTED_METHOD(TFuture<TCheckPermissionByAclResult>, CheckPermissionByAcl, (const std::optional<TString>&, NYTree::EPermission, NYTree::INodePtr, const TCheckPermissionByAclOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, TransferAccountResources, (const TString&, const TString&, NYTree::INodePtr, const TTransferAccountResourcesOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, TransferPoolResources, (const TString&, const TString&, const TString&, NYTree::INodePtr, const TTransferPoolResourcesOptions&));
+ UNSUPPORTED_METHOD(TFuture<NScheduler::TOperationId>, StartOperation, (NScheduler::EOperationType, const NYson::TYsonString&, const TStartOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AbortOperation, (const NScheduler::TOperationIdOrAlias&, const TAbortOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SuspendOperation, (const NScheduler::TOperationIdOrAlias&, const TSuspendOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ResumeOperation, (const NScheduler::TOperationIdOrAlias&, const TResumeOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, CompleteOperation, (const NScheduler::TOperationIdOrAlias&, const TCompleteOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, UpdateOperationParameters, (const NScheduler::TOperationIdOrAlias&, const NYson::TYsonString&, const TUpdateOperationParametersOptions&));
+ UNSUPPORTED_METHOD(TFuture<TOperation>, GetOperation, (const NScheduler::TOperationIdOrAlias&, const TGetOperationOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, DumpJobContext, (NJobTrackerClient::TJobId, const NYPath::TYPath&, const TDumpJobContextOptions&));
+ UNSUPPORTED_METHOD(TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr>, GetJobInput, (NJobTrackerClient::TJobId, const TGetJobInputOptions&));
+ UNSUPPORTED_METHOD(TFuture<NYson::TYsonString>, GetJobInputPaths, (NJobTrackerClient::TJobId, const TGetJobInputPathsOptions&));
+ UNSUPPORTED_METHOD(TFuture<NYson::TYsonString>, GetJobSpec, (NJobTrackerClient::TJobId, const TGetJobSpecOptions&));
+ UNSUPPORTED_METHOD(TFuture<TSharedRef>, GetJobStderr, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobStderrOptions&));
+ UNSUPPORTED_METHOD(TFuture<TSharedRef>, GetJobFailContext, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobFailContextOptions&));
+ UNSUPPORTED_METHOD(TFuture<TListOperationsResult>, ListOperations, (const TListOperationsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TListJobsResult>, ListJobs, (const NScheduler::TOperationIdOrAlias&, const TListJobsOptions&));
+ UNSUPPORTED_METHOD(TFuture<NYson::TYsonString>, GetJob, (const NScheduler::TOperationIdOrAlias&, NJobTrackerClient::TJobId, const TGetJobOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AbandonJob, (NJobTrackerClient::TJobId, const TAbandonJobOptions&));
+ UNSUPPORTED_METHOD(TFuture<TPollJobShellResponse>, PollJobShell, (NJobTrackerClient::TJobId, const std::optional<TString>&, const NYson::TYsonString&, const TPollJobShellOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AbortJob, (NJobTrackerClient::TJobId, const TAbortJobOptions&));
+ UNSUPPORTED_METHOD(TFuture<TClusterMeta>, GetClusterMeta, (const TGetClusterMetaOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, CheckClusterLiveness, (const TCheckClusterLivenessOptions&));
+ UNSUPPORTED_METHOD(TFuture<int>, BuildSnapshot, (const TBuildSnapshotOptions&));
+ UNSUPPORTED_METHOD(TFuture<TCellIdToSnapshotIdMap>, BuildMasterSnapshots, (const TBuildMasterSnapshotsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SwitchLeader, (NObjectClient::TCellId, const TString&, const TSwitchLeaderOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ResetStateHash, (NObjectClient::TCellId, const TResetStateHashOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, GCCollect, (const TGCCollectOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, KillProcess, (const TString&, const TKillProcessOptions&));
+ UNSUPPORTED_METHOD(TFuture<TString>, WriteCoreDump, (const TString&, const TWriteCoreDumpOptions&));
+ UNSUPPORTED_METHOD(TFuture<TGuid>, WriteLogBarrier, (const TString&, const TWriteLogBarrierOptions&));
+ UNSUPPORTED_METHOD(TFuture<TString>, WriteOperationControllerCoreDump, (NJobTrackerClient::TOperationId, const TWriteOperationControllerCoreDumpOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, HealExecNode, (const TString&, const THealExecNodeOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SuspendCoordinator, (NObjectClient::TCellId, const TSuspendCoordinatorOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ResumeCoordinator, (NObjectClient::TCellId, const TResumeCoordinatorOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, MigrateReplicationCards, (NObjectClient::TCellId, const TMigrateReplicationCardsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SuspendChaosCells, (const std::vector<NObjectClient::TCellId>&, const TSuspendChaosCellsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ResumeChaosCells, (const std::vector<NObjectClient::TCellId>&, const TResumeChaosCellsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SuspendTabletCells, (const std::vector<NObjectClient::TCellId>&, const TSuspendTabletCellsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, ResumeTabletCells, (const std::vector<NObjectClient::TCellId>&, const TResumeTabletCellsOptions&));
+ UNSUPPORTED_METHOD(TFuture<NChaosClient::TReplicationCardPtr>, GetReplicationCard, (NChaosClient::TReplicationCardId, const TGetReplicationCardOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, UpdateChaosTableReplicaProgress, (NChaosClient::TReplicaId, const TUpdateChaosTableReplicaProgressOptions&));
+ UNSUPPORTED_METHOD(TFuture<TMaintenanceId>, AddMaintenance, (EMaintenanceComponent, const TString&, EMaintenanceType, const TString&, const TAddMaintenanceOptions&));
+ UNSUPPORTED_METHOD(TFuture<TMaintenanceCounts>, RemoveMaintenance, (EMaintenanceComponent, const TString&, const TMaintenanceFilter&, const TRemoveMaintenanceOptions&));
+ UNSUPPORTED_METHOD(TFuture<TDisableChunkLocationsResult>, DisableChunkLocations, (const TString&, const std::vector<TGuid>&, const TDisableChunkLocationsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TDestroyChunkLocationsResult>, DestroyChunkLocations, (const TString&, const std::vector<TGuid>&, const TDestroyChunkLocationsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TResurrectChunkLocationsResult>, ResurrectChunkLocations, (const TString&, const std::vector<TGuid>&, const TResurrectChunkLocationsOptions&));
+ UNSUPPORTED_METHOD(TFuture<TPullRowsResult>, PullRows, (const NYPath::TYPath&, const TPullRowsOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, SetUserPassword, (const TString&, const TString&, const TString&, const TSetUserPasswordOptions&));
+ UNSUPPORTED_METHOD(TFuture<TIssueTokenResult>, IssueToken, (const TString&, const TString&, const TIssueTokenOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, RevokeToken, (const TString&, const TString&, const TString&, const TRevokeTokenOptions&));
+ UNSUPPORTED_METHOD(TFuture<TListUserTokensResult>, ListUserTokens, (const TString&, const TString&, const TListUserTokensOptions&));
+ UNSUPPORTED_METHOD(TFuture<NQueryTrackerClient::TQueryId>, StartQuery, (NQueryTrackerClient::EQueryEngine, const TString&, const TStartQueryOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AbortQuery, (NQueryTrackerClient::TQueryId, const TAbortQueryOptions&));
+ UNSUPPORTED_METHOD(TFuture<TQueryResult>, GetQueryResult, (NQueryTrackerClient::TQueryId, i64, const TGetQueryResultOptions&));
+ UNSUPPORTED_METHOD(TFuture<IUnversionedRowsetPtr>, ReadQueryResult, (NQueryTrackerClient::TQueryId, i64, const TReadQueryResultOptions&));
+ UNSUPPORTED_METHOD(TFuture<TQuery>, GetQuery, (NQueryTrackerClient::TQueryId, const TGetQueryOptions&));
+ UNSUPPORTED_METHOD(TFuture<TListQueriesResult>, ListQueries, (const TListQueriesOptions&));
+ UNSUPPORTED_METHOD(TFuture<void>, AlterQuery, (NQueryTrackerClient::TQueryId, const TAlterQueryOptions&));
+
+private:
+ THedgingExecutorPtr Executor_;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::IClientPtr CreateHedgingClient(const THedgingClientOptions& options)
+{
+ return New<THedgingClient>(options, CreateDummyPenaltyProvider());
+}
+
+NApi::IClientPtr CreateHedgingClient(const THedgingClientOptions& options,
+ const IPenaltyProviderPtr& penaltyProvider)
+{
+ return New<THedgingClient>(options, penaltyProvider);
+}
+
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config)
+{
+ return CreateHedgingClient(GetHedgingClientOptions(config));
+}
+
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config, const IClientsCachePtr& clientsCache)
+{
+ return CreateHedgingClient(GetHedgingClientOptions(config, clientsCache));
+}
+
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config,
+ const IClientsCachePtr& clientsCache,
+ const IPenaltyProviderPtr& penaltyProvider)
+{
+ return CreateHedgingClient(GetHedgingClientOptions(config, clientsCache), penaltyProvider);
+}
+
+THedgingClientOptions GetHedgingClientOptions(const THedgingClientConfig& config, TClientBuilder clientBuilder)
+{
+ THedgingClientOptions options;
+ options.BanPenalty = TDuration::MilliSeconds(config.GetBanPenalty());
+ options.BanDuration = TDuration::MilliSeconds(config.GetBanDuration());
+
+ NProfiling::TTagSet counterTagSet;
+
+ for (const auto& [tagName, tagValue] : config.GetTags()) {
+ counterTagSet.AddTag(NProfiling::TTag(tagName, tagValue));
+ }
+
+ options.Clients.reserve(config.GetClients().size());
+ for (const auto& client : config.GetClients()) {
+ options.Clients.emplace_back(
+ clientBuilder(client.GetClientConfig()),
+ client.GetClientConfig().GetClusterName(),
+ TDuration::MilliSeconds(client.GetInitialPenalty()),
+ New<TCounter>(counterTagSet.WithTag(NProfiling::TTag("yt_cluster", client.GetClientConfig().GetClusterName()))));
+ }
+ return options;
+}
+
+THedgingClientOptions GetHedgingClientOptions(const THedgingClientConfig& config)
+{
+ return GetHedgingClientOptions(config, [] (const auto& clientConfig) {
+ return CreateClient(clientConfig);
+ });
+}
+
+THedgingClientOptions GetHedgingClientOptions(const THedgingClientConfig& config, const IClientsCachePtr& clientsCache)
+{
+ return GetHedgingClientOptions(config, [clientsCache] (const auto& clientConfig) {
+ return clientsCache->GetClient(clientConfig.GetClusterName());
+ });
+}
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/hedging.h b/yt/yt/client/hedging/hedging.h
new file mode 100644
index 0000000000..3c515ce8b9
--- /dev/null
+++ b/yt/yt/client/hedging/hedging.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "public.h"
+
+#include "hedging_executor.h"
+#include "penalty_provider.h"
+
+#include <yt/yt/client/api/client.h>
+
+// @brief HedgingClient is a wrapper for several YT-clients with ability
+// to retry asynchronously the same request with different underlying-clients.
+// HedgingClient implements IClient interface and supports methods that do not change state of data on YT.
+// Currently supported methods: LookupRows, VersionedLookupRows, SelectRows, ExplainQuery,
+// CreateTableReader, GetNode, ListNode, NodeExists, CreateFileReader
+//
+// For initial configuration every YT-client needs an InitialPenalty value.
+// This value is used to determine in which order YT-clients will be used.
+//
+// MinInitialPenalty - minimal retry timeout value out of all YT-clients.
+// EffectivePenalty - is a delay value for starting a request with a corresponding YT-client.
+// For every client this value is calculated as: InitialPenalty - MinInitialPenalty.
+//
+// If any of the clients responses with a success result: requests to other YT-clients are cancelled.
+//
+// If any of the clients responses with an error: it's InitialPenalty is
+// increased by BanPenalty value for the next BanDuration time interval.
+// Both BanPenalty and BanDuration values are set in MultiClientCluster config.
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// from config.proto
+class THedgingClientConfig;
+
+// @brief Options for hedging client.
+// from hedging_executor.h
+struct THedgingClientOptions;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// @brief Method for creating HedgingClient with given options.
+NApi::IClientPtr CreateHedgingClient(const THedgingClientOptions& options);
+
+// @brief Method for creating HedgingClient with given options and ability to use penalty updater policy.
+// Currently for experimental usage.
+NApi::IClientPtr CreateHedgingClient(const THedgingClientOptions& options, const IPenaltyProviderPtr& penaltyProvider);
+
+// @brief Method for creating HedgingClient with given rpc clients config.
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config);
+
+// @brief Method for creating HedgingClient with given rpc clients config and preinitialized clients.
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config, const IClientsCachePtr& clientsCache);
+
+// @brief Method for creating HedgingClient with given rpc clients config, preinitialized clients and PenaltyProvider.
+NApi::IClientPtr CreateHedgingClient(const THedgingClientConfig& config, const IClientsCachePtr& clientsCache, const IPenaltyProviderPtr& penaltyProvider);
+
+// @brief Method for creating HedgingClient options from given config and preinitialized clients.
+THedgingClientOptions GetHedgingClientOptions(const THedgingClientConfig& config, const IClientsCachePtr& clientsCache);
+
+// @brief Method for creating HedgingClient options from given config.
+THedgingClientOptions GetHedgingClientOptions(const THedgingClientConfig& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/hedging_executor.cpp b/yt/yt/client/hedging/hedging_executor.cpp
new file mode 100644
index 0000000000..bd3e98e4fe
--- /dev/null
+++ b/yt/yt/client/hedging/hedging_executor.cpp
@@ -0,0 +1,80 @@
+#include "hedging_executor.h"
+
+#include "logger.h"
+
+#include <yt/yt/core/logging/log.h>
+
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+THedgingExecutor::THedgingExecutor(const THedgingClientOptions& options, const IPenaltyProviderPtr& penaltyProvider)
+ : BanPenalty_(NProfiling::DurationToCpuDuration(options.BanPenalty))
+ , BanDuration_(NProfiling::DurationToCpuDuration(options.BanDuration))
+ , PenaltyProvider_(penaltyProvider)
+{
+ Y_ENSURE(!options.Clients.empty(), "Clients should not be empty!");
+ for (const auto& clientOptions : options.Clients) {
+ Y_ENSURE(clientOptions.Client, "Client pointer should be valid!");
+ Clients_.emplace_back(
+ clientOptions.Client,
+ NProfiling::DurationToCpuDuration(clientOptions.InitialPenalty),
+ clientOptions.Counter ? clientOptions.Counter : New<TCounter>(clientOptions.Client->GetConnection()->GetClusterId()),
+ clientOptions.ClusterName);
+ }
+}
+
+NApi::IConnectionPtr THedgingExecutor::GetConnection()
+{
+ return Clients_[0].Client->GetConnection();
+}
+
+void THedgingExecutor::OnFinishRequest(
+ size_t clientIndex,
+ TDuration effectivePenalty,
+ NProfiling::TCpuDuration adaptivePenalty,
+ NProfiling::TCpuDuration externalPenalty,
+ NProfiling::TCpuInstant start,
+ const TError& error)
+{
+ auto& clientInfo = Clients_[clientIndex];
+ if (error.IsOK()) {
+ if (adaptivePenalty) {
+ TGuard guard(SpinLock_);
+ clientInfo.BanUntil = Max<NProfiling::TCpuInstant>();
+ clientInfo.AdaptivePenalty = 0;
+ }
+ clientInfo.Counter->SuccessRequestCount.Increment();
+ clientInfo.Counter->RequestDuration.Record(NProfiling::CpuDurationToDuration(NProfiling::GetCpuInstant() - start));
+ } else if (effectivePenalty && (error.GetCode() == EErrorCode::Canceled || error.GetCode() == EErrorCode::FutureCombinerShortcut)) {
+ clientInfo.Counter->CancelRequestCount.Increment();
+ } else {
+ with_lock (SpinLock_) {
+ clientInfo.BanUntil = NProfiling::GetCpuInstant() + BanDuration_;
+ clientInfo.AdaptivePenalty += BanPenalty_;
+ }
+ clientInfo.Counter->ErrorRequestCount.Increment();
+ YT_LOG_WARNING("client#%v failed with error %v", clientIndex, error);
+ }
+ clientInfo.Counter->EffectivePenalty.Update(effectivePenalty);
+ clientInfo.Counter->ExternalPenalty.Update(NProfiling::CpuDurationToDuration(externalPenalty));
+}
+
+THedgingExecutor::TEntry::TEntry(NApi::IClientPtr client,
+ NProfiling::TCpuDuration initialPenalty,
+ TCounterPtr counter,
+ TString clusterName)
+ : Client(std::move(client))
+ , ClusterName(std::move(clusterName))
+ , AdaptivePenalty(0)
+ , InitialPenalty(initialPenalty)
+ , ExternalPenalty(0)
+ , BanUntil(Max<NProfiling::TCpuInstant>())
+ , Counter(std::move(counter))
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/hedging_executor.h b/yt/yt/client/hedging/hedging_executor.h
new file mode 100644
index 0000000000..6c29a111f7
--- /dev/null
+++ b/yt/yt/client/hedging/hedging_executor.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include "config.h"
+#include "counter.h"
+#include "penalty_provider.h"
+#include "public.h"
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <library/cpp/iterator/enumerate.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/string.h>
+#include <util/generic/vector.h>
+
+#include <util/system/spinlock.h>
+
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THedgingExecutor)
+
+class THedgingExecutor final
+{
+public:
+ THedgingExecutor(const THedgingClientOptions& options, const IPenaltyProviderPtr& penaltyProvider);
+
+ NApi::IConnectionPtr GetConnection();
+
+ template <typename T>
+ TFuture<T> DoWithHedging(TCallback<TFuture<T>(NApi::IClientPtr)> callback)
+ {
+ auto now = NProfiling::GetCpuInstant();
+ auto clients = [&] {
+ TGuard guard(SpinLock_);
+ for (auto& client : Clients_) {
+ if (client.BanUntil < now) {
+ client.AdaptivePenalty = 0;
+ }
+ }
+
+ return Clients_;
+ }();
+
+ NProfiling::TCpuDuration minInitialPenalty = Max<i64>();
+ for (auto& client : clients) {
+ client.ExternalPenalty = PenaltyProvider_->Get(client.ClusterName);
+ NProfiling::TCpuDuration currentInitialPenalty = client.InitialPenalty + client.AdaptivePenalty + client.ExternalPenalty;
+ minInitialPenalty = Min(minInitialPenalty, currentInitialPenalty);
+ }
+
+ TVector<TFuture<T>> futures(Reserve(clients.size()));
+ for (auto [i, client] : Enumerate(clients)) {
+ TDuration effectivePenalty = NProfiling::CpuDurationToDuration(client.InitialPenalty + client.AdaptivePenalty + client.ExternalPenalty - minInitialPenalty);
+ if (effectivePenalty) {
+ auto delayedFuture = NConcurrency::TDelayedExecutor::MakeDelayed(effectivePenalty, NYT::NRpc::TDispatcher::Get()->GetHeavyInvoker());
+ futures.push_back(delayedFuture.Apply(BIND(callback, client.Client)));
+ } else {
+ futures.push_back(callback(client.Client));
+ }
+ futures.back().Subscribe(BIND(&THedgingExecutor::OnFinishRequest, MakeWeak(this), i, effectivePenalty, client.AdaptivePenalty, client.ExternalPenalty, now));
+ }
+
+ return AnySucceeded(std::move(futures));
+ }
+
+private:
+ void OnFinishRequest(
+ size_t clientIndex,
+ TDuration effectivePenalty,
+ NProfiling::TCpuDuration adaptivePenalty,
+ NProfiling::TCpuDuration externalPenalty,
+ NProfiling::TCpuInstant start,
+ const TError& r);
+
+ struct TEntry
+ {
+ TEntry(NApi::IClientPtr client,
+ NProfiling::TCpuDuration initialPenalty,
+ TCounterPtr counter,
+ TString clusterName);
+
+ NApi::IClientPtr Client;
+ TString ClusterName;
+ NProfiling::TCpuDuration AdaptivePenalty;
+ NProfiling::TCpuDuration InitialPenalty;
+ NProfiling::TCpuDuration ExternalPenalty;
+ NProfiling::TCpuInstant BanUntil;
+ TCounterPtr Counter;
+ };
+
+ TVector<TEntry> Clients_;
+ NProfiling::TCpuDuration BanPenalty_;
+ NProfiling::TCpuDuration BanDuration_;
+ IPenaltyProviderPtr PenaltyProvider_;
+ TSpinLock SpinLock_;
+};
+
+DEFINE_REFCOUNTED_TYPE(THedgingExecutor)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/logger.cpp b/yt/yt/client/hedging/logger.cpp
new file mode 100644
index 0000000000..97dd0f6fb4
--- /dev/null
+++ b/yt/yt/client/hedging/logger.cpp
@@ -0,0 +1,11 @@
+#include "logger.h"
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const NLogging::TLogger Logger("TYtHedgingClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/logger.h b/yt/yt/client/hedging/logger.h
new file mode 100644
index 0000000000..d4771e3331
--- /dev/null
+++ b/yt/yt/client/hedging/logger.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const NLogging::TLogger Logger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/options.cpp b/yt/yt/client/hedging/options.cpp
new file mode 100644
index 0000000000..f2f7cb772d
--- /dev/null
+++ b/yt/yt/client/hedging/options.cpp
@@ -0,0 +1,48 @@
+#include "options.h"
+
+#include <util/folder/dirut.h>
+#include <util/folder/path.h>
+
+#include <util/stream/file.h>
+#include <util/string/strip.h>
+
+#include <util/system/env.h>
+
+namespace NYT::NClient::NHedging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NApi::TClientOptions GetClientOpsFromEnv()
+{
+ NApi::TClientOptions options;
+
+ auto user = Strip(GetEnv("YT_USER"));
+ if (!user.empty()) {
+ options.User = user;
+ }
+
+ auto token = Strip(GetEnv("YT_TOKEN"));
+ if (!token.empty()) {
+ options.Token = token;
+ } else {
+ auto tokenPath = Strip(GetEnv("YT_TOKEN_PATH"));
+ if (tokenPath.empty()) {
+ tokenPath = GetHomeDir() + "/.yt/token";
+ }
+ TFsPath path(tokenPath);
+ if (path.IsFile()) {
+ options.Token = Strip(TIFStream(path).ReadAll());
+ }
+ }
+ return options;
+}
+
+const NApi::TClientOptions& GetClientOpsFromEnvStatic()
+{
+ static const NApi::TClientOptions options = GetClientOpsFromEnv();
+ return options;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging
diff --git a/yt/yt/client/hedging/options.h b/yt/yt/client/hedging/options.h
new file mode 100644
index 0000000000..a80d378792
--- /dev/null
+++ b/yt/yt/client/hedging/options.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <yt/yt/client/api/connection.h>
+
+namespace NYT::NClient::NHedging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fill client options from environment variable (client options is permanent for whole lifecycle of program).
+// UserName is extracted from YT_USER env variable or uses current system username.
+// Token is extracted from YT_TOKEN env variable or from file `~/.yt/token`.
+NApi::TClientOptions GetClientOpsFromEnv();
+
+//! Resolves options only once per launch and then returns the cached result.
+const NApi::TClientOptions& GetClientOpsFromEnvStatic();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging
diff --git a/yt/yt/client/hedging/penalty_provider.cpp b/yt/yt/client/hedging/penalty_provider.cpp
new file mode 100644
index 0000000000..b610f7f877
--- /dev/null
+++ b/yt/yt/client/hedging/penalty_provider.cpp
@@ -0,0 +1,230 @@
+#include "penalty_provider.h"
+
+#include "counter.h"
+#include "logger.h"
+#include "public.h"
+
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/xrange.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDummyLagProvider: public IPenaltyProvider {
+public:
+ NProfiling::TCpuDuration Get(const TString&) override {
+ return 0;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLagPenaltyProvider
+ : public IPenaltyProvider
+{
+public:
+ TLagPenaltyProvider(const TReplicationLagPenaltyProviderConfig& config, NApi::IClientPtr client)
+ : TablePath_(config.GetTablePath())
+ , MaxTabletLag_(TDuration::Seconds(config.GetMaxTabletLag()))
+ , LagPenalty_(NProfiling::DurationToCpuDuration(TDuration::MilliSeconds(config.GetLagPenalty())))
+ , MaxTabletsWithLagFraction_(config.GetMaxTabletsWithLagFraction())
+ , Client_(client)
+ , ClearPenaltiesOnErrors_(config.GetClearPenaltiesOnErrors())
+ , Counters_(New<TLagPenaltyProviderCounters>(TablePath_,
+ TVector<TString>{config.GetReplicaClusters().begin(), config.GetReplicaClusters().end()}))
+ , Executor_(New<NConcurrency::TPeriodicExecutor>(
+ NYT::NRpc::TDispatcher::Get()->GetLightInvoker(),
+ BIND(&TLagPenaltyProvider::UpdateCurrentLagPenalty, MakeWeak(this)),
+ TDuration::Seconds(config.GetCheckPeriod())))
+ {
+ Y_ENSURE(Executor_);
+ Y_ENSURE(Client_);
+
+ for (const auto& cluster : config.GetReplicaClusters()) {
+ auto [_, inserted] = ReplicaClusters_.try_emplace(cluster);
+ Y_ENSURE(inserted, "Replica cluster " << cluster << " is listed twice");
+ }
+
+ GetNodeOptions_.Timeout = TDuration::Seconds(5);
+ GetNodeOptions_.ReadFrom = NApi::EMasterChannelKind::Cache;
+
+ Executor_->Start();
+ }
+
+ // Checks that all ReplicaIds in ReplicaClusters_ have been set.
+ TError CheckAllReplicaIdsPresent() const
+ {
+ for (const auto& [cluster, info] : ReplicaClusters_) {
+ if (!info.ReplicaId) {
+ return TError{"ReplicaId was not found for %v", cluster};
+ }
+ }
+ return {};
+ }
+
+ // Fills ReplicaIds in ReplicaClusters_.
+ void UpdateReplicaIds()
+ {
+ auto replicasNode = NYTree::ConvertToNode(NConcurrency::WaitFor(Client_->GetNode(TablePath_ + "/@replicas", GetNodeOptions_)).ValueOrThrow())->AsMap();
+
+ for (const auto& row : replicasNode->GetChildren()) {
+ TString cluster = row.second->AsMap()->GetChildOrThrow("cluster_name")->AsString()->GetValue();
+ if (auto* info = ReplicaClusters_.FindPtr(cluster)) {
+ info->ReplicaId = NTabletClient::TTableReplicaId::FromString(row.first);
+ YT_LOG_INFO("Found ReplicaId %v for table %v in cluster %v", info->ReplicaId, TablePath_, cluster);
+ };
+ }
+ CheckAllReplicaIdsPresent().ThrowOnError();
+ }
+
+ ui64 GetTotalNumberOfTablets()
+ {
+ return NYTree::ConvertTo<ui64>(NConcurrency::WaitFor(Client_->GetNode(TablePath_ + "/@tablet_count", GetNodeOptions_)).ValueOrThrow());
+ }
+
+ // Returns a map: ReplicaId -> # of tablets.
+ THashMap<NTabletClient::TTableReplicaId, ui64> CalculateNumbersOfTabletsWithLag(const ui64 tabletsCount)
+ {
+ auto tabletsRange = xrange(tabletsCount);
+ auto tabletsInfo = NConcurrency::WaitFor(Client_->GetTabletInfos(TablePath_, {tabletsRange.begin(), tabletsRange.end()})).ValueOrThrow();
+
+ const auto now = TInstant::Now();
+ THashMap<NTabletClient::TTableReplicaId, ui64> tabletsWithLag;
+
+ for (const auto& tabletInfo : tabletsInfo) {
+ if (!tabletInfo.TableReplicaInfos) {
+ continue;
+ }
+
+ for (const auto& replicaInfo : *tabletInfo.TableReplicaInfos) {
+ auto lastReplicationTimestamp = TInstant::Seconds(NTransactionClient::UnixTimeFromTimestamp(replicaInfo.LastReplicationTimestamp));
+ if (now - lastReplicationTimestamp > MaxTabletLag_) {
+ ++tabletsWithLag[replicaInfo.ReplicaId];
+ }
+ }
+ }
+
+ return tabletsWithLag;
+ }
+
+ NProfiling::TCpuDuration CalculateLagPenalty(const ui64 tabletsCount, const ui64 tabletsWithLag)
+ {
+ return tabletsWithLag >= tabletsCount * MaxTabletsWithLagFraction_ ? LagPenalty_ : 0;
+ }
+
+ void UpdateCurrentLagPenalty()
+ {
+ try {
+ YT_LOG_INFO("Start penalty updater check for: %v", TablePath_);
+
+ if (!CheckAllReplicaIdsPresent().IsOK()) {
+ UpdateReplicaIds();
+ }
+
+ auto tabletsCount = GetTotalNumberOfTablets();
+ auto tabletsWithLag = CalculateNumbersOfTabletsWithLag(tabletsCount);
+
+ Counters_->TotalTabletsCount.Update(tabletsCount);
+
+ for (auto& [cluster, info] : ReplicaClusters_) {
+ Y_ASSERT(info.ReplicaId);
+ auto curTabletsWithLag = tabletsWithLag.Value(info.ReplicaId, 0);
+ NProfiling::TCpuDuration newLagPenalty = CalculateLagPenalty(tabletsCount, curTabletsWithLag);
+ info.CurrentLagPenalty.store(newLagPenalty, std::memory_order::relaxed);
+
+ Counters_->LagTabletsCount.at(cluster).Update(curTabletsWithLag);
+ YT_LOG_INFO(
+ "Finish penalty updater check (%v: %v/%v tablets lagging => penalty %v ms) for: %v",
+ cluster, curTabletsWithLag, tabletsCount,
+ NProfiling::CpuDurationToDuration(newLagPenalty).MilliSeconds(),
+ TablePath_
+ );
+ }
+
+ Counters_->SuccessRequestCount.Increment();
+ } catch (const std::exception& err) {
+ Counters_->ErrorRequestCount.Increment();
+
+ YT_LOG_ERROR("Lag penalty updater for %v failed: %v", TablePath_, err.what());
+
+ if (ClearPenaltiesOnErrors_) {
+ for (auto& [cluster, info] : ReplicaClusters_) {
+ info.CurrentLagPenalty.store(0, std::memory_order::relaxed);
+ YT_LOG_INFO("Clearing penalty for cluster %v and table %v", cluster, TablePath_);
+ }
+ }
+ }
+ }
+
+ NProfiling::TCpuDuration Get(const TString& cluster) override
+ {
+ if (const TReplicaInfo* info = ReplicaClusters_.FindPtr(cluster)) {
+ return info->CurrentLagPenalty.load(std::memory_order::relaxed);
+ }
+ return 0;
+ }
+
+ ~TLagPenaltyProvider()
+ {
+ YT_UNUSED_FUTURE(Executor_->Stop());
+ }
+
+private:
+ struct TReplicaInfo
+ {
+ NTabletClient::TTableReplicaId ReplicaId = {};
+ std::atomic<NProfiling::TCpuDuration> CurrentLagPenalty = 0;
+ };
+
+ const TString TablePath_;
+ THashMap<TString, TReplicaInfo> ReplicaClusters_;
+ const TDuration MaxTabletLag_;
+ const NProfiling::TCpuDuration LagPenalty_;
+ const float MaxTabletsWithLagFraction_;
+ NApi::IClientPtr Client_;
+ const bool ClearPenaltiesOnErrors_;
+ TLagPenaltyProviderCountersPtr Counters_;
+ NApi::TGetNodeOptions GetNodeOptions_;
+ NConcurrency::TPeriodicExecutorPtr Executor_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPenaltyProviderPtr CreateDummyPenaltyProvider()
+{
+ return New<TDummyLagProvider>();
+}
+
+IPenaltyProviderPtr CreateReplicationLagPenaltyProvider(
+ const TReplicationLagPenaltyProviderConfig& config,
+ NApi::IClientPtr client)
+{
+ return New<TLagPenaltyProvider>(config, client);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/penalty_provider.h b/yt/yt/client/hedging/penalty_provider.h
new file mode 100644
index 0000000000..04b1cea826
--- /dev/null
+++ b/yt/yt/client/hedging/penalty_provider.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+
+// @brief IPenaltyProvider interface is used in HedgingClient to provide external penalties for different clusters.
+// Current implementations are DummyPenaltyProvider and ReplicationLagPenaltyProvider.
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(IPenaltyProvider)
+
+class IPenaltyProvider
+ : public TRefCounted
+{
+public:
+ virtual NProfiling::TCpuDuration Get(const TString& cluster) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPenaltyProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// @brief DummyPenaltyProvider - always returns 0.
+IPenaltyProviderPtr CreateDummyPenaltyProvider();
+
+// From config.proto.
+class TReplicationLagPenaltyProviderConfig;
+
+// @brief ReplicationLagPenaltyProvider - periodically checks replication lag for given table AND replica cluster.
+// Based on values from TReplicationLagPenaltyProviderConfig add current number of tablets with lag, it either returns 0 or LagPenalty value.
+// Master client - main cluster with replicated table. ReplicaCluster + TablePath specifies concrete replica for table from main cluster.
+IPenaltyProviderPtr CreateReplicationLagPenaltyProvider(
+ const TReplicationLagPenaltyProviderConfig& config, NApi::IClientPtr client);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/public.h b/yt/yt/client/hedging/public.h
new file mode 100644
index 0000000000..ea6c577d56
--- /dev/null
+++ b/yt/yt/client/hedging/public.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <yt/yt/client/api/public.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TCounter)
+DECLARE_REFCOUNTED_STRUCT(TLagPenaltyProviderCounters)
+DECLARE_REFCOUNTED_CLASS(IClientsCache)
+
+DECLARE_REFCOUNTED_STRUCT(TClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/rpc.cpp b/yt/yt/client/hedging/rpc.cpp
new file mode 100644
index 0000000000..1aad3dc908
--- /dev/null
+++ b/yt/yt/client/hedging/rpc.cpp
@@ -0,0 +1,156 @@
+#include "rpc.h"
+#include "options.h"
+
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <yt/yt/client/api/client.h>
+
+#include <yt/yt/client/api/rpc_proxy/config.h>
+#include <yt/yt/client/api/rpc_proxy/connection.h>
+
+#include <util/string/strip.h>
+
+#include <util/system/env.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+NCompression::ECodec GetResponseCodecFromProto(const ECompressionCodec& protoCodec)
+{
+ switch (protoCodec) {
+ case ECompressionCodec::None:
+ return NCompression::ECodec::None;
+ case ECompressionCodec::Lz4:
+ return NCompression::ECodec::Lz4;
+ }
+ Y_UNREACHABLE();
+}
+
+NApi::NRpcProxy::TConnectionConfigPtr GetConnectionConfig(const TConfig& config)
+{
+ auto connectionConfig = New<NApi::NRpcProxy::TConnectionConfig>();
+ connectionConfig->SetDefaults();
+
+ connectionConfig->ClusterUrl = config.GetClusterName();
+ if (!config.GetProxyRole().empty()) {
+ connectionConfig->ProxyRole = config.GetProxyRole();
+ }
+ if (0 != config.GetChannelPoolSize()) {
+ connectionConfig->DynamicChannelPool->MaxPeerCount = config.GetChannelPoolSize();
+ }
+ if (0 != config.GetChannelPoolRebalanceIntervalSeconds()) {
+ connectionConfig->DynamicChannelPool->RandomPeerEvictionPeriod = TDuration::Seconds(config.GetChannelPoolRebalanceIntervalSeconds());
+ }
+ if (0 != config.GetModifyRowsBatchCapacity()) {
+ connectionConfig->ModifyRowsBatchCapacity = config.GetModifyRowsBatchCapacity();
+ }
+
+#define SET_TIMEOUT_OPTION(name) \
+ if (0 != config.Get##name()) connectionConfig->name = TDuration::MilliSeconds(config.Get ## name())
+
+ SET_TIMEOUT_OPTION(DefaultTransactionTimeout);
+ SET_TIMEOUT_OPTION(DefaultSelectRowsTimeout);
+ SET_TIMEOUT_OPTION(DefaultLookupRowsTimeout);
+ SET_TIMEOUT_OPTION(DefaultTotalStreamingTimeout);
+ SET_TIMEOUT_OPTION(DefaultStreamingStallTimeout);
+ SET_TIMEOUT_OPTION(DefaultPingPeriod);
+
+#undef SET_TIMEOUT_OPTION
+
+ connectionConfig->ResponseCodec = GetResponseCodecFromProto(config.GetResponseCodec());
+ connectionConfig->EnableRetries = config.GetEnableRetries();
+
+ if (config.HasRetryBackoffTime()) {
+ connectionConfig->RetryingChannel->RetryBackoffTime = TDuration::MilliSeconds(config.GetRetryBackoffTime());
+ }
+ if (config.HasRetryAttempts()) {
+ connectionConfig->RetryingChannel->RetryAttempts = config.GetRetryAttempts();
+ }
+ if (config.HasRetryTimeout()) {
+ connectionConfig->RetryingChannel->RetryTimeout = TDuration::MilliSeconds(config.GetRetryTimeout());
+ }
+
+ connectionConfig->Postprocess();
+
+ return connectionConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::pair<TStringBuf, TStringBuf> ExtractClusterAndProxyRole(TStringBuf clusterUrl)
+{
+ TStringBuf cluster;
+ TStringBuf proxyRole;
+ clusterUrl.Split('/', cluster, proxyRole);
+ return {cluster, proxyRole};
+}
+
+void SetClusterUrl(const NApi::NRpcProxy::TConnectionConfigPtr& config, TStringBuf clusterUrl)
+{
+ auto [cluster, proxyRole] = ExtractClusterAndProxyRole(clusterUrl);
+ if (!proxyRole.empty()) {
+ Y_ENSURE(!config->ProxyRole || config->ProxyRole.value().empty(), "ProxyRole specified in both: config and url");
+ config->ProxyRole = ToString(proxyRole);
+ }
+ config->ClusterUrl = ToString(cluster);
+}
+
+void SetClusterUrl(TConfig& config, TStringBuf clusterUrl)
+{
+ auto [cluster, proxyRole] = ExtractClusterAndProxyRole(clusterUrl);
+ if (!proxyRole.empty()) {
+ Y_ENSURE(config.GetProxyRole().empty(), "ProxyRole specified in both: config and url");
+ config.SetProxyRole(ToString(proxyRole));
+ }
+ config.SetClusterName(ToString(cluster));
+}
+
+NApi::IClientPtr CreateClient(const NApi::NRpcProxy::TConnectionConfigPtr& config, const NApi::TClientOptions& options)
+{
+ return NApi::NRpcProxy::CreateConnection(config)->CreateClient(options);
+}
+
+NApi::IClientPtr CreateClient(const TConfig& config, const NApi::TClientOptions& options)
+{
+ return CreateClient(GetConnectionConfig(config), options);
+}
+
+NApi::IClientPtr CreateClient(const NApi::NRpcProxy::TConnectionConfigPtr& config)
+{
+ return CreateClient(config, GetClientOpsFromEnvStatic());
+}
+
+NApi::IClientPtr CreateClient(const TConfig& config)
+{
+ return CreateClient(GetConnectionConfig(config));
+}
+
+NApi::IClientPtr CreateClient(TStringBuf clusterUrl)
+{
+ return CreateClient(clusterUrl, GetClientOpsFromEnvStatic());
+}
+
+NApi::IClientPtr CreateClient(TStringBuf cluster, TStringBuf proxyRole)
+{
+ auto config = New<NApi::NRpcProxy::TConnectionConfig>();
+ config->ClusterUrl = ToString(cluster);
+ if (!proxyRole.empty()) {
+ config->ProxyRole = ToString(proxyRole);
+ }
+ return CreateClient(config);
+}
+
+NApi::IClientPtr CreateClient()
+{
+ return CreateClient(Strip(GetEnv("YT_PROXY")));
+}
+
+NApi::IClientPtr CreateClient(TStringBuf clusterUrl, const NApi::TClientOptions& options)
+{
+ TConfig config;
+ SetClusterUrl(config, clusterUrl);
+ return CreateClient(config, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/rpc.h b/yt/yt/client/hedging/rpc.h
new file mode 100644
index 0000000000..102e6b6791
--- /dev/null
+++ b/yt/yt/client/hedging/rpc.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <yt/yt/client/api/public.h>
+#include <yt/yt/client/api/rpc_proxy/config.h>
+
+#include <util/generic/strbuf.h>
+
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConfig;
+
+NApi::NRpcProxy::TConnectionConfigPtr GetConnectionConfig(const TConfig& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Helper function to extract ClusterName and ProxyRole from clusterUrl.
+std::pair<TStringBuf, TStringBuf> ExtractClusterAndProxyRole(TStringBuf clusterUrl);
+
+//! Helper function to properly set ClusterName and ProxyRole from clusterUrl.
+// Expected that clusterUrl will be in format cluster[/proxyRole].
+void SetClusterUrl(const NApi::NRpcProxy::TConnectionConfigPtr& config, TStringBuf clusterUrl);
+void SetClusterUrl(TConfig& config, TStringBuf clusterUrl);
+
+NApi::IClientPtr CreateClient(const NApi::NRpcProxy::TConnectionConfigPtr& config, const NApi::TClientOptions& options);
+NApi::IClientPtr CreateClient(const TConfig& config, const NApi::TClientOptions& options);
+
+//! Shortcut to create rpc client with options from env variables
+NApi::IClientPtr CreateClient(const NApi::NRpcProxy::TConnectionConfigPtr& config);
+NApi::IClientPtr CreateClient(const TConfig& config);
+
+//! Shortcut to create client to specified cluster.
+// `clusterUrl` may have format cluster[/proxyRole],
+// where cluster may be short name of yt cluster (markov, hahn) or ip address + port.
+NApi::IClientPtr CreateClient(TStringBuf clusterUrl);
+
+//! Allows to specify proxyRole as dedicated option.
+NApi::IClientPtr CreateClient(TStringBuf cluster, TStringBuf proxyRole);
+
+//! Shortcut to create client with default config and options from env variables (use env:YT_PROXY as serverName).
+NApi::IClientPtr CreateClient();
+
+//! Shortcut to create client to cluster with custom options.
+NApi::IClientPtr CreateClient(TStringBuf clusterUrl, const NApi::TClientOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/cache_ut.cpp b/yt/yt/client/hedging/unittests/cache_ut.cpp
new file mode 100644
index 0000000000..25f61e7a2d
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/cache_ut.cpp
@@ -0,0 +1,110 @@
+#include <yt/yt/client/hedging/cache.h>
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/string/format.h>
+
+#include <util/generic/vector.h>
+
+#include <util/system/env.h>
+
+#include <thread>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// YT does not create physical connection immediately, so try to use this fact to create connection to non existence server.
+TEST(TClientsCacheTest, GetSameClient)
+{
+ SetEnv("YT_TOKEN", "AAAA-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ auto cache = CreateClientsCache();
+ auto client1 = cache->GetClient("localhost");
+ auto client2 = cache->GetClient("localhost");
+ EXPECT_TRUE(client1 == client2);
+}
+
+TEST(TClientsCacheTest, GetClientWithProxyRole)
+{
+ SetEnv("YT_TOKEN", "AAAA-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ auto cache = CreateClientsCache();
+ auto client1 = cache->GetClient("bigb@localhost");
+ auto client2 = cache->GetClient("localhost");
+ EXPECT_TRUE(client1 != client2);
+}
+
+TEST(TClientsCacheTest, MultiThreads)
+{
+ SetEnv("YT_TOKEN", "AAAA-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ auto cache = CreateClientsCache();
+ TVector<std::thread> threads(Reserve(10));
+ TVector<NApi::IClientPtr> clients(threads.capacity());
+ TVector<size_t> collisions(threads.capacity());
+
+ for (size_t i = 0 ; i < threads.capacity(); ++i) {
+ threads.emplace_back([=, &clients, &collisions] () {
+ try {
+ for (size_t j = 0; j < 1000; ++j) {
+ auto client = cache->GetClient(Format("localhost:6000%v", i));
+ if (client != clients[i]) {
+ clients[i] = client;
+ ++collisions[i];
+ }
+ }
+ } catch (...) {
+ collisions[i] = 100500; // exception marker
+ }
+ });
+ }
+
+ for (auto& t : threads) {
+ t.join();
+ }
+ for (const auto& client : clients) {
+ EXPECT_TRUE(client);
+ }
+ for (auto collision : collisions) {
+ EXPECT_EQ(1u, collision);
+ }
+}
+
+TEST(TClientsCacheTest, MakeClusterConfig) {
+ TClustersConfig clustersCfg;
+ clustersCfg.MutableDefaultConfig()->SetClusterName("seneca-nan"); // will be ignored
+ clustersCfg.MutableDefaultConfig()->SetProxyRole("default_role"); // can be overwritten
+ clustersCfg.MutableDefaultConfig()->SetChannelPoolSize(42u);
+ auto& senecaVlaCfg = (*clustersCfg.MutableClusterConfigs())["seneca-vla"];
+ senecaVlaCfg.SetClusterName(""); // will be ignored
+ senecaVlaCfg.SetProxyRole("seneca_vla_role"); // can be overwritten
+ senecaVlaCfg.SetChannelPoolSize(43u);
+
+ {
+ auto cfg = MakeClusterConfig(clustersCfg, "seneca-man");
+ EXPECT_EQ(cfg.GetClusterName(), "seneca-man");
+ EXPECT_EQ(cfg.GetProxyRole(), "default_role");
+ EXPECT_EQ(cfg.GetChannelPoolSize(), 42u);
+ }
+ {
+ auto cfg = MakeClusterConfig(clustersCfg, "seneca-man/overwriting_role");
+ EXPECT_EQ(cfg.GetClusterName(), "seneca-man");
+ EXPECT_EQ(cfg.GetProxyRole(), "overwriting_role");
+ EXPECT_EQ(cfg.GetChannelPoolSize(), 42u);
+ }
+ {
+ auto cfg = MakeClusterConfig(clustersCfg, "seneca-vla");
+ EXPECT_EQ(cfg.GetClusterName(), "seneca-vla");
+ EXPECT_EQ(cfg.GetProxyRole(), "seneca_vla_role");
+ EXPECT_EQ(cfg.GetChannelPoolSize(), 43u);
+ }
+ {
+ auto cfg = MakeClusterConfig(clustersCfg, "seneca-vla/overwriting_role");
+ EXPECT_EQ(cfg.GetClusterName(), "seneca-vla");
+ EXPECT_EQ(cfg.GetProxyRole(), "overwriting_role");
+ EXPECT_EQ(cfg.GetChannelPoolSize(), 43u);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/counters_ut.cpp b/yt/yt/client/hedging/unittests/counters_ut.cpp
new file mode 100644
index 0000000000..e6dc5eeb48
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/counters_ut.cpp
@@ -0,0 +1,241 @@
+#include <yt/yt/client/hedging/counter.h>
+#include <yt/yt/client/hedging/hedging.h>
+
+#include <yt/yt/client/unittests/mock/client.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/testing.h>
+#include <yt/yt/library/profiling/solomon/registry.h>
+
+#include <library/cpp/iterator/zip.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <util/generic/vector.h>
+
+#include <util/string/join.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::StrictMock;
+
+using namespace NYT::NProfiling;
+
+using TStrictMockClient = StrictMock<NApi::TMockClient>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+const auto SleepQuantum = TDuration::MilliSeconds(100);
+
+#define EXPECT_DURATION_NEAR(a, b) EXPECT_NEAR(a.MilliSeconds(), b.MilliSeconds(), 1)
+
+NApi::IClientPtr CreateTestHedgingClient(std::initializer_list<NApi::IClientPtr> clients,
+ std::initializer_list<TCounterPtr> counters,
+ TDuration banDuration = SleepQuantum * 5)
+{
+ THedgingClientOptions options;
+ options.BanPenalty = SleepQuantum * 2;
+ options.BanDuration = banDuration;
+ std::initializer_list<TDuration> initialPenalties = {TDuration::Zero(), SleepQuantum};
+
+ for (auto [client, initialPenalty, counter] : Zip(clients, initialPenalties, counters)) {
+ options.Clients.emplace_back(client, initialPenalty, counter);
+ }
+ return CreateHedgingClient(options);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THedgingClientCountersTest, CountersAfterSuccessFromFirstClient)
+{
+ NYPath::TYPath path = "/test/1234";
+ NApi::TListNodeOptions options;
+ options.Attributes = {"some_attribute"};
+
+ NYson::TYsonString firstClientResult(TStringBuf("FirstClientData"));
+
+ auto firstMockClient = New<TStrictMockClient>();
+ auto secondMockClient = New<TStrictMockClient>();
+
+ EXPECT_CALL(*firstMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(firstClientResult)));
+ EXPECT_CALL(*secondMockClient, ListNode(path, _)).Times(0);
+
+ auto solomon = New<TSolomonRegistry>();
+ TRegistry registry(solomon, "/d");
+
+ auto firstClientCounter = New<TCounter>(registry.WithTag("c", "first"));
+ auto secondClientCounter = New<TCounter>(registry.WithTag("c", "first"));
+
+ auto client = CreateTestHedgingClient({firstMockClient, secondMockClient},
+ {firstClientCounter, secondClientCounter});
+ auto queryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ // Wait for cancelled request finish.
+ Sleep(SleepQuantum);
+
+ // Success result from first client with effective initial penalty equals to 0 ms
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->SuccessRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->ErrorRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(TDuration::Zero(), TTesting::ReadTimeGauge(firstClientCounter->EffectivePenalty));
+
+ // Cancel result from second client with effective initial penalty increment to ban duration
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->SuccessRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->ErrorRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(secondClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(SleepQuantum, TTesting::ReadTimeGauge(secondClientCounter->EffectivePenalty));
+}
+
+TEST(THedgingClientCountersTest, CountersAfterFirstClientHasFailed)
+{
+ NYPath::TYPath path = "/test/1234";
+ NApi::TListNodeOptions options;
+ options.Attributes = {"some_attribute"};
+
+ NYson::TYsonString firstClientResult(TStringBuf("FirstClientData"));
+ NYson::TYsonString secondClientResult(TStringBuf("SecondClientData"));
+
+ auto firstMockClient = New<TStrictMockClient>();
+ auto secondMockClient = New<TStrictMockClient>();
+
+ EXPECT_CALL(*firstMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+ EXPECT_CALL(*secondMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(secondClientResult)));
+
+ auto solomon = New<TSolomonRegistry>();
+ TRegistry registry(solomon, "/d");
+
+ auto firstClientCounter = New<TCounter>(registry.WithTag("c", "first"));
+ auto secondClientCounter = New<TCounter>(registry.WithTag("c", "second"));
+
+ auto client = CreateTestHedgingClient({firstMockClient, secondMockClient},
+ {firstClientCounter, secondClientCounter},
+ TDuration::Seconds(5));
+ auto queryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ // Error result from first client with effective initial penalty equals to 0 ms
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->SuccessRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->ErrorRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(TDuration::Zero(), TTesting::ReadTimeGauge(firstClientCounter->EffectivePenalty));
+
+ // Success result from second client with effective initial penalty equals to 8 ms
+ EXPECT_EQ(1, TTesting::ReadCounter(secondClientCounter->SuccessRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->ErrorRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(SleepQuantum, TTesting::ReadTimeGauge(secondClientCounter->EffectivePenalty));
+}
+
+TEST(THedgingClientCountersTest, CountersWhenFirstClientIsBanned)
+{
+ NYPath::TYPath path = "/test/1234";
+ NApi::TListNodeOptions options;
+ options.Attributes = {"some_attribute"};
+
+ NYson::TYsonString firstClientResult(TStringBuf("FirstClientData"));
+ NYson::TYsonString secondClientResult(TStringBuf("SecondClientData"));
+
+ auto firstMockClient = New<TStrictMockClient>();
+ auto secondMockClient = New<TStrictMockClient>();
+
+ EXPECT_CALL(*firstMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+ EXPECT_CALL(*secondMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(secondClientResult)))
+ .WillOnce(Return(MakeFuture(secondClientResult)));
+
+ auto solomon = New<TSolomonRegistry>();
+ solomon->SetWindowSize(12);
+ TRegistry registry(solomon, "/d");
+
+ auto firstClientCounter = New<TCounter>(registry.WithTag("c", "first"));
+ auto secondClientCounter = New<TCounter>(registry.WithTag("c", "second"));
+
+ auto client = CreateTestHedgingClient({firstMockClient, secondMockClient},
+ {firstClientCounter, secondClientCounter});
+ auto firstQueryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ auto secondQueryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ // Wait for cancelled request finish.
+ Sleep(SleepQuantum);
+
+ // Cancel result from banned client with effective initial penalty equals to BanDuration ms
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->SuccessRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->ErrorRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(SleepQuantum, TTesting::ReadTimeGauge(firstClientCounter->EffectivePenalty));
+
+ // Success result from second client with effective initial penalty equals to 0 ms
+ EXPECT_EQ(2, TTesting::ReadCounter(secondClientCounter->SuccessRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->ErrorRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(TDuration::Zero(), TTesting::ReadTimeGauge(secondClientCounter->EffectivePenalty));
+}
+
+TEST(THedgingClientCountersTest, CountersAfterFirstClientBanHasElapsed)
+{
+ NYPath::TYPath path = "/test/1234";
+ NApi::TListNodeOptions options;
+ options.Attributes = {"some_attribute"};
+
+ NYson::TYsonString firstClientResult(TStringBuf("FirstClientData"));
+ NYson::TYsonString secondClientResult(TStringBuf("SecondClientData"));
+
+ auto firstMockClient = New<TStrictMockClient>();
+ auto secondMockClient = New<TStrictMockClient>();
+
+ EXPECT_CALL(*firstMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))))
+ .WillOnce(Return(MakeFuture(firstClientResult)));
+ EXPECT_CALL(*secondMockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(secondClientResult)));
+
+ auto solomon = New<TSolomonRegistry>();
+ solomon->SetWindowSize(12);
+ TRegistry registry(solomon, "/d");
+
+ auto firstClientCounter = New<TCounter>(registry.WithTag("c", "first"));
+ auto secondClientCounter = New<TCounter>(registry.WithTag("c", "second"));
+
+ auto banDuration = SleepQuantum * 2;
+
+ auto client = CreateTestHedgingClient({firstMockClient, secondMockClient},
+ {firstClientCounter, secondClientCounter},
+ banDuration);
+ auto firstQueryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ Sleep(banDuration);
+ auto secondQueryResult = NConcurrency::WaitFor(client->ListNode(path, options));
+
+ // Wait for cancelled request finish.
+ Sleep(SleepQuantum);
+
+ // Success result from first client, after ban time has elapsed, with effective initial penalty equals to 0 ms
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->SuccessRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(firstClientCounter->ErrorRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(firstClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(TDuration::Zero(), TTesting::ReadTimeGauge(firstClientCounter->EffectivePenalty));
+
+ // Cancel result from second client with effective initial penalty equals to 8 ms
+ EXPECT_EQ(1, TTesting::ReadCounter(secondClientCounter->SuccessRequestCount));
+ EXPECT_EQ(0, TTesting::ReadCounter(secondClientCounter->ErrorRequestCount));
+ EXPECT_EQ(1, TTesting::ReadCounter(secondClientCounter->CancelRequestCount));
+ EXPECT_DURATION_NEAR(SleepQuantum, TTesting::ReadTimeGauge(secondClientCounter->EffectivePenalty));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/hedging_ut.cpp b/yt/yt/client/hedging/unittests/hedging_ut.cpp
new file mode 100644
index 0000000000..6362d491b9
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/hedging_ut.cpp
@@ -0,0 +1,533 @@
+#include <yt/yt/client/hedging/cache.h>
+#include <yt/yt/client/hedging/counter.h>
+#include <yt/yt/client/hedging/hedging.h>
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/client/unittests/mock/client.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+
+#include <library/cpp/iterator/zip.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+namespace {
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::StrictMock;
+
+using TStrictMockClient = StrictMock<NApi::TMockClient>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const auto SleepQuantum = TDuration::MilliSeconds(100);
+const auto CheckPeriod = TDuration::Seconds(1);
+
+class TMockClientsCache
+ : public IClientsCache
+{
+public:
+ MOCK_METHOD(NApi::IClientPtr, GetClient, (TStringBuf url), (override));
+};
+
+NApi::IClientPtr CreateTestHedgingClient(
+ TDuration banPenalty,
+ TDuration banDuration,
+ std::initializer_list<NApi::IClientPtr> clients,
+ std::initializer_list<TDuration> initialPenalties = {TDuration::Zero(), SleepQuantum},
+ const IPenaltyProviderPtr& penaltyProvider = CreateDummyPenaltyProvider())
+{
+ THedgingClientOptions options;
+ options.BanPenalty = banPenalty;
+ options.BanDuration = banDuration;
+ size_t clientId = 0;
+ for (auto [client, initialPenalty] : Zip(clients, initialPenalties)) {
+ auto currCliendId = "seneca-" + ToString(++clientId);
+ options.Clients.emplace_back(client, currCliendId, initialPenalty, New<TCounter>(currCliendId));
+ }
+ return CreateHedgingClient(options, penaltyProvider);
+}
+
+IPenaltyProviderPtr CreateReplicationLagPenaltyProvider(
+ const NYPath::TYPath& path,
+ const TString& cluster,
+ TDuration maxTabletLag,
+ TDuration lagPenalty,
+ NApi::IClientPtr client,
+ const bool clearPenaltiesOnErrors = false,
+ const TDuration checkPeriod = CheckPeriod)
+{
+ TReplicationLagPenaltyProviderConfig config;
+
+ config.SetTablePath(path);
+ config.AddReplicaClusters(cluster);
+ config.SetMaxTabletsWithLagFraction(0.5);
+ config.SetMaxTabletLag(maxTabletLag.Seconds());
+ config.SetCheckPeriod(checkPeriod.Seconds());
+ config.SetLagPenalty(lagPenalty.MilliSeconds());
+ config.SetClearPenaltiesOnErrors(clearPenaltiesOnErrors);
+
+ return CreateReplicationLagPenaltyProvider(config, client);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Using LinkNode method for testing because it's return value is YsonString.
+// It makes easier to check from which client result has come from just by comparing corresponding string values.
+TEST(THedgingClientTest, GetResultFromClientWithMinEffectivePenalty)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _)).Times(0);
+
+ auto hedgingClient = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ SleepQuantum,
+ {mockClient1, mockClient2});
+
+ auto queryResult = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that query result is from first client, because it's effective initial penalty is minimal.
+ EXPECT_TRUE(queryResult.IsOK());
+ EXPECT_EQ(queryResult.Value().AsStringBuf(), clientResult1.AsStringBuf());
+}
+
+TEST(THedgingClientTest, GetclientResult2WhenFirstClientHasFailed)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(clientResult2)));
+
+ auto client = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ SleepQuantum * 2,
+ {mockClient1, mockClient2});
+
+ auto queryResult = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that query result is from second client, because first client returned failure and got banned.
+ EXPECT_TRUE(queryResult.IsOK());
+ EXPECT_EQ(queryResult.Value().AsStringBuf(), clientResult2.AsStringBuf());
+}
+
+TEST(THedgingClientTest, GetclientResult1AfterBanTimeHasElapsed)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))))
+ .WillOnce(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(clientResult2)));
+
+ auto banDuration = SleepQuantum * 2;
+ auto hedgingClient = CreateTestHedgingClient(
+ banDuration,
+ banDuration,
+ {mockClient1, mockClient2});
+
+ auto queryResult1 = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that first query result is from second client, because first client returned failure and got banned.
+ EXPECT_TRUE(queryResult1.IsOK());
+ EXPECT_EQ(queryResult1.Value().AsStringBuf(), clientResult2.AsStringBuf());
+
+ NConcurrency::TDelayedExecutor::WaitForDuration(banDuration);
+
+ auto queryResult2 = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that second query result is from first client, because ban time has elapsed and it's effective initial penalty is minimal again.
+ EXPECT_TRUE(queryResult2.IsOK());
+ EXPECT_EQ(queryResult2.Value().AsStringBuf(), clientResult1.AsStringBuf());
+}
+
+TEST(THedgingClientTest, GetclientResult2WhenFirstClientIsBanned)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ auto hedgingClient = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ TDuration::Seconds(2),
+ {mockClient1, mockClient2});
+
+ auto queryResult1 = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that first query result is from second client, because first client returned failure and got banned.
+ EXPECT_TRUE(queryResult1.IsOK());
+ EXPECT_EQ(queryResult1.Value().AsStringBuf(), clientResult2.AsStringBuf());
+
+ auto queryResult2 = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that second query result is from second client, because first client is still banned.
+ EXPECT_TRUE(queryResult2.IsOK());
+ EXPECT_EQ(queryResult2.Value().AsStringBuf(), clientResult2.AsStringBuf());
+}
+
+TEST(THedgingClientTest, GetclientResult2WhenFirstClientIsSleeping)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(NConcurrency::TDelayedExecutor::MakeDelayed(TDuration::Seconds(2)).Apply(BIND([=] () { return clientResult1; }))));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ auto hedgingClient = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ SleepQuantum,
+ {mockClient1, mockClient2});
+
+ auto queryResult = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that query result is from second client, because first client is sleeping.
+ EXPECT_TRUE(queryResult.IsOK());
+ EXPECT_EQ(queryResult.Value().AsStringBuf(), clientResult2.AsStringBuf());
+}
+
+TEST(THedgingClientTest, FirstClientIsBannedBecauseResponseWasCancelled)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(NConcurrency::TDelayedExecutor::MakeDelayed(SleepQuantum * 2).Apply(BIND([=] () { return clientResult1; }))))
+ .WillRepeatedly(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ auto client = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ TDuration::Seconds(2),
+ {mockClient1, mockClient2});
+
+ auto queryResult1 = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that query result is from second client, because first client is sleeping.
+ EXPECT_TRUE(queryResult1.IsOK());
+ EXPECT_EQ(queryResult1.Value().AsStringBuf(), clientResult2.AsStringBuf());
+
+ // Wait for finish of all requests
+ Sleep(SleepQuantum);
+
+ auto queryResult2 = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that second query result is from second client, because first client was cancelled and got banned.
+ EXPECT_TRUE(queryResult2.IsOK());
+ EXPECT_EQ(queryResult2.Value().AsStringBuf(), clientResult2.AsStringBuf());
+}
+
+TEST(THedgingClientTest, AmnestyBanPenaltyIfClientSucceeded)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+ NYson::TYsonString thirdClientResult(TStringBuf("ThirdClientData"));
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+ auto mockClient3 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillOnce(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))))
+ .WillRepeatedly(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(clientResult2)))
+ .WillOnce(Return(NConcurrency::TDelayedExecutor::MakeDelayed(TDuration::Seconds(100)).Apply(BIND([=] () { return clientResult2; }))))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+ EXPECT_CALL(*mockClient3, ListNode(path, _))
+ .WillRepeatedly(Return(NConcurrency::TDelayedExecutor::MakeDelayed(TDuration::Seconds(100)).Apply(BIND([=] () { return thirdClientResult; }))));
+
+ auto client = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ TDuration::Seconds(30),
+ {mockClient1, mockClient2, mockClient3},
+ {TDuration::Zero(), SleepQuantum, SleepQuantum * 2});
+
+ auto queryResult1 = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that query result is from second client, because first client finished with an error.
+ EXPECT_TRUE(queryResult1.IsOK());
+ EXPECT_EQ(queryResult1.Value().AsStringBuf(), clientResult2.AsStringBuf());
+
+ // Wait for finish of all requests
+ Sleep(SleepQuantum * 2);
+
+ auto queryResult2 = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that second query result is from first client, because other clients were sleeping.
+ EXPECT_TRUE(queryResult2.IsOK());
+ EXPECT_EQ(queryResult2.Value().AsStringBuf(), clientResult1.AsStringBuf());
+
+ // Wait for finish of all requests
+ Sleep(SleepQuantum * 2);
+
+ auto queryResult3 = NConcurrency::WaitFor(client->ListNode(path));
+ // Check that third query result is from first client again, because it's penalty was amnestied.
+ EXPECT_TRUE(queryResult3.IsOK());
+ EXPECT_EQ(queryResult3.Value().AsStringBuf(), clientResult1.AsStringBuf());
+}
+
+TEST(THedgingClientTest, MultiThread)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _)).WillRepeatedly([=] (const NYPath::TYPath&, const NApi::TListNodeOptions& options) {
+ if (options.Timeout) {
+ return NConcurrency::TDelayedExecutor::MakeDelayed(*options.Timeout).Apply(BIND([=] () {
+ return clientResult1;
+ }));
+ }
+ return MakeFuture(clientResult1);
+ });
+ EXPECT_CALL(*mockClient2, ListNode(path, _)).WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ auto hedgingClient = CreateTestHedgingClient(
+ TDuration::MilliSeconds(1),
+ SleepQuantum,
+ {mockClient1, mockClient2},
+ {SleepQuantum, SleepQuantum * 3});
+
+ auto threadPool = NConcurrency::CreateThreadPool(10, "test");
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < 100; ++i) {
+ futures.push_back(BIND([=] () {
+ for (int j = 0; j < 100; ++j) {
+ NApi::TListNodeOptions options;
+ // on each 5th request for 1st and 2nd thread, the first client will timeout
+ if (i < 2 && (0 == j % 5)) {
+ options.Timeout = TDuration::Seconds(2);
+ }
+ auto v = NConcurrency::WaitFor(hedgingClient->ListNode(path, options)).ValueOrThrow();
+ if (options.Timeout) {
+ EXPECT_EQ(clientResult2.AsStringBuf(), v.AsStringBuf());
+ } else {
+ EXPECT_EQ(clientResult1.AsStringBuf(), v.AsStringBuf());
+ }
+ }
+ }).AsyncVia(threadPool->GetInvoker()).Run());
+ }
+
+ for (const auto& future : futures) {
+ EXPECT_NO_THROW(future.Get().ThrowOnError());
+ }
+}
+
+TEST(THedgingClientTest, ResponseFromSecondClientWhenFirstHasReplicationLag)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ auto hedgingClient = CreateTestHedgingClient(
+ SleepQuantum * 2,
+ SleepQuantum,
+ {mockClient1, mockClient2});
+
+ auto queryResult = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that query result is from first client, because it's effective initial penalty is minimal.
+ EXPECT_TRUE(queryResult.IsOK());
+ EXPECT_EQ(queryResult.Value().AsStringBuf(), clientResult1.AsStringBuf());
+
+ TString cluster = "seneca-1";
+ auto maxTabletLag = TDuration::Seconds(10);
+ auto lagPenalty = 2 * SleepQuantum;
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-1\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ auto mockClient3 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient3, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*mockClient3, GetNode(path + "/@tablet_count", _))
+ .WillRepeatedly(Return(MakeFuture(tabletCountResult)));
+
+ std::vector<NApi::TTabletInfo> tabletInfos(1);
+ tabletInfos[0].TableReplicaInfos = std::make_optional(std::vector<NApi::TTabletInfo::TTableReplicaInfo>());
+ auto& replicaTabletsInfo = tabletInfos[0].TableReplicaInfos->emplace_back();
+ replicaTabletsInfo.ReplicaId = NTabletClient::TTableReplicaId::FromString("575f-131-40502c5-201b420f");
+ replicaTabletsInfo.LastReplicationTimestamp = NTransactionClient::TimestampFromUnixTime(
+ TInstant::Now().Seconds() - 2 * maxTabletLag.Seconds());
+
+ EXPECT_CALL(*mockClient3, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture(tabletInfos)));
+
+ auto penaltyProvier = CreateReplicationLagPenaltyProvider(
+ path,
+ cluster,
+ maxTabletLag,
+ lagPenalty,
+ mockClient3);
+ Sleep(2 * CheckPeriod);
+
+ auto hedgingClientWithPenaltyProvider = CreateTestHedgingClient(
+ TDuration::MilliSeconds(1),
+ SleepQuantum,
+ {mockClient1, mockClient2},
+ {TDuration::Zero(), SleepQuantum},
+ penaltyProvier);
+
+ auto queryResultWithReplicationLagPolicy = NConcurrency::WaitFor(hedgingClientWithPenaltyProvider->ListNode(path));
+
+ // Check that query result is from second client, because first client received penalty updater because of replication lag.
+ EXPECT_TRUE(queryResultWithReplicationLagPolicy.IsOK());
+ EXPECT_EQ(queryResultWithReplicationLagPolicy.Value().AsStringBuf(), clientResult2.AsStringBuf());
+}
+
+TEST(THedgingClientTest, CreatingHedgingClientWithPreinitializedClients)
+{
+ const TString clusterName = "test_cluster";
+ NYPath::TYPath path = "/test/1234";
+ NYson::TYsonString clientResult(TStringBuf("ClientData"));
+
+ auto mockClient = New<TStrictMockClient>();
+ EXPECT_CALL(*mockClient, ListNode(path, _))
+ .WillOnce(Return(MakeFuture(clientResult)));
+
+ auto mockClientsCache = New<StrictMock<TMockClientsCache>>();
+ EXPECT_CALL(*mockClientsCache, GetClient(clusterName)).WillOnce(Return(mockClient));
+
+ THedgingClientConfig hedgingClientConfig;
+ hedgingClientConfig.SetBanDuration(100);
+ hedgingClientConfig.SetBanPenalty(200);
+ auto clientOptions = hedgingClientConfig.AddClients();
+ clientOptions->SetInitialPenalty(0);
+ clientOptions->MutableClientConfig()->SetClusterName(clusterName);
+
+ auto hedgingClient = CreateHedgingClient(hedgingClientConfig, mockClientsCache);
+
+ auto queryResult = NConcurrency::WaitFor(hedgingClient->ListNode(path));
+ // Check that query result is from preinitialized client.
+ EXPECT_TRUE(queryResult.IsOK());
+ EXPECT_EQ(queryResult.Value().AsStringBuf(), clientResult.AsStringBuf());
+}
+
+TEST(THedgingClientTest, ResponseFromFirstClientWhenReplicationLagUpdaterFails)
+{
+ NYPath::TYPath path = "/test/1234";
+
+ auto mockClient1 = New<TStrictMockClient>();
+ auto mockClient2 = New<TStrictMockClient>();
+ NYson::TYsonString clientResult1(TStringBuf("FirstClientData"));
+ NYson::TYsonString clientResult2(TStringBuf("SecondClientData"));
+
+ EXPECT_CALL(*mockClient1, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult1)));
+ EXPECT_CALL(*mockClient2, ListNode(path, _))
+ .WillRepeatedly(Return(MakeFuture(clientResult2)));
+
+ TString cluster = "seneca-1";
+ auto maxTabletLag = TDuration::Seconds(10);
+ auto lagPenalty = 2 * SleepQuantum;
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-1\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ auto mockClient3 = New<TStrictMockClient>();
+
+ EXPECT_CALL(*mockClient3, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*mockClient3, GetNode(path + "/@tablet_count", _))
+ .WillOnce(Return(MakeFuture(tabletCountResult)))
+ .WillRepeatedly(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+
+ std::vector<NApi::TTabletInfo> tabletInfos(1);
+ tabletInfos[0].TableReplicaInfos = std::make_optional(std::vector<NApi::TTabletInfo::TTableReplicaInfo>());
+ auto& replicaTabletsInfo = tabletInfos[0].TableReplicaInfos->emplace_back();
+ replicaTabletsInfo.ReplicaId = NTabletClient::TTableReplicaId::FromString("575f-131-40502c5-201b420f");
+ replicaTabletsInfo.LastReplicationTimestamp = NTransactionClient::TimestampFromUnixTime(TInstant::Now().Seconds() - 2 * maxTabletLag.Seconds());
+
+ EXPECT_CALL(*mockClient3, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture(tabletInfos)));
+
+ auto penaltyProvider = CreateReplicationLagPenaltyProvider(
+ path,
+ cluster,
+ maxTabletLag,
+ lagPenalty,
+ mockClient3,
+ true,
+ 2 * CheckPeriod);
+ Sleep(CheckPeriod);
+
+ auto hedgingClientWithPenaltyProvider = CreateTestHedgingClient(
+ TDuration::MilliSeconds(1),
+ SleepQuantum,
+ {mockClient1, mockClient2},
+ {TDuration::Zero(), SleepQuantum},
+ penaltyProvider);
+
+ auto queryResultWithReplicationLagPolicy = NConcurrency::WaitFor(hedgingClientWithPenaltyProvider->ListNode(path));
+ // Check that query result is from second client, because first client received penalty because of replication lag.
+ EXPECT_TRUE(queryResultWithReplicationLagPolicy.IsOK());
+ EXPECT_EQ(queryResultWithReplicationLagPolicy.Value().AsStringBuf(), clientResult2.AsStringBuf());
+
+ Sleep(2 * CheckPeriod);
+
+ auto queryResultWithCleanedPenalty = NConcurrency::WaitFor(hedgingClientWithPenaltyProvider->ListNode(path));
+ // Check that query result is from first client, because replication lag was cleaned.
+ EXPECT_TRUE(queryResultWithCleanedPenalty.IsOK());
+ EXPECT_EQ(queryResultWithCleanedPenalty.Value().AsStringBuf(), clientResult1.AsStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/hook.cpp b/yt/yt/client/hedging/unittests/hook.cpp
new file mode 100644
index 0000000000..d7c527999b
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/hook.cpp
@@ -0,0 +1,20 @@
+#include <yt/yt/core/logging/log_manager.h>
+
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <library/cpp/testing/hook/hook.h>
+
+Y_TEST_HOOK_BEFORE_RUN(TEST_YT_SETUP)
+{
+ NYT::NLogging::TLogManager::Get()->ConfigureFromEnv();
+}
+
+Y_TEST_HOOK_AFTER_RUN(TEST_YT_TEARDOWN)
+{
+ NYT::Shutdown();
+#ifdef _asan_enabled_
+ // Wait for some time to ensure background cleanup is somewhat complete.
+ Sleep(TDuration::Seconds(1));
+ NYT::TRefCountedTrackerFacade::Dump();
+#endif
+}
diff --git a/yt/yt/client/hedging/unittests/options_ut.cpp b/yt/yt/client/hedging/unittests/options_ut.cpp
new file mode 100644
index 0000000000..3c9c11b322
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/options_ut.cpp
@@ -0,0 +1,83 @@
+#include <yt/yt/client/hedging/options.h>
+
+#include <library/cpp/testing/common/scope.h>
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <util/folder/dirut.h>
+#include <util/folder/tempdir.h>
+
+#include <util/stream/file.h>
+
+#include <util/system/env.h>
+
+namespace NYT::NClient::NHedging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TClientOptionsTest, TokenFromFile)
+{
+ TTempDir tmpDir;
+ MakeDirIfNotExist(tmpDir.Name() + "/.yt");
+ {
+ TFileOutput token(tmpDir.Name() + "/.yt/token");
+ token.Write(" AAAA-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \n");
+ }
+ NTesting::TScopedEnvironment envGuard{{
+ {"HOME", tmpDir.Name()},
+ {"YT_TOKEN", ""},
+ {"YT_TOKEN_PATH", ""},
+ }};
+ const auto clientOptions = GetClientOpsFromEnv();
+ EXPECT_TRUE(clientOptions.Token);
+ EXPECT_EQ("AAAA-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", *clientOptions.Token);
+}
+
+TEST(TClientOptionsTest, TokenFromYtTokenPath)
+{
+ TTempDir tmpDir;
+ const TString tokenPath = tmpDir.Name() + "/token";
+ {
+ TFileOutput token(tokenPath);
+ token.Write(" BBBB-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \n");
+ }
+ NTesting::TScopedEnvironment envGuard{{
+ {"YT_TOKEN", ""},
+ {"YT_TOKEN_PATH", tokenPath},
+ }};
+ const auto clientOptions = GetClientOpsFromEnv();
+ EXPECT_TRUE(clientOptions.Token);
+ EXPECT_EQ("BBBB-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", *clientOptions.Token);
+}
+
+TEST(TClientOptionsTest, TokenFromEnv)
+{
+ NTesting::TScopedEnvironment tokenGuard("YT_TOKEN", "BBBB-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
+ const auto& clientOptions = GetClientOpsFromEnv();
+ EXPECT_TRUE(clientOptions.Token);
+ EXPECT_EQ("BBBB-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", *clientOptions.Token);
+}
+
+TEST(TClientOptionsTest, UserFromEnv)
+{
+ NTesting::TScopedEnvironment envGuard{{
+ {"YT_TOKEN", "BBBB-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"},
+ {"YT_USER", "yt_test_user"},
+ }};
+ const auto& clientOptions = GetClientOpsFromEnv();
+ EXPECT_TRUE(clientOptions.User);
+ EXPECT_EQ("yt_test_user", *clientOptions.User);
+}
+
+TEST(TClientOptionsTest, AllowEmptyUser)
+{
+ NTesting::TScopedEnvironment envGuard{{
+ {"YT_TOKEN", "BBBB-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"},
+ {"YT_USER", ""},
+ }};
+ const auto& clientOptions = GetClientOpsFromEnv();
+ EXPECT_TRUE(!clientOptions.User);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging
diff --git a/yt/yt/client/hedging/unittests/penalty_provider_ut.cpp b/yt/yt/client/hedging/unittests/penalty_provider_ut.cpp
new file mode 100644
index 0000000000..914c0d6ce7
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/penalty_provider_ut.cpp
@@ -0,0 +1,224 @@
+#include <yt/yt/client/hedging/penalty_provider.h>
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/client/unittests/mock/client.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::StrictMock;
+
+using TStrictMockClient = StrictMock<NApi::TMockClient>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+ const auto CheckPeriod = TDuration::Seconds(1);
+
+ TReplicationLagPenaltyProviderConfig GenerateReplicationLagPenaltyProviderConfig(
+ const NYPath::TYPath& path,
+ const TString& cluster,
+ const ui32 maxLagInSeconds = 10,
+ const bool clearPenaltiesOnErrors = false,
+ const TDuration checkPeriod = CheckPeriod)
+ {
+ TReplicationLagPenaltyProviderConfig config;
+
+ config.SetTablePath(path);
+ config.AddReplicaClusters(cluster);
+ config.SetMaxTabletsWithLagFraction(0.5);
+ config.SetMaxTabletLag(maxLagInSeconds);
+ config.SetCheckPeriod(checkPeriod.Seconds());
+ config.SetClearPenaltiesOnErrors(clearPenaltiesOnErrors);
+
+ return config;
+ }
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLagPenaltyProviderTest, UpdateExternalPenaltyWhenReplicaHasLag)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-vla\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ ui32 maxLagInSeconds = 10;
+ TReplicationLagPenaltyProviderConfig config = GenerateReplicationLagPenaltyProviderConfig(path, cluster, maxLagInSeconds);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*client, GetNode(path + "/@tablet_count", _))
+ .WillRepeatedly(Return(MakeFuture(tabletCountResult)));
+
+ std::vector<NApi::TTabletInfo> tabletInfos(1);
+ tabletInfos[0].TableReplicaInfos = std::make_optional(std::vector<NApi::TTabletInfo::TTableReplicaInfo>());
+ auto& replicaTabletsInfo = tabletInfos[0].TableReplicaInfos->emplace_back();
+ replicaTabletsInfo.ReplicaId = NTabletClient::TTableReplicaId::FromString("575f-131-40502c5-201b420f");
+ replicaTabletsInfo.LastReplicationTimestamp = NTransactionClient::TimestampFromUnixTime(TInstant::Now().Seconds() - 2 * maxLagInSeconds);
+
+ EXPECT_CALL(*client, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture(tabletInfos)));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(2 * CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), NProfiling::DurationToCpuDuration(TDuration::MilliSeconds(config.GetLagPenalty())));
+}
+
+TEST(TLagPenaltyProviderTest, DoNotUpdatePenaltyWhenReplicaHasNoLag)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-vla\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ TReplicationLagPenaltyProviderConfig config = GenerateReplicationLagPenaltyProviderConfig(path, cluster);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*client, GetNode(path + "/@tablet_count", _))
+ .WillRepeatedly(Return(MakeFuture(tabletCountResult)));
+
+ std::vector<NApi::TTabletInfo> tabletInfos(1);
+ tabletInfos[0].TableReplicaInfos = std::make_optional(std::vector<NApi::TTabletInfo::TTableReplicaInfo>());
+ auto& replicaTabletsInfo = tabletInfos[0].TableReplicaInfos->emplace_back();
+ replicaTabletsInfo.ReplicaId = NTabletClient::TTableReplicaId::FromString("575f-131-40502c5-201b420f");
+ replicaTabletsInfo.LastReplicationTimestamp = NTransactionClient::TimestampFromUnixTime(TInstant::Now().Seconds());
+
+ EXPECT_CALL(*client, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture(tabletInfos)));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(2 * CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), 0);
+}
+
+TEST(TLagPenaltyProviderTest, DoNotUpdatePenaltyWhenGetReplicaIdFailed)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NClient::NHedging::NRpc::TReplicationLagPenaltyProviderConfig config = GenerateReplicationLagPenaltyProviderConfig(path, cluster);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(2 * CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), 0);
+}
+
+TEST(TLagPenaltyProviderTest, DoNotUpdatePenaltyWhenGetTabletsCountFailed)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-vla\"}}"));
+
+ TReplicationLagPenaltyProviderConfig config = GenerateReplicationLagPenaltyProviderConfig(path, cluster);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*client, GetNode(path + "/@tablet_count", _))
+ .WillRepeatedly(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(2 * CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), 0);
+}
+
+TEST(TLagPenaltyProviderTest, DoNotUpdatePenaltyWhenGetTabletsInfoFailed)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-vla\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ NClient::NHedging::NRpc::TReplicationLagPenaltyProviderConfig config =
+ GenerateReplicationLagPenaltyProviderConfig(path, cluster);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*client, GetNode(path + "/@tablet_count", _))
+ .WillRepeatedly(Return(MakeFuture(tabletCountResult)));
+
+ EXPECT_CALL(*client, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture<std::vector<NApi::TTabletInfo>>(TError("Failure"))));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(2 * CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), 0);
+}
+
+TEST(TLagPenaltyProviderTest, ClearPenaltiesAfterError)
+{
+ NYPath::TYPath path = "/test/1234";
+ TString cluster = "seneca-vla";
+
+ NYson::TYsonString replicasResult(TStringBuf("{\"575f-131-40502c5-201b420f\" = {\"cluster_name\" = \"seneca-vla\"}}"));
+ NYson::TYsonString tabletCountResult(TStringBuf("1"));
+
+ ui32 maxLagInSeconds = 10;
+ auto config =
+ GenerateReplicationLagPenaltyProviderConfig(path, cluster, maxLagInSeconds, true, 2 * CheckPeriod);
+
+ auto client = New<TStrictMockClient>();
+
+ EXPECT_CALL(*client, GetNode(path + "/@replicas", _))
+ .WillRepeatedly(Return(MakeFuture(replicasResult)));
+
+ EXPECT_CALL(*client, GetNode(path + "/@tablet_count", _))
+ .WillOnce(Return(MakeFuture(tabletCountResult)))
+ .WillRepeatedly(Return(MakeFuture<NYson::TYsonString>(TError("Failure"))));
+
+ std::vector<NApi::TTabletInfo> tabletInfos(1);
+ tabletInfos[0].TableReplicaInfos = std::make_optional(std::vector<NApi::TTabletInfo::TTableReplicaInfo>());
+ auto& replicaTabletsInfo = tabletInfos[0].TableReplicaInfos->emplace_back();
+ replicaTabletsInfo.ReplicaId = NTabletClient::TTableReplicaId::FromString("575f-131-40502c5-201b420f");
+ replicaTabletsInfo.LastReplicationTimestamp = NTransactionClient::TimestampFromUnixTime(TInstant::Now().Seconds() - 2 * maxLagInSeconds);
+
+ EXPECT_CALL(*client, GetTabletInfos(path, _, _))
+ .WillRepeatedly(Return(MakeFuture(tabletInfos)));
+
+ auto PenaltyProviderPtr = CreateReplicationLagPenaltyProvider(config, client);
+ Sleep(CheckPeriod);
+
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), NProfiling::DurationToCpuDuration(TDuration::MilliSeconds(config.GetLagPenalty())));
+
+ Sleep(2 * CheckPeriod);
+ EXPECT_EQ(PenaltyProviderPtr->Get(cluster), 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/rpc_ut.cpp b/yt/yt/client/hedging/unittests/rpc_ut.cpp
new file mode 100644
index 0000000000..09b7c4e820
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/rpc_ut.cpp
@@ -0,0 +1,35 @@
+#include <yt/yt/client/hedging/rpc.h>
+#include <yt/yt_proto/yt/client/hedging/proto/config.pb.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NClient::NHedging::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(RpcClientTest, SetClusterUrlWithoutProxy)
+{
+ TConfig config;
+ SetClusterUrl(config, "markov");
+ EXPECT_EQ("markov", config.GetClusterName());
+ EXPECT_EQ("", config.GetProxyRole());
+}
+
+TEST(RpcClientTest, SetClusterUrlWithProxy)
+{
+ TConfig config;
+ SetClusterUrl(config, "markov/bigb");
+ EXPECT_EQ("markov", config.GetClusterName());
+ EXPECT_EQ("bigb", config.GetProxyRole());
+}
+
+TEST(RpcClientTest, ProxyRoleOverride)
+{
+ TConfig config;
+ config.SetProxyRole("role");
+ EXPECT_THROW(SetClusterUrl(config, "markov/bigb"), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NClient::NHedging::NRpc
diff --git a/yt/yt/client/hedging/unittests/ya.make b/yt/yt/client/hedging/unittests/ya.make
new file mode 100644
index 0000000000..4ab3e1abbb
--- /dev/null
+++ b/yt/yt/client/hedging/unittests/ya.make
@@ -0,0 +1,30 @@
+GTEST()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+EXPLICIT_DATA()
+
+SRCS(
+ cache_ut.cpp
+ counters_ut.cpp
+ hedging_ut.cpp
+ options_ut.cpp
+ penalty_provider_ut.cpp
+ rpc_ut.cpp
+
+ GLOBAL hook.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ library/cpp/iterator
+ library/cpp/testing/common
+ library/cpp/testing/hook
+ yt/yt/client/hedging
+ yt/yt/client/unittests/mock
+ yt/yt/core
+ yt/yt/library/profiling/solomon
+)
+
+END()
diff --git a/yt/yt/client/hedging/ya.make b/yt/yt/client/hedging/ya.make
new file mode 100644
index 0000000000..acee58f50a
--- /dev/null
+++ b/yt/yt/client/hedging/ya.make
@@ -0,0 +1,28 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ cache.cpp
+ config.cpp
+ counter.cpp
+ hedging.cpp
+ hedging_executor.cpp
+ logger.cpp
+ options.cpp
+ penalty_provider.cpp
+ rpc.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/client
+ yt/yt/library/profiling
+ yt/yt_proto/yt/client/hedging
+
+ library/cpp/iterator
+)
+
+END()
+
+RECURSE_FOR_TESTS(unittests)
diff --git a/yt/yt/client/hive/public.h b/yt/yt/client/hive/public.h
new file mode 100644
index 0000000000..5e8c8284bc
--- /dev/null
+++ b/yt/yt/client/hive/public.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/hydra/public.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+namespace NYT::NHiveClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TTimestampMap;
+class TClusterDirectory;
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NTransactionClient::TTransactionId;
+using NTransactionClient::NullTransactionId;
+using NTransactionClient::TTimestamp;
+using NTransactionClient::NullTimestamp;
+
+using NHydra::TCellId;
+using NHydra::NullCellId;
+
+struct TTimestampMap;
+
+DECLARE_REFCOUNTED_STRUCT(ITransactionParticipant)
+
+YT_DEFINE_ERROR_ENUM(
+ ((MailboxNotCreatedYet) (2200))
+ ((ParticipantUnregistered) (2201))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHiveClient
diff --git a/yt/yt/client/hive/timestamp_map.cpp b/yt/yt/client/hive/timestamp_map.cpp
new file mode 100644
index 0000000000..02487f1525
--- /dev/null
+++ b/yt/yt/client/hive/timestamp_map.cpp
@@ -0,0 +1,77 @@
+#include "timestamp_map.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt_proto/yt/client/hive/proto/timestamp_map.pb.h>
+
+namespace NYT::NHiveClient {
+
+using namespace NObjectClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TTimestamp> TTimestampMap::FindTimestamp(TCellTag cellTag) const
+{
+ for (auto [someCellTag, someTimestamp] : Timestamps) {
+ if (someCellTag == cellTag) {
+ return someTimestamp;
+ }
+ }
+ return std::nullopt;
+}
+
+TTimestamp TTimestampMap::GetTimestamp(TCellTag cellTag) const
+{
+ auto timestamp = FindTimestamp(cellTag);
+ YT_VERIFY(timestamp);
+ return *timestamp;
+}
+
+void TTimestampMap::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist<TVectorSerializer<TTupleSerializer<std::pair<TCellTag, TTimestamp>, 2>>>(
+ context,
+ Timestamps);
+}
+
+void ToProto(NProto::TTimestampMap* protoMap, const TTimestampMap& map)
+{
+ protoMap->clear_cell_tags();
+ protoMap->clear_timestamps();
+ for (auto [cellTag, timestamp] : map.Timestamps) {
+ protoMap->add_cell_tags(ToProto<int>(cellTag));
+ protoMap->add_timestamps(timestamp);
+ }
+}
+
+void FromProto(TTimestampMap* map, const NProto::TTimestampMap& protoMap)
+{
+ map->Timestamps.clear();
+ YT_VERIFY(protoMap.cell_tags_size() == protoMap.timestamps_size());
+ for (int index = 0; index < protoMap.cell_tags_size(); ++index) {
+ map->Timestamps.emplace_back(
+ protoMap.cell_tags(index),
+ protoMap.timestamps(index));
+ }
+}
+
+void FormatValue(TStringBuilderBase* builder, const TTimestampMap& map, TStringBuf /*spec*/)
+{
+ builder->AppendChar('{');
+ bool first = true;
+ for (auto [cellTag, timestamp] : map.Timestamps) {
+ if (!first) {
+ builder->AppendString(TStringBuf(", "));
+ }
+ builder->AppendFormat("%v => %v", cellTag, timestamp);
+ first = false;
+ }
+ builder->AppendChar('}');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHiveClient
diff --git a/yt/yt/client/hive/timestamp_map.h b/yt/yt/client/hive/timestamp_map.h
new file mode 100644
index 0000000000..1958a8a96b
--- /dev/null
+++ b/yt/yt/client/hive/timestamp_map.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <optional>
+
+namespace NYT::NHiveClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTimestampMap
+{
+ TCompactVector<std::pair<NObjectClient::TCellTag, TTimestamp>, 4> Timestamps;
+
+ std::optional<TTimestamp> FindTimestamp(NObjectClient::TCellTag) const;
+ TTimestamp GetTimestamp(NObjectClient::TCellTag) const;
+
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+void ToProto(NProto::TTimestampMap* protoMap, const TTimestampMap& map);
+void FromProto(TTimestampMap* map, const NProto::TTimestampMap& protoMap);
+
+void FormatValue(TStringBuilderBase* builder, const TTimestampMap& map, TStringBuf spec);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHiveClient
diff --git a/yt/yt/client/hive/transaction_participant.h b/yt/yt/client/hive/transaction_participant.h
new file mode 100644
index 0000000000..e52d381f26
--- /dev/null
+++ b/yt/yt/client/hive/transaction_participant.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NHiveClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETransactionParticipantState,
+ // Participant is available for interaction; this does not imply any partcular health status.
+ (Valid)
+ // Cluster connection was terminated, participant is no longer usable.
+ (Invalidated)
+ // Participant is not known; e.g. cluster is not registered.
+ (NotRegistered)
+ // Participant is known to be unregistered and will never be resurrected.
+ (Unregistered)
+);
+
+struct ITransactionParticipant
+ : public virtual TRefCounted
+{
+ virtual TCellId GetCellId() const = 0;
+
+ //! Returns tag of a cluster timestamps were generated from.
+ virtual NApi::TClusterTag GetClockClusterTag() const = 0;
+
+ virtual const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() const = 0;
+
+ virtual ETransactionParticipantState GetState() const = 0;
+
+ virtual TFuture<void> PrepareTransaction(
+ TTransactionId transactionId,
+ TTimestamp prepareTimestamp,
+ NApi::TClusterTag prepareTimestampClusterTag,
+ const std::vector<TCellId>& cellIdsToSyncWith,
+ const NRpc::TAuthenticationIdentity& identity) = 0;
+ virtual TFuture<void> CommitTransaction(
+ TTransactionId transactionId,
+ TTimestamp commitTimestamp,
+ NApi::TClusterTag commitTimestampClusterTag,
+ const NRpc::TAuthenticationIdentity& identity) = 0;
+ virtual TFuture<void> AbortTransaction(
+ TTransactionId transactionId,
+ const NRpc::TAuthenticationIdentity& identity) = 0;
+
+ virtual TFuture<void> CheckAvailability() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITransactionParticipant)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHiveClient
diff --git a/yt/yt/client/hydra/public.h b/yt/yt/client/hydra/public.h
new file mode 100644
index 0000000000..5f1e63206b
--- /dev/null
+++ b/yt/yt/client/hydra/public.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <yt/yt/client/election/public.h>
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NHydra {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EPeerState,
+ ((None) (0))
+ ((Stopped) (1))
+ ((Elections) (2))
+ ((FollowerRecovery) (3))
+ ((Following) (4))
+ ((LeaderRecovery) (5))
+ ((Leading) (6))
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((NoSuchSnapshot) (600))
+ ((NoSuchChangelog) (601))
+ ((InvalidEpoch) (602))
+ ((InvalidVersion) (603))
+ ((OutOfOrderMutations) (609))
+ ((InvalidSnapshotVersion) (610))
+ ((ReadOnlySnapshotBuilt) (611))
+ ((ReadOnlySnapshotBuildFailed) (612))
+ ((BrokenChangelog) (613))
+ ((ChangelogIOError) (614))
+ ((InvalidChangelogState) (615))
+ ((ReadOnly) (616))
+);
+
+DEFINE_ENUM(EPeerKind,
+ ((Leader) (0))
+ ((Follower) (1))
+ ((LeaderOrFollower) (2))
+);
+
+using TRevision = ui64;
+constexpr TRevision NullRevision = 0;
+
+struct TVersion;
+struct TReachableState;
+struct TElectionPriority;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NElection::TCellId;
+using NElection::NullCellId;
+using NElection::TPeerId;
+using NElection::InvalidPeerId;
+using NElection::TPeerPriority;
+using NElection::TEpochId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHydra
diff --git a/yt/yt/client/hydra/version.cpp b/yt/yt/client/hydra/version.cpp
new file mode 100644
index 0000000000..904f090db6
--- /dev/null
+++ b/yt/yt/client/hydra/version.cpp
@@ -0,0 +1,112 @@
+#include "version.h"
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT::NHydra {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReachableState::TReachableState(int segmentId, i64 sequenceNumber) noexcept
+ : SegmentId(segmentId)
+ , SequenceNumber(sequenceNumber)
+{ }
+
+std::strong_ordering TReachableState::operator <=> (const TReachableState& other) const
+{
+ if (SegmentId != other.SegmentId) {
+ return SegmentId <=> other.SegmentId;
+ }
+ return SequenceNumber <=> other.SequenceNumber;
+}
+
+void FormatValue(TStringBuilderBase* builder, TReachableState state, TStringBuf /* spec */)
+{
+ builder->AppendFormat("%v:%v", state.SegmentId, state.SequenceNumber);
+}
+
+TString ToString(TReachableState state)
+{
+ return ToStringViaBuilder(state);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TElectionPriority::TElectionPriority(int lastMutationTerm, int segmentId, i64 sequenceNumber) noexcept
+ : LastMutationTerm(lastMutationTerm)
+ , ReachableState(segmentId, sequenceNumber)
+{ }
+
+TElectionPriority::TElectionPriority(int lastMutationTerm, TReachableState reachableState) noexcept
+ : LastMutationTerm(lastMutationTerm)
+ , ReachableState(reachableState)
+{ }
+
+std::strong_ordering TElectionPriority::operator <=> (const TElectionPriority& other) const
+{
+ if (LastMutationTerm != other.LastMutationTerm) {
+ return LastMutationTerm <=> other.LastMutationTerm;
+ }
+ return ReachableState.SequenceNumber <=> other.ReachableState.SequenceNumber;
+}
+
+void FormatValue(TStringBuilderBase* builder, TElectionPriority priority, TStringBuf /* spec */)
+{
+ builder->AppendFormat("{MutationTerm: %v, State: %v}",
+ priority.LastMutationTerm,
+ priority.ReachableState);
+}
+
+TString ToString(TElectionPriority state)
+{
+ return ToStringViaBuilder(state);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVersion::TVersion(int segmentId, int recordId) noexcept
+ : SegmentId(segmentId)
+ , RecordId(recordId)
+{ }
+
+std::strong_ordering TVersion::operator <=> (const TVersion& other) const
+{
+ if (SegmentId != other.SegmentId) {
+ return SegmentId <=> other.SegmentId;
+ }
+ return RecordId <=> other.RecordId;
+}
+
+TRevision TVersion::ToRevision() const
+{
+ return (static_cast<TRevision>(SegmentId) << 32) | static_cast<TRevision>(RecordId);
+}
+
+TVersion TVersion::FromRevision(TRevision revision)
+{
+ return TVersion(revision >> 32, revision & 0xffffffff);
+}
+
+TVersion TVersion::Advance(int delta) const
+{
+ YT_ASSERT(delta >= 0);
+ return TVersion(SegmentId, RecordId + delta);
+}
+
+TVersion TVersion::Rotate() const
+{
+ return TVersion(SegmentId + 1, 0);
+}
+
+void FormatValue(TStringBuilderBase* builder, TVersion version, TStringBuf /* spec */)
+{
+ builder->AppendFormat("%v:%v", version.SegmentId, version.RecordId);
+}
+
+TString ToString(TVersion version)
+{
+ return ToStringViaBuilder(version);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHydra
diff --git a/yt/yt/client/hydra/version.h b/yt/yt/client/hydra/version.h
new file mode 100644
index 0000000000..4ef7bfc7f5
--- /dev/null
+++ b/yt/yt/client/hydra/version.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/typetraits.h>
+
+namespace NYT::NHydra {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReachableState
+{
+ int SegmentId = 0;
+ i64 SequenceNumber = 0;
+
+ TReachableState() = default;
+ TReachableState(int segmentId, i64 sequenceNumber) noexcept;
+
+ std::strong_ordering operator <=> (const TReachableState& other) const;
+ bool operator == (const TReachableState& other) const = default;
+};
+
+void FormatValue(TStringBuilderBase* builder, TReachableState state, TStringBuf /*spec*/);
+TString ToString(TReachableState state);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TElectionPriority
+{
+ // Actual election priority is a pair (LastMutationTerm, ReachableState.SequenceNumber).
+ // Term is used to determine a new leader's term.
+ // ReachableState.SegmentId is used as a hint for recovery.
+
+ int LastMutationTerm = 0;
+ TReachableState ReachableState;
+
+ TElectionPriority() = default;
+ TElectionPriority(
+ int lastMutationTerm,
+ int segmentId,
+ i64 sequenceNumber) noexcept;
+ TElectionPriority(
+ int lastMutationTerm,
+ TReachableState reachableState) noexcept;
+
+ std::strong_ordering operator <=> (const TElectionPriority& other) const;
+ bool operator == (const TElectionPriority& other) const = default;
+};
+
+void FormatValue(TStringBuilderBase* builder, TElectionPriority state, TStringBuf /*spec*/);
+TString ToString(TElectionPriority state);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TVersion
+{
+ int SegmentId = 0;
+ int RecordId = 0;
+
+ TVersion() = default;
+ TVersion(int segmentId, int recordId) noexcept;
+
+ std::strong_ordering operator <=> (const TVersion& other) const;
+ bool operator == (const TVersion& other) const = default;
+
+ TRevision ToRevision() const;
+ static TVersion FromRevision(TRevision revision);
+
+ TVersion Advance(int delta = 1) const;
+ TVersion Rotate() const;
+};
+
+void FormatValue(TStringBuilderBase* builder, TVersion version, TStringBuf spec);
+TString ToString(TVersion version);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHydra
+
+Y_DECLARE_PODTYPE(NYT::NHydra::TVersion);
diff --git a/yt/yt/client/job_tracker_client/helpers.cpp b/yt/yt/client/job_tracker_client/helpers.cpp
new file mode 100644
index 0000000000..0c8df0bb8e
--- /dev/null
+++ b/yt/yt/client/job_tracker_client/helpers.cpp
@@ -0,0 +1,29 @@
+#include "helpers.h"
+
+namespace NYT::NJobTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsJobFinished(EJobState state)
+{
+ return state == EJobState::Completed ||
+ state == EJobState::Failed ||
+ state == EJobState::Aborted ||
+ state == EJobState::Lost;
+}
+
+bool IsJobInProgress(EJobState state)
+{
+ return state == EJobState::Waiting ||
+ state == EJobState::Running ||
+ state == EJobState::Aborting;
+}
+
+bool IsMasterJobType(EJobType jobType)
+{
+ return FirstMasterJobType <= jobType && jobType <= LastMasterJobType;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJobTrackerClient
diff --git a/yt/yt/client/job_tracker_client/helpers.h b/yt/yt/client/job_tracker_client/helpers.h
new file mode 100644
index 0000000000..7a11c648f7
--- /dev/null
+++ b/yt/yt/client/job_tracker_client/helpers.h
@@ -0,0 +1,14 @@
+#include "public.h"
+
+namespace NYT::NJobTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsJobFinished(EJobState state);
+bool IsJobInProgress(EJobState state);
+
+bool IsMasterJobType(EJobType jobType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJobTrackerClient
diff --git a/yt/yt/client/job_tracker_client/public.cpp b/yt/yt/client/job_tracker_client/public.cpp
new file mode 100644
index 0000000000..ab7e82cc78
--- /dev/null
+++ b/yt/yt/client/job_tracker_client/public.cpp
@@ -0,0 +1,13 @@
+#include "public.h"
+
+namespace NYT::NJobTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TJobId NullJobId;
+const TOperationId NullOperationId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJobTrackerClient
+
diff --git a/yt/yt/client/job_tracker_client/public.h b/yt/yt/client/job_tracker_client/public.h
new file mode 100644
index 0000000000..bfa4869039
--- /dev/null
+++ b/yt/yt/client/job_tracker_client/public.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <library/cpp/yt/misc/enum.h>
+#include <library/cpp/yt/misc/guid.h>
+
+namespace NYT::NJobTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TJobId = TGuid;
+extern const TJobId NullJobId;
+
+using TOperationId = TGuid;
+extern const TOperationId NullOperationId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Please keep the range of values small as this type
+// is used as a key of TEnumIndexedVector.
+DEFINE_ENUM(EJobType,
+ // Scheduler jobs
+ ((Map) ( 1))
+ ((PartitionMap) ( 2))
+ ((SortedMerge) ( 3))
+ ((OrderedMerge) ( 4))
+ ((UnorderedMerge) ( 5))
+ ((Partition) ( 6))
+ ((SimpleSort) ( 7))
+ ((FinalSort) ( 8))
+ ((SortedReduce) ( 9))
+ ((PartitionReduce) ( 10))
+ ((ReduceCombiner) ( 11))
+ ((RemoteCopy) ( 12))
+ ((IntermediateSort) ( 13))
+ ((OrderedMap) ( 14))
+ ((JoinReduce) ( 15))
+ ((Vanilla) ( 16))
+ ((ShallowMerge) ( 17))
+ ((SchedulerUnknown) ( 98)) // Used by node to report aborted jobs for which spec request has failed.
+
+ // Master jobs
+ ((ReplicateChunk) (100))
+ ((RemoveChunk) (101))
+ ((RepairChunk) (102))
+ ((SealChunk) (103))
+ ((MergeChunks) (104))
+ ((AutotomizeChunk) (105))
+ ((ReincarnateChunk) (106))
+);
+
+constexpr auto FirstSchedulerJobType = EJobType::Map;
+constexpr auto LastSchedulerJobType = EJobType::SchedulerUnknown;
+
+constexpr auto FirstMasterJobType = EJobType::ReplicateChunk;
+constexpr auto LastMasterJobType = EJobType::ReincarnateChunk;
+
+// NB: Please keep the range of values small as this type
+// is used as a key of TEnumIndexedVector.
+DEFINE_ENUM(EJobState,
+ ((Waiting) (0))
+ ((Running) (1))
+ // COMPAT(pogorelov): Remove it after 23.1
+ ((Aborting) (2))
+ // |Completed| is used as sentinel in NJobTrackerClient::HasJobFinished.
+ ((Completed) (3))
+ ((Failed) (4))
+ ((Aborted) (5))
+ // This sentinel is only used in TJob::GetStatisticsSuffix.
+ ((Lost) (7))
+ // Initial state of newly created job.
+ ((None) (8))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJobTrackerClient
diff --git a/yt/yt/client/journal_client/config.cpp b/yt/yt/client/journal_client/config.cpp
new file mode 100644
index 0000000000..dde03da128
--- /dev/null
+++ b/yt/yt/client/journal_client/config.cpp
@@ -0,0 +1,17 @@
+#include "config.h"
+namespace NYT::NJournalClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("replica_data_size_read_threshold", &TThis::ReplicaDataSizeReadThreshold)
+ .Default(1_MB);
+
+ registrar.Parameter("slow_path_delay", &TThis::SlowPathDelay)
+ .Default(TDuration::Seconds(5));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJournalClient
diff --git a/yt/yt/client/journal_client/config.h b/yt/yt/client/journal_client/config.h
new file mode 100644
index 0000000000..e44278f99b
--- /dev/null
+++ b/yt/yt/client/journal_client/config.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+#include <yt/yt/client/chunk_client/config.h>
+
+namespace NYT::NJournalClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkReaderConfig
+ : public virtual NChunkClient::TReplicationReaderConfig
+{
+public:
+ //! Reader will skip replicas with less than this amount of relevant data
+ //! data available.
+ i64 ReplicaDataSizeReadThreshold;
+
+ //! When fetching replica metas, journal reader will wait for this period of time
+ //! before starting slow path in hope to run fast path.
+ TDuration SlowPathDelay;
+
+ REGISTER_YSON_STRUCT(TChunkReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJournalClient
diff --git a/yt/yt/client/journal_client/public.cpp b/yt/yt/client/journal_client/public.cpp
new file mode 100644
index 0000000000..3c3fdaa136
--- /dev/null
+++ b/yt/yt/client/journal_client/public.cpp
@@ -0,0 +1,10 @@
+#include "public.h"
+
+namespace NYT::NJournalClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJournalClient
+
diff --git a/yt/yt/client/journal_client/public.h b/yt/yt/client/journal_client/public.h
new file mode 100644
index 0000000000..12dbfaf6a8
--- /dev/null
+++ b/yt/yt/client/journal_client/public.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NJournalClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TChunkReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr i64 DefaultReplicaLagLimit = 32768;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJournalClient
diff --git a/yt/yt/client/misc/config.cpp b/yt/yt/client/misc/config.cpp
new file mode 100644
index 0000000000..9adabdb19d
--- /dev/null
+++ b/yt/yt/client/misc/config.cpp
@@ -0,0 +1,15 @@
+#include "config.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TWorkloadConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("workload_descriptor", &TThis::WorkloadDescriptor)
+ .Default(TWorkloadDescriptor(EWorkloadCategory::UserBatch));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/config.h b/yt/yt/client/misc/config.h
new file mode 100644
index 0000000000..c8c82c1e06
--- /dev/null
+++ b/yt/yt/client/misc/config.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "workload.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWorkloadConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ TWorkloadDescriptor WorkloadDescriptor;
+
+ REGISTER_YSON_STRUCT(TWorkloadConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TWorkloadConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/io_tags.cpp b/yt/yt/client/misc/io_tags.cpp
new file mode 100644
index 0000000000..6e7af3312e
--- /dev/null
+++ b/yt/yt/client/misc/io_tags.cpp
@@ -0,0 +1,32 @@
+#include "io_tags.h"
+
+#include <yt/yt/core/ytree/helpers.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString FormatIOTag(ERawIOTag tag)
+{
+ return FormatEnum(tag);
+}
+
+TString FormatIOTag(EAggregateIOTag tag)
+{
+ return FormatEnum(tag) + "@";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void AddTagToBaggage(const NYTree::IAttributeDictionaryPtr& baggage, T tag, const TStringBuf& value)
+{
+ baggage->Set(FormatIOTag(tag), value);
+}
+
+template void AddTagToBaggage<ERawIOTag>(const NYTree::IAttributeDictionaryPtr& baggage, ERawIOTag tag, const TStringBuf& value);
+template void AddTagToBaggage<EAggregateIOTag>(const NYTree::IAttributeDictionaryPtr& baggage, EAggregateIOTag tag, const TStringBuf& value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/io_tags.h b/yt/yt/client/misc/io_tags.h
new file mode 100644
index 0000000000..17aee8a6dd
--- /dev/null
+++ b/yt/yt/client/misc/io_tags.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/attributes.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ERawIOTag,
+ ((ReadSessionId) (0))
+ ((WriteSessionId) (1))
+ ((JobId) (2))
+ ((ObjectPath) (3))
+ ((ObjectId) (4))
+ ((OperationId) (5))
+ ((ChunkId) (6))
+ ((LocationId) (7))
+ ((SlotIndex) (8))
+);
+
+DEFINE_ENUM(EAggregateIOTag,
+ ((Direction) (0))
+ ((Medium) (1))
+ ((DiskFamily) (2))
+ ((User) (3))
+ ((LocationType) (4))
+ ((DataNodeMethod) (5))
+ ((JobType) (6))
+ ((Account) (7))
+ ((ApiMethod ) (8))
+ ((ProxyKind ) (9))
+ ((Pool) (10))
+ ((PoolTree) (11))
+ ((OperationType) (12))
+ ((TaskName) (13))
+ ((JobIoKind) (14))
+ ((PoolPath) (15))
+ ((ErasureCodec) (16))
+ ((CompressionCodec) (17))
+);
+
+TString FormatIOTag(ERawIOTag tag);
+TString FormatIOTag(EAggregateIOTag tag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void AddTagToBaggage(const NYTree::IAttributeDictionaryPtr& baggage, T tag, const TStringBuf& value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/method_helpers.cpp b/yt/yt/client/misc/method_helpers.cpp
new file mode 100644
index 0000000000..2a835f3953
--- /dev/null
+++ b/yt/yt/client/misc/method_helpers.cpp
@@ -0,0 +1,12 @@
+#include "method_helpers.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+
+[[noreturn]] void ThrowUnimplementedClientMethodError(TStringBuf methodName, TStringBuf reason)
+{
+ throw TErrorException() <<= TError("%v method: %v", reason, methodName);
+}
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/method_helpers.h b/yt/yt/client/misc/method_helpers.h
new file mode 100644
index 0000000000..7c47a40c49
--- /dev/null
+++ b/yt/yt/client/misc/method_helpers.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <util/generic/string.h>
+#include <util/generic/yexception.h>
+#include <util/system/compiler.h>
+
+namespace NYT {
+
+[[noreturn]] void ThrowUnimplementedClientMethodError(TStringBuf methodName, TStringBuf reason);
+
+#define Y_COMMA() ,
+
+#define Y_FUNCTION_ARG_UNUSED(x) x
+#define Y_FUNCTION_ARG(i, x) x Y_CAT(a, i)
+#define Y_PASS_ARG(i, x) Y_CAT(a, i)
+
+#define Y_FUNCTION_ARG_UNUSED_COMMA(x) x Y_COMMA()
+#define Y_FUNCTION_ARG_COMMA(i, x) x Y_CAT(a, i) Y_COMMA()
+#define Y_PASS_ARG_COMMA(i, x) Y_CAT(a, i) Y_COMMA()
+
+#define Y_VA_ARGS(...) __VA_ARGS__
+
+// Map input arguments types.
+// Example: Y_METHOD_UNUSED_ARGS_DECLARATION((int, TString)) ---> int, TString.
+#define Y_METHOD_UNUSED_ARGS_DECLARATION(Args) Y_MAP_ARGS_WITH_LAST(Y_FUNCTION_ARG_UNUSED_COMMA, Y_FUNCTION_ARG_UNUSED, Y_PASS_VA_ARGS(Y_VA_ARGS Args))
+
+// Pass capture list as is.
+// Example: Y_PASS_CAPTURE_LIST((&a1, a2)) ---> &a1, a2.
+#define Y_PASS_CAPTURE_LIST(Args) Y_METHOD_UNUSED_ARGS_DECLARATION(Args)
+
+// Map input arguments types with parameter names introduction.
+// Example: Y_METHOD_USED_ARGS_DECLARATION((int, TString)) ---> int a2, TString a1.
+#define Y_METHOD_USED_ARGS_DECLARATION(Args) Y_MAP_ARGS_WITH_LAST_N(Y_FUNCTION_ARG_COMMA, Y_FUNCTION_ARG, Y_PASS_VA_ARGS(Y_VA_ARGS Args))
+
+// Map input arguments types into corresponding parameter names.
+// Example: Y_PASS_METHOD_USED_ARGS((int, TString)) ---> a2, a1.
+#define Y_PASS_METHOD_USED_ARGS(Args) Y_MAP_ARGS_WITH_LAST_N(Y_PASS_ARG_COMMA, Y_PASS_ARG, Y_PASS_VA_ARGS(Y_VA_ARGS Args))
+
+#define UNIMPLEMENTED_METHOD(ReturnType, MethodName, Args) NO_METHOD_IMPL(ReturnType, MethodName, "Not implemented", Args)
+#define UNIMPLEMENTED_CONST_METHOD(ReturnType, MethodName, Args) NO_CONST_METHOD_IMPL(ReturnType, MethodName, "Not implemented", Args)
+
+#define UNSUPPORTED_METHOD(ReturnType, MethodName, Args) NO_METHOD_IMPL(ReturnType, MethodName, "Not supported", Args)
+#define UNSUPPORTED_CONST_METHOD(ReturnType, MethodName, Args) NO_CONST_METHOD_IMPL(ReturnType, MethodName, "Not supported", Args)
+
+#define NO_METHOD_IMPL(ReturnType, MethodName, Reason, Args) \
+ ReturnType MethodName(Y_METHOD_UNUSED_ARGS_DECLARATION(Args)) override \
+ { \
+ ThrowUnimplementedClientMethodError(Y_STRINGIZE(MethodName), Y_STRINGIZE(Reason)); \
+ Y_UNREACHABLE(); \
+ } Y_SEMICOLON_GUARD
+
+#define NO_CONST_METHOD_IMPL(ReturnType, MethodName, Reason, Args) \
+ ReturnType MethodName(Y_METHOD_UNUSED_ARGS_DECLARATION(Args)) const override \
+ { \
+ ThrowUnimplementedClientMethodError(Y_STRINGIZE(MethodName), Y_STRINGIZE(Reason)); \
+ Y_UNREACHABLE(); \
+ } Y_SEMICOLON_GUARD
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/public.h b/yt/yt/client/misc/public.h
new file mode 100644
index 0000000000..a5f8132c17
--- /dev/null
+++ b/yt/yt/client/misc/public.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TWorkloadDescriptor;
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! The type of the workload.
+//! Order is given by |GetBasicPriority| function and not by concrete enum values.
+/*!
+ * This is the most fine-grained categorization available.
+ * Most subsystems will map EWorkloadCategory to their own coarser categories.
+ *
+ * NB: This enum is serializable, so please keep values intact or advance
+ * protocol version where appropriate.
+ */
+DEFINE_ENUM(EWorkloadCategory,
+ ((Idle) (0))
+ ((SystemArtifactCacheDownload) (9))
+ ((SystemRepair) (2))
+ ((SystemReincarnation) (17))
+ ((SystemReplication) (1))
+ ((SystemMerge) (16))
+ ((SystemTabletCompaction) (6))
+ ((SystemTabletLogging) (5))
+ ((SystemTabletPartitioning) (7))
+ ((SystemTabletPreload) (8))
+ ((SystemTabletRecovery) (10))
+ ((SystemTabletReplication) (14))
+ ((SystemTabletSnapshot) (13))
+ ((SystemTabletStoreFlush) (12))
+ ((UserBatch) (3))
+ ((UserInteractive) (11))
+ ((UserRealtime) (4))
+ ((UserDynamicStoreRead) (15))
+);
+
+struct TWorkloadDescriptor;
+
+DECLARE_REFCOUNTED_CLASS(TWorkloadConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/misc/workload.cpp b/yt/yt/client/misc/workload.cpp
new file mode 100644
index 0000000000..8e9a8e1d44
--- /dev/null
+++ b/yt/yt/client/misc/workload.cpp
@@ -0,0 +1,187 @@
+#include "workload.h"
+
+#include <yt/yt_proto/yt/client/misc/proto/workload.pb.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/fair_share_thread_pool.h>
+
+#include <yt/yt/core/rpc/service.h>
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYson;
+using namespace NRpc;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const i64 CategoryPriorityFactor = (i64) 1 << 56;
+static const i64 BandPriorityFactor = (i64) 1 << 48;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWorkloadDescriptor::TWorkloadDescriptor(
+ EWorkloadCategory category,
+ int band,
+ TInstant instant,
+ std::vector<TString> annotations,
+ std::optional<NConcurrency::TFairShareThreadPoolTag> compressionFairShareTag)
+ : Category(category)
+ , Band(band)
+ , Instant(instant)
+ , Annotations(std::move(annotations))
+ , CompressionFairShareTag(std::move(compressionFairShareTag))
+{ }
+
+TWorkloadDescriptor TWorkloadDescriptor::SetCurrentInstant() const
+{
+ return TWorkloadDescriptor(Category, Band, TInstant::Now(), Annotations, CompressionFairShareTag);
+}
+
+i64 TWorkloadDescriptor::GetPriority() const
+{
+ auto priority = GetBasicPriority(Category) + BandPriorityFactor * Band;
+ if (Category == EWorkloadCategory::UserBatch) {
+ priority -= Instant.MilliSeconds();
+ }
+ return priority;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+i64 GetBasicPriority(EWorkloadCategory category)
+{
+ switch (category) {
+ case EWorkloadCategory::Idle:
+ return 0;
+
+ case EWorkloadCategory::SystemReplication:
+ case EWorkloadCategory::SystemTabletCompaction:
+ case EWorkloadCategory::SystemTabletPartitioning:
+ case EWorkloadCategory::SystemTabletPreload:
+ case EWorkloadCategory::SystemTabletReplication:
+ case EWorkloadCategory::SystemTabletStoreFlush:
+ case EWorkloadCategory::SystemArtifactCacheDownload:
+ case EWorkloadCategory::UserBatch:
+ return CategoryPriorityFactor * 1;
+
+ case EWorkloadCategory::SystemRepair:
+ case EWorkloadCategory::SystemTabletSnapshot:
+ return CategoryPriorityFactor * 2;
+
+ case EWorkloadCategory::UserInteractive:
+ case EWorkloadCategory::SystemTabletRecovery:
+ return CategoryPriorityFactor * 3;
+
+ case EWorkloadCategory::UserRealtime:
+ case EWorkloadCategory::SystemTabletLogging:
+ return CategoryPriorityFactor * 4;
+
+ // Graceful fallback for possible future extensions of categories.
+ default:
+ return 0;
+ }
+}
+
+IInvokerPtr GetCompressionInvoker(const TWorkloadDescriptor& workloadDescriptor)
+{
+ if (workloadDescriptor.CompressionFairShareTag) {
+ return NRpc::TDispatcher::Get()->GetFairShareCompressionThreadPool()
+ ->GetInvoker(*workloadDescriptor.CompressionFairShareTag);
+ } else {
+ return CreateFixedPriorityInvoker(
+ NRpc::TDispatcher::Get()->GetPrioritizedCompressionPoolInvoker(),
+ workloadDescriptor.GetPriority());
+ }
+}
+
+struct TSerializableWorkloadDescriptor
+ : public TWorkloadDescriptor
+ , public TYsonStructLite
+{
+ REGISTER_YSON_STRUCT_LITE(TSerializableWorkloadDescriptor);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.BaseClassParameter("category", &TWorkloadDescriptor::Category);
+ registrar.BaseClassParameter("band", &TWorkloadDescriptor::Band)
+ .Default(0);
+ registrar.BaseClassParameter("annotations", &TWorkloadDescriptor::Annotations)
+ .Default();
+ }
+};
+
+void Serialize(const TWorkloadDescriptor& descriptor, IYsonConsumer* consumer)
+{
+ TSerializableWorkloadDescriptor wrapper = TSerializableWorkloadDescriptor::Create();
+ static_cast<TWorkloadDescriptor&>(wrapper) = descriptor;
+ Serialize(static_cast<const TYsonStructLite&>(wrapper), consumer);
+}
+
+void Deserialize(TWorkloadDescriptor& descriptor, INodePtr node)
+{
+ TSerializableWorkloadDescriptor wrapper = TSerializableWorkloadDescriptor::Create();
+ Deserialize(static_cast<TYsonStructLite&>(wrapper), node);
+ descriptor = static_cast<TWorkloadDescriptor&>(wrapper);
+}
+
+void Deserialize(TWorkloadDescriptor& descriptor, NYson::TYsonPullParserCursor* cursor)
+{
+ TSerializableWorkloadDescriptor wrapper = TSerializableWorkloadDescriptor::Create();
+ Deserialize(static_cast<TYsonStructLite&>(wrapper), cursor);
+ descriptor = static_cast<TWorkloadDescriptor&>(wrapper);
+}
+
+void ToProto(NYT::NProto::TWorkloadDescriptor* protoDescriptor, const TWorkloadDescriptor& descriptor)
+{
+ protoDescriptor->set_category(static_cast<int>(descriptor.Category));
+ protoDescriptor->set_band(descriptor.Band);
+ protoDescriptor->set_instant(ToProto<i64>(descriptor.Instant));
+ ToProto(protoDescriptor->mutable_annotations(), descriptor.Annotations);
+}
+
+void FromProto(TWorkloadDescriptor* descriptor, const NYT::NProto::TWorkloadDescriptor& protoDescriptor)
+{
+ descriptor->Category = EWorkloadCategory(protoDescriptor.category());
+ descriptor->Band = protoDescriptor.band();
+ descriptor->Instant = FromProto<TInstant>(protoDescriptor.instant());
+ FromProto(&descriptor->Annotations, protoDescriptor.annotations());
+}
+
+void FormatValue(
+ TStringBuilderBase* builder,
+ const TWorkloadDescriptor& descriptor,
+ TStringBuf /*format*/)
+{
+ builder->AppendFormat("%v:%v",
+ descriptor.Category,
+ descriptor.Band);
+ if (descriptor.Instant != TInstant::Zero()) {
+ builder->AppendFormat(":%v",
+ descriptor.Instant);
+ }
+ if (!descriptor.Annotations.empty()) {
+ builder->AppendString(":{");
+ for (size_t index = 0; index < descriptor.Annotations.size(); ++index) {
+ builder->AppendString(descriptor.Annotations[index]);
+ if (index != descriptor.Annotations.size() - 1) {
+ builder->AppendString(", ");
+ }
+ }
+ builder->AppendChar('}');
+ }
+}
+
+TString ToString(const TWorkloadDescriptor& descriptor)
+{
+ return ToStringViaBuilder(descriptor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/client/misc/workload.h b/yt/yt/client/misc/workload.h
new file mode 100644
index 0000000000..4fa95201e9
--- /dev/null
+++ b/yt/yt/client/misc/workload.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWorkloadDescriptor
+{
+ TWorkloadDescriptor(
+ EWorkloadCategory category = EWorkloadCategory::Idle,
+ int band = 0,
+ TInstant instant = {},
+ std::vector<TString> annotations = {},
+ std::optional<NConcurrency::TFairShareThreadPoolTag> compressionFairShareTag = {});
+
+ //! The type of the workload defining its basic priority.
+ EWorkloadCategory Category;
+
+ //! The relative importance of the workload (among others within the category).
+ //! Zero is the default value.
+ //! Larger is better.
+ int Band;
+
+ //! The time instant when this workload has been initiated.
+ //! #EWorkloadCategory::UserBatch workloads rely on this value for FIFO ordering.
+ TInstant Instant;
+
+ //! Arbitrary client-supplied strings to be logged at server-side.
+ std::vector<TString> Annotations;
+
+ //! If present, invoker from fair share thread pool will be used for decompression.
+ std::optional<NConcurrency::TFairShareThreadPoolTag> CompressionFairShareTag;
+
+ //! Updates the instant field with the current time.
+ TWorkloadDescriptor SetCurrentInstant() const;
+
+ //! Computes the aggregated priority.
+ //! Larger is better.
+ i64 GetPriority() const;
+};
+
+i64 GetBasicPriority(EWorkloadCategory category);
+
+IInvokerPtr GetCompressionInvoker(const TWorkloadDescriptor& workloadDescriptor);
+
+void FormatValue(
+ TStringBuilderBase* builder,
+ const TWorkloadDescriptor& descriptor,
+ TStringBuf format);
+TString ToString(const TWorkloadDescriptor& descriptor);
+
+void ToProto(NYT::NProto::TWorkloadDescriptor* protoDescriptor, const TWorkloadDescriptor& descriptor);
+void FromProto(TWorkloadDescriptor* descriptor, const NYT::NProto::TWorkloadDescriptor& protoDescriptor);
+
+void Serialize(const TWorkloadDescriptor& descriptor, NYson::IYsonConsumer* consumer);
+void Deserialize(TWorkloadDescriptor& descriptor, NYTree::INodePtr node);
+void Deserialize(TWorkloadDescriptor& descriptor, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/node_tracker_client/helpers.cpp b/yt/yt/client/node_tracker_client/helpers.cpp
new file mode 100644
index 0000000000..6f9d7b5365
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/helpers.cpp
@@ -0,0 +1,58 @@
+#include "helpers.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt_proto/yt/client/node_tracker_client/proto/node.pb.h>
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYPath::TYPath GetClusterNodesPath()
+{
+ return "//sys/cluster_nodes";
+}
+
+NYPath::TYPath GetExecNodesPath()
+{
+ return "//sys/exec_nodes";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+void FormatValue(TStringBuilderBase* builder, const TDiskLocationResources& locationResources, TStringBuf /*spec*/)
+{
+ builder->AppendFormat(
+ "{usage: %v, limit: %v, medium_index: %v}",
+ locationResources.usage(),
+ locationResources.limit(),
+ locationResources.medium_index());
+}
+
+TString ToString(const TDiskLocationResources& locationResources)
+{
+ return ToStringViaBuilder(locationResources);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TDiskResources& diskResources, TStringBuf /*spec*/)
+{
+ builder->AppendFormat(
+ "%v",
+ MakeFormattableView(diskResources.disk_location_resources(), [] (TStringBuilderBase* builder, const TDiskLocationResources& diskLocationResources) {
+ FormatValue(builder, diskLocationResources, "%v");
+ }));
+}
+
+TString ToString(const TDiskResources& diskResources)
+{
+ return ToStringViaBuilder(diskResources);
+}
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
+
diff --git a/yt/yt/client/node_tracker_client/helpers.h b/yt/yt/client/node_tracker_client/helpers.h
new file mode 100644
index 0000000000..43e17db4d6
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/helpers.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYPath::TYPath GetClusterNodesPath();
+
+NYPath::TYPath GetExecNodesPath();
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+void FormatValue(TStringBuilderBase* builder, const TDiskLocationResources& locationResources, TStringBuf spec);
+TString ToString(const TDiskLocationResources& locationResources);
+
+void FormatValue(TStringBuilderBase* builder, const TDiskResources& diskResources, TStringBuf spec);
+TString ToString(const TDiskResources& diskResources);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
diff --git a/yt/yt/client/node_tracker_client/node_directory.cpp b/yt/yt/client/node_tracker_client/node_directory.cpp
new file mode 100644
index 0000000000..90684e3143
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/node_directory.cpp
@@ -0,0 +1,751 @@
+#include "node_directory.h"
+#include "private.h"
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt_proto/yt/client/node_tracker_client/proto/node_directory.pb.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+#include <util/digest/numeric.h>
+
+namespace NYT::NNodeTrackerClient {
+
+using namespace NChunkClient;
+using namespace NYson;
+using namespace NYTree;
+using namespace NNet;
+using namespace NRpc;
+using namespace NConcurrency;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = NodeTrackerClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& NullNodeAddress()
+{
+ static const TString Result("<null>");
+ return Result;
+}
+
+const TNodeDescriptor& NullNodeDescriptor()
+{
+ static const TNodeDescriptor Result(NullNodeAddress());
+ return Result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+constexpr int TypicalTagCount = 16;
+
+// Cf. YT-10645
+TCompactVector<TStringBuf, TypicalTagCount> GetSortedTags(const std::vector<TString>& tags)
+{
+ TCompactVector<TStringBuf, TypicalTagCount> result;
+ result.reserve(tags.size());
+ for (const auto& tag : tags) {
+ result.push_back(tag);
+ }
+ std::sort(result.begin(), result.end());
+ return result;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNodeDescriptor::TNodeDescriptor()
+ : DefaultAddress_(NullNodeAddress())
+{ }
+
+TNodeDescriptor::TNodeDescriptor(const TString& defaultAddress)
+ : Addresses_{std::make_pair(DefaultNetworkName, defaultAddress)}
+ , DefaultAddress_(defaultAddress)
+{ }
+
+TNodeDescriptor::TNodeDescriptor(const std::optional<TString>& defaultAddress)
+{
+ if (defaultAddress) {
+ *this = TNodeDescriptor(*defaultAddress);
+ }
+}
+
+TNodeDescriptor::TNodeDescriptor(
+ TAddressMap addresses,
+ std::optional<TString> host,
+ std::optional<TString> rack,
+ std::optional<TString> dc,
+ const std::vector<TString>& tags,
+ std::optional<TInstant> lastSeenTime)
+ : Addresses_(std::move(addresses))
+ , DefaultAddress_(NNodeTrackerClient::GetDefaultAddress(Addresses_))
+ , Host_(std::move(host))
+ , Rack_(std::move(rack))
+ , DataCenter_(std::move(dc))
+ , Tags_(tags)
+ , LastSeenTime_(lastSeenTime ? InstantToCpuInstant(*lastSeenTime) : 0)
+{ }
+
+bool TNodeDescriptor::IsNull() const
+{
+ return Addresses_.empty();
+}
+
+const TAddressMap& TNodeDescriptor::Addresses() const
+{
+ return Addresses_;
+}
+
+const TString& TNodeDescriptor::GetDefaultAddress() const
+{
+ return DefaultAddress_;
+}
+
+const TString& TNodeDescriptor::GetAddressOrThrow(const TNetworkPreferenceList& networks) const
+{
+ return NNodeTrackerClient::GetAddressOrThrow(Addresses(), networks);
+}
+
+std::optional<TString> TNodeDescriptor::FindAddress(const TNetworkPreferenceList& networks) const
+{
+ return NNodeTrackerClient::FindAddress(Addresses(), networks);
+}
+
+const std::optional<TString>& TNodeDescriptor::GetHost() const
+{
+ return Host_;
+}
+
+const std::optional<TString>& TNodeDescriptor::GetRack() const
+{
+ return Rack_;
+}
+
+const std::optional<TString>& TNodeDescriptor::GetDataCenter() const
+{
+ return DataCenter_;
+}
+
+const std::vector<TString>& TNodeDescriptor::GetTags() const
+{
+ return Tags_;
+}
+
+std::optional<TInstant> TNodeDescriptor::GetLastSeenTime() const
+{
+ auto cpuTime = LastSeenTime_.Load();
+ if (cpuTime != 0) {
+ return CpuInstantToInstant(cpuTime);
+ } else {
+ return {};
+ }
+}
+
+void TNodeDescriptor::UpdateLastSeenTime(TInstant at) const
+{
+ auto cpuTime = InstantToCpuInstant(at);
+ if (auto currentTime = LastSeenTime_.Load(); cpuTime > currentTime) {
+ LastSeenTime_.Store(cpuTime);
+ }
+}
+
+void TNodeDescriptor::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist(context, Addresses_);
+ Persist(context, Host_);
+ Persist(context, Rack_);
+ Persist(context, DataCenter_);
+
+ if (context.IsLoad()) {
+ DefaultAddress_ = NNodeTrackerClient::GetDefaultAddress(Addresses_);
+ }
+}
+
+void FormatValue(TStringBuilderBase* builder, const TNodeDescriptor& descriptor, TStringBuf /*spec*/)
+{
+ if (descriptor.IsNull()) {
+ builder->AppendString(NullNodeAddress());
+ return;
+ }
+
+ builder->AppendString(descriptor.GetDefaultAddress());
+ if (const auto& host = descriptor.GetHost()) {
+ builder->AppendChar('$');
+ builder->AppendString(*host);
+ }
+ if (const auto& rack = descriptor.GetRack()) {
+ builder->AppendChar('@');
+ builder->AppendString(*rack);
+ }
+ if (const auto& dataCenter = descriptor.GetDataCenter()) {
+ builder->AppendChar('#');
+ builder->AppendString(*dataCenter);
+ }
+}
+
+TString ToString(const TNodeDescriptor& descriptor)
+{
+ return ToStringViaBuilder(descriptor);
+}
+
+std::optional<TString> FindDefaultAddress(const TAddressMap& addresses)
+{
+ if (addresses.empty()) {
+ return NullNodeAddress();
+ }
+
+ if (auto it = addresses.find(DefaultNetworkName); it != addresses.end()) {
+ return it->second;
+ }
+ return std::nullopt;
+}
+
+const TString& GetDefaultAddress(const TAddressMap& addresses)
+{
+ if (addresses.empty()) {
+ return NullNodeAddress();
+ }
+ return GetOrCrash(addresses, DefaultNetworkName);
+}
+
+const TString& GetDefaultAddress(const NProto::TAddressMap& addresses)
+{
+ if (addresses.entries_size() == 0) {
+ return NullNodeAddress();
+ }
+ for (const auto& entry : addresses.entries()) {
+ if (entry.network() == DefaultNetworkName) {
+ return entry.address();
+ }
+ }
+ YT_ABORT();
+}
+
+EAddressLocality ComputeAddressLocality(const TNodeDescriptor& first, const TNodeDescriptor& second)
+{
+ if (first.IsNull() || second.IsNull()) {
+ return EAddressLocality::None;
+ };
+
+ try {
+ // COMPAT(gritukan)
+ if (GetServiceHostName(first.GetDefaultAddress()) == GetServiceHostName(second.GetDefaultAddress())) {
+ return EAddressLocality::SameHost;
+ }
+
+ if (first.GetHost() && second.GetHost() && *first.GetHost() == *second.GetHost()) {
+ return EAddressLocality::SameHost;
+ }
+
+ if (first.GetRack() && second.GetRack() && *first.GetRack() == *second.GetRack()) {
+ return EAddressLocality::SameRack;
+ }
+
+ if (first.GetDataCenter() && second.GetDataCenter() && *first.GetDataCenter() == *second.GetDataCenter()) {
+ return EAddressLocality::SameDataCenter;
+ }
+ } catch (const std::exception&) {
+ // If one of the descriptors is malformed, treat it as None locality and ignore errors.
+ }
+
+ return EAddressLocality::None;
+}
+
+namespace NProto {
+
+void ToProto(NNodeTrackerClient::NProto::TAddressMap* protoAddresses, const NNodeTrackerClient::TAddressMap& addresses)
+{
+ for (const auto& [networkName, networkAddress] : addresses) {
+ auto* entry = protoAddresses->add_entries();
+ entry->set_network(networkName);
+ entry->set_address(networkAddress);
+ }
+}
+
+void FromProto(NNodeTrackerClient::TAddressMap* addresses, const NNodeTrackerClient::NProto::TAddressMap& protoAddresses)
+{
+ addresses->clear();
+ addresses->reserve(protoAddresses.entries_size());
+ for (const auto& entry : protoAddresses.entries()) {
+ YT_VERIFY(addresses->emplace(entry.network(), entry.address()).second);
+ }
+}
+
+void ToProto(NNodeTrackerClient::NProto::TNodeAddressMap* proto, const NNodeTrackerClient::TNodeAddressMap& nodeAddresses)
+{
+ for (const auto& [addressType, addresses] : nodeAddresses) {
+ auto* entry = proto->add_entries();
+ entry->set_address_type(static_cast<int>(addressType));
+ ToProto(entry->mutable_addresses(), addresses);
+ }
+}
+
+void FromProto(NNodeTrackerClient::TNodeAddressMap* nodeAddresses, const NNodeTrackerClient::NProto::TNodeAddressMap& proto)
+{
+ nodeAddresses->clear();
+ nodeAddresses->reserve(proto.entries_size());
+ for (const auto& entry : proto.entries()) {
+ NNodeTrackerClient::TAddressMap addresses;
+ FromProto(&addresses, entry.addresses());
+
+ YT_VERIFY(nodeAddresses->emplace(static_cast<EAddressType>(entry.address_type()), std::move(addresses)).second);
+ }
+}
+
+void ToProto(NNodeTrackerClient::NProto::TNodeDescriptor* protoDescriptor, const NNodeTrackerClient::TNodeDescriptor& descriptor)
+{
+ using NYT::ToProto;
+
+ ToProto(protoDescriptor->mutable_addresses(), descriptor.Addresses());
+
+ if (auto host = descriptor.GetHost()) {
+ protoDescriptor->set_host(*host);
+ } else {
+ protoDescriptor->clear_host();
+ }
+
+ if (auto rack = descriptor.GetRack()) {
+ protoDescriptor->set_rack(*rack);
+ } else {
+ protoDescriptor->clear_rack();
+ }
+
+ if (auto dataCenter = descriptor.GetDataCenter()) {
+ protoDescriptor->set_data_center(*dataCenter);
+ } else {
+ protoDescriptor->clear_data_center();
+ }
+
+ ToProto(protoDescriptor->mutable_tags(), descriptor.GetTags());
+
+ if (auto lastHeartbeatTime = descriptor.GetLastSeenTime()) {
+ protoDescriptor->set_last_seen_time(ToProto<i64>(*lastHeartbeatTime));
+ } else {
+ protoDescriptor->clear_last_seen_time();
+ }
+}
+
+void FromProto(NNodeTrackerClient::TNodeDescriptor* descriptor, const NNodeTrackerClient::NProto::TNodeDescriptor& protoDescriptor)
+{
+ using NYT::FromProto;
+
+ *descriptor = NNodeTrackerClient::TNodeDescriptor(
+ FromProto<NNodeTrackerClient::TAddressMap>(protoDescriptor.addresses()),
+ protoDescriptor.has_host() ? std::make_optional(protoDescriptor.host()) : std::nullopt,
+ protoDescriptor.has_rack() ? std::make_optional(protoDescriptor.rack()) : std::nullopt,
+ protoDescriptor.has_data_center() ? std::make_optional(protoDescriptor.data_center()) : std::nullopt,
+ FromProto<std::vector<TString>>(protoDescriptor.tags()),
+ protoDescriptor.has_last_seen_time() ? std::make_optional(FromProto<TInstant>(protoDescriptor.last_seen_time())) : std::nullopt);
+}
+
+} // namespace NProto
+
+bool operator == (const TNodeDescriptor& lhs, const TNodeDescriptor& rhs)
+{
+ return
+ lhs.GetDefaultAddress() == rhs.GetDefaultAddress() && // shortcut
+ lhs.Addresses() == rhs.Addresses() &&
+ lhs.GetHost() == rhs.GetHost() &&
+ lhs.GetRack() == rhs.GetRack() &&
+ lhs.GetDataCenter() == rhs.GetDataCenter() &&
+ GetSortedTags(lhs.GetTags()) == GetSortedTags(rhs.GetTags());
+}
+
+bool operator != (const TNodeDescriptor& lhs, const TNodeDescriptor& rhs)
+{
+ return !(lhs == rhs);
+}
+
+bool operator == (const TNodeDescriptor& lhs, const NProto::TNodeDescriptor& rhs)
+{
+ if (std::ssize(lhs.Addresses()) != rhs.addresses().entries_size()) {
+ return false;
+ }
+
+ for (const auto& protoEntry : rhs.addresses().entries()) {
+ const auto& network = protoEntry.network();
+ const auto& address = protoEntry.address();
+ auto it = lhs.Addresses().find(network);
+ if (it == lhs.Addresses().end()) {
+ return false;
+ }
+ if (it->second != address) {
+ return false;
+ }
+ }
+
+ const auto& lhsMaybeHost = lhs.GetHost();
+ auto lhsHost = lhsMaybeHost ? TStringBuf(*lhsMaybeHost) : TStringBuf();
+ if (lhsHost != rhs.host()) {
+ return false;
+ }
+
+ const auto& lhsMaybeRack = lhs.GetRack();
+ auto lhsRack = lhsMaybeRack ? TStringBuf(*lhsMaybeRack) : TStringBuf();
+ if (lhsRack != rhs.rack()) {
+ return false;
+ }
+
+ const auto& lhsMaybeDataCenter = lhs.GetDataCenter();
+ auto lhsDataCenter = lhsMaybeDataCenter ? TStringBuf(*lhsMaybeDataCenter) : TStringBuf();
+ if (lhsDataCenter != rhs.data_center()) {
+ return false;
+ }
+
+ const auto& lhsTags = lhs.GetTags();
+ auto rhsTags = FromProto<std::vector<TString>>(rhs.tags());
+ if (GetSortedTags(lhsTags) != GetSortedTags(rhsTags)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool operator != (const TNodeDescriptor& lhs, const NProto::TNodeDescriptor& rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TNodeDirectory::MergeFrom(const NProto::TNodeDirectory& source)
+{
+ std::vector<const NProto::TNodeDirectory_TItem*> items;
+ items.reserve(source.items_size());
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ for (const auto& item : source.items()) {
+ if (CheckNodeDescriptor(item.node_id(), item.node_descriptor())) {
+ items.push_back(&item);
+ }
+ }
+ }
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+ for (const auto* item : items) {
+ DoAddDescriptor(item->node_id(), item->node_descriptor());
+ }
+ }
+}
+
+void TNodeDirectory::MergeFrom(const TNodeDirectoryPtr& source)
+{
+ if (this == source.Get()) {
+ return;
+ }
+
+ std::vector<std::pair<TNodeId, TNodeDescriptor>> items;
+ {
+ auto thisGuard = WriterGuard(SpinLock_);
+ auto sourceGuard = ReaderGuard(source->SpinLock_);
+ items.reserve(source->IdToDescriptor_.size());
+ for (auto [id, descriptor] : source->IdToDescriptor_) {
+ if (CheckNodeDescriptor(id, *descriptor)) {
+ items.emplace_back(id, *descriptor);
+ }
+ }
+ }
+ {
+ auto thisGuard = WriterGuard(SpinLock_);
+ for (const auto& [id, descriptor] : items) {
+ DoAddDescriptor(id, descriptor);
+ }
+ }
+}
+
+void TNodeDirectory::DumpTo(NProto::TNodeDirectory* destination)
+{
+ auto guard = ReaderGuard(SpinLock_);
+ for (auto [id, descriptor] : IdToDescriptor_) {
+ auto* item = destination->add_items();
+ item->set_node_id(id);
+ ToProto(item->mutable_node_descriptor(), *descriptor);
+ }
+}
+
+void TNodeDirectory::Serialize(IYsonConsumer* consumer) const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ BuildYsonFluently(consumer)
+ .BeginList()
+ .DoFor(IdToDescriptor_, [&] (TFluentList fluent, const std::pair<TNodeId, const TNodeDescriptor*>& pair) {
+ fluent
+ .Item()
+ .BeginMap()
+ .Item("node_id").Value(pair.first)
+ .Item("addresses").Value(pair.second->Addresses())
+ .EndMap();
+ })
+ .EndList();
+}
+
+void Serialize(const TNodeDirectory& nodeDirectory, NYson::IYsonConsumer* consumer)
+{
+ nodeDirectory.Serialize(consumer);
+}
+
+void TNodeDirectory::AddDescriptor(TNodeId id, const TNodeDescriptor& descriptor)
+{
+ auto guard = WriterGuard(SpinLock_);
+ DoAddDescriptor(id, descriptor);
+}
+
+bool TNodeDirectory::CheckNodeDescriptor(TNodeId id, const TNodeDescriptor& descriptor)
+{
+ auto it = IdToDescriptor_.find(id);
+ if (it == IdToDescriptor_.end()) {
+ return true;
+ }
+
+ if (auto lastSeenOnline = descriptor.GetLastSeenTime()) {
+ it->second->UpdateLastSeenTime(*lastSeenOnline);
+ }
+
+ return *it->second != descriptor;
+}
+
+void TNodeDirectory::DoAddDescriptor(TNodeId id, const TNodeDescriptor& descriptor)
+{
+ if (!CheckNodeDescriptor(id, descriptor)) {
+ return;
+ }
+ DoCaptureAndAddDescriptor(id, TNodeDescriptor(descriptor));
+}
+
+bool TNodeDirectory::CheckNodeDescriptor(TNodeId id, const NProto::TNodeDescriptor& descriptor)
+{
+ auto it = IdToDescriptor_.find(id);
+ if (it == IdToDescriptor_.end()) {
+ return true;
+ }
+
+ if (descriptor.has_last_seen_time()) {
+ it->second->UpdateLastSeenTime(FromProto<TInstant>(descriptor.last_seen_time()));
+ }
+
+ return *it->second != descriptor;
+}
+
+void TNodeDirectory::DoAddDescriptor(TNodeId id, const NProto::TNodeDescriptor& protoDescriptor)
+{
+ if (!CheckNodeDescriptor(id, protoDescriptor)) {
+ return;
+ }
+ DoCaptureAndAddDescriptor(id, FromProto<TNodeDescriptor>(protoDescriptor));
+}
+
+void TNodeDirectory::DoCaptureAndAddDescriptor(TNodeId id, TNodeDescriptor&& descriptor)
+{
+ auto it = Descriptors_.find(descriptor);
+ if (it == Descriptors_.end()) {
+ it = Descriptors_.insert(std::move(descriptor)).first;
+ }
+ const auto* capturedDescriptor = &*it;
+ IdToDescriptor_[id] = capturedDescriptor;
+ AddressToDescriptor_[capturedDescriptor->GetDefaultAddress()] = capturedDescriptor;
+
+ OnDescriptorAdded(id, capturedDescriptor);
+}
+
+void TNodeDirectory::OnDescriptorAdded(TNodeId id, const TNodeDescriptor* descriptor)
+{
+ if (auto it = IdToPromise_.find(id); it != IdToPromise_.end()) {
+ it->second.TrySet(descriptor);
+ YT_LOG_DEBUG("Awaited node descriptor added (NodeId: %v, NodeAddress: %v)", id, descriptor->GetDefaultAddress());
+ IdToPromise_.erase(it);
+ }
+}
+
+const TNodeDescriptor* TNodeDirectory::FindDescriptor(TNodeId id) const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ auto it = IdToDescriptor_.find(id);
+ return it == IdToDescriptor_.end() ? nullptr : it->second;
+}
+
+const TNodeDescriptor& TNodeDirectory::GetDescriptor(TNodeId id) const
+{
+ const auto* result = FindDescriptor(id);
+ YT_VERIFY(result);
+ return *result;
+}
+
+TFuture<const TNodeDescriptor*> TNodeDirectory::GetAsyncDescriptor(TNodeId id)
+{
+ if (auto* descriptor = FindDescriptor(id)) {
+ return MakeFuture(descriptor);
+ }
+
+ TPromise<const TNodeDescriptor*> promise;
+ {
+ YT_LOG_DEBUG("Waiting for node descriptor (NodeId: %v)", id);
+ auto guard = WriterGuard(SpinLock_);
+ if (auto it = IdToPromise_.find(id); it != IdToPromise_.end()) {
+ promise = it->second;
+ } else {
+ promise = IdToPromise_[id] = NewPromise<const TNodeDescriptor*>();
+ }
+ }
+
+ return promise.ToFuture().ToUncancelable();
+}
+
+const TNodeDescriptor& TNodeDirectory::GetDescriptor(TChunkReplica replica) const
+{
+ return GetDescriptor(replica.GetNodeId());
+}
+
+std::vector<TNodeDescriptor> TNodeDirectory::GetDescriptors(const TChunkReplicaList& replicas) const
+{
+ std::vector<TNodeDescriptor> result;
+ for (auto replica : replicas) {
+ result.push_back(GetDescriptor(replica));
+ }
+ return result;
+}
+
+std::vector<std::pair<TNodeId, TNodeDescriptor>> TNodeDirectory::GetAllDescriptors() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ std::vector<std::pair<TNodeId, TNodeDescriptor>> result;
+ result.reserve(IdToDescriptor_.size());
+ for (auto [id, descriptor] : IdToDescriptor_) {
+ result.emplace_back(id, *descriptor);
+ }
+ return result;
+}
+
+const TNodeDescriptor* TNodeDirectory::FindDescriptor(const TString& address)
+{
+ auto guard = ReaderGuard(SpinLock_);
+ auto it = AddressToDescriptor_.find(address);
+ return it == AddressToDescriptor_.end() ? nullptr : it->second;
+}
+
+const TNodeDescriptor& TNodeDirectory::GetDescriptor(const TString& address)
+{
+ const auto* result = FindDescriptor(address);
+ YT_VERIFY(result);
+ return *result;
+}
+
+void TNodeDirectory::Save(TStreamSaveContext& context) const
+{
+ THashMap<TNodeId, TNodeDescriptor> idToDescriptor;
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ for (auto [id, descriptor] : IdToDescriptor_) {
+ YT_VERIFY(idToDescriptor.emplace(id, *descriptor).second);
+ }
+ }
+ using NYT::Save;
+ Save(context, idToDescriptor);
+}
+
+void TNodeDirectory::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ auto idToDescriptor = Load<THashMap<TNodeId, TNodeDescriptor>>(context);
+ auto guard = WriterGuard(SpinLock_);
+ for (const auto& [id, descriptor] : idToDescriptor) {
+ DoAddDescriptor(id, descriptor);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TAddressMap::const_iterator SelectAddress(const TAddressMap& addresses, const TNetworkPreferenceList& networks)
+{
+ for (const auto& network : networks) {
+ const auto it = addresses.find(network);
+ if (it != addresses.cend()) {
+ return it;
+ }
+ }
+
+ return addresses.cend();
+}
+
+} // namespace
+
+std::optional<TString> FindAddress(const TAddressMap& addresses, const TNetworkPreferenceList& networks)
+{
+ const auto it = SelectAddress(addresses, networks);
+ return it == addresses.cend() ? std::nullopt : std::make_optional(it->second);
+}
+
+const TString& GetAddressOrThrow(const TAddressMap& addresses, const TNetworkPreferenceList& networks)
+{
+ const auto it = SelectAddress(addresses, networks);
+ if (it != addresses.cend()) {
+ return it->second;
+ }
+
+ THROW_ERROR_EXCEPTION("Cannot select address for host %v since there is no compatible network",
+ FindDefaultAddress(addresses))
+ << TErrorAttribute("remote_networks", GetKeys(addresses))
+ << TErrorAttribute("local_networks", networks);
+}
+
+const TAddressMap& GetAddressesOrThrow(const TNodeAddressMap& nodeAddresses, EAddressType type)
+{
+ auto it = nodeAddresses.find(type);
+ if (it != nodeAddresses.cend()) {
+ return it->second;
+ }
+
+ THROW_ERROR_EXCEPTION("No addresses known for address type %Qlv", type)
+ << TErrorAttribute("known_types", GetKeys(nodeAddresses));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t THash<NYT::NNodeTrackerClient::TNodeDescriptor>::operator()(
+ const NYT::NNodeTrackerClient::TNodeDescriptor& nodeDescriptor) const
+{
+ size_t result = 0;
+ using namespace NYT;
+ HashCombine(result, nodeDescriptor.GetDefaultAddress());
+ HashCombine(result, nodeDescriptor.GetHost());
+ HashCombine(result, nodeDescriptor.GetRack());
+ HashCombine(result, nodeDescriptor.GetDataCenter());
+ for (const auto& [network, address] : nodeDescriptor.Addresses()) {
+ HashCombine(result, network);
+ HashCombine(result, address);
+ }
+ for (const auto& tag : NYT::NNodeTrackerClient::GetSortedTags(nodeDescriptor.GetTags())) {
+ HashCombine(result, tag);
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/client/node_tracker_client/node_directory.h b/yt/yt/client/node_tracker_client/node_directory.h
new file mode 100644
index 0000000000..a76245fd8b
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/node_directory.h
@@ -0,0 +1,195 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/chunk_client/chunk_replica.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/rpc/helpers.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/misc/property.h>
+#include <yt/yt/core/misc/copyable_atomic.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Network-related node information.
+class TNodeDescriptor
+{
+public:
+ TNodeDescriptor();
+ TNodeDescriptor(const TNodeDescriptor& other) = default;
+ TNodeDescriptor(TNodeDescriptor&& other) = default;
+ explicit TNodeDescriptor(const TString& defaultAddress);
+ explicit TNodeDescriptor(const std::optional<TString>& defaultAddress);
+ explicit TNodeDescriptor(
+ TAddressMap addresses,
+ std::optional<TString> host = std::nullopt,
+ std::optional<TString> rack = std::nullopt,
+ std::optional<TString> dc = std::nullopt,
+ const std::vector<TString>& tags = {},
+ std::optional<TInstant> lastSeenTime = {});
+
+ TNodeDescriptor& operator=(const TNodeDescriptor& other) = default;
+ TNodeDescriptor& operator=(TNodeDescriptor&& other) = default;
+
+ bool IsNull() const;
+
+ const TAddressMap& Addresses() const;
+
+ const TString& GetDefaultAddress() const;
+
+ const TString& GetAddressOrThrow(const TNetworkPreferenceList& networks) const;
+
+ std::optional<TString> FindAddress(const TNetworkPreferenceList& networks) const;
+
+ const std::optional<TString>& GetHost() const;
+ const std::optional<TString>& GetRack() const;
+ const std::optional<TString>& GetDataCenter() const;
+
+ const std::vector<TString>& GetTags() const;
+
+ //! GetLastSeenTime returns last instant when node was seen online on some master.
+ /*!
+ * Might be used for cheap and dirty availability check.
+ * This field is not persisted.
+ */
+ std::optional<TInstant> GetLastSeenTime() const;
+ void UpdateLastSeenTime(TInstant at) const;
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ TAddressMap Addresses_;
+ TString DefaultAddress_;
+ std::optional<TString> Host_;
+ std::optional<TString> Rack_;
+ std::optional<TString> DataCenter_;
+ std::vector<TString> Tags_;
+
+ // Not persisted.
+ mutable TCopyableAtomic<TCpuInstant> LastSeenTime_;
+};
+
+const TString& NullNodeAddress();
+const TNodeDescriptor& NullNodeDescriptor();
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator == (const TNodeDescriptor& lhs, const TNodeDescriptor& rhs);
+bool operator != (const TNodeDescriptor& lhs, const TNodeDescriptor& rhs);
+
+bool operator == (const TNodeDescriptor& lhs, const NProto::TNodeDescriptor& rhs);
+bool operator != (const TNodeDescriptor& lhs, const NProto::TNodeDescriptor& rhs);
+
+void FormatValue(TStringBuilderBase* builder, const TNodeDescriptor& descriptor, TStringBuf spec);
+TString ToString(const TNodeDescriptor& descriptor);
+
+// Accessors for some well-known addresses.
+std::optional<TString> FindDefaultAddress(const TAddressMap& addresses);
+const TString& GetDefaultAddress(const TAddressMap& addresses);
+const TString& GetDefaultAddress(const NProto::TAddressMap& addresses);
+
+const TString& GetAddressOrThrow(const TAddressMap& addresses, const TNetworkPreferenceList& networks);
+std::optional<TString> FindAddress(const TAddressMap& addresses, const TNetworkPreferenceList& networks);
+
+const TAddressMap& GetAddressesOrThrow(const TNodeAddressMap& nodeAddresses, EAddressType type);
+
+//! Keep the items in this particular order: the further the better.
+DEFINE_ENUM(EAddressLocality,
+ (None)
+ (SameDataCenter)
+ (SameRack)
+ (SameHost)
+);
+
+EAddressLocality ComputeAddressLocality(const TNodeDescriptor& first, const TNodeDescriptor& second);
+
+namespace NProto {
+
+void ToProto(NNodeTrackerClient::NProto::TAddressMap* protoAddresses, const NNodeTrackerClient::TAddressMap& addresses);
+void FromProto(NNodeTrackerClient::TAddressMap* addresses, const NNodeTrackerClient::NProto::TAddressMap& protoAddresses);
+
+void ToProto(NNodeTrackerClient::NProto::TNodeAddressMap* proto, const NNodeTrackerClient::TNodeAddressMap& nodeAddresses);
+void FromProto(NNodeTrackerClient::TNodeAddressMap* nodeAddresses, const NNodeTrackerClient::NProto::TNodeAddressMap& proto);
+
+void ToProto(NNodeTrackerClient::NProto::TNodeDescriptor* protoDescriptor, const NNodeTrackerClient::TNodeDescriptor& descriptor);
+void FromProto(NNodeTrackerClient::TNodeDescriptor* descriptor, const NNodeTrackerClient::NProto::TNodeDescriptor& protoDescriptor);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
+
+template <>
+struct THash<NYT::NNodeTrackerClient::TNodeDescriptor>
+{
+ size_t operator()(const NYT::NNodeTrackerClient::TNodeDescriptor& value) const;
+};
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Caches node descriptors obtained by fetch requests.
+/*!
+ * \note
+ * Thread affinity: thread-safe
+ */
+class TNodeDirectory
+ : public TRefCounted
+{
+public:
+ void MergeFrom(const NProto::TNodeDirectory& source);
+ void MergeFrom(const TNodeDirectoryPtr& source);
+ void DumpTo(NProto::TNodeDirectory* destination);
+ void Serialize(NYson::IYsonConsumer* consumer) const;
+
+ void AddDescriptor(TNodeId id, const TNodeDescriptor& descriptor);
+
+ const TNodeDescriptor* FindDescriptor(TNodeId id) const;
+ const TNodeDescriptor& GetDescriptor(TNodeId id) const;
+ TFuture<const TNodeDescriptor*> GetAsyncDescriptor(TNodeId id);
+ const TNodeDescriptor& GetDescriptor(NChunkClient::TChunkReplica replica) const;
+ std::vector<TNodeDescriptor> GetDescriptors(const NChunkClient::TChunkReplicaList& replicas) const;
+ std::vector<std::pair<NNodeTrackerClient::TNodeId, TNodeDescriptor>> GetAllDescriptors() const;
+
+ const TNodeDescriptor* FindDescriptor(const TString& address);
+ const TNodeDescriptor& GetDescriptor(const TString& address);
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ THashMap<TNodeId, const TNodeDescriptor*> IdToDescriptor_;
+ THashMap<TString, const TNodeDescriptor*> AddressToDescriptor_;
+ THashSet<TNodeDescriptor> Descriptors_;
+
+ THashMap<TNodeId, TPromise<const TNodeDescriptor*>> IdToPromise_;
+
+ bool CheckNodeDescriptor(TNodeId id, const TNodeDescriptor& descriptor);
+ void DoAddDescriptor(TNodeId id, const TNodeDescriptor& descriptor);
+ bool CheckNodeDescriptor(TNodeId id, const NProto::TNodeDescriptor& descriptor);
+ void DoAddDescriptor(TNodeId id, const NProto::TNodeDescriptor& protoDescriptor);
+ void DoCaptureAndAddDescriptor(TNodeId id, TNodeDescriptor&& descriptorHolder);
+
+ void OnDescriptorAdded(TNodeId id, const TNodeDescriptor* descriptor);
+};
+
+void Serialize(const TNodeDirectory& nodeDirectory, NYson::IYsonConsumer* consumer);
+
+DEFINE_REFCOUNTED_TYPE(TNodeDirectory)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
diff --git a/yt/yt/client/node_tracker_client/private.h b/yt/yt/client/node_tracker_client/private.h
new file mode 100644
index 0000000000..162e8bd259
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger NodeTrackerClientLogger("NodeTrackerClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
diff --git a/yt/yt/client/node_tracker_client/public.cpp b/yt/yt/client/node_tracker_client/public.cpp
new file mode 100644
index 0000000000..0ffc8e4ee8
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/public.cpp
@@ -0,0 +1,13 @@
+#include "public.h"
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString DefaultNetworkName("default");
+const TNetworkPreferenceList DefaultNetworkPreferences{DefaultNetworkName};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
+
diff --git a/yt/yt/client/node_tracker_client/public.h b/yt/yt/client/node_tracker_client/public.h
new file mode 100644
index 0000000000..5909215aa8
--- /dev/null
+++ b/yt/yt/client/node_tracker_client/public.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+namespace NYT::NNodeTrackerClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TNodeStatistics;
+class TNodeResources;
+class TNodeResourceLimitsOverrides;
+
+class TDiskResources;
+class TDiskLocationResources;
+
+class TAddressMap;
+class TNodeAddressMap;
+
+class TNodeDescriptor;
+class TNodeDirectory;
+
+} // namespace NProto
+
+YT_DEFINE_ERROR_ENUM(
+ ((NoSuchNode) (1600))
+ ((InvalidState) (1601))
+ ((NoSuchNetwork) (1602))
+ ((NoSuchRack) (1603))
+ ((NoSuchDataCenter) (1604))
+);
+
+DEFINE_ENUM(EAddressType,
+ ((InternalRpc) (0))
+ ((SkynetHttp) (1))
+ ((MonitoringHttp) (2))
+);
+
+using TNodeId = ui32;
+constexpr TNodeId InvalidNodeId = 0;
+constexpr TNodeId MaxNodeId = (1 << 24) - 1; // TNodeId must fit into 24 bits (see TChunkReplica)
+
+using THostId = NObjectClient::TObjectId;
+using TRackId = NObjectClient::TObjectId;
+using TDataCenterId = NObjectClient::TObjectId;
+
+// Only domain names, without port number.
+using TNetworkAddressList = std::vector<std::pair<TString, TString>>;
+using TNetworkPreferenceList = std::vector<TString>;
+
+// Network -> host:port.
+using TAddressMap = THashMap<TString, TString>;
+
+// Address type (e.g. RPC, HTTP) -> network -> host:port.
+using TNodeAddressMap = THashMap<EAddressType, TAddressMap>;
+
+DECLARE_REFCOUNTED_CLASS(TNodeDirectory)
+class TNodeDescriptor;
+
+extern const TString DefaultNetworkName;
+extern const TNetworkPreferenceList DefaultNetworkPreferences;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNodeTrackerClient
diff --git a/yt/yt/client/object_client/helpers-inl.h b/yt/yt/client/object_client/helpers-inl.h
new file mode 100644
index 0000000000..9297fec91d
--- /dev/null
+++ b/yt/yt/client/object_client/helpers-inl.h
@@ -0,0 +1,186 @@
+#ifndef HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+
+#include <util/random/random.h>
+
+namespace NYT::NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline EObjectType TypeFromId(TObjectId id)
+{
+ return EObjectType(id.Parts32[1] & 0xffff);
+}
+
+inline TCellTag CellTagFromId(TObjectId id)
+{
+ return TCellTag(id.Parts32[1] >> 16);
+}
+
+inline ui64 CounterFromId(TObjectId id)
+{
+ ui64 result;
+ result = id.Parts32[3];
+ result <<= 32;
+ result |= id.Parts32[2];
+ return result;
+}
+
+inline ui32 HashFromId(TObjectId id)
+{
+ return id.Parts32[0];
+}
+
+inline NHydra::TVersion VersionFromId(TObjectId id)
+{
+ YT_ASSERT(!IsSequoiaId(id));
+ return NHydra::TVersion::FromRevision(CounterFromId(id));
+}
+
+inline NTransactionClient::TTimestamp TimestampFromId(TObjectId id)
+{
+ YT_ASSERT(IsSequoiaId(id));
+ return CounterFromId(id) & ~SequoiaCounterMask;
+}
+
+inline EObjectType SchemaTypeFromType(EObjectType type)
+{
+ YT_ASSERT(HasSchema(type));
+ return EObjectType(static_cast<int>(type) | SchemaObjectTypeMask);
+}
+
+inline EObjectType TypeFromSchemaType(EObjectType type)
+{
+ YT_ASSERT(static_cast<int>(type) & SchemaObjectTypeMask);
+ return EObjectType(static_cast<int>(type) & ~SchemaObjectTypeMask);
+}
+
+inline TObjectId MakeId(
+ EObjectType type,
+ TCellTag cellTag,
+ ui64 counter,
+ ui32 hash)
+{
+ return TObjectId(
+ hash,
+ (static_cast<ui32>(cellTag.Underlying()) << 16) | static_cast<ui32>(type),
+ counter & 0xffffffff,
+ counter >> 32);
+}
+
+inline TObjectId MakeRandomId(
+ EObjectType type,
+ TCellTag cellTag)
+{
+ return MakeId(
+ type,
+ cellTag,
+ RandomNumber<ui64>(),
+ RandomNumber<ui32>());
+}
+
+inline bool IsWellKnownId(TObjectId id)
+{
+ return CounterFromId(id) & WellKnownCounterMask;
+}
+
+inline bool IsSequoiaId(TObjectId id)
+{
+ // NB: Well-known objects have Sequoia bit set.
+ return (CounterFromId(id) & SequoiaCounterMask) && !IsWellKnownId(id);
+}
+
+inline TObjectId MakeRegularId(
+ EObjectType type,
+ TCellTag cellTag,
+ NHydra::TVersion version,
+ ui32 hash)
+{
+ return TObjectId(
+ hash,
+ (static_cast<ui32>(cellTag.Underlying()) << 16) | static_cast<ui32>(type),
+ version.RecordId,
+ version.SegmentId);
+}
+
+inline TObjectId MakeSequoiaId(
+ EObjectType type,
+ TCellTag cellTag,
+ NTransactionClient::TTimestamp timestamp,
+ ui32 hash)
+{
+ YT_ASSERT(!(timestamp & SequoiaCounterMask));
+ return MakeId(
+ type,
+ cellTag,
+ timestamp | SequoiaCounterMask,
+ hash);
+}
+
+inline TObjectId MakeWellKnownId(
+ EObjectType type,
+ TCellTag cellTag,
+ ui64 counter /*= 0xffffffffffffffff*/)
+{
+ YT_VERIFY(counter & WellKnownCounterMask);
+ return MakeId(
+ type,
+ cellTag,
+ counter,
+ static_cast<ui32>(cellTag.Underlying() * 901517) ^ 0x140a8383);
+}
+
+inline TObjectId MakeSchemaObjectId(
+ EObjectType type,
+ TCellTag cellTag)
+{
+ return MakeWellKnownId(SchemaTypeFromType(type), cellTag);
+}
+
+inline TObjectId ReplaceTypeInId(
+ TObjectId id,
+ EObjectType type)
+{
+ auto result = id;
+ result.Parts32[1] &= ~0x0000ffff;
+ result.Parts32[1] |= static_cast<ui32>(type);
+ return result;
+}
+
+inline TObjectId ReplaceCellTagInId(
+ TObjectId id,
+ TCellTag cellTag)
+{
+ auto result = id;
+ result.Parts32[1] &= ~0xffff0000;
+ result.Parts32[1] |= static_cast<ui32>(cellTag.Underlying()) << 16;
+ return result;
+}
+
+template <int ShardCount>
+inline int GetShardIndex(TObjectId id)
+{
+ static_assert(IsPowerOf2(ShardCount), "Number of shards must be a power of 2");
+ return TDirectObjectIdHash()(id) & (ShardCount - 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE size_t TDirectObjectIdHash::operator()(TObjectId id) const
+{
+ return id.Parts32[0];
+}
+
+Y_FORCE_INLINE size_t TDirectVersionedObjectIdHash::operator()(const TVersionedObjectId& id) const
+{
+ return
+ TDirectObjectIdHash()(id.TransactionId) * 497 +
+ TDirectObjectIdHash()(id.ObjectId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NObjectClient
diff --git a/yt/yt/client/object_client/helpers.cpp b/yt/yt/client/object_client/helpers.cpp
new file mode 100644
index 0000000000..1b2c857543
--- /dev/null
+++ b/yt/yt/client/object_client/helpers.cpp
@@ -0,0 +1,263 @@
+#include "helpers.h"
+
+#include <yt/yt/core/misc/guid.h>
+
+namespace NYT::NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TStringBuf ObjectIdPathPrefix("#");
+
+NYPath::TYPath FromObjectId(TObjectId id)
+{
+ return TString(ObjectIdPathPrefix) + ToString(id);
+}
+
+bool IsVersionedType(EObjectType type)
+{
+ return
+ type == EObjectType::StringNode ||
+ type == EObjectType::Int64Node ||
+ type == EObjectType::Uint64Node ||
+ type == EObjectType::DoubleNode ||
+ type == EObjectType::BooleanNode ||
+ type == EObjectType::MapNode ||
+ type == EObjectType::ListNode ||
+ type == EObjectType::File ||
+ type == EObjectType::Table ||
+ type == EObjectType::ReplicatedTable ||
+ type == EObjectType::ReplicationLogTable ||
+ type == EObjectType::Journal ||
+ type == EObjectType::ChunkMap ||
+ type == EObjectType::LostChunkMap ||
+ type == EObjectType::LostVitalChunkMap ||
+ type == EObjectType::PrecariousChunkMap ||
+ type == EObjectType::PrecariousVitalChunkMap ||
+ type == EObjectType::OverreplicatedChunkMap ||
+ type == EObjectType::UnderreplicatedChunkMap ||
+ type == EObjectType::DataMissingChunkMap ||
+ type == EObjectType::ParityMissingChunkMap ||
+ type == EObjectType::OldestPartMissingChunkMap ||
+ type == EObjectType::QuorumMissingChunkMap ||
+ type == EObjectType::UnsafelyPlacedChunkMap ||
+ type == EObjectType::InconsistentlyPlacedChunkMap ||
+ type == EObjectType::UnexpectedOverreplicatedChunkMap ||
+ type == EObjectType::ReplicaTemporarilyUnavailableChunkMap ||
+ type == EObjectType::ForeignChunkMap ||
+ type == EObjectType::LocalLostChunkMap ||
+ type == EObjectType::LocalLostVitalChunkMap ||
+ type == EObjectType::LocalPrecariousChunkMap ||
+ type == EObjectType::LocalPrecariousVitalChunkMap ||
+ type == EObjectType::LocalOverreplicatedChunkMap ||
+ type == EObjectType::LocalUnderreplicatedChunkMap ||
+ type == EObjectType::LocalDataMissingChunkMap ||
+ type == EObjectType::LocalParityMissingChunkMap ||
+ type == EObjectType::LocalOldestPartMissingChunkMap ||
+ type == EObjectType::LocalQuorumMissingChunkMap ||
+ type == EObjectType::LocalUnsafelyPlacedChunkMap ||
+ type == EObjectType::LocalInconsistentlyPlacedChunkMap ||
+ type == EObjectType::LocalUnexpectedOverreplicatedChunkMap ||
+ type == EObjectType::LocalReplicaTemporarilyUnavailableChunkMap ||
+ type == EObjectType::RackMap ||
+ type == EObjectType::DataCenterMap ||
+ type == EObjectType::HostMap ||
+ type == EObjectType::ChunkLocationMap ||
+ type == EObjectType::ChunkListMap ||
+ type == EObjectType::ChunkViewMap ||
+ type == EObjectType::MediumMap ||
+ type == EObjectType::TransactionMap ||
+ type == EObjectType::TopmostTransactionMap ||
+ type == EObjectType::ClusterNodeNode ||
+ type == EObjectType::LegacyClusterNodeMap ||
+ type == EObjectType::ClusterNodeMap ||
+ type == EObjectType::DataNodeMap ||
+ type == EObjectType::ExecNodeMap ||
+ type == EObjectType::TabletNodeMap ||
+ type == EObjectType::ChaosNodeMap ||
+ type == EObjectType::Orchid ||
+ type == EObjectType::AccountMap ||
+ type == EObjectType::UserMap ||
+ type == EObjectType::GroupMap ||
+ type == EObjectType::AccountResourceUsageLeaseMap ||
+ type == EObjectType::SchedulerPoolTreeMap ||
+ type == EObjectType::Link ||
+ type == EObjectType::Document ||
+ type == EObjectType::LockMap ||
+ type == EObjectType::TabletMap ||
+ type == EObjectType::TabletCellMap ||
+ type == EObjectType::TabletCellNode ||
+ type == EObjectType::TabletCellBundleMap ||
+ type == EObjectType::TabletActionMap ||
+ type == EObjectType::AreaMap ||
+ type == EObjectType::ChaosCellMap ||
+ type == EObjectType::ChaosCellBundleMap ||
+ type == EObjectType::SysNode ||
+ type == EObjectType::PortalEntrance ||
+ type == EObjectType::PortalExit ||
+ type == EObjectType::PortalEntranceMap ||
+ type == EObjectType::PortalExitMap ||
+ type == EObjectType::CypressShardMap ||
+ type == EObjectType::EstimatedCreationTimeMap ||
+ type == EObjectType::NetworkProjectMap ||
+ type == EObjectType::HttpProxyRoleMap ||
+ type == EObjectType::RpcProxyRoleMap ||
+ type == EObjectType::MasterTableSchemaMap ||
+ type == EObjectType::ChaosReplicatedTable ||
+ type == EObjectType::AccessControlObjectNamespaceMap ||
+ type == EObjectType::HunkStorage ||
+ type == EObjectType::ZookeeperShardMap ||
+ type == EObjectType::Rootstock ||
+ type == EObjectType::RootstockMap ||
+ type == EObjectType::Scion ||
+ type == EObjectType::ScionMap;
+}
+
+bool IsUserType(EObjectType type)
+{
+ return
+ type == EObjectType::Transaction ||
+ type == EObjectType::Chunk ||
+ type == EObjectType::JournalChunk ||
+ type == EObjectType::ErasureChunk ||
+ type == EObjectType::ErasureJournalChunk ||
+ type == EObjectType::ChunkList ||
+ type == EObjectType::StringNode ||
+ type == EObjectType::Int64Node ||
+ type == EObjectType::Uint64Node ||
+ type == EObjectType::DoubleNode ||
+ type == EObjectType::BooleanNode ||
+ type == EObjectType::MapNode ||
+ type == EObjectType::ListNode ||
+ type == EObjectType::File ||
+ type == EObjectType::Table ||
+ type == EObjectType::ReplicatedTable ||
+ type == EObjectType::ReplicationLogTable ||
+ type == EObjectType::TableReplica ||
+ type == EObjectType::TabletAction ||
+ type == EObjectType::Journal ||
+ type == EObjectType::Link ||
+ type == EObjectType::AccessControlObject ||
+ type == EObjectType::Document ||
+ type == EObjectType::Account ||
+ type == EObjectType::SchedulerPool ||
+ type == EObjectType::SchedulerPoolTree ||
+ type == EObjectType::ChaosReplicatedTable ||
+ type == EObjectType::HunkStorage;
+}
+
+bool IsSchemafulType(EObjectType type)
+{
+ return
+ IsTableType(type) ||
+ type == EObjectType::ChaosReplicatedTable;
+}
+
+bool IsTableType(EObjectType type)
+{
+ return
+ type == EObjectType::Table ||
+ type == EObjectType::ReplicatedTable ||
+ type == EObjectType::ReplicationLogTable;
+}
+
+bool IsLogTableType(EObjectType type)
+{
+ return
+ type == EObjectType::ReplicatedTable ||
+ type == EObjectType::ReplicationLogTable;
+}
+
+bool IsTabletOwnerType(EObjectType type)
+{
+ return
+ IsTableType(type) ||
+ type == EObjectType::HunkStorage;
+}
+
+bool IsCellType(EObjectType type)
+{
+ return
+ type == EObjectType::TabletCell ||
+ type == EObjectType::ChaosCell;
+}
+
+bool IsCellBundleType(EObjectType type)
+{
+ return
+ type == EObjectType::TabletCellBundle ||
+ type == EObjectType::ChaosCellBundle;
+}
+
+bool IsAlienType(EObjectType type)
+{
+ return type == EObjectType::ChaosCell;
+}
+
+bool IsTabletType(EObjectType type)
+{
+ return
+ type == EObjectType::Tablet ||
+ type == EObjectType::HunkTablet;
+}
+
+bool IsReplicatedTableType(EObjectType type)
+{
+ return
+ type == EObjectType::ReplicatedTable ||
+ type == EObjectType::ReplicationCard;
+}
+
+bool IsTableReplicaType(EObjectType type)
+{
+ return
+ type == EObjectType::TableReplica ||
+ IsChaosTableReplicaType(type);
+}
+
+bool IsChaosTableReplicaType(EObjectType type)
+{
+ return type == EObjectType::ChaosTableReplica;
+}
+
+bool IsCollocationType(EObjectType type)
+{
+ return
+ type == EObjectType::TableCollocation ||
+ type == EObjectType::ReplicationCardCollocation;
+}
+
+bool IsMediumType(EObjectType type)
+{
+ return
+ type == EObjectType::Medium ||
+ type == EObjectType::S3Medium;
+}
+
+bool HasSchema(EObjectType type)
+{
+ if (type == EObjectType::Master) {
+ return false;
+ }
+ if (IsSchemaType(type)) {
+ return false;
+ }
+ return true;
+}
+
+bool IsSchemaType(EObjectType type)
+{
+ return (static_cast<ui32>(type) & SchemaObjectTypeMask) != 0;
+}
+
+bool IsGlobalCellId(TCellId cellId)
+{
+ auto type = TypeFromId(cellId);
+ return
+ type == EObjectType::MasterCell ||
+ type == EObjectType::ChaosCell;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NObjectClient
+
diff --git a/yt/yt/client/object_client/helpers.h b/yt/yt/client/object_client/helpers.h
new file mode 100644
index 0000000000..4f7280e4a8
--- /dev/null
+++ b/yt/yt/client/object_client/helpers.h
@@ -0,0 +1,191 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/hydra/version.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+
+namespace NYT::NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! |#|-prefix.
+extern const TStringBuf ObjectIdPathPrefix;
+
+//! Creates the YPath pointing to an object with a given #id.
+NYPath::TYPath FromObjectId(TObjectId id);
+
+//! Checks if the given type is versioned, i.e. represents a Cypress node.
+bool IsVersionedType(EObjectType type);
+
+//! Checks if the given type is user, i.e. regular users are allowed to create its instances.
+bool IsUserType(EObjectType type);
+
+//! Checks if the given type can have schema assigned to it, i.e. Cypress table or chaos replicated table.
+bool IsSchemafulType(EObjectType type);
+
+//! Checks if the given type is table, i.e. represents a Cypress table.
+bool IsTableType(EObjectType type);
+
+//! Checks if the given type is log table, i.e. table which contains replication log.
+bool IsLogTableType(EObjectType type);
+
+//! Checks if the given type is tablet owner.
+bool IsTabletOwnerType(EObjectType type);
+
+//! Checks if the given type is cell.
+bool IsCellType(EObjectType type);
+
+//! Checks if the given type is cell bundle.
+bool IsCellBundleType(EObjectType type);
+
+//! Checks if the given type allows alien objects.
+bool IsAlienType(EObjectType type);
+
+//! Checks if the given type is a tablet.
+bool IsTabletType(EObjectType type);
+
+//! Checks if the given type holds replication metadata.
+bool IsReplicatedTableType(EObjectType);
+
+//! Checks if the given type is a table replica.
+bool IsTableReplicaType(EObjectType type);
+
+//! Checks if the given type is a chaos replica.
+bool IsChaosTableReplicaType(EObjectType type);
+
+//! Checks if the given type is a collocation.
+bool IsCollocationType(EObjectType type);
+
+//! Checks if the given type is a medium;
+bool IsMediumType(EObjectType type);
+
+//! Extracts the type component from #id.
+EObjectType TypeFromId(TObjectId id);
+
+//! Extracts the cell id component from #id.
+TCellTag CellTagFromId(TObjectId id);
+
+//! Extracts the counter component from #id.
+ui64 CounterFromId(TObjectId id);
+
+//! Extracts the hash component from #id.
+ui32 HashFromId(TObjectId id);
+
+//! Extracts Hydra revision from #id for non-Sequoia objects.
+NHydra::TVersion VersionFromId(TObjectId id);
+
+//! Extracts the object creation timestamp from #id for Sequoia object.
+NTransactionClient::TTimestamp TimestampFromId(TObjectId id);
+
+//! Returns |true| iff a given #type denotes a schema.
+bool IsSchemaType(EObjectType type);
+
+//! Returns |true| iff a given regular #type has an associated schema type.
+bool HasSchema(EObjectType type);
+
+//! Returns the schema type for a given regular #type.
+EObjectType SchemaTypeFromType(EObjectType type);
+
+//! Returns the regular type for a given schema #type.
+EObjectType TypeFromSchemaType(EObjectType type);
+
+//! Constructs the id from its parts.
+TObjectId MakeId(
+ EObjectType type,
+ TCellTag cellTag,
+ ui64 counter,
+ ui32 hash);
+
+//! Creates a random id with given type and cell tag.
+TObjectId MakeRandomId(
+ EObjectType type,
+ TCellTag cellTag);
+
+//! Returns |true| if a given #id is well-known.
+/*
+ * This method checks the highest bit of counter part.
+ */
+bool IsWellKnownId(TObjectId id);
+
+//! Returns |true| if a given #id corresponds to Sequoia.
+/*
+ * This method checks the second highest bit of counter part.
+ */
+bool IsSequoiaId(TObjectId id);
+
+//! Constructs the id for a regular object.
+TObjectId MakeRegularId(
+ EObjectType type,
+ TCellTag cellTag,
+ NHydra::TVersion version,
+ ui32 hash);
+
+//! Constructs the id for a regular Sequoia object.
+TObjectId MakeSequoiaId(
+ EObjectType type,
+ TCellTag cellTag,
+ NTransactionClient::TTimestamp timestamp,
+ ui32 hash);
+
+//! Constructs the id corresponding to well-known (usually singleton) entities.
+/*
+ * The highest bit of #counter must be set.
+ */
+TObjectId MakeWellKnownId(
+ EObjectType type,
+ TCellTag cellTag,
+ ui64 counter = 0xffffffffffffffff);
+
+//! Returns the id of the schema object for a given regular type.
+TObjectId MakeSchemaObjectId(
+ EObjectType type,
+ TCellTag cellTag);
+
+//! Constructs a new object id by replacing type component in a given one.
+TObjectId ReplaceTypeInId(
+ TObjectId id,
+ EObjectType type);
+
+//! Constructs a new object id by replacing cell tag component in a given one.
+TObjectId ReplaceCellTagInId(
+ TObjectId id,
+ TCellTag cellTag);
+
+//! Useful for uniform partition of objects between shards.
+// NB: #shardCount must be a power of 2.
+template <int ShardCount>
+int GetShardIndex(TObjectId id);
+
+//! Returns true if #cellId is uniquely identified by its tag component.
+//! Currently these are master and chaos cells.
+bool IsGlobalCellId(TCellId cellId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Relies on first 32 bits of object id to be pseudo-random,
+//! cf. TObjectManager::GenerateId.
+struct TDirectObjectIdHash
+{
+ size_t operator()(TObjectId id) const;
+};
+
+//! Cf. TDirectObjectIdHash
+struct TDirectVersionedObjectIdHash
+{
+ size_t operator()(const TVersionedObjectId& id) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NObjectClient
+
+#define HELPERS_INL_H_
+#include "helpers-inl.h"
+#undef HELPERS_INL_H_
diff --git a/yt/yt/client/object_client/public.cpp b/yt/yt/client/object_client/public.cpp
new file mode 100644
index 0000000000..1a780352e5
--- /dev/null
+++ b/yt/yt/client/object_client/public.cpp
@@ -0,0 +1,68 @@
+#include "public.h"
+
+#include <yt/yt/core/misc/guid.h>
+
+#include <util/string/vector.h>
+
+namespace NYT::NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVersionedObjectId::TVersionedObjectId(TObjectId objectId)
+ : ObjectId(objectId)
+{ }
+
+TVersionedObjectId::TVersionedObjectId(
+ TObjectId objectId,
+ TTransactionId transactionId)
+ : ObjectId(objectId)
+ , TransactionId(transactionId)
+{ }
+
+bool TVersionedObjectId::IsBranched() const
+{
+ return TransactionId.operator bool();
+}
+
+TVersionedObjectId TVersionedObjectId::FromString(TStringBuf str)
+{
+ TStringBuf objectToken, transactionToken;
+ str.Split(':', objectToken, transactionToken);
+
+ auto objectId = TObjectId::FromString(objectToken);
+ auto transactionId =
+ transactionToken.empty()
+ ? NullTransactionId
+ : TTransactionId::FromString(transactionToken);
+ return TVersionedObjectId(objectId, transactionId);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TVersionedObjectId& id, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v:%v", id.ObjectId, id.TransactionId);
+}
+
+TString ToString(const TVersionedObjectId& id)
+{
+ return ToStringViaBuilder(id);
+}
+
+bool operator == (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs)
+{
+ return memcmp(&lhs, &rhs, sizeof (TVersionedObjectId)) == 0;
+}
+
+bool operator != (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs)
+{
+ return !(lhs == rhs);
+}
+
+bool operator < (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs)
+{
+ return memcmp(&lhs, &rhs, sizeof (TVersionedObjectId)) < 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NObjectClient
+
diff --git a/yt/yt/client/object_client/public.h b/yt/yt/client/object_client/public.h
new file mode 100644
index 0000000000..6818b03955
--- /dev/null
+++ b/yt/yt/client/object_client/public.h
@@ -0,0 +1,412 @@
+#pragma once
+
+#include <library/cpp/yt/misc/enum.h>
+#include <library/cpp/yt/misc/guid.h>
+#include <library/cpp/yt/misc/hash.h>
+
+#include <yt/yt/client/election/public.h>
+
+#include <library/cpp/yt/misc/strong_typedef.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/string/string_builder.h>
+
+namespace NYT::NObjectClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((PrerequisiteCheckFailed) (1000))
+ ((InvalidObjectLifeStage) (1001))
+ ((CrossCellAdditionalPath) (1002))
+ ((CrossCellRevisionPrerequisitePath) (1003))
+ ((ForwardedRequestFailed) (1004))
+ ((CannotCacheMutatingRequest) (1005))
+ ((InvalidObjectType) (1006))
+ ((RequestInvolvesSequoia) (1007))
+ ((RequestInvolvesCypress) (1008))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides a globally unique identifier for an object.
+/*!
+ * TGuid consists of four 32-bit parts.
+ * For TObjectId, these parts have the following meaning:
+ *
+ * Part 0: some hash
+ * Part 1: bits 0..15: object type
+ * bits 16..31: cell id
+ * Part 2: the lower part of 64-bit sequential counter
+ * Part 3: the higher part of 64-bit sequential counter
+ */
+using TObjectId = TGuid;
+
+//! The all-zero id used to denote a non-existing object.
+constexpr TObjectId NullObjectId = {};
+
+//! Used to mark counters for well-known ids.
+constexpr ui64 WellKnownCounterMask = 0x8000000000000000;
+
+//! Used to mark counters for Sequoia objects.
+constexpr ui64 SequoiaCounterMask = 0x4000000000000000;
+
+using NElection::TCellId;
+using NElection::NullCellId;
+
+//! Identifies a particular cell of YT cluster.
+//! Must be globally unique to prevent object ids from colliding.
+YT_DEFINE_STRONG_TYPEDEF(TCellTag, ui16)
+
+//! The minimum valid cell tag.
+constexpr auto MinValidCellTag = TCellTag(0x0000);
+
+//! The maximum valid cell tag.
+constexpr auto MaxValidCellTag = TCellTag(0xf000);
+
+//! A sentinel cell tag indicating that the request does not need replication.
+constexpr auto NotReplicatedCellTagSentinel = TCellTag(0xf001);
+
+//! A sentinel cell tag representing the primary master.
+constexpr auto PrimaryMasterCellTagSentinel = TCellTag(0xf003);
+
+//! A sentinel cell tag meaning nothing.
+constexpr auto InvalidCellTag = TCellTag(0xf004);
+
+//! A static limit for the number of secondary master cells.
+constexpr int MaxSecondaryMasterCells = 48;
+
+using TCellTagList = TCompactVector<TCellTag, MaxSecondaryMasterCells + 1>;
+using TCellIdList = TCompactVector<TCellId, MaxSecondaryMasterCells + 1>;
+
+//! Currently at most one additional path is expected (source paths for Copy and Move verbs).
+constexpr int TypicalAdditionalPathCount = 1;
+
+//! Describes the runtime type of an object.
+DEFINE_ENUM(EObjectType,
+ // Does not represent any actual type.
+ ((Null) (0))
+
+ // The following represent non-versioned objects.
+ // These must be created by calling TMasterYPathProxy::CreateObjects.
+
+ // Transaction Manager stuff
+ ((Transaction) ( 1))
+ ((AtomicTabletTransaction) ( 2))
+ ((NonAtomicTabletTransaction) ( 3))
+ ((NestedTransaction) ( 4))
+ ((ExternalizedTransaction) ( 5))
+ ((ExternalizedNestedTransaction) ( 6))
+ ((UploadTransaction) ( 7))
+ ((UploadNestedTransaction) ( 8))
+ ((TransactionMap) (407))
+ ((TopmostTransactionMap) (418))
+ ((LockMap) (422))
+
+ // Chunk Manager stuff
+ ((Chunk) (100))
+ ((ErasureChunk) (102)) // erasure chunk as a whole
+ ((ErasureChunkPart_0) (103)) // erasure chunk parts, mnemonic names are for debugging convenience only
+ ((ErasureChunkPart_1) (104))
+ ((ErasureChunkPart_2) (105))
+ ((ErasureChunkPart_3) (106))
+ ((ErasureChunkPart_4) (107))
+ ((ErasureChunkPart_5) (108))
+ ((ErasureChunkPart_6) (109))
+ ((ErasureChunkPart_7) (110))
+ ((ErasureChunkPart_8) (111))
+ ((ErasureChunkPart_9) (112))
+ ((ErasureChunkPart_10) (113))
+ ((ErasureChunkPart_11) (114))
+ ((ErasureChunkPart_12) (115))
+ ((ErasureChunkPart_13) (116))
+ ((ErasureChunkPart_14) (117))
+ ((ErasureChunkPart_15) (118))
+ ((JournalChunk) (119))
+ ((Artifact) (121))
+ ((ChunkMap) (402))
+ ((LostChunkMap) (403))
+ ((LostVitalChunkMap) (413))
+ ((PrecariousChunkMap) (410))
+ ((PrecariousVitalChunkMap) (411))
+ ((OverreplicatedChunkMap) (404))
+ ((UnderreplicatedChunkMap) (405))
+ ((DataMissingChunkMap) (419))
+ ((ParityMissingChunkMap) (420))
+ ((OldestPartMissingChunkMap) (428))
+ ((QuorumMissingChunkMap) (424))
+ ((UnsafelyPlacedChunkMap) (120))
+ ((InconsistentlyPlacedChunkMap) (160))
+ ((UnexpectedOverreplicatedChunkMap) (434))
+ ((ReplicaTemporarilyUnavailableChunkMap) (436))
+ ((ForeignChunkMap) (122))
+ ((LocalLostChunkMap) (450))
+ ((LocalLostVitalChunkMap) (451))
+ ((LocalPrecariousChunkMap) (452))
+ ((LocalPrecariousVitalChunkMap) (453))
+ ((LocalOverreplicatedChunkMap) (454))
+ ((LocalUnderreplicatedChunkMap) (455))
+ ((LocalDataMissingChunkMap) (456))
+ ((LocalParityMissingChunkMap) (457))
+ ((LocalOldestPartMissingChunkMap) (458))
+ ((LocalQuorumMissingChunkMap) (459))
+ ((LocalUnsafelyPlacedChunkMap) (460))
+ ((LocalInconsistentlyPlacedChunkMap) (461))
+ ((LocalUnexpectedOverreplicatedChunkMap) (462))
+ ((LocalReplicaTemporarilyUnavailableChunkMap) (463))
+ ((ChunkList) (101))
+ ((ChunkListMap) (406))
+ ((ChunkView) (123))
+ ((ChunkViewMap) (430))
+ ((Medium) (408))
+ ((S3Medium) (435))
+ ((MediumMap) (409))
+ ((ErasureJournalChunk) (124)) // erasure journal chunk as a whole
+ ((ErasureJournalChunkPart_0) (125)) // erasure chunk parts, mnemonic names are for debugging convenience only
+ ((ErasureJournalChunkPart_1) (126))
+ ((ErasureJournalChunkPart_2) (127))
+ ((ErasureJournalChunkPart_3) (128))
+ ((ErasureJournalChunkPart_4) (129))
+ ((ErasureJournalChunkPart_5) (130))
+ ((ErasureJournalChunkPart_6) (131))
+ ((ErasureJournalChunkPart_7) (132))
+ ((ErasureJournalChunkPart_8) (133))
+ ((ErasureJournalChunkPart_9) (134))
+ ((ErasureJournalChunkPart_10) (135))
+ ((ErasureJournalChunkPart_11) (136))
+ ((ErasureJournalChunkPart_12) (137))
+ ((ErasureJournalChunkPart_13) (138))
+ ((ErasureJournalChunkPart_14) (139))
+ ((ErasureJournalChunkPart_15) (140))
+ ((ChunkLocation) (141))
+ ((ChunkLocationMap) (142))
+
+ // The following represent versioned objects (AKA Cypress nodes).
+ // These must be created by calling TCypressYPathProxy::Create.
+ // NB: When adding a new type, don't forget to update IsVersionedType.
+
+ // Auxiliary
+ ((Lock) (200))
+
+ // Static nodes
+ ((StringNode) (300))
+ ((Int64Node) (301))
+ ((Uint64Node) (306))
+ ((DoubleNode) (302))
+ ((MapNode) (303))
+ ((ListNode) (304))
+ ((BooleanNode) (305))
+
+ // Dynamic nodes
+ ((File) (400))
+ ((Table) (401))
+ ((Journal) (423))
+ ((Orchid) (412))
+ ((Link) (417))
+ ((Document) (421))
+ ((ReplicatedTable) (425))
+ ((ReplicationLogTable) (431))
+
+ ((AccessControlObject) (307))
+ ((AccessControlObjectNamespace) (432))
+ ((AccessControlObjectNamespaceMap) (433))
+
+ // Cypress shards
+ ((CypressShard) (11004))
+ ((CypressShardMap) (11005))
+
+ // Portals
+ ((PortalEntrance) (11000))
+ ((PortalExit) (11001))
+ ((PortalEntranceMap) (11002))
+ ((PortalExitMap) (11003))
+
+ // Grafting
+ ((Rootstock) (12000))
+ ((Scion) (12001))
+ ((RootstockMap) (12002))
+ ((ScionMap) (12003))
+
+ // Security Manager stuff
+ ((Account) (500))
+ ((AccountMap) (414))
+ ((AccountResourceUsageLease) (505))
+ ((AccountResourceUsageLeaseMap) (506))
+ ((User) (501))
+ ((UserMap) (415))
+ ((Group) (502))
+ ((GroupMap) (416))
+ ((NetworkProject) (503))
+ ((NetworkProjectMap) (426))
+ ((ProxyRole) (504))
+ ((HttpProxyRoleMap) (427))
+ ((RpcProxyRoleMap) (429))
+
+ // Table Manager stuff
+ ((MasterTableSchema) (1300))
+ ((MasterTableSchemaMap) (1301))
+ ((TableCollocation) (1302))
+
+ // Global stuff
+ // A mysterious creature representing the master as a whole.
+ ((Master) (600))
+ ((MasterCell) (601))
+ ((SysNode) (602))
+ // Next two types would end with 'a' and 'b' in hex.
+ ((AliceAvenueEndpoint) (618))
+ ((BobAvenueEndpoint) (619))
+
+ // Tablet Manager stuff
+ ((TabletCell) (700))
+ ((TabletCellNode) (701))
+ ((Tablet) (702))
+ ((TabletMap) (703))
+ ((TabletCellMap) (710))
+ ((SortedDynamicTabletStore) (704))
+ ((OrderedDynamicTabletStore) (708))
+ ((TabletPartition) (705))
+ ((TabletCellBundle) (706))
+ ((TabletCellBundleMap) (707))
+ ((TableReplica) (709))
+ ((TabletAction) (711))
+ ((TabletActionMap) (712))
+ ((Area) (713))
+ ((AreaMap) (714))
+ ((HunkStorage) (715))
+ ((HunkTablet) (716))
+
+ // Node Tracker stuff
+ ((Rack) (800))
+ ((RackMap) (801))
+ ((ClusterNode) (802))
+ ((ClusterNodeNode) (803))
+ ((LegacyClusterNodeMap) (804))
+ ((ClusterNodeMap) (807))
+ ((DataNodeMap) (808))
+ ((ExecNodeMap) (809))
+ ((TabletNodeMap) (810))
+ ((ChaosNodeMap) (811))
+ ((DataCenter) (805))
+ ((DataCenterMap) (806))
+ ((Host) (812))
+ ((HostMap) (813))
+
+ // Job Tracker stuff
+ ((SchedulerJob) (900))
+ ((MasterJob) (901))
+
+ // Scheduler
+ ((Operation) (1000))
+ ((SchedulerPool) (1001))
+ ((SchedulerPoolTree) (1002))
+ ((SchedulerPoolTreeMap) (1003))
+
+ // Object manager stuff
+ ((EstimatedCreationTimeMap) (1100))
+
+ // Chaos stuff
+ ((ChaosCell) (1200))
+ ((ChaosCellBundle) (1201))
+ ((ChaosCellMap) (1202))
+ ((ChaosCellBundleMap) (1203))
+ ((ReplicationCard) (1204))
+ ((ChaosTableReplica) (1205))
+ ((ChaosReplicatedTable) (1206))
+ ((ReplicationCardCollocation) (1207))
+
+ // Zookeeper stuff
+ ((ZookeeperShard) (1400))
+ ((ZookeeperShardMap) (1401))
+);
+
+//! A bit mask marking schema types.
+constexpr ui32 SchemaObjectTypeMask = 0x8000;
+
+// The range of erasure chunk part types.
+constexpr EObjectType MinErasureChunkPartType = EObjectType::ErasureChunkPart_0;
+constexpr EObjectType MaxErasureChunkPartType = EObjectType::ErasureChunkPart_15;
+
+// The range of erasure journal chunk part types.
+constexpr EObjectType MinErasureJournalChunkPartType = EObjectType::ErasureJournalChunkPart_0;
+constexpr EObjectType MaxErasureJournalChunkPartType = EObjectType::ErasureJournalChunkPart_15;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TTransactionId = TObjectId;
+constexpr TTransactionId NullTransactionId = {};
+
+using TOperationId = TObjectId;
+using TJobId = TObjectId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Identifies a node possibly branched by a transaction.
+struct TVersionedObjectId
+{
+ //! Id of the node itself.
+ TObjectId ObjectId;
+
+ //! Id of the transaction that had branched the node.
+ //! #NullTransactionId if the node is not branched.
+ TTransactionId TransactionId;
+
+ //! Initializes a null instance.
+ /*!
+ * #NodeId is #NullObjectId, #TransactionId is #NullTransactionId.
+ */
+ TVersionedObjectId() = default;
+
+ //! Initializes an instance by given node. Sets #TransactionId to #NullTransactionId.
+ explicit TVersionedObjectId(TObjectId objectId);
+
+ //! Initializes an instance by given node and transaction ids.
+ TVersionedObjectId(TObjectId objectId, TTransactionId transactionId);
+
+ //! Checks that the id is branched, i.e. #TransactionId is not #NullTransactionId.
+ bool IsBranched() const;
+
+
+ static TVersionedObjectId FromString(TStringBuf str);
+};
+
+//! Formats id into a string (for debugging and logging purposes mainly).
+void FormatValue(TStringBuilderBase* builder, const TVersionedObjectId& id, TStringBuf spec);
+
+//! Converts id into a string (for debugging and logging purposes mainly).
+TString ToString(const TVersionedObjectId& id);
+
+//! Compares TVersionedNodeId s for equality.
+bool operator == (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs);
+
+//! Compares TVersionedNodeId s for inequality.
+bool operator != (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs);
+
+//! Compares TVersionedNodeId s for "less than".
+bool operator < (const TVersionedObjectId& lhs, const TVersionedObjectId& rhs);
+
+class TObjectServiceProxy;
+
+struct TDirectObjectIdHash;
+struct TDirectVersionedObjectIdHash;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NObjectClient
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+struct THash<NYT::NObjectClient::TVersionedObjectId>
+{
+ size_t operator()(const NYT::NObjectClient::TVersionedObjectId& id) const
+ {
+ auto result = THash<NYT::NObjectClient::TObjectId>()(id.ObjectId);
+ HashCombine(result, id.TransactionId);
+ return result;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_DECLARE_PODTYPE(NYT::NObjectClient::TVersionedObjectId);
diff --git a/yt/yt/client/query_client/public.h b/yt/yt/client/query_client/public.h
new file mode 100644
index 0000000000..260e4d9224
--- /dev/null
+++ b/yt/yt/client/query_client/public.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NQueryClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TQueryStatistics;
+
+} // namespace NProto
+
+struct TQueryStatistics;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryClient
+
diff --git a/yt/yt/client/query_client/query_builder.cpp b/yt/yt/client/query_client/query_builder.cpp
new file mode 100644
index 0000000000..b6658e1076
--- /dev/null
+++ b/yt/yt/client/query_client/query_builder.cpp
@@ -0,0 +1,159 @@
+#include "query_builder.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/string/join.h>
+#include <util/stream/str.h>
+
+namespace NYT::NQueryClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static std::vector<TString> Parenthesize(std::vector<TString> strings)
+{
+ for (auto& string : strings) {
+ string.prepend('(').append(')');
+ }
+ return strings;
+}
+
+void TQueryBuilder::SetSource(TString source)
+{
+ Source_ = std::move(source);
+}
+
+int TQueryBuilder::AddSelectExpression(TString expression)
+{
+ SelectEntries_.push_back(TEntryWithAlias{
+ std::move(expression),
+ std::nullopt,
+ });
+ return SelectEntries_.size() - 1;
+}
+
+int TQueryBuilder::AddSelectExpression(TString expression, TString alias)
+{
+ SelectEntries_.push_back(TEntryWithAlias{
+ std::move(expression),
+ std::move(alias),
+ });
+ return SelectEntries_.size() - 1;
+}
+
+void TQueryBuilder::AddWhereConjunct(TString expression)
+{
+ WhereConjuncts_.push_back(std::move(expression));
+}
+
+void TQueryBuilder::AddGroupByExpression(TString expression)
+{
+ GroupByEntries_.push_back(TEntryWithAlias{
+ std::move(expression),
+ std::nullopt
+ });
+}
+
+void TQueryBuilder::AddGroupByExpression(TString expression, TString alias)
+{
+ GroupByEntries_.push_back(TEntryWithAlias{
+ std::move(expression),
+ std::move(alias),
+ });
+}
+
+void TQueryBuilder::AddOrderByExpression(TString expression)
+{
+ OrderByEntries_.push_back(TOrderByEntry{
+ std::move(expression),
+ std::nullopt,
+ });
+}
+
+void TQueryBuilder::AddOrderByExpression(TString expression, std::optional<EOrderByDirection> direction)
+{
+ OrderByEntries_.push_back(TOrderByEntry{
+ std::move(expression),
+ direction,
+ });
+}
+
+void TQueryBuilder::AddOrderByAscendingExpression(TString expression)
+{
+ AddOrderByExpression(std::move(expression), EOrderByDirection::Ascending);
+}
+
+void TQueryBuilder::AddOrderByDescendingExpression(TString expression)
+{
+ AddOrderByExpression(std::move(expression), EOrderByDirection::Descending);
+}
+
+void TQueryBuilder::SetLimit(i64 limit)
+{
+ Limit_ = limit;
+}
+
+TString TQueryBuilder::Build()
+{
+ std::vector<TString> parts;
+ parts.reserve(8);
+
+ if (SelectEntries_.empty()) {
+ THROW_ERROR_EXCEPTION("Query must have at least one SELECT expression");
+ }
+ parts.push_back(JoinSeq(", ", SelectEntries_));
+
+ if (!Source_) {
+ THROW_ERROR_EXCEPTION("Source must be specified in query");
+ }
+ parts.push_back(Format("FROM [%v]", *Source_));
+
+ if (!WhereConjuncts_.empty()) {
+ parts.push_back("WHERE");
+ parts.push_back(JoinSeq(" AND ", Parenthesize(WhereConjuncts_)));
+ }
+
+ if (!OrderByEntries_.empty()) {
+ parts.push_back("ORDER BY");
+ parts.push_back(JoinSeq(", ", OrderByEntries_));
+ }
+
+ if (!GroupByEntries_.empty()) {
+ parts.push_back("GROUP BY");
+ parts.push_back(JoinSeq(", ", GroupByEntries_));
+ }
+
+ if (Limit_) {
+ parts.push_back(Format("LIMIT %v", *Limit_));
+ }
+
+ return JoinSeq(" ", parts);
+}
+
+void AppendToString(TString& dst, const TQueryBuilder::TEntryWithAlias& entry)
+{
+ TStringOutput output(dst);
+ if (entry.Expression == "*") {
+ output << "*";
+ return;
+ }
+ output << '(' << entry.Expression << ')';
+ if (entry.Alias) {
+ output << " AS " << *entry.Alias;
+ }
+}
+
+void AppendToString(TString& dst, const TQueryBuilder::TOrderByEntry& entry)
+{
+ TStringOutput output(dst);
+ output << '(' << entry.Expression << ')';
+ if (entry.Direction) {
+ TStringBuf directionString = (*entry.Direction == EOrderByDirection::Ascending)
+ ? "ASC"
+ : "DESC";
+ output << ' ' << directionString;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/query_client/query_builder.h b/yt/yt/client/query_client/query_builder.h
new file mode 100644
index 0000000000..1548e749a2
--- /dev/null
+++ b/yt/yt/client/query_client/query_builder.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/string.h>
+
+namespace NYT::NQueryClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EOrderByDirection,
+ (Ascending)
+ (Descending)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TQueryBuilder
+{
+public:
+ void SetSource(TString source);
+
+ int AddSelectExpression(TString expression);
+ int AddSelectExpression(TString expression, TString alias);
+
+ void AddWhereConjunct(TString expression);
+
+ void AddGroupByExpression(TString expression);
+ void AddGroupByExpression(TString expression, TString alias);
+
+ void AddOrderByExpression(TString expression);
+ void AddOrderByExpression(TString expression, std::optional<EOrderByDirection> direction);
+
+ void AddOrderByAscendingExpression(TString expression);
+ void AddOrderByDescendingExpression(TString expression);
+
+ void SetLimit(i64 limit);
+
+ TString Build();
+
+private:
+ struct TEntryWithAlias
+ {
+ TString Expression;
+ std::optional<TString> Alias;
+ };
+
+ struct TOrderByEntry
+ {
+ TString Expression;
+ std::optional<EOrderByDirection> Direction;
+ };
+
+private:
+ std::optional<TString> Source_;
+ std::vector<TEntryWithAlias> SelectEntries_;
+ std::vector<TString> WhereConjuncts_;
+ std::vector<TOrderByEntry> OrderByEntries_;
+ std::vector<TEntryWithAlias> GroupByEntries_;
+ std::optional<i64> Limit_;
+
+private:
+ // We overload this functions to allow the corresponding JoinSeq().
+ friend void AppendToString(TString& dst, const TEntryWithAlias& entry);
+ friend void AppendToString(TString& dst, const TOrderByEntry& entry);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/query_client/query_statistics.cpp b/yt/yt/client/query_client/query_statistics.cpp
new file mode 100644
index 0000000000..ac9e1349ef
--- /dev/null
+++ b/yt/yt/client/query_client/query_statistics.cpp
@@ -0,0 +1,117 @@
+#include "query_statistics.h"
+
+#include <yt/yt_proto/yt/client/query_client/proto/query_statistics.pb.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NQueryClient {
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TQueryStatistics::AddInnerStatistics(const TQueryStatistics& statistics)
+{
+ InnerStatistics.push_back(statistics);
+ IncompleteInput |= statistics.IncompleteInput;
+ IncompleteOutput |= statistics.IncompleteOutput;
+ MemoryUsage = std::max(MemoryUsage, statistics.MemoryUsage);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TQueryStatistics* serialized, const TQueryStatistics& original)
+{
+ serialized->set_rows_read(original.RowsRead);
+ serialized->set_data_weight_read(original.DataWeightRead);
+ serialized->set_rows_written(original.RowsWritten);
+ serialized->set_sync_time(ToProto<i64>(original.SyncTime));
+ serialized->set_async_time(ToProto<i64>(original.AsyncTime));
+ serialized->set_execute_time(ToProto<i64>(original.ExecuteTime));
+ serialized->set_read_time(ToProto<i64>(original.ReadTime));
+ serialized->set_write_time(ToProto<i64>(original.WriteTime));
+ serialized->set_codegen_time(ToProto<i64>(original.CodegenTime));
+ serialized->set_wait_on_ready_event_time(ToProto<i64>(original.WaitOnReadyEventTime));
+ serialized->set_incomplete_input(original.IncompleteInput);
+ serialized->set_incomplete_output(original.IncompleteOutput);
+ serialized->set_memory_usage(original.MemoryUsage);
+ ToProto(serialized->mutable_inner_statistics(), original.InnerStatistics);
+}
+
+void FromProto(TQueryStatistics* original, const NProto::TQueryStatistics& serialized)
+{
+ original->RowsRead = serialized.rows_read();
+ original->DataWeightRead = serialized.data_weight_read();
+ original->RowsWritten = serialized.rows_written();
+ original->SyncTime = FromProto<TDuration>(serialized.sync_time());
+ original->AsyncTime = FromProto<TDuration>(serialized.async_time());
+ original->ExecuteTime = FromProto<TDuration>(serialized.execute_time());
+ original->ReadTime = FromProto<TDuration>(serialized.read_time());
+ original->WriteTime = FromProto<TDuration>(serialized.write_time());
+ original->CodegenTime = FromProto<TDuration>(serialized.codegen_time());
+ original->WaitOnReadyEventTime = FromProto<TDuration>(serialized.wait_on_ready_event_time());
+ original->IncompleteInput = serialized.incomplete_input();
+ original->IncompleteOutput = serialized.incomplete_output();
+ original->MemoryUsage = serialized.memory_usage();
+ FromProto(&original->InnerStatistics, serialized.inner_statistics());
+}
+
+TString ToString(const TQueryStatistics& stats)
+{
+ return Format(
+ "{"
+ "RowsRead: %v, DataWeightRead: %v, RowsWritten: %v, "
+ "SyncTime: %v, AsyncTime: %v, ExecuteTime: %v, ReadTime: %v, WriteTime: %v, CodegenTime: %v, "
+ "WaitOnReadyEventTime: %v, IncompleteInput: %v, IncompleteOutput: %v, MemoryUsage: %v"
+ "}",
+ stats.RowsRead,
+ stats.DataWeightRead,
+ stats.RowsWritten,
+ stats.SyncTime,
+ stats.AsyncTime,
+ stats.ExecuteTime,
+ stats.ReadTime,
+ stats.WriteTime,
+ stats.CodegenTime,
+ stats.WaitOnReadyEventTime,
+ stats.IncompleteInput,
+ stats.IncompleteOutput,
+ stats.MemoryUsage);
+}
+
+void Serialize(const TQueryStatistics& statistics, NYson::IYsonConsumer* consumer)
+{
+ NYTree::BuildYsonMapFragmentFluently(consumer)
+ .Item("rows_read").Value(statistics.RowsRead)
+ .Item("data_weight_read").Value(statistics.DataWeightRead)
+ .Item("rows_written").Value(statistics.RowsWritten)
+ .Item("sync_time").Value(statistics.SyncTime.MilliSeconds())
+ .Item("async_time").Value(statistics.AsyncTime.MilliSeconds())
+ .Item("execute_time").Value(statistics.ExecuteTime.MilliSeconds())
+ .Item("read_time").Value(statistics.ReadTime.MilliSeconds())
+ .Item("write_time").Value(statistics.WriteTime.MilliSeconds())
+ .Item("codegen_time").Value(statistics.CodegenTime.MilliSeconds())
+ .Item("wait_on_ready_event_time").Value(statistics.WaitOnReadyEventTime.MilliSeconds())
+ .Item("incomplete_input").Value(statistics.IncompleteInput)
+ .Item("incomplete_output").Value(statistics.IncompleteOutput)
+ .Item("memory_usage").Value(statistics.MemoryUsage)
+ .DoIf(!statistics.InnerStatistics.empty(), [&] (NYTree::TFluentMap fluent) {
+ fluent
+ .Item("inner_statistics").DoListFor(statistics.InnerStatistics, [=] (
+ NYTree::TFluentList fluent,
+ const TQueryStatistics& statistics)
+ {
+ fluent
+ .Item().DoMap([&] (NYTree::TFluentMap fluent) {
+ Serialize(statistics, fluent.GetConsumer());
+ });
+ });
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/query_client/query_statistics.h b/yt/yt/client/query_client/query_statistics.h
new file mode 100644
index 0000000000..daf81cddf0
--- /dev/null
+++ b/yt/yt/client/query_client/query_statistics.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NQueryClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TQueryStatistics
+{
+ i64 RowsRead = 0;
+ i64 DataWeightRead = 0;
+ i64 RowsWritten = 0;
+ TDuration SyncTime;
+ TDuration AsyncTime;
+ TDuration ExecuteTime;
+ TDuration ReadTime;
+ TDuration WriteTime;
+ TDuration CodegenTime;
+ TDuration WaitOnReadyEventTime;
+ bool IncompleteInput = false;
+ bool IncompleteOutput = false;
+ size_t MemoryUsage = 0;
+
+ std::vector<TQueryStatistics> InnerStatistics;
+
+ void AddInnerStatistics(const TQueryStatistics& statistics);
+};
+
+void ToProto(NProto::TQueryStatistics* serialized, const TQueryStatistics& original);
+void FromProto(TQueryStatistics* original, const NProto::TQueryStatistics& serialized);
+
+TString ToString(const TQueryStatistics& stat);
+
+void Serialize(const TQueryStatistics& statistics, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/query_tracker_client/public.cpp b/yt/yt/client/query_tracker_client/public.cpp
new file mode 100644
index 0000000000..4df9bcaa18
--- /dev/null
+++ b/yt/yt/client/query_tracker_client/public.cpp
@@ -0,0 +1 @@
+#include "public.h"
diff --git a/yt/yt/client/query_tracker_client/public.h b/yt/yt/client/query_tracker_client/public.h
new file mode 100644
index 0000000000..56c3660c5b
--- /dev/null
+++ b/yt/yt/client/query_tracker_client/public.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <yt/yt/core/misc/guid.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NQueryTrackerClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((IncarnationMismatch) (3900))
+ ((QueryNotFound) (3901))
+ ((QueryResultNotFound) (3902))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TQueryId = TGuid;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_STRING_SERIALIZABLE_ENUM(EQueryEngine,
+ (Ql)
+ (Yql)
+ (Chyt)
+ (Mock)
+);
+
+DEFINE_STRING_SERIALIZABLE_ENUM(EQueryState,
+ (Draft)
+ (Pending)
+ (Running)
+ (Aborting)
+ (Aborted)
+ (Completing)
+ (Completed)
+ (Failing)
+ (Failed)
+);
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueryTrackerClient
diff --git a/yt/yt/client/query_tracker_client/ya.make b/yt/yt/client/query_tracker_client/ya.make
new file mode 100644
index 0000000000..bf27067fd3
--- /dev/null
+++ b/yt/yt/client/query_tracker_client/ya.make
@@ -0,0 +1,13 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ public.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+)
+
+END()
diff --git a/yt/yt/client/queue_client/common.cpp b/yt/yt/client/queue_client/common.cpp
new file mode 100644
index 0000000000..e7aa1798bd
--- /dev/null
+++ b/yt/yt/client/queue_client/common.cpp
@@ -0,0 +1,71 @@
+#include "common.h"
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <util/string/split.h>
+
+namespace NYT::NQueueClient {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TCrossClusterReference::operator==(const TCrossClusterReference& other) const
+{
+ return Cluster == other.Cluster && Path == other.Path;
+}
+
+bool TCrossClusterReference::operator<(const TCrossClusterReference& other) const
+{
+ return std::tie(Cluster, Path) < std::tie(other.Cluster, other.Path);
+}
+
+TCrossClusterReference::operator TRichYPath() const
+{
+ TRichYPath result = Path;
+ result.SetCluster(Cluster);
+ return result;
+}
+
+TCrossClusterReference TCrossClusterReference::FromString(TStringBuf path)
+{
+ TCrossClusterReference result;
+ if (!StringSplitter(path).Split(':').Limit(2).TryCollectInto(&result.Cluster, &result.Path)) {
+ THROW_ERROR_EXCEPTION("Ill-formed cross-cluster reference %Qv", path);
+ }
+ if (result.Cluster.empty()) {
+ THROW_ERROR_EXCEPTION("The cluster component of cross-cluster reference %Qv cannot be empty", path);
+ }
+ if (result.Path.empty()) {
+ THROW_ERROR_EXCEPTION("The path component of cross-cluster reference %Qv cannot be empty", path);
+ }
+ return result;
+}
+
+TString ToString(const TCrossClusterReference& queueRef)
+{
+ return Format("%v:%v", queueRef.Cluster, queueRef.Path);
+}
+
+void Serialize(const TCrossClusterReference& queueRef, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).Value(ToString(queueRef));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
+
+size_t THash<NYT::NQueueClient::TCrossClusterReference>::operator()(const NYT::NQueueClient::TCrossClusterReference& crossClusterRef) const
+{
+ using NYT::HashCombine;
+
+ size_t result = 0;
+ HashCombine(result, crossClusterRef.Cluster);
+ HashCombine(result, crossClusterRef.Path);
+ return result;
+}
diff --git a/yt/yt/client/queue_client/common.h b/yt/yt/client/queue_client/common.h
new file mode 100644
index 0000000000..29b6d903e0
--- /dev/null
+++ b/yt/yt/client/queue_client/common.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(achulkov2): Replace this with TRichYPath in YT-18038.
+struct TCrossClusterReference
+{
+ TString Cluster;
+ NYPath::TYPath Path;
+
+ bool operator ==(const TCrossClusterReference& other) const;
+ bool operator <(const TCrossClusterReference& other) const;
+
+ operator NYPath::TRichYPath() const;
+
+ static TCrossClusterReference FromString(TStringBuf path);
+};
+
+TString ToString(const TCrossClusterReference& queueRef);
+
+void Serialize(const TCrossClusterReference& queueRef, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
+
+template <>
+struct THash<NYT::NQueueClient::TCrossClusterReference>
+{
+ size_t operator()(const NYT::NQueueClient::TCrossClusterReference& crossClusterRef) const;
+};
diff --git a/yt/yt/client/queue_client/config.cpp b/yt/yt/client/queue_client/config.cpp
new file mode 100644
index 0000000000..16b1d7fd09
--- /dev/null
+++ b/yt/yt/client/queue_client/config.cpp
@@ -0,0 +1,45 @@
+#include "config.h"
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPartitionReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_row_count", &TThis::MaxRowCount)
+ .Default(1000);
+ registrar.Parameter("max_data_weight", &TThis::MaxDataWeight)
+ .Default(16_MB);
+ registrar.Parameter("data_weight_per_row_hint", &TThis::DataWeightPerRowHint)
+ .Default();
+
+ registrar.Parameter("use_native_tablet_node_api", &TThis::UseNativeTabletNodeApi)
+ .Default(false);
+ registrar.Parameter("use_pull_consumer", &TThis::UsePullConsumer)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->UsePullConsumer && !config->UseNativeTabletNodeApi) {
+ THROW_ERROR_EXCEPTION("PullConsumer can only be used with the native tablet node api for pulling rows");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TQueueAutoTrimConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable", &TThis::Enable)
+ .Default(false);
+ registrar.Parameter("retained_rows", &TThis::RetainedRows)
+ .Default();
+}
+
+bool operator==(const TQueueAutoTrimConfig& lhs, const TQueueAutoTrimConfig& rhs)
+{
+ return lhs.Enable == rhs.Enable && lhs.RetainedRows == rhs.RetainedRows;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/config.h b/yt/yt/client/queue_client/config.h
new file mode 100644
index 0000000000..043702c6b5
--- /dev/null
+++ b/yt/yt/client/queue_client/config.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPartitionReaderConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ i64 MaxRowCount;
+ i64 MaxDataWeight;
+
+ //! If set, this value is used to compute the number of rows to read considering the given MaxDataWeight.
+ std::optional<i64> DataWeightPerRowHint;
+
+ bool UseNativeTabletNodeApi;
+ bool UsePullConsumer;
+
+ REGISTER_YSON_STRUCT(TPartitionReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TPartitionReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+//! Automatic trimming configuration for a single queue.
+//!
+//! All rows up to the smallest offset among vital consumers are considered trimmable.
+//! If no other setting in the config explicitly prohibits trimming these rows,
+//! they will be trimmed automatically by the responsible queue agent.
+//! This is not applicable if no vital consumers exist for a queue.
+// TODO(achulkov2): Add example of how multiple vital/non-vital consumers and the options below interact.
+class TQueueAutoTrimConfig
+ : public NYTree::TYsonStructLite
+{
+public:
+ //! If set to false, no automatic trimming is performed.
+ bool Enable;
+
+ //! If set, this number of rows is guaranteed to be kept in each partition.
+ std::optional<i64> RetainedRows;
+
+ REGISTER_YSON_STRUCT_LITE(TQueueAutoTrimConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+bool operator==(const TQueueAutoTrimConfig& lhs, const TQueueAutoTrimConfig& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/consumer_client.cpp b/yt/yt/client/queue_client/consumer_client.cpp
new file mode 100644
index 0000000000..89c31e1ae2
--- /dev/null
+++ b/yt/yt/client/queue_client/consumer_client.cpp
@@ -0,0 +1,583 @@
+#include "consumer_client.h"
+#include "private.h"
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/helpers.h>
+#include <yt/yt/client/table_client/check_schema_compatibility.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/api/rowset.h>
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/client/transaction_client/helpers.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <library/cpp/iterator/functools.h>
+
+#include <util/string/join.h>
+
+namespace NYT::NQueueClient {
+
+using namespace NApi;
+using namespace NConcurrency;
+using namespace NHiveClient;
+using namespace NTableClient;
+using namespace NTabletClient;
+using namespace NTransactionClient;
+using namespace NYPath;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = QueueClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TTableSchemaPtr YTConsumerTableSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema("queue_cluster", EValueType::String, ESortOrder::Ascending).SetRequired(true),
+ TColumnSchema("queue_path", EValueType::String, ESortOrder::Ascending).SetRequired(true),
+ TColumnSchema("partition_index", EValueType::Uint64, ESortOrder::Ascending).SetRequired(true),
+ TColumnSchema("offset", EValueType::Uint64).SetRequired(true),
+}, /*strict*/ true, /*uniqueKeys*/ true);
+
+static const TTableSchemaPtr BigRTConsumerTableSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema("ShardId", EValueType::Uint64, ESortOrder::Ascending),
+ TColumnSchema("Offset", EValueType::Uint64),
+}, /*strict*/ true, /*uniqueKeys*/ true);
+
+class TGenericConsumerClient
+ : public ISubConsumerClient
+{
+public:
+ TGenericConsumerClient(
+ TYPath path,
+ TUnversionedOwningRow rowPrefix,
+ TStringBuf partitionIndexColumnName,
+ TStringBuf offsetColumnName,
+ bool decrementOffset,
+ const TTableSchemaPtr& tableSchema)
+ : Path_(std::move(path))
+ , RowPrefix_(std::move(rowPrefix))
+ , PartitionIndexColumnName_(std::move(partitionIndexColumnName))
+ , OffsetColumnName_(std::move(offsetColumnName))
+ , TableSchema_(tableSchema)
+ , NameTable_(TNameTable::FromSchema(*TableSchema_))
+ , PartitionIndexColumnId_(NameTable_->GetId(PartitionIndexColumnName_))
+ , OffsetColumnId_(NameTable_->GetId(OffsetColumnName_))
+ , SubConsumerColumnFilter_{PartitionIndexColumnId_, OffsetColumnId_}
+ , DecrementOffset_(decrementOffset)
+ , SubConsumerSchema_(New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema(TString(PartitionIndexColumnName_), EValueType::Uint64, ESortOrder::Ascending),
+ TColumnSchema(TString(OffsetColumnName_), EValueType::Uint64),
+ }, /*strict*/ true, /*uniqueKeys*/ true))
+ {
+ if (RowPrefix_.GetCount() == 0) {
+ RowPrefixCondition_ = "1 = 1";
+ } else {
+ TStringBuilder builder;
+ builder.AppendChar('(');
+ for (int index = 0; index < RowPrefix_.GetCount(); ++index) {
+ if (index != 0) {
+ builder.AppendString(", ");
+ }
+ builder.AppendFormat("[%v]", TableSchema_->Columns()[index].Name());
+ }
+ builder.AppendString(") = (");
+ for (int index = 0; index < RowPrefix_.GetCount(); ++index) {
+ if (index != 0) {
+ builder.AppendString(", ");
+ }
+ YT_VERIFY(RowPrefix_[index].Type == EValueType::String);
+ builder.AppendFormat("\"%v\"", RowPrefix_[index].AsStringBuf());
+ }
+ builder.AppendChar(')');
+ RowPrefixCondition_ = builder.Flush();
+ }
+ }
+
+ void Advance(
+ const ITransactionPtr& transaction,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset) const override
+ {
+ if (oldOffset) {
+ TUnversionedRowsBuilder keyRowsBuilder;
+ TUnversionedRowBuilder rowBuilder;
+ for (const auto& value : RowPrefix_) {
+ rowBuilder.AddValue(value);
+ }
+ rowBuilder.AddValue(MakeUnversionedUint64Value(partitionIndex, PartitionIndexColumnId_));
+ keyRowsBuilder.AddRow(rowBuilder.GetRow());
+
+ TVersionedLookupRowsOptions options;
+ options.RetentionConfig = New<TRetentionConfig>();
+ options.RetentionConfig->MaxDataVersions = 1;
+
+ auto partitionRowset = WaitFor(transaction->VersionedLookupRows(Path_, NameTable_, keyRowsBuilder.Build(), options))
+ .ValueOrThrow();
+ const auto& rows = partitionRowset->GetRows();
+
+ // XXX(max42): should we use name table from the rowset, or it coincides with our own name table?
+ auto offsetRowsetColumnId = partitionRowset->GetNameTable()->GetIdOrThrow(OffsetColumnName_);
+
+ THROW_ERROR_EXCEPTION_UNLESS(
+ std::ssize(partitionRowset->GetRows()) <= 1,
+ "The table for consumer %Qv should contain at most one row for partition %v when an old offset is specified",
+ Path_,
+ partitionIndex);
+
+ i64 currentOffset = 0;
+ TTimestamp offsetTimestamp = 0;
+ // If the key doesn't exist, or the offset value is null, the offset is -1 in BigRT terms and 0 in ours.
+ if (!rows.empty()) {
+ const auto& offsetValue = rows[0].Values()[0];
+ YT_VERIFY(offsetValue.Id == offsetRowsetColumnId);
+ offsetTimestamp = offsetValue.Timestamp;
+ if (offsetValue.Type != EValueType::Null) {
+ currentOffset = FromUnversionedValue<i64>(offsetValue);
+ if (DecrementOffset_) {
+ // We need to add 1, since BigRT stores the offset of the last read row.
+ ++currentOffset;
+ }
+ }
+
+ YT_LOG_DEBUG(
+ "Read current offset (Consumer: %v, PartitionIndex: %v, Offset: %v, Timestamp: %v)",
+ Path_,
+ partitionIndex,
+ currentOffset,
+ offsetTimestamp);
+ }
+
+ if (currentOffset != *oldOffset) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::ConsumerOffsetConflict,
+ "Offset conflict at partition %v of consumer %v: expected offset %v, found offset %v",
+ partitionIndex,
+ Path_,
+ *oldOffset,
+ currentOffset)
+ << TErrorAttribute("partition", partitionIndex)
+ << TErrorAttribute("consumer", Path_)
+ << TErrorAttribute("expected_offset", *oldOffset)
+ << TErrorAttribute("current_offset", currentOffset)
+ << TErrorAttribute("current_offset_timestamp", offsetTimestamp);
+ }
+ }
+
+ TUnversionedRowsBuilder rowsBuilder;
+ TUnversionedRowBuilder rowBuilder;
+ for (const auto& value : RowPrefix_) {
+ rowBuilder.AddValue(value);
+ }
+ rowBuilder.AddValue(MakeUnversionedUint64Value(partitionIndex, PartitionIndexColumnId_));
+ if (DecrementOffset_) {
+ if (newOffset >= 1) {
+ // We need to subtract 1, since BigRT stores the offset of the last read row.
+ rowBuilder.AddValue(MakeUnversionedUint64Value(newOffset - 1, OffsetColumnId_));
+ } else {
+ // For BigRT consumers we store 0 (in our terms) by storing null.
+ rowBuilder.AddValue(MakeUnversionedNullValue(OffsetColumnId_));
+ }
+ } else {
+ rowBuilder.AddValue(MakeUnversionedUint64Value(newOffset, OffsetColumnId_));
+ }
+ rowsBuilder.AddRow(rowBuilder.GetRow());
+
+ YT_LOG_DEBUG(
+ "Advancing consumer offset (Path: %v, Partition: %v, Offset: %v -> %v)",
+ Path_,
+ partitionIndex,
+ oldOffset,
+ newOffset);
+ transaction->WriteRows(Path_, NameTable_, rowsBuilder.Build());
+ }
+
+ TFuture<std::vector<TPartitionInfo>> CollectPartitions(
+ const IClientPtr& client,
+ int expectedPartitionCount,
+ bool withLastConsumeTime = false) const override
+ {
+ if (expectedPartitionCount <= 0) {
+ return MakeFuture(std::vector<TPartitionInfo>{});
+ }
+
+ auto selectQuery = Format(
+ "[%v], [%v] from [%v] where ([%v] between 0 and %v) and (%v)",
+ PartitionIndexColumnName_,
+ OffsetColumnName_,
+ Path_,
+ PartitionIndexColumnName_,
+ expectedPartitionCount - 1,
+ RowPrefixCondition_);
+ return BIND(
+ &TGenericConsumerClient::DoCollectPartitions,
+ MakeStrong(this),
+ client,
+ selectQuery,
+ withLastConsumeTime)
+ .AsyncVia(GetCurrentInvoker())
+ .Run()
+ .Apply(BIND([expectedPartitionCount] (const std::vector<TPartitionInfo>& partitionInfos) {
+ YT_VERIFY(std::ssize(partitionInfos) <= expectedPartitionCount);
+
+ std::vector<TPartitionInfo> result(expectedPartitionCount);
+ for (int partitionIndex = 0; partitionIndex < expectedPartitionCount; ++partitionIndex) {
+ result[partitionIndex] = {.PartitionIndex = partitionIndex, .NextRowIndex = 0};
+ }
+ for (const auto& partitionInfo : partitionInfos) {
+ result[partitionInfo.PartitionIndex] = partitionInfo;
+ }
+ return result;
+ }));
+ }
+
+ TFuture<std::vector<TPartitionInfo>> CollectPartitions(
+ const IClientPtr& client,
+ const std::vector<int>& partitionIndexes,
+ bool withLastConsumeTime) const override
+ {
+ auto selectQuery = Format(
+ "[%v], [%v] from [%v] where ([%v] in (%v)) and (%v)",
+ PartitionIndexColumnName_,
+ OffsetColumnName_,
+ Path_,
+ PartitionIndexColumnName_,
+ JoinSeq(",", partitionIndexes),
+ RowPrefixCondition_);
+ return BIND(
+ &TGenericConsumerClient::DoCollectPartitions,
+ MakeStrong(this),
+ client,
+ selectQuery,
+ withLastConsumeTime)
+ .AsyncVia(GetCurrentInvoker())
+ .Run()
+ .Apply(BIND([partitionIndexes] (const std::vector<TPartitionInfo>& partitionInfos) {
+ THashMap<int, TPartitionInfo> indexToPartitionInfo;
+ for (auto partitionIndex : partitionIndexes) {
+ indexToPartitionInfo[partitionIndex] = {
+ .PartitionIndex = partitionIndex,
+ .NextRowIndex = 0,
+ };
+ }
+ for (const auto& partitionInfo : partitionInfos) {
+ indexToPartitionInfo[partitionInfo.PartitionIndex] = partitionInfo;
+ }
+
+ std::vector<TPartitionInfo> result;
+ result.reserve(partitionIndexes.size());
+ for (auto partitionIndex : partitionIndexes) {
+ result.push_back(indexToPartitionInfo[partitionIndex]);
+ }
+
+ return result;
+ }));
+ }
+
+ TFuture<TCrossClusterReference> FetchTargetQueue(const IClientPtr& client) const override
+ {
+ return client->GetNode(Path_ + "/@target_queue")
+ .Apply(BIND([] (const TYsonString& ysonString) {
+ return TCrossClusterReference::FromString(ConvertTo<TString>(ysonString));
+ }));
+ }
+
+ TFuture<TPartitionStatistics> FetchPartitionStatistics(
+ const IClientPtr& client,
+ const TYPath& queue,
+ int partitionIndex) const override
+ {
+ return client->GetNode(queue + "/@tablets")
+ .Apply(BIND([queue, partitionIndex] (const TYsonString& ysonString) -> TPartitionStatistics {
+ auto tabletList = ConvertToNode(ysonString)->AsList();
+
+ for (const auto& tablet : tabletList->GetChildren()) {
+ const auto& tabletMapNode = tablet->AsMap();
+ auto tabletIndex = ConvertTo<i64>(tabletMapNode->FindChild("index"));
+
+ if (partitionIndex == tabletIndex) {
+ auto flushedDataWeight = ConvertTo<i64>(tabletMapNode->FindChild("statistics")->AsMap()->FindChild("uncompressed_data_size"));
+ auto flushedRowCount = ConvertTo<i64>(tabletMapNode->FindChild("flushed_row_count"));
+
+ return {.FlushedDataWeight = flushedDataWeight, .FlushedRowCount = flushedRowCount};
+ }
+ }
+
+ THROW_ERROR_EXCEPTION("Queue %Qv has no tablet with index %v", queue, partitionIndex);
+ }));
+ }
+
+private:
+ const TYPath Path_;
+ const TUnversionedOwningRow RowPrefix_;
+ //! A condition of form ([ColumnName0], [ColumnName1], ...) = (RowPrefix_[0], RowPrefix_[1], ...)
+ //! defining this subconsumer.
+ TString RowPrefixCondition_;
+ const TStringBuf PartitionIndexColumnName_;
+ const TStringBuf OffsetColumnName_;
+ const TTableSchemaPtr& TableSchema_;
+ const TNameTablePtr NameTable_;
+ const int PartitionIndexColumnId_;
+ const int OffsetColumnId_;
+ //! A column filter consisting of PartitionIndexColumnName_ and OffsetColumnName_.
+ const TColumnFilter SubConsumerColumnFilter_;
+
+ // COMPAT(achulkov2): Remove this once we drop support for legacy BigRT consumers.
+ //! Controls whether the offset is decremented before being written to the offset table.
+ //! BigRT stores the offset of the last read row, so for legacy BigRT consumers this option
+ //! should be set to true.
+ bool DecrementOffset_ = false;
+
+ TTableSchemaPtr SubConsumerSchema_;
+
+ std::vector<TPartitionInfo> DoCollectPartitions(
+ const IClientPtr& client,
+ const TString& selectQuery,
+ bool withLastConsumeTime) const
+ {
+ std::vector<TPartitionInfo> result;
+
+ YT_LOG_DEBUG("Collecting partitions (Query: %v)", selectQuery);
+
+ TSelectRowsOptions selectRowsOptions;
+ selectRowsOptions.Timestamp = AsyncLastCommittedTimestamp;
+ auto selectRowsResult = WaitFor(client->SelectRows(selectQuery, selectRowsOptions))
+ .ValueOrThrow();
+
+ // Note that after table construction table schema may have changed.
+ // We must be prepared for that.
+
+ auto partitionIndexRowsetColumnId =
+ selectRowsResult.Rowset->GetNameTable()->FindId(PartitionIndexColumnName_);
+ auto offsetRowsetColumnId = selectRowsResult.Rowset->GetNameTable()->FindId(OffsetColumnName_);
+
+ if (!partitionIndexRowsetColumnId || !offsetRowsetColumnId) {
+ THROW_ERROR_EXCEPTION(
+ "Table must have columns %Qv and %Qv",
+ PartitionIndexColumnName_,
+ OffsetColumnName_);
+ }
+
+ std::vector<ui64> partitionIndices;
+ for (auto row : selectRowsResult.Rowset->GetRows()) {
+
+ YT_VERIFY(row.GetCount() == 2);
+
+ const auto& partitionIndexValue = row[*partitionIndexRowsetColumnId];
+ if (partitionIndexValue.Type == EValueType::Null) {
+ // This is a weird row for partition Null. Just ignore it.
+ continue;
+ }
+ YT_VERIFY(partitionIndexValue.Type == EValueType::Uint64);
+
+ partitionIndices.push_back(FromUnversionedValue<ui64>(partitionIndexValue));
+
+ const auto& offsetValue = row[*offsetRowsetColumnId];
+
+ i64 offset;
+ if (offsetValue.Type == EValueType::Uint64) {
+ offset = FromUnversionedValue<i64>(offsetValue);
+ if (DecrementOffset_) {
+ ++offset;
+ }
+ } else if (offsetValue.Type == EValueType::Null) {
+ offset = 0;
+ } else {
+ YT_ABORT();
+ }
+
+ // NB: in BigRT offsets encode the last read row, while we operate with the first unread row.
+ result.emplace_back(TPartitionInfo{
+ .PartitionIndex = FromUnversionedValue<i64>(partitionIndexValue),
+ .NextRowIndex = offset,
+ });
+ }
+
+ if (!withLastConsumeTime) {
+ return result;
+ }
+
+ // Now do versioned lookups in order to obtain timestamps.
+
+ TUnversionedRowsBuilder builder;
+ for (ui64 partitionIndex : partitionIndices) {
+ TUnversionedRowBuilder rowBuilder;
+ for (const auto& value : RowPrefix_) {
+ rowBuilder.AddValue(value);
+ }
+ rowBuilder.AddValue(MakeUnversionedUint64Value(partitionIndex, PartitionIndexColumnId_));
+ builder.AddRow(rowBuilder.GetRow());
+ }
+
+ TVersionedLookupRowsOptions options;
+ // This allows easier detection of key set change during the query.
+ options.KeepMissingRows = true;
+ options.RetentionConfig = New<TRetentionConfig>();
+ options.RetentionConfig->MaxDataVersions = 1;
+ options.Timestamp = AsyncLastCommittedTimestamp;
+
+ auto versionedRowset = WaitFor(client->VersionedLookupRows(
+ Path_,
+ NameTable_,
+ builder.Build(),
+ options))
+ .ValueOrThrow();
+
+ YT_VERIFY(versionedRowset->GetRows().size() == partitionIndices.size());
+
+ for (const auto& [index, versionedRow] : Enumerate(versionedRowset->GetRows())) {
+ if (versionedRow.GetWriteTimestampCount() < 1) {
+ THROW_ERROR_EXCEPTION("Partition set changed during collection");
+ }
+ auto timestamp = versionedRow.WriteTimestamps()[0];
+ result[index].LastConsumeTime = TimestampToInstant(timestamp).first;
+ }
+
+ return result;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISubConsumerClientPtr CreateBigRTConsumerClient(
+ const TYPath& path,
+ const TTableSchema& schema)
+{
+ if (!schema.IsUniqueKeys()) {
+ THROW_ERROR_EXCEPTION("Consumer schema must have unique keys, schema does not")
+ << TErrorAttribute("actual_schema", schema);
+ }
+
+ if (schema == *BigRTConsumerTableSchema) {
+ return New<TGenericConsumerClient>(
+ path,
+ TUnversionedOwningRow(),
+ "ShardId",
+ "Offset",
+ /*decrementOffset*/ true,
+ BigRTConsumerTableSchema);
+ } else {
+ THROW_ERROR_EXCEPTION("Table schema is not recognized as a valid BigRT consumer schema")
+ << TErrorAttribute("expected_schema", *BigRTConsumerTableSchema)
+ << TErrorAttribute("actual_schema", schema);
+ }
+}
+
+ISubConsumerClientPtr CreateBigRTConsumerClient(
+ const IClientPtr& client,
+ const TYPath& path)
+{
+ auto tableInfo = WaitFor(client->GetTableMountCache()->GetTableInfo(path))
+ .ValueOrThrow();
+ auto schema = tableInfo->Schemas[ETableSchemaKind::Primary];
+
+ return CreateBigRTConsumerClient(path, *schema);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYTConsumerClient
+ : public IConsumerClient
+{
+public:
+ explicit TYTConsumerClient(TYPath path)
+ : Path_(std::move(path))
+ { }
+
+ ISubConsumerClientPtr GetSubConsumerClient(const TCrossClusterReference& queue) const override
+ {
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedStringValue(queue.Cluster, QueueClusterColumnId_));
+ builder.AddValue(MakeUnversionedStringValue(queue.Path, QueuePathColumnId_));
+ auto row = builder.FinishRow();
+
+ auto subConsumerClient = New<TGenericConsumerClient>(
+ Path_,
+ std::move(row),
+ "partition_index",
+ "offset",
+ /*decrementOffset*/ false,
+ YTConsumerTableSchema);
+
+ return subConsumerClient;
+ }
+
+private:
+ TYPath Path_;
+ static const TNameTablePtr NameTable_;
+ static const int QueueClusterColumnId_;
+ static const int QueuePathColumnId_;
+};
+
+const TNameTablePtr TYTConsumerClient::NameTable_ = TNameTable::FromSchema(*YTConsumerTableSchema);
+const int TYTConsumerClient::QueueClusterColumnId_ = TYTConsumerClient::NameTable_->GetId("queue_cluster");
+const int TYTConsumerClient::QueuePathColumnId_ = TYTConsumerClient::NameTable_->GetId("queue_path");
+
+////////////////////////////////////////////////////////////////////////////////
+
+IConsumerClientPtr CreateConsumerClient(
+ const TYPath& path,
+ const TTableSchema& schema)
+{
+ if (!schema.IsUniqueKeys()) {
+ THROW_ERROR_EXCEPTION("Consumer schema must have unique keys, schema does not")
+ << TErrorAttribute("actual_schema", schema);
+ }
+
+ if (schema == *YTConsumerTableSchema) {
+ return New<TYTConsumerClient>(path);
+ } else {
+ THROW_ERROR_EXCEPTION("Table schema %v is not recognized as a valid consumer schema")
+ << TErrorAttribute("expected_schema", *YTConsumerTableSchema)
+ << TErrorAttribute("actual_schema", schema);
+ }
+}
+
+IConsumerClientPtr CreateConsumerClient(
+ const IClientPtr& client,
+ const TYPath& path)
+{
+ auto tableInfo = WaitFor(client->GetTableMountCache()->GetTableInfo(path))
+ .ValueOrThrow();
+ auto schema = tableInfo->Schemas[ETableSchemaKind::Primary];
+
+ return CreateConsumerClient(path, *schema);
+}
+
+ISubConsumerClientPtr CreateSubConsumerClient(
+ const IClientPtr& client,
+ const TYPath& consumerPath,
+ TRichYPath queuePath)
+{
+ auto queueCluster = queuePath.GetCluster();
+ if (!queueCluster) {
+ if (auto clientCluster = client->GetClusterName()) {
+ queueCluster = *clientCluster;
+ }
+ }
+
+ if (!queueCluster) {
+ THROW_ERROR_EXCEPTION(
+ "Cannot create subconsumer for %Qv, could not infer cluster for queue %Qv from attributes or client",
+ consumerPath,
+ queuePath);
+ }
+
+ TCrossClusterReference queueRef;
+ queueRef.Cluster = *queueCluster;
+ queueRef.Path = queuePath.GetPath();
+
+ return CreateConsumerClient(client, consumerPath)->GetSubConsumerClient(queueRef);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/consumer_client.h b/yt/yt/client/queue_client/consumer_client.h
new file mode 100644
index 0000000000..5c239828fa
--- /dev/null
+++ b/yt/yt/client/queue_client/consumer_client.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "public.h"
+#include "common.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPartitionInfo
+{
+ i64 PartitionIndex = -1;
+ // TODO(max42): rename into Offset, this seems like a concise name.
+ i64 NextRowIndex = -1;
+ //! Latest time instant the corresponding partition was consumed.
+ TInstant LastConsumeTime;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Interface representing a consumer table.
+struct ISubConsumerClient
+ : public TRefCounted
+{
+ //! Advance the offset for the given partition, setting it to a new value within the given transaction.
+ //!
+ //! If oldOffset is specified, the current offset is read (within the transaction) and compared with oldOffset.
+ //! If they are equal, the new offset is written, otherwise an exception is thrown.
+ virtual void Advance(
+ const NApi::ITransactionPtr& transaction,
+ int partitionIndex,
+ std::optional<i64> oldOffset,
+ i64 newOffset) const = 0;
+
+ //! Collect partition infos. If there are entries in the consumer table with partition
+ //! indices outside of range [0, expectedPartitionCount), they will be ignored.
+ virtual TFuture<std::vector<TPartitionInfo>> CollectPartitions(
+ const NApi::IClientPtr& client,
+ int expectedPartitionCount,
+ bool withLastConsumeTime = false) const = 0;
+
+ //! Collect partition infos.
+ //! Entries in the consumer table with partition indices not in the given vector will be ignored.
+ virtual TFuture<std::vector<TPartitionInfo>> CollectPartitions(
+ const NApi::IClientPtr& client,
+ const std::vector<int>& partitionIndexes,
+ bool withLastConsumeTime = false) const = 0;
+
+ //! Fetch and parse the target_queue attribute of this consumer.
+ virtual TFuture<TCrossClusterReference> FetchTargetQueue(const NApi::IClientPtr& client) const = 0;
+
+ struct TPartitionStatistics
+ {
+ i64 FlushedDataWeight = 0;
+ i64 FlushedRowCount = 0;
+ };
+
+ // TODO(achulkov2): This should probably handle multiple partition indexes at some point.
+ // TODO(achulkov2): Move this to a separate IQueueClient class?
+ //! Fetch relevant per-tablet statistics for queue.
+ virtual TFuture<TPartitionStatistics> FetchPartitionStatistics(
+ const NApi::IClientPtr& client,
+ const NYPath::TYPath& queue,
+ int partitionIndex) const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISubConsumerClient)
+
+struct IConsumerClient
+ : public TRefCounted
+{
+ virtual ISubConsumerClientPtr GetSubConsumerClient(const TCrossClusterReference& queue) const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IConsumerClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(max42): get rid of the following two methods.
+// They are left as a temporary mean to keep API tests working.
+
+//! Creates a BigRT single-queue consumer client.
+ISubConsumerClientPtr CreateBigRTConsumerClient(
+ const NYPath::TYPath& path,
+ const NTableClient::TTableSchema& schema);
+
+//! Uses the table mount cache to fetch the consumer's schema and make
+//! sure the consumer actually has BigRT consumer schema.
+ISubConsumerClientPtr CreateBigRTConsumerClient(
+ const NApi::IClientPtr& client,
+ const NYPath::TYPath& path);
+
+//! Creates a native YT multi-queue consumer client.
+IConsumerClientPtr CreateConsumerClient(
+ const NYPath::TYPath& path,
+ const NTableClient::TTableSchema& schema);
+
+//! Uses the table mount cache to fetch the consumer's schema and
+//! make sure the consumer actually has YT consumer schema.
+IConsumerClientPtr CreateConsumerClient(
+ const NApi::IClientPtr& client,
+ const NYPath::TYPath& path);
+
+//! Uses the table mount cache to fetch the consumer's schema and
+//! make sure the consumer actually has YT consumer schema.
+//! Uses the given queue path to fetch the corresponding subconsumer.
+//! If no cluster is set for queue, it is inferred from the given client.
+ISubConsumerClientPtr CreateSubConsumerClient(
+ const NApi::IClientPtr& client,
+ const NYPath::TYPath& consumerPath,
+ NYPath::TRichYPath queuePath);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/partition_reader.cpp b/yt/yt/client/queue_client/partition_reader.cpp
new file mode 100644
index 0000000000..f9be907e40
--- /dev/null
+++ b/yt/yt/client/queue_client/partition_reader.cpp
@@ -0,0 +1,481 @@
+#include "partition_reader.h"
+#include "private.h"
+#include "queue_rowset.h"
+#include "common.h"
+#include "consumer_client.h"
+
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NQueueClient {
+
+using namespace NApi;
+using namespace NConcurrency;
+using namespace NProfiling;
+using namespace NTableClient;
+using namespace NTransactionClient;
+using namespace NYPath;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPartitionReader
+ : public IPartitionReader
+{
+public:
+ TPartitionReader(
+ TPartitionReaderConfigPtr config,
+ IClientPtr client,
+ TYPath consumerPath,
+ int partitionIndex)
+ : Config_(std::move(config))
+ , Client_(std::move(client))
+ , ConsumerPath_(std::move(consumerPath))
+ , PartitionIndex_(partitionIndex)
+ , Logger(QueueClientLogger.WithTag("Consumer: %v, Partition: %v", ConsumerPath_, PartitionIndex_))
+ , RowBatchReadOptions_({Config_->MaxRowCount, Config_->MaxDataWeight, Config_->DataWeightPerRowHint})
+ , PullQueueOptions_({.UseNativeTabletNodeApi = Config_->UseNativeTabletNodeApi})
+ { }
+
+ TFuture<void> Open() override
+ {
+ return BIND(&TPartitionReader::DoOpen, MakeStrong(this))
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ }
+
+ TFuture<IPersistentQueueRowsetPtr> Read() override
+ {
+ YT_VERIFY(Opened_);
+
+ return BIND(&TPartitionReader::DoRead, MakeStrong(this))
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ }
+
+private:
+ const TPartitionReaderConfigPtr Config_;
+ const IClientPtr Client_;
+ TYPath ConsumerPath_;
+ int PartitionIndex_;
+ NLogging::TLogger Logger;
+
+ bool Opened_ = false;
+
+ TYPath QueuePath_;
+ i64 ApproximateDataWeightPerRow_ = 0;
+ ISubConsumerClientPtr ConsumerClient_;
+ TQueueRowBatchReadOptions RowBatchReadOptions_;
+ TPullQueueOptions PullQueueOptions_;
+
+ class TPersistentQueueRowset
+ : public IPersistentQueueRowset
+ {
+ public:
+ TPersistentQueueRowset(IQueueRowsetPtr rowset, TWeakPtr<TPartitionReader> partitionReader, i64 currentOffset)
+ : Rowset_(std::move(rowset))
+ , PartitionReader_(std::move(partitionReader))
+ , CurrentOffset_(currentOffset)
+ { }
+
+ const NTableClient::TTableSchemaPtr& GetSchema() const override
+ {
+ return Rowset_->GetSchema();
+ }
+
+ const NTableClient::TNameTablePtr& GetNameTable() const override
+ {
+ return Rowset_->GetNameTable();
+ }
+
+ TSharedRange<NTableClient::TUnversionedRow> GetRows() const override
+ {
+ return Rowset_->GetRows();
+ }
+
+ i64 GetStartOffset() const override
+ {
+ return Rowset_->GetStartOffset();
+ }
+
+ i64 GetFinishOffset() const override
+ {
+ return Rowset_->GetFinishOffset();
+ }
+
+ void Commit(const NApi::ITransactionPtr& transaction) override
+ {
+ YT_VERIFY(transaction);
+
+ if (auto partitionReader = PartitionReader_.Lock()) {
+ // TODO(achulkov2): Check that this is the first uncommitted batch returned to the user and crash otherwise.
+ // Will be much easier to do once we figure out how prefetching & batch storage will look like.
+
+ // TODO(achulkov2): Mark this batch as committed in the partition reader.
+
+ transaction->AdvanceConsumer(
+ partitionReader->ConsumerPath_,
+ partitionReader->PartitionIndex_,
+ CurrentOffset_,
+ GetFinishOffset());
+ } else {
+ THROW_ERROR_EXCEPTION("Partition reader destroyed");
+ }
+ }
+
+ private:
+ IQueueRowsetPtr Rowset_;
+ TWeakPtr<TPartitionReader> PartitionReader_;
+ i64 CurrentOffset_;
+ };
+
+ IPersistentQueueRowsetPtr DoRead()
+ {
+ YT_LOG_DEBUG("Reading rowset");
+ TWallTimer timer;
+
+ if (!Config_->DataWeightPerRowHint && ApproximateDataWeightPerRow_) {
+ RowBatchReadOptions_.DataWeightPerRowHint = ApproximateDataWeightPerRow_;
+ }
+
+ auto currentOffset = FetchCurrentOffset();
+
+ // TODO(achulkov2): Log the options in the RPC method (via SetRequestInfo) instead?
+ YT_LOG_DEBUG(
+ "Pulling from queue (Offset: %v, MaxRowCount: %v, MaxDataWeight: %v, DataWeightPerRowHint: %v)",
+ currentOffset,
+ RowBatchReadOptions_.MaxRowCount,
+ RowBatchReadOptions_.MaxDataWeight,
+ RowBatchReadOptions_.DataWeightPerRowHint);
+ auto rowset = WaitFor(Client_->PullQueue(
+ QueuePath_,
+ currentOffset,
+ PartitionIndex_,
+ RowBatchReadOptions_,
+ PullQueueOptions_))
+ .ValueOrThrow();
+
+ HandleRowset(rowset);
+
+ YT_LOG_DEBUG("Rowset read (WallTime: %v)", timer.GetElapsedTime());
+
+ return New<TPersistentQueueRowset>(rowset, MakeWeak(this), currentOffset);
+
+ }
+
+ void HandleRowset(const IQueueRowsetPtr& rowset)
+ {
+ // TODO(achulkov2): When prefetching is implemented we'll have some sort of struct for holding the batch + stats.
+ // TODO(achulkov2): Don't burn CPU here and get the data weight from PullQueue somehow.
+ auto dataWeight = static_cast<i64>(GetDataWeight(rowset->GetRows()));
+ i64 rowCount = std::ssize(rowset->GetRows());
+
+ RecomputeApproximateDataWeightPerRow(dataWeight, rowCount);
+
+ YT_LOG_DEBUG(
+ "Rowset obtained (RowCount: %v, DataWeight: %v, StartOffset: %v, FinishOffset: %v)",
+ rowCount,
+ dataWeight,
+ rowset->GetStartOffset(),
+ rowset->GetFinishOffset());
+ }
+
+ void RecomputeApproximateDataWeightPerRow(i64 dataWeight, i64 rowCount)
+ {
+ if (rowCount == 0) {
+ return;
+ }
+
+ auto newDataWeightPerRowHint = dataWeight / rowCount;
+
+ if (ApproximateDataWeightPerRow_) {
+ // TODO(achulkov2): Anything better? Variable coefficient?
+ ApproximateDataWeightPerRow_ = (ApproximateDataWeightPerRow_ + newDataWeightPerRowHint) / 2;
+ } else {
+ ApproximateDataWeightPerRow_ = newDataWeightPerRowHint;
+ }
+
+ YT_LOG_DEBUG("Recomputed approximate data weight per row (ApproximateDataWeightPerRow: %v)", ApproximateDataWeightPerRow_);
+ }
+
+ // NB: Can throw.
+ i64 FetchCurrentOffset() const
+ {
+ TWallTimer timer;
+
+ std::vector<int> partitionIndexesToFetch{PartitionIndex_};
+ auto partitions = WaitFor(ConsumerClient_->CollectPartitions(Client_, partitionIndexesToFetch))
+ .ValueOrThrow();
+
+ YT_VERIFY(partitions.size() <= 1);
+
+ i64 currentOffset = 0;
+
+ if (!partitions.empty()) {
+ YT_VERIFY(partitions[0].PartitionIndex == PartitionIndex_);
+ currentOffset = partitions[0].NextRowIndex;
+ }
+
+ YT_LOG_DEBUG("Fetched current offset (Offset: %v, WallTime: %v)", currentOffset, timer.GetElapsedTime());
+ return currentOffset;
+ }
+
+ void DoOpen()
+ {
+ YT_LOG_DEBUG("Opening partition reader");
+
+ ConsumerClient_ = CreateBigRTConsumerClient(Client_, ConsumerPath_);
+
+ QueuePath_ = WaitFor(ConsumerClient_->FetchTargetQueue(Client_))
+ .ValueOrThrow().Path;
+
+ Logger.AddTag("Queue: %v", QueuePath_);
+
+ auto partitionStatistics = WaitFor(ConsumerClient_->FetchPartitionStatistics(Client_, QueuePath_, PartitionIndex_))
+ .ValueOrThrow();
+
+ RecomputeApproximateDataWeightPerRow(partitionStatistics.FlushedDataWeight, partitionStatistics.FlushedRowCount);
+
+ Opened_ = true;
+
+ YT_LOG_DEBUG("Partition reader opened");
+ }
+};
+
+IPartitionReaderPtr CreatePartitionReader(
+ TPartitionReaderConfigPtr config,
+ IClientPtr client,
+ TYPath path,
+ int partitionIndex)
+{
+ return New<TPartitionReader>(std::move(config), std::move(client), std::move(path), partitionIndex);
+}
+
+class TMultiQueueConsumerPartitionReader
+ : public IPartitionReader
+{
+public:
+ TMultiQueueConsumerPartitionReader(
+ TPartitionReaderConfigPtr config,
+ IClientPtr client,
+ TRichYPath consumerPath,
+ TRichYPath queuePath,
+ int partitionIndex)
+ : Config_(std::move(config))
+ , Client_(std::move(client))
+ , ConsumerPath_(std::move(consumerPath))
+ , QueuePath_(std::move(queuePath))
+ , PartitionIndex_(partitionIndex)
+ , RowBatchReadOptions_({Config_->MaxRowCount, Config_->MaxDataWeight, Config_->DataWeightPerRowHint})
+ , Logger(QueueClientLogger.WithTag("Consumer: %v, Queue: %v, Partition: %v", ConsumerPath_, QueuePath_, PartitionIndex_))
+ {
+ PullConsumerOptions_.UseNativeTabletNodeApi = Config_->UseNativeTabletNodeApi;
+ }
+
+ TFuture<void> Open() override
+ {
+ return BIND(&TMultiQueueConsumerPartitionReader::DoOpen, MakeStrong(this))
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ }
+
+ TFuture<IPersistentQueueRowsetPtr> Read() override
+ {
+ YT_VERIFY(Opened_);
+
+ return BIND(&TMultiQueueConsumerPartitionReader::DoRead, MakeStrong(this))
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ }
+
+private:
+ const TPartitionReaderConfigPtr Config_;
+ const IClientPtr Client_;
+ const TRichYPath ConsumerPath_;
+ const TRichYPath QueuePath_;
+ const int PartitionIndex_;
+ const TQueueRowBatchReadOptions RowBatchReadOptions_;
+ const NLogging::TLogger Logger;
+ TPullConsumerOptions PullConsumerOptions_;
+
+ ISubConsumerClientPtr ConsumerClient_;
+ bool Opened_ = false;
+
+ class TPersistentQueueRowset
+ : public IPersistentQueueRowset
+ {
+ public:
+ TPersistentQueueRowset(IQueueRowsetPtr rowset, TWeakPtr<TMultiQueueConsumerPartitionReader> partitionReader, i64 currentOffset)
+ : Rowset_(std::move(rowset))
+ , PartitionReader_(std::move(partitionReader))
+ , CurrentOffset_(currentOffset)
+ { }
+
+ const NTableClient::TTableSchemaPtr& GetSchema() const override
+ {
+ return Rowset_->GetSchema();
+ }
+
+ const NTableClient::TNameTablePtr& GetNameTable() const override
+ {
+ return Rowset_->GetNameTable();
+ }
+
+ TSharedRange<NTableClient::TUnversionedRow> GetRows() const override
+ {
+ return Rowset_->GetRows();
+ }
+
+ i64 GetStartOffset() const override
+ {
+ return Rowset_->GetStartOffset();
+ }
+
+ i64 GetFinishOffset() const override
+ {
+ return Rowset_->GetFinishOffset();
+ }
+
+ void Commit(const NApi::ITransactionPtr& transaction) override
+ {
+ YT_VERIFY(transaction);
+
+ if (auto partitionReader = PartitionReader_.Lock()) {
+ // TODO(achulkov2): Check that this is the first uncommitted batch returned to the user and crash otherwise.
+ // Will be much easier to do once we figure out how prefetching & batch storage will look like.
+
+ // TODO(achulkov2): Mark this batch as committed in the partition reader.
+
+ transaction->AdvanceConsumer(
+ partitionReader->ConsumerPath_.GetPath(),
+ partitionReader->QueuePath_,
+ partitionReader->PartitionIndex_,
+ CurrentOffset_,
+ GetFinishOffset());
+ } else {
+ THROW_ERROR_EXCEPTION("Partition reader destroyed");
+ }
+ }
+
+ private:
+ IQueueRowsetPtr Rowset_;
+ TWeakPtr<TMultiQueueConsumerPartitionReader> PartitionReader_;
+ i64 CurrentOffset_;
+ };
+
+ IPersistentQueueRowsetPtr DoRead()
+ {
+ YT_LOG_DEBUG("Reading rowset");
+ TWallTimer timer;
+
+ auto currentOffset = FetchCurrentOffset();
+
+ YT_LOG_DEBUG(
+ "Pulling from queue (Offset: %v, MaxRowCount: %v, MaxDataWeight: %v, DataWeightPerRowHint: %v)",
+ currentOffset,
+ RowBatchReadOptions_.MaxRowCount,
+ RowBatchReadOptions_.MaxDataWeight,
+ RowBatchReadOptions_.DataWeightPerRowHint);
+ auto asyncRowset = (Config_->UsePullConsumer
+ ? Client_->PullConsumer(
+ ConsumerPath_,
+ QueuePath_,
+ currentOffset,
+ PartitionIndex_,
+ RowBatchReadOptions_,
+ PullConsumerOptions_)
+ : Client_->PullQueue(
+ QueuePath_,
+ currentOffset,
+ PartitionIndex_,
+ RowBatchReadOptions_,
+ PullConsumerOptions_));
+ auto rowset = WaitFor(asyncRowset)
+ .ValueOrThrow();
+
+ HandleRowset(rowset);
+
+ YT_LOG_DEBUG("Rowset read (WallTime: %v)", timer.GetElapsedTime());
+
+ return New<TPersistentQueueRowset>(rowset, MakeWeak(this), currentOffset);
+
+ }
+
+ void HandleRowset(const IQueueRowsetPtr& rowset)
+ {
+ // TODO(achulkov2): When prefetching is implemented we'll have some sort of struct for holding the batch + stats.
+ // TODO(achulkov2): Don't burn CPU here and get the data weight from PullQueue somehow.
+ auto dataWeight = static_cast<i64>(GetDataWeight(rowset->GetRows()));
+ i64 rowCount = std::ssize(rowset->GetRows());
+
+ YT_LOG_DEBUG(
+ "Rowset obtained (RowCount: %v, DataWeight: %v, StartOffset: %v, FinishOffset: %v)",
+ rowCount,
+ dataWeight,
+ rowset->GetStartOffset(),
+ rowset->GetFinishOffset());
+ }
+
+ // NB: Can throw.
+ i64 FetchCurrentOffset() const
+ {
+ TWallTimer timer;
+
+ std::vector<int> partitionIndexesToFetch{PartitionIndex_};
+ auto partitions = WaitFor(ConsumerClient_->CollectPartitions(Client_, partitionIndexesToFetch))
+ .ValueOrThrow();
+
+ YT_VERIFY(partitions.size() <= 1);
+
+ i64 currentOffset = 0;
+
+ if (!partitions.empty()) {
+ YT_VERIFY(partitions[0].PartitionIndex == PartitionIndex_);
+ currentOffset = partitions[0].NextRowIndex;
+ }
+
+ YT_LOG_DEBUG("Fetched current offset (Offset: %v, WallTime: %v)", currentOffset, timer.GetElapsedTime());
+ return currentOffset;
+ }
+
+ void DoOpen()
+ {
+ YT_LOG_DEBUG("Opening partition reader");
+
+ auto queueCluster = QueuePath_.GetCluster();
+ if (!queueCluster) {
+ THROW_ERROR_EXCEPTION("Queue cluster must be specified");
+ }
+
+ ConsumerClient_ = CreateConsumerClient(Client_, ConsumerPath_.GetPath())->GetSubConsumerClient({
+ .Cluster = *queueCluster,
+ .Path = QueuePath_.GetPath(),
+ });
+
+ Opened_ = true;
+
+ YT_LOG_DEBUG("Partition reader opened");
+ }
+};
+
+IPartitionReaderPtr CreateMultiQueueConsumerPartitionReader(
+ TPartitionReaderConfigPtr config,
+ IClientPtr client,
+ TRichYPath consumerPath,
+ TRichYPath queuePath,
+ int partitionIndex)
+{
+ return New<TMultiQueueConsumerPartitionReader>(
+ std::move(config),
+ std::move(client),
+ std::move(consumerPath),
+ std::move(queuePath),
+ partitionIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/partition_reader.h b/yt/yt/client/queue_client/partition_reader.h
new file mode 100644
index 0000000000..019c7189c6
--- /dev/null
+++ b/yt/yt/client/queue_client/partition_reader.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "config.h"
+#include "public.h"
+
+#include <yt/yt/client/api/client.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPersistentQueueRowset
+ : public IQueueRowset
+{
+ //! Stages offset advancement GetStartOffset() -> GetFinishOffset() in the given transaction.
+ virtual void Commit(const NApi::ITransactionPtr& transaction) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPersistentQueueRowset)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPartitionReader
+ : public TRefCounted
+{
+ virtual TFuture<void> Open() = 0;
+ virtual TFuture<IPersistentQueueRowsetPtr> Read() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPartitionReader)
+
+IPartitionReaderPtr CreatePartitionReader(
+ TPartitionReaderConfigPtr config,
+ NApi::IClientPtr client,
+ NYPath::TYPath path,
+ int partitionIndex);
+
+IPartitionReaderPtr CreateMultiQueueConsumerPartitionReader(
+ TPartitionReaderConfigPtr config,
+ NApi::IClientPtr client,
+ NYPath::TRichYPath consumerPath,
+ NYPath::TRichYPath queuePath,
+ int partitionIndex);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/private.h b/yt/yt/client/queue_client/private.h
new file mode 100644
index 0000000000..9b88a00d1e
--- /dev/null
+++ b/yt/yt/client/queue_client/private.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger QueueClientLogger("QueueClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/public.h b/yt/yt/client/queue_client/public.h
new file mode 100644
index 0000000000..ffc1805416
--- /dev/null
+++ b/yt/yt/client/queue_client/public.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+#include <yt/yt/core/misc/error_code.h>
+#include <yt/yt/core/misc/ref_counted.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((ConsumerOffsetConflict) (3100))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(IQueueRowset)
+
+DECLARE_REFCOUNTED_STRUCT(IPersistentQueueRowset)
+
+DECLARE_REFCOUNTED_STRUCT(IConsumerClient)
+DECLARE_REFCOUNTED_STRUCT(ISubConsumerClient)
+
+DECLARE_REFCOUNTED_STRUCT(IPartitionReader)
+DECLARE_REFCOUNTED_CLASS(TPartitionReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/queue_rowset.cpp b/yt/yt/client/queue_client/queue_rowset.cpp
new file mode 100644
index 0000000000..08c33a5fa9
--- /dev/null
+++ b/yt/yt/client/queue_client/queue_rowset.cpp
@@ -0,0 +1,110 @@
+#include "queue_rowset.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NQueueClient {
+
+using namespace NApi;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void VerifyRowBatchReadOptions(const TQueueRowBatchReadOptions& options)
+{
+ if (options.MaxRowCount <= 0) {
+ THROW_ERROR_EXCEPTION("MaxRowCount is non-positive");
+ }
+ if (options.MaxDataWeight <= 0) {
+ THROW_ERROR_EXCEPTION("MaxDataWeight is non-positive");
+ }
+ if (options.DataWeightPerRowHint && *options.DataWeightPerRowHint <= 0) {
+ THROW_ERROR_EXCEPTION("DataWeightPerRowHint is non-positive");
+ }
+}
+
+i64 ComputeRowsToRead(const TQueueRowBatchReadOptions& options)
+{
+ VerifyRowBatchReadOptions(options);
+
+ auto rowsToRead = options.MaxRowCount;
+ if (options.DataWeightPerRowHint) {
+ auto rowsToReadWithHint = options.MaxDataWeight / *options.DataWeightPerRowHint;
+ rowsToRead = std::min(rowsToRead, rowsToReadWithHint);
+ }
+
+ rowsToRead = std::max<i64>(rowsToRead, 1);
+
+ return rowsToRead;
+}
+
+i64 GetStartOffset(const IUnversionedRowsetPtr& rowset)
+{
+ const auto& nameTable = rowset->GetNameTable();
+
+ YT_VERIFY(!rowset->GetRows().Empty());
+
+ auto rowIndexColumnId = nameTable->GetIdOrThrow("$row_index");
+ auto startOffsetValue = rowset->GetRows()[0][rowIndexColumnId];
+ THROW_ERROR_EXCEPTION_UNLESS(
+ startOffsetValue.Type == EValueType::Int64,
+ "Incorrect type %Qlv for $row_index column, %Qlv expected; only ordered dynamic tables are supported as queues",
+ startOffsetValue.Type,
+ EValueType::Int64);
+
+ return startOffsetValue.Data.Int64;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TQueueRowset
+ : public IQueueRowset
+{
+public:
+ TQueueRowset(IUnversionedRowsetPtr rowset, i64 startOffset)
+ : Rowset_(std::move(rowset))
+ , StartOffset_(startOffset)
+ { }
+
+ const TTableSchemaPtr& GetSchema() const override
+ {
+ return Rowset_->GetSchema();
+ }
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return Rowset_->GetNameTable();
+ }
+
+ TSharedRange<TUnversionedRow> GetRows() const override
+ {
+ return Rowset_->GetRows();
+ }
+
+ i64 GetStartOffset() const override
+ {
+ return StartOffset_;
+ }
+
+ i64 GetFinishOffset() const override
+ {
+ return StartOffset_ + std::ssize(GetRows());
+ }
+
+private:
+ const IUnversionedRowsetPtr Rowset_;
+ const i64 StartOffset_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IQueueRowsetPtr CreateQueueRowset(IUnversionedRowsetPtr rowset, i64 startOffset)
+{
+ return New<TQueueRowset>(std::move(rowset), startOffset);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/queue_client/queue_rowset.h b/yt/yt/client/queue_client/queue_rowset.h
new file mode 100644
index 0000000000..33cd30efee
--- /dev/null
+++ b/yt/yt/client/queue_client/queue_rowset.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/rowset.h>
+
+#include <yt/yt/client/table_client/public.h>
+
+namespace NYT::NQueueClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TQueueRowBatchReadOptions
+{
+ i64 MaxRowCount = 1000;
+ //! Currently only used if DataWeightPerRowHint is set.
+ i64 MaxDataWeight = 16_MB;
+
+ //! If set, this value is used to compute the number of rows to read considering the given MaxDataWeight.
+ std::optional<i64> DataWeightPerRowHint;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void VerifyRowBatchReadOptions(const TQueueRowBatchReadOptions& options);
+
+i64 ComputeRowsToRead(const TQueueRowBatchReadOptions& options);
+
+i64 GetStartOffset(const NApi::IUnversionedRowsetPtr& rowset);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IQueueRowset
+ : public NApi::IUnversionedRowset
+{
+ virtual i64 GetStartOffset() const = 0;
+ virtual i64 GetFinishOffset() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IQueueRowset)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IQueueRowsetPtr CreateQueueRowset(NApi::IUnversionedRowsetPtr rowset, i64 startOffset);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NQueueClient
diff --git a/yt/yt/client/scheduler/operation_cache.cpp b/yt/yt/client/scheduler/operation_cache.cpp
new file mode 100644
index 0000000000..4b7a197d29
--- /dev/null
+++ b/yt/yt/client/scheduler/operation_cache.cpp
@@ -0,0 +1,40 @@
+#include "operation_cache.h"
+#include "private.h"
+
+#include <yt/yt/client/api/client.h>
+
+namespace NYT::NScheduler {
+
+using namespace NApi;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOperationCache::TOperationCache(
+ TAsyncExpiringCacheConfigPtr config,
+ THashSet<TString> attributes,
+ NApi::IClientPtr client,
+ NProfiling::TProfiler profiler)
+ : TAsyncExpiringCache(
+ std::move(config),
+ SchedulerLogger.WithTag("Cache: Operation"),
+ std::move(profiler))
+ , Attributes_(std::move(attributes))
+ , Client_(std::move(client))
+{ }
+
+TFuture<TYsonString> TOperationCache::DoGet(const TOperationIdOrAlias& key, bool /*isPeriodicUpdate*/) noexcept
+{
+ auto options = TGetOperationOptions {
+ .Attributes = Attributes_,
+ .IncludeRuntime = true
+ };
+
+ return Client_->GetOperation(key, options).Apply(BIND([] (const TOperation& operation) {
+ return NYson::ConvertToYsonString(operation);
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
diff --git a/yt/yt/client/scheduler/operation_cache.h b/yt/yt/client/scheduler/operation_cache.h
new file mode 100644
index 0000000000..884905635d
--- /dev/null
+++ b/yt/yt/client/scheduler/operation_cache.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/scheduler/operation_id_or_alias.h>
+
+#include <yt/yt/core/misc/async_expiring_cache.h>
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This cache is able to store cached results `GetOperation` for given set of attributes.
+class TOperationCache
+ : public TAsyncExpiringCache<TOperationIdOrAlias, NYson::TYsonString>
+{
+public:
+ TOperationCache(
+ TAsyncExpiringCacheConfigPtr config,
+ THashSet<TString> attributes,
+ NApi::IClientPtr client,
+ NProfiling::TProfiler profiler = {});
+
+private:
+ const THashSet<TString> Attributes_;
+ const NApi::IClientPtr Client_;
+
+ TFuture<NYson::TYsonString> DoGet(
+ const TOperationIdOrAlias& key,
+ bool isPeriodicUpdate) noexcept override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TOperationCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
diff --git a/yt/yt/client/scheduler/operation_id_or_alias-inl.h b/yt/yt/client/scheduler/operation_id_or_alias-inl.h
new file mode 100644
index 0000000000..260795ebff
--- /dev/null
+++ b/yt/yt/client/scheduler/operation_id_or_alias-inl.h
@@ -0,0 +1,57 @@
+#ifndef OPERATION_ID_OR_ALIAS_INL_H_
+#error "Direct inclusion of this file is not allowed, include operation_id_or_alias.h"
+// For the sake of sane code completion.
+#include "operation_id_or_alias.h"
+#endif
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TProtoClass>
+void FromProto(TOperationIdOrAlias* operationIdOrAlias, const TProtoClass& enclosingProtoMessage)
+{
+ using NYT::FromProto;
+
+ switch (enclosingProtoMessage.operation_id_or_alias_case()) {
+ case TProtoClass::OperationIdOrAliasCase::kOperationId: {
+ operationIdOrAlias->Payload = FromProto<TOperationId>(enclosingProtoMessage.operation_id());
+ break;
+ }
+ case TProtoClass::OperationIdOrAliasCase::kOperationAlias: {
+ const auto& operationAlias = enclosingProtoMessage.operation_alias();
+ if (!operationAlias.StartsWith(OperationAliasPrefix)) {
+ THROW_ERROR_EXCEPTION("Operation alias should start with %Qv", OperationAliasPrefix)
+ << TErrorAttribute("operation_alias", operationAlias);
+ }
+ operationIdOrAlias->Payload = operationAlias;
+ break;
+ }
+ case TProtoClass::OperationIdOrAliasCase::OPERATION_ID_OR_ALIAS_NOT_SET: {
+ THROW_ERROR_EXCEPTION("None of operation id and operation alias is set in oneof OperationIdOrAlias proto");
+ }
+ }
+}
+
+template <class TProtoClassPtr>
+void ToProto(TProtoClassPtr enclosingProtoMessage, const TOperationIdOrAlias& operationIdOrAlias)
+{
+ using NYT::ToProto;
+
+ Visit(operationIdOrAlias.Payload,
+ [&] (const TOperationId& operationId) {
+ ToProto(enclosingProtoMessage->mutable_operation_id(), operationId);
+ },
+ [&] (const TString& alias) {
+ enclosingProtoMessage->set_operation_alias(alias);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
diff --git a/yt/yt/client/scheduler/operation_id_or_alias.cpp b/yt/yt/client/scheduler/operation_id_or_alias.cpp
new file mode 100644
index 0000000000..4a64364815
--- /dev/null
+++ b/yt/yt/client/scheduler/operation_id_or_alias.cpp
@@ -0,0 +1,62 @@
+#include "operation_id_or_alias.h"
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString OperationAliasPrefix("*");
+
+TOperationIdOrAlias::TOperationIdOrAlias(TOperationId id)
+ : Payload(id)
+{ }
+
+TOperationIdOrAlias::TOperationIdOrAlias(TString alias)
+ : Payload(std::move(alias))
+{ }
+
+bool TOperationIdOrAlias::operator ==(const TOperationIdOrAlias& other) const
+{
+ return Payload == other.Payload;
+}
+
+TOperationIdOrAlias TOperationIdOrAlias::FromString(TString operationIdOrAlias)
+{
+ if (!operationIdOrAlias.empty() && operationIdOrAlias[0] == '*') {
+ return TOperationIdOrAlias(operationIdOrAlias);
+ } else {
+ return TOperationIdOrAlias(TOperationId::FromString(operationIdOrAlias));
+ }
+}
+
+void FormatValue(TStringBuilderBase* builder, const TOperationIdOrAlias& operationIdOrAlias, TStringBuf /*format*/)
+{
+ Visit(operationIdOrAlias.Payload,
+ [&] (const TString& alias) {
+ builder->AppendFormat("%v", alias);
+ },
+ [&] (const TOperationId& operationId) {
+ builder->AppendFormat("%v", operationId);
+ });
+}
+
+TString ToString(const TOperationIdOrAlias& operationIdOrAlias)
+{
+ return ToStringViaBuilder(operationIdOrAlias);
+}
+
+TOperationIdOrAlias::operator size_t() const
+{
+ size_t result = 0;
+ Visit(Payload,
+ [&] (const TString& alias) {
+ HashCombine(result, alias);
+ },
+ [&] (const TOperationId& operationId) {
+ HashCombine(result, operationId);
+ });
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
diff --git a/yt/yt/client/scheduler/operation_id_or_alias.h b/yt/yt/client/scheduler/operation_id_or_alias.h
new file mode 100644
index 0000000000..db1d7bf956
--- /dev/null
+++ b/yt/yt/client/scheduler/operation_id_or_alias.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "public.h"
+
+#include <variant>
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const TString OperationAliasPrefix;
+
+// This wrapper is needed for ADL in FromProto/ToProto to work.
+struct TOperationIdOrAlias
+{
+ TOperationIdOrAlias() = default;
+ TOperationIdOrAlias(TOperationId id);
+ TOperationIdOrAlias(TString alias);
+
+ static TOperationIdOrAlias FromString(TString operationIdOrAlias);
+
+ std::variant<TOperationId, TString> Payload;
+
+ bool operator ==(const TOperationIdOrAlias& other) const;
+
+ operator size_t() const;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TOperationIdOrAlias& operationIdOrAlias, TStringBuf format);
+TString ToString(const TOperationIdOrAlias& operationIdOrAlias);
+
+// NB: TOperationIdOrAlias corresponds to a oneof group of fields in proto representation,
+// so we use an enclosing proto message object to properly serialize or deserialize it.
+template <class TProtoClass>
+void FromProto(TOperationIdOrAlias* operationIdOrAlias, const TProtoClass& enclosingProtoMessage);
+
+template <class TProtoClassPtr>
+void ToProto(TProtoClassPtr enclosingProtoMessage, const TOperationIdOrAlias& operationIdOrAlias);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
+
+#define OPERATION_ID_OR_ALIAS_INL_H_
+#include "operation_id_or_alias-inl.h"
+#undef OPERATION_ID_OR_ALIAS_INL_H_
diff --git a/yt/yt/client/scheduler/private.h b/yt/yt/client/scheduler/private.h
new file mode 100644
index 0000000000..736361c878
--- /dev/null
+++ b/yt/yt/client/scheduler/private.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger SchedulerLogger("Scheduler");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
+
diff --git a/yt/yt/client/scheduler/public.h b/yt/yt/client/scheduler/public.h
new file mode 100644
index 0000000000..ba1ded9db0
--- /dev/null
+++ b/yt/yt/client/scheduler/public.h
@@ -0,0 +1,158 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/job_tracker_client/public.h>
+
+namespace NYT::NScheduler {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NJobTrackerClient::TJobId;
+using NJobTrackerClient::TOperationId;
+
+DEFINE_ENUM(EOperationType,
+ (Map)
+ (Merge)
+ (Erase)
+ (Sort)
+ (Reduce)
+ (MapReduce)
+ (RemoteCopy)
+ (JoinReduce)
+ (Vanilla)
+);
+
+DEFINE_ENUM(EOperationState,
+ (None)
+ (Starting)
+ (Orphaned)
+ (WaitingForAgent)
+ (Initializing)
+ (Preparing)
+ (Materializing)
+ (ReviveInitializing)
+ (Reviving)
+ (RevivingJobs)
+ (Pending)
+ (Running)
+ (Completing)
+ (Completed)
+ (Aborting)
+ (Aborted)
+ (Failing)
+ (Failed)
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((NoSuchOperation) (200))
+ ((InvalidOperationState) (201))
+ ((TooManyOperations) (202))
+ ((NoSuchJob) (203))
+ ((AgentRevoked) (204))
+ ((OperationFailedOnJobRestart) (210))
+ ((OperationFailedWithInconsistentLocking) (211))
+ ((OperationControllerCrashed) (212))
+ ((TestingError) (213))
+ ((PoolTreesAreUnspecified) (214))
+ ((MaxFailedJobsLimitExceeded) (215))
+ ((OperationFailedToPrepare) (216))
+ ((WatcherHandlerFailed) (217))
+ ((MasterDisconnected) (218))
+ ((NoSuchJobShell) (219))
+);
+
+DEFINE_ENUM(EUnavailableChunkAction,
+ (Fail)
+ (Skip)
+ (Wait)
+);
+
+DEFINE_ENUM(ESchemaInferenceMode,
+ (Auto)
+ (FromInput)
+ (FromOutput)
+);
+
+DEFINE_ENUM(EAbortReason,
+ ((None) ( 0))
+ ((Scheduler) ( 1))
+ ((FailedChunks) ( 2))
+ ((ResourceOverdraft) ( 3))
+ ((Other) ( 4))
+ ((Preemption) ( 5))
+ ((UserRequest) ( 6))
+ ((NodeOffline) ( 7))
+ ((WaitingTimeout) ( 8))
+ ((AccountLimitExceeded) ( 9))
+ ((GetSpecFailed) ( 10))
+ ((Unknown) ( 11))
+ ((RevivalConfirmationTimeout) ( 12))
+ ((IntermediateChunkLimitExceeded) ( 13))
+ ((NodeBanned) ( 14))
+ ((SpeculativeRunWon) ( 15))
+ ((SpeculativeRunLost) ( 16))
+ ((ChunkMappingInvalidated) ( 17))
+ ((NodeWithZeroUserSlots) ( 18))
+ ((NodeSchedulingSegmentChanged) ( 19))
+ ((NodeFairShareTreeChanged) ( 20))
+ ((JobOnUnexpectedNode) ( 21))
+ ((ShallowMergeFailed) ( 22))
+ ((InconsistentJobState) ( 23))
+ // COMPAT(pogorelov)
+ ((JobStatisticsWaitTimeout) ( 24))
+ ((OperationFailed) ( 25))
+ ((JobRevivalDisabled) ( 26))
+ ((BannedInTentativeTree) ( 27))
+ ((Vanished) ( 28))
+ ((Unconfirmed) ( 29))
+ ((Suspended) ( 30))
+ ((ProbingRunWon) ( 31))
+ ((ProbingRunLost) ( 32))
+ ((ProbingToUnsuccessfulJob) ( 33))
+ ((JobProxyFailed) ( 34))
+ ((InterruptionTimeout) ( 35))
+ ((NodeResourceOvercommit) ( 36))
+ ((ProbingCompetitorResultLost) ( 37))
+ ((SpeculativeCompetitorResultLost) ( 38))
+ ((LayerProbingFailed) ( 39))
+ ((LayerProbingResultLost) ( 40))
+ ((LayerProbingToUnsuccessfulJob) ( 41))
+ ((LayerProbingRunLost) ( 42))
+ ((LayerProbingRunWon) ( 43))
+ ((OperationCompleted) ( 44))
+ ((OperationAborted) ( 45))
+ ((OperationFinished) ( 46))
+ ((JobMemoryThrashing) ( 47))
+ ((SchedulingFirst) (100))
+ ((SchedulingTimeout) (101))
+ ((SchedulingResourceOvercommit) (102))
+ ((SchedulingOperationSuspended) (103))
+ ((SchedulingJobSpecThrottling) (104))
+ ((SchedulingOther) (105))
+ ((SchedulingOperationDisabled) (106))
+ ((SchedulingOperationIsNotAlive) (107))
+ ((SchedulingLast) (199))
+);
+
+DEFINE_ENUM(EInterruptReason,
+ ((None) (0))
+ ((Preemption) (1))
+ ((UserRequest) (2))
+ ((JobSplit) (3))
+ ((Unknown) (4))
+ ((JobsDisabledOnNode) (5))
+);
+
+DEFINE_ENUM(EAutoMergeMode,
+ (Disabled)
+ (Relaxed)
+ (Economy)
+ (Manual)
+);
+
+DECLARE_REFCOUNTED_CLASS(TOperationCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NScheduler
diff --git a/yt/yt/client/security_client/access_control.cpp b/yt/yt/client/security_client/access_control.cpp
new file mode 100644
index 0000000000..4df05b77fb
--- /dev/null
+++ b/yt/yt/client/security_client/access_control.cpp
@@ -0,0 +1,64 @@
+#include "access_control.h"
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr TStringBuf AccessControlObjectNamespacePath = "//sys/access_control_object_namespaces/%lv";
+static constexpr TStringBuf AccessControlObjectPath = "//sys/access_control_object_namespaces/%lv/%lv";
+static constexpr TStringBuf AccessControlObjectPrincipalPath = "//sys/access_control_object_namespaces/%lv/%lv/principal";
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAccessControlObjectDescriptor::TAccessControlObjectDescriptor(
+ EAccessControlObjectNamespace accessControlObjectNamespace,
+ EAccessControlObject accessControlObject)
+ : Namespace_(accessControlObjectNamespace)
+ , Name_(accessControlObject)
+{ }
+
+TString TAccessControlObjectDescriptor::GetPath() const
+{
+ return Format(
+ AccessControlObjectPath,
+ Namespace_,
+ Name_);
+}
+
+TString TAccessControlObjectDescriptor::GetPrincipalPath() const
+{
+ return Format(
+ AccessControlObjectPrincipalPath,
+ Namespace_,
+ Name_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAccessControlObjectDescriptor GetAccessControlObjectDescriptor(
+ EAccessControlObject accessControlObjectName)
+{
+ auto iter = AccessControlObjects.find(accessControlObjectName);
+ YT_VERIFY(!iter.IsEnd());
+
+ return iter->second;
+}
+
+TString GetAccessControlObjectNamespacePath(EAccessControlObjectNamespace accessControlObjectNamespace)
+{
+ return Format(AccessControlObjectNamespacePath, accessControlObjectNamespace);
+}
+
+TString GetAccessControlObjectNamespaceName(EAccessControlObjectNamespace accessControlObjectNamespace)
+{
+ return Format("%lv", accessControlObjectNamespace);
+}
+
+TString GetAccessControlObjectName(EAccessControlObject accessControlObject)
+{
+ return Format("%lv", accessControlObject);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
diff --git a/yt/yt/client/security_client/access_control.h b/yt/yt/client/security_client/access_control.h
new file mode 100644
index 0000000000..643b04e5b4
--- /dev/null
+++ b/yt/yt/client/security_client/access_control.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAccessControlObjectDescriptor
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(EAccessControlObjectNamespace, Namespace);
+ DEFINE_BYVAL_RO_PROPERTY(EAccessControlObject, Name);
+
+public:
+ TAccessControlObjectDescriptor(
+ EAccessControlObjectNamespace accessControlObjectNamespace,
+ EAccessControlObject accessControlObject);
+
+ TString GetPath() const;
+ TString GetPrincipalPath() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ACCESS_CONTROL_ENTRY(namespace, name) \
+ {(name), TAccessControlObjectDescriptor((namespace), (name))}
+
+const std::vector<EAccessControlObjectNamespace> AccessControlObjectNamespaces = {
+ EAccessControlObjectNamespace::AdminCommands
+};
+
+const THashMap<EAccessControlObject, TAccessControlObjectDescriptor> AccessControlObjects = {
+ ACCESS_CONTROL_ENTRY(EAccessControlObjectNamespace::AdminCommands, EAccessControlObject::DisableChunkLocations),
+ ACCESS_CONTROL_ENTRY(EAccessControlObjectNamespace::AdminCommands, EAccessControlObject::DestroyChunkLocations),
+ ACCESS_CONTROL_ENTRY(EAccessControlObjectNamespace::AdminCommands, EAccessControlObject::ResurrectChunkLocations)
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAccessControlObjectDescriptor GetAccessControlObjectDescriptor(EAccessControlObject accessControlObjectName);
+
+TString GetAccessControlObjectNamespacePath(EAccessControlObjectNamespace accessControlObjectNamespace);
+
+TString GetAccessControlObjectNamespaceName(EAccessControlObjectNamespace accessControlObjectNamespace);
+
+TString GetAccessControlObjectName(EAccessControlObject accessControlObject);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
diff --git a/yt/yt/client/security_client/acl.cpp b/yt/yt/client/security_client/acl.cpp
new file mode 100644
index 0000000000..69db3d80cc
--- /dev/null
+++ b/yt/yt/client/security_client/acl.cpp
@@ -0,0 +1,201 @@
+#include "acl.h"
+
+#include <yt/yt/core/yson/pull_parser_deserialize.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NSecurityClient {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSerializableAccessControlEntry::TSerializableAccessControlEntry() = default;
+
+TSerializableAccessControlEntry::TSerializableAccessControlEntry(
+ ESecurityAction action,
+ std::vector<TString> subjects,
+ EPermissionSet permissions,
+ EAceInheritanceMode inheritanceMode)
+ : Action(action)
+ , Subjects(std::move(subjects))
+ , Permissions(permissions)
+ , InheritanceMode(inheritanceMode)
+{ }
+
+bool operator == (const TSerializableAccessControlEntry& lhs, const TSerializableAccessControlEntry& rhs)
+{
+ return
+ lhs.Action == rhs.Action &&
+ lhs.Subjects == rhs.Subjects &&
+ lhs.Permissions == rhs.Permissions &&
+ lhs.InheritanceMode == rhs.InheritanceMode &&
+ lhs.Columns == rhs.Columns &&
+ lhs.Vital == rhs.Vital;
+}
+
+bool operator != (const TSerializableAccessControlEntry& lhs, const TSerializableAccessControlEntry& rhs)
+{
+ return !(lhs == rhs);
+}
+
+// NB(levysotsky): We don't use TYsonStruct here
+// because we want to mirror the TAccessControlList structure,
+// and a vector of TYsonStruct-s cannot be declared (as it has no move constructor).
+void Serialize(const TSerializableAccessControlEntry& ace, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("action").Value(ace.Action)
+ .Item("subjects").Value(ace.Subjects)
+ .Item("permissions").Value(ace.Permissions)
+ // TODO(max42): YT-16347.
+ // Do not serialize this field by default
+ .Item("inheritance_mode").Value(ace.InheritanceMode)
+ .OptionalItem("columns", ace.Columns)
+ .OptionalItem("vital", ace.Vital)
+ .EndMap();
+}
+
+static void EnsureCorrect(const TSerializableAccessControlEntry& ace)
+{
+ if (ace.Action == ESecurityAction::Undefined) {
+ THROW_ERROR_EXCEPTION("%Qlv action is not allowed",
+ ESecurityAction::Undefined);
+ }
+
+ // Currently, we allow empty permissions with columns. They seem to be no-op.
+ bool onlyReadOrEmpty = None(ace.Permissions & ~EPermission::Read);
+ if (ace.Columns && !onlyReadOrEmpty) {
+ THROW_ERROR_EXCEPTION("ACE specifying columns may contain only %Qlv permission; found %Qlv",
+ EPermission::Read,
+ ace.Permissions);
+ }
+
+ bool hasRegisterQueueConsumer = Any(ace.Permissions & EPermission::RegisterQueueConsumer);
+ bool onlyRegisterQueueConsumer = ace.Permissions == EPermission::RegisterQueueConsumer;
+
+ if (hasRegisterQueueConsumer && !ace.Vital) {
+ THROW_ERROR_EXCEPTION("Permission %Qlv requires vitality to be specified",
+ EPermission::RegisterQueueConsumer);
+ }
+ if (ace.Vital && !onlyRegisterQueueConsumer) {
+ THROW_ERROR_EXCEPTION("ACE specifying vitality must contain a single %Qlv permission; found %Qlv",
+ EPermission::RegisterQueueConsumer,
+ ace.Permissions);
+ }
+}
+
+void Deserialize(TSerializableAccessControlEntry& ace, NYTree::INodePtr node)
+{
+ using NYTree::Deserialize;
+
+ auto mapNode = node->AsMap();
+
+ Deserialize(ace.Action, mapNode->GetChildOrThrow("action"));
+ Deserialize(ace.Subjects, mapNode->GetChildOrThrow("subjects"));
+ Deserialize(ace.Permissions, mapNode->GetChildOrThrow("permissions"));
+ if (auto inheritanceModeNode = mapNode->FindChild("inheritance_mode")) {
+ Deserialize(ace.InheritanceMode, inheritanceModeNode);
+ } else {
+ ace.InheritanceMode = EAceInheritanceMode::ObjectAndDescendants;
+ }
+ if (auto columnsNode = mapNode->FindChild("columns")) {
+ Deserialize(ace.Columns, columnsNode);
+ } else {
+ ace.Columns.reset();
+ }
+ if (auto vitalNode = mapNode->FindChild("vital")) {
+ Deserialize(ace.Vital, vitalNode);
+ } else {
+ ace.Vital.reset();
+ }
+ EnsureCorrect(ace);
+}
+
+void Deserialize(TSerializableAccessControlEntry& ace, NYson::TYsonPullParserCursor* cursor)
+{
+ auto HasAction = false;
+ auto HasSubjects = false;
+ auto HasPermissions = false;
+ ace.InheritanceMode = EAceInheritanceMode::ObjectAndDescendants;
+ cursor->ParseMap([&] (TYsonPullParserCursor* cursor) {
+ auto key = cursor->GetCurrent().UncheckedAsString();
+ if (key == TStringBuf("action")) {
+ cursor->Next();
+ HasAction = true;
+ Deserialize(ace.Action, cursor);
+ } else if (key == TStringBuf("subjects")) {
+ cursor->Next();
+ HasSubjects = true;
+ Deserialize(ace.Subjects, cursor);
+ } else if (key == TStringBuf("permissions")) {
+ cursor->Next();
+ HasPermissions = true;
+ Deserialize(ace.Permissions, cursor);
+ } else if (key == TStringBuf("inheritance_mode")) {
+ cursor->Next();
+ Deserialize(ace.InheritanceMode, cursor);
+ } else if (key == TStringBuf("columns")) {
+ cursor->Next();
+ Deserialize(ace.Columns, cursor);
+ } else if (key == TStringBuf("vital")) {
+ cursor->Next();
+ Deserialize(ace.Vital, cursor);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+ if (!(HasAction && HasSubjects && HasPermissions)) {
+ THROW_ERROR_EXCEPTION("Error parsing ACE: \"action\", \"subject\" and \"permissions\" fields are required");
+ }
+ EnsureCorrect(ace);
+}
+
+void TSerializableAccessControlEntry::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Action);
+ Persist(context, Subjects);
+ Persist(context, Permissions);
+ Persist(context, InheritanceMode);
+ // NB: Columns and Vital are not persisted since this method is intended only for use in controller.
+}
+
+bool operator == (const TSerializableAccessControlList& lhs, const TSerializableAccessControlList& rhs)
+{
+ return lhs.Entries == rhs.Entries;
+}
+
+bool operator != (const TSerializableAccessControlList& lhs, const TSerializableAccessControlList& rhs)
+{
+ return !(lhs == rhs);
+}
+
+void Serialize(const TSerializableAccessControlList& acl, NYson::IYsonConsumer* consumer)
+{
+ NYTree::Serialize(acl.Entries, consumer);
+}
+
+void Deserialize(TSerializableAccessControlList& acl, NYTree::INodePtr node)
+{
+ NYTree::Deserialize(acl.Entries, node);
+}
+
+void TSerializableAccessControlList::Persist(const TStreamPersistenceContext& context)
+{
+ NYT::Persist(context, Entries);
+}
+
+void Deserialize(TSerializableAccessControlList& acl, NYson::TYsonPullParserCursor* cursor)
+{
+ Deserialize(acl.Entries, cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
diff --git a/yt/yt/client/security_client/acl.h b/yt/yt/client/security_client/acl.h
new file mode 100644
index 0000000000..04ea66221a
--- /dev/null
+++ b/yt/yt/client/security_client/acl.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/permission.h>
+
+#include <vector>
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializableAccessControlEntry
+{
+ ESecurityAction Action = ESecurityAction::Undefined;
+ std::vector<TString> Subjects;
+ NYTree::EPermissionSet Permissions;
+ EAceInheritanceMode InheritanceMode = EAceInheritanceMode::ObjectAndDescendants;
+ std::optional<std::vector<TString>> Columns;
+ std::optional<bool> Vital;
+
+ TSerializableAccessControlEntry(
+ ESecurityAction action,
+ std::vector<TString> subjects,
+ NYTree::EPermissionSet permissions,
+ EAceInheritanceMode inheritanceMode = EAceInheritanceMode::ObjectAndDescendants);
+
+ // Use only for deserialization.
+ TSerializableAccessControlEntry();
+
+ // Used only for persistence in operation controller. Does not work with Columns and Vital fields.
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+bool operator == (const TSerializableAccessControlEntry& lhs, const TSerializableAccessControlEntry& rhs);
+bool operator != (const TSerializableAccessControlEntry& lhs, const TSerializableAccessControlEntry& rhs);
+
+void Serialize(const TSerializableAccessControlEntry& ace, NYson::IYsonConsumer* consumer);
+void Deserialize(TSerializableAccessControlEntry& ace, NYTree::INodePtr node);
+void Deserialize(TSerializableAccessControlEntry& ace, NYson::TYsonPullParserCursor* cursor);
+
+struct TSerializableAccessControlList
+{
+ std::vector<TSerializableAccessControlEntry> Entries;
+
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+bool operator == (const TSerializableAccessControlList& lhs, const TSerializableAccessControlList& rhs);
+bool operator != (const TSerializableAccessControlList& lhs, const TSerializableAccessControlList& rhs);
+
+void Serialize(const TSerializableAccessControlList& acl, NYson::IYsonConsumer* consumer);
+void Deserialize(TSerializableAccessControlList& acl, NYTree::INodePtr node);
+void Deserialize(TSerializableAccessControlList& acl, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
diff --git a/yt/yt/client/security_client/helpers.cpp b/yt/yt/client/security_client/helpers.cpp
new file mode 100644
index 0000000000..1ac9b7c11d
--- /dev/null
+++ b/yt/yt/client/security_client/helpers.cpp
@@ -0,0 +1,81 @@
+#include "helpers.h"
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/ytree/permission.h>
+
+namespace NYT::NSecurityClient {
+
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPath GetUserPath(const TString& name)
+{
+ return "//sys/users/" + ToYPathLiteral(name);
+}
+
+TYPath GetGroupPath(const TString& name)
+{
+ return "//sys/groups/" + ToYPathLiteral(name);
+}
+
+TYPath GetAccountPath(const TString& name)
+{
+ return "//sys/accounts/" + ToYPathLiteral(name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ESecurityAction CheckPermissionsByAclAndSubjectClosure(
+ const TSerializableAccessControlList& acl,
+ const THashSet<TString>& subjectClosure,
+ NYTree::EPermissionSet permissions)
+{
+ NYTree::EPermissionSet allowedPermissions = {};
+ NYTree::EPermissionSet deniedPermissions = {};
+
+ for (const auto& ace : acl.Entries) {
+ if (ace.Action != NSecurityClient::ESecurityAction::Allow && ace.Action != NSecurityClient::ESecurityAction::Deny) {
+ THROW_ERROR_EXCEPTION("Action %Qv is not supported", FormatEnum(ace.Action));
+ }
+ for (const auto& aceSubject : ace.Subjects) {
+ if (subjectClosure.contains(aceSubject)) {
+ if (ace.Action == NSecurityClient::ESecurityAction::Allow) {
+ allowedPermissions |= ace.Permissions;
+ } else {
+ deniedPermissions |= ace.Permissions;
+ }
+ }
+ }
+ }
+
+ return (allowedPermissions & ~deniedPermissions & permissions) == permissions
+ ? ESecurityAction::Allow
+ : ESecurityAction::Deny;
+}
+
+void ValidateSecurityTag(const TSecurityTag& tag)
+{
+ if (tag.empty()) {
+ THROW_ERROR_EXCEPTION("Security tag cannot be empty");
+ }
+ if (tag.length() > MaxSecurityTagLength) {
+ THROW_ERROR_EXCEPTION("Security tag %Qv is too long: %v > %v",
+ tag,
+ tag.length(),
+ MaxSecurityTagLength);
+ }
+}
+
+void ValidateSecurityTags(const std::vector<TSecurityTag>& tags)
+{
+ for (const auto& tag : tags) {
+ ValidateSecurityTag(tag);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
+
diff --git a/yt/yt/client/security_client/helpers.h b/yt/yt/client/security_client/helpers.h
new file mode 100644
index 0000000000..8f361b3aaa
--- /dev/null
+++ b/yt/yt/client/security_client/helpers.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "acl.h"
+#include "public.h"
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYPath::TYPath GetUserPath(const TString& name);
+NYPath::TYPath GetGroupPath(const TString& name);
+NYPath::TYPath GetAccountPath(const TString& name);
+
+////////////////////////////////////////////////////////////////////////////////
+
+ESecurityAction CheckPermissionsByAclAndSubjectClosure(
+ const TSerializableAccessControlList& acl,
+ const THashSet<TString>& subjectClosure,
+ NYTree::EPermissionSet permissions);
+
+void ValidateSecurityTag(const TSecurityTag& tag);
+void ValidateSecurityTags(const std::vector<TSecurityTag>& tags);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
+
diff --git a/yt/yt/client/security_client/public.cpp b/yt/yt/client/security_client/public.cpp
new file mode 100644
index 0000000000..08fecc7570
--- /dev/null
+++ b/yt/yt/client/security_client/public.cpp
@@ -0,0 +1,37 @@
+#include "public.h"
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString RootAccountName("root");
+const TString RootAccountCypressPath("//sys/account_tree");
+const TString TmpAccountName("tmp");
+const TString SysAccountName("sys");
+const TString IntermediateAccountName("intermediate");
+const TString ChunkWiseAccountingMigrationAccountName("chunk_wise_accounting_migration");
+const TString SequoiaAccountName("sequoia");
+
+const TString GuestUserName("guest");
+const TString JobUserName("job");
+const TString SchedulerUserName("scheduler");
+const TString ReplicatorUserName("replicator");
+const TString OwnerUserName("owner");
+const TString FileCacheUserName("file_cache");
+const TString OperationsCleanerUserName("operations_cleaner");
+const TString OperationsClientUserName("operations_client");
+const TString TabletCellChangeloggerUserName("tablet_cell_changelogger");
+const TString TabletCellSnapshotterUserName("tablet_cell_snapshotter");
+const TString TableMountInformerUserName("table_mount_informer");
+const TString AlienCellSynchronizerUserName("alien_cell_synchronizer");
+const TString QueueAgentUserName("queue_agent");
+const TString YqlAgentUserName("yql_agent");
+const TString TabletBalancerUserName("tablet_balancer");
+
+const TString EveryoneGroupName("everyone");
+const TString UsersGroupName("users");
+const TString SuperusersGroupName("superusers");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
diff --git a/yt/yt/client/security_client/public.h b/yt/yt/client/security_client/public.h
new file mode 100644
index 0000000000..68d79767c3
--- /dev/null
+++ b/yt/yt/client/security_client/public.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NSecurityClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TAccountId = NObjectClient::TObjectId;
+using TSubjectId = NObjectClient::TObjectId;
+using TUserId = NObjectClient::TObjectId;
+using TGroupId = NObjectClient::TObjectId;
+using TNetworkProjectId = NObjectClient::TObjectId;
+using TProxyRoleId = NObjectClient::TObjectId;
+using TAccountResourceUsageLeaseId = NObjectClient::TObjectId;
+
+extern const TString RootAccountName;
+extern const TString RootAccountCypressPath;
+extern const TString TmpAccountName;
+extern const TString SysAccountName;
+extern const TString IntermediateAccountName;
+extern const TString ChunkWiseAccountingMigrationAccountName;
+extern const TString SequoiaAccountName;
+
+using NRpc::RootUserName;
+extern const TString GuestUserName;
+extern const TString JobUserName;
+extern const TString SchedulerUserName;
+extern const TString FileCacheUserName;
+extern const TString OperationsCleanerUserName;
+extern const TString OperationsClientUserName;
+extern const TString TabletCellChangeloggerUserName;
+extern const TString TabletCellSnapshotterUserName;
+extern const TString TableMountInformerUserName;
+extern const TString AlienCellSynchronizerUserName;
+extern const TString QueueAgentUserName;
+extern const TString YqlAgentUserName;
+extern const TString TabletBalancerUserName;
+
+extern const TString EveryoneGroupName;
+extern const TString UsersGroupName;
+extern const TString SuperusersGroupName;
+extern const TString ReplicatorUserName;
+extern const TString OwnerUserName;
+
+using TSecurityTag = TString;
+constexpr int MaxSecurityTagLength = 128;
+
+DEFINE_ENUM(ESecurityAction,
+ ((Undefined)(0)) // Intermediate state, used internally.
+ ((Allow) (1)) // Let'em go!
+ ((Deny) (2)) // No way!
+);
+
+DEFINE_ENUM(EAceInheritanceMode,
+ ((ObjectAndDescendants) (0)) // ACE applies both to the object itself and its descendants.
+ ((ObjectOnly) (1)) // ACE applies to the object only.
+ ((DescendantsOnly) (2)) // ACE applies to descendants only.
+ ((ImmediateDescendantsOnly)(3)) // ACE applies to immediate (direct) descendants only.
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((AuthenticationError) (900))
+ ((AuthorizationError) (901))
+ ((AccountLimitExceeded) (902))
+ ((UserBanned) (903))
+ ((RequestQueueSizeLimitExceeded)(904))
+ ((NoSuchAccount) (905))
+ ((NoSuchSubject) (907))
+ ((SafeModeEnabled) (906))
+);
+
+// NB: Changing this list requires reign promotion.
+DEFINE_ENUM(EProxyKind,
+ ((Http) (1))
+ ((Rpc) (2))
+);
+
+DEFINE_ENUM(EAccessControlObjectNamespace,
+ (AdminCommands)
+);
+
+DEFINE_ENUM(EAccessControlObject,
+ (DisableChunkLocations)
+ (DestroyChunkLocations)
+ (ResurrectChunkLocations)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NSecurityClient
+
diff --git a/yt/yt/client/table_client/adapters.cpp b/yt/yt/client/table_client/adapters.cpp
new file mode 100644
index 0000000000..3b8cacc5a4
--- /dev/null
+++ b/yt/yt/client/table_client/adapters.cpp
@@ -0,0 +1,208 @@
+#include "adapters.h"
+#include "row_batch.h"
+
+#include <yt/yt/client/api/table_writer.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+#include <yt/yt/core/concurrency/throughput_throttler.h>
+#include <yt/yt/core/concurrency/periodic_yielder.h>
+
+namespace NYT::NTableClient {
+
+using namespace NApi;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TApiFromSchemalessWriterAdapter
+ : public NApi::ITableWriter
+{
+public:
+ explicit TApiFromSchemalessWriterAdapter(IUnversionedWriterPtr underlyingWriter)
+ : UnderlyingWriter_(std::move(underlyingWriter))
+ { }
+
+ bool Write(TRange<NTableClient::TUnversionedRow> rows) override
+ {
+ return UnderlyingWriter_->Write(rows);
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return UnderlyingWriter_->GetReadyEvent();
+ }
+
+ TFuture<void> Close() override
+ {
+ return UnderlyingWriter_->Close();
+ }
+
+ const NTableClient::TNameTablePtr& GetNameTable() const override
+ {
+ return UnderlyingWriter_->GetNameTable();
+ }
+
+ const NTableClient::TTableSchemaPtr& GetSchema() const override
+ {
+ return UnderlyingWriter_->GetSchema();
+ }
+
+private:
+ const IUnversionedWriterPtr UnderlyingWriter_;
+};
+
+NApi::ITableWriterPtr CreateApiFromSchemalessWriterAdapter(
+ IUnversionedWriterPtr underlyingWriter)
+{
+ return New<TApiFromSchemalessWriterAdapter>(std::move(underlyingWriter));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemalessApiFromWriterAdapter
+ : public IUnversionedWriter
+{
+public:
+ explicit TSchemalessApiFromWriterAdapter(NApi::ITableWriterPtr underlyingWriter)
+ : UnderlyingWriter_(std::move(underlyingWriter))
+ { }
+
+ bool Write(TRange<NTableClient::TUnversionedRow> rows) override
+ {
+ return UnderlyingWriter_->Write(rows);
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return UnderlyingWriter_->GetReadyEvent();
+ }
+
+ TFuture<void> Close() override
+ {
+ return UnderlyingWriter_->Close();
+ }
+
+ const NTableClient::TNameTablePtr& GetNameTable() const override
+ {
+ return UnderlyingWriter_->GetNameTable();
+ }
+
+ const NTableClient::TTableSchemaPtr& GetSchema() const override
+ {
+ return UnderlyingWriter_->GetSchema();
+ }
+
+private:
+ const NApi::ITableWriterPtr UnderlyingWriter_;
+};
+
+IUnversionedWriterPtr CreateSchemalessFromApiWriterAdapter(
+ NApi::ITableWriterPtr underlyingWriter)
+{
+ return New<TSchemalessApiFromWriterAdapter>(std::move(underlyingWriter));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PipeReaderToWriter(
+ const ITableReaderPtr& reader,
+ const IUnversionedRowsetWriterPtr& writer,
+ const TPipeReaderToWriterOptions& options)
+{
+ TPeriodicYielder yielder(TDuration::Seconds(1));
+
+ TRowBatchReadOptions readOptions{
+ .MaxRowsPerRead = options.BufferRowCount,
+ .MaxDataWeightPerRead = options.BufferDataWeight
+ };
+ while (auto batch = reader->Read(readOptions)) {
+ yielder.TryYield();
+
+ if (batch->IsEmpty()) {
+ WaitFor(reader->GetReadyEvent())
+ .ThrowOnError();
+ continue;
+ }
+
+ auto rows = batch->MaterializeRows();
+
+ if (options.ValidateValues) {
+ for (auto row : rows) {
+ for (const auto& value : row) {
+ ValidateStaticValue(value);
+ }
+ }
+ }
+
+ if (options.Throttler) {
+ i64 dataWeight = 0;
+ for (auto row : rows) {
+ dataWeight += GetDataWeight(row);
+ }
+ WaitFor(options.Throttler->Throttle(dataWeight))
+ .ThrowOnError();
+ }
+
+ if (!rows.empty() && options.PipeDelay) {
+ TDelayedExecutor::WaitForDuration(options.PipeDelay);
+ }
+
+ if (!writer->Write(rows)) {
+ WaitFor(writer->GetReadyEvent())
+ .ThrowOnError();
+ }
+ }
+
+ WaitFor(writer->Close())
+ .ThrowOnError();
+}
+
+void PipeInputToOutput(
+ IInputStream* input,
+ IOutputStream* output,
+ i64 bufferBlockSize)
+{
+ struct TWriteBufferTag { };
+ TBlob buffer(GetRefCountedTypeCookie<TWriteBufferTag>(), bufferBlockSize, /*initializeStorage*/ false);
+
+ TPeriodicYielder yielder(TDuration::Seconds(1));
+
+ while (true) {
+ yielder.TryYield();
+
+ size_t length = input->Read(buffer.Begin(), buffer.Size());
+ if (length == 0) {
+ break;
+ }
+
+ output->Write(buffer.Begin(), length);
+ }
+
+ output->Finish();
+}
+
+void PipeInputToOutput(
+ const NConcurrency::IAsyncInputStreamPtr& input,
+ IOutputStream* output,
+ i64 bufferBlockSize)
+{
+ struct TWriteBufferTag { };
+ auto buffer = TSharedMutableRef::Allocate<TWriteBufferTag>(bufferBlockSize, {.InitializeStorage = false});
+
+ while (true) {
+ auto length = WaitFor(input->Read(buffer))
+ .ValueOrThrow();
+
+ if (length == 0) {
+ break;
+ }
+
+ output->Write(buffer.Begin(), length);
+ }
+
+ output->Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/adapters.h b/yt/yt/client/table_client/adapters.h
new file mode 100644
index 0000000000..a897ccb5e5
--- /dev/null
+++ b/yt/yt/client/table_client/adapters.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "public.h"
+#include "unversioned_writer.h"
+
+#include <yt/yt/client/api/table_reader.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedWriterPtr CreateSchemalessFromApiWriterAdapter(
+ NApi::ITableWriterPtr underlyingWriter);
+
+NApi::ITableWriterPtr CreateApiFromSchemalessWriterAdapter(
+ IUnversionedWriterPtr underlyingWriter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPipeReaderToWriterOptions
+{
+ i64 BufferRowCount = 10000;
+ i64 BufferDataWeight = 16_MB;
+ bool ValidateValues = false;
+ NConcurrency::IThroughputThrottlerPtr Throttler;
+ // Used only for testing.
+ TDuration PipeDelay;
+};
+
+void PipeReaderToWriter(
+ const NApi::ITableReaderPtr& reader,
+ const IUnversionedRowsetWriterPtr& writer,
+ const TPipeReaderToWriterOptions& options);
+
+void PipeInputToOutput(
+ IInputStream* input,
+ IOutputStream* output,
+ i64 bufferBlockSize);
+
+void PipeInputToOutput(
+ const NConcurrency::IAsyncInputStreamPtr& input,
+ IOutputStream* output,
+ i64 bufferBlockSize);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/blob_reader.cpp b/yt/yt/client/table_client/blob_reader.cpp
new file mode 100644
index 0000000000..f40e28f031
--- /dev/null
+++ b/yt/yt/client/table_client/blob_reader.cpp
@@ -0,0 +1,219 @@
+#include "blob_reader.h"
+
+#include "name_table.h"
+#include "schema.h"
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/row_batch.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NTableClient {
+
+using namespace NApi;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString TBlobTableSchema::PartIndexColumn = "part_index";
+const TString TBlobTableSchema::DataColumn = "data";
+
+TTableSchemaPtr TBlobTableSchema::ToTableSchema() const
+{
+ auto columns = BlobIdColumns;
+ for (auto& idColumn : columns) {
+ idColumn.SetSortOrder(ESortOrder::Ascending);
+ }
+ columns.emplace_back(PartIndexColumn, EValueType::Int64);
+ columns.back().SetSortOrder(ESortOrder::Ascending);
+ columns.emplace_back(DataColumn, EValueType::String);
+ return New<TTableSchema>(
+ std::move(columns),
+ true, // strict
+ true); // uniqueKeys
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EColumnType,
+ ((PartIndex) (0))
+ ((Data) (1))
+);
+
+class TBlobTableReader
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TBlobTableReader(
+ ITableReaderPtr reader,
+ const std::optional<TString>& partIndexColumnName,
+ const std::optional<TString>& dataColumnName,
+ i64 startPartIndex,
+ const std::optional<i64>& offset,
+ const std::optional<i64>& partSize)
+ : Reader_(std::move(reader))
+ , PartIndexColumnName_(partIndexColumnName ? *partIndexColumnName : TBlobTableSchema::PartIndexColumn)
+ , DataColumnName_(dataColumnName ? *dataColumnName : TBlobTableSchema::DataColumn)
+ , Offset_(offset.value_or(0))
+ , PartSize_(partSize)
+ , PreviousPartSize_(partSize)
+ , NextPartIndex_(startPartIndex)
+ {
+ ColumnIndex_[EColumnType::PartIndex] = Reader_->GetNameTable()->GetIdOrRegisterName(PartIndexColumnName_);
+ ColumnIndex_[EColumnType::Data] = Reader_->GetNameTable()->GetIdOrRegisterName(DataColumnName_);
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ if (!Batch_ || Index_ >= Batch_->GetRowCount()) {
+ Index_ = 0;
+
+ NTableClient::TRowBatchReadOptions options{
+ .MaxRowsPerRead = 1
+ };
+ Batch_ = Reader_->Read(options);
+
+ if (!Batch_) {
+ return MakeFuture<TSharedRef>(TSharedRef());
+ }
+
+ if (Batch_->IsEmpty()) {
+ return Reader_->GetReadyEvent().Apply(
+ BIND(&TBlobTableReader::Read, MakeStrong(this)));
+ }
+ }
+ return MakeFuture(ProcessRow());
+ }
+
+private:
+ const ITableReaderPtr Reader_;
+ const TString PartIndexColumnName_;
+ const TString DataColumnName_;
+
+ i64 Offset_;
+ std::optional<i64> PartSize_;
+ std::optional<i64> PreviousPartSize_;
+
+ IUnversionedRowBatchPtr Batch_;
+ i64 Index_ = 0;
+ i64 NextPartIndex_;
+
+ TEnumIndexedVector<EColumnType, std::optional<size_t>> ColumnIndex_;
+
+ TSharedRef ProcessRow()
+ {
+ auto row = Batch_->MaterializeRows()[Index_++];
+ auto value = GetDataAndValidateRow(row);
+
+ auto holder = MakeSharedRangeHolder(Reader_);
+ auto result = TSharedRef(value.Data.String, value.Length, std::move(holder));
+ if (Offset_ > 0) {
+ if (Offset_ > std::ssize(result)) {
+ THROW_ERROR_EXCEPTION("Offset is out of bounds")
+ << TErrorAttribute("offset", Offset_)
+ << TErrorAttribute("part_size", result.Size())
+ << TErrorAttribute("part_index", NextPartIndex_ - 1);
+ }
+ result = result.Slice(result.Begin() + Offset_, result.End());
+ Offset_ = 0;
+ }
+ return result;
+ }
+
+ TUnversionedValue GetAndValidateValue(
+ TUnversionedRow row,
+ const TString& name,
+ EColumnType columnType,
+ EValueType expectedType)
+ {
+ auto columnIndex = ColumnIndex_[columnType];
+ if (!columnIndex) {
+ THROW_ERROR_EXCEPTION("Column %Qv not found", name);
+ }
+
+ TUnversionedValue columnValue;
+ bool found = false;
+ // NB: It is impossible to determine column index fast in schemaless reader.
+ for (const auto& value : row) {
+ if (value.Id == *columnIndex) {
+ columnValue = value;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ THROW_ERROR_EXCEPTION("Column %Qv not found", name);
+ }
+
+ if (columnValue.Type != expectedType) {
+ THROW_ERROR_EXCEPTION("Column %Qv must be of type %Qlv but has type %Qlv",
+ name,
+ expectedType,
+ columnValue.Type);
+ }
+
+ return columnValue;
+ }
+
+ TUnversionedValue GetDataAndValidateRow(TUnversionedRow row)
+ {
+ auto partIndexValue = GetAndValidateValue(row, PartIndexColumnName_, EColumnType::PartIndex, EValueType::Int64);
+ auto partIndex = partIndexValue.Data.Int64;
+
+ if (partIndex != NextPartIndex_) {
+ THROW_ERROR_EXCEPTION("Values of column %Qv must be consecutive but values %v and %v violate this property",
+ PartIndexColumnName_,
+ NextPartIndex_,
+ partIndex);
+ }
+
+ NextPartIndex_ = partIndex + 1;
+
+ auto value = GetAndValidateValue(row, DataColumnName_, EColumnType::Data, EValueType::String);
+
+ auto isPreviousPartWrong = PartSize_ && *PreviousPartSize_ != *PartSize_;
+ auto isCurrentPartWrong = PartSize_ && value.Length > *PartSize_;
+ if (isPreviousPartWrong || isCurrentPartWrong) {
+ i64 actualSize;
+ i64 wrongPartIndex;
+ if (isPreviousPartWrong) {
+ actualSize = *PreviousPartSize_;
+ wrongPartIndex = partIndex - 1;
+ } else {
+ actualSize = value.Length;
+ wrongPartIndex = partIndex;
+ }
+
+ THROW_ERROR_EXCEPTION("Inconsistent part size")
+ << TErrorAttribute("expected_size", *PartSize_)
+ << TErrorAttribute("actual_size", actualSize)
+ << TErrorAttribute("part_index", wrongPartIndex);
+ }
+ PreviousPartSize_ = value.Length;
+ return value;
+ }
+};
+
+IAsyncZeroCopyInputStreamPtr CreateBlobTableReader(
+ ITableReaderPtr reader,
+ const std::optional<TString>& partIndexColumnName,
+ const std::optional<TString>& dataColumnName,
+ i64 startPartIndex,
+ const std::optional<i64>& offset,
+ const std::optional<i64>& partSize)
+{
+ return New<TBlobTableReader>(
+ std::move(reader),
+ partIndexColumnName,
+ dataColumnName,
+ startPartIndex,
+ offset,
+ partSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/blob_reader.h b/yt/yt/client/table_client/blob_reader.h
new file mode 100644
index 0000000000..71f8810341
--- /dev/null
+++ b/yt/yt/client/table_client/blob_reader.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/api/table_reader.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBlobTableSchema
+{
+ // Names of special blob columns.
+ static const TString PartIndexColumn;
+ static const TString DataColumn;
+
+ // Do not specify anything except name and value
+ // type in all column schemas.
+ std::vector<TColumnSchema> BlobIdColumns;
+
+ TTableSchemaPtr ToTableSchema() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+NConcurrency::IAsyncZeroCopyInputStreamPtr CreateBlobTableReader(
+ NApi::ITableReaderPtr reader,
+ const std::optional<TString>& partIndexColumnName,
+ const std::optional<TString>& dataColumnName,
+ i64 startPartIndex,
+ const std::optional<i64>& offset = std::nullopt,
+ const std::optional<i64>& partSize = std::nullopt);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/check_schema_compatibility.cpp b/yt/yt/client/table_client/check_schema_compatibility.cpp
new file mode 100644
index 0000000000..1541aaa777
--- /dev/null
+++ b/yt/yt/client/table_client/check_schema_compatibility.cpp
@@ -0,0 +1,235 @@
+#include "check_schema_compatibility.h"
+#include "logical_type.h"
+#include "schema.h"
+#include "comparator.h"
+
+#include <yt/yt/client/complex_types/check_type_compatibility.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::pair<ESchemaCompatibility, TError> CheckTableSchemaCompatibilityImpl(
+ const TTableSchema& inputSchema,
+ const TTableSchema& outputSchema,
+ bool ignoreSortOrder)
+{
+ // If output schema is strict, check that input columns are subset of output columns.
+ if (outputSchema.GetStrict()) {
+ if (!inputSchema.GetStrict()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Incompatible strictness: input schema is not strict while output schema is"),
+ };
+ }
+
+ for (const auto& inputColumn : inputSchema.Columns()) {
+ if (!outputSchema.FindColumn(inputColumn.Name())) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v is found in input schema but is missing in output schema",
+ inputColumn.GetDiagnosticNameString()),
+ };
+ }
+ }
+ }
+
+ auto result = std::pair(ESchemaCompatibility::FullyCompatible, TError());
+
+ // Check that columns are the same.
+ for (const auto& outputColumn : outputSchema.Columns()) {
+ const auto* inputColumn = inputSchema.FindColumn(outputColumn.Name());
+ const auto* deletedColumn = inputSchema.FindDeletedColumn(outputColumn.StableName());
+
+ if (deletedColumn) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v in output schema was deleted in the input schema",
+ outputColumn.GetDiagnosticNameString()),
+ };
+ }
+
+ if (inputColumn) {
+ if (inputColumn->StableName() != outputColumn.StableName()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %Qv has stable name %Qv in input and %Qv in output schema",
+ inputColumn->Name(),
+ inputColumn->StableName().Get(),
+ outputColumn.StableName().Get())
+ };
+ }
+
+ auto currentTypeCompatibility = NComplexTypes::CheckTypeCompatibility(
+ inputColumn->LogicalType(), outputColumn.LogicalType());
+
+ if (currentTypeCompatibility.first < result.first) {
+ result = {
+ currentTypeCompatibility.first,
+ TError("Column %v input type is incompatible with output type",
+ inputColumn->GetDiagnosticNameString())
+ << currentTypeCompatibility.second
+ };
+ }
+
+ if (result.first == ESchemaCompatibility::Incompatible) {
+ break;
+ }
+
+ if (outputColumn.Expression() && inputColumn->Expression() != outputColumn.Expression()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v expression mismatch",
+ inputColumn->GetDiagnosticNameString()),
+ };
+ }
+ if (outputColumn.Aggregate() && inputColumn->Aggregate() != outputColumn.Aggregate()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v aggregate mismatch",
+ inputColumn->GetDiagnosticNameString()),
+ };
+ }
+ } else if (outputColumn.Expression()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Unexpected computed column %v in output schema",
+ outputColumn.GetDiagnosticNameString()),
+ };
+ } else if (!inputSchema.GetStrict()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v is present in output schema and is missing in non-strict input schema",
+ outputColumn.GetDiagnosticNameString()),
+ };
+ } else if (outputColumn.Required()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Required column %v is present in output schema and is missing in input schema",
+ outputColumn.GetDiagnosticNameString()),
+ };
+ }
+ }
+
+ for (const auto& deletedOutputColumn : outputSchema.DeletedColumns()) {
+ const auto& stableName = deletedOutputColumn.StableName();
+ const auto* inputColumn = inputSchema.FindColumnByStableName(stableName);
+ const auto* deletedColumn = inputSchema.FindDeletedColumn(stableName);
+
+ if (!inputColumn && !deletedColumn) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Deleted column \"%v\" is missing in the input schema",
+ deletedOutputColumn.StableName().Get())
+ };
+ }
+ }
+
+ for (const auto& deletedInputColumn : inputSchema.DeletedColumns()) {
+ const auto& stableName = deletedInputColumn.StableName();
+ const auto* deletedOutputColumn = outputSchema.FindDeletedColumn(stableName);
+ if (!deletedOutputColumn) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Deleted column \"%v\" must be deleted in the output schema",
+ deletedInputColumn.StableName().Get())
+ };
+ }
+ }
+
+ // Check that we don't lose complex types.
+ // We never want to teleport complex types to schemaless part of the chunk because we want to change their type from
+ // EValueType::Composite to EValueType::Any.
+ if (!outputSchema.GetStrict()) {
+ for (const auto& inputColumn : inputSchema.Columns()) {
+ if (!IsV3Composite(inputColumn.LogicalType())) {
+ continue;
+ }
+ if (!outputSchema.FindColumn(inputColumn.Name())) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Column %v of input schema with complex type %Qv is missing in strict part of output schema",
+ inputColumn.GetDiagnosticNameString(),
+ *inputColumn.LogicalType()),
+ };
+ }
+ }
+ }
+
+ if (ignoreSortOrder) {
+ return result;
+ }
+
+ // Check that output key columns form a prefix of input key columns.
+ int cmp = outputSchema.GetKeyColumnCount() - inputSchema.GetKeyColumnCount();
+ if (cmp > 0) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Output key columns are wider than input key columns"),
+ };
+ }
+
+ if (outputSchema.GetUniqueKeys()) {
+ if (!inputSchema.GetUniqueKeys()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Input schema \"unique_keys\" attribute is false"),
+ };
+ }
+ if (cmp != 0) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Input key columns are wider than output key columns"),
+ };
+ }
+ }
+
+ auto inputKeySchema = inputSchema.ToKeys();
+ auto outputKeySchema = outputSchema.ToKeys();
+
+ for (int index = 0; index < outputKeySchema->GetColumnCount(); ++index) {
+ const auto& inputColumn = inputKeySchema->Columns()[index];
+ const auto& outputColumn = outputKeySchema->Columns()[index];
+ if (inputColumn.StableName() != outputColumn.StableName()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Key columns do not match: input column %v, output column %v",
+ inputColumn.GetDiagnosticNameString(),
+ outputColumn.GetDiagnosticNameString())
+ };
+ }
+ if (inputColumn.SortOrder() != outputColumn.SortOrder()) {
+ return {
+ ESchemaCompatibility::Incompatible,
+ TError("Sort order of column %v does not match: input sort order %Qlv, output sort order %Qlv",
+ inputColumn.GetDiagnosticNameString(),
+ inputColumn.SortOrder(),
+ outputColumn.SortOrder())
+ };
+ }
+ }
+
+ return result;
+}
+
+std::pair<ESchemaCompatibility, TError> CheckTableSchemaCompatibility(
+ const TTableSchema& inputSchema,
+ const TTableSchema& outputSchema,
+ bool ignoreSortOrder)
+{
+ auto result = CheckTableSchemaCompatibilityImpl(inputSchema, outputSchema, ignoreSortOrder);
+ if (result.first != ESchemaCompatibility::FullyCompatible) {
+ result.second = TError(NTableClient::EErrorCode::IncompatibleSchemas, "Table schemas are incompatible")
+ << result.second
+ << TErrorAttribute("input_table_schema", inputSchema)
+ << TErrorAttribute("output_table_schema", outputSchema);
+ }
+ return result;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/check_schema_compatibility.h b/yt/yt/client/table_client/check_schema_compatibility.h
new file mode 100644
index 0000000000..0dac7cb20b
--- /dev/null
+++ b/yt/yt/client/table_client/check_schema_compatibility.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Validates that values from table with inputSchema also match outputSchema.
+//
+// Result pair contains following elements:
+// 1. Level of compatibility of the given schemas.
+// 2. If schemas are fully compatible error is empty otherwise it contains description
+// of incompatibility.
+std::pair<ESchemaCompatibility, TError> CheckTableSchemaCompatibility(
+ const TTableSchema& inputSchema,
+ const TTableSchema& outputSchema,
+ bool ignoreSortOrder);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/chunk_stripe_statistics.cpp b/yt/yt/client/table_client/chunk_stripe_statistics.cpp
new file mode 100644
index 0000000000..829220c548
--- /dev/null
+++ b/yt/yt/client/table_client/chunk_stripe_statistics.cpp
@@ -0,0 +1,68 @@
+#include "chunk_stripe_statistics.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkStripeStatistics::Persist(const NTableClient::TPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist(context, ChunkCount);
+ Persist(context, DataWeight);
+ Persist(context, RowCount);
+ Persist(context, ValueCount);
+ Persist(context, MaxBlockSize);
+}
+
+TChunkStripeStatistics operator + (
+ const TChunkStripeStatistics& lhs,
+ const TChunkStripeStatistics& rhs)
+{
+ TChunkStripeStatistics result;
+ result.ChunkCount = lhs.ChunkCount + rhs.ChunkCount;
+ result.DataWeight = lhs.DataWeight + rhs.DataWeight;
+ result.RowCount = lhs.RowCount + rhs.RowCount;
+ result.ValueCount = lhs.ValueCount + rhs.ValueCount;
+ result.MaxBlockSize = std::max(lhs.MaxBlockSize, rhs.MaxBlockSize);
+ return result;
+}
+
+TChunkStripeStatistics& operator += (
+ TChunkStripeStatistics& lhs,
+ const TChunkStripeStatistics& rhs)
+{
+ lhs.ChunkCount += rhs.ChunkCount;
+ lhs.DataWeight += rhs.DataWeight;
+ lhs.RowCount += rhs.RowCount;
+ lhs.ValueCount += rhs.ValueCount;
+ lhs.MaxBlockSize = std::max(lhs.MaxBlockSize, rhs.MaxBlockSize);
+ return lhs;
+}
+
+TChunkStripeStatisticsVector AggregateStatistics(
+ const TChunkStripeStatisticsVector& statistics)
+{
+ TChunkStripeStatistics sum;
+ for (const auto& stat : statistics) {
+ sum += stat;
+ }
+ return TChunkStripeStatisticsVector(1, sum);
+}
+
+void Serialize(const TChunkStripeStatistics& statistics, NYson::IYsonConsumer* consumer)
+{
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("chunk_count").Value(statistics.ChunkCount)
+ .Item("data_weight").Value(statistics.DataWeight)
+ .Item("row_count").Value(statistics.RowCount)
+ .OptionalItem("value_count", statistics.ValueCount)
+ .OptionalItem("max_block_size", statistics.MaxBlockSize)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/chunk_stripe_statistics.h b/yt/yt/client/table_client/chunk_stripe_statistics.h
new file mode 100644
index 0000000000..b517ab765e
--- /dev/null
+++ b/yt/yt/client/table_client/chunk_stripe_statistics.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "public.h"
+#include "serialize.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TChunkStripeStatistics
+{
+ int ChunkCount = 0;
+ i64 DataWeight = 0;
+ i64 RowCount = 0;
+ i64 ValueCount = 0;
+ i64 MaxBlockSize = 0;
+
+ void Persist(const TPersistenceContext& context);
+};
+
+TChunkStripeStatistics operator + (
+ const TChunkStripeStatistics& lhs,
+ const TChunkStripeStatistics& rhs);
+
+TChunkStripeStatistics& operator += (
+ TChunkStripeStatistics& lhs,
+ const TChunkStripeStatistics& rhs);
+
+using TChunkStripeStatisticsVector = TCompactVector<TChunkStripeStatistics, 1>;
+
+//! Adds up input statistics and returns a single-item vector with the sum.
+TChunkStripeStatisticsVector AggregateStatistics(
+ const TChunkStripeStatisticsVector& statistics);
+
+void Serialize(const TChunkStripeStatistics& statistics, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/column_rename_descriptor.cpp b/yt/yt/client/table_client/column_rename_descriptor.cpp
new file mode 100644
index 0000000000..ca9c9313cf
--- /dev/null
+++ b/yt/yt/client/table_client/column_rename_descriptor.cpp
@@ -0,0 +1,53 @@
+#include "column_rename_descriptor.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/parser.h>
+
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Deserialize(TColumnRenameDescriptors& value, INodePtr node)
+{
+ auto mapNode = node->AsMap();
+ value.clear();
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ value.push_back(TColumnRenameDescriptor());
+ value.back().OriginalName = key;
+ Deserialize(value.back().NewName, child);
+ }
+}
+
+void Serialize(const TColumnRenameDescriptors& value, IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ for (const auto& descriptor : value) {
+ consumer->OnKeyedItem(descriptor.OriginalName);
+ consumer->OnStringScalar(descriptor.NewName);
+ }
+ consumer->OnEndMap();
+}
+
+void ToProto(NProto::TColumnRenameDescriptor* protoDescriptor, const TColumnRenameDescriptor& descriptor)
+{
+ protoDescriptor->set_original_name(descriptor.OriginalName);
+ protoDescriptor->set_new_name(descriptor.NewName);
+}
+
+void FromProto(TColumnRenameDescriptor* descriptor, const NProto::TColumnRenameDescriptor& protoDescriptor)
+{
+ descriptor->OriginalName = protoDescriptor.original_name();
+ descriptor->NewName = protoDescriptor.new_name();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/column_rename_descriptor.h b/yt/yt/client/table_client/column_rename_descriptor.h
new file mode 100644
index 0000000000..8fc277e54d
--- /dev/null
+++ b/yt/yt/client/table_client/column_rename_descriptor.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TColumnRenameDescriptor
+{
+ TString OriginalName;
+ TString NewName;
+};
+
+void Deserialize(TColumnRenameDescriptors& value, NYTree::INodePtr node);
+void Serialize(const TColumnRenameDescriptors& value, NYson::IYsonConsumer* consumer);
+
+void ToProto(NProto::TColumnRenameDescriptor* protoDescriptor, const TColumnRenameDescriptor& descriptor);
+void FromProto(TColumnRenameDescriptor* descriptor, const NProto::TColumnRenameDescriptor& protoDescriptor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/column_sort_schema.cpp b/yt/yt/client/table_client/column_sort_schema.cpp
new file mode 100644
index 0000000000..af14339f88
--- /dev/null
+++ b/yt/yt/client/table_client/column_sort_schema.cpp
@@ -0,0 +1,153 @@
+#include "column_sort_schema.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TColumnSortSchema::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Name);
+ Persist(context, SortOrder);
+}
+
+void Serialize(const TColumnSortSchema& schema, IYsonConsumer* consumer)
+{
+ // COMPAT(gritukan): Serializing columns with ESortOrder::Ascending as map node
+ // will end up with a disaster during 21.1 -> 20.3 CA rollback. Remove this code
+ // when 21.1 will be stable.
+ if (schema.SortOrder == ESortOrder::Ascending) {
+ consumer->OnStringScalar(schema.Name);
+ } else {
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("name").Value(schema.Name)
+ .Item("sort_order").Value(schema.SortOrder)
+ .EndMap();
+ }
+}
+
+void Deserialize(TColumnSortSchema& schema, INodePtr node)
+{
+ if (node->GetType() == ENodeType::Map) {
+ auto mapNode = node->AsMap();
+ Deserialize(schema.Name, mapNode->GetChildOrThrow("name"));
+ Deserialize(schema.SortOrder, mapNode->GetChildOrThrow("sort_order"));
+ } else if (node->GetType() == ENodeType::String) {
+ Deserialize(schema.Name, node);
+ schema.SortOrder = ESortOrder::Ascending;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected type of column sort schema node; expected \"string\" or \"map\", %Qv found",
+ node->GetType());
+ }
+}
+
+void Deserialize(TColumnSortSchema& schema, TYsonPullParserCursor* cursor)
+{
+ Deserialize(schema, ExtractTo<INodePtr>(cursor));
+}
+
+bool operator == (const TColumnSortSchema& lhs, const TColumnSortSchema& rhs)
+{
+ return lhs.Name == rhs.Name && lhs.SortOrder == rhs.SortOrder;
+}
+
+bool operator != (const TColumnSortSchema& lhs, const TColumnSortSchema& rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateSortColumns(const std::vector<TColumnSortSchema>& columns)
+{
+ ValidateKeyColumnCount(columns.size());
+
+ THashSet<TString> names;
+ for (const auto& column : columns) {
+ if (!names.insert(column.Name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate sort column name %Qv",
+ column.Name);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TSortColumnsExt* protoSortColumns,
+ const TSortColumns& sortColumns)
+{
+ for (const auto& sortColumn : sortColumns) {
+ protoSortColumns->add_names(sortColumn.Name);
+ protoSortColumns->add_sort_orders(static_cast<int>(sortColumn.SortOrder));
+ }
+}
+
+void FromProto(
+ TSortColumns* sortColumns,
+ const NProto::TSortColumnsExt& protoSortColumns)
+{
+ YT_VERIFY(protoSortColumns.names_size() == protoSortColumns.sort_orders_size());
+ for (int columnIndex = 0; columnIndex < protoSortColumns.names_size(); ++columnIndex) {
+ TColumnSortSchema sortColumn{
+ .Name = protoSortColumns.names(columnIndex),
+ .SortOrder = CheckedEnumCast<ESortOrder>(protoSortColumns.sort_orders(columnIndex))
+ };
+ sortColumns->push_back(sortColumn);
+ }
+}
+
+void FormatValue(TStringBuilderBase* builder, const TSortColumns& sortColumns, TStringBuf /* format */)
+{
+ builder->AppendFormat("{ColumnNames: %v, Comparator: %v}",
+ GetColumnNames(sortColumns),
+ GetComparator(sortColumns));
+}
+
+TString ToString(const TSortColumns& sortColumns)
+{
+ return ToStringViaBuilder(sortColumns);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyColumns GetColumnNames(const TSortColumns& sortColumns)
+{
+ TKeyColumns keyColumns;
+ keyColumns.reserve(sortColumns.size());
+ for (const auto& sortColumn : sortColumns) {
+ keyColumns.push_back(sortColumn.Name);
+ }
+
+ return keyColumns;
+}
+
+std::vector<ESortOrder> GetSortOrders(const TSortColumns& sortColumns)
+{
+ std::vector<ESortOrder> sortOrders;
+ sortOrders.reserve(sortColumns.size());
+ for (const auto& sortColumn : sortColumns) {
+ sortOrders.push_back(sortColumn.SortOrder);
+ }
+
+ return sortOrders;
+}
+
+TComparator GetComparator(const TSortColumns& sortColumns)
+{
+ return TComparator(GetSortOrders(sortColumns));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/column_sort_schema.h b/yt/yt/client/table_client/column_sort_schema.h
new file mode 100644
index 0000000000..cb9e7fc114
--- /dev/null
+++ b/yt/yt/client/table_client/column_sort_schema.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "public.h"
+#include "comparator.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TColumnSortSchema
+{
+ TString Name;
+ ESortOrder SortOrder;
+
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+void Serialize(const TColumnSortSchema& schema, NYson::IYsonConsumer* consumer);
+void Deserialize(TColumnSortSchema& schema, NYTree::INodePtr node);
+void Deserialize(TColumnSortSchema& schema, NYson::TYsonPullParserCursor* cursor);
+
+bool operator == (const TColumnSortSchema& lhs, const TColumnSortSchema& rhs);
+bool operator != (const TColumnSortSchema& lhs, const TColumnSortSchema& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateSortColumns(const std::vector<TColumnSortSchema>& columns);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TSortColumnsExt* protoSortColumns,
+ const TSortColumns& sortColumns);
+
+void FromProto(
+ TSortColumns* sortColumns,
+ const NProto::TSortColumnsExt& protoSortColumns);
+
+void FormatValue(TStringBuilderBase* builder, const TSortColumns& key, TStringBuf format);
+TString ToString(const TSortColumns& key);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyColumns GetColumnNames(const TSortColumns& sortColumns);
+
+std::vector<ESortOrder> GetSortOrders(const TSortColumns& sortColumns);
+
+TComparator GetComparator(const TSortColumns& sortColumns);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/columnar-inl.h b/yt/yt/client/table_client/columnar-inl.h
new file mode 100644
index 0000000000..4d05349d61
--- /dev/null
+++ b/yt/yt/client/table_client/columnar-inl.h
@@ -0,0 +1,378 @@
+#ifndef COLUMNAR_INL_H_
+#error "Direct inclusion of this file is not allowed, include columnar.h"
+// For the sake of sane code completion.
+#include "columnar.h"
+#endif
+
+#include <library/cpp/yt/coding/zig_zag.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline i64 GetBitmapByteSize(i64 bitCount)
+{
+ return (bitCount + 7) / 8;
+}
+
+inline i64 DecodeStringOffset(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 index)
+{
+ YT_ASSERT(index >= 0 && index <= static_cast<i64>(offsets.Size()));
+ return index == 0
+ ? 0
+ : static_cast<ui32>(avgLength * index + ZigZagDecode64(offsets[index - 1]));
+}
+
+inline std::pair<i64, i64> DecodeStringRange(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 index)
+{
+ if (Y_UNLIKELY(index == 0)) {
+ return {
+ 0,
+ static_cast<i64>(avgLength + ZigZagDecode64(offsets[0]))
+ };
+ } else {
+ auto avgLengthTimesIndex = static_cast<ui32>(avgLength * index);
+ return {
+ static_cast<i64>(avgLengthTimesIndex + ZigZagDecode64(offsets[index - 1])),
+ static_cast<i64>(avgLengthTimesIndex + avgLength + ZigZagDecode64(offsets[index]))
+ };
+ }
+}
+
+namespace {
+
+template <
+ bool WithDictionary,
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T,
+ template <
+ bool WithBaseValue_,
+ bool WithZigZag_,
+ class T_
+ >
+ class TValueDecoder,
+ class TGetter,
+ class TConsumer
+>
+void DecodeVectorRleImpl(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TGetter getter,
+ TConsumer consumer)
+{
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ T currentDecodedValue;
+ i64 thresholdIndex = -1;
+ while (true) {
+ if (currentIndex >= thresholdIndex) {
+ if (currentIndex >= endIndex) {
+ break;
+ }
+ decltype(getter(0)) currentValue;
+ if constexpr(WithDictionary) {
+ auto dictionaryIndex = dictionaryIndexes[currentRleIndex];
+ if (dictionaryIndex == 0) {
+ currentValue = {};
+ } else {
+ currentValue = getter(dictionaryIndex - 1);
+ }
+ } else {
+ currentValue = getter(currentRleIndex);
+ }
+ currentDecodedValue = TValueDecoder<WithBaseValue, WithZigZag, T>::Run(currentValue, baseValue);
+ ++currentRleIndex;
+ thresholdIndex = currentRleIndex < static_cast<i64>(rleIndexes.Size())
+ ? std::min(static_cast<i64>(rleIndexes[currentRleIndex]), endIndex)
+ : endIndex;
+ }
+
+ if (currentIndex + 4 <= thresholdIndex) {
+ // Unrolled loop.
+ consumer(currentDecodedValue);
+ consumer(currentDecodedValue);
+ consumer(currentDecodedValue);
+ consumer(currentDecodedValue);
+ currentIndex += 4;
+ } else {
+ consumer(currentDecodedValue);
+ ++currentIndex;
+ }
+ }
+}
+
+template <
+ bool WithDictionary,
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T,
+ template <
+ bool WithBaseValue_,
+ bool WithZigZag_,
+ class T_
+ >
+ class TValueDecoder,
+ class TGetter,
+ class TConsumer
+>
+void DecodeVectorDirectImpl(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ TRange<ui32> dictionaryIndexes,
+ TGetter getter,
+ TConsumer consumer)
+{
+ for (i64 index = startIndex; index < endIndex; ++index) {
+ decltype(getter(0)) value;
+ if constexpr(WithDictionary) {
+ auto dictionaryIndex = dictionaryIndexes[index];
+ if (dictionaryIndex == 0) {
+ value = {};
+ } else {
+ value = getter(dictionaryIndex - 1);
+ }
+ } else {
+ value = getter(index);
+ }
+ auto decodedValue = TValueDecoder<WithBaseValue, WithZigZag, T>::Run(value, baseValue);
+ consumer(decodedValue);
+ }
+}
+
+template <
+ bool WithRle,
+ bool WithDictionary,
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T,
+ template <
+ bool WithBaseValue_,
+ bool WithZigZag_,
+ class T_
+ >
+ class TValueDecoder,
+ class TGetter,
+ class TConsumer
+>
+void DecodeVectorImpl(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TGetter getter,
+ TConsumer consumer)
+{
+ if constexpr(WithRle) {
+ DecodeVectorRleImpl<WithDictionary, WithBaseValue, WithZigZag, T, TValueDecoder>(
+ startIndex,
+ endIndex,
+ baseValue,
+ dictionaryIndexes,
+ rleIndexes,
+ std::forward<TGetter>(getter),
+ std::forward<TConsumer>(consumer));
+ } else {
+ DecodeVectorDirectImpl<WithDictionary, WithBaseValue, WithZigZag, T, TValueDecoder>(
+ startIndex,
+ endIndex,
+ baseValue,
+ dictionaryIndexes,
+ std::forward<TGetter>(getter),
+ std::forward<TConsumer>(consumer));
+ }
+}
+
+template <
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T
+>
+inline T DecodeIntegerValueImpl(
+ ui64 value,
+ ui64 baseValue)
+{
+ if constexpr(WithBaseValue) {
+ value += baseValue;
+ }
+ if constexpr(WithZigZag) {
+ value = static_cast<ui64>(ZigZagDecode64(value));
+ }
+ return static_cast<T>(value);
+}
+
+template <
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T
+>
+struct TIntegerValueDecoder
+{
+ static T Run(ui64 value, ui64 baseValue)
+ {
+ return DecodeIntegerValueImpl<WithBaseValue, WithZigZag, T>(value, baseValue);
+ }
+};
+
+template <
+ bool WithBaseValue,
+ bool WithZigZag,
+ class T
+>
+struct TIdentityValueDecoder
+{
+ template <class V>
+ static V Run(V value, ui64 /*baseValue*/)
+ {
+ return value;
+ }
+};
+
+template <
+ class T,
+ template <
+ bool WithBaseValue_,
+ bool WithZigZag_,
+ class T_
+ >
+ class TValueDecoder,
+ class TGetter,
+ class TConsumer
+>
+void DecodeVector(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ bool zigZagEncoded,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TGetter getter,
+ TConsumer consumer)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(!rleIndexes || rleIndexes[0] == 0);
+
+ #define XX_0(...) \
+ DecodeVectorImpl<__VA_ARGS__, T, TValueDecoder>( \
+ startIndex, \
+ endIndex, \
+ baseValue, \
+ dictionaryIndexes, \
+ rleIndexes, \
+ std::forward<TGetter>(getter), \
+ std::forward<TConsumer>(consumer));
+
+ #define XX_1(...) \
+ if (zigZagEncoded) { \
+ XX_0(__VA_ARGS__, true) \
+ } else { \
+ XX_0(__VA_ARGS__, false) \
+ }
+
+ #define XX_2(...) \
+ if (baseValue != 0) { \
+ XX_1(__VA_ARGS__, true) \
+ } else { \
+ XX_1(__VA_ARGS__, false) \
+ }
+
+ #define XX_3(...) \
+ if (dictionaryIndexes) { \
+ XX_2(__VA_ARGS__, true) \
+ } else { \
+ XX_2(__VA_ARGS__, false) \
+ }
+
+ #define XX_4() \
+ if (rleIndexes) { \
+ XX_3(true) \
+ } else { \
+ XX_3(false) \
+ }
+
+ XX_4()
+
+ #undef XX_0
+ #undef XX_1
+ #undef XX_2
+ #undef XX_3
+ #undef XX_4
+}
+
+} // namespace
+
+template <
+ class TFetcher,
+ class TConsumer
+>
+void DecodeIntegerVector(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ bool zigZagEncoded,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TFetcher fetcher,
+ TConsumer consumer)
+{
+ DecodeVector<ui64, TIntegerValueDecoder>(
+ startIndex,
+ endIndex,
+ baseValue,
+ zigZagEncoded,
+ dictionaryIndexes,
+ rleIndexes,
+ std::forward<TFetcher>(fetcher),
+ std::forward<TConsumer>(consumer));
+}
+
+template <
+ class T,
+ class TFetcher,
+ class TConsumer
+>
+void DecodeRawVector(
+ i64 startIndex,
+ i64 endIndex,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TFetcher fetcher,
+ TConsumer consumer)
+{
+ DecodeVector<T, TIdentityValueDecoder>(
+ startIndex,
+ endIndex,
+ 0,
+ false,
+ dictionaryIndexes,
+ rleIndexes,
+ std::forward<TFetcher>(fetcher),
+ std::forward<TConsumer>(consumer));
+}
+
+template <class T>
+T DecodeIntegerValue(
+ ui64 value,
+ ui64 baseValue,
+ bool zigZagEncoded)
+{
+ return zigZagEncoded
+ ? DecodeIntegerValueImpl<true, true, T>(value, baseValue)
+ : DecodeIntegerValueImpl<true, false, T>(value, baseValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/columnar.cpp b/yt/yt/client/table_client/columnar.cpp
new file mode 100644
index 0000000000..cce7b42c90
--- /dev/null
+++ b/yt/yt/client/table_client/columnar.cpp
@@ -0,0 +1,838 @@
+#include "columnar.h"
+
+#include <yt/yt/client/table_client/row_base.h>
+
+#include <yt/yt/library/numeric/algorithm_helpers.h>
+
+#include <util/system/cpu_id.h>
+
+#if defined(__clang__) && defined(__x86_64__)
+# include <immintrin.h>
+#endif
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+ui64 SafeReadQword(const ui64* ptr, const char* end)
+{
+ if (reinterpret_cast<const char*>(ptr) >= end) {
+ return 0;
+ }
+ ui64 qword = 0;
+ ::memcpy(&qword, ptr, std::min<size_t>(sizeof(ui64), end - reinterpret_cast<const char*>(ptr)));
+ return qword;
+}
+
+void SafeWriteQword(ui64* ptr, char* end, ui64 qword)
+{
+ if (reinterpret_cast<char*>(ptr) >= end) {
+ return;
+ }
+ ::memcpy(ptr, &qword, std::min<size_t>(sizeof(ui64), end - reinterpret_cast<char*>(ptr)));
+}
+
+template <bool Negate, class T>
+auto MaybeNegateValue(T value)
+{
+ if constexpr(Negate) {
+ value = ~value;
+ }
+ return value;
+}
+
+template <class T, bool Negate>
+std::tuple<const T*, T*> MaybeNegateAndCopyValues(
+ const void* beginInput,
+ const void* endInput,
+ void* output)
+{
+ const auto* currentTypedInput = static_cast<const T*>(beginInput);
+ const auto* endTypedInput = static_cast<const T*>(endInput);
+ auto* currentTypedOutput = static_cast<T*>(output);
+ while (currentTypedInput < endTypedInput) {
+ *currentTypedOutput++ = MaybeNegateValue<Negate>(*currentTypedInput++);
+ }
+ return {currentTypedInput, currentTypedOutput};
+}
+
+template <bool Negate>
+void CopyBitmapRangeToBitmapImpl(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(endIndex <= std::ssize(bitmap) * 8);
+ YT_VERIFY(endIndex - startIndex <= std::ssize(dst) * 8);
+
+ auto bitCount = endIndex - startIndex;
+ auto byteCount = GetBitmapByteSize(bitCount);
+
+ auto startQwordIndexRem = startIndex & 63;
+ auto startQwordIndexQuot = startIndex >> 6;
+
+ auto endQwordIndexQuot = endIndex >> 6;
+
+ const auto* beginQwordInput = reinterpret_cast<const ui64*>(bitmap.Begin()) + startQwordIndexQuot;
+ const auto* endQwordInput = beginQwordInput + endQwordIndexQuot - startQwordIndexQuot;
+ const auto* currentQwordInput = beginQwordInput;
+ auto* currentQwordOutput = reinterpret_cast<ui64*>(dst.Begin());
+
+ auto qwordShift = startQwordIndexRem;
+ auto qwordCoshift = 64 - startQwordIndexRem;
+
+ if (qwordShift == 0) {
+ const auto* beginByteInput = reinterpret_cast<const ui8*>(beginQwordInput);
+ const auto* endByteInput = beginByteInput + byteCount;
+ auto* beginByteOutput = reinterpret_cast<ui8*>(dst.Begin());
+ if constexpr(Negate) {
+ std::tie(currentQwordInput, currentQwordOutput) = MaybeNegateAndCopyValues<ui64, Negate>(
+ currentQwordInput,
+ endQwordInput - 1,
+ currentQwordOutput);
+ MaybeNegateAndCopyValues<ui8, Negate>(
+ currentQwordInput,
+ endByteInput,
+ currentQwordOutput);
+ } else {
+ ::memcpy(beginByteOutput, beginByteInput, byteCount);
+ }
+ return;
+ }
+
+ auto buildOutputQWord = [&] (ui64 qword1, ui64 qword2) {
+ qword1 >>= qwordShift;
+ qword2 &= (1ULL << qwordShift) - 1;
+ qword2 <<= qwordCoshift;
+ return MaybeNegateValue<Negate>(qword1 | qword2);
+ };
+
+ // Head
+ while (currentQwordInput < endQwordInput - 1) {
+ auto qword1 = currentQwordInput[0];
+ auto qword2 = currentQwordInput[1];
+ *currentQwordOutput = buildOutputQWord(qword1, qword2);
+ ++currentQwordInput;
+ ++currentQwordOutput;
+ }
+
+ // Tail
+ while (currentQwordInput <= endQwordInput) {
+ auto qword1 = SafeReadQword(currentQwordInput, bitmap.End());
+ auto qword2 = SafeReadQword(currentQwordInput + 1, bitmap.End());
+ SafeWriteQword(currentQwordOutput, dst.End(), buildOutputQWord(qword1, qword2));
+ ++currentQwordInput;
+ ++currentQwordOutput;
+ }
+}
+
+bool GetBit(TRef bitmap, i64 index)
+{
+ return (bitmap[index >> 3] & (1U << (index & 7))) != 0;
+}
+
+void SetBit(TMutableRef bitmap, i64 index, bool value)
+{
+ auto& byte = bitmap[index >> 3];
+ auto mask = (1U << (index & 7));
+ if (value) {
+ byte |= mask;
+ } else {
+ byte &= ~mask;
+ }
+}
+
+template <class F>
+void BuildBitmapFromRleImpl(
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ F valueFetcher,
+ TMutableRef dst)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(std::ssize(dst) * 8 >= endIndex - startIndex);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ auto currentInputIndex = startRleIndex;
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ i64 thresholdIndex = -1;
+ i64 currentOutputIndex = 0;
+
+ while (currentIndex < endIndex) {
+ #define XX1(currentBoolValue, fastpastCheck) \
+ while (currentIndex < thresholdIndex) { \
+ if (fastpastCheck && (currentOutputIndex & 7) == 0 && currentIndex + 8 <= thresholdIndex) { \
+ auto* currentByteOutput = reinterpret_cast<ui8*>(dst.Begin()) + (currentOutputIndex >> 3); \
+ auto currentByteValue = currentBoolValue ? ~static_cast<ui8>(0) : 0; \
+ while (currentIndex + 8 <= thresholdIndex) { \
+ *currentByteOutput++ = currentByteValue; \
+ currentOutputIndex += 8; \
+ currentIndex += 8; \
+ } \
+ } else { \
+ SetBit(dst, currentOutputIndex, currentBoolValue); \
+ ++currentIndex; \
+ ++currentOutputIndex; \
+ } \
+ }
+
+ #define XX2(currentBoolValue) \
+ if (thresholdIndex - currentIndex >= 16) { \
+ XX1(currentBoolValue, true) \
+ } else { \
+ XX1(currentBoolValue, false) \
+ }
+
+ ++currentRleIndex;
+ thresholdIndex = currentRleIndex < std::ssize(rleIndexes)
+ ? std::min(static_cast<i64>(rleIndexes[currentRleIndex]), endIndex)
+ : endIndex;
+ if (valueFetcher(currentInputIndex++)) {
+ XX2(true)
+ } else {
+ XX2(false)
+ }
+
+ #undef XX1
+ #undef XX2
+ }
+}
+
+template <typename F, typename TByte>
+void BuildBytemapFromRleImpl(
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ F valueFetcher,
+ TMutableRange<TByte> dst)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(std::ssize(dst) == endIndex - startIndex);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ auto currentInputIndex = startRleIndex;
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ bool currentBoolValue;
+ i64 thresholdIndex = -1;
+ i64 currentOutputIndex = 0;
+ while (true) {
+ if (currentIndex >= thresholdIndex) {
+ if (currentIndex >= endIndex) {
+ break;
+ }
+ ++currentRleIndex;
+ thresholdIndex = std::min(
+ endIndex,
+ currentRleIndex < std::ssize(rleIndexes) ? static_cast<i64>(rleIndexes[currentRleIndex]) : Max<i64>());
+ currentBoolValue = valueFetcher(currentInputIndex++);
+ }
+ if ((currentOutputIndex & 7) == 0 && currentIndex + 8 <= thresholdIndex) {
+ auto* currentQwordOutput = reinterpret_cast<ui64*>(dst.Begin()) + (currentOutputIndex >> 3);
+ auto currentQwordValue = currentBoolValue ? 0x0101010101010101ULL : 0ULL;
+ while (currentIndex + 8 <= thresholdIndex) {
+ *currentQwordOutput++ = currentQwordValue;
+ currentOutputIndex += 8;
+ currentIndex += 8;
+ }
+ } else {
+ dst[currentOutputIndex++] = static_cast<TByte>(currentBoolValue);
+ ++currentIndex;
+ }
+ }
+}
+
+#if defined(__clang__) && defined(__x86_64__)
+
+template <typename TByte>
+__attribute__((target("bmi2")))
+void DecodeBytemapFromBitmapImplBmi2(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst)
+{
+ auto index = startIndex;
+ while (index < endIndex) {
+ if ((index & 7) == 0 && index + 8 <= endIndex) {
+ const auto* currentInput = bitmap.Begin() + (index >> 3);
+ const auto* endInput = bitmap.Begin() + (endIndex >> 3);
+ auto* currentOutput = reinterpret_cast<ui64*>(dst.Begin() + (index - startIndex));
+ while (currentInput < endInput) {
+ // Cf. https://stackoverflow.com/questions/52098873/how-to-efficiently-convert-an-8-bit-bitmap-to-array-of-0-1-integers-with-x86-sim
+ constexpr ui64 Mask = 0x0101010101010101;
+ *currentOutput++ = __builtin_ia32_pdep_di(*currentInput++, Mask);
+ }
+ index = (endIndex & ~7);
+ } else {
+ dst[index - startIndex] = GetBit(bitmap, index);
+ ++index;
+ }
+ }
+}
+
+#endif
+
+template <typename TByte>
+void DecodeBytemapFromBitmapImplNoBmi2(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst)
+{
+ for (auto index = startIndex; index < endIndex; ++index) {
+ dst[index - startIndex] = GetBit(bitmap, index);
+ }
+}
+
+} // namespace
+
+void BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRef dst)
+{
+ YT_VERIFY(std::ssize(dst) >= GetBitmapByteSize(std::ssize(dictionaryIndexes)));
+
+ const auto* beginInput = dictionaryIndexes.Begin();
+ const auto* endInput = dictionaryIndexes.End();
+ const auto* endHeadInput = endInput - dictionaryIndexes.Size() % 8;
+ const auto* currentInput = beginInput;
+ auto* currentOutput = reinterpret_cast<ui8*>(dst.Begin());
+
+ // Head
+ while (currentInput < endHeadInput) {
+ ui8 result = 0;
+#define XX(shift) if (currentInput[shift] != 0) result |= (1U << shift);
+ XX(0)
+ XX(1)
+ XX(2)
+ XX(3)
+ XX(4)
+ XX(5)
+ XX(6)
+ XX(7)
+#undef XX
+ *currentOutput++ = result;
+ currentInput += 8;
+ }
+
+ if (currentInput == endInput) {
+ return;
+ }
+
+ // Tail
+ {
+ ui8 mask = 1;
+ ui8 result = 0;
+ while (currentInput < endInput) {
+ if (*currentInput++ != 0) {
+ result |= mask;
+ }
+ mask <<= 1;
+ }
+ *currentOutput++ = result;
+ }
+}
+
+void BuildValidityBitmapFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst)
+{
+ YT_VERIFY(rleIndexes.size() == dictionaryIndexes.size());
+
+ BuildBitmapFromRleImpl(
+ rleIndexes,
+ startIndex,
+ endIndex,
+ [&] (i64 inputIndex) { return dictionaryIndexes[inputIndex] != 0; },
+ dst);
+}
+
+template <typename TByte>
+void BuildNullBytemapFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<TByte> dst)
+{
+ YT_VERIFY(dst.Size() == dictionaryIndexes.Size());
+
+ const auto* beginInput = dictionaryIndexes.Begin();
+ const auto* endInput = dictionaryIndexes.End();
+ const auto* currentInput = beginInput;
+ auto* currentOutput = dst.Begin();
+ while (currentInput < endInput) {
+ *currentOutput++ = static_cast<TByte>(*currentInput++ == 0);
+ }
+}
+
+template <typename TByte>
+void BuildNullBytemapFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst)
+{
+ YT_VERIFY(rleIndexes.size() == dictionaryIndexes.size());
+
+ BuildBytemapFromRleImpl(
+ rleIndexes,
+ startIndex,
+ endIndex,
+ [&] (i64 inputIndex) { return dictionaryIndexes[inputIndex] == 0; },
+ dst);
+}
+
+void BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<ui32> dst)
+{
+ YT_VERIFY(dst.Size() == dictionaryIndexes.Size());
+
+ const auto* beginInput = dictionaryIndexes.Begin();
+ const auto* endInput = dictionaryIndexes.End();
+ const auto* currentInput = beginInput;
+ auto* currentOutput = dst.Begin();
+ while (currentInput < endInput) {
+ // NB: null becomes FFFFFFFF.
+ *currentOutput++ = (*currentInput++) - 1;
+ }
+}
+
+void BuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst)
+{
+ auto* currentOutput = dst.Begin();
+ DecodeRawVector<ui32>(
+ startIndex,
+ endIndex,
+ {},
+ rleIndexes,
+ [&] (auto index) {
+ return dictionaryIndexes[index];
+ },
+ [&] (auto value) {
+ *currentOutput++ = value - 1;
+ });
+ YT_VERIFY(currentOutput == dst.End());
+}
+
+void BuildIotaDictionaryIndexesFromRleIndexes(
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(endIndex - startIndex == std::ssize(dst));
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ auto* currentOutput = dst.Begin();
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ auto currentValue = static_cast<ui32>(-1);
+ i64 thresholdIndex = -1;
+ while (true) {
+ if (currentIndex >= thresholdIndex) {
+ if (currentIndex >= endIndex) {
+ break;
+ }
+ ++currentRleIndex;
+ thresholdIndex = currentRleIndex < static_cast<i64>(rleIndexes.Size())
+ ? std::min(static_cast<i64>(rleIndexes[currentRleIndex]), endIndex)
+ : endIndex;
+ ++currentValue;
+ }
+ *currentOutput++ = currentValue;
+ ++currentIndex;
+ }
+}
+
+i64 CountNullsInDictionaryIndexesWithZeroNull(TRange<ui32> dictionaryIndexes)
+{
+ const auto* beginInput = dictionaryIndexes.Begin();
+ const auto* endInput = dictionaryIndexes.End();
+ const auto* currentInput = beginInput;
+ i64 result = 0;
+ while (currentInput < endInput) {
+ if (*currentInput++ == 0) {
+ ++result;
+ }
+ }
+ return result;
+}
+
+i64 CountNullsInRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ const auto* currentInput = dictionaryIndexes.Begin() + startRleIndex;
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ i64 result = 0;
+ while (currentIndex < endIndex) {
+ ++currentRleIndex;
+ auto thresholdIndex = currentRleIndex < std::ssize(rleIndexes) ? static_cast<i64>(rleIndexes[currentRleIndex]) : Max<i64>();
+ auto currentValue = *currentInput++;
+ auto newIndex = std::min(endIndex, thresholdIndex);
+ if (currentValue == 0) {
+ result += (newIndex - currentIndex);
+ }
+ currentIndex = newIndex;
+ }
+ return result;
+}
+
+i64 CountOnesInBitmap(TRef bitmap, i64 startIndex, i64 endIndex)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(endIndex <= std::ssize(bitmap) * 8);
+
+ if (startIndex == endIndex) {
+ return 0;
+ }
+
+ const auto* qwords = reinterpret_cast<const ui64*>(bitmap.Begin());
+
+ auto startIndexRem = startIndex & 63;
+ auto startIndexQuot = startIndex >> 6;
+
+ auto endIndexRem = endIndex & 63;
+ auto endIndexQuot = endIndex >> 6;
+
+ // Tiny
+ if (startIndexQuot == endIndexQuot) {
+ auto qword = SafeReadQword(qwords + startIndexQuot, bitmap.End());
+ qword &= (1ULL << endIndexRem) - 1;
+ qword >>= startIndexRem;
+ return __builtin_popcountll(qword);
+ }
+
+ i64 result = 0;
+
+ // Head
+ if (startIndexRem != 0) {
+ auto qword = qwords[startIndexQuot];
+ qword >>= startIndexRem;
+ result += __builtin_popcountll(qword);
+ ++startIndexQuot;
+ startIndexRem = 0;
+ }
+
+ // Middle
+ {
+ const auto* currentQword = qwords + startIndexQuot;
+ const auto* endQword = qwords + endIndexQuot;
+ while (currentQword < endQword) {
+ result += __builtin_popcountll(*currentQword++);
+ }
+ }
+
+ // Tail
+ if (endIndexRem != 0) {
+ auto qword = SafeReadQword(qwords + endIndexQuot, bitmap.End());
+ qword &= (1ULL << endIndexRem) - 1;
+ result += __builtin_popcountll(qword);
+ }
+
+ return result;
+}
+
+i64 CountOnesInRleBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ auto currentInputIndex = startRleIndex;
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ i64 result = 0;
+ while (currentIndex < endIndex) {
+ ++currentRleIndex;
+ auto thresholdIndex = currentRleIndex < std::ssize(rleIndexes) ? static_cast<i64>(rleIndexes[currentRleIndex]) : Max<i64>();
+ auto currentValue = GetBit(bitmap, currentInputIndex++);
+ auto newIndex = std::min(endIndex, thresholdIndex);
+ if (currentValue) {
+ result += (newIndex - currentIndex);
+ }
+ currentIndex = newIndex;
+ }
+ return result;
+}
+
+void CopyBitmapRangeToBitmap(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst)
+{
+ CopyBitmapRangeToBitmapImpl<false>(
+ bitmap,
+ startIndex,
+ endIndex,
+ dst);
+}
+
+void CopyBitmapRangeToBitmapNegated(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst)
+{
+ CopyBitmapRangeToBitmapImpl<true>(
+ bitmap,
+ startIndex,
+ endIndex,
+ dst);
+}
+
+template <typename TByte>
+void DecodeBytemapFromBitmap(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(endIndex - startIndex == std::ssize(dst));
+
+#if defined(__clang__) && defined(__x86_64__)
+ if (NX86::CachedHaveBMI2()) {
+ DecodeBytemapFromBitmapImplBmi2(bitmap, startIndex, endIndex, dst);
+ return;
+ }
+#endif
+
+ DecodeBytemapFromBitmapImplNoBmi2(bitmap, startIndex, endIndex, dst);
+}
+
+void BuildValidityBitmapFromRleNullBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst)
+{
+ BuildBitmapFromRleImpl(
+ rleIndexes,
+ startIndex,
+ endIndex,
+ [&] (i64 inputIndex) { return !GetBit(bitmap, inputIndex); },
+ dst);
+}
+
+template <typename TByte>
+void BuildNullBytemapFromRleNullBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst)
+{
+ BuildBytemapFromRleImpl(
+ rleIndexes,
+ startIndex,
+ endIndex,
+ [&] (i64 inputIndex) { return GetBit(bitmap, inputIndex); },
+ dst);
+}
+
+void DecodeStringOffsets(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst)
+{
+ YT_VERIFY(startIndex <= endIndex);
+ YT_VERIFY(std::ssize(dst) == endIndex - startIndex + 1);
+
+ auto* currentOutput = reinterpret_cast<ui32*>(dst.Begin());
+
+ auto startOffset = DecodeStringOffset(offsets, avgLength, startIndex);
+
+ if (startIndex == 0) {
+ // See DecodeStringOffset for a special handing of 0.
+ *currentOutput++ = 0;
+ ++startIndex;
+ }
+
+ // Mind offsets[index - 1] in DecodeStringOffset.
+ const auto* currentInput = offsets.Begin() + startIndex - 1;
+ // No -1 here; will output endIndex - startIndex + 1 offsets.
+ const auto* endInput = offsets.Begin() + endIndex;
+ // See DecodeStringOffset.
+ auto avgLengthTimesIndex = startIndex * avgLength;
+ while (currentInput < endInput) {
+ *currentOutput++ = avgLengthTimesIndex + ZigZagDecode64(*currentInput++) - startOffset;
+ avgLengthTimesIndex += avgLength;
+ }
+}
+
+void DecodeStringPointersAndLengths(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ TRef stringData,
+ TMutableRange<const char*> strings,
+ TMutableRange<i32> lengths)
+{
+ YT_VERIFY(offsets.Size() == strings.Size());
+ YT_VERIFY(offsets.Size() == lengths.Size());
+
+ i64 startOffset = 0;
+ i64 avgLengthTimesIndex = 0;
+ for (size_t index = 0; index < offsets.size(); ++index) {
+ strings[index] = stringData.Begin() + startOffset;
+ avgLengthTimesIndex += avgLength;
+ i64 endOffset = avgLengthTimesIndex + ZigZagDecode64(offsets[index]);
+ i32 length = endOffset - startOffset;
+ lengths[index] = length;
+ startOffset = endOffset;
+ }
+}
+
+
+i64 CountTotalStringLengthInRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TRange<i32> stringLengths,
+ i64 startIndex,
+ i64 endIndex)
+{
+ YT_VERIFY(startIndex >= 0 && startIndex <= endIndex);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ auto startRleIndex = TranslateRleStartIndex(rleIndexes, startIndex);
+ const auto* currentInput = dictionaryIndexes.Begin() + startRleIndex;
+ auto currentIndex = startIndex;
+ auto currentRleIndex = startRleIndex;
+ i64 result = 0;
+ while (currentIndex < endIndex) {
+ ++currentRleIndex;
+ auto thresholdIndex = currentRleIndex < static_cast<i64>(rleIndexes.Size()) ? static_cast<i64>(rleIndexes[currentRleIndex]) : Max<i64>();
+ auto currentDictionaryIndex = *currentInput++;
+ auto newIndex = std::min(endIndex, thresholdIndex);
+ if (currentDictionaryIndex != 0) {
+ result += (newIndex - currentIndex) * stringLengths[currentDictionaryIndex - 1];
+ }
+ currentIndex = newIndex;
+ }
+ return result;
+}
+
+i64 TranslateRleIndex(
+ TRange<ui64> rleIndexes,
+ i64 index)
+{
+ YT_VERIFY(index >= 0);
+ YT_VERIFY(rleIndexes[0] == 0);
+
+ return BinarySearch(
+ static_cast<i64>(0),
+ static_cast<i64>(rleIndexes.size()),
+ [&] (i64 k) {
+ return static_cast<i64>(rleIndexes[k]) <= index;
+ }) - 1;
+}
+
+i64 TranslateRleStartIndex(
+ TRange<ui64> rleIndexes,
+ i64 index)
+{
+ return TranslateRleIndex(rleIndexes, index);
+}
+
+i64 TranslateRleEndIndex(
+ TRange<ui64> rleIndexes,
+ i64 index)
+{
+ YT_VERIFY(index >= 0);
+ if (index == 0) {
+ return 0;
+ }
+ return TranslateRleIndex(rleIndexes, index - 1) + 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template void BuildNullBytemapFromDictionaryIndexesWithZeroNull<ui8>(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<ui8> dst);
+
+template void BuildNullBytemapFromRleDictionaryIndexesWithZeroNull<ui8>(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui8> dst);
+
+template void BuildNullBytemapFromRleNullBitmap<ui8>(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui8> dst);
+
+template void DecodeBytemapFromBitmap<ui8>(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui8> dst);
+
+#if defined(__cpp_char8_t)
+/*
+ * Explicitly instantiate templates with char8_t.
+ * These are used by CHYT for ClickHouse interop.
+ */
+template void BuildNullBytemapFromDictionaryIndexesWithZeroNull<char8_t>(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<char8_t> dst);
+
+template void BuildNullBytemapFromRleDictionaryIndexesWithZeroNull<char8_t>(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<char8_t> dst);
+
+template void BuildNullBytemapFromRleNullBitmap<char8_t>(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<char8_t> dst);
+
+template void DecodeBytemapFromBitmap<char8_t>(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<char8_t> dst);
+#endif
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/columnar.h b/yt/yt/client/table_client/columnar.h
new file mode 100644
index 0000000000..1ac6dcb897
--- /dev/null
+++ b/yt/yt/client/table_client/columnar.h
@@ -0,0 +1,247 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/range.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the minimum number of bytes needed to store a bitmap with #bitCount bits.
+i64 GetBitmapByteSize(i64 bitCount);
+
+//! Builds validity bitmap from #dictionaryIndexes by replacing each zero with
+//! 0-bit and each non-zero with 1-bit.
+void BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRef dst);
+
+//! Same as #BuildValidityBitmapFromDictionaryIndexesWithZeroNull but for RLE.
+void BuildValidityBitmapFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst);
+
+//! Builds validity bitmap from #dictionaryIndexes by replacing each zero with
+//! 1-byte and each non-zero with 0-byte.
+//! The size of #dst must match the size of #dictionaryIndexes.
+template <typename TByte>
+void BuildNullBytemapFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<TByte> dst);
+
+//! Same as #BuildNullBytmapFromDictionaryIndexesWithZeroNull but for RLE.
+template <typename TByte>
+void BuildNullBytemapFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst);
+
+//! Copies dictionary indexes from #dictionaryIndexes to #dst subtracting one.
+//! Zeroes are replaced by unspecified values.
+//! The size of #dst must be exactly |endIndex - startIndex|.
+void BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TMutableRange<ui32> dst);
+
+//! Same as #BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull but for RLE.
+void BuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst);
+
+//! Writes a "iota"-like sequence (0, 1, 2, ...) to #dst indicating
+//! RLE groups in accordance to #rleIndexes.
+//! The size of #dst must be exactly |endIndex - startIndex|.
+void BuildIotaDictionaryIndexesFromRleIndexes(
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst);
+
+//! Counts the number of #indexes equal to zero.
+i64 CountNullsInDictionaryIndexesWithZeroNull(TRange<ui32> indexes);
+
+//! Same as #CountNullsInDictionaryIndexesWithZeroNull but for RLE.
+i64 CountNullsInRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex);
+
+//! Counts the number of 1-bits in range [#startIndex, #endIndex) in #bitmap.
+i64 CountOnesInBitmap(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex);
+
+//! Same as #CountOnesInBitmap but for RLE.
+i64 CountOnesInRleBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex);
+
+//! Copies bits in range [#startIndex, #endIndex) from #bitmap to #dst (representing another bitmap).
+//! #bitmap must be 8-byte aligned and its trailing qword must be readable.
+//! The byte size of #dst be be enough to store |endIndex - startIndex| bits.
+void CopyBitmapRangeToBitmap(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst);
+
+//! Same as #CopyBitmapRangeToBitmap but inverts the bits.
+void CopyBitmapRangeToBitmapNegated(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst);
+
+//! Decodes bits in range [#startIndex, #endIndex) from #bitmap to #dst bytemap
+//! (0 indicates |false|, 1 indicates |true|).
+//! #bitmap must be 8-byte aligned and its trailing qword must be readable.
+//! The size of #dst must be |endIndex - startIndex|.
+template <typename TByte>
+void DecodeBytemapFromBitmap(
+ TRef bitmap,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst);
+
+//! Decodes RLE #bitmap and inverts the bits.
+//! The byte size of #dst be enough to store |endIndex - startIndex| bits.
+void BuildValidityBitmapFromRleNullBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRef dst);
+
+//! Decodes RLE #bitmap into #dst bytemap.
+//! The size of #dst be |endIndex - startIndex|.
+template <typename TByte>
+void BuildNullBytemapFromRleNullBitmap(
+ TRef bitmap,
+ TRange<ui64> rleIndexes,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<TByte> dst);
+
+//! Decodes the starting offset of the #index-th string.
+//! #index must be in range [0, N], where N is the size of #offsets.
+i64 DecodeStringOffset(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 index);
+
+//! Decodes the starting offset and the ending offset of the #index-th string.
+//! #index must be in range [0, N), where N is the size of #offsets.
+std::pair<i64, i64> DecodeStringRange(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 index);
+
+//! Decodes start offsets of strings in [#startIndex, #endIndex)
+//! range given by #offsets. The resulting array contains 32-bit offsets and
+//! must be of size |endIndex - startIndex + 1| (the last element will indicate the
+//! offset where the last string ends).
+void DecodeStringOffsets(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ i64 startIndex,
+ i64 endIndex,
+ TMutableRange<ui32> dst);
+
+//! Decodes start pointers and lengths in [#startIndex, #endIndex) row range.
+//! The size of #strings and #lengths must be |endIndex - startIndex|.
+void DecodeStringPointersAndLengths(
+ TRange<ui32> offsets,
+ ui32 avgLength,
+ TRef stringData,
+ TMutableRange<const char*> strings,
+ TMutableRange<i32> lengths);
+
+//! Computes the total length of RLE and dictionary-encoded strings in
+//! a given row range [#startIndex, #endIndex).
+i64 CountTotalStringLengthInRleDictionaryIndexesWithZeroNull(
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TRange<i32> stringLengths,
+ i64 startIndex,
+ i64 endIndex);
+
+//! Given #rleIndexes, translates #index in actual rowspace to
+//! to the index in RLE rowspace.
+i64 TranslateRleIndex(
+ TRange<ui64> rleIndexes,
+ i64 index);
+
+//! A synonym for TranslateRleIndex.
+i64 TranslateRleStartIndex(
+ TRange<ui64> rleIndexes,
+ i64 index);
+
+//! Regards #index as the end (non-inclusive) index.
+//! Equivalent to |TranslateRleIndex(index - 1) + 1| (unless index = 0).
+i64 TranslateRleEndIndex(
+ TRange<ui64> rleIndexes,
+ i64 index);
+
+//! A unversal decoder for dictionary-encoded and RLE integer vectors.
+//! Values are first retrieved via #fetcher, then
+//! pass through #DecodeIntegerValue and finally are forwarded
+//! to #consumer.
+template <
+ class TFetcher,
+ class TConsumer
+>
+void DecodeIntegerVector(
+ i64 startIndex,
+ i64 endIndex,
+ ui64 baseValue,
+ bool zigZagEncoded,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TFetcher fetcher,
+ TConsumer consumer);
+
+//! A unversal decoder for dictionary-encoded and RLE vectors of raw values.
+//! Values are first retrieved via #fetcher and then are forwarded
+//! to #consumer.
+template <
+ class T,
+ class TFetcher,
+ class TConsumer
+>
+void DecodeRawVector(
+ i64 startIndex,
+ i64 endIndex,
+ TRange<ui32> dictionaryIndexes,
+ TRange<ui64> rleIndexes,
+ TFetcher fetcher,
+ TConsumer consumer);
+
+//! Decodes a single integer from YT chunk representation.
+template <class T>
+T DecodeIntegerValue(
+ ui64 value,
+ ui64 baseValue,
+ bool zigZagEncoded);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define COLUMNAR_INL_H_
+#include "columnar-inl.h"
+#undef COLUMNAR_INL_H_
diff --git a/yt/yt/client/table_client/columnar_statistics.cpp b/yt/yt/client/table_client/columnar_statistics.cpp
new file mode 100644
index 0000000000..a4023dca99
--- /dev/null
+++ b/yt/yt/client/table_client/columnar_statistics.cpp
@@ -0,0 +1,71 @@
+#include "columnar_statistics.h"
+
+#include <library/cpp/iterator/functools.h>
+
+#include <numeric>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNamedColumnarStatistics& TNamedColumnarStatistics::operator+=(const TNamedColumnarStatistics& other)
+{
+ for (const auto& [columnName, dataWeight] : other.ColumnDataWeights) {
+ ColumnDataWeights[columnName] += dataWeight;
+ }
+ if (other.TimestampTotalWeight) {
+ TimestampTotalWeight = TimestampTotalWeight.value_or(0) + *other.TimestampTotalWeight;
+ }
+ LegacyChunkDataWeight += other.LegacyChunkDataWeight;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TColumnarStatistics& TColumnarStatistics::operator +=(const TColumnarStatistics& other)
+{
+ if (ColumnDataWeights.empty()) {
+ ColumnDataWeights = other.ColumnDataWeights;
+ } else if (!other.ColumnDataWeights.empty()) {
+ YT_VERIFY(ColumnDataWeights.size() == other.ColumnDataWeights.size());
+ for (int index = 0; index < std::ssize(ColumnDataWeights); ++index) {
+ ColumnDataWeights[index] += other.ColumnDataWeights[index];
+ }
+ }
+ if (other.TimestampTotalWeight) {
+ TimestampTotalWeight = TimestampTotalWeight.value_or(0) + *other.TimestampTotalWeight;
+ }
+ LegacyChunkDataWeight += other.LegacyChunkDataWeight;
+ return *this;
+}
+
+TColumnarStatistics TColumnarStatistics::MakeEmpty(int columnCount)
+{
+ return TColumnarStatistics{std::vector<i64>(columnCount, 0), std::nullopt, 0};
+}
+
+TLightweightColumnarStatistics TColumnarStatistics::MakeLightweightStatistics() const
+{
+ return TLightweightColumnarStatistics{
+ .ColumnDataWeightsSum = std::accumulate(ColumnDataWeights.begin(), ColumnDataWeights.end(), (i64)0),
+ .TimestampTotalWeight = TimestampTotalWeight,
+ .LegacyChunkDataWeight = LegacyChunkDataWeight
+ };
+}
+
+TNamedColumnarStatistics TColumnarStatistics::MakeNamedStatistics(const std::vector<TString>& names) const
+{
+ TNamedColumnarStatistics result;
+ result.TimestampTotalWeight = TimestampTotalWeight;
+ result.LegacyChunkDataWeight = LegacyChunkDataWeight;
+
+ for (const auto& [name, columnDataWeight] : Zip(names, ColumnDataWeights)) {
+ result.ColumnDataWeights[name] = columnDataWeight;
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/columnar_statistics.h b/yt/yt/client/table_client/columnar_statistics.h
new file mode 100644
index 0000000000..2fa5eaf011
--- /dev/null
+++ b/yt/yt/client/table_client/columnar_statistics.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLightweightColumnarStatistics
+{
+ //! Sum of per-column data weight for chunks whose meta contains columnar statistics.
+ i64 ColumnDataWeightsSum = 0;
+ //! Total weight of all write and delete timestamps.
+ std::optional<i64> TimestampTotalWeight;
+ //! Total data weight of legacy chunks whose meta misses columnar statistics.
+ i64 LegacyChunkDataWeight = 0;
+};
+
+struct TNamedColumnarStatistics
+{
+ //! Per-column total data weight for chunks whose meta contains columnar statistics.
+ THashMap<TString, i64> ColumnDataWeights;
+ //! Total weight of all write and delete timestamps.
+ std::optional<i64> TimestampTotalWeight;
+ //! Total data weight of legacy chunks whose meta misses columnar statistics.
+ i64 LegacyChunkDataWeight = 0;
+
+ TNamedColumnarStatistics& operator +=(const TNamedColumnarStatistics& other);
+};
+
+struct TColumnarStatistics
+{
+ //! Per-column total data weight for chunks whose meta contains columnar statistics.
+ std::vector<i64> ColumnDataWeights;
+ //! Total weight of all write and delete timestamps.
+ std::optional<i64> TimestampTotalWeight;
+ //! Total data weight of legacy chunks whose meta misses columnar statistics.
+ i64 LegacyChunkDataWeight = 0;
+
+ TColumnarStatistics& operator +=(const TColumnarStatistics& other);
+
+ static TColumnarStatistics MakeEmpty(int columnCount);
+
+ TLightweightColumnarStatistics MakeLightweightStatistics() const;
+
+ TNamedColumnarStatistics MakeNamedStatistics(const std::vector<TString>& names) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/comparator-inl.h b/yt/yt/client/table_client/comparator-inl.h
new file mode 100644
index 0000000000..cfa50be6d8
--- /dev/null
+++ b/yt/yt/client/table_client/comparator-inl.h
@@ -0,0 +1,58 @@
+#ifndef COMPARATOR_INL_H_
+#error "Direct inclusion of this file is not allowed, include comparator.h"
+// For the sake of sane code completion.
+#include "comparator.h"
+#endif
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TComparer>
+int CompareKeys(TUnversionedValueRange lhs, TUnversionedValueRange rhs, const TComparer& prefixComparer)
+{
+ auto minCount = std::min(lhs.Size(), rhs.Size());
+ auto result = prefixComparer(lhs.Begin(), rhs.Begin(), minCount);
+ if (result == 0) {
+ if (lhs.Size() < rhs.Size()) {
+ result = -(minCount + 1);
+ } else if (lhs.Size() > rhs.Size()) {
+ result = minCount + 1;
+ }
+ }
+
+ return GetCompareSign(result);
+}
+
+template <typename TComparer>
+int CompareWithWidening(
+ TUnversionedValueRange keyPrefix,
+ TUnversionedValueRange boundKey,
+ const TComparer& prefixComparer)
+{
+ int result;
+ if (keyPrefix.size() < boundKey.size()) {
+ result = prefixComparer(keyPrefix.begin(), boundKey.begin(), keyPrefix.size());
+
+ if (result == 0) {
+ // Key is widened with nulls. Compare them with bound.
+ int index = keyPrefix.size();
+ while (index < std::ssize(boundKey)) {
+ if (boundKey[index].Type != EValueType::Null) {
+ // Negative value because null is less than non-null value type.
+ result = -(index + 1);
+ break;
+ }
+ ++index;
+ }
+ }
+ } else {
+ result = prefixComparer(keyPrefix.begin(), boundKey.begin(), boundKey.size());
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/comparator.cpp b/yt/yt/client/table_client/comparator.cpp
new file mode 100644
index 0000000000..1d8375387b
--- /dev/null
+++ b/yt/yt/client/table_client/comparator.cpp
@@ -0,0 +1,509 @@
+#include "comparator.h"
+
+#include "key_bound.h"
+#include "serialize.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NTableClient {
+
+using namespace NLogging;
+using namespace NYson;
+using namespace NYTree;
+
+//! Used only for YT_LOG_FATAL below.
+static const TLogger Logger("TableClientComparator");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TComparator::TComparator(std::vector<ESortOrder> sortOrders)
+ : SortOrders_(std::move(sortOrders))
+{ }
+
+void TComparator::Persist(const TPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, SortOrders_);
+}
+
+int TComparator::GetLength() const
+{
+ return SortOrders_.size();
+}
+
+void TComparator::ValidateKey(const TKey& key) const
+{
+ YT_LOG_FATAL_IF(
+ key.GetLength() != GetLength(),
+ "Comparator is used with key of different length (Key: %v, Comparator: %v)",
+ key,
+ *this);
+}
+
+void TComparator::ValidateKeyBound(const TKeyBound& keyBound) const
+{
+ YT_LOG_FATAL_IF(
+ static_cast<int>(keyBound.Prefix.GetCount()) > GetLength(),
+ "Comparator is used with longer key bound (KeyBound: %v, Comparator: %v)",
+ keyBound,
+ *this);
+}
+
+int TComparator::CompareValues(int index, const TUnversionedValue& lhs, const TUnversionedValue& rhs) const
+{
+ int valueComparisonResult = CompareRowValues(lhs, rhs);
+
+ if (SortOrders_[index] == ESortOrder::Descending) {
+ valueComparisonResult = -valueComparisonResult;
+ }
+
+ return valueComparisonResult;
+}
+
+TKeyBound TComparator::StrongerKeyBound(const TKeyBound& lhs, const TKeyBound& rhs) const
+{
+ YT_VERIFY(lhs);
+ YT_VERIFY(rhs);
+
+ YT_VERIFY(lhs.IsUpper == rhs.IsUpper);
+ auto comparisonResult = CompareKeyBounds(lhs, rhs);
+ if (lhs.IsUpper) {
+ comparisonResult = -comparisonResult;
+ }
+
+ return (comparisonResult <= 0) ? rhs : lhs;
+}
+
+void TComparator::ReplaceIfStrongerKeyBound(TKeyBound& lhs, const TKeyBound& rhs) const
+{
+ if (!lhs) {
+ lhs = rhs;
+ return;
+ }
+
+ if (!rhs) {
+ return;
+ }
+
+ YT_VERIFY(lhs.IsUpper == rhs.IsUpper);
+ auto comparisonResult = CompareKeyBounds(lhs, rhs);
+ if (lhs.IsUpper) {
+ comparisonResult = -comparisonResult;
+ }
+
+ if (comparisonResult < 0) {
+ lhs = rhs;
+ }
+}
+
+void TComparator::ReplaceIfStrongerKeyBound(TOwningKeyBound& lhs, const TOwningKeyBound& rhs) const
+{
+ if (!lhs) {
+ lhs = rhs;
+ return;
+ }
+
+ if (!rhs) {
+ return;
+ }
+
+ YT_VERIFY(lhs.IsUpper == rhs.IsUpper);
+ auto comparisonResult = CompareKeyBounds(lhs, rhs);
+ if (lhs.IsUpper) {
+ comparisonResult = -comparisonResult;
+ }
+
+ if (comparisonResult < 0) {
+ lhs = rhs;
+ }
+}
+
+TKeyBound TComparator::WeakerKeyBound(const TKeyBound& lhs, const TKeyBound& rhs) const
+{
+ YT_VERIFY(lhs.IsUpper == rhs.IsUpper);
+ auto comparisonResult = CompareKeyBounds(lhs, rhs);
+ if (lhs.IsUpper) {
+ comparisonResult = -comparisonResult;
+ }
+
+ return (comparisonResult >= 0) ? rhs : lhs;
+}
+
+bool TComparator::IsRangeEmpty(const TKeyBound& lowerBound, const TKeyBound& upperBound) const
+{
+ YT_VERIFY(!lowerBound.IsUpper);
+ YT_VERIFY(upperBound.IsUpper);
+ return CompareKeyBounds(lowerBound, upperBound, /* lowerVsUpper */ 1) >= 0;
+}
+
+bool TComparator::IsInteriorEmpty(const TKeyBound& lowerBound, const TKeyBound& upperBound) const
+{
+ YT_VERIFY(!lowerBound.IsUpper);
+ YT_VERIFY(upperBound.IsUpper);
+ return IsRangeEmpty(lowerBound, upperBound) || TryAsSingletonKey(lowerBound, upperBound);
+}
+
+bool TComparator::TestKey(const TKey& key, const TKeyBound& keyBound) const
+{
+ ValidateKey(key);
+ ValidateKeyBound(keyBound);
+
+ int comparisonResult = 0;
+
+ for (int index = 0; index < static_cast<int>(keyBound.Prefix.GetCount()); ++index) {
+ const auto& keyValue = key[index];
+ const auto& keyBoundValue = keyBound.Prefix[index];
+ comparisonResult = CompareValues(index, keyValue, keyBoundValue);
+ if (comparisonResult != 0) {
+ break;
+ }
+ }
+
+ if (keyBound.IsUpper) {
+ comparisonResult = -comparisonResult;
+ }
+
+ // Now:
+ // - comparisonResult > 0 means that key is strictly inside ray (i.e. test is positive);
+ // - comparisonResult == 0 means that key starts with key bound prefix (i.e. we should consider inclusiveness);
+ // - comparisonResult < 0 means that key is strictly outside ray (i.e. test is negative).
+
+ return comparisonResult > 0 || (comparisonResult == 0 && keyBound.IsInclusive);
+}
+
+int TComparator::CompareKeyBounds(const TKeyBound& lhs, const TKeyBound& rhs, int lowerVsUpper) const
+{
+ ValidateKeyBound(lhs);
+ ValidateKeyBound(rhs);
+
+ int comparisonResult = 0;
+
+ // In case when one key bound is a proper prefix of another, points to the shorter one.
+ const TKeyBound* shorter = nullptr;
+
+ for (int index = 0; ; ++index) {
+ if (index >= static_cast<int>(lhs.Prefix.GetCount()) &&
+ index >= static_cast<int>(rhs.Prefix.GetCount()))
+ {
+ // Prefixes coincide. Check if key bounds are indeed at the same point.
+ {
+ auto lhsInclusivenessAsUpper = (lhs.IsUpper && lhs.IsInclusive) || (!lhs.IsUpper && !lhs.IsInclusive);
+ auto rhsInclusivenessAsUpper = (rhs.IsUpper && rhs.IsInclusive) || (!rhs.IsUpper && !rhs.IsInclusive);
+ if (lhsInclusivenessAsUpper != rhsInclusivenessAsUpper) {
+ return lhsInclusivenessAsUpper - rhsInclusivenessAsUpper;
+ }
+ }
+
+ // Ok, they are indeed at the same point. How do we break ties?
+ if (lowerVsUpper == 0) {
+ // We are asked not to break ties.
+ return 0;
+ }
+
+ // Break ties using #upperFirst.
+ comparisonResult = lhs.IsUpper - rhs.IsUpper;
+
+ if (lowerVsUpper > 0) {
+ comparisonResult = -comparisonResult;
+ }
+ return comparisonResult;
+ } else if (index >= static_cast<int>(lhs.Prefix.GetCount())) {
+ shorter = &lhs;
+ break;
+ } else if (index >= static_cast<int>(rhs.Prefix.GetCount())) {
+ shorter = &rhs;
+ break;
+ } else {
+ const auto& lhsValue = lhs.Prefix[index];
+ const auto& rhsValue = rhs.Prefix[index];
+ comparisonResult = CompareValues(index, lhsValue, rhsValue);
+ if (comparisonResult != 0) {
+ return comparisonResult;
+ }
+ }
+ }
+ YT_VERIFY(shorter);
+
+ // By this moment, longer operand is strictly between shorter operand and toggleInclusiveness(shorter operand).
+ // Thus we have to check if shorter operand is "largest" among itself and its toggleInclusiveness counterpart.
+ if ((shorter->IsUpper && shorter->IsInclusive) || (!shorter->IsUpper && !shorter->IsInclusive)) {
+ comparisonResult = -1;
+ } else {
+ comparisonResult = 1;
+ }
+
+ // By now comparisonResult expresses if longer < shorter. Now check which hand is actually shorter.
+ if (shorter == &lhs) {
+ comparisonResult = -comparisonResult;
+ }
+
+ return comparisonResult;
+}
+
+int TComparator::CompareKeys(const TKey& lhs, const TKey& rhs) const
+{
+ ValidateKey(lhs);
+ ValidateKey(rhs);
+
+ for (int index = 0; index < lhs.GetLength(); ++index) {
+ auto valueComparisonResult = CompareValues(index, lhs[index], rhs[index]);
+ if (valueComparisonResult != 0) {
+ return valueComparisonResult;
+ }
+ }
+
+ return 0;
+}
+
+std::optional<TKey> TComparator::TryAsSingletonKey(const TKeyBound& lowerBound, const TKeyBound& upperBound) const
+{
+ ValidateKeyBound(lowerBound);
+ ValidateKeyBound(upperBound);
+ YT_VERIFY(!lowerBound.IsUpper);
+ YT_VERIFY(upperBound.IsUpper);
+
+ if (static_cast<int>(lowerBound.Prefix.GetCount()) != GetLength() ||
+ static_cast<int>(upperBound.Prefix.GetCount()) != GetLength())
+ {
+ return std::nullopt;
+ }
+
+ if (!lowerBound.IsInclusive || !upperBound.IsInclusive) {
+ return std::nullopt;
+ }
+
+ for (int index = 0; index < static_cast<int>(lowerBound.Prefix.GetCount()); ++index) {
+ if (CompareValues(index, lowerBound.Prefix[index], upperBound.Prefix[index]) != 0) {
+ return std::nullopt;
+ }
+ }
+
+ return TKey::FromRowUnchecked(lowerBound.Prefix);
+}
+
+TComparator TComparator::Trim(int keyColumnCount) const
+{
+ YT_VERIFY(keyColumnCount <= std::ssize(SortOrders_));
+
+ auto sortOrders = SortOrders_;
+ sortOrders.resize(keyColumnCount);
+ return TComparator(std::move(sortOrders));
+}
+
+bool TComparator::HasDescendingSortOrder() const
+{
+ return std::find(SortOrders_.begin(), SortOrders_.end(), ESortOrder::Descending) != SortOrders_.end();
+}
+
+TComparator::operator bool() const
+{
+ return !SortOrders_.empty();
+}
+
+void FormatValue(TStringBuilderBase* builder, const TComparator& comparator, TStringBuf /* spec */)
+{
+ builder->AppendFormat("{Length: %v, SortOrders: ", comparator.GetLength());
+ for (auto sortOrder : comparator.SortOrders()) {
+ switch (sortOrder) {
+ case ESortOrder::Ascending:
+ builder->AppendChar('A');
+ break;
+ case ESortOrder::Descending:
+ builder->AppendChar('D');
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+ builder->AppendChar('}');
+}
+
+TString ToString(const TComparator& comparator)
+{
+ return ToStringViaBuilder(comparator);
+}
+
+void Serialize(const TComparator& comparator, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .DoListFor(comparator.SortOrders(), [&] (TFluentList fluent, ESortOrder sortOrder) {
+ fluent.Item().Value(sortOrder);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int GetCompareSign(int value)
+{
+ return value != 0 ? value > 0 ? 1 : -1 : 0;
+}
+
+int ComparePrefix(const TUnversionedValue* lhs, const TUnversionedValue* rhs, int length)
+{
+ int index = 0;
+ while (index < length) {
+ int result = CompareRowValues(lhs[index], rhs[index]);
+ ++index;
+ if (result != 0) {
+ return result > 0 ? index : -index;
+ }
+ }
+
+ return 0;
+}
+
+int CompareKeys(TLegacyKey lhs, TLegacyKey rhs, TPrefixComparer prefixComparer)
+{
+ return CompareKeys(ToKeyRef(lhs), ToKeyRef(rhs), prefixComparer);
+}
+
+int CompareKeys(TLegacyKey lhs, TLegacyKey rhs, const TKeyComparer& prefixComparer)
+{
+ return CompareKeys(ToKeyRef(lhs), ToKeyRef(rhs), prefixComparer);
+}
+
+TKeyComparer::TKeyComparer(const TBase& base)
+ : TBase(base)
+{ }
+
+TKeyComparer::TKeyComparer()
+ : TBase(
+ New<TCaller>(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ FROM_HERE,
+#endif
+ nullptr,
+ &ComparePrefix),
+ &TCaller::StaticInvoke)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyRef ToKeyRef(TKey key)
+{
+ return key.Elements();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyBoundRef::TKeyBoundRef(
+ TKeyRef base,
+ bool inclusive,
+ bool upper)
+ : TKeyRef(base)
+ , Inclusive(inclusive)
+ , Upper(upper)
+{ }
+
+TKeyBoundRef ToKeyBoundRef(const TKeyBound& bound)
+{
+ return TKeyBoundRef(ToKeyRef(bound.Prefix), bound.IsInclusive, bound.IsUpper);
+}
+
+TKeyBoundRef ToKeyBoundRef(const TOwningKeyBound& bound)
+{
+ return TKeyBoundRef(ToKeyRef(bound.Prefix), bound.IsInclusive, bound.IsUpper);
+}
+
+std::pair<int, bool> GetBoundPrefixAndInclusiveness(TUnversionedRow row, bool isUpper, int keyLength);
+
+TKeyBoundRef ToKeyBoundRef(TUnversionedRow row, bool upper, int keyLength)
+{
+ if (!row) {
+ return TKeyBoundRef({}, /*inclusive*/ true, upper);
+ }
+
+ auto [prefixLength, inclusive] = GetBoundPrefixAndInclusiveness(row, upper, keyLength);
+ return TKeyBoundRef(
+ ToKeyRef(row, prefixLength),
+ inclusive,
+ upper);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int TestComparisonResult(int result, TRange<ESortOrder> sortOrders, bool inclusive, bool upper)
+{
+ if (result != 0) {
+ if (sortOrders[std::abs(result) - 1] == ESortOrder::Descending) {
+ result = -result;
+ }
+
+ if (upper) {
+ result = -result;
+ }
+ }
+
+ return result > 0 || inclusive && result == 0;
+}
+
+int TestComparisonResult(int result, bool inclusive, bool upper)
+{
+ if (upper) {
+ result = -result;
+ }
+
+ return result > 0 || inclusive && result == 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int CompareWithWidening(
+ TUnversionedValueRange keyPrefix,
+ TUnversionedValueRange boundKey)
+{
+ return CompareWithWidening(keyPrefix, boundKey, ComparePrefix);
+}
+
+int TestKey(TUnversionedValueRange key, const TKeyBoundRef& bound, TRange<ESortOrder> sortOrders)
+{
+ YT_VERIFY(bound.size() <= key.size());
+ int result = ComparePrefix(key.begin(), bound.begin(), bound.size());
+
+ return TestComparisonResult(result, sortOrders, bound.Inclusive, bound.Upper);
+}
+
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, TRange<ESortOrder> sortOrders)
+{
+ int result = CompareWithWidening(key, bound);
+ return TestComparisonResult(result, sortOrders, bound.Inclusive, bound.Upper);
+}
+
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound)
+{
+ int result = CompareWithWidening(key, bound);
+ return TestComparisonResult(result, bound.Inclusive, bound.Upper);
+}
+
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, TPrefixComparer prefixComparer)
+{
+ int result = CompareWithWidening(key, bound, prefixComparer);
+ return TestComparisonResult(result, bound.Inclusive, bound.Upper);
+}
+
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, const TKeyComparer& prefixComparer)
+{
+ int result = CompareWithWidening(key, bound, prefixComparer);
+ return TestComparisonResult(result, bound.Inclusive, bound.Upper);
+}
+
+int TestKeyWithWidening(TPrefixComparer prefixComparer, TUnversionedValueRange key, const TKeyBoundRef& bound)
+{
+ int result = CompareWithWidening(key, bound, prefixComparer);
+ return TestComparisonResult(result, bound.Inclusive, bound.Upper);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, ESortOrder sortOrder, TStringBuf /* spec */)
+{
+ FormatEnum(builder, sortOrder, true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/comparator.h b/yt/yt/client/table_client/comparator.h
new file mode 100644
index 0000000000..9c9e1d78d8
--- /dev/null
+++ b/yt/yt/client/table_client/comparator.h
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "public.h"
+
+#include "key.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/library/codegen/caller.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ESortOrder,
+ ((Ascending) (0))
+ ((Descending) (1))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Class that encapsulates all necessary information for key comparison
+//! and testing if key belongs to the ray defined by a key bound.
+class TComparator
+{
+public:
+ DEFINE_BYREF_RO_PROPERTY(std::vector<ESortOrder>, SortOrders);
+
+public:
+ TComparator() = default;
+ explicit TComparator(std::vector<ESortOrder> sortOrders);
+
+ void Persist(const TPersistenceContext& context);
+
+ //! Test if key #key belongs to the ray defined by #keyBound.
+ bool TestKey(const TKey& key, const TKeyBound& keyBound) const;
+
+ //! Compare key bounds according to their logical position on a line of all possible keys.
+ //! If lhs and rhs belong to the same point, compare lower limit against upper limit as
+ //! defined by #lowerVsUpperResult (i.e. if 0, lower == upper; if < 0, lower < upper; if > 0, lower > upper) .
+ int CompareKeyBounds(const TKeyBound& lhs, const TKeyBound& rhs, int lowerVsUpperResult = 0) const;
+
+ //! Compare two values belonging to the index #index of the key.
+ int CompareValues(int index, const TUnversionedValue& lhs, const TUnversionedValue& rhs) const;
+
+ //! Compare keys.
+ int CompareKeys(const TKey& lhs, const TKey& rhs) const;
+
+ //! Returns the strongest of two key bounds. Key bounds should be of same direction
+ //! (but possibly of different inclusiveness).
+ TKeyBound StrongerKeyBound(const TKeyBound& lhs, const TKeyBound& rhs) const;
+
+ //! Shorthand for #lhs = #StrongerKeyBound(#lhs, #rhs).
+ void ReplaceIfStrongerKeyBound(TKeyBound& lhs, const TKeyBound& rhs) const;
+
+ //! Same as previous for owning key bounds.
+ void ReplaceIfStrongerKeyBound(TOwningKeyBound& lhs, const TOwningKeyBound& rhs) const;
+
+ //! Returns the weakest of two key bounds. Key bounds should be of same direction
+ //! (but possibly of different inclusiveness).
+ TKeyBound WeakerKeyBound(const TKeyBound& lhs, const TKeyBound& rhs) const;
+
+ //! Check if the range defined by two key bounds is empty.
+ bool IsRangeEmpty(const TKeyBound& lowerBound, const TKeyBound& upperBound) const;
+
+ //! Check if the range defined by two key bounds has empty interior, i.e. is empty or is a singleton key.
+ bool IsInteriorEmpty(const TKeyBound& lowerBound, const TKeyBound& upperBound) const;
+
+ //! Return length of the primary key to which this comparator corresponds.
+ //! In particular, any key bound length passed as an argument must not exceed GetLength()
+ //! and any key length should be equal to GetLength().
+ int GetLength() const;
+
+ //! If there exists such key K that #lhs == ">= K" and #rhs == "<= K", return it.
+ std::optional<TKey> TryAsSingletonKey(const TKeyBound& lhs, const TKeyBound& rhs) const;
+
+ //! Returns a comparator that compares rows by first #keyColumnCount columns and ignores other.
+ TComparator Trim(int keyColumnCount) const;
+
+ //! Returns true if at least one column has descending sort order.
+ bool HasDescendingSortOrder() const;
+
+ //! Empty comparator is identified with an absence of comparator.
+ //! This may be used instead of TComparator.
+ explicit operator bool() const;
+
+private:
+ void ValidateKey(const TKey& key) const;
+ void ValidateKeyBound(const TKeyBound& keyBound) const;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TComparator& comparator, TStringBuf spec);
+TString ToString(const TComparator& comparator);
+
+void Serialize(const TComparator& comparator, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TPrefixComparer = int(const TUnversionedValue*, const TUnversionedValue*, int);
+
+////////////////////////////////////////////////////////////////////////////////
+
+int GetCompareSign(int value);
+
+//! Obeys the usual rule: the result's sign incidates the comparison outcome.
+//! Also |abs(result) - 1| is equal to index of first non-equal component.
+template <typename TComparer>
+int CompareKeys(TUnversionedValueRange lhs, TUnversionedValueRange rhs, const TComparer& prefixComparer);
+int ComparePrefix(const TUnversionedValue* lhs, const TUnversionedValue* rhs, int length);
+int CompareKeys(TLegacyKey lhs, TLegacyKey rhs, TPrefixComparer prefixComparer);
+int CompareKeys(TLegacyKey lhs, TLegacyKey rhs, const TKeyComparer& prefixComparer);
+
+class TKeyComparer
+ : public TCallback<TPrefixComparer>
+{
+public:
+ using TBase = TCallback<TPrefixComparer>;
+ using TBase::TBase;
+
+ using TCaller = NCodegen::TCGCaller<TPrefixComparer>;
+
+ TKeyComparer(const TBase& base);
+ TKeyComparer();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyRef ToKeyRef(TUnversionedRow row);
+TKeyRef ToKeyRef(TUnversionedRow row, int prefixLength);
+TKeyRef ToKeyRef(TKey key);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TKeyBoundRef
+ : public TKeyRef
+{
+public:
+ bool Inclusive;
+ bool Upper;
+
+ TKeyBoundRef(
+ TKeyRef base,
+ bool inclusive = false,
+ bool upper = false);
+};
+
+TKeyBoundRef ToKeyBoundRef(const TKeyBound& bound);
+TKeyBoundRef ToKeyBoundRef(const TOwningKeyBound& bound);
+TKeyBoundRef ToKeyBoundRef(TUnversionedRow row, bool upper, int keyLength);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TComparer>
+int CompareWithWidening(
+ TUnversionedValueRange keyPrefix,
+ TUnversionedValueRange boundKey,
+ const TComparer& prefixComparer);
+int CompareWithWidening(
+ TUnversionedValueRange keyPrefix,
+ TUnversionedValueRange boundKey);
+
+int TestKey(TUnversionedValueRange key, const TKeyBoundRef& bound, TRange<ESortOrder> sortOrders);
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound);
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, TPrefixComparer prefixComparer);
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, const TKeyComparer& prefixComparer);
+int TestKeyWithWidening(TUnversionedValueRange key, const TKeyBoundRef& bound, TRange<ESortOrder> sortOrders);
+
+int TestComparisonResult(int result, TRange<ESortOrder> sortOrders, bool inclusive, bool upper);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, ESortOrder sortOrder, TStringBuf /* spec */);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define COMPARATOR_INL_H_
+#include "comparator-inl.h"
+#undef COMPARATOR_INL_H_
diff --git a/yt/yt/client/table_client/composite_compare.cpp b/yt/yt/client/table_client/composite_compare.cpp
new file mode 100644
index 0000000000..7d28c21b3a
--- /dev/null
+++ b/yt/yt/client/table_client/composite_compare.cpp
@@ -0,0 +1,279 @@
+#include "composite_compare.h"
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/library/numeric/util.h>
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+// This file implements comparison for composite values.
+// Composite types that supports comparison are:
+// 1. Optional
+// 2. List
+// 3. Tuple
+// 4. Variant
+//
+// When we compare composite values we assume that they are well-formed yson representations of same type supporting comparison.
+// And we compare them in following manner:
+// 1. We scan two values simultaneously and look at their yson tokens and find first mismatching token.
+// 2. If one of the token is EndList (this only can happen if we parsing values of list type
+// and one list is shorter than another) that means that value containing EndList is less that other.
+// 3. Otherwise if one of the values is Entity (other value have to be non null value) that means
+// that value containing Entity is less than other.
+// 4. Otherwise it's 2 values of the same type and we can easily compare them.
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(ECompareClass, ui32,
+ ((Incomparable)(0))
+ ((EndList)(1))
+ ((Entity)(2))
+ ((BeginValue)(3))
+);
+
+// Helper function for GetCompareClass
+static constexpr ui32 SetMask(EYsonItemType type, ECompareClass compareClass)
+{
+ return static_cast<ui32>(compareClass) << (static_cast<ui32>(type) * 2);
+}
+
+static constexpr ECompareClass GetCompareClass(EYsonItemType type)
+{
+ // We have single integer where each pair of bits encodes map EYsonItemType -> ECompareClass.
+ constexpr ui32 compareClassMask =
+ SetMask(EYsonItemType::EndList, ECompareClass::EndList) |
+ SetMask(EYsonItemType::EntityValue, ECompareClass::Entity) |
+ SetMask(EYsonItemType::BeginList, ECompareClass::BeginValue) |
+ SetMask(EYsonItemType::Int64Value, ECompareClass::BeginValue) |
+ SetMask(EYsonItemType::Uint64Value, ECompareClass::BeginValue) |
+ SetMask(EYsonItemType::DoubleValue, ECompareClass::BeginValue) |
+ SetMask(EYsonItemType::BooleanValue, ECompareClass::BeginValue) |
+ SetMask(EYsonItemType::StringValue, ECompareClass::BeginValue);
+ static_assert(TEnumTraits<EYsonItemType>::GetDomainSize() * 2 <= 32);
+ return static_cast<ECompareClass>(0x3u & (compareClassMask >> (static_cast<ui32>(type) * 2)));
+}
+
+template <typename T>
+Y_FORCE_INLINE int ComparePrimitive(T lhs, T rhs)
+{
+ if (lhs == rhs) {
+ return 0;
+ } else if (lhs < rhs) {
+ return -1;
+ } else {
+ return 1;
+ }
+}
+
+template <>
+Y_FORCE_INLINE int ComparePrimitive<double>(double lhs, double rhs)
+{
+ if (lhs < rhs) {
+ return -1;
+ } else if (lhs > rhs) {
+ return 1;
+ } else if (std::isnan(lhs)) {
+ if (std::isnan(rhs)) {
+ return 0;
+ }
+ return 1;
+ } else if (std::isnan(rhs)) {
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+[[noreturn]] static void ThrowIncomparableYsonToken(EYsonItemType tokenType)
+{
+ THROW_ERROR_EXCEPTION("Incomparable YSON token %Qlv",
+ tokenType);
+}
+
+Y_FORCE_INLINE static int GetSign(int x)
+{
+ return static_cast<int>(0 < x) - static_cast<int>(0 > x);
+}
+
+Y_FORCE_INLINE static int CompareYsonItems(const TYsonItem& lhs, const TYsonItem& rhs)
+{
+ if (lhs.GetType() == rhs.GetType()) {
+ switch (lhs.GetType()) {
+ case EYsonItemType::EndOfStream:
+ case EYsonItemType::BeginList:
+ case EYsonItemType::EndList:
+ case EYsonItemType::EntityValue:
+ return 0;
+ case EYsonItemType::Int64Value:
+ return ComparePrimitive(lhs.UncheckedAsInt64(), rhs.UncheckedAsInt64());
+ case EYsonItemType::Uint64Value:
+ return ComparePrimitive(lhs.UncheckedAsUint64(), rhs.UncheckedAsUint64());
+ case EYsonItemType::DoubleValue:
+ return ComparePrimitive(lhs.UncheckedAsDouble(), rhs.UncheckedAsDouble());
+ case EYsonItemType::BooleanValue:
+ return ComparePrimitive(lhs.UncheckedAsBoolean(), rhs.UncheckedAsBoolean());
+ case EYsonItemType::StringValue:
+ return GetSign(TString::compare(lhs.UncheckedAsString(), rhs.UncheckedAsString()));
+
+ case EYsonItemType::BeginMap:
+ case EYsonItemType::EndMap:
+ case EYsonItemType::BeginAttributes:
+ case EYsonItemType::EndAttributes:
+ ThrowIncomparableYsonToken(lhs.GetType());
+ }
+ YT_ABORT();
+ }
+
+ const auto lhsClass = GetCompareClass(lhs.GetType());
+ const auto rhsClass = GetCompareClass(rhs.GetType());
+
+ if (lhsClass == ECompareClass::Incomparable) {
+ ThrowIncomparableYsonToken(lhs.GetType());
+ }
+ if (rhsClass == ECompareClass::Incomparable) {
+ ThrowIncomparableYsonToken(rhs.GetType());
+ }
+
+ if (lhsClass == ECompareClass::BeginValue && rhsClass == ECompareClass::BeginValue) {
+ THROW_ERROR_EXCEPTION("Incomparable scalar types %Qlv and %Qlv in YSON representation",
+ lhs.GetType(),
+ rhs.GetType());
+ }
+ return ComparePrimitive(static_cast<ui32>(lhsClass), static_cast<ui32>(rhsClass));
+}
+
+} // namespace
+
+int CompareCompositeValues(TYsonStringBuf lhs, TYsonStringBuf rhs)
+{
+ YT_ASSERT(lhs.GetType() == EYsonType::Node);
+ YT_ASSERT(rhs.GetType() == EYsonType::Node);
+
+ TMemoryInput lhsIn(lhs.AsStringBuf());
+ TMemoryInput rhsIn(rhs.AsStringBuf());
+
+ TYsonPullParser lhsParser(&lhsIn, EYsonType::Node);
+ TYsonPullParser rhsParser(&rhsIn, EYsonType::Node);
+
+ for (;;) {
+ const auto lhsItem = lhsParser.Next();
+ const auto rhsItem = rhsParser.Next();
+
+ auto res = CompareYsonItems(lhsItem, rhsItem);
+ if (res != 0) {
+ return res;
+ } else if (lhsItem.GetType() == EYsonItemType::EndOfStream) {
+ return 0;
+ }
+ Y_ASSERT(lhsItem.GetType() != EYsonItemType::EndOfStream &&
+ rhsItem.GetType() != EYsonItemType::EndOfStream);
+ }
+}
+
+TFingerprint CompositeFarmHash(TYsonStringBuf value)
+{
+ YT_ASSERT(value.GetType() == EYsonType::Node);
+
+ TMemoryInput in(value.AsStringBuf());
+ TYsonPullParser parser(&in, EYsonType::Node);
+
+ auto throwUnexpectedYsonToken = [] (const TYsonItem& item) {
+ THROW_ERROR_EXCEPTION("Unexpected YSON token %Qlv in composite value", item.GetType());
+ };
+
+ TFingerprint result;
+
+ auto item = parser.Next();
+ switch (item.GetType()) {
+ case EYsonItemType::BeginAttributes:
+ case EYsonItemType::EndAttributes:
+ case EYsonItemType::BeginMap:
+ case EYsonItemType::EndMap:
+ throwUnexpectedYsonToken(item);
+ [[fallthrough]]; // AUTOGENERATED_FALLTHROUGH_FIXME
+ case EYsonItemType::BeginList:
+ result = FarmFingerprint('[');
+ break;
+ case EYsonItemType::EndList:
+ result = FarmFingerprint(']');
+ break;
+ case EYsonItemType::EntityValue:
+ result = FarmFingerprint(0);
+ break;
+ case EYsonItemType::Int64Value:
+ result = FarmFingerprint(item.UncheckedAsInt64());
+ break;
+ case EYsonItemType::Uint64Value:
+ result = FarmFingerprint(item.UncheckedAsUint64());
+ break;
+ case EYsonItemType::DoubleValue: {
+ // NB. We cannot compute hash of double
+ // So we replicate logic of FarmFingerprint(const TUnversionedValue& ) here
+ // and cast double to ui64
+ result = FarmFingerprint(BitCast<ui64>(item.UncheckedAsDouble()));
+ break;
+ }
+ case EYsonItemType::BooleanValue:
+ result = FarmFingerprint(item.UncheckedAsBoolean());
+ break;
+ case EYsonItemType::StringValue: {
+ auto string = item.UncheckedAsString();
+ result = FarmFingerprint(FarmFingerprint(string.Data(), string.size()));
+ break;
+ }
+ case EYsonItemType::EndOfStream:
+ // Invalid yson, parser should have thrown.
+ Y_FAIL();
+ }
+
+ for (;;) {
+ item = parser.Next();
+ switch (item.GetType()) {
+ case EYsonItemType::BeginAttributes:
+ case EYsonItemType::EndAttributes:
+ case EYsonItemType::BeginMap:
+ case EYsonItemType::EndMap:
+ throwUnexpectedYsonToken(item);
+ continue;
+ case EYsonItemType::BeginList:
+ result = FarmFingerprint(result, '[');
+ continue;
+ case EYsonItemType::EndList:
+ result = FarmFingerprint(result, ']');
+ continue;
+ case EYsonItemType::EntityValue:
+ result = FarmFingerprint(result, 0);
+ continue;
+ case EYsonItemType::Int64Value:
+ result = FarmFingerprint(item.UncheckedAsInt64(), result);
+ continue;
+ case EYsonItemType::Uint64Value:
+ result = FarmFingerprint(result, item.UncheckedAsUint64());
+ continue;
+ case EYsonItemType::DoubleValue:
+ // NB. see comment above.
+ result = FarmFingerprint(BitCast<ui64>(item.UncheckedAsDouble()));
+ continue;
+ case EYsonItemType::BooleanValue:
+ result = FarmFingerprint(result, item.UncheckedAsBoolean());
+ continue;
+ case EYsonItemType::StringValue: {
+ auto string = item.UncheckedAsString();
+ result = FarmFingerprint(result, FarmFingerprint(string.Data(), string.size()));
+ continue;
+ }
+ case EYsonItemType::EndOfStream:
+ return result;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/composite_compare.h b/yt/yt/client/table_client/composite_compare.h
new file mode 100644
index 0000000000..75556472ad
--- /dev/null
+++ b/yt/yt/client/table_client/composite_compare.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+int CompareCompositeValues(NYson::TYsonStringBuf lhs, NYson::TYsonStringBuf rhs);
+TFingerprint CompositeFarmHash(NYson::TYsonStringBuf compositeValue);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/config.cpp b/yt/yt/client/table_client/config.cpp
new file mode 100644
index 0000000000..b837272905
--- /dev/null
+++ b/yt/yt/client/table_client/config.cpp
@@ -0,0 +1,402 @@
+#include "config.h"
+
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/client/tablet_client/config.h>
+#include <yt/yt/client/tablet_client/helpers.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/library/quantile_digest/config.h>
+
+namespace NYT::NTableClient {
+
+using namespace NChunkClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TRetentionConfigPtr& obj)
+{
+ static const TString NullPtrName("<nullptr>");
+ return obj
+ ? NYson::ConvertToYsonString(obj, NYson::EYsonFormat::Text).ToString()
+ : NullPtrName;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRetentionConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("min_data_versions", &TThis::MinDataVersions)
+ .GreaterThanOrEqual(0)
+ .Default(1);
+ registrar.Parameter("max_data_versions", &TThis::MaxDataVersions)
+ .GreaterThanOrEqual(0)
+ .Default(1);
+ registrar.Parameter("min_data_ttl", &TThis::MinDataTtl)
+ .Default(TDuration::Minutes(30));
+ registrar.Parameter("max_data_ttl", &TThis::MaxDataTtl)
+ .Default(TDuration::Minutes(30));
+ registrar.Parameter("ignore_major_timestamp", &TThis::IgnoreMajorTimestamp)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChunkReaderConfigPtr TChunkReaderConfig::GetDefault()
+{
+ return LeakyRefCountedSingleton<TChunkReaderConfig>();
+}
+
+void TChunkReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("sampling_mode", &TThis::SamplingMode)
+ .Default();
+
+ registrar.Parameter("sampling_rate", &TThis::SamplingRate)
+ .Default()
+ .InRange(0, 1);
+
+ registrar.Parameter("sampling_seed", &TThis::SamplingSeed)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->SamplingRate && !config->SamplingMode) {
+ config->SamplingMode = ESamplingMode::Row;
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THashTableChunkIndexWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("load_factor", &TThis::LoadFactor)
+ .Default(0.5)
+ .GreaterThan(0.)
+ .LessThanOrEqual(1.);
+ registrar.Parameter("rehash_trial_count", &TThis::RehashTrialCount)
+ .Default(3)
+ .GreaterThan(0);
+ registrar.Parameter("enable_group_reordering", &TThis::EnableGroupReordering)
+ .Default(false);
+ registrar.Parameter("max_block_size", &TThis::MaxBlockSize)
+ .Default(128_KB);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkIndexesWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("hash_table", &TThis::HashTable)
+ .DefaultNew();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSlimVersionedWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("top_value_quantile", &TThis::TopValueQuantile)
+ .Default(0.1)
+ .InRange(0.0, 1.0);
+ registrar.Parameter("enable_per_value_dictionary_encoding", &TThis::EnablePerValueDictionaryEncoding)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkWriterTestingOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("add_unsupported_feature", &TThis::AddUnsupportedFeature)
+ .Default(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkWriterConfig::Register(TRegistrar registrar)
+{
+ // Allow very small blocks for testing purposes.
+ registrar.Parameter("block_size", &TThis::BlockSize)
+ .GreaterThan(0)
+ .Default(16_MB);
+
+ registrar.Parameter("max_segment_value_count", &TThis::MaxSegmentValueCount)
+ .GreaterThan(0)
+ .Default(128 * 1024);
+
+ registrar.Parameter("max_buffer_size", &TThis::MaxBufferSize)
+ .GreaterThan(0)
+ .Default(16_MB);
+
+ registrar.Parameter("max_row_weight", &TThis::MaxRowWeight)
+ .GreaterThanOrEqual(5_MB)
+ .LessThanOrEqual(MaxRowWeightLimit)
+ .Default(16_MB);
+
+ registrar.Parameter("max_key_weight", &TThis::MaxKeyWeight)
+ .GreaterThan(0)
+ .LessThanOrEqual(MaxKeyWeightLimit)
+ .Default(16_KB);
+
+ registrar.Parameter("max_data_weight_between_blocks", &TThis::MaxDataWeightBetweenBlocks)
+ .GreaterThan(0)
+ .Default(2_GB);
+
+ registrar.Parameter("sample_rate", &TThis::SampleRate)
+ .InRange(0.0, 0.001)
+ .Default(0.0001);
+
+ registrar.Parameter("chunk_indexes", &TThis::ChunkIndexes)
+ .DefaultNew();
+
+ registrar.Parameter("slim", &TThis::Slim)
+ .DefaultNew();
+
+ registrar.Parameter("versioned_row_digest", &TThis::VersionedRowDigest)
+ .DefaultNew();
+
+ registrar.Parameter("testing_options", &TThis::TestingOptions)
+ .DefaultNew();
+
+ registrar.Parameter("key_filter", &TThis::KeyFilter)
+ .DefaultNew();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TKeyFilterWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable", &TThis::Enable)
+ .Default(false);
+
+ registrar.Parameter("block_size", &TThis::BlockSize)
+ .GreaterThan(0)
+ .Default(64_KB);
+
+ registrar.Parameter("trial_count", &TThis::TrialCount)
+ .GreaterThan(0)
+ .Default(100);
+
+ registrar.Parameter("bits_per_key", &TThis::BitsPerKey)
+ .InRange(0, 62)
+ .Optional();
+
+ registrar.Parameter("false_positive_rate", &TThis::FalsePositiveRate)
+ .InRange(0, 1.0 / (1ll << 62))
+ .Default()
+ .Optional();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->BitsPerKey && config->FalsePositiveRate) {
+ THROW_ERROR_EXCEPTION("At most one of \"bits_per_key\" and "
+ "\"false_positive_rate\" can be specified");
+ }
+
+ if (config->FalsePositiveRate) {
+ int bitsPerKey = 1;
+
+ while ((1ll << bitsPerKey) * *config->FalsePositiveRate < 1) {
+ ++bitsPerKey;
+ }
+
+ config->EffectiveBitsPerKey = bitsPerKey;
+ } else {
+ config->EffectiveBitsPerKey = config->BitsPerKey.value_or(DefaultBitsPerKey);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTableReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("suppress_access_tracking", &TThis::SuppressAccessTracking)
+ .Default(false);
+ registrar.Parameter("suppress_expiration_timeout_renewal", &TThis::SuppressExpirationTimeoutRenewal)
+ .Default(false);
+ registrar.Parameter("unavailable_chunk_strategy", &TThis::UnavailableChunkStrategy)
+ .Default(EUnavailableChunkStrategy::Restore);
+ registrar.Parameter("chunk_availability_policy", &TThis::ChunkAvailabilityPolicy)
+ .Default(EChunkAvailabilityPolicy::Repairable);
+ registrar.Parameter("max_read_duration", &TThis::MaxReadDuration)
+ .Default();
+ registrar.Parameter("dynamic_store_reader", &TThis::DynamicStoreReader)
+ .DefaultNew();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTypeConversionConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_type_conversion", &TThis::EnableTypeConversion)
+ .Default(false);
+ registrar.Parameter("enable_string_to_all_conversion", &TThis::EnableStringToAllConversion)
+ .Default(false);
+ registrar.Parameter("enable_all_to_string_conversion", &TThis::EnableAllToStringConversion)
+ .Default(false);
+ registrar.Parameter("enable_integral_type_conversion", &TThis::EnableIntegralTypeConversion)
+ .Default(true);
+ registrar.Parameter("enable_integral_to_double_conversion", &TThis::EnableIntegralToDoubleConversion)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->EnableTypeConversion) {
+ config->EnableStringToAllConversion = true;
+ config->EnableAllToStringConversion = true;
+ config->EnableIntegralTypeConversion = true;
+ config->EnableIntegralToDoubleConversion = true;
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TInsertRowsFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_null_to_yson_entity_conversion", &TThis::EnableNullToYsonEntityConversion)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChunkReaderOptionsPtr TChunkReaderOptions::GetDefault()
+{
+ return LeakyRefCountedSingleton<TChunkReaderOptions>();
+}
+
+void TChunkReaderOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_table_index", &TThis::EnableTableIndex)
+ .Default(false);
+
+ registrar.Parameter("enable_range_index", &TThis::EnableRangeIndex)
+ .Default(false);
+
+ registrar.Parameter("enable_row_index", &TThis::EnableRowIndex)
+ .Default(false);
+
+ registrar.Parameter("enable_tablet_index", &TThis::EnableTabletIndex)
+ .Default(false);
+
+ registrar.Parameter("dynamic_table", &TThis::DynamicTable)
+ .Default(false);
+
+ registrar.Parameter("enable_key_widening", &TThis::EnableKeyWidening)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->EnableRangeIndex && !config->EnableRowIndex) {
+ THROW_ERROR_EXCEPTION("\"enable_row_index\" must be set when \"enable_range_index\" is set");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChunkWriterOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("validate_sorted", &TThis::ValidateSorted)
+ .Default(true);
+ registrar.Parameter("validate_row_weight", &TThis::ValidateRowWeight)
+ .Default(false);
+ registrar.Parameter("validate_key_weight", &TThis::ValidateKeyWeight)
+ .Default(false);
+ registrar.Parameter("validate_duplicate_ids", &TThis::ValidateDuplicateIds)
+ .Default(false);
+ registrar.Parameter("validate_column_count", &TThis::ValidateColumnCount)
+ .Default(false);
+ registrar.Parameter("validate_any_is_valid_yson", &TThis::ValidateAnyIsValidYson)
+ .Default(false);
+ registrar.Parameter("validate_unique_keys", &TThis::ValidateUniqueKeys)
+ .Default(false);
+ registrar.Parameter("explode_on_validation_error", &TThis::ExplodeOnValidationError)
+ .Default(false);
+ registrar.Parameter("optimize_for", &TThis::OptimizeFor)
+ .Default(EOptimizeFor::Lookup);
+ registrar.Parameter("chunk_format", &TThis::ChunkFormat)
+ .Default();
+ registrar.Parameter("evaluate_computed_columns", &TThis::EvaluateComputedColumns)
+ .Default(true);
+ registrar.Parameter("enable_skynet_sharing", &TThis::EnableSkynetSharing)
+ .Default(false);
+ registrar.Parameter("return_boundary_keys", &TThis::ReturnBoundaryKeys)
+ .Default(true);
+ registrar.Parameter("cast_any_to_composite", &TThis::CastAnyToCompositeNode)
+ .Default();
+ registrar.Parameter("single_column_group_by_default", &TThis::SingleColumnGroupByDefault)
+ .Default();
+
+ registrar.Parameter("schema_modification", &TThis::SchemaModification)
+ .Default(ETableSchemaModification::None);
+ registrar.Parameter("max_heavy_columns", &TThis::MaxHeavyColumns)
+ .Default(0);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->ValidateUniqueKeys && !config->ValidateSorted) {
+ THROW_ERROR_EXCEPTION("\"validate_unique_keys\" is allowed to be true only if \"validate_sorted\" is true");
+ }
+
+ if (config->CastAnyToCompositeNode) {
+ try {
+ config->CastAnyToComposite = NYTree::ConvertTo<bool>(config->CastAnyToCompositeNode);
+ } catch (const std::exception&) {
+ // COMPAT: Do nothing for backward compatibility.
+ }
+ }
+
+ switch (config->SchemaModification) {
+ case ETableSchemaModification::None:
+ break;
+
+ case ETableSchemaModification::UnversionedUpdate:
+ if (!config->ValidateSorted || !config->ValidateUniqueKeys) {
+ THROW_ERROR_EXCEPTION(
+ "\"schema_modification\" is allowed to be %Qlv only if "
+ "\"validate_sorted\" and \"validate_unique_keys\" are true",
+ config->SchemaModification);
+ }
+ break;
+
+ case ETableSchemaModification::UnversionedUpdateUnsorted:
+ THROW_ERROR_EXCEPTION("\"schema_modification\" is not allowed to be %Qlv",
+ config->SchemaModification);
+
+ default:
+ YT_ABORT();
+ }
+
+ if (config->ChunkFormat) {
+ ValidateTableChunkFormatAndOptimizeFor(*config->ChunkFormat, config->OptimizeFor);
+ }
+ });
+}
+
+EChunkFormat TChunkWriterOptions::GetEffectiveChunkFormat(bool versioned) const
+{
+ return ChunkFormat.value_or(DefaultFormatFromOptimizeFor(OptimizeFor, versioned));
+}
+
+void TChunkWriterOptions::EnableValidationOptions(bool validateAnyIsValidYson)
+{
+ ValidateDuplicateIds = true;
+ ValidateRowWeight = true;
+ ValidateKeyWeight = true;
+ ValidateColumnCount = true;
+ ValidateAnyIsValidYson = validateAnyIsValidYson;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TVersionedRowDigestConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable", &TThis::Enable)
+ .Default(false);
+ registrar.Parameter("t_digest", &TThis::TDigest)
+ .DefaultNew();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/config.h b/yt/yt/client/table_client/config.h
new file mode 100644
index 0000000000..7b4c361014
--- /dev/null
+++ b/yt/yt/client/table_client/config.h
@@ -0,0 +1,363 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/chunk_client/config.h>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/library/quantile_digest/public.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetentionConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ int MinDataVersions;
+ int MaxDataVersions;
+ TDuration MinDataTtl;
+ TDuration MaxDataTtl;
+ bool IgnoreMajorTimestamp;
+
+ REGISTER_YSON_STRUCT(TRetentionConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRetentionConfig)
+
+TString ToString(const TRetentionConfigPtr& obj);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ESamplingMode,
+ ((Row) (1))
+ ((Block) (2))
+);
+
+class TChunkReaderConfig
+ : public virtual NChunkClient::TBlockFetcherConfig
+{
+public:
+ std::optional<ESamplingMode> SamplingMode;
+ std::optional<double> SamplingRate;
+ std::optional<ui64> SamplingSeed;
+
+ static TChunkReaderConfigPtr GetDefault();
+
+ REGISTER_YSON_STRUCT(TChunkReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkWriterTestingOptions
+ : public NYTree::TYsonStruct
+{
+public:
+ //! If true, unsupported chunk feature is added to chunk meta.
+ bool AddUnsupportedFeature;
+
+ REGISTER_YSON_STRUCT(TChunkWriterTestingOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkWriterTestingOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THashTableChunkIndexWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! Hash table load factor.
+ double LoadFactor;
+
+ //! Final hash table seed will be picked considering this number of rehash trials.
+ int RehashTrialCount;
+
+ // TODO(akozhikhov).
+ bool EnableGroupReordering;
+
+ //! Unless null, key set will be split to produce multiple hash tables,
+ //! each of which corresponds to a single system block and is not greater than #MaxBlockSize.
+ std::optional<int> MaxBlockSize;
+
+ REGISTER_YSON_STRUCT(THashTableChunkIndexWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THashTableChunkIndexWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkIndexesWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ THashTableChunkIndexWriterConfigPtr HashTable;
+
+ REGISTER_YSON_STRUCT(TChunkIndexesWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkIndexesWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSlimVersionedWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ double TopValueQuantile;
+ bool EnablePerValueDictionaryEncoding;
+
+ REGISTER_YSON_STRUCT(TSlimVersionedWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSlimVersionedWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkWriterConfig
+ : public NChunkClient::TEncodingWriterConfig
+{
+public:
+ i64 BlockSize;
+
+ i64 MaxSegmentValueCount;
+
+ i64 MaxBufferSize;
+
+ i64 MaxRowWeight;
+
+ i64 MaxKeyWeight;
+
+ //! This limits ensures that chunk index is dense enough
+ //! e.g. to produce good slices for reduce.
+ i64 MaxDataWeightBetweenBlocks;
+
+ double SampleRate;
+
+ TChunkIndexesWriterConfigPtr ChunkIndexes;
+
+ TSlimVersionedWriterConfigPtr Slim;
+
+ TVersionedRowDigestConfigPtr VersionedRowDigest;
+
+ TChunkWriterTestingOptionsPtr TestingOptions;
+
+ TKeyFilterWriterConfigPtr KeyFilter;
+
+ REGISTER_YSON_STRUCT(TChunkWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TKeyFilterWriterConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ bool Enable;
+
+ i64 BlockSize;
+
+ int TrialCount;
+
+ std::optional<int> BitsPerKey;
+ std::optional<double> FalsePositiveRate;
+
+ static constexpr int DefaultBitsPerKey = 8;
+ int EffectiveBitsPerKey;
+
+ REGISTER_YSON_STRUCT(TKeyFilterWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TKeyFilterWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableReaderConfig
+ : public virtual NChunkClient::TMultiChunkReaderConfig
+ , public virtual TChunkReaderConfig
+{
+public:
+ bool SuppressAccessTracking;
+ bool SuppressExpirationTimeoutRenewal;
+ EUnavailableChunkStrategy UnavailableChunkStrategy;
+ NChunkClient::EChunkAvailabilityPolicy ChunkAvailabilityPolicy;
+ std::optional<TDuration> MaxReadDuration;
+
+ NTabletClient::TRetryingRemoteDynamicStoreReaderConfigPtr DynamicStoreReader;
+
+ REGISTER_YSON_STRUCT(TTableReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableWriterConfig
+ : public TChunkWriterConfig
+ , public NChunkClient::TMultiChunkWriterConfig
+{
+ REGISTER_YSON_STRUCT(TTableWriterConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTypeConversionConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool EnableTypeConversion;
+ bool EnableStringToAllConversion;
+ bool EnableAllToStringConversion;
+ bool EnableIntegralTypeConversion;
+ bool EnableIntegralToDoubleConversion;
+
+ REGISTER_YSON_STRUCT(TTypeConversionConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTypeConversionConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInsertRowsFormatConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ bool EnableNullToYsonEntityConversion;
+
+ REGISTER_YSON_STRUCT(TInsertRowsFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TInsertRowsFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkReaderOptions
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ bool EnableTableIndex;
+ bool EnableRangeIndex;
+ bool EnableRowIndex;
+ bool DynamicTable;
+ bool EnableTabletIndex;
+ bool EnableKeyWidening;
+
+ static TChunkReaderOptionsPtr GetDefault();
+
+ REGISTER_YSON_STRUCT(TChunkReaderOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkReaderOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChunkWriterOptions
+ : public virtual NChunkClient::TEncodingWriterOptions
+{
+public:
+ bool ValidateSorted;
+ bool ValidateRowWeight;
+ bool ValidateKeyWeight;
+ bool ValidateDuplicateIds;
+ bool ValidateUniqueKeys;
+ bool ExplodeOnValidationError;
+ bool ValidateColumnCount;
+ bool ValidateAnyIsValidYson;
+ bool EvaluateComputedColumns;
+ bool EnableSkynetSharing;
+ bool ReturnBoundaryKeys;
+ bool CastAnyToComposite = false;
+ bool SingleColumnGroupByDefault = false;
+ NYTree::INodePtr CastAnyToCompositeNode;
+
+ ETableSchemaModification SchemaModification;
+
+ EOptimizeFor OptimizeFor;
+ std::optional<NChunkClient::EChunkFormat> ChunkFormat;
+ NChunkClient::EChunkFormat GetEffectiveChunkFormat(bool versioned) const;
+
+ //! Maximum number of heavy columns in approximate statistics.
+ int MaxHeavyColumns;
+
+ void EnableValidationOptions(bool validateAnyIsValidYson = false);
+
+ REGISTER_YSON_STRUCT(TChunkWriterOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChunkWriterOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVersionedRowDigestConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool Enable;
+ TTDigestConfigPtr TDigest;
+
+ REGISTER_YSON_STRUCT(TVersionedRowDigestConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TVersionedRowDigestConfig)
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct TRowBatchReadOptions
+{
+ //! The desired number of rows to read.
+ //! This is just an estimate; not all readers support this limit.
+ i64 MaxRowsPerRead = 10000;
+
+ //! The desired data weight to read.
+ //! This is just an estimate; not all readers support this limit.
+ i64 MaxDataWeightPerRead = 16_MB;
+
+ //! If true then the reader may return a columnar batch.
+ //! If false then the reader must return a non-columnar batch.
+ bool Columnar = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/helpers-inl.h b/yt/yt/client/table_client/helpers-inl.h
new file mode 100644
index 0000000000..cfb4bea1d9
--- /dev/null
+++ b/yt/yt/client/table_client/helpers-inl.h
@@ -0,0 +1,533 @@
+#ifndef HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+
+#include "row_buffer.h"
+#include "row_batch.h"
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <array>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+// Scalar inline types
+
+#define XX(T) \
+ template <> \
+ struct TUnversionedValueConversionTraits<T, void> \
+ { \
+ static constexpr bool Scalar = true; \
+ static constexpr bool Inline = true; \
+ };
+
+XX(std::nullopt_t)
+XX(i64)
+XX(ui64)
+XX(i32)
+XX(ui32)
+XX(i16)
+XX(ui16)
+XX(i8)
+XX(ui8)
+XX(bool)
+XX(double)
+XX(TInstant)
+XX(TDuration)
+
+#undef XX
+
+////////////////////////////////////////////////////////////////////////////////
+// Scalar non-inline types
+
+#define XX(T) \
+ template <> \
+ struct TUnversionedValueConversionTraits<T, void> \
+ { \
+ static constexpr bool Scalar = true; \
+ static constexpr bool Inline = false; \
+ };
+
+XX(TString)
+XX(TStringBuf)
+XX(TGuid)
+
+#undef XX
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+struct TUnversionedValueConversionTraits<T>
+{
+ static constexpr bool Scalar = true;
+ static constexpr bool Inline = !TEnumTraits<T>::IsStringSerializableEnum;
+};
+
+template <class T>
+struct TUnversionedValueConversionTraits<std::optional<T>, void>
+{
+ static constexpr bool Scalar = TUnversionedValueConversionTraits<T>::Scalar;
+ static constexpr bool Inline = TUnversionedValueConversionTraits<T>::Inline;
+};
+
+template <class T>
+struct TUnversionedValueConversionTraits<TAnnotatedValue<T>, void>
+{
+ static constexpr bool Scalar = TUnversionedValueConversionTraits<T>::Scalar;
+ static constexpr bool Inline = TUnversionedValueConversionTraits<T>::Inline;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const TAnnotatedValue<T>& value,
+ const TRowBufferPtr& rowBuffer,
+ int /*id*/,
+ EValueFlags /*flags*/)
+{
+ ToUnversionedValue(unversionedValue, value.Value, rowBuffer, value.Id, value.Flags);
+}
+
+template <class T>
+struct TValueWithIdTrait
+{
+ static constexpr bool WithId = false;
+};
+
+template <class T>
+struct TValueWithIdTrait<TAnnotatedValue<T>>
+{
+ static constexpr bool WithId = true;
+};
+
+template <class... Ts>
+struct TRowValueTypesChecker
+{
+ static constexpr bool AllWithIds = (... && TValueWithIdTrait<Ts>::WithId);
+ static constexpr bool AllWithoutIds = (... && !TValueWithIdTrait<Ts>::WithId);
+ static_assert(AllWithIds || AllWithoutIds, "All values must be wrapped into TValueWithId or none must be.");
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ T value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ if constexpr (TEnumTraits<T>::IsStringSerializableEnum) {
+ ToUnversionedValue(unversionedValue, NYT::FormatEnum(value), rowBuffer, id, flags);
+ } else if constexpr (TEnumTraits<T>::IsBitEnum) {
+ ToUnversionedValue(unversionedValue, static_cast<ui64>(value), rowBuffer, id, flags);
+ } else {
+ ToUnversionedValue(unversionedValue, static_cast<i64>(value), rowBuffer, id, flags);
+ }
+}
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void FromUnversionedValue(
+ T* value,
+ TUnversionedValue unversionedValue)
+{
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ *value = static_cast<T>(unversionedValue.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ *value = static_cast<T>(unversionedValue.Data.Uint64);
+ break;
+ case EValueType::String:
+ *value = NYT::ParseEnum<T>(unversionedValue.AsStringBuf());
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse enum value from %Qlv",
+ unversionedValue.Type);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ProtobufToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const google::protobuf::Message& value,
+ const NYson::TProtobufMessageType* type,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags);
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const T& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type*)
+{
+ ProtobufToUnversionedValueImpl(
+ unversionedValue,
+ value,
+ NYson::ReflectProtobufMessageType<T>(),
+ rowBuffer,
+ id,
+ flags);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void UnversionedValueToProtobufImpl(
+ google::protobuf::Message* value,
+ const NYson::TProtobufMessageType* type,
+ TUnversionedValue unversionedValue);
+
+template <class T>
+void FromUnversionedValue(
+ T* value,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type*)
+{
+ UnversionedValueToProtobufImpl(
+ value,
+ NYson::ReflectProtobufMessageType<T>(),
+ unversionedValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const std::optional<T>& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ if (value) {
+ ToUnversionedValue(unversionedValue, *value, rowBuffer, id, flags);
+ } else {
+ *unversionedValue = MakeUnversionedSentinelValue(EValueType::Null, id, flags);
+ }
+}
+
+template <class T>
+void FromUnversionedValue(
+ std::optional<T>* value,
+ TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = std::nullopt;
+ } else {
+ value->emplace();
+ FromUnversionedValue(&**value, unversionedValue);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void ToVersionedValue(
+ TVersionedValue* versionedValue,
+ T&& value,
+ const TRowBufferPtr& rowBuffer,
+ NTransactionClient::TTimestamp timestamp,
+ int id,
+ EValueFlags flags)
+{
+ ToUnversionedValue(
+ static_cast<TUnversionedValue*>(versionedValue),
+ std::forward<T>(value),
+ rowBuffer,
+ id,
+ flags);
+ versionedValue->Timestamp = timestamp;
+}
+
+template <class T>
+TVersionedValue ToVersionedValue(
+ T&& value,
+ const TRowBufferPtr& rowBuffer,
+ NTransactionClient::TTimestamp timestamp,
+ int id,
+ EValueFlags flags)
+{
+ TVersionedValue versionedValue;
+ ToVersionedValue(
+ &versionedValue,
+ std::forward<T>(value),
+ rowBuffer,
+ timestamp,
+ id,
+ flags);
+ return versionedValue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ListToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const std::function<bool(TUnversionedValue*)> producer,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags);
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const std::vector<T>& values,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ size_t index = 0;
+ ListToUnversionedValueImpl(
+ unversionedValue,
+ [&] (TUnversionedValue* itemValue) mutable -> bool {
+ if (index == values.size()) {
+ return false;
+ }
+ ToUnversionedValue(itemValue, values[index++], rowBuffer);
+ return true;
+ },
+ rowBuffer,
+ id,
+ flags);
+}
+
+void UnversionedValueToListImpl(
+ std::function<google::protobuf::Message*()> appender,
+ const NYson::TProtobufMessageType* type,
+ TUnversionedValue unversionedValue);
+
+template <class T>
+void FromUnversionedValue(
+ std::vector<T>* values,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type*)
+{
+ values->clear();
+ UnversionedValueToListImpl(
+ [&] {
+ values->emplace_back();
+ return &values->back();
+ },
+ NYson::ReflectProtobufMessageType<T>(),
+ unversionedValue);
+}
+
+void UnversionedValueToListImpl(
+ std::function<void(TUnversionedValue)> appender,
+ TUnversionedValue unversionedValue);
+
+template <class T>
+void FromUnversionedValue(
+ std::vector<T>* values,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<TUnversionedValueConversionTraits<T>::Scalar, void>::type*)
+{
+ values->clear();
+ UnversionedValueToListImpl(
+ [&] (TUnversionedValue itemValue) {
+ values->emplace_back();
+ FromUnversionedValue(&values->back(), itemValue);
+ },
+ unversionedValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void MapToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const std::function<bool(TString*, TUnversionedValue*)> producer,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags);
+
+template <class TKey, class TValue>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const THashMap<TKey, TValue>& map,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ auto it = map.begin();
+ MapToUnversionedValueImpl(
+ unversionedValue,
+ [&] (TString* itemKey, TUnversionedValue* itemValue) mutable -> bool {
+ if (it == map.end()) {
+ return false;
+ }
+ *itemKey = ToString(it->first);
+ ToUnversionedValue(itemValue, it->second, rowBuffer);
+ ++it;
+ return true;
+ },
+ rowBuffer,
+ id,
+ flags);
+}
+
+void UnversionedValueToMapImpl(
+ std::function<google::protobuf::Message*(TString)> appender,
+ const NYson::TProtobufMessageType* type,
+ TUnversionedValue unversionedValue);
+
+template <class TKey, class TValue>
+void FromUnversionedValue(
+ THashMap<TKey, TValue>* map,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<TValue*, ::google::protobuf::Message*>::value, void>::type*)
+{
+ map->clear();
+ UnversionedValueToMapImpl(
+ [&] (TString key) {
+ auto pair = map->emplace(FromString<TKey>(std::move(key)), TValue());
+ return &pair.first->second;
+ },
+ NYson::ReflectProtobufMessageType<TValue>(),
+ unversionedValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... Ts>
+auto ToUnversionedValues(
+ const TRowBufferPtr& rowBuffer,
+ Ts&&... values)
+ -> std::array<TUnversionedValue, sizeof...(Ts)>
+{
+ TRowValueTypesChecker<Ts...>();
+ std::array<TUnversionedValue, sizeof...(Ts)> array;
+ auto* current = array.data();
+ int id = 0;
+ (ToUnversionedValue(current++, std::forward<Ts>(values), rowBuffer, id++), ...);
+ return array;
+}
+
+template <class... Ts>
+void FromUnversionedRow(
+ TUnversionedRow row,
+ Ts*... values)
+{
+ if (row.GetCount() < sizeof...(Ts)) {
+ THROW_ERROR_EXCEPTION("Invalid number of values in row: expected >=%v, got %v",
+ sizeof...(Ts),
+ row.GetCount());
+ }
+ const auto* current = row.Begin();
+ (FromUnversionedValue(values, *current++), ...);
+}
+
+namespace NDetail {
+
+template <size_t Index, class... Ts>
+void TupleFromUnversionedRowHelper(std::tuple<Ts...>* tuple, TUnversionedRow row)
+{
+ if constexpr(Index < sizeof...(Ts)) {
+ FromUnversionedValue(&std::get<Index>(*tuple), row[Index]);
+ TupleFromUnversionedRowHelper<Index + 1, Ts...>(tuple, row);
+ }
+}
+
+} // namespace NDetail
+
+template <class... Ts>
+std::tuple<Ts...> FromUnversionedRow(TUnversionedRow row)
+{
+ if (row.GetCount() < sizeof...(Ts)) {
+ THROW_ERROR_EXCEPTION("Invalid number of values in row: expected >=%v, got %v",
+ sizeof...(Ts),
+ row.GetCount());
+ }
+ std::tuple<Ts...> result;
+ NDetail::TupleFromUnversionedRowHelper<0, Ts...>(&result, row);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TUnversionedValue ToUnversionedValue(T&& value, const TRowBufferPtr& rowBuffer, int id, EValueFlags flags)
+{
+ TUnversionedValue unversionedValue;
+ ToUnversionedValue(&unversionedValue, std::forward<T>(value), rowBuffer, id, flags);
+ return unversionedValue;
+}
+
+template <class T>
+T FromUnversionedValue(TUnversionedValue unversionedValue)
+{
+ T value;
+ FromUnversionedValue(&value, unversionedValue);
+ return value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... Ts>
+TUnversionedOwningRow MakeUnversionedOwningRow(Ts&&... values)
+{
+ TRowValueTypesChecker<Ts...>();
+ constexpr bool AllTypesInline = (... && TUnversionedValueConversionTraits<Ts>::Inline);
+ auto rowBuffer = AllTypesInline ? TRowBufferPtr() : New<TRowBuffer>();
+ TUnversionedOwningRowBuilder builder(sizeof...(Ts));
+ int id = 0;
+ (builder.AddValue(ToUnversionedValue(std::forward<Ts>(values), rowBuffer, id++)), ...);
+ return builder.FinishRow();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... Ts>
+void TUnversionedRowsBuilder::AddRow(Ts&&... values)
+{
+ TRowValueTypesChecker<Ts...>();
+ auto row = RowBuffer_->AllocateUnversioned(sizeof...(Ts));
+ auto* current = row.Begin();
+ int id = 0;
+ (ToUnversionedValue(current++, std::forward<Ts>(values), RowBuffer_, id++, /*flags*/ EValueFlags::None), ...);
+ Rows_.push_back(row);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TReader, class... TArgs>
+auto ReadRowBatch(const TIntrusivePtr<TReader>& reader, TArgs&&... args)
+{
+ while (true) {
+ auto batch = reader->Read(std::forward<TArgs>(args)...);
+ if (!batch || !batch->IsEmpty()) {
+ return batch;
+ }
+ NConcurrency::WaitFor(reader->GetReadyEvent())
+ .ThrowOnError();
+ }
+}
+
+template <class TWriter, class... TArgs>
+auto WriteRowBatch(const TIntrusivePtr<TWriter>& writer, TArgs&&... args)
+{
+ if (!writer->Write(std::forward<TArgs>(args)...)) {
+ NConcurrency::WaitFor(writer->GetReadyEvent())
+ .ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/helpers.cpp b/yt/yt/client/table_client/helpers.cpp
new file mode 100644
index 0000000000..83da6aab55
--- /dev/null
+++ b/yt/yt/client/table_client/helpers.cpp
@@ -0,0 +1,1510 @@
+#include "helpers.h"
+#include "schema.h"
+#include "name_table.h"
+#include "key_bound.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/protobuf_interop.h>
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NNet;
+using namespace NChunkClient;
+
+using namespace google::protobuf;
+using namespace google::protobuf::io;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsValidTableChunkFormat(EChunkFormat chunkFormat)
+{
+ return
+ chunkFormat == EChunkFormat::TableUnversionedSchemaful ||
+ chunkFormat == EChunkFormat::TableUnversionedSchemalessHorizontal ||
+ chunkFormat == EChunkFormat::TableUnversionedColumnar ||
+ chunkFormat == EChunkFormat::TableVersionedSimple ||
+ chunkFormat == EChunkFormat::TableVersionedIndexed ||
+ chunkFormat == EChunkFormat::TableVersionedColumnar ||
+ chunkFormat == EChunkFormat::TableVersionedSlim;
+}
+
+bool IsTableChunkFormatVersioned(EChunkFormat chunkFormat)
+{
+ return
+ chunkFormat == EChunkFormat::TableVersionedSimple ||
+ chunkFormat == EChunkFormat::TableVersionedIndexed ||
+ chunkFormat == EChunkFormat::TableVersionedColumnar ||
+ chunkFormat == EChunkFormat::TableVersionedSlim;
+}
+
+void ValidateTableChunkFormat(EChunkFormat chunkFormat)
+{
+ if (!IsValidTableChunkFormat(chunkFormat)) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::InvalidTableChunkFormat,
+ "%Qlv is not a valid table chunk format",
+ chunkFormat);
+ }
+}
+
+void ValidateTableChunkFormatAndOptimizeFor(
+ EChunkFormat chunkFormat,
+ EOptimizeFor optimizeFor)
+{
+ ValidateTableChunkFormat(chunkFormat);
+ if (OptimizeForFromFormat(chunkFormat) != optimizeFor) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::InvalidTableChunkFormat,
+ "%Qlv is not a valid %Qlv chunk format",
+ chunkFormat,
+ optimizeFor);
+ }
+}
+
+void ValidateTableChunkFormatVersioned(
+ EChunkFormat chunkFormat,
+ bool versioned)
+{
+ if (IsTableChunkFormatVersioned(chunkFormat) != versioned) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::InvalidTableChunkFormat,
+ "%Qlv is not a valid %v chunk format",
+ chunkFormat,
+ versioned ? "versioned" : "unversioned");
+ }
+}
+
+EOptimizeFor OptimizeForFromFormat(EChunkFormat chunkFormat)
+{
+ ValidateTableChunkFormat(chunkFormat);
+ switch (chunkFormat) {
+ case EChunkFormat::TableUnversionedSchemaful:
+ case EChunkFormat::TableUnversionedSchemalessHorizontal:
+ case EChunkFormat::TableVersionedSimple:
+ case EChunkFormat::TableVersionedIndexed:
+ case EChunkFormat::TableVersionedSlim:
+ return EOptimizeFor::Lookup;
+
+ case EChunkFormat::TableUnversionedColumnar:
+ case EChunkFormat::TableVersionedColumnar:
+ return EOptimizeFor::Scan;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+EChunkFormat DefaultFormatFromOptimizeFor(
+ EOptimizeFor optimizeFor,
+ bool versioned)
+{
+ if (versioned) {
+ switch (optimizeFor) {
+ case EOptimizeFor::Lookup:
+ return EChunkFormat::TableVersionedSimple;
+ case EOptimizeFor::Scan:
+ return EChunkFormat::TableVersionedColumnar;
+ default:
+ YT_ABORT();
+ }
+ } else {
+ switch (optimizeFor) {
+ case EOptimizeFor::Lookup:
+ return EChunkFormat::TableUnversionedSchemalessHorizontal;
+ case EOptimizeFor::Scan:
+ return EChunkFormat::TableUnversionedColumnar;
+ default:
+ YT_ABORT();
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void YTreeNodeToUnversionedValue(
+ TUnversionedOwningRowBuilder* builder,
+ const INodePtr& value,
+ int id,
+ EValueFlags flags)
+{
+ switch (value->GetType()) {
+ #define XX(type, cppType) \
+ case ENodeType::type: \
+ builder->AddValue(MakeUnversioned ## type ## Value(value->As ## type()->GetValue(), id, flags)); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+ case ENodeType::Entity:
+ builder->AddValue(MakeUnversionedSentinelValue(EValueType::Null, id, flags));
+ break;
+ default:
+ builder->AddValue(MakeUnversionedAnyValue(ConvertToYsonString(value).AsStringBuf(), id, flags));
+ break;
+ }
+}
+
+} // namespace
+
+TUnversionedOwningRow YsonToSchemafulRow(
+ const TString& yson,
+ const TTableSchema& tableSchema,
+ bool treatMissingAsNull,
+ NYson::EYsonType ysonType,
+ bool validateValues)
+{
+ auto nameTable = TNameTable::FromSchema(tableSchema);
+
+ auto rowParts = ConvertTo<THashMap<TString, INodePtr>>(
+ TYsonString(yson, ysonType));
+
+ TUnversionedOwningRowBuilder rowBuilder;
+ auto validateAndAddValue = [&rowBuilder, &validateValues] (const TUnversionedValue& value, const TColumnSchema& column) {
+ if (validateValues) {
+ ValidateValueType(
+ value,
+ column,
+ /*typeAnyAcceptsAllValues*/ true,
+ /*ignoreRequired*/ false,
+ /*validateAnyIsValidYson*/ false);
+ }
+
+ rowBuilder.AddValue(value);
+ };
+
+ auto addValue = [&] (int id, INodePtr value) {
+ try {
+ auto column = tableSchema.Columns()[id];
+ if (value->GetType() == ENodeType::Entity) {
+ validateAndAddValue(MakeUnversionedSentinelValue(
+ value->Attributes().Get<EValueType>("type", EValueType::Null), id), column);
+ return;
+ }
+
+ auto type = column.GetWireType();
+ switch (type) {
+ #define XX(type, cppType) \
+ case EValueType::type: \
+ validateAndAddValue(MakeUnversioned ## type ## Value(value->As ## type()->GetValue(), id), column); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+ case EValueType::Any:
+ validateAndAddValue(MakeUnversionedAnyValue(ConvertToYsonString(value).AsStringBuf(), id), column);
+ break;
+ case EValueType::Composite:
+ validateAndAddValue(MakeUnversionedCompositeValue(ConvertToYsonString(value).AsStringBuf(), id), column);
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported value type %Qlv",
+ type);
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing value of column %Qv",
+ tableSchema.Columns()[id].Name())
+ << ex;
+ }
+ };
+
+ const auto& keyColumns = tableSchema.GetKeyColumns();
+
+ // Key
+ for (int id = 0; id < std::ssize(keyColumns); ++id) {
+ auto it = rowParts.find(nameTable->GetName(id));
+ if (it == rowParts.end()) {
+ validateAndAddValue(MakeUnversionedSentinelValue(EValueType::Null, id), tableSchema.Columns()[id]);
+ } else {
+ addValue(id, it->second);
+ }
+ }
+
+ // Fixed values
+ for (int id = std::ssize(keyColumns); id < std::ssize(tableSchema.Columns()); ++id) {
+ auto it = rowParts.find(nameTable->GetName(id));
+ if (it != rowParts.end()) {
+ addValue(id, it->second);
+ } else if (treatMissingAsNull) {
+ validateAndAddValue(MakeUnversionedSentinelValue(EValueType::Null, id), tableSchema.Columns()[id]);
+ } else if (validateValues && tableSchema.Columns()[id].Required()) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Required column %v cannot have %Qlv value",
+ tableSchema.Columns()[id].GetDiagnosticNameString(),
+ EValueType::Null);
+ }
+ }
+
+ // Variable values
+ for (const auto& [name, value] : rowParts) {
+ int id = nameTable->GetIdOrRegisterName(name);
+ if (id >= std::ssize(tableSchema.Columns())) {
+ if (validateValues && tableSchema.GetStrict()) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Unknown column %Qv in strict schema",
+ name);
+ }
+ YTreeNodeToUnversionedValue(&rowBuilder, value, id, EValueFlags::None);
+ }
+ }
+
+ return rowBuilder.FinishRow();
+}
+
+TUnversionedOwningRow YsonToSchemalessRow(const TString& valueYson)
+{
+ TUnversionedOwningRowBuilder builder;
+
+ auto values = ConvertTo<std::vector<INodePtr>>(TYsonString(valueYson, EYsonType::ListFragment));
+ for (const auto& value : values) {
+ int id = value->Attributes().Get<int>("id");
+ auto flags = EValueFlags::None;
+ if (value->Attributes().Get<bool>("aggregate", false)) {
+ flags |= EValueFlags::Aggregate;
+ }
+ YTreeNodeToUnversionedValue(&builder, value, id, flags);
+ }
+
+ return builder.FinishRow();
+}
+
+TVersionedRow YsonToVersionedRow(
+ const TRowBufferPtr& rowBuffer,
+ const TString& keyYson,
+ const TString& valueYson,
+ const std::vector<TTimestamp>& deleteTimestamps,
+ const std::vector<TTimestamp>& extraWriteTimestamps)
+{
+ TVersionedRowBuilder builder(rowBuffer);
+
+ auto keys = ConvertTo<std::vector<INodePtr>>(TYsonString(keyYson, EYsonType::ListFragment));
+
+ for (auto key : keys) {
+ int id = key->Attributes().Get<int>("id");
+ switch (key->GetType()) {
+ #define XX(type, cppType) \
+ case ENodeType::type: \
+ builder.AddKey(MakeUnversioned ## type ## Value(key->As ## type()->GetValue(), id)); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+ case ENodeType::Entity:
+ builder.AddKey(MakeUnversionedSentinelValue(EValueType::Null, id));
+ break;
+ default:
+ YT_ABORT();
+ break;
+ }
+ }
+
+ auto values = ConvertTo<std::vector<INodePtr>>(TYsonString(valueYson, EYsonType::ListFragment));
+ for (auto value : values) {
+ int id = value->Attributes().Get<int>("id");
+ auto timestamp = value->Attributes().Get<TTimestamp>("ts");
+ auto flags = EValueFlags::None;
+ if (value->Attributes().Get<bool>("aggregate", false)) {
+ flags |= EValueFlags::Aggregate;
+ }
+ switch (value->GetType()) {
+ #define XX(type, cppType) \
+ case ENodeType::type: \
+ builder.AddValue(MakeVersioned ## type ## Value(value->As ## type()->GetValue(), timestamp, id, flags)); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+ case ENodeType::Entity:
+ builder.AddValue(MakeVersionedSentinelValue(EValueType::Null, timestamp, id, flags));
+ break;
+ default:
+ builder.AddValue(MakeVersionedAnyValue(ConvertToYsonString(value).AsStringBuf(), timestamp, id, flags));
+ break;
+ }
+ }
+
+ for (auto timestamp : deleteTimestamps) {
+ builder.AddDeleteTimestamp(timestamp);
+ }
+
+ for (auto timestamp : extraWriteTimestamps) {
+ builder.AddWriteTimestamp(timestamp);
+ }
+
+ return builder.FinishRow();
+}
+
+TVersionedOwningRow YsonToVersionedRow(
+ const TString& keyYson,
+ const TString& valueYson,
+ const std::vector<TTimestamp>& deleteTimestamps,
+ const std::vector<TTimestamp>& extraWriteTimestamps)
+{
+ // NB: this implementation is extra slow, it is intended only for using in tests.
+ auto rowBuffer = New<TRowBuffer>();
+ auto row = YsonToVersionedRow(rowBuffer, keyYson, valueYson, deleteTimestamps, extraWriteTimestamps);
+ return TVersionedOwningRow(row);
+}
+
+TUnversionedOwningRow YsonToKey(const TString& yson)
+{
+ TUnversionedOwningRowBuilder keyBuilder;
+ auto keyParts = ConvertTo<std::vector<INodePtr>>(
+ TYsonString(yson, EYsonType::ListFragment));
+
+ for (int id = 0; id < std::ssize(keyParts); ++id) {
+ const auto& keyPart = keyParts[id];
+ switch (keyPart->GetType()) {
+ #define XX(type, cppType) \
+ case ENodeType::type: \
+ keyBuilder.AddValue( \
+ MakeUnversioned ## type ## Value(keyPart->As ## type()->GetValue(), \
+ id)); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+ case ENodeType::Entity:
+ keyBuilder.AddValue(MakeUnversionedSentinelValue(
+ keyPart->Attributes().Get<EValueType>("type", EValueType::Null),
+ id));
+ break;
+ default:
+ keyBuilder.AddValue(MakeUnversionedAnyValue(
+ ConvertToYsonString(keyPart).AsStringBuf(),
+ id));
+ break;
+ }
+ }
+
+ return keyBuilder.FinishRow();
+}
+
+TString KeyToYson(TUnversionedRow row)
+{
+ return ConvertToYsonString(row, EYsonFormat::Text).ToString();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ std::nullopt_t,
+ const TRowBufferPtr& /*rowBuffer*/,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = MakeUnversionedSentinelValue(EValueType::Null, id, flags);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ TGuid value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ std::array<char, MaxGuidStringSize> buffer;
+ auto* bufferEnd = WriteGuidToBuffer(buffer.data(), value);
+ TStringBuf bufferStr(buffer.begin(), bufferEnd);
+ *unversionedValue = value
+ ? rowBuffer->CaptureValue(MakeUnversionedStringValue(bufferStr, id, flags))
+ : MakeUnversionedSentinelValue(EValueType::Null, id, flags);
+}
+
+void FromUnversionedValue(TGuid* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = TGuid();
+ return;
+ }
+ if (unversionedValue.Type != EValueType::String) {
+ THROW_ERROR_EXCEPTION("Cannot parse object id value from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = TGuid::FromString(unversionedValue.AsStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const TString& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ ToUnversionedValue(unversionedValue, static_cast<TStringBuf>(value), rowBuffer, id, flags);
+}
+
+void FromUnversionedValue(TString* value, TUnversionedValue unversionedValue)
+{
+ TStringBuf uncapturedValue;
+ FromUnversionedValue(&uncapturedValue, unversionedValue);
+ *value = TString(uncapturedValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ TStringBuf value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedStringValue(value, id, flags));
+}
+
+void FromUnversionedValue(TStringBuf* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = TStringBuf{};
+ return;
+ }
+ if (unversionedValue.Type != EValueType::String) {
+ THROW_ERROR_EXCEPTION("Cannot parse string value from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = unversionedValue.AsStringBuf();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const char* value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ ToUnversionedValue(unversionedValue, TStringBuf(value), rowBuffer, id, flags);
+}
+
+void FromUnversionedValue(const char** value, TUnversionedValue unversionedValue)
+{
+ *value = FromUnversionedValue<TStringBuf>(unversionedValue).data();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ bool value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedBooleanValue(value, id, flags));
+}
+
+void FromUnversionedValue(bool* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = false;
+ return;
+ }
+ if (unversionedValue.Type != EValueType::Boolean) {
+ THROW_ERROR_EXCEPTION("Cannot parse \"boolean\" value from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = unversionedValue.Data.Boolean;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const TYsonString& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ YT_ASSERT(value.GetType() == EYsonType::Node);
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(value.AsStringBuf(), id, flags));
+}
+
+void FromUnversionedValue(TYsonString* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse YSON string from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = TYsonString(unversionedValue.AsString());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const NYson::TYsonStringBuf& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ YT_ASSERT(value.GetType() == EYsonType::Node);
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(value.AsStringBuf(), id, flags));
+}
+
+void FromUnversionedValue(NYson::TYsonStringBuf* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse YSON string from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = TYsonStringBuf(unversionedValue.AsStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define XX(cppType, codeType, humanReadableType) \
+ void ToUnversionedValue( \
+ TUnversionedValue* unversionedValue, \
+ cppType value, \
+ const TRowBufferPtr& /*rowBuffer*/, \
+ int id, \
+ EValueFlags flags) \
+ { \
+ *unversionedValue = MakeUnversioned ## codeType ## Value(value, id, flags); \
+ } \
+ \
+ void FromUnversionedValue(cppType* value, TUnversionedValue unversionedValue) \
+ { \
+ switch (unversionedValue.Type) { \
+ case EValueType::Int64: \
+ *value = CheckedIntegralCast<cppType>(unversionedValue.Data.Int64); \
+ break; \
+ case EValueType::Uint64: \
+ *value = CheckedIntegralCast<cppType>(unversionedValue.Data.Uint64); \
+ break; \
+ default: \
+ THROW_ERROR_EXCEPTION("Cannot parse \"" #humanReadableType "\" value from %Qlv", \
+ unversionedValue.Type); \
+ } \
+ }
+
+XX(i64, Int64, int64)
+XX(ui64, Uint64, uint64)
+XX(i32, Int64, int32)
+XX(ui32, Uint64, uint32)
+XX(i16, Int64, int32)
+XX(ui16, Uint64, uint16)
+XX(i8, Int64, int8)
+XX(ui8, Uint64, uint8)
+
+#undef XX
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ double value,
+ const TRowBufferPtr& /*rowBuffer*/,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = MakeUnversionedDoubleValue(value, id, flags);
+}
+
+void FromUnversionedValue(double* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type != EValueType::Double) {
+ THROW_ERROR_EXCEPTION("Cannot parse \"double\" value from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = unversionedValue.Data.Double;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ TInstant value,
+ const TRowBufferPtr& /*rowBuffer*/,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = MakeUnversionedUint64Value(value.MicroSeconds(), id, flags);
+}
+
+void FromUnversionedValue(TInstant* value, TUnversionedValue unversionedValue)
+{
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ *value = TInstant::MicroSeconds(CheckedIntegralCast<ui64>(unversionedValue.Data.Int64));
+ break;
+ case EValueType::Uint64:
+ *value = TInstant::MicroSeconds(unversionedValue.Data.Uint64);
+ break;
+ case EValueType::String:
+ *value = TInstant::ParseIso8601(unversionedValue.AsStringBuf());
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse instant from %Qlv",
+ unversionedValue.Type);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ TDuration value,
+ const TRowBufferPtr& /*rowBuffer*/,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = MakeUnversionedUint64Value(value.MicroSeconds(), id, flags);
+}
+
+void FromUnversionedValue(TDuration* value, TUnversionedValue unversionedValue)
+{
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ *value = TDuration::MicroSeconds(CheckedIntegralCast<ui64>(unversionedValue.Data.Int64));
+ break;
+ case EValueType::Uint64:
+ *value = TDuration::MicroSeconds(unversionedValue.Data.Uint64);
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse duration from %Qlv",
+ unversionedValue.Type);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const IMapNodePtr& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(ConvertToYsonString(value).AsStringBuf(), id, flags));
+}
+
+void FromUnversionedValue(IMapNodePtr* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = nullptr;
+ }
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse YSON map from %Qlv",
+ unversionedValue.Type);
+ }
+ *value = ConvertTo<IMapNodePtr>(FromUnversionedValue<TYsonStringBuf>(unversionedValue));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const TIP6Address& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ ToUnversionedValue(unversionedValue, ToString(value), rowBuffer, id, flags);
+}
+
+void FromUnversionedValue(TIP6Address* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = TIP6Address();
+ }
+ auto strValue = FromUnversionedValue<TString>(unversionedValue);
+ *value = TIP6Address::FromString(strValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const TError& value,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ auto errorYson = ConvertToYsonString(value);
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(errorYson.AsStringBuf(), id, flags));
+}
+
+void FromUnversionedValue(TError* value, TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ *value = {};
+ }
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION(
+ "Cannot parse error from value of type %Qlv",
+ unversionedValue.Type);
+ }
+ *value = ConvertTo<TError>(FromUnversionedValue<TYsonStringBuf>(unversionedValue));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ProtobufToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const Message& value,
+ const TProtobufMessageType* type,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ auto byteSize = value.ByteSizeLong();
+ auto* pool = rowBuffer->GetPool();
+ auto* wireBuffer = pool->AllocateUnaligned(byteSize);
+ YT_VERIFY(value.SerializePartialToArray(wireBuffer, byteSize));
+ ArrayInputStream inputStream(wireBuffer, byteSize);
+ TString ysonBytes;
+ TStringOutput outputStream(ysonBytes);
+ TYsonWriter ysonWriter(&outputStream);
+ ParseProtobuf(&ysonWriter, &inputStream, type);
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(ysonBytes, id, flags));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void UnversionedValueToProtobufImpl(
+ Message* value,
+ const TProtobufMessageType* type,
+ TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ value->Clear();
+ return;
+ }
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse a protobuf message from %Qlv",
+ unversionedValue.Type);
+ }
+ TString wireBytes;
+ StringOutputStream outputStream(&wireBytes);
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Keep);
+ auto protobufWriter = CreateProtobufWriter(&outputStream, type, options);
+ ParseYsonStringBuffer(
+ unversionedValue.AsStringBuf(),
+ EYsonType::Node,
+ protobufWriter.get());
+ if (!value->ParseFromArray(wireBytes.data(), wireBytes.size())) {
+ THROW_ERROR_EXCEPTION("Error parsing %v from wire bytes",
+ value->GetTypeName());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ListToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const std::function<bool(TUnversionedValue*)> producer,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ TString ysonBytes;
+ TStringOutput outputStream(ysonBytes);
+ NYT::NYson::TYsonWriter writer(&outputStream);
+ writer.OnBeginList();
+
+ TUnversionedValue itemValue;
+ while (true) {
+ writer.OnListItem();
+ if (!producer(&itemValue)) {
+ break;
+ }
+ UnversionedValueToYson(itemValue, &writer);
+ }
+ writer.OnEndList();
+
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(ysonBytes, id, flags));
+}
+
+void UnversionedValueToListImpl(
+ std::function<google::protobuf::Message*()> appender,
+ const TProtobufMessageType* type,
+ TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ return;
+ }
+
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse vector from %Qlv",
+ unversionedValue.Type);
+ }
+
+ class TConsumer
+ : public IYsonConsumer
+ {
+ public:
+ TConsumer(
+ std::function<google::protobuf::Message*()> appender,
+ const TProtobufMessageType* type)
+ : Appender_(std::move(appender))
+ , Type_(type)
+ , OutputStream_(&WireBytes_)
+ { }
+
+ void OnStringScalar(TStringBuf value) override
+ {
+ GetUnderlying()->OnStringScalar(value);
+ }
+
+ void OnInt64Scalar(i64 value) override
+ {
+ GetUnderlying()->OnInt64Scalar(value);
+ }
+
+ void OnUint64Scalar(ui64 value) override
+ {
+ GetUnderlying()->OnUint64Scalar(value);
+ }
+
+ void OnDoubleScalar(double value) override
+ {
+ GetUnderlying()->OnDoubleScalar(value);
+ }
+
+ void OnBooleanScalar(bool value) override
+ {
+ GetUnderlying()->OnBooleanScalar(value);
+ }
+
+ void OnEntity() override
+ {
+ GetUnderlying()->OnEntity();
+ }
+
+ void OnBeginList() override
+ {
+ if (Depth_ > 0) {
+ GetUnderlying()->OnBeginList();
+ }
+ ++Depth_;
+ }
+
+ void OnListItem() override
+ {
+ if (Depth_ == 1) {
+ NextElement();
+ } else {
+ GetUnderlying()->OnListItem();
+ }
+ }
+
+ void OnEndList() override
+ {
+ --Depth_;
+ if (Depth_ == 0) {
+ FlushElement();
+ } else {
+ GetUnderlying()->OnEndList();
+ }
+ }
+
+ void OnBeginMap() override
+ {
+ ++Depth_;
+ GetUnderlying()->OnBeginMap();
+ }
+
+ void OnKeyedItem(TStringBuf key) override
+ {
+ GetUnderlying()->OnKeyedItem(key);
+ }
+
+ void OnEndMap() override
+ {
+ --Depth_;
+ GetUnderlying()->OnEndMap();
+ }
+
+ void OnBeginAttributes() override
+ {
+ GetUnderlying()->OnBeginAttributes();
+ }
+
+ void OnEndAttributes() override
+ {
+ GetUnderlying()->OnEndAttributes();
+ }
+
+ void OnRaw(TStringBuf yson, EYsonType type) override
+ {
+ GetUnderlying()->OnRaw(yson, type);
+ }
+
+ private:
+ const std::function<google::protobuf::Message*()> Appender_;
+ const TProtobufMessageType* const Type_;
+
+ std::unique_ptr<IYsonConsumer> Underlying_;
+ int Depth_ = 0;
+
+ TString WireBytes_;
+ StringOutputStream OutputStream_;
+
+
+ IYsonConsumer* GetUnderlying()
+ {
+ if (!Underlying_) {
+ THROW_ERROR_EXCEPTION("YSON value must be a list without attributes");
+ }
+ return Underlying_.get();
+ }
+
+ void NextElement()
+ {
+ FlushElement();
+ WireBytes_.clear();
+ Underlying_ = CreateProtobufWriter(&OutputStream_, Type_);
+ }
+
+ void FlushElement()
+ {
+ if (!Underlying_) {
+ return;
+ }
+ auto* value = Appender_();
+ if (!value->ParseFromArray(WireBytes_.data(), WireBytes_.size())) {
+ THROW_ERROR_EXCEPTION("Error parsing %v from wire bytes",
+ value->GetTypeName());
+ }
+ Underlying_.reset();
+ }
+ } consumer(std::move(appender), type);
+
+ ParseYsonStringBuffer(
+ unversionedValue.AsStringBuf(),
+ EYsonType::Node,
+ &consumer);
+}
+
+void UnversionedValueToListImpl(
+ std::function<void(TUnversionedValue)> appender,
+ TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ return;
+ }
+
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse a vector from %Qlv",
+ unversionedValue.Type);
+ }
+
+ class TConsumer
+ : public TYsonConsumerBase
+ {
+ public:
+ explicit TConsumer(std::function<void(TUnversionedValue)> appender)
+ : Appender_(std::move(appender))
+ { }
+
+ void OnStringScalar(TStringBuf value) override
+ {
+ EnsureInList();
+ Appender_(MakeUnversionedStringValue(value));
+ }
+
+ void OnInt64Scalar(i64 value) override
+ {
+ EnsureInList();
+ Appender_(MakeUnversionedInt64Value(value));
+ }
+
+ void OnUint64Scalar(ui64 value) override
+ {
+ EnsureInList();
+ Appender_(MakeUnversionedUint64Value(value));
+ }
+
+ void OnDoubleScalar(double value) override
+ {
+ EnsureInList();
+ Appender_(MakeUnversionedDoubleValue(value));
+ }
+
+ void OnBooleanScalar(bool value) override
+ {
+ EnsureInList();
+ Appender_(MakeUnversionedBooleanValue(value));
+ }
+
+ void OnEntity() override
+ {
+ THROW_ERROR_EXCEPTION("YSON entities are not supported");
+ }
+
+ void OnBeginList() override
+ {
+ EnsureNotInList();
+ InList_ = true;
+ }
+
+ void OnListItem() override
+ { }
+
+ void OnEndList() override
+ { }
+
+ void OnBeginMap() override
+ {
+ THROW_ERROR_EXCEPTION("YSON maps are not supported");
+ }
+
+ void OnKeyedItem(TStringBuf /*key*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnEndMap() override
+ {
+ YT_ABORT();
+ }
+
+ void OnBeginAttributes() override
+ {
+ THROW_ERROR_EXCEPTION("YSON attributes are not supported");
+ }
+
+ void OnEndAttributes() override
+ {
+ YT_ABORT();
+ }
+
+ private:
+ const std::function<void(TUnversionedValue)> Appender_;
+
+ bool InList_ = false;
+
+ void EnsureInList()
+ {
+ if (!InList_) {
+ THROW_ERROR_EXCEPTION("YSON list expected");
+ }
+ }
+
+ void EnsureNotInList()
+ {
+ if (InList_) {
+ THROW_ERROR_EXCEPTION("YSON list is unexpected");
+ }
+ }
+ } consumer(std::move(appender));
+
+ ParseYsonStringBuffer(
+ unversionedValue.AsStringBuf(),
+ EYsonType::Node,
+ &consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void MapToUnversionedValueImpl(
+ TUnversionedValue* unversionedValue,
+ const std::function<bool(TString*, TUnversionedValue*)> producer,
+ const TRowBufferPtr& rowBuffer,
+ int id,
+ EValueFlags flags)
+{
+ TString ysonBytes;
+ TStringOutput outputStream(ysonBytes);
+ NYT::NYson::TYsonWriter writer(&outputStream);
+ writer.OnBeginMap();
+
+ TString itemKey;
+ TUnversionedValue itemValue;
+ while (true) {
+ if (!producer(&itemKey, &itemValue)) {
+ break;
+ }
+ writer.OnKeyedItem(itemKey);
+ UnversionedValueToYson(itemValue, &writer);
+ }
+ writer.OnEndMap();
+
+ *unversionedValue = rowBuffer->CaptureValue(MakeUnversionedAnyValue(ysonBytes, id, flags));
+}
+
+void UnversionedValueToMapImpl(
+ std::function<google::protobuf::Message*(TString)> appender,
+ const TProtobufMessageType* type,
+ TUnversionedValue unversionedValue)
+{
+ if (unversionedValue.Type == EValueType::Null) {
+ return;
+ }
+
+ if (unversionedValue.Type != EValueType::Any) {
+ THROW_ERROR_EXCEPTION("Cannot parse map from %Qlv",
+ unversionedValue.Type);
+ }
+
+ class TConsumer
+ : public IYsonConsumer
+ {
+ public:
+ TConsumer(
+ std::function<google::protobuf::Message*(TString)> appender,
+ const TProtobufMessageType* type)
+ : Appender_(std::move(appender))
+ , Type_(type)
+ , OutputStream_(&WireBytes_)
+ { }
+
+ void OnStringScalar(TStringBuf value) override
+ {
+ GetUnderlying()->OnStringScalar(value);
+ }
+
+ void OnInt64Scalar(i64 value) override
+ {
+ GetUnderlying()->OnInt64Scalar(value);
+ }
+
+ void OnUint64Scalar(ui64 value) override
+ {
+ GetUnderlying()->OnUint64Scalar(value);
+ }
+
+ void OnDoubleScalar(double value) override
+ {
+ GetUnderlying()->OnDoubleScalar(value);
+ }
+
+ void OnBooleanScalar(bool value) override
+ {
+ GetUnderlying()->OnBooleanScalar(value);
+ }
+
+ void OnEntity() override
+ {
+ GetUnderlying()->OnEntity();
+ }
+
+ void OnBeginList() override
+ {
+ ++Depth_;
+ GetUnderlying()->OnBeginList();
+ }
+
+ void OnListItem() override
+ {
+ GetUnderlying()->OnListItem();
+ }
+
+ void OnEndList() override
+ {
+ --Depth_;
+ GetUnderlying()->OnEndList();
+ }
+
+ void OnBeginMap() override
+ {
+ if (Depth_ > 0) {
+ GetUnderlying()->OnBeginMap();
+ }
+ ++Depth_;
+ }
+
+ void OnKeyedItem(TStringBuf key) override
+ {
+ if (Depth_ == 1) {
+ NextElement(key);
+ } else {
+ GetUnderlying()->OnKeyedItem(key);
+ }
+ }
+
+ void OnEndMap() override
+ {
+ --Depth_;
+ if (Depth_ == 0) {
+ FlushElement();
+ } else {
+ GetUnderlying()->OnEndMap();
+ }
+ }
+
+ void OnBeginAttributes() override
+ {
+ GetUnderlying()->OnBeginAttributes();
+ }
+
+ void OnEndAttributes() override
+ {
+ GetUnderlying()->OnEndAttributes();
+ }
+
+ void OnRaw(TStringBuf yson, EYsonType type) override
+ {
+ GetUnderlying()->OnRaw(yson, type);
+ }
+
+ private:
+ const std::function<google::protobuf::Message*(TString)> Appender_;
+ const TProtobufMessageType* const Type_;
+
+ std::optional<TString> Key_;
+ std::unique_ptr<IYsonConsumer> Underlying_;
+ int Depth_ = 0;
+
+ TString WireBytes_;
+ StringOutputStream OutputStream_;
+
+
+ IYsonConsumer* GetUnderlying()
+ {
+ if (!Underlying_) {
+ THROW_ERROR_EXCEPTION("YSON value must be a list without attributes");
+ }
+ return Underlying_.get();
+ }
+
+ void NextElement(TStringBuf key)
+ {
+ FlushElement();
+ WireBytes_.clear();
+ Key_ = TString(key);
+ Underlying_ = CreateProtobufWriter(&OutputStream_, Type_);
+ }
+
+ void FlushElement()
+ {
+ if (!Underlying_) {
+ return;
+ }
+ auto* value = Appender_(*Key_);
+ if (!value->ParseFromArray(WireBytes_.data(), WireBytes_.size())) {
+ THROW_ERROR_EXCEPTION("Error parsing %v from wire bytes",
+ value->GetTypeName());
+ }
+ Underlying_.reset();
+ Key_.reset();
+ }
+ } consumer(std::move(appender), type);
+
+ ParseYsonStringBuffer(
+ unversionedValue.AsStringBuf(),
+ EYsonType::Node,
+ &consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void UnversionedValueToYson(TUnversionedValue unversionedValue, TCheckedInDebugYsonTokenWriter* tokenWriter)
+{
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ tokenWriter->WriteBinaryInt64(unversionedValue.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ tokenWriter->WriteBinaryUint64(unversionedValue.Data.Uint64);
+ return;
+ case EValueType::Double:
+ tokenWriter->WriteBinaryDouble(unversionedValue.Data.Double);
+ return;
+ case EValueType::String:
+ tokenWriter->WriteBinaryString(unversionedValue.AsStringBuf());
+ return;
+ case EValueType::Any:
+ case EValueType::Composite:
+ tokenWriter->WriteRawNodeUnchecked(unversionedValue.AsStringBuf());
+ return;
+ case EValueType::Boolean:
+ tokenWriter->WriteBinaryBoolean(unversionedValue.Data.Boolean);
+ return;
+ case EValueType::Null:
+ tokenWriter->WriteEntity();
+ return;
+ case EValueType::TheBottom:
+ case EValueType::Min:
+ case EValueType::Max:
+ YT_ABORT();
+ }
+ ThrowUnexpectedValueType(unversionedValue.Type);
+}
+
+void UnversionedValueToYson(TUnversionedValue unversionedValue, IYsonConsumer* consumer)
+{
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ consumer->OnInt64Scalar(unversionedValue.Data.Int64);
+ return;
+ case EValueType::Uint64:
+ consumer->OnUint64Scalar(unversionedValue.Data.Uint64);
+ return;
+ case EValueType::Double:
+ consumer->OnDoubleScalar(unversionedValue.Data.Double);
+ return;
+ case EValueType::String:
+ consumer->OnStringScalar(unversionedValue.AsStringBuf());
+ return;
+ case EValueType::Any:
+ case EValueType::Composite:
+ consumer->OnRaw(unversionedValue.AsStringBuf(), EYsonType::Node);
+ return;
+ case EValueType::Boolean:
+ consumer->OnBooleanScalar(unversionedValue.Data.Boolean);
+ return;
+ case EValueType::Null:
+ consumer->OnEntity();
+ return;
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ YT_ABORT();
+ }
+ ThrowUnexpectedValueType(unversionedValue.Type);
+}
+
+TYsonString UnversionedValueToYson(TUnversionedValue unversionedValue, bool enableRaw)
+{
+ TString data;
+ data.reserve(GetYsonSize(unversionedValue));
+ TStringOutput output(data);
+ TYsonWriter writer(&output, EYsonFormat::Binary, EYsonType::Node, /* enableRaw */ enableRaw);
+ UnversionedValueToYson(unversionedValue, &writer);
+ return TYsonString(std::move(data));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToAny(
+ TRowBuffer* rowBuffer,
+ TUnversionedValue* result,
+ TUnversionedValue* value,
+ EYsonFormat format)
+{
+ TStringStream stream;
+ NYson::TYsonWriter writer(&stream, format);
+
+ switch (value->Type) {
+ case EValueType::Null:
+ *result = MakeUnversionedNullValue();
+ return;
+ case EValueType::Any:
+ case EValueType::Composite: {
+ if (format == EYsonFormat::Binary) {
+ *result = *value;
+ return;
+ } else {
+ writer.OnRaw(value->AsStringBuf());
+ }
+ break;
+ }
+ case EValueType::String: {
+ writer.OnStringScalar(value->AsStringBuf());
+ break;
+ }
+ case EValueType::Int64: {
+ writer.OnInt64Scalar(value->Data.Int64);
+ break;
+ }
+ case EValueType::Uint64: {
+ writer.OnUint64Scalar(value->Data.Uint64);
+ break;
+ }
+ case EValueType::Double: {
+ writer.OnDoubleScalar(value->Data.Double);
+ break;
+ }
+ case EValueType::Boolean: {
+ writer.OnBooleanScalar(value->Data.Boolean);
+ break;
+ }
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ YT_ABORT();
+ }
+
+ writer.Flush();
+
+ *result = rowBuffer->CaptureValue(
+ MakeUnversionedAnyValue(TStringBuf(stream.Data(), stream.Size())));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDefaultUnversionedRowsBuilderTag
+{ };
+
+TUnversionedRowsBuilder::TUnversionedRowsBuilder()
+ : TUnversionedRowsBuilder(New<TRowBuffer>(TDefaultUnversionedRowsBuilderTag()))
+{ }
+
+TUnversionedRowsBuilder::TUnversionedRowsBuilder(TRowBufferPtr rowBuffer)
+ : RowBuffer_(std::move(rowBuffer))
+{ }
+
+void TUnversionedRowsBuilder::ReserveRows(int rowCount)
+{
+ Rows_.reserve(rowCount);
+}
+
+void TUnversionedRowsBuilder::AddRow(TUnversionedRow row)
+{
+ Rows_.push_back(RowBuffer_->CaptureRow(row));
+}
+
+void TUnversionedRowsBuilder::AddRow(TMutableUnversionedRow row)
+{
+ AddRow(TUnversionedRow(row));
+}
+
+void TUnversionedRowsBuilder::AddProtoRow(const TString& protoRow)
+{
+ auto& row = Rows_.emplace_back();
+ FromProto(&row, protoRow, RowBuffer_);
+}
+
+TSharedRange<TUnversionedRow> TUnversionedRowsBuilder::Build()
+{
+ return MakeSharedRange(std::move(Rows_), std::move(RowBuffer_));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(
+ NProto::TDataBlockMeta,
+ /*last_key*/ 9,
+ TUnversionedOwningRow)
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(
+ NProto::TBoundaryKeysExt,
+ /*min*/ 1,
+ TUnversionedOwningRow)
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(
+ NProto::TBoundaryKeysExt,
+ /*max*/ 2,
+ TUnversionedOwningRow)
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(
+ NProto::TSamplesExt,
+ /*entries*/ 1,
+ TUnversionedOwningRow)
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(
+ NProto::THeavyColumnStatisticsExt,
+ /*column_data_weights*/ 5,
+ TUnversionedOwningRow)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/helpers.h b/yt/yt/client/table_client/helpers.h
new file mode 100644
index 0000000000..a9a15b16a0
--- /dev/null
+++ b/yt/yt/client/table_client/helpers.h
@@ -0,0 +1,343 @@
+#pragma once
+
+#include "versioned_row.h"
+#include "unversioned_row.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsValidTableChunkFormat(NChunkClient::EChunkFormat chunkFormat);
+bool IsTableChunkFormatVersioned(NChunkClient::EChunkFormat chunkFormat);
+
+void ValidateTableChunkFormat(NChunkClient::EChunkFormat chunkFormat);
+void ValidateTableChunkFormatAndOptimizeFor(
+ NChunkClient::EChunkFormat chunkFormat,
+ EOptimizeFor optimizeFor);
+void ValidateTableChunkFormatVersioned(
+ NChunkClient::EChunkFormat chunkFormat,
+ bool versioned);
+
+EOptimizeFor OptimizeForFromFormat(NChunkClient::EChunkFormat chunkFormat);
+NChunkClient::EChunkFormat DefaultFormatFromOptimizeFor(
+ EOptimizeFor optimizeFor,
+ bool versioned);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Mostly used in unittests and for debugging purposes.
+// Quite inefficient.
+TUnversionedOwningRow YsonToSchemafulRow(
+ const TString& yson,
+ const TTableSchema& tableSchema,
+ bool treatMissingAsNull,
+ NYson::EYsonType ysonType = NYson::EYsonType::MapFragment,
+ bool validateValues = false);
+TUnversionedOwningRow YsonToSchemalessRow(
+ const TString& yson);
+TVersionedRow YsonToVersionedRow(
+ const TRowBufferPtr& rowBuffer,
+ const TString& keyYson,
+ const TString& valueYson,
+ const std::vector<TTimestamp>& deleteTimestamps = {},
+ const std::vector<TTimestamp>& extraWriteTimestamps = {});
+TVersionedOwningRow YsonToVersionedRow(
+ const TString& keyYson,
+ const TString& valueYson,
+ const std::vector<TTimestamp>& deleteTimestamps = {},
+ const std::vector<TTimestamp>& extraWriteTimestamps = {});
+TUnversionedOwningRow YsonToKey(const TString& yson);
+TString KeyToYson(TUnversionedRow row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TUnversionedValueConversionTraits
+{
+ // These are conservative defaults.
+ static constexpr bool Scalar = false;
+ static constexpr bool Inline = false;
+};
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, std::nullopt_t, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, TGuid value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TGuid* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const TString& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TString* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, TStringBuf value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TStringBuf* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const char* value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(const char** value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, bool value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(bool* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const NYson::TYsonString& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(NYson::TYsonString* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const NYson::TYsonStringBuf& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(NYson::TYsonStringBuf* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, i64 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(i64* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, ui64 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(ui64* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, i32 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(i32* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, ui32 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(ui32* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, i16 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(i16* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, ui16 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(ui16* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, i8 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(i8* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, ui8 value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(ui8* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, double value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(double* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, TInstant value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TInstant* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, TDuration value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TDuration* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const NYTree::IMapNodePtr& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(NYTree::IMapNodePtr* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const NNet::TIP6Address& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(NNet::TIP6Address* value, TUnversionedValue unversionedValue);
+
+void ToUnversionedValue(TUnversionedValue* unversionedValue, const TError& value, const TRowBufferPtr& rowBuffer, int id = 0, EValueFlags flags = EValueFlags::None);
+void FromUnversionedValue(TError* value, TUnversionedValue unversionedValue);
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ T value,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None);
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void FromUnversionedValue(
+ T* value,
+ TUnversionedValue unversionedValue);
+
+template <class T>
+TUnversionedValue ToUnversionedValue(
+ T&& value,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None);
+template <class T>
+T FromUnversionedValue(TUnversionedValue unversionedValue);
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const T& value,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None,
+ typename std::enable_if<std::is_convertible<T*, ::google::protobuf::Message*>::value, void>::type* = nullptr);
+template <class T>
+void FromUnversionedValue(
+ T* value,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<T*, ::google::protobuf::Message*>::value, void>::type* = nullptr);
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const std::optional<T>& value,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None);
+template <class T>
+void FromUnversionedValue(
+ std::optional<T>* value,
+ TUnversionedValue unversionedValue);
+
+template <class T>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const std::vector<T>& values,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None);
+template <class T>
+void FromUnversionedValue(
+ std::vector<T>* values,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<T*, ::google::protobuf::Message*>::value, void>::type* = nullptr);
+template <class T>
+void FromUnversionedValue(
+ std::vector<T>* values,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<TUnversionedValueConversionTraits<T>::Scalar, void>::type* = nullptr);
+
+template <class TKey, class TValue>
+void ToUnversionedValue(
+ TUnversionedValue* unversionedValue,
+ const THashMap<TKey, TValue>& map,
+ const TRowBufferPtr& rowBuffer,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None);
+template <class TKey, class TValue>
+void FromUnversionedValue(
+ THashMap<TKey, TValue>* map,
+ TUnversionedValue unversionedValue,
+ typename std::enable_if<std::is_convertible<TValue*, ::google::protobuf::Message*>::value, void>::type* = nullptr);
+
+//! Values get sequential ids 0..N-1 (unless wrapped into TValueWithId).
+template <class... Ts>
+auto ToUnversionedValues(
+ const TRowBufferPtr& rowBuffer,
+ Ts&&... values)
+-> std::array<TUnversionedValue, sizeof...(Ts)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void ToVersionedValue(
+ TVersionedValue* versionedValue,
+ T&& value,
+ const TRowBufferPtr& rowBuffer,
+ NTransactionClient::TTimestamp timestamp,
+ int id,
+ EValueFlags flags = EValueFlags::None);
+template <class T>
+TVersionedValue ToVersionedValue(
+ T&& value,
+ const TRowBufferPtr& rowBuffer,
+ NTransactionClient::TTimestamp timestamp,
+ int id,
+ EValueFlags flags = EValueFlags::None);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Unpacks individual values in #row to respective #values.
+//! The number of values in #row must be greater than or equal to the number of #values.
+template <class... Ts>
+void FromUnversionedRow(
+ TUnversionedRow row,
+ Ts*... values);
+
+//! Same as above but returns a tuple instead of placing values into the arguments.
+template <class... Ts>
+std::tuple<Ts...> FromUnversionedRow(TUnversionedRow row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables annotating values with id and flags.
+template <class T>
+struct TAnnotatedValue
+{
+ //! For an implicitly-generated deduction guide.
+ TAnnotatedValue(
+ const T& value,
+ int id = 0,
+ EValueFlags flags = EValueFlags::None)
+ : Value(value)
+ , Id(id)
+ , Flags(flags)
+ { }
+
+ const T& Value;
+ int Id;
+ EValueFlags Flags;
+};
+
+//! Constructs an owning row from arbitrarily-typed values.
+//! Values get sequential ids 0..N-1 (unless wrapped into TValueWithId).
+template <class... Ts>
+TUnversionedOwningRow MakeUnversionedOwningRow(Ts&&... values);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnversionedRowsBuilder
+ : private TNonCopyable
+{
+public:
+ TUnversionedRowsBuilder();
+ explicit TUnversionedRowsBuilder(TRowBufferPtr rowBuffer);
+
+ void ReserveRows(int rowCount);
+
+ void AddRow(TUnversionedRow row);
+ void AddRow(TMutableUnversionedRow row);
+ void AddProtoRow(const TString& protoRow);
+
+ //! Values get sequential ids 0..N-1 (unless wrapped into TValueWithId).
+ template <class... Ts>
+ void AddRow(Ts&&... values);
+
+ TSharedRange<TUnversionedRow> Build();
+
+private:
+ const TRowBufferPtr RowBuffer_;
+
+ std::vector<TUnversionedRow> Rows_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper encapsulating Read/GetReadyEvent calls for a typical row batch reader.
+/*!
+ * Invokes |Read| method and checks the result for emptiness.
+ * If empty, waits for the ready event and loops.
+ * Returns either a non-empty batch or null (indicating end-of-stream).
+ *
+ * All additional parameters are forwarded to |Read| call.
+ */
+template <class TReader, class... TArgs>
+auto ReadRowBatch(const TIntrusivePtr<TReader>& reader, TArgs&&... args);
+
+//! A helper encapsulating Write/GetReadyEvent calls for a typical row batch writer.
+/*!
+ * Invokes |Write| method and checks the result.
+ * If false, waits for the ready event.
+ *
+ * All additional parameters are forwarded to |Write| call.
+ */
+template <class TWriter, class... TArgs>
+auto WriteRowBatch(const TIntrusivePtr<TWriter>& writer, TArgs&&... args);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void UnversionedValueToYson(TUnversionedValue unversionedValue, NYson::IYsonConsumer* consumer);
+void UnversionedValueToYson(TUnversionedValue unversionedValue, NYson::TCheckedInDebugYsonTokenWriter* tokenWriter);
+NYson::TYsonString UnversionedValueToYson(TUnversionedValue unversionedValue, bool enableRaw = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToAny(TRowBuffer* context, TUnversionedValue* result, TUnversionedValue* value, NYson::EYsonFormat format = NYson::EYsonFormat::Binary);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define HELPERS_INL_H_
+#include "helpers-inl.h"
+#undef HELPERS_INL_H_
diff --git a/yt/yt/client/table_client/key.cpp b/yt/yt/client/table_client/key.cpp
new file mode 100644
index 0000000000..6710dd1e81
--- /dev/null
+++ b/yt/yt/client/table_client/key.cpp
@@ -0,0 +1,190 @@
+#include "key.h"
+
+#include "serialize.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NTableClient {
+
+using namespace NLogging;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Used only for YT_LOG_FATAL below.
+static const TLogger Logger("TableClientKey");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKey::TKey(TUnversionedValueRange range)
+ : Elements_(range)
+{ }
+
+TKey TKey::FromRow(TUnversionedRow row, std::optional<int> length)
+{
+ if (!row) {
+ return TKey();
+ }
+
+ int keyLength = length.value_or(row.GetCount());
+ YT_VERIFY(keyLength <= static_cast<int>(row.GetCount()));
+
+ ValidateValueTypes(row.FirstNElements(keyLength));
+
+ return TKey(row.FirstNElements(keyLength));
+}
+
+TKey TKey::FromRowUnchecked(TUnversionedRow row, std::optional<int> length)
+{
+ if (!row) {
+ return TKey();
+ }
+
+ int keyLength = length.value_or(row.GetCount());
+ YT_VERIFY(keyLength <= static_cast<int>(row.GetCount()));
+
+#ifndef NDEBUG
+ try {
+ ValidateValueTypes(row.FirstNElements(keyLength));
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Unexpected exception while building key from row");
+ }
+#endif
+
+ return TKey(row.FirstNElements(keyLength));
+}
+
+TKey::operator bool() const
+{
+ return static_cast<bool>(Elements_);
+}
+
+TUnversionedOwningRow TKey::AsOwningRow() const
+{
+ return *this ? TUnversionedOwningRow(Elements()) : TUnversionedOwningRow();
+}
+
+const TUnversionedValue& TKey::operator[](int index) const
+{
+ return Elements_[index];
+}
+
+int TKey::GetLength() const
+{
+ return std::ssize(Elements_);
+}
+
+const TUnversionedValue* TKey::Begin() const
+{
+ return Elements_.begin();
+}
+
+const TUnversionedValue* TKey::End() const
+{
+ return Elements_.end();
+}
+
+TUnversionedValueRange TKey::Elements() const
+{
+ return Elements_;
+}
+
+void TKey::ValidateValueTypes(TUnversionedValueRange range)
+{
+ for (const auto& value : range) {
+ ValidateDataValueType(value.Type);
+ }
+}
+
+void TKey::Persist(const TPersistenceContext& context)
+{
+ if (context.IsSave()) {
+ auto representation = *this
+ ? SerializeToString(Elements())
+ : SerializedNullRow;
+ NYT::Save(context.SaveContext(), representation);
+ } else {
+ TUnversionedRow row;
+ Load(context.LoadContext(), row);
+ if (row) {
+ // Row lifetime is ensured by row buffer in load context.
+ Elements_ = row.Elements();
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TKey& lhs, const TKey& rhs)
+{
+ if (!lhs || !rhs) {
+ return static_cast<bool>(lhs) == static_cast<bool>(rhs);
+ }
+
+ return CompareValueRanges(lhs.Elements(), rhs.Elements()) == 0;
+}
+
+bool operator!=(const TKey& lhs, const TKey& rhs)
+{
+ return !(lhs == rhs);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TKey& key, TStringBuf /*format*/)
+{
+ if (key) {
+ builder->AppendFormat("[%v]", JoinToString(key.Begin(), key.End()));
+ } else {
+ builder->AppendString("#");
+ }
+}
+
+TString ToString(const TKey& key)
+{
+ return ToStringViaBuilder(key);
+}
+
+void Serialize(const TKey& key, IYsonConsumer* consumer)
+{
+ if (key) {
+ BuildYsonFluently(consumer)
+ .DoListFor(MakeRange(key.Begin(), key.End()), [&](TFluentList fluent, const TUnversionedValue& value) {
+ fluent
+ .Item()
+ .Value(value);
+ });
+ } else {
+ BuildYsonFluently(consumer)
+ .Entity();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedOwningRow LegacyKeyToKeyFriendlyOwningRow(TUnversionedRow row, int keyLength)
+{
+ if (!row) {
+ return TUnversionedOwningRow();
+ }
+
+ TUnversionedOwningRowBuilder builder;
+ for (int index = 0; index < keyLength; ++index) {
+ TUnversionedValue value;
+ if (index < static_cast<int>(row.GetCount())) {
+ value = row[index];
+ if (value.Type == EValueType::Min || value.Type == EValueType::Max) {
+ value.Type = EValueType::Null;
+ }
+ } else {
+ value = MakeUnversionedNullValue();
+ }
+ builder.AddValue(value);
+ }
+ auto result = builder.FinishRow();
+ YT_VERIFY(result.GetCount() == keyLength);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/key.h b/yt/yt/client/table_client/key.h
new file mode 100644
index 0000000000..58f6bfd6e0
--- /dev/null
+++ b/yt/yt/client/table_client/key.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "unversioned_row.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This class represents a (contextually) schemaful comparable row. It behaves
+//! similarly to TUnversionedRow.
+class TKey
+{
+public:
+ //! A special null key, use it instead of enclosing TKey in std::optional.
+ TKey() = default;
+ explicit TKey(TUnversionedValueRange range);
+
+ //! Returns true if key is non-null and false otherwise.
+ explicit operator bool() const;
+
+ //! Construct from a given row and possibly key length and validate that row does not contain
+ //! setntinels of types Min, Max and Bottom. If key length is not specified, row length will be used instead.
+ static TKey FromRow(TUnversionedRow row, std::optional<int> length = {});
+
+ //! Same as above, but does not check that row does not contain sentinels.
+ //! NB: in debug mode value type check is still performed, but results in YT_ABORT().
+ static TKey FromRowUnchecked(TUnversionedRow row, std::optional<int> length = {});
+
+ //! Performs a deep copy of underlying values into owning row.
+ TUnversionedOwningRow AsOwningRow() const;
+
+ const TUnversionedValue& operator[](int index) const;
+
+ int GetLength() const;
+
+ //! Helpers for printing and hashing.
+ const TUnversionedValue* Begin() const;
+ const TUnversionedValue* End() const;
+ TUnversionedValueRange Elements() const;
+
+ void Persist(const TPersistenceContext& context);
+
+private:
+ TUnversionedValueRange Elements_;
+
+ static void ValidateValueTypes(TUnversionedValueRange range);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TKey& lhs, const TKey& rhs);
+bool operator!=(const TKey& lhs, const TKey& rhs);
+
+void FormatValue(TStringBuilderBase* builder, const TKey& key, TStringBuf format);
+TString ToString(const TKey& key);
+
+void Serialize(const TKey& key, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Helper interop function for fixing master's last_key returned from BeginUpload() RPC call
+//! and some other places.
+//!
+//! This is closely related to TKey since original key returned by master may contain sentinels
+//! or be of wrong length, so it is not suitable for TKey construction. We fix it by
+//! replacing all sentinels to <null> and by padding row with nulls or shortening it so that
+//! row length is exactly keyLength.
+//!
+//! NB: this method is inefficient (as it deals with owning rows), do not use it on hot path.
+TUnversionedOwningRow LegacyKeyToKeyFriendlyOwningRow(TUnversionedRow row, int keyLength);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+//! A hasher for TKey.
+template <>
+struct THash<NYT::NTableClient::TKey>
+{
+ inline size_t operator()(const NYT::NTableClient::TKey& key) const
+ {
+ return NYT::NTableClient::TDefaultUnversionedValueRangeHash()(key.Elements());
+ }
+};
diff --git a/yt/yt/client/table_client/key_bound.cpp b/yt/yt/client/table_client/key_bound.cpp
new file mode 100644
index 0000000000..6428d87e4e
--- /dev/null
+++ b/yt/yt/client/table_client/key_bound.cpp
@@ -0,0 +1,530 @@
+#include "key_bound.h"
+
+#include "helpers.h"
+#include "row_buffer.h"
+#include "serialize.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Used only for YT_LOG_FATAL below.
+static const TLogger Logger("TableClientKey");
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::pair<bool, bool> RelationToIsUpperAndIsInclusive(TStringBuf relation)
+{
+ if (relation == "<=") {
+ return {/* isInclusive */ true, /* isUpper */ true};
+ } else if (relation == ">=") {
+ return {/* isInclusive */ true, /* isUpper */ false};
+ } else if (relation == "<") {
+ return {/* isInclusive */ false, /* isUpper */ true};
+ } else if (relation == ">") {
+ return {/* isInclusive */ false, /* isUpper */ false};
+ } else {
+ THROW_ERROR_EXCEPTION(
+ "Error parsing relation literal %Qv; one of \"<=\", \">=\", \"<\", \">\" expected",
+ relation);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::FromRow(const TRow& row, bool isInclusive, bool isUpper)
+{
+ YT_VERIFY(row);
+
+ ValidateValueTypes(row);
+ TKeyBound result;
+ result.Prefix = row;
+ result.IsInclusive = isInclusive;
+ result.IsUpper = isUpper;
+ return result;
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::FromRow(TRow&& row, bool isInclusive, bool isUpper)
+{
+ YT_VERIFY(row);
+
+ ValidateValueTypes(row);
+ TKeyBound result;
+ result.Prefix = row;
+ result.IsInclusive = isInclusive;
+ result.IsUpper = isUpper;
+ return result;
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::FromRowUnchecked(const TRow& row, bool isInclusive, bool isUpper)
+{
+ YT_VERIFY(row);
+
+#ifndef NDEBUG
+ try {
+ ValidateValueTypes(row);
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Unexpected exception while building key bound from row");
+ }
+#endif
+
+ TKeyBound result;
+ result.Prefix = row;
+ result.IsInclusive = isInclusive;
+ result.IsUpper = isUpper;
+ return result;
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::FromRowUnchecked(TRow&& row, bool isInclusive, bool isUpper)
+{
+ YT_VERIFY(row);
+
+#ifndef NDEBUG
+ try {
+ ValidateValueTypes(row);
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Unexpected exception while building key bound from row");
+ }
+#endif
+
+ TKeyBound result;
+ result.Prefix = row;
+ result.IsInclusive = isInclusive;
+ result.IsUpper = isUpper;
+ return result;
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::MakeUniversal(bool isUpper)
+{
+ return TKeyBoundImpl<TRow, TKeyBound>::FromRow(EmptyKey(), /* isInclusive */ true, isUpper);
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::MakeEmpty(bool isUpper)
+{
+ return TKeyBoundImpl<TRow, TKeyBound>::FromRow(EmptyKey(), /* isInclusive */ false, isUpper);
+}
+
+template <class TRow, class TKeyBound>
+void TKeyBoundImpl<TRow, TKeyBound>::ValidateValueTypes(const TRow& row)
+{
+ YT_VERIFY(row);
+
+ for (const auto& value : row) {
+ ValidateDataValueType(value.Type);
+ }
+}
+
+template <class TRow, class TKeyBound>
+void TKeyBoundImpl<TRow, TKeyBound>::FormatValue(TStringBuilderBase* builder) const
+{
+ if (!Prefix) {
+ builder->AppendChar('#');
+ } else {
+ builder->AppendChar(IsUpper ? '<' : '>');
+ if (IsInclusive) {
+ builder->AppendChar('=');
+ }
+ builder->AppendString(ToString(Prefix, /*valuesOnly*/ true));
+ }
+}
+
+template <class TRow, class TKeyBound>
+TKeyBoundImpl<TRow, TKeyBound>::operator bool() const
+{
+ return static_cast<bool>(Prefix);
+}
+
+template <class TRow, class TKeyBound>
+bool TKeyBoundImpl<TRow, TKeyBound>::IsUniversal() const
+{
+ return IsInclusive && Prefix && Prefix.GetCount() == 0;
+}
+
+template <class TRow, class TKeyBound>
+bool TKeyBoundImpl<TRow, TKeyBound>::IsEmpty() const
+{
+ return !IsInclusive && Prefix && Prefix.GetCount() == 0;
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::Invert() const
+{
+ YT_VERIFY(Prefix);
+ return TKeyBound::FromRowUnchecked(Prefix, !IsInclusive, !IsUpper);
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::ToggleInclusiveness() const
+{
+ YT_VERIFY(Prefix);
+ return TKeyBound::FromRowUnchecked(Prefix, !IsInclusive, IsUpper);
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::UpperCounterpart() const
+{
+ YT_VERIFY(Prefix);
+ return IsUpper ? *static_cast<const TKeyBound*>(this) : Invert();
+}
+
+template <class TRow, class TKeyBound>
+TKeyBound TKeyBoundImpl<TRow, TKeyBound>::LowerCounterpart() const
+{
+ YT_VERIFY(Prefix);
+ return IsUpper ? Invert() : *static_cast<const TKeyBound*>(this);
+}
+
+template <class TRow, class TKeyBound>
+TStringBuf TKeyBoundImpl<TRow, TKeyBound>::GetRelation() const
+{
+ if (IsUpper && IsInclusive) {
+ return "<=";
+ } else if (IsUpper && !IsInclusive) {
+ return "<";
+ } else if (!IsUpper && IsInclusive) {
+ return ">=";
+ } else if (!IsUpper && !IsInclusive) {
+ return ">";
+ } else {
+ Y_UNREACHABLE();
+ }
+}
+
+template <class TRow, class TKeyBound>
+void TKeyBoundImpl<TRow, TKeyBound>::Persist(const TPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Prefix);
+ Persist(context, IsInclusive);
+ Persist(context, IsUpper);
+}
+
+template <class TRow, class TKeyBound>
+void TKeyBoundImpl<TRow, TKeyBound>::Serialize(IYsonConsumer* consumer) const
+{
+ if (*this) {
+ BuildYsonFluently(consumer)
+ .BeginList()
+ .Item().Value(GetRelation())
+ .Item().Value(Prefix)
+ .EndList();
+ } else {
+ BuildYsonFluently(consumer)
+ .Entity();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template class TKeyBoundImpl<TUnversionedRow, TKeyBound>;
+template class TKeyBoundImpl<TUnversionedOwningRow, TOwningKeyBound>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOwningKeyBound::operator TKeyBound() const
+{
+ TKeyBound result;
+ result.Prefix = Prefix;
+ result.IsInclusive = IsInclusive;
+ result.IsUpper = IsUpper;
+ return result;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TOwningKeyBound& keyBound, TStringBuf /*format*/)
+{
+ return keyBound.FormatValue(builder);
+}
+
+TString ToString(const TOwningKeyBound& keyBound)
+{
+ return ToStringViaBuilder(keyBound);
+}
+
+void PrintTo(const TOwningKeyBound& keyBound, ::std::ostream* os)
+{
+ *os << ToString(keyBound);
+}
+
+void Serialize(const TKeyBound& keyBound, IYsonConsumer* consumer)
+{
+ keyBound.Serialize(consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOwningKeyBound TKeyBound::ToOwning() const
+{
+ TOwningKeyBound result;
+ result.Prefix = TUnversionedOwningRow(Prefix);
+ result.IsInclusive = IsInclusive;
+ result.IsUpper = IsUpper;
+ return result;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TKeyBound& keyBound, TStringBuf /*format*/)
+{
+ return keyBound.FormatValue(builder);
+}
+
+TString ToString(const TKeyBound& keyBound)
+{
+ return ToStringViaBuilder(keyBound);
+}
+
+void PrintTo(const TKeyBound& keyBound, ::std::ostream* os)
+{
+ *os << ToString(keyBound);
+}
+
+void Serialize(const TOwningKeyBound& keyBound, IYsonConsumer* consumer)
+{
+ keyBound.Serialize(consumer);
+}
+
+void Deserialize(TOwningKeyBound& keyBound, const NYTree::INodePtr& node)
+{
+ if (node->GetType() == ENodeType::Entity) {
+ keyBound = TOwningKeyBound();
+ } else if (node->GetType() == ENodeType::List) {
+ auto listNode = node->AsList();
+ if (listNode->GetChildCount() != 2) {
+ THROW_ERROR_EXCEPTION(
+ "Error parsing key bound: list node must have exactly two elements, "
+ "first of which is a relation string literal, and second is a row; "
+ "%v elements found",
+ listNode->GetChildCount());
+ }
+ auto relationNode = listNode->GetChildOrThrow(0);
+ auto rowNode = listNode->GetChildOrThrow(1);
+
+ if (relationNode->GetType() != ENodeType::String) {
+ THROW_ERROR_EXCEPTION(
+ "Error parsing key bound: first element must be a string node; actual %Qv node",
+ relationNode->GetType());
+ }
+
+ auto relation = relationNode->GetValue<TString>();
+ auto [isInclusive, isUpper] = NDetail::RelationToIsUpperAndIsInclusive(relation);
+
+ TUnversionedOwningRow row;
+ Deserialize(row, rowNode);
+
+ keyBound = TOwningKeyBound::FromRow(row, isInclusive, isUpper);
+ } else {
+ THROW_ERROR_EXCEPTION("Error parsing key bound: expected %Qlv node, actual %Qlv node",
+ NYTree::ENodeType::List,
+ node->GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator ==(const TKeyBound& lhs, const TKeyBound& rhs)
+{
+ return
+ lhs.Prefix == rhs.Prefix &&
+ lhs.IsInclusive == rhs.IsInclusive &&
+ lhs.IsUpper == rhs.IsUpper;
+}
+
+bool operator ==(const TOwningKeyBound& lhs, const TOwningKeyBound& rhs)
+{
+ return static_cast<TKeyBound>(lhs) == static_cast<TKeyBound>(rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Common implementation for owning case and non-owning case over row buffer.
+// Returns pair {prefixKeyLength, inclusive flag} describing how to transform
+// legacy key into key bound.
+std::pair<int, bool> GetBoundPrefixAndInclusiveness(TUnversionedRow row, bool isUpper, int keyLength)
+{
+ YT_VERIFY(row);
+
+ // Flag indicating that row starts with #keyLength non-sentinel values followed by at least one arbitrary value.
+ bool isLongRow = false;
+
+ // If row contains at least one sentinel on first #keyLength positions, type of leftmost of them.
+ std::optional<EValueType> leftmostSentinelType;
+
+ // Length of the longest prefix of row which is free of sentinels. Prefix length is limited by #keyLength.
+ int prefixLength = 0;
+ for (int index = 0; index < static_cast<int>(row.GetCount()) && index <= keyLength; ++index) {
+ if (index == keyLength) {
+ isLongRow = true;
+ break;
+ }
+ if (row[index].Type != EValueType::Min && row[index].Type != EValueType::Max) {
+ ++prefixLength;
+ } else {
+ leftmostSentinelType = row[index].Type;
+ break;
+ }
+ }
+
+ // When dealing with legacy rows, upper limit is always exclusive and lower limit is always inclusive.
+ // We will call this kind of inclusiveness standard. This implies following cases for key bounds.
+ //
+ // (A) If row is long, upper limit will be inclusive and lower limit will be exclusive, i.e. inclusiveness is toggled.
+ // (B) Otherwise, if row has exactly length of #keyLength and does not contain sentinels, inclusiveness is standard.
+ //
+ // Suppose none of (A) and (B) happened. We know that prefix is strictly shorter than #keyLength. If may or may not be
+ // followed by a sentinel. Actually there is no difference if prefix is followed by Min or if it is not followed by sentinel.
+ // To prove this fact, consider row R = prefix + [Min], length(R) < #keyLength and key K, length(K) == #keyLength.
+ // It is easy to see that R is compared to K in exactly the same way as prefix is compared to K; this case
+ // corresponds to a key bound with standard inclusiveness.
+ //
+ // Similar argument shows that if prefix is followed by Max, key bound inclusiveness should be toggled.
+ //
+ // So, we have only two more cases:
+ //
+ // (C) Otherwise, if prefix is followed by Min or no sentinel, inclusiveness is standard.
+ // (D) Otherwise (prefix is followed by Max), inclusiveness is toggled.
+
+ // Cases (A) and (D).
+ bool toggleInclusiveness = isLongRow || leftmostSentinelType == EValueType::Max;
+
+ bool isInclusive = (isUpper && toggleInclusiveness) || (!isUpper && !toggleInclusiveness);
+
+ return {prefixLength, isInclusive};
+}
+
+TOwningKeyBound KeyBoundFromLegacyRow(TUnversionedRow row, bool isUpper, int keyLength)
+{
+ if (!row) {
+ return TOwningKeyBound::MakeUniversal(isUpper);
+ }
+
+ auto [prefixLength, isInclusive] = GetBoundPrefixAndInclusiveness(row, isUpper, keyLength);
+ return TOwningKeyBound::FromRow(
+ TUnversionedOwningRow(row.FirstNElements(prefixLength)),
+ isInclusive,
+ isUpper);
+}
+
+TKeyBound KeyBoundFromLegacyRow(TUnversionedRow row, bool isUpper, int keyLength, const TRowBufferPtr& rowBuffer)
+{
+ if (!row) {
+ return TKeyBound::MakeUniversal(isUpper);
+ }
+
+ auto [prefixLength, isInclusive] = GetBoundPrefixAndInclusiveness(row, isUpper, keyLength);
+ YT_VERIFY(prefixLength <= static_cast<int>(row.GetCount()));
+ row = rowBuffer->CaptureRow(MakeRange(row.Begin(), prefixLength));
+
+ return TKeyBound::FromRow(
+ row,
+ isInclusive,
+ isUpper);
+}
+
+TUnversionedOwningRow KeyBoundToLegacyRow(TKeyBound keyBound)
+{
+ if (!keyBound) {
+ return TUnversionedOwningRow();
+ }
+
+ TUnversionedOwningRowBuilder builder;
+ for (const auto& value : keyBound.Prefix) {
+ builder.AddValue(value);
+ }
+ auto shouldAddMax = (keyBound.IsUpper && keyBound.IsInclusive) || (!keyBound.IsUpper && !keyBound.IsInclusive);
+ if (shouldAddMax) {
+ builder.AddValue(MakeUnversionedSentinelValue(EValueType::Max));
+ }
+ return builder.FinishRow();
+}
+
+TUnversionedRow KeyBoundToLegacyRow(TKeyBound keyBound, const TRowBufferPtr& rowBuffer)
+{
+ if (!keyBound) {
+ return TUnversionedRow();
+ }
+
+ auto shouldAddMax = (keyBound.IsUpper && keyBound.IsInclusive) || (!keyBound.IsUpper && !keyBound.IsInclusive);
+
+ auto row = rowBuffer->AllocateUnversioned(keyBound.Prefix.GetCount() + (shouldAddMax ? 1 : 0));
+ memcpy(row.Begin(), keyBound.Prefix.Begin(), sizeof(TUnversionedValue) * keyBound.Prefix.GetCount());
+ if (shouldAddMax) {
+ row[keyBound.Prefix.GetCount()] = MakeUnversionedSentinelValue(EValueType::Max, keyBound.Prefix.GetCount());
+ }
+ for (auto& value : row) {
+ rowBuffer->CaptureValue(&value);
+ }
+
+ return row;
+}
+
+TKeyBound ShortenKeyBound(TKeyBound keyBound, int length, const TRowBufferPtr& rowBuffer)
+{
+ if (!keyBound) {
+ return TKeyBound();
+ }
+
+ if (static_cast<int>(keyBound.Prefix.GetCount()) <= length) {
+ // No need to change anything.
+ return keyBound;
+ }
+
+ // If we do perform shortening, resulting key bound is going to be inclusive despite the original inclusiveness.
+
+ auto result = TKeyBound::FromRowUnchecked(
+ rowBuffer->CaptureRow(MakeRange(keyBound.Prefix.Begin(), length)),
+ /* isInclusive */ true,
+ keyBound.IsUpper);
+
+ return result;
+}
+
+//! Owning version of #ShortenKeyBound.
+TOwningKeyBound ShortenKeyBound(TOwningKeyBound keyBound, int length)
+{
+ if (keyBound.Prefix.GetCount() <= length) {
+ // No need to change anything.
+ return keyBound;
+ }
+
+ // If we do perform shortening, resulting key bound is going to be inclusive despite the original inclusiveness.
+
+ return TOwningKeyBound::FromRowUnchecked(
+ TUnversionedOwningRow(keyBound.Prefix.FirstNElements(length)),
+ /* isInclusive */ true,
+ keyBound.IsUpper);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+size_t THash<NYT::NTableClient::TKeyBound>::operator()(const NYT::NTableClient::TKeyBound& keyBound) const
+{
+ using NYT::HashCombine;
+
+ size_t result = 0;
+ HashCombine(result, keyBound.Prefix);
+ HashCombine(result, keyBound.IsInclusive);
+ HashCombine(result, keyBound.IsUpper);
+
+ return result;
+}
diff --git a/yt/yt/client/table_client/key_bound.h b/yt/yt/client/table_client/key_bound.h
new file mode 100644
index 0000000000..cad65bc5dd
--- /dev/null
+++ b/yt/yt/client/table_client/key_bound.h
@@ -0,0 +1,190 @@
+#pragma once
+
+#include "unversioned_row.h"
+#include "key.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This class represents a (contextually) schemaful key bound. It defines
+//! an open or closed ray in a space of all possible keys.
+//! This is a CRTP base for common boilerplate code for owning and non-owning versions.
+template <class TRow, class TKeyBound>
+class TKeyBoundImpl
+{
+public:
+ // If #Prefix is a null row, key bound has a special meaning of being null bound.
+ // It is different from universal or empty bound; use it as a replacement for
+ // std::nullopt.
+ TRow Prefix;
+ bool IsInclusive = false;
+ bool IsUpper = false;
+
+ //! Construct from a given row and validate that row does not contain
+ //! setntinels of types Min, Max and Bottom.
+ static TKeyBound FromRow(const TRow& row, bool isInclusive, bool isUpper);
+
+ //! Same as previous but for rvalue refs.
+ static TKeyBound FromRow(TRow&& row, bool isInclusive, bool isUpper);
+
+ //! Construct from a given row without checking presence of types Min, Max and Bottom.
+ //! NB: in debug mode value type check is still performed, but results in YT_ABORT().
+ static TKeyBound FromRowUnchecked(const TRow& row, bool isInclusive, bool isUpper);
+
+ //! Same as previous but for rvalue refs.
+ static TKeyBound FromRowUnchecked(TRow&& row, bool isInclusive, bool isUpper);
+
+ // In order to reduce amount of repetitive constructions like
+ //
+ // TKeyBound::FromRow(row, /* isInclusive */ true, /* isUpper */ false)
+ //
+ // with two flags known in compile-time, we introduce a helper class
+ // allowing you to build key bounds as follows:
+ //
+ // TKeyBound::FromRow() >= row
+ //
+ // These helpers also support unchecked variant of static constructors above.
+ // They work both for owning and non-owning kinds of key bound.
+ #define XX(suffix) \
+ struct TBuilder ## suffix { \
+ TKeyBound operator > (const TRow& row) { return TKeyBound::FromRow ## suffix(row, false, false); } \
+ TKeyBound operator >=(const TRow& row) { return TKeyBound::FromRow ## suffix(row, true , false); } \
+ TKeyBound operator < (const TRow& row) { return TKeyBound::FromRow ## suffix(row, false, true ); } \
+ TKeyBound operator <=(const TRow& row) { return TKeyBound::FromRow ## suffix(row, true , true ); } \
+ TKeyBound operator > (TRow&& row) { return TKeyBound::FromRow ## suffix(row, false, false); } \
+ TKeyBound operator >=(TRow&& row) { return TKeyBound::FromRow ## suffix(row, true , false); } \
+ TKeyBound operator < (TRow&& row) { return TKeyBound::FromRow ## suffix(row, false, true ); } \
+ TKeyBound operator <=(TRow&& row) { return TKeyBound::FromRow ## suffix(row, true , true ); } \
+ }; \
+ TBuilder ## suffix static FromRow ## suffix() { return TBuilder##suffix(); }
+
+ XX()
+ XX(Unchecked)
+
+ #undef XX
+
+ //! Return a key bound that allows any key.
+ static TKeyBound MakeUniversal(bool isUpper);
+
+ //! Return a key bound that does not allow any key.
+ static TKeyBound MakeEmpty(bool isUpper);
+
+ void FormatValue(TStringBuilderBase* builder) const;
+
+ explicit operator bool() const;
+
+ //! Test if this key bound allows any key.
+ bool IsUniversal() const;
+
+ //! Test if this key bound allows no keys.
+ bool IsEmpty() const;
+
+ //! Return key bound which is complementary to current.
+ TKeyBound Invert() const;
+
+ //! Return key bound with same prefix and direction but toggled inclusiveness.
+ TKeyBound ToggleInclusiveness() const;
+
+ //! Return key bound that is upper among {*this, this->Invert()}.
+ TKeyBound UpperCounterpart() const;
+ //! Return key bound that is lower among {*this, this->Invert()}.
+ TKeyBound LowerCounterpart() const;
+
+ //! Returns string among {">=", ">", "<=", "<"} defining this key bound kind.
+ TStringBuf GetRelation() const;
+
+ void Persist(const TPersistenceContext& context);
+
+ void Serialize(NYson::IYsonConsumer* consumer) const;
+
+private:
+ static void ValidateValueTypes(const TRow& row);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TKeyBound
+ : public NDetail::TKeyBoundImpl<TUnversionedRow, TKeyBound>
+{
+public:
+ TOwningKeyBound ToOwning() const;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TKeyBound& keyBound, TStringBuf format);
+TString ToString(const TKeyBound& keyBound);
+
+void Serialize(const TKeyBound& keyBound, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOwningKeyBound
+ : public NDetail::TKeyBoundImpl<TUnversionedOwningRow, TOwningKeyBound>
+{
+public:
+ operator TKeyBound() const;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TOwningKeyBound& keyBound, TStringBuf format);
+TString ToString(const TOwningKeyBound& keyBound);
+
+void Serialize(const TOwningKeyBound& keyBound, NYson::IYsonConsumer* consumer);
+void Deserialize(TOwningKeyBound& keyBound, const NYTree::INodePtr& node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator ==(const TKeyBound& lhs, const TKeyBound& rhs);
+bool operator ==(const TOwningKeyBound& lhs, const TOwningKeyBound& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Interop functions.
+
+//! Returns significant prefix length and inclusiveness.
+std::pair<int, bool> GetBoundPrefixAndInclusiveness(TUnversionedRow row, bool isUpper, int keyLength);
+
+//! Convert legacy key bound expressed as a row possibly containing Min/Max to owning key bound.
+//! NB: key length is needed to properly distinguish if K + [min] is an inclusive K or exclusive K.
+TOwningKeyBound KeyBoundFromLegacyRow(TUnversionedRow row, bool isUpper, int keyLength);
+
+//! Same as previous, but non-owning variant over row buffer.
+TKeyBound KeyBoundFromLegacyRow(TUnversionedRow row, bool isUpper, int keyLength, const TRowBufferPtr& rowBuffer);
+
+//! Convert key bound to legacy key bound.
+TUnversionedOwningRow KeyBoundToLegacyRow(TKeyBound keyBound);
+
+//! Same as previous, but non-owning variant over row buffer.
+TUnversionedRow KeyBoundToLegacyRow(TKeyBound keyBound, const TRowBufferPtr& rowBuffer);
+
+//! Build the most accurate key bound of length #length corresponding to the ray containing
+//! ray corresponding to #keyBound.
+TKeyBound ShortenKeyBound(TKeyBound keyBound, int length, const TRowBufferPtr& rowBuffer);
+
+//! Owning version of #ShortenKeyBound.
+TOwningKeyBound ShortenKeyBound(TOwningKeyBound keyBound, int length);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Debug printers for Gtest unittests.
+
+void PrintTo(const TKeyBound& key, ::std::ostream* os);
+void PrintTo(const TOwningKeyBound& key, ::std::ostream* os);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+//! A hasher for TKeyBound.
+template <>
+struct THash<NYT::NTableClient::TKeyBound>
+{
+ size_t operator()(const NYT::NTableClient::TKeyBound& keyBound) const;
+};
diff --git a/yt/yt/client/table_client/key_bound_compressor.cpp b/yt/yt/client/table_client/key_bound_compressor.cpp
new file mode 100644
index 0000000000..35b87c7190
--- /dev/null
+++ b/yt/yt/client/table_client/key_bound_compressor.cpp
@@ -0,0 +1,164 @@
+#include "key_bound_compressor.h"
+
+#include <yt/yt/core/misc/collection_helpers.h>
+
+#include <yt/yt/core/logging/fluent_log.h>
+
+namespace NYT::NTableClient {
+
+using namespace NLogging;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyBoundCompressor::TKeyBoundCompressor(const TComparator& comparator)
+ : Comparator_(comparator)
+{ }
+
+void TKeyBoundCompressor::Add(TKeyBound keyBound)
+{
+ YT_VERIFY(keyBound);
+
+ AddedKeyBounds_.insert(keyBound);
+ AddedKeyBounds_.insert(keyBound.Invert());
+}
+
+void TKeyBoundCompressor::InitializeMapping()
+{
+ YT_VERIFY(!MappingInitialized_);
+ MappingInitialized_ = true;
+
+ SortedKeyBounds_.reserve(AddedKeyBounds_.size());
+ SortedKeyBounds_.insert(SortedKeyBounds_.end(), AddedKeyBounds_.begin(), AddedKeyBounds_.end());
+ std::sort(
+ SortedKeyBounds_.begin(),
+ SortedKeyBounds_.end(),
+ [&] (const TKeyBound& lhs, const TKeyBound& rhs) {
+ return Comparator_.CompareKeyBounds(lhs, rhs) < 0;
+ });
+
+ // Prepare images.
+ for (const auto& keyBound : AddedKeyBounds_) {
+ Mapping_[keyBound];
+ }
+
+ // Prepare component-wise image prefixes.
+ ComponentWisePrefixes_.resize(SortedKeyBounds_.size());
+ for (ssize_t index = 0; index < ssize(SortedKeyBounds_); ++index) {
+ ComponentWisePrefixes_[index] = RowBuffer_->AllocateUnversioned(SortedKeyBounds_[index].Prefix.GetCount());
+ }
+
+ // First, calculate global images.
+ // Recall that we are identifying (assuming comparator of length 2):
+ // * >=[foo, 2], >[foo, 2], <=[foo, 2], <[foo, 2]
+ // * >=[foo], <[foo]
+ // * >[foo], <=[foo]
+ // * but not >=[foo] with [foo].
+ for (
+ ssize_t beginIndex = 0, endIndex = 0, currentImage = 0;
+ beginIndex < ssize(SortedKeyBounds_);
+ beginIndex = endIndex, ++currentImage)
+ {
+ auto beginBound = SortedKeyBounds_[beginIndex];
+
+ while (true) {
+ if (endIndex >= ssize(SortedKeyBounds_)) {
+ break;
+ }
+ auto endBound = SortedKeyBounds_[endIndex];
+ if (beginBound != endBound &&
+ beginBound.Invert() != endBound &&
+ (beginBound.Prefix != endBound.Prefix ||
+ static_cast<int>(beginBound.Prefix.GetCount()) != Comparator_.GetLength()))
+ {
+ break;
+ }
+ Mapping_[endBound].Global = currentImage;
+ ++endIndex;
+ }
+ }
+
+ // Second, calculate component-wise images.
+ CalculateComponentWise(/*fromIndex*/ 0, /*toIndex*/ SortedKeyBounds_.size(), /*componentIndex*/ 0);
+ for (ssize_t index = 0; index < ssize(SortedKeyBounds_); ++index) {
+ const auto& keyBound = SortedKeyBounds_[index];
+ auto& componentWiseImage = Mapping_[keyBound].ComponentWise;
+ componentWiseImage.Prefix = ComponentWisePrefixes_[index];
+ componentWiseImage.IsInclusive = SortedKeyBounds_[index].IsInclusive;
+ componentWiseImage.IsUpper = SortedKeyBounds_[index].IsUpper;
+ }
+}
+
+void TKeyBoundCompressor::CalculateComponentWise(ssize_t fromIndex, ssize_t toIndex, ssize_t componentIndex)
+{
+ if (fromIndex == toIndex || componentIndex == Comparator_.GetLength()) {
+ return;
+ }
+ for (
+ ssize_t beginIndex = fromIndex, endIndex = fromIndex, currentImage = 0;
+ beginIndex < toIndex;
+ beginIndex = endIndex, ++currentImage)
+ {
+ // Skip a bunch of key bounds that are too short to have currently processed component.
+ while (true) {
+ if (beginIndex >= toIndex) {
+ break;
+ }
+ auto beginBound = SortedKeyBounds_[beginIndex];
+ if (beginBound.Prefix.GetCount() > componentIndex) {
+ break;
+ }
+ ++beginIndex;
+ }
+ if (beginIndex >= toIndex) {
+ break;
+ }
+
+ // Extract a contiguous segment of key bounds sharing the same value in current component.
+ endIndex = beginIndex;
+ auto beginBound = SortedKeyBounds_[beginIndex];
+ while (true) {
+ if (endIndex >= toIndex) {
+ break;
+ }
+ auto endBound = SortedKeyBounds_[endIndex];
+ if (endBound.Prefix.GetCount() <= componentIndex) {
+ break;
+ }
+ if (endBound.Prefix[componentIndex] != beginBound.Prefix[componentIndex]) {
+ break;
+ }
+ ComponentWisePrefixes_[endIndex][componentIndex] = MakeUnversionedInt64Value(currentImage, componentIndex);
+ ++endIndex;
+ }
+
+ CalculateComponentWise(beginIndex, endIndex, componentIndex + 1);
+ }
+}
+
+TKeyBoundCompressor::TImage TKeyBoundCompressor::GetImage(TKeyBound keyBound) const
+{
+ YT_VERIFY(MappingInitialized_);
+ return GetOrCrash(Mapping_, keyBound);
+}
+
+void TKeyBoundCompressor::Dump(const TLogger& logger)
+{
+ YT_VERIFY(MappingInitialized_);
+
+ TStructuredLogBatcher batcher(logger);
+
+ for (const auto& keyBound : SortedKeyBounds_) {
+ auto image = GetImage(keyBound);
+ batcher.AddItemFluently()
+ .BeginList()
+ .Item().Value(keyBound)
+ .Item().Value(image.ComponentWise)
+ .Item().Value(image.Global)
+ .EndList();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/key_bound_compressor.h b/yt/yt/client/table_client/key_bound_compressor.h
new file mode 100644
index 0000000000..98b0bffb02
--- /dev/null
+++ b/yt/yt/client/table_client/key_bound_compressor.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "key_bound.h"
+#include "comparator.h"
+#include "row_buffer.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Helper for isomorphic "compression" of key bounds for easier perception.
+//! Example:
+//!
+//! Preimage ComponentWise Global
+//! >= [] >= [] 0
+//! >= [bar] >= [0] 1
+//! >= [bar, 42] >= [0, 0] 2
+//! <= [bar, 57] <= [0, 1] 3
+//! <= [bar] <= [0] 4
+//! < [foo] < [1] 5
+//! >= [foo] >= [1] 5
+//! > [foo; 30] > [1, 0] 6
+//! < [foo; 99] < [1, 1] 7
+//! <= [foo; 99] <= [1, 1] 7
+//! > [foo; 99] > [1, 1] 7
+//! < [qux; 18] < [2, 0] 8
+//! < [zzz] < [3] 9
+//! <= [] < [] 10
+//! > [] > [] 10
+//!
+//! Note that ComponentWise is a TKeyBound -> TKeyBound mapping,
+//! while Global is a TKeyBound -> int mapping which identifies
+//! complementary key bounds and also full-length key bounds with
+//! coinciding prefixes.
+class TKeyBoundCompressor
+{
+public:
+ explicit TKeyBoundCompressor(const TComparator& comparator);
+
+ void Add(TKeyBound keyBound);
+
+ void InitializeMapping();
+
+ void Dump(const NLogging::TLogger& logger);
+
+ struct TImage
+ {
+ TKeyBound ComponentWise;
+ int Global = -1;
+ };
+
+ TImage GetImage(TKeyBound keyBound) const;
+
+private:
+ TComparator Comparator_;
+
+ THashSet<TKeyBound> AddedKeyBounds_;
+
+ std::vector<TKeyBound> SortedKeyBounds_;
+ std::vector<TMutableUnversionedRow> ComponentWisePrefixes_;
+
+ THashMap<TKeyBound, TImage> Mapping_;
+ bool MappingInitialized_ = false;
+
+ TRowBufferPtr RowBuffer_ = New<TRowBuffer>();
+
+ //! Recursive method for calculating component-wise images.
+ void CalculateComponentWise(ssize_t fromIndex, ssize_t toIndex, ssize_t componentIndex);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/logical_type-inl.h b/yt/yt/client/table_client/logical_type-inl.h
new file mode 100644
index 0000000000..28bd5b17a6
--- /dev/null
+++ b/yt/yt/client/table_client/logical_type-inl.h
@@ -0,0 +1,145 @@
+#ifndef LOGICAL_TYPE_INL_H_
+#error "Direct inclusion of this file is not allowed, include logical_type.h"
+// For the sake of sane code completion.
+#include "logical_type.h"
+#endif
+#undef LOGICAL_TYPE_INL_H_
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ELogicalMetatype TLogicalType::GetMetatype() const
+{
+ return Metatype_;
+}
+
+const TSimpleLogicalType& TLogicalType::UncheckedAsSimpleTypeRef() const
+{
+ return static_cast<const TSimpleLogicalType&>(*this);
+}
+
+const TDecimalLogicalType& TLogicalType::UncheckedAsDecimalTypeRef() const
+{
+ return static_cast<const TDecimalLogicalType&>(*this);
+}
+
+const TOptionalLogicalType& TLogicalType::UncheckedAsOptionalTypeRef() const
+{
+ return static_cast<const TOptionalLogicalType&>(*this);
+}
+
+const TListLogicalType& TLogicalType::UncheckedAsListTypeRef() const
+{
+ return static_cast<const TListLogicalType&>(*this);
+}
+
+const TStructLogicalType& TLogicalType::UncheckedAsStructTypeRef() const
+{
+ return static_cast<const TStructLogicalType&>(*this);
+}
+
+const TTupleLogicalType& TLogicalType::UncheckedAsTupleTypeRef() const
+{
+ return static_cast<const TTupleLogicalType&>(*this);
+}
+
+const TVariantTupleLogicalType& TLogicalType::UncheckedAsVariantTupleTypeRef() const
+{
+ return static_cast<const TVariantTupleLogicalType&>(*this);
+}
+
+const TVariantStructLogicalType& TLogicalType::UncheckedAsVariantStructTypeRef() const
+{
+ return static_cast<const TVariantStructLogicalType&>(*this);
+}
+
+const TDictLogicalType& TLogicalType::UncheckedAsDictTypeRef() const
+{
+ return static_cast<const TDictLogicalType&>(*this);
+}
+
+const TTaggedLogicalType& TLogicalType::UncheckedAsTaggedTypeRef() const
+{
+ return static_cast<const TTaggedLogicalType&>(*this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int TDecimalLogicalType::GetPrecision() const
+{
+ return Precision_;
+}
+
+int TDecimalLogicalType::GetScale() const
+{
+ return Scale_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLogicalTypePtr& TOptionalLogicalType::GetElement() const
+{
+ return Element_;
+}
+
+bool TOptionalLogicalType::IsElementNullable() const
+{
+ return ElementIsNullable_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ESimpleLogicalValueType TSimpleLogicalType::GetElement() const
+{
+ return Element_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLogicalTypePtr& TListLogicalType::GetElement() const
+{
+ return Element_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::vector<TStructField>& TStructLogicalTypeBase::GetFields() const
+{
+ return Fields_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::vector<TLogicalTypePtr>& TTupleLogicalTypeBase::GetElements() const
+{
+ return Elements_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLogicalTypePtr& TDictLogicalType::GetKey() const
+{
+ return Key_;
+}
+
+const TLogicalTypePtr& TDictLogicalType::GetValue() const
+{
+ return Value_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& TTaggedLogicalType::GetTag() const
+{
+ return Tag_;
+}
+
+const TLogicalTypePtr& TTaggedLogicalType::GetElement() const
+{
+ return Element_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient \ No newline at end of file
diff --git a/yt/yt/client/table_client/logical_type.cpp b/yt/yt/client/table_client/logical_type.cpp
new file mode 100644
index 0000000000..8d7687c755
--- /dev/null
+++ b/yt/yt/client/table_client/logical_type.cpp
@@ -0,0 +1,2214 @@
+#include "logical_type.h"
+#include "schema.h"
+#include "yt/yt/client/table_client/row_base.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/node.h>
+
+#include <util/charset/utf8.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWalkContext
+{
+ std::vector<TComplexTypeFieldDescriptor> Stack;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void WalkImpl(
+ TWalkContext* walkContext,
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::function<void(const TWalkContext&, const TComplexTypeFieldDescriptor&)>& onElement)
+{
+ onElement(*walkContext, descriptor);
+ walkContext->Stack.push_back(descriptor);
+ auto g = Finally([&]() {
+ walkContext->Stack.pop_back();
+ });
+ const auto metatype = descriptor.GetType()->GetMetatype();
+ switch (metatype) {
+ case ELogicalMetatype::Simple:
+ case ELogicalMetatype::Decimal:
+ return;
+ case ELogicalMetatype::Optional:
+ WalkImpl(walkContext, descriptor.OptionalElement(), onElement);
+ return;
+ case ELogicalMetatype::List:
+ WalkImpl(walkContext, descriptor.ListElement(), onElement);
+ return;
+ case ELogicalMetatype::Struct:
+ for (size_t i = 0; i < descriptor.GetType()->AsStructTypeRef().GetFields().size(); ++i) {
+ WalkImpl(walkContext, descriptor.StructField(i), onElement);
+ }
+ return;
+ case ELogicalMetatype::Tuple:
+ for (size_t i = 0; i < descriptor.GetType()->AsTupleTypeRef().GetElements().size(); ++i) {
+ WalkImpl(walkContext, descriptor.TupleElement(i), onElement);
+ }
+ return;
+ case ELogicalMetatype::VariantStruct: {
+ for (size_t i = 0; i < descriptor.GetType()->AsVariantStructTypeRef().GetFields().size(); ++i) {
+ WalkImpl(walkContext, descriptor.VariantStructField(i), onElement);
+ }
+ return;
+ }
+ case ELogicalMetatype::VariantTuple:
+ for (size_t i = 0; i < descriptor.GetType()->AsVariantTupleTypeRef().GetElements().size(); ++i) {
+ WalkImpl(walkContext, descriptor.VariantTupleElement(i), onElement);
+ }
+ return;
+ case ELogicalMetatype::Dict:
+ WalkImpl(walkContext, descriptor.DictKey(), onElement);
+ WalkImpl(walkContext, descriptor.DictValue(), onElement);
+ return;
+ case ELogicalMetatype::Tagged:
+ WalkImpl(walkContext, descriptor.TaggedElement(), onElement);
+ return;
+ }
+ YT_ABORT();
+}
+
+static void Walk(
+ const TComplexTypeFieldDescriptor& descriptor,
+ const std::function<void(const TWalkContext&, const TComplexTypeFieldDescriptor&)>& onElement)
+{
+ TWalkContext walkContext;
+ WalkImpl(&walkContext, descriptor, onElement);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T, typename F>
+const T& VerifiedCast(const F& from)
+{
+ const T* to = dynamic_cast<const T*>(&from);
+ YT_VERIFY(to != nullptr);
+ return *to;
+}
+
+TLogicalType::TLogicalType(ELogicalMetatype type)
+ : Metatype_(type)
+{}
+
+const TSimpleLogicalType& TLogicalType::AsSimpleTypeRef() const
+{
+ return VerifiedCast<TSimpleLogicalType>(*this);
+}
+
+const TDecimalLogicalType& TLogicalType::AsDecimalTypeRef() const
+{
+ return VerifiedCast<TDecimalLogicalType>(*this);
+}
+
+const TOptionalLogicalType& TLogicalType::AsOptionalTypeRef() const
+{
+ return VerifiedCast<TOptionalLogicalType>(*this);
+}
+
+const TListLogicalType& TLogicalType::AsListTypeRef() const
+{
+ return VerifiedCast<TListLogicalType>(*this);
+}
+
+const TStructLogicalType& TLogicalType::AsStructTypeRef() const
+{
+ return VerifiedCast<TStructLogicalType>(*this);
+}
+
+const TTupleLogicalType& TLogicalType::AsTupleTypeRef() const
+{
+ return VerifiedCast<TTupleLogicalType>(*this);
+}
+
+const TVariantTupleLogicalType& TLogicalType::AsVariantTupleTypeRef() const
+{
+ return VerifiedCast<TVariantTupleLogicalType>(*this);
+}
+
+const TVariantStructLogicalType& TLogicalType::AsVariantStructTypeRef() const
+{
+ return VerifiedCast<TVariantStructLogicalType>(*this);
+}
+
+const TDictLogicalType& TLogicalType::AsDictTypeRef() const
+{
+ return VerifiedCast<TDictLogicalType>(*this);
+}
+
+const TTaggedLogicalType& TLogicalType::AsTaggedTypeRef() const
+{
+ return VerifiedCast<TTaggedLogicalType>(*this);
+}
+
+const TLogicalTypePtr& TLogicalType::GetElement() const
+{
+ switch (Metatype_) {
+ case ELogicalMetatype::Optional:
+ return AsOptionalTypeRef().GetElement();
+ case ELogicalMetatype::List:
+ return AsListTypeRef().GetElement();
+ case ELogicalMetatype::Tagged:
+ return AsTaggedTypeRef().GetElement();
+ default:
+ YT_ABORT();
+ }
+}
+
+const std::vector<TLogicalTypePtr>& TLogicalType::GetElements() const
+{
+ switch (Metatype_) {
+ case ELogicalMetatype::Tuple:
+ return AsTupleTypeRef().GetElements();
+ case ELogicalMetatype::VariantTuple:
+ return AsVariantTupleTypeRef().GetElements();
+ default:
+ YT_ABORT();
+ }
+}
+
+const std::vector<TStructField>& TLogicalType::GetFields() const
+{
+ switch (Metatype_) {
+ case ELogicalMetatype::Struct:
+ return AsStructTypeRef().GetFields();
+ case ELogicalMetatype::VariantStruct:
+ return AsVariantStructTypeRef().GetFields();
+ default:
+ YT_ABORT();
+ }
+}
+
+static bool operator == (const TStructField& lhs, const TStructField& rhs)
+{
+ return (lhs.Name == rhs.Name) && (*lhs.Type == *rhs.Type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TLogicalType& logicalType)
+{
+ switch (logicalType.GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CamelCaseToUnderscoreCase(ToString(logicalType.AsSimpleTypeRef().GetElement()));
+ case ELogicalMetatype::Decimal:
+ return Format("decimal(%v,%v)",
+ logicalType.AsDecimalTypeRef().GetPrecision(),
+ logicalType.AsDecimalTypeRef().GetScale());
+ case ELogicalMetatype::Optional:
+ return Format("optional<%v>", *logicalType.AsOptionalTypeRef().GetElement());
+ case ELogicalMetatype::List:
+ return Format("list<%v>", *logicalType.AsListTypeRef().GetElement());
+ case ELogicalMetatype::Struct: {
+ TStringStream out;
+ out << "struct<";
+ bool first = true;
+ for (const auto& structItem : logicalType.AsStructTypeRef().GetFields()) {
+ if (first) {
+ first = false;
+ } else {
+ out << ';';
+ }
+ out << structItem.Name << '=' << ToString(*structItem.Type);
+ }
+ out << '>';
+ return out.Str();
+ }
+ case ELogicalMetatype::Tuple: {
+ TStringStream out;
+ out << "tuple<";
+ bool first = true;
+ for (const auto& element : logicalType.AsTupleTypeRef().GetElements()) {
+ if (first) {
+ first = false;
+ } else {
+ out << ';';
+ }
+ out << ToString(*element);
+ }
+ out << '>';
+ return out.Str();
+ }
+ case ELogicalMetatype::VariantTuple: {
+ TStringStream out;
+ out << "variant<";
+ bool first = true;
+ for (const auto& element : logicalType.AsVariantTupleTypeRef().GetElements()) {
+ if (first) {
+ first = false;
+ } else {
+ out << ';';
+ }
+ out << ToString(*element);
+ }
+ out << '>';
+ return out.Str();
+ }
+ case ELogicalMetatype::VariantStruct: {
+ TStringStream out;
+ out << "named_variant<";
+ bool first = true;
+ for (const auto& field : logicalType.AsVariantStructTypeRef().GetFields()) {
+ if (first) {
+ first = false;
+ } else {
+ out << ';';
+ }
+ out << field.Name << '=' << ToString(*field.Type);
+ }
+ out << '>';
+ return out.Str();
+
+ }
+ case ELogicalMetatype::Dict: {
+ const auto& dictType = logicalType.AsDictTypeRef();
+ TStringStream out;
+ out << "dict<" << ToString(*dictType.GetKey()) << ';' << ToString(*dictType.GetValue()) << '>';
+ return out.Str();
+ }
+ case ELogicalMetatype::Tagged: {
+ const auto& taggedType = logicalType.AsTaggedTypeRef();
+ TStringStream out;
+ out << "tagged<\"" << ToString(taggedType.GetTag()) << "\";" << ToString(*taggedType.GetElement()) << '>';
+ return out.Str();
+ }
+ }
+ YT_ABORT();
+}
+
+void PrintTo(ELogicalMetatype metatype, std::ostream* os)
+{
+ *os << ToString(metatype);
+}
+
+void PrintTo(const TLogicalType& type, std::ostream* os)
+{
+ *os << ToString(type);
+}
+
+void PrintTo(const TLogicalTypePtr& type, std::ostream* os)
+{
+ if (type) {
+ PrintTo(*type, os);
+ } else {
+ (*os) << "<nullptr>";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDecimalLogicalType::TDecimalLogicalType(int precision, int scale)
+ : TLogicalType(ELogicalMetatype::Decimal)
+ , Precision_(precision)
+ , Scale_(scale)
+{ }
+
+size_t TDecimalLogicalType::GetMemoryUsage() const
+{
+ return sizeof(*this);
+}
+
+int TDecimalLogicalType::GetTypeComplexity() const
+{
+ return 1;
+}
+
+void TDecimalLogicalType::ValidateNode(const TWalkContext& /*context*/) const
+{
+ if (Precision_ < MinPrecision || Precision_ > MaxPrecision) {
+ THROW_ERROR_EXCEPTION("Decimal precision %Qv is not in range [%v, %v]",
+ Precision_,
+ MinPrecision,
+ MaxPrecision);
+ }
+ if (Scale_ < 0) {
+ THROW_ERROR_EXCEPTION("Decimal scale %Qv is negative",
+ Scale_);
+ }
+
+ if (Scale_ > Precision_) {
+ THROW_ERROR_EXCEPTION("Decimal scale %Qv exceeds precision %Qv",
+ Scale_,
+ Precision_);
+ }
+}
+
+bool TDecimalLogicalType::IsNullable() const
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOptionalLogicalType::TOptionalLogicalType(TLogicalTypePtr element)
+ : TLogicalType(ELogicalMetatype::Optional)
+ , Element_(std::move(element))
+ , ElementIsNullable_(Element_->IsNullable())
+{ }
+
+std::optional<ESimpleLogicalValueType> TOptionalLogicalType::Simplify() const
+{
+ if (!IsElementNullable() && GetElement()->GetMetatype() == ELogicalMetatype::Simple) {
+ return GetElement()->AsSimpleTypeRef().GetElement();
+ } else {
+ return std::nullopt;
+ }
+}
+
+size_t TOptionalLogicalType::GetMemoryUsage() const
+{
+ if (Element_->GetMetatype() == ELogicalMetatype::Simple) {
+ // All optionals of simple logical types are signletons and therefore we assume they use no space.
+ return 0;
+ } else {
+ return sizeof(*this) + Element_->GetMemoryUsage();
+ }
+}
+
+int TOptionalLogicalType::GetTypeComplexity() const
+{
+ if (Element_->GetMetatype() == ELogicalMetatype::Simple) {
+ return 1;
+ } else {
+ return 1 + Element_->GetTypeComplexity();
+ }
+}
+
+void TOptionalLogicalType::ValidateNode(const TWalkContext&) const
+{ }
+
+bool TOptionalLogicalType::IsNullable() const
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSimpleLogicalType::TSimpleLogicalType(ESimpleLogicalValueType element)
+ : TLogicalType(ELogicalMetatype::Simple)
+ , Element_(element)
+{ }
+
+size_t TSimpleLogicalType::GetMemoryUsage() const
+{
+ // All simple logical types are signletons and therefore we assume they use no space.
+ return 0;
+}
+
+int TSimpleLogicalType::GetTypeComplexity() const
+{
+ return 1;
+}
+
+void TSimpleLogicalType::ValidateNode(const TWalkContext& context) const
+{
+ if (Element_ == ESimpleLogicalValueType::Any) {
+ if (context.Stack.empty() || context.Stack.back().GetType()->GetMetatype() != ELogicalMetatype::Optional) {
+ THROW_ERROR_EXCEPTION("Type %Qv is disallowed outside of optional",
+ ESimpleLogicalValueType::Any);
+ }
+ }
+}
+
+bool TSimpleLogicalType::IsNullable() const
+{
+ return GetPhysicalType(Element_) == EValueType::Null;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TListLogicalType::TListLogicalType(TLogicalTypePtr element)
+ : TLogicalType(ELogicalMetatype::List)
+ , Element_(std::move(element))
+{ }
+
+size_t TListLogicalType::GetMemoryUsage() const
+{
+ return sizeof(*this) + Element_->GetMemoryUsage();
+}
+
+int TListLogicalType::GetTypeComplexity() const
+{
+ return 1 + Element_->GetTypeComplexity();
+}
+
+void TListLogicalType::ValidateNode(const TWalkContext& /*context*/) const
+{ }
+
+bool TListLogicalType::IsNullable() const
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TComplexTypeFieldDescriptor::TComplexTypeFieldDescriptor(TLogicalTypePtr type)
+ : Type_(std::move(type))
+{ }
+
+TComplexTypeFieldDescriptor::TComplexTypeFieldDescriptor(const NYT::NTableClient::TColumnSchema& column)
+ : TComplexTypeFieldDescriptor(column.Name(), column.LogicalType())
+{ }
+
+TComplexTypeFieldDescriptor::TComplexTypeFieldDescriptor(TString columnName, TLogicalTypePtr type)
+ : Descriptor_(std::move(columnName))
+ , Type_(std::move(type))
+{ }
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::OptionalElement() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_ + ".<optional-element>", Type_->AsOptionalTypeRef().GetElement());
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::ListElement() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_ + ".<list-element>", Type_->AsListTypeRef().GetElement());
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::Field(size_t i) const
+{
+ switch (Type_->GetMetatype()) {
+ case ELogicalMetatype::Struct:
+ return StructField(i);
+ case ELogicalMetatype::VariantStruct:
+ return VariantStructField(i);
+ default:
+ YT_ABORT();
+ }
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::StructField(size_t i) const
+{
+ const auto& fields = Type_->AsStructTypeRef().GetFields();
+ YT_VERIFY(i < fields.size());
+ const auto& field = fields[i];
+ return TComplexTypeFieldDescriptor(Descriptor_ + "." + field.Name, field.Type);
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::Element(size_t i) const
+{
+ switch (Type_->GetMetatype()) {
+ case ELogicalMetatype::Tuple:
+ return TupleElement(i);
+ case ELogicalMetatype::VariantTuple:
+ return VariantTupleElement(i);
+ default:
+ YT_ABORT();
+ }
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::TupleElement(size_t i) const
+{
+ const auto& elements = Type_->AsTupleTypeRef().GetElements();
+ YT_VERIFY(i < elements.size());
+ return TComplexTypeFieldDescriptor(Descriptor_ + Format(".<tuple-element-%v>", i), elements[i]);
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::VariantTupleElement(size_t i) const
+{
+ const auto& elements = Type_->AsVariantTupleTypeRef().GetElements();
+ YT_VERIFY(i < elements.size());
+ return TComplexTypeFieldDescriptor(Descriptor_ + Format(".<variant-element-%v>", i), elements[i]);
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::VariantStructField(size_t i) const
+{
+ const auto& fields = Type_->AsVariantStructTypeRef().GetFields();
+ YT_VERIFY(i < fields.size());
+ const auto& field = fields[i];
+ return TComplexTypeFieldDescriptor(Descriptor_ + "." + field.Name, field.Type);
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::DictKey() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_ + ".<key>", Type_->AsDictTypeRef().GetKey());
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::DictValue() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_ + ".<value>", Type_->AsDictTypeRef().GetValue());
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::TaggedElement() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_ + ".<tagged-element>", Type_->AsTaggedTypeRef().GetElement());
+}
+
+TComplexTypeFieldDescriptor TComplexTypeFieldDescriptor::Detag() const
+{
+ return TComplexTypeFieldDescriptor(Descriptor_, DetagLogicalType(Type_));
+}
+
+const TString& TComplexTypeFieldDescriptor::GetDescription() const
+{
+ return Descriptor_;
+}
+
+const TLogicalTypePtr& TComplexTypeFieldDescriptor::GetType() const
+{
+ return Type_;
+}
+
+static std::pair<std::vector<TStructField>, bool> DetagFields(const std::vector<TStructField>& fields)
+{
+ bool changed = false;
+ std::vector<TStructField> result;
+ for (const auto& field : fields) {
+ result.emplace_back();
+ result.back().Name = field.Name;
+ result.back().Type = DetagLogicalType(field.Type);
+ if (result.back().Type.Get() != field.Type.Get()) {
+ changed = true;
+ }
+ }
+ return std::pair(result, changed);
+}
+
+static std::pair<std::vector<TLogicalTypePtr>, bool> DetagElements(const std::vector<TLogicalTypePtr>& elements)
+{
+ std::vector<TLogicalTypePtr> result;
+ bool changed = false;
+ for (const auto& element : elements) {
+ result.emplace_back(DetagLogicalType(element));
+ if (result.back().Get() != element.Get()) {
+ changed = true;
+ }
+ }
+ return std::pair(result, changed);
+}
+
+TLogicalTypePtr DetagLogicalType(const TLogicalTypePtr& type)
+{
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ case ELogicalMetatype::Decimal:
+ return type;
+ case ELogicalMetatype::Optional: {
+ const auto& element = type->AsOptionalTypeRef().GetElement();
+ auto detaggedElement = DetagLogicalType(element);
+ if (element.Get() != detaggedElement.Get()) {
+ return OptionalLogicalType(detaggedElement);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::List: {
+ const auto& element = type->AsListTypeRef().GetElement();
+ auto detaggedElement = DetagLogicalType(element);
+ if (element.Get() != detaggedElement.Get()) {
+ return ListLogicalType(detaggedElement);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::Struct: {
+ const auto [fields, changed] = DetagFields(type->AsStructTypeRef().GetFields());
+ if (changed) {
+ return StructLogicalType(fields);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::Tuple: {
+ const auto [elements, changed] = DetagElements(type->AsTupleTypeRef().GetElements());
+ if (changed) {
+ return TupleLogicalType(elements);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::VariantStruct: {
+ const auto [fields, changed] = DetagFields(type->AsVariantStructTypeRef().GetFields());
+ if (changed) {
+ return VariantStructLogicalType(fields);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::VariantTuple: {
+ const auto [elements, changed] = DetagElements(type->AsVariantTupleTypeRef().GetElements());
+ if (changed) {
+ return VariantTupleLogicalType(elements);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::Dict: {
+ const auto& dictType = type->AsDictTypeRef();
+ const auto& key = dictType.GetKey();
+ const auto& value = dictType.GetValue();
+ const auto detaggedKey = DetagLogicalType(key);
+ const auto detaggedValue = DetagLogicalType(value);
+ if (detaggedKey.Get() != key.Get() || detaggedValue.Get() != value.Get()) {
+ return DictLogicalType(
+ detaggedKey,
+ detaggedValue);
+ } else {
+ return type;
+ }
+ }
+ case ELogicalMetatype::Tagged:
+ return DetagLogicalType(type->AsTaggedTypeRef().GetElement());
+ }
+ YT_ABORT();
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructLogicalTypeBase::TStructLogicalTypeBase(ELogicalMetatype metatype, std::vector<TStructField> fields)
+ : TLogicalType(metatype)
+ , Fields_(std::move(fields))
+{ }
+
+size_t TStructLogicalTypeBase::GetMemoryUsage() const
+{
+ size_t result = sizeof(*this);
+ result += sizeof(TStructField) * Fields_.size();
+ for (const auto& field : Fields_) {
+ result += field.Type->GetMemoryUsage();
+ }
+ return result;
+}
+
+int TStructLogicalTypeBase::GetTypeComplexity() const
+{
+ int result = 1;
+ for (const auto& field : Fields_) {
+ result += field.Type->GetTypeComplexity();
+ }
+ return result;
+}
+
+void TStructLogicalTypeBase::ValidateNode(const TWalkContext& /*context*/) const
+{
+ THashSet<TStringBuf> usedNames;
+ for (size_t i = 0; i < Fields_.size(); ++i) {
+ const auto& field = Fields_[i];
+ if (field.Name.empty()) {
+ THROW_ERROR_EXCEPTION("Name of struct field #%v is empty",
+ i);
+ }
+ if (usedNames.contains(field.Name)) {
+ THROW_ERROR_EXCEPTION("Struct field name %Qv is used twice",
+ field.Name);
+ }
+ usedNames.emplace(field.Name);
+ if (field.Name.size() > MaxColumnNameLength) {
+ THROW_ERROR_EXCEPTION("Name of struct field #%v exceeds limit: %v > %v",
+ i,
+ field.Name.size(),
+ MaxColumnNameLength);
+ }
+ if (!IsUtf(field.Name)) {
+ THROW_ERROR_EXCEPTION("Name of struct field #%v is not valid utf8",
+ i);
+ }
+ }
+}
+
+bool TStructLogicalTypeBase::IsNullable() const
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTupleLogicalTypeBase::TTupleLogicalTypeBase(ELogicalMetatype metatype, std::vector<NYT::NTableClient::TLogicalTypePtr> elements)
+ : TLogicalType(metatype)
+ , Elements_(std::move(elements))
+{ }
+
+size_t TTupleLogicalTypeBase::GetMemoryUsage() const
+{
+ size_t result = sizeof(*this);
+ result += sizeof(TLogicalTypePtr) * Elements_.size();
+ for (const auto& element : Elements_) {
+ result += element->GetMemoryUsage();
+ }
+ return result;
+}
+
+int TTupleLogicalTypeBase::GetTypeComplexity() const
+{
+ int result = 1;
+ for (const auto& element : Elements_) {
+ result += element->GetTypeComplexity();
+ }
+ return result;
+}
+
+void TTupleLogicalTypeBase::ValidateNode(const TWalkContext& /*context*/) const
+{ }
+
+bool TTupleLogicalTypeBase::IsNullable() const
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructLogicalType::TStructLogicalType(std::vector<NYT::NTableClient::TStructField> fields)
+ : TStructLogicalTypeBase(ELogicalMetatype::Struct, std::move(fields))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTupleLogicalType::TTupleLogicalType(std::vector<NYT::NTableClient::TLogicalTypePtr> elements)
+ : TTupleLogicalTypeBase(ELogicalMetatype::Tuple, std::move(elements))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVariantStructLogicalType::TVariantStructLogicalType(std::vector<NYT::NTableClient::TStructField> fields)
+ : TStructLogicalTypeBase(ELogicalMetatype::VariantStruct, std::move(fields))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVariantTupleLogicalType::TVariantTupleLogicalType(std::vector<NYT::NTableClient::TLogicalTypePtr> elements)
+ : TTupleLogicalTypeBase(ELogicalMetatype::VariantTuple, std::move(elements))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDictLogicalType::TDictLogicalType(TLogicalTypePtr key, TLogicalTypePtr value)
+ : TLogicalType(ELogicalMetatype::Dict)
+ , Key_(std::move(key))
+ , Value_(std::move(value))
+{ }
+
+size_t TDictLogicalType::GetMemoryUsage() const
+{
+ return sizeof(*this) + Key_->GetMemoryUsage() + Value_->GetMemoryUsage();
+}
+
+int TDictLogicalType::GetTypeComplexity() const
+{
+ return 1 + Key_->GetTypeComplexity() + Value_->GetTypeComplexity();
+}
+
+void TDictLogicalType::ValidateNode(const TWalkContext&) const
+{
+ TComplexTypeFieldDescriptor descriptor("<dict-key>", GetKey());
+ Walk(descriptor, [](const TWalkContext&, const TComplexTypeFieldDescriptor& descriptor) {
+ const auto& logicalType = descriptor.GetType();
+
+ // NB. We intentionally list all metatypes and simple types here.
+ // We want careful decision if type can be used as dictionary key each time new type is added.
+ // Compiler will warn you (with error) if some enum is not handled.
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ switch (auto simpleType = logicalType->AsSimpleTypeRef().GetElement()) {
+ case ESimpleLogicalValueType::Any:
+ case ESimpleLogicalValueType::Json:
+ THROW_ERROR_EXCEPTION("%Qv is of type %Qv that is not allowed in dict key",
+ descriptor.GetDescription(),
+ simpleType);
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ case ESimpleLogicalValueType::Boolean:
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+ case ESimpleLogicalValueType::Float:
+ case ESimpleLogicalValueType::Double:
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ case ESimpleLogicalValueType::Interval:
+ case ESimpleLogicalValueType::Uuid:
+ return;
+ }
+ YT_ABORT();
+ case ELogicalMetatype::Optional:
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Dict:
+ case ELogicalMetatype::Tagged:
+ case ELogicalMetatype::Decimal:
+ return;
+ }
+ YT_ABORT();
+ });
+}
+
+bool TDictLogicalType::IsNullable() const
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTaggedLogicalType::TTaggedLogicalType(TString tag, NYT::NTableClient::TLogicalTypePtr element)
+ : TLogicalType(ELogicalMetatype::Tagged)
+ , Tag_(std::move(tag))
+ , Element_(std::move(element))
+{ }
+
+size_t TTaggedLogicalType::GetMemoryUsage() const
+{
+ return sizeof(*this) + GetElement()->GetMemoryUsage();
+}
+
+int TTaggedLogicalType::GetTypeComplexity() const
+{
+ return 1 + GetElement()->GetTypeComplexity();
+}
+
+void TTaggedLogicalType::ValidateNode(const TWalkContext&) const
+{
+ if (Tag_.empty()) {
+ THROW_ERROR_EXCEPTION("Tag is empty");
+ }
+
+ if (Tag_.size() > MaxColumnNameLength) {
+ THROW_ERROR_EXCEPTION("Tag is too big");
+ }
+
+ if (!IsUtf(Tag_)) {
+ THROW_ERROR_EXCEPTION("Tag is not valid utf8");
+ }
+}
+
+bool TTaggedLogicalType::IsNullable() const
+{
+ return GetElement()->IsNullable();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTypeV3Info
+{
+ ESimpleLogicalValueType V1Type;
+ EValueType WireType;
+ bool Required;
+ bool IsPureV1Type;
+};
+
+static TTypeV3Info GetTypeV3Info(const TLogicalTypePtr& logicalType)
+{
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple: {
+ auto element = logicalType->AsSimpleTypeRef().GetElement();
+ return {element, GetPhysicalType(element), !logicalType->IsNullable(), true};
+ }
+ case ELogicalMetatype::Decimal:
+ return {ESimpleLogicalValueType::String, EValueType::String, true, false};
+ case ELogicalMetatype::Optional: {
+ const auto& element = logicalType->AsOptionalTypeRef().GetElement();
+ if (element->IsNullable()) {
+ return {ESimpleLogicalValueType::Any, EValueType::Composite, false, false};
+ } else {
+ auto elementInfo = GetTypeV3Info(element);
+ elementInfo.Required = false;
+ return elementInfo;
+ }
+ }
+ case ELogicalMetatype::Tagged:
+ return GetTypeV3Info(logicalType->AsTaggedTypeRef().GetElement());
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::VariantTuple:
+ case ELogicalMetatype::Dict:
+ return {ESimpleLogicalValueType::Any, EValueType::Composite, true, false};
+ }
+ YT_ABORT();
+}
+
+std::pair<ESimpleLogicalValueType, bool> CastToV1Type(const TLogicalTypePtr& logicalType)
+{
+ auto info = GetTypeV3Info(logicalType);
+ return {info.V1Type, info.Required};
+}
+
+bool IsV1Type(const TLogicalTypePtr& logicalType)
+{
+ return GetTypeV3Info(logicalType).IsPureV1Type;
+}
+
+EValueType GetWireType(const TLogicalTypePtr& logicalType)
+{
+ return GetTypeV3Info(logicalType).WireType;
+}
+
+bool IsV3Composite(const TLogicalTypePtr& logicalType)
+{
+ return GetWireType(logicalType) == EValueType::Composite;
+}
+
+TLogicalTypePtr DenullifyLogicalType(const TLogicalTypePtr& type)
+{
+ auto detagged = DetagLogicalType(type);
+ if (detagged->GetMetatype() == ELogicalMetatype::Optional) {
+ const auto& optional = detagged->AsOptionalTypeRef();
+ if (!optional.IsElementNullable()) {
+ return optional.GetElement();
+ }
+ }
+ return detagged;
+}
+
+bool operator != (const TLogicalType& lhs, const TLogicalType& rhs)
+{
+ return !(lhs == rhs);
+}
+
+bool operator == (const std::vector<TLogicalTypePtr>& lhs, const std::vector<TLogicalTypePtr>& rhs)
+{
+ if (lhs.size() != rhs.size()) {
+ return false;
+ }
+ for (size_t i = 0; i < lhs.size(); ++i) {
+ if (*lhs[i] != *rhs[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool operator == (const TLogicalType& lhs, const TLogicalType& rhs)
+{
+ if (&lhs == &rhs) {
+ return true;
+ }
+
+ if (lhs.GetMetatype() != rhs.GetMetatype()) {
+ return false;
+ }
+
+ switch (lhs.GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return lhs.AsSimpleTypeRef().GetElement() == rhs.AsSimpleTypeRef().GetElement();
+ case ELogicalMetatype::Optional:
+ return *lhs.AsOptionalTypeRef().GetElement() == *rhs.AsOptionalTypeRef().GetElement();
+ case ELogicalMetatype::List:
+ return *lhs.AsListTypeRef().GetElement() == *rhs.AsListTypeRef().GetElement();
+ case ELogicalMetatype::Struct:
+ return lhs.AsStructTypeRef().GetFields() == rhs.AsStructTypeRef().GetFields();
+ case ELogicalMetatype::Tuple:
+ return lhs.AsTupleTypeRef().GetElements() == rhs.AsTupleTypeRef().GetElements();
+ case ELogicalMetatype::VariantStruct:
+ return lhs.AsVariantStructTypeRef().GetFields() == rhs.AsVariantStructTypeRef().GetFields();
+ case ELogicalMetatype::VariantTuple:
+ return lhs.AsVariantTupleTypeRef().GetElements() == rhs.AsVariantTupleTypeRef().GetElements();
+ case ELogicalMetatype::Dict:
+ return *lhs.AsDictTypeRef().GetKey() == *rhs.AsDictTypeRef().GetKey() &&
+ *lhs.AsDictTypeRef().GetValue() == *rhs.AsDictTypeRef().GetValue();
+ case ELogicalMetatype::Tagged:
+ return lhs.AsTaggedTypeRef().GetTag() == rhs.AsTaggedTypeRef().GetTag() &&
+ *lhs.AsTaggedTypeRef().GetElement() == *rhs.AsTaggedTypeRef().GetElement();
+ case ELogicalMetatype::Decimal:
+ return lhs.AsDecimalTypeRef().GetPrecision() == rhs.AsDecimalTypeRef().GetPrecision() &&
+ lhs.AsDecimalTypeRef().GetScale() == rhs.AsDecimalTypeRef().GetScale();
+ }
+ YT_ABORT();
+}
+
+void ValidateLogicalType(const TComplexTypeFieldDescriptor& rootDescriptor, std::optional<int> depthLimit)
+{
+ Walk(rootDescriptor, [&] (const TWalkContext& context, const TComplexTypeFieldDescriptor& descriptor) {
+ try {
+ descriptor.GetType()->ValidateNode(context);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("%Qv has bad type",
+ descriptor.GetDescription())
+ << ex;
+ }
+ if (depthLimit && std::ssize(context.Stack) > *depthLimit) {
+ THROW_ERROR_EXCEPTION("%Qv exceeds type depth limit",
+ descriptor.GetDescription())
+ << TErrorAttribute("limit", *depthLimit);
+ }
+ });
+}
+
+void ToProto(NProto::TLogicalType* protoLogicalType, const TLogicalTypePtr& logicalType)
+{
+ switch (logicalType->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ protoLogicalType->set_simple(static_cast<int>(logicalType->AsSimpleTypeRef().GetElement()));
+ return;
+ case ELogicalMetatype::Decimal:
+ protoLogicalType->mutable_decimal()->set_precision(logicalType->AsDecimalTypeRef().GetPrecision());
+ protoLogicalType->mutable_decimal()->set_scale(logicalType->AsDecimalTypeRef().GetScale());
+ return;
+ case ELogicalMetatype::Optional:
+ ToProto(protoLogicalType->mutable_optional(), logicalType->AsOptionalTypeRef().GetElement());
+ return;
+ case ELogicalMetatype::List:
+ ToProto(protoLogicalType->mutable_list(), logicalType->AsListTypeRef().GetElement());
+ return;
+ case ELogicalMetatype::Struct: {
+ auto protoStruct = protoLogicalType->mutable_struct_();
+ for (const auto& structField : logicalType->AsStructTypeRef().GetFields()) {
+ auto protoStructField = protoStruct->add_fields();
+ protoStructField->set_name(structField.Name);
+ ToProto(protoStructField->mutable_type(), structField.Type);
+ }
+ return;
+ }
+ case ELogicalMetatype::Tuple: {
+ auto protoTuple = protoLogicalType->mutable_tuple();
+ for (const auto& element : logicalType->AsTupleTypeRef().GetElements()) {
+ auto protoElement = protoTuple->add_elements();
+ ToProto(protoElement, element);
+ }
+ return;
+ }
+ case ELogicalMetatype::VariantStruct: {
+ auto protoVariantStruct = protoLogicalType->mutable_variant_struct();
+ for (const auto& field : logicalType->AsVariantStructTypeRef().GetFields()) {
+ auto protoField = protoVariantStruct->add_fields();
+ protoField->set_name(field.Name);
+ ToProto(protoField->mutable_type(), field.Type);
+ }
+ return;
+ }
+ case ELogicalMetatype::VariantTuple: {
+ auto protoVariantTuple = protoLogicalType->mutable_variant_tuple();
+ for (const auto& element : logicalType->AsVariantTupleTypeRef().GetElements()) {
+ auto protoElement = protoVariantTuple->add_elements();
+ ToProto(protoElement, element);
+ }
+ return;
+ }
+ case ELogicalMetatype::Dict: {
+ auto protoDict = protoLogicalType->mutable_dict();
+ const auto& dictLogicalType = logicalType->AsDictTypeRef();
+ ToProto(protoDict->mutable_key(), dictLogicalType.GetKey());
+ ToProto(protoDict->mutable_value(), dictLogicalType.GetValue());
+ return;
+ }
+ case ELogicalMetatype::Tagged: {
+ auto protoTagged = protoLogicalType->mutable_tagged();
+ const auto& taggedLogicalType = logicalType->AsTaggedTypeRef();
+ protoTagged->set_tag(taggedLogicalType.GetTag());
+ ToProto(protoTagged->mutable_element(), taggedLogicalType.GetElement());
+ return;
+ }
+ }
+ YT_ABORT();
+}
+
+void FromProto(TLogicalTypePtr* logicalType, const NProto::TLogicalType& protoLogicalType)
+{
+ switch (protoLogicalType.type_case()) {
+ case NProto::TLogicalType::TypeCase::kSimple:
+ *logicalType = SimpleLogicalType(CheckedEnumCast<ESimpleLogicalValueType>(protoLogicalType.simple()));
+ return;
+ case NProto::TLogicalType::TypeCase::kDecimal:
+ *logicalType = DecimalLogicalType(protoLogicalType.decimal().precision(), protoLogicalType.decimal().scale());
+ return;
+ case NProto::TLogicalType::TypeCase::kOptional: {
+ TLogicalTypePtr element;
+ FromProto(&element, protoLogicalType.optional());
+ *logicalType = OptionalLogicalType(element);
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kList: {
+ TLogicalTypePtr element;
+ FromProto(&element, protoLogicalType.list());
+ *logicalType = ListLogicalType(element);
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kStruct: {
+ std::vector<TStructField> fields;
+ for (const auto& protoField : protoLogicalType.struct_().fields()) {
+ TLogicalTypePtr fieldType;
+ FromProto(&fieldType, protoField.type());
+ fields.emplace_back(TStructField{protoField.name(), std::move(fieldType)});
+ }
+ *logicalType = StructLogicalType(std::move(fields));
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kTuple: {
+ std::vector<TLogicalTypePtr> elements;
+ for (const auto& protoField : protoLogicalType.tuple().elements()) {
+ elements.emplace_back();
+ FromProto(&elements.back(), protoField);
+ }
+ *logicalType = TupleLogicalType(std::move(elements));
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kVariantTuple: {
+ std::vector<TLogicalTypePtr> elements;
+ for (const auto& protoElement : protoLogicalType.variant_tuple().elements()) {
+ elements.emplace_back();
+ FromProto(&elements.back(), protoElement);
+ }
+ *logicalType = VariantTupleLogicalType(std::move(elements));
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kVariantStruct: {
+ std::vector<TStructField> fields;
+ for (const auto& protoField : protoLogicalType.variant_struct().fields()) {
+ TLogicalTypePtr fieldType;
+ FromProto(&fieldType, protoField.type());
+ fields.emplace_back(TStructField{protoField.name(), std::move(fieldType)});
+ }
+ *logicalType = VariantStructLogicalType(std::move(fields));
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kDict: {
+ TLogicalTypePtr keyType;
+ TLogicalTypePtr valueType;
+ FromProto(&keyType, protoLogicalType.dict().key());
+ FromProto(&valueType, protoLogicalType.dict().value());
+ *logicalType = DictLogicalType(keyType, valueType);
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::kTagged: {
+ TLogicalTypePtr element;
+ FromProto(&element, protoLogicalType.tagged().element());
+ *logicalType = TaggedLogicalType(protoLogicalType.tagged().tag(), std::move(element));
+ return;
+ }
+ case NProto::TLogicalType::TypeCase::TYPE_NOT_SET:
+ THROW_ERROR_EXCEPTION("Cannot parse unknown logical type from proto");
+ }
+ YT_ABORT();
+}
+
+void Serialize(const TStructField& structElement, NYson::IYsonConsumer* consumer)
+{
+ NYTree::BuildYsonFluently(consumer).BeginMap()
+ .Item("name").Value(structElement.Name)
+ .Item("type").Value(structElement.Type)
+ .EndMap();
+}
+
+void Deserialize(TStructField& structElement, NYTree::INodePtr node)
+{
+ const auto& mapNode = node->AsMap();
+ structElement.Name = NYTree::ConvertTo<TString>(mapNode->GetChildOrThrow("name"));
+ structElement.Type = NYTree::ConvertTo<TLogicalTypePtr>(mapNode->GetChildOrThrow("type"));
+}
+
+// TODO(levysotsky): Get rid of this variable when we are sure type_v2 is dead for good.
+static constexpr bool UseTypeV3ForSerialization = true;
+
+void Serialize(const TLogicalTypePtr& logicalType, NYson::IYsonConsumer* consumer)
+{
+ if (UseTypeV3ForSerialization) {
+ Serialize(TTypeV3LogicalTypeWrapper{logicalType}, consumer);
+ return;
+ }
+
+ const auto metatype = logicalType->GetMetatype();
+ switch (metatype) {
+ case ELogicalMetatype::Simple:
+ NYTree::BuildYsonFluently(consumer)
+ .Value(logicalType->AsSimpleTypeRef().GetElement());
+ return;
+ case ELogicalMetatype::Decimal:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("precision").Value(logicalType->AsDecimalTypeRef().GetPrecision())
+ .Item("scale").Value(logicalType->AsDecimalTypeRef().GetScale())
+ .EndMap();
+ return;
+ case ELogicalMetatype::Optional:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("element").Value(logicalType->AsOptionalTypeRef().GetElement())
+ .EndMap();
+ return;
+ case ELogicalMetatype::List:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("element").Value(logicalType->AsListTypeRef().GetElement())
+ .EndMap();
+ return;
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct: {
+ const auto& fields =
+ metatype == ELogicalMetatype::Struct ?
+ logicalType->AsStructTypeRef().GetFields() :
+ logicalType->AsVariantStructTypeRef().GetFields();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("fields").Value(fields)
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple: {
+ const auto& elements =
+ metatype == ELogicalMetatype::Tuple ?
+ logicalType->AsTupleTypeRef().GetElements() :
+ logicalType->AsVariantTupleTypeRef().GetElements();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("elements").Value(elements)
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Dict: {
+ const auto& key = logicalType->AsDictTypeRef().GetKey();
+ const auto& value = logicalType->AsDictTypeRef().GetValue();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("key").Value(key)
+ .Item("value").Value(value)
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Tagged: {
+ const auto& element = logicalType->AsTaggedTypeRef().GetElement();
+ const auto& tag = logicalType->AsTaggedTypeRef().GetTag();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("metatype").Value(metatype)
+ .Item("tag").Value(tag)
+ .Item("element").Value(element)
+ .EndMap();
+ return;
+ }
+ }
+ YT_ABORT();
+}
+
+void Deserialize(TLogicalTypePtr& logicalType, NYTree::INodePtr node)
+{
+ if (UseTypeV3ForSerialization) {
+ TTypeV3LogicalTypeWrapper wrapper;
+ Deserialize(wrapper, node);
+ logicalType = wrapper.LogicalType;
+ return;
+ }
+
+ if (node->GetType() == NYTree::ENodeType::String) {
+ auto simpleLogicalType = NYTree::ConvertTo<ESimpleLogicalValueType>(node);
+ logicalType = SimpleLogicalType(simpleLogicalType);
+ return;
+ }
+ if (node->GetType() != NYTree::ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Error parsing logical type: expected %Qlv or %Qlv, actual %Qlv",
+ NYTree::ENodeType::Map,
+ NYTree::ENodeType::String,
+ node->GetType());
+ }
+ auto mapNode = node->AsMap();
+
+ ELogicalMetatype metatype;
+ {
+ auto metatypeNode = mapNode->GetChildOrThrow("metatype");
+ metatype = NYTree::ConvertTo<ELogicalMetatype>(metatypeNode);
+ }
+ switch (metatype) {
+ case ELogicalMetatype::Simple: {
+ THROW_ERROR_EXCEPTION("Error parsing logical type: cannot parse simple type from %Qv",
+ NYTree::ENodeType::Map);
+ }
+ case ELogicalMetatype::Decimal: {
+ auto precision = mapNode->GetChildValueOrThrow<i64>("precision");
+ auto scale = mapNode->GetChildValueOrThrow<i64>("scale");
+ logicalType = DecimalLogicalType(precision, scale);
+ return;
+ }
+ case ELogicalMetatype::Optional: {
+ auto elementNode = mapNode->GetChildOrThrow("element");
+ auto element = NYTree::ConvertTo<TLogicalTypePtr>(elementNode);
+ logicalType = OptionalLogicalType(std::move(element));
+ return;
+ }
+ case ELogicalMetatype::List: {
+ auto elementNode = mapNode->GetChildOrThrow("element");
+ auto element = NYTree::ConvertTo<TLogicalTypePtr>(elementNode);
+ logicalType = ListLogicalType(std::move(element));
+ return;
+ }
+ case ELogicalMetatype::Struct: {
+ auto fieldsNode = mapNode->GetChildOrThrow("fields");
+ auto fields = NYTree::ConvertTo<std::vector<TStructField>>(fieldsNode);
+ logicalType = StructLogicalType(std::move(fields));
+ return;
+ }
+ case ELogicalMetatype::Tuple: {
+ auto elementsNode = mapNode->GetChildOrThrow("elements");
+ auto elements = NYTree::ConvertTo<std::vector<TLogicalTypePtr>>(elementsNode);
+ logicalType = TupleLogicalType(std::move(elements));
+ return;
+ }
+ case ELogicalMetatype::VariantStruct: {
+ auto fieldsNode = mapNode->GetChildOrThrow("fields");
+ auto fields = NYTree::ConvertTo<std::vector<TStructField>>(fieldsNode);
+ logicalType = VariantStructLogicalType(std::move(fields));
+ return;
+ }
+ case ELogicalMetatype::VariantTuple: {
+ auto elementsNode = mapNode->GetChildOrThrow("elements");
+ auto elements = NYTree::ConvertTo<std::vector<TLogicalTypePtr>>(elementsNode);
+ logicalType = VariantTupleLogicalType(std::move(elements));
+ return;
+ }
+ case ELogicalMetatype::Dict: {
+ auto keyNode = mapNode->GetChildOrThrow("key");
+ auto key = NYTree::ConvertTo<TLogicalTypePtr>(keyNode);
+ auto valueNode = mapNode->GetChildOrThrow("value");
+ auto value = NYTree::ConvertTo<TLogicalTypePtr>(valueNode);
+ logicalType = DictLogicalType(std::move(key), std::move(value));
+ return;
+ }
+ case ELogicalMetatype::Tagged: {
+ auto tagNode = mapNode->GetChildOrThrow("tag");
+ auto tag = NYTree::ConvertTo<TString>(tagNode);
+ auto elementNode = mapNode->GetChildOrThrow("element");
+ auto element = NYTree::ConvertTo<TLogicalTypePtr>(elementNode);
+ logicalType = TaggedLogicalType(std::move(tag), std::move(element));
+ return;
+ }
+ }
+ YT_ABORT();
+}
+
+bool IsComparable(const TLogicalTypePtr& type)
+{
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ switch (type->AsSimpleTypeRef().GetElement()) {
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int8:
+
+ case ESimpleLogicalValueType::Uint64:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint8:
+
+ case ESimpleLogicalValueType::Boolean:
+
+ case ESimpleLogicalValueType::Double:
+ case ESimpleLogicalValueType::Float:
+
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Utf8:
+
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ case ESimpleLogicalValueType::Interval:
+
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+
+ case ESimpleLogicalValueType::Uuid:
+ return true;
+
+ case ESimpleLogicalValueType::Any:
+ case ESimpleLogicalValueType::Json:
+ return false;
+ }
+ Y_FAIL();
+ case ELogicalMetatype::Decimal:
+ return true;
+ case ELogicalMetatype::Optional:
+ case ELogicalMetatype::List:
+ case ELogicalMetatype::Tagged:
+ return IsComparable(type->GetElement());
+
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple: {
+ for (const auto& element : type->GetElements()) {
+ if (!IsComparable(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct:
+ case ELogicalMetatype::Dict:
+ return false;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TV3Variant
+{ };
+
+using TV3TypeName = std::variant<ESimpleLogicalValueType, ELogicalMetatype, TV3Variant>;
+
+static const std::pair<ESimpleLogicalValueType, TString> V3SimpleLogicalValueTypeEncoding[] =
+{
+ {ESimpleLogicalValueType::Null, "null"},
+ {ESimpleLogicalValueType::Int64, "int64"},
+ {ESimpleLogicalValueType::Uint64, "uint64"},
+ {ESimpleLogicalValueType::Double, "double"},
+ {ESimpleLogicalValueType::Float, "float"},
+ {ESimpleLogicalValueType::Boolean, "bool"}, // NB. diff
+ {ESimpleLogicalValueType::String, "string"},
+ {ESimpleLogicalValueType::Any, "yson"}, // NB. diff
+ {ESimpleLogicalValueType::Json, "json"},
+ {ESimpleLogicalValueType::Int8, "int8"},
+ {ESimpleLogicalValueType::Uint8, "uint8"},
+
+ {ESimpleLogicalValueType::Int16, "int16"},
+ {ESimpleLogicalValueType::Uint16, "uint16"},
+
+ {ESimpleLogicalValueType::Int32, "int32"},
+ {ESimpleLogicalValueType::Uint32, "uint32"},
+
+ {ESimpleLogicalValueType::Utf8, "utf8"},
+
+ {ESimpleLogicalValueType::Date, "date"},
+ {ESimpleLogicalValueType::Datetime, "datetime"},
+ {ESimpleLogicalValueType::Timestamp, "timestamp"},
+ {ESimpleLogicalValueType::Interval, "interval"},
+
+ {ESimpleLogicalValueType::Void, "void"},
+
+ {ESimpleLogicalValueType::Uuid, "uuid"},
+};
+static_assert(std::size(V3SimpleLogicalValueTypeEncoding) == TEnumTraits<ESimpleLogicalValueType>::GetDomainSize());
+
+std::pair<ELogicalMetatype, TString> V3LogicalMetatypeEncoding[] =
+{
+ // NB. following metatypes are not included:
+ // - ELogicalMetatype::Simple
+ // - ELogicalMetatype::VariantStruct
+ // - ELogicalMetatype::VariantTuple
+ {ELogicalMetatype::Optional, "optional"},
+ {ELogicalMetatype::List, "list"},
+ {ELogicalMetatype::Struct, "struct"},
+ {ELogicalMetatype::Tuple, "tuple"},
+ {ELogicalMetatype::Dict, "dict"},
+ {ELogicalMetatype::Tagged, "tagged"},
+ {ELogicalMetatype::Decimal, "decimal"},
+};
+
+// NB ELogicalMetatype::{Simple,VariantStruct,VariantTuple} are not encoded therefore we have `-3` in static_assert below.
+static_assert(std::size(V3LogicalMetatypeEncoding) == TEnumTraits<ELogicalMetatype>::GetDomainSize() - 3);
+
+TV3TypeName FromTypeV3(TStringBuf stringBuf)
+{
+ static const auto map = [] {
+ THashMap<TStringBuf, TV3TypeName> res;
+ for (const auto& [value, string] : V3SimpleLogicalValueTypeEncoding) {
+ res[string] = value;
+ }
+ for (const auto& [value, string] : V3LogicalMetatypeEncoding) {
+ res[string] = value;
+ }
+ res["variant"] = TV3Variant{};
+ return res;
+ }();
+
+ auto it = map.find(stringBuf);
+ if (it == map.end()) {
+ THROW_ERROR_EXCEPTION("%Qv is not valid type_v3 simple type",
+ stringBuf);
+ }
+ return it->second;
+}
+
+TStringBuf ToTypeV3(ESimpleLogicalValueType value)
+{
+ for (const auto& [type, string] : V3SimpleLogicalValueTypeEncoding) {
+ if (type == value) {
+ return string;
+ }
+ }
+ YT_ABORT();
+}
+
+TStringBuf ToTypeV3(ELogicalMetatype value)
+{
+ YT_VERIFY(value != ELogicalMetatype::Simple);
+ for (const auto& [type, string] : V3LogicalMetatypeEncoding) {
+ if (type == value) {
+ return string;
+ }
+ }
+ YT_VERIFY(value == ELogicalMetatype::VariantStruct || value == ELogicalMetatype::VariantTuple);
+ return "variant";
+}
+
+struct TTypeV3MemberWrapper
+{
+ TStructField Member;
+};
+
+void Serialize(const TTypeV3MemberWrapper& wrapper, NYson::IYsonConsumer* consumer)
+{
+ NYTree::BuildYsonFluently(consumer).BeginMap()
+ .Item("type").Value(TTypeV3LogicalTypeWrapper{wrapper.Member.Type})
+ .Item("name").Value(wrapper.Member.Name)
+ .EndMap();
+}
+
+void Deserialize(TTypeV3MemberWrapper& wrapper, NYTree::INodePtr node)
+{
+ const auto& mapNode = node->AsMap();
+ auto wrappedType = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(mapNode->GetChildOrThrow("type"));
+ wrapper.Member.Type = wrappedType.LogicalType;
+ wrapper.Member.Name = NYTree::ConvertTo<TString>(mapNode->GetChildOrThrow("name"));
+}
+
+struct TTypeV3ElementWrapper
+{
+ TLogicalTypePtr Element;
+};
+
+void Serialize(const TTypeV3ElementWrapper& wrapper, NYson::IYsonConsumer* consumer)
+{
+ NYTree::BuildYsonFluently(consumer).BeginMap()
+ .Item("type").Value(TTypeV3LogicalTypeWrapper{wrapper.Element})
+ .EndMap();
+}
+
+void Deserialize(TTypeV3ElementWrapper& wrapper, NYTree::INodePtr node)
+{
+ const auto& mapNode = node->AsMap();
+ auto wrappedElement = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(mapNode->GetChildOrThrow("type"));
+ wrapper.Element = wrappedElement.LogicalType;
+}
+
+void Serialize(const TTypeV3LogicalTypeWrapper& wrapper, NYson::IYsonConsumer* consumer)
+{
+ using TWrapper = TTypeV3LogicalTypeWrapper;
+
+ const auto metatype = wrapper.LogicalType->GetMetatype();
+ switch (metatype) {
+ case ELogicalMetatype::Simple:
+ NYTree::BuildYsonFluently(consumer)
+ .Value(ToTypeV3(wrapper.LogicalType->AsSimpleTypeRef().GetElement()));
+ return;
+ case ELogicalMetatype::Decimal:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("precision").Value(wrapper.LogicalType->AsDecimalTypeRef().GetPrecision())
+ .Item("scale").Value(wrapper.LogicalType->AsDecimalTypeRef().GetScale())
+ .EndMap();
+ return;
+ case ELogicalMetatype::Optional:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("item").Value(TWrapper{wrapper.LogicalType->AsOptionalTypeRef().GetElement()})
+ .EndMap();
+ return;
+ case ELogicalMetatype::List:
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("item").Value(TWrapper{wrapper.LogicalType->AsListTypeRef().GetElement()})
+ .EndMap();
+ return;
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct: {
+ const auto& fields = wrapper.LogicalType->GetFields();
+
+ std::vector<TTypeV3MemberWrapper> wrappedMembers;
+ wrappedMembers.reserve(fields.size());
+ for (const auto& f : fields) {
+ wrappedMembers.emplace_back(TTypeV3MemberWrapper{f});
+ }
+
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("members").Value(wrappedMembers)
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple: {
+ const auto& elements = wrapper.LogicalType->GetElements();
+
+ std::vector<TTypeV3ElementWrapper> wrappedElements;
+ wrappedElements.reserve(elements.size());
+ for (const auto& e : elements) {
+ wrappedElements.emplace_back(TTypeV3ElementWrapper{e});
+ }
+
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("elements").Value(wrappedElements)
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Dict: {
+ const auto& key = wrapper.LogicalType->AsDictTypeRef().GetKey();
+ const auto& value = wrapper.LogicalType->AsDictTypeRef().GetValue();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("key").Value(TWrapper{key})
+ .Item("value").Value(TWrapper{value})
+ .EndMap();
+ return;
+ }
+ case ELogicalMetatype::Tagged: {
+ const auto& element = wrapper.LogicalType->AsTaggedTypeRef().GetElement();
+ const auto& tag = wrapper.LogicalType->AsTaggedTypeRef().GetTag();
+ NYTree::BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("type_name").Value(ToTypeV3(metatype))
+ .Item("tag").Value(tag)
+ .Item("item").Value(TWrapper{element})
+ .EndMap();
+ return;
+ }
+ }
+ YT_ABORT();
+}
+
+void Deserialize(TTypeV3LogicalTypeWrapper& wrapper, NYTree::INodePtr node)
+{
+ if (node->GetType() == NYTree::ENodeType::String) {
+ auto typeNameString = node->AsString()->GetValue();
+ auto typeName = FromTypeV3(typeNameString);
+ std::visit([&](const auto& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, ESimpleLogicalValueType>) {
+ wrapper.LogicalType = SimpleLogicalType(arg);
+ } else {
+ static_assert(std::is_same_v<T, ELogicalMetatype> || std::is_same_v<T, TV3Variant>);
+ THROW_ERROR_EXCEPTION("Type %Qv must be represented by map, not a string",
+ typeNameString);
+ }
+ }, typeName);
+ return;
+ }
+ if (node->GetType() != NYTree::ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Error parsing logical type: expected %Qlv or %Qlv, actual %Qlv",
+ NYTree::ENodeType::Map,
+ NYTree::ENodeType::String,
+ node->GetType());
+ }
+
+ auto mapNode = node->AsMap();
+ auto typeNameString = mapNode->GetChildValueOrThrow<TString>("type_name");
+ auto typeName = FromTypeV3(typeNameString);
+ std::visit([&](const auto& typeName) {
+ using T = std::decay_t<decltype(typeName)>;
+ if constexpr (std::is_same_v<T, ESimpleLogicalValueType>) {
+ wrapper.LogicalType = SimpleLogicalType(typeName);
+ } else {
+ ELogicalMetatype type;
+ if constexpr (std::is_same_v<T, ELogicalMetatype>) {
+ type = typeName;
+ } else {
+ const bool hasMembers = static_cast<bool>(mapNode->FindChild("members"));
+ const bool hasElements = static_cast<bool>(mapNode->FindChild("elements"));
+ if (hasMembers && hasElements) {
+ THROW_ERROR_EXCEPTION("\"variant\" cannot have both children \"elements\" and \"members\"");
+ } else if (hasMembers) {
+ type = ELogicalMetatype::VariantStruct;
+ } else if (hasElements) {
+ type = ELogicalMetatype::VariantTuple;
+ } else {
+ THROW_ERROR_EXCEPTION("\"variant\" must have \"elements\" or \"members\" child");
+ }
+ }
+ switch (type) {
+ case ELogicalMetatype::Simple:
+ // NB. FromTypeV3 never returns this value.
+ YT_ABORT();
+ case ELogicalMetatype::Decimal: {
+ auto precision = mapNode->GetChildValueOrThrow<int>("precision");
+ auto scale = mapNode->GetChildValueOrThrow<int>("scale");
+ wrapper.LogicalType = DecimalLogicalType(precision, scale);
+ return;
+ }
+ case ELogicalMetatype::Optional: {
+ auto itemNode = mapNode->GetChildOrThrow("item");
+ auto item = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(itemNode);
+ wrapper.LogicalType = OptionalLogicalType(std::move(item.LogicalType));
+ return;
+ }
+ case ELogicalMetatype::List: {
+ auto itemNode = mapNode->GetChildOrThrow("item");
+ auto wrappedItem = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(itemNode);
+ wrapper.LogicalType = ListLogicalType(std::move(wrappedItem.LogicalType));
+ return;
+ }
+ case ELogicalMetatype::Struct:
+ case ELogicalMetatype::VariantStruct: {
+ auto membersNode = mapNode->GetChildOrThrow("members");
+ auto wrappedMembers = NYTree::ConvertTo<std::vector<TTypeV3MemberWrapper>>(membersNode);
+
+ std::vector<TStructField> members;
+ members.reserve(wrappedMembers.size());
+ for (auto& w : wrappedMembers) {
+ members.emplace_back(w.Member);
+ }
+
+ if (type == ELogicalMetatype::Struct) {
+ wrapper.LogicalType = StructLogicalType(std::move(members));
+ } else {
+ wrapper.LogicalType = VariantStructLogicalType(std::move(members));
+ }
+ return;
+ }
+ case ELogicalMetatype::Tuple:
+ case ELogicalMetatype::VariantTuple: {
+ auto elementsNode = mapNode->GetChildOrThrow("elements");
+ auto elementsV3 = NYTree::ConvertTo<std::vector<TTypeV3ElementWrapper>>(elementsNode);
+
+ std::vector<TLogicalTypePtr> elements;
+ elements.reserve(elementsV3.size());
+ for (auto& e : elementsV3) {
+ elements.emplace_back(std::move(e.Element));
+ }
+ if (type == ELogicalMetatype::Tuple) {
+ wrapper.LogicalType = TupleLogicalType(std::move(elements));
+ } else {
+ wrapper.LogicalType = VariantTupleLogicalType(std::move(elements));
+ }
+ return;
+ }
+ case ELogicalMetatype::Dict: {
+ auto keyNode = mapNode->GetChildOrThrow("key");
+ auto key = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(keyNode);
+ auto valueNode = mapNode->GetChildOrThrow("value");
+ auto value = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(valueNode);
+ wrapper.LogicalType = DictLogicalType(std::move(key.LogicalType), std::move(value.LogicalType));
+ return;
+ }
+ case ELogicalMetatype::Tagged: {
+ auto tagNode = mapNode->GetChildOrThrow("tag");
+ auto tag = NYTree::ConvertTo<TString>(tagNode);
+ auto elementNode = mapNode->GetChildOrThrow("item");
+ auto element = NYTree::ConvertTo<TTypeV3LogicalTypeWrapper>(elementNode);
+ wrapper.LogicalType = TaggedLogicalType(std::move(tag), std::move(element.LogicalType));
+ return;
+ }
+ }
+ YT_ABORT();
+ }
+ }, typeName);
+}
+
+void DeserializeV3Impl(TLogicalTypePtr& type, TYsonPullParserCursor* cursor, int depth)
+{
+ // Check depth early to avoid stack overflow.
+ if (depth > MaxSchemaDepth) {
+ THROW_ERROR_EXCEPTION("Logical type exceeds depth limit during parsing")
+ << TErrorAttribute("limit", MaxSchemaDepth);
+ }
+
+ if ((*cursor)->GetType() == EYsonItemType::StringValue) {
+ auto typeNameString = (*cursor)->UncheckedAsString();
+ auto typeName = FromTypeV3(typeNameString);
+ std::visit([&](const auto& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, ESimpleLogicalValueType>) {
+ type = SimpleLogicalType(arg);
+ } else {
+ static_assert(std::is_same_v<T, ELogicalMetatype> || std::is_same_v<T, TV3Variant>);
+ THROW_ERROR_EXCEPTION("Type %Qv must be represented by map, not a string",
+ typeNameString);
+ }
+ }, typeName);
+ cursor->Next();
+ return;
+ }
+ if ((*cursor)->GetType() != EYsonItemType::BeginMap) {
+ THROW_ERROR_EXCEPTION("Error parsing logical type: expected %Qlv or %Qlv, actual %Qlv",
+ EYsonItemType::BeginMap,
+ EYsonItemType::BeginList,
+ (*cursor)->GetType());
+ }
+
+ std::optional<TV3TypeName> typeName;
+ std::optional<std::vector<TStructField>> members;
+ std::optional<std::vector<TLogicalTypePtr>> elements;
+ TLogicalTypePtr item, keyType, valueType;
+ std::optional<i64> precision, scale;
+ std::optional<TString> tag;
+
+ cursor->ParseMap([&] (TYsonPullParserCursor* cursor) {
+ EnsureYsonToken(TStringBuf("logical type attribute key"), *cursor, EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+ if (key == TStringBuf("type_name")) {
+ cursor->Next();
+ EnsureYsonToken(TStringBuf("logical type name"), *cursor, EYsonItemType::StringValue);
+ typeName = FromTypeV3((*cursor)->UncheckedAsString());
+ cursor->Next();
+ } else if (key == TStringBuf("item")) {
+ cursor->Next();
+ DeserializeV3Impl(item, cursor, depth + 1);
+ } else if (key == TStringBuf("members")) {
+ cursor->Next();
+ members.emplace();
+ cursor->ParseList([&] (TYsonPullParserCursor* cursor) {
+ std::optional<TString> name;
+ TLogicalTypePtr type;
+ cursor->ParseMap([&] (TYsonPullParserCursor* cursor) {
+ EnsureYsonToken(TStringBuf("logical type member attribute key"), *cursor, EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+ if (key == TStringBuf("name")) {
+ cursor->Next();
+ name = ExtractTo<TString>(cursor);
+ } else if (key == TStringBuf("type")) {
+ cursor->Next();
+ DeserializeV3Impl(type, cursor, depth + 1);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+ if (!name) {
+ THROW_ERROR_EXCEPTION("Name is required");
+ }
+ if (!type) {
+ THROW_ERROR_EXCEPTION("Type is required");
+ }
+ members->push_back({std::move(*name), std::move(type)});
+ });
+ } else if (key == TStringBuf("elements")) {
+ cursor->Next();
+ elements.emplace();
+ cursor->ParseList([&] (TYsonPullParserCursor* cursor) {
+ TLogicalTypePtr type;
+ cursor->ParseMap([&] (TYsonPullParserCursor* cursor) {
+ EnsureYsonToken(TStringBuf("logical type member attribute key"), *cursor, EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+ if (key == TStringBuf("type")) {
+ cursor->Next();
+ DeserializeV3Impl(type, cursor, depth + 1);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+ if (!type) {
+ THROW_ERROR_EXCEPTION("Type is required");
+ }
+ elements->push_back(std::move(type));
+ });
+ } else if (key == TStringBuf("precision")) {
+ cursor->Next();
+ precision = ExtractTo<i64>(cursor);
+ } else if (key == TStringBuf("scale")) {
+ cursor->Next();
+ scale = ExtractTo<i64>(cursor);
+ } else if (key == TStringBuf("key")) {
+ cursor->Next();
+ DeserializeV3Impl(keyType, cursor, depth + 1);
+ } else if (key == TStringBuf("value")) {
+ cursor->Next();
+ DeserializeV3Impl(valueType, cursor, depth + 1);
+ } else if (key == TStringBuf("tag")) {
+ cursor->Next();
+ tag = ExtractTo<TString>(cursor);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+
+ if (!typeName) {
+ THROW_ERROR_EXCEPTION("\"type_name\" is required");
+ }
+
+ type = std::visit([&](const auto& typeName) {
+ using T = std::decay_t<decltype(typeName)>;
+ if constexpr (std::is_same_v<T, ESimpleLogicalValueType>) {
+ return SimpleLogicalType(typeName);
+ } else {
+ ELogicalMetatype type;
+ if constexpr (std::is_same_v<T, ELogicalMetatype>) {
+ type = typeName;
+ } else {
+ if (members && elements) {
+ THROW_ERROR_EXCEPTION("\"variant\" cannot have both children \"elements\" and \"members\"");
+ } else if (members) {
+ type = ELogicalMetatype::VariantStruct;
+ } else if (elements) {
+ type = ELogicalMetatype::VariantTuple;
+ } else {
+ THROW_ERROR_EXCEPTION("\"variant\" must have \"elements\" or \"members\" child");
+ }
+ }
+ auto ensureIsPresent = [&] (const char* fieldName, const auto& value) {
+ if (!value) {
+ THROW_ERROR_EXCEPTION("Field %Qv is required for logical type %Qlv",
+ fieldName,
+ type);
+ }
+ };
+ switch (type) {
+ case ELogicalMetatype::Simple:
+ // NB. FromTypeV3 never returns this value.
+ YT_ABORT();
+ case ELogicalMetatype::Decimal:
+ ensureIsPresent("precision", precision);
+ ensureIsPresent("scale", scale);
+ return DecimalLogicalType(*precision, *scale);
+ case ELogicalMetatype::Optional:
+ ensureIsPresent("item", item);
+ return OptionalLogicalType(std::move(item));
+ case ELogicalMetatype::List:
+ ensureIsPresent("item", item);
+ return ListLogicalType(std::move(item));
+ case ELogicalMetatype::Struct:
+ ensureIsPresent("members", members);
+ return StructLogicalType(std::move(*members));
+ case ELogicalMetatype::VariantStruct:
+ ensureIsPresent("members", members);
+ return VariantStructLogicalType(std::move(*members));
+ case ELogicalMetatype::Tuple:
+ ensureIsPresent("elements", elements);
+ return TupleLogicalType(std::move(*elements));
+ case ELogicalMetatype::VariantTuple:
+ ensureIsPresent("elements", elements);
+ return VariantTupleLogicalType(std::move(*elements));
+ case ELogicalMetatype::Dict:
+ ensureIsPresent("key", keyType);
+ ensureIsPresent("value", valueType);
+ return DictLogicalType(std::move(keyType), std::move(valueType));
+ case ELogicalMetatype::Tagged:
+ ensureIsPresent("tag", tag);
+ ensureIsPresent("item", item);
+ return TaggedLogicalType(std::move(*tag), std::move(item));
+ }
+ YT_ABORT();
+ }
+ }, *typeName);
+}
+
+void Deserialize(TTypeV3LogicalTypeWrapper& wrapper, TYsonPullParserCursor* cursor)
+{
+ DeserializeV3(wrapper.LogicalType, cursor);
+}
+
+void DeserializeV3(TLogicalTypePtr& type, TYsonPullParserCursor* cursor)
+{
+ DeserializeV3Impl(type, cursor, /*depth*/ 0);
+}
+
+void Deserialize(TLogicalTypePtr& type, NYson::TYsonPullParserCursor* cursor)
+{
+ YT_VERIFY(UseTypeV3ForSerialization);
+ DeserializeV3(type, cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleTypeStore
+{
+public:
+ TSimpleTypeStore()
+ {
+ for (auto simpleLogicalType : TEnumTraits<ESimpleLogicalValueType>::GetDomainValues()) {
+ auto logicalType = New<TSimpleLogicalType>(simpleLogicalType);
+ SimpleTypeMap_[simpleLogicalType] = logicalType;
+ OptionalTypeMap_[simpleLogicalType] = New<TOptionalLogicalType>(logicalType);
+ }
+ }
+
+ const TLogicalTypePtr& GetSimpleType(ESimpleLogicalValueType type)
+ {
+ return GetOrCrash(SimpleTypeMap_, type);
+ }
+
+ const TLogicalTypePtr& GetOptionalType(ESimpleLogicalValueType type)
+ {
+ return GetOrCrash(OptionalTypeMap_, type);
+ }
+
+private:
+ THashMap<ESimpleLogicalValueType, TLogicalTypePtr> SimpleTypeMap_;
+ THashMap<ESimpleLogicalValueType, TLogicalTypePtr> OptionalTypeMap_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLogicalTypePtr OptionalLogicalType(TLogicalTypePtr element)
+{
+ if (element->GetMetatype() == ELogicalMetatype::Simple) {
+ auto simpleLogicalType = element->AsSimpleTypeRef().GetElement();
+ if (element.Get() == Singleton<TSimpleTypeStore>()->GetSimpleType(simpleLogicalType).Get()) {
+ return Singleton<TSimpleTypeStore>()->GetOptionalType(simpleLogicalType);
+ }
+ }
+ return New<TOptionalLogicalType>(std::move(element));
+}
+
+TLogicalTypePtr MakeOptionalIfNot(TLogicalTypePtr element)
+{
+ if (element->GetMetatype() == ELogicalMetatype::Optional) {
+ return element;
+ }
+ return OptionalLogicalType(std::move(element));
+}
+
+TLogicalTypePtr SimpleLogicalType(ESimpleLogicalValueType element)
+{
+ return Singleton<TSimpleTypeStore>()->GetSimpleType(element);
+}
+
+TLogicalTypePtr DecimalLogicalType(int precision, int scale)
+{
+ return New<TDecimalLogicalType>(precision, scale);
+}
+
+TLogicalTypePtr MakeLogicalType(ESimpleLogicalValueType element, bool required)
+{
+ if (element == ESimpleLogicalValueType::Null || element == ESimpleLogicalValueType::Void) {
+ if (required) {
+ THROW_ERROR_EXCEPTION("Null type cannot be required");
+ }
+ return Singleton<TSimpleTypeStore>()->GetSimpleType(element);
+ } else if (required) {
+ return Singleton<TSimpleTypeStore>()->GetSimpleType(element);
+ } else {
+ return Singleton<TSimpleTypeStore>()->GetOptionalType(element);
+ }
+}
+
+TLogicalTypePtr ListLogicalType(TLogicalTypePtr element)
+{
+ return New<TListLogicalType>(std::move(element));
+}
+
+TLogicalTypePtr StructLogicalType(std::vector<TStructField> fields)
+{
+ return New<TStructLogicalType>(std::move(fields));
+}
+
+TLogicalTypePtr TupleLogicalType(std::vector<TLogicalTypePtr> elements)
+{
+ return New<TTupleLogicalType>(std::move(elements));
+}
+
+TLogicalTypePtr VariantTupleLogicalType(std::vector<TLogicalTypePtr> elements)
+{
+ return New<TVariantTupleLogicalType>(std::move(elements));
+}
+
+TLogicalTypePtr VariantStructLogicalType(std::vector<TStructField> fields)
+{
+ return New<TVariantStructLogicalType>(std::move(fields));
+}
+
+TLogicalTypePtr DictLogicalType(TLogicalTypePtr key, TLogicalTypePtr value)
+{
+ return New<TDictLogicalType>(std::move(key), std::move(value));
+}
+
+TLogicalTypePtr TaggedLogicalType(TString tag, TLogicalTypePtr element)
+{
+ return New<TTaggedLogicalType>(std::move(tag), std::move(element));
+}
+
+TLogicalTypePtr NullLogicalType()
+{
+ return Singleton<TSimpleTypeStore>()->GetSimpleType(ESimpleLogicalValueType::Null);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+static inline size_t GetHash(
+ const THash<NYT::NTableClient::TLogicalType>& hasher,
+ const std::vector<NYT::NTableClient::TLogicalTypePtr>& elements)
+{
+ size_t result = 0;
+ for (const auto& element : elements) {
+ result = CombineHashes(result, hasher(*element));
+ }
+ return result;
+}
+
+static inline size_t GetHash(
+ const THash<NYT::NTableClient::TLogicalType>& hasher,
+ const std::vector<NYT::NTableClient::TStructField>& fields)
+{
+ size_t result = 0;
+ for (const auto& field : fields) {
+ result = CombineHashes(result, THash<TString>{}(field.Name));
+ result = CombineHashes(result, hasher(*field.Type));
+ }
+ return result;
+}
+
+size_t THash<NYT::NTableClient::TLogicalType>::operator()(const NYT::NTableClient::TLogicalType& logicalType) const
+{
+ using namespace NYT::NTableClient;
+ const auto typeHash = static_cast<size_t>(logicalType.GetMetatype());
+ switch (logicalType.GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ return CombineHashes(static_cast<size_t>(logicalType.AsSimpleTypeRef().GetElement()), typeHash);
+ case ELogicalMetatype::Decimal:
+ return CombineHashes(
+ CombineHashes(
+ static_cast<size_t>(logicalType.AsDecimalTypeRef().GetPrecision()),
+ static_cast<size_t>(logicalType.AsDecimalTypeRef().GetScale())
+ ),
+ typeHash);
+ case ELogicalMetatype::Optional:
+ return CombineHashes((*this)(*logicalType.AsOptionalTypeRef().GetElement()), typeHash);
+ case ELogicalMetatype::List:
+ return CombineHashes((*this)(*logicalType.AsListTypeRef().GetElement()), typeHash);
+ case ELogicalMetatype::Struct:
+ return CombineHashes(GetHash(*this, logicalType.AsStructTypeRef().GetFields()), typeHash);
+ case ELogicalMetatype::Tuple:
+ return CombineHashes(GetHash(*this, logicalType.AsTupleTypeRef().GetElements()), typeHash);
+ case ELogicalMetatype::VariantStruct:
+ return CombineHashes(GetHash(*this, logicalType.AsVariantStructTypeRef().GetFields()), typeHash);
+ case ELogicalMetatype::VariantTuple:
+ return CombineHashes(GetHash(*this, logicalType.AsVariantTupleTypeRef().GetElements()), typeHash);
+ case ELogicalMetatype::Dict:
+ return CombineHashes(
+ CombineHashes(
+ (*this)(*logicalType.AsDictTypeRef().GetKey()),
+ (*this)(*logicalType.AsDictTypeRef().GetValue())
+ ),
+ typeHash);
+ case ELogicalMetatype::Tagged:
+ return CombineHashes(
+ CombineHashes(
+ THash<TString>()(logicalType.AsTaggedTypeRef().GetTag()),
+ (*this)(*logicalType.AsTaggedTypeRef().GetElement())
+ ),
+ typeHash);
+ }
+ YT_ABORT();
+}
diff --git a/yt/yt/client/table_client/logical_type.h b/yt/yt/client/table_client/logical_type.h
new file mode 100644
index 0000000000..807b851e0c
--- /dev/null
+++ b/yt/yt/client/table_client/logical_type.h
@@ -0,0 +1,447 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/table_client/row_base.h>
+
+#include <yt/yt/core/yson/public.h>
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <util/generic/hash.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWalkContext;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ELogicalMetatype,
+ (Simple)
+ (Optional)
+ (List)
+ (Struct)
+ (Tuple)
+
+ // Variant with named elements.
+ (VariantStruct)
+
+ // Variant with unnamed elements.
+ (VariantTuple)
+
+ // Dict is effectively list of pairs. We have restrictions on the key type.
+ // YT doesn't check uniqueness of the keys.
+ (Dict)
+
+ (Tagged)
+
+ (Decimal)
+);
+
+class TLogicalType
+ : public virtual TRefCounted
+{
+public:
+ explicit TLogicalType(ELogicalMetatype type);
+ Y_FORCE_INLINE ELogicalMetatype GetMetatype() const;
+
+ const TSimpleLogicalType& AsSimpleTypeRef() const;
+ Y_FORCE_INLINE const TSimpleLogicalType& UncheckedAsSimpleTypeRef() const;
+ const TDecimalLogicalType& AsDecimalTypeRef() const;
+ Y_FORCE_INLINE const TDecimalLogicalType& UncheckedAsDecimalTypeRef() const;
+ const TOptionalLogicalType& AsOptionalTypeRef() const;
+ Y_FORCE_INLINE const TOptionalLogicalType& UncheckedAsOptionalTypeRef() const;
+ const TListLogicalType& AsListTypeRef() const;
+ Y_FORCE_INLINE const TListLogicalType& UncheckedAsListTypeRef() const;
+ const TStructLogicalType& AsStructTypeRef() const;
+ Y_FORCE_INLINE const TStructLogicalType& UncheckedAsStructTypeRef() const;
+ const TTupleLogicalType& AsTupleTypeRef() const;
+ Y_FORCE_INLINE const TTupleLogicalType& UncheckedAsTupleTypeRef() const;
+ const TVariantTupleLogicalType& AsVariantTupleTypeRef() const;
+ Y_FORCE_INLINE const TVariantTupleLogicalType& UncheckedAsVariantTupleTypeRef() const;
+ const TVariantStructLogicalType& AsVariantStructTypeRef() const;
+ Y_FORCE_INLINE const TVariantStructLogicalType& UncheckedAsVariantStructTypeRef() const;
+ const TDictLogicalType& AsDictTypeRef() const;
+ Y_FORCE_INLINE const TDictLogicalType& UncheckedAsDictTypeRef() const;
+ const TTaggedLogicalType& AsTaggedTypeRef() const;
+ Y_FORCE_INLINE const TTaggedLogicalType& UncheckedAsTaggedTypeRef() const;
+
+ virtual size_t GetMemoryUsage() const = 0;
+ virtual int GetTypeComplexity() const = 0;
+
+ // This function doesn't validate children of current node.
+ // Users should use ValidateLogicalType function.
+ virtual void ValidateNode(const TWalkContext& context) const = 0;
+
+ // Whether or not this type can have null value.
+ virtual bool IsNullable() const = 0;
+
+ //
+ // Additional helpers to decompose complex type.
+ // Logical type MUST have appropriate metatype otherwise abort() will be called.
+
+ //
+ // Return underlying element for Optional,List,Tagged.
+ const TLogicalTypePtr& GetElement() const;
+
+ //
+ // Return elements for Tuple,VariantTuple
+ const std::vector<TLogicalTypePtr>& GetElements() const;
+
+ //
+ // Return fields for Struct,VariantStruct
+ const std::vector<TStructField>& GetFields() const;
+
+private:
+ const ELogicalMetatype Metatype_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TLogicalType)
+
+TString ToString(const TLogicalType& logicalType);
+
+//! Debug printers for Gtest unittests.
+void PrintTo(ELogicalMetatype type, std::ostream* os);
+void PrintTo(const TLogicalType& type, std::ostream* os);
+void PrintTo(const TLogicalTypePtr& type, std::ostream* os);
+
+bool operator == (const TLogicalType& lhs, const TLogicalType& rhs);
+bool operator != (const TLogicalType& lhs, const TLogicalType& rhs);
+bool operator == (const TLogicalTypePtr& lhs, const TLogicalTypePtr& rhs) = delete;
+bool operator != (const TLogicalTypePtr& lhs, const TLogicalTypePtr& rhs) = delete;
+
+void ValidateLogicalType(const TComplexTypeFieldDescriptor& descriptor, std::optional<int> depthLimit = std::nullopt);
+
+// Function converts new type to old typesystem.
+// The first element of result is ESimpleLogicalValue type corresponding to logicalType
+// (as seen in `type` field of column schema).
+// The second element of result is false if logicalType is Null or it is optional<A> where A is any type otherwise it's true.
+std::pair<ESimpleLogicalValueType, bool> CastToV1Type(const TLogicalTypePtr& logicalType);
+
+EValueType GetWireType(const TLogicalTypePtr& logicalType);
+
+// Return true if given type is pure v1 type (i.e. expressible with `type` and `required` fields in schema).
+bool IsV1Type(const TLogicalTypePtr& logicalType);
+
+// Return true if this is new type expressible with EValueType::Composite type.
+bool IsV3Composite(const TLogicalTypePtr& logicalType);
+
+EValueType GetWireType(const TLogicalTypePtr& logicalType);
+
+// Try to remove top level optional type if that doesn't change UnversionedValue representation
+// of non null types.
+TLogicalTypePtr DenullifyLogicalType(const TLogicalTypePtr& logicalType);
+
+// Returns copy of the logical type with all tagged types replaces with its elements.
+TLogicalTypePtr DetagLogicalType(const TLogicalTypePtr& logicalType);
+
+void ToProto(NProto::TLogicalType* protoLogicalType, const TLogicalTypePtr& logicalType);
+void FromProto(TLogicalTypePtr* logicalType, const NProto::TLogicalType& protoLogicalType);
+
+bool IsComparable(const TLogicalTypePtr& type);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Special wrapper class that allows to serialize LogicalType in type_v3 format
+// https://a.yandex-team.ru/arc/trunk/arcadia/logfeller/mvp/docs/types_serialization.md
+struct TTypeV3LogicalTypeWrapper
+{
+ TLogicalTypePtr LogicalType;
+};
+
+void Serialize(const TTypeV3LogicalTypeWrapper& wrapper, NYson::IYsonConsumer* consumer);
+void Deserialize(TTypeV3LogicalTypeWrapper& wrapper, NYTree::INodePtr node);
+void Deserialize(TTypeV3LogicalTypeWrapper& wrapper, NYson::TYsonPullParserCursor* cursor);
+
+void Serialize(const TLogicalTypePtr& type, NYson::IYsonConsumer* consumer);
+void Deserialize(TLogicalTypePtr& type, NYTree::INodePtr node);
+
+void DeserializeV3(TLogicalTypePtr& type, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(TLogicalTypePtr& type, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecimalLogicalType
+ : public TLogicalType
+{
+public:
+ static constexpr int MinPrecision = 1;
+ static constexpr int MaxPrecision = 35;
+
+public:
+ TDecimalLogicalType(int precision, int scale);
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+ Y_FORCE_INLINE int GetPrecision() const;
+ Y_FORCE_INLINE int GetScale() const;
+
+private:
+ const int Precision_;
+ const int Scale_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOptionalLogicalType
+ : public TLogicalType
+{
+public:
+ explicit TOptionalLogicalType(TLogicalTypePtr element);
+
+ Y_FORCE_INLINE const TLogicalTypePtr& GetElement() const;
+
+ std::optional<ESimpleLogicalValueType> Simplify() const;
+
+ // Cached value of GetElement()->IsNullable(), useful for performance reasons.
+ Y_FORCE_INLINE bool IsElementNullable() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ const TLogicalTypePtr Element_;
+ const bool ElementIsNullable_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleLogicalType
+ : public TLogicalType
+{
+public:
+ explicit TSimpleLogicalType(ESimpleLogicalValueType element);
+
+ Y_FORCE_INLINE ESimpleLogicalValueType GetElement() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ ESimpleLogicalValueType Element_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListLogicalType
+ : public TLogicalType
+{
+public:
+ explicit TListLogicalType(TLogicalTypePtr element);
+
+ Y_FORCE_INLINE const TLogicalTypePtr& GetElement() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ TLogicalTypePtr Element_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Class builds descriptors of a complex value field.
+// Such descriptors are useful for generating error messages when working with complex types.
+class TComplexTypeFieldDescriptor
+{
+public:
+ explicit TComplexTypeFieldDescriptor(TLogicalTypePtr type);
+ explicit TComplexTypeFieldDescriptor(const TColumnSchema& column);
+ TComplexTypeFieldDescriptor(TString columnName, TLogicalTypePtr type);
+
+ TComplexTypeFieldDescriptor OptionalElement() const;
+ TComplexTypeFieldDescriptor ListElement() const;
+ TComplexTypeFieldDescriptor Element(size_t i) const;
+ TComplexTypeFieldDescriptor TupleElement(size_t i) const;
+ TComplexTypeFieldDescriptor VariantTupleElement(size_t i) const;
+ TComplexTypeFieldDescriptor Field(size_t i) const;
+ TComplexTypeFieldDescriptor StructField(size_t i) const;
+ TComplexTypeFieldDescriptor VariantStructField(size_t i) const;
+ TComplexTypeFieldDescriptor DictKey() const;
+ TComplexTypeFieldDescriptor DictValue() const;
+ TComplexTypeFieldDescriptor TaggedElement() const;
+
+ TComplexTypeFieldDescriptor Detag() const;
+
+ const TString& GetDescription() const;
+ const TLogicalTypePtr& GetType() const;
+
+private:
+ TString Descriptor_;
+ TLogicalTypePtr Type_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TStructField
+{
+ TString Name;
+ TLogicalTypePtr Type;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Base class for struct and named variant.
+class TStructLogicalTypeBase
+ : public TLogicalType
+{
+public:
+
+public:
+ TStructLogicalTypeBase(ELogicalMetatype metatype, std::vector<TStructField> fields);
+ Y_FORCE_INLINE const std::vector<TStructField>& GetFields() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ std::vector<TStructField> Fields_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTupleLogicalTypeBase
+ : public TLogicalType
+{
+public:
+ explicit TTupleLogicalTypeBase(ELogicalMetatype metatype, std::vector<TLogicalTypePtr> elements);
+
+ Y_FORCE_INLINE const std::vector<TLogicalTypePtr>& GetElements() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ std::vector<TLogicalTypePtr> Elements_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStructLogicalType
+ : public TStructLogicalTypeBase
+{
+public:
+ TStructLogicalType(std::vector<TStructField> fields);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTupleLogicalType
+ : public TTupleLogicalTypeBase
+{
+public:
+ TTupleLogicalType(std::vector<TLogicalTypePtr> elements);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVariantStructLogicalType
+ : public TStructLogicalTypeBase
+{
+public:
+ explicit TVariantStructLogicalType(std::vector<TStructField> fields);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVariantTupleLogicalType
+ : public TTupleLogicalTypeBase
+{
+public:
+ explicit TVariantTupleLogicalType(std::vector<TLogicalTypePtr> elements);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDictLogicalType
+ : public TLogicalType
+{
+public:
+ TDictLogicalType(TLogicalTypePtr key, TLogicalTypePtr value);
+
+ Y_FORCE_INLINE const TLogicalTypePtr& GetKey() const;
+ Y_FORCE_INLINE const TLogicalTypePtr& GetValue() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ TLogicalTypePtr Key_;
+ TLogicalTypePtr Value_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTaggedLogicalType
+ : public TLogicalType
+{
+public:
+ TTaggedLogicalType(TString tag, TLogicalTypePtr element);
+
+ Y_FORCE_INLINE const TString& GetTag() const;
+ Y_FORCE_INLINE const TLogicalTypePtr& GetElement() const;
+
+ size_t GetMemoryUsage() const override;
+ int GetTypeComplexity() const override;
+ void ValidateNode(const TWalkContext& context) const override;
+ bool IsNullable() const override;
+
+private:
+ const TString Tag_;
+ const TLogicalTypePtr Element_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLogicalTypePtr SimpleLogicalType(ESimpleLogicalValueType element);
+TLogicalTypePtr DecimalLogicalType(int precision, int scale);
+TLogicalTypePtr OptionalLogicalType(TLogicalTypePtr element);
+TLogicalTypePtr ListLogicalType(TLogicalTypePtr element);
+TLogicalTypePtr StructLogicalType(std::vector<TStructField> fields);
+TLogicalTypePtr TupleLogicalType(std::vector<TLogicalTypePtr> elements);
+TLogicalTypePtr VariantStructLogicalType(std::vector<TStructField> fields);
+TLogicalTypePtr VariantTupleLogicalType(std::vector<TLogicalTypePtr> elements);
+TLogicalTypePtr DictLogicalType(TLogicalTypePtr key, TLogicalTypePtr value);
+TLogicalTypePtr TaggedLogicalType(TString tag, TLogicalTypePtr element);
+TLogicalTypePtr NullLogicalType();
+
+TLogicalTypePtr MakeOptionalIfNot(TLogicalTypePtr element);
+
+// Creates logical type from legacy schema fields.
+// IMPORTANT: Only used for compatibility reasons.
+// In modern code, one should use OptionalLogicalType + SimpleLogicalType instead.
+TLogicalTypePtr MakeLogicalType(ESimpleLogicalValueType type, bool required);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+template <>
+struct THash<NYT::NTableClient::TLogicalType>
+{
+ size_t operator() (const NYT::NTableClient::TLogicalType& logicalType) const;
+};
+
+#define LOGICAL_TYPE_INL_H_
+#include "logical_type-inl.h"
+#undef LOGICAL_TYPE_INL_H_
diff --git a/yt/yt/client/table_client/name_table.cpp b/yt/yt/client/table_client/name_table.cpp
new file mode 100644
index 0000000000..d8918deb85
--- /dev/null
+++ b/yt/yt/client/table_client/name_table.cpp
@@ -0,0 +1,304 @@
+#include "name_table.h"
+
+#include "column_sort_schema.h"
+#include "schema.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNameTablePtr TNameTable::FromSchema(const TTableSchema& schema)
+{
+ auto nameTable = New<TNameTable>();
+ nameTable->NameToId_.reserve(schema.Columns().size());
+ for (const auto& column : schema.Columns()) {
+ nameTable->DoRegisterNameOrThrow(column.Name());
+ }
+ return nameTable;
+}
+
+TNameTablePtr TNameTable::FromSchemaStable(const TTableSchema& schema)
+{
+ auto nameTable = New<TNameTable>();
+ nameTable->NameToId_.reserve(schema.Columns().size());
+ for (const auto& column : schema.Columns()) {
+ nameTable->DoRegisterNameOrThrow(column.StableName().Get());
+ }
+ return nameTable;
+}
+
+TNameTablePtr TNameTable::FromKeyColumns(const TKeyColumns& keyColumns)
+{
+ auto nameTable = New<TNameTable>();
+ nameTable->NameToId_.reserve(keyColumns.size());
+ for (const auto& name : keyColumns) {
+ nameTable->DoRegisterNameOrThrow(name);
+ }
+ return nameTable;
+}
+
+TNameTablePtr TNameTable::FromSortColumns(const TSortColumns& sortColumns)
+{
+ return TNameTable::FromKeyColumns(GetColumnNames(sortColumns));
+}
+
+int TNameTable::GetSize() const
+{
+ auto guard = Guard(SpinLock_);
+ return IdToName_.size();
+}
+
+i64 TNameTable::GetByteSize() const
+{
+ auto guard = Guard(SpinLock_);
+ return ByteSize_;
+}
+
+void TNameTable::SetEnableColumnNameValidation()
+{
+ auto guard = Guard(SpinLock_);
+ EnableColumnNameValidation_ = true;
+}
+
+std::optional<int> TNameTable::FindId(TStringBuf name) const
+{
+ auto guard = Guard(SpinLock_);
+ auto it = NameToId_.find(name);
+ if (it == NameToId_.end()) {
+ return std::nullopt;
+ } else {
+ return std::make_optional(it->second);
+ }
+}
+
+int TNameTable::GetIdOrThrow(TStringBuf name) const
+{
+ auto optionalId = FindId(name);
+ if (!optionalId) {
+ THROW_ERROR_EXCEPTION("No such column %Qv", name);
+ }
+ return *optionalId;
+}
+
+int TNameTable::GetId(TStringBuf name) const
+{
+ auto index = FindId(name);
+ YT_VERIFY(index);
+ return *index;
+}
+
+TStringBuf TNameTable::GetName(int id) const
+{
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(id >= 0 && id < std::ssize(IdToName_));
+ return IdToName_[id];
+}
+
+TStringBuf TNameTable::GetNameOrThrow(int id) const
+{
+ auto guard = Guard(SpinLock_);
+ if (id < 0 || id >= std::ssize(IdToName_)) {
+ THROW_ERROR_EXCEPTION("Invalid column requested from name table: expected in range [0, %v), got %v",
+ IdToName_.size(),
+ id);
+ }
+ return IdToName_[id];
+}
+
+int TNameTable::RegisterName(TStringBuf name)
+{
+ auto guard = Guard(SpinLock_);
+ return DoRegisterName(name);
+}
+
+int TNameTable::RegisterNameOrThrow(TStringBuf name)
+{
+ auto guard = Guard(SpinLock_);
+ return DoRegisterNameOrThrow(name);
+}
+
+int TNameTable::GetIdOrRegisterName(TStringBuf name)
+{
+ auto guard = Guard(SpinLock_);
+ auto it = NameToId_.find(name);
+ if (it == NameToId_.end()) {
+ return DoRegisterName(name);
+ } else {
+ return it->second;
+ }
+}
+
+int TNameTable::DoRegisterName(TStringBuf name)
+{
+ int id = IdToName_.size();
+
+ if (id >= MaxColumnId) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::CorruptedNameTable,
+ "Cannot register column %Qv: column limit exceeded",
+ name)
+ << TErrorAttribute("max_column_id", MaxColumnId);
+ }
+
+ if (EnableColumnNameValidation_ && name.length() > MaxColumnNameLength) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::CorruptedNameTable,
+ "Cannot register column %Qv: column name is too long",
+ name)
+ << TErrorAttribute("max_column_name_length", MaxColumnNameLength);
+ }
+
+ const auto& savedName = IdToName_.emplace_back(name);
+ YT_VERIFY(NameToId_.emplace(savedName, id).second);
+ ByteSize_ += savedName.length();
+ return id;
+}
+
+int TNameTable::DoRegisterNameOrThrow(TStringBuf name)
+{
+ auto optionalId = NameToId_.find(name);
+ if (optionalId != NameToId_.end()) {
+ THROW_ERROR_EXCEPTION("Cannot register column %Qv: column already exists", name);
+ }
+ return DoRegisterName(name);
+}
+
+std::vector<TString> TNameTable::GetNames() const
+{
+ auto guard = Guard(SpinLock_);
+ std::vector<TString> result(IdToName_.begin(), IdToName_.end());
+ return result;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TNameTable& nameTable, TStringBuf /*spec*/)
+{
+ builder->AppendChar('{');
+ bool first = true;
+ for (const auto& name : nameTable.GetNames()) {
+ if (first) {
+ first = false;
+ } else {
+ builder->AppendString("; ");
+ }
+ builder->AppendFormat("%Qv=%v", name, nameTable.GetId(name));
+ }
+ builder->AppendChar('}');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNameTableReader::TNameTableReader(TNameTablePtr nameTable)
+ : NameTable_(std::move(nameTable))
+{
+ Fill();
+}
+
+TStringBuf TNameTableReader::FindName(int id) const
+{
+ if (id < 0) {
+ return {};
+ }
+
+ if (id >= std::ssize(IdToNameCache_)) {
+ Fill();
+
+ if (id >= std::ssize(IdToNameCache_)) {
+ return {};
+ }
+ }
+
+ return IdToNameCache_[id];
+}
+
+TStringBuf TNameTableReader::GetName(int id) const
+{
+ YT_ASSERT(id >= 0);
+ if (id >= std::ssize(IdToNameCache_)) {
+ Fill();
+ }
+
+ YT_ASSERT(id < std::ssize(IdToNameCache_));
+ return IdToNameCache_[id];
+}
+
+int TNameTableReader::GetSize() const
+{
+ Fill();
+ return static_cast<int>(IdToNameCache_.size());
+}
+
+void TNameTableReader::Fill() const
+{
+ int thisSize = static_cast<int>(IdToNameCache_.size());
+ int underlyingSize = NameTable_->GetSize();
+ for (int id = thisSize; id < underlyingSize; ++id) {
+ IdToNameCache_.push_back(std::string(NameTable_->GetName(id)));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNameTableWriter::TNameTableWriter(TNameTablePtr nameTable)
+ : NameTable_(std::move(nameTable))
+{ }
+
+std::optional<int> TNameTableWriter::FindId(TStringBuf name) const
+{
+ auto it = NameToId_.find(name);
+ if (it != NameToId_.end()) {
+ return it->second;
+ }
+
+ auto optionalId = NameTable_->FindId(name);
+ if (optionalId) {
+ Names_.push_back(TString(name));
+ YT_VERIFY(NameToId_.emplace(Names_.back(), *optionalId).second);
+ }
+ return optionalId;
+}
+
+int TNameTableWriter::GetIdOrThrow(TStringBuf name) const
+{
+ auto optionalId = FindId(name);
+ if (!optionalId) {
+ THROW_ERROR_EXCEPTION("No such column %Qv", name);
+ }
+ return *optionalId;
+}
+
+int TNameTableWriter::GetIdOrRegisterName(TStringBuf name)
+{
+ auto it = NameToId_.find(name);
+ if (it != NameToId_.end()) {
+ return it->second;
+ }
+
+ auto id = NameTable_->GetIdOrRegisterName(name);
+ Names_.push_back(TString(name));
+ YT_VERIFY(NameToId_.emplace(Names_.back(), id).second);
+ return id;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TNameTableExt* protoNameTable, const TNameTablePtr& nameTable)
+{
+ using NYT::ToProto;
+
+ ToProto(protoNameTable->mutable_names(), nameTable->GetNames());
+}
+
+void FromProto(TNameTablePtr* nameTable, const NProto::TNameTableExt& protoNameTable)
+{
+ using NYT::FromProto;
+
+ *nameTable = TNameTable::FromKeyColumns(FromProto<std::vector<TString>>(protoNameTable.names()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/name_table.h b/yt/yt/client/table_client/name_table.h
new file mode 100644
index 0000000000..feeabede18
--- /dev/null
+++ b/yt/yt/client/table_client/name_table.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A thread-safe id-to-name mapping.
+class TNameTable
+ : public virtual TRefCounted
+{
+public:
+ static TNameTablePtr FromSchema(const TTableSchema& schema);
+ static TNameTablePtr FromKeyColumns(const TKeyColumns& keyColumns);
+ static TNameTablePtr FromSortColumns(const TSortColumns& sortColumns);
+
+ // TODO(levysotsky): This one is currently used only for
+ // chunk name table. A separate type to chunk name table
+ // may be handy.
+ static TNameTablePtr FromSchemaStable(const TTableSchema& schema);
+
+ int GetSize() const;
+ i64 GetByteSize() const;
+
+ void SetEnableColumnNameValidation();
+
+ std::optional<int> FindId(TStringBuf name) const;
+ int GetIdOrThrow(TStringBuf name) const;
+ int GetId(TStringBuf name) const;
+ int RegisterName(TStringBuf name);
+ int RegisterNameOrThrow(TStringBuf name);
+ int GetIdOrRegisterName(TStringBuf name);
+
+ TStringBuf GetName(int id) const;
+ TStringBuf GetNameOrThrow(int id) const;
+
+ std::vector<TString> GetNames() const;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ bool EnableColumnNameValidation_ = false;
+
+ // String values are owned by IdToName_.
+ // NB: Names may be SSO-strings, using a deque to avoid string view invalidation.
+ std::deque<TString> IdToName_;
+ THashMap<TStringBuf, int> NameToId_;
+ i64 ByteSize_ = 0;
+
+ int DoRegisterName(TStringBuf name);
+ int DoRegisterNameOrThrow(TStringBuf name);
+};
+
+DEFINE_REFCOUNTED_TYPE(TNameTable)
+
+void FormatValue(TStringBuilderBase* builder, const TNameTable& nameTable, TStringBuf spec);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A non thread-safe read-only wrapper for TNameTable.
+class TNameTableReader
+ : private TNonCopyable
+{
+public:
+ explicit TNameTableReader(TNameTablePtr nameTable);
+
+ TStringBuf FindName(int id) const;
+ TStringBuf GetName(int id) const;
+ int GetSize() const;
+
+private:
+ const TNameTablePtr NameTable_;
+
+ mutable std::deque<std::string> IdToNameCache_; // Addresses of string data are immutable.
+
+ void Fill() const;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A non thread-safe read-write wrapper for TNameTable.
+class TNameTableWriter
+{
+public:
+ explicit TNameTableWriter(TNameTablePtr nameTable);
+
+ std::optional<int> FindId(TStringBuf name) const;
+ int GetIdOrThrow(TStringBuf name) const;
+ int GetIdOrRegisterName(TStringBuf name);
+
+private:
+ const TNameTablePtr NameTable_;
+
+ // String values are owned by Names_
+ // NB: Names may be SSO-strings, using a deque to avoid string view invalidation
+ mutable std::deque<TString> Names_;
+ mutable THashMap<TStringBuf, int> NameToId_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TNameTableExt* protoNameTable, const TNameTablePtr& nameTable);
+void ToProto(NProto::TNameTableExt* protoNameTable, const TNameTablePtr& nameTable);
+void FromProto(TNameTablePtr* nameTable, const NProto::TNameTableExt& protoNameTable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/pipe.cpp b/yt/yt/client/table_client/pipe.cpp
new file mode 100644
index 0000000000..232be888ce
--- /dev/null
+++ b/yt/yt/client/table_client/pipe.cpp
@@ -0,0 +1,292 @@
+#include "pipe.h"
+
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/misc/ring_queue.h>
+
+namespace NYT::NTableClient {
+
+using NChunkClient::NProto::TDataStatistics;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSchemafulPipeBufferTag
+{ };
+
+struct TSchemafulPipe::TData
+ : public TRefCounted
+{
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock);
+
+ const TRowBufferPtr RowBuffer;
+ TRingQueue<TUnversionedRow> RowQueue;
+
+ TPromise<void> ReaderReadyEvent;
+ TPromise<void> WriterReadyEvent = NewPromise<void>();
+
+ int RowsWritten = 0;
+ int RowsRead = 0;
+ bool WriterClosed = false;
+ bool Failed = false;
+
+ explicit TData(IMemoryChunkProviderPtr chunkProvider)
+ : RowBuffer(New<TRowBuffer>(TSchemafulPipeBufferTag(), std::move(chunkProvider)))
+ {
+ ResetReaderReadyEvent();
+ }
+
+ void ResetReaderReadyEvent()
+ {
+ ReaderReadyEvent = NewPromise<void>();
+ ReaderReadyEvent.OnCanceled(BIND([=, this, this_ = MakeStrong(this)] (const TError& error) {
+ Fail(TError(NYT::EErrorCode::Canceled, "Pipe reader canceled")
+ << error);
+ }));
+ }
+
+ void Fail(const TError& error)
+ {
+ YT_VERIFY(!error.IsOK());
+
+ TPromise<void> readerReadyEvent;
+ TPromise<void> writerReadyEvent;
+
+ {
+ auto guard = Guard(SpinLock);
+ if (WriterClosed || Failed)
+ return;
+
+ Failed = true;
+ readerReadyEvent = ReaderReadyEvent;
+ writerReadyEvent = WriterReadyEvent;
+ }
+
+ readerReadyEvent.TrySet(error);
+ writerReadyEvent.TrySet(error);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulPipe::TReader
+ : public ISchemafulUnversionedReader
+{
+public:
+ explicit TReader(TDataPtr data)
+ : Data_(std::move(data))
+ { }
+
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& options) override
+ {
+ std::vector<TUnversionedRow> rows;
+ rows.reserve(options.MaxRowsPerRead);
+ i64 dataWeight = 0;
+
+ {
+ auto guard = Guard(Data_->SpinLock);
+
+ if (Data_->WriterClosed && Data_->RowsWritten == Data_->RowsRead) {
+ return nullptr;
+ }
+
+ if (!Data_->Failed) {
+ auto& rowQueue = Data_->RowQueue;
+ while (!rowQueue.empty() &&
+ std::ssize(rows) < options.MaxRowsPerRead &&
+ dataWeight < options.MaxDataWeightPerRead)
+ {
+ auto row = rowQueue.front();
+ rowQueue.pop();
+ dataWeight += GetDataWeight(row);
+ rows.push_back(row);
+ ++Data_->RowsRead;
+ }
+ }
+
+ if (rows.empty()) {
+ ReadyEvent_ = Data_->ReaderReadyEvent.ToFuture();
+ }
+ }
+
+ return CreateBatchFromUnversionedRows(MakeSharedRange(std::move(rows), MakeStrong(this)));
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ return ReadyEvent_;
+ }
+
+ TDataStatistics GetDataStatistics() const override
+ {
+ return TDataStatistics();
+ }
+
+ NChunkClient::TCodecStatistics GetDecompressionStatistics() const override
+ {
+ return NChunkClient::TCodecStatistics();
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ return false;
+ }
+
+ std::vector<NChunkClient::TChunkId> GetFailedChunkIds() const override
+ {
+ return {};
+ }
+
+private:
+ const TDataPtr Data_;
+
+ TFuture<void> ReadyEvent_ = VoidFuture;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulPipe::TWriter
+ : public IUnversionedRowsetWriter
+{
+public:
+ explicit TWriter(TDataPtr data)
+ : Data_(std::move(data))
+ { }
+
+ TFuture<void> Close() override
+ {
+ TPromise<void> readerReadyEvent;
+ TPromise<void> writerReadyEvent;
+
+ bool doClose = false;
+
+ {
+ auto guard = Guard(Data_->SpinLock);
+
+ YT_VERIFY(!Data_->WriterClosed);
+ Data_->WriterClosed = true;
+
+ if (!Data_->Failed) {
+ doClose = true;
+ }
+
+ readerReadyEvent = Data_->ReaderReadyEvent;
+ writerReadyEvent = Data_->WriterReadyEvent;
+ }
+
+ readerReadyEvent.TrySet(TError());
+ if (doClose) {
+ writerReadyEvent.TrySet(TError());
+ }
+
+ return writerReadyEvent;
+ }
+
+ bool Write(TRange<TUnversionedRow> rows) override
+ {
+ // Copy data (no lock).
+ auto capturedRows = Data_->RowBuffer->CaptureRows(rows);
+
+ // Enqueue rows (with lock).
+ TPromise<void> readerReadyEvent;
+
+ {
+ auto guard = Guard(Data_->SpinLock);
+
+ YT_VERIFY(!Data_->WriterClosed);
+
+ if (Data_->Failed) {
+ return false;
+ }
+
+ for (auto row : capturedRows) {
+ Data_->RowQueue.push(row);
+ ++Data_->RowsWritten;
+ }
+
+ readerReadyEvent = std::move(Data_->ReaderReadyEvent);
+ Data_->ResetReaderReadyEvent();
+ }
+
+ // Signal readers.
+ readerReadyEvent.TrySet(TError());
+
+ return true;
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ // TODO(babenko): implement backpressure from reader
+ auto guard = Guard(Data_->SpinLock);
+ YT_VERIFY(Data_->Failed);
+ return Data_->WriterReadyEvent;
+ }
+
+private:
+ const TDataPtr Data_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchemafulPipe::TImpl
+ : public TRefCounted
+{
+public:
+ explicit TImpl(IMemoryChunkProviderPtr chunkProvider)
+ : Data_(New<TData>(std::move(chunkProvider)))
+ , Reader_(New<TReader>(Data_))
+ , Writer_(New<TWriter>(Data_))
+ { }
+
+ ISchemafulUnversionedReaderPtr GetReader() const
+ {
+ return Reader_;
+ }
+
+ IUnversionedRowsetWriterPtr GetWriter() const
+ {
+ return Writer_;
+ }
+
+ void Fail(const TError& error)
+ {
+ Data_->Fail(error);
+ }
+
+private:
+ TDataPtr Data_;
+ TReaderPtr Reader_;
+ TWriterPtr Writer_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemafulPipe::TSchemafulPipe(IMemoryChunkProviderPtr chunkProvider)
+ : Impl_(New<TImpl>(std::move(chunkProvider)))
+{ }
+
+TSchemafulPipe::~TSchemafulPipe() = default;
+
+ISchemafulUnversionedReaderPtr TSchemafulPipe::GetReader() const
+{
+ return Impl_->GetReader();
+}
+
+IUnversionedRowsetWriterPtr TSchemafulPipe::GetWriter() const
+{
+ return Impl_->GetWriter();
+}
+
+void TSchemafulPipe::Fail(const TError& error)
+{
+ Impl_->Fail(error);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/pipe.h b/yt/yt/client/table_client/pipe.h
new file mode 100644
index 0000000000..2c9ac00bf6
--- /dev/null
+++ b/yt/yt/client/table_client/pipe.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/memory/public.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A pipe connecting a schemaful writer to a schemaful reader.
+class TSchemafulPipe
+ : public TRefCounted
+{
+public:
+ explicit TSchemafulPipe(IMemoryChunkProviderPtr chunkProvider);
+ ~TSchemafulPipe();
+
+ //! Returns the reader side of the pipe.
+ ISchemafulUnversionedReaderPtr GetReader() const;
+
+ //! Returns the writer side of the pipe.
+ IUnversionedRowsetWriterPtr GetWriter() const;
+
+ //! When called, propagates the error to the reader.
+ void Fail(const TError& error);
+
+private:
+ class TImpl;
+ typedef TIntrusivePtr<TImpl> TImplPtr;
+
+ struct TData;
+ typedef TIntrusivePtr<TData> TDataPtr;
+
+ class TReader;
+ typedef TIntrusivePtr<TReader> TReaderPtr;
+
+ class TWriter;
+ typedef TIntrusivePtr<TWriter> TWriterPtr;
+
+
+ TIntrusivePtr<TImpl> Impl_;
+
+};
+
+DEFINE_REFCOUNTED_TYPE(TSchemafulPipe)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/public.cpp b/yt/yt/client/table_client/public.cpp
new file mode 100644
index 0000000000..648b4daa4f
--- /dev/null
+++ b/yt/yt/client/table_client/public.cpp
@@ -0,0 +1,19 @@
+#include "public.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString PrimaryLockName("<primary>");
+
+const TString SystemColumnNamePrefix("$");
+const TString TableIndexColumnName = SystemColumnNamePrefix + "table_index";
+const TString RowIndexColumnName = SystemColumnNamePrefix + "row_index";
+const TString RangeIndexColumnName = SystemColumnNamePrefix + "range_index";
+const TString TabletIndexColumnName = SystemColumnNamePrefix + "tablet_index";
+const TString TimestampColumnName = SystemColumnNamePrefix + "timestamp";
+const TString CumulativeDataWeightColumnName = SystemColumnNamePrefix + "cumulative_data_weight";
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/public.h b/yt/yt/client/table_client/public.h
new file mode 100644
index 0000000000..509f4b9dbc
--- /dev/null
+++ b/yt/yt/client/table_client/public.h
@@ -0,0 +1,396 @@
+#pragma once
+
+#include <yt/yt/client/chunk_client/public.h>
+
+#include <yt/yt/client/cypress_client/public.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/core/misc/range.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <util/generic/size_literals.h>
+
+#include <initializer_list>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TNameTableExt;
+class TLogicalType;
+class TColumnSchema;
+class TDeletedColumn;
+class TTableSchemaExt;
+class TKeyColumnsExt;
+class TSortColumnsExt;
+class TBoundaryKeysExt;
+class TBlockIndexesExt;
+class TDataBlockMetaExt;
+class TSystemBlockMetaExt;
+class TColumnarStatisticsExt;
+class TDataBlockMeta;
+class TSimpleVersionedBlockMeta;
+class TSlimVersionedBlockMeta;
+class TSchemaDictionary;
+class TColumnFilter;
+class TReqLookupRows;
+class TColumnRenameDescriptor;
+class THunkChunkRef;
+class TColumnMetaExt;
+class TVersionedRowDigestExt;
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TRefCountedDataBlockMeta = TRefCountedProto<NProto::TDataBlockMetaExt>;
+using TRefCountedDataBlockMetaPtr = TIntrusivePtr<TRefCountedDataBlockMeta>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TRefCountedColumnMeta = TRefCountedProto<NProto::TColumnMetaExt>;
+using TRefCountedColumnMetaPtr = TIntrusivePtr<TRefCountedColumnMeta>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NTransactionClient::TTimestamp;
+using NTransactionClient::NullTimestamp;
+using NTransactionClient::MinTimestamp;
+using NTransactionClient::MaxTimestamp;
+using NTransactionClient::SyncLastCommittedTimestamp;
+using NTransactionClient::AsyncLastCommittedTimestamp;
+using NTransactionClient::AllCommittedTimestamp;
+using NTransactionClient::NotPreparedTimestamp;
+
+using TKeyColumns = std::vector<TString>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Keep values below consistent with https://wiki.yandex-team.ru/yt/userdoc/tables.
+constexpr int MaxKeyColumnCount = 256;
+constexpr int TypicalColumnCount = 64;
+constexpr int MaxColumnLockCount = 32;
+constexpr int MaxColumnNameLength = 256;
+constexpr int MaxColumnLockLength = 256;
+constexpr int MaxColumnGroupLength = 256;
+
+// Only for dynamic tables.
+constexpr int MaxValuesPerRow = 1024;
+constexpr int MaxRowsPerRowset = 5 * 1024 * 1024;
+constexpr i64 MaxStringValueLength = 16_MB;
+constexpr i64 MaxAnyValueLength = 16_MB;
+constexpr i64 MaxCompositeValueLength = 16_MB;
+constexpr i64 MaxServerVersionedRowDataWeight = 512_MB;
+constexpr i64 MaxClientVersionedRowDataWeight = 128_MB;
+constexpr int MaxKeyColumnCountInDynamicTable = 32;
+constexpr int MaxTimestampCountPerRow = std::numeric_limits<ui16>::max();
+
+static_assert(
+ MaxTimestampCountPerRow <= std::numeric_limits<ui16>::max(),
+ "Max timestamp count cannot be larger than UINT16_MAX");
+
+// Only for static tables.
+constexpr i64 MaxRowWeightLimit = 128_MB;
+constexpr i64 MaxKeyWeightLimit = 256_KB;
+
+// NB(psushin): increasing this parameter requires rewriting all chunks,
+// so one probably should never want to do it.
+constexpr int MaxSampleSize = 64_KB;
+
+// This is a hard limit for static tables,
+// imposed by Id field size (16-bit) in TUnversionedValue.
+constexpr int MaxColumnId = 32 * 1024;
+
+constexpr int MaxSchemaTotalTypeComplexity = MaxColumnId;
+constexpr int MaxSchemaDepth = 32;
+
+extern const TString SystemColumnNamePrefix;
+extern const TString TableIndexColumnName;
+extern const TString RowIndexColumnName;
+extern const TString RangeIndexColumnName;
+extern const TString TabletIndexColumnName;
+extern const TString TimestampColumnName;
+extern const TString CumulativeDataWeightColumnName;
+extern const TString PrimaryLockName;
+
+constexpr int TypicalHunkColumnCount = 8;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EHunkValueTag, ui8,
+ ((Inline) (0))
+ ((LocalRef) (1))
+ ((GlobalRef)(2))
+);
+
+// Do not change these values since they are stored in the master snapshot.
+DEFINE_ENUM(ETableSchemaMode,
+ ((Weak) (0))
+ ((Strong) (1))
+);
+
+DEFINE_ENUM(EOptimizeFor,
+ ((Lookup) (0))
+ ((Scan) (1))
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((SortOrderViolation) (301))
+ ((InvalidDoubleValue) (302))
+ ((IncomparableTypes) (303))
+ ((UnhashableType) (304))
+ // E.g. name table with more than #MaxColumnId columns (may come from legacy chunks).
+ ((CorruptedNameTable) (305))
+ ((UniqueKeyViolation) (306))
+ ((SchemaViolation) (307))
+ ((RowWeightLimitExceeded) (308))
+ ((InvalidColumnFilter) (309))
+ ((InvalidColumnRenaming) (310))
+ ((IncompatibleKeyColumns) (311))
+ ((ReaderDeadlineExpired) (312))
+ ((TimestampOutOfRange) (313))
+ ((InvalidSchemaValue) (314))
+ ((FormatCannotRepresentRow) (315))
+ ((IncompatibleSchemas) (316))
+ ((InvalidPartitionedBy) (317))
+ ((MisconfiguredPartitions) (318))
+ ((TooManyRowsInRowset) (319))
+ ((TooManyColumnsInKey) (320))
+ ((TooManyValuesInRow) (321))
+ ((DuplicateColumnInSchema) (322))
+ ((MissingRequiredColumnInSchema) (323))
+ ((IncomparableComplexValues) (324))
+ ((KeyCannotBeNan) (325))
+ ((StringLikeValueLengthLimitExceeded)(326))
+ ((NameTableUpdateFailed) (327))
+ ((InvalidTableChunkFormat) (328))
+);
+
+DEFINE_ENUM(EControlAttribute,
+ (TableIndex)
+ (KeySwitch)
+ (RangeIndex)
+ (RowIndex)
+ (TabletIndex)
+ (EndOfStream)
+);
+
+DEFINE_ENUM(EUnavailableChunkStrategy,
+ ((ThrowError) (0))
+ ((Restore) (1))
+ ((Skip) (2))
+);
+
+DEFINE_ENUM(ETableSchemaModification,
+ ((None) (0))
+ ((UnversionedUpdate) (1))
+ ((UnversionedUpdateUnsorted) (2))
+);
+
+DEFINE_ENUM(EColumnarStatisticsFetcherMode,
+ ((FromNodes) (0))
+ ((FromMaster) (1))
+ ((Fallback) (2))
+);
+
+DEFINE_ENUM(ETablePartitionMode,
+ ((Sorted) (0))
+ ((Ordered) (1))
+ ((Unordered) (2))
+);
+
+DEFINE_ENUM(EMisconfiguredPartitionTactics,
+ ((Fail) (0))
+ ((Skip) (1))
+);
+
+using TTableId = NCypressClient::TNodeId;
+using TTableCollocationId = NObjectClient::TObjectId;
+using TMasterTableSchemaId = NObjectClient::TObjectId;
+
+//! NB: |int| is important since we use negative values to indicate that
+//! certain values need to be dropped. Cf. #TRowBuffer::CaptureAndPermuteRow.
+using TNameTableToSchemaIdMapping = TCompactVector<int, TypicalColumnCount>;
+
+using TIdMapping = TCompactVector<int, TypicalColumnCount>;
+
+using THunkColumnIds = TCompactVector<int, TypicalHunkColumnCount>;
+
+union TUnversionedValueData;
+
+enum class ESortOrder;
+enum class EValueType : ui8;
+enum class ESimpleLogicalValueType : ui32;
+enum class ELogicalMetatype;
+
+using TKeyColumnTypes = TCompactVector<EValueType, 16>;
+
+class TColumnFilter;
+
+struct TUnversionedValue;
+using TUnversionedValueRange = TRange<TUnversionedValue>;
+using TMutableUnversionedValueRange = TMutableRange<TUnversionedValue>;
+
+struct TVersionedValue;
+using TVersionedValueRange = TRange<TVersionedValue>;
+using TMutableVersionedValueRange = TMutableRange<TVersionedValue>;
+
+using TTimestampRange = TRange<TTimestamp>;
+using TMutableTimestampRange = TMutableRange<TTimestamp>;
+
+class TUnversionedOwningValue;
+
+struct TUnversionedRowHeader;
+struct TVersionedRowHeader;
+
+class TUnversionedRow;
+class TMutableUnversionedRow;
+class TUnversionedOwningRow;
+
+class TVersionedRow;
+class TMutableVersionedRow;
+class TVersionedOwningRow;
+
+class TKey;
+using TKeyRef = TUnversionedValueRange;
+
+using TLegacyKey = TUnversionedRow;
+using TLegacyMutableKey = TMutableUnversionedRow;
+using TLegacyOwningKey = TUnversionedOwningRow;
+
+// TODO(babenko): replace with TRange<TUnversionedRow>.
+using TRowRange = std::pair<TUnversionedRow, TUnversionedRow>;
+
+class TUnversionedRowBuilder;
+class TUnversionedOwningRowBuilder;
+
+struct TTypeErasedRow;
+
+class TKeyBound;
+class TOwningKeyBound;
+
+class TKeyComparer;
+
+struct TColumnRenameDescriptor;
+using TColumnRenameDescriptors = std::vector<TColumnRenameDescriptor>;
+
+class TStableName;
+
+class TColumnSchema;
+
+struct TColumnSortSchema;
+using TSortColumns = std::vector<TColumnSortSchema>;
+
+class TTableSchema;
+using TTableSchemaPtr = TIntrusivePtr<TTableSchema>;
+
+class TLegacyLockMask;
+using TLegacyLockBitmap = ui64;
+
+class TLockMask;
+using TLockBitmap = TCompactVector<ui64, 1>;
+
+class TComparator;
+
+DECLARE_REFCOUNTED_CLASS(TNameTable)
+class TNameTableReader;
+class TNameTableWriter;
+
+DECLARE_REFCOUNTED_CLASS(TRowBuffer)
+
+DECLARE_REFCOUNTED_STRUCT(ISchemalessUnversionedReader)
+DECLARE_REFCOUNTED_STRUCT(ISchemafulUnversionedReader)
+DECLARE_REFCOUNTED_STRUCT(IUnversionedWriter)
+DECLARE_REFCOUNTED_STRUCT(IUnversionedRowsetWriter)
+
+using TSchemalessWriterFactory = std::function<IUnversionedRowsetWriterPtr(
+ TNameTablePtr,
+ TTableSchemaPtr)>;
+
+DECLARE_REFCOUNTED_STRUCT(IVersionedReader)
+DECLARE_REFCOUNTED_STRUCT(IVersionedWriter)
+
+DECLARE_REFCOUNTED_CLASS(THashTableChunkIndexWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TChunkIndexesWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TSlimVersionedWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TChunkWriterTestingOptions)
+
+DECLARE_REFCOUNTED_CLASS(TChunkReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TChunkWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TKeyFilterWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TTableReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TTableWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TRetentionConfig)
+
+DECLARE_REFCOUNTED_CLASS(TTypeConversionConfig)
+DECLARE_REFCOUNTED_CLASS(TInsertRowsFormatConfig)
+
+DECLARE_REFCOUNTED_CLASS(TChunkReaderOptions)
+DECLARE_REFCOUNTED_CLASS(TChunkWriterOptions)
+
+DECLARE_REFCOUNTED_CLASS(TVersionedRowDigestConfig)
+
+class TSaveContext;
+class TLoadContext;
+using TPersistenceContext = TCustomPersistenceContext<TSaveContext, TLoadContext>;
+
+struct IWireProtocolReader;
+struct IWireProtocolWriter;
+
+using TSchemaData = std::vector<ui32>;
+
+DECLARE_REFCOUNTED_STRUCT(IWireProtocolRowsetReader)
+DECLARE_REFCOUNTED_STRUCT(IWireProtocolRowsetWriter)
+
+DECLARE_REFCOUNTED_STRUCT(IUnversionedRowBatch)
+DECLARE_REFCOUNTED_STRUCT(IUnversionedColumnarRowBatch)
+DECLARE_REFCOUNTED_STRUCT(IVersionedRowBatch)
+
+struct IValueConsumer;
+struct IFlushableValueConsumer;
+
+class TComplexTypeFieldDescriptor;
+
+DECLARE_REFCOUNTED_CLASS(TLogicalType)
+class TSimpleLogicalType;
+class TDecimalLogicalType;
+class TOptionalLogicalType;
+class TListLogicalType;
+class TStructLogicalType;
+class TTupleLogicalType;
+class TVariantTupleLogicalType;
+class TVariantStructLogicalType;
+class TDictLogicalType;
+class TTaggedLogicalType;
+
+struct TStructField;
+
+struct IRecordDescriptor;
+
+//! Enumeration is used to describe compatibility of two schemas (or logical types).
+//! Such compatibility tests are performed before altering table schema or before merge operation.
+DEFINE_ENUM(ESchemaCompatibility,
+ // Values are incompatible.
+ // E.g. Int8 and String.
+ (Incompatible)
+
+ // Values that satisfy old schema MIGHT satisfy new schema, dynamic check is required.
+ // E.g. Optional<Int8> and Int8, in this case we must check that value of old type is not NULL.
+ (RequireValidation)
+
+ // Values that satisfy old schema ALWAYS satisfy new schema.
+ // E.g. Int32 and Int64
+ (FullyCompatible)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/record_codegen_cpp.cpp b/yt/yt/client/table_client/record_codegen_cpp.cpp
new file mode 100644
index 0000000000..e2a3a3816d
--- /dev/null
+++ b/yt/yt/client/table_client/record_codegen_cpp.cpp
@@ -0,0 +1,36 @@
+#include "record_codegen_cpp.h"
+
+namespace NYT::NTableClient::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateKeyValueCount(TLegacyKey key, int count)
+{
+ if (static_cast<int>(key.GetCount()) != count) {
+ THROW_ERROR_EXCEPTION("Invalid number of key values: expected %v, got %v",
+ count,
+ key.GetCount());
+ }
+}
+
+int GetColumnIdOrThrow(std::optional<int> optionalId, TStringBuf name)
+{
+ if (!optionalId) {
+ THROW_ERROR_EXCEPTION("Column %Qv is not registered",
+ name);
+ }
+ return *optionalId;
+}
+
+void ValidateRowValueCount(TUnversionedRow row, int id)
+{
+ if (static_cast<int>(row.GetCount()) < id) {
+ THROW_ERROR_EXCEPTION("Too few values in row: expected > %v, actual %v",
+ id,
+ row.GetCount());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient::NDetail
diff --git a/yt/yt/client/table_client/record_codegen_cpp.h b/yt/yt/client/table_client/record_codegen_cpp.h
new file mode 100644
index 0000000000..7ff6afa1bb
--- /dev/null
+++ b/yt/yt/client/table_client/record_codegen_cpp.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/helpers.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_base.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <array>
+
+namespace NYT::NTableClient::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateKeyValueCount(TLegacyKey key, int count);
+int GetColumnIdOrThrow(std::optional<int> optionalId, TStringBuf name);
+void ValidateRowValueCount(TUnversionedRow row, int id);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient::NDetail
diff --git a/yt/yt/client/table_client/record_helpers-inl.h b/yt/yt/client/table_client/record_helpers-inl.h
new file mode 100644
index 0000000000..ce35d20e35
--- /dev/null
+++ b/yt/yt/client/table_client/record_helpers-inl.h
@@ -0,0 +1,166 @@
+#ifndef RECORD_HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include record_helpers.h"
+// For the sake of sane code completion.
+#include "record_helpers.h"
+#endif
+#undef RECORD_HELPERS_INL_H_
+
+#include "row_buffer.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+void ValidateRowNotNull(TUnversionedRow row);
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecord>
+TRecord ToRecord(
+ TUnversionedRow row,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ NTableClient::NDetail::ValidateRowNotNull(row);
+ return TRecord::FromUnversionedRow(row, idMapping);
+}
+
+template <class TRecord>
+std::vector<TRecord> ToRecords(
+ TRange<TUnversionedRow> rows,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ std::vector<TRecord> records;
+ records.reserve(rows.size());
+ for (auto row : rows) {
+ records.push_back(ToRecord<TRecord>(row, idMapping));
+ }
+ return records;
+}
+
+template <class TRecord>
+std::vector<TRecord> ToRecords(const NApi::IUnversionedRowsetPtr& rowset)
+{
+ typename TRecord::TRecordDescriptor::TIdMapping idMapping(rowset->GetNameTable());
+ return ToRecords<TRecord>(rowset->GetRows(), idMapping);
+}
+
+template <class TRecord>
+std::optional<TRecord> ToOptionalRecord(
+ TUnversionedRow row,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ if (!row) {
+ return std::nullopt;
+ }
+ return ToRecord<TRecord>(row, idMapping);
+}
+
+template <class TRecord>
+std::vector<std::optional<TRecord>> ToOptionalRecords(
+ TRange<TUnversionedRow> rows,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ std::vector<std::optional<TRecord>> records;
+ records.reserve(rows.size());
+ for (auto row : rows) {
+ records.push_back(ToOptionalRecord<TRecord>(row, idMapping));
+ }
+ return records;
+}
+
+template <class TRecord>
+std::vector<std::optional<TRecord>> ToOptionalRecords(const NApi::IUnversionedRowsetPtr& rowset)
+{
+ typename TRecord::TRecordDescriptor::TIdMapping idMapping(rowset->GetNameTable());
+ return ToOptionalRecords<TRecord>(rowset->GetRows(), idMapping);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecord>
+TUnversionedRow FromRecord(
+ const TRecord& record,
+ const TRowBufferPtr& rowBuffer,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ return record.ToUnversionedRow(rowBuffer, idMapping);
+}
+
+template <class TRecord>
+TUnversionedOwningRow FromRecord(
+ const TRecord& record,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ // TODO(babenko): optimize
+ auto rowBuffer = New<TRowBuffer>(TDefaultRowBufferPoolTag(), 256);
+ return TUnversionedOwningRow(FromRecord(record, rowBuffer, idMapping));
+}
+
+template <class TRecord>
+TSharedRange<TUnversionedRow> FromRecords(
+ TRange<TRecord> records,
+ const TRowBufferPtr& rowBuffer,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ std::vector<TUnversionedRow> rows;
+ rows.reserve(records.size());
+ for (const auto& record : records) {
+ rows.push_back(FromRecord(record, rowBuffer, idMapping));
+ }
+ return MakeSharedRange(std::move(rows), rowBuffer);
+}
+
+template <class TRecord>
+TSharedRange<TUnversionedRow> FromRecords(
+ TRange<TRecord> records,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping)
+{
+ return FromRecords(records, New<TRowBuffer>(), idMapping);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecordKey>
+TLegacyKey FromRecordKey(
+ const TRecordKey& recordKey,
+ const TRowBufferPtr& rowBuffer)
+{
+ return recordKey.ToKey(rowBuffer);
+}
+
+template <class TRecordKey>
+TLegacyOwningKey FromRecordKey(
+ const TRecordKey& recordKey)
+{
+ // TODO(babenko): optimize
+ auto rowBuffer = New<TRowBuffer>(TDefaultRowBufferPoolTag(), 256);
+ return TUnversionedOwningRow(FromRecordKey(recordKey, rowBuffer));
+}
+
+template <class TRecordKey>
+TSharedRange<TLegacyKey> FromRecordKeys(
+ TRange<TRecordKey> recordKeys,
+ const TRowBufferPtr& rowBuffer)
+{
+ std::vector<TLegacyKey> keys;
+ keys.reserve(recordKeys.size());
+ for (const auto& recordKey : recordKeys) {
+ keys.push_back(FromRecordKey(recordKey, rowBuffer));
+ }
+ return MakeSharedRange(std::move(keys), rowBuffer);
+}
+
+template <class TRecordKey>
+TSharedRange<TLegacyKey> FromRecordKeys(
+ TRange<TRecordKey> recordKeys)
+{
+ return FromRecordKeys(recordKeys, New<TRowBuffer>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/record_helpers.cpp b/yt/yt/client/table_client/record_helpers.cpp
new file mode 100644
index 0000000000..67842db2ee
--- /dev/null
+++ b/yt/yt/client/table_client/record_helpers.cpp
@@ -0,0 +1,20 @@
+#include "record_helpers.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+void ValidateRowNotNull(TUnversionedRow row)
+{
+ if (!row) {
+ THROW_ERROR_EXCEPTION("Row must not be null");
+ }
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/record_helpers.h b/yt/yt/client/table_client/record_helpers.h
new file mode 100644
index 0000000000..1ef7982a33
--- /dev/null
+++ b/yt/yt/client/table_client/record_helpers.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include "unversioned_row.h"
+
+#include <yt/yt/client/api/rowset.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecord>
+TRecord ToRecord(
+ TUnversionedRow row,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping);
+
+template <class TRecord>
+std::vector<TRecord> ToRecords(
+ TRange<TUnversionedRow> rows,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping);
+
+template <class TRecord>
+std::vector<TRecord> ToRecords(const NApi::IUnversionedRowsetPtr& rowset);
+
+template <class TRecord>
+std::optional<TRecord> ToOptionalRecord(
+ TUnversionedRow row,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping);
+
+template <class TRecord>
+std::vector<std::optional<TRecord>> ToOptionalRecords(
+ TRange<TUnversionedRow> rows,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping);
+
+template <class TRecord>
+std::vector<std::optional<TRecord>> ToOptionalRecords(const NApi::IUnversionedRowsetPtr& rowset);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecord>
+TUnversionedRow FromRecord(
+ const TRecord& record,
+ const TRowBufferPtr& rowBuffer,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping =
+ TRecord::TRecordDescriptor::Get()->GetIdMapping());
+
+template <class TRecord>
+TUnversionedOwningRow FromRecord(
+ const TRecord& record,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping =
+ TRecord::TRecordDescriptor::Get()->GetIdMapping());
+
+template <class TRecord>
+TSharedRange<TUnversionedRow> FromRecords(
+ TRange<TRecord> records,
+ const TRowBufferPtr& rowBuffer,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping =
+ TRecord::TRecordDescriptor::Get()->GetIdMapping());
+
+template <class TRecord>
+TSharedRange<TUnversionedRow> FromRecords(
+ TRange<TRecord> records,
+ const typename TRecord::TRecordDescriptor::TIdMapping& idMapping =
+ TRecord::TRecordDescriptor::Get()->GetIdMapping());
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRecordKey>
+TLegacyKey FromRecordKey(
+ const TRecordKey& recordKey,
+ const TRowBufferPtr& rowBuffer);
+
+template <class TRecordKey>
+TLegacyOwningKey FromRecordKey(
+ const TRecordKey& recordKey);
+
+template <class TRecordKey>
+TSharedRange<TLegacyKey> FromRecordKeys(
+ TRange<TRecordKey> recordKeys,
+ const TRowBufferPtr& rowBuffer);
+
+template <class TRecordKey>
+TSharedRange<TLegacyKey> FromRecordKeys(
+ TRange<TRecordKey> recordKeys);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define RECORD_HELPERS_INL_H_
+#include "record_helpers-inl.h"
+#undef RECORD_HELPERS_INL_H_
diff --git a/yt/yt/client/table_client/row_base.cpp b/yt/yt/client/table_client/row_base.cpp
new file mode 100644
index 0000000000..2eb15d37fd
--- /dev/null
+++ b/yt/yt/client/table_client/row_base.cpp
@@ -0,0 +1,189 @@
+#include "row_base.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintTo(EValueType type, std::ostream* os)
+{
+ *os << ToString(type);
+}
+
+void PrintTo(ESimpleLogicalValueType type, std::ostream* os)
+{
+ *os << ToString(type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TColumnFilter::TColumnFilter()
+ : Universal_(true)
+{ }
+
+TColumnFilter::TColumnFilter(const std::initializer_list<int>& indexes)
+ : Universal_(false)
+ , Indexes_(indexes.begin(), indexes.end())
+{ }
+
+TColumnFilter::TColumnFilter(TIndexes&& indexes)
+ : Universal_(false)
+ , Indexes_(std::move(indexes))
+{ }
+
+TColumnFilter::TColumnFilter(const std::vector<int>& indexes)
+ : Universal_(false)
+ , Indexes_(indexes.begin(), indexes.end())
+{ }
+
+TColumnFilter::TColumnFilter(int schemaColumnCount)
+ : Universal_(false)
+{
+ for (int i = 0; i < schemaColumnCount; ++i) {
+ Indexes_.push_back(i);
+ }
+}
+
+std::optional<int> TColumnFilter::FindPosition(int columnIndex) const
+{
+ if (Universal_) {
+ THROW_ERROR_EXCEPTION("Unable to search index in column filter with IsUniversal flag");
+ }
+ auto it = std::find(Indexes_.begin(), Indexes_.end(), columnIndex);
+ if (it == Indexes_.end()) {
+ return std::nullopt;
+ }
+ return std::distance(Indexes_.begin(), it);
+}
+
+int TColumnFilter::GetPosition(int columnIndex) const
+{
+ if (auto indexOrNull = FindPosition(columnIndex)) {
+ return *indexOrNull;
+ }
+
+ THROW_ERROR_EXCEPTION("Column filter does not contain column index %Qv", columnIndex);
+}
+
+bool TColumnFilter::ContainsIndex(int columnIndex) const
+{
+ if (Universal_) {
+ return true;
+ }
+
+ return std::find(Indexes_.begin(), Indexes_.end(), columnIndex) != Indexes_.end();
+}
+
+const TColumnFilter::TIndexes& TColumnFilter::GetIndexes() const
+{
+ YT_VERIFY(!Universal_);
+ return Indexes_;
+}
+
+bool TColumnFilter::IsUniversal() const
+{
+ return Universal_;
+}
+
+const TColumnFilter& TColumnFilter::MakeUniversal()
+{
+ static const TColumnFilter Result;
+ return Result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateDataValueType(EValueType type)
+{
+ if (type != EValueType::Int64 &&
+ type != EValueType::Uint64 &&
+ type != EValueType::Double &&
+ type != EValueType::Boolean &&
+ type != EValueType::String &&
+ type != EValueType::Any &&
+ type != EValueType::Composite &&
+ type != EValueType::Null)
+ {
+ THROW_ERROR_EXCEPTION("Invalid data value type %Qlv", type);
+ }
+}
+
+void ValidateKeyValueType(EValueType type)
+{
+ if (type != EValueType::Int64 &&
+ type != EValueType::Uint64 &&
+ type != EValueType::Double &&
+ type != EValueType::Boolean &&
+ type != EValueType::String &&
+ type != EValueType::Composite &&
+ type != EValueType::Null &&
+ type != EValueType::Min &&
+ type != EValueType::Max)
+ {
+ THROW_ERROR_EXCEPTION("Invalid key value type %Qlv", type);
+ }
+}
+
+void ValidateSchemaValueType(EValueType type)
+{
+ if (type != EValueType::Null &&
+ type != EValueType::Int64 &&
+ type != EValueType::Uint64 &&
+ type != EValueType::Double &&
+ type != EValueType::Boolean &&
+ type != EValueType::String &&
+ type != EValueType::Any &&
+ type != EValueType::Composite)
+ {
+ THROW_ERROR_EXCEPTION("Invalid value type %Qlv", type);
+ }
+}
+
+void ValidateColumnFilter(const TColumnFilter& columnFilter, int schemaColumnCount)
+{
+ if (columnFilter.IsUniversal()) {
+ return;
+ }
+
+ TCompactVector<bool, TypicalColumnCount> flags;
+ flags.resize(schemaColumnCount);
+
+ for (int index : columnFilter.GetIndexes()) {
+ if (index < 0 || index >= schemaColumnCount) {
+ THROW_ERROR_EXCEPTION("Column filter contains invalid index: actual %v, expected in range [0, %v]",
+ index,
+ schemaColumnCount - 1);
+ }
+ if (flags[index]) {
+ THROW_ERROR_EXCEPTION("Column filter contains duplicate index %v",
+ index);
+ }
+ flags[index] = true;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TColumnFilter& columnFilter)
+{
+ if (columnFilter.IsUniversal()) {
+ return TString("{All}");
+ } else {
+ return Format("{%v}", columnFilter.GetIndexes());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+[[noreturn]] void ThrowUnexpectedValueType(EValueType valueType)
+{
+ THROW_ERROR_EXCEPTION("Unexpected value type %Qlv",
+ valueType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/row_base.h b/yt/yt/client/table_client/row_base.h
new file mode 100644
index 0000000000..935ebb16af
--- /dev/null
+++ b/yt/yt/client/table_client/row_base.h
@@ -0,0 +1,458 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EValueType, ui8,
+ ((Min) (0x00))
+
+ ((TheBottom) (0x01))
+ ((Null) (0x02))
+
+ ((Int64) (0x03))
+ ((Uint64) (0x04))
+ ((Double) (0x05))
+ ((Boolean) (0x06))
+
+ ((String) (0x10))
+ ((Any) (0x11))
+
+ ((Composite) (0x12))
+
+ ((Max) (0xef))
+);
+
+static_assert(
+ EValueType::Int64 < EValueType::Uint64 &&
+ EValueType::Uint64 < EValueType::Double,
+ "Incorrect type order.");
+
+DEFINE_BIT_ENUM_WITH_UNDERLYING_TYPE(EValueFlags, ui8,
+ ((None) (0x0000))
+ ((Aggregate) (0x0001))
+ ((Hunk) (0x0002))
+);
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(ESimpleLogicalValueType, ui32,
+ ((Null) (0x02))
+
+ ((Int64) (0x03))
+ ((Uint64) (0x04))
+ ((Double) (0x05))
+ ((Boolean) (0x06))
+
+ ((String) (0x10))
+ ((Any) (0x11))
+
+ ((Int8) (0x1000))
+ ((Uint8) (0x1001))
+
+ ((Int16) (0x1003))
+ ((Uint16) (0x1004))
+
+ ((Int32) (0x1005))
+ ((Uint32) (0x1006))
+
+ ((Utf8) (0x1007))
+
+ ((Date) (0x1008))
+ ((Datetime) (0x1009))
+ ((Timestamp) (0x100a))
+ ((Interval) (0x100b))
+
+ ((Void) (0x100c))
+
+ ((Float) (0x100d))
+ ((Json) (0x100e))
+
+ ((Uuid) (0x100f))
+);
+
+//! Debug printers for Gtest unittests.
+void PrintTo(EValueType type, std::ostream* os);
+void PrintTo(ESimpleLogicalValueType type, std::ostream* os);
+
+inline bool IsIntegralTypeSigned(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+ return true;
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+ return false;
+ default:
+ YT_ABORT();
+ }
+}
+
+inline bool IsIntegralType(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ case ESimpleLogicalValueType::Uint64:
+ return true;
+ default:
+ return false;
+ }
+}
+
+inline bool IsStringLikeType(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Any:
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::Uuid:
+ return true;
+ default:
+ return false;
+ }
+}
+
+inline int GetIntegralTypeBitWidth(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Uint8:
+ return 8;
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Uint16:
+ return 16;
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Uint32:
+ return 32;
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint64:
+ return 64;
+ default:
+ YT_ABORT();
+ }
+}
+
+inline int GetIntegralTypeByteSize(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Uint8:
+ return 1;
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Uint16:
+ return 2;
+ case ESimpleLogicalValueType::Int32:
+ case ESimpleLogicalValueType::Uint32:
+ return 4;
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint64:
+ return 8;
+ default:
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr EValueType GetPhysicalType(ESimpleLogicalValueType type)
+{
+ switch (type) {
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Int64:
+ case ESimpleLogicalValueType::Uint64:
+ case ESimpleLogicalValueType::Double:
+ case ESimpleLogicalValueType::Boolean:
+ case ESimpleLogicalValueType::String:
+ case ESimpleLogicalValueType::Any:
+ return static_cast<EValueType>(type);
+
+ case ESimpleLogicalValueType::Int8:
+ case ESimpleLogicalValueType::Int16:
+ case ESimpleLogicalValueType::Int32:
+ return EValueType::Int64;
+
+ case ESimpleLogicalValueType::Uint8:
+ case ESimpleLogicalValueType::Uint16:
+ case ESimpleLogicalValueType::Uint32:
+ return EValueType::Uint64;
+
+ case ESimpleLogicalValueType::Utf8:
+ case ESimpleLogicalValueType::Json:
+ case ESimpleLogicalValueType::Uuid:
+ return EValueType::String;
+ case ESimpleLogicalValueType::Date:
+ case ESimpleLogicalValueType::Datetime:
+ case ESimpleLogicalValueType::Timestamp:
+ return EValueType::Uint64;
+ case ESimpleLogicalValueType::Interval:
+ return EValueType::Int64;
+
+ case ESimpleLogicalValueType::Void:
+ return EValueType::Null;
+
+ case ESimpleLogicalValueType::Float:
+ return EValueType::Double;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+inline ESimpleLogicalValueType GetLogicalType(EValueType type)
+{
+ switch (type) {
+ case EValueType::Null:
+ case EValueType::Int64:
+ case EValueType::Uint64:
+ case EValueType::Double:
+ case EValueType::Boolean:
+ case EValueType::String:
+ case EValueType::Any:
+ return static_cast<ESimpleLogicalValueType>(type);
+
+ case EValueType::Composite:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ THROW_ERROR_EXCEPTION("Value type %Qlv has no corresponding logical type",
+ type);
+
+ default:
+ YT_ABORT();
+ }
+}
+
+inline constexpr bool IsIntegralType(EValueType type)
+{
+ return type == EValueType::Int64 || type == EValueType::Uint64;
+}
+
+inline constexpr bool IsArithmeticType(EValueType type)
+{
+ return IsIntegralType(type) || type == EValueType::Double;
+}
+
+inline constexpr bool IsAnyOrComposite(EValueType type)
+{
+ return type == EValueType::Any || type == EValueType::Composite;
+}
+
+inline constexpr bool IsStringLikeType(EValueType type)
+{
+ return type == EValueType::String || IsAnyOrComposite(type);
+}
+
+inline constexpr bool IsValueType(EValueType type)
+{
+ return
+ type == EValueType::Int64 ||
+ type == EValueType::Uint64 ||
+ type == EValueType::Double ||
+ type == EValueType::Boolean ||
+ type == EValueType::String ||
+ type == EValueType::Any;
+}
+
+inline constexpr bool IsAnyColumnCompatibleType(EValueType type)
+{
+ return
+ type == EValueType::Null ||
+ type == EValueType::Int64 ||
+ type == EValueType::Uint64 ||
+ type == EValueType::Double ||
+ type == EValueType::Boolean ||
+ type == EValueType::String ||
+ type == EValueType::Any ||
+ type == EValueType::Composite;
+}
+
+inline constexpr bool IsSentinelType(EValueType type)
+{
+ return type == EValueType::Min || type == EValueType::Max;
+}
+
+inline constexpr bool IsPrimitiveType(EValueType type)
+{
+ return
+ type == EValueType::Int64 ||
+ type == EValueType::Uint64 ||
+ type == EValueType::Double ||
+ type == EValueType::Boolean ||
+ type == EValueType::String;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This class contains ordered collection of indexes of columns of some table.
+// Position in context of the class means position of some column index in ordered collection.
+class TColumnFilter
+{
+public:
+ using TIndexes = TCompactVector<int, TypicalColumnCount>;
+
+ TColumnFilter();
+ TColumnFilter(const std::initializer_list<int>& indexes);
+ explicit TColumnFilter(TIndexes&& indexes);
+ explicit TColumnFilter(const std::vector<int>& indexes);
+ explicit TColumnFilter(int schemaColumnCount);
+
+ bool ContainsIndex(int columnIndex) const;
+ int GetPosition(int columnIndex) const;
+ std::optional<int> FindPosition(int columnIndex) const;
+ const TIndexes& GetIndexes() const;
+
+ static const TColumnFilter& MakeUniversal();
+ bool IsUniversal() const;
+
+private:
+ bool Universal_;
+ TIndexes Indexes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TColumnFilter& columnFilter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTypeErasedRow
+{
+ const void* OpaqueHeader;
+
+ explicit operator bool() const
+ {
+ return OpaqueHeader != nullptr;
+ }
+};
+
+static_assert(std::is_pod<TTypeErasedRow>::value, "TTypeErasedRow must be POD.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Checks that #type is allowed to appear in data. Throws on failure.
+void ValidateDataValueType(EValueType type);
+
+//! Checks that #type is allowed to appear in keys. Throws on failure.
+void ValidateKeyValueType(EValueType type);
+
+//! Checks that #type is allowed to appear in schema. Throws on failure.
+void ValidateSchemaValueType(EValueType type);
+
+//! Checks that column filter contains indexes in range |[0, schemaColumnCount - 1]|.
+void ValidateColumnFilter(const TColumnFilter& columnFilter, int schemaColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+TValue MakeSentinelValue(EValueType type, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = type;
+ result.Flags = flags;
+ return result;
+}
+
+template <class TValue>
+TValue MakeNullValue(int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = EValueType::Null;
+ result.Flags = flags;
+ return result;
+}
+
+template <class TValue>
+TValue MakeInt64Value(i64 value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = EValueType::Int64;
+ result.Flags = flags;
+ result.Data.Int64 = value;
+ return result;
+}
+
+template <class TValue>
+TValue MakeUint64Value(ui64 value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = EValueType::Uint64;
+ result.Flags = flags;
+ result.Data.Uint64 = value;
+ return result;
+}
+
+template <class TValue>
+TValue MakeDoubleValue(double value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = EValueType::Double;
+ result.Flags = flags;
+ result.Data.Double = value;
+ return result;
+}
+
+template <class TValue>
+TValue MakeBooleanValue(bool value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = EValueType::Boolean;
+ result.Flags = flags;
+ result.Data.Boolean = value;
+ return result;
+}
+
+template <class TValue>
+TValue MakeStringLikeValue(EValueType valueType, TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ TValue result{};
+ result.Id = id;
+ result.Type = valueType;
+ result.Flags = flags;
+ result.Length = value.length();
+ result.Data.String = value.begin();
+ return result;
+}
+
+template <class TValue>
+TValue MakeStringValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeStringLikeValue<TValue>(EValueType::String, value, id, flags);
+}
+
+template <class TValue>
+TValue MakeAnyValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeStringLikeValue<TValue>(EValueType::Any, value, id, flags);
+}
+
+template <class TValue>
+TValue MakeCompositeValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeStringLikeValue<TValue>(EValueType::Composite, value, id, flags);
+}
+
+[[noreturn]] void ThrowUnexpectedValueType(EValueType valueType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/row_batch-inl.h b/yt/yt/client/table_client/row_batch-inl.h
new file mode 100644
index 0000000000..c491a6bece
--- /dev/null
+++ b/yt/yt/client/table_client/row_batch-inl.h
@@ -0,0 +1,70 @@
+#ifndef ROW_BATCH_INL_H_
+#error "Direct inclusion of this file is not allowed, include row_batch.h"
+// For the sake of sane code completion.
+#include "row_batch.h"
+#endif
+#undef ROW_BATCH_INL_H_
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TRange<T> IUnversionedColumnarRowBatch::TColumn::GetTypedValues() const
+{
+ YT_VERIFY(Values);
+ YT_VERIFY(Values->BitWidth == sizeof(T) * 8);
+ return TRange<T>(
+ reinterpret_cast<const T*>(Values->Data.Begin()),
+ reinterpret_cast<const T*>(Values->Data.End()));
+}
+
+
+template <class T>
+TRange<T> IUnversionedColumnarRowBatch::TColumn::GetRelevantTypedValues() const
+{
+ YT_VERIFY(Values);
+ YT_VERIFY(Values->BitWidth == sizeof(T) * 8);
+ YT_VERIFY(!Rle);
+ return TRange<T>(
+ reinterpret_cast<const T*>(Values->Data.Begin()) + StartIndex,
+ reinterpret_cast<const T*>(Values->Data.Begin()) + StartIndex + ValueCount);
+}
+
+inline TRef IUnversionedColumnarRowBatch::TColumn::GetBitmapValues() const
+{
+ YT_VERIFY(Values);
+ YT_VERIFY(Values->BitWidth == 1);
+ YT_VERIFY(Values->BaseValue == 0);
+ YT_VERIFY(!Values->ZigZagEncoded);
+ YT_VERIFY(!Rle);
+ YT_VERIFY(!Dictionary);
+ return Values->Data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+typename TRowBatchTrait<TRow>::IRowBatchPtr CreateBatchFromRows(
+ TSharedRange<TRow> rows)
+{
+ if constexpr (std::is_same_v<TRow, TVersionedRow>) {
+ return CreateBatchFromVersionedRows(rows);
+ } else {
+ return CreateBatchFromUnversionedRows(rows);
+ }
+}
+
+template <class TRow>
+typename TRowBatchTrait<TRow>::IRowBatchPtr CreateEmptyRowBatch()
+{
+ if constexpr (std::is_same_v<TRow, TVersionedRow>) {
+ return CreateEmptyVersionedRowBatch();
+ } else {
+ return CreateEmptyUnversionedRowBatch();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/row_batch.cpp b/yt/yt/client/table_client/row_batch.cpp
new file mode 100644
index 0000000000..64926a26fd
--- /dev/null
+++ b/yt/yt/client/table_client/row_batch.cpp
@@ -0,0 +1,144 @@
+#include "row_batch.h"
+#include "unversioned_row.h"
+#include "versioned_row.h"
+
+#include <atomic>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IUnversionedRowBatch::IsEmpty() const
+{
+ return GetRowCount() == 0;
+}
+
+IUnversionedColumnarRowBatchPtr IUnversionedRowBatch::TryAsColumnar()
+{
+ return dynamic_cast<IUnversionedColumnarRowBatch*>(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedColumnarRowBatch::TDictionaryId IUnversionedColumnarRowBatch::GenerateDictionaryId()
+{
+ static std::atomic<TDictionaryId> CurrentId;
+ return ++CurrentId;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedRowBatchPtr CreateBatchFromUnversionedRows(
+ TSharedRange<TUnversionedRow> rows)
+{
+ class TUnversionedRowBatch
+ : public IUnversionedRowBatch
+ {
+ public:
+ explicit TUnversionedRowBatch(TSharedRange<TUnversionedRow> rows)
+ : Rows_(std::move(rows))
+ { }
+
+ int GetRowCount() const override
+ {
+ return static_cast<int>(Rows_.size());
+ }
+
+ TSharedRange<TUnversionedRow> MaterializeRows() override
+ {
+ return Rows_;
+ }
+
+ private:
+ const TSharedRange<TUnversionedRow> Rows_;
+ };
+
+ return New<TUnversionedRowBatch>(rows);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedRowBatchPtr CreateEmptyUnversionedRowBatch()
+{
+ class TUnversionedRowBatch
+ : public IUnversionedRowBatch
+ {
+ public:
+ int GetRowCount() const override
+ {
+ return 0;
+ }
+
+ TSharedRange<TUnversionedRow> MaterializeRows() override
+ {
+ return {};
+ }
+
+ };
+
+ return New<TUnversionedRowBatch>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IVersionedRowBatch::IsEmpty() const
+{
+ return GetRowCount() == 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedRowBatchPtr CreateBatchFromVersionedRows(
+ TSharedRange<TVersionedRow> rows)
+{
+ class TVersionedRowBatch
+ : public IVersionedRowBatch
+ {
+ public:
+ explicit TVersionedRowBatch(TSharedRange<TVersionedRow> rows)
+ : Rows_(std::move(rows))
+ { }
+
+ int GetRowCount() const override
+ {
+ return static_cast<int>(Rows_.size());
+ }
+
+ TSharedRange<TVersionedRow> MaterializeRows() override
+ {
+ return Rows_;
+ }
+
+ private:
+ const TSharedRange<TVersionedRow> Rows_;
+ };
+
+ return New<TVersionedRowBatch>(rows);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedRowBatchPtr CreateEmptyVersionedRowBatch()
+{
+ class TVersionedRowBatch
+ : public IVersionedRowBatch
+ {
+ public:
+ int GetRowCount() const override
+ {
+ return 0;
+ }
+
+ TSharedRange<TVersionedRow> MaterializeRows() override
+ {
+ return {};
+ }
+
+ };
+
+ return New<TVersionedRowBatch>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/row_batch.h b/yt/yt/client/table_client/row_batch.h
new file mode 100644
index 0000000000..29ec2b53d5
--- /dev/null
+++ b/yt/yt/client/table_client/row_batch.h
@@ -0,0 +1,269 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/range.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <vector>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IUnversionedRowBatch
+ : public virtual TRefCounted
+{
+ //! Returns the number of rows in the batch.
+ //! This call is cheap (in contrast to #IUnversionedRowBatch::MaterializeRows).
+ virtual int GetRowCount() const = 0;
+
+ //! A helper method that returns |true| iff #GetRowCount is zero.
+ bool IsEmpty() const;
+
+ //! Tries to dynamic-cast the instance to IUnversionedColumnarRowBatch;
+ //! returns null on failure.
+ IUnversionedColumnarRowBatchPtr TryAsColumnar();
+
+ //! Returns the rows representing the batch.
+ //! If the batch is columnar then the rows are materialized on first
+ //! call to #IUnversionedRowBatch::MaterializeRows. This call could be slow.
+ //! Invoking #IUnversionedColumnarRowBatch::MaterializeColumns after this call is forbidden.
+ virtual TSharedRange<TUnversionedRow> MaterializeRows() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IUnversionedRowBatch)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IUnversionedColumnarRowBatch
+ : public IUnversionedRowBatch
+{
+ struct TValueBuffer;
+ struct TStringBuffer;
+ struct TBitmap;
+ struct TDictionaryEncoding;
+ struct TRleEncoding;
+ struct TColumn;
+
+ struct TValueBuffer
+ {
+ //! Bits per value: 8, 16, 32 or 64.
+ int BitWidth = -1;
+
+ //! For integer values, values in #Data must be adjusted by adding #BaseValue.
+ ui64 BaseValue = 0;
+
+ //! Indicates if zig-zag encoding is used for values.
+ bool ZigZagEncoded = false;
+
+ //! Memory region containing the values.
+ TRef Data;
+ };
+
+ struct TStringBuffer
+ {
+ //! Controls offset decoding.
+ /*
+ * If non-null then k-th value starts at offset
+ * 0 for k = 0;
+ * AvgLength * k + ZigZagDecode32(values[k - 1]) for k = 1, ..., n-1,
+ * where |values[i]| is the i-th raw unsigned 32-bit value stored in #TColumn::Values.
+ *
+ * Otherwise #TColumn::Values contains raw unsigned 32-bit offsets.
+ */
+ std::optional<ui32> AvgLength;
+
+ //! Memory region with string data. Offsets point here.
+ TRef Data;
+ };
+
+ struct TBitmap
+ {
+ //! Bits.
+ TRef Data;
+ };
+
+ //! An opaque dictionary id.
+ using TDictionaryId = ui64;
+
+ //! This dictionary id is invalid and cannot appear in #TDictionaryEncoding::Id.
+ static constexpr TDictionaryId NullDictionaryId = 0;
+
+ //! A helper for generating unique dictionary ids.
+ static TDictionaryId GenerateDictionaryId();
+
+ struct TDictionaryEncoding
+ {
+ //! The unique id of the dictionary.
+ //! Dictionary ids are never reused (within a process incarnation).
+ //! If you see a previously appearing ids then you could safely assume that this is exactly same dictionary.
+ TDictionaryId DictionaryId = NullDictionaryId;
+
+ //! If |true| then dictionary indexes are in fact 1-based; 0 in #TColumn::Values means null
+ //! and #TColumn::NullBitmap is not used; one should subtract 1 from value in #TColumn::Values
+ //! before dereferencing the dictionary.
+ bool ZeroMeansNull = false;
+
+ //! Contains dictionary values.
+ /*!
+ * Example (assuming ZeroMeansNull is true):
+ * Raw values: hello, world, <null>, world
+ * Index values: 1, 2, 0, 2
+ * Dictionary values: hello, world
+ */
+ const TColumn* ValueColumn;
+ };
+
+ struct TRleEncoding
+ {
+ //! Contains RLE-encoded values.
+ /*!
+ * Example:
+ * Raw values: 1, 1, 2, 3, 3, 3
+ * RLE values: 1, 2, 3
+ * RLE indexes: 0, 2, 3
+ */
+ const TColumn* ValueColumn;
+ };
+
+ struct TColumn
+ {
+ //! Id in name table.
+ //! -1 for non-root columns.
+ int Id = -1;
+
+ //! Index of the first relevant value in the column.
+ //! For non-RLE encoded columns, #Values typically provides a vector whose elements starting from #StartIndex
+ //! give the desired values. For dictionary-encoded columns, these elements are not actual values but rather
+ //! dictionary indexes (see #Dictionary).
+ //! For RLE encoded columns, #Values contains starting indexes of RLE segments and #StartIndex
+ //! must be compared against these indexes to obtain the relevant range of values (stored in #Rle).
+ i64 StartIndex = -1;
+
+ //! The number of relevant values in this column (starting from #StartIndex).
+ i64 ValueCount = -1;
+
+ //! The type of values in this column.
+ NTableClient::TLogicalTypePtr Type;
+
+ //! Bitmap with ones indicating nulls.
+ //! If both #NullBitmap and #Values are null then all values are null.
+ //! If just #NullBitmap is null then all values are non-null.
+ std::optional<TBitmap> NullBitmap;
+
+ //! If non-null then values are actually indexes in #Dictionary.
+ std::optional<TDictionaryEncoding> Dictionary;
+
+ //! If non-null then contiguous segments of coinciding #Values are collapsed.
+ //! #Rle describes the resulting values and #Values store RLE indexes.
+ std::optional<TRleEncoding> Rle;
+
+ //! Somewhat encoded values.
+ /*!
+ * Encoding proceeds as follows:
+ * 1) For signed integers, zig-zag encoding could be applied (see #TValueBuffer::ZigZagEncoded)
+ * 2) For integers, some base value could be subtracted (see #TValueBuffer::MinValue)
+ * 3) Dictionary encoding could be applied; in this case values are replaced with integer dictionary indexes
+ * 4) RLE encoding could be applied: in this case value repetitions are eliminated
+ */
+ std::optional<TValueBuffer> Values;
+
+ //! Contains string data and metadata for string-like values.
+ //! Null for other types.
+ std::optional<TStringBuffer> Strings;
+
+ //! A helper for accessing all #Values.
+ template <class T>
+ TRange<T> GetTypedValues() const;
+
+ //! Similar to #GetTypedValues but the returned range only covers the part
+ //! [#StartRowIndex, #StartRowIndex + #ValueCount). This is not applicable in RLE-encoded columns.
+ template <class T>
+ TRange<T> GetRelevantTypedValues() const;
+
+ //! Provides access to values as a bitmap.
+ TRef GetBitmapValues() const;
+ };
+
+ //! Returns the (root) columns representing the batch.
+ //! The batch must be columnar.
+ //! This call is fast.
+ //! Invoking #IUnversionedRowBatch::MaterializeRows after this call is forbidden.
+ virtual TRange<const TColumn*> MaterializeColumns() = 0;
+
+ //! Contains the ids of dictionaries that are guaranteed to never be used
+ //! past this batch. These ids, however, can appear in this very batch.
+ virtual TRange<TDictionaryId> GetRetiringDictionaryIds() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IUnversionedColumnarRowBatch)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IVersionedRowBatch
+ : public virtual TRefCounted
+{
+ //! Returns the number of rows in the batch.
+ //! This call is cheap (in contrast to #IVersionedRowBatch::MaterializeRows).
+ virtual int GetRowCount() const = 0;
+
+ //! Returns the rows representing the batch.
+ virtual TSharedRange<TVersionedRow> MaterializeRows() = 0;
+
+ // Extension methods
+
+ //! Returns |true| iff #GetRowCount is zero.
+ bool IsEmpty() const;
+};
+
+DEFINE_REFCOUNTED_TYPE(IVersionedRowBatch)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IUnversionedRowBatchPtr CreateBatchFromUnversionedRows(
+ TSharedRange<TUnversionedRow> rows);
+
+IUnversionedRowBatchPtr CreateEmptyUnversionedRowBatch();
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedRowBatchPtr CreateBatchFromVersionedRows(
+ TSharedRange<TVersionedRow> rows);
+
+IVersionedRowBatchPtr CreateEmptyVersionedRowBatch();
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+struct TRowBatchTrait;
+
+template <>
+struct TRowBatchTrait<TUnversionedRow>
+{
+ using IRowBatchPtr = IUnversionedRowBatchPtr;
+};
+
+template <>
+struct TRowBatchTrait<TVersionedRow>
+{
+ using IRowBatchPtr = IVersionedRowBatchPtr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRow>
+typename TRowBatchTrait<TRow>::IRowBatchPtr CreateBatchFromRows(
+ TSharedRange<TRow> rows);
+
+template <class TRow>
+typename TRowBatchTrait<TRow>::IRowBatchPtr CreateEmptyRowBatch();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define ROW_BATCH_INL_H_
+#include "row_batch-inl.h"
+#undef ROW_BATCH_INL_H_
diff --git a/yt/yt/client/table_client/row_buffer.cpp b/yt/yt/client/table_client/row_buffer.cpp
new file mode 100644
index 0000000000..40b85f7f1f
--- /dev/null
+++ b/yt/yt/client/table_client/row_buffer.cpp
@@ -0,0 +1,310 @@
+#include "row_buffer.h"
+
+#include "schema.h"
+#include "unversioned_row.h"
+#include "versioned_row.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChunkedMemoryPool* TRowBuffer::GetPool()
+{
+ return &Pool_;
+}
+
+TMutableUnversionedRow TRowBuffer::AllocateUnversioned(int valueCount)
+{
+ return TMutableUnversionedRow::Allocate(&Pool_, valueCount);
+}
+
+TMutableVersionedRow TRowBuffer::AllocateVersioned(
+ int keyCount,
+ int valueCount,
+ int writeTimestampCount,
+ int deleteTimestampCount)
+{
+ return TMutableVersionedRow::Allocate(
+ &Pool_,
+ keyCount,
+ valueCount,
+ writeTimestampCount,
+ deleteTimestampCount);
+}
+
+void TRowBuffer::CaptureValue(TUnversionedValue* value)
+{
+ if (IsStringLikeType(value->Type) && value->Data.String != nullptr) {
+ char* dst = Pool_.AllocateUnaligned(value->Length);
+ memcpy(dst, value->Data.String, value->Length);
+ value->Data.String = dst;
+ }
+}
+
+TVersionedValue TRowBuffer::CaptureValue(const TVersionedValue& value)
+{
+ auto capturedValue = value;
+ CaptureValue(&capturedValue);
+ return capturedValue;
+}
+
+TUnversionedValue TRowBuffer::CaptureValue(const TUnversionedValue& value)
+{
+ auto capturedValue = value;
+ CaptureValue(&capturedValue);
+ return capturedValue;
+}
+
+TMutableUnversionedRow TRowBuffer::CaptureRow(TUnversionedRow row, bool captureValues)
+{
+ if (!row) {
+ return TMutableUnversionedRow();
+ }
+
+ return CaptureRow(row.Elements(), captureValues);
+}
+
+void TRowBuffer::CaptureValues(TMutableUnversionedRow row)
+{
+ if (!row) {
+ return;
+ }
+
+ for (ui32 index = 0; index < row.GetCount(); ++index) {
+ CaptureValue(&row[index]);
+ }
+}
+
+TMutableUnversionedRow TRowBuffer::CaptureRow(TUnversionedValueRange values, bool captureValues)
+{
+ int count = std::ssize(values);
+ auto capturedRow = TMutableUnversionedRow::Allocate(&Pool_, count);
+ auto* capturedBegin = capturedRow.Begin();
+
+ ::memcpy(capturedBegin, values.Begin(), count * sizeof (TUnversionedValue));
+
+ if (captureValues) {
+ for (int index = 0; index < count; ++index) {
+ CaptureValue(&capturedBegin[index]);
+ }
+ }
+
+ return capturedRow;
+}
+
+std::vector<TMutableUnversionedRow> TRowBuffer::CaptureRows(TRange<TUnversionedRow> rows, bool captureValues)
+{
+ int rowCount = static_cast<int>(rows.Size());
+ std::vector<TMutableUnversionedRow> capturedRows(rowCount);
+ for (int index = 0; index < rowCount; ++index) {
+ capturedRows[index] = CaptureRow(rows[index], captureValues);
+ }
+ return capturedRows;
+}
+
+TMutableUnversionedRow TRowBuffer::CaptureAndPermuteRow(
+ TUnversionedRow row,
+ const TTableSchema& tableSchema,
+ int schemafulColumnCount,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer)
+{
+ int valueCount = schemafulColumnCount;
+
+ if (columnPresenceBuffer) {
+ ValidateDuplicateAndRequiredValueColumns(row, tableSchema, idMapping, columnPresenceBuffer);
+ }
+
+ for (const auto& value : row) {
+ ui16 originalId = value.Id;
+ YT_VERIFY(originalId < idMapping.size());
+ int mappedId = idMapping[originalId];
+ if (mappedId < 0) {
+ continue;
+ }
+ if (mappedId >= schemafulColumnCount) {
+ ++valueCount;
+ }
+ }
+
+ auto capturedRow = TMutableUnversionedRow::Allocate(&Pool_, valueCount);
+ for (int pos = 0; pos < schemafulColumnCount; ++pos) {
+ capturedRow[pos] = MakeUnversionedNullValue(pos);
+ }
+
+ valueCount = schemafulColumnCount;
+
+ for (const auto& value : row) {
+ ui16 originalId = value.Id;
+ int mappedId = idMapping[originalId];
+ if (mappedId < 0) {
+ continue;
+ }
+ int pos = mappedId < schemafulColumnCount ? mappedId : valueCount++;
+ capturedRow[pos] = value;
+ capturedRow[pos].Id = mappedId;
+ }
+
+ return capturedRow;
+}
+
+TMutableVersionedRow TRowBuffer::CaptureRow(TVersionedRow row, bool captureValues)
+{
+ if (!row) {
+ return TMutableVersionedRow();
+ }
+
+ auto capturedRow = TMutableVersionedRow::Allocate(
+ &Pool_,
+ row.GetKeyCount(),
+ row.GetValueCount(),
+ row.GetWriteTimestampCount(),
+ row.GetDeleteTimestampCount());
+ ::memcpy(capturedRow.BeginKeys(), row.BeginKeys(), sizeof(TUnversionedValue) * row.GetKeyCount());
+ ::memcpy(capturedRow.BeginValues(), row.BeginValues(), sizeof(TVersionedValue) * row.GetValueCount());
+ ::memcpy(capturedRow.BeginWriteTimestamps(), row.BeginWriteTimestamps(), sizeof(TTimestamp) * row.GetWriteTimestampCount());
+ ::memcpy(capturedRow.BeginDeleteTimestamps(), row.BeginDeleteTimestamps(), sizeof(TTimestamp) * row.GetDeleteTimestampCount());
+
+ if (captureValues) {
+ CaptureValues(capturedRow);
+ }
+
+ return capturedRow;
+}
+
+void TRowBuffer::CaptureValues(TMutableVersionedRow row)
+{
+ if (!row) {
+ return;
+ }
+
+ for (auto& value : row.Keys()) {
+ CaptureValue(&value);
+ }
+ for (auto& value : row.Values()) {
+ CaptureValue(&value);
+ }
+}
+
+TMutableVersionedRow TRowBuffer::CaptureAndPermuteRow(
+ TVersionedRow row,
+ const TTableSchema& tableSchema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer,
+ bool allowMissingKeyColumns)
+{
+ int keyColumnCount = tableSchema.GetKeyColumnCount();
+
+ if (!allowMissingKeyColumns) {
+ YT_VERIFY(keyColumnCount == row.GetKeyCount());
+ YT_VERIFY(keyColumnCount <= std::ssize(idMapping));
+ }
+
+ int valueCount = 0;
+ int deleteTimestampCount = row.GetDeleteTimestampCount();
+
+ TCompactVector<TTimestamp, 64> writeTimestamps;
+ for (const auto& value : row.Values()) {
+ ui16 originalId = value.Id;
+ YT_VERIFY(originalId < idMapping.size());
+ int mappedId = idMapping[originalId];
+ if (mappedId < 0) {
+ continue;
+ }
+ YT_VERIFY(mappedId < std::ssize(tableSchema.Columns()));
+ ++valueCount;
+ writeTimestamps.push_back(value.Timestamp);
+ }
+
+ std::sort(writeTimestamps.begin(), writeTimestamps.end(), std::greater<TTimestamp>());
+ writeTimestamps.erase(std::unique(writeTimestamps.begin(), writeTimestamps.end()), writeTimestamps.end());
+ int writeTimestampCount = static_cast<int>(writeTimestamps.size());
+
+ if (columnPresenceBuffer) {
+ ValidateDuplicateAndRequiredValueColumns(
+ row,
+ tableSchema,
+ idMapping,
+ columnPresenceBuffer,
+ writeTimestamps.data(),
+ writeTimestampCount);
+ }
+
+ auto capturedRow = TMutableVersionedRow::Allocate(
+ &Pool_,
+ keyColumnCount,
+ valueCount,
+ writeTimestampCount,
+ deleteTimestampCount);
+
+ ::memcpy(capturedRow.BeginWriteTimestamps(), writeTimestamps.data(), sizeof (TTimestamp) * writeTimestampCount);
+ ::memcpy(capturedRow.BeginDeleteTimestamps(), row.BeginDeleteTimestamps(), sizeof (TTimestamp) * deleteTimestampCount);
+
+ if (!allowMissingKeyColumns) {
+ int index = 0;
+ auto* dstValue = capturedRow.BeginKeys();
+ for (const auto* srcValue = row.BeginKeys(); srcValue != row.EndKeys(); ++srcValue, ++index) {
+ YT_VERIFY(idMapping[index] == index);
+ *dstValue++ = *srcValue;
+ }
+ } else {
+ for (int index = 0; index < keyColumnCount; ++index) {
+ capturedRow.Keys()[index] = MakeUnversionedNullValue(index);
+ }
+ for (const auto& srcValue : row.Keys()) {
+ ui16 originalId = srcValue.Id;
+ int mappedId = idMapping[originalId];
+ if (mappedId < 0) {
+ continue;
+ }
+ auto* dstValue = &capturedRow.Keys()[mappedId];
+ *dstValue = srcValue;
+ dstValue->Id = mappedId;
+ }
+ }
+
+ {
+ auto* dstValue = capturedRow.BeginValues();
+ for (const auto& srcValue : row.Values()) {
+ ui16 originalId = srcValue.Id;
+ int mappedId = idMapping[originalId];
+ if (mappedId < 0) {
+ continue;
+ }
+ *dstValue = srcValue;
+ dstValue->Id = mappedId;
+ ++dstValue;
+ }
+ }
+
+ return capturedRow;
+}
+
+void TRowBuffer::Absorb(TRowBuffer&& other)
+{
+ Pool_.Absorb(std::move(other.Pool_));
+}
+
+i64 TRowBuffer::GetSize() const
+{
+ return Pool_.GetSize();
+}
+
+i64 TRowBuffer::GetCapacity() const
+{
+ return Pool_.GetCapacity();
+}
+
+void TRowBuffer::Clear()
+{
+ Pool_.Clear();
+}
+
+void TRowBuffer::Purge()
+{
+ Pool_.Purge();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/row_buffer.h b/yt/yt/client/table_client/row_buffer.h
new file mode 100644
index 0000000000..48f6494016
--- /dev/null
+++ b/yt/yt/client/table_client/row_buffer.h
@@ -0,0 +1,114 @@
+#pragma once
+
+#include "public.h"
+#include "unversioned_row.h"
+#include "versioned_row.h"
+
+#include <library/cpp/yt/memory/chunked_memory_pool.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDefaultRowBufferPoolTag { };
+
+//! Holds data for a bunch of rows.
+/*!
+ * Acts as a ref-counted wrapped around TChunkedMemoryPool plus a bunch
+ * of helpers.
+ */
+class TRowBuffer
+ : public TRefCounted
+{
+public:
+ TRowBuffer(
+ TRefCountedTypeCookie tagCookie,
+ IMemoryChunkProviderPtr chunkProvider,
+ size_t startChunkSize = TChunkedMemoryPool::DefaultStartChunkSize)
+ : Pool_(
+ tagCookie,
+ std::move(chunkProvider),
+ startChunkSize)
+ { }
+
+ template <class TTag = TDefaultRowBufferPoolTag>
+ explicit TRowBuffer(
+ TTag = TDefaultRowBufferPoolTag(),
+ size_t startChunkSize = TChunkedMemoryPool::DefaultStartChunkSize)
+ : Pool_(
+ TTag(),
+ startChunkSize)
+ { }
+
+ template <class TTag>
+ TRowBuffer(
+ TTag,
+ IMemoryChunkProviderPtr chunkProvider)
+ : Pool_(
+ GetRefCountedTypeCookie<TTag>(),
+ std::move(chunkProvider))
+ { }
+
+ TChunkedMemoryPool* GetPool();
+
+ TMutableUnversionedRow AllocateUnversioned(int valueCount);
+ TMutableVersionedRow AllocateVersioned(
+ int keyCount,
+ int valueCount,
+ int writeTimestampCount,
+ int deleteTimestampCount);
+
+ void CaptureValue(TUnversionedValue* value);
+ TVersionedValue CaptureValue(const TVersionedValue& value);
+ TUnversionedValue CaptureValue(const TUnversionedValue& value);
+
+ TMutableUnversionedRow CaptureRow(TUnversionedRow row, bool captureValues = true);
+ void CaptureValues(TMutableUnversionedRow row);
+ TMutableUnversionedRow CaptureRow(TUnversionedValueRange values, bool captureValues = true);
+ std::vector<TMutableUnversionedRow> CaptureRows(TRange<TUnversionedRow> rows, bool captureValues = true);
+
+ TMutableVersionedRow CaptureRow(TVersionedRow row, bool captureValues = true);
+ void CaptureValues(TMutableVersionedRow row);
+
+ //! Captures the row applying #idMapping to value ids and placing values to the proper positions.
+ //! Skips values that map to negative ids via #idMapping.
+ //! The resulting row consists of a schemaful prefix consisting of values whose ids
+ //! (after remapping) are less than #schemafulColumnCount
+ //! and an arbitrarily-ordered suffix of values whose ids
+ //! (after remapping) are greater than or equal to #schemafulColumnCount.
+ TMutableUnversionedRow CaptureAndPermuteRow(
+ TUnversionedRow row,
+ const TTableSchema& tableSchema,
+ int schemafulColumnCount,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer);
+
+ //! Captures the row applying #idMapping to value ids.
+ //! #idMapping must be identity for key columns.
+ //! Skips values that map to negative ids via #idMapping.
+ TMutableVersionedRow CaptureAndPermuteRow(
+ TVersionedRow row,
+ const TTableSchema& tableSchema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer,
+ bool allowMissingKeyColumns = false);
+
+ i64 GetSize() const;
+ i64 GetCapacity() const;
+
+ //! Moves all the rows from other row buffer to the current one.
+ //! The other buffer becomes empty, like after Purge() call.
+ void Absorb(TRowBuffer&& other);
+
+ void Clear();
+ void Purge();
+
+private:
+ TChunkedMemoryPool Pool_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TRowBuffer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schema-inl.h b/yt/yt/client/table_client/schema-inl.h
new file mode 100644
index 0000000000..e2949f6aa9
--- /dev/null
+++ b/yt/yt/client/table_client/schema-inl.h
@@ -0,0 +1,227 @@
+#ifndef SCHEMA_INL_H_
+#error "Direct inclusion of this file is not allowed, include schema.h"
+// For the sake of sane code completion.
+#include "schema.h"
+#endif
+
+#include <yt/yt/core/misc/numeric_helpers.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TLegacyLockMask::TLegacyLockMask(TLegacyLockBitmap value)
+ : Data_(value)
+{ }
+
+inline ELockType TLegacyLockMask::Get(int index) const
+{
+ return ELockType((Data_ >> (BitsPerType * index)) & TypeMask);
+}
+
+inline void TLegacyLockMask::Set(int index, ELockType lock)
+{
+ YT_VERIFY(lock <= MaxOldLockType);
+ Data_ &= ~(TypeMask << (BitsPerType * index));
+ Data_ |= static_cast<TLegacyLockBitmap>(lock) << (BitsPerType * index);
+}
+
+inline void TLegacyLockMask::Enrich(int columnCount)
+{
+ auto primaryLockType = Get(PrimaryLockIndex);
+ auto maxLockType = primaryLockType;
+ for (int index = 1; index < columnCount; ++index) {
+ auto lockType = Get(index);
+ Set(index, GetStrongestLock(primaryLockType, lockType));
+ maxLockType = GetStrongestLock(maxLockType, lockType);
+ }
+ Set(PrimaryLockIndex, maxLockType);
+}
+
+inline TLegacyLockBitmap TLegacyLockMask::GetBitmap() const
+{
+ return Data_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TLockMask::TLockMask(TLockBitmap bitmap, int size)
+ : Bitmap_(std::move(bitmap))
+ , Size_(size)
+{ }
+
+inline ELockType TLockMask::Get(int index) const
+{
+ auto wordIndex = index / LocksPerWord;
+ if (wordIndex < std::ssize(Bitmap_)) {
+ auto wordPosition = index & (LocksPerWord - 1);
+ auto lock = (Bitmap_[wordIndex] >> (BitsPerType * wordPosition)) & LockMask;
+ return CheckedEnumCast<ELockType>(lock);
+ } else {
+ return ELockType::None;
+ }
+}
+
+inline void TLockMask::Set(int index, ELockType lock)
+{
+ if (index >= Size_) {
+ Reserve(index + 1);
+ }
+
+ auto& word = Bitmap_[index / LocksPerWord];
+ auto wordPosition = index & (LocksPerWord - 1);
+
+ word &= ~(LockMask << (BitsPerType * wordPosition));
+ word |= static_cast<ui64>(lock) << (BitsPerType * wordPosition);
+}
+
+inline void TLockMask::Enrich(int size)
+{
+ if (size > Size_) {
+ Reserve(size);
+ }
+ Size_ = size;
+
+ auto primaryLockType = Get(PrimaryLockIndex);
+ auto maxLockType = primaryLockType;
+ for (int index = 1; index < size; ++index) {
+ auto lockType = Get(index);
+ Set(index, GetStrongestLock(primaryLockType, lockType));
+ maxLockType = GetStrongestLock(maxLockType, lockType);
+ }
+ // TODO(gritukan): Do we really need both to promote all locks up to primary lock
+ // and promote primary lock to strongest of all locks in mask?
+ Set(PrimaryLockIndex, maxLockType);
+}
+
+inline int TLockMask::GetSize() const
+{
+ return Size_;
+}
+
+inline TLockBitmap TLockMask::GetBitmap() const
+{
+ return Bitmap_;
+}
+
+inline TLegacyLockMask TLockMask::ToLegacyMask() const
+{
+ TLegacyLockMask legacyMask;
+ for (int index = 0; index < TLegacyLockMask::MaxCount; ++index) {
+ legacyMask.Set(index, Get(index));
+ }
+
+ return legacyMask;
+}
+
+inline bool TLockMask::HasNewLocks() const
+{
+ for (int index = 0; index < Size_; ++index) {
+ if (Get(index) > MaxOldLockType) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+inline void TLockMask::Reserve(int size)
+{
+ YT_VERIFY(size < MaxSize);
+
+ int wordCount = DivCeil(size, LocksPerWord);
+ if (wordCount > std::ssize(Bitmap_)) {
+ Bitmap_.resize(wordCount);
+ }
+
+ Size_ = size;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr bool operator < (ESchemaCompatibility lhs, ESchemaCompatibility rhs)
+{
+ return static_cast<int>(lhs) < static_cast<int>(rhs);
+}
+
+constexpr bool operator > (ESchemaCompatibility lhs, ESchemaCompatibility rhs)
+{
+ return rhs < lhs;
+}
+
+constexpr bool operator <= (ESchemaCompatibility lhs, ESchemaCompatibility rhs)
+{
+ return !(rhs < lhs);
+}
+
+constexpr bool operator >= (ESchemaCompatibility lhs, ESchemaCompatibility rhs)
+{
+ return !(lhs < rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline size_t TTableSchemaHash::operator() (const TTableSchema& schema) const
+{
+ return THash<TTableSchema>()(schema);
+}
+
+inline size_t TTableSchemaHash::operator() (const TTableSchemaPtr& schema) const
+{
+ return THash<TTableSchema>()(*schema);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool TTableSchemaEquals::operator() (const TTableSchema& lhs, const TTableSchema& rhs) const
+{
+ return lhs == rhs;
+}
+
+inline bool TTableSchemaEquals::operator() (const TTableSchemaPtr& lhs, const TTableSchemaPtr& rhs) const
+{
+ return *lhs == *rhs;
+}
+
+inline bool TTableSchemaEquals::operator() (const TTableSchemaPtr& lhs, const TTableSchema& rhs) const
+{
+ return *lhs == rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline size_t TCellTaggedTableSchemaHash::operator() (const TCellTaggedTableSchema& cellTaggedSchema) const
+{
+ return MultiHash(cellTaggedSchema.TableSchema, cellTaggedSchema.CellTag);
+}
+
+inline size_t TCellTaggedTableSchemaHash::operator() (const TCellTaggedTableSchemaPtr& cellTaggedSchemaPtr) const
+{
+ YT_ASSERT(cellTaggedSchemaPtr.TableSchema);
+ return MultiHash(*cellTaggedSchemaPtr.TableSchema, cellTaggedSchemaPtr.CellTag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool TCellTaggedTableSchemaEquals::operator() (const TCellTaggedTableSchema& lhs, const TCellTaggedTableSchema& rhs) const
+{
+ return lhs.TableSchema == rhs.TableSchema && lhs.CellTag == rhs.CellTag;
+}
+
+inline bool TCellTaggedTableSchemaEquals::operator() (const TCellTaggedTableSchemaPtr& lhs, const TCellTaggedTableSchemaPtr& rhs) const
+{
+ YT_ASSERT(lhs.TableSchema && rhs.TableSchema);
+ return *lhs.TableSchema == *rhs.TableSchema && lhs.CellTag == rhs.CellTag;
+}
+
+inline bool TCellTaggedTableSchemaEquals::operator() (const TCellTaggedTableSchemaPtr& lhs, const TCellTaggedTableSchema& rhs) const
+{
+ YT_ASSERT(lhs.TableSchema);
+ return *lhs.TableSchema == rhs.TableSchema && lhs.CellTag == rhs.CellTag;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schema.cpp b/yt/yt/client/table_client/schema.cpp
new file mode 100644
index 0000000000..28ff427853
--- /dev/null
+++ b/yt/yt/client/table_client/schema.cpp
@@ -0,0 +1,2002 @@
+#include "schema.h"
+
+#include "column_sort_schema.h"
+#include "comparator.h"
+#include "logical_type.h"
+#include "unversioned_row.h"
+
+#include <optional>
+
+#include <yt/yt/client/tablet_client/public.h>
+
+#include <yt/yt/client/table_client/schema_serialization_helpers.h>
+
+#include <yt/yt/core/yson/public.h>
+#include <yt/yt/core/yson/pull_parser_deserialize.h>
+
+#include <yt/yt/core/ytree/serialize.h>
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/wire_protocol.pb.h>
+
+namespace NYT::NTableClient {
+
+using namespace NChunkClient;
+using namespace NObjectClient;
+using namespace NTabletClient;
+using namespace NYson;
+using namespace NYTree;
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+/////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+int GetLockPriority(ELockType lockType)
+{
+ switch (lockType) {
+ case ELockType::None:
+ return 0;
+ case ELockType::SharedWeak:
+ return 1;
+ case ELockType::SharedStrong:
+ return 2;
+ case ELockType::Exclusive:
+ return 3;
+ default:
+ YT_ABORT();
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+ELockType GetStrongestLock(ELockType lhs, ELockType rhs)
+{
+ return GetLockPriority(lhs) > GetLockPriority(rhs) ? lhs : rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLockMask MaxMask(TLockMask lhs, TLockMask rhs)
+{
+ int size = std::max<int>(lhs.GetSize(), rhs.GetSize());
+ for (int index = 0; index < size; ++index) {
+ lhs.Set(index, GetStrongestLock(lhs.Get(index), rhs.Get(index)));
+ }
+
+ return lhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStableName::TStableName(TString stableName)
+ : Name_(std::move(stableName))
+{ }
+
+const TString& TStableName::Get() const
+{
+ return Name_;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TStableName& stableName, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v", stableName.Get());
+}
+
+bool operator == (const TStableName& lhs, const TStableName& rhs)
+{
+ return lhs.Get() == rhs.Get();
+}
+
+bool operator < (const TStableName& lhs, const TStableName& rhs)
+{
+ return lhs.Get() < rhs.Get();
+}
+
+void ToProto(TString* protoStableName, const TStableName& stableName)
+{
+ *protoStableName = stableName.Get();
+}
+
+void FromProto(TStableName* stableName, const TString& protoStableName)
+{
+ *stableName = TStableName(protoStableName);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TColumnSchema::TColumnSchema()
+ : TColumnSchema(
+ TString(),
+ NullLogicalType(),
+ std::nullopt)
+{ }
+
+TColumnSchema::TColumnSchema(
+ TString name,
+ EValueType type,
+ std::optional<ESortOrder> sortOrder)
+ : TColumnSchema(
+ std::move(name),
+ MakeLogicalType(GetLogicalType(type), /*required*/ false),
+ sortOrder)
+{ }
+
+TColumnSchema::TColumnSchema(
+ TString name,
+ ESimpleLogicalValueType type,
+ std::optional<ESortOrder> sortOrder)
+ : TColumnSchema(
+ std::move(name),
+ MakeLogicalType(type, /*required*/ false),
+ sortOrder)
+{ }
+
+TColumnSchema::TColumnSchema(
+ TString name,
+ TLogicalTypePtr type,
+ std::optional<ESortOrder> sortOrder)
+ : StableName_(name)
+ , Name_(name)
+ , SortOrder_(sortOrder)
+{
+ SetLogicalType(std::move(type));
+}
+
+TColumnSchema& TColumnSchema::SetStableName(TStableName value)
+{
+ StableName_ = std::move(value);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetName(TString value)
+{
+ Name_ = value;
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetSortOrder(std::optional<ESortOrder> value)
+{
+ SortOrder_ = value;
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetLock(std::optional<TString> value)
+{
+ Lock_ = std::move(value);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetGroup(std::optional<TString> value)
+{
+ Group_ = std::move(value);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetExpression(std::optional<TString> value)
+{
+ Expression_ = std::move(value);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetAggregate(std::optional<TString> value)
+{
+ Aggregate_ = std::move(value);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetRequired(bool value)
+{
+ if (!IsOfV1Type_) {
+ THROW_ERROR_EXCEPTION("Cannot set required flag for non-v1 typed column");
+ }
+ SetLogicalType(MakeLogicalType(V1Type_, value));
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetMaxInlineHunkSize(std::optional<i64> value)
+{
+ MaxInlineHunkSize_ = value;
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetLogicalType(TLogicalTypePtr type)
+{
+ LogicalType_ = std::move(type);
+ IsOfV1Type_ = IsV1Type(LogicalType_);
+ std::tie(V1Type_, Required_) = NTableClient::CastToV1Type(LogicalType_);
+ return *this;
+}
+
+TColumnSchema& TColumnSchema::SetSimpleLogicalType(ESimpleLogicalValueType type)
+{
+ SetLogicalType(MakeLogicalType(type, /*required*/ false));
+ return *this;
+}
+
+EValueType TColumnSchema::GetWireType() const
+{
+ return NTableClient::GetWireType(LogicalType_);
+}
+
+i64 TColumnSchema::GetMemoryUsage() const
+{
+ return
+ sizeof(TColumnSchema) +
+ StableName_.Get().size() +
+ Name_.size() +
+ (LogicalType_ ? LogicalType_->GetMemoryUsage() : 0) +
+ (Lock_ ? Lock_->size() : 0) +
+ (Expression_ ? Expression_->size() : 0) +
+ (Aggregate_ ? Aggregate_->size() : 0) +
+ (Group_ ? Group_->size() : 0);
+}
+
+bool TColumnSchema::IsOfV1Type() const
+{
+ return IsOfV1Type_;
+}
+
+bool TColumnSchema::IsOfV1Type(ESimpleLogicalValueType type) const
+{
+ return IsOfV1Type_ && V1Type_ == type;
+}
+
+ESimpleLogicalValueType TColumnSchema::CastToV1Type() const
+{
+ return V1Type_;
+}
+
+bool TColumnSchema::IsRenamed() const
+{
+ return Name() != StableName().Get();
+}
+
+TString TColumnSchema::GetDiagnosticNameString() const
+{
+ if (IsRenamed()) {
+ return Format("%Qv (stable name %Qv)", Name(), StableName().Get());
+ } else {
+ return Format("%Qv", Name());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDeletedColumn::TDeletedColumn()
+{
+}
+
+TDeletedColumn::TDeletedColumn(TStableName stableName)
+ : StableName_(stableName)
+{
+}
+
+TDeletedColumn& TDeletedColumn::SetStableName(TStableName value)
+{
+ StableName_ = std::move(value);
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TColumnSchema& schema, TStringBuf /*spec*/)
+{
+ builder->AppendChar('{');
+
+ builder->AppendFormat("name=%Qv", schema.Name());
+
+ if (schema.IsRenamed()) {
+ builder->AppendFormat("; stable_name=%Qv", schema.StableName());
+ }
+
+ if (const auto& logicalType = schema.LogicalType()) {
+ builder->AppendFormat("; type=%v", *logicalType);
+ }
+
+ if (const auto& sortOrder = schema.SortOrder()) {
+ builder->AppendFormat("; sort_order=%v", *sortOrder);
+ }
+
+ if (const auto& lock = schema.Lock()) {
+ builder->AppendFormat("; lock=%v", *lock);
+ }
+
+ if (const auto& expression = schema.Expression()) {
+ builder->AppendFormat("; expression=%Qv", *expression);
+ }
+
+ if (const auto& aggregate = schema.Aggregate()) {
+ builder->AppendFormat("; aggregate=%v", *aggregate);
+ }
+
+ if (const auto& group = schema.Group()) {
+ builder->AppendFormat("; group=%v", *group);
+ }
+
+ builder->AppendFormat("; physical_type=%v", CamelCaseToUnderscoreCase(ToString(schema.CastToV1Type())));
+
+ builder->AppendFormat("; required=%v", schema.Required());
+
+ if (auto maxInlineHunkSize = schema.MaxInlineHunkSize()) {
+ builder->AppendFormat("; max_inline_hunk_size=%v", *maxInlineHunkSize);
+ }
+
+ builder->AppendChar('}');
+}
+
+void FormatValue(TStringBuilderBase* builder, const TDeletedColumn& schema, TStringBuf spec)
+{
+ builder->AppendChar('{');
+
+ FormatValue(builder, schema.StableName(), spec);
+ builder->AppendFormat("; deleted=true");
+
+ builder->AppendChar('}');
+}
+
+void ToProto(NProto::TColumnSchema* protoSchema, const TColumnSchema& schema)
+{
+ protoSchema->set_name(schema.Name());
+ if (schema.IsRenamed()) {
+ protoSchema->set_stable_name(schema.StableName().Get());
+ }
+
+ protoSchema->set_type(static_cast<int>(GetPhysicalType(schema.CastToV1Type())));
+
+ if (schema.IsOfV1Type()) {
+ protoSchema->set_simple_logical_type(static_cast<int>(schema.CastToV1Type()));
+ } else {
+ protoSchema->clear_simple_logical_type();
+ }
+ if (schema.Required()) {
+ protoSchema->set_required(true);
+ } else {
+ protoSchema->clear_required();
+ }
+ ToProto(protoSchema->mutable_logical_type(), schema.LogicalType());
+ if (schema.Lock()) {
+ protoSchema->set_lock(*schema.Lock());
+ } else {
+ protoSchema->clear_lock();
+ }
+ if (schema.Expression()) {
+ protoSchema->set_expression(*schema.Expression());
+ } else {
+ protoSchema->clear_expression();
+ }
+ if (schema.Aggregate()) {
+ protoSchema->set_aggregate(*schema.Aggregate());
+ } else {
+ protoSchema->clear_aggregate();
+ }
+ if (schema.SortOrder()) {
+ protoSchema->set_sort_order(static_cast<int>(*schema.SortOrder()));
+ } else {
+ protoSchema->clear_sort_order();
+ }
+ if (schema.Group()) {
+ protoSchema->set_group(*schema.Group());
+ } else {
+ protoSchema->clear_group();
+ }
+ if (schema.MaxInlineHunkSize()) {
+ protoSchema->set_max_inline_hunk_size(*schema.MaxInlineHunkSize());
+ } else {
+ protoSchema->clear_max_inline_hunk_size();
+ }
+}
+
+void ToProto(NProto::TDeletedColumn* protoSchema, const TDeletedColumn& schema)
+{
+ protoSchema->set_stable_name(schema.StableName().Get());
+}
+
+void FromProto(TColumnSchema* schema, const NProto::TColumnSchema& protoSchema)
+{
+ schema->SetName(protoSchema.name());
+ schema->SetStableName(protoSchema.has_stable_name()
+ ? TStableName(protoSchema.stable_name())
+ : TStableName(protoSchema.name()));
+
+ if (protoSchema.has_logical_type()) {
+ TLogicalTypePtr logicalType;
+ FromProto(&logicalType, protoSchema.logical_type());
+ schema->SetLogicalType(logicalType);
+ } else if (protoSchema.has_simple_logical_type()) {
+ schema->SetLogicalType(
+ MakeLogicalType(
+ CheckedEnumCast<ESimpleLogicalValueType>(protoSchema.simple_logical_type()),
+ protoSchema.required()));
+ } else {
+ auto physicalType = CheckedEnumCast<EValueType>(protoSchema.type());
+ schema->SetLogicalType(MakeLogicalType(GetLogicalType(physicalType), protoSchema.required()));
+ }
+
+ schema->SetLock(protoSchema.has_lock() ? std::make_optional(protoSchema.lock()) : std::nullopt);
+ schema->SetExpression(protoSchema.has_expression() ? std::make_optional(protoSchema.expression()) : std::nullopt);
+ schema->SetAggregate(protoSchema.has_aggregate() ? std::make_optional(protoSchema.aggregate()) : std::nullopt);
+ schema->SetSortOrder(protoSchema.has_sort_order() ? std::make_optional(CheckedEnumCast<ESortOrder>(protoSchema.sort_order())) : std::nullopt);
+ schema->SetGroup(protoSchema.has_group() ? std::make_optional(protoSchema.group()) : std::nullopt);
+ schema->SetMaxInlineHunkSize(protoSchema.has_max_inline_hunk_size() ? std::make_optional(protoSchema.max_inline_hunk_size()) : std::nullopt);
+}
+
+void FromProto(TDeletedColumn* schema, const NProto::TDeletedColumn& protoSchema)
+{
+ schema->SetStableName(TStableName{protoSchema.stable_name()});
+}
+
+void PrintTo(const TColumnSchema& columnSchema, std::ostream* os)
+{
+ *os << Format("%v", columnSchema);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableSchema::TNameMapping::TNameMapping(const TTableSchema& schema)
+ : Schema_(schema)
+{ }
+
+bool TTableSchema::TNameMapping::IsDeleted(const TStableName& stableName) const
+{
+ return Schema_.FindDeletedColumn(stableName) != nullptr;
+}
+
+TString TTableSchema::TNameMapping::StableNameToName(const TStableName& stableName) const
+{
+ auto* column = Schema_.FindColumnByStableName(stableName);
+ if (!column) {
+ if (Schema_.GetStrict()) {
+ THROW_ERROR_EXCEPTION("No column with stable name %Qv in strict schema", stableName);
+ }
+ return stableName.Get();
+ }
+ return column->Name();
+}
+
+TStableName TTableSchema::TNameMapping::NameToStableName(TStringBuf name) const
+{
+ auto* column = Schema_.FindColumn(name);
+ if (!column) {
+ if (Schema_.GetStrict()) {
+ THROW_ERROR_EXCEPTION("No column with name %Qv in strict schema", name);
+ }
+ return TStableName(TString(name));
+ }
+ return column->StableName();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::vector<TColumnSchema>& TTableSchema::Columns() const
+{
+ if (Columns_) [[likely]] {
+ return *Columns_;
+ } else {
+ static const std::vector<TColumnSchema> empty;
+ return empty;
+ }
+}
+
+const std::vector<TDeletedColumn>& TTableSchema::DeletedColumns() const
+{
+ return DeletedColumns_;
+}
+
+TTableSchema::TTableSchema(
+ std::vector<TColumnSchema> columns,
+ bool strict,
+ bool uniqueKeys,
+ ETableSchemaModification schemaModification,
+ std::vector<TDeletedColumn> deletedColumns)
+ : Strict_(strict)
+ , UniqueKeys_(uniqueKeys)
+ , SchemaModification_(schemaModification)
+ , Columns_(std::make_shared<const std::vector<TColumnSchema>>(std::move(columns)))
+ , DeletedColumns_(std::move(deletedColumns))
+{
+ for (int index = 0; index < std::ssize(Columns()); ++index) {
+ const auto& column = Columns()[index];
+ if (column.SortOrder()) {
+ ++KeyColumnCount_;
+ }
+ if (column.Expression()) {
+ HasComputedColumns_ = true;
+ }
+ if (column.Aggregate()) {
+ HasAggregateColumns_ = true;
+ }
+ if (column.MaxInlineHunkSize()) {
+ HunkColumnsIds_.push_back(index);
+ }
+ // NB(levysotsky): We ignore duplicates in both maps, they will be
+ // accounted for in consequent validation.
+ NameToColumnIndex_.emplace(column.Name(), index);
+ StableNameToColumnIndex_.emplace(column.StableName().Get(), index);
+ }
+ for (int index = 0; index < std::ssize(DeletedColumns()); ++index) {
+ const auto& deletedColumn = DeletedColumns()[index];
+ StableNameToDeletedColumnIndex_.emplace(deletedColumn.StableName().Get(), index);
+ }
+}
+
+const TColumnSchema* TTableSchema::FindColumnByStableName(const TStableName& stableName) const
+{
+ auto it = StableNameToColumnIndex_.find(stableName.Get());
+ if (it == StableNameToColumnIndex_.end()) {
+ return nullptr;
+ }
+ return &Columns()[it->second];
+}
+
+const TDeletedColumn* TTableSchema::FindDeletedColumn(const TStableName& stableName) const
+{
+ auto it = StableNameToDeletedColumnIndex_.find(stableName.Get());
+ if (it == StableNameToDeletedColumnIndex_.end()) {
+ return nullptr;
+ }
+ return &DeletedColumns()[it->second];
+}
+
+const TColumnSchema* TTableSchema::FindColumn(TStringBuf name) const
+{
+ auto it = NameToColumnIndex_.find(name);
+ if (it == NameToColumnIndex_.end()) {
+ return nullptr;
+ }
+ return &Columns()[it->second];
+}
+
+const TColumnSchema& TTableSchema::GetColumn(TStringBuf name) const
+{
+ auto* column = FindColumn(name);
+ YT_VERIFY(column);
+ return *column;
+}
+
+const TColumnSchema& TTableSchema::GetColumnOrThrow(TStringBuf name) const
+{
+ auto* column = FindColumn(name);
+ if (!column) {
+ THROW_ERROR_EXCEPTION("Missing schema column with name %Qv", name);
+ }
+ return *column;
+}
+
+int TTableSchema::GetColumnIndex(const TColumnSchema& column) const
+{
+ auto begin = Columns().begin();
+ auto end = Columns().end();
+ YT_VERIFY(begin <= &column && &column < end);
+
+ return &column - begin;
+}
+
+int TTableSchema::GetColumnIndex(TStringBuf name) const
+{
+ return GetColumnIndex(GetColumn(name));
+}
+
+int TTableSchema::GetColumnIndexOrThrow(TStringBuf name) const
+{
+ return GetColumnIndex(GetColumnOrThrow(name));
+}
+
+TTableSchema::TNameMapping TTableSchema::GetNameMapping() const
+{
+ return TNameMapping(*this);
+}
+
+TTableSchemaPtr TTableSchema::Filter(const TColumnFilter& columnFilter, bool discardSortOrder) const
+{
+ int newKeyColumnCount = 0;
+ std::vector<TColumnSchema> columns;
+
+ if (columnFilter.IsUniversal()) {
+ if (!discardSortOrder) {
+ return New<TTableSchema>(*this);
+ } else {
+ columns = Columns();
+ for (auto& column : columns) {
+ column.SetSortOrder(std::nullopt);
+ }
+ }
+ } else {
+ bool inKeyColumns = !discardSortOrder;
+ for (int id : columnFilter.GetIndexes()) {
+ if (id < 0 || id >= std::ssize(Columns())) {
+ THROW_ERROR_EXCEPTION("Invalid column during schema filtering: expected in range [0, %v), got %v",
+ Columns().size(),
+ id);
+ }
+
+ if (id != std::ssize(columns) || !Columns()[id].SortOrder()) {
+ inKeyColumns = false;
+ }
+
+ columns.push_back(Columns()[id]);
+
+ if (!inKeyColumns) {
+ columns.back().SetSortOrder(std::nullopt);
+ }
+
+ if (columns.back().SortOrder()) {
+ ++newKeyColumnCount;
+ }
+ }
+ }
+
+ return New<TTableSchema>(
+ std::move(columns),
+ Strict_,
+ UniqueKeys_ && (newKeyColumnCount == GetKeyColumnCount()),
+ ETableSchemaModification::None,
+ DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::Filter(const THashSet<TString>& columnNames, bool discardSortOrder) const
+{
+ TColumnFilter::TIndexes indexes;
+ for (const auto& column : Columns()) {
+ if (columnNames.find(column.Name()) != columnNames.end()) {
+ indexes.push_back(GetColumnIndex(column));
+ }
+ }
+ return Filter(TColumnFilter(std::move(indexes)), discardSortOrder);
+}
+
+TTableSchemaPtr TTableSchema::Filter(const std::optional<std::vector<TString>>& columnNames, bool discardSortOrder) const
+{
+ if (!columnNames) {
+ return Filter(TColumnFilter(), discardSortOrder);
+ }
+
+ return Filter(THashSet<TString>(columnNames->begin(), columnNames->end()), discardSortOrder);
+}
+
+bool TTableSchema::HasComputedColumns() const
+{
+ return HasComputedColumns_;
+}
+
+bool TTableSchema::HasAggregateColumns() const
+{
+ return HasAggregateColumns_;
+}
+
+bool TTableSchema::HasHunkColumns() const
+{
+ return !HunkColumnsIds_.empty();
+}
+
+bool TTableSchema::HasTimestampColumn() const
+{
+ return FindColumn(TimestampColumnName);
+}
+
+bool TTableSchema::IsSorted() const
+{
+ return KeyColumnCount_ > 0;
+}
+
+bool TTableSchema::IsUniqueKeys() const
+{
+ return UniqueKeys_;
+}
+
+bool TTableSchema::HasRenamedColumns() const
+{
+ return std::any_of(Columns().begin(), Columns().end(), [] (const TColumnSchema& column) {
+ return column.IsRenamed();
+ });
+}
+
+bool TTableSchema::IsEmpty() const
+{
+ return Columns().empty();
+}
+
+TKeyColumns TTableSchema::GetKeyColumnNames() const
+{
+ TKeyColumns keyColumns;
+ for (const auto& column : Columns()) {
+ if (column.SortOrder()) {
+ keyColumns.push_back(column.Name());
+ }
+ }
+ return keyColumns;
+}
+
+std::vector<TStableName> TTableSchema::GetKeyColumnStableNames() const
+{
+ std::vector<TStableName> keyColumns;
+ for (const auto& column : Columns()) {
+ if (column.SortOrder()) {
+ keyColumns.push_back(column.StableName());
+ }
+ }
+ return keyColumns;
+}
+
+TKeyColumns TTableSchema::GetKeyColumns() const
+{
+ return GetKeyColumnNames();
+}
+
+int TTableSchema::GetColumnCount() const
+{
+ return static_cast<int>(Columns().size());
+}
+
+std::vector<TString> TTableSchema::GetColumnNames() const
+{
+ std::vector<TString> result;
+ result.reserve(Columns().size());
+ for (const auto& column : Columns()) {
+ result.push_back(column.Name());
+ }
+ return result;
+}
+
+std::vector<TStableName> TTableSchema::GetColumnStableNames() const
+{
+ std::vector<TStableName> result;
+ result.reserve(Columns().size());
+ for (const auto& column : Columns()) {
+ result.push_back(column.StableName());
+ }
+ return result;
+}
+
+const THunkColumnIds& TTableSchema::GetHunkColumnIds() const
+{
+ return HunkColumnsIds_;
+}
+
+std::vector<TStableName> MapNamesToStableNames(
+ const TTableSchema& schema,
+ std::vector<TString> names,
+ const std::optional<TStringBuf>& missingColumnReplacement)
+{
+ std::vector<TStableName> stableNames;
+ stableNames.reserve(names.size());
+ for (const auto& name : names) {
+ const auto* column = schema.FindColumn(name);
+ if (column) {
+ stableNames.push_back(column->StableName());
+ } else if (!schema.GetStrict()) {
+ stableNames.push_back(TStableName(name));
+ } else if (missingColumnReplacement) {
+ stableNames.push_back(TStableName(TString(*missingColumnReplacement)));
+ } else {
+ THROW_ERROR_EXCEPTION("Column %Qv is missing in strict schema",
+ name);
+ }
+ }
+ return stableNames;
+}
+
+int TTableSchema::GetKeyColumnCount() const
+{
+ return KeyColumnCount_;
+}
+
+int TTableSchema::GetValueColumnCount() const
+{
+ return GetColumnCount() - GetKeyColumnCount();
+}
+
+TSortColumns TTableSchema::GetSortColumns(const std::optional<TNameMapping>& nameMapping) const
+{
+ auto actualNameMapping = nameMapping
+ ? *nameMapping
+ : GetNameMapping();
+
+ TSortColumns sortColumns;
+ sortColumns.reserve(GetKeyColumnCount());
+
+ for (const auto& column : Columns()) {
+ if (column.SortOrder()) {
+ const auto& name = actualNameMapping.StableNameToName(column.StableName());
+ sortColumns.push_back(TColumnSortSchema{
+ .Name = TString(name),
+ .SortOrder = *column.SortOrder(),
+ });
+ }
+ }
+
+ return sortColumns;
+}
+
+TTableSchemaPtr TTableSchema::SetUniqueKeys(bool uniqueKeys) const
+{
+ auto schema = *this;
+ schema.UniqueKeys_ = uniqueKeys;
+ return New<TTableSchema>(std::move(schema));
+}
+
+TTableSchemaPtr TTableSchema::SetSchemaModification(ETableSchemaModification schemaModification) const
+{
+ auto schema = *this;
+ schema.SchemaModification_ = schemaModification;
+ return New<TTableSchema>(std::move(schema));
+}
+
+bool TTableSchema::HasNontrivialSchemaModification() const
+{
+ return GetSchemaModification() != ETableSchemaModification::None;
+}
+
+TTableSchemaPtr TTableSchema::FromKeyColumns(const TKeyColumns& keyColumns)
+{
+ TTableSchema schema;
+ std::vector<TColumnSchema> columns;
+ for (const auto& columnName : keyColumns) {
+ columns.push_back(
+ TColumnSchema(columnName, ESimpleLogicalValueType::Any)
+ .SetSortOrder(ESortOrder::Ascending));
+ }
+ schema.Columns_ = std::make_shared<const std::vector<TColumnSchema>>(std::move(columns));
+ schema.KeyColumnCount_ = keyColumns.size();
+ ValidateTableSchema(schema);
+ return New<TTableSchema>(std::move(schema));
+}
+
+TTableSchemaPtr TTableSchema::FromSortColumns(const TSortColumns& sortColumns)
+{
+ TTableSchema schema;
+ std::vector<TColumnSchema> columns;
+ for (const auto& sortColumn : sortColumns) {
+ columns.push_back(
+ TColumnSchema(sortColumn.Name, ESimpleLogicalValueType::Any)
+ .SetSortOrder(sortColumn.SortOrder));
+ }
+ schema.Columns_ = std::make_shared<const std::vector<TColumnSchema>>(std::move(columns));
+ schema.KeyColumnCount_ = sortColumns.size();
+ ValidateTableSchema(schema);
+ return New<TTableSchema>(std::move(schema));
+}
+
+TTableSchemaPtr TTableSchema::ToQuery() const
+{
+ if (IsSorted()) {
+ return New<TTableSchema>(*this);
+ } else {
+ std::vector<TColumnSchema> columns {
+ TColumnSchema(TabletIndexColumnName, ESimpleLogicalValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending),
+ TColumnSchema(RowIndexColumnName, ESimpleLogicalValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending)
+ };
+ columns.insert(columns.end(), Columns().begin(), Columns().end());
+ return New<TTableSchema>(std::move(columns), true, false,
+ ETableSchemaModification::None, DeletedColumns_);
+ }
+}
+
+TTableSchemaPtr TTableSchema::ToWrite() const
+{
+ std::vector<TColumnSchema> columns;
+ if (IsSorted()) {
+ for (const auto& column : Columns()) {
+ if (!column.Expression()) {
+ columns.push_back(column);
+ }
+ }
+ } else {
+ columns.push_back(TColumnSchema(TabletIndexColumnName, ESimpleLogicalValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending));
+ for (const auto& column : Columns()) {
+ if (column.StableName().Get() != TimestampColumnName &&
+ column.StableName().Get() != CumulativeDataWeightColumnName)
+ {
+ columns.push_back(column);
+ }
+ }
+ }
+ return New<TTableSchema>(std::move(columns), Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::WithTabletIndex() const
+{
+ if (IsSorted()) {
+ return New<TTableSchema>(*this);
+ } else {
+ auto columns = Columns();
+ // XXX: Is it ok? $tablet_index is usually a key column.
+ columns.push_back(TColumnSchema(TabletIndexColumnName, ESimpleLogicalValueType::Int64));
+ return New<TTableSchema>(std::move(columns), Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+ }
+}
+
+TTableSchemaPtr TTableSchema::ToVersionedWrite() const
+{
+ if (IsSorted()) {
+ return New<TTableSchema>(*this);
+ } else {
+ auto columns = Columns();
+ columns.insert(columns.begin(), TColumnSchema(TabletIndexColumnName, ESimpleLogicalValueType::Int64)
+ .SetSortOrder(ESortOrder::Ascending));
+ return New<TTableSchema>(std::move(columns), Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+ }
+}
+
+TTableSchemaPtr TTableSchema::ToLookup() const
+{
+ std::vector<TColumnSchema> columns;
+ for (const auto& column : Columns()) {
+ if (column.SortOrder() && !column.Expression()) {
+ columns.push_back(column);
+ }
+ }
+ return New<TTableSchema>(std::move(columns), Strict_, UniqueKeys_, ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToDelete() const
+{
+ return ToLookup();
+}
+
+TTableSchemaPtr TTableSchema::ToKeys() const
+{
+ std::vector<TColumnSchema> columns(Columns().begin(), Columns().begin() + KeyColumnCount_);
+ return New<TTableSchema>(std::move(columns), Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToUniqueKeys() const
+{
+ return New<TTableSchema>(Columns(), Strict_, /*uniqueKeys*/ true,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToStrippedColumnAttributes() const
+{
+ std::vector<TColumnSchema> strippedColumns;
+ for (const auto& column : Columns()) {
+ auto& strippedColumn = strippedColumns.emplace_back(column.Name(), column.LogicalType());
+ strippedColumn.SetStableName(column.StableName());
+ }
+ return New<TTableSchema>(std::move(strippedColumns), Strict_, /*uniqueKeys*/ false,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToSortedStrippedColumnAttributes() const
+{
+ std::vector<TColumnSchema> strippedColumns;
+ for (const auto& column : Columns()) {
+ auto& strippedColumn = strippedColumns.emplace_back(column.Name(), column.LogicalType(), column.SortOrder());
+ strippedColumn.SetStableName(column.StableName());
+ }
+ return New<TTableSchema>(std::move(strippedColumns), Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToCanonical() const
+{
+ auto columns = Columns();
+ std::sort(
+ columns.begin() + KeyColumnCount_,
+ columns.end(),
+ [] (const TColumnSchema& lhs, const TColumnSchema& rhs) {
+ return lhs.Name() < rhs.Name();
+ });
+ return New<TTableSchema>(columns, Strict_, UniqueKeys_,
+ ETableSchemaModification::None, DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToSorted(const TKeyColumns& keyColumns) const
+{
+ TSortColumns sortColumns;
+ sortColumns.reserve(keyColumns.size());
+ for (const auto& keyColumn : keyColumns) {
+ sortColumns.push_back(TColumnSortSchema{
+ .Name = keyColumn,
+ .SortOrder = ESortOrder::Ascending
+ });
+ }
+
+ return TTableSchema::ToSorted(sortColumns);
+}
+
+TTableSchemaPtr TTableSchema::ToSorted(const TSortColumns& sortColumns) const
+{
+ int oldKeyColumnCount = 0;
+ auto columns = Columns();
+ for (int index = 0; index < std::ssize(sortColumns); ++index) {
+ auto it = std::find_if(
+ columns.begin() + index,
+ columns.end(),
+ [&] (const TColumnSchema& column) {
+ return column.Name() == sortColumns[index].Name;
+ });
+
+ if (it == columns.end()) {
+ if (Strict_) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::IncompatibleKeyColumns,
+ "Column %Qv is not found in strict schema",
+ sortColumns[index].Name)
+ << TErrorAttribute("schema", *this)
+ << TErrorAttribute("sort_columns", sortColumns);
+ } else {
+ columns.push_back(TColumnSchema(sortColumns[index].Name, EValueType::Any));
+ it = columns.end();
+ --it;
+ }
+ }
+
+ if (it->SortOrder()) {
+ ++oldKeyColumnCount;
+ }
+
+ std::swap(columns[index], *it);
+ columns[index].SetSortOrder(sortColumns[index].SortOrder);
+ }
+
+ auto uniqueKeys = UniqueKeys_ && oldKeyColumnCount == GetKeyColumnCount();
+
+ for (auto it = columns.begin() + sortColumns.size(); it != columns.end(); ++it) {
+ it->SetSortOrder(std::nullopt);
+ }
+
+ return New<TTableSchema>(std::move(columns), Strict_, uniqueKeys, GetSchemaModification(),
+ DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToReplicationLog() const
+{
+ std::vector<TColumnSchema> columns;
+ columns.push_back(TColumnSchema(TimestampColumnName, ESimpleLogicalValueType::Uint64));
+ if (IsSorted()) {
+ columns.push_back(TColumnSchema(TReplicationLogTable::ChangeTypeColumnName, ESimpleLogicalValueType::Int64));
+ for (const auto& column : Columns()) {
+ if (column.SortOrder()) {
+ columns.push_back(
+ TColumnSchema(
+ TReplicationLogTable::KeyColumnNamePrefix + column.Name(),
+ column.LogicalType()));
+ } else {
+ columns.push_back(
+ TColumnSchema(
+ TReplicationLogTable::ValueColumnNamePrefix + column.Name(),
+ MakeOptionalIfNot(column.LogicalType())));
+ columns.push_back(
+ TColumnSchema(TReplicationLogTable::FlagsColumnNamePrefix + column.Name(), ESimpleLogicalValueType::Uint64));
+ }
+ }
+ } else {
+ for (const auto& column : Columns()) {
+ columns.push_back(
+ TColumnSchema(
+ TReplicationLogTable::ValueColumnNamePrefix + column.Name(),
+ MakeOptionalIfNot(column.LogicalType()))
+ .SetMaxInlineHunkSize(column.MaxInlineHunkSize()));
+ }
+ columns.push_back(TColumnSchema(TReplicationLogTable::ValueColumnNamePrefix + TabletIndexColumnName, ESimpleLogicalValueType::Int64));
+ }
+ return New<TTableSchema>(
+ std::move(columns),
+ /* strict */ true,
+ /* uniqueKeys */ false,
+ ETableSchemaModification::None,
+ DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToUnversionedUpdate(bool sorted) const
+{
+ YT_VERIFY(IsSorted());
+
+ std::vector<TColumnSchema> columns;
+ columns.reserve(GetKeyColumnCount() + 1 + GetValueColumnCount() * 2);
+
+ // Keys.
+ for (int columnIndex = 0; columnIndex < GetKeyColumnCount(); ++columnIndex) {
+ auto column = Columns()[columnIndex];
+ if (!sorted) {
+ column.SetSortOrder(std::nullopt);
+ }
+ columns.push_back(column);
+ }
+
+ // Modification type.
+ columns.emplace_back(
+ TUnversionedUpdateSchema::ChangeTypeColumnName,
+ MakeLogicalType(ESimpleLogicalValueType::Uint64, /*required*/ true));
+
+ // Values.
+ for (int columnIndex = GetKeyColumnCount(); columnIndex < GetColumnCount(); ++columnIndex) {
+ const auto& column = Columns()[columnIndex];
+ YT_VERIFY(!column.SortOrder());
+ columns.emplace_back(
+ TUnversionedUpdateSchema::ValueColumnNamePrefix + column.Name(),
+ MakeOptionalIfNot(column.LogicalType()));
+ columns.emplace_back(
+ TUnversionedUpdateSchema::FlagsColumnNamePrefix + column.Name(),
+ MakeLogicalType(ESimpleLogicalValueType::Uint64, /*required*/ false));
+ }
+
+ return New<TTableSchema>(
+ std::move(columns),
+ /*strict*/ true,
+ /*uniqueKeys*/ sorted,
+ ETableSchemaModification::None,
+ DeletedColumns_);
+}
+
+TTableSchemaPtr TTableSchema::ToModifiedSchema(ETableSchemaModification schemaModification) const
+{
+ if (HasNontrivialSchemaModification()) {
+ THROW_ERROR_EXCEPTION("Cannot apply schema modification because schema is already modified")
+ << TErrorAttribute("existing_modification", GetSchemaModification())
+ << TErrorAttribute("requested_modification", schemaModification);
+ }
+ YT_VERIFY(GetSchemaModification() == ETableSchemaModification::None);
+
+ switch (schemaModification) {
+ case ETableSchemaModification::None:
+ return New<TTableSchema>(*this);
+
+ case ETableSchemaModification::UnversionedUpdate:
+ return ToUnversionedUpdate(/*sorted*/ true)
+ ->SetSchemaModification(schemaModification);
+
+ case ETableSchemaModification::UnversionedUpdateUnsorted:
+ return ToUnversionedUpdate(/*sorted*/ false)
+ ->SetSchemaModification(schemaModification);
+
+ default:
+ YT_ABORT();
+ }
+}
+
+TComparator TTableSchema::ToComparator() const
+{
+ std::vector<ESortOrder> sortOrders(KeyColumnCount_);
+ for (int index = 0; index < KeyColumnCount_; ++index) {
+ YT_VERIFY(Columns()[index].SortOrder());
+ sortOrders[index] = *Columns()[index].SortOrder();
+ }
+ return TComparator(std::move(sortOrders));
+}
+
+void TTableSchema::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, ToProto<NTableClient::NProto::TTableSchemaExt>(*this));
+}
+
+void TTableSchema::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ auto protoSchema = NYT::Load<NTableClient::NProto::TTableSchemaExt>(context);
+ *this = FromProto<TTableSchema>(protoSchema);
+}
+
+i64 TTableSchema::GetMemoryUsage() const
+{
+ i64 usage = sizeof(TTableSchema);
+ for (const auto& column : Columns()) {
+ usage += column.GetMemoryUsage();
+ }
+ return usage;
+}
+
+TKeyColumnTypes TTableSchema::GetKeyColumnTypes() const
+{
+ TKeyColumnTypes result(KeyColumnCount_);
+ for (int index = 0; index < KeyColumnCount_; ++index) {
+ result[index] = Columns()[index].GetWireType();
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TTableSchema& schema, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("<strict=%v;unique_keys=%v", schema.GetStrict(), schema.GetUniqueKeys());
+ if (schema.HasNontrivialSchemaModification()) {
+ builder->AppendFormat(";schema_modification=%v", schema.GetSchemaModification());
+ }
+ builder->AppendChar('>');
+ builder->AppendChar('[');
+ bool first = true;
+ for (const auto& column : schema.Columns()) {
+ if (!first) {
+ builder->AppendString(TStringBuf("; "));
+ }
+ builder->AppendFormat("%v", column);
+ first = false;
+ }
+ for (const auto& deletedColumn : schema.DeletedColumns()) {
+ if (!first) {
+ builder->AppendString(TStringBuf("; "));
+ }
+ builder->AppendFormat("%v", deletedColumn);
+ first = false;
+ }
+ builder->AppendChar(']');
+}
+
+TString ToString(const TTableSchema& schema)
+{
+ return ToStringViaBuilder(schema);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TTableSchemaPtr& schema, TStringBuf spec)
+{
+ if (schema) {
+ FormatValue(builder, *schema, spec);
+ } else {
+ builder->AppendString(TStringBuf("<null>"));
+ }
+}
+
+TString ToString(const TTableSchemaPtr& schema)
+{
+ return ToStringViaBuilder(schema);
+}
+
+TString SerializeToWireProto(const TTableSchemaPtr& schema)
+{
+ NTableClient::NProto::TTableSchemaExt protoSchema;
+ ToProto(&protoSchema, schema);
+ return protoSchema.SerializeAsString();
+}
+
+void DeserializeFromWireProto(TTableSchemaPtr* schema, const TString& serializedProto)
+{
+ NTableClient::NProto::TTableSchemaExt protoSchema;
+ if (!protoSchema.ParseFromString(serializedProto)) {
+ THROW_ERROR_EXCEPTION("Failed to deserialize table schema from wire proto");
+ }
+ FromProto(schema, protoSchema);
+}
+
+void ToProto(NProto::TTableSchemaExt* protoSchema, const TTableSchema& schema)
+{
+ ToProto(protoSchema->mutable_columns(), schema.Columns());
+ ToProto(protoSchema->mutable_deleted_columns(), schema.DeletedColumns());
+ protoSchema->set_strict(schema.GetStrict());
+ protoSchema->set_unique_keys(schema.GetUniqueKeys());
+ protoSchema->set_schema_modification(static_cast<int>(schema.GetSchemaModification()));
+}
+
+void FromProto(TTableSchema* schema, const NProto::TTableSchemaExt& protoSchema)
+{
+ *schema = TTableSchema(
+ FromProto<std::vector<TColumnSchema>>(protoSchema.columns()),
+ protoSchema.strict(),
+ protoSchema.unique_keys(),
+ CheckedEnumCast<ETableSchemaModification>(protoSchema.schema_modification()),
+ FromProto<std::vector<TDeletedColumn>>(protoSchema.deleted_columns()));
+}
+
+void FromProto(
+ TTableSchema* schema,
+ const NProto::TTableSchemaExt& protoSchema,
+ const NProto::TKeyColumnsExt& protoKeyColumns)
+{
+ auto columns = FromProto<std::vector<TColumnSchema>>(protoSchema.columns());
+ for (int columnIndex = 0; columnIndex < protoKeyColumns.names_size(); ++columnIndex) {
+ auto& columnSchema = columns[columnIndex];
+ YT_VERIFY(columnSchema.Name() == protoKeyColumns.names(columnIndex));
+ // TODO(gritukan): YT-14155
+ if (!columnSchema.SortOrder()) {
+ columnSchema.SetSortOrder(ESortOrder::Ascending);
+ }
+ }
+ for (int columnIndex = protoKeyColumns.names_size(); columnIndex < std::ssize(columns); ++columnIndex) {
+ auto& columnSchema = columns[columnIndex];
+ YT_VERIFY(!columnSchema.SortOrder());
+ }
+
+ auto deletedColumns = FromProto<std::vector<TDeletedColumn>>(protoSchema.deleted_columns());
+
+ *schema = TTableSchema(
+ std::move(columns),
+ protoSchema.strict(),
+ protoSchema.unique_keys(),
+ ETableSchemaModification::None,
+ deletedColumns);
+}
+
+void ToProto(NProto::TTableSchemaExt* protoSchema, const TTableSchemaPtr& schema)
+{
+ ToProto(protoSchema, *schema);
+}
+
+void FromProto(TTableSchemaPtr* schema, const NProto::TTableSchemaExt& protoSchema)
+{
+ *schema = New<TTableSchema>();
+ FromProto(schema->Get(), protoSchema);
+}
+
+void FromProto(
+ TTableSchemaPtr* schema,
+ const NProto::TTableSchemaExt& protoSchema,
+ const NProto::TKeyColumnsExt& keyColumnsExt)
+{
+ *schema = New<TTableSchema>();
+ FromProto(schema->Get(), protoSchema, keyColumnsExt);
+}
+
+void PrintTo(const TTableSchema& tableSchema, std::ostream* os)
+{
+ *os << Format("%v", tableSchema);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TColumnSchema& lhs, const TColumnSchema& rhs)
+{
+ return
+ lhs.StableName() == rhs.StableName() &&
+ lhs.Name() == rhs.Name() &&
+ *lhs.LogicalType() == *rhs.LogicalType() &&
+ lhs.Required() == rhs.Required() &&
+ lhs.SortOrder() == rhs.SortOrder() &&
+ lhs.Lock() == rhs.Lock() &&
+ lhs.Expression() == rhs.Expression() &&
+ lhs.Aggregate() == rhs.Aggregate() &&
+ lhs.Group() == rhs.Group() &&
+ lhs.MaxInlineHunkSize() == rhs.MaxInlineHunkSize();
+}
+
+bool operator==(const TDeletedColumn& lhs, const TDeletedColumn& rhs)
+{
+ return lhs.StableName() == rhs.StableName();
+}
+
+bool operator==(const TTableSchema& lhs, const TTableSchema& rhs)
+{
+ return
+ lhs.Columns() == rhs.Columns() &&
+ lhs.GetStrict() == rhs.GetStrict() &&
+ lhs.GetUniqueKeys() == rhs.GetUniqueKeys() &&
+ lhs.GetSchemaModification() == rhs.GetSchemaModification() &&
+ lhs.DeletedColumns() == rhs.DeletedColumns();
+}
+
+// Compat code for https://st.yandex-team.ru/YT-10668 workaround.
+bool IsEqualIgnoringRequiredness(const TTableSchema& lhs, const TTableSchema& rhs)
+{
+ auto dropRequiredness = [] (const TTableSchema& schema) {
+ std::vector<TColumnSchema> resultColumns;
+ for (auto column : schema.Columns()) {
+ if (column.LogicalType()->GetMetatype() == ELogicalMetatype::Optional) {
+ column.SetLogicalType(column.LogicalType()->AsOptionalTypeRef().GetElement());
+ }
+ resultColumns.emplace_back(column);
+ }
+ return TTableSchema(resultColumns, schema.GetStrict(), schema.GetUniqueKeys());
+ };
+ return dropRequiredness(lhs) == dropRequiredness(rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateKeyColumns(const TKeyColumns& keyColumns)
+{
+ ValidateKeyColumnCount(keyColumns.size());
+
+ THashSet<TString> names;
+ for (const auto& name : keyColumns) {
+ if (!names.insert(name).second) {
+ THROW_ERROR_EXCEPTION("Duplicate key column name %Qv",
+ name);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateSystemColumnSchema(
+ const TColumnSchema& columnSchema,
+ bool isTableSorted,
+ bool allowUnversionedUpdateColumns)
+{
+ static const auto allowedSortedTablesSystemColumns = THashMap<TString, ESimpleLogicalValueType>{
+ };
+
+ static const auto allowedOrderedTablesSystemColumns = THashMap<TString, ESimpleLogicalValueType>{
+ {TimestampColumnName, ESimpleLogicalValueType::Uint64},
+ {CumulativeDataWeightColumnName, ESimpleLogicalValueType::Int64}
+ };
+
+ auto validateType = [&] (ESimpleLogicalValueType expected) {
+ if (!columnSchema.IsOfV1Type(expected)) {
+ THROW_ERROR_EXCEPTION("Invalid type of column %Qv: expected %Qlv, got %Qlv",
+ columnSchema.GetDiagnosticNameString(),
+ expected,
+ *columnSchema.LogicalType());
+ }
+ };
+
+ const auto& name = columnSchema.Name();
+
+ if (columnSchema.IsRenamed()) {
+ THROW_ERROR_EXCEPTION("System column schema must have equal name and stable name")
+ << TErrorAttribute("name", name)
+ << TErrorAttribute("stable_name", columnSchema.StableName().Get());
+ }
+
+ const auto& allowedSystemColumns = isTableSorted
+ ? allowedSortedTablesSystemColumns
+ : allowedOrderedTablesSystemColumns;
+ auto it = allowedSystemColumns.find(name);
+
+ // Ordinary system column.
+ if (it != allowedSystemColumns.end()) {
+ validateType(it->second);
+ return;
+ }
+
+ if (allowUnversionedUpdateColumns) {
+ // Unversioned update schema system column.
+ if (name == TUnversionedUpdateSchema::ChangeTypeColumnName) {
+ validateType(ESimpleLogicalValueType::Uint64);
+ return;
+ } else if (name.StartsWith(TUnversionedUpdateSchema::FlagsColumnNamePrefix)) {
+ validateType(ESimpleLogicalValueType::Uint64);
+ return;
+ } else if (name.StartsWith(TUnversionedUpdateSchema::ValueColumnNamePrefix)) {
+ // Value can have any type.
+ return;
+ }
+ }
+
+ // Unexpected system column.
+ THROW_ERROR_EXCEPTION("System column name %Qv is not allowed here",
+ name);
+}
+
+void ValidateColumnSchema(
+ const TColumnSchema& columnSchema,
+ bool isTableSorted,
+ bool isTableDynamic,
+ bool allowUnversionedUpdateColumns)
+{
+ static const auto allowedAggregates = THashSet<TString>{
+ "sum",
+ "min",
+ "max",
+ "first",
+ "xdelta"
+ };
+
+ const auto& stableName = columnSchema.StableName();
+ if (stableName.Get().empty()) {
+ THROW_ERROR_EXCEPTION("Column stable name cannot be empty");
+ }
+
+ const auto& name = columnSchema.Name();
+ if (name.empty()) {
+ THROW_ERROR_EXCEPTION("Column name cannot be empty");
+ }
+
+ try {
+ if (stableName.Get().StartsWith(SystemColumnNamePrefix) || name.StartsWith(SystemColumnNamePrefix)) {
+ ValidateSystemColumnSchema(columnSchema, isTableSorted, allowUnversionedUpdateColumns);
+ }
+
+ if (stableName.Get().size() > MaxColumnNameLength) {
+ THROW_ERROR_EXCEPTION("Column stable name is longer than maximum allowed: %v > %v",
+ stableName.Get().size(),
+ MaxColumnNameLength);
+ }
+
+ if (name.size() > MaxColumnNameLength) {
+ THROW_ERROR_EXCEPTION("Column name is longer than maximum allowed: %v > %v",
+ name.size(),
+ MaxColumnNameLength);
+ }
+
+ {
+ TComplexTypeFieldDescriptor descriptor(name, columnSchema.LogicalType());
+ ValidateLogicalType(descriptor, MaxSchemaDepth);
+ }
+
+ if (!IsComparable(columnSchema.LogicalType()) &&
+ columnSchema.SortOrder() &&
+ !columnSchema.IsOfV1Type(ESimpleLogicalValueType::Any))
+ {
+ THROW_ERROR_EXCEPTION("Key column cannot be of %Qv type",
+ *columnSchema.LogicalType());
+ }
+
+ if (*DetagLogicalType(columnSchema.LogicalType()) == *SimpleLogicalType(ESimpleLogicalValueType::Any)) {
+ THROW_ERROR_EXCEPTION("Column of type %Qlv cannot be required",
+ ESimpleLogicalValueType::Any);
+ }
+
+ if (columnSchema.Lock()) {
+ if (columnSchema.Lock()->empty()) {
+ THROW_ERROR_EXCEPTION("Column lock name cannot be empty");
+ }
+ if (columnSchema.Lock()->size() > MaxColumnLockLength) {
+ THROW_ERROR_EXCEPTION("Column lock name is longer than maximum allowed: %v > %v",
+ columnSchema.Lock()->size(),
+ MaxColumnLockLength);
+ }
+ if (columnSchema.SortOrder()) {
+ THROW_ERROR_EXCEPTION("Column lock cannot be set on a key column");
+ }
+ }
+
+ if (columnSchema.Group()) {
+ if (columnSchema.Group()->empty()) {
+ THROW_ERROR_EXCEPTION("Column group should either be unset or be non-empty");
+ }
+ if (columnSchema.Group()->size() > MaxColumnGroupLength) {
+ THROW_ERROR_EXCEPTION("Column group name is longer than maximum allowed: %v > %v",
+ columnSchema.Group()->size(),
+ MaxColumnGroupLength);
+ }
+ }
+
+ ValidateSchemaValueType(columnSchema.GetWireType());
+
+ if (columnSchema.Expression() && !columnSchema.SortOrder() && isTableDynamic) {
+ THROW_ERROR_EXCEPTION("Non-key column cannot be computed");
+ }
+
+ if (columnSchema.Aggregate() && columnSchema.SortOrder()) {
+ THROW_ERROR_EXCEPTION("Key column cannot be aggregated");
+ }
+
+ if (columnSchema.Aggregate() && allowedAggregates.find(*columnSchema.Aggregate()) == allowedAggregates.end()) {
+ THROW_ERROR_EXCEPTION("Invalid aggregate function %Qv",
+ *columnSchema.Aggregate());
+ }
+
+ if (columnSchema.Expression() && columnSchema.Required()) {
+ THROW_ERROR_EXCEPTION("Computed column cannot be required");
+ }
+
+ if (columnSchema.MaxInlineHunkSize()) {
+ if (columnSchema.MaxInlineHunkSize() <= 0) {
+ THROW_ERROR_EXCEPTION("Max inline hunk size must be positive");
+ }
+ if (!IsStringLikeType(columnSchema.GetWireType())) {
+ THROW_ERROR_EXCEPTION("Max inline hunk size can only be set for string-like columns, not %Qlv",
+ columnSchema.GetWireType());
+ }
+ if (columnSchema.SortOrder()) {
+ THROW_ERROR_EXCEPTION("Max inline hunk size cannot be set for key column");
+ }
+ if (columnSchema.Aggregate()) {
+ THROW_ERROR_EXCEPTION("Max inline hunk size cannot be set for aggregate column");
+ }
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error validating schema of column %v",
+ columnSchema.GetDiagnosticNameString())
+ << ex;
+ }
+}
+
+void ValidateDynamicTableConstraints(const TTableSchema& schema)
+{
+ if (!schema.GetStrict()) {
+ THROW_ERROR_EXCEPTION("\"strict\" cannot be \"false\" for a dynamic table");
+ }
+
+ if (schema.IsSorted() && !schema.GetUniqueKeys()) {
+ THROW_ERROR_EXCEPTION("\"unique_keys\" cannot be \"false\" for a sorted dynamic table");
+ }
+
+ if (schema.GetKeyColumnCount() == std::ssize(schema.Columns())) {
+ THROW_ERROR_EXCEPTION("There must be at least one non-key column");
+ }
+
+ if (schema.GetKeyColumnCount() > MaxKeyColumnCountInDynamicTable) {
+ THROW_ERROR_EXCEPTION("Too many key columns: limit %v, actual: %v",
+ MaxKeyColumnCountInDynamicTable,
+ schema.GetKeyColumnCount());
+ }
+
+ for (const auto& column : schema.Columns()) {
+ try {
+ if (column.SortOrder() && (
+ column.GetWireType() == EValueType::Any ||
+ column.GetWireType() == EValueType::Composite ||
+ !column.IsOfV1Type()))
+ {
+ THROW_ERROR_EXCEPTION("Dynamic table cannot have key column of type %Qv",
+ *column.LogicalType());
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error validating column %v in dynamic table schema",
+ column.GetDiagnosticNameString())
+ << ex;
+ }
+ }
+}
+
+//! Validates that there are no duplicates among the column names.
+void ValidateColumnUniqueness(const TTableSchema& schema)
+{
+ THashSet<TStringBuf> columnNames;
+ THashSet<TStringBuf> columnStableNames;
+ for (const auto& column : schema.Columns()) {
+ if (!columnStableNames.insert(column.StableName().Get()).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column stable name %Qv in table schema",
+ column.StableName());
+ }
+ if (!columnNames.insert(column.Name()).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column name %Qv in table schema",
+ column.Name());
+ }
+ }
+ for (const auto& deletedColumn : schema.DeletedColumns()) {
+ if (!columnStableNames.insert(deletedColumn.StableName().Get()).second) {
+ THROW_ERROR_EXCEPTION("Duplicate column stable name %Qv in table schema",
+ deletedColumn.StableName());
+ }
+ }
+}
+
+void ValidatePivotKey(
+ TUnversionedRow pivotKey,
+ const TTableSchema& schema,
+ TStringBuf keyType,
+ bool validateRequired)
+{
+ if (static_cast<int>(pivotKey.GetCount()) > schema.GetKeyColumnCount()) {
+ auto titleKeyType = TString(keyType);
+ titleKeyType.to_title();
+ THROW_ERROR_EXCEPTION(NTableClient::EErrorCode::SchemaViolation, "%v key must form a prefix of key", titleKeyType);
+ }
+
+ for (int index = 0; index < static_cast<int>(pivotKey.GetCount()); ++index) {
+ if (pivotKey[index].Type != EValueType::Null && pivotKey[index].Type != schema.Columns()[index].GetWireType()) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Mismatched type of column %v in %v key: expected %Qlv, found %Qlv",
+ schema.Columns()[index].GetDiagnosticNameString(),
+ keyType,
+ schema.Columns()[index].GetWireType(),
+ pivotKey[index].Type);
+ }
+ if (validateRequired && pivotKey[index].Type == EValueType::Null && !schema.Columns()[index].LogicalType()->IsNullable()) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Unexpected null for required column %v in %v key",
+ schema.Columns()[index].GetDiagnosticNameString(),
+ keyType);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Validates that number of locks doesn't exceed #MaxColumnLockCount.
+void ValidateLocks(const TTableSchema& schema)
+{
+ THashSet<TString> lockNames;
+ YT_VERIFY(lockNames.insert(PrimaryLockName).second);
+ for (const auto& column : schema.Columns()) {
+ if (column.Lock()) {
+ lockNames.insert(*column.Lock());
+ }
+ }
+
+ if (lockNames.size() > MaxColumnLockCount) {
+ THROW_ERROR_EXCEPTION("Too many column locks in table schema: actual %v, limit %v",
+ lockNames.size(),
+ MaxColumnLockCount);
+ }
+}
+
+//! Validates that key columns form a prefix of a table schema.
+void ValidateKeyColumnsFormPrefix(const TTableSchema& schema)
+{
+ for (int index = 0; index < schema.GetKeyColumnCount(); ++index) {
+ if (!schema.Columns()[index].SortOrder()) {
+ THROW_ERROR_EXCEPTION("Key columns must form a prefix of schema");
+ }
+ }
+ // The fact that first GetKeyColumnCount() columns have SortOrder automatically
+ // implies that the rest of columns don't have SortOrder, so we don't need to check it.
+}
+
+//! Validates |$timestamp| column, if any.
+/*!
+ * Validate that:
+ * - |$timestamp| column cannot be a part of key.
+ * - |$timestamp| column can only be present in ordered tables.
+ * - |$timestamp| column has type |uint64|.
+ */
+void ValidateTimestampColumn(const TTableSchema& schema)
+{
+ auto* column = schema.FindColumn(TimestampColumnName);
+ if (!column) {
+ return;
+ }
+
+ if (column->SortOrder()) {
+ THROW_ERROR_EXCEPTION("Column %Qv cannot be a part of key",
+ TimestampColumnName);
+ }
+
+ if (!column->IsOfV1Type(ESimpleLogicalValueType::Uint64)) {
+ THROW_ERROR_EXCEPTION("Column %Qv must have %Qlv type",
+ TimestampColumnName,
+ EValueType::Uint64);
+ }
+
+ if (schema.IsSorted()) {
+ THROW_ERROR_EXCEPTION("Column %Qv cannot appear in a sorted table",
+ TimestampColumnName);
+ }
+}
+
+//! Validates |$cumulative_data_weight| column, if any.
+/*!
+ * Validate that:
+ * - |$cumulative_data_weight| column cannot be a part of key.
+ * - |$cumulative_data_weight| column can only be present in ordered tables.
+ * - |$cumulative_data_weight| column has type |int64|.
+ */
+void ValidateCumulativeDataWeightColumn(const TTableSchema& schema)
+{
+ auto* column = schema.FindColumn(CumulativeDataWeightColumnName);
+ if (!column) {
+ return;
+ }
+
+ if (column->SortOrder()) {
+ THROW_ERROR_EXCEPTION(
+ "Column %Qv cannot be a part of key",
+ CumulativeDataWeightColumnName);
+ }
+
+ if (!column->IsOfV1Type(ESimpleLogicalValueType::Int64)) {
+ THROW_ERROR_EXCEPTION(
+ "Column %Qv must have %Qlv type",
+ CumulativeDataWeightColumnName,
+ EValueType::Int64);
+ }
+
+ if (schema.IsSorted()) {
+ THROW_ERROR_EXCEPTION(
+ "Column %Qv cannot appear in a sorted table",
+ CumulativeDataWeightColumnName);
+ }
+}
+
+// Validate schema attributes.
+void ValidateSchemaAttributes(const TTableSchema& schema)
+{
+ if (schema.GetUniqueKeys() && schema.GetKeyColumnCount() == 0) {
+ THROW_ERROR_EXCEPTION("\"unique_keys\" can only be true if key columns are present");
+ }
+}
+
+void ValidateTableSchema(const TTableSchema& schema, bool isTableDynamic, bool allowUnversionedUpdateColumns)
+{
+ int totalTypeComplexity = 0;
+ for (const auto& column : schema.Columns()) {
+ ValidateColumnSchema(
+ column,
+ schema.IsSorted(),
+ isTableDynamic,
+ allowUnversionedUpdateColumns);
+ if (!schema.GetStrict() && column.IsRenamed()) {
+ THROW_ERROR_EXCEPTION("Renamed column %v in non-strict schema",
+ column.GetDiagnosticNameString());
+ }
+ totalTypeComplexity += column.LogicalType()->GetTypeComplexity();
+ }
+ if (totalTypeComplexity >= MaxSchemaTotalTypeComplexity) {
+ THROW_ERROR_EXCEPTION("Table schema is too complex, reduce number of columns or simplify their types");
+ }
+ ValidateColumnUniqueness(schema);
+ ValidateLocks(schema);
+ ValidateKeyColumnsFormPrefix(schema);
+ ValidateTimestampColumn(schema);
+ ValidateCumulativeDataWeightColumn(schema);
+ ValidateSchemaAttributes(schema);
+ if (isTableDynamic) {
+ ValidateDynamicTableConstraints(schema);
+ }
+}
+
+void ValidateNoDescendingSortOrder(const TTableSchema& schema)
+{
+ for (const auto& column : schema.Columns()) {
+ if (column.SortOrder() == ESortOrder::Descending) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::InvalidSchemaValue,
+ "Descending sort order is not available in this context yet")
+ << TErrorAttribute("column_name", column.Name());
+ }
+ }
+}
+
+void ValidateNoRenamedColumns(const TTableSchema& schema)
+{
+ for (const auto& column : schema.Columns()) {
+ if (column.IsRenamed()) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::InvalidSchemaValue,
+ "Table column renaming is not available yet")
+ << TErrorAttribute("renamed_column", column.GetDiagnosticNameString());
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashMap<TString, int> GetLocksMapping(
+ const NTableClient::TTableSchema& schema,
+ bool fullAtomicity,
+ std::vector<int>* columnIndexToLockIndex,
+ std::vector<TString>* lockIndexToName)
+{
+ if (columnIndexToLockIndex) {
+ // Assign dummy lock indexes to key components.
+ columnIndexToLockIndex->assign(schema.Columns().size(), -1);
+ }
+
+ if (lockIndexToName) {
+ lockIndexToName->push_back(PrimaryLockName);
+ }
+
+ THashMap<TString, int> groupToIndex;
+ if (fullAtomicity) {
+ // Assign lock indexes to data components.
+ for (int index = schema.GetKeyColumnCount(); index < std::ssize(schema.Columns()); ++index) {
+ const auto& columnSchema = schema.Columns()[index];
+ int lockIndex = PrimaryLockIndex;
+
+ if (columnSchema.Lock()) {
+ auto emplaced = groupToIndex.emplace(*columnSchema.Lock(), groupToIndex.size() + 1);
+ if (emplaced.second && lockIndexToName) {
+ lockIndexToName->push_back(*columnSchema.Lock());
+ }
+ lockIndex = emplaced.first->second;
+ }
+
+ if (columnIndexToLockIndex) {
+ (*columnIndexToLockIndex)[index] = lockIndex;
+ }
+ }
+ } else if (columnIndexToLockIndex) {
+ // No locking supported for non-atomic tablets, however we still need the primary
+ // lock descriptor to maintain last commit timestamps.
+ for (int index = schema.GetKeyColumnCount(); index < std::ssize(schema.Columns()); ++index) {
+ (*columnIndexToLockIndex)[index] = PrimaryLockIndex;
+ }
+ }
+ return groupToIndex;
+}
+
+TLockMask GetLockMask(
+ const NTableClient::TTableSchema& schema,
+ bool fullAtomicity,
+ const std::vector<TString>& locks,
+ ELockType lockType)
+{
+ auto groupToIndex = GetLocksMapping(schema, fullAtomicity);
+
+ TLockMask lockMask;
+ for (const auto& lock : locks) {
+ auto it = groupToIndex.find(lock);
+ if (it != groupToIndex.end()) {
+ lockMask.Set(it->second, lockType);
+ } else {
+ THROW_ERROR_EXCEPTION("Lock group %Qv not found in schema", lock);
+ }
+ }
+ return lockMask;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+void ToProto(TKeyColumnsExt* protoKeyColumns, const TKeyColumns& keyColumns)
+{
+ ToProto(protoKeyColumns->mutable_names(), keyColumns);
+}
+
+void FromProto(TKeyColumns* keyColumns, const TKeyColumnsExt& protoKeyColumns)
+{
+ *keyColumns = FromProto<TKeyColumns>(protoKeyColumns.names());
+}
+
+void ToProto(TColumnFilter* protoColumnFilter, const NTableClient::TColumnFilter& columnFilter)
+{
+ if (!columnFilter.IsUniversal()) {
+ for (auto index : columnFilter.GetIndexes()) {
+ protoColumnFilter->add_indexes(index);
+ }
+ }
+}
+
+void FromProto(NTableClient::TColumnFilter* columnFilter, const TColumnFilter& protoColumnFilter)
+{
+ *columnFilter = protoColumnFilter.indexes().empty()
+ ? NTableClient::TColumnFilter()
+ : NTableClient::TColumnFilter(FromProto<NTableClient::TColumnFilter::TIndexes>(protoColumnFilter.indexes()));
+}
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCellTaggedTableSchema::TCellTaggedTableSchema(TTableSchema tableSchema, TCellTag cellTag)
+ : TableSchema(std::move(tableSchema))
+ , CellTag(cellTag)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCellTaggedTableSchemaPtr::TCellTaggedTableSchemaPtr(TTableSchemaPtr tableSchema, TCellTag cellTag)
+ : TableSchema(std::move(tableSchema))
+ , CellTag(cellTag)
+{
+ YT_VERIFY(TableSchema);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+size_t THash<NYT::NTableClient::TStableName>::operator()(const NYT::NTableClient::TStableName& stableName) const
+{
+ return THash<TString>()(stableName.Get());
+}
+
+size_t THash<NYT::NTableClient::TColumnSchema>::operator()(const NYT::NTableClient::TColumnSchema& columnSchema) const
+{
+ return MultiHash(
+ columnSchema.StableName(),
+ columnSchema.Name(),
+ *columnSchema.LogicalType(),
+ columnSchema.SortOrder(),
+ columnSchema.Lock(),
+ columnSchema.Expression(),
+ columnSchema.Aggregate(),
+ columnSchema.Group(),
+ columnSchema.MaxInlineHunkSize());
+}
+
+size_t THash<NYT::NTableClient::TDeletedColumn>::operator()(const NYT::NTableClient::TDeletedColumn& columnSchema) const
+{
+ return THash<NYT::NTableClient::TStableName>()(columnSchema.StableName());
+}
+
+size_t THash<NYT::NTableClient::TTableSchema>::operator()(const NYT::NTableClient::TTableSchema& tableSchema) const
+{
+ size_t result = CombineHashes(THash<bool>()(tableSchema.GetUniqueKeys()), THash<bool>()(tableSchema.GetStrict()));
+ if (tableSchema.HasNontrivialSchemaModification()) {
+ result = CombineHashes(
+ result,
+ THash<NYT::NTableClient::ETableSchemaModification>()(tableSchema.GetSchemaModification()));
+ }
+ for (const auto& columnSchema : tableSchema.Columns()) {
+ result = CombineHashes(result, THash<NYT::NTableClient::TColumnSchema>()(columnSchema));
+ }
+ for (const auto& deletedColumnSchema : tableSchema.DeletedColumns()) {
+ result = CombineHashes(result, THash<NYT::NTableClient::TDeletedColumn>()(
+ deletedColumnSchema));
+ }
+ return result;
+}
diff --git a/yt/yt/client/table_client/schema.h b/yt/yt/client/table_client/schema.h
new file mode 100644
index 0000000000..e8420db240
--- /dev/null
+++ b/yt/yt/client/table_client/schema.h
@@ -0,0 +1,604 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/property.h>
+#include <yt/yt/core/misc/range.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <util/digest/multi.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int PrimaryLockIndex = 0;
+
+DEFINE_ENUM(ELockType,
+ ((None) (0))
+ ((SharedWeak) (1))
+ ((SharedStrong) (2))
+ ((Exclusive) (3))
+);
+
+// COMPAT(gritukan)
+constexpr ELockType MaxOldLockType = ELockType::Exclusive;
+
+ELockType GetStrongestLock(ELockType lhs, ELockType rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLegacyLockMask
+{
+public:
+ explicit TLegacyLockMask(TLegacyLockBitmap value = 0);
+
+ ELockType Get(int index) const;
+ void Set(int index, ELockType lock);
+
+ void Enrich(int columnCount);
+
+ TLegacyLockBitmap GetBitmap() const;
+
+ TLegacyLockMask(const TLegacyLockMask& other) = default;
+ TLegacyLockMask& operator= (const TLegacyLockMask& other) = default;
+
+ static constexpr int BitsPerType = 2;
+ static constexpr TLegacyLockBitmap TypeMask = (1 << BitsPerType) - 1;
+ static constexpr int MaxCount = 8 * sizeof(TLegacyLockBitmap) / BitsPerType;
+
+private:
+ TLegacyLockBitmap Data_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLockMask
+{
+public:
+ TLockMask() = default;
+
+ TLockMask(TLockBitmap bitmap, int size);
+
+ ELockType Get(int index) const;
+ void Set(int index, ELockType lock);
+
+ void Enrich(int size);
+
+ int GetSize() const;
+ TLockBitmap GetBitmap() const;
+
+ // COMPAT(gritukan)
+ TLegacyLockMask ToLegacyMask() const;
+ bool HasNewLocks() const;
+
+ static constexpr int BitsPerType = 4;
+ static_assert(static_cast<int>(TEnumTraits<ELockType>::GetMaxValue()) < (1 << BitsPerType));
+
+ static constexpr ui64 LockMask = (1 << BitsPerType) - 1;
+
+ static constexpr int LocksPerWord = 8 * sizeof(TLockBitmap::value_type) / BitsPerType;
+ static_assert(IsPowerOf2(LocksPerWord));
+
+ // Size of the lock mask should fit into ui16 for wire protocol.
+ static constexpr int MaxSize = (1 << 16) - 1;
+
+private:
+ TLockBitmap Bitmap_;
+ int Size_ = 0;
+
+ void Reserve(int size);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLockMask MaxMask(TLockMask lhs, TLockMask rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// Strong typedef to avoid mixing stable names and names.
+class TStableName
+{
+public:
+ explicit TStableName(TString stableName = "");
+ const TString& Get() const;
+
+private:
+ TString Name_;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TStableName& stableName, TStringBuf spec);
+
+bool operator == (const TStableName& lhs, const TStableName& rhs);
+bool operator < (const TStableName& lhs, const TStableName& rhs);
+
+void ToProto(TString* protoStableName, const TStableName& stableName);
+void FromProto(TStableName* stableName, const TString& protoStableName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TColumnSchema
+{
+public:
+ // Keep in sync with hasher below.
+ DEFINE_BYREF_RO_PROPERTY(TStableName, StableName);
+ DEFINE_BYREF_RO_PROPERTY(TString, Name);
+ DEFINE_BYREF_RO_PROPERTY(TLogicalTypePtr, LogicalType);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<ESortOrder>, SortOrder);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<TString>, Lock);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<TString>, Expression);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<TString>, Aggregate);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<TString>, Group);
+ DEFINE_BYREF_RO_PROPERTY(bool, Required);
+ DEFINE_BYREF_RO_PROPERTY(std::optional<i64>, MaxInlineHunkSize);
+
+public:
+ TColumnSchema();
+ TColumnSchema(
+ TString name,
+ EValueType type,
+ std::optional<ESortOrder> sortOrder = {});
+ TColumnSchema(
+ TString name,
+ ESimpleLogicalValueType type,
+ std::optional<ESortOrder> sortOrder = {});
+
+ TColumnSchema(
+ TString name,
+ TLogicalTypePtr type,
+ std::optional<ESortOrder> sortOrder = {});
+
+ TColumnSchema(const TColumnSchema&) = default;
+ TColumnSchema(TColumnSchema&&) = default;
+
+ TColumnSchema& operator=(const TColumnSchema&) = default;
+ TColumnSchema& operator=(TColumnSchema&&) = default;
+
+ TColumnSchema& SetStableName(TStableName stableName);
+ TColumnSchema& SetName(TString name);
+ TColumnSchema& SetLogicalType(TLogicalTypePtr valueType);
+ TColumnSchema& SetSimpleLogicalType(ESimpleLogicalValueType type);
+ TColumnSchema& SetSortOrder(std::optional<ESortOrder> value);
+ TColumnSchema& SetLock(std::optional<TString> value);
+ TColumnSchema& SetExpression(std::optional<TString> value);
+ TColumnSchema& SetAggregate(std::optional<TString> value);
+ TColumnSchema& SetGroup(std::optional<TString> value);
+ TColumnSchema& SetRequired(bool value);
+ TColumnSchema& SetMaxInlineHunkSize(std::optional<i64> value);
+
+ EValueType GetWireType() const;
+
+ i64 GetMemoryUsage() const;
+
+ // Check if column has plain old v1 type.
+ bool IsOfV1Type() const;
+
+ // Check if column has specified v1 type.
+ bool IsOfV1Type(ESimpleLogicalValueType type) const;
+
+ ESimpleLogicalValueType CastToV1Type() const;
+
+ bool IsRenamed() const;
+ TString GetDiagnosticNameString() const;
+
+private:
+ ESimpleLogicalValueType V1Type_;
+ bool IsOfV1Type_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDeletedColumn
+{
+public:
+ TDeletedColumn();
+ explicit TDeletedColumn(TStableName stableName);
+
+ DEFINE_BYREF_RO_PROPERTY(TStableName, StableName);
+ TDeletedColumn& SetStableName(TStableName stableName);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TColumnSchema& schema, TStringBuf spec);
+
+void Serialize(const TColumnSchema& schema, NYson::IYsonConsumer* consumer);
+
+void ToProto(NProto::TColumnSchema* protoSchema, const TColumnSchema& schema);
+void FromProto(TColumnSchema* schema, const NProto::TColumnSchema& protoSchema);
+
+void ToProto(NProto::TDeletedColumn* protoSchema, const TDeletedColumn& schema);
+void FromProto(TDeletedColumn* schema, const NProto::TDeletedColumn& protoSchema);
+
+void PrintTo(const TColumnSchema& columnSchema, std::ostream* os);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableSchema final
+{
+public:
+ class TNameMapping
+ {
+ public:
+ explicit TNameMapping(const TTableSchema& schema);
+
+ bool IsDeleted(const TStableName& stableName) const;
+ TString StableNameToName(const TStableName& stableName) const;
+ TStableName NameToStableName(TStringBuf name) const;
+
+ private:
+ const TTableSchema& Schema_;
+ };
+
+public:
+ const std::vector<TColumnSchema>& Columns() const;
+ const std::vector<TDeletedColumn>& DeletedColumns() const;
+
+ //! Strict schema forbids columns not specified in the schema.
+ DEFINE_BYVAL_RO_PROPERTY(bool, Strict, false);
+ DEFINE_BYVAL_RO_PROPERTY(bool, UniqueKeys, false);
+ DEFINE_BYVAL_RO_PROPERTY(ETableSchemaModification, SchemaModification, ETableSchemaModification::None);
+
+ //! Constructs an empty non-strict schema.
+ TTableSchema() = default;
+
+ //! Constructs a schema with given columns and strictness flag.
+ //! No validation is performed.
+ explicit TTableSchema(
+ std::vector<TColumnSchema> columns,
+ bool strict = true,
+ bool uniqueKeys = false,
+ ETableSchemaModification schemaModification = ETableSchemaModification::None,
+ std::vector<TDeletedColumn> deletedColumns = {});
+
+ const TColumnSchema* FindColumnByStableName(const TStableName& stableName) const;
+ const TDeletedColumn* FindDeletedColumn(const TStableName& stableName) const;
+
+ int GetColumnIndex(const TColumnSchema& column) const;
+
+ int GetColumnIndex(TStringBuf name) const;
+ int GetColumnIndexOrThrow(TStringBuf name) const;
+
+ TNameMapping GetNameMapping() const;
+
+ const TColumnSchema* FindColumn(TStringBuf name) const;
+ const TColumnSchema& GetColumn(TStringBuf name) const;
+ const TColumnSchema& GetColumnOrThrow(TStringBuf name) const;
+ std::vector<TString> GetColumnNames() const;
+
+ TTableSchemaPtr Filter(
+ const TColumnFilter& columnFilter,
+ bool discardSortOrder = false) const;
+ TTableSchemaPtr Filter(
+ const THashSet<TString>& columnNames,
+ bool discardSortOrder = false) const;
+ TTableSchemaPtr Filter(
+ const std::optional<std::vector<TString>>& columnNames,
+ bool discardSortOrder = false) const;
+
+ bool HasComputedColumns() const;
+ bool HasAggregateColumns() const;
+ bool HasHunkColumns() const;
+ bool HasTimestampColumn() const;
+ bool IsSorted() const;
+ bool IsUniqueKeys() const;
+ bool HasRenamedColumns() const;
+ bool IsEmpty() const;
+
+ std::vector<TStableName> GetKeyColumnStableNames() const;
+ TKeyColumns GetKeyColumnNames() const;
+ TKeyColumns GetKeyColumns() const;
+
+ int GetColumnCount() const;
+ int GetKeyColumnCount() const;
+ int GetValueColumnCount() const;
+ std::vector<TStableName> GetColumnStableNames() const;
+ const THunkColumnIds& GetHunkColumnIds() const;
+
+ TSortColumns GetSortColumns(const std::optional<TNameMapping>& nameMapping = std::nullopt) const;
+
+ bool HasNontrivialSchemaModification() const;
+
+ //! Constructs a non-strict schema from #keyColumns assigning all components EValueType::Any type.
+ //! #keyColumns could be empty, in which case an empty non-strict schema is returned.
+ //! The resulting schema is validated.
+ static TTableSchemaPtr FromKeyColumns(const TKeyColumns& keyColumns);
+
+ //! Same as above, but infers key column sort orders from #sortColumns.
+ static TTableSchemaPtr FromSortColumns(const TSortColumns& sortColumns);
+
+ //! Returns schema with `UniqueKeys' set to given value.
+ TTableSchemaPtr SetUniqueKeys(bool uniqueKeys) const;
+
+ //! Returns schema with `SchemaModification' set to given value.
+ TTableSchemaPtr SetSchemaModification(ETableSchemaModification schemaModification) const;
+
+ //! For sorted tables, return the current schema as-is.
+ //! For ordered tables, prepends the current schema with |(tablet_index, row_index)| key columns.
+ TTableSchemaPtr ToQuery() const;
+
+ //! For sorted tables, return the current schema without computed columns.
+ //! For ordered tables, prepends the current schema with |(tablet_index)| key column
+ //! but without |$timestamp| column, if any.
+ TTableSchemaPtr ToWrite() const;
+
+ //! For sorted tables, return the current schema
+ //! For ordered tables, prepends the current schema with |(tablet_index)| key column.
+ TTableSchemaPtr WithTabletIndex() const;
+
+ //! Returns the current schema as-is.
+ //! For ordered tables, prepends the current schema with |(tablet_index)| key column.
+ TTableSchemaPtr ToVersionedWrite() const;
+
+ //! For sorted tables, returns the non-computed key columns.
+ //! For ordered tables, returns an empty schema.
+ TTableSchemaPtr ToLookup() const;
+
+ //! For sorted tables, returns the non-computed key columns.
+ //! For ordered tables, returns an empty schema.
+ TTableSchemaPtr ToDelete() const;
+
+ //! Returns just the key columns.
+ TTableSchemaPtr ToKeys() const;
+
+ //! Returns the schema with UniqueKeys set to |true|.
+ TTableSchemaPtr ToUniqueKeys() const;
+
+ //! Returns the schema with all column attributes unset except
+ //! StableName, Name, Type and Required.
+ TTableSchemaPtr ToStrippedColumnAttributes() const;
+
+ //! Returns the schema with all column attributes unset except
+ //! StableName, Name, Type, Required and SortOrder.
+ TTableSchemaPtr ToSortedStrippedColumnAttributes() const;
+
+ //! Returns (possibly reordered) schema sorted by column names.
+ TTableSchemaPtr ToCanonical() const;
+
+ //! Returns (possibly reordered) schema with set key columns.
+ TTableSchemaPtr ToSorted(const TKeyColumns& keyColumns) const;
+ TTableSchemaPtr ToSorted(const TSortColumns& sortColumns) const;
+
+ //! Only applies to sorted replicated tables.
+ //! Returns the ordered schema used in replication logs.
+ TTableSchemaPtr ToReplicationLog() const;
+
+ //! Only applies to sorted dynamic tables.
+ //! Returns the static schema used for unversioned updates from bulk insert.
+ //! Key columns remain unchanged. Additional column |($change_type)| is prepended.
+ //! Each value column |name| is replaced with two columns |($value:name)| and |($flags:name)|.
+ //! If |sorted| is |false|, sort order is removed from key columns.
+ TTableSchemaPtr ToUnversionedUpdate(bool sorted = true) const;
+
+ TTableSchemaPtr ToModifiedSchema(ETableSchemaModification schemaModification) const;
+
+ TComparator ToComparator() const;
+
+ TKeyColumnTypes GetKeyColumnTypes() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+ i64 GetMemoryUsage() const;
+
+private:
+ std::shared_ptr<const std::vector<TColumnSchema>> Columns_;
+ std::vector<TDeletedColumn> DeletedColumns_;
+
+ int KeyColumnCount_ = 0;
+ bool HasComputedColumns_ = false;
+ bool HasAggregateColumns_ = false;
+ THunkColumnIds HunkColumnsIds_;
+
+ // NB: Strings are owned by Columns_, addresses are immutable
+ // inside TTableSchema.
+ THashMap<TStringBuf, int> StableNameToColumnIndex_;
+ THashMap<TStringBuf, int> NameToColumnIndex_;
+ THashMap<TStringBuf, int> StableNameToDeletedColumnIndex_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableSchema)
+
+void FormatValue(TStringBuilderBase* builder, const TTableSchema& schema, TStringBuf spec);
+void FormatValue(TStringBuilderBase* builder, const TTableSchemaPtr& schema, TStringBuf spec);
+
+TString ToString(const TTableSchema& schema);
+TString ToString(const TTableSchemaPtr& schema);
+
+//! Returns serialized NTableClient.NProto.TTableSchemaExt.
+TString SerializeToWireProto(const TTableSchemaPtr& schema);
+
+void DeserializeFromWireProto(TTableSchemaPtr* schema, const TString& serializedProto);
+
+void Serialize(const TTableSchema& schema, NYson::IYsonConsumer* consumer);
+void Deserialize(TTableSchema& schema, NYTree::INodePtr node);
+void Deserialize(TTableSchema& schema, NYson::TYsonPullParserCursor* cursor);
+
+void Serialize(const TTableSchemaPtr& schema, NYson::IYsonConsumer* consumer);
+void Deserialize(TTableSchemaPtr& schema, NYTree::INodePtr node);
+void Deserialize(TTableSchemaPtr& schema, NYson::TYsonPullParserCursor* cursor);
+
+void ToProto(NProto::TTableSchemaExt* protoSchema, const TTableSchema& schema);
+void FromProto(TTableSchema* schema, const NProto::TTableSchemaExt& protoSchema);
+void FromProto(
+ TTableSchema* schema,
+ const NProto::TTableSchemaExt& protoSchema,
+ const NProto::TKeyColumnsExt& keyColumnsExt);
+
+void ToProto(NProto::TTableSchemaExt* protoSchema, const TTableSchemaPtr& schema);
+void FromProto(TTableSchemaPtr* schema, const NProto::TTableSchemaExt& protoSchema);
+void FromProto(
+ TTableSchemaPtr* schema,
+ const NProto::TTableSchemaExt& protoSchema,
+ const NProto::TKeyColumnsExt& keyColumnsExt);
+
+void PrintTo(const TTableSchema& tableSchema, std::ostream* os);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator == (const TColumnSchema& lhs, const TColumnSchema& rhs);
+
+bool operator == (const TDeletedColumn& lhs, const TDeletedColumn& rhs);
+
+bool operator == (const TTableSchema& lhs, const TTableSchema& rhs);
+
+// Compat function for https://st.yandex-team.ru/YT-10668 workaround.
+bool IsEqualIgnoringRequiredness(const TTableSchema& lhs, const TTableSchema& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr TStringBuf NonexistentColumnName = "$__YT_NONEXISTENT_COLUMN_NAME__";
+
+std::vector<TStableName> MapNamesToStableNames(
+ const TTableSchema& schema,
+ std::vector<TString> names,
+ const std::optional<TStringBuf>& missingColumnReplacement = std::nullopt);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateKeyColumns(const TKeyColumns& keyColumns);
+
+void ValidateColumnSchema(
+ const TColumnSchema& columnSchema,
+ bool isTableSorted = false,
+ bool isTableDynamic = false,
+ bool allowUnversionedUpdateColumns = false);
+
+void ValidateTableSchema(
+ const TTableSchema& schema,
+ bool isTableDynamic = false,
+ bool allowUnversionedUpdateColumns = false);
+
+void ValidateNoDescendingSortOrder(const TTableSchema& schema);
+
+void ValidateNoRenamedColumns(const TTableSchema& schema);
+
+void ValidateColumnUniqueness(const TTableSchema& schema);
+
+void ValidatePivotKey(
+ TUnversionedRow pivotKey,
+ const TTableSchema& schema,
+ TStringBuf keyType = "pivot",
+ bool validateRequired = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashMap<TString, int> GetLocksMapping(
+ const NTableClient::TTableSchema& schema,
+ bool fullAtomicity,
+ std::vector<int>* columnIndexToLockIndex = nullptr,
+ std::vector<TString>* lockIndexToName = nullptr);
+
+TLockMask GetLockMask(
+ const NTableClient::TTableSchema& schema,
+ bool fullAtomicity,
+ const std::vector<TString>& locks,
+ ELockType lockType = ELockType::SharedWeak);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Need to place this into NProto for ADL to work properly since TKeyColumns is std::vector.
+namespace NProto {
+
+void ToProto(NProto::TKeyColumnsExt* protoKeyColumns, const TKeyColumns& keyColumns);
+void FromProto(TKeyColumns* keyColumns, const NProto::TKeyColumnsExt& protoKeyColumns);
+
+void ToProto(TColumnFilter* protoColumnFilter, const NTableClient::TColumnFilter& columnFilter);
+void FromProto(NTableClient::TColumnFilter* columnFilter, const TColumnFilter& protoColumnFilter);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Incompatible < RequireValidation < FullyCompatible
+constexpr bool operator < (ESchemaCompatibility lhs, ESchemaCompatibility rhs);
+constexpr bool operator <= (ESchemaCompatibility lhs, ESchemaCompatibility rhs);
+constexpr bool operator > (ESchemaCompatibility lhs, ESchemaCompatibility rhs);
+constexpr bool operator >= (ESchemaCompatibility lhs, ESchemaCompatibility rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTableSchemaHash
+{
+ size_t operator() (const TTableSchema& schema) const;
+ size_t operator() (const TTableSchemaPtr& schema) const;
+};
+
+struct TTableSchemaEquals
+{
+ bool operator() (const TTableSchema& lhs, const TTableSchema& rhs) const;
+ bool operator() (const TTableSchemaPtr& lhs, const TTableSchemaPtr& rhs) const;
+ bool operator() (const TTableSchemaPtr& lhs, const TTableSchema& rhs) const;
+};
+
+struct TCellTaggedTableSchema
+{
+ TCellTaggedTableSchema(TTableSchema tableSchema, NObjectClient::TCellTag cellTag);
+
+ TTableSchema TableSchema;
+ NObjectClient::TCellTag CellTag;
+};
+
+struct TCellTaggedTableSchemaPtr
+{
+ TCellTaggedTableSchemaPtr(TTableSchemaPtr tableSchema, NObjectClient::TCellTag cellTag);
+
+ TTableSchemaPtr TableSchema;
+ NObjectClient::TCellTag CellTag;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCellTaggedTableSchemaHash
+{
+ size_t operator() (const TCellTaggedTableSchema& cellTaggedSchema) const;
+ size_t operator() (const TCellTaggedTableSchemaPtr& cellTaggedSchemaPtr) const;
+};
+
+struct TCellTaggedTableSchemaEquals
+{
+ bool operator() (const TCellTaggedTableSchema& lhs, const TCellTaggedTableSchema& rhs) const;
+ bool operator() (const TCellTaggedTableSchemaPtr& lhs, const TCellTaggedTableSchemaPtr& rhs) const;
+ bool operator() (const TCellTaggedTableSchemaPtr& lhs, const TCellTaggedTableSchema& rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+struct THash<NYT::NTableClient::TStableName>
+{
+ size_t operator()(const NYT::NTableClient::TStableName& stableName) const;
+};
+
+template <>
+struct THash<NYT::NTableClient::TDeletedColumn>
+{
+ size_t operator()(const NYT::NTableClient::TDeletedColumn& deletedColumn) const;
+};
+
+template <>
+struct THash<NYT::NTableClient::TColumnSchema>
+{
+ size_t operator()(const NYT::NTableClient::TColumnSchema& columnSchema) const;
+};
+
+template <>
+struct THash<NYT::NTableClient::TTableSchema>
+{
+ size_t operator()(const NYT::NTableClient::TTableSchema& tableSchema) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define SCHEMA_INL_H_
+#include "schema-inl.h"
+#undef SCHEMA_INL_H_
diff --git a/yt/yt/client/table_client/schema_serialization_helpers.cpp b/yt/yt/client/table_client/schema_serialization_helpers.cpp
new file mode 100644
index 0000000000..f9ac42f59c
--- /dev/null
+++ b/yt/yt/client/table_client/schema_serialization_helpers.cpp
@@ -0,0 +1,343 @@
+
+#include "comparator.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/client/table_client/schema_serialization_helpers.h>
+
+namespace NYT::NTableClient {
+
+void Deserialize(TMaybeDeletedColumnSchema& schema, NYson::TYsonPullParserCursor* cursor)
+{
+ TSerializableColumnSchema wrapper = TSerializableColumnSchema::Create();
+ wrapper.DeserializeFromCursor(cursor);
+ schema = wrapper;
+}
+
+void Deserialize(TMaybeDeletedColumnSchema& schema, NYTree::INodePtr node)
+{
+ TSerializableColumnSchema wrapper = TSerializableColumnSchema::Create();
+ Deserialize(static_cast<NYTree::TYsonStructLite&>(wrapper), node);
+ schema = static_cast<TMaybeDeletedColumnSchema>(wrapper);
+}
+
+TDeletedColumn TMaybeDeletedColumnSchema::GetDeletedColumnSchema() const
+{
+ return TDeletedColumn(StableName());
+}
+
+void TSerializableColumnSchema::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("name", &TThis::Name_)
+ .Default();
+ registrar.Parameter("stable_name", &TThis::SerializedStableName_)
+ .Default();
+ registrar.Parameter("type", &TThis::LogicalTypeV1_)
+ .Default(std::nullopt);
+ registrar.Parameter("required", &TThis::RequiredV1_)
+ .Default(std::nullopt);
+ registrar.Parameter("type_v3", &TThis::LogicalTypeV3_)
+ .Default();
+ registrar.BaseClassParameter("lock", &TThis::Lock_)
+ .Default();
+ registrar.BaseClassParameter("expression", &TThis::Expression_)
+ .Default();
+ registrar.BaseClassParameter("aggregate", &TThis::Aggregate_)
+ .Default();
+ registrar.BaseClassParameter("sort_order", &TThis::SortOrder_)
+ .Default();
+ registrar.BaseClassParameter("group", &TThis::Group_)
+ .Default();
+ registrar.BaseClassParameter("max_inline_hunk_size", &TThis::MaxInlineHunkSize_)
+ .Default();
+ registrar.BaseClassParameter("deleted", &TThis::Deleted_).Default(std::nullopt);
+
+ registrar.Postprocessor([] (TSerializableColumnSchema* schema) {
+ schema->RunPostprocessor();
+ });
+}
+
+void TSerializableColumnSchema::DeserializeFromCursor(NYson::TYsonPullParserCursor* cursor)
+{
+ cursor->ParseMap([&] (NYson::TYsonPullParserCursor* cursor) {
+ EnsureYsonToken("column schema attribute key", *cursor, NYson::EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+ if (key == TStringBuf("name")) {
+ cursor->Next();
+ SetName(ExtractTo<TString>(cursor));
+ } else if (key == TStringBuf("required")) {
+ cursor->Next();
+ RequiredV1_ = ExtractTo<bool>(cursor);
+ } else if (key == TStringBuf("type")) {
+ cursor->Next();
+ LogicalTypeV1_ = ExtractTo<ESimpleLogicalValueType>(cursor);
+ } else if (key == TStringBuf("type_v3")) {
+ cursor->Next();
+ LogicalTypeV3_ = TTypeV3LogicalTypeWrapper();
+ Deserialize(*LogicalTypeV3_, cursor);
+ } else if (key == TStringBuf("lock")) {
+ cursor->Next();
+ SetLock(ExtractTo<std::optional<TString>>(cursor));
+ } else if (key == TStringBuf("expression")) {
+ cursor->Next();
+ SetExpression(ExtractTo<std::optional<TString>>(cursor));
+ } else if (key == TStringBuf("aggregate")) {
+ cursor->Next();
+ SetAggregate(ExtractTo<std::optional<TString>>(cursor));
+ } else if (key == TStringBuf("sort_order")) {
+ cursor->Next();
+ SetSortOrder(ExtractTo<std::optional<ESortOrder>>(cursor));
+ } else if (key == TStringBuf("group")) {
+ cursor->Next();
+ SetGroup(ExtractTo<std::optional<TString>>(cursor));
+ } else if (key == TStringBuf("max_inline_hunk_size")) {
+ cursor->Next();
+ SetMaxInlineHunkSize(ExtractTo<std::optional<i64>>(cursor));
+ } else if (key == TStringBuf("stable_name")) {
+ cursor->Next();
+ SerializedStableName_ = ExtractTo<TString>(cursor);
+ } else if (key == TStringBuf("deleted")) {
+ cursor->Next();
+ Deleted_ = ExtractTo<bool>(cursor);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+
+ RunPostprocessor();
+}
+
+void TSerializableColumnSchema::SetColumnSchema(const TColumnSchema& columnSchema)
+{
+ static_cast<TColumnSchema&>(*this) = columnSchema;
+ if (IsRenamed()) {
+ SerializedStableName_ = StableName().Get();
+ }
+ LogicalTypeV1_ = columnSchema.CastToV1Type();
+ RequiredV1_ = columnSchema.Required();
+ LogicalTypeV3_ = TTypeV3LogicalTypeWrapper{columnSchema.LogicalType()};
+}
+
+void TSerializableColumnSchema::SetDeletedColumnSchema(
+ const TDeletedColumn& deletedColumnSchema)
+{
+ Deleted_ = true;
+ StableName_ = deletedColumnSchema.StableName();
+}
+
+void TSerializableColumnSchema::RunPostprocessor()
+{
+ if (Deleted() && *Deleted()) {
+ if (!SerializedStableName_ || SerializedStableName_->empty()) {
+ THROW_ERROR_EXCEPTION("stable name should be set for a deleted column");
+ }
+ SetStableName(TStableName(*SerializedStableName_));
+ return;
+ }
+
+ // Name
+ if (Name().empty()) {
+ THROW_ERROR_EXCEPTION("Column name cannot be empty");
+ }
+
+ if (SerializedStableName_.has_value()) {
+ if (SerializedStableName_->empty()) {
+ THROW_ERROR_EXCEPTION("Column stable name cannot be empty");
+ }
+ SetStableName(TStableName(*SerializedStableName_));
+ } else {
+ SetStableName(TStableName(Name()));
+ }
+
+ try {
+ int setTypeVersion = 0;
+ if (LogicalTypeV3_) {
+ SetLogicalType(LogicalTypeV3_->LogicalType);
+ setTypeVersion = 3;
+ }
+
+ if (LogicalTypeV1_) {
+ if (setTypeVersion == 0) {
+ SetLogicalType(MakeLogicalType(*LogicalTypeV1_, RequiredV1_.value_or(false)));
+ setTypeVersion = 1;
+ } else {
+ if (*LogicalTypeV1_ != CastToV1Type()) {
+ THROW_ERROR_EXCEPTION(
+ "\"type_v%v\" does not match \"type\"; \"type_v%v\": %Qv \"type\": %Qlv expected \"type\": %Qlv",
+ setTypeVersion,
+ setTypeVersion,
+ *LogicalType(),
+ *LogicalTypeV1_,
+ CastToV1Type());
+ }
+ }
+ }
+
+ if (RequiredV1_ && setTypeVersion > 1 && *RequiredV1_ != Required()) {
+ THROW_ERROR_EXCEPTION(
+ "\"type_v%v\" does not match \"required\"; \"type_v%v\": %Qv \"required\": %Qlv",
+ setTypeVersion,
+ setTypeVersion,
+ *LogicalType(),
+ *RequiredV1_);
+ }
+
+ if (setTypeVersion == 0) {
+ THROW_ERROR_EXCEPTION("Column type is not specified");
+ }
+
+ if (*DetagLogicalType(LogicalType()) == *SimpleLogicalType(ESimpleLogicalValueType::Any)) {
+ THROW_ERROR_EXCEPTION("Column of type %Qlv cannot be \"required\"",
+ ESimpleLogicalValueType::Any);
+ }
+
+ // Lock
+ if (Lock() && Lock()->empty()) {
+ THROW_ERROR_EXCEPTION("Lock name cannot be empty");
+ }
+
+ // Group
+ if (Group() && Group()->empty()) {
+ THROW_ERROR_EXCEPTION("Group name cannot be empty");
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error validating column %Qv in table schema",
+ GetDiagnosticNameString())
+ << ex;
+ }
+}
+
+void Serialize(const TColumnSchema& schema, NYson::IYsonConsumer* consumer)
+{
+ TSerializableColumnSchema wrapper = TSerializableColumnSchema::Create();
+ wrapper.SetColumnSchema(schema);
+ Serialize(static_cast<const NYTree::TYsonStructLite&>(wrapper), consumer);
+}
+
+void Serialize(const TDeletedColumn& schema, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("stable_name");
+ consumer->OnStringScalar(schema.StableName().Get());
+ consumer->OnKeyedItem("deleted");
+ consumer->OnBooleanScalar(true);
+ consumer->OnEndMap();
+}
+
+void Serialize(const TTableSchema& schema, NYson::IYsonConsumer* consumer)
+{
+ auto position = NYTree::BuildYsonFluently(consumer)
+ .BeginAttributes()
+ .Item("strict").Value(schema.GetStrict())
+ .Item("unique_keys").Value(schema.GetUniqueKeys())
+ .DoIf(schema.HasNontrivialSchemaModification(), [&] (NYTree::TFluentMap fluent) {
+ fluent.Item("schema_modification").Value(schema.GetSchemaModification());
+ })
+ .EndAttributes()
+ .BeginList();
+
+ for (const auto& column : schema.Columns()) {
+ Serialize(column, position.Item().GetConsumer());
+ }
+ for (const auto& deletedColumn : schema.DeletedColumns()) {
+ Serialize(deletedColumn, position.Item().GetConsumer());
+ }
+
+ position.EndList();
+}
+
+void Serialize(const TTableSchemaPtr& schema, NYson::IYsonConsumer* consumer)
+{
+ Serialize(*schema, consumer);
+}
+
+void Deserialize(TTableSchema& schema, NYTree::INodePtr node)
+{
+ auto childNodes = node->AsList()->GetChildren();
+
+ std::vector<TColumnSchema> columns;
+ std::vector<TDeletedColumn> deletedColumns;
+
+ for (auto childNode : childNodes) {
+ auto wrapper = TSerializableColumnSchema::Create();
+ Deserialize(static_cast<NYTree::TYsonStructLite&>(wrapper), childNode);
+ if (wrapper.Deleted() && *wrapper.Deleted()) {
+ deletedColumns.push_back(TDeletedColumn(wrapper.StableName()));
+ } else {
+ columns.push_back(wrapper);
+ }
+ }
+
+ schema = TTableSchema(
+ columns,
+ node->Attributes().Get<bool>("strict", true),
+ node->Attributes().Get<bool>("unique_keys", false),
+ node->Attributes().Get<ETableSchemaModification>(
+ "schema_modification",
+ ETableSchemaModification::None),
+ deletedColumns);
+}
+
+void Deserialize(TTableSchema& schema, NYson::TYsonPullParserCursor* cursor)
+{
+ bool strict = true;
+ bool uniqueKeys = false;
+ ETableSchemaModification modification = ETableSchemaModification::None;
+
+ if ((*cursor)->GetType() == NYson::EYsonItemType::BeginAttributes) {
+ cursor->ParseAttributes([&] (NYson::TYsonPullParserCursor* cursor) {
+ EnsureYsonToken(TStringBuf("table schema attribute key"), *cursor, NYson::EYsonItemType::StringValue);
+ auto key = (*cursor)->UncheckedAsString();
+ if (key == TStringBuf("strict")) {
+ cursor->Next();
+ strict = ExtractTo<bool>(cursor);
+ } else if (key == TStringBuf("unique_keys")) {
+ cursor->Next();
+ uniqueKeys = ExtractTo<bool>(cursor);
+ } else if (key == TStringBuf("schema_modification")) {
+ cursor->Next();
+ modification = ExtractTo<ETableSchemaModification>(cursor);
+ } else {
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ });
+ }
+
+ EnsureYsonToken(TStringBuf("table schema"), *cursor, NYson::EYsonItemType::BeginList);
+
+ auto maybeDeletedColumns = ExtractTo<std::vector<TMaybeDeletedColumnSchema>>(cursor);
+
+ std::vector<TColumnSchema> columns;
+ std::vector<TDeletedColumn> deletedColumns;
+
+ for (const auto& maybeDeletedColumn : maybeDeletedColumns) {
+ if (maybeDeletedColumn.Deleted() && *maybeDeletedColumn.Deleted()) {
+ deletedColumns.push_back(maybeDeletedColumn.GetDeletedColumnSchema());
+ } else {
+ columns.push_back(static_cast<TColumnSchema>(maybeDeletedColumn));
+ }
+ }
+
+ schema = TTableSchema(columns, strict, uniqueKeys, modification, deletedColumns);
+}
+
+void Deserialize(TTableSchemaPtr& schema, NYTree::INodePtr node)
+{
+ TTableSchema actualSchema;
+ Deserialize(actualSchema, node);
+ schema = New<TTableSchema>(std::move(actualSchema));
+}
+
+void Deserialize(TTableSchemaPtr& schema, NYson::TYsonPullParserCursor* cursor)
+{
+ TTableSchema actualSchema;
+ Deserialize(actualSchema, cursor);
+ schema = New<TTableSchema>(std::move(actualSchema));
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schema_serialization_helpers.h b/yt/yt/client/table_client/schema_serialization_helpers.h
new file mode 100644
index 0000000000..43b9ad6070
--- /dev/null
+++ b/yt/yt/client/table_client/schema_serialization_helpers.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+
+#include <yt/yt/client/table_client/schema.h>
+
+namespace NYT::NTableClient {
+
+struct TMaybeDeletedColumnSchema : public TColumnSchema
+{
+ DEFINE_BYREF_RO_PROPERTY(std::optional<bool>, Deleted);
+
+ TDeletedColumn GetDeletedColumnSchema() const;
+};
+
+void Deserialize(TMaybeDeletedColumnSchema& schema, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(TMaybeDeletedColumnSchema& schema, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializableColumnSchema
+ : public TMaybeDeletedColumnSchema
+ , public NYTree::TYsonStructLite
+{
+ REGISTER_YSON_STRUCT_LITE(TSerializableColumnSchema);
+
+ static void Register(TRegistrar registrar);
+
+ void RunPostprocessor();
+
+public:
+ void DeserializeFromCursor(NYson::TYsonPullParserCursor* cursor);
+
+ void SetColumnSchema(const TColumnSchema& columnSchema);
+ void SetDeletedColumnSchema(const TDeletedColumn& deletedColumnSchema);
+
+private:
+ std::optional<TString> SerializedStableName_;
+
+ std::optional<ESimpleLogicalValueType> LogicalTypeV1_;
+ std::optional<bool> RequiredV1_;
+
+ std::optional<TTypeV3LogicalTypeWrapper> LogicalTypeV3_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schemaful_reader_adapter.cpp b/yt/yt/client/table_client/schemaful_reader_adapter.cpp
new file mode 100644
index 0000000000..ef0b5c4827
--- /dev/null
+++ b/yt/yt/client/table_client/schemaful_reader_adapter.cpp
@@ -0,0 +1,207 @@
+#include "schemaful_reader_adapter.h"
+#include "schemaless_row_reorderer.h"
+
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/row_batch.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+using namespace NChunkClient::NProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSchemafulReaderAdapter)
+
+struct TSchemafulReaderAdapterPoolTag { };
+
+class TSchemafulReaderAdapter
+ : public ISchemafulUnversionedReader
+{
+public:
+ TSchemafulReaderAdapter(
+ ISchemalessUnversionedReaderPtr underlyingReader,
+ TTableSchemaPtr schema,
+ TKeyColumns keyColumns,
+ bool ignoreRequired)
+ : UnderlyingReader_(std::move(underlyingReader))
+ , ReaderSchema_(std::move(schema))
+ , RowReorderer_(TNameTable::FromSchema(*ReaderSchema_), RowBuffer_, /*deepCapture*/ false, std::move(keyColumns))
+ , IgnoreRequired_(ignoreRequired)
+ { }
+
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& options) override
+ {
+ if (ErrorPromise_.IsSet()) {
+ return CreateEmptyUnversionedRowBatch();
+ }
+
+ std::vector<TUnversionedRow> schemafulRows;
+ schemafulRows.reserve(options.MaxRowsPerRead);
+ RowBuffer_->Clear();
+
+ CurrentBatch_ = UnderlyingReader_->Read(options);
+ if (!CurrentBatch_) {
+ return nullptr;
+ }
+
+ try {
+ for (auto schemalessRow : CurrentBatch_->MaterializeRows()) {
+ if (!schemalessRow) {
+ schemafulRows.push_back(TUnversionedRow());
+ continue;
+ }
+
+ auto schemafulRow = RowReorderer_.ReorderKey(schemalessRow);
+
+ for (int valueIndex = 0; valueIndex < std::ssize(ReaderSchema_->Columns()); ++valueIndex) {
+ const auto& value = schemafulRow[valueIndex];
+ ValidateDataValue(value);
+ // The underlying schemaless reader may unpack typed scalar values even
+ // if the schema has "any" type. For schemaful reader, this is not an expected behavior
+ // so we have to convert such values back into YSON.
+ // Cf. YT-5396
+ if (ReaderSchema_->Columns()[valueIndex].GetWireType() == EValueType::Any &&
+ value.Type != EValueType::Any &&
+ value.Type != EValueType::Null)
+ {
+ schemafulRow[valueIndex] = MakeAnyFromScalar(value);
+ } else {
+ ValidateValueType(value, *ReaderSchema_, valueIndex, /*typeAnyAcceptsAllValues*/ false, IgnoreRequired_);
+ }
+ }
+ schemafulRows.push_back(schemafulRow);
+ }
+ } catch (const std::exception& ex) {
+ ErrorPromise_.Set(ex);
+ return CreateEmptyUnversionedRowBatch();
+ }
+
+ return CreateBatchFromUnversionedRows(MakeSharedRange(std::move(schemafulRows), MakeStrong(this)));
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ if (ErrorPromise_.IsSet()) {
+ return ErrorPromise_.ToFuture();
+ } else {
+ return UnderlyingReader_->GetReadyEvent();
+ }
+ }
+
+ TDataStatistics GetDataStatistics() const override
+ {
+ return UnderlyingReader_->GetDataStatistics();
+ }
+
+ NChunkClient::TCodecStatistics GetDecompressionStatistics() const override
+ {
+ return UnderlyingReader_->GetDecompressionStatistics();
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ return UnderlyingReader_->IsFetchingCompleted();
+ }
+
+ std::vector<NChunkClient::TChunkId> GetFailedChunkIds() const override
+ {
+ return UnderlyingReader_->GetFailedChunkIds();
+ }
+
+private:
+ const ISchemalessUnversionedReaderPtr UnderlyingReader_;
+ const TTableSchemaPtr ReaderSchema_;
+
+ const TRowBufferPtr RowBuffer_ = New<TRowBuffer>(TSchemafulReaderAdapterPoolTag());
+ TSchemalessRowReorderer RowReorderer_;
+
+ const bool IgnoreRequired_;
+
+ IUnversionedRowBatchPtr CurrentBatch_;
+ TBlobOutput ValueBuffer_;
+
+ const TPromise<void> ErrorPromise_ = NewPromise<void>();
+
+
+ TUnversionedValue MakeAnyFromScalar(const TUnversionedValue& value)
+ {
+ ValueBuffer_.Clear();
+ TBufferedBinaryYsonWriter writer(&ValueBuffer_);
+ switch (value.Type) {
+ case EValueType::Int64:
+ writer.OnInt64Scalar(value.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ writer.OnUint64Scalar(value.Data.Uint64);
+ break;
+ case EValueType::Double:
+ writer.OnDoubleScalar(value.Data.Double);
+ break;
+ case EValueType::Boolean:
+ writer.OnBooleanScalar(value.Data.Boolean);
+ break;
+ case EValueType::String:
+ writer.OnStringScalar(value.AsStringBuf());
+ break;
+ case EValueType::Null:
+ writer.OnEntity();
+ break;
+ case EValueType::Composite:
+ writer.OnRaw(value.AsStringBuf());
+ break;
+
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ default:
+ YT_ABORT();
+ }
+ writer.Flush();
+ auto ysonSize = ValueBuffer_.Size();
+ auto* ysonBuffer = RowBuffer_->GetPool()->AllocateUnaligned(ysonSize);
+ ::memcpy(ysonBuffer, ValueBuffer_.Begin(), ysonSize);
+ return MakeUnversionedAnyValue(TStringBuf(ysonBuffer, ysonSize), value.Id);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSchemafulReaderAdapter)
+
+ISchemafulUnversionedReaderPtr CreateSchemafulReaderAdapter(
+ TSchemalessReaderFactory createReader,
+ TTableSchemaPtr schema,
+ const TColumnFilter& columnFilter,
+ bool ignoreRequired)
+{
+ TKeyColumns keyColumns;
+ for (const auto& columnSchema : schema->Columns()) {
+ keyColumns.push_back(columnSchema.Name());
+ }
+
+ auto nameTable = TNameTable::FromSchema(*schema);
+ auto underlyingReader = createReader(
+ nameTable,
+ columnFilter.IsUniversal() ? TColumnFilter(schema->GetColumnCount()) : columnFilter);
+
+ auto result = New<TSchemafulReaderAdapter>(
+ std::move(underlyingReader),
+ std::move(schema),
+ std::move(keyColumns),
+ ignoreRequired);
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schemaful_reader_adapter.h b/yt/yt/client/table_client/schemaful_reader_adapter.h
new file mode 100644
index 0000000000..b9ccdeaa82
--- /dev/null
+++ b/yt/yt/client/table_client/schemaful_reader_adapter.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "row_base.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TSchemalessReaderFactory = std::function<ISchemalessUnversionedReaderPtr(
+ TNameTablePtr nameTable,
+ const TColumnFilter& columnFilter)> ;
+
+ISchemafulUnversionedReaderPtr CreateSchemafulReaderAdapter(
+ TSchemalessReaderFactory createReader,
+ TTableSchemaPtr schema,
+ const TColumnFilter& columnFilter = {},
+ bool ignoreRequired = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schemaless_row_reorderer.cpp b/yt/yt/client/table_client/schemaless_row_reorderer.cpp
new file mode 100644
index 0000000000..88a11d7cc8
--- /dev/null
+++ b/yt/yt/client/table_client/schemaless_row_reorderer.cpp
@@ -0,0 +1,86 @@
+#include "schemaless_row_reorderer.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchemalessRowReorderer::TSchemalessRowReorderer(
+ TNameTablePtr nameTable,
+ TRowBufferPtr rowBuffer,
+ bool captureValues,
+ const TKeyColumns& keyColumns)
+ : KeyColumns_(keyColumns)
+ , RowBuffer_(std::move(rowBuffer))
+ , CaptureValues_(captureValues)
+ , NameTable_(nameTable)
+{
+ EmptyKey_.resize(KeyColumns_.size(), MakeUnversionedSentinelValue(EValueType::Null));
+ for (int i = 0; i < std::ssize(KeyColumns_); ++i) {
+ auto id = NameTable_->GetIdOrRegisterName(KeyColumns_[i]);
+ EmptyKey_[i].Id = id;
+ if (id >= std::ssize(IdMapping_)) {
+ IdMapping_.resize(id + 1, -1);
+ }
+ IdMapping_[id] = i;
+ }
+}
+
+TMutableUnversionedRow TSchemalessRowReorderer::ReorderRow(TUnversionedRow row)
+{
+ int valueCount = KeyColumns_.size() + row.GetCount();
+ auto result = RowBuffer_->AllocateUnversioned(valueCount);
+
+ // Initialize with empty key.
+ ::memcpy(result.Begin(), EmptyKey_.data(), KeyColumns_.size() * sizeof(TUnversionedValue));
+
+ int nextValueIndex = KeyColumns_.size();
+ int idMappingSize = static_cast<int>(IdMapping_.size());
+ for (auto value : row) {
+ if (CaptureValues_) {
+ RowBuffer_->CaptureValue(&value);
+ }
+ if (value.Id < idMappingSize) {
+ int keyIndex = IdMapping_[value.Id];
+ if (keyIndex >= 0) {
+ result.Begin()[keyIndex] = value;
+ --valueCount;
+ continue;
+ }
+ }
+ result.Begin()[nextValueIndex] = value;
+ ++nextValueIndex;
+ }
+
+ result.SetCount(valueCount);
+ return result;
+}
+
+TMutableUnversionedRow TSchemalessRowReorderer::ReorderKey(TUnversionedRow row)
+{
+ auto result = RowBuffer_->AllocateUnversioned(KeyColumns_.size());
+
+ // Initialize with empty key.
+ ::memcpy(result.Begin(), EmptyKey_.data(), KeyColumns_.size() * sizeof(TUnversionedValue));
+
+ int idMappingSize = static_cast<int>(IdMapping_.size());
+ for (auto value : row) {
+ if (CaptureValues_) {
+ RowBuffer_->CaptureValue(&value);
+ }
+ if (value.Id < idMappingSize) {
+ int keyIndex = IdMapping_[value.Id];
+ if (keyIndex >= 0) {
+ result.Begin()[keyIndex] = value;
+ }
+ }
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/schemaless_row_reorderer.h b/yt/yt/client/table_client/schemaless_row_reorderer.h
new file mode 100644
index 0000000000..9f001b464c
--- /dev/null
+++ b/yt/yt/client/table_client/schemaless_row_reorderer.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "unversioned_row.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Reorders values in original rows, putting key columns at the front.
+//! Omitted key columns are filled in with null values.
+//! All non-key columns are positioned after key ones, preserving order.
+class TSchemalessRowReorderer
+ : public TNonCopyable
+{
+public:
+ TSchemalessRowReorderer(
+ TNameTablePtr nameTable,
+ TRowBufferPtr rowBuffer,
+ bool captureValues,
+ const TKeyColumns& keyColumns);
+
+ TMutableUnversionedRow ReorderRow(TUnversionedRow row);
+
+ //! Preserves only key columns, non-key column are ignored.
+ TMutableUnversionedRow ReorderKey(TUnversionedRow row);
+
+private:
+ const TKeyColumns KeyColumns_;
+ const TRowBufferPtr RowBuffer_;
+ const bool CaptureValues_;
+ const TNameTablePtr NameTable_;
+
+ std::vector<int> IdMapping_;
+ std::vector<TUnversionedValue> EmptyKey_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/serialize.cpp b/yt/yt/client/table_client/serialize.cpp
new file mode 100644
index 0000000000..5f9ca0d24d
--- /dev/null
+++ b/yt/yt/client/table_client/serialize.cpp
@@ -0,0 +1,16 @@
+#include "serialize.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLoadContext::TLoadContext(
+ IZeroCopyInput* input,
+ TRowBufferPtr rowBuffer)
+ : NPhoenix::TLoadContext(input)
+ , RowBuffer_(std::move(rowBuffer))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/serialize.h b/yt/yt/client/table_client/serialize.h
new file mode 100644
index 0000000000..de4a59364d
--- /dev/null
+++ b/yt/yt/client/table_client/serialize.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/property.h>
+#include <yt/yt/core/misc/phoenix.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSaveContext
+ : public NPhoenix::TSaveContext
+{
+public:
+ using NPhoenix::TSaveContext::TSaveContext;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLoadContext
+ : public NPhoenix::TLoadContext
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(TRowBufferPtr, RowBuffer);
+
+public:
+ TLoadContext(
+ IZeroCopyInput* input,
+ TRowBufferPtr rowBuffer);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/table_consumer.cpp b/yt/yt/client/table_client/table_consumer.cpp
new file mode 100644
index 0000000000..68a38b549f
--- /dev/null
+++ b/yt/yt/client/table_client/table_consumer.cpp
@@ -0,0 +1,601 @@
+#include "table_consumer.h"
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <util/string/cast.h>
+
+#include <cmath>
+#include <variant>
+
+namespace NYT::NTableClient {
+
+using namespace NFormats;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonToUnversionedValueConverter::TYsonToUnversionedValueConverter(
+ const TYsonConverterConfig& config,
+ IValueConsumer* valueConsumer)
+ : TYsonToUnversionedValueConverter(config, std::vector<IValueConsumer*>{valueConsumer})
+{ }
+
+TYsonToUnversionedValueConverter::TYsonToUnversionedValueConverter(
+ const TYsonConverterConfig& config,
+ std::vector<IValueConsumer*> valueConsumers,
+ int tableIndex)
+ : ValueConsumers_(std::move(valueConsumers))
+ , ValueWriter_(&ValueBuffer_)
+ , ConvertedWriter_(&ConvertedBuffer_)
+{
+ SwitchToTable(tableIndex);
+
+ for (size_t tableIndex = 0; tableIndex < ValueConsumers_.size(); ++tableIndex) {
+ const auto& valueConsumer = ValueConsumers_[tableIndex];
+ const auto& nameTable = valueConsumer->GetNameTable();
+
+ for (const auto& column : valueConsumer->GetSchema()->Columns()) {
+ const auto id = nameTable->GetIdOrRegisterName(column.Name());
+ const auto key = std::pair(tableIndex, id);
+ auto converter = CreateYsonClientToServerConverter(TComplexTypeFieldDescriptor(column), config);
+ if (IsV3Composite(column.LogicalType())) {
+ ComplexTypeConverters_.emplace(key, std::move(converter));
+ } else if (converter) {
+ SimpleValueConverters_.emplace(key, std::move(converter));
+ }
+ }
+ }
+}
+
+IValueConsumer* TYsonToUnversionedValueConverter::SwitchToTable(int tableIndex)
+{
+ TableIndex_ = tableIndex;
+ YT_VERIFY(0 <= tableIndex && tableIndex < std::ssize(ValueConsumers_));
+ CurrentValueConsumer_ = ValueConsumers_[tableIndex];
+ YT_VERIFY(CurrentValueConsumer_ != nullptr);
+ return CurrentValueConsumer_;
+}
+
+void TYsonToUnversionedValueConverter::SetColumnIndex(int columnIndex)
+{
+ ColumnIndex_ = columnIndex;
+}
+
+void TYsonToUnversionedValueConverter::OnStringScalar(TStringBuf value)
+{
+ if (Depth_ == 0) {
+ auto unversionedValue = MakeUnversionedStringValue(value, ColumnIndex_);
+ if (!TryConvertAndFeedValueConsumer(unversionedValue)) {
+ CurrentValueConsumer_->OnValue(unversionedValue);
+ }
+ } else {
+ ValueWriter_.OnStringScalar(value);
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnInt64Scalar(i64 value)
+{
+ if (Depth_ == 0) {
+ auto unversionedValue = MakeUnversionedInt64Value(value, ColumnIndex_);
+ if (!TryConvertAndFeedValueConsumer(unversionedValue)) {
+ CurrentValueConsumer_->OnValue(unversionedValue);
+ }
+ } else {
+ ValueWriter_.OnInt64Scalar(value);
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnUint64Scalar(ui64 value)
+{
+ if (Depth_ == 0) {
+ auto unversionedValue = MakeUnversionedUint64Value(value, ColumnIndex_);
+ if (!TryConvertAndFeedValueConsumer(unversionedValue)) {
+ CurrentValueConsumer_->OnValue(unversionedValue);
+ }
+ } else {
+ ValueWriter_.OnUint64Scalar(value);
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnDoubleScalar(double value)
+{
+ if (Depth_ == 0) {
+ CurrentValueConsumer_->OnValue(MakeUnversionedDoubleValue(value, ColumnIndex_));
+ } else {
+ ValueWriter_.OnDoubleScalar(value);
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnBooleanScalar(bool value)
+{
+ if (Depth_ == 0) {
+ CurrentValueConsumer_->OnValue(MakeUnversionedBooleanValue(value, ColumnIndex_));
+ } else {
+ ValueWriter_.OnBooleanScalar(value);
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnEntity()
+{
+ if (Depth_ == 0) {
+ CurrentValueConsumer_->OnValue(MakeUnversionedSentinelValue(EValueType::Null, ColumnIndex_));
+ } else {
+ ValueWriter_.OnEntity();
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnBeginList()
+{
+ ValueWriter_.OnBeginList();
+ ++Depth_;
+}
+
+void TYsonToUnversionedValueConverter::OnBeginAttributes()
+{
+ if (Depth_ == 0) {
+ THROW_ERROR_EXCEPTION("Table values cannot have top-level attributes");
+ }
+
+ ValueWriter_.OnBeginAttributes();
+ ++Depth_;
+}
+
+void TYsonToUnversionedValueConverter::OnListItem()
+{
+ if (Depth_ > 0) {
+ ValueWriter_.OnListItem();
+ }
+}
+
+void TYsonToUnversionedValueConverter::OnBeginMap()
+{
+ ValueWriter_.OnBeginMap();
+ ++Depth_;
+}
+
+void TYsonToUnversionedValueConverter::OnKeyedItem(TStringBuf name)
+{
+ ValueWriter_.OnKeyedItem(name);
+}
+
+void TYsonToUnversionedValueConverter::OnEndMap()
+{
+ YT_VERIFY(Depth_ > 0);
+
+ --Depth_;
+ ValueWriter_.OnEndMap();
+ FlushCurrentValueIfCompleted();
+}
+
+void TYsonToUnversionedValueConverter::OnEndList()
+{
+ YT_VERIFY(Depth_ > 0);
+
+ --Depth_;
+ ValueWriter_.OnEndList();
+ FlushCurrentValueIfCompleted();
+}
+
+void TYsonToUnversionedValueConverter::OnEndAttributes()
+{
+ --Depth_;
+
+ YT_VERIFY(Depth_ > 0);
+ ValueWriter_.OnEndAttributes();
+}
+
+void TYsonToUnversionedValueConverter::FlushCurrentValueIfCompleted()
+{
+ if (Depth_ == 0) {
+ ValueWriter_.Flush();
+ auto accumulatedYson = TStringBuf(ValueBuffer_.Begin(), ValueBuffer_.Begin() + ValueBuffer_.Size());
+ auto it = ComplexTypeConverters_.find(std::pair(TableIndex_, ColumnIndex_));
+ TUnversionedValue value;
+ if (it == ComplexTypeConverters_.end()) {
+ value = MakeUnversionedAnyValue(accumulatedYson, ColumnIndex_);
+ } else {
+ const auto& converter = it->second;
+ if (converter) {
+ value = converter(MakeUnversionedStringValue(ValueBuffer_.Blob().ToStringBuf()));
+ value.Id = ColumnIndex_;
+ } else {
+ value = MakeUnversionedCompositeValue(accumulatedYson, ColumnIndex_);
+ }
+ }
+ CurrentValueConsumer_->OnValue(value);
+ ValueBuffer_.Clear();
+ }
+}
+
+bool TYsonToUnversionedValueConverter::TryConvertAndFeedValueConsumer(TUnversionedValue value)
+{
+ auto it = SimpleValueConverters_.find(std::pair(TableIndex_, ColumnIndex_));
+ if (it != SimpleValueConverters_.end()) {
+ auto converted = it->second(value);
+ converted.Id = ColumnIndex_;
+ CurrentValueConsumer_->OnValue(converted);
+ return true;
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableConsumer::TTableConsumer(
+ const TYsonConverterConfig& config,
+ std::vector<IValueConsumer*> valueConsumers,
+ int tableIndex)
+ : YsonToUnversionedValueConverter_(config, std::move(valueConsumers))
+{
+ for (auto* consumer : YsonToUnversionedValueConverter_.ValueConsumers()) {
+ NameTableWriters_.emplace_back(std::make_unique<TNameTableWriter>(consumer->GetNameTable()));
+ }
+ SwitchToTable(tableIndex);
+}
+
+TTableConsumer::TTableConsumer(const TYsonConverterConfig& config, IValueConsumer* valueConsumer)
+ : TTableConsumer(config, std::vector<IValueConsumer*>(1, valueConsumer))
+{ }
+
+TError TTableConsumer::AttachLocationAttributes(TError error) const
+{
+ return error << TErrorAttribute("row_index", RowIndex_);
+}
+
+void TTableConsumer::OnControlInt64Scalar(i64 value)
+{
+ switch (ControlAttribute_) {
+ case EControlAttribute::TableIndex:
+ if (value < 0 || value >= GetTableCount()) {
+ THROW_ERROR AttachLocationAttributes(TError(
+ "Invalid table index %v: expected integer in range [0,%v]",
+ value,
+ GetTableCount() - 1));
+ }
+ SwitchToTable(value);
+ break;
+
+ default:
+ ThrowControlAttributesNotSupported();
+ }
+}
+
+void TTableConsumer::OnControlStringScalar(TStringBuf /*value*/)
+{
+ ThrowControlAttributesNotSupported();
+}
+
+void TTableConsumer::OnStringScalar(TStringBuf value)
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ OnControlStringScalar(value);
+ ControlState_ = EControlState::ExpectEndAttributes;
+ return;
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnStringScalar(value);
+ }
+}
+
+void TTableConsumer::OnInt64Scalar(i64 value)
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ OnControlInt64Scalar(value);
+ ControlState_ = EControlState::ExpectEndAttributes;
+ return;
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnInt64Scalar(value);
+ }
+}
+
+void TTableConsumer::OnUint64Scalar(ui64 value)
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ ThrowInvalidControlAttribute("be an unsigned integer");
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnUint64Scalar(value);
+ }
+}
+
+void TTableConsumer::OnDoubleScalar(double value)
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ ThrowInvalidControlAttribute("be a double value");
+ return;
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnDoubleScalar(value);
+ }
+}
+
+void TTableConsumer::OnBooleanScalar(bool value)
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ ThrowInvalidControlAttribute("be a boolean value");
+ return;
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnBooleanScalar(value);
+ }
+}
+
+void TTableConsumer::OnEntity()
+{
+ switch (ControlState_) {
+ case EControlState::None:
+ break;
+
+ case EControlState::ExpectEntity:
+ YT_ASSERT(Depth_ == 0);
+ // Successfully processed control statement.
+ ControlState_ = EControlState::None;
+ return;
+
+ case EControlState::ExpectValue:
+ ThrowInvalidControlAttribute("be an entity");
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnEntity();
+ }
+}
+
+void TTableConsumer::OnBeginList()
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ ThrowInvalidControlAttribute("be a list");
+ return;
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ThrowMapExpected();
+ } else {
+ YsonToUnversionedValueConverter_.OnBeginList();
+ }
+ ++Depth_;
+}
+
+void TTableConsumer::OnBeginAttributes()
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ ThrowInvalidControlAttribute("have attributes");
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ ControlState_ = EControlState::ExpectName;
+ } else {
+ YsonToUnversionedValueConverter_.OnBeginAttributes();
+ }
+
+ ++Depth_;
+}
+
+void TTableConsumer::ThrowControlAttributesNotSupported() const
+{
+ THROW_ERROR AttachLocationAttributes(TError("Control attributes are not supported"));
+}
+
+void TTableConsumer::ThrowMapExpected() const
+{
+ THROW_ERROR AttachLocationAttributes(TError("Invalid row format, map expected"));
+}
+
+void TTableConsumer::ThrowEntityExpected() const
+{
+ THROW_ERROR AttachLocationAttributes(TError("Invalid control attributes syntax, entity expected"));
+}
+
+void TTableConsumer::ThrowInvalidControlAttribute(const TString& whatsWrong) const
+{
+ THROW_ERROR AttachLocationAttributes(TError("Control attribute %Qlv cannot %v",
+ ControlAttribute_,
+ whatsWrong));
+}
+
+void TTableConsumer::OnListItem()
+{
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ // Row separator, do nothing.
+ } else {
+ YsonToUnversionedValueConverter_.OnListItem();
+ }
+}
+
+void TTableConsumer::OnBeginMap()
+{
+ if (ControlState_ == EControlState::ExpectValue) {
+ YT_ASSERT(Depth_ == 1);
+ ThrowInvalidControlAttribute("be a map");
+ } else if (ControlState_ == EControlState::ExpectEntity) {
+ ThrowEntityExpected();
+ }
+
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ if (Depth_ == 0) {
+ CurrentValueConsumer_->OnBeginRow();
+ } else {
+ YsonToUnversionedValueConverter_.OnBeginMap();
+ }
+ ++Depth_;
+}
+
+void TTableConsumer::OnKeyedItem(TStringBuf name)
+{
+ switch (ControlState_) {
+ case EControlState::None:
+ break;
+
+ case EControlState::ExpectName:
+ YT_ASSERT(Depth_ == 1);
+ try {
+ ControlAttribute_ = ParseEnum<EControlAttribute>(ToString(name));
+ } catch (const std::exception&) {
+ // Ignore ex, our custom message is more meaningful.
+ THROW_ERROR AttachLocationAttributes(TError("Failed to parse control attribute name %Qv", name));
+ }
+ ControlState_ = EControlState::ExpectValue;
+ return;
+
+ case EControlState::ExpectEndAttributes:
+ YT_ASSERT(Depth_ == 1);
+ THROW_ERROR AttachLocationAttributes(TError("Too many control attributes per record: at most one attribute is allowed"));
+
+ default:
+ YT_ABORT();
+ }
+
+ YT_ASSERT(Depth_ > 0);
+ if (Depth_ == 1) {
+ int columnIndex = -1;
+ if (CurrentValueConsumer_->GetAllowUnknownColumns()) {
+ try {
+ columnIndex = CurrentNameTableWriter_->GetIdOrRegisterName(name);
+ } catch (const std::exception& ex) {
+ THROW_ERROR AttachLocationAttributes(TError("Failed to add column to name table for table writer")
+ << ex);
+ }
+ } else {
+ auto id = CurrentNameTableWriter_->FindId(name);
+ if (!id) {
+ THROW_ERROR AttachLocationAttributes(
+ TError(NTableClient::EErrorCode::SchemaViolation, "No column %Qv in table schema",
+ name));
+ }
+ columnIndex = *id;
+ }
+ YT_VERIFY(columnIndex != -1);
+ YsonToUnversionedValueConverter_.SetColumnIndex(columnIndex);
+ } else {
+ YsonToUnversionedValueConverter_.OnKeyedItem(name);
+ }
+}
+
+void TTableConsumer::OnEndMap()
+{
+ YT_ASSERT(Depth_ > 0);
+ // No control attribute allows map or composite values.
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ --Depth_;
+ if (Depth_ == 0) {
+ CurrentValueConsumer_->OnEndRow();
+ ++RowIndex_;
+ } else {
+ YsonToUnversionedValueConverter_.OnEndMap();
+ }
+}
+
+void TTableConsumer::OnEndList()
+{
+ // No control attribute allow list or composite values.
+ YT_ASSERT(ControlState_ == EControlState::None);
+
+ --Depth_;
+ YT_ASSERT(Depth_ > 0);
+
+ YsonToUnversionedValueConverter_.OnEndList();
+}
+
+void TTableConsumer::OnEndAttributes()
+{
+ --Depth_;
+
+ switch (ControlState_) {
+ case EControlState::ExpectName:
+ THROW_ERROR AttachLocationAttributes(TError("Too few control attributes per record: at least one attribute is required"));
+ break;
+
+ case EControlState::ExpectEndAttributes:
+ YT_ASSERT(Depth_ == 0);
+ ControlState_ = EControlState::ExpectEntity;
+ break;
+
+ case EControlState::None:
+ YT_ASSERT(Depth_ > 0);
+ YsonToUnversionedValueConverter_.OnEndAttributes();
+ break;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+void TTableConsumer::SwitchToTable(int tableIndex)
+{
+ YT_VERIFY(tableIndex >= 0 && tableIndex < GetTableCount());
+ CurrentValueConsumer_ = YsonToUnversionedValueConverter_.SwitchToTable(tableIndex);
+ CurrentNameTableWriter_ = NameTableWriters_[tableIndex].get();
+}
+
+int TTableConsumer::GetTableCount() const
+{
+ return NameTableWriters_.size();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/table_consumer.h b/yt/yt/client/table_client/table_consumer.h
new file mode 100644
index 0000000000..9355d695ba
--- /dev/null
+++ b/yt/yt/client/table_client/table_consumer.h
@@ -0,0 +1,159 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/complex_types/yson_format_conversion.h>
+#include <yt/yt/client/formats/public.h>
+
+#include <yt/yt/client/table_client/value_consumer.h>
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonToUnversionedValueConverter
+ : public NYson::TYsonConsumerBase
+{
+public:
+ DEFINE_BYREF_RO_PROPERTY(std::vector<IValueConsumer*>, ValueConsumers);
+
+public:
+ TYsonToUnversionedValueConverter(
+ const NComplexTypes::TYsonConverterConfig& config,
+ IValueConsumer* valueConsumers);
+
+ TYsonToUnversionedValueConverter(
+ const NComplexTypes::TYsonConverterConfig& config,
+ std::vector<IValueConsumer*> valueConsumers,
+ int tableIndex = 0);
+
+ IValueConsumer* SwitchToTable(int tableIndex);
+
+ // Set column index of next emitted value.
+ void SetColumnIndex(int columnIndex);
+
+ int GetDepth() const;
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf name) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndList() override;
+ void OnEndAttributes() override;
+
+private:
+ bool TryConvertAndFeedValueConsumer(TUnversionedValue value);
+
+ TBlobOutput ValueBuffer_;
+ NYson::TBufferedBinaryYsonWriter ValueWriter_;
+
+ // Key of ComplexTypeConverters_ map is <TableIndex;ColumnId>.
+ // All complex columns are present in this map.
+ //
+ // If no conversion is needed then values are empty functions.
+ // Otherwise values are converter functions.
+ THashMap<std::pair<int,int>, NComplexTypes::TYsonClientToServerConverter> ComplexTypeConverters_;
+ THashMap<std::pair<int,int>, NComplexTypes::TYsonClientToServerConverter> SimpleValueConverters_;
+
+ TBlobOutput ConvertedBuffer_;
+ NYson::TBufferedBinaryYsonWriter ConvertedWriter_;
+
+ IValueConsumer* CurrentValueConsumer_;
+ int Depth_ = 0;
+ int ColumnIndex_ = 0;
+ int TableIndex_ = 0;
+
+private:
+ void FlushCurrentValueIfCompleted();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETableConsumerControlState,
+ (None)
+ (ExpectName)
+ (ExpectValue)
+ (ExpectEndAttributes)
+ (ExpectEntity)
+);
+
+class TTableConsumer
+ : public NYson::TYsonConsumerBase
+{
+public:
+ TTableConsumer(
+ const NComplexTypes::TYsonConverterConfig& config,
+ IValueConsumer* consumer);
+ TTableConsumer(
+ const NComplexTypes::TYsonConverterConfig& config,
+ std::vector<IValueConsumer*> consumers,
+ int tableIndex = 0);
+
+protected:
+ using EControlState = ETableConsumerControlState;
+
+ TError AttachLocationAttributes(TError error) const;
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf name) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+
+ void ThrowMapExpected() const;
+ void ThrowEntityExpected() const;
+ void ThrowControlAttributesNotSupported() const;
+ void ThrowInvalidControlAttribute(const TString& whatsWrong) const;
+
+ void OnEndList() override;
+ void OnEndAttributes() override;
+
+ void OnControlInt64Scalar(i64 value);
+ void OnControlStringScalar(TStringBuf value);
+
+ void SwitchToTable(int tableIndex);
+
+private:
+ int GetTableCount() const;
+
+protected:
+ std::vector<std::unique_ptr<TNameTableWriter>> NameTableWriters_;
+
+ IValueConsumer* CurrentValueConsumer_ = nullptr;
+ TNameTableWriter* CurrentNameTableWriter_ = nullptr;
+
+ EControlState ControlState_ = EControlState::None;
+ EControlAttribute ControlAttribute_;
+
+ TYsonToUnversionedValueConverter YsonToUnversionedValueConverter_;
+
+ int Depth_ = 0;
+
+ i64 RowIndex_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/table_output.cpp b/yt/yt/client/table_client/table_output.cpp
new file mode 100644
index 0000000000..f96252f2ca
--- /dev/null
+++ b/yt/yt/client/table_client/table_output.cpp
@@ -0,0 +1,36 @@
+#include "table_output.h"
+
+namespace NYT::NTableClient {
+
+using namespace NFormats;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableOutput::TTableOutput(std::unique_ptr<IParser> parser)
+ : Parser_(std::move(parser))
+{ }
+
+TTableOutput::~TTableOutput() = default;
+
+void TTableOutput::DoWrite(const void* buf, size_t len)
+{
+ YT_VERIFY(ParserValid_);
+ try {
+ Parser_->Read(TStringBuf(static_cast<const char*>(buf), len));
+ } catch (const std::exception& ex) {
+ ParserValid_ = false;
+ throw;
+ }
+}
+
+void TTableOutput::DoFinish()
+{
+ if (ParserValid_) {
+ // Dump everything into consumer.
+ Parser_->Finish();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/table_output.h b/yt/yt/client/table_client/table_output.h
new file mode 100644
index 0000000000..d52c35bd1e
--- /dev/null
+++ b/yt/yt/client/table_client/table_output.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/formats/parser.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableOutput
+ : public IOutputStream
+{
+public:
+ explicit TTableOutput(std::unique_ptr<NFormats::IParser> parser);
+ ~TTableOutput() override;
+
+private:
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFinish() override;
+
+ const std::unique_ptr<NFormats::IParser> Parser_;
+
+ bool ParserValid_ = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unittests/columnar_ut.cpp b/yt/yt/client/table_client/unittests/columnar_ut.cpp
new file mode 100644
index 0000000000..66a5067af7
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/columnar_ut.cpp
@@ -0,0 +1,717 @@
+#include <yt/yt/client/table_client/columnar.h>
+#include <yt/yt/client/table_client/logical_type.h>
+
+#include <yt/yt/client/table_client/unittests/helpers/helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <array>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TBuildValidityBitmapFromDictionaryIndexesWithZeroNullTest, Empty)
+{
+ BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ TRange<ui32>(),
+ TMutableRef());
+}
+
+TEST(TBuildValidityBitmapFromDictionaryIndexesWithZeroNullTest, LessThanByte)
+{
+ std::array<ui32, 6> indexes{0, 0, 1, 3, 4, 0};
+ ui8 result;
+ BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ MakeRange(indexes),
+ TMutableRef(&result, 1));
+ EXPECT_EQ(0x1c, result);
+}
+
+TEST(TBuildValidityBitmapFromDictionaryIndexesWithZeroNullTest, TenBytes)
+{
+ std::array<ui32, 80> indexes;
+ for (int i = 0; i < std::ssize(indexes); ++i) {
+ indexes[i] = i % 2;
+ }
+ std::array<ui8, indexes.size() / 8> result;
+ BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ MakeRange(indexes),
+ TMutableRef(&result, result.size()));
+ for (int i = 0; i < std::ssize(result); ++i) {
+ EXPECT_EQ(0xaa, result[i]);
+ }
+}
+
+TEST(TBuildValidityBitmapFromDictionaryIndexesWithZeroNullTest, ManyBits)
+{
+ std::array<ui32, 8001> indexes;
+ for (int i = 0; i < std::ssize(indexes); ++i) {
+ indexes[i] = i % 2;
+ }
+ std::array<ui8, indexes.size() / 8 + 1> result;
+ BuildValidityBitmapFromDictionaryIndexesWithZeroNull(
+ MakeRange(indexes),
+ TMutableRef(&result, result.size()));
+ for (int i = 0; i < std::ssize(result) - 1; ++i) {
+ EXPECT_EQ(0xaa, result[i]);
+ }
+ EXPECT_EQ(0, result[result.size() - 1]);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TBuildDictionaryIndexesFromDictionaryIndexesWithZeroNullTest, Empty)
+{
+ BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull(
+ TRange<ui32>(),
+ TMutableRange<ui32>());
+}
+
+TEST(TBuildDictionaryIndexesFromDictionaryIndexesWithZeroNullTest, NonEmpty)
+{
+ std::array<ui32, 6> indexes{0, 1, 2, 3, 4, 5};
+ std::array<ui32, indexes.size()> result;
+ BuildDictionaryIndexesFromDictionaryIndexesWithZeroNull(
+ MakeRange(indexes),
+ MakeMutableRange(result));
+ for (int i = 1; i < std::ssize(result); ++i) {
+ EXPECT_EQ(indexes[i] - 1, result[i]);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCountNullsInDictionaryIndexesWithZeroNullTest, Empty)
+{
+ EXPECT_EQ(0, CountNullsInDictionaryIndexesWithZeroNull(TRange<ui32>()));
+}
+
+TEST(TCountNullsInDictionaryIndexesWithZeroNullTest, NonEmpty)
+{
+ std::array<ui32, 6> indexes{0, 1, 2, 0, 4, 5};
+ EXPECT_EQ(2, CountNullsInDictionaryIndexesWithZeroNull(MakeRange(indexes)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCountOnesInBitmapTest, Empty)
+{
+ EXPECT_EQ(0, CountOnesInBitmap(TRef(), 0, 0));
+}
+
+TEST(TCountOnesInBitmapTest, SingleByte)
+{
+ ui8 byte = 0xFF;
+ for (int i = 0; i < 8; ++i) {
+ for (int j = i; j < 8; ++j) {
+ EXPECT_EQ(j - i, CountOnesInBitmap(TRef(&byte, 1), i, j))
+ << "i = " << i << " j = " << j;
+ }
+ }
+}
+
+TEST(TCountOnesInBitmapTest, SevenBytes)
+{
+ std::array<ui8, 7> bytes = {0, 0xff, 0xff, 0xff, 0, 0, 1};
+ auto bitmap = TRef(bytes.data(), bytes.size());
+ EXPECT_EQ(25, CountOnesInBitmap(bitmap, 0, 56));
+}
+
+TEST(TCountOnesInBitmapTest, ThreeQwords)
+{
+ std::array<ui64, 3> qwords = {1, 1, 1};
+ auto bitmap = TRef(qwords.data(), 8 * qwords.size());
+ EXPECT_EQ(3, CountOnesInBitmap(bitmap, 0, 64 * 3));
+}
+
+TEST(TCountOnesInBitmapTest, QwordBorder)
+{
+ std::array<ui64, 2> qwords = {0xffffffffffffffffULL, 0xffffffffffffffffULL};
+ auto bitmap = TRef(qwords.data(), 8 * qwords.size());
+ for (int i = 0; i < 10; ++i) {
+ for (int j = 64; j < 74; ++j) {
+ EXPECT_EQ(j - i, CountOnesInBitmap(bitmap, i, j))
+ << "i = " << i << " j = " << j;
+ }
+ }
+}
+
+TEST(TCountOnesInBitmapTest, WholeQwordInTheMiddle)
+{
+ std::array<ui64, 3> qwords = {0xffffffffffffffffULL, 1, 0xffffffffffffffffULL};
+ auto bitmap = TRef(qwords.data(), 8 * qwords.size());
+ for (int i = 0; i < 10; ++i) {
+ for (int j = 128; j < 138; ++j) {
+ EXPECT_EQ(j - i - 63, CountOnesInBitmap(bitmap, i, j))
+ << "i = " << i << " j = " << j;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCopyBitmapRangeToBitmapTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{ };
+
+TEST_P(TCopyBitmapRangeToBitmapTest, CheckAll)
+{
+ auto [startIndex, endIndex] = GetParam();
+
+ std::array<ui64, 3> qwords = {0x1234567812345678ULL, 0x1234567812345678ULL, 0xabcdabcdabcdabcdULL};
+ auto bitmap = TRef(qwords.data(), 8 * qwords.size());
+
+ constexpr ssize_t ResultByteSize = 24;
+ auto byteCount = GetBitmapByteSize(endIndex - startIndex);
+ EXPECT_LE(byteCount, ResultByteSize);
+
+ std::array<ui8, ResultByteSize> guard;
+ for (int i = 0; i < ResultByteSize; ++i) {
+ guard[i] = i;
+ }
+
+ auto result = guard;
+ auto resultNegated = guard;
+
+ CopyBitmapRangeToBitmap(bitmap, startIndex, endIndex, TMutableRef(result.data(), byteCount));
+ CopyBitmapRangeToBitmapNegated(bitmap, startIndex, endIndex, TMutableRef(resultNegated.data(), byteCount));
+
+ auto getBit = [] (const auto& array, int index) {
+ return (reinterpret_cast<const char*>(array.begin())[index / 8] & (1U << (index % 8))) != 0;
+ };
+
+ for (int i = startIndex; i < endIndex; ++i) {
+ auto srcBit = getBit(bitmap, i);
+ auto dstBit = getBit(result, i - startIndex);
+ auto invertedDstBit = getBit(resultNegated, i - startIndex);
+ EXPECT_EQ(srcBit, dstBit) << "i = " << i;
+ EXPECT_NE(srcBit, invertedDstBit) << "i = " << i;
+ }
+
+ for (int i = byteCount; i < ResultByteSize; ++i) {
+ EXPECT_EQ(guard[i], result[i]);
+ EXPECT_EQ(guard[i], resultNegated[i]);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TCopyBitmapRangeToBitmapTest,
+ TCopyBitmapRangeToBitmapTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 0, 64),
+ std::make_tuple( 0, 192),
+ std::make_tuple( 64, 128),
+ std::make_tuple( 8, 16),
+ std::make_tuple( 10, 13),
+ std::make_tuple( 5, 120),
+ std::make_tuple( 23, 67),
+ std::make_tuple( 1, 191)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTranslateRleIndexTest, CheckAll)
+{
+ std::array<ui64, 8> rleIndexes{0, 1, 3, 10, 11, 15, 20, 21};
+ for (ui64 i = 0; i < rleIndexes[rleIndexes.size() - 1] + 10; ++i) {
+ auto j = TranslateRleIndex(MakeRange(rleIndexes), i);
+ EXPECT_LE(rleIndexes[j], i);
+ if (j < std::ssize(rleIndexes) - 1) {
+ EXPECT_LT(i, rleIndexes[j + 1]);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecodeRleVectorTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int, std::vector<int>>>
+{ };
+
+TEST_P(TDecodeRleVectorTest, CheckAll)
+{
+ std::vector<int> values{1, 2, 3, 4, 5, 6};
+ std::vector<ui64> rleIndexes{0, 1, 10, 11, 20, 25};
+ const auto& [startIndex, endIndex, expected] = GetParam();
+ std::vector<int> decoded;
+ DecodeRawVector<int>(
+ startIndex,
+ endIndex,
+ {},
+ rleIndexes,
+ [&] (auto index) {
+ return values[index];
+ },
+ [&] (auto value) {
+ decoded.push_back(value);
+ });
+ EXPECT_EQ(expected, decoded);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TDecodeRleVectorTest,
+ TDecodeRleVectorTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0, std::vector<int>{}),
+ std::make_tuple( 0, 1, std::vector<int>{1}),
+ std::make_tuple( 0, 5, std::vector<int>{1, 2, 2, 2, 2}),
+ std::make_tuple( 9, 13, std::vector<int>{2, 3, 4, 4}),
+ std::make_tuple( 20, 25, std::vector<int>{5, 5, 5, 5, 5}),
+ std::make_tuple( 25, 27, std::vector<int>{6, 6}),
+ std::make_tuple( 50, 53, std::vector<int>{6, 6, 6})));
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDecodeIntegerValueTest, CheckAll)
+{
+ EXPECT_EQ(100, DecodeIntegerValue<int>(100, 0, false));
+ EXPECT_EQ(100, DecodeIntegerValue<int>( 40, 60, false));
+ EXPECT_EQ( 9, DecodeIntegerValue<int>( 18, 0, true));
+ EXPECT_EQ(-10, DecodeIntegerValue<int>( 19, 0, true));
+ EXPECT_EQ( 9, DecodeIntegerValue<int>( 1, 17, true));
+ EXPECT_EQ(-10, DecodeIntegerValue<int>( 2, 17, true));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecodeStringsTest
+ : public ::testing::Test
+{
+protected:
+ std::vector<ui32> Offsets = {1, 2, 3, 4, 5};
+ static constexpr ui32 AvgLength = 10;
+ std::vector<ui32> Expected = {0, 9, 21, 28, 42, 47};
+};
+
+TEST_F(TDecodeStringsTest, Single)
+{
+ std::vector<ui32> result;
+ for (int index = 0; index <= std::ssize(Offsets); ++index) {
+ result.push_back(DecodeStringOffset(MakeRange(Offsets), AvgLength, index));
+ }
+ EXPECT_EQ(Expected, result);
+}
+
+TEST_F(TDecodeStringsTest, Multiple)
+{
+ for (int i = 0; i <= std::ssize(Offsets); ++i) {
+ for (int j = i; j <= std::ssize(Offsets); ++j) {
+ std::vector<ui32> result(j - i + 1);
+ DecodeStringOffsets(MakeRange(Offsets), AvgLength, i, j, MakeMutableRange(result));
+ std::vector<ui32> expected;
+ for (int k = i; k <= j; ++k) {
+ expected.push_back(Expected[k] - DecodeStringOffset(MakeRange(Offsets), AvgLength, i));
+ }
+ EXPECT_EQ(expected, result);
+ }
+ }
+}
+
+TEST_F(TDecodeStringsTest, PointersAndLengths)
+{
+ std::vector<char> chars(100);
+ std::vector<const char*> strings(Offsets.size());
+ std::vector<i32> lengths(Offsets.size());
+ DecodeStringPointersAndLengths(
+ MakeRange(Offsets),
+ AvgLength,
+ TRef(chars.data(), chars.size()),
+ MakeMutableRange(strings),
+ MakeMutableRange(lengths));
+ for (int i = 0; i < std::ssize(Offsets); ++i) {
+ EXPECT_EQ(Expected[i], strings[i] - chars.data());
+ EXPECT_EQ(static_cast<ssize_t>(Expected[i + 1] - Expected[i]), lengths[i]);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecodeNullsFromRleDictionaryIndexesWithZeroNullTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{
+protected:
+ void SetUp() override
+ {
+ Expected.resize(800);
+ std::fill(Expected.begin() + 3, Expected.begin() + 5, true);
+ std::fill(Expected.begin() + 20, Expected.begin() + 100, true);
+ std::fill(Expected.begin() + 200, Expected.begin() + 800, true);
+ }
+
+
+ std::vector<ui32> DictionaryIndexes = {0, 1, 0, 1, 0, 1};
+ std::vector<ui64> RleIndexes = {0, 3, 5, 20, 100, 200};
+ std::vector<bool> Expected;
+};
+
+TEST_P(TDecodeNullsFromRleDictionaryIndexesWithZeroNullTest, ValidityBitmap)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui8> bitmap(GetBitmapByteSize(endIndex - startIndex));
+ BuildValidityBitmapFromRleDictionaryIndexesWithZeroNull(
+ MakeRange(DictionaryIndexes),
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ TMutableRef(bitmap.data(), bitmap.size()));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_EQ(Expected[i], GetBit(TRef(bitmap.data(), bitmap.size()), i - startIndex))
+ << "i = " << i;
+ }
+}
+
+TEST_P(TDecodeNullsFromRleDictionaryIndexesWithZeroNullTest, NullBytemap)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui8> bytemap(endIndex - startIndex);
+ BuildNullBytemapFromRleDictionaryIndexesWithZeroNull(
+ MakeRange(DictionaryIndexes),
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ MakeMutableRange(bytemap));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_TRUE(bytemap[i - startIndex] == 0 || bytemap[i - startIndex] == 1);
+ EXPECT_EQ(Expected[i], bytemap[i - startIndex] == 0)
+ << "i = " << i;
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TDecodeNullsFromRleDictionaryIndexesWithZeroNullTest,
+ TDecodeNullsFromRleDictionaryIndexesWithZeroNullTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 0, 800),
+ std::make_tuple(256, 512),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 20, 100),
+ std::make_tuple( 90, 110)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNullTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{
+protected:
+ std::vector<ui32> DictionaryIndexes = {0, 1, 0, 2, 3};
+ std::vector<ui64> RleIndexes = {0, 3, 5, 10, 12};
+
+ static constexpr ui32 Z = static_cast<ui32>(-1);
+ std::vector<ui32> Expected = {Z, Z, Z, 0, 0, Z, Z, Z, Z, Z, 1, 1, 2, 2, 2};
+};
+
+TEST_P(TBuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNullTest, CheckAll)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui32> result(endIndex - startIndex);
+ BuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNull(
+ MakeRange(DictionaryIndexes),
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ MakeMutableRange(result));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_EQ(Expected[i], result[i - startIndex])
+ << "i = " << i;
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TBuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNullTest,
+ TBuildDictionaryIndexesFromRleDictionaryIndexesWithZeroNullTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 0, 15),
+ std::make_tuple( 3, 5),
+ std::make_tuple( 1, 10),
+ std::make_tuple( 13, 15)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBuildIotaDictionaryIndexesFromRleIndexesTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int, std::vector<ui32>>>
+{
+protected:
+ std::vector<ui64> RleIndexes = {0, 3, 5, 10, 12};
+};
+
+TEST_P(TBuildIotaDictionaryIndexesFromRleIndexesTest, CheckAll)
+{
+ auto [startIndex, endIndex, expected] = GetParam();
+ std::vector<ui32> result(endIndex - startIndex);
+ BuildIotaDictionaryIndexesFromRleIndexes(
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ MakeMutableRange(result));
+ EXPECT_EQ(expected, result);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TBuildIotaDictionaryIndexesFromRleIndexesTest,
+ TBuildIotaDictionaryIndexesFromRleIndexesTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0, std::vector<ui32>{}),
+ std::make_tuple( 0, 15, std::vector<ui32>{0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 4, 4}),
+ std::make_tuple( 3, 5, std::vector<ui32>{0, 0}),
+ std::make_tuple( 1, 10, std::vector<ui32>{0, 0, 1, 1, 2, 2, 2, 2, 2}),
+ std::make_tuple( 13, 15, std::vector<ui32>{0, 0})));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCountNullsInDictionaryIndexesWithZeroNullTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<std::vector<ui32>, int>>
+{ };
+
+TEST_P(TCountNullsInDictionaryIndexesWithZeroNullTest, CheckAll)
+{
+ const auto& [indexes, expected] = GetParam();
+ EXPECT_EQ(expected, CountNullsInDictionaryIndexesWithZeroNull(MakeRange(indexes)));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TCountNullsInDictionaryIndexesWithZeroNullTest,
+ TCountNullsInDictionaryIndexesWithZeroNullTest,
+ ::testing::Values(
+ std::make_tuple(std::vector<ui32>{}, 0),
+ std::make_tuple(std::vector<ui32>{0, 0, 0}, 3),
+ std::make_tuple(std::vector<ui32>{1, 2, 3}, 0),
+ std::make_tuple(std::vector<ui32>{1, 0, 3}, 1)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCountOnesInRleBitmapTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int, int>>
+{
+protected:
+ std::vector<ui64> RleIndexes = {0, 3, 5, 20, 50};
+ ui8 Bitmap = 0b10101;
+};
+
+TEST_P(TCountOnesInRleBitmapTest, CheckAll)
+{
+ auto [startIndex, endIndex, expected] = GetParam();
+ EXPECT_EQ(expected, CountOnesInRleBitmap(TRef(&Bitmap, 1), MakeRange(RleIndexes), startIndex, endIndex));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TCountOnesInRleBitmapTest,
+ TCountOnesInRleBitmapTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0, 0),
+ std::make_tuple( 50, 60, 10),
+ std::make_tuple( 40, 60, 10),
+ std::make_tuple( 60, 100, 40),
+ std::make_tuple( 3, 5, 0),
+ std::make_tuple( 2, 6, 2)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecodeNullsFromRleNullBitmapTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{
+protected:
+ void SetUp() override
+ {
+ Expected.resize(800);
+ std::fill(Expected.begin() + 3, Expected.begin() + 5, true);
+ std::fill(Expected.begin() + 20, Expected.begin() + 100, true);
+ std::fill(Expected.begin() + 200, Expected.begin() + 800, true);
+ }
+
+
+ std::vector<ui64> RleIndexes = {0, 3, 5, 20, 100, 200};
+ ui8 Bitmap = 0b010101;
+ std::vector<bool> Expected;
+};
+
+TEST_P(TDecodeNullsFromRleNullBitmapTest, ValidityBitmap)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui8> bitmap(GetBitmapByteSize(endIndex - startIndex));
+ BuildValidityBitmapFromRleNullBitmap(
+ TRef(&Bitmap, 1),
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ TMutableRef(bitmap.data(), bitmap.size()));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_EQ(Expected[i], GetBit(TRef(bitmap.data(), bitmap.size()), i - startIndex))
+ << "i = " << i;
+ }
+}
+
+TEST_P(TDecodeNullsFromRleNullBitmapTest, NullBytemap)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui8> bytemap(endIndex - startIndex);
+ BuildNullBytemapFromRleNullBitmap(
+ TRef(&Bitmap, 1),
+ MakeRange(RleIndexes),
+ startIndex,
+ endIndex,
+ MakeMutableRange(bytemap));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_TRUE(bytemap[i - startIndex] == 0 || bytemap[i - startIndex] == 1);
+ EXPECT_EQ(Expected[i], bytemap[i - startIndex] == 0)
+ << "i = " << i;
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TDecodeNullsFromRleNullBitmapTest,
+ TDecodeNullsFromRleNullBitmapTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 0, 800),
+ std::make_tuple(256, 512),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 20, 100),
+ std::make_tuple( 90, 110)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDecodeBytemapFromBitmapTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{
+protected:
+ void SetUp() override
+ {
+ Expected.resize(800);
+ std::fill(Expected.begin() + 3, Expected.begin() + 5, true);
+ std::fill(Expected.begin() + 20, Expected.begin() + 100, true);
+ std::fill(Expected.begin() + 200, Expected.begin() + 800, true);
+
+ Bitmap.resize(GetBitmapByteSize(Expected.size()));
+ for (int i = 0; i < std::ssize(Expected); ++i) {
+ SetBit(TMutableRef(Bitmap.data(), Bitmap.size()), i, Expected[i]);
+ }
+ }
+
+ std::vector<bool> Expected;
+ std::vector<ui8> Bitmap;
+};
+
+TEST_P(TDecodeBytemapFromBitmapTest, CheckAll)
+{
+ auto [startIndex, endIndex] = GetParam();
+ std::vector<ui8> bytemap(endIndex - startIndex);
+ DecodeBytemapFromBitmap(
+ TRef(Bitmap.data(), Bitmap.size()),
+ startIndex,
+ endIndex,
+ MakeMutableRange(bytemap));
+ for (int i = startIndex; i < endIndex; ++i) {
+ EXPECT_TRUE(bytemap[i - startIndex] == 0 || bytemap[i - startIndex] == 1);
+ EXPECT_EQ(Expected[i], bytemap[i - startIndex])
+ << "i = " << i;
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TDecodeBytemapFromBitmapTest,
+ TDecodeBytemapFromBitmapTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 18, 19),
+ std::make_tuple( 0, 512),
+ std::make_tuple( 0, 800),
+ std::make_tuple(256, 512),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 10, 20),
+ std::make_tuple( 20, 100),
+ std::make_tuple( 90, 110)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCountTotalStringLengthInRleDictionaryIndexesWithZeroNullTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int>>
+{
+protected:
+ void SetUp() override
+ {
+ int j = -1;
+ for (int i = 0; i < 40; ++i) {
+ if (j + 1 < std::ssize(RleIndexes) && i == static_cast<ssize_t>(RleIndexes[j + 1])) {
+ ++j;
+ }
+ auto dictionaryIndex = DictionaryIndexes[j];
+ if (dictionaryIndex != 0) {
+ auto stringIndex = dictionaryIndex - 1;
+ DecodedOffsets.push_back(DecodeStringOffset(Offsets, AvgLength, stringIndex));
+ } else {
+ DecodedOffsets.push_back(DecodedOffsets.empty() ? 0 : DecodedOffsets.back());
+ }
+ }
+
+ for (int i = 0; i < std::ssize(Offsets); ++i) {
+ auto [startOffset, endOffset] = DecodeStringRange(
+ MakeRange(Offsets),
+ AvgLength,
+ i);
+ Lengths.push_back(endOffset - startOffset);
+ }
+ }
+
+ std::vector<ui64> RleIndexes = {0, 1, 3, 10, 15, 16, 18};
+ std::vector<ui32> DictionaryIndexes = {0, 1, 0, 2, 3, 4, 5};
+ std::vector<ui32> Offsets = {1, 2, 3, 4, 5};
+ static constexpr ui32 AvgLength = 10;
+ std::vector<i32> Lengths;
+ std::vector<i64> DecodedOffsets;
+};
+
+TEST_P(TCountTotalStringLengthInRleDictionaryIndexesWithZeroNullTest, CheckAll)
+{
+ auto [startIndex, endIndex] = GetParam();
+
+ auto actual = CountTotalStringLengthInRleDictionaryIndexesWithZeroNull(
+ MakeRange(DictionaryIndexes),
+ MakeRange(RleIndexes),
+ MakeRange(Lengths),
+ startIndex,
+ endIndex);
+
+ i64 expected = 0;
+ for (int i = startIndex; i < endIndex; ++i) {
+ int j = TranslateRleIndex(MakeRange(RleIndexes), i);
+ auto k = DictionaryIndexes[j];
+ if (k != 0) {
+ auto [startOffset, endOffset] = DecodeStringRange(MakeRange(Offsets), AvgLength, k - 1);
+ expected += (endOffset - startOffset);
+ }
+ }
+
+ EXPECT_EQ(expected, actual);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TCountTotalStringLengthInRleDictionaryIndexesWithZeroNullTest,
+ TCountTotalStringLengthInRleDictionaryIndexesWithZeroNullTest,
+ ::testing::Values(
+ std::make_tuple( 0, 0),
+ std::make_tuple( 0, 30),
+ std::make_tuple( 1, 3),
+ std::make_tuple( 5, 10),
+ std::make_tuple( 4, 25),
+ std::make_tuple( 2, 4)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unittests/helpers/helpers.cpp b/yt/yt/client/table_client/unittests/helpers/helpers.cpp
new file mode 100644
index 0000000000..eadbab1083
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/helpers/helpers.cpp
@@ -0,0 +1,260 @@
+#include "helpers.h"
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/versioned_reader.h>
+
+namespace NYT::NTableClient {
+
+using namespace NChunkClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckEqual(const TUnversionedValue& expected, const TUnversionedValue& actual)
+{
+ // Fast path.
+ if (TBitwiseUnversionedValueEqual()(expected, actual)) {
+ return;
+ }
+
+ SCOPED_TRACE(Format("Expected: %v; Actual: %v", expected, actual));
+ ASSERT_TRUE(TBitwiseUnversionedValueEqual()(expected, actual));
+}
+
+void CheckEqual(const TVersionedValue& expected, const TVersionedValue& actual)
+{
+ // Fast path.
+ if (TBitwiseVersionedValueEqual()(expected, actual)) {
+ return;
+ }
+
+ SCOPED_TRACE(Format("Expected: %v; Actual: %v", expected, actual));
+ ASSERT_TRUE(TBitwiseVersionedValueEqual()(expected, actual));
+}
+
+void ExpectSchemafulRowsEqual(TUnversionedRow expected, TUnversionedRow actual)
+{
+ // Fast path.
+ if (TBitwiseUnversionedRowEqual()(expected, actual)) {
+ return;
+ }
+
+ SCOPED_TRACE(Format("Expected: %v; Actual: %v", expected, actual));
+
+ ASSERT_EQ(static_cast<bool>(expected), static_cast<bool>(actual));
+ if (!expected || !actual) {
+ return;
+ }
+ ASSERT_EQ(expected.GetCount(), actual.GetCount());
+
+ for (int valueIndex = 0; valueIndex < static_cast<int>(expected.GetCount()); ++valueIndex) {
+ SCOPED_TRACE(Format("Value index %v", valueIndex));
+ CheckEqual(expected[valueIndex], actual[valueIndex]);
+ }
+}
+
+void ExpectSchemalessRowsEqual(TUnversionedRow expected, TUnversionedRow actual, int keyColumnCount)
+{
+ // Fast path.
+ if (TBitwiseUnversionedRowEqual()(expected, actual)) {
+ return;
+ }
+
+ SCOPED_TRACE(Format("Expected: %v; Actual: %v", expected, actual));
+
+ ASSERT_EQ(static_cast<bool>(expected), static_cast<bool>(actual));
+ if (!expected || !actual) {
+ return;
+ }
+ ASSERT_EQ(expected.GetCount(), actual.GetCount());
+
+ for (int valueIndex = 0; valueIndex < keyColumnCount; ++valueIndex) {
+ SCOPED_TRACE(Format("Value index %v", valueIndex));
+ CheckEqual(expected[valueIndex], actual[valueIndex]);
+ }
+
+ for (int valueIndex = keyColumnCount; valueIndex < static_cast<int>(expected.GetCount()); ++valueIndex) {
+ SCOPED_TRACE(Format("Value index %v", valueIndex));
+
+ // Find value with the same id. Since this in schemaless read, value positions can be different.
+ bool found = false;
+ for (int index = keyColumnCount; index < static_cast<int>(expected.GetCount()); ++index) {
+ if (expected[valueIndex].Id == actual[index].Id) {
+ CheckEqual(expected[valueIndex], actual[index]);
+ found = true;
+ break;
+ }
+ }
+ ASSERT_TRUE(found);
+ }
+}
+
+void ExpectSchemafulRowsEqual(TVersionedRow expected, TVersionedRow actual)
+{
+ // Fast path.
+ if (TBitwiseVersionedRowEqual()(expected, actual)) {
+ return;
+ }
+
+ SCOPED_TRACE(Format("Expected: %v; Actual: %v", expected, actual));
+
+ ASSERT_EQ(static_cast<bool>(expected), static_cast<bool>(actual));
+ if (!expected || !actual) {
+ return;
+ }
+
+ ASSERT_EQ(expected.GetWriteTimestampCount(), actual.GetWriteTimestampCount());
+ for (int i = 0; i < expected.GetWriteTimestampCount(); ++i) {
+ SCOPED_TRACE(Format("Write Timestamp %v", i));
+ ASSERT_EQ(expected.WriteTimestamps()[i], actual.WriteTimestamps()[i]);
+ }
+
+ ASSERT_EQ(expected.GetDeleteTimestampCount(), actual.GetDeleteTimestampCount());
+ for (int i = 0; i < expected.GetDeleteTimestampCount(); ++i) {
+ SCOPED_TRACE(Format("Delete Timestamp %v", i));
+ ASSERT_EQ(expected.DeleteTimestamps()[i], actual.DeleteTimestamps()[i]);
+ }
+
+ ASSERT_EQ(expected.GetKeyCount(), actual.GetKeyCount());
+ for (int index = 0; index < expected.GetKeyCount(); ++index) {
+ SCOPED_TRACE(Format("Key index %v", index));
+ CheckEqual(expected.Keys()[index], actual.Keys()[index]);
+ }
+
+ ASSERT_EQ(expected.GetValueCount(), actual.GetValueCount());
+ for (int index = 0; index < expected.GetValueCount(); ++index) {
+ SCOPED_TRACE(Format("Value index %v", index));
+ CheckEqual(expected.Values()[index], actual.Values()[index]);
+ }
+}
+
+void CheckResult(std::vector<TVersionedRow> expected, IVersionedReaderPtr reader, bool filterOutNullRows)
+{
+ auto it = expected.begin();
+ std::vector<TVersionedRow> actual;
+ actual.reserve(100);
+
+ while (auto batch = reader->Read({.MaxRowsPerRead = 100})) {
+ if (batch->IsEmpty()) {
+ ASSERT_TRUE(reader->GetReadyEvent().Get().IsOK());
+ continue;
+ }
+
+ auto range = batch->MaterializeRows();
+ std::vector<TVersionedRow> actual(range.begin(), range.end());
+
+ if (filterOutNullRows) {
+ actual.erase(
+ std::remove_if(
+ actual.begin(),
+ actual.end(),
+ [] (TVersionedRow row) {
+ return !row;
+ }),
+ actual.end());
+ }
+
+ std::vector<TVersionedRow> ex(it, std::min(it + actual.size(), expected.end()));
+
+ CheckSchemafulResult(ex, actual);
+ it += ex.size();
+ }
+
+ ASSERT_TRUE(it == expected.end());
+}
+
+std::vector<std::pair<ui32, ui32>> GetTimestampIndexRanges(
+ TRange<TVersionedRow> rows,
+ TTimestamp timestamp)
+{
+ std::vector<std::pair<ui32, ui32>> indexRanges;
+ for (auto row : rows) {
+ // Find delete timestamp.
+ auto deleteTimestamp = NTableClient::NullTimestamp;
+ for (auto currentTimestamp : row.DeleteTimestamps()) {
+ if (currentTimestamp <= timestamp) {
+ deleteTimestamp = std::max(currentTimestamp, deleteTimestamp);
+ }
+ }
+
+ int lowerTimestampIndex = 0;
+ while (lowerTimestampIndex < row.GetWriteTimestampCount() &&
+ row.WriteTimestamps()[lowerTimestampIndex] > timestamp)
+ {
+ ++lowerTimestampIndex;
+ }
+
+ int upperTimestampIndex = lowerTimestampIndex;
+ while (upperTimestampIndex < row.GetWriteTimestampCount() &&
+ row.WriteTimestamps()[upperTimestampIndex] > deleteTimestamp)
+ {
+ ++upperTimestampIndex;
+ }
+
+ indexRanges.push_back(std::make_pair(lowerTimestampIndex, upperTimestampIndex));
+ }
+ return indexRanges;
+}
+
+std::vector<TUnversionedRow> CreateFilteredRangedRows(
+ const std::vector<TUnversionedRow>& initial,
+ TNameTablePtr writeNameTable,
+ TNameTablePtr readNameTable,
+ TColumnFilter columnFilter,
+ TLegacyReadRange readRange,
+ TChunkedMemoryPool* pool,
+ int keyColumnCount)
+{
+ std::vector<TUnversionedRow> rows;
+
+ int lowerRowIndex = 0;
+ if (readRange.LowerLimit().HasRowIndex()) {
+ lowerRowIndex = readRange.LowerLimit().GetRowIndex();
+ }
+
+ int upperRowIndex = initial.size();
+ if (readRange.UpperLimit().HasRowIndex()) {
+ upperRowIndex = readRange.UpperLimit().GetRowIndex();
+ }
+
+ auto fulfillLowerKeyLimit = [&] (TUnversionedRow row) {
+ return !readRange.LowerLimit().HasLegacyKey() ||
+ CompareValueRanges(
+ row.FirstNElements(keyColumnCount),
+ readRange.LowerLimit().GetLegacyKey().Elements()) >= 0;
+ };
+
+ auto fulfillUpperKeyLimit = [&] (TUnversionedRow row) {
+ return !readRange.UpperLimit().HasLegacyKey() ||
+ CompareValueRanges(
+ row.FirstNElements(keyColumnCount),
+ readRange.UpperLimit().GetLegacyKey().Elements()) < 0;
+ };
+
+ for (int rowIndex = lowerRowIndex; rowIndex < upperRowIndex; ++rowIndex) {
+ auto initialRow = initial[rowIndex];
+ if (fulfillLowerKeyLimit(initialRow) && fulfillUpperKeyLimit(initialRow)) {
+ auto row = TMutableUnversionedRow::Allocate(pool, initialRow.GetCount());
+ int count = 0;
+ for (const auto* it = initialRow.Begin(); it != initialRow.End(); ++it) {
+ auto name = writeNameTable->GetName(it->Id);
+ auto readerId = readNameTable->GetId(name);
+
+ if (columnFilter.ContainsIndex(readerId)) {
+ row[count] = *it;
+ row[count].Id = readerId;
+ ++count;
+ }
+ }
+ row.SetCount(count);
+ rows.push_back(row);
+ }
+ }
+
+ return rows;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unittests/helpers/helpers.h b/yt/yt/client/table_client/unittests/helpers/helpers.h
new file mode 100644
index 0000000000..e0d9cfa5f3
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/helpers/helpers.h
@@ -0,0 +1,231 @@
+#pragma once
+
+#include <yt/yt/core/test_framework/framework.h>
+
+
+#include <yt/yt/client/table_client/columnar.h>
+#include <yt/yt/client/table_client/config.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/row_batch.h>
+#include <yt/yt/client/table_client/versioned_row.h>
+
+#include <iostream>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ExpectSchemafulRowsEqual(TUnversionedRow expected, TUnversionedRow actual);
+
+void ExpectSchemalessRowsEqual(TUnversionedRow expected, TUnversionedRow actual, int keyColumnCount);
+
+void ExpectSchemafulRowsEqual(TVersionedRow expected, TVersionedRow actual);
+
+void CheckResult(std::vector<TVersionedRow> expected, IVersionedReaderPtr reader, bool filterOutNullRows = false);
+
+template <class TExpectedRow, class TActualRow>
+void CheckSchemafulResult(const std::vector<TExpectedRow>& expected, const std::vector<TActualRow>& actual)
+{
+ ASSERT_EQ(expected.size(), actual.size());
+ for (int i = 0; i < std::ssize(expected); ++i) {
+ ExpectSchemafulRowsEqual(expected[i], actual[i]);
+ }
+}
+
+template <class TExpectedRow, class TActualRow>
+void CheckSchemalessResult(
+ TRange<TExpectedRow> expected,
+ TRange<TActualRow> actual,
+ int keyColumnCount)
+{
+ ASSERT_EQ(expected.size(), actual.size());
+ for (int i = 0; i < std::ssize(expected); ++i) {
+ ExpectSchemalessRowsEqual(expected[i], actual[i], keyColumnCount);
+ }
+}
+
+template <class TRow, class TReader>
+void CheckSchemalessResult(
+ const std::vector<TRow>& expected,
+ TIntrusivePtr<TReader> reader,
+ int keyColumnCount,
+ TRowBatchReadOptions options = {})
+{
+ size_t offset = 0;
+ while (auto batch = reader->Read(options)) {
+ auto actual = batch->MaterializeRows();
+ if (actual.empty()) {
+ ASSERT_TRUE(reader->GetReadyEvent().Get().IsOK());
+ continue;
+ }
+
+ i64 dataWeight = 0;
+ for (int i = 0; i < std::ssize(actual) - 1; i++) {
+ dataWeight += GetDataWeight(actual[i]);
+ }
+
+ ASSERT_LT(dataWeight, options.MaxDataWeightPerRead);
+ ASSERT_LE(std::ssize(actual), options.MaxRowsPerRead);
+
+ CheckSchemalessResult(
+ MakeRange(expected).Slice(offset, std::min(expected.size(), offset + actual.size())),
+ actual,
+ keyColumnCount);
+ offset += actual.size();
+ }
+
+ ASSERT_EQ(offset, expected.size());
+}
+
+std::vector<std::pair<ui32, ui32>> GetTimestampIndexRanges(
+ TRange<NTableClient::TVersionedRow> rows,
+ NTableClient::TTimestamp timestamp);
+
+template <class T>
+void AppendVector(std::vector<T>* data, const std::vector<T> toAppend)
+{
+ data->insert(data->end(), toAppend.begin(), toAppend.end());
+}
+
+template <class T>
+TRange<T> GetTypedData(const NTableClient::IUnversionedColumnarRowBatch::TValueBuffer& buffer)
+{
+ return MakeRange(
+ reinterpret_cast<const T*>(buffer.Data.Begin()),
+ reinterpret_cast<const T*>(buffer.Data.End()));
+}
+
+inline bool GetBit(const NTableClient::IUnversionedColumnarRowBatch::TValueBuffer& buffer, int index)
+{
+ return (buffer.Data[index / 8] & (1 << (index % 8))) != 0;
+}
+
+inline bool GetBit(TRef data, int index)
+{
+ return (data[index / 8] & (1 << (index % 8))) != 0;
+}
+
+inline bool GetBit(const NTableClient::IUnversionedColumnarRowBatch::TBitmap& bitmap, int index)
+{
+ return GetBit(bitmap.Data, index);
+}
+
+inline void SetBit(TMutableRef data, int index, bool value)
+{
+ auto& byte = data[index / 8];
+ auto mask = 1ULL << (index % 8);
+ if (value) {
+ byte |= mask;
+ } else {
+ byte &= ~mask;
+ }
+}
+
+inline void ResolveRleEncoding(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn*& column,
+ i64& index)
+{
+ if (!column->Rle) {
+ return;
+ }
+
+ YT_ASSERT(column->Values->BitWidth == 64);
+ YT_ASSERT(!column->Values->ZigZagEncoded);
+ auto rleIndexes = GetTypedData<ui64>(*column->Values);
+ index = TranslateRleIndex(rleIndexes, index);
+ column = column->Rle->ValueColumn;
+}
+
+inline bool IsColumnValueNull(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn* column,
+ int index)
+{
+ return column->NullBitmap && GetBit(*column->NullBitmap, index);
+}
+
+inline bool ResolveDictionaryEncoding(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn*& column,
+ i64& index)
+{
+ if (!column->Dictionary) {
+ return true;
+ }
+
+ const auto& dictionary = *column->Dictionary;
+ YT_ASSERT(dictionary.ZeroMeansNull);
+ YT_ASSERT(column->Values->BitWidth == 32);
+ YT_ASSERT(!column->Values->ZigZagEncoded);
+ index = static_cast<i64>(GetTypedData<ui32>(*column->Values)[index]) - 1;
+ if (index < 0) {
+ return false;
+ }
+ column = column->Dictionary->ValueColumn;
+ return true;
+}
+
+inline TStringBuf DecodeStringFromColumn(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn& column,
+ i64 index)
+{
+ const auto& strings = *column.Strings;
+ YT_ASSERT(strings.AvgLength);
+ YT_ASSERT(column.Values->BitWidth == 32);
+ YT_ASSERT(column.Values->ZigZagEncoded);
+
+ auto getOffset = [&] (i64 index) {
+ return (index == 0)
+ ? 0
+ : *strings.AvgLength * index + ZigZagDecode64(GetTypedData<ui32>(*column.Values)[index - 1]);
+ };
+
+ i64 offset = getOffset(index);
+ i64 nextOffset = getOffset(index + 1);
+ return TStringBuf(strings.Data.Begin() + offset, strings.Data.Begin() + nextOffset);
+}
+
+template <class T>
+T DecodeIntegerFromColumn(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn& column,
+ i64 index)
+{
+ YT_ASSERT(column.Values->BitWidth == 64);
+ auto value = GetTypedData<ui64>(*column.Values)[index];
+ value += column.Values->BaseValue;
+ if (column.Values->ZigZagEncoded) {
+ value = static_cast<ui64>(ZigZagDecode64(value));
+ }
+ return static_cast<T>(value);
+}
+
+template <typename T>
+T DecodeFloatingPointFromColumn(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn& column,
+ i64 index)
+{
+ static_assert(std::is_floating_point_v<T>);
+
+ YT_ASSERT(column.Values->BitWidth == sizeof(T) * 8);
+ return GetTypedData<T>(*column.Values)[index];
+}
+
+inline bool DecodeBoolFromColumn(
+ const NTableClient::IUnversionedColumnarRowBatch::TColumn& column,
+ i64 index)
+{
+ YT_ASSERT(column.Values->BitWidth == 1);
+ return GetBit(*column.Values, index);
+}
+
+std::vector<TUnversionedRow> CreateFilteredRangedRows(
+ const std::vector<TUnversionedRow>& initial,
+ TNameTablePtr writeNameTable,
+ TNameTablePtr readNameTable,
+ TColumnFilter columnFilter,
+ NChunkClient::TLegacyReadRange readRange,
+ TChunkedMemoryPool* pool,
+ int keyColumnCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
diff --git a/yt/yt/client/table_client/unittests/helpers/ya.make b/yt/yt/client/table_client/unittests/helpers/ya.make
new file mode 100644
index 0000000000..8c5a0219f1
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/helpers/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ helpers.cpp
+)
+
+PEERDIR(
+ yt/yt/client
+ yt/yt/core/test_framework
+)
+
+END()
diff --git a/yt/yt/client/table_client/unittests/serialization_ut.cpp b/yt/yt/client/table_client/unittests/serialization_ut.cpp
new file mode 100644
index 0000000000..ed1c64a113
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/serialization_ut.cpp
@@ -0,0 +1,71 @@
+#include <yt/yt/client/formats/format.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/test_framework/framework.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSchemaSerialization, ParseUsingNodeAndSerialize)
+{
+ const char* schemaString = "<strict=%true;unique_keys=%false>"
+ "[{name=a;required=%false;type=int64;};{deleted=%true;stable_name=b}]";
+ TTableSchema schema;
+ Deserialize(schema, NYTree::ConvertToNode(NYson::TYsonString(TString(schemaString))));
+
+ EXPECT_EQ(1, std::ssize(schema.Columns()));
+ EXPECT_EQ("a", schema.Columns()[0].Name());
+ EXPECT_EQ(1, std::ssize(schema.DeletedColumns()));
+ EXPECT_EQ("b", schema.DeletedColumns()[0].StableName().Get());
+
+ NYT::NFormats::TFormat format(NFormats::EFormatType::Json);
+
+ TBlobOutput buffer;
+ auto consumer = CreateConsumerForFormat(format, NFormats::EDataType::Structured, &buffer);
+
+ Serialize(schema, consumer.get());
+ consumer->Flush();
+ auto ref = buffer.Flush();
+ auto buf = ref.ToStringBuf();
+
+ EXPECT_EQ(TString(buf.data(), buf.size()),
+R"RR({"$attributes":{"strict":true,"unique_keys":false},"$value":[{"name":"a","required":false,"type":"int64","type_v3":{"type_name":"optional","item":"int64"}},{"stable_name":"b","deleted":true}]})RR");
+}
+
+TEST(TSchemaSerialization, Cursor)
+{
+ const char* schemaString = "<strict=%true;unique_keys=%false>"
+ "[{name=a;required=%false;type=int64;};{deleted=%true;stable_name=b}]";
+
+ TMemoryInput input(schemaString);
+ NYson::TYsonPullParser parser(&input, NYson::EYsonType::Node);
+ NYson::TYsonPullParserCursor cursor(&parser);
+
+ TTableSchema schema;
+ Deserialize(schema, &cursor);
+
+ EXPECT_EQ(1, std::ssize(schema.Columns()));
+ EXPECT_EQ("a", schema.Columns()[0].Name());
+ EXPECT_EQ(1, std::ssize(schema.DeletedColumns()));
+ EXPECT_EQ("b", schema.DeletedColumns()[0].StableName().Get());
+}
+
+TEST(TSchemaSerialization, Deleted)
+{
+ const char* schemaString = "<strict=%true;unique_keys=%false>"
+ "[{name=a;required=%false;type=int64;};{deleted=%true;name=b}]";
+
+ TTableSchema schema;
+ EXPECT_THROW_WITH_SUBSTRING(
+ Deserialize(schema, NYTree::ConvertToNode(NYson::TYsonString(TString(schemaString)))),
+ "stable name should be set for a deleted column");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unittests/ya.make b/yt/yt/client/table_client/unittests/ya.make
new file mode 100644
index 0000000000..d7feeafbfd
--- /dev/null
+++ b/yt/yt/client/table_client/unittests/ya.make
@@ -0,0 +1,24 @@
+GTEST(unittester-client-table-client)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+ALLOCATOR(YT)
+
+SRCS(
+ columnar_ut.cpp
+ serialization_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/client
+ yt/yt/client/formats
+ yt/yt/client/table_client/unittests/helpers
+ yt/yt/client/unittests/mock
+ yt/yt/core/test_framework
+)
+
+SIZE(MEDIUM)
+
+END()
diff --git a/yt/yt/client/table_client/unordered_schemaful_reader.cpp b/yt/yt/client/table_client/unordered_schemaful_reader.cpp
new file mode 100644
index 0000000000..56037eeac0
--- /dev/null
+++ b/yt/yt/client/table_client/unordered_schemaful_reader.cpp
@@ -0,0 +1,312 @@
+#include "unordered_schemaful_reader.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NTableClient {
+
+using namespace NConcurrency;
+using namespace NChunkClient::NProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// 1. Sequential prefetch
+// - 0
+// - 1
+// - all
+// 2. Unordered
+// - full concurrency and prefetch
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnorderedSchemafulReader
+ : public ISchemafulUnversionedReader
+{
+public:
+ TUnorderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader,
+ int concurrency)
+ : GetNextReader_(std::move(getNextReader))
+ {
+ Sessions_.reserve(concurrency);
+ for (int index = 0; index < concurrency; ++index) {
+ auto reader = GetNextReader_();
+ if (!reader) {
+ Exhausted_ = true;
+ break;
+ }
+ Sessions_.emplace_back(std::move(reader));
+ }
+ }
+
+ ~TUnorderedSchemafulReader()
+ {
+ CancelableContext_->Cancel(TError("Reader destroyed"));
+ }
+
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& options) override
+ {
+ bool hasPending = false;
+
+ for (auto& session : Sessions_) {
+ if (session.Exhausted) {
+ continue;
+ }
+
+ if (session.ReadyEvent) {
+ if (!session.ReadyEvent->IsSet()) {
+ hasPending = true;
+ continue;
+ }
+
+ const auto& error = session.ReadyEvent->Get();
+ if (!error.IsOK()) {
+ auto guard = WriterGuard(SpinLock_);
+ ReadyEvent_ = MakePromise<void>(error);
+ return CreateEmptyUnversionedRowBatch();
+ }
+
+ session.ReadyEvent->Reset();
+ }
+
+ // TODO(babenko): consider adjusting options w.r.t. concurrency.
+ auto batch = session.Reader->Read(options);
+ if (!batch) {
+ session.Exhausted = true;
+ if (RefillSession(session)) {
+ hasPending = true;
+ }
+ continue;
+ }
+
+ if (!batch->IsEmpty()) {
+ return batch;
+ }
+
+ YT_ASSERT(!session.ReadyEvent);
+ UpdateSession(session);
+ hasPending = true;
+ }
+
+ if (!hasPending) {
+ return nullptr;
+ }
+
+ auto readyEvent = NewPromise<void>();
+ {
+ auto guard = WriterGuard(SpinLock_);
+ ReadyEvent_ = readyEvent;
+ }
+
+ for (auto& session : Sessions_) {
+ if (session.ReadyEvent) {
+ readyEvent.TrySetFrom(*session.ReadyEvent);
+ }
+ }
+
+ readyEvent.OnCanceled(BIND(&TUnorderedSchemafulReader::OnCanceled, MakeWeak(this)));
+
+ return CreateEmptyUnversionedRowBatch();
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ return DoGetReadyEvent();
+ }
+
+ TDataStatistics GetDataStatistics() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ auto dataStatistics = DataStatistics_;
+ for (const auto& session : Sessions_) {
+ if (session.Reader) {
+ dataStatistics += session.Reader->GetDataStatistics();
+ }
+ }
+ return dataStatistics;
+ }
+
+ NChunkClient::TCodecStatistics GetDecompressionStatistics() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ auto result = DecompressionStatistics_;
+ for (const auto& session : Sessions_) {
+ if (session.Reader) {
+ result += session.Reader->GetDecompressionStatistics();
+ }
+ }
+ return result;
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ for (const auto& session : Sessions_) {
+ if (session.Reader && !session.Reader->IsFetchingCompleted()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ std::vector<NChunkClient::TChunkId> GetFailedChunkIds() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ auto result = FailedChunkIds_;
+ for (const auto& session : Sessions_) {
+ if (session.Reader) {
+ auto failedChunkIds = session.Reader->GetFailedChunkIds();
+ result.insert(result.end(), failedChunkIds.begin(), failedChunkIds.end());
+ }
+ }
+ return result;
+ }
+
+private:
+ const std::function<ISchemafulUnversionedReaderPtr()> GetNextReader_;
+
+ struct TSession
+ {
+ explicit TSession(ISchemafulUnversionedReaderPtr reader)
+ : Reader(std::move(reader))
+ { }
+
+ ISchemafulUnversionedReaderPtr Reader;
+ TFutureHolder<void> ReadyEvent;
+ bool Exhausted = false;
+ };
+
+ std::vector<TSession> Sessions_;
+ bool Exhausted_ = false;
+ TDataStatistics DataStatistics_;
+ NChunkClient::TCodecStatistics DecompressionStatistics_;
+ std::vector<NChunkClient::TChunkId> FailedChunkIds_;
+
+ TPromise<void> ReadyEvent_ = MakePromise<void>(TError());
+ const TCancelableContextPtr CancelableContext_ = New<TCancelableContext>();
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+
+ TPromise<void> DoGetReadyEvent() const
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ return ReadyEvent_;
+ }
+
+ void UpdateSession(TSession& session)
+ {
+ session.ReadyEvent = session.Reader->GetReadyEvent();
+ session.ReadyEvent->Subscribe(BIND(&TUnorderedSchemafulReader::OnReady, MakeStrong(this)));
+ CancelableContext_->PropagateTo(*session.ReadyEvent);
+ }
+
+ bool RefillSession(TSession& session)
+ {
+ auto dataStatistics = session.Reader->GetDataStatistics();
+ auto cpuCompressionStatistics = session.Reader->GetDecompressionStatistics();
+ {
+ auto guard = WriterGuard(SpinLock_);
+ DataStatistics_ += dataStatistics;
+ DecompressionStatistics_ += cpuCompressionStatistics;
+ auto failedChunkIds = session.Reader->GetFailedChunkIds();
+ FailedChunkIds_.insert(FailedChunkIds_.end(), failedChunkIds.begin(), failedChunkIds.end());
+ session.Reader.Reset();
+ }
+
+ if (Exhausted_) {
+ return false;
+ }
+
+ auto reader = GetNextReader_();
+ if (!reader) {
+ Exhausted_ = true;
+ return false;
+ }
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+ session.Exhausted = false;
+ session.Reader = std::move(reader);
+ }
+
+ UpdateSession(session);
+ return true;
+ }
+
+ void OnReady(const TError& value)
+ {
+ DoGetReadyEvent().TrySet(value);
+ }
+
+ void OnCanceled(const TError& error)
+ {
+ DoGetReadyEvent().TrySet(TError(NYT::EErrorCode::Canceled, "Table reader canceled")
+ << error);
+ CancelableContext_->Cancel(error);
+ }
+};
+
+ISchemafulUnversionedReaderPtr CreateUnorderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader,
+ int concurrency)
+{
+ return New<TUnorderedSchemafulReader>(
+ std::move(getNextReader),
+ concurrency);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemafulUnversionedReaderPtr CreateOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader)
+{
+ return CreateUnorderedSchemafulReader(getNextReader, 1);
+}
+
+ISchemafulUnversionedReaderPtr CreatePrefetchingOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader)
+{
+ auto nextReader = getNextReader();
+ auto readerGenerator = [
+ nextReader = std::move(nextReader),
+ getNextReader = std::move(getNextReader)
+ ] () mutable -> ISchemafulUnversionedReaderPtr {
+ auto currentReader = nextReader;
+ if (currentReader) {
+ nextReader = getNextReader();
+ }
+ return currentReader;
+ };
+
+ return CreateUnorderedSchemafulReader(readerGenerator, 1);
+}
+
+ISchemafulUnversionedReaderPtr CreateFullPrefetchingOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader)
+{
+ std::vector<ISchemafulUnversionedReaderPtr> readers;
+
+ while (auto nextReader = getNextReader()) {
+ readers.push_back(nextReader);
+ }
+
+ auto readerGenerator = [
+ index = 0,
+ readers = std::move(readers)
+ ] () mutable -> ISchemafulUnversionedReaderPtr {
+ if (index == std::ssize(readers)) {
+ return nullptr;
+ }
+ return readers[index++];
+ };
+
+ return CreateUnorderedSchemafulReader(readerGenerator, 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unordered_schemaful_reader.h b/yt/yt/client/table_client/unordered_schemaful_reader.h
new file mode 100644
index 0000000000..4137a61ca7
--- /dev/null
+++ b/yt/yt/client/table_client/unordered_schemaful_reader.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemafulUnversionedReaderPtr CreateUnorderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader,
+ int concurrency);
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemafulUnversionedReaderPtr CreateOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader);
+
+ISchemafulUnversionedReaderPtr CreatePrefetchingOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader);
+
+ISchemafulUnversionedReaderPtr CreateFullPrefetchingOrderedSchemafulReader(
+ std::function<ISchemafulUnversionedReaderPtr()> getNextReader);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unversioned_reader.h b/yt/yt/client/table_client/unversioned_reader.h
new file mode 100644
index 0000000000..ec15a766b9
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_reader.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/client/chunk_client/reader_base.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IUnversionedReaderBase
+ : public virtual NChunkClient::IReaderBase
+{
+ //! NB: The returned batch can be invalidated by the next call to `Read`.
+ virtual IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& options = {}) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISchemafulUnversionedReader
+ : public IUnversionedReaderBase
+{ };
+
+DEFINE_REFCOUNTED_TYPE(ISchemafulUnversionedReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISchemalessUnversionedReader
+ : public IUnversionedReaderBase
+{
+ virtual const TNameTablePtr& GetNameTable() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISchemalessUnversionedReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unversioned_row.cpp b/yt/yt/client/table_client/unversioned_row.cpp
new file mode 100644
index 0000000000..9879da2eb6
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_row.cpp
@@ -0,0 +1,2097 @@
+#include "unversioned_row.h"
+
+#include "composite_compare.h"
+#include "helpers.h"
+#include "serialize.h"
+#include "unversioned_value.h"
+#include "validate_logical_type.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <yt/yt/core/misc/range.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <util/generic/ymath.h>
+
+#include <util/charset/utf8.h>
+#include <util/stream/str.h>
+
+#include <cmath>
+
+namespace NYT::NTableClient {
+
+using namespace NChunkClient;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString SerializedNullRow("");
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetByteSize(const TUnversionedValue& value)
+{
+ int result = MaxVarUint32Size * 2; // id and type
+
+ switch (value.Type) {
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ return result;
+
+ case EValueType::Int64:
+ case EValueType::Uint64:
+ result += MaxVarInt64Size;
+ return result;
+
+ case EValueType::Double:
+ result += sizeof(double);
+ return result;
+
+
+ case EValueType::Boolean:
+ result += 1;
+ return result;
+
+ case EValueType::String:
+ case EValueType::Any:
+ case EValueType::Composite:
+ result += MaxVarUint32Size + value.Length;
+ return result;
+ }
+
+ return result;
+}
+
+size_t WriteRowValue(char* output, const TUnversionedValue& value, bool isInlineHunkValue)
+{
+ char* current = output;
+
+ current += WriteVarUint32(current, value.Id);
+ current += WriteVarUint32(current, static_cast<ui16>(value.Type));
+
+ switch (value.Type) {
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ break;
+
+ case EValueType::Int64:
+ current += WriteVarInt64(current, value.Data.Int64);
+ break;
+
+ case EValueType::Uint64:
+ current += WriteVarUint64(current, value.Data.Uint64);
+ break;
+
+ case EValueType::Double:
+ ::memcpy(current, &value.Data.Double, sizeof (double));
+ current += sizeof (double);
+ break;
+
+ case EValueType::Boolean:
+ *current++ = value.Data.Boolean ? '\x01' : '\x00';
+ break;
+
+ case EValueType::String:
+ case EValueType::Any:
+ case EValueType::Composite:
+ current += WriteVarUint32(current, value.Length + (isInlineHunkValue ? 1 : 0));
+ if (isInlineHunkValue) {
+ *current++ = static_cast<char>(EHunkValueTag::Inline);
+ }
+ ::memcpy(current, value.Data.String, value.Length);
+ current += value.Length;
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ return current - output;
+}
+
+size_t ReadRowValue(const char* input, TUnversionedValue* value)
+{
+ const char* current = input;
+
+ ui32 id;
+ current += ReadVarUint32(current, &id);
+
+ ui32 typeValue;
+ current += ReadVarUint32(current, &typeValue);
+ auto type = static_cast<EValueType>(typeValue);
+
+ switch (type) {
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ *value = MakeUnversionedSentinelValue(type, id);
+ break;
+
+ case EValueType::Int64: {
+ i64 data;
+ current += ReadVarInt64(current, &data);
+ *value = MakeUnversionedInt64Value(data, id);
+ break;
+ }
+
+ case EValueType::Uint64: {
+ ui64 data;
+ current += ReadVarUint64(current, &data);
+ *value = MakeUnversionedUint64Value(data, id);
+ break;
+ }
+
+ case EValueType::Double: {
+ double data;
+ ::memcpy(&data, current, sizeof (double));
+ current += sizeof (double);
+ *value = MakeUnversionedDoubleValue(data, id);
+ break;
+ }
+
+ case EValueType::Boolean: {
+ bool data = (*current) == 1;
+ current += 1;
+ *value = MakeUnversionedBooleanValue(data, id);
+ break;
+ }
+
+ case EValueType::Any:
+ case EValueType::Composite:
+ case EValueType::String: {
+ ui32 length;
+ current += ReadVarUint32(current, &length);
+ TStringBuf data(current, current + length);
+ current += length;
+ *value = MakeUnversionedStringLikeValue(type, data, id);
+ break;
+ }
+
+ default:
+ ThrowUnexpectedValueType(type);
+ }
+
+ return current - input;
+}
+
+void Save(TStreamSaveContext& context, const TUnversionedValue& value)
+{
+ auto* output = context.GetOutput();
+ if (IsStringLikeType(value.Type)) {
+ output->Write(&value, sizeof (ui16) + sizeof (ui16) + sizeof (ui32)); // Id, Type, Length
+ if (value.Length != 0) {
+ output->Write(value.Data.String, value.Length);
+ }
+ } else {
+ output->Write(&value, sizeof (TUnversionedValue));
+ }
+}
+
+void Load(TStreamLoadContext& context, TUnversionedValue& value, TChunkedMemoryPool* pool)
+{
+ auto* input = context.GetInput();
+ const size_t fixedSize = sizeof (ui16) + sizeof (ui16) + sizeof (ui32); // Id, Type, Length
+ YT_VERIFY(input->Load(&value, fixedSize) == fixedSize);
+ if (IsStringLikeType(value.Type)) {
+ if (value.Length != 0) {
+ value.Data.String = pool->AllocateUnaligned(value.Length);
+ YT_VERIFY(input->Load(const_cast<char*>(value.Data.String), value.Length) == value.Length);
+ } else {
+ value.Data.String = nullptr;
+ }
+ } else {
+ YT_VERIFY(input->Load(&value.Data, sizeof (value.Data)) == sizeof (value.Data));
+ }
+}
+
+size_t GetYsonSize(const TUnversionedValue& value)
+{
+ switch (value.Type) {
+ case EValueType::Any:
+ case EValueType::Composite:
+ return value.Length;
+
+ case EValueType::Null:
+ // Marker type.
+ return 1;
+
+ case EValueType::Int64:
+ case EValueType::Uint64:
+ // Type marker + size;
+ return 1 + MaxVarInt64Size;
+
+ case EValueType::Double:
+ // Type marker + sizeof double.
+ return 1 + 8;
+
+ case EValueType::String:
+ // Type marker + length + string bytes.
+ return 1 + MaxVarInt32Size + value.Length;
+
+ case EValueType::Boolean:
+ // Type marker + value.
+ return 1 + 1;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+size_t WriteYson(char* buffer, const TUnversionedValue& unversionedValue)
+{
+ // TODO(psushin): get rid of output stream.
+ TMemoryOutput output(buffer, GetYsonSize(unversionedValue));
+ TYsonWriter writer(&output, EYsonFormat::Binary);
+ switch (unversionedValue.Type) {
+ case EValueType::Int64:
+ writer.OnInt64Scalar(unversionedValue.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ writer.OnUint64Scalar(unversionedValue.Data.Uint64);
+ break;
+
+ case EValueType::Double:
+ writer.OnDoubleScalar(unversionedValue.Data.Double);
+ break;
+
+ case EValueType::String:
+ writer.OnStringScalar(unversionedValue.AsStringBuf());
+ break;
+
+ case EValueType::Boolean:
+ writer.OnBooleanScalar(unversionedValue.Data.Boolean);
+ break;
+
+ case EValueType::Null:
+ writer.OnEntity();
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ return output.Buf() - buffer;
+}
+
+namespace {
+
+[[noreturn]] void ThrowIncomparableTypes(const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::IncomparableTypes,
+ "Cannot compare values of types %Qlv and %Qlv; only scalar types are allowed for key columns",
+ lhs.Type,
+ rhs.Type)
+ << TErrorAttribute("lhs_value", lhs)
+ << TErrorAttribute("rhs_value", rhs);
+}
+
+} // namespace
+
+Y_FORCE_INLINE bool IsSentinel(EValueType valueType)
+{
+ return valueType == EValueType::Min || valueType == EValueType::Max;
+}
+
+int CompareRowValues(const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ // TODO(babenko): check flags; forbid comparing hunks and aggregates.
+
+ if (lhs.Type == EValueType::Any || rhs.Type == EValueType::Any) {
+ if (!IsSentinel(lhs.Type) && !IsSentinel(rhs.Type)) {
+ // Never compare composite values with non-sentinels.
+ ThrowIncomparableTypes(lhs, rhs);
+ }
+ }
+
+ if (lhs.Type == EValueType::Composite || rhs.Type == EValueType::Composite) {
+ if (lhs.Type != rhs.Type) {
+ if (!IsSentinel(lhs.Type) && lhs.Type != EValueType::Null &&
+ !IsSentinel(rhs.Type) && rhs.Type != EValueType::Null)
+ {
+ ThrowIncomparableTypes(lhs, rhs);
+ }
+ return static_cast<int>(lhs.Type) - static_cast<int>(rhs.Type);
+ }
+ try {
+ auto lhsData = TYsonStringBuf(lhs.AsStringBuf());
+ auto rhsData = TYsonStringBuf(rhs.AsStringBuf());
+ return CompareCompositeValues(lhsData, rhsData);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::IncomparableComplexValues,
+ "Cannot compare complex values")
+ << TErrorAttribute("lhs_value", lhs)
+ << TErrorAttribute("rhs_value", rhs)
+ << ex;
+ }
+ }
+
+ if (Y_UNLIKELY(lhs.Type != rhs.Type)) {
+ return static_cast<int>(lhs.Type) - static_cast<int>(rhs.Type);
+ }
+
+ switch (lhs.Type) {
+ case EValueType::Int64: {
+ auto lhsValue = lhs.Data.Int64;
+ auto rhsValue = rhs.Data.Int64;
+ if (lhsValue < rhsValue) {
+ return -1;
+ } else if (lhsValue > rhsValue) {
+ return +1;
+ } else {
+ return 0;
+ }
+ }
+
+ case EValueType::Uint64: {
+ auto lhsValue = lhs.Data.Uint64;
+ auto rhsValue = rhs.Data.Uint64;
+ if (lhsValue < rhsValue) {
+ return -1;
+ } else if (lhsValue > rhsValue) {
+ return +1;
+ } else {
+ return 0;
+ }
+ }
+
+ case EValueType::Double: {
+ double lhsValue = lhs.Data.Double;
+ double rhsValue = rhs.Data.Double;
+ if (lhsValue < rhsValue) {
+ return -1;
+ } else if (lhsValue > rhsValue) {
+ return +1;
+ } else if (std::isnan(lhsValue)) {
+ if (std::isnan(rhsValue)) {
+ return 0;
+ } else {
+ return 1;
+ }
+ } else if (std::isnan(rhsValue)) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+
+ case EValueType::Boolean: {
+ bool lhsValue = lhs.Data.Boolean;
+ bool rhsValue = rhs.Data.Boolean;
+ if (lhsValue < rhsValue) {
+ return -1;
+ } else if (lhsValue > rhsValue) {
+ return +1;
+ } else {
+ return 0;
+ }
+ }
+
+ case EValueType::String: {
+ size_t lhsLength = lhs.Length;
+ size_t rhsLength = rhs.Length;
+ size_t minLength = std::min(lhsLength, rhsLength);
+ int result = ::memcmp(lhs.Data.String, rhs.Data.String, minLength);
+ if (result == 0) {
+ if (lhsLength < rhsLength) {
+ return -1;
+ } else if (lhsLength > rhsLength) {
+ return +1;
+ } else {
+ return 0;
+ }
+ } else {
+ return result;
+ }
+ }
+
+ // All sentinel types are equal.
+ default:
+ return 0;
+ }
+}
+
+bool operator == (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) == 0;
+}
+
+bool operator != (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) != 0;
+}
+
+bool operator <= (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) <= 0;
+}
+
+bool operator < (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) < 0;
+}
+
+bool operator >= (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) >= 0;
+}
+
+bool operator > (const TUnversionedValue& lhs, const TUnversionedValue& rhs)
+{
+ return CompareRowValues(lhs, rhs) > 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+int CompareValueRanges(TUnversionedValueRange lhs, TUnversionedValueRange rhs)
+{
+ auto* lhsCurrent = lhs.begin();
+ auto* rhsCurrent = rhs.begin();
+ while (lhsCurrent != lhs.end() && rhsCurrent != rhs.end()) {
+ int result = CompareRowValues(*lhsCurrent++, *rhsCurrent++);
+ if (result != 0) {
+ return result;
+ }
+ }
+ return std::ssize(lhs) - std::ssize(rhs);
+}
+
+int CompareRows(TUnversionedRow lhs, TUnversionedRow rhs, ui32 prefixLength)
+{
+ if (!lhs && !rhs) {
+ return 0;
+ }
+
+ if (lhs && !rhs) {
+ return +1;
+ }
+
+ if (!lhs && rhs) {
+ return -1;
+ }
+
+ return CompareValueRanges(
+ lhs.FirstNElements(std::min(lhs.GetCount(), prefixLength)),
+ rhs.FirstNElements(std::min(rhs.GetCount(), prefixLength)));
+}
+
+bool operator == (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) == 0;
+}
+
+bool operator != (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) != 0;
+}
+
+bool operator <= (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) <= 0;
+}
+
+bool operator < (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) < 0;
+}
+
+bool operator >= (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) >= 0;
+}
+
+bool operator > (TUnversionedRow lhs, TUnversionedRow rhs)
+{
+ return CompareRows(lhs, rhs) > 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator == (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) == 0;
+}
+
+bool operator != (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) != 0;
+}
+
+bool operator <= (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) <= 0;
+}
+
+bool operator < (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) < 0;
+}
+
+bool operator >= (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) >= 0;
+}
+
+bool operator > (TUnversionedRow lhs, const TUnversionedOwningRow& rhs)
+{
+ return CompareRows(lhs, rhs) > 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Needed by NYT::FarmFingerprint below.
+// TODO(babenko): consider refactoring.
+TFingerprint FarmFingerprint(const TUnversionedValue& value)
+{
+ return GetFarmFingerprint(value);
+}
+
+TFingerprint GetFarmFingerprint(TUnversionedValueRange range)
+{
+ return NYT::FarmFingerprint<TUnversionedValue>(range.begin(), range.end());
+}
+
+TFingerprint GetFarmFingerprint(TUnversionedRow row)
+{
+ return GetFarmFingerprint(row.Elements());
+}
+
+size_t GetUnversionedRowByteSize(ui32 valueCount)
+{
+ return sizeof(TUnversionedRowHeader) + sizeof(TUnversionedValue) * valueCount;
+}
+
+size_t GetDataWeight(TUnversionedRow row)
+{
+ if (!row) {
+ return 0;
+ }
+
+ size_t result = 1;
+ for (const auto& value : row) {
+ result += GetDataWeight(value);
+ }
+ return result;
+}
+
+size_t GetDataWeight(TRange<TUnversionedRow> rows)
+{
+ size_t result = 0;
+ for (auto row : rows) {
+ result += GetDataWeight(row);
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMutableUnversionedRow TMutableUnversionedRow::Allocate(TChunkedMemoryPool* pool, ui32 valueCount)
+{
+ size_t byteSize = GetUnversionedRowByteSize(valueCount);
+ return Create(pool->AllocateAligned(byteSize), valueCount);
+}
+
+TMutableUnversionedRow TMutableUnversionedRow::Create(void* buffer, ui32 valueCount)
+{
+ auto* header = reinterpret_cast<TUnversionedRowHeader*>(buffer);
+ header->Count = valueCount;
+ header->Capacity = valueCount;
+ return TMutableUnversionedRow(header);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TYsonAnyValidator
+ : public IYsonConsumer
+{
+public:
+ void OnStringScalar(TStringBuf /*value*/) override
+ { }
+
+ void OnInt64Scalar(i64 /*value*/) override
+ { }
+
+ void OnUint64Scalar(ui64 /*value*/) override
+ { }
+
+ void OnDoubleScalar(double /*value*/) override
+ { }
+
+ void OnBooleanScalar(bool /*value*/) override
+ { }
+
+ void OnEntity() override
+ { }
+
+ void OnBeginList() override
+ {
+ ++Depth_;
+ }
+
+ void OnListItem() override
+ { }
+
+ void OnEndList() override
+ {
+ --Depth_;
+ }
+
+ void OnBeginMap() override
+ {
+ ++Depth_;
+ }
+
+ void OnKeyedItem(TStringBuf /*key*/) override
+ { }
+
+ void OnEndMap() override
+ {
+ --Depth_;
+ }
+
+ void OnBeginAttributes() override
+ {
+ if (Depth_ == 0) {
+ THROW_ERROR_EXCEPTION("Table values cannot have top-level attributes");
+ }
+ }
+
+ void OnEndAttributes() override
+ { }
+
+ void OnRaw(TStringBuf /*yson*/, EYsonType /*type*/) override
+ { }
+
+private:
+ int Depth_ = 0;
+};
+
+void ValidateAnyValue(TStringBuf yson)
+{
+ TYsonAnyValidator validator;
+ ParseYsonStringBuffer(yson, EYsonType::Node, &validator);
+}
+
+void ValidateDynamicValue(const TUnversionedValue& value, bool isKey)
+{
+ switch (value.Type) {
+ case EValueType::String:
+ if (value.Length > MaxStringValueLength) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::StringLikeValueLengthLimitExceeded,
+ "Value of type %Qlv is too long for dynamic data: length %v, limit %v",
+ value.Type,
+ value.Length,
+ MaxStringValueLength);
+ }
+ break;
+
+ case EValueType::Any:
+ if (value.Length > MaxAnyValueLength) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::StringLikeValueLengthLimitExceeded,
+ "Value of type %Qlv is too long for dynamic data: length %v, limit %v",
+ value.Type,
+ value.Length,
+ MaxAnyValueLength);
+ }
+ ValidateAnyValue(value.AsStringBuf());
+ break;
+
+ case EValueType::Double:
+ if (isKey && std::isnan(value.Data.Double)) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::KeyCannotBeNan,
+ "Key of type \"double\" cannot be NaN");
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void ValidateClientRow(
+ TUnversionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable,
+ bool isKey,
+ bool allowMissingKeyColumns = false,
+ std::optional<int> tabletIndexColumnId = std::nullopt)
+{
+ if (!row) {
+ THROW_ERROR_EXCEPTION("Row cannot be null");
+ }
+
+ ValidateRowValueCount(row.GetCount());
+ ValidateKeyColumnCount(schema.GetKeyColumnCount());
+
+ bool keyColumnSeen[MaxKeyColumnCount]{};
+ bool haveDataColumns = false;
+
+ for (const auto& value : row) {
+ int mappedId = ApplyIdMapping(value, &idMapping);
+ if (mappedId < 0 || mappedId >= std::ssize(schema.Columns())) {
+ int size = nameTable->GetSize();
+ if (value.Id < 0 || value.Id >= size) {
+ THROW_ERROR_EXCEPTION("Expected value id in range [0:%v] but got %v",
+ size - 1,
+ value.Id);
+ }
+
+ THROW_ERROR_EXCEPTION("Unexpected column %Qv", nameTable->GetName(value.Id));
+ }
+
+ const auto& column = schema.Columns()[mappedId];
+ ValidateValueType(value, schema, mappedId, /*typeAnyAcceptsAllValues*/false);
+
+ if (Any(value.Flags & EValueFlags::Aggregate) && !column.Aggregate()) {
+ THROW_ERROR_EXCEPTION(
+ "\"aggregate\" flag is set for value in column %v which is not aggregating",
+ column.GetDiagnosticNameString());
+ }
+
+ if (mappedId < schema.GetKeyColumnCount()) {
+ if (keyColumnSeen[mappedId]) {
+ THROW_ERROR_EXCEPTION("Duplicate key column %v",
+ column.GetDiagnosticNameString());
+ }
+ keyColumnSeen[mappedId] = true;
+ ValidateKeyValue(value);
+ } else if (isKey) {
+ THROW_ERROR_EXCEPTION("Non-key column %v in a key",
+ column.GetDiagnosticNameString());
+ } else {
+ haveDataColumns = true;
+ ValidateDataValue(value);
+ }
+ }
+
+ if (!isKey && !haveDataColumns) {
+ THROW_ERROR_EXCEPTION("At least one non-key column must be specified");
+ }
+
+ if (tabletIndexColumnId) {
+ YT_VERIFY(std::ssize(idMapping) > *tabletIndexColumnId);
+ auto mappedId = idMapping[*tabletIndexColumnId];
+ YT_VERIFY(mappedId >= 0);
+ keyColumnSeen[mappedId] = true;
+ }
+
+ if (!allowMissingKeyColumns) {
+ for (int index = 0; index < schema.GetKeyColumnCount(); ++index) {
+ if (!keyColumnSeen[index] && !schema.Columns()[index].Expression()) {
+ THROW_ERROR_EXCEPTION("Missing key column %v",
+ schema.Columns()[index].GetDiagnosticNameString());
+ }
+ }
+ }
+
+ auto dataWeight = GetDataWeight(row);
+ if (dataWeight >= MaxClientVersionedRowDataWeight) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::RowWeightLimitExceeded,
+ "Row is too large: data weight %v, limit %v",
+ dataWeight,
+ MaxClientVersionedRowDataWeight);
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString SerializeToString(TUnversionedRow row)
+{
+ return row
+ ? SerializeToString(row.Elements())
+ : SerializedNullRow;
+}
+
+TString SerializeToString(TUnversionedValueRange range)
+{
+ int size = 2 * MaxVarUint32Size; // header size
+ for (const auto& value : range) {
+ size += GetByteSize(value);
+ }
+
+ TString buffer;
+ buffer.resize(size);
+
+ char* current = const_cast<char*>(buffer.data());
+ current += WriteVarUint32(current, 0); // format version
+ current += WriteVarUint32(current, range.size());
+
+ for (const auto& value : range) {
+ current += WriteRowValue(current, value);
+ }
+
+ buffer.resize(current - buffer.data());
+
+ return buffer;
+}
+
+TUnversionedOwningRow DeserializeFromString(TString&& data, std::optional<int> nullPaddingWidth = std::nullopt)
+{
+ if (data == SerializedNullRow) {
+ return TUnversionedOwningRow();
+ }
+ auto dataRef = TSharedRef::FromString(std::move(data));
+
+ const char* current = dataRef.begin();
+
+ ui32 version;
+ current += ReadVarUint32(current, &version);
+ YT_VERIFY(version == 0);
+
+ ui32 valueCount;
+ current += ReadVarUint32(current, &valueCount);
+
+ // TODO(max42): YT-14049.
+ int nullCount = nullPaddingWidth ? std::max<int>(0, *nullPaddingWidth - static_cast<int>(valueCount)) : 0;
+
+ size_t fixedSize = GetUnversionedRowByteSize(valueCount + nullCount);
+ auto rowData = TSharedMutableRef::Allocate<TOwningRowTag>(fixedSize, {.InitializeStorage = false});
+ auto* header = reinterpret_cast<TUnversionedRowHeader*>(rowData.Begin());
+
+ header->Count = static_cast<i32>(valueCount + nullCount);
+ header->Capacity = static_cast<i32>(valueCount + nullCount);
+
+ auto* values = reinterpret_cast<TUnversionedValue*>(header + 1);
+ for (int index = 0; index < static_cast<int>(valueCount); ++index) {
+ auto* value = values + index;
+ current += ReadRowValue(current, value);
+ }
+ for (int index = valueCount; index < static_cast<int>(valueCount + nullCount); ++index) {
+ values[index] = MakeUnversionedNullValue(index);
+ }
+
+ return TUnversionedOwningRow(std::move(rowData), std::move(dataRef));
+}
+
+TUnversionedRow DeserializeFromString(const TString& data, const TRowBufferPtr& rowBuffer)
+{
+ if (data == SerializedNullRow) {
+ return TUnversionedRow();
+ }
+
+ const char* current = data.data();
+
+ ui32 version;
+ current += ReadVarUint32(current, &version);
+ YT_VERIFY(version == 0);
+
+ ui32 valueCount;
+ current += ReadVarUint32(current, &valueCount);
+
+ auto row = rowBuffer->AllocateUnversioned(valueCount);
+
+ auto* values = row.begin();
+ for (int index = 0; index < static_cast<int>(valueCount); ++index) {
+ auto* value = values + index;
+ current += ReadRowValue(current, value);
+ rowBuffer->CaptureValue(value);
+ }
+
+ return row;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TUnversionedRow::Save(TSaveContext& context) const
+{
+ NYT::Save(context, SerializeToString(*this));
+}
+
+void TUnversionedRow::Load(TLoadContext& context)
+{
+ *this = DeserializeFromString(NYT::Load<TString>(context), context.GetRowBuffer());
+}
+
+void ValidateValueType(
+ const TUnversionedValue& value,
+ const TTableSchema& schema,
+ int schemaId,
+ bool typeAnyAcceptsAllValues,
+ bool ignoreRequired,
+ bool validateAnyIsValidYson)
+{
+ ValidateValueType(
+ value,
+ schema.Columns()[schemaId],
+ typeAnyAcceptsAllValues,
+ ignoreRequired,
+ validateAnyIsValidYson);
+}
+
+[[noreturn]] static void ThrowInvalidColumnType(EValueType expected, EValueType actual)
+{
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Invalid type: expected %Qlv, actual %Qlv",
+ expected,
+ actual);
+}
+
+static inline void ValidateColumnType(EValueType expected, const TUnversionedValue& value)
+{
+ if (value.Type != expected) {
+ ThrowInvalidColumnType(expected, value.Type);
+ }
+}
+
+template <ESimpleLogicalValueType logicalType>
+Y_FORCE_INLINE auto GetValue(const TUnversionedValue& value)
+{
+ constexpr auto physicalType = GetPhysicalType(logicalType);
+ ValidateColumnType(physicalType, value);
+ if constexpr (physicalType == EValueType::Int64) {
+ return value.Data.Int64;
+ } else if constexpr (physicalType == EValueType::Uint64) {
+ return value.Data.Uint64;
+ } else if constexpr (physicalType == EValueType::Double) {
+ return value.Data.Double;
+ } else if constexpr (physicalType == EValueType::Boolean) {
+ return value.Data.Boolean;
+ } else {
+ static_assert(physicalType == EValueType::String || physicalType == EValueType::Any);
+ return value.AsStringBuf();
+ }
+}
+
+static const TLogicalTypePtr& UnwrapTaggedAndOptional(const TLogicalTypePtr& type)
+{
+ const TLogicalTypePtr* current = &type;
+ while ((*current)->GetMetatype() == ELogicalMetatype::Tagged) {
+ current = &(*current)->UncheckedAsTaggedTypeRef().GetElement();
+ }
+
+ if ((*current)->GetMetatype() != ELogicalMetatype::Optional) {
+ return *current;
+ }
+
+ const auto& optionalType = (*current)->UncheckedAsOptionalTypeRef();
+ if (optionalType.IsElementNullable()) {
+ return *current;
+ }
+
+ current = &optionalType.GetElement();
+
+ while ((*current)->GetMetatype() == ELogicalMetatype::Tagged) {
+ current = &(*current)->UncheckedAsTaggedTypeRef().GetElement();
+ }
+
+ return *current;
+}
+
+void ValidateValueType(
+ const TUnversionedValue& value,
+ const TColumnSchema& columnSchema,
+ bool typeAnyAcceptsAllValues,
+ bool ignoreRequired,
+ bool validateAnyIsValidYson)
+{
+ if (value.Type == EValueType::Null) {
+ if (columnSchema.Required()) {
+ if (ignoreRequired) {
+ return;
+ }
+
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Required column %v cannot have %Qlv value",
+ columnSchema.GetDiagnosticNameString(),
+ value.Type);
+ } else {
+ return;
+ }
+ }
+
+ try {
+ auto v1Type = columnSchema.CastToV1Type();
+ switch (v1Type) {
+ case ESimpleLogicalValueType::Null:
+ case ESimpleLogicalValueType::Void:
+ // this case should be handled before
+ ValidateColumnType(EValueType::Null, value);
+ return;
+ case ESimpleLogicalValueType::Any:
+ if (columnSchema.IsOfV1Type()) {
+ if (!typeAnyAcceptsAllValues) {
+ ValidateColumnType(EValueType::Any, value);
+ } else if (!IsAnyColumnCompatibleType(value.Type)) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Cannot write value of type %Qlv into type any column",
+ value.Type);
+ }
+ if (IsAnyOrComposite(value.Type) && validateAnyIsValidYson) {
+ ValidateAnyValue(value.AsStringBuf());
+ }
+ } else {
+ ValidateColumnType(EValueType::Composite, value);
+ ValidateComplexLogicalType(value.AsStringBuf(), columnSchema.LogicalType());
+ }
+ return;
+ case ESimpleLogicalValueType::String:
+ if (columnSchema.IsOfV1Type()) {
+ ValidateSimpleLogicalType<ESimpleLogicalValueType::String>(GetValue<ESimpleLogicalValueType::String>(value));
+ } else {
+ ValidateColumnType(EValueType::String, value);
+ auto type = UnwrapTaggedAndOptional(columnSchema.LogicalType());
+ YT_VERIFY(type->GetMetatype() == ELogicalMetatype::Decimal);
+ NDecimal::TDecimal::ValidateBinaryValue(
+ value.AsStringBuf(),
+ type->UncheckedAsDecimalTypeRef().GetPrecision(),
+ type->UncheckedAsDecimalTypeRef().GetScale());
+ }
+ return;
+#define CASE(x) \
+ case x: \
+ ValidateSimpleLogicalType<x>(GetValue<x>(value)); \
+ return;
+
+ CASE(ESimpleLogicalValueType::Int64)
+ CASE(ESimpleLogicalValueType::Uint64)
+ CASE(ESimpleLogicalValueType::Double)
+ CASE(ESimpleLogicalValueType::Boolean)
+
+ CASE(ESimpleLogicalValueType::Float)
+
+ CASE(ESimpleLogicalValueType::Int8)
+ CASE(ESimpleLogicalValueType::Int16)
+ CASE(ESimpleLogicalValueType::Int32)
+
+ CASE(ESimpleLogicalValueType::Uint8)
+ CASE(ESimpleLogicalValueType::Uint16)
+ CASE(ESimpleLogicalValueType::Uint32)
+
+ CASE(ESimpleLogicalValueType::Utf8)
+ CASE(ESimpleLogicalValueType::Date)
+ CASE(ESimpleLogicalValueType::Datetime)
+ CASE(ESimpleLogicalValueType::Timestamp)
+ CASE(ESimpleLogicalValueType::Interval)
+ CASE(ESimpleLogicalValueType::Json)
+ CASE(ESimpleLogicalValueType::Uuid)
+#undef CASE
+ }
+ YT_ABORT();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::SchemaViolation,
+ "Error validating column %v",
+ columnSchema.GetDiagnosticNameString())
+ << ex;
+ }
+}
+
+void ValidateStaticValue(const TUnversionedValue& value)
+{
+ ValidateDataValueType(value.Type);
+ if (IsStringLikeType(value.Type)) {
+ if (value.Length > MaxRowWeightLimit) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::StringLikeValueLengthLimitExceeded,
+ "Value of type %Qlv is too long for static data: length %v, limit %v",
+ value.Type,
+ value.Length,
+ MaxRowWeightLimit);
+ }
+ }
+}
+
+void ValidateDataValue(const TUnversionedValue& value)
+{
+ ValidateDataValueType(value.Type);
+ ValidateDynamicValue(value, /*isKey*/ false);
+}
+
+void ValidateKeyValue(const TUnversionedValue& value)
+{
+ ValidateKeyValueType(value.Type);
+ ValidateDynamicValue(value, /*isKey*/ true);
+}
+
+void ValidateRowValueCount(int count)
+{
+ if (count < 0) {
+ THROW_ERROR_EXCEPTION("Negative number of values in row");
+ }
+ if (count > MaxValuesPerRow) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::TooManyValuesInRow,
+ "Too many values in row: actual %v, limit %v",
+ count,
+ MaxValuesPerRow);
+ }
+}
+
+void ValidateKeyColumnCount(int count)
+{
+ if (count < 0) {
+ THROW_ERROR_EXCEPTION("Negative number of key columns");
+ }
+ if (count > MaxKeyColumnCount) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::TooManyColumnsInKey,
+ "Too many columns in key: actual %v, limit %v",
+ count,
+ MaxKeyColumnCount);
+ }
+}
+
+void ValidateRowCount(int count)
+{
+ if (count < 0) {
+ THROW_ERROR_EXCEPTION("Negative number of rows in rowset");
+ }
+ if (count > MaxRowsPerRowset) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::TooManyRowsInRowset,
+ "Too many rows in rowset: actual %v, limit %v",
+ count,
+ MaxRowsPerRowset);
+ }
+}
+
+void ValidateClientDataRow(
+ TUnversionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable,
+ std::optional<int> tabletIndexColumnId,
+ bool allowMissingKeyColumns)
+{
+ ValidateClientRow(row, schema, idMapping, nameTable, false, allowMissingKeyColumns, tabletIndexColumnId);
+}
+
+void ValidateDuplicateAndRequiredValueColumns(
+ TUnversionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer)
+{
+ auto& columnSeen = *columnPresenceBuffer;
+ YT_VERIFY(std::ssize(columnSeen) >= schema.GetColumnCount());
+ std::fill(columnSeen.begin(), columnSeen.end(), 0);
+
+ for (const auto& value : row) {
+ int mappedId = ApplyIdMapping(value, &idMapping);
+ if (mappedId < 0) {
+ continue;
+ }
+ const auto& column = schema.Columns()[mappedId];
+
+ if (columnSeen[mappedId]) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::DuplicateColumnInSchema,
+ "Duplicate column %v in table schema",
+ column.GetDiagnosticNameString());
+ }
+ columnSeen[mappedId] = true;
+ }
+
+ for (int index = schema.GetKeyColumnCount(); index < schema.GetColumnCount(); ++index) {
+ if (!columnSeen[index] && schema.Columns()[index].Required()) {
+ THROW_ERROR_EXCEPTION(
+ NTableClient::EErrorCode::MissingRequiredColumnInSchema,
+ "Missing required column %v in table schema",
+ schema.Columns()[index].GetDiagnosticNameString());
+ }
+ }
+}
+
+void ValidateClientKey(TLegacyKey key)
+{
+ for (const auto& value : key) {
+ ValidateKeyValue(value);
+ }
+}
+
+void ValidateClientKey(
+ TLegacyKey key,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable)
+{
+ ValidateClientRow(key, schema, idMapping, nameTable, true);
+}
+
+void ValidateReadTimestamp(TTimestamp timestamp)
+{
+ if (timestamp != SyncLastCommittedTimestamp &&
+ timestamp != AsyncLastCommittedTimestamp &&
+ (timestamp < MinTimestamp || timestamp > MaxTimestamp))
+ {
+ THROW_ERROR_EXCEPTION("Invalid read timestamp %x", timestamp);
+ }
+}
+
+void ValidateGetInSyncReplicasTimestamp(TTimestamp timestamp)
+{
+ if (timestamp != SyncLastCommittedTimestamp &&
+ (timestamp < MinTimestamp || timestamp > MaxTimestamp))
+ {
+ THROW_ERROR_EXCEPTION("Invalid GetInSyncReplicas timestamp %x", timestamp);
+ }
+}
+
+void ValidateWriteTimestamp(TTimestamp timestamp)
+{
+ if (timestamp < MinTimestamp || timestamp > MaxTimestamp) {
+ THROW_ERROR_EXCEPTION("Invalid write timestamp %x", timestamp);
+ }
+}
+
+int ApplyIdMapping(
+ const TUnversionedValue& value,
+ const TNameTableToSchemaIdMapping* idMapping)
+{
+ auto valueId = value.Id;
+ if (idMapping) {
+ const auto& idMapping_ = *idMapping;
+ if (valueId >= idMapping_.size()) {
+ THROW_ERROR_EXCEPTION("Invalid column id during remapping: expected in range [0, %v), got %v",
+ idMapping_.size(),
+ valueId);
+ }
+ return idMapping_[valueId];
+ } else {
+ return valueId;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLegacyOwningKey GetKeySuccessorImpl(TLegacyKey key, ui32 prefixLength, EValueType sentinelType)
+{
+ auto length = std::min(prefixLength, key.GetCount());
+ TUnversionedOwningRowBuilder builder(length + 1);
+ for (int index = 0; index < static_cast<int>(length); ++index) {
+ builder.AddValue(key[index]);
+ }
+ builder.AddValue(MakeUnversionedSentinelValue(sentinelType));
+ return builder.FinishRow();
+}
+
+TLegacyKey GetKeySuccessorImpl(TLegacyKey key, ui32 prefixLength, EValueType sentinelType, const TRowBufferPtr& rowBuffer)
+{
+ auto length = std::min(prefixLength, key.GetCount());
+ auto result = rowBuffer->AllocateUnversioned(length + 1);
+ for (int index = 0; index < static_cast<int>(length); ++index) {
+ result[index] = rowBuffer->CaptureValue(key[index]);
+ }
+ result[length] = MakeUnversionedSentinelValue(sentinelType);
+ return result;
+}
+
+TLegacyOwningKey GetKeySuccessor(TLegacyKey key)
+{
+ return GetKeySuccessorImpl(
+ key,
+ key.GetCount(),
+ EValueType::Min);
+}
+
+TLegacyKey GetKeySuccessor(TLegacyKey key, const TRowBufferPtr& rowBuffer)
+{
+ return GetKeySuccessorImpl(
+ key,
+ key.GetCount(),
+ EValueType::Min,
+ rowBuffer);
+}
+
+TLegacyOwningKey GetKeyPrefixSuccessor(TLegacyKey key, ui32 prefixLength)
+{
+ return GetKeySuccessorImpl(
+ key,
+ prefixLength,
+ EValueType::Max);
+}
+
+TLegacyKey GetKeyPrefixSuccessor(TLegacyKey key, ui32 prefixLength, const TRowBufferPtr& rowBuffer)
+{
+ return GetKeySuccessorImpl(
+ key,
+ prefixLength,
+ EValueType::Max,
+ rowBuffer);
+}
+
+TLegacyOwningKey GetKeyPrefix(TLegacyKey key, ui32 prefixLength)
+{
+ return TLegacyOwningKey(key.FirstNElements(std::min(key.GetCount(), prefixLength)));
+}
+
+TLegacyKey GetKeyPrefix(TLegacyKey key, ui32 prefixLength, const TRowBufferPtr& rowBuffer)
+{
+ return rowBuffer->CaptureRow(key.FirstNElements(std::min(key.GetCount(), prefixLength)));
+}
+
+TLegacyKey GetStrictKey(TLegacyKey key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType)
+{
+ if (key.GetCount() > keyColumnCount) {
+ return GetKeyPrefix(key, keyColumnCount, rowBuffer);
+ } else {
+ return WidenKey(key, keyColumnCount, rowBuffer, sentinelType);
+ }
+}
+
+TLegacyKey GetStrictKeySuccessor(TLegacyKey key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType)
+{
+ if (key.GetCount() >= keyColumnCount) {
+ return GetKeyPrefixSuccessor(key, keyColumnCount, rowBuffer);
+ } else {
+ return WidenKeySuccessor(key, keyColumnCount, rowBuffer, sentinelType);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static TLegacyOwningKey MakeSentinelKey(EValueType type)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedSentinelValue(type));
+ return builder.FinishRow();
+}
+
+static const TLegacyOwningKey CachedMinKey = MakeSentinelKey(EValueType::Min);
+static const TLegacyOwningKey CachedMaxKey = MakeSentinelKey(EValueType::Max);
+
+const TLegacyOwningKey MinKey()
+{
+ return CachedMinKey;
+}
+
+const TLegacyOwningKey MaxKey()
+{
+ return CachedMaxKey;
+}
+
+static TLegacyOwningKey MakeEmptyKey()
+{
+ TUnversionedOwningRowBuilder builder;
+ return builder.FinishRow();
+}
+
+static const TLegacyOwningKey CachedEmptyKey = MakeEmptyKey();
+
+const TLegacyOwningKey EmptyKey()
+{
+ return CachedEmptyKey;
+}
+
+const TLegacyOwningKey& ChooseMinKey(const TLegacyOwningKey& a, const TLegacyOwningKey& b)
+{
+ int result = CompareRows(a, b);
+ return result <= 0 ? a : b;
+}
+
+const TLegacyOwningKey& ChooseMaxKey(const TLegacyOwningKey& a, const TLegacyOwningKey& b)
+{
+ int result = CompareRows(a, b);
+ return result >= 0 ? a : b;
+}
+
+void ToProto(TProtoStringType* protoRow, TUnversionedRow row)
+{
+ *protoRow = SerializeToString(row);
+}
+
+void ToProto(TProtoStringType* protoRow, const TUnversionedOwningRow& row)
+{
+ ToProto(protoRow, row.Get());
+}
+
+void ToProto(TProtoStringType* protoRow, TUnversionedValueRange range)
+{
+ *protoRow = SerializeToString(range);
+}
+
+void FromProto(TUnversionedOwningRow* row, const TProtoStringType& protoRow, std::optional<int> nullPaddingWidth)
+{
+ *row = DeserializeFromString(TString{protoRow}, nullPaddingWidth);
+}
+
+void FromProto(TUnversionedRow* row, const TProtoStringType& protoRow, const TRowBufferPtr& rowBuffer)
+{
+ if (protoRow == SerializedNullRow) {
+ *row = TUnversionedRow();
+ return;
+ }
+
+ const char* current = protoRow.data();
+
+ ui32 version;
+ current += ReadVarUint32(current, &version);
+ YT_VERIFY(version == 0);
+
+ ui32 valueCount;
+ current += ReadVarUint32(current, &valueCount);
+
+ auto mutableRow = rowBuffer->AllocateUnversioned(valueCount);
+ *row = mutableRow;
+
+ auto* values = mutableRow.Begin();
+ for (auto* value = values; value < values + valueCount; ++value) {
+ current += ReadRowValue(current, value);
+ rowBuffer->CaptureValue(value);
+ }
+}
+
+void ToBytes(TString* bytes, const TUnversionedOwningRow& row)
+{
+ *bytes = SerializeToString(row);
+}
+
+void FromBytes(TUnversionedOwningRow* row, TStringBuf bytes)
+{
+ *row = DeserializeFromString(TString(bytes));
+}
+
+void PrintTo(const TUnversionedOwningRow& key, ::std::ostream* os)
+{
+ *os << KeyToYson(key);
+}
+
+void PrintTo(const TUnversionedRow& value, ::std::ostream* os)
+{
+ *os << ToString(value);
+}
+
+TLegacyOwningKey RowToKey(
+ const TTableSchema& schema,
+ TUnversionedRow row)
+{
+ TUnversionedOwningRowBuilder builder;
+ for (int index = 0; index < schema.GetKeyColumnCount(); ++index) {
+ builder.AddValue(row[index]);
+ }
+ return builder.FinishRow();
+}
+
+namespace {
+
+template <class TRow>
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRowsImpl(
+ TRange<TRow> rows,
+ TRefCountedTypeCookie tagCookie)
+{
+ size_t bufferSize = 0;
+ bufferSize += sizeof (TUnversionedRow) * rows.Size();
+ for (auto row : rows) {
+ bufferSize += GetUnversionedRowByteSize(row.GetCount());
+ for (const auto& value : row) {
+ if (IsStringLikeType(value.Type)) {
+ bufferSize += value.Length;
+ }
+ }
+ }
+ auto buffer = TSharedMutableRef::Allocate(bufferSize, {.InitializeStorage = false}, tagCookie);
+
+ char* alignedPtr = buffer.Begin();
+ auto allocateAligned = [&] (size_t size) {
+ auto* result = alignedPtr;
+ alignedPtr += size;
+ return result;
+ };
+
+ char* unalignedPtr = buffer.End();
+ auto allocateUnaligned = [&] (size_t size) {
+ unalignedPtr -= size;
+ return unalignedPtr;
+ };
+
+ auto* capturedRows = reinterpret_cast<TUnversionedRow*>(allocateAligned(sizeof (TUnversionedRow) * rows.Size()));
+ for (size_t index = 0; index < rows.Size(); ++index) {
+ auto row = rows[index];
+ int valueCount = row.GetCount();
+ auto* capturedHeader = reinterpret_cast<TUnversionedRowHeader*>(allocateAligned(GetUnversionedRowByteSize(valueCount)));
+ capturedHeader->Capacity = valueCount;
+ capturedHeader->Count = valueCount;
+ auto capturedRow = TMutableUnversionedRow(capturedHeader);
+ capturedRows[index] = capturedRow;
+ ::memcpy(capturedRow.Begin(), row.Begin(), sizeof (TUnversionedValue) * row.GetCount());
+ for (auto& capturedValue : capturedRow) {
+ if (IsStringLikeType(capturedValue.Type)) {
+ auto* capturedString = allocateUnaligned(capturedValue.Length);
+ ::memcpy(capturedString, capturedValue.Data.String, capturedValue.Length);
+ capturedValue.Data.String = capturedString;
+ }
+ }
+ }
+
+ YT_VERIFY(alignedPtr == unalignedPtr);
+
+ return {
+ MakeSharedRange(
+ MakeRange(capturedRows, rows.Size()),
+ std::move(buffer.ReleaseHolder())),
+ bufferSize
+ };
+}
+
+} // namespace
+
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRows(
+ TRange<TUnversionedRow> rows,
+ TRefCountedTypeCookie tagCookie)
+{
+ return CaptureRowsImpl(rows, tagCookie);
+}
+
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRows(
+ TRange<TUnversionedOwningRow> rows,
+ TRefCountedTypeCookie tagCookie)
+{
+ return CaptureRowsImpl(rows, tagCookie);
+}
+
+void Serialize(const TUnversionedValue& value, IYsonConsumer* consumer, bool anyAsRaw)
+{
+ auto type = value.Type;
+ switch (type) {
+ case EValueType::Int64:
+ consumer->OnInt64Scalar(value.Data.Int64);
+ break;
+
+ case EValueType::Uint64:
+ consumer->OnUint64Scalar(value.Data.Uint64);
+ break;
+
+ case EValueType::Double:
+ consumer->OnDoubleScalar(value.Data.Double);
+ break;
+
+ case EValueType::Boolean:
+ consumer->OnBooleanScalar(value.Data.Boolean);
+ break;
+
+ case EValueType::String:
+ consumer->OnStringScalar(value.AsStringBuf());
+ break;
+
+ case EValueType::Any:
+ if (anyAsRaw) {
+ consumer->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ } else {
+ ParseYsonStringBuffer(value.AsStringBuf(), EYsonType::Node, consumer);
+ }
+ break;
+
+ case EValueType::Null:
+ consumer->OnEntity();
+ break;
+
+ case EValueType::Composite:
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("type");
+ consumer->OnStringScalar(FormatEnum(type));
+ consumer->OnEndAttributes();
+ if (anyAsRaw) {
+ consumer->OnRaw(value.AsStringBuf(), EYsonType::Node);
+ } else {
+ ParseYsonStringBuffer(value.AsStringBuf(), EYsonType::Node, consumer);
+ }
+ break;
+
+ default:
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("type");
+ consumer->OnStringScalar(FormatEnum(type));
+ consumer->OnEndAttributes();
+ consumer->OnEntity();
+ break;
+ }
+}
+
+void Serialize(TLegacyKey key, IYsonConsumer* consumer)
+{
+ consumer->OnBeginList();
+ for (const auto& value : key) {
+ consumer->OnListItem();
+ Serialize(value, consumer);
+ }
+ consumer->OnEndList();
+}
+
+void Serialize(const TLegacyOwningKey& key, IYsonConsumer* consumer)
+{
+ return Serialize(key.Get(), consumer);
+}
+
+void Deserialize(TLegacyOwningKey& key, INodePtr node)
+{
+ if (node->GetType() != ENodeType::List) {
+ THROW_ERROR_EXCEPTION("Key cannot be parsed from %Qlv",
+ node->GetType());
+ }
+
+ TUnversionedOwningRowBuilder builder;
+ int id = 0;
+ for (const auto& item : node->AsList()->GetChildren()) {
+ try {
+ switch (item->GetType()) {
+ #define XX(type, cppType) \
+ case ENodeType::type: \
+ builder.AddValue(MakeUnversioned ## type ## Value(item->As ## type()->GetValue(), id)); \
+ break;
+ ITERATE_SCALAR_YTREE_NODE_TYPES(XX)
+ #undef XX
+
+ case ENodeType::Entity: {
+ auto valueType = item->Attributes().Get<EValueType>("type", EValueType::Null);
+ if (valueType != EValueType::Null && !IsSentinelType(valueType)) {
+ THROW_ERROR_EXCEPTION("Entities can only represent %Qlv and sentinel values but "
+ "not values of type %Qlv",
+ EValueType::Null,
+ valueType);
+ }
+ builder.AddValue(MakeUnversionedSentinelValue(valueType, id));
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Key cannot contain %Qlv values",
+ item->GetType());
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error deserializing key component #%v", id)
+ << ex;
+ }
+ ++id;
+ }
+ key = builder.FinishRow();
+}
+
+void Deserialize(TLegacyOwningKey& key, TYsonPullParserCursor* cursor)
+{
+ // TODO(levysotsky): Speed up?
+ Deserialize(key, ExtractTo<INodePtr>(cursor));
+}
+
+void TUnversionedOwningRow::Save(TStreamSaveContext& context) const
+{
+ NYT::Save(context, SerializeToString(Get()));
+}
+
+void TUnversionedOwningRow::Load(TStreamLoadContext& context)
+{
+ *this = DeserializeFromString(NYT::Load<TString>(context));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedRowBuilder::TUnversionedRowBuilder(int initialValueCapacity /*= 16*/)
+{
+ RowData_.resize(GetUnversionedRowByteSize(initialValueCapacity));
+ Reset();
+ GetHeader()->Capacity = initialValueCapacity;
+}
+
+int TUnversionedRowBuilder::AddValue(const TUnversionedValue& value)
+{
+ auto* header = GetHeader();
+ if (header->Count == header->Capacity) {
+ auto valueCapacity = 2 * std::max(1U, header->Capacity);
+ RowData_.resize(GetUnversionedRowByteSize(valueCapacity));
+ header = GetHeader();
+ header->Capacity = valueCapacity;
+ }
+
+ *GetValue(header->Count) = value;
+ return header->Count++;
+}
+
+TMutableUnversionedRow TUnversionedRowBuilder::GetRow()
+{
+ return TMutableUnversionedRow(GetHeader());
+}
+
+void TUnversionedRowBuilder::Reset()
+{
+ auto* header = GetHeader();
+ header->Count = 0;
+}
+
+TUnversionedRowHeader* TUnversionedRowBuilder::GetHeader()
+{
+ return reinterpret_cast<TUnversionedRowHeader*>(RowData_.data());
+}
+
+TUnversionedValue* TUnversionedRowBuilder::GetValue(ui32 index)
+{
+ return reinterpret_cast<TUnversionedValue*>(GetHeader() + 1) + index;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedOwningRowBuilder::TUnversionedOwningRowBuilder(int initialValueCapacity /*= 16*/)
+ : InitialValueCapacity_(initialValueCapacity)
+{
+ Reset();
+}
+
+int TUnversionedOwningRowBuilder::AddValue(const TUnversionedValue& value)
+{
+ auto* header = GetHeader();
+ if (header->Count == header->Capacity) {
+ auto valueCapacity = 2 * std::max(1U, header->Capacity);
+ RowData_.Resize(GetUnversionedRowByteSize(valueCapacity));
+ header = GetHeader();
+ header->Capacity = valueCapacity;
+ }
+
+ auto* newValue = GetValue(header->Count);
+ *newValue = value;
+
+ if (IsStringLikeType(value.Type)) {
+ const char* oldStringDataPtr = StringData_.Begin();
+ auto oldStringDataLength = StringData_.Size();
+ StringData_.Append(value.Data.String, value.Length);
+ const char* newStringDataPtr = StringData_.Begin();
+ newValue->Data.String = newStringDataPtr + oldStringDataLength;
+ if (newStringDataPtr != oldStringDataPtr) {
+ for (int index = 0; index < static_cast<int>(header->Count); ++index) {
+ auto* existingValue = GetValue(index);
+ if (IsStringLikeType(existingValue->Type)) {
+ existingValue->Data.String = newStringDataPtr + (existingValue->Data.String - oldStringDataPtr);
+ }
+ }
+ }
+ }
+
+ return header->Count++;
+}
+
+TUnversionedValue* TUnversionedOwningRowBuilder::BeginValues()
+{
+ return reinterpret_cast<TUnversionedValue*>(GetHeader() + 1);
+}
+
+TUnversionedValue* TUnversionedOwningRowBuilder::EndValues()
+{
+ return BeginValues() + GetHeader()->Count;
+}
+
+TUnversionedOwningRow TUnversionedOwningRowBuilder::FinishRow()
+{
+ auto row = TUnversionedOwningRow(
+ TSharedMutableRef::FromBlob(std::move(RowData_)),
+ TSharedRef::FromBlob(std::move(StringData_)));
+ Reset();
+ return row;
+}
+
+void TUnversionedOwningRowBuilder::Reset()
+{
+ RowData_.Resize(GetUnversionedRowByteSize(InitialValueCapacity_));
+
+ auto* header = GetHeader();
+ header->Count = 0;
+ header->Capacity = InitialValueCapacity_;
+}
+
+TUnversionedRowHeader* TUnversionedOwningRowBuilder::GetHeader()
+{
+ return reinterpret_cast<TUnversionedRowHeader*>(RowData_.Begin());
+}
+
+TUnversionedValue* TUnversionedOwningRowBuilder::GetValue(ui32 index)
+{
+ return reinterpret_cast<TUnversionedValue*>(GetHeader() + 1) + index;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TUnversionedOwningRow::Init(TUnversionedValueRange range)
+{
+ int count = std::ssize(range);
+
+ size_t fixedSize = GetUnversionedRowByteSize(count);
+ RowData_ = TSharedMutableRef::Allocate<TOwningRowTag>(fixedSize, {.InitializeStorage = false});
+ auto* header = GetHeader();
+
+ header->Count = count;
+ header->Capacity = count;
+ ::memcpy(header + 1, range.begin(), sizeof(TUnversionedValue) * range.size());
+
+ size_t variableSize = 0;
+ for (const auto& otherValue : range) {
+ if (IsStringLikeType(otherValue.Type)) {
+ variableSize += otherValue.Length;
+ }
+ }
+
+ if (variableSize > 0) {
+ TBlob stringData;
+ stringData.Resize(variableSize);
+ char* current = stringData.Begin();
+ for (int index = 0; index < count; ++index) {
+ const auto& otherValue = range[index];
+ auto& value = reinterpret_cast<TUnversionedValue*>(header + 1)[index];
+ if (IsStringLikeType(otherValue.Type)) {
+ ::memcpy(current, otherValue.Data.String, otherValue.Length);
+ value.Data.String = current;
+ current += otherValue.Length;
+ }
+ }
+ StringData_ = TSharedRef::FromBlob(std::move(stringData));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLegacyOwningKey WidenKey(const TLegacyOwningKey& key, ui32 keyColumnCount, EValueType sentinelType)
+{
+ return WidenKeyPrefix(key, key.GetCount(), keyColumnCount, sentinelType);
+}
+
+TLegacyKey WidenKey(const TLegacyKey& key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType)
+{
+ return WidenKeyPrefix(key, key.GetCount(), keyColumnCount, rowBuffer, sentinelType);
+}
+
+TLegacyOwningKey WidenKeySuccessor(const TLegacyOwningKey& key, ui32 keyColumnCount, EValueType sentinelType)
+{
+ YT_VERIFY(static_cast<int>(keyColumnCount) >= key.GetCount());
+
+ TUnversionedOwningRowBuilder builder;
+ for (int index = 0; index < key.GetCount(); ++index) {
+ builder.AddValue(key[index]);
+ }
+
+ for (ui32 index = key.GetCount(); index < keyColumnCount; ++index) {
+ builder.AddValue(MakeUnversionedSentinelValue(sentinelType));
+ }
+
+ builder.AddValue(MakeUnversionedSentinelValue(EValueType::Max));
+
+ return builder.FinishRow();
+}
+
+TLegacyKey WidenKeySuccessor(const TLegacyKey& key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType)
+{
+ YT_VERIFY(keyColumnCount >= key.GetCount());
+
+ auto wideKey = rowBuffer->AllocateUnversioned(keyColumnCount + 1);
+
+ for (ui32 index = 0; index < key.GetCount(); ++index) {
+ wideKey[index] = rowBuffer->CaptureValue(key[index]);
+ }
+
+ for (ui32 index = key.GetCount(); index < keyColumnCount; ++index) {
+ wideKey[index] = MakeUnversionedSentinelValue(sentinelType);
+ }
+
+ wideKey[keyColumnCount] = MakeUnversionedSentinelValue(EValueType::Max);
+
+ return wideKey;
+}
+
+TLegacyOwningKey WidenKeyPrefix(const TLegacyOwningKey& key, ui32 prefixLength, ui32 keyColumnCount, EValueType sentinelType)
+{
+ YT_VERIFY(static_cast<int>(prefixLength) <= key.GetCount() && prefixLength <= keyColumnCount);
+
+ if (key.GetCount() == static_cast<int>(prefixLength) && prefixLength == keyColumnCount) {
+ return key;
+ }
+
+ TUnversionedOwningRowBuilder builder;
+ for (ui32 index = 0; index < prefixLength; ++index) {
+ builder.AddValue(key[index]);
+ }
+
+ for (ui32 index = prefixLength; index < keyColumnCount; ++index) {
+ builder.AddValue(MakeUnversionedSentinelValue(sentinelType));
+ }
+
+ return builder.FinishRow();
+}
+
+TLegacyKey WidenKeyPrefix(TLegacyKey key, ui32 prefixLength, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType)
+{
+ YT_VERIFY(prefixLength <= key.GetCount() && prefixLength <= keyColumnCount);
+
+ if (key.GetCount() == prefixLength && prefixLength == keyColumnCount) {
+ return rowBuffer->CaptureRow(key);
+ }
+
+ auto wideKey = rowBuffer->AllocateUnversioned(keyColumnCount);
+
+ for (ui32 index = 0; index < prefixLength; ++index) {
+ wideKey[index] = rowBuffer->CaptureValue(key[index]);
+ }
+
+ for (ui32 index = prefixLength; index < keyColumnCount; ++index) {
+ wideKey[index] = MakeUnversionedSentinelValue(sentinelType);
+ }
+
+ return wideKey;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRange<TRowRange> MakeSingletonRowRange(TLegacyKey lowerBound, TLegacyKey upperBound)
+{
+ auto rowBuffer = New<TRowBuffer>();
+ TCompactVector<TRowRange, 1> ranges(1, TRowRange(
+ rowBuffer->CaptureRow(lowerBound),
+ rowBuffer->CaptureRow(upperBound)));
+ return MakeSharedRange(
+ std::move(ranges),
+ std::move(rowBuffer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyRef ToKeyRef(TUnversionedRow row)
+{
+ return row.Elements();
+}
+
+TUnversionedValueRange ToKeyRef(TUnversionedRow row, int prefixLength)
+{
+ return row.FirstNElements(prefixLength);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, TUnversionedRow row, TStringBuf format)
+{
+ if (row) {
+ builder->AppendChar('[');
+ JoinToString(
+ builder,
+ row.Begin(),
+ row.End(),
+ [&] (TStringBuilderBase* builder, const TUnversionedValue& value) {
+ FormatValue(builder, value, format);
+ });
+ builder->AppendChar(']');
+ } else {
+ builder->AppendString("<null>");
+ }
+}
+
+void FormatValue(TStringBuilderBase* builder, TMutableUnversionedRow row, TStringBuf format)
+{
+ FormatValue(builder, TUnversionedRow(row), format);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TUnversionedOwningRow& row, TStringBuf format)
+{
+ FormatValue(builder, TUnversionedRow(row), format);
+}
+
+TString ToString(TUnversionedRow row, bool valuesOnly)
+{
+ return ToStringViaBuilder(row, valuesOnly ? "k" : "");
+}
+
+TString ToString(TMutableUnversionedRow row, bool valuesOnly)
+{
+ return ToString(TUnversionedRow(row), valuesOnly);
+}
+
+TString ToString(const TUnversionedOwningRow& row, bool valuesOnly)
+{
+ return ToString(row.Get(), valuesOnly);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TDefaultUnversionedValueRangeHash::operator()(TUnversionedValueRange range) const
+{
+ return GetFarmFingerprint(range);
+}
+
+bool TDefaultUnversionedValueRangeEqual::operator()(TUnversionedValueRange lhs, TUnversionedValueRange rhs) const
+{
+ if (lhs.size() != rhs.size()) {
+ return false;
+ }
+ for (size_t index = 0; index < lhs.size(); ++index) {
+ if (!TDefaultUnversionedValueEqual()(lhs[index], rhs[index])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TDefaultUnversionedRowHash::operator()(TUnversionedRow row) const
+{
+ return TDefaultUnversionedValueRangeHash()(row.Elements());
+}
+
+bool TDefaultUnversionedRowEqual::operator()(TUnversionedRow lhs, TUnversionedRow rhs) const
+{
+ return lhs == rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TBitwiseUnversionedValueRangeHash::operator()(TUnversionedValueRange range) const
+{
+ size_t result = 0;
+ for (const auto& value : range) {
+ HashCombine(result, TBitwiseUnversionedValueHash()(value));
+ }
+ return result;
+}
+
+bool TBitwiseUnversionedValueRangeEqual::operator()(TUnversionedValueRange lhs, TUnversionedValueRange rhs) const
+{
+ if (lhs.size() != rhs.size()) {
+ return false;
+ }
+ for (size_t index = 0; index < lhs.size(); ++index) {
+ if (!TBitwiseUnversionedValueEqual()(lhs[index], rhs[index])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TBitwiseUnversionedRowHash::operator()(TUnversionedRow row) const
+{
+ return TBitwiseUnversionedValueRangeHash()(row.Elements());
+}
+
+bool TBitwiseUnversionedRowEqual::operator()(TUnversionedRow lhs, TUnversionedRow rhs) const
+{
+ return TBitwiseUnversionedValueRangeEqual()(lhs.Elements(), rhs.Elements());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unversioned_row.h b/yt/yt/client/table_client/unversioned_row.h
new file mode 100644
index 0000000000..91e3d073ce
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_row.h
@@ -0,0 +1,965 @@
+#pragma once
+
+#include "public.h"
+#include "row_base.h"
+#include "unversioned_value.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/concurrency/fls.h>
+
+#include <yt/yt_proto/yt/core/misc/proto/guid.pb.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/memory/chunked_memory_pool.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const TString SerializedNullRow;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnversionedOwningValue
+{
+public:
+ TUnversionedOwningValue() = default;
+
+ TUnversionedOwningValue(TUnversionedOwningValue&& other)
+ {
+ std::swap(Value_, other.Value_);
+ }
+
+ TUnversionedOwningValue(const TUnversionedOwningValue& other)
+ {
+ Assign(other.Value_);
+ }
+
+ TUnversionedOwningValue(const TUnversionedValue& other)
+ {
+ Assign(other);
+ }
+
+ ~TUnversionedOwningValue()
+ {
+ Clear();
+ }
+
+ operator TUnversionedValue() const
+ {
+ return Value_;
+ }
+
+ TUnversionedOwningValue& operator = (TUnversionedOwningValue&& other)
+ {
+ std::swap(Value_, other.Value_);
+ return *this;
+ }
+
+ TUnversionedOwningValue& operator = (const TUnversionedOwningValue& other)
+ {
+ Clear();
+ Assign(other.Value_);
+ return *this;
+ }
+
+ TUnversionedOwningValue& operator = (const TUnversionedValue& other)
+ {
+ Clear();
+ Assign(other);
+ return *this;
+ }
+
+ void Clear()
+ {
+ if (IsStringLikeType(Value_.Type)) {
+ delete[] Value_.Data.String;
+ }
+ Value_.Type = EValueType::TheBottom;
+ Value_.Length = 0;
+ }
+
+private:
+ TUnversionedValue Value_{
+ .Id = 0,
+ .Type = EValueType::TheBottom,
+ .Flags = {},
+ .Length = 0,
+ .Data = {}
+ };
+
+ void Assign(const TUnversionedValue& other)
+ {
+ Value_ = other;
+ if (IsStringLikeType(Value_.Type)) {
+ auto newString = new char[Value_.Length];
+ ::memcpy(newString, Value_.Data.String, Value_.Length);
+ Value_.Data.String = newString;
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TUnversionedValue MakeUnversionedSentinelValue(EValueType type, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeSentinelValue<TUnversionedValue>(type, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedNullValue(int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeNullValue<TUnversionedValue>(id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedInt64Value(i64 value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeInt64Value<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedUint64Value(ui64 value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeUint64Value<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedDoubleValue(double value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeDoubleValue<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedBooleanValue(bool value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeBooleanValue<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedStringLikeValue(EValueType valueType, TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeStringLikeValue<TUnversionedValue>(valueType, value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedStringValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeStringValue<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedAnyValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeAnyValue<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedCompositeValue(TStringBuf value, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeCompositeValue<TUnversionedValue>(value, id, flags);
+}
+
+inline TUnversionedValue MakeUnversionedValueHeader(EValueType type, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ return MakeSentinelValue<TUnversionedValue>(type, id, flags);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TUnversionedRowHeader
+{
+ ui32 Count;
+ ui32 Capacity;
+};
+
+static_assert(
+ sizeof(TUnversionedRowHeader) == 8,
+ "TUnversionedRowHeader has to be exactly 8 bytes.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline size_t GetDataWeight(EValueType type)
+{
+ switch (type) {
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ return 0;
+
+ case EValueType::Int64:
+ return sizeof(i64);
+
+ case EValueType::Uint64:
+ return sizeof(ui64);
+
+ case EValueType::Double:
+ return sizeof(double);
+
+ case EValueType::Boolean:
+ return 1;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+inline size_t GetDataWeight(const TUnversionedValue& value)
+{
+ if (IsStringLikeType(value.Type)) {
+ return value.Length;
+ } else {
+ return GetDataWeight(value.Type);
+ }
+}
+
+size_t GetByteSize(const TUnversionedValue& value);
+size_t WriteRowValue(char* output, const TUnversionedValue& value, bool isInlineHunkValue = false);
+size_t ReadRowValue(const char* input, TUnversionedValue* value);
+
+void Save(TStreamSaveContext& context, const TUnversionedValue& value);
+void Load(TStreamLoadContext& context, TUnversionedValue& value, TChunkedMemoryPool* pool);
+
+//! Ternary comparison predicate for TUnversionedValue-s.
+//! Returns zero, positive or negative value depending on the outcome.
+//! Note that this ignores flags.
+int CompareRowValues(const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+
+//! Derived comparison operators.
+//! Note that these ignore flags.
+bool operator == (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+bool operator != (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+bool operator <= (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+bool operator < (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+bool operator >= (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+bool operator > (const TUnversionedValue& lhs, const TUnversionedValue& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Ternary comparison predicate for value ranges.
+//! Note that this ignores aggregate flags.
+int CompareValueRanges(
+ TUnversionedValueRange lhs,
+ TUnversionedValueRange rhs);
+
+//! Ternary comparison predicate for TUnversionedRow-s stripped to a given number of
+//! (leading) values.
+//! Note that this ignores aggregate flags.
+int CompareRows(
+ TUnversionedRow lhs,
+ TUnversionedRow rhs,
+ ui32 prefixLength = std::numeric_limits<ui32>::max());
+
+//! Derived comparison operators.
+//! Note that these ignore aggregate flags.
+bool operator == (TUnversionedRow lhs, TUnversionedRow rhs);
+bool operator != (TUnversionedRow lhs, TUnversionedRow rhs);
+bool operator <= (TUnversionedRow lhs, TUnversionedRow rhs);
+bool operator < (TUnversionedRow lhs, TUnversionedRow rhs);
+bool operator >= (TUnversionedRow lhs, TUnversionedRow rhs);
+bool operator > (TUnversionedRow lhs, TUnversionedRow rhs);
+
+//! Computes FarmHash forever-fixed fingerprint for a range of values.
+TFingerprint GetFarmFingerprint(TUnversionedValueRange range);
+
+//! Computes FarmHash forever-fixed fingerprint for an unversioned row.
+TFingerprint GetFarmFingerprint(TUnversionedRow row);
+
+//! Returns the number of bytes needed to store an unversioned row (not including string data).
+size_t GetUnversionedRowByteSize(ui32 valueCount);
+
+//! Returns the storage-invariant data weight of a given row.
+size_t GetDataWeight(TUnversionedRow row);
+
+size_t GetDataWeight(TRange<TUnversionedRow> rows);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A row with unversioned data.
+/*!
+ * A lightweight wrapper around |TUnversionedRowHeader*|.
+ *
+ * Provides access to a sequence of unversioned values.
+ * If data is schemaful then the positions of values must exactly match their ids.
+ *
+ * Memory layout:
+ * 1) TUnversionedRowHeader
+ * 2) TUnversionedValue per each value (#TUnversionedRowHeader::Count)
+ */
+class TUnversionedRow
+{
+public:
+ TUnversionedRow() = default;
+
+ explicit TUnversionedRow(const TUnversionedRowHeader* header)
+ : Header_(header)
+ { }
+
+ explicit TUnversionedRow(TTypeErasedRow erased)
+ : Header_(reinterpret_cast<const TUnversionedRowHeader*>(erased.OpaqueHeader))
+ { }
+
+ explicit operator bool() const
+ {
+ return Header_ != nullptr;
+ }
+
+ TTypeErasedRow ToTypeErasedRow() const
+ {
+ return {Header_};
+ }
+
+ const TUnversionedRowHeader* GetHeader() const
+ {
+ return Header_;
+ }
+
+ const TUnversionedValue* Begin() const
+ {
+ return reinterpret_cast<const TUnversionedValue*>(Header_ + 1);
+ }
+
+ const TUnversionedValue* End() const
+ {
+ return Begin() + GetCount();
+ }
+
+ TUnversionedValueRange Elements() const
+ {
+ return {Begin(), End()};
+ }
+
+ TUnversionedValueRange FirstNElements(int count) const
+ {
+ YT_ASSERT(count <= static_cast<int>(GetCount()));
+ return {Begin(), Begin() + count};
+ }
+
+ const TUnversionedValue& operator[] (int index) const
+ {
+ YT_ASSERT(index >= 0 && static_cast<ui32>(index) < GetCount());
+ return Begin()[index];
+ }
+
+ ui32 GetCount() const
+ {
+ return Header_ ? Header_->Count : 0u;
+ }
+
+ // STL interop.
+ const TUnversionedValue* begin() const
+ {
+ return Begin();
+ }
+
+ const TUnversionedValue* end() const
+ {
+ return End();
+ }
+
+ void Save(TSaveContext& context) const;
+ void Load(TLoadContext& context);
+
+private:
+ const TUnversionedRowHeader* Header_ = nullptr;
+};
+
+static_assert(
+ sizeof(TUnversionedRow) == sizeof(intptr_t),
+ "TUnversionedRow size must match that of a pointer.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Checks that #value type is compatible with the schema column type.
+/*
+ * If #typeAnyAcceptsAllValues is false columns of type EValueType::ANY accept only values of same type.
+ * If #typeAnyAcceptsAllValues is true then columns of type EValueType::ANY accept all values.
+ *
+ * \note Function throws an error if column has `Any' type and value has `non-Any' type.
+ */
+void ValidateValueType(
+ const TUnversionedValue& value,
+ const TTableSchema& schema,
+ int schemaId,
+ bool typeAnyAcceptsAllValues,
+ bool ignoreRequired = false,
+ bool validateAnyIsValidYson = false);
+void ValidateValueType(
+ const TUnversionedValue& value,
+ const TColumnSchema& columnSchema,
+ bool typeAnyAcceptsAllValues,
+ bool ignoreRequired = false,
+ bool validateAnyIsValidYson = false);
+
+//! Checks that #value is allowed to appear in static tables' data. Throws on failure.
+void ValidateStaticValue(const TUnversionedValue& value);
+
+//! Checks that #value is allowed to appear in dynamic tables' data. Throws on failure.
+void ValidateDataValue(const TUnversionedValue& value);
+
+//! Checks that #value is allowed to appear in dynamic tables' keys. Throws on failure.
+void ValidateKeyValue(const TUnversionedValue& value);
+
+//! Checks that #count represents an allowed number of values in a row. Throws on failure.
+void ValidateRowValueCount(int count);
+
+//! Checks that #count represents an allowed number of components in a key. Throws on failure.
+void ValidateKeyColumnCount(int count);
+
+//! Checks that #count represents an allowed number of rows in a rowset. Throws on failure.
+void ValidateRowCount(int count);
+
+//! Checks that #row is a valid client-side data row. Throws on failure.
+/*!
+ * Value ids in the row are first mapped via #idMapping.
+ * The row must obey the following properties:
+ * 1. Its value count must pass #ValidateRowValueCount checks.
+ * 2. It must contain all key components (values with ids in range [0, #schema.GetKeyColumnCount() - 1]).
+ * 3. Value types must either be null or match those given in #schema.
+ * 4. For values marked with #TUnversionedValue::Aggregate flag, the corresponding columns in #schema must
+ * be aggregating.
+ * 5. Versioned values must be sorted by |id| (in ascending order) and then by |timestamp| (in descending order).
+ * 6. At least one non-key column must be specified.
+ */
+void ValidateClientDataRow(
+ TUnversionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable,
+ std::optional<int> tabletIndexColumnId = std::nullopt,
+ bool allowMissingKeyColumns = false);
+
+//! Checks that #row contains no duplicate non-key columns and that all required columns are present. Skip values that map to negative ids via #idMapping.
+/*! It is assumed that ValidateClientDataRow was called before. */
+void ValidateDuplicateAndRequiredValueColumns(
+ TUnversionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer);
+
+//! Checks that #key is a valid client-side key. Throws on failure.
+/*! The components must pass #ValidateKeyValue check. */
+void ValidateClientKey(TLegacyKey key);
+
+//! Checks that #key is a valid client-side key. Throws on failure.
+/*! The key must obey the following properties:
+ * 1. It cannot be null.
+ * 2. It must contain exactly #schema.GetKeyColumnCount() components.
+ * 3. Value ids must be a permutation of {0, ..., #schema.GetKeyColumnCount() - 1}.
+ * 4. Value types must either be null of match those given in schema.
+ */
+void ValidateClientKey(
+ TLegacyKey key,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable);
+
+//! Checks if #timestamp is sane and can be used for data.
+//! Allows timestamps in range [MinTimestamp, MaxTimestamp] plus some sentinels
+//! (SyncLastCommittedTimestamp and AsyncLastCommittedTimestamp).
+void ValidateReadTimestamp(TTimestamp timestamp);
+
+//! Checks if #timestamp is sane and can be used for replica synchronization.
+//! Allows timestamps in range [MinTimestamp, MaxTimestamp].
+void ValidateGetInSyncReplicasTimestamp(TTimestamp timestamp);
+
+//! Checks if #timestamp is sane and can be used for writing (versioned) data.
+//! Allows timestamps in range [MinTimestamp, MaxTimestamp].
+void ValidateWriteTimestamp(TTimestamp timestamp);
+
+//! An internal helper used by validators.
+int ApplyIdMapping(
+ const TUnversionedValue& value,
+ const TNameTableToSchemaIdMapping* idMapping);
+
+//! Returns the successor of |key|, i.e. the key obtained from |key|
+//! by appending a |EValueType::Min| sentinel.
+TLegacyOwningKey GetKeySuccessor(TLegacyKey key);
+TLegacyKey GetKeySuccessor(TLegacyKey key, const TRowBufferPtr& rowBuffer);
+
+//! Returns the successor of |key| trimmed to a given length, i.e. the key
+//! obtained by trimming |key| to |prefixLength| and appending
+//! a |EValueType::Max| sentinel.
+TLegacyOwningKey GetKeyPrefixSuccessor(TLegacyKey key, ui32 prefixLength);
+TLegacyKey GetKeyPrefixSuccessor(TLegacyKey key, ui32 prefixLength, const TRowBufferPtr& rowBuffer);
+
+//! Returns key of a strict length (either trimmed key or widened key)
+TLegacyKey GetStrictKey(TLegacyKey key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType = EValueType::Null);
+TLegacyKey GetStrictKeySuccessor(TLegacyKey key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType = EValueType::Null);
+
+//! If #key has more than #prefixLength values then trims it this limit.
+TLegacyOwningKey GetKeyPrefix(TLegacyKey key, ui32 prefixLength);
+TLegacyKey GetKeyPrefix(TLegacyKey key, ui32 prefixLength, const TRowBufferPtr& rowBuffer);
+
+//! Makes a new, wider key padded with given sentinel values.
+TLegacyOwningKey WidenKey(const TLegacyOwningKey& key, ui32 keyColumnCount, EValueType sentinelType = EValueType::Null);
+TLegacyKey WidenKey(const TLegacyKey& key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType = EValueType::Null);
+
+//! Makes the key wider by appending given sentinel values up to |keyColumnCount| length
+//! and then appends a |EValueType::Max| sentinel.
+TLegacyOwningKey WidenKeySuccessor(const TLegacyOwningKey& key, ui32 keyColumnCount, EValueType sentinelType = EValueType::Null);
+TLegacyKey WidenKeySuccessor(const TLegacyKey& key, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType = EValueType::Null);
+
+//! Takes prefix of a key and makes it wider.
+TLegacyOwningKey WidenKeyPrefix(const TLegacyOwningKey& key, ui32 prefixLength, ui32 keyColumnCount, EValueType sentinelType = EValueType::Null);
+TLegacyKey WidenKeyPrefix(TLegacyKey key, ui32 prefixLength, ui32 keyColumnCount, const TRowBufferPtr& rowBuffer, EValueType sentinelType = EValueType::Null);
+
+//! Returns the key with no components.
+const TLegacyOwningKey EmptyKey();
+
+//! Returns the key with a single |Min| component.
+const TLegacyOwningKey MinKey();
+
+//! Returns the key with a single |Max| component.
+const TLegacyOwningKey MaxKey();
+
+//! Compares two keys, |a| and |b|, and returns a smaller one.
+//! Ties are broken in favour of the first argument.
+const TLegacyOwningKey& ChooseMinKey(const TLegacyOwningKey& a, const TLegacyOwningKey& b);
+
+//! Compares two keys, |a| and |b|, and returns a bigger one.
+//! Ties are broken in favour of the first argument.
+const TLegacyOwningKey& ChooseMaxKey(const TLegacyOwningKey& a, const TLegacyOwningKey& b);
+
+TString SerializeToString(TUnversionedRow row);
+TString SerializeToString(TUnversionedValueRange range);
+
+void ToProto(TProtoStringType* protoRow, TUnversionedRow row);
+void ToProto(TProtoStringType* protoRow, const TUnversionedOwningRow& row);
+void ToProto(TProtoStringType* protoRow, TUnversionedValueRange range);
+
+void FromProto(TUnversionedOwningRow* row, const TProtoStringType& protoRow, std::optional<int> nullPaddingWidth = {});
+void FromProto(TUnversionedRow* row, const TProtoStringType& protoRow, const TRowBufferPtr& rowBuffer);
+
+void ToBytes(TString* bytes, const TUnversionedOwningRow& row);
+
+void FromBytes(TUnversionedOwningRow* row, TStringBuf bytes);
+
+void Serialize(const TUnversionedValue& value, NYson::IYsonConsumer* consumer, bool anyAsRaw = false);
+void Serialize(TLegacyKey key, NYson::IYsonConsumer* consumer);
+void Serialize(const TLegacyOwningKey& key, NYson::IYsonConsumer* consumer);
+
+void Deserialize(TLegacyOwningKey& key, NYTree::INodePtr node);
+void Deserialize(TLegacyOwningKey& key, NYson::TYsonPullParserCursor* cursor);
+
+size_t GetYsonSize(const TUnversionedValue& value);
+size_t WriteYson(char* buffer, const TUnversionedValue& unversionedValue);
+
+//! Debug printers for Gtest unittests.
+void PrintTo(const TUnversionedOwningRow& key, ::std::ostream* os);
+void PrintTo(const TUnversionedRow& value, ::std::ostream* os);
+
+TLegacyOwningKey RowToKey(
+ const NTableClient::TTableSchema& schema,
+ TUnversionedRow row);
+
+//! Constructs a shared range of rows from a non-shared one.
+/*!
+ * The values contained in the rows are also captured.
+ * The underlying storage allocation has just the right size to contain the captured
+ * data and is marked with #tagCookie. The size of allocated space is returned via
+ * second value.
+ */
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRows(
+ TRange<TUnversionedRow> rows,
+ TRefCountedTypeCookie tagCookie);
+
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRows(
+ TRange<TUnversionedOwningRow> rows,
+ TRefCountedTypeCookie tagCookie);
+
+template <class TTag, class TRow>
+std::pair<TSharedRange<TUnversionedRow>, i64> CaptureRows(TRange<TRow> rows)
+{
+ return CaptureRows(rows, GetRefCountedTypeCookie<TTag>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A variant of TUnversionedRow that enables mutating access to its content.
+class TMutableUnversionedRow
+ : public TUnversionedRow
+{
+public:
+ TMutableUnversionedRow() = default;
+
+ explicit TMutableUnversionedRow(TUnversionedRowHeader* header)
+ : TUnversionedRow(header)
+ { }
+
+ explicit TMutableUnversionedRow(TTypeErasedRow row)
+ : TUnversionedRow(reinterpret_cast<const TUnversionedRowHeader*>(row.OpaqueHeader))
+ { }
+
+ static TMutableUnversionedRow Allocate(
+ TChunkedMemoryPool* pool,
+ ui32 valueCount);
+
+ static TMutableUnversionedRow Create(
+ void* buffer,
+ ui32 valueCount);
+
+ TUnversionedRowHeader* GetHeader()
+ {
+ return const_cast<TUnversionedRowHeader*>(TUnversionedRow::GetHeader());
+ }
+
+ TUnversionedValue* Begin()
+ {
+ return reinterpret_cast<TUnversionedValue*>(GetHeader() + 1);
+ }
+
+ TUnversionedValue* End()
+ {
+ return Begin() + GetCount();
+ }
+
+ const TUnversionedValue* Begin() const
+ {
+ return reinterpret_cast<const TUnversionedValue*>(TUnversionedRow::GetHeader() + 1);
+ }
+
+ const TUnversionedValue* End() const
+ {
+ return Begin() + GetCount();
+ }
+
+ TMutableUnversionedValueRange Elements()
+ {
+ return {Begin(), End()};
+ }
+
+ TMutableUnversionedValueRange FirstNElements(int count)
+ {
+ YT_ASSERT(count <= static_cast<int>(GetCount()));
+ return {Begin(), Begin() + count};
+ }
+
+ void SetCount(ui32 count)
+ {
+ YT_ASSERT(count <= GetHeader()->Capacity);
+ GetHeader()->Count = count;
+ }
+
+ TUnversionedValue& operator[] (ui32 index)
+ {
+ YT_ASSERT(index < GetHeader()->Count);
+ return Begin()[index];
+ }
+
+ // STL interop.
+ TUnversionedValue* begin()
+ {
+ return Begin();
+ }
+
+ TUnversionedValue* end()
+ {
+ return End();
+ }
+
+ const TUnversionedValue* begin() const
+ {
+ return Begin();
+ }
+
+ const TUnversionedValue* end() const
+ {
+ return End();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An owning variant of TUnversionedRow.
+/*!
+ * Instances of TUnversionedOwningRow are lightweight handles.
+ * Fixed part is stored in shared ref-counted blobs.
+ * Variable part is stored in shared strings.
+ */
+class TUnversionedOwningRow
+{
+public:
+ TUnversionedOwningRow() = default;
+
+ TUnversionedOwningRow(TUnversionedValueRange range)
+ {
+ Init(range);
+ }
+
+ explicit TUnversionedOwningRow(TUnversionedRow other)
+ {
+ if (other) {
+ Init(other.Elements());
+ }
+ }
+
+ TUnversionedOwningRow(const TUnversionedOwningRow& other)
+ : RowData_(other.RowData_)
+ , StringData_(other.StringData_)
+ { }
+
+ TUnversionedOwningRow(TUnversionedOwningRow&& other)
+ : RowData_(std::move(other.RowData_))
+ , StringData_(std::move(other.StringData_))
+ { }
+
+ explicit operator bool() const
+ {
+ return static_cast<bool>(RowData_);
+ }
+
+ operator TUnversionedRow() const
+ {
+ return Get();
+ }
+
+ TUnversionedRow Get() const
+ {
+ return TUnversionedRow(GetHeader());
+ }
+
+ const TUnversionedValue* Begin() const
+ {
+ const auto* header = GetHeader();
+ return header ? reinterpret_cast<const TUnversionedValue*>(header + 1) : nullptr;
+ }
+
+ const TUnversionedValue* End() const
+ {
+ return Begin() + GetCount();
+ }
+
+ TUnversionedValueRange Elements() const
+ {
+ return {Begin(), End()};
+ }
+
+ TUnversionedValueRange FirstNElements(int count) const
+ {
+ YT_ASSERT(count <= static_cast<int>(GetCount()));
+ return {Begin(), Begin() + count};
+ }
+
+ const TUnversionedValue& operator[] (int index) const
+ {
+ YT_ASSERT(index >= 0 && index < GetCount());
+ return Begin()[index];
+ }
+
+ int GetCount() const
+ {
+ const auto* header = GetHeader();
+ return header ? static_cast<int>(header->Count) : 0;
+ }
+
+ size_t GetSpaceUsed() const
+ {
+ return StringData_.GetHolder()->GetTotalByteSize().value_or(StringData_.Size()) +
+ RowData_.GetHolder()->GetTotalByteSize().value_or(RowData_.Size());
+ }
+
+
+ friend void swap(TUnversionedOwningRow& lhs, TUnversionedOwningRow& rhs)
+ {
+ using std::swap;
+
+ swap(lhs.RowData_, rhs.RowData_);
+ swap(lhs.StringData_, rhs.StringData_);
+ }
+
+ TUnversionedOwningRow& operator=(const TUnversionedOwningRow& other)
+ {
+ RowData_ = other.RowData_;
+ StringData_ = other.StringData_;
+ return *this;
+ }
+
+ TUnversionedOwningRow& operator=(TUnversionedOwningRow&& other)
+ {
+ RowData_ = std::move(other.RowData_);
+ StringData_ = std::move(other.StringData_);
+ return *this;
+ }
+
+ // STL interop.
+ const TUnversionedValue* begin() const
+ {
+ return Begin();
+ }
+
+ const TUnversionedValue* end() const
+ {
+ return End();
+ }
+
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ friend TLegacyOwningKey GetKeySuccessorImpl(const TLegacyOwningKey& key, int prefixLength, EValueType sentinelType);
+ friend TUnversionedOwningRow DeserializeFromString(TString&& data, std::optional<int> nullPaddingWidth);
+
+ friend class TUnversionedOwningRowBuilder;
+
+ TSharedMutableRef RowData_; // TRowHeader plus TValue-s
+ TSharedRef StringData_; // Holds string data
+
+
+ TUnversionedOwningRow(TSharedMutableRef&& rowData, TSharedRef&& stringData)
+ : RowData_(std::move(rowData))
+ , StringData_(std::move(stringData))
+ { }
+
+ void Init(TUnversionedValueRange range);
+
+ TUnversionedRowHeader* GetHeader()
+ {
+ return RowData_ ? reinterpret_cast<TUnversionedRowHeader*>(RowData_.Begin()) : nullptr;
+ }
+
+ const TUnversionedRowHeader* GetHeader() const
+ {
+ return RowData_ ? reinterpret_cast<const TUnversionedRowHeader*>(RowData_.Begin()) : nullptr;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper used for constructing TUnversionedRow instances.
+//! Only row values are kept, strings are only referenced.
+class TUnversionedRowBuilder
+{
+public:
+ static const int DefaultValueCapacity = 8;
+
+ explicit TUnversionedRowBuilder(int initialValueCapacity = DefaultValueCapacity);
+
+ int AddValue(const TUnversionedValue& value);
+ TMutableUnversionedRow GetRow();
+ void Reset();
+
+private:
+ static const int DefaultBlobCapacity =
+ sizeof(TUnversionedRowHeader) +
+ DefaultValueCapacity * sizeof(TUnversionedValue);
+
+ TCompactVector<char, DefaultBlobCapacity> RowData_;
+
+ TUnversionedRowHeader* GetHeader();
+ TUnversionedValue* GetValue(ui32 index);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TOwningRowTag
+{ };
+
+//! A helper used for constructing TUnversionedOwningRow instances.
+//! Keeps both row values and strings.
+class TUnversionedOwningRowBuilder
+{
+public:
+ static const int DefaultValueCapacity = 16;
+
+ explicit TUnversionedOwningRowBuilder(int initialValueCapacity = DefaultValueCapacity);
+
+ int AddValue(const TUnversionedValue& value);
+ TUnversionedValue* BeginValues();
+ TUnversionedValue* EndValues();
+
+ TUnversionedOwningRow FinishRow();
+
+private:
+ const int InitialValueCapacity_;
+
+ TBlob RowData_{GetRefCountedTypeCookie<TOwningRowTag>()};
+ TBlob StringData_{GetRefCountedTypeCookie<TOwningRowTag>()};
+
+ TUnversionedRowHeader* GetHeader();
+ TUnversionedValue* GetValue(ui32 index);
+ void Reset();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRange<TRowRange> MakeSingletonRowRange(TLegacyKey lowerBound, TLegacyKey upperBound);
+
+TKeyRef ToKeyRef(TUnversionedRow row);
+TKeyRef ToKeyRef(TUnversionedRow row, int prefixLength);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, TUnversionedRow row, TStringBuf format);
+void FormatValue(TStringBuilderBase* builder, TMutableUnversionedRow row, TStringBuf format);
+void FormatValue(TStringBuilderBase* builder, const TUnversionedOwningRow& row, TStringBuf format);
+
+TString ToString(TUnversionedRow row, bool valuesOnly = false);
+TString ToString(TMutableUnversionedRow row, bool valuesOnly = false);
+TString ToString(const TUnversionedOwningRow& row, bool valuesOnly = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Hash function may change in future. Use fingerprints for stability.
+struct TDefaultUnversionedValueRangeHash
+{
+ size_t operator()(TUnversionedValueRange range) const;
+};
+
+struct TDefaultUnversionedValueRangeEqual
+{
+ bool operator()(TUnversionedValueRange lhs, TUnversionedValueRange rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Hash function may change in future. Use fingerprints for stability.
+struct TDefaultUnversionedRowHash
+{
+ size_t operator()(TUnversionedRow row) const;
+};
+
+struct TDefaultUnversionedRowEqual
+{
+ bool operator()(TUnversionedRow lhs, TUnversionedRow rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBitwiseUnversionedValueRangeHash
+{
+ size_t operator()(TUnversionedValueRange range) const;
+};
+
+struct TBitwiseUnversionedValueRangeEqual
+{
+ bool operator()(TUnversionedValueRange lhs, TUnversionedValueRange rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBitwiseUnversionedRowHash
+{
+ size_t operator()(TUnversionedRow row) const;
+};
+
+struct TBitwiseUnversionedRowEqual
+{
+ bool operator()(TUnversionedRow lhs, TUnversionedRow rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+//! A hasher for TUnversionedRow.
+template <>
+struct THash<NYT::NTableClient::TUnversionedRow>
+{
+ inline size_t operator()(NYT::NTableClient::TUnversionedRow row) const
+ {
+ return NYT::NTableClient::TDefaultUnversionedRowHash()(row);
+ }
+};
diff --git a/yt/yt/client/table_client/unversioned_value.cpp b/yt/yt/client/table_client/unversioned_value.cpp
new file mode 100644
index 0000000000..9f602ed918
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_value.cpp
@@ -0,0 +1,239 @@
+#include "unversioned_value.h"
+
+#ifndef YT_COMPILING_UDF
+
+#include "unversioned_row.h"
+#include "composite_compare.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#endif
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStringBuf TUnversionedValue::AsStringBuf() const
+{
+ return TStringBuf(Data.String, Length);
+}
+
+TString TUnversionedValue::AsString() const
+{
+ return TString(Data.String, Length);
+}
+
+TFingerprint GetFarmFingerprint(const TUnversionedValue& value)
+{
+ auto type = value.Type;
+ switch (type) {
+ case EValueType::String:
+ return NYT::FarmFingerprint(value.Data.String, value.Length);
+
+ case EValueType::Int64:
+ return NYT::FarmFingerprint(std::bit_cast<ui64>(value.Data.Int64));
+
+ case EValueType::Uint64:
+ return NYT::FarmFingerprint(value.Data.Uint64);
+
+ case EValueType::Double:
+ return NYT::FarmFingerprint(std::bit_cast<ui64>(value.Data.Double));
+
+ case EValueType::Boolean:
+ return NYT::FarmFingerprint(static_cast<ui64>(value.Data.Boolean));
+
+ case EValueType::Null:
+ return NYT::FarmFingerprint(0);
+
+ case EValueType::Composite:
+ return CompositeFarmHash(NYson::TYsonStringBuf(value.AsStringBuf()));
+
+ default:
+#ifdef YT_COMPILING_UDF
+ YT_ABORT();
+#else
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::UnhashableType,
+ "Cannot hash values of type %Qlv; only scalar types are allowed for key columns",
+ type)
+ << TErrorAttribute("value", value);
+#endif
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintTo(const TUnversionedValue& value, ::std::ostream* os)
+{
+ *os << ToString(value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void AppendWithCut(TStringBuilderBase* builder, TStringBuf string)
+{
+ constexpr auto Cutoff = 128;
+ if (string.size() <= 2 * Cutoff + 3) {
+ builder->AppendString(string);
+ } else {
+ builder->AppendString(string.substr(0, Cutoff));
+ builder->AppendString("...");
+ builder->AppendString(string.substr(string.size() - Cutoff, Cutoff));
+ }
+}
+
+} // namespace
+
+void FormatValue(TStringBuilderBase* builder, const TUnversionedValue& value, TStringBuf format)
+{
+ using NTableClient::EValueFlags;
+ using NTableClient::EValueType;
+
+ bool noFlags = false;
+ for (char c : format) {
+ noFlags |= c == 'k';
+ }
+
+ if (!noFlags) {
+ if (Any(value.Flags & EValueFlags::Aggregate)) {
+ builder->AppendChar('%');
+ }
+ if (Any(value.Flags & EValueFlags::Hunk)) {
+ builder->AppendChar('&');
+ }
+ builder->AppendFormat("%v#", value.Id);
+ }
+ switch (value.Type) {
+ case EValueType::Null:
+ case EValueType::Min:
+ case EValueType::Max:
+ case EValueType::TheBottom:
+ builder->AppendFormat("<%v>", value.Type);
+ break;
+
+ case EValueType::Int64:
+ builder->AppendFormat("%v", value.Data.Int64);
+ break;
+
+ case EValueType::Uint64:
+ builder->AppendFormat("%vu", value.Data.Uint64);
+ break;
+
+ case EValueType::Double:
+ builder->AppendFormat("%v", value.Data.Double);
+ break;
+
+ case EValueType::Boolean:
+ builder->AppendFormat("%v", value.Data.Boolean);
+ break;
+
+ case EValueType::String: {
+ builder->AppendChar('"');
+ AppendWithCut(builder, value.AsStringBuf());
+ builder->AppendChar('"');
+ break;
+ }
+
+ case EValueType::Any:
+ case EValueType::Composite: {
+ if (value.Type == EValueType::Composite) {
+ // ermolovd@ says "composites" are comparable, in contrast to "any".
+ builder->AppendString("><");
+ }
+
+ auto compositeString = ConvertToYsonString(
+ NYson::TYsonString(value.AsString()),
+ NYson::EYsonFormat::Text);
+
+ AppendWithCut(builder, compositeString.AsStringBuf());
+ break;
+ }
+ }
+}
+
+TString ToString(const TUnversionedValue& value, bool valueOnly)
+{
+ return ToStringViaBuilder(value, valueOnly ? "k" : "");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TDefaultUnversionedValueHash::operator()(const TUnversionedValue& value) const
+{
+ return GetFarmFingerprint(value);
+}
+
+bool TDefaultUnversionedValueEqual::operator()(const TUnversionedValue& lhs, const TUnversionedValue& rhs) const
+{
+ return lhs == rhs;
+}
+
+size_t TBitwiseUnversionedValueHash::operator()(const TUnversionedValue& value) const
+{
+ size_t result = 0;
+ HashCombine(result, value.Id);
+ HashCombine(result, value.Flags);
+ HashCombine(result, value.Type);
+ switch (value.Type) {
+ case EValueType::Int64:
+ HashCombine(result, value.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ HashCombine(result, value.Data.Uint64);
+ break;
+ case EValueType::Double:
+ HashCombine(result, value.Data.Double);
+ break;
+ case EValueType::Boolean:
+ HashCombine(result, value.Data.Boolean);
+ break;
+ case EValueType::String:
+ case EValueType::Any:
+ case EValueType::Composite:
+ HashCombine(result, value.AsStringBuf());
+ break;
+ default:
+ break;
+ }
+ return result;
+}
+
+bool TBitwiseUnversionedValueEqual::operator()(const TUnversionedValue& lhs, const TUnversionedValue& rhs) const
+{
+ if (lhs.Id != rhs.Id) {
+ return false;
+ }
+ if (lhs.Flags != rhs.Flags) {
+ return false;
+ }
+ if (lhs.Type != rhs.Type) {
+ return false;
+ }
+ switch (lhs.Type) {
+ case EValueType::Int64:
+ return lhs.Data.Int64 == rhs.Data.Int64;
+ case EValueType::Uint64:
+ return lhs.Data.Uint64 == rhs.Data.Uint64;
+ case EValueType::Double:
+ return lhs.Data.Double == rhs.Data.Double;
+ case EValueType::Boolean:
+ return lhs.Data.Boolean == rhs.Data.Boolean;
+ case EValueType::String:
+ case EValueType::Any:
+ case EValueType::Composite:
+ if (lhs.Length != rhs.Length) {
+ return false;
+ }
+ return ::memcmp(lhs.Data.String, rhs.Data.String, lhs.Length) == 0;
+ default:
+ return true;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/unversioned_value.h b/yt/yt/client/table_client/unversioned_value.h
new file mode 100644
index 0000000000..d4fdc40c9d
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_value.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include "row_base.h"
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+#include <util/system/defaults.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Wire protocol readers/writer rely on this fixed layout.
+union TUnversionedValueData
+{
+ //! |Int64| value.
+ i64 Int64;
+ //! |Uint64| value.
+ ui64 Uint64;
+ //! |Double| value.
+ double Double;
+ //! |Boolean| value.
+ bool Boolean;
+ //! String value for |String| type or YSON-encoded value for |Any| type.
+ //! NB: string is not zero-terminated, so never use it as a TString.
+ //! Use #TUnversionedValue::AsStringBuf() or #TUnversionedValue::AsString() instead.
+ const char* String;
+};
+
+static_assert(
+ sizeof(TUnversionedValueData) == 8,
+ "TUnversionedValueData has to be exactly 8 bytes.");
+
+// NB: Wire protocol readers/writer rely on this fixed layout.
+struct TUnversionedValue
+{
+ //! Column id w.r.t. the name table.
+ ui16 Id;
+
+ //! Column type.
+ EValueType Type;
+
+ //! Various bit-packed flags.
+ EValueFlags Flags;
+
+ //! Length of a variable-sized value (only meaningful for string-like types).
+ ui32 Length;
+
+ //! Payload.
+ TUnversionedValueData Data;
+
+ //! Assuming #IsStringLikeType(Type), return string data as a TStringBuf.
+ TStringBuf AsStringBuf() const;
+ //! Assuming #IsStringLikeType(Type), return string data as a TString.
+ TString AsString() const;
+};
+
+static_assert(
+ sizeof(TUnversionedValue) == 16,
+ "TUnversionedValue has to be exactly 16 bytes.");
+static_assert(
+ std::is_pod_v<TUnversionedValue>,
+ "TUnversionedValue must be a POD type.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Computes FarmHash forever-fixed fingerprint for a given TUnversionedValue.
+TFingerprint GetFarmFingerprint(const TUnversionedValue& value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Debug printer for Gtest unittests.
+void PrintTo(const TUnversionedValue& value, ::std::ostream* os);
+
+void FormatValue(TStringBuilderBase* builder, const TUnversionedValue& value, TStringBuf format);
+TString ToString(const TUnversionedValue& value, bool valueOnly = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Hash function may change in future. Use fingerprints for stability.
+struct TDefaultUnversionedValueHash
+{
+ size_t operator()(const TUnversionedValue& value) const;
+};
+
+struct TDefaultUnversionedValueEqual
+{
+ bool operator()(const TUnversionedValue& lhs, const TUnversionedValue& rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBitwiseUnversionedValueHash
+{
+ size_t operator()(const TUnversionedValue& value) const;
+};
+
+struct TBitwiseUnversionedValueEqual
+{
+ bool operator()(const TUnversionedValue& lhs, const TUnversionedValue& rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+Y_DECLARE_PODTYPE(NYT::NTableClient::TUnversionedValue);
+
+//! A hasher for TUnversionedValue.
+template <>
+struct THash<NYT::NTableClient::TUnversionedValue>
+{
+ inline size_t operator()(const NYT::NTableClient::TUnversionedValue& value) const
+ {
+ return NYT::NTableClient::TDefaultUnversionedValueHash()(value);
+ }
+};
diff --git a/yt/yt/client/table_client/unversioned_writer.h b/yt/yt/client/table_client/unversioned_writer.h
new file mode 100644
index 0000000000..3e41ee3415
--- /dev/null
+++ b/yt/yt/client/table_client/unversioned_writer.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "unversioned_row.h"
+
+#include <yt/yt/client/chunk_client/writer_base.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/range.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * Writes non-versioned, fixed-width, strictly typed rowset with given schema.
+ * Useful for: query engine.
+ */
+struct IUnversionedRowsetWriter
+ : public virtual NChunkClient::IWriterBase
+{
+ /*!
+ * Writes given rows.
+ *
+ * The returned value is |true| iff one can write next rowset immediately,
+ * otherwise one should wait for |GetReadyEvent()| future.
+ *
+ * Every row must contain exactly one value for each column in schema, in the same order.
+ */
+ virtual bool Write(TRange<TUnversionedRow> rows) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IUnversionedRowsetWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Writes a schemaless unversioned rowset.
+/*!
+ * Writes unversioned rowset with schema and variable columns.
+ * Useful for: mapreduce jobs, write command.
+ */
+struct IUnversionedWriter
+ : public IUnversionedRowsetWriter
+{
+ virtual const TNameTablePtr& GetNameTable() const = 0;
+ virtual const TTableSchemaPtr& GetSchema() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IUnversionedWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/validate_logical_type-inl.h b/yt/yt/client/table_client/validate_logical_type-inl.h
new file mode 100644
index 0000000000..6bcf6a2740
--- /dev/null
+++ b/yt/yt/client/table_client/validate_logical_type-inl.h
@@ -0,0 +1,209 @@
+#ifndef VALIDATE_LOGICAL_TYPE_INL_H_
+#error "Direct inclusion of this file is not allowed, include validate_logical_type.h"
+// For the sake of sane code completion.
+#include "validate_logical_type.h"
+#endif
+
+#include "logical_type.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/charset/utf8.h>
+
+#include <cmath>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TNumber>
+static Y_FORCE_INLINE void ValidateNumericRange(TNumber value, TNumber min, TNumber max)
+{
+ static_assert(std::is_same_v<TNumber, i64> || std::is_same_v<TNumber, ui64> || std::is_same_v<TNumber, double>);
+ if (value < min || value > max) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Value %v is out of allowed range [%v, %v]",
+ value,
+ min,
+ max);
+ }
+}
+
+template <ESimpleLogicalValueType type>
+static constexpr auto GetLogicalTypeMax()
+{
+ // Ints
+ if constexpr (type == ESimpleLogicalValueType::Int8) {
+ return static_cast<i64>(Max<i8>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int16) {
+ return static_cast<i64>(Max<i16>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int32) {
+ return static_cast<i64>(Max<i32>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int64) {
+ return static_cast<i64>(Max<i64>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint8) { // Uints
+ return static_cast<ui64>(Max<ui8>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint16) {
+ return static_cast<ui64>(Max<ui16>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint32) {
+ return static_cast<ui64>(Max<ui32>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint64) {
+ return static_cast<ui64>(Max<ui64>());
+ } else if constexpr (type == ESimpleLogicalValueType::Date) { // Time types
+ return static_cast<ui64>(DateUpperBound - 1);
+ } else if constexpr (type == ESimpleLogicalValueType::Datetime) {
+ return static_cast<ui64>(DatetimeUpperBound - 1);
+ } else if constexpr (type == ESimpleLogicalValueType::Timestamp) {
+ return static_cast<ui64>(TimestampUpperBound - 1);
+ } else if constexpr (type == ESimpleLogicalValueType::Interval) {
+ return static_cast<i64>(TimestampUpperBound - 1);
+ } else if constexpr (type == ESimpleLogicalValueType::Float) {
+ return static_cast<double>(Max<float>());
+ } else {
+ // silly replacement for static_assert(false, ...);
+ static_assert(type == ESimpleLogicalValueType::Int8, "unsupported type");
+ }
+}
+
+template <ESimpleLogicalValueType type>
+static constexpr auto GetLogicalTypeMin()
+{
+ // Ints
+ if constexpr (type == ESimpleLogicalValueType::Int8) {
+ return static_cast<i64>(Min<i8>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int16) {
+ return static_cast<i64>(Min<i16>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int32) {
+ return static_cast<i64>(Min<i32>());
+ } else if constexpr (type == ESimpleLogicalValueType::Int64) {
+ return static_cast<i64>(Min<i64>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint8) { // Uints
+ return static_cast<ui64>(Min<ui8>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint16) {
+ return static_cast<ui64>(Min<ui16>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint32) {
+ return static_cast<ui64>(Min<ui32>());
+ } else if constexpr (type == ESimpleLogicalValueType::Uint64) {
+ return static_cast<ui64>(Min<ui64>());
+ } else if constexpr (type == ESimpleLogicalValueType::Date) { // Time types
+ return static_cast<ui64>(0);
+ } else if constexpr (type == ESimpleLogicalValueType::Datetime) {
+ return static_cast<ui64>(0);
+ } else if constexpr (type == ESimpleLogicalValueType::Timestamp) {
+ return static_cast<ui64>(0);
+ } else if constexpr (type == ESimpleLogicalValueType::Interval) {
+ return static_cast<i64>(-TimestampUpperBound + 1);
+ } else if constexpr (type == ESimpleLogicalValueType::Float) { // Floating point
+ return static_cast<double>(std::numeric_limits<float>::lowest());
+ } else {
+ // silly replacement for static_assert(false, ...);
+ static_assert(type == ESimpleLogicalValueType::Int8, "unsupported type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(i64 value)
+{
+ if constexpr (
+ type == ESimpleLogicalValueType::Int8 ||
+ type == ESimpleLogicalValueType::Int16 ||
+ type == ESimpleLogicalValueType::Int32 ||
+ type == ESimpleLogicalValueType::Interval)
+ {
+ NDetail::ValidateNumericRange(
+ value,
+ NDetail::GetLogicalTypeMin<type>(),
+ NDetail::GetLogicalTypeMax<type>());
+ } else {
+ static_assert(type == ESimpleLogicalValueType::Int64, "Bad logical type");
+ // Do nothing since Int64 doesn't require validation
+ }
+}
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(ui64 value)
+{
+ if constexpr (
+ type == ESimpleLogicalValueType::Uint8 ||
+ type == ESimpleLogicalValueType::Uint16 ||
+ type == ESimpleLogicalValueType::Uint32 ||
+ type == ESimpleLogicalValueType::Date ||
+ type == ESimpleLogicalValueType::Datetime ||
+ type == ESimpleLogicalValueType::Timestamp)
+ {
+ NDetail::ValidateNumericRange(
+ value,
+ NDetail::GetLogicalTypeMin<type>(),
+ NDetail::GetLogicalTypeMax<type>());
+ } else {
+ static_assert(type == ESimpleLogicalValueType::Uint64, "Bad logical type");
+ // Do nothing since Uint64 doesn't require validation
+ }
+}
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(double value)
+{
+ if constexpr (type == ESimpleLogicalValueType::Float) {
+ if (!std::isinf(value) && !std::isnan(value)) {
+ NDetail::ValidateNumericRange(
+ value,
+ NDetail::GetLogicalTypeMin<type>(),
+ NDetail::GetLogicalTypeMax<type>());
+ }
+ } else if constexpr (type == ESimpleLogicalValueType::Double) {
+ // do nothing
+ } else {
+ static_assert(type == ESimpleLogicalValueType::Double, "Bad logical type");
+ }
+}
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(bool /*value*/)
+{
+ if constexpr (type == ESimpleLogicalValueType::Boolean) {
+ // do nothing
+ } else {
+ static_assert(type == ESimpleLogicalValueType::Boolean, "Bad logical type");
+ }
+}
+
+template <ESimpleLogicalValueType type>
+void ValidateSimpleLogicalType(TStringBuf value)
+{
+ if constexpr (type == ESimpleLogicalValueType::String) {
+ // do nothing
+ } else if constexpr (type == ESimpleLogicalValueType::Utf8) {
+ if (UTF8Detect(value.data(), value.size()) == NotUTF8) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Not a valid utf8 string");
+ }
+ } else if constexpr (type == ESimpleLogicalValueType::Uuid) {
+ if (value.size() != 16) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Not a valid Uuid");
+ }
+ } else {
+ static_assert(type == ESimpleLogicalValueType::String, "Bad logical type");
+ }
+}
+
+template <>
+void ValidateSimpleLogicalType<ESimpleLogicalValueType::Json>(TStringBuf value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/validate_logical_type.cpp b/yt/yt/client/table_client/validate_logical_type.cpp
new file mode 100644
index 0000000000..8dc20d94e2
--- /dev/null
+++ b/yt/yt/client/table_client/validate_logical_type.cpp
@@ -0,0 +1,635 @@
+#include "validate_logical_type.h"
+#include "logical_type.h"
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+#include <util/stream/mem.h>
+#include <util/generic/adaptor.h>
+
+#include <library/cpp/json/json_reader.h>
+
+namespace NYT::NTableClient {
+
+using namespace NYson;
+using namespace NJson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EValueType physicalType>
+Y_FORCE_INLINE constexpr EYsonItemType ExpectedYsonItemType()
+{
+ if constexpr (physicalType == EValueType::Boolean) {
+ return EYsonItemType::BooleanValue;
+ } else if constexpr (physicalType == EValueType::Int64) {
+ return EYsonItemType::Int64Value;
+ } else if constexpr (physicalType == EValueType::Uint64) {
+ return EYsonItemType::Uint64Value;
+ } else if constexpr (physicalType == EValueType::Double) {
+ return EYsonItemType::DoubleValue;
+ } else if constexpr (physicalType == EValueType::String) {
+ return EYsonItemType::StringValue;
+ } else if constexpr (physicalType == EValueType::Null) {
+ return EYsonItemType::EntityValue;
+ } else {
+ static_assert(physicalType == EValueType::Boolean, "Unexpected value type");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TComplexLogicalTypeValidatorImpl
+{
+ class TFieldId;
+
+public:
+ TComplexLogicalTypeValidatorImpl(TYsonPullParser* parser, TComplexTypeFieldDescriptor descriptor)
+ : Cursor_(parser)
+ , RootDescriptor_(descriptor)
+ { }
+
+ void Validate()
+ {
+ return ValidateLogicalType(RootDescriptor_.GetType(), TFieldId());
+ }
+
+private:
+ void ValidateLogicalType(const TLogicalTypePtr& type, const TFieldId& fieldId)
+ {
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ ValidateSimpleType(type->UncheckedAsSimpleTypeRef().GetElement(), fieldId);
+ return;
+ case ELogicalMetatype::Optional:
+ ValidateOptionalType(type->UncheckedAsOptionalTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::List:
+ ValidateListType(type->UncheckedAsListTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::Struct:
+ ValidateStructType(type->UncheckedAsStructTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::Tuple:
+ ValidateTupleType(type->UncheckedAsTupleTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::VariantStruct:
+ ValidateVariantStructType(type->UncheckedAsVariantStructTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::VariantTuple:
+ ValidateVariantTupleType(type->UncheckedAsVariantTupleTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::Dict:
+ ValidateDictType(type->UncheckedAsDictTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::Tagged:
+ ValidateTaggedType(type->UncheckedAsTaggedTypeRef(), fieldId);
+ return;
+ case ELogicalMetatype::Decimal:
+ ValidateDecimalType(type->UncheckedAsDecimalTypeRef(), fieldId);
+ return;
+ }
+ YT_ABORT();
+ }
+
+ void ThrowUnexpectedYsonToken(EYsonItemType type, const TFieldId& fieldId)
+ {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ type,
+ Cursor_.GetCurrent().GetType());
+ }
+
+ Y_FORCE_INLINE void ValidateYsonTokenType(EYsonItemType type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() != type) {
+ ThrowUnexpectedYsonToken(type, fieldId);
+ }
+ }
+
+ template <ESimpleLogicalValueType type>
+ void ValidateSimpleType(const TFieldId& fieldId)
+ {
+ if constexpr (type == ESimpleLogicalValueType::Any) {
+ switch (Cursor_.GetCurrent().GetType()) {
+ case EYsonItemType::EntityValue:
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; unexpected entity value",
+ GetDescription(fieldId));
+ case EYsonItemType::Int64Value:
+ case EYsonItemType::BooleanValue:
+ case EYsonItemType::Uint64Value:
+ case EYsonItemType::DoubleValue:
+ case EYsonItemType::StringValue:
+ Cursor_.Next();
+ return;
+ case EYsonItemType::BeginAttributes:
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; unexpected top level attributes",
+ GetDescription(fieldId));
+
+ case EYsonItemType::BeginList:
+ case EYsonItemType::BeginMap: {
+ Cursor_.SkipComplexValue();
+ return;
+ }
+ default:
+ YT_ABORT();
+ }
+ } else {
+ static_assert(type != ESimpleLogicalValueType::Any);
+ constexpr auto expectedYsonEventType = ExpectedYsonItemType<GetPhysicalType(type)>();
+ if (Cursor_.GetCurrent().GetType() != expectedYsonEventType) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ expectedYsonEventType,
+ Cursor_.GetCurrent().GetType());
+ }
+
+ if constexpr (expectedYsonEventType == EYsonItemType::EntityValue) {
+ // nothing to check
+ } else if constexpr (expectedYsonEventType == EYsonItemType::BooleanValue) {
+ NTableClient::ValidateSimpleLogicalType<type>(Cursor_.GetCurrent().UncheckedAsBoolean());
+ } else if constexpr (expectedYsonEventType == EYsonItemType::Int64Value) {
+ NTableClient::ValidateSimpleLogicalType<type>(Cursor_.GetCurrent().UncheckedAsInt64());
+ } else if constexpr (expectedYsonEventType == EYsonItemType::Uint64Value) {
+ NTableClient::ValidateSimpleLogicalType<type>(Cursor_.GetCurrent().UncheckedAsUint64());
+ } else if constexpr (expectedYsonEventType == EYsonItemType::DoubleValue) {
+ NTableClient::ValidateSimpleLogicalType<type>(Cursor_.GetCurrent().UncheckedAsDouble());
+ } else if constexpr (expectedYsonEventType == EYsonItemType::StringValue) {
+ NTableClient::ValidateSimpleLogicalType<type>(Cursor_.GetCurrent().UncheckedAsString());
+ } else {
+ static_assert(expectedYsonEventType == EYsonItemType::EntityValue, "unexpected EYsonItemType");
+ }
+ Cursor_.Next();
+ }
+ }
+
+ Y_FORCE_INLINE void ValidateSimpleType(ESimpleLogicalValueType type, const TFieldId& fieldId)
+ {
+ switch (type) {
+#define CASE(x) \
+ case x: \
+ ValidateSimpleType<x>(fieldId); \
+ return;
+ CASE(ESimpleLogicalValueType::Null)
+ CASE(ESimpleLogicalValueType::Int64)
+ CASE(ESimpleLogicalValueType::Uint64)
+ CASE(ESimpleLogicalValueType::Double)
+ CASE(ESimpleLogicalValueType::Float)
+ CASE(ESimpleLogicalValueType::Boolean)
+ CASE(ESimpleLogicalValueType::String)
+ CASE(ESimpleLogicalValueType::Any)
+ CASE(ESimpleLogicalValueType::Json)
+ CASE(ESimpleLogicalValueType::Int8)
+ CASE(ESimpleLogicalValueType::Uint8)
+ CASE(ESimpleLogicalValueType::Int16)
+ CASE(ESimpleLogicalValueType::Uint16)
+ CASE(ESimpleLogicalValueType::Int32)
+ CASE(ESimpleLogicalValueType::Uint32)
+ CASE(ESimpleLogicalValueType::Utf8)
+ CASE(ESimpleLogicalValueType::Date)
+ CASE(ESimpleLogicalValueType::Datetime)
+ CASE(ESimpleLogicalValueType::Timestamp)
+ CASE(ESimpleLogicalValueType::Interval)
+ CASE(ESimpleLogicalValueType::Void)
+ CASE(ESimpleLogicalValueType::Uuid)
+#undef CASE
+ }
+ YT_ABORT();
+ }
+
+ void ValidateOptionalType(const TOptionalLogicalType& type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() == EYsonItemType::EntityValue) {
+ Cursor_.Next();
+ return;
+ }
+
+ if (!type.IsElementNullable()) {
+ ValidateLogicalType(type.GetElement(), fieldId.OptionalElement());
+ return;
+ }
+
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::BeginList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::BeginList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ if (Cursor_.GetCurrent().GetType() == EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; empty yson",
+ GetDescription(fieldId),
+ Cursor_.GetCurrent().GetType());
+ }
+ ValidateLogicalType(type.GetElement(), fieldId.OptionalElement());
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::EndList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ }
+
+ void ValidateListType(const TListLogicalType& type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::BeginList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::BeginList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ const auto& elementType = type.GetElement();
+ auto elementFieldId = fieldId.ListElement();
+ while (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ ValidateLogicalType(elementType, elementFieldId);
+ }
+ Cursor_.Next();
+ }
+
+ void ValidateStructType(const TStructLogicalType& type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::BeginList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::BeginList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ const auto& fields = type.GetFields();
+ for (size_t i = 0; i < fields.size(); ++i) {
+ if (Cursor_.GetCurrent().GetType() == EYsonItemType::EndList) {
+ do {
+ const auto& field = fields[i];
+ if (field.Type->GetMetatype() != ELogicalMetatype::Optional) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; struct ended before required field %Qv is set",
+ GetDescription(fieldId),
+ field.Name);
+ }
+ ++i;
+ } while (i < fields.size());
+ break;
+ }
+ const auto& field = fields[i];
+ ValidateLogicalType(field.Type, fieldId.StructField(i));
+ }
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::EndList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ }
+
+ void ValidateTupleType(const TTupleLogicalType& type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::BeginList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::BeginList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ const auto& elements = type.GetElements();
+ for (size_t i = 0; i < elements.size(); ++i) {
+ if (Cursor_.GetCurrent().GetType() == EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; expected %Qv got %Qv",
+ GetDescription(fieldId),
+ GetDescription(fieldId.TupleElement(i)),
+ EYsonItemType::EndList);
+ }
+ ValidateLogicalType(elements[i], fieldId.TupleElement(i));
+ }
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::EndList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ }
+
+ template <typename T>
+ Y_FORCE_INLINE void ValidateVariantTypeImpl(const T& type, const TFieldId& fieldId)
+ {
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::BeginList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::BeginList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::Int64Value) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::Int64Value,
+ Cursor_.GetCurrent().GetType());
+ }
+ const auto alternativeIndex = Cursor_.GetCurrent().UncheckedAsInt64();
+ Cursor_.Next();
+ if constexpr (std::is_same_v<T, TVariantTupleLogicalType>) {
+ const auto& elements = type.GetElements();
+ if (alternativeIndex < 0) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; variant alternative index %Qv is less than 0",
+ GetDescription(fieldId),
+ alternativeIndex);
+ }
+ if (alternativeIndex >= std::ssize(elements)) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; variant alternative index %Qv exceeds number of variant elements %Qv",
+ GetDescription(fieldId),
+ alternativeIndex,
+ elements.size());
+ }
+ ValidateLogicalType(elements[alternativeIndex], fieldId.VariantTupleElement(alternativeIndex));
+ } else {
+ static_assert(std::is_same_v<T, TVariantStructLogicalType>);
+ const auto& fields = type.GetFields();
+ if (alternativeIndex < 0) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; variant alternative index %Qv is less than 0",
+ GetDescription(fieldId),
+ alternativeIndex);
+ }
+ if (alternativeIndex >= std::ssize(fields)) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv; variant alternative index %Qv exceeds number of variant elements %Qv",
+ GetDescription(fieldId),
+ alternativeIndex,
+ fields.size());
+ }
+ ValidateLogicalType(fields[alternativeIndex].Type, fieldId.VariantStructField(alternativeIndex));
+ }
+
+ if (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation,
+ "Cannot parse %Qv: expected %Qlv, found %Qlv",
+ GetDescription(fieldId),
+ EYsonItemType::EndList,
+ Cursor_.GetCurrent().GetType());
+ }
+ Cursor_.Next();
+ }
+
+ void ValidateVariantTupleType(const TVariantTupleLogicalType& type, const TFieldId& fieldId)
+ {
+ ValidateVariantTypeImpl(type, fieldId);
+ }
+
+ void ValidateVariantStructType(const TVariantStructLogicalType& type, const TFieldId& fieldId)
+ {
+ ValidateVariantTypeImpl(type, fieldId);
+ }
+
+ void ValidateDictType(const TDictLogicalType& type, const TFieldId& fieldId)
+ {
+ ValidateYsonTokenType(EYsonItemType::BeginList, fieldId);
+ Cursor_.Next();
+ while (Cursor_.GetCurrent().GetType() != EYsonItemType::EndList) {
+ ValidateYsonTokenType(EYsonItemType::BeginList, fieldId);
+ Cursor_.Next();
+
+ ValidateLogicalType(type.GetKey(), fieldId.DictKey());
+ ValidateLogicalType(type.GetValue(), fieldId.DictValue());
+
+ ValidateYsonTokenType(EYsonItemType::EndList, fieldId);
+ Cursor_.Next();
+ }
+ Cursor_.Next();
+ }
+
+ Y_FORCE_INLINE void ValidateTaggedType(const TTaggedLogicalType& type, const TFieldId& fieldId)
+ {
+ ValidateLogicalType(type.GetElement(), fieldId.TaggedElement());
+ }
+
+ Y_FORCE_INLINE void ValidateDecimalType(const TDecimalLogicalType& type, const TFieldId& fieldId)
+ {
+ ValidateYsonTokenType(EYsonItemType::StringValue, fieldId);
+ try {
+ NDecimal::TDecimal::ValidateBinaryValue(
+ Cursor_.GetCurrent().UncheckedAsString(),
+ type.GetPrecision(),
+ type.GetScale());
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation, "Error validating field %Qv",
+ GetDescription(fieldId))
+ << ex;
+ }
+ Cursor_.Next();
+ }
+
+ TString GetDescription(const TFieldId& fieldId) const
+ {
+ return fieldId.GetDescriptor(RootDescriptor_).GetDescription();
+ }
+
+private:
+ class TFieldId
+ {
+ public:
+ // Root field id
+ TFieldId() = default;
+
+ TFieldId OptionalElement() const
+ {
+ return {this, 0};
+ }
+
+ TFieldId ListElement() const
+ {
+ return {this, 0};
+ }
+
+ TFieldId StructField(int i) const
+ {
+ return {this, i};
+ }
+
+ TFieldId TupleElement(int i) const
+ {
+ return {this, i};
+ }
+
+ TFieldId VariantStructField(int i) const
+ {
+ return {this, i};
+ }
+
+ TFieldId VariantTupleElement(int i) const
+ {
+ return {this, i};
+ }
+
+ TFieldId DictKey() const
+ {
+ return {this, 0};
+ }
+
+ TFieldId DictValue() const
+ {
+ return {this, 1};
+ }
+
+ TFieldId TaggedElement() const
+ {
+ return {this, 0};
+ }
+
+ TComplexTypeFieldDescriptor GetDescriptor(const TComplexTypeFieldDescriptor& root) const
+ {
+ std::vector<int> path;
+ const auto* current = this;
+ while (current->Parent_ != nullptr) {
+ path.push_back(current->SiblingIndex_);
+ current = current->Parent_;
+ }
+
+ auto descriptor = root;
+ for (const auto& childIndex : Reversed(path)) {
+ const auto& type = descriptor.GetType();
+ switch (type->GetMetatype()) {
+ case ELogicalMetatype::Simple:
+ case ELogicalMetatype::Decimal:
+ return descriptor;
+ case ELogicalMetatype::Optional:
+ descriptor = descriptor.OptionalElement();
+ continue;
+ case ELogicalMetatype::List:
+ descriptor = descriptor.ListElement();
+ continue;
+ case ELogicalMetatype::Struct:
+ descriptor = descriptor.StructField(childIndex);
+ continue;
+ case ELogicalMetatype::Tuple:
+ descriptor = descriptor.TupleElement(childIndex);
+ continue;
+ case ELogicalMetatype::VariantStruct:
+ descriptor = descriptor.VariantStructField(childIndex);
+ continue;
+ case ELogicalMetatype::VariantTuple:
+ descriptor = descriptor.VariantTupleElement(childIndex);
+ continue;
+ case ELogicalMetatype::Dict:
+ switch (childIndex) {
+ case 0:
+ descriptor = descriptor.DictKey();
+ continue;
+ case 1:
+ descriptor = descriptor.DictValue();
+ continue;
+ }
+ break;
+ case ELogicalMetatype::Tagged:
+ descriptor = descriptor.TaggedElement();
+ continue;
+ }
+ YT_ABORT();
+ }
+ return descriptor;
+ }
+
+ private:
+ TFieldId(const TFieldId* parent, int siblingIndex)
+ : Parent_(parent)
+ , SiblingIndex_(siblingIndex)
+ { }
+
+ private:
+ const TFieldId* Parent_ = nullptr;
+ int SiblingIndex_ = 0;
+ };
+
+private:
+ TYsonPullParserCursor Cursor_;
+ const TComplexTypeFieldDescriptor RootDescriptor_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateComplexLogicalType(TStringBuf ysonData, const TLogicalTypePtr& type)
+{
+ TMemoryInput in(ysonData);
+ TYsonPullParser parser(&in, EYsonType::Node);
+ TComplexLogicalTypeValidatorImpl validator(&parser, TComplexTypeFieldDescriptor(type));
+ validator.Validate();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TValidateJsonCallbacks
+ : public TJsonCallbacks
+{
+public:
+ TValidateJsonCallbacks()
+ : TJsonCallbacks(/* throwOnError */ true)
+ {}
+
+ bool OnDouble(double value) final
+ {
+ if (Y_UNLIKELY(std::isinf(value))) {
+ ythrow TJsonException() << "infinite values are not allowed";
+ }
+ return true;
+ }
+
+ bool OnEnd() final
+ {
+ if (Finished_) {
+ ythrow TJsonException() << "JSON value is already finished";
+ }
+ Finished_ = true;
+ return true;
+ }
+
+private:
+ bool Finished_ = false;
+};
+
+template <>
+void ValidateSimpleLogicalType<ESimpleLogicalValueType::Json>(TStringBuf value)
+{
+ TMemoryInput input(value);
+ TValidateJsonCallbacks callbacks;
+ try {
+ auto ok = ReadJson(&input, &callbacks);
+ // We expect all the errors to be thrown.
+ YT_VERIFY(ok);
+ } catch (const TJsonException& ex) {
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation, "Invalid JSON: %s", ex.AsStrBuf());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/validate_logical_type.h b/yt/yt/client/table_client/validate_logical_type.h
new file mode 100644
index 0000000000..add327a812
--- /dev/null
+++ b/yt/yt/client/table_client/validate_logical_type.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+#include "logical_type.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr ui64 DateUpperBound = 49673ull;
+constexpr ui64 DatetimeUpperBound = DateUpperBound * 86400ull;
+constexpr ui64 TimestampUpperBound = DatetimeUpperBound * 1000000ull;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(i64 value);
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(ui64 value);
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(double value);
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(bool value);
+
+template <ESimpleLogicalValueType type>
+Y_FORCE_INLINE void ValidateSimpleLogicalType(TStringBuf value);
+
+// Validates complex logical type yson representation.
+void ValidateComplexLogicalType(TStringBuf ysonData, const TLogicalTypePtr& type);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
+#define VALIDATE_LOGICAL_TYPE_INL_H_
+#include "validate_logical_type-inl.h"
+#undef VALIDATE_LOGICAL_TYPE_INL_H_
diff --git a/yt/yt/client/table_client/value_consumer.cpp b/yt/yt/client/table_client/value_consumer.cpp
new file mode 100644
index 0000000000..09cb24c78e
--- /dev/null
+++ b/yt/yt/client/table_client/value_consumer.cpp
@@ -0,0 +1,423 @@
+#include "value_consumer.h"
+#include "helpers.h"
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <util/string/cast.h>
+
+namespace NYT::NTableClient {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool ConvertToBooleanValue(TStringBuf stringValue)
+{
+ if (stringValue == "true") {
+ return true;
+ } else if (stringValue == "false") {
+ return false;
+ } else {
+ THROW_ERROR_EXCEPTION("Unable to convert value to boolean")
+ << TErrorAttribute("value", stringValue);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TValueConsumerBase::TValueConsumerBase(
+ TTableSchemaPtr schema,
+ TTypeConversionConfigPtr typeConversionConfig)
+ : Schema_(std::move(schema))
+ , TypeConversionConfig_(std::move(typeConversionConfig))
+{ }
+
+void TValueConsumerBase::InitializeIdToTypeMapping()
+{
+ const auto& nameTable = GetNameTable();
+ for (const auto& column : Schema_->Columns()) {
+ int id = nameTable->GetIdOrRegisterName(column.Name());
+ if (id >= static_cast<int>(NameTableIdToType_.size())) {
+ NameTableIdToType_.resize(id + 1, EValueType::Any);
+ }
+ NameTableIdToType_[id] = column.GetWireType();
+ }
+}
+
+template <typename T>
+void TValueConsumerBase::ProcessIntegralValue(const TUnversionedValue& value, EValueType columnType)
+{
+ auto integralValue = FromUnversionedValue<T>(value);
+ if (TypeConversionConfig_->EnableAllToStringConversion && columnType == EValueType::String) {
+ char buf[64];
+ char* end = buf + 64;
+ char* start = WriteDecIntToBufferBackwards(end, integralValue);
+ OnMyValue(MakeUnversionedStringValue(TStringBuf(start, end), value.Id));
+ } else if (TypeConversionConfig_->EnableIntegralToDoubleConversion && columnType == EValueType::Double) {
+ OnMyValue(MakeUnversionedDoubleValue(static_cast<double>(integralValue), value.Id));
+ } else {
+ OnMyValue(value);
+ }
+}
+
+void TValueConsumerBase::ProcessInt64Value(const TUnversionedValue& value, EValueType columnType)
+{
+ if (TypeConversionConfig_->EnableIntegralTypeConversion && columnType == EValueType::Uint64) {
+ i64 integralValue = value.Data.Int64;
+ if (integralValue < 0) {
+ ThrowConversionException(
+ value,
+ columnType,
+ TError("Unable to convert negative int64 to uint64")
+ << TErrorAttribute("value", integralValue));
+ } else {
+ OnMyValue(MakeUnversionedUint64Value(static_cast<ui64>(integralValue), value.Id));
+ }
+ } else {
+ ProcessIntegralValue<i64>(value, columnType);
+ }
+}
+
+void TValueConsumerBase::ProcessUint64Value(const TUnversionedValue& value, EValueType columnType)
+{
+ if (TypeConversionConfig_->EnableIntegralTypeConversion && columnType == EValueType::Int64) {
+ ui64 integralValue = value.Data.Uint64;
+ if (integralValue > std::numeric_limits<i64>::max()) {
+ ThrowConversionException(
+ value,
+ columnType,
+ TError("Unable to convert uint64 to int64 as it leads to an overflow")
+ << TErrorAttribute("value", integralValue));
+ } else {
+ OnMyValue(MakeUnversionedInt64Value(static_cast<i64>(integralValue), value.Id));
+ }
+ } else {
+ ProcessIntegralValue<ui64>(value, columnType);
+ }
+}
+
+void TValueConsumerBase::ProcessBooleanValue(const TUnversionedValue& value, EValueType columnType)
+{
+ if (TypeConversionConfig_->EnableAllToStringConversion && columnType == EValueType::String) {
+ TStringBuf stringValue = value.Data.Boolean ? "true" : "false";
+ OnMyValue(MakeUnversionedStringValue(stringValue, value.Id));
+ } else {
+ OnMyValue(value);
+ }
+}
+
+void TValueConsumerBase::ProcessDoubleValue(const TUnversionedValue& value, EValueType columnType)
+{
+ if (TypeConversionConfig_->EnableAllToStringConversion && columnType == EValueType::String) {
+ char buf[64];
+ auto length = FloatToString(value.Data.Double, buf, sizeof(buf));
+ TStringBuf stringValue(buf, buf + length);
+ OnMyValue(MakeUnversionedStringValue(stringValue, value.Id));
+ } else {
+ OnMyValue(value);
+ }
+}
+
+void TValueConsumerBase::ProcessStringValue(const TUnversionedValue& value, EValueType columnType)
+{
+ if (TypeConversionConfig_->EnableStringToAllConversion) {
+ TUnversionedValue convertedValue;
+ TStringBuf stringValue(value.Data.String, value.Length);
+ try {
+ switch (columnType) {
+ case EValueType::Int64:
+ case EValueType::Uint64: {
+ auto adjustedStringValue = stringValue;
+ if (!stringValue.empty() && stringValue.back() == 'u') {
+ adjustedStringValue = TStringBuf(value.Data.String, value.Length - 1);
+ }
+ if (columnType == EValueType::Int64) {
+ convertedValue = MakeUnversionedInt64Value(FromString<i64>(adjustedStringValue), value.Id);
+ } else {
+ convertedValue = MakeUnversionedUint64Value(FromString<ui64>(adjustedStringValue), value.Id);
+ }
+ break;
+ }
+ case EValueType::Double:
+ convertedValue = MakeUnversionedDoubleValue(FromString<double>(stringValue), value.Id);
+ break;
+ case EValueType::Boolean:
+ convertedValue = MakeUnversionedBooleanValue(ConvertToBooleanValue(stringValue), value.Id);
+ break;
+ default:
+ convertedValue = value;
+ break;
+ }
+ } catch (const std::exception& ex) {
+ ThrowConversionException(value, columnType, TError(ex));
+ }
+ OnMyValue(convertedValue);
+ } else {
+ OnMyValue(value);
+ }
+}
+
+void TValueConsumerBase::OnValue(const TUnversionedValue& value)
+{
+ EValueType columnType;
+ if (NameTableIdToType_.size() <= value.Id) {
+ columnType = EValueType::Any;
+ } else {
+ columnType = NameTableIdToType_[value.Id];
+ }
+
+ switch (value.Type) {
+ case EValueType::Int64:
+ ProcessInt64Value(value, columnType);
+ break;
+ case EValueType::Uint64:
+ ProcessUint64Value(value, columnType);
+ break;
+ case EValueType::Boolean:
+ ProcessBooleanValue(value, columnType);
+ break;
+ case EValueType::Double:
+ ProcessDoubleValue(value, columnType);
+ break;
+ case EValueType::String:
+ ProcessStringValue(value, columnType);
+ break;
+ default:
+ OnMyValue(value);
+ break;
+ }
+}
+
+const TTableSchemaPtr& TValueConsumerBase::GetSchema() const
+{
+ return Schema_;
+}
+
+void TValueConsumerBase::ThrowConversionException(const TUnversionedValue& value, EValueType columnType, const TError& ex)
+{
+ THROW_ERROR_EXCEPTION(EErrorCode::SchemaViolation, "Error while performing type conversion")
+ << ex
+ << TErrorAttribute("column", GetNameTable()->GetName(value.Id))
+ << TErrorAttribute("value_type", value.Type)
+ << TErrorAttribute("column_type", columnType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBuildingValueConsumer::TBuildingValueConsumer(
+ TTableSchemaPtr schema,
+ NLogging::TLogger logger,
+ bool convertNullToEntity,
+ TTypeConversionConfigPtr typeConversionConfig)
+ : TValueConsumerBase(std::move(schema), std::move(typeConversionConfig))
+ , Logger(std::move(logger))
+ , NameTable_(TNameTable::FromSchema(*Schema_))
+ , ConvertNullToEntity_(convertNullToEntity)
+ , WrittenFlags_(NameTable_->GetSize())
+{
+ InitializeIdToTypeMapping();
+}
+
+std::vector<TUnversionedRow> TBuildingValueConsumer::GetRows() const
+{
+ std::vector<TUnversionedRow> result;
+ result.reserve(Rows_.size());
+ for (const auto& row : Rows_) {
+ result.push_back(row);
+ }
+ return result;
+}
+
+void TBuildingValueConsumer::SetAggregate(bool value)
+{
+ Aggregate_ = value;
+}
+
+void TBuildingValueConsumer::SetTreatMissingAsNull(bool value)
+{
+ TreatMissingAsNull_ = value;
+}
+
+void TBuildingValueConsumer::SetAllowMissingKeyColumns(bool value)
+{
+ AllowMissingKeyColumns_ = value;
+}
+
+const TNameTablePtr& TBuildingValueConsumer::GetNameTable() const
+{
+ return NameTable_;
+}
+
+bool TBuildingValueConsumer::GetAllowUnknownColumns() const
+{
+ return false;
+}
+
+void TBuildingValueConsumer::OnBeginRow()
+{
+ // Do nothing.
+}
+
+TUnversionedValue TBuildingValueConsumer::MakeAnyFromScalar(const TUnversionedValue& value)
+{
+ NYson::TBufferedBinaryYsonWriter writer(&ValueBuffer_);
+ switch (value.Type) {
+ case EValueType::Int64:
+ writer.OnInt64Scalar(value.Data.Int64);
+ break;
+ case EValueType::Uint64:
+ writer.OnUint64Scalar(value.Data.Uint64);
+ break;
+ case EValueType::Double:
+ writer.OnDoubleScalar(value.Data.Double);
+ break;
+ case EValueType::Boolean:
+ writer.OnBooleanScalar(value.Data.Boolean);
+ break;
+ case EValueType::String:
+ writer.OnStringScalar(value.AsStringBuf());
+ break;
+ case EValueType::Null:
+ writer.OnEntity();
+ break;
+ default:
+ ThrowUnexpectedValueType(value.Type);
+ }
+ writer.Flush();
+
+ return MakeUnversionedAnyValue(
+ TStringBuf(
+ ValueBuffer_.Begin(),
+ ValueBuffer_.Begin() + ValueBuffer_.Size()),
+ value.Id,
+ value.Flags);
+}
+
+void TBuildingValueConsumer::OnMyValue(const TUnversionedValue& value)
+{
+ if (value.Id >= Schema_->GetColumnCount()) {
+ return;
+ }
+ auto valueCopy = value;
+ const auto& columnSchema = Schema_->Columns()[valueCopy.Id];
+ if (columnSchema.Aggregate() && Aggregate_) {
+ valueCopy.Flags |= EValueFlags::Aggregate;
+ }
+ if (columnSchema.IsOfV1Type(ESimpleLogicalValueType::Any) &&
+ valueCopy.Type != EValueType::Any &&
+ (valueCopy.Type != EValueType::Null || ConvertNullToEntity_))
+ {
+ if (valueCopy.Type == EValueType::Null && LogNullToEntity_) {
+ YT_LOG_DEBUG("Detected conversion null to yson entity");
+ LogNullToEntity_ = false;
+ }
+ Builder_.AddValue(MakeAnyFromScalar(valueCopy));
+ ValueBuffer_.Clear();
+ } else {
+ Builder_.AddValue(valueCopy);
+ }
+ WrittenFlags_[valueCopy.Id] = true;
+}
+
+void TBuildingValueConsumer::OnEndRow()
+{
+ for (int id = 0; id < std::ssize(WrittenFlags_); ++id) {
+ if (WrittenFlags_[id]) {
+ WrittenFlags_[id] = false;
+ } else if ((TreatMissingAsNull_ || id < Schema_->GetKeyColumnCount()) &&
+ !(AllowMissingKeyColumns_ && id < Schema_->GetKeyColumnCount()) &&
+ !Schema_->Columns()[id].Expression())
+ {
+ auto flags = EValueFlags::None;
+ if (Schema_->Columns()[id].Aggregate() && Aggregate_) {
+ flags |= EValueFlags::Aggregate;
+ }
+ Builder_.AddValue(MakeUnversionedSentinelValue(EValueType::Null, id, flags));
+ }
+ }
+ Rows_.emplace_back(Builder_.FinishRow());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWritingValueConsumerBufferTag
+{ };
+
+TWritingValueConsumer::TWritingValueConsumer(
+ IUnversionedWriterPtr writer,
+ TTypeConversionConfigPtr typeConversionConfig,
+ i64 maxRowBufferSize)
+ : TValueConsumerBase(writer->GetSchema(), std::move(typeConversionConfig))
+ , Writer_(std::move(writer))
+ , MaxRowBufferSize_(maxRowBufferSize)
+ , RowBuffer_(New<TRowBuffer>(TWritingValueConsumerBufferTag()))
+{
+ YT_VERIFY(Writer_);
+ InitializeIdToTypeMapping();
+}
+
+TFuture<void> TWritingValueConsumer::Flush()
+{
+ if (RowBuffer_->GetSize() == 0) {
+ return VoidFuture;
+ }
+
+ // We could have multiple value consumers writing into the same Writer.
+ // It means, that there could be multiple subscribers waiting to flush data on the same ready event.
+ // To make writing safe, we must double check that the writer is really ready, before flushing rows.
+
+ return
+ BIND([writer = Writer_, rowBuffer = RowBuffer_, rows = std::move(Rows_)] {
+ while (!writer->GetReadyEvent().IsSet() || !writer->GetReadyEvent().Get().IsOK()) {
+ WaitFor(writer->GetReadyEvent())
+ .ThrowOnError();
+ }
+
+ writer->Write(rows);
+ rowBuffer->Clear();
+ return writer->GetReadyEvent();
+ })
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+}
+
+const TNameTablePtr& TWritingValueConsumer::GetNameTable() const
+{
+ return Writer_->GetNameTable();
+}
+
+bool TWritingValueConsumer::GetAllowUnknownColumns() const
+{
+ return true;
+}
+
+void TWritingValueConsumer::OnBeginRow()
+{
+ YT_ASSERT(Values_.empty());
+}
+
+void TWritingValueConsumer::OnMyValue(const TUnversionedValue& value)
+{
+ Values_.push_back(RowBuffer_->CaptureValue(value));
+}
+
+void TWritingValueConsumer::OnEndRow()
+{
+ auto row = RowBuffer_->CaptureRow(MakeRange(Values_), false);
+ Values_.clear();
+ Rows_.push_back(row);
+
+ if (RowBuffer_->GetSize() >= MaxRowBufferSize_) {
+ auto error = WaitFor(Flush());
+ THROW_ERROR_EXCEPTION_IF_FAILED(error, "Table writer failed")
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/value_consumer.h b/yt/yt/client/table_client/value_consumer.h
new file mode 100644
index 0000000000..7c23ad8b5d
--- /dev/null
+++ b/yt/yt/client/table_client/value_consumer.h
@@ -0,0 +1,157 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+#include "unversioned_row.h"
+
+#include <yt/yt/core/misc/blob_output.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IValueConsumer
+{
+ virtual ~IValueConsumer() = default;
+
+ virtual const TNameTablePtr& GetNameTable() const = 0;
+ virtual const TTableSchemaPtr& GetSchema() const = 0;
+
+ virtual bool GetAllowUnknownColumns() const = 0;
+
+ virtual void OnBeginRow() = 0;
+ virtual void OnValue(const TUnversionedValue& value) = 0;
+ virtual void OnEndRow() = 0;
+};
+
+struct IFlushableValueConsumer
+ : public virtual IValueConsumer
+{
+ virtual TFuture<void> Flush() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TValueConsumerBase
+ : public virtual IValueConsumer
+{
+public:
+ TValueConsumerBase(
+ TTableSchemaPtr schema,
+ TTypeConversionConfigPtr typeConversionConfig);
+
+ void OnValue(const TUnversionedValue& value) override;
+ const TTableSchemaPtr& GetSchema() const override;
+
+protected:
+ const TTableSchemaPtr Schema_;
+
+ virtual void OnMyValue(const TUnversionedValue& value) = 0;
+
+ // This should be done in a separate base class method because we can't do
+ // it in a constructor (it depends on a derived type GetNameTable() implementation that
+ // can't be called from a parent class).
+ void InitializeIdToTypeMapping();
+
+private:
+ const TTypeConversionConfigPtr TypeConversionConfig_;
+
+ std::vector<EValueType> NameTableIdToType_;
+
+ // This template method is private and only used in value_consumer.cpp with T = i64/ui64,
+ // so it is not necessary to implement it in value_consumer-inl.h.
+ template <typename T>
+ void ProcessIntegralValue(const TUnversionedValue& value, EValueType columnType);
+
+ void ProcessInt64Value(const TUnversionedValue& value, EValueType columnType);
+ void ProcessUint64Value(const TUnversionedValue& value, EValueType columnType);
+ void ProcessBooleanValue(const TUnversionedValue& value, EValueType columnType);
+ void ProcessDoubleValue(const TUnversionedValue& value, EValueType columnType);
+ void ProcessStringValue(const TUnversionedValue& value, EValueType columnType);
+
+ void ThrowConversionException(const TUnversionedValue& value, EValueType columnType, const TError& error);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBuildingValueConsumer
+ : public TValueConsumerBase
+{
+public:
+ TBuildingValueConsumer(
+ TTableSchemaPtr schema,
+ NLogging::TLogger logger,
+ bool convertNullToEntity,
+ TTypeConversionConfigPtr typeConversionConfig = New<TTypeConversionConfig>());
+
+ std::vector<TUnversionedRow> GetRows() const;
+
+ const TNameTablePtr& GetNameTable() const override;
+
+ void SetAggregate(bool value);
+ void SetTreatMissingAsNull(bool value);
+ void SetAllowMissingKeyColumns(bool value);
+
+private:
+ bool GetAllowUnknownColumns() const override;
+
+ void OnBeginRow() override;
+ void OnMyValue(const TUnversionedValue& value) override;
+ void OnEndRow() override;
+
+ TUnversionedValue MakeAnyFromScalar(const TUnversionedValue& value);
+
+private:
+ const NLogging::TLogger Logger;
+ const TNameTablePtr NameTable_;
+ const bool ConvertNullToEntity_;
+
+ TUnversionedOwningRowBuilder Builder_;
+ std::vector<TUnversionedOwningRow> Rows_;
+ std::vector<bool> WrittenFlags_;
+ TBlobOutput ValueBuffer_;
+
+ bool Aggregate_ = false;
+ bool TreatMissingAsNull_ = false;
+ bool LogNullToEntity_ = true;
+ bool AllowMissingKeyColumns_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWritingValueConsumer
+ : public TValueConsumerBase
+ , public IFlushableValueConsumer
+{
+public:
+ explicit TWritingValueConsumer(
+ IUnversionedWriterPtr writer,
+ TTypeConversionConfigPtr typeConversionConfig = New<TTypeConversionConfig>(),
+ i64 maxRowBufferSize = 1_MB);
+
+ TFuture<void> Flush() override;
+ const TNameTablePtr& GetNameTable() const override;
+
+ bool GetAllowUnknownColumns() const override;
+
+ void OnBeginRow() override;
+ void OnMyValue(const TUnversionedValue& value) override;
+ void OnEndRow() override;
+
+private:
+ const IUnversionedWriterPtr Writer_;
+ const i64 MaxRowBufferSize_;
+
+ const TRowBufferPtr RowBuffer_;
+
+ std::vector<TUnversionedRow> Rows_;
+ TCompactVector<TUnversionedValue, TypicalColumnCount> Values_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/versioned_reader.cpp b/yt/yt/client/table_client/versioned_reader.cpp
new file mode 100644
index 0000000000..297855051d
--- /dev/null
+++ b/yt/yt/client/table_client/versioned_reader.cpp
@@ -0,0 +1,82 @@
+#include "versioned_reader.h"
+
+#include <yt/yt_proto/yt/client/chunk_client/proto/data_statistics.pb.h>
+
+namespace NYT::NTableClient {
+
+using namespace NChunkClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEmptyVersionedReader
+ : public IVersionedReader
+{
+public:
+ explicit TEmptyVersionedReader(int rowCount)
+ : RowCount_(rowCount)
+ { }
+
+ TFuture<void> Open() override
+ {
+ return VoidFuture;
+ }
+
+ IVersionedRowBatchPtr Read(const TRowBatchReadOptions& options) override
+ {
+ if (RowCount_ == 0) {
+ return nullptr;
+ }
+
+ std::vector<TVersionedRow> rows;
+ int rowCount = std::min<i64>(options.MaxRowsPerRead, RowCount_);
+ rows.reserve(rowCount);
+ for (int index = 0; index < rowCount; ++index) {
+ rows.push_back(TVersionedRow());
+ }
+
+ RowCount_ -= rowCount;
+
+ return CreateBatchFromVersionedRows(MakeSharedRange(std::move(rows)));
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ return VoidFuture;
+ }
+
+ NChunkClient::NProto::TDataStatistics GetDataStatistics() const override
+ {
+ return NChunkClient::NProto::TDataStatistics();
+ }
+
+ TCodecStatistics GetDecompressionStatistics() const override
+ {
+ return NChunkClient::TCodecStatistics();
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ return true;
+ }
+
+ std::vector<TChunkId> GetFailedChunkIds() const override
+ {
+ return std::vector<TChunkId>();
+ }
+
+private:
+ int RowCount_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TEmptyVersionedReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedReaderPtr CreateEmptyVersionedReader(int rowCount)
+{
+ return New<TEmptyVersionedReader>(rowCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/versioned_reader.h b/yt/yt/client/table_client/versioned_reader.h
new file mode 100644
index 0000000000..968a34f3b8
--- /dev/null
+++ b/yt/yt/client/table_client/versioned_reader.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "config.h"
+#include "versioned_row.h"
+#include "row_batch.h"
+
+#include <yt/yt/client/chunk_client/reader_base.h>
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Reads a schemaful versioned rowset.
+/*!
+ * Versioned rowset implies that it is:
+ * 1. Schemaful.
+ * 2. Sorted.
+ * 3. No two rows share the same key.
+ *
+ * Useful for: merging and compactions.
+ */
+struct IVersionedReader
+ : public virtual NChunkClient::IReaderBase
+{
+ virtual TFuture<void> Open() = 0;
+
+ //! Tries to read more rows from the reader.
+ /*!
+ * Depending on implementation, rows may come in two different flavors.
+ * (A) Rows containing no more than one versioned value for each cell,
+ * no more than one write timestamp (the last one), and no more than one
+ * delete timestamp (for merging).
+ * (B) Rows containing all available versions and two lists of timestamps: write and delete
+ * (for compactions).
+ *
+ * Value ids correspond to column indexes in schema.
+ * The returned rows are canonically sorted (see TVersionedRow).
+ *
+ * If |nullptr| is returned then the end of the rowset is reached.
+ * If non-null value is returned but the batch is empty then no more data is available
+ * at the moment. The caller must wait for the asynchronous flag provided by #GetReadyEvent
+ * to become set. The latter may indicate an error occurred while fetching more data.
+ *
+ * In Case A above row timestamps have the following meaning:
+ * 1. If the row is found and is known to be deleted then only the deletion
+ * timestamp is provided.
+ * 2. If the row is found and is known to exist then the last write
+ * timestamp and the last deleted timestamp (if exists) are provided.
+ */
+ virtual IVersionedRowBatchPtr Read(const TRowBatchReadOptions& options = {}) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IVersionedReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IVersionedReaderPtr CreateEmptyVersionedReader(int rowCount = 0);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/versioned_row.cpp b/yt/yt/client/table_client/versioned_row.cpp
new file mode 100644
index 0000000000..4d38689ff9
--- /dev/null
+++ b/yt/yt/client/table_client/versioned_row.cpp
@@ -0,0 +1,602 @@
+#include "versioned_row.h"
+
+#include "name_table.h"
+#include "row_buffer.h"
+#include "schema.h"
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <numeric>
+
+namespace NYT::NTableClient {
+
+using namespace NTransactionClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetByteSize(const TVersionedValue& value)
+{
+ return GetByteSize(static_cast<TUnversionedValue>(value)) + MaxVarInt64Size;
+}
+
+size_t GetDataWeight(const TVersionedValue& value)
+{
+ return GetDataWeight(static_cast<TUnversionedValue>(value)) + sizeof(TTimestamp);
+}
+
+size_t ReadValue(const char* input, TVersionedValue* value)
+{
+ int result = ReadRowValue(input, static_cast<TUnversionedValue*>(value));
+ result += ReadVarUint64(input + result, &value->Timestamp);
+ return result;
+}
+
+size_t WriteValue(char* output, const TVersionedValue& value)
+{
+ int result = WriteRowValue(output, static_cast<TUnversionedValue>(value));
+ result += WriteVarUint64(output + result, value.Timestamp);
+ return result;
+}
+
+void Save(TStreamSaveContext& context, const TVersionedValue& value)
+{
+ NYT::Save(context, value.Timestamp);
+ NTableClient::Save(context, static_cast<const TUnversionedValue&>(value));
+}
+
+void Load(TStreamLoadContext& context, TVersionedValue& value, TChunkedMemoryPool* pool)
+{
+ NYT::Load(context, value.Timestamp);
+ NTableClient::Load(context, static_cast<TUnversionedValue&>(value), pool);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetVersionedRowByteSize(
+ int keyCount,
+ int valueCount,
+ int writeTimestampCount,
+ int deleteTimestampCount)
+{
+ return
+ sizeof(TVersionedRowHeader) +
+ sizeof(TUnversionedValue) * keyCount +
+ sizeof(TVersionedValue) * valueCount +
+ sizeof(TTimestamp) * writeTimestampCount +
+ sizeof(TTimestamp) * deleteTimestampCount;
+}
+
+size_t GetDataWeight(TVersionedRow row)
+{
+ if (!row) {
+ return 0;
+ }
+
+ size_t result = 0;
+ result += std::accumulate(
+ row.BeginValues(),
+ row.EndValues(),
+ 0ll,
+ [] (size_t x, const TVersionedValue& value) {
+ return GetDataWeight(value) + x;
+ });
+
+ result += std::accumulate(
+ row.BeginKeys(),
+ row.EndKeys(),
+ 0ll,
+ [] (size_t x, const TUnversionedValue& value) {
+ return GetDataWeight(value) + x;
+ });
+
+ result += row.GetWriteTimestampCount() * sizeof(TTimestamp);
+ result += row.GetDeleteTimestampCount() * sizeof(TTimestamp);
+
+ return result;
+}
+
+void ValidateClientDataRow(
+ TVersionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable,
+ bool allowMissingKeyColumns)
+{
+ int keyCount = row.GetKeyCount();
+ ValidateKeyColumnCount(keyCount);
+ ValidateRowValueCount(row.GetValueCount());
+
+ if (!allowMissingKeyColumns) {
+ if (keyCount != schema.GetKeyColumnCount()) {
+ THROW_ERROR_EXCEPTION("Invalid key count: expected %v, got %v",
+ schema.GetKeyColumnCount(),
+ keyCount);
+ }
+
+ if (nameTable->GetSize() < keyCount) {
+ THROW_ERROR_EXCEPTION("Name table size is too small to contain all keys: expected >=%v, got %v",
+ row.GetKeyCount(),
+ nameTable->GetSize());
+ }
+
+ for (int index = 0; index < keyCount; ++index) {
+ const auto& expectedName = schema.Columns()[index].Name();
+ auto actualName = nameTable->GetName(index);
+ if (expectedName != actualName) {
+ THROW_ERROR_EXCEPTION("Invalid key column %v in name table: expected %Qv, got %Qv",
+ index,
+ expectedName,
+ actualName);
+ }
+ ValidateValueType(row.Keys()[index], schema, index, /*typeAnyAcceptsAllValues*/ false);
+ ValidateKeyValue(row.Keys()[index]);
+ }
+ } else {
+ for (int index = 0; index < keyCount; ++index) {
+ auto name = nameTable->GetName(index);
+ int columnIndex = schema.GetColumnIndexOrThrow(name);
+ if (columnIndex >= schema.GetColumnCount()) {
+ THROW_ERROR_EXCEPTION("Invalid key column %Qv: appears as a key column in row but actually is a data column in table",
+ name);
+ }
+ ValidateValueType(row.Keys()[index], schema, columnIndex, /*typeAnyAcceptsAllValues*/ false);
+ ValidateKeyValue(row.Keys()[index]);
+ }
+ }
+
+ auto validateTimestamps = [&] (const TTimestamp* begin, const TTimestamp* end) {
+ for (const auto* current = begin; current != end; ++current) {
+ ValidateWriteTimestamp(*current);
+ if (current != begin && *current >= *(current - 1)) {
+ THROW_ERROR_EXCEPTION("Timestamps are not monotonically decreasing: %v >= %v",
+ *current,
+ *(current - 1));
+ }
+ }
+ };
+ validateTimestamps(row.BeginWriteTimestamps(), row.EndWriteTimestamps());
+ validateTimestamps(row.BeginDeleteTimestamps(), row.EndDeleteTimestamps());
+
+ for (const auto* current = row.BeginValues(); current != row.EndValues(); ++current) {
+ if (current != row.BeginValues()) {
+ auto* prev = current - 1;
+ if (current->Id < prev->Id) {
+ THROW_ERROR_EXCEPTION("Value ids must be non-decreasing: %v < %v",
+ current->Id,
+ prev->Id);
+ }
+ if (current->Id == prev->Id && current->Timestamp >= prev->Timestamp) {
+ THROW_ERROR_EXCEPTION("Value timestamps must be decreasing: %v >= %v",
+ current->Timestamp,
+ prev->Timestamp);
+ }
+ }
+
+ const auto& value = *current;
+ int mappedId = ApplyIdMapping(value, &idMapping);
+
+ if (mappedId < 0 || mappedId >= std::ssize(schema.Columns())) {
+ int size = nameTable->GetSize();
+ if (value.Id < 0 || value.Id >= size) {
+ THROW_ERROR_EXCEPTION("Expected value id in range [0:%v] but got %v",
+ size - 1,
+ value.Id);
+ }
+
+ THROW_ERROR_EXCEPTION("Unexpected column %Qv", nameTable->GetName(value.Id));
+ }
+
+ if (mappedId < keyCount) {
+ THROW_ERROR_EXCEPTION("Key component %v appears in value part",
+ schema.Columns()[mappedId].GetDiagnosticNameString());
+ }
+
+ const auto& column = schema.Columns()[mappedId];
+ ValidateValueType(value, schema, mappedId, /*typeAnyAcceptsAllValues*/ false);
+
+ if (Any(value.Flags & EValueFlags::Aggregate) && !column.Aggregate()) {
+ THROW_ERROR_EXCEPTION(
+ "\"aggregate\" flag is set for value in non-aggregating column %v",
+ column.GetDiagnosticNameString());
+ }
+
+ if (mappedId < schema.GetKeyColumnCount()) {
+ THROW_ERROR_EXCEPTION("Key column %v in values",
+ column.GetDiagnosticNameString());
+ }
+
+ ValidateDataValue(value);
+ }
+
+ auto dataWeight = GetDataWeight(row);
+ if (dataWeight >= MaxClientVersionedRowDataWeight) {
+ THROW_ERROR_EXCEPTION("Row is too large: data weight %v, limit %v",
+ dataWeight,
+ MaxClientVersionedRowDataWeight);
+ }
+}
+
+void ValidateDuplicateAndRequiredValueColumns(
+ TVersionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer,
+ const TTimestamp* writeTimestamps,
+ int writeTimestampCount)
+{
+ if (writeTimestampCount == 0) {
+ return;
+ }
+
+ auto& columnSeen = *columnPresenceBuffer;
+ YT_VERIFY(std::ssize(columnSeen) >= schema.GetColumnCount());
+ std::fill(columnSeen.begin(), columnSeen.end(), 0);
+
+ for (const auto *valueGroupBeginIt = row.BeginValues(), *valueGroupEndIt = valueGroupBeginIt;
+ valueGroupBeginIt != row.EndValues();
+ valueGroupBeginIt = valueGroupEndIt)
+ {
+ while (valueGroupEndIt != row.EndValues() && valueGroupBeginIt->Id == valueGroupEndIt->Id) {
+ ++valueGroupEndIt;
+ }
+
+ int mappedId = ApplyIdMapping(*valueGroupBeginIt, &idMapping);
+ if (mappedId < 0) {
+ continue;
+ }
+ const auto& column = schema.Columns()[mappedId];
+
+ if (columnSeen[mappedId]) {
+ THROW_ERROR_EXCEPTION("Duplicate value group %v in versioned row",
+ column.GetDiagnosticNameString());
+ }
+ columnSeen[mappedId] = true;
+
+ if (column.Required()) {
+ auto mismatch = std::mismatch(
+ writeTimestamps,
+ writeTimestamps + writeTimestampCount,
+ valueGroupBeginIt,
+ valueGroupEndIt,
+ [] (TTimestamp expected, const TVersionedValue& actual) {
+ return expected == actual.Timestamp;
+ }
+ );
+ if (mismatch.first == writeTimestamps + writeTimestampCount) {
+ if (mismatch.second != valueGroupEndIt) {
+ THROW_ERROR_EXCEPTION(
+ "Row-wise write timestamps do not contain write timestamp %v for column %v",
+ *mismatch.second,
+ column.GetDiagnosticNameString());
+ }
+ } else {
+ THROW_ERROR_EXCEPTION(
+ "Required column %v does not contain value for timestamp %Qv",
+ column.GetDiagnosticNameString(),
+ *mismatch.first);
+ }
+ }
+ }
+
+ for (int index = schema.GetKeyColumnCount(); index < schema.GetColumnCount(); ++index) {
+ if (!columnSeen[index] && schema.Columns()[index].Required()) {
+ THROW_ERROR_EXCEPTION("Missing values for required column %v",
+ schema.Columns()[index].GetDiagnosticNameString());
+ }
+ }
+}
+
+TLegacyOwningKey ToOwningKey(TVersionedRow row)
+{
+ TUnversionedOwningRowBuilder builder;
+ for (const auto& value : row.Keys()) {
+ builder.AddValue(value);
+ }
+ return builder.FinishRow();
+}
+
+TKeyRef ToKeyRef(TVersionedRow row)
+{
+ return row.Keys();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVersionedRowBuilder::TVersionedRowBuilder(TRowBufferPtr buffer, bool compaction)
+ : Buffer_(std::move(buffer))
+ , Compaction_(compaction)
+{ }
+
+void TVersionedRowBuilder::AddKey(const TUnversionedValue& value)
+{
+ Keys_.push_back(Buffer_->CaptureValue(value));
+}
+
+void TVersionedRowBuilder::AddValue(const TVersionedValue& value)
+{
+ WriteTimestamps_.push_back(value.Timestamp);
+ Values_.push_back(Buffer_->CaptureValue(value));
+}
+
+void TVersionedRowBuilder::AddDeleteTimestamp(TTimestamp timestamp)
+{
+ DeleteTimestamps_.push_back(timestamp);
+}
+
+void TVersionedRowBuilder::AddWriteTimestamp(TTimestamp timestamp)
+{
+ WriteTimestamps_.push_back(timestamp);
+}
+
+TMutableVersionedRow TVersionedRowBuilder::FinishRow()
+{
+ std::sort(
+ Values_.begin(),
+ Values_.end(),
+ [] (const TVersionedValue& lhs, const TVersionedValue& rhs) -> bool {
+ if (lhs.Id < rhs.Id) {
+ return true;
+ }
+ if (lhs.Id > rhs.Id) {
+ return false;
+ }
+ if (lhs.Timestamp < rhs.Timestamp) {
+ return false;
+ }
+ if (lhs.Timestamp > rhs.Timestamp) {
+ return true;
+ }
+ return false;
+ });
+
+ std::sort(WriteTimestamps_.begin(), WriteTimestamps_.end(), std::greater<TTimestamp>());
+
+ if (Compaction_) {
+ WriteTimestamps_.erase(
+ std::unique(WriteTimestamps_.begin(), WriteTimestamps_.end()),
+ WriteTimestamps_.end());
+ } else if (!WriteTimestamps_.empty()) {
+ WriteTimestamps_.erase(WriteTimestamps_.begin() + 1, WriteTimestamps_.end());
+ }
+
+ std::sort(DeleteTimestamps_.begin(), DeleteTimestamps_.end(), std::greater<TTimestamp>());
+ DeleteTimestamps_.erase(
+ std::unique(DeleteTimestamps_.begin(), DeleteTimestamps_.end()),
+ DeleteTimestamps_.end());
+
+ auto row = Buffer_->AllocateVersioned(
+ Keys_.size(),
+ Values_.size(),
+ WriteTimestamps_.size(),
+ DeleteTimestamps_.size());
+
+ memcpy(row.BeginKeys(), Keys_.data(), sizeof (TUnversionedValue) * Keys_.size());
+ memcpy(row.BeginValues(), Values_.data(), sizeof (TVersionedValue)* Values_.size());
+ memcpy(row.BeginWriteTimestamps(), WriteTimestamps_.data(), sizeof (TTimestamp) * WriteTimestamps_.size());
+ memcpy(row.BeginDeleteTimestamps(), DeleteTimestamps_.data(), sizeof (TTimestamp) * DeleteTimestamps_.size());
+
+ Keys_.clear();
+ Values_.clear();
+ WriteTimestamps_.clear();
+ DeleteTimestamps_.clear();
+
+ return row;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVersionedOwningRow::TVersionedOwningRow(TVersionedRow other)
+{
+ if (!other) {
+ return;
+ }
+
+ size_t fixedSize = GetVersionedRowByteSize(
+ other.GetKeyCount(),
+ other.GetValueCount(),
+ other.GetWriteTimestampCount(),
+ other.GetDeleteTimestampCount());
+
+ size_t variableSize = 0;
+ auto adjustVariableSize = [&] (const TUnversionedValue& value) {
+ if (IsStringLikeType(value.Type)) {
+ variableSize += value.Length;
+ }
+ };
+
+ for (const auto& value : other.Keys()) {
+ adjustVariableSize(value);
+ }
+ for (const auto& value : other.Values()) {
+ adjustVariableSize(value);
+ }
+
+ Data_ = TSharedMutableRef::Allocate(fixedSize + variableSize, {.InitializeStorage = false});
+
+ ::memcpy(GetMutableHeader(), other.GetHeader(), fixedSize);
+
+ if (variableSize > 0) {
+ char* current = Data_.Begin() + fixedSize;
+ auto captureValue = [&] (TUnversionedValue* value) {
+ if (IsStringLikeType(value->Type)) {
+ ::memcpy(current, value->Data.String, value->Length);
+ value->Data.String = current;
+ current += value->Length;
+ }
+ };
+
+ for (int index = 0; index < other.GetKeyCount(); ++index) {
+ captureValue(BeginMutableKeys() + index);
+ }
+ for (int index = 0; index < other.GetValueCount(); ++index) {
+ captureValue(BeginMutableValues() + index);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TVersionedValue& value, TStringBuf /*format*/)
+{
+ Format(
+ builder,
+ "%v@%x",
+ static_cast<TUnversionedValue>(value),
+ value.Timestamp);
+}
+
+void FormatValue(TStringBuilderBase* builder, TVersionedRow row, TStringBuf /*format*/)
+{
+ if (!row) {
+ builder->AppendString("<null>");
+ return;
+ }
+
+ builder->AppendChar('[');
+ JoinToString(
+ builder,
+ row.BeginKeys(),
+ row.EndKeys(),
+ [&] (TStringBuilderBase* builder, const TUnversionedValue& value) {
+ FormatValue(builder, value, "k");
+ });
+ builder->AppendString(" | ");
+ JoinToString(builder, row.BeginValues(), row.EndValues(), TDefaultFormatter{});
+ builder->AppendString(" | ");
+ JoinToString(
+ builder,
+ row.BeginWriteTimestamps(),
+ row.EndWriteTimestamps(),
+ [] (TStringBuilderBase* builder, TTimestamp timestamp) {
+ builder->AppendFormat("%x", timestamp);
+ });
+ builder->AppendString(" | ");
+ JoinToString(
+ builder,
+ row.BeginDeleteTimestamps(),
+ row.EndDeleteTimestamps(),
+ [] (TStringBuilderBase* builder, TTimestamp timestamp) {
+ builder->AppendFormat("%x", timestamp);
+ });
+ builder->AppendChar(']');
+}
+
+void FormatValue(TStringBuilderBase* builder, TMutableVersionedRow row, TStringBuf /*format*/)
+{
+ FormatValue(builder, TVersionedRow(row), {});
+}
+
+void FormatValue(TStringBuilderBase* builder, TVersionedOwningRow row, TStringBuf /*format*/)
+{
+ FormatValue(builder, TVersionedRow(row), {});
+}
+
+TString ToString(const TVersionedValue& value)
+{
+ return ToStringViaBuilder(value);
+}
+
+TString ToString(TVersionedRow row)
+{
+ return ToStringViaBuilder(row);
+}
+
+TString ToString(TMutableVersionedRow row)
+{
+ return ToString(TVersionedRow(row));
+}
+
+TString ToString(const TVersionedOwningRow& row)
+{
+ return ToString(row.Get());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TBitwiseVersionedValueHash::operator()(const TVersionedValue& value) const
+{
+ size_t result = 0;
+ HashCombine(result, value.Timestamp);
+ HashCombine(result, TBitwiseUnversionedValueHash()(value));
+ return result;
+}
+
+bool TBitwiseVersionedValueEqual::operator()(const TVersionedValue& lhs, const TVersionedValue& rhs) const
+{
+ return lhs.Timestamp == rhs.Timestamp && TBitwiseUnversionedValueEqual()(lhs, rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TBitwiseVersionedRowHash::operator()(TVersionedRow row) const
+{
+ size_t result = 0;
+ for (const auto& value : row.Keys()) {
+ HashCombine(result, TBitwiseUnversionedValueHash()(value));
+ }
+ for (const auto& value : row.Values()) {
+ HashCombine(result, TBitwiseVersionedValueHash()(value));
+ }
+ // Write timestamps are omitted since these are inferred from values.
+ for (auto timestamp : row.DeleteTimestamps()) {
+ HashCombine(result, timestamp);
+ }
+ return result;
+}
+
+bool TBitwiseVersionedRowEqual::operator()(TVersionedRow lhs, TVersionedRow rhs) const
+{
+ if (!lhs && !rhs) {
+ return true;
+ }
+
+ if (!lhs || !rhs) {
+ return false;
+ }
+
+ if (lhs.GetKeyCount() != rhs.GetKeyCount()) {
+ return false;
+ }
+
+ for (int index = 0; index < lhs.GetKeyCount(); ++index) {
+ if (!TBitwiseUnversionedValueEqual()(lhs.Keys()[index], lhs.Keys()[index])) {
+ return false;
+ }
+ }
+
+ if (lhs.GetValueCount() != rhs.GetValueCount()) {
+ return false;
+ }
+
+ for (int i = 0; i < lhs.GetValueCount(); ++i) {
+ if (!TBitwiseVersionedValueEqual()(lhs.Values()[i], rhs.Values()[i])) {
+ return false;
+ }
+ }
+
+ if (lhs.GetWriteTimestampCount() != rhs.GetWriteTimestampCount()) {
+ return false;
+ }
+
+ for (int i = 0; i < lhs.GetWriteTimestampCount(); ++i) {
+ if (lhs.WriteTimestamps()[i] != rhs.WriteTimestamps()[i]) {
+ return false;
+ }
+ }
+
+ if (lhs.GetDeleteTimestampCount() != rhs.GetDeleteTimestampCount()) {
+ return false;
+ }
+
+ for (int i = 0; i < lhs.GetDeleteTimestampCount(); ++i) {
+ if (lhs.DeleteTimestamps()[i] != rhs.DeleteTimestamps()[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/versioned_row.h b/yt/yt/client/table_client/versioned_row.h
new file mode 100644
index 0000000000..515697e531
--- /dev/null
+++ b/yt/yt/client/table_client/versioned_row.h
@@ -0,0 +1,666 @@
+#pragma once
+
+#include "public.h"
+#include "unversioned_row.h"
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TVersionedValue
+ : public TUnversionedValue
+{
+ TTimestamp Timestamp;
+};
+
+static_assert(
+ sizeof(TVersionedValue) == 24,
+ "TVersionedValue has to be exactly 24 bytes.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TVersionedValue MakeVersionedValue(const TUnversionedValue& value, TTimestamp timestamp)
+{
+ TVersionedValue result;
+ static_cast<TUnversionedValue&>(result) = value;
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedSentinelValue(EValueType type, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeSentinelValue<TVersionedValue>(type, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedInt64Value(i64 value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeInt64Value<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedUint64Value(ui64 value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeUint64Value<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedDoubleValue(double value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeDoubleValue<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedBooleanValue(bool value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeBooleanValue<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedStringValue(TStringBuf value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeStringValue<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+inline TVersionedValue MakeVersionedAnyValue(TStringBuf value, TTimestamp timestamp, int id = 0, EValueFlags flags = EValueFlags::None)
+{
+ auto result = MakeAnyValue<TVersionedValue>(value, id, flags);
+ result.Timestamp = timestamp;
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TVersionedRowHeader
+{
+ ui32 ValueCount;
+ ui32 KeyCount;
+ ui32 WriteTimestampCount;
+ ui32 DeleteTimestampCount;
+};
+
+static_assert(
+ sizeof(TVersionedRowHeader) == 16,
+ "TVersionedRowHeader has to be exactly 16 bytes.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetByteSize(const TVersionedValue& value);
+size_t GetDataWeight(const TVersionedValue& value);
+
+size_t ReadValue(const char* input, TVersionedValue* value);
+size_t WriteValue(char* output, const TVersionedValue& value);
+
+void Save(TStreamSaveContext& context, const TVersionedValue& value);
+void Load(TStreamLoadContext& context, TVersionedValue& value, TChunkedMemoryPool* pool);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the number of bytes needed to store a versioned row (not including string data).
+size_t GetVersionedRowByteSize(
+ int keyCount,
+ int valueCount,
+ int writeTimestampCount,
+ int deleteTimestampCount);
+
+//! A row with versioned data.
+/*!
+ * A lightweight wrapper around |TVersionedRowHeader*|.
+ *
+ * Provides access to the following parts:
+ * 1) write timestamps, sorted in descending order;
+ * at most one if a specific revision is requested;
+ * 2) delete timestamps, sorted in descending order;
+ * at most one if a specific revision is requested;
+ * 3) unversioned keys;
+ * 4) versioned values, sorted by |id| (in ascending order) and then by |timestamp| (in descending order);
+ * note that no position-to-id matching is ever assumed.
+ *
+ * The order of values described in 4) is typically referred to as "canonical".
+ *
+ * Memory layout:
+ * 1) TVersionedRowHeader
+ * 2) TTimestamp per each write timestamp (#TVersionedRowHeader::WriteTimestampCount)
+ * 3) TTimestamp per each delete timestamp (#TVersionedRowHeader::DeleteTimestampCount)
+ * 4) TUnversionedValue per each key (#TVersionedRowHeader::KeyCount)
+ * 5) TVersionedValue per each value (#TVersionedRowHeader::ValueCount)
+ */
+class TVersionedRow
+{
+public:
+ TVersionedRow() = default;
+
+ explicit TVersionedRow(const TVersionedRowHeader* header)
+ : Header_(header)
+ { }
+
+ explicit TVersionedRow(TTypeErasedRow erased)
+ : Header_(reinterpret_cast<const TVersionedRowHeader*>(erased.OpaqueHeader))
+ { }
+
+ explicit operator bool()
+ {
+ return Header_ != nullptr;
+ }
+
+ TTypeErasedRow ToTypeErasedRow() const
+ {
+ return {Header_};
+ }
+
+ const TVersionedRowHeader* GetHeader() const
+ {
+ return Header_;
+ }
+
+ const TTimestamp* BeginWriteTimestamps() const
+ {
+ return reinterpret_cast<const TTimestamp*>(Header_ + 1);
+ }
+
+ const TTimestamp* EndWriteTimestamps() const
+ {
+ return BeginWriteTimestamps() + GetWriteTimestampCount();
+ }
+
+ TTimestampRange WriteTimestamps() const
+ {
+ return {BeginWriteTimestamps(), EndWriteTimestamps()};
+ }
+
+ const TTimestamp* BeginDeleteTimestamps() const
+ {
+ return EndWriteTimestamps();
+ }
+
+ const TTimestamp* EndDeleteTimestamps() const
+ {
+ return BeginDeleteTimestamps() + GetDeleteTimestampCount();
+ }
+
+ TTimestampRange DeleteTimestamps() const
+ {
+ return {BeginDeleteTimestamps(), EndDeleteTimestamps()};
+ }
+
+ const TUnversionedValue* BeginKeys() const
+ {
+ return reinterpret_cast<const TUnversionedValue*>(EndDeleteTimestamps());
+ }
+
+ const TUnversionedValue* EndKeys() const
+ {
+ return BeginKeys() + GetKeyCount();
+ }
+
+ TUnversionedValueRange Keys() const
+ {
+ return {BeginKeys(), EndKeys()};
+ }
+
+ const TVersionedValue* BeginValues() const
+ {
+ return reinterpret_cast<const TVersionedValue*>(EndKeys());
+ }
+
+ const TVersionedValue* EndValues() const
+ {
+ return BeginValues() + GetValueCount();
+ }
+
+ TVersionedValueRange Values() const
+ {
+ return {BeginValues(), EndValues()};
+ }
+
+ int GetKeyCount() const
+ {
+ return Header_->KeyCount;
+ }
+
+ int GetValueCount() const
+ {
+ return Header_->ValueCount;
+ }
+
+ int GetWriteTimestampCount() const
+ {
+ return Header_->WriteTimestampCount;
+ }
+
+ int GetDeleteTimestampCount() const
+ {
+ return Header_->DeleteTimestampCount;
+ }
+
+ const char* BeginMemory() const
+ {
+ return reinterpret_cast<const char*>(Header_);
+ }
+
+ const char* EndMemory() const
+ {
+ return BeginMemory() + GetVersionedRowByteSize(
+ GetKeyCount(),
+ GetValueCount(),
+ GetWriteTimestampCount(),
+ GetDeleteTimestampCount());
+ }
+
+private:
+ const TVersionedRowHeader* Header_ = nullptr;
+};
+
+static_assert(
+ sizeof(TVersionedRow) == sizeof(intptr_t),
+ "TVersionedRow size must match that of a pointer.");
+
+size_t GetDataWeight(TVersionedRow row);
+
+//! Checks that #row is a valid client-side versioned data row. Throws on failure.
+/*!
+ * Value ids in the row are first mapped via #idMapping.
+ * The row must obey the following properties:
+ * 1. Its value count must pass #ValidateRowValueCount checks.
+ * 2. Its key count must match the number of keys in #schema.
+ * 3. Name table must contain all key columns in the same order as in the schema.
+ * 4. Write and delete timestamps must pass #ValidateWriteTimestamp test and must be decreasing.
+ * 5. Value part must not contain key components.
+ * 6. Value types must either be null or match those given in #schema.
+ * 7. For values marked with #TUnversionedValue::Aggregate flag, the corresponding columns in #schema must
+ * be aggregating.
+ */
+void ValidateClientDataRow(
+ TVersionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ const TNameTablePtr& nameTable,
+ bool allowMissingKeyColumns = false);
+
+//! Checks that #row contains no duplicate value columns and that each required column
+//! contains a value for each known write timestamp.
+//! Skips values that map to negative ids with via #idMapping.
+/*! It is assumed that ValidateClientDataRow was called before. */
+void ValidateDuplicateAndRequiredValueColumns(
+ TVersionedRow row,
+ const TTableSchema& schema,
+ const TNameTableToSchemaIdMapping& idMapping,
+ std::vector<bool>* columnPresenceBuffer,
+ const TTimestamp* writeTimestamps,
+ int writeTimestampCount);
+
+TLegacyOwningKey ToOwningKey(TVersionedRow row);
+TKeyRef ToKeyRef(TVersionedRow row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A variant of TVersionedRow that enables mutating access to its content.
+class TMutableVersionedRow
+ : public TVersionedRow
+{
+public:
+ TMutableVersionedRow() = default;
+
+ explicit TMutableVersionedRow(TVersionedRowHeader* header)
+ : TVersionedRow(header)
+ { }
+
+ explicit TMutableVersionedRow(TTypeErasedRow row)
+ : TVersionedRow(reinterpret_cast<const TVersionedRowHeader*>(row.OpaqueHeader))
+ { }
+
+ static TMutableVersionedRow Allocate(
+ TChunkedMemoryPool* pool,
+ int keyCount,
+ int valueCount,
+ int writeTimestampCount,
+ int deleteTimestampCount)
+ {
+ size_t byteSize = GetVersionedRowByteSize(
+ keyCount,
+ valueCount,
+ writeTimestampCount,
+ deleteTimestampCount);
+ auto* header = reinterpret_cast<TVersionedRowHeader*>(pool->AllocateAligned(byteSize));
+ header->KeyCount = keyCount;
+ header->ValueCount = valueCount;
+ header->WriteTimestampCount = writeTimestampCount;
+ header->DeleteTimestampCount = deleteTimestampCount;
+ return TMutableVersionedRow(header);
+ }
+
+ TVersionedRowHeader* GetHeader()
+ {
+ return const_cast<TVersionedRowHeader*>(TVersionedRow::GetHeader());
+ }
+
+ TTimestamp* BeginWriteTimestamps()
+ {
+ return reinterpret_cast<TTimestamp*>(GetHeader() + 1);
+ }
+
+ TTimestamp* EndWriteTimestamps()
+ {
+ return BeginWriteTimestamps() + GetWriteTimestampCount();
+ }
+
+ TMutableTimestampRange WriteTimestamps()
+ {
+ return {BeginWriteTimestamps(), EndWriteTimestamps()};
+ }
+
+ TTimestamp* BeginDeleteTimestamps()
+ {
+ return EndWriteTimestamps();
+ }
+
+ TTimestamp* EndDeleteTimestamps()
+ {
+ return BeginDeleteTimestamps() + GetDeleteTimestampCount();
+ }
+
+ TMutableTimestampRange DeleteTimestamps()
+ {
+ return {BeginDeleteTimestamps(), EndDeleteTimestamps()};
+ }
+
+ TUnversionedValue* BeginKeys()
+ {
+ return reinterpret_cast<TUnversionedValue*>(EndDeleteTimestamps());
+ }
+
+ TUnversionedValue* EndKeys()
+ {
+ return BeginKeys() + GetKeyCount();
+ }
+
+ TMutableUnversionedValueRange Keys()
+ {
+ return {BeginKeys(), EndKeys()};
+ }
+
+ TVersionedValue* BeginValues()
+ {
+ return reinterpret_cast<TVersionedValue*>(EndKeys());
+ }
+
+ TVersionedValue* EndValues()
+ {
+ return BeginValues() + GetValueCount();
+ }
+
+ TMutableVersionedValueRange Values()
+ {
+ return {BeginValues(), EndValues()};
+ }
+
+ void SetKeyCount(int count)
+ {
+ GetHeader()->KeyCount = count;
+ }
+
+ void SetValueCount(int count)
+ {
+ GetHeader()->ValueCount = count;
+ }
+
+ void SetWriteTimestampCount(int count)
+ {
+ GetHeader()->WriteTimestampCount = count;
+ }
+
+ void SetDeleteTimestampCount(int count)
+ {
+ GetHeader()->DeleteTimestampCount = count;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper used for constructing TVersionedRow instances.
+/*!
+ * Not very efficient, only useful in tests.
+ * The resulting row is canonically ordered.
+ */
+class TVersionedRowBuilder
+{
+public:
+ /*!
+ * \param compaction - if unset, builder creates only one, latest write timestamp.
+ */
+ explicit TVersionedRowBuilder(TRowBufferPtr buffer, bool compaction = true);
+
+ void AddKey(const TUnversionedValue& value);
+ void AddValue(const TVersionedValue& value);
+ void AddDeleteTimestamp(TTimestamp timestamp);
+
+ // Sometimes versioned row have write timestamps without corresponding values,
+ // when reading with column filter.
+ void AddWriteTimestamp(TTimestamp timestamp);
+
+ TMutableVersionedRow FinishRow();
+
+private:
+ const TRowBufferPtr Buffer_;
+ const bool Compaction_;
+
+ std::vector<TUnversionedValue> Keys_;
+ std::vector<TVersionedValue> Values_;
+ std::vector<TTimestamp> WriteTimestamps_;
+ std::vector<TTimestamp> DeleteTimestamps_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An owning variant of TVersionedRow.
+/*!
+ * Instances of TVersionedOwningRow are lightweight handles.
+ * All data is stored in shared ref-counted blobs.
+ */
+class TVersionedOwningRow
+{
+public:
+ TVersionedOwningRow() = default;
+
+ explicit TVersionedOwningRow(TVersionedRow other);
+
+ TVersionedOwningRow(const TVersionedOwningRow& other)
+ : Data_(other.Data_)
+ { }
+
+ TVersionedOwningRow(TVersionedOwningRow&& other)
+ : Data_(std::move(other.Data_))
+ { }
+
+ explicit operator bool() const
+ {
+ return static_cast<bool>(Data_);
+ }
+
+ TVersionedRow Get() const
+ {
+ return TVersionedRow(GetHeader());
+ }
+
+ operator TVersionedRow() const
+ {
+ return Get();
+ }
+
+ const TTimestamp* BeginWriteTimestamps() const
+ {
+ return reinterpret_cast<const TTimestamp*>(GetHeader() + 1);
+ }
+
+ const TTimestamp* EndWriteTimestamps() const
+ {
+ return BeginWriteTimestamps() + GetWriteTimestampCount();
+ }
+
+ TTimestampRange WriteTimestamps() const
+ {
+ return {BeginWriteTimestamps(), EndWriteTimestamps()};
+ }
+
+ const TTimestamp* BeginDeleteTimestamps() const
+ {
+ return EndWriteTimestamps();
+ }
+
+ const TTimestamp* EndDeleteTimestamps() const
+ {
+ return BeginDeleteTimestamps() + GetDeleteTimestampCount();
+ }
+
+ TTimestampRange DeleteTimestamps() const
+ {
+ return {BeginDeleteTimestamps(), EndDeleteTimestamps()};
+ }
+
+ const TUnversionedValue* BeginKeys() const
+ {
+ return reinterpret_cast<const TUnversionedValue*>(EndDeleteTimestamps());
+ }
+
+ const TUnversionedValue* EndKeys() const
+ {
+ return BeginKeys() + GetKeyCount();
+ }
+
+ TUnversionedValueRange Keys() const
+ {
+ return {BeginKeys(), EndKeys()};
+ }
+
+ const TVersionedValue* BeginValues() const
+ {
+ return reinterpret_cast<const TVersionedValue*>(EndKeys());
+ }
+
+ const TVersionedValue* EndValues() const
+ {
+ return BeginValues() + GetValueCount();
+ }
+
+ TVersionedValueRange Values() const
+ {
+ return {BeginValues(), EndValues()};
+ }
+
+ int GetKeyCount() const
+ {
+ return GetHeader()->KeyCount;
+ }
+
+ int GetValueCount() const
+ {
+ return GetHeader()->ValueCount;
+ }
+
+ int GetWriteTimestampCount() const
+ {
+ return GetHeader()->WriteTimestampCount;
+ }
+
+ int GetDeleteTimestampCount() const
+ {
+ return GetHeader()->DeleteTimestampCount;
+ }
+
+ size_t GetByteSize() const
+ {
+ return Data_.Size();
+ }
+
+
+ friend void swap(TVersionedOwningRow& lhs, TVersionedOwningRow& rhs)
+ {
+ using std::swap;
+ swap(lhs.Data_, rhs.Data_);
+ }
+
+ TVersionedOwningRow& operator=(const TVersionedOwningRow& other)
+ {
+ Data_ = other.Data_;
+ return *this;
+ }
+
+ TVersionedOwningRow& operator=(TVersionedOwningRow&& other)
+ {
+ Data_ = std::move(other.Data_);
+ return *this;
+ }
+
+private:
+ TSharedMutableRef Data_;
+
+
+ const TVersionedRowHeader* GetHeader() const
+ {
+ return Data_ ? reinterpret_cast<const TVersionedRowHeader*>(Data_.Begin()) : nullptr;
+ }
+
+ TVersionedRowHeader* GetMutableHeader()
+ {
+ return Data_ ? reinterpret_cast<TVersionedRowHeader*>(Data_.Begin()) : nullptr;
+ }
+
+ TUnversionedValue* BeginMutableKeys()
+ {
+ return const_cast<TUnversionedValue*>(BeginKeys());
+ }
+
+ TVersionedValue* BeginMutableValues()
+ {
+ return const_cast<TVersionedValue*>(BeginValues());
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TVersionedValue& value, TStringBuf format);
+void FormatValue(TStringBuilderBase* builder, TVersionedRow row, TStringBuf format);
+void FormatValue(TStringBuilderBase* builder, TMutableVersionedRow row, TStringBuf format);
+void FormatValue(TStringBuilderBase* builder, const TVersionedOwningRow& row, TStringBuf format);
+
+TString ToString(const TVersionedValue& value);
+TString ToString(TVersionedRow row);
+TString ToString(TMutableVersionedRow row);
+TString ToString(const TVersionedOwningRow& row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBitwiseVersionedValueHash
+{
+ size_t operator()(const TVersionedValue& value) const;
+};
+
+struct TBitwiseVersionedValueEqual
+{
+ bool operator()(const TVersionedValue& lhs, const TVersionedValue& rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBitwiseVersionedRowHash
+{
+ size_t operator()(TVersionedRow row) const;
+};
+
+struct TBitwiseVersionedRowEqual
+{
+ bool operator()(TVersionedRow lhs, TVersionedRow rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/versioned_writer.h b/yt/yt/client/table_client/versioned_writer.h
new file mode 100644
index 0000000000..7c4c3980ea
--- /dev/null
+++ b/yt/yt/client/table_client/versioned_writer.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "versioned_row.h"
+
+#include <yt/yt/client/chunk_client/writer_base.h>
+
+#include <yt/yt/core/misc/range.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Writes a schemaful versioned rowset.
+/*!
+ * Writes versioned rowset with given schema.
+ * Useful for: compactions.
+ */
+struct IVersionedWriter
+ : public virtual NChunkClient::IWriterBase
+{
+ //! Enqueues more rows into the writer.
+ /*!
+ * Value ids must correspond to column indexes in schema.
+ * The rows must be canonically sorted (see TVersionedRow).
+ *
+ * If |false| is returned then the writer is overflowed (but the data is nevertheless accepted)
+ * The caller must wait for asynchronous flag provided by #GetReadyEvent to become set.
+ * The latter may indicate an error occurred while fetching more data.
+ */
+ virtual bool Write(TRange<TVersionedRow> rows) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IVersionedWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/wire_protocol.cpp b/yt/yt/client/table_client/wire_protocol.cpp
new file mode 100644
index 0000000000..904969d78a
--- /dev/null
+++ b/yt/yt/client/table_client/wire_protocol.cpp
@@ -0,0 +1,1291 @@
+#include "wire_protocol.h"
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/bitmap.h>
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+#include <library/cpp/yt/memory/chunked_output_stream.h>
+#include <library/cpp/yt/memory/chunked_memory_pool.h>
+
+#include <util/system/sanitizers.h>
+
+#include <google/protobuf/io/coded_stream.h>
+
+namespace NYT::NTableClient {
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+using NChunkClient::NProto::TDataStatistics;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const static NLogging::TLogger Logger("WireProtocol");
+
+struct TWireProtocolWriterTag
+{ };
+
+struct TWireProtocolReaderTag
+{ };
+
+static constexpr size_t ReaderBufferChunkSize = 4096;
+
+static constexpr size_t WriterInitialBufferCapacity = 1024;
+static constexpr size_t PreallocateBlockSize = 4096;
+
+static constexpr ui64 MinusOne = static_cast<ui64>(-1);
+
+static_assert(sizeof(i64) == SerializationAlignment, "Wrong serialization alignment");
+static_assert(sizeof(double) == SerializationAlignment, "Wrong serialization alignment");
+static_assert(sizeof(TUnversionedValue) == 16, "sizeof(TUnversionedValue) != 16");
+static_assert(sizeof(TUnversionedValueData) == 8, "sizeof(TUnversionedValueData) == 8");
+static_assert(sizeof(TUnversionedRowHeader) == 8, "sizeof(TUnversionedRowHeader) != 8");
+static_assert(sizeof(TVersionedValue) == 24, "sizeof(TVersionedValue) != 24");
+static_assert(sizeof(TVersionedRowHeader) == 16, "sizeof(TVersionedRowHeader) != 16");
+
+////////////////////////////////////////////////////////////////////////////////
+
+EWireProtocolCommand GetWireProtocolCommand(const TWireProtocolWriteCommand& command)
+{
+ EWireProtocolCommand result = EWireProtocolCommand::LookupRows;
+ Visit(command,
+ [&] (const TWriteRowCommand&) { result = EWireProtocolCommand::WriteRow; },
+ [&] (const TDeleteRowCommand&) { result = EWireProtocolCommand::DeleteRow; },
+ [&] (const TVersionedWriteRowCommand&) { result = EWireProtocolCommand::VersionedWriteRow; },
+ [&] (const TWriteAndLockRowCommand&) { result = EWireProtocolCommand::WriteAndLockRow; },
+ [&] (auto) { YT_ABORT(); });
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireProtocolWriter
+ : public IWireProtocolWriter
+{
+public:
+ TWireProtocolWriter()
+ {
+ EnsureCapacity(WriterInitialBufferCapacity);
+ }
+
+ size_t GetByteSize() const override
+ {
+ return Stream_.GetSize();
+ }
+
+ void WriteCommand(EWireProtocolCommand command) override
+ {
+ WriteUint64(static_cast<unsigned int>(command));
+ }
+
+ void WriteLegacyLockBitmap(TLegacyLockBitmap lockBitmap) override
+ {
+ WriteUint64(lockBitmap);
+ }
+
+ void WriteLockMask(TLockMask lockMask) override
+ {
+ auto size = lockMask.GetSize();
+ YT_VERIFY(size <= TLockMask::MaxSize);
+ auto wordCount = DivCeil(size, TLockMask::LocksPerWord);
+
+ WriteUint16(size);
+
+ auto bitmap = lockMask.GetBitmap();
+ YT_VERIFY(std::ssize(bitmap) >= wordCount);
+ for (int index = 0; index < wordCount; ++index) {
+ WriteUint64(bitmap[index]);
+ }
+ }
+
+ void WriteTableSchema(const TTableSchema& schema) override
+ {
+ WriteMessage(ToProto<NTableClient::NProto::TTableSchemaExt>(schema));
+ }
+
+ void WriteMessage(const ::google::protobuf::MessageLite& message) override
+ {
+ size_t size = static_cast<size_t>(message.ByteSizeLong());
+ WriteUint64(size);
+ EnsureAlignedUpCapacity(size);
+ YT_VERIFY(message.SerializePartialToArray(Current_, size));
+ memset(Current_ + size, 0, AlignUpSpace(size, SerializationAlignment));
+
+ NSan::CheckMemIsInitialized(Current_, AlignUp<size_t>(size, SerializationAlignment));
+ Current_ += AlignUp<size_t>(size, SerializationAlignment);
+ }
+
+ void WriteInt64(i64 value) override
+ {
+ WriteUint64(static_cast<ui64>(value));
+ }
+
+ size_t WriteSchemafulRow(
+ TUnversionedRow row,
+ const TNameTableToSchemaIdMapping* idMapping) override
+ {
+ size_t bytes = EstimateSchemafulRowByteSize(row);
+ EnsureCapacity(bytes);
+
+ if (!row) {
+ UnsafeWriteUint64(MinusOne);
+ return bytes;
+ }
+
+ UnsafeWriteUint64(row.GetCount());
+ UnsafeWriteSchemafulValueRange(row.Elements(), idMapping);
+ return bytes;
+ }
+
+ size_t WriteUnversionedRow(
+ TUnversionedRow row,
+ const TNameTableToSchemaIdMapping* idMapping) override
+ {
+ size_t bytes = EstimateUnversionedRowByteSize(row);
+ EnsureCapacity(bytes);
+
+ if (!row) {
+ UnsafeWriteUint64(MinusOne);
+ return bytes;
+ }
+
+ UnsafeWriteUint64(row.GetCount());
+ UnsafeWriteUnversionedValueRange(row.Elements(), idMapping);
+ return bytes;
+ }
+
+ size_t WriteVersionedRow(TVersionedRow row) override
+ {
+ size_t bytes = EstimateVersionedRowByteSize(row);
+ EnsureCapacity(bytes);
+
+ if (!row) {
+ UnsafeWriteUint64(MinusOne);
+ return bytes;
+ }
+
+ UnsafeWriteRaw(row.GetHeader(), sizeof(TVersionedRowHeader));
+ UnsafeWriteRaw(row.BeginWriteTimestamps(), sizeof(TTimestamp) * row.GetWriteTimestampCount());
+ UnsafeWriteRaw(row.BeginDeleteTimestamps(), sizeof(TTimestamp) * row.GetDeleteTimestampCount());
+
+ UnsafeWriteSchemafulValueRange(row.Keys(), nullptr);
+ UnsafeWriteVersionedValueRange(row.Values());
+ return bytes;
+ }
+
+ void WriteUnversionedValueRange(
+ TUnversionedValueRange valueRange,
+ const TNameTableToSchemaIdMapping* idMapping) override
+ {
+ size_t bytes = AlignUp<size_t>(8, SerializationAlignment); // -1 or value count
+ bytes += EstimateUnversionedValueRangeByteSize(valueRange);
+ EnsureCapacity(bytes);
+
+ UnsafeWriteUint64(valueRange.Size());
+ UnsafeWriteUnversionedValueRange(valueRange, idMapping);
+ }
+
+ void WriteUnversionedRowset(
+ TRange<TUnversionedRow> rowset,
+ const TNameTableToSchemaIdMapping* idMapping) override
+ {
+ WriteRowCount(rowset);
+ for (auto row : rowset) {
+ WriteUnversionedRow(row, idMapping);
+ }
+ }
+
+ void WriteSchemafulRowset(
+ TRange<TUnversionedRow> rowset,
+ const TNameTableToSchemaIdMapping* idMapping) override
+ {
+ WriteRowCount(rowset);
+ for (auto row : rowset) {
+ WriteSchemafulRow(row, idMapping);
+ }
+ }
+
+ void WriteVersionedRowset(TRange<TVersionedRow> rowset) override
+ {
+ WriteRowCount(rowset);
+ for (auto row : rowset) {
+ WriteVersionedRow(row);
+ }
+ }
+
+ std::vector<TSharedRef> Finish() override
+ {
+ FlushPreallocated();
+ return Stream_.Finish();
+ }
+
+private:
+ TChunkedOutputStream Stream_{GetRefCountedTypeCookie<TWireProtocolWriterTag>()};
+
+ char EmptyBuf_[0];
+ char* BeginPreallocated_ = EmptyBuf_;
+ char* EndPreallocated_ = EmptyBuf_;
+ char* Current_ = EmptyBuf_;
+
+ std::vector<TUnversionedValue> PooledValues_;
+
+ void FlushPreallocated()
+ {
+ if (!Current_) {
+ return;
+ }
+
+ YT_VERIFY(Current_ <= EndPreallocated_);
+ Stream_.Advance(Current_ - BeginPreallocated_);
+ BeginPreallocated_ = EndPreallocated_ = Current_ = EmptyBuf_;
+ }
+
+ void EnsureCapacity(size_t more)
+ {
+ if (Y_LIKELY(Current_ + more < EndPreallocated_)) {
+ return;
+ }
+
+ FlushPreallocated();
+
+ size_t size = std::max(PreallocateBlockSize, more);
+ Current_ = BeginPreallocated_ = Stream_.Preallocate(size);
+ EndPreallocated_ = BeginPreallocated_ + size;
+ }
+
+ void EnsureAlignedUpCapacity(size_t more)
+ {
+ EnsureCapacity(AlignUp<size_t>(more, SerializationAlignment));
+ }
+
+ void UnsafeWriteRaw(const void* buffer, size_t size)
+ {
+ NSan::CheckMemIsInitialized(buffer, size);
+
+ memcpy(Current_, buffer, size);
+ memset(Current_ + size, 0, AlignUp<size_t>(size, SerializationAlignment) - size);
+
+ NSan::CheckMemIsInitialized(Current_, AlignUp<size_t>(size, SerializationAlignment));
+ Current_ += AlignUp<size_t>(size, SerializationAlignment);
+ YT_ASSERT(Current_ <= EndPreallocated_);
+ }
+
+ template <class T>
+ void UnsafeWritePod(const T& value)
+ {
+ NSan::CheckMemIsInitialized(&value, sizeof(T));
+
+ static_assert(!std::is_reference<T>::value, "T must not be a reference");
+ static_assert(!std::is_pointer<T>::value, "T must not be a pointer");
+ // Do not use #UnsafeWriteRaw here to allow compiler to optimize memcpy & AlignUp.
+ // Both of them are constexprs.
+ memcpy(Current_, &value, sizeof(T));
+ memset(Current_ + sizeof(T), 0, AlignUpSpace(sizeof(T), SerializationAlignment));
+
+ NSan::CheckMemIsInitialized(Current_, AlignUp<size_t>(sizeof(T), SerializationAlignment));
+ Current_ += AlignUp<size_t>(sizeof(T), SerializationAlignment);
+ YT_ASSERT(Current_ <= EndPreallocated_);
+ }
+
+ void WriteUint16(ui16 value)
+ {
+ EnsureCapacity(AlignUp<size_t>(sizeof(ui16), SerializationAlignment));
+ UnsafeWritePod(value);
+ }
+
+ void WriteUint64(ui64 value)
+ {
+ EnsureCapacity(AlignUp<size_t>(sizeof(ui64), SerializationAlignment));
+ UnsafeWritePod(value);
+ }
+
+ void UnsafeWriteUint64(ui64 value)
+ {
+ UnsafeWritePod(value);
+ }
+
+ template <class TRow>
+ void WriteRowCount(TRange<TRow> rowset)
+ {
+ size_t rowCount = rowset.Size();
+ ValidateRowCount(rowCount);
+ WriteUint64(rowCount);
+ }
+
+ void UnsafeWriteSchemafulValue(const TUnversionedValue& value)
+ {
+ // Write data in-place.
+ if (IsStringLikeType(value.Type)) {
+ UnsafeWritePod<ui64>(value.Length);
+ UnsafeWriteRaw(value.Data.String, value.Length);
+ } else if (IsValueType(value.Type)) {
+ UnsafeWritePod(value.Data);
+ }
+ }
+
+ void UnsafeWriteUnversionedValue(const TUnversionedValue& value)
+ {
+ // Write header (id, type, flags, length).
+ UnsafeWritePod(*reinterpret_cast<const ui64*>(&value));
+ // Write data in-place.
+ if (IsStringLikeType(value.Type)) {
+ UnsafeWriteRaw(value.Data.String, value.Length);
+ } else if (IsValueType(value.Type)) {
+ UnsafeWritePod(value.Data);
+ }
+ }
+
+ void UnsafeWriteVersionedValue(const TVersionedValue& value)
+ {
+ // Write header (id, type, flags, length).
+ const ui64* rawValue = reinterpret_cast<const ui64*>(&value);
+ UnsafeWritePod<ui64>(rawValue[0]);
+ // Write data in-place.
+ if (IsStringLikeType(value.Type)) {
+ UnsafeWriteRaw(value.Data.String, value.Length);
+ } else if (IsValueType(value.Type)) {
+ UnsafeWritePod(value.Data);
+ }
+ // Write timestamp.
+ UnsafeWritePod<ui64>(value.Timestamp);
+ }
+
+ TUnversionedValueRange RemapValues(
+ TUnversionedValueRange values,
+ const TNameTableToSchemaIdMapping* idMapping)
+ {
+ auto valueCount = values.Size();
+ PooledValues_.resize(valueCount);
+ for (size_t index = 0; index < valueCount; ++index){
+ const auto& srcValue = values[index];
+ auto& dstValue = PooledValues_[index];
+ dstValue = srcValue;
+ dstValue.Id = static_cast<ui16>((*idMapping)[srcValue.Id]);
+ }
+
+ std::sort(
+ PooledValues_.begin(),
+ PooledValues_.end(),
+ [] (const TUnversionedValue& lhs, const TUnversionedValue& rhs) -> bool {
+ return lhs.Id < rhs.Id;
+ });
+
+ return MakeRange(PooledValues_);
+ }
+
+ void UnsafeWriteNullBitmap(TUnversionedValueRange values)
+ {
+ // TODO(lukyan): Allocate space and write directly.
+ auto nullBitmap = TBitmapOutput(values.Size());
+ for (int index = 0; index < std::ssize(values); ++index) {
+ nullBitmap.Append(values[index].Type == EValueType::Null);
+ }
+ UnsafeWriteRaw(nullBitmap.GetData(), nullBitmap.GetByteSize());
+ }
+
+ void UnsafeWriteSchemafulValueRange(
+ TUnversionedValueRange values,
+ const TNameTableToSchemaIdMapping* idMapping)
+ {
+ if (idMapping) {
+ values = RemapValues(values, idMapping);
+ }
+ UnsafeWriteNullBitmap(values);
+ for (const auto& value : values) {
+ UnsafeWriteSchemafulValue(value);
+ }
+ }
+
+ void UnsafeWriteUnversionedValueRange(
+ TUnversionedValueRange values,
+ const TNameTableToSchemaIdMapping* idMapping)
+ {
+ if (idMapping) {
+ values = RemapValues(values, idMapping);
+ }
+ for (const auto& value : values) {
+ UnsafeWriteUnversionedValue(value);
+ }
+ }
+
+ void UnsafeWriteVersionedValueRange(
+ TRange<TVersionedValue> values)
+ {
+ for (const auto& value : values) {
+ UnsafeWriteVersionedValue(value);
+ }
+ }
+
+ size_t EstimateSchemafulValueRangeByteSize(TUnversionedValueRange values)
+ {
+ size_t bytes = 0;
+ bytes += AlignUp<size_t>(NBitmapDetail::GetByteSize(values.Size()), SerializationAlignment); // null bitmap
+ for (const auto& value : values) {
+ if (IsStringLikeType(value.Type)) {
+ bytes += AlignUp<size_t>(8 + value.Length, SerializationAlignment);
+ } else if (value.Type != EValueType::Null) {
+ bytes += AlignUp<size_t>(8, SerializationAlignment);
+ }
+ }
+ return bytes;
+ }
+
+ size_t EstimateUnversionedValueRangeByteSize(TUnversionedValueRange values)
+ {
+ size_t bytes = 0;
+ for (const auto& value : values) {
+ bytes += AlignUp<size_t>(8, SerializationAlignment);
+ if (IsStringLikeType(value.Type)) {
+ bytes += AlignUp<size_t>(value.Length, SerializationAlignment);
+ } else if (value.Type != EValueType::Null) {
+ bytes += AlignUp<size_t>(8, SerializationAlignment);
+ }
+ }
+ return bytes;
+ }
+
+ size_t EstimateVersionedValueRangeByteSize(TRange<TVersionedValue> values)
+ {
+ size_t bytes = 0;
+ for (const auto& value : values) {
+ bytes += AlignUp<size_t>(16, SerializationAlignment);
+ if (IsStringLikeType(value.Type)) {
+ bytes += AlignUp<size_t>(value.Length, SerializationAlignment);
+ } else if (value.Type != EValueType::Null) {
+ bytes += AlignUp<size_t>(8, SerializationAlignment);
+ }
+ }
+ return bytes;
+ }
+
+ size_t EstimateSchemafulRowByteSize(TUnversionedRow row)
+ {
+ size_t bytes = AlignUp<size_t>(8, SerializationAlignment); // -1 or value count
+ if (row) {
+ bytes += EstimateSchemafulValueRangeByteSize(row.Elements());
+ }
+ return bytes;
+ }
+
+ size_t EstimateUnversionedRowByteSize(TUnversionedRow row)
+ {
+ size_t bytes = AlignUp<size_t>(8, SerializationAlignment); // -1 or value count
+ if (row) {
+ bytes += EstimateUnversionedValueRangeByteSize(row.Elements());
+ }
+ return bytes;
+ }
+
+ size_t EstimateVersionedRowByteSize(TVersionedRow row)
+ {
+ size_t bytes = AlignUp<size_t>(8, SerializationAlignment); // -1 or value count
+ if (row) {
+ bytes += AlignUp<size_t>(8, SerializationAlignment); // -1 or value count
+ bytes += AlignUp<size_t>(sizeof(TTimestamp) * (
+ row.GetWriteTimestampCount() +
+ row.GetDeleteTimestampCount()), // timestamps
+ SerializationAlignment);
+ bytes += EstimateSchemafulValueRangeByteSize(row.Keys());
+ bytes += EstimateVersionedValueRangeByteSize(row.Values());
+ }
+ return bytes;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IWireProtocolWriter> CreateWireProtocolWriter()
+{
+ return std::make_unique<TWireProtocolWriter>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireProtocolReader
+ : public IWireProtocolReader
+{
+public:
+ explicit TWireProtocolReader(TSharedRef data, TRowBufferPtr rowBuffer)
+ : RowBuffer_(rowBuffer ? std::move(rowBuffer) : New<TRowBuffer>(TWireProtocolReaderTag(), ReaderBufferChunkSize))
+ , Data_(std::move(data))
+ , Current_(Data_.Begin())
+ { }
+
+ const TRowBufferPtr& GetRowBuffer() const override
+ {
+ return RowBuffer_;
+ }
+
+ bool IsFinished() const override
+ {
+ return Current_ == Data_.End();
+ }
+
+ TIterator GetBegin() const override
+ {
+ return Data_.Begin();
+ }
+
+ TIterator GetEnd() const override
+ {
+ return Data_.End();
+ }
+
+ TIterator GetCurrent() const override
+ {
+ return Current_;
+ }
+
+ void SetCurrent(TIterator current) override
+ {
+ Current_ = current;
+ }
+
+ TSharedRef Slice(TIterator begin, TIterator end) override
+ {
+ return Data_.Slice(begin, end);
+ }
+
+ EWireProtocolCommand ReadCommand() override
+ {
+ return EWireProtocolCommand(ReadUint64());
+ }
+
+ TLegacyLockBitmap ReadLegacyLockBitmap() override
+ {
+ return ReadUint64();
+ }
+
+ TLockMask ReadLockMask() override
+ {
+ auto size = ReadUint16();
+ auto wordCount = DivCeil<int>(size, TLockMask::LocksPerWord);
+
+ TLockBitmap bitmap;
+ bitmap.reserve(wordCount);
+ for (int index = 0; index < wordCount; ++index) {
+ bitmap.push_back(ReadUint64());
+ }
+
+ return TLockMask(bitmap, size);
+ }
+
+ TTableSchema ReadTableSchema() override
+ {
+ NTableClient::NProto::TTableSchemaExt protoSchema;
+ ReadMessage(&protoSchema);
+ return FromProto<TTableSchema>(protoSchema);
+ }
+
+ void ReadMessage(::google::protobuf::MessageLite* message) override
+ {
+ size_t size = ReadUint64();
+ ::google::protobuf::io::CodedInputStream chunkStream(
+ reinterpret_cast<const ui8*>(Current_),
+ static_cast<int>(size));
+ Y_PROTOBUF_SUPPRESS_NODISCARD message->ParsePartialFromCodedStream(&chunkStream);
+ Current_ += AlignUp<size_t>(size, SerializationAlignment);
+ }
+
+ i64 ReadInt64() override
+ {
+ return static_cast<i64>(ReadUint64());
+ }
+
+ TUnversionedRow ReadSchemafulRow(const TSchemaData& schemaData, bool captureValues) override
+ {
+ auto valueCount = ReadUint64();
+ if (valueCount == MinusOne) {
+ return TUnversionedRow();
+ }
+ ValidateRowValueCount(valueCount);
+ auto row = RowBuffer_->AllocateUnversioned(valueCount);
+ DoReadSchemafulValueRange(schemaData, captureValues, row.Begin(), valueCount);
+ return row;
+ }
+
+ TUnversionedRow ReadUnversionedRow(bool captureValues, const TIdMapping* idMapping) override
+ {
+ auto valueCount = ReadUint64();
+ if (valueCount == MinusOne) {
+ return TUnversionedRow();
+ }
+ ValidateRowValueCount(valueCount);
+ auto row = RowBuffer_->AllocateUnversioned(valueCount);
+ DoReadUnversionedValueRange(captureValues, row.Begin(), valueCount, idMapping);
+ return row;
+ }
+
+ TVersionedRow ReadVersionedRow(const TSchemaData& schemaData, bool captureValues, const TIdMapping* valueIdMapping) override
+ {
+ union
+ {
+ ui64 Parts[2];
+ TVersionedRowHeader Value;
+ } header;
+
+ header.Parts[0] = ReadUint64();
+ if (header.Parts[0] == MinusOne) {
+ return TVersionedRow();
+ }
+ header.Parts[1] = ReadUint64();
+
+ ValidateKeyColumnCount(header.Value.KeyCount);
+ ValidateVersionedRowTimestampCount(header.Value);
+
+ auto row = TMutableVersionedRow::Allocate(
+ RowBuffer_->GetPool(),
+ header.Value.KeyCount,
+ header.Value.ValueCount,
+ header.Value.WriteTimestampCount,
+ header.Value.DeleteTimestampCount);
+
+ ReadRaw(row.BeginWriteTimestamps(), sizeof(TTimestamp) * row.GetWriteTimestampCount());
+ ReadRaw(row.BeginDeleteTimestamps(), sizeof(TTimestamp) * row.GetDeleteTimestampCount());
+
+ DoReadSchemafulValueRange(schemaData, captureValues, row.BeginKeys(), header.Value.KeyCount);
+ DoReadVersionedValueRange(captureValues, row.BeginValues(), header.Value.ValueCount, valueIdMapping);
+
+ ValidateVersionedRowDataWeight(row);
+
+ return row;
+ }
+
+ TSharedRange<TUnversionedRow> ReadSchemafulRowset(const TSchemaData& schemaData, bool captureValues) override
+ {
+ int rowCount = DoReadRowCount();
+ auto* rows = RowBuffer_->GetPool()->AllocateUninitialized<TUnversionedRow>(rowCount);
+ for (int index = 0; index < rowCount; ++index) {
+ rows[index] = ReadSchemafulRow(schemaData, captureValues);
+ }
+
+ auto range = TRange<TUnversionedRow>(rows, rows + rowCount);
+ return captureValues ? MakeSharedRange(range, RowBuffer_) : MakeSharedRange(range, RowBuffer_, Data_);
+ }
+
+ TSharedRange<TUnversionedRow> ReadUnversionedRowset(bool captureValues, const TIdMapping* idMapping) override
+ {
+ int rowCount = DoReadRowCount();
+ auto* rows = RowBuffer_->GetPool()->AllocateUninitialized<TUnversionedRow>(rowCount);
+ for (int index = 0; index < rowCount; ++index) {
+ rows[index] = ReadUnversionedRow(captureValues, idMapping);
+ }
+ auto range = TRange<TUnversionedRow>(rows, rows + rowCount);
+ return captureValues ? MakeSharedRange(range, RowBuffer_) : MakeSharedRange(range, RowBuffer_, Data_);
+ }
+
+ TSharedRange<TVersionedRow> ReadVersionedRowset(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ const TIdMapping* valueIdMapping) override
+ {
+ int rowCount = DoReadRowCount();
+ auto* rows = RowBuffer_->GetPool()->AllocateUninitialized<TVersionedRow>(rowCount);
+ for (int index = 0; index < rowCount; ++index) {
+ rows[index] = ReadVersionedRow(schemaData, captureValues, valueIdMapping);
+ }
+ auto range = TRange<TVersionedRow>(rows, rows + rowCount);
+ return captureValues ? MakeSharedRange(range, RowBuffer_) : MakeSharedRange(range, RowBuffer_, Data_);
+ }
+
+ TWireProtocolWriteCommand ReadWriteCommand(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ bool versionedWriteIsUnversioned) override
+ {
+ auto command = ReadCommand();
+ switch (command) {
+ case EWireProtocolCommand::WriteRow: {
+ auto row = ReadUnversionedRow(captureValues, /*idMapping*/ nullptr);
+ return TWriteRowCommand{
+ .Row = row
+ };
+ }
+ case EWireProtocolCommand::DeleteRow: {
+ auto row = ReadUnversionedRow(captureValues, /*idMapping*/ nullptr);
+ return TDeleteRowCommand{
+ .Row = row
+ };
+ }
+ case EWireProtocolCommand::VersionedWriteRow: {
+ if (versionedWriteIsUnversioned) {
+ auto unversionedRow = ReadUnversionedRow(captureValues, /*idMapping*/ nullptr);
+ return TVersionedWriteRowCommand{
+ .UnversionedRow = unversionedRow
+ };
+ } else {
+ auto versionedRow = ReadVersionedRow(schemaData, captureValues, /*valueIdMapping*/ nullptr);
+ return TVersionedWriteRowCommand{
+ .VersionedRow = versionedRow
+ };
+ }
+ }
+ // COMPAT(gritukan)
+ case EWireProtocolCommand::ReadLockWriteRow: {
+ TLockMask lockMask;
+ TLegacyLockMask legacyLocks(ReadLegacyLockBitmap());
+ for (int index = 0; index < TLegacyLockMask::MaxCount; ++index) {
+ lockMask.Set(index, legacyLocks.Get(index));
+ }
+
+ auto row = ReadUnversionedRow(captureValues, /*idMapping*/ nullptr);
+ return TWriteAndLockRowCommand{
+ .Row = row,
+ .LockMask = std::move(lockMask)
+ };
+ }
+ case EWireProtocolCommand::WriteAndLockRow: {
+ auto row = ReadUnversionedRow(captureValues, /*idMapping*/ nullptr);
+ auto lockMask = ReadLockMask();
+ return TWriteAndLockRowCommand{
+ .Row = row,
+ .LockMask = std::move(lockMask)
+ };
+ }
+ default:
+ YT_LOG_FATAL("Unknown write command (Command: %v)",
+ command);
+ }
+ }
+
+private:
+ const TRowBufferPtr RowBuffer_;
+
+ TSharedRef Data_;
+ TIterator Current_;
+
+ void ValidateSizeAvailable(size_t size)
+ {
+ if (Current_ + size > Data_.End()) {
+ THROW_ERROR_EXCEPTION("Premature end of stream while reading %v bytes", size);
+ }
+ }
+
+ void ReadRaw(void* buffer, size_t size)
+ {
+ ValidateSizeAvailable(size);
+
+ memcpy(buffer, Current_, size);
+ Current_ += size;
+ Current_ += AlignUpSpace(size, SerializationAlignment);
+ }
+
+ const char* PeekRaw(size_t size)
+ {
+ ValidateSizeAvailable(size);
+
+ auto result = Current_;
+ Current_ += size;
+ Current_ += AlignUpSpace(size, SerializationAlignment);
+ return result;
+ }
+
+ template <class T>
+ void ReadPod(T* value)
+ {
+ ValidateSizeAvailable(sizeof(T));
+
+ memcpy(value, Current_, sizeof(T));
+ Current_ += sizeof(T);
+ Current_ += AlignUpSpace(sizeof(T), SerializationAlignment);
+ }
+
+ ui64 ReadUint64()
+ {
+ ui64 value;
+ ReadPod(&value);
+ return value;
+ }
+
+ ui32 ReadUint32()
+ {
+ ui64 result = ReadUint64();
+ if (result > std::numeric_limits<ui32>::max()) {
+ THROW_ERROR_EXCEPTION("Value is out of range to fit into uint32");
+ }
+ return static_cast<ui32>(result);
+ }
+
+ ui16 ReadUint16()
+ {
+ ui64 result = ReadUint64();
+ if (result > std::numeric_limits<ui16>::max()) {
+ THROW_ERROR_EXCEPTION("Value is out of range to fit into uint16");
+ }
+ return static_cast<ui16>(result);
+ }
+
+ i32 ReadInt32()
+ {
+ i64 result = ReadInt64();
+ if (result < std::numeric_limits<i32>::min() || result > std::numeric_limits<i32>::max()) {
+ THROW_ERROR_EXCEPTION("Value is out of range to fit into int32");
+ }
+ return static_cast<i32>(result);
+ }
+
+ void DoReadStringData(EValueType type, ui32 length, const char** result, bool captureValues)
+ {
+ ui32 limit = 0;
+ if (type == EValueType::String) {
+ limit = MaxStringValueLength;
+ }
+ if (type == EValueType::Any) {
+ limit = MaxAnyValueLength;
+ }
+ if (type == EValueType::Composite) {
+ limit = MaxCompositeValueLength;
+ }
+ if (length > limit) {
+ THROW_ERROR_EXCEPTION("Value of type %Qlv is too long: length %v, limit %v",
+ type,
+ length,
+ limit);
+ }
+ if (captureValues) {
+ char* tmp = RowBuffer_->GetPool()->AllocateUnaligned(length);
+ ReadRaw(tmp, length);
+ *result = tmp;
+ } else {
+ *result = PeekRaw(length);
+ }
+ }
+
+ int DoReadRowCount()
+ {
+ int rowCount = ReadInt32();
+ ValidateRowCount(rowCount);
+ return rowCount;
+ }
+
+ void DoReadSchemafulValue(
+ ui32 schemaData,
+ bool null,
+ bool captureValues,
+ TUnversionedValue* value)
+ {
+ ui64* rawValue = reinterpret_cast<ui64*>(value);
+ rawValue[0] = schemaData;
+ if (null) {
+ value->Type = EValueType::Null;
+ // NB: Don't leave the second half uninitialized.
+ rawValue[1] = 0;
+ } else if (IsStringLikeType(value->Type)) {
+ value->Length = ReadUint32();
+ DoReadStringData(value->Type, value->Length, &value->Data.String, captureValues);
+ } else if (IsValueType(value->Type)) {
+ rawValue[1] = ReadUint64();
+ }
+ }
+
+ void DoReadUnversionedValue(bool captureValues, TUnversionedValue* value)
+ {
+ ui64* rawValue = reinterpret_cast<ui64*>(value);
+ rawValue[0] = ReadUint64();
+ if (IsStringLikeType(value->Type)) {
+ DoReadStringData(value->Type, value->Length, &value->Data.String, captureValues);
+ } else if (IsValueType(value->Type)) {
+ rawValue[1] = ReadUint64();
+ }
+ }
+
+ void DoReadVersionedValue(bool captureValues, TVersionedValue* value)
+ {
+ ui64* rawValue = reinterpret_cast<ui64*>(value);
+ rawValue[0] = ReadUint64();
+ if (IsStringLikeType(value->Type)) {
+ DoReadStringData(value->Type, value->Length, &value->Data.String, captureValues);
+ } else if (IsValueType(value->Type)) {
+ rawValue[1] = ReadUint64();
+ }
+ value->Timestamp = ReadUint64();
+ }
+
+ void DoReadSchemafulValueRange(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ TUnversionedValue* values,
+ ui32 valueCount)
+ {
+ auto bitmapPtr = PeekRaw(NBitmapDetail::GetByteSize(valueCount));
+ TBitmap nullBitmap(bitmapPtr);
+ for (size_t index = 0; index < valueCount; ++index) {
+ DoReadSchemafulValue(schemaData[index], nullBitmap[index], captureValues, &values[index]);
+ }
+ }
+
+ void DoApplyIdMapping(ui16* id, int index, const TIdMapping* idMapping)
+ {
+ if (*id >= idMapping->size()) {
+ THROW_ERROR_EXCEPTION("Value with index %v has id %v which is out of range [0, %v)",
+ index,
+ *id,
+ idMapping->size());
+ }
+ int mappedId = (*idMapping)[*id];
+ if (mappedId == -1) {
+ THROW_ERROR_EXCEPTION("Id mapping for value with index %v contains unexpected value %Qv",
+ index,
+ -1);
+ }
+ *id = mappedId;
+ }
+
+ void DoReadUnversionedValueRange(
+ bool captureValues,
+ TUnversionedValue* values,
+ ui32 valueCount,
+ const TIdMapping* idMapping)
+ {
+ for (size_t index = 0; index < valueCount; ++index) {
+ DoReadUnversionedValue(captureValues, &values[index]);
+ if (idMapping) {
+ DoApplyIdMapping(&values[index].Id, index, idMapping);
+ }
+ }
+ }
+
+ void DoReadVersionedValueRange(
+ bool captureValues,
+ TVersionedValue* values,
+ ui32 valueCount,
+ const TIdMapping* valueIdMapping)
+ {
+ for (size_t index = 0; index < valueCount; ++index) {
+ DoReadVersionedValue(captureValues, &values[index]);
+ if (valueIdMapping) {
+ DoApplyIdMapping(&values[index].Id, index, valueIdMapping);
+ }
+ }
+ }
+
+ void ValidateVersionedRowTimestampCount(const TVersionedRowHeader& rowHeader)
+ {
+ if (rowHeader.WriteTimestampCount > MaxTimestampCountPerRow) {
+ THROW_ERROR_EXCEPTION("Too many write timestamps in a versioned row");
+ }
+ if (rowHeader.DeleteTimestampCount > MaxTimestampCountPerRow) {
+ THROW_ERROR_EXCEPTION("Too many delete timestamps in a versioned row");
+ }
+ }
+
+ void ValidateVersionedRowDataWeight(TVersionedRow row)
+ {
+ auto dataWeight = GetDataWeight(row);
+ if (dataWeight > MaxServerVersionedRowDataWeight) {
+ THROW_ERROR_EXCEPTION("Versioned row data weight is too large: %v > %v",
+ dataWeight,
+ MaxServerVersionedRowDataWeight)
+ << TErrorAttribute("key", ToOwningKey(row));
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+auto IWireProtocolReader::GetSchemaData(
+ const TTableSchema& schema,
+ const TColumnFilter& filter) -> TSchemaData
+{
+ TSchemaData schemaData;
+ auto addColumn = [&] (int id) {
+ auto value = MakeUnversionedValueHeader(schema.Columns()[id].GetWireType(), id);
+ schemaData.push_back(*reinterpret_cast<ui32*>(&value));
+ };
+ if (!filter.IsUniversal()) {
+ schemaData.reserve(std::ssize(filter.GetIndexes()));
+ for (int id : filter.GetIndexes()) {
+ addColumn(id);
+ }
+ } else {
+ schemaData.reserve(schema.GetColumnCount());
+ for (int id = 0; id < schema.GetColumnCount(); ++id) {
+ addColumn(id);
+ }
+ }
+ return schemaData;
+}
+
+auto IWireProtocolReader::GetSchemaData(const TTableSchema& schema) -> TSchemaData
+{
+ TSchemaData schemaData;
+ schemaData.reserve(schema.GetColumnCount());
+ for (int id = 0; id < schema.GetColumnCount(); ++id) {
+ auto value = MakeUnversionedValueHeader(schema.Columns()[id].GetWireType(), id);
+ schemaData.push_back(*reinterpret_cast<ui32*>(&value));
+ }
+ return schemaData;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IWireProtocolReader> CreateWireProtocolReader(TSharedRef data, TRowBufferPtr rowBuffer)
+{
+ return std::make_unique<TWireProtocolReader>(std::move(data), std::move(rowBuffer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireProtocolRowsetReader
+ : public IWireProtocolRowsetReader
+{
+public:
+ TWireProtocolRowsetReader(
+ const std::vector<TSharedRef>& compressedBlocks,
+ NCompression::ECodec codecId,
+ TTableSchemaPtr schema,
+ bool schemaful,
+ const NLogging::TLogger& logger)
+ : CompressedBlocks_(compressedBlocks)
+ , Codec_(NCompression::GetCodec(codecId))
+ , Schema_(std::move(schema))
+ , Schemaful_(schemaful)
+ , Logger(logger.WithTag("ReaderId: %v", TGuid::Create()))
+ {
+ YT_LOG_DEBUG("Wire protocol rowset reader created (BlockCount: %v, TotalCompressedSize: %v)",
+ CompressedBlocks_.size(),
+ GetByteSize(CompressedBlocks_));
+ }
+
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& /*options*/) override
+ {
+ if (Finished_) {
+ return nullptr;
+ }
+
+ if (BlockIndex_ >= std::ssize(CompressedBlocks_)) {
+ Finished_ = true;
+ YT_LOG_DEBUG("Wire protocol rowset reader finished");
+ return nullptr;
+ }
+
+ const auto& compressedBlock = CompressedBlocks_[BlockIndex_];
+ YT_LOG_DEBUG("Started decompressing rowset reader block (BlockIndex: %v, CompressedSize: %v)",
+ BlockIndex_,
+ compressedBlock.Size());
+ auto uncompressedBlock = Codec_->Decompress(compressedBlock);
+ YT_LOG_DEBUG("Finished decompressing rowset reader block (BlockIndex: %v, UncompressedSize: %v)",
+ BlockIndex_,
+ uncompressedBlock.Size());
+
+ auto rowBuffer = New<TRowBuffer>(TWireProtocolReaderTag(), ReaderBufferChunkSize);
+ WireReader_ = CreateWireProtocolReader(uncompressedBlock, std::move(rowBuffer));
+
+ if (!SchemaChecked_) {
+ auto actualSchema = WireReader_->ReadTableSchema();
+
+ //
+ // NB this comparison is compat for YT-10668
+ // This could be replaced with simple `operator==', once all nodes and proxies are updated to have new schema
+ // representation introduced in cec93e9435fc3bbecc02ee5b8fd9ffa0eafc1672
+ //
+ // Guess it will be surely the case after after 01.11.2019
+ if (!IsEqualIgnoringRequiredness(*Schema_, actualSchema)) {
+ THROW_ERROR_EXCEPTION("Schema mismatch while parsing wire protocol");
+ }
+ SchemaChecked_ = true;
+ }
+
+ auto schemaData = WireReader_->GetSchemaData(*Schema_, TColumnFilter());
+
+ std::vector<TUnversionedRow> rows;
+ while (!WireReader_->IsFinished()) {
+ auto row = Schemaful_
+ ? WireReader_->ReadSchemafulRow(schemaData, false)
+ : WireReader_->ReadUnversionedRow(false);
+ rows.push_back(row);
+ }
+ ++BlockIndex_;
+
+ return CreateBatchFromUnversionedRows(MakeSharedRange(std::move(rows), MakeStrong(this)));
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ return VoidFuture;
+ }
+
+ TDataStatistics GetDataStatistics() const override
+ {
+ YT_ABORT();
+ }
+
+ NChunkClient::TCodecStatistics GetDecompressionStatistics() const override
+ {
+ YT_ABORT();
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ return false;
+ }
+
+ std::vector<NChunkClient::TChunkId> GetFailedChunkIds() const override
+ {
+ return {};
+ }
+
+private:
+ const std::vector<TSharedRef> CompressedBlocks_;
+ NCompression::ICodec* const Codec_;
+ const TTableSchemaPtr Schema_;
+ bool Schemaful_;
+ const NLogging::TLogger Logger;
+
+ int BlockIndex_ = 0;
+ std::unique_ptr<IWireProtocolReader> WireReader_;
+ bool Finished_ = false;
+ bool SchemaChecked_ = false;
+
+};
+
+IWireProtocolRowsetReaderPtr CreateWireProtocolRowsetReader(
+ const std::vector<TSharedRef>& compressedBlocks,
+ NCompression::ECodec codecId,
+ TTableSchemaPtr schema,
+ bool schemaful,
+ const NLogging::TLogger& logger)
+{
+ return New<TWireProtocolRowsetReader>(
+ compressedBlocks,
+ codecId,
+ std::move(schema),
+ schemaful,
+ logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWireProtocolRowsetWriter
+ : public IWireProtocolRowsetWriter
+{
+public:
+ TWireProtocolRowsetWriter(
+ NCompression::ECodec codecId,
+ size_t desiredUncompressedBlockSize,
+ TTableSchemaPtr schema,
+ bool schemaful,
+ const NLogging::TLogger& logger)
+ : Codec_(NCompression::GetCodec(codecId))
+ , DesiredUncompressedBlockSize_(desiredUncompressedBlockSize)
+ , Schema_(std::move(schema))
+ , Schemaful_(schemaful)
+ , Logger(logger.WithTag("WriterId: %v", TGuid::Create()))
+ {
+ YT_LOG_DEBUG("Wire protocol rowset writer created (Codec: %v, DesiredUncompressedBlockSize: %v)",
+ codecId,
+ DesiredUncompressedBlockSize_);
+ }
+
+ TFuture<void> Close() override
+ {
+ if (!Closed_) {
+ YT_LOG_DEBUG("Wire protocol rowset writer closed");
+ FlushBlock();
+ Closed_ = true;
+ }
+ return VoidFuture;
+ }
+
+ bool Write(TRange<TUnversionedRow> rows) override
+ {
+ YT_VERIFY(!Closed_);
+ for (auto row : rows) {
+ if (!WireWriter_) {
+ WireWriter_ = CreateWireProtocolWriter();
+ if (!SchemaWritten_) {
+ WireWriter_->WriteTableSchema(*Schema_);
+ SchemaWritten_ = true;
+ }
+ }
+ if (Schemaful_) {
+ WireWriter_->WriteSchemafulRow(row);
+ } else {
+ WireWriter_->WriteUnversionedRow(row);
+ }
+ if (WireWriter_->GetByteSize() >= DesiredUncompressedBlockSize_) {
+ FlushBlock();
+ }
+ }
+ return true;
+ }
+
+ TFuture<void> GetReadyEvent() override
+ {
+ return VoidFuture;
+ }
+
+ std::vector<TSharedRef> GetCompressedBlocks() override
+ {
+ YT_VERIFY(Closed_);
+ return CompressedBlocks_;
+ }
+
+private:
+ NCompression::ICodec* const Codec_;
+ const size_t DesiredUncompressedBlockSize_;
+ const TTableSchemaPtr Schema_;
+ const bool Schemaful_;
+ const NLogging::TLogger Logger;
+
+ std::vector<TSharedRef> CompressedBlocks_;
+ std::unique_ptr<IWireProtocolWriter> WireWriter_;
+ bool Closed_ = false;
+ bool SchemaWritten_ = false;
+
+
+ void FlushBlock()
+ {
+ if (!WireWriter_) {
+ return;
+ }
+
+ auto uncompressedBlocks = WireWriter_->Finish();
+
+ YT_LOG_DEBUG("Started compressing rowset writer block (BlockIndex: %v, UncompressedSize: %v)",
+ CompressedBlocks_.size(),
+ GetByteSize(uncompressedBlocks));
+ auto compressedBlock = Codec_->Compress(uncompressedBlocks);
+ YT_LOG_DEBUG("Finished compressing rowset writer block (BlockIndex: %v, CompressedSize: %v)",
+ CompressedBlocks_.size(),
+ compressedBlock.Size());
+
+ CompressedBlocks_.push_back(compressedBlock);
+ WireWriter_.reset();
+ }
+};
+
+IWireProtocolRowsetWriterPtr CreateWireProtocolRowsetWriter(
+ NCompression::ECodec codecId,
+ size_t desiredUncompressedBlockSize,
+ TTableSchemaPtr schema,
+ bool schemaful,
+ const NLogging::TLogger& logger)
+{
+ return New<TWireProtocolRowsetWriter>(
+ codecId,
+ desiredUncompressedBlockSize,
+ std::move(schema),
+ schemaful,
+ logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/table_client/wire_protocol.h b/yt/yt/client/table_client/wire_protocol.h
new file mode 100644
index 0000000000..a38259b986
--- /dev/null
+++ b/yt/yt/client/table_client/wire_protocol.h
@@ -0,0 +1,323 @@
+#pragma once
+
+#include "public.h"
+#include "schema.h"
+
+#include <yt/yt/client/api/public.h>
+
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_writer.h>
+#include <yt/yt/client/table_client/versioned_row.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/wire_protocol.pb.h>
+
+#include <yt/yt/core/misc/range.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EWireProtocolCommand,
+ // Read commands:
+
+ ((LookupRows)(1))
+ // Finds rows with given keys and fetches their components.
+ //
+ // Input:
+ // * TReqLookupRows
+ // * Unversioned rowset containing N keys
+ //
+ // Output:
+ // * N unversioned rows
+
+ ((VersionedLookupRows)(2))
+ // Finds rows with given keys and fetches their components.
+ //
+ // Input:
+ // * TReqLookupRows
+ // * Unversioned rowset containing N keys
+ //
+ // Output:
+ // * N versioned rows
+
+ // Write commands:
+
+ ((WriteRow)(100))
+ // Inserts a new row or completely replaces an existing one with matching key.
+ //
+ // Input:
+ // * Unversioned row
+ // Output:
+ // None
+
+ ((DeleteRow)(101))
+ // Deletes a row with a given key, if it exists.
+ //
+ // Input:
+ // * Key
+ // Output:
+ // None
+
+ ((VersionedWriteRow)(102))
+ // Writes a versioned row (possibly inserting new values and/or delete timestamps).
+ // Currently only used by replicator.
+ //
+ // Input:
+ // * Versioned row
+ // Output:
+ // None
+
+ // Other commands:
+
+ ((ReadLockWriteRow)(103))
+ // Take primary read lock and optionally modify row
+ // Deprecated.
+ //
+ // Input:
+ // * Key
+ // Output:
+ // None
+
+ ((WriteAndLockRow)(104))
+ // Take locks on row and optionally modify row
+ //
+ // Input:
+ // * Unversioned row
+ // * Lock mask
+ // Output:
+ // None
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWriteRowCommand
+{
+ TUnversionedRow Row;
+};
+
+struct TDeleteRowCommand
+{
+ TUnversionedRow Row;
+};
+
+struct TVersionedWriteRowCommand
+{
+ // Versioned write uses versioned rows for sorted tables
+ // and unversioned rows for ordered tables.
+ union
+ {
+ TUnversionedRow UnversionedRow;
+ TVersionedRow VersionedRow;
+ };
+};
+
+struct TWriteAndLockRowCommand
+{
+ TUnversionedRow Row;
+ TLockMask LockMask;
+};
+
+using TWireProtocolWriteCommand = std::variant<
+ TWriteRowCommand,
+ TDeleteRowCommand,
+ TVersionedWriteRowCommand,
+ TWriteAndLockRowCommand
+>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+EWireProtocolCommand GetWireProtocolCommand(const TWireProtocolWriteCommand& command);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Builds wire-encoded stream.
+struct IWireProtocolWriter
+{
+public:
+ virtual ~IWireProtocolWriter() = default;
+
+ virtual size_t GetByteSize() const = 0;
+
+ virtual void WriteCommand(EWireProtocolCommand command) = 0;
+
+ virtual void WriteLegacyLockBitmap(TLegacyLockBitmap lockBitmap) = 0;
+
+ virtual void WriteLockMask(TLockMask lockMask) = 0;
+
+ virtual void WriteTableSchema(const NTableClient::TTableSchema& schema) = 0;
+
+ virtual void WriteMessage(const ::google::protobuf::MessageLite& message) = 0;
+
+ virtual void WriteInt64(i64 value) = 0;
+
+ virtual size_t WriteUnversionedRow(
+ NTableClient::TUnversionedRow row,
+ const NTableClient::TNameTableToSchemaIdMapping* idMapping = nullptr) = 0;
+ virtual size_t WriteSchemafulRow(
+ NTableClient::TUnversionedRow row,
+ const NTableClient::TNameTableToSchemaIdMapping* idMapping = nullptr) = 0;
+ virtual size_t WriteVersionedRow(NTableClient::TVersionedRow row) = 0;
+
+ virtual void WriteUnversionedValueRange(
+ NTableClient::TUnversionedValueRange valueRange,
+ const NTableClient::TNameTableToSchemaIdMapping* idMapping = nullptr) = 0;
+
+ virtual void WriteUnversionedRowset(
+ TRange<NTableClient::TUnversionedRow> rowset,
+ const NTableClient::TNameTableToSchemaIdMapping* idMapping = nullptr) = 0;
+ virtual void WriteSchemafulRowset(
+ TRange<NTableClient::TUnversionedRow> rowset,
+ const NTableClient::TNameTableToSchemaIdMapping* idMapping = nullptr) = 0;
+ virtual void WriteVersionedRowset(TRange<NTableClient::TVersionedRow> rowset) = 0;
+
+ virtual std::vector<TSharedRef> Finish() = 0;
+
+ template <class TRow>
+ inline void WriteRowset(TRange<TRow> rowset);
+};
+
+template <>
+inline void IWireProtocolWriter::WriteRowset<NTableClient::TUnversionedRow>(
+ TRange<NTableClient::TUnversionedRow> rowset)
+{
+ return WriteUnversionedRowset(rowset);
+}
+
+template <>
+inline void IWireProtocolWriter::WriteRowset<NTableClient::TVersionedRow>(
+ TRange<NTableClient::TVersionedRow> rowset)
+{
+ return WriteVersionedRowset(rowset);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IWireProtocolWriter> CreateWireProtocolWriter();
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Reads wire-encoded stream.
+/*!
+ * All |ReadXXX| methods obey the following convention.
+ * Rows are captured by the row buffer passed in ctor.
+ * Values are either captured or not depending on |captureValues| argument.
+ */
+struct IWireProtocolReader
+{
+public:
+ using TIterator = const char*;
+
+ virtual ~IWireProtocolReader() = default;
+
+ virtual const NTableClient::TRowBufferPtr& GetRowBuffer() const = 0;
+
+ virtual bool IsFinished() const = 0;
+ virtual TIterator GetBegin() const = 0;
+ virtual TIterator GetEnd() const = 0;
+
+ virtual TIterator GetCurrent() const = 0;
+ virtual void SetCurrent(TIterator) = 0;
+
+ virtual TSharedRef Slice(TIterator begin, TIterator end) = 0;
+
+ virtual EWireProtocolCommand ReadCommand() = 0;
+
+ virtual TLegacyLockBitmap ReadLegacyLockBitmap() = 0;
+
+ virtual TLockMask ReadLockMask() = 0;
+
+ virtual NTableClient::TTableSchema ReadTableSchema() = 0;
+
+ virtual void ReadMessage(::google::protobuf::MessageLite* message) = 0;
+
+ virtual i64 ReadInt64() = 0;
+
+ virtual NTableClient::TUnversionedRow ReadUnversionedRow(
+ bool captureValues,
+ const TIdMapping* idMapping = nullptr) = 0;
+ virtual NTableClient::TUnversionedRow ReadSchemafulRow(
+ const TSchemaData& schemaData,
+ bool captureValues) = 0;
+ virtual NTableClient::TVersionedRow ReadVersionedRow(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ const TIdMapping* valueIdMapping = nullptr) = 0;
+
+ virtual TSharedRange<NTableClient::TUnversionedRow> ReadUnversionedRowset(
+ bool captureValues,
+ const TIdMapping* idMapping = nullptr) = 0;
+ virtual TSharedRange<NTableClient::TUnversionedRow> ReadSchemafulRowset(
+ const TSchemaData& schemaData,
+ bool captureValues) = 0;
+ virtual TSharedRange<NTableClient::TVersionedRow> ReadVersionedRowset(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ const TIdMapping* valueIdMapping = nullptr) = 0;
+
+ virtual TWireProtocolWriteCommand ReadWriteCommand(
+ const TSchemaData& schemaData,
+ bool captureValues,
+ bool versionedWriteIsUnversioned = false) = 0;
+
+ static TSchemaData GetSchemaData(
+ const NTableClient::TTableSchema& schema,
+ const NTableClient::TColumnFilter& filter);
+ static TSchemaData GetSchemaData(
+ const NTableClient::TTableSchema& schema);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates wire protocol reader.
+/*!
+ * If #rowBuffer is null, a default one is created.
+ */
+std::unique_ptr<IWireProtocolReader> CreateWireProtocolReader(
+ TSharedRef data,
+ TRowBufferPtr rowBuffer = TRowBufferPtr());
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IWireProtocolRowsetReader
+ : public NTableClient::ISchemafulUnversionedReader
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IWireProtocolRowsetReader)
+
+IWireProtocolRowsetReaderPtr CreateWireProtocolRowsetReader(
+ const std::vector<TSharedRef>& compressedBlocks,
+ NCompression::ECodec codecId,
+ NTableClient::TTableSchemaPtr schema,
+ bool schemaful,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IWireProtocolRowsetWriter
+ : public NTableClient::IUnversionedRowsetWriter
+{
+ virtual std::vector<TSharedRef> GetCompressedBlocks() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IWireProtocolRowsetWriter)
+
+IWireProtocolRowsetWriterPtr CreateWireProtocolRowsetWriter(
+ NCompression::ECodec codecId,
+ size_t desiredUncompressedBlockSize,
+ NTableClient::TTableSchemaPtr schema,
+ bool isSchemaful,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
+
diff --git a/yt/yt/client/tablet_client/config.cpp b/yt/yt/client/tablet_client/config.cpp
new file mode 100644
index 0000000000..193a6f0417
--- /dev/null
+++ b/yt/yt/client/tablet_client/config.cpp
@@ -0,0 +1,122 @@
+#include "config.h"
+
+namespace NYT::NTabletClient {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTableMountCacheConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("reject_if_entry_is_requested_but_not_ready", &TThis::RejectIfEntryIsRequestedButNotReady)
+ .Default(false);
+}
+
+TTableMountCacheConfigPtr TTableMountCacheConfig::ApplyDynamic(
+ const TTableMountCacheDynamicConfigPtr& dynamicConfig) const
+{
+ auto mergedConfig = CloneYsonStruct(MakeStrong(this));
+ mergedConfig->ApplyDynamicInplace(dynamicConfig);
+ UpdateYsonStructField(mergedConfig->RejectIfEntryIsRequestedButNotReady, dynamicConfig->RejectIfEntryIsRequestedButNotReady);
+ mergedConfig->Postprocess();
+ return mergedConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTableMountCacheDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("reject_if_entry_is_requested_but_not_ready", &TThis::RejectIfEntryIsRequestedButNotReady)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRemoteDynamicStoreReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("client_read_timeout", &TThis::ClientReadTimeout)
+ .Default(TDuration::Seconds(20));
+ registrar.Parameter("server_read_timeout", &TThis::ServerReadTimeout)
+ .Default(TDuration::Seconds(20));
+ registrar.Parameter("client_write_timeout", &TThis::ClientWriteTimeout)
+ .Default(TDuration::Seconds(20));
+ registrar.Parameter("server_write_timeout", &TThis::ServerWriteTimeout)
+ .Default(TDuration::Seconds(20));
+ registrar.Parameter("max_rows_per_server_read", &TThis::MaxRowsPerServerRead)
+ .GreaterThan(0)
+ .Default(1024);
+
+ registrar.Parameter("window_size", &TThis::WindowSize)
+ .Default(16_MB)
+ .GreaterThan(0);
+
+ registrar.Parameter("streaming_subrequest_failure_probability", &TThis::StreamingSubrequestFailureProbability)
+ .Default(0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRetryingRemoteDynamicStoreReaderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("retry_count", &TThis::RetryCount)
+ .Default(10)
+ .GreaterThan(0);
+ registrar.Parameter("locate_request_backoff_time", &TThis::LocateRequestBackoffTime)
+ .Default(TDuration::Seconds(10));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TReplicatedTableOptions::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_replicated_table_tracker", &TThis::EnableReplicatedTableTracker)
+ .Default(false);
+
+ registrar.Parameter("max_sync_replica_count", &TThis::MaxSyncReplicaCount)
+ .Alias("sync_replica_count")
+ .Optional();
+ registrar.Parameter("min_sync_replica_count", &TThis::MinSyncReplicaCount)
+ .Optional();
+
+ registrar.Parameter("sync_replica_lag_threshold", &TThis::SyncReplicaLagThreshold)
+ .Default(TDuration::Minutes(10));
+
+ registrar.Parameter("tablet_cell_bundle_name_ttl", &TThis::TabletCellBundleNameTtl)
+ .Default(TDuration::Seconds(300));
+ registrar.Parameter("tablet_cell_bundle_name_failure_interval", &TThis::RetryOnFailureInterval)
+ .Default(TDuration::Seconds(60));
+
+ registrar.Parameter("enable_preload_state_check", &TThis::EnablePreloadStateCheck)
+ .Default(false);
+ registrar.Parameter("incomplete_preload_grace_period", &TThis::IncompletePreloadGracePeriod)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("preferred_sync_replica_clusters", &TThis::PreferredSyncReplicaClusters)
+ .Default(std::nullopt);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MaxSyncReplicaCount && config->MinSyncReplicaCount && *config->MinSyncReplicaCount > *config->MaxSyncReplicaCount) {
+ THROW_ERROR_EXCEPTION("\"min_sync_replica_count\" must be less or equal to \"max_sync_replica_count\"");
+ }
+ });
+}
+
+std::tuple<int, int> TReplicatedTableOptions::GetEffectiveMinMaxReplicaCount(int replicaCount) const
+{
+ int maxSyncReplicas = 0;
+ int minSyncReplicas = 0;
+
+ if (!MaxSyncReplicaCount && !MinSyncReplicaCount) {
+ maxSyncReplicas = 1;
+ } else {
+ maxSyncReplicas = MaxSyncReplicaCount.value_or(replicaCount);
+ }
+
+ minSyncReplicas = MinSyncReplicaCount.value_or(maxSyncReplicas);
+
+ return std::make_tuple(minSyncReplicas, maxSyncReplicas);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/config.h b/yt/yt/client/tablet_client/config.h
new file mode 100644
index 0000000000..501a80dce6
--- /dev/null
+++ b/yt/yt/client/tablet_client/config.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/cache_config.h>
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableMountCacheConfig
+ : public TAsyncExpiringCacheConfig
+{
+public:
+ //! If entry is requested for the first time then allow only client who requested the entry to wait for it.
+ bool RejectIfEntryIsRequestedButNotReady;
+
+ TTableMountCacheConfigPtr ApplyDynamic(const TTableMountCacheDynamicConfigPtr& dynamicConfig) const;
+
+ REGISTER_YSON_STRUCT(TTableMountCacheConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableMountCacheConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableMountCacheDynamicConfig
+ : public TAsyncExpiringCacheDynamicConfig
+{
+public:
+ std::optional<bool> RejectIfEntryIsRequestedButNotReady;
+
+ REGISTER_YSON_STRUCT(TTableMountCacheDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableMountCacheDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteDynamicStoreReaderConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ TDuration ClientReadTimeout;
+ TDuration ServerReadTimeout;
+ TDuration ClientWriteTimeout;
+ TDuration ServerWriteTimeout;
+ i64 MaxRowsPerServerRead;
+
+ ssize_t WindowSize;
+
+ // Testing option.
+ double StreamingSubrequestFailureProbability;
+
+ REGISTER_YSON_STRUCT(TRemoteDynamicStoreReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRemoteDynamicStoreReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetryingRemoteDynamicStoreReaderConfig
+ : public TRemoteDynamicStoreReaderConfig
+{
+public:
+ //! Maximum number of locate requests.
+ int RetryCount;
+
+ //! Time to wait between making another locate request.
+ TDuration LocateRequestBackoffTime;
+
+ REGISTER_YSON_STRUCT(TRetryingRemoteDynamicStoreReaderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRetryingRemoteDynamicStoreReaderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplicatedTableOptions
+ : public NYTree::TYsonStruct
+{
+public:
+ bool EnableReplicatedTableTracker;
+
+ std::optional<int> MaxSyncReplicaCount;
+ std::optional<int> MinSyncReplicaCount;
+
+ TDuration SyncReplicaLagThreshold;
+
+ // TODO(akozhikhov): We probably do not need these in this per-table config.
+ TDuration TabletCellBundleNameTtl;
+ TDuration RetryOnFailureInterval;
+
+ bool EnablePreloadStateCheck;
+ TDuration IncompletePreloadGracePeriod;
+
+ std::optional<std::vector<TString>> PreferredSyncReplicaClusters;
+
+ std::tuple<int, int> GetEffectiveMinMaxReplicaCount(int replicaCount) const;
+
+ REGISTER_YSON_STRUCT(TReplicatedTableOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TReplicatedTableOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/helpers.cpp b/yt/yt/client/tablet_client/helpers.cpp
new file mode 100644
index 0000000000..fcc29c1a5e
--- /dev/null
+++ b/yt/yt/client/tablet_client/helpers.cpp
@@ -0,0 +1,24 @@
+#include "public.h"
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsStableReplicaMode(ETableReplicaMode mode)
+{
+ return
+ mode == ETableReplicaMode::Sync ||
+ mode == ETableReplicaMode::Async;
+}
+
+bool IsStableReplicaState(ETableReplicaState state)
+{
+ return
+ state == ETableReplicaState::Enabled ||
+ state == ETableReplicaState::Disabled;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
+
diff --git a/yt/yt/client/tablet_client/helpers.h b/yt/yt/client/tablet_client/helpers.h
new file mode 100644
index 0000000000..ef840061e1
--- /dev/null
+++ b/yt/yt/client/tablet_client/helpers.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsStableReplicaMode(ETableReplicaMode mode);
+bool IsStableReplicaState(ETableReplicaState state);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/public.cpp b/yt/yt/client/tablet_client/public.cpp
new file mode 100644
index 0000000000..b1bcfc8b8b
--- /dev/null
+++ b/yt/yt/client/tablet_client/public.cpp
@@ -0,0 +1,25 @@
+#include "public.h"
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TTabletCellId NullTabletCellId;
+const TTabletId NullTabletId;
+const TStoreId NullStoreId;
+const TPartitionId NullPartitionId;
+const THunkStorageId NullHunkStorageId;
+
+const TString TReplicationLogTable::ChangeTypeColumnName("change_type");
+const TString TReplicationLogTable::KeyColumnNamePrefix("key:");
+const TString TReplicationLogTable::ValueColumnNamePrefix("value:");
+const TString TReplicationLogTable::FlagsColumnNamePrefix("flags:");
+
+const TString TUnversionedUpdateSchema::ChangeTypeColumnName("$change_type");
+const TString TUnversionedUpdateSchema::ValueColumnNamePrefix("$value:");
+const TString TUnversionedUpdateSchema::FlagsColumnNamePrefix("$flags:");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
+
diff --git a/yt/yt/client/tablet_client/public.h b/yt/yt/client/tablet_client/public.h
new file mode 100644
index 0000000000..782a8d19ec
--- /dev/null
+++ b/yt/yt/client/tablet_client/public.h
@@ -0,0 +1,224 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/client/hydra/public.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETabletState,
+ // Individual states
+ ((Mounting) (0))
+ ((Mounted) (1))
+ ((Unmounting) (2))
+ ((Unmounted) (3))
+ ((Freezing) (4))
+ ((Frozen) (5))
+ ((Unfreezing) (6))
+ ((FrozenMounting) (7))
+
+ // Special states
+ ((None) (100))
+ ((Mixed) (101))
+ ((Transient) (102))
+);
+
+constexpr ETabletState MinValidTabletState = ETabletState::Mounting;
+constexpr ETabletState MaxValidTabletState = ETabletState::FrozenMounting;
+
+// Keep in sync with NRpcProxy::NProto::ETableReplicaMode.
+DEFINE_ENUM(ETableReplicaMode,
+ ((Sync) (0))
+ ((Async) (1))
+ ((AsyncToSync) (2))
+ ((SyncToAsync) (3))
+);
+
+DEFINE_ENUM(ETableReplicaContentType,
+ ((Data) (0))
+ ((Queue) (1))
+ ((External) (2))
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((TransactionLockConflict) (1700))
+ ((NoSuchTablet) (1701))
+ ((NoSuchCell) (1721))
+ ((TabletNotMounted) (1702))
+ ((AllWritesDisabled) (1703))
+ ((InvalidMountRevision) (1704))
+ ((TableReplicaAlreadyExists) (1705))
+ ((InvalidTabletState) (1706))
+ ((TableMountInfoNotReady) (1707))
+ ((TabletSnapshotExpired) (1708))
+ ((QueryInputRowCountLimitExceeded) (1709))
+ ((QueryOutputRowCountLimitExceeded) (1710))
+ ((QueryExpressionDepthLimitExceeded) (1711))
+ ((RowIsBlocked) (1712))
+ ((BlockedRowWaitTimeout) (1713))
+ ((NoSyncReplicas) (1714))
+ ((TableMustNotBeReplicated) (1715))
+ ((TableMustBeSorted) (1716))
+ ((TooManyRowsInTransaction) (1717))
+ ((UpstreamReplicaMismatch) (1718))
+ ((NoSuchDynamicStore) (1719))
+ ((BundleResourceLimitExceeded) (1720))
+ ((SyncReplicaIsNotKnown) (1722))
+ ((SyncReplicaIsNotInSyncMode) (1723))
+ ((SyncReplicaIsNotWritten) (1724))
+ ((RequestThrottled) (1725))
+ ((ColumnNotFound) (1726))
+ ((ReplicatorWriteBlockedByUser) (1727))
+ ((UserWriteBlockedByReplicator) (1728))
+ ((CannotCheckConflictsAgainstChunkStore) (1729))
+ ((InvalidBackupState) (1730))
+ ((WriteRetryIsImpossible) (1731))
+ ((SyncReplicaNotInSync) (1732))
+ ((BackupCheckpointRejected) (1733))
+ ((BackupInProgress) (1734))
+ ((ChunkIsNotPreloaded) (1735))
+ ((NoInSyncReplicas) (1736))
+ ((CellHasNoAssignedPeers) (1737))
+ ((TableSchemaIncompatible) (1738))
+ ((BundleIsBanned) (1739))
+);
+
+DEFINE_ENUM(EInMemoryMode,
+ ((None) (0))
+ ((Compressed) (1))
+ ((Uncompressed)(2))
+);
+
+DEFINE_ENUM(EOrderedTableBackupMode,
+ ((Exact) (0))
+ ((AtLeast) (1))
+ ((AtMost) (2))
+);
+
+using TTabletCellId = NHydra::TCellId;
+extern const TTabletCellId NullTabletCellId;
+
+using TTabletId = NObjectClient::TObjectId;
+extern const TTabletId NullTabletId;
+
+using TStoreId = NObjectClient::TObjectId;
+extern const TStoreId NullStoreId;
+
+using TPartitionId = NObjectClient::TObjectId;
+extern const TPartitionId NullPartitionId;
+
+using TTabletCellBundleId = NObjectClient::TObjectId;
+extern const TTabletCellBundleId NullTabletCellBundleId;
+
+using THunkStorageId = NObjectClient::TObjectId;
+extern const THunkStorageId NullHunkStorageId;
+
+constexpr int TypicalTableReplicaCount = 6;
+
+using TTableReplicaId = NObjectClient::TObjectId;
+using TTabletActionId = NObjectClient::TObjectId;
+using TDynamicStoreId = NObjectClient::TObjectId;
+using TTabletOwnerId = NObjectClient::TObjectId;
+using TAreaId = NObjectClient::TObjectId;
+
+DEFINE_BIT_ENUM(EReplicationLogDataFlags,
+ ((None) (0x0000))
+ ((Missing) (0x0001))
+ ((Aggregate) (0x0002))
+);
+
+struct TReplicationLogTable
+{
+ static const TString ChangeTypeColumnName;
+ static const TString KeyColumnNamePrefix;
+ static const TString ValueColumnNamePrefix;
+ static const TString FlagsColumnNamePrefix;
+};
+
+DEFINE_BIT_ENUM(EUnversionedUpdateDataFlags,
+ ((None) (0x0000))
+ ((Missing) (0x0001))
+ ((Aggregate) (0x0002))
+);
+
+constexpr EUnversionedUpdateDataFlags MinValidUnversionedUpdateDataFlags = EUnversionedUpdateDataFlags::None;
+constexpr EUnversionedUpdateDataFlags MaxValidUnversionedUpdateDataFlags =
+ EUnversionedUpdateDataFlags::Missing | EUnversionedUpdateDataFlags::Aggregate;
+
+struct TUnversionedUpdateSchema
+{
+ static const TString ChangeTypeColumnName;
+ static const TString ValueColumnNamePrefix;
+ static const TString FlagsColumnNamePrefix;
+};
+
+DEFINE_ENUM(ETabletCellHealth,
+ (Initializing)
+ (Good)
+ (Degraded)
+ (Failed)
+);
+
+DEFINE_ENUM(ETableReplicaState,
+ ((None) (0))
+ ((Disabling) (1))
+ ((Disabled) (2))
+ ((Enabling) (4))
+ ((Enabled) (3))
+);
+
+DEFINE_ENUM(ETableReplicaStatus,
+ ((Unknown) (0))
+
+ ((SyncInSync) (1))
+ ((SyncCatchingUp) (2))
+ ((SyncNotWritable) (3))
+
+ ((AsyncInSync) (4))
+ ((AsyncCatchingUp) (5))
+ ((AsyncNotWritable) (6))
+);
+
+DEFINE_ENUM(ETabletActionKind,
+ ((Move) (0))
+ ((Reshard) (1))
+);
+
+DEFINE_ENUM(ETabletActionState,
+ ((Preparing) (0))
+ ((Freezing) (1))
+ ((Frozen) (2))
+ ((Unmounting) (3))
+ ((Unmounted) (4))
+ ((Orphaned) (10))
+ ((Mounting) (5))
+ ((Mounted) (6))
+ ((Completed) (7))
+ ((Failing) (8))
+ ((Failed) (9))
+);
+
+DEFINE_ENUM(ETabletServiceFeatures,
+ ((WriteGenerations) (0))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TTableMountCacheConfig)
+DECLARE_REFCOUNTED_CLASS(TTableMountCacheDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TRemoteDynamicStoreReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TRetryingRemoteDynamicStoreReaderConfig)
+DECLARE_REFCOUNTED_CLASS(TReplicatedTableOptions)
+
+DECLARE_REFCOUNTED_STRUCT(TTableMountInfo)
+DECLARE_REFCOUNTED_STRUCT(TTabletInfo)
+DECLARE_REFCOUNTED_STRUCT(TTableReplicaInfo)
+DECLARE_REFCOUNTED_STRUCT(ITableMountCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/table_mount_cache.cpp b/yt/yt/client/tablet_client/table_mount_cache.cpp
new file mode 100644
index 0000000000..d7958f1ea3
--- /dev/null
+++ b/yt/yt/client/tablet_client/table_mount_cache.cpp
@@ -0,0 +1,182 @@
+#include "table_mount_cache.h"
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <yt/yt/client/table_client/key_bound.h>
+
+namespace NYT::NTabletClient {
+
+using namespace NTableClient;
+using namespace NObjectClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TKeyBound TTabletInfo::GetLowerKeyBound() const
+{
+ return TKeyBound::FromRow() >= PivotKey;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TTableMountInfo::IsSorted() const
+{
+ return Schemas[ETableSchemaKind::Primary]->IsSorted();
+}
+
+bool TTableMountInfo::IsOrdered() const
+{
+ return !IsSorted();
+}
+
+bool TTableMountInfo::IsReplicated() const
+{
+ return TypeFromId(TableId) == EObjectType::ReplicatedTable;
+}
+
+bool TTableMountInfo::IsChaosReplicated() const
+{
+ return TypeFromId(TableId) == EObjectType::ChaosReplicatedTable;
+}
+
+bool TTableMountInfo::IsReplicationLog() const
+{
+ return TypeFromId(TableId) == EObjectType::ReplicationLogTable;
+}
+
+bool TTableMountInfo::IsHunkStorage() const
+{
+ return TypeFromId(TableId) == EObjectType::HunkStorage;
+}
+
+bool TTableMountInfo::IsPhysicallyLog() const
+{
+ return IsReplicated() || IsReplicationLog();
+}
+
+bool TTableMountInfo::IsChaosReplica() const
+{
+ return TypeFromId(UpstreamReplicaId) == EObjectType::ChaosTableReplica;
+}
+
+TTabletInfoPtr TTableMountInfo::GetTabletByIndexOrThrow(int tabletIndex) const
+{
+ if (tabletIndex < 0 || tabletIndex >= std::ssize(Tablets)) {
+ THROW_ERROR_EXCEPTION("Invalid tablet index for table %v: expected in range [0,%v], got %v",
+ Path,
+ Tablets.size() - 1,
+ tabletIndex);
+ }
+ return Tablets[tabletIndex];
+}
+
+int TTableMountInfo::GetTabletIndexForKey(TUnversionedValueRange key) const
+{
+ ValidateDynamic();
+ auto it = std::upper_bound(
+ Tablets.begin(),
+ Tablets.end(),
+ key,
+ [&] (TUnversionedValueRange key, const TTabletInfoPtr& rhs) {
+ return CompareValueRanges(key, rhs->PivotKey.Elements()) < 0;
+ });
+ YT_VERIFY(it != Tablets.begin());
+ return std::distance(Tablets.begin(), it - 1);
+}
+
+int TTableMountInfo::GetTabletIndexForKey(TUnversionedRow key) const
+{
+ return GetTabletIndexForKey(key.Elements());
+}
+
+TTabletInfoPtr TTableMountInfo::GetTabletForKey(TUnversionedValueRange key) const
+{
+ auto index = GetTabletIndexForKey(key);
+ return Tablets[index];
+}
+
+TTabletInfoPtr TTableMountInfo::GetTabletForRow(TUnversionedRow row) const
+{
+ int keyColumnCount = Schemas[ETableSchemaKind::Primary]->GetKeyColumnCount();
+ YT_VERIFY(static_cast<int>(row.GetCount()) >= keyColumnCount);
+ return GetTabletForKey(row.FirstNElements(keyColumnCount));
+}
+
+TTabletInfoPtr TTableMountInfo::GetTabletForRow(TVersionedRow row) const
+{
+ int keyColumnCount = Schemas[ETableSchemaKind::Primary]->GetKeyColumnCount();
+ YT_VERIFY(row.GetKeyCount() == keyColumnCount);
+ return GetTabletForKey(row.Keys());
+}
+
+int TTableMountInfo::GetRandomMountedTabletIndex() const
+{
+ ValidateTabletOwner();
+
+ if (MountedTablets.empty()) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::TabletNotMounted,
+ "Table %v has no mounted tablets",
+ Path);
+ }
+
+ return RandomNumber(MountedTablets.size());
+}
+
+TTabletInfoPtr TTableMountInfo::GetRandomMountedTablet() const
+{
+ return MountedTablets[GetRandomMountedTabletIndex()];
+}
+
+void TTableMountInfo::ValidateTabletOwner() const
+{
+ if (!Dynamic && !IsHunkStorage()) {
+ THROW_ERROR_EXCEPTION("Table %v is neither dynamic nor a hunk storage", Path);
+ }
+}
+
+void TTableMountInfo::ValidateDynamic() const
+{
+ if (!Dynamic) {
+ THROW_ERROR_EXCEPTION("Table %v is not dynamic", Path);
+ }
+}
+
+void TTableMountInfo::ValidateSorted() const
+{
+ if (!IsSorted()) {
+ THROW_ERROR_EXCEPTION("Table %v is not sorted", Path);
+ }
+}
+
+void TTableMountInfo::ValidateOrdered() const
+{
+ if (!IsOrdered()) {
+ THROW_ERROR_EXCEPTION("Table %v is not ordered", Path);
+ }
+}
+
+void TTableMountInfo::ValidateNotPhysicallyLog() const
+{
+ if (IsPhysicallyLog()) {
+ THROW_ERROR_EXCEPTION("Table %v physically contains replication log", Path);
+ }
+}
+
+void TTableMountInfo::ValidateReplicated() const
+{
+ if (!IsReplicated()) {
+ THROW_ERROR_EXCEPTION("Table %v is not replicated", Path);
+ }
+}
+
+void TTableMountInfo::ValidateReplicationLog() const
+{
+ if (!IsReplicationLog()) {
+ THROW_ERROR_EXCEPTION("Table %v is not replication log", Path);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
+
diff --git a/yt/yt/client/tablet_client/table_mount_cache.h b/yt/yt/client/tablet_client/table_mount_cache.h
new file mode 100644
index 0000000000..bfc5732ed3
--- /dev/null
+++ b/yt/yt/client/tablet_client/table_mount_cache.h
@@ -0,0 +1,172 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/client/hive/public.h>
+
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+#include <yt/yt/client/table_client/public.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/versioned_row.h>
+
+#include <yt/yt/client/chaos_client/replication_card.h>
+
+#include <yt/yt/client/ypath/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <util/datetime/base.h>
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTabletInfo
+ : public TRefCounted
+{
+ TTabletId TabletId;
+ NHydra::TRevision MountRevision = NHydra::NullRevision;
+ ETabletState State;
+ EInMemoryMode InMemoryMode;
+ NTableClient::TLegacyOwningKey PivotKey;
+ TTabletCellId CellId;
+ NObjectClient::TObjectId TableId;
+ TInstant UpdateTime;
+ std::vector<TWeakPtr<TTableMountInfo>> Owners;
+
+ NTableClient::TKeyBound GetLowerKeyBound() const;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTabletInfo)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTableReplicaInfo
+ : public TRefCounted
+{
+ TTableReplicaId ReplicaId;
+ TString ClusterName;
+ NYPath::TYPath ReplicaPath;
+ ETableReplicaMode Mode;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableReplicaInfo)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Describes the primary and the auxiliary schemas derived from the table schema.
+//! Cf. TTableSchema::ToXXX methods.
+DEFINE_ENUM(ETableSchemaKind,
+ // Schema assigned to Cypress node, as is.
+ (Primary)
+ // Schema used for inserting rows.
+ (Write)
+ // Schema used for querying rows.
+ (Query)
+ // Schema used for deleting rows.
+ (Delete)
+ // Schema used for writing versioned rows (during replication).
+ (VersionedWrite)
+ // Schema used for looking up rows.
+ (Lookup)
+ // For sorted schemas, coincides with primary.
+ // For ordered, contains an additional tablet index columns.
+ (PrimaryWithTabletIndex)
+ // Schema used for replication log rows.
+ (ReplicationLog)
+);
+
+struct TTableMountInfo
+ : public TRefCounted
+{
+ NYPath::TYPath Path;
+ NObjectClient::TObjectId TableId;
+ TEnumIndexedVector<ETableSchemaKind, NTableClient::TTableSchemaPtr> Schemas;
+
+ NObjectClient::TObjectId HunkStorageId;
+
+ bool Dynamic;
+ TTableReplicaId UpstreamReplicaId;
+ bool NeedKeyEvaluation;
+
+ NChaosClient::TReplicationCardId ReplicationCardId;
+
+ std::vector<TTabletInfoPtr> Tablets;
+ std::vector<TTabletInfoPtr> MountedTablets;
+
+ std::vector<TTableReplicaInfoPtr> Replicas;
+
+ //! For sorted tables, these are -infinity and +infinity.
+ //! For ordered tablets, these are |[0]| and |[tablet_count]| resp.
+ NTableClient::TLegacyOwningKey LowerCapBound;
+ NTableClient::TLegacyOwningKey UpperCapBound;
+
+ // Master reply revision for master service cache invalidation.
+ NHydra::TRevision PrimaryRevision;
+ NHydra::TRevision SecondaryRevision;
+
+ bool EnableDetailedProfiling = false;
+
+ bool IsSorted() const;
+ bool IsOrdered() const;
+ bool IsReplicated() const;
+ bool IsChaosReplicated() const;
+ bool IsReplicationLog() const;
+ bool IsPhysicallyLog() const;
+ bool IsChaosReplica() const;
+ bool IsHunkStorage() const;
+
+ TTabletInfoPtr GetTabletByIndexOrThrow(int tabletIndex) const;
+ int GetTabletIndexForKey(NTableClient::TUnversionedValueRange key) const;
+ int GetTabletIndexForKey(NTableClient::TLegacyKey key) const;
+ TTabletInfoPtr GetTabletForKey(NTableClient::TUnversionedValueRange key) const;
+ TTabletInfoPtr GetTabletForRow(NTableClient::TUnversionedRow row) const;
+ TTabletInfoPtr GetTabletForRow(NTableClient::TVersionedRow row) const;
+ int GetRandomMountedTabletIndex() const;
+ TTabletInfoPtr GetRandomMountedTablet() const;
+
+ void ValidateTabletOwner() const;
+ void ValidateDynamic() const;
+ void ValidateSorted() const;
+ void ValidateOrdered() const;
+ void ValidateNotPhysicallyLog() const;
+ void ValidateReplicated() const;
+ void ValidateReplicationLog() const;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTableMountInfo)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITableMountCache
+ : public virtual TRefCounted
+{
+ virtual TFuture<TTableMountInfoPtr> GetTableInfo(const NYPath::TYPath& path) = 0;
+ virtual TTabletInfoPtr FindTabletInfo(TTabletId tabletId) = 0;
+ virtual void InvalidateTablet(TTabletInfoPtr tabletInfo) = 0;
+
+ //! If #error is unretryable, returns null.
+ //! Otherwise invalidates cached tablet info (if it can be inferred from #error)
+ //! and returns actual retryable error code as first element and
+ //! tablet info as second element.
+ virtual std::pair<std::optional<TErrorCode>, TTabletInfoPtr> InvalidateOnError(
+ const TError& error,
+ bool forceRetry) = 0;
+
+ virtual void Clear() = 0;
+
+ virtual void Reconfigure(TTableMountCacheConfigPtr config) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITableMountCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/table_mount_cache_detail.cpp b/yt/yt/client/tablet_client/table_mount_cache_detail.cpp
new file mode 100644
index 0000000000..88b09bd406
--- /dev/null
+++ b/yt/yt/client/tablet_client/table_mount_cache_detail.cpp
@@ -0,0 +1,266 @@
+#include "table_mount_cache_detail.h"
+
+#include "config.h"
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+namespace NYT::NTabletClient {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto TabletCacheSweepPeriod = TDuration::Seconds(60);
+
+///////////////////////////////////////////////////////////////////////////////
+
+TTabletInfoCache::TTabletInfoCache(NLogging::TLogger logger)
+ : Logger(std::move(logger))
+{ }
+
+TTabletInfoPtr TTabletInfoCache::Find(TTabletId tabletId)
+{
+ SweepExpiredEntries();
+
+ auto guard = ReaderGuard(MapLock_);
+ ProcessNextGCQueueEntry();
+ auto it = Map_.find(tabletId);
+ return it != Map_.end() ? it->second.Lock() : nullptr;
+}
+
+TTabletInfoPtr TTabletInfoCache::Insert(const TTabletInfoPtr& tabletInfo)
+{
+ SweepExpiredEntries();
+
+ auto guard = WriterGuard(MapLock_);
+ ProcessNextGCQueueEntry();
+ typename decltype(Map_)::insert_ctx context;
+ auto it = Map_.find(tabletInfo->TabletId, context);
+ if (it != Map_.end()) {
+ if (auto existingTabletInfo = it->second.Lock()) {
+ if (tabletInfo->MountRevision < existingTabletInfo->MountRevision) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::InvalidMountRevision,
+ "Tablet mount revision %x is outdated",
+ tabletInfo->MountRevision)
+ << TErrorAttribute("tablet_id", tabletInfo->TabletId);
+ }
+
+ for (const auto& owner : existingTabletInfo->Owners) {
+ if (!owner.IsExpired()) {
+ tabletInfo->Owners.push_back(owner);
+ }
+ }
+ }
+ it->second = MakeWeak(tabletInfo);
+ } else {
+ Map_.emplace_direct(context, tabletInfo->TabletId, tabletInfo);
+ guard.Release();
+
+ auto gcGuard = Guard(GCLock_);
+ GCQueue_.push(tabletInfo->TabletId);
+ }
+
+ return tabletInfo;
+}
+
+void TTabletInfoCache::Clear()
+{
+ decltype(Map_) other;
+ {
+ auto guard = WriterGuard(MapLock_);
+ other = std::move(Map_);
+ }
+}
+
+void TTabletInfoCache::SweepExpiredEntries()
+{
+ auto now = NProfiling::GetCpuInstant();
+ auto deadline = ExpiredEntriesSweepDeadline_.load(std::memory_order::relaxed);
+ if (now < deadline) {
+ return;
+ }
+
+ if (!ExpiredEntriesSweepDeadline_.compare_exchange_strong(deadline, now + NProfiling::DurationToCpuDuration(TabletCacheSweepPeriod))) {
+ return;
+ }
+
+ decltype(ExpiredTabletIds_) expiredTabletIds;
+ {
+ auto gcGuard = Guard(GCLock_);
+ expiredTabletIds = std::move(ExpiredTabletIds_);
+ }
+
+ if (!expiredTabletIds.empty()) {
+ YT_LOG_DEBUG("Start sweeping expired tablet info (ExpiredTabletCount: %v)",
+ expiredTabletIds.size());
+
+ for (auto id : expiredTabletIds) {
+ auto guard = WriterGuard(MapLock_);
+ if (auto it = Map_.find(id); it) {
+ if (it->second.IsExpired()) {
+ Map_.erase(it);
+ continue;
+ }
+
+ guard.Release();
+
+ auto gcGuard = Guard(GCLock_);
+ GCQueue_.push(id);
+ }
+ }
+
+ YT_LOG_DEBUG("Finish sweeping expired tablet info");
+ }
+}
+
+void TTabletInfoCache::ProcessNextGCQueueEntry()
+{
+ VERIFY_SPINLOCK_AFFINITY(MapLock_);
+ auto gcGuard = Guard(GCLock_);
+ if (!GCQueue_.empty()) {
+ const auto& id = GCQueue_.front();
+ if (auto it = Map_.find(id); it) {
+ if (it->second.IsExpired()) {
+ ExpiredTabletIds_.push_back(id);
+ } else {
+ GCQueue_.push(id);
+ }
+ }
+ GCQueue_.pop();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableMountCacheBase::TTableMountCacheBase(
+ TTableMountCacheConfigPtr config,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler)
+ : TAsyncExpiringCache(
+ config,
+ logger.WithTag("Cache: TableMount"),
+ profiler)
+ , Logger(std::move(logger))
+ , TabletInfoCache_(Logger)
+ , Config_(std::move(config))
+{ }
+
+TFuture<TTableMountInfoPtr> TTableMountCacheBase::GetTableInfo(const NYPath::TYPath& path)
+{
+ auto [future, requestInitialized] = TAsyncExpiringCache::GetExtended(path);
+
+ bool shouldThrow = false;
+ if (!requestInitialized && !future.IsSet()) {
+ auto guard = ReaderGuard(SpinLock_);
+ shouldThrow = Config_->RejectIfEntryIsRequestedButNotReady;
+ }
+ if (shouldThrow) {
+ // COMPAT(babenko): replace with TransientFailure error code.
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable,
+ "Mount info is unavailable, please try again")
+ << TError(NTabletClient::EErrorCode::TableMountInfoNotReady,
+ "Table mount info is not ready, but has already been requested")
+ << TErrorAttribute("path", path);
+ }
+
+ return future;
+}
+
+TTabletInfoPtr TTableMountCacheBase::FindTabletInfo(TTabletId tabletId)
+{
+ return TabletInfoCache_.Find(tabletId);
+}
+
+void TTableMountCacheBase::InvalidateTablet(TTabletInfoPtr tabletInfo)
+{
+ for (const auto& weakOwner : tabletInfo->Owners) {
+ if (auto owner = weakOwner.Lock()) {
+ InvalidateTable(owner);
+ }
+ }
+}
+
+std::pair<std::optional<TErrorCode>, TTabletInfoPtr> TTableMountCacheBase::InvalidateOnError(
+ const TError& error,
+ bool forceRetry)
+{
+ static const std::vector<TErrorCode> retriableCodes = {
+ NTabletClient::EErrorCode::NoSuchTablet,
+ NTabletClient::EErrorCode::TabletNotMounted,
+ NTabletClient::EErrorCode::InvalidMountRevision,
+ NYTree::EErrorCode::ResolveError
+ };
+
+ if (!error.IsOK()) {
+ for (auto code : retriableCodes) {
+ if (auto retriableError = error.FindMatching(code)) {
+ auto tabletId = retriableError->Attributes().Find<TTabletId>("tablet_id");
+ if (!tabletId) {
+ continue;
+ }
+
+ auto isTabletUnmounted = retriableError->Attributes().Get<bool>("is_tablet_unmounted", false);
+ auto tabletInfo = FindTabletInfo(*tabletId);
+ if (tabletInfo) {
+ YT_LOG_DEBUG(error,
+ "Invalidating tablet in table mount cache "
+ "(TabletId: %v, CellId: %v, MountRevision: %x, IsTabletUnmounted: %v, Owners: %v)",
+ tabletInfo->TabletId,
+ tabletInfo->CellId,
+ tabletInfo->MountRevision,
+ isTabletUnmounted,
+ MakeFormattableView(tabletInfo->Owners, [] (auto* builder, const auto& weakOwner) {
+ if (auto owner = weakOwner.Lock()) {
+ FormatValue(builder, owner->Path, TStringBuf());
+ } else {
+ builder->AppendString(TStringBuf("<expired>"));
+ }
+ }));
+
+ InvalidateTablet(tabletInfo);
+ }
+
+ std::optional<TErrorCode> retryableErrorCode = code;
+ if (code == NTabletClient::EErrorCode::TabletNotMounted &&
+ isTabletUnmounted &&
+ !forceRetry)
+ {
+ retryableErrorCode = std::nullopt;
+ }
+
+ return std::make_pair(retryableErrorCode, tabletInfo);
+ }
+ }
+ }
+
+ return std::make_pair(std::nullopt, nullptr);
+}
+
+void TTableMountCacheBase::Clear()
+{
+ TAsyncExpiringCache::Clear();
+ TabletInfoCache_.Clear();
+ YT_LOG_DEBUG("Table mount info cache cleared");
+}
+
+void TTableMountCacheBase::Reconfigure(TTableMountCacheConfigPtr config)
+{
+ TAsyncExpiringCache::Reconfigure(config);
+ {
+ auto guard = WriterGuard(SpinLock_);
+ Config_ = config;
+ }
+ YT_LOG_DEBUG("Table mount info cache reconfigured (NewConfig: %v)",
+ NYson::ConvertToYsonString(config, NYson::EYsonFormat::Text).AsStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/tablet_client/table_mount_cache_detail.h b/yt/yt/client/tablet_client/table_mount_cache_detail.h
new file mode 100644
index 0000000000..94a2026759
--- /dev/null
+++ b/yt/yt/client/tablet_client/table_mount_cache_detail.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "table_mount_cache.h"
+
+#include <yt/yt/core/misc/async_expiring_cache.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NTabletClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTabletInfoCache
+{
+public:
+ explicit TTabletInfoCache(NLogging::TLogger logger);
+
+ TTabletInfoPtr Find(TTabletId tabletId);
+ TTabletInfoPtr Insert(const TTabletInfoPtr& tabletInfo);
+ void Clear();
+
+private:
+ const NLogging::TLogger Logger;
+
+ std::atomic<NProfiling::TCpuInstant> ExpiredEntriesSweepDeadline_ = 0;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, MapLock_);
+ THashMap<TTabletId, TWeakPtr<TTabletInfo>> Map_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, GCLock_);
+ std::queue<TTabletId> GCQueue_;
+ std::vector<TTabletId> ExpiredTabletIds_;
+
+ void SweepExpiredEntries();
+ void ProcessNextGCQueueEntry();
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TTableMountCacheBase
+ : public ITableMountCache
+ , public TAsyncExpiringCache<NYPath::TYPath, TTableMountInfoPtr>
+{
+public:
+ TTableMountCacheBase(
+ TTableMountCacheConfigPtr config,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler = {});
+
+ TFuture<TTableMountInfoPtr> GetTableInfo(const NYPath::TYPath& path) override;
+ TTabletInfoPtr FindTabletInfo(TTabletId tabletId) override;
+ void InvalidateTablet(TTabletInfoPtr tabletInfo) override;
+ std::pair<std::optional<TErrorCode>, TTabletInfoPtr> InvalidateOnError(
+ const TError& error,
+ bool forceRetry) override;
+
+ void Clear() override;
+
+ void Reconfigure(TTableMountCacheConfigPtr config) override;
+
+protected:
+ const NLogging::TLogger Logger;
+
+ TTabletInfoCache TabletInfoCache_;
+
+ virtual void InvalidateTable(const TTableMountInfoPtr& tableInfo) = 0;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ TTableMountCacheConfigPtr Config_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTabletClient
diff --git a/yt/yt/client/transaction_client/batching_timestamp_provider.cpp b/yt/yt/client/transaction_client/batching_timestamp_provider.cpp
new file mode 100644
index 0000000000..e6a5e299ab
--- /dev/null
+++ b/yt/yt/client/transaction_client/batching_timestamp_provider.cpp
@@ -0,0 +1,170 @@
+#include "batching_timestamp_provider.h"
+#include "timestamp_provider.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/library/tracing/batch_trace.h>
+
+namespace NYT::NTransactionClient {
+
+using namespace NConcurrency;
+using namespace NTracing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBatchingTimestampProvider
+ : public ITimestampProvider
+{
+public:
+ TBatchingTimestampProvider(
+ ITimestampProviderPtr underlying,
+ TDuration batchPeriod)
+ : Underlying_(std::move(underlying))
+ , BatchPeriod_(batchPeriod)
+ { }
+
+ TFuture<TTimestamp> GenerateTimestamps(int count) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ TFuture<TTimestamp> result;
+
+ {
+ auto guard = Guard(SpinLock_);
+ PendingRequests_.emplace_back();
+ PendingRequests_.back().Count = count;
+ PendingRequests_.back().Promise = NewPromise<TTimestamp>();
+ result = PendingRequests_.back().Promise.ToFuture().ToUncancelable();
+
+ BatchTrace_.Join();
+
+ TNullTraceContextGuard nullTraceContextGuard;
+ MaybeScheduleSendGenerateRequest(guard);
+ }
+
+ return result;
+ }
+
+ TTimestamp GetLatestTimestamp() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return Underlying_->GetLatestTimestamp();
+ }
+
+ private:
+ const ITimestampProviderPtr Underlying_;
+ const TDuration BatchPeriod_;
+
+ struct TRequest
+ {
+ int Count;
+ TPromise<TTimestamp> Promise;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ bool GenerateInProgress_ = false;
+ bool FlushScheduled_ = false;
+ std::vector<TRequest> PendingRequests_;
+ TBatchTrace BatchTrace_;
+
+ TInstant LastRequestTime_;
+
+ void MaybeScheduleSendGenerateRequest(TGuard<NThreading::TSpinLock>& guard)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (PendingRequests_.empty() || GenerateInProgress_) {
+ return;
+ }
+
+ auto now = GetInstant();
+
+ if (LastRequestTime_ + BatchPeriod_ < now) {
+ SendGenerateRequest(guard);
+ } else if (!FlushScheduled_) {
+ FlushScheduled_ = true;
+ TDelayedExecutor::Submit(
+ BIND([this, this_ = MakeStrong(this)] {
+ auto guard = Guard(SpinLock_);
+ FlushScheduled_ = false;
+ if (GenerateInProgress_) {
+ return;
+ }
+ SendGenerateRequest(guard);
+ }),
+ BatchPeriod_ - (now - LastRequestTime_));
+ }
+ }
+
+ void SendGenerateRequest(TGuard<NThreading::TSpinLock>& guard)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ YT_VERIFY(!GenerateInProgress_);
+ GenerateInProgress_ = true;
+ LastRequestTime_ = GetInstant();
+
+ std::vector<TRequest> requests;
+ requests.swap(PendingRequests_);
+
+ auto [traceContext, sampled] = BatchTrace_.StartSpan("BatchingTimestampProvider:SendGenerateRequest");
+
+ guard.Release();
+
+ TTraceContextGuard traceContextGuard(traceContext);
+
+ int count = 0;
+ for (const auto& request : requests) {
+ count += request.Count;
+ }
+
+ Underlying_->GenerateTimestamps(count).Subscribe(BIND(
+ &TBatchingTimestampProvider::OnGenerateResponse,
+ MakeStrong(this),
+ Passed(std::move(requests))));
+ }
+
+ void OnGenerateResponse(
+ std::vector<TRequest> requests,
+ const TErrorOr<TTimestamp>& firstTimestampOrError)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ {
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(GenerateInProgress_);
+ GenerateInProgress_ = false;
+
+ MaybeScheduleSendGenerateRequest(guard);
+ }
+
+ if (firstTimestampOrError.IsOK()) {
+ auto timestamp = firstTimestampOrError.Value();
+ for (const auto& request : requests) {
+ request.Promise.Set(timestamp);
+ timestamp += request.Count;
+ }
+ } else {
+ for (const auto& request : requests) {
+ request.Promise.Set(TError(firstTimestampOrError));
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr CreateBatchingTimestampProvider(
+ ITimestampProviderPtr underlying,
+ TDuration batchPeriod)
+{
+ return New<TBatchingTimestampProvider>(std::move(underlying), batchPeriod);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/batching_timestamp_provider.h b/yt/yt/client/transaction_client/batching_timestamp_provider.h
new file mode 100644
index 0000000000..c8aeead636
--- /dev/null
+++ b/yt/yt/client/transaction_client/batching_timestamp_provider.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/datetime/base.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr CreateBatchingTimestampProvider(
+ ITimestampProviderPtr underlying,
+ TDuration batchPeriod);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/config.cpp b/yt/yt/client/transaction_client/config.cpp
new file mode 100644
index 0000000000..3264e5dcd1
--- /dev/null
+++ b/yt/yt/client/transaction_client/config.cpp
@@ -0,0 +1,34 @@
+#include "config.h"
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRemoteTimestampProviderConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("rpc_timeout", &TThis::RpcTimeout)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("latest_timestamp_update_period", &TThis::LatestTimestampUpdatePeriod)
+ // COMPAT(babenko)
+ .Alias("update_period")
+ .Default(TDuration::MilliSeconds(500));
+
+ registrar.Parameter("batch_period", &TThis::BatchPeriod)
+ .Default(TDuration::MilliSeconds(10));
+
+ registrar.Parameter("enable_timestamp_provider_discovery", &TThis::EnableTimestampProviderDiscovery)
+ .Default(false);
+ registrar.Parameter("timestamp_provider_discovery_period", &TThis::TimestampProviderDiscoveryPeriod)
+ .Default(TDuration::Minutes(1));
+ registrar.Parameter("timestamp_provider_discovery_period_splay", &TThis::TimestampProviderDiscoveryPeriodSplay)
+ .Default(TDuration::Seconds(10));
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->RetryAttempts = 100;
+ config->RetryTimeout = TDuration::Minutes(3);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/config.h b/yt/yt/client/transaction_client/config.h
new file mode 100644
index 0000000000..c4114b72c5
--- /dev/null
+++ b/yt/yt/client/transaction_client/config.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/rpc/config.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteTimestampProviderConfig
+ : public NRpc::TBalancingChannelConfig
+ , public NRpc::TRetryingChannelConfig
+{
+public:
+ //! Timeout for RPC requests to timestamp provider.
+ TDuration RpcTimeout;
+
+ //! Interval between consecutive updates of latest timestamp.
+ TDuration LatestTimestampUpdatePeriod;
+
+ //! All generation requests coming within this period are batched
+ //! together.
+ TDuration BatchPeriod;
+
+ bool EnableTimestampProviderDiscovery;
+ TDuration TimestampProviderDiscoveryPeriod;
+ TDuration TimestampProviderDiscoveryPeriodSplay;
+
+ REGISTER_YSON_STRUCT(TRemoteTimestampProviderConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRemoteTimestampProviderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/helpers.cpp b/yt/yt/client/transaction_client/helpers.cpp
new file mode 100644
index 0000000000..0350650991
--- /dev/null
+++ b/yt/yt/client/transaction_client/helpers.cpp
@@ -0,0 +1,101 @@
+#include "helpers.h"
+
+#include <yt/yt/client/object_client/helpers.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/guid.h>
+
+namespace NYT::NTransactionClient {
+
+using namespace NObjectClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsMasterTransactionId(TTransactionId id)
+{
+ auto type = TypeFromId(id);
+ // NB: Externalized transactions are for internal use only.
+ return type == NObjectClient::EObjectType::Transaction ||
+ type == NObjectClient::EObjectType::NestedTransaction ||
+ type == EObjectType::UploadTransaction ||
+ type == EObjectType::UploadNestedTransaction;
+}
+
+void ValidateTabletTransactionId(TTransactionId id)
+{
+ auto type = TypeFromId(id);
+ if (type != EObjectType::Transaction &&
+ type != EObjectType::AtomicTabletTransaction &&
+ type != EObjectType::NonAtomicTabletTransaction)
+ {
+ THROW_ERROR_EXCEPTION("%v is not a valid tablet transaction id",
+ id);
+ }
+}
+
+void ValidateMasterTransactionId(TTransactionId id)
+{
+ if (!IsMasterTransactionId(id)) {
+ THROW_ERROR_EXCEPTION("%v is not a valid master transaction id",
+ id);
+ }
+}
+
+std::pair<TInstant, TInstant> TimestampToInstant(TTimestamp timestamp)
+{
+ auto instant = TInstant::Seconds(UnixTimeFromTimestamp(timestamp));
+ return {
+ instant,
+ instant + TDuration::Seconds(1)
+ };
+}
+
+std::pair<TTimestamp, TTimestamp> InstantToTimestamp(TInstant instant)
+{
+ auto time = instant.Seconds();
+ return {
+ TimestampFromUnixTime(time),
+ TimestampFromUnixTime(time + 1)
+ };
+}
+
+std::pair<TDuration, TDuration> TimestampDiffToDuration(TTimestamp loTimestamp, TTimestamp hiTimestamp)
+{
+ YT_ASSERT(loTimestamp <= hiTimestamp);
+ auto loInstant = TimestampToInstant(loTimestamp);
+ auto hiInstant = TimestampToInstant(hiTimestamp);
+ return std::make_pair(
+ hiInstant.first >= loInstant.second ? hiInstant.first - loInstant.second : TDuration::Zero(),
+ hiInstant.second - loInstant.first);
+}
+
+ui64 UnixTimeFromTimestamp(TTimestamp timestamp)
+{
+ return timestamp >> TimestampCounterWidth;
+}
+
+TTimestamp TimestampFromUnixTime(ui64 time)
+{
+ return time << TimestampCounterWidth;
+}
+
+TTimestamp EmbedCellTagIntoTimestamp(TTimestamp timestamp, NObjectClient::TCellTag cellTag)
+{
+ static_assert(sizeof(TCellTag) == 2, "Invalid TCellTag size");
+
+ YT_VERIFY((timestamp & ((1ull << TimestampCounterWidth) - 1)) == 0);
+
+ return timestamp ^ (static_cast<ui32>(cellTag.Underlying()) << (TimestampCounterWidth - 16));
+}
+
+bool CanAdvanceTimestampWithEmbeddedCellTag(TTimestamp timestamp, int delta)
+{
+ static_assert(sizeof(TCellTag) == 2, "Invalid TCellTag size");
+
+ return (timestamp >> (TimestampCounterWidth - 16)) ==
+ ((timestamp + delta) >> (TimestampCounterWidth - 16));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/helpers.h b/yt/yt/client/transaction_client/helpers.h
new file mode 100644
index 0000000000..f2ccbee427
--- /dev/null
+++ b/yt/yt/client/transaction_client/helpers.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/datetime/base.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents that only values with timestamp in range [|RetentionTimestamp|, |Timestamp|]
+//! should be read.
+//! Note that both endpoints are inclusive.
+struct TReadTimestampRange
+{
+ NTransactionClient::TTimestamp Timestamp = NTransactionClient::NullTimestamp;
+ NTransactionClient::TTimestamp RetentionTimestamp = NTransactionClient::NullTimestamp;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Checks if #id represents a valid transaction accepted by masters:
+//! the type of #id must be either
+//! #EObjectType::Transaction or #EObjectType::NestedTransaction.
+bool IsMasterTransactionId(TTransactionId id);
+
+//! Checks if #id represents a valid transaction accepted by tablets:
+//! the type of #id must be either
+//! #EObjectType::Transaction, #EObjectType::AtomicTabletTransaction,
+//! or #EObjectType::NonAtomicTabletTransaction.
+void ValidateTabletTransactionId(TTransactionId id);
+
+//! Checks if #id represents a valid transaction accepted by masters:
+//! the type of #id must be one of
+//! #EObjectType::Transaction, #EObjectType::NestedTransaction,
+//! #EObjectType::UploadTransaction, or #EObjectType::UploadNestedTransaction.
+void ValidateMasterTransactionId(TTransactionId id);
+
+//! Returns a range of instants containing a given timestamp.
+std::pair<TInstant, TInstant> TimestampToInstant(TTimestamp timestamp);
+
+//! Returns a range of timestamps containing a given timestamp.
+std::pair<TTimestamp, TTimestamp> InstantToTimestamp(TInstant instant);
+
+//! Returns a range of durations between given timestamps.
+std::pair<TDuration, TDuration> TimestampDiffToDuration(TTimestamp loTimestamp, TTimestamp hiTimestamp);
+
+//! Extracts the "unix time" part of the timestamp.
+ui64 UnixTimeFromTimestamp(TTimestamp timestamp);
+
+//! Constructs the timestamp from a given unix time (assuming zero counter part).
+TTimestamp TimestampFromUnixTime(ui64 time);
+
+//! Embeds the cell tag into the higher counter bits of the timestamp (assuming zero rest counter part).
+TTimestamp EmbedCellTagIntoTimestamp(TTimestamp timestamp, NObjectClient::TCellTag cellTag);
+
+//! Checks if the timestamp with embedded cell tag can be advanced by |delta|
+//! without overflowing counter part.
+bool CanAdvanceTimestampWithEmbeddedCellTag(TTimestamp timestamp, int delta);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/noop_timestamp_provider.cpp b/yt/yt/client/transaction_client/noop_timestamp_provider.cpp
new file mode 100644
index 0000000000..52aa1a955c
--- /dev/null
+++ b/yt/yt/client/transaction_client/noop_timestamp_provider.cpp
@@ -0,0 +1,34 @@
+#include "noop_timestamp_provider.h"
+
+#include "private.h"
+#include "timestamp_provider.h"
+
+namespace NYT::NTransactionClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TNoopTimestampProvider
+ : public ITimestampProvider
+{
+public:
+ TFuture<TTimestamp> GenerateTimestamps(int /*count*/) override
+ {
+ return MakeFuture(NullTimestamp);
+ }
+
+ TTimestamp GetLatestTimestamp() override
+ {
+ return NullTimestamp;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr CreateNoopTimestampProvider()
+{
+ return New<TNoopTimestampProvider>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNTransactionClient
diff --git a/yt/yt/client/transaction_client/noop_timestamp_provider.h b/yt/yt/client/transaction_client/noop_timestamp_provider.h
new file mode 100644
index 0000000000..e4cbc7fbd1
--- /dev/null
+++ b/yt/yt/client/transaction_client/noop_timestamp_provider.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NTransactionClient {
+
+///////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr CreateNoopTimestampProvider();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/private.h b/yt/yt/client/transaction_client/private.h
new file mode 100644
index 0000000000..24c8359ee4
--- /dev/null
+++ b/yt/yt/client/transaction_client/private.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger TransactionClientLogger("TransactionClient");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/public.h b/yt/yt/client/transaction_client/public.h
new file mode 100644
index 0000000000..ac38af4dd6
--- /dev/null
+++ b/yt/yt/client/transaction_client/public.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <yt/yt/client/object_client/public.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/memory/ref_counted.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETransactionType,
+ ((Master) (0)) // Accepted by both masters and tablets
+ ((Tablet) (1)) // Accepted by tablets only
+);
+
+DEFINE_ENUM(EAtomicity,
+ ((Full) (0)) // 2PC enabled, Percolator mode :)
+ ((None) (1)) // 2PC disabled, HBase mode
+);
+
+DEFINE_ENUM(EDurability,
+ ((Sync) (0)) // Wait for Hydra commit result
+ ((Async) (1)) // Reply as soon as the request is enqueued to Hydra
+);
+
+//! Only applies to ordered tables.
+DEFINE_ENUM(ECommitOrdering,
+ ((Weak) (0)) // Rows are appended to tablet in order of participant commits
+ ((Strong) (1)) // Rows are appended to tablet in order of timestamps
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((NoSuchTransaction) (11000))
+ ((NestedExternalTransactionExists) (11001))
+ ((TransactionDepthLimitReached) (11002))
+ ((InvalidTransactionState) (11003))
+ ((ParticipantFailedToPrepare) (11004))
+ ((SomeParticipantsAreDown) (11005))
+ ((AlienTransactionsForbidden) (11006))
+ ((MalformedAlienTransaction) (11007))
+ ((InvalidTransactionAtomicity) (11008))
+ ((UploadTransactionCannotHaveNested)(11009))
+ ((ForeignParentTransaction) (11010))
+ ((ForeignPrerequisiteTransaction) (11011))
+ ((IncompletePrepareSignature) (11012))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NObjectClient::TTransactionId;
+using NObjectClient::NullTransactionId;
+
+//! Timestamp is a cluster-wide unique monotonically increasing number
+//! used to implement the MVCC paradigm.
+/*!
+ * Timestamp is a 64-bit unsigned integer of the following structure:
+ * bits 0-29: auto-incrementing counter (allowing up to ~10^9 timestamps per second)
+ * bits 30-61: Unix time in seconds (from 1 Jan 1970)
+ * bits 62-63: reserved
+ */
+using TTimestamp = ui64;
+
+//! Number of bits in the counter part.
+constexpr int TimestampCounterWidth = 30;
+
+// Timestamp values range:
+//! Minimum valid (non-sentinel) timestamp.
+constexpr TTimestamp MinTimestamp = 0x0000000000000001ULL;
+//! Maximum valid (non-sentinel) timestamp.
+constexpr TTimestamp MaxTimestamp = 0x3fffffffffffff00ULL;
+
+// User sentinels:
+//! Uninitialized/invalid timestamp.
+constexpr TTimestamp NullTimestamp = 0x0000000000000000ULL;
+//! Truly (serializable) latest committed version.
+//! May cause row blocking if concurrent writes are in progress.
+constexpr TTimestamp SyncLastCommittedTimestamp = 0x3fffffffffffff01ULL;
+//! Relaxed (non-serializable) latest committed version.
+//! Never leads to row blocking but may miss some concurrent writes.
+constexpr TTimestamp AsyncLastCommittedTimestamp = 0x3fffffffffffff04ULL;
+//! Used to fetch all committed values during e.g. flushes or compactions.
+//! Returns all versions that were committed at the moment the reader was created.
+//! Never leads to row blocking but may miss some concurrent writes.
+constexpr TTimestamp AllCommittedTimestamp = 0x3fffffffffffff03ULL;
+
+// System sentinels:
+//! Used by TSortedDynamicStore to mark values being written by transactions.
+constexpr TTimestamp UncommittedTimestamp = 0x3fffffffffffff02ULL;
+//! Used by TSortedDynamicStore in TLockDescriptor::PrepareTimestamp.
+//! Must be larger than SyncLastCommittedTimestamp.
+constexpr TTimestamp NotPreparedTimestamp = 0x3fffffffffffffffULL;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(ITimestampProvider)
+DECLARE_REFCOUNTED_CLASS(TRemoteTimestampProviderConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/remote_timestamp_provider.cpp b/yt/yt/client/transaction_client/remote_timestamp_provider.cpp
new file mode 100644
index 0000000000..045620a75e
--- /dev/null
+++ b/yt/yt/client/transaction_client/remote_timestamp_provider.cpp
@@ -0,0 +1,111 @@
+#include "remote_timestamp_provider.h"
+#include "batching_timestamp_provider.h"
+#include "timestamp_provider_base.h"
+#include "private.h"
+#include "config.h"
+#include "timestamp_service_proxy.h"
+
+#include <yt/yt/core/rpc/balancing_channel.h>
+#include <yt/yt/core/rpc/retrying_channel.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NTransactionClient {
+
+using namespace NRpc;
+using namespace NYTree;
+using namespace NObjectClient;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateTimestampProviderChannel(
+ TRemoteTimestampProviderConfigPtr config,
+ IChannelFactoryPtr channelFactory)
+{
+ auto endpointDescription = TString("TimestampProvider");
+ auto endpointAttributes = ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("timestamp_provider").Value(true)
+ .EndMap());
+ auto channel = CreateBalancingChannel(
+ config,
+ std::move(channelFactory),
+ std::move(endpointDescription),
+ std::move(endpointAttributes));
+ channel = CreateRetryingChannel(
+ config,
+ std::move(channel));
+ return channel;
+}
+
+IChannelPtr CreateTimestampProviderChannelFromAddresses(
+ TRemoteTimestampProviderConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ const std::vector<TString>& discoveredAddresses)
+{
+ auto channelConfig = CloneYsonStruct(config);
+ if (!discoveredAddresses.empty()) {
+ channelConfig->Addresses = discoveredAddresses;
+ }
+ return CreateTimestampProviderChannel(channelConfig, channelFactory);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteTimestampProvider
+ : public TTimestampProviderBase
+{
+public:
+ TRemoteTimestampProvider(
+ IChannelPtr channel,
+ TRemoteTimestampProviderConfigPtr config)
+ : TTimestampProviderBase(config->LatestTimestampUpdatePeriod)
+ , Config_(std::move(config))
+ , Proxy_(std::move(channel))
+ {
+ Proxy_.SetDefaultTimeout(Config_->RpcTimeout);
+ }
+
+private:
+ const TRemoteTimestampProviderConfigPtr Config_;
+
+ TTimestampServiceProxy Proxy_;
+
+ TFuture<TTimestamp> DoGenerateTimestamps(int count) override
+ {
+ auto req = Proxy_.GenerateTimestamps();
+ req->SetResponseHeavy(true);
+ req->set_count(count);
+ return req->Invoke().Apply(BIND([] (const TTimestampServiceProxy::TRspGenerateTimestampsPtr& rsp) {
+ return static_cast<TTimestamp>(rsp->timestamp());
+ }));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITimestampProviderPtr CreateRemoteTimestampProvider(
+ TRemoteTimestampProviderConfigPtr config,
+ IChannelPtr channel)
+{
+ return New<TRemoteTimestampProvider>(std::move(channel), std::move(config));
+}
+
+ITimestampProviderPtr CreateBatchingRemoteTimestampProvider(
+ TRemoteTimestampProviderConfigPtr config,
+ IChannelPtr channel)
+{
+ auto underlying = CreateRemoteTimestampProvider(config, std::move(channel));
+ return CreateBatchingTimestampProvider(
+ std::move(underlying),
+ config->BatchPeriod);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
+
diff --git a/yt/yt/client/transaction_client/remote_timestamp_provider.h b/yt/yt/client/transaction_client/remote_timestamp_provider.h
new file mode 100644
index 0000000000..91a7179e92
--- /dev/null
+++ b/yt/yt/client/transaction_client/remote_timestamp_provider.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NRpc::IChannelPtr CreateTimestampProviderChannel(
+ TRemoteTimestampProviderConfigPtr config,
+ NRpc::IChannelFactoryPtr channelFactory);
+
+NRpc::IChannelPtr CreateTimestampProviderChannelFromAddresses(
+ TRemoteTimestampProviderConfigPtr config,
+ NRpc::IChannelFactoryPtr channelFactory,
+ const std::vector<TString>& addresses);
+
+ITimestampProviderPtr CreateBatchingTimestampProvider(
+ ITimestampProviderPtr underlying,
+ TDuration batchPeriod);
+
+ITimestampProviderPtr CreateRemoteTimestampProvider(
+ TRemoteTimestampProviderConfigPtr config,
+ NRpc::IChannelPtr channel);
+
+ITimestampProviderPtr CreateBatchingRemoteTimestampProvider(
+ TRemoteTimestampProviderConfigPtr config,
+ NRpc::IChannelPtr channel);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/timestamp_provider.h b/yt/yt/client/transaction_client/timestamp_provider.h
new file mode 100644
index 0000000000..5bcefafc9b
--- /dev/null
+++ b/yt/yt/client/transaction_client/timestamp_provider.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Manages cluster-wide unique timestamps.
+/*!
+ * Thread affinity: any
+ */
+struct ITimestampProvider
+ : public virtual TRefCounted
+{
+ //! Generates a contiguous range of timestamps (of size #count)
+ //! that are guaranteed to be larger than all timestamps previously obtained via this instance.
+ //! Returns the first timestamp of that range.
+ virtual TFuture<TTimestamp> GenerateTimestamps(int count = 1) = 0;
+
+ //! Returns the latest timestamp returned from #GenerateTimestamps.
+ virtual TTimestamp GetLatestTimestamp() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITimestampProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
+
diff --git a/yt/yt/client/transaction_client/timestamp_provider_base.cpp b/yt/yt/client/transaction_client/timestamp_provider_base.cpp
new file mode 100644
index 0000000000..88db51c486
--- /dev/null
+++ b/yt/yt/client/transaction_client/timestamp_provider_base.cpp
@@ -0,0 +1,101 @@
+#include "timestamp_provider_base.h"
+#include "private.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+namespace NYT::NTransactionClient {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = TransactionClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTimestampProviderBase::TTimestampProviderBase(std::optional<TDuration> latestTimestampUpdatePeriod)
+ : LatestTimestampUpdatePeriod_(latestTimestampUpdatePeriod)
+{ }
+
+TFuture<TTimestamp> TTimestampProviderBase::GenerateTimestamps(int count)
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ YT_LOG_DEBUG("Generating fresh timestamps (Count: %v)", count);
+
+ return DoGenerateTimestamps(count).Apply(BIND(
+ &TTimestampProviderBase::OnGenerateTimestamps,
+ MakeStrong(this),
+ count));
+}
+
+TTimestamp TTimestampProviderBase::GetLatestTimestamp()
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto result = LatestTimestamp_.load(std::memory_order::relaxed);
+
+ if (LatestTimestampUpdatePeriod_ && ++GetLatestTimestampCallCounter_ == 1) {
+ LatestTimestampExecutor_ = New<TPeriodicExecutor>(
+ GetSyncInvoker(),
+ BIND(&TTimestampProviderBase::UpdateLatestTimestamp, MakeWeak(this)),
+ *LatestTimestampUpdatePeriod_);
+ LatestTimestampExecutor_->Start();
+ }
+
+ return result;
+
+}
+
+TFuture<TTimestamp> TTimestampProviderBase::OnGenerateTimestamps(
+ int count,
+ const TErrorOr<TTimestamp>& timestampOrError)
+{
+ if (!timestampOrError.IsOK()) {
+ auto error = TError("Error generating fresh timestamps") << timestampOrError;
+ YT_LOG_ERROR(error);
+ return MakeFuture<TTimestamp>(error);
+ }
+
+ auto firstTimestamp = timestampOrError.Value();
+ auto lastTimestamp = firstTimestamp + count - 1;
+
+ YT_LOG_DEBUG("Fresh timestamps generated (Timestamps: %v-%v)",
+ firstTimestamp,
+ lastTimestamp);
+
+ auto latestTimestamp = LatestTimestamp_.load(std::memory_order::relaxed);
+ while (true) {
+ if (latestTimestamp >= lastTimestamp) {
+ break;
+ }
+ if (LatestTimestamp_.compare_exchange_weak(latestTimestamp, lastTimestamp, std::memory_order::relaxed)) {
+ break;
+ }
+ }
+
+ return MakeFuture<TTimestamp>(firstTimestamp);
+}
+
+void TTimestampProviderBase::UpdateLatestTimestamp()
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ YT_LOG_DEBUG("Updating latest timestamp");
+ GenerateTimestamps(1).Subscribe(
+ BIND([] (const TErrorOr<TTimestamp>& timestampOrError) {
+ if (timestampOrError.IsOK()) {
+ YT_LOG_DEBUG("Latest timestamp updated (Timestamp: %v)",
+ timestampOrError.Value());
+ } else {
+ YT_LOG_WARNING(timestampOrError, "Error updating latest timestamp");
+ }
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
diff --git a/yt/yt/client/transaction_client/timestamp_provider_base.h b/yt/yt/client/transaction_client/timestamp_provider_base.h
new file mode 100644
index 0000000000..cc3168819e
--- /dev/null
+++ b/yt/yt/client/transaction_client/timestamp_provider_base.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "timestamp_provider.h"
+
+#include <yt/yt/core/concurrency/public.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Mostly implements the tracking of the latest timestamp, thus factoring out
+//! common code between the actual implementations.
+class TTimestampProviderBase
+ : public ITimestampProvider
+{
+protected:
+ explicit TTimestampProviderBase(std::optional<TDuration> latestTimestampUpdatePeriod);
+
+ virtual TFuture<TTimestamp> DoGenerateTimestamps(int count) = 0;
+
+public:
+ TFuture<TTimestamp> GenerateTimestamps(int count) override;
+ TTimestamp GetLatestTimestamp() override;
+
+private:
+ const std::optional<TDuration> LatestTimestampUpdatePeriod_;
+
+ std::atomic<i64> GetLatestTimestampCallCounter_ = 0;
+ NConcurrency::TPeriodicExecutorPtr LatestTimestampExecutor_;
+ std::atomic<TTimestamp> LatestTimestamp_ = MinTimestamp;
+
+ TFuture<TTimestamp> OnGenerateTimestamps(
+ int count,
+ const TErrorOr<TTimestamp>& timestampOrError);
+ void UpdateLatestTimestamp();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
+
diff --git a/yt/yt/client/transaction_client/timestamp_service_proxy.h b/yt/yt/client/transaction_client/timestamp_service_proxy.h
new file mode 100644
index 0000000000..070505d2dd
--- /dev/null
+++ b/yt/yt/client/transaction_client/timestamp_service_proxy.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt_proto/yt/client/transaction_client/proto/timestamp_service.pb.h>
+
+#include <yt/yt/core/rpc/client.h>
+
+namespace NYT::NTransactionClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimestampServiceProxy
+ : public NRpc::TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TTimestampServiceProxy, TimestampService);
+
+ DEFINE_RPC_PROXY_METHOD(NProto, GenerateTimestamps,
+ .SetMultiplexingBand(NRpc::EMultiplexingBand::Control));
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTransactionClient
+
diff --git a/yt/yt/client/unittests/check_schema_compatibility_ut.cpp b/yt/yt/client/unittests/check_schema_compatibility_ut.cpp
new file mode 100644
index 0000000000..c1bb4440f5
--- /dev/null
+++ b/yt/yt/client/unittests/check_schema_compatibility_ut.cpp
@@ -0,0 +1,349 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/check_schema_compatibility.h>
+
+namespace NYT::NTableClient {
+
+static void PrintTo(ESchemaCompatibility typeCompatibility, std::ostream* stream)
+{
+ (*stream) << "ESchemaCompatibility::" << ToString(typeCompatibility).c_str();
+}
+
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTableSchemaCompatibilityTest
+ : public ::testing::Test
+{ };
+
+TEST_F(TTableSchemaCompatibilityTest, CheckTableSchemaCompatibilityTest)
+{
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }, /*strict*/ false),
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ TColumnSchema("bar", EValueType::Int64),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Int64),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", EValueType::Uint64),
+ }),
+ /*ignoreSortOrder*/ true ).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", ESimpleLogicalValueType::Int32),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", ESimpleLogicalValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::RequireValidation,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", ESimpleLogicalValueType::Int32),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", ESimpleLogicalValueType::Int8),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::RequireValidation,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ }),
+ TTableSchema({
+ TColumnSchema("foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ // Missing "foo" values are filled with nulls that's OK.
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({}, /*strict*/ true),
+ TTableSchema({
+ TColumnSchema("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ // First table might have column foo with value of any type
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({}, /*strict*/ false),
+ TTableSchema({
+ TColumnSchema("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ // Missing "foo" values are filled with nulls that's OK.
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({}, /*strict*/ true),
+ TTableSchema({
+ TColumnSchema("foo", ESimpleLogicalValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ListLogicalType(SimpleLogicalType((ESimpleLogicalValueType::Int64)))),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64),
+ }, /*strict*/ true),
+ TTableSchema({
+ TColumnSchema("b", ESimpleLogicalValueType::Int64),
+ }, /*strict*/ false),
+ /*ignoreSortOrder*/ true).first
+ );
+
+ //
+ // ignoreSortOrder = false
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ /*ignoreSortOrder*/ false).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64),
+ }),
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ /*ignoreSortOrder*/ false).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64),
+ }),
+ /*ignoreSortOrder*/ false).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }, /*strict*/ true, /*uniqueKeys*/ true),
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64),
+ }, /*strict*/ true, /*uniqueKeys*/ true),
+ /*ignoreSortOrder*/ false).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }, /*strict*/ true, /*uniqueKeys*/ true),
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }, /*strict*/ true, /*uniqueKeys*/ true),
+ /*ignoreSortOrder*/ false).first
+ );
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ TTableSchema({
+ TColumnSchema("b", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ TColumnSchema("a", ESimpleLogicalValueType::Int64, ESortOrder::Ascending),
+ }),
+ /*ignoreSortOrder*/ false).first
+ );
+}
+
+TEST_F(TTableSchemaCompatibilityTest, TestCheckTableSchemaCompatibility)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Ascending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String, ESortOrder::Ascending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Ascending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ false).first);
+
+ EXPECT_EQ(
+ ESchemaCompatibility::FullyCompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ false).first);
+
+ EXPECT_EQ(
+ ESchemaCompatibility::Incompatible,
+ CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("key2", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }),
+ false).first);
+}
+
+TEST_F(TTableSchemaCompatibilityTest, DeletedColumns)
+{
+ auto comp = CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }, true, true, ETableSchemaModification::None,
+ {
+ TDeletedColumn(TStableName("value1")),
+ }),
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ TColumnSchema("value1", ESimpleLogicalValueType::String),
+ }, true, true, {}),
+ false);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, comp.first);
+ EXPECT_EQ("Column \"value1\" in output schema was deleted in the input schema",
+ comp.second.InnerErrors()[0].GetMessage());
+
+
+ comp = CheckTableSchemaCompatibility(
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }, true, true, ETableSchemaModification::None, {}),
+ TTableSchema({
+ TColumnSchema("key1", ESimpleLogicalValueType::String, ESortOrder::Descending),
+ TColumnSchema("value", ESimpleLogicalValueType::String),
+ }, true, true, ETableSchemaModification::None,
+ {
+ TDeletedColumn(TStableName("value1")),
+ }),
+ false);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, comp.first);
+ EXPECT_EQ("Deleted column \"value1\" is missing in the input schema",
+ comp.second.InnerErrors()[0].GetMessage());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/check_type_compatibility_ut.cpp b/yt/yt/client/unittests/check_type_compatibility_ut.cpp
new file mode 100644
index 0000000000..488ee42800
--- /dev/null
+++ b/yt/yt/client/unittests/check_type_compatibility_ut.cpp
@@ -0,0 +1,477 @@
+#include "logical_type_shortcuts.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/complex_types/check_type_compatibility.h>
+
+using namespace NYT::NComplexTypes;
+using namespace NYT::NTableClient::NLogicalTypeShortcuts;
+
+using NYT::NTableClient::ESchemaCompatibility;
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void PrintTo(ESchemaCompatibility typeCompatibility, std::ostream* stream)
+{
+ (*stream) << "ESchemaCompatibility::" << ToString(typeCompatibility).c_str();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
+
+TEST(TCheckTypeCompatibilityTest, SimpleTypes)
+{
+ // Int
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Int8(), Int16()).first);
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Int8(), Int64()).first);
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Int16(), Int64()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Int16(), Int8()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Int64(), Int8()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Int64(), Int32()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Int8(), Uint64()).first);
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Int8(), Double()).first);
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Int8(), String()).first);
+
+ // Uint
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Uint8(), Uint16()).first);
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Uint8(), Uint64()).first);
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Uint16(), Uint64()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Uint16(), Uint8()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Uint64(), Uint8()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Uint64(), Uint32()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Uint8(), Int64()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Uint8(), Double()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Uint8(), String()).first);
+
+ // Float
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Float(), Double()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(Double(), Float()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Float(), String()).first);
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(Double(), Int64()).first);
+
+ // String
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible, CheckTypeCompatibility(Utf8(), String()).first);
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation, CheckTypeCompatibility(String(), Utf8()).first);
+ EXPECT_EQ(ESchemaCompatibility::Incompatible, CheckTypeCompatibility(String(), Json()).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, Optional)
+{
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ Optional(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Optional(Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ Optional(Optional(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Optional(Optional(Int8())),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Optional(Int8()),
+ Optional(Optional(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Optional(Optional(Int8())),
+ Optional(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Optional(Optional(Int8())),
+ Optional(Optional(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Optional(Int8()),
+ Optional(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ Optional(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Int32(),
+ Optional(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Optional(List(Int64())),
+ List(Int64())).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, List)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ List(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ List(Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ List(List(Int8())),
+ List(List(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ List(Int8()),
+ List(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ List(Int8()),
+ List(Optional(Int16()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ List(Int32()),
+ List(Optional(Int16()))).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, Tuple)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ Tuple(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8()),
+ Tuple(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8()),
+ Tuple(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8()),
+ Tuple(Int16(), Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8()),
+ Tuple(Int16(), Int32())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8()),
+ Tuple(Optional(Int16()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Tuple(Int32()),
+ Tuple(Optional(Int16()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8()),
+ Tuple(Int8(), Int8(), Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8()),
+ Tuple(Int8(), Int8(), Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8(), Int8()),
+ Tuple(Int8(), Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8()),
+ Tuple(Int8(), Int8(), Optional(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Tuple(Int8(), Int8(), Optional(Int8())),
+ Tuple(Int8(), Int8())).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, Struct)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ Struct("a", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("b", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Struct("a", Int16()),
+ Struct("a", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int16()),
+ Struct("a", Int8(), "b", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int8(), "b", Optional(Int8()))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int8(), "b", Optional(Optional(Int8())))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int8(), "b", Null())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8()),
+ Struct("a", Int8(), "b", Optional(List(String())))).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8(), "b", Int8()),
+ Struct("a", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Struct("a", Int8(), "b", Optional(Int8())),
+ Struct("a", Int8())).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, VariantTuple)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ VariantTuple(Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int8()),
+ VariantTuple(Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int8(), Uint8()),
+ VariantTuple(Int16(), Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int8(), Uint8()),
+ VariantTuple(Int8(), Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int8(), Uint8()),
+ VariantTuple(Int32(), Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint8()),
+ VariantTuple(Int8(), Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16()),
+ VariantTuple(Int8(), Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16()),
+ VariantTuple(Int16(), Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16()),
+ VariantTuple(Int16(), Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16()),
+ VariantTuple(Int16(), Uint16(), String())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16(), String()),
+ VariantTuple(Int16(), Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantTuple(Int16(), Uint16(), String()),
+ VariantTuple(Int16(), Uint16())).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, VariantStruct)
+{
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Int8(),
+ VariantStruct("a", Int8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int8()),
+ Int8()).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int8()),
+ VariantStruct("a", Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int8(), "b", Uint8()),
+ VariantStruct("a", Int16(), "b", Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int8(), "b", Uint8()),
+ VariantStruct("a", Int8(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int8(), "b", Uint8()),
+ VariantStruct("a", Int32(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint8()),
+ VariantStruct("a", Int8(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16()),
+ VariantStruct("a", Int8(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16()),
+ VariantStruct("a", Int16(), "b", Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16()),
+ VariantStruct("a", Int16(), "b", Uint8())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16()),
+ VariantStruct("a", Int16(), "b", Uint16(), "c", String())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16(), "c", String()),
+ VariantStruct("a", Int16(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16(), "c", String()),
+ VariantStruct("a", Int16(), "b", Uint16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ VariantStruct("a", Int16(), "b", Uint16()),
+ VariantStruct("b", Uint16(), "a", Int16())).first);
+}
+
+TEST(TCheckTypeCompatibilityTest, Dict)
+{
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int16()),
+ Dict(Utf8(), Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int16()),
+ Dict(Utf8(), Int32())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int16()),
+ Dict(String(), Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::FullyCompatible,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int16()),
+ Dict(String(), Int32())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Dict(String(), Int32()),
+ Dict(Utf8(), Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Dict(String(), Int32()),
+ Dict(Utf8(), Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::RequireValidation,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int32()),
+ Dict(String(), Int16())).first);
+
+ EXPECT_EQ(ESchemaCompatibility::Incompatible,
+ CheckTypeCompatibility(
+ Dict(Utf8(), Int32()),
+ Dict(String(), Uint16())).first);
+}
diff --git a/yt/yt/client/unittests/chunk_replica_ut.cpp b/yt/yt/client/unittests/chunk_replica_ut.cpp
new file mode 100644
index 0000000000..a8c19c5aa3
--- /dev/null
+++ b/yt/yt/client/unittests/chunk_replica_ut.cpp
@@ -0,0 +1,47 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/chunk_client/chunk_replica.h>
+
+#include <util/random/random.h>
+
+#include <stdlib.h>
+
+namespace NYT::NChunkClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TChunkReplicaWithMediumTest, Simple)
+{
+ int totalNodes = 10;
+ srand(0);
+ for (int i = 0; i < totalNodes; ++i) {
+ int nodeId = rand() % (1<<23);
+ int replicaIndex = rand() % (1<<4);
+ int mediumIndex = rand() % (1<<7);
+ TChunkReplicaWithMedium replica(nodeId, replicaIndex, mediumIndex);
+
+ EXPECT_THAT(replica.GetNodeId(), nodeId);
+ EXPECT_THAT(replica.GetReplicaIndex(), replicaIndex);
+ EXPECT_THAT(replica.GetMediumIndex(), mediumIndex);
+
+ ui64 value;
+ ToProto(&value, replica);
+
+ TChunkReplicaWithMedium replica1;
+ FromProto(&replica1, value);
+
+ EXPECT_THAT(replica.GetNodeId(), replica1.GetNodeId());
+ EXPECT_THAT(replica.GetReplicaIndex(), replica1.GetReplicaIndex());
+ EXPECT_THAT(replica.GetMediumIndex(), replica1.GetMediumIndex());
+
+ TChunkReplica replica2(replica);
+ EXPECT_THAT(replica.GetNodeId(), replica2.GetNodeId());
+ EXPECT_THAT(replica.GetReplicaIndex(), replica2.GetReplicaIndex());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NChunkClient
diff --git a/yt/yt/client/unittests/column_sort_schema_ut.cpp b/yt/yt/client/unittests/column_sort_schema_ut.cpp
new file mode 100644
index 0000000000..897e4cdcc3
--- /dev/null
+++ b/yt/yt/client/unittests/column_sort_schema_ut.cpp
@@ -0,0 +1,70 @@
+#include <yt/yt/client/table_client/column_sort_schema.h>
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TColumnSortSchemaTest, Serialize)
+{
+ auto test = [] (TString name, ESortOrder sortOrder, TString expected) {
+ TColumnSortSchema schema{
+ .Name = name,
+ .SortOrder = sortOrder
+ };
+ auto serialized = ConvertToYsonString(schema, NYson::EYsonFormat::Text).ToString();
+ EXPECT_EQ(serialized, expected);
+ };
+
+ test("foo", ESortOrder::Ascending, "\"foo\"");
+ test("bar", ESortOrder::Descending, "{\"name\"=\"bar\";\"sort_order\"=\"descending\";}");
+}
+
+TEST(TColumnSortSchemaTest, Deserialize)
+{
+ auto test = [] (TString serialized, TString name, ESortOrder sortOrder) {
+ auto schema = ConvertTo<TColumnSortSchema>(TYsonString(serialized));
+ EXPECT_EQ(schema.Name, name);
+ EXPECT_EQ(schema.SortOrder, sortOrder);
+ };
+
+ test("{\"name\"=\"foo\";\"sort_order\"=\"ascending\"}", "foo", ESortOrder::Ascending);
+ test("{\"name\"=\"bar\";\"sort_order\"=\"descending\"}", "bar", ESortOrder::Descending);
+ test("\"baz\"", "baz", ESortOrder::Ascending);
+ EXPECT_THROW_WITH_SUBSTRING(test("42", "foo", ESortOrder::Ascending), "Unexpected type of column sort schema node");
+}
+
+TEST(TColumnSortSchemaTest, ProtobufConversion)
+{
+ TSortColumns sortColumns;
+ sortColumns.push_back(TColumnSortSchema{
+ .Name = "foo",
+ .SortOrder = ESortOrder::Ascending
+ });
+ sortColumns.push_back(TColumnSortSchema{
+ .Name = "bar",
+ .SortOrder = ESortOrder::Descending
+ });
+
+ NProto::TSortColumnsExt sortColumnsExt;
+ ToProto(&sortColumnsExt, sortColumns);
+
+ TSortColumns parsedSortColumns;
+ FromProto(&parsedSortColumns, sortColumnsExt);
+
+ EXPECT_EQ(sortColumns, parsedSortColumns);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/comparator_ut.cpp b/yt/yt/client/unittests/comparator_ut.cpp
new file mode 100644
index 0000000000..192b303bbe
--- /dev/null
+++ b/yt/yt/client/unittests/comparator_ut.cpp
@@ -0,0 +1,365 @@
+#include "key_helpers.h"
+
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <library/cpp/iterator/zip.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto IntValue1 = MakeUnversionedInt64Value(-7);
+static const auto IntValue2 = MakeUnversionedInt64Value(18);
+static const auto IntValue3 = MakeUnversionedInt64Value(42);
+static const auto StrValue1 = MakeUnversionedStringValue("foo");
+static const auto StrValue2 = MakeUnversionedStringValue("bar");
+static const auto NullValue = MakeUnversionedNullValue();
+static const auto MaxValue = MakeUnversionedSentinelValue(EValueType::Max);
+static const auto MinValue = MakeUnversionedSentinelValue(EValueType::Min);
+
+static const std::vector<TUnversionedValue> AllValues = {
+ IntValue1,
+ IntValue2,
+ StrValue1,
+ StrValue2,
+ NullValue,
+ MaxValue,
+ MinValue,
+};
+
+static const std::vector<TUnversionedValue> NoSentinelValues = {
+ IntValue1,
+ IntValue2,
+ StrValue1,
+ StrValue2,
+ NullValue,
+};
+
+class TComparatorTest
+ : public ::testing::Test
+{
+protected:
+ void InvokeForAllRows(
+ const std::vector<TUnversionedValue>& possibleValues,
+ int minLength,
+ int maxLength,
+ std::function<void(const TUnversionedOwningRow& row)> callback)
+ {
+ std::vector<TUnversionedValue> stack;
+ auto recursiveFill = [&] (int index, auto recursiveFill) {
+ if (index >= minLength && index <= maxLength) {
+ callback(MakeRow(stack));
+ }
+ if (index == maxLength) {
+ return;
+ }
+ for (const auto& value : possibleValues) {
+ stack.push_back(value);
+ recursiveFill(index + 1, recursiveFill);
+ stack.pop_back();
+ }
+ };
+ recursiveFill(0, recursiveFill);
+ }
+
+ std::vector<TKey> GenerateKeys(
+ const std::vector<TUnversionedValue>& possibleValues,
+ int keyLength)
+ {
+ RowStorage_.clear();
+ InvokeForAllRows(possibleValues, keyLength, keyLength, [&] (auto row) { RowStorage_.push_back(row); });
+
+ std::vector<TKey> keys;
+ keys.reserve(RowStorage_.size());
+ for (const auto& row : RowStorage_) {
+ keys.push_back(TKey::FromRow(row));
+ }
+ return keys;
+ }
+
+ std::vector<TComparator> GenerateComparators(int minLength, int maxLength)
+ {
+ std::vector<TComparator> result;
+ std::vector<ESortOrder> stack;
+ auto recursiveFill = [&] (int index, auto recursiveFill) {
+ if (index >= minLength && index <= maxLength) {
+ result.emplace_back(stack);
+ }
+ if (index == maxLength) {
+ return;
+ }
+ for (const auto& value : TEnumTraits<ESortOrder>::GetDomainValues()) {
+ stack.push_back(value);
+ recursiveFill(index + 1, recursiveFill);
+ stack.pop_back();
+ }
+ };
+ recursiveFill(0, recursiveFill);
+ return result;
+ }
+
+private:
+ std::vector<TUnversionedOwningRow> RowStorage_;
+};
+
+TEST_F(TComparatorTest, StressNewAndLegacyTestEquivalence)
+{
+ constexpr int KeyLength = 3;
+ const auto Comparator = MakeComparator(KeyLength);
+ constexpr int LegacyRowLength = 5;
+
+ // Generate all possible keys of length 3.
+ auto allKeys = GenerateKeys(NoSentinelValues, KeyLength);
+
+ auto validateTestPreservation = [&] (const TKeyBound& keyBound, TUnversionedRow legacyRow) {
+ bool isUpper = keyBound.IsUpper;
+ for (const auto& key : allKeys) {
+ auto legacyTest = isUpper ? key.AsOwningRow() < legacyRow : key.AsOwningRow() >= legacyRow;
+ auto newTest = Comparator.TestKey(key, keyBound);
+
+ if (legacyTest != newTest) {
+ Cerr
+ << "Legacy row: " << ToString(legacyRow) << Endl
+ << "Key bound: " << ToString(keyBound) << Endl
+ << "Key: " << ToString(key.AsOwningRow()) << Endl
+ << "LegacyTest: " << legacyTest << Endl
+ << "NewTest: " << newTest << Endl;
+ // Somehow ASSERTTs do not stop execution in our gtest :(
+ THROW_ERROR_EXCEPTION("Failure");
+ }
+ }
+ };
+
+ // Legacy -> New.
+ // Check that all possible legacy bounds of length up to 5 produce
+ // same test result as corresponding key bounds over all keys of length 3.
+
+ auto validateCurrentLegacyRow = [&] (const auto& legacyRow) {
+ for (bool isUpper : {false, true}) {
+ auto keyBound = KeyBoundFromLegacyRow(legacyRow, isUpper, KeyLength);
+ validateTestPreservation(keyBound, legacyRow);
+ }
+ };
+
+ // Test all possible legacy bounds of length up to 5.
+ InvokeForAllRows(AllValues, 0, LegacyRowLength, validateCurrentLegacyRow);
+
+ // New -> Legacy.
+ // Check that all possible key bounds of length up to 3 produce
+ // same test result as corresponding legacy bounds over all keys of length 3.
+
+ auto validateCurrentKeyBound = [&] (const auto& row) {
+ for (bool isUpper : {false, true}) {
+ for (bool isInclusive : {false, true}) {
+ auto keyBound = TOwningKeyBound::FromRow(row, isInclusive, isUpper);
+ auto legacyRow = KeyBoundToLegacyRow(keyBound);
+ validateTestPreservation(keyBound, legacyRow);
+ }
+ }
+ };
+
+ // Test all possible key bounds of length up to 3.
+ InvokeForAllRows(NoSentinelValues, 0, KeyLength, validateCurrentKeyBound);
+}
+
+TEST_F(TComparatorTest, KeyBoundComparisonWellFormedness)
+{
+ auto testComparator = [&] (const TComparator& comparator) {
+ std::vector<TOwningKeyBound> keyBounds;
+ InvokeForAllRows(NoSentinelValues, 0, comparator.GetLength(), [&] (const auto& row) {
+ for (bool isUpper : {false, true}) {
+ for (bool isInclusive : {false, true}) {
+ keyBounds.emplace_back(TOwningKeyBound::FromRow(row, isInclusive, isUpper));
+ }
+ }
+ });
+
+ for (int lowerVsUpperResult : {-1, 0, 1}) {
+ for (const auto& keyBoundA : keyBounds) {
+ // Reflexivity.
+ EXPECT_EQ(0, comparator.CompareKeyBounds(keyBoundA, keyBoundA, lowerVsUpperResult));
+ for (const auto& keyBoundB : keyBounds) {
+ // Antisymmetry.
+ EXPECT_EQ(
+ comparator.CompareKeyBounds(keyBoundA, keyBoundB, lowerVsUpperResult),
+ -comparator.CompareKeyBounds(keyBoundB, keyBoundA, lowerVsUpperResult));
+ for (const auto& keyBoundC : keyBounds) {
+ // Transitivity.
+ int compAB = comparator.CompareKeyBounds(keyBoundA, keyBoundB, lowerVsUpperResult);
+ int compBC = comparator.CompareKeyBounds(keyBoundB, keyBoundC, lowerVsUpperResult);
+ int compAC = comparator.CompareKeyBounds(keyBoundA, keyBoundC, lowerVsUpperResult);
+ if (compAB == -1 && compBC == -1) {
+ EXPECT_EQ(compAC, -1);
+ } else if (compAB <= 0 && compBC <= 0) {
+ EXPECT_LE(compAC, 0);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ constexpr int MaxKeyLength = 2;
+
+ auto comparators = GenerateComparators(1, MaxKeyLength);
+
+ for (const auto& comparator : comparators) {
+ testComparator(comparator);
+ }
+}
+
+TEST_F(TComparatorTest, KeyBoundLowerVsUpperResult)
+{
+ constexpr int KeyLength = 3;
+ const auto Comparator = MakeComparator(KeyLength);
+
+ std::vector<TOwningKeyBound> keyBounds;
+ InvokeForAllRows(NoSentinelValues, 0, KeyLength, [&] (const auto& row) {
+ for (bool isInclusive : {false, true}) {
+ auto upperBound = TOwningKeyBound::FromRow(row, isInclusive, /* isUpper */ true);
+ auto lowerBound = upperBound.Invert();
+ for (int lowerVsUpperResult : {-1, 0, 1}) {
+ EXPECT_EQ(lowerVsUpperResult, Comparator.CompareKeyBounds(lowerBound, upperBound, lowerVsUpperResult));
+ }
+ }
+ });
+}
+
+TEST_F(TComparatorTest, KeyBoundMonotonicity)
+{
+ auto testComparator = [&] (const TComparator& comparator) {;
+ // Generate all possible keys of length 3.
+ auto allKeys = GenerateKeys(NoSentinelValues, comparator.GetLength());
+
+ // And all possible upper key bounds of length up to 3.
+ std::vector<TOwningKeyBound> keyBounds;
+ InvokeForAllRows(NoSentinelValues, 0, comparator.GetLength(), [&] (const auto& row) {
+ for (bool isInclusive : {false, true}) {
+ auto upperBound = TOwningKeyBound::FromRow(row, isInclusive, /* isUpper */ true);
+ keyBounds.emplace_back(std::move(upperBound));
+ }
+ });
+
+ std::sort(keyBounds.begin(), keyBounds.end(), [&] (const auto& lhs, const auto& rhs) {
+ return comparator.CompareKeyBounds(lhs, rhs) < 0;
+ });
+
+ // Check that for any key K, predicate "key bound KB admits K" is monotonic
+ // while iterating with KB over #keyBounds (i.e. is false up to some moment, and
+ // is true after that).
+
+ for (const auto& key : allKeys) {
+ bool previousTestResult = false;
+ for (const auto& keyBound : keyBounds) {
+ auto testResult = comparator.TestKey(key, keyBound);
+ EXPECT_LE(previousTestResult, testResult);
+ previousTestResult = testResult;
+ }
+ }
+ };
+
+ constexpr int MaxKeyLength = 3;
+ for (const auto& comparator : GenerateComparators(1, MaxKeyLength)) {
+ testComparator(comparator);
+ }
+}
+
+TEST_F(TComparatorTest, SortOrder)
+{
+ auto row1 = MakeRow({IntValue1});
+ auto key1 = TKey::FromRow(row1);
+ auto keyBoundLe2 = MakeKeyBound({IntValue2}, /* isInclusive */ true, /* isUpper */ true);
+ auto row3 = MakeRow({IntValue3});
+ auto key3 = TKey::FromRow(row3);
+
+ auto comparatorAscending = TComparator({ESortOrder::Ascending});
+ auto comparatorDescending = TComparator({ESortOrder::Descending});
+
+ EXPECT_LT(comparatorAscending.CompareKeys(key1, key3), 0);
+ EXPECT_GT(comparatorDescending.CompareKeys(key1, key3), 0);
+ EXPECT_TRUE(comparatorAscending.TestKey(key1, keyBoundLe2));
+ EXPECT_FALSE(comparatorAscending.TestKey(key3, keyBoundLe2));
+ EXPECT_FALSE(comparatorDescending.TestKey(key1, keyBoundLe2));
+ EXPECT_TRUE(comparatorDescending.TestKey(key3, keyBoundLe2));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TComparatorTest, CompareWithWidening)
+{
+ auto int1 = MakeUnversionedInt64Value(1);
+ auto int2 = MakeUnversionedInt64Value(2);
+ auto null = MakeUnversionedSentinelValue(EValueType::Null);
+
+ // Equal length.
+ EXPECT_EQ(CompareWithWidening({}, {}), 0);
+ EXPECT_EQ(CompareWithWidening({int1}, {int1}), 0);
+ EXPECT_EQ(CompareWithWidening({int1}, {int2}), -1);
+ EXPECT_EQ(CompareWithWidening({int2}, {int1}), 1);
+ EXPECT_EQ(CompareWithWidening({int1, int2}, {int1, int1}), 2);
+ EXPECT_EQ(CompareWithWidening({int1, int1}, {int1, int2}), -2);
+
+ // Different length.
+ EXPECT_EQ(CompareWithWidening({int1}, {int1, int2}), -2);
+ EXPECT_EQ(CompareWithWidening({int1}, {int1, null}), 0);
+ EXPECT_EQ(CompareWithWidening({int1, null}, {int1, null}), 0);
+ EXPECT_EQ(CompareWithWidening({int1, null}, {int1, null, null}), 0);
+ EXPECT_EQ(CompareWithWidening({int1, null}, {int1, null, int1}), -3);
+
+ // Lower bound shorter bound.
+ EXPECT_FALSE(TestKeyWithWidening({int1, int2}, TKeyBoundRef({int1}, false)));
+ EXPECT_TRUE(TestKeyWithWidening({int1, int2}, TKeyBoundRef({int1}, true)));
+
+ // Upper bound shorter bound.
+ EXPECT_FALSE(TestKeyWithWidening({int1, int2}, TKeyBoundRef({int1}, false, true)));
+ EXPECT_TRUE(TestKeyWithWidening({int1, int2}, TKeyBoundRef({int1}, true, true)));
+
+ // Lower bound equal length.
+ EXPECT_FALSE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int1}, false)));
+ EXPECT_TRUE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int1}, true)));
+
+ EXPECT_FALSE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int2}, false)));
+ EXPECT_FALSE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int2}, true)));
+
+ // Upper bound.
+ EXPECT_TRUE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int2}, false, true)));
+ EXPECT_TRUE(TestKeyWithWidening({int1, int1}, TKeyBoundRef({int1, int2}, true, true)));
+
+ // Lower bound with null and widening.
+ EXPECT_FALSE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, null}, false)));
+ EXPECT_TRUE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, null}, true)));
+
+ // Upper bound with null and widening.
+ EXPECT_FALSE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, null}, false ,true)));
+ EXPECT_TRUE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, null}, true ,true)));
+
+ // Lower bound longer bound.
+ EXPECT_FALSE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, int2}, false)));
+ EXPECT_FALSE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, int2}, true)));
+
+ EXPECT_TRUE(TestKeyWithWidening({int2}, TKeyBoundRef({int1, int2}, false)));
+ EXPECT_TRUE(TestKeyWithWidening({int2}, TKeyBoundRef({int1, int2}, true)));
+
+ // Upper bound longer bound.
+ EXPECT_TRUE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, int2}, false ,true)));
+ EXPECT_TRUE(TestKeyWithWidening({int1}, TKeyBoundRef({int1, int2}, true ,true)));
+
+ EXPECT_FALSE(TestKeyWithWidening({int2}, TKeyBoundRef({int1, int2}, false ,true)));
+ EXPECT_FALSE(TestKeyWithWidening({int2}, TKeyBoundRef({int1, int2}, true ,true)));
+
+ auto sortOrdersAAD = {ESortOrder::Ascending, ESortOrder::Ascending, ESortOrder::Descending};
+
+ EXPECT_FALSE(TestKeyWithWidening({int1, null}, TKeyBoundRef({int1, null, int1})));
+ EXPECT_TRUE(TestKeyWithWidening({int1, null}, TKeyBoundRef({int1, null, int1}), sortOrdersAAD));
+
+ EXPECT_TRUE(TestKeyWithWidening({int1, null}, TKeyBoundRef({int1, null, int1}, false, true)));
+ EXPECT_FALSE(TestKeyWithWidening({int1, null}, TKeyBoundRef({int1, null, int1}, false, true), sortOrdersAAD));
+}
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/composite_compare_ut.cpp b/yt/yt/client/unittests/composite_compare_ut.cpp
new file mode 100644
index 0000000000..81a12bec20
--- /dev/null
+++ b/yt/yt/client/unittests/composite_compare_ut.cpp
@@ -0,0 +1,76 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/composite_compare.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCompositeCompare, Simple)
+{
+ auto compare = [] (TStringBuf lhs, TStringBuf rhs) {
+ return CompareCompositeValues(NYson::TYsonStringBuf(lhs), NYson::TYsonStringBuf(rhs));
+ };
+
+ EXPECT_EQ(-1, compare("-4", "42"));
+ EXPECT_EQ(1, compare("42", "-4"));
+ EXPECT_EQ(0, compare("-4", "-4"));
+
+ EXPECT_EQ(-1, compare("4u", "42u"));
+ EXPECT_EQ(1, compare("42u", "4u"));
+ EXPECT_EQ(0, compare("4u", "4u"));
+
+ EXPECT_EQ(-1, compare("4.0", "4.2"));
+ EXPECT_EQ(1, compare("4.2", "4.0"));
+ EXPECT_EQ(0, compare("4.0", "4.0"));
+
+ EXPECT_EQ(1, compare("%nan ", "-5.0"));
+ EXPECT_EQ(-1, compare("-5.0", "%nan "));
+ EXPECT_EQ(0, compare("%nan ", "%nan "));
+
+ EXPECT_EQ(-1, compare("%false", "%true"));
+ EXPECT_EQ(1, compare("%true", "%false"));
+ EXPECT_EQ(0, compare("%true", "%true"));
+
+ EXPECT_EQ(1, compare("foo", "bar"));
+ EXPECT_EQ(-1, compare("foo", "fooo"));
+ EXPECT_EQ(0, compare("foo", "foo"));
+
+ // Tuple<Int64,Int64> or List<Int64>
+ EXPECT_EQ(-1, compare("[1; 2]", "[1; 3]"));
+ EXPECT_EQ(1, compare("[1; 3]", "[1; 2]"));
+
+ // List<Int64>
+ EXPECT_EQ(1, compare("[1; 2; 3]", "[1; 2]"));
+ EXPECT_EQ(-1, compare("[1; 2]", "[1; 2; 3]"));
+ EXPECT_EQ(0, compare("[1; 2; 3]", "[1; 2; 3]"));
+
+ // List<Optional<Int64>>
+ EXPECT_EQ(1, compare("[1; 2; #]", "[1; 2]"));
+ EXPECT_EQ(-1, compare("[1; 2]", "[1; 2; #]"));
+ EXPECT_EQ(-1, compare("[1; 2; #]", "[1; 2; 3]"));
+ EXPECT_EQ(1, compare("[1; 2; 3]", "[1; 2; #]"));
+}
+
+TEST(TCompositeCompare, CompositeFingerprint)
+{
+ auto getFarmHash = [] (TStringBuf value) {
+ return CompositeFarmHash(NYson::TYsonStringBuf(value));
+ };
+
+ EXPECT_EQ(getFarmHash("-42"), GetFarmFingerprint(MakeUnversionedInt64Value(-42)));
+ EXPECT_EQ(getFarmHash("100500u"), GetFarmFingerprint(MakeUnversionedUint64Value(100500)));
+ EXPECT_EQ(getFarmHash("3.25"), GetFarmFingerprint(MakeUnversionedDoubleValue(3.25)));
+ EXPECT_EQ(getFarmHash("%true"), GetFarmFingerprint(MakeUnversionedBooleanValue(true)));
+ EXPECT_EQ(getFarmHash("%false"), GetFarmFingerprint(MakeUnversionedBooleanValue(false)));
+ EXPECT_EQ(getFarmHash("#"), GetFarmFingerprint(MakeUnversionedNullValue()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/connection_ut.cpp b/yt/yt/client/unittests/connection_ut.cpp
new file mode 100644
index 0000000000..303733964d
--- /dev/null
+++ b/yt/yt/client/unittests/connection_ut.cpp
@@ -0,0 +1,48 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/api/rpc_proxy/private.h>
+
+namespace NYT::NApi::NRpcProxy {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProxyUrlTest
+ : public ::testing::Test
+{ };
+
+TEST_F(TProxyUrlTest, ParseProxyUrlAliasingRules)
+{
+ auto emptyRules = ParseProxyUrlAliasingRules("");
+ ASSERT_EQ(emptyRules.size(), 0u);
+ auto notEmptyRules = ParseProxyUrlAliasingRules(R"({primary="localhost:12345"})");
+ ASSERT_EQ(notEmptyRules.size(), 1u);
+ ASSERT_EQ(notEmptyRules.at("primary"), "localhost:12345");
+}
+
+TEST_F(TProxyUrlTest, ApplyProxyUrlAliasingRules)
+{
+ {
+ TString url = "markov";
+ ApplyProxyUrlAliasingRules(url);
+ ASSERT_EQ(url, "markov");
+ }
+ // See ENV in ya.make
+ {
+ TString url = "primary";
+ ApplyProxyUrlAliasingRules(url);
+ ASSERT_EQ(url, "localhost:12345");
+ }
+}
+
+TEST_F(TProxyUrlTest, NormalizeHttpProxyUrl)
+{
+ ASSERT_EQ(NormalizeHttpProxyUrl("markov"), "http://markov.yt.yandex.net");
+ // See ENV in ya.make
+ ASSERT_EQ(NormalizeHttpProxyUrl("primary"), "http://localhost:12345");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NApi::NRpcProxy
diff --git a/yt/yt/client/unittests/dsv_parser_ut.cpp b/yt/yt/client/unittests/dsv_parser_ut.cpp
new file mode 100644
index 0000000000..0a0c724f9e
--- /dev/null
+++ b/yt/yt/client/unittests/dsv_parser_ut.cpp
@@ -0,0 +1,365 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/client/formats/dsv_parser.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NYson;
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDsvParserTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("integer"));
+ EXPECT_CALL(Mock, OnStringScalar("42"));
+ EXPECT_CALL(Mock, OnKeyedItem("string"));
+ EXPECT_CALL(Mock, OnStringScalar("some"));
+ EXPECT_CALL(Mock, OnKeyedItem("double"));
+ EXPECT_CALL(Mock, OnStringScalar("10"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnKeyedItem("one"));
+ EXPECT_CALL(Mock, OnStringScalar("1"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "integer=42\tstring=some\tdouble=10\n"
+ "foo=bar\tone=1\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, EmptyInput)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ TString input = "";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, BinaryData)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ auto a = TString("\0\0\0\0", 4);
+ auto b = TString("\x80\0\x16\xC8", 4);
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("ntr"));
+ EXPECT_CALL(Mock, OnStringScalar(a));
+ EXPECT_CALL(Mock, OnKeyedItem("xrp"));
+ EXPECT_CALL(Mock, OnStringScalar(b));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "ntr=\\0\\0\\0\\0\txrp=\x80\\0\x16\xC8\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, EmptyRecord)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, EmptyRecords)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "\n\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, EmptyKeysAndValues)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem(""));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "=\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, UnescapedZeroInInput)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ TString input = TString("a\0b=v", 5);
+ EXPECT_ANY_THROW(
+ ParseDsv(input, &Mock);
+ );
+}
+
+TEST(TDsvParserTest, ZerosAreNotTerminals)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ TString key = TString("a\0b", 3);
+ TString value = TString("c\0d", 3);
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem(key));
+ EXPECT_CALL(Mock, OnStringScalar(value));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "a\\0b=c\\0d\n";
+ ParseDsv(input, &Mock);
+}
+
+TEST(TDsvParserTest, UnterminatedRecord)
+{
+ NiceMock<TMockYsonConsumer> Mock;
+
+ TString input = "a=b";
+ EXPECT_ANY_THROW(
+ ParseDsv(input, &Mock);
+ );
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTskvParserTest: public ::testing::Test
+{
+public:
+ StrictMock<TMockYsonConsumer> Mock;
+ NiceMock<TMockYsonConsumer> ErrorMock;
+
+ TDsvFormatConfigPtr Config;
+
+ void SetUp() override {
+ Config = New<TDsvFormatConfig>();
+ Config->LinePrefix = "tskv";
+ }
+};
+
+TEST_F(TTskvParserTest, Simple)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("id"));
+ EXPECT_CALL(Mock, OnStringScalar("1"));
+ EXPECT_CALL(Mock, OnKeyedItem("guid"));
+ EXPECT_CALL(Mock, OnStringScalar("100500"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("id"));
+ EXPECT_CALL(Mock, OnStringScalar("2"));
+ EXPECT_CALL(Mock, OnKeyedItem("guid"));
+ EXPECT_CALL(Mock, OnStringScalar("20025"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "tskv\n"
+ "tskv\tid=1\tguid=100500\t\n"
+ "tskv\tid=2\tguid=20025\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, SimpleWithNewLine)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "tskv\tfoo=bar\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, Escaping)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a=b"));
+ EXPECT_CALL(Mock, OnStringScalar("c=d or e=f"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key_with_\t,\r_and_\n"));
+ EXPECT_CALL(Mock, OnStringScalar("value_with_\t,\\_and_\r\n"));
+ EXPECT_CALL(Mock, OnKeyedItem("another_key"));
+ EXPECT_CALL(Mock, OnStringScalar("another_value"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "t\\s\\kv\n"
+ "tskv" "\t" "a\\=b" "=" "c\\=d or e=f" "\n" // Note: unescaping is less strict
+ "tskv" "\t"
+ "key_with_\\t,\r_and_\\n"
+ "="
+ "value_with_\\t,\\\\_and_\\r\\n"
+ "\t"
+ "an\\other_\\key=anoth\\er_v\\alue"
+ "\n";
+
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, DisabledEscaping)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a\\"));
+ EXPECT_CALL(Mock, OnStringScalar("b\\t=c\\=d or e=f\\0"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "tskv\t\\x\\y\n"
+ "tskv" "\t" "a\\=b\\t" "=" "c\\=d or e=f\\0" "\n";
+
+ Config->EnableEscaping = false;
+
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, AllowedUnescapedSymbols)
+{
+ Config->LinePrefix = "prefix_with_=";
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("just_key"));
+ EXPECT_CALL(Mock, OnStringScalar("value_with_="));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "prefix_with_=" "\t" "just_key" "=" "value_with_=" "\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, UndefinedValues)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("b"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "tskv" "\t" "tskv" "\t" "tskv" "\n"
+ "tskv\t" "some_key" "\t\t\t" "a=b" "\t" "another_key" "\n" // Note: consequent \t
+ "tskv\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+
+TEST_F(TTskvParserTest, OnlyLinePrefix)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "tskv\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, OnlyLinePrefixAndTab)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "tskv\t\n";
+ ParseDsv(input, &Mock, Config);
+}
+
+TEST_F(TTskvParserTest, NotFinishedLinePrefix)
+{
+ TString input = "tsk";
+
+ EXPECT_ANY_THROW(
+ ParseDsv(input, &ErrorMock, Config)
+ );
+}
+
+TEST_F(TTskvParserTest, WrongLinePrefix)
+{
+ TString input =
+ "tskv\ta=b\n"
+ "tZkv\tc=d\te=f\n"
+ "tskv\ta=b\n";
+
+ EXPECT_ANY_THROW(
+ ParseDsv(input, &ErrorMock, Config);
+ );
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NDriver
diff --git a/yt/yt/client/unittests/dsv_writer_ut.cpp b/yt/yt/client/unittests/dsv_writer_ut.cpp
new file mode 100644
index 0000000000..b5f96caacd
--- /dev/null
+++ b/yt/yt/client/unittests/dsv_writer_ut.cpp
@@ -0,0 +1,316 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/formats/dsv_parser.h>
+#include <yt/yt/client/formats/dsv_writer.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDsvWriterTest, StringScalar)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnStringScalar("0-2-xb-1234");
+ EXPECT_EQ("0-2-xb-1234", outputStream.Str());
+}
+
+TEST(TDsvWriterTest, ListContainingDifferentTypes)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnBeginList();
+ consumer.OnListItem();
+ consumer.OnInt64Scalar(100);
+ consumer.OnListItem();
+ consumer.OnStringScalar("foo");
+ consumer.OnListItem();
+ consumer.OnListItem();
+ consumer.OnBeginMap();
+ consumer.OnKeyedItem("a");
+ consumer.OnStringScalar("10");
+ consumer.OnKeyedItem("b");
+ consumer.OnStringScalar("c");
+ consumer.OnEndMap();
+ consumer.OnEndList();
+
+ TString output =
+ "100\n"
+ "foo\n"
+ "\n"
+ "a=10\tb=c\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TDsvWriterTest, ListInsideList)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnBeginList();
+ consumer.OnListItem();
+ EXPECT_ANY_THROW(consumer.OnBeginList());
+}
+
+TEST(TDsvWriterTest, ListInsideMap)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnBeginMap();
+ consumer.OnKeyedItem("foo");
+ EXPECT_ANY_THROW(consumer.OnBeginList());
+}
+
+TEST(TDsvWriterTest, MapInsideMap)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnBeginMap();
+ consumer.OnKeyedItem("foo");
+ EXPECT_ANY_THROW(consumer.OnBeginMap());
+}
+
+TEST(TDsvWriterTest, WithoutEsacping)
+{
+ auto config = New<TDsvFormatConfig>();
+ config->EnableEscaping = false;
+
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream, config);
+
+ consumer.OnStringScalar("string_with_\t_\\_=_and_\n");
+
+ TString output = "string_with_\t_\\_=_and_\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TDsvWriterTest, ListUsingOnRaw)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnRaw("[10; 20; 30]", EYsonType::Node);
+ TString output =
+ "10\n"
+ "20\n"
+ "30\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TDsvWriterTest, MapUsingOnRaw)
+{
+ TStringStream outputStream;
+ TDsvNodeConsumer consumer(&outputStream);
+
+ consumer.OnRaw("{a=b; c=d}", EYsonType::Node);
+ TString output = "a=b\tc=d";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDsvWriterTest, SimpleTabular)
+{
+ auto nameTable = New<TNameTable>();
+ auto integerId = nameTable->RegisterName("integer");
+ auto stringId = nameTable->RegisterName("string");
+ auto doubleId = nameTable->RegisterName("double");
+ auto fooId = nameTable->RegisterName("foo");
+ auto oneId = nameTable->RegisterName("one");
+ auto tableIndexId = nameTable->RegisterName(TableIndexColumnName);
+ auto rowIndexId = nameTable->RegisterName(RowIndexColumnName);
+ auto rangeIndexId = nameTable->RegisterName(RangeIndexColumnName);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedInt64Value(42, integerId));
+ row1.AddValue(MakeUnversionedStringValue("some", stringId));
+ row1.AddValue(MakeUnversionedDoubleValue(10., doubleId));
+ row1.AddValue(MakeUnversionedInt64Value(2, tableIndexId));
+ row1.AddValue(MakeUnversionedInt64Value(42, rowIndexId));
+ row1.AddValue(MakeUnversionedInt64Value(1, rangeIndexId));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("bar", fooId));
+ row2.AddValue(MakeUnversionedSentinelValue(EValueType::Null, integerId));
+ row2.AddValue(MakeUnversionedInt64Value(1, oneId));
+ row2.AddValue(MakeUnversionedInt64Value(2, tableIndexId));
+ row2.AddValue(MakeUnversionedInt64Value(43, rowIndexId));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow()};
+
+ TStringStream outputStream;
+ auto config = New<TDsvFormatConfig>();
+ config->EnableTableIndex = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableTableIndex = true;
+ auto writer = CreateSchemalessWriterForDsv(
+ config,
+ nameTable,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&outputStream)),
+ false,
+ controlAttributes,
+ 0);
+
+ EXPECT_EQ(true, writer->Write(rows));
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "integer=42\tstring=some\tdouble=10.\t@table_index=2\n"
+ "foo=bar\tone=1\t@table_index=2\n";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TDsvWriterTest, AnyTabular)
+{
+ auto nameTable = New<TNameTable>();
+ auto anyId = nameTable->RegisterName("any");
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedAnyValue("[]", anyId));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ TStringStream outputStream;
+ auto controlAttributes = New<TControlAttributesConfig>();
+ auto writer = CreateSchemalessWriterForDsv(
+ New<TDsvFormatConfig>(),
+ nameTable,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&outputStream)),
+ false,
+ controlAttributes,
+ 0);
+
+ EXPECT_FALSE(writer->Write(rows));
+ EXPECT_ANY_THROW(writer->GetReadyEvent().Get().ThrowOnError());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTskvWriterTest, SimpleTabular)
+{
+ auto nameTable = New<TNameTable>();
+ auto id1 = nameTable->RegisterName("id");
+ auto id2 = nameTable->RegisterName("guid");
+ auto tableIndexId = nameTable->RegisterName(TableIndexColumnName);
+ auto rowIndexId = nameTable->RegisterName(RowIndexColumnName);
+ auto rangeIndexId = nameTable->RegisterName(RangeIndexColumnName);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedInt64Value(2, tableIndexId));
+ row1.AddValue(MakeUnversionedInt64Value(42, rowIndexId));
+ row1.AddValue(MakeUnversionedInt64Value(1, rangeIndexId));
+
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("1", id1));
+ row2.AddValue(MakeUnversionedInt64Value(100500, id2));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("2", id1));
+ row3.AddValue(MakeUnversionedInt64Value(20025, id2));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow(), row3.GetRow() };
+
+ TStringStream outputStream;
+ auto config = New<TDsvFormatConfig>();
+ config->LinePrefix = "tskv";
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ auto writer = CreateSchemalessWriterForDsv(
+ config,
+ nameTable,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&outputStream)),
+ false,
+ controlAttributes,
+ 0);
+
+ EXPECT_EQ(true, writer->Write(rows));
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "tskv\n"
+ "tskv\tid=1\tguid=100500\n"
+ "tskv\tid=2\tguid=20025\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TTskvWriterTest, Escaping)
+{
+ auto key1 = TString("\0 is escaped", 12);
+
+ auto nameTable = New<TNameTable>();
+ auto id1 = nameTable->RegisterName(key1);
+ auto id2 = nameTable->RegisterName("Escaping in in key: \r \t \n \\ =");
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue(key1, id1));
+ row.AddValue(MakeUnversionedStringValue("Escaping in value: \r \t \n \\ =", id2));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ TStringStream outputStream;
+ auto config = New<TDsvFormatConfig>();
+ config->LinePrefix = "tskv";
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ auto writer = CreateSchemalessWriterForDsv(
+ config,
+ nameTable,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&outputStream)),
+ false,
+ controlAttributes,
+ 0);
+
+ EXPECT_EQ(true, writer->Write(rows));
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "tskv"
+ "\t"
+
+ "\\0 is escaped"
+ "="
+ "\\0 is escaped"
+
+ "\t"
+
+ "Escaping in in key: \\r \\t \\n \\\\ \\="
+ "="
+ "Escaping in value: \\r \\t \\n \\\\ =" // Note: = is not escaped
+
+ "\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/farm_fingerprint_stability_ut.cpp b/yt/yt/client/unittests/farm_fingerprint_stability_ut.cpp
new file mode 100644
index 0000000000..1d01e1eef6
--- /dev/null
+++ b/yt/yt/client/unittests/farm_fingerprint_stability_ut.cpp
@@ -0,0 +1,99 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/unversioned_value.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+/////////////////////////////////////////////////////////////////////////////
+
+// The goal is to provide a sanity check for stability of FarmHash and FarmFingerprint functions
+// for TUnversionedRow and TUnversionedValue.
+class TFarmHashTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<
+ std::tuple<TUnversionedValue, TUnversionedValue, ui64, ui64, ui64>>
+{ };
+
+TEST_P(TFarmHashTest, TFarmHashUnversionedValueTest)
+{
+ const auto& params = GetParam();
+
+ std::array<TUnversionedValue, 2> values{std::get<0>(params), std::get<1>(params)};
+ auto valueRange = MakeRange(values);
+
+ auto expected1 = std::get<2>(params);
+ auto expected2 = std::get<3>(params);
+ auto expected3 = std::get<4>(params);
+
+ static_assert(std::is_same_v<ui64, decltype(TDefaultUnversionedValueHash()(values[0]))>);
+ EXPECT_EQ(expected1, TDefaultUnversionedValueHash()(values[0]));
+ EXPECT_EQ(expected2, TDefaultUnversionedValueHash()(values[1]));
+
+ static_assert(std::is_same_v<ui64, decltype(GetFarmFingerprint(values[0]))>);
+ EXPECT_EQ(expected1, GetFarmFingerprint(values[0]));
+ EXPECT_EQ(expected2, GetFarmFingerprint(values[1]));
+
+ static_assert(std::is_same_v<ui64, decltype(GetFarmFingerprint(valueRange))>);
+ EXPECT_EQ(expected3, GetFarmFingerprint(valueRange));
+}
+
+TEST_P(TFarmHashTest, TFarmHashUnversionedRowTest)
+{
+ const auto& params = GetParam();
+
+ char buf[sizeof(TUnversionedRowHeader) + 2 * sizeof(TUnversionedValue)];
+ *reinterpret_cast<TUnversionedRowHeader*>(buf) = TUnversionedRowHeader{2, 2};
+ *reinterpret_cast<TUnversionedValue*>(buf + sizeof(TUnversionedRowHeader)) = std::get<0>(params);
+ *reinterpret_cast<TUnversionedValue*>(buf + sizeof(TUnversionedRowHeader) + sizeof(TUnversionedValue)) = std::get<1>(params);
+ TUnversionedRow row(reinterpret_cast<TUnversionedRowHeader*>(buf));
+
+ auto expected = std::get<4>(params);
+
+ static_assert(std::is_same_v<ui64, decltype(TDefaultUnversionedRowHash()(row))>);
+ EXPECT_EQ(expected, TDefaultUnversionedRowHash()(row));
+
+ static_assert(std::is_same_v<ui64, decltype(GetFarmFingerprint(row))>);
+ EXPECT_EQ(expected, GetFarmFingerprint(row));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TFarmHashTest,
+ TFarmHashTest,
+ ::testing::Values(
+ std::make_tuple(
+ MakeUnversionedInt64Value(12345678, /*id*/ 0, EValueFlags::None),
+ MakeUnversionedUint64Value(42, /*id*/ 1, EValueFlags::Aggregate),
+ 18329046069279503950ULL,
+ 17355217915646310598ULL,
+ 16453323425893019626ULL),
+ std::make_tuple(
+ MakeUnversionedUint64Value(12345678, /*id*/ 1, EValueFlags::Aggregate),
+ MakeUnversionedBooleanValue(true, /*id*/ 2, EValueFlags::Aggregate),
+ 18329046069279503950ULL,
+ 10105606910506535461ULL,
+ 10502610411105654667ULL),
+ std::make_tuple(
+ MakeUnversionedDoubleValue(42.0, /*id*/ 2, EValueFlags::Aggregate),
+ MakeUnversionedStringValue("0", /*id*/ 3, EValueFlags::None),
+ 6259286942292166412ULL,
+ 15198969275252572735ULL,
+ 12125805494429148155ULL),
+ std::make_tuple(
+ MakeUnversionedBooleanValue(false, /*id*/ 3, EValueFlags::Aggregate),
+ MakeUnversionedStringValue("", /*id*/ 4, EValueFlags::None),
+ 0ULL,
+ 11160318154034397263ULL,
+ 10248854568006048452ULL),
+ std::make_tuple(
+ MakeUnversionedStringValue("abc", /*id*/ 4, EValueFlags::None),
+ MakeUnversionedInt64Value(-1000000, /*id*/ 5, EValueFlags::None),
+ 2640714258260161385ULL,
+ 13952380479379003069ULL,
+ 9998489714118868374ULL)));
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/format_writer_ut.h b/yt/yt/client/unittests/format_writer_ut.h
new file mode 100644
index 0000000000..4680090755
--- /dev/null
+++ b/yt/yt/client/unittests/format_writer_ut.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <yt/yt/client/formats/format.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+namespace NYT::NFormats {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestNameTableExpansion(ISchemalessFormatWriterPtr writer, NTableClient::TNameTablePtr nameTable)
+{
+ // We write five rows, on each iteration we double number of
+ // columns in the NameTable.
+ for (int iteration = 0; iteration < 5; ++iteration) {
+ NTableClient::TUnversionedOwningRowBuilder row;
+ for (int index = 0; index < (1 << iteration); ++index) {
+ auto key = "Column" + ToString(index);
+ auto value = "Value" + ToString(index);
+ int columnId = nameTable->GetIdOrRegisterName(key);
+ row.AddValue(NTableClient::MakeUnversionedStringValue(value, columnId));
+ }
+ auto completeRow = row.FinishRow();
+ EXPECT_EQ(true, writer->Write({completeRow.Get()}));
+ }
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/key_bound_compressor_ut.cpp b/yt/yt/client/unittests/key_bound_compressor_ut.cpp
new file mode 100644
index 0000000000..8add67880f
--- /dev/null
+++ b/yt/yt/client/unittests/key_bound_compressor_ut.cpp
@@ -0,0 +1,88 @@
+#include "key_helpers.h"
+
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/key_bound_compressor.h>
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/iterator/zip.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TKeyBoundCompressorTest, Simple)
+{
+ TRowBufferPtr rowBuffer = New<TRowBuffer>();
+ auto preimageRow = [&] (std::optional<TStringBuf> first = std::nullopt, std::optional<int> second = std::nullopt) {
+ TUnversionedRowBuilder builder;
+ if (first) {
+ builder.AddValue(MakeUnversionedStringValue(*first, 0));
+ }
+ if (second) {
+ builder.AddValue(MakeUnversionedInt64Value(*second, 1));
+ }
+ return rowBuffer->CaptureRow(builder.GetRow());
+ };
+
+ auto imageRow = [&] (std::optional<int> first = std::nullopt, std::optional<int> second = std::nullopt) {
+ TUnversionedRowBuilder builder;
+ if (first) {
+ builder.AddValue(MakeUnversionedInt64Value(*first, 0));
+ }
+ if (second) {
+ builder.AddValue(MakeUnversionedInt64Value(*second, 1));
+ }
+ return rowBuffer->CaptureRow(builder.GetRow());
+ };
+
+ // See comment in key_bound_compressor.h for the readable version of the list below.
+ std::vector<std::tuple<TKeyBound, TKeyBound, int>> tuples{
+ {TKeyBound::FromRow() >= preimageRow( ), TKeyBound::FromRow() >= imageRow( ), 0},
+ {TKeyBound::FromRow() >= preimageRow("bar" ), TKeyBound::FromRow() >= imageRow(0 ), 1},
+ {TKeyBound::FromRow() >= preimageRow("bar", 42), TKeyBound::FromRow() >= imageRow(0, 0), 2},
+ {TKeyBound::FromRow() <= preimageRow("bar", 57), TKeyBound::FromRow() <= imageRow(0, 1), 3},
+ {TKeyBound::FromRow() <= preimageRow("bar" ), TKeyBound::FromRow() <= imageRow(0 ), 4},
+ {TKeyBound::FromRow() < preimageRow("foo" ), TKeyBound::FromRow() < imageRow(1 ), 5},
+ {TKeyBound::FromRow() >= preimageRow("foo" ), TKeyBound::FromRow() >= imageRow(1 ), 5},
+ {TKeyBound::FromRow() > preimageRow("foo", 30), TKeyBound::FromRow() > imageRow(1, 0), 6},
+ {TKeyBound::FromRow() < preimageRow("foo", 99), TKeyBound::FromRow() < imageRow(1, 1), 7},
+ {TKeyBound::FromRow() <= preimageRow("foo", 99), TKeyBound::FromRow() <= imageRow(1, 1), 7},
+ {TKeyBound::FromRow() > preimageRow("foo", 99), TKeyBound::FromRow() > imageRow(1, 1), 7},
+ {TKeyBound::FromRow() < preimageRow("qux", 18), TKeyBound::FromRow() < imageRow(2, 0), 8},
+ {TKeyBound::FromRow() < preimageRow("zzz" ), TKeyBound::FromRow() < imageRow(3 ), 9},
+ {TKeyBound::FromRow() <= preimageRow( ), TKeyBound::FromRow() <= imageRow( ), 10},
+ {TKeyBound::FromRow() > preimageRow( ), TKeyBound::FromRow() > imageRow( ), 10},
+ };
+
+ TComparator comparator(std::vector<ESortOrder>{ESortOrder::Ascending, ESortOrder::Ascending});
+ TKeyBoundCompressor compressor(comparator);
+
+ for (const auto& [key, _1, _2] : tuples) {
+ compressor.Add(key);
+ }
+
+ compressor.InitializeMapping();
+
+ for (const auto& [key, componentWise, total] : tuples) {
+ auto [actualComponentWise, actualTotal] = compressor.GetImage(key);
+ TKeyBound qwe = actualComponentWise;
+ (void)qwe;
+ EXPECT_EQ(componentWise, actualComponentWise)
+ << "For key " << ToString(key);
+ EXPECT_EQ(total, actualTotal)
+ << "For key " << ToString(key);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/key_bound_ut.cpp b/yt/yt/client/unittests/key_bound_ut.cpp
new file mode 100644
index 0000000000..c92be63a2f
--- /dev/null
+++ b/yt/yt/client/unittests/key_bound_ut.cpp
@@ -0,0 +1,258 @@
+#include "key_helpers.h"
+
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/iterator/zip.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TKeyBoundTest, Simple)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedDoubleValue(3.14, 0));
+ builder.AddValue(MakeUnversionedInt64Value(-42, 1));
+ builder.AddValue(MakeUnversionedUint64Value(27, 2));
+ TString str = "Foo";
+ builder.AddValue(MakeUnversionedStringValue(str, 3));
+
+ auto owningRow = builder.FinishRow();
+ // Builder captures string, so this address is different from str.data().
+ auto* strPtr = owningRow[3].Data.String;
+
+ auto row = owningRow;
+ auto rowBeginPtr = row.Begin();
+ {
+ auto keyBound = TKeyBound::FromRow(row, /* isInclusive */ false, /* isUpper */ false);
+ EXPECT_EQ(row, keyBound.Prefix);
+ EXPECT_EQ(rowBeginPtr, keyBound.Prefix.Begin());
+ }
+ {
+ // Steal row.
+ auto stolenKeyBound = TKeyBound::FromRow(std::move(row), /* isInclusive */ false, /* isUpper */ false);
+ EXPECT_EQ(owningRow, stolenKeyBound.Prefix);
+ EXPECT_EQ(rowBeginPtr, stolenKeyBound.Prefix.Begin());
+ }
+ {
+ auto owningKeyBound = TOwningKeyBound::FromRow(owningRow, /* isInclusive */ false, /* isUpper */ false);
+ EXPECT_EQ(owningRow, owningKeyBound.Prefix);
+ }
+ {
+ // Steal owningRow.
+ auto stolenOwningKeyBound = TOwningKeyBound::FromRow(std::move(owningRow), /* isInclusive */ false, /* isUpper */ false);
+ EXPECT_EQ(EValueType::String, stolenOwningKeyBound.Prefix[3].Type);
+ EXPECT_EQ(strPtr, stolenOwningKeyBound.Prefix[3].Data.String);
+ }
+}
+
+TEST(TKeyBound, Helper)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedDoubleValue(3.14, 0));
+ builder.AddValue(MakeUnversionedInt64Value(-42, 1));
+ builder.AddValue(MakeUnversionedUint64Value(27, 2));
+ TString str = "Foo";
+ builder.AddValue(MakeUnversionedStringValue(str, 3));
+
+ auto owningRow = builder.FinishRow();
+
+ auto explicitGT = TOwningKeyBound::FromRow(owningRow, /* isInclusive */ false, /* isUpper */ false);
+ auto helperGT = TOwningKeyBound::FromRow() > owningRow;
+ auto helperGTUnchecked = TOwningKeyBound::FromRowUnchecked() > owningRow;
+ EXPECT_EQ(explicitGT, helperGT);
+ EXPECT_EQ(explicitGT, helperGTUnchecked);
+
+ auto explicitGE = TOwningKeyBound::FromRow(owningRow, /* isInclusive */ true, /* isUpper */ false);
+ auto helperGE = TOwningKeyBound::FromRow() >= owningRow;
+ auto helperGEUnchecked = TOwningKeyBound::FromRowUnchecked() >= owningRow;
+ EXPECT_EQ(explicitGE, helperGE);
+ EXPECT_EQ(explicitGE, helperGEUnchecked);
+
+ auto explicitLT = TOwningKeyBound::FromRow(owningRow, /* isInclusive */ false, /* isUpper */ true);
+ auto helperLT = TOwningKeyBound::FromRow() < owningRow;
+ auto helperLTUnchecked = TOwningKeyBound::FromRowUnchecked() < owningRow;
+ EXPECT_EQ(explicitLT, helperLT);
+ EXPECT_EQ(explicitLT, helperLTUnchecked);
+
+ auto explicitLE = TOwningKeyBound::FromRow(owningRow, /* isInclusive */ true, /* isUpper */ true);
+ auto helperLE = TOwningKeyBound::FromRow() <= owningRow;
+ auto helperLEUnchecked = TOwningKeyBound::FromRowUnchecked() <= owningRow;
+ EXPECT_EQ(explicitLE, helperLE);
+ EXPECT_EQ(explicitLE, helperLEUnchecked);
+}
+
+TEST(TKeyBoundTest, KeyBoundToLegacyRow)
+{
+ auto intValue = MakeUnversionedInt64Value(42);
+ auto maxValue = MakeUnversionedSentinelValue(EValueType::Max);
+
+ std::vector<TOwningKeyBound> keyBounds = {
+ MakeKeyBound({intValue}, /* isInclusive */ false, /* isUpper */ false),
+ MakeKeyBound({intValue}, /* isInclusive */ false, /* isUpper */ true),
+ MakeKeyBound({intValue}, /* isInclusive */ true, /* isUpper */ false),
+ MakeKeyBound({intValue}, /* isInclusive */ true, /* isUpper */ true),
+ };
+
+ auto expectedLegacyRows = {
+ MakeRow({intValue, maxValue}),
+ MakeRow({intValue}),
+ MakeRow({intValue}),
+ MakeRow({intValue, maxValue}),
+ };
+
+ for (const auto& [keyBound, legacyRow] : Zip(keyBounds, expectedLegacyRows)) {
+ EXPECT_EQ(KeyBoundToLegacyRow(keyBound), legacyRow);
+ }
+}
+
+TEST(TKeyBoundTest, KeyBoundFromLegacyRow)
+{
+ auto intValue1 = MakeUnversionedInt64Value(42);
+ auto intValue2 = MakeUnversionedInt64Value(-7);
+ auto intValue3 = MakeUnversionedInt64Value(0);
+ auto maxValue = MakeUnversionedSentinelValue(EValueType::Max);
+ auto minValue = MakeUnversionedSentinelValue(EValueType::Min);
+ const int KeyLength = 2;
+
+ // Refer to comment in KeyBoundFromLegacyRow for detailed explanation of possible cases.
+
+ // (A)
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, intValue3}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, intValue3}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ true, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, maxValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, maxValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ true, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, minValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2, minValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ true, /* isUpper */ true));
+
+ // (B)
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, intValue2}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1, intValue2}, /* isInclusive */ false, /* isUpper */ true));
+
+ // (C)
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ true));
+
+ // (C), arbitrary garbage after first sentinel does not change outcome.
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, minValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, minValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, maxValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, maxValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, intValue2}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, minValue, intValue2}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ true));
+
+ // (D)
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ true));
+
+ // (D), arbitrary garbage after first sentinel does not change outcome.
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, minValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, minValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, maxValue}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, maxValue}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ true));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, intValue2}), /* isUpper */ false, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ false, /* isUpper */ false));
+ EXPECT_EQ(
+ KeyBoundFromLegacyRow(MakeRow({intValue1, maxValue, intValue2}), /* isUpper */ true, KeyLength),
+ MakeKeyBound({intValue1}, /* isInclusive */ true, /* isUpper */ true));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TKeyBoundTest, Serialization)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedDoubleValue(3.14, 0));
+ builder.AddValue(MakeUnversionedInt64Value(-42, 1));
+ builder.AddValue(MakeUnversionedUint64Value(27, 2));
+ TString str = "Foo";
+ builder.AddValue(MakeUnversionedStringValue(str, 3));
+
+ auto owningRow = builder.FinishRow();
+
+ auto keyBoundGT = TOwningKeyBound::FromRow() > owningRow;
+ auto keyBoundGE = TOwningKeyBound::FromRow() >= owningRow;
+ auto keyBoundLT = TOwningKeyBound::FromRow() < owningRow;
+ auto keyBoundLE = TOwningKeyBound::FromRow() <= owningRow;
+
+ EXPECT_EQ(
+ ConvertToYsonString(keyBoundGT, EYsonFormat::Text).AsStringBuf(),
+ "[\">\";[3.14;-42;27u;\"Foo\";];]");
+ EXPECT_EQ(
+ ConvertToYsonString(keyBoundGE, EYsonFormat::Text).AsStringBuf(),
+ "[\">=\";[3.14;-42;27u;\"Foo\";];]");
+ EXPECT_EQ(
+ ConvertToYsonString(keyBoundLT, EYsonFormat::Text).AsStringBuf(),
+ "[\"<\";[3.14;-42;27u;\"Foo\";];]");
+ EXPECT_EQ(
+ ConvertToYsonString(keyBoundLE, EYsonFormat::Text).AsStringBuf(),
+ "[\"<=\";[3.14;-42;27u;\"Foo\";];]");
+ EXPECT_EQ(
+ ConvertToYsonString(TOwningKeyBound(), EYsonFormat::Text).AsStringBuf(),
+ "#");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/key_helpers.cpp b/yt/yt/client/unittests/key_helpers.cpp
new file mode 100644
index 0000000000..f6ee9ad97c
--- /dev/null
+++ b/yt/yt/client/unittests/key_helpers.cpp
@@ -0,0 +1,28 @@
+#include "key_helpers.h"
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedOwningRow MakeRow(const std::vector<TUnversionedValue>& values)
+{
+ TUnversionedOwningRowBuilder builder;
+ for (const auto& value : values) {
+ builder.AddValue(value);
+ }
+ return builder.FinishRow();
+}
+
+TOwningKeyBound MakeKeyBound(const std::vector<TUnversionedValue>& values, bool isInclusive, bool isUpper)
+{
+ return TOwningKeyBound::FromRow(MakeRow(values), isInclusive, isUpper);
+}
+
+TComparator MakeComparator(int keyLength)
+{
+ return TComparator(std::vector<ESortOrder>(keyLength, ESortOrder::Ascending));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/key_helpers.h b/yt/yt/client/unittests/key_helpers.h
new file mode 100644
index 0000000000..c0df86521a
--- /dev/null
+++ b/yt/yt/client/unittests/key_helpers.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <yt/yt/client/table_client/key.h>
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/comparator.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUnversionedOwningRow MakeRow(const std::vector<TUnversionedValue>& values);
+TOwningKeyBound MakeKeyBound(const std::vector<TUnversionedValue>& values, bool isInclusive, bool isUpper);
+TComparator MakeComparator(int keyLength);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/key_ut.cpp b/yt/yt/client/unittests/key_ut.cpp
new file mode 100644
index 0000000000..0d35528f7b
--- /dev/null
+++ b/yt/yt/client/unittests/key_ut.cpp
@@ -0,0 +1,63 @@
+#include <yt/yt/client/table_client/key.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TKeyTest, Simple)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedDoubleValue(3.14, 0));
+ builder.AddValue(MakeUnversionedInt64Value(-42, 1));
+ builder.AddValue(MakeUnversionedUint64Value(27, 2));
+ TString str = "Foo";
+ builder.AddValue(MakeUnversionedStringValue(str, 3));
+
+ auto row = builder.FinishRow();
+ {
+ auto key = TKey::FromRow(row);
+ EXPECT_EQ(row, key.AsOwningRow());
+ EXPECT_EQ(row.Begin(), key.Begin());
+ EXPECT_EQ(4, key.GetLength());
+ }
+ {
+ TUnversionedOwningRow shortenedRow(row.FirstNElements(2));
+ auto key = TKey::FromRow(row, /*length*/2);
+ EXPECT_EQ(shortenedRow, key.AsOwningRow());
+ EXPECT_EQ(row.Begin(), key.Begin());
+ EXPECT_EQ(2, key.GetLength());
+ }
+}
+
+TEST(TKeyTest, Serialization)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedDoubleValue(3.14, 0));
+ builder.AddValue(MakeUnversionedInt64Value(-42, 1));
+ builder.AddValue(MakeUnversionedUint64Value(27, 2));
+ TString str = "Foo";
+ builder.AddValue(MakeUnversionedStringValue(str, 3));
+ auto row = builder.FinishRow();
+ TKey key = TKey::FromRow(row);
+
+ EXPECT_EQ(
+ ConvertToYsonString(key, EYsonFormat::Text).AsStringBuf(),
+ "[3.14;-42;27u;\"Foo\";]");
+ EXPECT_EQ(
+ ConvertToYsonString(TKey(), EYsonFormat::Text).AsStringBuf(),
+ "#");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/logical_type_shortcuts.h b/yt/yt/client/unittests/logical_type_shortcuts.h
new file mode 100644
index 0000000000..7b22006a06
--- /dev/null
+++ b/yt/yt/client/unittests/logical_type_shortcuts.h
@@ -0,0 +1,135 @@
+#include <yt/yt/client/table_client/logical_type.h>
+
+/**
+ * This file contains a bunch of shortcut functions for creating logical types.
+ * They are useful in places where we have to create a lot of logical types e.g. in tests.
+ */
+
+namespace NYT::NTableClient::NLogicalTypeShortcuts {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Simple types:
+// TLogicalTypePtr Int8();
+// and friends.
+
+#define CREATE_SIMPLE_TYPE_FUNCTION(name) \
+ inline TLogicalTypePtr name() \
+ { \
+ return SimpleLogicalType(ESimpleLogicalValueType::name); \
+ }
+
+CREATE_SIMPLE_TYPE_FUNCTION(Int8)
+CREATE_SIMPLE_TYPE_FUNCTION(Int16)
+CREATE_SIMPLE_TYPE_FUNCTION(Int32)
+CREATE_SIMPLE_TYPE_FUNCTION(Int64)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Uint8)
+CREATE_SIMPLE_TYPE_FUNCTION(Uint16)
+CREATE_SIMPLE_TYPE_FUNCTION(Uint32)
+CREATE_SIMPLE_TYPE_FUNCTION(Uint64)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Float)
+CREATE_SIMPLE_TYPE_FUNCTION(Double)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Utf8)
+CREATE_SIMPLE_TYPE_FUNCTION(String)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Date)
+CREATE_SIMPLE_TYPE_FUNCTION(Datetime)
+CREATE_SIMPLE_TYPE_FUNCTION(Timestamp)
+CREATE_SIMPLE_TYPE_FUNCTION(Interval)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Json)
+
+CREATE_SIMPLE_TYPE_FUNCTION(Null)
+CREATE_SIMPLE_TYPE_FUNCTION(Void)
+CREATE_SIMPLE_TYPE_FUNCTION(Uuid)
+
+#undef CREATE_SIMPLE_TYPE_FUNCTION
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TLogicalTypePtr Yson()
+{
+ return SimpleLogicalType(ESimpleLogicalValueType::Any);
+}
+
+inline TLogicalTypePtr Bool()
+{
+ return SimpleLogicalType(ESimpleLogicalValueType::Boolean);
+}
+
+inline TLogicalTypePtr Optional(const TLogicalTypePtr& element)
+{
+ return OptionalLogicalType(element);
+}
+
+inline TLogicalTypePtr Decimal(int precision, int scale)
+{
+ return DecimalLogicalType(precision, scale);
+}
+
+inline TLogicalTypePtr List(const TLogicalTypePtr& element)
+{
+ return ListLogicalType(element);
+}
+
+template <typename... T>
+inline TLogicalTypePtr Tuple(const T&... args)
+{
+ return TupleLogicalType(std::vector<TLogicalTypePtr>{args...});
+}
+
+namespace NPrivate {
+inline void StructFieldList(std::vector<TStructField>* /*fields*/)
+{ }
+
+template <typename... T>
+inline void StructFieldList(
+ std::vector<TStructField>* fields,
+ const TString& name,
+ const TLogicalTypePtr& type,
+ const T&... args)
+{
+ fields->push_back({name, type});
+ StructFieldList(fields, args...);
+}
+
+} // namespace NPrivate
+
+template <typename... T>
+inline TLogicalTypePtr Struct(const T&... args)
+{
+ std::vector<TStructField> fields;
+ NPrivate::StructFieldList(&fields, args...);
+ return StructLogicalType(fields);
+}
+
+template <typename... T>
+inline TLogicalTypePtr VariantTuple(const T&... args)
+{
+ return VariantTupleLogicalType(std::vector<TLogicalTypePtr>{args...});
+}
+
+template <typename... T>
+inline TLogicalTypePtr VariantStruct(const T&... args)
+{
+ std::vector<TStructField> fields;
+ NPrivate::StructFieldList(&fields, args...);
+ return VariantStructLogicalType(fields);
+}
+
+inline TLogicalTypePtr Dict(const TLogicalTypePtr& key, const TLogicalTypePtr& value)
+{
+ return DictLogicalType(key, value);
+}
+
+inline TLogicalTypePtr Tagged(TString tag, const TLogicalTypePtr& element)
+{
+ return TaggedLogicalType(std::move(tag), element);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient::NLogicalTypeShortcuts
diff --git a/yt/yt/client/unittests/logical_type_ut.cpp b/yt/yt/client/unittests/logical_type_ut.cpp
new file mode 100644
index 0000000000..0cf744313e
--- /dev/null
+++ b/yt/yt/client/unittests/logical_type_ut.cpp
@@ -0,0 +1,1059 @@
+#include "logical_type_shortcuts.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/resource/resource.h>
+
+#include <util/string/split.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLogicalTypeTest, TestCastToV1Type)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ EXPECT_EQ(
+ CastToV1Type(Int64()),
+ std::pair(ESimpleLogicalValueType::Int64, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Optional(Int64())),
+ std::pair(ESimpleLogicalValueType::Int64, false));
+
+ EXPECT_EQ(
+ CastToV1Type(Optional(Optional(Int64()))),
+ std::pair(ESimpleLogicalValueType::Any, false));
+
+ EXPECT_EQ(
+ CastToV1Type(Null()),
+ std::pair(ESimpleLogicalValueType::Null, false));
+
+ EXPECT_EQ(
+ CastToV1Type(Optional(Null())),
+ std::pair(ESimpleLogicalValueType::Any, false));
+
+ EXPECT_EQ(
+ CastToV1Type(List(Int64())),
+ std::pair(ESimpleLogicalValueType::Any, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Struct("value", Int64())),
+ std::pair(ESimpleLogicalValueType::Any, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Tuple(Int64(), Uint64())),
+ std::pair(ESimpleLogicalValueType::Any, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Dict(String(), Uint64())),
+ std::pair(ESimpleLogicalValueType::Any, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Tagged("foo", String())),
+ std::pair(ESimpleLogicalValueType::String, true));
+
+ EXPECT_EQ(
+ CastToV1Type(Tagged("foo", Optional(String()))),
+ std::pair(ESimpleLogicalValueType::String, false));
+
+ EXPECT_EQ(
+ CastToV1Type(Tagged("foo", Optional(Optional(String())))),
+ std::pair(ESimpleLogicalValueType::Any, false));
+
+ EXPECT_EQ(
+ CastToV1Type(TaggedLogicalType("foo", List(String()))),
+ std::pair(ESimpleLogicalValueType::Any, true));
+
+ EXPECT_EQ(
+ CastToV1Type(DecimalLogicalType(3, 2)),
+ std::pair(ESimpleLogicalValueType::String, true));
+}
+
+TEST(TLogicalTypeTest, DictValidationTest)
+{
+ EXPECT_NO_THROW(DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))
+ ));
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any)),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))),
+ "is not allowed in dict key");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ ListLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))),
+ "is not allowed in dict key");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ StructLogicalType({
+ {"foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"bar", SimpleLogicalType(ESimpleLogicalValueType::Any)},
+ }),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))),
+ "is not allowed in dict key");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ VariantStructLogicalType({
+ {"foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"bar", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))},
+ }),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))),
+ "is not allowed in dict key");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ TaggedLogicalType("bar", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))),
+ "is not allowed in dict key");
+ EXPECT_NO_THROW(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("example_column", DictLogicalType(
+ TaggedLogicalType("bar", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ))));
+
+ EXPECT_NO_THROW(
+ ValidateLogicalType(
+ TComplexTypeFieldDescriptor("example_column",
+ DictLogicalType(
+ DecimalLogicalType(3, 2),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ )
+ )
+ ));
+}
+
+TEST(TLogicalTypeTest, TestDetag)
+{
+ EXPECT_EQ(
+ *DetagLogicalType(
+ TaggedLogicalType("tag", SimpleLogicalType(ESimpleLogicalValueType::String))
+ ),
+ *SimpleLogicalType(ESimpleLogicalValueType::String)
+ );
+ EXPECT_EQ(
+ *DetagLogicalType(
+ TaggedLogicalType("tag",
+ StructLogicalType({
+ {"list", TaggedLogicalType("tag2", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int8)))},
+ {"tuple", TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Double),
+ TaggedLogicalType("tag3", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))),
+ })
+ },
+ })
+ )
+ ),
+ *StructLogicalType({
+ {"list", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int8))},
+ {"tuple", TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Double),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ })
+ },
+ })
+ );
+}
+
+
+static const std::vector<TLogicalTypePtr> ComplexTypeExampleList = {
+ // Simple types
+ SimpleLogicalType(ESimpleLogicalValueType::Null),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Utf8),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+
+
+ // Decimals
+ DecimalLogicalType(1, 0),
+ DecimalLogicalType(1, 1),
+ DecimalLogicalType(3, 2),
+ DecimalLogicalType(35, 0),
+ DecimalLogicalType(35, 35),
+ OptionalLogicalType(DecimalLogicalType(1, 0)),
+ OptionalLogicalType(DecimalLogicalType(1, 1)),
+ OptionalLogicalType(DecimalLogicalType(3, 2)),
+ OptionalLogicalType(DecimalLogicalType(35, 0)),
+ OptionalLogicalType(DecimalLogicalType(35, 35)),
+
+ // Optionals
+ OptionalLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Utf8))),
+ OptionalLogicalType(
+ ListLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Utf8)))),
+
+ // Lists
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Utf8)),
+ ListLogicalType(
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Utf8))),
+ ListLogicalType(
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String))),
+
+ // Structs
+ StructLogicalType({
+ {"key", SimpleLogicalType(ESimpleLogicalValueType::Utf8)},
+ {"value", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Utf8))},
+ }),
+ StructLogicalType({
+ {"value", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Utf8))},
+ {"key", SimpleLogicalType(ESimpleLogicalValueType::Utf8)},
+ }),
+ StructLogicalType({
+ {"key", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"value", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ }),
+
+ // Tuples
+ TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ }),
+ TupleLogicalType({
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ }),
+ TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ }),
+
+ // VariantTuple
+ VariantTupleLogicalType(
+ {
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ }),
+ VariantTupleLogicalType(
+ {
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ }),
+ VariantTupleLogicalType(
+ {
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ }),
+
+ // VariantStruct
+ VariantStructLogicalType(
+ {
+ {"string", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"int", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }),
+ VariantStructLogicalType(
+ {
+ {"int", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"string", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }),
+ VariantStructLogicalType(
+ {
+ {"string", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"string", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }),
+
+ // Dict
+ DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ ),
+ DictLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ ),
+ DictLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null)),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))
+ ),
+
+ // Tagged
+ TaggedLogicalType("foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ TaggedLogicalType("bar", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ TaggedLogicalType("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ TaggedLogicalType("foo", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+};
+
+TEST(TLogicalTypeTest, TestAllTypesAreInExamples)
+{
+ auto allMetatypes = TEnumTraits<ELogicalMetatype>::GetDomainValues();
+ std::set<ELogicalMetatype> actualMetatypes;
+ for (const auto& example : ComplexTypeExampleList) {
+ actualMetatypes.insert(example->GetMetatype());
+ }
+ // This test checks that we have all top level metatypes in our ComplexTypeExampleList
+ // and therefore all the tests that use this example list cover all all complex metatypes.
+ EXPECT_EQ(actualMetatypes, std::set<ELogicalMetatype>(allMetatypes.begin(), allMetatypes.end()));
+}
+
+TEST(TLogicalTypeTest, TestAllExamplesHaveDifferentHash)
+{
+ std::map<int, TLogicalTypePtr> hashToType;
+ auto hashFunction = THash<TLogicalType>();
+ for (const auto& example : ComplexTypeExampleList) {
+ auto hash = hashFunction(*example);
+ auto it = hashToType.find(hash);
+ if (it != hashToType.end()) {
+ ADD_FAILURE() << Format(
+ "Type %Qv and %Qv have the same hash %v",
+ *it->second,
+ *example,
+ hash);
+ } else {
+ hashToType[hash] = example;
+ }
+ }
+}
+
+TEST(TLogicalTypeTest, TestAllExamplesAreNotEqual)
+{
+ // Hopefully our example list will never be so big that n^2 complexity is a problem.
+ for (const auto& lhs : ComplexTypeExampleList) {
+ for (const auto& rhs : ComplexTypeExampleList) {
+ if (&lhs == &rhs) {
+ EXPECT_EQ(*lhs, *rhs);
+ } else {
+ EXPECT_NE(*lhs, *rhs);
+ }
+ }
+ }
+}
+
+TEST(TLogicalTypeTest, TestBadProtobufDeserialization)
+{
+ NProto::TLogicalType proto;
+
+ TLogicalTypePtr deserializedType;
+ EXPECT_THROW_WITH_SUBSTRING(
+ FromProto(&deserializedType, proto),
+ "Cannot parse unknown logical type from proto");
+}
+
+TEST(TLogicalTypeTest, TestIsComparable) {
+ EXPECT_TRUE(IsComparable(
+ OptionalLogicalType(
+ OptionalLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )
+ )
+ ));
+
+ EXPECT_TRUE(IsComparable(
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )
+ ));
+
+ EXPECT_TRUE(IsComparable(
+ TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ })
+ ));
+
+ EXPECT_FALSE(IsComparable(
+ StructLogicalType({
+ {"foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"bar", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ })
+ ));
+
+ EXPECT_FALSE(IsComparable(
+ DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String)
+ )
+ ));
+
+ EXPECT_FALSE(IsComparable(
+ VariantStructLogicalType({
+ {"foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"bar", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ })
+ ));
+
+ EXPECT_TRUE(IsComparable(
+ VariantTupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ })
+ ));
+
+ EXPECT_TRUE(IsComparable(DecimalLogicalType(3, 2)));
+}
+
+TString CanonizeYsonString(TString input)
+{
+ auto node = ConvertToNode(TYsonString(input));
+ auto binaryYson = ConvertToYsonString(node);
+
+ TStringStream out;
+ {
+ TYsonWriter writer(&out, EYsonFormat::Pretty);
+ ParseYsonStringBuffer(binaryYson.AsStringBuf(), EYsonType::Node, &writer);
+ }
+ return out.Str();
+}
+
+TString ToTypeV3(const TLogicalTypePtr& logicalType)
+{
+ TTypeV3LogicalTypeWrapper w{logicalType};
+ auto ysonString = ConvertToYsonString(w);
+ return CanonizeYsonString(ysonString.ToString());
+}
+
+class TLogicalTypeYson
+ : public ::testing::TestWithParam<bool>
+{
+public:
+ TLogicalTypePtr FromTypeV3(TString yson)
+ {
+ const auto parseFromNode = GetParam();
+ const auto ysonStr = TYsonStringBuf(yson, EYsonType::Node);
+ if (parseFromNode) {
+ auto wrapper = ConvertTo<TTypeV3LogicalTypeWrapper>(ConvertTo<INodePtr>(ysonStr));
+ return wrapper.LogicalType;
+ } else {
+ auto wrapper = ConvertTo<TTypeV3LogicalTypeWrapper>(ysonStr);
+ return wrapper.LogicalType;
+ }
+ }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ ParseFromNode,
+ TLogicalTypeYson,
+ ::testing::Values(true));
+
+INSTANTIATE_TEST_SUITE_P(
+ ParseFromCursor,
+ TLogicalTypeYson,
+ ::testing::Values(false));
+
+#define CHECK_TYPE_V3(type, expectedYson) \
+ do { \
+ EXPECT_EQ(ToTypeV3(type), CanonizeYsonString(expectedYson)); \
+ EXPECT_EQ(*type, *FromTypeV3(expectedYson)); \
+} while(0)
+
+TEST_P(TLogicalTypeYson, TestTypeV3Yson)
+{
+ // Ill formed.
+ EXPECT_THROW_THAT(FromTypeV3("{}"), ::testing::AnyOf(
+ ::testing::HasSubstr("has no child with key"),
+ ::testing::HasSubstr("is required")));
+
+ // Simple types.
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Int64), "int64");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Int64), *FromTypeV3("{type_name=int64;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Uint64), "uint64");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Uint64), *FromTypeV3("{type_name=uint64;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Double), "double");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Double), *FromTypeV3("{type_name=double;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Float), "float");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Float), *FromTypeV3("{type_name=float;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Boolean), "bool");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Boolean), *FromTypeV3("{type_name=bool;}"));
+ EXPECT_THROW_WITH_SUBSTRING(FromTypeV3("boolean"), "is not valid type_v3 simple type");
+ EXPECT_THROW_WITH_SUBSTRING(FromTypeV3("{type_name=boolean}"), "is not valid type_v3 simple type");
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::String), "string");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::String), *FromTypeV3("{type_name=string;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Null), "null");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Null), *FromTypeV3("{type_name=null;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Any), "yson");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Any), *FromTypeV3("{type_name=yson;}"));
+ EXPECT_THROW_WITH_SUBSTRING(FromTypeV3("{type_name=any}"), "is not valid type_v3 simple type");
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Int32), "int32");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Int32), *FromTypeV3("{type_name=int32;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Int16), "int16");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Int16), *FromTypeV3("{type_name=int16;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Int8), "int8");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Int8), *FromTypeV3("{type_name=int8;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Uint32), "uint32");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Uint32), *FromTypeV3("{type_name=uint32;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Uint16), "uint16");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Uint16), *FromTypeV3("{type_name=uint16;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Uint8), "uint8");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Uint8), *FromTypeV3("{type_name=uint8;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Utf8), "utf8");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Utf8), *FromTypeV3("{type_name=utf8;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Date), "date");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Date), *FromTypeV3("{type_name=date;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Datetime), "datetime");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Datetime), *FromTypeV3("{type_name=datetime;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Timestamp), "timestamp");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Timestamp), *FromTypeV3("{type_name=timestamp;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Interval), "interval");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Interval), *FromTypeV3("{type_name=interval;}"));
+
+ CHECK_TYPE_V3(SimpleLogicalType(ESimpleLogicalValueType::Json), "json");
+ EXPECT_EQ(*SimpleLogicalType(ESimpleLogicalValueType::Json), *FromTypeV3("{type_name=json;}"));
+
+ // Optional.
+ CHECK_TYPE_V3(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean)),
+ "{type_name=optional; item=bool}");
+
+ CHECK_TYPE_V3(
+ OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))),
+ R"(
+ {
+ type_name=optional;
+ item={
+ type_name=optional;
+ item=yson;
+ }
+ }
+ )");
+
+ EXPECT_THROW_THAT(FromTypeV3("{type_name=optional}"), ::testing::AnyOf(
+ ::testing::HasSubstr("has no child with key"),
+ ::testing::HasSubstr("is required")));
+
+ // List
+ CHECK_TYPE_V3(
+ ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean)),
+ "{type_name=list; item=bool}");
+
+ CHECK_TYPE_V3(
+ ListLogicalType(ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))),
+ R"(
+ {
+ type_name=list;
+ item={
+ type_name=list;
+ item=yson;
+ }
+ }
+ )");
+
+ EXPECT_THROW_THAT(FromTypeV3("{type_name=list}"), ::testing::AnyOf(
+ ::testing::HasSubstr("has no child with key"),
+ ::testing::HasSubstr("is required")));
+
+ // Struct
+ CHECK_TYPE_V3(
+ StructLogicalType({
+ {"a", SimpleLogicalType(ESimpleLogicalValueType::Boolean)},
+ {"b", StructLogicalType({
+ { "foo", SimpleLogicalType(ESimpleLogicalValueType::Boolean) },
+ })},
+ }),
+ R"(
+ {
+ type_name=struct;
+ members=[
+ {name=a; type=bool};
+ {
+ name=b;
+ type={
+ type_name=struct;
+ members=[{name=foo;type=bool}];
+ };
+ };
+ ];
+ }
+ )");
+
+ // Tuple
+ CHECK_TYPE_V3(
+ TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean),
+ TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean)
+ })
+ }),
+ R"(
+ {
+ type_name=tuple;
+ elements=[
+ {type=bool};
+ {
+ type={
+ type_name=tuple;
+ elements=[{type=bool}];
+ };
+ };
+ ];
+ }
+ )");
+
+ // VariantStruct
+ CHECK_TYPE_V3(
+ VariantStructLogicalType({
+ {"a", SimpleLogicalType(ESimpleLogicalValueType::Boolean)},
+ {"b", VariantStructLogicalType({
+ { "foo", SimpleLogicalType(ESimpleLogicalValueType::Boolean) },
+ })},
+ }),
+ R"(
+ {
+ type_name=variant;
+ members=[
+ {name=a; type=bool};
+ {
+ name=b;
+ type={
+ type_name=variant;
+ members=[{name=foo;type=bool}];
+ };
+ };
+ ];
+ }
+ )");
+
+ // Tuple
+ CHECK_TYPE_V3(
+ VariantTupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean),
+ VariantTupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean)
+ })
+ }),
+ R"(
+ {
+ type_name=variant;
+ elements=[
+ {type=bool};
+ {
+ type={
+ type_name=variant;
+ elements=[{type=bool}];
+ };
+ };
+ ];
+ }
+ )");
+
+ // Dict
+ CHECK_TYPE_V3(
+ DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean),
+ DictLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean)),
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean)
+ )
+ ),
+ R"(
+ {
+ type_name=dict;
+ key=bool;
+ value={
+ type_name=dict;
+ key={type_name=optional; item=bool};
+ value=bool;
+ }
+ }
+ )");
+
+ // Tagged
+ CHECK_TYPE_V3(
+ TaggedLogicalType(
+ "foo",
+ TaggedLogicalType(
+ "bar",
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean)
+ )
+ ),
+ R"(
+ {
+ type_name=tagged;
+ tag=foo;
+ item={
+ type_name=tagged;
+ tag=bar;
+ item=bool;
+ }
+ }
+ )");
+
+ // Decimal
+ CHECK_TYPE_V3(
+ DecimalLogicalType(3, 2),
+ R"(
+ {
+ type_name=decimal;
+ precision=3;
+ scale=2;
+ }
+ )");
+}
+
+class TLogicalTypeTestExamples
+ : public ::testing::TestWithParam<TLogicalTypePtr>
+{ };
+
+TEST_P(TLogicalTypeTestExamples, TestProtoSerialization)
+{
+ auto type = GetParam();
+
+ NProto::TLogicalType proto;
+ ToProto(&proto, type);
+
+ TLogicalTypePtr deserializedType;
+ FromProto(&deserializedType, proto);
+
+ EXPECT_EQ(*type, *deserializedType);
+}
+
+TEST_P(TLogicalTypeTestExamples, TestYsonSerialization)
+{
+ auto type = GetParam();
+ auto yson = ConvertToYsonString(type);
+ auto deserializedType = ConvertTo<TLogicalTypePtr>(yson);
+ EXPECT_EQ(*type, *deserializedType);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Examples,
+ TLogicalTypeTestExamples,
+ ::testing::ValuesIn(ComplexTypeExampleList));
+
+using TCombineTypeFunc = std::function<TLogicalTypePtr(const TLogicalTypePtr&)>;
+
+std::vector<std::pair<TString, TCombineTypeFunc>> CombineFunctions = {
+ {
+ "optional",
+ [](const TLogicalTypePtr& type) {
+ return OptionalLogicalType(type);
+ },
+ },
+ {
+ "list",
+ [](const TLogicalTypePtr& type) {
+ return ListLogicalType(type);
+ },
+ },
+ {
+ "struct",
+ [](const TLogicalTypePtr& type) {
+ return StructLogicalType({{"field", type}});
+ },
+ },
+ {
+ "tuple",
+ [](const TLogicalTypePtr& type) {
+ return TupleLogicalType({type});
+ },
+ },
+ {
+ "variant_struct",
+ [](const TLogicalTypePtr& type) {
+ return VariantStructLogicalType({{"field", type}});
+ },
+ },
+ {
+ "variant_tuple",
+ [](const TLogicalTypePtr& type) {
+ return VariantTupleLogicalType({type});
+ },
+ },
+ {
+ "dict",
+ [](const TLogicalTypePtr& type) {
+ return DictLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String), type);
+ },
+ },
+ {
+ "dict-key",
+ [](const TLogicalTypePtr& type) {
+ return DictLogicalType(type, SimpleLogicalType(ESimpleLogicalValueType::String));
+ },
+ },
+ {
+ "dict-value",
+ [](const TLogicalTypePtr& type) {
+ return DictLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String), type);
+ }
+ },
+ {
+ "tagged",
+ [](const TLogicalTypePtr& type) {
+ return TaggedLogicalType("foo", type);
+ }
+ }
+};
+
+TEST(TLogicalTypeTest, TestAllTypesInCombineFunctions)
+{
+ const auto allMetatypes = TEnumTraits<ELogicalMetatype>::GetDomainValues();
+ std::set<ELogicalMetatype> actualMetatypes;
+ for (const auto& [name, function] : CombineFunctions) {
+ auto combined = function(SimpleLogicalType(ESimpleLogicalValueType::Int64));
+ actualMetatypes.insert(combined->GetMetatype());
+ }
+ std::set<ELogicalMetatype> expectedMetatypes(allMetatypes.begin(), allMetatypes.end());
+ expectedMetatypes.erase(ELogicalMetatype::Simple);
+ expectedMetatypes.erase(ELogicalMetatype::Decimal);
+ EXPECT_EQ(actualMetatypes, expectedMetatypes);
+}
+
+class TCombineLogicalMetatypeTests
+ : public ::testing::TestWithParam<std::pair<TString,TCombineTypeFunc>>
+{ };
+
+INSTANTIATE_TEST_SUITE_P(
+ CombineFunctions,
+ TCombineLogicalMetatypeTests,
+ ::testing::ValuesIn(CombineFunctions));
+
+TEST_P(TCombineLogicalMetatypeTests, TestValidateStruct)
+{
+ auto badType = StructLogicalType({{"", SimpleLogicalType(ESimpleLogicalValueType::Int64)}});
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", badType)),
+ "Name of struct field #0 is empty");
+
+ const auto& [combineName, combineFunc] = GetParam();
+ const auto combinedType1 = combineFunc(badType);
+ const auto combinedType2 = combineFunc(combinedType1);
+ EXPECT_NE(*combinedType1, *badType);
+ EXPECT_NE(*combinedType1, *combinedType2);
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", combinedType1)),
+ "Name of struct field #0 is empty");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", combinedType2)),
+ "Name of struct field #0 is empty");
+}
+
+TEST_P(TCombineLogicalMetatypeTests, TestValidateAny)
+{
+ const auto& [combineName, combineFunc] = GetParam();
+
+ if (combineName == "optional" || combineName == "dict-key") {
+ // Skip tests for these combiners.
+ return;
+ }
+
+ auto badType = SimpleLogicalType(ESimpleLogicalValueType::Any);
+ auto goodType = OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any));
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", badType)),
+ "is disallowed outside of optional");
+ EXPECT_NO_THROW(ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", goodType)));
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", combineFunc(badType))),
+ "is disallowed outside of optional");
+ EXPECT_NO_THROW(ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", combineFunc(goodType))));
+}
+
+TEST_P(TCombineLogicalMetatypeTests, TestTrivialDetag)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ const auto& [combineName, combineFunc] = GetParam();
+ if (combineName == "tagged") {
+ // Skip test for this combiner.
+ return;
+ }
+ const auto& logicalType = Utf8();
+
+ const auto combinedType = combineFunc(logicalType);
+
+ const auto detaggedType = DetagLogicalType(combinedType);
+ EXPECT_EQ(*detaggedType, *combinedType);
+ EXPECT_EQ(detaggedType.Get(), combinedType.Get());
+}
+
+TEST_P(TCombineLogicalMetatypeTests, TestNonTrivialDetag)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ const auto& [combineName, combineFunc] = GetParam();
+ if (combineName == "tagged") {
+ // Skip test for this combiner.
+ return;
+ }
+ const auto& logicalType = TaggedLogicalType("foo", Utf8());
+
+ const auto combinedType = combineFunc(logicalType);
+
+ const auto detaggedType = DetagLogicalType(combinedType);
+ const auto expectedDetaggedType = combineFunc(Utf8());
+
+ EXPECT_EQ(*expectedDetaggedType, *detaggedType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStructValidationTest
+ : public ::testing::TestWithParam<ELogicalMetatype>
+{ };
+
+INSTANTIATE_TEST_SUITE_P(
+ Tests,
+ TStructValidationTest,
+ ::testing::ValuesIn({ELogicalMetatype::Struct, ELogicalMetatype::VariantStruct}));
+
+TEST_P(TStructValidationTest, Test)
+{
+ auto metatype = GetParam();
+ auto validate = [&] (const std::vector<TStructField>& fields) {
+ std::function<TLogicalTypePtr(std::vector<TStructField> fields)> typeConstructor;
+ if (metatype == ELogicalMetatype::Struct) {
+ typeConstructor = StructLogicalType;
+ } else {
+ YT_VERIFY(metatype == ELogicalMetatype::VariantStruct);
+ typeConstructor = VariantStructLogicalType;
+ }
+ ValidateLogicalType(TComplexTypeFieldDescriptor("test-column", typeConstructor(fields)));
+ };
+
+ EXPECT_NO_THROW(validate({}));
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ validate({{"", SimpleLogicalType(ESimpleLogicalValueType::Int64)}}),
+ "Name of struct field #0 is empty");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ validate({
+ {"a", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"a", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }),
+ "Struct field name \"a\" is used twice");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ validate({
+ {TString(257, 'a'), SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }),
+ "Name of struct field #0 exceeds limit");
+ EXPECT_THROW_WITH_SUBSTRING(
+ validate({
+ {"\xFF", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }),
+ "Name of struct field #0 is not valid utf8");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<std::vector<TString>> ParseData(TStringBuf data, int expectedFieldsCount) {
+ TString noComments;
+ {
+ TMemoryInput in(data);
+ TString line;
+ while (in.ReadLine(line)) {
+ if (StripString(line).StartsWith('#')) {
+ continue;
+ }
+ noComments += line;
+ }
+ }
+
+ std::vector<std::vector<TString>> result;
+ for (TStringBuf record : StringSplitter(noComments).SplitByString(";;")) {
+ record = StripString(record);
+ if (record.Empty()) {
+ continue;
+ }
+ std::vector<TString> fields;
+ for (TStringBuf field : StringSplitter(record).SplitByString("::")) {
+ fields.emplace_back(StripString(field));
+ }
+ if (static_cast<int>(fields.size()) != expectedFieldsCount) {
+ ythrow yexception() << "Unexpected field count expected: " << expectedFieldsCount << " actual: " << fields.size();
+ }
+ result.push_back(fields);
+ }
+ return result;
+}
+
+TEST(TTestLogicalTypesWithData, GoodTypes)
+{
+ auto records = ParseData(NResource::Find("/types/good"), 2);
+
+ for (const auto& record : records) {
+ const auto& typeYson = record.at(0);
+ // TODO(levysotsky): Remove when tz_* types are supported.
+ if (typeYson.Contains("tz_")) {
+ continue;
+ }
+ const auto& typeText = record.at(1);
+ TString context = Format("text: %v\nyson: %v\n", typeText, typeYson);
+ auto wrapError = [&] (const std::exception& ex) {
+ return yexception() << "Unexpected error: " << ex.what() << '\n' << context;
+ };
+
+ TLogicalTypePtr type;
+ try {
+ type = ConvertTo<TLogicalTypePtr>(TYsonStringBuf(typeYson));
+ } catch (const std::exception& ex) {
+ ythrow wrapError(ex);
+ }
+
+ auto serialized = ConvertToYsonString(type);
+ TLogicalTypePtr type2;
+ try {
+ type2 = ConvertTo<TLogicalTypePtr>(serialized);
+ } catch (const std::exception& ex) {
+ ythrow wrapError(ex);
+ }
+
+ EXPECT_EQ(*type, *type2);
+ }
+}
+
+TEST(TTestLogicalTypesWithData, BadTypes)
+{
+ auto records = ParseData(NResource::Find("/types/bad"), 3);
+
+ for (const auto& record : records) {
+ const auto& typeYson = record.at(0);
+ // TODO(levysotsky): Remove when tz_* types are supported.
+ if (typeYson.Contains("tz_")) {
+ continue;
+ }
+
+ EXPECT_THROW(ConvertTo<TLogicalTypePtr>(TYsonStringBuf(typeYson)), TErrorException);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/mock/client.h b/yt/yt/client/unittests/mock/client.h
new file mode 100644
index 0000000000..a9d1f6b1ea
--- /dev/null
+++ b/yt/yt/client/unittests/mock/client.h
@@ -0,0 +1,613 @@
+#pragma once
+
+#include <yt/yt/client/api/connection.h>
+#include <yt/yt/client/api/client.h>
+#include <yt/yt/client/api/file_writer.h>
+#include <yt/yt/client/api/journal_reader.h>
+#include <yt/yt/client/api/journal_writer.h>
+#include <yt/yt/client/api/transaction.h>
+
+#include <yt/yt/client/chaos_client/replication_card_cache.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/client/tablet_client/table_mount_cache.h>
+
+#include <yt/yt/client/transaction_client/timestamp_provider.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockClient
+ : public IClient
+{
+public:
+ MOCK_METHOD(IConnectionPtr, GetConnection, (), (override));
+
+ MOCK_METHOD(std::optional<TStringBuf>, GetClusterName, (bool fetchIfNull), (override));
+
+ MOCK_METHOD(TFuture<ITransactionPtr>, StartTransaction, (
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IUnversionedRowsetPtr>, LookupRows, (
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IVersionedRowsetPtr>, VersionedLookupRows, (
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<IUnversionedRowsetPtr>>, MultiLookup, (
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TSelectRowsResult>, SelectRows, (
+ const TString& query,
+ const TSelectRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullQueue, (
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullQueueOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NQueueClient::IQueueRowsetPtr>, PullConsumer, (
+ const NYPath::TRichYPath& consumerPath,
+ const NYPath::TRichYPath& queuePath,
+ i64 offset,
+ int partitionIndex,
+ const NQueueClient::TQueueRowBatchReadOptions& rowBatchReadOptions,
+ const TPullConsumerOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RegisterQueueConsumer, (
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ bool vital,
+ const TRegisterQueueConsumerOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UnregisterQueueConsumer, (
+ const NYPath::TRichYPath& queuePath,
+ const NYPath::TRichYPath& consumerPath,
+ const TUnregisterQueueConsumerOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<TListQueueConsumerRegistrationsResult>>, ListQueueConsumerRegistrations, (
+ const std::optional<NYPath::TRichYPath>& queuePath,
+ const std::optional<NYPath::TRichYPath>& consumerPath,
+ const TListQueueConsumerRegistrationsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, ExplainQuery, (
+ const TString& query,
+ const TExplainQueryOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TPullRowsResult>, PullRows, (
+ const NYPath::TYPath& path,
+ const TPullRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<ITableReaderPtr>, CreateTableReader, (
+ const NYPath::TRichYPath& path,
+ const TTableReaderOptions& options), (override));
+
+ MOCK_METHOD(TFuture<ITableWriterPtr>, CreateTableWriter, (
+ const NYPath::TRichYPath& path,
+ const TTableWriterOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetNode, (
+ const NYPath::TYPath& path,
+ const TGetNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SetNode, (
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, MultisetAttributesNode, (
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RemoveNode, (
+ const NYPath::TYPath& path,
+ const TRemoveNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, ListNode, (
+ const NYPath::TYPath& path,
+ const TListNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, CreateNode, (
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TLockNodeResult>, LockNode, (
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UnlockNode, (
+ const NYPath::TYPath& path,
+ const TUnlockNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, CopyNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TCopyNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, MoveNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TMoveNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, LinkNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TLinkNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ConcatenateNodes, (
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ExternalizeNode, (
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, InternalizeNode, (
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<bool>, NodeExists, (
+ const NYPath::TYPath& path,
+ const TNodeExistsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NObjectClient::TObjectId>, CreateObject, (
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IFileReaderPtr>, CreateFileReader, (
+ const NYPath::TYPath& path,
+ const TFileReaderOptions& options), (override));
+
+ MOCK_METHOD(IFileWriterPtr, CreateFileWriter, (
+ const NYPath::TRichYPath& path,
+ const TFileWriterOptions& options), (override));
+
+ MOCK_METHOD(IJournalReaderPtr, CreateJournalReader, (
+ const NYPath::TYPath& path,
+ const TJournalReaderOptions& options), (override));
+
+ MOCK_METHOD(IJournalWriterPtr, CreateJournalWriter, (
+ const NYPath::TYPath& path,
+ const TJournalWriterOptions& options), (override));
+
+ MOCK_METHOD(TFuture<int>, BuildSnapshot, (const TBuildSnapshotOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TCellIdToSnapshotIdMap>, BuildMasterSnapshots, (
+ const TBuildMasterSnapshotsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SwitchLeader, (
+ NHydra::TCellId cellId,
+ const TString& newLeaderAddress,
+ const TSwitchLeaderOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ResetStateHash, (
+ NHydra::TCellId cellId,
+ const TResetStateHashOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, GCCollect, (
+ const TGCCollectOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, KillProcess, (
+ const TString& address,
+ const TKillProcessOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TString>, WriteCoreDump, (
+ const TString& address,
+ const TWriteCoreDumpOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TGuid>, WriteLogBarrier, (
+ const TString& address,
+ const TWriteLogBarrierOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TString>, WriteOperationControllerCoreDump, (
+ NJobTrackerClient::TOperationId operationId,
+ const TWriteOperationControllerCoreDumpOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, HealExecNode, (
+ const TString& address,
+ const THealExecNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SuspendCoordinator, (
+ NObjectClient::TCellId coordinatorCellid,
+ const TSuspendCoordinatorOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ResumeCoordinator, (
+ NObjectClient::TCellId coordinatorCellid,
+ const TResumeCoordinatorOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, MigrateReplicationCards, (
+ NObjectClient::TCellId chaosCellid,
+ const TMigrateReplicationCardsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SuspendChaosCells, (
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendChaosCellsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ResumeChaosCells, (
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeChaosCellsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SuspendTabletCells, (
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TSuspendTabletCellsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ResumeTabletCells, (
+ const std::vector<NObjectClient::TCellId>& cellIds,
+ const TResumeTabletCellsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TMaintenanceId>, AddMaintenance, (
+ EMaintenanceComponent component,
+ const TString& address,
+ EMaintenanceType type,
+ const TString& comment,
+ const TAddMaintenanceOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TMaintenanceCounts>, RemoveMaintenance, (
+ EMaintenanceComponent component,
+ const TString& address,
+ const TMaintenanceFilter& filter,
+ const TRemoveMaintenanceOptions& options), (override));
+
+ // IClient
+ NTabletClient::ITableMountCachePtr TableMountCache;
+ NTransactionClient::ITimestampProviderPtr TimestampProvider;
+
+ MOCK_METHOD(void, Terminate, (), (override));
+ MOCK_METHOD(const NChaosClient::IReplicationCardCachePtr&, GetReplicationCardCache, (), (override));
+
+ const NTabletClient::ITableMountCachePtr& GetTableMountCache() override
+ {
+ return TableMountCache;
+ }
+ const NTransactionClient::ITimestampProviderPtr& GetTimestampProvider() override
+ {
+ return TimestampProvider;
+ }
+
+ MOCK_METHOD(ITransactionPtr, AttachTransaction, (
+ NTransactionClient::TTransactionId transactionId,
+ const TTransactionAttachOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, MountTable, (
+ const NYPath::TYPath& path,
+ const TMountTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UnmountTable, (
+ const NYPath::TYPath& path,
+ const TUnmountTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RemountTable, (
+ const NYPath::TYPath& path,
+ const TRemountTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, FreezeTable, (
+ const NYPath::TYPath& path,
+ const TFreezeTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UnfreezeTable, (
+ const NYPath::TYPath& path,
+ const TUnfreezeTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ReshardTable, (
+ const NYPath::TYPath& path,
+ const std::vector<NTableClient::TLegacyOwningKey>& pivotKeys,
+ const TReshardTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ReshardTable, (
+ const NYPath::TYPath& path,
+ int tabletCount,
+ const TReshardTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, ReshardTableAutomatic, (
+ const NYPath::TYPath& path,
+ const TReshardTableAutomaticOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, TrimTable, (
+ const NYPath::TYPath& path,
+ int tabletIndex,
+ i64 trimmedRowCount,
+ const TTrimTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AlterTable, (
+ const NYPath::TYPath& path,
+ const TAlterTableOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AlterTableReplica, (
+ NTabletClient::TTableReplicaId replicaId,
+ const TAlterTableReplicaOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetTablePivotKeys, (
+ const NYPath::TYPath& path,
+ const TGetTablePivotKeysOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, CreateTableBackup,(
+ const TBackupManifestPtr& manifest,
+ const TCreateTableBackupOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RestoreTableBackup, (
+ const TBackupManifestPtr& manifest,
+ const TRestoreTableBackupOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (
+ const NYPath::TYPath& path,
+ const NTableClient::TNameTablePtr& nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TGetInSyncReplicasOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<NTabletClient::TTableReplicaId>>, GetInSyncReplicas, (
+ const NYPath::TYPath& path,
+ const TGetInSyncReplicasOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<TTabletInfo>>, GetTabletInfos, (
+ const NYPath::TYPath& path,
+ const std::vector<int>& tabletIndexes,
+ const TGetTabletInfosOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TGetTabletErrorsResult>, GetTabletErrors, (
+ const NYPath::TYPath& path,
+ const TGetTabletErrorsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<NTabletClient::TTabletActionId>>, BalanceTabletCells, (
+ const TString& tabletCellBundle,
+ const std::vector<NYPath::TYPath>& movableTables,
+ const TBalanceTabletCellsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NChaosClient::TReplicationCardPtr>, GetReplicationCard, (
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TGetReplicationCardOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UpdateChaosTableReplicaProgress, (
+ NChaosClient::TReplicaId replicaId,
+ const TUpdateChaosTableReplicaProgressOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AlterReplicationCard, (
+ NChaosClient::TReplicationCardId replicationCardId,
+ const TAlterReplicationCardOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TSkynetSharePartsLocationsPtr>, LocateSkynetShare, (
+ const NYPath::TRichYPath& path,
+ const TLocateSkynetShareOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<NTableClient::TColumnarStatistics>>, GetColumnarStatistics, (
+ const std::vector<NYPath::TRichYPath>& path,
+ const TGetColumnarStatisticsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TMultiTablePartitions>, PartitionTables, (
+ const std::vector<NYPath::TRichYPath>& paths,
+ const TPartitionTablesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, TruncateJournal, (
+ const NYPath::TYPath& path,
+ i64 rowCount,
+ const TTruncateJournalOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TGetFileFromCacheResult>, GetFileFromCache, (
+ const TString& md5,
+ const TGetFileFromCacheOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TPutFileToCacheResult>, PutFileToCache, (
+ const NYPath::TYPath& path,
+ const TString& expectedMD5,
+ const TPutFileToCacheOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AddMember, (
+ const TString& group,
+ const TString& member,
+ const TAddMemberOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RemoveMember, (
+ const TString& group,
+ const TString& member,
+ const TRemoveMemberOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TCheckPermissionResponse>, CheckPermission, (
+ const TString& user,
+ const NYPath::TYPath& path,
+ NYTree::EPermission permission,
+ const TCheckPermissionOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TCheckPermissionByAclResult>, CheckPermissionByAcl, (
+ const std::optional<TString>& user,
+ NYTree::EPermission permission,
+ NYTree::INodePtr acl,
+ const TCheckPermissionByAclOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, TransferAccountResources, (
+ const TString& srcAccount,
+ const TString& dstAccount,
+ NYTree::INodePtr resourceDelta,
+ const TTransferAccountResourcesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, TransferPoolResources, (
+ const TString& srcPool,
+ const TString& dstPool,
+ const TString& poolTree,
+ NYTree::INodePtr resourceDelta,
+ const TTransferPoolResourcesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NScheduler::TOperationId>, StartOperation, (
+ NScheduler::EOperationType type,
+ const NYson::TYsonString& spec,
+ const TStartOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AbortOperation, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TAbortOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SuspendOperation, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TSuspendOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ResumeOperation, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TResumeOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, CompleteOperation, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TCompleteOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UpdateOperationParameters, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const NYson::TYsonString& parameters,
+ const TUpdateOperationParametersOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TOperation>, GetOperation, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TGetOperationOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, DumpJobContext, (
+ NJobTrackerClient::TJobId jobId,
+ const NYPath::TYPath& path,
+ const TDumpJobContextOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr>, GetJobInput, (
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetJobInputPaths, (
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobInputPathsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetJobSpec, (
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobSpecOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TSharedRef>, GetJobStderr, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobStderrOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TSharedRef>, GetJobFailContext, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobFailContextOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TListOperationsResult>, ListOperations, (
+ const TListOperationsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TListJobsResult>, ListJobs, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ const TListJobsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetJob, (
+ const NScheduler::TOperationIdOrAlias& operationIdOrAlias,
+ NJobTrackerClient::TJobId jobId,
+ const TGetJobOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AbandonJob, (
+ NJobTrackerClient::TJobId jobId,
+ const TAbandonJobOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TPollJobShellResponse>, PollJobShell, (
+ NJobTrackerClient::TJobId jobId,
+ const std::optional<TString>& shellName,
+ const NYson::TYsonString& parameters,
+ const TPollJobShellOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AbortJob, (
+ NJobTrackerClient::TJobId jobId,
+ const TAbortJobOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TClusterMeta>, GetClusterMeta, (
+ const TGetClusterMetaOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, CheckClusterLiveness, (
+ const TCheckClusterLivenessOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TDisableChunkLocationsResult>, DisableChunkLocations, (
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDisableChunkLocationsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TDestroyChunkLocationsResult>, DestroyChunkLocations, (
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TDestroyChunkLocationsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TResurrectChunkLocationsResult>, ResurrectChunkLocations, (
+ const TString& nodeAddress,
+ const std::vector<TGuid>& locationUuids,
+ const TResurrectChunkLocationsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SetUserPassword, (
+ const TString& user,
+ const TString& currentPasswordSha256,
+ const TString& newPasswordSha256,
+ const TSetUserPasswordOptions& options),
+ (override));
+
+ MOCK_METHOD(TFuture<TIssueTokenResult>, IssueToken, (
+ const TString& user,
+ const TString& passwordSha256,
+ const TIssueTokenOptions& options),
+ (override));
+
+ MOCK_METHOD(TFuture<void>, RevokeToken, (
+ const TString& user,
+ const TString& passwordSha256,
+ const TString& tokenSha256,
+ const TRevokeTokenOptions& options),
+ (override));
+
+ MOCK_METHOD(TFuture<TListUserTokensResult>, ListUserTokens, (
+ const TString& user,
+ const TString& passwordSha256,
+ const TListUserTokensOptions& options),
+ (override));
+
+ MOCK_METHOD(TFuture<NQueryTrackerClient::TQueryId>, StartQuery, (
+ NQueryTrackerClient::EQueryEngine engine,
+ const TString& query,
+ const TStartQueryOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AbortQuery, (
+ NQueryTrackerClient::TQueryId queryId,
+ const TAbortQueryOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TQueryResult>, GetQueryResult, (
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TGetQueryResultOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IUnversionedRowsetPtr>, ReadQueryResult, (
+ NQueryTrackerClient::TQueryId queryId,
+ i64 resultIndex,
+ const TReadQueryResultOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TQuery>, GetQuery, (
+ NQueryTrackerClient::TQueryId queryId,
+ const TGetQueryOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TListQueriesResult>, ListQueries, (
+ const TListQueriesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, AlterQuery, (
+ NQueryTrackerClient::TQueryId queryId, const TAlterQueryOptions& options), (override));
+};
+
+DEFINE_REFCOUNTED_TYPE(TMockClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/unittests/mock/connection.h b/yt/yt/client/unittests/mock/connection.h
new file mode 100644
index 0000000000..ff328081a2
--- /dev/null
+++ b/yt/yt/client/unittests/mock/connection.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <yt/yt/client/api/connection.h>
+
+#include <yt/yt/client/hive/transaction_participant.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockConnection
+ : public IConnection
+{
+public:
+ MOCK_METHOD(TClusterTag, GetClusterTag, (), (const, override));
+ MOCK_METHOD(const TString&, GetLoggingTag, (), (const, override));
+ MOCK_METHOD(const TString&, GetClusterId, (), (const, override));
+ MOCK_METHOD(const std::optional<TString>&, GetClusterName, (), (const, override));
+
+ MOCK_METHOD(IInvokerPtr, GetInvoker, (), (override));
+
+ MOCK_METHOD(bool, IsSameCluster, (const IConnectionPtr&), (const, override));
+
+ MOCK_METHOD(IClientPtr, CreateClient, (const TClientOptions& options), (override));
+
+ MOCK_METHOD(NHiveClient::ITransactionParticipantPtr, CreateTransactionParticipant,
+ (NHiveClient::TCellId,
+ const TTransactionParticipantOptions&), (override));
+
+ MOCK_METHOD(void, ClearMetadataCaches, (), (override));
+ MOCK_METHOD(void, Terminate, (), (override));
+
+ MOCK_METHOD(NYson::TYsonString, GetConfigYson, (), (const, override));
+};
+
+DEFINE_REFCOUNTED_TYPE(TMockConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/unittests/mock/table_value_consumer.h b/yt/yt/client/unittests/mock/table_value_consumer.h
new file mode 100644
index 0000000000..0ad0ab5fb5
--- /dev/null
+++ b/yt/yt/client/unittests/mock/table_value_consumer.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/value_consumer.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockValueConsumer
+ : public TValueConsumerBase
+{
+public:
+ TMockValueConsumer(
+ TNameTablePtr nameTable,
+ bool allowUnknownColumns,
+ TTableSchemaPtr schema = New<TTableSchema>(),
+ TTypeConversionConfigPtr typeConversionConfig = New<TTypeConversionConfig>())
+ : TValueConsumerBase(std::move(schema), std::move(typeConversionConfig))
+ , NameTable_(nameTable)
+ , AllowUnknowsColumns_(allowUnknownColumns)
+ {
+ InitializeIdToTypeMapping();
+ }
+
+ MOCK_METHOD(void, OnBeginRow, (), (override));
+ MOCK_METHOD(void, OnMyValue, (const TUnversionedValue& value), (override));
+ MOCK_METHOD(void, OnEndRow, (), (override));
+
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return NameTable_;
+ }
+
+ bool GetAllowUnknownColumns() const override
+ {
+ return AllowUnknowsColumns_;
+ }
+
+private:
+ TNameTablePtr NameTable_;
+ bool AllowUnknowsColumns_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/mock/transaction.h b/yt/yt/client/unittests/mock/transaction.h
new file mode 100644
index 0000000000..d22136846d
--- /dev/null
+++ b/yt/yt/client/unittests/mock/transaction.h
@@ -0,0 +1,209 @@
+#pragma once
+
+#include <yt/yt/client/api/file_writer.h>
+#include <yt/yt/client/api/journal_reader.h>
+#include <yt/yt/client/api/journal_writer.h>
+#include <yt/yt/client/api/transaction.h>
+
+#include <library/cpp/testing/gtest_extensions/gtest_extensions.h>
+
+namespace NYT::NApi {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockTransaction
+ : public ITransaction
+{
+public:
+ MOCK_METHOD(IConnectionPtr, GetConnection, (), (override));
+
+ MOCK_METHOD(TFuture<ITransactionPtr>, StartTransaction, (
+ NTransactionClient::ETransactionType type,
+ const TTransactionStartOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IUnversionedRowsetPtr>, LookupRows, (
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TLookupRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IVersionedRowsetPtr>, VersionedLookupRows, (
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ const TSharedRange<NTableClient::TLegacyKey>& keys,
+ const TVersionedLookupRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<std::vector<IUnversionedRowsetPtr>>, MultiLookup, (
+ const std::vector<TMultiLookupSubrequest>& subrequests,
+ const TMultiLookupOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TSelectRowsResult>, SelectRows, (
+ const TString& query,
+ const TSelectRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, ExplainQuery, (
+ const TString& query,
+ const TExplainQueryOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TPullRowsResult>, PullRows, (
+ const NYPath::TYPath& path,
+ const TPullRowsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<ITableReaderPtr>, CreateTableReader, (
+ const NYPath::TRichYPath& path,
+ const TTableReaderOptions& options), (override));
+
+ MOCK_METHOD(TFuture<ITableWriterPtr>, CreateTableWriter, (
+ const NYPath::TRichYPath& path,
+ const TTableWriterOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, GetNode, (
+ const NYPath::TYPath& path,
+ const TGetNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, SetNode, (
+ const NYPath::TYPath& path,
+ const NYson::TYsonString& value,
+ const TSetNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, MultisetAttributesNode, (
+ const NYPath::TYPath& path,
+ const NYTree::IMapNodePtr& attributes,
+ const TMultisetAttributesNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, RemoveNode, (
+ const NYPath::TYPath& path,
+ const TRemoveNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NYson::TYsonString>, ListNode, (
+ const NYPath::TYPath& path,
+ const TListNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, CreateNode, (
+ const NYPath::TYPath& path,
+ NObjectClient::EObjectType type,
+ const TCreateNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<TLockNodeResult>, LockNode, (
+ const NYPath::TYPath& path,
+ NCypressClient::ELockMode mode,
+ const TLockNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, UnlockNode, (
+ const NYPath::TYPath& path,
+ const TUnlockNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, CopyNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TCopyNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, MoveNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TMoveNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NCypressClient::TNodeId>, LinkNode, (
+ const NYPath::TYPath& srcPath,
+ const NYPath::TYPath& dstPath,
+ const TLinkNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ConcatenateNodes, (
+ const std::vector<NYPath::TRichYPath>& srcPaths,
+ const NYPath::TRichYPath& dstPath,
+ const TConcatenateNodesOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, ExternalizeNode, (
+ const NYPath::TYPath& path,
+ NObjectClient::TCellTag cellTag,
+ const TExternalizeNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<void>, InternalizeNode, (
+ const NYPath::TYPath& path,
+ const TInternalizeNodeOptions& options), (override));
+
+ MOCK_METHOD(TFuture<bool>, NodeExists, (
+ const NYPath::TYPath& path,
+ const TNodeExistsOptions& options), (override));
+
+ MOCK_METHOD(TFuture<NObjectClient::TObjectId>, CreateObject, (
+ NObjectClient::EObjectType type,
+ const TCreateObjectOptions& options), (override));
+
+ MOCK_METHOD(TFuture<IFileReaderPtr>, CreateFileReader, (
+ const NYPath::TYPath& path,
+ const TFileReaderOptions& options), (override));
+
+ MOCK_METHOD(IFileWriterPtr, CreateFileWriter, (
+ const NYPath::TRichYPath& path,
+ const TFileWriterOptions& options), (override));
+
+ MOCK_METHOD(IJournalReaderPtr, CreateJournalReader, (
+ const NYPath::TYPath& path,
+ const TJournalReaderOptions& options), (override));
+
+ MOCK_METHOD(IJournalWriterPtr, CreateJournalWriter, (
+ const NYPath::TYPath& path,
+ const TJournalWriterOptions& options), (override));
+
+ // ITransaction
+ IClientPtr Client;
+ NTransactionClient::ETransactionType Type;
+ NTransactionClient::TTransactionId Id;
+ NTransactionClient::TTimestamp StartTimestamp;
+ NTransactionClient::EAtomicity Atomicity;
+ NTransactionClient::EDurability Durability;
+ TDuration Timeout;
+
+ IClientPtr GetClient() const override
+ {
+ return Client;
+ }
+ NTransactionClient::ETransactionType GetType() const override
+ {
+ return Type;
+ }
+ MOCK_METHOD(NTransactionClient::TTransactionId, GetId, (), (const, override));
+
+ NTransactionClient::TTimestamp GetStartTimestamp() const override
+ {
+ return StartTimestamp;
+ }
+ NTransactionClient::EAtomicity GetAtomicity() const override
+ {
+ return Atomicity;
+ }
+ NTransactionClient::EDurability GetDurability() const override
+ {
+ return Durability;
+ }
+ TDuration GetTimeout() const override
+ {
+ return Timeout;
+ }
+
+ MOCK_METHOD(TFuture<void>, Ping, (const NApi::TTransactionPingOptions& options), (override));
+ MOCK_METHOD(TFuture<TTransactionCommitResult>, Commit, (const TTransactionCommitOptions& options), (override));
+ MOCK_METHOD(TFuture<void>, Abort, (const TTransactionAbortOptions& options), (override));
+ MOCK_METHOD(void, Detach, (), (override));
+ MOCK_METHOD(TFuture<TTransactionFlushResult>, Flush, (), (override));
+
+ MOCK_METHOD(void, RegisterAlienTransaction, (const NApi::ITransactionPtr& transaction), (override));
+
+ MOCK_METHOD(void, SubscribeCommitted, (const TCommittedHandler& callback), (override));
+ MOCK_METHOD(void, UnsubscribeCommitted, (const TCommittedHandler& callback), (override));
+ MOCK_METHOD(void, SubscribeAborted, (const TAbortedHandler& callback), (override));
+ MOCK_METHOD(void, UnsubscribeAborted, (const TAbortedHandler& callback), (override));
+
+ MOCK_METHOD(void, ModifyRows, (
+ const NYPath::TYPath& path,
+ NTableClient::TNameTablePtr nameTable,
+ TSharedRange<TRowModification> modifications,
+ const TModifyRowsOptions& options), (override));
+};
+
+DEFINE_REFCOUNTED_TYPE(TMockTransaction)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NApi
diff --git a/yt/yt/client/unittests/mock/ya.make b/yt/yt/client/unittests/mock/ya.make
new file mode 100644
index 0000000000..483405dac0
--- /dev/null
+++ b/yt/yt/client/unittests/mock/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ client.h
+ connection.h
+ transaction.h
+ table_value_consumer.h
+)
+
+PEERDIR(
+ library/cpp/testing/gtest_extensions
+ yt/yt/build
+ yt/yt/core
+)
+
+END()
diff --git a/yt/yt/client/unittests/named_yson_token_ut.cpp b/yt/yt/client/unittests/named_yson_token_ut.cpp
new file mode 100644
index 0000000000..c330595c62
--- /dev/null
+++ b/yt/yt/client/unittests/named_yson_token_ut.cpp
@@ -0,0 +1,301 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/complex_types/yson_format_conversion.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/parser.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NComplexTypes {
+
+using NFormats::EComplexTypeMode;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const auto KeyValueStruct = StructLogicalType({
+ {"key", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+});
+
+const auto IntStringVariant = VariantStructLogicalType({
+ {"int", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"string", SimpleLogicalType(ESimpleLogicalValueType::String)},
+});
+
+thread_local TYsonConverterConfig PositionalToNamedConfigInstance;
+
+class TWithConfig
+{
+public:
+ TWithConfig(const TYsonConverterConfig& config)
+ : OldConfig_(PositionalToNamedConfigInstance)
+ {
+ PositionalToNamedConfigInstance = config;
+ }
+
+ ~TWithConfig()
+ {
+ PositionalToNamedConfigInstance = OldConfig_;
+ }
+private:
+ TYsonConverterConfig OldConfig_;
+};
+
+TString CanonizeYson(TStringBuf yson)
+{
+ TString result;
+ {
+ TStringOutput out(result);
+ TYsonWriter writer(&out, EYsonFormat::Pretty, EYsonType::Node);
+ ParseYsonStringBuffer(yson, EYsonType::Node, &writer);
+ }
+ return result;
+}
+
+TString ConvertYson(
+ bool namedToPositional,
+ const TLogicalTypePtr& type,
+ TStringBuf sourceYson)
+{
+ TComplexTypeFieldDescriptor descriptor("<test-field>", type);
+ std::variant<TYsonServerToClientConverter, TYsonClientToServerConverter> converter;
+ try {
+ if (namedToPositional) {
+ TYsonConverterConfig config{
+ .ComplexTypeMode = EComplexTypeMode::Named,
+ };
+ converter = CreateYsonClientToServerConverter(descriptor, config);
+ } else {
+ converter = CreateYsonServerToClientConverter(descriptor, PositionalToNamedConfigInstance);
+ }
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "cannot create converter: " << ex.what();
+ return "";
+ }
+ TString convertedYson;
+ Visit(
+ converter,
+ [&] (const TYsonServerToClientConverter& serverToClientConverter) {
+ if (serverToClientConverter) {
+ TStringOutput out(convertedYson);
+ TYsonWriter writer(&out, EYsonFormat::Pretty);
+ serverToClientConverter(MakeUnversionedStringValue(sourceYson), &writer);
+ } else {
+ convertedYson = CanonizeYson(sourceYson);
+ }
+ },
+ [&] (const TYsonClientToServerConverter& clientToServerConverter) {
+ if (clientToServerConverter) {
+ auto value = clientToServerConverter(MakeUnversionedStringValue(sourceYson));
+ convertedYson = value.AsStringBuf();
+ } else {
+ convertedYson = CanonizeYson(sourceYson);
+ }
+ });
+ return convertedYson;
+}
+
+void CheckYsonConversion(
+ bool namedToPositional,
+ const TLogicalTypePtr& type,
+ TStringBuf sourceYson,
+ TStringBuf expectedConvertedYson)
+{
+ TString convertedYson;
+ try {
+ convertedYson = ConvertYson(namedToPositional, type, sourceYson);
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "conversion error: " << ex.what();
+ return;
+ }
+
+ EXPECT_EQ(CanonizeYson(convertedYson), CanonizeYson(expectedConvertedYson));
+}
+
+#define CHECK_POSITIONAL_TO_NAMED(type, positionalYson, namedYson) \
+ do { \
+ SCOPED_TRACE("positional -> named error"); \
+ CheckYsonConversion(false, type, positionalYson, namedYson); \
+ } while (0)
+
+#define CHECK_NAMED_TO_POSITIONAL(type, namedYson, positionalYson) \
+ do { \
+ SCOPED_TRACE("named -> positional error"); \
+ CheckYsonConversion(true, type, namedYson, positionalYson); \
+ } while (0)
+
+#define CHECK_NAMED_TO_POSITIONAL_THROWS(type, namedYson, exceptionSubstring) \
+ do { \
+ TString tmp; \
+ EXPECT_THROW_WITH_SUBSTRING(ConvertYson(true, type, namedYson), exceptionSubstring); \
+ } while (0)
+
+#define CHECK_POSITIONAL_TO_NAMED_THROWS(type, namedYson, exceptionSubstring) \
+ do { \
+ TString tmp; \
+ EXPECT_THROW_WITH_SUBSTRING(ConvertYson(false, type, namedYson), exceptionSubstring); \
+ } while (0)
+
+#define CHECK_BIDIRECTIONAL(type, positionalYson, namedYson) \
+ do { \
+ CHECK_POSITIONAL_TO_NAMED(type, positionalYson, namedYson); \
+ CHECK_NAMED_TO_POSITIONAL(type, namedYson, positionalYson); \
+ } while (0)
+
+TEST(TNamedPositionalYsonConverter, TestSimpleTypes)
+{
+ CHECK_BIDIRECTIONAL(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ "-42",
+ "-42");
+
+ CHECK_BIDIRECTIONAL(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ "foo",
+ "foo");
+}
+
+TEST(TNamedPositionalYsonConverter, TestStruct)
+{
+ CHECK_BIDIRECTIONAL(KeyValueStruct, "[foo; bar]", "{key=foo; value=bar}");
+ CHECK_BIDIRECTIONAL(KeyValueStruct, "[qux; #]", "{key=qux; value=#}");
+
+ CHECK_POSITIONAL_TO_NAMED(KeyValueStruct, "[baz]", "{key=baz; value=#}");
+ CHECK_NAMED_TO_POSITIONAL(KeyValueStruct, "{key=baz}", "[baz; #]");
+
+ CHECK_NAMED_TO_POSITIONAL_THROWS(KeyValueStruct, "{}", "is missing while parsing");
+ CHECK_NAMED_TO_POSITIONAL_THROWS(KeyValueStruct, "{value=baz}", "is missing while parsing");
+}
+
+TEST(TNamedPositionalYsonConverter, TestStructSkipNullValues)
+{
+ TYsonConverterConfig config{
+ .SkipNullValues = true,
+ };
+ TWithConfig g(config);
+
+ CHECK_POSITIONAL_TO_NAMED(KeyValueStruct, "[foo; bar]", "{key=foo; value=bar}");
+ CHECK_POSITIONAL_TO_NAMED(KeyValueStruct, "[foo; #]", "{key=foo}");
+ CHECK_POSITIONAL_TO_NAMED(KeyValueStruct, "[foo]", "{key=foo}");
+
+ auto type2 = StructLogicalType({
+ {"opt_int", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"opt_opt_int", OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64)))},
+ {"list_null", ListLogicalType(NullLogicalType())},
+ {"null", NullLogicalType()},
+ });
+ CHECK_POSITIONAL_TO_NAMED(type2, "[42; [#]; []; #]", "{opt_int=42; opt_opt_int=[#]; list_null=[]}");
+ CHECK_POSITIONAL_TO_NAMED(type2, "[#; #; [#; #;]]", "{list_null=[#; #;]}");
+}
+
+TEST(TNamedPositionalYsonConverter, TestVariantStruct)
+{
+ CHECK_BIDIRECTIONAL(IntStringVariant, "[0; 42]", "[int; 42]");
+ CHECK_BIDIRECTIONAL(IntStringVariant, "[1; foo]", "[string; foo]");
+
+ CHECK_NAMED_TO_POSITIONAL_THROWS(IntStringVariant, "[str; foo]", "Unknown variant field");
+}
+
+TEST(TNamedPositionalYsonConverter, TestOptional)
+{
+ CHECK_BIDIRECTIONAL(
+ OptionalLogicalType(KeyValueStruct),
+ "[foo; bar]",
+ "{key=foo; value=bar}");
+
+ CHECK_BIDIRECTIONAL(
+ OptionalLogicalType(KeyValueStruct),
+ "#",
+ "#");
+
+ CHECK_BIDIRECTIONAL(
+ OptionalLogicalType(OptionalLogicalType(KeyValueStruct)),
+ "[[foo; bar]]",
+ "[{key=foo; value=bar}]");
+
+ CHECK_BIDIRECTIONAL(
+ OptionalLogicalType(OptionalLogicalType(KeyValueStruct)),
+ "#",
+ "#");
+
+ CHECK_BIDIRECTIONAL(
+ OptionalLogicalType(OptionalLogicalType(KeyValueStruct)),
+ "[#]",
+ "[#]");
+}
+
+TEST(TNamedPositionalYsonConverter, TestList)
+{
+ CHECK_BIDIRECTIONAL(
+ ListLogicalType(KeyValueStruct),
+ "[[foo; bar]; [qux; #]]",
+ "[{key=foo; value=bar}; {key=qux; value=#};]");
+}
+
+TEST(TNamedPositionalYsonConverter, TestTuple)
+{
+ CHECK_BIDIRECTIONAL(
+ TupleLogicalType({KeyValueStruct, IntStringVariant, SimpleLogicalType(ESimpleLogicalValueType::Utf8)}),
+ "[[foo; bar]; [0; 5]; foo]",
+ "[{key=foo; value=bar}; [int; 5]; foo;]");
+}
+
+TEST(TNamedPositionalYsonConverter, TestVariantTuple)
+{
+ auto type = VariantTupleLogicalType({KeyValueStruct, IntStringVariant, SimpleLogicalType(ESimpleLogicalValueType::Utf8)});
+ CHECK_BIDIRECTIONAL(
+ type,
+ "[0; [foo; #]]",
+ "[0; {key=foo; value=#}]");
+
+ CHECK_BIDIRECTIONAL(
+ type,
+ "[1; [1; bar]]",
+ "[1; [string; bar]]");
+
+ CHECK_BIDIRECTIONAL(
+ type,
+ "[2; qux]",
+ "[2; qux]");
+}
+
+TEST(TNamedPositionalYsonConverter, TestDict)
+{
+ CHECK_BIDIRECTIONAL(
+ DictLogicalType(KeyValueStruct, IntStringVariant),
+ "[ [[foo; #]; [0; 0]] ; [[bar; qux;]; [1; baz;]]; ]",
+ "[ [{key=foo; value=#}; [int; 0]] ; [{key=bar; value=qux;}; [string; baz;]] ]");
+}
+
+TEST(TNamedPositionalYsonConverter, TestStringDictAsYsonMap)
+{
+ TYsonConverterConfig config{
+ .StringKeyedDictMode = NFormats::EDictMode::Named,
+ };
+ TWithConfig g(config);
+
+ CHECK_POSITIONAL_TO_NAMED(
+ DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ "[[key1; 1]; [key2; 2]]", "{key1=1; key2=2}");
+}
+
+TEST(TNamedPositionalYsonConverter, TestTagged)
+{
+ CHECK_BIDIRECTIONAL(
+ TaggedLogicalType("foo", KeyValueStruct),
+ "[foo; bar]",
+ "{key=foo; value=bar}");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/unittests/node_directory_ut.cpp b/yt/yt/client/unittests/node_directory_ut.cpp
new file mode 100644
index 0000000000..f1842b1491
--- /dev/null
+++ b/yt/yt/client/unittests/node_directory_ut.cpp
@@ -0,0 +1,33 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/client/node_tracker_client/node_directory.h>
+
+namespace NYT::NNodeTrackerClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TGetAddress, Test)
+{
+ const TAddressMap map{{"ipv4", "127.0.0.1:8081"}, {"ipv6", "::1:8081"}, {"default", "localhost:8081"}};
+
+ EXPECT_EQ("::1:8081", GetAddressOrThrow(map, { "ipv6", "ipv4" }));
+ EXPECT_EQ("127.0.0.1:8081", GetAddressOrThrow(map, { "ipv4", "ipv6" }));
+ EXPECT_THROW(GetAddressOrThrow(map, { "wrong" }), TErrorException);
+}
+
+TEST(TFindAddress, Test)
+{
+ const TAddressMap map{{"ipv4", "127.0.0.1:8081"}, {"ipv6", "::1:8081"}, {"default", "localhost:8081"}};
+
+ EXPECT_EQ("::1:8081", *FindAddress(map, {"ipv6", "ipv4"}));
+ EXPECT_EQ("127.0.0.1:8081", *FindAddress(map, {"ipv4", "ipv6"}));
+ EXPECT_FALSE(FindAddress(map, {"wrong"}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NNodeTrackerClient
+
diff --git a/yt/yt/client/unittests/protobuf_format_ut.cpp b/yt/yt/client/unittests/protobuf_format_ut.cpp
new file mode 100644
index 0000000000..af9d2a0155
--- /dev/null
+++ b/yt/yt/client/unittests/protobuf_format_ut.cpp
@@ -0,0 +1,4657 @@
+#include "row_helpers.h"
+#include "yson_helpers.h"
+#include "yt/yt/client/table_client/public.h"
+
+#include <yt/yt/client/unittests/protobuf_format_ut.pb.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/json/json_parser.h>
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/client/formats/config.h>
+#include <yt/yt/client/formats/parser.h>
+#include <yt/yt/client/formats/lenval_control_constants.h>
+#include <yt/yt/client/formats/protobuf_writer.h>
+#include <yt/yt/client/formats/protobuf_parser.h>
+#include <yt/yt/client/formats/protobuf.h>
+#include <yt/yt/client/formats/format.h>
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/value_consumer.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/library/named_value/named_value.h>
+
+#include <util/random/fast.h>
+
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+
+using namespace std::string_view_literals;
+
+
+namespace NYT {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NFormats;
+using namespace NTableClient;
+using namespace NConcurrency;
+using namespace NProtobufFormatTest;
+
+using ::google::protobuf::FileDescriptor;
+using NNamedValue::MakeRow;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EProtoFormatType,
+ (FileDescriptorLegacy)
+ (FileDescriptor)
+ (Structured)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define EXPECT_NODES_EQUAL(a, b) \
+ EXPECT_TRUE(AreNodesEqual((a), (b))) \
+ << #a ": " << ConvertToYsonString((a), EYsonFormat::Text).ToString() \
+ << "\n\n" #b ": " << ConvertToYsonString((b), EYsonFormat::Text).ToString();
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ConvertToTextYson(const INodePtr& node)
+{
+ return ConvertToYsonString(node, EYsonFormat::Text).ToString();
+}
+
+// Hardcoded serialization of file descriptor used in old format description.
+TString FileDescriptorLegacy = "\x0a\xb6\x03\x0a\x29\x6a\x75\x6e\x6b\x2f\x65\x72\x6d\x6f\x6c\x6f\x76\x64\x2f\x74\x65\x73\x74\x2d\x70\x72\x6f\x74\x6f\x62"
+ "\x75\x66\x2f\x6d\x65\x73\x73\x61\x67\x65\x2e\x70\x72\x6f\x74\x6f\x22\x2d\x0a\x0f\x54\x45\x6d\x62\x65\x64\x65\x64\x4d\x65\x73\x73\x61\x67\x65\x12"
+ "\x0b\x0a\x03\x4b\x65\x79\x18\x01\x20\x01\x28\x09\x12\x0d\x0a\x05\x56\x61\x6c\x75\x65\x18\x02\x20\x01\x28\x09\x22\xb3\x02\x0a\x08\x54\x4d\x65\x73"
+ "\x73\x61\x67\x65\x12\x0e\x0a\x06\x44\x6f\x75\x62\x6c\x65\x18\x01\x20\x01\x28\x01\x12\x0d\x0a\x05\x46\x6c\x6f\x61\x74\x18\x02\x20\x01\x28\x02\x12"
+ "\x0d\x0a\x05\x49\x6e\x74\x36\x34\x18\x03\x20\x01\x28\x03\x12\x0e\x0a\x06\x55\x49\x6e\x74\x36\x34\x18\x04\x20\x01\x28\x04\x12\x0e\x0a\x06\x53\x49"
+ "\x6e\x74\x36\x34\x18\x05\x20\x01\x28\x12\x12\x0f\x0a\x07\x46\x69\x78\x65\x64\x36\x34\x18\x06\x20\x01\x28\x06\x12\x10\x0a\x08\x53\x46\x69\x78\x65"
+ "\x64\x36\x34\x18\x07\x20\x01\x28\x10\x12\x0d\x0a\x05\x49\x6e\x74\x33\x32\x18\x08\x20\x01\x28\x05\x12\x0e\x0a\x06\x55\x49\x6e\x74\x33\x32\x18\x09"
+ "\x20\x01\x28\x0d\x12\x0e\x0a\x06\x53\x49\x6e\x74\x33\x32\x18\x0a\x20\x01\x28\x11\x12\x0f\x0a\x07\x46\x69\x78\x65\x64\x33\x32\x18\x0b\x20\x01\x28"
+ "\x07\x12\x10\x0a\x08\x53\x46\x69\x78\x65\x64\x33\x32\x18\x0c\x20\x01\x28\x0f\x12\x0c\x0a\x04\x42\x6f\x6f\x6c\x18\x0d\x20\x01\x28\x08\x12\x0e\x0a"
+ "\x06\x53\x74\x72\x69\x6e\x67\x18\x0e\x20\x01\x28\x09\x12\x0d\x0a\x05\x42\x79\x74\x65\x73\x18\x0f\x20\x01\x28\x0c\x12\x14\x0a\x04\x45\x6e\x75\x6d"
+ "\x18\x10\x20\x01\x28\x0e\x32\x06\x2e\x45\x45\x6e\x75\x6d\x12\x21\x0a\x07\x4d\x65\x73\x73\x61\x67\x65\x18\x11\x20\x01\x28\x0b\x32\x10\x2e\x54\x45"
+ "\x6d\x62\x65\x64\x65\x64\x4d\x65\x73\x73\x61\x67\x65\x2a\x24\x0a\x05\x45\x45\x6e\x75\x6d\x12\x07\x0a\x03\x4f\x6e\x65\x10\x01\x12\x07\x0a\x03\x54"
+ "\x77\x6f\x10\x02\x12\x09\x0a\x05\x54\x68\x72\x65\x65\x10\x03";
+
+TString GenerateRandomLenvalString(TFastRng64& rng, ui32 size)
+{
+ TString result;
+ result.append(reinterpret_cast<const char*>(&size), sizeof(size));
+
+ size += sizeof(ui32);
+
+ while (result.size() < size) {
+ ui64 num = rng.GenRand();
+ result.append(reinterpret_cast<const char*>(&num), sizeof(num));
+ }
+ if (result.size() > size) {
+ result.resize(size);
+ }
+ return result;
+}
+
+static TProtobufFormatConfigPtr MakeProtobufFormatConfig(const std::vector<const ::google::protobuf::Descriptor*>& descriptorList)
+{
+ ::google::protobuf::FileDescriptorSet fileDescriptorSet;
+ THashSet<const ::google::protobuf::FileDescriptor*> files;
+
+ std::function<void(const ::google::protobuf::FileDescriptor*)> addFile;
+ addFile = [&] (const ::google::protobuf::FileDescriptor* fileDescriptor) {
+ if (!files.insert(fileDescriptor).second) {
+ return;
+ }
+
+ // N.B. We want to write dependencies in fileDescriptorSet in topological order
+ // so we traverse dependencies first and the add current fileDescriptor.
+ for (int i = 0; i < fileDescriptor->dependency_count(); ++i) {
+ addFile(fileDescriptor->dependency(i));
+ }
+ fileDescriptor->CopyTo(fileDescriptorSet.add_file());
+ };
+ std::vector<TString> typeNames;
+
+ for (const auto* descriptor : descriptorList) {
+ addFile(descriptor->file());
+ typeNames.push_back(descriptor->full_name());
+ }
+
+ auto formatConfigYsonString = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("file_descriptor_set_text").Value(fileDescriptorSet.ShortDebugString())
+ .Item("type_names").Value(typeNames)
+ .EndMap();
+
+ return ConvertTo<TProtobufFormatConfigPtr>(formatConfigYsonString);
+}
+
+INodePtr ParseYson(TStringBuf data)
+{
+ return ConvertToNode(NYson::TYsonString(TString{data}));
+}
+
+TString LenvalBytes(const ::google::protobuf::Message& message)
+{
+ TStringStream out;
+ ui32 messageSize = static_cast<ui32>(message.ByteSizeLong());
+ out.Write(&messageSize, sizeof(messageSize));
+ if (!message.SerializeToArcadiaStream(&out)) {
+ THROW_ERROR_EXCEPTION("Can not serialize message");
+ }
+ return out.Str();
+}
+
+void EnsureTypesMatch(EValueType expected, EValueType actual)
+{
+ if (expected != actual) {
+ THROW_ERROR_EXCEPTION("Mismatching type: expected %Qlv, actual %Qlv",
+ expected,
+ actual);
+ }
+}
+
+double GetDouble(const TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Double, row.Type);
+ return row.Data.Double;
+}
+
+template <typename TMessage>
+TCollectingValueConsumer ParseRows(
+ const TMessage& message,
+ const TProtobufFormatConfigPtr& config,
+ const TTableSchemaPtr& schema = New<TTableSchema>(),
+ int count = 1)
+{
+ TString lenvalBytes;
+ TStringOutput out(lenvalBytes);
+ auto messageSize = static_cast<ui32>(message.ByteSize());
+ for (int i = 0; i < count; ++i) {
+ out.Write(&messageSize, sizeof(messageSize));
+ if (!message.SerializeToArcadiaStream(&out)) {
+ THROW_ERROR_EXCEPTION("Failed to serialize message");
+ }
+ }
+
+ TCollectingValueConsumer rowCollector(schema);
+ auto parser = CreateParserForProtobuf(&rowCollector, config, 0);
+ parser->Read(lenvalBytes);
+ parser->Finish();
+ if (static_cast<ssize_t>(rowCollector.Size()) != count) {
+ THROW_ERROR_EXCEPTION("rowCollector has wrong size: expected %v, actual %v",
+ count,
+ rowCollector.Size());
+ }
+ return rowCollector;
+}
+
+template <typename TMessage>
+TCollectingValueConsumer ParseRows(
+ const TMessage& message,
+ const INodePtr& config,
+ const TTableSchemaPtr& schema = New<TTableSchema>(),
+ int count = 1)
+{
+ return ParseRows(message, ConvertTo<TProtobufFormatConfigPtr>(config->Attributes().ToMap()), schema, count);
+}
+
+
+void AddDependencies(
+ const FileDescriptor* fileDescriptor,
+ std::vector<const FileDescriptor*>& fileDescriptors,
+ THashSet<const FileDescriptor*>& fileDescriptorSet)
+{
+ if (fileDescriptorSet.contains(fileDescriptor)) {
+ return;
+ }
+ fileDescriptorSet.insert(fileDescriptor);
+ for (int i = 0; i < fileDescriptor->dependency_count(); ++i) {
+ AddDependencies(fileDescriptor->dependency(i), fileDescriptors, fileDescriptorSet);
+ }
+ fileDescriptors.push_back(fileDescriptor);
+}
+
+template <typename ... Ts>
+INodePtr CreateFileDescriptorConfig(std::optional<EComplexTypeMode> complexTypeMode = {})
+{
+ std::vector<const FileDescriptor*> fileDescriptors;
+ THashSet<const FileDescriptor*> fileDescriptorSet;
+ std::vector<const FileDescriptor*> originalFileDescriptors = {Ts::descriptor()->file()...};
+
+ for (auto d : originalFileDescriptors) {
+ AddDependencies(d, fileDescriptors, fileDescriptorSet);
+ }
+
+ ::google::protobuf::FileDescriptorSet fileDescriptorSetProto;
+ for (auto fileDescriptor : fileDescriptors) {
+ fileDescriptor->CopyTo(fileDescriptorSetProto.add_file());
+ }
+ TString fileDescriptorSetText;
+ ::google::protobuf::TextFormat::Printer().PrintToString(fileDescriptorSetProto, &fileDescriptorSetText);
+ std::vector<TString> typeNames = {Ts::descriptor()->full_name()...};
+ return BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("file_descriptor_set_text").Value(fileDescriptorSetText)
+ .Item("type_names").Value(typeNames)
+ .OptionalItem("complex_type_mode", complexTypeMode)
+ .EndAttributes()
+ .Value("protobuf");
+}
+
+static const auto EnumerationsConfig = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("EEnum")
+ .BeginMap()
+ .Item("One").Value(1)
+ .Item("Two").Value(2)
+ .Item("Three").Value(3)
+ .Item("MinusFortyTwo").Value(-42)
+ .Item("MaxInt32").Value(std::numeric_limits<int>::max())
+ .Item("MinInt32").Value(std::numeric_limits<int>::min())
+ .EndMap()
+ .EndMap();
+
+INodePtr CreateAllFieldsConfig(EProtoFormatType protoFormatType)
+{
+ switch (protoFormatType) {
+ case EProtoFormatType::FileDescriptor:
+ return CreateFileDescriptorConfig<TMessage>();
+ case EProtoFormatType::FileDescriptorLegacy:
+ return BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("file_descriptor_set")
+ .Value(FileDescriptorLegacy)
+ .Item("file_indices")
+ .BeginList()
+ .Item().Value(0)
+ .EndList()
+ .Item("message_indices")
+ .BeginList()
+ .Item().Value(1)
+ .EndList()
+ .EndAttributes()
+ .Value("protobuf");
+ case EProtoFormatType::Structured:
+ return BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("enumerations").Value(EnumerationsConfig)
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Double")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("double")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Float")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("float")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Int64")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("UInt64")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SInt64")
+ .Item("field_number").Value(5)
+ .Item("proto_type").Value("sint64")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Fixed64")
+ .Item("field_number").Value(6)
+ .Item("proto_type").Value("fixed64")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SFixed64")
+ .Item("field_number").Value(7)
+ .Item("proto_type").Value("sfixed64")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Int32")
+ .Item("field_number").Value(8)
+ .Item("proto_type").Value("int32")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("UInt32")
+ .Item("field_number").Value(9)
+ .Item("proto_type").Value("uint32")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SInt32")
+ .Item("field_number").Value(10)
+ .Item("proto_type").Value("sint32")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Fixed32")
+ .Item("field_number").Value(11)
+ .Item("proto_type").Value("fixed32")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SFixed32")
+ .Item("field_number").Value(12)
+ .Item("proto_type").Value("sfixed32")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Bool")
+ .Item("field_number").Value(13)
+ .Item("proto_type").Value("bool")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("String")
+ .Item("field_number").Value(14)
+ .Item("proto_type").Value("string")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Bytes")
+ .Item("field_number").Value(15)
+ .Item("proto_type").Value("bytes")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Enum")
+ .Item("field_number").Value(16)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Message")
+ .Item("field_number").Value(17)
+ .Item("proto_type").Value("message")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("AnyWithMap")
+ .Item("field_number").Value(18)
+ .Item("proto_type").Value("any")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("AnyWithInt64")
+ .Item("field_number").Value(19)
+ .Item("proto_type").Value("any")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("AnyWithString")
+ .Item("field_number").Value(20)
+ .Item("proto_type").Value("any")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("OtherColumns")
+ .Item("field_number").Value(21)
+ .Item("proto_type").Value("other_columns")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("MissingInt64")
+ .Item("field_number").Value(22)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .Value("protobuf");
+ }
+ Y_FAIL();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLenvalEntry
+{
+ TString RowData;
+ ui32 TableIndex;
+ ui64 TabletIndex;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLenvalParser
+{
+public:
+ explicit TLenvalParser(IInputStream* input)
+ : Input_(input)
+ { }
+
+ explicit TLenvalParser(TStringBuf input)
+ : StreamHolder_(std::make_unique<TMemoryInput>(input))
+ , Input_(StreamHolder_.get())
+ { }
+
+ std::optional<TLenvalEntry> Next()
+ {
+ ui32 rowSize;
+ size_t read = Input_->Load(&rowSize, sizeof(rowSize));
+ if (read == 0) {
+ return std::nullopt;
+ } else if (read < sizeof(rowSize)) {
+ THROW_ERROR_EXCEPTION("corrupted lenval: can't read row length");
+ }
+ switch (rowSize) {
+ case LenvalTableIndexMarker: {
+ ui32 tableIndex;
+ read = Input_->Load(&tableIndex, sizeof(tableIndex));
+ if (read != sizeof(tableIndex)) {
+ THROW_ERROR_EXCEPTION("corrupted lenval: can't read table index");
+ }
+ CurrentTableIndex_ = tableIndex;
+ return Next();
+ }
+ case LenvalTabletIndexMarker: {
+ ui64 tabletIndex;
+ read = Input_->Load(&tabletIndex, sizeof(tabletIndex));
+ if (read != sizeof(tabletIndex)) {
+ THROW_ERROR_EXCEPTION("corrupted lenval: can't read tablet index");
+ }
+ CurrentTabletIndex_ = tabletIndex;
+ return Next();
+ }
+ case LenvalEndOfStream:
+ EndOfStream_ = true;
+ return std::nullopt;
+ case LenvalKeySwitch:
+ case LenvalRangeIndexMarker:
+ case LenvalRowIndexMarker:
+ THROW_ERROR_EXCEPTION("marker is unsupported");
+ default: {
+ TLenvalEntry result;
+ result.RowData.resize(rowSize);
+ result.TableIndex = CurrentTableIndex_;
+ result.TabletIndex = CurrentTabletIndex_;
+ Input_->Load(result.RowData.Detach(), rowSize);
+
+ return result;
+ }
+ }
+ }
+
+ bool IsEndOfStream() const
+ {
+ return EndOfStream_;
+ }
+
+private:
+ std::unique_ptr<IInputStream> StreamHolder_;
+ IInputStream* Input_;
+ ui32 CurrentTableIndex_ = 0;
+ ui64 CurrentTabletIndex_ = 0;
+ bool EndOfStream_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TProtobufFormatConfigPtr ParseAndValidateConfig(const INodePtr& node, std::vector<TTableSchemaPtr> schemas = {})
+{
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(node);
+ if (schemas.empty()) {
+ schemas.assign(config->Tables.size(), New<TTableSchema>());
+ }
+ New<TProtobufParserFormatDescription>()->Init(config, schemas);
+ New<TProtobufWriterFormatDescription>()->Init(config, schemas);
+ return config;
+}
+
+} // namespace
+
+INodePtr BuildEmbeddedConfig(EComplexTypeMode complexTypeMode, EProtoFormatType formatType) {
+ if (formatType == EProtoFormatType::FileDescriptor) {
+ return CreateFileDescriptorConfig<NYT::TEmbeddingMessage>(complexTypeMode);
+ }
+
+ auto config = BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("tables").BeginList()
+ .Item().BeginMap()
+ .Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("*")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("embedded_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("other_columns_field")
+ .Item("field_number").Value(15)
+ .Item("proto_type").Value("other_columns")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("embedded_num")
+ .Item("field_number").Value(10)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("embedded_extra_field")
+ .Item("field_number").Value(11)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("variant")
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("str_variant")
+ .Item("field_number").Value(101)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("uint_variant")
+ .Item("field_number").Value(102)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("*")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("embedded_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("embedded2_num")
+ .Item("field_number").Value(10)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("embedded2_struct")
+ .Item("field_number").Value(17)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("float1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("float")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("string1")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("embedded2_repeated")
+ .Item("field_number").Value(42)
+ .Item("proto_type").Value("string")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("num")
+ .Item("field_number").Value(12)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("extra_field")
+ .Item("field_number").Value(13)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .Item("complex_type_mode").Value(complexTypeMode)
+ .EndAttributes()
+ .Value("protobuf");
+ return config;
+}
+
+TTableSchemaPtr BuildEmbeddedSchema() {
+ auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"num", SimpleLogicalType(ESimpleLogicalValueType::Uint64)},
+ {"embedded_num", SimpleLogicalType(ESimpleLogicalValueType::Uint64)},
+ {"variant", VariantStructLogicalType({
+ {"str_variant", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"uint_variant", SimpleLogicalType(ESimpleLogicalValueType::Uint64)},
+ })},
+ {"extra_column", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64))},
+ {"embedded2_num", SimpleLogicalType(ESimpleLogicalValueType::Uint64)},
+ {"embedded2_struct", StructLogicalType({
+ {"float1", SimpleLogicalType(ESimpleLogicalValueType::Float)},
+ {"string1", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ })},
+ {"embedded2_repeated", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"other_complex_field", StructLogicalType({
+ {"one", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"two", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"three", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+ {"extra_int", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+
+ });
+ return schema;
+}
+
+TEST(TProtobufFormat, TestConfigParsingEmbedded) {
+ auto config = BuildEmbeddedConfig(EComplexTypeMode::Positional, EProtoFormatType::Structured);
+ auto schema = BuildEmbeddedSchema();
+
+ EXPECT_NO_THROW(
+ ParseAndValidateConfig(config->Attributes().ToMap(), {schema})
+ );
+}
+
+TEST(TProtobufFormat, TestConfigParsing)
+{
+ // Empty config.
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(ParseYson("{}")),
+ "one of \"tables\", \"file_descriptor_set\" and \"file_descriptor_set_text\" must be specified");
+
+ // Broken protobuf.
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(ParseYson(R"({file_descriptor_set="dfgxx"; file_indices=[0]; message_indices=[0]})")),
+ "Error parsing \"file_descriptor_set\" in protobuf config");
+
+ EXPECT_NO_THROW(ParseAndValidateConfig(
+ CreateAllFieldsConfig(EProtoFormatType::Structured)->Attributes().ToMap()
+ ));
+
+ EXPECT_NO_THROW(ParseAndValidateConfig(
+ CreateAllFieldsConfig(EProtoFormatType::FileDescriptorLegacy)->Attributes().ToMap()
+ ));
+
+ EXPECT_NO_THROW(ParseAndValidateConfig(
+ CreateAllFieldsConfig(EProtoFormatType::FileDescriptor)->Attributes().ToMap()
+ ));
+
+ auto embeddedInsideNonembeddedConfig = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables").BeginList()
+ .Item().BeginMap()
+ .Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("embedded_message1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("embedded_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("field1")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("embedded_message2")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("embedded_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("field2")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto schemaForEmbedded = New<TTableSchema>(std::vector{
+ TColumnSchema("field1", StructLogicalType({
+ {"embedded_message2", StructLogicalType({
+ {"field2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ })},
+ }))
+ });
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(embeddedInsideNonembeddedConfig, {schemaForEmbedded}),
+ "embedded_message inside of structured_message is not allowed");
+
+ auto repeatedEmbeddedConfig = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("*")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("embedded_message")
+ .Item("repeated").Value(true)
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("field1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(repeatedEmbeddedConfig),
+ R"(type "embedded_message" can not be repeated)");
+
+ auto multipleOtherColumnsConfig = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Other1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("other_columns")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Other2")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("other_columns")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(multipleOtherColumnsConfig),
+ "Multiple \"other_columns\" in protobuf config are not allowed");
+
+ auto duplicateColumnNamesConfig = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(duplicateColumnNamesConfig),
+ "Multiple fields with same column name \"SomeColumn\" are forbidden in protobuf format");
+
+ auto anyCorrespondsToStruct = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("any")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto schema = New<TTableSchema>(std::vector{
+ TColumnSchema("SomeColumn", StructLogicalType({})),
+ });
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(anyCorrespondsToStruct, {schema}),
+ "Table schema and protobuf format config mismatch");
+
+ auto configWithBytes = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("bytes")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto schemaWithUtf8 = New<TTableSchema>(std::vector{
+ TColumnSchema("SomeColumn", SimpleLogicalType(ESimpleLogicalValueType::Utf8)),
+ });
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(configWithBytes, {schemaWithUtf8}),
+ "mismatch: expected logical type to be one of");
+
+ auto configWithPackedNonRepeated = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .Item("packed").Value(true)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto schemaWithInt64List = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"SomeColumn", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ });
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(configWithPackedNonRepeated, {schemaWithInt64List}),
+ "Field \"SomeColumn\" is marked \"packed\" but is not marked \"repeated\"");
+
+ auto configWithPackedRepeatedString = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .Item("packed").Value(true)
+ .Item("repeated").Value(true)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto schemaWithStringList = New<TTableSchema>(std::vector{
+ TColumnSchema("SomeColumn", ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String)))
+ });
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(configWithPackedRepeatedString, {schemaWithStringList}),
+ "packed protobuf field must have primitive numeric type, got \"string\"");
+
+ auto configWithMissingFieldNumber = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("SomeColumn")
+ .Item("proto_type").Value("string")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseAndValidateConfig(configWithMissingFieldNumber, {schemaWithStringList}),
+ "\"field_number\" is required");
+}
+
+TEST(TProtobufFormat, TestParseBigZigZag)
+{
+ constexpr i32 value = Min<i32>();
+ TMessage message;
+ message.set_int32_field(value);
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(CreateAllFieldsConfig(EProtoFormatType::Structured)->Attributes().ToMap());
+ auto rowCollector = ParseRows(message, config);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(0, "Int32")), value);
+}
+
+TEST(TProtobufFormat, TestParseEnumerationString)
+{
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(CreateAllFieldsConfig(EProtoFormatType::Structured)->Attributes().ToMap());
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::One);
+ auto rowCollector = ParseRows(message, config);
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "Enum")), "One");
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::Two);
+ auto rowCollector = ParseRows(message, config);
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "Enum")), "Two");
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::Three);
+ auto rowCollector = ParseRows(message, config);
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "Enum")), "Three");
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::MinusFortyTwo);
+ auto rowCollector = ParseRows(message, config);
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "Enum")), "MinusFortyTwo");
+ }
+}
+
+TEST(TProtobufFormat, TestParseWrongEnumeration)
+{
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(CreateAllFieldsConfig(EProtoFormatType::Structured)->Attributes().ToMap());
+ TMessage message;
+ auto enumTag = TMessage::descriptor()->FindFieldByName("enum_field")->number();
+ message.mutable_unknown_fields()->AddVarint(enumTag, 30);
+ EXPECT_ANY_THROW(ParseRows(message, config));
+}
+
+TEST(TProtobufFormat, TestParseEnumerationInt)
+{
+ TCollectingValueConsumer rowCollector;
+
+ auto config = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Enum")
+ .Item("field_number").Value(16)
+ .Item("proto_type").Value("enum_int")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto parser = CreateParserForProtobuf(&rowCollector, ConvertTo<TProtobufFormatConfigPtr>(config), 0);
+
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::One);
+ parser->Read(LenvalBytes(message));
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::Two);
+ parser->Read(LenvalBytes(message));
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::Three);
+ parser->Read(LenvalBytes(message));
+ }
+ {
+ TMessage message;
+ message.set_enum_field(EEnum::MinusFortyTwo);
+ parser->Read(LenvalBytes(message));
+ }
+ {
+ TMessage message;
+ auto enumTag = TMessage::descriptor()->FindFieldByName("enum_field")->number();
+ message.mutable_unknown_fields()->AddVarint(enumTag, 100500);
+ parser->Read(LenvalBytes(message));
+ }
+
+ parser->Finish();
+
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(0, "Enum")), 1);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(1, "Enum")), 2);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(2, "Enum")), 3);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(3, "Enum")), -42);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(4, "Enum")), 100500);
+}
+
+TEST(TProtobufFormat, TestParseRandomGarbage)
+{
+ // Check that we never crash.
+
+ TFastRng64 rng(42);
+ for (int i = 0; i != 1000; ++i) {
+ auto bytes = GenerateRandomLenvalString(rng, 8);
+
+ TCollectingValueConsumer rowCollector;
+ auto parser = CreateParserForProtobuf(
+ &rowCollector,
+ ConvertTo<TProtobufFormatConfigPtr>(CreateAllFieldsConfig(EProtoFormatType::Structured)->Attributes().ToMap()),
+ 0);
+ try {
+ parser->Read(bytes);
+ parser->Finish();
+ } catch (...) {
+ }
+ }
+}
+
+TEST(TProtobufFormat, TestParseZeroColumns)
+{
+ auto config = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ TCollectingValueConsumer rowCollector;
+ auto parser = CreateParserForProtobuf(
+ &rowCollector,
+ ConvertTo<TProtobufFormatConfigPtr>(config),
+ 0);
+
+ // Empty lenval values.
+ parser->Read("\0\0\0\0"sv);
+ parser->Read("\0\0\0\0"sv);
+
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<ssize_t>(rowCollector.Size()), 2);
+ EXPECT_EQ(static_cast<int>(rowCollector.GetRow(0).GetCount()), 0);
+ EXPECT_EQ(static_cast<int>(rowCollector.GetRow(1).GetCount()), 0);
+}
+
+TEST(TProtobufFormat, TestWriteEnumerationString)
+{
+ auto config = CreateAllFieldsConfig(EProtoFormatType::Structured);
+
+ auto nameTable = New<TNameTable>();
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ config->Attributes(),
+ {New<TTableSchema>()},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"Enum", "MinusFortyTwo"}
+ }).Get()
+ });
+ writer->Write({
+ MakeRow(nameTable, {
+ {"Enum", "Three"},
+ }).Get()
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput si(result);
+ TLenvalParser parser(&si);
+ {
+ auto row = parser.Next();
+ ASSERT_TRUE(row);
+ NYT::TMessage message;
+ ASSERT_TRUE(message.ParseFromString(row->RowData));
+ ASSERT_EQ(message.enum_field(), NYT::EEnum::MinusFortyTwo);
+ }
+ {
+ auto row = parser.Next();
+ ASSERT_TRUE(row);
+ NYT::TMessage message;
+ ASSERT_TRUE(message.ParseFromString(row->RowData));
+ ASSERT_EQ(message.enum_field(), NYT::EEnum::Three);
+ }
+ {
+ auto row = parser.Next();
+ ASSERT_FALSE(row);
+ }
+}
+
+TEST(TProtobufFormat, TestWriteEnumerationInt)
+{
+ auto config = BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("Enum")
+ .Item("field_number").Value(16)
+ .Item("proto_type").Value("enum_int")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .Value("protobuf");
+
+ auto nameTable = New<TNameTable>();
+
+ auto writeAndParseRow = [&] (TUnversionedRow row, TMessage* message) {
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ config->Attributes(),
+ {New<TTableSchema>()},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+ writer->Write({row});
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput si(result);
+ TLenvalParser parser(&si);
+ auto protoRow = parser.Next();
+ ASSERT_TRUE(protoRow);
+
+ ASSERT_TRUE(message->ParseFromString(protoRow->RowData));
+
+ auto nextProtoRow = parser.Next();
+ ASSERT_FALSE(nextProtoRow);
+ };
+
+ {
+ TMessage message;
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", -42},
+ }).Get(),
+ &message);
+ ASSERT_EQ(message.enum_field(), EEnum::MinusFortyTwo);
+ }
+ {
+ TMessage message;
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", static_cast<ui64>(std::numeric_limits<i32>::max())},
+ }).Get(),
+ &message);
+ ASSERT_EQ(message.enum_field(), EEnum::MaxInt32);
+ }
+ {
+ TMessage message;
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", std::numeric_limits<i32>::max()},
+ }).Get(),
+ &message);
+ ASSERT_EQ(message.enum_field(), EEnum::MaxInt32);
+ }
+ {
+ TMessage message;
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", std::numeric_limits<i32>::min()},
+ }).Get(),
+ &message);
+ ASSERT_EQ(message.enum_field(), EEnum::MinInt32);
+ }
+
+ TMessage message;
+ ASSERT_THROW(
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", static_cast<i64>(std::numeric_limits<i32>::max()) + 1},
+ }).Get(),
+ &message),
+ TErrorException);
+
+ ASSERT_THROW(
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", static_cast<i64>(std::numeric_limits<i32>::min()) - 1},
+ }).Get(),
+ &message),
+ TErrorException);
+
+ ASSERT_THROW(
+ writeAndParseRow(
+ MakeRow(nameTable, {
+ {"Enum", static_cast<ui64>(std::numeric_limits<i32>::max()) + 1},
+ }).Get(),
+ &message),
+ TErrorException);
+}
+
+
+TEST(TProtobufFormat, TestWriteZeroColumns)
+{
+ auto config = BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .Value("protobuf");
+
+ auto nameTable = New<TNameTable>();
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ config->Attributes(),
+ {New<TTableSchema>()},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"Int64", -1},
+ {"String", "this_is_string"},
+ }).Get()
+ });
+ writer->Write({MakeRow(nameTable, { }).Get()});
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ ASSERT_EQ(result, "\0\0\0\0\0\0\0\0"sv);
+}
+
+TEST(TProtobufFormat, TestTabletIndex)
+{
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap());
+
+ auto nameTable = New<TNameTable>();
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto controlAttributesConfig = New<TControlAttributesConfig>();
+ controlAttributesConfig->EnableTabletIndex = true;
+
+ auto writer = CreateWriterForProtobuf(
+ config,
+ {New<TTableSchema>()},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ controlAttributesConfig,
+ 0);
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TabletIndexColumnName, 1LL << 50},
+ {"int64_field", -2345},
+ }).Get(),
+ MakeRow(nameTable, {
+ {TabletIndexColumnName, 12},
+ {"int64_field", 2345},
+ }).Get(),
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput si(result);
+ TLenvalParser parser(&si);
+ {
+ auto row = parser.Next();
+ ASSERT_TRUE(row);
+ ASSERT_EQ(row->TabletIndex, 1ULL << 50);
+ NYT::TMessage message;
+ ASSERT_TRUE(message.ParseFromString(row->RowData));
+ ASSERT_EQ(message.int64_field(), -2345);
+ }
+ {
+ auto row = parser.Next();
+ ASSERT_TRUE(row);
+ ASSERT_EQ(static_cast<int>(row->TabletIndex), 12);
+ NYT::TMessage message;
+ ASSERT_TRUE(message.ParseFromString(row->RowData));
+ ASSERT_EQ(message.int64_field(), 2345);
+ }
+ {
+ auto row = parser.Next();
+ ASSERT_FALSE(row);
+ }
+}
+
+TEST(TProtobufFormat, TestContext)
+{
+ auto config = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ TCollectingValueConsumer rowCollector;
+ auto parser = CreateParserForProtobuf(
+ &rowCollector,
+ ConvertTo<TProtobufFormatConfigPtr>(config),
+ 0);
+
+ TString context;
+ try {
+ TMessage message;
+ message.set_string_field("PYSHCH-PYSHCH");
+ parser->Read(LenvalBytes(message));
+ parser->Finish();
+ GTEST_FATAL_FAILURE_("expected to throw");
+ } catch (const NYT::TErrorException& e) {
+ context = *e.Error().Attributes().Find<TString>("context");
+ }
+ ASSERT_NE(context.find("PYSHCH-PYSHCH"), TString::npos);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTableSchemaPtr CreateSchemaWithStructuredMessage()
+{
+ auto keyValueStruct = StructLogicalType({
+ {"key", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"value", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ });
+
+ return New<TTableSchema>(std::vector<TColumnSchema>{
+ {"first", StructLogicalType({
+ {"field_missing_from_proto1", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int32))},
+ {"enum_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"int64_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"another_repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"message_field", keyValueStruct},
+ {"repeated_message_field", ListLogicalType(keyValueStruct)},
+ {"any_int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"any_map_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))},
+ {"optional_int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"repeated_optional_any_field", ListLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any)))},
+ {"packed_repeated_enum_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"optional_repeated_bool_field", OptionalLogicalType(ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean)))},
+ {"oneof_field", VariantStructLogicalType({
+ {"oneof_string_field_1", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_string_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_message_field", keyValueStruct},
+ })},
+ {"optional_oneof_field", OptionalLogicalType(VariantStructLogicalType({
+ {"oneof_string_field_1", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_string_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_message_field", keyValueStruct},
+ }))},
+ {"map_field", DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ OptionalLogicalType(keyValueStruct))
+ },
+ {"field_missing_from_proto2", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int32))},
+ })},
+ {"repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"another_repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"repeated_message_field", ListLogicalType(keyValueStruct)},
+ {"second", StructLogicalType({
+ {"one", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"two", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"three", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+ {"any_field", SimpleLogicalType(ESimpleLogicalValueType::Any)},
+
+ {"int64_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"uint64_field", SimpleLogicalType(ESimpleLogicalValueType::Uint64)},
+ {"int32_field", SimpleLogicalType(ESimpleLogicalValueType::Int32)},
+ {"uint32_field", SimpleLogicalType(ESimpleLogicalValueType::Uint32)},
+
+ {"enum_int_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"enum_string_string_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"enum_string_int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+
+ {"repeated_optional_any_field", ListLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any)))},
+
+ {"other_complex_field", StructLogicalType({
+ {"one", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"two", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"three", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+
+ {"utf8_field", SimpleLogicalType(ESimpleLogicalValueType::Utf8)},
+
+ {"packed_repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+
+ {"optional_repeated_int64_field", OptionalLogicalType(ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64)))},
+
+ {"oneof_field", VariantStructLogicalType({
+ {"oneof_string_field_1", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_string_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_message_field", keyValueStruct},
+ })},
+
+ {"optional_oneof_field", OptionalLogicalType(VariantStructLogicalType({
+ {"oneof_string_field_1", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_string_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"oneof_message_field", keyValueStruct},
+ }))},
+
+ {"map_field", DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ OptionalLogicalType(keyValueStruct))
+ },
+ });
+}
+
+INodePtr CreateConfigWithStructuredMessage(EComplexTypeMode complexTypeMode, EProtoFormatType formatType)
+{
+ if (formatType == EProtoFormatType::FileDescriptor) {
+ return CreateFileDescriptorConfig<TMessageWithStructuredEmbedded>(complexTypeMode);
+ }
+ YT_VERIFY(formatType == EProtoFormatType::Structured);
+
+ auto buildOneofConfig = [] (TString prefix, int fieldNumberOffset) {
+ return BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("name").Value(prefix + "oneof_field")
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value(prefix + "oneof_string_field_1")
+ .Item("field_number").Value(101 + fieldNumberOffset)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value(prefix + "oneof_string_field")
+ .Item("field_number").Value(102 + fieldNumberOffset)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value(prefix + "oneof_message_field")
+ .Item("field_number").Value(1000 + fieldNumberOffset)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("key")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("value")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+ };
+ auto oneofConfig = buildOneofConfig("", 0);
+ auto optionalOneofConfig = buildOneofConfig("optional_", 1000);
+
+ auto keyValueFields = BuildYsonStringFluently()
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("key")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("value")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList();
+
+ return BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("enumerations").Value(EnumerationsConfig)
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("first")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("packed_repeated_enum_field")
+ .Item("field_number").Value(11)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .Item("repeated").Value(true)
+ .Item("packed").Value(true)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("message_field")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").Value(keyValueFields)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("repeated_int64_field")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("another_repeated_int64_field")
+ .Item("field_number").Value(9)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("repeated_message_field")
+ .Item("field_number").Value(5)
+ .Item("proto_type").Value("structured_message")
+ .Item("repeated").Value(true)
+ .Item("fields").Value(keyValueFields)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("any_int64_field")
+ .Item("field_number").Value(6)
+ .Item("proto_type").Value("any")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("any_map_field")
+ .Item("field_number").Value(7)
+ .Item("proto_type").Value("any")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("optional_int64_field")
+ .Item("field_number").Value(8)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated_optional_any_field")
+ .Item("field_number").Value(10)
+ .Item("proto_type").Value("any")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("optional_repeated_bool_field")
+ .Item("field_number").Value(12)
+ .Item("proto_type").Value("bool")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item().Value(oneofConfig)
+ .Item().Value(optionalOneofConfig)
+ .Item()
+ .BeginMap()
+ .Item("name").Value("map_field")
+ .Item("field_number").Value(13)
+ .Item("proto_type").Value("structured_message")
+ .Item("repeated").Value(true)
+ .Item("fields")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("key")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("value")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").Value(keyValueFields)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("second")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("one")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("two")
+ .Item("field_number").Value(500000000)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("three")
+ .Item("field_number").Value(100500)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated_message_field")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("structured_message")
+ .Item("repeated").Value(true)
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("key")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("value")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated_int64_field")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("another_repeated_int64_field")
+ .Item("field_number").Value(13)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ // In schema it is of type "any".
+ .Item("name").Value("any_field")
+ .Item("field_number").Value(5)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ // The next fields are for type casting testing
+ .Item()
+ .BeginMap()
+ // In schema it is of type "int64".
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(6)
+ .Item("proto_type").Value("int32")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ // In schema it is of type "uint64".
+ .Item("name").Value("uint64_field")
+ .Item("field_number").Value(7)
+ .Item("proto_type").Value("uint32")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ // In schema it is of type "int32".
+ .Item("name").Value("int32_field")
+ .Item("field_number").Value(8)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ // In schema it is of type "uint32".
+ .Item("name").Value("uint32_field")
+ .Item("field_number").Value(9)
+ .Item("proto_type").Value("uint64")
+ .EndMap()
+
+ // Enums.
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_int_field")
+ .Item("field_number").Value(10)
+ .Item("proto_type").Value("enum_int")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_string_string_field")
+ .Item("field_number").Value(11)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_string_int64_field")
+ .Item("field_number").Value(12)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("utf8_field")
+ .Item("field_number").Value(16)
+ .Item("proto_type").Value("string")
+ .EndMap()
+
+ // list<optional<any>>.
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated_optional_any_field")
+ .Item("field_number").Value(14)
+ .Item("proto_type").Value("any")
+ .Item("repeated").Value(true)
+ .EndMap()
+
+ // Other columns.
+ .Item()
+ .BeginMap()
+ .Item("name").Value("other_columns_field")
+ .Item("field_number").Value(15)
+ .Item("proto_type").Value("other_columns")
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("packed_repeated_int64_field")
+ .Item("field_number").Value(17)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .Item("packed").Value(true)
+ .EndMap()
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("optional_repeated_int64_field")
+ .Item("field_number").Value(18)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+
+ .Item().Value(oneofConfig)
+ .Item().Value(optionalOneofConfig)
+
+ .Item()
+ .BeginMap()
+ .Item("name").Value("map_field")
+ .Item("field_number").Value(19)
+ .Item("proto_type").Value("structured_message")
+ .Item("repeated").Value(true)
+ .Item("fields")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("key")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("value")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").Value(keyValueFields)
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .Item("complex_type_mode").Value(complexTypeMode)
+ .EndAttributes()
+ .Value("protobuf");
+}
+
+using TProtobufFormatStructuredMessageParameter = std::tuple<EComplexTypeMode, int, EProtoFormatType>;
+
+class TProtobufFormatStructuredMessage
+ : public ::testing::TestWithParam<TProtobufFormatStructuredMessageParameter>
+{ };
+
+INSTANTIATE_TEST_SUITE_P(
+ FileDescriptor,
+ TProtobufFormatStructuredMessage,
+ ::testing::Values(TProtobufFormatStructuredMessageParameter{
+ EComplexTypeMode::Positional,
+ 1,
+ EProtoFormatType::FileDescriptor}));
+
+INSTANTIATE_TEST_SUITE_P(
+ Positional,
+ TProtobufFormatStructuredMessage,
+ ::testing::Values(TProtobufFormatStructuredMessageParameter{
+ EComplexTypeMode::Positional,
+ 1,
+ EProtoFormatType::Structured}));
+
+INSTANTIATE_TEST_SUITE_P(
+ Named,
+ TProtobufFormatStructuredMessage,
+ ::testing::Values(TProtobufFormatStructuredMessageParameter{
+ EComplexTypeMode::Named,
+ 1,
+ EProtoFormatType::Structured}));
+
+INSTANTIATE_TEST_SUITE_P(
+ ManyRows,
+ TProtobufFormatStructuredMessage,
+ ::testing::Values(TProtobufFormatStructuredMessageParameter{
+ EComplexTypeMode::Named,
+ 30000,
+ EProtoFormatType::Structured}));
+
+TEST_P(TProtobufFormatStructuredMessage, EmbeddedWrite)
+{
+ auto [complexTypeMode, rowCount, protoFormatType] = GetParam();
+
+ auto nameTable = New<TNameTable>();
+ auto numId = nameTable->RegisterName("num");
+ auto embeddedNumId = nameTable->RegisterName("embedded_num");
+ auto variantId = nameTable->RegisterName("variant");
+ auto embedded2NumId = nameTable->RegisterName("embedded2_num");
+ auto embedded2StructId = nameTable->RegisterName("embedded2_struct");
+ auto embedded2RepeatedId = nameTable->RegisterName("embedded2_repeated");
+ auto extraIntId = nameTable->RegisterName("extra_int");
+ auto otherComplexFieldId = nameTable->RegisterName("other_complex_field");
+
+ //message T2 {
+ // optional ui64 embedded2_num;
+ //};
+ //message T1 {
+ // required T2 t2 [embedded];
+ // optional ui64 embedded_num;
+ //};
+ //
+ //message T {
+ // required T1 t1 [embedded];
+ // optional ui64 num;
+ //};
+
+ auto schema = BuildEmbeddedSchema();
+ auto config = BuildEmbeddedConfig(complexTypeMode, protoFormatType);
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ ConvertTo<TProtobufFormatConfigPtr>(config->Attributes()),
+ {schema},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedUint64Value(789, numId));
+ builder.AddValue(MakeUnversionedUint64Value(123, embeddedNumId));
+ builder.AddValue(MakeUnversionedUint64Value(456, embedded2NumId));
+ builder.AddValue(MakeUnversionedCompositeValue("[1; 555u]", variantId));
+ auto embeddedYson = BuildYsonStringFluently()
+ .BeginList()
+ // float1
+ .Item().Value(1.5f)
+ // string1
+ .Item().Value("abc")
+ .EndList();
+ auto embeddedYsonStr = embeddedYson.ToString();
+ builder.AddValue(MakeUnversionedCompositeValue(embeddedYsonStr, embedded2StructId));
+ auto repeatedYsonStr = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value("a")
+ .Item().Value("b")
+ .EndList()
+ .ToString();
+ builder.AddValue(MakeUnversionedCompositeValue(repeatedYsonStr, embedded2RepeatedId));
+ builder.AddValue(MakeUnversionedInt64Value(111, extraIntId));
+ auto otherComplexFieldYson = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(22)
+ .Item().Value(23)
+ .Item().Value(24)
+ .EndList();
+ auto otherComplexFieldYsonStr = otherComplexFieldYson.ToString();
+ builder.AddValue(MakeUnversionedCompositeValue(otherComplexFieldYsonStr, otherComplexFieldId));
+
+
+ auto rows = std::vector<TUnversionedRow>(rowCount, builder.GetRow());
+ writer->Write(rows);
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput input(result);
+ TLenvalParser lenvalParser(&input);
+
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TEmbeddingMessage message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ EXPECT_EQ(message.num(), 789UL);
+ EXPECT_EQ(message.t1().embedded_num(), 123UL);
+ EXPECT_EQ(message.t1().t2().embedded2_num(), 456UL);
+
+ EXPECT_FALSE(message.t1().has_str_variant());
+ EXPECT_TRUE(message.t1().has_uint_variant());
+ EXPECT_EQ(message.t1().uint_variant(), 555UL);
+
+ EXPECT_EQ(message.t1().t2().embedded2_struct().float1(), 1.5f);
+ EXPECT_EQ(message.t1().t2().embedded2_struct().string1(), "abc");
+
+ ASSERT_EQ(message.t1().t2().embedded2_repeated_size(), 2);
+ EXPECT_EQ(message.t1().t2().embedded2_repeated(0), "a");
+ EXPECT_EQ(message.t1().t2().embedded2_repeated(1), "b");
+
+ {
+ auto otherColumns = ConvertToNode(TYsonString(message.other_columns_field()))->AsMap();
+ auto mode = complexTypeMode;
+ auto expected = ([&] {
+ switch (mode) {
+ case EComplexTypeMode::Named:
+ return BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("one").Value(22)
+ .Item("two").Value(23)
+ .Item("three").Value(24)
+ .EndMap();
+ case EComplexTypeMode::Positional:
+ return ConvertToNode(otherComplexFieldYson);
+ }
+ YT_ABORT();
+ })();
+
+ EXPECT_NODES_EQUAL(expected, otherColumns->GetChildOrThrow("other_complex_field"));
+ EXPECT_EQ(ConvertTo<i64>(otherColumns->GetChildOrThrow("extra_int")), 111);
+ }
+
+ ASSERT_FALSE(message.has_extra_field());
+ ASSERT_FALSE(message.t1().has_embedded_extra_field());
+ }
+
+ ASSERT_FALSE(lenvalParser.Next());
+}
+
+TEST_P(TProtobufFormatStructuredMessage, Write)
+{
+ auto [complexTypeMode, rowCount, protoFormatType] = GetParam();
+
+ auto nameTable = New<TNameTable>();
+ auto firstId = nameTable->RegisterName("first");
+ auto secondId = nameTable->RegisterName("second");
+ auto repeatedMessageId = nameTable->RegisterName("repeated_message_field");
+ auto repeatedInt64Id = nameTable->RegisterName("repeated_int64_field");
+ auto anotherRepeatedInt64Id = nameTable->RegisterName("another_repeated_int64_field");
+ auto anyFieldId = nameTable->RegisterName("any_field");
+ auto int64FieldId = nameTable->RegisterName("int64_field");
+ auto uint64FieldId = nameTable->RegisterName("uint64_field");
+ auto int32FieldId = nameTable->RegisterName("int32_field");
+ auto uint32FieldId = nameTable->RegisterName("uint32_field");
+ auto enumIntFieldId = nameTable->RegisterName("enum_int_field");
+ auto enumStringStringFieldId = nameTable->RegisterName("enum_string_string_field");
+ auto enumStringInt64FieldId = nameTable->RegisterName("enum_string_int64_field");
+ auto utf8FieldId = nameTable->RegisterName("utf8_field");
+ auto repeatedOptionalAnyFieldId = nameTable->RegisterName("repeated_optional_any_field");
+ auto otherComplexFieldId = nameTable->RegisterName("other_complex_field");
+ auto packedRepeatedInt64FieldId = nameTable->RegisterName("packed_repeated_int64_field");
+ auto optionalRepeatedInt64FieldId = nameTable->RegisterName("optional_repeated_int64_field");
+ auto oneofFieldId = nameTable->RegisterName("oneof_field");
+ auto optionalOneofFieldId = nameTable->RegisterName("optional_oneof_field");
+ auto mapFieldId = nameTable->RegisterName("map_field");
+
+ auto schema = CreateSchemaWithStructuredMessage();
+ auto config = CreateConfigWithStructuredMessage(complexTypeMode, protoFormatType);
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ ConvertTo<TProtobufFormatConfigPtr>(config->Attributes()),
+ {schema},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+
+ auto firstYsonStr = BuildYsonStringFluently()
+ .BeginList()
+ // field_missing_from_proto1
+ .Item().Value(11111)
+ // enum_field
+ .Item().Value("Two")
+ // int64_field
+ .Item().Value(44)
+ // repeated_int64_field
+ .Item()
+ .BeginList()
+ .Item().Value(55)
+ .Item().Value(56)
+ .Item().Value(57)
+ .EndList()
+ // another_repeated_int64_field
+ .Item()
+ .BeginList()
+ .EndList()
+ // message_field
+ .Item()
+ .BeginList()
+ .Item().Value("key")
+ .Item().Value("value")
+ .EndList()
+ // repeated_message_field
+ .Item()
+ .BeginList()
+ .Item()
+ .BeginList()
+ .Item().Value("key1")
+ .Item().Value("value1")
+ .EndList()
+ .Item()
+ .BeginList()
+ .Item().Value("key2")
+ .Item().Value("value2")
+ .EndList()
+ .EndList()
+ // any_int64_field
+ .Item().Value(45)
+ // any_map_field
+ .Item()
+ .BeginMap()
+ .Item("key").Value("value")
+ .EndMap()
+ // optional_int64_field
+ .Item().Entity()
+ // repeated_optional_any_field
+ .Item()
+ .BeginList()
+ .Item().Value(2)
+ .Item().Entity()
+ .Item().Value("foo")
+ .EndList()
+ // packed_repeated_enum_field
+ .Item()
+ .BeginList()
+ .Item().Value("MinusFortyTwo")
+ .Item().Value("Two")
+ .EndList()
+ // optional_repeated_bool_field
+ .Item()
+ .BeginList()
+ .Item().Value(false)
+ .Item().Value(true)
+ .Item().Value(false)
+ .EndList()
+ // oneof_field
+ .Item()
+ .BeginList()
+ // message_field
+ .Item().Value(2)
+ .Item().BeginList()
+ .Item().Value("foo")
+ .Item().Entity()
+ .EndList()
+ .EndList()
+ // optional_oneof_field
+ .Item()
+ .Entity()
+ // map_field
+ .Item()
+ .BeginList()
+ .Item().BeginList()
+ .Item().Value(13)
+ .Item().BeginList()
+ .Item().Value("bac")
+ .Item().Value("cab")
+ .EndList()
+ .EndList()
+ .Item().BeginList()
+ .Item().Value(15)
+ .Item().BeginList()
+ .Item().Value("ya")
+ .Item().Value("make")
+ .EndList()
+ .EndList()
+ .EndList()
+ .EndList()
+ .ToString();
+
+ auto secondYsonStr = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(101)
+ .Item().Value(102)
+ .Item().Value(103)
+ .EndList()
+ .ToString();
+
+ auto repeatedMessageYsonStr = BuildYsonStringFluently()
+ .BeginList()
+ .Item()
+ .BeginList()
+ .Item().Value("key11")
+ .Item().Value("value11")
+ .EndList()
+ .Item()
+ .BeginList()
+ .Item().Value("key21")
+ .Item().Value("value21")
+ .EndList()
+ .EndList()
+ .ToString();
+
+ auto repeatedInt64Yson = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(31)
+ .Item().Value(32)
+ .Item().Value(33)
+ .EndList();
+ auto repeatedInt64YsonStr = repeatedInt64Yson.ToString();
+
+ auto anotherRepeatedInt64YsonStr = BuildYsonStringFluently()
+ .BeginList()
+ .EndList()
+ .ToString();
+
+ auto repeatedOptionalAnyYson = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(1)
+ .Item().Value("abc")
+ .Item().Entity()
+ .Item().Value(true)
+ .EndList();
+ auto repeatedOptionalAnyYsonStr = repeatedOptionalAnyYson.ToString();
+
+ auto otherComplexFieldYson = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(22)
+ .Item().Value(23)
+ .Item().Value(24)
+ .EndList();
+ auto otherComplexFieldYsonStr = otherComplexFieldYson.ToString();
+
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedCompositeValue(firstYsonStr, firstId));
+ builder.AddValue(MakeUnversionedCompositeValue(secondYsonStr, secondId));
+ builder.AddValue(MakeUnversionedCompositeValue(repeatedMessageYsonStr, repeatedMessageId));
+ builder.AddValue(MakeUnversionedCompositeValue(repeatedInt64YsonStr, repeatedInt64Id));
+ builder.AddValue(MakeUnversionedCompositeValue(anotherRepeatedInt64YsonStr, anotherRepeatedInt64Id));
+ builder.AddValue(MakeUnversionedInt64Value(4321, anyFieldId));
+
+ builder.AddValue(MakeUnversionedInt64Value(-64, int64FieldId));
+ builder.AddValue(MakeUnversionedUint64Value(64, uint64FieldId));
+ builder.AddValue(MakeUnversionedInt64Value(-32, int32FieldId));
+ builder.AddValue(MakeUnversionedUint64Value(32, uint32FieldId));
+
+ builder.AddValue(MakeUnversionedInt64Value(-42, enumIntFieldId));
+ builder.AddValue(MakeUnversionedStringValue("Three", enumStringStringFieldId));
+ builder.AddValue(MakeUnversionedInt64Value(1, enumStringInt64FieldId));
+
+ const auto HelloWorldInRussian = "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82, \xd0\xbc\xd0\xb8\xd1\x80!";
+ builder.AddValue(MakeUnversionedStringValue(HelloWorldInRussian, utf8FieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue(repeatedOptionalAnyYsonStr, repeatedOptionalAnyFieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue(otherComplexFieldYsonStr, otherComplexFieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue("[12;-10;123456789000;]", packedRepeatedInt64FieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue("[1;2;3]", optionalRepeatedInt64FieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue("[0; foobaz]", oneofFieldId));
+ builder.AddValue(MakeUnversionedNullValue(optionalOneofFieldId));
+
+ builder.AddValue(MakeUnversionedCompositeValue("[[2; [x; y]]; [5; [z; w]]]", mapFieldId));
+
+ auto rows = std::vector<TUnversionedRow>(rowCount, builder.GetRow());
+ writer->Write(rows);
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput input(result);
+ TLenvalParser lenvalParser(&input);
+
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TMessageWithStructuredEmbedded message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ const auto& first = message.first();
+ EXPECT_EQ(first.enum_field(), EEnum::Two);
+ EXPECT_EQ(first.int64_field(), 44);
+ std::vector<i64> firstRepeatedInt64Field(
+ first.repeated_int64_field().begin(),
+ first.repeated_int64_field().end());
+ EXPECT_EQ(firstRepeatedInt64Field, (std::vector<i64>{55, 56, 57}));
+ std::vector<i64> firstAnotherRepeatedInt64Field(
+ first.another_repeated_int64_field().begin(),
+ first.another_repeated_int64_field().end());
+ EXPECT_EQ(firstAnotherRepeatedInt64Field, (std::vector<i64>{}));
+ EXPECT_EQ(first.message_field().key(), "key");
+ EXPECT_EQ(first.message_field().value(), "value");
+ ASSERT_EQ(first.repeated_message_field_size(), 2);
+ EXPECT_EQ(first.repeated_message_field(0).key(), "key1");
+ EXPECT_EQ(first.repeated_message_field(0).value(), "value1");
+ EXPECT_EQ(first.repeated_message_field(1).key(), "key2");
+ EXPECT_EQ(first.repeated_message_field(1).value(), "value2");
+
+ EXPECT_NODES_EQUAL(
+ ConvertToNode(TYsonString(first.any_int64_field())),
+ BuildYsonNodeFluently().Value(45));
+
+ EXPECT_NODES_EQUAL(
+ ConvertToNode(TYsonString(first.any_map_field())),
+ BuildYsonNodeFluently().BeginMap()
+ .Item("key").Value("value")
+ .EndMap());
+
+ std::vector<TYsonString> firstRepeatedOptionalAnyField(
+ first.repeated_optional_any_field().begin(),
+ first.repeated_optional_any_field().end());
+
+ EXPECT_NODES_EQUAL(
+ ConvertToNode(firstRepeatedOptionalAnyField),
+ BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Value(2)
+ .Item().Entity()
+ .Item().Value("foo")
+ .EndList());
+
+ EXPECT_FALSE(first.has_optional_int64_field());
+
+ std::vector<EEnum> actualFirstPackedRepeatedEnumField;
+ for (auto x : first.packed_repeated_enum_field()) {
+ actualFirstPackedRepeatedEnumField.push_back(static_cast<EEnum>(x));
+ }
+ auto expectedFirstPackedRepeatedEnumField = std::vector<EEnum>{EEnum::MinusFortyTwo, EEnum::Two};
+ EXPECT_EQ(expectedFirstPackedRepeatedEnumField, actualFirstPackedRepeatedEnumField);
+
+ std::vector<bool> firstOptionalRepeatedBoolField(
+ first.optional_repeated_bool_field().begin(),
+ first.optional_repeated_bool_field().end());
+ auto expectedFirstOptionalRepeatedBoolField = std::vector<bool>{false, true, false};
+ EXPECT_EQ(expectedFirstOptionalRepeatedBoolField, firstOptionalRepeatedBoolField);
+
+ EXPECT_FALSE(first.has_oneof_string_field_1());
+ EXPECT_FALSE(first.has_oneof_string_field());
+ EXPECT_TRUE(first.has_oneof_message_field());
+ EXPECT_EQ(first.oneof_message_field().key(), "foo");
+ EXPECT_FALSE(first.oneof_message_field().has_value());
+
+ EXPECT_FALSE(first.has_optional_oneof_string_field_1());
+ EXPECT_FALSE(first.has_optional_oneof_string_field());
+ EXPECT_FALSE(first.has_optional_oneof_message_field());
+
+ EXPECT_EQ(std::ssize(first.map_field()), 2);
+ ASSERT_EQ(static_cast<int>(first.map_field().count(13)), 1);
+ EXPECT_EQ(first.map_field().at(13).key(), "bac");
+ EXPECT_EQ(first.map_field().at(13).value(), "cab");
+ ASSERT_EQ(static_cast<int>(first.map_field().count(15)), 1);
+ EXPECT_EQ(first.map_field().at(15).key(), "ya");
+ EXPECT_EQ(first.map_field().at(15).value(), "make");
+
+ const auto& second = message.second();
+ EXPECT_EQ(second.one(), 101);
+ EXPECT_EQ(second.two(), 102);
+ EXPECT_EQ(second.three(), 103);
+
+ ASSERT_EQ(message.repeated_message_field_size(), 2);
+ EXPECT_EQ(message.repeated_message_field(0).key(), "key11");
+ EXPECT_EQ(message.repeated_message_field(0).value(), "value11");
+ EXPECT_EQ(message.repeated_message_field(1).key(), "key21");
+ EXPECT_EQ(message.repeated_message_field(1).value(), "value21");
+
+ std::vector<i64> repeatedInt64Field(
+ message.repeated_int64_field().begin(),
+ message.repeated_int64_field().end());
+ EXPECT_EQ(repeatedInt64Field, (std::vector<i64>{31, 32, 33}));
+
+ std::vector<i64> anotherRepeatedInt64Field(
+ message.another_repeated_int64_field().begin(),
+ message.another_repeated_int64_field().end());
+ EXPECT_EQ(anotherRepeatedInt64Field, (std::vector<i64>{}));
+
+ EXPECT_EQ(message.int64_any_field(), 4321);
+
+ // Note the reversal of 32 <-> 64.
+ EXPECT_EQ(message.int32_field(), -64);
+ EXPECT_EQ(message.uint32_field(), 64u);
+ EXPECT_EQ(message.int64_field(), -32);
+ EXPECT_EQ(message.uint64_field(), 32u);
+
+ EXPECT_EQ(message.enum_int_field(), EEnum::MinusFortyTwo);
+ EXPECT_EQ(message.enum_string_string_field(), EEnum::Three);
+ EXPECT_EQ(message.enum_string_int64_field(), EEnum::One);
+
+ EXPECT_EQ(message.utf8_field(), HelloWorldInRussian);
+
+ std::vector<TYsonString> repeatedOptionalAnyField(
+ message.repeated_optional_any_field().begin(),
+ message.repeated_optional_any_field().end());
+ EXPECT_NODES_EQUAL(ConvertToNode(repeatedOptionalAnyField), ConvertToNode(repeatedOptionalAnyYson));
+
+ {
+ auto otherColumns = ConvertToNode(TYsonString(message.other_columns_field()))->AsMap();
+ auto mode = complexTypeMode;
+ auto expected = ([&] {
+ switch (mode) {
+ case EComplexTypeMode::Named:
+ return BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("one").Value(22)
+ .Item("two").Value(23)
+ .Item("three").Value(24)
+ .EndMap();
+ case EComplexTypeMode::Positional:
+ return ConvertToNode(otherComplexFieldYson);
+ }
+ YT_ABORT();
+ })();
+
+ EXPECT_NODES_EQUAL(expected, otherColumns->GetChildOrThrow("other_complex_field"));
+ }
+
+ std::vector<i64> actualPackedRepeatedInt64Field(
+ message.packed_repeated_int64_field().begin(),
+ message.packed_repeated_int64_field().end());
+ auto expectedPackedRepeatedInt64Field = std::vector<i64>{12, -10, 123456789000LL};
+ EXPECT_EQ(expectedPackedRepeatedInt64Field, actualPackedRepeatedInt64Field);
+
+ std::vector<i64> actualOptionalRepeatedInt64Field(
+ message.optional_repeated_int64_field().begin(),
+ message.optional_repeated_int64_field().end());
+ auto expectedOptionalRepeatedInt64Field = std::vector<i64>{1, 2, 3};
+ EXPECT_EQ(expectedOptionalRepeatedInt64Field, actualOptionalRepeatedInt64Field);
+
+ EXPECT_TRUE(message.has_oneof_string_field_1());
+ EXPECT_EQ(message.oneof_string_field_1(), "foobaz");
+ EXPECT_FALSE(message.has_oneof_string_field());
+ EXPECT_FALSE(message.has_oneof_message_field());
+
+ EXPECT_FALSE(message.has_optional_oneof_string_field_1());
+ EXPECT_FALSE(message.has_optional_oneof_string_field());
+ EXPECT_FALSE(message.has_optional_oneof_message_field());
+
+ EXPECT_EQ(std::ssize(message.map_field()), 2);
+ ASSERT_EQ(static_cast<int>(message.map_field().count(2)), 1);
+ EXPECT_EQ(message.map_field().at(2).key(), "x");
+ EXPECT_EQ(message.map_field().at(2).value(), "y");
+ ASSERT_EQ(static_cast<int>(message.map_field().count(5)), 1);
+ EXPECT_EQ(message.map_field().at(5).key(), "z");
+ EXPECT_EQ(message.map_field().at(5).value(), "w");
+ }
+
+ ASSERT_FALSE(lenvalParser.Next());
+}
+
+INodePtr SortMapByKey(const INodePtr& node)
+{
+ auto keyValuePairs = ConvertTo<std::vector<std::pair<i64, INodePtr>>>(node);
+ std::sort(std::begin(keyValuePairs), std::end(keyValuePairs));
+ return ConvertTo<INodePtr>(keyValuePairs);
+}
+
+TEST_P(TProtobufFormatStructuredMessage, EmbeddedParse)
+{
+ auto [complexTypeMode, rowCount, protoFormatType] = GetParam();
+
+ auto schema = BuildEmbeddedSchema();
+ auto config = BuildEmbeddedConfig(complexTypeMode, protoFormatType);
+
+ NYT::TEmbeddingMessage message;
+
+ message.set_num(789);
+ auto* t1 = message.mutable_t1();
+ t1->set_embedded_num(123);
+ auto* t2 = t1->mutable_t2();
+ t2->set_embedded2_num(456);
+ t1->set_uint_variant(555);
+ t2->add_embedded2_repeated("a");
+ t2->add_embedded2_repeated("b");
+ t2->add_embedded2_repeated("c");
+ auto* embedded2_struct = t2->mutable_embedded2_struct();
+ embedded2_struct->set_float1(1.5f);
+ embedded2_struct->set_string1("abc");
+
+ //message.set_extra_field("*");
+ //t1->set_embedded_extra_field("*");
+
+ auto rowCollector = ParseRows(message, config, schema, rowCount);
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ EXPECT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "num")), 789u);
+ EXPECT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "embedded_num")), 123u);
+ EXPECT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "embedded2_num")), 456u);
+ EXPECT_NODES_EQUAL(
+ GetComposite(rowCollector.GetRowValue(rowIndex, "variant")),
+ ConvertToNode(TYsonString(TStringBuf("[1; 555u]"))));
+
+ auto embedded2_repeatedNode = GetComposite(rowCollector.GetRowValue(rowIndex, "embedded2_repeated"));
+ ASSERT_EQ(embedded2_repeatedNode->GetType(), ENodeType::List);
+ const auto& embedded2_repeatedList = embedded2_repeatedNode->AsList();
+ ASSERT_EQ(embedded2_repeatedList->GetChildCount(), 3);
+ EXPECT_EQ(embedded2_repeatedList->GetChildValueOrThrow<TString>(0), "a");
+ EXPECT_EQ(embedded2_repeatedList->GetChildValueOrThrow<TString>(1), "b");
+ EXPECT_EQ(embedded2_repeatedList->GetChildValueOrThrow<TString>(2), "c");
+
+ auto embedded2_structNode = GetComposite(rowCollector.GetRowValue(rowIndex, "embedded2_struct"));
+ ASSERT_EQ(embedded2_structNode->GetType(), ENodeType::List);
+ const auto& embedded2_structList = embedded2_structNode->AsList();
+ ASSERT_EQ(embedded2_structList->GetChildCount(), 2);
+ EXPECT_EQ(embedded2_structList->GetChildValueOrThrow<double>(0), 1.5f);
+ EXPECT_EQ(embedded2_structList->GetChildValueOrThrow<TString>(1), "abc");
+ }
+}
+
+TEST_P(TProtobufFormatStructuredMessage, Parse)
+{
+ auto [complexTypeMode, rowCount, protoFormatType] = GetParam();
+
+ auto schema = CreateSchemaWithStructuredMessage();
+ auto config = CreateConfigWithStructuredMessage(complexTypeMode, protoFormatType);
+
+ NYT::TMessageWithStructuredEmbedded message;
+
+ auto* first = message.mutable_first();
+ first->set_enum_field(EEnum::Two);
+ first->set_int64_field(44);
+
+ first->add_repeated_int64_field(55);
+ first->add_repeated_int64_field(56);
+ first->add_repeated_int64_field(57);
+
+ // another_repeated_int64_field is intentionally empty.
+
+ first->mutable_message_field()->set_key("key");
+ first->mutable_message_field()->set_value("value");
+ auto* firstSubfield1 = first->add_repeated_message_field();
+ firstSubfield1->set_key("key1");
+ firstSubfield1->set_value("value1");
+ auto* firstSubfield2 = first->add_repeated_message_field();
+ firstSubfield2->set_key("key2");
+ firstSubfield2->set_value("value2");
+
+ first->set_any_int64_field(BuildYsonStringFluently().Value(4422).ToString());
+ first->set_any_map_field(
+ BuildYsonStringFluently()
+ .BeginMap()
+ .Item("key").Value("value")
+ .EndMap()
+ .ToString());
+
+ first->add_repeated_optional_any_field("%false");
+ first->add_repeated_optional_any_field("42");
+ first->add_repeated_optional_any_field("#");
+
+ first->add_packed_repeated_enum_field(EEnum::MaxInt32);
+ first->add_packed_repeated_enum_field(EEnum::MinusFortyTwo);
+
+ // optional_repeated_bool_field is intentionally empty.
+
+ first->mutable_oneof_message_field()->set_key("KEY");
+
+ // optional_oneof_field is intentionally empty.
+
+ (*first->mutable_map_field())[111].set_key("key111");
+ (*first->mutable_map_field())[111].set_value("value111");
+ (*first->mutable_map_field())[222].set_key("key222");
+ (*first->mutable_map_field())[222].set_value("value222");
+
+ auto* second = message.mutable_second();
+ second->set_one(101);
+ second->set_two(102);
+ second->set_three(103);
+
+ message.add_repeated_int64_field(31);
+ message.add_repeated_int64_field(32);
+ message.add_repeated_int64_field(33);
+
+ // another_repeated_int64_field is intentionally empty.
+
+ auto* subfield1 = message.add_repeated_message_field();
+ subfield1->set_key("key11");
+ subfield1->set_value("value11");
+ auto* subfield2 = message.add_repeated_message_field();
+ subfield2->set_key("key21");
+ subfield2->set_value("value21");
+
+ message.set_int64_any_field(4321);
+
+ // Note the reversal of 32 <-> 64.
+ message.set_int64_field(-32);
+ message.set_uint64_field(32);
+ message.set_int32_field(-64);
+ message.set_uint32_field(64);
+
+ // Note that we don't set the "enum_string_int64_field" as it would fail during parsing.
+ message.set_enum_int_field(EEnum::MinusFortyTwo);
+ message.set_enum_string_string_field(EEnum::Three);
+
+ const auto HelloWorldInChinese = "\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c";
+ message.set_utf8_field(HelloWorldInChinese);
+
+ message.add_repeated_optional_any_field("#");
+ message.add_repeated_optional_any_field("1");
+ message.add_repeated_optional_any_field("\"qwe\"");
+ message.add_repeated_optional_any_field("%true");
+
+ auto otherComplexFieldPositional = BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Value(301)
+ .Item().Value(302)
+ .Item().Value(303)
+ .EndList();
+
+ auto mode = complexTypeMode;
+ auto otherComplexField = ([&] {
+ switch (mode) {
+ case EComplexTypeMode::Named:
+ return BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("one").Value(301)
+ .Item("two").Value(302)
+ .Item("three").Value(303)
+ .EndMap();
+ case EComplexTypeMode::Positional:
+ return otherComplexFieldPositional;
+ }
+ YT_ABORT();
+ })();
+ auto otherColumnsYson = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("other_complex_field").Value(otherComplexField)
+ .EndMap();
+ message.set_other_columns_field(otherColumnsYson.ToString());
+
+ message.add_packed_repeated_int64_field(-123456789000LL);
+ message.add_packed_repeated_int64_field(0);
+
+ message.add_optional_repeated_int64_field(-4242);
+
+ // optional_oneof_field is intentionally empty.
+
+ message.set_oneof_string_field("spam");
+
+ (*message.mutable_map_field())[777].set_key("key777");
+ (*message.mutable_map_field())[777].set_value("value777");
+ (*message.mutable_map_field())[888].set_key("key888");
+ (*message.mutable_map_field())[888].set_value("value888");
+
+ auto rowCollector = ParseRows(message, config, schema, rowCount);
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ auto firstNode = GetComposite(rowCollector.GetRowValue(rowIndex, "first"));
+ ASSERT_EQ(firstNode->GetType(), ENodeType::List);
+ const auto& firstList = firstNode->AsList();
+ ASSERT_EQ(firstList->GetChildCount(), 17);
+
+ EXPECT_EQ(firstList->GetChildOrThrow(0)->GetType(), ENodeType::Entity);
+ EXPECT_EQ(firstList->GetChildValueOrThrow<TString>(1), "Two");
+ EXPECT_EQ(firstList->GetChildValueOrThrow<i64>(2), 44);
+
+ ASSERT_EQ(firstList->GetChildOrThrow(3)->GetType(), ENodeType::List);
+ EXPECT_EQ(ConvertTo<std::vector<i64>>(firstList->GetChildOrThrow(3)), (std::vector<i64>{55, 56, 57}));
+
+ ASSERT_EQ(firstList->GetChildOrThrow(4)->GetType(), ENodeType::List);
+ EXPECT_EQ(ConvertTo<std::vector<i64>>(firstList->GetChildOrThrow(4)), (std::vector<i64>{}));
+
+ ASSERT_EQ(firstList->GetChildOrThrow(5)->GetType(), ENodeType::List);
+ EXPECT_EQ(firstList->GetChildOrThrow(5)->AsList()->GetChildValueOrThrow<TString>(0), "key");
+ EXPECT_EQ(firstList->GetChildOrThrow(5)->AsList()->GetChildValueOrThrow<TString>(1), "value");
+
+ ASSERT_EQ(firstList->GetChildOrThrow(6)->GetType(), ENodeType::List);
+ ASSERT_EQ(firstList->GetChildOrThrow(6)->AsList()->GetChildCount(), 2);
+
+ const auto& firstSubNode1 = firstList->GetChildOrThrow(6)->AsList()->GetChildOrThrow(0);
+ ASSERT_EQ(firstSubNode1->GetType(), ENodeType::List);
+ ASSERT_EQ(firstSubNode1->AsList()->GetChildCount(), 2);
+ EXPECT_EQ(firstSubNode1->AsList()->GetChildValueOrThrow<TString>(0), "key1");
+ EXPECT_EQ(firstSubNode1->AsList()->GetChildValueOrThrow<TString>(1), "value1");
+
+ const auto& firstSubNode2 = firstList->GetChildOrThrow(6)->AsList()->GetChildOrThrow(1);
+ ASSERT_EQ(firstSubNode2->GetType(), ENodeType::List);
+ ASSERT_EQ(firstSubNode2->AsList()->GetChildCount(), 2);
+ EXPECT_EQ(firstSubNode2->AsList()->GetChildValueOrThrow<TString>(0), "key2");
+ EXPECT_EQ(firstSubNode2->AsList()->GetChildValueOrThrow<TString>(1), "value2");
+
+ ASSERT_EQ(firstList->GetChildOrThrow(7)->GetType(), ENodeType::Int64);
+ EXPECT_EQ(firstList->GetChildValueOrThrow<i64>(7), 4422);
+
+ ASSERT_EQ(firstList->GetChildOrThrow(8)->GetType(), ENodeType::Map);
+ EXPECT_NODES_EQUAL(
+ firstList->GetChildOrThrow(8),
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("key").Value("value")
+ .EndMap());
+
+ ASSERT_EQ(firstList->GetChildOrThrow(9)->GetType(), ENodeType::Entity);
+
+ EXPECT_NODES_EQUAL(
+ firstList->GetChildOrThrow(10),
+ BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Value(false)
+ .Item().Value(42)
+ .Item().Entity()
+ .EndList());
+
+ EXPECT_NODES_EQUAL(
+ firstList->GetChildOrThrow(11),
+ BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Value("MaxInt32")
+ .Item().Value("MinusFortyTwo")
+ .EndList());
+
+ // optional_repeated_bool_field.
+ ASSERT_EQ(firstList->GetChildOrThrow(12)->GetType(), ENodeType::Entity);
+
+ // oneof_field.
+ EXPECT_NODES_EQUAL(
+ firstList->GetChildOrThrow(13),
+ BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Value(2)
+ .Item().BeginList()
+ .Item().Value("KEY")
+ .Item().Entity()
+ .EndList()
+ .EndList());
+
+ // optional_oneof_field.
+ ASSERT_EQ(firstList->GetChildOrThrow(14)->GetType(), ENodeType::Entity);
+
+ // map_field.
+ EXPECT_NODES_EQUAL(
+ SortMapByKey(firstList->GetChildOrThrow(15)),
+ BuildYsonNodeFluently()
+ .BeginList()
+ .Item().BeginList()
+ .Item().Value(111)
+ .Item().BeginList()
+ .Item().Value("key111")
+ .Item().Value("value111")
+ .EndList()
+ .EndList()
+ .Item().BeginList()
+ .Item().Value(222)
+ .Item().BeginList()
+ .Item().Value("key222")
+ .Item().Value("value222")
+ .EndList()
+ .EndList()
+ .EndList());
+
+ // field_missing_from_proto2.
+ ASSERT_EQ(firstList->GetChildOrThrow(16)->GetType(), ENodeType::Entity);
+
+ auto secondNode = GetComposite(rowCollector.GetRowValue(rowIndex, "second"));
+ ASSERT_EQ(secondNode->GetType(), ENodeType::List);
+ EXPECT_EQ(ConvertTo<std::vector<i64>>(secondNode), (std::vector<i64>{101, 102, 103}));
+
+ auto repeatedMessageNode = GetComposite(rowCollector.GetRowValue(rowIndex, "repeated_message_field"));
+ ASSERT_EQ(repeatedMessageNode->GetType(), ENodeType::List);
+ ASSERT_EQ(repeatedMessageNode->AsList()->GetChildCount(), 2);
+
+ const auto& subNode1 = repeatedMessageNode->AsList()->GetChildOrThrow(0);
+ ASSERT_EQ(subNode1->GetType(), ENodeType::List);
+ ASSERT_EQ(subNode1->AsList()->GetChildCount(), 2);
+ EXPECT_EQ(subNode1->AsList()->GetChildValueOrThrow<TString>(0), "key11");
+ EXPECT_EQ(subNode1->AsList()->GetChildValueOrThrow<TString>(1), "value11");
+
+ const auto& subNode2 = repeatedMessageNode->AsList()->GetChildOrThrow(1);
+ ASSERT_EQ(subNode2->GetType(), ENodeType::List);
+ ASSERT_EQ(subNode2->AsList()->GetChildCount(), 2);
+ EXPECT_EQ(subNode2->AsList()->GetChildValueOrThrow<TString>(0), "key21");
+ EXPECT_EQ(subNode2->AsList()->GetChildValueOrThrow<TString>(1), "value21");
+
+ auto repeatedInt64Node = GetComposite(rowCollector.GetRowValue(rowIndex, "repeated_int64_field"));
+ EXPECT_EQ(ConvertTo<std::vector<i64>>(repeatedInt64Node), (std::vector<i64>{31, 32, 33}));
+
+ auto anotherRepeatedInt64Node = GetComposite(rowCollector.GetRowValue(rowIndex, "another_repeated_int64_field"));
+ EXPECT_EQ(ConvertTo<std::vector<i64>>(anotherRepeatedInt64Node), (std::vector<i64>{}));
+
+ auto anyValue = rowCollector.GetRowValue(rowIndex, "any_field");
+ ASSERT_EQ(anyValue.Type, EValueType::Int64);
+ EXPECT_EQ(anyValue.Data.Int64, 4321);
+
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "int64_field")), -64);
+ EXPECT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "uint64_field")), 64u);
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "int32_field")), -32);
+ EXPECT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "uint32_field")), 32u);
+
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "enum_int_field")), -42);
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "enum_string_string_field")), "Three");
+
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "utf8_field")), HelloWorldInChinese);
+
+ auto repeatedRepeatedOptionalAnyNode = GetComposite(rowCollector.GetRowValue(rowIndex, "repeated_optional_any_field"));
+ auto expectedRepeatedOptionalAnyNode = BuildYsonNodeFluently()
+ .BeginList()
+ .Item().Entity()
+ .Item().Value(1)
+ .Item().Value("qwe")
+ .Item().Value(true)
+ .EndList();
+ EXPECT_NODES_EQUAL(repeatedRepeatedOptionalAnyNode, expectedRepeatedOptionalAnyNode);
+
+ auto actualOtherComplexField = GetComposite(rowCollector.GetRowValue(rowIndex, "other_complex_field"));
+ EXPECT_NODES_EQUAL(actualOtherComplexField, otherComplexFieldPositional);
+
+ EXPECT_NODES_EQUAL(
+ GetComposite(rowCollector.GetRowValue(rowIndex, "packed_repeated_int64_field")),
+ ConvertToNode(TYsonString(TStringBuf("[-123456789000;0]"))));
+
+ EXPECT_NODES_EQUAL(
+ GetComposite(rowCollector.GetRowValue(rowIndex, "optional_repeated_int64_field")),
+ ConvertToNode(TYsonString(TStringBuf("[-4242]"))));
+
+ EXPECT_NODES_EQUAL(
+ GetComposite(rowCollector.GetRowValue(rowIndex, "oneof_field")),
+ ConvertToNode(TYsonString(TStringBuf("[1; \"spam\"]"))));
+
+ EXPECT_FALSE(rowCollector.FindRowValue(rowIndex, "optional_oneof_field"));
+
+ // map_field.
+ EXPECT_NODES_EQUAL(
+ SortMapByKey(GetComposite(rowCollector.GetRowValue(rowIndex, "map_field"))),
+ ConvertToNode(TYsonString(TStringBuf("[[777; [key777; value777]]; [888; [key888; value888]]]"))));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TTableSchemaPtr> CreateSeveralTablesSchemas()
+{
+ return {
+ New<TTableSchema>(std::vector<TColumnSchema>{
+ {"embedded", StructLogicalType({
+ {"enum_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"int64_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ })},
+ {"repeated_int64_field", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ {"any_field", SimpleLogicalType(ESimpleLogicalValueType::Any)},
+ }),
+ New<TTableSchema>(std::vector<TColumnSchema>{
+ {"enum_field", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"int64_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }),
+ // Empty schema.
+ New<TTableSchema>(),
+ };
+}
+
+INodePtr CreateSeveralTablesConfig(EProtoFormatType protoFormatType)
+{
+ if (protoFormatType == EProtoFormatType::FileDescriptor) {
+ return CreateFileDescriptorConfig<TSeveralTablesMessageFirst, TSeveralTablesMessageSecond, TSeveralTablesMessageThird>();
+ }
+ YT_VERIFY(protoFormatType == EProtoFormatType::Structured);
+
+ return BuildYsonNodeFluently()
+ .BeginAttributes()
+ .Item("enumerations").Value(EnumerationsConfig)
+ .Item("tables")
+ .BeginList()
+ // Table #1.
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("embedded")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated_int64_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .Item("repeated").Value(true)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ // In schema it is of type "any".
+ .Item("name").Value("any_field")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+
+ // Table #2.
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("enum_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("enum_string")
+ .Item("enumeration_name").Value("EEnum")
+ .EndMap()
+ .EndList()
+ .EndMap()
+
+ // Table #3.
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("string_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .Value("protobuf");
+}
+
+using TProtobufFormatSeveralTablesParam = std::tuple<EProtoFormatType>;
+
+class TProtobufFormatSeveralTables
+ : public ::testing::TestWithParam<TProtobufFormatSeveralTablesParam>
+{ };
+
+INSTANTIATE_TEST_SUITE_P(
+ FileDescriptor,
+ TProtobufFormatSeveralTables,
+ ::testing::Values(TProtobufFormatSeveralTablesParam{
+ EProtoFormatType::FileDescriptor}));
+
+INSTANTIATE_TEST_SUITE_P(
+ Structured,
+ TProtobufFormatSeveralTables,
+ ::testing::Values(TProtobufFormatSeveralTablesParam{
+ EProtoFormatType::Structured}));
+
+TEST_P(TProtobufFormatSeveralTables, Write)
+{
+ auto [protoFormatType] = GetParam();
+
+ auto schemas = CreateSeveralTablesSchemas();
+ auto configNode = CreateSeveralTablesConfig(protoFormatType);
+
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(configNode->Attributes().ToMap());
+
+ auto nameTable = New<TNameTable>();
+ auto embeddedId = nameTable->RegisterName("embedded");
+ auto anyFieldId = nameTable->RegisterName("any_field");
+ auto int64FieldId = nameTable->RegisterName("int64_field");
+ auto repeatedInt64Id = nameTable->RegisterName("repeated_int64_field");
+ auto enumFieldId = nameTable->RegisterName("enum_field");
+ auto stringFieldId = nameTable->RegisterName("string_field");
+ auto tableIndexId = nameTable->RegisterName(TableIndexColumnName);
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto controlAttributesConfig = New<TControlAttributesConfig>();
+ controlAttributesConfig->EnableTableIndex = true;
+ controlAttributesConfig->EnableEndOfStream = true;
+ auto writer = CreateWriterForProtobuf(
+ std::move(config),
+ schemas,
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ std::move(controlAttributesConfig),
+ 0);
+
+ auto embeddedYson = BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value("Two")
+ .Item().Value(44)
+ .EndList()
+ .ToString();
+
+ auto repeatedInt64Yson = ConvertToYsonString(std::vector<i64>{31, 32, 33}).ToString();
+
+ {
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedCompositeValue(embeddedYson, embeddedId));
+ builder.AddValue(MakeUnversionedCompositeValue(repeatedInt64Yson, repeatedInt64Id));
+ builder.AddValue(MakeUnversionedInt64Value(4321, anyFieldId));
+ writer->Write({builder.GetRow()});
+ }
+ {
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedStringValue("Two", enumFieldId));
+ builder.AddValue(MakeUnversionedInt64Value(999, int64FieldId));
+ builder.AddValue(MakeUnversionedInt64Value(1, tableIndexId));
+ writer->Write({builder.GetRow()});
+ }
+ {
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedStringValue("blah", stringFieldId));
+ builder.AddValue(MakeUnversionedInt64Value(2, tableIndexId));
+ writer->Write({builder.GetRow()});
+ }
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput input(result);
+ TLenvalParser lenvalParser(&input);
+
+ {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TSeveralTablesMessageFirst message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ const auto& embedded = message.embedded();
+ EXPECT_EQ(embedded.enum_field(), EEnum::Two);
+ EXPECT_EQ(embedded.int64_field(), 44);
+
+ std::vector<i64> repeatedInt64Field(
+ message.repeated_int64_field().begin(),
+ message.repeated_int64_field().end());
+ EXPECT_EQ(repeatedInt64Field, (std::vector<i64>{31, 32, 33}));
+ EXPECT_EQ(message.int64_field(), 4321);
+ }
+ {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TSeveralTablesMessageSecond message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ EXPECT_EQ(message.enum_field(), EEnum::Two);
+ EXPECT_EQ(message.int64_field(), 999);
+ }
+ {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TSeveralTablesMessageThird message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ EXPECT_EQ(message.string_field(), "blah");
+ }
+ ASSERT_FALSE(lenvalParser.IsEndOfStream());
+ ASSERT_FALSE(lenvalParser.Next());
+ ASSERT_TRUE(lenvalParser.IsEndOfStream());
+ ASSERT_FALSE(lenvalParser.Next());
+}
+
+TEST_P(TProtobufFormatSeveralTables, Parse)
+{
+ auto [protoFormatType] = GetParam();
+
+ auto schemas = CreateSeveralTablesSchemas();
+ auto configNode = CreateSeveralTablesConfig(protoFormatType);
+ auto config = ConvertTo<TProtobufFormatConfigPtr>(configNode->Attributes().ToMap());
+
+ std::vector<TCollectingValueConsumer> rowCollectors;
+ std::vector<std::unique_ptr<IParser>> parsers;
+ for (const auto& schema : schemas) {
+ rowCollectors.emplace_back(schema);
+ }
+ for (int tableIndex = 0; tableIndex < static_cast<int>(schemas.size()); ++tableIndex) {
+ parsers.push_back(CreateParserForProtobuf(
+ &rowCollectors[tableIndex],
+ config,
+ tableIndex));
+ }
+
+ NYT::TSeveralTablesMessageFirst firstMessage;
+ auto* embedded = firstMessage.mutable_embedded();
+ embedded->set_enum_field(EEnum::Two);
+ embedded->set_int64_field(44);
+
+ firstMessage.add_repeated_int64_field(55);
+ firstMessage.add_repeated_int64_field(56);
+ firstMessage.add_repeated_int64_field(57);
+
+ firstMessage.set_int64_field(4444);
+
+ NYT::TSeveralTablesMessageSecond secondMessage;
+ secondMessage.set_enum_field(EEnum::Two);
+ secondMessage.set_int64_field(44);
+
+ NYT::TSeveralTablesMessageThird thirdMessage;
+ thirdMessage.set_string_field("blah");
+
+ auto parse = [] (auto& parser, const auto& message) {
+ TString lenvalBytes;
+ {
+ TStringOutput out(lenvalBytes);
+ auto messageSize = static_cast<ui32>(message.ByteSizeLong());
+ out.Write(&messageSize, sizeof(messageSize));
+ ASSERT_TRUE(message.SerializeToArcadiaStream(&out));
+ }
+ parser->Read(lenvalBytes);
+ parser->Finish();
+ };
+
+ parse(parsers[0], firstMessage);
+ parse(parsers[1], secondMessage);
+ parse(parsers[2], thirdMessage);
+
+ {
+ const auto& rowCollector = rowCollectors[0];
+ ASSERT_EQ(static_cast<int>(rowCollector.Size()), 1);
+
+ auto embeddedNode = GetComposite(rowCollector.GetRowValue(0, "embedded"));
+ ASSERT_EQ(ConvertToTextYson(embeddedNode), "[\"Two\";44;]");
+
+ auto repeatedInt64Node = GetComposite(rowCollector.GetRowValue(0, "repeated_int64_field"));
+ ASSERT_EQ(ConvertToTextYson(repeatedInt64Node), "[55;56;57;]");
+
+ auto int64Field = GetInt64(rowCollector.GetRowValue(0, "any_field"));
+ EXPECT_EQ(int64Field, 4444);
+ }
+
+ {
+ const auto& rowCollector = rowCollectors[1];
+ ASSERT_EQ(static_cast<int>(rowCollector.Size()), 1);
+
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "enum_field")), "Two");
+ EXPECT_EQ(GetInt64(rowCollector.GetRowValue(0, "int64_field")), 44);
+ }
+
+ {
+ const auto& rowCollector = rowCollectors[2];
+ ASSERT_EQ(static_cast<int>(rowCollector.Size()), 1);
+
+ EXPECT_EQ(GetString(rowCollector.GetRowValue(0, "string_field")), "blah");
+ }
+}
+
+TEST(TProtobufFormat, SchemaConfigMismatch)
+{
+ auto createParser = [] (const TTableSchemaPtr& schema, const INodePtr& configNode) {
+ TCollectingValueConsumer rowCollector(schema);
+ return CreateParserForProtobuf(
+ &rowCollector,
+ ConvertTo<TProtobufFormatConfigPtr>(configNode),
+ 0);
+ };
+ auto createSeveralTableWriter = [] (const std::vector<TTableSchemaPtr>& schemas, const INodePtr& configNode) {
+ TString result;
+ TStringOutput resultStream(result);
+ return CreateWriterForProtobuf(
+ ConvertTo<TProtobufFormatConfigPtr>(configNode),
+ schemas,
+ New<TNameTable>(),
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+ };
+ auto createWriter = [&] (const TTableSchemaPtr& schema, const INodePtr& configNode) {
+ createSeveralTableWriter({schema}, configNode);
+ };
+
+ auto schema_struct_with_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"struct", StructLogicalType({
+ {"int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+ });
+
+ auto schema_struct_with_uint64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"struct", StructLogicalType({
+ {"int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64))},
+ })},
+ });
+
+ auto config_struct_with_int64 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("struct")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(2)
+ // Wrong type.
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ // OK.
+ EXPECT_NO_THROW(createParser(schema_struct_with_int64, config_struct_with_int64));
+ EXPECT_NO_THROW(createWriter(schema_struct_with_int64, config_struct_with_int64));
+
+ // Types mismatch.
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(schema_struct_with_uint64, config_struct_with_int64),
+ "signedness of both types must be the same");
+ EXPECT_THROW_WITH_SUBSTRING(
+ createWriter(schema_struct_with_uint64, config_struct_with_int64),
+ "signedness of both types must be the same");
+
+ // No schema for structured field is Ok.
+ EXPECT_NO_THROW(createParser(New<TTableSchema>(), config_struct_with_int64));
+ EXPECT_NO_THROW(createWriter(New<TTableSchema>(), config_struct_with_int64));
+
+ auto schema_list_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"repeated", ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )},
+ });
+
+ auto schema_list_optional_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"repeated", ListLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))
+ )},
+ });
+
+ auto config_repeated_int64 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("repeated")
+ .Item("field_number").Value(1)
+ .Item("repeated").Value(true)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ // OK.
+ EXPECT_NO_THROW(createParser(schema_list_int64, config_repeated_int64));
+ EXPECT_NO_THROW(createWriter(schema_list_int64, config_repeated_int64));
+
+ // No schema for repeated field is Ok.
+ EXPECT_NO_THROW(createParser(New<TTableSchema>(), config_repeated_int64));
+ EXPECT_NO_THROW(createWriter(New<TTableSchema>(), config_repeated_int64));
+
+ // List of optional is not allowed.
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(schema_list_optional_int64, config_repeated_int64),
+ "unexpected logical metatype \"optional\"");
+ EXPECT_THROW_WITH_SUBSTRING(
+ createWriter(schema_list_optional_int64, config_repeated_int64),
+ "unexpected logical metatype \"optional\"");
+
+ auto schema_optional_list_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"repeated", OptionalLogicalType(
+ ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))
+ )},
+ });
+
+ // Optional list is OK.
+ EXPECT_NO_THROW(createParser(schema_optional_list_int64, config_repeated_int64));
+ EXPECT_NO_THROW(createWriter(schema_optional_list_int64, config_repeated_int64));
+
+ auto schema_optional_optional_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"field", OptionalLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))
+ )},
+ });
+
+ auto config_int64 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ // Optional of optional is not allowed.
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(schema_optional_optional_int64, config_int64),
+ "unexpected logical metatype \"optional\"");
+ EXPECT_THROW_WITH_SUBSTRING(
+ createWriter(schema_optional_optional_int64, config_int64),
+ "unexpected logical metatype \"optional\"");
+
+ auto schema_struct_with_both = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"struct", StructLogicalType({
+ {"required_field", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"optional_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+ });
+
+ auto config_struct_with_required = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("struct")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("required_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto config_struct_with_optional = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("struct")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("optional_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ auto config_struct_with_unknown = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("struct")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("required_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("optional_field")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("unknown_field")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ // Schema has more fields, non-optional field is missing in protobuf config.
+ // Parser should fail.
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(schema_struct_with_both, config_struct_with_optional),
+ "non-optional field \"required_field\" in schema is missing from protobuf config");
+ // Writer feels OK.
+ EXPECT_NO_THROW(createWriter(schema_struct_with_both, config_struct_with_optional));
+
+ // Schema has more fields, optional field is missing in protobuf config.
+ // It's OK for both the writer and the parser.
+ EXPECT_NO_THROW(createParser(schema_struct_with_both, config_struct_with_required));
+ EXPECT_NO_THROW(createWriter(schema_struct_with_both, config_struct_with_required));
+
+ // Protobuf config has more fields, it is always OK.
+ EXPECT_NO_THROW(createParser(schema_struct_with_both, config_struct_with_unknown));
+ EXPECT_NO_THROW(createWriter(schema_struct_with_both, config_struct_with_unknown));
+
+ auto schema_int64 = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"int64_field", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ });
+
+ auto config_two_tables = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("int64_field")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ EXPECT_NO_THROW(createWriter(schema_int64, config_two_tables));
+ EXPECT_THROW_WITH_SUBSTRING(
+ createSeveralTableWriter({schema_int64, schema_int64, schema_int64}, config_two_tables),
+ "Number of schemas is greater than number of tables in protobuf config: 3 > 2");
+
+ auto schema_variant_with_int = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"variant", VariantStructLogicalType({
+ {"a", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ })},
+ });
+ auto schema_variant_with_optional_int = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"variant", VariantStructLogicalType({
+ {"a", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ })},
+ });
+
+ auto config_with_oneof = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("tables")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("columns")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("variant")
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item()
+ .BeginMap()
+ .Item("name").Value("a")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ // Oneof fields require schematized columns.
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(New<TTableSchema>(), config_with_oneof),
+ "requires a corresponding schematized column");
+ EXPECT_THROW_WITH_SUBSTRING(
+ createWriter(New<TTableSchema>(), config_with_oneof),
+ "requires a corresponding schematized column");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ createParser(schema_variant_with_optional_int, config_with_oneof),
+ "Optional variant field \"variant.a\"");
+ EXPECT_THROW_WITH_SUBSTRING(
+ createWriter(schema_variant_with_optional_int, config_with_oneof),
+ "Optional variant field \"variant.a\"");
+ EXPECT_NO_THROW(createParser(schema_variant_with_int, config_with_oneof));
+ EXPECT_NO_THROW(createWriter(schema_variant_with_int, config_with_oneof));
+}
+
+TEST(TProtobufFormat, MultipleOtherColumns)
+{
+ auto nameTable = New<TNameTable>();
+
+ TString data;
+ TStringOutput resultStream(data);
+
+ auto controlAttributesConfig = New<TControlAttributesConfig>();
+ controlAttributesConfig->EnableTableIndex = true;
+ controlAttributesConfig->EnableEndOfStream = true;
+
+ auto protoWriter = CreateWriterForProtobuf(
+ MakeProtobufFormatConfig({TOtherColumnsMessage::descriptor(), TOtherColumnsMessage::descriptor()}),
+ std::vector<TTableSchemaPtr>(2, New<TTableSchema>()),
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ controlAttributesConfig,
+ 0);
+
+ protoWriter->Write(
+ std::vector<TUnversionedRow>{
+ NNamedValue::MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"field1", "foo"},
+ }),
+ NNamedValue::MakeRow(nameTable, {
+ {TableIndexColumnName, 1},
+ {"field2", "bar"},
+ }),
+ }
+ );
+ WaitFor(protoWriter->Close())
+ .ThrowOnError();
+
+ std::vector<TString> otherColumnsValue;
+ auto parser = TLenvalParser(data);
+ while (auto item = parser.Next()) {
+ TOtherColumnsMessage message;
+ bool parsed = message.ParseFromString(item->RowData);
+ EXPECT_TRUE(parsed);
+ otherColumnsValue.push_back(CanonizeYson(message.other_columns_field()));
+ }
+
+ EXPECT_EQ(
+ otherColumnsValue,
+ std::vector<TString>({
+ CanonizeYson("{field1=foo}"),
+ CanonizeYson("{field2=bar}"),
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TProtobufFormatAllFieldsParameter = std::tuple<int, EProtoFormatType>;
+class TProtobufFormatAllFields
+ : public ::testing::TestWithParam<TProtobufFormatAllFieldsParameter>
+{
+public:
+ bool IsLegacyFormat() const
+ {
+ auto [rowCount, protoFormatType] = GetParam();
+ return protoFormatType == EProtoFormatType::FileDescriptorLegacy;
+ }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ Specification,
+ TProtobufFormatAllFields,
+ ::testing::Values(TProtobufFormatAllFieldsParameter{1, EProtoFormatType::Structured}));
+
+INSTANTIATE_TEST_SUITE_P(
+ FileDescriptorLegacy,
+ TProtobufFormatAllFields,
+ ::testing::Values(TProtobufFormatAllFieldsParameter{1, EProtoFormatType::FileDescriptorLegacy}));
+
+INSTANTIATE_TEST_SUITE_P(
+ FileDescriptor,
+ TProtobufFormatAllFields,
+ ::testing::Values(TProtobufFormatAllFieldsParameter{1, EProtoFormatType::FileDescriptor}));
+
+INSTANTIATE_TEST_SUITE_P(
+ ManyRows,
+ TProtobufFormatAllFields,
+ ::testing::Values(TProtobufFormatAllFieldsParameter{50000, EProtoFormatType::Structured}));
+
+TEST_P(TProtobufFormatAllFields, Writer)
+{
+ auto [rowCount, protoFormatType] = GetParam();
+ auto config = CreateAllFieldsConfig(protoFormatType);
+
+ auto nameTable = New<TNameTable>();
+
+ auto doubleId = nameTable->RegisterName("Double");
+ auto floatId = nameTable->RegisterName("Float");
+
+ auto int64Id = nameTable->RegisterName("Int64");
+ auto uint64Id = nameTable->RegisterName("UInt64");
+ auto sint64Id = nameTable->RegisterName("SInt64");
+ auto fixed64Id = nameTable->RegisterName("Fixed64");
+ auto sfixed64Id = nameTable->RegisterName("SFixed64");
+
+ auto int32Id = nameTable->RegisterName("Int32");
+ auto uint32Id = nameTable->RegisterName("UInt32");
+ auto sint32Id = nameTable->RegisterName("SInt32");
+ auto fixed32Id = nameTable->RegisterName("Fixed32");
+ auto sfixed32Id = nameTable->RegisterName("SFixed32");
+
+ auto boolId = nameTable->RegisterName("Bool");
+ auto stringId = nameTable->RegisterName("String");
+ auto bytesId = nameTable->RegisterName("Bytes");
+
+ auto enumId = nameTable->RegisterName("Enum");
+
+ auto messageId = nameTable->RegisterName("Message");
+
+ auto anyWithMapId = nameTable->RegisterName("AnyWithMap");
+ auto anyWithInt64Id = nameTable->RegisterName("AnyWithInt64");
+ auto anyWithStringId = nameTable->RegisterName("AnyWithString");
+
+ auto otherInt64ColumnId = nameTable->RegisterName("OtherInt64Column");
+ auto otherDoubleColumnId = nameTable->RegisterName("OtherDoubleColumn");
+ auto otherStringColumnId = nameTable->RegisterName("OtherStringColumn");
+ auto otherNullColumnId = nameTable->RegisterName("OtherNullColumn");
+ auto otherBooleanColumnId = nameTable->RegisterName("OtherBooleanColumn");
+ auto otherAnyColumnId = nameTable->RegisterName("OtherAnyColumn");
+
+ auto tableIndexColumnId = nameTable->RegisterName(TableIndexColumnName);
+ auto rowIndexColumnId = nameTable->RegisterName(RowIndexColumnName);
+ auto rangeIndexColumnId = nameTable->RegisterName(RangeIndexColumnName);
+
+ auto missintInt64Id = nameTable->RegisterName("MissingInt64");
+
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateWriterForProtobuf(
+ config->Attributes(),
+ {New<TTableSchema>()},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+
+ TEmbeddedMessage embeddedMessage;
+ embeddedMessage.set_key("embedded_key");
+ embeddedMessage.set_value("embedded_value");
+ TString embeddedMessageBytes;
+ ASSERT_TRUE(embeddedMessage.SerializeToString(&embeddedMessageBytes));
+
+ auto mapNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("Key").Value("Value")
+ .Item("Another")
+ .BeginList()
+ .Item().Value(1)
+ .Item().Value("two")
+ .EndList()
+ .EndMap();
+ auto ysonString = ConvertToYsonString(mapNode).ToString();
+
+ TUnversionedRowBuilder builder;
+ for (const auto& value : {
+ MakeUnversionedDoubleValue(3.14159, doubleId),
+ MakeUnversionedDoubleValue(2.71828, floatId),
+
+ MakeUnversionedInt64Value(-1, int64Id),
+ MakeUnversionedUint64Value(2, uint64Id),
+ MakeUnversionedInt64Value(-3, sint64Id),
+ MakeUnversionedUint64Value(4, fixed64Id),
+ MakeUnversionedInt64Value(-5, sfixed64Id),
+
+ MakeUnversionedInt64Value(-6, int32Id),
+ MakeUnversionedUint64Value(7, uint32Id),
+ MakeUnversionedInt64Value(-8, sint32Id),
+ MakeUnversionedUint64Value(9, fixed32Id),
+ MakeUnversionedInt64Value(-10, sfixed32Id),
+
+ MakeUnversionedBooleanValue(true, boolId),
+ MakeUnversionedStringValue("this_is_string", stringId),
+ MakeUnversionedStringValue("this_is_bytes", bytesId),
+
+ MakeUnversionedStringValue("Two", enumId),
+
+ MakeUnversionedStringValue(embeddedMessageBytes, messageId),
+
+ MakeUnversionedNullValue(missintInt64Id),
+
+ MakeUnversionedInt64Value(12, tableIndexColumnId),
+ MakeUnversionedInt64Value(42, rowIndexColumnId),
+ MakeUnversionedInt64Value(333, rangeIndexColumnId),
+ }) {
+ builder.AddValue(value);
+ }
+
+ if (!IsLegacyFormat()) {
+ builder.AddValue(MakeUnversionedAnyValue(ysonString, anyWithMapId));
+ builder.AddValue(MakeUnversionedInt64Value(22, anyWithInt64Id));
+ builder.AddValue(MakeUnversionedStringValue("some_string", anyWithStringId));
+
+ builder.AddValue(MakeUnversionedInt64Value(-123, otherInt64ColumnId));
+ builder.AddValue(MakeUnversionedDoubleValue(-123.456, otherDoubleColumnId));
+ builder.AddValue(MakeUnversionedStringValue("some_string", otherStringColumnId));
+ builder.AddValue(MakeUnversionedBooleanValue(true, otherBooleanColumnId));
+ builder.AddValue(MakeUnversionedAnyValue(ysonString, otherAnyColumnId));
+ builder.AddValue(MakeUnversionedNullValue(otherNullColumnId));
+ }
+
+ auto row = builder.GetRow();
+ std::vector<TUnversionedRow> rows(rowCount, row);
+ writer->Write(rows);
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput input(result);
+ TLenvalParser lenvalParser(&input);
+
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ auto entry = lenvalParser.Next();
+ ASSERT_TRUE(entry);
+
+ NYT::TMessage message;
+ ASSERT_TRUE(message.ParseFromString(entry->RowData));
+
+ EXPECT_DOUBLE_EQ(message.double_field(), 3.14159);
+ EXPECT_FLOAT_EQ(message.float_field(), 2.71828);
+ EXPECT_EQ(message.int64_field(), -1);
+ EXPECT_EQ(message.uint64_field(), 2u);
+ EXPECT_EQ(message.sint64_field(), -3);
+ EXPECT_EQ(message.fixed64_field(), 4u);
+ EXPECT_EQ(message.sfixed64_field(), -5);
+
+ EXPECT_EQ(message.int32_field(), -6);
+ EXPECT_EQ(message.uint32_field(), 7u);
+ EXPECT_EQ(message.sint32_field(), -8);
+ EXPECT_EQ(message.fixed32_field(), 9u);
+ EXPECT_EQ(message.sfixed32_field(), -10);
+
+ EXPECT_EQ(message.bool_field(), true);
+ EXPECT_EQ(message.string_field(), "this_is_string");
+ EXPECT_EQ(message.bytes_field(), "this_is_bytes");
+
+ EXPECT_EQ(message.enum_field(), EEnum::Two);
+
+ EXPECT_EQ(message.message_field().key(), "embedded_key");
+ EXPECT_EQ(message.message_field().value(), "embedded_value");
+
+ if (!IsLegacyFormat()) {
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(message.any_field_with_map())), mapNode));
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(message.any_field_with_int64())),
+ BuildYsonNodeFluently().Value(22)));
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(message.any_field_with_string())),
+ BuildYsonNodeFluently().Value("some_string")));
+
+ auto otherColumnsMap = ConvertToNode(TYsonString(message.other_columns_field()))->AsMap();
+ EXPECT_EQ(otherColumnsMap->GetChildValueOrThrow<i64>("OtherInt64Column"), -123);
+ EXPECT_DOUBLE_EQ(otherColumnsMap->GetChildValueOrThrow<double>("OtherDoubleColumn"), -123.456);
+ EXPECT_EQ(otherColumnsMap->GetChildValueOrThrow<TString>("OtherStringColumn"), "some_string");
+ EXPECT_EQ(otherColumnsMap->GetChildValueOrThrow<bool>("OtherBooleanColumn"), true);
+ EXPECT_TRUE(AreNodesEqual(otherColumnsMap->GetChildOrThrow("OtherAnyColumn"), mapNode));
+ EXPECT_EQ(otherColumnsMap->GetChildOrThrow("OtherNullColumn")->GetType(), ENodeType::Entity);
+
+ auto keys = otherColumnsMap->GetKeys();
+ std::sort(keys.begin(), keys.end());
+ std::vector<TString> expectedKeys = {
+ "OtherInt64Column",
+ "OtherDoubleColumn",
+ "OtherStringColumn",
+ "OtherBooleanColumn",
+ "OtherAnyColumn",
+ "OtherNullColumn"};
+ std::sort(expectedKeys.begin(), expectedKeys.end());
+ EXPECT_EQ(expectedKeys, keys);
+ }
+ }
+
+ ASSERT_FALSE(lenvalParser.Next());
+}
+
+TEST_P(TProtobufFormatAllFields, Parser)
+{
+ auto [rowCount, protoFormatType] = GetParam();
+
+ auto config = CreateAllFieldsConfig(protoFormatType);
+
+ TMessage message;
+ message.set_double_field(3.14159);
+ message.set_float_field(2.71828);
+
+ message.set_int64_field(-1);
+ message.set_uint64_field(2);
+ message.set_sint64_field(-3);
+ message.set_fixed64_field(4);
+ message.set_sfixed64_field(-5);
+
+ message.set_int32_field(-6);
+ message.set_uint32_field(7);
+ message.set_sint32_field(-8);
+ message.set_fixed32_field(9);
+ message.set_sfixed32_field(-10);
+
+ message.set_bool_field(true);
+ message.set_string_field("this_is_string");
+ message.set_bytes_field("this_is_bytes");
+ message.set_enum_field(EEnum::Three);
+
+ message.mutable_message_field()->set_key("embedded_key");
+ message.mutable_message_field()->set_value("embedded_value");
+
+ auto mapNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("Key").Value("Value")
+ .Item("Another")
+ .BeginList()
+ .Item().Value(1)
+ .Item().Value("two")
+ .EndList()
+ .EndMap();
+
+ auto otherColumnsNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("OtherInt64Column").Value(-123)
+ .Item("OtherDoubleColumn").Value(-123.456)
+ .Item("OtherStringColumn").Value("some_string")
+ .Item("OtherBooleanColumn").Value(true)
+ .Item("OtherAnyColumn").Value(mapNode)
+ .Item("OtherNullColumn").Entity()
+ .EndMap();
+
+ if (!IsLegacyFormat()) {
+ message.set_any_field_with_map(ConvertToYsonString(mapNode).ToString());
+ message.set_any_field_with_int64(BuildYsonStringFluently().Value(22).ToString());
+ message.set_any_field_with_string(BuildYsonStringFluently().Value("some_string").ToString());
+ message.set_other_columns_field(ConvertToYsonString(otherColumnsNode).ToString());
+ }
+
+ auto rowCollector = ParseRows(
+ message,
+ ConvertTo<TProtobufFormatConfigPtr>(config->Attributes().ToMap()),
+ New<TTableSchema>(),
+ rowCount);
+
+ for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
+ int expectedSize = IsLegacyFormat() ? 17 : 26;
+ ASSERT_EQ(static_cast<int>(rowCollector.GetRow(rowIndex).GetCount()), expectedSize);
+
+ ASSERT_DOUBLE_EQ(GetDouble(rowCollector.GetRowValue(rowIndex, "Double")), 3.14159);
+ ASSERT_NEAR(GetDouble(rowCollector.GetRowValue(rowIndex, "Float")), 2.71828, 1e-5);
+
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "Int64")), -1);
+ ASSERT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "UInt64")), 2u);
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "SInt64")), -3);
+ ASSERT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "Fixed64")), 4u);
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "SFixed64")), -5);
+
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "Int32")), -6);
+ ASSERT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "UInt32")), 7u);
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "SInt32")), -8);
+ ASSERT_EQ(GetUint64(rowCollector.GetRowValue(rowIndex, "Fixed32")), 9u);
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "SFixed32")), -10);
+
+ ASSERT_EQ(GetBoolean(rowCollector.GetRowValue(rowIndex, "Bool")), true);
+ ASSERT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "String")), "this_is_string");
+ ASSERT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "Bytes")), "this_is_bytes");
+
+ if (IsLegacyFormat()) {
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "Enum")), 3);
+ } else {
+ ASSERT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "Enum")), "Three");
+ }
+
+ TEmbeddedMessage embeddedMessage;
+ ASSERT_TRUE(embeddedMessage.ParseFromString(GetString(rowCollector.GetRowValue(rowIndex, "Message"))));
+ ASSERT_EQ(embeddedMessage.key(), "embedded_key");
+ ASSERT_EQ(embeddedMessage.value(), "embedded_value");
+
+ if (!IsLegacyFormat()) {
+ ASSERT_TRUE(AreNodesEqual(GetAny(rowCollector.GetRowValue(rowIndex, "AnyWithMap")), mapNode));
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "AnyWithInt64")), 22);
+ ASSERT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "AnyWithString")), "some_string");
+
+ ASSERT_EQ(GetInt64(rowCollector.GetRowValue(rowIndex, "OtherInt64Column")), -123);
+ ASSERT_DOUBLE_EQ(GetDouble(rowCollector.GetRowValue(rowIndex, "OtherDoubleColumn")), -123.456);
+ ASSERT_EQ(GetString(rowCollector.GetRowValue(rowIndex, "OtherStringColumn")), "some_string");
+ ASSERT_EQ(GetBoolean(rowCollector.GetRowValue(rowIndex, "OtherBooleanColumn")), true);
+ ASSERT_TRUE(AreNodesEqual(GetAny(rowCollector.GetRowValue(rowIndex, "OtherAnyColumn")), mapNode));
+ ASSERT_EQ(rowCollector.GetRowValue(rowIndex, "OtherNullColumn").Type, EValueType::Null);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufFormatCompat
+ : public ::testing::Test
+{
+public:
+ static TTableSchemaPtr GetEarlySchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", OptionalLogicalType(VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ }))},
+ });
+ return schema;
+ }
+
+ static TTableSchemaPtr GetFirstMiddleSchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", OptionalLogicalType(VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))},
+ {"b", OptionalLogicalType(StructLogicalType({
+ {"x", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))},
+ });
+ return schema;
+ }
+
+ static TTableSchemaPtr GetSecondMiddleSchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", OptionalLogicalType(VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))},
+ {"b", OptionalLogicalType(StructLogicalType({
+ {"x", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"y", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ }))},
+ });
+ return schema;
+ }
+
+ static TTableSchemaPtr GetThirdMiddleSchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", OptionalLogicalType(VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))},
+ {"b", OptionalLogicalType(StructLogicalType({
+ {"x", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"y", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"z", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ }))},
+ });
+ return schema;
+ }
+
+ static TTableSchemaPtr GetLateSchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", OptionalLogicalType(VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"f3", SimpleLogicalType(ESimpleLogicalValueType::Boolean)},
+ }))},
+ {"c", OptionalLogicalType(ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean)))},
+ {"b", OptionalLogicalType(StructLogicalType({
+ {"x", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"y", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"z", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ }))},
+ });
+ return schema;
+ }
+
+ static TProtobufFormatConfigPtr GetFirstMiddleConfig()
+ {
+ static const auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap().Item("tables").BeginList().Item().BeginMap().Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("a")
+ .Item("field_number").Value(0)
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("f1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("b")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("x")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList().EndMap().EndList().EndMap());
+ return config;
+ }
+
+ static TProtobufFormatConfigPtr GetSecondMiddleConfig()
+ {
+ static const auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap().Item("tables").BeginList().Item().BeginMap().Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("a")
+ .Item("field_number").Value(0)
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("f1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("f2")
+ .Item("field_number").Value(101)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("b")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields")
+ .BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("x")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("y")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList().EndMap().EndList().EndMap());
+ return config;
+ }
+};
+
+template <typename TMessage>
+TMessage WriteRow(
+ TUnversionedRow row,
+ const TProtobufFormatConfigPtr& config,
+ const TTableSchemaPtr& schema,
+ const TNameTablePtr& nameTable)
+{
+ TString result;
+ TStringOutput resultStream(result);
+
+ auto writer = CreateWriterForProtobuf(
+ config,
+ {schema},
+ nameTable,
+ CreateAsyncAdapter(&resultStream),
+ true,
+ New<TControlAttributesConfig>(),
+ 0);
+ writer->Write(std::vector<TUnversionedRow>{row});
+ writer->Close().Get().ThrowOnError();
+
+ TStringInput input(result);
+ TLenvalParser lenvalParser(&input);
+ auto entry = lenvalParser.Next();
+ if (!entry) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream in lenval parser");
+ }
+ TMessage message;
+ if (!message.ParseFromString(entry->RowData)) {
+ THROW_ERROR_EXCEPTION("Failed to parse message");
+ }
+ if (lenvalParser.Next()) {
+ THROW_ERROR_EXCEPTION("Unexpected entry in lenval parser");
+ }
+ return message;
+}
+
+TEST_F(TProtobufFormatCompat, Write)
+{
+ auto nameTable = TNameTable::FromSchema(*GetLateSchema());
+ auto config = GetSecondMiddleConfig();
+
+ auto writeRow = [&] (TUnversionedRow row, const TTableSchemaPtr& schema) {
+ return WriteRow<NYT::TCompatMessage>(row, config, schema, nameTable);
+ };
+
+ {
+ auto earlyRow = MakeRow(nameTable, {
+ {"a", EValueType::Composite, "[0; -24]"}
+ });
+
+ SCOPED_TRACE("early");
+ auto message = writeRow(earlyRow, GetEarlySchema());
+ EXPECT_EQ(message.f1(), -24);
+ EXPECT_FALSE(message.has_f2());
+ EXPECT_EQ(message.has_b(), false);
+ }
+ {
+ auto firstMiddleRow = MakeRow(nameTable, {
+ {"a", EValueType::Composite, "[1; foobar]"},
+ {"b", EValueType::Composite, "[foo]"},
+ });
+
+ SCOPED_TRACE("firstMiddle");
+ auto message = writeRow(firstMiddleRow, GetFirstMiddleSchema());
+ EXPECT_FALSE(message.has_f1());
+ EXPECT_EQ(message.f2(), "foobar");
+ EXPECT_EQ(message.b().x(), "foo");
+ EXPECT_EQ(message.b().has_y(), false);
+ }
+ {
+ auto secondMiddleRow = MakeRow(nameTable, {
+ {"a", EValueType::Composite, "[1; foobar]"},
+ {"b", EValueType::Composite, "[foo; bar]"},
+ });
+
+ SCOPED_TRACE("secondMiddle");
+ auto message = writeRow(secondMiddleRow, GetSecondMiddleSchema());
+ EXPECT_FALSE(message.has_f1());
+ EXPECT_EQ(message.f2(), "foobar");
+ EXPECT_EQ(message.b().x(), "foo");
+ EXPECT_EQ(message.b().y(), "bar");
+ }
+ {
+ auto thirdMiddleRow = MakeRow(nameTable, {
+ {"a", EValueType::Composite, "[1; foobar]"},
+ {"b", EValueType::Composite, "[foo; bar; spam]"},
+ });
+
+ SCOPED_TRACE("thirdMiddle");
+ auto message = writeRow(thirdMiddleRow, GetThirdMiddleSchema());
+ EXPECT_FALSE(message.has_f1());
+ EXPECT_EQ(message.f2(), "foobar");
+ EXPECT_EQ(message.b().x(), "foo");
+ EXPECT_EQ(message.b().y(), "bar");
+ }
+ {
+ auto lateRow = MakeRow(nameTable, {
+ {"a", EValueType::Composite, "[2; %true]"},
+ {"c", EValueType::Composite, "[%false; %true; %false]"},
+ {"b", EValueType::Composite, "[foo; bar; spam]"},
+ });
+
+ SCOPED_TRACE("late");
+ auto message = writeRow(lateRow, GetLateSchema());
+ EXPECT_FALSE(message.has_f1());
+ EXPECT_FALSE(message.has_f2());
+ EXPECT_EQ(message.b().x(), "foo");
+ EXPECT_EQ(message.b().y(), "bar");
+ }
+}
+
+TEST_F(TProtobufFormatCompat, Parse)
+{
+ auto config = GetSecondMiddleConfig();
+
+ NYT::TCompatMessage message;
+ message.set_f2("Sandiego");
+ message.mutable_b()->set_x("foo");
+ message.mutable_b()->set_y("bar");
+
+ {
+ SCOPED_TRACE("early");
+ auto collector = ParseRows(message, config, GetEarlySchema());
+ EXPECT_FALSE(collector.FindRowValue(0, "a"));
+ EXPECT_FALSE(collector.GetNameTable()->FindId("b"));
+ EXPECT_FALSE(collector.GetNameTable()->FindId("c"));
+ }
+ {
+ SCOPED_TRACE("firstMiddle");
+ auto collector = ParseRows(message, config, GetFirstMiddleSchema());
+ EXPECT_NODES_EQUAL(
+ GetComposite(collector.GetRowValue(0, "a")),
+ ConvertToNode(TYsonString(TStringBuf("[1;Sandiego]"))));
+ EXPECT_NODES_EQUAL(GetComposite(collector.GetRowValue(0, "b")), ConvertToNode(TYsonString(TStringBuf("[foo]"))));
+ EXPECT_FALSE(collector.GetNameTable()->FindId("c"));
+ }
+ {
+ SCOPED_TRACE("secondMiddle");
+ auto collector = ParseRows(message, config, GetSecondMiddleSchema());
+ EXPECT_NODES_EQUAL(
+ GetComposite(collector.GetRowValue(0, "a")),
+ ConvertToNode(TYsonString(TStringBuf("[1;Sandiego]"))));
+ EXPECT_NODES_EQUAL(GetComposite(collector.GetRowValue(0, "b")), ConvertToNode(TYsonString(TStringBuf("[foo;bar]"))));
+ EXPECT_FALSE(collector.GetNameTable()->FindId("c"));
+ }
+ {
+ SCOPED_TRACE("thirdMiddle");
+ auto collector = ParseRows(message, config, GetThirdMiddleSchema());
+ EXPECT_NODES_EQUAL(
+ GetComposite(collector.GetRowValue(0, "a")),
+ ConvertToNode(TYsonString(TStringBuf("[1;Sandiego]"))));
+ EXPECT_NODES_EQUAL(GetComposite(collector.GetRowValue(0, "b")), ConvertToNode(TYsonString(TStringBuf("[foo;bar;#]"))));
+ EXPECT_FALSE(collector.GetNameTable()->FindId("c"));
+ }
+ {
+ SCOPED_TRACE("late");
+ auto collector = ParseRows(message, config, GetLateSchema());
+ EXPECT_NODES_EQUAL(
+ GetComposite(collector.GetRowValue(0, "a")),
+ ConvertToNode(TYsonString(TStringBuf("[1;Sandiego]"))));
+ EXPECT_NODES_EQUAL(GetComposite(collector.GetRowValue(0, "b")), ConvertToNode(TYsonString(TStringBuf("[foo;bar;#]"))));
+ EXPECT_TRUE(collector.GetNameTable()->FindId("c"));
+ }
+}
+
+TEST_F(TProtobufFormatCompat, ParseWrong)
+{
+ NYT::TCompatMessage message;
+ message.set_f1(42);
+ message.mutable_b()->set_x("foo");
+ message.mutable_b()->set_y("bar");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetFirstMiddleConfig(), GetFirstMiddleSchema()),
+ "Unexpected field number 2");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufFormatEnumCompat
+ : public ::testing::Test
+{
+public:
+ static TTableSchemaPtr CreateTableSchema()
+ {
+ static const auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"optional_enum", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"required_enum", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"repeated_enum", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"packed_repeated_enum", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"inner", OptionalLogicalType(StructLogicalType({
+ {"optional_enum", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"required_enum", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"repeated_enum", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"packed_repeated_enum", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ }))},
+ });
+ return schema;
+ }
+ static TProtobufFormatConfigPtr CreateProtobufFormatConfig()
+ {
+ static const auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("enumerations").BeginMap()
+ .Item("ECompatEnum")
+ .BeginMap()
+ .Item("One").Value(1)
+ .Item("Two").Value(2)
+ .Item("Three").Value(3)
+ .EndMap()
+ .EndMap()
+ .Item("tables").BeginList().Item().BeginMap().Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("optional_enum")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("enum_string")
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("required_enum")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("enum_string")
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("repeated_enum")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("enum_string")
+ .Item("repeated").Value(true)
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("packed_repeated_enum")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("enum_string")
+ .Item("repeated").Value(true)
+ .Item("packed").Value(true)
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("inner")
+ .Item("field_number").Value(100)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("optional_enum")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("enum_string")
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("required_enum")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("enum_string")
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("repeated_enum")
+ .Item("field_number").Value(3)
+ .Item("proto_type").Value("enum_string")
+ .Item("repeated").Value(true)
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("packed_repeated_enum")
+ .Item("field_number").Value(4)
+ .Item("proto_type").Value("enum_string")
+ .Item("repeated").Value(true)
+ .Item("packed").Value(true)
+ .Item("enum_writing_mode").Value("skip_unknown_values")
+ .Item("enumeration_name").Value("ECompatEnum")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList().EndMap().EndList().EndMap());
+ return config;
+ }
+
+};
+
+TEST_F(TProtobufFormatEnumCompat, WriteCanSkipUnknownEnumValues)
+{
+ auto schema = CreateTableSchema();
+ auto config = CreateProtobufFormatConfig();
+
+ auto nameTable = TNameTable::FromSchema(*schema);
+
+ auto row = MakeRow(nameTable, {
+ {"optional_enum", "MinusFortyTwo"},
+ {"required_enum", "One"},
+ {"repeated_enum", EValueType::Composite, "[MinusFortyTwo;One;MinusFortyTwo]"},
+ {"packed_repeated_enum", EValueType::Composite, "[MinusFortyTwo;Two;MinusFortyTwo]"},
+ {"inner", EValueType::Composite, "[MinusFortyTwo;Two;[MinusFortyTwo;Two];[One;MinusFortyTwo]]"},
+ });
+
+ auto collectRepeated = [](const auto& repeated) {
+ std::vector<TEnumCompat::ECompatEnum> values;
+ for (auto value : repeated) {
+ values.push_back(static_cast<TEnumCompat::ECompatEnum>(value));
+ }
+ return values;
+ };
+
+ auto message = WriteRow<TEnumCompat>(row, config, schema, nameTable);
+
+ EXPECT_FALSE(message.has_optional_enum());
+ EXPECT_EQ(message.required_enum(), TEnumCompat::One);
+ EXPECT_EQ(collectRepeated(message.repeated_enum()), std::vector{TEnumCompat::One});
+ EXPECT_EQ(collectRepeated(message.packed_repeated_enum()), std::vector{TEnumCompat::Two});
+
+ ASSERT_TRUE(message.has_inner());
+ EXPECT_FALSE(message.inner().has_optional_enum());
+ EXPECT_EQ(message.inner().required_enum(), TEnumCompat::Two);
+ EXPECT_EQ(collectRepeated(message.inner().repeated_enum()), std::vector{TEnumCompat::Two});
+ EXPECT_EQ(collectRepeated(message.inner().packed_repeated_enum()), std::vector{TEnumCompat::One});
+}
+
+TEST_F(TProtobufFormatEnumCompat, WriteDoesntSkipRequiredFields)
+{
+ auto schema = CreateTableSchema();
+ auto config = CreateProtobufFormatConfig();
+
+ auto nameTable = TNameTable::FromSchema(*schema);
+
+ {
+ auto row = MakeRow(nameTable, {{"required_enum", "MinusFortyTwo"}});
+ EXPECT_THROW_WITH_SUBSTRING(WriteRow<TEnumCompat>(row, config, schema, nameTable), "Invalid value for enum");
+ }
+ {
+ auto row = MakeRow(nameTable, {{"inner", EValueType::Composite, "[#;MinusFortyTwo;#;#]"},});
+ EXPECT_THROW_WITH_SUBSTRING(WriteRow<TEnumCompat>(row, config, schema, nameTable), "Invalid value for enum");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufFormatRuntimeErrors
+ : public ::testing::Test
+{
+public:
+ static TTableSchemaPtr GetSchemaWithVariant(bool optional = false)
+ {
+ auto variantType = VariantStructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ });
+ return New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", optional ? OptionalLogicalType(variantType) : variantType},
+ });
+ }
+
+ static TTableSchemaPtr GetSchemaWithStruct(bool optional = false)
+ {
+ auto structType = StructLogicalType({
+ {"f1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"f2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ });
+ return New<TTableSchema>(std::vector<TColumnSchema>{
+ {"a", optional ? OptionalLogicalType(structType) : structType},
+ });
+ }
+
+ static TProtobufFormatConfigPtr GetConfigWithVariant()
+ {
+ static const auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap().Item("tables").BeginList().Item().BeginMap().Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("a")
+ .Item("proto_type").Value("oneof")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("f1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("f2")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList().EndMap().EndList().EndMap());
+ return config;
+ }
+
+ static TProtobufFormatConfigPtr GetConfigWithStruct()
+ {
+ static const auto config = ConvertTo<TProtobufFormatConfigPtr>(BuildYsonNodeFluently()
+ .BeginMap().Item("tables").BeginList().Item().BeginMap().Item("columns").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("a")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("structured_message")
+ .Item("fields").BeginList()
+ .Item().BeginMap()
+ .Item("name").Value("f1")
+ .Item("field_number").Value(1)
+ .Item("proto_type").Value("int64")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("name").Value("f2")
+ .Item("field_number").Value(2)
+ .Item("proto_type").Value("string")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList().EndMap().EndList().EndMap());
+ return config;
+ }
+};
+
+TEST_F(TProtobufFormatRuntimeErrors, ParseVariant)
+{
+ {
+ SCOPED_TRACE("Optional variant, all missing");
+ TMessageWithOneof message;
+ auto collector = ParseRows(message, GetConfigWithVariant(), GetSchemaWithVariant(/* optional */ true));
+ EXPECT_FALSE(collector.FindRowValue(0, "a"));
+ }
+ {
+ SCOPED_TRACE("All missing");
+ TMessageWithOneof message;
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetConfigWithVariant(), GetSchemaWithVariant()),
+ "required field \"<root>.a\" is missing");
+ }
+ {
+ SCOPED_TRACE("two alternatives");
+ TMessageWithStruct::TStruct message;
+ message.set_f1(5);
+ message.set_f2("boo");
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetConfigWithVariant(), GetSchemaWithVariant()),
+ "multiple entries for oneof field \"<root>.a\"");
+ }
+}
+
+TEST_F(TProtobufFormatRuntimeErrors, ParseStruct)
+{
+ {
+ SCOPED_TRACE("Optional submessage missing");
+ TMessageWithStruct message;
+ auto collector = ParseRows(message, GetConfigWithStruct(), GetSchemaWithStruct(/* optional */ true));
+ EXPECT_FALSE(collector.FindRowValue(0, "a"));
+ }
+ {
+ SCOPED_TRACE("Required submessage missing");
+ TMessageWithStruct message;
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetConfigWithStruct(), GetSchemaWithStruct()),
+ "required field \"<root>.a\" is missing");
+ }
+ {
+ SCOPED_TRACE("All fields missing");
+ TMessageWithStruct message;
+ message.mutable_a();
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetConfigWithStruct(), GetSchemaWithStruct()),
+ "required field \"<root>.a.f1\" is missing");
+ }
+ {
+ SCOPED_TRACE("Second field missing");
+ TMessageWithStruct message;
+ message.mutable_a()->set_f1(17);
+ EXPECT_THROW_WITH_SUBSTRING(
+ ParseRows(message, GetConfigWithStruct(), GetSchemaWithStruct()),
+ "required field \"<root>.a.f2\" is missing");
+ }
+ {
+ SCOPED_TRACE("All present");
+ TMessageWithStruct message;
+ message.mutable_a()->set_f1(17);
+ message.mutable_a()->set_f2("foobar");
+ auto collector = ParseRows(message, GetConfigWithStruct(), GetSchemaWithStruct());
+ EXPECT_NODES_EQUAL(
+ GetComposite(collector.GetRowValue(0, "a")),
+ ConvertToNode(TYsonString(TStringBuf("[17;foobar]"))));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/client/unittests/protobuf_format_ut.proto b/yt/yt/client/unittests/protobuf_format_ut.proto
new file mode 100644
index 0000000000..06258de619
--- /dev/null
+++ b/yt/yt/client/unittests/protobuf_format_ut.proto
@@ -0,0 +1,255 @@
+import "yt/yt_proto/yt/formats/extension.proto";
+
+package NYT.NProtobufFormatTest;
+
+enum EEnum
+{
+ One = 1;
+ Two = 2;
+ Three = 3;
+
+ MinusFortyTwo = -42;
+
+ MinInt32 = -2147483648;
+ MaxInt32 = 2147483647;
+}
+
+message TEmbeddedStruct {
+ optional float float1 = 1;
+ optional string string1 = 2;
+};
+
+message TEmbedded2Message {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional uint64 embedded2_num = 10;
+ optional TEmbeddedStruct embedded2_struct = 17;
+ repeated string embedded2_repeated = 42;
+};
+
+message TEmbedded1Message {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ optional TEmbedded2Message t2 = 1 [(NYT.flags) = EMBEDDED];
+ oneof variant {
+ string str_variant = 101;
+ uint64 uint_variant = 102;
+ }
+ optional uint64 embedded_num = 10; // make intentional field_num collision!
+ optional string embedded_extra_field = 11;
+};
+message TEmbeddingMessage {
+ optional bytes other_columns_field = 15 [(NYT.flags) = OTHER_COLUMNS];
+ optional TEmbedded1Message t1 = 2 [(NYT.flags) = EMBEDDED];
+ optional uint64 num = 12;
+ optional string extra_field = 13;
+};
+
+message TEmbeddedMessage
+{
+ optional string key = 1;
+ optional string value = 2;
+}
+
+message TMessageWithStructuredEmbedded
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ message TFirstMessage
+ {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ optional EEnum enum_field = 1 [(NYT.flags) = ENUM_STRING];
+ optional int64 int64_field = 2;
+ repeated int64 repeated_int64_field = 3;
+ optional TEmbeddedMessage message_field = 4;
+ repeated TEmbeddedMessage repeated_message_field = 5;
+ optional bytes any_int64_field = 6 [(NYT.flags) = ANY];
+ optional bytes any_map_field = 7 [(NYT.flags) = ANY];
+ optional int64 optional_int64_field = 8;
+ repeated int64 another_repeated_int64_field = 9;
+ repeated bytes repeated_optional_any_field = 10 [(NYT.flags) = ANY];
+ repeated EEnum packed_repeated_enum_field = 11 [packed=true, (NYT.flags) = ENUM_STRING];
+ repeated bool optional_repeated_bool_field = 12;
+ oneof oneof_field {
+ string oneof_string_field_1 = 101;
+ string oneof_string_field = 102;
+ TEmbeddedMessage oneof_message_field = 1000;
+ }
+ oneof optional_oneof_field {
+ string optional_oneof_string_field_1 = 201;
+ string optional_oneof_string_field = 202;
+ TEmbeddedMessage optional_oneof_message_field = 2000;
+ }
+ map<int64, TEmbeddedMessage> map_field = 13 [(NYT.flags) = MAP_AS_DICT];
+ }
+
+ message TSecondMessage
+ {
+ optional int64 one = 2;
+ optional int64 two = 500000000;
+ optional int64 three = 100500;
+ }
+
+ optional TFirstMessage first = 1;
+ optional TSecondMessage second = 2;
+ repeated TEmbeddedMessage repeated_message_field = 3;
+ repeated int64 repeated_int64_field = 4;
+ optional int64 int64_any_field = 5 [(NYT.column_name) = "any_field"];
+
+ optional int32 int32_field = 6 [(NYT.column_name) = "int64_field"];
+ optional uint32 uint32_field = 7 [(NYT.column_name) = "uint64_field"];
+ optional int64 int64_field = 8 [(NYT.column_name) = "int32_field"];
+ optional uint64 uint64_field = 9 [(NYT.column_name) = "uint32_field"];
+
+ optional EEnum enum_int_field = 10 [(NYT.flags) = ENUM_INT];
+ optional EEnum enum_string_string_field = 11 [(NYT.flags) = ENUM_STRING];
+ optional EEnum enum_string_int64_field = 12 [(NYT.flags) = ENUM_STRING];
+
+
+ repeated int64 another_repeated_int64_field = 13;
+
+ repeated bytes repeated_optional_any_field = 14 [(NYT.flags) = ANY];
+
+ optional bytes other_columns_field = 15 [(NYT.flags) = OTHER_COLUMNS];
+
+ optional string utf8_field = 16;
+
+ repeated int64 packed_repeated_int64_field = 17 [packed=true];
+
+ repeated int64 optional_repeated_int64_field = 18;
+
+ oneof oneof_field {
+ string oneof_string_field_1 = 101;
+ string oneof_string_field = 102;
+ TEmbeddedMessage oneof_message_field = 1000;
+ }
+
+ oneof optional_oneof_field {
+ string optional_oneof_string_field_1 = 201;
+ string optional_oneof_string_field = 202;
+ TEmbeddedMessage optional_oneof_message_field = 2000;
+ }
+
+ map<int64, TEmbeddedMessage> map_field = 19 [(NYT.flags) = MAP_AS_DICT];
+}
+
+message TSeveralTablesMessageFirst
+{
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+
+ message TEmbedded
+ {
+ optional EEnum enum_field = 1 [(NYT.flags) = ENUM_STRING];
+ optional int64 int64_field = 2;
+ }
+ optional TEmbedded embedded = 1;
+ repeated int64 repeated_int64_field = 2;
+ optional int64 int64_field = 3 [(NYT.column_name) = "any_field"];
+}
+
+message TSeveralTablesMessageSecond
+{
+ optional EEnum enum_field = 1 [(NYT.flags) = ENUM_STRING];
+ optional int64 int64_field = 2;
+}
+
+message TSeveralTablesMessageThird
+{
+ optional string string_field = 1;
+}
+
+message TMessage
+{
+ optional double double_field = 1 [(NYT.column_name) = "Double"];
+ optional float float_field = 2 [(NYT.column_name) = "Float"];
+
+ optional int64 int64_field = 3 [(NYT.column_name) = "Int64"];
+ optional uint64 uint64_field = 4 [(NYT.column_name) = "UInt64"];
+ optional sint64 sint64_field = 5 [(NYT.column_name) = "SInt64"];
+ optional fixed64 fixed64_field = 6 [(NYT.column_name) = "Fixed64"];
+ optional sfixed64 sfixed64_field = 7 [(NYT.column_name) = "SFixed64"];
+
+ optional int32 int32_field = 8 [(NYT.column_name) = "Int32"];
+ optional uint32 uint32_field = 9 [(NYT.column_name) = "UInt32"];
+ optional sint32 sint32_field = 10 [(NYT.column_name) = "SInt32"];
+ optional fixed32 fixed32_field = 11 [(NYT.column_name) = "Fixed32"];
+ optional sfixed32 sfixed32_field = 12 [(NYT.column_name) = "SFixed32"];
+
+ optional bool bool_field = 13 [(NYT.column_name) = "Bool"];
+ optional string string_field = 14 [(NYT.column_name) = "String"];
+ optional bytes bytes_field = 15 [(NYT.column_name) = "Bytes"];
+
+ optional EEnum enum_field = 16 [(NYT.column_name) = "Enum", (NYT.flags) = ENUM_STRING];
+ optional TEmbeddedMessage message_field = 17 [(NYT.column_name) = "Message"];
+
+ optional bytes any_field_with_map = 18 [(NYT.column_name) = "AnyWithMap", (NYT.flags) = ANY];
+ optional bytes any_field_with_int64 = 19 [(NYT.column_name) = "AnyWithInt64", (NYT.flags) = ANY];
+ optional bytes any_field_with_string = 20 [(NYT.column_name) = "AnyWithString", (NYT.flags) = ANY];
+ optional bytes other_columns_field = 21 [(NYT.flags) = OTHER_COLUMNS];
+
+ optional int64 missing_int64_field = 22 [(NYT.column_name) = "MissingInt64"];
+}
+
+message TCompatMessage
+{
+ message TEmbedded
+ {
+ optional string x = 1;
+ optional string y = 2;
+ }
+
+ oneof a {
+ int64 f1 = 1;
+ string f2 = 101;
+ }
+ optional TEmbedded b = 2;
+}
+
+message TMessageWithOneof
+{
+ oneof variant {
+ int64 f1 = 1;
+ string f2 = 2;
+ }
+}
+
+message TMessageWithStruct
+{
+ message TStruct
+ {
+ optional int64 f1 = 1;
+ optional string f2 = 2;
+ }
+ optional TStruct a = 1;
+}
+
+message TOtherColumnsMessage
+{
+ optional bytes other_columns_field = 1 [(NYT.flags) = OTHER_COLUMNS];
+}
+
+message TEnumCompat {
+ option (NYT.default_field_flags) = SERIALIZATION_YT;
+ option (NYT.default_field_flags) = ENUM_SKIP_UNKNOWN_VALUES;
+
+ enum ECompatEnum {
+ One = 1;
+ Two = 2;
+ Three = 3;
+ }
+
+
+ message TStruct
+ {
+ optional ECompatEnum optional_enum = 1;
+ required ECompatEnum required_enum = 2;
+ repeated ECompatEnum repeated_enum = 3;
+ repeated ECompatEnum packed_repeated_enum = 4 [packed=true, (NYT.flags) = ENUM_STRING];
+ }
+
+ optional ECompatEnum optional_enum = 1;
+ required ECompatEnum required_enum = 2;
+ repeated ECompatEnum repeated_enum = 3;
+ repeated ECompatEnum packed_repeated_enum = 4 [packed=true, (NYT.flags) = ENUM_STRING];
+
+ optional TStruct inner = 100;
+}
diff --git a/yt/yt/client/unittests/query_builder_ut.cpp b/yt/yt/client/unittests/query_builder_ut.cpp
new file mode 100644
index 0000000000..30b8045b5c
--- /dev/null
+++ b/yt/yt/client/unittests/query_builder_ut.cpp
@@ -0,0 +1,48 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/query_client/query_builder.h>
+
+namespace NYT::NQueryClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TQueryBuilderTest, Simple)
+{
+ TQueryBuilder b;
+ int xIndex = b.AddSelectExpression("x");
+ int yIndex = b.AddSelectExpression("y", "y_alias");
+ int zIndex = b.AddSelectExpression("z");
+
+ b.SetSource("//t");
+
+ b.AddWhereConjunct("x > y_alias");
+ b.AddWhereConjunct("y = 177 OR y % 2 = 0");
+
+ b.AddOrderByAscendingExpression("z");
+ b.AddOrderByDescendingExpression("x");
+ b.AddOrderByExpression("x + y", EOrderByDirection::Descending);
+ b.AddOrderByExpression("z - y_alias");
+
+ b.AddGroupByExpression("x + y * z", "group_expr");
+ b.AddGroupByExpression("x - 1");
+
+ b.SetLimit(43);
+
+ EXPECT_EQ(xIndex, 0);
+ EXPECT_EQ(yIndex, 1);
+ EXPECT_EQ(zIndex, 2);
+
+ EXPECT_EQ(b.Build(),
+ "(x), (y) AS y_alias, (z) "
+ "FROM [//t] "
+ "WHERE (x > y_alias) AND (y = 177 OR y % 2 = 0) "
+ "ORDER BY (z) ASC, (x) DESC, (x + y) DESC, (z - y_alias) "
+ "GROUP BY (x + y * z) AS group_expr, (x - 1) "
+ "LIMIT 43");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NQueryClient
diff --git a/yt/yt/client/unittests/read_limit_ut.cpp b/yt/yt/client/unittests/read_limit_ut.cpp
new file mode 100644
index 0000000000..1f08d4c945
--- /dev/null
+++ b/yt/yt/client/unittests/read_limit_ut.cpp
@@ -0,0 +1,196 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/client/table_client/key_bound.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NChunkClient {
+namespace {
+
+using namespace NTableClient;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+NTableClient::TUnversionedOwningRow MakeRow(std::vector<int> values)
+{
+ TUnversionedOwningRowBuilder builder;
+ for (auto value : values) {
+ builder.AddValue(MakeUnversionedInt64Value(value));
+ }
+
+ return builder.FinishRow();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+TString DumpToYson(T obj)
+{
+ return ConvertToYsonString(obj, NYson::EYsonFormat::Text).ToString();
+}
+
+TEST(TLegacyReadLimitTest, Simple)
+{
+ TLegacyReadLimit limit;
+ EXPECT_EQ("{}", DumpToYson(limit));
+ EXPECT_TRUE(limit.IsTrivial());
+
+ limit.SetRowIndex(0);
+ EXPECT_EQ("{\"row_index\"=0;}", DumpToYson(limit));
+ EXPECT_FALSE(limit.IsTrivial());
+}
+
+TEST(TLegacyReadRangeTest, Simple)
+{
+ TLegacyReadRange range;
+ EXPECT_EQ("{}", DumpToYson(range));
+}
+
+TEST(TLegacyReadRangeTest, Simple2)
+{
+ auto range = ConvertTo<TLegacyReadRange>(TYsonString(TStringBuf("{lower_limit={row_index=1};upper_limit={row_index=2}}")));
+ EXPECT_EQ(1, range.LowerLimit().GetRowIndex());
+ EXPECT_EQ(2, range.UpperLimit().GetRowIndex());
+}
+
+TEST(TReadLimitTest, ProtobufConversion)
+{
+ TReadLimit readLimit;
+ auto keyBound = TOwningKeyBound::FromRow(/* prefix */MakeRow({1, 42}), /* isUpper */true, /* isInclusive */true);
+ readLimit.KeyBound() = keyBound;
+ readLimit.SetRowIndex(1);
+ readLimit.SetOffset(2);
+ readLimit.SetChunkIndex(3);
+ readLimit.SetTabletIndex(4);
+
+ NProto::TReadLimit protoReadLimit;
+ ToProto(&protoReadLimit, readLimit);
+
+ TReadLimit newReadLimit(protoReadLimit, /* isUpper */true);
+
+ EXPECT_EQ(newReadLimit.KeyBound(), keyBound);
+ EXPECT_EQ(newReadLimit.GetRowIndex(), 1);
+ EXPECT_EQ(newReadLimit.GetOffset(), 2);
+ EXPECT_EQ(newReadLimit.GetChunkIndex(), 3);
+ EXPECT_EQ(newReadLimit.GetTabletIndex(), 4);
+}
+
+TEST(TReadLimitTest, LegacyKey)
+{
+ TReadLimit readLimit;
+ auto keyBound = TOwningKeyBound::FromRow(/* prefix */MakeRow({1, 42}), /* isInclusive */true, /* isUpper */true);
+ readLimit.KeyBound() = keyBound;
+ readLimit.SetRowIndex(1);
+ readLimit.SetOffset(2);
+ readLimit.SetChunkIndex(3);
+ readLimit.SetTabletIndex(4);
+ EXPECT_FALSE(readLimit.IsTrivial());
+
+ NProto::TReadLimit protoReadLimit;
+ ToProto(&protoReadLimit, readLimit);
+ protoReadLimit.clear_key_bound_prefix();
+ protoReadLimit.clear_key_bound_is_inclusive();
+
+ TReadLimit newReadLimit(protoReadLimit, /* isUpper */true, /* keyLength */2);
+
+ EXPECT_EQ(newReadLimit.KeyBound(), keyBound);
+ EXPECT_EQ(newReadLimit.GetRowIndex(), 1);
+ EXPECT_EQ(newReadLimit.GetOffset(), 2);
+ EXPECT_EQ(newReadLimit.GetChunkIndex(), 3);
+ EXPECT_EQ(newReadLimit.GetTabletIndex(), 4);
+ EXPECT_FALSE(newReadLimit.IsTrivial());
+}
+
+TEST(TReadLimitTest, Interop)
+{
+ TReadLimit readLimitA;
+ readLimitA.SetRowIndex(1);
+ readLimitA.SetOffset(2);
+ readLimitA.SetChunkIndex(3);
+ readLimitA.SetTabletIndex(4);
+
+ TLegacyReadLimit legacyReadLimitA;
+ legacyReadLimitA.SetRowIndex(1);
+ legacyReadLimitA.SetOffset(2);
+ legacyReadLimitA.SetChunkIndex(3);
+ legacyReadLimitA.SetTabletIndex(4);
+
+ // I am too lazy to write a proper comparison operator,
+ // so let us just compare string read limit representations.
+
+ EXPECT_EQ(ToString(legacyReadLimitA), ToString(ReadLimitToLegacyReadLimit(readLimitA)));
+ EXPECT_EQ(ToString(readLimitA), ToString(ReadLimitFromLegacyReadLimitKeyless(legacyReadLimitA)));
+ EXPECT_EQ(ToString(readLimitA), ToString(ReadLimitFromLegacyReadLimit(legacyReadLimitA, /* isUpper */ true, /* keyLength */ 1)));
+
+ TReadLimit readLimitB;
+ readLimitB.KeyBound() = TOwningKeyBound::FromRow() > MakeRow({42});
+
+ TLegacyReadLimit legacyReadLimitB;
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedInt64Value(42));
+ builder.AddValue(MakeUnversionedSentinelValue(EValueType::Max));
+ legacyReadLimitB.SetLegacyKey(builder.FinishRow());
+
+ EXPECT_EQ(ToString(legacyReadLimitB), ToString(ReadLimitToLegacyReadLimit(readLimitB)));
+ EXPECT_EQ(ToString(readLimitB), ToString(ReadLimitFromLegacyReadLimit(legacyReadLimitB, /* isUpper */ false, /* keyLength */ 1)));
+ // Crashes: ReadLimitFromLegacyReadLimitKeyless(legacyReadLimitB).
+}
+
+TEST(TReadLimitTest, Trivial)
+{
+ TReadLimit readLimit;
+ EXPECT_TRUE(readLimit.IsTrivial());
+
+ NProto::TReadLimit protoReadLimit;
+ ToProto(&protoReadLimit, readLimit);
+
+ TReadLimit newReadLimit(protoReadLimit, /* isUpper */true);
+ EXPECT_TRUE(newReadLimit.IsTrivial());
+}
+
+TEST(TReadRangeTest, ProtobufConversion)
+{
+ TReadRange readRange;
+ auto lowerKeyBound = TOwningKeyBound::FromRow(/* prefix */MakeRow({1, 42}), /* isInclusive */true, /* isUpper */false);
+ readRange.LowerLimit().KeyBound() = lowerKeyBound;
+ readRange.LowerLimit().SetRowIndex(1);
+ auto upperKeyBound = TOwningKeyBound::FromRow(/* prefix */MakeRow({12, 13}), /* isInclusive */true, /* isUpper */true);
+ readRange.UpperLimit().KeyBound() = upperKeyBound;
+ readRange.UpperLimit().SetRowIndex(2);
+
+ NProto::TReadRange protoReadRange;
+ ToProto(&protoReadRange, readRange);
+
+ TReadRange newReadRange(protoReadRange);
+
+ EXPECT_EQ(newReadRange.LowerLimit().KeyBound(), lowerKeyBound);
+ EXPECT_EQ(newReadRange.LowerLimit().GetRowIndex(), 1);
+ EXPECT_EQ(newReadRange.UpperLimit().KeyBound(), upperKeyBound);
+ EXPECT_EQ(newReadRange.UpperLimit().GetRowIndex(), 2);
+}
+
+TEST(TReadRangeTest, Trvial)
+{
+ TReadRange readRange;
+ EXPECT_TRUE(readRange.LowerLimit().IsTrivial());
+ EXPECT_TRUE(readRange.UpperLimit().IsTrivial());
+
+ NProto::TReadRange protoReadRange;
+ ToProto(&protoReadRange, readRange);
+
+ TReadRange newReadRange(protoReadRange);
+
+ EXPECT_TRUE(newReadRange.LowerLimit().IsTrivial());
+ EXPECT_TRUE(newReadRange.UpperLimit().IsTrivial());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NChunkClient
+
diff --git a/yt/yt/client/unittests/replication_progress_ut.cpp b/yt/yt/client/unittests/replication_progress_ut.cpp
new file mode 100644
index 0000000000..0c00a02d69
--- /dev/null
+++ b/yt/yt/client/unittests/replication_progress_ut.cpp
@@ -0,0 +1,410 @@
+#include <yt/yt/client/chaos_client/replication_card.h>
+#include <yt/yt/client/chaos_client/replication_card_serialization.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <library/cpp/iterator/zip.h>
+
+namespace NYT::NChaosClient {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool Equal(const TReplicationProgress& lhs, const TReplicationProgress& rhs)
+{
+ if (lhs.UpperKey != rhs.UpperKey || lhs.Segments.size() != rhs.Segments.size()) {
+ return false;
+ }
+ for (const auto& [lhs, rhs] : Zip(lhs.Segments, rhs.Segments)) {
+ if (lhs.Timestamp != rhs.Timestamp || lhs.LowerKey != rhs.LowerKey) {
+ return false;
+ }
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUpdateReplicationProgressTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ const char*,
+ const char*,
+ const char*>>
+{ };
+
+TEST_P(TUpdateReplicationProgressTest, Simple)
+{
+ const auto& params = GetParam();
+ auto progress = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<0>(params)));
+ const auto& update = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<1>(params)));
+ const auto& expected = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<2>(params)));
+
+ UpdateReplicationProgress(&progress, update);
+
+ EXPECT_TRUE(Equal(progress, expected))
+ << "progress: " << std::get<0>(params) << std::endl
+ << "update: " << std::get<1>(params) << std::endl
+ << "expected: " << std::get<2>(params) << std::endl
+ << "actual: " << ConvertToYsonString(progress, EYsonFormat::Text).AsStringBuf() << std::endl;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TUpdateReplicationProgressTest,
+ TUpdateReplicationProgressTest,
+ ::testing::Values(
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1u}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1u}];upper_key=[<type=max>#]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1u}];upper_key=[<type=max>#]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[2]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1};{lower_key=[2];timestamp=0}];upper_key=[<type=max>#]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[2]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[2]}"),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=2}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[1];timestamp=1};{lower_key=[3];timestamp=3}];upper_key=[4]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1};{lower_key=[2];timestamp=2};{lower_key=[3];timestamp=3};{lower_key=[4];timestamp=2}];upper_key=[<type=max>#]}")
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompareReplicationProgressTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ const char*,
+ const char*,
+ bool>>
+{ };
+
+TEST_P(TCompareReplicationProgressTest, Simple)
+{
+ const auto& params = GetParam();
+ const auto& progress = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<0>(params)));
+ const auto& other = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<1>(params)));
+ bool expected = std::get<2>(params);
+
+ bool result = IsReplicationProgressGreaterOrEqual(progress, other);
+
+ EXPECT_EQ(result, expected)
+ << "progress: " << std::get<0>(params) << std::endl
+ << "other: " << std::get<1>(params) << std::endl
+ << "expected: " << expected << std::endl
+ << "actual: " << result << std::endl;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TCompareReplicationProgressTest,
+ TCompareReplicationProgressTest,
+ ::testing::Values(
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[<type=max>#]}",
+ false),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[1];timestamp=0};];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[<type=max>#]}",
+ false),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[1]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=2}];upper_key=[<type=max>#]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1};{lower_key=[4];timestamp=2}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[1];timestamp=0};{lower_key=[3];timestamp=1}];upper_key=[4]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[1]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1};{lower_key=[2];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1};{lower_key=[2];timestamp=0}];upper_key=[<type=max>#]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[2];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ true),
+ std::make_tuple(
+ "{segments=[{lower_key=[2];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[3];timestamp=0}];upper_key=[<type=max>#]}",
+ false),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}",
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[<type=max>#]}",
+ false)
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGatherReplicationProgressTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ std::vector<const char*>,
+ const char*>>
+{ };
+
+TEST_P(TGatherReplicationProgressTest, Simple)
+{
+ const auto& params = GetParam();
+ const auto& serializedProgresses = std::get<0>(params);
+ const auto& expected = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<1>(params)));
+
+ std::vector<TReplicationProgress> progresses;
+ std::vector<TUnversionedRow> pivotKeys;
+ for (const auto& serialized : serializedProgresses) {
+ progresses.push_back(ConvertTo<TReplicationProgress>(TYsonStringBuf(serialized)));
+ pivotKeys.push_back(progresses.back().Segments.front().LowerKey);
+ }
+
+ auto result = GatherReplicationProgress(progresses, pivotKeys, progresses.back().UpperKey.Get());
+
+ EXPECT_TRUE(IsReplicationProgressEqual(result, expected))
+ << "progresses: " << Format("%v", std::get<0>(params)) << std::endl
+ << "expected: " << std::get<1>(params) << std::endl
+ << "actual: " << ConvertToYsonString(result, EYsonFormat::Text).AsStringBuf() << std::endl;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TGatherReplicationProgressTest,
+ TGatherReplicationProgressTest,
+ ::testing::Values(
+ std::make_tuple(
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[2]}"
+ },
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[2]}"),
+ std::make_tuple(
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[0];timestamp=0}];upper_key=[2]}"
+ },
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[2]}"),
+ std::make_tuple(
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[2]}",
+ "{segments=[{lower_key=[2];timestamp=1}];upper_key=[3]}"
+ },
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[3]}"),
+ std::make_tuple(
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[2]}",
+ "{segments=[{lower_key=[2];timestamp=1}];upper_key=[3]}"
+ },
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1}];upper_key=[3]}")
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TScatterReplicationProgressTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ const char*,
+ const char*,
+ const char*,
+ std::vector<const char*>>>
+{ };
+
+TEST_P(TScatterReplicationProgressTest, Simple)
+{
+ const auto& params = GetParam();
+ const auto& progress = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<0>(params)));
+ const auto& owningPivotKeys = ConvertTo<std::vector<TUnversionedOwningRow>>(TYsonStringBuf(std::get<1>(params)));
+ const auto& upperKey = ConvertTo<TUnversionedOwningRow>(TYsonStringBuf(std::get<2>(params)));
+ const auto& serializedExpected = std::get<3>(params);
+
+ std::vector<TUnversionedRow> pivotKeys;
+ for (const auto& row : owningPivotKeys) {
+ pivotKeys.push_back(row.Get());
+ }
+
+ std::vector<TReplicationProgress> expected;
+ for (const auto& serialized : serializedExpected) {
+ expected.push_back(ConvertTo<TReplicationProgress>(TYsonStringBuf(serialized)));
+ }
+
+ auto result = ScatterReplicationProgress(progress, pivotKeys, upperKey.Get());
+ bool allEqual = true;
+ if (expected.size() != result.size()) {
+ allEqual = false;
+ } else {
+ for (int index = 0; index < std::ssize(expected); ++index) {
+ if (!IsReplicationProgressEqual(result[index], expected[index])) {
+ allEqual = false;
+ break;
+ }
+ }
+ }
+
+ EXPECT_TRUE(allEqual)
+ << "progresses: " << std::get<0>(params) << std::endl
+ << "pivot keys: " << std::get<1>(params) << std::endl
+ << "upper key: " << std::get<2>(params) << std::endl
+ << "expected: " << Format("%v", expected) << std::endl
+ << "actual: " << Format("%v", result) << std::endl;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TScatterReplicationProgressTest,
+ TScatterReplicationProgressTest,
+ ::testing::Values(
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "[[]; [1]]",
+ "[<type=max>#]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[<type=max>#]}"
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[<type=max>#]}",
+ "[[]; [1]]",
+ "[2]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[2]}"
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[2]}",
+ "[[]; [1]]",
+ "[2]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[];timestamp=0}];upper_key=[1]}",
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[2]}"
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1};{lower_key=[4];timestamp=0}];upper_key=[6]}",
+ "[[1]; [3]]",
+ "[5]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[1];timestamp=0};{lower_key=[2];timestamp=1}];upper_key=[3]}",
+ "{segments=[{lower_key=[3];timestamp=1};{lower_key=[4];timestamp=0}];upper_key=[5]}",
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1};{lower_key=[4];timestamp=0}];upper_key=[6]}",
+ "[[1]; [4]]",
+ "[6]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[1];timestamp=0};{lower_key=[2];timestamp=1}];upper_key=[4]}",
+ "{segments=[{lower_key=[4];timestamp=0}];upper_key=[6]}"
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1};{lower_key=[4];timestamp=0}];upper_key=[6]}",
+ "[[3]; [4]]",
+ "[5]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[3];timestamp=1}];upper_key=[4]}",
+ "{segments=[{lower_key=[4];timestamp=0}];upper_key=[5]}"
+ }),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[2];timestamp=1};{lower_key=[4];timestamp=0}];upper_key=[6]}",
+ "[[1]; [2]; [3]; [4]; [5]]",
+ "[6]",
+ std::vector<const char*>{
+ "{segments=[{lower_key=[1];timestamp=0}];upper_key=[2]}",
+ "{segments=[{lower_key=[2];timestamp=1}];upper_key=[3]}",
+ "{segments=[{lower_key=[3];timestamp=1}];upper_key=[4]}",
+ "{segments=[{lower_key=[4];timestamp=0}];upper_key=[5]}",
+ "{segments=[{lower_key=[5];timestamp=0}];upper_key=[6]}"
+ })
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplicationProgressTimestampForKeyTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ const char*,
+ const char*,
+ std::optional<TTimestamp>>>
+{ };
+
+TEST_P(TReplicationProgressTimestampForKeyTest, Simple)
+{
+ const auto& params = GetParam();
+ const auto& progress = ConvertTo<TReplicationProgress>(TYsonStringBuf(std::get<0>(params)));
+ const auto& key = ConvertTo<TUnversionedOwningRow>(TYsonStringBuf(std::get<1>(params)));
+ const auto& expected = std::get<2>(params);
+
+ auto result = FindReplicationProgressTimestampForKey(progress, key.Elements());
+
+ EXPECT_EQ(result, expected)
+ << "progresses: " << std::get<0>(params) << std::endl
+ << "key: " << std::get<1>(params) << std::endl
+ << "expected: " << Format("%v", expected) << std::endl
+ << "actual: " << Format("%v", result) << std::endl;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TReplicationProgressTimestampForKeyTest,
+ TReplicationProgressTimestampForKeyTest,
+ ::testing::Values(
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[<type=max>#]}",
+ "[1]",
+ 1),
+ std::make_tuple(
+ "{segments=[{lower_key=[1];timestamp=1}];upper_key=[<type=max>#]}",
+ "[]",
+ std::nullopt),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[1]}",
+ "[1]",
+ std::nullopt),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1}];upper_key=[1]}",
+ "[2]",
+ std::nullopt),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1}];upper_key=[<type=max>#]}",
+ "[1]",
+ 1),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=0};{lower_key=[1];timestamp=1};{lower_key=[2];timestamp=2};];upper_key=[<type=max>#]}",
+ "[1]",
+ 1),
+ std::make_tuple(
+ "{segments=[{lower_key=[];timestamp=1};{lower_key=[2];timestamp=2};];upper_key=[<type=max>#]}",
+ "[1]",
+ 1)
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NChaosClient
diff --git a/yt/yt/client/unittests/row_helpers.cpp b/yt/yt/client/unittests/row_helpers.cpp
new file mode 100644
index 0000000000..d28628c5ab
--- /dev/null
+++ b/yt/yt/client/unittests/row_helpers.cpp
@@ -0,0 +1,70 @@
+#include "row_helpers.h"
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT {
+
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void EnsureTypesMatch(EValueType expected, EValueType actual)
+{
+ if (expected != actual) {
+ THROW_ERROR_EXCEPTION("Unexpected type of TUnversionedValue: expected %Qlv, actual %Qlv",
+ expected,
+ actual);
+ }
+}
+
+i64 GetInt64(const TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Int64, row.Type);
+ return row.Data.Int64;
+}
+
+ui64 GetUint64(const TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Uint64, row.Type);
+ return row.Data.Uint64;
+}
+
+double GetDouble(const NTableClient::TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Double, row.Type);
+ return row.Data.Double;
+}
+
+bool GetBoolean(const TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Boolean, row.Type);
+ return row.Data.Boolean;
+}
+
+TString GetString(const TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::String, row.Type);
+ return row.AsString();
+}
+
+NYTree::INodePtr GetAny(const NTableClient::TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Any, row.Type);
+ return NYTree::ConvertToNode(NYson::TYsonString(row.AsString()));
+}
+
+NYTree::INodePtr GetComposite(const NTableClient::TUnversionedValue& row)
+{
+ EnsureTypesMatch(EValueType::Composite, row.Type);
+ return NYTree::ConvertToNode(NYson::TYsonString(row.AsString()));
+}
+
+bool IsNull(const NTableClient::TUnversionedValue& row)
+{
+ return row.Type == EValueType::Null;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/unittests/row_helpers.h b/yt/yt/client/unittests/row_helpers.h
new file mode 100644
index 0000000000..4a3fbd854f
--- /dev/null
+++ b/yt/yt/client/unittests/row_helpers.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/value_consumer.h>
+
+#include <vector>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCollectingValueConsumer
+ : public NTableClient::IValueConsumer
+{
+public:
+ explicit TCollectingValueConsumer(NTableClient::TTableSchemaPtr schema = New<NTableClient::TTableSchema>())
+ : Schema_(std::move(schema))
+ { }
+
+ explicit TCollectingValueConsumer(NTableClient::TNameTablePtr nameTable, NTableClient::TTableSchemaPtr schema = New<NTableClient::TTableSchema>())
+ : Schema_(std::move(schema))
+ , NameTable_(std::move(nameTable))
+ { }
+
+ const NTableClient::TNameTablePtr& GetNameTable() const override
+ {
+ return NameTable_;
+ }
+
+ const NTableClient::TTableSchemaPtr& GetSchema() const override
+ {
+ return Schema_;
+ }
+
+ bool GetAllowUnknownColumns() const override
+ {
+ return true;
+ }
+
+ void OnBeginRow() override
+ { }
+
+ void OnValue(const NTableClient::TUnversionedValue& value) override
+ {
+ Builder_.AddValue(value);
+ }
+
+ void OnEndRow() override
+ {
+ RowList_.emplace_back(Builder_.FinishRow());
+ }
+
+ NTableClient::TUnversionedRow GetRow(size_t rowIndex)
+ {
+ return RowList_.at(rowIndex);
+ }
+
+ std::optional<NTableClient::TUnversionedValue> FindRowValue(size_t rowIndex, TStringBuf columnName) const
+ {
+ NTableClient::TUnversionedRow row = RowList_.at(rowIndex);
+ auto id = GetNameTable()->GetIdOrThrow(columnName);
+
+ for (const auto& value : row) {
+ if (value.Id == id) {
+ return value;
+ }
+ }
+ return std::nullopt;
+ }
+
+ NTableClient::TUnversionedValue GetRowValue(size_t rowIndex, TStringBuf columnName) const
+ {
+ auto row = FindRowValue(rowIndex, columnName);
+ if (!row) {
+ THROW_ERROR_EXCEPTION("Cannot find column %Qv", columnName);
+ }
+ return *row;
+ }
+
+ size_t Size() const
+ {
+ return RowList_.size();
+ }
+
+ const std::vector<NTableClient::TUnversionedOwningRow>& GetRowList() const {
+ return RowList_;
+ }
+
+private:
+ const NTableClient::TTableSchemaPtr Schema_;
+ const NTableClient::TNameTablePtr NameTable_ = New<NTableClient::TNameTable>();
+ NTableClient::TUnversionedOwningRowBuilder Builder_;
+ std::vector<NTableClient::TUnversionedOwningRow> RowList_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+i64 GetInt64(const NTableClient::TUnversionedValue& row);
+ui64 GetUint64(const NTableClient::TUnversionedValue& row);
+double GetDouble(const NTableClient::TUnversionedValue& row);
+bool GetBoolean(const NTableClient::TUnversionedValue& row);
+TString GetString(const NTableClient::TUnversionedValue& row);
+NYTree::INodePtr GetAny(const NTableClient::TUnversionedValue& row);
+NYTree::INodePtr GetComposite(const NTableClient::TUnversionedValue& row);
+bool IsNull(const NTableClient::TUnversionedValue& row);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/unittests/row_ut.cpp b/yt/yt/client/unittests/row_ut.cpp
new file mode 100644
index 0000000000..2eacf5e19c
--- /dev/null
+++ b/yt/yt/client/unittests/row_ut.cpp
@@ -0,0 +1,165 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/versioned_row.h>
+#include <yt/yt/client/table_client/row_buffer.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <limits>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CheckSerialize(TUnversionedRow original)
+{
+ auto serialized = NYT::ToProto<TString>(original);
+ auto deserialized = NYT::FromProto<TUnversionedOwningRow>(serialized);
+
+ ASSERT_EQ(original, deserialized);
+}
+
+TEST(TUnversionedRowTest, Serialize1)
+{
+ TUnversionedOwningRowBuilder builder;
+ auto row = builder.FinishRow();
+ CheckSerialize(row);
+}
+
+TEST(TUnversionedRowTest, Serialize2)
+{
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedSentinelValue(EValueType::Null, 0));
+ builder.AddValue(MakeUnversionedInt64Value(42, 1));
+ builder.AddValue(MakeUnversionedDoubleValue(0.25, 2));
+ CheckSerialize(builder.FinishRow());
+}
+
+TEST(TUnversionedRowTest, Serialize3)
+{
+ // TODO(babenko): cannot test Any type at the moment since CompareRowValues does not work
+ // for it.
+ TUnversionedOwningRowBuilder builder;
+ builder.AddValue(MakeUnversionedStringValue("string1", 10));
+ builder.AddValue(MakeUnversionedInt64Value(1234, 20));
+ builder.AddValue(MakeUnversionedStringValue("string2", 30));
+ builder.AddValue(MakeUnversionedDoubleValue(4321.0, 1000));
+ builder.AddValue(MakeUnversionedStringValue("", 10000));
+ CheckSerialize(builder.FinishRow());
+}
+
+TEST(TUnversionedRowTest, Serialize4)
+{
+ // TODO(babenko): cannot test Any type at the moment since CompareRowValues does not work
+ // for it.
+ TUnversionedRowBuilder builder;
+ builder.AddValue(MakeUnversionedStringValue("string1"));
+ builder.AddValue(MakeUnversionedStringValue("string2"));
+ CheckSerialize(builder.GetRow());
+}
+
+TEST(TUnversionedRowTest, Serialize5)
+{
+ CheckSerialize(TUnversionedRow());
+}
+
+TEST(TUnversionedValueTest, CompareNaN)
+{
+ auto nanValue = MakeUnversionedDoubleValue(std::numeric_limits<double>::quiet_NaN());
+ auto doubleValue = MakeUnversionedDoubleValue(3.14);
+ EXPECT_EQ(CompareRowValues(nanValue, nanValue), 0);
+ EXPECT_EQ(CompareRowValues(nanValue, doubleValue), 1);
+ EXPECT_EQ(CompareRowValues(doubleValue, nanValue), -1);
+
+ static const char* stringValueData = "foo";
+ auto stringValue = MakeUnversionedStringValue(stringValueData);
+
+ EXPECT_NO_THROW(CompareRowValues(nanValue, stringValue));
+ EXPECT_NO_THROW(CompareRowValues(stringValue, nanValue));
+}
+
+TEST(TUnversionedValueTest, CompareComposite)
+{
+ auto compositeValue = MakeUnversionedCompositeValue("[]");
+ auto stringValue = MakeUnversionedStringValue("foo");
+ auto anyValue = MakeUnversionedAnyValue("[]");
+ auto nullValue = MakeUnversionedSentinelValue(EValueType::Null);
+ EXPECT_THROW_WITH_SUBSTRING(CompareRowValues(compositeValue, stringValue), "Cannot compare values of types");
+ EXPECT_THROW_WITH_SUBSTRING(CompareRowValues(stringValue, compositeValue), "Cannot compare values of types");
+
+ EXPECT_THROW_WITH_SUBSTRING(CompareRowValues(compositeValue, anyValue), "Cannot compare values of types");
+ EXPECT_THROW_WITH_SUBSTRING(CompareRowValues(anyValue, compositeValue), "Cannot compare values of types");
+
+ EXPECT_TRUE(CompareRowValues(compositeValue, nullValue) > 0);
+ EXPECT_TRUE(CompareRowValues(nullValue, compositeValue) < 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFormatTest, UnversionedValue)
+{
+ auto value = MakeUnversionedInt64Value(123, 7, EValueFlags::Aggregate | EValueFlags::Hunk);
+
+ EXPECT_EQ(Format("%v", value), "%&7#123");
+ EXPECT_EQ(Format("%kv", value), "123");
+}
+
+TEST(TFormatTest, VersionedValue)
+{
+ auto value = MakeInt64Value<TVersionedValue>(123, 7, EValueFlags::Aggregate | EValueFlags::Hunk);
+ value.Timestamp = 0x1234567890abcdef;
+
+ EXPECT_EQ(Format("%v", value), "%&7#123@1234567890abcdef");
+}
+
+TEST(TFormatTest, UnversionedRow)
+{
+ auto value1 = MakeUnversionedInt64Value(123, 2, EValueFlags::Aggregate);
+ auto value2 = MakeUnversionedInt64Value(234, 3, EValueFlags::Hunk);
+ auto value3 = MakeUnversionedInt64Value(345, 4, EValueFlags::Aggregate | EValueFlags::Hunk);
+
+ TUnversionedRowBuilder builder;
+ builder.AddValue(value1);
+ builder.AddValue(value2);
+ builder.AddValue(value3);
+ auto row = builder.GetRow();
+
+ EXPECT_EQ(Format("%v", row), "[%2#123, &3#234, %&4#345]");
+ EXPECT_EQ(Format("%kv", row), "[123, 234, 345]");
+}
+
+TEST(TFormatTest, VersionedRow)
+{
+ auto key1 = MakeUnversionedInt64Value(6, 0);
+ auto key2 = MakeUnversionedInt64Value(7, 1, EValueFlags::Hunk);
+ auto key3 = MakeUnversionedInt64Value(8, 2, EValueFlags::Aggregate);
+
+ auto value1 = MakeInt64Value<TVersionedValue>(123, 3, EValueFlags::Aggregate);
+ value1.Timestamp = 0xaaa;
+ auto value2 = MakeInt64Value<TVersionedValue>(234, 4, EValueFlags::Hunk);
+ value2.Timestamp = 0xbbb;
+ auto value3 = MakeInt64Value<TVersionedValue>(345, 5, EValueFlags::Aggregate | EValueFlags::Hunk);
+ value3.Timestamp = 0xccc;
+
+ TVersionedRowBuilder builder(New<TRowBuffer>());
+ builder.AddKey(key1);
+ builder.AddKey(key2);
+ builder.AddKey(key3);
+
+ builder.AddValue(value1);
+ builder.AddValue(value2);
+ builder.AddValue(value3);
+
+ builder.AddDeleteTimestamp(0xeee);
+
+ auto row = builder.FinishRow();
+
+ EXPECT_EQ(Format("%v", row), "[6, 7, 8 | %3#123@aaa, &4#234@bbb, %&5#345@ccc | ccc, bbb, aaa | eee]");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/schema_ut.cpp b/yt/yt/client/unittests/schema_ut.cpp
new file mode 100644
index 0000000000..3482b866b5
--- /dev/null
+++ b/yt/yt/client/unittests/schema_ut.cpp
@@ -0,0 +1,506 @@
+#include "logical_type_shortcuts.h"
+#include "yt/yt/client/table_client/logical_type.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/schema_serialization_helpers.h>
+
+#include <yt/yt_proto/yt/client/table_chunk_format/proto/chunk_meta.pb.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <random>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TColumnSchema ColumnFromYson(const TString& yson)
+{
+ auto maybeDeletedColumn = ConvertTo<TMaybeDeletedColumnSchema>(TYsonStringBuf(yson));
+ YT_VERIFY(!maybeDeletedColumn.Deleted());
+ return static_cast<TColumnSchema>(maybeDeletedColumn);
+}
+
+TEST(TTableSchemaTest, ColumnTypeV1Deserialization)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ {
+ auto column = ColumnFromYson(
+ "{"
+ " name=x;"
+ " type=int64;"
+ "}");
+ EXPECT_EQ(*column.LogicalType(), *Optional(Int64()));
+ EXPECT_EQ(column.IsOfV1Type(), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Int64), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Uint64), false);
+ EXPECT_EQ(column.CastToV1Type(), ESimpleLogicalValueType::Int64);
+ EXPECT_EQ(column.GetWireType(), EValueType::Int64);
+ EXPECT_EQ(column.Required(), false);
+ EXPECT_EQ(IsV3Composite(column.LogicalType()), false);
+ }
+
+ {
+ auto column = ColumnFromYson(
+ "{"
+ " name=x;"
+ " type=uint64;"
+ " required=%true"
+ "}");
+ EXPECT_EQ(*column.LogicalType(), *Uint64());
+ EXPECT_EQ(column.IsOfV1Type(), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Uint64), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Int64), false);
+ EXPECT_EQ(column.CastToV1Type(), ESimpleLogicalValueType::Uint64);
+ EXPECT_EQ(column.GetWireType(), EValueType::Uint64);
+ EXPECT_EQ(column.Required(), true);
+ EXPECT_EQ(IsV3Composite(column.LogicalType()), false);
+ }
+
+ {
+ auto column = ColumnFromYson(
+ "{"
+ " name=x;"
+ " type=null;"
+ "}");
+ EXPECT_EQ(*column.LogicalType(), *SimpleLogicalType(ESimpleLogicalValueType::Null));
+ EXPECT_EQ(column.Required(), false);
+ EXPECT_EQ(column.IsOfV1Type(), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Null), true);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Int64), false);
+ EXPECT_EQ(column.CastToV1Type(), ESimpleLogicalValueType::Null);
+ EXPECT_EQ(column.GetWireType(), EValueType::Null);
+ EXPECT_EQ(column.Required(), false);
+ EXPECT_EQ(IsV3Composite(column.LogicalType()), false);
+ }
+
+ EXPECT_ANY_THROW(ColumnFromYson(
+ "{"
+ " name=x;"
+ " type=null;"
+ " required=%true;"
+ "}"));
+}
+
+TEST(TTableSchemaTest, ColumnTypeV3Deserialization)
+{
+ using namespace NLogicalTypeShortcuts;
+ auto listUtf8Column = ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=list;
+ item=utf8;
+ }
+ }
+ )");
+ EXPECT_EQ(*listUtf8Column.LogicalType(), *List(Utf8()));
+ EXPECT_EQ(listUtf8Column.Required(), true);
+ EXPECT_EQ(listUtf8Column.IsOfV1Type(), false);
+ EXPECT_EQ(listUtf8Column.IsOfV1Type(ESimpleLogicalValueType::Utf8), false);
+ EXPECT_EQ(listUtf8Column.IsOfV1Type(ESimpleLogicalValueType::Any), false);
+ EXPECT_EQ(listUtf8Column.CastToV1Type(), ESimpleLogicalValueType::Any);
+ EXPECT_EQ(listUtf8Column.GetWireType(), EValueType::Composite);
+ EXPECT_EQ(IsV3Composite(listUtf8Column.LogicalType()), true);
+
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=list;
+ item=utf8;
+ };
+ required=%true;
+ }
+ )");
+ EXPECT_EQ(column, listUtf8Column);
+ }
+
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=list;
+ item=utf8;
+ };
+ type=any;
+ }
+ )");
+ EXPECT_EQ(column, listUtf8Column);
+ }
+
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=optional;
+ item={
+ type_name=optional;
+ item=utf8;
+ }
+ };
+ type=any;
+ required=%false;
+ }
+ )");
+ EXPECT_EQ(*column.LogicalType(), *Optional(Optional(Utf8())));
+ EXPECT_EQ(column.Required(), false);
+ EXPECT_EQ(column.IsOfV1Type(), false);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Utf8), false);
+ EXPECT_EQ(column.IsOfV1Type(ESimpleLogicalValueType::Any), false);
+ EXPECT_EQ(column.CastToV1Type(), ESimpleLogicalValueType::Any);
+ EXPECT_EQ(column.GetWireType(), EValueType::Composite);
+ EXPECT_EQ(IsV3Composite(column.LogicalType()), true);
+ }
+
+ {
+ auto decimalColumn = ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=decimal;
+ precision=4;
+ scale=2;
+ }
+ }
+ )");
+ EXPECT_EQ(*decimalColumn.LogicalType(), *Decimal(4, 2));
+ EXPECT_EQ(decimalColumn.Required(), true);
+ EXPECT_EQ(decimalColumn.IsOfV1Type(), false);
+ EXPECT_EQ(decimalColumn.IsOfV1Type(ESimpleLogicalValueType::String), false);
+ EXPECT_EQ(decimalColumn.IsOfV1Type(ESimpleLogicalValueType::Any), false);
+ EXPECT_EQ(decimalColumn.CastToV1Type(), ESimpleLogicalValueType::String);
+ EXPECT_EQ(decimalColumn.GetWireType(), EValueType::String);
+ EXPECT_EQ(IsV3Composite(decimalColumn.LogicalType()), false);
+ }
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=optional;
+ item={
+ type_name=optional;
+ item=utf8;
+ }
+ };
+ required=%true;
+ }
+ )"),
+ R"("type_v3" does not match "required")"
+ );
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ColumnFromYson(R"(
+ {
+ name=x;
+ type_v3={
+ type_name=optional;
+ item={
+ type_name=optional;
+ item=utf8
+ }
+ };
+ type=utf8;
+ }
+ )"),
+ R"("type_v3" does not match "type")"
+ );
+}
+
+TEST(TTableSchemaTest, MaxInlineHunkSizeSerialization)
+{
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type=string;
+ }
+ )");
+ auto serializedColumn = ConvertToAttributes(column);
+ EXPECT_FALSE(serializedColumn->FindYson("max_inline_hunk_size").operator bool());
+ }
+
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type=string;
+ max_inline_hunk_size=100
+ }
+ )");
+ auto serializedColumn = ConvertToAttributes(column);
+ EXPECT_EQ(100, serializedColumn->Get<i64>("max_inline_hunk_size"));
+ }
+}
+
+TEST(TTableSchemaTest, MaxInlineHunkSizeDeserialization)
+{
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type=string;
+ }
+ )");
+ EXPECT_FALSE(column.MaxInlineHunkSize().has_value());
+ }
+
+ {
+ auto column = ColumnFromYson(R"(
+ {
+ name=x;
+ type=string;
+ max_inline_hunk_size=100
+ }
+ )");
+ EXPECT_EQ(column.MaxInlineHunkSize(), 100);
+ }
+}
+
+TEST(TTableSchemaTest, ColumnSchemaValidation)
+{
+ auto expectBad = [] (const auto& schema) {
+ EXPECT_THROW(ValidateColumnSchema(schema, true, true), std::exception);
+ };
+
+ // Empty names are not ok.
+ expectBad(TColumnSchema("", EValueType::String));
+
+ // Names starting from SystemColumnNamePrefix are not ok.
+ expectBad(TColumnSchema(SystemColumnNamePrefix + "Name", EValueType::String));
+
+ // Names longer than MaxColumnNameLength are not ok.
+ expectBad(TColumnSchema(TString(MaxColumnNameLength + 1, 'z'), EValueType::String));
+
+ // Empty lock names are not ok.
+ expectBad(
+ TColumnSchema("Name", EValueType::String)
+ .SetLock(TString("")));
+
+ // Locks on key columns are not ok.
+ expectBad(
+ TColumnSchema("Name", EValueType::String)
+ .SetSortOrder(ESortOrder::Ascending)
+ .SetLock(TString("LockName")));
+
+ // Locks longer than MaxColumnLockLength are not ok.
+ expectBad(
+ TColumnSchema("Name", EValueType::String)
+ .SetLock(TString(MaxColumnLockLength + 1, 'z')));
+
+ // Column type should be valid according to the ValidateSchemaValueType function.
+ // Non-key columns can't be computed.
+ expectBad(
+ TColumnSchema("Name", EValueType::String)
+ .SetExpression(TString("SomeExpression")));
+
+ // Key columns can't be aggregated.
+ expectBad(
+ TColumnSchema("Name", EValueType::String)
+ .SetSortOrder(ESortOrder::Ascending)
+ .SetAggregate(TString("sum")));
+
+ ValidateColumnSchema(TColumnSchema("Name", EValueType::String));
+ ValidateColumnSchema(TColumnSchema("Name", EValueType::Any));
+ ValidateColumnSchema(
+ TColumnSchema(TString(256, 'z'), EValueType::String)
+ .SetLock(TString(256, 'z')));
+ ValidateColumnSchema(
+ TColumnSchema("Name", EValueType::String)
+ .SetSortOrder(ESortOrder::Ascending)
+ .SetExpression(TString("SomeExpression")));
+ ValidateColumnSchema(
+ TColumnSchema("Name", EValueType::String)
+ .SetAggregate(TString("sum")));
+
+ // Struct field validation
+ expectBad(
+ TColumnSchema("Column", StructLogicalType({
+ {"", SimpleLogicalType(ESimpleLogicalValueType::Int8)}
+ })));
+ expectBad(
+ TColumnSchema("Column", StructLogicalType({
+ {TString(257, 'a'), SimpleLogicalType(ESimpleLogicalValueType::Int8)}
+ })));
+
+ expectBad(
+ TColumnSchema("Column", StructLogicalType({
+ {"\255", SimpleLogicalType(ESimpleLogicalValueType::Int8)}
+ })));
+
+ ValidateColumnSchema(
+ TColumnSchema("Column", ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int8)), ESortOrder::Ascending)
+ );
+
+ expectBad(
+ TColumnSchema("Column", ListLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))), ESortOrder::Ascending)
+ );
+
+ expectBad(
+ TColumnSchema("Column", EValueType::String)
+ .SetMaxInlineHunkSize(0)
+ );
+
+ expectBad(
+ TColumnSchema("Column", EValueType::String)
+ .SetMaxInlineHunkSize(-1)
+ );
+
+ expectBad(
+ TColumnSchema("Column", EValueType::Int64)
+ .SetMaxInlineHunkSize(100)
+ );
+
+ expectBad(
+ TColumnSchema("Column", EValueType::String, ESortOrder::Ascending)
+ .SetMaxInlineHunkSize(100)
+ );
+
+ ValidateColumnSchema(
+ TColumnSchema("Column", EValueType::String)
+ .SetMaxInlineHunkSize(100)
+ );
+
+ ValidateColumnSchema(
+ TColumnSchema("Column", EValueType::Any)
+ .SetMaxInlineHunkSize(100)
+ );
+
+ expectBad(
+ TColumnSchema("Column", StructLogicalType({
+ {"foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"bar", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }), ESortOrder::Ascending)
+ );
+}
+
+TEST(TTableSchemaTest, ValidateTableSchemaTest)
+{
+ auto expectBad = [] (const auto& schemaString) {
+ TTableSchema schema;
+ Deserialize(schema, ConvertToNode(TYsonString(TStringBuf(schemaString))));
+
+ EXPECT_THROW(ValidateTableSchema(schema, true), std::exception);
+ };
+ expectBad("[{name=x;type=int64;sort_order=ascending;expression=z}; {name=y;type=uint64;sort_order=ascending}; {name=a;type=int64}]");
+ expectBad("[{name=x;type=int64;sort_order=ascending;expression=y}; {name=y;type=uint64;sort_order=ascending}; {name=a;type=int64}]");
+ expectBad("[{name=x;type=int64;sort_order=ascending;expression=x}; {name=y;type=uint64;sort_order=ascending}; {name=a;type=int64}]");
+ expectBad("[{name=x;type=int64;sort_order=ascending;expression=\"uint64(y)\"}; {name=y;type=uint64;sort_order=ascending}; {name=a;type=int64}]");
+}
+
+TEST(TTableSchemaTest, ColumnSchemaProtobufBackwardCompatibility)
+{
+ NProto::TColumnSchema columnSchemaProto;
+ columnSchemaProto.set_name("foo");
+ columnSchemaProto.set_type(static_cast<int>(EValueType::Uint64));
+
+ TColumnSchema columnSchema;
+ FromProto(&columnSchema, columnSchemaProto);
+
+ EXPECT_EQ(*columnSchema.LogicalType(), *OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64)));
+ EXPECT_EQ(columnSchema.GetWireType(), EValueType::Uint64);
+ EXPECT_EQ(columnSchema.Name(), "foo");
+ EXPECT_EQ(columnSchema.StableName().Get(), "foo");
+
+ columnSchemaProto.set_simple_logical_type(static_cast<int>(ESimpleLogicalValueType::Uint32));
+ columnSchemaProto.set_name("foo");
+ columnSchemaProto.set_stable_name("foo_stable");
+ FromProto(&columnSchema, columnSchemaProto);
+
+ EXPECT_EQ(*columnSchema.LogicalType(), *OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint32)));
+ EXPECT_EQ(columnSchema.GetWireType(), EValueType::Uint64);
+ EXPECT_EQ(columnSchema.Name(), "foo");
+ EXPECT_EQ(columnSchema.StableName().Get(), "foo_stable");
+}
+
+TEST(TTableSchemaTest, EqualIgnoringRequiredness)
+{
+ auto schema1 = TTableSchema({
+ TColumnSchema("foo", SimpleLogicalType(ESimpleLogicalValueType::Int64)),
+ });
+
+ auto schema2 = TTableSchema({
+ TColumnSchema("foo", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ });
+
+ auto schema3 = TTableSchema({
+ TColumnSchema("foo", SimpleLogicalType(ESimpleLogicalValueType::String)),
+ });
+
+ EXPECT_TRUE(schema1 != schema2);
+ EXPECT_TRUE(IsEqualIgnoringRequiredness(schema1, schema2));
+ EXPECT_FALSE(IsEqualIgnoringRequiredness(schema1, schema3));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLockMaskTest, Simple)
+{
+ TLockMask mask;
+ mask.Set(17, ELockType::SharedStrong);
+ EXPECT_EQ(mask.Get(17), ELockType::SharedStrong);
+}
+
+TEST(TLockMaskTest, RandomChanges)
+{
+ constexpr int IterationCount = 1'000'000;
+ constexpr int MaxLockIndex = 256;
+ constexpr int MaxLockValue = static_cast<int>(TEnumTraits<ELockType>::GetMaxValue());
+
+ std::mt19937 rng(42);
+
+ TLockMask mask;
+ std::vector<ELockType> locks(MaxLockIndex, ELockType::None);
+ for (int iteration = 0; iteration < IterationCount; ++iteration) {
+ if (rng() % 2 == 0) {
+ int index = rng() % MaxLockIndex;
+ EXPECT_EQ(locks[index], mask.Get(index));
+ } else {
+ int index = rng() % MaxLockIndex;
+ auto value = CheckedEnumCast<ELockType>(rng() % MaxLockValue);
+ mask.Set(index, value);
+ locks[index] = value;
+ }
+ }
+}
+
+TEST(TLockMaskTest, ConvertToLegacy)
+{
+ TLockMask mask;
+ mask.Set(2, ELockType::Exclusive);
+ mask.Set(18, ELockType::SharedStrong);
+
+ EXPECT_FALSE(mask.HasNewLocks());
+ auto legacyLocks = mask.ToLegacyMask();
+ for (int index = 0; index < TLegacyLockMask::MaxCount; ++index) {
+ auto lock = legacyLocks.Get(index);
+ if (index == 2) {
+ EXPECT_EQ(lock, ELockType::Exclusive);
+ } else if (index == 18) {
+ EXPECT_EQ(lock, ELockType::SharedStrong);
+ } else {
+ EXPECT_EQ(lock, ELockType::None);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/schemaful_dsv_parser_ut.cpp b/yt/yt/client/unittests/schemaful_dsv_parser_ut.cpp
new file mode 100644
index 0000000000..000ae5f635
--- /dev/null
+++ b/yt/yt/client/unittests/schemaful_dsv_parser_ut.cpp
@@ -0,0 +1,259 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/client/formats/schemaful_dsv_parser.h>
+
+#include <yt/yt/core/yson/null_consumer.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NYson;
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSchemafulDsvParserTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("5"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar("6"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("100"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar("max\tignat"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "5\t6\n"
+ "100\tmax\\tignat\n";
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->Columns->push_back("b");
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSchemafulDsvParserTest, TableIndex)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("table_index"));
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("x"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("table_index"));
+ EXPECT_CALL(Mock, OnInt64Scalar(0));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("y"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("z"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "1\tx\n"
+ "0\ty\n"
+ "0\tz\n";
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->EnableTableIndex = true;
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+TEST(TSchemafulDsvParserTest, TooManyRows)
+{
+ TString input = "5\t6\n";
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = {"a"};
+
+ EXPECT_THROW({ ParseSchemafulDsv(input, GetNullYsonConsumer(), config); }, std::exception);
+}
+
+TEST(TSchemafulDsvParserTest, SpecialSymbols)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ auto value = TString("6\0", 2);
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("5\r"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar(value));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input("5\r\t6\0\n", 6);
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->Columns->push_back("b");
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+TEST(TSchemafulDsvParserTest, EnabledEscaping)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ auto value = TString("6\0", 2);
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("5\r\r"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar(value));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input("5\r\\r\t6\0\n", 8);
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->Columns->push_back("b");
+ config->EnableEscaping = true;
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+TEST(TSchemafulDsvParserTest, DisabledEscaping)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ auto value = TString("6\0", 2);
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("5\r\\r"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar(value));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input("5\r\\r\t6\0\n", 8);
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->Columns->push_back("b");
+ config->EnableEscaping = false;
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+TEST(TSchemafulDsvParserTest, ColumnsNamesHeader)
+{
+ TString input("a\tb\n1\t2\n");
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = std::vector<TString>();
+ config->Columns->push_back("a");
+ config->Columns->push_back("b");
+ config->EnableColumnNamesHeader = true;
+
+ EXPECT_THROW(ParseSchemafulDsv(input, GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TSchemafulDsvParserTest, MissingValueModePrintSentinel)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ TString input = "x\t\tz\n";
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("x"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnStringScalar("z"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto config = New<TSchemafulDsvFormatConfig>();
+ config->Columns = {"a", "b", "c"};
+ // By default missing_value_mode = fail and no sentinel values are used,
+ // i. e. there is no way to represent YSON entity with this format.
+
+ ParseSchemafulDsv(input, &Mock, config);
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("x"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnEntity());
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnStringScalar("z"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ config->MissingValueMode = EMissingSchemafulDsvValueMode::PrintSentinel;
+ // By default missing_value_sentinel = "".
+
+ ParseSchemafulDsv(input, &Mock, config);
+
+ input = "null\tNULL\t\n";
+
+ config->MissingValueSentinel = "NULL";
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("null"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnEntity());
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ ParseSchemafulDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/schemaful_dsv_writer_ut.cpp b/yt/yt/client/unittests/schemaful_dsv_writer_ut.cpp
new file mode 100644
index 0000000000..90a3af0dcb
--- /dev/null
+++ b/yt/yt/client/unittests/schemaful_dsv_writer_ut.cpp
@@ -0,0 +1,344 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include "format_writer_ut.h"
+
+#include <yt/yt/client/formats/schemaful_dsv_writer.h>
+#include <yt/yt/client/formats/format.h>
+
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <limits>
+
+namespace NYT::NFormats {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NTableClient;
+
+class TSchemalessWriterForSchemafulDsvTest
+ : public ::testing::Test
+{
+protected:
+ TNameTablePtr NameTable_;
+ int KeyAId_;
+ int KeyBId_;
+ int KeyCId_;
+ int KeyDId_;
+ int TableIndexId_;
+ int RangeIndexId_;
+ int RowIndexId_;
+ TSchemafulDsvFormatConfigPtr Config_;
+
+ ISchemalessFormatWriterPtr Writer_;
+
+ TStringStream OutputStream_;
+
+ TSchemalessWriterForSchemafulDsvTest() {
+ NameTable_ = New<TNameTable>();
+ KeyAId_ = NameTable_->RegisterName("column_a");
+ KeyBId_ = NameTable_->RegisterName("column_b");
+ KeyCId_ = NameTable_->RegisterName("column_c");
+ KeyDId_ = NameTable_->RegisterName("column_d");
+ TableIndexId_ = NameTable_->RegisterName(TableIndexColumnName);
+ RowIndexId_ = NameTable_->RegisterName(RowIndexColumnName);
+ RangeIndexId_ = NameTable_->RegisterName(RangeIndexColumnName);
+
+ Config_ = New<TSchemafulDsvFormatConfig>();
+ }
+
+ void CreateStandardWriter() {
+ auto controlAttributesConfig = New<TControlAttributesConfig>();
+ controlAttributesConfig->EnableTableIndex = Config_->EnableTableIndex;
+ Writer_ = CreateSchemalessWriterForSchemafulDsv(
+ Config_,
+ NameTable_,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&OutputStream_)),
+ false, // enableContextSaving
+ controlAttributesConfig,
+ 0 /* keyColumnCount */);
+ }
+};
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, Simple)
+{
+ Config_->Columns = {"column_b", "column_c", "column_a"};
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("value_a", KeyAId_));
+ row1.AddValue(MakeUnversionedInt64Value(-42, KeyBId_));
+ row1.AddValue(MakeUnversionedBooleanValue(true, KeyCId_));
+ row1.AddValue(MakeUnversionedStringValue("garbage", KeyDId_));
+
+ // Ignore system columns.
+ row1.AddValue(MakeUnversionedInt64Value(2, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, RowIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(1, RangeIndexId_));
+
+ TUnversionedRowBuilder row2;
+ // The order is reversed.
+ row2.AddValue(MakeUnversionedStringValue("value_c", KeyCId_));
+ row2.AddValue(MakeUnversionedBooleanValue(false, KeyBId_));
+ row2.AddValue(MakeUnversionedInt64Value(23, KeyAId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow(), row2.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "-42\ttrue\tvalue_a\n"
+ "false\tvalue_c\t23\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+// This test shows the actual behavior of writer. It is OK to change it in the future. :)
+TEST_F(TSchemalessWriterForSchemafulDsvTest, TrickyDoubleRepresentations)
+{
+ Config_->Columns = {"column_a", "column_b", "column_c", "column_d"};
+ CreateStandardWriter();
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedDoubleValue(1.234567890123456, KeyAId_));
+ row1.AddValue(MakeUnversionedDoubleValue(42, KeyBId_));
+ row1.AddValue(MakeUnversionedDoubleValue(1e300, KeyCId_));
+ row1.AddValue(MakeUnversionedDoubleValue(-1e-300, KeyDId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput = "1.234567890123456\t42.\t1e+300\t-1e-300\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, IntegralTypeRepresentations)
+{
+ Config_->Columns = {"column_a", "column_b", "column_c", "column_d"};
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedInt64Value(0LL, KeyAId_));
+ row1.AddValue(MakeUnversionedInt64Value(-1LL, KeyBId_));
+ row1.AddValue(MakeUnversionedInt64Value(1LL, KeyCId_));
+ row1.AddValue(MakeUnversionedInt64Value(99LL, KeyDId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedInt64Value(123LL, KeyAId_));
+ row2.AddValue(MakeUnversionedInt64Value(-123LL, KeyBId_));
+ row2.AddValue(MakeUnversionedInt64Value(1234LL, KeyCId_));
+ row2.AddValue(MakeUnversionedInt64Value(-1234LL, KeyDId_));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedUint64Value(0ULL, KeyAId_));
+ row3.AddValue(MakeUnversionedUint64Value(98ULL, KeyBId_));
+ row3.AddValue(MakeUnversionedUint64Value(987ULL, KeyCId_));
+ row3.AddValue(MakeUnversionedUint64Value(9876ULL, KeyDId_));
+
+ TUnversionedRowBuilder row4;
+ row4.AddValue(MakeUnversionedInt64Value(std::numeric_limits<i64>::max(), KeyAId_));
+ row4.AddValue(MakeUnversionedInt64Value(std::numeric_limits<i64>::min(), KeyBId_));
+ row4.AddValue(MakeUnversionedInt64Value(std::numeric_limits<i64>::min() + 1LL, KeyCId_));
+ row4.AddValue(MakeUnversionedUint64Value(std::numeric_limits<ui64>::max(), KeyDId_));
+
+ std::vector<TUnversionedRow> rows =
+ {row1.GetRow(), row2.GetRow(), row3.GetRow(), row4.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput =
+ "0\t-1\t1\t99\n"
+ "123\t-123\t1234\t-1234\n"
+ "0\t98\t987\t9876\n"
+ "9223372036854775807\t-9223372036854775808\t-9223372036854775807\t18446744073709551615\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, EmptyColumnList)
+{
+ Config_->Columns = std::vector<TString>();
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedInt64Value(0LL, KeyAId_));
+
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput = "\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, MissingValueMode)
+{
+ Config_->Columns = {"column_a", "column_b", "column_c"};
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("Value1A", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("Value1B", KeyBId_));
+ row1.AddValue(MakeUnversionedStringValue("Value1C", KeyCId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("Value2A", KeyAId_));
+ row2.AddValue(MakeUnversionedStringValue("Value2C", KeyCId_));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("Value3A", KeyAId_));
+ row3.AddValue(MakeUnversionedStringValue("Value3B", KeyBId_));
+ row3.AddValue(MakeUnversionedStringValue("Value3C", KeyCId_));
+
+ std::vector<TUnversionedRow> rows =
+ {row1.GetRow(), row2.GetRow(), row3.GetRow()};
+
+ {
+ Config_->MissingValueMode = EMissingSchemafulDsvValueMode::SkipRow;
+ CreateStandardWriter();
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput =
+ "Value1A\tValue1B\tValue1C\n"
+ "Value3A\tValue3B\tValue3C\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+ OutputStream_.Clear();
+ }
+
+ {
+ Config_->MissingValueMode = EMissingSchemafulDsvValueMode::Fail;
+ CreateStandardWriter();
+ EXPECT_EQ(false, Writer_->Write(rows));
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+ OutputStream_.Clear();
+ }
+
+ {
+ Config_->MissingValueMode = EMissingSchemafulDsvValueMode::PrintSentinel;
+ Config_->MissingValueSentinel = "~";
+ CreateStandardWriter();
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput =
+ "Value1A\tValue1B\tValue1C\n"
+ "Value2A\t~\tValue2C\n"
+ "Value3A\tValue3B\tValue3C\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+ OutputStream_.Clear();
+ }
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, NameTableExpansion)
+{
+ Config_->Columns = {"Column1"};
+ Config_->MissingValueMode = {EMissingSchemafulDsvValueMode::PrintSentinel};
+ CreateStandardWriter();
+ TestNameTableExpansion(Writer_, NameTable_);
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, TableIndex)
+{
+ Config_->Columns = {"column_a", "column_b", "column_c", "column_d"};
+ Config_->EnableTableIndex = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row0;
+ row0.AddValue(MakeUnversionedInt64Value(0LL, KeyAId_));
+ row0.AddValue(MakeUnversionedInt64Value(1LL, KeyBId_));
+ row0.AddValue(MakeUnversionedInt64Value(2LL, KeyCId_));
+ row0.AddValue(MakeUnversionedInt64Value(3LL, KeyDId_));
+
+ // It's necessary to specify a column corresponding to the table index
+ // when enable_table_index = true.
+ EXPECT_EQ(false, Writer_->Write(std::vector<TUnversionedRow>{row0.GetRow()}));
+
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedInt64Value(42LL, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(0LL, KeyAId_));
+ row1.AddValue(MakeUnversionedInt64Value(1LL, KeyBId_));
+ row1.AddValue(MakeUnversionedInt64Value(2LL, KeyCId_));
+ row1.AddValue(MakeUnversionedInt64Value(3LL, KeyDId_));
+
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedInt64Value(42LL, TableIndexId_));
+ row2.AddValue(MakeUnversionedInt64Value(4LL, KeyAId_));
+ row2.AddValue(MakeUnversionedInt64Value(5LL, KeyBId_));
+ row2.AddValue(MakeUnversionedInt64Value(6LL, KeyCId_));
+ row2.AddValue(MakeUnversionedInt64Value(7LL, KeyDId_));
+
+ EXPECT_EQ(true, Writer_->Write(std::vector<TUnversionedRow>{row1.GetRow(), row2.GetRow()}));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedInt64Value(23LL, TableIndexId_));
+ row3.AddValue(MakeUnversionedUint64Value(8LL, KeyAId_));
+ row3.AddValue(MakeUnversionedUint64Value(9LL, KeyBId_));
+ row3.AddValue(MakeUnversionedUint64Value(10LL, KeyCId_));
+ row3.AddValue(MakeUnversionedUint64Value(11ULL, KeyDId_));
+
+ EXPECT_EQ(true, Writer_->Write(std::vector<TUnversionedRow>{row3.GetRow()}));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+ TString expectedOutput =
+ "42\t0\t1\t2\t3\n"
+ "42\t4\t5\t6\t7\n"
+ "23\t8\t9\t10\t11\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, ValidateDuplicateNames)
+{
+ Config_->Columns = {"column_a", "column_b", "column_a"};
+ Config_->EnableTableIndex = true;
+ EXPECT_THROW(CreateStandardWriter(), TErrorException);
+}
+
+TEST_F(TSchemalessWriterForSchemafulDsvTest, ColumnsHeader)
+{
+ Config_->Columns = {"column_b", "column_c", "column_a"};
+ Config_->EnableColumnNamesHeader = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("value_a", KeyAId_));
+ row1.AddValue(MakeUnversionedInt64Value(-42, KeyBId_));
+ row1.AddValue(MakeUnversionedBooleanValue(true, KeyCId_));
+ std::vector<TUnversionedRow> rows = {row1.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "column_b\tcolumn_c\tcolumn_a\n"
+ "-42\ttrue\tvalue_a\n";
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/skiff_format_ut.cpp b/yt/yt/client/unittests/skiff_format_ut.cpp
new file mode 100644
index 0000000000..33496dc77b
--- /dev/null
+++ b/yt/yt/client/unittests/skiff_format_ut.cpp
@@ -0,0 +1,3006 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include "logical_type_shortcuts.h"
+#include "value_examples.h"
+#include "row_helpers.h"
+#include "yson_helpers.h"
+
+#include <yt/yt/client/formats/config.h>
+#include <yt/yt/client/formats/parser.h>
+#include <yt/yt/client/formats/skiff_parser.h>
+#include <yt/yt/client/formats/skiff_writer.h>
+#include <yt/yt/client/formats/format.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/validate_logical_type.h>
+
+#include <yt/yt/library/named_value/named_value.h>
+#include <yt/yt/library/skiff_ext/schema_match.h>
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <util/stream/null.h>
+#include <util/string/hex.h>
+
+namespace NYT {
+
+namespace {
+
+using namespace NFormats;
+using namespace NNamedValue;
+using namespace NSkiff;
+using namespace NSkiffExt;
+using namespace NTableClient;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ConvertToSkiffSchemaShortDebugString(INodePtr node)
+{
+ auto skiffFormatConfig = ConvertTo<TSkiffFormatConfigPtr>(std::move(node));
+ auto skiffSchemas = ParseSkiffSchemas(skiffFormatConfig->SkiffSchemaRegistry, skiffFormatConfig->TableSkiffSchemas);
+ TStringStream result;
+ result << '{';
+ for (const auto& schema : skiffSchemas) {
+ result << GetShortDebugString(schema);
+ result << ',';
+ }
+ result << '}';
+ return result.Str();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ConvertToYsonTextStringStable(const INodePtr& node)
+{
+ TStringStream out;
+ TYsonWriter writer(&out, EYsonFormat::Text);
+ VisitTree(node, &writer, true, TAttributeFilter());
+ writer.Flush();
+ return out.Str();
+}
+
+TTableSchemaPtr CreateSingleValueTableSchema(const TLogicalTypePtr& logicalType)
+{
+ std::vector<TColumnSchema> columns;
+ if (logicalType) {
+ columns.emplace_back("value", logicalType);
+
+ }
+ auto strict = static_cast<bool>(logicalType);
+ return New<TTableSchema>(columns, strict);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSkiffSchemaParse, TestAllowedTypes)
+{
+ EXPECT_EQ(
+ "{uint64,}",
+
+ ConvertToSkiffSchemaShortDebugString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("table_skiff_schemas")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("uint64")
+ .EndMap()
+ .EndList()
+ .EndMap()));
+
+ EXPECT_EQ(
+ "{string32,}",
+
+ ConvertToSkiffSchemaShortDebugString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("table_skiff_schemas")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("string32")
+ .EndMap()
+ .EndList()
+ .EndMap()));
+
+ EXPECT_EQ(
+ "{variant8<string32;int64;>,}",
+
+ ConvertToSkiffSchemaShortDebugString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("table_skiff_schemas")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("variant8")
+ .Item("children")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("string32")
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("int64")
+ .EndMap()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()));
+
+ EXPECT_EQ(
+ "{variant8<int64;string32;>,}",
+
+ ConvertToSkiffSchemaShortDebugString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("skiff_schema_registry")
+ .BeginMap()
+ .Item("item1")
+ .BeginMap()
+ .Item("wire_type")
+ .Value("int64")
+ .EndMap()
+ .Item("item2")
+ .BeginMap()
+ .Item("wire_type")
+ .Value("string32")
+ .EndMap()
+ .EndMap()
+ .Item("table_skiff_schemas")
+ .BeginList()
+ .Item()
+ .BeginMap()
+ .Item("wire_type")
+ .Value("variant8")
+ .Item("children")
+ .BeginList()
+ .Item().Value("$item1")
+ .Item().Value("$item2")
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap()));
+}
+
+TEST(TSkiffSchemaParse, TestRecursiveTypesAreDisallowed)
+{
+ try {
+ ConvertToSkiffSchemaShortDebugString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("skiff_schema_registry")
+ .BeginMap()
+ .Item("item1")
+ .BeginMap()
+ .Item("wire_type")
+ .Value("variant8")
+ .Item("children")
+ .BeginList()
+ .Item().Value("$item1")
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .Item("table_skiff_schemas")
+ .BeginList()
+ .Item().Value("$item1")
+ .EndList()
+ .EndMap());
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("recursive types are forbidden"));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSkiffSchemaDescription, TestDescriptionDerivation)
+{
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ })->SetName("Bar"),
+ });
+
+ auto tableDescriptionList = CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ EXPECT_EQ(std::ssize(tableDescriptionList), 1);
+ EXPECT_EQ(tableDescriptionList[0].HasOtherColumns, false);
+ EXPECT_EQ(tableDescriptionList[0].SparseFieldDescriptionList.empty(), true);
+
+ auto denseFieldDescriptionList = tableDescriptionList[0].DenseFieldDescriptionList;
+ EXPECT_EQ(std::ssize(denseFieldDescriptionList), 2);
+
+ EXPECT_EQ(denseFieldDescriptionList[0].Name(), "Foo");
+ EXPECT_EQ(denseFieldDescriptionList[0].ValidatedSimplify(), EWireType::Uint64);
+}
+
+TEST(TSkiffSchemaDescription, TestKeySwitchColumn)
+{
+ {
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$key_switch"),
+ });
+
+ auto tableDescriptionList = CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ EXPECT_EQ(std::ssize(tableDescriptionList), 1);
+ EXPECT_EQ(tableDescriptionList[0].KeySwitchFieldIndex, std::optional<size_t>(1));
+ }
+ {
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("$key_switch"),
+ });
+
+ try {
+ auto tableDescriptionList = CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Column \"$key_switch\" has unexpected Skiff type"));
+ }
+ }
+}
+
+TEST(TSkiffSchemaDescription, TestDisallowEmptyNames)
+{
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName(""),
+ });
+
+ try {
+ CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("must have a name"));
+ }
+}
+
+TEST(TSkiffSchemaDescription, TestWrongRowType)
+{
+ auto schema = CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Bar"),
+ });
+
+ try {
+ CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Invalid wire type for table row"));
+ }
+}
+
+TEST(TSkiffSchemaDescription, TestOtherColumnsOk)
+{
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Bar"),
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("$other_columns"),
+ });
+
+ auto tableDescriptionList = CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ASSERT_EQ(std::ssize(tableDescriptionList), 1);
+ ASSERT_EQ(tableDescriptionList[0].HasOtherColumns, true);
+}
+
+TEST(TSkiffSchemaDescription, TestOtherColumnsWrongType)
+{
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Bar"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("$other_columns"),
+ });
+
+ try {
+ CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Invalid wire type for column \"$other_columns\""));
+ }
+}
+
+TEST(TSkiffSchemaDescription, TestOtherColumnsWrongPlace)
+{
+ auto schema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Foo"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("$other_columns"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("Bar"),
+ });
+
+ try {
+ CreateTableDescriptionList({schema}, RangeIndexColumnName, RowIndexColumnName);
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Invalid placement of special column \"$other_columns\""));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISchemalessFormatWriterPtr CreateSkiffWriter(
+ std::shared_ptr<TSkiffSchema> skiffSchema,
+ TNameTablePtr nameTable,
+ IOutputStream* outputStream,
+ const std::vector<TTableSchemaPtr>& tableSchemaList,
+ int keyColumnCount = 0,
+ bool enableEndOfStream = false)
+{
+ auto controlAttributesConfig = New<TControlAttributesConfig>();
+ controlAttributesConfig->EnableKeySwitch = (keyColumnCount > 0);
+ controlAttributesConfig->EnableEndOfStream = enableEndOfStream;
+ return CreateWriterForSkiff(
+ {std::move(skiffSchema)},
+ std::move(nameTable),
+ tableSchemaList,
+ NConcurrency::CreateAsyncAdapter(outputStream),
+ false,
+ controlAttributesConfig,
+ keyColumnCount);
+}
+
+TString TableToSkiff(
+ const TLogicalTypePtr& logicalType,
+ const std::shared_ptr<TSkiffSchema>& typeSchema,
+ const TNamedValue::TValue& value)
+{
+ auto schema = CreateSingleValueTableSchema(logicalType);
+ auto skiffSchema = CreateTupleSchema({
+ typeSchema->SetName("value")
+ });
+
+ auto nameTable = New<TNameTable>();
+
+ TStringStream resultStream;
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {schema});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", value}
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ auto result = resultStream.Str();
+ if (!TStringBuf(result).StartsWith(TString(2, '\0'))) {
+ THROW_ERROR_EXCEPTION("Expected skiff value to start with \\x00\\x00, but prefix is %Qv",
+ EscapeC(result.substr(0, 2)));
+ }
+
+ return result.substr(2);
+}
+
+TNamedValue::TValue SkiffToTable(
+ const TLogicalTypePtr& logicalType,
+ const std::shared_ptr<TSkiffSchema>& typeSchema,
+ const TString& skiffValue)
+{
+ auto schema = CreateSingleValueTableSchema(logicalType);
+ auto skiffSchema = CreateTupleSchema({
+ typeSchema->SetName("value")
+ });
+ auto nameTable = New<TNameTable>();
+
+ TCollectingValueConsumer rowCollector(schema);
+ auto parser = CreateParserForSkiff(skiffSchema, &rowCollector);
+ parser->Read(TString(2, 0));
+ parser->Read(skiffValue);
+ parser->Finish();
+
+ if (rowCollector.Size() != 1) {
+ THROW_ERROR_EXCEPTION("Expected 1 row collected actual: %Qv",
+ rowCollector.Size());
+ }
+ auto value = rowCollector.GetRowValue(0, "value");
+ return TNamedValue::ExtractValue(value);
+}
+
+#define CHECK_BIDIRECTIONAL_CONVERSION(logicalTypeArg, skiffSchemaArg, tableValueArg, hexSkiffArg) \
+ do { \
+ try { \
+ TLogicalTypePtr logicalType = (logicalTypeArg); \
+ std::shared_ptr<TSkiffSchema> skiffSchema = (skiffSchemaArg); \
+ TNamedValue::TValue tableValue = (tableValueArg); \
+ TString hexSkiff = (hexSkiffArg); \
+ auto nameTable = New<TNameTable>(); \
+ auto actualSkiff = TableToSkiff(logicalType, skiffSchema, tableValue); \
+ EXPECT_EQ(HexEncode(actualSkiff), hexSkiff); \
+ auto actualValue = SkiffToTable(logicalType, skiffSchema, HexDecode(hexSkiff)); \
+ EXPECT_EQ(actualValue, tableValue); \
+ } catch (const std::exception& ex) { \
+ ADD_FAILURE() << "unexpected exception: " << ex.what(); \
+ } \
+ } while (0)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestAllWireTypes(bool useSchema)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("int64"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("uint64"),
+ CreateSimpleTypeSchema(EWireType::Double)->SetName("double_1"),
+ CreateSimpleTypeSchema(EWireType::Double)->SetName("double_2"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("boolean"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("string32"),
+ CreateSimpleTypeSchema(EWireType::Nothing)->SetName("null"),
+
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName("opt_int64"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ })->SetName("opt_uint64"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Double),
+ })->SetName("opt_double_1"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Double),
+ })->SetName("opt_double_2"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ })->SetName("opt_boolean"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::String32),
+ })->SetName("opt_string32"),
+ });
+ std::vector<TTableSchemaPtr> tableSchemas;
+ if (useSchema) {
+ tableSchemas.push_back(New<TTableSchema>(std::vector{
+ TColumnSchema("int64", EValueType::Int64),
+ TColumnSchema("uint64", EValueType::Uint64),
+ TColumnSchema("double_1", EValueType::Double),
+ TColumnSchema("double_2", ESimpleLogicalValueType::Float),
+ TColumnSchema("boolean", EValueType::Boolean),
+ TColumnSchema("string32", EValueType::String),
+ TColumnSchema("null", EValueType::Null),
+ TColumnSchema("opt_int64", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))),
+ TColumnSchema("opt_uint64", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64))),
+ TColumnSchema("opt_double_1", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Double))),
+ TColumnSchema("opt_double_2", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Float))),
+ TColumnSchema("opt_boolean", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Boolean))),
+ TColumnSchema("opt_string32", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))),
+ }));
+ } else {
+ tableSchemas.push_back(New<TTableSchema>());
+ }
+ auto nameTable = New<TNameTable>();
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, tableSchemas);
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"int64", -1},
+ {"uint64", 2u},
+ {"double_1", 3.0},
+ {"double_2", 3.0},
+ {"boolean", true},
+ {"string32", "four"},
+ {"null", nullptr},
+
+ {"opt_int64", -5},
+ {"opt_uint64", 6u},
+ {"opt_double_1", 7.0},
+ {"opt_double_2", 7.0},
+ {"opt_boolean", false},
+ {"opt_string32", "eight"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Write({
+ MakeRow(nameTable, {
+ {"int64", -9},
+ {"uint64", 10u},
+ {"double_1", 11.0},
+ {"double_2", 11.0},
+ {"boolean", false},
+ {"string32", "twelve"},
+ {"null", nullptr},
+
+ {"opt_int64", nullptr},
+ {"opt_uint64", nullptr},
+ {"opt_double_1", nullptr},
+ {"opt_double_2", nullptr},
+ {"opt_boolean", nullptr},
+ {"opt_string32", nullptr},
+ {TableIndexColumnName, 0},
+ }).Get()
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ }
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -1);
+ ASSERT_EQ(checkedSkiffParser.ParseUint64(), 2u);
+ // double_1
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 3.0);
+ // double_2
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 3.0);
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), true);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "four");
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -5);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseUint64(), 6u);
+
+ // double_1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 7.0);
+
+ // double_2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 7.0);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "eight");
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -9);
+ ASSERT_EQ(checkedSkiffParser.ParseUint64(), 10u);
+ // double_1
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 11.0);
+ // double_2
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 11.0);
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "twelve");
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ // double_1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ // double_2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestAllWireTypesNoSchema)
+{
+ TestAllWireTypes(false);
+}
+
+TEST(TSkiffWriter, TestAllWireTypesWithSchema)
+{
+ TestAllWireTypes(true);
+}
+
+class TSkiffYsonWireTypeP
+ : public ::testing::TestWithParam<std::tuple<
+ TLogicalTypePtr,
+ TNamedValue::TValue,
+ TString
+ >>
+{
+public:
+ static std::vector<ParamType> GetCases()
+ {
+ using namespace NLogicalTypeShortcuts;
+ std::vector<ParamType> result;
+
+ for (const auto& example : GetPrimitiveValueExamples()) {
+ result.emplace_back(example.LogicalType, example.Value, example.PrettyYson);
+ result.emplace_back(nullptr, example.Value, example.PrettyYson);
+ }
+
+ for (const auto type : TEnumTraits<ESimpleLogicalValueType>::GetDomainValues()) {
+ auto logicalType = OptionalLogicalType(SimpleLogicalType(type));
+ if (IsV3Composite(logicalType)) {
+ // Optional<Null> is not v1 type
+ continue;
+ }
+ result.emplace_back(logicalType, nullptr, "#");
+ }
+ return result;
+ }
+
+ static const std::vector<ParamType> Cases;
+};
+
+const std::vector<TSkiffYsonWireTypeP::ParamType> TSkiffYsonWireTypeP::Cases = TSkiffYsonWireTypeP::GetCases();
+
+INSTANTIATE_TEST_SUITE_P(
+ Cases,
+ TSkiffYsonWireTypeP,
+ ::testing::ValuesIn(TSkiffYsonWireTypeP::Cases));
+
+TEST_P(TSkiffYsonWireTypeP, Test)
+{
+ const auto& [logicalType, value, expectedYson] = GetParam();
+ TTableSchemaPtr tableSchema;
+ if (logicalType) {
+ tableSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema("column", logicalType),
+ });
+ } else {
+ tableSchema = New<TTableSchema>();
+ }
+ auto skiffTableSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("column"),
+ });
+ auto nameTable = New<TNameTable>();
+ TStringStream actualSkiffDataStream;
+ auto writer = CreateSkiffWriter(skiffTableSchema, nameTable, &actualSkiffDataStream, {tableSchema});
+ writer->Write({
+ MakeRow(nameTable, {{"column", value}})
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ auto actualSkiffData = actualSkiffDataStream.Str();
+ {
+ TMemoryInput in(actualSkiffData);
+ TCheckedSkiffParser parser(CreateVariant16Schema({skiffTableSchema}), &in);
+ EXPECT_EQ(parser.ParseVariant16Tag(), 0);
+ auto actualYson = parser.ParseYson32();
+ parser.ValidateFinished();
+
+ EXPECT_EQ(CanonizeYson(actualYson), CanonizeYson(expectedYson));
+ }
+
+ TCollectingValueConsumer rowCollector(nameTable);
+ auto parser = CreateParserForSkiff(skiffTableSchema, tableSchema, &rowCollector);
+ parser->Read(actualSkiffDataStream.Str());
+ parser->Finish();
+ auto actualValue = rowCollector.GetRowValue(0, "column");
+ EXPECT_EQ(actualValue, TNamedValue("column", value).ToUnversionedValue(nameTable));
+}
+
+TEST(TSkiffWriter, TestYsonWireType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson32"),
+
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ })->SetName("opt_yson32"),
+ });
+ auto nameTable = New<TNameTable>();
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ // Row 0 (Null)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", nullptr},
+ {"opt_yson32", nullptr},
+ }).Get(),
+ });
+
+ // Row 1 (Int64)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", -5},
+ {"opt_yson32", -6},
+ }).Get(),
+ });
+
+ // Row 2 (Uint64)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", 42u},
+ {"opt_yson32", 43u},
+ }).Get(),
+ });
+
+ // Row 3 ((Double)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", 2.7182818},
+ {"opt_yson32", 3.1415926},
+ }).Get(),
+ });
+
+ // Row 4 ((Boolean)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", true},
+ {"opt_yson32", false},
+ }).Get(),
+ });
+
+ // Row 5 ((String)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", "Yin"},
+ {"opt_yson32", "Yang"},
+ }).Get(),
+ });
+
+ // Row 6 ((Any)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+
+ {"yson32", EValueType::Any, "{foo=bar;}"},
+ {"opt_yson32", EValueType::Any, "{bar=baz;}"},
+ }).Get(),
+ });
+
+ // Row 7 ((missing optional values)
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ }
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ auto parseYson = [] (TCheckedSkiffParser* parser) {
+ auto yson = TString{parser->ParseYson32()};
+ return ConvertToNode(TYsonString(yson));
+ };
+
+ // Row 0 (Null)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->GetType(), ENodeType::Entity);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ // Row 1 (Int64)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsInt64()->GetValue(), -5);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsInt64()->GetValue(), -6);
+
+ // Row 2 (Uint64)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsUint64()->GetValue(), 42u);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsUint64()->GetValue(), 43u);
+
+ // Row 3 (Double)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsDouble()->GetValue(), 2.7182818);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsDouble()->GetValue(), 3.1415926);
+
+ // Row 4 (Boolean)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsBoolean()->GetValue(), true);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsBoolean()->GetValue(), false);
+
+ // Row 5 (String)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsString()->GetValue(), "Yin");
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsString()->GetValue(), "Yang");
+
+ // Row 6 (Any)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsMap()->GetChildOrThrow("foo")->AsString()->GetValue(), "bar");
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->AsMap()->GetChildOrThrow("bar")->AsString()->GetValue(), "baz");
+
+ // Row 7 (Null)
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser)->GetType(), ENodeType::Entity);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+class TSkiffFormatSmallIntP
+: public ::testing::TestWithParam<std::tuple<
+ std::shared_ptr<TSkiffSchema>,
+ TLogicalTypePtr,
+ TNamedValue::TValue,
+ TString
+>>
+{
+public:
+ static std::vector<ParamType> GetCases()
+ {
+ using namespace NLogicalTypeShortcuts;
+
+ std::vector<ParamType> result;
+
+ auto addSimpleCase = [&result] (
+ EWireType wireType,
+ const TLogicalTypePtr& logicalType,
+ auto value,
+ TStringBuf skiffValue)
+ {
+ auto simpleSkiffSchema = CreateSimpleTypeSchema(wireType);
+ auto simpleSkiffData = TString(2, 0) + skiffValue;
+ result.emplace_back(simpleSkiffSchema, logicalType, value, simpleSkiffData);
+ };
+
+ auto addListCase = [&result] (
+ EWireType wireType,
+ const TLogicalTypePtr& logicalType,
+ auto value,
+ TStringBuf skiffValue)
+ {
+ auto listSkiffSchema = CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(wireType)});
+ auto listSkiffData = TString(3, 0) + skiffValue + TString(1, '\xff');
+ auto listValue = TNamedValue::TValue{
+ TNamedValue::TComposite{
+ BuildYsonStringFluently()
+ .BeginList()
+ .Item().Value(value)
+ .EndList().ToString()
+ }
+ };
+ result.emplace_back(listSkiffSchema, List(logicalType), listValue, listSkiffData);
+ };
+
+ auto addSimpleAndListCases = [&] (
+ EWireType wireType,
+ const TLogicalTypePtr& logicalType,
+ auto value,
+ TStringBuf skiffValue)
+ {
+ addSimpleCase(wireType, logicalType, value, skiffValue);
+ addListCase(wireType, logicalType, value, skiffValue);
+ };
+
+ auto addMultiCase = [&] (EWireType wireType, auto value, TStringBuf skiffValue) {
+ auto add = [&] (const TLogicalTypePtr& logicalType) {
+ addSimpleAndListCases(wireType, logicalType, value, skiffValue);
+ };
+ addSimpleCase(wireType, Yson(), value, skiffValue);
+
+ using T = std::decay_t<decltype(value)>;
+ static_assert(std::is_integral_v<T>);
+ if constexpr (std::is_signed_v<T>) {
+ if (std::numeric_limits<i8>::min() <= value && value <= std::numeric_limits<i8>::max()) {
+ add(Int8());
+ }
+ if (std::numeric_limits<i16>::min() <= value && value <= std::numeric_limits<i16>::max()) {
+ add(Int16());
+ }
+ if (std::numeric_limits<i32>::min() <= value && value <= std::numeric_limits<i32>::max()) {
+ add(Int32());
+ }
+ add(Int64());
+ } else {
+ if (value <= std::numeric_limits<ui8>::max()) {
+ add(Uint8());
+ }
+ if (value <= std::numeric_limits<ui16>::max()) {
+ add(Uint16());
+ }
+ if (value <= std::numeric_limits<ui32>::max()) {
+ add(Uint32());
+ }
+ add(Uint64());
+ }
+ };
+ addMultiCase(EWireType::Int8, 0, TStringBuf("\x00"sv));
+ addMultiCase(EWireType::Int8, 42, TStringBuf("*"));
+ addMultiCase(EWireType::Int8, -42, TStringBuf("\xd6"sv));
+ addMultiCase(EWireType::Int8, 127, TStringBuf("\x7f"sv));
+ addMultiCase(EWireType::Int8, -128, TStringBuf("\x80"sv));
+
+ addMultiCase(EWireType::Int16, 0, TStringBuf("\x00\x00"sv));
+ addMultiCase(EWireType::Int16, 42, TStringBuf("\x2a\x00"sv));
+ addMultiCase(EWireType::Int16, -42, TStringBuf("\xd6\xff"sv));
+ addMultiCase(EWireType::Int16, 0x7fff, TStringBuf("\xff\x7f"sv));
+ addMultiCase(EWireType::Int16, -0x8000, TStringBuf("\x00\x80"sv));
+
+ addMultiCase(EWireType::Int32, 0, TStringBuf("\x00\x00\x00\x00"sv));
+ addMultiCase(EWireType::Int32, 42, TStringBuf("\x2a\x00\x00\x00"sv));
+ addMultiCase(EWireType::Int32, -42, TStringBuf("\xd6\xff\xff\xff"sv));
+ addMultiCase(EWireType::Int32, 0x7fffffff, TStringBuf("\xff\xff\xff\x7f"sv));
+ addMultiCase(EWireType::Int32, -0x80000000l, TStringBuf("\x00\x00\x00\x80"sv));
+
+ addMultiCase(EWireType::Uint8, 0ull, TStringBuf("\x00"sv));
+ addMultiCase(EWireType::Uint8, 42ull, TStringBuf("*"));
+ addMultiCase(EWireType::Uint8, 255ull, TStringBuf("\xff"sv));
+
+ addMultiCase(EWireType::Uint16, 0ull, TStringBuf("\x00\x00"sv));
+ addMultiCase(EWireType::Uint16, 42ull, TStringBuf("\x2a\x00"sv));
+ addMultiCase(EWireType::Uint16, 0xFFFFull, TStringBuf("\xff\xff"sv));
+
+ addMultiCase(EWireType::Uint32, 0ull, TStringBuf("\x00\x00\x00\x00"sv));
+ addMultiCase(EWireType::Uint32, 42ull, TStringBuf("\x2a\x00\x00\x00"sv));
+ addMultiCase(EWireType::Uint32, 0xFFFFFFFFull, TStringBuf("\xff\xff\xff\xff"sv));
+
+ addSimpleAndListCases(EWireType::Uint16, Date(), 0ull, TStringBuf("\x00\x00"sv));
+ addSimpleAndListCases(EWireType::Uint16, Date(), 42ull, TStringBuf("\x2a\x00"sv));
+ addSimpleAndListCases(EWireType::Uint16, Date(), DateUpperBound - 1, TStringBuf("\x08\xc2"sv));
+
+ addSimpleAndListCases(EWireType::Uint32, Datetime(), 0ull, TStringBuf("\x00\x00\x00\x00"sv));
+ addSimpleAndListCases(EWireType::Uint32, Datetime(), 42ull, TStringBuf("\x2a\x00\x00\x00"sv));
+ addSimpleAndListCases(EWireType::Uint32, Datetime(), DatetimeUpperBound - 1, TStringBuf("\x7f\xdd\xce\xff"sv));
+
+ return result;
+ }
+
+ static const std::vector<ParamType> Cases;
+};
+
+const std::vector<TSkiffFormatSmallIntP::ParamType> TSkiffFormatSmallIntP::Cases = TSkiffFormatSmallIntP::GetCases();
+
+INSTANTIATE_TEST_SUITE_P(
+ Cases,
+ TSkiffFormatSmallIntP,
+ ::testing::ValuesIn(TSkiffFormatSmallIntP::Cases));
+
+TEST_P(TSkiffFormatSmallIntP, Test)
+{
+ const auto& [skiffValueSchema, logicalType, value, expectedSkiffData] = GetParam();
+
+ const auto nameTable = New<TNameTable>();
+
+ TStringStream actualSkiffData;
+ auto skiffTableSchema = CreateTupleSchema({
+ skiffValueSchema->SetName("column")
+ });
+ auto tableSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema("column", logicalType),
+ });
+ auto writer = CreateSkiffWriter(skiffTableSchema, nameTable, &actualSkiffData, {tableSchema});
+ writer->Write({
+ MakeRow(nameTable, {{"column", value}})
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ EXPECT_EQ(actualSkiffData.Str(), expectedSkiffData);
+
+ TCollectingValueConsumer rowCollector(nameTable);
+ auto parser = CreateParserForSkiff(skiffTableSchema, tableSchema, &rowCollector);
+ parser->Read(expectedSkiffData);
+ parser->Finish();
+ auto actualValue = rowCollector.GetRowValue(0, "column");
+
+ EXPECT_EQ(actualValue, TNamedValue("common", value).ToUnversionedValue(nameTable));
+}
+
+TEST(TSkiffWriter, TestBadSmallIntegers)
+{
+ using namespace NLogicalTypeShortcuts;
+ auto writeSkiffValue = [] (
+ std::shared_ptr<TSkiffSchema>&& typeSchema,
+ TLogicalTypePtr logicalType,
+ TNamedValue::TValue value)
+ {
+ TStringStream result;
+ auto skiffSchema = CreateTupleSchema({
+ typeSchema->SetName("column")
+ });
+ auto tableSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ TColumnSchema("column", std::move(logicalType)),
+ });
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &result, {tableSchema});
+ writer->Write({
+ MakeRow(nameTable, {{"column", std::move(value)}})
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ return result.Str();
+ };
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int8), Int64(), 128),
+ "is out of range for possible values");
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int8), Int64(), -129),
+ "is out of range for possible values");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int16), Int64(), 0x8000),
+ "is out of range for possible values");
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int16), Int64(), -0x8001),
+ "is out of range for possible values");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int32), Int64(), 0x80000000ll),
+ "is out of range for possible values");
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Int32), Int64(), -0x80000001ll),
+ "is out of range for possible values");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Uint8), Uint64(), 256ull),
+ "is out of range for possible values");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Uint16), Uint64(), 0x1FFFFull),
+ "is out of range for possible values");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ writeSkiffValue(CreateSimpleTypeSchema(EWireType::Uint32), Uint64(), 0x100000000ull),
+ "is out of range for possible values");
+}
+
+class TSkiffFormatUuidTestP : public ::testing::TestWithParam<std::tuple<
+ TNameTablePtr,
+ TTableSchemaPtr,
+ std::shared_ptr<TSkiffSchema>,
+ std::vector<TUnversionedOwningRow>,
+ TString
+>>
+{
+public:
+ static std::vector<ParamType> GetCases()
+ {
+ using namespace NLogicalTypeShortcuts;
+
+ auto nameTable = New<TNameTable>();
+ const auto stringUuidValue = TStringBuf("\xee\x1f\x37\x70" "\xb9\x93\x64\xb5" "\xe4\xdf\xe9\x03" "\x67\x5c\x30\x62");
+ const auto uint128UuidValue = TStringBuf("\x62\x30\x5c\x67" "\x03\xe9\xdf\xe4" "\xb5\x64\x93\xb9" "\x70\x37\x1f\xee");
+
+ const auto requiredTableSchema = New<TTableSchema>(std::vector<TColumnSchema>{TColumnSchema("uuid", Uuid())});
+ const auto optionalTableSchema = New<TTableSchema>(std::vector<TColumnSchema>{TColumnSchema("uuid", Optional(Uuid()))});
+
+ const auto optionalUint128SkiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Uint128),
+ })->SetName("uuid"),
+ });
+
+ const auto requiredUint128SkiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint128)->SetName("uuid"),
+ });
+
+ const auto optionalStringSkiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::String32),
+ })->SetName("uuid"),
+ });
+
+ const auto requiredStringSkiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("uuid"),
+ });
+
+ std::vector<ParamType> result;
+
+ result.emplace_back(
+ nameTable,
+ requiredTableSchema,
+ requiredUint128SkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + uint128UuidValue);
+
+ result.emplace_back(
+ nameTable,
+ optionalTableSchema,
+ requiredUint128SkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + uint128UuidValue);
+
+ result.emplace_back(
+ nameTable,
+ requiredTableSchema,
+ optionalUint128SkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + "\1" + uint128UuidValue);
+
+ result.emplace_back(
+ nameTable,
+ optionalTableSchema,
+ optionalUint128SkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + "\1" + uint128UuidValue);
+
+ const TString uuidLen = TString(TStringBuf("\x10\x00\x00\x00"sv));
+
+ result.emplace_back(
+ nameTable,
+ requiredTableSchema,
+ requiredStringSkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + uuidLen + stringUuidValue);
+
+ result.emplace_back(
+ nameTable,
+ optionalTableSchema,
+ requiredStringSkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + uuidLen + stringUuidValue);
+
+ result.emplace_back(
+ nameTable,
+ requiredTableSchema,
+ optionalStringSkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + "\1" + uuidLen + stringUuidValue);
+
+ result.emplace_back(
+ nameTable,
+ optionalTableSchema,
+ optionalStringSkiffSchema,
+ std::vector<TUnversionedOwningRow>{
+ MakeRow(nameTable, {{"uuid", stringUuidValue}}),
+ },
+ TString(2, '\0') + "\1" + uuidLen + stringUuidValue);
+
+ return result;
+ }
+
+ static const std::vector<ParamType> Cases;
+};
+
+const std::vector<TSkiffFormatUuidTestP::ParamType> TSkiffFormatUuidTestP::Cases = TSkiffFormatUuidTestP::GetCases();
+
+INSTANTIATE_TEST_SUITE_P(
+ Cases,
+ TSkiffFormatUuidTestP,
+ ::testing::ValuesIn(TSkiffFormatUuidTestP::Cases));
+
+TEST_P(TSkiffFormatUuidTestP, Test)
+{
+ const auto& [nameTable, tableSchema, skiffSchema, rows, skiffString] = GetParam();
+
+ TStringStream result;
+ std::vector<TUnversionedRow> nonOwningRows;
+ for (const auto& row : rows) {
+ nonOwningRows.emplace_back(row);
+ }
+ auto skiffWriter = CreateSkiffWriter(skiffSchema, nameTable, &result, {tableSchema});
+ skiffWriter->Write(MakeRange(nonOwningRows));
+ skiffWriter->Close().Get().ThrowOnError();
+ ASSERT_EQ(result.Str(), skiffString);
+
+ TCollectingValueConsumer rowCollector(nameTable);
+ auto requiredParser = CreateParserForSkiff(skiffSchema, tableSchema, &rowCollector);
+ requiredParser->Read(result.Str());
+ requiredParser->Finish();
+ ASSERT_EQ(rowCollector.GetRowList(), rows);
+}
+
+TEST(TSkiffFormatUuidTest, TestError)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ auto nameTable = New<TNameTable>();
+ auto tableSchema = New<TTableSchema>(
+ std::vector<TColumnSchema>{TColumnSchema("uuid", Optional(Uuid()))});
+
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Uint128)->SetName("uuid"),
+ });
+
+ TStringStream result;
+ auto skiffWriter = CreateSkiffWriter(skiffSchema, nameTable, &result, {tableSchema});
+ skiffWriter->Write({
+ MakeRow(nameTable, {{"uuid", nullptr}}),
+ });
+ EXPECT_THROW_WITH_SUBSTRING(skiffWriter->Close().Get().ThrowOnError(),
+ "Unexpected type");
+
+}
+
+class TSkiffWriterSingular
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<ESimpleLogicalValueType>
+{};
+
+INSTANTIATE_TEST_SUITE_P(
+ Singular,
+ TSkiffWriterSingular,
+ ::testing::Values(ESimpleLogicalValueType::Null, ESimpleLogicalValueType::Void));
+
+TEST_P(TSkiffWriterSingular, TestOptionalSingular)
+{
+ const auto singularType = GetParam();
+
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ })->SetName("opt_null"),
+ });
+
+ auto nameTable = New<TNameTable>();
+ const std::vector<TTableSchemaPtr> tableSchemas = {
+ New<TTableSchema>(std::vector{
+ TColumnSchema("opt_null", OptionalLogicalType(SimpleLogicalType(singularType))),
+ }),
+ };
+
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, tableSchemas);
+ // Row 0
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"opt_null", nullptr},
+ }).Get(),
+ });
+ // Row 1
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"opt_null", EValueType::Composite, "[#]"},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ }
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestRearrange)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("number"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::String32),
+ })->SetName("eng"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::String32),
+ })->SetName("rus"),
+ });
+ auto nameTable = New<TNameTable>();
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"number", 1},
+ {"eng", "one"},
+ {"rus", nullptr},
+ }).Get()
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"eng", nullptr},
+ {"number", 2},
+ {"rus", "dva"},
+ }).Get()
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"rus", "tri"},
+ {"eng", "three"},
+ {"number", 3},
+ }).Get()
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ }
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 2);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "dva");
+
+ // row 2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 3);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "three");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "tri");
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestMissingRequiredField)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("number"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("eng"),
+ });
+ auto nameTable = New<TNameTable>();
+ TString result;
+ try {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"number", 1},
+ }).Get()
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Unexpected type of \"eng\" column"));
+ }
+}
+
+TEST(TSkiffWriter, TestSparse)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("int64"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("uint64"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("string32"),
+ })->SetName("$sparse_columns"),
+ });
+
+ auto nameTable = New<TNameTable>();
+ TString result;
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"int64", -1},
+ {"string32", "minus one"},
+ }).Get(),
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"string32", "minus five"},
+ {"int64", -5},
+ }).Get(),
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"uint64", 42u},
+ }).Get(),
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"int64", -8},
+ {"uint64", nullptr},
+ {"string32", nullptr},
+ }).Get(),
+ });
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -1);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 2);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "minus one");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 2);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "minus five");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -5);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ // row 2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseUint64(), 42u);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ // row 3
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), -8);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ // row 4
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestMissingFields)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ });
+
+ try {
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"unknown_column", "four"},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Column \"unknown_column\" is not described by Skiff schema"));
+ }
+
+ try {
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto unknownColumnId = nameTable->RegisterName("unknown_column");
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{New<TTableSchema>()});
+
+ ASSERT_TRUE(unknownColumnId < nameTable->GetId("value"));
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"unknown_column", "four"},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+ ADD_FAILURE();
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Column \"unknown_column\" is not described by Skiff schema"));
+ }
+}
+
+TEST(TSkiffWriter, TestOtherColumns)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64)
+ })->SetName("int64_column"),
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("$other_columns"),
+ });
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ nameTable->RegisterName("string_column");
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()});
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"string_column", "foo"},
+ }).Get(),
+ });
+
+ // Row 1.
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"int64_column", 42},
+ }).Get(),
+ });
+ // Row 2.
+ writer->Write({
+ MakeRow(nameTable, {
+ {TableIndexColumnName, 0},
+ {"other_string_column", "bar"},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ auto parseYson = [] (TCheckedSkiffParser* parser) {
+ auto yson = TString{parser->ParseYson32()};
+ return ConvertToYsonTextStringStable(ConvertToNode(TYsonString(yson)));
+ };
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser), "{\"string_column\"=\"foo\";}");
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 42);
+ ASSERT_EQ(parseYson(&checkedSkiffParser), "{}");
+
+ // row 2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(parseYson(&checkedSkiffParser), "{\"other_string_column\"=\"bar\";}");
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestKeySwitch)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$key_switch"),
+ });
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()}, 1);
+
+ writer->Write({
+ // Row 0.
+ MakeRow(nameTable, {
+ {"value", "one"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ // Row 1.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "one"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ // Row 2.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "two"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ TString buf;
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+
+ // row 2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "two");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), true);
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestEndOfStream)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ });
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()}, 1, true);
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "zero"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ // Row 1.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "one"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ TString buf;
+
+ // Row 0.
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "zero");
+
+ // Row 1.
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+
+ // End of stream.
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0xffff);
+
+ // The End.
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestRowRangeIndex)
+{
+ const auto rowAndRangeIndex = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName("$range_index"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName("$row_index"),
+ });
+
+ struct TRow {
+ int TableIndex;
+ std::optional<int> RangeIndex;
+ std::optional<int> RowIndex;
+ };
+ auto generateUnversionedRow = [] (const TRow& row, const TNameTablePtr& nameTable) {
+ std::vector<TNamedValue> values = {
+ {TableIndexColumnName, row.TableIndex},
+ };
+ if (row.RangeIndex) {
+ values.emplace_back(RangeIndexColumnName, *row.RangeIndex);
+ }
+ if (row.RowIndex) {
+ values.push_back({RowIndexColumnName, *row.RowIndex});
+ }
+ return MakeRow(nameTable, values);
+ };
+
+ auto skiffWrite = [generateUnversionedRow] (const std::vector<TRow>& rows, const std::shared_ptr<TSkiffSchema>& skiffSchema) {
+ std::vector<TTableSchemaPtr> tableSchemas;
+ {
+ THashSet<int> tableIndices;
+ for (const auto& row : rows) {
+ tableIndices.insert(row.TableIndex);
+ }
+ tableSchemas.assign(tableIndices.size(), New<TTableSchema>());
+ }
+
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(
+ skiffSchema,
+ nameTable,
+ &resultStream,
+ tableSchemas);
+
+ for (const auto& row : rows) {
+ writer->Write({generateUnversionedRow(row, nameTable)});
+ }
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ return HexEncode(resultStream.Str());
+ };
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 0, 2},
+ }, rowAndRangeIndex).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "00" "00"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 0, 3},
+ }, rowAndRangeIndex).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "00" "01""03000000""00000000"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 1, 2},
+ {0, 1, 3},
+ }, rowAndRangeIndex).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "01""01000000""00000000" "01""02000000""00000000"
+ "0000" "00" "00"
+ );
+
+ EXPECT_THROW_WITH_SUBSTRING(skiffWrite({{0, 0, {}}}, rowAndRangeIndex), "index requested but reader did not return it");
+ EXPECT_THROW_WITH_SUBSTRING(skiffWrite({{0, {}, 0}}, rowAndRangeIndex), "index requested but reader did not return it");
+
+ const auto rowAndRangeIndexAllowMissing = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ })->SetName("$range_index"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ })->SetName("$row_index"),
+ });
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 0, 2},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "00" "00"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 0, 3},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "00" "01""03000000""00000000"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, 0},
+ {0, 0, 1},
+ {0, 1, 2},
+ {0, 1, 3},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "01""00000000""00000000" "01""00000000""00000000"
+ "0000" "00" "00"
+ "0000" "01""01000000""00000000" "01""02000000""00000000"
+ "0000" "00" "00"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, {}, {}},
+ {0, {}, {}},
+ {0, {}, {}},
+ {0, {}, {}},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "02" "02"
+ "0000" "02" "02"
+ "0000" "02" "02"
+ "0000" "02" "02"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, {}, 0},
+ {0, {}, 1},
+ {0, {}, 3},
+ {0, {}, 4},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "02" "01""00000000""00000000"
+ "0000" "02" "00"
+ "0000" "02" "01""03000000""00000000"
+ "0000" "02" "00"
+ );
+
+ EXPECT_STREQ(
+ skiffWrite({
+ {0, 0, {}},
+ {0, 0, {}},
+ {0, 1, {}},
+ {0, 1, {}},
+ }, rowAndRangeIndexAllowMissing).data(),
+
+ "0000" "01""00000000""00000000" "02"
+ "0000" "00" "02"
+ "0000" "01""01000000""00000000" "02"
+ "0000" "00" "02"
+ );
+}
+
+TEST(TSkiffWriter, TestRowIndexOnlyOrRangeIndexOnly)
+{
+ TString columnNameList[] = {
+ RowIndexColumnName,
+ RangeIndexColumnName,
+ };
+
+ for (const auto& columnName : columnNameList) {
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName(columnName),
+ });
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()}, 1);
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {columnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+}
+
+TEST(TSkiffWriter, TestComplexType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateRepeatedVariant8Schema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("x"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("y"),
+ })
+ })->SetName("points")
+ })->SetName("value"),
+ });
+
+ {
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("value", StructLogicalType({
+ {"name", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {
+ "points",
+ ListLogicalType(
+ StructLogicalType({
+ {"x", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"y", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ })
+ )
+ }
+ })),
+ });
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{tableSchema});
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", EValueType::Composite, "[foo;[[0; 1];[2;3]]]"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "foo");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 2);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 3);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), EndOfSequenceTag<ui8>());
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+}
+
+TEST(TSkiffWriter, TestEmptyComplexType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ })
+ })->SetName("value"),
+ });
+
+ {
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("value", OptionalLogicalType(
+ StructLogicalType({
+ {"name", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))
+ ),
+ });
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{tableSchema});
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", nullptr},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 0);
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+}
+
+TEST(TSkiffWriter, TestSparseComplexType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ })->SetName("value"),
+ })->SetName("$sparse_columns"),
+ });
+
+ {
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("value", OptionalLogicalType(
+ StructLogicalType({
+ {"name", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))
+ ),
+ });
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{tableSchema});
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", EValueType::Composite, "[foo;bar;]"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "foo");
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "bar");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+}
+
+TEST(TSkiffWriter, TestSparseComplexTypeWithExtraOptional)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ })
+ })->SetName("value"),
+ })->SetName("$sparse_columns"),
+ });
+
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("value", OptionalLogicalType(
+ StructLogicalType({
+ {"name", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ }))
+ ),
+ });
+
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{tableSchema});
+
+ // Row 0.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", EValueType::Composite, "[foo;bar;]"},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "foo");
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "bar");
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), EndOfSequenceTag<ui16>());
+
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+}
+
+TEST(TSkiffWriter, TestBadWireTypeForSimpleColumn)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ })
+ })->SetName("opt_yson32"),
+ });
+ auto nameTable = New<TNameTable>();
+ TStringStream resultStream;
+ EXPECT_THROW_WITH_SUBSTRING(
+ CreateSkiffWriter(skiffSchema, nameTable, &resultStream, std::vector{New<TTableSchema>()}),
+ "cannot be represented with Skiff schema"
+ );
+}
+
+TEST(TSkiffWriter, TestMissingComplexColumn)
+{
+ auto optionalSkiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Int64)}),
+ })->SetName("opt_list"),
+ });
+ auto requiredSkiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Int64)})->SetName("opt_list"),
+ });
+
+ { // Non optional Skiff schema
+ auto nameTable = New<TNameTable>();
+ EXPECT_THROW_WITH_SUBSTRING(
+ CreateSkiffWriter(requiredSkiffSchema, nameTable, &Cnull, std::vector{New<TTableSchema>()}),
+ "cannot be represented with Skiff schema"
+ );
+ }
+
+ {
+ auto nameTable = New<TNameTable>();
+ TStringStream resultStream;
+ auto writer = CreateSkiffWriter(optionalSkiffSchema, nameTable, &resultStream, std::vector{New<TTableSchema>()});
+ writer->Write({
+ MakeRow(nameTable, { }).Get(),
+ MakeRow(nameTable, {
+ {"opt_list", nullptr},
+ }).Get(),
+ MakeRow(nameTable, { }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_EQ(HexEncode(resultStream.Str()), "0000" "00" "0000" "00" "0000" "00");
+ }
+}
+
+TEST(TSkiffWriter, TestSkippedFields)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("number"),
+ CreateSimpleTypeSchema(EWireType::Nothing)->SetName("string"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName(RangeIndexColumnName),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName(RowIndexColumnName),
+ CreateSimpleTypeSchema(EWireType::Double)->SetName("double"),
+ });
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("number", EValueType::Int64),
+ TColumnSchema("string", EValueType::String),
+ TColumnSchema("double", EValueType::Double),
+ });
+
+ auto nameTable = New<TNameTable>();
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {tableSchema});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"number", 1},
+ {"string", "hello"},
+ {RangeIndexColumnName, 0},
+ {RowIndexColumnName, 0},
+ {"double", 1.5},
+ }).Get()
+ });
+ writer->Write({
+ MakeRow(nameTable, {
+ {"number", 1},
+ {RangeIndexColumnName, 5},
+ {RowIndexColumnName, 1},
+ {"double", 2.5},
+ }).Get()
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 1.5);
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 5);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseDouble(), 2.5);
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+
+}
+
+TEST(TSkiffWriter, TestSkippedFieldsOutOfRange)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Nothing)->SetName("string"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName(RangeIndexColumnName),
+ });
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("string", EValueType::String),
+ });
+
+ auto nameTable = New<TNameTable>();
+ TString result;
+ {
+ TStringOutput resultStream(result);
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {tableSchema});
+
+ writer->Write({
+ MakeRow(nameTable, {
+ {"string", "hello"},
+ {RangeIndexColumnName, 0},
+ }).Get()
+ });
+ writer->Write({
+ MakeRow(nameTable, {
+ {RangeIndexColumnName, 5},
+ }).Get()
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(result);
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseVariant8Tag(), 1);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 5);
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+ }
+
+}
+
+TEST(TSkiffWriter, TestSkippedFieldsAndKeySwitch)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("value"),
+ CreateSimpleTypeSchema(EWireType::Nothing)->SetName("skipped"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$key_switch"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("value1"),
+ });
+ TStringStream resultStream;
+ auto nameTable = New<TNameTable>();
+ auto writer = CreateSkiffWriter(skiffSchema, nameTable, &resultStream, {New<TTableSchema>()}, 1);
+
+ writer->Write({
+ // Row 0.
+ MakeRow(nameTable, {
+ {"value", "one"},
+ {"value1", 0},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ // Row 1.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "one"},
+ {"value1", 1},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ // Row 2.
+ writer->Write({
+ MakeRow(nameTable, {
+ {"value", "two"},
+ {"value1", 2},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ });
+ writer->Close()
+ .Get()
+ .ThrowOnError();
+
+ TStringInput resultInput(resultStream.Str());
+ TCheckedSkiffParser checkedSkiffParser(CreateVariant16Schema({skiffSchema}), &resultInput);
+
+ TString buf;
+
+ // row 0
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 0);
+
+ // row 1
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "one");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), false);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 1);
+
+ // row 2
+ ASSERT_EQ(checkedSkiffParser.ParseVariant16Tag(), 0);
+ ASSERT_EQ(checkedSkiffParser.ParseString32(), "two");
+ ASSERT_EQ(checkedSkiffParser.ParseBoolean(), true);
+ ASSERT_EQ(checkedSkiffParser.ParseInt64(), 2);
+
+ // end
+ ASSERT_EQ(checkedSkiffParser.HasMoreData(), false);
+ checkedSkiffParser.ValidateFinished();
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSkiffParser, Simple)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("int64"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("uint64"),
+ CreateSimpleTypeSchema(EWireType::Double)->SetName("double"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("boolean"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("string32"),
+ CreateSimpleTypeSchema(EWireType::Nothing)->SetName("null"),
+
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ })->SetName("opt_int64"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ })->SetName("opt_uint64"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Double),
+ })->SetName("opt_double"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ })->SetName("opt_boolean"),
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::String32),
+ })->SetName("opt_string32"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteInt64(-1);
+ checkedSkiffWriter.WriteUint64(2);
+ checkedSkiffWriter.WriteDouble(3.0);
+ checkedSkiffWriter.WriteBoolean(true);
+ checkedSkiffWriter.WriteString32("foo");
+
+ checkedSkiffWriter.WriteVariant8Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(0);
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 1);
+
+ ASSERT_EQ(GetInt64(collectedRows.GetRowValue(0, "int64")), -1);
+ ASSERT_EQ(GetUint64(collectedRows.GetRowValue(0, "uint64")), 2u);
+ ASSERT_EQ(GetDouble(collectedRows.GetRowValue(0, "double")), 3.0);
+ ASSERT_EQ(GetBoolean(collectedRows.GetRowValue(0, "boolean")), true);
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(0, "string32")), "foo");
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "null")), true);
+
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "opt_int64")), true);
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "opt_uint64")), true);
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "opt_double")), true);
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "opt_boolean")), true);
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(0, "opt_string32")), true);
+}
+
+TEST(TSkiffParser, TestOptionalNull)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ })->SetName("opt_null"),
+ });
+ auto nameTable = New<TNameTable>();
+
+ {
+ TCollectingValueConsumer collectedRows;
+ EXPECT_THROW_WITH_SUBSTRING(
+ CreateParserForSkiff(skiffSchema, &collectedRows),
+ "cannot be represented with Skiff schema");
+ }
+
+ auto tableSchema = New<TTableSchema>(std::vector{
+ TColumnSchema("opt_null", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))),
+ });
+
+ TCollectingValueConsumer collectedRows(tableSchema);
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(0);
+
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(1);
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+
+ ASSERT_EQ(collectedRows.GetRowValue(0, "opt_null").Type, EValueType::Null);
+}
+
+TEST(TSkiffParser, TestSparse)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("int64"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("uint64"),
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("string32"),
+ })->SetName("$sparse_columns"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // row 1
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ // sparse fields begin
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteInt64(-42);
+ checkedSkiffWriter.WriteVariant16Tag(1);
+ checkedSkiffWriter.WriteUint64(54);
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ // row 2
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ // sparse fields begin
+ checkedSkiffWriter.WriteVariant16Tag(2);
+ checkedSkiffWriter.WriteString32("foo");
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+
+ ASSERT_EQ(GetInt64(collectedRows.GetRowValue(0, "int64")), -42);
+ ASSERT_EQ(GetUint64(collectedRows.GetRowValue(0, "uint64")), 54u);
+ ASSERT_FALSE(collectedRows.FindRowValue(0, "string32"));
+
+ ASSERT_FALSE(collectedRows.FindRowValue(1, "int64"));
+ ASSERT_FALSE(collectedRows.FindRowValue(1, "uint64"));
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(1, "string32")), "foo");
+}
+
+TEST(TSkiffParser, TestYsonWireType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // Row 0.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("-42");
+
+ // Row 1.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("42u");
+
+ // Row 2.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("\"foobar\"");
+
+ // Row 3.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("%true");
+
+ // Row 4.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("{foo=bar}");
+
+ // Row 5.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32("#");
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 6);
+ ASSERT_EQ(GetInt64(collectedRows.GetRowValue(0, "yson")), -42);
+ ASSERT_EQ(GetUint64(collectedRows.GetRowValue(1, "yson")), 42u);
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(2, "yson")), "foobar");
+ ASSERT_EQ(GetBoolean(collectedRows.GetRowValue(3, "yson")), true);
+ ASSERT_EQ(GetAny(collectedRows.GetRowValue(4, "yson"))->AsMap()->GetChildOrThrow("foo")->AsString()->GetValue(), "bar");
+ ASSERT_EQ(IsNull(collectedRows.GetRowValue(5, "yson")), true);
+}
+
+TEST(TSkiffParser, TestBadYsonWireType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson"),
+ });
+
+ auto parseYsonUsingSkiff = [&] (TStringBuf ysonValue) {
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+ TStringStream dataStream;
+ ASSERT_NO_THROW({
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteYson32(ysonValue);
+
+ checkedSkiffWriter.Finish();
+ });
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+ };
+
+ try {
+ parseYsonUsingSkiff("[42");
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Premature end of stream"));
+ }
+
+ try {
+ parseYsonUsingSkiff("<foo=bar>42");
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Table values cannot have top-level attributes"));
+ }
+}
+
+TEST(TSkiffParser, TestSpecialColumns)
+{
+ std::shared_ptr<TSkiffSchema> skiffSchemaList[] = {
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$key_switch"),
+ }),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$row_switch"),
+ }),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("yson"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("$range_switch"),
+ }),
+ };
+
+ for (const auto& skiffSchema : skiffSchemaList) {
+ try {
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+ } catch (std::exception& e) {
+ EXPECT_THAT(e.what(), testing::HasSubstr("Skiff parser does not support \"$key_switch\""));
+ }
+ }
+}
+
+TEST(TSkiffParser, TestOtherColumns)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("$other_columns"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // Row 0.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteString32("row_0");
+ checkedSkiffWriter.WriteYson32("{foo=-42;}");
+
+ // Row 1.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteString32("row_1");
+ checkedSkiffWriter.WriteYson32("{bar=qux;baz={boolean=%false;};}");
+
+ // Row 2.
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(0, "name")), "row_0");
+ ASSERT_EQ(GetInt64(collectedRows.GetRowValue(0, "foo")), -42);
+
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(1, "name")), "row_1");
+ ASSERT_EQ(GetString(collectedRows.GetRowValue(1, "bar")), "qux");
+ ASSERT_EQ(ConvertToYsonTextStringStable(GetAny(collectedRows.GetRowValue(1, "baz"))), "{\"boolean\"=%false;}");
+}
+
+TEST(TSkiffParser, TestComplexColumn)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("value"),
+ })->SetName("column")
+ });
+
+ TCollectingValueConsumer collectedRows(
+ New<TTableSchema>(std::vector{
+ TColumnSchema("column", NTableClient::StructLogicalType({
+ {"key", NTableClient::SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", NTableClient::SimpleLogicalType(ESimpleLogicalValueType::Int64)}
+ }))
+ }));
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // Row 0.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteString32("row_0");
+ checkedSkiffWriter.WriteInt64(42);
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 1);
+ ASSERT_EQ(ConvertToYsonTextStringStable(GetComposite(collectedRows.GetRowValue(0, "column"))), "[\"row_0\";42;]");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSkiffParser, TestEmptyInput)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("column"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+
+ {
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+ parser->Finish();
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 0);
+ }
+ {
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+ parser->Read("");
+ parser->Finish();
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 0);
+ }
+ {
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+ parser->Read("");
+ parser->Read("");
+ parser->Finish();
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 0);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSkiffParser, ColumnIds)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("field_a"),
+ CreateSimpleTypeSchema(EWireType::Uint64)->SetName("field_b")
+ });
+
+ TCollectingValueConsumer collectedRows;
+ collectedRows.GetNameTable()->GetIdOrRegisterName("field_b");
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteInt64(-1);
+ checkedSkiffWriter.WriteUint64(2);
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 1);
+
+ ASSERT_EQ(GetInt64(collectedRows.GetRowValue(0, "field_a")), -1);
+ ASSERT_EQ(GetUint64(collectedRows.GetRowValue(0, "field_b")), 2u);
+}
+
+TEST(TSkiffParser, TestSparseComplexType)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("name"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("value"),
+ })->SetName("value"),
+ })->SetName("$sparse_columns"),
+ });
+
+ TCollectingValueConsumer collectedRows(
+ New<TTableSchema>(std::vector{
+ TColumnSchema("value", OptionalLogicalType(
+ StructLogicalType({
+ {"name", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", SimpleLogicalType(ESimpleLogicalValueType::Int64)}
+ })
+ ))
+ }));
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // Row 0.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteString32("row_0");
+ checkedSkiffWriter.WriteInt64(10);
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ // Row 1.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+ EXPECT_EQ(ConvertToYsonTextStringStable(GetComposite(collectedRows.GetRowValue(0, "value"))), "[\"row_0\";10;]");
+ EXPECT_FALSE(collectedRows.FindRowValue(1, "value"));
+}
+
+TEST(TSkiffParser, TestSparseComplexTypeWithExtraOptional)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateRepeatedVariant16Schema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("value"),
+ })
+ })->SetName("column"),
+ })->SetName("$sparse_columns"),
+ });
+
+ TCollectingValueConsumer collectedRows(
+ New<TTableSchema>(std::vector{
+ TColumnSchema("column", OptionalLogicalType(
+ StructLogicalType({
+ {"key", NTableClient::SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"value", NTableClient::SimpleLogicalType(ESimpleLogicalValueType::Int64)}
+ })
+ ))
+ }));
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ TStringStream dataStream;
+ TCheckedSkiffWriter checkedSkiffWriter(CreateVariant16Schema({skiffSchema}), &dataStream);
+
+ // Row 0.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant8Tag(1);
+ checkedSkiffWriter.WriteString32("row_0");
+ checkedSkiffWriter.WriteInt64(42);
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ // Row 1.
+ checkedSkiffWriter.WriteVariant16Tag(0);
+ checkedSkiffWriter.WriteVariant16Tag(EndOfSequenceTag<ui16>());
+
+ checkedSkiffWriter.Finish();
+
+ parser->Read(dataStream.Str());
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+ ASSERT_EQ(ConvertToYsonTextStringStable(GetComposite(collectedRows.GetRowValue(0, "column"))), "[\"row_0\";42;]");
+ ASSERT_FALSE(collectedRows.FindRowValue(1, "column"));
+}
+
+
+TEST(TSkiffParser, TestBadWireTypeForSimpleColumn)
+{
+ auto skiffSchema = CreateTupleSchema({
+ CreateVariant8Schema({
+ CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ })
+ })->SetName("opt_yson32"),
+ });
+
+ TCollectingValueConsumer collectedRows;
+ EXPECT_THROW_WITH_SUBSTRING(
+ CreateParserForSkiff(skiffSchema, &collectedRows),
+ "cannot be represented with Skiff schema"
+ );
+}
+
+TEST(TSkiffParser, TestEmptyColumns)
+{
+ auto skiffSchema = CreateTupleSchema({});
+ TCollectingValueConsumer collectedRows;
+ auto parser = CreateParserForSkiff(skiffSchema, &collectedRows);
+
+ parser->Read(TStringBuf("\x00\x00\x00\x00"sv));
+ parser->Finish();
+
+ ASSERT_EQ(static_cast<int>(collectedRows.Size()), 2);
+}
+
+TEST(TSkiffFormat, TestTimestamp)
+{
+ using namespace NLogicalTypeShortcuts;
+ CHECK_BIDIRECTIONAL_CONVERSION(Timestamp(), CreateSimpleTypeSchema(EWireType::Uint64), 42ull, "2A000000" "00000000");
+ CHECK_BIDIRECTIONAL_CONVERSION(Interval(), CreateSimpleTypeSchema(EWireType::Int64), 42, "2A000000" "00000000");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/client/unittests/skiff_yson_converter_ut.cpp b/yt/yt/client/unittests/skiff_yson_converter_ut.cpp
new file mode 100644
index 0000000000..18ecfac352
--- /dev/null
+++ b/yt/yt/client/unittests/skiff_yson_converter_ut.cpp
@@ -0,0 +1,728 @@
+#include "logical_type_shortcuts.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/formats/skiff_yson_converter.h>
+
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/skiff/skiff.h>
+#include <library/cpp/skiff/skiff_schema.h>
+
+#include <util/string/hex.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NTableClient;
+using namespace NSkiff;
+using namespace NYson;
+using namespace NTableClient::NLogicalTypeShortcuts;
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::shared_ptr<TSkiffSchema> SkiffOptional(std::shared_ptr<TSkiffSchema> skiffSchema)
+{
+ return CreateVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ std::move(skiffSchema)
+ });
+}
+
+TString ConvertYsonHex(
+ const TLogicalTypePtr& logicalType,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ TStringBuf ysonString,
+ const TYsonToSkiffConverterConfig& config = {})
+{
+ auto converter = CreateYsonToSkiffConverter(
+ TComplexTypeFieldDescriptor("test-field", logicalType),
+ skiffSchema,
+ config);
+
+ // Yson parsers have a bug when they can't parse some values that end unexpectedly.
+ TString spacedYsonInput = TString{ysonString} + " ";
+
+ TStringStream out;
+ {
+ TCheckedInDebugSkiffWriter writer(skiffSchema, &out);
+
+ TMemoryInput in(spacedYsonInput);
+ TYsonPullParser pullParser(&in, EYsonType::Node);
+ TYsonPullParserCursor cursor(&pullParser);
+
+ converter(&cursor, &writer);
+
+ EXPECT_EQ(cursor.GetCurrent().GetType(), EYsonItemType::EndOfStream);
+ writer.Finish();
+ }
+
+ auto result = HexEncode(out.Str());
+ result.to_lower();
+ return result;
+}
+
+TString ConvertHexToTextYson(
+ const TLogicalTypePtr& logicalType,
+ const std::shared_ptr<TSkiffSchema>& skiffSchema,
+ TStringBuf hexString,
+ const TSkiffToYsonConverterConfig& config = {})
+{
+ auto converter = CreateSkiffToYsonConverter(TComplexTypeFieldDescriptor("test-field", logicalType), skiffSchema, config);
+
+
+ TStringStream binaryOut;
+ {
+ TString binaryString = HexDecode(hexString);
+ TMemoryInput in(binaryString);
+ TCheckedInDebugSkiffParser parser(skiffSchema, &in);
+
+ auto writer = TCheckedInDebugYsonTokenWriter(&binaryOut);
+ converter(&parser, &writer);
+ EXPECT_EQ(parser.GetReadBytesCount(), binaryString.size());
+ }
+ binaryOut.Finish();
+
+ TStringStream out;
+ {
+ auto writer = TYsonWriter(&out, EYsonFormat::Text);
+ ParseYsonStringBuffer(binaryOut.Str(), EYsonType::Node, &writer);
+ }
+ out.Finish();
+
+ return out.Str();
+}
+
+
+#define CHECK_BIDIRECTIONAL_CONVERSION(logicalType, skiffSchema, ysonString, skiffString, ...) \
+ do { \
+ std::tuple<TYsonToSkiffConverterConfig,TSkiffToYsonConverterConfig> cfg = {__VA_ARGS__}; \
+ auto actualSkiffString = ConvertYsonHex(logicalType, skiffSchema, ysonString, std::get<0>(cfg)); \
+ EXPECT_EQ(actualSkiffString, skiffString) << "Yson -> Skiff conversion error"; \
+ auto actualYsonString = ConvertHexToTextYson(logicalType, skiffSchema, skiffString, std::get<1>(cfg)); \
+ EXPECT_EQ(actualYsonString, ysonString) << "Skiff -> Yson conversion error"; \
+ } while (0)
+
+
+TEST(TYsonSkiffConverterTest, TestSimpleTypes)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Int8(),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ "-42",
+ "d6ffffff" "ffffffff");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Uint64(),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ "42u",
+ "2a000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Uint64(),
+ CreateSimpleTypeSchema(EWireType::Uint64),
+ "8u",
+ "08000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Bool(),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ "%true",
+ "01");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Double(),
+ CreateSimpleTypeSchema(EWireType::Double),
+ "0.",
+ "00000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Float(),
+ CreateSimpleTypeSchema(EWireType::Double),
+ "0.",
+ "00000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ String(),
+ CreateSimpleTypeSchema(EWireType::String32),
+ "\"foo\"",
+ "03000000" "666f6f");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Null(),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ "#",
+ "");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Uuid(),
+ CreateSimpleTypeSchema(EWireType::Uint128),
+ "\"\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\"",
+ "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Uuid(),
+ CreateSimpleTypeSchema(EWireType::String32),
+ "\"\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\"",
+ "10000000f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
+}
+
+TEST(TYsonSkiffConverterTest, TestYson32)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Yson(),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ "-42",
+ "02000000" "0253");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Yson(),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ "#",
+ "01000000" "23");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Yson(),
+ CreateSimpleTypeSchema(EWireType::Yson32),
+ "[1;2;[3;];]",
+ "0e000000" "5b02023b02043b5b02063b5d3b5d");
+}
+
+TEST(TYsonSkiffConverterTest, TestOptionalTypes)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Int64()),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ "-42",
+ "01" "d6ffffff" "ffffffff");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Int64()),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ "#",
+ "00");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(Bool())),
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean))),
+ "[%true;]",
+ "01" "01" "01");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(Bool())),
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean))),
+ "[#;]",
+ "01" "00");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(Bool())),
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean))),
+ "#",
+ "00");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(List(Bool())),
+ SkiffOptional(CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Boolean)})),
+ "#",
+ "00");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(List(Bool()))),
+ SkiffOptional(
+ SkiffOptional(
+ CreateRepeatedVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Boolean)
+ })
+ )
+ ),
+ "[[%true;%false;%true;];]",
+ "01" "01" "0001" "0000" "0001" "ff");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(List(Bool()))),
+ SkiffOptional(
+ SkiffOptional(
+ CreateRepeatedVariant8Schema({
+ CreateSimpleTypeSchema(EWireType::Boolean)
+ })
+ )
+ ),
+ "[#;]",
+ "0100");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertYsonHex(
+ Optional(Optional(Bool())),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean)),
+ " [ %true ] "),
+ "Optional nesting mismatch");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertHexToTextYson(
+ Optional(Bool()),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ "00"),
+ "Optional nesting mismatch");
+
+ TYsonToSkiffConverterConfig ysonToSkiffConfig;
+ ysonToSkiffConfig.AllowOmitTopLevelOptional = true;
+
+ TSkiffToYsonConverterConfig skiffToYsonConfig;
+ skiffToYsonConfig.AllowOmitTopLevelOptional = true;
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(Bool())),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean)),
+ "[%true;]",
+ "01" "01",
+ ysonToSkiffConfig,
+ skiffToYsonConfig);
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Optional(Optional(Bool())),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean)),
+ "[#;]",
+ "00",
+ ysonToSkiffConfig,
+ skiffToYsonConfig);
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertYsonHex(
+ Optional(Optional(Bool())),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Boolean)),
+ " # ",
+ ysonToSkiffConfig),
+ "value expected to be nonempty");
+}
+
+TEST(TYsonSkiffConverterTest, TestListTypes)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ List(Bool()),
+ CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Boolean)}),
+ "[]",
+ "ff");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ List(Bool()),
+ CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Boolean)}),
+ "[%true;%true;%true;]",
+ "00" "01" "00" "01" "00" "01" "ff");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ List(List(Bool())),
+ CreateRepeatedVariant8Schema({CreateRepeatedVariant8Schema({CreateSimpleTypeSchema(EWireType::Boolean)})}),
+ "[[];[%true;];[%true;%true;];]",
+ "00" "ff" "00" "0001ff" "00" "00010001ff" "ff");
+}
+
+TEST(TYsonSkiffConverterTest, TestStruct)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Struct(
+ "key", String(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ }),
+ "[\"true\";%true;]",
+ "04000000" "74727565" "01");
+}
+
+TEST(TYsonSkiffConverterTest, TestSkippedFields)
+{
+ TString skiffString;
+ skiffString = ConvertYsonHex(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "04000000" "74727565" "01"sv);
+
+ skiffString = ConvertYsonHex(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("subkey"),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "01000000" "00000000"sv);
+
+ try {
+ ConvertHexToTextYson(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("subkey"),
+ }),
+ "01000000" "00000000");
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::ContainsRegex("Non optional struct field .* is missing"));
+ }
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Struct(
+ "key", Optional(String()),
+ "subkey", Int64(),
+ "value", Optional(Bool())
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64)->SetName("subkey"),
+ }),
+ "[#;15;#;]",
+ "0f000000" "00000000");
+}
+
+TEST(TYsonSkiffConverterTest, TestUnknownSkiffFields)
+{
+ TString skiffString;
+ skiffString = ConvertYsonHex(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::String32))->SetName("key2"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "04000000" "74727565" "00" "01"sv);
+
+ skiffString = ConvertYsonHex(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Yson32))->SetName("value2"),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "04000000" "74727565" "01" "00"sv);
+
+
+ try {
+ ConvertYsonHex(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ CreateSimpleTypeSchema(EWireType::Yson32)->SetName("value2"),
+ }),
+ " [ true ; 1; %true ] ");
+ GTEST_FAIL() << "exception expected";
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::ContainsRegex("Non optional Skiff field .* is missing corresponding logical struct field"));
+ }
+
+ try {
+ ConvertHexToTextYson(
+ Struct(
+ "key", String(),
+ "subkey", Int64(),
+ "value", Bool()
+ ),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32)->SetName("key"),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::String32))->SetName("key2"),
+ CreateSimpleTypeSchema(EWireType::Boolean)->SetName("value"),
+ }),
+ "04000000" "74727565" "00" "01"sv);
+ GTEST_FAIL() << "expected_exception";
+ } catch (const std::exception& e) {
+ EXPECT_THAT(e.what(), testing::ContainsRegex("is not found in logical type"));
+ }
+}
+
+TEST(TYsonSkiffConverterTest, TestTuple)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Tuple(String(), Bool()),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ }),
+ "[\"true\";%true;]",
+ "04000000" "74727565" "01");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ Tuple(Int64(), Optional(Int64())),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ }),
+ "[2;42;]",
+ "02000000" "00000000" "01" "2a000000" "00000000");
+}
+
+TEST(TYsonSkiffConverterTest, TestTupleSkippedFields)
+{
+ TString skiffString;
+ skiffString = ConvertYsonHex(
+ Tuple(String(), Int64(), Bool()),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "04000000" "74727565" "01"sv);
+
+ skiffString = ConvertYsonHex(
+ Tuple(String(), Int64(), Bool()),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ }),
+ " [ true ; 1; %true ] ");
+ EXPECT_EQ(skiffString, "01000000" "00000000"sv);
+
+ skiffString = ConvertYsonHex(
+ Tuple(Optional(String()), Int64(), Optional(Bool())),
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::Nothing),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Nothing)
+ }),
+ "[#;15;#;]"
+ );
+ EXPECT_EQ(skiffString, "0f000000" "00000000"sv);
+}
+
+TEST(TYsonSkiffConverterTest, TestDict)
+{
+ const auto logicalType = Dict(String(), Int64());
+ const auto skiffSchema = CreateRepeatedVariant8Schema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32),
+ CreateSimpleTypeSchema(EWireType::Int64)
+ })
+ });
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ skiffSchema,
+ "[[\"one\";1;];[\"two\";2;];]",
+ "00" "03000000" "6f6e65" "01000000" "00000000"
+ "00" "03000000" "74776f" "02000000" "00000000"
+ "ff"
+ );
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertHexToTextYson(logicalType, skiffSchema, "01" "01000000" "6f" "01000000" "00000000" "ff"),
+ "Unexpected repeated_variant8 tag"
+ );
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertHexToTextYson(logicalType, skiffSchema, "00" "01000000" "6f" "01000000" "00000000"),
+ "Premature end of stream"
+ );
+}
+
+TEST(TYsonSkiffConverterTest, TestTagged)
+{
+ const auto logicalType = Tagged(
+ "tag",
+ Dict(Tagged("tag", String()), Int64()));
+ const auto skiffSchema = CreateRepeatedVariant8Schema({
+ CreateTupleSchema({
+ CreateSimpleTypeSchema(EWireType::String32),
+ CreateSimpleTypeSchema(EWireType::Int64)
+ })
+ });
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ skiffSchema,
+ "[[\"one\";1;];[\"two\";2;];]",
+ "00" "03000000" "6f6e65" "01000000" "00000000"
+ "00" "03000000" "74776f" "02000000" "00000000"
+ "ff"
+ );
+}
+
+TEST(TYsonSkiffConverterTest, TestOptionalVariantSimilarity)
+{
+ auto logicalType = Optional(
+ VariantTuple(Null(), Int64())
+ );
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64))),
+ "[1;42;]",
+ "01" "01" "2a000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64))),
+ "[0;#;]",
+ "01" "00");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ SkiffOptional(SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64))),
+ "#",
+ "00");
+
+ TYsonToSkiffConverterConfig ysonToSkiffConfig;
+ ysonToSkiffConfig.AllowOmitTopLevelOptional = true;
+
+ TSkiffToYsonConverterConfig skiffToYsonConfig;
+ skiffToYsonConfig.AllowOmitTopLevelOptional = true;
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ "[1;42;]",
+ "01" "2a000000" "00000000",
+ ysonToSkiffConfig,
+ skiffToYsonConfig);
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ logicalType,
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ "[0;#;]",
+ "00",
+ ysonToSkiffConfig,
+ skiffToYsonConfig);
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertYsonHex(
+ logicalType,
+ SkiffOptional(CreateSimpleTypeSchema(EWireType::Int64)),
+ "#",
+ ysonToSkiffConfig),
+ "value expected to be nonempty"
+ );
+}
+
+class TYsonSkiffConverterTestVariant
+ : public ::testing::TestWithParam<std::tuple<ELogicalMetatype, EWireType>>
+{
+public:
+ TLogicalTypePtr VariantLogicalType(const std::vector<TLogicalTypePtr>& elements)
+ {
+ auto [metatype, wireType] = GetParam();
+ if (metatype == ELogicalMetatype::VariantTuple) {
+ return VariantTupleLogicalType(elements);
+ } else {
+ std::vector<TStructField> fields;
+ for (size_t i = 0; i < elements.size(); ++i) {
+ fields.push_back({Format("field%v", i), elements[i]});
+ }
+ return VariantStructLogicalType(fields);
+ }
+ }
+
+ std::shared_ptr<TSkiffSchema> VariantSkiffSchema(std::vector<std::shared_ptr<TSkiffSchema>> elements)
+ {
+ for (size_t i = 0; i < elements.size(); ++i) {
+ elements[i]->SetName(Format("field%v", i));
+ }
+ auto [metatype, wireType] = GetParam();
+ if (wireType == EWireType::Variant8) {
+ return CreateVariant8Schema(std::move(elements));
+ } else if (wireType == EWireType::Variant16) {
+ return CreateVariant16Schema(std::move(elements));
+ }
+ Y_UNREACHABLE();
+ }
+
+ TString VariantTagInfix() const
+ {
+ auto [metatype, wireType] = GetParam();
+ if (wireType == EWireType::Variant16) {
+ return "00";
+ }
+ return {};
+ }
+};
+
+TEST_P(TYsonSkiffConverterTestVariant, TestVariant)
+{
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ VariantLogicalType({
+ Int64(),
+ Bool()
+ }),
+ VariantSkiffSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ }),
+ "[0;42;]",
+ "00" + VariantTagInfix() + "2a000000" "00000000");
+
+ CHECK_BIDIRECTIONAL_CONVERSION(
+ VariantLogicalType({
+ Int64(),
+ Bool()
+ }),
+ VariantSkiffSchema({
+ CreateSimpleTypeSchema(EWireType::Int64),
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ }),
+ "[1;%true;]",
+ "01" + VariantTagInfix() + "01");
+}
+
+TEST_P(TYsonSkiffConverterTestVariant, TestMalformedVariants)
+{
+ auto logicalType = VariantLogicalType({
+ Bool(),
+ Int64(),
+ });
+ auto skiffSchema = VariantSkiffSchema({
+ CreateSimpleTypeSchema(EWireType::Boolean),
+ CreateSimpleTypeSchema(EWireType::Int64),
+ });
+
+ EXPECT_THROW_WITH_SUBSTRING(ConvertYsonHex(logicalType, skiffSchema, "[2; 42]"), "Yson to Skiff conversion error");
+ EXPECT_THROW_WITH_SUBSTRING(ConvertYsonHex(logicalType, skiffSchema, "[]"), "Yson to Skiff conversion error");
+ EXPECT_THROW_WITH_SUBSTRING(ConvertYsonHex(logicalType, skiffSchema, "[0]"), "Yson to Skiff conversion error");
+
+ EXPECT_THROW_WITH_SUBSTRING(ConvertHexToTextYson(logicalType, skiffSchema, "02" + VariantTagInfix() + "00"),
+ "Skiff to Yson conversion error");
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Variants,
+ TYsonSkiffConverterTestVariant,
+ ::testing::Combine(
+ ::testing::ValuesIn({ELogicalMetatype::VariantStruct, ELogicalMetatype::VariantTuple}),
+ ::testing::ValuesIn({EWireType::Variant8, EWireType::Variant16})
+ )
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/table_consumer_ut.cpp b/yt/yt/client/unittests/table_consumer_ut.cpp
new file mode 100644
index 0000000000..4a345861d9
--- /dev/null
+++ b/yt/yt/client/unittests/table_consumer_ut.cpp
@@ -0,0 +1,131 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/unittests/mock/table_value_consumer.h>
+
+#include <yt/yt/client/table_client/schema.h>
+#include <yt/yt/client/table_client/table_consumer.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/yson/parser.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+using namespace NFormats;
+using namespace NComplexTypes;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEmptyValueConsumer
+ : public IValueConsumer
+{
+ const TNameTablePtr& GetNameTable() const override
+ {
+ return NameTable;
+ }
+
+ const TTableSchemaPtr& GetSchema() const override
+ {
+ return Schema_;
+ }
+
+ bool GetAllowUnknownColumns() const override
+ {
+ return true;
+ }
+
+ void OnBeginRow() override
+ { }
+
+ void OnValue(const TUnversionedValue& /*value*/) override
+ { }
+
+ void OnEndRow() override
+ { }
+
+private:
+ const TTableSchemaPtr Schema_ = New<TTableSchema>();
+ const TNameTablePtr NameTable = New<TNameTable>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTableConsumer, EntityAsNull)
+{
+ StrictMock<TMockValueConsumer> mock(New<TNameTable>(), true);
+ EXPECT_CALL(mock, OnBeginRow());
+ EXPECT_CALL(mock, OnMyValue(MakeUnversionedSentinelValue(EValueType::Null, 0)));
+ EXPECT_CALL(mock, OnEndRow());
+
+ TYsonConverterConfig config{
+ .ComplexTypeMode = EComplexTypeMode::Positional,
+ };
+ std::unique_ptr<IYsonConsumer> consumer(new TTableConsumer(config, &mock));
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("a");
+ consumer->OnEntity();
+ consumer->OnEndMap();
+}
+
+TEST(TTableConsumer, TopLevelAttributes)
+{
+ StrictMock<TMockValueConsumer> mock(New<TNameTable>(), true);
+ EXPECT_CALL(mock, OnBeginRow());
+
+ TYsonConverterConfig config{
+ .ComplexTypeMode = EComplexTypeMode::Positional,
+ };
+ std::unique_ptr<IYsonConsumer> consumer(new TTableConsumer(config, &mock));
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("a");
+ EXPECT_THROW(consumer->OnBeginAttributes(), std::exception);
+}
+
+TEST(TTableConsumer, RowAttributes)
+{
+ StrictMock<TMockValueConsumer> mock(New<TNameTable>(), true);
+
+ TYsonConverterConfig config{
+ .ComplexTypeMode = EComplexTypeMode::Positional,
+ };
+ std::unique_ptr<IYsonConsumer> consumer(new TTableConsumer(config, &mock));
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("table_index");
+ consumer->OnInt64Scalar(0);
+ consumer->OnEndAttributes();
+ EXPECT_THROW(consumer->OnBeginMap(), std::exception);
+}
+
+TEST(TYsonParserTest, ContextInExceptions_TableConsumer)
+{
+ try {
+ TEmptyValueConsumer emptyValueConsumer;
+
+ TYsonConverterConfig config{
+ .ComplexTypeMode = EComplexTypeMode::Positional,
+ };
+ TTableConsumer consumer(config, &emptyValueConsumer);
+ TYsonParser parser(&consumer, EYsonType::ListFragment);
+ parser.Read("{foo=bar};");
+ parser.Read("{bar=baz};YT_LOG_IN");
+ parser.Read("FO something happened");
+ parser.Finish();
+ GTEST_FAIL() << "Expected exception to be thrown";
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("YT_LOG_INFO something happened"));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NVersionedTableClient
+
diff --git a/yt/yt/client/unittests/time_text_ut.cpp b/yt/yt/client/unittests/time_text_ut.cpp
new file mode 100644
index 0000000000..a8b5d1a10a
--- /dev/null
+++ b/yt/yt/client/unittests/time_text_ut.cpp
@@ -0,0 +1,67 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/complex_types/time_text.h>
+
+namespace NYT::NComplexTypes {
+
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TimeConverter, Correct)
+{
+ EXPECT_EQ(BinaryTimeFromText("2021-09-03", ESimpleLogicalValueType::Date), 18873U);
+
+ EXPECT_EQ(BinaryTimeFromText("2021-09-03T10:12:41Z", ESimpleLogicalValueType::Datetime), 1630663961U);
+
+ EXPECT_EQ(BinaryTimeFromText("2021-09-03T10:12:41Z", ESimpleLogicalValueType::Timestamp), 1630663961000000ULL);
+ EXPECT_EQ(BinaryTimeFromText("2021-09-03T10:12:41.1Z", ESimpleLogicalValueType::Timestamp), 1630663961100000ULL);
+ EXPECT_EQ(BinaryTimeFromText("2021-09-03T10:12:41.123456Z", ESimpleLogicalValueType::Timestamp), 1630663961123456ULL);
+}
+
+TEST(TimeConverter, InvalidLength)
+{
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09", ESimpleLogicalValueType::Date),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T", ESimpleLogicalValueType::Date),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T10:12:41Z", ESimpleLogicalValueType::Date),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T10:12:41.1Z", ESimpleLogicalValueType::Date),
+ "Invalid date string length");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03", ESimpleLogicalValueType::Datetime),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T10:12:41", ESimpleLogicalValueType::Datetime),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T10:12:41.1Z", ESimpleLogicalValueType::Datetime),
+ "Invalid date string length");
+
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03", ESimpleLogicalValueType::Timestamp),
+ "Invalid date string length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T10:12:41.1234567Z", ESimpleLogicalValueType::Timestamp),
+ "Invalid date string length");
+}
+
+TEST(TimeConverter, InvalidFormat)
+{
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-3T", ESimpleLogicalValueType::Date),
+ "Could not parse date");
+ EXPECT_THROW_WITH_SUBSTRING(
+ BinaryTimeFromText("2021-09-03T23:59:59H", ESimpleLogicalValueType::Datetime),
+ "Could not parse date");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/unittests/unordered_reader_ut.cpp b/yt/yt/client/unittests/unordered_reader_ut.cpp
new file mode 100644
index 0000000000..43a5e74933
--- /dev/null
+++ b/yt/yt/client/unittests/unordered_reader_ut.cpp
@@ -0,0 +1,104 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/unordered_schemaful_reader.h>
+#include <yt/yt/client/table_client/unversioned_reader.h>
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/row_batch.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+using namespace NTableClient;
+using namespace NChunkClient;
+using namespace NChunkClient::NProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnorderedReaderTest
+ : public ::testing::Test
+{ };
+
+class TSchemafulReaderMock
+ : public ISchemafulUnversionedReader
+{
+public:
+ IUnversionedRowBatchPtr Read(const TRowBatchReadOptions& /*options*/ = {}) override
+ {
+ return ReadyEvent_.IsSet() ? nullptr : CreateEmptyUnversionedRowBatch();
+ }
+
+ TFuture<void> GetReadyEvent() const override
+ {
+ return ReadyEvent_;
+ }
+
+ void SetReadyEvent(const TError& error)
+ {
+ ReadyEvent_.Set(error);
+ }
+
+ TDataStatistics GetDataStatistics() const override
+ {
+ return {};
+ }
+
+ NChunkClient::TCodecStatistics GetDecompressionStatistics() const override
+ {
+ return {};
+ }
+
+ bool IsFetchingCompleted() const override
+ {
+ return false;
+ }
+
+ std::vector<TChunkId> GetFailedChunkIds() const override
+ {
+ return {};
+ }
+
+private:
+ const TPromise<void> ReadyEvent_ = NewPromise<void>();
+};
+
+TEST_F(TUnorderedReaderTest, Simple)
+{
+ auto reader1 = New<TSchemafulReaderMock>();
+ auto reader2 = New<TSchemafulReaderMock>();
+
+ auto subqueryReaderCreator = [&, index = 0] () mutable -> ISchemafulUnversionedReaderPtr {
+ if (index == 0) {
+ ++index;
+ return reader1;
+ } else if (index == 1) {
+ ++index;
+ return reader2;
+ } else {
+ return nullptr;
+ }
+ };
+
+ auto mergingReader = CreateUnorderedSchemafulReader(subqueryReaderCreator, 2);
+
+ EXPECT_TRUE(mergingReader->Read().operator bool());
+
+ reader1->SetReadyEvent(TError());
+ reader2->SetReadyEvent(TError("Error"));
+
+ EXPECT_TRUE(mergingReader->GetReadyEvent().IsSet());
+ EXPECT_TRUE(mergingReader->GetReadyEvent().Get().IsOK());
+
+ EXPECT_TRUE(mergingReader->Read().operator bool());
+ EXPECT_TRUE(mergingReader->GetReadyEvent().IsSet());
+ EXPECT_EQ("Error", mergingReader->GetReadyEvent().Get().GetMessage());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/client/unittests/unversioned_row_ut.cpp b/yt/yt/client/unittests/unversioned_row_ut.cpp
new file mode 100644
index 0000000000..e20e3c0d1a
--- /dev/null
+++ b/yt/yt/client/unittests/unversioned_row_ut.cpp
@@ -0,0 +1,250 @@
+#include <yt/yt/client/table_client/helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+
+#include <util/stream/str.h>
+
+namespace NYT::NTableClient {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TUnversionedValue, TestConversionToYsonTokenWriter)
+{
+ auto convert = [] (TUnversionedValue value) {
+ TStringStream stream;
+ TCheckedInDebugYsonTokenWriter tokenWriter(&stream);
+ UnversionedValueToYson(value, &tokenWriter);
+ tokenWriter.Finish();
+ return stream.Str();
+ };
+
+ {
+ auto value = MakeUnversionedInt64Value(-42);
+ i64 parsed = 0;
+ EXPECT_NO_THROW(parsed = ConvertTo<i64>(TYsonString(convert(value))));
+ EXPECT_EQ(parsed, -42);
+ }
+ {
+ auto value = MakeUnversionedUint64Value(std::numeric_limits<ui64>::max());
+ ui64 parsed = 0;
+ EXPECT_NO_THROW(parsed = ConvertTo<ui64>(TYsonString(convert(value))));
+ EXPECT_EQ(parsed, std::numeric_limits<ui64>::max());
+ }
+ {
+ auto value = MakeUnversionedDoubleValue(2.718);
+ double parsed = 0.0;
+ EXPECT_NO_THROW(parsed = ConvertTo<double>(TYsonString(convert(value))));
+ EXPECT_DOUBLE_EQ(parsed, 2.718);
+ }
+ {
+ auto value = MakeUnversionedStringValue("boo");
+ TString parsed;
+ EXPECT_NO_THROW(parsed = ConvertTo<TString>(TYsonString(convert(value))));
+ EXPECT_EQ(parsed, "boo");
+ }
+ {
+ auto value = MakeUnversionedNullValue();
+ TString str;
+ EXPECT_NO_THROW(str = convert(value));
+ EXPECT_EQ(str, "#");
+ }
+ {
+ auto value = MakeUnversionedAnyValue("{x=y;z=<a=b>2}");
+ INodePtr parsed;
+ EXPECT_NO_THROW(parsed = ConvertTo<INodePtr>(TYsonString(convert(value))));
+ auto expected = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("x").Value("y")
+ .Item("z")
+ .BeginAttributes()
+ .Item("a").Value("b")
+ .EndAttributes()
+ .Value(2)
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(parsed, expected))
+ << "parsed: " << ConvertToYsonString(parsed, EYsonFormat::Pretty).AsStringBuf()
+ << "\nexpected: " << ConvertToYsonString(expected, EYsonFormat::Pretty).AsStringBuf();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Some sanity tests.
+static_assert(TUnversionedValueConversionTraits<i64>::Inline, "i64 must be inline.");
+static_assert(TUnversionedValueConversionTraits<i64>::Scalar, "i64 must be scalar.");
+static_assert(TUnversionedValueConversionTraits<std::optional<i64>>::Inline, "i64? must be inline.");
+static_assert(TUnversionedValueConversionTraits<std::optional<i64>>::Scalar, "i64? must be scalar.");
+static_assert(!TUnversionedValueConversionTraits<TString>::Inline, "TString must not be inline.");
+static_assert(TUnversionedValueConversionTraits<TString>::Scalar, "TString must be scalar.");
+static_assert(TUnversionedValueConversionTraits<TAnnotatedValue<i64>>::Scalar, "i64 must be scalar.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMakeUnversionedOwningRow, Empty)
+{
+ auto row = MakeUnversionedOwningRow();
+ EXPECT_EQ(0, row.GetCount());
+}
+
+template <class T>
+void CheckSingleValue(T value)
+{
+ auto row = MakeUnversionedOwningRow(value);
+ EXPECT_EQ(1, row.GetCount());
+ EXPECT_EQ(0, row[0].Id);
+ EXPECT_EQ(value, FromUnversionedValue<T>(row[0]));
+}
+
+TEST(TMakeUnversionedOwningRow, SingleValue)
+{
+ CheckSingleValue(TGuid::Create());
+ CheckSingleValue(TString("hello"));
+ CheckSingleValue(TStringBuf("hello"));
+ CheckSingleValue(true);
+ CheckSingleValue(TYsonString(TStringBuf("{a=1}")));
+ CheckSingleValue(static_cast<i64>(-123));
+ CheckSingleValue(static_cast<ui64>(123));
+ CheckSingleValue(static_cast<i32>(-17));
+ CheckSingleValue(static_cast<ui32>(17));
+ CheckSingleValue(static_cast<i16>(-2342));
+ CheckSingleValue(static_cast<ui16>(2342));
+ CheckSingleValue(static_cast<i8>(-12));
+ CheckSingleValue(static_cast<ui8>(12));
+ CheckSingleValue(static_cast<double>(3.14));
+ CheckSingleValue(TInstant::Now());
+ CheckSingleValue(TDuration::Seconds(10));
+}
+
+TEST(TMakeUnversionedOwningRow, CharPtr)
+{
+ auto row = MakeUnversionedOwningRow("test");
+ EXPECT_EQ(1, row.GetCount());
+ EXPECT_EQ(0, row[0].Id);
+ EXPECT_EQ("test", FromUnversionedValue<TString>(row[0]));
+}
+
+TEST(TMakeUnversionedOwningRow, NullValue)
+{
+ auto row = MakeUnversionedOwningRow(std::nullopt);
+ EXPECT_EQ(1, row.GetCount());
+ EXPECT_EQ(0, row[0].Id);
+ EXPECT_EQ(EValueType::Null, row[0].Type);
+}
+
+TEST(TMakeUnversionedOwningRow, Tuple)
+{
+ auto row = MakeUnversionedOwningRow(TString("hello"), true);
+ EXPECT_EQ(2, row.GetCount());
+ EXPECT_EQ(0, row[0].Id);
+ EXPECT_EQ("hello", FromUnversionedValue<TString>(row[0]));
+ EXPECT_EQ(1, row[1].Id);
+ EXPECT_EQ(true, FromUnversionedValue<bool>(row[1]));
+}
+
+TEST(TMakeUnversionedOwningRow, FromUnversionedRow)
+{
+ auto row = MakeUnversionedOwningRow(TString("hello"), TStringBuf("world"), 123);
+ TString a;
+ TStringBuf b;
+ i16 c;
+ FromUnversionedRow(row, &a, &b, &c);
+ EXPECT_EQ("hello", a);
+ EXPECT_EQ("world", b);
+ EXPECT_EQ(123, c);
+}
+
+TEST(TMakeUnversionedOwningRow, TupleFromUnversionedRow)
+{
+ auto row = MakeUnversionedOwningRow(TString("hello"), TStringBuf("world"), 123);
+ auto [a, b, c] = FromUnversionedRow<TString, TStringBuf, i16>(row);
+ EXPECT_EQ("hello", a);
+ EXPECT_EQ("world", b);
+ EXPECT_EQ(123, c);
+}
+
+TEST(TMakeUnversionedOwningRow, ExplicitIds)
+{
+ auto row = MakeUnversionedOwningRow(
+ TAnnotatedValue{TString("hello"), 10},
+ TAnnotatedValue{TStringBuf("world"), 20});
+ EXPECT_EQ(2, row.GetCount());
+ EXPECT_EQ(10, row[0].Id);
+ EXPECT_EQ(20, row[1].Id);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TUnversionedRowsBuilder, Empty)
+{
+ TUnversionedRowsBuilder builder;
+ auto rows = builder.Build();
+ EXPECT_EQ(0, std::ssize(rows));
+}
+
+TEST(TUnversionedRowsBuilder, SomeValues)
+{
+ TUnversionedRowsBuilder builder;
+ builder.AddRow(1, "hello");
+ builder.AddRow(2, "world");
+ auto rows = builder.Build();
+ EXPECT_EQ(2, std::ssize(rows));
+ {
+ auto [i, s] = FromUnversionedRow<int, TString>(rows[0]);
+ EXPECT_EQ(1, i);
+ EXPECT_EQ("hello", s);
+ EXPECT_EQ(0, rows[0][0].Id);
+ EXPECT_EQ(1, rows[0][1].Id);
+ }
+ {
+ auto [i, s] = FromUnversionedRow<int, TString>(rows[1]);
+ EXPECT_EQ(2, i);
+ EXPECT_EQ("world", s);
+ EXPECT_EQ(0, rows[1][0].Id);
+ EXPECT_EQ(1, rows[1][1].Id);
+ }
+}
+
+TEST(TUnversionedRowsBuilder, AnnotatedValue)
+{
+ TUnversionedRowsBuilder builder;
+ builder.AddRow(TAnnotatedValue{1, 10}, TAnnotatedValue{"hello", 20});
+ builder.AddRow(TAnnotatedValue{2, 30}, TAnnotatedValue{"world", 40});
+ builder.AddRow(TAnnotatedValue{77, 1, EValueFlags::Aggregate});
+ auto rows = builder.Build();
+ EXPECT_EQ(3, std::ssize(rows));
+ {
+ auto [i, s] = FromUnversionedRow<int, TString>(rows[0]);
+ EXPECT_EQ(1, i);
+ EXPECT_EQ("hello", s);
+ EXPECT_EQ(10, rows[0][0].Id);
+ EXPECT_EQ(20, rows[0][1].Id);
+ }
+ {
+ auto [i, s] = FromUnversionedRow<int, TString>(rows[1]);
+ EXPECT_EQ(2, i);
+ EXPECT_EQ("world", s);
+ EXPECT_EQ(30, rows[1][0].Id);
+ EXPECT_EQ(40, rows[1][1].Id);
+ }
+ {
+ auto [i] = FromUnversionedRow<int>(rows[2]);
+ EXPECT_EQ(77, i);
+ EXPECT_EQ(1, rows[2][0].Id);
+ EXPECT_EQ(EValueFlags::Aggregate, rows[2][0].Flags);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/uuid_text_ut.cpp b/yt/yt/client/unittests/uuid_text_ut.cpp
new file mode 100644
index 0000000000..655904c189
--- /dev/null
+++ b/yt/yt/client/unittests/uuid_text_ut.cpp
@@ -0,0 +1,64 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/complex_types/uuid_text.h>
+
+namespace NYT::NComplexTypes {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestBidirectionalTextYqlUuidConversion(TStringBuf bytes, TStringBuf text)
+{
+ std::array<char, UuidYqlTextSize> bytesToText;
+ TextYqlUuidFromBytes(bytes, bytesToText.data());
+ EXPECT_EQ(TString(bytesToText.data(), bytesToText.size()), text);
+
+ std::array<char, UuidBinarySize> textToBytes;
+ TextYqlUuidToBytes(text, textToBytes.data());
+ EXPECT_EQ(TString(textToBytes.data(), textToBytes.size()), bytes);
+}
+
+TEST(UuidConverter, TextYql)
+{
+ TestBidirectionalTextYqlUuidConversion(
+ TStringBuf("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", UuidBinarySize),
+ "00000000-0000-0000-0000-000000000000");
+
+ TestBidirectionalTextYqlUuidConversion(
+ "\x01\x10\x20\x30\x40\x50\x60\x70\x80\x90\xa0\xb0\xc0\xd0\xe0\xf0",
+ "30201001-5040-7060-8090-a0b0c0d0e0f0");
+}
+
+TEST(UuidConverter, InvalidTextYql)
+{
+ std::array<char, UuidBinarySize> textToBytes;
+ EXPECT_THROW_WITH_SUBSTRING(
+ TextYqlUuidToBytes("00000000-0000-0000-000000-0000000000", textToBytes.data()),
+ "Unexpected character: \"0\", expected: \"-\"");
+ EXPECT_THROW_WITH_SUBSTRING(
+ TextYqlUuidToBytes("00000000-0000-0000-0000-00-0000000000", textToBytes.data()),
+ "Invalid text yql UUID length");
+ EXPECT_THROW_WITH_SUBSTRING(
+ TextYqlUuidToBytes("g0000000-0000-0000-0000-000000000000", textToBytes.data()),
+ "Could not parse hex byte");
+}
+
+TEST(UuidConverter, Guid)
+{
+ TString bytes = "\x01\x10\x20\x30\x40\x50\x60\x70\x80\x90\xa0\xb0\xc0\xd0\xe0\xf0";
+ auto guid = GuidFromBytes(bytes);
+ std::array<char, UuidBinarySize> guidToBytes;
+ GuidToBytes(guid, guidToBytes.data());
+ EXPECT_EQ(
+ bytes,
+ TStringBuf(guidToBytes.data(), guidToBytes.size()));
+
+
+ EXPECT_EQ(guid.Parts32[0], 0xc0d0e0f0U);
+ EXPECT_EQ(guid.Parts32[1], 0x8090a0b0U);
+ EXPECT_EQ(guid.Parts32[2], 0x40506070U);
+ EXPECT_EQ(guid.Parts32[3], 0x01102030U);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NComplexTypes
diff --git a/yt/yt/client/unittests/validate_logical_type_ut.cpp b/yt/yt/client/unittests/validate_logical_type_ut.cpp
new file mode 100644
index 0000000000..1bebe1c952
--- /dev/null
+++ b/yt/yt/client/unittests/validate_logical_type_ut.cpp
@@ -0,0 +1,467 @@
+#include "logical_type_shortcuts.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/validate_logical_type.h>
+
+#include <util/string/escape.h>
+
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ERROR_ATTRS(logicalType, ysonString) \
+ " at " << __FILE__ ":" << __LINE__ << '\n' \
+ << "Type: " << ToString(*logicalType) << '\n' \
+ << "YsonString: " << '"' << EscapeC(ysonString) << '"' << '\n'\
+
+#define EXPECT_BAD_TYPE(logicalTypeExpr, ysonString) \
+ do { \
+ auto logicalType = logicalTypeExpr; \
+ try { \
+ ValidateComplexLogicalType(ysonString, logicalType); \
+ ADD_FAILURE() << "Expected type failure" << ERROR_ATTRS(logicalType, ysonString); \
+ } catch (const std::exception& ex) { \
+ if (!IsSchemaViolationError(ex)) { \
+ ADD_FAILURE() << "Unexpected error" << ERROR_ATTRS(logicalType, ysonString) \
+ << "what: " << ex.what(); \
+ } \
+ } \
+ } while (0)
+
+#define EXPECT_GOOD_TYPE(logicalTypeExpr, ysonString) \
+ do { \
+ auto logicalType = logicalTypeExpr; \
+ try { \
+ ValidateComplexLogicalType(ysonString, logicalType); \
+ } catch (const std::exception& ex) { \
+ ADD_FAILURE() << "Unexpected error" << ERROR_ATTRS(logicalType, ysonString) \
+ << "what: " << ex.what(); \
+ } \
+ } while (0)
+
+bool IsSchemaViolationError(const std::exception& ex) {
+ auto errorException = dynamic_cast<const TErrorException*>(&ex);
+ if (!errorException) {
+ return false;
+ }
+ return errorException->Error().FindMatching(NYT::NTableClient::EErrorCode::SchemaViolation).has_value();
+}
+
+// TODO (ermolovd): we should use functions from NLogicalTypeShortcuts here
+
+TEST(TValidateLogicalTypeTest, TestBasicTypes)
+{
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::String), " foo ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Int64), " 42 ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Uint64), " 42u ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Double), " 3.14 ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Float), " 3.14 ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Boolean), " %false ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Null), " # ");
+
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::String), " 76 ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Int64), " %true ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Uint64), " 14 ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Double), " bar ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Float), " rab ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Boolean), " 1 ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Null), " 0 ");
+}
+
+TEST(TValidateLogicalTypeTest, TestSimpleOptionalTypes)
+{
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)), " foo ");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)), " # ");
+ EXPECT_BAD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)), " 42 ");
+
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64)), " 42u ");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64)), " # ");
+ EXPECT_BAD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Uint64)), " 42 ");
+}
+
+TEST(TValidateLogicalTypeTest, TestOptionalNull)
+{
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null)), " # ");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null)), " [#] ");
+
+ EXPECT_BAD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null)), " [] ");
+ EXPECT_BAD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null)), " [[#]] ");
+
+ EXPECT_GOOD_TYPE(OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))), " [[#]] ");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))), " [#] ");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))), " # ");
+ EXPECT_BAD_TYPE(OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))), " [[[#]]] ");
+ EXPECT_BAD_TYPE(OptionalLogicalType(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))), " [[[]]] ");
+}
+
+TEST(TValidateLogicalTypeTest, TestLogicalTypes)
+{
+#define CHECK_GOOD(type, value) \
+ do { \
+ EXPECT_GOOD_TYPE(SimpleLogicalType(type), value); \
+ } while (0)
+
+#define CHECK_BAD(type, value) \
+ do { \
+ EXPECT_BAD_TYPE(SimpleLogicalType(type), value); \
+ } while (0)
+
+ // Int8
+ CHECK_GOOD(ESimpleLogicalValueType::Int8, " 127 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Int8, " -128 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int8, " 128 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int8, " -129 ");
+
+ // Uint8
+ CHECK_GOOD(ESimpleLogicalValueType::Uint8, " 127u ");
+ CHECK_GOOD(ESimpleLogicalValueType::Uint8, " 128u ");
+ CHECK_GOOD(ESimpleLogicalValueType::Uint8, " 255u ");
+ CHECK_BAD(ESimpleLogicalValueType::Uint8, " 256u ");
+ CHECK_BAD(ESimpleLogicalValueType::Uint8, " 100500u ");
+
+ // Int16
+ CHECK_GOOD(ESimpleLogicalValueType::Int16, " 32767 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Int16, " -32768 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int16, " 32768 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int16, " -32769 ");
+
+ // Uint16
+ CHECK_GOOD(ESimpleLogicalValueType::Uint16, " 32768u ");
+ CHECK_GOOD(ESimpleLogicalValueType::Uint16, " 65535u ");
+ CHECK_BAD(ESimpleLogicalValueType::Uint16, " 65536u ");
+
+ // Int32
+ CHECK_GOOD(ESimpleLogicalValueType::Int32, " 2147483647 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Int32, " -2147483648 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int32, " 2147483648 ");
+ CHECK_BAD(ESimpleLogicalValueType::Int32, " -2147483649 ");
+
+ // Uint32
+ CHECK_GOOD(ESimpleLogicalValueType::Uint32, " 2147483648u ");
+ CHECK_GOOD(ESimpleLogicalValueType::Uint32, " 4294967295u ");
+ CHECK_BAD(ESimpleLogicalValueType::Uint32, " 4294967297u ");
+
+ // Uint64
+ CHECK_GOOD(ESimpleLogicalValueType::Int64, " 2147483648 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Int64, " -2147483649 ");
+
+ // Uint64
+ CHECK_GOOD(ESimpleLogicalValueType::Uint64, " 4294967297u ");
+
+ // Utf8
+ CHECK_GOOD(ESimpleLogicalValueType::Utf8, " foo ");
+ CHECK_GOOD(ESimpleLogicalValueType::Utf8, " \"фу\" ");
+ CHECK_BAD(ESimpleLogicalValueType::Utf8, " \"\244\" ");
+
+ // Float
+ CHECK_GOOD(ESimpleLogicalValueType::Float, " 3.14 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Float, " -3.14e35 ");
+ CHECK_GOOD(ESimpleLogicalValueType::Float, " 3.14e35 ");
+ CHECK_BAD(ESimpleLogicalValueType::Float, " 3.14e39 ");
+ CHECK_BAD(ESimpleLogicalValueType::Float, " -3.14e39 ");
+ CHECK_BAD(ESimpleLogicalValueType::Float, " blah");
+}
+
+TEST(TValidateLogicalTypeTest, TestAnyType)
+{
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), " foo ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), " 42 ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), " 15u ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), " %true ");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), " 3.14 ");
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), "#");
+
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), "[142; 53u; {foo=bar; bar=[baz]};]");
+
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any)), "#");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any)), "[{bar=<type=list>[]}; bar; [baz];]");
+
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), "<>1");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Any), "[<>1]");
+}
+
+
+TEST(TValidateLogicalTypeTest, TestJsonType)
+{
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "\"foo\"" )");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "42" )");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "true" )");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "3.14" )");
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "null" )");
+
+ // Infinities are disallowed.
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "Infinity" )");
+
+ EXPECT_GOOD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "[142, 53, {\"foo\":\"bar\", \"bar\":[\"baz\"]}]" )");
+ // Extra comma.
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "[142, 53,]" )");
+ // Wrong delimiter.
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "[142; 53]" )");
+
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Json)), "#");
+ EXPECT_GOOD_TYPE(OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Json)), R"( "[{\"bar\": []}, \"bar\", [\"baz\"]]" )");
+
+ // Non-UTF-8.
+ EXPECT_BAD_TYPE(SimpleLogicalType(ESimpleLogicalValueType::Json), R"( "\xFF" )");
+}
+
+TEST(TValidateLogicalTypeTest, TestDecimalType)
+{
+ using namespace NLogicalTypeShortcuts;
+
+ EXPECT_GOOD_TYPE(Decimal(3, 2), R"("\x80\x00\x01\x3a")"); // 314
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"("\x80\x00\x11\x3a")");
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"("\x80\x11\x3a")");
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"("")");
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"(3.14)");
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"(3)");
+ EXPECT_BAD_TYPE(Decimal(3, 2), R"(#)");
+
+ EXPECT_GOOD_TYPE(Decimal(15, 0), R"("\x80\x03\x8d\x7e\xa4\xc6\x7f\xff")"); // 10 ** 15
+ EXPECT_BAD_TYPE(Decimal(15, 0), R"("\x80\x03\x8d\x7e\xa4\xc6\x80\x00")");
+ EXPECT_BAD_TYPE(Decimal(15, 0), R"("\x80\x00\x01\x3a")");
+ EXPECT_BAD_TYPE(Decimal(15, 0), R"("")");
+
+ EXPECT_GOOD_TYPE(Decimal(35, 0), R"("\x80\x13\x42\x61\x72\xc7\x4d\x82\x2b\x87\x8f\xe7\xff\xff\xff\xff")"); // 10 ** 35
+ EXPECT_BAD_TYPE(Decimal(35, 0), R"("\x80\x13\x42\x61\x72\xc7\x4d\x82\x2b\x87\x8f\xe8\x00\x00\x00\x00")");
+ EXPECT_BAD_TYPE(Decimal(35, 0), R"("\x80\x03\x8d\x7e\xa4\xc6\x7f\xff")");
+ EXPECT_BAD_TYPE(Decimal(35, 0), R"("\x80\x00\x01\x3a")");
+ EXPECT_BAD_TYPE(Decimal(35, 0), R"("")");
+}
+
+TEST(TValidateLogicalTypeTest, TestOptionalCompositeType)
+{
+ const auto optionalOptionalInt = OptionalLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))
+ );
+ EXPECT_GOOD_TYPE(optionalOptionalInt, " [5] ");
+ EXPECT_GOOD_TYPE(optionalOptionalInt, " [#] ");
+ EXPECT_GOOD_TYPE(optionalOptionalInt, " # ");
+ EXPECT_BAD_TYPE(optionalOptionalInt, " 5 ");
+ EXPECT_BAD_TYPE(optionalOptionalInt, " [] ");
+ EXPECT_BAD_TYPE(optionalOptionalInt, " [5; 5] ");
+
+ const auto optionalListInt = OptionalLogicalType(
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )
+ );
+ EXPECT_GOOD_TYPE(optionalListInt, "[5]");
+ EXPECT_GOOD_TYPE(optionalListInt, "[5; 5]");
+ EXPECT_GOOD_TYPE(optionalListInt, "[]");
+ EXPECT_BAD_TYPE(optionalListInt, "[[5]]");
+
+ const auto optionalOptionalListInt = OptionalLogicalType(
+ OptionalLogicalType(
+ ListLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )
+ )
+ );
+ EXPECT_GOOD_TYPE(optionalOptionalListInt, "[[5]]");
+ EXPECT_GOOD_TYPE(optionalOptionalListInt, "[[5; 5]]");
+ EXPECT_GOOD_TYPE(optionalOptionalListInt, "[[]]");
+ EXPECT_BAD_TYPE(optionalOptionalListInt, "[[[5]]]");
+ EXPECT_BAD_TYPE(optionalOptionalListInt, "[5]");
+
+ const auto optionalOptionalOptionalAny = OptionalLogicalType(
+ OptionalLogicalType(
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Any))
+ )
+ );
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " [[5]] ");
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " [[#]] ");
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " [#] ");
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " # ");
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " [[[]]] ");
+ EXPECT_GOOD_TYPE(optionalOptionalOptionalAny, " [[{foo=bar}]] ");
+ EXPECT_BAD_TYPE(optionalOptionalOptionalAny, " [] ");
+ EXPECT_BAD_TYPE(optionalOptionalOptionalAny, " [[]] ");
+}
+
+TEST(TValidateLogicalTypeTest, TestListType)
+{
+ const auto listInt = ListLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64));
+ EXPECT_GOOD_TYPE(listInt, " [3] ");
+ EXPECT_GOOD_TYPE(listInt, " [5] ");
+ EXPECT_GOOD_TYPE(listInt, " [5;42;] ");
+
+ EXPECT_BAD_TYPE(listInt, " [5;#;] ");
+ EXPECT_BAD_TYPE(listInt, " {} ");
+}
+
+TEST(TValidateLogicalTypeTest, TestStructType)
+{
+ const auto struct1 = StructLogicalType({
+ {"number", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"english", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"russian", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ });
+
+ EXPECT_GOOD_TYPE(struct1, " [3; three; TRI ] ");
+ EXPECT_GOOD_TYPE(struct1, " [1; one; # ] ");
+ EXPECT_GOOD_TYPE(struct1, " [1; one ] ");
+
+ EXPECT_BAD_TYPE(struct1, " [ # ; three; TRI ] ");
+ EXPECT_BAD_TYPE(struct1, " [ 3 ; # ; TRI ] ");
+ EXPECT_BAD_TYPE(struct1, " [ 1 ] ");
+ EXPECT_BAD_TYPE(struct1, " [ ] ");
+
+ const auto struct2 = StructLogicalType({
+ {"key", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"subkey", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ {"value", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ });
+
+ EXPECT_GOOD_TYPE(struct2, " [k ; s ; v ] ");
+ EXPECT_GOOD_TYPE(struct2, " [# ; # ; #] ");
+ EXPECT_GOOD_TYPE(struct2, " [# ; # ;] ");
+ EXPECT_GOOD_TYPE(struct2, " [# ; ] ");
+ EXPECT_GOOD_TYPE(struct2, " [ ] ");
+ EXPECT_BAD_TYPE(struct2, " [ 2 ] ");
+}
+
+TEST(TValidateLogicalTypeTest, TestTupleType)
+{
+ const auto tuple1 = TupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ });
+
+ EXPECT_GOOD_TYPE(tuple1, " [3; three; TRI ] ");
+ EXPECT_GOOD_TYPE(tuple1, " [1; one; # ] ");
+
+ EXPECT_BAD_TYPE(tuple1, " [3u; three; TRI ] ");
+ EXPECT_BAD_TYPE(tuple1, " [1; one ] ");
+ EXPECT_BAD_TYPE(tuple1, " [ # ; three; TRI ] ");
+ EXPECT_BAD_TYPE(tuple1, " [ 3 ; # ; TRI ] ");
+ EXPECT_BAD_TYPE(tuple1, " [ 1 ] ");
+ EXPECT_BAD_TYPE(tuple1, " [ ] ");
+
+ const auto tuple2 = TupleLogicalType({
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String)),
+ });
+
+ EXPECT_GOOD_TYPE(tuple2, " [k ; s ; v ] ");
+ EXPECT_GOOD_TYPE(tuple2, " [# ; # ; #] ");
+
+ EXPECT_BAD_TYPE(tuple2, " [# ; # ;] ");
+ EXPECT_BAD_TYPE(tuple2, " [# ; ] ");
+ EXPECT_BAD_TYPE(tuple2, " [ ] ");
+ EXPECT_BAD_TYPE(tuple2, " [ 2 ] ");
+}
+
+void TestVariantImpl(ELogicalMetatype metatype)
+{
+ auto createVariantType = [&] (std::vector<TStructField> fields) {
+ if (metatype == ELogicalMetatype::VariantStruct) {
+ return VariantStructLogicalType(std::move(fields));
+ } else {
+ YT_VERIFY(metatype == ELogicalMetatype::VariantTuple);
+ std::vector<TLogicalTypePtr> elements;
+ for (const auto& f : fields) {
+ elements.push_back(f.Type);
+ }
+ return VariantTupleLogicalType(std::move(elements));
+ }
+ };
+
+ auto variant1 = createVariantType({
+ {"field1", SimpleLogicalType(ESimpleLogicalValueType::Boolean)},
+ {"field2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ });
+
+ EXPECT_GOOD_TYPE(variant1, "[0; %true]");
+ EXPECT_GOOD_TYPE(variant1, "[1; \"false\"]");
+
+ EXPECT_BAD_TYPE(variant1, "[0; \"false\"]");
+ EXPECT_BAD_TYPE(variant1, "[0; %true; %true]");
+ EXPECT_BAD_TYPE(variant1, "[1; %false]");
+ EXPECT_BAD_TYPE(variant1, "[]");
+ EXPECT_BAD_TYPE(variant1, "[0]");
+ EXPECT_BAD_TYPE(variant1, "[1]");
+ EXPECT_BAD_TYPE(variant1, "[-1; %true]");
+ EXPECT_BAD_TYPE(variant1, "[-1; \"true\"]");
+ EXPECT_BAD_TYPE(variant1, "[2; \"false\"]");
+}
+
+TEST(TValidateLogicalTypeTest, TestVariantStructType)
+{
+ TestVariantImpl(ELogicalMetatype::VariantStruct);
+}
+
+TEST(TValidateLogicalTypeTest, TestVariantTupleType)
+{
+ TestVariantImpl(ELogicalMetatype::VariantTuple);
+}
+
+TEST(TValidateLogicalTypeTest, TestDictType)
+{
+ const auto stringToInt = DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ );
+
+ EXPECT_GOOD_TYPE(stringToInt, "[]");
+ EXPECT_GOOD_TYPE(stringToInt, "[[\"foo\"; 0]]");
+ EXPECT_GOOD_TYPE(stringToInt, "[[foo; 0;]]");
+
+ EXPECT_BAD_TYPE(stringToInt, " # ");
+ EXPECT_BAD_TYPE(stringToInt, " foo ");
+ EXPECT_BAD_TYPE(stringToInt, "[foo; 0]");
+ EXPECT_BAD_TYPE(stringToInt, "[[foo; 0; 0]]");
+ EXPECT_BAD_TYPE(stringToInt, "[[foo;]]");
+ EXPECT_BAD_TYPE(stringToInt, "{foo=0;}"); // <- This is not a dict!
+}
+
+TEST(TValidateLogicalTypeTest, TestTaggedType)
+{
+ const auto taggedDict = TaggedLogicalType(
+ "tag",
+ DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )
+ );
+
+ EXPECT_GOOD_TYPE(taggedDict, "[[\"foo\"; 0]]");
+ EXPECT_BAD_TYPE(taggedDict, "[[foo; 0; 0]]");
+
+ auto taggedVariant = TaggedLogicalType(
+ "tag",
+ VariantTupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Boolean),
+ SimpleLogicalType(ESimpleLogicalValueType::String),
+ })
+ );
+
+ EXPECT_GOOD_TYPE(taggedVariant, "[0; %true]");
+ EXPECT_BAD_TYPE(taggedVariant, "[0; \"false\"]");
+
+ const auto taggedStruct = TaggedLogicalType(
+ "tag",
+ StructLogicalType({
+ {"number", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"english", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"russian", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::String))},
+ })
+ );
+
+ EXPECT_GOOD_TYPE(taggedStruct, " [3; three; TRI ] ");
+ EXPECT_GOOD_TYPE(taggedStruct, " [1; one; # ] ");
+ EXPECT_GOOD_TYPE(taggedStruct, " [1; one ] ");
+
+ EXPECT_BAD_TYPE(taggedStruct, " [ # ; three; TRI ] ");
+ EXPECT_BAD_TYPE(taggedStruct, " [ 3 ; # ; TRI ] ");
+ EXPECT_BAD_TYPE(taggedStruct, " [ 1 ] ");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableChunkFormat
diff --git a/yt/yt/client/unittests/value_examples.cpp b/yt/yt/client/unittests/value_examples.cpp
new file mode 100644
index 0000000000..c2714fc856
--- /dev/null
+++ b/yt/yt/client/unittests/value_examples.cpp
@@ -0,0 +1,147 @@
+#include "value_examples.h"
+
+#include "logical_type_shortcuts.h"
+
+#include <yt/yt/library/decimal/decimal.h>
+
+#include <cmath>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NLogicalTypeShortcuts;
+using namespace NNamedValue;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TValueExample::TValueExample(TLogicalTypePtr logicalType, TNamedValue::TValue value, TString prettyYson)
+ : LogicalType(std::move(logicalType))
+ , Value(std::move(value))
+ , PrettyYson(std::move(prettyYson))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TValueExample> GetPrimitiveValueExamples()
+{
+ static const std::vector<TValueExample> valueExamples = {
+ TValueExample{Int8(), 0, "0"},
+ TValueExample{Int8(), -5, "-5"},
+ TValueExample{Int8(), 42, "42"},
+ TValueExample{Int8(), -128, "-128"},
+ TValueExample{Int8(), 127, "127"},
+
+ TValueExample{Int16(), 0, "0"},
+ TValueExample{Int16(), -6, "-6"},
+ TValueExample{Int16(), 43, "43"},
+ TValueExample{Int16(), 0x7FFF, "32767"},
+ TValueExample{Int16(), -0x8000, "-32768"},
+
+ TValueExample{Int32(), 0, "0"},
+ TValueExample{Int32(), -7, "-7"},
+ TValueExample{Int32(), 44, "44"},
+ TValueExample{Int32(), 0x7FFFFFFF, "2147483647"},
+ TValueExample{Int32(), -0x80000000ll, "-2147483648"},
+
+ TValueExample{Int64(), 0, "0"},
+ TValueExample{Int64(), -7, "-7"},
+ TValueExample{Int64(), 45, "45"},
+ TValueExample{Int64(), 0x7FFFFFFFFFFFFFFFll, "9223372036854775807"},
+ TValueExample{Int64(), i64(-0x8000000000000000ll), "-9223372036854775808"},
+
+ TValueExample{Uint8(), 0ull, "0u"},
+ TValueExample{Uint8(), 46ull, "46u"},
+ TValueExample{Uint8(), 255ull, "255u"},
+
+ TValueExample{Uint16(), 0ull, "0u"},
+ TValueExample{Uint16(), 47ull, "47u"},
+ TValueExample{Uint16(), 0xFFFFull, "65535u"},
+
+ TValueExample{Uint32(), 0ull, "0u"},
+ TValueExample{Uint32(), 48ull, "48u"},
+ TValueExample{Uint32(), 0xFFFFFFFFull, "4294967295u"},
+
+ TValueExample{Uint64(), 0ull, "0u"},
+ TValueExample{Uint64(), 49ull, "49u"},
+ TValueExample{Uint64(), 0xFFFFFFFFFFFFFFFFull, "18446744073709551615u"},
+
+ TValueExample{String(), "", R"("")"},
+ TValueExample{String(), "foo", R"("foo")"},
+ TValueExample{String(), TString(TStringBuf("\xf0\x00"sv)), R"("\xf0\x00")"},
+
+ TValueExample{Utf8(), "", R"("")"},
+ TValueExample{Utf8(), "bar", R"("bar")"},
+
+ TValueExample{Bool(), true, "%true"},
+ TValueExample{Bool(), false, "%false"},
+
+ // NB. .125 = 1 / 8 is
+ TValueExample{Double(), 3.125, "3.125"},
+ TValueExample{Double(), 2.775, "2.775"},
+ // TPrimitiveTypeExample{Double(), std::nan("1"), "%nan"},
+ TValueExample{Double(), INFINITY, "%inf"},
+ TValueExample{Double(), -INFINITY, "%-inf"},
+
+ TValueExample{Float(), 5.125, "5.125"},
+ TValueExample{Float(), 6.775, "6.775"},
+
+ TValueExample{Null(), nullptr, "#"},
+ TValueExample{Void(), nullptr, "#"},
+
+ TValueExample{Json(), "83", R"("83")"},
+ TValueExample{Json(), "[]", R"("[]")"},
+
+ TValueExample{
+ Uuid(),
+ TString(16, 0),
+ TString(TStringBuf(R"("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")"))
+ },
+ TValueExample{
+ Uuid(),
+ TString(TStringBuf("\x01\x23\x45\x67\x89\xAB\xCD\xEF\xFE\xDC\xBA\x98\x76\x54\x32\x10"sv)),
+ TString(TStringBuf(R"("\x01\x23\x45\x67\x89\xAB\xCD\xEF\xFE\xDC\xBA\x98\x76\x54\x32\x10")"))
+ },
+
+ TValueExample{Date(), 0ull, "0u"},
+ TValueExample{Date(), 18431ull, "18431u"},
+ TValueExample{Date(), 49672ull, "49672u"},
+
+ TValueExample{Datetime(), 0ull, "0u"},
+ TValueExample{Datetime(), 668800588ull, "668800588u"},
+ TValueExample{Datetime(), 4291747199ull, "4291747199u"},
+
+ TValueExample{Timestamp(), 0ull, "0u"},
+ TValueExample{Timestamp(), 2508452463052426ull, "2508452463052426u"},
+ TValueExample{Timestamp(), 4291747199999999ull, "4291747199999999u"},
+
+ TValueExample{Interval(), 0, "0"},
+ TValueExample{Timestamp(), 2208610308646589ll, "2208610308646589"},
+ TValueExample{Timestamp(), 1187314596653899ll, "1187314596653899"},
+ TValueExample{Timestamp(), 4291747199999999ll, "4291747199999999"},
+ TValueExample{Timestamp(), -4291747199999999ll, "-4291747199999999"},
+
+ TValueExample{Yson(), "qux", R"("qux")"},
+
+ TValueExample{Decimal(3, 2), NDecimal::TDecimal::TextToBinary("3.14", 3, 2), R"("\x80\x00\x01\x3a")"},
+ };
+
+ THashSet<ESimpleLogicalValueType> allValueTypes;
+ for (const auto value : TEnumTraits<ESimpleLogicalValueType>::GetDomainValues()) {
+ allValueTypes.insert(value);
+ }
+ for (const auto& example : valueExamples) {
+ if (example.LogicalType->GetMetatype() == ELogicalMetatype::Simple) {
+ allValueTypes.erase(example.LogicalType->AsSimpleTypeRef().GetElement());
+ }
+ }
+ if (!allValueTypes.empty()) {
+ THROW_ERROR_EXCEPTION("PrimitiveTypeExample variable doesn't contain values: %v",
+ allValueTypes);
+ }
+ return valueExamples;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/value_examples.h b/yt/yt/client/unittests/value_examples.h
new file mode 100644
index 0000000000..06644e2cd6
--- /dev/null
+++ b/yt/yt/client/unittests/value_examples.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <yt/yt/library/named_value/named_value.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+
+namespace NYT::NTableClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TValueExample
+{
+ TLogicalTypePtr LogicalType;
+ NNamedValue::TNamedValue::TValue Value;
+ TString PrettyYson;
+
+ TValueExample(TLogicalTypePtr logicalType, NNamedValue::TNamedValue::TValue value, TString prettyYson);
+};
+
+std::vector<TValueExample> GetPrimitiveValueExamples();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/web_json_writer_ut.cpp b/yt/yt/client/unittests/web_json_writer_ut.cpp
new file mode 100644
index 0000000000..a440002a8b
--- /dev/null
+++ b/yt/yt/client/unittests/web_json_writer_ut.cpp
@@ -0,0 +1,1570 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/formats/web_json_writer.h>
+
+#include <yt/yt/client/table_client/logical_type.h>
+#include <yt/yt/client/table_client/name_table.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/json/json_parser.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/library/named_value/named_value.h>
+
+#include <limits>
+
+namespace NYT::NFormats {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NTableClient;
+
+using NNamedValue::MakeRow;
+
+INodePtr ParseJsonToNode(TStringBuf string)
+{
+ TBuildingYsonConsumerViaTreeBuilder<INodePtr> builder(EYsonType::Node);
+ TMemoryInput stream(string);
+
+ // For plain (raw) JSON parsing we need to switch off
+ // "smart" attribute analysis and UTF-8 decoding.
+ auto config = New<NJson::TJsonFormatConfig>();
+ config->EncodeUtf8 = false;
+ config->Plain = true;
+
+ NJson::ParseJson(&stream, &builder, std::move(config));
+ return builder.Finish();
+}
+
+class TWriterForWebJson
+ : public ::testing::Test
+{
+protected:
+ TNameTablePtr NameTable_ = New<TNameTable>();
+ TWebJsonFormatConfigPtr Config_ = New<TWebJsonFormatConfig>();
+ TStringStream OutputStream_;
+ ISchemalessFormatWriterPtr Writer_;
+
+ void CreateStandardWriter(const std::vector<TTableSchemaPtr>& schemas = {New<TTableSchema>()})
+ {
+ Writer_ = CreateWriterForWebJson(
+ Config_,
+ NameTable_,
+ schemas,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&OutputStream_)));
+ }
+};
+
+TEST_F(TWriterForWebJson, Simple)
+{
+ Config_->MaxAllColumnNamesCount = 2;
+
+ CreateStandardWriter();
+
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", 100500u},
+ {"column_b", true},
+ {"column_c", "row1_c"},
+ {RowIndexColumnName, 0},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row2_c"},
+ {"column_b", "row2_b"},
+ {RowIndexColumnName, 1},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ WaitFor(Writer_->Close())
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"column_a\":{"
+ "\"$type\":\"uint64\","
+ "\"$value\":\"100500\""
+ "},"
+ "\"column_b\":{"
+ "\"$type\":\"boolean\","
+ "\"$value\":\"true\""
+ "},"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_c\""
+ "}"
+ "},"
+ "{"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row2_c\""
+ "},"
+ "\"column_b\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row2_b\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"false\","
+ "\"incomplete_all_column_names\":\"true\","
+ "\"all_column_names\":["
+ "\"column_a\","
+ "\"column_b\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, SliceColumnsByMaxCount)
+{
+ Config_->MaxSelectedColumnCount = 2;
+
+ CreateStandardWriter();
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", "row1_a"},
+ {"column_b", "row1_b"},
+ {"column_c", "row1_c"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row2_c"},
+ {"column_b", "row2_b"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row3_c"},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"column_a\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_a\""
+ "},"
+ "\"column_b\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_b\""
+ "}"
+ "},"
+ "{"
+ "\"column_b\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row2_b\""
+ "}"
+ "},"
+ "{"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"true\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"column_a\","
+ "\"column_b\","
+ "\"column_c\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, SliceStrings)
+{
+ Config_->FieldWeightLimit = 6;
+
+ CreateStandardWriter();
+
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_b", "row1_b"},
+ {"column_c", "rooooow1_c"},
+ {"column_a", "row1_a"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row2_c"},
+ {"column_b", "rooow2_b"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row3_c"},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"column_b\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_b\""
+ "},"
+ "\"column_c\":{"
+ "\"$incomplete\":true,"
+ "\"$type\":\"string\","
+ "\"$value\":\"rooooo\""
+ "},"
+ "\"column_a\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_a\""
+ "}"
+ "},"
+ "{"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row2_c\""
+ "},"
+ "\"column_b\":{"
+ "\"$incomplete\":true,"
+ "\"$type\":\"string\","
+ "\"$value\":\"rooow2\""
+ "}"
+ "},"
+ "{"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row3_c\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"false\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"column_a\","
+ "\"column_b\","
+ "\"column_c\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, ReplaceAnyWithNull)
+{
+ Config_->FieldWeightLimit = 8;
+
+ CreateStandardWriter();
+
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_b", EValueType::Any, "{key=a}"},
+ {"column_c", "row1_c"},
+ {"column_a", "row1_a"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", EValueType::Any, "{key=aaaaaa}"},
+ {"column_b", "row2_b"},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row3_c"},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ WaitFor(Writer_->Close())
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"column_b\":{"
+ "\"key\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"a\""
+ "}"
+ "},"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_c\""
+ "},"
+ "\"column_a\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row1_a\""
+ "}"
+ "},"
+ "{"
+ "\"column_c\":{"
+ "\"$incomplete\":true,"
+ "\"$type\":\"any\","
+ "\"$value\":\"\""
+ "},"
+ "\"column_b\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row2_b\""
+ "}"
+ "},"
+ "{"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"row3_c\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"false\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"column_a\","
+ "\"column_b\","
+ "\"column_c\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, SkipSystemColumns)
+{
+ Config_->SkipSystemColumns = false;
+
+ CreateStandardWriter();
+
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {TableIndexColumnName, 0},
+ {RowIndexColumnName, 1},
+ {TabletIndexColumnName, 2},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ WaitFor(Writer_->Close())
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"$$table_index\":{"
+ "\"$type\":\"int64\","
+ "\"$value\":\"0\""
+ "},"
+ "\"$$row_index\":{"
+ "\"$type\":\"int64\","
+ "\"$value\":\"1\""
+ "},"
+ "\"$$tablet_index\":{"
+ "\"$type\":\"int64\","
+ "\"$value\":\"2\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"false\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"$row_index\","
+ "\"$table_index\","
+ "\"$tablet_index\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, SkipUnregisteredColumns)
+{
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ int keyDId = -1;
+ row.AddValue(MakeUnversionedBooleanValue(true, keyDId));
+ std::vector<TUnversionedRow> rows = {row.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ keyDId = NameTable_->RegisterName("column_d");
+
+ rows.clear();
+ row.Reset();
+ row.AddValue(MakeUnversionedBooleanValue(true, keyDId));
+ rows.push_back(row.GetRow());
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close();
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "},"
+ "{"
+ "\"column_d\":{"
+ "\"$type\":\"boolean\","
+ "\"$value\":\"true\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"false\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"column_d\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+TEST_F(TWriterForWebJson, SliceColumnsByName)
+{
+ Config_->ColumnNames = {
+ "column_b",
+ "column_c",
+ "$tablet_index"};
+ Config_->MaxSelectedColumnCount = 2;
+ Config_->SkipSystemColumns = false;
+
+ CreateStandardWriter();
+
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", 100500u},
+ {"column_b", 0.42},
+ {"column_c", "abracadabra"},
+ {TabletIndexColumnName, 10},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ WaitFor(Writer_->Close())
+ .ThrowOnError();
+ auto result = ParseJsonToNode(OutputStream_.Str());
+
+ TString expectedOutput =
+ "{"
+ "\"rows\":["
+ "{"
+ "\"column_b\":{"
+ "\"$type\":\"double\","
+ "\"$value\":\"0.42\""
+ "},"
+ "\"column_c\":{"
+ "\"$type\":\"string\","
+ "\"$value\":\"abracadabra\""
+ "},"
+ "\"$$tablet_index\":{"
+ "\"$type\":\"int64\","
+ "\"$value\":\"10\""
+ "}"
+ "}"
+ "],"
+ "\"incomplete_columns\":\"true\","
+ "\"incomplete_all_column_names\":\"false\","
+ "\"all_column_names\":["
+ "\"$tablet_index\","
+ "\"column_a\","
+ "\"column_b\","
+ "\"column_c\""
+ "]"
+ "}";
+
+ EXPECT_EQ(std::ssize(expectedOutput), Writer_->GetWrittenSize());
+ EXPECT_EQ(expectedOutput, OutputStream_.Str());
+}
+
+template <typename TValue>
+void CheckYqlValue(
+ const INodePtr& valueNode,
+ const TValue& expectedValue)
+{
+ using TDecayedValue = std::decay_t<TValue>;
+ if constexpr (std::is_convertible_v<TDecayedValue, TString>) {
+ ASSERT_EQ(valueNode->GetType(), ENodeType::String);
+ EXPECT_EQ(valueNode->GetValue<TString>(), expectedValue);
+ } else if constexpr (std::is_same_v<TDecayedValue, double>) {
+ ASSERT_EQ(valueNode->GetType(), ENodeType::String);
+ EXPECT_FLOAT_EQ(FromString<double>(valueNode->GetValue<TString>()), expectedValue);
+ } else if constexpr (std::is_same_v<TDecayedValue, bool>) {
+ ASSERT_EQ(valueNode->GetType(), ENodeType::Boolean);
+ EXPECT_EQ(valueNode->GetValue<bool>(), expectedValue);
+ } else if constexpr (std::is_same_v<TDecayedValue, INodePtr>) {
+ EXPECT_TRUE(AreNodesEqual(valueNode, expectedValue))
+ << "actualValueNode is " << ConvertToYsonString(valueNode, EYsonFormat::Pretty).AsStringBuf()
+ << "\nexpectedValue is " << ConvertToYsonString(expectedValue, EYsonFormat::Pretty).AsStringBuf();
+ } else {
+ static_assert(TDependentFalse<TDecayedValue>, "Type not allowed");
+ }
+}
+
+template <typename TType>
+void CheckYqlType(
+ const INodePtr& typeNode,
+ const TType& expectedType,
+ const std::vector<INodePtr>& yqlTypes)
+{
+ ASSERT_EQ(typeNode->GetType(), ENodeType::String);
+ auto typeIndexString = typeNode->GetValue<TString>();
+ auto typeIndex = FromString<int>(typeIndexString);
+ ASSERT_LT(typeIndex, static_cast<int>(yqlTypes.size()));
+ ASSERT_GE(typeIndex, 0);
+ const auto& yqlType = yqlTypes[typeIndex];
+ EXPECT_EQ(yqlType->GetType(), ENodeType::List);
+
+ auto expectedTypeNode = [&] () -> INodePtr {
+ using TDecayedType = std::decay_t<TType>;
+ if constexpr (std::is_convertible_v<TDecayedType, TString>) {
+ return ConvertToNode(TYsonString(TString(expectedType)));
+ } else if constexpr (std::is_same_v<TDecayedType, INodePtr>) {
+ return expectedType;
+ } else {
+ static_assert(TDependentFalse<TDecayedType>, "Type not allowed");
+ }
+ }();
+ EXPECT_TRUE(AreNodesEqual(yqlType, expectedTypeNode))
+ << "yqlType is " << ConvertToYsonString(yqlType, EYsonFormat::Pretty).AsStringBuf()
+ << "\nexpectedTypeNode is " << ConvertToYsonString(expectedTypeNode, EYsonFormat::Pretty).AsStringBuf();
+}
+
+template <typename TValue, typename TType>
+void CheckYqlTypeAndValue(
+ const INodePtr& row,
+ TStringBuf name,
+ const TType& expectedType,
+ const TValue& expectedValue,
+ const std::vector<INodePtr>& yqlTypes)
+{
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto entry = row->AsMap()->FindChild(TString(name));
+ ASSERT_TRUE(entry);
+ ASSERT_EQ(entry->GetType(), ENodeType::List);
+ ASSERT_EQ(entry->AsList()->GetChildCount(), 2);
+ auto valueNode = entry->AsList()->GetChildOrThrow(0);
+ CheckYqlValue(valueNode, expectedValue);
+ auto typeNode = entry->AsList()->GetChildOrThrow(1);
+ CheckYqlType(typeNode, expectedType, yqlTypes);
+}
+
+#define CHECK_YQL_TYPE_AND_VALUE(row, name, expectedType, expectedValue, yqlTypes) \
+ do { \
+ SCOPED_TRACE(name); \
+ CheckYqlTypeAndValue(row, name, expectedType, expectedValue, yqlTypes); \
+ } while (0)
+
+TEST_F(TWriterForWebJson, YqlValueFormat_SimpleTypes)
+{
+ Config_->MaxAllColumnNamesCount = 2;
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+
+ // We will emulate writing rows from two tables.
+ CreateStandardWriter(std::vector{New<TTableSchema>(), New<TTableSchema>()});
+
+ {
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", 100500u},
+ {"column_b", true},
+ {"column_c", "row1_c"},
+ {RowIndexColumnName, 0},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_c", "row2_c"},
+ {"column_b", "row2_b"},
+ {RowIndexColumnName, 1},
+ {TableIndexColumnName, 0},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_a", -100500},
+ {"column_b", EValueType::Any, "{x=2;y=3}"},
+ {"column_c", 2.71828},
+ {RowIndexColumnName, 1},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto incompleteColumns = result->AsMap()->FindChild("incomplete_columns");
+ ASSERT_TRUE(incompleteColumns);
+ auto incompleteAllColumnNames = result->AsMap()->FindChild("incomplete_all_column_names");
+ ASSERT_TRUE(incompleteAllColumnNames);
+ auto allColumnNames = result->AsMap()->FindChild("all_column_names");
+ ASSERT_TRUE(allColumnNames);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(incompleteColumns->GetType(), ENodeType::String);
+ EXPECT_EQ(incompleteColumns->GetValue<TString>(), "false");
+
+ ASSERT_EQ(incompleteAllColumnNames->GetType(), ENodeType::String);
+ EXPECT_EQ(incompleteAllColumnNames->GetValue<TString>(), "true");
+
+ ASSERT_EQ(allColumnNames->GetType(), ENodeType::List);
+ std::vector<TString> allColumnNamesVector;
+ ASSERT_NO_THROW(allColumnNamesVector = ConvertTo<decltype(allColumnNamesVector)>(allColumnNames));
+ EXPECT_EQ(allColumnNamesVector, (std::vector<TString>{"column_a", "column_b"}));
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 3);
+
+ auto row1 = rows->AsList()->GetChildOrThrow(0);
+ auto row2 = rows->AsList()->GetChildOrThrow(1);
+ auto row3 = rows->AsList()->GetChildOrThrow(2);
+
+ ASSERT_EQ(row1->GetType(), ENodeType::Map);
+ EXPECT_EQ(row1->AsMap()->GetChildCount(), 3);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_a", R"(["DataType"; "Uint64"])", "100500", yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_b", R"(["DataType"; "Boolean"])", true, yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_c", R"(["DataType"; "String"])", "row1_c", yqlTypes);
+
+ ASSERT_EQ(row2->GetType(), ENodeType::Map);
+ EXPECT_EQ(row2->AsMap()->GetChildCount(), 2);
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_b", R"(["DataType"; "String"])", "row2_b", yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_c", R"(["DataType"; "String"])", "row2_c", yqlTypes);
+
+ ASSERT_EQ(row3->GetType(), ENodeType::Map);
+ EXPECT_EQ(row3->AsMap()->GetChildCount(), 3);
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_a", R"(["DataType"; "Int64"])", "-100500", yqlTypes);
+ auto row3BValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ val = {
+ x = {
+ "$type" = "int64";
+ "$value" = "2";
+ };
+ y = {
+ "$type" = "int64";
+ "$value" = "3";
+ }
+ }
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_b", R"(["DataType"; "Yson"])", row3BValue, yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_c", R"(["DataType"; "Double"])", 2.71828, yqlTypes);
+}
+
+TEST_F(TWriterForWebJson, ColumnNameEncoding)
+{
+ Config_->MaxAllColumnNamesCount = 2;
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+
+ CreateStandardWriter();
+
+ {
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", 100500u},
+ {"column_non_ascii_\xd0\x81", -100500},
+ }).Get()
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto incompleteColumns = result->AsMap()->FindChild("incomplete_columns");
+ ASSERT_TRUE(incompleteColumns);
+ auto incompleteAllColumnNames = result->AsMap()->FindChild("incomplete_all_column_names");
+ ASSERT_TRUE(incompleteAllColumnNames);
+ auto allColumnNames = result->AsMap()->FindChild("all_column_names");
+ ASSERT_TRUE(allColumnNames);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(allColumnNames->GetType(), ENodeType::List);
+ std::vector<TString> allColumnNamesVector;
+ ASSERT_NO_THROW(allColumnNamesVector = ConvertTo<decltype(allColumnNamesVector)>(allColumnNames));
+ EXPECT_EQ(allColumnNamesVector, (std::vector<TString>{"column_a", "column_non_ascii_\xc3\x90\xc2\x81"}));
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 1);
+
+ auto row1 = rows->AsList()->GetChildOrThrow(0);
+
+ ASSERT_EQ(row1->GetType(), ENodeType::Map);
+ EXPECT_EQ(row1->AsMap()->GetChildCount(), 2);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_a", R"(["DataType"; "Uint64"])", "100500", yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_non_ascii_\xc3\x90\xc2\x81", R"(["DataType"; "Int64"])", "-100500", yqlTypes);
+}
+
+TEST_F(TWriterForWebJson, YqlValueFormat_ComplexTypes)
+{
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+
+ auto firstSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"column_a", OptionalLogicalType(
+ ListLogicalType(MakeLogicalType(ESimpleLogicalValueType::Int64, true)))},
+ {"column_b", StructLogicalType({
+ {"key", MakeLogicalType(ESimpleLogicalValueType::String, true)},
+ {"value", MakeLogicalType(ESimpleLogicalValueType::String, true)},
+ {"variant_tuple", VariantTupleLogicalType({
+ MakeLogicalType(ESimpleLogicalValueType::Int8, true),
+ MakeLogicalType(ESimpleLogicalValueType::Boolean, false),
+ })},
+ {"variant_struct", VariantStructLogicalType({
+ {"a", MakeLogicalType(ESimpleLogicalValueType::Int8, true)},
+ {"b", MakeLogicalType(ESimpleLogicalValueType::Boolean, false)},
+ })},
+ {"dict", DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String)
+ )},
+ {"tagged", TaggedLogicalType(
+ "MyTag",
+ SimpleLogicalType(ESimpleLogicalValueType::Int64)
+ )},
+ {"timestamp", SimpleLogicalType(ESimpleLogicalValueType::Timestamp)},
+ {"date", SimpleLogicalType(ESimpleLogicalValueType::Date)},
+ {"datetime", SimpleLogicalType(ESimpleLogicalValueType::Datetime)},
+ {"interval", SimpleLogicalType(ESimpleLogicalValueType::Interval)},
+ {"json", SimpleLogicalType(ESimpleLogicalValueType::Json)},
+ {"float", SimpleLogicalType(ESimpleLogicalValueType::Float)},
+ })},
+ {"column_c", ListLogicalType(StructLogicalType({
+ {"very_optional_key", OptionalLogicalType(MakeLogicalType(ESimpleLogicalValueType::String, false))},
+ {"optional_value", MakeLogicalType(ESimpleLogicalValueType::String, false)},
+ }))},
+ });
+
+ auto secondSchema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"column_a", VariantTupleLogicalType({
+ SimpleLogicalType(ESimpleLogicalValueType::Null),
+ SimpleLogicalType(ESimpleLogicalValueType::Any),
+ })},
+ {"column_b", SimpleLogicalType(ESimpleLogicalValueType::Null)},
+ {"column_c", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Null))},
+ {"column_d", OptionalLogicalType(SimpleLogicalType(ESimpleLogicalValueType::Int64))},
+ });
+
+ auto firstColumnAType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "OptionalType";
+ [
+ "ListType";
+ ["DataType"; "Int64"]
+ ]
+ ])")));
+ auto firstColumnBType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "StructType";
+ [
+ [
+ "key";
+ ["DataType"; "String"]
+ ];
+ [
+ "value";
+ ["DataType"; "String"]
+ ];
+ [
+ "variant_tuple";
+ [
+ "VariantType";
+ [
+ "TupleType";
+ [
+ ["DataType"; "Int8"];
+ [
+ "OptionalType";
+ ["DataType"; "Boolean"]
+ ]
+ ]
+ ]
+ ]
+ ];
+ [
+ "variant_struct";
+ [
+ "VariantType";
+ [
+ "StructType";
+ [
+ [
+ "a";
+ ["DataType"; "Int8"]
+ ];
+ [
+ "b";
+ [
+ "OptionalType";
+ ["DataType"; "Boolean"]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+ [
+ "dict";
+ [
+ "DictType";
+ ["DataType"; "Int64"];
+ ["DataType"; "String"]
+ ]
+ ];
+ [
+ "tagged";
+ [
+ "TaggedType";
+ "MyTag";
+ ["DataType"; "Int64"]
+ ]
+ ];
+ [
+ "timestamp";
+ ["DataType"; "Timestamp"]
+ ];
+ [
+ "date";
+ ["DataType"; "Date"]
+ ];
+ [
+ "datetime";
+ ["DataType"; "Datetime"]
+ ];
+ [
+ "interval";
+ ["DataType"; "Interval"]
+ ];
+ [
+ "json";
+ ["DataType"; "Json"]
+ ];
+ [
+ "float";
+ ["DataType"; "Float"]
+ ];
+ ]
+ ])")));
+ auto firstColumnCType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "ListType";
+ [
+ "StructType";
+ [
+ [
+ "very_optional_key";
+ [
+ "OptionalType";
+ [
+ "OptionalType";
+ ["DataType"; "String"]
+ ]
+ ]
+ ];
+ [
+ "optional_value";
+ [
+ "OptionalType";
+ ["DataType"; "String"]
+ ]
+ ]
+ ]
+ ]
+ ])")));
+ auto secondColumnAType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "VariantType";
+ [
+ "TupleType";
+ [
+ ["NullType"];
+ ["DataType"; "Yson"];
+ ]
+ ]
+ ])")));
+ auto secondColumnBType = ConvertToNode(TYsonString(TStringBuf(R"(["NullType"])")));
+ auto secondColumnCType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "OptionalType";
+ [
+ "NullType";
+ ]
+ ])")));
+ auto secondColumnDType = ConvertToNode(TYsonString(TStringBuf(R"([
+ "OptionalType";
+ ["DataType"; "Int64"]
+ ])")));
+
+ CreateStandardWriter(std::vector{firstSchema, secondSchema});
+ {
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {"column_a", EValueType::Composite, R"([-1; -2; -5])"},
+ {
+ "column_b",
+ EValueType::Composite,
+ R"([
+ "key";
+ "value";
+ [0; 7];
+ [1; #];
+ [[1; "a"]; [2; "b"]];
+ 99;
+ 100u;
+ 101u;
+ 102u;
+ 103;
+ "[\"a\", {\"b\": 42}]";
+ -3.25;
+ ])",
+ },
+ {"column_c", EValueType::Composite, R"([[[#]; "value"]; [["key"]; #]])"},
+ {"column_d", -49},
+ {TableIndexColumnName, 0},
+ {RowIndexColumnName, 0},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_a", EValueType::Composite, R"([0; -2; -5; 177])"},
+ {
+ "column_b",
+ EValueType::Composite,
+ R"([
+ "key1";
+ "value1";
+ [1; %false];
+ [1; #];
+ [];
+ 199;
+ 0u;
+ 1101u;
+ 1102u;
+ 1103;
+ "null";
+ 0.0;
+ ])",
+ },
+ {"column_c", EValueType::Composite, R"([[#; #]; [["key1"]; #]])"},
+ {"column_d", 49u},
+ {RowIndexColumnName, 1},
+ }).Get(),
+ MakeRow(NameTable_, {
+ {"column_a", EValueType::Composite, "[]"},
+ {
+ "column_b",
+ EValueType::Composite,
+ R"([
+ "key2";
+ "value2";
+ [0; 127];
+ [1; %true];
+ [[0; ""]];
+ 399;
+ 30u;
+ 3101u;
+ 3202u;
+ 3103;
+ "{\"x\": false}";
+ 1e10;
+ ])"
+ },
+ {"column_c", EValueType::Composite, "[[[key]; #]]"},
+ {"column_d", "49"},
+ {RowIndexColumnName, 2},
+ }).Get(),
+
+ MakeRow(NameTable_, {
+ {"column_a", nullptr},
+ {
+ "column_b",
+ EValueType::Composite,
+ // First string is valid UTF-8, the second one should be Base64 encoded.
+ "["
+ "\"\xC3\xBF\";"
+ "\"\xFA\xFB\xFC\xFD\";"
+ R"(
+ [0; 127];
+ [1; %true];
+ [[-1; "-1"]; [0; ""]];
+ 499;
+ 40u;
+ 4101u;
+ 4202u;
+ 4103;
+ "{}";
+ -2.125;
+ ])",
+ },
+ {"column_c", EValueType::Composite, "[]"},
+ {"column_d", EValueType::Any, "{x=49}"},
+ {RowIndexColumnName, 3},
+ }).Get(),
+
+ // Here come rows from the second table.
+ MakeRow(NameTable_, {
+ {"column_a", EValueType::Composite, "[0; #]"},
+ {"column_b", nullptr},
+ {"column_c", nullptr},
+ {"column_d", -49},
+ {TableIndexColumnName, 1},
+ {RowIndexColumnName, 0},
+ }).Get(),
+
+ MakeRow(NameTable_, {
+ {"column_a", EValueType::Composite, "[1; {z=z}]"},
+ {"column_b", nullptr},
+ {"column_c", EValueType::Composite, "[#]"},
+ {"column_d", nullptr},
+ {TableIndexColumnName, 1},
+ {RowIndexColumnName, 1},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto incompleteColumns = result->AsMap()->FindChild("incomplete_columns");
+ ASSERT_TRUE(incompleteColumns);
+ auto incompleteAllColumnNames = result->AsMap()->FindChild("incomplete_all_column_names");
+ ASSERT_TRUE(incompleteAllColumnNames);
+ auto allColumnNames = result->AsMap()->FindChild("all_column_names");
+ ASSERT_TRUE(allColumnNames);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(incompleteColumns->GetType(), ENodeType::String);
+ EXPECT_EQ(incompleteColumns->GetValue<TString>(), "false");
+
+ ASSERT_EQ(incompleteAllColumnNames->GetType(), ENodeType::String);
+ EXPECT_EQ(incompleteAllColumnNames->GetValue<TString>(), "false");
+
+ ASSERT_EQ(allColumnNames->GetType(), ENodeType::List);
+ std::vector<TString> allColumnNamesVector;
+ ASSERT_NO_THROW(allColumnNamesVector = ConvertTo<decltype(allColumnNamesVector)>(allColumnNames));
+ EXPECT_EQ(allColumnNamesVector, (std::vector<TString>{"column_a", "column_b", "column_c", "column_d"}));
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 6);
+
+ auto row1 = rows->AsList()->GetChildOrThrow(0);
+ auto row2 = rows->AsList()->GetChildOrThrow(1);
+ auto row3 = rows->AsList()->GetChildOrThrow(2);
+ auto row4 = rows->AsList()->GetChildOrThrow(3);
+ auto row5 = rows->AsList()->GetChildOrThrow(4);
+ auto row6 = rows->AsList()->GetChildOrThrow(5);
+
+ ASSERT_EQ(row1->GetType(), ENodeType::Map);
+ EXPECT_EQ(row1->AsMap()->GetChildCount(), 4);
+ auto row1AValue = ConvertToNode(TYsonString(TStringBuf(R"([{"val"=["-1"; "-2"; "-5"]}])")));
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_a", firstColumnAType, row1AValue, yqlTypes);
+ auto row1BValue = ConvertToNode(TYsonString(TStringBuf(
+ R"([
+ "key";
+ "value";
+ ["0"; "7"];
+ ["1"; #];
+ {"val"=[["1"; "a"]; ["2"; "b"]]};
+ "99";
+ "100";
+ "101";
+ "102";
+ "103";
+ "[\"a\", {\"b\": 42}]";
+ "-3.25";
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_b", firstColumnBType, row1BValue, yqlTypes);
+ auto row1CValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ "val"=[
+ [[#]; ["value"]];
+ [[["key"]]; #]
+ ]
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_c", firstColumnCType, row1CValue, yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row1, "column_d", R"(["DataType"; "Int64"])", "-49", yqlTypes);
+
+ ASSERT_EQ(row2->GetType(), ENodeType::Map);
+ EXPECT_EQ(row2->AsMap()->GetChildCount(), 4);
+ auto row2AValue = ConvertToNode(TYsonString(TStringBuf(R"([{"val"=["0"; "-2"; "-5"; "177"]}])")));
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_a", firstColumnAType, row2AValue, yqlTypes);
+ auto row2BValue = ConvertToNode(TYsonString(TStringBuf(
+ R"([
+ "key1";
+ "value1";
+ ["1"; [%false]];
+ ["1"; #];
+ {"val"=[]};
+ "199";
+ "0";
+ "1101";
+ "1102";
+ "1103";
+ "null";
+ "0";
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_b", firstColumnBType, row2BValue, yqlTypes);
+ auto row2CValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ "val"=[
+ [#; #];
+ [[["key1"]]; #]
+ ]
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_c", firstColumnCType, row2CValue, yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row2, "column_d", R"(["DataType"; "Uint64"])", "49", yqlTypes);
+
+ ASSERT_EQ(row3->GetType(), ENodeType::Map);
+ EXPECT_EQ(row3->AsMap()->GetChildCount(), 4);
+ auto row3AValue = ConvertToNode(TYsonString(TStringBuf(R"([{"val"=[]}])")));
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_a", firstColumnAType, row3AValue, yqlTypes);
+ auto row3BValue = ConvertToNode(TYsonString(TStringBuf(
+ R"([
+ "key2";
+ "value2";
+ ["0"; "127"];
+ ["1"; [%true]];
+ {"val"=[["0"; ""]]};
+ "399";
+ "30";
+ "3101";
+ "3202";
+ "3103";
+ "{\"x\": false}";
+ "10000000000";
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_b", firstColumnBType, row3BValue, yqlTypes);
+ auto row3CValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ "val"=[
+ [[["key"]]; #]
+ ]
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_c", firstColumnCType, row3CValue, yqlTypes);
+ CHECK_YQL_TYPE_AND_VALUE(row3, "column_d", R"(["DataType"; "String"])", "49", yqlTypes);
+
+ ASSERT_EQ(row4->GetType(), ENodeType::Map);
+ EXPECT_EQ(row4->AsMap()->GetChildCount(), 4);
+ auto row4AValue = ConvertToNode(TYsonString(TStringBuf(R"(#)")));
+ CHECK_YQL_TYPE_AND_VALUE(row4, "column_a", firstColumnAType, row4AValue, yqlTypes);
+
+ auto row4BValue = ConvertToNode(TYsonString(TStringBuf(
+ "["
+ "\"\xC3\xBF\";"
+ R"(
+ {"b64" = %true; "val" = "+vv8/Q=="};
+ ["0"; "127"];
+ ["1"; [%true]];
+ {"val"=[["-1"; "-1"]; ["0"; ""]]};
+ "499";
+ "40";
+ "4101";
+ "4202";
+ "4103";
+ "{}";
+ "-2.125";
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row4, "column_b", firstColumnBType, row4BValue, yqlTypes);
+
+ auto row4CValue = ConvertToNode(TYsonString(TStringBuf(R"({"val"=[]})")));
+ CHECK_YQL_TYPE_AND_VALUE(row4, "column_c", firstColumnCType, row4CValue, yqlTypes);
+ auto row4DValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ val = {
+ x = {
+ "$type" = "int64";
+ "$value" = "49";
+ }
+ }
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row4, "column_d", R"(["DataType"; "Yson"])", row4DValue, yqlTypes);
+
+ // Here must come rows from the second table.
+
+ ASSERT_EQ(row5->GetType(), ENodeType::Map);
+ EXPECT_EQ(row5->AsMap()->GetChildCount(), 4);
+ auto row5AValue = ConvertToNode(TYsonString(TStringBuf(R"(["0"; #])")));
+ CHECK_YQL_TYPE_AND_VALUE(row5, "column_a", secondColumnAType, row5AValue, yqlTypes);
+ auto row5BValue = ConvertToNode(TYsonString(TStringBuf(R"(#)")));
+ CHECK_YQL_TYPE_AND_VALUE(row5, "column_b", secondColumnBType, row5BValue, yqlTypes);
+ auto row5CValue = ConvertToNode(TYsonString(TStringBuf(R"(#)")));
+ CHECK_YQL_TYPE_AND_VALUE(row5, "column_c", secondColumnCType, row5CValue, yqlTypes);
+ auto row5DValue = ConvertToNode(TYsonString(TStringBuf(R"(["-49"])")));
+ CHECK_YQL_TYPE_AND_VALUE(row5, "column_d", secondColumnDType, row5DValue, yqlTypes);
+
+ ASSERT_EQ(row6->GetType(), ENodeType::Map);
+ EXPECT_EQ(row6->AsMap()->GetChildCount(), 4);
+ auto row6AValue = ConvertToNode(TYsonString(TStringBuf(R"([
+ "1";
+ {
+ val = {
+ z = {
+ "$type" = "string";
+ "$value" = "z";
+ }
+ }
+ };
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row6, "column_a", secondColumnAType, row6AValue, yqlTypes);
+ auto row6BValue = ConvertToNode(TYsonString(TStringBuf(R"(#)")));
+ CHECK_YQL_TYPE_AND_VALUE(row6, "column_b", secondColumnBType, row6BValue, yqlTypes);
+ auto row6CValue = ConvertToNode(TYsonString(TStringBuf(R"([#])")));
+ CHECK_YQL_TYPE_AND_VALUE(row6, "column_c", secondColumnCType, row6CValue, yqlTypes);
+ auto row6DValue = ConvertToNode(TYsonString(TStringBuf(R"(#)")));
+ CHECK_YQL_TYPE_AND_VALUE(row6, "column_d", secondColumnDType, row6DValue, yqlTypes);
+}
+
+TEST_F(TWriterForWebJson, YqlValueFormat_Incomplete)
+{
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+ Config_->FieldWeightLimit = 215;
+ Config_->StringWeightLimit = 10;
+
+ auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"column_a", StructLogicalType({
+ {"field1", SimpleLogicalType(ESimpleLogicalValueType::Int64)},
+ {"list", ListLogicalType(
+ VariantStructLogicalType({
+ {"a", DictLogicalType(
+ SimpleLogicalType(ESimpleLogicalValueType::Int64),
+ SimpleLogicalType(ESimpleLogicalValueType::String)
+ )},
+ {"b", SimpleLogicalType(ESimpleLogicalValueType::Any)},
+ })
+ )},
+ {"field2", SimpleLogicalType(ESimpleLogicalValueType::String)},
+ {"field3", MakeLogicalType(ESimpleLogicalValueType::Int64, false)},
+ })},
+ {"column_b", SimpleLogicalType(ESimpleLogicalValueType::Any)},
+ {"column_c", MakeLogicalType(ESimpleLogicalValueType::String, false)},
+ });
+
+ auto yqlTypeA = ConvertToNode(TYsonString(TStringBuf(R"([
+ "StructType";
+ [
+ [
+ "field1";
+ ["DataType"; "Int64"]
+ ];
+ [
+ "list";
+ [
+ "ListType";
+ [
+ "VariantType";
+ [
+ "StructType";
+ [
+ [
+ "a";
+ [
+ "DictType";
+ ["DataType"; "Int64"];
+ ["DataType"; "String"]
+ ]
+ ];
+ [
+ "b";
+ ["DataType"; "Yson"]
+ ];
+ ]
+ ]
+ ]
+ ]
+ ];
+ [
+ "field2";
+ ["DataType"; "String"]
+ ];
+ [
+ "field3";
+ [
+ "OptionalType";
+ ["DataType"; "Int64"]
+ ]
+ ];
+ ]
+ ])")));
+
+ auto yqlTypeB = ConvertToNode(TYsonString(TStringBuf(R"(["DataType"; "Yson"])")));
+ auto yqlTypeC = ConvertToNode(TYsonString(TStringBuf(R"(["OptionalType"; ["DataType"; "String"]])")));
+ {
+ CreateStandardWriter({schema});
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {
+ {
+ "column_a",
+ EValueType::Composite,
+ R"([
+ -1;
+ [
+ [
+ 0;
+ [
+ [-2; "UTF:)" + TString("\xF0\x90\x8D\x88") + "\xF0\x90\x8D\x88" + R"("];
+ [2; "!UTF:)" + TString("\xFA\xFB\xFC\xFD\xFA\xFB\xFC\xFD") + R"("];
+ [0; ""];
+ ]
+ ];
+ [
+ 1;
+ "{kinda_long_key = kinda_even_longer_value}"
+ ];
+ [
+ 0;
+ [
+ [0; "One more quite long string"];
+ [1; "One more quite long string"];
+ [2; "One more quite long string"];
+ [3; "One more quite long string"];
+ [4; "One more quite long string"];
+ [5; "One more quite long string"];
+ ]
+ ];
+ [
+ 1;
+ "{kinda_long_key = kinda_even_longer_value}"
+ ];
+ ];
+ "I'm short";
+ 424242238133245
+ ])"
+ },
+ {"column_b", EValueType::Any, "{kinda_long_key = kinda_even_longer_value}"},
+ {"column_c", "One more quite long string"},
+ }).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 1);
+
+ auto row = rows->AsList()->GetChildOrThrow(0);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ EXPECT_EQ(row->AsMap()->GetChildCount(), 3);
+
+ auto rowAValue = ConvertToNode(TYsonString(R"([
+ "-1";
+ {
+ "inc" = %true;
+ "val" = [
+ [
+ "0";
+ {
+ "val" = [
+ ["-2"; {"inc"=%true; "val"="UTF:)" + TString("\xF0\x90\x8D\x88") + R"("}];
+ ["2"; {"inc"=%true; "b64"=%true; "val"="IVVURjr6"}];
+ ["0"; ""];
+ ]
+ }
+ ];
+ [
+ "1";
+ {"val"=""; "inc"=%true}
+ ];
+ [
+ "0";
+ {
+ "inc" = %true;
+ "val" = [
+ ["0"; {"val"="One more q"; "inc"=%true}];
+ ["1"; {"val"="One more "; "inc"=%true}];
+ ];
+ }
+ ];
+ ];
+ };
+ {
+ "val" = "";
+ "inc" = %true;
+ };
+ ["424242238133245"];
+ ])"));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+
+ // Simple values are not truncated to |StringWeightLimit|
+ auto rowBValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ val = {
+ kinda_long_key = {
+ "$type" = "string";
+ "$value" = kinda_even_longer_value;
+ }
+ }
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_b", yqlTypeB, rowBValue, yqlTypes);
+ auto rowCValue = ConvertToNode(TYsonString(TStringBuf(R"(["One more quite long string"])")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_c", yqlTypeC, rowCValue, yqlTypes);
+}
+
+
+TEST_F(TWriterForWebJson, YqlValueFormat_Any)
+{
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+
+ auto schema = New<TTableSchema>(std::vector<TColumnSchema>{
+ {"column_a", MakeLogicalType(ESimpleLogicalValueType::Any, false)},
+ });
+
+ auto yqlTypeA = ConvertToNode(TYsonString(TStringBuf(R"([
+ "OptionalType";
+ ["DataType"; "Yson"]
+ ])")));
+
+ CreateStandardWriter({schema});
+ {
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {{"column_a", EValueType::Any, "{x=y;z=2}"}}).Get(),
+ MakeRow(NameTable_, {{"column_a", true}}).Get(),
+ MakeRow(NameTable_, {{"column_a", -42}}).Get(),
+ MakeRow(NameTable_, {{"column_a", 42u}}).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 4);
+
+ {
+ auto row = rows->AsList()->GetChildOrThrow(0);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto rowAValue = ConvertToNode(TYsonString(TStringBuf(R"([
+ {
+ val = {
+ x = {
+ "$type" = "string";
+ "$value" = "y";
+ };
+ z = {
+ "$type" = "int64";
+ "$value" = "2";
+ }
+ }
+ }
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+ }
+ {
+ auto row = rows->AsList()->GetChildOrThrow(1);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto rowAValue = ConvertToNode(TYsonString(TStringBuf(R"([
+ {
+ val = {
+ "$type" = "boolean";
+ "$value" = "true";
+ }
+ }
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+ }
+ {
+ auto row = rows->AsList()->GetChildOrThrow(2);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto rowAValue = ConvertToNode(TYsonString(TStringBuf(R"([
+ {
+ val = {
+ "$type" = "int64";
+ "$value" = "-42";
+ }
+ }
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+ }
+ {
+ auto row = rows->AsList()->GetChildOrThrow(3);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto rowAValue = ConvertToNode(TYsonString(TStringBuf(R"([
+ {
+ val = {
+ "$type" = "uint64";
+ "$value" = "42";
+ }
+ }
+ ])")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+ }
+}
+
+TEST_F(TWriterForWebJson, YqlValueFormat_CompositeNoSchema)
+{
+ Config_->ValueFormat = EWebJsonValueFormat::Yql;
+
+ auto schema = New<TTableSchema>();
+
+ auto yqlTypeA = ConvertToNode(TYsonString(TStringBuf(R"(["DataType"; "Yson"])")));
+
+ CreateStandardWriter({schema});
+ {
+ bool written = Writer_->Write({
+ MakeRow(NameTable_, {{"column_a", EValueType::Composite, "[1;2]"}}).Get(),
+ });
+ EXPECT_TRUE(written);
+ Writer_->Close().Get().ThrowOnError();
+ }
+
+ auto result = ParseJsonToNode(OutputStream_.Str());
+ ASSERT_EQ(result->GetType(), ENodeType::Map);
+
+ auto rows = result->AsMap()->FindChild("rows");
+ ASSERT_TRUE(rows);
+ auto yqlTypeRegistry = result->AsMap()->FindChild("yql_type_registry");
+ ASSERT_TRUE(yqlTypeRegistry);
+
+ ASSERT_EQ(yqlTypeRegistry->GetType(), ENodeType::List);
+ auto yqlTypes = ConvertTo<std::vector<INodePtr>>(yqlTypeRegistry);
+
+ ASSERT_EQ(rows->GetType(), ENodeType::List);
+ ASSERT_EQ(rows->AsList()->GetChildCount(), 1);
+
+ {
+ auto row = rows->AsList()->GetChildOrThrow(0);
+ ASSERT_EQ(row->GetType(), ENodeType::Map);
+ auto rowAValue = ConvertToNode(TYsonString(TStringBuf(R"({
+ "val" = [
+ {
+ "$type" = "int64";
+ "$value" = "1";
+ };
+ {
+ "$type" = "int64";
+ "$value" = "2";
+ }
+ ]
+ })")));
+ CHECK_YQL_TYPE_AND_VALUE(row, "column_a", yqlTypeA, rowAValue, yqlTypes);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/wire_protocol_ut.cpp b/yt/yt/client/unittests/wire_protocol_ut.cpp
new file mode 100644
index 0000000000..40845d5dbf
--- /dev/null
+++ b/yt/yt/client/unittests/wire_protocol_ut.cpp
@@ -0,0 +1,285 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/wire_protocol.h>
+
+#include <cstring>
+
+namespace NYT::NTableClient {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWireProtocolTestTag
+{ };
+
+class TWireProtocolTest
+ : public ::testing::Test
+{
+public:
+ static std::vector<EValueType> GetValueTypes()
+ {
+ return {
+ EValueType::Null,
+ EValueType::Int64,
+ EValueType::Uint64,
+ EValueType::Double,
+ EValueType::Boolean,
+ EValueType::String,
+ EValueType::Any
+ };
+ }
+
+ static TUnversionedValue MakeValueSample(ui16 id, EValueType type, EValueFlags flags = EValueFlags::None)
+ {
+ switch (type) {
+ case EValueType::Null:
+ return MakeUnversionedNullValue(id, flags);
+ case EValueType::Int64:
+ return MakeUnversionedInt64Value(0x0123456789ABCDEFLL, id, flags);
+ case EValueType::Uint64:
+ return MakeUnversionedUint64Value(0xFEDCBA9876543210ULL, id, flags);
+ case EValueType::Double:
+ return MakeUnversionedDoubleValue(3.141592653589793, id, flags);
+ case EValueType::Boolean:
+ return MakeUnversionedBooleanValue(false, id, flags);
+ case EValueType::String:
+ return MakeUnversionedStringValue("s", id, flags);
+ case EValueType::Any:
+ return MakeUnversionedAnyValue("{}", id, flags);
+ default:
+ YT_ABORT();
+ }
+ }
+
+ static TUnversionedOwningRow MakeUnversionedRowSample()
+ {
+ TUnversionedOwningRowBuilder builder;
+ ui16 id = 0;
+ for (auto type : GetValueTypes()) {
+ for (auto flags : {EValueFlags::Aggregate, EValueFlags::None}) {
+ builder.AddValue(MakeValueSample(id, type, flags));
+ }
+ }
+ return builder.FinishRow();
+ }
+
+ static TUnversionedOwningRow MakeSchemafulRowSample()
+ {
+ TUnversionedOwningRowBuilder builder;
+ ui16 id = 0;
+ for (auto type : GetValueTypes()) {
+ // This one does not support aggregate flags.
+ builder.AddValue(MakeValueSample(id, type));
+ }
+ return builder.FinishRow();
+ }
+
+ static std::vector<ui32> ExtractSchemaData(TUnversionedRow row, EValueType nullType)
+ {
+ std::vector<ui32> result;
+ for (ui32 index = 0; index < row.GetCount(); ++index) {
+ ui32 tmp = static_cast<ui32>(row[index].Id);
+ if (row[index].Type == EValueType::Null) {
+ tmp |= static_cast<ui32>(nullType) << 16;
+ } else {
+ tmp |= static_cast<ui32>(row[index].Type) << 16;
+ }
+ result.push_back(tmp);
+ }
+ return result;
+ }
+
+ // We cannot use CompareRows, because it does not check aggregate bits and does not compare anys.
+ void CheckEquals(TUnversionedRow lhs, TUnversionedRow rhs)
+ {
+ ASSERT_EQ(lhs.GetCount(), rhs.GetCount()) << "rows have different length";
+
+ for (ui32 i = 0; i < lhs.GetCount(); ++i) {
+ const auto& lhsValue = lhs[i];
+ const auto& rhsValue = rhs[i];
+ SCOPED_TRACE(Format("#%v: LHS = %v ; RHS = %v", i, lhsValue, rhsValue));
+ EXPECT_TRUE(TBitwiseUnversionedValueEqual()(lhsValue, rhsValue));
+ }
+ }
+
+ void CheckEquals(const std::vector<unsigned char>& canonical, const TSharedRef& actual)
+ {
+ ASSERT_EQ(canonical.size(), actual.Size());
+
+ for (size_t i = 0; i < canonical.size(); ++i) {
+ auto canonicalByte = static_cast<unsigned char>(canonical[i]);
+ auto actualByte = static_cast<unsigned char>(actual[i]);
+ if (canonicalByte != 0xcf) { // 0xcf marks garbage due to alignment.
+ EXPECT_EQ(canonicalByte, actualByte);
+ }
+ }
+ }
+
+ static void Dump(const TSharedRef& blob)
+ {
+ Cerr << "=== BEGIN DUMP ===" << Endl;
+ for (size_t i = 0; i < blob.Size(); ++i) {
+ if (i % 16 == 0) {
+ Cerr << Endl;
+ }
+ Cerr << Format("0x%02x, ", static_cast<unsigned char>(blob.Begin()[i]));
+ }
+ Cerr << Endl << "=== END DUMP ===" << Endl;
+ }
+};
+
+TEST_F(TWireProtocolTest, UnversionedRow)
+{
+ auto originalRow = MakeUnversionedRowSample();
+
+ auto writer = CreateWireProtocolWriter();
+ writer->WriteUnversionedRow(originalRow);
+ auto blob = MergeRefsToRef<TWireProtocolTestTag>(writer->Finish());
+ Dump(blob);
+
+ auto reader = CreateWireProtocolReader(blob);
+ auto reconstructedRow = reader->ReadUnversionedRow(true);
+ CheckEquals(originalRow, reconstructedRow);
+
+ // This is a canonical dump. Do not break it.
+ const std::vector<unsigned char> canonicalBlob({
+ // value count
+ 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // one value per row
+ 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
+ 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
+ 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
+ 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
+ 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40,
+ 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40,
+ 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, 0x73, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x73, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ 0x00, 0x00, 0x11, 0x01, 0x02, 0x00, 0x00, 0x00, 0x7b, 0x7d, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x00, 0x00, 0x7b, 0x7d, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ });
+
+ CheckEquals(canonicalBlob, blob);
+}
+
+TEST_F(TWireProtocolTest, SchemafulRow)
+{
+ auto originalRow = MakeSchemafulRowSample();
+
+ auto writer = CreateWireProtocolWriter();
+ writer->WriteSchemafulRow(originalRow);
+ auto blob = MergeRefsToRef<TWireProtocolTestTag>(writer->Finish());
+ Dump(blob);
+
+ auto reader = CreateWireProtocolReader(blob);
+ auto reconstructedRow = reader->ReadSchemafulRow(ExtractSchemaData(originalRow, EValueType::Int64), true);
+ CheckEquals(originalRow, reconstructedRow);
+
+ // This is a canonical dump. Do not break it.
+ const std::vector<unsigned char> canonicalBlob({
+ // value count
+ 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // null bitmap
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // one value per row
+ 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
+ 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
+ 0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7b, 0x7d, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ });
+
+ CheckEquals(canonicalBlob, blob);
+}
+
+// Test that schemaful reader/writer properly treats null bitmap.
+TEST_F(TWireProtocolTest, Regression1)
+{
+ const std::vector<unsigned char> blob({
+ // value count = 4
+ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // null bitmap = 1 << 3
+ 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // id = 0, type = int64, data = 1
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // id = 1, type = int64, data = 1
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // id = 2, type = string, data = "2"
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x32, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ // id = 3, type = string, data = (null)
+ });
+
+ const std::vector<ui32> blobSchemaData({
+ 0x00030000, // id = 0, type = int64
+ 0x00030001, // id = 1, type = int64
+ 0x00100002, // id = 2, type = int64
+ 0x00100003, // id = 3, type = int64
+ });
+
+ auto reader = CreateWireProtocolReader(TSharedRef::MakeCopy<TWireProtocolTestTag>(
+ TRef(blob.data(), blob.size())));
+ auto row = reader->ReadSchemafulRow(blobSchemaData, false);
+ EXPECT_TRUE(reader->GetCurrent() == reader->GetEnd());
+
+ ASSERT_EQ(static_cast<int>(row.GetCount()), 4);
+
+ EXPECT_EQ(row[0].Id, 0);
+ EXPECT_EQ(row[0].Type, EValueType::Int64);
+ EXPECT_EQ(row[0].Flags, EValueFlags::None);
+ EXPECT_EQ(row[0].Data.Int64, 1);
+
+ EXPECT_EQ(row[1].Id, 1);
+ EXPECT_EQ(row[1].Type, EValueType::Int64);
+ EXPECT_EQ(row[1].Flags, EValueFlags::None);
+ EXPECT_EQ(row[1].Data.Int64, 1);
+
+ EXPECT_EQ(row[2].Id, 2);
+ EXPECT_EQ(row[2].Type, EValueType::String);
+ EXPECT_EQ(row[2].Flags, EValueFlags::None);
+ EXPECT_EQ(static_cast<int>(row[2].Length), 1);
+ EXPECT_EQ(row[2].Data.String[0], '2');
+
+ EXPECT_EQ(row[3].Id, 3);
+ EXPECT_EQ(row[3].Type, EValueType::Null);
+ EXPECT_EQ(row[3].Flags, EValueFlags::None);
+}
+
+// Test sentinel values (min & max) serializability.
+TEST_F(TWireProtocolTest, Regression2)
+{
+ const std::vector<unsigned char> blob({
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value count = 3
+ 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // id = 0, type = null
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // id = 1, type = min
+ 0x02, 0x00, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, // id = 2, type = max
+ });
+
+ auto reader = CreateWireProtocolReader(TSharedRef::MakeCopy<TWireProtocolTestTag>(
+ TRef(blob.data(), blob.size())));
+ auto row = reader->ReadUnversionedRow(true);
+ EXPECT_TRUE(reader->GetCurrent() == reader->GetEnd());
+
+ ASSERT_EQ(static_cast<int>(row.GetCount()), 3);
+
+ EXPECT_EQ(row[0].Id, 0);
+ EXPECT_EQ(row[0].Type, EValueType::Null);
+ EXPECT_EQ(row[0].Flags, EValueFlags::None);
+
+ EXPECT_EQ(row[1].Id, 1);
+ EXPECT_EQ(row[1].Type, EValueType::Min);
+ EXPECT_EQ(row[1].Flags, EValueFlags::None);
+
+ EXPECT_EQ(row[2].Id, 2);
+ EXPECT_EQ(row[2].Type, EValueType::Max);
+ EXPECT_EQ(row[2].Flags, EValueFlags::None);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NTableClient
diff --git a/yt/yt/client/unittests/ya.make b/yt/yt/client/unittests/ya.make
new file mode 100644
index 0000000000..eafbec6fe2
--- /dev/null
+++ b/yt/yt/client/unittests/ya.make
@@ -0,0 +1,84 @@
+GTEST(unittester-client)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+ALLOCATOR(YT)
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ protobuf_format_ut.proto
+
+ check_schema_compatibility_ut.cpp
+ check_type_compatibility_ut.cpp
+ chunk_replica_ut.cpp
+ column_sort_schema_ut.cpp
+ comparator_ut.cpp
+ composite_compare_ut.cpp
+ connection_ut.cpp
+ dsv_parser_ut.cpp
+ dsv_writer_ut.cpp
+ farm_fingerprint_stability_ut.cpp
+ key_bound_ut.cpp
+ key_bound_compressor_ut.cpp
+ key_helpers.cpp
+ key_ut.cpp
+ logical_type_ut.cpp
+ named_yson_token_ut.cpp
+ uuid_text_ut.cpp
+ time_text_ut.cpp
+ node_directory_ut.cpp
+ protobuf_format_ut.cpp
+ query_builder_ut.cpp
+ read_limit_ut.cpp
+ replication_progress_ut.cpp
+ row_helpers.cpp
+ row_ut.cpp
+ schemaful_dsv_parser_ut.cpp
+ schemaful_dsv_writer_ut.cpp
+ schema_ut.cpp
+ skiff_format_ut.cpp
+ skiff_yson_converter_ut.cpp
+ table_consumer_ut.cpp
+ unordered_reader_ut.cpp
+ unversioned_row_ut.cpp
+ validate_logical_type_ut.cpp
+ value_examples.cpp
+ web_json_writer_ut.cpp
+ wire_protocol_ut.cpp
+ yamred_dsv_parser_ut.cpp
+ yamred_dsv_writer_ut.cpp
+ yamr_parser_ut.cpp
+ yamr_writer_ut.cpp
+ ypath_ut.cpp
+ yson_helpers.cpp
+ zookeeper_bus_ut.cpp
+ zookeeper_protocol_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/build
+ yt/yt/core/test_framework
+ yt/yt/core
+ yt/yt/client
+ yt/yt/client/formats
+ yt/yt/client/unittests/mock
+ yt/yt/library/named_value
+
+ yt/yt_proto/yt/formats
+)
+
+RESOURCE(
+ ${ARCADIA_ROOT}/library/cpp/type_info/test-data/good-types.txt /types/good
+ ${ARCADIA_ROOT}/library/cpp/type_info/test-data/bad-types.txt /types/bad
+)
+
+ENV(YT_PROXY_URL_ALIASING_CONFIG=\"{primary=\\\"localhost:12345\\\"}\") # For TProxyUrlTest
+
+SIZE(MEDIUM)
+
+REQUIREMENTS(ram:12)
+
+END()
diff --git a/yt/yt/client/unittests/yamr_parser_ut.cpp b/yt/yt/client/unittests/yamr_parser_ut.cpp
new file mode 100644
index 0000000000..74b8f530a1
--- /dev/null
+++ b/yt/yt/client/unittests/yamr_parser_ut.cpp
@@ -0,0 +1,606 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/client/formats/yamr_parser.h>
+
+#include <yt/yt/core/yson/null_consumer.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NYson;
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamrParserTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("table_index"));
+ EXPECT_CALL(Mock, OnInt64Scalar(2));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "key1\tvalue1\n"
+ "2\n"
+ "key2\tvalue2\n";
+
+ ParseYamr(input, &Mock);
+}
+
+TEST(TYamrParserTest, ValueWithTabs)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar(TStringBuf("key1\0", 5)));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value with \t and some other"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar(TStringBuf("another\0 value with \t", 21)));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input(
+ "key1\0\tvalue with \t and some other\n"
+ "key2\tanother\0 value with \t\n",
+ 34 +
+ 27);
+
+ ParseYamr(input, &Mock);
+}
+
+TEST(TYamrParserTest, SimpleWithSubkey)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "key1\tsubkey1\tvalue1\n"
+ "key2\tsubkey2\tvalue2\n";
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrParserTest, IncompleteRows)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "key1\tsubkey1\tvalue1\n"
+ "key\tsubkey\n"
+ "key2\tsubkey2\tvalue2\n";
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrParserTest, IncorrectIncompleteRows)
+{
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = false;
+
+ EXPECT_THROW(ParseYamr("\n", GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr("key\n", GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr("key\tvalue\nkey\n", GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TYamrParserTest, TabsInValue)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("a\tb\\tc\t"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto config = New<TYamrFormatConfig>();
+ TString input = "key\ta\tb\\tc\t";
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrParserTest, Escaping)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("\tkey\t"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("\n"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("a\tb\t\n"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+ config->EnableEscaping = true;
+
+ TString input = "\\tkey\\t\t\\n\ta\tb\t\\n\n";
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrParserTest, CustomSeparators)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto config = New<TYamrFormatConfig>();
+ config->RecordSeparator = 'Y';
+ config->FieldSeparator = 'X';
+
+ TString input = "keyXvalueYkey2Xvalue2Y";
+ ParseYamr(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamrLenvalParserTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("table_index"));
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (2 * 4 + 4 + 6) + 8 // all i32 + lengths of keys
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->Lenval = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrLenvalParserTest, SimpleWithSubkey)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("subkey2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x07\x00\x00\x00" "subkey1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x07\x00\x00\x00" "subkey2"
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (3 * 4 + 4 + 7 + 6) // all i32 + lengths of keys
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+ config->Lenval = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrLenvalParserTest, EmptyFields)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ , 3 * 4
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+ config->Lenval = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrLenvalParserTest, HugeLength)
+{
+ TString input = TString(
+ "\xFF\xFF\xFF\xFF"
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ , 3 * 4
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+ config->Lenval = true;
+
+ EXPECT_THROW(ParseYamr(input, GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TYamrLenvalParserTest, SimpleEndOfMessage)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key1"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("table_index"));
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("key2"));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar("value2"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\xfb\xff\xff\xff" "\x02\x00\x00\x00\x00\x00\x00\x00"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->Lenval = true;
+ config->EnableEom = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrLenvalParserTest, EmptyFieldsWithEOM)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("value"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ "\xfb\xff\xff\xff" "\x01\x00\x00\x00\x00\x00\x00\x00"
+ , 3 * 4 + 12
+ );
+
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = true;
+ config->Lenval = true;
+ config->EnableEom = true;
+
+ ParseYamr(input, &Mock, config);
+}
+
+TEST(TYamrParserTest, IncorrectPlaceOfEOM)
+{
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = false;
+ config->Lenval = true;
+ config->EnableEom = true;
+
+ TString input1 = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\xfb\xff\xff\xff" "\x02\x00\x00\x00\x00\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ TString input2 = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+
+ "\xfb\xff\xff\xff" "\x02\x00\x00\x00\x00\x00\x00\x00"
+
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ EXPECT_THROW(ParseYamr(input1, GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr(input2, GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TYamrParserTest, IncorrectEOM)
+{
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = false;
+ config->Lenval = true;
+ config->EnableEom = true;
+
+ // Garbage after EOM marker
+ TString input1 = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\xfb\xff\xff\xff" "\x01\x00\x00\x00\x00\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ // Row count mismatch
+ TString input2 = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\xfb\xff\xff\xff" "\x03\x00\x00\x00\x00\x00\x00\x00"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ // Missing EOM marker
+ TString input3 = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ , 2 * (2 * 4 + 4 + 6) + 8 // all i32 + lengths of keys
+ );
+
+ // Missing EOM marker with empty fields
+ TString input4 = TString(
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ "\x00\x00\x00\x00"
+ , 3 * 4
+ );
+
+ EXPECT_THROW(ParseYamr(input1, GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr(input2, GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr(input3, GetNullYsonConsumer(), config), std::exception);
+ EXPECT_THROW(ParseYamr(input4, GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TYamrParserTest, UnsupportedEOMInTextMode)
+{
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = false;
+ config->Lenval = false;
+ config->EnableEom = true;
+
+ TString input = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\xfb\xff\xff\xff" "\x02\x00\x00\x00\x00\x00\x00\x00"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ EXPECT_THROW(ParseYamr(input, GetNullYsonConsumer(), config), std::exception);
+}
+
+TEST(TYamrParserTest, UnexpectedEOM)
+{
+ auto config = New<TYamrFormatConfig>();
+ config->HasSubkey = false;
+ config->Lenval = true;
+ config->EnableEom = false;
+
+ TString input = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xff\xff\xff\xff" "\x01\x00\x00\x00"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\xfb\xff\xff\xff" "\x02\x00\x00\x00\x00\x00\x00\x00"
+ , 2 * (2 * 4 + 4 + 6) + 8 + 12 // all i32 + lengths of keys
+ );
+
+ EXPECT_THROW(ParseYamr(input, GetNullYsonConsumer(), config), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/yamr_writer_ut.cpp b/yt/yt/client/unittests/yamr_writer_ut.cpp
new file mode 100644
index 0000000000..747f542807
--- /dev/null
+++ b/yt/yt/client/unittests/yamr_writer_ut.cpp
@@ -0,0 +1,644 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/client/formats/yamr_writer.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NFormats {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NTableClient;
+
+class TSchemalessWriterForYamrTest
+ : public ::testing::Test
+{
+protected:
+ TNameTablePtr NameTable_;
+ int KeyId_;
+ int SubkeyId_;
+ int ValueId_;
+ int TableIndexId_;
+ int RangeIndexId_;
+ int RowIndexId_;
+
+ TYamrFormatConfigPtr Config_;
+
+ IUnversionedRowsetWriterPtr Writer_;
+
+ TStringStream OutputStream_;
+
+ TSchemalessWriterForYamrTest() {
+ NameTable_ = New<TNameTable>();
+ KeyId_ = NameTable_->RegisterName("key");
+ SubkeyId_ = NameTable_->RegisterName("subkey");
+ ValueId_ = NameTable_->RegisterName("value");
+ TableIndexId_ = NameTable_->RegisterName(TableIndexColumnName);
+ RowIndexId_ = NameTable_->RegisterName(RowIndexColumnName);
+ RangeIndexId_ = NameTable_->RegisterName(RangeIndexColumnName);
+
+ Config_ = New<TYamrFormatConfig>();
+ }
+
+ void CreateStandardWriter(TControlAttributesConfigPtr controlAttributes = New<TControlAttributesConfig>())
+ {
+ Writer_ = CreateSchemalessWriterForYamr(
+ Config_,
+ NameTable_,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&OutputStream_)),
+ false, /* enableContextSaving */
+ controlAttributes,
+ 0 /* keyColumnCount */);
+ }
+};
+
+TEST_F(TSchemalessWriterForYamrTest, Simple)
+{
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+
+ // Ignore system columns.
+ row1.AddValue(MakeUnversionedInt64Value(2, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, RowIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(1, RangeIndexId_));
+
+ // Note that key and value follow not in order.
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "key1\tvalue1\n"
+ "key2\tvalue2\n";
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SimpleWithSubkey)
+{
+ Config_->HasSubkey = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedStringValue("subkey1", SubkeyId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("subkey2", SubkeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "key1\tsubkey1\tvalue1\n"
+ "key2\tsubkey2\tvalue2\n";
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SubkeyCouldBeSkipped)
+{
+ Config_->HasSubkey = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("key", KeyId_));
+ row.AddValue(MakeUnversionedStringValue("value", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = "key\t\tvalue\n";
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SubkeyCouldBeNull)
+{
+ Config_->HasSubkey = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("key", KeyId_));
+ row.AddValue(MakeUnversionedSentinelValue(EValueType::Null, SubkeyId_));
+ row.AddValue(MakeUnversionedStringValue("value", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = "key\t\tvalue\n";
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, NonNullTerminatedStrings)
+{
+ Config_->HasSubkey = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ const char* longString = "trashkeytrashsubkeytrashvalue";
+ row.AddValue(MakeUnversionedStringValue(TStringBuf(longString + 5, 3), KeyId_));
+ row.AddValue(MakeUnversionedStringValue(TStringBuf(longString + 13, 6), SubkeyId_));
+ row.AddValue(MakeUnversionedStringValue(TStringBuf(longString + 24, 5), ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = "key\tsubkey\tvalue\n";
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SkippedKey)
+{
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("value", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_FALSE(Writer_->Write(rows));
+
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SkippedValue)
+{
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("key", KeyId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_FALSE(Writer_->Write(rows));
+
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+}
+
+TEST_F(TSchemalessWriterForYamrTest, NotStringType) {
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("key", KeyId_));
+ row.AddValue(MakeUnversionedInt64Value(42, ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_FALSE(Writer_->Write(rows));
+
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+}
+
+TEST_F(TSchemalessWriterForYamrTest, ExtraItem)
+{
+ int trashId = NameTable_->RegisterName("trash");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("key", KeyId_));
+ row.AddValue(MakeUnversionedStringValue("value", ValueId_));
+ // This value will be ignored.
+ row.AddValue(MakeUnversionedStringValue("trash", trashId));
+ // This value will also be ignored because Config_->HasSubkey is off,
+ // despite the fact it has non-string type.
+ row.AddValue(MakeUnversionedInt64Value(42, SubkeyId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = "key\tvalue\n";
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, Escaping)
+{
+ Config_->HasSubkey = true;
+ Config_->EnableEscaping = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("\n", KeyId_));
+ row.AddValue(MakeUnversionedStringValue("\t", SubkeyId_));
+ row.AddValue(MakeUnversionedStringValue("\n", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = "\\n\t\\t\t\\n\n";
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SimpleWithTableIndex)
+{
+ Config_->EnableTableIndex = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableTableIndex = true;
+ CreateStandardWriter(controlAttributes);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("value3", ValueId_));
+ row3.AddValue(MakeUnversionedInt64Value(23, TableIndexId_));
+
+ rows = { row3.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "42\n"
+ "key1\tvalue1\n"
+ "key2\tvalue2\n"
+ "23\n"
+ "key3\tvalue3\n";
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, SimpleWithRowIndexAndTableIndex)
+{
+ Config_->EnableTableIndex = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableTableIndex = true;
+ controlAttributes->EnableRowIndex = true;
+ CreateStandardWriter(controlAttributes);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(0, RowIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(0, RangeIndexId_));
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("value3", ValueId_));
+ row3.AddValue(MakeUnversionedInt64Value(5, RowIndexId_));
+ row3.AddValue(MakeUnversionedInt64Value(1, RangeIndexId_));
+ rows = { row3.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row4;
+ row4.AddValue(MakeUnversionedStringValue("key4", KeyId_));
+ row4.AddValue(MakeUnversionedStringValue("value4", ValueId_));
+ row4.AddValue(MakeUnversionedInt64Value(23, TableIndexId_));
+ row4.AddValue(MakeUnversionedInt64Value(10, RowIndexId_));
+ row4.AddValue(MakeUnversionedInt64Value(2, RangeIndexId_));
+ rows = { row4.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output =
+ "42\n0\n"
+ "key1\tvalue1\n"
+ "key2\tvalue2\n"
+ "42\n5\n"
+ "key3\tvalue3\n"
+ "23\n10\n"
+ "key4\tvalue4\n";
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, Lenval)
+{
+ Config_->HasSubkey = true;
+ Config_->Lenval = true;
+ CreateStandardWriter();
+
+ // Note that order in both rows is unusual.
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("subkey1", SubkeyId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedStringValue("subkey2", SubkeyId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x07\x00\x00\x00" "subkey1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x07\x00\x00\x00" "subkey2"
+ "\x06\x00\x00\x00" "value2"
+ , 2 * (3 * 4 + 4 + 6 + 7) // all i32 + lengths of keys
+ );
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, LenvalWithEmptyFields)
+{
+ Config_->HasSubkey = true;
+ Config_->Lenval = true;
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("subkey1", SubkeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("", SubkeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("subkey3", SubkeyId_));
+ row3.AddValue(MakeUnversionedStringValue("", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow(), row3.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = TString(
+ "\x00\x00\x00\x00" ""
+ "\x07\x00\x00\x00" "subkey1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x00\x00\x00\x00" ""
+ "\x06\x00\x00\x00" "value2"
+
+ "\x04\x00\x00\x00" "key3"
+ "\x07\x00\x00\x00" "subkey3"
+ "\x00\x00\x00\x00" ""
+
+ , 9 * 4 + (7 + 6) + (4 + 6) + (4 + 7) // all i32 + lengths of keys
+ );
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, LenvalWithKeySwitch)
+{
+ Config_->HasSubkey = true;
+ Config_->Lenval = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableKeySwitch = true;
+
+ Writer_ = CreateSchemalessWriterForYamr(
+ Config_,
+ NameTable_,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&OutputStream_)),
+ false, /* enableContextSaving */
+ controlAttributes,
+ 1 /* keyColumnCount */);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("subkey1", SubkeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("subkey21", SubkeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value21", ValueId_));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("subkey22", SubkeyId_));
+ row3.AddValue(MakeUnversionedStringValue("value22", ValueId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow(), row3.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row4;
+ row4.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row4.AddValue(MakeUnversionedStringValue("subkey3", SubkeyId_));
+ row4.AddValue(MakeUnversionedStringValue("value3", ValueId_));
+
+ rows = { row4.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output = TString(
+ "\x04\x00\x00\x00" "key1"
+ "\x07\x00\x00\x00" "subkey1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\xfe\xff\xff\xff" // key switch
+
+ "\x04\x00\x00\x00" "key2"
+ "\x08\x00\x00\x00" "subkey21"
+ "\x07\x00\x00\x00" "value21"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x08\x00\x00\x00" "subkey22"
+ "\x07\x00\x00\x00" "value22"
+
+ "\xfe\xff\xff\xff"
+
+ "\x04\x00\x00\x00" "key3"
+ "\x07\x00\x00\x00" "subkey3"
+ "\x06\x00\x00\x00" "value3"
+
+ , 14 * 4 + (4 + 7 + 6) + (4 + 8 + 7) + (4 + 8 + 7) + (4 + 7 + 6) // all i32 + lengths of keys
+ );
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, LenvalWithTableIndex)
+{
+ Config_->EnableTableIndex = true;
+ Config_->Lenval = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableTableIndex = true;
+ CreateStandardWriter(controlAttributes);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("value3", ValueId_));
+ row3.AddValue(MakeUnversionedInt64Value(23, TableIndexId_));
+
+ rows = { row3.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output(
+ "\xff\xff\xff\xff" "\x2a\x00\x00\x00" // 42
+
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\xff\xff\xff\xff" "\x17\x00\x00\x00" // 23
+
+ "\x04\x00\x00\x00" "key3"
+ "\x06\x00\x00\x00" "value3"
+ , 10 * 4 + 3 * (4 + 6));
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+TEST_F(TSchemalessWriterForYamrTest, LenvalWithRangeAndRowIndex)
+{
+ Config_->Lenval = true;
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableRowIndex = true;
+ controlAttributes->EnableRangeIndex = true;
+ CreateStandardWriter(controlAttributes);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("key1", KeyId_));
+ row1.AddValue(MakeUnversionedStringValue("value1", ValueId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, RangeIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(23, RowIndexId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("key2", KeyId_));
+ row2.AddValue(MakeUnversionedStringValue("value2", ValueId_));
+ row2.AddValue(MakeUnversionedInt64Value(42, RangeIndexId_));
+ row2.AddValue(MakeUnversionedInt64Value(24, RowIndexId_));
+
+ std::vector<TUnversionedRow> rows = { row1.GetRow(), row2.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ TUnversionedRowBuilder row3;
+ row3.AddValue(MakeUnversionedStringValue("key3", KeyId_));
+ row3.AddValue(MakeUnversionedStringValue("value3", ValueId_));
+ row3.AddValue(MakeUnversionedInt64Value(42, RangeIndexId_));
+ row3.AddValue(MakeUnversionedInt64Value(25, RowIndexId_));
+
+ rows = { row3.GetRow() };
+ EXPECT_EQ(true, Writer_->Write(rows));
+
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString output(
+ "\xfd\xff\xff\xff" "\x2a\x00\x00\x00" // 42
+ "\xfc\xff\xff\xff" "\x17\x00\x00\x00\x00\x00\x00\x00" // 23
+
+ "\x04\x00\x00\x00" "key1"
+ "\x06\x00\x00\x00" "value1"
+
+ "\x04\x00\x00\x00" "key2"
+ "\x06\x00\x00\x00" "value2"
+
+ "\x04\x00\x00\x00" "key3"
+ "\x06\x00\x00\x00" "value3"
+ , 11 * 4 + 3 * (4 + 6));
+
+ EXPECT_EQ(output, OutputStream_.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/yamred_dsv_parser_ut.cpp b/yt/yt/client/unittests/yamred_dsv_parser_ut.cpp
new file mode 100644
index 0000000000..d29c9a4df6
--- /dev/null
+++ b/yt/yt/client/unittests/yamred_dsv_parser_ut.cpp
@@ -0,0 +1,187 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/client/formats/yamred_dsv_parser.h>
+
+namespace NYT::NFormats {
+namespace {
+
+using namespace NYson;
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamredDsvParserTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key_a"));
+ EXPECT_CALL(Mock, OnStringScalar("1"));
+ EXPECT_CALL(Mock, OnKeyedItem("key_b"));
+ EXPECT_CALL(Mock, OnStringScalar("2"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey_x"));
+ EXPECT_CALL(Mock, OnStringScalar("3"));
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("5"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar("6"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key_a"));
+ EXPECT_CALL(Mock, OnStringScalar("7"));
+ EXPECT_CALL(Mock, OnKeyedItem("key_b"));
+ EXPECT_CALL(Mock, OnStringScalar("8"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey_x"));
+ EXPECT_CALL(Mock, OnStringScalar("9"));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar("max\tignat"));
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("100"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "1 2\t3\ta=5\tb=6\n"
+ "7 8\t9\tb=max\\tignat\ta=100\n";
+
+ auto config = New<TYamredDsvFormatConfig>();
+ config->HasSubkey = true;
+ config->KeyColumnNames.push_back("key_a");
+ config->KeyColumnNames.push_back("key_b");
+ config->SubkeyColumnNames.push_back("subkey_x");
+
+ ParseYamredDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamredDsvParserTest, EmptyField)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar(""));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("0 1"));
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("b"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "\t0 1\ta=b\n";
+
+ auto config = New<TYamredDsvFormatConfig>();
+ config->HasSubkey = true;
+ config->KeyColumnNames.push_back("key");
+ config->SubkeyColumnNames.push_back("subkey");
+
+ ParseYamredDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamredDsvParserTest, Escaping)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("\t"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("0\n1"));
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("\tb\nc"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "\\t\t0\\n1\ta=\\tb\\nc\n";
+
+ auto config = New<TYamredDsvFormatConfig>();
+ config->HasSubkey = true;
+ config->EnableEscaping = true;
+ config->KeyColumnNames.push_back("key");
+ config->SubkeyColumnNames.push_back("subkey");
+
+ ParseYamredDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYamredDsvParserTest, Lenval)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("a"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("bc"));
+ EXPECT_CALL(Mock, OnKeyedItem("d"));
+ EXPECT_CALL(Mock, OnStringScalar("e"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x01\x00\x00\x00" "a"
+ "\x02\x00\x00\x00" "bc"
+ "\x03\x00\x00\x00" "d=e"
+ , 3 * 4 + 1 + 2 + 3
+ );
+
+ auto config = New<TYamredDsvFormatConfig>();
+ config->Lenval = true;
+ config->HasSubkey = true;
+ config->KeyColumnNames.push_back("key");
+ config->SubkeyColumnNames.push_back("subkey");
+
+ ParseYamredDsv(input, &Mock, config);
+}
+
+TEST(TYamredDsvParserTest, EOM)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar("a"));
+ EXPECT_CALL(Mock, OnKeyedItem("subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("bc"));
+ EXPECT_CALL(Mock, OnKeyedItem("d"));
+ EXPECT_CALL(Mock, OnStringScalar("e"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = TString(
+ "\x01\x00\x00\x00" "a"
+ "\x02\x00\x00\x00" "bc"
+ "\x03\x00\x00\x00" "d=e"
+ "\xfb\xff\xff\xff" "\x01\x00\x00\x00\x00\x00\x00\x00"
+ , 3 * 4 + 1 + 2 + 3 + 12
+ );
+
+ auto config = New<TYamredDsvFormatConfig>();
+ config->Lenval = true;
+ config->EnableEom = true;
+ config->HasSubkey = true;
+ config->KeyColumnNames.push_back("key");
+ config->SubkeyColumnNames.push_back("subkey");
+
+ ParseYamredDsv(input, &Mock, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/yamred_dsv_writer_ut.cpp b/yt/yt/client/unittests/yamred_dsv_writer_ut.cpp
new file mode 100644
index 0000000000..3537702fe1
--- /dev/null
+++ b/yt/yt/client/unittests/yamred_dsv_writer_ut.cpp
@@ -0,0 +1,425 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+#include <yt/yt/client/table_client/name_table.h>
+
+#include <yt/yt/client/formats/yamred_dsv_writer.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <util/string/vector.h>
+
+#include <cstdio>
+
+
+namespace NYT::NFormats {
+namespace {
+
+typedef TVector<TString> VectorStrok;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NTableClient;
+
+class TSchemalessWriterForYamredDsvTest
+ : public ::testing::Test
+{
+protected:
+ TNameTablePtr NameTable_;
+ TYamredDsvFormatConfigPtr Config_;
+ IUnversionedRowsetWriterPtr Writer_;
+
+ TStringStream OutputStream_;
+
+ int KeyAId_;
+ int KeyBId_;
+ int KeyCId_;
+ int ValueXId_;
+ int ValueYId_;
+ int TableIndexId_;
+ int RangeIndexId_;
+ int RowIndexId_;
+
+ TSchemalessWriterForYamredDsvTest()
+ {
+ NameTable_ = New<TNameTable>();
+ KeyAId_ = NameTable_->RegisterName("key_a");
+ KeyBId_ = NameTable_->RegisterName("key_b");
+ KeyCId_ = NameTable_->RegisterName("key_c");
+ ValueXId_ = NameTable_->RegisterName("value_x");
+ ValueYId_ = NameTable_->RegisterName("value_y");
+ TableIndexId_ = NameTable_->RegisterName(TableIndexColumnName);
+ RowIndexId_ = NameTable_->RegisterName(RowIndexColumnName);
+ RangeIndexId_ = NameTable_->RegisterName(RangeIndexColumnName);
+ Config_ = New<TYamredDsvFormatConfig>();
+ }
+
+ void CreateStandardWriter(TControlAttributesConfigPtr controlAttributes = New<TControlAttributesConfig>())
+ {
+ Writer_ = CreateSchemalessWriterForYamredDsv(
+ Config_,
+ NameTable_,
+ CreateAsyncAdapter(static_cast<IOutputStream*>(&OutputStream_)),
+ false, /* enableContextSaving */
+ controlAttributes,
+ 0 /* keyColumnCount */);
+ }
+
+ // Splits output into key and sorted vector of values that are entries of the last YAMR column.
+ // Returns true if success (there are >= 2 values after splitting by field separator), otherwise false.
+ bool ExtractKeyValue(TString output, TString& key, VectorStrok& value, char fieldSeparator = '\t')
+ {
+ char delimiter[2] = {fieldSeparator, 0};
+ // Splitting by field separator.
+ value = SplitString(output, delimiter, 0 /* maxFields */, KEEP_EMPTY_TOKENS);
+ // We should at least have key and the rest of values.
+ if (value.size() < 2)
+ return false;
+ key = value[0];
+ value.erase(value.begin());
+ std::sort(value.begin(), value.end());
+ return true;
+ }
+
+ // The same function as previous, version with subkey.
+ bool ExtractKeySubkeyValue(TString output, TString& key, TString& subkey, VectorStrok& value, char fieldSeparator = '\t')
+ {
+ char delimiter[2] = {fieldSeparator, 0};
+ // Splitting by field separator.
+ value = SplitString(output, delimiter, 0 /* maxFields */, KEEP_EMPTY_TOKENS);
+ // We should at least have key, subkey and the rest of values.
+ if (value.size() < 3)
+ return false;
+ key = value[0];
+ subkey = value[1];
+ value.erase(value.begin(), value.end());
+ std::sort(value.begin(), value.end());
+ return true;
+ }
+
+ // Compares output and expected output ignoring the order of entries in YAMR value column.
+ void CompareKeyValue(TString output, TString expected, char recordSeparator = '\n', char fieldSeparator = '\t')
+ {
+ char delimiter[2] = {recordSeparator, 0};
+ VectorStrok outputRows = SplitString(output, delimiter, 0 /* maxFields */ , KEEP_EMPTY_TOKENS);
+ VectorStrok expectedRows = SplitString(expected, delimiter, 0 /* maxFields */, KEEP_EMPTY_TOKENS);
+ EXPECT_EQ(outputRows.size(), expectedRows.size());
+ // Since there is \n after each row, there will be an extra empty string in both vectors.
+ EXPECT_EQ(outputRows.back(), "");
+ ASSERT_EQ(expectedRows.back(), "");
+ outputRows.pop_back();
+ expectedRows.pop_back();
+
+ TString outputKey;
+ TString expectedKey;
+ VectorStrok outputValue;
+ VectorStrok expectedValue;
+ for (int rowIndex = 0; rowIndex < static_cast<int>(outputRows.size()); rowIndex++) {
+ EXPECT_TRUE(ExtractKeyValue(outputRows[rowIndex], outputKey, outputValue, fieldSeparator));
+ ASSERT_TRUE(ExtractKeyValue(expectedRows[rowIndex], expectedKey, expectedValue, fieldSeparator));
+ EXPECT_EQ(outputKey, expectedKey);
+ EXPECT_EQ(outputValue, expectedValue);
+ }
+ }
+
+ // The same function as previous, version with subkey.
+ void CompareKeySubkeyValue(TString output, TString expected, char recordSeparator = '\n', char fieldSeparator = '\t')
+ {
+ char delimiter[2] = {recordSeparator, 0};
+ VectorStrok outputRows = SplitString(output, delimiter, 0 /* maxFields */ , KEEP_EMPTY_TOKENS);
+ VectorStrok expectedRows = SplitString(expected, delimiter, 0 /* maxFields */, KEEP_EMPTY_TOKENS);
+ EXPECT_EQ(outputRows.size(), expectedRows.size());
+ // Since there is \n after each row, there will be an extra empty string in both vectors.
+ EXPECT_EQ(outputRows.back(), "");
+ ASSERT_EQ(expectedRows.back(), "");
+ outputRows.pop_back();
+ expectedRows.pop_back();
+
+ TString outputKey;
+ TString expectedKey;
+ TString outputSubkey;
+ TString expectedSubkey;
+ VectorStrok outputValue;
+ VectorStrok expectedValue;
+ for (int rowIndex = 0; rowIndex < static_cast<int>(outputRows.size()); rowIndex++) {
+ EXPECT_TRUE(ExtractKeySubkeyValue(outputRows[rowIndex], outputKey, outputSubkey, outputValue, fieldSeparator));
+ ASSERT_TRUE(ExtractKeySubkeyValue(expectedRows[rowIndex], expectedKey, expectedSubkey, expectedValue, fieldSeparator));
+ EXPECT_EQ(outputKey, expectedKey);
+ EXPECT_EQ(outputSubkey, expectedSubkey);
+ EXPECT_EQ(outputValue, expectedValue);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, Simple)
+{
+ Config_->KeyColumnNames.emplace_back("key_a");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("a1", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("x", ValueXId_));
+ row1.AddValue(MakeUnversionedSentinelValue(EValueType::Null, ValueYId_));
+
+ // Ignore system columns.
+ row1.AddValue(MakeUnversionedInt64Value(2, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(42, RowIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(1, RangeIndexId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("a2", KeyAId_));
+ row2.AddValue(MakeUnversionedStringValue("y", ValueYId_));
+ row2.AddValue(MakeUnversionedStringValue("b", KeyBId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow(), row2.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "a1\tvalue_x=x\n"
+ "a2\tvalue_y=y\tkey_b=b\n";
+
+ TString output = OutputStream_.Str();
+
+ CompareKeyValue(expectedOutput, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, SimpleWithSubkey)
+{
+ Config_->HasSubkey = true;
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->KeyColumnNames.emplace_back("key_b");
+ Config_->SubkeyColumnNames.emplace_back("key_c");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("b1", KeyBId_));
+ row1.AddValue(MakeUnversionedStringValue("c", KeyCId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+ row2.AddValue(MakeUnversionedStringValue("b2", KeyBId_));
+ row2.AddValue(MakeUnversionedStringValue("c", KeyCId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow(), row2.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput =
+ "a b1\tc\t\n"
+ "a b2\tc\t\n";
+
+ TString output = OutputStream_.Str();
+
+ CompareKeySubkeyValue(expectedOutput, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, Lenval)
+{
+ Config_->Lenval = true;
+ Config_->HasSubkey = true;
+ Config_->EnableTableIndex = true;
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->KeyColumnNames.emplace_back("key_b");
+ Config_->SubkeyColumnNames.emplace_back("key_c");
+
+ auto controlAttributes = New<TControlAttributesConfig>();
+ controlAttributes->EnableTableIndex = true;
+ controlAttributes->EnableRowIndex = true;
+ controlAttributes->EnableRangeIndex = true;
+ CreateStandardWriter(controlAttributes);
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("b1", KeyBId_));
+ row1.AddValue(MakeUnversionedStringValue("c", KeyCId_));
+ row1.AddValue(MakeUnversionedStringValue("x", ValueXId_));
+
+ row1.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(23, RangeIndexId_));
+ row1.AddValue(MakeUnversionedInt64Value(17, RowIndexId_));
+
+ TUnversionedRowBuilder row2;
+ row2.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+ row2.AddValue(MakeUnversionedStringValue("b2", KeyBId_));
+ row2.AddValue(MakeUnversionedStringValue("c", KeyCId_));
+
+ row2.AddValue(MakeUnversionedInt64Value(42, TableIndexId_));
+ row2.AddValue(MakeUnversionedInt64Value(23, RangeIndexId_));
+ row2.AddValue(MakeUnversionedInt64Value(18, RowIndexId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow(), row2.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput = TString(
+ "\xff\xff\xff\xff" "\x2a\x00\x00\x00" // Table index.
+ "\xfd\xff\xff\xff" "\x17\x00\x00\x00" // Range index.
+ "\xfc\xff\xff\xff" "\x11\x00\x00\x00\x00\x00\x00\x00" // Row index.
+
+ "\x04\x00\x00\x00" "a b1"
+ "\x01\x00\x00\x00" "c"
+ "\x09\x00\x00\x00" "value_x=x"
+
+ "\x04\x00\x00\x00" "a b2"
+ "\x01\x00\x00\x00" "c"
+ "\x00\x00\x00\x00" "",
+
+ 13 * 4 + 4 + 1 + 9 + 4 + 1 + 0
+ );
+
+ TString output = OutputStream_.Str();
+ EXPECT_EQ(expectedOutput, output)
+ << "expected length: " << expectedOutput.length()
+ << ", "
+ << "actual length: " << output.length();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, Escaping)
+{
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->KeyColumnNames.emplace_back("key_b");
+ int columnWithEscapedNameId = NameTable_->GetIdOrRegisterName("value\t_t");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("a\n", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("\nb\t", KeyBId_));
+ row1.AddValue(MakeUnversionedStringValue("\nva\\lue\t", columnWithEscapedNameId));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput = "a\\n \\nb\\t\tvalue\\t_t=\\nva\\\\lue\\t\n";
+ TString output = OutputStream_.Str();
+
+ EXPECT_EQ(expectedOutput, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, SkippedKey)
+{
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->KeyColumnNames.emplace_back("key_b");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("b", KeyBId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_FALSE(Writer_->Write(rows));
+
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, SkippedSubkey)
+{
+ Config_->HasSubkey = true;
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->SubkeyColumnNames.emplace_back("key_c");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_FALSE(Writer_->Write(rows));
+
+ EXPECT_THROW(Writer_->Close()
+ .Get()
+ .ThrowOnError(), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, NonStringValues)
+{
+ Config_->HasSubkey = true;
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->SubkeyColumnNames.emplace_back("key_c");
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row;
+ row.AddValue(MakeUnversionedInt64Value(-42, KeyAId_));
+ row.AddValue(MakeUnversionedUint64Value(18, KeyCId_));
+ row.AddValue(MakeUnversionedBooleanValue(true, KeyBId_));
+ row.AddValue(MakeUnversionedDoubleValue(3.14, ValueXId_));
+ row.AddValue(MakeUnversionedStringValue("yt", ValueYId_));
+
+ std::vector<TUnversionedRow> rows = { row.GetRow() };
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput = "-42\t18\tkey_b=true\tvalue_x=3.14\tvalue_y=yt\n";
+ TString output = OutputStream_.Str();
+
+ EXPECT_EQ(expectedOutput, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSchemalessWriterForYamredDsvTest, ErasingSubkeyColumnsWhenHasSubkeyIsFalse)
+{
+ Config_->KeyColumnNames.emplace_back("key_a");
+ Config_->SubkeyColumnNames.emplace_back("key_b");
+ // Config->HasSubkey = false by default.
+ CreateStandardWriter();
+
+ TUnversionedRowBuilder row1;
+ row1.AddValue(MakeUnversionedStringValue("a", KeyAId_));
+ row1.AddValue(MakeUnversionedStringValue("b", KeyBId_));
+ row1.AddValue(MakeUnversionedStringValue("c", KeyCId_));
+ row1.AddValue(MakeUnversionedStringValue("x", ValueXId_));
+
+ std::vector<TUnversionedRow> rows = {row1.GetRow()};
+
+ EXPECT_EQ(true, Writer_->Write(rows));
+ Writer_->Close()
+ .Get()
+ .ThrowOnError();
+
+ TString expectedOutput = "a\tkey_c=c\tvalue_x=x\n";
+ TString output = OutputStream_.Str();
+
+ EXPECT_EQ(expectedOutput, output);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFormats
diff --git a/yt/yt/client/unittests/ypath_ut.cpp b/yt/yt/client/unittests/ypath_ut.cpp
new file mode 100644
index 0000000000..68f5136e12
--- /dev/null
+++ b/yt/yt/client/unittests/ypath_ut.cpp
@@ -0,0 +1,951 @@
+#include "key_helpers.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/ypath/rich.h>
+
+#include <yt/yt/client/chunk_client/helpers.h>
+
+#include <yt/yt/client/table_client/comparator.h>
+#include <yt/yt/client/table_client/key_bound.h>
+
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_service.h>
+
+#include <util/string/vector.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+using namespace NYPath;
+using namespace NChunkClient;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathTest
+ : public ::testing::Test
+{
+public:
+ IYPathServicePtr RootService;
+
+ void SetUp() override
+ {
+ RootService = GetEphemeralNodeFactory()->CreateMap();
+ }
+
+ static TYsonString TextifyYson(const TYsonString& data)
+ {
+ return ConvertToYsonString(data, NYson::EYsonFormat::Text);
+ }
+
+ void Set(const TYPath& path, const TString& value)
+ {
+ SyncYPathSet(RootService, path, TYsonString(value));
+ }
+
+ void Remove(const TYPath& path)
+ {
+ SyncYPathRemove(RootService, path);
+ }
+
+ TYsonString Get(const TYPath& path)
+ {
+ return TextifyYson(SyncYPathGet(RootService, path));
+ }
+
+ std::vector<TString> List(const TYPath& path)
+ {
+ return SyncYPathList(RootService, path);
+ }
+
+ void Check(const TYPath& path, const TString& expected)
+ {
+ TYsonString output = Get(path);
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(TYsonString(expected)),
+ ConvertToNode(output)));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TYPathTest, MapModification)
+{
+ Set("/map", "{hello=world; list=[0;a;{}]; n=1}");
+
+ Set("/map/hello", "not_world");
+ Check("", "{map={hello=not_world;list=[0;a;{}];n=1}}");
+
+ Set("/map/list/2/some", "value");
+ Check("", "{map={hello=not_world;list=[0;a;{some=value}];n=1}}");
+
+ Remove("/map/n");
+ Check("", "{map={hello=not_world;list=[0;a;{some=value}]}}");
+
+ Set("/map/list", "[]");
+ Check("", "{map={hello=not_world;list=[]}}");
+
+ Remove("/map/hello");
+ Check("", "{map={list=[]}}");
+
+ Remove("/map");
+ Check("", "{}");
+}
+
+TEST_F(TYPathTest, ListModification)
+{
+ Set("/list", "[1;2;3]");
+ Check("", "{list=[1;2;3]}");
+ Check("/list", "[1;2;3]");
+ Check("/list/0", "1");
+ Check("/list/1", "2");
+ Check("/list/2", "3");
+ Check("/list/-1", "3");
+ Check("/list/-2", "2");
+ Check("/list/-3", "1");
+
+ Set("/list/end", "4");
+ Check("/list", "[1;2;3;4]");
+
+ Set("/list/end", "5");
+ Check("/list", "[1;2;3;4;5]");
+
+ Set("/list/2", "100");
+ Check("/list", "[1;2;100;4;5]");
+
+ Set("/list/-2", "3");
+ Check("/list", "[1;2;100;3;5]");
+
+ Remove("/list/4");
+ Check("/list", "[1;2;100;3]");
+
+ Remove("/list/2");
+ Check("/list", "[1;2;3]");
+
+ Remove("/list/-1");
+ Check("/list", "[1;2]");
+
+ Set("/list/before:0", "0");
+ Check("/list", "[0;1;2]");
+
+ Set("/list/after:1", "3");
+ Check("/list", "[0;1;3;2]");
+
+ Set("/list/after:-1", "4");
+ Check("/list", "[0;1;3;2;4]");
+
+ Set("/list/before:-1", "5");
+ Check("/list", "[0;1;3;2;5;4]");
+
+ Set("/list/begin", "6");
+ Check("/list", "[6;0;1;3;2;5;4]");
+}
+
+TEST_F(TYPathTest, ListReassignment)
+{
+ Set("/list", "[a;b;c]");
+ Set("/list", "[1;2;3]");
+
+ Check("", "{list=[1;2;3]}");
+}
+
+TEST_F(TYPathTest, Clear)
+{
+ Set("/my", "{list=<type=list>[1;2];map=<type=map>{a=1;b=2}}");
+
+ Remove("/my/list/*");
+ Check("/my/list", "<type=list>[]");
+ Check("/my/list/@", "{type=list}");
+
+ Remove("/my/map/*");
+ Check("/my/map", "<type=map>{}");
+ Check("/my/map/@", "{type=map}");
+}
+
+TEST_F(TYPathTest, Ls)
+{
+ Set("", "{a={x1={y1=1}};b={x2={y2=2}};c={x3={y3=3}};d={x4={y4=4}}}");
+
+ Remove("/b");
+ Set("/e", "5");
+
+ auto result = List("");
+ std::sort(result.begin(), result.end());
+
+ std::vector<TString> expected;
+ expected.push_back("a");
+ expected.push_back("c");
+ expected.push_back("d");
+ expected.push_back("e");
+
+ EXPECT_EQ(expected, result);
+}
+
+TEST_F(TYPathTest, LsOnUnsupportedNodes)
+{
+ EXPECT_ANY_THROW({
+ Set("list", "[1; 2; 3; 4]");
+ List("list");
+ });
+
+ EXPECT_ANY_THROW({
+ Set("str", "aaa");
+ List("str");
+ });
+
+ EXPECT_ANY_THROW({
+ Set("int", "42");
+ List("int");
+ });
+
+ EXPECT_ANY_THROW({
+ Set("double", "3.14");
+ List("double");
+ });
+
+ EXPECT_ANY_THROW({
+ Set("entity", "#");
+ List("entity");
+ });
+}
+
+TEST_F(TYPathTest, Attributes)
+{
+ Set("/root", "<attr=100;mode=rw> {nodes=[1; 2]}");
+ Check("/root/@", "{attr=100;mode=rw}");
+ Check("/root/@attr", "100");
+
+ Set("/root/value", "<>500");
+ Check("/root/value", "500");
+
+ Remove("/root/@*");
+ Check("/root/@", "{}");
+
+ Remove("/root/nodes");
+ Remove("/root/value");
+ Check("", "{root={}}");
+
+ Set("/root/2", "<author=ignat> #");
+ Check("", "{root={\"2\"=<author=ignat>#}}");
+ Check("/root/2/@", "{author=ignat}");
+ Check("/root/2/@author", "ignat");
+
+ // note: empty attributes are shown when nested
+ Set("/root/3", "<dir=<file=<>-100>#>#");
+ Check("/root/3/@", "{dir=<file=<>-100>#}");
+ Check("/root/3/@dir/@", "{file=<>-100}");
+ Check("/root/3/@dir/@file", "<>-100");
+ Check("/root/3/@dir/@file/@", "{}");
+}
+
+TEST_F(TYPathTest, RemoveAll)
+{
+ // from map
+ Set("/map", "{foo=bar;key=value}");
+ Remove("/map/*");
+ Check("/map", "{}");
+
+ // from list
+ Set("/list", "[10;20;30]");
+ Remove("/list/*");
+ Check("/list", "[]");
+
+ // from attributes
+ Set("/attr", "<foo=bar;key=value>42");
+ Remove("/attr/@*");
+ Check("/attr/@", "{}");
+}
+
+TEST_F(TYPathTest, InvalidCases)
+{
+ Set("/root", "{}");
+
+ // exception when setting attributes
+ EXPECT_ANY_THROW(Set("/root/some", "[10; {key=value;foo=<attr=42a>bar}]"));
+ Check("/root", "{}");
+
+ EXPECT_ANY_THROW(Set("/a/b", "1")); // /a must exist
+ EXPECT_ANY_THROW(Set("a", "{}")); // must start with '/'
+ EXPECT_ANY_THROW(Set("/root/", "{}")); // cannot end with '/'
+ EXPECT_ANY_THROW(Set("", "[]")); // change the type of root
+ EXPECT_ANY_THROW(Remove("")); // remove the root
+ EXPECT_ANY_THROW(Get("/b")); // get non-existent path
+
+ // get non-existent attribute from non-existent node
+ EXPECT_ANY_THROW(Get("/b/@some"));
+
+ // get non-existent attribute from existent node
+ EXPECT_ANY_THROW({
+ Set("/c", "{}");
+ Get("/c/@some");
+ });
+
+ // remove non-existing child
+ EXPECT_ANY_THROW(Remove("/a"));
+}
+
+TEST_F(TYPathTest, ParseRichYPath1)
+{
+ auto path = NYPath::TRichYPath::Parse("<a=b>//home/ignat{a,b}[1:2]");
+ EXPECT_EQ(path.GetPath(), "//home/ignat");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf("{a=b;columns=[a;b]; ranges=[{upper_limit={key=[2]};lower_limit={key=[1]}}]}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath2)
+{
+ auto path = NYPath::TRichYPath::Parse("<a=b>//home");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf("{a=b}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath3)
+{
+ auto path = NYPath::TRichYPath::Parse("//home");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(EmptyAttributes())));
+}
+
+TEST_F(TYPathTest, ParseRichYPath4)
+{
+ auto path = NYPath::TRichYPath::Parse("//home[:]");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf("{ranges=[{}]}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath5)
+{
+ auto path = NYPath::TRichYPath::Parse("//home[(x, y):(a, b)]");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf(
+ "{ranges=[{lower_limit={key=[x;y]};upper_limit={key=[a;b]}}]}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath6)
+{
+ auto path = NYPath::TRichYPath::Parse("//home[#1:#2,x:y]");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf(
+ "{ranges=["
+ "{lower_limit={row_index=1};upper_limit={row_index=2}};"
+ "{lower_limit={key=[x]};upper_limit={key=[y]}}"
+ "]}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath7)
+{
+ auto path = NYPath::TRichYPath::Parse("//home[x:#1000]");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf(
+ "{ranges=["
+ "{lower_limit={key=[x]};upper_limit={row_index=1000}};"
+ "]}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath8)
+{
+ auto path = NYPath::TRichYPath::Parse(" <a=b> //home");
+ EXPECT_EQ(path.GetPath(), "//home");
+ EXPECT_TRUE(
+ AreNodesEqual(
+ ConvertToNode(path.Attributes()),
+ ConvertToNode(TYsonString(TStringBuf("{a=b}")))));
+}
+
+TEST_F(TYPathTest, ParseRichYPath9)
+{
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("@home"),
+ std::exception,
+ "does not start with a valid root-designator");
+}
+
+TEST_F(TYPathTest, ParseRichYPath10)
+{
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse(" \n <a=b>\n//home"),
+ std::exception,
+ "does not start with a valid root-designator");
+}
+
+TEST_F(TYPathTest, ParseRichYPath11)
+{
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse(" \n//home"),
+ std::exception,
+ "does not start with a valid root-designator");
+}
+
+TEST_F(TYPathTest, IgnoreAmpersand1)
+{
+ Set("&/a", "b");
+ Check("/a", "b");
+ Check("&/a", "b");
+}
+
+TEST_F(TYPathTest, IgnoreAmpersand2)
+{
+ Set("/list", "[]");
+ Set("/list&/end", "0");
+ Check("/list", "[0]");
+}
+
+TEST_F(TYPathTest, IgnoreAmpersand3)
+{
+ Set("/map", "{}");
+ Set("/map/@attr", "value");
+ Check("/map&/@attr", "value");
+}
+
+TEST_F(TYPathTest, Cluster)
+{
+ {
+ auto path = TRichYPath::Parse("mycluster://home/mytable");
+ EXPECT_EQ("//home/mytable", path.GetPath());
+ EXPECT_EQ("mycluster", path.GetCluster());
+ }
+
+ {
+ auto path = TRichYPath::Parse(" <> mycluster://home/mytable");
+ EXPECT_EQ("//home/mytable", path.GetPath());
+ EXPECT_EQ("mycluster", path.GetCluster());
+ }
+
+ {
+ auto path = TRichYPath::Parse("long-cluster-name_with_underscores://home/long-table-name");
+ EXPECT_EQ("//home/long-table-name", path.GetPath());
+ EXPECT_EQ("long-cluster-name_with_underscores", path.GetCluster());
+ }
+
+ {
+ auto path = TRichYPath::Parse("//home/mytable");
+ EXPECT_EQ("//home/mytable", path.GetPath());
+ EXPECT_FALSE(path.GetCluster().has_value());
+ }
+
+ {
+ auto path = TRichYPath::Parse("//path:with:colons/my:table");
+ EXPECT_EQ("//path:with:colons/my:table", path.GetPath());
+ EXPECT_FALSE(path.GetCluster().has_value());
+ }
+
+ {
+ auto path = TRichYPath::Parse("//path-with-dashes/my-table");
+ EXPECT_EQ("//path-with-dashes/my-table", path.GetPath());
+ EXPECT_FALSE(path.GetCluster().has_value());
+ }
+
+ {
+ // NB: There doesn't seem to be a feasible way to check this without interpreting the actual tokens.
+ auto path = TRichYPath::Parse("replica://primary://tmp/queue");
+ EXPECT_EQ("//primary://tmp/queue", path.GetPath());
+ EXPECT_EQ("replica", path.GetCluster());
+ }
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("bad+cluster!name://home/mytable"),
+ std::exception,
+ "illegal symbol");
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(TRichYPath::Parse("://home/mytable"),
+ std::exception,
+ "cannot be empty");
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("localhost:1234://queue"),
+ std::exception,
+ "does not start with a valid root-designator");
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("pythia:markov://queue"),
+ std::exception,
+ "does not start with a valid root-designator");
+}
+
+TEST_F(TYPathTest, NewReadRanges)
+{
+ TComparator comparatorAsc1({ESortOrder::Ascending});
+ TComparator comparatorAsc2({ESortOrder::Ascending, ESortOrder::Ascending});
+ TComparator comparatorDesc1({ESortOrder::Descending});
+ TComparator comparatorDesc2({ESortOrder::Descending, ESortOrder::Descending});
+
+ auto makeRow = [&] (const std::vector<int> values) {
+ std::vector<TUnversionedValue> unversionedValues;
+ unversionedValues.reserve(values.size());
+ for (int value : values) {
+ unversionedValues.emplace_back(MakeUnversionedInt64Value(value));
+ }
+ return MakeRow(std::move(unversionedValues));
+ };
+
+ {
+ std::vector<TReadRange> ranges(1);
+ // No ranges.
+ EXPECT_EQ(
+ ranges,
+ TRichYPath::Parse("//t").GetNewRanges());
+ }
+ {
+ // Some row index ranges in short form.
+ std::vector<TReadRange> ranges(2);
+ ranges[0].LowerLimit().SetRowIndex(1);
+ ranges[0].UpperLimit().SetRowIndex(2);
+
+ ranges[1].LowerLimit().SetRowIndex(3);
+
+ EXPECT_EQ(
+ ranges,
+ TRichYPath::Parse("//t[#1, #3:]").GetNewRanges());
+ }
+
+ {
+ // Key range without provided comparator is not allowed.
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("//t[foo]").GetNewRanges(),
+ std::exception,
+ "for an unsorted object");
+ }
+
+ {
+ // Some key ranges in short form.
+ std::vector<TReadRange> ranges(2);
+ // (123,456):789
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({123, 456});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({789});
+ // (424,242)
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({424, 242});
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({424, 242});
+
+ auto ypath = TRichYPath::Parse("//t[(123,456):789, (424,242)]");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorAsc2));
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorDesc2));
+ }
+
+ {
+ // Same key ranges in short form, but in context of shorter key prefix.
+ // Inclusiveness of some key bounds is toggled.
+ std::vector<TReadRange> ranges(2);
+ // (123,456):789
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() > makeRow({123});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({789});
+ // (424,242); in ascending case it transforms into empty range.
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() > makeRow({});
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({});
+
+ auto ypath = TRichYPath::Parse("//t[(123,456):789, (424,242)]");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorAsc1));
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ ypath.GetNewRanges(comparatorDesc1),
+ std::exception,
+ "Read limit key cannot be longer");
+ }
+
+ {
+ // Short key in context of a longer key prefix.
+ std::vector<TReadRange> ranges(2);
+ // (123):456
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({123});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({456});
+ // (789)
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({789});
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({789});
+
+ auto ypath = TRichYPath::Parse("//t[(123):456, (789)]");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorAsc2));
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorDesc2));
+ }
+
+ {
+ // Some row index ranges in full form.
+ std::vector<TReadRange> ranges(2);
+ // #42:#57
+ ranges[0].LowerLimit().SetRowIndex(42);
+ ranges[0].UpperLimit().SetRowIndex(57);
+ // #123
+ ranges[1].LowerLimit().SetRowIndex(123);
+ ranges[1].UpperLimit().SetRowIndex(124);
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ "{lower_limit={row_index=42};upper_limit={row_index=57}};"
+ "{exact={row_index=123}};"
+ "]>//t");
+
+ EXPECT_EQ(ranges, ypath.GetNewRanges());
+ }
+
+ {
+ // Lower limit simultaneously with exact.
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("<ranges=[{lower_limit={};exact={}}]>//t").GetNewRanges(),
+ std::exception,
+ "Exact limit cannot be specified simultaneously");
+ }
+
+ {
+ // Key bound in exact limit.
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse(R"(<ranges=[{exact={key_bound=["<="; []]}}]>//t)").GetNewRanges(),
+ std::exception,
+ "Key bound cannot be specified in exact");
+ }
+
+ {
+ // Key and key bound together, first case is regular for backward-compatible serialization,
+ // second contains incorrect key which should be ignored.
+ std::vector<TReadRange> ranges(2);
+ // (42):
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42});
+
+ // (57):(23)
+
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({57});
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({23});
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ R"({lower_limit={key_bound=[">=";[42]]; key=[42]}};)"
+ R"({lower_limit={key_bound=[">=";[57]]; key=[123;asd]}; upper_limit={key_bound=["<"; [23]]; key=[]}};)"
+ "]>//t");
+
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc1));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorDesc1));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc2));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorDesc2));
+ }
+
+ {
+ // Key with sentinels (ok for ascending and not ok for descending).
+ std::vector<TReadRange> ranges(1);
+ // (42):(57,<type=max>#)
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({57});
+
+ auto ypath = TRichYPath::Parse("<ranges=[{lower_limit={key=[42]};upper_limit={key=[57;<type=max>#]}}]>//tmp/t");
+
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc2));
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ ypath.GetNewRanges(comparatorDesc2),
+ std::exception,
+ "Sentinel values are not allowed");
+ }
+
+ {
+ // Key with sentinels (ok for ascending and not ok for descending).
+ std::vector<TReadRange> ranges(1);
+ // (42):(57,<type=max>#)
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({57});
+
+ auto ypath = TRichYPath::Parse("<ranges=[{lower_limit={key=[42]};upper_limit={key=[57;<type=max>#]}}]>//tmp/t");
+
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc2));
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ ypath.GetNewRanges(comparatorDesc2),
+ std::exception,
+ "Sentinel values are not allowed");
+ }
+
+ {
+ // Correct usage of key bounds for previous case.
+ std::vector<TReadRange> ranges(1);
+ // >=(42):<=(57)
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({57});
+
+ auto ypath = TRichYPath::Parse(
+ R"(<ranges=[{lower_limit={key_bound=[">=";[42]]};upper_limit={key_bound=["<=";[57]];}}]>//tmp/t)");
+
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc2));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorDesc2));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorAsc1));
+ EXPECT_EQ(ranges, ypath.GetNewRanges(comparatorDesc1));
+ }
+
+ {
+ std::vector<TReadRange> ranges(1);
+ // >=(42,57) with shorter comparator, i.e not ok both for ascending and descending sort order.
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42, 57});
+
+ auto ypath = TRichYPath::Parse(R"(<ranges=[{lower_limit={key_bound=[">="; [42;57]]};}]>//tmp/t)");
+
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ ypath.GetNewRanges(comparatorAsc1),
+ std::exception,
+ "Key bound length must not exceed");
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ ypath.GetNewRanges(comparatorDesc1),
+ std::exception,
+ "Key bound length must not exceed");
+ }
+
+ {
+ // Exact consisting of multiple independent selectors.
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("<ranges=[{exact={row_index=42;chunk_index=23}}]>//tmp/t").GetNewRanges(),
+ std::exception,
+ "Exact read limit must have exactly one independent selector");
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("<ranges=[{exact={tablet_index=42;chunk_index=23}}]>//tmp/t").GetNewRanges(),
+ std::exception,
+ "Exact read limit must have exactly one independent selector");
+ EXPECT_THROW_MESSAGE_HAS_SUBSTR(
+ TRichYPath::Parse("<ranges=[{exact={tablet_index=42;row_index=12;chunk_index=23}}]>//tmp/t").GetNewRanges(),
+ std::exception,
+ "Exact read limit must have exactly one independent selector");
+ }
+ {
+ // Exact consisting of tablet_index or tablet_index + row_index (which is OK).
+ std::vector<TReadRange> ranges(2);
+ // tablet_index = 2
+ ranges[0].LowerLimit().SetTabletIndex(2);
+ ranges[0].UpperLimit().SetTabletIndex(3);
+ // tablet_index = 2; row_index = 42;
+ ranges[1].LowerLimit().SetTabletIndex(2);
+ ranges[1].LowerLimit().SetRowIndex(42);
+ ranges[1].UpperLimit().SetTabletIndex(2);
+ ranges[1].UpperLimit().SetRowIndex(43);
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ "{exact={tablet_index=2}};"
+ "{exact={tablet_index=2;row_index=42}};"
+ "]>//tmp/t");
+ }
+
+ {
+ // Short key in context of a longer key prefix, long key in context of a shorter key prefix
+ // and a key of proper length but with sentinels; long form. With ascending comparator
+ // we should replace latter two ranges with an empty range.
+ std::vector<TReadRange> ranges(3);
+ // [(42)]
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeRow({42});
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeRow({42});
+ // [(42, 43, 44)]
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() > makeRow({});
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({});
+ // [(42, <type=max>#)]
+ ranges[2].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() > makeRow({});
+ ranges[2].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() < makeRow({});
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ "{exact={key=[42];}};"
+ "{exact={key=[42;43;44];}};"
+ "{exact={key=[42;<type=max>#];}};"
+ "]>//tmp/t");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparatorAsc2));
+ }
+}
+
+TEST_F(TYPathTest, RangesTypeHintsInt64)
+{
+ // Verify that int64 key in YPath can be implicitly converted to uint64 and double.
+
+ TComparator comparator({ESortOrder::Ascending, ESortOrder::Ascending});
+
+ auto makeMixedRow = [&] (ui64 first, double second) {
+ std::vector<TUnversionedValue> unversionedValues;
+ unversionedValues.emplace_back(MakeUnversionedUint64Value(first));
+ unversionedValues.emplace_back(MakeUnversionedDoubleValue(second));
+ return MakeRow(std::move(unversionedValues));
+ };
+
+ std::vector<TReadRange> ranges(2);
+ // [(42, 43)]
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeMixedRow(42u, 43.0);
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeMixedRow(42u, 43.0);
+ // [(39, 45)]
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeMixedRow(39u, 45.0);
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeMixedRow(39u, 45.0);
+
+ std::vector<TReadRange> smallRanges{ranges[0]};
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ "{exact={key=[42;43];}};"
+ "{exact={key=[39;45];}};"
+ "]>//tmp/t");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparator, {NTableClient::EValueType::Uint64, NTableClient::EValueType::Double}));
+
+ auto smallYpath = TRichYPath::Parse("//tmp/t[(42, 43)]");
+ EXPECT_EQ(
+ smallRanges,
+ smallYpath.GetNewRanges(comparator, {NTableClient::EValueType::Uint64, NTableClient::EValueType::Double}));
+}
+
+TEST_F(TYPathTest, RangesTypeHintsUint64)
+{
+ // Verify that uint64 key in YPath can be implicitly converted to int64 and double.
+
+ TComparator comparator({ESortOrder::Ascending, ESortOrder::Ascending});
+
+ auto makeMixedRow = [&] (i64 first, double second) {
+ std::vector<TUnversionedValue> unversionedValues;
+ unversionedValues.emplace_back(MakeUnversionedInt64Value(first));
+ unversionedValues.emplace_back(MakeUnversionedDoubleValue(second));
+ return MakeRow(std::move(unversionedValues));
+ };
+
+ std::vector<TReadRange> ranges(2);
+ // [(42, 43)]
+ ranges[0].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeMixedRow(42, 43.0);
+ ranges[0].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeMixedRow(42, 43.0);
+ // [(39, 45)]
+ ranges[1].LowerLimit().KeyBound() = TOwningKeyBound::FromRow() >= makeMixedRow(39, 45.0);
+ ranges[1].UpperLimit().KeyBound() = TOwningKeyBound::FromRow() <= makeMixedRow(39, 45.0);
+
+ std::vector<TReadRange> smallRanges{ranges[0]};
+
+ auto ypath = TRichYPath::Parse(
+ "<ranges=["
+ "{exact={key=[42u;43u];}};"
+ "{exact={key=[39u;45u];}};"
+ "]>//tmp/t");
+
+ EXPECT_EQ(
+ ranges,
+ ypath.GetNewRanges(comparator, {NTableClient::EValueType::Int64, NTableClient::EValueType::Double}));
+
+ auto smallYpath = TRichYPath::Parse("//tmp/t[(42u, 43u)]");
+ EXPECT_EQ(
+ smallRanges,
+ smallYpath.GetNewRanges(comparator, {NTableClient::EValueType::Int64, NTableClient::EValueType::Double}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRichYPathToStringTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<TString>
+{ };
+
+TEST_P(TRichYPathToStringTest, TestRichYPathToString)
+{
+ auto path = NYPath::TRichYPath::Parse(GetParam());
+ auto newPathString = ToString(path);
+ auto parsedPath = NYPath::TRichYPath::Parse(newPathString);
+ EXPECT_EQ(path.GetPath(), parsedPath.GetPath());
+ EXPECT_TRUE(path.Attributes() == parsedPath.Attributes());
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ TRichYPathToStringTest,
+ TRichYPathToStringTest,
+ ::testing::Values(
+ "//home/ignat",
+ "<a=b>//home",
+ "<a=b>//home/ignat{a,b}[1:2]",
+ "//home[#1:#2,x:y]",
+ "<a=b;c=d>//home{a,b}[(x, y):(a, b),#1:#2]",
+ "<ranges={lower_limit={chunk_index=10}}>//home/ignat/my_table{}",
+ "<a=b; columns=[key1;key2;key3]>//tmp/[0, (3, abc, true), :12u]"
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEmbeddedYPathOpsTest
+ : public ::testing::Test
+{
+public:
+ static INodePtr ParseNode(const TString& data)
+ {
+ return ConvertToNode(TYsonString(data));
+ }
+
+ static void ExpectEqual(INodePtr node, const TString& ysonString)
+ {
+ EXPECT_EQ(ConvertToYsonString(node, EYsonFormat::Text).AsStringBuf(), ysonString);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TEmbeddedYPathOpsTest, SimpleMap)
+{
+ const auto node = ParseNode(R""({foo={bar="baz"}})"");
+ ExpectEqual(GetNodeByYPath(node, "/foo/bar"), R""("baz")"");
+
+ EXPECT_THROW(GetNodeByYPath(node, "/foo/qux"), std::exception);
+ EXPECT_THROW(GetNodeByYPath(node, "/foo/bar/qux"), std::exception);
+}
+
+TEST_F(TEmbeddedYPathOpsTest, SimpleList)
+{
+ const auto node = ParseNode(R""({home={roizner={list=[100; 500; {foo=bar}; 42]}}})"");
+ ExpectEqual(GetNodeByYPath(node, "/home/roizner/list/1"), "500");
+ ExpectEqual(GetNodeByYPath(node, "/home/roizner/list/-1"), "42");
+ ExpectEqual(GetNodeByYPath(node, "/home/roizner/list/2/foo"), R""("bar")"");
+
+ EXPECT_THROW(GetNodeByYPath(node, "/home/roizner/list/4"), std::exception);
+ EXPECT_THROW(GetNodeByYPath(node, "/home/roizner/list/-5"), std::exception);
+}
+
+TEST_F(TEmbeddedYPathOpsTest, attributes)
+{
+ const auto node = ParseNode(R""({home=<account=sys>{dir1=<account=root;user_attr=<omg="embedded attributes">{foo=bar}>{};dir2={}}})"");
+ ExpectEqual(GetNodeByYPath(node, "/home/dir1/@account"), R""("root")"");
+ ExpectEqual(GetNodeByYPath(node, "/home/dir1/@user_attr/foo"), R""("bar")"");
+ ExpectEqual(GetNodeByYPath(node, "/home/dir1/@user_attr/@omg"), R""("embedded attributes")"");
+ ExpectEqual(GetNodeByYPath(node, "/home/dir1/@"), R""({"account"="root";"user_attr"=<"omg"="embedded attributes";>{"foo"="bar";};})"");
+
+ EXPECT_THROW(GetNodeByYPath(node, "/home/dir1/@user_attr/bar"), std::exception);
+ EXPECT_THROW(GetNodeByYPath(node, "/home/dir2/@account"), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/client/unittests/yson_helpers.cpp b/yt/yt/client/unittests/yson_helpers.cpp
new file mode 100644
index 0000000000..669585caf7
--- /dev/null
+++ b/yt/yt/client/unittests/yson_helpers.cpp
@@ -0,0 +1,29 @@
+#include "yson_helpers.h"
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/yson/string.h>
+
+namespace NYT {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString CanonizeYson(TStringBuf input)
+{
+ auto node = ConvertToNode(TYsonString(input));
+ auto binaryYson = ConvertToYsonString(node);
+
+ TStringStream out;
+ {
+ TYsonWriter writer(&out, NYson::EYsonFormat::Pretty);
+ ParseYsonStringBuffer(binaryYson.AsStringBuf(), EYsonType::Node, &writer);
+ }
+ return out.Str();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/client/unittests/yson_helpers.h b/yt/yt/client/unittests/yson_helpers.h
new file mode 100644
index 0000000000..3cab460345
--- /dev/null
+++ b/yt/yt/client/unittests/yson_helpers.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString CanonizeYson(TStringBuf yson);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT \ No newline at end of file
diff --git a/yt/yt/client/unittests/zookeeper_bus_ut.cpp b/yt/yt/client/unittests/zookeeper_bus_ut.cpp
new file mode 100644
index 0000000000..559f503037
--- /dev/null
+++ b/yt/yt/client/unittests/zookeeper_bus_ut.cpp
@@ -0,0 +1,165 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/zookeeper/packet.h>
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/client.h>
+#include <yt/yt/core/bus/server.h>
+
+#include <yt/yt/core/bus/tcp/config.h>
+#include <yt/yt/core/bus/tcp/client.h>
+#include <yt/yt/core/bus/tcp/server.h>
+
+#include <yt/yt/core/net/socket.h>
+
+#include <library/cpp/testing/common/network.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+
+namespace NYT::NZookeeper {
+namespace {
+
+using namespace NBus;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRefArray CreateMessage(int size)
+{
+ auto data = TSharedMutableRef::Allocate(size);
+ return TSharedRefArray(TSharedRef(data));
+}
+
+TSharedRefArray Serialize(TString message)
+{
+ return TSharedRefArray(TSharedRef::FromString(message));
+}
+
+TString Deserialize(TSharedRefArray message)
+{
+ YT_ASSERT(message.Size() == 1);
+ const auto& part = message[0];
+ return TString(part.Begin(), part.Size());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReplyingBusHandler
+ : public IMessageHandler
+{
+public:
+ TReplyingBusHandler(TString message)
+ : Message_(std::move(message))
+ { }
+
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr replyBus) noexcept override
+ {
+ EXPECT_EQ(1, std::ssize(message));
+ auto replyMessage = Serialize(Message_);
+ replyBus->Send(replyMessage, NBus::TSendOptions(EDeliveryTrackingLevel::None));
+ }
+
+private:
+ const TString Message_;
+};
+
+class TCheckingBusHandler
+ : public IMessageHandler
+{
+public:
+ explicit TCheckingBusHandler(int numRepliesWaiting, TString message)
+ : Message_(std::move(message))
+ , NumRepliesWaiting(numRepliesWaiting)
+ { }
+
+ void WaitUntilDone()
+ {
+ Event_.Wait();
+ }
+
+private:
+ const TString Message_;
+
+ std::atomic<int> NumRepliesWaiting;
+ NThreading::TEvent Event_;
+
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr /*replyBus*/) noexcept override
+ {
+ auto value = Deserialize(message);
+ EXPECT_EQ(Message_, value);
+
+ if (--NumRepliesWaiting == 0) {
+ Event_.NotifyAll();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZookeeperBusTest
+ : public testing::Test
+{
+public:
+ NTesting::TPortHolder Port;
+ TString Address;
+
+ TZookeeperBusTest()
+ {
+ Port = NTesting::GetFreePort();
+ Address = Format("localhost:%v", Port);
+ }
+
+ IBusServerPtr StartBusServer(IMessageHandlerPtr handler)
+ {
+ auto config = TBusServerConfig::CreateTcp(Port);
+ auto server = CreateBusServer(
+ config,
+ GetZookeeperPacketTranscoderFactory());
+ server->Start(handler);
+ return server;
+ }
+
+ void TestReplies(int numRequests, const TString& message)
+ {
+ auto server = StartBusServer(New<TReplyingBusHandler>(message));
+ auto client = CreateBusClient(
+ TBusClientConfig::CreateTcp(Address),
+ GetZookeeperPacketTranscoderFactory());
+ auto handler = New<TCheckingBusHandler>(numRequests, message);
+ auto bus = client->CreateBus(handler);
+
+ std::vector<TFuture<void>> results;
+ for (int i = 0; i < numRequests; ++i) {
+ if (auto result = bus->Send(CreateMessage(10), NBus::TSendOptions(EDeliveryTrackingLevel::None))) {
+ results.push_back(result);
+ }
+ }
+
+ for (const auto& result : results) {
+ auto error = result.Get();
+ EXPECT_TRUE(error.IsOK());
+ }
+
+ handler->WaitUntilDone();
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TZookeeperBusTest, Simple)
+{
+ TestReplies(1, "42");
+ TestReplies(100, "abacaba");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/unittests/zookeeper_protocol_ut.cpp b/yt/yt/client/unittests/zookeeper_protocol_ut.cpp
new file mode 100644
index 0000000000..03539fc3c0
--- /dev/null
+++ b/yt/yt/client/unittests/zookeeper_protocol_ut.cpp
@@ -0,0 +1,106 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/client/zookeeper/protocol.h>
+
+namespace NYT::NZookeeper {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TZookeeperProtocolTest, Simple)
+{
+ const TString LongString{100'000, 'a'};
+
+ auto writer = CreateZookeeperProtocolWriter();
+ writer->WriteByte('$');
+ writer->WriteInt(42);
+ writer->WriteInt(std::numeric_limits<int>::min());
+ writer->WriteInt(std::numeric_limits<int>::max());
+ writer->WriteLong(123);
+ writer->WriteLong(std::numeric_limits<i64>::min());
+ writer->WriteLong(std::numeric_limits<i64>::max());
+ writer->WriteBool(false);
+ writer->WriteBool(true);
+ writer->WriteString("abacaba");
+ writer->WriteString(LongString);
+ writer->WriteString("");
+
+ auto data = writer->Finish();
+ auto reader = CreateZookeeperProtocolReader(std::move(data));
+ EXPECT_EQ('$', reader->ReadByte());
+ EXPECT_EQ(42, reader->ReadInt());
+ EXPECT_EQ(std::numeric_limits<int>::min(), reader->ReadInt());
+ EXPECT_EQ(std::numeric_limits<int>::max(), reader->ReadInt());
+ EXPECT_EQ(123, reader->ReadLong());
+ EXPECT_EQ(std::numeric_limits<i64>::min(), reader->ReadLong());
+ EXPECT_EQ(std::numeric_limits<i64>::max(), reader->ReadLong());
+ EXPECT_EQ(false, reader->ReadBool());
+ EXPECT_EQ(true, reader->ReadBool());
+ EXPECT_EQ("abacaba", reader->ReadString());
+ EXPECT_EQ(LongString, reader->ReadString());
+
+ EXPECT_FALSE(reader->IsFinished());
+ EXPECT_THROW_WITH_SUBSTRING(reader->ValidateFinished(), "Expected end of stream");
+
+ TString longString;
+ longString.resize(1'000'000);
+ reader->ReadString(&longString);
+ EXPECT_EQ(LongString, LongString);
+
+ EXPECT_TRUE(reader->IsFinished());
+ reader->ValidateFinished();
+}
+
+TEST(TZookeeperProtocolTest, WriterReallocation)
+{
+ constexpr int Count = 954023;
+
+ auto writer = CreateZookeeperProtocolWriter();
+ for (int i = 0; i < Count; ++i) {
+ writer->WriteLong(i);
+ }
+
+ auto data = writer->Finish();
+ EXPECT_EQ(8u * Count, data.Size());
+
+ auto reader = CreateZookeeperProtocolReader(std::move(data));
+ for (int i = 0; i < Count; ++i) {
+ EXPECT_EQ(i, reader->ReadLong());
+ }
+ reader->ValidateFinished();
+}
+
+TEST(TZookeeperProtocolTest, CorruptedInput)
+{
+ {
+ auto reader = CreateZookeeperProtocolReader(TSharedRef::MakeEmpty());
+ EXPECT_THROW_WITH_SUBSTRING(reader->ReadInt(), "Premature end of stream");
+ EXPECT_THROW_WITH_SUBSTRING(reader->ReadString(), "Premature end of stream");
+ }
+ {
+ auto writer = CreateZookeeperProtocolWriter();
+ writer->WriteString(TString{100'000, 'a'});
+ auto data = writer->Finish();
+ data = data.Slice(0, 12345);
+ auto reader = CreateZookeeperProtocolReader(std::move(data));
+ EXPECT_THROW_WITH_SUBSTRING(reader->ReadString(), "Premature end of stream");
+ }
+ {
+ auto writer = CreateZookeeperProtocolWriter();
+ writer->WriteLong(1234);
+ writer->WriteLong(std::numeric_limits<i64>::max());
+ writer->WriteLong('a');
+ writer->WriteLong('b');
+
+ auto data = writer->Finish();
+ auto reader = CreateZookeeperProtocolReader(std::move(data));
+ EXPECT_EQ(1234, reader->ReadLong());
+ // NB: Offset + Size may overflow here if implemented carelessly.
+ EXPECT_THROW_WITH_SUBSTRING(reader->ReadString(), "Premature end of stream");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/ya.make b/yt/yt/client/ya.make
new file mode 100644
index 0000000000..ce32b4215d
--- /dev/null
+++ b/yt/yt/client/ya.make
@@ -0,0 +1,207 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+INCLUDE(../RpcProxyProtocolVersion.txt)
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ api/config.cpp
+ api/client.cpp
+ api/client_cache.cpp
+ api/delegating_client.cpp
+ api/helpers.cpp
+ api/internal_client.cpp
+ api/operation_archive_schema.cpp
+ api/public.cpp
+ api/rowset.cpp
+ api/skynet.cpp
+ api/transaction.cpp
+ api/persistent_queue.cpp
+ api/sticky_transaction_pool.cpp
+
+ api/rpc_proxy/address_helpers.cpp
+ api/rpc_proxy/public.cpp
+ api/rpc_proxy/config.cpp
+ api/rpc_proxy/helpers.cpp
+ api/rpc_proxy/client_impl.cpp
+ api/rpc_proxy/client_base.cpp
+ api/rpc_proxy/connection.cpp
+ api/rpc_proxy/connection_impl.cpp
+ api/rpc_proxy/file_reader.cpp
+ api/rpc_proxy/file_writer.cpp
+ api/rpc_proxy/journal_reader.cpp
+ api/rpc_proxy/journal_writer.cpp
+ api/rpc_proxy/table_mount_cache.cpp
+ api/rpc_proxy/table_reader.cpp
+ api/rpc_proxy/table_writer.cpp
+ api/rpc_proxy/timestamp_provider.cpp
+ api/rpc_proxy/transaction.cpp
+ api/rpc_proxy/transaction_impl.cpp
+ api/rpc_proxy/row_stream.cpp
+ api/rpc_proxy/wire_row_stream.cpp
+
+ election/public.cpp
+
+ hive/timestamp_map.cpp
+
+ hydra/version.cpp
+
+ chaos_client/helpers.cpp
+ chaos_client/replication_card.cpp
+ chaos_client/replication_card_cache.cpp
+ chaos_client/replication_card_serialization.cpp
+
+ chunk_client/chunk_replica.cpp
+ chunk_client/config.cpp
+ chunk_client/data_statistics.cpp
+ chunk_client/helpers.cpp
+ chunk_client/public.cpp
+ chunk_client/read_limit.cpp
+ chunk_client/ready_event_reader_base.cpp
+
+ journal_client/public.cpp
+ journal_client/config.cpp
+
+ cypress_client/public.cpp
+
+ node_tracker_client/node_directory.cpp
+ node_tracker_client/helpers.cpp
+ node_tracker_client/public.cpp
+
+ object_client/public.cpp
+ object_client/helpers.cpp
+
+ scheduler/operation_id_or_alias.cpp
+ scheduler/operation_cache.cpp
+
+ security_client/acl.cpp
+ security_client/access_control.cpp
+ security_client/public.cpp
+ security_client/helpers.cpp
+
+ table_client/public.cpp
+ table_client/adapters.cpp
+ table_client/table_output.cpp
+ table_client/blob_reader.cpp
+ table_client/check_schema_compatibility.cpp
+ table_client/chunk_stripe_statistics.cpp
+ table_client/config.cpp
+ table_client/column_rename_descriptor.cpp
+ table_client/column_sort_schema.cpp
+ table_client/comparator.cpp
+ table_client/key.cpp
+ table_client/key_bound.cpp
+ table_client/key_bound_compressor.cpp
+ table_client/pipe.cpp
+ table_client/versioned_row.cpp
+ table_client/unversioned_row.cpp
+ table_client/unversioned_value.cpp
+ table_client/versioned_reader.cpp
+ table_client/row_base.cpp
+ table_client/row_batch.cpp
+ table_client/row_buffer.cpp
+ table_client/schema.cpp
+ table_client/schema_serialization_helpers.cpp
+ table_client/serialize.cpp
+ table_client/logical_type.cpp
+ table_client/name_table.cpp
+ table_client/wire_protocol.cpp
+ table_client/columnar_statistics.cpp
+ table_client/helpers.cpp
+ table_client/value_consumer.cpp
+ table_client/table_consumer.cpp
+ table_client/schemaless_row_reorderer.cpp
+ table_client/schemaful_reader_adapter.cpp
+ table_client/unordered_schemaful_reader.cpp
+ table_client/validate_logical_type.cpp
+ table_client/composite_compare.cpp
+ table_client/columnar.cpp
+ table_client/record_codegen_cpp.cpp
+ table_client/record_helpers.cpp
+
+ tablet_client/config.cpp
+ tablet_client/table_mount_cache_detail.cpp
+ tablet_client/table_mount_cache.cpp
+ tablet_client/public.cpp
+ tablet_client/helpers.cpp
+
+ queue_client/common.cpp
+ queue_client/config.cpp
+ queue_client/consumer_client.cpp
+ queue_client/partition_reader.cpp
+ queue_client/queue_rowset.cpp
+
+ ypath/rich.cpp
+ ypath/parser_detail.cpp
+
+ transaction_client/batching_timestamp_provider.cpp
+ transaction_client/config.cpp
+ transaction_client/helpers.cpp
+ transaction_client/noop_timestamp_provider.cpp
+ transaction_client/remote_timestamp_provider.cpp
+ transaction_client/timestamp_provider_base.cpp
+
+ misc/config.cpp
+ misc/io_tags.cpp
+ misc/method_helpers.cpp
+ misc/workload.cpp
+
+ job_tracker_client/public.cpp
+ job_tracker_client/helpers.cpp
+
+ query_client/query_builder.cpp
+ query_client/query_statistics.cpp
+
+ complex_types/check_yson_token.cpp
+ complex_types/check_type_compatibility.cpp
+ complex_types/infinite_entity.cpp
+ complex_types/yson_format_conversion.cpp
+ complex_types/uuid_text.cpp
+ complex_types/time_text.cpp
+
+ zookeeper/packet.cpp
+ zookeeper/protocol.cpp
+ zookeeper/requests.cpp
+)
+
+SRCS(
+ ${YT_SRCS}
+ yt/yt/client/api/rpc_proxy/protocol_version_variables.h.in
+)
+
+PEERDIR(
+ yt/yt/client/query_tracker_client
+ yt/yt/core
+ yt/yt/core/http
+ yt/yt/library/auth
+ yt/yt/library/decimal
+ yt/yt/library/re2
+ yt/yt/library/erasure
+ yt/yt/library/numeric
+ yt/yt/library/quantile_digest
+ yt/yt_proto/yt/client
+ library/cpp/json
+ contrib/libs/pfr
+)
+
+END()
+
+RECURSE(
+ arrow
+ driver
+ federated
+ hedging
+)
+
+IF (NOT OPENSOURCE)
+ RECURSE(
+ table_client/benchmark
+ )
+ENDIF()
+
+RECURSE_FOR_TESTS(
+ unittests
+ table_client/unittests
+)
diff --git a/yt/yt/client/ypath/parser_detail.cpp b/yt/yt/client/ypath/parser_detail.cpp
new file mode 100644
index 0000000000..996dd52159
--- /dev/null
+++ b/yt/yt/client/ypath/parser_detail.cpp
@@ -0,0 +1,460 @@
+#include "parser_detail.h"
+
+#include <yt/yt/client/table_client/unversioned_row.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/misc/parser_helpers.h>
+
+#include <yt/yt/core/yson/token.h>
+#include <yt/yt/core/yson/tokenizer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYPath {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NTableClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const NYson::ETokenType BeginColumnSelectorToken = NYson::ETokenType::LeftBrace;
+const NYson::ETokenType EndColumnSelectorToken = NYson::ETokenType::RightBrace;
+const NYson::ETokenType ColumnSeparatorToken = NYson::ETokenType::Comma;
+const NYson::ETokenType BeginRowSelectorToken = NYson::ETokenType::LeftBracket;
+const NYson::ETokenType EndRowSelectorToken = NYson::ETokenType::RightBracket;
+const NYson::ETokenType RowIndexMarkerToken = NYson::ETokenType::Hash;
+const NYson::ETokenType BeginTupleToken = NYson::ETokenType::LeftParenthesis;
+const NYson::ETokenType EndTupleToken = NYson::ETokenType::RightParenthesis;
+const NYson::ETokenType KeySeparatorToken = NYson::ETokenType::Comma;
+const NYson::ETokenType RangeToken = NYson::ETokenType::Colon;
+const NYson::ETokenType RangeSeparatorToken = NYson::ETokenType::Comma;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ThrowUnexpectedToken(const TToken& token)
+{
+ THROW_ERROR_EXCEPTION("Unexpected token %Qv",
+ token);
+}
+
+TString ParseAttributes(const TString& str, const IAttributeDictionaryPtr& attributes)
+{
+ int spaceCount = 0;
+ {
+ size_t index = 0;
+ while (index < str.size() && IsSpace(str[index])) {
+ ++index;
+ }
+ if (index == str.size() || str[index] != TokenTypeToChar(NYson::ETokenType::LeftAngle)) {
+ return str;
+ }
+ spaceCount = index;
+ }
+
+ NYson::TTokenizer tokenizer(TStringBuf(str).SubStr(spaceCount));
+ tokenizer.ParseNext();
+ if (tokenizer.CurrentToken().GetType() != NYson::ETokenType::LeftAngle) {
+ ThrowUnexpectedToken(tokenizer.CurrentToken());
+ }
+
+ int depth = 0;
+ int attrStartPosition = spaceCount + 1;
+
+ while (true) {
+ switch (tokenizer.CurrentToken().GetType()) {
+ case NYson::ETokenType::LeftAngle:
+ ++depth;
+ break;
+ case NYson::ETokenType::RightAngle:
+ --depth;
+ break;
+ default:
+ break;
+ }
+
+ if (depth == 0) {
+ break;
+ }
+
+ if (!tokenizer.ParseNext()) {
+ THROW_ERROR_EXCEPTION("Unmatched '<' in YPath");
+ }
+ }
+
+ int attrEndPosition = spaceCount + tokenizer.GetPosition() - 1;
+ int pathStartPosition = attrEndPosition + 1;
+
+ TYsonString attrYson(
+ str.substr(attrStartPosition, attrEndPosition - attrStartPosition),
+ NYson::EYsonType::MapFragment);
+ attributes->MergeFrom(*ConvertToAttributes(attrYson));
+
+ return TrimLeadingWhitespaces(str.substr(pathStartPosition));
+}
+
+bool IsValidClusterSymbol(char c)
+{
+ return IsAsciiAlnum(c) || c == '-' || c == '_';
+}
+
+bool IsRootDesignator(char c)
+{
+ return c == '/' || c == '#';
+}
+
+bool StartsWithRootDesignator(TStringBuf str)
+{
+ size_t nonSpaceIndex = str.find_first_not_of(' ');
+ if (nonSpaceIndex != TStringBuf::npos && !IsRootDesignator(str[nonSpaceIndex])) {
+ return false;
+ }
+
+ return true;
+}
+
+TString ParseCluster(TString str, const IAttributeDictionaryPtr& attributes)
+{
+ if (str.empty()) {
+ return str;
+ }
+
+ if (StartsWithRootDesignator(str)) {
+ return str;
+ }
+
+ auto clusterSeparatorIndex = str.find_first_of(':');
+ if (clusterSeparatorIndex == TString::npos) {
+ THROW_ERROR_EXCEPTION(
+ "Path %Qv does not start with a valid root-designator, cluster://path short-form assumed; "
+ "no \':\' separator symbol found to parse cluster",
+ str);
+ }
+
+ const auto clusterName = str.substr(0, clusterSeparatorIndex);
+
+ if (clusterName.empty()) {
+ THROW_ERROR_EXCEPTION(
+ "Path %Qv does not start with a valid root-designator, cluster://path short-form assumed; "
+ "cluster name cannot be empty",
+ str);
+ }
+
+ auto illegalSymbolIt = std::find_if_not(clusterName.begin(), clusterName.end(), &IsValidClusterSymbol);
+ if (illegalSymbolIt != clusterName.end()) {
+ THROW_ERROR_EXCEPTION(
+ "Path %Qv does not start with a valid root-designator, cluster://path short-form assumed; "
+ "cluster name contains illegal symbol %Qv",
+ str,
+ *illegalSymbolIt);
+ }
+
+ auto remainingString = str.substr(clusterSeparatorIndex + 1);
+
+ if (!StartsWithRootDesignator(remainingString)) {
+ THROW_ERROR_EXCEPTION(
+ "Path %Qv does not start with a valid root-designator, cluster://path short-form assumed; "
+ "path %Qv after cluster-separator does not start with a valid root-designator",
+ str,
+ remainingString);
+ }
+
+ attributes->Set("cluster", clusterName);
+ return remainingString;
+
+
+ // NB. If the path had attributes, then the leading spaces must be removed in preceding ParseAttributes()
+ // call. We can encounter the path with leading spaces here if it didn't have attributes and passed through
+ // ParseAttributes() unchanged. In this case, the path is most likely incorrect, so it returns unchanged from
+ // this function. For example, " <> cluster://path" will become "cluster://path" after call to ParseAttributes(),
+ // and "//path" after this function. But " cluster://path" will pass unchanged throughout ParseAttributes(), and
+ // will remain " cluster://path" after this function.
+ size_t index = 0;
+ size_t clusterStart = index;
+ while (index < str.size() && (IsAsciiAlnum(str[index]) || str[index] == '-' || str[index] == '_')) {
+ ++index;
+ }
+ if (index >= str.size() || str[index] != ':') {
+ // Not a cluster name, so return the string as-is.
+ return str;
+ }
+ size_t clusterEnd = index;
+ if (clusterStart == clusterEnd) {
+ THROW_ERROR_EXCEPTION("Cluster name cannot be empty");
+ }
+ attributes->Set("cluster", str.substr(clusterStart, clusterEnd - clusterStart));
+ ++index;
+ return str.substr(index);
+}
+
+void ParseColumns(NYson::TTokenizer& tokenizer, IAttributeDictionary* attributes)
+{
+ if (tokenizer.GetCurrentType() != BeginColumnSelectorToken) {
+ return;
+ }
+
+ std::vector<TString> columns;
+
+ tokenizer.ParseNext();
+ while (tokenizer.GetCurrentType() != EndColumnSelectorToken) {
+ TString begin;
+ switch (tokenizer.GetCurrentType()) {
+ case NYson::ETokenType::String:
+ begin.assign(tokenizer.CurrentToken().GetStringValue());
+ tokenizer.ParseNext();
+ break;
+ default:
+ ThrowUnexpectedToken(tokenizer.CurrentToken());
+ YT_ABORT();
+ }
+
+ columns.push_back(begin);
+
+ switch (tokenizer.GetCurrentType()) {
+ case ColumnSeparatorToken:
+ tokenizer.ParseNext();
+ break;
+ case EndColumnSelectorToken:
+ break;
+ default:
+ ThrowUnexpectedToken(tokenizer.CurrentToken());
+ YT_ABORT();
+ }
+ }
+ tokenizer.ParseNext();
+
+ attributes->Set("columns", ConvertToYsonString(columns));
+}
+
+void ParseKeyPart(
+ NYson::TTokenizer& tokenizer,
+ TUnversionedOwningRowBuilder* rowBuilder)
+{
+ // We don't fill id here, because key part columns are well known.
+ // Also we don't have a name table for them :)
+ TUnversionedValue value;
+
+ switch (tokenizer.GetCurrentType()) {
+ case NYson::ETokenType::String: {
+ auto str = tokenizer.CurrentToken().GetStringValue();
+ value = MakeUnversionedStringValue(str);
+ break;
+ }
+
+ case NYson::ETokenType::Int64: {
+ value = MakeUnversionedInt64Value(tokenizer.CurrentToken().GetInt64Value());
+ break;
+ }
+
+ case NYson::ETokenType::Uint64: {
+ value = MakeUnversionedUint64Value(tokenizer.CurrentToken().GetUint64Value());
+ break;
+ }
+
+ case NYson::ETokenType::Double: {
+ value = MakeUnversionedDoubleValue(tokenizer.CurrentToken().GetDoubleValue());
+ break;
+ }
+
+ case NYson::ETokenType::Boolean: {
+ value = MakeUnversionedBooleanValue(tokenizer.CurrentToken().GetBooleanValue());
+ break;
+ }
+
+ case NYson::ETokenType::Hash: {
+ value = MakeUnversionedSentinelValue(EValueType::Null);
+ break;
+ }
+
+ default:
+ ThrowUnexpectedToken(tokenizer.CurrentToken());
+ break;
+ }
+ rowBuilder->AddValue(value);
+ tokenizer.ParseNext();
+}
+
+// NB: since our final result while parsing YPath is an attribute dictionary, we intentionally refrain
+// from using TReadLimit (or even TLegacyReadLimit) as an intermediate structure here. YPath short
+// form is parsed using merely NYTree primitives and unversioned rows.
+
+//! This enum and the following structure define one of the following row limit specifications:
+//! - (None): empty limit;
+//! - (RowIndex): row index;
+//! - (Key): row (note that in short form this key may not include min/max sentinels).
+DEFINE_ENUM(EYPathLimitKind,
+ (None)
+ (RowIndex)
+ (Key)
+);
+
+//! This is an intermediate representation used only while parsing rich YPath short form.
+struct TYPathLimit
+{
+ EYPathLimitKind Kind = EYPathLimitKind::None;
+ //! Actual row for (Key) and null row for (RowIndex) and (None).
+ TUnversionedOwningRow Row;
+ //! Actual row index for (RowIndex) and std::nullopt for (Key), and (None).
+ std::optional<i64> RowIndex;
+};
+
+void Serialize(const TYPathLimit& limit, IYsonConsumer* consumer)
+{
+ switch (limit.Kind) {
+ case EYPathLimitKind::RowIndex:
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("row_index").Value(limit.RowIndex)
+ .EndMap();
+ return;
+ case EYPathLimitKind::Key:
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key").Value(limit.Row)
+ .EndMap();
+ return;
+ default:
+ // None is not handled by this function.
+ YT_ABORT();
+ }
+}
+
+void ParseRowLimit(
+ NYson::TTokenizer& tokenizer,
+ std::vector<NYson::ETokenType> separators,
+ TYPathLimit* limit)
+{
+ if (std::find(separators.begin(), separators.end(), tokenizer.GetCurrentType()) != separators.end()) {
+ return;
+ }
+
+ TUnversionedOwningRowBuilder rowBuilder;
+ bool hasKeyLimit = false;
+ switch (tokenizer.GetCurrentType()) {
+ case RowIndexMarkerToken:
+ tokenizer.ParseNext();
+ limit->RowIndex = tokenizer.CurrentToken().GetInt64Value();
+ limit->Kind = EYPathLimitKind::RowIndex;
+ tokenizer.ParseNext();
+ break;
+
+ case BeginTupleToken:
+ tokenizer.ParseNext();
+ hasKeyLimit = true;
+ while (tokenizer.GetCurrentType() != EndTupleToken) {
+ ParseKeyPart(tokenizer, &rowBuilder);
+ switch (tokenizer.GetCurrentType()) {
+ case KeySeparatorToken:
+ tokenizer.ParseNext();
+ break;
+ case EndTupleToken:
+ break;
+ default:
+ ThrowUnexpectedToken(tokenizer.CurrentToken());
+ YT_ABORT();
+ }
+ }
+ tokenizer.ParseNext();
+ break;
+
+ default:
+ ParseKeyPart(tokenizer, &rowBuilder);
+ hasKeyLimit = true;
+ break;
+ }
+
+ if (hasKeyLimit) {
+ auto key = rowBuilder.FinishRow();
+ limit->Row = key;
+ limit->Kind = EYPathLimitKind::Key;
+ }
+
+ tokenizer.CurrentToken().ExpectTypes(separators);
+}
+
+void ParseRowRanges(NYson::TTokenizer& tokenizer, IAttributeDictionary* attributes)
+{
+ if (tokenizer.GetCurrentType() == BeginRowSelectorToken) {
+ tokenizer.ParseNext();
+
+ std::vector<INodePtr> ranges;
+
+ bool finished = false;
+ while (!finished) {
+ auto rangeNode = GetEphemeralNodeFactory()->CreateMap();
+
+ TYPathLimit lowerLimit;
+ TYPathLimit upperLimit;
+ bool isTwoSideRange = false;
+ ParseRowLimit(
+ tokenizer,
+ {RangeToken, RangeSeparatorToken, EndRowSelectorToken},
+ &lowerLimit);
+ if (tokenizer.GetCurrentType() == RangeToken) {
+ isTwoSideRange = true;
+ tokenizer.ParseNext();
+
+ ParseRowLimit(
+ tokenizer,
+ {RangeSeparatorToken, EndRowSelectorToken},
+ &upperLimit);
+ }
+
+ if (isTwoSideRange) {
+ if (lowerLimit.Kind != EYPathLimitKind::None) {
+ rangeNode->AddChild("lower_limit", ConvertToNode(lowerLimit));
+ }
+ if (upperLimit.Kind != EYPathLimitKind::None) {
+ rangeNode->AddChild("upper_limit", ConvertToNode(upperLimit));
+ }
+ } else {
+ if (lowerLimit.Kind != EYPathLimitKind::None) {
+ rangeNode->AddChild("exact", ConvertToNode(lowerLimit));
+ } else {
+ // Universal limit is represented by an empty map, so do nothing.
+ }
+ }
+
+ if (tokenizer.CurrentToken().GetType() == EndRowSelectorToken) {
+ finished = true;
+ }
+ tokenizer.ParseNext();
+
+ ranges.emplace_back(std::move(rangeNode));
+ }
+
+ attributes->Set("ranges", ConvertToYsonString(ranges));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRichYPath ParseRichYPathImpl(const TString& str)
+{
+ auto attributes = CreateEphemeralAttributes();
+
+ auto strWithoutAttributes = ParseAttributes(str, attributes);
+ strWithoutAttributes = ParseCluster(std::move(strWithoutAttributes), attributes);
+ TTokenizer ypathTokenizer(strWithoutAttributes);
+
+ while (ypathTokenizer.GetType() != ETokenType::EndOfStream && ypathTokenizer.GetType() != ETokenType::Range) {
+ ypathTokenizer.Advance();
+ }
+ auto path = TYPath(ypathTokenizer.GetPrefix());
+ auto rangeStr = ypathTokenizer.GetToken();
+
+ if (ypathTokenizer.GetType() == ETokenType::Range) {
+ NYson::TTokenizer ysonTokenizer(rangeStr);
+ ysonTokenizer.ParseNext();
+ ParseColumns(ysonTokenizer, attributes.Get());
+ ParseRowRanges(ysonTokenizer, attributes.Get());
+ ysonTokenizer.CurrentToken().ExpectType(NYson::ETokenType::EndOfStream);
+ }
+ return TRichYPath(path, *attributes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/client/ypath/parser_detail.h b/yt/yt/client/ypath/parser_detail.h
new file mode 100644
index 0000000000..953df87034
--- /dev/null
+++ b/yt/yt/client/ypath/parser_detail.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "rich.h"
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRichYPath ParseRichYPathImpl(const TString& str);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/client/ypath/public.h b/yt/yt/client/ypath/public.h
new file mode 100644
index 0000000000..2726c549a1
--- /dev/null
+++ b/yt/yt/client/ypath/public.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/ypath/public.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((InvalidReadRange) (2700))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRichYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/client/ypath/rich.cpp b/yt/yt/client/ypath/rich.cpp
new file mode 100644
index 0000000000..ac68331821
--- /dev/null
+++ b/yt/yt/client/ypath/rich.cpp
@@ -0,0 +1,741 @@
+#include "rich.h"
+
+#include "parser_detail.h"
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/client/table_client/column_sort_schema.h>
+#include <yt/yt/client/table_client/column_rename_descriptor.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYPath {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NChunkClient;
+using namespace NTableClient;
+using namespace NSecurityClient;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRichYPath::TRichYPath()
+{ }
+
+TRichYPath::TRichYPath(const TRichYPath& other)
+ : Path_(other.Path_)
+ , Attributes_(other.Attributes_ ? other.Attributes_->Clone() : nullptr)
+{ }
+
+TRichYPath::TRichYPath(const char* path)
+ : Path_(path)
+{
+ *this = Normalize();
+}
+
+TRichYPath::TRichYPath(const TYPath& path)
+ : Path_(path)
+{
+ *this = Normalize();
+}
+
+TRichYPath::TRichYPath(TRichYPath&& other)
+ : Path_(std::move(other.Path_))
+ , Attributes_(std::move(other.Attributes_))
+{ }
+
+TRichYPath::TRichYPath(const TYPath& path, const IAttributeDictionary& attributes)
+ : Path_(path)
+ , Attributes_(attributes.Clone())
+{ }
+
+const TYPath& TRichYPath::GetPath() const
+{
+ return Path_;
+}
+
+void TRichYPath::SetPath(const TYPath& path)
+{
+ Path_ = path;
+}
+
+const IAttributeDictionary& TRichYPath::Attributes() const
+{
+ return Attributes_ ? *Attributes_ : EmptyAttributes();
+}
+
+IAttributeDictionary& TRichYPath::Attributes()
+{
+ if (!Attributes_) {
+ Attributes_ = CreateEphemeralAttributes();
+ }
+ return *Attributes_;
+}
+
+TRichYPath& TRichYPath::operator = (const TRichYPath& other)
+{
+ if (this != &other) {
+ Path_ = other.Path_;
+ Attributes_ = other.Attributes_ ? other.Attributes_->Clone() : nullptr;
+ }
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator== (const TRichYPath& lhs, const TRichYPath& rhs)
+{
+ return lhs.GetPath() == rhs.GetPath() && lhs.Attributes() == rhs.Attributes();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void AppendAttributes(TStringBuilderBase* builder, const IAttributeDictionary& attributes, EYsonFormat ysonFormat)
+{
+ TString attrString;
+ TStringOutput output(attrString);
+ TYsonWriter writer(&output, ysonFormat, EYsonType::MapFragment);
+
+ BuildYsonAttributesFluently(&writer)
+ .Items(attributes);
+
+ if (!attrString.empty()) {
+ builder->AppendChar(TokenTypeToChar(NYson::ETokenType::LeftAngle));
+ builder->AppendString(attrString);
+ builder->AppendChar(TokenTypeToChar(NYson::ETokenType::RightAngle));
+ }
+}
+
+template <class TFunc>
+auto RunAttributeAccessor(const TRichYPath& path, const TString& key, TFunc accessor) -> decltype(accessor())
+{
+ try {
+ return accessor();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing attribute %Qv of rich YPath %v",
+ key,
+ path.GetPath()) << ex;
+ }
+}
+
+template <class T>
+T GetAttribute(const TRichYPath& path, const TString& key, const T& defaultValue)
+{
+ return RunAttributeAccessor(path, key, [&] () {
+ return path.Attributes().Get(key, defaultValue);
+ });
+}
+
+template <class T>
+typename TOptionalTraits<T>::TOptional FindAttribute(const TRichYPath& path, const TString& key)
+{
+ return RunAttributeAccessor(path, key, [&] () {
+ return path.Attributes().Find<T>(key);
+ });
+}
+
+TYsonString FindAttributeYson(const TRichYPath& path, const TString& key)
+{
+ return RunAttributeAccessor(path, key, [&] () {
+ return path.Attributes().FindYson(key);
+ });
+}
+
+} // namespace
+
+TRichYPath TRichYPath::Parse(const TString& str)
+{
+ return ParseRichYPathImpl(str);
+}
+
+TRichYPath TRichYPath::Normalize() const
+{
+ auto parsed = TRichYPath::Parse(Path_);
+ parsed.Attributes().MergeFrom(Attributes());
+ return parsed;
+}
+
+void TRichYPath::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, Path_);
+ Save(context, Attributes_);
+}
+
+void TRichYPath::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ Load(context, Path_);
+ Load(context, Attributes_);
+}
+
+bool TRichYPath::GetAppend(bool defaultValue) const
+{
+ return GetAttribute(*this, "append", defaultValue);
+}
+
+void TRichYPath::SetAppend(bool value)
+{
+ Attributes().Set("append", value);
+}
+
+bool TRichYPath::GetTeleport() const
+{
+ return GetAttribute(*this, "teleport", false);
+}
+
+bool TRichYPath::GetPrimary() const
+{
+ return GetAttribute(*this, "primary", false);
+}
+
+bool TRichYPath::GetForeign() const
+{
+ return GetAttribute(*this, "foreign", false);
+}
+
+void TRichYPath::SetForeign(bool value)
+{
+ Attributes().Set("foreign", value);
+}
+
+std::optional<std::vector<TString>> TRichYPath::GetColumns() const
+{
+ if (Attributes().Contains("channel")) {
+ THROW_ERROR_EXCEPTION("Deprecated attribute \"channel\" in YPath");
+ }
+ return FindAttribute<std::vector<TString>>(*this, "columns");
+}
+
+void TRichYPath::SetColumns(const std::vector<TString>& columns)
+{
+ Attributes().Set("columns", columns);
+}
+
+std::vector<NChunkClient::TLegacyReadRange> TRichYPath::GetRanges() const
+{
+ // COMPAT(ignat): top-level "lower_limit" and "upper_limit" are processed for compatibility.
+ auto optionalLowerLimit = FindAttribute<TLegacyReadLimit>(*this, "lower_limit");
+ auto optionalUpperLimit = FindAttribute<TLegacyReadLimit>(*this, "upper_limit");
+ auto optionalRanges = FindAttribute<std::vector<TLegacyReadRange>>(*this, "ranges");
+
+ if (optionalLowerLimit || optionalUpperLimit) {
+ if (optionalRanges) {
+ THROW_ERROR_EXCEPTION("YPath cannot be annotated with both multiple (\"ranges\" attribute) "
+ "and single (\"lower_limit\" or \"upper_limit\" attributes) ranges");
+ }
+ return std::vector<TLegacyReadRange>({
+ TLegacyReadRange(
+ optionalLowerLimit.value_or(TLegacyReadLimit()),
+ optionalUpperLimit.value_or(TLegacyReadLimit())
+ )});
+ } else {
+ return optionalRanges.value_or(std::vector<TLegacyReadRange>({TLegacyReadRange()}));
+ }
+}
+
+TUnversionedValue TryConvertValue(const TUnversionedValue& from, EValueType dstType)
+{
+ if (from.Type != NTableClient::EValueType::Int64 && from.Type != NTableClient::EValueType::Uint64) {
+ return from;
+ }
+
+ auto convert = [&] (auto srcValue, bool isSigned) {
+ const ui64 maxInt64 = std::numeric_limits<i64>::max();
+ switch (dstType) {
+ case EValueType::Uint64:
+ if (isSigned && srcValue < 0) {
+ // Casting from negative int64 to uint64 doesn't seem to be a valid thing expected by user,
+ // so we just return without conversion.
+ return from;
+ }
+ return MakeUnversionedUint64Value(static_cast<ui64>(srcValue));
+ case EValueType::Int64:
+ if (!isSigned && static_cast<ui64>(srcValue) > maxInt64) {
+ // The unsigned value is too large to fit into int64. We also don't perform the conversion.
+ return from;
+ }
+ return MakeUnversionedInt64Value(static_cast<i64>(srcValue));
+ case EValueType::Double:
+ return MakeUnversionedDoubleValue(static_cast<double>(srcValue));
+ default:
+ return from;
+ }
+ YT_ABORT();
+ };
+
+ switch (from.Type) {
+ case NTableClient::EValueType::Int64:
+ return convert(from.Data.Int64, true);
+ case NTableClient::EValueType::Uint64:
+ return convert(from.Data.Uint64, false);
+ default:
+ return from;
+ }
+ YT_ABORT();
+}
+
+//! Return read range corresponding to given map node.
+NChunkClient::TReadRange RangeNodeToReadRange(
+ const NTableClient::TComparator& comparator,
+ const IMapNodePtr& rangeNode,
+ const NTableClient::TKeyColumnTypes& conversionTypeHints)
+{
+ auto lowerLimitNode = rangeNode->FindChild("lower_limit");
+ auto upperLimitNode = rangeNode->FindChild("upper_limit");
+ auto exactNode = rangeNode->FindChild("exact");
+
+ if (exactNode && (lowerLimitNode || upperLimitNode)) {
+ THROW_ERROR_EXCEPTION("Exact limit cannot be specified simultaneously with lower or upper limits");
+ }
+
+ auto deserializeLimit = [&] (const IMapNodePtr& limitNode, TReadLimit& readLimit, bool isUpper, bool isExact) {
+ auto keyNode = limitNode->FindChild("key");
+ if (keyNode) {
+ limitNode->RemoveChild(keyNode);
+ }
+
+ auto keyBoundNode = limitNode->FindChild("key_bound");
+
+ // Check that key bound is not specified in exact clause.
+ if (keyBoundNode && isExact) {
+ THROW_ERROR_EXCEPTION("Key bound cannot be specified in exact limit, specify lower or upper limit instead");
+ }
+
+ // Check that key or key bound do not appear for unsorted tables.
+ if ((keyNode || keyBoundNode) && !comparator) {
+ THROW_ERROR_EXCEPTION("Cannot use key or key bound in read limit for an unsorted object");
+ }
+
+ // NB: for the sake of compatibility, we support specifying both key and key bound in read limit.
+ // In this case we consider only key bound and completely ignore key.
+
+ if (keyNode && !keyBoundNode) {
+ // Before deserializing, we may need to transform legacy key into key bound.
+ auto owningKey = ConvertTo<TUnversionedOwningRow>(keyNode);
+ TOwningKeyBound keyBound;
+
+ // Perform type conversion, if required.
+ if (!conversionTypeHints.empty()) {
+ TUnversionedOwningRowBuilder newOwningKey;
+ const int typedKeyCount = std::min(owningKey.GetCount(), static_cast<int>(conversionTypeHints.size()));
+ for (int i = 0; i < typedKeyCount; ++i) {
+ newOwningKey.AddValue(TryConvertValue(owningKey[i], conversionTypeHints[i]));
+ }
+ for (int i = typedKeyCount; i < owningKey.GetCount(); ++i) {
+ newOwningKey.AddValue(owningKey[i]);
+ }
+ owningKey = newOwningKey.FinishRow();
+ }
+
+ bool containsSentinels = std::find_if(
+ owningKey.begin(),
+ owningKey.end(),
+ [&] (const TUnversionedValue& value) {
+ return IsSentinelType(value.Type);
+ }) != owningKey.end();
+
+ // For tables with descending sort order we are allowed to impose stricter rules regarding
+ // what can be specified as a "key" in a read limit.
+ if (comparator.HasDescendingSortOrder()) {
+ // First of all, we do not want to state if <min> is less than any value in descending sort order
+ // or not. That's why we simply prohibit keys with sentinels when comparator is not totally ascending.
+ if (containsSentinels) {
+ THROW_ERROR_EXCEPTION(
+ "Sentinel values are not allowed in read limit key when at least one column "
+ "has descending sort order");
+ }
+ // Also, we prohibit keys that are longer than comparator.
+ if (owningKey.GetCount() > comparator.GetLength()) {
+ THROW_ERROR_EXCEPTION(
+ "Read limit key cannot be longer than schema key prefix when at least one column "
+ "has descending sort order");
+ }
+
+ keyBound = TOwningKeyBound::FromRow(owningKey, /* isInclusive */ !isUpper, isUpper);
+ } else {
+ // Existing code may specify arbitrary garbage as legacy keys and we have no solution other
+ // than interpreting it as a key bound using interop method.
+
+ if (isExact && (owningKey.GetCount() > comparator.GetLength() || containsSentinels)) {
+ // NB: there are two tricky cases when read limit is exact:
+ // - (1) if specified key is longer than comparator. Recall that (in old terms)
+ // there may be no keys between (foo, bar) and (foo, bar, <max>) in a table with single
+ // key column.
+ // - (2) if specified key contains sentinels.
+ //
+ // For non-totally ascending comparators we throw error right in deserializeLimit in
+ // both cases.
+ //
+ // For totally ascending comparators we can't throw error and we must somehow
+ // represent an empty range because there may be already existing code that specifies
+ // such limits and expects the empty result, but not an error.
+ //
+ // That's why we replace such key with (empty) key bound >[] to make sure this range
+ // will not contain any key. Also, note that simply skipping such range is incorrect
+ // as it would break range indices (kudos to ifsmirnov@ for this nice observation).
+ keyBound = TOwningKeyBound::MakeEmpty(/* isUpper */ false);
+ } else {
+ keyBound = KeyBoundFromLegacyRow(owningKey, isUpper, comparator.GetLength());
+ }
+ }
+ limitNode->AddChild("key_bound", ConvertToNode(keyBound));
+ }
+
+ // We got rid of "key" map node key, so we may just use Deserialize which simply constructs
+ // read limit from node representation.
+ Deserialize(readLimit, limitNode);
+
+ // Note that "key_bound" is either specified explicitly or obtained from "key" at this point.
+ // For the former case, we did not yet perform validation of key bound sanity, we will do that
+ // later. For the latter case the check is redundant, but let's perform it anyway.
+
+ // If key bound was explicitly specified (i.e. we did not transform legacy key into key bound),
+ // we still need to check that key bound length is no more than comparator length.
+ if (readLimit.KeyBound() && readLimit.KeyBound().Prefix.GetCount() > comparator.GetLength()) {
+ THROW_ERROR_EXCEPTION("Key bound length must not exceed schema key prefix length");
+ }
+
+ // Check that key bound is of correct direction.
+ if (readLimit.KeyBound() && readLimit.KeyBound().IsUpper != isUpper) {
+ // Key bound for exact limit is formed only by us and it has always a correct (lower) direction.
+ YT_VERIFY(!isExact);
+ THROW_ERROR_EXCEPTION(
+ "Key bound for %v limit has wrong relation type %Qv",
+ isUpper ? "upper" : "lower",
+ readLimit.KeyBound().GetRelation());
+ }
+
+ // Validate that exact limit contains at most one independent selector.
+ if (exactNode && (readLimit.HasIndependentSelectors() || readLimit.IsTrivial())) {
+ THROW_ERROR_EXCEPTION("Exact read limit must have exactly one independent selector specified");
+ }
+ };
+
+ TReadRange result;
+
+ if (lowerLimitNode) {
+ deserializeLimit(lowerLimitNode->AsMap(), result.LowerLimit(), /* isUpper */ false, /* isExact */ false);
+ }
+ if (upperLimitNode) {
+ deserializeLimit(upperLimitNode->AsMap(), result.UpperLimit(), /* isUpper */ true, /* isExact */ false);
+ }
+ if (exactNode) {
+ // Exact limit is transformed into pair of lower and upper limit. First, deserialize exact limit
+ // as if it was lower limit.
+ deserializeLimit(exactNode->AsMap(), result.LowerLimit(), /* isUpper */ false, /* isExact */ true);
+
+ // Now copy lower limit to upper limit and either increment single integer selector, or
+ // invert key bound selector.
+ result.UpperLimit() = result.LowerLimit().ToExactUpperCounterpart();
+ }
+
+ return result;
+}
+
+std::vector<NChunkClient::TReadRange> TRichYPath::GetNewRanges(
+ const NTableClient::TComparator& comparator,
+ const NTableClient::TKeyColumnTypes& conversionTypeHints) const
+{
+ // TODO(max42): YT-14242. Top-level "lower_limit" and "upper_limit" are processed for compatibility.
+ // But we should deprecate this one day.
+ auto optionalRangeNodes = FindAttribute<std::vector<IMapNodePtr>>(*this, "ranges");
+ auto optionalLowerLimitNode = FindAttribute<IMapNodePtr>(*this, "lower_limit");
+ auto optionalUpperLimitNode = FindAttribute<IMapNodePtr>(*this, "upper_limit");
+
+ if (optionalLowerLimitNode || optionalUpperLimitNode) {
+ if (optionalRangeNodes) {
+ THROW_ERROR_EXCEPTION("YPath cannot be annotated with both multiple (\"ranges\" attribute) "
+ "and single (\"lower_limit\" or \"upper_limit\" attributes) ranges");
+ }
+ THashMap<TString, IMapNodePtr> rangeNode;
+ if (optionalLowerLimitNode) {
+ rangeNode["lower_limit"] = optionalLowerLimitNode;
+ }
+ if (optionalUpperLimitNode) {
+ rangeNode["upper_limit"] = optionalUpperLimitNode;
+ }
+ optionalRangeNodes = {ConvertToNode(rangeNode)->AsMap()};
+ }
+
+ if (!optionalRangeNodes) {
+ return {TReadRange()};
+ }
+
+ std::vector<NChunkClient::TReadRange> readRanges;
+
+ for (const auto& rangeNode : *optionalRangeNodes) {
+ // For the sake of unchanged range node in error message.
+ auto rangeNodeCopy = ConvertToNode(rangeNode);
+ try {
+ readRanges.emplace_back(RangeNodeToReadRange(comparator, rangeNode, conversionTypeHints));
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ NYPath::EErrorCode::InvalidReadRange, "Invalid read range")
+ << TErrorAttribute("range", rangeNodeCopy)
+ << ex;
+ }
+ }
+
+ return readRanges;
+}
+
+void TRichYPath::SetRanges(const std::vector<NChunkClient::TReadRange>& ranges)
+{
+ Attributes().Set("ranges", ranges);
+ // COMPAT(ignat)
+ Attributes().Remove("lower_limit");
+ Attributes().Remove("upper_limit");
+}
+
+bool TRichYPath::HasNontrivialRanges() const
+{
+ auto optionalLowerLimit = FindAttribute<TLegacyReadLimit>(*this, "lower_limit");
+ auto optionalUpperLimit = FindAttribute<TLegacyReadLimit>(*this, "upper_limit");
+ auto optionalRanges = FindAttribute<std::vector<TLegacyReadRange>>(*this, "ranges");
+
+ return optionalLowerLimit || optionalUpperLimit || optionalRanges;
+}
+
+std::optional<TString> TRichYPath::GetFileName() const
+{
+ return FindAttribute<TString>(*this, "file_name");
+}
+
+std::optional<bool> TRichYPath::GetExecutable() const
+{
+ return FindAttribute<bool>(*this, "executable");
+}
+
+TYsonString TRichYPath::GetFormat() const
+{
+ return FindAttributeYson(*this, "format");
+}
+
+TTableSchemaPtr TRichYPath::GetSchema() const
+{
+ return RunAttributeAccessor(*this, "schema", [&] {
+ auto schema = FindAttribute<TTableSchemaPtr>(*this, "schema");
+ if (schema) {
+ ValidateTableSchema(*schema);
+ }
+ return schema;
+ });
+}
+
+std::optional<TColumnRenameDescriptors> TRichYPath::GetColumnRenameDescriptors() const
+{
+ return FindAttribute<TColumnRenameDescriptors>(*this, "rename_columns");
+}
+
+TSortColumns TRichYPath::GetSortedBy() const
+{
+ return GetAttribute(*this, "sorted_by", TSortColumns());
+}
+
+void TRichYPath::SetSortedBy(const TSortColumns& value)
+{
+ if (value.empty()) {
+ Attributes().Remove("sorted_by");
+ } else {
+ Attributes().Set("sorted_by", value);
+ }
+}
+
+std::optional<i64> TRichYPath::GetRowCountLimit() const
+{
+ return RunAttributeAccessor(*this, "row_count_limit", [&] {
+ auto rowCountLimit = FindAttribute<i64>(*this, "row_count_limit");
+ if (rowCountLimit && *rowCountLimit < 0) {
+ THROW_ERROR_EXCEPTION("Row count limit should be non-negative")
+ << TErrorAttribute("row_count_limit", rowCountLimit);
+ }
+ return rowCountLimit;
+ });
+}
+
+std::optional<NTransactionClient::TTimestamp> TRichYPath::GetTimestamp() const
+{
+ return FindAttribute<NTransactionClient::TTimestamp>(*this, "timestamp");
+}
+
+std::optional<NTransactionClient::TTimestamp> TRichYPath::GetRetentionTimestamp() const
+{
+ return FindAttribute<NTransactionClient::TTimestamp>(*this, "retention_timestamp");
+}
+
+std::optional<NTransactionClient::TTimestamp> TRichYPath::GetOutputTimestamp() const
+{
+ return FindAttribute<NTransactionClient::TTimestamp>(*this, "output_timestamp");
+}
+
+std::optional<NTableClient::EOptimizeFor> TRichYPath::GetOptimizeFor() const
+{
+ return FindAttribute<NTableClient::EOptimizeFor>(*this, "optimize_for");
+}
+
+std::optional<NChunkClient::EChunkFormat> TRichYPath::GetChunkFormat() const
+{
+ return FindAttribute<NChunkClient::EChunkFormat>(*this, "chunk_format");
+}
+
+std::optional<NCompression::ECodec> TRichYPath::GetCompressionCodec() const
+{
+ return FindAttribute<NCompression::ECodec>(*this, "compression_codec");
+}
+
+std::optional<NErasure::ECodec> TRichYPath::GetErasureCodec() const
+{
+ return FindAttribute<NErasure::ECodec>(*this, "erasure_codec");
+}
+
+bool TRichYPath::GetAutoMerge() const
+{
+ return GetAttribute<bool>(*this, "auto_merge", true);
+}
+
+std::optional<NObjectClient::TTransactionId> TRichYPath::GetTransactionId() const
+{
+ return FindAttribute<NObjectClient::TTransactionId>(*this, "transaction_id");
+}
+
+std::optional<std::vector<TSecurityTag>> TRichYPath::GetSecurityTags() const
+{
+ return FindAttribute<std::vector<TSecurityTag>>(*this, "security_tags");
+}
+
+bool TRichYPath::GetBypassArtifactCache() const
+{
+ return GetAttribute<bool>(*this, "bypass_artifact_cache", false);
+}
+
+ETableSchemaModification TRichYPath::GetSchemaModification() const
+{
+ return GetAttribute<ETableSchemaModification>(
+ *this,
+ "schema_modification",
+ ETableSchemaModification::None);
+}
+
+bool TRichYPath::GetPartiallySorted() const
+{
+ return GetAttribute<bool>(*this, "partially_sorted", false);
+}
+
+std::optional<bool> TRichYPath::GetChunkUniqueKeys() const
+{
+ return FindAttribute<bool>(*this, "chunk_unique_keys");
+}
+
+std::optional<bool> TRichYPath::GetCopyFile() const
+{
+ return FindAttribute<bool>(*this, "copy_file");
+}
+
+std::optional<TSortColumns> TRichYPath::GetChunkSortColumns() const
+{
+ return FindAttribute<TSortColumns>(*this, "chunk_sort_columns");
+}
+
+std::optional<TString> TRichYPath::GetCluster() const
+{
+ return FindAttribute<TString>(*this, "cluster");
+}
+
+void TRichYPath::SetCluster(const TString& value)
+{
+ Attributes().Set("cluster", value);
+}
+
+std::optional<std::vector<TString>> TRichYPath::GetClusters() const
+{
+ return FindAttribute<std::vector<TString>>(*this, "clusters");
+}
+
+void TRichYPath::SetClusters(const std::vector<TString>& value)
+{
+ Attributes().Set("clusters", value);
+}
+
+bool TRichYPath::GetCreate() const
+{
+ return GetAttribute<bool>(*this, "create", false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ConvertToString(const TRichYPath& path, EYsonFormat ysonFormat)
+{
+ TStringBuilder builder;
+ AppendAttributes(&builder, path.Attributes(), ysonFormat);
+ builder.AppendString(path.GetPath());
+ return builder.Flush();
+}
+
+TString ToString(const TRichYPath& path)
+{
+ // NB: we intentionally use Text format since string-representation of rich ypath should be readable.
+ return ConvertToString(path, EYsonFormat::Text);
+}
+
+std::vector<TRichYPath> Normalize(const std::vector<TRichYPath>& paths)
+{
+ std::vector<TRichYPath> result;
+ for (const auto& path : paths) {
+ result.push_back(path.Normalize());
+ }
+ return result;
+}
+
+void Serialize(const TRichYPath& richPath, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginAttributes()
+ .Items(richPath.Attributes())
+ .EndAttributes()
+ .Value(richPath.GetPath());
+}
+
+void Deserialize(TRichYPath& richPath, INodePtr node)
+{
+ if (node->GetType() != ENodeType::String) {
+ THROW_ERROR_EXCEPTION("YPath can only be parsed from %Qlv but got %Qlv",
+ ENodeType::String,
+ node->GetType());
+ }
+ richPath.SetPath(node->GetValue<TString>());
+ richPath.Attributes().Clear();
+ richPath.Attributes().MergeFrom(node->Attributes());
+ richPath = richPath.Normalize();
+}
+
+void Deserialize(TRichYPath& richPath, TYsonPullParserCursor* cursor)
+{
+ Deserialize(richPath, ExtractTo<INodePtr>(cursor));
+}
+
+void ToProto(TString* protoPath, const TRichYPath& path)
+{
+ *protoPath = ConvertToString(path, EYsonFormat::Binary);
+}
+
+void FromProto(TRichYPath* path, const TString& protoPath)
+{
+ *path = TRichYPath::Parse(protoPath);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
+
+size_t THash<NYT::NYPath::TRichYPath>::operator()(const NYT::NYPath::TRichYPath& richYPath) const
+{
+ return ComputeHash(NYT::NYPath::ToString(richYPath));
+}
diff --git a/yt/yt/client/ypath/rich.h b/yt/yt/client/ypath/rich.h
new file mode 100644
index 0000000000..142c87ad26
--- /dev/null
+++ b/yt/yt/client/ypath/rich.h
@@ -0,0 +1,194 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/attributes.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/library/erasure/public.h>
+
+#include <yt/yt/client/table_client/column_sort_schema.h>
+#include <yt/yt/client/table_client/schema.h>
+
+#include <yt/yt/client/chunk_client/read_limit.h>
+
+#include <yt/yt/client/transaction_client/public.h>
+
+#include <yt/yt/client/security_client/public.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! YPath string plus attributes.
+class TRichYPath
+{
+public:
+ TRichYPath();
+ TRichYPath(const TRichYPath& other);
+ TRichYPath(TRichYPath&& other);
+ TRichYPath(const char* path);
+ TRichYPath(const TYPath& path);
+ TRichYPath(const TYPath& path, const NYTree::IAttributeDictionary& attributes);
+ TRichYPath& operator = (const TRichYPath& other);
+
+ static TRichYPath Parse(const TString& str);
+ TRichYPath Normalize() const;
+
+ const TYPath& GetPath() const;
+ void SetPath(const TYPath& path);
+
+ const NYTree::IAttributeDictionary& Attributes() const;
+ NYTree::IAttributeDictionary& Attributes();
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+ // Attribute accessors.
+ // "append"
+ bool GetAppend(bool defaultValue = false) const;
+ void SetAppend(bool value);
+
+ // "teleport"
+ bool GetTeleport() const;
+
+ // "primary"
+ bool GetPrimary() const;
+
+ // "foreign"
+ bool GetForeign() const;
+ void SetForeign(bool value);
+
+ // "columns"
+ std::optional<std::vector<TString>> GetColumns() const;
+ void SetColumns(const std::vector<TString>& columns);
+
+ // "rename_columns"
+ std::optional<NTableClient::TColumnRenameDescriptors> GetColumnRenameDescriptors() const;
+
+ // "ranges"
+ //! COMPAT(ignat): also "lower_limit" and "upper_limit"
+ //! This method returns legacy read ranges completely ignoring key bounds.
+ std::vector<NChunkClient::TLegacyReadRange> GetRanges() const;
+
+ //! Get ranges, possibly transforming legacy keys into new key bounds using provided comparator.
+ //! This method throws if comparator is not present and keys are present.
+ //! In case when at least one column is non-ascending, some requirements are even stronger,
+ //! refer to the implementation for details.
+ std::vector<NChunkClient::TReadRange> GetNewRanges(
+ const NTableClient::TComparator& comparator = NTableClient::TComparator(),
+ const NTableClient::TKeyColumnTypes& conversionTypeHints = {}) const;
+
+ void SetRanges(const std::vector<NChunkClient::TReadRange>& ranges);
+ bool HasNontrivialRanges() const;
+
+ // "file_name"
+ std::optional<TString> GetFileName() const;
+
+ // "executable"
+ std::optional<bool> GetExecutable() const;
+
+ // "format"
+ NYson::TYsonString GetFormat() const;
+
+ // "schema"
+ NTableClient::TTableSchemaPtr GetSchema() const;
+
+ // "sorted_by"
+ NTableClient::TSortColumns GetSortedBy() const;
+ void SetSortedBy(const NTableClient::TSortColumns& value);
+
+ // "row_count_limit"
+ std::optional<i64> GetRowCountLimit() const;
+
+ // "timestamp"
+ std::optional<NTransactionClient::TTimestamp> GetTimestamp() const;
+
+ // "retention_timestamp"
+ std::optional<NTransactionClient::TTimestamp> GetRetentionTimestamp() const;
+
+ // "output_timestamp"
+ std::optional<NTransactionClient::TTimestamp> GetOutputTimestamp() const;
+
+ // "optimize_for"
+ std::optional<NTableClient::EOptimizeFor> GetOptimizeFor() const;
+
+ // "chunk_format"
+ std::optional<NChunkClient::EChunkFormat> GetChunkFormat() const;
+
+ // "compression_codec"
+ std::optional<NCompression::ECodec> GetCompressionCodec() const;
+
+ // "erasure_codec"
+ std::optional<NErasure::ECodec> GetErasureCodec() const;
+
+ // "auto_merge"
+ bool GetAutoMerge() const;
+
+ // "transaction_id"
+ std::optional<NObjectClient::TTransactionId> GetTransactionId() const;
+
+ // "security_tags"
+ std::optional<std::vector<NSecurityClient::TSecurityTag>> GetSecurityTags() const;
+
+ // "bypass_artifact_cache"
+ bool GetBypassArtifactCache() const;
+
+ // "schema_modification"
+ NTableClient::ETableSchemaModification GetSchemaModification() const;
+
+ // "partially_sorted"
+ bool GetPartiallySorted() const;
+
+ // "chunk_unique_keys"
+ std::optional<bool> GetChunkUniqueKeys() const;
+
+ // "copy_file"
+ std::optional<bool> GetCopyFile() const;
+
+ // "chunk_sort_columns"
+ std::optional<NTableClient::TSortColumns> GetChunkSortColumns() const;
+
+ // "cluster"
+ std::optional<TString> GetCluster() const;
+ void SetCluster(const TString& value);
+
+ // "clusters"
+ std::optional<std::vector<TString>> GetClusters() const;
+ void SetClusters(const std::vector<TString>& value);
+
+ // "create"
+ bool GetCreate() const;
+
+private:
+ TYPath Path_;
+ NYTree::IAttributeDictionaryPtr Attributes_;
+};
+
+bool operator== (const TRichYPath& lhs, const TRichYPath& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToString(const TRichYPath& path);
+
+std::vector<TRichYPath> Normalize(const std::vector<TRichYPath>& paths);
+
+void Serialize(const TRichYPath& richPath, NYson::IYsonConsumer* consumer);
+void Deserialize(TRichYPath& richPath, NYTree::INodePtr node);
+void Deserialize(TRichYPath& richPath, NYson::TYsonPullParserCursor* cursor);
+
+void ToProto(TString* protoPath, const TRichYPath& path);
+void FromProto(TRichYPath* path, const TString& protoPath);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
+
+template <>
+struct THash<NYT::NYPath::TRichYPath>
+{
+ size_t operator()(const NYT::NYPath::TRichYPath& richYPath) const;
+};
diff --git a/yt/yt/client/zookeeper/packet.cpp b/yt/yt/client/zookeeper/packet.cpp
new file mode 100644
index 0000000000..c9ec915778
--- /dev/null
+++ b/yt/yt/client/zookeeper/packet.cpp
@@ -0,0 +1,299 @@
+#include "packet.h"
+
+#include <library/cpp/yt/memory/chunked_memory_allocator.h>
+
+namespace NYT::NZookeeper {
+
+using namespace NBus;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EPacketPhase,
+ (Header)
+ (Message)
+ (Finished)
+);
+
+struct TPacketDecoderTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPacketTranscoderBase
+{
+ union {
+ int MessageSize;
+ char Data[sizeof(int)];
+ } Header_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPacketDecoder
+ : public IPacketDecoder
+ , public TPacketTranscoderBase
+{
+public:
+ TPacketDecoder()
+ : Allocator_(
+ PacketDecoderChunkSize,
+ TChunkedMemoryAllocator::DefaultMaxSmallBlockSizeRatio,
+ GetRefCountedTypeCookie<TPacketDecoderTag>())
+ {
+ Restart();
+ }
+
+ void Restart() override
+ {
+ PacketSize_ = 0;
+ Message_.Reset();
+
+ BeginPhase(
+ EPacketPhase::Header,
+ Header_.Data,
+ sizeof(Header_.Data));
+ }
+
+ bool IsInProgress() const override
+ {
+ return !IsFinished();
+ }
+
+ bool IsFinished() const override
+ {
+ return Phase_ == EPacketPhase::Finished;
+ }
+
+ TMutableRef GetFragment() override
+ {
+ return TMutableRef(FragmentPtr_, FragmentRemaining_);
+ }
+
+ bool Advance(size_t size) override
+ {
+ YT_ASSERT(FragmentRemaining_ != 0);
+ YT_ASSERT(size <= FragmentRemaining_);
+
+ PacketSize_ += size;
+ FragmentRemaining_ -= size;
+ FragmentPtr_ += size;
+ if (FragmentRemaining_ == 0) {
+ return EndPhase();
+ } else {
+ return true;
+ }
+ }
+
+ EPacketType GetPacketType() const override
+ {
+ return EPacketType::Message;
+ }
+
+ EPacketFlags GetPacketFlags() const override
+ {
+ return EPacketFlags::None;
+ }
+
+ TPacketId GetPacketId() const override
+ {
+ return {};
+ }
+
+ size_t GetPacketSize() const override
+ {
+ return PacketSize_;
+ }
+
+ TSharedRefArray GrabMessage() const override
+ {
+ return TSharedRefArray(Message_);
+ }
+
+private:
+ EPacketPhase Phase_ = EPacketPhase::Finished;
+ char* FragmentPtr_ = nullptr;
+ size_t FragmentRemaining_ = 0;
+
+ TChunkedMemoryAllocator Allocator_;
+ constexpr static i64 PacketDecoderChunkSize = 16_KB;
+
+ TSharedRef Message_;
+
+ size_t PacketSize_ = 0;
+
+ void BeginPhase(
+ EPacketPhase phase,
+ char* fragmentPtr,
+ size_t fragmentSize)
+ {
+ Phase_ = phase;
+ FragmentPtr_ = fragmentPtr;
+ FragmentRemaining_ = fragmentSize;
+ }
+
+ bool EndPhase()
+ {
+ switch (Phase_) {
+ case EPacketPhase::Header: {
+ std::reverse(std::begin(Header_.Data), std::end(Header_.Data));
+ auto messageSize = Header_.MessageSize;
+ auto message = Allocator_.AllocateAligned(messageSize);
+ BeginPhase(
+ EPacketPhase::Message,
+ /*fragmentPtr*/ message.begin(),
+ /*fragmentSize*/ message.size());
+ Message_ = std::move(message);
+
+ return true;
+ }
+ case EPacketPhase::Message:
+ BeginPhase(
+ EPacketPhase::Finished,
+ /*fragmentPtr*/ nullptr,
+ /*fragmentSize*/ 0);
+
+ return true;
+ default:
+ YT_ABORT();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPacketEncoder
+ : public IPacketEncoder
+ , public TPacketTranscoderBase
+{
+public:
+ size_t GetPacketSize(
+ EPacketType type,
+ const TSharedRefArray& /*message*/,
+ size_t messageSize) override
+ {
+ YT_ASSERT(type == EPacketType::Message);
+ YT_ASSERT(messageSize > 0);
+
+ return sizeof(Header_) + messageSize;
+ }
+
+ bool Start(
+ EPacketType type,
+ EPacketFlags flags,
+ bool /*generateChecksums*/,
+ int /*checksummedPartCount*/,
+ TPacketId /*packetId*/,
+ TSharedRefArray messageParts) override
+ {
+ YT_ASSERT(type == EPacketType::Message);
+ YT_ASSERT(flags == EPacketFlags::None);
+ YT_ASSERT(!messageParts.Empty());
+
+ i64 messageSize = 0;
+ for (const auto& messagePart : messageParts) {
+ messageSize += messagePart.size();
+ }
+ Header_.MessageSize = messageSize;
+ std::reverse(std::begin(Header_.Data), std::end(Header_.Data));
+
+ MessageParts_ = std::move(messageParts);
+
+ Phase_ = EPacketPhase::Header;
+ CurrentMessagePart_ = 0;
+
+ return true;
+ }
+
+ TMutableRef GetFragment() override
+ {
+ switch (Phase_) {
+ case EPacketPhase::Header:
+ return TMutableRef(Header_.Data, sizeof(Header_.Data));
+ case EPacketPhase::Message: {
+ const auto& messagePart = MessageParts_[CurrentMessagePart_];
+ return TMutableRef(
+ const_cast<char*>(messagePart.begin()),
+ messagePart.size());
+ }
+ default:
+ YT_ABORT();
+ }
+ }
+
+ bool IsFragmentOwned() const override
+ {
+ switch (Phase_) {
+ case EPacketPhase::Header:
+ return false;
+ case EPacketPhase::Message:
+ return true;
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void NextFragment() override
+ {
+ switch (Phase_) {
+ case EPacketPhase::Header:
+ Phase_ = EPacketPhase::Message;
+ CurrentMessagePart_ = 0;
+ break;
+ case EPacketPhase::Message:
+ if (CurrentMessagePart_ + 1 < MessageParts_.size()) {
+ ++CurrentMessagePart_;
+ } else {
+ Phase_ = EPacketPhase::Finished;
+ }
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+
+ bool IsFinished() const override
+ {
+ return Phase_ == EPacketPhase::Finished;
+ }
+
+private:
+ EPacketPhase Phase_ = EPacketPhase::Finished;
+
+ TSharedRefArray MessageParts_;
+ size_t CurrentMessagePart_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPacketTranscoderFactory
+ : public IPacketTranscoderFactory
+{
+ std::unique_ptr<IPacketDecoder> CreateDecoder(
+ const NLogging::TLogger& /*logger*/,
+ bool /*verifyChecksum*/) const override
+ {
+ return std::make_unique<TPacketDecoder>();
+ }
+
+ std::unique_ptr<IPacketEncoder> CreateEncoder(
+ const NLogging::TLogger& /*logger*/) const override
+ {
+ return std::make_unique<TPacketEncoder>();
+ }
+
+ bool SupportsHandshakes() const override
+ {
+ return false;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPacketTranscoderFactory* GetZookeeperPacketTranscoderFactory()
+{
+ return LeakySingleton<TPacketTranscoderFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/zookeeper/packet.h b/yt/yt/client/zookeeper/packet.h
new file mode 100644
index 0000000000..dd7eb806e1
--- /dev/null
+++ b/yt/yt/client/zookeeper/packet.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/tcp/packet.h>
+
+namespace NYT::NZookeeper {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NBus::IPacketTranscoderFactory* GetZookeeperPacketTranscoderFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/zookeeper/protocol.cpp b/yt/yt/client/zookeeper/protocol.cpp
new file mode 100644
index 0000000000..a450599d32
--- /dev/null
+++ b/yt/yt/client/zookeeper/protocol.cpp
@@ -0,0 +1,204 @@
+#include "protocol.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NZookeeper {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZookeeperProtocolReader
+ : public IZookeeperProtocolReader
+{
+public:
+ explicit TZookeeperProtocolReader(TSharedRef data)
+ : Data_(std::move(data))
+ { }
+
+ char ReadByte() override
+ {
+ return DoReadInt<char>();
+ }
+
+ int ReadInt() override
+ {
+ return DoReadInt<int>();
+ }
+
+ i64 ReadLong() override
+ {
+ return DoReadInt<i64>();
+ }
+
+ bool ReadBool() override
+ {
+ auto value = ReadByte();
+ return value > 0;
+ }
+
+ TString ReadString() override
+ {
+ TString result;
+ ReadString(&result);
+
+ return result;
+ }
+
+ void ReadString(TString* result) override
+ {
+ auto length = ReadInt();
+ ValidateSizeAvailable(length);
+
+ result->resize(length);
+ auto begin = Data_.begin() + Offset_;
+ std::copy(begin, begin + length, result->begin());
+ Offset_ += length;
+ }
+
+ TSharedRef GetSuffix() const override
+ {
+ return Data_.Slice(Offset_, Data_.Size());
+ }
+
+ bool IsFinished() const override
+ {
+ return Offset_ == std::ssize(Data_);
+ }
+
+ void ValidateFinished() const override
+ {
+ if (!IsFinished()) {
+ THROW_ERROR_EXCEPTION("Expected end of stream")
+ << TErrorAttribute("offset", Offset_)
+ << TErrorAttribute("message_size", Data_.size());
+ }
+ }
+
+private:
+ const TSharedRef Data_;
+ i64 Offset_ = 0;
+
+ template <typename T>
+ T DoReadInt()
+ {
+ ValidateSizeAvailable(sizeof(T));
+
+ union {
+ T value;
+ char bytes[sizeof(T)];
+ } result;
+
+ memcpy(result.bytes, Data_.begin() + Offset_, sizeof(T));
+ std::reverse(result.bytes, result.bytes + sizeof(T));
+ Offset_ += sizeof(T);
+
+ return result.value;
+ }
+
+ void ValidateSizeAvailable(i64 size)
+ {
+ if (std::ssize(Data_) - Offset_ < size) {
+ THROW_ERROR_EXCEPTION("Premature end of stream while reading %v bytes", size);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IZookeeperProtocolReader> CreateZookeeperProtocolReader(TSharedRef data)
+{
+ return std::make_unique<TZookeeperProtocolReader>(std::move(data));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZookeeperProtocolWriter
+ : public IZookeeperProtocolWriter
+{
+public:
+ TZookeeperProtocolWriter()
+ : Buffer_(AllocateBuffer(InitialBufferSize))
+ { }
+
+ void WriteByte(char value) override
+ {
+ DoWriteInt(value);
+ }
+
+ void WriteInt(int value) override
+ {
+ DoWriteInt(value);
+ }
+
+ void WriteLong(i64 value) override
+ {
+ DoWriteInt(value);
+ }
+
+ void WriteBool(bool value) override
+ {
+ WriteByte(value ? 1 : 0);
+ }
+
+ void WriteString(const TString& value) override
+ {
+ WriteInt(value.size());
+
+ EnsureFreeSpace(value.size());
+
+ std::copy(value.begin(), value.end(), Buffer_.begin() + Size_);
+ Size_ += value.size();
+ }
+
+ TSharedRef Finish() override
+ {
+ return Buffer_.Slice(0, Size_);
+ }
+
+private:
+ struct TZookeeperProtocolWriterTag
+ { };
+
+ constexpr static i64 InitialBufferSize = 16_KB;
+ constexpr static i64 BufferSizeMultiplier = 2;
+
+ TSharedMutableRef Buffer_;
+ i64 Size_ = 0;
+
+ template <typename T>
+ void DoWriteInt(T value)
+ {
+ EnsureFreeSpace(sizeof(T));
+
+ memcpy(Buffer_.begin() + Size_, &value, sizeof(T));
+ std::reverse(Buffer_.begin() + Size_, Buffer_.begin() + Size_ + sizeof(T));
+ Size_+= sizeof(T);
+ }
+
+ void EnsureFreeSpace(i64 size)
+ {
+ if (Size_ + size <= std::ssize(Buffer_)) {
+ return;
+ }
+
+ auto newSize = std::max<i64>(Size_ + size, Buffer_.size() * BufferSizeMultiplier);
+ auto newBuffer = AllocateBuffer(newSize);
+ std::copy(Buffer_.begin(), Buffer_.begin() + Size_, newBuffer.begin());
+ Buffer_ = std::move(newBuffer);
+ }
+
+ static TSharedMutableRef AllocateBuffer(i64 capacity)
+ {
+ return TSharedMutableRef::Allocate<TZookeeperProtocolWriterTag>(capacity);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IZookeeperProtocolWriter> CreateZookeeperProtocolWriter()
+{
+ return std::make_unique<TZookeeperProtocolWriter>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/zookeeper/protocol.h b/yt/yt/client/zookeeper/protocol.h
new file mode 100644
index 0000000000..29fea1da57
--- /dev/null
+++ b/yt/yt/client/zookeeper/protocol.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NZookeeper {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IZookeeperProtocolReader
+{
+ virtual ~IZookeeperProtocolReader() = default;
+
+ virtual char ReadByte() = 0;
+ virtual int ReadInt() = 0;
+ virtual i64 ReadLong() = 0;
+
+ virtual bool ReadBool() = 0;
+
+ virtual TString ReadString() = 0;
+ virtual void ReadString(TString* result) = 0;
+
+ virtual TSharedRef GetSuffix() const = 0;
+
+ //! Returns true if input is fully consumed and false otherwise.
+ virtual bool IsFinished() const = 0;
+ //! Throws an error if input is not fully consumed. Does nothing otherwise.
+ virtual void ValidateFinished() const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IZookeeperProtocolReader> CreateZookeeperProtocolReader(TSharedRef data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IZookeeperProtocolWriter
+{
+ virtual ~IZookeeperProtocolWriter() = default;
+
+ virtual void WriteByte(char value) = 0;
+ virtual void WriteInt(int value) = 0;
+ virtual void WriteLong(i64 value) = 0;
+
+ virtual void WriteBool(bool value) = 0;
+
+ virtual void WriteString(const TString& value) = 0;
+
+ virtual TSharedRef Finish() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IZookeeperProtocolWriter> CreateZookeeperProtocolWriter();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/zookeeper/public.h b/yt/yt/client/zookeeper/public.h
new file mode 100644
index 0000000000..7f3c7c6823
--- /dev/null
+++ b/yt/yt/client/zookeeper/public.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NZookeeperClient {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TZookeeperPath = TString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TSessionId = ui64;
+
+constexpr TSessionId NullSessionId = 0;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeperClient
diff --git a/yt/yt/client/zookeeper/requests.cpp b/yt/yt/client/zookeeper/requests.cpp
new file mode 100644
index 0000000000..26eafd1a03
--- /dev/null
+++ b/yt/yt/client/zookeeper/requests.cpp
@@ -0,0 +1,28 @@
+#include "requests.h"
+
+namespace NYT::NZookeeper {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TReqStartSession::Deserialize(IZookeeperProtocolReader* reader)
+{
+ ProtocolVersion = reader->ReadInt();
+ LastZxidSeen = reader->ReadLong();
+ Timeout = TDuration::MilliSeconds(reader->ReadInt());
+ SessionId = reader->ReadLong();
+ Password = reader->ReadString();
+ ReadOnly = reader->ReadBool();
+}
+
+void TRspStartSession::Serialize(IZookeeperProtocolWriter* writer) const
+{
+ writer->WriteInt(ProtocolVersion);
+ writer->WriteInt(Timeout.MilliSeconds());
+ writer->WriteLong(SessionId);
+ writer->WriteString(Password);
+ writer->WriteBool(ReadOnly);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/client/zookeeper/requests.h b/yt/yt/client/zookeeper/requests.h
new file mode 100644
index 0000000000..14a1e20c17
--- /dev/null
+++ b/yt/yt/client/zookeeper/requests.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "public.h"
+
+#include "protocol.h"
+
+namespace NYT::NZookeeper {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ERequestType,
+ ((None) (-1))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReqStartSession
+{
+ int ProtocolVersion = -1;
+ i64 LastZxidSeen = -1;
+ TDuration Timeout = TDuration::Zero();
+ i64 SessionId = -1;
+ TString Password;
+ bool ReadOnly = false;
+
+ void Deserialize(IZookeeperProtocolReader* reader);
+};
+
+struct TRspStartSession
+{
+ int ProtocolVersion = -1;
+ TDuration Timeout = TDuration::Zero();
+ i64 SessionId = -1;
+ TString Password;
+ bool ReadOnly = false;
+
+ void Serialize(IZookeeperProtocolWriter* writer) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NZookeeper
diff --git a/yt/yt/core/actions/bind-inl.h b/yt/yt/core/actions/bind-inl.h
new file mode 100644
index 0000000000..f88d3116d9
--- /dev/null
+++ b/yt/yt/core/actions/bind-inl.h
@@ -0,0 +1,603 @@
+#ifndef BIND_INL_H_
+#error "Direct inclusion of this file is not allowed, include bind.h"
+// For the sake of sane code completion.
+#include "bind.h"
+#endif
+#undef BIND_INL_H_
+
+#include <yt/yt/core/concurrency/propagating_storage.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+class TUnretainedWrapper
+{
+public:
+ explicit TUnretainedWrapper(T* x)
+ : T_(x)
+ { }
+
+ T* Get() const
+ {
+ return T_;
+ }
+
+private:
+ T* T_;
+};
+
+template <class T>
+class TOwnedWrapper
+{
+public:
+ explicit TOwnedWrapper(T* x)
+ : T_(x)
+ { }
+
+ TOwnedWrapper(const TOwnedWrapper& other)
+ : T_(other.T_)
+ {
+ other.T_ = nullptr;
+ }
+
+ ~TOwnedWrapper()
+ {
+ delete T_;
+ }
+
+ T* Get() const
+ {
+ return T_;
+ }
+
+private:
+ mutable T* T_;
+};
+
+template <class T>
+class TPassedWrapper
+{
+public:
+ explicit TPassedWrapper(T&& x)
+ : IsValid_(true)
+ , T_(std::move(x))
+ { }
+
+ TPassedWrapper(const TPassedWrapper& other)
+ : IsValid_(other.IsValid_)
+ , T_(std::move(other.T_))
+ {
+ other.IsValid_ = false;
+ }
+
+ TPassedWrapper(TPassedWrapper&& other)
+ : IsValid_(other.IsValid_)
+ , T_(std::move(other.T_))
+ {
+ other.IsValid_ = false;
+ }
+
+ T&& Get() const
+ {
+ YT_ASSERT(IsValid_);
+ IsValid_ = false;
+ return std::move(T_);
+ }
+
+private:
+ mutable bool IsValid_;
+ mutable T T_;
+};
+
+template <class T>
+class TConstRefWrapper
+{
+public:
+ explicit TConstRefWrapper(const T& x)
+ : T_(&x)
+ { }
+
+ const T& Get() const
+ {
+ return *T_;
+ }
+
+private:
+ const T* T_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+inline const T& Unwrap(T& value)
+{
+ return value;
+}
+
+template <class T>
+inline T* Unwrap(const TUnretainedWrapper<T>& wrapper)
+{
+ return wrapper.Get();
+}
+
+template <class T>
+inline T* Unwrap(const TOwnedWrapper<T>& wrapper)
+{
+ return wrapper.Get();
+}
+
+template <class T>
+inline T&& Unwrap(const TPassedWrapper<T>& wrapper)
+{
+ return wrapper.Get();
+}
+
+template <class T>
+inline const T& Unwrap(const TConstRefWrapper<T>& wrapper)
+{
+ return wrapper.Get();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TIgnoreResultWrapper
+{
+public:
+ explicit TIgnoreResultWrapper(const T& functor)
+ : Functor_(functor)
+ { }
+
+ T& Get()
+ {
+ return Functor_;
+ }
+
+private:
+ T Functor_;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+static auto IgnoreResult(const T& x)
+{
+ return NYT::NDetail::TIgnoreResultWrapper<T>(x);
+}
+
+template <class T>
+static auto Unretained(T* x)
+{
+ return NYT::NDetail::TUnretainedWrapper<T>(x);
+}
+
+template <class T>
+static auto Owned(T* x)
+{
+ return NYT::NDetail::TOwnedWrapper<T>(x);
+}
+
+template <class T>
+static auto Passed(T&& x)
+{
+ return NYT::NDetail::TPassedWrapper<T>(std::forward<T>(x));
+}
+
+template <class T>
+static auto ConstRef(const T& x)
+{
+ return NYT::NDetail::TConstRefWrapper<T>(x);
+}
+
+template <class U>
+static U& WrapToPassed(U& arg)
+{
+ return arg;
+}
+
+template <class U>
+static auto WrapToPassed(U&& arg)
+{
+ return Passed(std::move(arg));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class F>
+struct TFunctorTraits;
+
+template <class TMethod>
+class TMethodInvoker
+{
+public:
+ explicit TMethodInvoker(TMethod method)
+ : Method_(method)
+ { }
+
+ template <class D, class... XAs>
+ auto operator()(D* object, XAs&&... args) const
+ {
+ static_assert(
+ !std::is_array_v<D>,
+ "First bound argument to a method cannot be an array");
+
+ return (object->*Method_)(std::forward<XAs>(args)...);
+ }
+
+ template <class D, class... XAs>
+ void operator()(const TWeakPtr<D>& ref, XAs&&... args) const
+ {
+ using TResult = typename TFunctorTraits<TMethod>::TResult;
+ static_assert(
+ std::is_void_v<TResult>,
+ "Weak calls are only supported for methods with a void return type");
+
+ auto strongRef = ref.Lock();
+ auto* object = strongRef.Get();
+
+ if (!object) {
+ return;
+ }
+
+ (object->*Method_)(std::forward<XAs>(args)...);
+ }
+
+ template <class D, class... XAs>
+ auto operator()(const TIntrusivePtr<D>& ref, XAs&&... args) const
+ {
+ auto* object = ref.Get();
+ return (object->*Method_)(std::forward<XAs>(args)...);
+ }
+
+private:
+ const TMethod Method_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TToVoidSignature;
+
+template <class TR, class... TAs>
+struct TToVoidSignature<TR(TAs...)>
+{
+ using TType = void (TAs...);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class>
+struct TCallableSignature;
+
+template <class TR, class TC, class... TAs>
+struct TCallableSignature<TR (TC::*)(TAs...) const>
+{
+ using TSignature = TR (TAs...);
+};
+
+template <class TR, class TC, class... TAs>
+struct TCallableSignature<TR (TC::*)(TAs...)>
+{
+ using TSignature = TR (TAs...);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Matches functor and TCallback
+template <class F>
+struct TFunctorTraits
+ : public TCallableSignature<decltype(&F::operator())>
+{
+ using TInvoker = F;
+};
+
+template <class TR, class... TAs>
+struct TFunctorTraits<TR (TAs...)>
+{
+ using TInvoker = TR (*)(TAs...);
+ using TSignature = TR (TAs...);
+};
+
+// Matches function.
+template <class TR, class... TAs>
+struct TFunctorTraits<TR (*)(TAs...)>
+{
+ using TInvoker = TR (*)(TAs...);
+ using TSignature = TR (TAs...);
+};
+
+// Matches method.
+template <class TR, class C, class... TAs>
+struct TFunctorTraits<TR (C::*)(TAs...)>
+{
+ using TInvoker = TMethodInvoker<TR (C::*)(TAs...)>;
+ using TSignature = TR (C*, TAs...);
+ using TResult = TR;
+};
+
+template <class TR, class C, class... TAs>
+struct TFunctorTraits<TR (C::*)(TAs...) const>
+{
+ using TInvoker = TMethodInvoker<TR (C::*)(TAs...) const>;
+ using TSignature = TR (const C*, TAs...);
+ using TResult = TR;
+};
+
+template <class TR, class C, class... TAs>
+struct TFunctorTraits<TR (C::*)(TAs...) noexcept>
+{
+ using TInvoker = TMethodInvoker<TR (C::*)(TAs...) noexcept>;
+ using TSignature = TR (C*, TAs...);
+ using TResult = TR;
+};
+
+template <class TR, class C, class... TAs>
+struct TFunctorTraits<TR (C::*)(TAs...) const noexcept>
+{
+ using TInvoker = TMethodInvoker<TR (C::*)(TAs...) const noexcept>;
+ using TSignature = TR (const C*, TAs...);
+ using TResult = TR;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TOmitResultInvoker
+{
+public:
+ using TInvoker = typename TFunctorTraits<T>::TInvoker;
+
+ explicit TOmitResultInvoker(NDetail::TIgnoreResultWrapper<T>&& invoker)
+ : Invoker_(std::move(invoker.Get()))
+ { }
+
+ template <class... XAs>
+ void operator()(XAs&&... args) const
+ {
+ Invoker_(std::forward<XAs>(args)...);
+ }
+
+private:
+ TInvoker Invoker_;
+};
+
+template <class T>
+struct TFunctorTraits<NDetail::TIgnoreResultWrapper<T>>
+{
+ using TInvoker = TOmitResultInvoker<T>;
+ using TSignature = typename TToVoidSignature<typename TFunctorTraits<T>::TSignature>::TType;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TIsNonConstReference
+ : public std::false_type
+{ };
+
+template <class T>
+struct TIsNonConstReference<T&>
+ : public std::true_type
+{ };
+
+template <class T>
+struct TIsNonConstReference<const T&>
+ : public std::false_type
+{ };
+
+template <class T>
+struct TCheckNoRawPtrToRefCountedType
+{
+ static_assert(
+ !(std::is_pointer_v<T> && (
+ std::is_convertible_v<T, const TRefCounted*> ||
+ std::is_convertible_v<T, TRefCounted*>
+ )),
+ "T has reference-counted type and should not be bound by the raw pointer");
+};
+
+template <class... TArgs>
+struct TCheckParamsNoRawPtrToRefCountedType
+ : public std::tuple<TCheckNoRawPtrToRefCountedType<TArgs>...>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <unsigned N, class TSignature>
+struct TSplitHelper;
+
+template <unsigned N, class TSignature>
+struct TSplit
+ : public TSplitHelper<N, TSignature>
+{ };
+
+template <class TSignature>
+struct TSplit<0, TSignature>
+{
+ using TResult = TSignature;
+};
+
+template <unsigned N, class TR, class TA0, class... TAs>
+struct TSplitHelper<N, TR (TA0, TAs...)>
+ : public TSplit<N - 1, TR (TAs...)>
+{
+ static_assert(
+ !TIsNonConstReference<TA0>::value,
+ "T is a non-const reference and should not be bound.");
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <bool Propagate>
+class TPropagateMixin;
+
+template <>
+class TPropagateMixin<true>
+{
+public:
+ TPropagateMixin()
+ : Storage_(NConcurrency::GetCurrentPropagatingStorage())
+ { }
+
+ NConcurrency::TPropagatingStorageGuard MakePropagatingStorageGuard()
+ {
+ return NConcurrency::TPropagatingStorageGuard(Storage_);
+ }
+
+private:
+ const NConcurrency::TPropagatingStorage Storage_;
+};
+
+template <>
+class TPropagateMixin<false>
+{
+public:
+ std::monostate MakePropagatingStorageGuard()
+ {
+ return {};
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <bool Propagate, class TFunctor, class TSequence, class... TBs>
+class TBindState;
+
+template <bool Propagate, class TFunctor, class... TBs, size_t... BoundIndexes>
+class TBindState<Propagate, TFunctor, std::index_sequence<BoundIndexes...>, TBs...>
+ : public NDetail::TBindStateBase
+ , public TPropagateMixin<Propagate>
+{
+public:
+ template <class XFunctor, class... XBs>
+ TBindState(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const NYT::TSourceLocation& location,
+#endif
+ XFunctor&& functor,
+ XBs&&... boundArgs)
+ : TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ location
+#endif
+ )
+ , Functor(std::forward<XFunctor>(functor))
+ , BoundArgs(std::forward<XBs>(boundArgs)...)
+ { }
+
+ // Keep minimum frame count.
+ template <class... TAs>
+ static auto Run(TCallArg<TAs>... args, NDetail::TBindStateBase* base)
+ {
+ auto* volatile state = static_cast<TBindState*>(base);
+
+ // Prevent optimizing |state| away for GDB printer.
+ // See devtools/gdb/yt_fibers_printer.py.
+ auto* volatile unoptimizedState = state;
+ Y_UNUSED(unoptimizedState);
+
+ auto propagatingStorageGuard = state->MakePropagatingStorageGuard();
+ Y_UNUSED(propagatingStorageGuard);
+
+ return state->Functor(
+ NDetail::Unwrap(std::get<BoundIndexes>(state->BoundArgs))...,
+ std::forward<TAs>(args)...);
+ }
+
+private:
+ TFunctor Functor;
+ const std::tuple<TBs...> BoundArgs;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSignature>
+struct TBindHelper;
+
+template <class TR, class... TAs>
+struct TBindHelper<TR(TAs...)>
+{
+ template <class TState>
+ static constexpr auto GetInvokeFunction()
+ {
+ return &TState::template Run<TAs...>;
+ }
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ bool Propagate,
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ class TTag,
+ int Counter,
+#endif
+ class TFunctor,
+ class... TBs>
+auto Bind(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ TFunctor&& functor,
+ TBs&&... bound)
+{
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ Y_UNUSED(location);
+#endif
+
+ using TTraits = NDetail::TFunctorTraits<typename std::decay_t<TFunctor>>;
+ using TRunSignature = typename NDetail::TSplit<sizeof...(TBs), typename TTraits::TSignature>::TResult;
+
+ NYT::NDetail::TCheckParamsNoRawPtrToRefCountedType<typename std::decay_t<TBs>...> checkParamsIsRawPtrToRefCountedType;
+ Y_UNUSED(checkParamsIsRawPtrToRefCountedType);
+
+ using TState = NYT::NDetail::TBindState<
+ Propagate,
+ typename TTraits::TInvoker,
+ typename std::make_index_sequence<sizeof...(TBs)>,
+ typename std::decay_t<TBs>...>;
+
+ using THelper = NYT::NDetail::TBindHelper<TRunSignature>;
+
+ return TExtendedCallback<TRunSignature>{
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ NewWithLocation<TState, TTag, Counter>(location, location, std::forward<TFunctor>(functor), std::forward<TBs>(bound)...),
+#else
+ New<TState>(std::forward<TFunctor>(functor), std::forward<TBs>(bound)...),
+#endif
+ THelper::template GetInvokeFunction<TState>()};
+}
+
+template <
+ bool Propagate,
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ class TTag,
+ int Counter,
+#endif
+ class T
+>
+auto Bind(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ const TCallback<T>& callback)
+{
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ Y_UNUSED(location);
+#endif
+ return TExtendedCallback<T>(callback);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/bind.h b/yt/yt/core/actions/bind.h
new file mode 100644
index 0000000000..5c4ec018a6
--- /dev/null
+++ b/yt/yt/core/actions/bind.h
@@ -0,0 +1,233 @@
+#pragma once
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// This file defines a set of argument wrappers that can be used specify
+// the reference counting and reference semantics of arguments that are bound
+// by the #Bind() function in "bind.h".
+//
+////////////////////////////////////////////////////////////////////////////////
+//
+// ARGUMENT BINDING WRAPPERS
+//
+// The wrapper functions are #Unretained(), #Owned(), #Passed() and #ConstRef().
+//
+// #Unretained() allows #Bind() to bind a non-reference counted class,
+// and to disable reference counting on arguments that are reference counted
+// objects.
+//
+// #Owned() transfers ownership of an object to the #TCallback<> resulting from
+// binding; the object will be deleted when the #TCallback<> is deleted.
+//
+// #Passed() is for transferring movable-but-not-copyable types through
+// a #TCallback<>. Logically, this signifies a destructive transfer of the state
+// of the argument into the target function. Invoking #TCallback<>::operator() twice
+// on a TCallback that was created with a #Passed() argument will YT_ASSERT()
+// because the first invocation would have already transferred ownership
+// to the target function.
+//
+// #ConstRef() allows binding a const reference to an argument rather than
+// a copy.
+//
+//
+// EXAMPLE OF Unretained()
+//
+// class TFoo {
+// public:
+// void Bar() { Cout << "Hello!" << Endl; }
+// };
+//
+// // Somewhere else.
+// TFoo foo;
+// TClosure cb = Bind(&TFoo::Bar, Unretained(&foo));
+// cb(); // Prints "Hello!".
+//
+// Without the #Unretained() wrapper on |&foo|, the above call would fail
+// to compile because |TFoo| does not support the Ref() and Unref() methods.
+//
+//
+// EXAMPLE OF Owned()
+//
+// void Foo(int* arg) { Cout << *arg << Endl; }
+//
+// int* px = new int(17);
+// TClosure cb = Bind(&Foo, Owned(px));
+//
+// cb(); // Prints "17"
+// cb(); // Prints "17"
+// *px = 42;
+// cb(); // Prints "42"
+//
+// cb.Reset(); // |px| is deleted.
+// // Also will happen when |cb| goes out of scope.
+//
+// Without #Owned(), someone would have to know to delete |px| when the last
+// reference to the #TCallback<> is deleted.
+//
+//
+// EXAMPLE OF Passed()
+//
+// void TakesOwnership(TIntrusivePtr<TFoo> arg) { ... }
+// TIntrusivePtr<TFoo> CreateFoo() { return New<TFoo>(); }
+// TIntrusivePtr<TFoo> foo = New<TFoo>();
+//
+// // |cb| is given ownership of the |TFoo| instance. |foo| is now NULL.
+// // You may also use std::move(foo), but its more verbose.
+// TClosure cb = Bind(&TakesOwnership, Passed(&foo));
+//
+// // Operator() was never called so |cb| still owns the instance and deletes
+// // it on #Reset().
+// cb.Reset();
+//
+// // |cb| is given a new |TFoo| created by |CreateFoo()|.
+// TClosure cb = Bind(&TakesOwnership, Passed(CreateFoo()));
+//
+// // |arg| in TakesOwnership() is given ownership of |TFoo|.
+// // |cb| no longer owns |TFoo| and, if reset, would not delete anything.
+// cb(); // |TFoo| is now transferred to |arg| and deleted.
+// cb(); // This YT_ASSERT()s since |TFoo| already been used once.
+//
+//
+// EXAMPLE OF ConstRef()
+//
+// void Foo(int arg) { Cout << arg << Endl; }
+//
+// int n = 1;
+// TClosure noRef = Bind(&Foo, n);
+// TClosure hasRef = Bind(&Foo, ConstRef(n));
+//
+// noRef(); // Prints "1"
+// hasRef(); // Prints "1"
+// n = 2;
+// noRef(); // Prints "1"
+// hasRef(); // Prints "2"
+//
+// Note that because #ConstRef() takes a reference on |n|,
+// |n| must outlive all its bound callbacks.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+static auto Unretained(T* x);
+
+template <class T>
+static auto Owned(T* x);
+
+template <class T>
+static auto Passed(T&& x);
+
+template <class T>
+static auto ConstRef(const T& x);
+
+template <class T>
+static auto IgnoreResult(const T& x);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TFuture;
+
+// NB: Needed here due to TExtendedCallback::Via and TExtendedCallback::AsyncVia.
+template <class T>
+struct TFutureTraits
+{
+ using TUnderlying = T;
+ using TWrapped = TFuture<T>;
+};
+
+template <class T>
+struct TFutureTraits<TFuture<T>>
+{
+ using TUnderlying = T;
+ using TWrapped = TFuture<T>;
+};
+
+
+template <class TSignature>
+struct TExtendedCallback;
+
+template <class TR, class... TAs>
+struct TExtendedCallback<TR(TAs...)>
+ : public TCallback<TR(TAs...)>
+{
+ using TCallback<TR(TAs...)>::TCallback;
+
+ TExtendedCallback(const TCallback<TR(TAs...)>& callback)
+ : TCallback<TR(TAs...)>(callback)
+ { }
+#ifndef __cpp_impl_three_way_comparison
+ using TCallback<TR(TAs...)>::operator ==;
+ using TCallback<TR(TAs...)>::operator !=;
+#endif
+ // TODO: Make &&
+ TExtendedCallback Via(TIntrusivePtr<IInvoker> invoker) const;
+
+ TExtendedCallback<typename TFutureTraits<TR>::TWrapped(TAs...)>
+ AsyncVia(TIntrusivePtr<IInvoker> invoker) const;
+
+ //! This version of AsyncVia is designed for cases when invoker may discard submitted callbacks
+ //! (for example, if it is cancellable invoker). Regular AsyncVia results in "Promise abandoned"
+ //! error, which is almost non-informative and quite frustrating, while this overload
+ //! allows you to specify the cancellation error, which costs a bit more allocations
+ //! but much more convenient.
+ TExtendedCallback<typename TFutureTraits<TR>::TWrapped(TAs...)>
+ AsyncViaGuarded(TIntrusivePtr<IInvoker> invoker, TError cancellationError) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ bool Propagate,
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ class TTag,
+ int Counter,
+#endif
+ class TFunctor,
+ class... TBs>
+auto Bind(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ TFunctor&& functor,
+ TBs&&... bound);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ bool Propagate,
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ class TTag,
+ int Counter,
+#endif
+ class T
+>
+auto Bind(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ const TCallback<T>& callback);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ #define BIND_IMPL(propagate, ...) ::NYT::Bind<propagate, ::NYT::TCurrentTranslationUnitTag, __COUNTER__>(FROM_HERE, __VA_ARGS__)
+#else
+ #define BIND_IMPL(propagate, ...) ::NYT::Bind<propagate>(__VA_ARGS__)
+#endif
+
+#define BIND(...) BIND_IMPL(true, __VA_ARGS__)
+#define BIND_NO_PROPAGATE(...) BIND_IMPL(false, __VA_ARGS__)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define BIND_INL_H_
+#include "bind-inl.h"
+#undef BIND_INL_H_
diff --git a/yt/yt/core/actions/callback.h b/yt/yt/core/actions/callback.h
new file mode 100644
index 0000000000..762a2c278a
--- /dev/null
+++ b/yt/yt/core/actions/callback.h
@@ -0,0 +1,261 @@
+#pragma once
+
+// NOTE: Header files that do not require the full definition of #TCallback<> or
+// #TClosure should include "public.h" instead of this file.
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// WHAT IS THIS
+//
+// The templated #TCallback<> class is a generalized function object.
+// Together with the #Bind() function in "bind.h" they provide a type-safe
+// method for performing currying of arguments and creating a "closure".
+//
+// In programming languages, a closure is a first-class function where all its
+// parameters have been bound (usually via currying). Closures are well-suited
+// for representing and passing around a unit of delayed execution.
+//
+//
+// MEMORY MANAGEMENT AND PASSING
+//
+// The #TCallback<> objects themselves should be passed by const reference, and
+// stored by copy. They internally store their state in a reference-counted
+// class and thus do not need to be deleted.
+//
+// The reason to pass via a const reference is to avoid unnecessary
+// Ref/Unref pairs to the internal state.
+//
+// However, the #TCallback<> have Ref/Unref-efficient move constructors and
+// assignment operators so they also may be efficiently moved.
+//
+//
+// EXAMPLE USAGE
+//
+// (see "bind_ut.cpp")
+//
+//
+// HOW THE IMPLEMENTATION WORKS:
+//
+// There are three main components to the system:
+// 1) The #TCallback<> classes.
+// 2) The #Bind() functions.
+// 3) The arguments wrappers (e.g., #Unretained() and #ConstRef()).
+//
+// The #TCallback<> classes represent a generic function pointer. Internally,
+// it stores a reference-counted piece of state that represents the target
+// function and all its bound parameters. Each #TCallback<> specialization has
+// a templated constructor that takes an #TBindState<>*. In the context of
+// the constructor, the static type of this #TBindState<> pointer uniquely
+// identifies the function it is representing, all its bound parameters,
+// and operator() that is capable of invoking the target.
+//
+// #TCallback<>'s constructor takes the #TBindState<>* that has the full static
+// type and erases the target function type as well as the types of the bound
+// parameters. It does this by storing a pointer to the specific operator()
+// and upcasting the state of #TBindState<>* to a #TBindStateBase*.
+// This is safe as long as this #TBindStateBase pointer is only used with
+// the stored operator() pointer.
+//
+// To #TBindState<> objects are created inside the #Bind() functions.
+// These functions, along with a set of internal templates, are responsible for:
+//
+// - Unwrapping the function signature into return type, and parameters,
+// - Determining the number of parameters that are bound,
+// - Creating the #TBindState<> storing the bound parameters,
+// - Performing compile-time asserts to avoid error-prone behavior,
+// - Returning a #TCallback<> with an arity matching the number of unbound
+// parameters and that knows the correct reference counting semantics for
+// the target object if we are binding a method.
+//
+// The #Bind() functions do the above using type-inference, and template
+// specializations.
+//
+// By default #Bind() will store copies of all bound parameters, and attempt
+// to reference count a target object if the function being bound is
+// a class method.
+//
+// To change this behavior, we introduce a set of argument wrappers
+// (e.g., #Unretained(), and #ConstRef()). These are simple container templates
+// that are passed by value, and wrap a pointer to an argument.
+// See the file-level comment in "bind.h" for more information.
+//
+// These types are passed to #Unwrap() functions, and #TMaybeRefCountHelper()
+// functions respectively to modify the behavior of #Bind(). #Unwrap()
+// and #TMaybeRefCountHelper() functions change behavior by doing partial
+// specialization based on whether or not a parameter is a wrapper type.
+//
+// #ConstRef() is similar to #tr1::cref().
+// #Unretained() is specific.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include "public.h"
+#include "callback_internal.h"
+
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+#include <yt/yt/core/misc/source_location.h>
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// We pass trivially copyable arguments by value. This helps to avoid
+// putting them on the stack sometimes.
+//
+// Kudos to folly::Function authors.
+template <typename T>
+using TCallArg = std::conditional_t<std::is_trivially_copyable_v<T>, T, T&&>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class S1, class S2>
+struct TCallableBindState;
+
+template <class S1, class R2, class... TArgs2>
+struct TCallableBindState<S1, R2(TArgs2...)>
+ : public NYT::NDetail::TBindStateBase
+{
+ TCallback<S1> Callback;
+
+ explicit TCallableBindState(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ TCallback<S1> callback)
+ : NYT::NDetail::TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ location
+#endif
+ )
+ , Callback(std::move(callback))
+ { }
+
+ static R2 Run(TCallArg<TArgs2>... args, NYT::NDetail::TBindStateBase* base)
+ {
+ auto* state = static_cast<TCallableBindState*>(base);
+ return state->Callback(std::forward<TArgs2>(args)...);
+ }
+};
+
+template <class S1, class... TArgs2>
+struct TCallableBindState<S1, void(TArgs2...)>
+ : public NYT::NDetail::TBindStateBase
+{
+ TCallback<S1> Callback;
+
+ explicit TCallableBindState(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location,
+#endif
+ TCallback<S1> callback)
+ : NYT::NDetail::TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ location
+#endif
+ )
+ , Callback(std::move(callback))
+ { }
+
+ static void Run(TCallArg<TArgs2>... args, NYT::NDetail::TBindStateBase* base)
+ {
+ auto* state = static_cast<TCallableBindState*>(base);
+ state->Callback(std::forward<TArgs2>(args)...);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class R, class... TArgs>
+class TCallback<R(TArgs...)>
+ : public NYT::NDetail::TCallbackBase
+{
+private:
+ // We pass TBindStateBase as a last argument.
+ // Thus, all the arguments for calling the user's function are already
+ // at the right registers.
+ //
+ // Kudos to folly::Function authors.
+ typedef R(*TTypedInvokeFunction)(TCallArg<TArgs>..., NYT::NDetail::TBindStateBase*);
+
+public:
+ typedef R(TSignature)(TArgs...);
+
+ TCallback()
+ : TCallbackBase(TIntrusivePtr<NYT::NDetail::TBindStateBase>())
+ { }
+
+ TCallback(const TCallback& other)
+ : TCallbackBase(other)
+ { }
+
+ TCallback(TCallback&& other) noexcept
+ : TCallbackBase(std::move(other))
+ { }
+
+ TCallback(TIntrusivePtr<NYT::NDetail::TBindStateBase>&& bindState, TTypedInvokeFunction invokeFunction)
+ : TCallbackBase(std::move(bindState))
+ {
+ UntypedInvoke = reinterpret_cast<TUntypedInvokeFunction>(invokeFunction);
+ }
+
+ template <class R2, class... TArgs2>
+ explicit operator TCallback<R2(TArgs2...)>() const
+ {
+ return CastImpl<R2, TArgs2...>();
+ }
+
+ template <class... TArgs2>
+ operator TCallback<R(TArgs2...)>() const
+ {
+ return CastImpl<R, TArgs2...>();
+ }
+
+#ifndef __cpp_impl_three_way_comparison
+ using TCallbackBase::operator ==;
+ using TCallbackBase::operator !=;
+#endif
+
+ TCallback& operator=(const TCallback& other)
+ {
+ TCallback(other).Swap(*this);
+ return *this;
+ }
+
+ TCallback& operator=(TCallback&& other)
+ {
+ TCallback(std::move(other)).Swap(*this);
+ return *this;
+ }
+
+ R operator()(TArgs... args) const
+ {
+ auto invokeFunction = reinterpret_cast<TTypedInvokeFunction>(UntypedInvoke);
+ return invokeFunction(std::forward<TArgs>(args)..., BindState.Get());
+ }
+
+ R Run(TArgs... args) const
+ {
+ return operator()(std::forward<TArgs>(args)...);
+ }
+
+private:
+ template <class R2, class... TArgs2>
+ TCallback<R2(TArgs2...)> CastImpl() const
+ {
+ typedef TCallableBindState<R(TArgs...), R2(TArgs2...)> TBindState;
+
+ return TCallback<R2(TArgs2...)>(
+ New<TBindState>(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ BindState->Location,
+#endif
+ *this),
+ &TBindState::Run);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/actions/callback_internal-inl.h b/yt/yt/core/actions/callback_internal-inl.h
new file mode 100644
index 0000000000..aae361f5c1
--- /dev/null
+++ b/yt/yt/core/actions/callback_internal-inl.h
@@ -0,0 +1,71 @@
+#ifndef CALLBACK_INTERNAL_INL_H_
+#error "Direct inclusion of this file is not allowed, include callback_internal.h"
+// For the sake of sane code completion.
+#include "callback_internal.h"
+#endif
+#undef CALLBACK_INTERNAL_INL_H_
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TBindStateBase::TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location
+#endif
+ )
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ : Location(location)
+#endif
+{ }
+
+inline TCallbackBase::operator bool() const
+{
+ return static_cast<bool>(BindState);
+}
+
+inline void TCallbackBase::Reset()
+{
+ BindState = nullptr;
+ UntypedInvoke = nullptr;
+}
+
+inline void* TCallbackBase::GetHandle() const
+{
+ return (void*)((size_t)(void*)BindState.Get() ^ (size_t)(void*)UntypedInvoke);
+}
+
+inline void TCallbackBase::Swap(TCallbackBase& other)
+{
+ auto tempBindState = std::move(other.BindState);
+ auto tempUntypedInvoke = std::move(other.UntypedInvoke);
+
+ other.BindState = std::move(BindState);
+ other.UntypedInvoke = std::move(UntypedInvoke);
+
+ BindState = std::move(tempBindState);
+ UntypedInvoke = std::move(tempUntypedInvoke);
+}
+
+#ifndef __cpp_impl_three_way_comparison
+inline bool TCallbackBase::operator == (const TCallbackBase& other) const
+{
+ return
+ BindState == other.BindState &&
+ UntypedInvoke == other.UntypedInvoke;
+}
+
+inline bool TCallbackBase::operator != (const TCallbackBase& other) const
+{
+ return !(*this == other);
+}
+#endif
+
+inline TCallbackBase::TCallbackBase(TIntrusivePtr<TBindStateBase>&& bindState)
+ : BindState(std::move(bindState))
+ , UntypedInvoke(nullptr)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/yt/core/actions/callback_internal.h b/yt/yt/core/actions/callback_internal.h
new file mode 100644
index 0000000000..ee9583450f
--- /dev/null
+++ b/yt/yt/core/actions/callback_internal.h
@@ -0,0 +1,116 @@
+#pragma once
+/*
+$$==============================================================================
+$$ The following code is merely an adaptation of Chromium's Binds and Callbacks.
+$$ Kudos to Chromium authors.
+$$
+$$ Original Chromium revision:
+$$ - git-treeish: 206a2ae8a1ebd2b040753fff7da61bbca117757f
+$$ - git-svn-id: svn://svn.chromium.org/chrome/trunk/src@115607
+$$
+$$ See bind.h for an extended commentary.
+$$==============================================================================
+*/
+
+#include <yt/yt/core/misc/common.h>
+
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+#include <yt/yt/core/misc/source_location.h>
+#endif
+
+namespace NYT::NDetail {
+/*! \internal */
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque handle representing bound arguments.
+/*!
+ * #TBindStateBase is used to provide an opaque handle that the #TCallback<> class
+ * can use to represent a function object with bound arguments. It behaves as
+ * an existential type that is used by a corresponding invoke function
+ * to perform the function execution. This allows us to shield the #TCallback<>
+ * class from the types of the bound argument via "type erasure."
+ */
+struct TBindStateBase
+ : public TRefCounted
+{
+ explicit TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ const TSourceLocation& location
+#endif
+ );
+
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ TSourceLocation Location;
+#endif
+};
+
+//! Holds the TCallback methods that don't require specialization to reduce
+//! template bloat.
+class TCallbackBase
+{
+public:
+ //! Returns true iff #TCallback<> is not null (does not refer to anything).
+ explicit operator bool() const;
+
+ //! Returns the #TCallback<> into an uninitialized state.
+ void Reset();
+
+ //! Returns a magical handle.
+ void* GetHandle() const;
+
+#ifndef __cpp_impl_three_way_comparison
+ //! Returns |true| iff this callback is equal to the other (which may be null).
+ bool operator == (const TCallbackBase& other) const;
+
+ //! Returns |true| iff this callback is not equal to the other (which may be null).
+ bool operator != (const TCallbackBase& other) const;
+#else
+ bool operator== (const TCallbackBase&) const = default;
+#endif
+
+protected:
+ //! Swaps the state and the invoke function with other callback (without typechecking!).
+ void Swap(TCallbackBase& other);
+
+ /*!
+ * Yup, out-of-line copy constructor. Yup, explicit.
+ */
+ explicit TCallbackBase(const TCallbackBase& other) = default;
+
+ /*!
+ * We can efficiently move-construct callbacks avoiding extra interlocks
+ * while moving reference counted #TBindStateBase.
+ */
+ explicit TCallbackBase(TCallbackBase&& other) noexcept = default;
+
+ /*!
+ * We can construct #TCallback<> from a rvalue reference to the #TBindStateBase
+ * since the #TBindStateBase is created at the #Bind() site.
+ */
+ explicit TCallbackBase(TIntrusivePtr<TBindStateBase>&& bindState);
+
+protected:
+ /*!
+ * In C++, it is safe to cast function pointers to function pointers of
+ * another type. It is not okay to use void*.
+ * We create a TUntypedInvokeFunction type that can store our
+ * function pointer, and then cast it back to the original type on usage.
+ */
+ typedef void(*TUntypedInvokeFunction)();
+
+ TIntrusivePtr<TBindStateBase> BindState;
+ TUntypedInvokeFunction UntypedInvoke;
+
+private:
+ TCallbackBase& operator=(const TCallbackBase&) = delete;
+ TCallbackBase& operator=(TCallbackBase&&) noexcept = delete;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+/*! \endinternal */
+} // namespace NYT::NDetail
+
+#define CALLBACK_INTERNAL_INL_H_
+#include "callback_internal-inl.h"
+#undef CALLBACK_INTERNAL_INL_H_
+
diff --git a/yt/yt/core/actions/cancelable_context-inl.h b/yt/yt/core/actions/cancelable_context-inl.h
new file mode 100644
index 0000000000..f4d64d7853
--- /dev/null
+++ b/yt/yt/core/actions/cancelable_context-inl.h
@@ -0,0 +1,20 @@
+#ifndef CANCELABLE_CONTEXT_INL_H_
+#error "Direct inclusion of this file is not allowed, include cancelable_context.h"
+// For the sake of sane code completion.
+#include "cancelable_context.h"
+#endif
+#undef CANCELABLE_CONTEXT_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TCancelableContext::PropagateTo(const TFuture<T>& future)
+{
+ PropagateTo(future.AsVoid());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/cancelable_context.cpp b/yt/yt/core/actions/cancelable_context.cpp
new file mode 100644
index 0000000000..c04b6c27b0
--- /dev/null
+++ b/yt/yt/core/actions/cancelable_context.cpp
@@ -0,0 +1,150 @@
+#include "cancelable_context.h"
+#include "callback.h"
+#include "invoker_detail.h"
+#include "current_invoker.h"
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCancelableContext::TCancelableInvoker
+ : public TInvokerWrapper
+{
+public:
+ TCancelableInvoker(
+ TCancelableContextPtr context,
+ IInvokerPtr underlyingInvoker)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ , Context_(std::move(context))
+ {
+ YT_VERIFY(Context_);
+ }
+
+ void Invoke(TClosure callback) override
+ {
+ YT_ASSERT(callback);
+
+ if (Context_->Canceled_) {
+ return;
+ }
+
+ return UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE([this, this_ = MakeStrong(this), callback = std::move(callback)] {
+ if (Context_->Canceled_) {
+ return;
+ }
+
+ TCurrentInvokerGuard guard(this);
+ callback();
+ }));
+ }
+
+private:
+ const TCancelableContextPtr Context_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TCancelableContext::IsCanceled() const
+{
+ return Canceled_;
+}
+
+void TCancelableContext::Cancel(const TError& error)
+{
+ THashSet<TWeakPtr<TCancelableContext>> propagateToContexts;
+ THashSet<TFuture<void>> propagateToFutures;
+ {
+ auto guard = Guard(SpinLock_);
+ if (Canceled_) {
+ return;
+ }
+ CancelationError_ = error;
+ Canceled_ = true;
+ PropagateToContexts_.swap(propagateToContexts);
+ PropagateToFutures_.swap(propagateToFutures);
+ }
+
+ Handlers_.FireAndClear(error);
+
+ for (const auto& weakContext : propagateToContexts) {
+ auto context = weakContext.Lock();
+ if (context) {
+ context->Cancel(error);
+ }
+ }
+
+ for (const auto& future : propagateToFutures) {
+ future.Cancel(error);
+ }
+}
+
+IInvokerPtr TCancelableContext::CreateInvoker(IInvokerPtr underlyingInvoker)
+{
+ return New<TCancelableInvoker>(this, std::move(underlyingInvoker));
+}
+
+void TCancelableContext::SubscribeCanceled(const TCallback<void(const TError&)>& callback)
+{
+ auto guard = Guard(SpinLock_);
+ if (Canceled_) {
+ guard.Release();
+ callback(CancelationError_);
+ return;
+ }
+ Handlers_.Subscribe(callback);
+}
+
+void TCancelableContext::UnsubscribeCanceled(const TCallback<void(const TError&)>& /*callback*/)
+{
+ YT_ABORT();
+}
+
+void TCancelableContext::PropagateTo(const TCancelableContextPtr& context)
+{
+ auto weakContext = MakeWeak(context);
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (Canceled_) {
+ guard.Release();
+ context->Cancel(CancelationError_);
+ return;
+ }
+ PropagateToContexts_.insert(context);
+ }
+
+ context->SubscribeCanceled(BIND_NO_PROPAGATE([=, this, weakThis = MakeWeak(this)] (const TError& /*error*/) {
+ if (auto this_ = weakThis.Lock()) {
+ auto guard = Guard(SpinLock_);
+ PropagateToContexts_.erase(context);
+ }
+ }));
+}
+
+void TCancelableContext::PropagateTo(const TFuture<void>& future)
+{
+ {
+ auto guard = Guard(SpinLock_);
+ if (Canceled_) {
+ guard.Release();
+ future.Cancel(CancelationError_);
+ return;
+ }
+
+ PropagateToFutures_.insert(future);
+ }
+
+ future.Subscribe(BIND_NO_PROPAGATE([=, this, weakThis = MakeWeak(this)] (const TError&) {
+ if (auto this_ = weakThis.Lock()) {
+ auto guard = Guard(SpinLock_);
+ PropagateToFutures_.erase(future);
+ }
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/cancelable_context.h b/yt/yt/core/actions/cancelable_context.h
new file mode 100644
index 0000000000..709c3d0fab
--- /dev/null
+++ b/yt/yt/core/actions/cancelable_context.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "public.h"
+#include "future.h"
+#include "signal.h"
+
+#include <library/cpp/yt/memory/weak_ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Maintains a flag indicating if the context is canceled.
+//! Propagates cancelation to other contexts and futures.
+/*!
+ * \note
+ * Thread-affinity: any
+ */
+class TCancelableContext
+ : public TRefCounted
+{
+public:
+ //! Returns |true| iff the context is canceled.
+ bool IsCanceled() const;
+
+ //! Marks the context as canceled raising the handlers
+ //! and propagates cancelation.
+ void Cancel(const TError& error);
+
+ //! Raised when the context is canceled.
+ DECLARE_SIGNAL(void(const TError&), Canceled);
+
+ //! Registers another context for propagating cancelation.
+ void PropagateTo(const TCancelableContextPtr& context);
+
+ //! Registers a future for propagating cancelation.
+ template <class T>
+ void PropagateTo(const TFuture<T>& future);
+ void PropagateTo(const TFuture<void>& future);
+
+ //! Creates a new invoker wrapping the existing one.
+ /*!
+ * Callbacks are executed by the underlying invoker as long as the context
+ * is not canceled. Double check is employed: the first one happens
+ * at the instant the callback is enqueued and the second one -- when
+ * the callback starts executing.
+ */
+ IInvokerPtr CreateInvoker(IInvokerPtr underlyingInvoker);
+
+private:
+ class TCancelableInvoker;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::atomic<bool> Canceled_ = {false};
+ TError CancelationError_;
+ TCallbackList<void(const TError&)> Handlers_;
+ THashSet<TWeakPtr<TCancelableContext>> PropagateToContexts_;
+ THashSet<TFuture<void>> PropagateToFutures_;
+
+};
+
+DEFINE_REFCOUNTED_TYPE(TCancelableContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define CANCELABLE_CONTEXT_INL_H_
+#include "cancelable_context-inl.h"
+#undef CANCELABLE_CONTEXT_INL_H_
diff --git a/yt/yt/core/actions/current_invoker.cpp b/yt/yt/core/actions/current_invoker.cpp
new file mode 100644
index 0000000000..3ed572a257
--- /dev/null
+++ b/yt/yt/core/actions/current_invoker.cpp
@@ -0,0 +1,52 @@
+#include "current_invoker.h"
+
+#include "invoker_util.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+thread_local IInvoker* CurrentInvoker;
+
+IInvoker* GetCurrentInvoker()
+{
+ if (CurrentInvoker) {
+ return CurrentInvoker;
+ }
+ return GetSyncInvoker().Get();
+}
+
+void SetCurrentInvoker(IInvoker* invoker)
+{
+ CurrentInvoker = invoker;
+}
+
+TCurrentInvokerGuard::TCurrentInvokerGuard(IInvoker* invoker)
+ : NConcurrency::TContextSwitchGuard(
+ [this] () noexcept {
+ Restore();
+ },
+ nullptr)
+ , Active_(true)
+ , SavedInvoker_(std::move(invoker))
+{
+ std::swap(CurrentInvoker, SavedInvoker_);
+}
+
+void TCurrentInvokerGuard::Restore()
+{
+ if (!Active_) {
+ return;
+ }
+ Active_ = false;
+ CurrentInvoker = std::move(SavedInvoker_);
+}
+
+TCurrentInvokerGuard::~TCurrentInvokerGuard()
+{
+ Restore();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/current_invoker.h b/yt/yt/core/actions/current_invoker.h
new file mode 100644
index 0000000000..38f3207662
--- /dev/null
+++ b/yt/yt/core/actions/current_invoker.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+// Current invoker API
+//
+// Note that this invoker is passed by raw pointer to avoid contention at its
+// ref counter. The caller of #SetCurrentInvoker must ensure that the invoker
+// remains alive.
+
+IInvoker* GetCurrentInvoker();
+void SetCurrentInvoker(IInvoker* invoker);
+
+//! Swaps the current active invoker with a provided one.
+class TCurrentInvokerGuard
+ : public NConcurrency::TContextSwitchGuard
+{
+public:
+ explicit TCurrentInvokerGuard(IInvoker* invoker);
+ ~TCurrentInvokerGuard();
+
+private:
+ void Restore();
+
+ bool Active_;
+ IInvoker* SavedInvoker_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/future-inl.h b/yt/yt/core/actions/future-inl.h
new file mode 100644
index 0000000000..28fa8d8323
--- /dev/null
+++ b/yt/yt/core/actions/future-inl.h
@@ -0,0 +1,2519 @@
+#ifndef FUTURE_INL_H_
+#error "Direct inclusion of this file is not allowed, include future.h"
+// For the sake of sane code completion.
+#include "future.h"
+#endif
+#undef FUTURE_INL_H_
+
+#include "bind.h"
+#include "invoker_util.h"
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <atomic>
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+// Forward declarations
+
+namespace NConcurrency {
+
+// scheduler.h
+TCallback<void(const NYT::TError&)> GetCurrentFiberCanceler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Thrown when a fiber is being terminated by an external event.
+class TFiberCanceledException
+{ };
+
+} // namespace NConcurrency
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline NYT::TError MakeAbandonedError()
+{
+ return NYT::TError(NYT::EErrorCode::Canceled, "Promise abandoned");
+}
+
+inline NYT::TError MakeCanceledError(const NYT::TError& error)
+{
+ return NYT::TError(NYT::EErrorCode::Canceled, "Operation canceled")
+ << error;
+}
+
+template <class T>
+TFuture<T> MakeWellKnownFuture(NYT::TErrorOr<T> value)
+{
+ return TFuture<T>(New<NYT::NDetail::TPromiseState<T>>(true, -1, -1, -1, std::move(value)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, TFutureCallbackCookie MinCookie, TFutureCallbackCookie MaxCookie>
+class TFutureCallbackList
+{
+public:
+ static bool IsValidCookie(TFutureCallbackCookie cookie)
+ {
+ return cookie >= MinCookie && cookie <= MaxCookie;
+ }
+
+ TFutureCallbackCookie Add(T callback)
+ {
+ YT_ASSERT(callback);
+ TFutureCallbackCookie cookie;
+ if (SpareCookies_.empty()) {
+ cookie = static_cast<TFutureCallbackCookie>(Callbacks_.size());
+ Callbacks_.push_back(std::move(callback));
+ } else {
+ cookie = SpareCookies_.back();
+ SpareCookies_.pop_back();
+ YT_ASSERT(!Callbacks_[cookie]);
+ Callbacks_[cookie] = std::move(callback);
+ }
+ cookie += MinCookie;
+ YT_ASSERT(cookie <= MaxCookie);
+ return cookie;
+ }
+
+ bool TryRemove(TFutureCallbackCookie cookie, TGuard<NThreading::TSpinLock>* guard)
+ {
+ if (!IsValidCookie(cookie)) {
+ return false;
+ }
+ cookie -= MinCookie;
+ YT_ASSERT(cookie >= 0 && cookie < static_cast<int>(Callbacks_.size()));
+ YT_ASSERT(Callbacks_[cookie]);
+ SpareCookies_.push_back(cookie);
+ auto callback = std::move(Callbacks_[cookie]);
+ // Make sure callback is not being destroyed under spinlock.
+ guard->Release();
+ return true;
+ }
+
+ template <class... As>
+ void RunAndClear(As&&... args)
+ {
+ for (const auto& callback : Callbacks_) {
+ if (callback) {
+ RunNoExcept(callback, std::forward<As>(args)...);
+ }
+ }
+ Callbacks_.clear();
+ SpareCookies_.clear();
+ }
+
+ bool IsEmpty() const
+ {
+ return Callbacks_.size() == SpareCookies_.size();
+ }
+
+private:
+ static constexpr int TypicalCount = 8;
+ TCompactVector<T, TypicalCount> Callbacks_;
+ TCompactVector<TFutureCallbackCookie, TypicalCount> SpareCookies_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCancelableStateBase
+ : public TRefCountedBase
+{
+public:
+ TCancelableStateBase(bool wellKnown, int cancelableRefCount)
+ : WellKnown_(wellKnown)
+ , CancelableRefCount_(cancelableRefCount)
+ { }
+
+ virtual ~TCancelableStateBase() noexcept = default;
+
+ virtual bool Cancel(const NYT::TError& error) noexcept = 0;
+
+ void RefCancelable()
+ {
+ if (WellKnown_) {
+ return;
+ }
+ auto oldCount = CancelableRefCount_++;
+ YT_ASSERT(oldCount > 0);
+ }
+
+ void UnrefCancelable()
+ {
+ if (WellKnown_) {
+ return;
+ }
+ auto oldCount = CancelableRefCount_--;
+ YT_ASSERT(oldCount > 0);
+ if (oldCount == 1) {
+ DestroyRefCounted();
+ }
+ }
+
+protected:
+ const bool WellKnown_;
+
+ //! Number of cancelables plus one if FutureRefCount_ > 0.
+ std::atomic<int> CancelableRefCount_;
+
+ template <class T>
+ static void DestroyRefCountedImpl(T* obj)
+ {
+ // No virtual call when T is final.
+ obj->~T();
+#ifdef _win_
+ ::_aligned_free(obj);
+#else
+ ::free(obj);
+#endif
+ }
+};
+
+Y_FORCE_INLINE void Ref(TCancelableStateBase* state)
+{
+ state->RefCancelable();
+}
+
+Y_FORCE_INLINE void Unref(TCancelableStateBase* state)
+{
+ state->UnrefCancelable();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TFutureState<void>
+ : public TCancelableStateBase
+{
+public:
+ using TVoidResultHandler = TCallback<void(const NYT::TError&)>;
+ using TVoidResultHandlers = TFutureCallbackList<TVoidResultHandler, 0, (1ULL << 30) - 1>;
+
+ using TCancelHandler = TCallback<void(const NYT::TError&)>;
+ using TCancelHandlers = TCompactVector<TCancelHandler, 8>;
+
+ void RefFuture()
+ {
+ if (WellKnown_) {
+ return;
+ }
+ auto oldCount = FutureRefCount_++;
+ YT_ASSERT(oldCount > 0);
+ }
+
+ bool TryRefFuture()
+ {
+ if (WellKnown_) {
+ return true;
+ }
+ auto oldCount = FutureRefCount_.load();
+ while (true) {
+ if (oldCount == 0) {
+ return false;
+ }
+ auto newCount = oldCount + 1;
+ if (FutureRefCount_.compare_exchange_weak(oldCount, newCount)) {
+ return true;
+ }
+ }
+ }
+
+ void UnrefFuture()
+ {
+ if (WellKnown_) {
+ return;
+ }
+ auto oldCount = FutureRefCount_--;
+ YT_ASSERT(oldCount > 0);
+ if (oldCount == 1) {
+ OnLastFutureRefLost();
+ }
+ }
+
+ void RefPromise()
+ {
+ YT_ASSERT(!WellKnown_);
+ auto oldCount = PromiseRefCount_++;
+ YT_ASSERT(oldCount > 0 && FutureRefCount_ > 0);
+ }
+
+ void UnrefPromise()
+ {
+ YT_ASSERT(!WellKnown_);
+ auto oldCount = PromiseRefCount_--;
+ YT_ASSERT(oldCount > 0);
+ if (oldCount == 1) {
+ OnLastPromiseRefLost();
+ }
+ }
+
+ const NYT::TError& Get() const
+ {
+ WaitUntilSet();
+ return ResultError_;
+ }
+
+ NYT::TError GetUnique()
+ {
+ return Get();
+ }
+
+ std::optional<TError> TryGet() const
+ {
+ if (!CheckIfSet()) {
+ return std::nullopt;
+ }
+ return ResultError_;
+ }
+
+ std::optional<TError> TryGetUnique()
+ {
+ return TryGet();
+ }
+
+ void Set(const NYT::TError& error)
+ {
+ DoTrySet<true>(error);
+ }
+
+ bool TrySet(const NYT::TError& error)
+ {
+ // Fast path.
+ if (Set_) {
+ return false;
+ }
+
+ // Slow path.
+ return DoTrySet<false>(error);
+ }
+
+ TFutureCallbackCookie Subscribe(TVoidResultHandler handler);
+ void Unsubscribe(TFutureCallbackCookie cookie);
+
+ bool Cancel(const NYT::TError& error) noexcept override;
+
+ bool OnCanceled(TCancelHandler handler);
+
+ bool IsSet() const
+ {
+ return Set_ || AbandonedUnset_;
+ }
+
+ bool IsCanceled() const
+ {
+ return Canceled_;
+ }
+
+ bool Wait(TDuration timeout) const;
+ bool Wait(TInstant deadline) const;
+
+protected:
+ //! Number of promises.
+ std::atomic<int> PromiseRefCount_;
+ //! Number of futures plus one if PromiseRefCount_ > 0.
+ std::atomic<int> FutureRefCount_;
+
+ //! Protects the following section of members.
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::atomic<bool> Canceled_ = false;
+ NYT::TError CancelationError_;
+ std::atomic<bool> Set_;
+ std::atomic<bool> AbandonedUnset_ = false;
+ NYT::TError ResultError_;
+ bool HasHandlers_ = false;
+ TVoidResultHandlers VoidResultHandlers_;
+ TCancelHandlers CancelHandlers_;
+ mutable std::unique_ptr<NThreading::TEvent> ReadyEvent_;
+
+ TFutureState(int promiseRefCount, int futureRefCount, int cancelableRefCount)
+ : TCancelableStateBase(false, cancelableRefCount)
+ , PromiseRefCount_(promiseRefCount)
+ , FutureRefCount_(futureRefCount)
+ , Set_(false)
+ { }
+
+ TFutureState(bool wellKnown, int promiseRefCount, int futureRefCount, int cancelableRefCount, NYT::TError&& error)
+ : TCancelableStateBase(wellKnown, cancelableRefCount)
+ , PromiseRefCount_(promiseRefCount)
+ , FutureRefCount_(futureRefCount)
+ , Set_(true)
+ , ResultError_(std::move(error))
+ { }
+
+ void InstallAbandonedError();
+ void InstallAbandonedError() const;
+
+ virtual void ResetResult();
+ virtual void SetResultError(const NYT::TError& error);
+ virtual bool TrySetError(const NYT::TError& error);
+
+ template <bool MustSet, class F>
+ bool DoRunSetter(F setter)
+ {
+ NThreading::TEvent* readyEvent = nullptr;
+ bool canceled;
+ {
+ auto guard = Guard(SpinLock_);
+ YT_ASSERT(!AbandonedUnset_);
+ if (MustSet && !Canceled_) {
+ YT_VERIFY(!Set_);
+ } else if (Set_) {
+ return false;
+ }
+ RunNoExcept(setter);
+ Set_ = true;
+ canceled = Canceled_;
+ readyEvent = ReadyEvent_.get();
+ }
+
+ if (readyEvent) {
+ readyEvent->NotifyAll();
+ }
+
+ if (!canceled) {
+ CancelHandlers_.clear();
+ }
+
+ VoidResultHandlers_.RunAndClear(ResultError_);
+
+ return true;
+ }
+
+ template <bool MustSet>
+ bool DoTrySet(const NYT::TError& error)
+ {
+ // Calling subscribers may release the last reference to this.
+ TIntrusivePtr<TFutureState<void>> this_(this);
+
+ return DoRunSetter<MustSet>([&] {
+ SetResultError(error);
+ });
+ }
+
+ virtual bool DoUnsubscribe(TFutureCallbackCookie cookie, TGuard<NThreading::TSpinLock>* guard);
+
+ void WaitUntilSet() const;
+ bool CheckIfSet() const;
+
+private:
+ void OnLastFutureRefLost();
+ void OnLastPromiseRefLost();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TFutureState
+ : public TFutureState<void>
+{
+public:
+ using TResultHandler = TCallback<void(const NYT::TErrorOr<T>&)>;
+ using TResultHandlers = TFutureCallbackList<TResultHandler, (1ULL << 30), (1ULL << 31) - 1>;
+
+ using TUniqueResultHandler = TCallback<void(NYT::TErrorOr<T>&&)>;
+
+private:
+ std::optional<TErrorOr<T>> Result_;
+#ifndef NDEBUG
+ mutable std::atomic<bool> ResultMovedOut_ = false;
+#endif
+
+ TResultHandlers ResultHandlers_;
+ TUniqueResultHandler UniqueResultHandler_;
+
+
+ template <bool MustSet, class U>
+ bool DoTrySet(U&& value) noexcept
+ {
+ // Calling subscribers may release the last reference to this.
+ TIntrusivePtr<TFutureState<void>> this_(this);
+
+ if (!DoRunSetter<MustSet>([&] {
+ Result_.emplace(std::forward<U>(value));
+ if (!Result_->IsOK()) {
+ ResultError_ = *Result_;
+ }
+ }))
+ {
+ return false;
+ }
+
+ // It is possible that the result has already been moved out by, e.g., GetUnique.
+ // Hence GetResult must only be called when we actually have handlers to invoke.
+ if (!ResultHandlers_.IsEmpty()) {
+ ResultHandlers_.RunAndClear(GetResult());
+ }
+
+ if (UniqueResultHandler_) {
+ RunNoExcept(UniqueResultHandler_, GetUniqueResult());
+ UniqueResultHandler_ = {};
+ }
+
+ return true;
+ }
+
+
+ const NYT::TErrorOr<T>& GetResult() const
+ {
+#ifndef NDEBUG
+ YT_ASSERT(!ResultMovedOut_);
+#endif
+ YT_ASSERT(Result_);
+ return *Result_;
+ }
+
+ const std::optional<TErrorOr<T>>& GetOptionalResult() const
+ {
+#ifndef NDEBUG
+ YT_ASSERT(!ResultMovedOut_);
+#endif
+ return Result_;
+ }
+
+ NYT::TErrorOr<T> GetUniqueResult()
+ {
+#ifndef NDEBUG
+ YT_ASSERT(!ResultMovedOut_.exchange(true));
+#endif
+ auto result = std::move(*Result_);
+ Result_.reset();
+ return result;
+ }
+
+
+ bool TrySetError(const NYT::TError& error) override
+ {
+ return TrySet(error);
+ }
+
+ void ResetResult() override
+ {
+ Result_.reset();
+ }
+
+ void SetResultError(const NYT::TError& error) override
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ TFutureState<void>::SetResultError(error);
+ Result_ = error;
+ }
+
+ bool DoUnsubscribe(TFutureCallbackCookie cookie, TGuard<NThreading::TSpinLock>* guard) override
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ return
+ ResultHandlers_.TryRemove(cookie, guard) ||
+ TFutureState<void>::DoUnsubscribe(cookie, guard);
+ }
+
+protected:
+ TFutureState(int promiseRefCount, int futureRefCount, int cancelableRefCount)
+ : TFutureState<void>(promiseRefCount, futureRefCount, cancelableRefCount)
+ { }
+
+ TFutureState(bool wellKnown, int promiseRefCount, int futureRefCount, int cancelableRefCount, NYT::TErrorOr<T>&& value)
+ : TFutureState<void>(wellKnown, promiseRefCount, futureRefCount, cancelableRefCount, NYT::TError(static_cast<const NYT::TError&>(value)))
+ , Result_(std::move(value))
+ { }
+
+ TFutureState(bool wellKnown, int promiseRefCount, int futureRefCount, int cancelableRefCount, T&& value)
+ : TFutureState<void>(wellKnown, promiseRefCount, futureRefCount, cancelableRefCount, NYT::TError())
+ , Result_(std::move(value))
+ { }
+
+public:
+ const NYT::TErrorOr<T>& Get() const
+ {
+ WaitUntilSet();
+ return GetResult();
+ }
+
+ NYT::TErrorOr<T> GetUnique()
+ {
+ // Fast path.
+ if (Set_) {
+ return GetUniqueResult();
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ return GetUniqueResult();
+ }
+ if (!ReadyEvent_) {
+ ReadyEvent_.reset(new NThreading::TEvent());
+ }
+ }
+
+ ReadyEvent_->Wait();
+
+ return GetUniqueResult();
+ }
+
+ std::optional<TErrorOr<T>> TryGet() const
+ {
+ if (!CheckIfSet()) {
+ return std::nullopt;
+ }
+ return GetOptionalResult();
+ }
+
+ std::optional<TErrorOr<T>> TryGetUnique()
+ {
+ if (!CheckIfSet()) {
+ return std::nullopt;
+ }
+ return GetUniqueResult();
+ }
+
+ template <class U>
+ void Set(U&& value)
+ {
+ DoTrySet<true>(std::forward<U>(value));
+ }
+
+ template <class U>
+ bool TrySet(U&& value)
+ {
+ // Fast path.
+ if (Set_) {
+ return false;
+ }
+
+ // Slow path.
+ return DoTrySet<false>(std::forward<U>(value));
+ }
+
+ TFutureCallbackCookie Subscribe(TResultHandler handler)
+ {
+ // Fast path.
+ if (Set_) {
+ RunNoExcept(handler, GetResult());
+ return NullFutureCallbackCookie;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ guard.Release();
+ RunNoExcept(handler, GetResult());
+ return NullFutureCallbackCookie;
+ } else {
+ HasHandlers_ = true;
+ return ResultHandlers_.Add(std::move(handler));
+ }
+ }
+ }
+
+ void SubscribeUnique(TUniqueResultHandler handler)
+ {
+ // Fast path.
+ if (Set_) {
+ RunNoExcept(handler, GetUniqueResult());
+ return;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ guard.Release();
+ RunNoExcept(handler, GetUniqueResult());
+ } else {
+ YT_ASSERT(!UniqueResultHandler_);
+ YT_ASSERT(ResultHandlers_.IsEmpty());
+ UniqueResultHandler_ = std::move(handler);
+ HasHandlers_ = true;
+ }
+ }
+ }
+};
+
+template <class T>
+Y_FORCE_INLINE void Ref(TFutureState<T>* state)
+{
+ state->RefFuture();
+}
+
+template <class T>
+Y_FORCE_INLINE void Unref(TFutureState<T>* state)
+{
+ state->UnrefFuture();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TPromiseState
+ : public TFutureState<T>
+{
+public:
+ TPromiseState(int promiseRefCount, int futureRefCount, int cancelableRefCount)
+ : TFutureState<T>(promiseRefCount, futureRefCount, cancelableRefCount)
+ { }
+
+ template <class U>
+ TPromiseState(bool wellKnown, int promiseRefCount, int futureRefCount, int cancelableRefCount, U&& value)
+ : TFutureState<T>(wellKnown, promiseRefCount, futureRefCount, cancelableRefCount, std::forward<U>(value))
+ { }
+};
+
+template <class T>
+Y_FORCE_INLINE void Ref(TPromiseState<T>* state)
+{
+ state->RefPromise();
+}
+
+template <class T>
+Y_FORCE_INLINE void Unref(TPromiseState<T>* state)
+{
+ state->UnrefPromise();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class S>
+struct TPromiseSetter;
+
+template <class T, class F>
+void InterceptExceptions(const TPromise<T>& promise, const F& func)
+{
+ try {
+ func();
+ } catch (const NYT::TErrorException& ex) {
+ promise.Set(ex.Error());
+ } catch (const std::exception& ex) {
+ promise.Set(NYT::TError(ex));
+ } catch (NConcurrency::TFiberCanceledException& ) {
+ promise.Set(MakeAbandonedError());
+ }
+}
+
+template <class R, class T, class... TArgs>
+struct TPromiseSetter<T, R(TArgs...)>
+{
+ template <class... TCallArgs>
+ static void Do(const TPromise<T>& promise, const TCallback<T(TArgs...)>& callback, TCallArgs&&... args)
+ {
+ InterceptExceptions(
+ promise,
+ [&] {
+ promise.Set(callback(std::forward<TCallArgs>(args)...));
+ });
+ }
+};
+
+template <class R, class T, class... TArgs>
+struct TPromiseSetter<T, NYT::TErrorOr<R>(TArgs...)>
+{
+ template <class... TCallArgs>
+ static void Do(const TPromise<T>& promise, const TCallback<TErrorOr<T>(TArgs...)>& callback, TCallArgs&&... args)
+ {
+ InterceptExceptions(
+ promise,
+ [&] {
+ promise.Set(callback(std::forward<TCallArgs>(args)...));
+ });
+ }
+};
+
+template <class... TArgs>
+struct TPromiseSetter<void, void(TArgs...)>
+{
+ template <class... TCallArgs>
+ static void Do(const TPromise<void>& promise, const TCallback<void(TArgs...)>& callback, TCallArgs&&... args)
+ {
+ InterceptExceptions(
+ promise,
+ [&] {
+ callback(std::forward<TCallArgs>(args)...);
+ promise.Set();
+ });
+ }
+};
+
+template <class T, class... TArgs>
+struct TPromiseSetter<T, TFuture<T>(TArgs...)>
+{
+ template <class... TCallArgs>
+ static void Do(const TPromise<T>& promise, const TCallback<TFuture<T>(TArgs...)>& callback, TCallArgs&&... args)
+ {
+ InterceptExceptions(
+ promise,
+ [&] {
+ promise.SetFrom(callback(std::forward<TCallArgs>(args)...));
+ });
+ }
+};
+
+template <class T, class... TArgs>
+struct TPromiseSetter<T, NYT::TErrorOr<TFuture<T>>(TArgs...)>
+{
+ template <class... TCallArgs>
+ static void Do(const TPromise<T>& promise, const TCallback<TFuture<T>(TArgs...)>& callback, TCallArgs&&... args)
+ {
+ InterceptExceptions(
+ promise,
+ [&] {
+ auto result = callback(std::forward<TCallArgs>(args)...);
+ if (result.IsOK()) {
+ promise.SetFrom(std::move(result));
+ } else {
+ promise.Set(NYT::TError(result));
+ }
+ });
+ }
+};
+
+template <class R, class T>
+void ApplyHelperHandler(const TPromise<T>& promise, const TCallback<R()>& callback, const NYT::TError& value)
+{
+ if (value.IsOK()) {
+ TPromiseSetter<T, R()>::Do(promise, callback);
+ } else {
+ promise.Set(NYT::TError(value));
+ }
+}
+
+template <class R, class T, class U>
+void ApplyHelperHandler(const TPromise<T>& promise, const TCallback<R(const U&)>& callback, const NYT::TErrorOr<U>& value)
+{
+ if (value.IsOK()) {
+ TPromiseSetter<T, R(const U&)>::Do(promise, callback, value.Value());
+ } else {
+ promise.Set(NYT::TError(value));
+ }
+}
+
+template <class R, class T, class U>
+void ApplyHelperHandler(const TPromise<T>& promise, const TCallback<R(const NYT::TErrorOr<U>&)>& callback, const NYT::TErrorOr<U>& value)
+{
+ TPromiseSetter<T, R(const NYT::TErrorOr<U>&)>::Do(promise, callback, value);
+}
+
+template <class R, class T, class S>
+TFuture<R> ApplyHelper(TFutureBase<T> this_, TCallback<S> callback)
+{
+ YT_ASSERT(this_);
+
+ auto promise = NewPromise<R>();
+
+ this_.Subscribe(BIND_NO_PROPAGATE([=, callback = std::move(callback)] (const NYT::TErrorOr<T>& value) {
+ ApplyHelperHandler(promise, callback, value);
+ }));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([cancelable = this_.AsCancelable()] (const NYT::TError& error) {
+ cancelable.Cancel(error);
+ }));
+
+ return promise;
+}
+
+template <class R, class T, class U>
+void ApplyHelperHandler(const TPromise<T>& promise, const TCallback<R(U&&)>& callback, NYT::TErrorOr<U>&& value)
+{
+ if (value.IsOK()) {
+ TPromiseSetter<T, R(U&&)>::Do(promise, callback, std::move(value.Value()));
+ } else {
+ promise.Set(NYT::TError(value));
+ }
+}
+
+template <class R, class T, class U>
+void ApplyHelperHandler(const TPromise<T>& promise, const TCallback<R(NYT::TErrorOr<U>&&)>& callback, NYT::TErrorOr<U>&& value)
+{
+ TPromiseSetter<T, R(NYT::TErrorOr<U>&&)>::Do(promise, callback, std::move(value));
+}
+
+template <class R, class T, class S>
+TFuture<R> ApplyUniqueHelper(TFutureBase<T> this_, TCallback<S> callback)
+{
+ YT_ASSERT(this_);
+
+ auto promise = NewPromise<R>();
+
+ this_.SubscribeUnique(BIND_NO_PROPAGATE([=, callback = std::move(callback)] (NYT::TErrorOr<T>&& value) {
+ ApplyHelperHandler(promise, callback, std::move(value));
+ }));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([cancelable = this_.AsCancelable()] (const NYT::TError& error) {
+ cancelable.Cancel(error);
+ }));
+
+ return promise;
+}
+
+template <class T, class D>
+TFuture<T> ApplyTimeoutHelper(TFutureBase<T> this_, D timeoutOrDeadline, IInvokerPtr invoker)
+{
+ auto promise = NewPromise<T>();
+
+ auto cookie = NConcurrency::TDelayedExecutor::Submit(
+ BIND([=, cancelable = this_.AsCancelable()] (bool aborted) {
+ NYT::TError error;
+ if (aborted) {
+ error = NYT::TError(NYT::EErrorCode::Canceled, "Operation aborted");
+ } else {
+ error = NYT::TError(NYT::EErrorCode::Timeout, "Operation timed out");
+ if constexpr (std::is_same_v<D, TDuration>) {
+ error = error << NYT::TErrorAttribute("timeout", timeoutOrDeadline);
+ }
+ if constexpr (std::is_same_v<D, TInstant>) {
+ error = error << NYT::TErrorAttribute("deadline", timeoutOrDeadline);
+ }
+ }
+ promise.TrySet(error);
+ cancelable.Cancel(error);
+ }),
+ timeoutOrDeadline,
+ std::move(invoker));
+
+ this_.Subscribe(BIND_NO_PROPAGATE([=] (const NYT::TErrorOr<T>& value) {
+ NConcurrency::TDelayedExecutor::Cancel(cookie);
+ promise.TrySet(value);
+ }));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([=, cancelable = this_.AsCancelable()] (const NYT::TError& error) {
+ NConcurrency::TDelayedExecutor::Cancel(cookie);
+ cancelable.Cancel(error);
+ }));
+
+ return promise;
+}
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TPromise<T> NewPromise()
+{
+ return TPromise<T>(New<NYT::NDetail::TPromiseState<T>>(1, 1, 1));
+}
+
+template <class T>
+TPromise<T> MakePromise(NYT::TErrorOr<T> value)
+{
+ return TPromise<T>(New<NYT::NDetail::TPromiseState<T>>(false, 1, 1, 1, std::move(value)));
+}
+
+template <class T>
+TPromise<T> MakePromise(T value)
+{
+ return TPromise<T>(New<NYT::NDetail::TPromiseState<T>>(false, 1, 1, 1, std::move(value)));
+}
+
+template <class T>
+TFuture<T> MakeFuture(NYT::TErrorOr<T> value)
+{
+ return TFuture<T>(New<NYT::NDetail::TPromiseState<T>>(false, 0, 1, 1, std::move(value)));
+}
+
+template <class T>
+TFuture<T> MakeFuture(T value)
+{
+ return TFuture<T>(New<NYT::NDetail::TPromiseState<T>>(false, 0, 1, 1, std::move(value)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool operator==(const TCancelable& lhs, const TCancelable& rhs)
+{
+ return lhs.Impl_ == rhs.Impl_;
+}
+
+inline bool operator!=(const TCancelable& lhs, const TCancelable& rhs)
+{
+ return !(lhs == rhs);
+}
+
+inline void swap(TCancelable& lhs, TCancelable& rhs)
+{
+ using std::swap;
+ swap(lhs.Impl_, rhs.Impl_);
+}
+
+template <class T>
+bool operator==(const TFuture<T>& lhs, const TFuture<T>& rhs)
+{
+ return lhs.Impl_ == rhs.Impl_;
+}
+
+template <class T>
+bool operator!=(const TFuture<T>& lhs, const TFuture<T>& rhs)
+{
+ return !(lhs == rhs);
+}
+
+template <class T>
+void swap(TFuture<T>& lhs, TFuture<T>& rhs)
+{
+ using std::swap;
+ swap(lhs.Impl_, rhs.Impl_);
+}
+
+template <class T>
+bool operator==(const TPromise<T>& lhs, const TPromise<T>& rhs)
+{
+ return lhs.Impl_ == rhs.Impl_;
+}
+
+template <class T>
+bool operator!=(const TPromise<T>& lhs, const TPromise<T>& rhs)
+{
+ return *(lhs == rhs);
+}
+
+template <class T>
+void swap(TPromise<T>& lhs, TPromise<T>& rhs)
+{
+ using std::swap;
+ swap(lhs.Impl_, rhs.Impl_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TCancelable::operator bool() const
+{
+ return Impl_.operator bool();
+}
+
+inline void TCancelable::Reset()
+{
+ Impl_.Reset();
+}
+
+inline bool TCancelable::Cancel(const NYT::TError& error) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Cancel(error);
+}
+
+inline TCancelable::TCancelable(TIntrusivePtr<NYT::NDetail::TCancelableStateBase> impl)
+ : Impl_(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFutureBase<T>::operator bool() const
+{
+ return Impl_.operator bool();
+}
+
+template <class T>
+void TFutureBase<T>::Reset()
+{
+ Impl_.Reset();
+}
+
+template <class T>
+bool TFutureBase<T>::IsSet() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->IsSet();
+}
+
+template <class T>
+const NYT::TErrorOr<T>& TFutureBase<T>::Get() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Get();
+}
+
+template <class T>
+TErrorOr<T> TFutureBase<T>::GetUnique() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->GetUnique();
+}
+
+template <class T>
+bool TFutureBase<T>::Wait(TDuration timeout) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Wait(timeout);
+}
+
+template <class T>
+bool TFutureBase<T>::Wait(TInstant deadline) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Wait(deadline);
+}
+
+template <class T>
+std::optional<TErrorOr<T>> TFutureBase<T>::TryGet() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->TryGet();
+}
+
+template <class T>
+std::optional<TErrorOr<T>> TFutureBase<T>::TryGetUnique() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->TryGetUnique();
+}
+
+template <class T>
+TFutureCallbackCookie TFutureBase<T>::Subscribe(TCallback<void(const NYT::TErrorOr<T>&)> handler) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Subscribe(std::move(handler));
+}
+
+template <class T>
+void TFutureBase<T>::Unsubscribe(TFutureCallbackCookie cookie) const
+{
+ YT_ASSERT(Impl_);
+ Impl_->Unsubscribe(cookie);
+}
+
+template <class T>
+void TFutureBase<T>::SubscribeUnique(TCallback<void(NYT::TErrorOr<T>&&)> handler) const
+{
+ YT_ASSERT(Impl_);
+ Impl_->SubscribeUnique(std::move(handler));
+}
+
+template <class T>
+bool TFutureBase<T>::Cancel(const NYT::TError& error) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Cancel(error);
+}
+
+template <class T>
+TFuture<T> TFutureBase<T>::ToUncancelable() const
+{
+ if (!Impl_ || IsSet()) {
+ return TFuture<T>(Impl_);
+ }
+
+ auto promise = NewPromise<T>();
+
+ this->Subscribe(BIND_NO_PROPAGATE([=] (const NYT::TErrorOr<T>& value) {
+ promise.Set(value);
+ }));
+
+ static const auto NoopHandler = BIND_NO_PROPAGATE([] (const NYT::TError&) { });
+ promise.OnCanceled(NoopHandler);
+
+ return promise;
+}
+
+template <class T>
+TFuture<T> TFutureBase<T>::ToImmediatelyCancelable() const
+{
+ if (!Impl_) {
+ return TFuture<T>();
+ }
+
+ auto promise = NewPromise<T>();
+
+ this->Subscribe(BIND_NO_PROPAGATE([=] (const NYT::TErrorOr<T>& value) {
+ promise.TrySet(value);
+ }));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([=, cancelable = AsCancelable()] (const NYT::TError& error) {
+ cancelable.Cancel(error);
+ promise.TrySet(NYT::NDetail::MakeCanceledError(error));
+ }));
+
+ return promise;
+}
+
+template <class T>
+TFuture<T> TFutureBase<T>::WithDeadline(TInstant deadline, IInvokerPtr invoker) const
+{
+ YT_ASSERT(Impl_);
+
+ if (IsSet()) {
+ return TFuture<T>(Impl_);
+ }
+
+ return NYT::NDetail::ApplyTimeoutHelper(*this, deadline, std::move(invoker));
+}
+
+template <class T>
+TFuture<T> TFutureBase<T>::WithTimeout(TDuration timeout, IInvokerPtr invoker) const
+{
+ YT_ASSERT(Impl_);
+
+ if (IsSet()) {
+ return TFuture<T>(Impl_);
+ }
+
+ return NYT::NDetail::ApplyTimeoutHelper(*this, timeout, std::move(invoker));
+}
+
+template <class T>
+TFuture<T> TFutureBase<T>::WithTimeout(
+ std::optional<TDuration> timeout,
+ IInvokerPtr invoker) const
+{
+ return timeout ? WithTimeout(*timeout, std::move(invoker)) : TFuture<T>(Impl_);
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::Apply(TCallback<R(const NYT::TErrorOr<T>&)> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::Apply(TCallback<TErrorOr<R>(const NYT::TErrorOr<T>&)> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::Apply(TCallback<TFuture<R>(const NYT::TErrorOr<T>&)> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::ApplyUnique(TCallback<R(NYT::TErrorOr<T>&&)> callback) const
+{
+ return NYT::NDetail::ApplyUniqueHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::ApplyUnique(TCallback<TErrorOr<R>(NYT::TErrorOr<T>&&)> callback) const
+{
+ return NYT::NDetail::ApplyUniqueHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFutureBase<T>::ApplyUnique(TCallback<TFuture<R>(NYT::TErrorOr<T>&&)> callback) const
+{
+ return NYT::NDetail::ApplyUniqueHelper<R>(*this, std::move(callback));
+}
+
+template <class T>
+template <class U>
+TFuture<U> TFutureBase<T>::As() const
+{
+ if constexpr (std::is_same_v<U, void>) {
+ return TFuture<void>(Impl_);
+ }
+
+ if (!Impl_) {
+ return TFuture<U>();
+ }
+
+ auto promise = NewPromise<U>();
+
+ Subscribe(BIND_NO_PROPAGATE([=] (const NYT::TErrorOr<T>& value) {
+ promise.Set(NYT::TErrorOr<U>(value));
+ }));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([cancelable = AsCancelable()] (const NYT::TError& error) {
+ cancelable.Cancel(error);
+ }));
+
+ return promise;
+}
+
+template <class T>
+TFuture<void> TFutureBase<T>::AsVoid() const
+{
+ return TFuture<void>(Impl_);
+}
+
+template <class T>
+TCancelable TFutureBase<T>::AsCancelable() const
+{
+ return TCancelable(Impl_);
+}
+
+template <class T>
+TFutureBase<T>::TFutureBase(TIntrusivePtr<NYT::NDetail::TFutureState<T>> impl)
+ : Impl_(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFuture<T>::TFuture(std::nullopt_t)
+{ }
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::Apply(TCallback<R(const T&)> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, callback);
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::Apply(TCallback<R(T)> callback) const
+{
+ return this->Apply(TCallback<R(const T&)>(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::Apply(TCallback<TFuture<R>(const T&)> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, callback);
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::Apply(TCallback<TFuture<R>(T)> callback) const
+{
+ return this->Apply(TCallback<TFuture<R>(const T&)>(callback));
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::ApplyUnique(TCallback<R(T&&)> callback) const
+{
+ return NYT::NDetail::ApplyUniqueHelper<R>(*this, callback);
+}
+
+template <class T>
+template <class R>
+TFuture<R> TFuture<T>::ApplyUnique(TCallback<TFuture<R>(T&&)> callback) const
+{
+ return NYT::NDetail::ApplyUniqueHelper<R>(*this, callback);
+}
+
+template <class T>
+TFuture<T>::TFuture(TIntrusivePtr<NYT::NDetail::TFutureState<T>> impl)
+ : TFutureBase<T>(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TFuture<void>::TFuture(std::nullopt_t)
+{ }
+
+template <class R>
+TFuture<R> TFuture<void>::Apply(TCallback<R()> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, callback);
+}
+
+template <class R>
+TFuture<R> TFuture<void>::Apply(TCallback<TFuture<R>()> callback) const
+{
+ return NYT::NDetail::ApplyHelper<R>(*this, callback);
+}
+
+inline TFuture<void>::TFuture(TIntrusivePtr<NYT::NDetail::TFutureState<void>> impl)
+ : TFutureBase<void>(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TPromiseBase<T>::operator bool() const
+{
+ return Impl_.operator bool();
+}
+
+template <class T>
+void TPromiseBase<T>::Reset()
+{
+ Impl_.Reset();
+}
+
+template <class T>
+bool TPromiseBase<T>::IsSet() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->IsSet();
+}
+
+template <class T>
+void TPromiseBase<T>::Set(const NYT::TErrorOr<T>& value) const
+{
+ YT_ASSERT(Impl_);
+ Impl_->Set(value);
+}
+
+template <class T>
+void TPromiseBase<T>::Set(NYT::TErrorOr<T>&& value) const
+{
+ YT_ASSERT(Impl_);
+ Impl_->Set(std::move(value));
+}
+
+template <class T>
+template <class U>
+void TPromiseBase<T>::SetFrom(const TFuture<U>& another) const
+{
+ YT_ASSERT(Impl_);
+
+ auto this_ = *this;
+
+ another.Subscribe(BIND_NO_PROPAGATE([this_] (const NYT::TErrorOr<U>& value) {
+ this_.Set(value);
+ }));
+
+ OnCanceled(BIND_NO_PROPAGATE([anotherCancelable = another.AsCancelable()] (const NYT::TError& error) {
+ anotherCancelable.Cancel(error);
+ }));
+}
+
+template <class T>
+bool TPromiseBase<T>::TrySet(const NYT::TErrorOr<T>& value) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->TrySet(value);
+}
+
+template <class T>
+bool TPromiseBase<T>::TrySet(NYT::TErrorOr<T>&& value) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->TrySet(std::move(value));
+}
+
+template <class T>
+template <class U>
+inline void TPromiseBase<T>::TrySetFrom(TFuture<U> another) const
+{
+ YT_ASSERT(Impl_);
+
+ auto this_ = *this;
+
+ another.Subscribe(BIND_NO_PROPAGATE([this_] (const NYT::TErrorOr<U>& value) {
+ this_.TrySet(value);
+ }));
+
+ OnCanceled(BIND_NO_PROPAGATE([anotherCancelable = another.AsCancelable()] (const NYT::TError& error) {
+ anotherCancelable.Cancel(error);
+ }));
+}
+
+template <class T>
+const NYT::TErrorOr<T>& TPromiseBase<T>::Get() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->Get();
+}
+
+template <class T>
+std::optional<TErrorOr<T>> TPromiseBase<T>::TryGet() const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->TryGet();
+}
+
+template <class T>
+bool TPromiseBase<T>::IsCanceled() const
+{
+ return Impl_->IsCanceled();
+}
+
+template <class T>
+bool TPromiseBase<T>::OnCanceled(TCallback<void(const NYT::TError&)> handler) const
+{
+ YT_ASSERT(Impl_);
+ return Impl_->OnCanceled(std::move(handler));
+}
+
+template <class T>
+TFuture<T> TPromiseBase<T>::ToFuture() const
+{
+ return TFuture<T>(Impl_);
+}
+
+template <class T>
+TPromiseBase<T>::operator TFuture<T>() const
+{
+ return TFuture<T>(Impl_);
+}
+
+template <class T>
+TPromiseBase<T>::TPromiseBase(TIntrusivePtr<NYT::NDetail::TPromiseState<T>> impl)
+ : Impl_(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TPromise<T>::TPromise(std::nullopt_t)
+{ }
+
+template <class T>
+void TPromise<T>::Set(const T& value) const
+{
+ YT_ASSERT(this->Impl_);
+ this->Impl_->Set(value);
+}
+
+template <class T>
+void TPromise<T>::Set(T&& value) const
+{
+ YT_ASSERT(this->Impl_);
+ this->Impl_->Set(std::move(value));
+}
+
+template <class T>
+void TPromise<T>::Set(const NYT::TError& error) const
+{
+ Set(NYT::TErrorOr<T>(error));
+}
+
+template <class T>
+void TPromise<T>::Set(NYT::TError&& error) const
+{
+ Set(NYT::TErrorOr<T>(std::move(error)));
+}
+
+template <class T>
+bool TPromise<T>::TrySet(const T& value) const
+{
+ YT_ASSERT(this->Impl_);
+ return this->Impl_->TrySet(value);
+}
+
+template <class T>
+bool TPromise<T>::TrySet(T&& value) const
+{
+ YT_ASSERT(this->Impl_);
+ return this->Impl_->TrySet(std::move(value));
+}
+
+template <class T>
+bool TPromise<T>::TrySet(const NYT::TError& error) const
+{
+ return TrySet(NYT::TErrorOr<T>(error));
+}
+
+template <class T>
+bool TPromise<T>::TrySet(NYT::TError&& error) const
+{
+ return TrySet(NYT::TErrorOr<T>(std::move(error)));
+}
+
+template <class T>
+TPromise<T>::TPromise(TIntrusivePtr<NYT::NDetail::TPromiseState<T>> impl)
+ : TPromiseBase<T>(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TPromise<void>::TPromise(std::nullopt_t)
+{ }
+
+inline void TPromise<void>::Set() const
+{
+ YT_ASSERT(this->Impl_);
+ this->Impl_->Set(NYT::TError());
+}
+
+inline bool TPromise<void>::TrySet() const
+{
+ YT_ASSERT(this->Impl_);
+ return this->Impl_->TrySet(NYT::TError());
+}
+
+inline TPromise<void>::TPromise(TIntrusivePtr<NYT::NDetail::TPromiseState<void>> impl)
+ : TPromiseBase<void>(std::move(impl))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class TSignature>
+struct TAsyncViaHelper;
+
+template <class R, class... TArgs>
+struct TAsyncViaHelper<R(TArgs...)>
+{
+ using TUnderlying = typename TFutureTraits<R>::TUnderlying;
+ using TSourceCallback = TExtendedCallback<R(TArgs...)>;
+ using TTargetCallback = TExtendedCallback<TFuture<TUnderlying>(TArgs...)>;
+
+ static void Inner(
+ const TSourceCallback& this_,
+ const TPromise<TUnderlying>& promise,
+ TArgs... args)
+ {
+ auto canceler = NConcurrency::GetCurrentFiberCanceler();
+ if (canceler) {
+ promise.OnCanceled(std::move(canceler));
+ }
+
+ if (promise.IsCanceled()) {
+ promise.Set(NYT::TError(
+ NYT::EErrorCode::Canceled,
+ "Computation was canceled before it was started"));
+ return;
+ }
+
+ NYT::NDetail::TPromiseSetter<TUnderlying, R(TArgs...)>::Do(promise, this_, std::forward<TArgs>(args)...);
+ }
+
+ static TFuture<TUnderlying> Outer(
+ const TSourceCallback& this_,
+ const IInvokerPtr& invoker,
+ TArgs... args)
+ {
+ auto promise = NewPromise<TUnderlying>();
+ invoker->Invoke(BIND(&Inner, this_, promise, WrapToPassed(std::forward<TArgs>(args))...));
+ return promise;
+ }
+
+ static TFuture<TUnderlying> OuterGuarded(
+ const TSourceCallback& this_,
+ const IInvokerPtr& invoker,
+ NYT::TError cancellationError,
+ TArgs... args)
+ {
+ auto promise = NewPromise<TUnderlying>();
+ GuardedInvoke(
+ invoker,
+ BIND(&Inner, this_, promise, WrapToPassed(std::forward<TArgs>(args))...),
+ BIND([promise, cancellationError = std::move(cancellationError)] {
+ promise.Set(std::move(cancellationError));
+ }));
+ return promise;
+ }
+
+ static TTargetCallback Do(
+ TSourceCallback this_,
+ IInvokerPtr invoker)
+ {
+ return BIND_NO_PROPAGATE(&Outer, std::move(this_), std::move(invoker));
+ }
+
+ static TTargetCallback DoGuarded(
+ TSourceCallback this_,
+ IInvokerPtr invoker,
+ NYT::TError cancellationError)
+ {
+ return BIND_NO_PROPAGATE(&OuterGuarded, std::move(this_), std::move(invoker), std::move(cancellationError));
+ }
+};
+
+} // namespace NDetail
+
+template <class R, class... TArgs>
+TExtendedCallback<typename TFutureTraits<R>::TWrapped(TArgs...)>
+TExtendedCallback<R(TArgs...)>::AsyncVia(IInvokerPtr invoker) const
+{
+ return NYT::NDetail::TAsyncViaHelper<R(TArgs...)>::Do(*this, std::move(invoker));
+}
+
+template <class R, class... TArgs>
+TExtendedCallback<typename TFutureTraits<R>::TWrapped(TArgs...)>
+TExtendedCallback<R(TArgs...)>::AsyncViaGuarded(IInvokerPtr invoker, NYT::TError cancellationError) const
+{
+ return NYT::NDetail::TAsyncViaHelper<R(TArgs...)>::DoGuarded(*this, std::move(invoker), std::move(cancellationError));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFutureHolder<T>::TFutureHolder(std::nullopt_t)
+{ }
+
+template <class T>
+TFutureHolder<T>::TFutureHolder(TFuture<T> future)
+ : Future_(std::move(future))
+{ }
+
+template <class T>
+TFutureHolder<T>::~TFutureHolder()
+{
+ if (Future_) {
+ Future_.Cancel(NYT::TError("Future holder destroyed"));
+ }
+}
+
+template <class T>
+TFutureHolder<T>::operator bool() const
+{
+ return static_cast<bool>(Future_);
+}
+
+template <class T>
+TFuture<T>& TFutureHolder<T>::Get()
+{
+ return Future_;
+}
+
+template <class T>
+const TFuture<T>& TFutureHolder<T>::Get() const
+{
+ return Future_;
+}
+
+template <class T>
+const TFuture<T>& TFutureHolder<T>::operator*() const // noexcept
+{
+ return Future_;
+}
+
+template <class T>
+TFuture<T>& TFutureHolder<T>::operator*() // noexcept
+{
+ return Future_;
+}
+
+template <class T>
+const TFuture<T>* TFutureHolder<T>::operator->() const // noexcept
+{
+ return &Future_;
+}
+
+template <class T>
+TFuture<T>* TFutureHolder<T>::operator->() // noexcept
+{
+ return &Future_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+class TFutureCombinerResultHolder
+{
+public:
+ using TResult = std::vector<T>;
+
+ explicit TFutureCombinerResultHolder(int size)
+ : Result_(size)
+ { }
+
+ bool TrySetResult(int index, const NYT::TErrorOr<T>& errorOrValue)
+ {
+ if (errorOrValue.IsOK()) {
+ Result_[index] = errorOrValue.Value();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool TrySetPromise(const TPromise<TResult>& promise)
+ {
+ return promise.TrySet(std::move(Result_));
+ }
+
+private:
+ TResult Result_;
+};
+
+template <class T>
+class TFutureCombinerResultHolder<TErrorOr<T>>
+{
+public:
+ using TResult = std::vector<TErrorOr<T>>;
+
+ explicit TFutureCombinerResultHolder(int size)
+ : Result_(size)
+ { }
+
+ bool TrySetResult(int index, const NYT::TErrorOr<T>& errorOrValue)
+ {
+ Result_[index] = errorOrValue;
+ return true;
+ }
+
+ bool TrySetPromise(const TPromise<TResult>& promise)
+ {
+ return promise.TrySet(std::move(Result_));
+ }
+
+private:
+ TResult Result_;
+};
+
+template <>
+class TFutureCombinerResultHolder<void>
+{
+public:
+ using TResult = void;
+
+ explicit TFutureCombinerResultHolder(int /*size*/)
+ { }
+
+ bool TrySetResult(int /*index*/, const NYT::TError& error)
+ {
+ return error.IsOK();
+ }
+
+ bool TrySetPromise(const TPromise<TResult>& promise)
+ {
+ return promise.TrySet();
+ }
+};
+
+template <class T>
+class TFutureCombinerBase
+ : public TRefCounted
+{
+protected:
+ const std::vector<TFuture<T>> Futures_;
+
+ explicit TFutureCombinerBase(std::vector<TFuture<T>> futures)
+ : Futures_(std::move(futures))
+ { }
+
+ void CancelFutures(const NYT::TError& error)
+ {
+ for (const auto& future : Futures_) {
+ future.Cancel(error);
+ }
+ }
+
+ bool TryAcquireFuturesCancelLatch()
+ {
+ return !FuturesCancelLatch_.exchange(true);
+ }
+
+ void OnCanceled(const NYT::TError& error)
+ {
+ if (TryAcquireFuturesCancelLatch()) {
+ CancelFutures(error);
+ }
+ }
+
+private:
+ std::atomic<bool> FuturesCancelLatch_ = false;
+};
+
+template <class T>
+class TFutureCombinerWithSubscriptionBase
+ : public TFutureCombinerBase<T>
+{
+protected:
+ using TFutureCombinerBase<T>::TFutureCombinerBase;
+
+ void RegisterSubscriptionCookies(std::vector<TFutureCallbackCookie>&& cookies)
+ {
+ SubscriptionCookies_ = std::move(cookies);
+ YT_ASSERT(this->Futures_.size() == SubscriptionCookies_.size());
+ MaybeUnsubscribeFromFutures();
+ }
+
+ void OnCombinerFinished()
+ {
+ MaybeUnsubscribeFromFutures();
+ }
+
+private:
+ std::vector<TFutureCallbackCookie> SubscriptionCookies_;
+ std::atomic<int> SubscriptionLatch_ = 0;
+
+ void MaybeUnsubscribeFromFutures()
+ {
+ if (++SubscriptionLatch_ != 2) {
+ return;
+ }
+ for (size_t index = 0; index < this->Futures_.size(); ++index) {
+ this->Futures_[index].Unsubscribe(SubscriptionCookies_[index]);
+ }
+ }
+};
+
+template <class T>
+class TAnyFutureCombiner
+ : public TFutureCombinerWithSubscriptionBase<T>
+{
+public:
+ TAnyFutureCombiner(
+ std::vector<TFuture<T>> futures,
+ bool skipErrors,
+ TFutureCombinerOptions options)
+ : TFutureCombinerWithSubscriptionBase<T>(std::move(futures))
+ , SkipErrors_(skipErrors)
+ , Options_(options)
+ { }
+
+ TFuture<T> Run()
+ {
+ if (this->Futures_.empty()) {
+ return MakeFuture<T>(NYT::TError(
+ NYT::EErrorCode::FutureCombinerFailure,
+ "Any-of combiner failure: empty input"));
+ }
+
+ std::vector<TFutureCallbackCookie> subscriptionCookies;
+ subscriptionCookies.reserve(this->Futures_.size());
+ for (const auto& future : this->Futures_) {
+ TFutureCallbackCookie cookie;
+ if (future.IsSet()) {
+ cookie = NullFutureCallbackCookie;
+ OnFutureSet(future.Get());
+ } else {
+ cookie = future.Subscribe(BIND_NO_PROPAGATE(&TAnyFutureCombiner::OnFutureSet, MakeStrong(this)));
+ }
+ subscriptionCookies.push_back(cookie);
+ }
+ this->RegisterSubscriptionCookies(std::move(subscriptionCookies));
+
+ if (Options_.PropagateCancelationToInput) {
+ Promise_.OnCanceled(BIND_NO_PROPAGATE(&TAnyFutureCombiner::OnCanceled, MakeWeak(this)));
+ }
+
+ return Promise_;
+ }
+
+private:
+ const bool SkipErrors_;
+ const TFutureCombinerOptions Options_;
+ const TPromise<T> Promise_ = NewPromise<T>();
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ErrorsLock_);
+ std::vector<TError> Errors_;
+
+ void OnFutureSet(const NYT::TErrorOr<T>& result)
+ {
+ if (SkipErrors_ && !result.IsOK()) {
+ RegisterError(result);
+ return;
+ }
+
+ if (Promise_.TrySet(result)) {
+ this->OnCombinerFinished();
+ }
+
+ if (Options_.CancelInputOnShortcut &&
+ this->Futures_.size() > 1 &&
+ this->TryAcquireFuturesCancelLatch())
+ {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-of combiner shortcut: some response received"));
+ }
+ }
+
+ void RegisterError(const NYT::TError& error)
+ {
+ auto guard = Guard(ErrorsLock_);
+
+ Errors_.push_back(error);
+
+ if (Errors_.size() < this->Futures_.size()) {
+ return;
+ }
+
+ auto combinerError = NYT::TError(
+ NYT::EErrorCode::FutureCombinerFailure,
+ "Any-of combiner failure: all responses have failed")
+ << Errors_;
+
+ guard.Release();
+
+ if (Promise_.TrySet(combinerError)) {
+ this->OnCombinerFinished();
+ }
+ }
+};
+
+template <class T, class TResultHolder>
+class TAllFutureCombiner
+ : public TFutureCombinerBase<T>
+{
+public:
+ TAllFutureCombiner(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options)
+ : TFutureCombinerBase<T>(std::move(futures))
+ , Options_(options)
+ , ResultHolder_(this->Futures_.size())
+ { }
+
+ TFuture<typename TResultHolder::TResult> Run()
+ {
+ if (this->Futures_.empty()) {
+ return MakeFuture<typename TResultHolder::TResult>({});
+ }
+
+ for (int index = 0; index < static_cast<int>(this->Futures_.size()); ++index) {
+ const auto& future = this->Futures_[index];
+ if (future.IsSet()) {
+ OnFutureSet(index, future.Get());
+ } else {
+ future.Subscribe(BIND_NO_PROPAGATE(&TAllFutureCombiner::OnFutureSet, MakeStrong(this), index));
+ }
+ }
+
+ if (Options_.PropagateCancelationToInput) {
+ Promise_.OnCanceled(BIND_NO_PROPAGATE(&TAllFutureCombiner::OnCanceled, MakeWeak(this)));
+ }
+
+ return Promise_;
+ }
+
+private:
+ const TFutureCombinerOptions Options_;
+ const TPromise<typename TResultHolder::TResult> Promise_ = NewPromise<typename TResultHolder::TResult>();
+
+ TResultHolder ResultHolder_;
+
+ std::atomic<int> ResponseCount_ = 0;
+
+ void OnFutureSet(int index, const NYT::TErrorOr<T>& result)
+ {
+ if (!ResultHolder_.TrySetResult(index, result)) {
+ NYT::TError error(result);
+ Promise_.TrySet(error);
+
+ if (Options_.CancelInputOnShortcut &&
+ this->Futures_.size() > 1 &&
+ this->TryAcquireFuturesCancelLatch())
+ {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "All-of combiner shortcut: some response failed")
+ << error);
+ }
+
+ return;
+ }
+
+ if (++ResponseCount_ == static_cast<int>(this->Futures_.size())) {
+ ResultHolder_.TrySetPromise(Promise_);
+ }
+ }
+};
+
+template <class T, class TResultHolder>
+class TAnyNFutureCombiner
+ : public TFutureCombinerWithSubscriptionBase<T>
+{
+public:
+ TAnyNFutureCombiner(
+ std::vector<TFuture<T>> futures,
+ int n,
+ bool skipErrors,
+ TFutureCombinerOptions options)
+ : TFutureCombinerWithSubscriptionBase<T>(std::move(futures))
+ , Options_(options)
+ , N_(n)
+ , SkipErrors_(skipErrors)
+ , ResultHolder_(n)
+ {
+ YT_VERIFY(N_ >= 0);
+ }
+
+ TFuture<typename TResultHolder::TResult> Run()
+ {
+ if (N_ == 0) {
+ if (Options_.CancelInputOnShortcut && !this->Futures_.empty()) {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-N-of combiner shortcut: no responses needed"));
+ }
+
+ return MakeFuture<typename TResultHolder::TResult>({});
+ }
+
+ if (static_cast<int>(this->Futures_.size()) < N_) {
+ if (Options_.CancelInputOnShortcut) {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-N-of combiner shortcut: too few inputs given"));
+ }
+
+ return MakeFuture<typename TResultHolder::TResult>(NYT::TError(
+ NYT::EErrorCode::FutureCombinerFailure,
+ "Any-N-of combiner failure: %v responses needed, %v inputs given",
+ N_,
+ this->Futures_.size()));
+ }
+
+ std::vector<TFutureCallbackCookie> subscriptionCookies;
+ subscriptionCookies.reserve(this->Futures_.size());
+ for (int index = 0; index < static_cast<int>(this->Futures_.size()); ++index) {
+ TFutureCallbackCookie cookie;
+ const auto& future = this->Futures_[index];
+ if (future.IsSet()) {
+ cookie = NullFutureCallbackCookie;
+ OnFutureSet(index, future.Get());
+ } else {
+ cookie = future.Subscribe(
+ BIND(&TAnyNFutureCombiner::OnFutureSet, MakeStrong(this), index));
+ }
+ subscriptionCookies.push_back(cookie);
+ }
+ this->RegisterSubscriptionCookies(std::move(subscriptionCookies));
+
+ if (Options_.PropagateCancelationToInput) {
+ Promise_.OnCanceled(BIND(&TAnyNFutureCombiner::OnCanceled, MakeWeak(this)));
+ }
+
+ return Promise_;
+ }
+
+private:
+ const TFutureCombinerOptions Options_;
+ const int N_;
+ const bool SkipErrors_;
+ const TPromise<typename TResultHolder::TResult> Promise_ = NewPromise<typename TResultHolder::TResult>();
+
+ TResultHolder ResultHolder_;
+
+ std::atomic<int> ResponseCount_ = 0;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ErrorsLock_);
+ std::vector<TError> Errors_;
+
+ void OnFutureSet(int /*index*/, const NYT::TErrorOr<T>& result)
+ {
+ if (SkipErrors_ && !result.IsOK()) {
+ RegisterError(result);
+ return;
+ }
+
+ int responseIndex = ResponseCount_++;
+ if (responseIndex >= N_) {
+ return;
+ }
+
+ if (!ResultHolder_.TrySetResult(responseIndex, result)) {
+ NYT::TError error(result);
+ if (Promise_.TrySet(error)) {
+ this->OnCombinerFinished();
+ }
+
+ if (Options_.CancelInputOnShortcut &&
+ this->Futures_.size() > 1 &&
+ this->TryAcquireFuturesCancelLatch())
+ {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-N-of combiner shortcut: some input failed"));
+ }
+ return;
+ }
+
+ if (responseIndex == N_ - 1) {
+ if (ResultHolder_.TrySetPromise(Promise_)) {
+ this->OnCombinerFinished();
+ }
+
+ if (Options_.CancelInputOnShortcut &&
+ responseIndex < static_cast<int>(this->Futures_.size()) - 1 &&
+ this->TryAcquireFuturesCancelLatch())
+ {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-N-of combiner shortcut: enough responses received"));
+ }
+ }
+ }
+
+ void RegisterError(const NYT::TError& error)
+ {
+ auto guard = Guard(ErrorsLock_);
+
+ Errors_.push_back(error);
+
+ auto totalCount = static_cast<int>(this->Futures_.size());
+ auto failedCount = static_cast<int>(Errors_.size());
+ if (totalCount - failedCount >= N_) {
+ return;
+ }
+
+ auto combinerError = NYT::TError(
+ NYT::EErrorCode::FutureCombinerFailure,
+ "Any-N-of combiner failure: %v responses needed, %v failed, %v inputs given",
+ N_,
+ failedCount,
+ totalCount)
+ << Errors_;
+
+ guard.Release();
+
+ if (Promise_.TrySet(combinerError)) {
+ this->OnCombinerFinished();
+ }
+
+ if (Options_.CancelInputOnShortcut &&
+ this->TryAcquireFuturesCancelLatch())
+ {
+ this->CancelFutures(NYT::TError(
+ NYT::EErrorCode::FutureCombinerShortcut,
+ "Any-N-of combiner shortcut: one of responses failed")
+ << error);
+ }
+ }
+};
+
+} // namespace NDetail
+
+template <class T>
+TFuture<T> AnySucceeded(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options)
+{
+ if (futures.size() == 1) {
+ return std::move(futures[0]);
+ }
+ return New<NYT::NDetail::TAnyFutureCombiner<T>>(std::move(futures), true, options)
+ ->Run();
+}
+
+template <class T>
+TFuture<T> AnySet(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options)
+{
+ return New<NYT::NDetail::TAnyFutureCombiner<T>>(std::move(futures), false, options)
+ ->Run();
+}
+
+template <class T>
+TFuture<typename TFutureCombinerTraits<T>::TCombinedVector> AllSucceeded(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options)
+{
+ auto size = futures.size();
+ if constexpr (std::is_same_v<T, void>) {
+ if (size == 0) {
+ return VoidFuture;
+ }
+ if (size == 1) {
+ return std::move(futures[0]);
+ }
+ }
+ using TResultHolder = NYT::NDetail::TFutureCombinerResultHolder<T>;
+ return New<NYT::NDetail::TAllFutureCombiner<T, TResultHolder>>(std::move(futures), options)
+ ->Run();
+}
+
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AllSet(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options)
+{
+ using TResultHolder = NYT::NDetail::TFutureCombinerResultHolder<TErrorOr<T>>;
+ return New<NYT::NDetail::TAllFutureCombiner<T, TResultHolder>>(std::move(futures), options)
+ ->Run();
+}
+
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AllSetWithTimeout(
+ std::vector<TFuture<T>> futures,
+ TDuration timeout,
+ TFutureCombinerOptions options,
+ IInvokerPtr invoker)
+{
+ std::vector<TPromise<T>> promises(futures.size());
+ for (int index = 0; index < static_cast<int>(futures.size()); ++index) {
+ auto promise = NewPromise<T>();
+ futures[index].Subscribe(BIND([promise] (const NYT::TErrorOr<T>& value) {
+ promise.TrySet(value);
+ }));
+ promise.OnCanceled(BIND([future = futures[index]] (const NYT::TError& error) {
+ future.Cancel(error);
+ }));
+ promises[index] = promise;
+ }
+
+ std::vector<TFuture<T>> wrappedFutures(promises.size());
+ std::transform(promises.begin(), promises.end(), wrappedFutures.begin(), [] (const TPromise<T>& promise) {
+ return promise.ToFuture();
+ });
+
+ auto combinedFuture = AllSet(wrappedFutures, options);
+
+ auto cookie = NConcurrency::TDelayedExecutor::Submit(
+ BIND([promises, futures] {
+ for (int index = 0; index < static_cast<int>(futures.size()); ++index) {
+ auto error = NYT::TError(NYT::EErrorCode::Timeout, "Operation timed out");
+ promises[index].TrySet(error);
+ futures[index].Cancel(error);
+ }
+ }),
+ timeout,
+ std::move(invoker));
+
+ combinedFuture.AsVoid().Subscribe(BIND([cookie] (const NYT::TError& /*error*/) {
+ NConcurrency::TDelayedExecutor::Cancel(cookie);
+ }));
+
+ return combinedFuture;
+}
+
+template <class T>
+TFuture<typename TFutureCombinerTraits<T>::TCombinedVector> AnyNSucceeded(
+ std::vector<TFuture<T>> futures,
+ int n,
+ TFutureCombinerOptions options)
+{
+ auto size = futures.size();
+ if constexpr (std::is_same_v<T, void>) {
+ if (size == 1 && n == 1) {
+ return std::move(futures[0]);
+ }
+ }
+ using TResultHolder = NYT::NDetail::TFutureCombinerResultHolder<T>;
+ return New<NYT::NDetail::TAnyNFutureCombiner<T, TResultHolder>>(std::move(futures), n, true, options)
+ ->Run();
+}
+
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AnyNSet(
+ std::vector<TFuture<T>> futures,
+ int n,
+ TFutureCombinerOptions options)
+{
+ using TResultHolder = NYT::NDetail::TFutureCombinerResultHolder<TErrorOr<T>>;
+ return New<NYT::NDetail::TAnyNFutureCombiner<T, TResultHolder>>(std::move(futures), n, false, options)
+ ->Run();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+class TCancelableBoundedConcurrencyRunner
+ : public TRefCounted
+{
+public:
+ TCancelableBoundedConcurrencyRunner(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit)
+ : Callbacks_(std::move(callbacks))
+ , ConcurrencyLimit_(concurrencyLimit)
+ , Futures_(Callbacks_.size(), VoidFuture)
+ , Results_(Callbacks_.size())
+ , CurrentIndex_(std::min(ConcurrencyLimit_, ssize(Callbacks_)))
+ { }
+
+ TFuture<std::vector<TErrorOr<T>>> Run()
+ {
+ if (Callbacks_.empty()) {
+ return MakeFuture(std::vector<TErrorOr<T>>());
+ }
+
+ // No need to acquire SpinLock here.
+ auto startImmediatelyCount = CurrentIndex_;
+
+ for (int index = 0; index < startImmediatelyCount; ++index) {
+ RunCallback(index);
+ }
+
+ Promise_.OnCanceled(BIND(&TCancelableBoundedConcurrencyRunner::OnCanceled, MakeWeak(this)));
+
+ return Promise_;
+ }
+
+private:
+ const std::vector<TCallback<TFuture<T>()>> Callbacks_;
+ const i64 ConcurrencyLimit_;
+ const TPromise<std::vector<TErrorOr<T>>> Promise_ = NewPromise<std::vector<TErrorOr<T>>>();
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::optional<TError> CancelationError_;
+ std::vector<TFuture<void>> Futures_;
+ std::vector<TErrorOr<T>> Results_;
+ i64 CurrentIndex_;
+ int FinishedCount_ = 0;
+
+
+ void RunCallback(int index)
+ {
+ auto future = Callbacks_[index]();
+
+ if (future.IsSet()) {
+ OnResult(index, std::move(future.Get()));
+ return;
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (CancelationError_) {
+ guard.Release();
+ future.Cancel(*CancelationError_);
+ return;
+ }
+
+ Futures_[index] = future.template As<void>();
+ }
+
+ future.Subscribe(
+ BIND(&TCancelableBoundedConcurrencyRunner::OnResult, MakeStrong(this), index));
+ }
+
+ void OnResult(int index, const NYT::TErrorOr<T>& result)
+ {
+ int newIndex;
+ int finishedCount;
+ {
+ auto guard = Guard(SpinLock_);
+ if (CancelationError_) {
+ return;
+ }
+
+ newIndex = CurrentIndex_++;
+ finishedCount = ++FinishedCount_;
+ Results_[index] = result;
+ }
+
+ if (finishedCount == ssize(Callbacks_)) {
+ Promise_.TrySet(Results_);
+ }
+
+ if (newIndex < ssize(Callbacks_)) {
+ RunCallback(newIndex);
+ }
+ }
+
+ void OnCanceled(const NYT::TError& error)
+ {
+ auto wrappedError = NYT::TError(NYT::EErrorCode::Canceled, "Canceled")
+ << error;
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (CancelationError_) {
+ return;
+ }
+ CancelationError_ = wrappedError;
+ }
+
+ // NB: Setting of CancelationError_ disallows modification of CurrentIndex_ and Futures_.
+ for (int index = 0; index < std::min(ssize(Futures_), CurrentIndex_); ++index) {
+ Futures_[index].Cancel(wrappedError);
+ }
+
+ Promise_.TrySet(wrappedError);
+ }
+};
+
+template <class T>
+class TBoundedConcurrencyRunner
+ : public TRefCounted
+{
+public:
+ TBoundedConcurrencyRunner(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit)
+ : Callbacks_(std::move(callbacks))
+ , ConcurrencyLimit_(concurrencyLimit)
+ , Results_(Callbacks_.size())
+ { }
+
+ TFuture<std::vector<TErrorOr<T>>> Run()
+ {
+ if (Callbacks_.empty()) {
+ return MakeFuture(std::vector<TErrorOr<T>>());
+ }
+ int startImmediatelyCount = std::min(ConcurrencyLimit_, static_cast<int>(Callbacks_.size()));
+ CurrentIndex_ = startImmediatelyCount;
+ for (int index = 0; index < startImmediatelyCount; ++index) {
+ RunCallback(index);
+ }
+ return Promise_;
+ }
+
+private:
+ const std::vector<TCallback<TFuture<T>()>> Callbacks_;
+ const int ConcurrencyLimit_;
+ const TPromise<std::vector<TErrorOr<T>>> Promise_ = NewPromise<std::vector<TErrorOr<T>>>();
+
+ std::vector<TErrorOr<T>> Results_;
+ std::atomic<int> CurrentIndex_;
+ std::atomic<int> FinishedCount_ = 0;
+
+ void RunCallback(int index)
+ {
+ auto future = Callbacks_[index]();
+ if (future.IsSet()) {
+ OnResult(index, future.Get());
+ } else {
+ future.Subscribe(
+ BIND(&TBoundedConcurrencyRunner::OnResult, MakeStrong(this), index));
+ }
+ }
+
+ void OnResult(int index, const NYT::TErrorOr<T>& result)
+ {
+ Results_[index] = result;
+
+ int newIndex = CurrentIndex_++;
+ if (newIndex < static_cast<ssize_t>(Callbacks_.size())) {
+ RunCallback(newIndex);
+ }
+
+ if (++FinishedCount_ == static_cast<ssize_t>(Callbacks_.size())) {
+ Promise_.Set(Results_);
+ }
+ }
+};
+
+} // namespace NDetail
+
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> RunWithBoundedConcurrency(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit)
+{
+ YT_VERIFY(concurrencyLimit >= 0);
+ return New<NYT::NDetail::TBoundedConcurrencyRunner<T>>(std::move(callbacks), concurrencyLimit)
+ ->Run();
+}
+
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> CancelableRunWithBoundedConcurrency(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit)
+{
+ YT_VERIFY(concurrencyLimit >= 0);
+ return New<NYT::NDetail::TCancelableBoundedConcurrencyRunner<T>>(std::move(callbacks), concurrencyLimit)
+ ->Run();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+//! A hasher for TFuture.
+template <class T>
+struct THash<NYT::TFuture<T>>
+{
+ size_t operator () (const NYT::TFuture<T>& future) const
+ {
+ return THash<NYT::TIntrusivePtr<NYT::NDetail::TFutureState<T>>>()(future.Impl_);
+ }
+};
+
+//! A hasher for TPromise.
+template <class T>
+struct THash<NYT::TPromise<T>>
+{
+ size_t operator () (const NYT::TPromise<T>& promise) const
+ {
+ return THash<NYT::TIntrusivePtr<NYT::NDetail::TPromiseState<T>>>()(promise.Impl_);
+ }
+};
diff --git a/yt/yt/core/actions/future.cpp b/yt/yt/core/actions/future.cpp
new file mode 100644
index 0000000000..5d4580d350
--- /dev/null
+++ b/yt/yt/core/actions/future.cpp
@@ -0,0 +1,258 @@
+#include "future.h"
+#include "invoker_util.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TFuture<void> VoidFuture = NDetail::MakeWellKnownFuture(TError());
+const TFuture<bool> TrueFuture = NDetail::MakeWellKnownFuture(TErrorOr<bool>(true));
+const TFuture<bool> FalseFuture = NDetail::MakeWellKnownFuture(TErrorOr<bool>(false));
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+TFutureCallbackCookie TFutureState<void>::Subscribe(TVoidResultHandler handler)
+{
+ // Fast path.
+ if (Set_) {
+ RunNoExcept(handler, ResultError_);
+ return NullFutureCallbackCookie;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ guard.Release();
+ RunNoExcept(handler, ResultError_);
+ return NullFutureCallbackCookie;
+ } else {
+ HasHandlers_ = true;
+ return VoidResultHandlers_.Add(std::move(handler));
+ }
+ }
+}
+
+void TFutureState<void>::Unsubscribe(TFutureCallbackCookie cookie)
+{
+ // Fast path.
+ if (Set_ || cookie == NullFutureCallbackCookie) {
+ return;
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (Set_) {
+ return;
+ }
+ YT_VERIFY(DoUnsubscribe(cookie, &guard));
+ }
+}
+
+bool TFutureState<void>::Cancel(const TError& error) noexcept
+{
+ // NB: Cancel() could have been invoked when the last future reference
+ // is already released.
+ if (!TryRefFuture()) {
+ // The instance is mostly dead anyway.
+ return false;
+ }
+ // The reference is acquired above.
+ TIntrusivePtr<TFutureState<void>> this_(this, /* addReference */ false);
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (Set_ || AbandonedUnset_ || Canceled_) {
+ return false;
+ }
+ CancelationError_ = error;
+ Canceled_ = true;
+ }
+
+ if (CancelHandlers_.empty()) {
+ if (!TrySetError(NDetail::MakeCanceledError(error))) {
+ return false;
+ }
+ } else {
+ for (const auto& handler : CancelHandlers_) {
+ RunNoExcept(handler, error);
+ }
+ CancelHandlers_.clear();
+ }
+
+ return true;
+}
+
+bool TFutureState<void>::OnCanceled(TCancelHandler handler)
+{
+ // Fast path.
+ if (Set_) {
+ return false;
+ }
+ if (Canceled_) {
+ RunNoExcept(handler, CancelationError_);
+ return true;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Canceled_) {
+ guard.Release();
+ RunNoExcept(handler, CancelationError_);
+ return true;
+ } else if (!Set_) {
+ CancelHandlers_.push_back(std::move(handler));
+ return true;
+ }
+
+ return false;
+ }
+}
+
+bool TFutureState<void>::Wait(TInstant deadline) const
+{
+ // Fast path.
+ if (Set_ || AbandonedUnset_) {
+ return true;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ return true;
+ }
+ if (!ReadyEvent_) {
+ ReadyEvent_.reset(new NThreading::TEvent());
+ }
+ }
+
+ return ReadyEvent_->Wait(deadline);
+}
+
+bool TFutureState<void>::Wait(TDuration timeout) const
+{
+ return Wait(timeout.ToDeadLine());
+}
+
+void TFutureState<void>::InstallAbandonedError() const
+{
+ const_cast<TFutureState<void>*>(this)->InstallAbandonedError();
+}
+
+void TFutureState<void>::InstallAbandonedError()
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ if (AbandonedUnset_ && !Set_) {
+ SetResultError(NDetail::MakeAbandonedError());
+ Set_ = true;
+ }
+}
+
+void TFutureState<void>::ResetResult()
+{ }
+
+void TFutureState<void>::SetResultError(const TError& error)
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ ResultError_ = error;
+}
+
+bool TFutureState<void>::TrySetError(const TError& error)
+{
+ return TrySet(error);
+}
+
+bool TFutureState<void>::DoUnsubscribe(TFutureCallbackCookie cookie, TGuard<NThreading::TSpinLock>* guard)
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ return VoidResultHandlers_.TryRemove(cookie, guard);
+}
+
+void TFutureState<void>::WaitUntilSet() const
+{
+ // Fast path.
+ if (Set_) {
+ return;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ if (Set_) {
+ return ;
+ }
+ if (!ReadyEvent_) {
+ ReadyEvent_ = std::make_unique<NThreading::TEvent>();
+ }
+ }
+
+ ReadyEvent_->Wait();
+}
+
+bool TFutureState<void>::CheckIfSet() const
+{
+ // Fast path.
+ if (Set_) {
+ return true;
+ } else if (!AbandonedUnset_) {
+ return false;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(SpinLock_);
+ InstallAbandonedError();
+ return Set_;
+ }
+}
+
+void TFutureState<void>::OnLastFutureRefLost()
+{
+ ResetResult();
+ UnrefCancelable();
+}
+
+void TFutureState<void>::OnLastPromiseRefLost()
+{
+ // Check for fast path.
+ if (Set_) {
+ // Just kill the fake weak reference.
+ UnrefFuture();
+ return;
+ }
+
+ // Another fast path: no subscribers.
+ {
+ auto guard = Guard(SpinLock_);
+ if (!HasHandlers_ && !Canceled_) {
+ YT_ASSERT(!AbandonedUnset_);
+ AbandonedUnset_ = true;
+ // Cannot access this after UnrefFuture; in particular, cannot touch SpinLock_ in guard's dtor.
+ guard.Release();
+ UnrefFuture();
+ return;
+ }
+ }
+
+ // Slow path: notify the subscribers in a dedicated thread.
+ GetFinalizerInvoker()->Invoke(BIND([this] {
+ // Set the promise if the value is still missing.
+ TrySetError(NDetail::MakeAbandonedError());
+ // Kill the fake weak reference.
+ UnrefFuture();
+ }));
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/future.h b/yt/yt/core/actions/future.h
new file mode 100644
index 0000000000..9a7c4fcd8b
--- /dev/null
+++ b/yt/yt/core/actions/future.h
@@ -0,0 +1,724 @@
+#pragma once
+
+#include "public.h"
+#include "callback.h"
+#include "invoker.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <optional>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * Futures and Promises come in pairs and provide means for one party
+ * to wait for the result of the computation performed by the other party.
+ *
+ * TPromise<T> encapsulates the value-returning mechanism while
+ * TFuture<T> enables the clients to wait for this value.
+ * The value type is always TErrorOr<T> (which reduces to just TError for |T = void|).
+ *
+ * TPromise<T> is implicitly convertible to TFuture<T> while the reverse conversion
+ * is not allowed. This prevents a "malicious" client from setting the value
+ * by itself.
+ *
+ * TPromise<T> and TFuture<T> are lightweight refcounted handles pointing to the internal
+ * shared state. TFuture<T> acts as a weak reference while TPromise<T> acts as
+ * a strong reference. When no outstanding strong references (i.e. promises) to
+ * the shared state remain, the state automatically becomes failed
+ * with NYT::EErrorCode::Canceled error code.
+ *
+ * Promises support advisory cancellation. Consumer that holds a future might call Cancel() to notify
+ * producer that value is no longer needed. By default, Cancel() just Set()-s the associated shared state,
+ * and is equivalent to TrySet(TError(...)). If promise has associated cancelation handlers, Cancel()
+ * invokes them instead. It is cancelation handler's job to call TrySet() on the corresponding promise.
+ *
+ * Futures and Promises are thread-safe.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+class TPromiseState;
+template <class T>
+void Ref(TPromiseState<T>* state);
+template <class T>
+void Unref(TPromiseState<T>* state);
+
+class TCancelableStateBase;
+void Ref(TCancelableStateBase* state);
+void Unref(TCancelableStateBase* state);
+
+template <class T>
+class TFutureState;
+template <class T>
+void Ref(TFutureState<T>* state);
+template <class T>
+void Unref(TFutureState<T>* state);
+
+//! Constructs a well-known pre-set future like #VoidFuture.
+//! For such futures ref-counting is essentially disabled.
+template <class T>
+[[nodiscard]] TFuture<T> MakeWellKnownFuture(TErrorOr<T> value);
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an empty (unset) promise.
+template <class T>
+[[nodiscard]] TPromise<T> NewPromise();
+
+//! Constructs a pre-set promise.
+template <class T>
+[[nodiscard]] TPromise<T> MakePromise(TErrorOr<T> value);
+template <class T>
+[[nodiscard]] TPromise<T> MakePromise(T value);
+
+//! Constructs a successful pre-set future.
+template <class T>
+[[nodiscard]] TFuture<T> MakeFuture(TErrorOr<T> value);
+template <class T>
+[[nodiscard]] TFuture<T> MakeFuture(T value);
+
+////////////////////////////////////////////////////////////////////////////////
+// Comparison and swap.
+
+template <class T>
+bool operator==(const TFuture<T>& lhs, const TFuture<T>& rhs);
+template <class T>
+bool operator!=(const TFuture<T>& lhs, const TFuture<T>& rhs);
+template <class T>
+void swap(TFuture<T>& lhs, TFuture<T>& rhs);
+
+template <class T>
+bool operator==(const TPromise<T>& lhs, const TPromise<T>& rhs);
+template <class T>
+bool operator!=(const TPromise<T>& lhs, const TPromise<T>& rhs);
+template <class T>
+void swap(TPromise<T>& lhs, TPromise<T>& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+// A bunch of widely-used preset futures.
+
+//! A pre-set successful |void| future.
+extern const TFuture<void> VoidFuture;
+
+//! A pre-set successful |bool| future with |true| value.
+extern const TFuture<bool> TrueFuture;
+
+//! A pre-set successful |bool| future with |false| value.
+extern const TFuture<bool> FalseFuture;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TFutureBase;
+
+template <class T>
+class TPromiseBase;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A handle able of canceling some future.
+class TCancelable
+{
+public:
+ //! Creates a null cancelable.
+ TCancelable() = default;
+
+ //! Checks if the cancelable is null.
+ explicit operator bool() const;
+
+ //! Drops underlying associated state resetting the cancelable to null.
+ void Reset();
+
+ //! Notifies the producer that the promised value is no longer needed.
+ //! Returns |true| if succeeded, |false| is the promise was already set or canceled.
+ bool Cancel(const TError& error) const;
+
+private:
+ explicit TCancelable(TIntrusivePtr<NYT::NDetail::TCancelableStateBase> impl);
+
+ TIntrusivePtr<NYT::NDetail::TCancelableStateBase> Impl_;
+
+ friend bool operator==(const TCancelable& lhs, const TCancelable& rhs);
+ friend bool operator!=(const TCancelable& lhs, const TCancelable& rhs);
+ friend void swap(TCancelable& lhs, TCancelable& rhs);
+ template <class U>
+ friend struct ::THash;
+ template <class U>
+ friend class TFutureBase;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque future callback id.
+using TFutureCallbackCookie = int;
+constexpr TFutureCallbackCookie NullFutureCallbackCookie = -1;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base class for both TFuture<T> and its specialization TFuture<void>.
+/*!
+ * The resulting value can be accessed by either subscribing (#Subscribe)
+ * for it or retrieving it explicitly (#Get, #TryGet). Also it is possible
+ * to move the value out of the future state (#SubscribeUnique, #GetUnique, #TryGetUnique).
+ * In the latter case, however, at most one extraction is possible;
+ * further attempts to access the value will result in UB.
+ * In particular, at most one call to #SubscribeUnique, #GetUnique, and #TryGetUnique (expect
+ * for calls returning null) must happen to any future state (possibly shared by multiple
+ * TFuture instances).
+ */
+template <class T>
+class TFutureBase
+{
+public:
+ using TValueType = T;
+
+ //! Creates a null future.
+ TFutureBase() = default;
+
+ //! Checks if the future is null.
+ explicit operator bool() const;
+
+ //! Drops underlying associated state resetting the future to null.
+ void Reset();
+
+ //! Checks if the value is set.
+ bool IsSet() const;
+
+ //! Gets the value.
+ /*!
+ * This call will block until the value is set.
+ */
+ const TErrorOr<T>& Get() const;
+
+ //! Extracts the value by moving it out of the future state.
+ /*!
+ * This call will block until the value is set.
+ */
+ TErrorOr<T> GetUnique() const;
+
+ //! Waits for the value to become set.
+ /*!
+ * This call blocks until either the value is set or #timeout (if given) expires.
+ */
+ bool Wait(TDuration timeout = TDuration::Max()) const;
+
+ //! Waits for the value to become set.
+ /*!
+ * This call blocks until either the value is set or #deadline is reached.
+ */
+ bool Wait(TInstant deadline) const;
+
+ //! Gets the value; returns null if the value is not set yet.
+ /*!
+ * This call does not block.
+ */
+ std::optional<TErrorOr<T>> TryGet() const;
+
+ //! Extracts the value by moving it out of the future state; returns null if the value is not set yet.
+ /*!
+ * This call does not block.
+ */
+ std::optional<TErrorOr<T>> TryGetUnique() const;
+
+ //! Attaches a result handler.
+ /*!
+ * \param handler A callback to call when the value gets set
+ * (passing the value as a parameter).
+ *
+ * \returns a cookie that can later be passed to #Unsubscribe to remove the handler.
+ *
+ * \note
+ * If the value is set before the call to #Subscribe, then
+ * #callback gets called synchronously. In this case the returned
+ * cookie is #NullFutureCallbackCookie.
+ *
+ * \note
+ * If the callback throws an exception, the program terminates with
+ * a call to std::terminate. This is because the subscribers are notified synchronously
+ * and thus we have to ensure that the promise state remains valid by correctly
+ * finishing the Set call.
+ */
+ TFutureCallbackCookie Subscribe(TCallback<void(const TErrorOr<T>&)> handler) const;
+
+ //! Unsubscribes a previously subscribed callback.
+ /*!
+ * Callback cookies are recycled; don't retry calls to this function.
+ */
+ void Unsubscribe(TFutureCallbackCookie cookie) const;
+
+ //! Similar to #Subscribe but enables moving the value to the handler.
+ /*!
+ * Normally at most one such handler could be attached.
+ */
+ void SubscribeUnique(TCallback<void(TErrorOr<T>&&)> handler) const;
+
+ //! Notifies the producer that the promised value is no longer needed.
+ //! Returns |true| if succeeded, |false| is the promise was already set or canceled.
+ bool Cancel(const TError& error) const;
+
+ //! Returns a wrapper that suppresses cancellation attempts.
+ TFuture<T> ToUncancelable() const;
+
+ //! Returns a wrapper that handles cancellation requests by immediately becoming set
+ //! with NYT::EErrorCode::Canceled code.
+ TFuture<T> ToImmediatelyCancelable() const;
+
+ //! Returns a future that is either set to an actual value (if the original one is set in timely manner)
+ //! or to |EErrorCode::Timeout| (in case the deadline is reached).
+ //! The timeout event is handled in #invoker (DelayedExecutor is null).
+ TFuture<T> WithDeadline(TInstant deadline, IInvokerPtr invoker = nullptr) const;
+
+ //! Returns a future that is either set to an actual value (if the original one is set in timely manner)
+ //! or to |EErrorCode::Timeout| (in case of timeout).
+ //! The timeout event is handled in #invoker (DelayedExecutor is null).
+ TFuture<T> WithTimeout(TDuration timeout, IInvokerPtr invoker = nullptr) const;
+ TFuture<T> WithTimeout(std::optional<TDuration> timeout, IInvokerPtr invoker = nullptr) const;
+
+ //! Chains the asynchronous computation with another one.
+ template <class R>
+ TFuture<R> Apply(TCallback<R(const TErrorOr<T>&)> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<TErrorOr<R>(const TErrorOr<T>&)> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<TFuture<R>(const TErrorOr<T>&)> callback) const;
+
+ //! Same as #Apply but assumes that this chaining will be the only subscriber.
+ template <class R>
+ TFuture<R> ApplyUnique(TCallback<R(TErrorOr<T>&&)> callback) const;
+ template <class R>
+ TFuture<R> ApplyUnique(TCallback<TErrorOr<R>(TErrorOr<T>&&)> callback) const;
+ template <class R>
+ TFuture<R> ApplyUnique(TCallback<TFuture<R>(TErrorOr<T>&&)> callback) const;
+
+ //! Converts (successful) result to |U|; propagates errors as is.
+ template <class U>
+ TFuture<U> As() const;
+
+ //! Converts to TFuture<void> by discarding the value; propagates errors as is.
+ TFuture<void> AsVoid() const;
+
+ //! Converts to TCancelable interface.
+ TCancelable AsCancelable() const;
+
+protected:
+ explicit TFutureBase(TIntrusivePtr<NYT::NDetail::TFutureState<T>> impl);
+
+ TIntrusivePtr<NYT::NDetail::TFutureState<T>> Impl_;
+
+ template <class U>
+ friend bool operator==(const TFuture<U>& lhs, const TFuture<U>& rhs);
+ template <class U>
+ friend bool operator!=(const TFuture<U>& lhs, const TFuture<U>& rhs);
+ template <class U>
+ friend void swap(TFuture<U>& lhs, TFuture<U>& rhs);
+ template <class U>
+ friend struct ::THash;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TFuture
+ : public TFutureBase<T>
+{
+public:
+ TFuture() = default;
+ TFuture(std::nullopt_t);
+
+ //! Chains the asynchronous computation with another one.
+ template <class R>
+ TFuture<R> Apply(TCallback<R(const T&)> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<R(T)> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<TFuture<R>(const T&)> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<TFuture<R>(T)> callback) const;
+
+ //! Same as #Apply but assumes that this chaining will be the only subscriber.
+ template <class R>
+ TFuture<R> ApplyUnique(TCallback<R(T&&)> callback) const;
+ template <class R>
+ TFuture<R> ApplyUnique(TCallback<TFuture<R>(T&&)> callback) const;
+
+ using TFutureBase<T>::Apply;
+ using TFutureBase<T>::ApplyUnique;
+
+private:
+ explicit TFuture(TIntrusivePtr<NYT::NDetail::TFutureState<T>> impl);
+
+ template <class U>
+ friend TFuture<U> MakeFuture(TErrorOr<U> value);
+ template <class U>
+ friend TFuture<U> NDetail::MakeWellKnownFuture(TErrorOr<U> value);
+ template <class U>
+ friend TFuture<U> MakeFuture(U value);
+ template <class U>
+ friend class TFutureBase;
+ template <class U>
+ friend class TPromiseBase;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TFuture<void>
+ : public TFutureBase<void>
+{
+public:
+ TFuture() = default;
+ TFuture(std::nullopt_t);
+
+ //! Chains the asynchronous computation with another one.
+ template <class R>
+ TFuture<R> Apply(TCallback<R()> callback) const;
+ template <class R>
+ TFuture<R> Apply(TCallback<TFuture<R>()> callback) const;
+
+ using TFutureBase<void>::Apply;
+
+private:
+ explicit TFuture(const TIntrusivePtr<NYT::NDetail::TFutureState<void>> impl);
+
+ template <class U>
+ friend TFuture<U> MakeFuture(TErrorOr<U> value);
+ template <class U>
+ friend TFuture<U> NDetail::MakeWellKnownFuture(TErrorOr<U> value);
+ template <class U>
+ // XXX(babenko): 'NYT::' is a workaround; cf. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52625
+ friend class NYT::TFutureBase;
+ template <class U>
+ friend class TPromiseBase;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base class for both TPromise<T> and its specialization TPromise<void>.
+template <class T>
+class TPromiseBase
+{
+public:
+ using TValueType = T;
+
+ //! Creates a null promise.
+ TPromiseBase() = default;
+
+ //! Checks if the promise is null.
+ explicit operator bool() const;
+
+ //! Drops underlying associated state resetting the promise to null.
+ void Reset();
+
+ //! Checks if the value is set.
+ bool IsSet() const;
+
+ //! Sets the value.
+ /*!
+ * Calling this method also invokes all the subscribers.
+ */
+ void Set(const TErrorOr<T>& value) const;
+ void Set(TErrorOr<T>&& value) const;
+
+ //! Sets the value when #another future is set.
+ template <class U>
+ void SetFrom(const TFuture<U>& another) const;
+
+ //! Atomically invokes |Set|, if not already set or canceled.
+ //! Returns |true| if succeeded, |false| is the promise was already set or canceled.
+ bool TrySet(const TErrorOr<T>& value) const;
+ bool TrySet(TErrorOr<T>&& value) const;
+
+ //! Similar to #SetFrom but calls #TrySet instead of #Set.
+ template <class U>
+ void TrySetFrom(TFuture<U> another) const;
+
+ //! Gets the value.
+ /*!
+ * This call will block until the value is set.
+ */
+ const TErrorOr<T>& Get() const;
+
+ //! Gets the value if set.
+ /*!
+ * This call does not block.
+ */
+ std::optional<TErrorOr<T>> TryGet() const;
+
+ //! Checks if the promise is canceled.
+ bool IsCanceled() const;
+
+ //! Attaches a cancellation handler.
+ /*!
+ * \param handler A callback to call when TFuture<T>::Cancel is triggered
+ * by the client.
+ *
+ * Returns true if handler was successfully registered or was invoked inline.
+ *
+ * \note
+ * If the value is set before the call to #handlered, then
+ * #handler is discarded.
+ */
+ bool OnCanceled(TCallback<void (const TError&)> handler) const;
+
+ //! Converts promise into future.
+ operator TFuture<T>() const;
+ TFuture<T> ToFuture() const;
+
+protected:
+ explicit TPromiseBase(TIntrusivePtr<NYT::NDetail::TPromiseState<T>> impl);
+
+ TIntrusivePtr<NYT::NDetail::TPromiseState<T>> Impl_;
+
+ template <class U>
+ friend bool operator==(const TPromise<U>& lhs, const TPromise<U>& rhs);
+ template <class U>
+ friend bool operator!=(const TPromise<U>& lhs, const TPromise<U>& rhs);
+ template <class U>
+ friend void swap(TPromise<U>& lhs, TPromise<U>& rhs);
+ template <class U>
+ friend struct ::hash;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TPromise
+ : public TPromiseBase<T>
+{
+public:
+ TPromise() = default;
+ TPromise(std::nullopt_t);
+
+ void Set(const T& value) const;
+ void Set(T&& value) const;
+ void Set(const TError& error) const;
+ void Set(TError&& error) const;
+ using TPromiseBase<T>::Set;
+
+ bool TrySet(const T& value) const;
+ bool TrySet(T&& value) const;
+ bool TrySet(const TError& error) const;
+ bool TrySet(TError&& error) const;
+ using TPromiseBase<T>::TrySet;
+
+private:
+ explicit TPromise(TIntrusivePtr<NYT::NDetail::TPromiseState<T>> impl);
+
+ template <class U>
+ friend TPromise<U> NewPromise();
+ template <class U>
+ friend TPromise<U> MakePromise(TErrorOr<U> value);
+ template <class U>
+ friend TPromise<U> MakePromise(U value);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TPromise<void>
+ : public TPromiseBase<void>
+{
+public:
+ TPromise() = default;
+ TPromise(std::nullopt_t);
+
+ void Set() const;
+ using TPromiseBase<void>::Set;
+
+ bool TrySet() const;
+ using TPromiseBase<void>::TrySet;
+
+private:
+ explicit TPromise(TIntrusivePtr<NYT::NDetail::TPromiseState<void>> state);
+
+ template <class U>
+ friend TPromise<U> NewPromise();
+ template <class U>
+ friend TPromise<U> MakePromise(TErrorOr<U> value);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides a noncopyable but movable wrapper around TFuture<T> whose destructor
+//! cancels the underlying future.
+/*!
+ * TFutureHolder wraps a (typically resource-consuming) computation and cancels it on scope exit
+ * thus preventing leaking this computation.
+ */
+template <class T>
+class TFutureHolder
+{
+public:
+ //! Constructs an empty holder.
+ TFutureHolder() = default;
+
+ //! Constructs an empty holder.
+ TFutureHolder(std::nullopt_t);
+
+ //! Wraps #future into a holder.
+ TFutureHolder(TFuture<T> future);
+
+ //! Cancels the underlying future (if any).
+ ~TFutureHolder();
+
+ TFutureHolder(const TFutureHolder<T>& other) = delete;
+ TFutureHolder(TFutureHolder<T>&& other) = default;
+
+ TFutureHolder& operator = (const TFutureHolder<T>& other) = delete;
+ TFutureHolder& operator = (TFutureHolder<T>&& other) = default;
+
+ //! Returns |true| if the holder has an underlying future.
+ explicit operator bool() const;
+
+ //! Returns the underlying future.
+ const TFuture<T>& Get() const;
+
+ //! Returns the underlying future.
+ TFuture<T>& Get();
+
+ //! Returns the underlying future.
+ const TFuture<T>& operator*() const; // noexcept
+
+ //! Returns the underlying future.
+ TFuture<T>& operator*(); // noexcept
+
+ //! Returns the underlying future.
+ const TFuture<T>* operator->() const; // noexcept
+
+ //! Returns the underlying future.
+ TFuture<T>* operator->(); // noexcept
+
+private:
+ TFuture<T> Future_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Future combiners: take a set of futures and meld them into a new one.
+
+template <class T>
+struct TFutureCombinerTraits
+{
+ using TCombinedVector = std::vector<T>;
+};
+
+template <>
+struct TFutureCombinerTraits<void>
+{
+ using TCombinedVector = void;
+};
+
+struct TFutureCombinerOptions
+{
+ //! If true, canceling the future returned from the combiner
+ //! automatically cancels the original input futures.
+ bool PropagateCancelationToInput = true;
+
+ //! If true, the combiner cancels all irrelevant input futures
+ //! when the combined future gets set. E.g. when #AnySucceeded
+ //! notices some of its inputs being set, it cancels the others.
+ bool CancelInputOnShortcut = true;
+};
+
+//! Returns the future that gets set when any of #futures is set.
+//! The value of the returned future is set to the value of that first-set
+//! future among #futures.
+//! Individual errors are ignored; if all input futures fail then an error is reported.
+template <class T>
+TFuture<T> AnySucceeded(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options = {});
+
+//! Same as above.
+//! Errors happening in #futures are regarded as regular values; the first-set (either
+//! successfully or not) future completes the whole computation.
+template <class T>
+TFuture<T> AnySet(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options = {});
+
+//! Returns the future that gets set when all of #futures are set.
+//! The values of #futures are collected and returned in the
+//! value of the combined future (the order matches that of #futures).
+//! When some of #futures fail, its error is immediately propagated into the combined future
+//! and thus interrupts the whole computation.
+template <class T>
+TFuture<typename TFutureCombinerTraits<T>::TCombinedVector> AllSucceeded(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options = {});
+
+//! Same as above.
+//! The values of #futures are wrapped in #TErrorOr; individual
+//! errors are propagated as-is and do not interrupt the whole computation.
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AllSet(
+ std::vector<TFuture<T>> futures,
+ TFutureCombinerOptions options = {});
+
+//! Same as above, but with an additional timeout parameter: a timeout error is returned for futures
+//! that don't complete within the given duration.
+//! The timeout event is handled in #invoker (DelayedExecutor is null).
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AllSetWithTimeout(
+ std::vector<TFuture<T>> futures,
+ TDuration timeout,
+ TFutureCombinerOptions options = {},
+ IInvokerPtr invoker = nullptr);
+
+//! Returns the future that gets set when #n of #futures are set.
+//! The values of #futures are collected and returned in the
+//! value of the combined future (in the order of their fulfillment,
+//! which is typically racy).
+//! Individual errors are ignored; if less than #n successful results
+//! could be collected then an error is reported.
+template <class T>
+TFuture<typename TFutureCombinerTraits<T>::TCombinedVector> AnyNSucceeded(
+ std::vector<TFuture<T>> futures,
+ int n,
+ TFutureCombinerOptions options = {});
+
+//! Same as above.
+//! The values of #futures are wrapped in TErrorOr; individual
+//! errors are propagated as-is and do not interrupt the whole computation.
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> AnyNSet(
+ std::vector<TFuture<T>> futures,
+ int n,
+ TFutureCombinerOptions options = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(akozhikhov): Drop this version in favor of the one below.
+//! Executes given #callbacks, allowing up to #concurrencyLimit simultaneous invocations.
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> RunWithBoundedConcurrency(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit);
+
+//! Same as above but supports cancelation.
+template <class T>
+TFuture<std::vector<TErrorOr<T>>> CancelableRunWithBoundedConcurrency(
+ std::vector<TCallback<TFuture<T>()>> callbacks,
+ int concurrencyLimit);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Used for marking unused futures for easier search.
+#define YT_UNUSED_FUTURE(var) Y_UNUSED(var)
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define FUTURE_INL_H_
+#include "future-inl.h"
+#undef FUTURE_INL_H_
diff --git a/yt/yt/core/actions/invoker-inl.h b/yt/yt/core/actions/invoker-inl.h
new file mode 100644
index 0000000000..4abd399de7
--- /dev/null
+++ b/yt/yt/core/actions/invoker-inl.h
@@ -0,0 +1,29 @@
+#ifndef INVOKER_INL_H_
+#error "Direct inclusion of this file is not allowed, include invoker.h"
+// For the sake of sane code completion.
+#include "invoker.h"
+#endif
+#undef INVOKER_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class R, class... TArgs>
+TExtendedCallback<R(TArgs...)>
+TExtendedCallback<R(TArgs...)>::Via(IInvokerPtr invoker) const
+{
+ static_assert(
+ std::is_void_v<R>,
+ "Via() can only be used with void return type.");
+ YT_ASSERT(invoker);
+
+ auto this_ = *this;
+ return BIND_NO_PROPAGATE([=, invoker = std::move(invoker)] (TArgs... args) {
+ invoker->Invoke(BIND_NO_PROPAGATE(this_, WrapToPassed(std::forward<TArgs>(args))...));
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker.h b/yt/yt/core/actions/invoker.h
new file mode 100644
index 0000000000..b0aa140647
--- /dev/null
+++ b/yt/yt/core/actions/invoker.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "callback.h"
+#include "bind.h"
+
+#include <yt/yt/core/threading/public.h>
+
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IInvoker
+ : public virtual TRefCounted
+{
+ //! Schedules invocation of a given callback.
+ virtual void Invoke(TClosure callback) = 0;
+
+ //! Schedules multiple callbacks.
+ virtual void Invoke(TMutableRange<TClosure> callbacks) = 0;
+
+ //! Returns the thread id this invoker is bound to.
+ //! For invokers not bound to any particular thread,
+ //! returns |InvalidThreadId|.
+ virtual NThreading::TThreadId GetThreadId() const = 0;
+
+ //! Returns true if this invoker is either equal to #invoker or wraps it,
+ //! in some sense.
+ virtual bool CheckAffinity(const IInvokerPtr& invoker) const = 0;
+
+ //! Returns true if invoker is serialized, i.e. never executes
+ //! two callbacks concurrently.
+ virtual bool IsSerialized() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IInvoker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPrioritizedInvoker
+ : public virtual IInvoker
+{
+ using IInvoker::Invoke;
+
+ //! Schedules invocation of a given callback with a given priority.
+ /*
+ * Larger priority values dominate over smaller ones.
+ *
+ * While a typical invoker executes callbacks in the order they were
+ * enqueued via IInvoker::Invoke (holds for most but not all invoker types),
+ * callbacks enqueued via IPrioritizedInvoker::Invoke are subject to reordering.
+ */
+ virtual void Invoke(TClosure callback, i64 priority) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPrioritizedInvoker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISuspendableInvoker
+ : public virtual IInvoker
+{
+ using IInvoker::Invoke;
+
+ //! Puts invoker into suspended mode.
+ /*
+ * Warning: This function is not thread-safe.
+ * When all currently executing callbacks will be finished, returned future will be set.
+ * All incoming callbacks will be queued until Resume is called.
+ */
+ virtual TFuture<void> Suspend() = 0;
+
+ //! Puts invoker out of suspended mode.
+ /*
+ * Warning: This function is not thread-safe.
+ * All queued callbacks will be at once submitted to the underlying invoker.
+ * All incoming callbacks will be at once propagated to underlying invoker.
+ */
+ virtual void Resume() = 0;
+
+ //! Returns true when invoker is suspended (i.e. no callbacks are submitted).
+ virtual bool IsSuspended() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISuspendableInvoker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define INVOKER_INL_H_
+#include "invoker-inl.h"
+#undef INVOKER_INL_H_
+
diff --git a/yt/yt/core/actions/invoker_detail.cpp b/yt/yt/core/actions/invoker_detail.cpp
new file mode 100644
index 0000000000..d70aa77081
--- /dev/null
+++ b/yt/yt/core/actions/invoker_detail.cpp
@@ -0,0 +1,68 @@
+#include "invoker_detail.h"
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/tag.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInvokerWrapper::TInvokerWrapper(IInvokerPtr underlyingInvoker)
+ : UnderlyingInvoker_(std::move(underlyingInvoker))
+{
+ YT_VERIFY(UnderlyingInvoker_);
+}
+
+void TInvokerWrapper::Invoke(TClosure callback)
+{
+ return UnderlyingInvoker_->Invoke(std::move(callback));
+}
+
+void TInvokerWrapper::Invoke(TMutableRange<TClosure> callbacks)
+{
+ return UnderlyingInvoker_->Invoke(callbacks);
+}
+
+NThreading::TThreadId TInvokerWrapper::GetThreadId() const
+{
+ return UnderlyingInvoker_->GetThreadId();
+}
+
+bool TInvokerWrapper::CheckAffinity(const IInvokerPtr& invoker) const
+{
+ return
+ invoker.Get() == this ||
+ UnderlyingInvoker_->CheckAffinity(invoker);
+}
+
+bool TInvokerWrapper::IsSerialized() const
+{
+ return UnderlyingInvoker_->IsSerialized();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInvokerProfileWrapper::TInvokerProfileWrapper(NProfiling::IRegistryImplPtr registry, const TString& invokerFamily, const NProfiling::TTagSet& tagSet)
+{
+ auto profiler = NProfiling::TProfiler("/invoker", NProfiling::TProfiler::DefaultNamespace, tagSet, registry).WithHot();
+ WaitTimer_ = profiler.Timer(invokerFamily + "/wait");
+}
+
+TClosure TInvokerProfileWrapper::WrapCallback(TClosure callback)
+{
+ auto invokedAt = GetCpuInstant();
+
+ return BIND([invokedAt, waitTimer = WaitTimer_, callback = std::move(callback)] {
+ // Measure the time from WrapCallback() to callback().
+ auto waitTime = CpuDurationToDuration(GetCpuInstant() - invokedAt);
+ waitTimer.Record(waitTime);
+ callback();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker_detail.h b/yt/yt/core/actions/invoker_detail.h
new file mode 100644
index 0000000000..a445d00d52
--- /dev/null
+++ b/yt/yt/core/actions/invoker_detail.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "public.h"
+#include "invoker.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInvokerWrapper
+ : public virtual IInvoker
+{
+public:
+ void Invoke(TClosure callback) override;
+
+ void Invoke(TMutableRange<TClosure> callbacks) override;
+
+ NThreading::TThreadId GetThreadId() const override;
+ bool CheckAffinity(const IInvokerPtr& invoker) const override;
+ bool IsSerialized() const override;
+
+protected:
+ explicit TInvokerWrapper(IInvokerPtr underlyingInvoker);
+
+ IInvokerPtr UnderlyingInvoker_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper base which makes callbacks track their invocation time and profile their wait time.
+class TInvokerProfileWrapper
+{
+public:
+ /*!
+ * #registry defines a profile registry where sensors data is stored.
+ * #invokerFamily defines a family of invokers, e.g. "serialized" or "prioriized" and appears in sensor's name.
+ * #tagSet defines a particular instance of the invoker and appears in sensor's tags.
+ */
+ TInvokerProfileWrapper(NProfiling::IRegistryImplPtr registry, const TString& invokerFamily, const NProfiling::TTagSet& tagSet);
+
+protected:
+ TClosure WrapCallback(TClosure callback);
+
+private:
+ NProfiling::TEventTimer WaitTimer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker_pool-inl.h b/yt/yt/core/actions/invoker_pool-inl.h
new file mode 100644
index 0000000000..fe467b85be
--- /dev/null
+++ b/yt/yt/core/actions/invoker_pool-inl.h
@@ -0,0 +1,74 @@
+#ifndef INVOKER_POOL_INL_H_
+#error "Direct inclusion of this file is not allowed, include invoker_pool.h"
+// For the sake of sane code completion.
+#include "invoker_pool.h"
+#endif
+#undef INVOKER_POOL_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+struct TDummyInvokerHolder
+{ };
+
+// TInvokerHolder represents any object type capable of holding underlying invokers.
+// It only needs to outlive underlying invokers and is not used in other way.
+// TInvokerPoolWrapper with TDummyInvokerHolder is used in tests.
+template <class TInvoker, class TInvokerHolder = TDummyInvokerHolder>
+class TInvokerPoolWrapper
+ : public IGenericInvokerPool<TInvoker>
+{
+private:
+ using TInvokerPtr = TIntrusivePtr<TInvoker>;
+
+public:
+ explicit TInvokerPoolWrapper(
+ std::vector<TInvokerPtr> invokers,
+ TInvokerHolder invokerHolder = TDummyInvokerHolder())
+ : InvokerHolder_(std::move(invokerHolder))
+ , Invokers_(std::move(invokers))
+ { }
+
+ int GetSize() const override
+ {
+ return Invokers_.size();
+ }
+
+protected:
+ const TInvokerPtr& DoGetInvoker(int index) const override
+ {
+ YT_VERIFY(0 <= index && index < std::ssize(Invokers_));
+ return Invokers_[index];
+ }
+
+private:
+ const TInvokerHolder InvokerHolder_;
+ std::vector<TInvokerPtr> Invokers_;
+};
+
+} // namespace NDetail
+
+template <class TInvokerFunctor, class TInputInvoker, class TOutputInvoker>
+TIntrusivePtr<IGenericInvokerPool<TOutputInvoker>> TransformInvokerPool(
+ TIntrusivePtr<IGenericInvokerPool<TInputInvoker>> inputInvokerPool,
+ TInvokerFunctor&& functor)
+{
+ const auto invokerCount = inputInvokerPool->GetSize();
+
+ std::vector<TIntrusivePtr<TOutputInvoker>> invokers;
+ invokers.reserve(invokerCount);
+ for (int invokerIndex = 0; invokerIndex < invokerCount; ++invokerIndex) {
+ invokers.push_back(functor(inputInvokerPool->GetInvoker(invokerIndex)));
+ }
+
+ return New<NYT::NDetail::TInvokerPoolWrapper<TOutputInvoker, TIntrusivePtr<IGenericInvokerPool<TInputInvoker>>>>(
+ std::move(invokers),
+ std::move(inputInvokerPool));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker_pool.cpp b/yt/yt/core/actions/invoker_pool.cpp
new file mode 100644
index 0000000000..60e6153f88
--- /dev/null
+++ b/yt/yt/core/actions/invoker_pool.cpp
@@ -0,0 +1,32 @@
+#include "invoker_pool.h"
+
+#include "future.h"
+#include "invoker.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> SuspendInvokerPool(const ISuspendableInvokerPoolPtr& invokerPool)
+{
+ const auto size = invokerPool->GetSize();
+
+ std::vector<TFuture<void>> futures;
+ futures.reserve(size);
+ for (int i = 0; i < size; ++i) {
+ futures.push_back(invokerPool->GetInvoker(i)->Suspend());
+ }
+
+ return AllSucceeded(std::move(futures));
+}
+
+void ResumeInvokerPool(const ISuspendableInvokerPoolPtr& invokerPool)
+{
+ for (int i = 0, size = invokerPool->GetSize(); i < size; ++i) {
+ invokerPool->GetInvoker(i)->Resume();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker_pool.h b/yt/yt/core/actions/invoker_pool.h
new file mode 100644
index 0000000000..c04b2f0d9f
--- /dev/null
+++ b/yt/yt/core/actions/invoker_pool.h
@@ -0,0 +1,131 @@
+#pragma once
+
+#include "public.h"
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides access to indexed invokers.
+/*
+ * Underlying invokers are supposed to share common state in the pool
+ * and work in cooperative way (e.g.: pool could share CPU between invokers fairly).
+ *
+ * Interface is generic, but user is supposed to work with instantiations for well known
+ * invoker types: see below IInvokerPool, IPrioritizedInvokerPool, ISuspendableInvokerPool, etc.
+*/
+template <class TInvoker>
+class IGenericInvokerPool
+ : public virtual TRefCounted
+{
+public:
+ //! Returns number of invokers in the pool.
+ virtual int GetSize() const = 0;
+
+ //! Returns reference to the invoker from the underlying storage by the integer #index.
+ //! Parameter #index is supposed to take values in the [0, implementation-defined limit) range.
+ const TIntrusivePtr<TInvoker>& GetInvoker(int index) const
+ {
+ return DoGetInvoker(index);
+ }
+
+ //! Returns reference to the invoker from the underlying storage by the enum #index.
+ //! Parameter #index is supposed to take values in the [0, implementation-defined limit) range.
+ template <class E>
+ requires TEnumTraits<E>::IsEnum
+ const TIntrusivePtr<TInvoker>& GetInvoker(E index) const
+ {
+ return DoGetInvoker(ToUnderlying(index));
+ }
+
+protected:
+ //! Returns reference to the invoker from the underlying storage by the integer #index.
+ virtual const TIntrusivePtr<TInvoker>& DoGetInvoker(int index) const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(IInvokerPool)
+DEFINE_REFCOUNTED_TYPE(IPrioritizedInvokerPool)
+DEFINE_REFCOUNTED_TYPE(ISuspendableInvokerPool)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! For each underlying invoker calls Suspend. Returns combined future.
+TFuture<void> SuspendInvokerPool(const ISuspendableInvokerPoolPtr& suspendableInvokerPool);
+
+//! For each underlying invoker calls Resume.
+void ResumeInvokerPool(const ISuspendableInvokerPoolPtr& suspendableInvokerPool);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IDiagnosableInvokerPool
+ : public IInvokerPool
+{
+public:
+ struct TInvokerStatistics
+ {
+ ui64 EnqueuedActionCount = 0;
+ ui64 DequeuedActionCount = 0;
+ ui64 WaitingActionCount = 0;
+ TDuration AverageWaitTime = {};
+ };
+
+ //! Returns statistics of the invoker by the integer #index.
+ //! Parameter #index is supposed to take values in the [0, implementation-defined limit) range.
+ TInvokerStatistics GetInvokerStatistics(int index) const
+ {
+ return DoGetInvokerStatistics(index);
+ }
+
+ //! Returns statistics of the invoker by the integer #index.
+ //! Parameter #index is supposed to take values in the [0, implementation-defined limit) range.
+ template <class E>
+ requires TEnumTraits<E>::IsEnum
+ TInvokerStatistics GetInvokerStatistics(E index) const
+ {
+ return DoGetInvokerStatistics(ToUnderlying(index));
+ }
+
+protected:
+ virtual TInvokerStatistics DoGetInvokerStatistics(int index) const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IDiagnosableInvokerPool)
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+//! Helper is provided for step-by-step inferring of the TOutputInvoker given template arguments.
+template <class TInvokerFunctor, class TInputInvoker>
+struct TTransformInvokerPoolHelper
+{
+ using TInputInvokerPtr = TIntrusivePtr<TInputInvoker>;
+ using TOutputInvokerPtr = std::invoke_result_t<TInvokerFunctor, TInputInvokerPtr>;
+ using TOutputInvoker = typename TOutputInvokerPtr::TUnderlying;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Applies #functor (TInputInvoker is supposed to represent mapping from TInputInvokerPtr to TOutputInvokerPtr)
+//! to all invokers in the #inputInvokerPool producing IGenericInvokerPool<TOutputInvoker>.
+//! Output invoker pool is guaranteed to capture input invoker pool so feel free to chain TransformInvokerPool calls.
+template <
+ class TInvokerFunctor,
+ class TInputInvoker,
+ class TOutputInvoker = typename NDetail::TTransformInvokerPoolHelper<TInvokerFunctor, TInputInvoker>::TOutputInvoker>
+TIntrusivePtr<IGenericInvokerPool<TOutputInvoker>> TransformInvokerPool(
+ TIntrusivePtr<IGenericInvokerPool<TInputInvoker>> inputInvokerPool,
+ TInvokerFunctor&& functor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define INVOKER_POOL_INL_H_
+#include "invoker_pool-inl.h"
+#undef INVOKER_POOL_INL_H_
diff --git a/yt/yt/core/actions/invoker_util.cpp b/yt/yt/core/actions/invoker_util.cpp
new file mode 100644
index 0000000000..7b1abcd1e0
--- /dev/null
+++ b/yt/yt/core/actions/invoker_util.cpp
@@ -0,0 +1,187 @@
+#include "invoker_util.h"
+#include "invoker.h"
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/fls.h>
+#include <yt/yt/core/concurrency/system_invokers.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+#include <yt/yt/core/misc/singleton.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <stack>
+
+namespace NYT {
+
+using namespace NConcurrency;
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSyncInvoker
+ : public IInvoker
+{
+public:
+ void Invoke(TClosure callback) override
+ {
+ static TFlsSlot<TFiberState> StateSlot;
+ auto& state = *StateSlot;
+
+ // We optimize invocation of recursion-free callbacks, i.e. those that do
+ // not invoke anything in our invoker. This is done by introducing the AlreadyInvoking
+ // flag which allows us to handle such case without allocation of the deferred
+ // callback queue.
+
+ if (state.AlreadyInvoking) {
+ // Ensure deferred callback queue exists and push our callback into it.
+ if (!state.DeferredCallbacks) {
+ state.DeferredCallbacks.emplace();
+ }
+ state.DeferredCallbacks->push(std::move(callback));
+ } else {
+ // We are the outermost callback; execute synchronously.
+ state.AlreadyInvoking = true;
+ callback();
+ callback.Reset();
+ // If some callbacks were deferred, execute them until the queue is drained.
+ // Note that some of the callbacks may defer new callbacks, which is perfectly valid.
+ if (state.DeferredCallbacks) {
+ while (!state.DeferredCallbacks->empty()) {
+ state.DeferredCallbacks->front()();
+ state.DeferredCallbacks->pop();
+ }
+ // Reset queue to reduce fiber memory footprint.
+ state.DeferredCallbacks.reset();
+ }
+ state.AlreadyInvoking = false;
+ }
+ }
+
+ void Invoke(TMutableRange<TClosure> callbacks) override
+ {
+ for (auto& callback : callbacks) {
+ Invoke(std::move(callback));
+ }
+ }
+
+ bool CheckAffinity(const IInvokerPtr& invoker) const override
+ {
+ return invoker.Get() == this;
+ }
+
+ bool IsSerialized() const override
+ {
+ return true;
+ }
+
+ TThreadId GetThreadId() const override
+ {
+ return InvalidThreadId;
+ }
+
+private:
+ struct TFiberState
+ {
+ bool AlreadyInvoking = false;
+ std::optional<TRingQueue<TClosure>> DeferredCallbacks;
+ };
+};
+
+IInvokerPtr GetSyncInvoker()
+{
+ return LeakyRefCountedSingleton<TSyncInvoker>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullInvoker
+ : public IInvoker
+{
+public:
+ void Invoke(TClosure /*callback*/) override
+ { }
+
+ void Invoke(TMutableRange<TClosure> /*callbacks*/) override
+ { }
+
+ bool CheckAffinity(const IInvokerPtr& /*invoker*/) const override
+ {
+ return false;
+ }
+
+ bool IsSerialized() const override
+ {
+ // Null invoker never executes any callbacks,
+ // so formally it is serialized.
+ return true;
+ }
+
+ TThreadId GetThreadId() const override
+ {
+ return InvalidThreadId;
+ }
+};
+
+IInvokerPtr GetNullInvoker()
+{
+ return LeakyRefCountedSingleton<TNullInvoker>();
+}
+
+IInvokerPtr GetFinalizerInvoker()
+{
+ return NConcurrency::GetFinalizerInvoker();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void GuardedInvoke(
+ const IInvokerPtr& invoker,
+ TClosure onSuccess,
+ TClosure onCancel)
+{
+ YT_ASSERT(invoker);
+ YT_ASSERT(onSuccess);
+ YT_ASSERT(onCancel);
+
+ class TGuard
+ {
+ public:
+ explicit TGuard(TClosure onCancel)
+ : OnCancel_(std::move(onCancel))
+ { }
+
+ TGuard(TGuard&& other) = default;
+
+ ~TGuard()
+ {
+ if (OnCancel_) {
+ OnCancel_();
+ }
+ }
+
+ void Release()
+ {
+ OnCancel_.Reset();
+ }
+
+ private:
+ TClosure OnCancel_;
+ };
+
+ auto doInvoke = [] (TClosure onSuccess, TGuard guard) {
+ guard.Release();
+ onSuccess();
+ };
+
+ invoker->Invoke(BIND_NO_PROPAGATE(
+ std::move(doInvoke),
+ Passed(std::move(onSuccess)),
+ Passed(TGuard(std::move(onCancel)))));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/invoker_util.h b/yt/yt/core/actions/invoker_util.h
new file mode 100644
index 0000000000..2136aa5bc8
--- /dev/null
+++ b/yt/yt/core/actions/invoker_util.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/public.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the synchronous-ish invoker that defers recurrent action invocation.
+/*!
+ * The invoker's |Invoke| method invokes the closure immediately unless invoking
+ * code is already running within an action in a sync invoker. In latter case all
+ * subsequent actions are put into the deferred action queue and are executed one
+ * by one in synchronous manner after completion of the outermost action.
+ * This is quite similar to BFS over the recursion tree.
+ *
+ * Such implementation ensures that Subscribe chains over futures from sync invoker
+ * do not lead to an unbounded recursion. Note that the invocation order is slightly
+ * different from the "truly synchronous" invoker that always invokes actions synchronously,
+ * which corresponds to DFS over the recursion tree.
+ */
+IInvokerPtr GetSyncInvoker();
+
+//! Returns the null invoker, i.e. the invoker whose |Invoke|
+//! method does nothing.
+IInvokerPtr GetNullInvoker();
+
+//! Returns a special per-process invoker that handles all asynchronous finalization
+//! activities (fiber unwinding, abandoned promise cancelation etc).
+IInvokerPtr GetFinalizerInvoker();
+
+//! Tries to invoke #onSuccess via #invoker.
+//! If the invoker discards the callback without executing it then
+//! #onCancel is run.
+void GuardedInvoke(
+ const IInvokerPtr& invoker,
+ TClosure onSuccess,
+ TClosure onCancel);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/new_with_offloaded_dtor-inl.h b/yt/yt/core/actions/new_with_offloaded_dtor-inl.h
new file mode 100644
index 0000000000..d445c9b39d
--- /dev/null
+++ b/yt/yt/core/actions/new_with_offloaded_dtor-inl.h
@@ -0,0 +1,29 @@
+#ifndef NEW_WITH_OFFLOADED_DTOR_INL_H_
+#error "Direct inclusion of this file is not allowed, include new_with_offloaded_dtor.h"
+// For the sake of sane code completion.
+#include "new_with_offloaded_dtor.h"
+#endif
+
+#include "invoker.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class... TArgs>
+TIntrusivePtr<T> NewWithOffloadedDtor(
+ IInvokerPtr dtorInvoker,
+ TArgs&&... args)
+{
+ return NewWithDeleter<T>(
+ [dtorInvoker = std::move(dtorInvoker)] (T* obj) {
+ dtorInvoker->Invoke(BIND([obj] {
+ NYT::NDetail::DestroyRefCountedImpl<T>(obj);
+ }));
+ },
+ std::forward<TArgs>(args)...);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/new_with_offloaded_dtor.h b/yt/yt/core/actions/new_with_offloaded_dtor.h
new file mode 100644
index 0000000000..404ce5ad3e
--- /dev/null
+++ b/yt/yt/core/actions/new_with_offloaded_dtor.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Instantiates an object whose destruction will be offloaded to a given #invoker.
+template <class T, class... TArgs>
+TIntrusivePtr<T> NewWithOffloadedDtor(
+ IInvokerPtr dtorInvoker,
+ TArgs&&... args);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define NEW_WITH_OFFLOADED_DTOR_INL_H_
+#include "new_with_offloaded_dtor-inl.h"
+#undef NEW_WITH_OFFLOADED_DTOR_PTR_INL_H_
diff --git a/yt/yt/core/actions/public.h b/yt/yt/core/actions/public.h
new file mode 100644
index 0000000000..55b180d488
--- /dev/null
+++ b/yt/yt/core/actions/public.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSignature>
+class TCallback;
+
+typedef TCallback<void()> TClosure;
+
+template <class TSignature>
+class TCallbackList;
+
+template <class T>
+class TFuture;
+
+template <>
+class TFuture<void>;
+
+template <class T>
+class TPromise;
+
+template <>
+class TPromise<void>;
+
+template <class T>
+class TFutureHolder;
+
+DECLARE_REFCOUNTED_STRUCT(IInvoker)
+DECLARE_REFCOUNTED_STRUCT(IPrioritizedInvoker)
+DECLARE_REFCOUNTED_STRUCT(ISuspendableInvoker)
+
+template <class TInvoker>
+class IGenericInvokerPool;
+
+using IInvokerPool = IGenericInvokerPool<IInvoker>;
+using IPrioritizedInvokerPool = IGenericInvokerPool<IPrioritizedInvoker>;
+using ISuspendableInvokerPool = IGenericInvokerPool<ISuspendableInvoker>;
+
+DECLARE_REFCOUNTED_TYPE(IInvokerPool)
+DECLARE_REFCOUNTED_TYPE(IPrioritizedInvokerPool)
+DECLARE_REFCOUNTED_TYPE(ISuspendableInvokerPool)
+DECLARE_REFCOUNTED_CLASS(IDiagnosableInvokerPool)
+
+DECLARE_REFCOUNTED_CLASS(TCancelableContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/signal-inl.h b/yt/yt/core/actions/signal-inl.h
new file mode 100644
index 0000000000..cb87697f0d
--- /dev/null
+++ b/yt/yt/core/actions/signal-inl.h
@@ -0,0 +1,188 @@
+#ifndef SIGNAL_INL_H_
+#error "Direct inclusion of this file is not allowed, include signal.h"
+// For the sake of sane code completion.
+#include "signal.h"
+#endif
+#undef SIGNAL_INL_H_
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResult, class... TArgs>
+void TCallbackList<TResult(TArgs...)>::Subscribe(const TCallback& callback)
+{
+ auto guard = WriterGuard(SpinLock_);
+ Callbacks_.push_back(callback);
+}
+
+template <class TResult, class... TArgs>
+void TCallbackList<TResult(TArgs...)>::Unsubscribe(const TCallback& callback)
+{
+ auto guard = WriterGuard(SpinLock_);
+ for (auto it = Callbacks_.begin(); it != Callbacks_.end(); ++it) {
+ if (*it == callback) {
+ Callbacks_.erase(it);
+ break;
+ }
+ }
+}
+
+template <class TResult, class... TArgs>
+std::vector<TCallback<TResult(TArgs...)>> TCallbackList<TResult(TArgs...)>::ToVector() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return std::vector<TCallback>(Callbacks_.begin(), Callbacks_.end());
+}
+
+template <class TResult, class... TArgs>
+int TCallbackList<TResult(TArgs...)>::Size() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return Callbacks_.size();
+}
+
+template <class TResult, class... TArgs>
+bool TCallbackList<TResult(TArgs...)>::Empty() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return Callbacks_.empty();
+}
+
+template <class TResult, class... TArgs>
+void TCallbackList<TResult(TArgs...)>::Clear()
+{
+ auto guard = WriterGuard(SpinLock_);
+ Callbacks_.clear();
+}
+
+template <class TResult, class... TArgs>
+template <class... TCallArgs>
+void TCallbackList<TResult(TArgs...)>::Fire(TCallArgs&&... args) const
+{
+ TCallbackVector callbacks;
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ callbacks = Callbacks_;
+ }
+
+ for (const auto& callback : callbacks) {
+ callback.Run(std::forward<TCallArgs>(args)...);
+ }
+}
+
+template <class TResult, class... TArgs>
+template <class... TCallArgs>
+void TCallbackList<TResult(TArgs...)>::FireAndClear(TCallArgs&&... args)
+{
+ TCallbackVector callbacks;
+ {
+ auto guard = WriterGuard(SpinLock_);
+ callbacks.swap(Callbacks_);
+ }
+
+ for (const auto& callback : callbacks) {
+ callback(std::forward<TCallArgs>(args)...);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResult, class... TArgs>
+void TSimpleCallbackList<TResult(TArgs...)>::Subscribe(const TCallback& callback)
+{
+ Callbacks_.push_back(callback);
+}
+
+template <class TResult, class... TArgs>
+void TSimpleCallbackList<TResult(TArgs...)>::Unsubscribe(const TCallback& callback)
+{
+ Callbacks_.erase(std::find(Callbacks_.begin(), Callbacks_.end(), callback));
+}
+
+template <class TResult, class... TArgs>
+template <class... TCallArgs>
+void TSimpleCallbackList<TResult(TArgs...)>::Fire(TCallArgs&&... args) const
+{
+ for (const auto& callback : Callbacks_) {
+ callback(std::forward<TCallArgs>(args)...);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResult, class... TArgs>
+void TSingleShotCallbackList<TResult(TArgs...)>::Subscribe(const TCallback& callback)
+{
+ auto guard = WriterGuard(SpinLock_);
+ if (Fired_.load(std::memory_order::acquire)) {
+ guard.Release();
+ std::apply(callback, Args_);
+ return;
+ }
+ Callbacks_.push_back(callback);
+}
+
+template <class TResult, class... TArgs>
+bool TSingleShotCallbackList<TResult(TArgs...)>::TrySubscribe(const TCallback& callback)
+{
+ auto guard = WriterGuard(SpinLock_);
+ if (Fired_.load(std::memory_order::acquire)) {
+ return false;
+ }
+ Callbacks_.push_back(callback);
+ return true;
+}
+
+template <class TResult, class... TArgs>
+void TSingleShotCallbackList<TResult(TArgs...)>::Unsubscribe(const TCallback& callback)
+{
+ auto guard = WriterGuard(SpinLock_);
+ for (auto it = Callbacks_.begin(); it != Callbacks_.end(); ++it) {
+ if (*it == callback) {
+ Callbacks_.erase(it);
+ break;
+ }
+ }
+}
+
+template <class TResult, class... TArgs>
+std::vector<TCallback<TResult(TArgs...)>> TSingleShotCallbackList<TResult(TArgs...)>::ToVector() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return std::vector<TCallback>(Callbacks_.begin(), Callbacks_.end());
+}
+
+template <class TResult, class... TArgs>
+template <class... TCallArgs>
+bool TSingleShotCallbackList<TResult(TArgs...)>::Fire(TCallArgs&&... args)
+{
+ TCallbackVector callbacks;
+ {
+ auto guard = WriterGuard(SpinLock_);
+ if (Fired_.load(std::memory_order::acquire)) {
+ return false;
+ }
+ Args_ = std::make_tuple(std::forward<TCallArgs>(args)...);
+ callbacks.swap(Callbacks_);
+ Fired_.store(true, std::memory_order::release);
+ }
+
+ for (const auto& callback : callbacks) {
+ std::apply(callback, Args_);
+ }
+
+ return true;
+}
+
+template <class TResult, class... TArgs>
+bool TSingleShotCallbackList<TResult(TArgs...)>::IsFired() const
+{
+ return Fired_.load(std::memory_order::acquire);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/actions/signal.h b/yt/yt/core/actions/signal.h
new file mode 100644
index 0000000000..bceefacb55
--- /dev/null
+++ b/yt/yt/core/actions/signal.h
@@ -0,0 +1,272 @@
+#pragma once
+
+#include "callback.h"
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * A client may subscribe to a list (adding a new handler to it),
+ * unsubscribe from it (removing an earlier added handler),
+ * and fire it thus invoking the callbacks added so far.
+ *
+ * Thread affinity: any.
+ */
+template <class TSignature>
+class TCallbackList
+{ };
+
+template <class TResult, class... TArgs>
+class TCallbackList<TResult(TArgs...)>
+{
+public:
+ typedef NYT::TCallback<TResult(TArgs...)> TCallback;
+
+ //! Adds a new handler to the list.
+ /*!
+ * \param callback A handler to be added.
+ */
+ void Subscribe(const TCallback& callback);
+
+ //! Removes a handler from the list.
+ /*!
+ * \param callback A handler to be removed.
+ */
+ void Unsubscribe(const TCallback& callback);
+
+ //! Returns the vector of currently added callbacks.
+ std::vector<TCallback> ToVector() const;
+
+ //! Returns the number of handlers.
+ int Size() const;
+
+ //! Returns |true| if there are no handlers.
+ bool Empty() const;
+
+ //! Clears the list of handlers.
+ void Clear();
+
+ //! Runs all handlers in the list.
+ //! The return values (if any) are ignored.
+ template <class... TCallArgs>
+ void Fire(TCallArgs&&... args) const;
+
+ //! Runs all handlers in the list and clears the list.
+ //! The return values (if any) are ignored.
+ template <class... TCallArgs>
+ void FireAndClear(TCallArgs&&... args);
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ using TCallbackVector = TCompactVector<TCallback, 4>;
+ TCallbackVector Callbacks_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * Similar to TCallbackList, but single-threaded and copyable.
+ *
+ * Cannot be used from multiple threads.
+ */
+template <class TSignature>
+class TSimpleCallbackList
+{ };
+
+template <class TResult, class... TArgs>
+class TSimpleCallbackList<TResult(TArgs...)>
+{
+public:
+ using TCallback = NYT::TCallback<TResult(TArgs...)>;
+
+ //! Adds a new handler to the list.
+ /*!
+ * \param callback A handler to be added.
+ */
+ void Subscribe(const TCallback& callback);
+
+ //! Removes a handler from the list.
+ /*!
+ * \param callback A handler to be removed.
+ */
+ void Unsubscribe(const TCallback& callback);
+
+ //! Runs all handlers in the list.
+ //! The return values (if any) are ignored.
+ template <class... TCallArgs>
+ void Fire(TCallArgs&&... args) const;
+
+private:
+ using TCallbackVector = TCompactVector<TCallback, 4>;
+ TCallbackVector Callbacks_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * Similar to TCallbackList but can only be fired once.
+ * When fired, captures the arguments and in subsequent calls
+ * to Subscribe instantly invokes the subscribers.
+ *
+ * Thread affinity: any.
+ */
+template <class TSignature>
+class TSingleShotCallbackList
+{ };
+
+template <class TResult, class... TArgs>
+class TSingleShotCallbackList<TResult(TArgs...)>
+{
+public:
+ using TCallback = NYT::TCallback<TResult(TArgs...)>;
+
+ //! Adds a new handler to the list.
+ /*!
+ * If the list was already fired then #callback is invoked in situ.
+ * \param callback A handler to be added.
+ */
+ void Subscribe(const TCallback& callback);
+
+ //! Tries to add a new handler to the list.
+ /*!
+ * If the list was already fired then returns |false|.
+ * Otherwise atomically installs the handler.
+ * \param callback A handler to be added.
+ */
+ bool TrySubscribe(const TCallback& callback);
+
+ //! Removes a handler from the list.
+ /*!
+ * \param callback A handler to be removed.
+ */
+ void Unsubscribe(const TCallback& callback);
+
+ //! Returns the vector of currently added callbacks.
+ std::vector<TCallback> ToVector() const;
+
+ //! Runs all handlers in the list.
+ //! The return values (if any) are ignored.
+ /*!
+ * \returns |true| if this is the first attempt to fire the list.
+ */
+ template <class... TCallArgs>
+ bool Fire(TCallArgs&&... args);
+
+ //! \returns |true| if the list was fired.
+ bool IsFired() const;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ std::atomic<bool> Fired_ = false;
+ using TCallbackVector = TCompactVector<TCallback, 4>;
+ TCallbackVector Callbacks_;
+ std::tuple<typename std::decay<TArgs>::type...> Args_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DEFINE_SIGNAL(TSignature, name) \
+protected: \
+ ::NYT::TCallbackList<TSignature> name##_; \
+public: \
+ void Subscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ name##_.Subscribe(callback); \
+ } \
+ \
+ void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ name##_.Unsubscribe(callback); \
+ } \
+ static_assert(true)
+
+#define DEFINE_SIGNAL_SIMPLE(TSignature, name) \
+protected: \
+ ::NYT::TSimpleCallbackList<TSignature> name##_; \
+public: \
+ void Subscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ name##_.Subscribe(callback); \
+ } \
+ \
+ void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ name##_.Unsubscribe(callback); \
+ } \
+ static_assert(true)
+
+#define DEFINE_SIGNAL_WITH_ACCESSOR(TSignature, name) \
+ DEFINE_SIGNAL(TSignature, name); \
+public: \
+ ::NYT::TCallbackList<TSignature>* Get##name##Signal() \
+ { \
+ return &name##_; \
+ } \
+ static_assert(true)
+
+#define DEFINE_SIGNAL_OVERRIDE(TSignature, name) \
+protected: \
+ ::NYT::TCallbackList<TSignature> name##_; \
+public: \
+ virtual void Subscribe##name(const ::NYT::TCallback<TSignature>& callback) override \
+ { \
+ name##_.Subscribe(callback); \
+ } \
+ \
+ virtual void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) override \
+ { \
+ name##_.Unsubscribe(callback); \
+ } \
+ static_assert(true)
+
+#define DECLARE_SIGNAL(TSignature, name) \
+ void Subscribe##name(const ::NYT::TCallback<TSignature>& callback); \
+ void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback)
+
+#define DECLARE_SIGNAL_WITH_ACCESSOR(TSignature, name) \
+ DECLARE_SIGNAL(TSignature, name); \
+ ::NYT::TCallbackList<TSignature>* Get##name##Signal()
+
+#define DECLARE_SIGNAL_OVERRIDE(TSignature, name) \
+ virtual void Subscribe##name(const ::NYT::TCallback<TSignature>& callback) override; \
+ virtual void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) override
+
+#define DECLARE_INTERFACE_SIGNAL(TSignature, name) \
+ virtual void Subscribe##name(const ::NYT::TCallback<TSignature>& callback) = 0; \
+ virtual void Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) = 0
+
+#define DELEGATE_SIGNAL_WITH_RENAME(declaringType, TSignature, name, delegateTo, delegateName) \
+ void declaringType::Subscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ (delegateTo).Subscribe##delegateName(callback); \
+ } \
+ \
+ void declaringType::Unsubscribe##name(const ::NYT::TCallback<TSignature>& callback) \
+ { \
+ (delegateTo).Unsubscribe##delegateName(callback); \
+ } \
+ static_assert(true)
+
+#define DELEGATE_SIGNAL(declaringType, TSignature, name, delegateTo) \
+ DELEGATE_SIGNAL_WITH_RENAME(declaringType, TSignature, name, delegateTo, name)
+
+#define DELEGATE_SIGNAL_WITH_ACCESSOR(declaringType, TSignature, name, delegateTo) \
+ DELEGATE_SIGNAL(declaringType, TSignature, name, delegateTo); \
+ ::NYT::TCallbackList<TSignature>* declaringType::Get##name##Signal() \
+ { \
+ return (delegateTo).Get##name##Signal(); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SIGNAL_INL_H_
+#include "signal-inl.h"
+#undef SIGNAL_INL_H_
diff --git a/yt/yt/core/actions/unittests/actions_ut.cpp b/yt/yt/core/actions/unittests/actions_ut.cpp
new file mode 100644
index 0000000000..6cbc7c8dbe
--- /dev/null
+++ b/yt/yt/core/actions/unittests/actions_ut.cpp
@@ -0,0 +1,98 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TestCancelableRunWithBoundedConcurrency, TestSimple)
+{
+ int x = 0;
+
+ auto future = CancelableRunWithBoundedConcurrency<void>(
+ {
+ BIND([&] {
+ ++x;
+ return VoidFuture;
+ })
+ },
+ /*concurrencyLimit*/ 1);
+ WaitFor(future)
+ .ThrowOnError();
+
+ EXPECT_EQ(x, 1);
+}
+
+TEST(TestCancelableRunWithBoundedConcurrency, TestManyCallbacks)
+{
+ auto threadPool = CreateThreadPool(4, "ThreadPool");
+
+ std::atomic<int> x = 0;
+
+ const int callbackCount = 10000;
+ std::vector<TCallback<TFuture<void>()>> callbacks;
+ callbacks.reserve(callbackCount);
+ for (int i = 0; i < callbackCount; ++i) {
+ callbacks.push_back(BIND([&] {
+ ++x;
+ })
+ .AsyncVia(threadPool->GetInvoker()));
+ }
+
+ auto future = CancelableRunWithBoundedConcurrency(
+ std::move(callbacks),
+ /*concurrencyLimit*/ 10);
+ WaitFor(future)
+ .ThrowOnError();
+
+ EXPECT_EQ(x, callbackCount);
+}
+
+TEST(TestCancelableRunWithBoundedConcurrency, TestCancelation)
+{
+ auto threadPool = CreateThreadPool(4, "ThreadPool");
+
+ std::atomic<int> x = 0;
+ std::atomic<int> canceledCount = 0;
+
+ std::vector<TCallback<TFuture<void>()>> callbacks;
+ for (int i = 0; i < 9; ++i) {
+ callbacks.push_back(BIND([&] {
+ if (x++ < 5) {
+ return VoidFuture;
+ }
+
+ auto promise = NewPromise<void>();
+ promise.OnCanceled(BIND([&, promise] (const TError& /*error*/) {
+ ++canceledCount;
+ }));
+
+ return promise.ToFuture();
+ }));
+ }
+
+ auto future = CancelableRunWithBoundedConcurrency<void>(
+ std::move(callbacks),
+ /*concurrencyLimit*/ 5);
+
+ while (x < 9) {
+ Sleep(TDuration::MilliSeconds(10));
+ }
+
+ future.Cancel(TError("Canceled"));
+
+ EXPECT_EQ(x, 9);
+ EXPECT_EQ(canceledCount, 4);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/actions/unittests/invoker_ut.cpp b/yt/yt/core/actions/unittests/invoker_ut.cpp
new file mode 100644
index 0000000000..be43a0b3d7
--- /dev/null
+++ b/yt/yt/core/actions/unittests/invoker_ut.cpp
@@ -0,0 +1,244 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+#include <yt/yt/library/profiling/public.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+#include <yt/yt/library/profiling/solomon/sensor_dump.pb.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTraverser final
+{
+ int MaxDepth = 0;
+ int CurrentDepth = 0;
+
+ std::vector<int> VisitedNodes;
+
+ bool Binary = false;
+
+ //! If binary is true, Call(x) leads to Call(2 * x) and Call(2 * x + 1);
+ //! otherwise Call(x) leads to Call(x + 1).
+ explicit TTraverser(bool binary)
+ : Binary(binary)
+ { }
+
+ void Call(int node, int limit)
+ {
+ ++CurrentDepth;
+ auto finally = Finally([&] { --CurrentDepth; });
+ if (MaxDepth < CurrentDepth) {
+ MaxDepth = CurrentDepth;
+ }
+
+ if (node > limit) {
+ return;
+ }
+
+ VisitedNodes.push_back(node);
+
+ if (Binary) {
+ GetSyncInvoker()->Invoke(BIND(&TTraverser::Call, MakeStrong(this), 2 * node, limit));
+ GetSyncInvoker()->Invoke(BIND(&TTraverser::Call, MakeStrong(this), 2 * node + 1, limit));
+ } else {
+ GetSyncInvoker()->Invoke(BIND(&TTraverser::Call, MakeStrong(this), node + 1, limit));
+ }
+ }
+};
+
+TEST(TestSyncInvoker, TraverseLinear)
+{
+ auto traverser = New<TTraverser>(/*binary*/ false);
+
+ GetSyncInvoker()->Invoke(BIND(&TTraverser::Call, traverser, 1, 1000));
+ EXPECT_EQ(1, traverser->MaxDepth);
+
+ std::vector<int> expectedNodes(1000);
+ std::iota(expectedNodes.begin(), expectedNodes.end(), 1);
+ EXPECT_EQ(expectedNodes, traverser->VisitedNodes);
+}
+
+TEST(TestSyncInvoker, TraverseBinary)
+{
+ auto traverser = New<TTraverser>(/*binary*/ true);
+
+ GetSyncInvoker()->Invoke(BIND(&TTraverser::Call, traverser, 1, 1000));
+ EXPECT_EQ(1, traverser->MaxDepth);
+
+ std::vector<int> expectedNodes(1000);
+ std::iota(expectedNodes.begin(), expectedNodes.end(), 1);
+ EXPECT_EQ(expectedNodes, traverser->VisitedNodes);
+}
+
+TEST(TestSyncInvoker, SleepyFiber)
+{
+ // This test ensures that deferred callbacks in the sync invoker
+ // are tracked per-fiber, which allows them to sleep without
+ // breaking the synchronous manner of execution for other fibers.
+
+ auto queue = New<TActionQueue>("Test");
+ auto invoker = queue->GetInvoker();
+
+ auto completionFlag = NewPromise<void>();
+
+ std::vector<TString> events;
+
+ auto actionA = [&] {
+ events.push_back("A started");
+
+ auto actionB = [&] {
+ events.push_back("B started");
+ events.push_back("B out");
+ WaitFor(completionFlag.ToFuture())
+ .ThrowOnError();
+ events.push_back("B in");
+ events.push_back("B finished");
+ };
+
+ GetSyncInvoker()->Invoke(BIND(actionB));
+
+ events.push_back("A finished");
+ };
+
+ auto actionC = [&] {
+ events.push_back("C started");
+
+ auto actionD = [&] {
+ events.push_back("D started");
+ completionFlag.Set();
+ events.push_back("D finished");
+ };
+
+ GetSyncInvoker()->Invoke(BIND(actionD));
+
+ events.push_back("C finished");
+ };
+
+ auto asyncA = BIND(actionA)
+ .AsyncVia(invoker)
+ .Run();
+ auto asyncC = BIND(actionC)
+ .AsyncVia(invoker)
+ .Run();
+
+ AllSucceeded(std::vector<TFuture<void>>{asyncA, asyncC})
+ .Get()
+ .ThrowOnError();
+
+ std::vector<TString> expectedEvents{
+ "A started", "B started", "B out", "C started", "D started", "D finished", "C finished", "B in", "B finished", "A finished",
+ };
+
+ EXPECT_EQ(expectedEvents, events);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the aggregated summary of all duration-like time series with sensor name #sensorName within #sensorDump.
+const NProfiling::NProto::TSummaryDuration& GetSummaryDuration(
+ const NProfiling::NProto::TSensorDump& sensorDump,
+ const TString& sensorName)
+{
+ for (const auto& cube : sensorDump.cubes()) {
+ if (cube.name() == sensorName) {
+ for (const auto& projection : cube.projections()) {
+ if (projection.has_value() && projection.has_timer() && projection.tag_ids_size() == 0) {
+ return projection.timer();
+ }
+ }
+ }
+ }
+
+ static NProfiling::NProto::TSummaryDuration empty;
+ return empty;
+}
+
+TEST(TestInvokersWaitTime, SerializedInvoker)
+{
+ auto registry = New<NProfiling::TSolomonRegistry>();
+ registry->SetWindowSize(5);
+
+ auto threadPool = CreateThreadPool(2, "Test");
+ auto serializedInvoker = CreateSerializedInvoker(threadPool->GetInvoker(), "test", registry);
+
+ const auto actionWaitTimeMcs = 2'000'000ll;
+
+ auto action = [] {
+ Sleep(TDuration::MicroSeconds(actionWaitTimeMcs));
+ };
+
+ auto async1 = BIND(action)
+ .AsyncVia(serializedInvoker).Run();
+
+ auto async2 = BIND(action)
+ .AsyncVia(serializedInvoker).Run();
+
+ auto async3 = BIND(action)
+ .AsyncVia(serializedInvoker).Run();
+
+ AllSucceeded(std::vector<TFuture<void>>{async1, async2, async3})
+ .Get()
+ .ThrowOnError();
+
+ registry->ProcessRegistrations();
+ registry->Collect();
+
+ auto sensorDump = registry->DumpSensors();
+ const auto& summary = GetSummaryDuration(sensorDump, "yt/invoker/serialized/wait");
+
+ // The action has been executed 3 times.
+ EXPECT_EQ(summary.count(), 3);
+ // The first action had to wait for 0 seconds, the second for 2 seconds, the third for 4 seconds.
+ EXPECT_NEAR(summary.sum(), 0 * actionWaitTimeMcs + 1 * actionWaitTimeMcs + 2 * actionWaitTimeMcs, 100'000ll);
+ EXPECT_NEAR(summary.max(), 2 * actionWaitTimeMcs, 100'000ll);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TestInvokersWaitTime, PrioritizedInvoker)
+{
+ auto registry = New<NProfiling::TSolomonRegistry>();
+ registry->SetWindowSize(5);
+
+ auto threadPool = CreateThreadPool(2, "Test");
+ auto invoker = CreatePrioritizedInvoker(threadPool->GetInvoker(), "test", registry);
+
+ const auto actionWaitTimeMcs = 2'000'000ll;
+
+ auto action = [] {
+ Sleep(TDuration::MicroSeconds(actionWaitTimeMcs));
+ };
+
+ auto async = BIND(action)
+ .AsyncVia(invoker).Run();
+
+ AllSucceeded(std::vector<TFuture<void>>{async})
+ .Get()
+ .ThrowOnError();
+
+ registry->ProcessRegistrations();
+ registry->Collect();
+
+ auto sensorDump = registry->DumpSensors();
+ const auto& summary = GetSummaryDuration(sensorDump, "yt/invoker/prioritized/wait");
+
+ // The action has been executed 1 time.
+ EXPECT_EQ(summary.count(), 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/actions/unittests/new_with_offloaded_dtor_ut.cpp b/yt/yt/core/actions/unittests/new_with_offloaded_dtor_ut.cpp
new file mode 100644
index 0000000000..5c9a9ffab9
--- /dev/null
+++ b/yt/yt/core/actions/unittests/new_with_offloaded_dtor_ut.cpp
@@ -0,0 +1,68 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/new_with_offloaded_dtor.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/core/misc/ref_counted_tracker.h>
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const NLogging::TLogger Logger("Test");
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOffloadedDtorObject
+ : public TRefCounted
+{
+public:
+ explicit TOffloadedDtorObject(TThreadId dtorThreadId)
+ : DtorThreadId_(dtorThreadId)
+ {
+ YT_LOG_INFO("TOffloadedDtorObject::TOffloadedDtorObject()");
+ }
+
+ ~TOffloadedDtorObject()
+ {
+ YT_LOG_INFO("TOffloadedDtorObject::~TOffloadedDtorObject()");
+ EXPECT_EQ(GetCurrentThreadId(), DtorThreadId_);
+ }
+
+private:
+ const TThreadId DtorThreadId_;
+};
+
+TEST(TNewWithOffloadedDtorTest, OffloadDtor)
+{
+ auto dtorQueue = New<TActionQueue>("Offload");
+ auto dtorInvoker = dtorQueue->GetInvoker();
+ auto dtorThreadId = dtorInvoker->GetThreadId();
+ EXPECT_NE(dtorThreadId, InvalidThreadId);
+
+ auto typeKey = GetRefCountedTypeKey<TOffloadedDtorObject>();
+ EXPECT_EQ(TRefCountedTracker::Get()->GetObjectsAllocated(typeKey), 0u);
+ EXPECT_EQ(TRefCountedTracker::Get()->GetObjectsAlive(typeKey), 0u);
+
+ auto obj = NewWithOffloadedDtor<TOffloadedDtorObject>(dtorInvoker, dtorThreadId);
+ EXPECT_EQ(TRefCountedTracker::Get()->GetObjectsAllocated(typeKey), 1u);
+ EXPECT_EQ(TRefCountedTracker::Get()->GetObjectsAlive(typeKey), 1u);
+
+ obj.Reset();
+ WaitForPredicate([=] {
+ return TRefCountedTracker::Get()->GetObjectsAlive(typeKey) == 0u;
+ });
+
+ dtorQueue->Shutdown(/*graceful*/ true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/actions/unittests/ya.make b/yt/yt/core/actions/unittests/ya.make
new file mode 100644
index 0000000000..7c46294142
--- /dev/null
+++ b/yt/yt/core/actions/unittests/ya.make
@@ -0,0 +1,45 @@
+GTEST(unittester-core-actions)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ actions_ut.cpp
+ invoker_ut.cpp
+ new_with_offloaded_dtor_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+ library/cpp/testing/common
+ yt/yt/library/profiling
+ yt/yt/library/profiling/solomon
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/bus/bus.h b/yt/yt/core/bus/bus.h
new file mode 100644
index 0000000000..18185be42f
--- /dev/null
+++ b/yt/yt/core/bus/bus.h
@@ -0,0 +1,169 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/signal.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ITERATE_BUS_NETWORK_STATISTICS_COUNTER_FIELDS(func) \
+ func(InBytes, in_bytes) \
+ func(InPackets, in_packets) \
+ \
+ func(OutBytes, out_bytes) \
+ func(OutPackets, out_packets) \
+ \
+ func(StalledReads, stalled_reads) \
+ func(StalledWrites, stalled_writes) \
+ \
+ func(ReadErrors, read_errors) \
+ func(WriteErrors, write_errors) \
+ \
+ func(Retransmits, retransmits) \
+ \
+ func(EncoderErrors, encoder_errors) \
+ func(DecoderErrors, decoder_errors)
+
+#define ITERATE_BUS_NETWORK_STATISTICS_GAUGE_FIELDS(func) \
+ func(PendingOutPackets, pending_out_packets) \
+ func(PendingOutBytes, pending_out_bytes) \
+ \
+ func(ClientConnections, client_connections) \
+ func(ServerConnections, server_connections)
+
+#define ITERATE_BUS_NETWORK_STATISTICS_FIELDS(func) \
+ ITERATE_BUS_NETWORK_STATISTICS_COUNTER_FIELDS(func) \
+ ITERATE_BUS_NETWORK_STATISTICS_GAUGE_FIELDS(func)
+
+struct TBusNetworkStatistics
+{
+ #define XX(camelCaseField, snakeCaseField) i64 camelCaseField = 0;
+ ITERATE_BUS_NETWORK_STATISTICS_FIELDS(XX)
+ #undef XX
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSendOptions
+{
+ static constexpr int AllParts = -1;
+
+ explicit TSendOptions(
+ EDeliveryTrackingLevel trackingLevel = EDeliveryTrackingLevel::None,
+ int checksummedPartCount = AllParts)
+ : TrackingLevel(trackingLevel)
+ , ChecksummedPartCount(checksummedPartCount)
+ { }
+
+ EDeliveryTrackingLevel TrackingLevel;
+ int ChecksummedPartCount;
+ bool EnableSendCancelation = false;
+};
+
+//! A bus, i.e. something capable of transmitting messages.
+struct IBus
+ : public virtual TRefCounted
+{
+ //! Returns a textual representation of the bus' endpoint.
+ //! Typically used for logging.
+ virtual const TString& GetEndpointDescription() const = 0;
+
+ //! Returns the bus' endpoint attributes.
+ //! Typically used for constructing errors.
+ virtual const NYTree::IAttributeDictionary& GetEndpointAttributes() const = 0;
+
+ //! Returns the bus' endpoint address as it was provided by the configuration (e.g.: non-resolved FQDN).
+ //! Empty if it is not supported by the implementation (e.g.: Unix sockets).
+ virtual const TString& GetEndpointAddress() const = 0;
+
+ //! Returns the bus' endpoint network address (e.g. a resolved IP address).
+ //! Null if it is not supported by the implementation (e.g. for a client-side bus).
+ virtual const NNet::TNetworkAddress& GetEndpointNetworkAddress() const = 0;
+
+ //! Returns the statistics for the whole network this bus is bound to.
+ virtual TBusNetworkStatistics GetNetworkStatistics() const = 0;
+
+ //! Returns true if the bus' endpoint belongs to our process.
+ virtual bool IsEndpointLocal() const = 0;
+
+ //! Returns a future indicating the moment when the bus is actually ready to send messages.
+ /*!
+ * Some bus implementations are not immediately ready upon creation. E.g. a client socket
+ * needs to perform a DNS resolve for its underlying address and establish a connection
+ * before messages can actually go through. #IBus::Send can still be invoked before
+ * these background activities are finished but for the sake of diagnostics it is sometimes
+ * beneficial to catch the exact moment the connection becomes ready.
+ */
+ virtual TFuture<void> GetReadyFuture() const = 0;
+
+ //! Asynchronously sends a message via the bus.
+ /*!
+ * \param message A message to send.
+ * \return An asynchronous flag indicating if the delivery (not the processing!) of the message
+ * was successful.
+ *
+ * Underlying transport may support delivery cancellation. In that case, when returned future is cancelled,
+ * message is dropped from the send queue.
+ *
+ * \note Thread affinity: any
+ */
+ virtual TFuture<void> Send(TSharedRefArray message, const TSendOptions& options) = 0;
+
+ //! For socket buses, updates the TOS level.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual void SetTosLevel(TTosLevel tosLevel) = 0;
+
+ //! Terminates the bus.
+ /*!
+ * Does not block -- termination typically happens in background.
+ * It is safe to call this method multiple times.
+ * On terminated the instance is no longer usable.
+
+ * \note Thread affinity: any.
+ */
+ virtual void Terminate(const TError& error) = 0;
+
+ //! Invoked upon bus termination
+ //! (either due to call to #Terminate or other party's failure).
+ DECLARE_INTERFACE_SIGNAL(void(const TError&), Terminated);
+};
+
+DEFINE_REFCOUNTED_TYPE(IBus)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Handles incoming bus messages.
+struct IMessageHandler
+ : public virtual TRefCounted
+{
+ //! Raised whenever a new message arrives via the bus.
+ /*!
+ * \param message The just arrived message.
+ * \param replyBus A bus that can be used for replying back.
+ *
+ * \note
+ * Thread affinity: this method is called from an unspecified thread
+ * and must return ASAP. No context switch or fiber cancelation is possible.
+ *
+ */
+ virtual void HandleMessage(TSharedRefArray message, IBusPtr replyBus) noexcept = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IMessageHandler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/client.h b/yt/yt/core/bus/client.h
new file mode 100644
index 0000000000..ea91efd822
--- /dev/null
+++ b/yt/yt/core/bus/client.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCreateBusOptions
+{
+ EMultiplexingBand MultiplexingBand = EMultiplexingBand::Default;
+};
+
+//! A factory for creating client IBus-es.
+/*!
+ * Thread affinity: any.
+ */
+struct IBusClient
+ : public virtual TRefCounted
+{
+ //! Returns a textual representation of the bus' endpoint.
+ //! Typically used for logging.
+ virtual const TString& GetEndpointDescription() const = 0;
+
+ //! Returns the bus' endpoint attributes.
+ //! Typically used for constructing errors.
+ virtual const NYTree::IAttributeDictionary& GetEndpointAttributes() const = 0;
+
+ //! Creates a new bus.
+ /*!
+ * The bus will point to the address supplied during construction.
+ *
+ * \param handler A handler that will process incoming messages.
+ * \return A new bus.
+ *
+ */
+ virtual IBusPtr CreateBus(
+ IMessageHandlerPtr handler,
+ const TCreateBusOptions& options = {}) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IBusClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/private.h b/yt/yt/core/bus/private.h
new file mode 100644
index 0000000000..9619611995
--- /dev/null
+++ b/yt/yt/core/bus/private.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger BusLogger("Bus");
+inline const NProfiling::TProfiler BusProfiler("/bus");
+
+using TConnectionId = TGuid;
+using TPacketId = TGuid;
+
+DECLARE_REFCOUNTED_CLASS(TTcpConnection)
+
+DEFINE_ENUM(EConnectionType,
+ (Client)
+ (Server)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/public.cpp b/yt/yt/core/bus/public.cpp
new file mode 100644
index 0000000000..6cff5f25eb
--- /dev/null
+++ b/yt/yt/core/bus/public.cpp
@@ -0,0 +1,13 @@
+#include "public.h"
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString DefaultNetworkName("default");
+const TString LocalNetworkName("local");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/public.h b/yt/yt/core/bus/public.h
new file mode 100644
index 0000000000..62ef29c279
--- /dev/null
+++ b/yt/yt/core/bus/public.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <util/generic/size_literals.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(IBus)
+DECLARE_REFCOUNTED_STRUCT(IMessageHandler)
+DECLARE_REFCOUNTED_STRUCT(IBusClient)
+DECLARE_REFCOUNTED_STRUCT(IBusServer)
+
+struct TBusNetworkStatistics;
+
+using TTosLevel = int;
+constexpr int DefaultTosLevel = 0;
+constexpr int BlackHoleTosLevel = -1;
+
+constexpr size_t MaxMessagePartCount = 1 << 28;
+constexpr size_t MaxMessagePartSize = 1_GB;
+
+DEFINE_ENUM(EDeliveryTrackingLevel,
+ (None)
+ (ErrorOnly)
+ (Full)
+);
+
+DEFINE_ENUM(EMultiplexingBand,
+ ((Default) (0))
+ ((Control) (1))
+ ((Heavy) (2))
+ ((Interactive) (3))
+ ((RealTime) (4))
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((TransportError) (100))
+);
+
+extern const TString DefaultNetworkName;
+extern const TString LocalNetworkName;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/server.h b/yt/yt/core/bus/server.h
new file mode 100644
index 0000000000..dd7a35bc13
--- /dev/null
+++ b/yt/yt/core/bus/server.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A server-side bus listener.
+/*!
+ * An instance on this interface listens for incoming
+ * messages and notifies IMessageHandlerPtr.
+ */
+struct IBusServer
+ : public virtual TRefCounted
+{
+ //! Synchronously starts the listener.
+ /*
+ * \param handler Incoming messages handler.
+ */
+ virtual void Start(IMessageHandlerPtr handler) = 0;
+
+ //! Asynchronously stops the listener.
+ /*!
+ * After this call the instance is no longer usable.
+ * No new incoming messages are accepted.
+ *
+ * \returns the future indicating the moment when the server is fully stopped;
+ * e.g. the server socket is closed.
+ */
+ virtual TFuture<void> Stop() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IBusServer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/client.cpp b/yt/yt/core/bus/tcp/client.cpp
new file mode 100644
index 0000000000..33bdc13e5d
--- /dev/null
+++ b/yt/yt/core/bus/tcp/client.cpp
@@ -0,0 +1,222 @@
+#include "client.h"
+#include "private.h"
+#include "client.h"
+#include "config.h"
+#include "connection.h"
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <errno.h>
+
+#ifdef _unix_
+ #include <netinet/tcp.h>
+ #include <sys/socket.h>
+#endif
+
+namespace NYT::NBus {
+
+using namespace NNet;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = BusLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A lightweight proxy controlling the lifetime of client #TTcpConnection.
+/*!
+ * When the last strong reference vanishes, it calls IBus::Terminate
+ * for the underlying connection.
+ */
+class TTcpClientBusProxy
+ : public IBus
+{
+public:
+ explicit TTcpClientBusProxy(TTcpConnectionPtr connection)
+ : Connection_(std::move(connection))
+ { }
+
+ ~TTcpClientBusProxy()
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ Connection_->Terminate(TError(NBus::EErrorCode::TransportError, "Bus terminated"));
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetEndpointDescription();
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetEndpointAttributes();
+ }
+
+ const TString& GetEndpointAddress() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetEndpointAddress();
+ }
+
+ const NNet::TNetworkAddress& GetEndpointNetworkAddress() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetEndpointNetworkAddress();
+ }
+
+ bool IsEndpointLocal() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->IsEndpointLocal();
+ }
+
+ TBusNetworkStatistics GetNetworkStatistics() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetNetworkStatistics();
+ }
+
+ TFuture<void> GetReadyFuture() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->GetReadyFuture();
+ }
+
+ TFuture<void> Send(TSharedRefArray message, const TSendOptions& options) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->Send(std::move(message), options);
+ }
+
+ void SetTosLevel(TTosLevel tosLevel) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Connection_->SetTosLevel(tosLevel);
+ }
+
+ void Terminate(const TError& error) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ Connection_->Terminate(error);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ Connection_->SubscribeTerminated(callback);
+ }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ Connection_->UnsubscribeTerminated(callback);
+ }
+
+private:
+ const TTcpConnectionPtr Connection_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpBusClient
+ : public IBusClient
+{
+public:
+ TTcpBusClient(
+ TBusClientConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+ : Config_(std::move(config))
+ , PacketTranscoderFactory_(packetTranscoderFactory)
+ {
+ if (Config_->Address) {
+ EndpointDescription_ = *Config_->Address;
+ } else if (Config_->UnixDomainSocketPath) {
+ EndpointDescription_ = Format("unix://%v", *Config_->UnixDomainSocketPath);
+ }
+
+ EndpointAttributes_ = ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("address").Value(EndpointDescription_)
+ .EndMap());
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription_;
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ IBusPtr CreateBus(IMessageHandlerPtr handler, const TCreateBusOptions& options) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto id = TConnectionId::Create();
+
+ YT_LOG_DEBUG("Connecting to server (Address: %v, ConnectionId: %v, MultiplexingBand: %v)",
+ EndpointDescription_,
+ id,
+ options.MultiplexingBand);
+
+ auto endpointAttributes = ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Items(*EndpointAttributes_)
+ .Item("connection_id").Value(id)
+ .EndMap());
+
+ auto poller = TTcpDispatcher::TImpl::Get()->GetXferPoller();
+
+ auto connection = New<TTcpConnection>(
+ Config_,
+ EConnectionType::Client,
+ id,
+ INVALID_SOCKET,
+ options.MultiplexingBand,
+ EndpointDescription_,
+ *endpointAttributes,
+ TNetworkAddress(),
+ Config_->Address,
+ Config_->UnixDomainSocketPath,
+ std::move(handler),
+ std::move(poller),
+ PacketTranscoderFactory_);
+ connection->Start();
+
+ return New<TTcpClientBusProxy>(std::move(connection));
+ }
+
+private:
+ const TBusClientConfigPtr Config_;
+
+ IPacketTranscoderFactory* const PacketTranscoderFactory_;
+
+ TString EndpointDescription_;
+ IAttributeDictionaryPtr EndpointAttributes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IBusClientPtr CreateBusClient(
+ TBusClientConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+{
+ return New<TTcpBusClient>(std::move(config), packetTranscoderFactory);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/client.h b/yt/yt/core/bus/tcp/client.h
new file mode 100644
index 0000000000..5b38dd1d51
--- /dev/null
+++ b/yt/yt/core/bus/tcp/client.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include "packet.h"
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Initializes a new client for communicating with a given address.
+IBusClientPtr CreateBusClient(
+ TBusClientConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory = GetYTPacketTranscoderFactory());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/config.cpp b/yt/yt/core/bus/tcp/config.cpp
new file mode 100644
index 0000000000..6ce170526a
--- /dev/null
+++ b/yt/yt/core/bus/tcp/config.cpp
@@ -0,0 +1,146 @@
+#include "config.h"
+
+#include <yt/yt/core/net/address.h>
+
+namespace NYT::NBus {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TMultiplexingBandConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("tos_level", &TThis::TosLevel)
+ .Default(NYT::NBus::DefaultTosLevel);
+
+ registrar.Parameter("network_to_tos_level", &TThis::NetworkToTosLevel)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTcpDispatcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("thread_pool_size", &TThis::ThreadPoolSize)
+ .Default(8);
+
+ registrar.Parameter("network_bandwidth", &TThis::NetworkBandwidth)
+ .Default();
+
+ registrar.Parameter("networks", &TThis::Networks)
+ .Default();
+
+ registrar.Parameter("multiplexing_bands", &TThis::MultiplexingBands)
+ .Default();
+}
+
+TTcpDispatcherConfigPtr TTcpDispatcherConfig::ApplyDynamic(
+ const TTcpDispatcherDynamicConfigPtr& dynamicConfig) const
+{
+ auto mergedConfig = CloneYsonStruct(MakeStrong(this));
+ UpdateYsonStructField(mergedConfig->ThreadPoolSize, dynamicConfig->ThreadPoolSize);
+ UpdateYsonStructField(mergedConfig->Networks, dynamicConfig->Networks);
+ UpdateYsonStructField(mergedConfig->MultiplexingBands, dynamicConfig->MultiplexingBands);
+ mergedConfig->Postprocess();
+ return mergedConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTcpDispatcherDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("thread_pool_size", &TThis::ThreadPoolSize)
+ .Optional()
+ .GreaterThan(0);
+
+ registrar.Parameter("network_bandwidth", &TThis::NetworkBandwidth)
+ .Default();
+
+ registrar.Parameter("networks", &TThis::Networks)
+ .Default();
+
+ registrar.Parameter("multiplexing_bands", &TThis::MultiplexingBands)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBusServerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("port", &TThis::Port)
+ .Default();
+ registrar.Parameter("unix_domain_socket_path", &TThis::UnixDomainSocketPath)
+ .Default();
+ registrar.Parameter("max_backlog_size", &TThis::MaxBacklogSize)
+ .Default(8192);
+ registrar.Parameter("max_simultaneous_connections", &TThis::MaxSimultaneousConnections)
+ .Default(50000);
+}
+
+TBusServerConfigPtr TBusServerConfig::CreateTcp(int port)
+{
+ auto config = New<TBusServerConfig>();
+ config->Port = port;
+ return config;
+}
+
+TBusServerConfigPtr TBusServerConfig::CreateUds(const TString& socketPath)
+{
+ auto config = New<TBusServerConfig>();
+ config->UnixDomainSocketPath = socketPath;
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBusConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_quick_ack", &TThis::EnableQuickAck)
+ .Default(true);
+ registrar.Parameter("bind_retry_count", &TThis::BindRetryCount)
+ .Default(5);
+ registrar.Parameter("bind_retry_backoff", &TThis::BindRetryBackoff)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("read_stall_timeout", &TThis::ReadStallTimeout)
+ .Default(TDuration::Minutes(1));
+ registrar.Parameter("write_stall_timeout", &TThis::WriteStallTimeout)
+ .Default(TDuration::Minutes(1));
+ registrar.Parameter("verify_checksums", &TThis::VerifyChecksums)
+ .Default(true);
+ registrar.Parameter("generate_checksums", &TThis::GenerateChecksums)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBusClientConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("address", &TThis::Address)
+ .Default();
+ registrar.Parameter("unix_domain_socket_path", &TThis::UnixDomainSocketPath)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (!config->Address && !config->UnixDomainSocketPath) {
+ THROW_ERROR_EXCEPTION("\"address\" and \"unix_domain_socket_path\" cannot be both missing");
+ }
+ });
+}
+
+TBusClientConfigPtr TBusClientConfig::CreateTcp(const TString& address)
+{
+ auto config = New<TBusClientConfig>();
+ config->Address = address;
+ return config;
+}
+
+TBusClientConfigPtr TBusClientConfig::CreateUds(const TString& socketPath)
+{
+ auto config = New<TBusClientConfig>();
+ config->UnixDomainSocketPath = socketPath;
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/config.h b/yt/yt/core/bus/tcp/config.h
new file mode 100644
index 0000000000..e2a1ec47f6
--- /dev/null
+++ b/yt/yt/core/bus/tcp/config.h
@@ -0,0 +1,140 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMultiplexingBandConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ int TosLevel;
+ THashMap<TString, int> NetworkToTosLevel;
+
+ REGISTER_YSON_STRUCT(TMultiplexingBandConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TMultiplexingBandConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpDispatcherConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ int ThreadPoolSize;
+
+ //! Used for profiling export and alerts.
+ std::optional<i64> NetworkBandwidth;
+
+ THashMap<TString, std::vector<NNet::TIP6Network>> Networks;
+
+ TEnumIndexedVector<EMultiplexingBand, TMultiplexingBandConfigPtr> MultiplexingBands;
+
+ TTcpDispatcherConfigPtr ApplyDynamic(const TTcpDispatcherDynamicConfigPtr& dynamicConfig) const;
+
+ REGISTER_YSON_STRUCT(TTcpDispatcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTcpDispatcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpDispatcherDynamicConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<int> ThreadPoolSize;
+
+ std::optional<i64> NetworkBandwidth;
+
+ std::optional<THashMap<TString, std::vector<NNet::TIP6Network>>> Networks;
+
+ std::optional<TEnumIndexedVector<EMultiplexingBand, TMultiplexingBandConfigPtr>> MultiplexingBands;
+
+ REGISTER_YSON_STRUCT(TTcpDispatcherDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTcpDispatcherDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusConfig
+ : public NNet::TDialerConfig
+{
+public:
+ bool EnableQuickAck;
+
+ int BindRetryCount;
+ TDuration BindRetryBackoff;
+
+ TDuration ReadStallTimeout;
+ TDuration WriteStallTimeout;
+
+ bool VerifyChecksums;
+ bool GenerateChecksums;
+
+ REGISTER_YSON_STRUCT(TBusConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBusConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusServerConfig
+ : public TBusConfig
+{
+public:
+ std::optional<int> Port;
+ std::optional<TString> UnixDomainSocketPath;
+ int MaxBacklogSize;
+ int MaxSimultaneousConnections;
+
+ static TBusServerConfigPtr CreateTcp(int port);
+ static TBusServerConfigPtr CreateUds(const TString& socketPath);
+
+ REGISTER_YSON_STRUCT(TBusServerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBusServerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusClientConfig
+ : public TBusConfig
+{
+public:
+ std::optional<TString> Address;
+ std::optional<TString> UnixDomainSocketPath;
+
+ static TBusClientConfigPtr CreateTcp(const TString& address);
+ static TBusClientConfigPtr CreateUds(const TString& socketPath);
+
+ REGISTER_YSON_STRUCT(TBusClientConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBusClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/tcp/connection.cpp b/yt/yt/core/bus/tcp/connection.cpp
new file mode 100644
index 0000000000..84dcfda62c
--- /dev/null
+++ b/yt/yt/core/bus/tcp/connection.cpp
@@ -0,0 +1,1596 @@
+#include "connection.h"
+
+#include "config.h"
+#include "server.h"
+#include "dispatcher_impl.h"
+
+#include <yt/yt/core/misc/fs.h>
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/net/socket.h>
+#include <yt/yt/core/net/dialer.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <util/system/error.h>
+#include <util/system/guard.h>
+
+#ifdef __linux__
+#include <netinet/tcp.h>
+#endif
+
+#include <cerrno>
+
+namespace NYT::NBus {
+
+using namespace NConcurrency;
+using namespace NFS;
+using namespace NNet;
+using namespace NYTree;
+using namespace NYson;
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr size_t ReadBufferSize = 16_KB;
+static constexpr size_t MaxBatchReadSize = 64_KB;
+static constexpr auto ReadTimeWarningThreshold = TDuration::MilliSeconds(100);
+
+static constexpr size_t MaxFragmentsPerWrite = 256;
+static constexpr size_t WriteBufferSize = 16_KB;
+static constexpr size_t MaxBatchWriteSize = 64_KB;
+static constexpr size_t MaxWriteCoalesceSize = 1_KB;
+static constexpr auto WriteTimeWarningThreshold = TDuration::MilliSeconds(100);
+
+static constexpr auto HandshakePacketId = TPacketId(1, 0, 0, 0);
+
+static constexpr i64 PendingOutBytesFlushThreshold = 1_MBs;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTcpConnectionReadBufferTag { };
+struct TTcpConnectionWriteBufferTag { };
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TTcpConnection::TPacket::MarkEncoded()
+{
+ auto expected = EPacketState::Queued;
+ return State.compare_exchange_strong(expected, EPacketState::Encoded);
+}
+
+void TTcpConnection::TPacket::OnCancel(const TError& /* error */)
+{
+ auto expected = EPacketState::Queued;
+ if (!State.compare_exchange_strong(expected, EPacketState::Canceled)) {
+ return;
+ }
+
+ Message.Reset();
+ if (Connection) {
+ Connection->DecrementPendingOut(PacketSize);
+ }
+}
+
+void TTcpConnection::TPacket::EnableCancel(TTcpConnectionPtr connection)
+{
+ Connection = std::move(connection);
+ if (!Promise.OnCanceled(BIND(&TPacket::OnCancel, MakeWeak(this)))) {
+ OnCancel(TError());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTcpConnection::TTcpConnection(
+ TBusConfigPtr config,
+ EConnectionType connectionType,
+ TConnectionId id,
+ SOCKET socket,
+ EMultiplexingBand multiplexingBand,
+ const TString& endpointDescription,
+ const IAttributeDictionary& endpointAttributes,
+ const TNetworkAddress& endpointNetworkAddress,
+ const std::optional<TString>& endpointAddress,
+ const std::optional<TString>& unixDomainSocketPath,
+ IMessageHandlerPtr handler,
+ IPollerPtr poller,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+ : Config_(std::move(config))
+ , ConnectionType_(connectionType)
+ , Id_(id)
+ , EndpointDescription_(endpointDescription)
+ , EndpointAttributes_(endpointAttributes.Clone())
+ , EndpointNetworkAddress_(endpointNetworkAddress)
+ , EndpointAddress_(endpointAddress)
+ , UnixDomainSocketPath_(unixDomainSocketPath)
+ , Handler_(std::move(handler))
+ , Poller_(std::move(poller))
+ , Logger(BusLogger.WithTag("ConnectionId: %v, RemoteAddress: %v",
+ Id_,
+ EndpointDescription_))
+ , LoggingTag_(Format("ConnectionId: %v", Id_))
+ , GenerateChecksums_(Config_->GenerateChecksums)
+ , Socket_(socket)
+ , MultiplexingBand_(multiplexingBand)
+ , Decoder_(packetTranscoderFactory->CreateDecoder(Logger, Config_->VerifyChecksums))
+ , ReadStallTimeout_(NProfiling::DurationToCpuDuration(Config_->ReadStallTimeout))
+ , Encoder_(packetTranscoderFactory->CreateEncoder(Logger))
+ , WriteStallTimeout_(NProfiling::DurationToCpuDuration(Config_->WriteStallTimeout))
+ , SupportsHandshakes_(packetTranscoderFactory->SupportsHandshakes())
+{ }
+
+TTcpConnection::~TTcpConnection()
+{
+ Close();
+}
+
+void TTcpConnection::Close()
+{
+ {
+ auto guard = Guard(Lock_);
+
+ if (Error_.Load().IsOK()) {
+ Error_.Store(TError(NBus::EErrorCode::TransportError, "Bus terminated")
+ << *EndpointAttributes_);
+ }
+
+ if (State_ == EState::Open) {
+ UpdateConnectionCount(-1);
+ }
+
+ UpdateTcpStatistics();
+
+ UnarmPoller();
+
+ CloseSocket();
+
+ State_ = EState::Closed;
+ PendingControl_.store(static_cast<ui64>(EPollControl::Offline));
+ }
+
+ DiscardOutcomingMessages();
+ DiscardUnackedMessages();
+
+ while (!QueuedPackets_.empty()) {
+ const auto& packet = QueuedPackets_.front();
+ if (packet->Connection) {
+ packet->OnCancel(TError());
+ } else {
+ DecrementPendingOut(packet->PacketSize);
+ }
+ QueuedPackets_.pop();
+ }
+
+ while (!EncodedPackets_.empty()) {
+ const auto& packet = EncodedPackets_.front();
+ DecrementPendingOut(packet->PacketSize);
+ EncodedPackets_.pop();
+ }
+
+ EncodedFragments_.clear();
+
+ FlushStatistics();
+}
+
+void TTcpConnection::Start()
+{
+ // Offline in PendingControl_ prevents retrying events until end of Open().
+ YT_VERIFY(Any(static_cast<EPollControl>(PendingControl_.load()) & EPollControl::Offline));
+
+ if (AbortIfNetworkingDisabled()) {
+ return;
+ }
+
+ if (!Poller_->TryRegister(this)) {
+ Abort(TError(NBus::EErrorCode::TransportError, "Cannot register connection pollable"));
+ return;
+ }
+
+ TTcpDispatcher::TImpl::Get()->RegisterConnection(this);
+ InitBuffers();
+
+ switch (ConnectionType_) {
+ case EConnectionType::Client:
+ YT_VERIFY(Socket_ == INVALID_SOCKET);
+ State_ = EState::Resolving;
+ ResolveAddress();
+ break;
+
+ case EConnectionType::Server: {
+ auto guard = Guard(Lock_);
+ YT_VERIFY(Socket_ != INVALID_SOCKET);
+ State_ = EState::Opening;
+ SetupNetwork(EndpointNetworkAddress_);
+ Open();
+ guard.Release();
+ ReadyPromise_.TrySet();
+ break;
+ }
+
+ default:
+ YT_ABORT();
+ }
+}
+
+void TTcpConnection::RunPeriodicCheck()
+{
+ if (State_ != EState::Open) {
+ return;
+ }
+
+ FlushStatistics();
+
+ auto now = NProfiling::GetCpuInstant();
+
+ if (LastIncompleteWriteTime_.load(std::memory_order::relaxed) < now - WriteStallTimeout_) {
+ UpdateBusCounter(&TBusNetworkBandCounters::StalledWrites, 1);
+ Terminate(TError(
+ NBus::EErrorCode::TransportError,
+ "Socket write stalled")
+ << TErrorAttribute("timeout", Config_->WriteStallTimeout)
+ << TErrorAttribute("pending_control", static_cast<EPollControl>(PendingControl_.load())));
+ return;
+ }
+
+ if (LastIncompleteReadTime_.load(std::memory_order::relaxed) < now - ReadStallTimeout_) {
+ UpdateBusCounter(&TBusNetworkBandCounters::StalledReads, 1);
+ Terminate(TError(
+ NBus::EErrorCode::TransportError,
+ "Socket read stalled")
+ << TErrorAttribute("timeout", Config_->ReadStallTimeout)
+ << TErrorAttribute("pending_control", static_cast<EPollControl>(PendingControl_.load())));
+ return;
+ }
+}
+
+EPollablePriority TTcpConnection::GetPriority() const
+{
+ switch (MultiplexingBand_.load(std::memory_order::relaxed)) {
+ case EMultiplexingBand::RealTime:
+ return EPollablePriority::RealTime;
+ default:
+ return EPollablePriority::Normal;
+ }
+}
+
+const TString& TTcpConnection::GetLoggingTag() const
+{
+ return LoggingTag_;
+}
+
+void TTcpConnection::EnqueueHandshake()
+{
+ if (!SupportsHandshakes_) {
+ return;
+ }
+
+ NProto::THandshake handshake;
+ ToProto(handshake.mutable_foreign_connection_id(), Id_);
+ if (ConnectionType_ == EConnectionType::Client) {
+ handshake.set_multiplexing_band(ToProto<int>(MultiplexingBand_.load()));
+ }
+
+ auto message = MakeHandshakeMessage(handshake);
+ auto messageSize = GetByteSize(message);
+
+ EnqueuePacket(
+ // COMPAT(babenko)
+ EPacketType::Message,
+ EPacketFlags::None,
+ 1,
+ HandshakePacketId,
+ std::move(message),
+ messageSize);
+
+ YT_LOG_DEBUG("Handshake enqueued");
+}
+
+TSharedRefArray TTcpConnection::MakeHandshakeMessage(const NProto::THandshake& handshake)
+{
+ auto protoSize = handshake.ByteSize();
+ auto totalSize = sizeof(HandshakeMessageSignature) + protoSize;
+
+ TSharedRefArrayBuilder builder(1, totalSize);
+ auto ref = builder.AllocateAndAdd(totalSize);
+ char* ptr = ref.Begin();
+
+ *reinterpret_cast<ui32*>(ptr) = HandshakeMessageSignature;
+ ptr += sizeof(ui32);
+
+ SerializeProtoToRef(handshake, TMutableRef(ptr, protoSize));
+ ptr += protoSize;
+
+ return builder.Finish();
+}
+
+std::optional<NProto::THandshake> TTcpConnection::TryParseHandshakeMessage(const TSharedRefArray& message)
+{
+ if (message.Size() != 1) {
+ YT_LOG_ERROR("Handshake packet contains invalid number of parts (PartCount: %v)",
+ message.Size());
+ return {};
+ }
+
+ const auto& part = message[0];
+ if (part.Size() < sizeof(HandshakeMessageSignature)) {
+ YT_LOG_ERROR("Handshake packet size is too small (Size: %v)",
+ part.Size());
+ return {};
+ }
+
+ auto signature = *reinterpret_cast<const ui32*>(part.Begin());
+ if (signature != HandshakeMessageSignature) {
+ YT_LOG_ERROR("Invalid handshake packet signature (Expected: %x, Actual: %x)",
+ HandshakeMessageSignature,
+ signature);
+ return {};
+ }
+
+ NProto::THandshake handshake;
+ if (!TryDeserializeProto(&handshake, part.Slice(sizeof(HandshakeMessageSignature), part.Size()))) {
+ YT_LOG_ERROR("Error deserializing handshake packet");
+ return {};
+ }
+
+ return handshake;
+}
+
+void TTcpConnection::UpdateConnectionCount(int delta)
+{
+ switch (ConnectionType_) {
+ case EConnectionType::Client:
+ UpdateBusCounter(&TBusNetworkBandCounters::ClientConnections, delta);
+ break;
+
+ case EConnectionType::Server:
+ UpdateBusCounter(&TBusNetworkBandCounters::ServerConnections, delta);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+void TTcpConnection::IncrementPendingOut(i64 packetSize)
+{
+ UpdateBusCounter(&TBusNetworkBandCounters::PendingOutPackets, +1);
+ if (UpdateBusCounter(&TBusNetworkBandCounters::PendingOutBytes, packetSize) > PendingOutBytesFlushThreshold) {
+ FlushBusStatistics();
+ }
+}
+
+void TTcpConnection::DecrementPendingOut(i64 packetSize)
+{
+ UpdateBusCounter(&TBusNetworkBandCounters::PendingOutPackets, -1);
+ UpdateBusCounter(&TBusNetworkBandCounters::PendingOutBytes, -packetSize);
+}
+
+TConnectionId TTcpConnection::GetId() const
+{
+ return Id_;
+}
+
+void TTcpConnection::Open()
+{
+ State_ = EState::Open;
+
+ YT_LOG_DEBUG("Connection established (LocalPort: %v)", GetSocketPort());
+
+ if (LastIncompleteWriteTime_ != std::numeric_limits<NProfiling::TCpuInstant>::max()) {
+ // Rewind stall detection if already armed by pending send
+ LastIncompleteWriteTime_ = NProfiling::GetCpuInstant();
+ }
+
+ UpdateConnectionCount(+1);
+ FlushStatistics();
+
+ // Go online and start event processing.
+ auto previousPendingControl = static_cast<EPollControl>(PendingControl_.fetch_and(~static_cast<ui64>(EPollControl::Offline)));
+
+ ArmPoller();
+
+ // Something might be pending already, for example Terminate.
+ if (Any(previousPendingControl & ~EPollControl::Offline)) {
+ YT_LOG_TRACE("Retrying event processing for Open (PendingControl: %v)", previousPendingControl);
+ Poller_->Retry(this);
+ }
+}
+
+void TTcpConnection::ResolveAddress()
+{
+ if (UnixDomainSocketPath_) {
+ if (!IsLocalBusTransportEnabled()) {
+ Abort(TError(NBus::EErrorCode::TransportError, "Local bus transport is not available"));
+ return;
+ }
+
+ NetworkName_ = LocalNetworkName;
+ // NB(gritukan): Unix domain socket path cannot be longer than 108 symbols, so let's try to shorten it.
+ OnAddressResolved(
+ TNetworkAddress::CreateUnixDomainSocketAddress(GetShortestPath(*UnixDomainSocketPath_)));
+ } else {
+ TStringBuf hostName;
+ try {
+ ParseServiceAddress(*EndpointAddress_, &hostName, &Port_);
+ } catch (const std::exception& ex) {
+ Abort(TError(ex).SetCode(NBus::EErrorCode::TransportError));
+ return;
+ }
+
+ TAddressResolver::Get()->Resolve(TString(hostName)).Subscribe(
+ BIND(&TTcpConnection::OnAddressResolveFinished, MakeStrong(this))
+ .Via(Poller_->GetInvoker()));
+ }
+}
+
+void TTcpConnection::OnAddressResolveFinished(const TErrorOr<TNetworkAddress>& result)
+{
+ if (!result.IsOK()) {
+ Abort(result);
+ return;
+ }
+
+ TNetworkAddress address(result.Value(), Port_);
+ OnAddressResolved(address);
+
+ YT_LOG_DEBUG("Connection network address resolved (Address: %v, NetworkName: %v)",
+ address,
+ NetworkName_);
+}
+
+void TTcpConnection::OnAddressResolved(const TNetworkAddress& address)
+{
+ State_ = EState::Opening;
+ SetupNetwork(address);
+ ConnectSocket(address);
+}
+
+void TTcpConnection::SetupNetwork(const TNetworkAddress& address)
+{
+ NetworkName_ = TTcpDispatcher::TImpl::Get()->GetNetworkNameForAddress(address);
+ NetworkCounters_ = TTcpDispatcher::TImpl::Get()->GetCounters(NetworkName_);
+
+ // Suppress checksum generation for local traffic.
+ if (NetworkName_ == LocalNetworkName) {
+ GenerateChecksums_ = false;
+ }
+}
+
+void TTcpConnection::Abort(const TError& error)
+{
+ // Fast path.
+ if (State_ == EState::Aborted || State_ == EState::Closed) {
+ return;
+ }
+
+ // Construct a detailed error.
+ YT_VERIFY(!error.IsOK());
+ auto detailedError = error << *EndpointAttributes_;
+
+ {
+ auto guard = Guard(Lock_);
+
+ if (State_ == EState::Aborted || State_ == EState::Closed) {
+ return;
+ }
+
+ UnarmPoller();
+
+ if (State_ == EState::Open) {
+ UpdateConnectionCount(-1);
+ }
+
+ State_ = EState::Aborted;
+
+ Error_.Store(detailedError);
+
+ // Prevent starting new OnSocketRead/OnSocketWrite and Retry.
+ // Already running will continue, Unregister will drain them.
+ PendingControl_.fetch_or(static_cast<ui64>(EPollControl::Shutdown));
+ }
+
+ YT_LOG_DEBUG(detailedError, "Connection aborted");
+
+ // OnShutdown() will be called after draining events from thread pools.
+ YT_UNUSED_FUTURE(Poller_->Unregister(this));
+
+ ReadyPromise_.TrySet(detailedError);
+}
+
+bool TTcpConnection::AbortIfNetworkingDisabled()
+{
+ if (!TTcpDispatcher::Get()->IsNetworkingDisabled()) {
+ return false;
+ }
+ Abort(TError(NBus::EErrorCode::TransportError, "Networking is disabled"));
+ return true;
+}
+
+void TTcpConnection::InitBuffers()
+{
+ ReadBuffer_ = TBlob(GetRefCountedTypeCookie<TTcpConnectionReadBufferTag>(), ReadBufferSize, /*initializeStorage*/ false);
+
+ WriteBuffers_.push_back(std::make_unique<TBlob>(GetRefCountedTypeCookie<TTcpConnectionWriteBufferTag>()));
+ WriteBuffers_[0]->Reserve(WriteBufferSize);
+}
+
+int TTcpConnection::GetSocketPort()
+{
+ TNetworkAddress address;
+ auto* sockAddr = address.GetSockAddr();
+ socklen_t sockAddrLen = address.GetLength();
+ int result = getsockname(Socket_, sockAddr, &sockAddrLen);
+ if (result < 0) {
+ return -1;
+ }
+
+ switch (sockAddr->sa_family) {
+ case AF_INET:
+ return ntohs(reinterpret_cast<sockaddr_in*>(sockAddr)->sin_port);
+
+ case AF_INET6:
+ return ntohs(reinterpret_cast<sockaddr_in6*>(sockAddr)->sin6_port);
+
+ default:
+ return -1;
+ }
+}
+
+void TTcpConnection::ConnectSocket(const TNetworkAddress& address)
+{
+ auto dialer = CreateAsyncDialer(
+ Config_,
+ Poller_,
+ Logger);
+ DialerSession_ = dialer->CreateSession(
+ address,
+ BIND(&TTcpConnection::OnDialerFinished, MakeWeak(this)));
+ DialerSession_->Dial();
+}
+
+void TTcpConnection::OnDialerFinished(const TErrorOr<SOCKET>& socketOrError)
+{
+ DialerSession_.Reset();
+
+ if (!socketOrError.IsOK()) {
+ Abort(TError(
+ NBus::EErrorCode::TransportError,
+ "Error connecting to %v",
+ EndpointDescription_)
+ << socketOrError);
+ return;
+ }
+
+ {
+ auto guard = Guard(Lock_);
+
+ if (State_ != EState::Opening) {
+ return;
+ }
+
+ Socket_ = socketOrError.Value();
+
+ auto tosLevel = TosLevel_.load();
+ if (tosLevel != DefaultTosLevel) {
+ InitSocketTosLevel(tosLevel);
+ }
+
+ Open();
+ }
+
+ ReadyPromise_.TrySet();
+}
+
+const TString& TTcpConnection::GetEndpointDescription() const
+{
+ return EndpointDescription_;
+}
+
+const IAttributeDictionary& TTcpConnection::GetEndpointAttributes() const
+{
+ return *EndpointAttributes_;
+}
+
+const TString& TTcpConnection::GetEndpointAddress() const
+{
+ if (EndpointAddress_) {
+ return *EndpointAddress_;
+ } else {
+ static const TString EmptyAddress;
+ return EmptyAddress;
+ }
+}
+
+const TNetworkAddress& TTcpConnection::GetEndpointNetworkAddress() const
+{
+ return EndpointNetworkAddress_;
+}
+
+bool TTcpConnection::IsEndpointLocal() const
+{
+ return false;
+}
+
+TBusNetworkStatistics TTcpConnection::GetNetworkStatistics() const
+{
+ if (auto networkCounters = NetworkCounters_.AcquireHazard()) {
+ return networkCounters->ToStatistics();
+ } else {
+ return {};
+ }
+}
+
+TBusNetworkStatistics TTcpConnection::GetBusStatistics() const
+{
+ return BusCounters_.ToStatistics();
+}
+
+TFuture<void> TTcpConnection::GetReadyFuture() const
+{
+ return ReadyPromise_.ToFuture();
+}
+
+TFuture<void> TTcpConnection::Send(TSharedRefArray message, const TSendOptions& options)
+{
+ if (TTcpDispatcher::Get()->IsNetworkingDisabled()) {
+ return MakeFuture(TError(NBus::EErrorCode::TransportError, "Networking is disabled"));
+ }
+
+ if (message.Size() > MaxMessagePartCount) {
+ return MakeFuture<void>(TError(
+ NRpc::EErrorCode::TransportError,
+ "Message exceeds part count limit: %v > %v",
+ message.Size(),
+ MaxMessagePartCount));
+ }
+
+ for (size_t index = 0; index < message.Size(); ++index) {
+ const auto& part = message[index];
+ if (part.Size() > MaxMessagePartSize) {
+ return MakeFuture<void>(TError(
+ NRpc::EErrorCode::TransportError,
+ "Message part %v exceeds size limit: %v > %v",
+ index,
+ part.Size(),
+ MaxMessagePartSize));
+ }
+ }
+
+ TQueuedMessage queuedMessage(std::move(message), options);
+ auto promise = queuedMessage.Promise;
+ auto pendingOutPayloadBytes = PendingOutPayloadBytes_.fetch_add(queuedMessage.PayloadSize);
+
+ // Log first to avoid producing weird traces.
+ YT_LOG_DEBUG("Outcoming message enqueued (PacketId: %v, PendingOutPayloadBytes: %v)",
+ queuedMessage.PacketId,
+ pendingOutPayloadBytes);
+
+ if (LastIncompleteWriteTime_ == std::numeric_limits<NProfiling::TCpuInstant>::max()) {
+ // Arm stall detection.
+ LastIncompleteWriteTime_ = NProfiling::GetCpuInstant();
+ }
+
+ QueuedMessages_.Enqueue(std::move(queuedMessage));
+
+ // Wake up the event processing if needed.
+ {
+ auto previousPendingControl = static_cast<EPollControl>(PendingControl_.fetch_or(static_cast<ui64>(EPollControl::Write)));
+ if (None(previousPendingControl)) {
+ YT_LOG_TRACE("Retrying event processing for Send");
+ Poller_->Retry(this);
+ }
+ }
+
+ // Double-check the state not to leave any dangling outcoming messages.
+ if (State_.load() == EState::Closed) {
+ DiscardOutcomingMessages();
+ }
+
+ return promise;
+}
+
+void TTcpConnection::SetTosLevel(TTosLevel tosLevel)
+{
+ if (TosLevel_.load() == tosLevel) {
+ return;
+ }
+
+ {
+ auto guard = Guard(Lock_);
+ if (Socket_ != INVALID_SOCKET) {
+ InitSocketTosLevel(tosLevel);
+ }
+ }
+
+ TosLevel_.store(tosLevel);
+}
+
+void TTcpConnection::Terminate(const TError& error)
+{
+ // Construct a detailed error.
+ YT_VERIFY(!error.IsOK());
+ auto detailedError = error << *EndpointAttributes_;
+
+ auto guard = Guard(Lock_);
+
+ if (!Error_.Load().IsOK() ||
+ State_ == EState::Aborted ||
+ State_ == EState::Closed)
+ {
+ YT_LOG_DEBUG("Connection is already terminated, termination request ignored (State: %v, PendingControl: %v, PendingOutPayloadBytes: %v)",
+ State_.load(),
+ static_cast<EPollControl>(PendingControl_.load()),
+ PendingOutPayloadBytes_.load());
+ return;
+ }
+
+ YT_LOG_DEBUG("Sending termination request");
+
+ // Save error for OnTerminate().
+ Error_.Store(detailedError);
+
+ // Arm calling OnTerminate() from OnEvent().
+ auto previousPendingControl = static_cast<EPollControl>(PendingControl_.fetch_or(static_cast<ui64>(EPollControl::Terminate)));
+
+ // To recover from bogus state always retry processing unless socket is offline
+ if (None(previousPendingControl & EPollControl::Offline)) {
+ YT_LOG_TRACE("Retrying event processing for Terminate (PendingControl: %v)", previousPendingControl);
+ Poller_->Retry(this);
+ }
+}
+
+void TTcpConnection::SubscribeTerminated(const TCallback<void(const TError&)>& callback)
+{
+ Terminated_.Subscribe(callback);
+}
+
+void TTcpConnection::UnsubscribeTerminated(const TCallback<void(const TError&)>& callback)
+{
+ Terminated_.Unsubscribe(callback);
+}
+
+void TTcpConnection::OnEvent(EPollControl control)
+{
+ EPollControl action;
+ {
+ auto rawPendingControl = PendingControl_.load(std::memory_order::acquire);
+ while (true) {
+ auto pendingControl = static_cast<EPollControl>(rawPendingControl);
+ // New events could come while previous handler is still running.
+ if (Any(pendingControl & (EPollControl::Running | EPollControl::Shutdown))) {
+ if (!PendingControl_.compare_exchange_weak(rawPendingControl, static_cast<ui64>(pendingControl | control))) {
+ continue;
+ }
+ // CAS succeeded, bail out.
+ YT_LOG_TRACE("Event handler is already running (PendingControl: %v)",
+ pendingControl);
+ return;
+ }
+
+ action = pendingControl | control;
+
+ // Clear Read/Write before operation. Consequent event will raise it
+ // back and retry handling. OnSocketRead() always consumes all backlog
+ // or aborts connection if something went wrong, otherwise if something
+ // left then handling should raise Read in PendingControl_ back.
+ if (!PendingControl_.compare_exchange_weak(rawPendingControl, static_cast<ui64>(EPollControl::Running))) {
+ continue;
+ }
+
+ // CAS succeeded, bail out.
+ break;
+ }
+ }
+
+ // OnEvent should never be called for an offline socket.
+ YT_VERIFY(None(action & EPollControl::Offline));
+
+ if (AbortIfNetworkingDisabled()) {
+ return;
+ }
+
+ if (Any(action & EPollControl::Terminate)) {
+ OnTerminate();
+ // Leave Running flag set in PendingControl_ to drain further events and
+ // prevent Retry which could race with Unregister()/OnShutdown().
+ return;
+ }
+
+ YT_LOG_TRACE("Event processing started");
+
+ // NB: Try to read from the socket before writing into it to avoid
+ // getting SIGPIPE when the other party closes the connection.
+ if (Any(action & EPollControl::Read)) {
+ OnSocketRead();
+ }
+
+ if (State_ == EState::Open) {
+ if (!std::exchange(HandshakeEnqueued_, true)) {
+ EnqueueHandshake();
+ }
+ ProcessQueuedMessages();
+ OnSocketWrite();
+ }
+
+ YT_LOG_TRACE("Event processing finished (HasUnsentData: %v)",
+ HasUnsentData());
+
+ FlushBusStatistics();
+
+ // Finally, clear Running flag and recheck new pending events.
+ //
+ // Looping here around one pollable could cause starvation for others and
+ // increase latency for events already picked by this thread. So, put it
+ // away into retry queue without waking other threads. This or any other
+ // thread will handle it on next iteration after handling picked events.
+ //
+ // Do not retry processing if socket is already started shutdown sequence.
+ // Retry request could be picked by thread which already passed draining.
+ {
+ auto previousPendingControl = static_cast<EPollControl>(PendingControl_.fetch_and(~static_cast<ui64>(EPollControl::Running)));
+ YT_ASSERT(Any(previousPendingControl & EPollControl::Running));
+ if (Any(previousPendingControl & ~EPollControl::Running) && None(previousPendingControl & EPollControl::Shutdown)) {
+ YT_LOG_TRACE("Retrying event processing for OnEvent (PendingControl: %v)", previousPendingControl);
+ Poller_->Retry(this, false);
+ }
+ }
+}
+
+void TTcpConnection::OnShutdown()
+{
+ // Perform the initial cleanup (the final one will be in dtor).
+ Close();
+
+ auto error = Error_.Load();
+ YT_LOG_DEBUG(error, "Connection terminated");
+
+ Terminated_.Fire(error);
+}
+
+void TTcpConnection::OnSocketRead()
+{
+ YT_LOG_TRACE("Started serving read request");
+
+ size_t bytesReadTotal = 0;
+ while (true) {
+ // Check if the decoder is expecting a chunk of large enough size.
+ auto decoderChunk = Decoder_->GetFragment();
+ size_t decoderChunkSize = decoderChunk.Size();
+
+ if (decoderChunkSize >= ReadBufferSize) {
+ // Read directly into the decoder buffer.
+ size_t bytesToRead = std::min(decoderChunkSize, MaxBatchReadSize);
+ YT_LOG_TRACE("Reading from socket into decoder (BytesToRead: %v)",bytesToRead);
+
+ size_t bytesRead;
+ if (!ReadSocket(decoderChunk.Begin(), bytesToRead, &bytesRead)) {
+ break;
+ }
+ bytesReadTotal += bytesRead;
+
+ if (!AdvanceDecoder(bytesRead)) {
+ return;
+ }
+ } else {
+ // Read a chunk into the read buffer.
+ YT_LOG_TRACE("Reading from socket into buffer (BytesToRead: %v)", ReadBuffer_.Size());
+
+ size_t bytesRead;
+ if (!ReadSocket(ReadBuffer_.Begin(), ReadBuffer_.Size(), &bytesRead)) {
+ break;
+ }
+ bytesReadTotal += bytesRead;
+
+ // Feed the read buffer to the decoder.
+ const char* recvBegin = ReadBuffer_.Begin();
+ size_t recvRemaining = bytesRead;
+ while (recvRemaining != 0) {
+ decoderChunk = Decoder_->GetFragment();
+ decoderChunkSize = decoderChunk.Size();
+ size_t bytesToCopy = std::min(recvRemaining, decoderChunkSize);
+ YT_LOG_TRACE("Feeding buffer into decoder (DecoderNeededBytes: %v, RemainingBufferBytes: %v, BytesToCopy: %v)",
+ decoderChunkSize,
+ recvRemaining,
+ bytesToCopy);
+ std::copy(recvBegin, recvBegin + bytesToCopy, decoderChunk.Begin());
+ if (!AdvanceDecoder(bytesToCopy)) {
+ return;
+ }
+ recvBegin += bytesToCopy;
+ recvRemaining -= bytesToCopy;
+ }
+ YT_LOG_TRACE("Buffer exhausted");
+ }
+ }
+
+ LastIncompleteReadTime_ = HasUnreadData()
+ ? NProfiling::GetCpuInstant()
+ : std::numeric_limits<NProfiling::TCpuInstant>::max();
+
+ YT_LOG_TRACE("Finished serving read request (BytesReadTotal: %v)",
+ bytesReadTotal);
+}
+
+bool TTcpConnection::HasUnreadData() const
+{
+ return Decoder_->IsInProgress();
+}
+
+bool TTcpConnection::ReadSocket(char* buffer, size_t size, size_t* bytesRead)
+{
+ NProfiling::TWallTimer timer;
+ auto result = HandleEintr(recv, Socket_, buffer, size, 0);
+ auto elapsed = timer.GetElapsedTime();
+ if (elapsed > ReadTimeWarningThreshold) {
+ YT_LOG_DEBUG("Socket read took too long (Elapsed: %v)",
+ elapsed);
+ }
+
+ if (!CheckReadError(result)) {
+ *bytesRead = 0;
+ return false;
+ }
+
+ *bytesRead = static_cast<size_t>(result);
+ UpdateBusCounter(&TBusNetworkBandCounters::InBytes, result);
+
+ YT_LOG_TRACE("Socket read (BytesRead: %v)", *bytesRead);
+
+ if (Config_->EnableQuickAck) {
+ if (!TrySetSocketEnableQuickAck(Socket_)) {
+ YT_LOG_TRACE("Failed to set socket quick ack option");
+ }
+ }
+
+ return true;
+}
+
+bool TTcpConnection::CheckReadError(ssize_t result)
+{
+ if (result == 0) {
+ Abort(TError(NBus::EErrorCode::TransportError, "Socket was closed"));
+ return false;
+ }
+
+ if (result < 0) {
+ int error = LastSystemError();
+ if (IsSocketError(error)) {
+ UpdateBusCounter(&TBusNetworkBandCounters::ReadErrors, 1);
+ Abort(TError(NBus::EErrorCode::TransportError, "Socket read error")
+ << TError::FromSystem(error));
+ }
+ return false;
+ }
+
+ return true;
+}
+
+bool TTcpConnection::AdvanceDecoder(size_t size)
+{
+ if (!Decoder_->Advance(size)) {
+ UpdateBusCounter(&TBusNetworkBandCounters::DecoderErrors, 1);
+ Abort(TError(NBus::EErrorCode::TransportError, "Error decoding incoming packet"));
+ return false;
+ }
+
+ if (Decoder_->IsFinished()) {
+ bool result = OnPacketReceived();
+ Decoder_->Restart();
+ return result;
+ }
+
+ return true;
+}
+
+bool TTcpConnection::OnPacketReceived() noexcept
+{
+ UpdateBusCounter(&TBusNetworkBandCounters::InPackets, 1);
+ switch (Decoder_->GetPacketType()) {
+ case EPacketType::Ack:
+ return OnAckPacketReceived();
+ case EPacketType::Message:
+ return OnMessagePacketReceived();
+ default:
+ YT_LOG_ERROR("Packet of unknown type received, ignored (PacketId: %v, PacketType: %v)",
+ Decoder_->GetPacketId(),
+ Decoder_->GetPacketType());
+ break;
+ }
+}
+
+bool TTcpConnection::OnAckPacketReceived()
+{
+ if (UnackedPackets_.empty()) {
+ Abort(TError(NBus::EErrorCode::TransportError, "Unexpected ack received"));
+ return false;
+ }
+
+ auto& unackedMessage = UnackedPackets_.front();
+
+ if (Decoder_->GetPacketId() != unackedMessage->PacketId) {
+ Abort(TError(
+ NBus::EErrorCode::TransportError,
+ "Ack for invalid packet ID received: expected %v, found %v",
+ unackedMessage->PacketId,
+ Decoder_->GetPacketId()));
+ return false;
+ }
+
+ YT_LOG_DEBUG("Ack received (PacketId: %v)", Decoder_->GetPacketId());
+
+ if (unackedMessage->Promise) {
+ unackedMessage->Promise.TrySet(TError());
+ }
+
+ UnackedPackets_.pop();
+
+ return true;
+}
+
+bool TTcpConnection::OnMessagePacketReceived()
+{
+ // COMPAT(babenko)
+ if (Decoder_->GetPacketId() == HandshakePacketId) {
+ return OnHandshakePacketReceived();
+ }
+
+ YT_LOG_DEBUG("Incoming message received (PacketId: %v, PacketSize: %v, PacketFlags: %v)",
+ Decoder_->GetPacketId(),
+ Decoder_->GetPacketSize(),
+ Decoder_->GetPacketFlags());
+
+ if (Any(Decoder_->GetPacketFlags() & EPacketFlags::RequestAcknowledgement)) {
+ EnqueuePacket(EPacketType::Ack, EPacketFlags::None, 0, Decoder_->GetPacketId());
+ }
+
+ auto message = Decoder_->GrabMessage();
+ Handler_->HandleMessage(std::move(message), this);
+
+ return true;
+}
+
+bool TTcpConnection::OnHandshakePacketReceived()
+{
+ auto optionalHandshake = TryParseHandshakeMessage(Decoder_->GrabMessage());
+ if (!optionalHandshake) {
+ return false;
+ }
+
+ const auto& handshake = *optionalHandshake;
+ auto optionalMultiplexingBand = handshake.has_multiplexing_band()
+ ? std::make_optional(FromProto<EMultiplexingBand>(handshake.multiplexing_band()))
+ : std::nullopt;
+
+ YT_LOG_DEBUG("Handshake received (ForeignConnectionId: %v, MultiplexingBand: %v)",
+ FromProto<TConnectionId>(handshake.foreign_connection_id()),
+ optionalMultiplexingBand);
+
+ if (ConnectionType_ == EConnectionType::Server && optionalMultiplexingBand) {
+ auto guard = Guard(Lock_);
+ UpdateConnectionCount(-1);
+ MultiplexingBand_.store(*optionalMultiplexingBand);
+ UpdateConnectionCount(+1);
+ }
+
+ return true;
+}
+
+TTcpConnection::TPacket* TTcpConnection::EnqueuePacket(
+ EPacketType type,
+ EPacketFlags flags,
+ int checksummedPartCount,
+ TPacketId packetId,
+ TSharedRefArray message,
+ size_t payloadSize)
+{
+ size_t packetSize = Encoder_->GetPacketSize(type, message, payloadSize);
+ auto packetHolder = New<TPacket>(
+ type,
+ flags,
+ checksummedPartCount,
+ packetId,
+ std::move(message),
+ payloadSize,
+ packetSize);
+ auto* packet = packetHolder.Get();
+ QueuedPackets_.push(std::move(packetHolder));
+ IncrementPendingOut(packetSize);
+ return packet;
+}
+
+void TTcpConnection::OnSocketWrite()
+{
+ YT_LOG_TRACE("Started serving write request");
+
+ size_t bytesWrittenTotal = 0;
+ while (true) {
+ if (!HasUnsentData()) {
+ // Unarm stall detection at end of write
+ LastIncompleteWriteTime_ = std::numeric_limits<NProfiling::TCpuInstant>::max();
+ break;
+ }
+
+ if (!MaybeEncodeFragments()) {
+ break;
+ }
+
+ size_t bytesWritten;
+ bool success = WriteFragments(&bytesWritten);
+ bytesWrittenTotal += bytesWritten;
+
+ FlushWrittenFragments(bytesWritten);
+ FlushWrittenPackets(bytesWritten);
+
+ if (bytesWritten) {
+ // Rearm stall detection after progress.
+ LastIncompleteWriteTime_ = NProfiling::GetCpuInstant();
+ }
+
+ if (!success) {
+ break;
+ }
+ }
+
+ YT_LOG_TRACE("Finished serving write request (BytesWrittenTotal: %v)", bytesWrittenTotal);
+}
+
+bool TTcpConnection::HasUnsentData() const
+{
+ return !EncodedFragments_.empty() || !QueuedPackets_.empty() || !EncodedPackets_.empty();
+}
+
+bool TTcpConnection::WriteFragments(size_t* bytesWritten)
+{
+ YT_LOG_TRACE("Writing fragments (EncodedFragments: %v)", EncodedFragments_.size());
+
+ auto fragmentIt = EncodedFragments_.begin();
+ auto fragmentEnd = EncodedFragments_.end();
+
+ SendVector_.clear();
+ size_t bytesAvailable = MaxBatchWriteSize;
+
+ while (fragmentIt != fragmentEnd &&
+ SendVector_.size() < MaxFragmentsPerWrite &&
+ bytesAvailable > 0)
+ {
+ const auto& fragment = *fragmentIt;
+ size_t size = std::min(fragment.Size(), bytesAvailable);
+ struct iovec item;
+ item.iov_base = const_cast<char*>(fragment.Begin());
+ item.iov_len = size;
+ SendVector_.push_back(item);
+ EncodedFragments_.move_forward(fragmentIt);
+ bytesAvailable -= size;
+ }
+
+ NProfiling::TWallTimer timer;
+ auto result = HandleEintr(::writev, Socket_, SendVector_.data(), SendVector_.size());
+ auto elapsed = timer.GetElapsedTime();
+ if (elapsed > WriteTimeWarningThreshold) {
+ YT_LOG_DEBUG("Socket write took too long (Elapsed: %v)",
+ elapsed);
+ }
+
+ *bytesWritten = result >= 0 ? static_cast<size_t>(result) : 0;
+ bool isOK = CheckWriteError(result);
+ if (isOK) {
+ UpdateBusCounter(&TBusNetworkBandCounters::OutBytes, *bytesWritten);
+ YT_LOG_TRACE("Socket written (BytesWritten: %v)", *bytesWritten);
+ }
+ return isOK;
+}
+
+void TTcpConnection::FlushWrittenFragments(size_t bytesWritten)
+{
+ size_t bytesToFlush = bytesWritten;
+ YT_LOG_TRACE("Flushing fragments (BytesWritten: %v)", bytesWritten);
+
+ while (bytesToFlush != 0) {
+ YT_ASSERT(!EncodedFragments_.empty());
+ auto& fragment = EncodedFragments_.front();
+
+ if (fragment.Size() > bytesToFlush) {
+ size_t bytesRemaining = fragment.Size() - bytesToFlush;
+ YT_LOG_TRACE("Partial write (Size: %v, RemainingSize: %v)",
+ fragment.Size(),
+ bytesRemaining);
+ fragment = TRef(fragment.End() - bytesRemaining, bytesRemaining);
+ break;
+ }
+
+ YT_LOG_TRACE("Full write (Size: %v)", fragment.Size());
+
+ bytesToFlush -= fragment.Size();
+ EncodedFragments_.pop();
+ }
+}
+
+void TTcpConnection::FlushWrittenPackets(size_t bytesWritten)
+{
+ size_t bytesToFlush = bytesWritten;
+ YT_LOG_TRACE("Flushing packets (BytesWritten: %v)", bytesWritten);
+
+ while (bytesToFlush != 0) {
+ YT_ASSERT(!EncodedPacketSizes_.empty());
+ auto& packetSize = EncodedPacketSizes_.front();
+
+ if (packetSize > bytesToFlush) {
+ size_t bytesRemaining = packetSize - bytesToFlush;
+ YT_LOG_TRACE("Partial write (Size: %v, RemainingSize: %v)",
+ packetSize,
+ bytesRemaining);
+ packetSize = bytesRemaining;
+ break;
+ }
+
+ YT_LOG_TRACE("Full write (Size: %v)", packetSize);
+
+ bytesToFlush -= packetSize;
+ OnPacketSent();
+ EncodedPacketSizes_.pop();
+ }
+}
+
+bool TTcpConnection::MaybeEncodeFragments()
+{
+ if (!EncodedFragments_.empty() || QueuedPackets_.empty()) {
+ return true;
+ }
+
+ // Discard all buffers except for a single one.
+ WriteBuffers_.resize(1);
+ auto* buffer = WriteBuffers_.back().get();
+ buffer->Clear();
+
+ size_t encodedSize = 0;
+ size_t coalescedSize = 0;
+
+ auto flushCoalesced = [&] () {
+ if (coalescedSize > 0) {
+ EncodedFragments_.push(TRef(buffer->End() - coalescedSize, coalescedSize));
+ coalescedSize = 0;
+ }
+ };
+
+ auto coalesce = [&] (TRef fragment) {
+ if (buffer->Size() + fragment.Size() > buffer->Capacity()) {
+ // Make sure we never reallocate.
+ flushCoalesced();
+ WriteBuffers_.push_back(std::make_unique<TBlob>(GetRefCountedTypeCookie<TTcpConnectionWriteBufferTag>()));
+ buffer = WriteBuffers_.back().get();
+ buffer->Reserve(std::max(WriteBufferSize, fragment.Size()));
+ }
+ buffer->Append(fragment);
+ coalescedSize += fragment.Size();
+ };
+
+ while (EncodedFragments_.size() < MaxFragmentsPerWrite &&
+ encodedSize <= MaxBatchWriteSize &&
+ !QueuedPackets_.empty())
+ {
+ auto& queuedPacket = QueuedPackets_.front();
+ YT_LOG_TRACE("Checking packet cancel state (PacketId: %v)", queuedPacket->PacketId);
+ if (!queuedPacket->MarkEncoded()) {
+ YT_LOG_TRACE("Packet was canceled (PacketId: %v)", queuedPacket->PacketId);
+ QueuedPackets_.pop();
+ continue;
+ }
+
+ // Move the packet from queued to encoded.
+ if (Any(queuedPacket->Flags & EPacketFlags::RequestAcknowledgement)) {
+ UnackedPackets_.push(queuedPacket);
+ }
+ EncodedPackets_.push(std::move(queuedPacket));
+ QueuedPackets_.pop();
+
+ const auto& packet = EncodedPackets_.back();
+
+ // Encode the packet.
+ YT_LOG_TRACE("Starting encoding packet (PacketId: %v)", packet->PacketId);
+
+ bool encodeResult = Encoder_->Start(
+ packet->Type,
+ packet->Flags,
+ GenerateChecksums_,
+ packet->ChecksummedPartCount,
+ packet->PacketId,
+ packet->Message);
+ if (!encodeResult) {
+ UpdateBusCounter(&TBusNetworkBandCounters::EncoderErrors, 1);
+ Abort(TError(NBus::EErrorCode::TransportError, "Error encoding outcoming packet"));
+ return false;
+ }
+
+ do {
+ auto fragment = Encoder_->GetFragment();
+ if (!Encoder_->IsFragmentOwned() || fragment.Size() <= MaxWriteCoalesceSize) {
+ coalesce(fragment);
+ } else {
+ flushCoalesced();
+ EncodedFragments_.push(fragment);
+ }
+ YT_LOG_TRACE("Fragment encoded (Size: %v)", fragment.Size());
+ Encoder_->NextFragment();
+ } while (!Encoder_->IsFinished());
+
+ EncodedPacketSizes_.push(packet->PacketSize);
+ encodedSize += packet->PacketSize;
+
+ YT_LOG_TRACE("Finished encoding packet (PacketId: %v)", packet->PacketId);
+ }
+
+ flushCoalesced();
+
+ return true;
+}
+
+bool TTcpConnection::CheckWriteError(ssize_t result)
+{
+ if (result < 0) {
+ int error = LastSystemError();
+ if (IsSocketError(error)) {
+ UpdateBusCounter(&TBusNetworkBandCounters::WriteErrors, 1);
+ Abort(TError(NBus::EErrorCode::TransportError, "Socket write error")
+ << TError::FromSystem(error));
+ }
+ return false;
+ }
+
+ return true;
+}
+
+void TTcpConnection::OnPacketSent()
+{
+ const auto& packet = EncodedPackets_.front();
+ switch (packet->Type) {
+ case EPacketType::Ack:
+ OnAckPacketSent(*packet);
+ break;
+
+ case EPacketType::Message:
+ // COMPAT(babenko)
+ if (packet->PacketId == HandshakePacketId) {
+ OnHandshakePacketSent();
+ } else {
+ OnMessagePacketSent(*packet);
+ }
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ DecrementPendingOut(packet->PacketSize);
+ UpdateBusCounter(&TBusNetworkBandCounters::OutPackets, 1);
+
+ EncodedPackets_.pop();
+}
+
+void TTcpConnection::OnAckPacketSent(const TPacket& packet)
+{
+ YT_LOG_DEBUG("Ack sent (PacketId: %v)",
+ packet.PacketId);
+}
+
+void TTcpConnection::OnMessagePacketSent(const TPacket& packet)
+{
+ YT_LOG_DEBUG("Outcoming message sent (PacketId: %v)",
+ packet.PacketId);
+
+ PendingOutPayloadBytes_.fetch_sub(packet.PayloadSize);
+
+ // Arm read stall timeout for incoming ACK.
+ if (Any(packet.Flags & EPacketFlags::RequestAcknowledgement) &&
+ LastIncompleteReadTime_ == std::numeric_limits<NProfiling::TCpuInstant>::max())
+ {
+ LastIncompleteReadTime_ = NProfiling::GetCpuInstant();
+ }
+}
+
+void TTcpConnection::OnHandshakePacketSent()
+{
+ YT_LOG_DEBUG("Handshake sent");
+}
+
+void TTcpConnection::OnTerminate()
+{
+ if (State_ == EState::Aborted || State_ == EState::Closed) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Termination request received");
+
+ Abort(Error_.Load());
+}
+
+void TTcpConnection::ProcessQueuedMessages()
+{
+ auto messages = QueuedMessages_.DequeueAll();
+
+ for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
+ auto& queuedMessage = *it;
+
+ auto packetId = queuedMessage.PacketId;
+ auto flags = queuedMessage.Options.TrackingLevel == EDeliveryTrackingLevel::Full
+ ? EPacketFlags::RequestAcknowledgement
+ : EPacketFlags::None;
+
+ auto* packet = EnqueuePacket(
+ EPacketType::Message,
+ flags,
+ GenerateChecksums_ ? queuedMessage.Options.ChecksummedPartCount : 0,
+ packetId,
+ std::move(queuedMessage.Message),
+ queuedMessage.PayloadSize);
+
+ packet->Promise = queuedMessage.Promise;
+ if (queuedMessage.Options.EnableSendCancelation) {
+ packet->EnableCancel(MakeStrong(this));
+ }
+
+ YT_LOG_DEBUG("Outcoming message dequeued (PacketId: %v, PacketSize: %v, Flags: %v)",
+ packetId,
+ packet->PacketSize,
+ flags);
+
+ if (queuedMessage.Promise && !queuedMessage.Options.EnableSendCancelation && !Any(flags & EPacketFlags::RequestAcknowledgement)) {
+ queuedMessage.Promise.TrySet();
+ }
+ }
+}
+
+void TTcpConnection::DiscardOutcomingMessages()
+{
+ auto error = Error_.Load();
+
+ auto guard = Guard(QueuedMessagesDiscardLock_);
+ auto queuedMessages = QueuedMessages_.DequeueAll();
+ guard.Release();
+
+ for (const auto& queuedMessage : queuedMessages) {
+ YT_LOG_DEBUG("Outcoming message discarded (PacketId: %v)",
+ queuedMessage.PacketId);
+ if (queuedMessage.Promise) {
+ queuedMessage.Promise.TrySet(error);
+ }
+ }
+}
+
+void TTcpConnection::DiscardUnackedMessages()
+{
+ auto error = Error_.Load();
+
+
+ while (!UnackedPackets_.empty()) {
+ auto& message = UnackedPackets_.front();
+ if (message->Promise) {
+ message->Promise.TrySet(error);
+ }
+ UnackedPackets_.pop();
+ }
+}
+
+int TTcpConnection::GetSocketError() const
+{
+ return NNet::GetSocketError(Socket_);
+}
+
+bool TTcpConnection::IsSocketError(ssize_t result)
+{
+ return
+ result != EWOULDBLOCK &&
+ result != EAGAIN &&
+ result != EINPROGRESS;
+}
+
+void TTcpConnection::CloseSocket()
+{
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ if (Socket_ != INVALID_SOCKET) {
+ NNet::CloseSocket(Socket_);
+ Socket_ = INVALID_SOCKET;
+ }
+}
+
+void TTcpConnection::ArmPoller()
+{
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+ YT_VERIFY(Socket_ != INVALID_SOCKET);
+
+ Poller_->Arm(Socket_, this, EPollControl::Read | EPollControl::Write | EPollControl::EdgeTriggered);
+}
+
+void TTcpConnection::UnarmPoller()
+{
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ if (Socket_ != INVALID_SOCKET) {
+ Poller_->Unarm(Socket_, this);
+ }
+}
+
+void TTcpConnection::InitSocketTosLevel(TTosLevel tosLevel)
+{
+ if (TosLevel_ == BlackHoleTosLevel && tosLevel != BlackHoleTosLevel) {
+ if (!TrySetSocketInputFilter(Socket_, false)) {
+ YT_LOG_DEBUG("Failed to remove socket input filter");
+ }
+ }
+
+ if (tosLevel == BlackHoleTosLevel) {
+ if (TrySetSocketInputFilter(Socket_, true)) {
+ YT_LOG_DEBUG("Socket TOS level set to BlackHole");
+ } else {
+ YT_LOG_DEBUG("Failed to set socket input filter");
+ }
+ } else if (TrySetSocketTosLevel(Socket_, tosLevel)) {
+ YT_LOG_DEBUG("Socket TOS level set (TosLevel: %x)",
+ tosLevel);
+ } else {
+ YT_LOG_DEBUG("Failed to set socket TOS level");
+ }
+}
+
+void TTcpConnection::FlushStatistics()
+{
+ UpdateTcpStatistics();
+ FlushBusStatistics();
+}
+
+template <class T, class U>
+i64 TTcpConnection::UpdateBusCounter(T TBusNetworkBandCounters::* field, U delta)
+{
+ auto band = MultiplexingBand_.load(std::memory_order::relaxed);
+ (BusCountersDelta_.PerBandCounters[band].*field).fetch_add(delta, std::memory_order::relaxed);
+ return (BusCounters_.PerBandCounters[band].*field).fetch_add(delta, std::memory_order::relaxed) + delta;
+}
+
+void TTcpConnection::UpdateTcpStatistics()
+{
+#ifdef _linux_
+ if (Socket_ != INVALID_SOCKET) {
+ tcp_info info;
+ socklen_t len = sizeof(info);
+ int ret = ::getsockopt(Socket_, IPPROTO_TCP, TCP_INFO, &info, &len);
+ if (ret == 0) {
+ // Handle counter overflow.
+ i64 delta = info.tcpi_total_retrans < LastRetransmitCount_
+ ? info.tcpi_total_retrans + (Max<ui32>() - LastRetransmitCount_)
+ : info.tcpi_total_retrans - LastRetransmitCount_;
+ UpdateBusCounter(&TBusNetworkBandCounters::Retransmits, delta);
+ LastRetransmitCount_ = info.tcpi_total_retrans;
+ }
+ }
+#else
+ Y_UNUSED(LastRetransmitCount_);
+#endif
+}
+
+void TTcpConnection::FlushBusStatistics()
+{
+ auto networkCounters = NetworkCounters_.Acquire();
+ if (!networkCounters) {
+ return;
+ }
+
+ for (auto band : TEnumTraits<EMultiplexingBand>::GetDomainValues()) {
+#define XX(camelCaseField, snakeCaseField) networkCounters->PerBandCounters[band].camelCaseField.fetch_add(BusCountersDelta_.PerBandCounters[band].camelCaseField.exchange(0));
+ ITERATE_BUS_NETWORK_STATISTICS_FIELDS(XX)
+#undef XX
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/connection.h b/yt/yt/core/bus/tcp/connection.h
new file mode 100644
index 0000000000..57d44f128a
--- /dev/null
+++ b/yt/yt/core/bus/tcp/connection.h
@@ -0,0 +1,330 @@
+#pragma once
+
+#include "packet.h"
+#include "dispatcher_impl.h"
+
+#include <yt/yt/core/bus/private.h>
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt_proto/yt/core/bus/proto/bus.pb.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/misc/atomic_object.h>
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+#include <yt/yt/core/misc/ring_queue.h>
+#include <yt/yt/core/misc/atomic_ptr.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/concurrency/pollable_detail.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <util/network/init.h>
+
+#ifdef _win_
+#include <winsock2.h>
+
+#include <stddef.h>
+#include <sys/uio.h>
+#include <fcntl.h>
+#endif
+
+#include <atomic>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETcpConnectionState,
+ (None)
+ (Resolving)
+ (Opening)
+ (Open)
+ (Closed)
+ (Aborted)
+);
+
+DEFINE_ENUM(EPacketState,
+ (Queued)
+ (Encoded)
+ (Canceled)
+);
+
+class TTcpConnection
+ : public IBus
+ , public NConcurrency::TPollableBase
+{
+public:
+ TTcpConnection(
+ TBusConfigPtr config,
+ EConnectionType connectionType,
+ TConnectionId id,
+ SOCKET socket,
+ EMultiplexingBand multiplexingBand,
+ const TString& endpointDescription,
+ const NYTree::IAttributeDictionary& endpointAttributes,
+ const NNet::TNetworkAddress& endpointNetworkAddress,
+ const std::optional<TString>& endpointAddress,
+ const std::optional<TString>& unixDomainSocketPath,
+ IMessageHandlerPtr handler,
+ NConcurrency::IPollerPtr poller,
+ IPacketTranscoderFactory* packetTranscoderFactory);
+
+ ~TTcpConnection();
+
+ void Start();
+ void RunPeriodicCheck();
+
+ TConnectionId GetId() const;
+ TBusNetworkStatistics GetBusStatistics() const;
+
+ // IPollable implementation.
+ NConcurrency::EPollablePriority GetPriority() const override;
+ const TString& GetLoggingTag() const override;
+ void OnEvent(NConcurrency::EPollControl control) override;
+ void OnShutdown() override;
+
+ // IBus implementation.
+ const TString& GetEndpointDescription() const override;
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override;
+ const TString& GetEndpointAddress() const override;
+ const NNet::TNetworkAddress& GetEndpointNetworkAddress() const override;
+ bool IsEndpointLocal() const override;
+ TBusNetworkStatistics GetNetworkStatistics() const override;
+ TFuture<void> GetReadyFuture() const override;
+ TFuture<void> Send(TSharedRefArray message, const TSendOptions& options) override;
+ void SetTosLevel(TTosLevel tosLevel) override;
+ void Terminate(const TError& error) override;
+
+ DECLARE_SIGNAL_OVERRIDE(void(const TError&), Terminated);
+
+private:
+ using EState = ETcpConnectionState;
+
+ struct TQueuedMessage
+ {
+ TQueuedMessage() = default;
+
+ TQueuedMessage(TSharedRefArray message, const TSendOptions& options)
+ : Promise((options.TrackingLevel != EDeliveryTrackingLevel::None || options.EnableSendCancelation)
+ ? NewPromise<void>()
+ : std::nullopt)
+ , Message(std::move(message))
+ , PayloadSize(GetByteSize(Message))
+ , Options(options)
+ , PacketId(TPacketId::Create())
+ { }
+
+ TPromise<void> Promise;
+ TSharedRefArray Message;
+ size_t PayloadSize;
+ TSendOptions Options;
+ TPacketId PacketId;
+ };
+
+ struct TPacket final
+ {
+ TPacket(
+ EPacketType type,
+ EPacketFlags flags,
+ int checksummedPartCount,
+ TPacketId packetId,
+ TSharedRefArray message,
+ size_t payloadSize,
+ size_t packetSize)
+ : Type(type)
+ , Flags(flags)
+ , ChecksummedPartCount(checksummedPartCount)
+ , PacketId(packetId)
+ , Message(std::move(message))
+ , PayloadSize(payloadSize)
+ , PacketSize(packetSize)
+ { }
+
+ EPacketType Type;
+ EPacketFlags Flags;
+ int ChecksummedPartCount;
+ TPacketId PacketId;
+
+ TSharedRefArray Message;
+
+ size_t PayloadSize;
+ size_t PacketSize;
+
+ std::atomic<EPacketState> State = EPacketState::Queued;
+ TPromise<void> Promise;
+ TTcpConnectionPtr Connection;
+
+ bool MarkEncoded();
+ void OnCancel(const TError& error);
+ void EnableCancel(TTcpConnectionPtr connection);
+ };
+
+ using TPacketPtr = TIntrusivePtr<TPacket>;
+
+ const TBusConfigPtr Config_;
+ const EConnectionType ConnectionType_;
+ const TConnectionId Id_;
+ const TString EndpointDescription_;
+ const NYTree::IAttributeDictionaryPtr EndpointAttributes_;
+ const NNet::TNetworkAddress EndpointNetworkAddress_;
+ const std::optional<TString> EndpointAddress_;
+ const std::optional<TString> UnixDomainSocketPath_;
+ const std::optional<TString> AbstractUnixDomainSocketName_;
+ const IMessageHandlerPtr Handler_;
+ const NConcurrency::IPollerPtr Poller_;
+
+ const NLogging::TLogger Logger;
+ const TString LoggingTag_;
+
+ const TPromise<void> ReadyPromise_ = NewPromise<void>();
+
+ TString NetworkName_;
+
+ TBusNetworkCounters BusCounters_;
+ TBusNetworkCounters BusCountersDelta_;
+ TAtomicPtr<TBusNetworkCounters, /*EnableAcquireHazard*/ true> NetworkCounters_;
+
+ bool GenerateChecksums_ = true;
+
+ // Only used by client sockets.
+ int Port_ = 0;
+
+ std::atomic<EState> State_ = EState::None;
+
+ // Actually stores NConcurrency::EPollControl.
+ std::atomic<ui64> PendingControl_ = static_cast<ui64>(NConcurrency::EPollControl::Offline);
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ SOCKET Socket_ = INVALID_SOCKET;
+
+ std::atomic<EMultiplexingBand> MultiplexingBand_ = EMultiplexingBand::Default;
+
+ TAtomicObject<TError> Error_;
+
+ NNet::IAsyncDialerSessionPtr DialerSession_;
+
+ TSingleShotCallbackList<void(const TError&)> Terminated_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, QueuedMessagesDiscardLock_);
+ TMpscStack<TQueuedMessage> QueuedMessages_;
+ std::atomic<size_t> PendingOutPayloadBytes_ = 0;
+
+ std::unique_ptr<IPacketDecoder> Decoder_;
+ const NProfiling::TCpuDuration ReadStallTimeout_;
+ std::atomic<NProfiling::TCpuInstant> LastIncompleteReadTime_ = std::numeric_limits<NProfiling::TCpuInstant>::max();
+ TBlob ReadBuffer_;
+
+ TRingQueue<TPacketPtr> QueuedPackets_;
+ TRingQueue<TPacketPtr> EncodedPackets_;
+ TRingQueue<TPacketPtr> UnackedPackets_;
+
+ std::unique_ptr<IPacketEncoder> Encoder_;
+ const NProfiling::TCpuDuration WriteStallTimeout_;
+ std::atomic<NProfiling::TCpuInstant> LastIncompleteWriteTime_ = std::numeric_limits<NProfiling::TCpuInstant>::max();
+ std::vector<std::unique_ptr<TBlob>> WriteBuffers_;
+ TRingQueue<TRef> EncodedFragments_;
+ TRingQueue<size_t> EncodedPacketSizes_;
+
+ std::vector<struct iovec> SendVector_;
+
+ std::atomic<TTosLevel> TosLevel_ = DefaultTosLevel;
+
+ i64 LastRetransmitCount_ = 0;
+
+ bool SupportsHandshakes_ = false;
+ bool HandshakeEnqueued_ = false;
+
+ void Open();
+ void Close();
+
+ void Abort(const TError& error);
+ bool AbortIfNetworkingDisabled();
+
+ void InitBuffers();
+
+ int GetSocketPort();
+
+ void ConnectSocket(const NNet::TNetworkAddress& address);
+ void OnDialerFinished(const TErrorOr<SOCKET>& socketOrError);
+
+ void ResolveAddress();
+ void OnAddressResolveFinished(const TErrorOr<NNet::TNetworkAddress>& result);
+ void OnAddressResolved(const NNet::TNetworkAddress& address);
+ void SetupNetwork(const NNet::TNetworkAddress& address);
+
+ int GetSocketError() const;
+ bool IsSocketError(ssize_t result);
+
+ void CloseSocket();
+
+ void ArmPoller();
+ void UnarmPoller();
+
+ void OnSocketRead();
+ bool HasUnreadData() const;
+ bool ReadSocket(char* buffer, size_t size, size_t* bytesRead);
+ bool CheckReadError(ssize_t result);
+ bool AdvanceDecoder(size_t size);
+ bool OnPacketReceived() noexcept;
+ bool OnAckPacketReceived();
+ bool OnMessagePacketReceived();
+ bool OnHandshakePacketReceived();
+
+ TPacket* EnqueuePacket(
+ EPacketType type,
+ EPacketFlags flags,
+ int checksummedPartCount,
+ TPacketId packetId,
+ TSharedRefArray message = {},
+ size_t payloadSize = 0);
+
+ void OnSocketWrite();
+ bool HasUnsentData() const;
+ bool WriteFragments(size_t* bytesWritten);
+ void FlushWrittenFragments(size_t bytesWritten);
+ void FlushWrittenPackets(size_t bytesWritten);
+ bool MaybeEncodeFragments();
+ bool CheckWriteError(ssize_t result);
+ void OnPacketSent();
+ void OnAckPacketSent(const TPacket& packet);
+ void OnMessagePacketSent(const TPacket& packet);
+ void OnHandshakePacketSent();
+ void OnTerminate();
+ void ProcessQueuedMessages();
+ void DiscardOutcomingMessages();
+ void DiscardUnackedMessages();
+
+ void EnqueueHandshake();
+ TSharedRefArray MakeHandshakeMessage(const NProto::THandshake& handshake);
+ std::optional<NProto::THandshake> TryParseHandshakeMessage(const TSharedRefArray& message);
+
+ void UpdateConnectionCount(int delta);
+
+ void IncrementPendingOut(i64 packetSize);
+ void DecrementPendingOut(i64 packetSize);
+
+ void FlushStatistics();
+
+ template <class T, class U>
+ i64 UpdateBusCounter(T TBusNetworkBandCounters::* field, U delta);
+
+ void UpdateTcpStatistics();
+ void FlushBusStatistics();
+
+ void InitSocketTosLevel(int tosLevel);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTcpConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/dispatcher.cpp b/yt/yt/core/bus/tcp/dispatcher.cpp
new file mode 100644
index 0000000000..8c3eeba6ef
--- /dev/null
+++ b/yt/yt/core/bus/tcp/dispatcher.cpp
@@ -0,0 +1,63 @@
+#include "dispatcher.h"
+#include "dispatcher_impl.h"
+
+#include <yt/yt/core/bus/private.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTcpDispatcher::TTcpDispatcher()
+ : Impl_(New<TImpl>())
+{
+ BusProfiler.WithSparse().AddProducer("", Impl_);
+}
+
+TTcpDispatcher* TTcpDispatcher::Get()
+{
+ return LeakySingleton<TTcpDispatcher>();
+}
+
+void TTcpDispatcher::Configure(const TTcpDispatcherConfigPtr& config)
+{
+ Impl_->Configure(config);
+}
+
+const TBusNetworkCountersPtr& TTcpDispatcher::GetCounters(const TString& networkName)
+{
+ return Impl_->GetCounters(networkName);
+}
+
+NConcurrency::IPollerPtr TTcpDispatcher::GetXferPoller()
+{
+ return Impl_->GetXferPoller();
+}
+
+void TTcpDispatcher::DisableNetworking()
+{
+ Impl_->DisableNetworking();
+}
+
+bool TTcpDispatcher::IsNetworkingDisabled()
+{
+ return Impl_->IsNetworkingDisabled();
+}
+
+const TString& TTcpDispatcher::GetNetworkNameForAddress(const NNet::TNetworkAddress& address)
+{
+ return Impl_->GetNetworkNameForAddress(address);
+}
+
+TTosLevel TTcpDispatcher::GetTosLevelForBand(EMultiplexingBand band)
+{
+ return Impl_->GetTosLevelForBand(band);
+}
+
+NYTree::IYPathServicePtr TTcpDispatcher::GetOrchidService()
+{
+ return Impl_->GetOrchidService();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/dispatcher.h b/yt/yt/core/bus/tcp/dispatcher.h
new file mode 100644
index 0000000000..5fa4b8c75a
--- /dev/null
+++ b/yt/yt/core/bus/tcp/dispatcher.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBusNetworkBandCounters
+{
+ #define XX(camelCaseField, snakeCaseField) std::atomic<i64> camelCaseField = 0;
+ ITERATE_BUS_NETWORK_STATISTICS_FIELDS(XX)
+ #undef XX
+};
+
+struct TBusNetworkCounters final
+{
+ static constexpr bool EnableHazard = true;
+
+ TEnumIndexedVector<EMultiplexingBand, TBusNetworkBandCounters> PerBandCounters;
+
+ TBusNetworkStatistics ToStatistics() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpDispatcher
+{
+public:
+ static TTcpDispatcher* Get();
+
+ const TBusNetworkCountersPtr& GetCounters(const TString& networkName);
+
+ //! Returns the poller used by TCP transport.
+ NConcurrency::IPollerPtr GetXferPoller();
+
+ //! Reconfigures the dispatcher.
+ void Configure(const TTcpDispatcherConfigPtr& config);
+
+ //! Disables all networking. Safety measure for local runs and snapshot validation.
+ void DisableNetworking();
+
+ //! Returns true if networking is disabled.
+ bool IsNetworkingDisabled();
+
+ //! Returns the network name for a given #address.
+ const TString& GetNetworkNameForAddress(const NNet::TNetworkAddress& address);
+
+ //! Returns the TOS level configured for a band.
+ TTosLevel GetTosLevelForBand(EMultiplexingBand band);
+
+ //! Provides diagnostics for the whole TCP bus subsystem.
+ NYTree::IYPathServicePtr GetOrchidService();
+
+private:
+ TTcpDispatcher();
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+ friend class TTcpConnection;
+ friend class TTcpBusClient;
+ friend class TTcpBusServerBase;
+ template <class TServer>
+ friend class TTcpBusServerProxy;
+
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/dispatcher_impl.cpp b/yt/yt/core/bus/tcp/dispatcher_impl.cpp
new file mode 100644
index 0000000000..dbde627429
--- /dev/null
+++ b/yt/yt/core/bus/tcp/dispatcher_impl.cpp
@@ -0,0 +1,319 @@
+#include "dispatcher_impl.h"
+#include "config.h"
+#include "connection.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/ytree/ypath_service.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+namespace NYT::NBus {
+
+using namespace NConcurrency;
+using namespace NProfiling;
+using namespace NNet;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = BusLogger;
+
+static constexpr auto PeriodicCheckPeriod = TDuration::MilliSeconds(100);
+static constexpr auto PerConnectionPeriodicCheckPeriod = TDuration::Seconds(10);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNetworkAddress GetLocalBusAddress(int port)
+{
+ auto name = Format("yt-local-bus-%v", port);
+ return TNetworkAddress::CreateAbstractUnixDomainSocketAddress(name);
+}
+
+bool IsLocalBusTransportEnabled()
+{
+#ifdef _linux_
+ return true;
+#else
+ return false;
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBusNetworkStatistics TBusNetworkCounters::ToStatistics() const
+{
+ TBusNetworkStatistics result;
+ for (auto band : TEnumTraits<EMultiplexingBand>::GetDomainValues()) {
+#define XX(camelCaseField, snakeCaseField) result.camelCaseField += PerBandCounters[band].camelCaseField.load(std::memory_order::relaxed);
+ ITERATE_BUS_NETWORK_STATISTICS_FIELDS(XX)
+#undef XX
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TIntrusivePtr<TTcpDispatcher::TImpl>& TTcpDispatcher::TImpl::Get()
+{
+ return TTcpDispatcher::Get()->Impl_;
+}
+
+const TBusNetworkCountersPtr& TTcpDispatcher::TImpl::GetCounters(const TString& networkName)
+{
+ auto [statistics, ok] = NetworkStatistics_.FindOrInsert(networkName, [] {
+ return TNetworkStatistics{};
+ });
+
+ return statistics->Counters;
+}
+
+IPollerPtr TTcpDispatcher::TImpl::GetOrCreatePoller(
+ IThreadPoolPollerPtr* pollerPtr,
+ bool isXfer,
+ const TString& threadNamePrefix)
+{
+ {
+ auto guard = ReaderGuard(PollerLock_);
+ if (*pollerPtr) {
+ return *pollerPtr;
+ }
+ }
+
+ IPollerPtr poller;
+ {
+ auto guard = WriterGuard(PollerLock_);
+ if (!*pollerPtr) {
+ *pollerPtr = CreateThreadPoolPoller(isXfer ? Config_->ThreadPoolSize : 1, threadNamePrefix);
+ }
+ poller = *pollerPtr;
+ }
+
+ StartPeriodicExecutors();
+
+ return poller;
+}
+
+void TTcpDispatcher::TImpl::DisableNetworking()
+{
+ YT_LOG_INFO("Networking disabled");
+
+ NetworkingDisabled_.store(true);
+}
+
+bool TTcpDispatcher::TImpl::IsNetworkingDisabled()
+{
+ return NetworkingDisabled_.load();
+}
+
+const TString& TTcpDispatcher::TImpl::GetNetworkNameForAddress(const TNetworkAddress& address)
+{
+ if (address.IsUnix()) {
+ return LocalNetworkName;
+ }
+
+ if (!address.IsIP6()) {
+ return DefaultNetworkName;
+ }
+
+ auto ip6Address = address.ToIP6Address();
+
+ {
+ auto guard = ReaderGuard(NetworksLock_);
+ for (const auto& [networkAddress, networkName] : Networks_) {
+ if (networkAddress.Contains(ip6Address)) {
+ return networkName;
+ }
+ }
+ }
+
+ return DefaultNetworkName;
+}
+
+TTosLevel TTcpDispatcher::TImpl::GetTosLevelForBand(EMultiplexingBand band)
+{
+ if (band < TEnumTraits<EMultiplexingBand>::GetMinValue() || band > TEnumTraits<EMultiplexingBand>::GetMaxValue()) {
+ return DefaultTosLevel;
+ }
+ const auto& bandDescriptor = BandToDescriptor_[band];
+ return bandDescriptor.TosLevel.load(std::memory_order::relaxed);
+}
+
+IPollerPtr TTcpDispatcher::TImpl::GetAcceptorPoller()
+{
+ static const TString ThreadNamePrefix("BusAcpt");
+ return GetOrCreatePoller(&AcceptorPoller_, false, ThreadNamePrefix);
+}
+
+IPollerPtr TTcpDispatcher::TImpl::GetXferPoller()
+{
+ static const TString ThreadNamePrefix("BusXfer");
+ return GetOrCreatePoller(&XferPoller_, true, ThreadNamePrefix);
+}
+
+void TTcpDispatcher::TImpl::Configure(const TTcpDispatcherConfigPtr& config)
+{
+ {
+ auto guard = WriterGuard(PollerLock_);
+
+ Config_ = config;
+
+ if (XferPoller_) {
+ XferPoller_->Reconfigure(Config_->ThreadPoolSize);
+ }
+ }
+
+ {
+ auto guard = WriterGuard(NetworksLock_);
+
+ Networks_.clear();
+
+ for (const auto& [networkName, networkAddresses] : config->Networks) {
+ for (const auto& prefix : networkAddresses) {
+ Networks_.emplace_back(prefix, networkName);
+ }
+ }
+
+ // Put more specific networks first in match order.
+ std::sort(Networks_.begin(), Networks_.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first.GetMaskSize() > rhs.first.GetMaskSize();
+ });
+ }
+
+ for (auto band : TEnumTraits<EMultiplexingBand>::GetDomainValues()) {
+ const auto& bandConfig = config->MultiplexingBands[band];
+ auto& bandDescriptor = BandToDescriptor_[band];
+ bandDescriptor.TosLevel.store(bandConfig ? bandConfig->TosLevel : DefaultTosLevel);
+ }
+}
+
+void TTcpDispatcher::TImpl::RegisterConnection(TTcpConnectionPtr connection)
+{
+ ConnectionsToRegister_.Enqueue(std::move(connection));
+}
+
+void TTcpDispatcher::TImpl::StartPeriodicExecutors()
+{
+ auto poller = GetXferPoller();
+ auto invoker = poller->GetInvoker();
+
+ auto guard = Guard(PeriodicExecutorsLock_);
+ if (!PeriodicCheckExecutor_) {
+ PeriodicCheckExecutor_ = New<TPeriodicExecutor>(
+ invoker,
+ BIND(&TImpl::OnPeriodicCheck, MakeWeak(this)),
+ PeriodicCheckPeriod);
+ PeriodicCheckExecutor_->Start();
+ }
+}
+
+void TTcpDispatcher::TImpl::CollectSensors(ISensorWriter* writer)
+{
+ NetworkStatistics_.IterateReadOnly([&] (const auto& name, const auto& statistics) {
+ const auto& counters = statistics.Counters;
+ TWithTagGuard networkTagGuard(writer, "network", name);
+ for (auto band : TEnumTraits<EMultiplexingBand>::GetDomainValues()) {
+ TWithTagGuard bandTagGuard(writer, "band", FormatEnum(band));
+ #define XX(camelCaseField, snakeCaseField) writer->AddCounter("/" #snakeCaseField, counters->PerBandCounters[band].camelCaseField.load(std::memory_order::relaxed));
+ ITERATE_BUS_NETWORK_STATISTICS_COUNTER_FIELDS(XX)
+ #undef XX
+ #define XX(camelCaseField, snakeCaseField) writer->AddGauge("/" #snakeCaseField, counters->PerBandCounters[band].camelCaseField.load(std::memory_order::relaxed));
+ ITERATE_BUS_NETWORK_STATISTICS_GAUGE_FIELDS(XX)
+ #undef XX
+ }
+ });
+
+ TTcpDispatcherConfigPtr config;
+ {
+ auto guard = ReaderGuard(PollerLock_);
+ config = Config_;
+ }
+
+ if (config->NetworkBandwidth) {
+ writer->AddGauge("/network_bandwidth_limit", *config->NetworkBandwidth);
+ }
+}
+
+std::vector<TTcpConnectionPtr> TTcpDispatcher::TImpl::GetConnections()
+{
+ std::vector<TTcpConnectionPtr> result;
+ result.reserve(ConnectionList_.size());
+ for (const auto& weakConnection : ConnectionList_) {
+ if (auto connection = weakConnection.Lock()) {
+ result.push_back(connection);
+ }
+ }
+ return result;
+}
+
+void TTcpDispatcher::TImpl::BuildOrchid(IYsonConsumer* consumer)
+{
+ std::vector<std::pair<TTcpConnectionPtr, TBusNetworkStatistics>> connectionsWithStatistics;
+ for (const auto& connection : GetConnections()) {
+ connectionsWithStatistics.emplace_back(connection, connection->GetBusStatistics());
+ }
+ SortBy(connectionsWithStatistics, [] (const auto& connectionWithStatistics) {
+ return -connectionWithStatistics.second.PendingOutBytes;
+ });
+
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("connections").DoMapFor(connectionsWithStatistics, [] (auto fluent, const auto& connectionWithStatistics) {
+ const auto& [connection, statistics] = connectionWithStatistics;
+ fluent
+ .Item(ToString(connection->GetId())).BeginMap()
+ .Item("address").Value(connection->GetEndpointAddress())
+ .Item("statistics").BeginMap()
+ .Item("in_bytes").Value(statistics.InBytes)
+ .Item("in_packets").Value(statistics.InPackets)
+ .Item("out_bytes").Value(statistics.OutBytes)
+ .Item("out_packets").Value(statistics.OutPackets)
+ .Item("pending_out_bytes").Value(statistics.PendingOutBytes)
+ .Item("pending_out_packets").Value(statistics.PendingOutPackets)
+ .EndMap()
+ .EndMap();
+ })
+ .EndMap();
+}
+
+
+IYPathServicePtr TTcpDispatcher::TImpl::GetOrchidService()
+{
+ return IYPathService::FromProducer(BIND(&TImpl::BuildOrchid, MakeStrong(this)))
+ ->Via(GetXferPoller()->GetInvoker());
+}
+
+void TTcpDispatcher::TImpl::OnPeriodicCheck()
+{
+ for (auto&& connection : ConnectionsToRegister_.DequeueAll()) {
+ ConnectionList_.push_back(std::move(connection));
+ }
+
+ i64 connectionsToCheck = std::max(
+ std::ssize(ConnectionList_) *
+ static_cast<i64>(PeriodicCheckPeriod.GetValue()) /
+ static_cast<i64>(PerConnectionPeriodicCheckPeriod.GetValue()),
+ static_cast<i64>(1));
+ for (i64 index = 0; index < connectionsToCheck && !ConnectionList_.empty(); ++index) {
+ auto& weakConnection = ConnectionList_[CurrentConnectionListIndex_];
+ if (auto connection = weakConnection.Lock()) {
+ connection->RunPeriodicCheck();
+ ++CurrentConnectionListIndex_;
+ } else {
+ std::swap(weakConnection, ConnectionList_.back());
+ ConnectionList_.pop_back();
+ }
+ if (CurrentConnectionListIndex_ >= std::ssize(ConnectionList_)) {
+ CurrentConnectionListIndex_ = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/dispatcher_impl.h b/yt/yt/core/bus/tcp/dispatcher_impl.h
new file mode 100644
index 0000000000..f4fee69fab
--- /dev/null
+++ b/yt/yt/core/bus/tcp/dispatcher_impl.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "private.h"
+#include "dispatcher.h"
+#include "config.h"
+
+#include <yt/yt/library/profiling/producer.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/fork_aware_rw_spin_lock.h>
+
+#include <atomic>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NNet::TNetworkAddress GetLocalBusAddress(int port);
+bool IsLocalBusTransportEnabled();
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpDispatcher::TImpl
+ : public NProfiling::ISensorProducer
+{
+public:
+ static const TIntrusivePtr<TImpl>& Get();
+
+ const TBusNetworkCountersPtr& GetCounters(const TString& networkName);
+
+ void DisableNetworking();
+ bool IsNetworkingDisabled();
+
+ const TString& GetNetworkNameForAddress(const NNet::TNetworkAddress& address);
+
+ TTosLevel GetTosLevelForBand(EMultiplexingBand band);
+
+ NConcurrency::IPollerPtr GetAcceptorPoller();
+ NConcurrency::IPollerPtr GetXferPoller();
+
+ void Configure(const TTcpDispatcherConfigPtr& config);
+
+ void RegisterConnection(TTcpConnectionPtr connection);
+
+ void CollectSensors(NProfiling::ISensorWriter* writer) override;
+
+ NYTree::IYPathServicePtr GetOrchidService();
+
+private:
+ friend class TTcpDispatcher;
+
+ DECLARE_NEW_FRIEND()
+
+ void StartPeriodicExecutors();
+ void OnPeriodicCheck();
+
+ NConcurrency::IPollerPtr GetOrCreatePoller(
+ NConcurrency::IThreadPoolPollerPtr* poller,
+ bool isXfer,
+ const TString& threadNamePrefix);
+
+ std::vector<TTcpConnectionPtr> GetConnections();
+ void BuildOrchid(NYson::IYsonConsumer* consumer);
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, PollerLock_);
+ TTcpDispatcherConfigPtr Config_ = New<TTcpDispatcherConfig>();
+ NConcurrency::IThreadPoolPollerPtr AcceptorPoller_;
+ NConcurrency::IThreadPoolPollerPtr XferPoller_;
+
+ TMpscStack<TWeakPtr<TTcpConnection>> ConnectionsToRegister_;
+ std::vector<TWeakPtr<TTcpConnection>> ConnectionList_;
+ int CurrentConnectionListIndex_ = 0;
+
+ struct TNetworkStatistics
+ {
+ const TBusNetworkCountersPtr Counters = New<TBusNetworkCounters>();
+ };
+
+ NConcurrency::TSyncMap<TString, TNetworkStatistics> NetworkStatistics_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, PeriodicExecutorsLock_);
+ NConcurrency::TPeriodicExecutorPtr ProfilingExecutor_;
+ NConcurrency::TPeriodicExecutorPtr PeriodicCheckExecutor_;
+
+ std::atomic<bool> NetworkingDisabled_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TForkAwareReaderWriterSpinLock, NetworksLock_);
+ std::vector<std::pair<NNet::TIP6Network, TString>> Networks_;
+
+ struct TBandDescriptor
+ {
+ std::atomic<TTosLevel> TosLevel = DefaultTosLevel;
+ };
+
+ TEnumIndexedVector<EMultiplexingBand, TBandDescriptor> BandToDescriptor_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/packet.cpp b/yt/yt/core/bus/tcp/packet.cpp
new file mode 100644
index 0000000000..7b2987757c
--- /dev/null
+++ b/yt/yt/core/bus/tcp/packet.cpp
@@ -0,0 +1,531 @@
+#include "packet.h"
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/misc/checksum.h>
+
+#include <library/cpp/yt/string/guid.h>
+
+#include <library/cpp/yt/memory/chunked_memory_allocator.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr ui32 PacketSignature = 0x78616d4f;
+constexpr ui32 NullPacketPartSize = 0xffffffff;
+
+constexpr int TypicalPacketPartCount = 16;
+constexpr int TypicalVariableHeaderSize = TypicalPacketPartCount * (sizeof(ui32) + sizeof(ui64));
+constexpr i64 PacketDecoderChunkSize = 16_KB;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPacketDecoderTag { };
+
+////////////////////////////////////////////////////////////////////////////////
+
+#pragma pack(push, 4)
+
+struct TPacketHeader
+{
+ // Should be equal to PacketSignature.
+ ui32 Signature;
+ EPacketType Type;
+ EPacketFlags Flags;
+ TPacketId PacketId;
+ ui32 PartCount;
+ ui64 Checksum;
+};
+
+/*
+ Variable-sized header:
+ ui32 PartSizes[PartCount];
+ ui64 PartChecksums[PartCount];
+ ui64 Checksum;
+*/
+
+#pragma pack(pop)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EPacketPhase,
+ (FixedHeader)
+ (VariableHeader)
+ (MessagePart)
+ (Finished)
+);
+
+template <class TDerived>
+class TPacketTranscoderBase
+{
+public:
+ explicit TPacketTranscoderBase(const NLogging::TLogger& logger)
+ : Logger(logger)
+ { }
+
+ TMutableRef GetFragment()
+ {
+ return TMutableRef(FragmentPtr_, FragmentRemaining_);
+ }
+
+ bool IsFinished() const
+ {
+ return Phase_ == EPacketPhase::Finished;
+ }
+
+protected:
+ const NLogging::TLogger& Logger;
+
+ EPacketPhase Phase_ = EPacketPhase::Finished;
+ char* FragmentPtr_ = nullptr;
+ size_t FragmentRemaining_ = 0;
+
+ TPacketHeader FixedHeader_;
+
+ TCompactVector<char, TypicalVariableHeaderSize> VariableHeader_;
+ size_t VariableHeaderSize_;
+ ui32* PartSizes_;
+ ui64* PartChecksums_;
+
+ int PartIndex_ = -1;
+ TSharedRefArray Message_;
+
+ bool IsVariablePacket() const
+ {
+ return FixedHeader_.Type == EPacketType::Message || FixedHeader_.PartCount > 0;
+ }
+
+ void AllocateVariableHeader()
+ {
+ VariableHeaderSize_ =
+ (sizeof(ui32) + sizeof(ui64)) * FixedHeader_.PartCount +
+ sizeof(ui64);
+ VariableHeader_.reserve(VariableHeaderSize_);
+ PartSizes_ = reinterpret_cast<ui32*>(VariableHeader_.data());
+ PartChecksums_ = reinterpret_cast<ui64*>(PartSizes_ + FixedHeader_.PartCount);
+ }
+
+ TChecksum GetFixedChecksum()
+ {
+ return GetChecksum(TRef(&FixedHeader_, sizeof(FixedHeader_) - sizeof(ui64)));
+ }
+
+ TChecksum GetVariableChecksum()
+ {
+ return GetChecksum(TRef(VariableHeader_.data(), VariableHeaderSize_ - sizeof(ui64)));
+ }
+
+ void BeginPhase(EPacketPhase phase, void* fragment, size_t size)
+ {
+ Phase_ = phase;
+ FragmentPtr_ = static_cast<char*>(fragment);
+ FragmentRemaining_ = size;
+ }
+
+ bool EndPhase()
+ {
+ switch (Phase_) {
+ case EPacketPhase::FixedHeader:
+ return AsDerived()->EndFixedHeaderPhase();
+
+ case EPacketPhase::VariableHeader:
+ return AsDerived()->EndVariableHeaderPhase();
+
+ case EPacketPhase::MessagePart:
+ return AsDerived()->EndMessagePartPhase();
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void SetFinished()
+ {
+ Phase_ = EPacketPhase::Finished;
+ FragmentPtr_ = nullptr;
+ FragmentRemaining_ = 0;
+ }
+
+ TDerived* AsDerived()
+ {
+ return static_cast<TDerived*>(this);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPacketDecoder
+ : public IPacketDecoder
+ , public TPacketTranscoderBase<TPacketDecoder>
+{
+public:
+ TPacketDecoder(const NLogging::TLogger& logger, bool verifyChecksum)
+ : TPacketTranscoderBase(logger)
+ , Allocator_(
+ PacketDecoderChunkSize,
+ TChunkedMemoryAllocator::DefaultMaxSmallBlockSizeRatio,
+ GetRefCountedTypeCookie<TPacketDecoderTag>())
+ , VerifyChecksum_(verifyChecksum)
+ {
+ Restart();
+ }
+
+ TMutableRef GetFragment() override
+ {
+ return TPacketTranscoderBase::GetFragment();
+ }
+
+ bool IsFinished() const override
+ {
+ return TPacketTranscoderBase::IsFinished();
+ }
+
+ bool Advance(size_t size) override
+ {
+ YT_ASSERT(FragmentRemaining_ != 0);
+ YT_ASSERT(size <= FragmentRemaining_);
+
+ PacketSize_ += size;
+ FragmentRemaining_ -= size;
+ FragmentPtr_ += size;
+ if (FragmentRemaining_ == 0) {
+ return EndPhase();
+ } else {
+ return true;
+ }
+ }
+
+ void Restart() override
+ {
+ Phase_ = EPacketPhase::FixedHeader;
+ PacketSize_ = 0;
+ Parts_.clear();
+ PartIndex_ = -1;
+ Message_.Reset();
+
+ BeginPhase(EPacketPhase::FixedHeader, &FixedHeader_, sizeof(TPacketHeader));
+ }
+
+ bool IsInProgress() const override
+ {
+ return Phase_ != EPacketPhase::Finished && PacketSize_ > 0;
+ }
+
+ EPacketType GetPacketType() const override
+ {
+ return FixedHeader_.Type;
+ }
+
+ EPacketFlags GetPacketFlags() const override
+ {
+ return FixedHeader_.Flags;
+ }
+
+ TPacketId GetPacketId() const override
+ {
+ return FixedHeader_.PacketId;
+ }
+
+ TSharedRefArray GrabMessage() const override
+ {
+ return std::move(Message_);
+ }
+
+ size_t GetPacketSize() const override
+ {
+ return PacketSize_;
+ }
+
+private:
+ friend class TPacketTranscoderBase<TPacketDecoder>;
+
+ TChunkedMemoryAllocator Allocator_;
+
+ std::vector<TSharedRef> Parts_;
+
+ size_t PacketSize_ = 0;
+
+ const bool VerifyChecksum_;
+
+ bool EndFixedHeaderPhase()
+ {
+ if (FixedHeader_.Signature != PacketSignature) {
+ YT_LOG_ERROR("Packet header signature mismatch (PacketId: %v, ExpectedSignature: %X, ActualSignature: %X)",
+ FixedHeader_.PacketId,
+ PacketSignature,
+ FixedHeader_.Signature);
+ return false;
+ }
+
+ if (FixedHeader_.PartCount > MaxMessagePartCount) {
+ YT_LOG_ERROR("Invalid packet part count (PacketId: %v, PartCount: %v)",
+ FixedHeader_.PacketId,
+ FixedHeader_.PartCount);
+ return false;
+ }
+
+ if (VerifyChecksum_) {
+ auto expectedChecksum = FixedHeader_.Checksum;
+ if (expectedChecksum != NullChecksum) {
+ auto actualChecksum = GetFixedChecksum();
+ if (expectedChecksum != actualChecksum) {
+ YT_LOG_ERROR("Fixed packet header checksum mismatch (PacketId: %v)",
+ FixedHeader_.PacketId);
+ return false;
+ }
+ }
+ }
+
+ if (IsVariablePacket()) {
+ AllocateVariableHeader();
+ BeginPhase(EPacketPhase::VariableHeader, VariableHeader_.data(), VariableHeaderSize_);
+ } else {
+ SetFinished();
+ }
+
+ return true;
+ }
+
+ bool EndVariableHeaderPhase()
+ {
+ if (VerifyChecksum_) {
+ auto expectedChecksum = PartChecksums_[FixedHeader_.PartCount];
+ if (expectedChecksum != NullChecksum) {
+ auto actualChecksum = GetVariableChecksum();
+ if (expectedChecksum != actualChecksum) {
+ YT_LOG_ERROR("Variable packet header checksum mismatch (PacketId: %v)",
+ FixedHeader_.PacketId);
+ return false;
+ }
+ }
+ }
+
+ for (int index = 0; index < static_cast<int>(FixedHeader_.PartCount); ++index) {
+ ui32 partSize = PartSizes_[index];
+ if (partSize != NullPacketPartSize && partSize > MaxMessagePartSize) {
+ YT_LOG_ERROR("Invalid packet part size (PacketId: %v, PartIndex: %v, PartSize: %v)",
+ FixedHeader_.PacketId,
+ index,
+ partSize);
+ return false;
+ }
+ }
+
+ NextMessagePartPhase();
+ return true;
+ }
+
+ bool EndMessagePartPhase()
+ {
+ if (VerifyChecksum_) {
+ auto expectedChecksum = PartChecksums_[PartIndex_];
+ if (expectedChecksum != NullChecksum) {
+ auto actualChecksum = GetChecksum(Parts_[PartIndex_]);
+ if (expectedChecksum != actualChecksum) {
+ YT_LOG_ERROR("Packet part checksum mismatch (PacketId: %v)",
+ FixedHeader_.PacketId);
+ return false;
+ }
+ }
+ }
+
+ NextMessagePartPhase();
+ return true;
+ }
+
+ void NextMessagePartPhase()
+ {
+ while (true) {
+ ++PartIndex_;
+ if (PartIndex_ == static_cast<int>(FixedHeader_.PartCount)) {
+ Message_ = TSharedRefArray(std::move(Parts_), TSharedRefArray::TMoveParts{});
+ SetFinished();
+ break;
+ }
+
+ ui32 partSize = PartSizes_[PartIndex_];
+ if (partSize == NullPacketPartSize) {
+ Parts_.push_back(TSharedRef());
+ } else if (partSize == 0) {
+ Parts_.push_back(TSharedRef::MakeEmpty());
+ } else {
+ auto part = Allocator_.AllocateAligned(partSize);
+ BeginPhase(EPacketPhase::MessagePart, part.Begin(), part.Size());
+ Parts_.push_back(std::move(part));
+ break;
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPacketEncoder
+ : public IPacketEncoder
+ , public TPacketTranscoderBase<TPacketEncoder>
+{
+public:
+ explicit TPacketEncoder(const NLogging::TLogger& logger)
+ : TPacketTranscoderBase(logger)
+ {
+ FixedHeader_.Signature = PacketSignature;
+ }
+
+ TMutableRef GetFragment() override
+ {
+ return TPacketTranscoderBase::GetFragment();
+ }
+
+ bool IsFinished() const override
+ {
+ return TPacketTranscoderBase::IsFinished();
+ }
+
+ size_t GetPacketSize(
+ EPacketType type,
+ const TSharedRefArray& message,
+ size_t payloadSize) override
+ {
+ size_t size = sizeof(TPacketHeader);
+ if (type == EPacketType::Message || !message.Empty()) {
+ size +=
+ message.Size() * (sizeof(ui32) + sizeof(ui64)) +
+ sizeof(ui64) +
+ payloadSize;
+ }
+ return size;
+ }
+
+ bool Start(
+ EPacketType type,
+ EPacketFlags flags,
+ bool generateChecksums,
+ int checksummedPartCount,
+ TPacketId packetId,
+ TSharedRefArray message) override
+ {
+ PartIndex_ = -1;
+ Message_ = std::move(message);
+
+ FixedHeader_.Type = type;
+ FixedHeader_.Flags = flags;
+ FixedHeader_.PacketId = packetId;
+ FixedHeader_.PartCount = Message_.Size();
+ FixedHeader_.Checksum = generateChecksums ? GetFixedChecksum() : NullChecksum;
+
+ if (IsVariablePacket()) {
+ AllocateVariableHeader();
+
+ for (int index = 0; index < static_cast<int>(Message_.Size()); ++index) {
+ const auto& part = Message_[index];
+ if (part) {
+ PartSizes_[index] = part.Size();
+ PartChecksums_[index] =
+ generateChecksums && (index < checksummedPartCount || checksummedPartCount == TSendOptions::AllParts)
+ ? GetChecksum(part)
+ : NullChecksum;
+ } else {
+ PartSizes_[index] = NullPacketPartSize;
+ PartChecksums_[index] = NullChecksum;
+ }
+ }
+
+ PartChecksums_[Message_.Size()] = generateChecksums ? GetVariableChecksum() : NullChecksum;
+ }
+
+ BeginPhase(EPacketPhase::FixedHeader, &FixedHeader_, sizeof (TPacketHeader));
+ return true;
+ }
+
+ bool IsFragmentOwned() const override
+ {
+ return Phase_ == EPacketPhase::MessagePart;
+ }
+
+ void NextFragment() override
+ {
+ EndPhase();
+ }
+
+private:
+ friend class TPacketTranscoderBase<TPacketEncoder>;
+
+ bool EndFixedHeaderPhase()
+ {
+ if (IsVariablePacket()) {
+ BeginPhase(EPacketPhase::VariableHeader, VariableHeader_.data(), VariableHeaderSize_);
+ } else {
+ SetFinished();
+ }
+ return true;
+ }
+
+ bool EndVariableHeaderPhase()
+ {
+ NextMessagePartPhase();
+ return true;
+ }
+
+ bool EndMessagePartPhase()
+ {
+ NextMessagePartPhase();
+ return true;
+ }
+
+ void NextMessagePartPhase()
+ {
+ while (true) {
+ ++PartIndex_;
+ if (PartIndex_ == static_cast<int>(FixedHeader_.PartCount)) {
+ break;
+ }
+
+ const auto& part = Message_[PartIndex_];
+ if (part.Size() != 0) {
+ BeginPhase(EPacketPhase::MessagePart, const_cast<char*>(part.Begin()), part.Size());
+ return;
+ }
+ }
+
+ Message_.Reset();
+ SetFinished();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPacketTranscoderFactory
+ : public IPacketTranscoderFactory
+{
+ std::unique_ptr<IPacketDecoder> CreateDecoder(
+ const NLogging::TLogger& logger,
+ bool verifyChecksum) const override
+ {
+ return std::make_unique<TPacketDecoder>(logger, verifyChecksum);
+ }
+
+ std::unique_ptr<IPacketEncoder> CreateEncoder(
+ const NLogging::TLogger& logger) const override
+ {
+ return std::make_unique<TPacketEncoder>(logger);
+ }
+
+ bool SupportsHandshakes() const override
+ {
+ return true;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPacketTranscoderFactory* GetYTPacketTranscoderFactory()
+{
+ return LeakySingleton<TPacketTranscoderFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
+Y_DECLARE_PODTYPE(NYT::NBus::TPacketHeader);
diff --git a/yt/yt/core/bus/tcp/packet.h b/yt/yt/core/bus/tcp/packet.h
new file mode 100644
index 0000000000..7e8180a99c
--- /dev/null
+++ b/yt/yt/core/bus/tcp/packet.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "private.h"
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EPacketType, i16,
+ ((Message)(0))
+ ((Ack) (1))
+);
+
+DEFINE_BIT_ENUM_WITH_UNDERLYING_TYPE(EPacketFlags, ui16,
+ ((None) (0x0000))
+ ((RequestAcknowledgement) (0x0001))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPacketDecoder
+{
+ virtual ~IPacketDecoder() = default;
+
+ virtual void Restart() = 0;
+
+ virtual bool IsInProgress() const = 0;
+ virtual bool IsFinished() const = 0;
+
+ virtual TMutableRef GetFragment() = 0;
+ virtual bool Advance(size_t size) = 0;
+
+ virtual EPacketType GetPacketType() const = 0;
+ virtual EPacketFlags GetPacketFlags() const = 0;
+ virtual TPacketId GetPacketId() const = 0;
+ virtual size_t GetPacketSize() const = 0;
+ virtual TSharedRefArray GrabMessage() const = 0;
+};
+
+struct IPacketEncoder
+{
+ virtual ~IPacketEncoder() = default;
+
+ virtual size_t GetPacketSize(
+ EPacketType type,
+ const TSharedRefArray& message,
+ size_t payloadSize) = 0;
+
+ virtual bool Start(
+ EPacketType type,
+ EPacketFlags flags,
+ bool generateChecksums,
+ int checksummedPartCount,
+ TPacketId packetId,
+ TSharedRefArray message) = 0;
+
+ virtual TMutableRef GetFragment() = 0;
+ virtual bool IsFragmentOwned() const = 0;
+
+ virtual void NextFragment() = 0;
+
+ virtual bool IsFinished() const = 0;
+};
+
+struct IPacketTranscoderFactory
+{
+ virtual ~IPacketTranscoderFactory() = default;
+
+ virtual std::unique_ptr<IPacketDecoder> CreateDecoder(
+ const NLogging::TLogger& logger,
+ bool verifyChecksum) const = 0;
+ virtual std::unique_ptr<IPacketEncoder> CreateEncoder(
+ const NLogging::TLogger& logger) const = 0;
+
+ virtual bool SupportsHandshakes() const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPacketTranscoderFactory* GetYTPacketTranscoderFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/tcp/private.h b/yt/yt/core/bus/tcp/private.h
new file mode 100644
index 0000000000..a7114a203c
--- /dev/null
+++ b/yt/yt/core/bus/tcp/private.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <yt/yt/core/bus/private.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr ui32 HandshakeMessageSignature = 0x68737562;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/tcp/public.h b/yt/yt/core/bus/tcp/public.h
new file mode 100644
index 0000000000..5e7c54af91
--- /dev/null
+++ b/yt/yt/core/bus/tcp/public.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <yt/yt/core/bus/public.h>
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBusNetworkCounters;
+using TBusNetworkCountersPtr = TIntrusivePtr<TBusNetworkCounters>;
+
+DECLARE_REFCOUNTED_CLASS(TMultiplexingBandConfig)
+DECLARE_REFCOUNTED_CLASS(TTcpDispatcherConfig)
+DECLARE_REFCOUNTED_CLASS(TTcpDispatcherDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TBusConfig)
+DECLARE_REFCOUNTED_CLASS(TBusServerConfig)
+DECLARE_REFCOUNTED_CLASS(TBusClientConfig)
+
+struct IPacketTranscoderFactory;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/tcp/server.cpp b/yt/yt/core/bus/tcp/server.cpp
new file mode 100644
index 0000000000..491fe78344
--- /dev/null
+++ b/yt/yt/core/bus/tcp/server.cpp
@@ -0,0 +1,492 @@
+#include "server.h"
+#include "config.h"
+#include "server.h"
+#include "connection.h"
+#include "dispatcher_impl.h"
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/server.h>
+#include <yt/yt/core/bus/private.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/socket.h>
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/concurrency/pollable_detail.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+#include <cerrno>
+
+namespace NYT::NBus {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpBusServerBase
+ : public TPollableBase
+{
+public:
+ TTcpBusServerBase(
+ TBusServerConfigPtr config,
+ IPollerPtr poller,
+ IMessageHandlerPtr handler,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+ : Config_(std::move(config))
+ , Poller_(std::move(poller))
+ , Handler_(std::move(handler))
+ , PacketTranscoderFactory_(std::move(packetTranscoderFactory))
+ {
+ YT_VERIFY(Config_);
+ YT_VERIFY(Poller_);
+ YT_VERIFY(Handler_);
+
+ if (Config_->Port) {
+ Logger.AddTag("ServerPort: %v", *Config_->Port);
+ }
+ if (Config_->UnixDomainSocketPath) {
+ Logger.AddTag("UnixDomainSocketPath: %v", *Config_->UnixDomainSocketPath);
+ }
+ }
+
+ ~TTcpBusServerBase()
+ {
+ CloseServerSocket();
+ }
+
+ void Start()
+ {
+ OpenServerSocket();
+ if (!Poller_->TryRegister(this)) {
+ CloseServerSocket();
+ THROW_ERROR_EXCEPTION("Cannot register server pollable");
+ }
+ ArmPoller();
+ }
+
+ TFuture<void> Stop()
+ {
+ YT_LOG_INFO("Stopping Bus server");
+ UnarmPoller();
+ return Poller_->Unregister(this).Apply(BIND([this, this_ = MakeStrong(this)] {
+ YT_LOG_INFO("Bus server stopped");
+ }));
+ }
+
+ // IPollable implementation.
+ const TString& GetLoggingTag() const override
+ {
+ return Logger.GetTag();
+ }
+
+ void OnEvent(EPollControl /*control*/) override
+ {
+ OnAccept();
+ }
+
+ void OnShutdown() override
+ {
+ {
+ auto guard = Guard(ControlSpinLock_);
+ CloseServerSocket();
+ }
+
+ decltype(Connections_) connections;
+ {
+ auto guard = WriterGuard(ConnectionsSpinLock_);
+ std::swap(connections, Connections_);
+ }
+
+ for (const auto& connection : connections) {
+ connection->Terminate(TError(
+ NRpc::EErrorCode::TransportError,
+ "Bus server terminated"));
+ }
+ }
+
+protected:
+ const TBusServerConfigPtr Config_;
+ const IPollerPtr Poller_;
+ const IMessageHandlerPtr Handler_;
+
+ IPacketTranscoderFactory* const PacketTranscoderFactory_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ControlSpinLock_);
+ SOCKET ServerSocket_ = INVALID_SOCKET;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, ConnectionsSpinLock_);
+ THashSet<TTcpConnectionPtr> Connections_;
+
+ NLogging::TLogger Logger = BusLogger;
+
+ virtual void CreateServerSocket() = 0;
+
+ virtual void InitClientSocket(SOCKET clientSocket) = 0;
+
+ void OnConnectionTerminated(const TTcpConnectionPtr& connection, const TError& /*error*/)
+ {
+ auto guard = WriterGuard(ConnectionsSpinLock_);
+ // NB: Connection could be missing, see OnShutdown.
+ Connections_.erase(connection);
+ }
+
+ void OpenServerSocket()
+ {
+ auto guard = Guard(ControlSpinLock_);
+
+ YT_LOG_DEBUG("Opening server socket");
+
+ CreateServerSocket();
+
+ try {
+ ListenSocket(ServerSocket_, Config_->MaxBacklogSize);
+ } catch (const std::exception& ex) {
+ CloseServerSocket();
+ throw;
+ }
+
+ YT_LOG_DEBUG("Server socket opened");
+ }
+
+ void CloseServerSocket()
+ {
+ if (ServerSocket_ != INVALID_SOCKET) {
+ CloseSocket(ServerSocket_);
+ if (Config_->UnixDomainSocketPath) {
+ unlink(Config_->UnixDomainSocketPath->c_str());
+ }
+ ServerSocket_ = INVALID_SOCKET;
+ YT_LOG_DEBUG("Server socket closed");
+ }
+ }
+
+ int GetTotalServerConnectionCount(const TString& clientNetwork)
+ {
+ const auto& dispatcher = TTcpDispatcher::TImpl::Get();
+ int result = 0;
+ const auto& counters = dispatcher->GetCounters(clientNetwork);
+ for (auto band : TEnumTraits<EMultiplexingBand>::GetDomainValues()) {
+ result += counters->PerBandCounters[band].ServerConnections.load(std::memory_order::relaxed);
+ }
+ return result;
+ }
+
+ void OnAccept()
+ {
+ while (true) {
+ TNetworkAddress clientAddress;
+ SOCKET clientSocket;
+ try {
+ clientSocket = AcceptSocket(ServerSocket_, &clientAddress);
+ } catch (const std::exception& ex) {
+ YT_LOG_WARNING(ex, "Error accepting client connection");
+ break;
+ }
+
+ if (clientSocket == INVALID_SOCKET) {
+ break;
+ }
+
+ auto rejectConnection = [&] {
+ CloseSocket(clientSocket);
+ };
+
+ auto connectionId = TConnectionId::Create();
+
+ const auto& dispatcher = TTcpDispatcher::TImpl::Get();
+ auto clientNetwork = dispatcher->GetNetworkNameForAddress(clientAddress);
+ auto connectionCount = GetTotalServerConnectionCount(clientNetwork);
+ auto connectionLimit = Config_->MaxSimultaneousConnections;
+ auto formattedClientAddress = ToString(clientAddress, NNet::TNetworkAddressFormatOptions{.IncludePort = false});
+ if (connectionCount >= connectionLimit) {
+ YT_LOG_WARNING("Connection dropped (Address: %v, ConnectionCount: %v, ConnectionLimit: %v)",
+ formattedClientAddress,
+ connectionCount,
+ connectionLimit);
+ rejectConnection();
+ continue;
+ }
+
+ YT_LOG_DEBUG("Connection accepted (ConnectionId: %v, Address: %v, Network: %v, ConnectionCount: %v, ConnectionLimit: %v)",
+ connectionId,
+ formattedClientAddress,
+ clientNetwork,
+ connectionCount,
+ connectionLimit);
+
+ InitClientSocket(clientSocket);
+
+ auto address = ToString(clientAddress);
+ auto endpointDescription = address;
+ auto endpointAttributes = ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("address").Value(address)
+ .Item("network").Value(clientNetwork)
+ .EndMap());
+
+ auto poller = TTcpDispatcher::TImpl::Get()->GetXferPoller();
+
+ auto connection = New<TTcpConnection>(
+ Config_,
+ EConnectionType::Server,
+ connectionId,
+ clientSocket,
+ EMultiplexingBand::Default,
+ endpointDescription,
+ *endpointAttributes,
+ clientAddress,
+ address,
+ std::nullopt,
+ Handler_,
+ std::move(poller),
+ PacketTranscoderFactory_);
+
+ {
+ auto guard = WriterGuard(ConnectionsSpinLock_);
+ YT_VERIFY(Connections_.insert(connection).second);
+ }
+
+ connection->SubscribeTerminated(BIND_NO_PROPAGATE(
+ &TTcpBusServerBase::OnConnectionTerminated,
+ MakeWeak(this),
+ connection));
+
+ connection->Start();
+ }
+ }
+
+ void BindSocket(const TNetworkAddress& address, const TString& errorMessage)
+ {
+ for (int attempt = 1; attempt <= Config_->BindRetryCount; ++attempt) {
+ try {
+ NNet::BindSocket(ServerSocket_, address);
+ return;
+ } catch (const std::exception& ex) {
+ if (attempt == Config_->BindRetryCount) {
+ CloseServerSocket();
+
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::TransportError, errorMessage)
+ << ex;
+ } else {
+ YT_LOG_WARNING(ex, "Error binding socket, starting %v retry", attempt + 1);
+ Sleep(Config_->BindRetryBackoff);
+ }
+ }
+ }
+ }
+
+ void ArmPoller()
+ {
+ auto guard = Guard(ControlSpinLock_);
+ if (ServerSocket_ != INVALID_SOCKET) {
+ Poller_->Arm(ServerSocket_, this, EPollControl::Read | EPollControl::EdgeTriggered);
+ }
+ }
+
+ void UnarmPoller()
+ {
+ auto guard = Guard(ControlSpinLock_);
+ if (ServerSocket_ != INVALID_SOCKET) {
+ Poller_->Unarm(ServerSocket_, this);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRemoteTcpBusServer
+ : public TTcpBusServerBase
+{
+public:
+ using TTcpBusServerBase::TTcpBusServerBase;
+
+private:
+ void CreateServerSocket() override
+ {
+ ServerSocket_ = CreateTcpServerSocket();
+
+ auto serverAddress = TNetworkAddress::CreateIPv6Any(*Config_->Port);
+ BindSocket(serverAddress, Format("Failed to bind a server socket to port %v", Config_->Port));
+ }
+
+ void InitClientSocket(SOCKET clientSocket) override
+ {
+ if (Config_->EnableNoDelay) {
+ if (!TrySetSocketNoDelay(clientSocket)) {
+ YT_LOG_DEBUG("Failed to set socket no delay option");
+ }
+ }
+
+ if (!TrySetSocketKeepAlive(clientSocket)) {
+ YT_LOG_DEBUG("Failed to set socket keep alive option");
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLocalTcpBusServer
+ : public TTcpBusServerBase
+{
+public:
+ TLocalTcpBusServer(
+ TBusServerConfigPtr config,
+ IPollerPtr poller,
+ IMessageHandlerPtr handler,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+ : TTcpBusServerBase(
+ std::move(config),
+ std::move(poller),
+ std::move(handler),
+ packetTranscoderFactory)
+ { }
+
+private:
+ void CreateServerSocket() override
+ {
+ ServerSocket_ = CreateUnixServerSocket();
+
+ {
+ TNetworkAddress netAddress;
+ if (Config_->UnixDomainSocketPath) {
+ // NB(gritukan): Unix domain socket path cannot be longer than 108 symbols, so let's try to shorten it.
+ netAddress = TNetworkAddress::CreateUnixDomainSocketAddress(NFS::GetShortestPath(*Config_->UnixDomainSocketPath));
+ } else {
+ netAddress = GetLocalBusAddress(*Config_->Port);
+ }
+ BindSocket(netAddress, "Failed to bind a local server socket");
+ }
+ }
+
+ void InitClientSocket(SOCKET /*clientSocket*/) override
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A lightweight proxy controlling the lifetime of a TCP bus server.
+/*!
+ * When the last strong reference vanishes, it unregisters the underlying
+ * server instance.
+ */
+template <class TServer>
+class TTcpBusServerProxy
+ : public IBusServer
+{
+public:
+ explicit TTcpBusServerProxy(
+ TBusServerConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+ : Config_(std::move(config))
+ , PacketTranscoderFactory_(packetTranscoderFactory)
+ {
+ YT_VERIFY(Config_);
+ }
+
+ ~TTcpBusServerProxy()
+ {
+ YT_UNUSED_FUTURE(Stop());
+ }
+
+ void Start(IMessageHandlerPtr handler) override
+ {
+ auto server = New<TServer>(
+ Config_,
+ TTcpDispatcher::TImpl::Get()->GetAcceptorPoller(),
+ std::move(handler),
+ PacketTranscoderFactory_);
+
+ Server_.Store(server);
+ server->Start();
+ }
+
+ TFuture<void> Stop() override
+ {
+ if (auto server = Server_.Exchange(nullptr)) {
+ return server->Stop();
+ } else {
+ return VoidFuture;
+ }
+ }
+
+private:
+ const TBusServerConfigPtr Config_;
+
+ IPacketTranscoderFactory* const PacketTranscoderFactory_;
+
+ TAtomicIntrusivePtr<TServer> Server_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompositeBusServer
+ : public IBusServer
+{
+public:
+ explicit TCompositeBusServer(std::vector<IBusServerPtr> servers)
+ : Servers_(std::move(servers))
+ { }
+
+ // IBusServer implementation.
+
+ void Start(IMessageHandlerPtr handler) override
+ {
+ for (const auto& server : Servers_) {
+ server->Start(handler);
+ }
+ }
+
+ TFuture<void> Stop() override
+ {
+ std::vector<TFuture<void>> asyncResults;
+ for (const auto& server : Servers_) {
+ asyncResults.push_back(server->Stop());
+ }
+ return AllSucceeded(asyncResults);
+ }
+
+private:
+ const std::vector<IBusServerPtr> Servers_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IBusServerPtr CreateBusServer(
+ TBusServerConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory)
+{
+ std::vector<IBusServerPtr> servers;
+
+ if (config->Port) {
+ servers.push_back(
+ New<TTcpBusServerProxy<TRemoteTcpBusServer>>(
+ config,
+ packetTranscoderFactory));
+ }
+#ifdef _linux_
+ // Abstract unix sockets are supported only on Linux.
+ servers.push_back(
+ New<TTcpBusServerProxy<TLocalTcpBusServer>>(
+ config,
+ packetTranscoderFactory));
+#endif
+
+ return New<TCompositeBusServer>(std::move(servers));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
+
diff --git a/yt/yt/core/bus/tcp/server.h b/yt/yt/core/bus/tcp/server.h
new file mode 100644
index 0000000000..7dcb351e10
--- /dev/null
+++ b/yt/yt/core/bus/tcp/server.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "public.h"
+
+#include "packet.h"
+
+namespace NYT::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IBusServerPtr CreateBusServer(
+ TBusServerConfigPtr config,
+ IPacketTranscoderFactory* packetTranscoderFactory = GetYTPacketTranscoderFactory());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/unittests/bus_ut.cpp b/yt/yt/core/bus/unittests/bus_ut.cpp
new file mode 100644
index 0000000000..1bf05df99c
--- /dev/null
+++ b/yt/yt/core/bus/unittests/bus_ut.cpp
@@ -0,0 +1,367 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/client.h>
+#include <yt/yt/core/bus/server.h>
+
+#include <yt/yt/core/bus/tcp/config.h>
+#include <yt/yt/core/bus/tcp/client.h>
+#include <yt/yt/core/bus/tcp/server.h>
+
+#include <yt/yt/core/net/socket.h>
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <library/cpp/testing/common/network.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+
+namespace NYT::NBus {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRefArray CreateMessage(int numParts, int partSize = 1)
+{
+ auto data = TSharedMutableRef::Allocate(numParts * partSize);
+
+ std::vector<TSharedRef> parts;
+ for (int i = 0; i < numParts; ++i) {
+ parts.push_back(data.Slice(i * partSize, (i + 1) * partSize));
+ }
+
+ return TSharedRefArray(std::move(parts), TSharedRefArray::TMoveParts{});
+}
+
+TSharedRefArray Serialize(TString str)
+{
+ return TSharedRefArray(TSharedRef::FromString(str));
+}
+
+TString Deserialize(TSharedRefArray message)
+{
+ YT_ASSERT(message.Size() == 1);
+ const auto& part = message[0];
+ return TString(part.Begin(), part.Size());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEmptyBusHandler
+ : public IMessageHandler
+{
+public:
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr replyBus) noexcept override
+ {
+ Y_UNUSED(message);
+ Y_UNUSED(replyBus);
+ }
+};
+
+class TCountingBusHandler
+ : public IMessageHandler
+{
+public:
+ void HandleMessage(
+ TSharedRefArray /*message*/,
+ IBusPtr /*replyBus*/) noexcept override
+ {
+ Count++;
+ }
+
+ std::atomic<int> Count = 0;
+};
+
+class TReplying42BusHandler
+ : public IMessageHandler
+{
+public:
+ TReplying42BusHandler(int numParts)
+ : NumPartsExpecting(numParts)
+ { }
+
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr replyBus) noexcept override
+ {
+ EXPECT_EQ(NumPartsExpecting, std::ssize(message));
+ auto replyMessage = Serialize("42");
+ replyBus->Send(replyMessage, NBus::TSendOptions(EDeliveryTrackingLevel::None));
+ }
+private:
+ int NumPartsExpecting;
+};
+
+class TChecking42BusHandler
+ : public IMessageHandler
+{
+public:
+ explicit TChecking42BusHandler(int numRepliesWaiting)
+ : NumRepliesWaiting(numRepliesWaiting)
+ { }
+
+ void WaitUntilDone()
+ {
+ Event_.Wait();
+ }
+
+private:
+ std::atomic<int> NumRepliesWaiting;
+ NThreading::TEvent Event_;
+
+
+ void HandleMessage(
+ TSharedRefArray message,
+ IBusPtr /*replyBus*/) noexcept override
+ {
+ auto value = Deserialize(message);
+ EXPECT_EQ("42", value);
+
+ if (--NumRepliesWaiting == 0) {
+ Event_.NotifyAll();
+ }
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusTest
+ : public testing::Test
+{
+public:
+ NTesting::TPortHolder Port;
+ TString Address;
+
+ TBusTest()
+ {
+ Port = NTesting::GetFreePort();
+ Address = Format("localhost:%v", Port);
+ }
+
+ IBusServerPtr StartBusServer(IMessageHandlerPtr handler)
+ {
+ auto config = TBusServerConfig::CreateTcp(Port);
+ auto server = CreateBusServer(config);
+ server->Start(handler);
+ return server;
+ }
+
+ void TestReplies(int numRequests, int numParts, EDeliveryTrackingLevel level = EDeliveryTrackingLevel::Full)
+ {
+ auto server = StartBusServer(New<TReplying42BusHandler>(numParts));
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Address));
+ auto handler = New<TChecking42BusHandler>(numRequests);
+ auto bus = client->CreateBus(handler);
+ auto message = CreateMessage(numParts);
+
+ std::vector<TFuture<void>> results;
+ for (int i = 0; i < numRequests; ++i) {
+ auto result = bus->Send(message, NBus::TSendOptions(level));
+ if (result) {
+ results.push_back(result);
+ }
+ }
+
+ for (const auto& result : results) {
+ auto error = result.Get();
+ EXPECT_TRUE(error.IsOK());
+ }
+
+ handler->WaitUntilDone();
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TBusTest, ConfigDefaultConstructor)
+{
+ auto config = New<TBusClientConfig>();
+}
+
+TEST_F(TBusTest, CreateBusClientConfig)
+{
+ auto config = TBusClientConfig::CreateTcp(Address);
+ EXPECT_EQ(Address, *config->Address);
+ EXPECT_FALSE(config->UnixDomainSocketPath);
+}
+
+TEST_F(TBusTest, CreateUdsBusClientConfig)
+{
+ auto config = TBusClientConfig::CreateUds("unix-socket");
+ EXPECT_EQ("unix-socket", *config->UnixDomainSocketPath);
+}
+
+TEST_F(TBusTest, OK)
+{
+ auto server = StartBusServer(New<TEmptyBusHandler>());
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Address));
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ auto message = CreateMessage(1);
+ auto result = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full))
+ .Get();
+ EXPECT_TRUE(result.IsOK());
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TBusTest, Terminate)
+{
+ auto server = StartBusServer(New<TEmptyBusHandler>());
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Address));
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ auto message = CreateMessage(1);
+
+ auto terminated = NewPromise<void>();
+ bus->SubscribeTerminated(
+ BIND([&] (const TError& error) {
+ terminated.Set(error);
+ }));
+ auto error = TError(TErrorCode(54321), "Terminated");
+ bus->Terminate(error);
+ bus->Terminate(TError(TErrorCode(12345), "Ignored"));
+ EXPECT_EQ(terminated.Get().GetCode(), error.GetCode());
+ bus->Terminate(TError(TErrorCode(12345), "Ignored"));
+
+ auto result = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full));
+ EXPECT_TRUE(result.IsSet());
+ EXPECT_EQ(result.Get().GetCode(), error.GetCode());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TBusTest, TerminateBeforeAccept)
+{
+ /* make blocking server socket */
+ auto serverSocket = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
+ EXPECT_NE(serverSocket, INVALID_SOCKET);
+ NNet::SetReuseAddrFlag(serverSocket);
+ NNet::BindSocket(serverSocket, NNet::TNetworkAddress::CreateIPv6Loopback(Port));
+ NNet::ListenSocket(serverSocket, 0);
+
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Address));
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+
+ auto terminated = NewPromise<void>();
+ bus->SubscribeTerminated(
+ BIND([&] (const TError& error) {
+ terminated.Set(error);
+ }));
+ auto error = TError(TErrorCode(54321), "Terminated");
+ bus->Terminate(error);
+ EXPECT_FALSE(terminated.IsSet());
+
+ NNet::TNetworkAddress clientAddress;
+ auto clientSocket = NNet::AcceptSocket(serverSocket, &clientAddress);
+ EXPECT_NE(clientSocket, INVALID_SOCKET);
+
+ EXPECT_EQ(terminated.Get().GetCode(), error.GetCode());
+
+ NNet::CloseSocket(clientSocket);
+ NNet::CloseSocket(serverSocket);
+}
+
+TEST_F(TBusTest, Failed)
+{
+ auto port = NTesting::GetFreePort();
+
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Format("localhost:%v", port)));
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ auto message = CreateMessage(1);
+ auto result = bus->Send(message, NBus::TSendOptions(EDeliveryTrackingLevel::Full)).Get();
+ EXPECT_FALSE(result.IsOK());
+}
+
+TEST_F(TBusTest, BlackHole)
+{
+ auto server = StartBusServer(New<TEmptyBusHandler>());
+ auto config = TBusClientConfig::CreateTcp(Address);
+
+ config->ReadStallTimeout = TDuration::Seconds(1);
+
+ auto client = CreateBusClient(config);
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ auto message = CreateMessage(1);
+ auto options = TSendOptions(EDeliveryTrackingLevel::Full);
+
+ bus->Send(message, options)
+ .Get()
+ .ThrowOnError();
+
+ bus->SetTosLevel(BlackHoleTosLevel);
+
+ auto result = bus->Send(message, options).Get();
+ EXPECT_FALSE(result.IsOK());
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TBusTest, SendCancel)
+{
+ auto handler = New<TCountingBusHandler>();
+ auto server = StartBusServer(handler);
+ auto client = CreateBusClient(TBusClientConfig::CreateTcp(Address));
+ auto bus = client->CreateBus(New<TEmptyBusHandler>());
+ auto message = CreateMessage(4, 16_MB);
+
+ auto options = NBus::TSendOptions(EDeliveryTrackingLevel::Full);
+ options.EnableSendCancelation = true;
+
+ for (int i = 0; i < 16; i++) {
+ auto future = bus->Send(message, options);
+ future.Cancel(TError("Canceled"));
+ }
+
+ Sleep(TDuration::Seconds(1));
+ ASSERT_LE(handler->Count, 16);
+ handler->Count = 0;
+
+ options.TrackingLevel = EDeliveryTrackingLevel::None;
+ for (int i = 0; i < 2; i++) {
+ bus->Send(message, options).Cancel(TError("Canceled"));
+ }
+
+ Sleep(TDuration::Seconds(1));
+ ASSERT_LE(handler->Count, 16);
+ handler->Count = 0;
+
+ server->Stop()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TBusTest, OneReplyNoTracking)
+{
+ TestReplies(1, 1, EDeliveryTrackingLevel::None);
+}
+
+TEST_F(TBusTest, OneReplyFullTracking)
+{
+ TestReplies(1, 1, EDeliveryTrackingLevel::Full);
+}
+
+TEST_F(TBusTest, OneReplyErrorOnlyTracking)
+{
+ TestReplies(1, 1, EDeliveryTrackingLevel::ErrorOnly);
+}
+
+TEST_F(TBusTest, ManyReplies)
+{
+ TestReplies(1000, 100);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NBus
diff --git a/yt/yt/core/bus/unittests/ya.make b/yt/yt/core/bus/unittests/ya.make
new file mode 100644
index 0000000000..1c8bcf49f8
--- /dev/null
+++ b/yt/yt/core/bus/unittests/ya.make
@@ -0,0 +1,41 @@
+GTEST(unittester-core-bus)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ bus_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/crypto
+ yt/yt/core/test_framework
+ library/cpp/testing/common
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/compression/brotli.cpp b/yt/yt/core/compression/brotli.cpp
new file mode 100644
index 0000000000..55e101c7e9
--- /dev/null
+++ b/yt/yt/core/compression/brotli.cpp
@@ -0,0 +1,69 @@
+#include "brotli.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <library/cpp/streams/brotli/brotli.h>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = CompressionLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void BrotliCompress(int level, TSource* source, TBlob* output)
+{
+ ui64 totalInputSize = source->Available();
+ output->Resize(sizeof(totalInputSize), /*initializeStorage*/ false);
+
+ // Write input size that will be used during decompression.
+ TMemoryOutput memoryOutput(output->Begin(), sizeof(totalInputSize));
+ WritePod(memoryOutput, totalInputSize);
+
+ TBlobSink sink(output);
+ try {
+ TBrotliCompress compress(&sink, level);
+ while (source->Available() > 0) {
+ size_t read;
+ const char* ptr = source->Peek(&read);
+ if (read > 0) {
+ compress.Write(ptr, read);
+ source->Skip(read);
+ }
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Brotli compression failed");
+ }
+}
+
+void BrotliDecompress(TSource* source, TBlob* output)
+{
+ ui64 outputSize;
+ ReadPod(*source, outputSize);
+
+ output->Resize(outputSize, /*initializeStorage*/ false);
+
+ TBrotliDecompress decompress(source);
+ ui64 remainingSize = outputSize;
+ while (remainingSize > 0) {
+ ui64 offset = outputSize - remainingSize;
+ ui64 read = decompress.Read(output->Begin() + offset, remainingSize);
+ if (read == 0) {
+ break;
+ }
+ remainingSize -= read;
+ }
+
+ if (remainingSize != 0) {
+ THROW_ERROR_EXCEPTION("Brotli decompression failed: input stream is not fully consumed")
+ << TErrorAttribute("remaining_size", remainingSize);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/brotli.h b/yt/yt/core/compression/brotli.h
new file mode 100644
index 0000000000..cbf7ad3705
--- /dev/null
+++ b/yt/yt/core/compression/brotli.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void BrotliCompress(int level, TSource* source, TBlob* output);
+void BrotliDecompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/bzip2.cpp b/yt/yt/core/compression/bzip2.cpp
new file mode 100644
index 0000000000..90e85216ae
--- /dev/null
+++ b/yt/yt/core/compression/bzip2.cpp
@@ -0,0 +1,133 @@
+#include "bzip2.h"
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <contrib/libs/libbz2/bzlib.h>
+
+#include <util/generic/utility.h>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+constexpr size_t MinBlobSize = 1024;
+
+ui64 GetTotalOut(const bz_stream& bzStream)
+{
+ ui64 result = bzStream.total_out_hi32;
+ result <<= 32;
+ result |= bzStream.total_out_lo32;
+ return result;
+}
+
+void PeekInputBytes(TSource* source, bz_stream* bzStream)
+{
+ size_t bufLen;
+ const char* buf = source->Peek(&bufLen);
+ bufLen = std::min(source->Available(), bufLen);
+ // const_cast is due to C API, data will not be modified.
+ bzStream->next_in = const_cast<char*>(buf);
+ bzStream->avail_in = bufLen;
+}
+
+void DirectOutputToBlobEnd(TBlob* blob, bz_stream* bzStream)
+{
+ if (blob->Size() == blob->Capacity()) {
+ YT_VERIFY(blob->Capacity() >= MinBlobSize);
+
+ constexpr double growFactor = 1.5;
+ blob->Reserve(growFactor * blob->Capacity());
+ }
+ bzStream->next_out = blob->End();
+ bzStream->avail_out = blob->Capacity() - blob->Size();
+}
+
+void ActualizeOutputBlobSize(TBlob* blob, bz_stream* bzStream)
+{
+ ui64 totalOut = GetTotalOut(*bzStream);
+ YT_VERIFY(totalOut >= blob->Size());
+ blob->Resize(totalOut, /*initializeStorage*/ false);
+}
+
+} // namespace
+
+void Bzip2Compress(TSource* source, TBlob* output, int level)
+{
+ YT_VERIFY(source);
+ YT_VERIFY(output);
+ YT_VERIFY(1 <= level && level <= 9);
+
+ bz_stream bzStream;
+ Zero(bzStream);
+ int ret = BZ2_bzCompressInit(&bzStream, level, 0, 0);
+ YT_VERIFY(ret == BZ_OK);
+ auto finally = Finally([&] { BZ2_bzCompressEnd(&bzStream); });
+
+ output->Reserve(std::max(
+ MinBlobSize,
+ source->Available() / 8)); // just a heuristic
+ output->Resize(0);
+ while (source->Available()) {
+ PeekInputBytes(source, &bzStream);
+ size_t peekedSize = bzStream.avail_in;
+
+ DirectOutputToBlobEnd(output, &bzStream);
+
+ ret = BZ2_bzCompress(&bzStream, BZ_RUN);
+ YT_VERIFY(ret == BZ_RUN_OK);
+
+ ActualizeOutputBlobSize(output, &bzStream);
+
+ size_t processedInputSize = peekedSize - bzStream.avail_in;
+ source->Skip(processedInputSize);
+ }
+
+ do {
+ DirectOutputToBlobEnd(output, &bzStream);
+
+ ret = BZ2_bzCompress(&bzStream, BZ_FINISH);
+ YT_VERIFY(ret == BZ_FINISH_OK || ret == BZ_STREAM_END);
+
+ ActualizeOutputBlobSize(output, &bzStream);
+ } while (ret != BZ_STREAM_END);
+}
+
+void Bzip2Decompress(TSource* source, TBlob* output)
+{
+ output->Reserve(std::max(MinBlobSize, source->Available()));
+ output->Resize(0);
+ while (source->Available() > 0) {
+ bz_stream bzStream{};
+
+ YT_VERIFY(BZ2_bzDecompressInit(&bzStream, 0, 0) == BZ_OK);
+
+ auto finallyGuard = Finally([&] {
+ BZ2_bzDecompressEnd(&bzStream);
+ });
+
+ int result;
+ do {
+ PeekInputBytes(source, &bzStream);
+ size_t peekedSize = bzStream.avail_in;
+
+ DirectOutputToBlobEnd(output, &bzStream);
+
+ result = BZ2_bzDecompress(&bzStream);
+ if (result != BZ_OK && result != BZ_STREAM_END) {
+ THROW_ERROR_EXCEPTION("BZip2 decompression failed: BZ2_bzDecompress returned an error")
+ << TErrorAttribute("error", result);
+ }
+
+ ActualizeOutputBlobSize(output, &bzStream);
+
+ size_t processedInputSize = peekedSize - bzStream.avail_in;
+ source->Skip(processedInputSize);
+ } while (result != BZ_STREAM_END);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/bzip2.h b/yt/yt/core/compression/bzip2.h
new file mode 100644
index 0000000000..44d84adf5d
--- /dev/null
+++ b/yt/yt/core/compression/bzip2.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Bzip2Compress(TSource* source, TBlob* output, int level);
+void Bzip2Decompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/codec.cpp b/yt/yt/core/compression/codec.cpp
new file mode 100644
index 0000000000..36e789e26d
--- /dev/null
+++ b/yt/yt/core/compression/codec.cpp
@@ -0,0 +1,457 @@
+#include "codec.h"
+#include "bzip2.h"
+#include "lz.h"
+#include "lzma.h"
+#include "snappy.h"
+#include "zlib.h"
+#include "zstd.h"
+#include "brotli.h"
+
+#include <util/generic/algorithm.h>
+
+namespace NYT::NCompression {
+
+using namespace NDetail;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TCodec>
+struct TCompressedBlockTag { };
+
+template <class TCodec>
+struct TDecompressedBlockTag { };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TCodec>
+class TCodecBase
+ : public ICodec
+{
+public:
+ TSharedRef Compress(const TSharedRef& block) override
+ {
+ return Run(&TCodec::DoCompress, GetRefCountedTypeCookie<TCompressedBlockTag<TCodec>>(), block);
+ }
+
+ TSharedRef Compress(const std::vector<TSharedRef>& blocks) override
+ {
+ return Run(&TCodec::DoCompress, GetRefCountedTypeCookie<TCompressedBlockTag<TCodec>>(), blocks);
+ }
+
+ TSharedRef Decompress(const TSharedRef& block) override
+ {
+ return Run(&TCodec::DoDecompress, GetRefCountedTypeCookie<TDecompressedBlockTag<TCodec>>(), block);
+ }
+
+ TSharedRef Decompress(const std::vector<TSharedRef>& blocks) override
+ {
+ return Run(&TCodec::DoDecompress, GetRefCountedTypeCookie<TDecompressedBlockTag<TCodec>>(), blocks);
+ }
+
+private:
+ TSharedRef Run(
+ void (TCodec::*converter)(TSource* source, TBlob* output),
+ TRefCountedTypeCookie blobCookie,
+ const TSharedRef& ref)
+ {
+ TRefSource input(ref);
+ auto outputBlob = TBlob(blobCookie, 0, false);
+ (static_cast<TCodec*>(this)->*converter)(&input, &outputBlob);
+ return FinalizeBlob(&outputBlob, blobCookie);
+ }
+
+ TSharedRef Run(
+ void (TCodec::*converter)(TSource* source, TBlob* output),
+ TRefCountedTypeCookie blobCookie,
+ const std::vector<TSharedRef>& refs)
+ {
+ if (refs.size() == 1) {
+ return Run(converter, blobCookie, refs.front());
+ }
+
+ TRefsVectorSource input(refs);
+ auto outputBlob = TBlob(blobCookie, 0, false);
+ (static_cast<TCodec*>(this)->*converter)(&input, &outputBlob);
+ return FinalizeBlob(&outputBlob, blobCookie);
+ }
+
+ static TSharedRef FinalizeBlob(TBlob* blob, TRefCountedTypeCookie blobCookie)
+ {
+ // For blobs smaller than 16K, do nothing.
+ // For others, allow up to 5% capacity overhead.
+ if (blob->Capacity() >= 16_KB &&
+ blob->Capacity() >= 1.05 * blob->Size())
+ {
+ *blob = TBlob(blobCookie, blob->ToRef());
+ }
+ return TSharedRef::FromBlob(std::move(*blob));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNoneCodec
+ : public ICodec
+{
+public:
+ TSharedRef Compress(const TSharedRef& block) override
+ {
+ return block;
+ }
+
+ TSharedRef Compress(const std::vector<TSharedRef>& blocks) override
+ {
+ return MergeRefsToRef<TCompressedBlockTag<TNoneCodec>>(blocks);
+ }
+
+ TSharedRef Decompress(const TSharedRef& block) override
+ {
+ return block;
+ }
+
+ TSharedRef Decompress(const std::vector<TSharedRef>& blocks) override
+ {
+ return MergeRefsToRef<TDecompressedBlockTag<TNoneCodec>>(blocks);
+ }
+
+ ECodec GetId() const override
+ {
+ return ECodec::None;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSnappyCodec
+ : public TCodecBase<TSnappyCodec>
+{
+public:
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ SnappyCompress(source, output);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ SnappyDecompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ return ECodec::Snappy;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZlibCodec
+ : public TCodecBase<TZlibCodec>
+{
+public:
+ explicit TZlibCodec(int level)
+ : Level_(level)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ ZlibCompress(Level_, source, output);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ ZlibDecompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ switch (Level_) {
+
+#define CASE(level) case level: return PP_CONCAT(ECodec::Zlib_, level);
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CASE
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+private:
+ const int Level_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLz4Codec
+ : public TCodecBase<TLz4Codec>
+{
+public:
+ explicit TLz4Codec(bool highCompression)
+ : HighCompression_(highCompression)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ Lz4Compress(source, output, HighCompression_);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ Lz4Decompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ return HighCompression_ ? ECodec::Lz4HighCompression : ECodec::Lz4;
+ }
+
+private:
+ const bool HighCompression_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZstdCodec
+ : public TCodecBase<TZstdCodec>
+{
+public:
+ explicit TZstdCodec(int level)
+ : Level_(level)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ ZstdCompress(Level_, source, output);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ ZstdDecompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ switch (Level_) {
+
+#define CASE(level) case level: return PP_CONCAT(ECodec::Zstd_, level);
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9)(10)(11)(12)(13)(14)(15)(16)(17)(18)(19)(20)(21))
+#undef CASE
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+private:
+ const int Level_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBrotliCodec
+ : public TCodecBase<TBrotliCodec>
+{
+public:
+ explicit TBrotliCodec(int level)
+ : Level_(level)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ BrotliCompress(Level_, source, output);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ BrotliDecompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ switch (Level_) {
+
+#define CASE(level) case level: return PP_CONCAT(ECodec::Brotli_, level);
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9)(10)(11))
+#undef CASE
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+private:
+ const int Level_;
+};
+
+class TLzmaCodec
+ : public TCodecBase<TLzmaCodec>
+{
+public:
+ explicit TLzmaCodec(int level)
+ : Level_(level)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ LzmaCompress(Level_, source, output);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ LzmaDecompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ switch (Level_) {
+
+#define CASE(level) case level: return PP_CONCAT(ECodec::Lzma_, level);
+ PP_FOR_EACH(CASE, (0)(1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CASE
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+private:
+ const int Level_;
+};
+
+class TBzip2Codec
+ : public TCodecBase<TBzip2Codec>
+{
+public:
+ explicit TBzip2Codec(int level)
+ : Level_(level)
+ { }
+
+ void DoCompress(TSource* source, TBlob* output)
+ {
+ Bzip2Compress(source, output, Level_);
+ }
+
+ void DoDecompress(TSource* source, TBlob* output)
+ {
+ Bzip2Decompress(source, output);
+ }
+
+ ECodec GetId() const override
+ {
+ switch (Level_) {
+
+#define CASE(level) case level: return PP_CONCAT(ECodec::Bzip2_, level);
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CASE
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+private:
+ const int Level_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ICodec* GetCodec(ECodec id)
+{
+ switch (id) {
+ case ECodec::None: {
+ static TNoneCodec result;
+ return &result;
+ }
+
+ case ECodec::Snappy: {
+ static TSnappyCodec result;
+ return &result;
+ }
+
+ case ECodec::Lz4: {
+ static TLz4Codec result(false);
+ return &result;
+ }
+
+ case ECodec::Lz4HighCompression: {
+ static TLz4Codec result(true);
+ return &result;
+ }
+
+#define CASE(param) \
+ case ECodec::PP_CONCAT(CODEC, PP_CONCAT(_, param)): { \
+ static PP_CONCAT(T, PP_CONCAT(CODEC, Codec)) result(param); \
+ return &result; \
+ }
+
+#define CODEC Zlib
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CODEC
+
+#define CODEC Brotli
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9)(10)(11))
+#undef CODEC
+
+#define CODEC Lzma
+ PP_FOR_EACH(CASE, (0)(1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CODEC
+
+#define CODEC Bzip2
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9))
+#undef CODEC
+
+#define CODEC Zstd
+ PP_FOR_EACH(CASE, (1)(2)(3)(4)(5)(6)(7)(8)(9)(10)(11)(12)(13)(14)(15)(16)(17)(18)(19)(20)(21))
+#undef CODEC
+
+#undef CASE
+
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported compression codec %Qlv",
+ id);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const THashSet<ECodec>& GetDeprecatedCodecIds()
+{
+ static const THashSet<ECodec> deprecatedCodecIds{
+ ECodec::QuickLz
+ };
+ return deprecatedCodecIds;
+}
+
+const THashMap<TString, TString>& GetDeprecatedCodecNameToAlias()
+{
+ static const THashMap<TString, TString> deprecatedCodecNameToAlias = {
+ {"zlib6", FormatEnum(ECodec::Zlib_6)},
+ {"gzip_normal", FormatEnum(ECodec::Zlib_6)},
+ {"zlib9", FormatEnum(ECodec::Zlib_9)},
+ {"gzip_best_compression", FormatEnum(ECodec::Zlib_9)},
+ {"zstd", FormatEnum(ECodec::Zstd_3)},
+ {"brotli3", FormatEnum(ECodec::Brotli_3)},
+ {"brotli5", FormatEnum(ECodec::Brotli_5)},
+ {"brotli8", FormatEnum(ECodec::Brotli_8)}
+ };
+ return deprecatedCodecNameToAlias;
+}
+
+const std::vector<ECodec>& GetSupportedCodecIds()
+{
+ static const std::vector<ECodec> supportedCodecIds = [] {
+ std::vector<ECodec> supportedCodecIds;
+ for (auto codecId : TEnumTraits<ECodec>::GetDomainValues()) {
+ if (!GetDeprecatedCodecIds().contains(codecId)) {
+ supportedCodecIds.push_back(codecId);
+ }
+ }
+ SortUnique(supportedCodecIds);
+ return supportedCodecIds;
+ }();
+ return supportedCodecIds;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression
+
diff --git a/yt/yt/core/compression/codec.h b/yt/yt/core/compression/codec.h
new file mode 100644
index 0000000000..57483645e5
--- /dev/null
+++ b/yt/yt/core/compression/codec.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NCompression {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A generic interface for compression/decompression.
+struct ICodec
+{
+ virtual ~ICodec() = default;
+
+ //! Compress a given block.
+ virtual TSharedRef Compress(const TSharedRef& block) = 0;
+
+ //! Compress a vector of blocks.
+ virtual TSharedRef Compress(const std::vector<TSharedRef>& blocks) = 0;
+
+ //! Decompress a given block.
+ virtual TSharedRef Decompress(const TSharedRef& block) = 0;
+
+ //! Decompress a vector of blocks.
+ virtual TSharedRef Decompress(const std::vector<TSharedRef>& blocks) = 0;
+
+ //! Returns codec id
+ virtual ECodec GetId() const = 0;
+};
+
+//! Returns a codec for the registered id.
+ICodec* GetCodec(ECodec id);
+
+//! Deprecated codecs information.
+const THashSet<ECodec>& GetDeprecatedCodecIds();
+const THashMap<TString, TString>& GetDeprecatedCodecNameToAlias();
+const std::vector<ECodec>& GetSupportedCodecIds();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression
+
diff --git a/yt/yt/core/compression/lz.cpp b/yt/yt/core/compression/lz.cpp
new file mode 100644
index 0000000000..bab1a3c39a
--- /dev/null
+++ b/yt/yt/core/compression/lz.cpp
@@ -0,0 +1,303 @@
+#include "private.h"
+#include "public.h"
+#include "lz.h"
+
+#define LZ4_DISABLE_DEPRECATE_WARNINGS
+
+#include <contrib/libs/lz4/lz4.h>
+#include <contrib/libs/lz4/lz4hc.h>
+
+namespace NYT::NCompression::NDetail {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * V0 wire format has no header at all.
+ * Wire format goes simply as a sequence of blocks, each block is annotated
+ * with a header of type TBlockHeader.
+ *
+ * V1 wire format has a preceding header which stores total uncompressed size
+ * in 31-bit integer (sic!). Header structure is:
+ *
+ * { i32 Signature; i32 Size; }
+ *
+ * V2 wire format has a preceding header which stores total uncompressed size
+ * in 64-bit integer. Header structure is:
+ *
+ * { ui32 Signature; ui32 Padding; ui64 Size; }
+ *
+ */
+
+struct THeader
+{
+ static constexpr ui32 SignatureV1 = (1 << 30) + 1;
+ static constexpr ui32 SignatureV2 = (1 << 30) + 2;
+
+ ui32 Signature = static_cast<ui32>(-1);
+ ui32 Size = 0;
+};
+
+struct TBlockHeader
+{
+ ui32 CompressedSize = 0;
+ ui32 UncompressedSize = 0;
+};
+
+static_assert(
+ sizeof(THeader) == sizeof(TBlockHeader),
+ "Header and block header must be same size for backward compatibility.");
+
+static constexpr size_t MaxLzBlockSize = 1_GB;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NCompression
+
+Y_DECLARE_PODTYPE(NYT::NCompression::NDetail::THeader);
+Y_DECLARE_PODTYPE(NYT::NCompression::NDetail::TBlockHeader);
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+struct TLz4CompressedTag
+{ };
+
+bool IsExtendedHeader(size_t totalUncompressedSize)
+{
+ return totalUncompressedSize > std::numeric_limits<i32>::max();
+}
+
+template <class TEstimateCompressedSizeFn>
+size_t GenericEstimateTotalCompressedSize(
+ size_t totalUncompressedSize,
+ TEstimateCompressedSizeFn&& estimateCompressedSizeFn)
+{
+ size_t result = sizeof(THeader);
+ if (IsExtendedHeader(totalUncompressedSize)) {
+ result += sizeof(ui64);
+ }
+ // Estimate number of blocks, assuming that source feeds data in large chunks.
+ size_t quotient = totalUncompressedSize / MaxLzBlockSize;
+ if (quotient > 0) {
+ result += quotient * (sizeof(TBlockHeader) + estimateCompressedSizeFn(MaxLzBlockSize));
+ }
+ size_t remainder = totalUncompressedSize - quotient * MaxLzBlockSize;
+ if (remainder > 0) {
+ result += sizeof(TBlockHeader) + estimateCompressedSizeFn(remainder);
+ }
+ return result;
+}
+
+template <class TEstimateCompressedSizeFn, class TCompressFn>
+void GenericBlockCompress(
+ TSource* source,
+ TBlob* sink,
+ TEstimateCompressedSizeFn&& estimateCompressedSizeFn,
+ TCompressFn&& compressFn)
+{
+ size_t totalUncompressedSize = source->Available();
+ size_t totalCompressedSizeBound = GenericEstimateTotalCompressedSize(totalUncompressedSize, estimateCompressedSizeFn);
+
+ sink->Reserve(totalCompressedSizeBound);
+ YT_ASSERT(sink->IsEmpty());
+
+ size_t currentPosition = 0;
+
+ // Write out header.
+ if (IsExtendedHeader(totalUncompressedSize)) {
+ THeader header;
+ header.Signature = THeader::SignatureV2;
+ header.Size = 0;
+ TMemoryOutput memoryOutput(sink->Begin() + currentPosition, sizeof(THeader) + sizeof(ui64));
+ WritePod(memoryOutput, header);
+ WritePod(memoryOutput, static_cast<ui64>(totalUncompressedSize)); // Force serialization type.
+ currentPosition += sizeof(THeader) + sizeof(ui64);
+ } else {
+ THeader header;
+ header.Signature = THeader::SignatureV1;
+ header.Size = static_cast<ui32>(totalUncompressedSize);
+ TMemoryOutput memoryOutput(sink->Begin() + currentPosition, sizeof(THeader));
+ WritePod(memoryOutput, header);
+ currentPosition += sizeof(THeader);
+ }
+
+ // Write out body.
+ while (totalUncompressedSize > 0) {
+ YT_VERIFY(source->Available() == totalUncompressedSize);
+
+ size_t inputSize = 0, inputOffset = 0;
+ const char* input = source->Peek(&inputSize);
+
+ inputSize = std::min(inputSize, totalUncompressedSize);
+
+ while (inputSize > 0) {
+ ui32 uncompressedSize = static_cast<ui32>(std::min(MaxLzBlockSize, inputSize));
+ ui32 compressedSizeBound = estimateCompressedSizeFn(uncompressedSize);
+
+ // XXX(sandello): We can underestimate output buffer size if source feeds data in tiny chunks.
+ sink->Reserve(currentPosition + sizeof(TBlockHeader) + compressedSizeBound);
+
+ auto compressedSize = compressFn(
+ input + inputOffset,
+ sink->Begin() + currentPosition + sizeof(TBlockHeader),
+ uncompressedSize);
+ // Non-positive return values indicate an error during compression.
+ YT_VERIFY(compressedSize > 0);
+ // COMPAT(sandello): Since block header may be interpreted as global header we have to make sure
+ // that we never produce forward-incompatible signature (which aliases to compressed size).
+ // Currently we treat all values <= 2^30 as proper sizes and all other values as signatures.
+ YT_VERIFY(compressedSize <= static_cast<int>(MaxLzBlockSize));
+
+ TBlockHeader header;
+ header.CompressedSize = static_cast<ui32>(compressedSize);
+ header.UncompressedSize = static_cast<ui32>(uncompressedSize);
+
+ TMemoryOutput memoryOutput(sink->Begin() + currentPosition, sizeof(TBlockHeader));
+ WritePod(memoryOutput, header);
+
+ currentPosition += sizeof(TBlockHeader);
+ currentPosition += header.CompressedSize;
+ sink->Resize(currentPosition, /*initializeStorage*/ false);
+
+ inputSize -= uncompressedSize;
+ inputOffset += uncompressedSize;
+ }
+
+ source->Skip(inputOffset);
+ totalUncompressedSize -= inputOffset;
+ }
+
+ YT_VERIFY(source->Available() == 0);
+}
+
+template <class TTag, class TDecompressFn>
+void GenericBlockDecompress(TSource* source, TBlob* sink, TDecompressFn&& decompressFn)
+{
+ if (source->Available() == 0) {
+ return;
+ }
+
+ ui64 totalUncompressedSize = 0;
+ bool oldStyle = false;
+ bool hasBlockHeader = false;
+
+ TBlockHeader blockHeader;
+
+ {
+ THeader header;
+ ReadPod(*source, header);
+ switch (header.Signature) {
+ case THeader::SignatureV1:
+ totalUncompressedSize = header.Size;
+ break;
+
+ case THeader::SignatureV2:
+ ReadPod(*source, totalUncompressedSize);
+ break;
+
+ default:
+ // COMPAT(ignat): Needed to read old-style blocks.
+ blockHeader.CompressedSize = header.Signature;
+ blockHeader.UncompressedSize = header.Size;
+ oldStyle = true;
+ hasBlockHeader = true;
+ totalUncompressedSize = 0;
+ break;
+ }
+ }
+
+ sink->Reserve(totalUncompressedSize);
+ YT_ASSERT(sink->IsEmpty());
+
+ TBlob inputBuffer(GetRefCountedTypeCookie<TTag>(), 0, false);
+
+ while (source->Available() > 0) {
+ if (Y_UNLIKELY(hasBlockHeader)) {
+ hasBlockHeader = false;
+ } else {
+ ReadPod(*source, blockHeader);
+ }
+
+ size_t oldSize = sink->Size();
+ size_t newSize = oldSize + blockHeader.UncompressedSize;
+ sink->Resize(newSize, /*initializeStorage*/ false);
+
+ size_t inputSize;
+ const char* input = source->Peek(&inputSize);
+
+ inputSize = std::min(inputSize, source->Available());
+
+ // Fast-path; omit extra copy and feed data directly from source buffer.
+ bool hasCompleteBlock = inputSize >= blockHeader.CompressedSize;
+
+ if (!hasCompleteBlock) {
+ // Slow-path; coalesce input into a single slice.
+ inputBuffer.Resize(blockHeader.CompressedSize, /*initializeStorage*/ false);
+ ReadRef(*source, TMutableRef(inputBuffer.Begin(), inputBuffer.Size()));
+ input = inputBuffer.Begin();
+ }
+
+ decompressFn(input, blockHeader.CompressedSize, sink->Begin() + oldSize, blockHeader.UncompressedSize);
+
+ if (hasCompleteBlock) {
+ source->Skip(blockHeader.CompressedSize);
+ }
+ }
+
+ if (!oldStyle) {
+ YT_VERIFY(sink->Size() == totalUncompressedSize);
+ }
+}
+
+} // namespace
+
+void Lz4Compress(TSource* source, TBlob* sink, bool highCompression)
+{
+ auto compressFn = highCompression ? LZ4_compressHC : LZ4_compress;
+
+ GenericBlockCompress(
+ source,
+ sink,
+ [] (size_t size) {
+ return static_cast<size_t>(LZ4_compressBound(static_cast<int>(size)));
+ },
+ compressFn);
+}
+
+void Lz4Decompress(TSource* source, TBlob* sink)
+{
+ GenericBlockDecompress<TLz4CompressedTag>(
+ source,
+ sink,
+ [] (const char* input, size_t inputSize, char* output, size_t outputSize) {
+ if (inputSize > Max<int>()) {
+ THROW_ERROR_EXCEPTION("LZ4 decompression failed: input size is too big")
+ << TErrorAttribute("size", inputSize);
+ }
+ if (outputSize > Max<int>()) {
+ THROW_ERROR_EXCEPTION("LZ4 decompression failed: output size is too big")
+ << TErrorAttribute("size", outputSize);
+ }
+ int rv = LZ4_decompress_safe(input, output, inputSize, static_cast<int>(outputSize));
+ if (rv < 0) {
+ THROW_ERROR_EXCEPTION("LZ4 decompression failed: LZ4_decompress_safe returned an error")
+ << TErrorAttribute("error", rv);
+ }
+ if (rv != static_cast<int>(outputSize)) {
+ THROW_ERROR_EXCEPTION("LZ4 decompression failed: output size mismatch")
+ << TErrorAttribute("expected_size", inputSize)
+ << TErrorAttribute("actual_size", rv);
+ }
+ }
+ );
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/lz.h b/yt/yt/core/compression/lz.h
new file mode 100644
index 0000000000..3bf0c5760e
--- /dev/null
+++ b/yt/yt/core/compression/lz.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Lz4Compress(TSource* source, TBlob* sink, bool highCompression);
+void Lz4Decompress(TSource* source, TBlob* sink);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/lzma.cpp b/yt/yt/core/compression/lzma.cpp
new file mode 100644
index 0000000000..6b4dde6725
--- /dev/null
+++ b/yt/yt/core/compression/lzma.cpp
@@ -0,0 +1,219 @@
+#include "lzma.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/finally.h>
+
+extern "C" {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#include <contrib/libs/lzmasdk/LzmaEnc.h>
+#include <contrib/libs/lzmasdk/LzmaDec.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // extern "C"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+const auto& Logger = CompressionLogger;
+
+// ISzAlloc is an interface containing alloc/free functions (with its own signatures)
+// required by lzma API.
+class TSzAlloc
+ : public ISzAlloc
+{
+public:
+ TSzAlloc()
+ {
+ Alloc = &MallocWrap;
+ Free = &FreeWrap;
+ }
+
+ static void* MallocWrap(const ISzAlloc*, size_t len)
+ {
+ return malloc(len);
+ }
+
+ static void FreeWrap(const ISzAlloc*, void* ptr)
+ {
+ free(ptr);
+ }
+};
+
+TSzAlloc Alloc;
+
+class TLzmaSinkWrapper
+ : public ISeqOutStream
+{
+public:
+ explicit TLzmaSinkWrapper(TBlob* output)
+ : Output_(output)
+ {
+ Write = &TLzmaSinkWrapper::WriteDataProc;
+ }
+
+ size_t WriteData(const void* buffer, size_t size)
+ {
+ Output_->Append(buffer, size);
+ return size;
+ }
+
+ static size_t WriteDataProc(const ISeqOutStream* lzmaWriteWrapper, const void* buffer, size_t size)
+ {
+ TLzmaSinkWrapper* pThis = const_cast<TLzmaSinkWrapper*>(static_cast<const TLzmaSinkWrapper*>(lzmaWriteWrapper));
+ return pThis->WriteData(buffer, size);
+ }
+
+private:
+ TBlob* const Output_;
+};
+
+class TLzmaSourceWrapper
+ : public ISeqInStream
+{
+public:
+ explicit TLzmaSourceWrapper(TSource* source)
+ : Source_(source)
+ {
+ Read = ReadDataProc;
+ }
+
+ SRes ReadData(void* buffer, size_t* size)
+ {
+ size_t peekedSize;
+ const void* peekedData = Source_->Peek(&peekedSize);
+ peekedSize = std::min(*size, peekedSize);
+ peekedSize = std::min(peekedSize, Source_->Available());
+
+ memcpy(buffer, peekedData, peekedSize);
+
+ Source_->Skip(peekedSize);
+ *size = peekedSize;
+ return SZ_OK;
+ }
+
+ static SRes ReadDataProc(const ISeqInStream* lzmaReadWrapper, void* buffer, size_t* size)
+ {
+ TLzmaSourceWrapper* pThis = const_cast<TLzmaSourceWrapper*>(static_cast<const TLzmaSourceWrapper*>(lzmaReadWrapper));
+ return pThis->ReadData(buffer, size);
+ }
+
+private:
+ TSource* const Source_;
+};
+
+} // namespace
+
+void LzmaCompress(int level, TSource* source, TBlob* output)
+{
+ YT_VERIFY(0 <= level && level <= 9);
+
+ TLzmaSourceWrapper reader(source);
+ TLzmaSinkWrapper writer(output);
+
+ auto handle = LzmaEnc_Create(&Alloc);
+ YT_VERIFY(handle);
+
+ auto finallyGuard = Finally([&] {
+ LzmaEnc_Destroy(handle, &Alloc, &Alloc);
+ });
+
+ auto checkError = [] (SRes result) {
+ YT_LOG_FATAL_IF(result != SZ_OK, "Lzma compression failed (Error: %v)",
+ result);
+ };
+
+ {
+ // Set properties.
+ CLzmaEncProps props;
+ LzmaEncProps_Init(&props);
+ props.level = level;
+ props.writeEndMark = 1;
+ checkError(LzmaEnc_SetProps(handle, &props));
+ }
+
+ {
+ // Write properties.
+ Byte propsBuffer[LZMA_PROPS_SIZE];
+ size_t propsBufferSize = LZMA_PROPS_SIZE;
+ checkError(LzmaEnc_WriteProperties(handle, propsBuffer, &propsBufferSize));
+ writer.WriteData(propsBuffer, sizeof(propsBuffer));
+ }
+
+ // Compress data.
+ checkError(LzmaEnc_Encode(handle, &writer, &reader, nullptr, &Alloc, &Alloc));
+}
+
+void LzmaDecompress(TSource* source, TBlob* output)
+{
+ Byte propsBuffer[LZMA_PROPS_SIZE];
+ ReadRef(*source, TMutableRef(reinterpret_cast<char*>(propsBuffer), sizeof(propsBuffer)));
+
+ CLzmaDec handle;
+ LzmaDec_Construct(&handle);
+
+ {
+ auto result = LzmaDec_Allocate(&handle, propsBuffer, LZMA_PROPS_SIZE, &Alloc);
+ if(result != SZ_OK) {
+ THROW_ERROR_EXCEPTION("Lzma decompression failed: LzmaDec_Allocate returned an error")
+ << TErrorAttribute("error", result);
+ }
+ }
+
+ auto finallyGuard = Finally([&] {
+ LzmaDec_Free(&handle, &Alloc);
+ });
+
+ LzmaDec_Init(&handle);
+
+ auto status = LZMA_STATUS_NOT_FINISHED;
+ while (source->Available() > 0) {
+ size_t sourceDataSize;
+ const Byte* sourceData = reinterpret_cast<const Byte*>(source->Peek(&sourceDataSize));
+
+ sourceDataSize = std::min(sourceDataSize, source->Available());
+
+ size_t oldDicPos = handle.dicPos;
+ size_t bufferSize = sourceDataSize;
+
+ {
+ auto result = LzmaDec_DecodeToDic(
+ &handle,
+ handle.dicBufSize,
+ sourceData,
+ &bufferSize, // It's input buffer size before call, read byte count afterwards.
+ LZMA_FINISH_ANY,
+ &status);
+ if(result != SZ_OK) {
+ THROW_ERROR_EXCEPTION("Lzma decompression failed: LzmaDec_DecodeToDic returned an error")
+ << TErrorAttribute("error", result);
+ }
+ }
+
+ output->Append(handle.dic + oldDicPos, handle.dicPos - oldDicPos);
+
+ sourceData += bufferSize;
+ sourceDataSize -= bufferSize;
+
+ // Strange lzma api requires us to update this index by hand.
+ if (handle.dicPos == handle.dicBufSize) {
+ handle.dicPos = 0;
+ }
+
+ source->Skip(bufferSize);
+ }
+
+ if (status != LZMA_STATUS_FINISHED_WITH_MARK) {
+ THROW_ERROR_EXCEPTION("Lzma decompression failed: unexpected final status")
+ << TErrorAttribute("status", static_cast<int>(status));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/lzma.h b/yt/yt/core/compression/lzma.h
new file mode 100644
index 0000000000..edb3cd2cf3
--- /dev/null
+++ b/yt/yt/core/compression/lzma.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void LzmaCompress(int level, TSource* source, TBlob* output);
+void LzmaDecompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
+
diff --git a/yt/yt/core/compression/private.h b/yt/yt/core/compression/private.h
new file mode 100644
index 0000000000..da62a73fce
--- /dev/null
+++ b/yt/yt/core/compression/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NCompression {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger CompressionLogger("Compression");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression
diff --git a/yt/yt/core/compression/public.cpp b/yt/yt/core/compression/public.cpp
new file mode 100644
index 0000000000..e8556f148d
--- /dev/null
+++ b/yt/yt/core/compression/public.cpp
@@ -0,0 +1,10 @@
+#include "public.h"
+
+namespace NYT::NCompression {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NChunkClient
+
diff --git a/yt/yt/core/compression/public.h b/yt/yt/core/compression/public.h
new file mode 100644
index 0000000000..c0921f70a8
--- /dev/null
+++ b/yt/yt/core/compression/public.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NCompression {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICodec;
+
+DEFINE_AMBIGUOUS_ENUM_WITH_UNDERLYING_TYPE(ECodec, i8,
+ ((None) (0))
+ ((Snappy) (1))
+ ((Lz4) (4))
+ ((Lz4HighCompression) (5))
+
+ ((Brotli_1) (11))
+ ((Brotli_2) (12))
+ ((Brotli_3) (8))
+ ((Brotli_4) (13))
+ ((Brotli_5) (9))
+ ((Brotli_6) (14))
+ ((Brotli_7) (15))
+ ((Brotli_8) (10))
+ ((Brotli_9) (16))
+ ((Brotli_10) (17))
+ ((Brotli_11) (18))
+
+ ((Zlib_1) (19))
+ ((Zlib_2) (20))
+ ((Zlib_3) (21))
+ ((Zlib_4) (22))
+ ((Zlib_5) (23))
+ ((Zlib_6) (2))
+ ((Zlib_7) (24))
+ ((Zlib_8) (25))
+ ((Zlib_9) (3))
+
+ ((Zstd_1) (26))
+ ((Zstd_2) (27))
+ ((Zstd_3) (28))
+ ((Zstd_4) (29))
+ ((Zstd_5) (30))
+ ((Zstd_6) (31))
+ ((Zstd_7) (32))
+ ((Zstd_8) (33))
+ ((Zstd_9) (34))
+ ((Zstd_10) (35))
+ ((Zstd_11) (36))
+ ((Zstd_12) (37))
+ ((Zstd_13) (38))
+ ((Zstd_14) (39))
+ ((Zstd_15) (40))
+ ((Zstd_16) (41))
+ ((Zstd_17) (42))
+ ((Zstd_18) (43))
+ ((Zstd_19) (44))
+ ((Zstd_20) (45))
+ ((Zstd_21) (46))
+
+ ((Lzma_0) (47))
+ ((Lzma_1) (48))
+ ((Lzma_2) (49))
+ ((Lzma_3) (50))
+ ((Lzma_4) (51))
+ ((Lzma_5) (52))
+ ((Lzma_6) (53))
+ ((Lzma_7) (54))
+ ((Lzma_8) (55))
+ ((Lzma_9) (56))
+
+ ((Bzip2_1) (57))
+ ((Bzip2_2) (58))
+ ((Bzip2_3) (59))
+ ((Bzip2_4) (60))
+ ((Bzip2_5) (61))
+ ((Bzip2_6) (62))
+ ((Bzip2_7) (63))
+ ((Bzip2_8) (64))
+ ((Bzip2_9) (65))
+
+ // Deprecated
+ ((Zlib6) (2))
+ ((GzipNormal) (2))
+ ((Zlib9) (3))
+ ((GzipBestCompression) (3))
+ ((Zstd) (28))
+ ((Brotli3) (8))
+ ((Brotli5) (9))
+ ((Brotli8) (10))
+ ((QuickLz) (6))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression
diff --git a/yt/yt/core/compression/snappy.cpp b/yt/yt/core/compression/snappy.cpp
new file mode 100644
index 0000000000..7ecd4966d0
--- /dev/null
+++ b/yt/yt/core/compression/snappy.cpp
@@ -0,0 +1,104 @@
+#include "snappy.h"
+
+#include <contrib/libs/snappy/snappy-stubs-internal.h>
+#include <contrib/libs/snappy/snappy.h>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPreloadingSource
+ : public TSource
+{
+public:
+ explicit TPreloadingSource(TSource* source)
+ : Source_(source)
+ , Length_(std::min(Source_->Available(), Buffer_.size()))
+ {
+ ReadRef(*Source_, TMutableRef(Buffer_.data(), Length_));
+ }
+
+ size_t Available() const override
+ {
+ return Source_->Available() + Length_ - Position_;
+ }
+
+ const char* Peek(size_t* length) override
+ {
+ if (Y_UNLIKELY(Position_ < Length_)) {
+ *length = Length_ - Position_;
+ return Buffer_.begin() + Position_;
+ } else {
+ return Source_->Peek(length);
+ }
+ }
+
+ void Skip(size_t length) override
+ {
+ if (Y_UNLIKELY(Position_ < Length_)) {
+ auto delta = std::min(length, Length_ - Position_);
+ Position_ += delta;
+ length -= delta;
+ }
+ if (length > 0) {
+ Source_->Skip(length);
+ }
+ }
+
+ const char* begin() const
+ {
+ return Buffer_.begin();
+ }
+
+ const char* end() const
+ {
+ return Buffer_.begin() + Length_;
+ }
+
+private:
+ TSource* const Source_;
+ std::array<char, snappy::Varint::kMax32> Buffer_;
+ size_t Length_ = 0;
+ size_t Position_ = 0;
+};
+
+void SnappyCompress(TSource* source, TBlob* output)
+{
+ // Snappy implementation relies on entire input length to fit into an integer.
+ if (source->Available() > std::numeric_limits<int>::max()) {
+ THROW_ERROR_EXCEPTION("Snappy compression failed: input size is too big")
+ << TErrorAttribute("size", source->Available());
+ }
+
+ output->Resize(snappy::MaxCompressedLength(source->Available()), /*initializeStorage*/ false);
+ snappy::UncheckedByteArraySink writer(output->Begin());
+ size_t compressedSize = snappy::Compress(source, &writer);
+ output->Resize(compressedSize);
+}
+
+void SnappyDecompress(TSource* source, TBlob* output)
+{
+ // Empty input leads to an empty output in Snappy.
+ if (source->Available() == 0) {
+ return;
+ }
+
+ // We hack into Snappy implementation to preallocate appropriate buffer.
+ ui32 uncompressedSize = 0;
+ TPreloadingSource preloadingSource(source);
+ snappy::Varint::Parse32WithLimit(
+ preloadingSource.begin(),
+ preloadingSource.end(),
+ &uncompressedSize);
+
+ output->Resize(uncompressedSize, /*initializeStorage*/ false);
+
+ if (!snappy::RawUncompress(&preloadingSource, output->Begin())) {
+ THROW_ERROR_EXCEPTION("Snappy compression failed: RawUncompress returned an error");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/snappy.h b/yt/yt/core/compression/snappy.h
new file mode 100644
index 0000000000..877613ca53
--- /dev/null
+++ b/yt/yt/core/compression/snappy.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SnappyCompress(TSource* source, TBlob* output);
+void SnappyDecompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/stream.cpp b/yt/yt/core/compression/stream.cpp
new file mode 100644
index 0000000000..3c4ee31df0
--- /dev/null
+++ b/yt/yt/core/compression/stream.cpp
@@ -0,0 +1,107 @@
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t TSource::DoRead(void* buffer, size_t size)
+{
+ size_t peekedLen;
+ const char* peekedPtr = Peek(&peekedLen);
+ auto toRead = Min(size, peekedLen, Available());
+ ::memcpy(buffer, peekedPtr, toRead);
+ Skip(toRead);
+ return toRead;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSink::DoWrite(const void* buf, size_t size)
+{
+ Append(static_cast<const char*>(buf), size);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRefSource::TRefSource(TRef block)
+ : Block_(block)
+ , Available_(Block_.Size())
+{ }
+
+size_t TRefSource::Available() const
+{
+ return Available_;
+}
+
+const char* TRefSource::Peek(size_t* len)
+{
+ *len = Block_.Size() - Position_;
+ return Block_.Begin() + Position_;
+}
+
+void TRefSource::Skip(size_t len)
+{
+ size_t toSkip = std::min(Available_, len);
+ Position_ += toSkip;
+ Available_ -= toSkip;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRefsVectorSource::TRefsVectorSource(const std::vector<TSharedRef>& blocks)
+ : Blocks_(blocks)
+ , Available_(GetByteSize(blocks))
+{
+ SkipCompletedBlocks();
+}
+
+size_t TRefsVectorSource::Available() const
+{
+ return Available_;
+}
+
+const char* TRefsVectorSource::Peek(size_t* len)
+{
+ if (Index_ == Blocks_.size()) {
+ *len = 0;
+ return nullptr;
+ }
+ *len = Blocks_[Index_].Size() - Position_;
+ return Blocks_[Index_].Begin() + Position_;
+}
+
+void TRefsVectorSource::Skip(size_t len)
+{
+ while (len > 0 && Index_ < Blocks_.size()) {
+ size_t toSkip = std::min(Blocks_[Index_].Size() - Position_, len);
+
+ Position_ += toSkip;
+ SkipCompletedBlocks();
+
+ len -= toSkip;
+ Available_ -= toSkip;
+ }
+}
+
+void TRefsVectorSource::SkipCompletedBlocks()
+{
+ while (Index_ < Blocks_.size() && Position_ == Blocks_[Index_].Size()) {
+ ++Index_;
+ Position_ = 0;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBlobSink::TBlobSink(TBlob* output)
+ : Output_(output)
+{ }
+
+void TBlobSink::Append(const char* data, size_t size)
+{
+ Output_->Append(data, size);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/stream.h b/yt/yt/core/compression/stream.h
new file mode 100644
index 0000000000..eb7ab8df9c
--- /dev/null
+++ b/yt/yt/core/compression/stream.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <contrib/libs/snappy/snappy-sinksource.h>
+#include <contrib/libs/snappy/snappy.h>
+
+#include <array>
+#include <vector>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSource
+ : public snappy::Source
+ , public IInputStream
+{
+public:
+ using Source::Skip;
+
+protected:
+ size_t DoRead(void* buffer, size_t size) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSink
+ : public snappy::Sink
+ , public IOutputStream
+{
+protected:
+ void DoWrite(const void *buf, size_t size) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRefSource
+ : public TSource
+{
+public:
+ explicit TRefSource(TRef block);
+
+ size_t Available() const override;
+ const char* Peek(size_t* len) override;
+ void Skip(size_t len) override;
+
+private:
+ const TRef Block_;
+ size_t Available_;
+ size_t Position_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRefsVectorSource
+ : public TSource
+{
+public:
+ explicit TRefsVectorSource(const std::vector<TSharedRef>& blocks);
+
+ size_t Available() const override;
+ const char* Peek(size_t* len) override;
+ void Skip(size_t len) override;
+
+private:
+ void SkipCompletedBlocks();
+
+ const std::vector<TSharedRef>& Blocks_;
+ size_t Available_;
+ size_t Index_ = 0;
+ size_t Position_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBlobSink
+ : public TSink
+{
+public:
+ explicit TBlobSink(TBlob* output);
+
+ void Append(const char* data, size_t size) override;
+
+private:
+ TBlob* const Output_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/unittests/codec_ut.cpp b/yt/yt/core/compression/unittests/codec_ut.cpp
new file mode 100644
index 0000000000..43923cca71
--- /dev/null
+++ b/yt/yt/core/compression/unittests/codec_ut.cpp
@@ -0,0 +1,123 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <contrib/libs/snappy/snappy-sinksource.h>
+#include <contrib/libs/snappy/snappy.h>
+
+namespace NYT::NCompression {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCodecTest
+ : public ::testing::TestWithParam<std::tuple<ECodec, ui64>>
+{
+protected:
+ ICodec* TheCodec()
+ {
+ return GetCodec(std::get<0>(GetParam()));
+ }
+
+ size_t ThePartSize()
+ {
+ return std::get<1>(GetParam());
+ }
+
+ void TestCase(const std::vector<TString>& pieces)
+ {
+ std::vector<TSharedRef> refs;
+ size_t length = 0;
+
+ for (const auto& piece : pieces) {
+ refs.push_back(TSharedRef::FromString(piece));
+ length += piece.length();
+ }
+
+ auto compressed = TheCodec()->Compress(refs).Split(ThePartSize());
+ auto decompressed = TheCodec()->Decompress(compressed);
+
+ ASSERT_EQ(length, decompressed.Size());
+
+ size_t offset = 0;
+ for (const auto& piece : pieces) {
+ auto actualSharedRef = decompressed.Slice(offset, offset + piece.length());
+ auto actualStringBuf = TStringBuf(actualSharedRef.begin(), actualSharedRef.end());
+ auto expectedStringBuf = TStringBuf(piece.begin(), piece.end());
+
+ EXPECT_EQ(expectedStringBuf, actualStringBuf);
+
+ offset += piece.length();
+ }
+ }
+};
+
+TEST_P(TCodecTest, HelloWorld)
+{
+ TestCase({"hello world"});
+}
+
+TEST_P(TCodecTest, 64KB)
+{
+ TestCase({TString(64 * 1024, 'a')});
+}
+
+TEST_P(TCodecTest, 1MB)
+{
+ TestCase({TString(1 * 1024 * 1024, 'a')});
+}
+
+TEST_P(TCodecTest, VectorHelloWorld)
+{
+ TestCase({
+ "", "", "hello",
+ "", "", "world",
+ "", "", TString(10000, 'a'),
+ "", "", TString(10000, 'b'),
+ "", ""});
+}
+
+TEST_P(TCodecTest, VectorEmptyRefs)
+{
+ TestCase({"", "", ""});
+}
+
+TEST_P(TCodecTest, VectorSingleCharacters)
+{
+ std::vector<TString> input(1000, "a");
+ TestCase(input);
+}
+
+TEST_P(TCodecTest, VectorExpBuffers)
+{
+ std::vector<TString> input;
+ for (int i = 0; i < 15; ++i) {
+ input.emplace_back(1 << i, 'a' + i);
+ }
+ TestCase(input);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ TCodecTest,
+ ::testing::Combine(
+ ::testing::ValuesIn(GetSupportedCodecIds()),
+ ::testing::ValuesIn(std::vector<ui64>({static_cast<ui64>(-1), 1, 1024}))),
+ [] (const ::testing::TestParamInfo<std::tuple<ECodec, ui64>>& info) -> std::string {
+ return
+ "Codec_" +
+ std::string(TEnumTraits<ECodec>::ToString(std::get<0>(info.param)).c_str()) + "_PartSize_" +
+ ::testing::PrintToString(std::get<1>(info.param));
+ });
+
+TEST_F(TCodecTest, QuickLZDeprecated)
+{
+ EXPECT_THROW_WITH_SUBSTRING(
+ GetCodec(ECodec::QuickLz),
+ "Unsupported compression codec \"quick_lz\"");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NCompression
diff --git a/yt/yt/core/compression/unittests/stream_ut.cpp b/yt/yt/core/compression/unittests/stream_ut.cpp
new file mode 100644
index 0000000000..d20c788855
--- /dev/null
+++ b/yt/yt/core/compression/unittests/stream_ut.cpp
@@ -0,0 +1,93 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/random.h>
+
+#include <library/cpp/streams/brotli/brotli.h>
+
+namespace NYT::NCompression {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TCompressStream>
+TString Compress(TString data)
+{
+ TString compressed;
+ TStringOutput output(compressed);
+ TCompressStream compressStream(&output, 11);
+ compressStream.Write(data.data(), data.size());
+ compressStream.Finish();
+ output.Finish();
+ return compressed;
+}
+
+template <typename TDecompressStream>
+TString Decompress(TString data)
+{
+ TStringInput input(data);
+ TDecompressStream decompressStream(&input);
+ return decompressStream.ReadAll();
+}
+
+template <typename TCompressStream, typename TDecompressStream>
+void TestCase(const TString& s)
+{
+ EXPECT_EQ(s, Decompress<TDecompressStream>(Compress<TCompressStream>(s)));
+}
+
+TString GenerateRandomString(size_t size)
+{
+ TRandomGenerator generator(42);
+ TString result;
+ result.reserve(size + sizeof(ui64));
+ while (result.size() < size) {
+ ui64 value = generator.Generate<ui64>();
+ result += TStringBuf(reinterpret_cast<const char*>(&value), sizeof(value));
+ }
+ result.resize(size);
+ return result;
+}
+
+TEST(TBrotliStreamTest, HelloWorld)
+{
+ TestCase<TBrotliCompress, TBrotliDecompress>("hello world");
+}
+
+TEST(TBrotliStreamTest, SeveralStreams)
+{
+ auto s1 = GenerateRandomString(1 << 15);
+ auto s2 = GenerateRandomString(1 << 15);
+ auto c1 = Compress<TBrotliCompress>(s1);
+ auto c2 = Compress<TBrotliCompress>(s2);
+ EXPECT_EQ(s1 + s2, Decompress<TBrotliDecompress>(c1 + c2));
+}
+
+TEST(TBrotliStreamTest, IncompleteStream)
+{
+ TString manyAs(64 * 1024, 'a');
+ auto compressed = Compress<TBrotliCompress>(manyAs);
+ TString truncated(compressed.Data(), compressed.Size() - 1);
+ EXPECT_THROW(Decompress<TBrotliDecompress>(truncated), std::exception);
+}
+
+TEST(TBrotliStreamTest, 64KB)
+{
+ auto manyAs = TString(64 * 1024, 'a');
+ TString str("Hello from the Matrix!@#% How are you?}{\n\t\a");
+ TestCase<TBrotliCompress, TBrotliDecompress>(manyAs + str + manyAs);
+}
+
+TEST(TBrotliStreamTest, 1MB)
+{
+ TestCase<TBrotliCompress, TBrotliDecompress>(GenerateRandomString(1 * 1024 * 1024));
+}
+
+TEST(TBrotliStreamTest, Empty)
+{
+ TestCase<TBrotliCompress, TBrotliDecompress>("");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NCompression
diff --git a/yt/yt/core/compression/unittests/ya.make b/yt/yt/core/compression/unittests/ya.make
new file mode 100644
index 0000000000..04b0750ac9
--- /dev/null
+++ b/yt/yt/core/compression/unittests/ya.make
@@ -0,0 +1,40 @@
+GTEST(unittester-core-compression)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ codec_ut.cpp
+ stream_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/compression/zlib.cpp b/yt/yt/core/compression/zlib.cpp
new file mode 100644
index 0000000000..ee096829c0
--- /dev/null
+++ b/yt/yt/core/compression/zlib.cpp
@@ -0,0 +1,155 @@
+#include "zlib.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/serialize.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <zlib.h>
+
+#include <array>
+#include <iostream>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr size_t MaxZlibUInt = std::numeric_limits<uInt>::max();
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ZlibCompress(int level, TSource* source, TBlob* output)
+{
+ z_stream stream{};
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.opaque = Z_NULL;
+
+ int ret = deflateInit(&stream, level);
+ YT_VERIFY(ret == Z_OK);
+
+ auto finallyGuard = Finally([&] {
+ deflateEnd(&stream);
+ });
+
+ size_t totalUncompressedSize = source->Available();
+ size_t totalCompressedSize = deflateBound(&stream, totalUncompressedSize);
+
+ // Write out header.
+ output->Reserve(sizeof(ui64) + totalCompressedSize);
+ output->Resize(sizeof(ui64), /*initializeStorage*/ false);
+ {
+ TMemoryOutput memoryOutput(output->Begin(), sizeof(ui64));
+ WritePod(memoryOutput, static_cast<ui64>(totalUncompressedSize)); // Force serialization type.
+ }
+
+ // Write out body.
+ do {
+ size_t inputAvailable;
+ const char* inputNext = source->Peek(&inputAvailable);
+ inputAvailable = std::min(inputAvailable, source->Available());
+ inputAvailable = std::min(inputAvailable, MaxZlibUInt);
+
+ stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(inputNext));
+ stream.avail_in = static_cast<uInt>(inputAvailable);
+
+ int flush = (stream.avail_in == source->Available()) ? Z_FINISH : Z_NO_FLUSH;
+
+ do {
+ size_t outputAvailable = output->Capacity() - output->Size();
+ if (outputAvailable == 0) {
+ // XXX(sandello): Somehow we have missed our buffer estimate.
+ // Reallocate it to provide enough capacity.
+ size_t sourceAvailable = source->Available() - inputAvailable + stream.avail_in;
+ outputAvailable = deflateBound(&stream, sourceAvailable);
+ output->Reserve(output->Size() + outputAvailable);
+ }
+ outputAvailable = std::min(outputAvailable, MaxZlibUInt);
+
+ stream.next_out = reinterpret_cast<Bytef*>(output->End());
+ stream.avail_out = static_cast<uInt>(outputAvailable);
+
+ ret = deflate(&stream, flush);
+ // We should not throw exception here since caller does not expect such behavior.
+ YT_VERIFY(ret == Z_OK || ret == Z_STREAM_END);
+
+ output->Resize(output->Size() + outputAvailable - stream.avail_out, /*initializeStorage*/ false);
+ } while (stream.avail_out == 0);
+
+ // Entire input was consumed.
+ YT_VERIFY(stream.avail_in == 0);
+ source->Skip(inputAvailable - stream.avail_in);
+ } while (source->Available() > 0);
+
+ YT_VERIFY(ret == Z_STREAM_END);
+}
+
+void ZlibDecompress(TSource* source, TBlob* output)
+{
+ if (source->Available() == 0) {
+ return;
+ }
+
+ // Read header.
+ ui64 totalUncompressedSize;
+ ReadPod(*source, totalUncompressedSize);
+
+ // We add one additional byte to process correctly last inflate.
+ output->Reserve(totalUncompressedSize + 1);
+
+ z_stream stream{};
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.opaque = Z_NULL;
+
+ YT_VERIFY(inflateInit(&stream) == Z_OK);
+
+ auto finallyGuard = Finally([&] {
+ inflateEnd(&stream);
+ });
+
+ // Read body.
+ int result;
+ do {
+ size_t inputAvailable;
+ const char* inputNext = source->Peek(&inputAvailable);
+ inputAvailable = std::min(inputAvailable, source->Available());
+ inputAvailable = std::min(inputAvailable, MaxZlibUInt);
+
+ stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(inputNext));
+ stream.avail_in = static_cast<uInt>(inputAvailable);
+
+ int flush = (stream.avail_in == source->Available()) ? Z_FINISH : Z_NO_FLUSH;
+
+ size_t outputAvailable = output->Capacity() - output->Size();
+ outputAvailable = std::min(outputAvailable, MaxZlibUInt);
+
+ stream.next_out = reinterpret_cast<Bytef*>(output->End());
+ stream.avail_out = static_cast<uInt>(outputAvailable);
+
+ result = inflate(&stream, flush);
+ if (result != Z_OK && result != Z_STREAM_END) {
+ THROW_ERROR_EXCEPTION("Zlib compression failed: inflate returned an error")
+ << TErrorAttribute("error", result);
+ }
+
+ source->Skip(inputAvailable - stream.avail_in);
+
+ output->Resize(output->Size() + outputAvailable - stream.avail_out, /*initializeStorage*/ false);
+ } while (result != Z_STREAM_END);
+
+ if (source->Available() != 0) {
+ THROW_ERROR_EXCEPTION("Zlib compression failed: input stream is not fully consumed")
+ << TErrorAttribute("remaining_size", source->Available());
+ }
+ if (output->Size() != totalUncompressedSize) {
+ THROW_ERROR_EXCEPTION("Zlib decompression failed: output size mismatch")
+ << TErrorAttribute("expected_size", totalUncompressedSize)
+ << TErrorAttribute("actual_size", output->Size());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
diff --git a/yt/yt/core/compression/zlib.h b/yt/yt/core/compression/zlib.h
new file mode 100644
index 0000000000..ab8d951794
--- /dev/null
+++ b/yt/yt/core/compression/zlib.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ZlibCompress(int level, TSource* source, TBlob* output);
+void ZlibDecompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/zstd.cpp b/yt/yt/core/compression/zstd.cpp
new file mode 100644
index 0000000000..3ee31b31ac
--- /dev/null
+++ b/yt/yt/core/compression/zstd.cpp
@@ -0,0 +1,177 @@
+#include "zstd.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/finally.h>
+
+#define ZSTD_STATIC_LINKING_ONLY
+#include <contrib/libs/zstd/include/zstd.h>
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = CompressionLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TZstdCompressBufferTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ZstdCompress(int level, TSource* source, TBlob* output)
+{
+ ui64 totalInputSize = source->Available();
+ output->Resize(ZSTD_compressBound(totalInputSize) + sizeof(totalInputSize), /*initializeStorage*/ false);
+ size_t curOutputPos = 0;
+
+ // Write input size that will be used during decompression.
+ {
+ TMemoryOutput memoryOutput(output->Begin(), sizeof(totalInputSize));
+ WritePod(memoryOutput, totalInputSize);
+ curOutputPos += sizeof(totalInputSize);
+ }
+
+ auto checkError = [] (size_t result) {
+ YT_LOG_FATAL_IF(ZSTD_isError(result), "Zstd compression failed (Error: %v)",
+ ZSTD_getErrorName(result));
+ };
+
+ auto context = ZSTD_createCCtx();
+ auto contextGuard = Finally([&] () {
+ ZSTD_freeCCtx(context);
+ });
+
+ {
+ auto result = ZSTD_CCtx_setParameter(context, ZSTD_c_compressionLevel, level);
+ checkError(result);
+ }
+
+ struct CompressResult
+ {
+ size_t Result;
+ size_t ProcessedBytes;
+ };
+
+ auto compressToOutput = [&] (const char* buffer, size_t size, bool isLastBlock) -> CompressResult {
+ ZSTD_EndDirective mode = isLastBlock ? ZSTD_e_end : ZSTD_e_continue;
+
+ ZSTD_inBuffer zstdInput = {buffer, size, 0};
+ ZSTD_outBuffer zstdOutput = {output->Begin(), output->Size(), curOutputPos};
+
+ size_t result = ZSTD_compressStream2(context, &zstdOutput, &zstdInput, mode);
+ checkError(result);
+ curOutputPos = zstdOutput.pos;
+
+ return CompressResult{
+ .Result = result,
+ .ProcessedBytes = zstdInput.pos,
+ };
+ };
+
+ auto recommendedInputSize = ZSTD_CStreamInSize();
+
+ auto block = TBlob(GetRefCountedTypeCookie<TZstdCompressBufferTag>());
+ size_t blockSize = 0;
+
+ auto compressDataFromBlock = [&] (bool isLastBlock) {
+ size_t remainingSize = blockSize;
+ while (remainingSize > 0) {
+ auto compressResult = compressToOutput(
+ block.Begin() + (blockSize - remainingSize),
+ remainingSize,
+ isLastBlock);
+ remainingSize -= compressResult.ProcessedBytes;
+ if (isLastBlock ? compressResult.Result == 0 : remainingSize == 0) {
+ break;
+ }
+ }
+ blockSize = 0;
+ };
+
+ while (source->Available() > 0) {
+ size_t inputSize;
+ const char* inputPtr = source->Peek(&inputSize);
+
+ bool shouldFlushBlock = false;
+ if (inputSize < recommendedInputSize) {
+ if (block.IsEmpty()) {
+ block.Resize(recommendedInputSize, /*initializeStorage*/ false);
+ }
+ if (blockSize + inputSize <= block.Size()) {
+ // Append to block.
+ memcpy(block.Begin() + blockSize, inputPtr, inputSize);
+ blockSize += inputSize;
+ source->Skip(inputSize);
+ continue;
+ } else {
+ shouldFlushBlock = true;
+ }
+ } else {
+ shouldFlushBlock = blockSize > 0;
+ }
+
+ if (shouldFlushBlock) {
+ compressDataFromBlock(/*isLastBlock*/ false);
+ }
+
+ size_t remainingSize = inputSize;
+
+ bool isLastBlock = source->Available() == remainingSize;
+ while (true) {
+ auto compressResult = compressToOutput(
+ inputPtr + (inputSize - remainingSize),
+ remainingSize,
+ isLastBlock);
+ source->Skip(compressResult.ProcessedBytes);
+ remainingSize -= compressResult.ProcessedBytes;
+ if (isLastBlock ? compressResult.Result == 0 : remainingSize == 0) {
+ break;
+ }
+ }
+ }
+
+ if (blockSize > 0) {
+ compressDataFromBlock(/*isLastBlock*/ true);
+ }
+
+ output->Resize(curOutputPos);
+}
+
+void ZstdDecompress(TSource* source, TBlob* output)
+{
+ ui64 outputSize;
+ ReadPod(*source, outputSize);
+
+ output->Resize(outputSize, /*initializeStorage*/ false);
+ void* outputPtr = output->Begin();
+
+ size_t inputSize;
+ const void* inputPtr = source->Peek(&inputSize);
+
+ // TODO(babenko): something weird is going on here.
+ TBlob input;
+ if (auto available = source->Available(); available > inputSize) {
+ input.Resize(available, /*initializeStorage*/ false);
+ ReadRef(*source, TMutableRef(input.Begin(), available));
+ inputPtr = input.Begin();
+ inputSize = input.Size();
+ }
+
+ size_t decompressedSize = ZSTD_decompress(outputPtr, outputSize, inputPtr, inputSize);
+ if (ZSTD_isError(decompressedSize)) {
+ THROW_ERROR_EXCEPTION("Zstd decompression failed: ZSTD_decompress returned an error")
+ << TErrorAttribute("error", ZSTD_getErrorName(decompressedSize));
+ }
+ if (decompressedSize != outputSize) {
+ THROW_ERROR_EXCEPTION("Zstd decompression failed: output size mismatch")
+ << TErrorAttribute("expected_size", outputSize)
+ << TErrorAttribute("actual_size", decompressedSize);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/compression/zstd.h b/yt/yt/core/compression/zstd.h
new file mode 100644
index 0000000000..d812be590d
--- /dev/null
+++ b/yt/yt/core/compression/zstd.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "stream.h"
+
+namespace NYT::NCompression::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ZstdCompress(int level, TSource* source, TBlob* output);
+void ZstdDecompress(TSource* source, TBlob* output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCompression::NDetail
+
diff --git a/yt/yt/core/concurrency/action_queue.cpp b/yt/yt/core/concurrency/action_queue.cpp
new file mode 100644
index 0000000000..fa1f5e4206
--- /dev/null
+++ b/yt/yt/core/concurrency/action_queue.cpp
@@ -0,0 +1,722 @@
+#include "action_queue.h"
+#include "single_queue_scheduler_thread.h"
+#include "fair_share_queue_scheduler_thread.h"
+#include "private.h"
+#include "profiling_helpers.h"
+#include "system_invokers.h"
+
+#include <yt/yt/core/actions/current_invoker.h>
+#include <yt/yt/core/actions/invoker_util.h>
+#include <yt/yt/core/actions/invoker_detail.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/misc/crash_handler.h>
+#include <yt/yt/core/misc/ring_queue.h>
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <util/thread/lfqueue.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+using namespace NYPath;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TActionQueue::TImpl
+ : public TRefCounted
+{
+public:
+ explicit TImpl(const TString& threadName)
+ : Queue_(New<TMpscInvokerQueue>(
+ CallbackEventCount_,
+ GetThreadTags(threadName)))
+ , Invoker_(Queue_)
+ , Thread_(New<TMpscSingleQueueSchedulerThread>(
+ Queue_,
+ CallbackEventCount_,
+ threadName,
+ threadName))
+ , ShutdownCookie_(RegisterShutdownCallback(
+ Format("ActionQueue(%v)", threadName),
+ BIND_NO_PROPAGATE(&TImpl::Shutdown, MakeWeak(this), /*graceful*/ false),
+ /*priority*/ 100))
+ { }
+
+ ~TImpl()
+ {
+ Shutdown(/*graceful*/ false);
+ }
+
+ void Shutdown(bool graceful)
+ {
+ if (Stopped_.exchange(true)) {
+ return;
+ }
+
+ Queue_->Shutdown();
+
+ ShutdownInvoker_->Invoke(BIND_NO_PROPAGATE([graceful, thread = Thread_, queue = Queue_] {
+ thread->Stop(graceful);
+ queue->DrainConsumer();
+ }));
+ }
+
+ const IInvokerPtr& GetInvoker()
+ {
+ EnsureStarted();
+ return Invoker_;
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TMpscInvokerQueuePtr Queue_;
+ const IInvokerPtr Invoker_;
+ const TMpscSingleQueueSchedulerThreadPtr Thread_;
+
+ const TShutdownCookie ShutdownCookie_;
+ const IInvokerPtr ShutdownInvoker_ = GetShutdownInvoker();
+
+ std::atomic<bool> Started_ = false;
+ std::atomic<bool> Stopped_ = false;
+
+
+ void EnsureStarted()
+ {
+ if (Started_.load(std::memory_order::relaxed)) {
+ return;
+ }
+ if (Started_.exchange(true)) {
+ return;
+ }
+ Thread_->Start();
+ }
+};
+
+TActionQueue::TActionQueue(TString threadName)
+ : Impl_(New<TImpl>(std::move(threadName)))
+{ }
+
+TActionQueue::~TActionQueue() = default;
+
+void TActionQueue::Shutdown(bool graceful)
+{
+ return Impl_->Shutdown(graceful);
+}
+
+const IInvokerPtr& TActionQueue::GetInvoker()
+{
+ return Impl_->GetInvoker();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializedInvoker
+ : public TInvokerWrapper
+ , public TInvokerProfileWrapper
+{
+public:
+ TSerializedInvoker(IInvokerPtr underlyingInvoker, const NProfiling::TTagSet& tagSet, NProfiling::IRegistryImplPtr registry)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ , TInvokerProfileWrapper(std::move(registry), "/serialized", tagSet)
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ auto wrappedCallback = WrapCallback(std::move(callback));
+
+ auto guard = Guard(Lock_);
+ if (Dead_) {
+ return;
+ }
+ Queue_.push(std::move(wrappedCallback));
+ TrySchedule(std::move(guard));
+ }
+
+ bool IsSerialized() const override
+ {
+ return true;
+ }
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TRingQueue<TClosure> Queue_;
+ bool CallbackScheduled_ = false;
+ bool Dead_ = false;
+
+
+ class TInvocationGuard
+ {
+ public:
+ explicit TInvocationGuard(TIntrusivePtr<TSerializedInvoker> owner)
+ : Owner_(std::move(owner))
+ { }
+
+ TInvocationGuard(TInvocationGuard&& other) = default;
+ TInvocationGuard(const TInvocationGuard& other) = delete;
+
+ void Activate()
+ {
+ YT_ASSERT(!Activated_);
+ Activated_ = true;
+ }
+
+ void Reset()
+ {
+ Owner_.Reset();
+ }
+
+ ~TInvocationGuard()
+ {
+ if (Owner_) {
+ Owner_->OnFinished(Activated_);
+ }
+ }
+
+ private:
+ TIntrusivePtr<TSerializedInvoker> Owner_;
+ bool Activated_ = false;
+
+ };
+
+ void TrySchedule(TGuard<NThreading::TSpinLock>&& guard)
+ {
+ if (std::exchange(CallbackScheduled_, true)) {
+ return;
+ }
+ guard.Release();
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TSerializedInvoker::RunCallback,
+ MakeStrong(this),
+ Passed(TInvocationGuard(this))));
+ }
+
+ void DrainQueue(TGuard<NThreading::TSpinLock>&& guard)
+ {
+ std::vector<TClosure> callbacks;
+ while (!Queue_.empty()) {
+ callbacks.push_back(std::move(Queue_.front()));
+ Queue_.pop();
+ }
+ guard.Release();
+ callbacks.clear();
+ }
+
+ void RunCallback(TInvocationGuard invocationGuard)
+ {
+ invocationGuard.Activate();
+
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ TOneShotContextSwitchGuard contextSwitchGuard([&] {
+ invocationGuard.Reset();
+ OnFinished(true);
+ });
+
+ auto lockGuard = Guard(Lock_);
+
+ if (Queue_.empty()) {
+ return;
+ }
+
+ auto callback = std::move(Queue_.front());
+ Queue_.pop();
+
+ lockGuard.Release();
+
+ callback();
+ }
+
+ void OnFinished(bool activated)
+ {
+ auto guard = Guard(Lock_);
+
+ YT_VERIFY(std::exchange(CallbackScheduled_, false));
+
+ if (activated) {
+ if (!Queue_.empty()) {
+ TrySchedule(std::move(guard));
+ }
+ } else {
+ Dead_ = true;
+ DrainQueue(std::move(guard));
+ }
+ }
+};
+
+IInvokerPtr CreateSerializedInvoker(IInvokerPtr underlyingInvoker, const NProfiling::TTagSet& tagSet, NProfiling::IRegistryImplPtr registry)
+{
+ if (underlyingInvoker->IsSerialized()) {
+ return underlyingInvoker;
+ }
+
+ return New<TSerializedInvoker>(std::move(underlyingInvoker), tagSet, registry);
+}
+
+IInvokerPtr CreateSerializedInvoker(IInvokerPtr underlyingInvoker, const TString& invokerName, NProfiling::IRegistryImplPtr registry)
+{
+ NProfiling::TTagSet tagSet;
+ tagSet.AddTag(std::pair<TString, TString>("invoker", invokerName));
+ return CreateSerializedInvoker(std::move(underlyingInvoker), std::move(tagSet), std::move(registry));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrioritizedInvoker
+ : public TInvokerWrapper
+ , public TInvokerProfileWrapper
+ , public virtual IPrioritizedInvoker
+{
+public:
+ TPrioritizedInvoker(IInvokerPtr underlyingInvoker, const NProfiling::TTagSet& tagSet, NProfiling::IRegistryImplPtr registry)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ , TInvokerProfileWrapper(std::move(registry), "/prioritized", tagSet)
+ { }
+
+ using TInvokerWrapper::Invoke;
+
+ void Invoke(TClosure callback) override
+ {
+ auto wrappedCallback = WrapCallback(std::move(callback));
+ UnderlyingInvoker_->Invoke(std::move(wrappedCallback));
+ }
+
+ void Invoke(TClosure callback, i64 priority) override
+ {
+ {
+ auto guard = Guard(SpinLock_);
+ Heap_.push_back({std::move(callback), priority, Counter_--});
+ std::push_heap(Heap_.begin(), Heap_.end());
+ }
+ auto wrappedCallback = WrapCallback(BIND_NO_PROPAGATE(&TPrioritizedInvoker::DoExecute, MakeStrong(this)));
+ UnderlyingInvoker_->Invoke(std::move(wrappedCallback));
+ }
+
+private:
+ struct TEntry
+ {
+ TClosure Callback;
+ i64 Priority;
+ i64 Index;
+
+ bool operator < (const TEntry& other) const
+ {
+ return std::tie(Priority, Index) < std::tie(other.Priority, other.Index);
+ }
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::vector<TEntry> Heap_;
+ i64 Counter_ = 0;
+
+ void DoExecute()
+ {
+ auto guard = Guard(SpinLock_);
+ std::pop_heap(Heap_.begin(), Heap_.end());
+ auto callback = std::move(Heap_.back().Callback);
+ Heap_.pop_back();
+ guard.Release();
+ callback();
+ }
+
+};
+
+IPrioritizedInvokerPtr CreatePrioritizedInvoker(IInvokerPtr underlyingInvoker, const NProfiling::TTagSet& tagSet, NProfiling::IRegistryImplPtr registry)
+{
+ return New<TPrioritizedInvoker>(std::move(underlyingInvoker), std::move(tagSet), std::move(registry));
+}
+
+IPrioritizedInvokerPtr CreatePrioritizedInvoker(IInvokerPtr underlyingInvoker, const TString& invokerName, NProfiling::IRegistryImplPtr registry)
+{
+ NProfiling::TTagSet tagSet;
+ tagSet.AddTag(std::pair<TString, TString>("invoker", invokerName));
+ return CreatePrioritizedInvoker(std::move(underlyingInvoker), std::move(tagSet), std::move(registry));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFakePrioritizedInvoker
+ : public TInvokerWrapper
+ , public virtual IPrioritizedInvoker
+{
+public:
+ explicit TFakePrioritizedInvoker(IInvokerPtr underlyingInvoker)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ { }
+
+ using TInvokerWrapper::Invoke;
+
+ void Invoke(TClosure callback, i64 /*priority*/) override
+ {
+ return UnderlyingInvoker_->Invoke(std::move(callback));
+ }
+};
+
+IPrioritizedInvokerPtr CreateFakePrioritizedInvoker(IInvokerPtr underlyingInvoker)
+{
+ return New<TFakePrioritizedInvoker>(std::move(underlyingInvoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFixedPriorityInvoker
+ : public TInvokerWrapper
+{
+public:
+ TFixedPriorityInvoker(
+ IPrioritizedInvokerPtr underlyingInvoker,
+ i64 priority)
+ : TInvokerWrapper(underlyingInvoker)
+ , UnderlyingInvoker_(std::move(underlyingInvoker))
+ , Priority_(priority)
+ { }
+
+ using TInvokerWrapper::Invoke;
+
+ void Invoke(TClosure callback) override
+ {
+ return UnderlyingInvoker_->Invoke(std::move(callback), Priority_);
+ }
+
+private:
+ const IPrioritizedInvokerPtr UnderlyingInvoker_;
+ const i64 Priority_;
+
+};
+
+IInvokerPtr CreateFixedPriorityInvoker(
+ IPrioritizedInvokerPtr underlyingInvoker,
+ i64 priority)
+{
+ return New<TFixedPriorityInvoker>(
+ std::move(underlyingInvoker),
+ priority);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBoundedConcurrencyInvoker
+ : public TInvokerWrapper
+{
+public:
+ TBoundedConcurrencyInvoker(
+ IInvokerPtr underlyingInvoker,
+ int maxConcurrentInvocations)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ , MaxConcurrentInvocations_(maxConcurrentInvocations)
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ auto guard = Guard(SpinLock_);
+ if (Semaphore_ < MaxConcurrentInvocations_) {
+ YT_VERIFY(Queue_.empty());
+ IncrementSemaphore(+1);
+ guard.Release();
+ RunCallback(std::move(callback));
+ } else {
+ Queue_.push(std::move(callback));
+ }
+ }
+
+private:
+ const int MaxConcurrentInvocations_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TRingQueue<TClosure> Queue_;
+ int Semaphore_ = 0;
+
+ static thread_local TBoundedConcurrencyInvoker* CurrentSchedulingInvoker_;
+
+private:
+ class TInvocationGuard
+ {
+ public:
+ explicit TInvocationGuard(TIntrusivePtr<TBoundedConcurrencyInvoker> owner)
+ : Owner_(std::move(owner))
+ { }
+
+ TInvocationGuard(TInvocationGuard&& other) = default;
+ TInvocationGuard(const TInvocationGuard& other) = delete;
+
+ ~TInvocationGuard()
+ {
+ if (Owner_) {
+ Owner_->OnFinished();
+ }
+ }
+
+ private:
+ TIntrusivePtr<TBoundedConcurrencyInvoker> Owner_;
+ };
+
+ void IncrementSemaphore(int delta)
+ {
+ Semaphore_ += delta;
+ YT_ASSERT(Semaphore_ >= 0 && Semaphore_ <= MaxConcurrentInvocations_);
+ }
+
+ void RunCallback(TClosure callback)
+ {
+ // If UnderlyingInvoker_ is already terminated, Invoke may drop the guard right away.
+ // Protect by setting CurrentSchedulingInvoker_ and checking it on entering ScheduleMore.
+ CurrentSchedulingInvoker_ = this;
+
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TBoundedConcurrencyInvoker::DoRunCallback,
+ MakeStrong(this),
+ std::move(callback),
+ Passed(TInvocationGuard(this))));
+
+ // Don't leave a dangling pointer behind.
+ CurrentSchedulingInvoker_ = nullptr;
+ }
+
+ void DoRunCallback(const TClosure& callback, TInvocationGuard /*invocationGuard*/)
+ {
+ TCurrentInvokerGuard guard(UnderlyingInvoker_.Get()); // sic!
+ callback();
+ }
+
+ void OnFinished()
+ {
+ auto guard = Guard(SpinLock_);
+ // See RunCallback.
+ if (Queue_.empty() || CurrentSchedulingInvoker_ == this) {
+ IncrementSemaphore(-1);
+ } else {
+ auto callback = std::move(Queue_.front());
+ Queue_.pop();
+ guard.Release();
+ RunCallback(std::move(callback));
+ }
+ }
+};
+
+thread_local TBoundedConcurrencyInvoker* TBoundedConcurrencyInvoker::CurrentSchedulingInvoker_;
+
+IInvokerPtr CreateBoundedConcurrencyInvoker(
+ IInvokerPtr underlyingInvoker,
+ int maxConcurrentInvocations)
+{
+ return New<TBoundedConcurrencyInvoker>(
+ std::move(underlyingInvoker),
+ maxConcurrentInvocations);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendableInvoker
+ : public TInvokerWrapper
+ , public virtual ISuspendableInvoker
+{
+public:
+ explicit TSuspendableInvoker(IInvokerPtr underlyingInvoker)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ Queue_.Enqueue(std::move(callback));
+ ScheduleMore();
+ }
+
+ TFuture<void> Suspend() override
+ {
+ YT_VERIFY(!Suspended_.exchange(true));
+ {
+ auto guard = Guard(SpinLock_);
+ FreeEvent_ = NewPromise<void>();
+ if (ActiveInvocationCount_ == 0) {
+ FreeEvent_.Set();
+ }
+ }
+ return FreeEvent_;
+ }
+
+ void Resume() override
+ {
+ YT_VERIFY(Suspended_.exchange(false));
+ {
+ auto guard = Guard(SpinLock_);
+ FreeEvent_.Reset();
+ }
+ ScheduleMore();
+ }
+
+ bool IsSuspended() override
+ {
+ return Suspended_;
+ }
+
+private:
+ std::atomic<bool> Suspended_ = {false};
+ std::atomic<bool> SchedulingMore_ = {false};
+ std::atomic<int> ActiveInvocationCount_ = {0};
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ TLockFreeQueue<TClosure> Queue_;
+
+ TPromise<void> FreeEvent_;
+
+ // TODO(acid): Think how to merge this class with implementation in other invokers.
+ class TInvocationGuard
+ {
+ public:
+ explicit TInvocationGuard(TIntrusivePtr<TSuspendableInvoker> owner)
+ : Owner_(std::move(owner))
+ { }
+
+ TInvocationGuard(TInvocationGuard&& other) = default;
+ TInvocationGuard(const TInvocationGuard& other) = delete;
+
+ void Reset()
+ {
+ Owner_.Reset();
+ }
+
+ ~TInvocationGuard()
+ {
+ if (Owner_) {
+ Owner_->OnFinished();
+ }
+ }
+
+ private:
+ TIntrusivePtr<TSuspendableInvoker> Owner_;
+ };
+
+
+ void RunCallback(TClosure callback, TInvocationGuard invocationGuard)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ TOneShotContextSwitchGuard contextSwitchGuard([&] {
+ invocationGuard.Reset();
+ OnFinished();
+ });
+ callback();
+ }
+
+ void OnFinished()
+ {
+ YT_VERIFY(ActiveInvocationCount_ > 0);
+
+ if (--ActiveInvocationCount_ == 0 && Suspended_) {
+ auto guard = Guard(SpinLock_);
+ if (FreeEvent_ && !FreeEvent_.IsSet()) {
+ auto freeEvent = FreeEvent_;
+ guard.Release();
+ freeEvent.Set();
+ }
+ }
+ }
+
+ void ScheduleMore()
+ {
+ if (Suspended_ || SchedulingMore_.exchange(true)) {
+ return;
+ }
+
+ while (!Suspended_) {
+ ++ActiveInvocationCount_;
+ TInvocationGuard guard(this);
+
+ TClosure callback;
+ if (Suspended_ || !Queue_.Dequeue(&callback)) {
+ break;
+ }
+
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TSuspendableInvoker::RunCallback,
+ MakeStrong(this),
+ Passed(std::move(callback)),
+ Passed(std::move(guard))));
+ }
+
+ SchedulingMore_ = false;
+ if (!Queue_.IsEmpty()) {
+ ScheduleMore();
+ }
+ }
+};
+
+ISuspendableInvokerPtr CreateSuspendableInvoker(IInvokerPtr underlyingInvoker)
+{
+ return New<TSuspendableInvoker>(std::move(underlyingInvoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMemoryTaggingInvoker
+ : public TInvokerWrapper
+{
+public:
+ TMemoryTaggingInvoker(IInvokerPtr invoker, TMemoryTag memoryTag)
+ : TInvokerWrapper(std::move(invoker))
+ , MemoryTag_(memoryTag)
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TMemoryTaggingInvoker::RunCallback,
+ MakeStrong(this),
+ Passed(std::move(callback))));
+ }
+
+private:
+ TMemoryTag MemoryTag_;
+
+ void RunCallback(TClosure callback)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ TMemoryTagGuard memoryTagGuard(MemoryTag_);
+ callback();
+ }
+};
+
+IInvokerPtr CreateMemoryTaggingInvoker(IInvokerPtr underlyingInvoker, TMemoryTag tag)
+{
+ return New<TMemoryTaggingInvoker>(std::move(underlyingInvoker), tag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCodicilGuardedInvoker
+ : public TInvokerWrapper
+{
+public:
+ TCodicilGuardedInvoker(IInvokerPtr invoker, TString codicil)
+ : TInvokerWrapper(std::move(invoker))
+ , Codicil_(std::move(codicil))
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TCodicilGuardedInvoker::RunCallback,
+ MakeStrong(this),
+ Passed(std::move(callback))));
+ }
+
+private:
+ const TString Codicil_;
+
+ void RunCallback(TClosure callback)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ TCodicilGuard codicilGuard(Codicil_);
+ callback();
+ }
+};
+
+IInvokerPtr CreateCodicilGuardedInvoker(IInvokerPtr underlyingInvoker, TString codicil)
+{
+ return New<TCodicilGuardedInvoker>(std::move(underlyingInvoker), std::move(codicil));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/action_queue.h b/yt/yt/core/concurrency/action_queue.h
new file mode 100644
index 0000000000..c6223487a6
--- /dev/null
+++ b/yt/yt/core/concurrency/action_queue.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/library/profiling/public.h>
+#include <yt/yt/library/profiling/tag.h>
+
+#include <library/cpp/yt/memory/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// XXX(sandello): Facade does not have to be ref-counted.
+class TActionQueue
+ : public TRefCounted
+{
+public:
+ explicit TActionQueue(TString threadName = "ActionQueue");
+ virtual ~TActionQueue();
+
+ void Shutdown(bool graceful = false);
+
+ const IInvokerPtr& GetInvoker();
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TActionQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an invoker that executes all callbacks in the
+//! context of #underlyingInvoker (possibly in different threads)
+//! but in a serialized fashion (i.e. all queued callbacks are executed
+//! in the proper order and no two callbacks are executed in parallel).
+//! #invokerName is used as a profiling tag.
+//! #registry is needed for testing purposes only.
+IInvokerPtr CreateSerializedInvoker(
+ IInvokerPtr underlyingInvoker,
+ const TString& invokerName = "default",
+ NProfiling::IRegistryImplPtr registry = nullptr);
+
+IInvokerPtr CreateSerializedInvoker(
+ IInvokerPtr underlyingInvoker,
+ const NProfiling::TTagSet& tagSet,
+ NProfiling::IRegistryImplPtr registry = nullptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a wrapper around IInvoker that supports callback reordering.
+//! Callbacks with the highest priority are executed first.
+//! #invokerName is used as a profiling tag.
+//! #registry is needed for testing purposes only.
+IPrioritizedInvokerPtr CreatePrioritizedInvoker(
+ IInvokerPtr underlyingInvoker,
+ const TString& invokerName = "default",
+ NProfiling::IRegistryImplPtr registry = nullptr);
+
+IPrioritizedInvokerPtr CreatePrioritizedInvoker(
+ IInvokerPtr underlyingInvoker,
+ const NProfiling::TTagSet& tagSet,
+ NProfiling::IRegistryImplPtr registry = nullptr);
+
+//! Creates a wrapper around IInvoker that implements IPrioritizedInvoker but
+//! does not perform any actual reordering. Priorities passed to #IPrioritizedInvoker::Invoke
+//! are ignored.
+IPrioritizedInvokerPtr CreateFakePrioritizedInvoker(IInvokerPtr underlyingInvoker);
+
+//! Creates a wrapper around IPrioritizedInvoker turning it into a regular IInvoker.
+//! All callbacks are propagated with a given fixed #priority.
+IInvokerPtr CreateFixedPriorityInvoker(
+ IPrioritizedInvokerPtr underlyingInvoker,
+ i64 priority);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an invoker that executes all callbacks in the
+//! context of #underlyingInvoker allowing up to #maxConcurrentInvocations
+//! outstanding requests to the latter.
+IInvokerPtr CreateBoundedConcurrencyInvoker(
+ IInvokerPtr underlyingInvoker,
+ int maxConcurrentInvocations);
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISuspendableInvokerPtr CreateSuspendableInvoker(IInvokerPtr underlyingInvoker);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an invoker that runs each callback within a memory context defined by a
+//! given memory tag. Every allocation performed by a callback will be properly tagged.
+IInvokerPtr CreateMemoryTaggingInvoker(
+ IInvokerPtr underlyingInvoker,
+ TMemoryTag memoryTag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an invoker that creates a codicil guard with a given string before each
+//! callback invocation.
+IInvokerPtr CreateCodicilGuardedInvoker(
+ IInvokerPtr underlyingInvoker,
+ TString codicil);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_barrier.cpp b/yt/yt/core/concurrency/async_barrier.cpp
new file mode 100644
index 0000000000..14a85051e8
--- /dev/null
+++ b/yt/yt/core/concurrency/async_barrier.cpp
@@ -0,0 +1,87 @@
+#include "async_barrier.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncBarrierCookie TAsyncBarrier::Insert()
+{
+ auto guard = Guard(Lock_);
+ SlotOccupied_.push(true);
+ return TAsyncBarrierCookie(FirstSlotCookie_ + std::ssize(SlotOccupied_) - 1);
+}
+
+void TAsyncBarrier::Remove(TAsyncBarrierCookie cookie)
+{
+ std::vector<TPromise<void>> promises;
+
+ {
+ auto guard = Guard(Lock_);
+
+ YT_VERIFY(static_cast<i64>(cookie) >= FirstSlotCookie_);
+ YT_VERIFY(std::exchange(SlotOccupied_[static_cast<i64>(cookie) - FirstSlotCookie_], false));
+
+ while (!SlotOccupied_.empty() && !SlotOccupied_.front()) {
+ SlotOccupied_.pop();
+ ++FirstSlotCookie_;
+ }
+
+ while (!Subscriptions_.empty() && Subscriptions_.front().BarrierCookie <= FirstSlotCookie_) {
+ promises.push_back(std::move(Subscriptions_.front().Promise));
+ Subscriptions_.pop();
+ }
+ }
+
+ for (const auto& promise : promises) {
+ promise.Set();
+ }
+}
+
+TFuture<void> TAsyncBarrier::GetBarrierFuture()
+{
+ auto guard = Guard(Lock_);
+
+ if (SlotOccupied_.empty()) {
+ return VoidFuture;
+ }
+
+ auto barrierCookie = FirstSlotCookie_ + std::ssize(SlotOccupied_);
+ if (!Subscriptions_.empty() && Subscriptions_.back().BarrierCookie == barrierCookie) {
+ return Subscriptions_.back().Future;
+ }
+
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture().ToUncancelable();
+ Subscriptions_.push({
+ .BarrierCookie = barrierCookie,
+ .Promise = std::move(promise),
+ .Future = future,
+ });
+ return future;
+}
+
+void TAsyncBarrier::Clear(const TError& error)
+{
+ std::vector<TPromise<void>> promises;
+
+ {
+ auto guard = Guard(Lock_);
+
+ SlotOccupied_.clear();
+
+ while (!Subscriptions_.empty()) {
+ promises.push_back(std::move(Subscriptions_.front().Promise));
+ Subscriptions_.pop();
+ }
+
+ FirstSlotCookie_ = 1;
+ }
+
+ for (const auto& promise : promises) {
+ promise.Set(error);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_barrier.h b/yt/yt/core/concurrency/async_barrier.h
new file mode 100644
index 0000000000..962c6b7214
--- /dev/null
+++ b/yt/yt/core/concurrency/async_barrier.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/misc/strong_typedef.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque handle to an item in TAsyncBarrier.
+YT_DEFINE_STRONG_TYPEDEF(TAsyncBarrierCookie, i64);
+
+//! A sentinel value that can never be returned by TAsyncBarrier::Insert.
+inline const TAsyncBarrierCookie InvalidAsyncBarrierCookie = TAsyncBarrierCookie(0);
+
+//! Maintains (but does not store) a set of items and enables tracking moments
+//! when all items that are currently present in the set become removed.
+/*!
+ * \note
+ * Thread affinity: any
+ */
+class TAsyncBarrier final
+{
+public:
+ //! Inserts a new item into the set.
+ //! Returns the cookie to be used later for removal.
+ TAsyncBarrierCookie Insert();
+
+ //! Removes an existing item from the set.
+ void Remove(TAsyncBarrierCookie cookie);
+
+ //! Returns a future that becomes set when all items currently present in the
+ //! set are removed.
+ TFuture<void> GetBarrierFuture();
+
+ //! Clears the state and also propagates #error to all subscribers.
+ void Clear(const TError& error);
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ i64 FirstSlotCookie_ = 1;
+ TRingQueue<bool> SlotOccupied_;
+
+ struct TSubscription
+ {
+ i64 BarrierCookie;
+ TPromise<void> Promise;
+ TFuture<void> Future;
+ };
+
+ TRingQueue<TSubscription> Subscriptions_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_rw_lock-inl.h b/yt/yt/core/concurrency/async_rw_lock-inl.h
new file mode 100644
index 0000000000..e0873078f6
--- /dev/null
+++ b/yt/yt/core/concurrency/async_rw_lock-inl.h
@@ -0,0 +1,43 @@
+#ifndef ASYNC_RW_LOCK_INL_H_
+#error "Direct inclusion of this file is not allowed, include async_rw_lock.h"
+// For the sake of sane code completion.
+#include "async_rw_lock.h"
+#endif
+#undef ASYNC_RW_LOCK_INL_H_
+
+#include <util/system/guard.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TTraits>
+TAsyncReaderWriterLockGuard<TTraits>::~TAsyncReaderWriterLockGuard()
+{
+ Release();
+}
+
+template <typename TTraits>
+TFuture<TIntrusivePtr<TAsyncReaderWriterLockGuard<TTraits>>>
+ TAsyncReaderWriterLockGuard<TTraits>::Acquire(TAsyncReaderWriterLock* lock)
+{
+ const auto& impl = lock->Impl_;
+ return TTraits::Acquire(impl).Apply(BIND([impl = impl] () mutable {
+ auto guard = New<TAsyncReaderWriterLockGuard>();
+ guard->LockImpl_ = std::move(impl);
+ return guard;
+ }));
+}
+
+template <typename TTraits>
+void TAsyncReaderWriterLockGuard<TTraits>::Release()
+{
+ if (LockImpl_) {
+ TTraits::Release(LockImpl_);
+ LockImpl_ = nullptr;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_rw_lock.cpp b/yt/yt/core/concurrency/async_rw_lock.cpp
new file mode 100644
index 0000000000..2d0546dcb7
--- /dev/null
+++ b/yt/yt/core/concurrency/async_rw_lock.cpp
@@ -0,0 +1,129 @@
+#include "async_rw_lock.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TAsyncLockReaderTraits::Acquire(const TAsyncReaderWriterLock::TImplPtr& impl)
+{
+ return impl->AcquireReader();
+}
+
+void TAsyncLockReaderTraits::Release(const TAsyncReaderWriterLock::TImplPtr& impl)
+{
+ impl->ReleaseReader();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TAsyncLockWriterTraits::Acquire(const TAsyncReaderWriterLock::TImplPtr& impl)
+{
+ return impl->AcquireWriter();
+}
+
+void TAsyncLockWriterTraits::Release(const TAsyncReaderWriterLock::TImplPtr& impl)
+{
+ impl->ReleaseWriter();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TAsyncReaderWriterLock::AcquireReader()
+{
+ return Impl_->AcquireReader();
+}
+
+void TAsyncReaderWriterLock::ReleaseReader()
+{
+ Impl_->ReleaseReader();
+}
+
+TFuture<void> TAsyncReaderWriterLock::AcquireWriter()
+{
+ return Impl_->AcquireWriter();
+}
+
+void TAsyncReaderWriterLock::ReleaseWriter()
+{
+ Impl_->ReleaseWriter();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TAsyncReaderWriterLock::TImpl::AcquireReader()
+{
+ auto guard = Guard(SpinLock_);
+
+ if (!HasActiveWriter_ && WriterPromiseQueue_.empty()) {
+ ++ActiveReaderCount_;
+ return VoidFuture;
+ }
+
+ auto promise = NewPromise<void>();
+ ReaderPromiseQueue_.push_back(promise);
+ return promise;
+}
+
+void TAsyncReaderWriterLock::TImpl::ReleaseReader()
+{
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(ActiveReaderCount_ > 0);
+
+ --ActiveReaderCount_;
+ if (ActiveReaderCount_ == 0 && !WriterPromiseQueue_.empty()) {
+ auto promise = WriterPromiseQueue_.front();
+ WriterPromiseQueue_.pop();
+ HasActiveWriter_ = true;
+ guard.Release();
+ promise.Set();
+ }
+}
+
+TFuture<void> TAsyncReaderWriterLock::TImpl::AcquireWriter()
+{
+ auto guard = Guard(SpinLock_);
+
+ if (ActiveReaderCount_ == 0 && !HasActiveWriter_) {
+ HasActiveWriter_ = true;
+ return VoidFuture;
+ }
+
+ auto promise = NewPromise<void>();
+ WriterPromiseQueue_.push(promise);
+ return promise;
+}
+
+void TAsyncReaderWriterLock::TImpl::ReleaseWriter()
+{
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(HasActiveWriter_);
+
+ HasActiveWriter_ = false;
+ if (WriterPromiseQueue_.empty()) {
+ // Run all readers.
+ if (!ReaderPromiseQueue_.empty()) {
+ std::vector<TPromise<void>> readerPromiseQueue;
+ readerPromiseQueue.swap(ReaderPromiseQueue_);
+ ActiveReaderCount_ += readerPromiseQueue.size();
+ guard.Release();
+
+ // Promise subscribers must be synchronous to avoid hanging on some reader.
+ TForbidContextSwitchGuard contextSwitchGuard;
+ for (auto& promise : readerPromiseQueue) {
+ promise.Set();
+ }
+ }
+ } else {
+ auto promise = WriterPromiseQueue_.front();
+ WriterPromiseQueue_.pop();
+ HasActiveWriter_ = true;
+ guard.Release();
+ promise.Set();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_rw_lock.h b/yt/yt/core/concurrency/async_rw_lock.h
new file mode 100644
index 0000000000..ed60643ef7
--- /dev/null
+++ b/yt/yt/core/concurrency/async_rw_lock.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <queue>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncReaderWriterLock
+ : public TNonCopyable
+{
+public:
+ TFuture<void> AcquireReader();
+ void ReleaseReader();
+
+ TFuture<void> AcquireWriter();
+ void ReleaseWriter();
+
+private:
+ class TImpl final
+ {
+ public:
+ TFuture<void> AcquireReader();
+ void ReleaseReader();
+
+ TFuture<void> AcquireWriter();
+ void ReleaseWriter();
+
+ private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ int ActiveReaderCount_ = 0;
+ bool HasActiveWriter_ = false;
+
+ std::vector<TPromise<void>> ReaderPromiseQueue_;
+ std::queue<TPromise<void>> WriterPromiseQueue_;
+ };
+
+ using TImplPtr = TIntrusivePtr<TImpl>;
+
+ const TImplPtr Impl_ = New<TImpl>();
+
+ template <class TTraits>
+ friend class TAsyncReaderWriterLockGuard;
+
+ friend struct TAsyncLockReaderTraits;
+ friend struct TAsyncLockWriterTraits;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTraits>
+class TAsyncReaderWriterLockGuard
+ : public TRefCounted
+{
+public:
+ ~TAsyncReaderWriterLockGuard();
+
+ static TFuture<TIntrusivePtr<TAsyncReaderWriterLockGuard>> Acquire(TAsyncReaderWriterLock* lock);
+ void Release();
+
+private:
+ TAsyncReaderWriterLock::TImplPtr LockImpl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAsyncLockReaderTraits
+{
+ static TFuture<void> Acquire(const TAsyncReaderWriterLock::TImplPtr& impl);
+ static void Release(const TAsyncReaderWriterLock::TImplPtr& impl);
+};
+
+struct TAsyncLockWriterTraits
+{
+ static TFuture<void> Acquire(const TAsyncReaderWriterLock::TImplPtr& impl);
+ static void Release(const TAsyncReaderWriterLock::TImplPtr& impl);
+};
+
+using TAsyncLockReaderGuard = TAsyncReaderWriterLockGuard<TAsyncLockReaderTraits>;
+using TAsyncLockWriterGuard = TAsyncReaderWriterLockGuard<TAsyncLockWriterTraits>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define ASYNC_RW_LOCK_INL_H_
+#include "async_rw_lock-inl.h"
+#undef ASYNC_RW_LOCK_INL_H_
diff --git a/yt/yt/core/concurrency/async_semaphore.cpp b/yt/yt/core/concurrency/async_semaphore.cpp
new file mode 100644
index 0000000000..98a2b11b2c
--- /dev/null
+++ b/yt/yt/core/concurrency/async_semaphore.cpp
@@ -0,0 +1,286 @@
+#include "async_semaphore.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncSemaphore::TAsyncSemaphore(i64 totalSlots)
+ : TotalSlots_(totalSlots)
+ , FreeSlots_(totalSlots)
+{
+ YT_VERIFY(TotalSlots_ >= 0);
+}
+
+void TAsyncSemaphore::SetTotal(i64 totalSlots)
+{
+ YT_VERIFY(totalSlots >= 0);
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+ auto delta = totalSlots - TotalSlots_;
+ TotalSlots_ += delta;
+ FreeSlots_ += delta;
+ }
+
+ Release(0);
+}
+
+void TAsyncSemaphore::Release(i64 slots /* = 1 */)
+{
+ YT_VERIFY(slots >= 0);
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ FreeSlots_ += slots;
+ YT_ASSERT(FreeSlots_ <= TotalSlots_);
+
+ if (Releasing_) {
+ return;
+ }
+
+ Releasing_ = true;
+ }
+
+ while (true) {
+ std::vector<TWaiter> waitersToRelease;
+ TPromise<void> readyEventToSet;
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ while (!Waiters_.empty() && FreeSlots_ >= Waiters_.front().Slots) {
+ auto& waiter = Waiters_.front();
+ FreeSlots_ -= waiter.Slots;
+ waitersToRelease.push_back(std::move(waiter));
+ Waiters_.pop();
+ }
+
+ if (ReadyEvent_ && FreeSlots_ > 0) {
+ swap(readyEventToSet, ReadyEvent_);
+ }
+
+ if (waitersToRelease.empty() && !readyEventToSet) {
+ Releasing_ = false;
+ break;
+ }
+ }
+
+ for (const auto& waiter : waitersToRelease) {
+ // NB: This may lead to a reentrant invocation of Release if the invoker discards the callback.
+ waiter.Invoker->Invoke(BIND(waiter.Handler, Passed(TAsyncSemaphoreGuard(this, waiter.Slots))));
+ }
+
+ if (readyEventToSet) {
+ readyEventToSet.Set();
+ }
+ }
+}
+
+void TAsyncSemaphore::Acquire(i64 slots /* = 1 */)
+{
+ YT_VERIFY(slots >= 0);
+
+ auto guard = WriterGuard(SpinLock_);
+ FreeSlots_ -= slots;
+}
+
+bool TAsyncSemaphore::TryAcquire(i64 slots /*= 1*/)
+{
+ YT_VERIFY(slots >= 0);
+
+ auto guard = WriterGuard(SpinLock_);
+ if (FreeSlots_ < slots) {
+ return false;
+ }
+ FreeSlots_ -= slots;
+ return true;
+}
+
+void TAsyncSemaphore::AsyncAcquire(
+ const TCallback<void(TAsyncSemaphoreGuard)>& handler,
+ IInvokerPtr invoker,
+ i64 slots)
+{
+ YT_VERIFY(slots >= 0);
+
+ auto guard = WriterGuard(SpinLock_);
+ if (FreeSlots_ >= slots) {
+ FreeSlots_ -= slots;
+ guard.Release();
+ invoker->Invoke(BIND(handler, Passed(TAsyncSemaphoreGuard(this, slots))));
+ } else {
+ Waiters_.push(TWaiter{handler, std::move(invoker), slots});
+ }
+}
+
+bool TAsyncSemaphore::IsReady() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ return FreeSlots_ > 0;
+}
+
+bool TAsyncSemaphore::IsFree() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ return FreeSlots_ == TotalSlots_;
+}
+
+i64 TAsyncSemaphore::GetTotal() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ return TotalSlots_;
+}
+
+i64 TAsyncSemaphore::GetUsed() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ return TotalSlots_ - FreeSlots_;
+}
+
+i64 TAsyncSemaphore::GetFree() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+
+ return FreeSlots_;
+}
+
+TFuture<void> TAsyncSemaphore::GetReadyEvent()
+{
+ auto guard = WriterGuard(SpinLock_);
+
+ if (FreeSlots_ > 0) {
+ return VoidFuture;
+ } else if (!ReadyEvent_) {
+ ReadyEvent_ = NewPromise<void>();
+ }
+
+ return ReadyEvent_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProfiledAsyncSemaphore::TProfiledAsyncSemaphore(
+ i64 totalSlots,
+ NProfiling::TGauge gauge)
+ : TAsyncSemaphore(totalSlots)
+ , Gauge_(std::move(gauge))
+{ }
+
+void TProfiledAsyncSemaphore::Release(i64 slots /* = 1 */)
+{
+ TAsyncSemaphore::Release(slots);
+ Profile();
+}
+
+void TProfiledAsyncSemaphore::Acquire(i64 slots /* = 1 */)
+{
+ TAsyncSemaphore::Acquire(slots);
+ Profile();
+}
+
+bool TProfiledAsyncSemaphore::TryAcquire(i64 slots /* = 1 */)
+{
+ if (TAsyncSemaphore::TryAcquire(slots)) {
+ Profile();
+ return true;
+ }
+ return false;
+}
+
+void TProfiledAsyncSemaphore::Profile()
+{
+ Gauge_.Update(GetUsed());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncSemaphoreGuard::TAsyncSemaphoreGuard(TAsyncSemaphoreGuard&& other) noexcept
+{
+ MoveFrom(std::move(other));
+}
+
+TAsyncSemaphoreGuard::~TAsyncSemaphoreGuard()
+{
+ Release();
+}
+
+TAsyncSemaphoreGuard& TAsyncSemaphoreGuard::operator=(TAsyncSemaphoreGuard&& other)
+{
+ if (this != &other) {
+ Release();
+ MoveFrom(std::move(other));
+ }
+ return *this;
+}
+
+void TAsyncSemaphoreGuard::MoveFrom(TAsyncSemaphoreGuard&& other)
+{
+ Semaphore_ = other.Semaphore_;
+ Slots_ = other.Slots_;
+
+ other.Semaphore_ = nullptr;
+ other.Slots_ = 0;
+}
+
+void swap(TAsyncSemaphoreGuard& lhs, TAsyncSemaphoreGuard& rhs)
+{
+ std::swap(lhs.Semaphore_, rhs.Semaphore_);
+ std::swap(lhs.Slots_, rhs.Slots_);
+}
+
+TAsyncSemaphoreGuard::TAsyncSemaphoreGuard()
+ : Slots_(0)
+{ }
+
+TAsyncSemaphoreGuard::TAsyncSemaphoreGuard(TAsyncSemaphorePtr semaphore, i64 slots)
+ : Slots_(slots)
+ , Semaphore_(semaphore)
+{ }
+
+TAsyncSemaphoreGuard TAsyncSemaphoreGuard::Acquire(TAsyncSemaphorePtr semaphore, i64 slots /*= 1*/)
+{
+ semaphore->Acquire(slots);
+ return TAsyncSemaphoreGuard(semaphore, slots);
+}
+
+TAsyncSemaphoreGuard TAsyncSemaphoreGuard::TryAcquire(TAsyncSemaphorePtr semaphore, i64 slots /*= 1*/)
+{
+ if (semaphore->TryAcquire(slots)) {
+ return TAsyncSemaphoreGuard(semaphore, slots);
+ } else {
+ return TAsyncSemaphoreGuard();
+ }
+}
+
+TAsyncSemaphoreGuard TAsyncSemaphoreGuard::TransferSlots(i64 slotsToTransfer)
+{
+ YT_VERIFY(slotsToTransfer >= 0 && slotsToTransfer <= Slots_);
+ Slots_ -= slotsToTransfer;
+ TAsyncSemaphoreGuard spawnedGuard;
+ spawnedGuard.Semaphore_ = Semaphore_;
+ spawnedGuard.Slots_ = slotsToTransfer;
+ return spawnedGuard;
+}
+
+void TAsyncSemaphoreGuard::Release()
+{
+ if (Semaphore_) {
+ Semaphore_->Release(Slots_);
+ Semaphore_ = nullptr;
+ Slots_ = 0;
+ }
+}
+
+TAsyncSemaphoreGuard::operator bool() const
+{
+ return static_cast<bool>(Semaphore_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_semaphore.h b/yt/yt/core/concurrency/async_semaphore.h
new file mode 100644
index 0000000000..3f780093a7
--- /dev/null
+++ b/yt/yt/core/concurrency/async_semaphore.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncSemaphoreGuard
+ : private TNonCopyable
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(i64, Slots);
+
+public:
+ TAsyncSemaphoreGuard();
+ TAsyncSemaphoreGuard(TAsyncSemaphoreGuard&& other) noexcept;
+ ~TAsyncSemaphoreGuard();
+
+ TAsyncSemaphoreGuard& operator=(TAsyncSemaphoreGuard&& other);
+
+ static TAsyncSemaphoreGuard Acquire(TAsyncSemaphorePtr semaphore, i64 slots = 1);
+ static TAsyncSemaphoreGuard TryAcquire(TAsyncSemaphorePtr semaphore, i64 slots = 1);
+
+ friend void swap(TAsyncSemaphoreGuard& lhs, TAsyncSemaphoreGuard& rhs);
+
+ TAsyncSemaphoreGuard TransferSlots(i64 slotsToTransfer);
+
+ void Release();
+
+ explicit operator bool() const;
+
+private:
+ friend class TAsyncSemaphore;
+
+ TAsyncSemaphorePtr Semaphore_;
+
+ TAsyncSemaphoreGuard(TAsyncSemaphorePtr semaphore, i64 slots);
+
+ void MoveFrom(TAsyncSemaphoreGuard&& other);
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Custom semaphore class with async acquire operation.
+class TAsyncSemaphore
+ : public TRefCounted
+{
+public:
+ explicit TAsyncSemaphore(i64 totalSlots);
+
+ //! Updates the total number of slots.
+ void SetTotal(i64 totalSlots);
+
+ //! Releases a given number of slots.
+ virtual void Release(i64 slots = 1);
+
+ //! Acquires a given number of slots.
+ //! Cannot fail, may lead to an overcommit.
+ virtual void Acquire(i64 slots = 1);
+
+ //! Tries to acquire a given number of slots.
+ //! Returns |true| on success (the number of remaining slots is non-negative).
+ virtual bool TryAcquire(i64 slots = 1);
+
+ //! Runs #handler when a given number of slots becomes available.
+ //! These slots are immediately captured by TAsyncSemaphoreGuard instance passed to #handler.
+ // XXX(babenko): passing invoker is a temporary workaround until YT-3801 is fixed
+ void AsyncAcquire(
+ const TCallback<void(TAsyncSemaphoreGuard)>& handler,
+ IInvokerPtr invoker,
+ i64 slots = 1);
+
+ //! Returns |true| iff at least one slot is free.
+ bool IsReady() const;
+
+ //! Returns |true| iff all slots are free.
+ bool IsFree() const;
+
+ //! Returns the total number of slots.
+ i64 GetTotal() const;
+
+ //! Returns the number of used slots.
+ i64 GetUsed() const;
+
+ //! Returns the number of free slots.
+ i64 GetFree() const;
+
+ TFuture<void> GetReadyEvent();
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ i64 TotalSlots_;
+ i64 FreeSlots_;
+
+ bool Releasing_ = false;
+
+ TPromise<void> ReadyEvent_;
+
+ struct TWaiter
+ {
+ TCallback<void(TAsyncSemaphoreGuard)> Handler;
+ IInvokerPtr Invoker;
+ i64 Slots;
+ };
+
+ std::queue<TWaiter> Waiters_;
+
+};
+
+DEFINE_REFCOUNTED_TYPE(TAsyncSemaphore)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProfiledAsyncSemaphore
+ : public TAsyncSemaphore
+{
+public:
+ TProfiledAsyncSemaphore(
+ i64 totalSlots,
+ NProfiling::TGauge gauge);
+
+ void Release(i64 slots = 1) override;
+ void Acquire(i64 slots = 1) override;
+ bool TryAcquire(i64 slots = 1) override;
+
+private:
+ NProfiling::TGauge Gauge_;
+
+ void Profile();
+};
+
+DEFINE_REFCOUNTED_TYPE(TProfiledAsyncSemaphore)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_stream.cpp b/yt/yt/core/concurrency/async_stream.cpp
new file mode 100644
index 0000000000..6c43031fe0
--- /dev/null
+++ b/yt/yt/core/concurrency/async_stream.cpp
@@ -0,0 +1,1058 @@
+#include "async_stream.h"
+#include "scheduler.h"
+
+#include <util/stream/buffered.h>
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <queue>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSyncInputStreamAdapter
+ : public IInputStream
+{
+public:
+ TSyncInputStreamAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ EWaitForStrategy strategy)
+ : UnderlyingStream_(std::move(underlyingStream))
+ , Strategy_(strategy)
+ { }
+
+private:
+ const IAsyncInputStreamPtr UnderlyingStream_;
+ const EWaitForStrategy Strategy_;
+
+ size_t DoRead(void* buffer, size_t length) override
+ {
+ if (length == 0) {
+ return 0;
+ }
+
+ // When using WaitFor, we protect ourselves from TFiberCancelledException by
+ // introducing our own read buffer and additional data copying. In case of
+ // Get, there are no means of cancellation, so reading directly to the destination
+ // buffer is just fine.
+ TSharedMutableRef readBuffer;
+ if (Strategy_ == EWaitForStrategy::WaitFor) {
+ struct TSyncInputStreamAdapterIntermediateBufferTag { };
+ readBuffer = TSharedMutableRef::Allocate<TSyncInputStreamAdapterIntermediateBufferTag>(length);
+ } else {
+ readBuffer = TSharedMutableRef(buffer, length, /*holder*/ nullptr);
+ }
+
+ auto bytesRead = WaitForWithStrategy(UnderlyingStream_->Read(readBuffer), Strategy_)
+ .ValueOrThrow();
+
+ if (Strategy_ == EWaitForStrategy::WaitFor) {
+ memcpy(buffer, readBuffer.Begin(), bytesRead);
+ }
+
+ return bytesRead;
+ }
+};
+
+std::unique_ptr<IInputStream> CreateSyncAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ EWaitForStrategy strategy)
+{
+ YT_VERIFY(underlyingStream);
+ return std::make_unique<TSyncInputStreamAdapter>(
+ std::move(underlyingStream),
+ strategy);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncInputStreamAdapter
+ : public IAsyncInputStream
+{
+public:
+ TAsyncInputStreamAdapter(
+ IInputStream* underlyingStream,
+ IInvokerPtr invoker)
+ : UnderlyingStream_(underlyingStream)
+ , Invoker_(std::move(invoker))
+ { }
+
+ TFuture<size_t> Read(const TSharedMutableRef& buffer) override
+ {
+ return
+ BIND(&TAsyncInputStreamAdapter::DoRead, MakeStrong(this), buffer)
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+private:
+ size_t DoRead(const TSharedMutableRef& buffer) const
+ {
+ return UnderlyingStream_->Read(buffer.Begin(), buffer.Size());
+ }
+
+private:
+ IInputStream* const UnderlyingStream_;
+ const IInvokerPtr Invoker_;
+};
+
+IAsyncInputStreamPtr CreateAsyncAdapter(
+ IInputStream* underlyingStream,
+ IInvokerPtr invoker)
+{
+ YT_VERIFY(underlyingStream);
+ return New<TAsyncInputStreamAdapter>(underlyingStream, std::move(invoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSyncBufferedOutputStreamAdapter
+ : public IZeroCopyOutput
+{
+public:
+ TSyncBufferedOutputStreamAdapter(
+ IAsyncOutputStreamPtr underlyingStream,
+ EWaitForStrategy strategy,
+ size_t bufferCapacity)
+ : UnderlyingStream_(std::move(underlyingStream))
+ , Strategy_(strategy)
+ , BufferCapacity_(bufferCapacity)
+ {
+ Reset();
+ }
+
+ virtual ~TSyncBufferedOutputStreamAdapter()
+ {
+ try {
+ Finish();
+ } catch (...) {
+ }
+ }
+
+private:
+ const IAsyncOutputStreamPtr UnderlyingStream_;
+ const EWaitForStrategy Strategy_;
+ const size_t BufferCapacity_;
+ size_t CurrentBufferSize_;
+ TSharedMutableRef Buffer_;
+
+ struct TBufferTag
+ { };
+
+ void Reset()
+ {
+ CurrentBufferSize_ = 0;
+ Buffer_ = TSharedMutableRef::Allocate<TBufferTag>(BufferCapacity_);
+ }
+
+ void* WriteToBuffer(const void* data, size_t length)
+ {
+ YT_ASSERT(length <= GetBufferSpaceLeft());
+ char* ptr = Buffer_.Begin() + CurrentBufferSize_;
+ ::memcpy(Buffer_.Begin() + CurrentBufferSize_, data, length);
+ CurrentBufferSize_ += length;
+ return ptr;
+ }
+
+ void WriteToStream(const void* data, size_t length)
+ {
+ auto sharedBuffer = TSharedRef::MakeCopy<TBufferTag>(TRef(data, length));
+ auto future = UnderlyingStream_->Write(std::move(sharedBuffer));
+ WaitForWithStrategy(std::move(future), Strategy_)
+ .ThrowOnError();
+ }
+
+ size_t GetBufferSpaceLeft() const
+ {
+ return BufferCapacity_ - CurrentBufferSize_;
+ }
+
+ size_t GetBufferSize() const
+ {
+ return CurrentBufferSize_;
+ }
+
+protected:
+ size_t DoNext(void** ptr) override
+ {
+ if (GetBufferSpaceLeft() == 0) {
+ DoFlush();
+ }
+
+ auto size = GetBufferSpaceLeft();
+ *ptr = Buffer_.Begin() + CurrentBufferSize_;
+ CurrentBufferSize_ += size;
+
+ return size;
+ }
+
+ void DoUndo(size_t size) override
+ {
+ YT_VERIFY(CurrentBufferSize_ >= size);
+ CurrentBufferSize_ -= size;
+ }
+
+ void DoWrite(const void* buffer, size_t length) override
+ {
+ if (length > GetBufferSpaceLeft()) {
+ DoFlush();
+ }
+ if (length <= GetBufferSpaceLeft()) {
+ WriteToBuffer(buffer, length);
+ } else {
+ WriteToStream(buffer, length);
+ }
+ }
+
+ void DoFlush() override
+ {
+ if (CurrentBufferSize_ == 0) {
+ return;
+ }
+ auto writeFuture = UnderlyingStream_->Write(Buffer_.Slice(0, CurrentBufferSize_));
+ WaitForWithStrategy(std::move(writeFuture), Strategy_)
+ .ThrowOnError();
+ Reset();
+ }
+};
+
+std::unique_ptr<IZeroCopyOutput> CreateBufferedSyncAdapter(
+ IAsyncOutputStreamPtr underlyingStream,
+ EWaitForStrategy strategy,
+ size_t bufferSize)
+{
+ YT_VERIFY(underlyingStream);
+ return std::make_unique<TSyncBufferedOutputStreamAdapter>(
+ std::move(underlyingStream),
+ strategy,
+ bufferSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFlushableAsyncOutputStreamAdapter
+ : public IFlushableAsyncOutputStream
+{
+public:
+ TFlushableAsyncOutputStreamAdapter(
+ IOutputStream* underlyingStream,
+ IInvokerPtr invoker)
+ : UnderlyingStream_(underlyingStream)
+ , Invoker_(std::move(invoker))
+ { }
+
+ TFuture<void> Write(const TSharedRef& buffer) override
+ {
+ return BIND(&TFlushableAsyncOutputStreamAdapter::DoWrite, MakeStrong(this), buffer)
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+ TFuture<void> Flush() override
+ {
+ return BIND(&TFlushableAsyncOutputStreamAdapter::DoFlush, MakeStrong(this))
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+ TFuture<void> Close() override
+ {
+ return BIND(&TFlushableAsyncOutputStreamAdapter::DoFinish, MakeStrong(this))
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+private:
+ void DoWrite(const TSharedRef& buffer) const
+ {
+ UnderlyingStream_->Write(buffer.Begin(), buffer.Size());
+ }
+
+ void DoFlush() const
+ {
+ UnderlyingStream_->Flush();
+ }
+
+ void DoFinish() const
+ {
+ UnderlyingStream_->Finish();
+ }
+
+private:
+ IOutputStream* const UnderlyingStream_;
+ const IInvokerPtr Invoker_;
+};
+
+IFlushableAsyncOutputStreamPtr CreateAsyncAdapter(
+ IOutputStream* underlyingStream,
+ IInvokerPtr invoker)
+{
+ YT_VERIFY(underlyingStream);
+ return New<TFlushableAsyncOutputStreamAdapter>(underlyingStream, std::move(invoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef IAsyncZeroCopyInputStream::ReadAll()
+{
+ struct TTag
+ { };
+
+ std::vector<TSharedRef> chunks;
+
+ // TODO(prime@): Add hard limit on body size.
+ while (true) {
+ auto chunk = WaitFor(Read())
+ .ValueOrThrow();
+
+ if (chunk.Empty()) {
+ break;
+ }
+
+ chunks.emplace_back(TSharedRef::MakeCopy<TTag>(chunk));
+ }
+
+ return MergeRefsToRef<TTag>(std::move(chunks));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZeroCopyInputStreamAdapter
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TZeroCopyInputStreamAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t blockSize)
+ : UnderlyingStream_(std::move(underlyingStream))
+ , BlockSize_(blockSize)
+ { }
+
+ TFuture<TSharedRef> Read() override
+ {
+ struct TZeroCopyInputStreamAdapterBlockTag
+ { };
+
+ auto promise = NewPromise<TSharedRef>();
+ auto block = TSharedMutableRef::Allocate<TZeroCopyInputStreamAdapterBlockTag>(BlockSize_, {.InitializeStorage = false});
+
+ DoRead(promise, std::move(block), 0);
+
+ return promise;
+ }
+
+private:
+ const IAsyncInputStreamPtr UnderlyingStream_;
+ const size_t BlockSize_;
+
+
+ void DoRead(
+ TPromise<TSharedRef> promise,
+ TSharedMutableRef block,
+ size_t offset)
+ {
+ if (block.Size() == offset) {
+ promise.Set(std::move(block));
+ return;
+ }
+
+ UnderlyingStream_->Read(block.Slice(offset, block.Size())).Subscribe(
+ BIND(
+ &TZeroCopyInputStreamAdapter::OnRead,
+ MakeStrong(this),
+ std::move(promise),
+ std::move(block),
+ offset).Via(GetSyncInvoker()));
+ }
+
+ void OnRead(
+ TPromise<TSharedRef> promise,
+ TSharedMutableRef block,
+ size_t offset,
+ const TErrorOr<size_t>& result)
+ {
+ if (!result.IsOK()) {
+ promise.Set(TError(result));
+ return;
+ }
+
+ auto bytes = result.Value();
+
+ if (bytes == 0) {
+ promise.Set(offset == 0 ? TSharedRef() : block.Slice(0, offset));
+ return;
+ }
+
+ DoRead(std::move(promise), std::move(block), offset + bytes);
+ }
+};
+
+IAsyncZeroCopyInputStreamPtr CreateZeroCopyAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t blockSize)
+{
+ YT_VERIFY(underlyingStream);
+ return New<TZeroCopyInputStreamAdapter>(underlyingStream, blockSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCopyingInputStreamAdapter
+ : public IAsyncInputStream
+{
+public:
+ explicit TCopyingInputStreamAdapter(IAsyncZeroCopyInputStreamPtr underlyingStream)
+ : UnderlyingStream_(underlyingStream)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ }
+
+ TFuture<size_t> Read(const TSharedMutableRef& buffer) override
+ {
+ if (CurrentBlock_) {
+ // NB(psushin): no swapping here, it's a _copying_ adapter!
+ // Also, #buffer may be constructed via FromNonOwningRef.
+ return MakeFuture<size_t>(DoCopy(buffer));
+ } else {
+ return UnderlyingStream_->Read().Apply(
+ BIND(&TCopyingInputStreamAdapter::OnRead, MakeStrong(this), buffer));
+ }
+ }
+
+private:
+ const IAsyncZeroCopyInputStreamPtr UnderlyingStream_;
+
+ TSharedRef CurrentBlock_;
+ i64 CurrentOffset_ = 0;
+
+
+ size_t DoCopy(const TMutableRef& buffer)
+ {
+ size_t remaining = CurrentBlock_.Size() - CurrentOffset_;
+ size_t bytes = std::min(buffer.Size(), remaining);
+ ::memcpy(buffer.Begin(), CurrentBlock_.Begin() + CurrentOffset_, bytes);
+ CurrentOffset_ += bytes;
+ if (CurrentOffset_ == std::ssize(CurrentBlock_)) {
+ CurrentBlock_.Reset();
+ CurrentOffset_ = 0;
+ }
+ return bytes;
+ }
+
+ size_t OnRead(const TSharedMutableRef& buffer, const TSharedRef& block)
+ {
+ CurrentBlock_ = block;
+ return DoCopy(buffer);
+ }
+
+};
+
+IAsyncInputStreamPtr CreateCopyingAdapter(IAsyncZeroCopyInputStreamPtr underlyingStream)
+{
+ return New<TCopyingInputStreamAdapter>(underlyingStream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZeroCopyOutputStreamAdapter
+ : public IAsyncZeroCopyOutputStream
+{
+public:
+ explicit TZeroCopyOutputStreamAdapter(IAsyncOutputStreamPtr underlyingStream)
+ : UnderlyingStream_(underlyingStream)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ }
+
+ TFuture<void> Write(const TSharedRef& data) override
+ {
+ YT_ASSERT(data);
+ return Push(data);
+ }
+
+ TFuture<void> Close() override
+ {
+ return Push(TSharedRef());
+ }
+
+private:
+ const IAsyncOutputStreamPtr UnderlyingStream_;
+
+ struct TEntry
+ {
+ // If `Block' is null it means close was requested.
+ TSharedRef Block;
+ TPromise<void> Promise;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::queue<TEntry> Queue_;
+ TError Error_;
+ bool Closed_ = false;
+
+ TFuture<void> Push(const TSharedRef& data)
+ {
+ TPromise<void> promise;
+ bool needInvoke;
+ {
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(!Closed_);
+ if (!Error_.IsOK()) {
+ return MakeFuture(Error_);
+ }
+ promise = NewPromise<void>();
+ Queue_.push(TEntry{data, promise});
+ needInvoke = (Queue_.size() == 1);
+ Closed_ = !data;
+ }
+ if (needInvoke) {
+ TFuture<void> invokeResult;
+ if (data) {
+ invokeResult = UnderlyingStream_->Write(data);
+ } else {
+ invokeResult = UnderlyingStream_->Close();
+ }
+ invokeResult.Subscribe(
+ BIND(&TZeroCopyOutputStreamAdapter::OnWritten, MakeStrong(this)));
+ }
+ return promise;
+ }
+
+ void OnWritten(const TError& error)
+ {
+ TSharedRef data;
+ bool hasData = NotifyAndFetchNext(error, &data);
+ while (hasData) {
+ if (error.IsOK()) {
+ TFuture<void> result;
+ if (data) {
+ result = UnderlyingStream_->Write(data);
+ } else {
+ result = UnderlyingStream_->Close();
+ }
+ auto mayWriteResult = result.TryGet();
+ if (!mayWriteResult || !mayWriteResult->IsOK()) {
+ result.Subscribe(
+ BIND(&TZeroCopyOutputStreamAdapter::OnWritten, MakeStrong(this)));
+ break;
+ }
+ }
+
+ hasData = NotifyAndFetchNext(error, &data);
+ }
+ }
+
+ // Set current entry promise to error and tries to fetch next entry data.
+ // Return `false' if there no next entry.
+ // Otherwise return `true' and fill data with next entry Block.
+ bool NotifyAndFetchNext(const TError& error, TSharedRef* data)
+ {
+ TPromise<void> promise;
+ bool hasData = false;
+ {
+ auto guard = Guard(SpinLock_);
+ auto& entry = Queue_.front();
+ promise = std::move(entry.Promise);
+ if (!error.IsOK() && Error_.IsOK()) {
+ Error_ = error;
+ }
+ Queue_.pop();
+ hasData = !Queue_.empty();
+ if (hasData) {
+ *data = Queue_.front().Block;
+ }
+ }
+ promise.Set(error);
+ return hasData;
+ }
+};
+
+IAsyncZeroCopyOutputStreamPtr CreateZeroCopyAdapter(IAsyncOutputStreamPtr underlyingStream)
+{
+ return New<TZeroCopyOutputStreamAdapter>(underlyingStream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCopyingOutputStreamAdapter
+ : public IAsyncOutputStream
+{
+public:
+ explicit TCopyingOutputStreamAdapter(IAsyncZeroCopyOutputStreamPtr underlyingStream)
+ : UnderlyingStream_(underlyingStream)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ }
+
+ TFuture<void> Write(const TSharedRef& buffer) override
+ {
+ struct TCopyingOutputStreamAdapterBlockTag { };
+ auto block = TSharedMutableRef::Allocate<TCopyingOutputStreamAdapterBlockTag>(buffer.Size(), {.InitializeStorage = false});
+ ::memcpy(block.Begin(), buffer.Begin(), buffer.Size());
+ return UnderlyingStream_->Write(block);
+ }
+
+ TFuture<void> Close() override
+ {
+ return UnderlyingStream_->Close();
+ }
+
+private:
+ const IAsyncZeroCopyOutputStreamPtr UnderlyingStream_;
+};
+
+IAsyncOutputStreamPtr CreateCopyingAdapter(IAsyncZeroCopyOutputStreamPtr underlyingStream)
+{
+ return New<TCopyingOutputStreamAdapter>(underlyingStream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrefetchingInputStreamAdapter
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TPrefetchingInputStreamAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream,
+ size_t windowSize)
+ : UnderlyingStream_(underlyingStream)
+ , WindowSize_(windowSize)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ YT_VERIFY(WindowSize_ > 0);
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ auto guard = Guard(SpinLock_);
+ if (!Error_.IsOK()) {
+ return MakeFuture<TSharedRef>(Error_);
+ }
+ if (PrefetchedBlocks_.empty()) {
+ return Prefetch(&guard).Apply(
+ BIND(&TPrefetchingInputStreamAdapter::OnPrefetched, MakeStrong(this)));
+ }
+ return MakeFuture<TSharedRef>(PopBlock(&guard));
+ }
+
+private:
+ const IAsyncZeroCopyInputStreamPtr UnderlyingStream_;
+ const size_t WindowSize_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TError Error_;
+ std::queue<TSharedRef> PrefetchedBlocks_;
+ size_t PrefetchedSize_ = 0;
+ TFuture<void> OutstandingResult_;
+
+
+ TFuture<void> Prefetch(TGuard<NThreading::TSpinLock>* guard)
+ {
+ if (OutstandingResult_) {
+ return OutstandingResult_;
+ }
+ auto promise = NewPromise<void>();
+ OutstandingResult_ = promise;
+ guard->Release();
+ UnderlyingStream_->Read().Subscribe(BIND(
+ &TPrefetchingInputStreamAdapter::OnRead,
+ MakeStrong(this),
+ promise));
+ return promise;
+ }
+
+ void OnRead(TPromise<void> promise, const TErrorOr<TSharedRef>& result)
+ {
+ {
+ auto guard = Guard(SpinLock_);
+ PushBlock(&guard, result);
+ }
+ promise.Set(result);
+ }
+
+ TSharedRef OnPrefetched()
+ {
+ auto guard = Guard(SpinLock_);
+ return PopBlock(&guard);
+ }
+
+ void PushBlock(TGuard<NThreading::TSpinLock>* guard, const TErrorOr<TSharedRef>& result)
+ {
+ YT_ASSERT(OutstandingResult_);
+ OutstandingResult_.Reset();
+ if (!result.IsOK()) {
+ Error_ = TError(result);
+ return;
+ }
+ const auto& block = result.Value();
+ PrefetchedBlocks_.push(block);
+ PrefetchedSize_ += block.Size();
+ if (block && PrefetchedSize_ < WindowSize_) {
+ YT_UNUSED_FUTURE(Prefetch(guard));
+ }
+ }
+
+ TSharedRef PopBlock(TGuard<NThreading::TSpinLock>* guard)
+ {
+ YT_ASSERT(!PrefetchedBlocks_.empty());
+ auto block = PrefetchedBlocks_.front();
+ PrefetchedBlocks_.pop();
+ PrefetchedSize_ -= block.Size();
+ if (!OutstandingResult_ && PrefetchedSize_ < WindowSize_) {
+ YT_UNUSED_FUTURE(Prefetch(guard));
+ }
+ return block;
+ }
+
+};
+
+IAsyncZeroCopyInputStreamPtr CreatePrefetchingAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream,
+ size_t windowSize)
+{
+ return New<TPrefetchingInputStreamAdapter>(underlyingStream, windowSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBufferingInputStreamAdapterBufferTag { };
+
+class TBufferingInputStreamAdapter
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TBufferingInputStreamAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t windowSize)
+ : UnderlyingStream_(underlyingStream)
+ , WindowSize_(windowSize)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ YT_VERIFY(WindowSize_ > 0);
+
+ Buffer_ = TSharedMutableRef::Allocate<TBufferingInputStreamAdapterBufferTag>(WindowSize_, {.InitializeStorage = false});
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ auto guard = Guard(SpinLock_);
+ if (PrefetchedSize_ == 0) {
+ if (EndOfStream_) {
+ return MakeFuture<TSharedRef>(TSharedRef());
+ }
+ if (!Error_.IsOK()) {
+ return MakeFuture<TSharedRef>(Error_);
+ }
+ return Prefetch(&guard).Apply(
+ BIND(&TBufferingInputStreamAdapter::OnPrefetched, MakeStrong(this)));
+ }
+ return MakeFuture<TSharedRef>(CopyPrefetched(&guard));
+ }
+
+private:
+ const IAsyncInputStreamPtr UnderlyingStream_;
+ const size_t WindowSize_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TError Error_;
+ TSharedMutableRef Prefetched_;
+ TSharedMutableRef Buffer_;
+ size_t PrefetchedSize_ = 0;
+ bool EndOfStream_ = false;
+ TFuture<void> OutstandingResult_;
+
+ TFuture<void> Prefetch(TGuard<NThreading::TSpinLock>* guard)
+ {
+ if (OutstandingResult_) {
+ return OutstandingResult_;
+ }
+ auto promise = NewPromise<void>();
+ OutstandingResult_ = promise;
+ guard->Release();
+ UnderlyingStream_->Read(Buffer_.Slice(0, WindowSize_ - PrefetchedSize_)).Subscribe(BIND(
+ &TBufferingInputStreamAdapter::OnRead,
+ MakeStrong(this),
+ promise));
+ return promise;
+ }
+
+ void OnRead(TPromise<void> promise, const TErrorOr<size_t>& result)
+ {
+ {
+ auto guard = Guard(SpinLock_);
+ AppendPrefetched(&guard, result);
+ }
+ promise.Set(result);
+ }
+
+ TSharedRef OnPrefetched()
+ {
+ auto guard = Guard(SpinLock_);
+ YT_ASSERT(PrefetchedSize_ != 0);
+ return CopyPrefetched(&guard);
+ }
+
+ void AppendPrefetched(TGuard<NThreading::TSpinLock>* guard, const TErrorOr<size_t>& result)
+ {
+ YT_ASSERT(OutstandingResult_);
+ OutstandingResult_.Reset();
+ if (!result.IsOK()) {
+ Error_ = TError(result);
+ return;
+ } else if (result.Value() == 0) {
+ EndOfStream_ = true;
+ return;
+ }
+ size_t bytes = result.Value();
+ if (bytes != 0) {
+ if (PrefetchedSize_ == 0) {
+ Prefetched_ = Buffer_;
+ Buffer_ = TSharedMutableRef::Allocate<TBufferingInputStreamAdapterBufferTag>(WindowSize_, {.InitializeStorage = false});
+ } else {
+ ::memcpy(Prefetched_.Begin() + PrefetchedSize_, Buffer_.Begin(), bytes);
+ }
+ PrefetchedSize_ += bytes;
+
+ }
+ // Stop reading on the end of stream or full buffer.
+ if (bytes != 0 && PrefetchedSize_ < WindowSize_) {
+ YT_UNUSED_FUTURE(Prefetch(guard));
+ }
+ }
+
+ TSharedRef CopyPrefetched(TGuard<NThreading::TSpinLock>* guard)
+ {
+ YT_ASSERT(PrefetchedSize_ != 0);
+ auto block = Prefetched_.Slice(0, PrefetchedSize_);
+ Prefetched_ = TSharedMutableRef();
+ PrefetchedSize_ = 0;
+ if (!OutstandingResult_) {
+ YT_UNUSED_FUTURE(Prefetch(guard));
+ }
+ return block;
+ }
+
+};
+
+IAsyncZeroCopyInputStreamPtr CreateBufferingAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t windowSize)
+{
+ return New<TBufferingInputStreamAdapter>(underlyingStream, windowSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TExpiringInputStreamAdapter
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TExpiringInputStreamAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream,
+ TDuration timeout)
+ : UnderlyingStream_(underlyingStream)
+ , Timeout_(timeout)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ YT_VERIFY(Timeout_ > TDuration::Zero());
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (PendingBlock_) {
+ auto block = std::move(PendingBlock_);
+ PendingBlock_.reset();
+
+ return MakeFuture<TSharedRef>(*block);
+ }
+
+ auto promise = NewPromise<TSharedRef>();
+ Cookie_ = TDelayedExecutor::Submit(
+ BIND(&TExpiringInputStreamAdapter::OnTimeout, MakeWeak(this), promise), Timeout_);
+
+ YT_ASSERT(!Promise_);
+ Promise_ = promise;
+
+ if (!Fetching_) {
+ Fetching_ = true;
+ guard.Release();
+
+ UnderlyingStream_->Read().Subscribe(
+ BIND(&TExpiringInputStreamAdapter::OnRead, MakeWeak(this)));
+ }
+ return promise;
+ }
+
+private:
+ const IAsyncZeroCopyInputStreamPtr UnderlyingStream_;
+ const TDuration Timeout_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ bool Fetching_ = false;
+ std::optional<TErrorOr<TSharedRef>> PendingBlock_;
+ TPromise<TSharedRef> Promise_;
+ TDelayedExecutorCookie Cookie_;
+
+ void OnRead(const TErrorOr<TSharedRef>& value)
+ {
+ TPromise<TSharedRef> promise;
+ auto guard = Guard(SpinLock_);
+ Fetching_ = false;
+ if (Promise_) {
+ swap(Promise_, promise);
+ TDelayedExecutor::CancelAndClear(Cookie_);
+ guard.Release();
+ promise.Set(value);
+ } else {
+ PendingBlock_ = value;
+ }
+
+ }
+
+ void OnTimeout(TPromise<TSharedRef> promise, bool aborted)
+ {
+ bool timedOut = false;
+ {
+ auto guard = Guard(SpinLock_);
+ if (promise == Promise_) {
+ Promise_ = TPromise<TSharedRef>();
+ timedOut = true;
+ }
+ }
+
+ if (timedOut) {
+ TError error;
+ if (aborted) {
+ error = TError(NYT::EErrorCode::Canceled, "Operation aborted");
+ } else {
+ error = TError(NYT::EErrorCode::Timeout, "Operation timed out")
+ << TErrorAttribute("timeout", Timeout_);
+ }
+ promise.Set(error);
+ }
+ }
+};
+
+IAsyncZeroCopyInputStreamPtr CreateExpiringAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream,
+ TDuration timeout)
+{
+ return New<TExpiringInputStreamAdapter>(underlyingStream, timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConcurrentInputStreamAdapter
+ : public IAsyncZeroCopyInputStream
+{
+public:
+ TConcurrentInputStreamAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream)
+ : UnderlyingStream_(underlyingStream)
+ {
+ YT_VERIFY(UnderlyingStream_);
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (PendingBlock_) {
+ auto block = std::move(PendingBlock_);
+ PendingBlock_.reset();
+
+ return MakeFuture<TSharedRef>(*block);
+ }
+
+ auto newPromise = NewPromise<TSharedRef>();
+ auto oldPromise = newPromise;
+ swap(oldPromise, Promise_);
+
+ if (!Fetching_) {
+ Fetching_ = true;
+ guard.Release();
+
+ UnderlyingStream_->Read().Subscribe(
+ BIND(&TConcurrentInputStreamAdapter::OnRead, MakeWeak(this)));
+ }
+ // Always set the pending promise from previous Read.
+ if (oldPromise) {
+ guard.Release();
+ oldPromise.TrySet(TError(NYT::EErrorCode::Canceled, "Read canceled"));
+ }
+ return newPromise;
+ }
+
+private:
+ const IAsyncZeroCopyInputStreamPtr UnderlyingStream_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ bool Fetching_ = false;
+ std::optional<TErrorOr<TSharedRef>> PendingBlock_;
+ TPromise<TSharedRef> Promise_;
+
+ void OnRead(const TErrorOr<TSharedRef>& value)
+ {
+ TPromise<TSharedRef> promise;
+ {
+ auto guard = Guard(SpinLock_);
+ Fetching_ = false;
+ YT_ASSERT(Promise_);
+ swap(promise, Promise_);
+ if (promise.IsSet()) {
+ YT_ASSERT(!PendingBlock_);
+ PendingBlock_ = value;
+ return;
+ }
+ }
+ promise.Set(value);
+ }
+};
+
+IAsyncZeroCopyInputStreamPtr CreateConcurrentAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream)
+{
+ return New<TConcurrentInputStreamAdapter>(underlyingStream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB(levysotsky): Doesn't close the output stream.
+void PipeInputToOutput(
+ IAsyncZeroCopyInputStreamPtr input,
+ IAsyncOutputStreamPtr output)
+{
+ while (true) {
+ auto asyncBlock = input->Read();
+ auto block = WaitFor(asyncBlock)
+ .ValueOrThrow();
+ if (!block || block.Empty()) {
+ break;
+ }
+ WaitFor(output->Write(block))
+ .ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> ExpectEndOfStream(
+ const IAsyncZeroCopyInputStreamPtr& input)
+{
+ YT_VERIFY(input);
+ return input->Read().Apply(BIND([] (const TSharedRef& ref) {
+ if (ref) {
+ THROW_ERROR_EXCEPTION("Expected end-of-stream, received a non-null ref of size %v",
+ ref.Size());
+ }
+ }));
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_stream.h b/yt/yt/core/concurrency/async_stream.h
new file mode 100644
index 0000000000..ea07583c18
--- /dev/null
+++ b/yt/yt/core/concurrency/async_stream.h
@@ -0,0 +1,229 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides an asynchronous interface for reading from a stream.
+struct IAsyncInputStream
+ : public virtual TRefCounted
+{
+ //! Starts reading another block of data.
+ /*!
+ * Call #Read and provide a buffer to start reading.
+ * One must not call #Read again before the previous call is complete.
+ * Returns number of bytes read or an error.
+ */
+ [[nodiscard]] virtual TFuture<size_t> Read(const TSharedMutableRef& buffer) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncInputStream)
+
+//! Creates a synchronous adapter from a given asynchronous stream.
+/*!
+ * NB: in order to ensure memory safety with WaitFor strategy, data is read to an
+ * intermediate shared buffer and then copied to the destination buffer.
+ * Do not use this wrapper in throughput-critical code, prefer using
+ * async or async zero-copy input stream interface instead.
+ */
+std::unique_ptr<IInputStream> CreateSyncAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ EWaitForStrategy strategy = EWaitForStrategy::WaitFor);
+
+//! Creates an asynchronous adapter from a given synchronous stream.
+/*!
+ * Caller may provide an invoker for all calls to the underlying stream.
+ * This way one can ensure that current thread will not block in calls
+ * to the adapter.
+ */
+IAsyncInputStreamPtr CreateAsyncAdapter(
+ IInputStream* underlyingStream,
+ IInvokerPtr invoker = GetSyncInvoker());
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides an asynchronous interface for writing to a stream.
+struct IAsyncOutputStream
+ : public virtual TRefCounted
+{
+ //! Starts writing another block of data.
+ /*!
+ * Call #Write to issue a write request.
+ * Buffer passed to #Write must remain valid until the returned future is set.
+ * One must not call #Write again before the previous call is complete.
+ *
+ * Implementations must not rely on the content of #buffer to remain immutable
+ * between calls to #Write; e.g. clients are allowed to reuse a single (mutable)
+ * buffer between these calls.
+ */
+ [[nodiscard]] virtual TFuture<void> Write(const TSharedRef& buffer) = 0;
+
+ //! Finalizes stream.
+ /*! Call #Close to complete writes.
+ * #Close shouldn't be called before previous #Write call is complete.
+ * #Write/#Close mustn't be called after #Close was called.
+ */
+ [[nodiscard]] virtual TFuture<void> Close() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncOutputStream)
+
+struct IFlushableAsyncOutputStream
+ : public IAsyncOutputStream
+{
+ //! Starts flushing the stream.
+ /*! Call #Flush to complete preceding writes.
+ * #Flush shouldn't be called before previous #Write call is complete.
+ * #Flush mustn't be called after #Close was called.
+ */
+ [[nodiscard]] virtual TFuture<void> Flush() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFlushableAsyncOutputStream)
+
+//! Creates a synchronous buffering adapter from a given asynchronous stream.
+/*!
+ * Not thread safe.
+ */
+std::unique_ptr<IZeroCopyOutput> CreateBufferedSyncAdapter(
+ IAsyncOutputStreamPtr underlyingStream,
+ EWaitForStrategy strategy = EWaitForStrategy::WaitFor,
+ size_t bufferSize = 8_KB);
+
+//! Creates an asynchronous adapter from a given synchronous stream.
+/*!
+ * Caller may provide an invoker for all calls to the underlying stream.
+ * This way one can ensure that current thread will not block in calls
+ * to the adapter.
+ */
+IFlushableAsyncOutputStreamPtr CreateAsyncAdapter(
+ IOutputStream* underlyingStream,
+ IInvokerPtr invoker = GetSyncInvoker());
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Similar to IAsyncInputStream but is essentially zero-copy, i.e.
+//! produces a sequence of memory blocks with shared ownership.
+struct IAsyncZeroCopyInputStream
+ : public virtual TRefCounted
+{
+ //! Requests another block of data.
+ /*!
+ * Returns the data or an error.
+ * If a null TSharedRef is returned then end-of-stream is reached.
+ * One must not call #Read again before the previous call is complete.
+ * A sane implementation must guarantee that it returns blocks of sensible size.
+ */
+ [[nodiscard]] virtual TFuture<TSharedRef> Read() = 0;
+
+ // Extension methods
+
+ //! Reads all content from the stream by iteratively calling #Read until the stream is exhausted.
+ /*!
+ * \note
+ * May (and typically will) cause fiber context switch.
+ */
+ TSharedRef ReadAll();
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncZeroCopyInputStream)
+
+//! Creates a zero-copy adapter from a given asynchronous stream.
+IAsyncZeroCopyInputStreamPtr CreateZeroCopyAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t blockSize = 64 * 1024);
+
+//! Creates a copying adapter from a given asynchronous zero-copy stream.
+IAsyncInputStreamPtr CreateCopyingAdapter(IAsyncZeroCopyInputStreamPtr underlyingStream);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Similar to IAsyncOutputStream but is essentially zero-copy, i.e.
+//! consumes a sequence of memory blocks with shared ownership.
+struct IAsyncZeroCopyOutputStream
+ : public virtual TRefCounted
+{
+ //! Enqueues another block of data.
+ /*!
+ * Returns an error, if any.
+ * In contrast to IAsyncOutputStream, one may call #Write again before
+ * the previous call is complete. The returned future, however, provides
+ * means to implement backpressure.
+ *
+ * NB: this shared ref should become unique ref.
+ */
+ [[nodiscard]] virtual TFuture<void> Write(const TSharedRef& data) = 0;
+
+ //! Indicates that the stream is closed.
+ /*!
+ * No #Write calls are possible after #Close.
+ */
+ [[nodiscard]] virtual TFuture<void> Close() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncZeroCopyOutputStream)
+
+//! Creates a zero-copy adapter from a given asynchronous stream.
+IAsyncZeroCopyOutputStreamPtr CreateZeroCopyAdapter(IAsyncOutputStreamPtr underlyingStream);
+
+//! Creates a copying adapter from a given asynchronous zero-copy stream.
+IAsyncOutputStreamPtr CreateCopyingAdapter(IAsyncZeroCopyOutputStreamPtr underlyingStream);
+
+//! Creates an adapter that prefetches data in background.
+/*!
+ * The adapter tries to maintain up to #windowSize bytes of data by
+ * retrieving blocks from #underlyingStream in background.
+ */
+IAsyncZeroCopyInputStreamPtr CreatePrefetchingAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream,
+ size_t windowSize);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an adapter that prefetches data into the buffer on bytes scale.
+/*!
+ * The adapder differs from PrefetchingAdapter:
+ * - it is based on IAsyncInputStreamPtr underlying stream;
+ * - it works with bytes instead of blocks, provided by zero-copy adapters.
+ */
+IAsyncZeroCopyInputStreamPtr CreateBufferingAdapter(
+ IAsyncInputStreamPtr underlyingStream,
+ size_t windowSize);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an adapter that returns an error if no data is read within timeout.
+NConcurrency::IAsyncZeroCopyInputStreamPtr CreateExpiringAdapter(
+ NConcurrency::IAsyncZeroCopyInputStreamPtr underlyingStream,
+ TDuration timeout);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an adapter that can process concurrent Read() requests.
+IAsyncZeroCopyInputStreamPtr CreateConcurrentAdapter(
+ IAsyncZeroCopyInputStreamPtr underlyingStream);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PipeInputToOutput(
+ NConcurrency::IAsyncZeroCopyInputStreamPtr input,
+ NConcurrency::IAsyncOutputStreamPtr output);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> ExpectEndOfStream(
+ const IAsyncZeroCopyInputStreamPtr& input);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_stream_pipe.cpp b/yt/yt/core/concurrency/async_stream_pipe.cpp
new file mode 100644
index 0000000000..794786a9e8
--- /dev/null
+++ b/yt/yt/core/concurrency/async_stream_pipe.cpp
@@ -0,0 +1,55 @@
+#include "async_stream_pipe.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAsyncStreamPipeTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncStreamPipe::TItem::TItem(TSharedRef sharedRef, TPromise<void> writeComplete)
+ : Data(std::move(sharedRef))
+ , WriteComplete(std::move(writeComplete))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TAsyncStreamPipe::Write(const TSharedRef& buffer)
+{
+ if (!buffer) {
+ // Empty buffer has special meaning in our queue, so we don't write it.
+ return VoidFuture;
+ }
+
+ auto writeComplete = NewPromise<void>();
+ Queue_.Enqueue(TItem(TSharedRef::MakeCopy<TAsyncStreamPipeTag>(buffer), writeComplete));
+ return writeComplete;
+}
+
+TFuture<TSharedRef> TAsyncStreamPipe::Read()
+{
+ auto result = Queue_.Dequeue();
+ return result.Apply(BIND([] (TItem item) {
+ item.WriteComplete.Set();
+ return item.Data;
+ }));
+}
+
+TFuture<void> TAsyncStreamPipe::Close()
+{
+ Queue_.Enqueue(TItem(TSharedRef(), NewPromise<void>()));
+ return VoidFuture;
+}
+
+TFuture<void> TAsyncStreamPipe::Abort(const TError& error)
+{
+ auto writeComplete = NewPromise<void>();
+ Queue_.Enqueue(error);
+ return writeComplete;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/async_stream_pipe.h b/yt/yt/core/concurrency/async_stream_pipe.h
new file mode 100644
index 0000000000..2dbb6a527b
--- /dev/null
+++ b/yt/yt/core/concurrency/async_stream_pipe.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/concurrency/nonblocking_queue.h>
+
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncStreamPipe
+ : public IAsyncZeroCopyInputStream
+ , public IAsyncOutputStream
+{
+public:
+ TFuture<TSharedRef> Read() override;
+
+ TFuture<void> Write(const TSharedRef& buffer) override;
+ TFuture<void> Close() override;
+
+ TFuture<void> Abort(const TError& error);
+
+private:
+ struct TItem
+ {
+ // If Data is empty it means close was requested.
+ TSharedRef Data;
+ TPromise<void> WriteComplete;
+
+ TItem(TSharedRef sharedRef, TPromise<void> writeComplete);
+ };
+
+ TNonblockingQueue<TItem> Queue_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TAsyncStreamPipe)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/config.cpp b/yt/yt/core/concurrency/config.cpp
new file mode 100644
index 0000000000..e116aa00c5
--- /dev/null
+++ b/yt/yt/core/concurrency/config.cpp
@@ -0,0 +1,84 @@
+#include "config.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TThroughputThrottlerConfigPtr TThroughputThrottlerConfig::Create(std::optional<double> limit)
+{
+ auto result = New<TThroughputThrottlerConfig>();
+ result->Limit = limit;
+ return result;
+}
+
+void TThroughputThrottlerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("limit", &TThis::Limit)
+ .Default()
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("period", &TThis::Period)
+ .Default(TDuration::MilliSeconds(1000));
+}
+
+std::optional<i64> TThroughputThrottlerConfig::GetMaxAvailable() const {
+ if (Limit.has_value()) {
+ return static_cast<i64>(Period.SecondsFloat() * *Limit);
+ } else {
+ return std::nullopt;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRelativeThroughputThrottlerConfigPtr TRelativeThroughputThrottlerConfig::Create(std::optional<double> limit)
+{
+ auto result = New<TRelativeThroughputThrottlerConfig>();
+ result->Limit = limit;
+ return result;
+}
+
+void TRelativeThroughputThrottlerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("relative_limit", &TThis::RelativeLimit)
+ .InRange(0.0, 1.0)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TThroughputThrottlerConfig::operator==(const NConcurrency::TThroughputThrottlerConfig& other)
+{
+ return Limit == other.Limit && Period == other.Period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPrefetchingThrottlerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable", &TThis::Enable)
+ .Default(true);
+ registrar.Parameter("target_rps", &TThis::TargetRps)
+ .Default(1.0)
+ .GreaterThan(1e-3);
+ registrar.Parameter("min_prefetch_amount", &TThis::MinPrefetchAmount)
+ .Default(1)
+ .GreaterThanOrEqual(1);
+ registrar.Parameter("max_prefetch_amount", &TThis::MaxPrefetchAmount)
+ .Default(10)
+ .GreaterThanOrEqual(1);
+ registrar.Parameter("window", &TThis::Window)
+ .GreaterThan(TDuration::MilliSeconds(1))
+ .Default(TDuration::Seconds(1));
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MinPrefetchAmount > config->MaxPrefetchAmount) {
+ THROW_ERROR_EXCEPTION("\"min_prefetch_amount\" should be less than or equal \"max_prefetch_amount\"")
+ << TErrorAttribute("min_prefetch_amount", config->MinPrefetchAmount)
+ << TErrorAttribute("max_prefetch_amount", config->MaxPrefetchAmount);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/config.h b/yt/yt/core/concurrency/config.h
new file mode 100644
index 0000000000..9c02253f3e
--- /dev/null
+++ b/yt/yt/core/concurrency/config.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThroughputThrottlerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! Limit on average throughput (per sec). Null means unlimited.
+ std::optional<double> Limit;
+
+ //! Period for leaky bucket algorithm.
+ TDuration Period;
+
+ std::optional<i64> GetMaxAvailable() const;
+
+ static TThroughputThrottlerConfigPtr Create(std::optional<double> limit);
+
+ bool operator==(const TThroughputThrottlerConfig& other);
+
+ REGISTER_YSON_STRUCT(TThroughputThrottlerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TThroughputThrottlerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This wrapper may be helpful if we have some parametrization over the limit
+//! (e.g. in network bandwidth limit on nodes).
+//! The exact logic of limit/relative_limit clash resolution
+//! and the parameter access are external to the config itself.
+class TRelativeThroughputThrottlerConfig
+ : public TThroughputThrottlerConfig
+{
+public:
+ std::optional<double> RelativeLimit;
+
+ static TRelativeThroughputThrottlerConfigPtr Create(std::optional<double> limit);
+
+ REGISTER_YSON_STRUCT(TRelativeThroughputThrottlerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRelativeThroughputThrottlerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrefetchingThrottlerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! RPS limit for requests to the underlying throttler.
+ double TargetRps;
+
+ //! Minimum amount to be prefetched from the underlying throttler.
+ i64 MinPrefetchAmount;
+
+ //! Maximum amount to be prefetched from the underlying throttler.
+ //! Guards from a uncontrolled growth of the requested amount.
+ i64 MaxPrefetchAmount;
+
+ //! Time window for the RPS estimation.
+ TDuration Window;
+
+ //! Enable the prefetching throttler.
+ //! If disabled #CreatePrefetchingThrottler() will not create #TPrefetchingThrottler
+ //! and will return the underlying throttler instead.
+ //! #TPrefetchingThrottler itself does not check this field.
+ bool Enable;
+
+ REGISTER_YSON_STRUCT(TPrefetchingThrottlerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TPrefetchingThrottlerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/coroutine-inl.h b/yt/yt/core/concurrency/coroutine-inl.h
new file mode 100644
index 0000000000..e3bc8757a6
--- /dev/null
+++ b/yt/yt/core/concurrency/coroutine-inl.h
@@ -0,0 +1,118 @@
+#ifndef COROUTINE_INL_H_
+#error "Direct inclusion of this file is not allowed, include coroutine.h"
+// For the sake of sane code completion.
+#include "coroutine.h"
+#endif
+#undef COROUTINE_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class TCallee, class TCaller, class TArguments, size_t... Indexes>
+void Invoke(
+ TCallee& callee,
+ TCaller& caller,
+ TArguments&& arguments,
+ std::index_sequence<Indexes...>)
+{
+ callee(
+ caller,
+ std::get<Indexes>(std::forward<TArguments>(arguments))...);
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class R, class... TArgs>
+TCoroutine<R(TArgs...)>::TCoroutine(TCoroutine<R(TArgs...)>::TCallee&& callee, const EExecutionStackKind stackKind)
+ : NDetail::TCoroutineBase(stackKind)
+ , Callee_(std::move(callee))
+{ }
+
+template <class R, class... TArgs>
+template <class... TParams>
+const std::optional<R>& TCoroutine<R(TArgs...)>::Run(TParams&& ... params)
+{
+ static_assert(sizeof...(TParams) == sizeof...(TArgs),
+ "TParams<> and TArgs<> have different length");
+ Arguments_ = std::make_tuple(std::forward<TParams>(params)...);
+ JumpToCoroutine();
+ return Result_;
+}
+
+template <class R, class... TArgs>
+template <class Q>
+typename TCoroutine<R(TArgs...)>::TArguments&& TCoroutine<R(TArgs...)>::Yield(Q&& result)
+{
+ Result_ = std::forward<Q>(result);
+ JumpToCaller();
+ return std::move(Arguments_);
+}
+
+template <class R, class... TArgs>
+void TCoroutine<R(TArgs...)>::Invoke()
+{
+ try {
+ NDetail::Invoke(
+ Callee_,
+ *this,
+ std::move(Arguments_),
+ std::make_index_sequence<sizeof...(TArgs)>());
+ Result_.reset();
+ } catch (...) {
+ Result_.reset();
+ throw;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... TArgs>
+TCoroutine<void(TArgs...)>::TCoroutine(TCoroutine<void(TArgs...)>::TCallee&& callee, const EExecutionStackKind stackKind)
+ : NDetail::TCoroutineBase(stackKind)
+ , Callee_(std::move(callee))
+{ }
+
+template <class... TArgs>
+template <class... TParams>
+bool TCoroutine<void(TArgs...)>::Run(TParams&& ... params)
+{
+ static_assert(sizeof...(TParams) == sizeof...(TArgs),
+ "TParams<> and TArgs<> have different length");
+ Arguments_ = std::make_tuple(std::forward<TParams>(params)...);
+ JumpToCoroutine();
+ return Result_;
+}
+
+template <class... TArgs>
+void TCoroutine<void(TArgs...)>::Invoke()
+{
+ try {
+ NDetail::Invoke(
+ Callee_,
+ *this,
+ std::move(Arguments_),
+ std::make_index_sequence<sizeof...(TArgs)>());
+ Result_ = false;
+ } catch (...) {
+ Result_ = false;
+ throw;
+ }
+}
+
+
+template <class... TArgs>
+typename TCoroutine<void(TArgs...)>::TArguments&& TCoroutine<void(TArgs...)>::Yield()
+{
+ Result_ = true;
+ JumpToCaller();
+ return std::move(Arguments_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/coroutine.cpp b/yt/yt/core/concurrency/coroutine.cpp
new file mode 100644
index 0000000000..a5d3c51fdd
--- /dev/null
+++ b/yt/yt/core/concurrency/coroutine.cpp
@@ -0,0 +1,51 @@
+#include "coroutine.h"
+
+namespace NYT::NConcurrency::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCoroutineBase::TCoroutineBase(const EExecutionStackKind stackKind)
+ : CoroutineStack_(CreateExecutionStack(stackKind))
+ , CoroutineContext_({
+ this,
+ TArrayRef(static_cast<char*>(CoroutineStack_->GetStack()), CoroutineStack_->GetSize())})
+{ }
+
+void TCoroutineBase::DoRun()
+{
+ try {
+ Invoke();
+ } catch (...) {
+ CoroutineException_ = std::current_exception();
+ }
+
+ Completed_ = true;
+ JumpToCaller();
+
+ YT_ABORT();
+}
+
+void TCoroutineBase::JumpToCaller()
+{
+ CoroutineContext_.SwitchTo(&CallerContext_);
+}
+
+void TCoroutineBase::JumpToCoroutine()
+{
+ CallerContext_.SwitchTo(&CoroutineContext_);
+
+ if (CoroutineException_) {
+ std::exception_ptr exception;
+ std::swap(exception, CoroutineException_);
+ std::rethrow_exception(std::move(exception));
+ }
+}
+
+bool TCoroutineBase::IsCompleted() const
+{
+ return Completed_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency::NDetail
diff --git a/yt/yt/core/concurrency/coroutine.h b/yt/yt/core/concurrency/coroutine.h
new file mode 100644
index 0000000000..98bd79e8a1
--- /dev/null
+++ b/yt/yt/core/concurrency/coroutine.h
@@ -0,0 +1,119 @@
+#pragma once
+
+#include "public.h"
+#include "execution_stack.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <util/system/context.h>
+
+#include <optional>
+#include <type_traits>
+
+#ifdef _win_
+#undef Yield
+#endif
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+class TCoroutineBase
+ : public ITrampoLine
+{
+protected:
+ bool Completed_ = false;
+
+ TExceptionSafeContext CallerContext_;
+
+ std::shared_ptr<TExecutionStack> CoroutineStack_;
+ TExceptionSafeContext CoroutineContext_;
+ std::exception_ptr CoroutineException_;
+
+ TCoroutineBase(const EExecutionStackKind stackKind);
+
+ TCoroutineBase(const TCoroutineBase& other) = delete;
+ TCoroutineBase& operator=(const TCoroutineBase& other) = delete;
+
+ virtual ~TCoroutineBase() = default;
+
+ virtual void Invoke() = 0;
+
+ // ITrampoLine implementation
+ void DoRun() override;
+
+ void JumpToCaller();
+ void JumpToCoroutine();
+
+public:
+ bool IsCompleted() const;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class R, class... TArgs>
+class TCoroutine<R(TArgs...)>
+ : public NDetail::TCoroutineBase
+{
+public:
+ using TCallee = TCallback<void(TCoroutine&, TArgs...)>;
+ using TArguments = std::tuple<TArgs...>;
+
+ TCoroutine() = default;
+ TCoroutine(TCallee&& callee, const EExecutionStackKind stackKind = EExecutionStackKind::Small);
+
+ template <class... TParams>
+ const std::optional<R>& Run(TParams&&... params);
+
+ template <class Q>
+ TArguments&& Yield(Q&& result);
+
+private:
+ void Invoke() override;
+
+private:
+ const TCallee Callee_;
+
+ TArguments Arguments_;
+ std::optional<R> Result_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... TArgs>
+class TCoroutine<void(TArgs...)>
+ : public NDetail::TCoroutineBase
+{
+public:
+ using TCallee = TCallback<void(TCoroutine&, TArgs...)>;
+ using TArguments = std::tuple<TArgs...>;
+
+ TCoroutine() = default;
+ TCoroutine(TCallee&& callee, const EExecutionStackKind stackKind = EExecutionStackKind::Small);
+
+ template <class... TParams>
+ bool Run(TParams&&... params);
+
+ TArguments&& Yield();
+
+private:
+ void Invoke() override;
+
+private:
+ const TCallee Callee_;
+
+ TArguments Arguments_;
+ bool Result_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define COROUTINE_INL_H_
+#include "coroutine-inl.h"
+#undef COROUTINE_INL_H_
diff --git a/yt/yt/core/concurrency/delayed_executor.cpp b/yt/yt/core/concurrency/delayed_executor.cpp
new file mode 100644
index 0000000000..e7f040bccc
--- /dev/null
+++ b/yt/yt/core/concurrency/delayed_executor.cpp
@@ -0,0 +1,441 @@
+#include "delayed_executor.h"
+#include "action_queue.h"
+#include "scheduler.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/relaxed_mpsc_queue.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/threading/thread.h>
+
+#include <util/datetime/base.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto CoalescingInterval = TDuration::MicroSeconds(100);
+static constexpr auto LateWarningThreshold = TDuration::Seconds(1);
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+struct TDelayedExecutorEntry
+ : public TRefCounted
+{
+ struct TComparer
+ {
+ bool operator()(const TDelayedExecutorCookie& lhs, const TDelayedExecutorCookie& rhs) const
+ {
+ if (lhs->Deadline != rhs->Deadline) {
+ return lhs->Deadline < rhs->Deadline;
+ }
+ // Break ties.
+ return lhs < rhs;
+ }
+ };
+
+ TDelayedExecutorEntry(
+ TDelayedExecutor::TDelayedCallback callback,
+ TInstant deadline,
+ IInvokerPtr invoker)
+ : Callback(std::move(callback))
+ , Deadline(deadline)
+ , Invoker(std::move(invoker))
+ { }
+
+ TDelayedExecutor::TDelayedCallback Callback;
+ std::atomic<bool> CallbackTaken = false;
+ TInstant Deadline;
+ IInvokerPtr Invoker;
+
+ bool Canceled = false;
+ std::optional<std::set<TDelayedExecutorCookie, TComparer>::iterator> Iterator;
+};
+
+DEFINE_REFCOUNTED_TYPE(TDelayedExecutorEntry)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDelayedExecutorImpl
+{
+public:
+ using TDelayedCallback = TDelayedExecutor::TDelayedCallback;
+
+ static TDelayedExecutorImpl* Get()
+ {
+ return LeakySingleton<TDelayedExecutorImpl>();
+ }
+
+ TFuture<void> MakeDelayed(TDuration delay, IInvokerPtr invoker)
+ {
+ auto promise = NewPromise<void>();
+
+ auto cookie = Submit(
+ BIND_NO_PROPAGATE([=] (bool aborted) mutable {
+ if (aborted) {
+ promise.TrySet(TError(NYT::EErrorCode::Canceled, "Delayed promise aborted"));
+ } else {
+ promise.TrySet();
+ }
+ }),
+ delay,
+ std::move(invoker));
+
+ promise.OnCanceled(BIND_NO_PROPAGATE([=, cookie = std::move(cookie)] (const TError& error) {
+ TDelayedExecutor::Cancel(cookie);
+ promise.TrySet(TError(NYT::EErrorCode::Canceled, "Delayed callback canceled")
+ << error);
+ }));
+
+ return promise;
+ }
+
+ void WaitForDuration(TDuration duration)
+ {
+ if (duration == TDuration::Zero()) {
+ return;
+ }
+
+ auto error = WaitFor(MakeDelayed(duration, nullptr));
+ if (error.GetCode() == NYT::EErrorCode::Canceled) {
+ throw TFiberCanceledException();
+ }
+
+ error.ThrowOnError();
+ }
+
+ TDelayedExecutorCookie Submit(TClosure closure, TDuration delay, IInvokerPtr invoker)
+ {
+ YT_VERIFY(closure);
+ return Submit(
+ BIND_NO_PROPAGATE(&ClosureToDelayedCallbackAdapter, std::move(closure)),
+ delay.ToDeadLine(),
+ std::move(invoker));
+ }
+
+ TDelayedExecutorCookie Submit(TClosure closure, TInstant deadline, IInvokerPtr invoker)
+ {
+ YT_VERIFY(closure);
+ return Submit(
+ BIND_NO_PROPAGATE(&ClosureToDelayedCallbackAdapter, std::move(closure)),
+ deadline,
+ std::move(invoker));
+ }
+
+ TDelayedExecutorCookie Submit(TDelayedCallback callback, TDuration delay, IInvokerPtr invoker)
+ {
+ YT_VERIFY(callback);
+ return Submit(
+ std::move(callback),
+ delay.ToDeadLine(),
+ std::move(invoker));
+ }
+
+ TDelayedExecutorCookie Submit(TDelayedCallback callback, TInstant deadline, IInvokerPtr invoker)
+ {
+ YT_VERIFY(callback);
+ auto entry = New<TDelayedExecutorEntry>(std::move(callback), deadline, std::move(invoker));
+ PollerThread_->EnqueueSubmission(entry);
+
+ if (!PollerThread_->Start()) {
+ if (auto callback = TakeCallback(entry)) {
+ callback(/*aborted*/ true);
+ }
+#if defined(_asan_enabled_)
+ NSan::MarkAsIntentionallyLeaked(entry.Get());
+#endif
+ }
+
+ return entry;
+ }
+
+ void Cancel(TDelayedExecutorEntryPtr entry)
+ {
+ if (!entry) {
+ return;
+ }
+ PollerThread_->EnqueueCancelation(std::move(entry));
+ // No #EnsureStarted call is needed here: having #entry implies that #Submit call has been previously made.
+ // Also in contrast to #Submit we have no special handling for #entry in case the Poller Thread
+ // has been already terminated.
+ }
+
+private:
+ class TPollerThread
+ : public NThreading::TThread
+ {
+ public:
+ TPollerThread()
+ : TThread(
+ "DelayedPoller",
+ NThreading::EThreadPriority::Normal,
+ /*shutdownPriority*/ 200)
+ { }
+
+ void EnqueueSubmission(TDelayedExecutorEntryPtr entry)
+ {
+ SubmitQueue_.Enqueue(std::move(entry));
+
+ if (NotificationScheduled_.load(std::memory_order::relaxed)) {
+ return;
+ }
+ if (!NotificationScheduled_.exchange(true)) {
+ EventCount_->NotifyOne();
+ }
+ }
+
+ void EnqueueCancelation(TDelayedExecutorEntryPtr entry)
+ {
+ CancelQueue_.Enqueue(std::move(entry));
+ }
+
+ private:
+ const TIntrusivePtr<NThreading::TEventCount> EventCount_ = New<NThreading::TEventCount>();
+
+ std::atomic<bool> NotificationScheduled_ = false;
+
+ //! Only touched from DelayedPoller thread.
+ std::set<TDelayedExecutorEntryPtr, TDelayedExecutorEntry::TComparer> ScheduledEntries_;
+
+ //! Enqueued from any thread, dequeued from DelayedPoller thread.
+ TRelaxedMpscQueue<TDelayedExecutorEntryPtr> SubmitQueue_;
+ TRelaxedMpscQueue<TDelayedExecutorEntryPtr> CancelQueue_;
+ TActionQueuePtr DelayedQueue_;
+ IInvokerPtr DelayedInvoker_;
+
+ NProfiling::TGauge ScheduledCallbacksGauge_ = ConcurrencyProfiler.Gauge("/delayed_executor/scheduled_callbacks");
+ NProfiling::TCounter SubmittedCallbacksCounter_ = ConcurrencyProfiler.Counter("/delayed_executor/submitted_callbacks");
+ NProfiling::TCounter CanceledCallbacksCounter_ = ConcurrencyProfiler.Counter("/delayed_executor/canceled_callbacks");
+ NProfiling::TCounter StaleCallbacksCounter_ = ConcurrencyProfiler.Counter("/delayed_executor/stale_callbacks");
+
+
+ void StartPrologue() override
+ {
+ // Boot the Delayed Executor thread up.
+ // Do it here to avoid weird crashes when execl is being used in another thread.
+ DelayedQueue_ = New<TActionQueue>("DelayedExecutor");
+ DelayedInvoker_ = DelayedQueue_->GetInvoker();
+ }
+
+ void StopPrologue() override
+ {
+ EventCount_->NotifyOne();
+ }
+
+ void ThreadMain() override
+ {
+ while (true) {
+ auto cookie = EventCount_->PrepareWait();
+ // Reset notificagtion flag before processing queues but after prepare wait.
+ // Otherwise notifies occurred after processing queues and before wait
+ // can be lost. No new notifies can happen if notify flag is true before
+ // prepare wait.
+ NotificationScheduled_.store(false);
+
+ ProcessQueues();
+
+ if (IsStopping()) {
+ break;
+ }
+
+ auto now = GetInstant();
+
+ auto deadline = !ScheduledEntries_.empty()
+ ? std::max((*ScheduledEntries_.begin())->Deadline, now + CoalescingInterval)
+ : TInstant::Max();
+ EventCount_->Wait(cookie, deadline);
+ }
+
+ // Perform graceful shutdown.
+
+ // First run the scheduled callbacks with |aborted = true|.
+ // NB: The callbacks are forwarded to the DelayedExecutor thread to prevent any user-code
+ // from leaking to the Delayed Poller thread, which is, e.g., fiber-unfriendly.
+ auto runAbort = [&] (const TDelayedExecutorEntryPtr& entry) {
+ RunCallback(entry, /*aborted*/ true);
+ };
+ for (const auto& entry : ScheduledEntries_) {
+ runAbort(entry);
+ }
+ ScheduledEntries_.clear();
+
+ // Now we handle the queued callbacks similarly.
+ {
+ TDelayedExecutorEntryPtr entry;
+ while (SubmitQueue_.TryDequeue(&entry)) {
+ runAbort(entry);
+ }
+ }
+
+ // As for the cancelation queue, we just drop these entries.
+ {
+ TDelayedExecutorEntryPtr entry;
+ while (CancelQueue_.TryDequeue(&entry)) {
+ }
+ }
+
+ // Gracefully (sic!) shut the Delayed Executor thread down
+ // to ensure invocation of the callbacks scheduled above.
+ DelayedQueue_->Shutdown(/*graceful*/ true);
+ }
+
+ void ProcessQueues()
+ {
+ auto now = TInstant::Now();
+
+ {
+ int submittedCallbacks = 0;
+ TDelayedExecutorEntryPtr entry;
+ while (SubmitQueue_.TryDequeue(&entry)) {
+ if (entry->Canceled) {
+ continue;
+ }
+ if (entry->Deadline + LateWarningThreshold < now) {
+ StaleCallbacksCounter_.Increment();
+ YT_LOG_DEBUG("Found a late delayed submitted callback (Deadline: %v, Now: %v)",
+ entry->Deadline,
+ now);
+ }
+ auto [it, inserted] = ScheduledEntries_.insert(entry);
+ YT_VERIFY(inserted);
+ entry->Iterator = it;
+ ++submittedCallbacks;
+ }
+ SubmittedCallbacksCounter_.Increment(submittedCallbacks);
+ }
+
+ {
+ int canceledCallbacks = 0;
+ TDelayedExecutorEntryPtr entry;
+ while (CancelQueue_.TryDequeue(&entry)) {
+ if (entry->Canceled) {
+ continue;
+ }
+ entry->Canceled = true;
+ TakeCallback(entry);
+ if (entry->Iterator) {
+ ScheduledEntries_.erase(*entry->Iterator);
+ entry->Iterator.reset();
+ }
+ ++canceledCallbacks;
+ }
+ CanceledCallbacksCounter_.Increment(canceledCallbacks);
+ }
+
+ ScheduledCallbacksGauge_.Update(ScheduledEntries_.size());
+ while (!ScheduledEntries_.empty()) {
+ auto it = ScheduledEntries_.begin();
+ const auto& entry = *it;
+ if (entry->Deadline > now + CoalescingInterval) {
+ break;
+ }
+ if (entry->Deadline + LateWarningThreshold < now) {
+ StaleCallbacksCounter_.Increment();
+ YT_LOG_DEBUG("Found a late delayed scheduled callback (Deadline: %v, Now: %v)",
+ entry->Deadline,
+ now);
+ }
+ RunCallback(entry, false);
+ entry->Iterator.reset();
+ ScheduledEntries_.erase(it);
+ }
+ }
+
+ void RunCallback(const TDelayedExecutorEntryPtr& entry, bool abort)
+ {
+ if (auto callback = TakeCallback(entry)) {
+ (entry->Invoker ? entry->Invoker : DelayedInvoker_)->Invoke(BIND_NO_PROPAGATE(std::move(callback), abort));
+ }
+ }
+ };
+
+ using TPollerThreadPtr = TIntrusivePtr<TPollerThread>;
+ const TPollerThreadPtr PollerThread_ = New<TPollerThread>();
+
+
+ static void ClosureToDelayedCallbackAdapter(const TClosure& closure, bool aborted)
+ {
+ if (aborted) {
+ return;
+ }
+ closure();
+ }
+
+ static TDelayedExecutor::TDelayedCallback TakeCallback(const TDelayedExecutorEntryPtr& entry)
+ {
+ if (entry->CallbackTaken.exchange(true)) {
+ return {};
+ } else {
+ return std::move(entry->Callback);
+ }
+ }
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<void> TDelayedExecutor::MakeDelayed(
+ TDuration delay,
+ IInvokerPtr invoker)
+{
+ return NDetail::TDelayedExecutorImpl::Get()->MakeDelayed(delay, std::move(invoker));
+}
+
+void TDelayedExecutor::WaitForDuration(TDuration duration)
+{
+ NDetail::TDelayedExecutorImpl::Get()->WaitForDuration(duration);
+}
+
+TDelayedExecutorCookie TDelayedExecutor::Submit(
+ TDelayedCallback callback,
+ TDuration delay,
+ IInvokerPtr invoker)
+{
+ return NDetail::TDelayedExecutorImpl::Get()->Submit(std::move(callback), delay, std::move(invoker));
+}
+
+TDelayedExecutorCookie TDelayedExecutor::Submit(
+ TClosure closure,
+ TDuration delay,
+ IInvokerPtr invoker)
+{
+ return NDetail::TDelayedExecutorImpl::Get()->Submit(std::move(closure), delay, std::move(invoker));
+}
+
+TDelayedExecutorCookie TDelayedExecutor::Submit(
+ TDelayedCallback callback,
+ TInstant deadline,
+ IInvokerPtr invoker)
+{
+ return NDetail::TDelayedExecutorImpl::Get()->Submit(std::move(callback), deadline, std::move(invoker));
+}
+
+TDelayedExecutorCookie TDelayedExecutor::Submit(
+ TClosure closure,
+ TInstant deadline,
+ IInvokerPtr invoker)
+{
+ return NDetail::TDelayedExecutorImpl::Get()->Submit(std::move(closure), deadline, std::move(invoker));
+}
+
+void TDelayedExecutor::Cancel(const TDelayedExecutorCookie& cookie)
+{
+ NDetail::TDelayedExecutorImpl::Get()->Cancel(cookie);
+}
+
+void TDelayedExecutor::CancelAndClear(TDelayedExecutorCookie& cookie)
+{
+ NDetail::TDelayedExecutorImpl::Get()->Cancel(std::move(cookie));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/delayed_executor.h b/yt/yt/core/concurrency/delayed_executor.h
new file mode 100644
index 0000000000..a2762d797f
--- /dev/null
+++ b/yt/yt/core/concurrency/delayed_executor.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Manages delayed callback execution.
+class TDelayedExecutor
+{
+public:
+ typedef TCallback<void(bool)> TDelayedCallback;
+
+ //! Constructs a future that gets set when a given #delay elapses.
+ /*!
+ * \param delay Delay after which the returned future is set.
+ * \param invoker Invoker where the future becomes set (DelayedExecutor if null).
+ * Note that during shutdown this future will be resolved prematurely with an error.
+ */
+ static TFuture<void> MakeDelayed(
+ TDuration delay,
+ IInvokerPtr invoker = nullptr);
+
+ //! Constructs a future that gets set when a given #duration elapses and
+ //! immediately waits for it.
+ /*!
+ * This is barely equivalent to MakeDelayed and WaitFor combination.
+ * The result of waiting is ignored.
+ */
+ static void WaitForDuration(TDuration duration);
+
+ //! Submits #callback for execution after a given #delay.
+ /*!
+ * #callback is guaranteed to be invoked exactly once unless the cookie was cancelled (cf. #Cancel).
+ * The exact thread where the invocation takes place is unspecified.
+ *
+ * |aborted| flag is provided to the callback to indicate whether this is a premature execution
+ * due to shutdown or not.
+ *
+ * Note that after shutdown has been initiated, #Submit may start invoking the newly-passed callbacks
+ * immediately with |aborted = true|. It is guaranteed, though, that each #Submit call may only
+ * cause an immediate execution of *its* callback but not others.
+ *
+ * \param callback A callback to execute.
+ * \param delay Execution delay.
+ * \param invoker Invoker that will be handling #callback (DelayedExecutor if null).
+ * \return An opaque cancelation cookie.
+ */
+ static TDelayedExecutorCookie Submit(
+ TDelayedCallback callback,
+ TDuration delay,
+ IInvokerPtr invoker = nullptr);
+
+ //! Submits #closure for execution after a given #delay.
+ /*!
+ * \param closure A closure to execute.
+ * \param delay Execution delay.
+ * \param invoker Invoker that will be handling #callback (DelayedExecutor if null).
+ * \return An opaque cancelation cookie.
+ */
+ static TDelayedExecutorCookie Submit(
+ TClosure closure,
+ TDuration delay,
+ IInvokerPtr invoker = nullptr);
+
+ //! Submits #callback for execution at a given #deadline.
+ /*!
+ * \param callback A callback to execute.
+ * \param deadline Execution deadline.
+ * \param invoker Invoker that will be handling #callback (DelayedExecutor if null).
+ * \return An opaque cancelation cookie.
+ */
+ static TDelayedExecutorCookie Submit(
+ TDelayedCallback callback,
+ TInstant deadline,
+ IInvokerPtr invoker = nullptr);
+
+ //! Submits #closure for execution at a given #deadline.
+ /*!
+ * \param closure A closure to execute.
+ * \param delay Execution deadline.
+ * \param invoker Invoker that will be handling #callback (DelayedExecutor if null).
+ * \return An opaque cancelation cookie.
+ */
+ static TDelayedExecutorCookie Submit(
+ TClosure closure,
+ TInstant deadline,
+ IInvokerPtr invoker = nullptr);
+
+ //! Cancels an earlier scheduled execution.
+ /*!
+ * This call is "safe", i.e. cannot lead to immediate execution of any callbacks even
+ * during shutdown.
+ *
+ * Cancelation should always be regarded as a hint. It is inherently racy and
+ * is not guaranteed to be handled during and after shutdown.
+ */
+ static void Cancel(const TDelayedExecutorCookie& cookie);
+
+ //! The same as Cancel, but in addition also clears the cookie.
+ static void CancelAndClear(TDelayedExecutorCookie& cookie);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/execution_stack.cpp b/yt/yt/core/concurrency/execution_stack.cpp
new file mode 100644
index 0000000000..618eb3bbee
--- /dev/null
+++ b/yt/yt/core/concurrency/execution_stack.cpp
@@ -0,0 +1,247 @@
+#include "execution_stack.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/ref_tracked.h>
+
+#if defined(_unix_)
+# include <sys/mman.h>
+# include <limits.h>
+# include <unistd.h>
+# if !defined(__x86_64__) && !defined(__arm64__) && !defined(__aarch64__)
+# error Unsupported platform
+# endif
+#endif
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/object_pool.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/system/sanitizers.h>
+
+namespace NYT::NConcurrency {
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Stack sizes.
+#if defined(_asan_enabled_) || defined(_msan_enabled_)
+ static constexpr size_t SmallExecutionStackSize = 2_MB;
+ static constexpr size_t LargeExecutionStackSize = 64_MB;
+#else
+ static constexpr size_t SmallExecutionStackSize = 256_KB;
+ static constexpr size_t LargeExecutionStackSize = 8_MB;
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+TExecutionStackBase::TExecutionStackBase(size_t size)
+ : Stack_(nullptr)
+ , Size_(RoundUpToPage(size))
+{
+ auto cookie = GetRefCountedTypeCookie<TExecutionStack>();
+ TRefCountedTrackerFacade::AllocateSpace(cookie, Size_);
+}
+
+TExecutionStackBase::~TExecutionStackBase()
+{
+ auto cookie = GetRefCountedTypeCookie<TExecutionStack>();
+ TRefCountedTrackerFacade::FreeSpace(cookie, Size_);
+}
+
+void* TExecutionStackBase::GetStack() const
+{
+ return Stack_;
+}
+
+size_t TExecutionStackBase::GetSize() const
+{
+ return Size_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#if defined(_unix_)
+
+TExecutionStack::TExecutionStack(size_t size)
+ : TExecutionStackBase(size)
+{
+ const size_t guardSize = GuardPageCount * GetPageSize();
+
+ int flags =
+#if defined(_darwin_)
+ MAP_ANON | MAP_PRIVATE;
+#else
+ MAP_ANONYMOUS | MAP_PRIVATE;
+#endif
+
+ Base_ = reinterpret_cast<char*>(::mmap(
+ 0,
+ guardSize * 2 + Size_,
+ PROT_READ | PROT_WRITE,
+ flags,
+ -1,
+ 0));
+
+ auto checkOom = [] {
+ if (LastSystemError() == ENOMEM) {
+ fprintf(stderr, "Out-of-memory condition detected while allocating execution stack; terminating\n");
+ _exit(9);
+ }
+ };
+
+ if (Base_ == MAP_FAILED) {
+ checkOom();
+ YT_LOG_FATAL(TError::FromSystem(), "Failed to allocate execution stack (Size: %v)", Size_);
+ }
+
+ if (::mprotect(Base_, guardSize, PROT_NONE) == -1) {
+ checkOom();
+ YT_LOG_FATAL(TError::FromSystem(), "Failed to protect execution stack from below (GuardSize: %v)", guardSize);
+ }
+
+ if (::mprotect(Base_ + guardSize + Size_, guardSize, PROT_NONE) == -1) {
+ checkOom();
+ YT_LOG_FATAL(TError::FromSystem(), "Failed to protect execution stack from above (GuardSize: %v)", guardSize);
+ }
+
+ Stack_ = Base_ + guardSize;
+ YT_VERIFY((reinterpret_cast<uintptr_t>(Stack_)& 15) == 0);
+}
+
+TExecutionStack::~TExecutionStack()
+{
+ const size_t guardSize = GuardPageCount * GetPageSize();
+ ::munmap(Base_, guardSize * 2 + Size_);
+}
+
+#elif defined(_win_)
+
+TExecutionStack::TExecutionStack(size_t size)
+ : TExecutionStackBase(size)
+ , Handle_(::CreateFiber(Size_, &FiberTrampoline, this))
+ , Trampoline_(nullptr)
+{ }
+
+TExecutionStack::~TExecutionStack()
+{
+ ::DeleteFiber(Handle_);
+}
+
+static thread_local void* FiberTrampolineOpaque;
+
+void TExecutionStack::SetOpaque(void* opaque)
+{
+ FiberTrampolineOpaque = opaque;
+}
+
+void* TExecutionStack::GetOpaque()
+{
+ return FiberTrampolineOpaque;
+}
+
+void TExecutionStack::SetTrampoline(void (*trampoline)(void*))
+{
+ YT_ASSERT(!Trampoline_);
+ Trampoline_ = trampoline;
+}
+
+VOID CALLBACK TExecutionStack::FiberTrampoline(PVOID opaque)
+{
+ auto* stack = reinterpret_cast<TExecutionStack*>(opaque);
+ stack->Trampoline_(FiberTrampolineOpaque);
+}
+
+#else
+# error Unsupported platform
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <EExecutionStackKind Kind, size_t Size>
+class TPooledExecutionStack
+ : public TExecutionStack
+ , public TRefTracked<TPooledExecutionStack<Kind, Size>>
+{
+public:
+ TPooledExecutionStack()
+ : TExecutionStack(Size)
+ { }
+};
+
+std::shared_ptr<TExecutionStack> CreateExecutionStack(EExecutionStackKind kind)
+{
+ switch (kind) {
+ case EExecutionStackKind::Small:
+ return ObjectPool<TPooledExecutionStack<EExecutionStackKind::Small, SmallExecutionStackSize>>().Allocate();
+ case EExecutionStackKind::Large:
+ return ObjectPool<TPooledExecutionStack<EExecutionStackKind::Large, LargeExecutionStackSize>>().Allocate();
+ default:
+ YT_ABORT();
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+static std::atomic<int> SmallFiberStackPoolSize = {1024};
+static std::atomic<int> LargeFiberStackPoolSize = {1024};
+
+int GetFiberStackPoolSize(EExecutionStackKind stackKind)
+{
+ switch (stackKind) {
+ case EExecutionStackKind::Small: return SmallFiberStackPoolSize.load(std::memory_order::relaxed);
+ case EExecutionStackKind::Large: return LargeFiberStackPoolSize.load(std::memory_order::relaxed);
+ default: YT_ABORT();
+ }
+}
+
+void SetFiberStackPoolSize(EExecutionStackKind stackKind, int poolSize)
+{
+ if (poolSize < 0) {
+ YT_LOG_FATAL("Invalid fiber stack pool size (Size: %v, Kind: %v)",
+ poolSize,
+ stackKind);
+ }
+ switch (stackKind) {
+ case EExecutionStackKind::Small: SmallFiberStackPoolSize = poolSize; break;
+ case EExecutionStackKind::Large: LargeFiberStackPoolSize = poolSize; break;
+ default: YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NConcurrency::EExecutionStackKind Kind, size_t Size>
+struct TPooledObjectTraits<NConcurrency::TPooledExecutionStack<Kind, Size>, void>
+ : public TPooledObjectTraitsBase<NConcurrency::TPooledExecutionStack<Kind, Size>>
+{
+ using TStack = NConcurrency::TPooledExecutionStack<Kind, Size>;
+
+ static void Clean(TStack* stack)
+ {
+#if defined(_asan_enabled_)
+ if (stack->GetStack()) {
+ NSan::Poison(stack->GetStack(), stack->GetSize());
+ }
+#else
+ Y_UNUSED(stack);
+#endif
+ }
+
+ static int GetMaxPoolSize()
+ {
+ return NConcurrency::GetFiberStackPoolSize(Kind);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/concurrency/execution_stack.h b/yt/yt/core/concurrency/execution_stack.h
new file mode 100644
index 0000000000..d62f8cd026
--- /dev/null
+++ b/yt/yt/core/concurrency/execution_stack.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TExecutionStackBase
+{
+public:
+ TExecutionStackBase(const TExecutionStackBase& other) = delete;
+ TExecutionStackBase& operator=(const TExecutionStackBase& other) = delete;
+
+ virtual ~TExecutionStackBase();
+
+ void* GetStack() const;
+ size_t GetSize() const;
+
+protected:
+ void* Stack_;
+ size_t Size_;
+
+ explicit TExecutionStackBase(size_t size);
+};
+
+#if defined(_unix_)
+
+//! Mapped memory with a few extra guard pages.
+class TExecutionStack
+ : public TExecutionStackBase
+{
+public:
+ explicit TExecutionStack(size_t size);
+ ~TExecutionStack();
+
+private:
+ char* Base_;
+
+ static const int GuardPageCount = 256;
+};
+
+#elif defined(_win_)
+
+//! Stack plus Window fiber holder.
+class TExecutionStack
+ : public TExecutionStackBase
+{
+public:
+ explicit TExecutionStack(size_t size);
+ ~TExecutionStack();
+
+ static void SetOpaque(void* opaque);
+ static void* GetOpaque();
+
+ void SetTrampoline(void (*callee)(void*));
+
+private:
+ friend class TExecutionContext;
+
+ void* Handle_;
+ void (*Trampoline_)(void*);
+
+ static VOID CALLBACK FiberTrampoline(PVOID opaque);
+
+ friend TExecutionContext CreateExecutionContext(
+ TExecutionStack* stack,
+ void (*trampoline)(void*));
+
+};
+
+#else
+# error Unsupported platform
+#endif
+
+std::shared_ptr<TExecutionStack> CreateExecutionStack(EExecutionStackKind kind);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the current global limit for the number of pooled fiber stacks of a given size.
+int GetFiberStackPoolSize(EExecutionStackKind stackKind);
+
+//! Sets the global limit for the number of pooled fiber stacks of a given size.
+void SetFiberStackPoolSize(EExecutionStackKind stackKind, int poolSize);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NTY::NConcurrency
+
diff --git a/yt/yt/core/concurrency/fair_share_action_queue-inl.h b/yt/yt/core/concurrency/fair_share_action_queue-inl.h
new file mode 100644
index 0000000000..11cf50e4eb
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_action_queue-inl.h
@@ -0,0 +1,61 @@
+#ifndef FAIR_SHARE_ACTION_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include fair_share_action_queue.h"
+// For the sake of sane code completion.
+#include "fair_share_action_queue.h"
+#endif
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename EQueue>
+class TEnumIndexedFairShareActionQueue
+ : public IEnumIndexedFairShareActionQueue<EQueue>
+{
+public:
+ TEnumIndexedFairShareActionQueue(
+ const TString& threadName,
+ const std::vector<TString>& queueNames,
+ const THashMap<TString, std::vector<TString>>& queueToBucket)
+ : Queue_(CreateFairShareActionQueue(threadName, queueNames, queueToBucket))
+ { }
+
+ const IInvokerPtr& GetInvoker(EQueue queue) override
+ {
+ return Queue_->GetInvoker(static_cast<int>(queue));
+ }
+
+ void Reconfigure(const THashMap<TString, double>& newBucketWeights) override
+ {
+ Queue_->Reconfigure(newBucketWeights);
+ }
+
+private:
+ const IFairShareActionQueuePtr Queue_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename EQueue, typename EBucket>
+IEnumIndexedFairShareActionQueuePtr<EQueue> CreateEnumIndexedFairShareActionQueue(
+ const TString& threadName,
+ const THashMap<EBucket, std::vector<EQueue>>& queueToBucket)
+{
+ std::vector<TString> queueNames;
+ for (const auto& queueName : TEnumTraits<EQueue>::GetDomainNames()) {
+ queueNames.push_back(TString{queueName});
+ }
+ THashMap<TString, std::vector<TString>> stringBuckets;
+ for (const auto& [bucketName, bucket] : queueToBucket) {
+ auto& stringBucket = stringBuckets[ToString(bucketName)];
+ stringBucket.reserve(bucket.size());
+ for (const auto& queue : bucket) {
+ stringBucket.push_back(ToString(queue));
+ }
+ }
+ return New<TEnumIndexedFairShareActionQueue<EQueue>>(threadName, queueNames, stringBuckets);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_action_queue.cpp b/yt/yt/core/concurrency/fair_share_action_queue.cpp
new file mode 100644
index 0000000000..21357112cd
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_action_queue.cpp
@@ -0,0 +1,181 @@
+#include "fair_share_action_queue.h"
+
+#include "fair_share_queue_scheduler_thread.h"
+#include "profiling_helpers.h"
+#include "system_invokers.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+#include <yt/yt/core/actions/invoker_detail.h>
+
+#include <yt/yt/core/misc/collection_helpers.h>
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+using namespace NYPath;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareActionQueue
+ : public IFairShareActionQueue
+{
+public:
+ TFairShareActionQueue(
+ const TString& threadName,
+ const std::vector<TString>& queueNames,
+ const THashMap<TString, std::vector<TString>>& queueToBucket)
+ : ShutdownCookie_(RegisterShutdownCallback(
+ Format("FairShareActionQueue(%v)", threadName),
+ BIND_NO_PROPAGATE(&TFairShareActionQueue::Shutdown, MakeWeak(this), /*graceful*/ false),
+ /*priority*/ 100))
+ {
+ THashMap<TString, int> queueNameToIndex;
+ for (int queueIndex = 0; queueIndex < std::ssize(queueNames); ++queueIndex) {
+ YT_VERIFY(queueNameToIndex.emplace(queueNames[queueIndex], queueIndex).second);
+ }
+
+ QueueIndexToBucketIndex_.resize(queueNames.size(), -1);
+ QueueIndexToBucketQueueIndex_.resize(queueNames.size(), -1);
+ BucketNames_.reserve(queueNames.size());
+
+ std::vector<TBucketDescription> bucketDescriptions;
+ THashSet<TString> createdQueues;
+ int nextBucketIndex = 0;
+ for (const auto& [bucketName, bucketQueues] : queueToBucket) {
+ int bucketIndex = nextBucketIndex++;
+ auto& bucketDescription = bucketDescriptions.emplace_back();
+ bucketDescription.BucketTagSet = GetBucketTags(threadName, bucketName);
+ for (int bucketQueueIndex = 0; bucketQueueIndex < std::ssize(bucketQueues); ++bucketQueueIndex) {
+ const auto& queueName = bucketQueues[bucketQueueIndex];
+ auto queueIndex = GetOrCrash(queueNameToIndex, queueName);
+ YT_VERIFY(createdQueues.insert(queueName).second);
+ QueueIndexToBucketIndex_[queueIndex] = bucketIndex;
+ QueueIndexToBucketQueueIndex_[queueIndex] = bucketQueueIndex;
+ bucketDescription.QueueTagSets.push_back(GetQueueTags(threadName, queueName));
+ bucketDescription.QueueProfilerTags.push_back(New<NYTProf::TProfilerTag>("queue", queueName));
+ }
+ BucketNames_.push_back(bucketName);
+ }
+
+ // Create separate buckets for queues with no bucket specified.
+ for (const auto& queueName : queueNames) {
+ if (createdQueues.contains(queueName)) {
+ continue;
+ }
+
+ auto& bucketDescription = bucketDescriptions.emplace_back();
+ bucketDescription.BucketTagSet = GetBucketTags(threadName, queueName);
+ bucketDescription.QueueTagSets.push_back(GetQueueTags(threadName, queueName));
+ bucketDescription.QueueProfilerTags.push_back(New<NYTProf::TProfilerTag>("queue", queueName));
+ auto queueIndex = GetOrCrash(queueNameToIndex, queueName);
+ QueueIndexToBucketIndex_[queueIndex] = nextBucketIndex++;
+ QueueIndexToBucketQueueIndex_[queueIndex] = 0;
+ YT_VERIFY(createdQueues.emplace(queueName).second);
+ BucketNames_.push_back(queueName);
+ }
+
+ YT_VERIFY(createdQueues.size() == queueNames.size());
+ YT_VERIFY(BucketNames_.size() == bucketDescriptions.size());
+
+ for (int queueIndex = 0; queueIndex < std::ssize(queueNames); ++queueIndex) {
+ YT_VERIFY(QueueIndexToBucketIndex_[queueIndex] != -1);
+ YT_VERIFY(QueueIndexToBucketQueueIndex_[queueIndex] != -1);
+ }
+
+ Queue_ = New<TFairShareInvokerQueue>(CallbackEventCount_, std::move(bucketDescriptions));
+ Thread_ = New<TFairShareQueueSchedulerThread>(Queue_, CallbackEventCount_, threadName, threadName);
+ }
+
+ ~TFairShareActionQueue()
+ {
+ Shutdown(/*graceful*/ false);
+ }
+
+ void Shutdown(bool graceful)
+ {
+ if (Stopped_.exchange(true)) {
+ return;
+ }
+
+ Queue_->Shutdown();
+
+ ShutdownInvoker_->Invoke(BIND([graceful, thread = Thread_, queue = Queue_] {
+ thread->Stop(graceful);
+ queue->DrainConsumer();
+ }));
+ }
+
+ const IInvokerPtr& GetInvoker(int index) override
+ {
+ YT_ASSERT(0 <= index && index < std::ssize(QueueIndexToBucketIndex_));
+
+ EnsuredStarted();
+ return Queue_->GetInvoker(
+ QueueIndexToBucketIndex_[index],
+ QueueIndexToBucketQueueIndex_[index]);
+ }
+
+ void Reconfigure(const THashMap<TString, double>& newBucketWeights) override
+ {
+ std::vector<double> weights(BucketNames_.size());
+ for (int bucketIndex = 0; bucketIndex < std::ssize(BucketNames_); ++bucketIndex) {
+ const auto& bucketName = BucketNames_[bucketIndex];
+ if (auto it = newBucketWeights.find(bucketName); it != newBucketWeights.end()) {
+ weights[bucketIndex] = it->second;
+ } else {
+ weights[bucketIndex] = 1.0;
+ }
+ }
+
+ Queue_->Reconfigure(std::move(weights));
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+
+ const TShutdownCookie ShutdownCookie_;
+ const IInvokerPtr ShutdownInvoker_ = GetShutdownInvoker();
+
+ TFairShareInvokerQueuePtr Queue_;
+ TFairShareQueueSchedulerThreadPtr Thread_;
+
+ std::vector<int> QueueIndexToBucketIndex_;
+ std::vector<int> QueueIndexToBucketQueueIndex_;
+
+ std::vector<TString> BucketNames_;
+
+ std::atomic<bool> Started_ = false;
+ std::atomic<bool> Stopped_ = false;
+
+
+ void EnsuredStarted()
+ {
+ if (Started_.load(std::memory_order::relaxed)) {
+ return;
+ }
+ if (Started_.exchange(true)) {
+ return;
+ }
+ Thread_->Start();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFairShareActionQueuePtr CreateFairShareActionQueue(
+ const TString& threadName,
+ const std::vector<TString>& queueNames,
+ const THashMap<TString, std::vector<TString>>& queueToBucket)
+{
+ return New<TFairShareActionQueue>(threadName, queueNames, queueToBucket);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_action_queue.h b/yt/yt/core/concurrency/fair_share_action_queue.h
new file mode 100644
index 0000000000..f0d69caa50
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_action_queue.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/range.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IFairShareActionQueue
+ : public TRefCounted
+{
+ virtual const IInvokerPtr& GetInvoker(int index) = 0;
+
+ virtual void Reconfigure(const THashMap<TString, double>& newBucketWeights) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFairShareActionQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFairShareActionQueuePtr CreateFairShareActionQueue(
+ const TString& threadName,
+ const std::vector<TString>& queueNames,
+ const THashMap<TString, std::vector<TString>>& queueToBucket = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename EQueue>
+struct IEnumIndexedFairShareActionQueue
+ : public TRefCounted
+{
+ virtual const IInvokerPtr& GetInvoker(EQueue queue) = 0;
+
+ virtual void Reconfigure(const THashMap<TString, double>& newBucketWeights) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename EQueue, typename EBucket = EQueue>
+IEnumIndexedFairShareActionQueuePtr<EQueue> CreateEnumIndexedFairShareActionQueue(
+ const TString& threadName,
+ const THashMap<EBucket, std::vector<EQueue>>& queueToBucket = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define FAIR_SHARE_ACTION_QUEUE_INL_H_
+#include "fair_share_action_queue-inl.h"
+#undef FAIR_SHARE_ACTION_QUEUE_INL_H_
diff --git a/yt/yt/core/concurrency/fair_share_invoker_pool.cpp b/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
new file mode 100644
index 0000000000..1c632ea9ef
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_invoker_pool.cpp
@@ -0,0 +1,318 @@
+#include "fair_share_invoker_pool.h"
+
+#include "scheduler.h"
+
+#include <yt/yt/core/actions/current_invoker.h>
+#include <yt/yt/core/actions/invoker_detail.h>
+
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/memory/weak_ptr.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <optional>
+#include <utility>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareCallbackQueue
+ : public IFairShareCallbackQueue
+{
+public:
+ explicit TFairShareCallbackQueue(int bucketCount)
+ : Buckets_(bucketCount)
+ , ExcessTimes_(bucketCount, 0)
+ { }
+
+ void Enqueue(TClosure callback, int bucketIndex) override
+ {
+ auto guard = Guard(Lock_);
+
+ YT_VERIFY(IsValidBucketIndex(bucketIndex));
+ Buckets_[bucketIndex].push(std::move(callback));
+ }
+
+ bool TryDequeue(TClosure* resultCallback, int* resultBucketIndex) override
+ {
+ YT_VERIFY(resultCallback != nullptr);
+ YT_VERIFY(resultBucketIndex != nullptr);
+
+ auto guard = Guard(Lock_);
+
+ auto optionalBucketIndex = GetStarvingBucketIndex();
+ if (!optionalBucketIndex) {
+ return false;
+ }
+ auto bucketIndex = *optionalBucketIndex;
+
+ TruncateExcessTimes(ExcessTimes_[bucketIndex]);
+
+ *resultCallback = std::move(Buckets_[bucketIndex].front());
+ Buckets_[bucketIndex].pop();
+
+ *resultBucketIndex = bucketIndex;
+
+ return true;
+ }
+
+ void AccountCpuTime(int bucketIndex, TCpuDuration cpuTime) override
+ {
+ auto guard = Guard(Lock_);
+
+ ExcessTimes_[bucketIndex] += cpuTime;
+ }
+
+private:
+ using TBuckets = std::vector<TRingQueue<TClosure>>;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ TBuckets Buckets_;
+ std::vector<TCpuDuration> ExcessTimes_;
+
+ std::optional<int> GetStarvingBucketIndex() const
+ {
+ auto minExcessTime = std::numeric_limits<TCpuDuration>::max();
+ std::optional<int> minBucketIndex;
+ for (int index = 0; index < std::ssize(Buckets_); ++index) {
+ if (Buckets_[index].empty()) {
+ continue;
+ }
+ if (!minBucketIndex || ExcessTimes_[index] < minExcessTime) {
+ minExcessTime = ExcessTimes_[index];
+ minBucketIndex = index;
+ }
+ }
+ return minBucketIndex;
+ }
+
+ void TruncateExcessTimes(TCpuDuration delta)
+ {
+ for (int index = 0; index < std::ssize(Buckets_); ++index) {
+ if (ExcessTimes_[index] >= delta) {
+ ExcessTimes_[index] -= delta;
+ } else {
+ ExcessTimes_[index] = 0;
+ }
+ }
+ }
+
+ bool IsValidBucketIndex(int index) const
+ {
+ return 0 <= index && index < std::ssize(Buckets_);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFairShareCallbackQueuePtr CreateFairShareCallbackQueue(int bucketCount)
+{
+ YT_VERIFY(0 < bucketCount && bucketCount < 100);
+ return New<TFairShareCallbackQueue>(bucketCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareInvokerPool
+ : public IDiagnosableInvokerPool
+{
+public:
+ TFairShareInvokerPool(
+ IInvokerPtr underlyingInvoker,
+ int invokerCount,
+ TFairShareCallbackQueueFactory callbackQueueFactory)
+ : UnderlyingInvoker_(std::move(underlyingInvoker))
+ , Queue_(callbackQueueFactory(invokerCount))
+ {
+ Invokers_.reserve(invokerCount);
+ for (int index = 0; index < invokerCount; ++index) {
+ Invokers_.push_back(New<TInvoker>(UnderlyingInvoker_, index, MakeWeak(this)));
+ }
+ InvokerQueueStates_.resize(invokerCount);
+ }
+
+ int GetSize() const override
+ {
+ return Invokers_.size();
+ }
+
+ void Enqueue(TClosure callback, int index)
+ {
+ auto now = GetInstant();
+ {
+ auto guard = WriterGuard(InvokerQueueStatesLock_);
+
+ auto& queueState = InvokerQueueStates_[index];
+
+ queueState.ActionEnqueueTimes.push(now);
+ queueState.SumOfActionEnqueueTimes += now.GetValue();
+ ++queueState.EnqueuedActionCount;
+ }
+
+ Queue_->Enqueue(std::move(callback), index);
+ UnderlyingInvoker_->Invoke(BIND_NO_PROPAGATE(
+ &TFairShareInvokerPool::Run,
+ MakeStrong(this)));
+ }
+
+protected:
+ const IInvokerPtr& DoGetInvoker(int index) const override
+ {
+ YT_VERIFY(IsValidInvokerIndex(index));
+ return Invokers_[index];
+ }
+
+ TInvokerStatistics DoGetInvokerStatistics(int index) const override
+ {
+ YT_VERIFY(IsValidInvokerIndex(index));
+
+ auto now = GetInstant();
+
+ auto guard = ReaderGuard(InvokerQueueStatesLock_);
+
+ const auto& queueState = InvokerQueueStates_[index];
+
+ auto waitingActionCount = queueState.ActionEnqueueTimes.size();
+ auto averageWaitTime = waitingActionCount > 0
+ ? ValueToDuration(now.GetValue() - queueState.SumOfActionEnqueueTimes / waitingActionCount)
+ : TDuration::Zero();
+
+ return {
+ queueState.EnqueuedActionCount,
+ queueState.DequeuedActionCount,
+ waitingActionCount,
+ averageWaitTime,
+ };
+ }
+
+private:
+ const IInvokerPtr UnderlyingInvoker_;
+
+ std::vector<IInvokerPtr> Invokers_;
+
+ struct TInvokerQueueState
+ {
+ TRingQueue<TInstant> ActionEnqueueTimes;
+ TInstant::TValue SumOfActionEnqueueTimes = {};
+ ui64 EnqueuedActionCount = 0;
+ ui64 DequeuedActionCount = 0;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, InvokerQueueStatesLock_);
+ std::vector<TInvokerQueueState> InvokerQueueStates_;
+
+ IFairShareCallbackQueuePtr Queue_;
+
+ class TCpuTimeAccounter
+ {
+ public:
+ TCpuTimeAccounter(int index, IFairShareCallbackQueue* queue)
+ : Index_(index)
+ , Queue_(queue)
+ , ContextSwitchGuard_(
+ /* out */ [this] { Account(); },
+ /* in */ [] { })
+ { }
+
+ void Account()
+ {
+ if (Accounted_) {
+ return;
+ }
+ Accounted_ = true;
+ Queue_->AccountCpuTime(Index_, Timer_.GetElapsedCpuTime());
+ Timer_.Stop();
+ }
+
+ ~TCpuTimeAccounter()
+ {
+ Account();
+ }
+
+ private:
+ const int Index_;
+ bool Accounted_ = false;
+ IFairShareCallbackQueue* Queue_;
+ TWallTimer Timer_;
+ TContextSwitchGuard ContextSwitchGuard_;
+ };
+
+ class TInvoker
+ : public TInvokerWrapper
+ {
+ public:
+ TInvoker(IInvokerPtr underlyingInvoker_, int index, TWeakPtr<TFairShareInvokerPool> parent)
+ : TInvokerWrapper(std::move(underlyingInvoker_))
+ , Index_(index)
+ , Parent_(std::move(parent))
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ if (auto strongParent = Parent_.Lock()) {
+ strongParent->Enqueue(std::move(callback), Index_);
+ }
+ }
+
+ private:
+ const int Index_;
+ const TWeakPtr<TFairShareInvokerPool> Parent_;
+ };
+
+ bool IsValidInvokerIndex(int index) const
+ {
+ return 0 <= index && index < std::ssize(Invokers_);
+ }
+
+ void Run()
+ {
+ TClosure callback;
+ int bucketIndex = -1;
+ YT_VERIFY(Queue_->TryDequeue(&callback, &bucketIndex));
+ YT_VERIFY(IsValidInvokerIndex(bucketIndex));
+
+ {
+ auto guard = WriterGuard(InvokerQueueStatesLock_);
+
+ auto& queueState = InvokerQueueStates_[bucketIndex];
+ YT_VERIFY(!queueState.ActionEnqueueTimes.empty());
+
+ auto currentActionEnqueueTime = queueState.ActionEnqueueTimes.front();
+ queueState.ActionEnqueueTimes.pop();
+ queueState.SumOfActionEnqueueTimes -= currentActionEnqueueTime.GetValue();
+ ++queueState.DequeuedActionCount;
+ }
+
+ {
+ TCurrentInvokerGuard currentInvokerGuard(Invokers_[bucketIndex].Get());
+ TCpuTimeAccounter cpuTimeAccounter(bucketIndex, Queue_.Get());
+ callback();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDiagnosableInvokerPoolPtr CreateFairShareInvokerPool(
+ IInvokerPtr underlyingInvoker,
+ int invokerCount,
+ TFairShareCallbackQueueFactory callbackQueueFactory)
+{
+ YT_VERIFY(0 < invokerCount && invokerCount < 100);
+ return New<TFairShareInvokerPool>(
+ std::move(underlyingInvoker),
+ invokerCount,
+ std::move(callbackQueueFactory));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_invoker_pool.h b/yt/yt/core/concurrency/fair_share_invoker_pool.h
new file mode 100644
index 0000000000..ec2d1fb916
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_invoker_pool.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "public.h"
+#include "private.h"
+
+#include <yt/yt/core/actions/invoker_pool.h>
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <functional>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents several buckets of callbacks with one CPU time counter per each bucket.
+struct IFairShareCallbackQueue
+ : public virtual TRefCounted
+{
+ //! Enqueues #callback to bucket with index #bucketIndex.
+ virtual void Enqueue(TClosure callback, int bucketIndex) = 0;
+
+ //! Dequeues callback from the most starving bucket (due to CPU time counters) and
+ //! stores it at #resultCallback, also stores index of the bucket at #resultBucketIndex.
+ //! Returns |true| if and only if there is at least one callback in the queue.
+ virtual bool TryDequeue(TClosure* resultCallback, int* resultBucketIndex) = 0;
+
+ //! Add #cpuTime to the CPU time counter for bucket with index #bucketIndex.
+ virtual void AccountCpuTime(int bucketIndex, NProfiling::TCpuDuration cpuTime) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFairShareCallbackQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates multithreaded implementation of the IFairShareCallbackQueue with #bucketCount buckets.
+IFairShareCallbackQueuePtr CreateFairShareCallbackQueue(int bucketCount);
+
+using TFairShareCallbackQueueFactory = std::function<IFairShareCallbackQueuePtr(int)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an invoker pool that holds number of invokers and shares CPU time between them fairly.
+//! Pay attention: CPU time accounting between consecutive context switches is not supported yet,
+//! so use with care in case of multiple workers in the underlying invoker.
+//! Factory #callbackQueueFactory is used by the invoker pool for creation of the storage for callbacks.
+//! Ability to specify #callbackQueueFactory is provided for testing purposes.
+IDiagnosableInvokerPoolPtr CreateFairShareInvokerPool(
+ IInvokerPtr underlyingInvoker,
+ int invokerCount,
+ TFairShareCallbackQueueFactory callbackQueueFactory = CreateFairShareCallbackQueue);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_invoker_queue.cpp b/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
new file mode 100644
index 0000000000..0f575d35f4
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_invoker_queue.cpp
@@ -0,0 +1,158 @@
+#include "fair_share_invoker_queue.h"
+
+#include "invoker_queue.h"
+
+#include <library/cpp/yt/threading/event_count.h>
+
+#include <cmath>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+using namespace NYPath;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFairShareInvokerQueue::TFairShareInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const std::vector<TBucketDescription>& bucketDescriptions)
+ : Weights_(bucketDescriptions.size(), 1.0)
+{
+ Buckets_.reserve(bucketDescriptions.size());
+ for (const auto& bucketDescription : bucketDescriptions) {
+ auto& bucket = Buckets_.emplace_back();
+ bucket.Queue = New<TMpscInvokerQueue>(
+ callbackEventCount,
+ bucketDescription.QueueTagSets,
+ bucketDescription.QueueProfilerTags,
+ bucketDescription.BucketTagSet);
+ int profilingTagCount = bucketDescription.QueueTagSets.size();
+ bucket.Invokers.reserve(profilingTagCount);
+ for (int index = 0; index < profilingTagCount; ++index) {
+ bucket.Invokers.push_back(bucket.Queue->GetProfilingTagSettingInvoker(index));
+ }
+ }
+}
+
+TFairShareInvokerQueue::~TFairShareInvokerQueue() = default;
+
+void TFairShareInvokerQueue::SetThreadId(NThreading::TThreadId threadId)
+{
+ for (auto& bucket : Buckets_) {
+ bucket.Queue->SetThreadId(threadId);
+ }
+}
+
+const IInvokerPtr& TFairShareInvokerQueue::GetInvoker(int bucketIndex, int queueIndex) const
+{
+ YT_ASSERT(0 <= bucketIndex && bucketIndex < static_cast<int>(Buckets_.size()));
+ const auto& bucket = Buckets_[bucketIndex];
+
+ YT_ASSERT(0 <= queueIndex && queueIndex < static_cast<int>(bucket.Invokers.size()));
+ return bucket.Invokers[queueIndex];
+}
+
+void TFairShareInvokerQueue::Shutdown()
+{
+ for (auto& bucket : Buckets_) {
+ bucket.Queue->Shutdown();
+ }
+}
+
+void TFairShareInvokerQueue::DrainProducer()
+{
+ for (auto& bucket : Buckets_) {
+ bucket.Queue->DrainProducer();
+ }
+}
+
+void TFairShareInvokerQueue::DrainConsumer()
+{
+ for (auto& bucket : Buckets_) {
+ bucket.Queue->DrainConsumer();
+ }
+}
+
+bool TFairShareInvokerQueue::IsRunning() const
+{
+ for (const auto& bucket : Buckets_) {
+ if (!bucket.Queue->IsRunning()) {
+ return false;
+ }
+ }
+ return true;
+}
+
+TClosure TFairShareInvokerQueue::BeginExecute(TEnqueuedAction* action)
+{
+ YT_VERIFY(!CurrentBucket_);
+
+ // Reconfigure queue if required.
+ if (NeedToReconfigure_) {
+ auto guard = Guard(WeightsLock_);
+ YT_ASSERT(Weights_.size() == Buckets_.size());
+ for (int bucketIndex = 0; bucketIndex < std::ssize(Weights_); ++bucketIndex) {
+ Buckets_[bucketIndex].InversedWeight = std::ceil(UnitWeight * Weights_[bucketIndex]);
+ }
+ NeedToReconfigure_ = false;
+ }
+
+ // Check if any callback is ready at all.
+ CurrentBucket_ = GetStarvingBucket();
+ if (!CurrentBucket_) {
+ return TClosure();
+ }
+
+ // Reduce excesses (with truncation).
+ auto delta = CurrentBucket_->ExcessTime;
+ for (auto& bucket : Buckets_) {
+ bucket.ExcessTime = std::max<NProfiling::TCpuDuration>(bucket.ExcessTime - delta, 0);
+ }
+
+ // Pump the starving queue.
+ return CurrentBucket_->Queue->BeginExecute(action);
+}
+
+void TFairShareInvokerQueue::EndExecute(TEnqueuedAction* action)
+{
+ if (!CurrentBucket_) {
+ return;
+ }
+
+ CurrentBucket_->Queue->EndExecute(action);
+ CurrentBucket_->ExcessTime += (action->FinishedAt - action->StartedAt);
+ CurrentBucket_ = nullptr;
+}
+
+void TFairShareInvokerQueue::Reconfigure(std::vector<double> weights)
+{
+ auto guard = Guard(WeightsLock_);
+ Weights_ = std::move(weights);
+
+ NeedToReconfigure_ = true;
+}
+
+TFairShareInvokerQueue::TBucket* TFairShareInvokerQueue::GetStarvingBucket()
+{
+ // Compute min excess over non-empty queues.
+ auto minExcessTime = std::numeric_limits<NProfiling::TCpuDuration>::max();
+ TBucket* minBucket = nullptr;
+ for (auto& bucket : Buckets_) {
+ const auto& queue = bucket.Queue;
+ YT_ASSERT(queue);
+ if (!queue->IsEmpty()) {
+ auto weightedExcessTime = bucket.ExcessTime * bucket.InversedWeight;
+ if (weightedExcessTime < minExcessTime) {
+ minExcessTime = weightedExcessTime;
+ minBucket = &bucket;
+ }
+ }
+ }
+ return minBucket;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/fair_share_invoker_queue.h b/yt/yt/core/concurrency/fair_share_invoker_queue.h
new file mode 100644
index 0000000000..3a199c8849
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_invoker_queue.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/library/ytprof/api/api.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBucketDescription
+{
+ std::vector<NProfiling::TTagSet> QueueTagSets;
+ std::vector<NYTProf::TProfilerTagPtr> QueueProfilerTags;
+ NProfiling::TTagSet BucketTagSet;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareInvokerQueue
+ : public TRefCounted
+{
+public:
+ TFairShareInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const std::vector<TBucketDescription>& bucketDescriptions);
+
+ ~TFairShareInvokerQueue();
+
+ void SetThreadId(NThreading::TThreadId threadId);
+
+ const IInvokerPtr& GetInvoker(int bucketIndex, int queueIndex) const;
+
+ void Shutdown();
+
+ void DrainProducer();
+ void DrainConsumer();
+
+ bool IsRunning() const;
+
+ TClosure BeginExecute(TEnqueuedAction* action);
+ void EndExecute(TEnqueuedAction* action);
+
+ void Reconfigure(std::vector<double> weights);
+
+private:
+ constexpr static i64 UnitWeight = 1000;
+
+ struct TBucket
+ {
+ TMpscInvokerQueuePtr Queue;
+ std::vector<IInvokerPtr> Invokers;
+ NProfiling::TCpuDuration ExcessTime = 0;
+
+ // ceil(UnitWeight / weight).
+ i64 InversedWeight = UnitWeight;
+ };
+
+ std::vector<TBucket> Buckets_;
+
+ std::atomic<bool> NeedToReconfigure_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, WeightsLock_);
+ std::vector<double> Weights_;
+
+ TBucket* CurrentBucket_ = nullptr;
+
+ TBucket* GetStarvingBucket();
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareInvokerQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp b/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
new file mode 100644
index 0000000000..0ba7d4100c
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.cpp
@@ -0,0 +1,39 @@
+#include "fair_share_queue_scheduler_thread.h"
+
+#include <yt/yt/library/profiling/tag.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFairShareQueueSchedulerThread::TFairShareQueueSchedulerThread(
+ TFairShareInvokerQueuePtr queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName)
+ , Queue_(std::move(queue))
+{ }
+
+TClosure TFairShareQueueSchedulerThread::BeginExecute()
+{
+ return Queue_->BeginExecute(&CurrentAction_);
+}
+
+void TFairShareQueueSchedulerThread::EndExecute()
+{
+ Queue_->EndExecute(&CurrentAction_);
+}
+
+void TFairShareQueueSchedulerThread::OnStart()
+{
+ Queue_->SetThreadId(GetThreadId());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.h b/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.h
new file mode 100644
index 0000000000..5716b472ad
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_queue_scheduler_thread.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "scheduler_thread.h"
+#include "fair_share_invoker_queue.h"
+#include "invoker_queue.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareQueueSchedulerThread
+ : public TSchedulerThread
+{
+public:
+ TFairShareQueueSchedulerThread(
+ TFairShareInvokerQueuePtr queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName);
+
+protected:
+ const TFairShareInvokerQueuePtr Queue_;
+
+ TEnqueuedAction CurrentAction_;
+
+ TClosure BeginExecute() override;
+ void EndExecute() override;
+
+ void OnStart() override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareQueueSchedulerThread)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_thread_pool.cpp b/yt/yt/core/concurrency/fair_share_thread_pool.cpp
new file mode 100644
index 0000000000..6ba9925197
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_thread_pool.cpp
@@ -0,0 +1,588 @@
+#include "fair_share_thread_pool.h"
+
+#include "private.h"
+#include "invoker_queue.h"
+#include "profiling_helpers.h"
+#include "scheduler_thread.h"
+#include "thread_pool_detail.h"
+
+#include <yt/yt/core/actions/current_invoker.h>
+
+#include <yt/yt/core/misc/heap.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <yt/yt/core/profiling/tscp.h>
+
+#include <library/cpp/yt/memory/weak_ptr.h>
+
+#include <util/generic/xrange.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+struct THeapItem;
+class TFairShareQueue;
+
+DECLARE_REFCOUNTED_CLASS(TBucket)
+
+class TBucket
+ : public IInvoker
+{
+public:
+ TBucket(TFairShareThreadPoolTag tag, TWeakPtr<TFairShareQueue> parent)
+ : Tag(std::move(tag))
+ , Parent(std::move(parent))
+ { }
+
+ void RunCallback(const TClosure& callback)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ callback();
+ }
+
+ void Invoke(TClosure callback) override;
+
+ void Invoke(TMutableRange<TClosure> callbacks) override;
+
+ void Drain()
+ {
+ Queue.clear();
+ }
+
+ NThreading::TThreadId GetThreadId() const override
+ {
+ return NThreading::InvalidThreadId;
+ }
+
+ bool CheckAffinity(const IInvokerPtr& invoker) const override
+ {
+ return invoker.Get() == this;
+ }
+
+ bool IsSerialized() const override
+ {
+ return false;
+ }
+
+ ~TBucket();
+
+ TFairShareThreadPoolTag Tag;
+ TWeakPtr<TFairShareQueue> Parent;
+ TRingQueue<TEnqueuedAction> Queue;
+ THeapItem* HeapIterator = nullptr;
+ i64 WaitTime = 0;
+
+ TCpuDuration ExcessTime = 0;
+ int CurrentExecutions = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucket)
+
+struct THeapItem
+{
+ TBucketPtr Bucket;
+
+ THeapItem(const THeapItem&) = delete;
+ THeapItem& operator=(const THeapItem&) = delete;
+
+ explicit THeapItem(TBucketPtr bucket)
+ : Bucket(std::move(bucket))
+ {
+ AdjustBackReference(this);
+ }
+
+ THeapItem(THeapItem&& other) noexcept
+ : Bucket(std::move(other.Bucket))
+ {
+ AdjustBackReference(this);
+ }
+
+ THeapItem& operator=(THeapItem&& other) noexcept
+ {
+ Bucket = std::move(other.Bucket);
+ AdjustBackReference(this);
+
+ return *this;
+ }
+
+ void AdjustBackReference(THeapItem* iterator)
+ {
+ if (Bucket) {
+ Bucket->HeapIterator = iterator;
+ }
+ }
+
+ ~THeapItem()
+ {
+ if (Bucket) {
+ Bucket->HeapIterator = nullptr;
+ }
+ }
+};
+
+bool operator < (const THeapItem& lhs, const THeapItem& rhs)
+{
+ return lhs.Bucket->ExcessTime < rhs.Bucket->ExcessTime;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto LogDurationThreshold = TDuration::Seconds(1);
+
+DECLARE_REFCOUNTED_TYPE(TFairShareQueue)
+
+class TFairShareQueue
+ : public TRefCounted
+{
+public:
+ TFairShareQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TTagSet& tags)
+ : CallbackEventCount_(std::move(callbackEventCount))
+ {
+ auto profiler = TProfiler{"/fair_share_queue"}.WithHot().WithTags(tags);
+ BucketCounter_ = profiler.Summary("/buckets");
+ SizeCounter_ = profiler.Summary("/size");
+ WaitTimeCounter_ = profiler.Timer("/time/wait");
+ ExecTimeCounter_ = profiler.Timer("/time/exec");
+ TotalTimeCounter_ = profiler.Timer("/time/total");
+ }
+
+ ~TFairShareQueue()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount)
+ {
+ ThreadCount_.store(threadCount);
+ }
+
+ IInvokerPtr GetInvoker(const TFairShareThreadPoolTag& tag)
+ {
+ auto guard = Guard(TagMappingSpinLock_);
+
+ auto inserted = TagToBucket_.emplace(tag, nullptr).first;
+ auto invoker = inserted->second.Lock();
+
+ if (!invoker) {
+ invoker = New<TBucket>(tag, MakeWeak(this));
+ inserted->second = invoker;
+ }
+
+ BucketCounter_.Record(TagToBucket_.size());
+ return invoker;
+ }
+
+ void Invoke(TClosure callback, TBucket* bucket)
+ {
+ auto guard = Guard(SpinLock_);
+
+ QueueSize_.fetch_add(1, std::memory_order::relaxed);
+
+ if (!bucket->HeapIterator) {
+ // Otherwise ExcessTime will be recalculated in AccountCurrentlyExecutingBuckets.
+ if (bucket->CurrentExecutions == 0 && !Heap_.empty()) {
+ bucket->ExcessTime = Heap_.front().Bucket->ExcessTime;
+ }
+
+ Heap_.emplace_back(bucket);
+ AdjustHeapBack(Heap_.begin(), Heap_.end());
+ YT_VERIFY(bucket->HeapIterator);
+ }
+
+ YT_ASSERT(callback);
+
+ TEnqueuedAction action;
+ action.Finished = false;
+ action.EnqueuedAt = GetCpuInstant();
+ action.Callback = BIND(&TBucket::RunCallback, MakeStrong(bucket), std::move(callback));
+ bucket->Queue.push(std::move(action));
+
+ guard.Release();
+
+ CallbackEventCount_->NotifyOne();
+ }
+
+ void RemoveBucket(TBucket* bucket)
+ {
+ auto guard = Guard(TagMappingSpinLock_);
+ auto it = TagToBucket_.find(bucket->Tag);
+
+ if (it != TagToBucket_.end() && it->second.IsExpired()) {
+ TagToBucket_.erase(it);
+ }
+
+ BucketCounter_.Record(TagToBucket_.size());
+ }
+
+ void Shutdown()
+ {
+ Drain();
+ }
+
+ void Drain()
+ {
+ auto guard = Guard(SpinLock_);
+ for (const auto& item : Heap_) {
+ item.Bucket->Drain();
+ }
+ }
+
+ TClosure BeginExecute(TEnqueuedAction* action, int index)
+ {
+ auto& threadState = ThreadStates_[index];
+
+ YT_ASSERT(!threadState.Bucket);
+
+ YT_ASSERT(action && action->Finished);
+
+ auto tscp = NProfiling::TTscp::Get();
+
+ TBucketPtr bucket;
+ {
+ auto guard = Guard(SpinLock_);
+ bucket = GetStarvingBucket(action, tscp);
+
+ if (!bucket) {
+ return TClosure();
+ }
+
+ ++bucket->CurrentExecutions;
+
+ threadState.Bucket = bucket;
+ threadState.AccountedAt = tscp.Instant;
+
+ action->StartedAt = tscp.Instant;
+ bucket->WaitTime = action->StartedAt - action->EnqueuedAt;
+ }
+
+ YT_ASSERT(action && !action->Finished);
+
+ WaitTimeCounter_.Record(CpuDurationToDuration(bucket->WaitTime));
+ return std::move(action->Callback);
+ }
+
+ void EndExecute(TEnqueuedAction* action, int index)
+ {
+ auto& threadState = ThreadStates_[index];
+
+ if (!threadState.Bucket) {
+ return;
+ }
+
+ YT_ASSERT(action);
+
+ if (action->Finished) {
+ return;
+ }
+
+ auto tscp = NProfiling::TTscp::Get();
+
+ action->FinishedAt = tscp.Instant;
+
+ int queueSize = QueueSize_.fetch_sub(1, std::memory_order::relaxed) - 1;
+ SizeCounter_.Record(queueSize);
+
+ auto timeFromStart = CpuDurationToDuration(action->FinishedAt - action->StartedAt);
+ auto timeFromEnqueue = CpuDurationToDuration(action->FinishedAt - action->EnqueuedAt);
+ ExecTimeCounter_.Record(timeFromStart);
+ TotalTimeCounter_.Record(timeFromEnqueue);
+
+ if (timeFromStart > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback execution took too long (Wait: %v, Execution: %v, Total: %v)",
+ CpuDurationToDuration(action->StartedAt - action->EnqueuedAt),
+ timeFromStart,
+ timeFromEnqueue);
+ }
+
+ auto waitTime = CpuDurationToDuration(action->StartedAt - action->EnqueuedAt);
+
+ if (waitTime > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback wait took too long (Wait: %v, Execution: %v, Total: %v)",
+ waitTime,
+ timeFromStart,
+ timeFromEnqueue);
+ }
+
+ action->Finished = true;
+
+ // Remove outside lock because of lock inside RemoveBucket.
+ TBucketPtr bucket;
+ {
+ auto guard = Guard(SpinLock_);
+ bucket = std::move(threadState.Bucket);
+
+ UpdateExcessTime(bucket.Get(), tscp.Instant - threadState.AccountedAt);
+
+ YT_VERIFY(bucket->CurrentExecutions-- > 0);
+ }
+ }
+
+private:
+ struct TThreadState
+ {
+ TCpuInstant AccountedAt = 0;
+ TBucketPtr Bucket;
+ };
+
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ std::vector<THeapItem> Heap_;
+
+ std::atomic<int> ThreadCount_ = 0;
+ std::array<TThreadState, TThreadPoolBase::MaxThreadCount> ThreadStates_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, TagMappingSpinLock_);
+ THashMap<TFairShareThreadPoolTag, TWeakPtr<TBucket>> TagToBucket_;
+
+ std::atomic<int> QueueSize_ = 0;
+
+ NProfiling::TSummary BucketCounter_;
+ NProfiling::TSummary SizeCounter_;
+ TEventTimer WaitTimeCounter_;
+ TEventTimer ExecTimeCounter_;
+ TEventTimer TotalTimeCounter_;
+
+
+ void AccountCurrentlyExecutingBuckets(NProfiling::TTscp tscp)
+ {
+ int threadCount = ThreadCount_.load();
+ for (int index = 0; index < threadCount; ++index) {
+ auto& threadState = ThreadStates_[index];
+ if (!threadState.Bucket) {
+ continue;
+ }
+
+ auto duration = tscp.Instant - threadState.AccountedAt;
+ threadState.AccountedAt = tscp.Instant;
+
+ UpdateExcessTime(threadState.Bucket.Get(), duration);
+ }
+ }
+
+ void UpdateExcessTime(TBucket* bucket, TCpuDuration duration)
+ {
+ bucket->ExcessTime += duration;
+
+ auto positionInHeap = bucket->HeapIterator;
+ if (!positionInHeap) {
+ return;
+ }
+
+ size_t indexInHeap = positionInHeap - Heap_.data();
+ YT_VERIFY(indexInHeap < Heap_.size());
+ SiftDown(Heap_.begin(), Heap_.end(), Heap_.begin() + indexInHeap, std::less<>());
+ }
+
+ TBucketPtr GetStarvingBucket(TEnqueuedAction* action, NProfiling::TTscp tscp)
+ {
+ // For each currently evaluating buckets recalculate excess time.
+ AccountCurrentlyExecutingBuckets(tscp);
+
+ #ifdef YT_ENABLE_TRACE_LOGGING
+ if (Logger.IsLevelEnabled(NLogging::ELogLevel::Trace)) {
+ auto guard = Guard(TagMappingSpinLock_);
+ YT_LOG_TRACE("Buckets: [%v]",
+ MakeFormattableView(
+ TagToBucket_,
+ [] (auto* builder, const auto& tagToBucket) {
+ if (auto item = tagToBucket.second.Lock()) {
+ auto excess = CpuDurationToDuration(tagToBucket.second.Lock()->ExcessTime).MilliSeconds();
+ builder->AppendFormat("(%v %v)", tagToBucket.first, excess);
+ } else {
+ builder->AppendFormat("(%v *)", tagToBucket.first);
+ }
+ }));
+ }
+ #endif
+
+ if (Heap_.empty()) {
+ return nullptr;
+ }
+
+ auto bucket = Heap_.front().Bucket;
+ YT_VERIFY(!bucket->Queue.empty());
+ *action = std::move(bucket->Queue.front());
+ bucket->Queue.pop();
+
+ if (bucket->Queue.empty()) {
+ ExtractHeap(Heap_.begin(), Heap_.end());
+ Heap_.pop_back();
+ }
+
+ return bucket;
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBucket::Invoke(TClosure callback)
+{
+ if (auto parent = Parent.Lock()) {
+ parent->Invoke(std::move(callback), this);
+ }
+}
+
+void TBucket::Invoke(TMutableRange<TClosure> callbacks)
+{
+ if (auto parent = Parent.Lock()) {
+ for (auto& callback : callbacks) {
+ parent->Invoke(std::move(callback), this);
+ }
+ }
+}
+
+TBucket::~TBucket()
+{
+ if (auto parent = Parent.Lock()) {
+ parent->RemoveBucket(this);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareThread
+ : public TSchedulerThread
+{
+public:
+ TFairShareThread(
+ TFairShareQueuePtr queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority,
+ int index)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName,
+ threadPriority,
+ /*shutdownPriority*/ 0)
+ , Queue_(std::move(queue))
+ , Index_(index)
+ { }
+
+protected:
+ const TFairShareQueuePtr Queue_;
+ const int Index_;
+
+ TEnqueuedAction CurrentAction;
+
+ TClosure BeginExecute() override
+ {
+ return Queue_->BeginExecute(&CurrentAction, Index_);
+ }
+
+ void EndExecute() override
+ {
+ Queue_->EndExecute(&CurrentAction, Index_);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareThread)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareThreadPool
+ : public IFairShareThreadPool
+ , public TThreadPoolBase
+{
+public:
+ TFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix)
+ : TThreadPoolBase(threadNamePrefix)
+ , Queue_(New<TFairShareQueue>(
+ CallbackEventCount_,
+ GetThreadTags(ThreadNamePrefix_)))
+ {
+ Configure(threadCount);
+ EnsureStarted();
+ }
+
+ ~TFairShareThreadPool()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount) override
+ {
+ TThreadPoolBase::Configure(threadCount);
+ }
+
+ IInvokerPtr GetInvoker(const TFairShareThreadPoolTag& tag) override
+ {
+ EnsureStarted();
+ return Queue_->GetInvoker(tag);
+ }
+
+ void Shutdown() override
+ {
+ TThreadPoolBase::Shutdown();
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TFairShareQueuePtr Queue_;
+
+
+ void DoShutdown() override
+ {
+ Queue_->Shutdown();
+ TThreadPoolBase::DoShutdown();
+ }
+
+ TClosure MakeFinalizerCallback() override
+ {
+ return BIND_NO_PROPAGATE([queue = Queue_, callback = TThreadPoolBase::MakeFinalizerCallback()] {
+ callback();
+ queue->Drain();
+ });
+ }
+
+ void DoConfigure(int threadCount) override
+ {
+ Queue_->Configure(threadCount);
+ TThreadPoolBase::DoConfigure(threadCount);
+ }
+
+ TSchedulerThreadBasePtr SpawnThread(int index) override
+ {
+ return New<TFairShareThread>(
+ Queue_,
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ MakeThreadName(index),
+ ThreadPriority_,
+ index);
+ }
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFairShareThreadPoolPtr CreateFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix)
+{
+ return New<TFairShareThreadPool>(
+ threadCount,
+ threadNamePrefix);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_share_thread_pool.h b/yt/yt/core/concurrency/fair_share_thread_pool.h
new file mode 100644
index 0000000000..77178465c8
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_share_thread_pool.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IFairShareThreadPool
+ : public virtual TRefCounted
+{
+ virtual IInvokerPtr GetInvoker(const TFairShareThreadPoolTag& tag) = 0;
+
+ virtual void Configure(int threadCount) = 0;
+
+ virtual void Shutdown() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFairShareThreadPool)
+
+IFairShareThreadPoolPtr CreateFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/fair_throttler.cpp b/yt/yt/core/concurrency/fair_throttler.cpp
new file mode 100644
index 0000000000..6bf04d8248
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_throttler.cpp
@@ -0,0 +1,965 @@
+#include "fair_throttler.h"
+
+#include "private.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/misc/fs.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <util/random/shuffle.h>
+
+#include <util/folder/path.h>
+
+#include <util/system/file_lock.h>
+#include <util/system/file.h>
+#include <util/system/filemap.h>
+#include <util/system/mlock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFairThrottlerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("total_limit", &TThis::TotalLimit)
+ .Default(125_MB);
+
+ registrar.Parameter("distribution_period", &TThis::DistributionPeriod)
+ .Default(TDuration::MilliSeconds(100))
+ .GreaterThan(TDuration::Zero());
+
+ registrar.Parameter("bucket_accumulation_ticks", &TThis::BucketAccumulationTicks)
+ .Default(5);
+
+ registrar.Parameter("global_accumulation_ticks", &TThis::GlobalAccumulationTicks)
+ .Default(5);
+
+ registrar.Parameter("ipc_path", &TThis::IPCPath)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFairThrottlerBucketConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("weight", &TThis::Weight)
+ .Default(1.0)
+ .GreaterThanOrEqual(0.01)
+ .LessThanOrEqual(100);
+
+ registrar.Parameter("limit", &TThis::Limit)
+ .Default();
+
+ registrar.Parameter("relative_limit", &TThis::RelativeLimit)
+ .Default();
+
+ registrar.Parameter("guarantee", &TThis::Guarantee)
+ .Default();
+
+ registrar.Parameter("relative_guaratee", &TThis::RelativeGuarantee)
+ .Default();
+}
+
+std::optional<i64> TFairThrottlerBucketConfig::GetLimit(i64 totalLimit)
+{
+ if (Limit) {
+ return Limit;
+ }
+
+ if (RelativeLimit) {
+ return totalLimit * *RelativeLimit;
+ }
+
+ return {};
+}
+
+std::optional<i64> TFairThrottlerBucketConfig::GetGuarantee(i64 totalLimit)
+{
+ if (Guarantee) {
+ return Guarantee;
+ }
+
+ if (RelativeGuarantee) {
+ return totalLimit * *RelativeGuarantee;
+ }
+
+ return {};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr TStringBuf LockFileName = "lock";
+static constexpr TStringBuf SharedFileName = "shared.v1";
+static constexpr TStringBuf BucketsFileName = "buckets.v1";
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileIPCBucket
+ : public IIPCBucket
+{
+public:
+ TFileIPCBucket(const TString& path, bool create)
+ : File_(path, OpenAlways | RdWr)
+ {
+ if (create) {
+ File_.Flock(LOCK_EX | LOCK_NB);
+ }
+
+ File_.Resize(sizeof(TBucket));
+
+ Map_ = std::make_unique<TFileMap>(File_, TMemoryMapCommon::oRdWr);
+ Map_->Map(0, Map_->Length());
+ LockMemory(Map_->Ptr(), Map_->Length());
+ }
+
+ TBucket* State() override
+ {
+ return reinterpret_cast<TBucket*>(Map_->Ptr());
+ }
+
+private:
+ TFile File_;
+ std::unique_ptr<TFileMap> Map_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TFileIPCBucket)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileThrottlerIPC
+ : public IThrottlerIPC
+{
+public:
+ explicit TFileThrottlerIPC(const TString& path)
+ : Path_(path)
+ {
+ NFS::MakeDirRecursive(Path_);
+ NFS::MakeDirRecursive(Path_ + "/" + BucketsFileName);
+
+ Lock_ = std::make_unique<TFileLock>(Path_ + "/" + LockFileName);
+
+ SharedBucketFile_ = TFile(Path_ + "/" + SharedFileName, OpenAlways | RdWr);
+ SharedBucketFile_.Resize(sizeof(TSharedBucket));
+
+ SharedBucketMap_ = std::make_unique<TFileMap>(SharedBucketFile_, TMemoryMapCommon::oRdWr);
+ SharedBucketMap_->Map(0, SharedBucketMap_->Length());
+ LockMemory(SharedBucketMap_->Ptr(), SharedBucketMap_->Length());
+ }
+
+ bool TryLock() override
+ {
+ try {
+ SharedBucketFile_.Flock(LOCK_EX | LOCK_NB);
+ return true;
+ } catch (const TSystemError& e) {
+ if (e.Status() != EWOULDBLOCK) {
+ throw;
+ }
+
+ return false;
+ }
+ }
+
+ TSharedBucket* State() override
+ {
+ return reinterpret_cast<TSharedBucket*>(SharedBucketMap_->Ptr());
+ }
+
+ std::vector<IIPCBucketPtr> ListBuckets() override
+ {
+ Lock_->Acquire();
+ auto release = Finally([this] {
+ Lock_->Release();
+ });
+
+ Reload();
+
+ std::vector<IIPCBucketPtr> buckets;
+ for (const auto& bucket : OpenBuckets_) {
+ buckets.push_back(bucket.second);
+ }
+ return buckets;
+ }
+
+ IIPCBucketPtr AddBucket() override
+ {
+ Lock_->Acquire();
+ auto release = Finally([this] {
+ Lock_->Release();
+ });
+
+ auto id = TGuid::Create();
+ OwnedBuckets_.insert(ToString(id));
+ return New<TFileIPCBucket>(Path_ + "/" + BucketsFileName + "/" + ToString(id), true);
+ }
+
+private:
+ const TString Path_;
+
+ std::unique_ptr<TFileLock> Lock_;
+
+ TFile SharedBucketFile_;
+ std::unique_ptr<TFileMap> SharedBucketMap_;
+
+ THashMap<TString, IIPCBucketPtr> OpenBuckets_;
+ THashSet<TString> OwnedBuckets_;
+
+ void Reload()
+ {
+ TVector<TString> currentBuckets;
+ TFsPath{Path_ + "/" + BucketsFileName}.ListNames(currentBuckets);
+
+ auto openBuckets = std::move(OpenBuckets_);
+ for (const auto& fileName : currentBuckets) {
+ if (OwnedBuckets_.find(fileName) != OwnedBuckets_.end()) {
+ continue;
+ }
+
+ try {
+ auto bucketPath = Path_ + "/" + BucketsFileName + "/" + fileName;
+
+ TFileLock lockCheck(bucketPath);
+ if (lockCheck.TryAcquire()) {
+ NFS::Remove(bucketPath);
+ continue;
+ }
+
+ if (auto it = openBuckets.find(fileName); it != openBuckets.end()) {
+ OpenBuckets_.emplace(fileName, it->second);
+ continue;
+ }
+
+ OpenBuckets_[fileName] = New<TFileIPCBucket>(bucketPath, false);
+ } catch (const TSystemError& ex) {
+ continue;
+ }
+ }
+ }
+};
+
+IThrottlerIPCPtr CreateFileThrottlerIPC(const TString& path)
+{
+ return New<TFileThrottlerIPC>(path);
+}
+
+DEFINE_REFCOUNTED_TYPE(TFileThrottlerIPC)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TBucketThrottleRequest)
+
+struct TBucketThrottleRequest
+ : public TRefCounted
+{
+ explicit TBucketThrottleRequest(i64 amount)
+ : Pending(amount)
+ { }
+
+ i64 Pending;
+ i64 Reserved = 0;
+ TPromise<void> Promise = NewPromise<void>();
+ std::atomic<bool> Cancelled = false;
+ NProfiling::TCpuInstant StartTime = NProfiling::GetCpuInstant();
+
+ void Cancel(const TError& /*error*/)
+ {
+ Cancelled = true;
+ Promise.TrySet(TError(NYT::EErrorCode::Canceled, "Cancelled"));
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucketThrottleRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLeakyCounter
+{
+ TLeakyCounter(int windowSize)
+ : Value(std::make_shared<std::atomic<i64>>(0))
+ , Window(windowSize)
+ { }
+
+ std::shared_ptr<std::atomic<i64>> Value;
+
+ std::vector<i64> Window;
+ int WindowPosition = 0;
+
+ i64 Increment(i64 delta)
+ {
+ return Increment(delta, delta);
+ }
+
+ i64 Increment(i64 delta, i64 limit)
+ {
+ auto maxValue = std::accumulate(Window.begin(), Window.end(), i64(0)) - Window[WindowPosition];
+
+ auto currentValue = Value->load();
+ do {
+ if (currentValue <= maxValue) {
+ break;
+ }
+ } while (!Value->compare_exchange_strong(currentValue, maxValue));
+
+ Window[WindowPosition] = limit;
+
+ WindowPosition++;
+ if (WindowPosition >= std::ssize(Window)) {
+ WindowPosition = 0;
+ }
+
+ *Value += delta;
+ return std::max<i64>(currentValue - maxValue, 0);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSharedBucket final
+{
+ TSharedBucket(int windowSize)
+ : Limit(windowSize)
+ { }
+
+ TLeakyCounter Limit;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSharedBucket)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBucketThrottler
+ : public IThroughputThrottler
+{
+public:
+ TBucketThrottler(
+ const NLogging::TLogger& logger,
+ const NProfiling::TProfiler& profiler,
+ const TSharedBucketPtr& sharedBucket,
+ TFairThrottlerConfigPtr config)
+ : Logger(logger)
+ , SharedBucket_(sharedBucket)
+ , Value_(profiler.Counter("/value"))
+ , WaitTime_(profiler.Timer("/wait_time"))
+ , Quota_(config->BucketAccumulationTicks)
+ , DistributionPeriod_(config->DistributionPeriod)
+ {
+ profiler.AddFuncGauge("/queue_size", MakeStrong(this), [this] {
+ return GetQueueTotalAmount();
+ });
+
+ profiler.AddFuncGauge("/quota", MakeStrong(this), [this] {
+ return Quota_.Value->load();
+ });
+
+ profiler.AddFuncGauge("/throttled", MakeStrong(this), [this] {
+ return IsOverdraft();
+ });
+ }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ if (TryAcquire(amount)) {
+ return VoidFuture;
+ }
+
+ auto request = New<TBucketThrottleRequest>(amount);
+ QueueSize_ += amount;
+
+ request->Promise.OnCanceled(BIND(&TBucketThrottleRequest::Cancel, MakeWeak(request)));
+ request->Promise.ToFuture().Subscribe(BIND(&TBucketThrottler::OnRequestComplete, MakeWeak(this), amount));
+
+ YT_LOG_DEBUG("Started waiting for throttler (Amount: %v)", amount);
+
+ auto guard = Guard(Lock_);
+ Queue_.push_back(request);
+ return request->Promise.ToFuture();
+ }
+
+ bool TryAcquire(i64 amount) override
+ {
+ YT_VERIFY(amount >= 0);
+
+ auto available = Quota_.Value->load();
+ auto globalAvailable = IsLimited() ? 0 : SharedBucket_->Limit.Value->load();
+
+ if (amount > available + globalAvailable) {
+ return false;
+ }
+
+ auto globalConsumed = std::min(amount - available, globalAvailable);
+ *Quota_.Value -= amount - globalConsumed;
+ *SharedBucket_->Limit.Value -= globalConsumed;
+
+ Value_.Increment(amount);
+ Usage_ += amount;
+
+ return true;
+ }
+
+ i64 TryAcquireAvailable(i64 amount) override
+ {
+ YT_VERIFY(amount >= 0);
+
+ auto available = Quota_.Value->load();
+ auto globalAvailable = IsLimited() ? 0 : SharedBucket_->Limit.Value->load();
+
+ auto consumed = std::min(amount, available + globalAvailable);
+
+ auto globalConsumed = std::min(consumed - available, globalAvailable);
+ *Quota_.Value -= amount - globalConsumed;
+ *SharedBucket_->Limit.Value -= globalConsumed;
+
+ Value_.Increment(consumed);
+ Usage_ += consumed;
+
+ return consumed;
+ }
+
+ void Acquire(i64 amount) override
+ {
+ YT_VERIFY(amount >= 0);
+
+ auto available = Quota_.Value->load();
+ auto globalAvailable = IsLimited() ? 0 : SharedBucket_->Limit.Value->load();
+
+ auto globalConsumed = std::min(amount - available, globalAvailable);
+ *Quota_.Value -= amount - globalConsumed;
+ *SharedBucket_->Limit.Value -= globalConsumed;
+
+ Value_.Increment(amount);
+ Usage_ += amount;
+ }
+
+ bool IsOverdraft() override
+ {
+ return GetQueueTotalAmount() > 0;
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ return Max<i64>(-Quota_.Value->load(), 0) + QueueSize_.load();
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ auto queueTotalAmount = GetQueueTotalAmount();
+ if (queueTotalAmount == 0) {
+ return TDuration::Zero();
+ }
+
+ auto limit = LastLimit_.load();
+ auto distributionPeriod = DistributionPeriod_.load();
+ if (limit == 0) {
+ return distributionPeriod;
+ }
+
+ return queueTotalAmount / limit * distributionPeriod;
+ }
+
+ i64 GetAvailable() const override
+ {
+ auto available = Quota_.Value->load();
+ auto globalAvailable = IsLimited() ? 0 : SharedBucket_->Limit.Value->load();
+ return available + globalAvailable;
+ }
+
+ struct TBucketState
+ {
+ i64 Usage; // Quota usage on current iteration.
+ i64 Quota;
+ i64 Overdraft; // Unpaid overdraft from previous iterations.
+ i64 QueueSize; // Total size of all queued requests.
+ };
+
+ TBucketState Peek()
+ {
+ auto quota = Quota_.Value->load();
+
+ return TBucketState{
+ .Usage = Usage_.exchange(0),
+ .Quota = quota,
+ .Overdraft = Max<i64>(-quota, 0),
+ .QueueSize = QueueSize_.load(),
+ };
+ }
+
+ i64 SatisfyRequests(i64 quota)
+ {
+ std::vector<TBucketThrottleRequestPtr> readyList;
+
+ {
+ auto guard = Guard(Lock_);
+ while (!Queue_.empty()) {
+ auto request = Queue_.front();
+ if (request->Cancelled.load()) {
+ quota += request->Reserved;
+ Queue_.pop_front();
+ continue;
+ }
+
+ if (request->Pending <= quota) {
+ quota -= request->Pending;
+ Queue_.pop_front();
+ readyList.push_back(std::move(request));
+ } else {
+ request->Pending -= quota;
+ request->Reserved += quota;
+ quota = 0;
+ break;
+ }
+ }
+ }
+
+ auto now = NProfiling::GetCpuInstant();
+ for (const auto& request : readyList) {
+ auto waitTime = NProfiling::CpuDurationToDuration(now - request->StartTime);
+
+ WaitTime_.Record(waitTime);
+ Value_.Increment(request->Pending + request->Reserved);
+ Usage_ += request->Pending + request->Reserved;
+
+ request->Promise.TrySet();
+ }
+
+ return quota;
+ }
+
+ i64 Refill(i64 quota)
+ {
+ LastLimit_ = quota;
+
+ if (Quota_.Value->load() < 0) {
+ auto remainingQuota = Quota_.Increment(quota);
+ return SatisfyRequests(remainingQuota);
+ } else {
+ auto remainingQuota = SatisfyRequests(quota);
+ return Quota_.Increment(remainingQuota, quota);
+ }
+ }
+
+ void SetDistributionPeriod(TDuration distributionPeriod)
+ {
+ DistributionPeriod_.store(distributionPeriod);
+ }
+
+ void SetLimited(bool limited)
+ {
+ Limited_ = limited;
+ }
+
+ bool IsLimited() const
+ {
+ return Limited_.load();
+ }
+
+private:
+ NLogging::TLogger Logger;
+
+ TSharedBucketPtr SharedBucket_;
+
+ NProfiling::TCounter Value_;
+ NProfiling::TEventTimer WaitTime_;
+
+ TLeakyCounter Quota_;
+ std::atomic<i64> LastLimit_ = 0;
+ std::atomic<i64> QueueSize_ = 0;
+ std::atomic<i64> Usage_ = 0;
+
+ std::atomic<bool> Limited_ = {false};
+
+ std::atomic<TDuration> DistributionPeriod_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ std::deque<TBucketThrottleRequestPtr> Queue_;
+
+ void OnRequestComplete(i64 amount, const TError& /*error*/)
+ {
+ QueueSize_ -= amount;
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucketThrottler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFairThrottler::TFairThrottler(
+ TFairThrottlerConfigPtr config,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler)
+ : Logger(std::move(logger))
+ , Profiler_(std::move(profiler))
+ , SharedBucket_(New<TSharedBucket>(config->GlobalAccumulationTicks))
+ , Config_(std::move(config))
+{
+ if (Config_->IPCPath) {
+ IPC_ = New<TFileThrottlerIPC>(*Config_->IPCPath);
+
+ SharedBucket_->Limit.Value = std::shared_ptr<std::atomic<i64>>(
+ &IPC_->State()->Value,
+ [ipc=IPC_] (auto /* ptr */) { }
+ );
+
+ Profiler_.AddFuncGauge("/leader", MakeStrong(this), [this] {
+ return IsLeader_.load();
+ });
+ } else {
+ IsLeader_ = true;
+ }
+
+ ScheduleLimitUpdate(TInstant::Now());
+
+ Profiler_.AddFuncGauge("/shared_quota", MakeStrong(this), [this] {
+ return SharedBucket_->Limit.Value->load();
+ });
+
+ Profiler_.AddFuncGauge("/total_limit", MakeStrong(this), [this] {
+ auto guard = Guard(Lock_);
+ return Config_->TotalLimit;
+ });
+}
+
+IThroughputThrottlerPtr TFairThrottler::CreateBucketThrottler(
+ const TString& name,
+ TFairThrottlerBucketConfigPtr config)
+{
+ if (!config) {
+ config = New<TFairThrottlerBucketConfig>();
+ }
+
+ auto guard = Guard(Lock_);
+ if (auto it = Buckets_.find(name); it != Buckets_.end()) {
+ it->second.Throttler->SetLimited(config->Limit || config->RelativeLimit);
+
+ it->second.Config = std::move(config);
+ it->second.Throttler->SetDistributionPeriod(Config_->DistributionPeriod);
+ return it->second.Throttler;
+ }
+
+ auto throttler = New<TBucketThrottler>(
+ Logger.WithTag("Bucket: %v", name),
+ Profiler_.WithTag("bucket", name),
+ SharedBucket_,
+ Config_);
+
+ throttler->SetLimited(config->Limit || config->RelativeLimit);
+
+ IIPCBucketPtr ipc;
+ if (IPC_) {
+ ipc = IPC_->AddBucket();
+ }
+
+ Buckets_[name] = TBucket{
+ .Config = std::move(config),
+ .Throttler = throttler,
+ .IPC = ipc,
+ };
+
+ return throttler;
+}
+
+void TFairThrottler::Reconfigure(
+ TFairThrottlerConfigPtr config,
+ const THashMap<TString, TFairThrottlerBucketConfigPtr>& buckets)
+{
+ for (const auto& [name, config] : buckets) {
+ CreateBucketThrottler(name, config);
+ }
+
+ auto guard = Guard(Lock_);
+ Config_ = std::move(config);
+}
+
+void TFairThrottler::DoUpdateLeader()
+{
+ auto guard = Guard(Lock_);
+
+ std::vector<double> weights;
+ weights.reserve(Buckets_.size());
+
+ std::vector<std::optional<i64>> limits;
+ limits.reserve(Buckets_.size());
+
+ std::vector<i64> demands;
+ demands.reserve(Buckets_.size());
+
+ std::vector<TBucketThrottler::TBucketState> states;
+ states.reserve(Buckets_.size());
+
+ THashMap<TString, i64> bucketDemands;
+ for (const auto& [name, bucket] : Buckets_) {
+ auto state = bucket.Throttler->Peek();
+
+ weights.push_back(bucket.Config->Weight);
+
+ if (auto limit = bucket.Config->GetLimit(Config_->TotalLimit)) {
+ limits.push_back(*limit * Config_->DistributionPeriod.SecondsFloat());
+ } else {
+ limits.emplace_back();
+ }
+
+ auto guarantee = bucket.Config->GetGuarantee(Config_->TotalLimit);
+ auto demand = state.Usage + state.Overdraft + state.QueueSize;
+ if (guarantee && *guarantee > demand) {
+ demand = *guarantee;
+ }
+
+ demands.push_back(demand);
+
+ bucketDemands[name] = demands.back();
+ states.push_back(state);
+ }
+
+ std::vector<IIPCBucketPtr> remoteBuckets;
+ if (IPC_) {
+ remoteBuckets = IPC_->ListBuckets();
+
+ for (const auto& remote : remoteBuckets) {
+ auto state = remote->State();
+
+ weights.push_back(state->Weight.load());
+
+ if (auto limit = state->Limit.load(); limit != -1) {
+ limits.push_back(limit);
+ } else {
+ limits.emplace_back();
+ }
+
+ demands.push_back(state->Demand.load());
+ }
+ }
+
+ auto tickLimit = i64(Config_->TotalLimit * Config_->DistributionPeriod.SecondsFloat());
+ auto tickIncome = ComputeFairDistribution(tickLimit, weights, demands, limits);
+
+ // Distribute remaining quota according to weights and limits.
+ auto freeQuota = tickLimit - std::accumulate(tickIncome.begin(), tickIncome.end(), i64(0));
+ for (int i = 0; i < std::ssize(tickIncome); i++) {
+ demands[i] = freeQuota;
+
+ if (limits[i]) {
+ (*limits[i]) -= tickIncome[i];
+ }
+ }
+ auto freeIncome = ComputeFairDistribution(freeQuota, weights, demands, limits);
+
+ THashMap<TString, i64> bucketUsage;
+ THashMap<TString, i64> bucketIncome;
+ THashMap<TString, i64> bucketQuota;
+
+ i64 leakedQuota = 0;
+ int i = 0;
+ for (const auto& [name, bucket] : Buckets_) {
+ auto state = states[i];
+ auto newLimit = tickIncome[i] + freeIncome[i];
+
+ bucketUsage[name] = state.Usage;
+ bucketQuota[name] = state.Quota;
+ bucketIncome[name] = newLimit;
+
+ leakedQuota += bucket.Throttler->Refill(newLimit);
+
+ ++i;
+ }
+
+ for (const auto& remote : remoteBuckets) {
+ auto state = remote->State();
+
+ state->InFlow += tickIncome[i] + freeIncome[i];
+
+ leakedQuota += state->OutFlow.exchange(0);
+
+ ++i;
+ }
+
+ if (Buckets_.empty() && remoteBuckets.empty()) {
+ leakedQuota = tickLimit;
+ }
+
+ i64 droppedQuota = SharedBucket_->Limit.Increment(leakedQuota);
+
+ RefillFromSharedBucket();
+
+ YT_LOG_DEBUG("Fair throttler tick (SharedBucket: %v, TickLimit: %v, FreeQuota: %v, DroppedQuota: %v)",
+ SharedBucket_->Limit.Value->load(),
+ tickLimit, // How many bytes was distributed?
+ freeQuota, // How many bytes was left unconsumed?
+ droppedQuota);
+
+ YT_LOG_DEBUG("Fair throttler tick details (BucketIncome: %v, BucketUsage: %v, BucketDemands: %v, BucketQuota: %v)",
+ bucketIncome,
+ bucketUsage,
+ bucketDemands,
+ bucketQuota);
+}
+
+void TFairThrottler::DoUpdateFollower()
+{
+ auto guard = Guard(Lock_);
+
+ THashMap<TString, i64> bucketIncome;
+ THashMap<TString, i64> bucketUsage;
+ THashMap<TString, i64> bucketDemands;
+ THashMap<TString, i64> bucketQuota;
+
+ i64 inFlow = 0;
+ i64 outFlow = 0;
+
+ for (const auto& [name, bucket] : Buckets_) {
+ auto ipc = bucket.IPC->State();
+
+ ipc->Weight = bucket.Config->Weight;
+ if (auto limit = bucket.Config->GetLimit(Config_->TotalLimit)) {
+ ipc->Limit = *limit;
+ } else {
+ ipc->Limit = -1;
+ }
+
+ auto state = bucket.Throttler->Peek();
+ auto guarantee = bucket.Config->GetGuarantee(Config_->TotalLimit);
+ auto demand = state.Usage + state.Overdraft + state.QueueSize;
+ if (guarantee && *guarantee > demand) {
+ demand = *guarantee;
+ }
+
+ ipc->Demand = demand;
+ bucketDemands[name] = demand;
+
+ auto in = ipc->InFlow.exchange(0);
+ auto out = bucket.Throttler->Refill(in);
+ ipc->OutFlow += out;
+
+ bucketIncome[name] = in;
+ bucketUsage[name] = state.Usage;
+ bucketQuota[name] = state.Quota;
+
+ inFlow += in;
+ outFlow += out;
+ }
+
+ RefillFromSharedBucket();
+
+ YT_LOG_DEBUG("Fair throttler tick (SharedBucket: %v, InFlow: %v, OutFlow: %v)",
+ SharedBucket_->Limit.Value->load(),
+ inFlow,
+ outFlow);
+
+ YT_LOG_DEBUG("Fair throttler tick details (BucketIncome: %v, BucketUsage: %v, BucketDemands: %v, BucketQuota: %v)",
+ bucketIncome,
+ bucketUsage,
+ bucketDemands,
+ bucketQuota);
+}
+
+void TFairThrottler::RefillFromSharedBucket()
+{
+ std::vector<TBucketThrottlerPtr> throttlers;
+ for (const auto& [name, bucket] : Buckets_) {
+ throttlers.push_back(bucket.Throttler);
+ }
+ Shuffle(throttlers.begin(), throttlers.end());
+
+ for (const auto& throttler : throttlers) {
+ auto limit = SharedBucket_->Limit.Value->load();
+ if (limit <= 0) {
+ break;
+ }
+
+ if (throttler->IsLimited()) {
+ continue;
+ }
+
+ *SharedBucket_->Limit.Value -= limit - throttler->SatisfyRequests(limit);
+ }
+}
+
+void TFairThrottler::UpdateLimits(TInstant at)
+{
+ if (!IsLeader_ && IPC_->TryLock()) {
+ IsLeader_ = true;
+
+ YT_LOG_DEBUG("Throttler is leader");
+ }
+
+ if (IsLeader_) {
+ DoUpdateLeader();
+ } else {
+ DoUpdateFollower();
+ }
+
+ auto guard = Guard(Lock_);
+ ScheduleLimitUpdate(at + Config_->DistributionPeriod);
+}
+
+void TFairThrottler::ScheduleLimitUpdate(TInstant at)
+{
+ TDelayedExecutor::Submit(
+ BIND(&TFairThrottler::UpdateLimits, MakeWeak(this), at),
+ at);
+}
+
+std::vector<i64> TFairThrottler::ComputeFairDistribution(
+ i64 totalLimit,
+ const std::vector<double>& weights,
+ const std::vector<i64>& demands,
+ const std::vector<std::optional<i64>>& limits)
+{
+ YT_VERIFY(weights.size() == demands.size() && weights.size() == limits.size());
+
+ const auto& Logger = ConcurrencyLogger;
+
+ if (weights.empty()) {
+ return {};
+ }
+
+ std::vector<std::pair<double, int>> queue;
+ for (int i = 0; i < std::ssize(weights); ++i) {
+ queue.emplace_back(Min(demands[i], limits[i].value_or(Max<i64>())) / weights[i], i);
+ }
+ std::sort(queue.begin(), queue.end());
+
+ std::vector<i64> totalLimits;
+ totalLimits.resize(weights.size());
+
+ double remainingWeight = std::accumulate(weights.begin(), weights.end(), 0.0);
+ i64 remainingCapacity = totalLimit;
+
+ int i = 0;
+ for ( ; i < std::ssize(weights); ++i) {
+ auto [targetShare, targetIndex] = queue[i];
+
+ YT_LOG_TRACE("Examining bucket (Index: %v, TargetShare: %v, RemainingWeight: %v, RemainingCapacity: %v)",
+ targetIndex,
+ targetShare,
+ remainingWeight,
+ remainingCapacity);
+
+ if (targetShare * remainingWeight >= static_cast<double>(remainingCapacity)) {
+ break;
+ }
+
+ totalLimits[targetIndex] = Min(demands[targetIndex], limits[targetIndex].value_or(Max<i64>()));
+ remainingCapacity -= totalLimits[targetIndex];
+ remainingWeight -= weights[targetIndex];
+
+ YT_LOG_TRACE("Satisfied demand (Index: %v, Capacity: %v)",
+ targetIndex,
+ totalLimits[targetIndex]);
+ }
+
+ auto finalShare = Max<double>(remainingCapacity, 0l) / Max(remainingWeight, 0.01);
+ for (int j = i; j < std::ssize(weights); j++) {
+ auto bucketIndex = queue[j].second;
+ totalLimits[bucketIndex] = weights[bucketIndex] * finalShare;
+
+ YT_LOG_TRACE("Distributed remains (Index: %v, Capacity: %v)",
+ bucketIndex,
+ totalLimits[bucketIndex]);
+ }
+
+ return totalLimits;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fair_throttler.h b/yt/yt/core/concurrency/fair_throttler.h
new file mode 100644
index 0000000000..10e2ab2368
--- /dev/null
+++ b/yt/yt/core/concurrency/fair_throttler.h
@@ -0,0 +1,163 @@
+#pragma once
+
+#include "throughput_throttler.h"
+
+#include "yt/yt/core/ytree/yson_struct.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFairThrottlerConfig
+ : public NYTree::TYsonStruct
+{
+ i64 TotalLimit;
+
+ TDuration DistributionPeriod;
+
+ int BucketAccumulationTicks;
+
+ int GlobalAccumulationTicks;
+
+ std::optional<TString> IPCPath;
+
+ REGISTER_YSON_STRUCT(TFairThrottlerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairThrottlerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFairThrottlerBucketConfig
+ : public NYTree::TYsonStruct
+{
+ double Weight;
+
+ std::optional<i64> Limit;
+ std::optional<double> RelativeLimit;
+ std::optional<i64> GetLimit(i64 totalLimit);
+
+ std::optional<i64> Guarantee;
+ std::optional<double> RelativeGuarantee;
+ std::optional<i64> GetGuarantee(i64 totalLimit);
+
+ REGISTER_YSON_STRUCT(TFairThrottlerBucketConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairThrottlerBucketConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IIPCBucket
+ : public TRefCounted
+{
+ // NB: This struct is shared between processes. All changes must be backward compatible.
+ struct TBucket
+ {
+ std::atomic<double> Weight;
+ std::atomic<i64> Limit;
+ std::atomic<i64> Demand;
+ std::atomic<i64> InFlow;
+ std::atomic<i64> OutFlow;
+ };
+
+ virtual TBucket* State() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IIPCBucket)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IThrottlerIPC
+ : public TRefCounted
+{
+ // NB: This struct is shared between processes. All changes must be backward compatible.
+ struct TSharedBucket
+ {
+ std::atomic<i64> Value = 0;
+ };
+
+ virtual bool TryLock() = 0;
+ virtual TSharedBucket* State() = 0;
+ virtual std::vector<IIPCBucketPtr> ListBuckets() = 0;
+ virtual IIPCBucketPtr AddBucket() = 0;
+};
+
+IThrottlerIPCPtr CreateFileThrottlerIPC(const TString& path);
+
+DEFINE_REFCOUNTED_TYPE(IThrottlerIPC)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TSharedBucket)
+
+//! TFairThrottler manages a group of throttlers, distributing traffic according to fair share policy.
+/*!
+ * TFairThrottler distributes TotalLimit * DistributionPeriod bytes every DistributionPeriod.
+ *
+ * At the beginning of the period, TFairThrottler distributes new quota between buckets. Buckets
+ * accumulate quota for N ticks. After N ticks overflown quota is transferred into shared bucket.
+ * Shared bucket accumulates quota for M ticks. Overflown quota from shared bucket is discarded.
+ *
+ * Throttled requests may consume quota from both local bucket and shared bucket.
+ */
+class TFairThrottler
+ : public TRefCounted
+{
+public:
+ TFairThrottler(
+ TFairThrottlerConfigPtr config,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler);
+
+ IThroughputThrottlerPtr CreateBucketThrottler(
+ const TString& name,
+ TFairThrottlerBucketConfigPtr config);
+
+ void Reconfigure(
+ TFairThrottlerConfigPtr config,
+ const THashMap<TString, TFairThrottlerBucketConfigPtr>& bucketConfigs);
+
+ static std::vector<i64> ComputeFairDistribution(
+ i64 totalLimit,
+ const std::vector<double>& weights,
+ const std::vector<i64>& demands,
+ const std::vector<std::optional<i64>>& limits);
+
+private:
+ const NLogging::TLogger Logger;
+ const NProfiling::TProfiler Profiler_;
+
+ TSharedBucketPtr SharedBucket_;
+ std::atomic<bool> IsLeader_ = false;
+
+ struct TBucket
+ {
+ TFairThrottlerBucketConfigPtr Config;
+ TBucketThrottlerPtr Throttler;
+ IIPCBucketPtr IPC;
+ };
+
+ // Protects all Config_ and Buckets_.
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TFairThrottlerConfigPtr Config_;
+ THashMap<TString, TBucket> Buckets_;
+
+ IThrottlerIPCPtr IPC_;
+
+ void DoUpdateLeader();
+ void DoUpdateFollower();
+ void RefillFromSharedBucket();
+ void UpdateLimits(TInstant at);
+ void ScheduleLimitUpdate(TInstant at);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairThrottler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fiber.cpp b/yt/yt/core/concurrency/fiber.cpp
new file mode 100644
index 0000000000..6da8552aef
--- /dev/null
+++ b/yt/yt/core/concurrency/fiber.cpp
@@ -0,0 +1,297 @@
+#include "fiber.h"
+
+#include "execution_stack.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/misc/singleton.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <util/system/yield.h>
+
+#include <util/random/random.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFiberProfiler
+ : public ISensorProducer
+{
+public:
+ void OnStackAllocated(i64 stackSize)
+ {
+ StackBytesAllocated_.fetch_add(stackSize, std::memory_order::relaxed);
+ StackBytesAlive_.fetch_add(stackSize, std::memory_order::relaxed);
+ }
+
+ void OnStackFreed(i64 stackSize)
+ {
+ StackBytesFreed_.fetch_add(stackSize, std::memory_order::relaxed);
+ StackBytesAlive_.fetch_sub(stackSize, std::memory_order::relaxed);
+ }
+
+ void OnFiberCreated()
+ {
+ FibersCreated_.fetch_add(1, std::memory_order::relaxed);
+ }
+
+ static TFiberProfiler* Get()
+ {
+ return LeakyRefCountedSingleton<TFiberProfiler>().Get();
+ }
+
+private:
+ std::atomic<i64> StackBytesAllocated_ = 0;
+ std::atomic<i64> StackBytesFreed_ = 0;
+ std::atomic<i64> StackBytesAlive_ = 0;
+ std::atomic<i64> FibersCreated_ = 0;
+
+ DECLARE_LEAKY_REF_COUNTED_SINGLETON_FRIEND()
+
+ TFiberProfiler()
+ {
+ TProfiler("").AddProducer("/fiber", MakeStrong(this));
+ }
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ writer->AddCounter("/created", FibersCreated_.load(std::memory_order::relaxed));
+
+ writer->AddCounter("/stack/bytes_allocated", StackBytesAllocated_.load(std::memory_order::relaxed));
+ writer->AddCounter("/stack/bytes_freed", StackBytesFreed_.load(std::memory_order::relaxed));
+ writer->AddGauge("/stack/bytes_alive", StackBytesAlive_.load(std::memory_order::relaxed));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFiberIdGenerator
+{
+public:
+ static TFiberIdGenerator* Get()
+ {
+ return LeakySingleton<TFiberIdGenerator>();
+ }
+
+ TFiberId Generate()
+ {
+ const TFiberId Factor = std::numeric_limits<TFiberId>::max() - 173864;
+ YT_ASSERT(Factor % 2 == 1); // Factor must be coprime with 2^n.
+
+ while (true) {
+ auto seed = Seed_++;
+ auto id = seed * Factor;
+ if (id != InvalidFiberId) {
+ return id;
+ }
+ }
+ }
+
+private:
+ std::atomic<TFiberId> Seed_;
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+
+ TFiberIdGenerator()
+ {
+ Seed_.store(static_cast<TFiberId>(::time(nullptr)));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFiberRegistry
+{
+public:
+ //! Do not rename, change the signature, or drop Y_NO_INLINE.
+ //! Used in devtools/gdb/yt_fibers_printer.py.
+ static Y_NO_INLINE TFiberRegistry* Get()
+ {
+ return LeakySingleton<TFiberRegistry>();
+ }
+
+ TFiber::TCookie Register(TFiber* fiber)
+ {
+ auto guard = Guard(Lock_);
+ return Fibers_.insert(Fibers_.begin(), fiber);
+ }
+
+ void Unregister(TFiber::TCookie cookie)
+ {
+ auto guard = Guard(Lock_);
+ Fibers_.erase(cookie);
+ }
+
+ std::vector<TFiberPtr> List()
+ {
+ auto guard = Guard(Lock_);
+ std::vector<TFiberPtr> fibers;
+ for (const auto& fiber : Fibers_) {
+ fibers.push_back(fiber);
+ }
+ return fibers;
+ }
+
+private:
+ NThreading::TForkAwareSpinLock Lock_;
+ std::list<TFiber*> Fibers_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFiber::TFiber(EExecutionStackKind stackKind)
+ : Stack_(CreateExecutionStack(stackKind))
+ , RegistryCookie_(TFiberRegistry::Get()->Register(this))
+ , MachineContext_({
+ this,
+ TArrayRef(static_cast<char*>(Stack_->GetStack()), Stack_->GetSize()),
+ })
+{
+ TFiberProfiler::Get()->OnFiberCreated();
+ TFiberProfiler::Get()->OnStackAllocated(Stack_->GetSize());
+}
+
+TFiber::~TFiber()
+{
+ YT_VERIFY(GetState() == EFiberState::Finished);
+ TFiberProfiler::Get()->OnStackFreed(Stack_->GetSize());
+ TFiberRegistry::Get()->Unregister(RegistryCookie_);
+}
+
+bool TFiber::CheckFreeStackSpace(size_t space) const
+{
+ return reinterpret_cast<char*>(Stack_->GetStack()) + space < __builtin_frame_address(0);
+}
+
+TExceptionSafeContext* TFiber::GetMachineContext()
+{
+ return &MachineContext_;
+}
+
+TFiberId TFiber::GetFiberId() const
+{
+ return FiberId_.load(std::memory_order::relaxed);
+}
+
+
+EFiberState TFiber::GetState() const
+{
+ return State_.load(std::memory_order::relaxed);
+}
+
+void TFiber::SetRunning()
+{
+ auto expectedState = State_.load(std::memory_order::relaxed);
+ std::optional<NProfiling::TWallTimer> lockedTimer;
+ do {
+ YT_VERIFY(expectedState != EFiberState::Running);
+ if (expectedState == EFiberState::Introspecting) {
+ if (!lockedTimer) {
+ lockedTimer.emplace();
+ }
+ ThreadYield();
+ expectedState = State_.load();
+ continue;
+ }
+ } while (!State_.compare_exchange_weak(expectedState, EFiberState::Running));
+
+ if (lockedTimer) {
+ YT_LOG_WARNING("Fiber execution was delayed due to introspection (FiberId: %x, Delay: %v)",
+ GetFiberId(),
+ lockedTimer->GetElapsedTime());
+ }
+}
+
+void TFiber::SetWaiting()
+{
+ WaitingSince_.store(GetApproximateCpuInstant(), std::memory_order::release);
+ State_.store(EFiberState::Waiting, std::memory_order::release);
+}
+
+void TFiber::SetFinished()
+{
+ State_.store(EFiberState::Finished);
+ Clear();
+}
+
+void TFiber::SetIdle()
+{
+ State_.store(EFiberState::Idle);
+ Clear();
+}
+
+bool TFiber::TryIntrospectWaiting(EFiberState& state, const std::function<void()>& func)
+{
+ state = State_.load();
+ if (state != EFiberState::Waiting) {
+ return false;
+ }
+ if (!State_.compare_exchange_strong(state, EFiberState::Introspecting)) {
+ return false;
+ }
+ auto guard = Finally([&] {
+ YT_VERIFY(State_.exchange(state) == EFiberState::Introspecting);
+ });
+ func();
+ return true;
+}
+
+TInstant TFiber::GetWaitingSince() const
+{
+ YT_VERIFY(State_.load() == EFiberState::Introspecting);
+ return CpuInstantToInstant(WaitingSince_.load());
+}
+
+const TPropagatingStorage& TFiber::GetPropagatingStorage() const
+{
+ YT_VERIFY(State_.load() == EFiberState::Introspecting);
+ return NConcurrency::GetPropagatingStorage(*Fls_);
+}
+
+TFls* TFiber::GetFls() const
+{
+ return Fls_.get();
+}
+
+void TFiber::Recreate()
+{
+ FiberId_.store(TFiberIdGenerator::Get()->Generate(), std::memory_order::release);
+ Fls_ = std::make_unique<TFls>();
+}
+
+void TFiber::Clear()
+{
+ FiberId_.store(InvalidFiberId);
+ Fls_.reset();
+}
+
+std::vector<TFiberPtr> TFiber::List()
+{
+ return TFiberRegistry::Get()->List();
+}
+
+namespace NDetail {
+
+void FiberTrampoline();
+
+} // namespace NDetail
+
+void TFiber::DoRunNaked()
+{
+ NDetail::FiberTrampoline();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fiber.h b/yt/yt/core/concurrency/fiber.h
new file mode 100644
index 0000000000..1212fd9752
--- /dev/null
+++ b/yt/yt/core/concurrency/fiber.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "private.h"
+#include "propagating_storage.h"
+#include "fls.h"
+
+#include <util/system/context.h>
+
+#include <atomic>
+#include <list>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFiber
+ : public TRefCounted
+ , public ITrampoLine
+{
+public:
+ using TCookie = std::list<TFiber*>::iterator;
+
+ explicit TFiber(EExecutionStackKind stackKind = EExecutionStackKind::Small);
+ ~TFiber();
+
+ void Recreate();
+
+ bool CheckFreeStackSpace(size_t space) const;
+
+ TExceptionSafeContext* GetMachineContext();
+ TFiberId GetFiberId() const;
+ EFiberState GetState() const;
+
+ void SetRunning();
+ void SetWaiting();
+ void SetFinished();
+ void SetIdle();
+
+ bool TryIntrospectWaiting(EFiberState& state, const std::function<void()>& func);
+
+ TInstant GetWaitingSince() const;
+ const TPropagatingStorage& GetPropagatingStorage() const;
+ TFls* GetFls() const;
+
+ static std::vector<TFiberPtr> List();
+
+private:
+ const std::shared_ptr<TExecutionStack> Stack_;
+ const TCookie RegistryCookie_;
+ TExceptionSafeContext MachineContext_;
+
+ std::atomic<TFiberId> FiberId_ = InvalidFiberId;
+ std::atomic<EFiberState> State_ = EFiberState::Created;
+ std::atomic<TCpuInstant> WaitingSince_ = 0;
+
+ std::unique_ptr<TFls> Fls_;
+
+ void Clear();
+
+ void DoRunNaked() override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TFiber)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fiber_scheduler_thread.cpp b/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
new file mode 100644
index 0000000000..b28cf8f811
--- /dev/null
+++ b/yt/yt/core/concurrency/fiber_scheduler_thread.cpp
@@ -0,0 +1,1029 @@
+#include "fiber_scheduler_thread.h"
+
+#include "private.h"
+#include "fiber.h"
+
+#include <yt/yt/core/misc/finally.h>
+#include <yt/yt/core/misc/shutdown.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/library/profiling/producer.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <library/cpp/yt/memory/memory_tag.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <library/cpp/yt/memory/memory_tag.h>
+
+#include <util/thread/lfstack.h>
+
+#include <thread>
+
+namespace NYT::NConcurrency {
+
+using namespace NLogging;
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+DECLARE_REFCOUNTED_CLASS(TRefCountedGauge)
+
+class TRefCountedGauge
+ : public TRefCounted
+ , public NProfiling::TGauge
+{
+public:
+ TRefCountedGauge(const NProfiling::TRegistry& profiler, const TString& name)
+ : NProfiling::TGauge(profiler.Gauge(name))
+ { }
+
+ void Increment(i64 delta)
+ {
+ auto value = Value_.fetch_add(delta, std::memory_order::relaxed) + delta;
+ NProfiling::TGauge::Update(value);
+ }
+
+private:
+ std::atomic<i64> Value_ = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(TRefCountedGauge)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RunInFiberContext(TFiber* fiber, TClosure callback);
+void SwitchFromThread(TFiberPtr targetFiber);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Non POD TLS sometimes does not work correctly in dynamic library.
+struct TFiberContext
+{
+ TFiberContext() = default;
+
+ TFiberContext(
+ TFiberSchedulerThread* fiberThread,
+ const TString& threadGroupName)
+ : FiberThread(fiberThread)
+ , WaitingFibersCounter(New<NDetail::TRefCountedGauge>(
+ NProfiling::TRegistry("/fiber").WithTag("thread", threadGroupName).WithHot(),
+ "/waiting"))
+ { }
+
+ TFiberSchedulerThread* const FiberThread = nullptr;
+ const TRefCountedGaugePtr WaitingFibersCounter;
+
+ TExceptionSafeContext MachineContext;
+ TClosure AfterSwitch;
+ TFiberPtr ResumerFiber;
+ TFiberPtr CurrentFiber;
+};
+
+static thread_local TFiberContext* FiberContext;
+
+// Forbid inlining these accessors to prevent the compiler from
+// miss-optimizing TLS access in presence of fiber context switches.
+Y_NO_INLINE TFiberContext* TryGetFiberContext()
+{
+ return FiberContext;
+}
+
+Y_NO_INLINE void SetFiberContext(TFiberContext* context)
+{
+ FiberContext = context;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFiberContextGuard
+{
+public:
+ explicit TFiberContextGuard(TFiberContext* context)
+ {
+ SetFiberContext(context);
+ }
+
+ ~TFiberContextGuard()
+ {
+ SetFiberContext(nullptr);
+ }
+
+ TFiberContextGuard(const TFiberContextGuard&) = delete;
+ TFiberContextGuard operator=(const TFiberContextGuard&) = delete;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TMemoryTag SwapMemoryTag(TMemoryTag tag)
+{
+ auto result = GetCurrentMemoryTag();
+ SetCurrentMemoryTag(tag);
+ return result;
+}
+
+Y_FORCE_INLINE TFiberId SwapCurrentFiberId(TFiberId fiberId)
+{
+ auto result = GetCurrentFiberId();
+ SetCurrentFiberId(fiberId);
+ return result;
+}
+
+Y_FORCE_INLINE ELogLevel SwapMinLogLevel(ELogLevel minLogLevel)
+{
+ auto result = GetThreadMinLogLevel();
+ SetThreadMinLogLevel(minLogLevel);
+ return result;
+}
+
+Y_FORCE_INLINE TExceptionSafeContext* GetMachineContext()
+{
+ return &TryGetFiberContext()->MachineContext;
+}
+
+Y_FORCE_INLINE void SetAfterSwitch(TClosure&& closure)
+{
+ auto* context = TryGetFiberContext();
+ YT_VERIFY(!context->AfterSwitch);
+ context->AfterSwitch = std::move(closure);
+}
+
+Y_FORCE_INLINE TClosure ExtractAfterSwitch()
+{
+ auto* context = TryGetFiberContext();
+ return std::move(context->AfterSwitch);
+}
+
+Y_FORCE_INLINE void SetResumerFiber(TFiberPtr fiber)
+{
+ auto* context = TryGetFiberContext();
+ YT_VERIFY(!context->ResumerFiber);
+ context->ResumerFiber = std::move(fiber);
+}
+
+Y_FORCE_INLINE TFiberPtr ExtractResumerFiber()
+{
+ return std::move(TryGetFiberContext()->ResumerFiber);
+}
+
+Y_FORCE_INLINE TFiber* TryGetResumerFiber()
+{
+ return TryGetFiberContext()->ResumerFiber.Get();
+}
+
+Y_FORCE_INLINE TFiberPtr SwapCurrentFiber(TFiberPtr fiber)
+{
+ return std::exchange(TryGetFiberContext()->CurrentFiber, std::move(fiber));
+}
+
+Y_FORCE_INLINE TFiber* TryGetCurrentFiber()
+{
+ auto* context = TryGetFiberContext();
+ return context ? context->CurrentFiber.Get() : nullptr;
+}
+
+Y_FORCE_INLINE TFiber* GetCurrentFiber()
+{
+ auto* fiber = TryGetFiberContext()->CurrentFiber.Get();
+ YT_VERIFY(fiber);
+ return fiber;
+}
+
+Y_FORCE_INLINE TFiberSchedulerThread* TryGetFiberThread()
+{
+ return TryGetFiberContext()->FiberThread;
+}
+
+Y_FORCE_INLINE TRefCountedGaugePtr GetWaitingFibersCounter()
+{
+ return TryGetFiberContext()->WaitingFibersCounter;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RunAfterSwitch()
+{
+ if (auto afterSwitch = ExtractAfterSwitch()) {
+ afterSwitch();
+ }
+}
+
+void SwitchMachineContext(TExceptionSafeContext* from, TExceptionSafeContext* to)
+{
+ from->SwitchTo(to);
+
+ RunAfterSwitch();
+
+ // TODO(lukyan): Allow to set after switch inside itself
+ YT_VERIFY(!ExtractAfterSwitch());
+}
+
+void SwitchFromThread(TFiberPtr targetFiber)
+{
+ YT_ASSERT(targetFiber);
+
+ targetFiber->SetRunning();
+
+ auto* targetContext = targetFiber->GetMachineContext();
+
+ auto currentFiber = SwapCurrentFiber(std::move(targetFiber));
+ YT_VERIFY(!currentFiber);
+
+ SwitchMachineContext(GetMachineContext(), targetContext);
+
+ YT_VERIFY(!TryGetCurrentFiber());
+}
+
+[[noreturn]] void SwitchToThread()
+{
+ auto currentFiber = SwapCurrentFiber(nullptr);
+ auto* currentContext = currentFiber->GetMachineContext();
+ currentFiber.Reset();
+
+ SwitchMachineContext(currentContext, GetMachineContext());
+
+ YT_ABORT();
+}
+
+void SwitchFromFiber(TFiberPtr targetFiber)
+{
+ YT_ASSERT(targetFiber);
+
+ targetFiber->SetRunning();
+ auto* targetContext = targetFiber->GetMachineContext();
+
+ auto currentFiber = SwapCurrentFiber(std::move(targetFiber));
+ YT_VERIFY(currentFiber->GetState() != EFiberState::Waiting);
+ auto* currentContext = currentFiber->GetMachineContext();
+
+ SwitchMachineContext(currentContext, targetContext);
+
+ YT_VERIFY(TryGetCurrentFiber() == currentFiber);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef YT_REUSE_FIBERS
+
+class TIdleFiberPool
+{
+public:
+ static TIdleFiberPool* Get()
+ {
+ return LeakySingleton<TIdleFiberPool>();
+ }
+
+ void EnqueueIdleFiber(TFiberPtr fiber)
+ {
+ IdleFibers_.Enqueue(std::move(fiber));
+ if (DestroyingIdleFibers_.load()) {
+ DoDestroyIdleFibers();
+ }
+ }
+
+ TFiberPtr TryDequeueIdleFiber()
+ {
+ TFiberPtr fiber;
+ IdleFibers_.Dequeue(&fiber);
+ return fiber;
+ }
+
+private:
+ const TShutdownCookie ShutdownCookie_ = RegisterShutdownCallback(
+ "FiberManager",
+ BIND_NO_PROPAGATE(&TIdleFiberPool::DestroyIdleFibers, this),
+ /*priority*/ -100);
+
+ TLockFreeStack<TFiberPtr> IdleFibers_;
+ std::atomic<bool> DestroyingIdleFibers_ = false;
+
+
+ void DestroyIdleFibers()
+ {
+ DestroyingIdleFibers_.store(true);
+ DoDestroyIdleFibers();
+ }
+
+ void DoDestroyIdleFibers()
+ {
+ auto destroyFibers = [&] {
+ TFiberContext fiberContext;
+ TFiberContextGuard fiberContextGuard(&fiberContext);
+
+ std::vector<TFiberPtr> fibers;
+ IdleFibers_.DequeueAll(&fibers);
+
+ for (const auto& fiber : fibers) {
+ SwitchFromThread(std::move(fiber));
+ }
+ };
+
+ #ifdef _unix_
+ // The current thread could be already exiting and MacOS has some issues
+ // with registering new thread-local terminators in this case:
+ // https://github.com/lionheart/openradar-mirror/issues/20926
+ // As a matter of workaround, we offload all finalization logic to a separate
+ // temporary thread.
+ std::thread thread([&] {
+ ::TThread::SetCurrentThreadName("IdleFiberDtor");
+
+ destroyFibers();
+ });
+ thread.join();
+ #else
+ // Starting threads in exit handlers on Windows causes immediate calling exit
+ // so the routine will not be executed. Moreover, if we try to join this thread we'll get deadlock
+ // because this thread will try to acquire atexit lock which is owned by this thread.
+ destroyFibers();
+ #endif
+ }
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+};
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FiberTrampoline()
+{
+ RunAfterSwitch();
+
+ YT_LOG_DEBUG("Fiber started");
+
+ auto* currentFiber = GetCurrentFiber();
+
+ // Break loop to terminate fiber
+ while (auto* fiberThread = TryGetFiberThread()) {
+ YT_VERIFY(!TryGetResumerFiber());
+
+ TCallback<void()> callback;
+ {
+ // We wrap fiberThread->OnExecute() into a propagating storage guard to ensure
+ // that the propagating storage created there won't spill into the fiber callbacks.
+ TNullPropagatingStorageGuard guard;
+ YT_VERIFY(guard.GetOldStorage().IsNull());
+ callback = fiberThread->OnExecute();
+ }
+
+ if (!callback) {
+ break;
+ }
+
+ try {
+ RunInFiberContext(currentFiber, std::move(callback));
+ } catch (const TFiberCanceledException&) {
+ // Just swallow.
+ }
+
+ // Trace context can be restored for resumer fiber, so current trace context and memory tag are
+ // not necessarily null. Check them after switch from and returning into current fiber.
+ if (auto resumerFiber = ExtractResumerFiber()) {
+ // Suspend current fiber.
+#ifdef YT_REUSE_FIBERS
+ {
+ // TODO(lukyan): Use simple callbacks without memory allocation.
+ // Make TFiber::MakeIdle method instead of lambda function.
+ // Switch out and add fiber to idle fibers.
+ // Save fiber in AfterSwitch because it can be immediately concurrently reused.
+ SetAfterSwitch(BIND_NO_PROPAGATE([currentFiber = MakeStrong(currentFiber)] () mutable {
+ currentFiber->SetIdle();
+ TIdleFiberPool::Get()->EnqueueIdleFiber(std::move(currentFiber));
+ }));
+ }
+
+ // Switched to ResumerFiber or thread main.
+ SwitchFromFiber(std::move(resumerFiber));
+#else
+ SetAfterSwitch(BIND_NO_PROPAGATE([
+ currentFiber = MakeStrong(currentFiber),
+ resumerFiber = std::move(resumerFiber)
+ ] () mutable {
+ currentFiber.Reset();
+ SwitchFromThread(std::move(resumerFiber));
+ }));
+ break;
+#endif
+ }
+ }
+
+ YT_LOG_DEBUG("Fiber finished");
+
+ SetAfterSwitch(BIND_NO_PROPAGATE([currentFiber = MakeStrong(currentFiber)] () mutable {
+ currentFiber->SetFinished();
+ currentFiber.Reset();
+ }));
+
+ // All allocated objects in this frame must be destroyed here.
+ SwitchToThread();
+}
+
+void YieldFiber(TClosure afterSwitch)
+{
+ YT_VERIFY(TryGetCurrentFiber());
+
+ SetAfterSwitch(std::move(afterSwitch));
+
+ // Try to get resumer.
+ auto targetFiber = ExtractResumerFiber();
+
+ // If there is no resumer switch to idle fiber. Or switch to thread main.
+#ifdef YT_REUSE_FIBERS
+ if (!targetFiber) {
+ targetFiber = TIdleFiberPool::Get()->TryDequeueIdleFiber();
+ }
+#endif
+
+ if (!targetFiber) {
+ targetFiber = New<TFiber>();
+ }
+
+ auto waitingFibersCounter = GetWaitingFibersCounter();
+ waitingFibersCounter->Increment(1);
+
+ SwitchFromFiber(std::move(targetFiber));
+ YT_VERIFY(TryGetResumerFiber());
+
+ waitingFibersCounter->Increment(-1);
+}
+
+void ResumeFiber(TFiberPtr targetFiber)
+{
+ TMemoryTagGuard guard(NullMemoryTag);
+
+ auto currentFiber = MakeStrong(GetCurrentFiber());
+
+ SetResumerFiber(currentFiber);
+ SetAfterSwitch(BIND([currentFiber = std::move(currentFiber)] {
+ currentFiber->SetWaiting();
+ }));
+
+ SwitchFromFiber(std::move(targetFiber));
+ YT_VERIFY(!TryGetResumerFiber());
+}
+
+class TFiberSwitchHandler;
+TFiberSwitchHandler* TryGetFiberSwitchHandler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TCanceler)
+
+class TCanceler
+ : public ::NYT::NDetail::TBindStateBase
+{
+public:
+ explicit TCanceler(TFiberId id)
+ : TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ TSourceLocation("", 0)
+#endif
+ )
+ , FiberId_(id)
+ { }
+
+ bool IsCanceled() const
+ {
+ return Canceled_.load(std::memory_order::relaxed);
+ }
+
+ void SetFuture(TFuture<void> awaitable)
+ {
+ auto guard = Guard(Lock_);
+ Future_ = std::move(awaitable);
+ }
+
+ void ResetFuture()
+ {
+ auto guard = Guard(Lock_);
+ Future_.Reset();
+ }
+
+ void Cancel(const TError& error)
+ {
+ bool expected = false;
+ if (!Canceled_.compare_exchange_strong(expected, true, std::memory_order::relaxed)) {
+ return;
+ }
+
+ TFuture<void> future;
+ {
+ auto guard = Guard(Lock_);
+ CancelationError_ = error;
+ future = std::move(Future_);
+ }
+
+ if (future) {
+ YT_LOG_DEBUG("Sending cancelation to fiber, propagating to the awaited future (TargetFiberId: %x)",
+ FiberId_);
+ future.Cancel(error);
+ } else {
+ YT_LOG_DEBUG("Sending cancelation to fiber (TargetFiberId: %x)",
+ FiberId_);
+ }
+ }
+
+ TError GetCancelationError() const
+ {
+ auto guard = Guard(Lock_);
+ return CancelationError_;
+ }
+
+ void Run(const TError& error)
+ {
+ Cancel(error);
+ }
+
+ static void StaticInvoke(const TError& error, NYT::NDetail::TBindStateBase* stateBase)
+ {
+ auto* state = static_cast<TCanceler*>(stateBase);
+ return state->Run(error);
+ }
+
+ TFiberId GetFiberId() const
+ {
+ return FiberId_;
+ }
+
+private:
+ const TFiberId FiberId_;
+
+ std::atomic<bool> Canceled_ = false;
+ NThreading::TSpinLock Lock_;
+ TError CancelationError_;
+ TFuture<void> Future_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCanceler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TContextSwitchManager
+{
+public:
+ static TContextSwitchManager* Get()
+ {
+ return Singleton<TContextSwitchManager>();
+ }
+
+ void RegisterGlobalHandlers(
+ TGlobalContextSwitchHandler outHandler,
+ TGlobalContextSwitchHandler inHandler)
+ {
+ auto guard = Guard(Lock_);
+ int index = HandlerCount_.load();
+ YT_VERIFY(index < MaxHandlerCount);
+ Handlers_[index] = {outHandler, inHandler};
+ ++HandlerCount_;
+ }
+
+ void OnOut()
+ {
+ int count = HandlerCount_.load(std::memory_order::acquire);
+ for (int index = 0; index < count; ++index) {
+ if (const auto& handler = Handlers_[index].Out) {
+ handler();
+ }
+ }
+ }
+
+ void OnIn()
+ {
+ int count = HandlerCount_.load();
+ for (int index = count - 1; index >= 0; --index) {
+ if (const auto& handler = Handlers_[index].In) {
+ handler();
+ }
+ }
+ }
+
+private:
+ NThreading::TForkAwareSpinLock Lock_;
+
+ struct TGlobalContextSwitchHandlers
+ {
+ TGlobalContextSwitchHandler Out;
+ TGlobalContextSwitchHandler In;
+ };
+
+ static constexpr int MaxHandlerCount = 16;
+ std::array<TGlobalContextSwitchHandlers, MaxHandlerCount> Handlers_;
+ std::atomic<int> HandlerCount_ = 0;
+
+ TContextSwitchManager() = default;
+ Y_DECLARE_SINGLETON_FRIEND()
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! All context thread local variables which must be preserved for each fiber are listed here.
+class TBaseSwitchHandler
+{
+protected:
+ void OnSwitch()
+ {
+ FiberId_ = SwapCurrentFiberId(FiberId_);
+ MemoryTag_ = SwapMemoryTag(MemoryTag_);
+ Fls_ = SwapCurrentFls(Fls_);
+ MinLogLevel_ = SwapMinLogLevel(MinLogLevel_);
+ }
+
+ ~TBaseSwitchHandler()
+ {
+ YT_VERIFY(FiberId_ == InvalidFiberId);
+ YT_VERIFY(MemoryTag_ == NullMemoryTag);
+ YT_VERIFY(!Fls_);
+ YT_VERIFY(MinLogLevel_ == ELogLevel::Minimum);
+ }
+
+private:
+ TMemoryTag MemoryTag_ = NullMemoryTag;
+ TFls* Fls_ = nullptr;
+ TFiberId FiberId_ = InvalidFiberId;
+ ELogLevel MinLogLevel_ = ELogLevel::Minimum;
+};
+
+class TFiberSwitchHandler
+ : public TBaseSwitchHandler
+{
+public:
+ // On start fiber running.
+ explicit TFiberSwitchHandler(TFiber* fiber)
+ : Fiber_(fiber)
+ {
+ SavedThis_ = std::exchange(This_, this);
+
+ YT_VERIFY(SwapCurrentFiberId(fiber->GetFiberId()) == InvalidFiberId);
+ YT_VERIFY(!SwapCurrentFls(fiber->GetFls()));
+ }
+
+ // On finish fiber running.
+ ~TFiberSwitchHandler()
+ {
+ YT_VERIFY(This_ == this);
+ YT_VERIFY(UserHandlers_.empty());
+
+ YT_VERIFY(SwapCurrentFiberId(InvalidFiberId) == Fiber_->GetFiberId());
+ YT_VERIFY(SwapCurrentFls(nullptr) == Fiber_->GetFls());
+
+ // Support case when current fiber has been resumed, but finished without WaitFor.
+ // There is preserved context of resumer fiber saved in switchHandler. Restore it.
+ // If there are no values for resumer the following call will swap null with null.
+ OnSwitch();
+ }
+
+ TFiberSwitchHandler(const TFiberSwitchHandler&) = delete;
+ TFiberSwitchHandler(TFiberSwitchHandler&&) = delete;
+
+ TCancelerPtr& Canceler()
+ {
+ return Canceler_;
+ }
+
+ class TGuard
+ {
+ public:
+ TGuard(const TGuard&) = delete;
+ TGuard(TGuard&&) = delete;
+
+ TGuard()
+ : SwitchHandler_(This_)
+ {
+ YT_VERIFY(SwitchHandler_);
+ SwitchHandler_->OnOut();
+ }
+
+ ~TGuard()
+ {
+ SwitchHandler_->OnIn();
+ }
+
+ private:
+ TFiberSwitchHandler* const SwitchHandler_;
+ };
+
+private:
+ friend TContextSwitchGuard;
+ friend TFiberSwitchHandler* TryGetFiberSwitchHandler();
+
+ const TFiber* const Fiber_;
+
+ TFiberSwitchHandler* SavedThis_;
+ static thread_local TFiberSwitchHandler* This_;
+
+ struct TContextSwitchHandlers
+ {
+ TContextSwitchHandler Out;
+ TContextSwitchHandler In;
+ };
+
+ TCompactVector<TContextSwitchHandlers, 16> UserHandlers_;
+
+ TCancelerPtr Canceler_;
+
+ void OnSwitch()
+ {
+ // In user defined context switch callbacks (ContextSwitchGuard) Swap must be used. It preserves context
+ // from fiber resumer.
+ // In internal SwitchIn/SwitchOut Get/Set must be used.
+
+ TBaseSwitchHandler::OnSwitch();
+
+ std::swap(SavedThis_, This_);
+ }
+
+ // On finish fiber running.
+ void OnOut()
+ {
+ TContextSwitchManager::Get()->OnOut();
+
+ for (auto it = UserHandlers_.begin(); it != UserHandlers_.end(); ++it) {
+ if (it->Out) {
+ it->Out();
+ }
+ }
+
+ OnSwitch();
+ }
+
+ // On start fiber running.
+ void OnIn()
+ {
+ OnSwitch();
+
+ for (auto it = UserHandlers_.rbegin(); it != UserHandlers_.rend(); ++it) {
+ if (it->In) {
+ it->In();
+ }
+ }
+
+ TContextSwitchManager::Get()->OnIn();
+ }
+};
+
+thread_local TFiberSwitchHandler* TFiberSwitchHandler::This_;
+
+TFiberSwitchHandler* TryGetFiberSwitchHandler()
+{
+ return TFiberSwitchHandler::This_;
+}
+
+TFiberSwitchHandler* GetFiberSwitchHandler()
+{
+ auto* switchHandler = TryGetFiberSwitchHandler();
+ YT_VERIFY(switchHandler);
+ return switchHandler;
+}
+
+// Prevent inlining for backtrace examination.
+// See devtools/gdb/yt_fibers_printer.py.
+Y_NO_INLINE void RunInFiberContext(TFiber* fiber, TClosure callback)
+{
+ fiber->Recreate();
+ TFiberSwitchHandler switchHandler(fiber);
+ TNullPropagatingStorageGuard nullPropagatingStorageGuard;
+ callback();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Compared to GuardedInvoke TResumeGuard reduces frame count in backtrace.
+class TResumeGuard
+{
+public:
+ TResumeGuard(TFiberPtr fiber, TCancelerPtr canceler)
+ : Fiber_(std::move(fiber))
+ , Canceler_(std::move(canceler))
+ { }
+
+ explicit TResumeGuard(TResumeGuard&& other)
+ : Fiber_(std::move(other.Fiber_))
+ , Canceler_(std::move(other.Canceler_))
+ { }
+
+ TResumeGuard(const TResumeGuard&) = delete;
+
+ TResumeGuard& operator=(const TResumeGuard&) = delete;
+ TResumeGuard& operator=(TResumeGuard&&) = delete;
+
+ void operator()()
+ {
+ YT_VERIFY(Fiber_);
+ Canceler_.Reset();
+ NDetail::ResumeFiber(std::move(Fiber_));
+ }
+
+ ~TResumeGuard()
+ {
+ if (Fiber_) {
+ YT_LOG_TRACE("Unwinding fiber (TargetFiberId: %x)", Canceler_->GetFiberId());
+
+ Canceler_->Run(TError("Fiber resumer is lost"));
+ Canceler_.Reset();
+
+ GetFinalizerInvoker()->Invoke(
+ BIND_NO_PROPAGATE(&NDetail::ResumeFiber, Passed(std::move(Fiber_))));
+ }
+ }
+
+private:
+ TFiberPtr Fiber_;
+ TCancelerPtr Canceler_;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFiberSchedulerThread::TFiberSchedulerThread(
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority,
+ int shutdownPriority)
+ : TThread(threadName, threadPriority, shutdownPriority)
+ , ThreadGroupName_(threadGroupName)
+{ }
+
+void TFiberSchedulerThread::ThreadMain()
+{
+ // Hold this strongly.
+ auto this_ = MakeStrong(this);
+
+ try {
+ YT_LOG_DEBUG("Thread started (Name: %v)",
+ GetThreadName());
+
+ NDetail::TFiberContext fiberContext(this, ThreadGroupName_);
+ NDetail::TFiberContextGuard fiberContextGuard(&fiberContext);
+
+ NDetail::SwitchFromThread(New<TFiber>());
+
+ YT_LOG_DEBUG("Thread stopped (Name: %v)",
+ GetThreadName());
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Unhandled exception in thread main (Name: %v)",
+ GetThreadName());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+thread_local TFiberId CurrentFiberId;
+
+TFiberId GetCurrentFiberId()
+{
+ return CurrentFiberId;
+}
+
+void SetCurrentFiberId(TFiberId id)
+{
+ CurrentFiberId = id;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool CheckFreeStackSpace(size_t space)
+{
+ auto* currentFiber = NDetail::TryGetCurrentFiber();
+ return !currentFiber || currentFiber->CheckFreeStackSpace(space);
+}
+
+TFiberCanceler GetCurrentFiberCanceler()
+{
+ auto* switchHandler = NDetail::TryGetFiberSwitchHandler();
+ if (!switchHandler) {
+ // Not in fiber context.
+ return {};
+ }
+
+ if (!switchHandler->Canceler()) {
+ TMemoryTagGuard guard(NullMemoryTag);
+ switchHandler->Canceler() = New<NDetail::TCanceler>(GetCurrentFiberId());
+ }
+
+ return TFiberCanceler(switchHandler->Canceler(), &NDetail::TCanceler::StaticInvoke);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WaitUntilSet(TFuture<void> future, IInvokerPtr invoker)
+{
+ YT_VERIFY(future);
+ YT_ASSERT(invoker);
+
+ TMemoryTagGuard memoryTagGuard(NullMemoryTag);
+
+ auto* currentFiber = NDetail::TryGetCurrentFiber();
+ if (!currentFiber) {
+ // When called from a fiber-unfriendly context, we fallback to blocking wait.
+ YT_VERIFY(invoker == GetCurrentInvoker());
+ YT_VERIFY(invoker == GetSyncInvoker());
+ YT_VERIFY(future.Wait());
+ return;
+ }
+
+ YT_VERIFY(invoker != GetSyncInvoker());
+
+ // Ensure canceler created.
+ GetCurrentFiberCanceler();
+
+ const auto& canceler = NDetail::GetFiberSwitchHandler()->Canceler();
+ if (canceler->IsCanceled()) {
+ future.Cancel(canceler->GetCancelationError());
+ }
+
+ canceler->SetFuture(future);
+ auto finally = Finally([&] {
+ canceler->ResetFuture();
+ });
+
+ // TODO(lukyan): transfer resumer as argument of AfterSwitch.
+ // Use CallOnTop like in boost.
+ auto afterSwitch = BIND_NO_PROPAGATE([
+ canceler,
+ invoker = std::move(invoker),
+ future = std::move(future),
+ currentFiber = MakeStrong(currentFiber)
+ ] () mutable {
+ currentFiber->SetWaiting();
+ future.Subscribe(BIND_NO_PROPAGATE([
+ invoker = std::move(invoker),
+ currentFiber = std::move(currentFiber),
+ canceler = std::move(canceler)
+ ] (const TError&) mutable {
+ YT_LOG_DEBUG("Waking up fiber (TargetFiberId: %x)",
+ canceler->GetFiberId());
+
+ invoker->Invoke(
+ BIND_NO_PROPAGATE(NDetail::TResumeGuard(std::move(currentFiber), std::move(canceler))));
+ }));
+ });
+
+ {
+ NDetail::TFiberSwitchHandler::TGuard switchGuard;
+ NDetail::YieldFiber(std::move(afterSwitch));
+ }
+
+ if (canceler->IsCanceled()) {
+ YT_LOG_DEBUG("Throwing fiber cancelation exception");
+ throw TFiberCanceledException();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void InstallGlobalContextSwitchHandlers(
+ TGlobalContextSwitchHandler outHandler,
+ TGlobalContextSwitchHandler inHandler)
+{
+ NDetail::TContextSwitchManager::Get()->RegisterGlobalHandlers(
+ outHandler,
+ inHandler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TContextSwitchGuard::TContextSwitchGuard(
+ TContextSwitchHandler outHandler,
+ TContextSwitchHandler inHandler)
+{
+ if (auto* context = NDetail::TryGetFiberSwitchHandler()) {
+ context->UserHandlers_.push_back({std::move(outHandler), std::move(inHandler)});
+ }
+}
+
+TContextSwitchGuard::~TContextSwitchGuard()
+{
+ if (auto* context = NDetail::TryGetFiberSwitchHandler()) {
+ YT_VERIFY(!context->UserHandlers_.empty());
+ context->UserHandlers_.pop_back();
+ }
+}
+
+TOneShotContextSwitchGuard::TOneShotContextSwitchGuard(TContextSwitchHandler outHandler)
+ : TContextSwitchGuard(
+ [this, handler = std::move(outHandler)] () noexcept {
+ if (!Active_) {
+ return;
+ }
+ Active_ = false;
+ handler();
+ },
+ nullptr)
+ , Active_(true)
+{ }
+
+TForbidContextSwitchGuard::TForbidContextSwitchGuard()
+ : TOneShotContextSwitchGuard([] { YT_ABORT(); })
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fiber_scheduler_thread.h b/yt/yt/core/concurrency/fiber_scheduler_thread.h
new file mode 100644
index 0000000000..377025c51a
--- /dev/null
+++ b/yt/yt/core/concurrency/fiber_scheduler_thread.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/threading/thread.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Executes actions in fiber context.
+class TFiberSchedulerThread
+ : public NThreading::TThread
+{
+public:
+ TFiberSchedulerThread(
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority = NThreading::EThreadPriority::Normal,
+ int shutdownPriority = 0);
+
+ //! Empty callback signals about stopping.
+ virtual TClosure OnExecute() = 0;
+
+private:
+ void ThreadMain() override;
+
+ const TString ThreadGroupName_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fls-inl.h b/yt/yt/core/concurrency/fls-inl.h
new file mode 100644
index 0000000000..eb588c0511
--- /dev/null
+++ b/yt/yt/core/concurrency/fls-inl.h
@@ -0,0 +1,118 @@
+#ifndef FLS_INL_H_
+#error "Direct inclusion of this file is not allowed, include fls.h"
+// For the sake of sane code completion.
+#include "fls.h"
+#endif
+#undef FLS_INL_H_
+
+#include <library/cpp/yt/memory/memory_tag.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+using TFlsSlotCtor = TFls::TCookie(*)();
+using TFlsSlotDtor = void(*)(TFls::TCookie cookie);
+
+int AllocateFlsSlot(TFlsSlotDtor dtor);
+TFls* GetPerThreadFls();
+
+extern thread_local TFls* CurrentFls;
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TFls::TCookie TFls::Get(int index) const
+{
+ // NB: This produces the best assembly so far, with just one
+ // additional compare + branch (which is perfectly predictable).
+ // Reinterpret casts are required since incrementing a pointer
+ // past the allocated storage is UB.
+ auto ptr = reinterpret_cast<uintptr_t>(Slots_.data()) + index * sizeof(TCookie);
+ if (Y_UNLIKELY(ptr >= reinterpret_cast<uintptr_t>(Slots_.data() + Slots_.size()))) {
+ return nullptr;
+ }
+ return *reinterpret_cast<const TCookie*>(ptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TFls* GetCurrentFls()
+{
+ auto* fls = NDetail::CurrentFls;
+ if (Y_UNLIKELY(!fls)) {
+ fls = NDetail::GetPerThreadFls();
+ }
+ return fls;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFlsSlot<T>::TFlsSlot()
+ : Index_(NDetail::AllocateFlsSlot([] (TFls::TCookie cookie) {
+ delete static_cast<T*>(cookie);
+ }))
+{ }
+
+template <class T>
+Y_FORCE_INLINE T* TFlsSlot<T>::operator->()
+{
+ return GetOrCreate();
+}
+
+template <class T>
+Y_FORCE_INLINE const T* TFlsSlot<T>::operator->() const
+{
+ return GetOrCreate();
+}
+
+template <class T>
+Y_FORCE_INLINE T& TFlsSlot<T>::operator*()
+{
+ return *GetOrCreate();
+}
+
+template <class T>
+Y_FORCE_INLINE const T& TFlsSlot<T>::operator*() const
+{
+ return *GetOrCreate();
+}
+
+template <class T>
+Y_FORCE_INLINE T* TFlsSlot<T>::GetOrCreate() const
+{
+ auto cookie = GetCurrentFls()->Get(Index_);
+ if (Y_UNLIKELY(!cookie)) {
+ cookie = Create();
+ }
+ return static_cast<T*>(cookie);
+}
+
+template <class T>
+T* TFlsSlot<T>::Create() const
+{
+ TMemoryTagGuard guard(NullMemoryTag);
+ auto cookie = new T();
+ GetCurrentFls()->Set(Index_, cookie);
+ return static_cast<T*>(cookie);
+}
+
+template <class T>
+const T* TFlsSlot<T>::Get(const TFls& fls) const
+{
+ return static_cast<const T*>(fls.Get(Index_));
+}
+
+template <class T>
+Y_FORCE_INLINE bool TFlsSlot<T>::IsInitialized() const
+{
+ return static_cast<bool>(GetCurrentFls()->Get(Index_));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/fls.cpp b/yt/yt/core/concurrency/fls.cpp
new file mode 100644
index 0000000000..447aae688d
--- /dev/null
+++ b/yt/yt/core/concurrency/fls.cpp
@@ -0,0 +1,85 @@
+#include "fls.h"
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <util/system/sanitizers.h>
+
+#include <array>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+constexpr int MaxFlsSize = 256;
+std::atomic<int> FlsSize;
+
+NThreading::TForkAwareSpinLock FlsLock;
+std::array<TFlsSlotDtor, MaxFlsSize> FlsDtors;
+
+thread_local TFls* PerThreadFls;
+thread_local TFls* CurrentFls;
+
+int AllocateFlsSlot(TFlsSlotDtor dtor)
+{
+ auto guard = Guard(FlsLock);
+
+ int index = FlsSize++;
+ YT_VERIFY(index < MaxFlsSize);
+
+ FlsDtors[index] = dtor;
+
+ return index;
+}
+
+void DestructFlsSlot(int index, TFls::TCookie cookie)
+{
+ FlsDtors[index](cookie);
+}
+
+TFls* GetPerThreadFls()
+{
+ if (!PerThreadFls) {
+ // This is only needed when some code attempts to interact with FLS outside of a fiber context.
+ // Unfortunately there's no safe place to destroy this FLS upon thread shutdown.
+ PerThreadFls = new TFls();
+ NSan::MarkAsIntentionallyLeaked(PerThreadFls);
+ }
+ return PerThreadFls;
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFls::~TFls()
+{
+ for (int index = 0; index < std::ssize(Slots_); ++index) {
+ if (auto cookie = Slots_[index]) {
+ NDetail::DestructFlsSlot(index, cookie);
+ }
+ }
+}
+
+void TFls::Set(int index, TCookie cookie)
+{
+ if (Y_UNLIKELY(index >= std::ssize(Slots_))) {
+ int newSize = NDetail::FlsSize.load();
+ YT_VERIFY(index < newSize);
+ Slots_.resize(newSize);
+ }
+ Slots_[index] = cookie;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFls* SwapCurrentFls(TFls* newFls)
+{
+ return std::exchange(NDetail::CurrentFls, newFls);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency::NDetail
+
diff --git a/yt/yt/core/concurrency/fls.h b/yt/yt/core/concurrency/fls.h
new file mode 100644
index 0000000000..616fd7d590
--- /dev/null
+++ b/yt/yt/core/concurrency/fls.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFls
+{
+public:
+ TFls() = default;
+ ~TFls();
+
+ TFls(const TFls&) = delete;
+ TFls& operator=(const TFls&) = delete;
+
+ using TCookie = void*;
+
+ TCookie Get(int index) const;
+ void Set(int index, TCookie cookie);
+
+private:
+ std::vector<TCookie> Slots_;
+};
+
+TFls* GetCurrentFls();
+TFls* SwapCurrentFls(TFls* newFls);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TFlsSlot
+{
+public:
+ TFlsSlot();
+
+ const T& operator*() const;
+ T& operator*();
+
+ const T* operator->() const;
+ T* operator->();
+
+ T* GetOrCreate() const;
+ const T* Get(const TFls& fls) const;
+
+ bool IsInitialized() const;
+
+private:
+ T* Create() const;
+
+ const int Index_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define FLS_INL_H_
+#include "fls-inl.h"
+#undef FLS_INL_H_
diff --git a/yt/yt/core/concurrency/invoker_alarm.cpp b/yt/yt/core/concurrency/invoker_alarm.cpp
new file mode 100644
index 0000000000..026a602c58
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_alarm.cpp
@@ -0,0 +1,96 @@
+#include "invoker_alarm.h"
+#include "delayed_executor.h"
+
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInvokerAlarm::TInvokerAlarm(IInvokerPtr invoker)
+ : Invoker_(std::move(invoker))
+{
+ VERIFY_INVOKER_THREAD_AFFINITY(Invoker_, HomeThread);
+}
+
+void TInvokerAlarm::Arm(TClosure callback, TInstant deadline)
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+ YT_VERIFY(callback);
+
+ auto epoch = ++Epoch_;
+ Callback_ = std::move(callback);
+ Deadline_ = deadline;
+
+ TDelayedExecutor::Submit(
+ BIND([=, this, weakThis = MakeWeak(this)] {
+ auto strongThis = weakThis.Lock();
+ if (!strongThis) {
+ return;
+ }
+
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ if (Epoch_ != epoch || !Callback_) {
+ return;
+ }
+
+ InvokeCallback();
+ }),
+ deadline,
+ Invoker_);
+}
+
+void TInvokerAlarm::Arm(TClosure callback, TDuration delay)
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ Arm(std::move(callback), delay.ToDeadLine());
+}
+
+void TInvokerAlarm::Disarm()
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ ++Epoch_;
+ Callback_.Reset();
+ Deadline_ = TInstant::Max();
+}
+
+bool TInvokerAlarm::IsArmed() const
+{
+ return static_cast<bool>(Callback_);
+}
+
+bool TInvokerAlarm::Check()
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ if (!Callback_) {
+ return false;
+ }
+
+ if (NProfiling::GetInstant() < Deadline_) {
+ return false;
+ }
+
+ InvokeCallback();
+ return true;
+}
+
+void TInvokerAlarm::InvokeCallback()
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ // Beware of recursion, reset the state before invoking the callback.
+ auto callback = std::move(Callback_);
+ Deadline_ = TInstant::Max();
+
+ callback();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/invoker_alarm.h b/yt/yt/core/concurrency/invoker_alarm.h
new file mode 100644
index 0000000000..fa51d4e89f
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_alarm.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "public.h"
+#include "thread_affinity.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables notifying a given invoker that a certain deadline has been reached.
+/*!
+ * The invoker is passed to the ctor and must be single-threaded.
+ *
+ * Alarm can be set via #Arm and dismissed via #Disarm. At most one deadline
+ * can be active at the moment; arming the alarm with a new deadline dismisses
+ * the previous one.
+ *
+ * When the deadline is reached, a callback (given upon arming the alarm) is enqueued to the invoker.
+ *
+ * Clients may also call #Check at any time to see if the deadline is already reached;
+ * if so, the callback is invoked synchronously (and its scheduled invocation becomes a no-op).
+ *
+ * \note
+ * Thread-affininty: single-threaded (moreover, all methods must be called within the invoker).
+ */
+class TInvokerAlarm
+ : public TRefCounted
+{
+public:
+ explicit TInvokerAlarm(IInvokerPtr invoker);
+
+ void Arm(TClosure callback, TInstant deadline);
+ void Arm(TClosure callback, TDuration delay);
+ void Disarm();
+
+ bool IsArmed() const;
+
+ bool Check();
+
+private:
+ const IInvokerPtr Invoker_;
+
+ TClosure Callback_;
+ TInstant Deadline_ = TInstant::Max();
+ ui64 Epoch_ = 0;
+
+ DECLARE_THREAD_AFFINITY_SLOT(HomeThread);
+
+ void InvokeCallback();
+};
+
+DEFINE_REFCOUNTED_TYPE(TInvokerAlarm)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/invoker_queue.cpp b/yt/yt/core/concurrency/invoker_queue.cpp
new file mode 100644
index 0000000000..8ef001416f
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_queue.cpp
@@ -0,0 +1,592 @@
+#include "invoker_queue.h"
+#include "private.h"
+
+#include <yt/yt/core/actions/invoker_detail.h>
+#include <yt/yt/core/actions/current_invoker.h>
+
+#include <yt/yt/core/profiling/tscp.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+using namespace NYTProf;
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+constinit thread_local TCpuProfilerTagGuard CpuProfilerTagGuard;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TMpmcQueueImpl::Enqueue(TEnqueuedAction&& action)
+{
+ DoEnqueue(
+ action.EnqueuedAt,
+ [&] (TBucket* bucket) {
+ EnqueueTo(bucket, std::move(action));
+ });
+}
+
+void TMpmcQueueImpl::Enqueue(TMutableRange<TEnqueuedAction> actions)
+{
+ if (Y_UNLIKELY(actions.empty())) {
+ return;
+ }
+
+ DoEnqueue(
+ actions[0].EnqueuedAt,
+ [&] (TBucket* bucket) {
+ EnqueueTo(bucket, actions);
+ });
+}
+
+template <class T>
+void TMpmcQueueImpl::DoEnqueue(TCpuInstant instant, T&& func)
+{
+ auto currentSelector = BucketSelector_.load(std::memory_order::acquire);
+ auto currentBucketIndex = currentSelector & 1;
+ auto& bucket = Buckets_[currentBucketIndex];
+ func(&bucket);
+ auto currentSelectorState = currentSelector & 3;
+ if (currentSelectorState == 0 || currentSelectorState == 3) {
+ auto currentEpoch = currentSelector >> 2;
+ auto newEpoch = EpochFromInstant(instant);
+ if (newEpoch != currentEpoch) {
+ auto newSelectorState = (currentSelectorState == 0 ? 1UL : 2UL);
+ auto newSelector = newSelectorState | (newEpoch << 2);
+ BucketSelector_.compare_exchange_weak(currentSelector, newSelector);
+ }
+ }
+}
+
+Y_FORCE_INLINE void TMpmcQueueImpl::EnqueueTo(TBucket* bucket, TEnqueuedAction&& action)
+{
+ YT_VERIFY(bucket->enqueue(std::move(action)));
+ Size_.fetch_add(1, std::memory_order::release);
+}
+
+Y_FORCE_INLINE void TMpmcQueueImpl::EnqueueTo(TBucket* bucket, TMutableRange<TEnqueuedAction> actions)
+{
+ auto size = std::ssize(actions);
+ YT_VERIFY(bucket->enqueue_bulk(std::make_move_iterator(actions.Begin()), size));
+ Size_.fetch_add(size, std::memory_order::release);
+}
+
+bool TMpmcQueueImpl::TryDequeue(TEnqueuedAction* action, TConsumerToken* token)
+{
+ if (Size_.load() <= 0) {
+ return false;
+ }
+
+ // Fast path.
+ if (Size_.fetch_sub(1) <= 0) {
+ Size_.fetch_add(1);
+
+ // Slow path.
+ auto queueSize = Size_.load();
+ while (queueSize > 0 && !Size_.compare_exchange_weak(queueSize, queueSize - 1));
+
+ if (queueSize <= 0) {
+ return false;
+ }
+ }
+
+ constexpr int MaxSpinCount = 100;
+ for (int spinCount = 0; ; spinCount++) {
+ auto currentSelector = BucketSelector_.load(std::memory_order::acquire);
+ auto currentBucketIndex = (currentSelector & 3) >> 1;
+ auto& bucket = Buckets_[currentBucketIndex];
+ bool result = token
+ ? bucket.try_dequeue((*token)[currentBucketIndex], *action)
+ : bucket.try_dequeue(*action);
+ if (result) {
+ break;
+ }
+ auto currentSelectorState = currentSelector & 3;
+ if (currentSelectorState == 1 || currentSelectorState == 2 || spinCount > MaxSpinCount) {
+ auto newSelectorState = currentSelectorState <= 1 ? 3UL : 0UL;
+ auto newEpoch = EpochFromInstant(GetApproximateCpuInstant());
+ auto newSelector = newSelectorState | (newEpoch << 2);
+ BucketSelector_.compare_exchange_weak(currentSelector, newSelector);
+ spinCount = 0;
+ }
+ }
+
+ return true;
+}
+
+void TMpmcQueueImpl::DrainProducer()
+{
+ auto queueSize = Size_.load();
+ // Must use CAS to prevent modifying Size_ if it is negative.
+ while (queueSize > 0 && !Size_.compare_exchange_weak(queueSize, 0));
+
+ TEnqueuedAction action;
+ while (queueSize-- > 0) {
+ [&] {
+ while (true) {
+ for (auto& bucket : Buckets_) {
+ if (bucket.try_dequeue(action)) {
+ return;
+ }
+ }
+ }
+ }();
+ }
+}
+
+void TMpmcQueueImpl::DrainConsumer()
+{
+ DrainProducer();
+}
+
+TMpmcQueueImpl::TConsumerToken TMpmcQueueImpl::MakeConsumerToken()
+{
+ return {
+ moodycamel::ConsumerToken(Buckets_[0]),
+ moodycamel::ConsumerToken(Buckets_[1]),
+ };
+}
+
+bool TMpmcQueueImpl::IsEmpty() const
+{
+ return Size_.load() <= 0;
+}
+
+bool TMpmcQueueImpl::HasSingleConsumer() const
+{
+ return false;
+}
+
+ui64 TMpmcQueueImpl::EpochFromInstant(TCpuInstant instant)
+{
+ // ~1 ms for 1 GHz clock.
+ return instant >> 20;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void TMpscQueueImpl::Enqueue(TEnqueuedAction&& action)
+{
+ Queue_.Enqueue(std::move(action));
+}
+
+Y_FORCE_INLINE void TMpscQueueImpl::Enqueue(TMutableRange<TEnqueuedAction> actions)
+{
+ for (auto& action : actions) {
+ Enqueue(std::move(action));
+ }
+}
+
+Y_FORCE_INLINE bool TMpscQueueImpl::TryDequeue(TEnqueuedAction* action, TConsumerToken* /*token*/)
+{
+ return Queue_.TryDequeue(action);
+}
+
+void TMpscQueueImpl::DrainProducer()
+{
+ Queue_.DrainProducer();
+}
+
+void TMpscQueueImpl::DrainConsumer()
+{
+ Queue_.DrainConsumer();
+}
+
+TMpscQueueImpl::TConsumerToken TMpscQueueImpl::MakeConsumerToken()
+{
+ return {};
+}
+
+bool TMpscQueueImpl::IsEmpty() const
+{
+ return Queue_.IsEmpty();
+}
+
+bool TMpscQueueImpl::HasSingleConsumer() const
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+class TProfilingTagSettingInvoker
+ : public IInvoker
+{
+public:
+ TProfilingTagSettingInvoker(
+ TWeakPtr<TInvokerQueue<TQueueImpl>> queue,
+ NProfiling::TTagId profilingTag,
+ TProfilerTagPtr profilerTag)
+ : Queue_(std::move(queue))
+ , ProfilingTag_(profilingTag)
+ , ProfilerTag_(std::move(profilerTag))
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ if (auto queue = Queue_.Lock()) {
+ queue->Invoke(std::move(callback), ProfilingTag_, ProfilerTag_);
+ }
+ }
+
+ void Invoke(TMutableRange<TClosure> callbacks) override
+ {
+ if (auto queue = Queue_.Lock()) {
+ for (auto& callback : callbacks) {
+ queue->Invoke(std::move(callback), ProfilingTag_, ProfilerTag_);
+ }
+ }
+ }
+
+ TThreadId GetThreadId() const override
+ {
+ if (auto queue = Queue_.Lock()) {
+ return queue->GetThreadId();
+ } else {
+ return {};
+ }
+ }
+
+ bool CheckAffinity(const IInvokerPtr& invoker) const override
+ {
+ return invoker.Get() == this;
+ }
+
+ bool IsSerialized() const override
+ {
+ if (auto queue = Queue_.Lock()) {
+ return queue->IsSerialized();
+ } else {
+ return true;
+ }
+ }
+
+private:
+ const TWeakPtr<TInvokerQueue<TQueueImpl>> Queue_;
+ const int ProfilingTag_;
+ const TProfilerTagPtr ProfilerTag_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+TInvokerQueue<TQueueImpl>::TInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TTagSet& counterTagSet)
+ : CallbackEventCount_(std::move(callbackEventCount))
+{
+ Counters_.push_back(CreateCounters(counterTagSet));
+}
+
+template <class TQueueImpl>
+TInvokerQueue<TQueueImpl>::TInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const std::vector<TTagSet>& counterTagSets,
+ const std::vector<NYTProf::TProfilerTagPtr>& profilerTags,
+ const TTagSet& cumulativeCounterTagSet)
+ : CallbackEventCount_(std::move(callbackEventCount))
+{
+ YT_VERIFY(counterTagSets.size() == profilerTags.size());
+
+ Counters_.reserve(counterTagSets.size());
+ for (const auto& tagSet : counterTagSets) {
+ Counters_.push_back(CreateCounters(tagSet));
+ }
+
+ CumulativeCounters_ = CreateCounters(cumulativeCounterTagSet);
+
+ ProfilingTagSettingInvokers_.reserve(Counters_.size());
+ for (int index = 0; index < std::ssize(Counters_); ++index) {
+ ProfilingTagSettingInvokers_.push_back(
+ New<TProfilingTagSettingInvoker<TQueueImpl>>(MakeWeak(this), index, profilerTags[index]));
+ }
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::SetThreadId(TThreadId threadId)
+{
+ ThreadId_ = threadId;
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::Invoke(TClosure callback)
+{
+ YT_ASSERT(Counters_.size() == 1);
+ Invoke(std::move(callback), /*profilingTag*/ 0, /*profilerTag*/ nullptr);
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::Invoke(TMutableRange<TClosure> callbacks)
+{
+ EnqueueCallbacks(callbacks);
+ CallbackEventCount_->NotifyMany(std::ssize(callbacks));
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::Invoke(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ TProfilerTagPtr profilerTag)
+{
+ EnqueueCallback(std::move(callback), profilingTag, profilerTag);
+ CallbackEventCount_->NotifyOne();
+}
+
+template <class TQueueImpl>
+TEnqueuedAction TInvokerQueue<TQueueImpl>::MakeAction(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ TProfilerTagPtr profilerTag,
+ TCpuInstant cpuInstant)
+{
+ YT_ASSERT(callback);
+ YT_ASSERT(profilingTag >= 0 && profilingTag < std::ssize(Counters_));
+
+ YT_LOG_TRACE("Callback enqueued (Callback: %v, ProfilingTag: %v)",
+ callback.GetHandle(),
+ profilingTag);
+
+ return {
+ .Finished = false,
+ .EnqueuedAt = cpuInstant,
+ .Callback = std::move(callback),
+ .ProfilingTag = profilingTag,
+ .ProfilerTag = std::move(profilerTag)
+ };
+}
+
+template <class TQueueImpl>
+TCpuInstant TInvokerQueue<TQueueImpl>::EnqueueCallback(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ TProfilerTagPtr profilerTag)
+{
+ if (!Running_.load(std::memory_order::relaxed)) {
+ DrainProducer();
+ YT_LOG_TRACE(
+ "Queue had been shut down, incoming action ignored (Callback: %v)",
+ callback.GetHandle());
+ return GetCpuInstant();
+ }
+
+ auto cpuInstant = GetCpuInstant();
+
+ auto action = MakeAction(std::move(callback), profilingTag, std::move(profilerTag), cpuInstant);
+
+ auto updateCounters = [&] (const TCountersPtr& counters) {
+ if (counters) {
+ counters->ActiveCallbacks += 1;
+ counters->EnqueuedCounter.Increment();
+ }
+ };
+ updateCounters(Counters_[profilingTag]);
+ updateCounters(CumulativeCounters_);
+
+ QueueImpl_.Enqueue(std::move(action));
+ return cpuInstant;
+}
+
+template <class TQueueImpl>
+TCpuInstant TInvokerQueue<TQueueImpl>::EnqueueCallbacks(
+ TMutableRange<TClosure> callbacks,
+ NProfiling::TTagId profilingTag,
+ TProfilerTagPtr profilerTag)
+{
+ auto cpuInstant = GetCpuInstant();
+
+ if (!Running_.load(std::memory_order::relaxed)) {
+ DrainProducer();
+ return cpuInstant;
+ }
+
+ std::vector<TEnqueuedAction> actions;
+ actions.reserve(callbacks.size());
+
+ for (auto& callback : callbacks) {
+ actions.push_back(MakeAction(std::move(callback), profilingTag, profilerTag, cpuInstant));
+ }
+
+ auto updateCounters = [&] (const TCountersPtr& counters) {
+ if (counters) {
+ counters->ActiveCallbacks += std::ssize(actions);
+ counters->EnqueuedCounter.Increment(std::ssize(actions));
+ }
+ };
+ updateCounters(Counters_[profilingTag]);
+ updateCounters(CumulativeCounters_);
+
+ QueueImpl_.Enqueue(actions);
+ return cpuInstant;
+}
+
+template <class TQueueImpl>
+TThreadId TInvokerQueue<TQueueImpl>::GetThreadId() const
+{
+ return ThreadId_;
+}
+
+template <class TQueueImpl>
+bool TInvokerQueue<TQueueImpl>::CheckAffinity(const IInvokerPtr& invoker) const
+{
+ return invoker.Get() == this;
+}
+
+template <class TQueueImpl>
+bool TInvokerQueue<TQueueImpl>::IsSerialized() const
+{
+ return QueueImpl_.HasSingleConsumer();
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::Shutdown()
+{
+ Running_.store(false, std::memory_order::relaxed);
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::DrainProducer()
+{
+ YT_VERIFY(!Running_.load(std::memory_order::relaxed));
+
+ QueueImpl_.DrainProducer();
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::DrainConsumer()
+{
+ YT_VERIFY(!Running_.load(std::memory_order::relaxed));
+
+ QueueImpl_.DrainConsumer();
+}
+
+template <class TQueueImpl>
+TClosure TInvokerQueue<TQueueImpl>::BeginExecute(TEnqueuedAction* action, typename TQueueImpl::TConsumerToken* token)
+{
+ YT_ASSERT(action && action->Finished);
+
+ if (!QueueImpl_.TryDequeue(action, token)) {
+ return {};
+ }
+
+ auto cpuInstant = GetCpuInstant();
+
+ action->StartedAt = cpuInstant;
+
+ auto waitTime = CpuDurationToDuration(action->StartedAt - action->EnqueuedAt);
+
+ auto updateCounters = [&] (const TCountersPtr& counters) {
+ if (counters) {
+ counters->DequeuedCounter.Increment();
+ counters->WaitTimer.Record(waitTime);
+ }
+ };
+ updateCounters(Counters_[action->ProfilingTag]);
+ updateCounters(CumulativeCounters_);
+
+ if (const auto& profilerTag = action->ProfilerTag) {
+ CpuProfilerTagGuard = TCpuProfilerTagGuard(profilerTag);
+ } else {
+ CpuProfilerTagGuard = {};
+ }
+
+ SetCurrentInvoker(GetProfilingTagSettingInvoker(action->ProfilingTag));
+
+ return std::move(action->Callback);
+}
+
+template <class TQueueImpl>
+void TInvokerQueue<TQueueImpl>::EndExecute(TEnqueuedAction* action)
+{
+ CpuProfilerTagGuard = TCpuProfilerTagGuard{};
+ SetCurrentInvoker(nullptr);
+
+ YT_ASSERT(action);
+
+ if (action->Finished) {
+ return;
+ }
+
+ auto cpuInstant = GetCpuInstant();
+ action->FinishedAt = cpuInstant;
+ action->Finished = true;
+
+ auto timeFromStart = CpuDurationToDuration(action->FinishedAt - action->StartedAt);
+ auto timeFromEnqueue = CpuDurationToDuration(action->FinishedAt - action->EnqueuedAt);
+
+ auto updateCounters = [&] (const TCountersPtr& counters) {
+ if (counters) {
+ counters->ExecTimer.Record(timeFromStart);
+ counters->CumulativeTimeCounter.Add(timeFromStart);
+ counters->TotalTimer.Record(timeFromEnqueue);
+ counters->ActiveCallbacks -= 1;
+ }
+ };
+ updateCounters(Counters_[action->ProfilingTag]);
+ updateCounters(CumulativeCounters_);
+}
+
+template <class TQueueImpl>
+typename TQueueImpl::TConsumerToken TInvokerQueue<TQueueImpl>::MakeConsumerToken()
+{
+ return QueueImpl_.MakeConsumerToken();
+}
+
+template <class TQueueImpl>
+bool TInvokerQueue<TQueueImpl>::IsEmpty() const
+{
+ return QueueImpl_.IsEmpty();
+}
+
+template <class TQueueImpl>
+bool TInvokerQueue<TQueueImpl>::IsRunning() const
+{
+ return Running_.load(std::memory_order::relaxed);
+}
+
+template <class TQueueImpl>
+IInvoker* TInvokerQueue<TQueueImpl>::GetProfilingTagSettingInvoker(int profilingTag)
+{
+ if (ProfilingTagSettingInvokers_.empty()) {
+ // Fast path.
+ YT_ASSERT(profilingTag == 0);
+ return this;
+ } else {
+ YT_ASSERT(0 <= profilingTag && profilingTag < std::ssize(Counters_));
+ return ProfilingTagSettingInvokers_[profilingTag].Get();
+ }
+}
+
+template <class TQueueImpl>
+typename TInvokerQueue<TQueueImpl>::TCountersPtr TInvokerQueue<TQueueImpl>::CreateCounters(const TTagSet& tagSet)
+{
+ auto profiler = TProfiler("/action_queue").WithTags(tagSet).WithHot();
+
+ auto counters = std::make_unique<TCounters>();
+ counters->EnqueuedCounter = profiler.Counter("/enqueued");
+ counters->DequeuedCounter = profiler.Counter("/dequeued");
+ counters->WaitTimer = profiler.Timer("/time/wait");
+ counters->ExecTimer = profiler.Timer("/time/exec");
+ counters->CumulativeTimeCounter = profiler.TimeCounter("/time/cumulative");
+ counters->TotalTimer = profiler.Timer("/time/total");
+
+ profiler.AddFuncGauge("/size", MakeStrong(this), [counters = counters.get()] {
+ return counters->ActiveCallbacks.load();
+ });
+
+ return counters;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template class TInvokerQueue<TMpmcQueueImpl>;
+template class TInvokerQueue<TMpscQueueImpl>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/invoker_queue.h b/yt/yt/core/concurrency/invoker_queue.h
new file mode 100644
index 0000000000..5b39571236
--- /dev/null
+++ b/yt/yt/core/concurrency/invoker_queue.h
@@ -0,0 +1,214 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/library/ytprof/api/api.h>
+
+#include <yt/yt/core/concurrency/moody_camel_concurrent_queue.h>
+
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/misc/mpsc_queue.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+
+#include <atomic>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEnqueuedAction
+{
+ bool Finished = true;
+ NProfiling::TCpuInstant EnqueuedAt = 0;
+ NProfiling::TCpuInstant StartedAt = 0;
+ NProfiling::TCpuInstant FinishedAt = 0;
+ TClosure Callback;
+ int ProfilingTag = 0;
+ NYTProf::TProfilerTagPtr ProfilerTag;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMpmcQueueImpl
+{
+public:
+ using TConsumerToken = std::array<moodycamel::ConsumerToken, 2>;
+
+ void Enqueue(TEnqueuedAction&& action);
+ void Enqueue(TMutableRange<TEnqueuedAction> actions);
+ bool TryDequeue(TEnqueuedAction* action, TConsumerToken* token = nullptr);
+
+ void DrainProducer();
+ void DrainConsumer();
+
+ TConsumerToken MakeConsumerToken();
+
+ bool IsEmpty() const;
+
+ bool HasSingleConsumer() const;
+
+private:
+ using TBucket = moodycamel::ConcurrentQueue<TEnqueuedAction>;
+ std::array<TBucket, 2> Buckets_;
+
+ alignas(CacheLineSize) std::atomic<int> Size_ = 0;
+
+ // Bit 0: producer bucket index
+ // Bit 1: consumer bucket index
+ // Bits 2..63: epoch
+ //
+ // Transitions:
+ // *---------------------------*
+ // | |
+ // \|/ |
+ // 0=00 (produce to B0, consume from B0)<---*---*
+ // | | |
+ // | epoch changes | |
+ // \|/ | |
+ // 1=01 (produce to B1, consume from B0) | | dequeue spin limit exceeded
+ // | | |
+ // | B0 is exhausted | |
+ // \|/ | |
+ // 3=11 (produce to B1, consume from B1)<---*---*
+ // | |
+ // | epoch changes |
+ // \|/ |
+ // 2=10 (produce to B0, consume from B1) |
+ // | |
+ // | B1 is exhausted |
+ // | |
+ // *---------------------------*
+ alignas(CacheLineSize) std::atomic<ui64> BucketSelector_ = 0;
+
+ template <class T>
+ void DoEnqueue(TCpuInstant instant, T&& func);
+ void EnqueueTo(TBucket* bucket, TEnqueuedAction&& action);
+ void EnqueueTo(TBucket* bucket, TMutableRange<TEnqueuedAction> actions);
+
+ static ui64 EpochFromInstant(TCpuInstant instant);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMpscQueueImpl
+{
+public:
+ using TConsumerToken = std::monostate;
+
+ void Enqueue(TEnqueuedAction&& action);
+ void Enqueue(TMutableRange<TEnqueuedAction> actions);
+ bool TryDequeue(TEnqueuedAction* action, TConsumerToken* token = nullptr);
+
+ void DrainProducer();
+ void DrainConsumer();
+
+ TConsumerToken MakeConsumerToken();
+
+ bool IsEmpty() const;
+
+ bool HasSingleConsumer() const;
+
+private:
+ TMpscQueue<TEnqueuedAction> Queue_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+class TInvokerQueue
+ : public IInvoker
+{
+public:
+ TInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const NProfiling::TTagSet& counterTagSet);
+
+ TInvokerQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const std::vector<NProfiling::TTagSet>& counterTagSets,
+ const std::vector<NYTProf::TProfilerTagPtr>& profilerTags,
+ const NProfiling::TTagSet& cumulativeCounterTagSet);
+
+ void SetThreadId(NThreading::TThreadId threadId);
+
+ void Invoke(TClosure callback) override;
+
+ void Invoke(TMutableRange<TClosure> callbacks) override;
+
+ void Invoke(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ NYTProf::TProfilerTagPtr profilerTag);
+
+ TEnqueuedAction MakeAction(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ NYTProf::TProfilerTagPtr profilerTag,
+ TCpuInstant cpuInstant);
+
+ TCpuInstant EnqueueCallback(
+ TClosure callback,
+ NProfiling::TTagId profilingTag,
+ NYTProf::TProfilerTagPtr profilerTag);
+
+ TCpuInstant EnqueueCallbacks(
+ TMutableRange<TClosure> callbacks,
+ NProfiling::TTagId profilingTag = 0,
+ NYTProf::TProfilerTagPtr profilerTag = nullptr);
+
+ NThreading::TThreadId GetThreadId() const override;
+ bool CheckAffinity(const IInvokerPtr& invoker) const override;
+ bool IsSerialized() const override;
+
+ void Shutdown();
+
+ void DrainProducer();
+ void DrainConsumer();
+
+ TClosure BeginExecute(TEnqueuedAction* action, typename TQueueImpl::TConsumerToken* token = nullptr);
+ void EndExecute(TEnqueuedAction* action);
+
+ typename TQueueImpl::TConsumerToken MakeConsumerToken();
+
+ bool IsEmpty() const;
+ bool IsRunning() const;
+
+ IInvoker* GetProfilingTagSettingInvoker(int profilingTag);
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_;
+
+ TQueueImpl QueueImpl_;
+
+ NThreading::TThreadId ThreadId_ = NThreading::InvalidThreadId;
+ std::atomic<bool> Running_ = true;
+
+ struct TCounters
+ {
+ NProfiling::TCounter EnqueuedCounter;
+ NProfiling::TCounter DequeuedCounter;
+ NProfiling::TEventTimer WaitTimer;
+ NProfiling::TEventTimer ExecTimer;
+ NProfiling::TTimeCounter CumulativeTimeCounter;
+ NProfiling::TEventTimer TotalTimer;
+ std::atomic<int> ActiveCallbacks = 0;
+ };
+ using TCountersPtr = std::unique_ptr<TCounters>;
+
+ std::vector<TCountersPtr> Counters_;
+ TCountersPtr CumulativeCounters_;
+
+ std::vector<IInvokerPtr> ProfilingTagSettingInvokers_;
+
+ TCountersPtr CreateCounters(const NProfiling::TTagSet& tagSet);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/lease_manager.cpp b/yt/yt/core/concurrency/lease_manager.cpp
new file mode 100644
index 0000000000..a5db4a0f85
--- /dev/null
+++ b/yt/yt/core/concurrency/lease_manager.cpp
@@ -0,0 +1,145 @@
+#include "lease_manager.h"
+#include "delayed_executor.h"
+#include "thread_affinity.h"
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLease NullLease;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLeaseEntry
+ : public TRefCounted
+{
+ bool IsValid = true;
+ TDuration Timeout;
+ TClosure OnExpired;
+ NConcurrency::TDelayedExecutorCookie Cookie;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock);
+
+ TLeaseEntry(TDuration timeout, TClosure onExpired)
+ : Timeout(timeout)
+ , OnExpired(std::move(onExpired))
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TLeaseEntry)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLeaseManager::TImpl
+ : private TNonCopyable
+{
+public:
+ static TLease CreateLease(TDuration timeout, TClosure onExpired)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_ASSERT(onExpired);
+
+ auto lease = New<TLeaseEntry>(timeout, std::move(onExpired));
+ auto guard = Guard(lease->SpinLock);
+ lease->Cookie = TDelayedExecutor::Submit(
+ BIND(&TImpl::OnLeaseExpired, lease),
+ timeout);
+ return lease;
+ }
+
+ static bool RenewLease(TLease lease, std::optional<TDuration> timeout)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_ASSERT(lease);
+
+ auto guard = Guard(lease->SpinLock);
+
+ if (!lease->IsValid) {
+ return false;
+ }
+
+ if (timeout) {
+ lease->Timeout = *timeout;
+ }
+
+ TDelayedExecutor::Cancel(lease->Cookie);
+ lease->Cookie = TDelayedExecutor::Submit(
+ BIND(&TImpl::OnLeaseExpired, lease),
+ lease->Timeout);
+
+ return true;
+ }
+
+ static bool CloseLease(TLease lease)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (!lease) {
+ return false;
+ }
+
+ auto guard = Guard(lease->SpinLock);
+
+ if (!lease->IsValid) {
+ return false;
+ }
+
+ InvalidateLease(lease);
+ return true;
+ }
+
+private:
+ static void OnLeaseExpired(TLease lease, bool aborted)
+ {
+ if (aborted) {
+ return;
+ }
+
+ auto guard = Guard(lease->SpinLock);
+
+ if (!lease->IsValid) {
+ return;
+ }
+
+ auto onExpired = lease->OnExpired;
+ InvalidateLease(lease);
+ guard.Release();
+
+ onExpired();
+ }
+
+ static void InvalidateLease(TLease lease)
+ {
+ VERIFY_SPINLOCK_AFFINITY(lease->SpinLock);
+
+ TDelayedExecutor::CancelAndClear(lease->Cookie);
+ lease->IsValid = false;
+ lease->OnExpired.Reset();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLease TLeaseManager::CreateLease(TDuration timeout, TClosure onExpired)
+{
+ return TImpl::CreateLease(timeout, std::move(onExpired));
+}
+
+bool TLeaseManager::RenewLease(TLease lease, std::optional<TDuration> timeout)
+{
+ return TImpl::RenewLease(std::move(lease), timeout);
+}
+
+bool TLeaseManager::CloseLease(TLease lease)
+{
+ return TImpl::CloseLease(std::move(lease));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NConcurrency::NYT
+
+
diff --git a/yt/yt/core/concurrency/lease_manager.h b/yt/yt/core/concurrency/lease_manager.h
new file mode 100644
index 0000000000..8a0a1f6c03
--- /dev/null
+++ b/yt/yt/core/concurrency/lease_manager.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <optional>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Manages lease expiration.
+/*!
+ * A lease is an opaque entity.
+ * It is assigned a timeout and an expiration handler upon creation.
+ * The lease must be continuously renewed by calling #Renew.
+ * If #Renew is not called during the timeout, the lease expires and the handler is invoked.
+ * Closing the lease releases resources and cancels expiration notification.
+ */
+class TLeaseManager
+ : public TNonCopyable
+{
+public:
+ TLeaseManager() = delete;
+
+ //! Creates a new lease with a given timeout and a given expiration callback.
+ static TLease CreateLease(TDuration timeout, TClosure onExpired);
+
+ //! Renews the lease.
+ /*!
+ * \param lease A lease to renew.
+ * \param timeout A new timeout (if |std::nullopt| then the old one is preserved).
+ * \returns True iff the lease is still valid (i.e. not expired).
+ */
+ static bool RenewLease(TLease lease, std::optional<TDuration> timeout = std::nullopt);
+
+ //! Closes the lease.
+ /*!
+ * \returns True iff the lease is still valid (i.e. not expired).
+ */
+ static bool CloseLease(TLease lease);
+
+private:
+ class TImpl;
+};
+
+//! An invalid lease.
+extern const TLease NullLease;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/moody_camel_concurrent_queue.h b/yt/yt/core/concurrency/moody_camel_concurrent_queue.h
new file mode 100644
index 0000000000..3b43aaad78
--- /dev/null
+++ b/yt/yt/core/concurrency/moody_camel_concurrent_queue.h
@@ -0,0 +1,3742 @@
+// Provides a C++11 implementation of a multi-producer, multi-consumer lock-free queue.
+// An overview, including benchmark results, is provided here:
+// http://moodycamel.com/blog/2014/a-fast-general-purpose-lock-free-queue-for-c++
+// The full design is also described in excruciating detail at:
+// http://moodycamel.com/blog/2014/detailed-design-of-a-lock-free-queue
+
+// Simplified BSD license:
+// Copyright (c) 2013-2020, Cameron Desrochers.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// - Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+// - Redistributions in binary form must reproduce the above copyright notice, this list of
+// conditions and the following disclaimer in the documentation and/or other materials
+// provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Also dual-licensed under the Boost Software License (see LICENSE.md)
+
+#pragma once
+
+#if defined(__GNUC__)
+// Disable -Wconversion warnings (spuriously triggered when Traits::size_t and
+// Traits::index_t are set to < 32 bits, causing integer promotion, causing warnings
+// upon assigning any computed values)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+
+#ifdef MCDBGQ_USE_RELACY
+#pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
+#endif
+#endif
+
+#if defined(_MSC_VER) && (!defined(_HAS_CXX17) || !_HAS_CXX17)
+// VS2019 with /W4 warns about constant conditional expressions but unless /std=c++17 or higher
+// does not support `if constexpr`, so we have no choice but to simply disable the warning
+#pragma warning(push)
+#pragma warning(disable: 4127) // conditional expression is constant
+#endif
+
+#if defined(__APPLE__)
+#include "TargetConditionals.h"
+#endif
+
+#ifdef MCDBGQ_USE_RELACY
+#include "relacy/relacy_std.hpp" // Y_IGNORE
+#include "relacy_shims.h" // Y_IGNORE
+// We only use malloc/free anyway, and the delete macro messes up `= delete` method declarations.
+// We'll override the default trait malloc ourselves without a macro.
+#undef new
+#undef delete
+#undef malloc
+#undef free
+#else
+#include <atomic> // Requires C++11. Sorry VS2010.
+#include <cassert>
+#endif
+#include <cstddef> // for max_align_t
+#include <cstdint>
+#include <cstdlib>
+#include <type_traits>
+#include <algorithm>
+#include <utility>
+#include <limits>
+#include <climits> // for CHAR_BIT
+#include <array>
+#include <thread> // partly for __WINPTHREADS_VERSION if on MinGW-w64 w/ POSIX threading
+
+// Platform-specific definitions of a numeric thread ID type and an invalid value
+namespace moodycamel { namespace details {
+ template<typename thread_id_t> struct thread_id_converter {
+ typedef thread_id_t thread_id_numeric_size_t;
+ typedef thread_id_t thread_id_hash_t;
+ static thread_id_hash_t prehash(thread_id_t const& x) { return x; }
+ };
+} }
+#if defined(MCDBGQ_USE_RELACY)
+namespace moodycamel { namespace details {
+ typedef std::uint32_t thread_id_t;
+ static const thread_id_t invalid_thread_id = 0xFFFFFFFFU;
+ static const thread_id_t invalid_thread_id2 = 0xFFFFFFFEU;
+ static inline thread_id_t thread_id() { return rl::thread_index(); }
+} }
+#elif defined(_WIN32) || defined(__WINDOWS__) || defined(__WIN32__)
+// No sense pulling in windows.h in a header, we'll manually declare the function
+// we use and rely on backwards-compatibility for this not to break
+extern "C" __declspec(dllimport) unsigned long __stdcall GetCurrentThreadId(void);
+namespace moodycamel { namespace details {
+ static_assert(sizeof(unsigned long) == sizeof(std::uint32_t), "Expected size of unsigned long to be 32 bits on Windows");
+ typedef std::uint32_t thread_id_t;
+ static const thread_id_t invalid_thread_id = 0; // See http://blogs.msdn.com/b/oldnewthing/archive/2004/02/23/78395.aspx
+ static const thread_id_t invalid_thread_id2 = 0xFFFFFFFFU; // Not technically guaranteed to be invalid, but is never used in practice. Note that all Win32 thread IDs are presently multiples of 4.
+ static inline thread_id_t thread_id() { return static_cast<thread_id_t>(::GetCurrentThreadId()); }
+} }
+#elif defined(__arm__) || defined(_M_ARM) || defined(__aarch64__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
+namespace moodycamel { namespace details {
+ static_assert(sizeof(std::thread::id) == 4 || sizeof(std::thread::id) == 8, "std::thread::id is expected to be either 4 or 8 bytes");
+
+ typedef std::thread::id thread_id_t;
+ static const thread_id_t invalid_thread_id; // Default ctor creates invalid ID
+
+ // Note we don't define a invalid_thread_id2 since std::thread::id doesn't have one; it's
+ // only used if MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED is defined anyway, which it won't
+ // be.
+ static inline thread_id_t thread_id() { return std::this_thread::get_id(); }
+
+ template<std::size_t> struct thread_id_size { };
+ template<> struct thread_id_size<4> { typedef std::uint32_t numeric_t; };
+ template<> struct thread_id_size<8> { typedef std::uint64_t numeric_t; };
+
+ template<> struct thread_id_converter<thread_id_t> {
+ typedef thread_id_size<sizeof(thread_id_t)>::numeric_t thread_id_numeric_size_t;
+#ifndef __APPLE__
+ typedef std::size_t thread_id_hash_t;
+#else
+ typedef thread_id_numeric_size_t thread_id_hash_t;
+#endif
+
+ static thread_id_hash_t prehash(thread_id_t const& x)
+ {
+#ifndef __APPLE__
+ return std::hash<std::thread::id>()(x);
+#else
+ return *reinterpret_cast<thread_id_hash_t const*>(&x);
+#endif
+ }
+ };
+} }
+#else
+// Use a nice trick from this answer: http://stackoverflow.com/a/8438730/21475
+// In order to get a numeric thread ID in a platform-independent way, we use a thread-local
+// static variable's address as a thread identifier :-)
+#if defined(__GNUC__) || defined(__INTEL_COMPILER)
+#define MOODYCAMEL_THREADLOCAL __thread
+#elif defined(_MSC_VER)
+#define MOODYCAMEL_THREADLOCAL __declspec(thread)
+#else
+// Assume C++11 compliant compiler
+#define MOODYCAMEL_THREADLOCAL thread_local
+#endif
+namespace moodycamel { namespace details {
+ typedef std::uintptr_t thread_id_t;
+ static const thread_id_t invalid_thread_id = 0; // Address can't be nullptr
+ static const thread_id_t invalid_thread_id2 = 1; // Member accesses off a null pointer are also generally invalid. Plus it's not aligned.
+ inline thread_id_t thread_id() { static MOODYCAMEL_THREADLOCAL int x; return reinterpret_cast<thread_id_t>(&x); }
+} }
+#endif
+
+// Constexpr if
+#ifndef MOODYCAMEL_CONSTEXPR_IF
+#if (defined(_MSC_VER) && defined(_HAS_CXX17) && _HAS_CXX17) || __cplusplus > 201402L
+#define MOODYCAMEL_CONSTEXPR_IF if constexpr
+#define MOODYCAMEL_MAYBE_UNUSED [[maybe_unused]]
+#else
+#define MOODYCAMEL_CONSTEXPR_IF if
+#define MOODYCAMEL_MAYBE_UNUSED
+#endif
+#endif
+
+// Exceptions
+#ifndef MOODYCAMEL_EXCEPTIONS_ENABLED
+#if (defined(_MSC_VER) && defined(_CPPUNWIND)) || (defined(__GNUC__) && defined(__EXCEPTIONS)) || (!defined(_MSC_VER) && !defined(__GNUC__))
+#define MOODYCAMEL_EXCEPTIONS_ENABLED
+#endif
+#endif
+#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED
+#define MOODYCAMEL_TRY try
+#define MOODYCAMEL_CATCH(...) catch(__VA_ARGS__)
+#define MOODYCAMEL_RETHROW throw
+#define MOODYCAMEL_THROW(expr) throw (expr)
+#else
+#define MOODYCAMEL_TRY MOODYCAMEL_CONSTEXPR_IF (true)
+#define MOODYCAMEL_CATCH(...) else MOODYCAMEL_CONSTEXPR_IF (false)
+#define MOODYCAMEL_RETHROW
+#define MOODYCAMEL_THROW(expr)
+#endif
+
+#ifndef MOODYCAMEL_NOEXCEPT
+#if !defined(MOODYCAMEL_EXCEPTIONS_ENABLED)
+#define MOODYCAMEL_NOEXCEPT
+#define MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr) true
+#define MOODYCAMEL_NOEXCEPT_ASSIGN(type, valueType, expr) true
+#elif defined(_MSC_VER) && defined(_NOEXCEPT) && _MSC_VER < 1800
+// VS2012's std::is_nothrow_[move_]constructible is broken and returns true when it shouldn't :-(
+// We have to assume *all* non-trivial constructors may throw on VS2012!
+#define MOODYCAMEL_NOEXCEPT _NOEXCEPT
+#define MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr) (std::is_rvalue_reference<valueType>::value && std::is_move_constructible<type>::value ? std::is_trivially_move_constructible<type>::value : std::is_trivially_copy_constructible<type>::value)
+#define MOODYCAMEL_NOEXCEPT_ASSIGN(type, valueType, expr) ((std::is_rvalue_reference<valueType>::value && std::is_move_assignable<type>::value ? std::is_trivially_move_assignable<type>::value || std::is_nothrow_move_assignable<type>::value : std::is_trivially_copy_assignable<type>::value || std::is_nothrow_copy_assignable<type>::value) && MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr))
+#elif defined(_MSC_VER) && defined(_NOEXCEPT) && _MSC_VER < 1900
+#define MOODYCAMEL_NOEXCEPT _NOEXCEPT
+#define MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr) (std::is_rvalue_reference<valueType>::value && std::is_move_constructible<type>::value ? std::is_trivially_move_constructible<type>::value || std::is_nothrow_move_constructible<type>::value : std::is_trivially_copy_constructible<type>::value || std::is_nothrow_copy_constructible<type>::value)
+#define MOODYCAMEL_NOEXCEPT_ASSIGN(type, valueType, expr) ((std::is_rvalue_reference<valueType>::value && std::is_move_assignable<type>::value ? std::is_trivially_move_assignable<type>::value || std::is_nothrow_move_assignable<type>::value : std::is_trivially_copy_assignable<type>::value || std::is_nothrow_copy_assignable<type>::value) && MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr))
+#else
+#define MOODYCAMEL_NOEXCEPT noexcept
+#define MOODYCAMEL_NOEXCEPT_CTOR(type, valueType, expr) noexcept(expr)
+#define MOODYCAMEL_NOEXCEPT_ASSIGN(type, valueType, expr) noexcept(expr)
+#endif
+#endif
+
+#ifndef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+#ifdef MCDBGQ_USE_RELACY
+#define MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+#else
+// VS2013 doesn't support `thread_local`, and MinGW-w64 w/ POSIX threading has a crippling bug: http://sourceforge.net/p/mingw-w64/bugs/445
+// g++ <=4.7 doesn't support thread_local either.
+// Finally, iOS/ARM doesn't have support for it either, and g++/ARM allows it to compile but it's unconfirmed to actually work
+#if (!defined(_MSC_VER) || _MSC_VER >= 1900) && (!defined(__MINGW32__) && !defined(__MINGW64__) || !defined(__WINPTHREADS_VERSION)) && (!defined(__GNUC__) || __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) && (!defined(__APPLE__) || !TARGET_OS_IPHONE) && !defined(__arm__) && !defined(_M_ARM) && !defined(__aarch64__)
+// Assume `thread_local` is fully supported in all other C++11 compilers/platforms
+//#define MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED // always disabled for now since several users report having problems with it on
+#endif
+#endif
+#endif
+
+// VS2012 doesn't support deleted functions.
+// In this case, we declare the function normally but don't define it. A link error will be generated if the function is called.
+#ifndef MOODYCAMEL_DELETE_FUNCTION
+#if defined(_MSC_VER) && _MSC_VER < 1800
+#define MOODYCAMEL_DELETE_FUNCTION
+#else
+#define MOODYCAMEL_DELETE_FUNCTION = delete
+#endif
+#endif
+
+namespace moodycamel { namespace details {
+#ifndef MOODYCAMEL_ALIGNAS
+// VS2013 doesn't support alignas or alignof, and align() requires a constant literal
+#if defined(_MSC_VER) && _MSC_VER <= 1800
+#define MOODYCAMEL_ALIGNAS(alignment) __declspec(align(alignment))
+#define MOODYCAMEL_ALIGNOF(obj) __alignof(obj)
+#define MOODYCAMEL_ALIGNED_TYPE_LIKE(T, obj) typename details::Vs2013Aligned<std::alignment_of<obj>::value, T>::type
+ template<int Align, typename T> struct Vs2013Aligned { }; // default, unsupported alignment
+ template<typename T> struct Vs2013Aligned<1, T> { typedef __declspec(align(1)) T type; };
+ template<typename T> struct Vs2013Aligned<2, T> { typedef __declspec(align(2)) T type; };
+ template<typename T> struct Vs2013Aligned<4, T> { typedef __declspec(align(4)) T type; };
+ template<typename T> struct Vs2013Aligned<8, T> { typedef __declspec(align(8)) T type; };
+ template<typename T> struct Vs2013Aligned<16, T> { typedef __declspec(align(16)) T type; };
+ template<typename T> struct Vs2013Aligned<32, T> { typedef __declspec(align(32)) T type; };
+ template<typename T> struct Vs2013Aligned<64, T> { typedef __declspec(align(64)) T type; };
+ template<typename T> struct Vs2013Aligned<128, T> { typedef __declspec(align(128)) T type; };
+ template<typename T> struct Vs2013Aligned<256, T> { typedef __declspec(align(256)) T type; };
+#else
+ template<typename T> struct identity { typedef T type; };
+#define MOODYCAMEL_ALIGNAS(alignment) alignas(alignment)
+#define MOODYCAMEL_ALIGNOF(obj) alignof(obj)
+#define MOODYCAMEL_ALIGNED_TYPE_LIKE(T, obj) alignas(alignof(obj)) typename details::identity<T>::type
+#endif
+#endif
+} }
+
+
+// TSAN can false report races in lock-free code. To enable TSAN to be used from projects that use this one,
+// we can apply per-function compile-time suppression.
+// See https://clang.llvm.org/docs/ThreadSanitizer.html#has-feature-thread-sanitizer
+#define MOODYCAMEL_NO_TSAN
+#if defined(__has_feature)
+ #if __has_feature(thread_sanitizer)
+ #undef MOODYCAMEL_NO_TSAN
+ #define MOODYCAMEL_NO_TSAN __attribute__((no_sanitize("thread")))
+ #endif // TSAN
+#endif // TSAN
+
+// Compiler-specific likely/unlikely hints
+namespace moodycamel { namespace details {
+#if defined(__GNUC__)
+ static inline bool (likely)(bool x) { return __builtin_expect((x), true); }
+ static inline bool (unlikely)(bool x) { return __builtin_expect((x), false); }
+#else
+ static inline bool (likely)(bool x) { return x; }
+ static inline bool (unlikely)(bool x) { return x; }
+#endif
+} }
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+#include "internal/concurrentqueue_internal_debug.h" //Y_IGNORE
+#endif
+
+namespace moodycamel {
+namespace details {
+ template<typename T>
+ struct const_numeric_max {
+ static_assert(std::is_integral<T>::value, "const_numeric_max can only be used with integers");
+ static const T value = std::numeric_limits<T>::is_signed
+ ? (static_cast<T>(1) << (sizeof(T) * CHAR_BIT - 1)) - static_cast<T>(1)
+ : static_cast<T>(-1);
+ };
+
+#if defined(__GLIBCXX__)
+ typedef ::max_align_t std_max_align_t; // libstdc++ forgot to add it to std:: for a while
+#else
+ typedef std::max_align_t std_max_align_t; // Others (e.g. MSVC) insist it can *only* be accessed via std::
+#endif
+
+ // Some platforms have incorrectly set max_align_t to a type with <8 bytes alignment even while supporting
+ // 8-byte aligned scalar values (*cough* 32-bit iOS). Work around this with our own union. See issue #64.
+ typedef union {
+ std_max_align_t x;
+ long long y;
+ void* z;
+ } max_align_t;
+}
+
+// Default traits for the ConcurrentQueue. To change some of the
+// traits without re-implementing all of them, inherit from this
+// struct and shadow the declarations you wish to be different;
+// since the traits are used as a template type parameter, the
+// shadowed declarations will be used where defined, and the defaults
+// otherwise.
+struct ConcurrentQueueDefaultTraits
+{
+ // General-purpose size type. std::size_t is strongly recommended.
+ typedef std::size_t size_t;
+
+ // The type used for the enqueue and dequeue indices. Must be at least as
+ // large as size_t. Should be significantly larger than the number of elements
+ // you expect to hold at once, especially if you have a high turnover rate;
+ // for example, on 32-bit x86, if you expect to have over a hundred million
+ // elements or pump several million elements through your queue in a very
+ // short space of time, using a 32-bit type *may* trigger a race condition.
+ // A 64-bit int type is recommended in that case, and in practice will
+ // prevent a race condition no matter the usage of the queue. Note that
+ // whether the queue is lock-free with a 64-int type depends on the whether
+ // std::atomic<std::uint64_t> is lock-free, which is platform-specific.
+ typedef std::size_t index_t;
+
+ // Internally, all elements are enqueued and dequeued from multi-element
+ // blocks; this is the smallest controllable unit. If you expect few elements
+ // but many producers, a smaller block size should be favoured. For few producers
+ // and/or many elements, a larger block size is preferred. A sane default
+ // is provided. Must be a power of 2.
+ static const size_t BLOCK_SIZE = 32;
+
+ // For explicit producers (i.e. when using a producer token), the block is
+ // checked for being empty by iterating through a list of flags, one per element.
+ // For large block sizes, this is too inefficient, and switching to an atomic
+ // counter-based approach is faster. The switch is made for block sizes strictly
+ // larger than this threshold.
+ static const size_t EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD = 32;
+
+ // How many full blocks can be expected for a single explicit producer? This should
+ // reflect that number's maximum for optimal performance. Must be a power of 2.
+ static const size_t EXPLICIT_INITIAL_INDEX_SIZE = 32;
+
+ // How many full blocks can be expected for a single implicit producer? This should
+ // reflect that number's maximum for optimal performance. Must be a power of 2.
+ static const size_t IMPLICIT_INITIAL_INDEX_SIZE = 32;
+
+ // The initial size of the hash table mapping thread IDs to implicit producers.
+ // Note that the hash is resized every time it becomes half full.
+ // Must be a power of two, and either 0 or at least 1. If 0, implicit production
+ // (using the enqueue methods without an explicit producer token) is disabled.
+ static const size_t INITIAL_IMPLICIT_PRODUCER_HASH_SIZE = 32;
+
+ // Controls the number of items that an explicit consumer (i.e. one with a token)
+ // must consume before it causes all consumers to rotate and move on to the next
+ // internal queue.
+ static const std::uint32_t EXPLICIT_CONSUMER_CONSUMPTION_QUOTA_BEFORE_ROTATE = 256;
+
+ // The maximum number of elements (inclusive) that can be enqueued to a sub-queue.
+ // Enqueue operations that would cause this limit to be surpassed will fail. Note
+ // that this limit is enforced at the block level (for performance reasons), i.e.
+ // it's rounded up to the nearest block size.
+ static const size_t MAX_SUBQUEUE_SIZE = details::const_numeric_max<size_t>::value;
+
+ // The number of times to spin before sleeping when waiting on a semaphore.
+ // Recommended values are on the order of 1000-10000 unless the number of
+ // consumer threads exceeds the number of idle cores (in which case try 0-100).
+ // Only affects instances of the BlockingConcurrentQueue.
+ static const int MAX_SEMA_SPINS = 10000;
+
+
+#ifndef MCDBGQ_USE_RELACY
+ // Memory allocation can be customized if needed.
+ // malloc should return nullptr on failure, and handle alignment like std::malloc.
+#if defined(malloc) || defined(free)
+ // Gah, this is 2015, stop defining macros that break standard code already!
+ // Work around malloc/free being special macros:
+ static inline void* WORKAROUND_malloc(size_t size) { return malloc(size); }
+ static inline void WORKAROUND_free(void* ptr) { return free(ptr); }
+ static inline void* (malloc)(size_t size) { return WORKAROUND_malloc(size); }
+ static inline void (free)(void* ptr) { return WORKAROUND_free(ptr); }
+#else
+ static inline void* malloc(size_t size) { return std::malloc(size); }
+ static inline void free(void* ptr) { return std::free(ptr); }
+#endif
+#else
+ // Debug versions when running under the Relacy race detector (ignore
+ // these in user code)
+ static inline void* malloc(size_t size) { return rl::rl_malloc(size, $); }
+ static inline void free(void* ptr) { return rl::rl_free(ptr, $); }
+#endif
+};
+
+
+// When producing or consuming many elements, the most efficient way is to:
+// 1) Use one of the bulk-operation methods of the queue with a token
+// 2) Failing that, use the bulk-operation methods without a token
+// 3) Failing that, create a token and use that with the single-item methods
+// 4) Failing that, use the single-parameter methods of the queue
+// Having said that, don't create tokens willy-nilly -- ideally there should be
+// a maximum of one token per thread (of each kind).
+struct ProducerToken;
+struct ConsumerToken;
+
+template<typename T, typename Traits> class ConcurrentQueue;
+template<typename T, typename Traits> class BlockingConcurrentQueue;
+class ConcurrentQueueTests;
+
+
+namespace details
+{
+ struct ConcurrentQueueProducerTypelessBase
+ {
+ ConcurrentQueueProducerTypelessBase* next;
+ std::atomic<bool> inactive;
+ ProducerToken* token;
+
+ ConcurrentQueueProducerTypelessBase()
+ : next(nullptr), inactive(false), token(nullptr)
+ {
+ }
+ };
+
+ template<bool use32> struct _hash_32_or_64 {
+ static inline std::uint32_t hash(std::uint32_t h)
+ {
+ // MurmurHash3 finalizer -- see https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp
+ // Since the thread ID is already unique, all we really want to do is propagate that
+ // uniqueness evenly across all the bits, so that we can use a subset of the bits while
+ // reducing collisions significantly
+ h ^= h >> 16;
+ h *= 0x85ebca6b;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35;
+ return h ^ (h >> 16);
+ }
+ };
+ template<> struct _hash_32_or_64<1> {
+ static inline std::uint64_t hash(std::uint64_t h)
+ {
+ h ^= h >> 33;
+ h *= 0xff51afd7ed558ccd;
+ h ^= h >> 33;
+ h *= 0xc4ceb9fe1a85ec53;
+ return h ^ (h >> 33);
+ }
+ };
+ template<std::size_t size> struct hash_32_or_64 : public _hash_32_or_64<(size > 4)> { };
+
+ static inline size_t hash_thread_id(thread_id_t id)
+ {
+ static_assert(sizeof(thread_id_t) <= 8, "Expected a platform where thread IDs are at most 64-bit values");
+ return static_cast<size_t>(hash_32_or_64<sizeof(thread_id_converter<thread_id_t>::thread_id_hash_t)>::hash(
+ thread_id_converter<thread_id_t>::prehash(id)));
+ }
+
+ template<typename T>
+ static inline bool circular_less_than(T a, T b)
+ {
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable: 4554)
+#endif
+ static_assert(std::is_integral<T>::value && !std::numeric_limits<T>::is_signed, "circular_less_than is intended to be used only with unsigned integer types");
+ return static_cast<T>(a - b) > static_cast<T>(static_cast<T>(1) << static_cast<T>(sizeof(T) * CHAR_BIT - 1));
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+ }
+
+ template<typename U>
+ static inline char* align_for(char* ptr)
+ {
+ const std::size_t alignment = std::alignment_of<U>::value;
+ return ptr + (alignment - (reinterpret_cast<std::uintptr_t>(ptr) % alignment)) % alignment;
+ }
+
+ template<typename T>
+ static inline T ceil_to_pow_2(T x)
+ {
+ static_assert(std::is_integral<T>::value && !std::numeric_limits<T>::is_signed, "ceil_to_pow_2 is intended to be used only with unsigned integer types");
+
+ // Adapted from http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
+ --x;
+ x |= x >> 1;
+ x |= x >> 2;
+ x |= x >> 4;
+ for (std::size_t i = 1; i < sizeof(T); i <<= 1) {
+ x |= x >> (i << 3);
+ }
+ ++x;
+ return x;
+ }
+
+ template<typename T>
+ static inline void swap_relaxed(std::atomic<T>& left, std::atomic<T>& right)
+ {
+ T temp = std::move(left.load(std::memory_order::relaxed));
+ left.store(std::move(right.load(std::memory_order::relaxed)), std::memory_order::relaxed);
+ right.store(std::move(temp), std::memory_order::relaxed);
+ }
+
+ template<typename T>
+ static inline T const& nomove(T const& x)
+ {
+ return x;
+ }
+
+ template<bool Enable>
+ struct nomove_if
+ {
+ template<typename T>
+ static inline T const& eval(T const& x)
+ {
+ return x;
+ }
+ };
+
+ template<>
+ struct nomove_if<false>
+ {
+ template<typename U>
+ static inline auto eval(U&& x)
+ -> decltype(std::forward<U>(x))
+ {
+ return std::forward<U>(x);
+ }
+ };
+
+ template<typename It>
+ static inline auto deref_noexcept(It& it) MOODYCAMEL_NOEXCEPT -> decltype(*it)
+ {
+ return *it;
+ }
+
+#if defined(__clang__) || !defined(__GNUC__) || __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)
+ template<typename T> struct is_trivially_destructible : std::is_trivially_destructible<T> { };
+#else
+ template<typename T> struct is_trivially_destructible : std::has_trivial_destructor<T> { };
+#endif
+
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+#ifdef MCDBGQ_USE_RELACY
+ typedef RelacyThreadExitListener ThreadExitListener;
+ typedef RelacyThreadExitNotifier ThreadExitNotifier;
+#else
+ struct ThreadExitListener
+ {
+ typedef void (*callback_t)(void*);
+ callback_t callback;
+ void* userData;
+
+ ThreadExitListener* next; // reserved for use by the ThreadExitNotifier
+ };
+
+
+ class ThreadExitNotifier
+ {
+ public:
+ static void subscribe(ThreadExitListener* listener)
+ {
+ auto& tlsInst = instance();
+ listener->next = tlsInst.tail;
+ tlsInst.tail = listener;
+ }
+
+ static void unsubscribe(ThreadExitListener* listener)
+ {
+ auto& tlsInst = instance();
+ ThreadExitListener** prev = &tlsInst.tail;
+ for (auto ptr = tlsInst.tail; ptr != nullptr; ptr = ptr->next) {
+ if (ptr == listener) {
+ *prev = ptr->next;
+ break;
+ }
+ prev = &ptr->next;
+ }
+ }
+
+ private:
+ ThreadExitNotifier() : tail(nullptr) { }
+ ThreadExitNotifier(ThreadExitNotifier const&) MOODYCAMEL_DELETE_FUNCTION;
+ ThreadExitNotifier& operator=(ThreadExitNotifier const&) MOODYCAMEL_DELETE_FUNCTION;
+
+ ~ThreadExitNotifier()
+ {
+ // This thread is about to exit, let everyone know!
+ assert(this == &instance() && "If this assert fails, you likely have a buggy compiler! Change the preprocessor conditions such that MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED is no longer defined.");
+ for (auto ptr = tail; ptr != nullptr; ptr = ptr->next) {
+ ptr->callback(ptr->userData);
+ }
+ }
+
+ // Thread-local
+ static inline ThreadExitNotifier& instance()
+ {
+ static thread_local ThreadExitNotifier notifier;
+ return notifier;
+ }
+
+ private:
+ ThreadExitListener* tail;
+ };
+#endif
+#endif
+
+ template<typename T> struct static_is_lock_free_num { enum { value = 0 }; };
+ template<> struct static_is_lock_free_num<signed char> { enum { value = ATOMIC_CHAR_LOCK_FREE }; };
+ template<> struct static_is_lock_free_num<short> { enum { value = ATOMIC_SHORT_LOCK_FREE }; };
+ template<> struct static_is_lock_free_num<int> { enum { value = ATOMIC_INT_LOCK_FREE }; };
+ template<> struct static_is_lock_free_num<long> { enum { value = ATOMIC_LONG_LOCK_FREE }; };
+ template<> struct static_is_lock_free_num<long long> { enum { value = ATOMIC_LLONG_LOCK_FREE }; };
+ template<typename T> struct static_is_lock_free : static_is_lock_free_num<typename std::make_signed<T>::type> { };
+ template<> struct static_is_lock_free<bool> { enum { value = ATOMIC_BOOL_LOCK_FREE }; };
+ template<typename U> struct static_is_lock_free<U*> { enum { value = ATOMIC_POINTER_LOCK_FREE }; };
+}
+
+
+struct ProducerToken
+{
+ template<typename T, typename Traits>
+ explicit ProducerToken(ConcurrentQueue<T, Traits>& queue);
+
+ template<typename T, typename Traits>
+ explicit ProducerToken(BlockingConcurrentQueue<T, Traits>& queue);
+
+ ProducerToken(ProducerToken&& other) MOODYCAMEL_NOEXCEPT
+ : producer(other.producer)
+ {
+ other.producer = nullptr;
+ if (producer != nullptr) {
+ producer->token = this;
+ }
+ }
+
+ inline ProducerToken& operator=(ProducerToken&& other) MOODYCAMEL_NOEXCEPT
+ {
+ swap(other);
+ return *this;
+ }
+
+ void swap(ProducerToken& other) MOODYCAMEL_NOEXCEPT
+ {
+ std::swap(producer, other.producer);
+ if (producer != nullptr) {
+ producer->token = this;
+ }
+ if (other.producer != nullptr) {
+ other.producer->token = &other;
+ }
+ }
+
+ // A token is always valid unless:
+ // 1) Memory allocation failed during construction
+ // 2) It was moved via the move constructor
+ // (Note: assignment does a swap, leaving both potentially valid)
+ // 3) The associated queue was destroyed
+ // Note that if valid() returns true, that only indicates
+ // that the token is valid for use with a specific queue,
+ // but not which one; that's up to the user to track.
+ inline bool valid() const { return producer != nullptr; }
+
+ ~ProducerToken()
+ {
+ if (producer != nullptr) {
+ producer->token = nullptr;
+ producer->inactive.store(true, std::memory_order::release);
+ }
+ }
+
+ // Disable copying and assignment
+ ProducerToken(ProducerToken const&) MOODYCAMEL_DELETE_FUNCTION;
+ ProducerToken& operator=(ProducerToken const&) MOODYCAMEL_DELETE_FUNCTION;
+
+private:
+ template<typename T, typename Traits> friend class ConcurrentQueue;
+ friend class ConcurrentQueueTests;
+
+protected:
+ details::ConcurrentQueueProducerTypelessBase* producer;
+};
+
+
+struct ConsumerToken
+{
+ template<typename T, typename Traits>
+ explicit ConsumerToken(ConcurrentQueue<T, Traits>& q);
+
+ template<typename T, typename Traits>
+ explicit ConsumerToken(BlockingConcurrentQueue<T, Traits>& q);
+
+ ConsumerToken(ConsumerToken&& other) MOODYCAMEL_NOEXCEPT
+ : initialOffset(other.initialOffset), lastKnownGlobalOffset(other.lastKnownGlobalOffset), itemsConsumedFromCurrent(other.itemsConsumedFromCurrent), currentProducer(other.currentProducer), desiredProducer(other.desiredProducer)
+ {
+ }
+
+ inline ConsumerToken& operator=(ConsumerToken&& other) MOODYCAMEL_NOEXCEPT
+ {
+ swap(other);
+ return *this;
+ }
+
+ void swap(ConsumerToken& other) MOODYCAMEL_NOEXCEPT
+ {
+ std::swap(initialOffset, other.initialOffset);
+ std::swap(lastKnownGlobalOffset, other.lastKnownGlobalOffset);
+ std::swap(itemsConsumedFromCurrent, other.itemsConsumedFromCurrent);
+ std::swap(currentProducer, other.currentProducer);
+ std::swap(desiredProducer, other.desiredProducer);
+ }
+
+ // Disable copying and assignment
+ ConsumerToken(ConsumerToken const&) MOODYCAMEL_DELETE_FUNCTION;
+ ConsumerToken& operator=(ConsumerToken const&) MOODYCAMEL_DELETE_FUNCTION;
+
+private:
+ template<typename T, typename Traits> friend class ConcurrentQueue;
+ friend class ConcurrentQueueTests;
+
+private: // but shared with ConcurrentQueue
+ std::uint32_t initialOffset;
+ std::uint32_t lastKnownGlobalOffset;
+ std::uint32_t itemsConsumedFromCurrent;
+ details::ConcurrentQueueProducerTypelessBase* currentProducer;
+ details::ConcurrentQueueProducerTypelessBase* desiredProducer;
+};
+
+// Need to forward-declare this swap because it's in a namespace.
+// See http://stackoverflow.com/questions/4492062/why-does-a-c-friend-class-need-a-forward-declaration-only-in-other-namespaces
+template<typename T, typename Traits>
+inline void swap(typename ConcurrentQueue<T, Traits>::ImplicitProducerKVP& a, typename ConcurrentQueue<T, Traits>::ImplicitProducerKVP& b) MOODYCAMEL_NOEXCEPT;
+
+
+template<typename T, typename Traits = ConcurrentQueueDefaultTraits>
+class ConcurrentQueue
+{
+public:
+ typedef ::moodycamel::ProducerToken producer_token_t;
+ typedef ::moodycamel::ConsumerToken consumer_token_t;
+
+ typedef typename Traits::index_t index_t;
+ typedef typename Traits::size_t size_t;
+
+ static const size_t BLOCK_SIZE = static_cast<size_t>(Traits::BLOCK_SIZE);
+ static const size_t EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD = static_cast<size_t>(Traits::EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD);
+ static const size_t EXPLICIT_INITIAL_INDEX_SIZE = static_cast<size_t>(Traits::EXPLICIT_INITIAL_INDEX_SIZE);
+ static const size_t IMPLICIT_INITIAL_INDEX_SIZE = static_cast<size_t>(Traits::IMPLICIT_INITIAL_INDEX_SIZE);
+ static const size_t INITIAL_IMPLICIT_PRODUCER_HASH_SIZE = static_cast<size_t>(Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE);
+ static const std::uint32_t EXPLICIT_CONSUMER_CONSUMPTION_QUOTA_BEFORE_ROTATE = static_cast<std::uint32_t>(Traits::EXPLICIT_CONSUMER_CONSUMPTION_QUOTA_BEFORE_ROTATE);
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable: 4307) // + integral constant overflow (that's what the ternary expression is for!)
+#pragma warning(disable: 4309) // static_cast: Truncation of constant value
+#endif
+ static const size_t MAX_SUBQUEUE_SIZE = (details::const_numeric_max<size_t>::value - static_cast<size_t>(Traits::MAX_SUBQUEUE_SIZE) < BLOCK_SIZE) ? details::const_numeric_max<size_t>::value : ((static_cast<size_t>(Traits::MAX_SUBQUEUE_SIZE) + (BLOCK_SIZE - 1)) / BLOCK_SIZE * BLOCK_SIZE);
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+ static_assert(!std::numeric_limits<size_t>::is_signed && std::is_integral<size_t>::value, "Traits::size_t must be an unsigned integral type");
+ static_assert(!std::numeric_limits<index_t>::is_signed && std::is_integral<index_t>::value, "Traits::index_t must be an unsigned integral type");
+ static_assert(sizeof(index_t) >= sizeof(size_t), "Traits::index_t must be at least as wide as Traits::size_t");
+ static_assert((BLOCK_SIZE > 1) && !(BLOCK_SIZE & (BLOCK_SIZE - 1)), "Traits::BLOCK_SIZE must be a power of 2 (and at least 2)");
+ static_assert((EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD > 1) && !(EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD & (EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD - 1)), "Traits::EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD must be a power of 2 (and greater than 1)");
+ static_assert((EXPLICIT_INITIAL_INDEX_SIZE > 1) && !(EXPLICIT_INITIAL_INDEX_SIZE & (EXPLICIT_INITIAL_INDEX_SIZE - 1)), "Traits::EXPLICIT_INITIAL_INDEX_SIZE must be a power of 2 (and greater than 1)");
+ static_assert((IMPLICIT_INITIAL_INDEX_SIZE > 1) && !(IMPLICIT_INITIAL_INDEX_SIZE & (IMPLICIT_INITIAL_INDEX_SIZE - 1)), "Traits::IMPLICIT_INITIAL_INDEX_SIZE must be a power of 2 (and greater than 1)");
+ static_assert((INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) || !(INITIAL_IMPLICIT_PRODUCER_HASH_SIZE & (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE - 1)), "Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE must be a power of 2");
+ static_assert(INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0 || INITIAL_IMPLICIT_PRODUCER_HASH_SIZE >= 1, "Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE must be at least 1 (or 0 to disable implicit enqueueing)");
+
+public:
+ // Creates a queue with at least `capacity` element slots; note that the
+ // actual number of elements that can be inserted without additional memory
+ // allocation depends on the number of producers and the block size (e.g. if
+ // the block size is equal to `capacity`, only a single block will be allocated
+ // up-front, which means only a single producer will be able to enqueue elements
+ // without an extra allocation -- blocks aren't shared between producers).
+ // This method is not thread safe -- it is up to the user to ensure that the
+ // queue is fully constructed before it starts being used by other threads (this
+ // includes making the memory effects of construction visible, possibly with a
+ // memory barrier).
+ explicit ConcurrentQueue(size_t capacity = 6 * BLOCK_SIZE)
+ : producerListTail(nullptr),
+ producerCount(0),
+ initialBlockPoolIndex(0),
+ nextExplicitConsumerId(0),
+ globalExplicitConsumerOffset(0)
+ {
+ implicitProducerHashResizeInProgress.clear(std::memory_order::relaxed);
+ populate_initial_implicit_producer_hash();
+ populate_initial_block_list(capacity / BLOCK_SIZE + ((capacity & (BLOCK_SIZE - 1)) == 0 ? 0 : 1));
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ // Track all the producers using a fully-resolved typed list for
+ // each kind; this makes it possible to debug them starting from
+ // the root queue object (otherwise wacky casts are needed that
+ // don't compile in the debugger's expression evaluator).
+ explicitProducers.store(nullptr, std::memory_order::relaxed);
+ implicitProducers.store(nullptr, std::memory_order::relaxed);
+#endif
+ }
+
+ // Computes the correct amount of pre-allocated blocks for you based
+ // on the minimum number of elements you want available at any given
+ // time, and the maximum concurrent number of each type of producer.
+ ConcurrentQueue(size_t minCapacity, size_t maxExplicitProducers, size_t maxImplicitProducers)
+ : producerListTail(nullptr),
+ producerCount(0),
+ initialBlockPoolIndex(0),
+ nextExplicitConsumerId(0),
+ globalExplicitConsumerOffset(0)
+ {
+ implicitProducerHashResizeInProgress.clear(std::memory_order::relaxed);
+ populate_initial_implicit_producer_hash();
+ size_t blocks = (((minCapacity + BLOCK_SIZE - 1) / BLOCK_SIZE) - 1) * (maxExplicitProducers + 1) + 2 * (maxExplicitProducers + maxImplicitProducers);
+ populate_initial_block_list(blocks);
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ explicitProducers.store(nullptr, std::memory_order::relaxed);
+ implicitProducers.store(nullptr, std::memory_order::relaxed);
+#endif
+ }
+
+ // Note: The queue should not be accessed concurrently while it's
+ // being deleted. It's up to the user to synchronize this.
+ // This method is not thread safe.
+ ~ConcurrentQueue()
+ {
+ // Destroy producers
+ auto ptr = producerListTail.load(std::memory_order::relaxed);
+ while (ptr != nullptr) {
+ auto next = ptr->next_prod();
+ if (ptr->token != nullptr) {
+ ptr->token->producer = nullptr;
+ }
+ destroy(ptr);
+ ptr = next;
+ }
+
+ // Destroy implicit producer hash tables
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE != 0) {
+ auto hash = implicitProducerHash.load(std::memory_order::relaxed);
+ while (hash != nullptr) {
+ auto prev = hash->prev;
+ if (prev != nullptr) { // The last hash is part of this object and was not allocated dynamically
+ for (size_t i = 0; i != hash->capacity; ++i) {
+ hash->entries[i].~ImplicitProducerKVP();
+ }
+ hash->~ImplicitProducerHash();
+ (Traits::free)(hash);
+ }
+ hash = prev;
+ }
+ }
+
+ // Destroy global free list
+ auto block = freeList.head_unsafe();
+ while (block != nullptr) {
+ auto next = block->freeListNext.load(std::memory_order::relaxed);
+ if (block->dynamicallyAllocated) {
+ destroy(block);
+ }
+ block = next;
+ }
+
+ // Destroy initial free list
+ destroy_array(initialBlockPool, initialBlockPoolSize);
+ }
+
+ // Disable copying and copy assignment
+ ConcurrentQueue(ConcurrentQueue const&) MOODYCAMEL_DELETE_FUNCTION;
+ ConcurrentQueue& operator=(ConcurrentQueue const&) MOODYCAMEL_DELETE_FUNCTION;
+
+ // Moving is supported, but note that it is *not* a thread-safe operation.
+ // Nobody can use the queue while it's being moved, and the memory effects
+ // of that move must be propagated to other threads before they can use it.
+ // Note: When a queue is moved, its tokens are still valid but can only be
+ // used with the destination queue (i.e. semantically they are moved along
+ // with the queue itself).
+ ConcurrentQueue(ConcurrentQueue&& other) MOODYCAMEL_NOEXCEPT
+ : producerListTail(other.producerListTail.load(std::memory_order::relaxed)),
+ producerCount(other.producerCount.load(std::memory_order::relaxed)),
+ initialBlockPoolIndex(other.initialBlockPoolIndex.load(std::memory_order::relaxed)),
+ initialBlockPool(other.initialBlockPool),
+ initialBlockPoolSize(other.initialBlockPoolSize),
+ freeList(std::move(other.freeList)),
+ nextExplicitConsumerId(other.nextExplicitConsumerId.load(std::memory_order::relaxed)),
+ globalExplicitConsumerOffset(other.globalExplicitConsumerOffset.load(std::memory_order::relaxed))
+ {
+ // Move the other one into this, and leave the other one as an empty queue
+ implicitProducerHashResizeInProgress.clear(std::memory_order::relaxed);
+ populate_initial_implicit_producer_hash();
+ swap_implicit_producer_hashes(other);
+
+ other.producerListTail.store(nullptr, std::memory_order::relaxed);
+ other.producerCount.store(0, std::memory_order::relaxed);
+ other.nextExplicitConsumerId.store(0, std::memory_order::relaxed);
+ other.globalExplicitConsumerOffset.store(0, std::memory_order::relaxed);
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ explicitProducers.store(other.explicitProducers.load(std::memory_order::relaxed), std::memory_order::relaxed);
+ other.explicitProducers.store(nullptr, std::memory_order::relaxed);
+ implicitProducers.store(other.implicitProducers.load(std::memory_order::relaxed), std::memory_order::relaxed);
+ other.implicitProducers.store(nullptr, std::memory_order::relaxed);
+#endif
+
+ other.initialBlockPoolIndex.store(0, std::memory_order::relaxed);
+ other.initialBlockPoolSize = 0;
+ other.initialBlockPool = nullptr;
+
+ reown_producers();
+ }
+
+ inline ConcurrentQueue& operator=(ConcurrentQueue&& other) MOODYCAMEL_NOEXCEPT
+ {
+ return swap_internal(other);
+ }
+
+ // Swaps this queue's state with the other's. Not thread-safe.
+ // Swapping two queues does not invalidate their tokens, however
+ // the tokens that were created for one queue must be used with
+ // only the swapped queue (i.e. the tokens are tied to the
+ // queue's movable state, not the object itself).
+ inline void swap(ConcurrentQueue& other) MOODYCAMEL_NOEXCEPT
+ {
+ swap_internal(other);
+ }
+
+private:
+ ConcurrentQueue& swap_internal(ConcurrentQueue& other)
+ {
+ if (this == &other) {
+ return *this;
+ }
+
+ details::swap_relaxed(producerListTail, other.producerListTail);
+ details::swap_relaxed(producerCount, other.producerCount);
+ details::swap_relaxed(initialBlockPoolIndex, other.initialBlockPoolIndex);
+ std::swap(initialBlockPool, other.initialBlockPool);
+ std::swap(initialBlockPoolSize, other.initialBlockPoolSize);
+ freeList.swap(other.freeList);
+ details::swap_relaxed(nextExplicitConsumerId, other.nextExplicitConsumerId);
+ details::swap_relaxed(globalExplicitConsumerOffset, other.globalExplicitConsumerOffset);
+
+ swap_implicit_producer_hashes(other);
+
+ reown_producers();
+ other.reown_producers();
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ details::swap_relaxed(explicitProducers, other.explicitProducers);
+ details::swap_relaxed(implicitProducers, other.implicitProducers);
+#endif
+
+ return *this;
+ }
+
+public:
+ // Enqueues a single item (by copying it).
+ // Allocates memory if required. Only fails if memory allocation fails (or implicit
+ // production is disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE is 0,
+ // or Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Thread-safe.
+ inline bool enqueue(T const& item)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue<CanAlloc>(item);
+ }
+
+ // Enqueues a single item (by moving it, if possible).
+ // Allocates memory if required. Only fails if memory allocation fails (or implicit
+ // production is disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE is 0,
+ // or Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Thread-safe.
+ inline bool enqueue(T&& item)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue<CanAlloc>(std::move(item));
+ }
+
+ // Enqueues a single item (by copying it) using an explicit producer token.
+ // Allocates memory if required. Only fails if memory allocation fails (or
+ // Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Thread-safe.
+ inline bool enqueue(producer_token_t const& token, T const& item)
+ {
+ return inner_enqueue<CanAlloc>(token, item);
+ }
+
+ // Enqueues a single item (by moving it, if possible) using an explicit producer token.
+ // Allocates memory if required. Only fails if memory allocation fails (or
+ // Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Thread-safe.
+ inline bool enqueue(producer_token_t const& token, T&& item)
+ {
+ return inner_enqueue<CanAlloc>(token, std::move(item));
+ }
+
+ // Enqueues several items.
+ // Allocates memory if required. Only fails if memory allocation fails (or
+ // implicit production is disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE
+ // is 0, or Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Note: Use std::make_move_iterator if the elements should be moved instead of copied.
+ // Thread-safe.
+ template<typename It>
+ bool enqueue_bulk(It itemFirst, size_t count)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue_bulk<CanAlloc>(itemFirst, count);
+ }
+
+ // Enqueues several items using an explicit producer token.
+ // Allocates memory if required. Only fails if memory allocation fails
+ // (or Traits::MAX_SUBQUEUE_SIZE has been defined and would be surpassed).
+ // Note: Use std::make_move_iterator if the elements should be moved
+ // instead of copied.
+ // Thread-safe.
+ template<typename It>
+ bool enqueue_bulk(producer_token_t const& token, It itemFirst, size_t count)
+ {
+ return inner_enqueue_bulk<CanAlloc>(token, itemFirst, count);
+ }
+
+ // Enqueues a single item (by copying it).
+ // Does not allocate memory. Fails if not enough room to enqueue (or implicit
+ // production is disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE
+ // is 0).
+ // Thread-safe.
+ inline bool try_enqueue(T const& item)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue<CannotAlloc>(item);
+ }
+
+ // Enqueues a single item (by moving it, if possible).
+ // Does not allocate memory (except for one-time implicit producer).
+ // Fails if not enough room to enqueue (or implicit production is
+ // disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE is 0).
+ // Thread-safe.
+ inline bool try_enqueue(T&& item)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue<CannotAlloc>(std::move(item));
+ }
+
+ // Enqueues a single item (by copying it) using an explicit producer token.
+ // Does not allocate memory. Fails if not enough room to enqueue.
+ // Thread-safe.
+ inline bool try_enqueue(producer_token_t const& token, T const& item)
+ {
+ return inner_enqueue<CannotAlloc>(token, item);
+ }
+
+ // Enqueues a single item (by moving it, if possible) using an explicit producer token.
+ // Does not allocate memory. Fails if not enough room to enqueue.
+ // Thread-safe.
+ inline bool try_enqueue(producer_token_t const& token, T&& item)
+ {
+ return inner_enqueue<CannotAlloc>(token, std::move(item));
+ }
+
+ // Enqueues several items.
+ // Does not allocate memory (except for one-time implicit producer).
+ // Fails if not enough room to enqueue (or implicit production is
+ // disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE is 0).
+ // Note: Use std::make_move_iterator if the elements should be moved
+ // instead of copied.
+ // Thread-safe.
+ template<typename It>
+ bool try_enqueue_bulk(It itemFirst, size_t count)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) return false;
+ else return inner_enqueue_bulk<CannotAlloc>(itemFirst, count);
+ }
+
+ // Enqueues several items using an explicit producer token.
+ // Does not allocate memory. Fails if not enough room to enqueue.
+ // Note: Use std::make_move_iterator if the elements should be moved
+ // instead of copied.
+ // Thread-safe.
+ template<typename It>
+ bool try_enqueue_bulk(producer_token_t const& token, It itemFirst, size_t count)
+ {
+ return inner_enqueue_bulk<CannotAlloc>(token, itemFirst, count);
+ }
+
+
+
+ // Attempts to dequeue from the queue.
+ // Returns false if all producer streams appeared empty at the time they
+ // were checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename U>
+ bool try_dequeue(U& item)
+ {
+ // Instead of simply trying each producer in turn (which could cause needless contention on the first
+ // producer), we score them heuristically.
+ size_t nonEmptyCount = 0;
+ ProducerBase* best = nullptr;
+ size_t bestSize = 0;
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); nonEmptyCount < 3 && ptr != nullptr; ptr = ptr->next_prod()) {
+ auto size = ptr->size_approx();
+ if (size > 0) {
+ if (size > bestSize) {
+ bestSize = size;
+ best = ptr;
+ }
+ ++nonEmptyCount;
+ }
+ }
+
+ // If there was at least one non-empty queue but it appears empty at the time
+ // we try to dequeue from it, we need to make sure every queue's been tried
+ if (nonEmptyCount > 0) {
+ if ((details::likely)(best->dequeue(item))) {
+ return true;
+ }
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ if (ptr != best && ptr->dequeue(item)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Attempts to dequeue from the queue.
+ // Returns false if all producer streams appeared empty at the time they
+ // were checked (so, the queue is likely but not guaranteed to be empty).
+ // This differs from the try_dequeue(item) method in that this one does
+ // not attempt to reduce contention by interleaving the order that producer
+ // streams are dequeued from. So, using this method can reduce overall throughput
+ // under contention, but will give more predictable results in single-threaded
+ // consumer scenarios. This is mostly only useful for internal unit tests.
+ // Never allocates. Thread-safe.
+ template<typename U>
+ bool try_dequeue_non_interleaved(U& item)
+ {
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ if (ptr->dequeue(item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Attempts to dequeue from the queue using an explicit consumer token.
+ // Returns false if all producer streams appeared empty at the time they
+ // were checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename U>
+ bool try_dequeue(consumer_token_t& token, U& item)
+ {
+ // The idea is roughly as follows:
+ // Every 256 items from one producer, make everyone rotate (increase the global offset) -> this means the highest efficiency consumer dictates the rotation speed of everyone else, more or less
+ // If you see that the global offset has changed, you must reset your consumption counter and move to your designated place
+ // If there's no items where you're supposed to be, keep moving until you find a producer with some items
+ // If the global offset has not changed but you've run out of items to consume, move over from your current position until you find an producer with something in it
+
+ if (token.desiredProducer == nullptr || token.lastKnownGlobalOffset != globalExplicitConsumerOffset.load(std::memory_order::relaxed)) {
+ if (!update_current_producer_after_rotation(token)) {
+ return false;
+ }
+ }
+
+ // If there was at least one non-empty queue but it appears empty at the time
+ // we try to dequeue from it, we need to make sure every queue's been tried
+ if (static_cast<ProducerBase*>(token.currentProducer)->dequeue(item)) {
+ if (++token.itemsConsumedFromCurrent == EXPLICIT_CONSUMER_CONSUMPTION_QUOTA_BEFORE_ROTATE) {
+ globalExplicitConsumerOffset.fetch_add(1, std::memory_order::relaxed);
+ }
+ return true;
+ }
+
+ auto tail = producerListTail.load(std::memory_order::acquire);
+ auto ptr = static_cast<ProducerBase*>(token.currentProducer)->next_prod();
+ if (ptr == nullptr) {
+ ptr = tail;
+ }
+ while (ptr != static_cast<ProducerBase*>(token.currentProducer)) {
+ if (ptr->dequeue(item)) {
+ token.currentProducer = ptr;
+ token.itemsConsumedFromCurrent = 1;
+ return true;
+ }
+ ptr = ptr->next_prod();
+ if (ptr == nullptr) {
+ ptr = tail;
+ }
+ }
+ return false;
+ }
+
+ // Attempts to dequeue several elements from the queue.
+ // Returns the number of items actually dequeued.
+ // Returns 0 if all producer streams appeared empty at the time they
+ // were checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename It>
+ size_t try_dequeue_bulk(It itemFirst, size_t max)
+ {
+ size_t count = 0;
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ count += ptr->dequeue_bulk(itemFirst, max - count);
+ if (count == max) {
+ break;
+ }
+ }
+ return count;
+ }
+
+ // Attempts to dequeue several elements from the queue using an explicit consumer token.
+ // Returns the number of items actually dequeued.
+ // Returns 0 if all producer streams appeared empty at the time they
+ // were checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename It>
+ size_t try_dequeue_bulk(consumer_token_t& token, It itemFirst, size_t max)
+ {
+ if (token.desiredProducer == nullptr || token.lastKnownGlobalOffset != globalExplicitConsumerOffset.load(std::memory_order::relaxed)) {
+ if (!update_current_producer_after_rotation(token)) {
+ return 0;
+ }
+ }
+
+ size_t count = static_cast<ProducerBase*>(token.currentProducer)->dequeue_bulk(itemFirst, max);
+ if (count == max) {
+ if ((token.itemsConsumedFromCurrent += static_cast<std::uint32_t>(max)) >= EXPLICIT_CONSUMER_CONSUMPTION_QUOTA_BEFORE_ROTATE) {
+ globalExplicitConsumerOffset.fetch_add(1, std::memory_order::relaxed);
+ }
+ return max;
+ }
+ token.itemsConsumedFromCurrent += static_cast<std::uint32_t>(count);
+ max -= count;
+
+ auto tail = producerListTail.load(std::memory_order::acquire);
+ auto ptr = static_cast<ProducerBase*>(token.currentProducer)->next_prod();
+ if (ptr == nullptr) {
+ ptr = tail;
+ }
+ while (ptr != static_cast<ProducerBase*>(token.currentProducer)) {
+ auto dequeued = ptr->dequeue_bulk(itemFirst, max);
+ count += dequeued;
+ if (dequeued != 0) {
+ token.currentProducer = ptr;
+ token.itemsConsumedFromCurrent = static_cast<std::uint32_t>(dequeued);
+ }
+ if (dequeued == max) {
+ break;
+ }
+ max -= dequeued;
+ ptr = ptr->next_prod();
+ if (ptr == nullptr) {
+ ptr = tail;
+ }
+ }
+ return count;
+ }
+
+
+
+ // Attempts to dequeue from a specific producer's inner queue.
+ // If you happen to know which producer you want to dequeue from, this
+ // is significantly faster than using the general-case try_dequeue methods.
+ // Returns false if the producer's queue appeared empty at the time it
+ // was checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename U>
+ inline bool try_dequeue_from_producer(producer_token_t const& producer, U& item)
+ {
+ return static_cast<ExplicitProducer*>(producer.producer)->dequeue(item);
+ }
+
+ // Attempts to dequeue several elements from a specific producer's inner queue.
+ // Returns the number of items actually dequeued.
+ // If you happen to know which producer you want to dequeue from, this
+ // is significantly faster than using the general-case try_dequeue methods.
+ // Returns 0 if the producer's queue appeared empty at the time it
+ // was checked (so, the queue is likely but not guaranteed to be empty).
+ // Never allocates. Thread-safe.
+ template<typename It>
+ inline size_t try_dequeue_bulk_from_producer(producer_token_t const& producer, It itemFirst, size_t max)
+ {
+ return static_cast<ExplicitProducer*>(producer.producer)->dequeue_bulk(itemFirst, max);
+ }
+
+
+ // Returns an estimate of the total number of elements currently in the queue. This
+ // estimate is only accurate if the queue has completely stabilized before it is called
+ // (i.e. all enqueue and dequeue operations have completed and their memory effects are
+ // visible on the calling thread, and no further operations start while this method is
+ // being called).
+ // Thread-safe.
+ size_t size_approx() const
+ {
+ size_t size = 0;
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ size += ptr->size_approx();
+ }
+ return size;
+ }
+
+
+ // Returns true if the underlying atomic variables used by
+ // the queue are lock-free (they should be on most platforms).
+ // Thread-safe.
+ static bool is_lock_free()
+ {
+ return
+ details::static_is_lock_free<bool>::value == 2 &&
+ details::static_is_lock_free<size_t>::value == 2 &&
+ details::static_is_lock_free<std::uint32_t>::value == 2 &&
+ details::static_is_lock_free<index_t>::value == 2 &&
+ details::static_is_lock_free<void*>::value == 2 &&
+ details::static_is_lock_free<typename details::thread_id_converter<details::thread_id_t>::thread_id_numeric_size_t>::value == 2;
+ }
+
+
+private:
+ friend struct ProducerToken;
+ friend struct ConsumerToken;
+ struct ExplicitProducer;
+ friend struct ExplicitProducer;
+ struct ImplicitProducer;
+ friend struct ImplicitProducer;
+ friend class ConcurrentQueueTests;
+
+ enum AllocationMode { CanAlloc, CannotAlloc };
+
+
+ ///////////////////////////////
+ // Queue methods
+ ///////////////////////////////
+
+ template<AllocationMode canAlloc, typename U>
+ inline bool inner_enqueue(producer_token_t const& token, U&& element)
+ {
+ return static_cast<ExplicitProducer*>(token.producer)->ConcurrentQueue::ExplicitProducer::template enqueue<canAlloc>(std::forward<U>(element));
+ }
+
+ template<AllocationMode canAlloc, typename U>
+ inline bool inner_enqueue(U&& element)
+ {
+ auto producer = get_or_add_implicit_producer();
+ return producer == nullptr ? false : producer->ConcurrentQueue::ImplicitProducer::template enqueue<canAlloc>(std::forward<U>(element));
+ }
+
+ template<AllocationMode canAlloc, typename It>
+ inline bool inner_enqueue_bulk(producer_token_t const& token, It itemFirst, size_t count)
+ {
+ return static_cast<ExplicitProducer*>(token.producer)->ConcurrentQueue::ExplicitProducer::template enqueue_bulk<canAlloc>(itemFirst, count);
+ }
+
+ template<AllocationMode canAlloc, typename It>
+ inline bool inner_enqueue_bulk(It itemFirst, size_t count)
+ {
+ auto producer = get_or_add_implicit_producer();
+ return producer == nullptr ? false : producer->ConcurrentQueue::ImplicitProducer::template enqueue_bulk<canAlloc>(itemFirst, count);
+ }
+
+ inline bool update_current_producer_after_rotation(consumer_token_t& token)
+ {
+ // Ah, there's been a rotation, figure out where we should be!
+ auto tail = producerListTail.load(std::memory_order::acquire);
+ if (token.desiredProducer == nullptr && tail == nullptr) {
+ return false;
+ }
+ auto prodCount = producerCount.load(std::memory_order::relaxed);
+ auto globalOffset = globalExplicitConsumerOffset.load(std::memory_order::relaxed);
+ if ((details::unlikely)(token.desiredProducer == nullptr)) {
+ // Aha, first time we're dequeueing anything.
+ // Figure out our local position
+ // Note: offset is from start, not end, but we're traversing from end -- subtract from count first
+ std::uint32_t offset = prodCount - 1 - (token.initialOffset % prodCount);
+ token.desiredProducer = tail;
+ for (std::uint32_t i = 0; i != offset; ++i) {
+ token.desiredProducer = static_cast<ProducerBase*>(token.desiredProducer)->next_prod();
+ if (token.desiredProducer == nullptr) {
+ token.desiredProducer = tail;
+ }
+ }
+ }
+
+ std::uint32_t delta = globalOffset - token.lastKnownGlobalOffset;
+ if (delta >= prodCount) {
+ delta = delta % prodCount;
+ }
+ for (std::uint32_t i = 0; i != delta; ++i) {
+ token.desiredProducer = static_cast<ProducerBase*>(token.desiredProducer)->next_prod();
+ if (token.desiredProducer == nullptr) {
+ token.desiredProducer = tail;
+ }
+ }
+
+ token.lastKnownGlobalOffset = globalOffset;
+ token.currentProducer = token.desiredProducer;
+ token.itemsConsumedFromCurrent = 0;
+ return true;
+ }
+
+
+ ///////////////////////////
+ // Free list
+ ///////////////////////////
+
+ template <typename N>
+ struct FreeListNode
+ {
+ FreeListNode() : freeListRefs(0), freeListNext(nullptr) { }
+
+ std::atomic<std::uint32_t> freeListRefs;
+ std::atomic<N*> freeListNext;
+ };
+
+ // A simple CAS-based lock-free free list. Not the fastest thing in the world under heavy contention, but
+ // simple and correct (assuming nodes are never freed until after the free list is destroyed), and fairly
+ // speedy under low contention.
+ template<typename N> // N must inherit FreeListNode or have the same fields (and initialization of them)
+ struct FreeList
+ {
+ FreeList() : freeListHead(nullptr) { }
+ FreeList(FreeList&& other) : freeListHead(other.freeListHead.load(std::memory_order::relaxed)) { other.freeListHead.store(nullptr, std::memory_order::relaxed); }
+ void swap(FreeList& other) { details::swap_relaxed(freeListHead, other.freeListHead); }
+
+ FreeList(FreeList const&) MOODYCAMEL_DELETE_FUNCTION;
+ FreeList& operator=(FreeList const&) MOODYCAMEL_DELETE_FUNCTION;
+
+ inline void add(N* node)
+ {
+#ifdef MCDBGQ_NOLOCKFREE_FREELIST
+ debug::DebugLock lock(mutex);
+#endif
+ // We know that the should-be-on-freelist bit is 0 at this point, so it's safe to
+ // set it using a fetch_add
+ if (node->freeListRefs.fetch_add(SHOULD_BE_ON_FREELIST, std::memory_order::acq_rel) == 0) {
+ // Oh look! We were the last ones referencing this node, and we know
+ // we want to add it to the free list, so let's do it!
+ add_knowing_refcount_is_zero(node);
+ }
+ }
+
+ inline N* try_get()
+ {
+#ifdef MCDBGQ_NOLOCKFREE_FREELIST
+ debug::DebugLock lock(mutex);
+#endif
+ auto head = freeListHead.load(std::memory_order::acquire);
+ while (head != nullptr) {
+ auto prevHead = head;
+ auto refs = head->freeListRefs.load(std::memory_order::relaxed);
+ if ((refs & REFS_MASK) == 0 || !head->freeListRefs.compare_exchange_strong(refs, refs + 1, std::memory_order::acquire, std::memory_order::relaxed)) {
+ head = freeListHead.load(std::memory_order::acquire);
+ continue;
+ }
+
+ // Good, reference count has been incremented (it wasn't at zero), which means we can read the
+ // next and not worry about it changing between now and the time we do the CAS
+ auto next = head->freeListNext.load(std::memory_order::relaxed);
+ if (freeListHead.compare_exchange_strong(head, next, std::memory_order::acquire, std::memory_order::relaxed)) {
+ // Yay, got the node. This means it was on the list, which means shouldBeOnFreeList must be false no
+ // matter the refcount (because nobody else knows it's been taken off yet, it can't have been put back on).
+ assert((head->freeListRefs.load(std::memory_order::relaxed) & SHOULD_BE_ON_FREELIST) == 0);
+
+ // Decrease refcount twice, once for our ref, and once for the list's ref
+ head->freeListRefs.fetch_sub(2, std::memory_order::release);
+ return head;
+ }
+
+ // OK, the head must have changed on us, but we still need to decrease the refcount we increased.
+ // Note that we don't need to release any memory effects, but we do need to ensure that the reference
+ // count decrement happens-after the CAS on the head.
+ refs = prevHead->freeListRefs.fetch_sub(1, std::memory_order::acq_rel);
+ if (refs == SHOULD_BE_ON_FREELIST + 1) {
+ add_knowing_refcount_is_zero(prevHead);
+ }
+ }
+
+ return nullptr;
+ }
+
+ // Useful for traversing the list when there's no contention (e.g. to destroy remaining nodes)
+ N* head_unsafe() const { return freeListHead.load(std::memory_order::relaxed); }
+
+ private:
+ inline void add_knowing_refcount_is_zero(N* node)
+ {
+ // Since the refcount is zero, and nobody can increase it once it's zero (except us, and we run
+ // only one copy of this method per node at a time, i.e. the single thread case), then we know
+ // we can safely change the next pointer of the node; however, once the refcount is back above
+ // zero, then other threads could increase it (happens under heavy contention, when the refcount
+ // goes to zero in between a load and a refcount increment of a node in try_get, then back up to
+ // something non-zero, then the refcount increment is done by the other thread) -- so, if the CAS
+ // to add the node to the actual list fails, decrease the refcount and leave the add operation to
+ // the next thread who puts the refcount back at zero (which could be us, hence the loop).
+ auto head = freeListHead.load(std::memory_order::relaxed);
+ while (true) {
+ node->freeListNext.store(head, std::memory_order::relaxed);
+ node->freeListRefs.store(1, std::memory_order::release);
+ if (!freeListHead.compare_exchange_strong(head, node, std::memory_order::release, std::memory_order::relaxed)) {
+ // Hmm, the add failed, but we can only try again when the refcount goes back to zero
+ if (node->freeListRefs.fetch_add(SHOULD_BE_ON_FREELIST - 1, std::memory_order::release) == 1) {
+ continue;
+ }
+ }
+ return;
+ }
+ }
+
+ private:
+ // Implemented like a stack, but where node order doesn't matter (nodes are inserted out of order under contention)
+ std::atomic<N*> freeListHead;
+
+ static const std::uint32_t REFS_MASK = 0x7FFFFFFF;
+ static const std::uint32_t SHOULD_BE_ON_FREELIST = 0x80000000;
+
+#ifdef MCDBGQ_NOLOCKFREE_FREELIST
+ debug::DebugMutex mutex;
+#endif
+ };
+
+
+ ///////////////////////////
+ // Block
+ ///////////////////////////
+
+ enum InnerQueueContext { implicit_context = 0, explicit_context = 1 };
+
+ struct Block
+ {
+ Block()
+ : next(nullptr), elementsCompletelyDequeued(0), freeListRefs(0), freeListNext(nullptr), shouldBeOnFreeList(false), dynamicallyAllocated(true)
+ {
+#ifdef MCDBGQ_TRACKMEM
+ owner = nullptr;
+#endif
+ }
+
+ template<InnerQueueContext context>
+ inline bool is_empty() const
+ {
+ MOODYCAMEL_CONSTEXPR_IF (context == explicit_context && BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD) {
+ // Check flags
+ for (size_t i = 0; i < BLOCK_SIZE; ++i) {
+ if (!emptyFlags[i].load(std::memory_order::relaxed)) {
+ return false;
+ }
+ }
+
+ // Aha, empty; make sure we have all other memory effects that happened before the empty flags were set
+ std::atomic_thread_fence(std::memory_order::acquire);
+ return true;
+ }
+ else {
+ // Check counter
+ if (elementsCompletelyDequeued.load(std::memory_order::relaxed) == BLOCK_SIZE) {
+ std::atomic_thread_fence(std::memory_order::acquire);
+ return true;
+ }
+ assert(elementsCompletelyDequeued.load(std::memory_order::relaxed) <= BLOCK_SIZE);
+ return false;
+ }
+ }
+
+ // Returns true if the block is now empty (does not apply in explicit context)
+ template<InnerQueueContext context>
+ inline bool set_empty(MOODYCAMEL_MAYBE_UNUSED index_t i)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (context == explicit_context && BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD) {
+ // Set flag
+ assert(!emptyFlags[BLOCK_SIZE - 1 - static_cast<size_t>(i & static_cast<index_t>(BLOCK_SIZE - 1))].load(std::memory_order::relaxed));
+ emptyFlags[BLOCK_SIZE - 1 - static_cast<size_t>(i & static_cast<index_t>(BLOCK_SIZE - 1))].store(true, std::memory_order::release);
+ return false;
+ }
+ else {
+ // Increment counter
+ auto prevVal = elementsCompletelyDequeued.fetch_add(1, std::memory_order::release);
+ assert(prevVal < BLOCK_SIZE);
+ return prevVal == BLOCK_SIZE - 1;
+ }
+ }
+
+ // Sets multiple contiguous item statuses to 'empty' (assumes no wrapping and count > 0).
+ // Returns true if the block is now empty (does not apply in explicit context).
+ template<InnerQueueContext context>
+ inline bool set_many_empty(MOODYCAMEL_MAYBE_UNUSED index_t i, size_t count)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (context == explicit_context && BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD) {
+ // Set flags
+ std::atomic_thread_fence(std::memory_order::release);
+ i = BLOCK_SIZE - 1 - static_cast<size_t>(i & static_cast<index_t>(BLOCK_SIZE - 1)) - count + 1;
+ for (size_t j = 0; j != count; ++j) {
+ assert(!emptyFlags[i + j].load(std::memory_order::relaxed));
+ emptyFlags[i + j].store(true, std::memory_order::relaxed);
+ }
+ return false;
+ }
+ else {
+ // Increment counter
+ auto prevVal = elementsCompletelyDequeued.fetch_add(count, std::memory_order::release);
+ assert(prevVal + count <= BLOCK_SIZE);
+ return prevVal + count == BLOCK_SIZE;
+ }
+ }
+
+ template<InnerQueueContext context>
+ inline void set_all_empty()
+ {
+ MOODYCAMEL_CONSTEXPR_IF (context == explicit_context && BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD) {
+ // Set all flags
+ for (size_t i = 0; i != BLOCK_SIZE; ++i) {
+ emptyFlags[i].store(true, std::memory_order::relaxed);
+ }
+ }
+ else {
+ // Reset counter
+ elementsCompletelyDequeued.store(BLOCK_SIZE, std::memory_order::relaxed);
+ }
+ }
+
+ template<InnerQueueContext context>
+ inline void reset_empty()
+ {
+ MOODYCAMEL_CONSTEXPR_IF (context == explicit_context && BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD) {
+ // Reset flags
+ for (size_t i = 0; i != BLOCK_SIZE; ++i) {
+ emptyFlags[i].store(false, std::memory_order::relaxed);
+ }
+ }
+ else {
+ // Reset counter
+ elementsCompletelyDequeued.store(0, std::memory_order::relaxed);
+ }
+ }
+
+ inline T* operator[](index_t idx) MOODYCAMEL_NOEXCEPT { return static_cast<T*>(static_cast<void*>(elements)) + static_cast<size_t>(idx & static_cast<index_t>(BLOCK_SIZE - 1)); }
+ inline T const* operator[](index_t idx) const MOODYCAMEL_NOEXCEPT { return static_cast<T const*>(static_cast<void const*>(elements)) + static_cast<size_t>(idx & static_cast<index_t>(BLOCK_SIZE - 1)); }
+
+ private:
+ static_assert(std::alignment_of<T>::value <= sizeof(T), "The queue does not support types with an alignment greater than their size at this time");
+ MOODYCAMEL_ALIGNED_TYPE_LIKE(char[sizeof(T) * BLOCK_SIZE], T) elements;
+ public:
+ Block* next;
+ std::atomic<size_t> elementsCompletelyDequeued;
+ std::atomic<bool> emptyFlags[BLOCK_SIZE <= EXPLICIT_BLOCK_EMPTY_COUNTER_THRESHOLD ? BLOCK_SIZE : 1];
+ public:
+ std::atomic<std::uint32_t> freeListRefs;
+ std::atomic<Block*> freeListNext;
+ std::atomic<bool> shouldBeOnFreeList;
+ bool dynamicallyAllocated; // Perhaps a better name for this would be 'isNotPartOfInitialBlockPool'
+
+#ifdef MCDBGQ_TRACKMEM
+ void* owner;
+#endif
+ };
+ static_assert(std::alignment_of<Block>::value >= std::alignment_of<T>::value, "Internal error: Blocks must be at least as aligned as the type they are wrapping");
+
+
+#ifdef MCDBGQ_TRACKMEM
+public:
+ struct MemStats;
+private:
+#endif
+
+ ///////////////////////////
+ // Producer base
+ ///////////////////////////
+
+ struct ProducerBase : public details::ConcurrentQueueProducerTypelessBase
+ {
+ ProducerBase(ConcurrentQueue* parent_, bool isExplicit_) :
+ tailIndex(0),
+ headIndex(0),
+ dequeueOptimisticCount(0),
+ dequeueOvercommit(0),
+ tailBlock(nullptr),
+ isExplicit(isExplicit_),
+ parent(parent_)
+ {
+ }
+
+ virtual ~ProducerBase() { }
+
+ template<typename U>
+ inline bool dequeue(U& element)
+ {
+ if (isExplicit) {
+ return static_cast<ExplicitProducer*>(this)->dequeue(element);
+ }
+ else {
+ return static_cast<ImplicitProducer*>(this)->dequeue(element);
+ }
+ }
+
+ template<typename It>
+ inline size_t dequeue_bulk(It& itemFirst, size_t max)
+ {
+ if (isExplicit) {
+ return static_cast<ExplicitProducer*>(this)->dequeue_bulk(itemFirst, max);
+ }
+ else {
+ return static_cast<ImplicitProducer*>(this)->dequeue_bulk(itemFirst, max);
+ }
+ }
+
+ inline ProducerBase* next_prod() const { return static_cast<ProducerBase*>(next); }
+
+ inline size_t size_approx() const
+ {
+ auto tail = tailIndex.load(std::memory_order::relaxed);
+ auto head = headIndex.load(std::memory_order::relaxed);
+ return details::circular_less_than(head, tail) ? static_cast<size_t>(tail - head) : 0;
+ }
+
+ inline index_t getTail() const { return tailIndex.load(std::memory_order::relaxed); }
+ protected:
+ std::atomic<index_t> tailIndex; // Where to enqueue to next
+ std::atomic<index_t> headIndex; // Where to dequeue from next
+
+ std::atomic<index_t> dequeueOptimisticCount;
+ std::atomic<index_t> dequeueOvercommit;
+
+ Block* tailBlock;
+
+ public:
+ bool isExplicit;
+ ConcurrentQueue* parent;
+
+ protected:
+#ifdef MCDBGQ_TRACKMEM
+ friend struct MemStats;
+#endif
+ };
+
+
+ ///////////////////////////
+ // Explicit queue
+ ///////////////////////////
+
+ struct ExplicitProducer : public ProducerBase
+ {
+ explicit ExplicitProducer(ConcurrentQueue* parent_) :
+ ProducerBase(parent_, true),
+ blockIndex(nullptr),
+ pr_blockIndexSlotsUsed(0),
+ pr_blockIndexSize(EXPLICIT_INITIAL_INDEX_SIZE >> 1),
+ pr_blockIndexFront(0),
+ pr_blockIndexEntries(nullptr),
+ pr_blockIndexRaw(nullptr)
+ {
+ size_t poolBasedIndexSize = details::ceil_to_pow_2(parent_->initialBlockPoolSize) >> 1;
+ if (poolBasedIndexSize > pr_blockIndexSize) {
+ pr_blockIndexSize = poolBasedIndexSize;
+ }
+
+ new_block_index(0); // This creates an index with double the number of current entries, i.e. EXPLICIT_INITIAL_INDEX_SIZE
+ }
+
+ ~ExplicitProducer()
+ {
+ // Destruct any elements not yet dequeued.
+ // Since we're in the destructor, we can assume all elements
+ // are either completely dequeued or completely not (no halfways).
+ if (this->tailBlock != nullptr) { // Note this means there must be a block index too
+ // First find the block that's partially dequeued, if any
+ Block* halfDequeuedBlock = nullptr;
+ if ((this->headIndex.load(std::memory_order::relaxed) & static_cast<index_t>(BLOCK_SIZE - 1)) != 0) {
+ // The head's not on a block boundary, meaning a block somewhere is partially dequeued
+ // (or the head block is the tail block and was fully dequeued, but the head/tail are still not on a boundary)
+ size_t i = (pr_blockIndexFront - pr_blockIndexSlotsUsed) & (pr_blockIndexSize - 1);
+ while (details::circular_less_than<index_t>(pr_blockIndexEntries[i].base + BLOCK_SIZE, this->headIndex.load(std::memory_order::relaxed))) {
+ i = (i + 1) & (pr_blockIndexSize - 1);
+ }
+ assert(details::circular_less_than<index_t>(pr_blockIndexEntries[i].base, this->headIndex.load(std::memory_order::relaxed)));
+ halfDequeuedBlock = pr_blockIndexEntries[i].block;
+ }
+
+ // Start at the head block (note the first line in the loop gives us the head from the tail on the first iteration)
+ auto block = this->tailBlock;
+ do {
+ block = block->next;
+ if (block->ConcurrentQueue::Block::template is_empty<explicit_context>()) {
+ continue;
+ }
+
+ size_t i = 0; // Offset into block
+ if (block == halfDequeuedBlock) {
+ i = static_cast<size_t>(this->headIndex.load(std::memory_order::relaxed) & static_cast<index_t>(BLOCK_SIZE - 1));
+ }
+
+ // Walk through all the items in the block; if this is the tail block, we need to stop when we reach the tail index
+ auto lastValidIndex = (this->tailIndex.load(std::memory_order::relaxed) & static_cast<index_t>(BLOCK_SIZE - 1)) == 0 ? BLOCK_SIZE : static_cast<size_t>(this->tailIndex.load(std::memory_order::relaxed) & static_cast<index_t>(BLOCK_SIZE - 1));
+ while (i != BLOCK_SIZE && (block != this->tailBlock || i != lastValidIndex)) {
+ (*block)[i++]->~T();
+ }
+ } while (block != this->tailBlock);
+ }
+
+ // Destroy all blocks that we own
+ if (this->tailBlock != nullptr) {
+ auto block = this->tailBlock;
+ do {
+ auto nextBlock = block->next;
+ if (block->dynamicallyAllocated) {
+ destroy(block);
+ }
+ else {
+ this->parent->add_block_to_free_list(block);
+ }
+ block = nextBlock;
+ } while (block != this->tailBlock);
+ }
+
+ // Destroy the block indices
+ auto header = static_cast<BlockIndexHeader*>(pr_blockIndexRaw);
+ while (header != nullptr) {
+ auto prev = static_cast<BlockIndexHeader*>(header->prev);
+ header->~BlockIndexHeader();
+ (Traits::free)(header);
+ header = prev;
+ }
+ }
+
+ template<AllocationMode allocMode, typename U>
+ inline bool enqueue(U&& element)
+ {
+ index_t currentTailIndex = this->tailIndex.load(std::memory_order::relaxed);
+ index_t newTailIndex = 1 + currentTailIndex;
+ if ((currentTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0) {
+ // We reached the end of a block, start a new one
+ auto startBlock = this->tailBlock;
+ auto originalBlockIndexSlotsUsed = pr_blockIndexSlotsUsed;
+ if (this->tailBlock != nullptr && this->tailBlock->next->ConcurrentQueue::Block::template is_empty<explicit_context>()) {
+ // We can re-use the block ahead of us, it's empty!
+ this->tailBlock = this->tailBlock->next;
+ this->tailBlock->ConcurrentQueue::Block::template reset_empty<explicit_context>();
+
+ // We'll put the block on the block index (guaranteed to be room since we're conceptually removing the
+ // last block from it first -- except instead of removing then adding, we can just overwrite).
+ // Note that there must be a valid block index here, since even if allocation failed in the ctor,
+ // it would have been re-attempted when adding the first block to the queue; since there is such
+ // a block, a block index must have been successfully allocated.
+ }
+ else {
+ // Whatever head value we see here is >= the last value we saw here (relatively),
+ // and <= its current value. Since we have the most recent tail, the head must be
+ // <= to it.
+ auto head = this->headIndex.load(std::memory_order::relaxed);
+ assert(!details::circular_less_than<index_t>(currentTailIndex, head));
+ if (!details::circular_less_than<index_t>(head, currentTailIndex + BLOCK_SIZE)
+ || (MAX_SUBQUEUE_SIZE != details::const_numeric_max<size_t>::value && (MAX_SUBQUEUE_SIZE == 0 || MAX_SUBQUEUE_SIZE - BLOCK_SIZE < currentTailIndex - head))) {
+ // We can't enqueue in another block because there's not enough leeway -- the
+ // tail could surpass the head by the time the block fills up! (Or we'll exceed
+ // the size limit, if the second part of the condition was true.)
+ return false;
+ }
+ // We're going to need a new block; check that the block index has room
+ if (pr_blockIndexRaw == nullptr || pr_blockIndexSlotsUsed == pr_blockIndexSize) {
+ // Hmm, the circular block index is already full -- we'll need
+ // to allocate a new index. Note pr_blockIndexRaw can only be nullptr if
+ // the initial allocation failed in the constructor.
+
+ MOODYCAMEL_CONSTEXPR_IF (allocMode == CannotAlloc) {
+ return false;
+ }
+ else if (!new_block_index(pr_blockIndexSlotsUsed)) {
+ return false;
+ }
+ }
+
+ // Insert a new block in the circular linked list
+ auto newBlock = this->parent->ConcurrentQueue::template requisition_block<allocMode>();
+ if (newBlock == nullptr) {
+ return false;
+ }
+#ifdef MCDBGQ_TRACKMEM
+ newBlock->owner = this;
+#endif
+ newBlock->ConcurrentQueue::Block::template reset_empty<explicit_context>();
+ if (this->tailBlock == nullptr) {
+ newBlock->next = newBlock;
+ }
+ else {
+ newBlock->next = this->tailBlock->next;
+ this->tailBlock->next = newBlock;
+ }
+ this->tailBlock = newBlock;
+ ++pr_blockIndexSlotsUsed;
+ }
+
+ MOODYCAMEL_CONSTEXPR_IF (!MOODYCAMEL_NOEXCEPT_CTOR(T, U, new ((T*)nullptr) T(std::forward<U>(element)))) {
+ // The constructor may throw. We want the element not to appear in the queue in
+ // that case (without corrupting the queue):
+ MOODYCAMEL_TRY {
+ new ((*this->tailBlock)[currentTailIndex]) T(std::forward<U>(element));
+ }
+ MOODYCAMEL_CATCH (...) {
+ // Revert change to the current block, but leave the new block available
+ // for next time
+ pr_blockIndexSlotsUsed = originalBlockIndexSlotsUsed;
+ this->tailBlock = startBlock == nullptr ? this->tailBlock : startBlock;
+ MOODYCAMEL_RETHROW;
+ }
+ }
+ else {
+ (void)startBlock;
+ (void)originalBlockIndexSlotsUsed;
+ }
+
+ // Add block to block index
+ auto& entry = blockIndex.load(std::memory_order::relaxed)->entries[pr_blockIndexFront];
+ entry.base = currentTailIndex;
+ entry.block = this->tailBlock;
+ blockIndex.load(std::memory_order::relaxed)->front.store(pr_blockIndexFront, std::memory_order::release);
+ pr_blockIndexFront = (pr_blockIndexFront + 1) & (pr_blockIndexSize - 1);
+
+ MOODYCAMEL_CONSTEXPR_IF (!MOODYCAMEL_NOEXCEPT_CTOR(T, U, new ((T*)nullptr) T(std::forward<U>(element)))) {
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+ }
+
+ // Enqueue
+ new ((*this->tailBlock)[currentTailIndex]) T(std::forward<U>(element));
+
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+
+ template<typename U>
+ bool dequeue(U& element)
+ {
+ auto tail = this->tailIndex.load(std::memory_order::relaxed);
+ auto overcommit = this->dequeueOvercommit.load(std::memory_order::relaxed);
+ if (details::circular_less_than<index_t>(this->dequeueOptimisticCount.load(std::memory_order::relaxed) - overcommit, tail)) {
+ // Might be something to dequeue, let's give it a try
+
+ // Note that this if is purely for performance purposes in the common case when the queue is
+ // empty and the values are eventually consistent -- we may enter here spuriously.
+
+ // Note that whatever the values of overcommit and tail are, they are not going to change (unless we
+ // change them) and must be the same value at this point (inside the if) as when the if condition was
+ // evaluated.
+
+ // We insert an acquire fence here to synchronize-with the release upon incrementing dequeueOvercommit below.
+ // This ensures that whatever the value we got loaded into overcommit, the load of dequeueOptisticCount in
+ // the fetch_add below will result in a value at least as recent as that (and therefore at least as large).
+ // Note that I believe a compiler (signal) fence here would be sufficient due to the nature of fetch_add (all
+ // read-modify-write operations are guaranteed to work on the latest value in the modification order), but
+ // unfortunately that can't be shown to be correct using only the C++11 standard.
+ // See http://stackoverflow.com/questions/18223161/what-are-the-c11-memory-ordering-guarantees-in-this-corner-case
+ std::atomic_thread_fence(std::memory_order::acquire);
+
+ // Increment optimistic counter, then check if it went over the boundary
+ auto myDequeueCount = this->dequeueOptimisticCount.fetch_add(1, std::memory_order::relaxed);
+
+ // Note that since dequeueOvercommit must be <= dequeueOptimisticCount (because dequeueOvercommit is only ever
+ // incremented after dequeueOptimisticCount -- this is enforced in the `else` block below), and since we now
+ // have a version of dequeueOptimisticCount that is at least as recent as overcommit (due to the release upon
+ // incrementing dequeueOvercommit and the acquire above that synchronizes with it), overcommit <= myDequeueCount.
+ // However, we can't assert this since both dequeueOptimisticCount and dequeueOvercommit may (independently)
+ // overflow; in such a case, though, the logic still holds since the difference between the two is maintained.
+
+ // Note that we reload tail here in case it changed; it will be the same value as before or greater, since
+ // this load is sequenced after (happens after) the earlier load above. This is supported by read-read
+ // coherency (as defined in the standard), explained here: http://en.cppreference.com/w/cpp/atomic/memory_order
+ tail = this->tailIndex.load(std::memory_order::acquire);
+ if ((details::likely)(details::circular_less_than<index_t>(myDequeueCount - overcommit, tail))) {
+ // Guaranteed to be at least one element to dequeue!
+
+ // Get the index. Note that since there's guaranteed to be at least one element, this
+ // will never exceed tail. We need to do an acquire-release fence here since it's possible
+ // that whatever condition got us to this point was for an earlier enqueued element (that
+ // we already see the memory effects for), but that by the time we increment somebody else
+ // has incremented it, and we need to see the memory effects for *that* element, which is
+ // in such a case is necessarily visible on the thread that incremented it in the first
+ // place with the more current condition (they must have acquired a tail that is at least
+ // as recent).
+ auto index = this->headIndex.fetch_add(1, std::memory_order::acq_rel);
+
+
+ // Determine which block the element is in
+
+ auto localBlockIndex = blockIndex.load(std::memory_order::acquire);
+ auto localBlockIndexHead = localBlockIndex->front.load(std::memory_order::acquire);
+
+ // We need to be careful here about subtracting and dividing because of index wrap-around.
+ // When an index wraps, we need to preserve the sign of the offset when dividing it by the
+ // block size (in order to get a correct signed block count offset in all cases):
+ auto headBase = localBlockIndex->entries[localBlockIndexHead].base;
+ auto blockBaseIndex = index & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ auto offset = static_cast<size_t>(static_cast<typename std::make_signed<index_t>::type>(blockBaseIndex - headBase) / BLOCK_SIZE);
+ auto block = localBlockIndex->entries[(localBlockIndexHead + offset) & (localBlockIndex->size - 1)].block;
+
+ // Dequeue
+ auto& el = *((*block)[index]);
+ if (!MOODYCAMEL_NOEXCEPT_ASSIGN(T, T&&, element = std::move(el))) {
+ // Make sure the element is still fully dequeued and destroyed even if the assignment
+ // throws
+ struct Guard {
+ Block* block;
+ index_t index;
+
+ ~Guard()
+ {
+ (*block)[index]->~T();
+ block->ConcurrentQueue::Block::template set_empty<explicit_context>(index);
+ }
+ } guard = { block, index };
+
+ element = std::move(el); // NOLINT
+ }
+ else {
+ element = std::move(el); // NOLINT
+ el.~T(); // NOLINT
+ block->ConcurrentQueue::Block::template set_empty<explicit_context>(index);
+ }
+
+ return true;
+ }
+ else {
+ // Wasn't anything to dequeue after all; make the effective dequeue count eventually consistent
+ this->dequeueOvercommit.fetch_add(1, std::memory_order::release); // Release so that the fetch_add on dequeueOptimisticCount is guaranteed to happen before this write
+ }
+ }
+
+ return false;
+ }
+
+ template<AllocationMode allocMode, typename It>
+ bool MOODYCAMEL_NO_TSAN enqueue_bulk(It itemFirst, size_t count)
+ {
+ // First, we need to make sure we have enough room to enqueue all of the elements;
+ // this means pre-allocating blocks and putting them in the block index (but only if
+ // all the allocations succeeded).
+ index_t startTailIndex = this->tailIndex.load(std::memory_order::relaxed);
+ auto startBlock = this->tailBlock;
+ auto originalBlockIndexFront = pr_blockIndexFront;
+ auto originalBlockIndexSlotsUsed = pr_blockIndexSlotsUsed;
+
+ Block* firstAllocatedBlock = nullptr;
+
+ // Figure out how many blocks we'll need to allocate, and do so
+ size_t blockBaseDiff = ((startTailIndex + count - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1)) - ((startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1));
+ index_t currentTailIndex = (startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ if (blockBaseDiff > 0) {
+ // Allocate as many blocks as possible from ahead
+ while (blockBaseDiff > 0 && this->tailBlock != nullptr && this->tailBlock->next != firstAllocatedBlock && this->tailBlock->next->ConcurrentQueue::Block::template is_empty<explicit_context>()) {
+ blockBaseDiff -= static_cast<index_t>(BLOCK_SIZE);
+ currentTailIndex += static_cast<index_t>(BLOCK_SIZE);
+
+ this->tailBlock = this->tailBlock->next;
+ firstAllocatedBlock = firstAllocatedBlock == nullptr ? this->tailBlock : firstAllocatedBlock;
+
+ auto& entry = blockIndex.load(std::memory_order::relaxed)->entries[pr_blockIndexFront];
+ entry.base = currentTailIndex;
+ entry.block = this->tailBlock;
+ pr_blockIndexFront = (pr_blockIndexFront + 1) & (pr_blockIndexSize - 1);
+ }
+
+ // Now allocate as many blocks as necessary from the block pool
+ while (blockBaseDiff > 0) {
+ blockBaseDiff -= static_cast<index_t>(BLOCK_SIZE);
+ currentTailIndex += static_cast<index_t>(BLOCK_SIZE);
+
+ auto head = this->headIndex.load(std::memory_order::relaxed);
+ assert(!details::circular_less_than<index_t>(currentTailIndex, head));
+ bool full = !details::circular_less_than<index_t>(head, currentTailIndex + BLOCK_SIZE) || (MAX_SUBQUEUE_SIZE != details::const_numeric_max<size_t>::value && (MAX_SUBQUEUE_SIZE == 0 || MAX_SUBQUEUE_SIZE - BLOCK_SIZE < currentTailIndex - head));
+ if (pr_blockIndexRaw == nullptr || pr_blockIndexSlotsUsed == pr_blockIndexSize || full) {
+ MOODYCAMEL_CONSTEXPR_IF (allocMode == CannotAlloc) {
+ // Failed to allocate, undo changes (but keep injected blocks)
+ pr_blockIndexFront = originalBlockIndexFront;
+ pr_blockIndexSlotsUsed = originalBlockIndexSlotsUsed;
+ this->tailBlock = startBlock == nullptr ? firstAllocatedBlock : startBlock;
+ return false;
+ }
+ else if (full || !new_block_index(originalBlockIndexSlotsUsed)) {
+ // Failed to allocate, undo changes (but keep injected blocks)
+ pr_blockIndexFront = originalBlockIndexFront;
+ pr_blockIndexSlotsUsed = originalBlockIndexSlotsUsed;
+ this->tailBlock = startBlock == nullptr ? firstAllocatedBlock : startBlock;
+ return false;
+ }
+
+ // pr_blockIndexFront is updated inside new_block_index, so we need to
+ // update our fallback value too (since we keep the new index even if we
+ // later fail)
+ originalBlockIndexFront = originalBlockIndexSlotsUsed;
+ }
+
+ // Insert a new block in the circular linked list
+ auto newBlock = this->parent->ConcurrentQueue::template requisition_block<allocMode>();
+ if (newBlock == nullptr) {
+ pr_blockIndexFront = originalBlockIndexFront;
+ pr_blockIndexSlotsUsed = originalBlockIndexSlotsUsed;
+ this->tailBlock = startBlock == nullptr ? firstAllocatedBlock : startBlock;
+ return false;
+ }
+
+#ifdef MCDBGQ_TRACKMEM
+ newBlock->owner = this;
+#endif
+ newBlock->ConcurrentQueue::Block::template set_all_empty<explicit_context>();
+ if (this->tailBlock == nullptr) {
+ newBlock->next = newBlock;
+ }
+ else {
+ newBlock->next = this->tailBlock->next;
+ this->tailBlock->next = newBlock;
+ }
+ this->tailBlock = newBlock;
+ firstAllocatedBlock = firstAllocatedBlock == nullptr ? this->tailBlock : firstAllocatedBlock;
+
+ ++pr_blockIndexSlotsUsed;
+
+ auto& entry = blockIndex.load(std::memory_order::relaxed)->entries[pr_blockIndexFront];
+ entry.base = currentTailIndex;
+ entry.block = this->tailBlock;
+ pr_blockIndexFront = (pr_blockIndexFront + 1) & (pr_blockIndexSize - 1);
+ }
+
+ // Excellent, all allocations succeeded. Reset each block's emptiness before we fill them up, and
+ // publish the new block index front
+ auto block = firstAllocatedBlock;
+ while (true) {
+ block->ConcurrentQueue::Block::template reset_empty<explicit_context>();
+ if (block == this->tailBlock) {
+ break;
+ }
+ block = block->next;
+ }
+
+ MOODYCAMEL_CONSTEXPR_IF (MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))) {
+ blockIndex.load(std::memory_order::relaxed)->front.store((pr_blockIndexFront - 1) & (pr_blockIndexSize - 1), std::memory_order::release);
+ }
+ }
+
+ // Enqueue, one block at a time
+ index_t newTailIndex = startTailIndex + static_cast<index_t>(count);
+ currentTailIndex = startTailIndex;
+ auto endBlock = this->tailBlock;
+ this->tailBlock = startBlock;
+ assert((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) != 0 || firstAllocatedBlock != nullptr || count == 0);
+ if ((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0 && firstAllocatedBlock != nullptr) {
+ this->tailBlock = firstAllocatedBlock;
+ }
+ while (true) {
+ index_t stopIndex = (currentTailIndex & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ if (details::circular_less_than<index_t>(newTailIndex, stopIndex)) {
+ stopIndex = newTailIndex;
+ }
+ MOODYCAMEL_CONSTEXPR_IF (MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))) {
+ while (currentTailIndex != stopIndex) {
+ new ((*this->tailBlock)[currentTailIndex++]) T(*itemFirst++);
+ }
+ }
+ else {
+ MOODYCAMEL_TRY {
+ while (currentTailIndex != stopIndex) {
+ // Must use copy constructor even if move constructor is available
+ // because we may have to revert if there's an exception.
+ // Sorry about the horrible templated next line, but it was the only way
+ // to disable moving *at compile time*, which is important because a type
+ // may only define a (noexcept) move constructor, and so calls to the
+ // cctor will not compile, even if they are in an if branch that will never
+ // be executed
+ new ((*this->tailBlock)[currentTailIndex]) T(details::nomove_if<(bool)!MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))>::eval(*itemFirst));
+ ++currentTailIndex;
+ ++itemFirst;
+ }
+ }
+ MOODYCAMEL_CATCH (...) {
+ // Oh dear, an exception's been thrown -- destroy the elements that
+ // were enqueued so far and revert the entire bulk operation (we'll keep
+ // any allocated blocks in our linked list for later, though).
+ auto constructedStopIndex = currentTailIndex;
+ auto lastBlockEnqueued = this->tailBlock;
+
+ pr_blockIndexFront = originalBlockIndexFront;
+ pr_blockIndexSlotsUsed = originalBlockIndexSlotsUsed;
+ this->tailBlock = startBlock == nullptr ? firstAllocatedBlock : startBlock;
+
+ if (!details::is_trivially_destructible<T>::value) {
+ auto block = startBlock;
+ if ((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0) {
+ block = firstAllocatedBlock;
+ }
+ currentTailIndex = startTailIndex;
+ while (true) {
+ stopIndex = (currentTailIndex & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ if (details::circular_less_than<index_t>(constructedStopIndex, stopIndex)) {
+ stopIndex = constructedStopIndex;
+ }
+ while (currentTailIndex != stopIndex) {
+ (*block)[currentTailIndex++]->~T();
+ }
+ if (block == lastBlockEnqueued) {
+ break;
+ }
+ block = block->next;
+ }
+ }
+ MOODYCAMEL_RETHROW;
+ }
+ }
+
+ if (this->tailBlock == endBlock) {
+ assert(currentTailIndex == newTailIndex);
+ break;
+ }
+ this->tailBlock = this->tailBlock->next;
+ }
+
+ MOODYCAMEL_CONSTEXPR_IF (!MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))) {
+ if (firstAllocatedBlock != nullptr)
+ blockIndex.load(std::memory_order::relaxed)->front.store((pr_blockIndexFront - 1) & (pr_blockIndexSize - 1), std::memory_order::release);
+ }
+
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+
+ template<typename It>
+ size_t dequeue_bulk(It& itemFirst, size_t max)
+ {
+ auto tail = this->tailIndex.load(std::memory_order::relaxed);
+ auto overcommit = this->dequeueOvercommit.load(std::memory_order::relaxed);
+ auto desiredCount = static_cast<size_t>(tail - (this->dequeueOptimisticCount.load(std::memory_order::relaxed) - overcommit));
+ if (details::circular_less_than<size_t>(0, desiredCount)) {
+ desiredCount = desiredCount < max ? desiredCount : max;
+ std::atomic_thread_fence(std::memory_order::acquire);
+
+ auto myDequeueCount = this->dequeueOptimisticCount.fetch_add(desiredCount, std::memory_order::relaxed);
+
+ tail = this->tailIndex.load(std::memory_order::acquire);
+ auto actualCount = static_cast<size_t>(tail - (myDequeueCount - overcommit));
+ if (details::circular_less_than<size_t>(0, actualCount)) {
+ actualCount = desiredCount < actualCount ? desiredCount : actualCount;
+ if (actualCount < desiredCount) {
+ this->dequeueOvercommit.fetch_add(desiredCount - actualCount, std::memory_order::release);
+ }
+
+ // Get the first index. Note that since there's guaranteed to be at least actualCount elements, this
+ // will never exceed tail.
+ auto firstIndex = this->headIndex.fetch_add(actualCount, std::memory_order::acq_rel);
+
+ // Determine which block the first element is in
+ auto localBlockIndex = blockIndex.load(std::memory_order::acquire);
+ auto localBlockIndexHead = localBlockIndex->front.load(std::memory_order::acquire);
+
+ auto headBase = localBlockIndex->entries[localBlockIndexHead].base;
+ auto firstBlockBaseIndex = firstIndex & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ auto offset = static_cast<size_t>(static_cast<typename std::make_signed<index_t>::type>(firstBlockBaseIndex - headBase) / BLOCK_SIZE);
+ auto indexIndex = (localBlockIndexHead + offset) & (localBlockIndex->size - 1);
+
+ // Iterate the blocks and dequeue
+ auto index = firstIndex;
+ do {
+ auto firstIndexInBlock = index;
+ index_t endIndex = (index & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ endIndex = details::circular_less_than<index_t>(firstIndex + static_cast<index_t>(actualCount), endIndex) ? firstIndex + static_cast<index_t>(actualCount) : endIndex;
+ auto block = localBlockIndex->entries[indexIndex].block;
+ if (MOODYCAMEL_NOEXCEPT_ASSIGN(T, T&&, details::deref_noexcept(itemFirst) = std::move((*(*block)[index])))) {
+ while (index != endIndex) {
+ auto& el = *((*block)[index]);
+ *itemFirst++ = std::move(el);
+ el.~T();
+ ++index;
+ }
+ }
+ else {
+ MOODYCAMEL_TRY {
+ while (index != endIndex) {
+ auto& el = *((*block)[index]);
+ *itemFirst = std::move(el);
+ ++itemFirst;
+ el.~T();
+ ++index;
+ }
+ }
+ MOODYCAMEL_CATCH (...) {
+ // It's too late to revert the dequeue, but we can make sure that all
+ // the dequeued objects are properly destroyed and the block index
+ // (and empty count) are properly updated before we propagate the exception
+ do {
+ block = localBlockIndex->entries[indexIndex].block;
+ while (index != endIndex) {
+ (*block)[index++]->~T();
+ }
+ block->ConcurrentQueue::Block::template set_many_empty<explicit_context>(firstIndexInBlock, static_cast<size_t>(endIndex - firstIndexInBlock));
+ indexIndex = (indexIndex + 1) & (localBlockIndex->size - 1);
+
+ firstIndexInBlock = index;
+ endIndex = (index & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ endIndex = details::circular_less_than<index_t>(firstIndex + static_cast<index_t>(actualCount), endIndex) ? firstIndex + static_cast<index_t>(actualCount) : endIndex;
+ } while (index != firstIndex + actualCount);
+
+ MOODYCAMEL_RETHROW;
+ }
+ }
+ block->ConcurrentQueue::Block::template set_many_empty<explicit_context>(firstIndexInBlock, static_cast<size_t>(endIndex - firstIndexInBlock));
+ indexIndex = (indexIndex + 1) & (localBlockIndex->size - 1);
+ } while (index != firstIndex + actualCount);
+
+ return actualCount;
+ }
+ else {
+ // Wasn't anything to dequeue after all; make the effective dequeue count eventually consistent
+ this->dequeueOvercommit.fetch_add(desiredCount, std::memory_order::release);
+ }
+ }
+
+ return 0;
+ }
+
+ private:
+ struct BlockIndexEntry
+ {
+ index_t base;
+ Block* block;
+ };
+
+ struct BlockIndexHeader
+ {
+ size_t size;
+ std::atomic<size_t> front; // Current slot (not next, like pr_blockIndexFront)
+ BlockIndexEntry* entries;
+ void* prev;
+ };
+
+
+ bool new_block_index(size_t numberOfFilledSlotsToExpose)
+ {
+ auto prevBlockSizeMask = pr_blockIndexSize - 1;
+
+ // Create the new block
+ pr_blockIndexSize <<= 1;
+ auto newRawPtr = static_cast<char*>((Traits::malloc)(sizeof(BlockIndexHeader) + std::alignment_of<BlockIndexEntry>::value - 1 + sizeof(BlockIndexEntry) * pr_blockIndexSize));
+ if (newRawPtr == nullptr) {
+ pr_blockIndexSize >>= 1; // Reset to allow graceful retry
+ return false;
+ }
+
+ auto newBlockIndexEntries = reinterpret_cast<BlockIndexEntry*>(details::align_for<BlockIndexEntry>(newRawPtr + sizeof(BlockIndexHeader)));
+
+ // Copy in all the old indices, if any
+ size_t j = 0;
+ if (pr_blockIndexSlotsUsed != 0) {
+ auto i = (pr_blockIndexFront - pr_blockIndexSlotsUsed) & prevBlockSizeMask;
+ do {
+ newBlockIndexEntries[j++] = pr_blockIndexEntries[i];
+ i = (i + 1) & prevBlockSizeMask;
+ } while (i != pr_blockIndexFront);
+ }
+
+ // Update everything
+ auto header = new (newRawPtr) BlockIndexHeader;
+ header->size = pr_blockIndexSize;
+ header->front.store(numberOfFilledSlotsToExpose - 1, std::memory_order::relaxed);
+ header->entries = newBlockIndexEntries;
+ header->prev = pr_blockIndexRaw; // we link the new block to the old one so we can free it later
+
+ pr_blockIndexFront = j;
+ pr_blockIndexEntries = newBlockIndexEntries;
+ pr_blockIndexRaw = newRawPtr;
+ blockIndex.store(header, std::memory_order::release);
+
+ return true;
+ }
+
+ private:
+ std::atomic<BlockIndexHeader*> blockIndex;
+
+ // To be used by producer only -- consumer must use the ones in referenced by blockIndex
+ size_t pr_blockIndexSlotsUsed;
+ size_t pr_blockIndexSize;
+ size_t pr_blockIndexFront; // Next slot (not current)
+ BlockIndexEntry* pr_blockIndexEntries;
+ void* pr_blockIndexRaw;
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ public:
+ ExplicitProducer* nextExplicitProducer;
+ private:
+#endif
+
+#ifdef MCDBGQ_TRACKMEM
+ friend struct MemStats;
+#endif
+ };
+
+
+ //////////////////////////////////
+ // Implicit queue
+ //////////////////////////////////
+
+ struct ImplicitProducer : public ProducerBase
+ {
+ ImplicitProducer(ConcurrentQueue* parent_) :
+ ProducerBase(parent_, false),
+ nextBlockIndexCapacity(IMPLICIT_INITIAL_INDEX_SIZE),
+ blockIndex(nullptr)
+ {
+ new_block_index();
+ }
+
+ ~ImplicitProducer()
+ {
+ // Note that since we're in the destructor we can assume that all enqueue/dequeue operations
+ // completed already; this means that all undequeued elements are placed contiguously across
+ // contiguous blocks, and that only the first and last remaining blocks can be only partially
+ // empty (all other remaining blocks must be completely full).
+
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ // Unregister ourselves for thread termination notification
+ if (!this->inactive.load(std::memory_order::relaxed)) {
+ details::ThreadExitNotifier::unsubscribe(&threadExitListener);
+ }
+#endif
+
+ // Destroy all remaining elements!
+ auto tail = this->tailIndex.load(std::memory_order::relaxed);
+ auto index = this->headIndex.load(std::memory_order::relaxed);
+ Block* block = nullptr;
+ assert(index == tail || details::circular_less_than(index, tail));
+ bool forceFreeLastBlock = index != tail; // If we enter the loop, then the last (tail) block will not be freed
+ while (index != tail) {
+ if ((index & static_cast<index_t>(BLOCK_SIZE - 1)) == 0 || block == nullptr) {
+ if (block != nullptr) {
+ // Free the old block
+ this->parent->add_block_to_free_list(block);
+ }
+
+ block = get_block_index_entry_for_index(index)->value.load(std::memory_order::relaxed);
+ }
+
+ ((*block)[index])->~T();
+ ++index;
+ }
+ // Even if the queue is empty, there's still one block that's not on the free list
+ // (unless the head index reached the end of it, in which case the tail will be poised
+ // to create a new block).
+ if (this->tailBlock != nullptr && (forceFreeLastBlock || (tail & static_cast<index_t>(BLOCK_SIZE - 1)) != 0)) {
+ this->parent->add_block_to_free_list(this->tailBlock);
+ }
+
+ // Destroy block index
+ auto localBlockIndex = blockIndex.load(std::memory_order::relaxed);
+ if (localBlockIndex != nullptr) {
+ for (size_t i = 0; i != localBlockIndex->capacity; ++i) {
+ localBlockIndex->index[i]->~BlockIndexEntry();
+ }
+ do {
+ auto prev = localBlockIndex->prev;
+ localBlockIndex->~BlockIndexHeader();
+ (Traits::free)(localBlockIndex);
+ localBlockIndex = prev;
+ } while (localBlockIndex != nullptr);
+ }
+ }
+
+ template<AllocationMode allocMode, typename U>
+ inline bool enqueue(U&& element)
+ {
+ index_t currentTailIndex = this->tailIndex.load(std::memory_order::relaxed);
+ index_t newTailIndex = 1 + currentTailIndex;
+ if ((currentTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0) {
+ // We reached the end of a block, start a new one
+ auto head = this->headIndex.load(std::memory_order::relaxed);
+ assert(!details::circular_less_than<index_t>(currentTailIndex, head));
+ if (!details::circular_less_than<index_t>(head, currentTailIndex + BLOCK_SIZE) || (MAX_SUBQUEUE_SIZE != details::const_numeric_max<size_t>::value && (MAX_SUBQUEUE_SIZE == 0 || MAX_SUBQUEUE_SIZE - BLOCK_SIZE < currentTailIndex - head))) {
+ return false;
+ }
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ // Find out where we'll be inserting this block in the block index
+ BlockIndexEntry* idxEntry;
+ if (!insert_block_index_entry<allocMode>(idxEntry, currentTailIndex)) {
+ return false;
+ }
+
+ // Get ahold of a new block
+ auto newBlock = this->parent->ConcurrentQueue::template requisition_block<allocMode>();
+ if (newBlock == nullptr) {
+ rewind_block_index_tail();
+ idxEntry->value.store(nullptr, std::memory_order::relaxed);
+ return false;
+ }
+#ifdef MCDBGQ_TRACKMEM
+ newBlock->owner = this;
+#endif
+ newBlock->ConcurrentQueue::Block::template reset_empty<implicit_context>();
+
+ MOODYCAMEL_CONSTEXPR_IF (!MOODYCAMEL_NOEXCEPT_CTOR(T, U, new ((T*)nullptr) T(std::forward<U>(element)))) {
+ // May throw, try to insert now before we publish the fact that we have this new block
+ MOODYCAMEL_TRY {
+ new ((*newBlock)[currentTailIndex]) T(std::forward<U>(element));
+ }
+ MOODYCAMEL_CATCH (...) {
+ rewind_block_index_tail();
+ idxEntry->value.store(nullptr, std::memory_order::relaxed);
+ this->parent->add_block_to_free_list(newBlock);
+ MOODYCAMEL_RETHROW;
+ }
+ }
+
+ // Insert the new block into the index
+ idxEntry->value.store(newBlock, std::memory_order::relaxed);
+
+ this->tailBlock = newBlock;
+
+ MOODYCAMEL_CONSTEXPR_IF (!MOODYCAMEL_NOEXCEPT_CTOR(T, U, new ((T*)nullptr) T(std::forward<U>(element)))) {
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+ }
+
+ // Enqueue
+ new ((*this->tailBlock)[currentTailIndex]) T(std::forward<U>(element));
+
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+
+ template<typename U>
+ bool dequeue(U& element)
+ {
+ // See ExplicitProducer::dequeue for rationale and explanation
+ index_t tail = this->tailIndex.load(std::memory_order::relaxed);
+ index_t overcommit = this->dequeueOvercommit.load(std::memory_order::relaxed);
+ if (details::circular_less_than<index_t>(this->dequeueOptimisticCount.load(std::memory_order::relaxed) - overcommit, tail)) {
+ std::atomic_thread_fence(std::memory_order::acquire);
+
+ index_t myDequeueCount = this->dequeueOptimisticCount.fetch_add(1, std::memory_order::relaxed);
+ tail = this->tailIndex.load(std::memory_order::acquire);
+ if ((details::likely)(details::circular_less_than<index_t>(myDequeueCount - overcommit, tail))) {
+ index_t index = this->headIndex.fetch_add(1, std::memory_order::acq_rel);
+
+ // Determine which block the element is in
+ auto entry = get_block_index_entry_for_index(index);
+
+ // Dequeue
+ auto block = entry->value.load(std::memory_order::relaxed);
+ auto& el = *((*block)[index]);
+
+ if (!MOODYCAMEL_NOEXCEPT_ASSIGN(T, T&&, element = std::move(el))) {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ // Note: Acquiring the mutex with every dequeue instead of only when a block
+ // is released is very sub-optimal, but it is, after all, purely debug code.
+ debug::DebugLock lock(producer->mutex);
+#endif
+ struct Guard {
+ Block* block;
+ index_t index;
+ BlockIndexEntry* entry;
+ ConcurrentQueue* parent;
+
+ ~Guard()
+ {
+ (*block)[index]->~T();
+ if (block->ConcurrentQueue::Block::template set_empty<implicit_context>(index)) {
+ entry->value.store(nullptr, std::memory_order::relaxed);
+ parent->add_block_to_free_list(block);
+ }
+ }
+ } guard = { block, index, entry, this->parent };
+
+ element = std::move(el); // NOLINT
+ }
+ else {
+ element = std::move(el); // NOLINT
+ el.~T(); // NOLINT
+
+ if (block->ConcurrentQueue::Block::template set_empty<implicit_context>(index)) {
+ {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ // Add the block back into the global free pool (and remove from block index)
+ entry->value.store(nullptr, std::memory_order::relaxed);
+ }
+ this->parent->add_block_to_free_list(block); // releases the above store
+ }
+ }
+
+ return true;
+ }
+ else {
+ this->dequeueOvercommit.fetch_add(1, std::memory_order::release);
+ }
+ }
+
+ return false;
+ }
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable: 4706) // assignment within conditional expression
+#endif
+ template<AllocationMode allocMode, typename It>
+ bool enqueue_bulk(It itemFirst, size_t count)
+ {
+ // First, we need to make sure we have enough room to enqueue all of the elements;
+ // this means pre-allocating blocks and putting them in the block index (but only if
+ // all the allocations succeeded).
+
+ // Note that the tailBlock we start off with may not be owned by us any more;
+ // this happens if it was filled up exactly to the top (setting tailIndex to
+ // the first index of the next block which is not yet allocated), then dequeued
+ // completely (putting it on the free list) before we enqueue again.
+
+ index_t startTailIndex = this->tailIndex.load(std::memory_order::relaxed);
+ auto startBlock = this->tailBlock;
+ Block* firstAllocatedBlock = nullptr;
+ auto endBlock = this->tailBlock;
+
+ // Figure out how many blocks we'll need to allocate, and do so
+ size_t blockBaseDiff = ((startTailIndex + count - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1)) - ((startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1));
+ index_t currentTailIndex = (startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ if (blockBaseDiff > 0) {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ do {
+ blockBaseDiff -= static_cast<index_t>(BLOCK_SIZE);
+ currentTailIndex += static_cast<index_t>(BLOCK_SIZE);
+
+ // Find out where we'll be inserting this block in the block index
+ BlockIndexEntry* idxEntry = nullptr; // initialization here unnecessary but compiler can't always tell
+ Block* newBlock;
+ bool indexInserted = false;
+ auto head = this->headIndex.load(std::memory_order::relaxed);
+ assert(!details::circular_less_than<index_t>(currentTailIndex, head));
+ bool full = !details::circular_less_than<index_t>(head, currentTailIndex + BLOCK_SIZE) || (MAX_SUBQUEUE_SIZE != details::const_numeric_max<size_t>::value && (MAX_SUBQUEUE_SIZE == 0 || MAX_SUBQUEUE_SIZE - BLOCK_SIZE < currentTailIndex - head));
+
+ if (full || !(indexInserted = insert_block_index_entry<allocMode>(idxEntry, currentTailIndex)) || (newBlock = this->parent->ConcurrentQueue::template requisition_block<allocMode>()) == nullptr) {
+ // Index allocation or block allocation failed; revert any other allocations
+ // and index insertions done so far for this operation
+ if (indexInserted) {
+ rewind_block_index_tail();
+ idxEntry->value.store(nullptr, std::memory_order::relaxed);
+ }
+ currentTailIndex = (startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ for (auto block = firstAllocatedBlock; block != nullptr; block = block->next) {
+ currentTailIndex += static_cast<index_t>(BLOCK_SIZE);
+ idxEntry = get_block_index_entry_for_index(currentTailIndex);
+ idxEntry->value.store(nullptr, std::memory_order::relaxed);
+ rewind_block_index_tail();
+ }
+ this->parent->add_blocks_to_free_list(firstAllocatedBlock);
+ this->tailBlock = startBlock;
+
+ return false;
+ }
+
+#ifdef MCDBGQ_TRACKMEM
+ newBlock->owner = this;
+#endif
+ newBlock->ConcurrentQueue::Block::template reset_empty<implicit_context>();
+ newBlock->next = nullptr;
+
+ // Insert the new block into the index
+ idxEntry->value.store(newBlock, std::memory_order::relaxed);
+
+ // Store the chain of blocks so that we can undo if later allocations fail,
+ // and so that we can find the blocks when we do the actual enqueueing
+ if ((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) != 0 || firstAllocatedBlock != nullptr) {
+ assert(this->tailBlock != nullptr);
+ this->tailBlock->next = newBlock;
+ }
+ this->tailBlock = newBlock;
+ endBlock = newBlock;
+ firstAllocatedBlock = firstAllocatedBlock == nullptr ? newBlock : firstAllocatedBlock;
+ } while (blockBaseDiff > 0);
+ }
+
+ // Enqueue, one block at a time
+ index_t newTailIndex = startTailIndex + static_cast<index_t>(count);
+ currentTailIndex = startTailIndex;
+ this->tailBlock = startBlock;
+ assert((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) != 0 || firstAllocatedBlock != nullptr || count == 0);
+ if ((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0 && firstAllocatedBlock != nullptr) {
+ this->tailBlock = firstAllocatedBlock;
+ }
+ while (true) {
+ index_t stopIndex = (currentTailIndex & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ if (details::circular_less_than<index_t>(newTailIndex, stopIndex)) {
+ stopIndex = newTailIndex;
+ }
+ MOODYCAMEL_CONSTEXPR_IF (MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))) {
+ while (currentTailIndex != stopIndex) {
+ new ((*this->tailBlock)[currentTailIndex++]) T(*itemFirst++);
+ }
+ }
+ else {
+ MOODYCAMEL_TRY {
+ while (currentTailIndex != stopIndex) {
+ new ((*this->tailBlock)[currentTailIndex]) T(details::nomove_if<(bool)!MOODYCAMEL_NOEXCEPT_CTOR(T, decltype(*itemFirst), new ((T*)nullptr) T(details::deref_noexcept(itemFirst)))>::eval(*itemFirst));
+ ++currentTailIndex;
+ ++itemFirst;
+ }
+ }
+ MOODYCAMEL_CATCH (...) {
+ auto constructedStopIndex = currentTailIndex;
+ auto lastBlockEnqueued = this->tailBlock;
+
+ if (!details::is_trivially_destructible<T>::value) {
+ auto block = startBlock;
+ if ((startTailIndex & static_cast<index_t>(BLOCK_SIZE - 1)) == 0) {
+ block = firstAllocatedBlock;
+ }
+ currentTailIndex = startTailIndex;
+ while (true) {
+ stopIndex = (currentTailIndex & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ if (details::circular_less_than<index_t>(constructedStopIndex, stopIndex)) {
+ stopIndex = constructedStopIndex;
+ }
+ while (currentTailIndex != stopIndex) {
+ (*block)[currentTailIndex++]->~T();
+ }
+ if (block == lastBlockEnqueued) {
+ break;
+ }
+ block = block->next;
+ }
+ }
+
+ currentTailIndex = (startTailIndex - 1) & ~static_cast<index_t>(BLOCK_SIZE - 1);
+ for (auto block = firstAllocatedBlock; block != nullptr; block = block->next) {
+ currentTailIndex += static_cast<index_t>(BLOCK_SIZE);
+ auto idxEntry = get_block_index_entry_for_index(currentTailIndex);
+ idxEntry->value.store(nullptr, std::memory_order::relaxed);
+ rewind_block_index_tail();
+ }
+ this->parent->add_blocks_to_free_list(firstAllocatedBlock);
+ this->tailBlock = startBlock;
+ MOODYCAMEL_RETHROW;
+ }
+ }
+
+ if (this->tailBlock == endBlock) {
+ assert(currentTailIndex == newTailIndex);
+ break;
+ }
+ this->tailBlock = this->tailBlock->next;
+ }
+ this->tailIndex.store(newTailIndex, std::memory_order::release);
+ return true;
+ }
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+ template<typename It>
+ size_t dequeue_bulk(It& itemFirst, size_t max)
+ {
+ auto tail = this->tailIndex.load(std::memory_order::relaxed);
+ auto overcommit = this->dequeueOvercommit.load(std::memory_order::relaxed);
+ auto desiredCount = static_cast<size_t>(tail - (this->dequeueOptimisticCount.load(std::memory_order::relaxed) - overcommit));
+ if (details::circular_less_than<size_t>(0, desiredCount)) {
+ desiredCount = desiredCount < max ? desiredCount : max;
+ std::atomic_thread_fence(std::memory_order::acquire);
+
+ auto myDequeueCount = this->dequeueOptimisticCount.fetch_add(desiredCount, std::memory_order::relaxed);
+
+ tail = this->tailIndex.load(std::memory_order::acquire);
+ auto actualCount = static_cast<size_t>(tail - (myDequeueCount - overcommit));
+ if (details::circular_less_than<size_t>(0, actualCount)) {
+ actualCount = desiredCount < actualCount ? desiredCount : actualCount;
+ if (actualCount < desiredCount) {
+ this->dequeueOvercommit.fetch_add(desiredCount - actualCount, std::memory_order::release);
+ }
+
+ // Get the first index. Note that since there's guaranteed to be at least actualCount elements, this
+ // will never exceed tail.
+ auto firstIndex = this->headIndex.fetch_add(actualCount, std::memory_order::acq_rel);
+
+ // Iterate the blocks and dequeue
+ auto index = firstIndex;
+ BlockIndexHeader* localBlockIndex;
+ auto indexIndex = get_block_index_index_for_index(index, localBlockIndex);
+ do {
+ auto blockStartIndex = index;
+ index_t endIndex = (index & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ endIndex = details::circular_less_than<index_t>(firstIndex + static_cast<index_t>(actualCount), endIndex) ? firstIndex + static_cast<index_t>(actualCount) : endIndex;
+
+ auto entry = localBlockIndex->index[indexIndex];
+ auto block = entry->value.load(std::memory_order::relaxed);
+ if (MOODYCAMEL_NOEXCEPT_ASSIGN(T, T&&, details::deref_noexcept(itemFirst) = std::move((*(*block)[index])))) {
+ while (index != endIndex) {
+ auto& el = *((*block)[index]);
+ *itemFirst++ = std::move(el);
+ el.~T();
+ ++index;
+ }
+ }
+ else {
+ MOODYCAMEL_TRY {
+ while (index != endIndex) {
+ auto& el = *((*block)[index]);
+ *itemFirst = std::move(el);
+ ++itemFirst;
+ el.~T();
+ ++index;
+ }
+ }
+ MOODYCAMEL_CATCH (...) {
+ do {
+ entry = localBlockIndex->index[indexIndex];
+ block = entry->value.load(std::memory_order::relaxed);
+ while (index != endIndex) {
+ (*block)[index++]->~T();
+ }
+
+ if (block->ConcurrentQueue::Block::template set_many_empty<implicit_context>(blockStartIndex, static_cast<size_t>(endIndex - blockStartIndex))) {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ entry->value.store(nullptr, std::memory_order::relaxed);
+ this->parent->add_block_to_free_list(block);
+ }
+ indexIndex = (indexIndex + 1) & (localBlockIndex->capacity - 1);
+
+ blockStartIndex = index;
+ endIndex = (index & ~static_cast<index_t>(BLOCK_SIZE - 1)) + static_cast<index_t>(BLOCK_SIZE);
+ endIndex = details::circular_less_than<index_t>(firstIndex + static_cast<index_t>(actualCount), endIndex) ? firstIndex + static_cast<index_t>(actualCount) : endIndex;
+ } while (index != firstIndex + actualCount);
+
+ MOODYCAMEL_RETHROW;
+ }
+ }
+ if (block->ConcurrentQueue::Block::template set_many_empty<implicit_context>(blockStartIndex, static_cast<size_t>(endIndex - blockStartIndex))) {
+ {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ // Note that the set_many_empty above did a release, meaning that anybody who acquires the block
+ // we're about to free can use it safely since our writes (and reads!) will have happened-before then.
+ entry->value.store(nullptr, std::memory_order::relaxed);
+ }
+ this->parent->add_block_to_free_list(block); // releases the above store
+ }
+ indexIndex = (indexIndex + 1) & (localBlockIndex->capacity - 1);
+ } while (index != firstIndex + actualCount);
+
+ return actualCount;
+ }
+ else {
+ this->dequeueOvercommit.fetch_add(desiredCount, std::memory_order::release);
+ }
+ }
+
+ return 0;
+ }
+
+ private:
+ // The block size must be > 1, so any number with the low bit set is an invalid block base index
+ static const index_t INVALID_BLOCK_BASE = 1;
+
+ struct BlockIndexEntry
+ {
+ std::atomic<index_t> key;
+ std::atomic<Block*> value;
+ };
+
+ struct BlockIndexHeader
+ {
+ size_t capacity;
+ std::atomic<size_t> tail;
+ BlockIndexEntry* entries;
+ BlockIndexEntry** index;
+ BlockIndexHeader* prev;
+ };
+
+ template<AllocationMode allocMode>
+ inline bool insert_block_index_entry(BlockIndexEntry*& idxEntry, index_t blockStartIndex)
+ {
+ auto localBlockIndex = blockIndex.load(std::memory_order::relaxed); // We're the only writer thread, relaxed is OK
+ if (localBlockIndex == nullptr) {
+ return false; // this can happen if new_block_index failed in the constructor
+ }
+ size_t newTail = (localBlockIndex->tail.load(std::memory_order::relaxed) + 1) & (localBlockIndex->capacity - 1);
+ idxEntry = localBlockIndex->index[newTail];
+ if (idxEntry->key.load(std::memory_order::relaxed) == INVALID_BLOCK_BASE ||
+ idxEntry->value.load(std::memory_order::relaxed) == nullptr) {
+
+ idxEntry->key.store(blockStartIndex, std::memory_order::relaxed);
+ localBlockIndex->tail.store(newTail, std::memory_order::release);
+ return true;
+ }
+
+ // No room in the old block index, try to allocate another one!
+ MOODYCAMEL_CONSTEXPR_IF (allocMode == CannotAlloc) {
+ return false;
+ }
+ else if (!new_block_index()) {
+ return false;
+ }
+ localBlockIndex = blockIndex.load(std::memory_order::relaxed);
+ newTail = (localBlockIndex->tail.load(std::memory_order::relaxed) + 1) & (localBlockIndex->capacity - 1);
+ idxEntry = localBlockIndex->index[newTail];
+ assert(idxEntry->key.load(std::memory_order::relaxed) == INVALID_BLOCK_BASE);
+ idxEntry->key.store(blockStartIndex, std::memory_order::relaxed);
+ localBlockIndex->tail.store(newTail, std::memory_order::release);
+ return true;
+ }
+
+ inline void rewind_block_index_tail()
+ {
+ auto localBlockIndex = blockIndex.load(std::memory_order::relaxed);
+ localBlockIndex->tail.store((localBlockIndex->tail.load(std::memory_order::relaxed) - 1) & (localBlockIndex->capacity - 1), std::memory_order::relaxed);
+ }
+
+ inline BlockIndexEntry* get_block_index_entry_for_index(index_t index) const
+ {
+ BlockIndexHeader* localBlockIndex;
+ auto idx = get_block_index_index_for_index(index, localBlockIndex);
+ return localBlockIndex->index[idx];
+ }
+
+ inline size_t get_block_index_index_for_index(index_t index, BlockIndexHeader*& localBlockIndex) const
+ {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ debug::DebugLock lock(mutex);
+#endif
+ index &= ~static_cast<index_t>(BLOCK_SIZE - 1);
+ localBlockIndex = blockIndex.load(std::memory_order::acquire);
+ auto tail = localBlockIndex->tail.load(std::memory_order::acquire);
+ auto tailBase = localBlockIndex->index[tail]->key.load(std::memory_order::relaxed);
+ assert(tailBase != INVALID_BLOCK_BASE);
+ // Note: Must use division instead of shift because the index may wrap around, causing a negative
+ // offset, whose negativity we want to preserve
+ auto offset = static_cast<size_t>(static_cast<typename std::make_signed<index_t>::type>(index - tailBase) / BLOCK_SIZE);
+ size_t idx = (tail + offset) & (localBlockIndex->capacity - 1);
+ assert(localBlockIndex->index[idx]->key.load(std::memory_order::relaxed) == index && localBlockIndex->index[idx]->value.load(std::memory_order::relaxed) != nullptr);
+ return idx;
+ }
+
+ bool new_block_index()
+ {
+ auto prev = blockIndex.load(std::memory_order::relaxed);
+ size_t prevCapacity = prev == nullptr ? 0 : prev->capacity;
+ auto entryCount = prev == nullptr ? nextBlockIndexCapacity : prevCapacity;
+ auto raw = static_cast<char*>((Traits::malloc)(
+ sizeof(BlockIndexHeader) +
+ std::alignment_of<BlockIndexEntry>::value - 1 + sizeof(BlockIndexEntry) * entryCount +
+ std::alignment_of<BlockIndexEntry*>::value - 1 + sizeof(BlockIndexEntry*) * nextBlockIndexCapacity));
+ if (raw == nullptr) {
+ return false;
+ }
+
+ auto header = new (raw) BlockIndexHeader;
+ auto entries = reinterpret_cast<BlockIndexEntry*>(details::align_for<BlockIndexEntry>(raw + sizeof(BlockIndexHeader)));
+ auto index = reinterpret_cast<BlockIndexEntry**>(details::align_for<BlockIndexEntry*>(reinterpret_cast<char*>(entries) + sizeof(BlockIndexEntry) * entryCount));
+ if (prev != nullptr) {
+ auto prevTail = prev->tail.load(std::memory_order::relaxed);
+ auto prevPos = prevTail;
+ size_t i = 0;
+ do {
+ prevPos = (prevPos + 1) & (prev->capacity - 1);
+ index[i++] = prev->index[prevPos];
+ } while (prevPos != prevTail);
+ assert(i == prevCapacity);
+ }
+ for (size_t i = 0; i != entryCount; ++i) {
+ new (entries + i) BlockIndexEntry;
+ entries[i].key.store(INVALID_BLOCK_BASE, std::memory_order::relaxed);
+ index[prevCapacity + i] = entries + i;
+ }
+ header->prev = prev;
+ header->entries = entries;
+ header->index = index;
+ header->capacity = nextBlockIndexCapacity;
+ header->tail.store((prevCapacity - 1) & (nextBlockIndexCapacity - 1), std::memory_order::relaxed);
+
+ blockIndex.store(header, std::memory_order::release);
+
+ nextBlockIndexCapacity <<= 1;
+
+ return true;
+ }
+
+ private:
+ size_t nextBlockIndexCapacity;
+ std::atomic<BlockIndexHeader*> blockIndex;
+
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ public:
+ details::ThreadExitListener threadExitListener;
+ private:
+#endif
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ public:
+ ImplicitProducer* nextImplicitProducer;
+ private:
+#endif
+
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODBLOCKINDEX
+ mutable debug::DebugMutex mutex;
+#endif
+#ifdef MCDBGQ_TRACKMEM
+ friend struct MemStats;
+#endif
+ };
+
+
+ //////////////////////////////////
+ // Block pool manipulation
+ //////////////////////////////////
+
+ void populate_initial_block_list(size_t blockCount)
+ {
+ initialBlockPoolSize = blockCount;
+ if (initialBlockPoolSize == 0) {
+ initialBlockPool = nullptr;
+ return;
+ }
+
+ initialBlockPool = create_array<Block>(blockCount);
+ if (initialBlockPool == nullptr) {
+ initialBlockPoolSize = 0;
+ }
+ for (size_t i = 0; i < initialBlockPoolSize; ++i) {
+ initialBlockPool[i].dynamicallyAllocated = false;
+ }
+ }
+
+ inline Block* try_get_block_from_initial_pool()
+ {
+ if (initialBlockPoolIndex.load(std::memory_order::relaxed) >= initialBlockPoolSize) {
+ return nullptr;
+ }
+
+ auto index = initialBlockPoolIndex.fetch_add(1, std::memory_order::relaxed);
+
+ return index < initialBlockPoolSize ? (initialBlockPool + index) : nullptr;
+ }
+
+ inline void add_block_to_free_list(Block* block)
+ {
+#ifdef MCDBGQ_TRACKMEM
+ block->owner = nullptr;
+#endif
+ freeList.add(block);
+ }
+
+ inline void add_blocks_to_free_list(Block* block)
+ {
+ while (block != nullptr) {
+ auto next = block->next;
+ add_block_to_free_list(block);
+ block = next;
+ }
+ }
+
+ inline Block* try_get_block_from_free_list()
+ {
+ return freeList.try_get();
+ }
+
+ // Gets a free block from one of the memory pools, or allocates a new one (if applicable)
+ template<AllocationMode canAlloc>
+ Block* requisition_block()
+ {
+ auto block = try_get_block_from_initial_pool();
+ if (block != nullptr) {
+ return block;
+ }
+
+ block = try_get_block_from_free_list();
+ if (block != nullptr) {
+ return block;
+ }
+
+ MOODYCAMEL_CONSTEXPR_IF (canAlloc == CanAlloc) {
+ return create<Block>();
+ }
+ else {
+ return nullptr;
+ }
+ }
+
+
+#ifdef MCDBGQ_TRACKMEM
+ public:
+ struct MemStats {
+ size_t allocatedBlocks;
+ size_t usedBlocks;
+ size_t freeBlocks;
+ size_t ownedBlocksExplicit;
+ size_t ownedBlocksImplicit;
+ size_t implicitProducers;
+ size_t explicitProducers;
+ size_t elementsEnqueued;
+ size_t blockClassBytes;
+ size_t queueClassBytes;
+ size_t implicitBlockIndexBytes;
+ size_t explicitBlockIndexBytes;
+
+ friend class ConcurrentQueue;
+
+ private:
+ static MemStats getFor(ConcurrentQueue* q)
+ {
+ MemStats stats = { 0 };
+
+ stats.elementsEnqueued = q->size_approx();
+
+ auto block = q->freeList.head_unsafe();
+ while (block != nullptr) {
+ ++stats.allocatedBlocks;
+ ++stats.freeBlocks;
+ block = block->freeListNext.load(std::memory_order::relaxed);
+ }
+
+ for (auto ptr = q->producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ bool implicit = dynamic_cast<ImplicitProducer*>(ptr) != nullptr;
+ stats.implicitProducers += implicit ? 1 : 0;
+ stats.explicitProducers += implicit ? 0 : 1;
+
+ if (implicit) {
+ auto prod = static_cast<ImplicitProducer*>(ptr);
+ stats.queueClassBytes += sizeof(ImplicitProducer);
+ auto head = prod->headIndex.load(std::memory_order::relaxed);
+ auto tail = prod->tailIndex.load(std::memory_order::relaxed);
+ auto hash = prod->blockIndex.load(std::memory_order::relaxed);
+ if (hash != nullptr) {
+ for (size_t i = 0; i != hash->capacity; ++i) {
+ if (hash->index[i]->key.load(std::memory_order::relaxed) != ImplicitProducer::INVALID_BLOCK_BASE && hash->index[i]->value.load(std::memory_order::relaxed) != nullptr) {
+ ++stats.allocatedBlocks;
+ ++stats.ownedBlocksImplicit;
+ }
+ }
+ stats.implicitBlockIndexBytes += hash->capacity * sizeof(typename ImplicitProducer::BlockIndexEntry);
+ for (; hash != nullptr; hash = hash->prev) {
+ stats.implicitBlockIndexBytes += sizeof(typename ImplicitProducer::BlockIndexHeader) + hash->capacity * sizeof(typename ImplicitProducer::BlockIndexEntry*);
+ }
+ }
+ for (; details::circular_less_than<index_t>(head, tail); head += BLOCK_SIZE) {
+ //auto block = prod->get_block_index_entry_for_index(head);
+ ++stats.usedBlocks;
+ }
+ }
+ else {
+ auto prod = static_cast<ExplicitProducer*>(ptr);
+ stats.queueClassBytes += sizeof(ExplicitProducer);
+ auto tailBlock = prod->tailBlock;
+ bool wasNonEmpty = false;
+ if (tailBlock != nullptr) {
+ auto block = tailBlock;
+ do {
+ ++stats.allocatedBlocks;
+ if (!block->ConcurrentQueue::Block::template is_empty<explicit_context>() || wasNonEmpty) {
+ ++stats.usedBlocks;
+ wasNonEmpty = wasNonEmpty || block != tailBlock;
+ }
+ ++stats.ownedBlocksExplicit;
+ block = block->next;
+ } while (block != tailBlock);
+ }
+ auto index = prod->blockIndex.load(std::memory_order::relaxed);
+ while (index != nullptr) {
+ stats.explicitBlockIndexBytes += sizeof(typename ExplicitProducer::BlockIndexHeader) + index->size * sizeof(typename ExplicitProducer::BlockIndexEntry);
+ index = static_cast<typename ExplicitProducer::BlockIndexHeader*>(index->prev);
+ }
+ }
+ }
+
+ auto freeOnInitialPool = q->initialBlockPoolIndex.load(std::memory_order::relaxed) >= q->initialBlockPoolSize ? 0 : q->initialBlockPoolSize - q->initialBlockPoolIndex.load(std::memory_order::relaxed);
+ stats.allocatedBlocks += freeOnInitialPool;
+ stats.freeBlocks += freeOnInitialPool;
+
+ stats.blockClassBytes = sizeof(Block) * stats.allocatedBlocks;
+ stats.queueClassBytes += sizeof(ConcurrentQueue);
+
+ return stats;
+ }
+ };
+
+ // For debugging only. Not thread-safe.
+ MemStats getMemStats()
+ {
+ return MemStats::getFor(this);
+ }
+ private:
+ friend struct MemStats;
+#endif
+
+
+ //////////////////////////////////
+ // Producer list manipulation
+ //////////////////////////////////
+
+ ProducerBase* recycle_or_create_producer(bool isExplicit)
+ {
+ bool recycled;
+ return recycle_or_create_producer(isExplicit, recycled);
+ }
+
+ ProducerBase* recycle_or_create_producer(bool isExplicit, bool& recycled)
+ {
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODHASH
+ debug::DebugLock lock(implicitProdMutex);
+#endif
+ // Try to re-use one first
+ for (auto ptr = producerListTail.load(std::memory_order::acquire); ptr != nullptr; ptr = ptr->next_prod()) {
+ if (ptr->inactive.load(std::memory_order::relaxed) && ptr->isExplicit == isExplicit) {
+ bool expected = true;
+ if (ptr->inactive.compare_exchange_strong(expected, /* desired */ false, std::memory_order::acquire, std::memory_order::relaxed)) {
+ // We caught one! It's been marked as activated, the caller can have it
+ recycled = true;
+ return ptr;
+ }
+ }
+ }
+
+ recycled = false;
+ return add_producer(isExplicit ? static_cast<ProducerBase*>(create<ExplicitProducer>(this)) : create<ImplicitProducer>(this));
+ }
+
+ ProducerBase* add_producer(ProducerBase* producer)
+ {
+ // Handle failed memory allocation
+ if (producer == nullptr) {
+ return nullptr;
+ }
+
+ producerCount.fetch_add(1, std::memory_order::relaxed);
+
+ // Add it to the lock-free list
+ auto prevTail = producerListTail.load(std::memory_order::relaxed);
+ do {
+ producer->next = prevTail;
+ } while (!producerListTail.compare_exchange_weak(prevTail, producer, std::memory_order::release, std::memory_order::relaxed));
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ if (producer->isExplicit) {
+ auto prevTailExplicit = explicitProducers.load(std::memory_order::relaxed);
+ do {
+ static_cast<ExplicitProducer*>(producer)->nextExplicitProducer = prevTailExplicit;
+ } while (!explicitProducers.compare_exchange_weak(prevTailExplicit, static_cast<ExplicitProducer*>(producer), std::memory_order::release, std::memory_order::relaxed));
+ }
+ else {
+ auto prevTailImplicit = implicitProducers.load(std::memory_order::relaxed);
+ do {
+ static_cast<ImplicitProducer*>(producer)->nextImplicitProducer = prevTailImplicit;
+ } while (!implicitProducers.compare_exchange_weak(prevTailImplicit, static_cast<ImplicitProducer*>(producer), std::memory_order::release, std::memory_order::relaxed));
+ }
+#endif
+
+ return producer;
+ }
+
+ void reown_producers()
+ {
+ // After another instance is moved-into/swapped-with this one, all the
+ // producers we stole still think their parents are the other queue.
+ // So fix them up!
+ for (auto ptr = producerListTail.load(std::memory_order::relaxed); ptr != nullptr; ptr = ptr->next_prod()) {
+ ptr->parent = this;
+ }
+ }
+
+
+ //////////////////////////////////
+ // Implicit producer hash
+ //////////////////////////////////
+
+ struct ImplicitProducerKVP
+ {
+ std::atomic<details::thread_id_t> key;
+ ImplicitProducer* value; // No need for atomicity since it's only read by the thread that sets it in the first place
+
+ ImplicitProducerKVP() : value(nullptr) { }
+
+ ImplicitProducerKVP(ImplicitProducerKVP&& other) MOODYCAMEL_NOEXCEPT
+ {
+ key.store(other.key.load(std::memory_order::relaxed), std::memory_order::relaxed);
+ value = other.value;
+ }
+
+ inline ImplicitProducerKVP& operator=(ImplicitProducerKVP&& other) MOODYCAMEL_NOEXCEPT
+ {
+ swap(other);
+ return *this;
+ }
+
+ inline void swap(ImplicitProducerKVP& other) MOODYCAMEL_NOEXCEPT
+ {
+ if (this != &other) {
+ details::swap_relaxed(key, other.key);
+ std::swap(value, other.value);
+ }
+ }
+ };
+
+ template<typename XT, typename XTraits>
+ friend void moodycamel::swap(typename ConcurrentQueue<XT, XTraits>::ImplicitProducerKVP&, typename ConcurrentQueue<XT, XTraits>::ImplicitProducerKVP&) MOODYCAMEL_NOEXCEPT;
+
+ struct ImplicitProducerHash
+ {
+ size_t capacity;
+ ImplicitProducerKVP* entries;
+ ImplicitProducerHash* prev;
+ };
+
+ inline void populate_initial_implicit_producer_hash()
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) {
+ return;
+ }
+ else {
+ implicitProducerHashCount.store(0, std::memory_order::relaxed);
+ auto hash = &initialImplicitProducerHash;
+ hash->capacity = INITIAL_IMPLICIT_PRODUCER_HASH_SIZE;
+ hash->entries = &initialImplicitProducerHashEntries[0];
+ for (size_t i = 0; i != INITIAL_IMPLICIT_PRODUCER_HASH_SIZE; ++i) {
+ initialImplicitProducerHashEntries[i].key.store(details::invalid_thread_id, std::memory_order::relaxed);
+ }
+ hash->prev = nullptr;
+ implicitProducerHash.store(hash, std::memory_order::relaxed);
+ }
+ }
+
+ void swap_implicit_producer_hashes(ConcurrentQueue& other)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (INITIAL_IMPLICIT_PRODUCER_HASH_SIZE == 0) {
+ return;
+ }
+ else {
+ // Swap (assumes our implicit producer hash is initialized)
+ initialImplicitProducerHashEntries.swap(other.initialImplicitProducerHashEntries);
+ initialImplicitProducerHash.entries = &initialImplicitProducerHashEntries[0];
+ other.initialImplicitProducerHash.entries = &other.initialImplicitProducerHashEntries[0];
+
+ details::swap_relaxed(implicitProducerHashCount, other.implicitProducerHashCount);
+
+ details::swap_relaxed(implicitProducerHash, other.implicitProducerHash);
+ if (implicitProducerHash.load(std::memory_order::relaxed) == &other.initialImplicitProducerHash) {
+ implicitProducerHash.store(&initialImplicitProducerHash, std::memory_order::relaxed);
+ }
+ else {
+ ImplicitProducerHash* hash;
+ for (hash = implicitProducerHash.load(std::memory_order::relaxed); hash->prev != &other.initialImplicitProducerHash; hash = hash->prev) {
+ continue;
+ }
+ hash->prev = &initialImplicitProducerHash;
+ }
+ if (other.implicitProducerHash.load(std::memory_order::relaxed) == &initialImplicitProducerHash) {
+ other.implicitProducerHash.store(&other.initialImplicitProducerHash, std::memory_order::relaxed);
+ }
+ else {
+ ImplicitProducerHash* hash;
+ for (hash = other.implicitProducerHash.load(std::memory_order::relaxed); hash->prev != &initialImplicitProducerHash; hash = hash->prev) {
+ continue;
+ }
+ hash->prev = &other.initialImplicitProducerHash;
+ }
+ }
+ }
+
+ // Only fails (returns nullptr) if memory allocation fails
+ ImplicitProducer* get_or_add_implicit_producer()
+ {
+ // Note that since the data is essentially thread-local (key is thread ID),
+ // there's a reduced need for fences (memory ordering is already consistent
+ // for any individual thread), except for the current table itself.
+
+ // Start by looking for the thread ID in the current and all previous hash tables.
+ // If it's not found, it must not be in there yet, since this same thread would
+ // have added it previously to one of the tables that we traversed.
+
+ // Code and algorithm adapted from http://preshing.com/20130605/the-worlds-simplest-lock-free-hash-table
+
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODHASH
+ debug::DebugLock lock(implicitProdMutex);
+#endif
+
+ auto id = details::thread_id();
+ auto hashedId = details::hash_thread_id(id);
+
+ auto mainHash = implicitProducerHash.load(std::memory_order::acquire);
+ assert(mainHash != nullptr); // silence clang-tidy and MSVC warnings (hash cannot be null)
+ for (auto hash = mainHash; hash != nullptr; hash = hash->prev) {
+ // Look for the id in this hash
+ auto index = hashedId;
+ while (true) { // Not an infinite loop because at least one slot is free in the hash table
+ index &= hash->capacity - 1;
+
+ auto probedKey = hash->entries[index].key.load(std::memory_order::relaxed);
+ if (probedKey == id) {
+ // Found it! If we had to search several hashes deep, though, we should lazily add it
+ // to the current main hash table to avoid the extended search next time.
+ // Note there's guaranteed to be room in the current hash table since every subsequent
+ // table implicitly reserves space for all previous tables (there's only one
+ // implicitProducerHashCount).
+ auto value = hash->entries[index].value;
+ if (hash != mainHash) {
+ index = hashedId;
+ while (true) {
+ index &= mainHash->capacity - 1;
+ probedKey = mainHash->entries[index].key.load(std::memory_order::relaxed);
+ auto empty = details::invalid_thread_id;
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ auto reusable = details::invalid_thread_id2;
+ if ((probedKey == empty && mainHash->entries[index].key.compare_exchange_strong(empty, id, std::memory_order::relaxed, std::memory_order::relaxed)) ||
+ (probedKey == reusable && mainHash->entries[index].key.compare_exchange_strong(reusable, id, std::memory_order::acquire, std::memory_order::acquire))) {
+#else
+ if ((probedKey == empty && mainHash->entries[index].key.compare_exchange_strong(empty, id, std::memory_order::relaxed, std::memory_order::relaxed))) {
+#endif
+ mainHash->entries[index].value = value;
+ break;
+ }
+ ++index;
+ }
+ }
+
+ return value;
+ }
+ if (probedKey == details::invalid_thread_id) {
+ break; // Not in this hash table
+ }
+ ++index;
+ }
+ }
+
+ // Insert!
+ auto newCount = 1 + implicitProducerHashCount.fetch_add(1, std::memory_order::relaxed);
+ while (true) {
+ // NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
+ if (newCount >= (mainHash->capacity >> 1) && !implicitProducerHashResizeInProgress.test_and_set(std::memory_order::acquire)) {
+ // We've acquired the resize lock, try to allocate a bigger hash table.
+ // Note the acquire fence synchronizes with the release fence at the end of this block, and hence when
+ // we reload implicitProducerHash it must be the most recent version (it only gets changed within this
+ // locked block).
+ mainHash = implicitProducerHash.load(std::memory_order::acquire);
+ if (newCount >= (mainHash->capacity >> 1)) {
+ auto newCapacity = mainHash->capacity << 1;
+ while (newCount >= (newCapacity >> 1)) {
+ newCapacity <<= 1;
+ }
+ auto raw = static_cast<char*>((Traits::malloc)(sizeof(ImplicitProducerHash) + std::alignment_of<ImplicitProducerKVP>::value - 1 + sizeof(ImplicitProducerKVP) * newCapacity));
+ if (raw == nullptr) {
+ // Allocation failed
+ implicitProducerHashCount.fetch_sub(1, std::memory_order::relaxed);
+ implicitProducerHashResizeInProgress.clear(std::memory_order::relaxed);
+ return nullptr;
+ }
+
+ auto newHash = new (raw) ImplicitProducerHash;
+ newHash->capacity = (size_t)newCapacity;
+ newHash->entries = reinterpret_cast<ImplicitProducerKVP*>(details::align_for<ImplicitProducerKVP>(raw + sizeof(ImplicitProducerHash)));
+ for (size_t i = 0; i != newCapacity; ++i) {
+ new (newHash->entries + i) ImplicitProducerKVP;
+ newHash->entries[i].key.store(details::invalid_thread_id, std::memory_order::relaxed);
+ }
+ newHash->prev = mainHash;
+ implicitProducerHash.store(newHash, std::memory_order::release);
+ implicitProducerHashResizeInProgress.clear(std::memory_order::release);
+ mainHash = newHash;
+ }
+ else {
+ implicitProducerHashResizeInProgress.clear(std::memory_order::release);
+ }
+ }
+
+ // If it's < three-quarters full, add to the old one anyway so that we don't have to wait for the next table
+ // to finish being allocated by another thread (and if we just finished allocating above, the condition will
+ // always be true)
+ if (newCount < (mainHash->capacity >> 1) + (mainHash->capacity >> 2)) {
+ bool recycled;
+ auto producer = static_cast<ImplicitProducer*>(recycle_or_create_producer(false, recycled));
+ if (producer == nullptr) {
+ implicitProducerHashCount.fetch_sub(1, std::memory_order::relaxed);
+ return nullptr;
+ }
+ if (recycled) {
+ implicitProducerHashCount.fetch_sub(1, std::memory_order::relaxed);
+ }
+
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ producer->threadExitListener.callback = &ConcurrentQueue::implicit_producer_thread_exited_callback;
+ producer->threadExitListener.userData = producer;
+ details::ThreadExitNotifier::subscribe(&producer->threadExitListener);
+#endif
+
+ auto index = hashedId;
+ while (true) {
+ index &= mainHash->capacity - 1;
+ auto probedKey = mainHash->entries[index].key.load(std::memory_order::relaxed);
+
+ auto empty = details::invalid_thread_id;
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ auto reusable = details::invalid_thread_id2;
+ if ((probedKey == empty && mainHash->entries[index].key.compare_exchange_strong(empty, id, std::memory_order::relaxed, std::memory_order::relaxed)) ||
+ (probedKey == reusable && mainHash->entries[index].key.compare_exchange_strong(reusable, id, std::memory_order::acquire, std::memory_order::acquire))) {
+#else
+ if ((probedKey == empty && mainHash->entries[index].key.compare_exchange_strong(empty, id, std::memory_order::relaxed, std::memory_order::relaxed))) {
+#endif
+ mainHash->entries[index].value = producer;
+ break;
+ }
+ ++index;
+ }
+ return producer;
+ }
+
+ // Hmm, the old hash is quite full and somebody else is busy allocating a new one.
+ // We need to wait for the allocating thread to finish (if it succeeds, we add, if not,
+ // we try to allocate ourselves).
+ mainHash = implicitProducerHash.load(std::memory_order::acquire);
+ }
+ }
+
+#ifdef MOODYCAMEL_CPP11_THREAD_LOCAL_SUPPORTED
+ void implicit_producer_thread_exited(ImplicitProducer* producer)
+ {
+ // Remove from thread exit listeners
+ details::ThreadExitNotifier::unsubscribe(&producer->threadExitListener);
+
+ // Remove from hash
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODHASH
+ debug::DebugLock lock(implicitProdMutex);
+#endif
+ auto hash = implicitProducerHash.load(std::memory_order::acquire);
+ assert(hash != nullptr); // The thread exit listener is only registered if we were added to a hash in the first place
+ auto id = details::thread_id();
+ auto hashedId = details::hash_thread_id(id);
+ details::thread_id_t probedKey;
+
+ // We need to traverse all the hashes just in case other threads aren't on the current one yet and are
+ // trying to add an entry thinking there's a free slot (because they reused a producer)
+ for (; hash != nullptr; hash = hash->prev) {
+ auto index = hashedId;
+ do {
+ index &= hash->capacity - 1;
+ probedKey = hash->entries[index].key.load(std::memory_order::relaxed);
+ if (probedKey == id) {
+ hash->entries[index].key.store(details::invalid_thread_id2, std::memory_order::release);
+ break;
+ }
+ ++index;
+ } while (probedKey != details::invalid_thread_id); // Can happen if the hash has changed but we weren't put back in it yet, or if we weren't added to this hash in the first place
+ }
+
+ // Mark the queue as being recyclable
+ producer->inactive.store(true, std::memory_order::release);
+ }
+
+ static void implicit_producer_thread_exited_callback(void* userData)
+ {
+ auto producer = static_cast<ImplicitProducer*>(userData);
+ auto queue = producer->parent;
+ queue->implicit_producer_thread_exited(producer);
+ }
+#endif
+
+ //////////////////////////////////
+ // Utility functions
+ //////////////////////////////////
+
+ template<typename TAlign>
+ static inline void* aligned_malloc(size_t size)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (std::alignment_of<TAlign>::value <= std::alignment_of<details::max_align_t>::value)
+ return (Traits::malloc)(size);
+ else {
+ size_t alignment = std::alignment_of<TAlign>::value;
+ void* raw = (Traits::malloc)(size + alignment - 1 + sizeof(void*));
+ if (!raw)
+ return nullptr;
+ char* ptr = details::align_for<TAlign>(reinterpret_cast<char*>(raw) + sizeof(void*));
+ *(reinterpret_cast<void**>(ptr) - 1) = raw;
+ return ptr;
+ }
+ }
+
+ template<typename TAlign>
+ static inline void aligned_free(void* ptr)
+ {
+ MOODYCAMEL_CONSTEXPR_IF (std::alignment_of<TAlign>::value <= std::alignment_of<details::max_align_t>::value)
+ return (Traits::free)(ptr);
+ else
+ (Traits::free)(ptr ? *(reinterpret_cast<void**>(ptr) - 1) : nullptr);
+ }
+
+ template<typename U>
+ static inline U* create_array(size_t count)
+ {
+ assert(count > 0);
+ U* p = static_cast<U*>(aligned_malloc<U>(sizeof(U) * count));
+ if (p == nullptr)
+ return nullptr;
+
+ for (size_t i = 0; i != count; ++i)
+ new (p + i) U();
+ return p;
+ }
+
+ template<typename U>
+ static inline void destroy_array(U* p, size_t count)
+ {
+ if (p != nullptr) {
+ assert(count > 0);
+ for (size_t i = count; i != 0; )
+ (p + --i)->~U();
+ }
+ aligned_free<U>(p);
+ }
+
+ template<typename U>
+ static inline U* create()
+ {
+ void* p = aligned_malloc<U>(sizeof(U));
+ return p != nullptr ? new (p) U : nullptr;
+ }
+
+ template<typename U, typename A1>
+ static inline U* create(A1&& a1)
+ {
+ void* p = aligned_malloc<U>(sizeof(U));
+ return p != nullptr ? new (p) U(std::forward<A1>(a1)) : nullptr;
+ }
+
+ template<typename U>
+ static inline void destroy(U* p)
+ {
+ if (p != nullptr)
+ p->~U();
+ aligned_free<U>(p);
+ }
+
+private:
+ std::atomic<ProducerBase*> producerListTail;
+ std::atomic<std::uint32_t> producerCount;
+
+ std::atomic<size_t> initialBlockPoolIndex;
+ Block* initialBlockPool;
+ size_t initialBlockPoolSize;
+
+#ifndef MCDBGQ_USEDEBUGFREELIST
+ FreeList<Block> freeList;
+#else
+ debug::DebugFreeList<Block> freeList;
+#endif
+
+ std::atomic<ImplicitProducerHash*> implicitProducerHash;
+ std::atomic<size_t> implicitProducerHashCount; // Number of slots logically used
+ ImplicitProducerHash initialImplicitProducerHash;
+ std::array<ImplicitProducerKVP, INITIAL_IMPLICIT_PRODUCER_HASH_SIZE> initialImplicitProducerHashEntries;
+ std::atomic_flag implicitProducerHashResizeInProgress;
+
+ std::atomic<std::uint32_t> nextExplicitConsumerId;
+ std::atomic<std::uint32_t> globalExplicitConsumerOffset;
+
+#ifdef MCDBGQ_NOLOCKFREE_IMPLICITPRODHASH
+ debug::DebugMutex implicitProdMutex;
+#endif
+
+#ifdef MOODYCAMEL_QUEUE_INTERNAL_DEBUG
+ std::atomic<ExplicitProducer*> explicitProducers;
+ std::atomic<ImplicitProducer*> implicitProducers;
+#endif
+};
+
+
+template<typename T, typename Traits>
+ProducerToken::ProducerToken(ConcurrentQueue<T, Traits>& queue)
+ : producer(queue.recycle_or_create_producer(true))
+{
+ if (producer != nullptr) {
+ producer->token = this;
+ }
+}
+
+template<typename T, typename Traits>
+ProducerToken::ProducerToken(BlockingConcurrentQueue<T, Traits>& queue)
+ : producer(reinterpret_cast<ConcurrentQueue<T, Traits>*>(&queue)->recycle_or_create_producer(true))
+{
+ if (producer != nullptr) {
+ producer->token = this;
+ }
+}
+
+template<typename T, typename Traits>
+ConsumerToken::ConsumerToken(ConcurrentQueue<T, Traits>& queue)
+ : itemsConsumedFromCurrent(0), currentProducer(nullptr), desiredProducer(nullptr)
+{
+ initialOffset = queue.nextExplicitConsumerId.fetch_add(1, std::memory_order::release);
+ lastKnownGlobalOffset = (std::uint32_t)-1;
+}
+
+template<typename T, typename Traits>
+ConsumerToken::ConsumerToken(BlockingConcurrentQueue<T, Traits>& queue)
+ : itemsConsumedFromCurrent(0), currentProducer(nullptr), desiredProducer(nullptr)
+{
+ initialOffset = reinterpret_cast<ConcurrentQueue<T, Traits>*>(&queue)->nextExplicitConsumerId.fetch_add(1, std::memory_order::release);
+ lastKnownGlobalOffset = (std::uint32_t)-1;
+}
+
+template<typename T, typename Traits>
+inline void swap(ConcurrentQueue<T, Traits>& a, ConcurrentQueue<T, Traits>& b) MOODYCAMEL_NOEXCEPT
+{
+ a.swap(b);
+}
+
+inline void swap(ProducerToken& a, ProducerToken& b) MOODYCAMEL_NOEXCEPT
+{
+ a.swap(b);
+}
+
+inline void swap(ConsumerToken& a, ConsumerToken& b) MOODYCAMEL_NOEXCEPT
+{
+ a.swap(b);
+}
+
+template<typename T, typename Traits>
+inline void swap(typename ConcurrentQueue<T, Traits>::ImplicitProducerKVP& a, typename ConcurrentQueue<T, Traits>::ImplicitProducerKVP& b) MOODYCAMEL_NOEXCEPT
+{
+ a.swap(b);
+}
+
+}
+
+#if defined(_MSC_VER) && (!defined(_HAS_CXX17) || !_HAS_CXX17)
+#pragma warning(pop)
+#endif
+
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
diff --git a/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp b/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
new file mode 100644
index 0000000000..38fffcdc78
--- /dev/null
+++ b/yt/yt/core/concurrency/new_fair_share_thread_pool.cpp
@@ -0,0 +1,1164 @@
+#include "two_level_fair_share_thread_pool.h"
+#include "private.h"
+#include "notify_manager.h"
+#include "profiling_helpers.h"
+#include "scheduler_thread.h"
+#include "thread_pool_detail.h"
+
+#include <yt/yt/core/actions/current_invoker.h>
+
+#include <yt/yt/core/misc/finally.h>
+#include <yt/yt/core/misc/heap.h>
+#include <yt/yt/core/misc/ring_queue.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+#include <yt/yt/core/misc/range_formatters.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/memory/public.h>
+
+#include <util/system/spinlock.h>
+
+#include <util/generic/xrange.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+inline const NLogging::TLogger Logger("FairShareThreadPool");
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+DECLARE_REFCOUNTED_CLASS(TTwoLevelFairShareQueue)
+DECLARE_REFCOUNTED_CLASS(TBucket)
+
+struct TExecutionPool;
+
+// High 16 bits is thread index and 48 bits for thread pool ptr.
+thread_local TPackedPtr ThreadCookie = 0;
+
+static constexpr auto LogDurationThreshold = TDuration::Seconds(1);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class THeapItem
+{
+public:
+ THeapItem(const THeapItem&) = delete;
+ THeapItem& operator=(const THeapItem&) = delete;
+
+ explicit THeapItem(T* ptr)
+ : Ptr_(std::move(ptr))
+ {
+ AdjustBackReference();
+ }
+
+ ~THeapItem()
+ {
+ if (Ptr_) {
+ YT_ASSERT(Ptr_->PositionInHeap_ == this);
+ Ptr_->PositionInHeap_ = nullptr;
+ }
+ }
+
+ T& operator* ()
+ {
+ return *Ptr_;
+ }
+
+ const T& operator*() const
+ {
+ return *Ptr_;
+ }
+
+ T* operator->()
+ {
+ return Ptr_;
+ }
+
+ const T* operator->() const
+ {
+ return Ptr_;
+ }
+
+ bool operator < (const THeapItem<T>& other) const
+ {
+ return *Ptr_ < *other;
+ }
+
+ THeapItem(THeapItem&& other) noexcept
+ : Ptr_(std::move(other.Ptr_))
+ {
+ other.Ptr_ = nullptr;
+ AdjustBackReference();
+ }
+
+ THeapItem& operator=(THeapItem&& other) noexcept
+ {
+ Ptr_ = std::move(other.Ptr_);
+ other.Ptr_ = nullptr;
+ AdjustBackReference();
+
+ return *this;
+ }
+
+private:
+ T* Ptr_;
+
+ void AdjustBackReference()
+ {
+ if (Ptr_) {
+ Ptr_->PositionInHeap_ = this;
+ }
+ }
+};
+
+template <class T>
+class THeapItemBase
+{
+public:
+ friend THeapItem<T>;
+
+ THeapItem<T>* GetPositionInHeap() const
+ {
+ return PositionInHeap_;
+ }
+
+ ~THeapItemBase()
+ {
+ YT_ASSERT(!PositionInHeap_);
+ }
+
+private:
+ THeapItem<T>* PositionInHeap_ = nullptr;
+};
+
+template <class T>
+class TPriorityQueue
+{
+public:
+ void Insert(T* object)
+ {
+ Items_.emplace_back(object);
+ SiftUp(Items_.begin(), Items_.end(), Items_.end() - 1);
+ YT_ASSERT(object->GetPositionInHeap());
+ }
+
+ void Extract(const T* object)
+ {
+ auto* positionInHeap = object->GetPositionInHeap();
+ YT_ASSERT(Items_.data() <= positionInHeap && positionInHeap < Items_.data() + Items_.size());
+
+ std::swap(*positionInHeap, Items_.back());
+ SiftDown(
+ Items_.data(),
+ Items_.data() + std::ssize(Items_) - 1,
+ positionInHeap);
+
+ YT_ASSERT(Items_.back()->GetPositionInHeap() == object->GetPositionInHeap());
+ Items_.pop_back();
+ }
+
+ void AdjustDown(const T* object)
+ {
+ auto* positionInHeap = object->GetPositionInHeap();
+ YT_ASSERT(Items_.data() <= positionInHeap && positionInHeap < Items_.data() + Items_.size());
+ SiftDown(
+ Items_.data(),
+ Items_.data() + std::ssize(Items_),
+ positionInHeap);
+ }
+
+ T* GetFront()
+ {
+ YT_ASSERT(!Empty());
+ return &*Items_.front();
+ }
+
+ bool Empty() const
+ {
+ return Items_.empty();
+ }
+
+ size_t GetSize() const
+ {
+ return Items_.size();
+ }
+
+ T& operator[] (size_t index)
+ {
+ return *Items_[index];
+ }
+
+ template <class F>
+ void ForEach(F&& functor)
+ {
+ for (auto& item : Items_) {
+ functor(&*item);
+ }
+ }
+
+ void Clear()
+ {
+ Items_.clear();
+ }
+
+private:
+ std::vector<THeapItem<T>> Items_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAction
+{
+ TCpuInstant EnqueuedAt = 0;
+ TCpuInstant StartedAt = 0;
+
+ // Callback keeps raw ptr to bucket to minimize bucket ref count.
+ TClosure Callback;
+ TBucketPtr BucketHolder;
+
+ TPackedPtr EnqueuedThreadCookie = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEnqueuedTime
+ : public THeapItemBase<TEnqueuedTime>
+{
+ TCpuInstant Value = 0;
+};
+
+bool operator < (const TEnqueuedTime& lhs, const TEnqueuedTime& rhs)
+{
+ return lhs.Value < rhs.Value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Data for scheduling on the first level.
+struct TExecutionPool
+ : public THeapItemBase<TExecutionPool>
+{
+ const TString PoolName;
+
+ // Profiling sensors.
+ const NProfiling::TSummary BucketCounter;
+ const NProfiling::TSummary SizeCounter;
+ const NProfiling::TCounter DequeuedCounter;
+ const TEventTimer WaitTimeCounter;
+ const TEventTimer ExecTimeCounter;
+ const TEventTimer TotalTimeCounter;
+ const NProfiling::TTimeCounter CumulativeTimeCounter;
+
+ // Action count is used to decide whether to reset excess time or not.
+ size_t ActionCountInQueue = 0;
+
+ TCpuDuration NextUpdateWeightInstant = 0;
+ double InverseWeight = 1.0;
+ TCpuDuration ExcessTime = 0;
+ int BucketRefs = 0;
+
+ TPriorityQueue<TBucket> ActiveBucketsHeap;
+ TCpuDuration LastBucketExcessTime = 0;
+
+ TExecutionPool(TString poolName, const TProfiler& profiler)
+ : PoolName(std::move(poolName))
+ , BucketCounter(profiler.Summary("/buckets"))
+ , SizeCounter(profiler.Summary("/size"))
+ , DequeuedCounter(profiler.Counter("/dequeued"))
+ , WaitTimeCounter(profiler.Timer("/time/wait"))
+ , ExecTimeCounter(profiler.Timer("/time/exec"))
+ , TotalTimeCounter(profiler.Timer("/time/total"))
+ , CumulativeTimeCounter(profiler.TimeCounter("/time/cumulative"))
+ { }
+};
+
+bool operator < (const TExecutionPool& lhs, const TExecutionPool& rhs)
+{
+ return lhs.ExcessTime < rhs.ExcessTime;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Data for scheduling on the second level.
+struct TBucketBase
+{
+ const TString BucketName;
+ const TString PoolName;
+
+ TRingQueue<TAction> ActionQueue;
+ TExecutionPool* Pool = nullptr;
+
+ TCpuDuration ExcessTime = 0;
+
+ TEnqueuedTime EnqueuedTime;
+
+ TBucketBase(TString bucketName, TString poolName)
+ : BucketName(std::move(bucketName))
+ , PoolName(std::move(poolName))
+ { }
+};
+
+bool operator < (const TBucketBase& lhs, const TBucketBase& rhs)
+{
+ return std::tie(lhs.ExcessTime, lhs.EnqueuedTime) < std::tie(rhs.ExcessTime, rhs.EnqueuedTime);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBucket
+ : public IInvoker
+ , public THeapItemBase<TBucket>
+ , public TBucketBase
+{
+public:
+ TBucket(TString bucketName, TString poolName, TTwoLevelFairShareQueuePtr parent)
+ : TBucketBase(std::move(bucketName), std::move(poolName))
+ , Parent_(std::move(parent))
+ { }
+
+ ~TBucket();
+
+ void RunCallback(const TClosure& callback, TCpuInstant cpuInstant)
+ {
+ YT_LOG_TRACE("Executing callback (EnqueuedAt: %v)", cpuInstant);
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ callback.Run();
+ }
+
+ bool IsSerialized() const override
+ {
+ return false;
+ }
+
+ void Invoke(TClosure callback) override;
+
+ void Invoke(TMutableRange<TClosure> callbacks) override
+ {
+ for (auto& callback : callbacks) {
+ Invoke(std::move(callback));
+ }
+ }
+
+ NThreading::TThreadId GetThreadId() const override
+ {
+ return NThreading::InvalidThreadId;
+ }
+
+ bool CheckAffinity(const IInvokerPtr& invoker) const override
+ {
+ return invoker.Get() == this;
+ }
+
+private:
+ const TTwoLevelFairShareQueuePtr Parent_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucket)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ERequest,
+ (None)
+ (EndExecute)
+ (FetchNext)
+);
+
+class TTwoLevelFairShareQueue
+ : public TRefCounted
+ , protected TNotifyManager
+{
+public:
+ TTwoLevelFairShareQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider,
+ bool verboseLogging)
+ : TNotifyManager(std::move(callbackEventCount), GetThreadTags(threadNamePrefix), TDuration::MilliSeconds(10))
+ , ThreadNamePrefix_(threadNamePrefix)
+ , Profiler_(TProfiler{"/fair_share_queue"}
+ .WithHot())
+ , PoolWeightProvider_(std::move(poolWeightProvider))
+ , VerboseLogging_(verboseLogging)
+ { }
+
+ ~TTwoLevelFairShareQueue()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount)
+ {
+ ThreadCount_.store(threadCount);
+ }
+
+ // GetInvoker is protected by mapping lock (can be sharded).
+ IInvokerPtr GetInvoker(const TString& poolName, const TString& bucketName)
+ {
+ // TODO(lukyan): Use reader guard and update it to writer if needed.
+ auto guard = Guard(MappingLock_);
+
+ auto [bucketIt, bucketInserted] = BucketMapping_.emplace(std::make_pair(poolName, bucketName), nullptr);
+
+ auto bucket = bucketIt->second ? DangerousGetPtr(bucketIt->second) : nullptr;
+ if (!bucket) {
+ bucket = New<TBucket>(bucketName, poolName, MakeStrong(this));
+ bucketIt->second = bucket.Get();
+ }
+
+ return bucket;
+ }
+
+ // GetInvoker is protected by mapping lock (can be sharded).
+ void RemoveBucket(TBucket* bucket)
+ {
+ {
+ auto guard = Guard(MappingLock_);
+ auto bucketIt = BucketMapping_.find(std::make_pair(bucket->PoolName, bucket->BucketName));
+
+ if (bucketIt != BucketMapping_.end() && bucketIt->second == bucket) {
+ BucketMapping_.erase(bucketIt);
+ }
+ }
+
+ if (bucket->Pool) {
+ UnlinkBucketQueue_.Enqueue(bucket->Pool);
+ }
+ }
+
+ // Invoke is lock free.
+ void Invoke(TClosure callback, TBucket* bucket)
+ {
+ if (Stopped_.load()) {
+ return;
+ }
+
+ auto cpuInstant = GetCpuInstant();
+
+ YT_LOG_TRACE("Invoking action (EnqueuedAt: %v, Invoker: %v)",
+ cpuInstant,
+ ThreadNamePrefix_);
+
+ TAction action;
+ action.EnqueuedAt = cpuInstant;
+ // Callback keeps raw ptr to bucket to minimize bucket ref count.
+ action.Callback = BIND(&TBucket::RunCallback, Unretained(bucket), std::move(callback), cpuInstant);
+ action.BucketHolder = MakeStrong(bucket);
+ action.EnqueuedThreadCookie = ThreadCookie;
+
+ InvokeQueue_.Enqueue(std::move(action));
+
+ NotifyFromInvoke(cpuInstant, ActiveThreads_.load() == 0);
+ }
+
+ void StopPrologue()
+ {
+ GetEventCount()->NotifyAll();
+ }
+
+ TClosure OnExecute(int index, bool fetchNext, std::function<bool()> isStopping)
+ {
+ while (true) {
+ auto cookie = GetEventCount()->PrepareWait();
+
+ auto hasAction = ThreadStates_[index].Action.BucketHolder;
+ int activeThreadDelta = hasAction ? -1 : 0;
+
+ auto callback = DoOnExecute(index, fetchNext);
+
+ if (callback) {
+ activeThreadDelta += 1;
+ }
+
+ YT_VERIFY(activeThreadDelta >= -1 && activeThreadDelta <= 1);
+
+ if (activeThreadDelta != 0) {
+ auto activeThreads = ActiveThreads_.fetch_add(activeThreadDelta);
+ auto newActiveThreads = activeThreads + activeThreadDelta;
+ YT_VERIFY(newActiveThreads >= 0 && newActiveThreads <= TThreadPoolBase::MaxThreadCount);
+ activeThreadDelta = 0;
+ }
+
+ if (callback || isStopping()) {
+ CancelWait();
+ return callback;
+ }
+
+ YT_VERIFY(fetchNext);
+ Wait(cookie, isStopping);
+ }
+ }
+
+ void Shutdown()
+ {
+ Drain();
+ }
+
+ void Drain()
+ {
+ Stopped_.store(true);
+ auto guard = Guard(MainLock_);
+
+ WaitHeap_.Clear();
+
+ std::vector<TBucket*> buckets;
+ ActivePoolsHeap_.ForEach([&] (auto* pool) {
+ pool->ActiveBucketsHeap.ForEach([&] (auto* bucket) {
+ buckets.push_back(bucket);
+ });
+ pool->ActiveBucketsHeap.Clear();
+ });
+ ActivePoolsHeap_.Clear();
+
+ // Actions hold strong references to buckets.
+ // Buckets' ActionQueue must be cleared before destroying actions.
+ std::vector<TAction> actions;
+ for (auto* bucket : buckets) {
+ while (!bucket->ActionQueue.empty()) {
+ actions.push_back(std::move(bucket->ActionQueue.front()));
+ bucket->ActionQueue.pop();
+ }
+ }
+
+ actions.clear();
+
+ InvokeQueue_.DequeueAll();
+ }
+
+private:
+ struct TThreadState
+ {
+ std::atomic<ERequest> Request = ERequest::None;
+
+ TCpuInstant AccountedAt = 0;
+ TAction Action;
+
+ // Used to store bucket ref under lock to destroy it outside.
+ TBucketPtr BucketToUnref;
+ int LastActionsInQueue;
+ TDuration TimeFromStart;
+ TDuration TimeFromEnqueue;
+ };
+
+ static_assert(sizeof(TThreadState) >= CacheLineSize);
+
+ const TString ThreadNamePrefix_;
+ const TProfiler Profiler_;
+ const IPoolWeightProviderPtr PoolWeightProvider_;
+ const bool VerboseLogging_;
+
+ // TODO(lukyan): Sharded mapping.
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, MappingLock_);
+ THashMap<std::pair<TString, TString>, TBucket*> BucketMapping_;
+ TMpscStack<TExecutionPool*> UnlinkBucketQueue_;
+
+ std::atomic<bool> Stopped_ = false;
+ TMpscStack<TAction> InvokeQueue_;
+ char Padding0_[CacheLineSize - sizeof(TMpscStack<TAction>)];
+
+ // Use simple non adaptive spinlock without complex wait strategies.
+ ::TSpinLock MainLock_;
+ char Padding1_[CacheLineSize - sizeof(::TSpinLock)];
+
+ std::array<TThreadState, TThreadPoolBase::MaxThreadCount> ThreadStates_;
+
+ THashMap<TString, std::unique_ptr<TExecutionPool>> PoolMapping_;
+ TPriorityQueue<TExecutionPool> ActivePoolsHeap_;
+ TCpuDuration LastPoolExcessTime_ = 0;
+ TPriorityQueue<TEnqueuedTime> WaitHeap_;
+
+ // Buffer to keep actions during distribution to threads.
+ std::array<TAction, TThreadPoolBase::MaxThreadCount> OtherActions_;
+
+ std::atomic<int> ThreadCount_ = 0;
+ std::atomic<int> ActiveThreads_ = 0;
+
+ TExecutionPool* GetOrRegisterPool(TString poolName)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ auto [mappingIt, inserted] = PoolMapping_.emplace(poolName, nullptr);
+ if (!inserted) {
+ YT_ASSERT(mappingIt->second->PoolName == poolName);
+ } else {
+ YT_LOG_TRACE("Creating pool (PoolName: %v)", poolName);
+ mappingIt->second = std::make_unique<TExecutionPool>(
+ poolName,
+ Profiler_.WithTags(GetBucketTags(ThreadNamePrefix_, poolName)));
+ }
+
+ return mappingIt->second.get();
+ }
+
+ void ConsumeInvokeQueue()
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ Y_UNUSED(Padding0_);
+ Y_UNUSED(Padding1_);
+
+ InvokeQueue_.DequeueAll(true, [&] (auto& action) {
+ auto* bucket = action.BucketHolder.Get();
+
+ if (bucket->Pool == nullptr) {
+ bucket->Pool = GetOrRegisterPool(bucket->PoolName);
+ bucket->Pool->BucketRefs++;
+ }
+
+ auto* pool = bucket->Pool;
+ if (!pool->GetPositionInHeap()) {
+ // ExcessTime can be greater than last pool excess time
+ // if the pool is "currently executed" and reschedules action.
+ if (pool->ExcessTime < LastPoolExcessTime_) {
+ // Use last pool excess time to schedule new pool
+ // after earlier scheduled pools (and not yet executed) in queue.
+
+ YT_LOG_DEBUG_IF(VerboseLogging_, "Initial pool excess time (Name: %v, ExcessTime: %v -> %v)",
+ pool->PoolName,
+ pool->ExcessTime,
+ LastPoolExcessTime_);
+
+ pool->ExcessTime = LastPoolExcessTime_;
+ }
+
+ ActivePoolsHeap_.Insert(pool);
+ }
+
+ ++pool->ActionCountInQueue;
+
+ auto enqueuedAt = action.EnqueuedAt;
+
+ bool wasEmpty = bucket->ActionQueue.empty();
+ bucket->ActionQueue.push(std::move(action));
+
+ if (wasEmpty) {
+ bucket->EnqueuedTime.Value = enqueuedAt;
+ }
+
+ YT_ASSERT(wasEmpty == !bucket->GetPositionInHeap());
+
+ if (!bucket->GetPositionInHeap()) {
+ // ExcessTime can be greater than last bucket excess time
+ // if the bucket is "currently executed" and reschedules action.
+ if (bucket->ExcessTime < pool->LastBucketExcessTime) {
+ // Use last bucket excess time to schedule new bucket
+ // after earlier scheduled buckets (and not yet executed) in queue.
+
+ YT_LOG_DEBUG_IF(VerboseLogging_, "Initial bucket excess time (Name: %v, ExcessTime: %v -> %v)",
+ bucket->BucketName,
+ bucket->ExcessTime,
+ pool->LastBucketExcessTime);
+
+ bucket->ExcessTime = pool->LastBucketExcessTime;
+ }
+
+ pool->ActiveBucketsHeap.Insert(bucket);
+ pool->BucketCounter.Record(pool->ActiveBucketsHeap.GetSize());
+ WaitHeap_.Insert(&bucket->EnqueuedTime);
+ }
+ });
+
+ UnlinkBucketQueue_.DequeueAll(true, [&] (TExecutionPool* pool) {
+ YT_VERIFY(pool->BucketRefs > 0);
+ if (--pool->BucketRefs == 0) {
+ auto poolIt = PoolMapping_.find(pool->PoolName);
+ YT_VERIFY(poolIt != PoolMapping_.end() && poolIt->second.get() == pool);
+ PoolMapping_.erase(poolIt);
+ }
+ });
+ }
+
+ void ServeBeginExecute(TThreadState* threadState, TCpuInstant currentInstant, TAction action)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ YT_ASSERT(!threadState->Action.Callback);
+ YT_ASSERT(!threadState->Action.BucketHolder);
+
+ action.StartedAt = currentInstant;
+
+ threadState->AccountedAt = currentInstant;
+ threadState->Action = std::move(action);
+ }
+
+ void ServeEndExecute(TThreadState* threadState, TCpuInstant /*cpuInstant*/)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ auto action = std::move(threadState->Action);
+ YT_ASSERT(!threadState->Action.Callback);
+ YT_ASSERT(!action.Callback);
+
+ if (!action.BucketHolder) {
+ // There was no action in begin execute.
+ return;
+ }
+
+ // Try not to change bucket ref count under lock.
+ auto bucket = std::move(action.BucketHolder);
+
+ auto& pool = *bucket->Pool;
+ YT_ASSERT(pool.PoolName == bucket->PoolName);
+
+ // LastActionsInQueue is used to update SizeCounter outside lock.
+ threadState->LastActionsInQueue = --pool.ActionCountInQueue;
+
+ // Do not destroy bucket pointer under lock. Move it in thread state in other place and
+ // destroy in corresponding thread after combiner.
+ threadState->BucketToUnref = std::move(bucket);
+ }
+
+ void UpdateExcessTime(TBucket* bucket, TCpuDuration duration, TCpuInstant currentInstant)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ auto* pool = bucket->Pool;
+
+ if (PoolWeightProvider_ && pool->NextUpdateWeightInstant < currentInstant) {
+ pool->NextUpdateWeightInstant = currentInstant + DurationToCpuDuration(TDuration::Seconds(1));
+ pool->InverseWeight = 1.0 / PoolWeightProvider_->GetWeight(pool->PoolName);
+ }
+
+ YT_LOG_DEBUG_IF(VerboseLogging_, "Increment excess time (BucketName: %v, PoolName: %v, ExcessTime: %v -> %v)",
+ bucket->BucketName,
+ bucket->PoolName,
+ bucket->ExcessTime,
+ bucket->ExcessTime + duration);
+
+ pool->ExcessTime += duration * pool->InverseWeight;
+ bucket->ExcessTime += duration;
+
+ if (auto* positionInHeap = pool->GetPositionInHeap()) {
+ ActivePoolsHeap_.AdjustDown(pool);
+ }
+
+ if (auto* positionInHeap = bucket->GetPositionInHeap()) {
+ pool->ActiveBucketsHeap.AdjustDown(bucket);
+ }
+
+ // No need to update wait heap.
+ YT_ASSERT(!bucket->EnqueuedTime.GetPositionInHeap() == !bucket->GetPositionInHeap());
+ }
+
+ bool GetStarvingBucket(TAction* action)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ YT_LOG_DEBUG_IF(VerboseLogging_, "Buckets: %v",
+ MakeFormattableView(
+ xrange(size_t(0), ActivePoolsHeap_.GetSize()),
+ [&] (auto* builder, auto index) {
+ auto& pool = ActivePoolsHeap_[index];
+ builder->AppendFormat("%v [", CpuDurationToDuration(pool.ExcessTime));
+
+ for (size_t bucketIndex = 0; bucketIndex < pool.ActiveBucketsHeap.GetSize(); ++bucketIndex) {
+ const auto& bucket = pool.ActiveBucketsHeap[bucketIndex];
+
+ builder->AppendFormat("%Qv:%v/%v ",
+ bucket.BucketName,
+ CpuDurationToDuration(bucket.ExcessTime),
+ bucket.ActionQueue.front().EnqueuedAt);
+ }
+ builder->AppendFormat("]");
+ }));
+
+ if (ActivePoolsHeap_.Empty()) {
+ return false;
+ }
+
+ auto* pool = ActivePoolsHeap_.GetFront();
+ LastPoolExcessTime_ = pool->ExcessTime;
+
+ auto* bucket = pool->ActiveBucketsHeap.GetFront();
+ pool->LastBucketExcessTime = bucket->ExcessTime;
+
+ YT_ASSERT(!bucket->ActionQueue.empty());
+ *action = std::move(bucket->ActionQueue.front());
+ bucket->ActionQueue.pop();
+
+ YT_ASSERT(bucket == action->BucketHolder);
+
+ if (bucket->ActionQueue.empty()) {
+ bucket->EnqueuedTime.Value = std::numeric_limits<TCpuInstant>::max();
+
+ WaitHeap_.Extract(&bucket->EnqueuedTime);
+
+ pool->ActiveBucketsHeap.Extract(bucket);
+ pool->BucketCounter.Record(pool->ActiveBucketsHeap.GetSize());
+
+ if (pool->ActiveBucketsHeap.Empty()) {
+ ActivePoolsHeap_.Extract(pool);
+ }
+ } else {
+ bucket->EnqueuedTime.Value = bucket->ActionQueue.front().EnqueuedAt;
+ WaitHeap_.AdjustDown(&bucket->EnqueuedTime);
+ }
+
+ return true;
+ }
+
+ Y_NO_INLINE std::tuple<int, int> ServeCombinedRequests(TCpuInstant currentInstant, int currentThreadIndex)
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ auto threadCount = ThreadCount_.load();
+ // Thread pool size can be reconfigures during serving requests.
+ threadCount = std::max(threadCount, currentThreadIndex + 1);
+
+ // Saved thread requests. They must be saved before consuming invoke queue.
+ std::array<bool, TThreadPoolBase::MaxThreadCount> threadRequests{false};
+ std::array<int, TThreadPoolBase::MaxThreadCount> threadIds;
+ int requestCount = 0;
+
+ YT_LOG_TRACE("Updating excess time");
+
+ // Recalculate excess time for all currently evaluating or evaluated recently (end execute) buckets
+ for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex) {
+ auto& threadState = ThreadStates_[threadIndex];
+
+ // TODO(lukyan): Can skip (for threads without requests) or throttle UpdateExcessTime if it happens frequently.
+ // For each currently evaluating buckets recalculate excess time.
+ if (auto* bucket = threadState.Action.BucketHolder.Get()) {
+
+ // TODO(lukyan): Update last excess time for pool without active buckets.
+ UpdateExcessTime(bucket, currentInstant - threadState.AccountedAt, currentInstant);
+ threadState.AccountedAt = currentInstant;
+ }
+
+ auto request = threadState.Request.load(std::memory_order::acquire);
+ if (request != ERequest::None) {
+ ServeEndExecute(&threadState, currentInstant);
+
+ if (request == ERequest::FetchNext) {
+ // Save requests before ConsumeInvokeQueue. Otherwise some thread can schedule action
+ // but action can not be fetched (schedule and fetch happens after ConsumeInvokeQueue).
+ threadRequests[threadIndex] = true;
+ threadIds[requestCount++] = threadIndex;
+ } else {
+ threadState.Request.store(ERequest::None, std::memory_order::release);
+ }
+ }
+ }
+
+ YT_LOG_TRACE("Consuming invoke queue");
+
+ ConsumeInvokeQueue();
+
+ int fetchedActions = 0;
+ int otherActionCount = 0;
+
+ // Schedule actions to desired threads.
+ while (fetchedActions < requestCount) {
+ TAction action;
+
+ if (!GetStarvingBucket(&action)) {
+ break;
+ }
+
+ ++fetchedActions;
+
+ int threadIndex = -1;
+
+ auto unpackedCookie = TTaggedPtr<TTwoLevelFairShareQueue>::Unpack(action.EnqueuedThreadCookie);
+ // TODO(lukyan): Check also wait time. If it is too high, no matter where to schedule.
+ if (unpackedCookie.Ptr == this) {
+ threadIndex = unpackedCookie.Tag;
+ }
+
+ if (threadIndex != -1 && threadRequests[threadIndex]) {
+ ServeBeginExecute(&ThreadStates_[threadIndex], currentInstant, std::move(action));
+ threadRequests[threadIndex] = false;
+ ThreadStates_[threadIndex].Request.store(ERequest::None, std::memory_order::release);
+ } else {
+ OtherActions_[otherActionCount++] = std::move(action);
+ }
+ }
+
+ // Schedule other actions.
+ for (int threadIndex : MakeRange(threadIds.data(), requestCount)) {
+ if (threadRequests[threadIndex]) {
+ if (otherActionCount > 0) {
+ ServeBeginExecute(&ThreadStates_[threadIndex], currentInstant, std::move(OtherActions_[--otherActionCount]));
+ }
+
+ ThreadStates_[threadIndex].Request.store(ERequest::None, std::memory_order::release);
+ }
+ }
+
+ return {requestCount, fetchedActions};
+ }
+
+ TCpuInstant GetMinEnqueuedAt()
+ {
+ VERIFY_SPINLOCK_AFFINITY(MainLock_);
+
+ return WaitHeap_.Empty()
+ ? std::numeric_limits<TCpuInstant>::max()
+ : WaitHeap_.GetFront()->Value;
+ }
+
+ TClosure DoOnExecute(int index, bool fetchNext)
+ {
+ auto cpuInstant = GetCpuInstant();
+ auto& threadState = ThreadStates_[index];
+
+ const auto& oldAction = threadState.Action;
+ if (oldAction.BucketHolder) {
+ auto waitTime = CpuDurationToDuration(oldAction.StartedAt - oldAction.EnqueuedAt);
+ auto timeFromStart = CpuDurationToDuration(cpuInstant - oldAction.StartedAt);
+ auto timeFromEnqueue = CpuDurationToDuration(cpuInstant - oldAction.EnqueuedAt);
+
+ threadState.TimeFromStart = timeFromStart;
+ threadState.TimeFromEnqueue = timeFromEnqueue;
+
+ if (timeFromStart > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback execution took too long (Wait: %v, Execution: %v, Total: %v)",
+ waitTime,
+ timeFromStart,
+ timeFromEnqueue);
+ }
+
+ if (waitTime > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback wait took too long (Wait: %v, Execution: %v, Total: %v)",
+ waitTime,
+ timeFromStart,
+ timeFromEnqueue);
+ }
+ }
+
+ auto finally = Finally([&] {
+ auto bucketToUndef = std::move(threadState.BucketToUnref);
+ if (bucketToUndef) {
+ auto* pool = bucketToUndef->Pool;
+ pool->SizeCounter.Record(threadState.LastActionsInQueue);
+ pool->DequeuedCounter.Increment(1);
+ pool->ExecTimeCounter.Record(threadState.TimeFromStart);
+ pool->TotalTimeCounter.Record(threadState.TimeFromEnqueue);
+ pool->CumulativeTimeCounter.Add(threadState.TimeFromStart);
+ bucketToUndef.Reset();
+ }
+
+ const auto& action = threadState.Action;
+ if (action.BucketHolder) {
+ auto waitTime = CpuDurationToDuration(action.StartedAt - action.EnqueuedAt);
+ action.BucketHolder->Pool->WaitTimeCounter.Record(waitTime);
+ }
+ });
+
+ auto& request = threadState.Request;
+ YT_VERIFY(request == ERequest::None);
+ request.store(fetchNext ? ERequest::FetchNext : ERequest::EndExecute);
+
+ if (MainLock_.IsLocked() || !MainLock_.TryAcquire()) {
+ // Locked here.
+ while (true) {
+ SpinLockPause();
+
+ if (request.load(std::memory_order::acquire) == ERequest::None) {
+ return std::move(threadState.Action.Callback);
+ } else if (!MainLock_.IsLocked() && MainLock_.TryAcquire()) {
+ break;
+ }
+ }
+ }
+
+ ResetMinEnqueuedAt();
+
+ YT_LOG_TRACE("Started serving requests");
+ auto [requests, fetchedActions] = ServeCombinedRequests(cpuInstant, index);
+
+ // Evaluate notify condition here, but call NotifyAfterFetch outside lock.
+ auto newMinEnqueuedAt = GetMinEnqueuedAt();
+ MainLock_.Release();
+
+ auto endInstant = GetCpuInstant();
+ YT_LOG_TRACE("Finished serving requests (Duration: %v, Requests: %v, FetchCount: %v, MinEnqueuedAt: %v)",
+ CpuDurationToDuration(endInstant - cpuInstant),
+ requests,
+ fetchedActions,
+ CpuInstantToInstant(newMinEnqueuedAt));
+
+ NotifyAfterFetch(endInstant, newMinEnqueuedAt);
+
+ return std::move(threadState.Action.Callback);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTwoLevelFairShareQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBucket::Invoke(TClosure callback)
+{
+ Parent_->Invoke(std::move(callback), this);
+}
+
+TBucket::~TBucket()
+{
+ Parent_->RemoveBucket(this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareThread
+ : public TSchedulerThread
+{
+public:
+ TFairShareThread(
+ TTwoLevelFairShareQueuePtr queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ int index)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName)
+ , Queue_(std::move(queue))
+ , Index_(index)
+ { }
+
+protected:
+ const TTwoLevelFairShareQueuePtr Queue_;
+ const int Index_;
+
+ void OnStart() override
+ {
+ ThreadCookie = TTaggedPtr(Queue_.Get(), static_cast<ui16>(Index_)).Pack();
+ }
+
+ void StopPrologue() override
+ {
+ Queue_->StopPrologue();
+ }
+
+ TClosure OnExecute() override
+ {
+ bool fetchNext = !TSchedulerThread::IsStopping() || TSchedulerThread::GracefulStop_;
+
+ return Queue_->OnExecute(Index_, fetchNext, [&] {
+ return TSchedulerThread::IsStopping();
+ });
+ }
+
+ TClosure BeginExecute() override
+ {
+ Y_UNREACHABLE();
+ }
+
+ void EndExecute() override
+ {
+ Y_UNREACHABLE();
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareThread)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTwoLevelFairShareThreadPool
+ : public ITwoLevelFairShareThreadPool
+ , public TThreadPoolBase
+{
+public:
+ TTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider,
+ bool verboseLogging)
+ : TThreadPoolBase(threadNamePrefix)
+ , Queue_(New<TTwoLevelFairShareQueue>(
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ std::move(poolWeightProvider),
+ verboseLogging))
+ {
+ Configure(threadCount);
+ }
+
+ ~TTwoLevelFairShareThreadPool()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount) override
+ {
+ TThreadPoolBase::Configure(threadCount);
+ }
+
+ IInvokerPtr GetInvoker(
+ const TString& poolName,
+ const TFairShareThreadPoolTag& bucketName) override
+ {
+ EnsureStarted();
+ return Queue_->GetInvoker(poolName, bucketName);
+ }
+
+ void Shutdown() override
+ {
+ TThreadPoolBase::Shutdown();
+ }
+
+ int GetThreadCount() override
+ {
+ return TThreadPoolBase::GetThreadCount();
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TTwoLevelFairShareQueuePtr Queue_;
+
+ void DoShutdown() override
+ {
+ TThreadPoolBase::DoShutdown();
+ }
+
+ TClosure MakeFinalizerCallback() override
+ {
+ return BIND([queue = Queue_, callback = TThreadPoolBase::MakeFinalizerCallback()] {
+ callback();
+ queue->Drain();
+ });
+ }
+
+ void DoConfigure(int threadCount) override
+ {
+ Queue_->Configure(threadCount);
+ TThreadPoolBase::DoConfigure(threadCount);
+ }
+
+ TSchedulerThreadBasePtr SpawnThread(int index) override
+ {
+ return New<TFairShareThread>(
+ Queue_,
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ MakeThreadName(index),
+ index);
+ }
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITwoLevelFairShareThreadPoolPtr CreateNewTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider = nullptr,
+ bool verboseLogging = false)
+{
+ return New<TTwoLevelFairShareThreadPool>(
+ threadCount,
+ threadNamePrefix,
+ std::move(poolWeightProvider),
+ verboseLogging);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/new_fair_share_thread_pool.h b/yt/yt/core/concurrency/new_fair_share_thread_pool.h
new file mode 100644
index 0000000000..abc8c74fab
--- /dev/null
+++ b/yt/yt/core/concurrency/new_fair_share_thread_pool.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITwoLevelFairShareThreadPoolPtr CreateNewTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider = nullptr,
+ bool verboseLogging = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/nonblocking_batch-inl.h b/yt/yt/core/concurrency/nonblocking_batch-inl.h
new file mode 100644
index 0000000000..7f2ae26778
--- /dev/null
+++ b/yt/yt/core/concurrency/nonblocking_batch-inl.h
@@ -0,0 +1,150 @@
+#ifndef NONBLOCKING_BATCH_INL_H_
+#error "Direct inclusion of this file is not allowed, include nonblocking_batch.h"
+// For the sake of sane code completion.
+#include "nonblocking_batch.h"
+#endif
+#undef NONBLOCKING_BATCH_INL_H_
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TNonblockingBatch<T>::TNonblockingBatch(int maxBatchSize, TDuration batchDuration)
+ : MaxBatchSize_(maxBatchSize)
+ , BatchDuration_(batchDuration)
+{ }
+
+template <class T>
+TNonblockingBatch<T>::~TNonblockingBatch()
+{
+ auto guard = Guard(SpinLock_);
+ ResetTimer(guard);
+}
+
+template <class T>
+template <class... U>
+void TNonblockingBatch<T>::Enqueue(U&& ... u)
+{
+ auto guard = Guard(SpinLock_);
+ CurrentBatch_.emplace_back(std::forward<U>(u)...);
+ StartTimer(guard);
+ CheckFlush(guard);
+}
+
+template <class T>
+TFuture<typename TNonblockingBatch<T>::TBatch> TNonblockingBatch<T>::DequeueBatch()
+{
+ auto guard = Guard(SpinLock_);
+ auto promise = NewPromise<TBatch>();
+ Promises_.push_back(promise);
+ StartTimer(guard);
+ CheckReturn(guard);
+ return promise.ToFuture();
+}
+
+template <class T>
+void TNonblockingBatch<T>::Drop()
+{
+ std::queue<TBatch> batches;
+ std::deque<TPromise<TBatch>> promises;
+ {
+ auto guard = Guard(SpinLock_);
+ Batches_.swap(batches);
+ Promises_.swap(promises);
+ CurrentBatch_.clear();
+ ResetTimer(guard);
+ }
+ for (auto&& promise : promises) {
+ promise.Set(TBatch{});
+ }
+}
+
+template <class T>
+void TNonblockingBatch<T>::UpdateMaxBatchSize(int maxBatchSize)
+{
+ auto guard = Guard(SpinLock_);
+ MaxBatchSize_ = maxBatchSize;
+}
+
+template <class T>
+void TNonblockingBatch<T>::UpdateBatchDuration(TDuration batchDuration)
+{
+ auto guard = Guard(SpinLock_);
+ BatchDuration_ = batchDuration;
+}
+
+void UpdateBatchDuration(int batchDuration);
+
+template <class T>
+void TNonblockingBatch<T>::ResetTimer(TGuard<NThreading::TSpinLock>& /*guard*/)
+{
+ if (TimerState_ == ETimerState::Started) {
+ ++FlushGeneration_;
+ TDelayedExecutor::CancelAndClear(BatchFlushCookie_);
+ }
+ TimerState_ = ETimerState::Initial;
+}
+
+template <class T>
+void TNonblockingBatch<T>::StartTimer(TGuard<NThreading::TSpinLock>& /*guard*/)
+{
+ if (TimerState_ == ETimerState::Initial && !Promises_.empty() && !CurrentBatch_.empty()) {
+ TimerState_ = ETimerState::Started;
+ BatchFlushCookie_ = TDelayedExecutor::Submit(
+ BIND(&TNonblockingBatch::OnBatchTimeout, MakeWeak(this), FlushGeneration_),
+ BatchDuration_);
+ }
+}
+
+template <class T>
+bool TNonblockingBatch<T>::IsFlushNeeded(TGuard<NThreading::TSpinLock>& /*guard*/) const
+{
+ return
+ static_cast<int>(CurrentBatch_.size()) == MaxBatchSize_ ||
+ TimerState_ == ETimerState::Finished;
+}
+
+template <class T>
+void TNonblockingBatch<T>::CheckFlush(TGuard<NThreading::TSpinLock>& guard)
+{
+ if (!IsFlushNeeded(guard)) {
+ return;
+ }
+ ResetTimer(guard);
+ Batches_.push(std::move(CurrentBatch_));
+ CurrentBatch_.clear();
+ CheckReturn(guard);
+}
+
+template <class T>
+void TNonblockingBatch<T>::CheckReturn(TGuard<NThreading::TSpinLock>& guard)
+{
+ if (Promises_.empty() || Batches_.empty()) {
+ return;
+ }
+ auto batch = std::move(Batches_.front());
+ Batches_.pop();
+ auto promise = std::move(Promises_.front());
+ Promises_.pop_front();
+ guard.Release();
+ promise.Set(std::move(batch));
+}
+
+template <class T>
+void TNonblockingBatch<T>::OnBatchTimeout(ui64 generation)
+{
+ auto guard = Guard(SpinLock_);
+ if (generation != FlushGeneration_) {
+ // Chunk had been prepared.
+ return;
+ }
+ TimerState_ = ETimerState::Finished;
+ CheckFlush(guard);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/nonblocking_batch.h b/yt/yt/core/concurrency/nonblocking_batch.h
new file mode 100644
index 0000000000..dce5e7647a
--- /dev/null
+++ b/yt/yt/core/concurrency/nonblocking_batch.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <yt/yt/core/actions/future.h>
+
+#include <queue>
+#include <vector>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETNonblockingBatchTimerState,
+ (Initial)
+ (Started)
+ (Finished)
+);
+
+//! Nonblocking MPMC queue that supports batching.
+/*!
+ * TNonblockingBatch accepts 2 parameters:
+ * - batchElements is maximum number of elements to be placed inside batch.
+ * - batchDuration is a time period to create the batch.
+ * If producer exceeds batchDuration the consumer receives awaited batch.
+ * If there is no consumer thus the batch will be limited by batchElements.
+ */
+template <class T>
+class TNonblockingBatch
+ : public TRefCounted
+{
+public:
+ using TBatch = std::vector<T>;
+
+ TNonblockingBatch(int maxBatchSize, TDuration batchDuration);
+ ~TNonblockingBatch();
+
+ template <class... U>
+ void Enqueue(U&& ... u);
+
+ TFuture<TBatch> DequeueBatch();
+ void Drop();
+
+ void UpdateMaxBatchSize(int maxBatchSize);
+ void UpdateBatchDuration(TDuration batchDuration);
+
+private:
+ using ETimerState = ETNonblockingBatchTimerState;
+
+ int MaxBatchSize_;
+ TDuration BatchDuration_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TBatch CurrentBatch_;
+ ETimerState TimerState_ = ETimerState::Initial;
+ std::queue<TBatch> Batches_;
+ std::deque<TPromise<TBatch>> Promises_;
+ TDelayedExecutorCookie BatchFlushCookie_;
+ ui64 FlushGeneration_ = 0;
+
+ void ResetTimer(TGuard<NThreading::TSpinLock>& guard);
+ void StartTimer(TGuard<NThreading::TSpinLock>& guard);
+ bool IsFlushNeeded(TGuard<NThreading::TSpinLock>& guard) const;
+ void CheckFlush(TGuard<NThreading::TSpinLock>& guard);
+ void CheckReturn(TGuard<NThreading::TSpinLock>& guard);
+ void OnBatchTimeout(ui64 generation);
+};
+
+template <class T>
+using TNonblockingBatchPtr = TIntrusivePtr<TNonblockingBatch<T>>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define NONBLOCKING_BATCH_INL_H_
+#include "nonblocking_batch-inl.h"
+#undef NONBLOCKING_BATCH_INL_H_
diff --git a/yt/yt/core/concurrency/nonblocking_queue-inl.h b/yt/yt/core/concurrency/nonblocking_queue-inl.h
new file mode 100644
index 0000000000..d35401753a
--- /dev/null
+++ b/yt/yt/core/concurrency/nonblocking_queue-inl.h
@@ -0,0 +1,50 @@
+#ifndef NONBLOCKING_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include nonblocking_queue.h"
+// For the sake of sane code completion.
+#include "nonblocking_queue.h"
+#endif
+#undef NONBLOCKING_QUEUE_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TNonblockingQueue<T>::Enqueue(TFuture<T> asyncValue)
+{
+ auto guard = Guard(SpinLock_);
+ if (PromiseQueue_.empty()) {
+ ValueQueue_.push(std::move(asyncValue));
+ } else {
+ auto promise = PromiseQueue_.front();
+ PromiseQueue_.pop();
+ guard.Release();
+ promise.SetFrom(std::move(asyncValue));
+ }
+}
+
+template <class T>
+template <class TArg>
+void TNonblockingQueue<T>::Enqueue(TArg&& value)
+{
+ Enqueue(MakeFuture<T>(std::forward<TArg>(value)));
+}
+
+template <class T>
+TFuture<T> TNonblockingQueue<T>::Dequeue()
+{
+ auto guard = Guard(SpinLock_);
+ if (ValueQueue_.empty()) {
+ auto promise = NewPromise<T>();
+ PromiseQueue_.push(promise);
+ return promise.ToFuture();
+ } else {
+ auto future = std::move(ValueQueue_.front());
+ ValueQueue_.pop();
+ return future;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/nonblocking_queue.h b/yt/yt/core/concurrency/nonblocking_queue.h
new file mode 100644
index 0000000000..64d676d652
--- /dev/null
+++ b/yt/yt/core/concurrency/nonblocking_queue.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TNonblockingQueue
+ : private TNonCopyable
+{
+public:
+ void Enqueue(TFuture<T> asyncValue);
+
+ // This template is required to enable perfect forwarding.
+ template <class TArg>
+ void Enqueue(TArg&& value);
+
+ // Dequeued futures could be set in arbitrary order.
+ TFuture<T> Dequeue();
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ TRingQueue<TFuture<T>> ValueQueue_;
+ TRingQueue<TPromise<T>> PromiseQueue_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define NONBLOCKING_QUEUE_INL_H_
+#include "nonblocking_queue-inl.h"
+#undef NONBLOCKING_QUEUE_INL_H_
diff --git a/yt/yt/core/concurrency/notify_manager.cpp b/yt/yt/core/concurrency/notify_manager.cpp
new file mode 100644
index 0000000000..743f06f0ac
--- /dev/null
+++ b/yt/yt/core/concurrency/notify_manager.cpp
@@ -0,0 +1,205 @@
+#include "notify_manager.h"
+#include "private.h"
+
+#define PERIODIC_POLLING
+
+namespace NYT::NConcurrency {
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr auto WaitLimit = TDuration::MicroSeconds(64);
+constexpr auto WaitTimeWarningThreshold = TDuration::Seconds(30);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNotifyManager::TNotifyManager(
+ TIntrusivePtr<NThreading::TEventCount> eventCount,
+ const NProfiling::TTagSet& tagSet,
+ const TDuration pollingPeriod)
+ : EventCount_(std::move(eventCount))
+ , WakeupCounter_(NProfiling::TProfiler("/action_queue")
+ .WithTags(tagSet)
+ .WithHot()
+ .Counter("/wakeup"))
+ , WakeupByTimeoutCounter_(NProfiling::TProfiler("/action_queue")
+ .WithTags(tagSet)
+ .Counter("/wakeup_by_timeout"))
+ , PollingPeriod_(pollingPeriod)
+{ }
+
+TCpuInstant TNotifyManager::GetMinEnqueuedAt() const
+{
+ return MinEnqueuedAt_.load(std::memory_order::acquire);
+}
+
+TCpuInstant TNotifyManager::UpdateMinEnqueuedAt(TCpuInstant newMinEnqueuedAt)
+{
+ auto minEnqueuedAt = MinEnqueuedAt_.load();
+
+ while (newMinEnqueuedAt < minEnqueuedAt) {
+ if (MinEnqueuedAt_.compare_exchange_weak(minEnqueuedAt, newMinEnqueuedAt)) {
+ minEnqueuedAt = newMinEnqueuedAt;
+ YT_VERIFY(minEnqueuedAt != SentinelMinEnqueuedAt);
+ break;
+ }
+ }
+
+ return minEnqueuedAt;
+}
+
+TCpuInstant TNotifyManager::ResetMinEnqueuedAt()
+{
+ // Disables notifies of already enqueued actions from invoke and
+ // allows to set MinEnqueuedAt in NotifyFromInvoke for new actions.
+ return MinEnqueuedAt_.exchange(SentinelMinEnqueuedAt);
+}
+
+void TNotifyManager::NotifyFromInvoke(TCpuInstant cpuInstant, bool force)
+{
+ auto minEnqueuedAt = GetMinEnqueuedAt();
+
+ if (minEnqueuedAt == SentinelMinEnqueuedAt) {
+ MinEnqueuedAt_.compare_exchange_strong(minEnqueuedAt, cpuInstant);
+ }
+
+ auto waitTime = CpuDurationToDuration(cpuInstant - minEnqueuedAt);
+ bool needNotify = force || waitTime > WaitLimit;
+
+ YT_LOG_TRACE("Notify from invoke (Force: %v, Decision: %v, WaitTime: %v, MinEnqueuedAt: %v)",
+ force,
+ needNotify,
+ waitTime,
+ CpuInstantToInstant(minEnqueuedAt));
+
+ if (needNotify) {
+ NotifyOne(cpuInstant);
+ }
+}
+
+void TNotifyManager::NotifyAfterFetch(TCpuInstant cpuInstant, TCpuInstant newMinEnqueuedAt)
+{
+ auto minEnqueuedAt = UpdateMinEnqueuedAt(newMinEnqueuedAt);
+
+ // If there are actions and wait time is small do not wakeup other threads.
+ auto waitTime = CpuDurationToDuration(cpuInstant - minEnqueuedAt);
+
+ if (waitTime > WaitLimit) {
+ YT_LOG_TRACE("Notify after fetch (WaitTime: %v, MinEnqueuedAt: %v)",
+ waitTime,
+ CpuInstantToInstant(minEnqueuedAt));
+
+ NotifyOne(cpuInstant);
+ }
+
+ // Reset LockedInstant to suppress action stuck warnings in case of progress.
+ LockedInstant_ = cpuInstant;
+}
+
+void TNotifyManager::Wait(NThreading::TEventCount::TCookie cookie, std::function<bool()> isStopping)
+{
+ if (UnlockNotifies()) {
+ // We must call either Wait or CancelWait.
+ EventCount_->CancelWait();
+ return;
+ }
+
+#ifdef PERIODIC_POLLING
+ // One waiter makes periodic polling.
+ bool firstWaiter = !PollingWaiterLock_.exchange(true);
+ if (firstWaiter) {
+ while (true) {
+ bool notified = EventCount_->Wait(cookie, PollingPeriod_);
+ if (notified) {
+ break;
+ }
+
+ // Check wait time.
+ auto minEnqueuedAt = GetMinEnqueuedAt();
+ auto cpuInstant = GetCpuInstant();
+ auto waitTime = CpuDurationToDuration(cpuInstant - minEnqueuedAt);
+
+ if (waitTime > WaitLimit) {
+ YT_LOG_DEBUG("Wake up by timeout (WaitTime: %v, MinEnqueuedAt: %v)",
+ waitTime,
+ CpuInstantToInstant(minEnqueuedAt));
+
+ WakeupByTimeoutCounter_.Increment();
+
+ break;
+ }
+
+ cookie = EventCount_->PrepareWait();
+
+ // We have to check stopping between Prepare and Wait.
+ // If we check before PrepareWait stop can occur (and notify) after check and before prepare
+ // wait. In this case we can miss it and go waiting.
+ if (isStopping()) {
+ EventCount_->CancelWait();
+ break;
+ }
+ }
+
+ PollingWaiterLock_.store(false);
+ } else {
+ EventCount_->Wait(cookie);
+ }
+#else
+ Y_UNUSED(isStopping);
+ Y_UNUSED(PollingPeriod);
+ EventCount_->Wait(cookie);
+#endif
+
+ UnlockNotifies();
+
+ WakeupCounter_.Increment();
+}
+
+void TNotifyManager::CancelWait()
+{
+ EventCount_->CancelWait();
+
+ // TODO(lukyan): This logic can be moved into NotifyAfterFetch.
+#ifdef PERIODIC_POLLING
+ // If we got an action and PollingWaiterLock_ is not locked (no polling waiter) wake up other thread.
+ if (!PollingWaiterLock_.load()) {
+ EventCount_->NotifyOne();
+ }
+#endif
+}
+
+NThreading::TEventCount* TNotifyManager::GetEventCount()
+{
+ return EventCount_.Get();
+}
+
+// Returns true if was locked.
+bool TNotifyManager::UnlockNotifies()
+{
+ return NotifyLock_.exchange(false);
+}
+
+void TNotifyManager::NotifyOne(TCpuInstant cpuInstant)
+{
+ if (!NotifyLock_.exchange(true)) {
+ LockedInstant_ = cpuInstant;
+ YT_LOG_TRACE("Notify futex (MinEnqueuedAt: %v)",
+ CpuInstantToInstant(GetMinEnqueuedAt()));
+ EventCount_->NotifyOne();
+ } else {
+ auto lockedInstant = LockedInstant_.load();
+ auto waitTime = CpuDurationToDuration(cpuInstant - lockedInstant);
+ if (waitTime > WaitTimeWarningThreshold) {
+ // Notifications are locked during more than 30 seconds.
+ YT_LOG_WARNING("Action is probably stuck (MinEnqueuedAt: %v, LockedInstant: %v, WaitTime: %v)",
+ CpuInstantToInstant(GetMinEnqueuedAt()),
+ lockedInstant,
+ waitTime);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/notify_manager.h b/yt/yt/core/concurrency/notify_manager.h
new file mode 100644
index 0000000000..f62383e3f2
--- /dev/null
+++ b/yt/yt/core/concurrency/notify_manager.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNotifyManager
+{
+public:
+ TNotifyManager(
+ TIntrusivePtr<NThreading::TEventCount> eventCount,
+ const NProfiling::TTagSet& counterTagSet,
+ const TDuration pollingPeriod);
+
+ TCpuInstant ResetMinEnqueuedAt();
+
+ TCpuInstant UpdateMinEnqueuedAt(TCpuInstant newMinEnqueuedAt);
+
+ void NotifyFromInvoke(TCpuInstant cpuInstant, bool force);
+
+ // Must be called after DoCancelWait.
+ void NotifyAfterFetch(TCpuInstant cpuInstant, TCpuInstant newMinEnqueuedAt);
+
+ void Wait(NThreading::TEventCount::TCookie cookie, std::function<bool()> isStopping);
+
+ void CancelWait();
+
+ NThreading::TEventCount* GetEventCount();
+
+private:
+ static constexpr TCpuInstant SentinelMinEnqueuedAt = std::numeric_limits<TCpuInstant>::max();
+
+ const TIntrusivePtr<NThreading::TEventCount> EventCount_;
+ const NProfiling::TCounter WakeupCounter_;
+ const NProfiling::TCounter WakeupByTimeoutCounter_;
+ const TDuration PollingPeriod_;
+
+ std::atomic<bool> NotifyLock_ = false;
+ // LockedInstant is used for debug and check purpose.
+ std::atomic<TCpuInstant> LockedInstant_ = 0;
+ std::atomic<bool> PollingWaiterLock_ = false;
+ std::atomic<TCpuInstant> MinEnqueuedAt_ = SentinelMinEnqueuedAt;
+
+ // Returns true if was locked.
+ bool UnlockNotifies();
+
+ void NotifyOne(TCpuInstant cpuInstant);
+
+ TCpuInstant GetMinEnqueuedAt() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/periodic_executor.cpp b/yt/yt/core/concurrency/periodic_executor.cpp
new file mode 100644
index 0000000000..d979cb294d
--- /dev/null
+++ b/yt/yt/core/concurrency/periodic_executor.cpp
@@ -0,0 +1,296 @@
+#include "periodic_executor.h"
+#include "scheduler.h"
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/utilex/random.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPeriodicExecutorOptions TPeriodicExecutorOptions::WithJitter(TDuration period)
+{
+ return {
+ .Period = period,
+ .Jitter = DefaultJitter
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPeriodicExecutor::TPeriodicExecutor(
+ IInvokerPtr invoker,
+ TClosure callback,
+ std::optional<TDuration> period)
+ : TPeriodicExecutor(
+ std::move(invoker),
+ std::move(callback),
+ {.Period = period})
+{ }
+
+TPeriodicExecutor::TPeriodicExecutor(
+ IInvokerPtr invoker,
+ TClosure callback,
+ TPeriodicExecutorOptions options)
+ : Invoker_(std::move(invoker))
+ , Callback_(std::move(callback))
+ , Period_(options.Period)
+ , Splay_(options.Splay)
+ , Jitter_(options.Jitter)
+{
+ YT_VERIFY(Invoker_);
+ YT_VERIFY(Callback_);
+}
+
+void TPeriodicExecutor::Start()
+{
+ auto guard = Guard(SpinLock_);
+
+ if (Started_) {
+ return;
+ }
+
+ ExecutedPromise_ = TPromise<void>();
+ IdlePromise_ = TPromise<void>();
+ Started_ = true;
+ if (Period_) {
+ PostDelayedCallback(RandomDuration(Splay_));
+ }
+}
+
+void TPeriodicExecutor::DoStop(TGuard<NThreading::TSpinLock>& guard)
+{
+ if (!Started_) {
+ return;
+ }
+
+ Started_ = false;
+ OutOfBandRequested_ = false;
+ auto executedPromise = ExecutedPromise_;
+ auto executionCanceler = ExecutionCanceler_;
+ TDelayedExecutor::CancelAndClear(Cookie_);
+
+ guard.Release();
+
+ if (executedPromise) {
+ executedPromise.TrySet(MakeStoppedError());
+ }
+
+ if (executionCanceler) {
+ executionCanceler(MakeStoppedError());
+ }
+}
+
+TFuture<void> TPeriodicExecutor::Stop()
+{
+ auto guard = Guard(SpinLock_);
+ if (ExecutingCallback_) {
+ InitIdlePromise();
+ auto idlePromise = IdlePromise_;
+ DoStop(guard);
+ return idlePromise;
+ } else {
+ DoStop(guard);
+ return VoidFuture;
+ }
+}
+
+TError TPeriodicExecutor::MakeStoppedError()
+{
+ return TError(NYT::EErrorCode::Canceled, "Periodic executor is stopped");
+}
+
+void TPeriodicExecutor::InitIdlePromise()
+{
+ if (IdlePromise_) {
+ return;
+ }
+
+ if (Started_) {
+ IdlePromise_ = NewPromise<void>();
+ } else {
+ IdlePromise_ = MakePromise<void>(TError());
+ }
+}
+
+void TPeriodicExecutor::InitExecutedPromise()
+{
+ if (ExecutedPromise_) {
+ return;
+ }
+
+ if (Started_) {
+ ExecutedPromise_ = NewPromise<void>();
+ } else {
+ ExecutedPromise_ = MakePromise<void>(MakeStoppedError());
+ }
+}
+
+void TPeriodicExecutor::ScheduleOutOfBand()
+{
+ auto guard = Guard(SpinLock_);
+ if (!Started_)
+ return;
+
+ if (Busy_) {
+ OutOfBandRequested_ = true;
+ } else {
+ guard.Release();
+ PostCallback();
+ }
+}
+
+void TPeriodicExecutor::PostDelayedCallback(TDuration delay)
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ TDelayedExecutor::CancelAndClear(Cookie_);
+ Cookie_ = TDelayedExecutor::Submit(
+ BIND_NO_PROPAGATE(&TPeriodicExecutor::OnTimer, MakeWeak(this)),
+ delay,
+ GetSyncInvoker());
+}
+
+void TPeriodicExecutor::PostCallback()
+{
+ auto this_ = MakeWeak(this);
+ GuardedInvoke(
+ Invoker_,
+ BIND_NO_PROPAGATE(&TPeriodicExecutor::OnCallbackSuccess, this_),
+ BIND_NO_PROPAGATE(&TPeriodicExecutor::OnCallbackFailure, this_));
+}
+
+void TPeriodicExecutor::OnTimer(bool aborted)
+{
+ if (aborted) {
+ return;
+ }
+ PostCallback();
+}
+
+void TPeriodicExecutor::OnCallbackSuccess()
+{
+ TPromise<void> executedPromise;
+ {
+ auto guard = Guard(SpinLock_);
+ if (!Started_ || Busy_) {
+ return;
+ }
+ Busy_ = true;
+ ExecutingCallback_ = true;
+ ExecutionCanceler_ = GetCurrentFiberCanceler();
+ TDelayedExecutor::CancelAndClear(Cookie_);
+ if (ExecutedPromise_) {
+ executedPromise = ExecutedPromise_;
+ ExecutedPromise_ = TPromise<void>();
+ }
+ if (IdlePromise_) {
+ IdlePromise_ = NewPromise<void>();
+ }
+ }
+
+ auto cleanup = [=, this] (bool aborted) {
+ if (aborted) {
+ return;
+ }
+
+ TPromise<void> idlePromise;
+ {
+ auto guard = Guard(SpinLock_);
+ idlePromise = IdlePromise_;
+ ExecutingCallback_ = false;
+ ExecutionCanceler_.Reset();
+ }
+
+ if (idlePromise) {
+ idlePromise.TrySet();
+ }
+
+ if (executedPromise) {
+ executedPromise.TrySet();
+ }
+
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(Busy_);
+ Busy_ = false;
+
+ if (!Started_) {
+ return;
+ }
+
+ if (OutOfBandRequested_) {
+ OutOfBandRequested_ = false;
+ guard.Release();
+ PostCallback();
+ } else if (Period_) {
+ PostDelayedCallback(NextDelay());
+ }
+ };
+
+ try {
+ Callback_();
+ } catch (const TFiberCanceledException&) {
+ // There's very little we can do here safely;
+ // in particular, we should refrain from setting promises;
+ // let's forward the call to the delayed executor.
+ TDelayedExecutor::Submit(
+ BIND([this_ = MakeStrong(this), cleanup = std::move(cleanup)] (bool aborted) {
+ cleanup(aborted);
+ }),
+ TDuration::Zero());
+ throw;
+ }
+
+ cleanup(false);
+}
+
+void TPeriodicExecutor::OnCallbackFailure()
+{
+ auto guard = Guard(SpinLock_);
+
+ if (!Started_) {
+ return;
+ }
+
+ if (Period_) {
+ PostDelayedCallback(NextDelay());
+ }
+}
+
+void TPeriodicExecutor::SetPeriod(std::optional<TDuration> period)
+{
+ auto guard = Guard(SpinLock_);
+
+ // Kick-start invocations, if needed.
+ if (Started_ && period && (!Period_ || *period < *Period_) && !Busy_) {
+ PostDelayedCallback(RandomDuration(Splay_));
+ }
+
+ Period_ = period;
+}
+
+TFuture<void> TPeriodicExecutor::GetExecutedEvent()
+{
+ auto guard = Guard(SpinLock_);
+ InitExecutedPromise();
+ return ExecutedPromise_.ToFuture().ToUncancelable();
+}
+
+TDuration TPeriodicExecutor::NextDelay()
+{
+ if (Jitter_ == 0.0) {
+ return *Period_;
+ } else {
+ auto period = *Period_;
+ period += RandomDuration(period) * Jitter_ - period * Jitter_ / 2.;
+ return period;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/periodic_executor.h b/yt/yt/core/concurrency/periodic_executor.h
new file mode 100644
index 0000000000..7d14eed51b
--- /dev/null
+++ b/yt/yt/core/concurrency/periodic_executor.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include "public.h"
+#include "delayed_executor.h"
+
+#include <yt/yt/core/actions/callback.h>
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPeriodicExecutorOptions
+{
+ static constexpr double DefaultJitter = 0.2;
+
+ //! Interval between usual consequent invocations; if null then no invocations will be happening.
+ std::optional<TDuration> Period;
+ TDuration Splay;
+ double Jitter = 0.0;
+
+ //! Sets #Period and Applies set#DefaultJitter.
+ static TPeriodicExecutorOptions WithJitter(TDuration period);
+};
+
+//! Helps to perform certain actions periodically.
+class TPeriodicExecutor
+ : public TRefCounted
+{
+public:
+ //! Initializes an instance.
+ /*!
+ * \note
+ * We must call #Start to activate the instance.
+ *
+ * \param invoker Invoker used for wrapping actions.
+ * \param callback Callback to invoke periodically.
+ * \param options Period, splay, etc.
+ */
+ TPeriodicExecutor(
+ IInvokerPtr invoker,
+ TClosure callback,
+ TPeriodicExecutorOptions options);
+
+ TPeriodicExecutor(
+ IInvokerPtr invoker,
+ TClosure callback,
+ std::optional<TDuration> period = {});
+
+ //! Starts the instance.
+ //! The first invocation happens with a random delay within splay time.
+ void Start();
+
+ //! Stops the instance, cancels all subsequent invocations.
+ //! Returns a future that becomes set when all outstanding callback
+ //! invocations are finished and no more invocations are expected to happen.
+ TFuture<void> Stop();
+
+ //! Requests an immediate invocation.
+ void ScheduleOutOfBand();
+
+ //! Changes execution period.
+ void SetPeriod(std::optional<TDuration> period);
+
+ //! Returns the future that become set when
+ //! at least one action be fully executed from the moment of method call.
+ //! Cancellation of the returned future will not affect the action
+ //! or other futures returned by this method.
+ TFuture<void> GetExecutedEvent();
+
+private:
+ const IInvokerPtr Invoker_;
+ const TClosure Callback_;
+ std::optional<TDuration> Period_;
+ const TDuration Splay_;
+ const double Jitter_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ bool Started_ = false;
+ bool Busy_ = false;
+ bool OutOfBandRequested_ = false;
+ bool ExecutingCallback_ = false;
+ TCallback<void(const TError&)> ExecutionCanceler_;
+ TDelayedExecutorCookie Cookie_;
+ TPromise<void> IdlePromise_;
+ TPromise<void> ExecutedPromise_;
+
+ void DoStop(TGuard<NThreading::TSpinLock>& guard);
+
+ static TError MakeStoppedError();
+
+ void InitIdlePromise();
+ void InitExecutedPromise();
+
+ void PostDelayedCallback(TDuration delay);
+ void PostCallback();
+
+ void OnTimer(bool aborted);
+ void OnCallbackSuccess();
+ void OnCallbackFailure();
+
+ TDuration NextDelay();
+};
+
+DEFINE_REFCOUNTED_TYPE(TPeriodicExecutor)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/periodic_yielder.cpp b/yt/yt/core/concurrency/periodic_yielder.cpp
new file mode 100644
index 0000000000..55292f7cfb
--- /dev/null
+++ b/yt/yt/core/concurrency/periodic_yielder.cpp
@@ -0,0 +1,43 @@
+#include "periodic_yielder.h"
+
+#include "scheduler.h"
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPeriodicYielder::TPeriodicYielder(TDuration period)
+ : Period_(DurationToCpuDuration(period))
+ , Disabled_(false)
+{ }
+
+bool TPeriodicYielder::TryYield()
+{
+ if (Disabled_) {
+ return false;
+ }
+
+ if (GetCpuInstant() - LastYieldTime_ > Period_) {
+ Yield();
+ LastYieldTime_ = GetCpuInstant();
+ return true;
+ }
+
+ return false;
+}
+
+void TPeriodicYielder::SetDisabled(bool value)
+{
+ Disabled_ = value;
+}
+
+void TPeriodicYielder::SetPeriod(TDuration value)
+{
+ Period_ = DurationToCpuDuration(value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/periodic_yielder.h b/yt/yt/core/concurrency/periodic_yielder.h
new file mode 100644
index 0000000000..1ab66804d7
--- /dev/null
+++ b/yt/yt/core/concurrency/periodic_yielder.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPeriodicYielder
+{
+public:
+ TPeriodicYielder() = default;
+
+ explicit TPeriodicYielder(TDuration period);
+
+ //! Returns true, if we have released the thread and got back to execution.
+ bool TryYield();
+
+ void SetPeriod(TDuration period);
+
+ void SetDisabled(bool value);
+
+private:
+ NProfiling::TCpuDuration Period_;
+ NProfiling::TCpuInstant LastYieldTime_ = NProfiling::GetCpuInstant();
+ bool Disabled_ = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/pollable_detail.cpp b/yt/yt/core/concurrency/pollable_detail.cpp
new file mode 100644
index 0000000000..991a8eea91
--- /dev/null
+++ b/yt/yt/core/concurrency/pollable_detail.cpp
@@ -0,0 +1,24 @@
+#include "pollable_detail.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPollableBase::SetCookie(TCookiePtr cookie)
+{
+ Cookie_ = std::move(cookie);
+}
+
+void* TPollableBase::GetCookie() const
+{
+ return Cookie_.Get();
+}
+
+EPollablePriority TPollableBase::GetPriority() const
+{
+ return EPollablePriority::Normal;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/pollable_detail.h b/yt/yt/core/concurrency/pollable_detail.h
new file mode 100644
index 0000000000..4270e7c270
--- /dev/null
+++ b/yt/yt/core/concurrency/pollable_detail.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "poller.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base class for implementing IPollable.
+class TPollableBase
+ : public IPollable
+{
+public:
+ void SetCookie(TCookiePtr cookie) override;
+ void* GetCookie() const override;
+
+ EPollablePriority GetPriority() const override;
+
+private:
+ TCookiePtr Cookie_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/poller.h b/yt/yt/core/concurrency/poller.h
new file mode 100644
index 0000000000..8a2c2e3a97
--- /dev/null
+++ b/yt/yt/core/concurrency/poller.h
@@ -0,0 +1,124 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Describes the types of events a pollable entity may be interested in.
+DEFINE_BIT_ENUM(EPollControl,
+ ((None) (0x0))
+ ((Read) (0x1)) // Pending read (OnEvent, Arm)
+ ((Write) (0x2)) // Pending write (OnEvent, Arm)
+ ((Retry) (0x8)) // Retry requested (OnEvent)
+ ((EdgeTriggered)(0x10)) // TODO(khlebnikov) make it default (Arm)
+ ((ReadHup) (0x20))
+ ((BacklogEmpty) (0x40)) // Socket is consumed for now (used for optimization purposes)
+ ((Offline) (0x80)) // Cannot handle events (for external use)
+ ((Terminate) (0x100)) // Termination requested (for external use)
+ ((Running) (0x200)) // Operation in progress (for external use)
+ ((Shutdown) (0x400)) // Shutdown in progress (for external use)
+);
+
+//! Poller may provide separate sets of threads for handling pollables of
+//! various priorities.
+DEFINE_ENUM(EPollablePriority,
+ ((Normal) (0))
+ ((RealTime) (1))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Describes an FD-backed pollable entity.
+struct IPollable
+ : public virtual TRefCounted
+{
+ //! Cookie is an opaque ref-counted object that could be attached to a pollable by its poller.
+ using TCookiePtr = TRefCountedPtr;
+
+ //! Attaches a cookie.
+ virtual void SetCookie(TCookiePtr cookie) = 0;
+
+ //! Returns the attached (type-erased) cookie.
+ virtual void* GetCookie() const = 0;
+
+ //! Returns a human-readable string used for diagnostic purposes.
+ virtual const TString& GetLoggingTag() const = 0;
+
+ //! Returns the priority of this pollable.
+ //! The result need not be stable (i.e. may vary across calls).
+ virtual EPollablePriority GetPriority() const = 0;
+
+ //! Called by the poller when the appropriate event is triggered for the FD.
+ virtual void OnEvent(EPollControl control) = 0;
+
+ //! Called by the poller when the pollable entity is unregistered.
+ virtual void OnShutdown() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPollable)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables polling a collection of IPollable-s.
+/*!
+ * A poller is typically implemented as a thread or a pool of threads running
+ * a polling loop.
+ *
+ * Additionally a poller provides means to execute arbitrary callbacks via IInvoker interface,
+ * \see IPoller::GetInvoker.
+ */
+struct IPoller
+ : public virtual TRefCounted
+{
+ //! Shuts the poller down; e.g. reliably terminates the threads.
+ //! The poller is not longer usable after this call.
+ virtual void Shutdown() = 0;
+
+ //! Tries to register a pollable entity but does not arm the poller yet.
+ //! Returns |false| if the poller is already shutting down.
+ virtual bool TryRegister(const IPollablePtr& pollable) = 0;
+
+ //! Unregisters the previously registered entity.
+ /*!
+ * If the pollable entity was previously armed, one should unarm it first
+ * before unregistering. Not doing so is OK for the poller, however
+ * in this case #IPollable::OnShutdown and #IPollable::OnEvent could be invoked concurrently.
+ *
+ * At the same time, if the poller was properly unarmed before unregistering,
+ * it is guaranteed that #IPollable::OnShutdown and #IPollable::OnEvent will
+ * not be run concurrently.
+ *
+ * The entity gets unregistered asynchronously.
+ *
+ * \returns a future that is set when the entity becomes unregistered
+ * (after #IPollable::OnShutdown is invoked)
+ */
+ virtual TFuture<void> Unregister(const IPollablePtr& pollable) = 0;
+
+ //! Arms the poller to handle events of a given type for a given entity.
+ //! Can be called multiple times if one-shot mode is used.
+ virtual void Arm(TFileDescriptor fd, const IPollablePtr& pollable, EPollControl control) = 0;
+
+ //! Schedule call of #IPollable::OnEvent with EPollControl::Retry.
+ //! From OnEvent could be called with wakeup = false to not wake new thread.
+ virtual void Retry(const IPollablePtr& pollable, bool wakeup = true) = 0;
+
+ //! Unarms the poller.
+ virtual void Unarm(TFileDescriptor fd, const IPollablePtr& pollable) = 0;
+
+ //! Returns the invoker capable of executing arbitrary callbacks
+ //! in the poller's context.
+ virtual IInvokerPtr GetInvoker() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPoller)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/private.h b/yt/yt/core/concurrency/private.h
new file mode 100644
index 0000000000..7105cf4bfa
--- /dev/null
+++ b/yt/yt/core/concurrency/private.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEnqueuedAction;
+
+class TMpmcQueueImpl;
+class TMpscQueueImpl;
+
+template <class TQueueImpl>
+class TInvokerQueue;
+
+template <class TQueueImpl>
+using TInvokerQueuePtr = TIntrusivePtr<TInvokerQueue<TQueueImpl>>;
+
+using TMpmcInvokerQueue = TInvokerQueue<TMpmcQueueImpl>;
+using TMpmcInvokerQueuePtr = TIntrusivePtr<TMpmcInvokerQueue>;
+
+using TMpscInvokerQueue = TInvokerQueue<TMpscQueueImpl>;
+using TMpscInvokerQueuePtr = TIntrusivePtr<TMpscInvokerQueue>;
+
+template <class TQueueImpl>
+class TSingleQueueSchedulerThread;
+
+template <class TQueueImpl>
+using TSingleQueueSchedulerThreadPtr = TIntrusivePtr<TSingleQueueSchedulerThread<TQueueImpl>>;
+
+template <class TQueueImpl>
+class TSuspendableSingleQueueSchedulerThread;
+
+template <class TQueueImpl>
+using TSuspendableSingleQueueSchedulerThreadPtr = TIntrusivePtr<TSuspendableSingleQueueSchedulerThread<TQueueImpl>>;
+
+using TMpmcSingleQueueSchedulerThread = TSingleQueueSchedulerThread<TMpmcQueueImpl>;
+using TMpmcSingleQueueSchedulerThreadPtr = TIntrusivePtr<TMpmcSingleQueueSchedulerThread>;
+
+using TMpscSingleQueueSchedulerThread = TSingleQueueSchedulerThread<TMpscQueueImpl>;
+using TMpscSingleQueueSchedulerThreadPtr = TIntrusivePtr<TMpscSingleQueueSchedulerThread>;
+
+using TMpscSuspendableSingleQueueSchedulerThread = TSuspendableSingleQueueSchedulerThread<TMpscQueueImpl>;
+using TMpscSuspendableSingleQueueSchedulerThreadPtr = TIntrusivePtr<TMpscSuspendableSingleQueueSchedulerThread>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TFiber)
+
+DECLARE_REFCOUNTED_CLASS(TFairShareInvokerQueue)
+DECLARE_REFCOUNTED_CLASS(TFairShareQueueSchedulerThread)
+DECLARE_REFCOUNTED_STRUCT(IFairShareCallbackQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger ConcurrencyLogger("Concurrency");
+inline const NProfiling::TProfiler ConcurrencyProfiler("/concurrency");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/profiling_helpers.cpp b/yt/yt/core/concurrency/profiling_helpers.cpp
new file mode 100644
index 0000000000..4eb9c44c43
--- /dev/null
+++ b/yt/yt/core/concurrency/profiling_helpers.cpp
@@ -0,0 +1,44 @@
+#include "profiling_helpers.h"
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTagSet GetThreadTags(
+ const TString& threadName)
+{
+ TTagSet tags;
+ tags.AddTag(std::pair<TString, TString>("thread", threadName));
+ return tags;
+}
+
+TTagSet GetBucketTags(
+ const TString& threadName,
+ const TString& bucketName)
+{
+ TTagSet tags;
+
+ tags.AddTag(std::pair<TString, TString>("thread", threadName));
+ tags.AddTag(std::pair<TString, TString>("bucket", bucketName), -1);
+
+ return tags;
+}
+
+TTagSet GetQueueTags(
+ const TString& threadName,
+ const TString& queueName)
+{
+ TTagSet tags;
+
+ tags.AddTag(std::pair<TString, TString>("thread", threadName));
+ tags.AddTag(std::pair<TString, TString>("queue", queueName), -1);
+
+ return tags;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/profiling_helpers.h b/yt/yt/core/concurrency/profiling_helpers.h
new file mode 100644
index 0000000000..a5338b32cd
--- /dev/null
+++ b/yt/yt/core/concurrency/profiling_helpers.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/yt/library/profiling/tag.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NProfiling::TTagSet GetThreadTags(const TString& threadName);
+
+NProfiling::TTagSet GetBucketTags(
+ const TString& threadName,
+ const TString& bucketName);
+
+NProfiling::TTagSet GetQueueTags(
+ const TString& threadName,
+ const TString& queueName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/propagating_storage-inl.h b/yt/yt/core/concurrency/propagating_storage-inl.h
new file mode 100644
index 0000000000..50497d3e0d
--- /dev/null
+++ b/yt/yt/core/concurrency/propagating_storage-inl.h
@@ -0,0 +1,69 @@
+#ifndef PROPAGATING_STORAGE_INL_H_
+#error "Direct inclusion of this file is not allowed, include propagating_storage.h"
+// For the sake of sane code completion.
+#include "propagating_storage.h"
+#endif
+#undef PROPAGATING_STORAGE_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+bool TPropagatingStorage::Has() const
+{
+ return FindRaw(typeid(T)) != nullptr;
+}
+
+template <class T>
+const T& TPropagatingStorage::GetOrCrash() const
+{
+ const auto* result = Find<T>();
+ YT_VERIFY(result);
+ return *result;
+}
+
+template <class T>
+const T* TPropagatingStorage::Find() const
+{
+ const auto* result = FindRaw(typeid(T));
+ return result ? std::any_cast<T>(result) : nullptr;
+}
+
+template <class T>
+std::optional<T> TPropagatingStorage::Exchange(T value)
+{
+ auto result = ExchangeRaw(std::make_any<T>(std::move(value)));
+ return result ? std::make_optional<T>(std::any_cast<T>(std::move(*result))) : std::nullopt;
+}
+
+template <class T>
+std::optional<T> TPropagatingStorage::Remove()
+{
+ auto result = RemoveRaw(typeid(T));
+ return result ? std::make_optional<T>(std::any_cast<T>(std::move(*result))) : std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TPropagatingValueGuard<T>::TPropagatingValueGuard(T value)
+{
+ auto& storage = GetCurrentPropagatingStorage();
+ OldValue_ = storage.Exchange<T>(std::move(value));
+}
+
+template <class T>
+TPropagatingValueGuard<T>::~TPropagatingValueGuard()
+{
+ auto& storage = GetCurrentPropagatingStorage();
+ if (OldValue_) {
+ storage.Exchange<T>(std::move(*OldValue_));
+ } else {
+ storage.Remove<T>();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/propagating_storage.cpp b/yt/yt/core/concurrency/propagating_storage.cpp
new file mode 100644
index 0000000000..69d6fe65ae
--- /dev/null
+++ b/yt/yt/core/concurrency/propagating_storage.cpp
@@ -0,0 +1,270 @@
+#include "propagating_storage.h"
+
+#include <library/cpp/yt/small_containers/compact_flat_map.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPropagatingStorageImplBase
+{
+public:
+ using TStorage = TCompactFlatMap<std::type_index, std::any, 16>;
+
+ bool IsEmpty() const
+ {
+ return Data_.empty();
+ }
+
+ const std::any* GetRaw(const std::type_info& typeInfo) const
+ {
+ auto iter = Data_.find(std::type_index(typeInfo));
+ return iter == Data_.end() ? nullptr : &iter->second;
+ }
+
+ std::optional<std::any> ExchangeRaw(std::any value)
+ {
+ std::type_index key(value.type());
+ auto iter = Data_.find(key);
+ if (iter == Data_.end()) {
+ Data_.emplace(key, std::move(value));
+ return std::nullopt;
+ }
+ return std::exchange(iter->second, std::move(value));
+ }
+
+ std::optional<std::any> RemoveRaw(const std::type_info& typeInfo)
+ {
+ auto iter = Data_.find(std::type_index(typeInfo));
+ if (iter == Data_.end()) {
+ return std::nullopt;
+ }
+ auto result = std::make_optional<std::any>(iter->second);
+ Data_.erase(iter);
+ return result;
+ }
+
+ DEFINE_SIGNAL_SIMPLE(void(), OnBeforeUninstall);
+ DEFINE_SIGNAL_SIMPLE(void(), OnAfterInstall);
+
+private:
+ TStorage Data_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPropagatingStorage::TImpl
+ : public TRefCounted
+ , public TPropagatingStorageImplBase
+{
+public:
+ TImpl() = default;
+
+ TIntrusivePtr<TImpl> Clone() const
+ {
+ return New<TImpl>(static_cast<const TPropagatingStorageImplBase&>(*this));
+ }
+
+private:
+ DECLARE_NEW_FRIEND()
+
+ explicit TImpl(const TPropagatingStorageImplBase& base)
+ : TPropagatingStorageImplBase(base)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPropagatingStorage::TPropagatingStorage() = default;
+
+TPropagatingStorage::TPropagatingStorage(TIntrusivePtr<TImpl> impl)
+ : Impl_(std::move(impl))
+{ }
+
+TPropagatingStorage::~TPropagatingStorage() = default;
+
+TPropagatingStorage::TPropagatingStorage(const TPropagatingStorage& other) = default;
+TPropagatingStorage::TPropagatingStorage(TPropagatingStorage&& other) = default;
+
+TPropagatingStorage& TPropagatingStorage::operator=(const TPropagatingStorage& other) = default;
+TPropagatingStorage& TPropagatingStorage::operator=(TPropagatingStorage&& other) = default;
+
+bool TPropagatingStorage::IsNull() const
+{
+ return !static_cast<bool>(Impl_);
+}
+
+bool TPropagatingStorage::IsEmpty() const
+{
+ return !Impl_ || Impl_->IsEmpty();
+}
+
+const std::any* TPropagatingStorage::FindRaw(const std::type_info& typeInfo) const
+{
+ if (!Impl_) {
+ return nullptr;
+ }
+ return Impl_->GetRaw(typeInfo);
+}
+
+std::optional<std::any> TPropagatingStorage::ExchangeRaw(std::any value)
+{
+ EnsureUnique();
+ return Impl_->ExchangeRaw(std::move(value));
+}
+
+std::optional<std::any> TPropagatingStorage::RemoveRaw(const std::type_info& typeInfo)
+{
+ EnsureUnique();
+ return Impl_->RemoveRaw(typeInfo);
+}
+
+void TPropagatingStorage::SubscribeOnAfterInstall(const TCallback<void()>& callback)
+{
+ EnsureUnique();
+ Impl_->SubscribeOnAfterInstall(callback);
+}
+
+void TPropagatingStorage::UnsubscribeOnAfterInstall(const TCallback<void()>& callback)
+{
+ EnsureUnique();
+ Impl_->UnsubscribeOnAfterInstall(callback);
+}
+
+void TPropagatingStorage::SubscribeOnBeforeUninstall(const TCallback<void()>& callback)
+{
+ EnsureUnique();
+ Impl_->SubscribeOnBeforeUninstall(callback);
+}
+
+void TPropagatingStorage::UnsubscribeOnBeforeUninstall(const TCallback<void()>& callback)
+{
+ EnsureUnique();
+ Impl_->UnsubscribeOnBeforeUninstall(callback);
+}
+
+TPropagatingStorage TPropagatingStorage::Create()
+{
+ return TPropagatingStorage(New<TImpl>());
+}
+
+void TPropagatingStorage::EnsureUnique()
+{
+ if (!Impl_) {
+ Impl_ = New<TImpl>();
+ return;
+ }
+
+ // NB(gepardo). It can be proved that this code doesn't clone only if there are no references to this storage
+ // in other threads, so our copy-on-write mechanism doesn't result in data races.
+ //
+ // Basically, we need to prove the following:
+ //
+ // 1) All the previous unrefs happens-before we obtain the reference count. This is true, because GetRefCount()
+ // does acquire-load on the reference counter, while Unref() does release-store on it.
+ //
+ // 2) Modifying the object happens-before taking any new references. This is true, because we are the only owner
+ // of the reference, so Ref() can only be done later in this thread, so modifications will be sequenced-before
+ // taking new references.
+ auto refCount = Impl_->GetRefCount();
+ if (refCount == 1) {
+ return;
+ }
+ YT_VERIFY(refCount > 1);
+ Impl_ = Impl_->Clone();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPropagatingStorageManager
+{
+public:
+ static TPropagatingStorageManager* Get()
+ {
+ return Singleton<TPropagatingStorageManager>();
+ }
+
+ TPropagatingStorage& GetCurrentPropagatingStorage()
+ {
+ return *Slot_;
+ }
+
+ const TPropagatingStorage& GetPropagatingStorage(const TFls& fls)
+ {
+ return *Slot_.Get(fls);
+ }
+
+ void InstallGlobalSwitchHandler(TPropagatingStorageGlobalSwitchHandler handler)
+ {
+ auto guard = Guard(Lock_);
+ int index = SwitchHandlerCount_.load();
+ YT_VERIFY(index < MaxSwitchHandlerCount);
+ SwitchHandlers_[index] = handler;
+ ++SwitchHandlerCount_;
+ }
+
+ TPropagatingStorage SwitchPropagatingStorage(TPropagatingStorage newStorage)
+ {
+ auto& storage = *Slot_;
+ int count = SwitchHandlerCount_.load(std::memory_order::acquire);
+ for (int index = 0; index < count; ++index) {
+ SwitchHandlers_[index](storage, newStorage);
+ }
+ return std::exchange(storage, std::move(newStorage));
+ }
+
+private:
+ TFlsSlot<TPropagatingStorage> Slot_;
+
+ NThreading::TForkAwareSpinLock Lock_;
+
+ static constexpr int MaxSwitchHandlerCount = 16;
+ std::array<TPropagatingStorageGlobalSwitchHandler, MaxSwitchHandlerCount> SwitchHandlers_;
+ std::atomic<int> SwitchHandlerCount_ = 0;
+
+ TPropagatingStorageManager() = default;
+ Y_DECLARE_SINGLETON_FRIEND()
+};
+
+TPropagatingStorage& GetCurrentPropagatingStorage()
+{
+ return TPropagatingStorageManager::Get()->GetCurrentPropagatingStorage();
+}
+
+const TPropagatingStorage& GetPropagatingStorage(const TFls& fls)
+{
+ return TPropagatingStorageManager::Get()->GetPropagatingStorage(fls);
+}
+
+void InstallGlobalPropagatingStorageSwitchHandler(TPropagatingStorageGlobalSwitchHandler handler)
+{
+ TPropagatingStorageManager::Get()->InstallGlobalSwitchHandler(handler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPropagatingStorageGuard::TPropagatingStorageGuard(TPropagatingStorage storage)
+ : OldStorage_(TPropagatingStorageManager::Get()->SwitchPropagatingStorage(std::move(storage)))
+{ }
+
+TPropagatingStorageGuard::~TPropagatingStorageGuard()
+{
+ TPropagatingStorageManager::Get()->SwitchPropagatingStorage(std::move(OldStorage_));
+}
+
+const TPropagatingStorage& TPropagatingStorageGuard::GetOldStorage() const
+{
+ return OldStorage_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNullPropagatingStorageGuard::TNullPropagatingStorageGuard()
+ : TPropagatingStorageGuard(TPropagatingStorage())
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/propagating_storage.h b/yt/yt/core/concurrency/propagating_storage.h
new file mode 100644
index 0000000000..7daaca4996
--- /dev/null
+++ b/yt/yt/core/concurrency/propagating_storage.h
@@ -0,0 +1,153 @@
+#pragma once
+
+#include "public.h"
+#include "fls.h"
+
+#include <yt/yt/core/actions/signal.h>
+
+#include <any>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fiber-local key-value storage that is able to propagate between fibers.
+//!
+//! When a new callback is created via BIND, then a copy is made into its bind state. So,
+//! this storage is propagated between the fibers creating each other.
+//!
+//! It is a key value storage where keys are type names. Thus, you may consider it as a
+//! set of singletons.
+//!
+//! TPropagatingStorage is copy-on-write, so copying it between fibers is cheap if no values
+//! are modified.
+class TPropagatingStorage
+{
+public:
+ //! Creates a null storage.
+ TPropagatingStorage();
+
+ //! Creates an empty, non-null storage.
+ static TPropagatingStorage Create();
+
+ ~TPropagatingStorage();
+
+ TPropagatingStorage(const TPropagatingStorage& other);
+ TPropagatingStorage(TPropagatingStorage&& other);
+
+ TPropagatingStorage& operator=(const TPropagatingStorage& other);
+ TPropagatingStorage& operator=(TPropagatingStorage&& other);
+
+ //! Returns true if the storage is null.
+ //!
+ //! If the propagating storage is null, it means that there is no underlying storage to keep
+ //! the data in it.
+ //!
+ //! In all other ways, it is indistinguishable from empty storage. If you read from it, you
+ //! will get nulls. If you try to modify it, the underlying storage will be created.
+ //!
+ //! You probably don't want to use this function, as it's mostly used in fiber scheduler to
+ //! verify that propagating storage doesn't leak to unwanted places. Use IsEmpty() instead.
+ bool IsNull() const;
+
+ bool IsEmpty() const;
+
+ template <class T>
+ bool Has() const;
+
+ template <class T>
+ const T& GetOrCrash() const;
+
+ template <class T>
+ const T* Find() const;
+
+ template <class T>
+ std::optional<T> Exchange(T value);
+
+ template <class T>
+ std::optional<T> Remove();
+
+ DECLARE_SIGNAL(void(), OnAfterInstall);
+ DECLARE_SIGNAL(void(), OnBeforeUninstall);
+
+private:
+ class TImpl;
+ TIntrusivePtr<TImpl> Impl_;
+
+ explicit TPropagatingStorage(TIntrusivePtr<TImpl> impl);
+
+ const std::any* FindRaw(const std::type_info& typeInfo) const;
+ std::optional<std::any> ExchangeRaw(std::any value);
+ std::optional<std::any> RemoveRaw(const std::type_info& typeInfo);
+
+ void EnsureUnique();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPropagatingStorage& GetCurrentPropagatingStorage();
+const TPropagatingStorage& GetPropagatingStorage(const NConcurrency::TFls& fls);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Use function pointer to minimize the overhead.
+using TPropagatingStorageGlobalSwitchHandler = void(*)(
+ const TPropagatingStorage& oldStorage,
+ const TPropagatingStorage& newStorage);
+
+void InstallGlobalPropagatingStorageSwitchHandler(
+ TPropagatingStorageGlobalSwitchHandler handler);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPropagatingStorageGuard
+{
+public:
+ explicit TPropagatingStorageGuard(TPropagatingStorage storage);
+ ~TPropagatingStorageGuard();
+
+ TPropagatingStorageGuard(const TPropagatingStorageGuard& other) = delete;
+ TPropagatingStorageGuard(TPropagatingStorageGuard&& other) = delete;
+ TPropagatingStorageGuard& operator=(const TPropagatingStorageGuard& other) = delete;
+ TPropagatingStorageGuard& operator=(TPropagatingStorageGuard&& other) = delete;
+
+ const TPropagatingStorage& GetOldStorage() const;
+
+private:
+ TPropagatingStorage OldStorage_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullPropagatingStorageGuard
+ : public TPropagatingStorageGuard
+{
+public:
+ TNullPropagatingStorageGuard();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TPropagatingValueGuard
+{
+public:
+ explicit TPropagatingValueGuard(T value);
+ ~TPropagatingValueGuard();
+
+ TPropagatingValueGuard(const TPropagatingValueGuard& other) = delete;
+ TPropagatingValueGuard(TPropagatingValueGuard&& other) = delete;
+ TPropagatingValueGuard& operator=(const TPropagatingValueGuard& other) = delete;
+ TPropagatingValueGuard& operator=(TPropagatingValueGuard&& other) = delete;
+
+private:
+ std::optional<T> OldValue_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define PROPAGATING_STORAGE_INL_H_
+#include "propagating_storage-inl.h"
+#undef PROPAGATING_STORAGE_INL_H_
diff --git a/yt/yt/core/concurrency/public.h b/yt/yt/core/concurrency/public.h
new file mode 100644
index 0000000000..fe3ac26f34
--- /dev/null
+++ b/yt/yt/core/concurrency/public.h
@@ -0,0 +1,124 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Enables fiber instances reuse for improved performance.
+#define YT_REUSE_FIBERS
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TActionQueue)
+DECLARE_REFCOUNTED_STRUCT(IThreadPool)
+
+DECLARE_REFCOUNTED_STRUCT(ISuspendableActionQueue)
+
+DECLARE_REFCOUNTED_CLASS(TPeriodicExecutor)
+DECLARE_REFCOUNTED_CLASS(TInvokerAlarm)
+
+DECLARE_REFCOUNTED_CLASS(TAsyncSemaphore)
+DECLARE_REFCOUNTED_CLASS(TProfiledAsyncSemaphore)
+
+DECLARE_REFCOUNTED_STRUCT(IFairShareActionQueue)
+
+DECLARE_REFCOUNTED_STRUCT(IQuantizedExecutor)
+
+namespace NDetail {
+
+DECLARE_REFCOUNTED_STRUCT(TDelayedExecutorEntry)
+
+} // namespace NDetail
+
+using TDelayedExecutorCookie = NDetail::TDelayedExecutorEntryPtr;
+
+DECLARE_REFCOUNTED_CLASS(TThroughputThrottlerConfig)
+DECLARE_REFCOUNTED_CLASS(TRelativeThroughputThrottlerConfig)
+DECLARE_REFCOUNTED_CLASS(TPrefetchingThrottlerConfig)
+DECLARE_REFCOUNTED_STRUCT(IThroughputThrottler)
+DECLARE_REFCOUNTED_STRUCT(IReconfigurableThroughputThrottler)
+
+DECLARE_REFCOUNTED_STRUCT(IAsyncInputStream)
+DECLARE_REFCOUNTED_STRUCT(IAsyncOutputStream)
+
+DECLARE_REFCOUNTED_STRUCT(IFlushableAsyncOutputStream)
+
+DECLARE_REFCOUNTED_STRUCT(IAsyncZeroCopyInputStream)
+DECLARE_REFCOUNTED_STRUCT(IAsyncZeroCopyOutputStream)
+
+DECLARE_REFCOUNTED_STRUCT(IFairShareThreadPool)
+
+DECLARE_REFCOUNTED_CLASS(TAsyncStreamPipe)
+
+DEFINE_ENUM(EWaitForStrategy,
+ (WaitFor)
+ (Get)
+);
+
+class TAsyncSemaphore;
+
+DEFINE_ENUM(EExecutionStackKind,
+ (Small) // 256 Kb (default)
+ (Large) // 8 Mb
+);
+
+class TExecutionStack;
+
+template <class TSignature>
+class TCoroutine;
+
+template <class T>
+class TNonblockingQueue;
+
+template <typename EQueue>
+struct IEnumIndexedFairShareActionQueue;
+
+template <typename EQueue>
+using IEnumIndexedFairShareActionQueuePtr = TIntrusivePtr<IEnumIndexedFairShareActionQueue<EQueue>>;
+
+DECLARE_REFCOUNTED_STRUCT(TLeaseEntry)
+using TLease = TLeaseEntryPtr;
+
+DECLARE_REFCOUNTED_STRUCT(IPollable)
+DECLARE_REFCOUNTED_STRUCT(IPoller)
+DECLARE_REFCOUNTED_STRUCT(IThreadPoolPoller)
+
+DECLARE_REFCOUNTED_CLASS(TThread)
+
+using TFiberId = size_t;
+constexpr size_t InvalidFiberId = 0;
+
+DEFINE_ENUM(EFiberState,
+ (Created)
+ (Running)
+ (Introspecting)
+ (Waiting)
+ (Idle)
+ (Finished)
+);
+
+using TFairShareThreadPoolTag = TString;
+
+DECLARE_REFCOUNTED_STRUCT(IPoolWeightProvider)
+
+DECLARE_REFCOUNTED_STRUCT(ITwoLevelFairShareThreadPool)
+
+DECLARE_REFCOUNTED_CLASS(TFiber)
+
+DECLARE_REFCOUNTED_STRUCT(TFairThrottlerConfig)
+DECLARE_REFCOUNTED_STRUCT(TFairThrottlerBucketConfig)
+DECLARE_REFCOUNTED_STRUCT(IThrottlerIPC)
+DECLARE_REFCOUNTED_STRUCT(IIPCBucket)
+
+DECLARE_REFCOUNTED_CLASS(TFairThrottler)
+DECLARE_REFCOUNTED_CLASS(TBucketThrottler)
+
+DECLARE_REFCOUNTED_STRUCT(ICallbackProvider)
+
+class TPropagatingStorage;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/quantized_executor.cpp b/yt/yt/core/concurrency/quantized_executor.cpp
new file mode 100644
index 0000000000..93893e2b14
--- /dev/null
+++ b/yt/yt/core/concurrency/quantized_executor.cpp
@@ -0,0 +1,287 @@
+#include "quantized_executor.h"
+
+#include "private.h"
+#include "action_queue.h"
+#include "delayed_executor.h"
+#include "scheduler_api.h"
+#include "suspendable_action_queue.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TQuantizedExecutor
+ : public IQuantizedExecutor
+{
+public:
+ TQuantizedExecutor(
+ TString name,
+ ICallbackProviderPtr callbackProvider,
+ int workerCount)
+ : Name_(std::move(name))
+ , Logger(ConcurrencyLogger.WithTag("Executor: %v", Name_))
+ , ControlQueue_(New<TActionQueue>(Format("%vCtl", Name_)))
+ , ControlInvoker_(ControlQueue_->GetInvoker())
+ , CallbackProvider_(std::move(callbackProvider))
+ , DesiredWorkerCount_(workerCount)
+ {
+ VERIFY_INVOKER_THREAD_AFFINITY(ControlInvoker_, ControlThread);
+ }
+
+ void Initialize(TCallback<void()> workerInitializer) override
+ {
+ WorkerInitializer_ = std::move(workerInitializer);
+
+ BIND(&TQuantizedExecutor::DoReconfigure, MakeStrong(this))
+ .AsyncVia(ControlInvoker_)
+ .Run()
+ .Get()
+ .ThrowOnError();
+ }
+
+ TFuture<void> Run(TDuration timeout) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return BIND(&TQuantizedExecutor::StartQuantum, MakeStrong(this), timeout)
+ .AsyncVia(ControlInvoker_)
+ .Run();
+ }
+
+ void Reconfigure(int workerCount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ DesiredWorkerCount_ = workerCount;
+ }
+
+private:
+ const TString Name_;
+
+ const TLogger Logger;
+
+ const TActionQueuePtr ControlQueue_;
+ const IInvokerPtr ControlInvoker_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, CallbackProviderLock_);
+ ICallbackProviderPtr CallbackProvider_;
+
+ TCallback<void()> WorkerInitializer_;
+
+ std::vector<ISuspendableActionQueuePtr> Workers_;
+ std::vector<IInvokerPtr> Invokers_;
+ std::atomic<int> ActiveWorkerCount_ = 0;
+ std::atomic<int> DesiredWorkerCount_ = 0;
+
+ int QuantumIndex_ = 0;
+
+ std::atomic<bool> FinishingQuantum_ = false;
+
+ bool Running_ = false;
+ TPromise<void> QuantumFinished_;
+
+ DECLARE_THREAD_AFFINITY_SLOT(ControlThread);
+
+ void DoReconfigure()
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+ YT_VERIFY(!Running_);
+
+ int desiredWorkerCount = DesiredWorkerCount_.load();
+ if (ActiveWorkerCount_ == desiredWorkerCount) {
+ return;
+ }
+
+ int currentWorkerCount = std::ssize(Workers_);
+
+ YT_LOG_DEBUG("Updating worker count (WorkerCount: %v -> %v)",
+ currentWorkerCount,
+ desiredWorkerCount);
+
+ if (desiredWorkerCount > currentWorkerCount) {
+ Workers_.reserve(desiredWorkerCount);
+ Invokers_.reserve(desiredWorkerCount);
+ for (int index = currentWorkerCount; index < desiredWorkerCount; ++index) {
+ auto worker = CreateSuspendableActionQueue(Format("%v:%v", Name_, index));
+
+ // NB: #GetInvoker initializes queue.
+ Invokers_.push_back(worker->GetInvoker());
+
+ if (WorkerInitializer_) {
+ BIND(WorkerInitializer_)
+ .AsyncVia(Invokers_.back())
+ .Run()
+ .Get();
+ }
+ worker->Suspend(/*immediately*/ true)
+ .Get()
+ .ThrowOnError();
+ Workers_.emplace_back(std::move(worker));
+ }
+ }
+
+ YT_VERIFY(std::ssize(Workers_) >= desiredWorkerCount);
+ ActiveWorkerCount_ = desiredWorkerCount;
+ }
+
+ TFuture<void> StartQuantum(TDuration timeout)
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ DoReconfigure();
+
+ ++QuantumIndex_;
+
+ YT_LOG_TRACE("Starting quantum (Index: %v, Timeout: %v)",
+ QuantumIndex_,
+ timeout);
+
+ YT_VERIFY(!Running_);
+ Running_ = true;
+
+ QuantumFinished_ = NewPromise<void>();
+ YT_UNUSED_FUTURE(QuantumFinished_
+ .ToFuture()
+ .Apply(BIND(&TQuantizedExecutor::OnQuantumFinished, MakeWeak(this), QuantumIndex_)
+ .Via(ControlInvoker_)));
+
+ TDelayedExecutor::Submit(
+ BIND(&TQuantizedExecutor::OnTimeoutReached, MakeWeak(this), QuantumIndex_),
+ timeout,
+ ControlInvoker_);
+
+ ResumeWorkers();
+
+ for (int index = 0; index < std::ssize(Workers_); ++index) {
+ OnWorkerReady(index, QuantumIndex_);
+ }
+
+ return QuantumFinished_.ToFuture();
+ }
+
+ void FinishQuantum(int quantumIndex, bool immediately)
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ if (!Running_ || quantumIndex != QuantumIndex_) {
+ return;
+ }
+
+ if (FinishingQuantum_ && !immediately) {
+ return;
+ }
+
+ YT_LOG_TRACE("Finishing quantum (Index: %v, Immediately: %v)",
+ quantumIndex,
+ immediately);
+
+ FinishingQuantum_ = true;
+
+ QuantumFinished_.TrySetFrom(SuspendWorkers(immediately));
+ }
+
+ TFuture<void> SuspendWorkers(bool immediately)
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ std::vector<TFuture<void>> futures;
+ futures.reserve(Workers_.size());
+ for (const auto& worker : Workers_) {
+ futures.push_back(worker->Suspend(immediately));
+ }
+
+ return AllSucceeded(std::move(futures));
+ }
+
+ void ResumeWorkers()
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ for (const auto& worker : Workers_) {
+ worker->Resume();
+ }
+ }
+
+ void OnWorkerReady(int workerIndex, int quantumIndex)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (!Running_ || quantumIndex != QuantumIndex_) {
+ return;
+ }
+
+ // Worker is disabled, do not schedule new callbacks to it.
+ if (workerIndex >= ActiveWorkerCount_) {
+ return;
+ }
+
+ if (FinishingQuantum_) {
+ return;
+ }
+
+ TCallback<void()> callback;
+ {
+ auto guard = Guard(CallbackProviderLock_);
+ callback = CallbackProvider_->ExtractCallback();
+ }
+
+ if (!callback) {
+ ControlInvoker_->Invoke(
+ BIND(&TQuantizedExecutor::FinishQuantum, MakeStrong(this), quantumIndex, /*immediate*/ false));
+ return;
+ }
+
+ const auto& invoker = Invokers_[workerIndex];
+ invoker->Invoke(BIND([=, this, this_ = MakeStrong(this)] {
+ callback();
+
+ OnWorkerReady(workerIndex, quantumIndex);
+ }));
+ }
+
+ void OnTimeoutReached(int quantumIndex)
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ if (quantumIndex != QuantumIndex_) {
+ return;
+ }
+
+ YT_LOG_TRACE("Quantum timeout reached (Index: %v)",
+ quantumIndex);
+
+ FinishQuantum(quantumIndex, /*immediate*/ true);
+ }
+
+ void OnQuantumFinished(int quantumIndex)
+ {
+ VERIFY_THREAD_AFFINITY(ControlThread);
+
+ YT_LOG_TRACE("Quantum finished (Index: %v)",
+ quantumIndex);
+
+ FinishingQuantum_ = false;
+ Running_ = false;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IQuantizedExecutorPtr CreateQuantizedExecutor(
+ TString name,
+ ICallbackProviderPtr callbackProvider,
+ int workerCount)
+{
+ return New<TQuantizedExecutor>(
+ std::move(name),
+ std::move(callbackProvider),
+ workerCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/quantized_executor.h b/yt/yt/core/concurrency/quantized_executor.h
new file mode 100644
index 0000000000..bea5a4f64c
--- /dev/null
+++ b/yt/yt/core/concurrency/quantized_executor.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICallbackProvider
+ : public TRefCounted
+{
+ virtual TCallback<void()> ExtractCallback() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ICallbackProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A device that executes provided callbacks in multiple threads
+//! during some quanta of time. At another time worker threads are
+//! blocked.
+struct IQuantizedExecutor
+ : public TRefCounted
+{
+ virtual void Initialize(TCallback<void()> workerInitializer = {}) = 0;
+
+ //! Starts new quantum of time, returns a future that becomes set
+ //! when quantum ends.
+ /*!
+ * Quantum ends when either #timeout is reached or there are no more
+ * enqueued callbacks in underlying threads.
+ * Quantum completion is implemented via suspension of underlying
+ * suspendable action queue. Timeout corresponds to immediate suspension
+ * and extracted null callback from callback provider corresponds to
+ * non-immediate suspension. Cf. ISuspendableActionQueue::Suspend.
+ */
+ virtual TFuture<void> Run(TDuration timeout) = 0;
+
+ //! Updates number of workers.
+ virtual void Reconfigure(int workerCount) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IQuantizedExecutor)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IQuantizedExecutorPtr CreateQuantizedExecutor(
+ TString name,
+ ICallbackProviderPtr callbackProvider,
+ int workerCount);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/scheduler.h b/yt/yt/core/concurrency/scheduler.h
new file mode 100644
index 0000000000..e15f726042
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "scheduler_api.h"
diff --git a/yt/yt/core/concurrency/scheduler_api-inl.h b/yt/yt/core/concurrency/scheduler_api-inl.h
new file mode 100644
index 0000000000..579d4e8c6d
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_api-inl.h
@@ -0,0 +1,85 @@
+#ifndef SCHEDULER_API_INL_H_
+#error "Direct inclusion of this file is not allowed, include scheduler_api.h"
+// For the sake of sane code completion.
+#include "scheduler_api.h"
+#endif
+#undef SCHEDULER_API_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitFor(TFuture<T> future, IInvokerPtr invoker)
+{
+ YT_ASSERT(future);
+ YT_ASSERT(invoker);
+
+ WaitUntilSet(future.AsVoid(), std::move(invoker));
+
+ return future.Get();
+}
+
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForFast(TFuture<T> future)
+{
+ YT_ASSERT(future);
+
+ if (!future.IsSet()) {
+ WaitUntilSet(future.AsVoid(), GetCurrentInvoker());
+ }
+
+ return future.Get();
+}
+
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForUnique(const TFuture<T>& future, IInvokerPtr invoker)
+{
+ YT_ASSERT(future);
+ YT_ASSERT(invoker);
+
+ WaitUntilSet(future.AsVoid(), std::move(invoker));
+
+ return future.GetUnique();
+}
+
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForUniqueFast(const TFuture<T>& future)
+{
+ YT_ASSERT(future);
+
+ if (!future.IsSet()) {
+ WaitUntilSet(future.AsVoid(), GetCurrentInvoker());
+ }
+
+ return future.GetUnique();
+}
+
+template <class T>
+TErrorOr<T> WaitForWithStrategy(
+ TFuture<T> future,
+ EWaitForStrategy strategy)
+{
+ switch (strategy) {
+ case EWaitForStrategy::WaitFor:
+ return WaitFor(std::move(future));
+ case EWaitForStrategy::Get:
+ return future.Get();
+ default:
+ YT_ABORT();
+ }
+}
+
+inline void Yield()
+{
+ WaitUntilSet(VoidFuture);
+}
+
+inline void SwitchTo(IInvokerPtr invoker)
+{
+ WaitUntilSet(VoidFuture, std::move(invoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/scheduler_api.h b/yt/yt/core/concurrency/scheduler_api.h
new file mode 100644
index 0000000000..9556538465
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_api.h
@@ -0,0 +1,134 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+#ifdef _win_
+#undef Yield
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+// Forward declaration
+IInvoker* GetCurrentInvoker();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TFiberCanceler = TCallback<void(const TError&)>;
+
+TFiberCanceler GetCurrentFiberCanceler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the current fiber id.
+TFiberId GetCurrentFiberId();
+
+//! Sets the current fiber id.
+void SetCurrentFiberId(TFiberId id);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Use function pointer to minimize the overhead.
+using TGlobalContextSwitchHandler = void(*)();
+
+void InstallGlobalContextSwitchHandlers(
+ TGlobalContextSwitchHandler outHandler,
+ TGlobalContextSwitchHandler inHandler);
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TContextSwitchHandler = std::function<void()>;
+
+class TContextSwitchGuard
+{
+public:
+ TContextSwitchGuard(
+ TContextSwitchHandler outHandler,
+ TContextSwitchHandler inHandler);
+
+ TContextSwitchGuard(const TContextSwitchGuard& other) = delete;
+ ~TContextSwitchGuard();
+};
+
+class TOneShotContextSwitchGuard
+ : public TContextSwitchGuard
+{
+public:
+ explicit TOneShotContextSwitchGuard(TContextSwitchHandler outHandler);
+
+private:
+ bool Active_;
+};
+
+class TForbidContextSwitchGuard
+ : public TOneShotContextSwitchGuard
+{
+public:
+ TForbidContextSwitchGuard();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Blocks the current fiber until #future is set.
+//! The fiber is resceduled to #invoker.
+void WaitUntilSet(
+ TFuture<void> future,
+ IInvokerPtr invoker = GetCurrentInvoker());
+
+//! Blocks the current fiber until #future is set and returns the resulting value.
+//! The fiber is rescheduled to #invoker.
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitFor(
+ TFuture<T> future,
+ IInvokerPtr invoker = GetCurrentInvoker());
+
+//! Similar to #WaitFor but if #future is already set then the fiber
+//! is not rescheduled. If not, the fiber is rescheduled via
+//! the current invoker.
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForFast(
+ TFuture<T> future);
+
+//! Similar to #WaitFor but extracts the value from #future via |GetUnique|.
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForUnique(
+ const TFuture<T>& future,
+ IInvokerPtr invoker = GetCurrentInvoker());
+
+//! From the authors of #WaitForUnique and #WaitForFast.
+template <class T>
+[[nodiscard]] TErrorOr<T> WaitForUniqueFast(
+ const TFuture<T>& future);
+
+//! A possibly blocking version of #WaitFor.
+template <class T>
+TErrorOr<T> WaitForWithStrategy(
+ TFuture<T> future,
+ EWaitForStrategy strategy);
+
+//! Reschedules the current fiber to the current invoker.
+void Yield();
+
+//! Reschedules the current fiber to #invoker.
+void SwitchTo(IInvokerPtr invoker);
+
+//! Returns |true| if there is enough remaining stack space.
+bool CheckFreeStackSpace(size_t space);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
+
+#define SCHEDULER_API_INL_H_
+#include "scheduler_api-inl.h"
+#undef SCHEDULER_API_INL_H_
diff --git a/yt/yt/core/concurrency/scheduler_thread.cpp b/yt/yt/core/concurrency/scheduler_thread.cpp
new file mode 100644
index 0000000000..f0b6d2f0b8
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_thread.cpp
@@ -0,0 +1,92 @@
+#include "scheduler_thread.h"
+#include "private.h"
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSchedulerThreadBase::~TSchedulerThreadBase()
+{
+ Stop();
+}
+
+TSchedulerThreadBase::TSchedulerThreadBase(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority,
+ int shutdownPriority)
+ : TFiberSchedulerThread(
+ threadGroupName,
+ threadName,
+ threadPriority,
+ shutdownPriority)
+ , CallbackEventCount_(std::move(callbackEventCount))
+{ }
+
+void TSchedulerThreadBase::OnStart()
+{ }
+
+void TSchedulerThreadBase::OnStop()
+{ }
+
+void TSchedulerThreadBase::Stop(bool graceful)
+{
+ GracefulStop_.store(graceful);
+ TThread::Stop();
+}
+
+void TSchedulerThreadBase::Stop()
+{
+ TThread::Stop();
+}
+
+void TSchedulerThreadBase::StartEpilogue()
+{
+ OnStart();
+}
+
+void TSchedulerThreadBase::StopPrologue()
+{
+ CallbackEventCount_->NotifyAll();
+}
+
+void TSchedulerThreadBase::StopEpilogue()
+{
+ OnStop();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClosure TSchedulerThread::OnExecute()
+{
+ EndExecute();
+
+ while (true) {
+ auto cookie = CallbackEventCount_->PrepareWait();
+
+ // Stop flag must be checked after PrepareWait because thread is notified when stop flag is set.
+ // Otherwise, we can miss notification.
+ bool stopping = IsStopping();
+ if (stopping && !GracefulStop_.load()) {
+ CallbackEventCount_->CancelWait();
+ return TClosure();
+ }
+
+ auto callback = BeginExecute();
+
+ if (callback || stopping) {
+ CallbackEventCount_->CancelWait();
+ return callback;
+ }
+
+ CallbackEventCount_->Wait(cookie);
+ EndExecute();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/scheduler_thread.h b/yt/yt/core/concurrency/scheduler_thread.h
new file mode 100644
index 0000000000..7b64db28e9
--- /dev/null
+++ b/yt/yt/core/concurrency/scheduler_thread.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "fiber_scheduler_thread.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSchedulerThreadBase)
+
+class TSchedulerThreadBase
+ : public TFiberSchedulerThread
+{
+public:
+ ~TSchedulerThreadBase();
+
+ void Stop(bool graceful);
+
+ void Stop();
+
+protected:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_;
+ std::atomic<bool> GracefulStop_ = false;
+
+ TSchedulerThreadBase(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority = NThreading::EThreadPriority::Normal,
+ int shutdownPriority = 0);
+
+ virtual void OnStart();
+ virtual void OnStop();
+
+private:
+ void StartEpilogue() override;
+ void StopPrologue() override;
+ void StopEpilogue() override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSchedulerThreadBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSchedulerThread
+ : public TSchedulerThreadBase
+{
+protected:
+ using TSchedulerThreadBase::TSchedulerThreadBase;
+
+ TClosure OnExecute() override;
+
+ virtual TClosure BeginExecute() = 0;
+ virtual void EndExecute() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} //namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp b/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
new file mode 100644
index 0000000000..7cdd25cc0c
--- /dev/null
+++ b/yt/yt/core/concurrency/single_queue_scheduler_thread.cpp
@@ -0,0 +1,152 @@
+#include "single_queue_scheduler_thread.h"
+#include "invoker_queue.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+TSingleQueueSchedulerThread<TQueueImpl>::TSingleQueueSchedulerThread(
+ TInvokerQueuePtr<TQueueImpl> queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority,
+ int shutdownPriority)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName,
+ threadPriority,
+ shutdownPriority)
+ , Queue_(std::move(queue))
+ , Token_(Queue_->MakeConsumerToken())
+{ }
+
+template <class TQueueImpl>
+TClosure TSingleQueueSchedulerThread<TQueueImpl>::BeginExecute()
+{
+ return Queue_->BeginExecute(&CurrentAction_, &Token_);
+}
+
+template <class TQueueImpl>
+void TSingleQueueSchedulerThread<TQueueImpl>::EndExecute()
+{
+ Queue_->EndExecute(&CurrentAction_);
+}
+
+template <class TQueueImpl>
+void TSingleQueueSchedulerThread<TQueueImpl>::OnStart()
+{
+ Queue_->SetThreadId(GetThreadId());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template class TSingleQueueSchedulerThread<TMpmcQueueImpl>;
+template class TSingleQueueSchedulerThread<TMpscQueueImpl>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+TSuspendableSingleQueueSchedulerThread<TQueueImpl>::TSuspendableSingleQueueSchedulerThread(
+ TInvokerQueuePtr<TQueueImpl> queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName)
+ , Queue_(std::move(queue))
+ , Token_(Queue_->MakeConsumerToken())
+{ }
+
+template <class TQueueImpl>
+TFuture<void> TSuspendableSingleQueueSchedulerThread<TQueueImpl>::Suspend(bool immediately)
+{
+ auto guard = Guard(Lock_);
+
+ if (!Suspending_.exchange(true)) {
+ SuspendImmediately_ = immediately;
+ SuspendedPromise_ = NewPromise<void>();
+ ResumeEvent_ = New<NThreading::TEvent>();
+ } else if (immediately) {
+ SuspendImmediately_ = true;
+ }
+
+ return SuspendedPromise_.ToFuture();
+}
+
+template <class TQueueImpl>
+void TSuspendableSingleQueueSchedulerThread<TQueueImpl>::Resume()
+{
+ YT_VERIFY(Suspending_);
+ YT_VERIFY(SuspendedPromise_.IsSet());
+
+ auto guard = Guard(Lock_);
+
+ Suspending_ = false;
+ SuspendImmediately_ = false;
+
+ ResumeEvent_->NotifyAll();
+}
+
+template <class TQueueImpl>
+void TSuspendableSingleQueueSchedulerThread<TQueueImpl>::Shutdown(bool graceful)
+{
+ auto guard = Guard(Lock_);
+
+ if (Suspending_) {
+ Suspending_ = false;
+ SuspendImmediately_ = false;
+
+ ResumeEvent_->NotifyAll();
+ }
+
+ Stop(graceful);
+}
+
+template <class TQueueImpl>
+TClosure TSuspendableSingleQueueSchedulerThread<TQueueImpl>::BeginExecute()
+{
+ if (Suspending_ && (SuspendImmediately_ || Queue_->IsEmpty())) {
+ TIntrusivePtr<NThreading::TEvent> resumeEvent;
+ {
+ auto guard = Guard(Lock_);
+
+ SuspendedPromise_.Set();
+ resumeEvent = ResumeEvent_;
+ }
+
+ resumeEvent->Wait();
+ }
+
+ return Queue_->BeginExecute(&CurrentAction_, &Token_);
+}
+
+template <class TQueueImpl>
+void TSuspendableSingleQueueSchedulerThread<TQueueImpl>::EndExecute()
+{
+ Queue_->EndExecute(&CurrentAction_);
+}
+
+template <class TQueueImpl>
+void TSuspendableSingleQueueSchedulerThread<TQueueImpl>::OnStart()
+{
+ Queue_->SetThreadId(GetThreadId());
+}
+
+template <class TQueueImpl>
+void TSuspendableSingleQueueSchedulerThread<TQueueImpl>::OnStop()
+{
+ Queue_->DrainConsumer();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template class TSuspendableSingleQueueSchedulerThread<TMpscQueueImpl>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/single_queue_scheduler_thread.h b/yt/yt/core/concurrency/single_queue_scheduler_thread.h
new file mode 100644
index 0000000000..8ffe2232cc
--- /dev/null
+++ b/yt/yt/core/concurrency/single_queue_scheduler_thread.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "private.h"
+#include "scheduler_thread.h"
+#include "invoker_queue.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+class TSingleQueueSchedulerThread
+ : public TSchedulerThread
+{
+public:
+ TSingleQueueSchedulerThread(
+ TInvokerQueuePtr<TQueueImpl> queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority = NThreading::EThreadPriority::Normal,
+ int shutdownPriority = 0);
+
+protected:
+ const TInvokerQueuePtr<TQueueImpl> Queue_;
+ typename TQueueImpl::TConsumerToken Token_;
+
+ TEnqueuedAction CurrentAction_;
+
+ TClosure BeginExecute() override;
+ void EndExecute() override;
+
+ void OnStart() override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TQueueImpl>
+class TSuspendableSingleQueueSchedulerThread
+ : public TSchedulerThread
+{
+public:
+ TSuspendableSingleQueueSchedulerThread(
+ TInvokerQueuePtr<TQueueImpl> queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName);
+
+ TFuture<void> Suspend(bool immediately);
+
+ void Resume();
+
+ void Shutdown(bool graceful);
+
+protected:
+ const TInvokerQueuePtr<TQueueImpl> Queue_;
+ typename TQueueImpl::TConsumerToken Token_;
+
+ TEnqueuedAction CurrentAction_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ std::atomic<bool> Suspending_ = false;
+
+ std::atomic<bool> SuspendImmediately_ = false;
+ TPromise<void> SuspendedPromise_ = NewPromise<void>();
+ TIntrusivePtr<NThreading::TEvent> ResumeEvent_;
+
+ TClosure BeginExecute() override;
+ void EndExecute() override;
+
+ void OnStart() override;
+ void OnStop() override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/suspendable_action_queue.cpp b/yt/yt/core/concurrency/suspendable_action_queue.cpp
new file mode 100644
index 0000000000..85b1c9aacf
--- /dev/null
+++ b/yt/yt/core/concurrency/suspendable_action_queue.cpp
@@ -0,0 +1,106 @@
+#include "suspendable_action_queue.h"
+
+#include "profiling_helpers.h"
+#include "single_queue_scheduler_thread.h"
+#include "system_invokers.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendableActionQueue
+ : public ISuspendableActionQueue
+{
+public:
+ explicit TSuspendableActionQueue(const TString& threadName)
+ : Queue_(New<TMpscInvokerQueue>(
+ CallbackEventCount_,
+ GetThreadTags(threadName)))
+ , Invoker_(Queue_)
+ , Thread_(New<TMpscSuspendableSingleQueueSchedulerThread>(
+ Queue_,
+ CallbackEventCount_,
+ threadName,
+ threadName))
+ , ShutdownCookie_(RegisterShutdownCallback(
+ Format("SuspendableActionQueue(%v)", threadName),
+ BIND_NO_PROPAGATE(&TSuspendableActionQueue::Shutdown, MakeWeak(this), /*graceful*/ false),
+ /*priority*/ 100))
+ { }
+
+ ~TSuspendableActionQueue()
+ {
+ Shutdown(/*graceful*/ false);
+ }
+
+ void Shutdown(bool graceful) final
+ {
+ if (Stopped_.exchange(true)) {
+ return;
+ }
+
+ Queue_->Shutdown();
+
+ ShutdownInvoker_->Invoke(BIND([graceful, thread = Thread_, queue = Queue_] {
+ thread->Shutdown(graceful);
+ queue->DrainConsumer();
+ }));
+ }
+
+ const IInvokerPtr& GetInvoker() override
+ {
+ EnsureStarted();
+ return Invoker_;
+ }
+
+ TFuture<void> Suspend(bool immediately) override
+ {
+ auto future = Thread_->Suspend(immediately);
+
+ // Invoke empty callback to wake up thread.
+ Queue_->Invoke(BIND([] { }));
+
+ return future;
+ }
+
+ void Resume() override
+ {
+ Thread_->Resume();
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TMpscInvokerQueuePtr Queue_;
+ const IInvokerPtr Invoker_;
+ const TMpscSuspendableSingleQueueSchedulerThreadPtr Thread_;
+
+ const TShutdownCookie ShutdownCookie_;
+ const IInvokerPtr ShutdownInvoker_ = GetShutdownInvoker();
+
+ std::atomic<bool> Started_ = false;
+ std::atomic<bool> Stopped_ = false;
+
+ void EnsureStarted()
+ {
+ if (Started_.load(std::memory_order::relaxed)) {
+ return;
+ }
+ if (Started_.exchange(true)) {
+ return;
+ }
+ Thread_->Start();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISuspendableActionQueuePtr CreateSuspendableActionQueue(const TString& threadName)
+{
+ return New<TSuspendableActionQueue>(threadName);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/suspendable_action_queue.h b/yt/yt/core/concurrency/suspendable_action_queue.h
new file mode 100644
index 0000000000..0909aa0b49
--- /dev/null
+++ b/yt/yt/core/concurrency/suspendable_action_queue.h
@@ -0,0 +1,36 @@
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISuspendableActionQueue
+ : public TRefCounted
+{
+ virtual const IInvokerPtr& GetInvoker() = 0;
+
+ //! Returns a future that becomes set when action queue is suspended
+ //! and thread is blocked.
+ //! If #immediately is true, queue is suspended just after completion
+ //! of current fiber.
+ //! If #immediately is false, queue is suspended when underlying queue
+ //! becomes empty.
+ virtual TFuture<void> Suspend(bool immediately) = 0;
+
+ //! Resumes queue. Queue should be suspended prior to this call.
+ virtual void Resume() = 0;
+
+ virtual void Shutdown(bool graceful) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ISuspendableActionQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+ISuspendableActionQueuePtr CreateSuspendableActionQueue(const TString& threadName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/system_invokers.cpp b/yt/yt/core/concurrency/system_invokers.cpp
new file mode 100644
index 0000000000..c3c389cdd4
--- /dev/null
+++ b/yt/yt/core/concurrency/system_invokers.cpp
@@ -0,0 +1,76 @@
+#include "system_invokers.h"
+#include "action_queue.h"
+#include "profiling_helpers.h"
+#include "single_queue_scheduler_thread.h"
+
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <library/cpp/yt/memory/leaky_singleton.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTag>
+class TSystemInvokerThread
+{
+public:
+ TSystemInvokerThread(
+ const TString& threadName,
+ int shutdownPriority)
+ : Queue_(New<TMpscInvokerQueue>(
+ CallbackEventCount_,
+ GetThreadTags(threadName)))
+ , Invoker_(Queue_)
+ , Thread_(New<TMpscSingleQueueSchedulerThread>(
+ Queue_,
+ CallbackEventCount_,
+ threadName,
+ threadName,
+ NThreading::EThreadPriority::Normal,
+ /*shutdownPriority*/ shutdownPriority - 1))
+ , ShutdownCookie_(RegisterShutdownCallback(
+ Format("SystemInvokerThread:%v", threadName),
+ BIND_NO_PROPAGATE(&TSystemInvokerThread::Shutdown, this),
+ shutdownPriority))
+ {
+ Thread_->Start();
+ }
+
+ const IInvokerPtr& GetInvoker()
+ {
+ return Invoker_;
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TMpscInvokerQueuePtr Queue_;
+ const IInvokerPtr Invoker_;
+ const TMpscSingleQueueSchedulerThreadPtr Thread_;
+ const TShutdownCookie ShutdownCookie_;
+
+ void Shutdown()
+ {
+ Thread_->Stop(/*graceful*/ true);
+ }
+};
+
+IInvokerPtr GetFinalizerInvoker()
+{
+ struct TTag
+ { };
+ static const auto invoker = LeakySingleton<TSystemInvokerThread<TTag>>("Finalizer", -300)->GetInvoker();
+ return invoker;
+}
+
+IInvokerPtr GetShutdownInvoker()
+{
+ struct TTag
+ { };
+ static const auto invoker = LeakySingleton<TSystemInvokerThread<TTag>>("Shutdown", -200)->GetInvoker();
+ return invoker;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/system_invokers.h b/yt/yt/core/concurrency/system_invokers.h
new file mode 100644
index 0000000000..8aab9d5729
--- /dev/null
+++ b/yt/yt/core/concurrency/system_invokers.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IInvokerPtr GetFinalizerInvoker();
+IInvokerPtr GetShutdownInvoker();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/thread_affinity-inl.h b/yt/yt/core/concurrency/thread_affinity-inl.h
new file mode 100644
index 0000000000..7d68fe6a71
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_affinity-inl.h
@@ -0,0 +1,32 @@
+#ifndef THREAD_AFFINITY_INL_H_
+#error "Direct inclusion of this file is not allowed, include thread_affinity.h"
+// For the sake of sane code completion.
+#include "thread_affinity.h"
+#endif
+#undef THREAD_AFFINITY_INL_H_
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Forward declaration.
+bool VerifyInvokerAffinity(const IInvokerPtr& invoker);
+
+bool VerifySerializedInvokerAffinity(const IInvokerPtr& invoker);
+
+template <class T>
+bool VerifyInvokersAffinity(const T& invokers)
+{
+ for (const auto& invoker : invokers) {
+ if (VerifyInvokerAffinity(invoker)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool VerifyInvokerPoolAffinity(const IInvokerPoolPtr& invokerPool);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/thread_affinity.cpp b/yt/yt/core/concurrency/thread_affinity.cpp
new file mode 100644
index 0000000000..2dfa0f19f9
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_affinity.cpp
@@ -0,0 +1,61 @@
+#include "thread_affinity.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+#include <yt/yt/core/actions/invoker_pool.h>
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TThreadAffinitySlot::Check(TThreadId threadId)
+{
+ auto expectedId = InvalidThreadId;
+ if (!BoundId_.compare_exchange_strong(expectedId, threadId)) {
+ YT_VERIFY(expectedId == threadId);
+ }
+}
+
+void TThreadAffinitySlot::Check()
+{
+ Check(GetCurrentThreadId());
+}
+
+TThreadId TThreadAffinitySlot::GetBoundThreadId() const
+{
+ return BoundId_;
+}
+
+bool VerifyInvokerAffinity(const IInvokerPtr& invoker)
+{
+ auto currentInvoker = GetCurrentInvoker();
+ return
+ currentInvoker->CheckAffinity(invoker) ||
+ invoker->CheckAffinity(currentInvoker);
+}
+
+bool VerifySerializedInvokerAffinity(const IInvokerPtr& invoker)
+{
+ return
+ VerifyInvokerAffinity(invoker) &&
+ invoker->IsSerialized();
+}
+
+bool VerifyInvokerPoolAffinity(const IInvokerPoolPtr& invokerPool)
+{
+ for (int index = 0; index < invokerPool->GetSize(); ++index) {
+ if (VerifyInvokerAffinity(invokerPool->GetInvoker(index))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/thread_affinity.h b/yt/yt/core/concurrency/thread_affinity.h
new file mode 100644
index 0000000000..c6c465f35e
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_affinity.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/threading/public.h>
+
+#include <library/cpp/yt/misc/preprocessor.h>
+
+#include <atomic>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * Allows to annotate certain functions with thread affinity.
+ * The checks are performed at run-time to ensure that each function
+ * invocation that is annotated with a particular affinity slot
+ * takes place in one thread.
+ *
+ * The usage is as follows.
+ * - For each thread that may invoke your functions declare a slot with
+ * \code
+ * DECLARE_THREAD_AFFINITY_SLOT(Thread);
+ * \endcode
+ * - Write
+ * \code
+ * VERIFY_THREAD_AFFINITY(Thread);
+ * \endcode
+ * at the beginning of each function in the group.
+ *
+ * Please refer to the unit test for an actual usage example
+ * (unittests/thread_affinity_ut.cpp).
+ */
+class TThreadAffinitySlot
+{
+public:
+ //! Checks if the slot matches the given thread id.
+ void Check(NThreading::TThreadId threadId);
+
+ //! Checks if the slot matches the current thread id.
+ void Check();
+
+ //! Returns thread id used for affinity check
+ //! or #InvalidThreadId if bound thread is still undefined.
+ NThreading::TThreadId GetBoundThreadId() const;
+
+private:
+ std::atomic<NThreading::TThreadId> BoundId_ = NThreading::InvalidThreadId;
+};
+
+#ifdef YT_ENABLE_THREAD_AFFINITY_CHECK
+
+#define DECLARE_THREAD_AFFINITY_SLOT(slot) \
+ mutable ::NYT::NConcurrency::TThreadAffinitySlot PP_CONCAT(slot, _Slot)
+
+#define VERIFY_THREAD_AFFINITY(slot) \
+ PP_CONCAT(slot, _Slot).Check()
+
+#define VERIFY_SPINLOCK_AFFINITY(spinLock) \
+ YT_VERIFY((spinLock).IsLocked());
+
+#define VERIFY_READER_SPINLOCK_AFFINITY(spinLock) \
+ YT_VERIFY((spinLock).IsLockedByReader());
+
+#define VERIFY_WRITER_SPINLOCK_AFFINITY(spinLock) \
+ YT_VERIFY((spinLock).IsLockedByWriter());
+
+#define VERIFY_INVOKER_AFFINITY(invoker) \
+ YT_VERIFY(::NYT::NConcurrency::VerifyInvokerAffinity(invoker))
+
+#define VERIFY_SERIALIZED_INVOKER_AFFINITY(invoker) \
+ YT_VERIFY(::NYT::NConcurrency::VerifySerializedInvokerAffinity(invoker))
+
+#define VERIFY_INVOKERS_AFFINITY(...) \
+ YT_VERIFY(::NYT::NConcurrency::VerifyInvokersAffinity(__VA_ARGS__))
+
+#define VERIFY_INVOKER_POOL_AFFINITY(invokerPool) \
+ YT_VERIFY(::NYT::NConcurrency::VerifyInvokerPoolAffinity(invokerPool))
+
+#define VERIFY_INVOKER_THREAD_AFFINITY(invoker, slot) \
+ PP_CONCAT(slot, _Slot).Check((invoker)->GetThreadId());
+
+#else
+
+// Expand macros to null but take care of the trailing semicolon.
+#define DECLARE_THREAD_AFFINITY_SLOT(slot) struct PP_CONCAT(TNullThreadAffinitySlot_, __LINE__) { }
+#define VERIFY_THREAD_AFFINITY(slot) do { } while (false)
+#define VERIFY_SPINLOCK_AFFINITY(spinLock) do { } while (false)
+#define VERIFY_READER_SPINLOCK_AFFINITY(spinLock) do { } while (false)
+#define VERIFY_WRITER_SPINLOCK_AFFINITY(spinLock) do { } while (false)
+#define VERIFY_INVOKER_AFFINITY(invoker) do { } while (false)
+#define VERIFY_SERIALIZED_INVOKER_AFFINITY(invoker) do { } while (false)
+#define VERIFY_INVOKERS_AFFINITY(...) do { } while (false)
+#define VERIFY_INVOKER_POOL_AFFINITY(invokerPool) do { } while (false)
+#define VERIFY_INVOKER_THREAD_AFFINITY(invoker, slot) do { } while (false)
+
+#endif
+
+//! This is a mere declaration and intentionally does not check anything.
+#define VERIFY_THREAD_AFFINITY_ANY() do { } while (false)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
+#define THREAD_AFFINITY_INL_H_
+#include "thread_affinity-inl.h"
+#undef THREAD_AFFINITY_INL_H_
diff --git a/yt/yt/core/concurrency/thread_pool.cpp b/yt/yt/core/concurrency/thread_pool.cpp
new file mode 100644
index 0000000000..b516b0632c
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool.cpp
@@ -0,0 +1,229 @@
+#include "thread_pool.h"
+#include "notify_manager.h"
+#include "single_queue_scheduler_thread.h"
+#include "private.h"
+#include "profiling_helpers.h"
+#include "thread_pool_detail.h"
+
+#include <yt/yt/core/actions/invoker_detail.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+using namespace NYPath;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This routines could be placed in TThreadPool class but it causes a circular ref.
+class TInvokerQueueAdapter
+ : public TMpmcInvokerQueue
+ , public TNotifyManager
+{
+public:
+ TInvokerQueueAdapter(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TTagSet& counterTagSet,
+ const TDuration pollingPeriod)
+ : TMpmcInvokerQueue(callbackEventCount, counterTagSet)
+ , TNotifyManager(callbackEventCount, counterTagSet, pollingPeriod)
+ { }
+
+ TClosure OnExecute(TEnqueuedAction* action, bool fetchNext, std::function<bool()> isStopping)
+ {
+ while (true) {
+ int activeThreadDelta = !action->Finished ? -1 : 0;
+ TMpmcInvokerQueue::EndExecute(action);
+
+ auto cookie = GetEventCount()->PrepareWait();
+ auto minEnqueuedAt = ResetMinEnqueuedAt();
+
+ TClosure callback;
+ if (fetchNext) {
+ callback = TMpmcInvokerQueue::BeginExecute(action);
+
+ if (callback) {
+ YT_ASSERT(action->EnqueuedAt > 0);
+ minEnqueuedAt = action->EnqueuedAt;
+ activeThreadDelta += 1;
+ }
+ }
+
+ YT_VERIFY(activeThreadDelta <= 1 && activeThreadDelta >= -1);
+ if (activeThreadDelta != 0) {
+ auto activeThreads = ActiveThreads_.fetch_add(activeThreadDelta) + activeThreadDelta;
+ YT_VERIFY(activeThreads >= 0 && activeThreads <= TThreadPoolBase::MaxThreadCount);
+ }
+
+ if (callback || isStopping()) {
+ CancelWait();
+
+ NotifyAfterFetch(GetCpuInstant(), minEnqueuedAt);
+ return callback;
+ }
+
+ Wait(cookie, isStopping);
+ }
+ }
+
+ void Invoke(TClosure callback) override
+ {
+ auto cpuInstant = TMpmcInvokerQueue::EnqueueCallback(
+ std::move(callback),
+ /*profilingTag*/ 0,
+ /*profilerTag*/ nullptr);
+
+ NotifyFromInvoke(cpuInstant, ActiveThreads_.load() == 0);
+ }
+
+ void Invoke(TMutableRange<TClosure> callbacks) override
+ {
+ auto cpuInstant = TMpmcInvokerQueue::EnqueueCallbacks(callbacks);
+ NotifyFromInvoke(cpuInstant, ActiveThreads_.load() == 0);
+ }
+
+private:
+ std::atomic<int> ActiveThreads_ = 0;
+};
+
+class TThreadPoolThread
+ : public TSchedulerThread
+{
+public:
+ TThreadPoolThread(
+ TIntrusivePtr<TInvokerQueueAdapter> queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ NThreading::EThreadPriority threadPriority)
+ : TSchedulerThread(
+ callbackEventCount,
+ threadGroupName,
+ threadName,
+ threadPriority,
+ /*shutdownPriority*/ 0)
+ , Queue_(std::move(queue))
+ { }
+
+protected:
+ const TIntrusivePtr<TInvokerQueueAdapter> Queue_;
+ TEnqueuedAction CurrentAction_;
+
+ TClosure OnExecute() override
+ {
+ bool fetchNext = !TSchedulerThread::IsStopping() || TSchedulerThread::GracefulStop_;
+
+ return Queue_->OnExecute(&CurrentAction_, fetchNext, [&] {
+ return TSchedulerThread::IsStopping();
+ });
+ }
+
+ TClosure BeginExecute() override
+ {
+ Y_UNREACHABLE();
+ }
+
+ void EndExecute() override
+ {
+ Y_UNREACHABLE();
+ }
+};
+
+class TThreadPool
+ : public IThreadPool
+ , public TThreadPoolBase
+{
+public:
+ TThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ NThreading::EThreadPriority threadPriority,
+ const TDuration pollingPeriod)
+ : TThreadPoolBase(threadNamePrefix, threadPriority)
+ , Queue_(New<TInvokerQueueAdapter>(
+ CallbackEventCount_,
+ GetThreadTags(ThreadNamePrefix_),
+ pollingPeriod))
+ , Invoker_(Queue_)
+ {
+ Configure(threadCount);
+ }
+
+ ~TThreadPool()
+ {
+ Shutdown();
+ }
+
+ const IInvokerPtr& GetInvoker() override
+ {
+ EnsureStarted();
+ return Invoker_;
+ }
+
+ void Configure(int threadCount) override
+ {
+ TThreadPoolBase::Configure(threadCount);
+ }
+
+ int GetThreadCount() override
+ {
+ return TThreadPoolBase::GetThreadCount();
+ }
+
+ void Shutdown() override
+ {
+ TThreadPoolBase::Shutdown();
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TIntrusivePtr<TInvokerQueueAdapter> Queue_;
+ const IInvokerPtr Invoker_;
+
+ void DoShutdown() override
+ {
+ Queue_->Shutdown();
+ TThreadPoolBase::DoShutdown();
+ }
+
+ TClosure MakeFinalizerCallback() override
+ {
+ return BIND_NO_PROPAGATE([queue = Queue_, callback = TThreadPoolBase::MakeFinalizerCallback()] {
+ callback();
+ queue->DrainConsumer();
+ });
+ }
+
+ TSchedulerThreadBasePtr SpawnThread(int index) override
+ {
+ return New<TThreadPoolThread>(
+ Queue_,
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ MakeThreadName(index),
+ ThreadPriority_);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IThreadPoolPtr CreateThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ NThreading::EThreadPriority threadPriority,
+ TDuration pollingPeriod)
+{
+ return New<TThreadPool>(
+ threadCount,
+ threadNamePrefix,
+ threadPriority,
+ pollingPeriod);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/thread_pool.h b/yt/yt/core/concurrency/thread_pool.h
new file mode 100644
index 0000000000..2792f9a876
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/threading/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IThreadPool
+ : public virtual TRefCounted
+{
+ virtual void Shutdown() = 0;
+
+ //! Returns current thread count, it can differ from value set by Configure()
+ //! because it clamped between 1 and maximum thread count.
+ virtual int GetThreadCount() = 0;
+ virtual void Configure(int threadCount) = 0;
+
+ virtual const IInvokerPtr& GetInvoker() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IThreadPool)
+
+IThreadPoolPtr CreateThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ NThreading::EThreadPriority threadPriority = NThreading::EThreadPriority::Normal,
+ TDuration pollingPeriod = TDuration::MilliSeconds(10));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/thread_pool_detail.cpp b/yt/yt/core/concurrency/thread_pool_detail.cpp
new file mode 100644
index 0000000000..90b56511b6
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool_detail.cpp
@@ -0,0 +1,139 @@
+#include "thread_pool_detail.h"
+
+#include "system_invokers.h"
+#include "private.h"
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <algorithm>
+
+namespace NYT::NConcurrency {
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TThreadPoolBase::TThreadPoolBase(
+ TString threadNamePrefix,
+ NThreading::EThreadPriority threadPriority)
+ : ThreadNamePrefix_(std::move(threadNamePrefix))
+ , ThreadPriority_(threadPriority)
+ , ShutdownCookie_(RegisterShutdownCallback(
+ Format("ThreadPool(%v)", ThreadNamePrefix_),
+ BIND_NO_PROPAGATE(&TThreadPoolBase::Shutdown, MakeWeak(this)),
+ /*priority*/ 100))
+{ }
+
+void TThreadPoolBase::Configure(int threadCount)
+{
+ DoConfigure(std::clamp(threadCount, 1, MaxThreadCount));
+}
+
+void TThreadPoolBase::Shutdown()
+{
+ if (!ShutdownFlag_.exchange(true)) {
+ StartFlag_ = true;
+ DoShutdown();
+ }
+}
+
+void TThreadPoolBase::EnsureStarted()
+{
+ if (!StartFlag_.exchange(true)) {
+ Resize();
+ DoStart();
+ }
+}
+
+TString TThreadPoolBase::MakeThreadName(int index)
+{
+ return Format("%v:%v", ThreadNamePrefix_, index);
+}
+
+void TThreadPoolBase::DoStart()
+{
+ decltype(Threads_) threads;
+ {
+ auto guard = Guard(SpinLock_);
+ threads = Threads_;
+ }
+
+ for (const auto& thread : threads) {
+ thread->Start();
+ }
+}
+
+void TThreadPoolBase::DoShutdown()
+{
+ GetFinalizerInvoker()->Invoke(MakeFinalizerCallback());
+}
+
+TClosure TThreadPoolBase::MakeFinalizerCallback()
+{
+ decltype(Threads_) threads;
+ {
+ auto guard = Guard(SpinLock_);
+ std::swap(threads, Threads_);
+ }
+
+ return BIND_NO_PROPAGATE([threads = std::move(threads)] () {
+ for (const auto& thread : threads) {
+ thread->Stop();
+ }
+ });
+}
+
+int TThreadPoolBase::GetThreadCount()
+{
+ auto guard = Guard(SpinLock_);
+ return std::ssize(Threads_);
+}
+
+void TThreadPoolBase::DoConfigure(int threadCount)
+{
+ ThreadCount_.store(threadCount);
+ if (StartFlag_.load()) {
+ Resize();
+ }
+}
+
+void TThreadPoolBase::Resize()
+{
+ decltype(Threads_) threadsToStart;
+ decltype(Threads_) threadsToStop;
+
+ int threadCount;
+ int oldThreadCount;
+ {
+ auto guard = Guard(SpinLock_);
+ oldThreadCount = std::ssize(Threads_);
+
+ threadCount = ThreadCount_.load();
+
+ while (std::ssize(Threads_) < threadCount) {
+ auto thread = SpawnThread(std::ssize(Threads_));
+ threadsToStart.push_back(thread);
+ Threads_.push_back(thread);
+ }
+
+ while (std::ssize(Threads_) > threadCount) {
+ threadsToStop.push_back(Threads_.back());
+ Threads_.pop_back();
+ }
+ }
+
+ YT_LOG_DEBUG("Thread pool reconfigured (ThreadNamePrefix: %v, ThreadPoolSize: %v -> %v)",
+ ThreadNamePrefix_,
+ oldThreadCount,
+ threadCount);
+
+ for (const auto& thread : threadsToStop) {
+ thread->Stop();
+ }
+
+ DoStart();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/thread_pool_detail.h b/yt/yt/core/concurrency/thread_pool_detail.h
new file mode 100644
index 0000000000..412208a9bc
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool_detail.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "scheduler_thread.h"
+
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThreadPoolBase
+ : public virtual TRefCounted
+{
+public:
+ static constexpr int MaxThreadCount = 64;
+
+ explicit TThreadPoolBase(
+ TString threadNamePrefix,
+ NThreading::EThreadPriority threadPriority = NThreading::EThreadPriority::Normal);
+
+ void Configure(int threadCount);
+ void Shutdown();
+ void EnsureStarted();
+
+ int GetThreadCount();
+
+protected:
+ const TString ThreadNamePrefix_;
+ const NThreading::EThreadPriority ThreadPriority_;
+
+ const TShutdownCookie ShutdownCookie_;
+
+ std::atomic<int> ThreadCount_ = 0;
+ std::atomic<bool> StartFlag_ = false;
+ std::atomic<bool> ShutdownFlag_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::vector<TSchedulerThreadBasePtr> Threads_;
+
+ void Resize();
+
+ TString MakeThreadName(int index);
+
+ virtual void DoStart();
+ virtual void DoShutdown();
+ virtual TClosure MakeFinalizerCallback();
+ virtual void DoConfigure(int threadCount);
+
+ virtual TSchedulerThreadBasePtr SpawnThread(int index) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/thread_pool_poller.cpp b/yt/yt/core/concurrency/thread_pool_poller.cpp
new file mode 100644
index 0000000000..121057dd4d
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool_poller.cpp
@@ -0,0 +1,437 @@
+#include "thread_pool.h"
+#include "poller.h"
+#include "thread_pool_poller.h"
+#include "private.h"
+#include "profiling_helpers.h"
+#include "scheduler_thread.h"
+
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+#include <yt/yt/core/misc/ref_tracked.h>
+
+#include <yt/yt/core/profiling/tscp.h>
+
+#include <library/cpp/yt/threading/notification_handle.h>
+
+#include <util/system/thread.h>
+
+#include <util/thread/lfqueue.h>
+
+#include <util/network/pollerimpl.h>
+
+#include <array>
+
+namespace NYT::NConcurrency {
+
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto PollerThreadQuantum = TDuration::MilliSeconds(100);
+static constexpr int MaxEventsPerPoll = 1024;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThreadPoolPoller;
+
+namespace {
+
+struct TPollableCookie
+ : public TRefCounted
+{
+ explicit TPollableCookie(TThreadPoolPoller* pollerThread)
+ : PollerThread(pollerThread)
+ { }
+
+ static TPollableCookie* FromPollable(IPollable* pollable)
+ {
+ return static_cast<TPollableCookie*>(pollable->GetCookie());
+ }
+
+ TThreadPoolPoller* const PollerThread = nullptr;
+
+ // Active event count is equal to 2 * (active events) + (1 for unregister flag).
+ std::atomic<int> ActiveEventCount = 1;
+ const TPromise<void> UnregisterPromise = NewPromise<void>();
+};
+
+EContPoll ToImplControl(EPollControl control)
+{
+ int implControl = CONT_POLL_ONE_SHOT;
+ if (Any(control & EPollControl::EdgeTriggered)) {
+ implControl = CONT_POLL_EDGE_TRIGGERED;
+ }
+ if (Any(control & EPollControl::BacklogEmpty)) {
+ implControl |= CONT_POLL_BACKLOG_EMPTY;
+ }
+ if (Any(control & EPollControl::Read)) {
+ implControl |= CONT_POLL_READ;
+ }
+ if (Any(control & EPollControl::Write)) {
+ implControl |= CONT_POLL_WRITE;
+ }
+ if (Any(control & EPollControl::ReadHup)) {
+ implControl |= CONT_POLL_RDHUP;
+ }
+ return EContPoll(implControl);
+}
+
+EPollControl FromImplControl(int implControl)
+{
+ auto control = EPollControl::None;
+ if (implControl & CONT_POLL_READ) {
+ control |= EPollControl::Read;
+ }
+ if (implControl & CONT_POLL_WRITE) {
+ control |= EPollControl::Write;
+ }
+ if (implControl & CONT_POLL_RDHUP) {
+ control |= EPollControl::ReadHup;
+ }
+ return control;
+}
+
+bool TryAcquireEventCount(IPollable* pollable)
+{
+ auto* cookie = TPollableCookie::FromPollable(pollable);
+ YT_VERIFY(cookie);
+ YT_VERIFY(cookie->GetRefCount() > 0);
+
+ auto oldEventCount = cookie->ActiveEventCount.fetch_add(2);
+ if (oldEventCount & 1) {
+ return true;
+ }
+
+ cookie->ActiveEventCount.fetch_sub(2);
+ return false;
+}
+
+EThreadPriority PollablePriorityToThreadPriority(EPollablePriority priority)
+{
+ switch (priority) {
+ case EPollablePriority::RealTime:
+ return EThreadPriority::RealTime;
+
+ default:
+ return EThreadPriority::Normal;
+ }
+}
+
+TString PollablePriorityToPollerThreadNameSuffix(EPollablePriority priority)
+{
+ switch (priority) {
+ case EPollablePriority::RealTime:
+ return "RT";
+
+ default:
+ return "";
+ }
+}
+
+} // namespace
+
+class TThreadPoolPoller
+ : public IThreadPoolPoller
+ , public NThreading::TThread
+{
+public:
+ TThreadPoolPoller(int threadCount, const TString& threadNamePrefix, const TDuration pollingPeriod)
+ : TThread(Format("%v:%v", threadNamePrefix, "Poll"))
+ , Logger(ConcurrencyLogger.WithTag("ThreadNamePrefix: %v", threadNamePrefix))
+ {
+ PollerImpl_.Set(nullptr, WakeupHandle_.GetFD(), CONT_POLL_EDGE_TRIGGERED | CONT_POLL_READ);
+
+ for (auto priority : TEnumTraits<EPollablePriority>::GetDomainValues()) {
+ HandlerThreadPool_[priority] = CreateThreadPool(
+ threadCount,
+ threadNamePrefix + PollablePriorityToPollerThreadNameSuffix(priority),
+ PollablePriorityToThreadPriority(priority),
+ pollingPeriod);
+ HandlerInvoker_[priority] = HandlerThreadPool_[priority]->GetInvoker();
+ }
+ }
+
+ void Reconfigure(int threadCount) override
+ {
+ for (auto priority : TEnumTraits<EPollablePriority>::GetDomainValues()) {
+ HandlerThreadPool_[priority]->Configure(threadCount);
+ }
+ }
+
+ // TODO(lukyan): Remove TryRegister and Unregister. Do it in Arm/Unarm.
+ bool TryRegister(const IPollablePtr& pollable) override
+ {
+ if (IsStopping()) {
+ return false;
+ }
+
+ pollable->SetCookie(New<TPollableCookie>(this));
+ RegisterQueue_.Enqueue(pollable);
+
+ YT_LOG_DEBUG("Pollable registered (%v)",
+ pollable->GetLoggingTag());
+
+ return true;
+ }
+
+ // TODO(lukyan): Method OnShutdown in the interface and returned future are redundant.
+ // Shutdown can be done by subscribing returned future or some promise can be set inside OnShutdown.
+ TFuture<void> Unregister(const IPollablePtr& pollable) override
+ {
+ auto* cookie = TPollableCookie::FromPollable(pollable.Get());
+
+ if (!cookie) {
+ // Pollable was not registered.
+ return VoidFuture;
+ }
+
+ DoUnregister(pollable);
+ return cookie->UnregisterPromise.ToFuture();
+ }
+
+ void Arm(TFileDescriptor fd, const IPollablePtr& pollable, EPollControl control) override
+ {
+ YT_LOG_TRACE("Arming poller (FD: %v, Control: %v, %v)",
+ fd,
+ control,
+ pollable->GetLoggingTag());
+ PollerImpl_.Set(pollable.Get(), fd, ToImplControl(control));
+ }
+
+ void Unarm(TFileDescriptor fd, const IPollablePtr&) override
+ {
+ YT_LOG_TRACE("Unarming poller (FD: %v)",
+ fd);
+ PollerImpl_.Remove(fd);
+ }
+
+ void Retry(const IPollablePtr& pollable, bool /*wakeup*/) override
+ {
+ if (TryAcquireEventCount(pollable.Get())) {
+ HandlerInvoker_[pollable->GetPriority()]->Invoke(BIND(TRunEventGuard(pollable.Get(), EPollControl::Retry)));
+ }
+ }
+
+ IInvokerPtr GetInvoker() const override
+ {
+ return HandlerInvoker_[EPollablePriority::Normal];
+ }
+
+ void Shutdown() override
+ {
+ TThread::Stop();
+ }
+
+private:
+ class TRunEventGuard
+ {
+ public:
+ TRunEventGuard(IPollable* pollable, EPollControl control)
+ : Pollable_(pollable)
+ , Control_(control)
+ { }
+
+ explicit TRunEventGuard(TRunEventGuard&& other)
+ : Pollable_(std::move(other.Pollable_))
+ , Control_(std::move(other.Control_))
+ {
+ other.Pollable_ = nullptr;
+ }
+
+ TRunEventGuard(const TRunEventGuard&) = delete;
+
+ TRunEventGuard& operator=(const TRunEventGuard&) = delete;
+ TRunEventGuard& operator=(TRunEventGuard&&) = delete;
+
+ ~TRunEventGuard()
+ {
+ if (Pollable_) {
+ // This is unlikely but might happen on thread pool termination.
+ GetFinalizerInvoker()->Invoke(BIND(&Destroy, Unretained(Pollable_)));
+ }
+ }
+
+ void operator()()
+ {
+ Pollable_->OnEvent(Control_);
+ Destroy(Pollable_);
+ Pollable_ = nullptr;
+ }
+
+ private:
+ IPollable* Pollable_;
+ EPollControl Control_;
+
+ static void Destroy(IPollable* pollable)
+ {
+ auto* cookie = TPollableCookie::FromPollable(pollable);
+ YT_VERIFY(cookie);
+ auto activeEventCount = cookie->ActiveEventCount.fetch_sub(2) - 2;
+ if (activeEventCount == 0) {
+ pollable->OnShutdown();
+ cookie->UnregisterPromise.Set();
+ auto pollerThread = MakeStrong(cookie->PollerThread);
+ pollerThread->UnregisterQueue_.Enqueue(pollable);
+ pollerThread->WakeupHandle_.Raise();
+ }
+ }
+ };
+
+ const NLogging::TLogger Logger;
+
+ TEnumIndexedVector<EPollablePriority, IThreadPoolPtr> HandlerThreadPool_;
+ TEnumIndexedVector<EPollablePriority, IInvokerPtr> HandlerInvoker_;
+
+ // Only makes sense for "select" backend.
+ struct TMutexLocking
+ {
+ using TMyMutex = TMutex;
+ };
+
+ using TPollerImpl = ::TPollerImpl<TMutexLocking>;
+ TPollerImpl PollerImpl_;
+
+ TNotificationHandle WakeupHandle_;
+ TMpscStack<IPollablePtr> RegisterQueue_;
+ TMpscStack<IPollablePtr> UnregisterQueue_;
+ THashSet<IPollablePtr> Pollables_;
+
+ std::array<TPollerImpl::TEvent, MaxEventsPerPoll> PooledImplEvents_;
+
+ TEnumIndexedVector<EPollablePriority, std::vector<TClosure>> Callbacks_;
+
+ bool DoUnregister(const IPollablePtr& pollable)
+ {
+ YT_LOG_DEBUG("Requesting pollable unregistration (%v)",
+ pollable->GetLoggingTag());
+
+ auto* cookie = TPollableCookie::FromPollable(pollable.Get());
+ YT_VERIFY(cookie);
+ auto activeEventCount = cookie->ActiveEventCount.load();
+
+ while (true) {
+ // Otherwise pollable has been already unregistered.
+ if (!(activeEventCount & 1)) {
+ YT_LOG_DEBUG("Pollable is already unregistered (%v)",
+ pollable->GetLoggingTag());
+ return false;
+ }
+
+ if (cookie->ActiveEventCount.compare_exchange_weak(activeEventCount, activeEventCount & ~1)) {
+ // Poller guarantees that OnShutdown is never executed concurrently with OnEvent().
+ // Otherwise it will be removed in TRunEventGuard.
+ if (activeEventCount == 1) {
+ pollable->OnShutdown();
+ cookie->UnregisterPromise.Set();
+ cookie->PollerThread->UnregisterQueue_.Enqueue(pollable);
+ cookie->PollerThread->WakeupHandle_.Raise();
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void HandleEvents()
+ {
+ int eventCount = PollerImpl_.Wait(PooledImplEvents_.data(), PooledImplEvents_.size(), PollerThreadQuantum.MicroSeconds());
+
+ for (int index = 0; index < eventCount; ++index) {
+ const auto& event = PooledImplEvents_[index];
+ auto control = FromImplControl(PollerImpl_.ExtractFilter(&event));
+ auto* pollable = static_cast<IPollable*>(PollerImpl_.ExtractEvent(&event));
+
+ if (!pollable) {
+ WakeupHandle_.Clear();
+ continue;
+ }
+
+ YT_LOG_TRACE("Got pollable event (Pollable: %v, Control: %v)",
+ pollable->GetLoggingTag(),
+ control);
+
+ YT_VERIFY(pollable->GetRefCount() > 0);
+
+ // Can safely dereference pollable because even unregistered pollables are hold in Pollables_.
+ if (TryAcquireEventCount(pollable)) {
+ auto priority = pollable->GetPriority();
+ Callbacks_[priority].push_back(BIND(TRunEventGuard(pollable, control)));
+ }
+ }
+
+ for (auto priority : TEnumTraits<EPollablePriority>::GetDomainValues()) {
+ HandlerInvoker_[priority]->Invoke(Callbacks_[priority]);
+ Callbacks_[priority].clear();
+ }
+ }
+
+ void ThreadMain() override
+ {
+ // Hold this strongly.
+ auto this_ = MakeStrong(this);
+
+ std::vector<IPollablePtr> unregisterItems;
+
+ try {
+ YT_LOG_DEBUG("Thread started (Name: %v)",
+ GetThreadName());
+
+ while (!IsStopping()) {
+ // Save items from unregister queue before processing register queue.
+ // Otherwise registration and unregistration can be reordered:
+ // item was enqueued in register and unregister queues after processing register queue;
+ // thus we try to remove item that has not been inserted in Pollables.
+ // Newely enqueued items in Unregister and Register queues will be processed at the next iteration.
+ UnregisterQueue_.DequeueAll(false, [&] (const auto& pollable) {
+ unregisterItems.push_back(std::move(pollable));
+ });
+
+ RegisterQueue_.DequeueAll(false, [&] (auto& pollable) {
+ InsertOrCrash(Pollables_, std::move(pollable));
+ });
+
+ HandleEvents();
+
+ for (const auto& pollable : unregisterItems) {
+ EraseOrCrash(Pollables_, pollable);
+ }
+
+ unregisterItems.clear();
+ }
+
+ YT_LOG_DEBUG("Thread stopped (Name: %v)",
+ GetThreadName());
+ } catch (const std::exception& ex) {
+ YT_LOG_FATAL(ex, "Unhandled exception in executor thread (Name: %v)",
+ GetThreadName());
+ }
+
+ // Shutdown here.
+ for (const auto& pollable : Pollables_) {
+ DoUnregister(pollable);
+ }
+ }
+
+ void StopPrologue() override
+ {
+ WakeupHandle_.Raise();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IThreadPoolPollerPtr CreateThreadPoolPoller(
+ int threadCount,
+ const TString& threadNamePrefix,
+ const TDuration pollingPeriod)
+{
+ auto poller = New<TThreadPoolPoller>(threadCount, threadNamePrefix, pollingPeriod);
+ poller->Start();
+ return poller;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/thread_pool_poller.h b/yt/yt/core/concurrency/thread_pool_poller.h
new file mode 100644
index 0000000000..4bfa95f590
--- /dev/null
+++ b/yt/yt/core/concurrency/thread_pool_poller.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "poller.h"
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IThreadPoolPoller
+ : public IPoller
+{
+ //! Reconfigures number of polling threads.
+ virtual void Reconfigure(int threadCount) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IThreadPoolPoller)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IThreadPoolPollerPtr CreateThreadPoolPoller(
+ int threadCount,
+ const TString& threadNamePrefix,
+ const TDuration pollingPeriod = TDuration::MilliSeconds(10));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/throughput_throttler.cpp b/yt/yt/core/concurrency/throughput_throttler.cpp
new file mode 100644
index 0000000000..eaa996fc8f
--- /dev/null
+++ b/yt/yt/core/concurrency/throughput_throttler.cpp
@@ -0,0 +1,1184 @@
+#include "throughput_throttler.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <queue>
+
+namespace NYT::NConcurrency {
+
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TThrottlerRequest)
+
+struct TThrottlerRequest
+ : public TRefCounted
+{
+ explicit TThrottlerRequest(i64 amount)
+ : Amount(amount)
+ { }
+
+ i64 Amount;
+ TPromise<void> Promise;
+ std::atomic_flag Set = ATOMIC_FLAG_INIT;
+ NProfiling::TCpuInstant StartTime = NProfiling::GetCpuInstant();
+ NTracing::TTraceContextPtr TraceContext;
+};
+
+DEFINE_REFCOUNTED_TYPE(TThrottlerRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReconfigurableThroughputThrottler
+ : public IReconfigurableThroughputThrottler
+{
+public:
+ TReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfigPtr config,
+ const TLogger& logger,
+ const NProfiling::TProfiler& profiler)
+ : Logger(logger)
+ , ValueCounter_(profiler.Counter("/value"))
+ , QueueSizeCounter_(profiler.Gauge("/queue_size"))
+ , WaitTimer_(profiler.Timer("/wait_time"))
+ {
+ Reconfigure(config);
+ }
+
+ TFuture<void> GetAvailableFuture() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return DoThrottle(0);
+ }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ // Fast lane.
+ if (amount == 0) {
+ return VoidFuture;
+ }
+
+ return DoThrottle(amount);
+ }
+
+ bool TryAcquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ // Fast lane (only).
+ if (amount == 0) {
+ return true;
+ }
+
+ if (Limit_.load() >= 0) {
+ while (true) {
+ TryUpdateAvailable();
+ auto available = Available_.load();
+ if (available < 0) {
+ return false;
+ }
+ if (Available_.compare_exchange_weak(available, available - amount)) {
+ break;
+ }
+ }
+ }
+
+ ValueCounter_.Increment(amount);
+ return true;
+ }
+
+ i64 TryAcquireAvailable(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ // Fast lane (only).
+ if (amount == 0) {
+ return 0;
+ }
+
+ if (Limit_.load() >= 0) {
+ while (true) {
+ TryUpdateAvailable();
+ auto available = Available_.load();
+ if (available < 0) {
+ return 0;
+ }
+ i64 acquire = std::min(amount, available);
+ if (Available_.compare_exchange_weak(available, available - acquire)) {
+ amount = acquire;
+ break;
+ }
+ }
+ }
+
+ ValueCounter_.Increment(amount);
+ return amount;
+ }
+
+ void Acquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ // Fast lane (only).
+ if (amount == 0) {
+ return;
+ }
+
+ TryUpdateAvailable();
+ if (Limit_.load() >= 0) {
+ Available_ -= amount;
+ }
+
+ ValueCounter_.Increment(amount);
+ }
+
+ bool IsOverdraft() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ // Fast lane (only).
+ TryUpdateAvailable();
+
+ if (Limit_.load() < 0) {
+ return false;
+ }
+
+ return Available_ <= 0;
+ }
+
+ void Reconfigure(TThroughputThrottlerConfigPtr config) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ DoReconfigure(config->Limit, config->Period);
+ }
+
+ void SetLimit(std::optional<double> limit) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ DoReconfigure(limit, Period_);
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ // Fast lane (only).
+ return QueueTotalAmount_;
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto queueTotalCount = QueueTotalAmount_.load();
+ auto limit = Limit_.load();
+ if (queueTotalCount == 0 || limit <= 0) {
+ return TDuration::Zero();
+ }
+
+ return queueTotalCount / limit * TDuration::Seconds(1);
+ }
+
+ TThroughputThrottlerConfigPtr GetConfig() const override
+ {
+ auto result = New<TThroughputThrottlerConfig>();
+ result->Limit = Limit_.load();
+ result->Period = Period_.load();
+ return result;
+ }
+
+ i64 GetAvailable() const override
+ {
+ return Available_.load();
+ }
+
+private:
+ const TLogger Logger;
+
+ NProfiling::TCounter ValueCounter_;
+ NProfiling::TGauge QueueSizeCounter_;
+ NProfiling::TEventTimer WaitTimer_;
+
+ std::atomic<TInstant> LastUpdated_ = TInstant::Zero();
+ std::atomic<i64> Available_ = 0;
+ std::atomic<i64> QueueTotalAmount_ = 0;
+
+ //! Protects the section immediately following it.
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ // -1 indicates no limit
+ std::atomic<double> Limit_;
+ std::atomic<TDuration> Period_;
+ TDelayedExecutorCookie UpdateCookie_;
+
+ std::queue<TThrottlerRequestPtr> Requests_;
+
+ TFuture<void> DoThrottle(i64 amount)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ ValueCounter_.Increment(amount);
+ if (Limit_.load() < 0) {
+ return VoidFuture;
+ }
+
+ while (true) {
+ TryUpdateAvailable();
+
+ if (QueueTotalAmount_ > 0) {
+ break;
+ }
+
+ auto available = Available_.load();
+ if (available <= 0) {
+ break;
+ }
+
+ if (Available_.compare_exchange_strong(available, available - amount)) {
+ return VoidFuture;
+ }
+ }
+
+ // Slow lane.
+ auto guard = Guard(SpinLock_);
+
+ if (Limit_.load() < 0) {
+ return VoidFuture;
+ }
+
+ // Enqueue request to be executed later.
+ YT_LOG_DEBUG("Started waiting for throttler (Amount: %v)", amount);
+ auto promise = NewPromise<void>();
+ auto request = New<TThrottlerRequest>(amount);
+ request->TraceContext = NTracing::CreateTraceContextFromCurrent("Throttler");
+ promise.OnCanceled(BIND([weakRequest = MakeWeak(request), amount, this, this_ = MakeStrong(this)] (const TError& error) {
+ auto request = weakRequest.Lock();
+ if (request && !request->Set.test_and_set()) {
+ NTracing::TTraceContextFinishGuard guard(std::move(request->TraceContext));
+ YT_LOG_DEBUG("Canceled waiting for throttler (Amount: %v)",
+ amount);
+ request->Promise.Set(TError(NYT::EErrorCode::Canceled, "Throttled request canceled")
+ << error);
+ QueueTotalAmount_ -= amount;
+ QueueSizeCounter_.Update(QueueTotalAmount_);
+ }
+ }));
+ request->Promise = std::move(promise);
+ Requests_.push(request);
+ QueueTotalAmount_ += amount;
+ QueueSizeCounter_.Update(QueueTotalAmount_);
+
+ ScheduleUpdate();
+
+ return request->Promise;
+ }
+
+ void DoReconfigure(std::optional<double> limit, TDuration period)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ // Slow lane (only).
+ auto guard = Guard(SpinLock_);
+
+ Limit_ = limit.value_or(-1);
+ Period_ = period;
+ TDelayedExecutor::CancelAndClear(UpdateCookie_);
+ auto now = GetInstant();
+ if (limit && *limit > 0) {
+ auto lastUpdated = LastUpdated_.load();
+ auto maxAvailable = static_cast<i64>(Period_.load().SecondsFloat()) * *limit;
+
+ if (lastUpdated == TInstant::Zero()) {
+ Available_ = maxAvailable;
+ LastUpdated_ = now;
+ } else {
+ auto millisecondsPassed = (now - lastUpdated).MilliSeconds();
+ auto deltaAvailable = static_cast<i64>(millisecondsPassed * *limit / 1000);
+ auto newAvailable = Available_.load() + deltaAvailable;
+ if (newAvailable > maxAvailable) {
+ LastUpdated_ = now;
+ newAvailable = maxAvailable;
+ } else {
+ LastUpdated_ = lastUpdated + TDuration::MilliSeconds(deltaAvailable * 1000 / *limit);
+ // Just in case.
+ LastUpdated_ = std::min(LastUpdated_.load(), now);
+ }
+ Available_ = newAvailable;
+ }
+ } else {
+ Available_ = 0;
+ LastUpdated_ = now;
+ }
+ ProcessRequests(std::move(guard));
+ }
+
+ void ScheduleUpdate()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (UpdateCookie_) {
+ return;
+ }
+
+ auto limit = Limit_.load();
+ YT_VERIFY(limit >= 0);
+
+ auto delay = Max<i64>(0, -Available_ * 1000 / limit);
+ UpdateCookie_ = TDelayedExecutor::Submit(
+ BIND_NO_PROPAGATE(&TReconfigurableThroughputThrottler::Update, MakeWeak(this)),
+ TDuration::MilliSeconds(delay));
+ }
+
+ void TryUpdateAvailable()
+ {
+ auto limit = Limit_.load();
+ if (limit < 0) {
+ return;
+ }
+
+ auto period = Period_.load();
+ auto current = GetInstant();
+ auto lastUpdated = LastUpdated_.load();
+
+ auto millisecondsPassed = (current - lastUpdated).MilliSeconds();
+ auto deltaAvailable = static_cast<i64>(millisecondsPassed * limit / 1000);
+
+ if (deltaAvailable == 0) {
+ return;
+ }
+
+ current = lastUpdated + TDuration::MilliSeconds(deltaAvailable * 1000 / limit);
+
+ if (LastUpdated_.compare_exchange_strong(lastUpdated, current)) {
+ auto available = Available_.load();
+ auto throughputPerPeriod = static_cast<i64>(period.SecondsFloat() * limit);
+
+ while (true) {
+ auto newAvailable = available + deltaAvailable;
+ if (newAvailable > throughputPerPeriod) {
+ newAvailable = throughputPerPeriod;
+ }
+ if (Available_.compare_exchange_weak(available, newAvailable)) {
+ break;
+ }
+ }
+ }
+ }
+
+ void Update()
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(SpinLock_);
+ UpdateCookie_.Reset();
+ TryUpdateAvailable();
+
+ ProcessRequests(std::move(guard));
+ }
+
+ void ProcessRequests(TGuard<NThreading::TSpinLock> guard)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ std::vector<TThrottlerRequestPtr> readyList;
+
+ auto limit = Limit_.load();
+ while (!Requests_.empty() && (limit < 0 || Available_ >= 0)) {
+ const auto& request = Requests_.front();
+ if (!request->Set.test_and_set()) {
+ NTracing::TTraceContextGuard traceGuard(std::move(request->TraceContext));
+
+ auto waitTime = NProfiling::CpuDurationToDuration(NProfiling::GetCpuInstant() - request->StartTime);
+ YT_LOG_DEBUG("Finished waiting for throttler (Amount: %v, WaitTime: %v)",
+ request->Amount,
+ waitTime);
+
+ if (limit) {
+ Available_ -= request->Amount;
+ }
+ readyList.push_back(request);
+ QueueTotalAmount_ -= request->Amount;
+ QueueSizeCounter_.Update(QueueTotalAmount_);
+ WaitTimer_.Record(waitTime);
+ }
+ Requests_.pop();
+ }
+
+ if (!Requests_.empty()) {
+ ScheduleUpdate();
+ }
+
+ guard.Release();
+
+ for (const auto& request : readyList) {
+ request->Promise.Set();
+ }
+ }
+
+};
+
+IReconfigurableThroughputThrottlerPtr CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfigPtr config,
+ const TLogger& logger,
+ const NProfiling::TProfiler& profiler)
+{
+ return New<TReconfigurableThroughputThrottler>(
+ config,
+ logger,
+ profiler);
+}
+
+IReconfigurableThroughputThrottlerPtr CreateNamedReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfigPtr config,
+ const TString& name,
+ TLogger logger,
+ NProfiling::TProfiler profiler)
+{
+ return CreateReconfigurableThroughputThrottler(
+ std::move(config),
+ logger.WithTag("Throttler: %v", name),
+ profiler.WithPrefix("/" + CamelCaseToUnderscoreCase(name)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUnlimitedThroughputThrottler
+ : public IThroughputThrottler
+{
+public:
+ explicit TUnlimitedThroughputThrottler(
+ const NProfiling::TProfiler& profiler = {})
+ : ValueCounter_(profiler.Counter("/value"))
+ { }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ ValueCounter_.Increment(amount);
+ return VoidFuture;
+ }
+
+ bool TryAcquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ ValueCounter_.Increment(amount);
+ return true;
+ }
+
+ i64 TryAcquireAvailable(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ ValueCounter_.Increment(amount);
+ return amount;
+ }
+
+ void Acquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ ValueCounter_.Increment(amount);
+ }
+
+ bool IsOverdraft() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return false;
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return 0;
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ return TDuration::Zero();
+ }
+
+ i64 GetAvailable() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ NProfiling::TCounter ValueCounter_;
+};
+
+IThroughputThrottlerPtr GetUnlimitedThrottler()
+{
+ return LeakyRefCountedSingleton<TUnlimitedThroughputThrottler>();
+}
+
+IThroughputThrottlerPtr CreateNamedUnlimitedThroughputThrottler(
+ const TString& name,
+ NProfiling::TProfiler profiler)
+{
+ profiler = profiler.WithPrefix("/" + CamelCaseToUnderscoreCase(name));
+ return New<TUnlimitedThroughputThrottler>(profiler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCombinedThroughputThrottler
+ : public IThroughputThrottler
+{
+public:
+ explicit TCombinedThroughputThrottler(const std::vector<IThroughputThrottlerPtr>& throttlers)
+ : Throttlers_(throttlers)
+ { }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ SelfQueueSize_ += amount;
+
+ std::vector<TFuture<void>> asyncResults;
+ for (const auto& throttler : Throttlers_) {
+ asyncResults.push_back(throttler->Throttle(amount));
+ }
+
+ return AllSucceeded(asyncResults).Apply(BIND([weakThis = MakeWeak(this), amount] (const TError& /* error */ ) {
+ if (auto this_ = weakThis.Lock()) {
+ this_->SelfQueueSize_ -= amount;
+ }
+ }));
+ }
+
+ bool TryAcquire(i64 /*amount*/) override
+ {
+ YT_ABORT();
+ }
+
+ i64 TryAcquireAvailable(i64 /*amount*/) override
+ {
+ YT_ABORT();
+ }
+
+ void Acquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ for (const auto& throttler : Throttlers_) {
+ throttler->Acquire(amount);
+ }
+ }
+
+ bool IsOverdraft() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ for (const auto& throttler : Throttlers_) {
+ if (throttler->IsOverdraft()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto totalQueueSize = SelfQueueSize_.load();
+ for (const auto& throttler : Throttlers_) {
+ totalQueueSize += std::max<i64>(throttler->GetQueueTotalAmount() - SelfQueueSize_.load(), 0);
+ }
+
+ return totalQueueSize;
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ TDuration result = TDuration::Zero();
+ for (const auto& throttler : Throttlers_) {
+ result = std::max(result, throttler->GetEstimatedOverdraftDuration());
+ }
+ return result;
+ }
+
+ i64 GetAvailable() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ const std::vector<IThroughputThrottlerPtr> Throttlers_;
+
+ std::atomic<i64> SelfQueueSize_ = 0;
+};
+
+IThroughputThrottlerPtr CreateCombinedThrottler(
+ const std::vector<IThroughputThrottlerPtr>& throttlers)
+{
+ return New<TCombinedThroughputThrottler>(throttlers);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStealingThrottler
+ : public IThroughputThrottler
+{
+public:
+ TStealingThrottler(
+ IThroughputThrottlerPtr stealer,
+ IThroughputThrottlerPtr underlying)
+ : Stealer_(std::move(stealer))
+ , Underlying_(std::move(underlying))
+ { }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ auto future = Stealer_->Throttle(amount);
+ future.Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TError& error) {
+ if (error.IsOK()) {
+ Underlying_->Acquire(amount);
+ }
+ }));
+ return future;
+ }
+
+ bool TryAcquire(i64 amount) override
+ {
+ if (Stealer_->TryAcquire(amount)) {
+ Underlying_->Acquire(amount);
+ return true;
+ }
+
+ return false;
+ }
+
+ i64 TryAcquireAvailable(i64 amount) override
+ {
+ auto result = Stealer_->TryAcquireAvailable(amount);
+ Underlying_->Acquire(result);
+
+ return result;
+ }
+
+ void Acquire(i64 amount) override
+ {
+ Stealer_->Acquire(amount);
+ Underlying_->Acquire(amount);
+ }
+
+ bool IsOverdraft() override
+ {
+ return Stealer_->IsOverdraft();
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ return Stealer_->GetQueueTotalAmount();
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ return Stealer_->GetEstimatedOverdraftDuration();
+ }
+
+ i64 GetAvailable() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ const IThroughputThrottlerPtr Stealer_;
+ const IThroughputThrottlerPtr Underlying_;
+};
+
+IThroughputThrottlerPtr CreateStealingThrottler(
+ IThroughputThrottlerPtr stealer,
+ IThroughputThrottlerPtr underlying)
+{
+ return New<TStealingThrottler>(
+ std::move(stealer),
+ std::move(underlying));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrefetchingThrottler
+ : public IThroughputThrottler
+{
+public:
+ TPrefetchingThrottler(
+ const TPrefetchingThrottlerConfigPtr& config,
+ const IThroughputThrottlerPtr& underlying,
+ TLogger logger)
+ : Config_(config)
+ , Underlying_(underlying)
+ , Logger(std::move(logger))
+ { }
+
+ TFuture<void> Throttle(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ TPromise<void> promise;
+ i64 incomingRequestId;
+
+ {
+ auto guard = Guard(Lock_);
+
+ if (DoTryAcquire(amount)) {
+ return VoidFuture;
+ }
+
+ promise = NewPromise<void>();
+
+ Balance_ -= amount;
+ incomingRequestId = ++IncomingRequestId_;
+ IncomingRequests_.emplace_back(TIncomingRequest{amount, promise, incomingRequestId});
+ }
+
+ YT_LOG_DEBUG("Enqueued a request to the prefetching throttler (Id: %v, Amount: %v)",
+ incomingRequestId,
+ amount);
+
+ RequestUnderlyingIfNeeded();
+
+ return promise.ToFuture();
+ }
+
+ bool TryAcquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ {
+ auto guard = Guard(Lock_);
+
+ if (DoTryAcquire(amount)) {
+ return true;
+ }
+ }
+
+ StockUp(amount);
+
+ return false;
+ }
+
+ i64 TryAcquireAvailable(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ {
+ auto guard = Guard(Lock_);
+
+ ++IncomingRequestCountPastWindow_;
+
+ if (IncomingRequests_.empty() && Available_ > 0) {
+ auto acquire = std::min(Available_, amount);
+ Available_ -= acquire;
+
+ return acquire;
+ }
+ }
+
+ StockUp(amount);
+
+ return 0;
+ }
+
+ void Acquire(i64 amount) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+ YT_VERIFY(amount >= 0);
+
+ i64 forecastedAvailable;
+
+ {
+ auto guard = Guard(Lock_);
+ ++IncomingRequestCountPastWindow_;
+ // Note that #Available_ can go negative.
+ Available_ -= amount;
+ forecastedAvailable = Available_ + Balance_;
+ }
+
+ if (forecastedAvailable < 0) {
+ StockUp(-forecastedAvailable);
+ }
+ }
+
+ bool IsOverdraft() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(Lock_);
+
+ return Available_ <= 0;
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return Underlying_->GetQueueTotalAmount();
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return Underlying_->GetEstimatedOverdraftDuration();
+ }
+
+ i64 GetAvailable() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(Lock_);
+
+ return Available_;
+ }
+
+private:
+ struct TIncomingRequest
+ {
+ //! The amount in the incoming request.
+ i64 Amount;
+
+ //! The promise created for the incoming request.
+ //! It will be fulfilled when the Underlying_ will yield enough
+ //! to satisfy the Amount fields of all the requests upto this one in the IncomingRequests_ queue.
+ TPromise<void> Promise;
+
+ //! The id of the incoming request.
+ i64 Id;
+ };
+
+ struct TUnderlyingRequest
+ {
+ //! The amount in the underlying request.
+ i64 Amount;
+
+ //! When the request to the Underlying_ was made.
+ TInstant Timestamp;
+
+ //! How many incoming requests was received since the previous underlying request.
+ //! Used to estimate the incoming RPS for the logging purposes.
+ i64 IncomingRequestCount;
+ };
+
+ const TPrefetchingThrottlerConfigPtr Config_;
+
+ //! The underlying throttler for which the RPS is limited.
+ const IThroughputThrottlerPtr Underlying_;
+
+ const TLogger Logger;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ //! The amount already received from the Underlying_ and not yet consumed.
+ //! That is the amount that can be handed to incoming requests immediately and without requests to the Underlying_.
+ i64 Available_ = 0;
+
+ //! Total inflight underlying amount in the UnderlyingRequests_ minus total incoming amount in the IncomingRequests_.
+ //! Negative value for the sum (Available_ + Balance_) means that a new request to the Underlying_ is required.
+ i64 Balance_ = 0;
+
+ //! The minimum amount to be requested from the Underlying_ next time.
+ i64 PrefetchAmount_ = 1;
+
+ //! The sum of the IncomingRequestCount fields for requests in the UnderlyingRequests_ window.
+ //! Used to estimate the incoming RPS.
+ i64 IncomingRequestCountInWindow_ = 0;
+
+ //! The count of incoming requests since the last outgoing request.
+ //! It will be moved into IncomingRequestCount field when a new request to the Underlying_ will be made.
+ i64 IncomingRequestCountPastWindow_ = 0;
+
+ //! The sum of the IncomingRequestCount fields for requests in the UnderlyingRequests_ window.
+ //! Used to estimate the incoming RPS.
+ i64 UnderlyingAmountInWindow_ = 0;
+
+ //! The time point at which the UnderlyingRequests_ was dropped to avoid unbounded grouth of the prefetch value.
+ TInstant RpsStatisticsStart_ = TInstant::Now();
+
+ //! The FIFO of the incoming requests containing requested amounts and given promises.
+ std::deque<TIncomingRequest> IncomingRequests_;
+
+ //! The window of the requests to the Underlying_ used to estimate underlying RPS along with incoming RPS.
+ std::deque<TUnderlyingRequest> UnderlyingRequests_;
+
+ //! The id of the last incoming request.
+ i64 IncomingRequestId_ = 0;
+
+ //! The id of the last underlying request.
+ i64 UnderlyingRequestId_ = 0;
+
+ //! The total count of responses from the underlying throttler.
+ i64 UnderlyingResponseCount_ = 0;
+
+ //! If there are no pending incoming requests, tries to acquire #amount from the local #Available_.
+ bool DoTryAcquire(i64 amount)
+ {
+ ++IncomingRequestCountPastWindow_;
+
+ if (IncomingRequests_.empty() && amount <= Available_) {
+ Available_ -= amount;
+ return true;
+ }
+ return false;
+ }
+
+ //! Requests #amount from the underlying throttler for future requests.
+ //! Triggered when a #TryAcquire or #TryAcquireAvailable incoming request can not be fully satisfied from #Available_.
+ //! And when the sum of #Available_ and #Balance_ becomes negative after #Acquire.
+ void StockUp(i64 amount)
+ {
+ amount = std::clamp(amount, Config_->MinPrefetchAmount, Config_->MaxPrefetchAmount);
+
+ i64 underlyingRequestId = 0;
+ {
+ auto guard = Guard(Lock_);
+ underlyingRequestId = ++UnderlyingRequestId_;
+ }
+
+ Throttle(amount)
+ .Subscribe(BIND(&TPrefetchingThrottler::OnThrottlingResponse, MakeWeak(this), amount, underlyingRequestId));
+
+ auto guard = Guard(Lock_);
+ // Do not count this recursive request as an incoming one.
+ --IncomingRequestCountPastWindow_;
+ }
+
+ //! Checks that the sum of #Available_ and #Balance_ is enough to satisfy pending incoming requests.
+ //! If it is not, makes a request to the #Underlying_ with an appropriate amount.
+ void RequestUnderlyingIfNeeded()
+ {
+ i64 underlyingAmount = 0;
+ i64 underlyingRequestId = 0;
+ double incomingRps = 0.0;
+ double underlyingRps = 0.0;
+ auto now = TInstant::Now();
+ i64 balance;
+ i64 prefetchAmount;
+
+ {
+ auto guard = Guard(Lock_);
+
+ YT_VERIFY(GetOutstandingRequestCount() > 0 || Balance_ <= 0);
+
+ auto forecastedAvailable = Available_ + Balance_;
+ if (forecastedAvailable >= 0) {
+ return;
+ }
+
+ UpdatePrefetch(now);
+ underlyingAmount = std::max(PrefetchAmount_, -forecastedAvailable);
+ Balance_ += underlyingAmount;
+
+ incomingRps = IncomingRps();
+ underlyingRps = UnderlyingRps();
+
+ RegisterUnderlyingRequest(now, underlyingAmount);
+
+ underlyingRequestId = ++UnderlyingRequestId_;
+
+ YT_VERIFY(Available_ + Balance_ >= 0);
+
+ balance = Balance_;
+ prefetchAmount = PrefetchAmount_;
+ }
+
+ YT_LOG_DEBUG("Request to the underlying throttler (Id: %v, UnderlyingAmount: %v, Balance: %v, Prefetch: %v, IncomingRps: %v, UnderlyingRps: %v)",
+ underlyingRequestId,
+ underlyingAmount,
+ balance,
+ prefetchAmount,
+ incomingRps,
+ underlyingRps);
+
+ Underlying_->Throttle(underlyingAmount)
+ .Subscribe(BIND(&TPrefetchingThrottler::OnThrottlingResponse, MakeWeak(this), underlyingAmount, underlyingRequestId));
+ }
+
+ //! Drops outdated requests to the #Underlying_ from the window used for RPS estimation.
+ void UpdateRpsWindow(TInstant now)
+ {
+ while (!UnderlyingRequests_.empty() && UnderlyingRequests_.front().Timestamp + Config_->Window < now) {
+ IncomingRequestCountInWindow_ -= UnderlyingRequests_.front().IncomingRequestCount;
+ UnderlyingAmountInWindow_ -= UnderlyingRequests_.front().Amount;
+ UnderlyingRequests_.pop_front();
+ }
+ }
+
+ //! Registers the new request to the underlying throttler in the RPS window.
+ void RegisterUnderlyingRequest(TInstant now, i64 underlyingAmount)
+ {
+ IncomingRequestCountInWindow_ += IncomingRequestCountPastWindow_;
+ UnderlyingAmountInWindow_ += underlyingAmount;
+ UnderlyingRequests_.push_back({underlyingAmount, now, IncomingRequestCountPastWindow_});
+ IncomingRequestCountPastWindow_ = 0;
+ }
+
+ //! Recalculates #PrefetchAmount_ to be requested from the #Underlying_ based on the current RPS estimation.
+ void UpdatePrefetch(TInstant now)
+ {
+ UpdateRpsWindow(now);
+
+ auto underlyingRps = UnderlyingRps();
+ if (UnderlyingRequests_.size() > 0 && UnderlyingAmountInWindow_ > 0) {
+ PrefetchAmount_ = UnderlyingAmountInWindow_ * underlyingRps / Config_->TargetRps / UnderlyingRequests_.size();
+ }
+ PrefetchAmount_ = std::clamp(PrefetchAmount_, Config_->MinPrefetchAmount, Config_->MaxPrefetchAmount);
+
+ YT_LOG_DEBUG("Recalculate the amount to prefetch from the underlying throttler (RequestsInWindow: %v, Window: %v, UnderlyingRps: %v, TargetRps: %v, PrefetchAmount: %v)",
+ UnderlyingRequests_.size(),
+ Config_->Window,
+ underlyingRps,
+ Config_->TargetRps,
+ PrefetchAmount_);
+ }
+
+ //! Handles a response from the underlying throttler.
+ void OnThrottlingResponse(i64 available, i64 id, const TError& error)
+ {
+ YT_LOG_DEBUG("Response from the underlying throttler (Id: %v, Amount: %v, Result: %v)",
+ id,
+ available,
+ error.IsOK());
+
+ if (error.IsOK()) {
+ SatisfyIncomingRequests(available);
+ } else {
+ YT_LOG_ERROR(error, "Error requesting the underlying throttler");
+ DropAllIncomingRequests(available, error);
+ }
+ }
+
+ //! Distributes the #available amount from a positive response from the #Underlying_ among
+ //! the incoming requests waiting in the queue.
+ void SatisfyIncomingRequests(i64 available)
+ {
+ std::vector<TIncomingRequest> fulfilled;
+
+ {
+ auto guard = Guard(Lock_);
+
+ // Note that #Available_ can be negative.
+ // Moreover even the sum #Available_ + #Balance_ can be negative temporarily
+ // if a concurrent request decreased the #Balance already but has not requested the #Underlying_ yet.
+ Balance_ -= available;
+ available += Available_;
+ Available_ = 0;
+ ++UnderlyingResponseCount_;
+
+ while (!IncomingRequests_.empty() && available >= IncomingRequests_.front().Amount) {
+ auto& request = IncomingRequests_.front();
+ available -= request.Amount;
+ Balance_ += request.Amount;
+ fulfilled.emplace_back(std::move(request));
+ IncomingRequests_.pop_front();
+ }
+
+ Available_ = available;
+ }
+
+ for (auto& request : fulfilled) {
+ // The recursive call to #Throttle from #StockUp creates
+ // a recursive call to #SatisfyIncomingRequests when the corresponding #promise is set.
+ // So that #promise should be set without holding the #Lock_.
+ auto result = request.Promise.TrySet();
+ YT_LOG_DEBUG("Sent the response for the incoming request (Id: %v, Amount: %v, Result: %v)",
+ request.Id,
+ request.Amount,
+ result);
+ }
+ }
+
+ //! Drops all incoming requests propagating an #error received from the underlying throttler.
+ void DropAllIncomingRequests(i64 available, const TError& error)
+ {
+ std::vector<TIncomingRequest> fulfilled;
+
+ {
+ auto guard = Guard(Lock_);
+
+ Balance_ -= available;
+ ++UnderlyingResponseCount_;
+
+ while (!IncomingRequests_.empty()) {
+ auto& request = IncomingRequests_.front();
+ Balance_ += request.Amount;
+ fulfilled.emplace_back(std::move(request));
+ IncomingRequests_.pop_front();
+ }
+ }
+
+ for (auto& request : fulfilled) {
+ request.Promise.Set(error);
+ YT_LOG_DEBUG("Dropped the incoming request (Id: %v, Amount: %v)",
+ request.Id,
+ request.Amount);
+ }
+ }
+
+ //! Estimate RPS of incoming requests.
+ double IncomingRps() const
+ {
+ return (IncomingRequestCountInWindow_ + IncomingRequestCountPastWindow_) / Config_->Window.SecondsFloat();
+ }
+
+ //! Estimate RPS of requests to the underlying throttler.
+ double UnderlyingRps() const
+ {
+ return UnderlyingRequests_.size() / Config_->Window.SecondsFloat();
+ }
+
+ //! Calculate how many requests to the underlying throttler are still active.
+ i64 GetOutstandingRequestCount() const
+ {
+ return UnderlyingRequestId_ - UnderlyingResponseCount_;
+ }
+};
+
+IThroughputThrottlerPtr CreatePrefetchingThrottler(
+ const TPrefetchingThrottlerConfigPtr& config,
+ const IThroughputThrottlerPtr& underlying,
+ const TLogger& Logger)
+{
+ if (config->Enable) {
+ return New<TPrefetchingThrottler>(
+ config,
+ underlying,
+ Logger);
+ } else {
+ return underlying;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/throughput_throttler.h b/yt/yt/core/concurrency/throughput_throttler.h
new file mode 100644
index 0000000000..2e9608616b
--- /dev/null
+++ b/yt/yt/core/concurrency/throughput_throttler.h
@@ -0,0 +1,151 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables throttling sync and async operations.
+/*!
+ * This interface and its implementations are vastly inspired by the "token bucket" algorithm and
+ * |DataTransferThrottler| class from Hadoop.
+ *
+ * Thread affinity: any
+ */
+struct IThroughputThrottler
+ : public virtual TRefCounted
+{
+ //! Assuming that we are about to utilize #amount units of some resource (e.g. bytes or requests),
+ //! returns a future that is set when enough time has passed
+ //! to ensure proper bandwidth utilization.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual TFuture<void> Throttle(i64 amount) = 0;
+
+ //! Tries to acquire #amount units for utilization.
+ //! Returns |true| if the request could be served without overdraft.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual bool TryAcquire(i64 amount) = 0;
+
+ //! Tries to acquire #amount units for utilization.
+ //! Returns number of bytes that could be served without overdraft.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual i64 TryAcquireAvailable(i64 amount) = 0;
+
+ //! Unconditionally acquires #amount units for utilization.
+ //! This request could easily lead to an overdraft.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual void Acquire(i64 amount) = 0;
+
+ //! Returns |true| if the throttling limit has been exceeded.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual bool IsOverdraft() = 0;
+
+ //! Returns total byte amount of all waiting requests.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual i64 GetQueueTotalAmount() const = 0;
+
+ //! Returns estimated duration to drain current request queue.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual TDuration GetEstimatedOverdraftDuration() const = 0;
+
+ //! Returns number of bytes in bucket, can be negative.
+ /*!
+ * \note Thread affinity: any
+ */
+ virtual i64 GetAvailable() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IThroughputThrottler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enables dynamic changes of throttling configuration.
+/*!
+ * Thread affinity: any
+ */
+struct IReconfigurableThroughputThrottler
+ : public IThroughputThrottler
+{
+ //! Updates the configuration.
+ virtual void Reconfigure(TThroughputThrottlerConfigPtr config) = 0;
+
+ //! Updates the limit.
+ //! See TThroughputThrottlerConfig::Limit.
+ virtual void SetLimit(std::optional<double> limit) = 0;
+
+ //! Returns a future that is set when throttler has become available.
+ virtual TFuture<void> GetAvailableFuture() = 0;
+
+ //! Return current throttler config.
+ virtual TThroughputThrottlerConfigPtr GetConfig() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IReconfigurableThroughputThrottler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Constructs a throttler from #config.
+IReconfigurableThroughputThrottlerPtr CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfigPtr config,
+ const NLogging::TLogger& logger = NLogging::TLogger(),
+ const NProfiling::TProfiler& profiler = {});
+
+//! Constructs a throttler from #config and initializes logger and profiler.
+IReconfigurableThroughputThrottlerPtr CreateNamedReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfigPtr config,
+ const TString& name,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler = {});
+
+//! Returns a throttler that imposes no throughput limit.
+IThroughputThrottlerPtr GetUnlimitedThrottler();
+
+//! Returns a throttler that imposes no throughput limit and profiles throughput.
+IThroughputThrottlerPtr CreateNamedUnlimitedThroughputThrottler(
+ const TString& name,
+ NProfiling::TProfiler profiler = {});
+
+//! This throttler is DEPRECATED. Use TFairThrottler instead.
+IThroughputThrottlerPtr CreateCombinedThrottler(
+ const std::vector<IThroughputThrottlerPtr>& throttlers);
+
+//! This throttler is DEPRECATED. Use TFairThrottler instead.
+IThroughputThrottlerPtr CreateStealingThrottler(
+ IThroughputThrottlerPtr stealer,
+ IThroughputThrottlerPtr underlying);
+
+//! This throttler limits RPS for the underlying throttler.
+//! If this throttler's invocation RPS is higher than the specified limit,
+//! throttling amounts are batched in a "prefetching" manner so that
+//! a single request to the underlying throttler serves multiple incoming throttling requests.
+IThroughputThrottlerPtr CreatePrefetchingThrottler(
+ const TPrefetchingThrottlerConfigPtr& config,
+ const IThroughputThrottlerPtr& underlying,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
new file mode 100644
index 0000000000..bcc04edca8
--- /dev/null
+++ b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.cpp
@@ -0,0 +1,717 @@
+#include "two_level_fair_share_thread_pool.h"
+#include "private.h"
+#include "invoker_queue.h"
+#include "profiling_helpers.h"
+#include "scheduler_thread.h"
+#include "thread_pool_detail.h"
+
+#include <yt/yt/core/actions/current_invoker.h>
+
+#include <yt/yt/core/misc/heap.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <yt/yt/core/profiling/tscp.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/memory/weak_ptr.h>
+
+#include <util/generic/xrange.h>
+
+#include <util/system/yield.h>
+
+namespace NYT::NConcurrency {
+
+using namespace NProfiling;
+
+static const auto& Logger = ConcurrencyLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+struct THeapItem;
+class TTwoLevelFairShareQueue;
+
+DECLARE_REFCOUNTED_STRUCT(TBucket)
+
+struct TBucket
+ : public IInvoker
+{
+ TBucket(size_t poolId, TFairShareThreadPoolTag tag, TWeakPtr<TTwoLevelFairShareQueue> parent)
+ : PoolId(poolId)
+ , Tag(std::move(tag))
+ , Parent(std::move(parent))
+ { }
+
+ void RunCallback(const TClosure& callback)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ callback();
+ }
+
+ void Invoke(TClosure callback) override;
+
+ void Invoke(TMutableRange<TClosure> callbacks) override;
+
+ void Drain()
+ {
+ Queue.clear();
+ }
+
+ NThreading::TThreadId GetThreadId() const override
+ {
+ return NThreading::InvalidThreadId;
+ }
+
+ bool CheckAffinity(const IInvokerPtr& invoker) const override
+ {
+ return invoker.Get() == this;
+ }
+
+ bool IsSerialized() const override
+ {
+ return false;
+ }
+
+ ~TBucket();
+
+ const size_t PoolId;
+ const TFairShareThreadPoolTag Tag;
+ TWeakPtr<TTwoLevelFairShareQueue> Parent;
+ TRingQueue<TEnqueuedAction> Queue;
+ THeapItem* HeapIterator = nullptr;
+ NProfiling::TCpuDuration WaitTime = 0;
+
+ TCpuDuration ExcessTime = 0;
+ int CurrentExecutions = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(TBucket)
+
+struct THeapItem
+{
+ TBucketPtr Bucket;
+
+ THeapItem(const THeapItem&) = delete;
+ THeapItem& operator=(const THeapItem&) = delete;
+
+ explicit THeapItem(TBucketPtr bucket)
+ : Bucket(std::move(bucket))
+ {
+ AdjustBackReference(this);
+ }
+
+ THeapItem(THeapItem&& other) noexcept
+ : Bucket(std::move(other.Bucket))
+ {
+ AdjustBackReference(this);
+ }
+
+ THeapItem& operator=(THeapItem&& other) noexcept
+ {
+ Bucket = std::move(other.Bucket);
+ AdjustBackReference(this);
+
+ return *this;
+ }
+
+ void AdjustBackReference(THeapItem* iterator)
+ {
+ if (Bucket) {
+ Bucket->HeapIterator = iterator;
+ }
+ }
+
+ ~THeapItem()
+ {
+ if (Bucket) {
+ Bucket->HeapIterator = nullptr;
+ }
+ }
+};
+
+bool operator < (const THeapItem& lhs, const THeapItem& rhs)
+{
+ return lhs.Bucket->ExcessTime < rhs.Bucket->ExcessTime;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto LogDurationThreshold = TDuration::Seconds(1);
+
+DECLARE_REFCOUNTED_TYPE(TTwoLevelFairShareQueue)
+
+class TTwoLevelFairShareQueue
+ : public TRefCounted
+{
+public:
+ TTwoLevelFairShareQueue(
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider)
+ : CallbackEventCount_(std::move(callbackEventCount))
+ , ThreadNamePrefix_(threadNamePrefix)
+ , Profiler_(TProfiler("/fair_share_queue")
+ .WithHot())
+ , PoolWeightProvider_(std::move(poolWeightProvider))
+ { }
+
+ ~TTwoLevelFairShareQueue()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount)
+ {
+ ThreadCount_.store(threadCount);
+ }
+
+ IInvokerPtr GetInvoker(const TString& poolName, const TFairShareThreadPoolTag& tag)
+ {
+ while (true) {
+ auto guard = Guard(SpinLock_);
+
+ auto poolIt = NameToPoolId_.find(poolName);
+ if (poolIt == NameToPoolId_.end()) {
+ auto newPoolId = GetLowestEmptyPoolId();
+
+ auto profiler = Profiler_.WithTags(GetBucketTags(ThreadNamePrefix_, poolName));
+ auto newPool = std::make_unique<TExecutionPool>(poolName, profiler);
+ if (newPoolId >= IdToPool_.size()) {
+ IdToPool_.emplace_back();
+ }
+ IdToPool_[newPoolId] = std::move(newPool);
+ poolIt = NameToPoolId_.emplace(poolName, newPoolId).first;
+ }
+
+ auto poolId = poolIt->second;
+ const auto& pool = IdToPool_[poolId];
+ if (PoolWeightProvider_) {
+ pool->Weight = PoolWeightProvider_->GetWeight(poolName);
+ }
+
+ TBucketPtr bucket;
+ auto bucketIt = pool->TagToBucket.find(tag);
+ if (bucketIt == pool->TagToBucket.end()) {
+ bucket = New<TBucket>(poolId, tag, MakeWeak(this));
+ YT_VERIFY(pool->TagToBucket.emplace(tag, bucket.Get()).second);
+ pool->BucketCounter.Update(pool->TagToBucket.size());
+ } else {
+ bucket = DangerousGetPtr<TBucket>(bucketIt->second);
+ if (!bucket) {
+ // Bucket is already being destroyed; backoff and retry.
+ guard.Release();
+ ThreadYield();
+ continue;
+ }
+ }
+
+ return bucket;
+ }
+ }
+
+ void Invoke(TClosure callback, TBucket* bucket)
+ {
+ auto guard = Guard(SpinLock_);
+ const auto& pool = IdToPool_[bucket->PoolId];
+
+ pool->SizeCounter.Record(++pool->Size);
+
+ if (!bucket->HeapIterator) {
+ // Otherwise ExcessTime will be recalculated in AccountCurrentlyExecutingBuckets.
+ if (bucket->CurrentExecutions == 0 && !pool->Heap.empty()) {
+ bucket->ExcessTime = pool->Heap.front().Bucket->ExcessTime;
+ }
+
+ pool->Heap.emplace_back(bucket);
+ AdjustHeapBack(pool->Heap.begin(), pool->Heap.end());
+ YT_VERIFY(bucket->HeapIterator);
+ }
+
+ YT_ASSERT(callback);
+
+ TEnqueuedAction action;
+ action.Finished = false;
+ action.EnqueuedAt = GetCpuInstant();
+ action.Callback = BIND(&TBucket::RunCallback, MakeStrong(bucket), std::move(callback));
+ bucket->Queue.push(std::move(action));
+
+ guard.Release();
+
+ CallbackEventCount_->NotifyOne();
+ }
+
+ void RemoveBucket(TBucket* bucket)
+ {
+ auto guard = Guard(SpinLock_);
+
+ auto& pool = IdToPool_[bucket->PoolId];
+
+ auto it = pool->TagToBucket.find(bucket->Tag);
+ YT_VERIFY(it != pool->TagToBucket.end());
+ YT_VERIFY(it->second == bucket);
+ pool->TagToBucket.erase(it);
+ pool->BucketCounter.Update(pool->TagToBucket.size());
+
+ if (pool->TagToBucket.empty()) {
+ YT_VERIFY(NameToPoolId_.erase(pool->PoolName) == 1);
+ pool.reset();
+ }
+ }
+
+ void Shutdown()
+ {
+ Drain();
+ }
+
+ void Drain()
+ {
+ auto guard = Guard(SpinLock_);
+
+ for (const auto& pool : IdToPool_) {
+ if (pool) {
+ for (const auto& item : pool->Heap) {
+ item.Bucket->Drain();
+ }
+ }
+ }
+ }
+
+ TClosure BeginExecute(TEnqueuedAction* action, int index)
+ {
+ auto& threadState = ThreadStates_[index];
+
+ YT_ASSERT(!threadState.Bucket);
+ YT_ASSERT(action && action->Finished);
+
+ auto currentInstant = GetCpuInstant();
+
+ TBucketPtr bucket;
+ {
+ auto guard = Guard(SpinLock_);
+ bucket = GetStarvingBucket(action);
+
+ if (!bucket) {
+ return TClosure();
+ }
+
+ ++bucket->CurrentExecutions;
+
+ threadState.Bucket = bucket;
+ threadState.AccountedAt = currentInstant;
+
+ action->StartedAt = currentInstant;
+ bucket->WaitTime = action->StartedAt - action->EnqueuedAt;
+ }
+
+ YT_ASSERT(action && !action->Finished);
+
+ {
+ auto guard = Guard(SpinLock_);
+ auto& pool = IdToPool_[bucket->PoolId];
+
+ pool->WaitTimeCounter.Record(CpuDurationToDuration(bucket->WaitTime));
+ }
+
+ return std::move(action->Callback);
+ }
+
+ void EndExecute(TEnqueuedAction* action, int index)
+ {
+ auto& threadState = ThreadStates_[index];
+ if (!threadState.Bucket) {
+ return;
+ }
+
+ YT_ASSERT(action);
+
+ if (action->Finished) {
+ return;
+ }
+
+ auto currentInstant = GetCpuInstant();
+
+ action->FinishedAt = currentInstant;
+
+ auto timeFromStart = CpuDurationToDuration(action->FinishedAt - action->StartedAt);
+ auto timeFromEnqueue = CpuDurationToDuration(action->FinishedAt - action->EnqueuedAt);
+
+ {
+ auto guard = Guard(SpinLock_);
+ const auto& pool = IdToPool_[threadState.Bucket->PoolId];
+ pool->SizeCounter.Record(--pool->Size);
+ pool->ExecTimeCounter.Record(timeFromStart);
+ pool->TotalTimeCounter.Record(timeFromEnqueue);
+ }
+
+ if (timeFromStart > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback execution took too long (Wait: %v, Execution: %v, Total: %v)",
+ CpuDurationToDuration(action->StartedAt - action->EnqueuedAt),
+ timeFromStart,
+ timeFromEnqueue);
+ }
+
+ auto waitTime = CpuDurationToDuration(action->StartedAt - action->EnqueuedAt);
+
+ if (waitTime > LogDurationThreshold) {
+ YT_LOG_DEBUG("Callback wait took too long (Wait: %v, Execution: %v, Total: %v)",
+ waitTime,
+ timeFromStart,
+ timeFromEnqueue);
+ }
+
+ action->Finished = true;
+
+ // Remove outside lock because of lock inside RemoveBucket.
+ TBucketPtr bucket;
+ {
+ auto guard = Guard(SpinLock_);
+ bucket = std::move(threadState.Bucket);
+
+ UpdateExcessTime(bucket.Get(), currentInstant - threadState.AccountedAt);
+ threadState.AccountedAt = currentInstant;
+
+ YT_VERIFY(bucket->CurrentExecutions-- > 0);
+ }
+ }
+
+private:
+ struct TThreadState
+ {
+ TCpuInstant AccountedAt = 0;
+ TBucketPtr Bucket;
+ };
+
+ struct TExecutionPool
+ {
+ TExecutionPool(const TString& poolName, const TProfiler& profiler)
+ : PoolName(poolName)
+ , BucketCounter(profiler.Gauge("/buckets"))
+ , SizeCounter(profiler.Summary("/size"))
+ , WaitTimeCounter(profiler.Timer("/time/wait"))
+ , ExecTimeCounter(profiler.Timer("/time/exec"))
+ , TotalTimeCounter(profiler.Timer("/time/total"))
+ { }
+
+ TBucketPtr GetStarvingBucket(TEnqueuedAction* action)
+ {
+ if (!Heap.empty()) {
+ auto bucket = Heap.front().Bucket;
+ YT_VERIFY(!bucket->Queue.empty());
+ *action = std::move(bucket->Queue.front());
+ bucket->Queue.pop();
+
+ if (bucket->Queue.empty()) {
+ ExtractHeap(Heap.begin(), Heap.end());
+ Heap.pop_back();
+ }
+
+ return bucket;
+ }
+
+ return nullptr;
+ }
+
+ const TString PoolName;
+
+ TGauge BucketCounter;
+ std::atomic<i64> Size = 0;
+ NProfiling::TSummary SizeCounter;
+ TEventTimer WaitTimeCounter;
+ TEventTimer ExecTimeCounter;
+ TEventTimer TotalTimeCounter;
+
+ double Weight = 1.0;
+
+ TCpuDuration ExcessTime = 0;
+ std::vector<THeapItem> Heap;
+ THashMap<TFairShareThreadPoolTag, TBucket*> TagToBucket;
+ };
+
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_;
+ const TString ThreadNamePrefix_;
+ const TProfiler Profiler_;
+ const IPoolWeightProviderPtr PoolWeightProvider_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::vector<std::unique_ptr<TExecutionPool>> IdToPool_;
+ THashMap<TString, int> NameToPoolId_;
+
+ std::atomic<int> ThreadCount_ = 0;
+ std::array<TThreadState, TThreadPoolBase::MaxThreadCount> ThreadStates_;
+
+
+ size_t GetLowestEmptyPoolId()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ size_t id = 0;
+ while (id < IdToPool_.size() && IdToPool_[id]) {
+ ++id;
+ }
+ return id;
+ }
+
+ void AccountCurrentlyExecutingBuckets()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ auto currentInstant = GetCpuInstant();
+ auto threadCount = ThreadCount_.load();
+ for (int index = 0; index < threadCount; ++index) {
+ auto& threadState = ThreadStates_[index];
+ if (!threadState.Bucket) {
+ continue;
+ }
+
+ auto duration = currentInstant - threadState.AccountedAt;
+ threadState.AccountedAt = currentInstant;
+
+ UpdateExcessTime(threadState.Bucket.Get(), duration);
+ }
+ }
+
+ void UpdateExcessTime(TBucket* bucket, TCpuDuration duration)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ const auto& pool = IdToPool_[bucket->PoolId];
+
+ pool->ExcessTime += duration / pool->Weight;
+ bucket->ExcessTime += duration;
+
+ auto positionInHeap = bucket->HeapIterator;
+ if (!positionInHeap) {
+ return;
+ }
+
+ size_t indexInHeap = positionInHeap - pool->Heap.data();
+ YT_VERIFY(indexInHeap < pool->Heap.size());
+ SiftDown(pool->Heap.begin(), pool->Heap.end(), pool->Heap.begin() + indexInHeap, std::less<>());
+ }
+
+ TBucketPtr GetStarvingBucket(TEnqueuedAction* action)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ // For each currently evaluating buckets recalculate excess time.
+ AccountCurrentlyExecutingBuckets();
+
+ // Compute min excess over non-empty queues.
+ auto minExcessTime = std::numeric_limits<NProfiling::TCpuDuration>::max();
+
+ int minPoolIndex = -1;
+ for (int index = 0; index < static_cast<int>(IdToPool_.size()); ++index) {
+ const auto& pool = IdToPool_[index];
+ if (pool && !pool->Heap.empty() && pool->ExcessTime < minExcessTime) {
+ minExcessTime = pool->ExcessTime;
+ minPoolIndex = index;
+ }
+ }
+
+ YT_LOG_TRACE("Buckets: %v",
+ MakeFormattableView(
+ xrange(size_t(0), IdToPool_.size()),
+ [&] (auto* builder, auto index) {
+ const auto& pool = IdToPool_[index];
+ if (!pool) {
+ builder->AppendString("<null>");
+ return;
+ }
+ builder->AppendFormat("[%v %v ", index, pool->ExcessTime);
+ for (const auto& [tagId, rawBucket] : pool->TagToBucket) {
+ if (auto bucket = DangerousGetPtr<TBucket>(rawBucket)) {
+ auto excess = CpuDurationToDuration(bucket->ExcessTime).MilliSeconds();
+ builder->AppendFormat("(%v %v) ", tagId, excess);
+ } else {
+ builder->AppendFormat("(%v ?) ", tagId);
+ }
+ }
+ builder->AppendFormat("]");
+ }));
+
+ if (minPoolIndex >= 0) {
+ // Reduce excesses (with truncation).
+ auto delta = IdToPool_[minPoolIndex]->ExcessTime;
+ for (const auto& pool : IdToPool_) {
+ if (pool) {
+ pool->ExcessTime = std::max<NProfiling::TCpuDuration>(pool->ExcessTime - delta, 0);
+ }
+ }
+ return IdToPool_[minPoolIndex]->GetStarvingBucket(action);
+ }
+
+ return nullptr;
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTwoLevelFairShareQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBucket::Invoke(TClosure callback)
+{
+ if (auto parent = Parent.Lock()) {
+ parent->Invoke(std::move(callback), this);
+ }
+}
+
+void TBucket::Invoke(TMutableRange<TClosure> callbacks)
+{
+ if (auto parent = Parent.Lock()) {
+ for (auto& callback : callbacks) {
+ parent->Invoke(std::move(callback), this);
+ }
+ }
+}
+
+TBucket::~TBucket()
+{
+ if (auto parent = Parent.Lock()) {
+ parent->RemoveBucket(this);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareThread
+ : public TSchedulerThread
+{
+public:
+ TFairShareThread(
+ TTwoLevelFairShareQueuePtr queue,
+ TIntrusivePtr<NThreading::TEventCount> callbackEventCount,
+ const TString& threadGroupName,
+ const TString& threadName,
+ int index)
+ : TSchedulerThread(
+ std::move(callbackEventCount),
+ threadGroupName,
+ threadName)
+ , Queue_(std::move(queue))
+ , Index_(index)
+ { }
+
+protected:
+ const TTwoLevelFairShareQueuePtr Queue_;
+ const int Index_;
+
+ TEnqueuedAction CurrentAction;
+
+ TClosure BeginExecute() override
+ {
+ return Queue_->BeginExecute(&CurrentAction, Index_);
+ }
+
+ void EndExecute() override
+ {
+ Queue_->EndExecute(&CurrentAction, Index_);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFairShareThread)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTwoLevelFairShareThreadPool
+ : public ITwoLevelFairShareThreadPool
+ , public TThreadPoolBase
+{
+public:
+ TTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider)
+ : TThreadPoolBase(threadNamePrefix)
+ , Queue_(New<TTwoLevelFairShareQueue>(
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ std::move(poolWeightProvider)))
+ {
+ Configure(threadCount);
+ EnsureStarted();
+ }
+
+ ~TTwoLevelFairShareThreadPool()
+ {
+ Shutdown();
+ }
+
+ void Configure(int threadCount) override
+ {
+ TThreadPoolBase::Configure(threadCount);
+ }
+
+ int GetThreadCount() override
+ {
+ return TThreadPoolBase::GetThreadCount();
+ }
+
+ IInvokerPtr GetInvoker(
+ const TString& poolName,
+ const TFairShareThreadPoolTag& tag) override
+ {
+ EnsureStarted();
+ return Queue_->GetInvoker(poolName, tag);
+ }
+
+ void Shutdown() override
+ {
+ TThreadPoolBase::Shutdown();
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> CallbackEventCount_ = New<NThreading::TEventCount>();
+ const TTwoLevelFairShareQueuePtr Queue_;
+
+
+ void DoShutdown() override
+ {
+ Queue_->Shutdown();
+ TThreadPoolBase::DoShutdown();
+ }
+
+ TClosure MakeFinalizerCallback() override
+ {
+ return BIND_NO_PROPAGATE([queue = Queue_, callback = TThreadPoolBase::MakeFinalizerCallback()] {
+ callback();
+ queue->Drain();
+ });
+ }
+
+ void DoConfigure(int threadCount) override
+ {
+ Queue_->Configure(threadCount);
+ TThreadPoolBase::DoConfigure(threadCount);
+ }
+
+ TSchedulerThreadBasePtr SpawnThread(int index) override
+ {
+ return New<TFairShareThread>(
+ Queue_,
+ CallbackEventCount_,
+ ThreadNamePrefix_,
+ MakeThreadName(index),
+ index);
+ }
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+ITwoLevelFairShareThreadPoolPtr CreateTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider)
+{
+ return New<TTwoLevelFairShareThreadPool>(
+ threadCount,
+ threadNamePrefix,
+ std::move(poolWeightProvider));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/two_level_fair_share_thread_pool.h b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.h
new file mode 100644
index 0000000000..4cf0ab3f5a
--- /dev/null
+++ b/yt/yt/core/concurrency/two_level_fair_share_thread_pool.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+namespace NYT::NConcurrency {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPoolWeightProvider
+ : public virtual TRefCounted
+{
+ virtual double GetWeight(const TString& poolName) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPoolWeightProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ITwoLevelFairShareThreadPool
+ : public virtual TRefCounted
+{
+ virtual int GetThreadCount() = 0;
+ virtual void Configure(int threadCount) = 0;
+
+ virtual IInvokerPtr GetInvoker(
+ const TString& poolName,
+ const TFairShareThreadPoolTag& tag) = 0;
+
+ virtual void Shutdown() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ITwoLevelFairShareThreadPool)
+
+ITwoLevelFairShareThreadPoolPtr CreateTwoLevelFairShareThreadPool(
+ int threadCount,
+ const TString& threadNamePrefix,
+ IPoolWeightProviderPtr poolWeightProvider = nullptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/async_barrier_ut.cpp b/yt/yt/core/concurrency/unittests/async_barrier_ut.cpp
new file mode 100644
index 0000000000..58d6b4d728
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/async_barrier_ut.cpp
@@ -0,0 +1,101 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/async_barrier.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TAsyncBarrierTest, Empty)
+{
+ TAsyncBarrier barrier;
+ auto future = barrier.GetBarrierFuture();
+ EXPECT_TRUE(future.IsSet());
+}
+
+TEST(TAsyncBarrierTest, Simple)
+{
+ TAsyncBarrier barrier;
+ auto cookie = barrier.Insert();
+ auto future = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future.IsSet());
+ barrier.Remove(cookie);
+ EXPECT_TRUE(future.IsSet());
+}
+
+TEST(TAsyncBarrierTest, ReuseFuture)
+{
+ TAsyncBarrier barrier;
+ auto cookie = barrier.Insert();
+ auto future1 = barrier.GetBarrierFuture();
+ auto future2 = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future1.IsSet());
+ EXPECT_EQ(future1, future2);
+ barrier.Remove(cookie);
+ EXPECT_TRUE(future1.IsSet());
+}
+
+TEST(TAsyncBarrierTest, Overlapped)
+{
+ TAsyncBarrier barrier;
+
+ auto cookie1 = barrier.Insert();
+
+ auto future1 = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future1.IsSet());
+
+ auto cookie2 = barrier.Insert();
+
+ auto future2 = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future1.IsSet());
+
+ barrier.Remove(cookie1);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_FALSE(future2.IsSet());
+
+ barrier.Remove(cookie2);
+ EXPECT_TRUE(future2.IsSet());
+}
+
+TEST(TAsyncBarrierTest, Nested)
+{
+ TAsyncBarrier barrier;
+
+ auto cookie1 = barrier.Insert();
+
+ auto future1 = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future1.IsSet());
+
+ auto cookie2 = barrier.Insert();
+
+ auto future2 = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future1.IsSet());
+
+ barrier.Remove(cookie2);
+ EXPECT_FALSE(future2.IsSet());
+ EXPECT_FALSE(future1.IsSet());
+
+ barrier.Remove(cookie1);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future2.IsSet());
+}
+
+TEST(TAsyncBarrierTest, Clear)
+{
+ TAsyncBarrier barrier;
+
+ Y_UNUSED(barrier.Insert());
+
+ auto future = barrier.GetBarrierFuture();
+ EXPECT_FALSE(future.IsSet());
+
+ barrier.Clear(TError("oops"));
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_FALSE(future.Get().IsOK());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT:::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/async_rw_lock_ut.cpp b/yt/yt/core/concurrency/unittests/async_rw_lock_ut.cpp
new file mode 100644
index 0000000000..a7aba0e59d
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/async_rw_lock_ut.cpp
@@ -0,0 +1,128 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/async_rw_lock.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncReaderWriterLockTest
+ : public ::testing::Test
+{
+protected:
+ TAsyncReaderWriterLock lock;
+};
+
+TEST_F(TAsyncReaderWriterLockTest, ReadMode)
+{
+ EXPECT_TRUE(lock.AcquireReader().IsSet());
+ EXPECT_TRUE(lock.AcquireReader().IsSet());
+
+ lock.ReleaseReader();
+ EXPECT_TRUE(lock.AcquireReader().IsSet());
+
+ lock.ReleaseReader();
+ lock.ReleaseReader();
+}
+
+TEST_F(TAsyncReaderWriterLockTest, WriteMode)
+{
+ EXPECT_TRUE(lock.AcquireWriter().IsSet());
+
+ auto firstFuture = lock.AcquireWriter();
+ EXPECT_FALSE(firstFuture.IsSet());
+
+ auto secondFuture = lock.AcquireWriter();
+ EXPECT_FALSE(secondFuture.IsSet());
+
+ lock.ReleaseWriter();
+ EXPECT_TRUE(firstFuture.IsSet());
+ EXPECT_FALSE(secondFuture.IsSet());
+
+ lock.ReleaseWriter();
+ EXPECT_TRUE(secondFuture.IsSet());
+
+ lock.ReleaseWriter();
+}
+
+TEST_F(TAsyncReaderWriterLockTest, WriteAfterRead)
+{
+ YT_UNUSED_FUTURE(lock.AcquireReader());
+
+ auto future = lock.AcquireWriter();
+ EXPECT_FALSE(future.IsSet());
+
+ lock.ReleaseReader();
+ EXPECT_TRUE(future.IsSet());
+
+ lock.ReleaseWriter();
+}
+
+TEST_F(TAsyncReaderWriterLockTest, ServeWriteFirst)
+{
+ YT_UNUSED_FUTURE(lock.AcquireReader());
+
+ auto writeFuture = lock.AcquireWriter();
+ EXPECT_FALSE(writeFuture.IsSet());
+
+ auto readFuture = lock.AcquireReader();
+ EXPECT_FALSE(readFuture.IsSet());
+
+ lock.ReleaseReader();
+ EXPECT_TRUE(writeFuture.IsSet());
+ EXPECT_FALSE(readFuture.IsSet());
+
+ lock.ReleaseWriter();
+ EXPECT_TRUE(readFuture.IsSet());
+
+ lock.ReleaseReader();
+}
+
+TEST_F(TAsyncReaderWriterLockTest, CreateReaderGuard)
+{
+ auto firstGuard = TAsyncLockReaderGuard::Acquire(&lock);
+ EXPECT_TRUE(firstGuard.IsSet());
+
+ {
+ auto secondGuard = TAsyncLockReaderGuard::Acquire(&lock);
+ EXPECT_TRUE(secondGuard.IsSet());
+ }
+
+ auto thirdGuard = TAsyncLockReaderGuard::Acquire(&lock);
+ EXPECT_TRUE(thirdGuard.IsSet());
+}
+
+TEST_F(TAsyncReaderWriterLockTest, CreateWriterGuard)
+{
+ auto guardFuture = TAsyncLockWriterGuard::Acquire(&lock);
+ EXPECT_TRUE(guardFuture.IsSet());
+
+ auto guard = guardFuture.Get().Value();
+
+ auto future = lock.AcquireWriter();
+ EXPECT_FALSE(future.IsSet());
+
+ guard->Release();
+ EXPECT_TRUE(future.IsSet());
+
+ lock.ReleaseWriter();
+}
+
+TEST_F(TAsyncReaderWriterLockTest, ReleaseInDestructor)
+{
+ TFuture<void> future;
+ {
+ auto writeGuard = TAsyncLockWriterGuard::Acquire(&lock).Get().Value();
+ future = lock.AcquireReader();
+ EXPECT_FALSE(future.IsSet());
+ }
+ EXPECT_TRUE(future.IsSet());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/concurrency/unittests/async_stream_pipe_ut.cpp b/yt/yt/core/concurrency/unittests/async_stream_pipe_ut.cpp
new file mode 100644
index 0000000000..55b329391e
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/async_stream_pipe_ut.cpp
@@ -0,0 +1,62 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/async_stream_pipe.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetString(const TSharedRef& sharedRef)
+{
+ return TString(sharedRef.Begin(), sharedRef.Size());
+}
+
+TEST(TAsyncStreamPipeTest, Simple)
+{
+ auto pipe = New<TAsyncStreamPipe>();
+
+ {
+ const auto readResult = pipe->Read();
+ ASSERT_FALSE(readResult.IsSet());
+
+ auto writeResult = pipe->Write(TSharedRef::FromString("FOO"));
+ ASSERT_TRUE(readResult.IsSet());
+ ASSERT_TRUE(readResult.Get().IsOK());
+ ASSERT_EQ(GetString(readResult.Get().Value()), "FOO");
+ ASSERT_TRUE(writeResult.IsSet());
+ ASSERT_TRUE(writeResult.Get().IsOK());
+ }
+
+ {
+ auto writeResult = pipe->Write(TSharedRef::FromString("BAR_BAZ"));
+ EXPECT_FALSE(writeResult.IsSet());
+
+ const auto readResult = pipe->Read();
+ ASSERT_TRUE(readResult.IsSet());
+ ASSERT_TRUE(readResult.Get().IsOK());
+ ASSERT_EQ(GetString(readResult.Get().Value()), "BAR_BAZ");
+ ASSERT_TRUE(writeResult.IsSet());
+ ASSERT_TRUE(writeResult.Get().IsOK());
+
+ }
+
+ {
+ const auto readResult = pipe->Read();
+ ASSERT_FALSE(readResult.IsSet());
+
+ const auto closed = pipe->Close();
+ ASSERT_TRUE(closed.IsSet());
+
+ ASSERT_TRUE(readResult.IsSet());
+ ASSERT_TRUE(readResult.Get().IsOK());
+ ASSERT_EQ(GetString(readResult.Get().Value()), "");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/concurrency/unittests/async_stream_ut.cpp b/yt/yt/core/concurrency/unittests/async_stream_ut.cpp
new file mode 100644
index 0000000000..48f09107f4
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/async_stream_ut.cpp
@@ -0,0 +1,151 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/concurrency/async_stream_pipe.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetString(const TSharedRef& sharedRef)
+{
+ return TString(sharedRef.Begin(), sharedRef.Size());
+}
+
+TSharedRef ReadAlreadySetValue(const IAsyncZeroCopyInputStreamPtr& input)
+{
+
+ auto result = input->Read();
+ {
+ EXPECT_TRUE(result.IsSet());
+ // We can't use ASSERT_ in non-void functions (check gtest FAQ)
+ // so we use TryGet() here in order to avoid hanging and make test crash.
+ EXPECT_TRUE(result.TryGet()->IsOK());
+ }
+ return result.TryGet()->Value();
+}
+
+TEST(TAsyncOutputStreamTest, Simple)
+{
+ auto pipe = New<TAsyncStreamPipe>();
+ auto asyncWriter = CreateZeroCopyAdapter(static_cast<IAsyncOutputStreamPtr>(pipe));
+
+ auto writeResult = asyncWriter->Write(TSharedRef::FromString("foo"));
+ ASSERT_FALSE(writeResult.IsSet());
+
+ auto readResult1 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult1), "foo");
+ ASSERT_TRUE(writeResult.IsSet());
+ ASSERT_TRUE(writeResult.Get().IsOK());
+
+ auto closeResult = asyncWriter->Close();
+ ASSERT_TRUE(writeResult.IsSet());
+
+ auto readResult2 = ReadAlreadySetValue(pipe);
+ ASSERT_FALSE(readResult2);
+}
+
+TEST(TAsyncOutputStreamTest, MultipleWrites)
+{
+ auto pipe = New<TAsyncStreamPipe>();
+ auto asyncWriter = CreateZeroCopyAdapter(static_cast<IAsyncOutputStreamPtr>(pipe));
+
+ auto writeResult1 = asyncWriter->Write(TSharedRef::FromString("foo"));
+ auto writeResult2 = asyncWriter->Write(TSharedRef::FromString("bar"));
+ auto writeResult3 = asyncWriter->Write(TSharedRef::FromString("baz"));
+ auto closeResult = asyncWriter->Close();
+
+ ASSERT_FALSE(writeResult1.IsSet());
+ ASSERT_FALSE(writeResult2.IsSet());
+ ASSERT_FALSE(writeResult3.IsSet());
+ ASSERT_FALSE(closeResult.IsSet());
+
+ auto readResult1 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult1), "foo");
+ ASSERT_TRUE(writeResult1.IsSet());
+ ASSERT_FALSE(writeResult2.IsSet());
+ ASSERT_FALSE(writeResult3.IsSet());
+ ASSERT_FALSE(closeResult.IsSet());
+
+ auto readResult2 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult2), "bar");
+ ASSERT_TRUE(writeResult1.IsSet());
+ ASSERT_TRUE(writeResult2.IsSet());
+ ASSERT_FALSE(writeResult3.IsSet());
+ ASSERT_FALSE(closeResult.IsSet());
+
+ auto readResult3 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult3), "baz");
+ ASSERT_TRUE(writeResult1.IsSet());
+ ASSERT_TRUE(writeResult2.IsSet());
+ ASSERT_TRUE(writeResult3.IsSet());
+ ASSERT_TRUE(closeResult.IsSet());
+
+ auto readResult4 = ReadAlreadySetValue(pipe);
+ ASSERT_FALSE(readResult4);
+}
+
+TEST(TAsyncOutputStreamTest, TestEmptyString)
+{
+ auto pipe = New<TAsyncStreamPipe>();
+ auto asyncWriter = CreateZeroCopyAdapter(static_cast<IAsyncOutputStreamPtr>(pipe));
+
+ auto writeResult1 = asyncWriter->Write(TSharedRef::FromString(""));
+ auto writeResult2 = asyncWriter->Write(TSharedRef::FromString(""));
+ auto closeResult = asyncWriter->Close();
+
+ auto readResult1 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult1), "");
+
+ auto readResult2 = ReadAlreadySetValue(pipe);
+ ASSERT_EQ(GetString(readResult1), "");
+
+ auto readResult3 = ReadAlreadySetValue(pipe);
+ ASSERT_FALSE(readResult3);
+ ASSERT_TRUE(writeResult1.IsSet());
+ ASSERT_TRUE(writeResult2.IsSet());
+ ASSERT_TRUE(closeResult.IsSet());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This class restricts max size of a read.
+class TMaxBlockSizeInputStream
+ : public IInputStream
+{
+public:
+ TMaxBlockSizeInputStream(IInputStream* inputStream, size_t maxBlockSize)
+ : InputStream_(inputStream)
+ , MaxBlockSize_(maxBlockSize)
+ { }
+
+protected:
+ virtual size_t DoRead(void* buf, size_t len) override
+ {
+ return InputStream_->Read(buf, std::min(len, MaxBlockSize_));
+ }
+
+private:
+ IInputStream* const InputStream_;
+ const size_t MaxBlockSize_;
+};
+
+//! This test creates a big block size async zero copy input stream
+//! over a small block size async input stream to provoke a stack overflow.
+TEST(IAsyncZeroCopyInputStreamTest, NoStackOverflow)
+{
+ TString buf(512_KB, 'a');
+ TMemoryInput memoryInput(buf.data(), buf.size());
+ TMaxBlockSizeInputStream maxBlockSizeInputStream(&memoryInput, 1);
+ auto asyncInputStream = CreateAsyncAdapter(&maxBlockSizeInputStream);
+ auto asyncZeroCopyInputStream = CreateZeroCopyAdapter(asyncInputStream, 256_KB);
+ asyncZeroCopyInputStream->ReadAll();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT:::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/async_yson_writer_ut.cpp b/yt/yt/core/concurrency/unittests/async_yson_writer_ut.cpp
new file mode 100644
index 0000000000..9344d741e7
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/async_yson_writer_ut.cpp
@@ -0,0 +1,155 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/async_writer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT::NYson {
+namespace {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TYsonString ConvertToListFragment(const std::vector<T>& items)
+{
+ TStringStream output;
+ TYsonWriter writer(&output, EYsonFormat::Binary, EYsonType::ListFragment);
+ for (const auto& item : items) {
+ using NYT::NYTree::Serialize;
+ writer.OnListItem();
+ Serialize(item, &writer);
+ }
+ return TYsonString(output.Str(), EYsonType::ListFragment);
+}
+
+template <class K, class V>
+TYsonString ConvertToMapFragment(const std::vector<std::pair<K, V>>& items)
+{
+ TStringStream output;
+ TYsonWriter writer(&output, EYsonFormat::Binary, EYsonType::MapFragment);
+ for (const auto& item : items) {
+ using NYT::NYTree::Serialize;
+ writer.OnKeyedItem(item.first);
+ Serialize(item.second, &writer);
+ }
+ return TYsonString(output.Str(), EYsonType::MapFragment);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TAsyncYsonWriterTest, SyncNode)
+{
+ TAsyncYsonWriter asyncWriter(EYsonType::Node);
+ asyncWriter.OnInt64Scalar(123);
+ EXPECT_EQ(
+ ConvertToYsonString(123),
+ asyncWriter.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, SyncList)
+{
+ TAsyncYsonWriter writer(EYsonType::Node);
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnStringScalar("a");
+ writer.OnListItem();
+ writer.OnStringScalar("b");
+ writer.OnListItem();
+ writer.OnStringScalar("c");
+ writer.OnEndList();
+ EXPECT_EQ(
+ ConvertToYsonString(std::vector<TString>{"a", "b", "c"}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, SyncListFragment)
+{
+ TAsyncYsonWriter writer(EYsonType::ListFragment);
+ writer.OnListItem();
+ writer.OnStringScalar("a");
+ writer.OnListItem();
+ writer.OnStringScalar("b");
+ writer.OnListItem();
+ writer.OnStringScalar("c");
+ EXPECT_EQ(
+ ConvertToListFragment(std::vector<TString>{"a", "b", "c"}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, SyncMapFragment)
+{
+ TAsyncYsonWriter writer(EYsonType::MapFragment);
+ writer.OnKeyedItem("a");
+ writer.OnInt64Scalar(1);
+ writer.OnKeyedItem("b");
+ writer.OnInt64Scalar(2);
+ writer.OnKeyedItem("c");
+ writer.OnInt64Scalar(3);
+ EXPECT_EQ(
+ ConvertToMapFragment(std::vector<std::pair<TString, int>>{{"a", 1}, {"b", 2}, {"c", 3}}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, AsyncNode)
+{
+ TAsyncYsonWriter asyncWriter(EYsonType::Node);
+ asyncWriter.OnRaw(MakeFuture(ConvertToYsonString(123)));
+ EXPECT_EQ(
+ ConvertToYsonString(123),
+ asyncWriter.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, AsyncListFragment)
+{
+ TAsyncYsonWriter writer(EYsonType::ListFragment);
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(1)));
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(2)));
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(3)));
+ EXPECT_EQ(
+ ConvertToListFragment(std::vector<int>{1, 2, 3}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, AsyncList)
+{
+ TAsyncYsonWriter writer(EYsonType::Node);
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(1)));
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(2)));
+ writer.OnListItem();
+ writer.OnRaw(MakeFuture(ConvertToYsonString(3)));
+ writer.OnEndList();
+ EXPECT_EQ(
+ ConvertToYsonString(std::vector<int>{1, 2, 3}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+TEST(TAsyncYsonWriterTest, AsyncMap)
+{
+ TAsyncYsonWriter writer(EYsonType::Node);
+ writer.OnBeginMap();
+ writer.OnKeyedItem("a");
+ writer.OnRaw(MakeFuture(ConvertToYsonString(1)));
+ writer.OnKeyedItem("b");
+ writer.OnRaw(MakeFuture(ConvertToYsonString(2)));
+ writer.OnKeyedItem("c");
+ writer.OnRaw(MakeFuture(ConvertToYsonString(3)));
+ writer.OnEndMap();
+
+ EXPECT_EQ(
+ ConvertToYsonString(THashMap<TString, int>{{"a", 1}, {"b", 2}, {"c", 3}}),
+ writer.Finish().Get().ValueOrThrow());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/concurrency/unittests/coroutines_ut.cpp b/yt/yt/core/concurrency/unittests/coroutines_ut.cpp
new file mode 100644
index 0000000000..e13d191aa5
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/coroutines_ut.cpp
@@ -0,0 +1,157 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <yt/yt/core/concurrency/coroutine.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCoroutineTest
+ : public ::testing::Test
+{ };
+
+void Coroutine0(TCoroutine<int()>& self)
+{
+ self.Yield(1);
+ self.Yield(2);
+ self.Yield(3);
+ self.Yield(4);
+ self.Yield(5);
+}
+
+TEST_F(TCoroutineTest, Nullary)
+{
+ TCoroutine<int()> coro(BIND(&Coroutine0));
+ EXPECT_FALSE(coro.IsCompleted());
+
+ int i;
+ std::optional<int> actual;
+ for (i = 1; /**/; ++i) {
+ actual = coro.Run();
+ if (coro.IsCompleted()) {
+ break;
+ }
+ EXPECT_TRUE(actual);
+ EXPECT_EQ(i, *actual);
+ }
+
+ EXPECT_FALSE(actual);
+ EXPECT_EQ(6, i);
+
+ EXPECT_TRUE(coro.IsCompleted());
+}
+
+void Coroutine1(TCoroutine<int(int)>& self, int arg)
+{
+ EXPECT_EQ(0, arg);
+ std::tie(arg) = self.Yield(arg + 1);
+ EXPECT_EQ(2, arg);
+ std::tie(arg) = self.Yield(arg + 1);
+ EXPECT_EQ(4, arg);
+ std::tie(arg) = self.Yield(arg + 1);
+ EXPECT_EQ(6, arg);
+ std::tie(arg) = self.Yield(arg + 1);
+ EXPECT_EQ(8, arg);
+ std::tie(arg) = self.Yield(arg + 1);
+ EXPECT_EQ(10, arg);
+}
+
+TEST_F(TCoroutineTest, Unary)
+{
+ TCoroutine<int(int)> coro(BIND(&Coroutine1));
+ EXPECT_FALSE(coro.IsCompleted());
+
+ // Alternative syntax.
+ int i = 0, j = 0;
+ std::optional<int> actual;
+ while ((actual = coro.Run(j))) {
+ ++i;
+ EXPECT_EQ(i * 2 - 1, *actual);
+ EXPECT_EQ(i * 2 - 2, j);
+ j = *actual+ 1;
+ }
+
+ EXPECT_FALSE(actual);
+ EXPECT_EQ(5, i);
+ EXPECT_EQ(10, j);
+
+ EXPECT_TRUE(coro.IsCompleted());
+}
+
+// In this case I've got lazy and set up these test cases.
+struct TTestCase {
+ int lhs;
+ int rhs;
+ int sum;
+};
+
+std::vector<TTestCase> Coroutine2TestCases = {
+ { 10, 20, 30 },
+ { 11, 21, 32 },
+ { 12, 22, 34 },
+ { 13, 23, 36 },
+ { 14, 24, 38 },
+ { 15, 25, 40 }
+};
+
+void Coroutine2(TCoroutine<int(int, int)>& self, int lhs, int rhs)
+{
+ for (int i = 0; i < std::ssize(Coroutine2TestCases); ++i) {
+ EXPECT_EQ(Coroutine2TestCases[i].lhs, lhs) << "Iteration #" << i;
+ EXPECT_EQ(Coroutine2TestCases[i].rhs, rhs) << "Iteration #" << i;
+ std::tie(lhs, rhs) = self.Yield(lhs + rhs);
+ }
+}
+
+TEST_F(TCoroutineTest, Binary)
+{
+ TCoroutine<int(int, int)> coro(BIND(&Coroutine2));
+ EXPECT_FALSE(coro.IsCompleted());
+
+ int i = 0;
+ std::optional<int> actual;
+ for (
+ i = 0;
+ (actual = coro.Run(
+ i < std::ssize(Coroutine2TestCases) ? Coroutine2TestCases[i].lhs : 0,
+ i < std::ssize(Coroutine2TestCases) ? Coroutine2TestCases[i].rhs : 0));
+ ++i
+ ) {
+ EXPECT_EQ(Coroutine2TestCases[i].sum, *actual);
+ }
+
+ EXPECT_FALSE(actual);
+ EXPECT_EQ(i, std::ssize(Coroutine2TestCases));
+
+ EXPECT_TRUE(coro.IsCompleted());
+}
+
+void Coroutine3(TCoroutine<void()>& self)
+{
+ for (int i = 0; i < 10; ++i) {
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(1));
+ self.Yield();
+ }
+}
+
+TEST_W(TCoroutineTest, WaitFor)
+{
+ TCoroutine<void()> coro(BIND(&Coroutine3));
+ for (int i = 0; i < 11; ++i) {
+ EXPECT_FALSE(coro.IsCompleted());
+ coro.Run();
+ }
+ EXPECT_TRUE(coro.IsCompleted());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/count_down_latch_ut.cpp b/yt/yt/core/concurrency/unittests/count_down_latch_ut.cpp
new file mode 100644
index 0000000000..6078e838a5
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/count_down_latch_ut.cpp
@@ -0,0 +1,81 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <library/cpp/yt/threading/count_down_latch.h>
+
+#include <thread>
+
+namespace NYT::NConcurrency {
+namespace {
+
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WaitForLatch(const TCountDownLatch& latch)
+{
+ latch.Wait();
+ EXPECT_EQ(0, latch.GetCount());
+}
+
+TEST(TCountDownLatch, TwoThreads)
+{
+ TCountDownLatch latch(2);
+
+ std::thread t1(std::bind(&WaitForLatch, std::cref(latch)));
+ std::thread t2(std::bind(&WaitForLatch, std::cref(latch)));
+
+ EXPECT_EQ(2, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(1, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(0, latch.GetCount());
+
+ t1.join();
+ t2.join();
+}
+
+TEST(TCountDownLatch, TwoThreadsPredecremented)
+{
+ TCountDownLatch latch(2);
+
+ EXPECT_EQ(2, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(1, latch.GetCount());
+ latch.CountDown();
+ EXPECT_EQ(0, latch.GetCount());
+
+ std::thread t1(std::bind(&WaitForLatch, std::cref(latch)));
+ std::thread t2(std::bind(&WaitForLatch, std::cref(latch)));
+
+ t1.join();
+ t2.join();
+}
+
+TEST(TCountDownLatch, TwoThreadsTwoLatches)
+{
+ TCountDownLatch first(1);
+ TCountDownLatch second(1);
+
+ std::thread t1([&] () {
+ first.Wait();
+ second.CountDown();
+ EXPECT_EQ(0, first.GetCount());
+ EXPECT_EQ(0, second.GetCount());
+ });
+
+ std::thread t2([&] () {
+ first.CountDown();
+ second.Wait();
+ EXPECT_EQ(0, first.GetCount());
+ EXPECT_EQ(0, second.GetCount());
+ });
+
+ t1.join();
+ t2.join();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/delayed_executor_ut.cpp b/yt/yt/core/concurrency/unittests/delayed_executor_ut.cpp
new file mode 100644
index 0000000000..b453b96fba
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/delayed_executor_ut.cpp
@@ -0,0 +1,128 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+using ::testing::TProbeState;
+using ::testing::TProbe;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDelayedExecutorTest, SubmitLarge)
+{
+ auto fired = std::make_shared<std::atomic<int>>(0);
+ auto state = std::make_shared<TProbeState>();
+
+ auto cookie = TDelayedExecutor::Submit(
+ BIND([fired, state, probe = TProbe(state.get())] () { ++*fired; }),
+ TDuration::MilliSeconds(1000));
+
+ Sleep(TDuration::MilliSeconds(500));
+
+ EXPECT_EQ(0, *fired);
+
+ Sleep(TDuration::MilliSeconds(700));
+
+ EXPECT_EQ(1, *fired);
+ EXPECT_EQ(1, state->Constructors);
+ EXPECT_EQ(1, state->Destructors);
+}
+
+TEST(TDelayedExecutorTest, SubmitSmall)
+{
+ auto fired = std::make_shared<std::atomic<int>>(0);
+ auto state = std::make_shared<TProbeState>();
+
+ auto cookie = TDelayedExecutor::Submit(
+ BIND([fired, state, probe = TProbe(state.get())] () { ++*fired; }),
+ TDuration::MilliSeconds(100));
+
+ Sleep(TDuration::MilliSeconds(50));
+
+ EXPECT_EQ(0, *fired);
+
+ Sleep(TDuration::MilliSeconds(70));
+
+ EXPECT_EQ(1, *fired);
+ EXPECT_EQ(1, state->Constructors);
+ EXPECT_EQ(1, state->Destructors);
+}
+
+TEST(TDelayedExecutorTest, SubmitZeroDelay)
+{
+ auto fired = std::make_shared<std::atomic<int>>(0);
+ auto state = std::make_shared<TProbeState>();
+
+ auto cookie1 = TDelayedExecutor::Submit(
+ BIND([fired, state, probe = TProbe(state.get())] () { ++*fired; }),
+ TDuration::MilliSeconds(0));
+
+ Sleep(TDuration::MilliSeconds(10));
+
+ EXPECT_EQ(1, *fired);
+
+ auto cookie2 = TDelayedExecutor::Submit(
+ BIND([fired, state, probe = TProbe(state.get())]() { ++*fired; }),
+ TDuration::MilliSeconds(10));
+
+ Sleep(TDuration::MilliSeconds(50));
+
+ EXPECT_EQ(2, *fired);
+ EXPECT_EQ(2, state->Constructors);
+ EXPECT_EQ(2, state->Destructors);
+}
+
+TEST(TDelayedExecutorTest, StressTest)
+{
+ auto fired = std::make_shared<std::atomic<int>>(0);
+
+ int total = 100;
+ for (int i = 0; i < total; ++i) {
+ auto start = TInstant::Now();
+ auto delay = rand() % 50;
+
+ auto cookie = TDelayedExecutor::Submit(
+ BIND([start, delay, fired] () {
+ i64 diff = (TInstant::Now() - start).MilliSeconds();
+ EXPECT_LE(delay, diff + 10);
+ EXPECT_LE(diff, delay + 100);
+ ++*fired;
+ }),
+ TDuration::MilliSeconds(delay));
+
+ Sleep(TDuration::MilliSeconds(rand() % 50));
+ }
+
+ Sleep(TDuration::MilliSeconds(50));
+
+ EXPECT_EQ(total, *fired);
+}
+
+TEST(TDelayedExecutorTest, SubmitAndCancel)
+{
+ auto fired = std::make_shared<std::atomic<int>>(0);
+ auto state = std::make_shared<TProbeState>();
+
+ auto cookie = TDelayedExecutor::Submit(
+ BIND([fired, state, probe = TProbe(state.get())] () { ++*fired; }),
+ TDuration::MilliSeconds(10));
+
+ TDelayedExecutor::CancelAndClear(cookie);
+
+ Sleep(TDuration::MilliSeconds(50));
+
+ EXPECT_EQ(0, *fired);
+ EXPECT_EQ(1, state->Constructors);
+ EXPECT_EQ(1, state->Destructors);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/concurrency/unittests/fair_share_invoker_pool_ut.cpp b/yt/yt/core/concurrency/unittests/fair_share_invoker_pool_ut.cpp
new file mode 100644
index 0000000000..ae9da43c6c
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/fair_share_invoker_pool_ut.cpp
@@ -0,0 +1,592 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/fair_share_invoker_pool.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+#include <yt/yt/core/misc/collection_helpers.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+
+#include <util/datetime/base.h>
+
+#include <algorithm>
+#include <array>
+#include <utility>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto Quantum = TDuration::MilliSeconds(100);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockFairShareCallbackQueue
+ : public IFairShareCallbackQueue
+{
+public:
+ explicit TMockFairShareCallbackQueue(int bucketCount)
+ : UnderlyingCallbackQueue_(CreateFairShareCallbackQueue(bucketCount))
+ , TotalCpuTime_(bucketCount)
+ { }
+
+ void Enqueue(TClosure callback, int bucketIndex) override
+ {
+ UnderlyingCallbackQueue_->Enqueue(std::move(callback), bucketIndex);
+ }
+
+ bool TryDequeue(TClosure* resultCallback, int* resultBucketIndex) override
+ {
+ return UnderlyingCallbackQueue_->TryDequeue(resultCallback, resultBucketIndex);
+ }
+
+ void AccountCpuTime(int bucketIndex, NProfiling::TCpuDuration cpuTime) override
+ {
+ YT_VERIFY(IsValidBucketIndex(bucketIndex));
+ TotalCpuTime_[bucketIndex] += cpuTime;
+ UnderlyingCallbackQueue_->AccountCpuTime(bucketIndex, cpuTime);
+ }
+
+ NProfiling::TCpuDuration GetTotalCpuTime(int bucketIndex) const
+ {
+ YT_VERIFY(IsValidBucketIndex(bucketIndex));
+ return TotalCpuTime_[bucketIndex];
+ }
+
+private:
+ const IFairShareCallbackQueuePtr UnderlyingCallbackQueue_;
+ std::vector<std::atomic<NProfiling::TCpuDuration>> TotalCpuTime_;
+
+ bool IsValidBucketIndex(int bucketIndex) const
+ {
+ return 0 <= bucketIndex && bucketIndex < std::ssize(TotalCpuTime_);
+ }
+};
+
+using TMockFairShareCallbackQueuePtr = TIntrusivePtr<TMockFairShareCallbackQueue>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareInvokerPoolTest
+ : public ::testing::Test
+{
+protected:
+ std::array<TLazyIntrusivePtr<TActionQueue>, 2> Queues_;
+
+ THashMap<IInvoker*, int> InvokerToIndex_;
+
+ TMockFairShareCallbackQueuePtr MockCallbackQueue;
+
+ struct TInvocationOrder
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ std::vector<int> InvokerIndexes_;
+ } InvocationOrder_;
+
+ void TearDown() override
+ {
+ for (int i = 0; i < std::ssize(Queues_); ++i) {
+ if (Queues_[i]) {
+ Queues_[i]->Shutdown();
+ }
+ }
+ }
+
+ template <typename TInvokerPoolPtr>
+ void InitializeInvokerToIndexMapping(const TInvokerPoolPtr& invokerPool, int invokerCount)
+ {
+ YT_VERIFY(invokerCount > 0);
+ InvokerToIndex_.clear();
+ for (int i = 0; i < invokerCount; ++i) {
+ auto invoker = invokerPool->GetInvoker(i);
+ InvokerToIndex_[invoker.Get()] = i;
+ }
+ }
+
+ int GetInvokerIndex(IInvoker* invokerAddress) const
+ {
+ return GetOrCrash(InvokerToIndex_, invokerAddress);
+ }
+
+ int GetCurrentInvokerIndex() const
+ {
+ return GetInvokerIndex(GetCurrentInvoker());
+ }
+
+ void ClearInvocationOrder()
+ {
+ auto guard = Guard(InvocationOrder_.Lock_);
+ InvocationOrder_.InvokerIndexes_.clear();
+ }
+
+ void PushInvokerIndexToInvocationOrder()
+ {
+ auto currentInvokerIndex = GetCurrentInvokerIndex();
+ auto guard = Guard(InvocationOrder_.Lock_);
+ InvocationOrder_.InvokerIndexes_.push_back(currentInvokerIndex);
+ }
+
+ std::vector<int> GetInvocationOrder()
+ {
+ auto guard = Guard(InvocationOrder_.Lock_);
+ return InvocationOrder_.InvokerIndexes_;
+ }
+
+ IDiagnosableInvokerPoolPtr CreateInvokerPool(IInvokerPtr underlyingInvoker, int invokerCount)
+ {
+ auto result = CreateFairShareInvokerPool(
+ std::move(underlyingInvoker),
+ invokerCount,
+ [this] (int bucketCount) {
+ YT_VERIFY(bucketCount > 0);
+ MockCallbackQueue = New<TMockFairShareCallbackQueue>(bucketCount);
+ return MockCallbackQueue;
+ });
+ InitializeInvokerToIndexMapping(result, invokerCount);
+ return result;
+ }
+
+ void ExpectInvokerIndex(int invokerIndex)
+ {
+ EXPECT_EQ(invokerIndex, GetCurrentInvokerIndex());
+ }
+
+ void ExpectTotalCpuTime(int bucketIndex, TDuration expectedCpuTime, TDuration precision = Quantum / 2)
+ {
+ // Push dummy callback to the scheduler queue and synchronously wait for it
+ // to ensure that all possible CPU time accounters were destroyed during fiber stack unwinding.
+ for (int i = 0; i < std::ssize(Queues_); ++i) {
+ if (Queues_[i]) {
+ auto invoker = Queues_[i]->GetInvoker();
+ BIND([] { }).AsyncVia(invoker).Run().Get().ThrowOnError();
+ }
+ }
+
+ auto precisionValue = NProfiling::DurationToValue(precision);
+ auto expectedValue = NProfiling::DurationToValue(expectedCpuTime);
+ auto actualValue = NProfiling::CpuDurationToValue(MockCallbackQueue->GetTotalCpuTime(bucketIndex));
+ EXPECT_GT(precisionValue, std::abs(expectedValue - actualValue));
+ }
+
+ void DoTestFairness(IInvokerPoolPtr invokerPool, int invokerCount)
+ {
+ YT_VERIFY(1 < invokerCount && invokerCount < 5);
+
+ // Each invoker executes some number of callbacks of the same duration |Quantum * (2 ^ #invokerIndex)|.
+ // Individual duration of callback and number of callbacks chosen
+ // such that total duration is same for all invokers.
+ auto getWeight = [] (int invokerIndex) {
+ return (1 << invokerIndex);
+ };
+ auto getSpinDuration = [getWeight] (int invokerIndex) {
+ return Quantum * getWeight(invokerIndex);
+ };
+ auto getCallbackCount = [getWeight, invokerCount] (int invokerIndex) {
+ // Weights are supposed to be in the ascending order.
+ return 4 * getWeight(invokerCount - 1) / getWeight(invokerIndex);
+ };
+
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < invokerCount; ++i) {
+ for (int j = 0, callbackCount = getCallbackCount(i); j < callbackCount; ++j) {
+ futures.push_back(
+ BIND([this, spinDuration = getSpinDuration(i)] {
+ PushInvokerIndexToInvocationOrder();
+ Spin(spinDuration);
+ }).AsyncVia(invokerPool->GetInvoker(i)).Run());
+ }
+ }
+
+ AllSucceeded(futures).Get().ThrowOnError();
+
+ auto invocationOrder = GetInvocationOrder();
+
+ // Test is considered successful if at any moment of the execution
+ // deviation of the weighted count of executed callbacks per invoker
+ // is not greater than the threshold (see in the code below).
+ std::vector<int> invocationCount(invokerCount);
+ for (auto invokerIndex : invocationOrder) {
+ YT_VERIFY(0 <= invokerIndex && invokerIndex < invokerCount);
+
+ ++invocationCount[invokerIndex];
+
+ auto getWeightedInvocationCount = [getWeight, &invocationCount] (int invokerIndex) {
+ return invocationCount[invokerIndex] * getWeight(invokerIndex);
+ };
+
+ auto minWeightedInvocationCount = getWeightedInvocationCount(0);
+ auto maxWeightedInvocationCount = minWeightedInvocationCount;
+ for (int i = 0; i < invokerCount; ++i) {
+ auto weightedInvocationCount = getWeightedInvocationCount(i);
+ minWeightedInvocationCount = std::min(minWeightedInvocationCount, weightedInvocationCount);
+ maxWeightedInvocationCount = std::max(maxWeightedInvocationCount, weightedInvocationCount);
+ }
+
+ // Compare threshold and deviation.
+ EXPECT_GE(getWeight(invokerCount - 1), maxWeightedInvocationCount - minWeightedInvocationCount);
+ }
+
+ for (int i = 0; i < invokerCount; ++i) {
+ EXPECT_EQ(getCallbackCount(i), invocationCount[i]);
+ }
+ }
+
+ void DoTestFairness(int invokerCount)
+ {
+ DoTestFairness(
+ CreateInvokerPool(Queues_[0]->GetInvoker(), invokerCount),
+ invokerCount);
+ }
+
+ void DoTestSwitchTo(int switchToCount)
+ {
+ YT_VERIFY(switchToCount > 0);
+
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), switchToCount + 1);
+
+ auto callback = BIND([this, invokerPool, switchToCount] () {
+ for (int i = 1; i <= switchToCount; ++i) {
+ ExpectInvokerIndex(i - 1);
+ Spin(Quantum * i);
+ SwitchTo(invokerPool->GetInvoker(i));
+ }
+ ExpectInvokerIndex(switchToCount);
+ Spin(Quantum * (switchToCount + 1));
+ }).AsyncVia(invokerPool->GetInvoker(0));
+
+ callback.Run().Get().ThrowOnError();
+
+ for (int i = 0; i <= switchToCount; ++i) {
+ ExpectTotalCpuTime(i, Quantum * (i + 1));
+ }
+ }
+
+ void DoTestWaitFor(int waitForCount)
+ {
+ YT_VERIFY(waitForCount > 0);
+
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), 2);
+
+ auto callback = BIND([waitForCount] {
+ Spin(Quantum);
+ for (int i = 0; i < waitForCount; ++i) {
+ TDelayedExecutor::WaitForDuration(Quantum);
+ Spin(Quantum);
+ }
+ }).AsyncVia(invokerPool->GetInvoker(0));
+
+ callback.Run().Get().ThrowOnError();
+
+ ExpectTotalCpuTime(0, Quantum * (waitForCount + 1));
+ ExpectTotalCpuTime(1, TDuration::Zero());
+ }
+
+ void DoTestGetAverageWaitTime(int invokerCount, std::vector<int> waitingActionCounts)
+ {
+ YT_VERIFY(std::ssize(waitingActionCounts) == invokerCount);
+
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), invokerCount);
+
+ // Test plan:
+ // - Each invoker in the pool will have a blocker action followed by |waitingActionCounts[i]| waiting actions.
+ // - Testing is done in |invokerCount| stages:
+ // (1) The blocker action of the i-th invoker starts and triggers the |stageStartedEvents[i]|.
+ // (2) We check current average wait time returned by every invoker.
+ // (3) We trigger |stageFinishedEvents[i]| to release the blocker action of the i-th invoker.
+
+ std::vector<NThreading::TEvent> stageStartedEvents(invokerCount);
+ std::vector<NThreading::TEvent> stageFinishedEvents(invokerCount);
+ std::vector<TInstant> blockingActionEnqueueTimes;
+ std::vector<TFuture<void>> blockingActionFutures;
+ std::vector<std::vector<TInstant>> waitingActionEnqueueTimesPerInvoker(invokerCount);
+ std::vector<std::vector<TFuture<void>>> waitingActionFuturesPerInvoker(invokerCount);
+
+ // Enqueue actions to invokers.
+ for (int invokerIndex = 0; invokerIndex < invokerCount; ++invokerIndex) {
+ blockingActionEnqueueTimes.push_back(NProfiling::GetInstant());
+ blockingActionFutures.emplace_back(
+ BIND([&stageFinishedEvents, &stageStartedEvents, invokerIndex] {
+ stageStartedEvents[invokerIndex].NotifyOne();
+ YT_VERIFY(stageFinishedEvents[invokerIndex].Wait(Quantum * 100));
+ })
+ .AsyncVia(invokerPool->GetInvoker(invokerIndex))
+ .Run());
+ Spin(Quantum);
+
+ auto waitingActionCount = waitingActionCounts[invokerIndex];
+ auto& waitingActionEnqueueTimes = waitingActionEnqueueTimesPerInvoker[invokerIndex];
+ auto& waitingActionFutures = waitingActionFuturesPerInvoker[invokerIndex];
+
+ for (int i = 0; i < waitingActionCount; ++i) {
+ waitingActionEnqueueTimes.push_back(NProfiling::GetInstant());
+ waitingActionFutures.emplace_back(BIND([] {}).AsyncVia(invokerPool->GetInvoker(invokerIndex)).Run());
+
+ Spin(Quantum);
+ }
+ }
+
+ // Test average wait time.
+ for (int stage = 0; stage < invokerCount; ++stage) {
+ YT_VERIFY(stageStartedEvents[stage].Wait(Quantum * 100));
+
+ // Collect average wait times.
+ std::vector<TDuration> averageWaitTimes(invokerCount);
+ for (int invokerIndex = 0; invokerIndex < invokerCount; ++invokerIndex) {
+ averageWaitTimes[invokerIndex] = invokerPool->GetInvokerStatistics(invokerIndex).AverageWaitTime;
+ }
+ auto averageWaitTimesCollectedTime = NProfiling::GetInstant();
+
+ // Check the collected average wait times.
+ // We go through all enqueued actions of all invokers in reverse order.
+ // Actions were enqueued with an interval of 1 |Quantum|, so to estimate a lower bound of
+ // how long the current action has been waiting we will count the number of actions,
+ // which were enqueued after it.
+ int enqueuedAfterCurrentActionCount = 0;
+ for (int invokerIndex = invokerCount - 1; invokerIndex >= 0; --invokerIndex) {
+ static const auto Margin = TDuration::MicroSeconds(10);
+
+ auto averageWaitTime = averageWaitTimes[invokerIndex];
+ auto waitingActionCount = waitingActionCounts[invokerIndex];
+ auto blockingActionEnqueueTime = blockingActionEnqueueTimes[invokerIndex];
+ const auto& waitingActionEnqueueTimes = waitingActionEnqueueTimesPerInvoker[invokerIndex];
+
+ auto totalWaitTimeLowerBound = TDuration::Zero();
+ for (int i = 0; i < waitingActionCount; ++i) {
+ totalWaitTimeLowerBound += (Quantum - Margin) * (enqueuedAfterCurrentActionCount + 1);
+ ++enqueuedAfterCurrentActionCount;
+ }
+ // Account for the blocking action of this invoker.
+ if (invokerIndex > stage) {
+ // Blocking action of this invoker is still waiting.
+ totalWaitTimeLowerBound += (Quantum - Margin) * (enqueuedAfterCurrentActionCount + 1);
+ ++waitingActionCount;
+ }
+ ++enqueuedAfterCurrentActionCount;
+
+ // Check upper bound too, to avoid monstrous numbers due to UB or other reasons.
+ auto totalWaitTimeUpperBound = TDuration::Zero();
+ for (auto actionEnqueueTime : waitingActionEnqueueTimes) {
+ totalWaitTimeUpperBound += (averageWaitTimesCollectedTime - actionEnqueueTime) + Margin;
+ }
+ // Account for the blocking action of this invoker.
+ if (invokerIndex > stage) {
+ // Blocking action of this invoker is still waiting.
+ totalWaitTimeUpperBound += (averageWaitTimesCollectedTime - blockingActionEnqueueTime) + Margin;
+ }
+
+ EXPECT_GE(averageWaitTime * waitingActionCount, totalWaitTimeLowerBound);
+ EXPECT_LE(averageWaitTime * waitingActionCount, totalWaitTimeUpperBound);
+ }
+
+ // Finish current stage.
+ stageFinishedEvents[stage].NotifyOne();
+ }
+
+ // Wait for all actions to finish.
+ WaitFor(AllSet(blockingActionFutures)).ThrowOnError();
+ for (int invokerIndex = 0; invokerIndex < invokerCount; ++invokerIndex) {
+ WaitFor(AllSet(waitingActionFuturesPerInvoker[invokerIndex])).ThrowOnError();
+ }
+ }
+
+ static void Spin(TDuration duration)
+ {
+ NProfiling::TFiberWallTimer timer;
+ while (timer.GetElapsedTime() < duration) {
+ }
+ }
+};
+
+TEST_F(TFairShareInvokerPoolTest, Fairness2)
+{
+ DoTestFairness(2);
+}
+
+TEST_F(TFairShareInvokerPoolTest, Fairness3)
+{
+ DoTestFairness(3);
+}
+
+TEST_F(TFairShareInvokerPoolTest, Fairness4)
+{
+ DoTestFairness(4);
+}
+
+TEST_F(TFairShareInvokerPoolTest, SwitchTo12)
+{
+ DoTestSwitchTo(1);
+}
+
+TEST_F(TFairShareInvokerPoolTest, SwitchTo123)
+{
+ DoTestSwitchTo(2);
+}
+
+TEST_F(TFairShareInvokerPoolTest, SwitchTo1234)
+{
+ DoTestSwitchTo(3);
+}
+
+TEST_F(TFairShareInvokerPoolTest, SwitchTo121)
+{
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), 2);
+
+ auto callback = BIND([this, invokerPool] {
+ SwitchTo(invokerPool->GetInvoker(0));
+ ExpectInvokerIndex(0);
+ Spin(Quantum);
+
+ SwitchTo(invokerPool->GetInvoker(1));
+ ExpectInvokerIndex(1);
+ Spin(Quantum * 3);
+
+ SwitchTo(invokerPool->GetInvoker(0));
+ ExpectInvokerIndex(0);
+ Spin(Quantum);
+ }).AsyncVia(invokerPool->GetInvoker(0));
+
+ callback.Run().Get().ThrowOnError();
+
+ ExpectTotalCpuTime(0, Quantum * 2);
+ ExpectTotalCpuTime(1, Quantum * 3);
+}
+
+TEST_F(TFairShareInvokerPoolTest, SwitchTo111AndSwitchTo222)
+{
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), 2);
+
+ std::vector<TFuture<void>> futures;
+
+ futures.push_back(
+ BIND([this] {
+ ExpectInvokerIndex(0);
+ Spin(Quantum);
+ SwitchTo(GetCurrentInvoker());
+
+ ExpectInvokerIndex(0);
+ Spin(Quantum);
+ SwitchTo(GetCurrentInvoker());
+
+ ExpectInvokerIndex(0);
+ Spin(Quantum);
+ }).AsyncVia(invokerPool->GetInvoker(0)).Run());
+
+ futures.push_back(
+ BIND([this] {
+ ExpectInvokerIndex(1);
+ Spin(Quantum);
+ SwitchTo(GetCurrentInvoker());
+
+ ExpectInvokerIndex(1);
+ Spin(Quantum);
+ SwitchTo(GetCurrentInvoker());
+
+ ExpectInvokerIndex(1);
+ Spin(Quantum);
+ }).AsyncVia(invokerPool->GetInvoker(1)).Run());
+
+ AllSucceeded(futures).Get().ThrowOnError();
+
+ ExpectTotalCpuTime(0, Quantum * 3);
+ ExpectTotalCpuTime(1, Quantum * 3);
+}
+
+TEST_F(TFairShareInvokerPoolTest, WaitFor1)
+{
+ DoTestWaitFor(1);
+}
+
+TEST_F(TFairShareInvokerPoolTest, WaitFor2)
+{
+ DoTestWaitFor(2);
+}
+
+TEST_F(TFairShareInvokerPoolTest, WaitFor3)
+{
+ DoTestWaitFor(3);
+}
+
+TEST_F(TFairShareInvokerPoolTest, CpuTimeAccountingBetweenContextSwitchesIsNotSupportedYet)
+{
+ auto threadPool = CreateThreadPool(2, "ThreadPool");
+ auto invokerPool = CreateInvokerPool(threadPool->GetInvoker(), 2);
+
+ NThreading::TEvent started;
+
+ // Start busy loop in the first thread via first fair share invoker.
+ auto future = BIND([this, &started] {
+ Spin(Quantum * 10);
+
+ auto invocationOrder = GetInvocationOrder();
+ EXPECT_TRUE(invocationOrder.empty());
+
+ started.NotifyOne();
+
+ Spin(Quantum * 50);
+
+ invocationOrder = GetInvocationOrder();
+ EXPECT_TRUE(!invocationOrder.empty());
+ }).AsyncVia(invokerPool->GetInvoker(0)).Run();
+
+ YT_VERIFY(started.Wait(Quantum * 100));
+
+ // After 10 quantums of time (see notification of the #started variable) we start Fairness test in the second thread.
+ // In case of better implementation we expect to have non-fair CPU time distribution between first and second invokers,
+ // because first invoker is given more CPU time in the first thread (at least within margin of 10 quantums).
+ // But CPU accounting is not supported for running callbacks, therefore we expect Fairness test to pass.
+ DoTestFairness(invokerPool, 2);
+
+ future.Get().ThrowOnError();
+}
+
+TEST_F(TFairShareInvokerPoolTest, GetAverageWaitTimeIsZeroForEmptyPool)
+{
+ auto invokerPool = CreateInvokerPool(Queues_[0]->GetInvoker(), 1);
+
+ EXPECT_EQ(TDuration::Zero(), invokerPool->GetInvokerStatistics(0).AverageWaitTime);
+
+ WaitFor(BIND([] {
+ Spin(Quantum);
+ }).AsyncVia(invokerPool->GetInvoker(0)).Run()).ThrowOnError();
+
+ EXPECT_EQ(TDuration::Zero(), invokerPool->GetInvokerStatistics(0).AverageWaitTime);
+}
+
+TEST_F(TFairShareInvokerPoolTest, GetAverageWaitTimeOneBucketOneWaitingAction)
+{
+ DoTestGetAverageWaitTime(
+ /* invokerCount */ 1,
+ /* waitingActionCounts */ {1});
+}
+
+TEST_F(TFairShareInvokerPoolTest, GetAverageWaitTimeOneBucketTenWaitingActions)
+{
+ DoTestGetAverageWaitTime(
+ /* invokerCount */ 1,
+ /* waitingActionCounts */ {10});
+}
+
+TEST_F(TFairShareInvokerPoolTest, GetAverageWaitTimeTwoBuckets)
+{
+ DoTestGetAverageWaitTime(
+ /* invokerCount */ 2,
+ /* waitingActionCounts */ {4, 8});
+}
+
+TEST_F(TFairShareInvokerPoolTest, GetAverageWaitTimeThreeBuckets)
+{
+ DoTestGetAverageWaitTime(
+ /* invokerCount */ 3,
+ /* waitingActionCounts */ {1, 2, 3});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/fair_share_thread_pool_ut.cpp b/yt/yt/core/concurrency/unittests/fair_share_thread_pool_ut.cpp
new file mode 100644
index 0000000000..a05f5bc864
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/fair_share_thread_pool_ut.cpp
@@ -0,0 +1,41 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/fair_share_thread_pool.h>
+
+#include <yt/yt/core/actions/invoker.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <util/random/random.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFairShareThreadPoolTest, Configure)
+{
+ auto threadPool = CreateFairShareThreadPool(1, "Test");
+ auto counter = std::make_shared<std::atomic<int>>();
+ auto callback = BIND([=] { ++*counter; });
+ std::vector<TFuture<void>> futures;
+
+ const int N = 10000;
+ for (int i = 0; i < N; ++i) {
+ auto invoker = threadPool->GetInvoker(ToString(RandomNumber<size_t>(10)));
+ futures.push_back(callback.AsyncVia(invoker).Run());
+ if (i % 100 == 0) {
+ threadPool->Configure(RandomNumber<size_t>(10) + 1);
+ }
+ }
+
+ AllSucceeded(std::move(futures))
+ .Get();
+ threadPool->Shutdown();
+ EXPECT_EQ(N, counter->load());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/fair_throttler_ut.cpp b/yt/yt/core/concurrency/unittests/fair_throttler_ut.cpp
new file mode 100644
index 0000000000..67fb2c9a3c
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/fair_throttler_ut.cpp
@@ -0,0 +1,317 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/fair_throttler.h>
+
+#include <library/cpp/testing/common/env.h>
+
+#include <vector>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFairThrottlerDistributionTest, SingleWeight)
+{
+ for (double weight : std::vector<double>{1, 0.01, 100}) {
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(10, {weight}, {5}, {{}}), std::vector<i64>{5});
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(10, {weight}, {5}, {4}), std::vector<i64>{4});
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(10, {weight}, {12}, {{}}), std::vector<i64>{10});
+ }
+}
+
+TEST(TFairThrottlerDistributionTest, TwoBuckets)
+{
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(
+ 10,
+ {1, 1},
+ {5, 10},
+ {{}, {}}),
+ (std::vector<i64>{5, 5}));
+
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(
+ 10,
+ {1, 1},
+ {2, 10},
+ {{}, {}}),
+ (std::vector<i64>{2, 8}));
+
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(
+ 10,
+ {2, 3},
+ {10, 10},
+ {{}, {}}),
+ (std::vector<i64>{4, 6}));
+
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(
+ 10,
+ {2, 3},
+ {10, 10},
+ {{}, {5}}),
+ (std::vector<i64>{5, 5}));
+
+ ASSERT_EQ(TFairThrottler::ComputeFairDistribution(
+ 10,
+ {2, 3},
+ {0, 10},
+ {{}, {5}}),
+ (std::vector<i64>{0, 5}));
+}
+
+static NLogging::TLogger Logger{"FairThrottlerTest"};
+
+struct TFairThrottlerTest
+ : public ::testing::Test
+{
+ TFairThrottlerConfigPtr Config = New<TFairThrottlerConfig>();
+ TFairThrottlerBucketConfigPtr BucketConfig = New<TFairThrottlerBucketConfig>();
+ TFairThrottlerPtr FairThrottler;
+
+ TFairThrottlerTest()
+ {
+ Config->TotalLimit = 100;
+
+ auto logger = Logger.WithTag("Test: %v", ::testing::UnitTest::GetInstance()->current_test_info()->name());
+ FairThrottler = New<TFairThrottler>(Config, logger, NProfiling::TProfiler{});
+ }
+};
+
+TEST_F(TFairThrottlerTest, SingleBucketNoRequests)
+{
+ FairThrottler->CreateBucketThrottler("main", BucketConfig);
+ Sleep(TDuration::Seconds(1));
+}
+
+TEST_F(TFairThrottlerTest, TwoBucketNoRequests)
+{
+ FairThrottler->CreateBucketThrottler("first", BucketConfig);
+ FairThrottler->CreateBucketThrottler("second", BucketConfig);
+ Sleep(TDuration::Seconds(1));
+}
+
+TEST_F(TFairThrottlerTest, SingleBucketRequests)
+{
+ auto bucket = FairThrottler->CreateBucketThrottler("main", BucketConfig);
+
+ std::vector<TFuture<void>> requests;
+
+ int blocked = 0;
+ for (int i = 0; i < 10; i++) {
+ auto req = bucket->Throttle(10);
+ blocked += req.IsSet() ? 0 : 1;
+
+ Sleep(TDuration::MilliSeconds(200));
+ requests.push_back(req);
+ }
+
+ ASSERT_LE(blocked, 2);
+}
+
+int CountCompleted(const std::vector<TFuture<void>>& requests)
+{
+ int complete = 0;
+ for (const auto& req : requests) {
+ if (req.IsSet()) {
+ complete++;
+ }
+ }
+ return complete;
+}
+
+TEST_F(TFairThrottlerTest, EvenDistributionTwoBuckets)
+{
+ auto first = FairThrottler->CreateBucketThrottler("first", BucketConfig);
+ auto second = FairThrottler->CreateBucketThrottler("second", BucketConfig);
+
+ std::vector<TFuture<void>> firstRequests, secondRequests;
+
+ for (int i = 0; i < 100; i++) {
+ firstRequests.push_back(first->Throttle(10));
+ secondRequests.push_back(second->Throttle(10));
+ }
+
+ Sleep(TDuration::Seconds(5));
+
+ auto firstComplete = CountCompleted(firstRequests);
+ auto secondComplete = CountCompleted(secondRequests);
+
+ ASSERT_GE(firstComplete, 20);
+ ASSERT_GE(secondComplete, 20);
+ ASSERT_LE(firstComplete, 30);
+ ASSERT_LE(secondComplete, 30);
+}
+
+TEST_F(TFairThrottlerTest, BucketWeight)
+{
+ auto first = FairThrottler->CreateBucketThrottler("light", BucketConfig);
+
+ auto heavyConfig = New<TFairThrottlerBucketConfig>();
+ heavyConfig->Weight = 4;
+
+ auto second = FairThrottler->CreateBucketThrottler("heavy", heavyConfig);
+
+ std::vector<TFuture<void>> firstRequests, secondRequests;
+
+ for (int i = 0; i < 100; i++) {
+ firstRequests.push_back(first->Throttle(10));
+ secondRequests.push_back(second->Throttle(10));
+ }
+
+ Sleep(TDuration::Seconds(5));
+
+ auto firstComplete = CountCompleted(firstRequests);
+ auto secondComplete = CountCompleted(secondRequests);
+
+ ASSERT_GE(firstComplete, 8);
+ ASSERT_GE(secondComplete, 35);
+ ASSERT_LE(firstComplete, 12);
+ ASSERT_LE(secondComplete, 45);
+}
+
+TEST_F(TFairThrottlerTest, BucketLimit)
+{
+ auto unlimitedBucket = FairThrottler->CreateBucketThrottler("first", BucketConfig);
+
+ auto limitedConfig = New<TFairThrottlerBucketConfig>();
+ limitedConfig->RelativeLimit = 0.5;
+ limitedConfig->Weight = 100;
+
+ auto limitedBucket = FairThrottler->CreateBucketThrottler("second", limitedConfig);
+
+ std::vector<TFuture<void>> unlimitedRequests, limitedRequest;
+
+ for (int i = 0; i < 100; i++) {
+ unlimitedRequests.push_back(unlimitedBucket->Throttle(10));
+ limitedRequest.push_back(limitedBucket->Throttle(10));
+ }
+
+ Sleep(TDuration::Seconds(5));
+
+ auto unlimitedComplete = CountCompleted(unlimitedRequests);
+ auto limitedComplete = CountCompleted(limitedRequest);
+
+ ASSERT_GE(unlimitedComplete, 20);
+ ASSERT_GE(limitedComplete, 20);
+ ASSERT_LE(unlimitedComplete, 30);
+ ASSERT_LE(limitedComplete, 30);
+}
+
+TEST_F(TFairThrottlerTest, Cancel)
+{
+ auto bucket = FairThrottler->CreateBucketThrottler("main", BucketConfig);
+
+ std::vector<TFuture<void>> cancelledRequests;
+ for (int i = 0; i < 100; i++) {
+ cancelledRequests.push_back(bucket->Throttle(10));
+ }
+
+ ASSERT_FALSE(bucket->TryAcquire(1));
+
+ for (const auto& req : cancelledRequests) {
+ req.Cancel(TError("Cancel"));
+ }
+
+ Sleep(TDuration::MilliSeconds(500));
+
+ ASSERT_TRUE(bucket->TryAcquire(1));
+}
+
+TEST_F(TFairThrottlerTest, AcquireOverdraft)
+{
+ auto bucket = FairThrottler->CreateBucketThrottler("main", BucketConfig);
+
+ for (int i = 0; i < 100; i++) {
+ bucket->Acquire(5);
+ }
+
+ for (int i = 0; i < 3; i++) {
+ ASSERT_FALSE(bucket->TryAcquire(1));
+ ASSERT_TRUE(bucket->IsOverdraft());
+
+ Sleep(TDuration::Seconds(1));
+ }
+
+ Sleep(TDuration::Seconds(5));
+ ASSERT_TRUE(bucket->TryAcquire(1));
+ ASSERT_FALSE(bucket->IsOverdraft());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFairThrottlerIPCTest
+ : public ::testing::Test
+{
+ TFairThrottlerConfigPtr Config = New<TFairThrottlerConfig>();
+ TFairThrottlerBucketConfigPtr BucketConfig = New<TFairThrottlerBucketConfig>();
+
+ TFairThrottlerPtr DatNode, ExeNode;
+
+ TFairThrottlerIPCTest()
+ {
+ TString testName = ::testing::UnitTest::GetInstance()->current_test_info()->name();
+
+ Config->IPCPath = GetOutputPath() / (testName + ".throttler");
+ Config->TotalLimit = 100;
+
+ auto logger = Logger.WithTag("Test: %v", testName);
+
+ DatNode = New<TFairThrottler>(Config, logger.WithTag("Node: dat"), NProfiling::TProfiler{});
+ ExeNode = New<TFairThrottler>(Config, logger.WithTag("Node: exe"), NProfiling::TProfiler{});
+ }
+};
+
+TEST_F(TFairThrottlerIPCTest, TwoBucket)
+{
+ auto first = DatNode->CreateBucketThrottler("first", BucketConfig);
+ auto second = ExeNode->CreateBucketThrottler("second", BucketConfig);
+
+ std::vector<TFuture<void>> firstRequests, secondRequests;
+
+ for (int i = 0; i < 100; i++) {
+ firstRequests.push_back(first->Throttle(10));
+ secondRequests.push_back(second->Throttle(10));
+ }
+
+ Sleep(TDuration::Seconds(5));
+
+ auto firstComplete = CountCompleted(firstRequests);
+ auto secondComplete = CountCompleted(secondRequests);
+
+ ASSERT_GE(firstComplete, 20);
+ ASSERT_GE(secondComplete, 20);
+ ASSERT_LE(firstComplete, 30);
+ ASSERT_LE(secondComplete, 30);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef _win_
+TEST(TFileIPC, Test)
+{
+ auto path = GetOutputPath() / "test_ipc";
+
+ auto a = CreateFileThrottlerIPC(path);
+ auto b = CreateFileThrottlerIPC(path);
+
+ ASSERT_TRUE(a->TryLock());
+ ASSERT_FALSE(b->TryLock());
+
+ ASSERT_EQ(0u, a->ListBuckets().size());
+ ASSERT_EQ(0u, b->ListBuckets().size());
+
+ auto b0 = a->AddBucket();
+
+ ASSERT_EQ(0u, a->ListBuckets().size());
+ ASSERT_EQ(1u, b->ListBuckets().size());
+
+ b0.Reset();
+
+ ASSERT_EQ(0u, a->ListBuckets().size());
+ ASSERT_EQ(0u, b->ListBuckets().size());
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/fls_ut.cpp b/yt/yt/core/concurrency/unittests/fls_ut.cpp
new file mode 100644
index 0000000000..19d3802594
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/fls_ut.cpp
@@ -0,0 +1,113 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/fls.h>
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/actions/callback.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <util/system/yield.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::atomic<int> CtorCalls;
+std::atomic<int> DtorCalls;
+
+struct TMyValue
+{
+ TString Value;
+ static void Reset()
+ {
+ CtorCalls = 0;
+ DtorCalls = 0;
+ }
+
+ TMyValue()
+ {
+ ++CtorCalls;
+ }
+
+ ~TMyValue()
+ {
+ ++DtorCalls;
+ }
+};
+
+class TFlsTest
+ : public ::testing::Test
+{
+protected:
+ const TActionQueuePtr ActionQueue = New<TActionQueue>();
+
+ void SetUp() override
+ {
+ TMyValue::Reset();
+ }
+
+ void TearDown() override
+ {
+ ActionQueue->Shutdown();
+ }
+};
+
+TFlsSlot<TMyValue> Slot;
+
+TEST_F(TFlsTest, IsInitialized)
+{
+ BIND([&] {
+ EXPECT_FALSE(Slot.IsInitialized());
+ })
+ .AsyncVia(ActionQueue->GetInvoker())
+ .Run()
+ .Get();
+
+ EXPECT_EQ(CtorCalls, 0);
+ EXPECT_EQ(DtorCalls, 0);
+}
+
+TEST_F(TFlsTest, TwoFibers)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+
+ auto f1 = BIND([&] {
+ Slot->Value = "fiber1";
+ WaitFor(p1.ToFuture())
+ .ThrowOnError();
+ EXPECT_EQ("fiber1", Slot->Value);
+ })
+ .AsyncVia(ActionQueue->GetInvoker())
+ .Run();
+
+ auto f2 = BIND([&] {
+ Slot->Value = "fiber2";
+ WaitFor(p2.ToFuture())
+ .ThrowOnError();
+ EXPECT_EQ("fiber2", Slot->Value);
+ })
+ .AsyncVia(ActionQueue->GetInvoker())
+ .Run();
+
+ p1.Set();
+ p2.Set();
+
+ WaitFor(f1)
+ .ThrowOnError();
+ WaitFor(f2)
+ .ThrowOnError();
+
+ EXPECT_EQ(CtorCalls, 2);
+ while (DtorCalls != 2) {
+ ThreadYield();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/invoker_alarm_ut.cpp b/yt/yt/core/concurrency/unittests/invoker_alarm_ut.cpp
new file mode 100644
index 0000000000..fa58f5f8c3
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/invoker_alarm_ut.cpp
@@ -0,0 +1,89 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/invoker_alarm.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/actions/invoker.h>
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TInvokerAlarmTest
+ : public ::testing::Test
+{
+protected:
+ const TActionQueuePtr ActionQueue_ = New<TActionQueue>();
+ const ISuspendableInvokerPtr Invoker_ = CreateSuspendableInvoker(ActionQueue_->GetInvoker());
+ const TInvokerAlarmPtr Alarm_ = New<TInvokerAlarm>(Invoker_);
+
+ template <class F>
+ void Do(F func)
+ {
+ BIND(func)
+ .AsyncVia(ActionQueue_->GetInvoker())
+ .Run()
+ .Get();
+ }
+};
+
+TEST_F(TInvokerAlarmTest, CheckUnarmed)
+{
+ Do([&] {
+ EXPECT_FALSE(Alarm_->Check());
+ });
+}
+
+TEST_F(TInvokerAlarmTest, ArmDisarm)
+{
+ Do([&] {
+ auto invoked = std::make_shared<std::atomic<bool>>();
+ Alarm_->Arm(BIND([=] { *invoked = true; }), TDuration::MilliSeconds(100));
+ Alarm_->Disarm();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(200));
+ EXPECT_FALSE(*invoked);
+ });
+}
+
+TEST_F(TInvokerAlarmTest, DisarmUnarmed)
+{
+ Do([&] {
+ EXPECT_FALSE(Alarm_->IsArmed());
+ Alarm_->Disarm();
+ EXPECT_FALSE(Alarm_->IsArmed());
+ });
+}
+
+TEST_F(TInvokerAlarmTest, ArmSchedulesCallback)
+{
+ Do([&] {
+ auto invoked = std::make_shared<std::atomic<bool>>();
+ Alarm_->Arm(BIND([=] { *invoked = true; }), TDuration::MilliSeconds(100));
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(200));
+ EXPECT_TRUE(*invoked);
+ EXPECT_FALSE(Alarm_->IsArmed());
+ });
+}
+
+TEST_F(TInvokerAlarmTest, CheckInvokesCallback)
+{
+ Do([&] {
+ auto invoked = std::make_shared<std::atomic<bool>>();
+ Alarm_->Arm(BIND([=] { *invoked = true; }), TDuration::MilliSeconds(100));
+ Invoker_->Suspend().Get();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(200));
+ EXPECT_FALSE(*invoked);
+ EXPECT_TRUE(Alarm_->Check());
+ EXPECT_TRUE(*invoked);
+ EXPECT_FALSE(Alarm_->IsArmed());
+ Invoker_->Resume();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/invoker_pool_ut.cpp b/yt/yt/core/concurrency/unittests/invoker_pool_ut.cpp
new file mode 100644
index 0000000000..8f346ed163
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/invoker_pool_ut.cpp
@@ -0,0 +1,291 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/core/actions/invoker.h>
+#include <yt/yt/core/actions/invoker_detail.h>
+#include <yt/yt/core/actions/invoker_pool.h>
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <library/cpp/yt/threading/count_down_latch.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPrioritizedInvokerPtr CreatePrioritizedInvokerTest(IInvokerPtr underlyingInvoker)
+{
+ return CreatePrioritizedInvoker(std::move(underlyingInvoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IInvokerPoolPtr CreateDummyInvokerPool(IInvokerPtr underlyingInvoker, int invokerCount)
+{
+ YT_VERIFY(invokerCount > 0);
+ std::vector<IInvokerPtr> underlyingInvokers(invokerCount, underlyingInvoker);
+ return New<NYT::NDetail::TInvokerPoolWrapper<IInvoker>>(std::move(underlyingInvokers));
+}
+
+IInvokerPtr CreateIdenticalInvoker(IInvokerPtr invoker)
+{
+ return invoker;
+}
+
+IInvokerPtr CreateIdenticalInvokerByConstReference(const IInvokerPtr& invoker)
+{
+ return invoker;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockInvoker;
+using IMockInvokerPool = IGenericInvokerPool<TMockInvoker>;
+using IMockInvokerPoolPtr = TIntrusivePtr<IMockInvokerPool>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockInvoker
+ : public TInvokerWrapper
+{
+public:
+ explicit TMockInvoker(IInvokerPtr underlyingInvoker)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ , InvocationCount_(0)
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ ++InvocationCount_;
+ if (Bounded_ ) {
+ EXPECT_TRUE(Parent_.Lock());
+ }
+ TInvokerWrapper::Invoke(std::move(callback));
+ }
+
+ void Bound(const IMockInvokerPoolPtr& parent)
+ {
+ Bounded_ = true;
+ Parent_ = MakeWeak(parent);
+ }
+
+ DEFINE_BYVAL_RO_PROPERTY(int, InvocationCount);
+
+private:
+ bool Bounded_ = false;
+ TWeakPtr<IMockInvokerPool> Parent_;
+};
+
+using TMockInvokerPtr = TIntrusivePtr<TMockInvoker>;
+
+TMockInvokerPtr CreateMockInvoker(IInvokerPtr underlyingInvoker)
+{
+ return New<TMockInvoker>(std::move(underlyingInvoker));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTransformInvokerPoolTest
+ : public ::testing::Test
+{
+protected:
+ const TActionQueuePtr Queue_ = New<TActionQueue>();
+
+ void TearDown() override
+ {
+ Queue_->Shutdown();
+ }
+
+ template <class TInvokerPoolPtr, class TGetCallbackCount>
+ static void CallPerInvoker(
+ const TInvokerPoolPtr& invokerPool,
+ const TGetCallbackCount& getCallbackCount)
+ {
+ for (int i = 0, size = invokerPool->GetSize(); i < size; ++i) {
+ const auto callbackCount = getCallbackCount(i);
+
+ for (int j = 0; j < callbackCount; ++j) {
+ invokerPool->GetInvoker(i)->Invoke(BIND([] { }));
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTransformInvokerPoolTest, Simple)
+{
+ static constexpr auto InvokerCount = 5;
+
+ auto inputInvokerPool = CreateDummyInvokerPool(Queue_->GetInvoker(), InvokerCount);
+ auto mockInvokerPool = TransformInvokerPool(std::move(inputInvokerPool), CreateMockInvoker);
+ auto outputInvokerPool = TransformInvokerPool(mockInvokerPool, CreateSuspendableInvoker);
+
+ EXPECT_EQ(InvokerCount, outputInvokerPool->GetSize());
+
+ const auto getCallbackCount = [] (int invokerIndex) {
+ return 2 * (invokerIndex + 1);
+ };
+
+ CallPerInvoker(outputInvokerPool, getCallbackCount);
+
+ for (int i = 0; i < InvokerCount; ++i) {
+ EXPECT_EQ(getCallbackCount(i), mockInvokerPool->GetInvoker(i)->GetInvocationCount());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTransformInvokerPoolTest, OutputPoolOutlivesInputPool)
+{
+ // NB! We do not use std::move in that test intentionally.
+
+ static constexpr auto InvokerCount = 10;
+
+ auto inputInvokerPool = CreateDummyInvokerPool(Queue_->GetInvoker(), InvokerCount);
+ auto mockInvokerPool = TransformInvokerPool(inputInvokerPool, CreateMockInvoker);
+
+ // Bound invokers and invoker pool so destruction of the pool breaks invokers functionality.
+ for (int i = 0; i < InvokerCount; ++i) {
+ mockInvokerPool->GetInvoker(i)->Bound(mockInvokerPool);
+ }
+
+ auto outputInvokerPool = TransformInvokerPool(mockInvokerPool, CreateIdenticalInvoker);
+
+ // Manually destroy underlying invoker pools.
+ inputInvokerPool.Reset();
+ mockInvokerPool.Reset();
+
+ // Check that output invoker pool still works correctly.
+ CallPerInvoker(
+ outputInvokerPool,
+ /*getCallbackCount*/ [] (int /*invokerIndex*/) {
+ return 1;
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTransformInvokerPoolTest, InstantiateForWellKnownTypes)
+{
+ const auto testOne = [this] (auto invokerFunctor) {
+ auto inputInvokerPool = CreateDummyInvokerPool(Queue_->GetInvoker(), /* invokerCount */ 10);
+ return TransformInvokerPool(std::move(inputInvokerPool), invokerFunctor);
+ };
+
+ IInvokerPoolPtr x = testOne(CreateIdenticalInvoker);
+ ISuspendableInvokerPoolPtr y = testOne(CreateSuspendableInvoker);
+ IPrioritizedInvokerPoolPtr z = testOne(CreatePrioritizedInvokerTest);
+ IInvokerPoolPtr w = testOne(CreateIdenticalInvokerByConstReference);
+
+ const auto testPair = [this] (auto firstInvokerFunctor, auto secondInvokerFunctor) {
+ auto inputInvokerPool = CreateDummyInvokerPool(Queue_->GetInvoker(), /* invokerCount */ 10);
+ auto intermediateInvokerPool = TransformInvokerPool(std::move(inputInvokerPool), firstInvokerFunctor);
+ auto outputInvokerPool = TransformInvokerPool(std::move(intermediateInvokerPool), secondInvokerFunctor);
+ };
+
+ testPair(CreateIdenticalInvoker, CreateIdenticalInvoker);
+ testPair(CreateIdenticalInvoker, CreateSuspendableInvoker);
+ testPair(CreateIdenticalInvoker, CreatePrioritizedInvokerTest);
+
+ testPair(CreateSuspendableInvoker, CreateIdenticalInvoker);
+ testPair(CreateSuspendableInvoker, CreateSuspendableInvoker);
+ testPair(CreateSuspendableInvoker, CreatePrioritizedInvokerTest);
+
+ testPair(CreatePrioritizedInvokerTest, CreateIdenticalInvoker);
+ testPair(CreatePrioritizedInvokerTest, CreateSuspendableInvoker);
+ testPair(CreatePrioritizedInvokerTest, CreatePrioritizedInvokerTest);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTransformInvokerPoolTest, Chaining)
+{
+ static constexpr auto InvokerCount = 10;
+
+ // NB! Intentionally do not use 'auto' here.
+ IMockInvokerPoolPtr invokerPool = TransformInvokerPool(TransformInvokerPool(TransformInvokerPool(
+ CreateDummyInvokerPool(Queue_->GetInvoker(), InvokerCount),
+ CreateSuspendableInvoker),
+ CreatePrioritizedInvokerTest),
+ CreateMockInvoker);
+
+ const auto getCallbackCount = [] (int invokerIndex) {
+ return 2 * (invokerIndex + 1);
+ };
+
+ CallPerInvoker(invokerPool, getCallbackCount);
+
+ for (int i = 0; i < InvokerCount; ++i) {
+ EXPECT_EQ(getCallbackCount(i), invokerPool->GetInvoker(i)->GetInvocationCount());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTransformInvokerPoolTest, ReturnTypeConvertability)
+{
+ auto invokerPool = CreateDummyInvokerPool(Queue_->GetInvoker(), /* invokerCount */ 10);
+ auto suspendableInvokerPool = TransformInvokerPool(invokerPool, CreateSuspendableInvoker);
+ auto prioritizedInvokerPool = TransformInvokerPool(suspendableInvokerPool, CreatePrioritizedInvokerTest);
+
+ EXPECT_TRUE((std::is_convertible<decltype(invokerPool), IInvokerPoolPtr>::value));
+ EXPECT_FALSE((std::is_convertible<decltype(invokerPool), ISuspendableInvokerPoolPtr>::value));
+ EXPECT_FALSE((std::is_convertible<decltype(invokerPool), IPrioritizedInvokerPoolPtr>::value));
+
+ EXPECT_FALSE((std::is_convertible<decltype(suspendableInvokerPool), IInvokerPoolPtr>::value));
+ EXPECT_TRUE((std::is_convertible<decltype(suspendableInvokerPool), ISuspendableInvokerPoolPtr>::value));
+ EXPECT_FALSE((std::is_convertible<decltype(suspendableInvokerPool), IPrioritizedInvokerPoolPtr>::value));
+
+ EXPECT_FALSE((std::is_convertible<decltype(prioritizedInvokerPool), IInvokerPoolPtr>::value));
+ EXPECT_FALSE((std::is_convertible<decltype(prioritizedInvokerPool), ISuspendableInvokerPoolPtr>::value));
+ EXPECT_TRUE((std::is_convertible<decltype(prioritizedInvokerPool), IPrioritizedInvokerPoolPtr>::value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EInvokerIndex,
+ (ZeroInvoker)
+ (FirstInvoker)
+ (SecondInvoker)
+);
+
+TEST(TInvokerPoolTest, IndexByEnum)
+{
+ auto queue = New<TActionQueue>();
+
+ auto invokerPool = CreateDummyInvokerPool(queue->GetInvoker(), /* invokerCount */ 3);
+
+ EXPECT_EQ(invokerPool->GetInvoker(0).Get(), invokerPool->GetInvoker(EInvokerIndex::ZeroInvoker).Get());
+ EXPECT_EQ(invokerPool->GetInvoker(1).Get(), invokerPool->GetInvoker(EInvokerIndex::FirstInvoker).Get());
+ EXPECT_EQ(invokerPool->GetInvoker(2).Get(), invokerPool->GetInvoker(EInvokerIndex::SecondInvoker).Get());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TPrioritizedInvokerTest, SamePriorityOrder)
+{
+ auto actionQueue = New<TActionQueue>();
+ auto prioritizedInvoker = CreatePrioritizedInvoker(actionQueue->GetInvoker());
+
+ int actualOrder = 0;
+ TCountDownLatch latch(100);
+
+ for (int index = 0; index < 100; ++index) {
+ prioritizedInvoker->Invoke(BIND([&, expected = index] {
+ EXPECT_EQ(expected, actualOrder++);
+ Sleep(TDuration::MilliSeconds(1));
+ latch.CountDown();
+ }), 0);
+ }
+
+ latch.Wait();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/nonblocking_batch_ut.cpp b/yt/yt/core/concurrency/unittests/nonblocking_batch_ut.cpp
new file mode 100644
index 0000000000..c498eca18f
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/nonblocking_batch_ut.cpp
@@ -0,0 +1,163 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/nonblocking_batch.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto Quantum = TDuration::Seconds(1);
+
+template <class T>
+void EnqueueAll(
+ const TNonblockingBatchPtr<int>& batch,
+ std::initializer_list<T> list)
+{
+ for (auto&& element : list) {
+ batch->Enqueue(std::move(element));
+ }
+}
+
+TEST(TNonblockingBatchTest, Simple)
+{
+ auto b = New<TNonblockingBatch<int>>(3, TDuration::Max());
+ b->Enqueue(1);
+ auto e1 = b->DequeueBatch();
+ auto e2 = b->DequeueBatch();
+ ASSERT_FALSE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ b->Enqueue(2);
+ ASSERT_FALSE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ b->Enqueue(3);
+ ASSERT_TRUE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ ASSERT_EQ(e1.Get().ValueOrThrow(), std::vector<int>({1, 2, 3}));
+ b->Enqueue(10);
+ b->Enqueue(11);
+ ASSERT_FALSE(e2.IsSet());
+ b->Enqueue(12);
+ ASSERT_TRUE(e2.IsSet());
+ ASSERT_EQ(e2.Get().ValueOrThrow(), std::vector<int>({10, 11, 12}));
+ b->Enqueue(0);
+ b->Enqueue(1);
+ b->Enqueue(2);
+ auto e3 = b->DequeueBatch();
+ ASSERT_TRUE(e3.IsSet());
+ ASSERT_EQ(e3.Get().ValueOrThrow(), std::vector<int>({0, 1, 2}));
+}
+
+TEST(TNonblockingBatchTest, Duration)
+{
+ auto timeout = Quantum;
+ auto overTimeout = timeout * 2;
+
+ auto b = New<TNonblockingBatch<int>>(2, timeout);
+ auto e1 = b->DequeueBatch();
+ Sleep(overTimeout);
+ ASSERT_FALSE(e1.IsSet());
+ b->Enqueue(1);
+ auto e2 = b->DequeueBatch();
+ ASSERT_FALSE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ Sleep(overTimeout);
+ ASSERT_TRUE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ ASSERT_EQ(e1.Get().ValueOrThrow(), std::vector<int>{1});
+ b->Enqueue(2);
+ ASSERT_FALSE(e2.IsSet());
+ b->Enqueue(3);
+ ASSERT_TRUE(e2.IsSet());
+ ASSERT_EQ(e2.Get().ValueOrThrow(), std::vector<int>({2, 3}));
+}
+
+TEST(TNonblockingBatchTest, Dequeue)
+{
+ auto timeout = Quantum;
+ auto overTimeout = timeout * 2;
+
+ auto b = New<TNonblockingBatch<int>>(2, timeout);
+ EnqueueAll(b, {1, 2, 3, 4, 5});
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({1, 2}));
+ }
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({3, 4}));
+ }
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_FALSE(e.IsSet());
+ Sleep(overTimeout);
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({5}));
+ }
+ EnqueueAll(b, {6, 7, 8});
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({6, 7}));
+ }
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_FALSE(e.IsSet());
+ EnqueueAll(b, {9, 10, 11});
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({8, 9}));
+ }
+ {
+ auto e = b->DequeueBatch();
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({10, 11}));
+ }
+}
+
+TEST(TNonblockingBatchTest, Drop)
+{
+ auto timeout = Quantum;
+
+ auto b = New<TNonblockingBatch<int>>(2, timeout);
+ auto e1 = b->DequeueBatch();
+ auto e2 = b->DequeueBatch();
+ ASSERT_FALSE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ EnqueueAll(b, {1, 2, 3});
+ ASSERT_TRUE(e1.IsSet());
+ ASSERT_FALSE(e2.IsSet());
+ b->Drop();
+ ASSERT_EQ(e1.Get().ValueOrThrow(), std::vector<int>({1, 2}));
+ ASSERT_TRUE(e2.IsSet());
+ ASSERT_EQ(e2.Get().ValueOrThrow(), std::vector<int>());
+ b->Enqueue(10);
+ auto e3 = b->DequeueBatch();
+ ASSERT_FALSE(e3.IsSet());
+ b->Drop();
+ ASSERT_TRUE(e3.IsSet());
+ ASSERT_EQ(e3.Get().ValueOrThrow(), std::vector<int>());
+}
+
+TEST(TNonblockingBatchTest, EnqueueTimeout)
+{
+ auto timeout = Quantum;
+ auto overTimeout = timeout * 2;
+
+ auto b = New<TNonblockingBatch<int>>(3, timeout);
+ Sleep(overTimeout);
+ {
+ auto e = b->DequeueBatch();
+ b->Enqueue(1);
+ ASSERT_FALSE(e.IsSet());
+ Sleep(overTimeout);
+ ASSERT_TRUE(e.IsSet());
+ ASSERT_EQ(e.Get().ValueOrThrow(), std::vector<int>({1}));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/nonblocking_queue_ut.cpp b/yt/yt/core/concurrency/unittests/nonblocking_queue_ut.cpp
new file mode 100644
index 0000000000..20ca635dcf
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/nonblocking_queue_ut.cpp
@@ -0,0 +1,102 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/nonblocking_queue.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TNonblockingQueueTest, DequeueFirst)
+{
+ TNonblockingQueue<int> queue;
+ auto result1 = queue.Dequeue();
+ auto result2 = queue.Dequeue();
+
+ EXPECT_FALSE(result1.IsSet());
+ EXPECT_FALSE(result2.IsSet());
+
+ queue.Enqueue(1);
+
+ EXPECT_TRUE(result1.IsSet());
+ EXPECT_EQ(1, result1.Get().Value());
+
+ queue.Enqueue(2);
+
+ EXPECT_TRUE(result2.IsSet());
+ EXPECT_EQ(2, result2.Get().Value());
+}
+
+TEST(TNonblockingQueueTest, EnqueueFirst)
+{
+ TNonblockingQueue<int> queue;
+ queue.Enqueue(1);
+ queue.Enqueue(2);
+
+ auto result1 = queue.Dequeue();
+ EXPECT_TRUE(result1.IsSet());
+ EXPECT_EQ(1, result1.Get().Value());
+
+ auto result2 = queue.Dequeue();
+ EXPECT_TRUE(result2.IsSet());
+ EXPECT_EQ(2, result2.Get().Value());
+}
+
+TEST(TNonblockingQueueTest, Mixed)
+{
+ TNonblockingQueue<int> queue;
+ queue.Enqueue(1);
+
+ auto result1 = queue.Dequeue();
+ EXPECT_TRUE(result1.IsSet());
+ EXPECT_EQ(1, result1.Get().Value());
+
+ auto result2 = queue.Dequeue();
+ EXPECT_FALSE(result2.IsSet());
+
+ queue.Enqueue(2);
+ EXPECT_TRUE(result2.IsSet());
+ EXPECT_EQ(2, result2.Get().Value());
+}
+
+TEST(TNonblockingQueueTest, DequeueFirstAsync)
+{
+ TNonblockingQueue<int> queue;
+ auto result = queue.Dequeue();
+ EXPECT_FALSE(result.IsSet());
+
+ auto promise = NewPromise<int>();
+ queue.Enqueue(promise.ToFuture());
+ EXPECT_FALSE(result.IsSet());
+
+ promise.Set(1);
+ EXPECT_TRUE(result.IsSet());
+ EXPECT_EQ(result.Get().Value(), 1);
+}
+
+TEST(TNonblockingQueueTest, EnqueueFirstAsync)
+{
+ TNonblockingQueue<int> queue;
+ auto promise1 = NewPromise<int>();
+ auto promise2 = NewPromise<int>();
+ queue.Enqueue(promise1.ToFuture());
+ queue.Enqueue(promise2.ToFuture());
+
+ auto result1 = queue.Dequeue();
+ EXPECT_FALSE(result1.IsSet());
+
+ promise1.Set(1);
+ EXPECT_TRUE(result1.IsSet());
+ EXPECT_EQ(result1.Get().Value(), 1);
+
+ promise2.Set(2);
+ auto result2 = queue.Dequeue();
+ EXPECT_TRUE(result2.IsSet());
+ EXPECT_EQ(result2.Get().Value(), 2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/periodic_ut.cpp b/yt/yt/core/concurrency/unittests/periodic_ut.cpp
new file mode 100644
index 0000000000..8578e75cac
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/periodic_ut.cpp
@@ -0,0 +1,229 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <exception>
+#include <atomic>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPeriodicTest
+ : public ::testing::Test
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_W(TPeriodicTest, Simple)
+{
+ std::atomic<int> count = {0};
+ auto callback = BIND([&] () {
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(200));
+ ++count;
+ });
+
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(100));
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(600));
+ WaitFor(executor->Stop())
+ .ThrowOnError();
+ EXPECT_EQ(2, count.load());
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(600));
+ WaitFor(executor->Stop())
+ .ThrowOnError();
+ EXPECT_EQ(4, count.load());
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(250));
+ WaitFor(executor->GetExecutedEvent())
+ .ThrowOnError();
+ EXPECT_EQ(6, count.load());
+ WaitFor(executor->Stop())
+ .ThrowOnError();
+}
+
+TEST_W(TPeriodicTest, ParallelStop)
+{
+ std::atomic<int> count = {0};
+ auto callback = BIND([&] () {
+ ++count;
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(500));
+ ++count;
+ });
+
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(10));
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ {
+ auto future1 = executor->Stop();
+ auto future2 = executor->Stop();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(1, count.load());
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ {
+ auto future1 = executor->Stop();
+ auto future2 = executor->Stop();
+ auto future3 = executor->Stop();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2, future3})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(2, count.load());
+}
+
+TEST_W(TPeriodicTest, ParallelOnExecuted1)
+{
+ std::atomic<int> count = 0;
+
+ auto callback = BIND([&] () {
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(500));
+ ++count;
+ });
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(10));
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ {
+ auto future1 = executor->GetExecutedEvent();
+ auto future2 = executor->GetExecutedEvent();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(2, count.load());
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(450));
+ {
+ auto future1 = executor->GetExecutedEvent();
+ auto future2 = executor->GetExecutedEvent();
+ auto future3 = executor->GetExecutedEvent();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2, future3})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(4, count.load());
+}
+
+TEST_W(TPeriodicTest, ParallelOnExecuted2)
+{
+ std::atomic<int> count = 0;
+
+ auto callback = BIND([&] () {
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+ ++count;
+ });
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(400));
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ {
+ auto future1 = executor->GetExecutedEvent();
+ auto future2 = executor->GetExecutedEvent();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(2, count.load());
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+ {
+ auto future1 = executor->GetExecutedEvent();
+ auto future2 = executor->GetExecutedEvent();
+ auto future3 = executor->GetExecutedEvent();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2, future3})))
+ .ThrowOnError();
+ }
+ EXPECT_EQ(3, count.load());
+}
+
+TEST_W(TPeriodicTest, OnExecutedEventCanceled)
+{
+ std::atomic<int> count = 0;
+
+ auto callback = BIND([&] () {
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+ ++count;
+ });
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(400));
+
+ executor->Start();
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ {
+ auto future1 = executor->GetExecutedEvent();
+ auto future2 = executor->GetExecutedEvent();
+
+ // Cancellation of the executed event future must not propagate to the underlying event.
+ auto future3 = executor->GetExecutedEvent();
+ future3.Cancel(TError(NYT::EErrorCode::Canceled, "Canceled"));
+
+ EXPECT_NO_THROW(WaitFor(AllSucceeded(std::vector<TFuture<void>>({future1, future2})))
+ .ThrowOnError());
+ }
+ EXPECT_EQ(2, count.load());
+}
+
+TEST_W(TPeriodicTest, Stop)
+{
+ auto neverSetPromise = NewPromise<void>();
+ auto immediatelyCancelableFuture = neverSetPromise.ToFuture().ToImmediatelyCancelable();
+ auto callback = BIND([&] {
+ WaitUntilSet(immediatelyCancelableFuture);
+ });
+ auto actionQueue = New<TActionQueue>();
+ auto executor = New<TPeriodicExecutor>(
+ actionQueue->GetInvoker(),
+ callback,
+ TDuration::MilliSeconds(100));
+
+ executor->Start();
+ // Wait for the callback to enter WaitFor.
+ Sleep(TDuration::MilliSeconds(100));
+ WaitFor(executor->Stop())
+ .ThrowOnError();
+
+ EXPECT_TRUE(immediatelyCancelableFuture.IsSet());
+ EXPECT_EQ(NYT::EErrorCode::Canceled, immediatelyCancelableFuture.Get().GetCode());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/propagating_storage_ut.cpp b/yt/yt/core/concurrency/unittests/propagating_storage_ut.cpp
new file mode 100644
index 0000000000..8202dd0619
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/propagating_storage_ut.cpp
@@ -0,0 +1,156 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/propagating_storage.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFirst
+{
+ TString Value;
+};
+
+struct TSecond
+{
+ TString Value;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TPropagatingStorageTest, Simple)
+{
+ auto actionQueue = New<TActionQueue>();
+
+ auto& storage = GetCurrentPropagatingStorage();
+ storage.Exchange<TFirst>({"hello"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+
+ WaitFor(
+ BIND([actionQueue] {
+ auto& storage = GetCurrentPropagatingStorage();
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ storage.Exchange<TFirst>({"inner"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "inner");
+ storage.Exchange<TSecond>({"some"});
+
+ WaitFor(
+ BIND([] {
+ auto& storage = GetCurrentPropagatingStorage();
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "inner");
+ storage.Remove<TFirst>();
+ ASSERT_FALSE(storage.Has<TFirst>());
+ ASSERT_EQ(storage.Find<TFirst>(), nullptr);
+ })
+ .AsyncVia(actionQueue->GetInvoker())
+ .Run())
+ .ThrowOnError();
+
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "inner");
+ ASSERT_EQ(storage.GetOrCrash<TSecond>().Value, "some");
+ })
+ .AsyncVia(actionQueue->GetInvoker())
+ .Run())
+ .ThrowOnError();
+
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ ASSERT_FALSE(storage.Has<TSecond>());
+}
+
+TEST(TPropagatingStorageTest, Cow)
+{
+ auto actionQueue = New<TActionQueue>();
+
+ auto& storage = GetCurrentPropagatingStorage();
+ storage.Exchange<TFirst>({"hello"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < 10; ++i) {
+ futures.push_back(
+ BIND([] {
+ auto& storage = GetCurrentPropagatingStorage();
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ })
+ .AsyncVia(actionQueue->GetInvoker())
+ .Run());
+ }
+
+ futures.push_back(
+ BIND([] {
+ auto& storage = GetCurrentPropagatingStorage();
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ storage.Exchange<TFirst>({"goodbye"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "goodbye");
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "goodbye");
+ })
+ .AsyncVia(actionQueue->GetInvoker())
+ .Run());
+
+ WaitFor(AllSucceeded(std::move(futures)))
+ .ThrowOnError();
+}
+
+TEST(TPropagatingStorageTest, Null)
+{
+ TNullPropagatingStorageGuard guard;
+
+ auto &storage = GetCurrentPropagatingStorage();
+
+ ASSERT_TRUE(storage.IsNull());
+ ASSERT_TRUE(storage.IsEmpty());
+
+ storage.Exchange<TFirst>({"goodbye"});
+ ASSERT_FALSE(storage.IsNull());
+ ASSERT_FALSE(storage.IsEmpty());
+
+ storage.Remove<TFirst>();
+ ASSERT_FALSE(storage.IsNull());
+ ASSERT_TRUE(storage.IsEmpty());
+}
+
+TEST(TPropagatingStorageTest, PropagatingValue)
+{
+ auto& storage = GetCurrentPropagatingStorage();
+ storage.Exchange<TFirst>({"hello"});
+ storage.Exchange<TSecond>({"world"});
+
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ ASSERT_EQ(storage.GetOrCrash<TSecond>().Value, "world");
+
+ {
+ TPropagatingValueGuard<TFirst> guard({"next"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "next");
+ ASSERT_EQ(storage.GetOrCrash<TSecond>().Value, "world");
+ }
+
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ ASSERT_EQ(storage.GetOrCrash<TSecond>().Value, "world");
+
+ storage.Remove<TSecond>();
+
+ {
+ TPropagatingValueGuard<TSecond> guard({"earth"});
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ ASSERT_EQ(storage.GetOrCrash<TSecond>().Value, "earth");
+ }
+
+ ASSERT_EQ(storage.GetOrCrash<TFirst>().Value, "hello");
+ ASSERT_FALSE(storage.Has<TSecond>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/quantized_executor_ut.cpp b/yt/yt/core/concurrency/unittests/quantized_executor_ut.cpp
new file mode 100644
index 0000000000..3f7c7df097
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/quantized_executor_ut.cpp
@@ -0,0 +1,248 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/quantized_executor.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <limits>
+#include <random>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleCallbackProvider
+ : public ICallbackProvider
+{
+public:
+ explicit TSimpleCallbackProvider(i64 iterations)
+ : Iterations_(iterations)
+ { }
+
+ TCallback<void()> ExtractCallback() override
+ {
+ if (IterationIndex_ < Iterations_) {
+ ++IterationIndex_;
+ return BIND([this, this_ = MakeStrong(this)] {
+ Sleep(TDuration::MilliSeconds(5));
+ ++Counter_;
+ });
+ } else {
+ return {};
+ }
+ }
+
+ i64 GetCounter() const
+ {
+ return Counter_;
+ }
+
+private:
+ const i64 Iterations_;
+ i64 IterationIndex_ = 0;
+
+ std::atomic<i64> Counter_ = 0;
+};
+
+class TInitializingCallbackProvider
+ : public ICallbackProvider
+{
+public:
+ TCallback<void()> GetInitializer()
+ {
+ return BIND([this, this_ = MakeStrong(this)] {
+ Initialized_ = true;
+ });
+ }
+
+ TCallback<void()> ExtractCallback() override
+ {
+ if (Finished_) {
+ return {};
+ } else {
+ return BIND([this, this_ = MakeStrong(this)] {
+ Finished_ = true;
+ });
+ }
+ }
+
+ bool IsInitialized() const
+ {
+ return Initialized_;
+ }
+
+ bool IsFinished() const
+ {
+ return Finished_;
+ }
+
+private:
+ bool Initialized_ = false;
+ bool Finished_ = false;
+};
+
+class TLongCallbackProvider
+ : public ICallbackProvider
+{
+public:
+ explicit TLongCallbackProvider(i64 iterations)
+ : Iterations_(iterations)
+ { }
+
+ TCallback<void()> ExtractCallback() override
+ {
+ if (!Extracted_) {
+ Extracted_ = true;
+ return BIND([this, this_ = MakeStrong(this)] {
+ for (int iteration = 0; iteration < Iterations_; ++iteration) {
+ Sleep(TDuration::MilliSeconds(10));
+ Yield();
+ }
+
+ Completed_ = true;
+ });
+ } else {
+ return {};
+ }
+ }
+
+ bool IsCompleted() const
+ {
+ return Completed_;
+ }
+
+private:
+ const i64 Iterations_;
+ bool Extracted_ = false;
+
+ std::atomic<bool> Completed_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TQuantizedExecutorTest
+ : public ::testing::Test
+{
+protected:
+ TIntrusivePtr<TSimpleCallbackProvider> SimpleCallbackProvider_;
+ TIntrusivePtr<TLongCallbackProvider> LongCallbackProvider_;
+ IQuantizedExecutorPtr Executor_;
+
+ void InitSimple(int workerCount, i64 iterationCount)
+ {
+ SimpleCallbackProvider_ = New<TSimpleCallbackProvider>(iterationCount);
+ Executor_ = CreateQuantizedExecutor("test", SimpleCallbackProvider_, workerCount);
+ Executor_->Initialize();
+ }
+
+ void InitLong(int workerCount, i64 iterationCount)
+ {
+ LongCallbackProvider_ = New<TLongCallbackProvider>(iterationCount);
+ Executor_ = CreateQuantizedExecutor("test", LongCallbackProvider_, workerCount);
+ Executor_->Initialize();
+ }
+};
+
+TEST_F(TQuantizedExecutorTest, Simple)
+{
+ InitSimple(/*workerCount*/ 1, /*iterationCount*/ 100);
+
+ WaitFor(Executor_->Run(TDuration::Max()))
+ .ThrowOnError();
+
+ EXPECT_EQ(SimpleCallbackProvider_->GetCounter(), 100);
+}
+
+TEST_F(TQuantizedExecutorTest, Timeout)
+{
+ InitSimple(/*workerCount*/ 4, /*iterationCount*/ std::numeric_limits<i64>::max());
+
+ for (int index = 1; index <= 10; ++index) {
+ WaitFor(Executor_->Run(TDuration::MilliSeconds(100)))
+ .ThrowOnError();
+
+ auto counter = SimpleCallbackProvider_->GetCounter();
+ EXPECT_GE(counter, /*workerCount*/ 4 * /*milliseconds*/ 100.0 / /*period*/ 5 * index * 0.75);
+ EXPECT_LE(counter, /*workerCount*/ 4 * /*milliseconds*/ 100.0 / /*period*/ 5 * index * 1.25);
+ }
+}
+
+TEST_F(TQuantizedExecutorTest, LongCallback1)
+{
+ InitLong(/*workerCount*/ 4, /*iterationCount*/ 20);
+
+ auto future = Executor_->Run(TDuration::MilliSeconds(500));
+
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(300));
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(LongCallbackProvider_->IsCompleted());
+}
+
+TEST_F(TQuantizedExecutorTest, LongCallback2)
+{
+ InitLong(/*workerCount*/ 4, /*iterationCount*/ 100);
+
+ for (int index = 0; index < 9; ++index) {
+ WaitFor(Executor_->Run(TDuration::MilliSeconds(100)))
+ .ThrowOnError();
+ EXPECT_FALSE(LongCallbackProvider_->IsCompleted());
+ }
+
+ WaitFor(Executor_->Run(TDuration::MilliSeconds(300)))
+ .ThrowOnError();
+ EXPECT_TRUE(LongCallbackProvider_->IsCompleted());
+}
+
+TEST_F(TQuantizedExecutorTest, Reconfigure)
+{
+ InitSimple(/*workerCount*/ 10, /*iterationCount*/ std::numeric_limits<i64>::max());
+
+ i64 lastCounter = 0;
+
+ auto run = [&] (int workerCount) {
+ Executor_->Reconfigure(workerCount);
+
+ WaitFor(Executor_->Run(TDuration::MilliSeconds(100)))
+ .ThrowOnError();
+
+ auto counter = SimpleCallbackProvider_->GetCounter();
+ auto result = counter - lastCounter;
+ lastCounter = counter;
+
+ return result;
+ };
+
+ std::mt19937 rng(42);
+
+ for (int index = 0; index < 10; ++index) {
+ auto workerCount = rng() % 5 + 1;
+ auto increment = run(workerCount);
+
+ EXPECT_GE(increment, workerCount * /*milliseconds*/ 100.0 / /*period*/ 5 * 0.75);
+ EXPECT_LE(increment, workerCount * /*milliseconds*/ 100.0 / /*period*/ 5 * 1.25);
+ }
+}
+
+TEST_F(TQuantizedExecutorTest, WorkerInitializer)
+{
+ auto callbackProvider = New<TInitializingCallbackProvider>();
+ EXPECT_FALSE(callbackProvider->IsInitialized());
+ EXPECT_FALSE(callbackProvider->IsFinished());
+
+ Executor_ = CreateQuantizedExecutor("test", callbackProvider, /*workerCount*/ 1);
+ Executor_->Initialize(callbackProvider->GetInitializer());
+
+ EXPECT_TRUE(callbackProvider->IsInitialized());
+ EXPECT_FALSE(callbackProvider->IsFinished());
+
+ WaitFor(Executor_->Run(TDuration::MilliSeconds(300)))
+ .ThrowOnError();
+
+ EXPECT_TRUE(callbackProvider->IsInitialized());
+ EXPECT_TRUE(callbackProvider->IsFinished());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/scheduler_ut.cpp b/yt/yt/core/concurrency/unittests/scheduler_ut.cpp
new file mode 100644
index 0000000000..f5d0b69d3f
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/scheduler_ut.cpp
@@ -0,0 +1,1637 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+#include <yt/yt/core/actions/current_invoker.h>
+// TODO(lukyan): Move invoker_detail to concurrency? Merge concurrency and actions?
+#include <yt/yt/core/actions/invoker_detail.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/fair_share_thread_pool.h>
+#include <yt/yt/core/concurrency/two_level_fair_share_thread_pool.h>
+#include <yt/yt/core/concurrency/new_fair_share_thread_pool.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/tracing/config.h>
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <library/cpp/yt/threading/count_down_latch.h>
+
+#include <util/system/compiler.h>
+#include <util/system/thread.h>
+#include <util/system/type_name.h>
+
+#include <exception>
+
+namespace NYT::NConcurrency {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NTracing;
+
+using ::testing::ContainsRegex;
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr auto SleepQuantum = TDuration::MilliSeconds(100);
+
+inline const NLogging::TLogger Logger("SchedulerTest");
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFuture<T> MakeDelayedFuture(T x)
+{
+ return TDelayedExecutor::MakeDelayed(SleepQuantum)
+ .Apply(BIND([=] () { return x; }));
+}
+
+class TSchedulerTest
+ : public ::testing::Test
+{
+protected:
+ TLazyIntrusivePtr<TActionQueue> Queue1;
+ TLazyIntrusivePtr<TActionQueue> Queue2;
+
+ void TearDown() override
+ {
+ if (Queue1.HasValue()) {
+ Queue1->Shutdown();
+ }
+ if (Queue2.HasValue()) {
+ Queue2->Shutdown();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+int RecursiveFunction(size_t maxDepth, size_t currentDepth)
+{
+ if (currentDepth >= maxDepth) {
+ return 0;
+ }
+
+// It seems that aarch64 requires more stack to throw an exception.
+#if defined(__aarch64__) || defined(__arm64__)
+ constexpr size_t requiredStackSpace = 60_KB;
+#else
+ constexpr size_t requiredStackSpace = 40_KB;
+#endif
+
+ if (!CheckFreeStackSpace(requiredStackSpace)) {
+ THROW_ERROR_EXCEPTION("Evaluation depth causes stack overflow");
+ }
+
+ std::array<int, 4 * 1024> array;
+
+ for (size_t i = 0; i < array.size(); ++i) {
+ array[i] = rand();
+ }
+
+ return std::accumulate(array.begin(), array.end(), 0)
+ + RecursiveFunction(maxDepth, currentDepth + 1);
+}
+
+// Fiber stack base address is unavailable on Windows (and always returns nullptr).
+#ifndef _win_
+TEST_W(TSchedulerTest, CheckFiberStack)
+{
+ auto asyncResult1 = BIND(&RecursiveFunction, 10, 0)
+ .AsyncVia(Queue1->GetInvoker())
+ .Run();
+
+ WaitFor(asyncResult1).ThrowOnError();
+
+#if defined(_asan_enabled_) || defined(_msan_enabled_)
+ constexpr size_t tooLargeDepth = 160;
+#else
+ constexpr size_t tooLargeDepth = 20;
+#endif
+
+ auto asyncResult2 = BIND(&RecursiveFunction, tooLargeDepth, 0)
+ .AsyncVia(Queue1->GetInvoker())
+ .Run();
+
+ EXPECT_THROW_THAT(
+ WaitFor(asyncResult2).ThrowOnError(),
+ ContainsRegex("Evaluation depth causes stack overflow"));
+}
+#endif
+
+TEST_W(TSchedulerTest, SimpleAsync)
+{
+ auto asyncA = MakeDelayedFuture(3);
+ int a = WaitFor(asyncA).ValueOrThrow();
+
+ auto asyncB = MakeDelayedFuture(4);
+ int b = WaitFor(asyncB).ValueOrThrow();
+
+ EXPECT_EQ(7, a + b);
+}
+
+#ifdef YT_ENABLE_THREAD_AFFINITY_CHECK
+
+TEST_W(TSchedulerTest, SwitchToInvoker1)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ auto id0 = GetCurrentThreadId();
+ auto id1 = invoker->GetThreadId();
+
+ EXPECT_NE(id0, id1);
+
+ for (int i = 0; i < 10; ++i) {
+ SwitchTo(invoker);
+ EXPECT_EQ(GetCurrentThreadId(), id1);
+ }
+}
+
+TEST_W(TSchedulerTest, SwitchToInvoker2)
+{
+ auto invoker1 = Queue1->GetInvoker();
+ auto invoker2 = Queue2->GetInvoker();
+
+ auto id0 = GetCurrentThreadId();
+ auto id1 = invoker1->GetThreadId();
+ auto id2 = invoker2->GetThreadId();
+
+ EXPECT_NE(id0, id1);
+ EXPECT_NE(id0, id2);
+ EXPECT_NE(id1, id2);
+
+ for (int i = 0; i < 10; ++i) {
+ SwitchTo(invoker1);
+ EXPECT_EQ(GetCurrentThreadId(), id1);
+
+ SwitchTo(invoker2);
+ EXPECT_EQ(GetCurrentThreadId(), id2);
+ }
+}
+
+#endif
+
+TEST_W(TSchedulerTest, SwitchToCancelableInvoker1)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(GetCurrentInvoker());
+
+ context->Cancel(TError("Error"));
+
+ EXPECT_THROW({ SwitchTo(invoker); }, TFiberCanceledException);
+}
+
+TEST_W(TSchedulerTest, SwitchToCancelableInvoker2)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker1 = context->CreateInvoker(Queue1->GetInvoker());
+ auto invoker2 = context->CreateInvoker(Queue2->GetInvoker());
+
+ EXPECT_NO_THROW({ SwitchTo(invoker1); });
+
+ context->Cancel(TError("Error"));
+
+ EXPECT_THROW({ SwitchTo(invoker2); }, TFiberCanceledException);
+}
+
+TEST_W(TSchedulerTest, SwitchToCancelableInvoker3)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker1 = context->CreateInvoker(Queue1->GetInvoker());
+ auto invoker2 = context->CreateInvoker(Queue2->GetInvoker());
+
+ EXPECT_NO_THROW({ SwitchTo(invoker1); });
+
+ EXPECT_NO_THROW({ SwitchTo(invoker2); });
+
+ EXPECT_NO_THROW({ SwitchTo(invoker1); });
+
+ context->Cancel(TError("Error"));
+
+ EXPECT_THROW({ SwitchTo(invoker2); }, TFiberCanceledException);
+}
+
+TEST_W(TSchedulerTest, WaitForCancelableInvoker1)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(Queue1->GetInvoker());
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ TDelayedExecutor::Submit(
+ BIND([=] () mutable {
+ context->Cancel(TError("Error"));
+ promise.Set();
+ }),
+ SleepQuantum);
+ WaitFor(BIND([=] () {
+ EXPECT_THROW({ WaitFor(future).ThrowOnError(); }, TFiberCanceledException);
+ })
+ .AsyncVia(invoker)
+ .Run()).ThrowOnError();
+}
+
+TEST_W(TSchedulerTest, WaitForCancelableInvoker2)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(Queue1->GetInvoker());
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ WaitFor(BIND([=] () mutable {
+ context->Cancel(TError("Error"));
+ promise.Set();
+ EXPECT_THROW({ WaitFor(future).ThrowOnError(); }, TFiberCanceledException);
+ })
+ .AsyncVia(invoker)
+ .Run()).ThrowOnError();
+}
+
+TEST_W(TSchedulerTest, TerminatedCaught)
+{
+ auto context = New<TCancelableContext>();
+
+ auto invoker1 = context->CreateInvoker(Queue1->GetInvoker());
+ auto invoker2 = Queue2->GetInvoker();
+
+ SwitchTo(invoker2);
+
+ context->Cancel(TError("Error"));
+
+ EXPECT_THROW({ SwitchTo(invoker1); }, TFiberCanceledException);
+}
+
+TEST_W(TSchedulerTest, TerminatedPropagated)
+{
+ TLazyIntrusivePtr<TActionQueue> queue3;
+
+ auto future = BIND([this] {
+ auto context = New<TCancelableContext>();
+
+ auto invoker1 = context->CreateInvoker(Queue1->GetInvoker());
+ auto invoker2 = Queue2->GetInvoker();
+
+ SwitchTo(invoker2);
+
+ context->Cancel(TError("Error"));
+
+ SwitchTo(invoker1);
+
+ YT_ABORT();
+ })
+ .AsyncVia(queue3->GetInvoker())
+ .Run();
+
+ EXPECT_THROW_WITH_ERROR_CODE(WaitFor(future).ThrowOnError(), NYT::EErrorCode::Canceled);
+}
+
+TEST_W(TSchedulerTest, CurrentInvokerAfterSwitch1)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ SwitchTo(invoker);
+
+ EXPECT_EQ(invoker, GetCurrentInvoker());
+}
+
+TEST_W(TSchedulerTest, CurrentInvokerAfterSwitch2)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(Queue1->GetInvoker());
+
+ SwitchTo(invoker);
+
+ EXPECT_EQ(invoker, GetCurrentInvoker());
+}
+
+#ifdef YT_ENABLE_THREAD_AFFINITY_CHECK
+
+TEST_W(TSchedulerTest, InvokerAffinity1)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ SwitchTo(invoker);
+
+ VERIFY_INVOKER_AFFINITY(invoker);
+}
+
+TEST_W(TSchedulerTest, InvokerAffinity2)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(Queue1->GetInvoker());
+
+ SwitchTo(invoker);
+
+ VERIFY_INVOKER_AFFINITY(invoker);
+ VERIFY_INVOKER_AFFINITY(Queue1->GetInvoker());
+}
+
+using TSchedulerDeathTest = TSchedulerTest;
+
+TEST_W(TSchedulerDeathTest, SerializedInvokerAffinity)
+{
+ auto actionQueueInvoker = Queue1->GetInvoker();
+
+ auto threadPool = CreateThreadPool(4, "TestThreads");
+ auto threadPoolInvoker = threadPool->GetInvoker();
+ auto serializedThreadPoolInvoker = CreateSerializedInvoker(threadPoolInvoker);
+
+ SwitchTo(actionQueueInvoker);
+
+ VERIFY_INVOKER_AFFINITY(actionQueueInvoker);
+ VERIFY_SERIALIZED_INVOKER_AFFINITY(actionQueueInvoker);
+
+ SwitchTo(serializedThreadPoolInvoker);
+
+ VERIFY_INVOKER_AFFINITY(serializedThreadPoolInvoker);
+ VERIFY_SERIALIZED_INVOKER_AFFINITY(serializedThreadPoolInvoker);
+
+ SwitchTo(threadPoolInvoker);
+
+ VERIFY_INVOKER_AFFINITY(threadPoolInvoker);
+ ASSERT_DEATH({ VERIFY_SERIALIZED_INVOKER_AFFINITY(threadPoolInvoker); }, ".*");
+}
+
+#endif
+
+TEST_F(TSchedulerTest, CurrentInvokerSync)
+{
+ auto currentInvokerPtr = GetCurrentInvoker();
+ const auto& currentInvoker = *currentInvokerPtr;
+ EXPECT_EQ(GetSyncInvoker(), currentInvokerPtr)
+ << "Current invoker: " << TypeName(currentInvoker);
+}
+
+TEST_F(TSchedulerTest, CurrentInvokerInActionQueue)
+{
+ auto invoker = Queue1->GetInvoker();
+ BIND([=] () {
+ EXPECT_EQ(invoker, GetCurrentInvoker());
+ })
+ .AsyncVia(invoker).Run()
+ .Get();
+}
+
+TEST_F(TSchedulerTest, Intercept)
+{
+ auto invoker = Queue1->GetInvoker();
+ int counter1 = 0;
+ int counter2 = 0;
+ BIND([&] () {
+ TContextSwitchGuard guard(
+ [&] {
+ EXPECT_EQ(counter1, 0);
+ EXPECT_EQ(counter2, 0);
+ ++counter1;
+ },
+ [&] {
+ EXPECT_EQ(counter1, 1);
+ EXPECT_EQ(counter2, 0);
+ ++counter2;
+ });
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ })
+ .AsyncVia(invoker).Run()
+ .Get();
+ EXPECT_EQ(counter1, 1);
+ EXPECT_EQ(counter2, 1);
+}
+
+TEST_F(TSchedulerTest, InterceptEnclosed)
+{
+ auto invoker = Queue1->GetInvoker();
+ int counter1 = 0;
+ int counter2 = 0;
+ int counter3 = 0;
+ int counter4 = 0;
+ BIND([&] () {
+ {
+ TContextSwitchGuard guard(
+ [&] { ++counter1; },
+ [&] { ++counter2; });
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ {
+ TContextSwitchGuard guard2(
+ [&] { ++counter3; },
+ [&] { ++counter4; });
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ }
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ }
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ })
+ .AsyncVia(invoker).Run()
+ .Get();
+ EXPECT_EQ(counter1, 3);
+ EXPECT_EQ(counter2, 3);
+ EXPECT_EQ(counter3, 1);
+ EXPECT_EQ(counter4, 1);
+}
+
+TEST_F(TSchedulerTest, CurrentInvokerConcurrent)
+{
+ auto invoker1 = Queue1->GetInvoker();
+ auto invoker2 = Queue2->GetInvoker();
+
+ auto result1 = BIND([=] () {
+ EXPECT_EQ(invoker1, GetCurrentInvoker());
+ }).AsyncVia(invoker1).Run();
+
+ auto result2 = BIND([=] () {
+ EXPECT_EQ(invoker2, GetCurrentInvoker());
+ }).AsyncVia(invoker2).Run();
+
+ result1.Get();
+ result2.Get();
+}
+
+TEST_W(TSchedulerTest, WaitForAsyncVia)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ auto x = BIND([&] () { }).AsyncVia(invoker).Run();
+
+ WaitFor(x)
+ .ThrowOnError();
+}
+
+// Various invokers.
+
+TEST_F(TSchedulerTest, WaitForInSerializedInvoker1)
+{
+ auto invoker = CreateSerializedInvoker(Queue1->GetInvoker());
+ BIND([&] () {
+ for (int i = 0; i < 10; ++i) {
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ }
+ }).AsyncVia(invoker).Run().Get().ThrowOnError();
+}
+
+TEST_F(TSchedulerTest, WaitForInSerializedInvoker2)
+{
+ // NB! This may be confusing, but serialized invoker is expected to start
+ // executing next action if current action is blocked on WaitFor.
+
+ auto invoker = CreateSerializedInvoker(Queue1->GetInvoker());
+ std::vector<TFuture<void>> futures;
+
+ bool finishedFirstAction = false;
+ futures.emplace_back(BIND([&] () {
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ finishedFirstAction = true;
+ }).AsyncVia(invoker).Run());
+
+ futures.emplace_back(BIND([&] () {
+ if (finishedFirstAction) {
+ THROW_ERROR_EXCEPTION("Serialization error");
+ }
+ }).AsyncVia(invoker).Run());
+
+ AllSucceeded(futures).Get().ThrowOnError();
+}
+
+TEST_F(TSchedulerTest, WaitForInBoundedConcurrencyInvoker1)
+{
+ auto invoker = CreateBoundedConcurrencyInvoker(Queue1->GetInvoker(), 1);
+ BIND([&] () {
+ for (int i = 0; i < 10; ++i) {
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+ }
+ }).AsyncVia(invoker).Run().Get().ThrowOnError();
+}
+
+TEST_F(TSchedulerTest, WaitForInBoundedConcurrencyInvoker2)
+{
+ auto invoker = CreateBoundedConcurrencyInvoker(Queue1->GetInvoker(), 2);
+
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+
+ auto a1 = BIND([&] () {
+ promise.Set();
+ });
+
+ auto a2 = BIND([&] () {
+ invoker->Invoke(a1);
+ WaitFor(future)
+ .ThrowOnError();
+ });
+
+ a2.AsyncVia(invoker).Run().Get().ThrowOnError();
+}
+
+TEST_F(TSchedulerTest, WaitForInBoundedConcurrencyInvoker3)
+{
+ auto invoker = CreateBoundedConcurrencyInvoker(Queue1->GetInvoker(), 1);
+
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+
+ bool a1called = false;
+ bool a1finished = false;
+ auto a1 = BIND([&] () {
+ a1called = true;
+ WaitFor(future)
+ .ThrowOnError();
+ a1finished = true;
+ });
+
+ bool a2called = false;
+ auto a2 = BIND([&] () {
+ a2called = true;
+ });
+
+ invoker->Invoke(a1);
+ invoker->Invoke(a2);
+
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(a1called);
+ EXPECT_FALSE(a1finished);
+ EXPECT_FALSE(a2called);
+
+ promise.Set();
+
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(a1called);
+ EXPECT_TRUE(a1finished);
+ EXPECT_TRUE(a2called);
+}
+
+TEST_F(TSchedulerTest, PropagateFiberCancelationToFuture)
+{
+ auto p1 = NewPromise<void>();
+ auto f1 = p1.ToFuture();
+
+ auto a = BIND([=] () mutable {
+ WaitUntilSet(f1);
+ });
+
+ auto f2 = a.AsyncVia(Queue1->GetInvoker()).Run();
+
+ Sleep(SleepQuantum);
+
+ f2.Cancel(TError("Error"));
+
+ Sleep(SleepQuantum);
+
+ EXPECT_TRUE(p1.IsCanceled());
+}
+
+TEST_F(TSchedulerTest, FiberUnwindOrder)
+{
+ auto p1 = NewPromise<void>();
+ // Add empty callback
+ p1.OnCanceled(BIND([] (const TError& /*error*/) { }));
+ auto f1 = p1.ToFuture();
+
+ auto f2 = BIND([=] () mutable {
+ auto finally = Finally([&] {
+ EXPECT_TRUE(f1.IsSet());
+ });
+
+ NYT::NConcurrency::GetCurrentFiberCanceler().Run(TError("Error"));
+
+ WaitUntilSet(f1);
+ }).AsyncVia(Queue1->GetInvoker()).Run();
+
+ Sleep(SleepQuantum);
+ EXPECT_FALSE(f2.IsSet());
+
+ p1.Set();
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(f2.IsSet());
+ EXPECT_FALSE(f2.Get().IsOK());
+}
+
+TEST_F(TSchedulerTest, TestWaitUntilSet)
+{
+ auto p1 = NewPromise<void>();
+ auto f1 = p1.ToFuture();
+
+ BIND([=] () {
+ Sleep(SleepQuantum);
+ p1.Set();
+ }).AsyncVia(Queue1->GetInvoker()).Run();
+
+ WaitUntilSet(f1);
+ EXPECT_TRUE(f1.IsSet());
+ EXPECT_TRUE(f1.Get().IsOK());
+}
+
+TEST_F(TSchedulerTest, AsyncViaCanceledBeforeStart)
+{
+ auto invoker = Queue1->GetInvoker();
+ auto asyncResult1 = BIND([] () {
+ Sleep(SleepQuantum * 10);
+ }).AsyncVia(invoker).Run();
+ auto asyncResult2 = BIND([] () {
+ Sleep(SleepQuantum * 10);
+ }).AsyncVia(invoker).Run();
+ EXPECT_FALSE(asyncResult1.IsSet());
+ EXPECT_FALSE(asyncResult2.IsSet());
+ asyncResult2.Cancel(TError("Error"));
+ EXPECT_TRUE(asyncResult1.Get().IsOK());
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(asyncResult2.IsSet());
+ EXPECT_EQ(NYT::EErrorCode::Canceled, asyncResult2.Get().GetCode());
+}
+
+TEST_F(TSchedulerTest, CancelCurrentFiber)
+{
+ auto invoker = Queue1->GetInvoker();
+ auto asyncResult = BIND([=] () {
+ NYT::NConcurrency::GetCurrentFiberCanceler().Run(TError("Error"));
+ SwitchTo(invoker);
+ }).AsyncVia(invoker).Run();
+ asyncResult.Get();
+ EXPECT_TRUE(asyncResult.IsSet());
+ EXPECT_EQ(NYT::EErrorCode::Canceled, asyncResult.Get().GetCode());
+}
+
+TEST_F(TSchedulerTest, YieldToFromCanceledFiber)
+{
+ auto promise = NewPromise<void>();
+ auto invoker1 = Queue1->GetInvoker();
+ auto invoker2 = Queue2->GetInvoker();
+
+ auto asyncResult = BIND([=] () mutable {
+ BIND([=] () {
+ NYT::NConcurrency::GetCurrentFiberCanceler().Run(TError("Error"));
+ }).AsyncVia(invoker2).Run().Get();
+ WaitFor(promise.ToFuture(), invoker2)
+ .ThrowOnError();
+ }).AsyncVia(invoker1).Run();
+
+ promise.Set();
+ asyncResult.Get();
+
+ EXPECT_TRUE(asyncResult.IsSet());
+ EXPECT_TRUE(asyncResult.Get().IsOK());
+}
+
+TEST_F(TSchedulerTest, JustYield1)
+{
+ auto invoker = Queue1->GetInvoker();
+ auto asyncResult = BIND([] () {
+ for (int i = 0; i < 10; ++i) {
+ Yield();
+ }
+ }).AsyncVia(invoker).Run().Get();
+ EXPECT_TRUE(asyncResult.IsOK());
+}
+
+TEST_F(TSchedulerTest, JustYield2)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ bool flag = false;
+
+ auto asyncResult = BIND([&] () {
+ for (int i = 0; i < 2; ++i) {
+ Sleep(SleepQuantum);
+ Yield();
+ }
+ flag = true;
+ }).AsyncVia(invoker).Run();
+
+ // This callback must complete before the first.
+ auto errorOrValue = BIND([&] () {
+ return flag;
+ }).AsyncVia(invoker).Run().Get();
+
+ EXPECT_TRUE(errorOrValue.IsOK());
+ EXPECT_FALSE(errorOrValue.Value());
+ EXPECT_TRUE(asyncResult.Get().IsOK());
+}
+
+TEST_F(TSchedulerTest, CancelInAdjacentCallback)
+{
+ auto invoker = Queue1->GetInvoker();
+ auto asyncResult1 = BIND([=] () {
+ NYT::NConcurrency::GetCurrentFiberCanceler().Run(TError("Error"));
+ }).AsyncVia(invoker).Run().Get();
+ auto asyncResult2 = BIND([=] () {
+ Yield();
+ }).AsyncVia(invoker).Run().Get();
+ EXPECT_TRUE(asyncResult1.IsOK());
+ EXPECT_TRUE(asyncResult2.IsOK());
+}
+
+TEST_F(TSchedulerTest, CancelInApply)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ BIND([=] () {
+ auto promise = NewPromise<void>();
+
+ promise.ToFuture().Apply(BIND([] {
+ auto canceler = NYT::NConcurrency::GetCurrentFiberCanceler();
+ canceler(TError("kek"));
+
+ auto p = NewPromise<void>();
+ WaitFor(p.ToFuture())
+ .ThrowOnError();
+ }));
+
+ promise.Set();
+
+ promise.ToFuture().Apply(BIND([] {
+ auto canceler = NYT::NConcurrency::GetCurrentFiberCanceler();
+ canceler(TError("kek"));
+
+ auto p = NewPromise<void>();
+ WaitFor(p.ToFuture())
+ .ThrowOnError();
+ }));
+ })
+ .AsyncVia(invoker)
+ .Run()
+ .Get();
+}
+
+TEST_F(TSchedulerTest, CancelInApplyUnique)
+{
+ auto invoker = Queue1->GetInvoker();
+
+ BIND([=] () {
+ auto promise = NewPromise<int>();
+
+ auto f2 = promise.ToFuture().ApplyUnique(BIND([] (TErrorOr<int>&& /* error */) {
+ auto canceler = NYT::NConcurrency::GetCurrentFiberCanceler();
+ canceler(TError("kek"));
+
+ auto p = NewPromise<void>();
+ WaitFor(p.ToFuture())
+ .ThrowOnError();
+ }));
+
+ promise.Set(42);
+ })
+ .AsyncVia(invoker)
+ .Run()
+ .Get();
+}
+
+TEST_F(TSchedulerTest, CancelInAdjacentThread)
+{
+ auto closure = TCallback<void(const TError&)>();
+ auto invoker = Queue1->GetInvoker();
+ auto asyncResult1 = BIND([=, &closure] () {
+ closure = NYT::NConcurrency::GetCurrentFiberCanceler();
+ }).AsyncVia(invoker).Run().Get();
+ closure.Run(TError("Error")); // *evil laugh*
+ auto asyncResult2 = BIND([=] () {
+ Yield();
+ }).AsyncVia(invoker).Run().Get();
+ closure.Reset(); // *evil smile*
+ EXPECT_TRUE(asyncResult1.IsOK());
+ EXPECT_TRUE(asyncResult2.IsOK());
+}
+
+TEST_F(TSchedulerTest, SerializedDoubleWaitFor)
+{
+ std::atomic<bool> flag(false);
+
+ auto threadPool = CreateThreadPool(3, "MyPool");
+ auto serializedInvoker = CreateSerializedInvoker(threadPool->GetInvoker());
+
+ auto promise = NewPromise<void>();
+
+ BIND([&] () {
+ WaitFor(VoidFuture)
+ .ThrowOnError();
+ WaitFor(VoidFuture)
+ .ThrowOnError();
+ promise.Set();
+
+ Sleep(SleepQuantum);
+ flag = true;
+ })
+ .Via(serializedInvoker)
+ .Run();
+
+ promise.ToFuture().Get();
+
+ auto result = BIND([&] () -> bool {
+ return flag;
+ })
+ .AsyncVia(serializedInvoker)
+ .Run()
+ .Get()
+ .ValueOrThrow();
+
+ EXPECT_TRUE(result);
+}
+
+void CheckCurrentFiberRunDuration(TDuration actual, TDuration lo, TDuration hi)
+{
+ EXPECT_LE(actual, hi);
+ EXPECT_GE(actual, lo);
+}
+
+TEST_W(TSchedulerTest, FiberTiming)
+{
+ NProfiling::TFiberWallTimer timer;
+
+ CheckCurrentFiberRunDuration(timer.GetElapsedTime(), TDuration::MilliSeconds(0), TDuration::MilliSeconds(100));
+ Sleep(TDuration::Seconds(1));
+ CheckCurrentFiberRunDuration(timer.GetElapsedTime(),TDuration::MilliSeconds(900), TDuration::MilliSeconds(1100));
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ CheckCurrentFiberRunDuration(timer.GetElapsedTime(),TDuration::MilliSeconds(900), TDuration::MilliSeconds(1100));
+}
+
+TEST_W(TSchedulerTest, CancelDelayedFuture)
+{
+ auto future = TDelayedExecutor::MakeDelayed(TDuration::Seconds(10));
+ future.Cancel(TError("Error"));
+ EXPECT_TRUE(future.IsSet());
+ auto error = future.Get();
+ EXPECT_EQ(NYT::EErrorCode::Canceled, error.GetCode());
+ EXPECT_EQ(1, std::ssize(error.InnerErrors()));
+ EXPECT_EQ(NYT::EErrorCode::Generic, error.InnerErrors()[0].GetCode());
+}
+
+class TVerifyingMemoryTagGuard
+{
+public:
+ explicit TVerifyingMemoryTagGuard(TMemoryTag tag)
+ : Tag_(tag)
+ , SavedTag_(GetCurrentMemoryTag())
+ {
+ SetCurrentMemoryTag(Tag_);
+ }
+
+ ~TVerifyingMemoryTagGuard()
+ {
+ auto tag = GetCurrentMemoryTag();
+ EXPECT_EQ(tag, Tag_);
+ SetCurrentMemoryTag(SavedTag_);
+ }
+
+ TVerifyingMemoryTagGuard(const TVerifyingMemoryTagGuard& other) = delete;
+ TVerifyingMemoryTagGuard(TVerifyingMemoryTagGuard&& other) = delete;
+
+private:
+ const TMemoryTag Tag_;
+ const TMemoryTag SavedTag_;
+};
+
+class TWrappingInvoker
+ : public TInvokerWrapper
+{
+public:
+ explicit TWrappingInvoker(IInvokerPtr underlyingInvoker)
+ : TInvokerWrapper(std::move(underlyingInvoker))
+ { }
+
+ void Invoke(TClosure callback) override
+ {
+ UnderlyingInvoker_->Invoke(BIND(
+ &TWrappingInvoker::RunCallback,
+ MakeStrong(this),
+ Passed(std::move(callback))));
+ }
+
+ void RunCallback(TClosure callback)
+ {
+ TCurrentInvokerGuard currentInvokerGuard(this);
+ DoRunCallback(callback);
+ }
+
+ void virtual DoRunCallback(TClosure callback) = 0;
+};
+
+class TVerifyingMemoryTaggingInvoker
+ : public TWrappingInvoker
+{
+public:
+ TVerifyingMemoryTaggingInvoker(IInvokerPtr invoker, TMemoryTag memoryTag)
+ : TWrappingInvoker(std::move(invoker))
+ , MemoryTag_(memoryTag)
+ { }
+
+private:
+ const TMemoryTag MemoryTag_;
+
+ void DoRunCallback(TClosure callback) override
+ {
+ TVerifyingMemoryTagGuard memoryTagGuard(MemoryTag_);
+ callback();
+ }
+};
+
+TEST_W(TSchedulerTest, MemoryTagAndResumer)
+{
+ auto actionQueue = New<TActionQueue>();
+
+ auto invoker1 = New<TVerifyingMemoryTaggingInvoker>(actionQueue->GetInvoker(), 1);
+ auto invoker2 = New<TVerifyingMemoryTaggingInvoker>(actionQueue->GetInvoker(), 2);
+
+ auto asyncResult = BIND([=] {
+ EXPECT_EQ(GetCurrentMemoryTag(), 1u);
+ SwitchTo(invoker2);
+ EXPECT_EQ(GetCurrentMemoryTag(), 1u);
+ })
+ .AsyncVia(invoker1)
+ .Run();
+
+ WaitFor(asyncResult)
+ .ThrowOnError();
+}
+
+void CheckTraceContextTime(const NTracing::TTraceContextPtr& traceContext, TDuration lo, TDuration hi)
+{
+ auto actual = traceContext->GetElapsedTime();
+ EXPECT_LE(actual, hi);
+ EXPECT_GE(actual, lo);
+}
+
+TEST_W(TSchedulerTest, TraceContextZeroTiming)
+{
+ auto traceContext = NTracing::TTraceContext::NewRoot("Test");
+
+ {
+ NTracing::TTraceContextGuard guard(traceContext);
+ Sleep(TDuration::Seconds(0));
+ }
+
+ CheckTraceContextTime(traceContext, TDuration::MilliSeconds(0), TDuration::MilliSeconds(100));
+}
+
+TEST_W(TSchedulerTest, TraceContextThreadSleepTiming)
+{
+ auto traceContext = NTracing::TTraceContext::NewRoot("Test");
+
+ {
+ NTracing::TTraceContextGuard guard(traceContext);
+ Sleep(TDuration::Seconds(1));
+ }
+
+ CheckTraceContextTime(traceContext, TDuration::MilliSeconds(900), TDuration::MilliSeconds(1100));
+}
+
+TEST_W(TSchedulerTest, TraceContextFiberSleepTiming)
+{
+ auto traceContext = NTracing::TTraceContext::NewRoot("Test");
+
+ {
+ NTracing::TTraceContextGuard guard(traceContext);
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ }
+
+ CheckTraceContextTime(traceContext, TDuration::MilliSeconds(0), TDuration::MilliSeconds(100));
+}
+
+TEST_W(TSchedulerTest, TraceContextTimingPropagationViaBind)
+{
+ auto traceContext = NTracing::TTraceContext::NewRoot("Test");
+ auto actionQueue = New<TActionQueue>();
+
+ {
+ NTracing::TTraceContextGuard guard(traceContext);
+ auto asyncResult = BIND([] {
+ Sleep(TDuration::MilliSeconds(700));
+ })
+ .AsyncVia(actionQueue->GetInvoker())
+ .Run();
+ Sleep(TDuration::MilliSeconds(300));
+ WaitFor(asyncResult)
+ .ThrowOnError();
+ }
+
+ CheckTraceContextTime(traceContext, TDuration::MilliSeconds(900), TDuration::MilliSeconds(1100));
+}
+
+TEST_W(TSchedulerTest, TraceBaggagePropagation)
+{
+ auto traceContext = TTraceContext::NewRoot("Test");
+ auto baggage = CreateEphemeralAttributes();
+ baggage->Set("myKey", "myValue");
+ baggage->Set("myKey2", "myValue2");
+ auto expected = ConvertToYsonString(baggage);
+ traceContext->PackBaggage(baggage);
+
+ auto childContext = traceContext->CreateChild("Child");
+ auto childBaggage = childContext->UnpackBaggage();
+ auto result = ConvertToYsonString(childBaggage);
+ childBaggage->Set("myKey3", "myValue3");
+ childContext->PackBaggage(std::move(childBaggage));
+
+ EXPECT_EQ(expected, result);
+}
+
+TEST_W(TSchedulerTest, TraceDisableSendBaggage)
+{
+ auto parentContext = TTraceContext::NewRoot("Test");
+ auto parentBaggage = CreateEphemeralAttributes();
+ parentBaggage->Set("myKey", "myValue");
+ parentBaggage->Set("myKey2", "myValue2");
+ parentContext->PackBaggage(parentBaggage);
+ auto parentBaggageString = ConvertToYsonString(parentBaggage);
+
+ auto originalConfig = GetTracingConfig();
+ auto guard = Finally([&] {
+ SetTracingConfig(originalConfig);
+ });
+
+ {
+ auto config = New<TTracingConfig>();
+ config->SendBaggage = true;
+ SetTracingConfig(std::move(config));
+ NTracing::NProto::TTracingExt tracingExt;
+ ToProto(&tracingExt, parentContext);
+ auto traceContext = TTraceContext::NewChildFromRpc(tracingExt, "Span");
+ auto baggage = traceContext->UnpackBaggage();
+ ASSERT_NE(baggage, nullptr);
+ EXPECT_EQ(ConvertToYsonString(baggage), parentBaggageString);
+ }
+
+ {
+ auto config = New<TTracingConfig>();
+ config->SendBaggage = false;
+ SetTracingConfig(std::move(config));
+ NTracing::NProto::TTracingExt tracingExt;
+ ToProto(&tracingExt, parentContext);
+ auto traceContext = TTraceContext::NewChildFromRpc(tracingExt, "Span");
+ EXPECT_EQ(traceContext->UnpackBaggage(), nullptr);
+ }
+}
+
+TEST_W(TSchedulerTest, WaitForFast1)
+{
+ auto future = MakeFuture<int>(123);
+ TContextSwitchGuard guard(
+ [] { EXPECT_FALSE(true);},
+ nullptr);
+ auto value = WaitForFast(future)
+ .ValueOrThrow();
+ EXPECT_EQ(123, value);
+}
+
+TEST_W(TSchedulerTest, WaitForFast2)
+{
+ auto future = TDelayedExecutor::MakeDelayed(TDuration::MilliSeconds(100))
+ .Apply(BIND([] { return 123; }));
+ auto switched = std::make_shared<bool>();
+ TContextSwitchGuard guard(
+ [=] { *switched = true;},
+ nullptr);
+ auto value = WaitForFast(future)
+ .ValueOrThrow();
+ EXPECT_EQ(123, value);
+ EXPECT_TRUE(*switched);
+}
+
+TEST_W(TSchedulerTest, FutureUpdatedRaceInWaitFor_YT_18899)
+{
+ auto threadPool = CreateThreadPool(1, "TestThreads");
+ auto threadPoolInvoker = threadPool->GetInvoker();
+ auto serializedInvoker = CreateSerializedInvoker(threadPoolInvoker);
+
+ // N.B. We are testing race, so make a bunch of reps here.
+ for (int i = 0; i != 1'000; ++i) {
+ auto promise = NewPromise<void>();
+ auto modifiedFuture = promise.ToFuture();
+
+ modifiedFuture.Apply(
+ BIND([&] {
+ modifiedFuture = MakeFuture(TError{"error that should not be seen"});
+ })
+ .AsyncVia(serializedInvoker)
+ );
+
+ NThreading::TCountDownLatch latch{1};
+
+ auto testResultFuture = BIND([&] {
+ latch.CountDown();
+ // N.B. `future` object will be modified after we enter `WaitFor` function.
+ // We expect to get result of original future that will succeed.
+ WaitFor(modifiedFuture)
+ .ThrowOnError();
+ })
+ .AsyncVia(serializedInvoker)
+ .Run();
+
+ // Wait until serialized executor starts executing action.
+ latch.Wait();
+
+ BIND([&] {
+ // N.B. waiting action is inside WairFor now, because:
+ // - we know that waiting action had started execution before this action was scheduled
+ // - this action is executed inside the same serialized invoker.
+ promise.Set();
+ })
+ .AsyncVia(serializedInvoker)
+ .Run();
+
+ ASSERT_NO_THROW(testResultFuture
+ .Get()
+ .ThrowOnError());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendableInvokerTest
+ : public ::testing::Test
+{
+protected:
+ TLazyIntrusivePtr<TActionQueue> Queue1;
+
+ void TearDown() override
+ {
+ if (Queue1.HasValue()) {
+ Queue1->Shutdown();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TSuspendableInvokerTest, PollSuspendFuture)
+{
+ std::atomic<bool> flag(false);
+
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+
+ BIND([&] () {
+ Sleep(SleepQuantum * 10);
+ flag = true;
+ })
+ .Via(suspendableInvoker)
+ .Run();
+
+ auto future = suspendableInvoker->Suspend();
+
+ while (!flag) {
+ EXPECT_EQ(flag, future.IsSet());
+ Sleep(SleepQuantum);
+ }
+ Sleep(SleepQuantum);
+ EXPECT_EQ(flag, future.IsSet());
+}
+
+TEST_F(TSuspendableInvokerTest, SuspendableDoubleWaitFor)
+{
+ std::atomic<bool> flag(false);
+
+ auto threadPool = CreateThreadPool(3, "MyPool");
+ auto suspendableInvoker = CreateSuspendableInvoker(threadPool->GetInvoker());
+
+ auto promise = NewPromise<void>();
+
+ auto setFlagFuture = BIND([&] () {
+ WaitFor(VoidFuture)
+ .ThrowOnError();
+ Sleep(SleepQuantum);
+ WaitFor(VoidFuture)
+ .ThrowOnError();
+ promise.Set();
+
+ Sleep(SleepQuantum * 10);
+ flag = true;
+ })
+ .AsyncVia(suspendableInvoker)
+ .Run();
+
+ suspendableInvoker->Suspend().Get();
+ EXPECT_FALSE(promise.ToFuture().IsSet());
+ suspendableInvoker->Resume();
+ promise.ToFuture().Get();
+
+ setFlagFuture.Get();
+
+ auto flagValue = BIND([&] () -> bool {
+ return flag;
+ })
+ .AsyncVia(suspendableInvoker)
+ .Run()
+ .Get()
+ .ValueOrThrow();
+
+ EXPECT_TRUE(flagValue);
+}
+
+TEST_F(TSuspendableInvokerTest, EarlySuspend)
+{
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+ suspendableInvoker->Suspend().Get();
+
+ auto promise = NewPromise<void>();
+
+ BIND([&] () {
+ promise.Set();
+ })
+ .Via(suspendableInvoker)
+ .Run();
+
+ EXPECT_FALSE(promise.IsSet());
+ suspendableInvoker->Resume();
+ promise.ToFuture().Get();
+ EXPECT_TRUE(promise.IsSet());
+}
+
+TEST_F(TSuspendableInvokerTest, ResumeBeforeFullSuspend)
+{
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+
+ BIND([&] () {
+ Sleep(SleepQuantum);
+ })
+ .Via(suspendableInvoker)
+ .Run();
+
+ auto firstFuture = suspendableInvoker->Suspend();
+
+ EXPECT_FALSE(firstFuture.IsSet());
+ suspendableInvoker->Resume();
+ EXPECT_FALSE(firstFuture.Get().IsOK());
+}
+
+TEST_F(TSuspendableInvokerTest, AllowSuspendOnContextSwitch)
+{
+ std::atomic<bool> flag(false);
+
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+
+ auto setFlagFuture = BIND([&] () {
+ Sleep(SleepQuantum);
+ WaitUntilSet(future);
+ flag = true;
+ })
+ .AsyncVia(suspendableInvoker)
+ .Run();
+
+ auto suspendFuture = suspendableInvoker->Suspend();
+ EXPECT_FALSE(suspendFuture.IsSet());
+ EXPECT_TRUE(suspendFuture.Get().IsOK());
+
+ suspendableInvoker->Resume();
+ promise.Set();
+ setFlagFuture.Get();
+ EXPECT_TRUE(flag);
+}
+
+TEST_F(TSuspendableInvokerTest, SuspendResumeOnFinishedRace)
+{
+ std::atomic<bool> flag(false);
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+
+ BIND([&] () {
+ for (int i = 0; i < 100; ++i) {
+ Sleep(TDuration::MilliSeconds(1));
+ Yield();
+ }
+ })
+ .Via(suspendableInvoker)
+ .Run();
+
+ int hits = 0;
+ while (hits < 100) {
+ flag = false;
+ auto future = suspendableInvoker->Suspend()
+ .Apply(BIND([=, &flag] () { flag = true; }));
+
+ if (future.IsSet()) {
+ ++hits;
+ }
+ suspendableInvoker->Resume();
+ auto error = future.Get();
+ if (!error.IsOK()) {
+ EXPECT_FALSE(flag);
+ YT_VERIFY(error.GetCode() == NYT::EErrorCode::Canceled);
+ } else {
+ EXPECT_TRUE(flag);
+ }
+ }
+ auto future = suspendableInvoker->Suspend();
+ EXPECT_TRUE(future.Get().IsOK());
+}
+
+TEST_F(TSuspendableInvokerTest, ResumeInApply)
+{
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+
+ BIND([&] () {
+ Sleep(SleepQuantum);
+ })
+ .Via(suspendableInvoker)
+ .Run();
+
+ auto suspendFuture = suspendableInvoker->Suspend()
+ .Apply(BIND([=] () { suspendableInvoker->Resume(); }));
+
+ EXPECT_TRUE(suspendFuture.Get().IsOK());
+}
+
+TEST_F(TSuspendableInvokerTest, VerifySerializedActionsOrder)
+{
+ auto suspendableInvoker = CreateSuspendableInvoker(Queue1->GetInvoker());
+
+ suspendableInvoker->Suspend()
+ .Get();
+
+ const int totalActionCount = 100000;
+
+ std::atomic<int> actionIndex = {0};
+ std::atomic<int> reorderingCount = {0};
+
+ for (int i = 0; i < totalActionCount / 2; ++i) {
+ BIND([&actionIndex, &reorderingCount, i] () {
+ reorderingCount += (actionIndex != i);
+ ++actionIndex;
+ })
+ .Via(suspendableInvoker)
+ .Run();
+ }
+
+ TDelayedExecutor::Submit(
+ BIND([&] () {
+ suspendableInvoker->Resume();
+ }),
+ SleepQuantum / 10);
+
+ for (int i = totalActionCount / 2; i < totalActionCount; ++i) {
+ BIND([&actionIndex, &reorderingCount, i] () {
+ reorderingCount += (actionIndex != i);
+ ++actionIndex;
+ })
+ .Via(suspendableInvoker)
+ .Run();
+ }
+
+ while (actionIndex < totalActionCount) {
+ Sleep(SleepQuantum);
+ }
+ EXPECT_EQ(actionIndex, totalActionCount);
+ EXPECT_EQ(reorderingCount, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairShareSchedulerTest
+ : public TSchedulerTest
+ , public ::testing::WithParamInterface<std::tuple<int, int, int, TDuration>>
+{ };
+
+TEST_P(TFairShareSchedulerTest, TwoLevelFairness)
+{
+ size_t numThreads = std::get<0>(GetParam());
+ size_t numWorkers = std::get<1>(GetParam());
+ size_t numPools = std::get<2>(GetParam());
+ auto work = std::get<3>(GetParam());
+
+
+ YT_VERIFY(numWorkers > 0);
+ YT_VERIFY(numThreads > 0);
+ YT_VERIFY(numPools > 0);
+ YT_VERIFY(numWorkers > numPools);
+ YT_VERIFY(numThreads <= numWorkers);
+
+ auto threadPool = CreateNewTwoLevelFairShareThreadPool(
+ numThreads,
+ "MyFairSharePool",
+ /*poolWeightProvider*/ nullptr,
+ /*verboseLogging*/ true);
+
+ std::vector<TDuration> progresses(numWorkers);
+ std::vector<TDuration> pools(numPools);
+
+ auto getShift = [&] (size_t id) {
+ return SleepQuantum * (id + 1) / numWorkers;
+ };
+
+ std::vector<TFuture<void>> futures;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, lock);
+
+ for (size_t id = 0; id < numWorkers; ++id) {
+ auto invoker = threadPool->GetInvoker(Format("pool%v", id % numPools), Format("worker%v", id));
+ auto worker = [&, id] () mutable {
+ auto poolId = id % numPools;
+
+ auto initialShift = getShift(id);
+ {
+ auto startInstant = GetCpuInstant();
+ Sleep(initialShift);
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ pools[poolId] += elapsedTime;
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+
+ while (progresses[id] < work + initialShift) {
+ NProfiling::TWallTimer timer;
+ {
+ auto guard = Guard(lock);
+
+ if (numThreads == 1) {
+ auto minPool = TDuration::Max();
+ for (size_t index = 0; index < numPools; ++index) {
+ bool hasBucketsInPool = false;
+ for (size_t workerId = index; workerId < numWorkers; workerId += numPools) {
+ if (progresses[workerId] < work + getShift(workerId)) {
+ hasBucketsInPool = true;
+ }
+ }
+ if (hasBucketsInPool && pools[index] < minPool) {
+ minPool = pools[index];
+ }
+ }
+
+ if (pools[poolId].MilliSeconds() != minPool.MilliSeconds()) {
+ YT_LOG_ERROR("Pools time: [%v]",
+ MakeFormattableView(
+ pools,
+ [&] (auto* builder, const auto& pool) {
+ builder->AppendFormat("%v", pool.MilliSeconds());
+ }));
+ }
+
+ EXPECT_EQ(pools[poolId].MilliSeconds(), minPool.MilliSeconds());
+
+ auto min = TDuration::Max();
+ for (size_t index = poolId; index < numWorkers; index += numPools) {
+ if (progresses[index] < min && progresses[index] < work + getShift(index)) {
+ min = progresses[index];
+ }
+ }
+
+ if (progresses[id].MilliSeconds() != min.MilliSeconds()) {
+ YT_LOG_ERROR("Pools time: [%v]",
+ MakeFormattableView(
+ pools,
+ [&] (auto* builder, const auto& pool) {
+ builder->AppendFormat("%v", pool.MilliSeconds());
+ }));
+
+ YT_LOG_ERROR("Progresses time: [%v]",
+ MakeFormattableView(
+ progresses,
+ [&] (auto* builder, const auto& progress) {
+ builder->AppendFormat("%v", progress.MilliSeconds());
+ }));
+ }
+
+ EXPECT_EQ(progresses[id].MilliSeconds(), min.MilliSeconds());
+ }
+ }
+
+ {
+ auto startInstant = GetCpuInstant();
+ Sleep(SleepQuantum * (id + numWorkers) / numWorkers);
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ pools[poolId] += elapsedTime;
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+ }
+ };
+
+ auto result = BIND(worker)
+ .AsyncVia(invoker)
+ .Run();
+
+ futures.push_back(result);
+
+ // Random stuck.
+ if (id == 1) {
+ Sleep(TDuration::MilliSeconds(3));
+ }
+ }
+
+ WaitFor(AllSucceeded(futures))
+ .ThrowOnError();
+}
+
+TEST_P(TFairShareSchedulerTest, Fairness)
+{
+ size_t numThreads = std::get<0>(GetParam());
+ size_t numWorkers = std::get<1>(GetParam());
+ size_t numPools = std::get<2>(GetParam());
+ auto work = std::get<3>(GetParam());
+
+
+ YT_VERIFY(numWorkers > 0);
+ YT_VERIFY(numThreads > 0);
+ YT_VERIFY(numPools > 0);
+ YT_VERIFY(numWorkers > numPools);
+ YT_VERIFY(numThreads <= numWorkers);
+
+ if (numPools != 1) {
+ return;
+ }
+
+ auto threadPool = CreateFairShareThreadPool(numThreads, "MyFairSharePool");
+
+ std::vector<TDuration> progresses(numWorkers);
+
+ auto getShift = [&] (size_t id) {
+ return SleepQuantum * (id + 1) / numWorkers;
+ };
+
+ std::vector<TFuture<void>> futures;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, lock);
+
+ for (size_t id = 0; id < numWorkers; ++id) {
+ auto invoker = threadPool->GetInvoker(Format("worker%v", id));
+ auto worker = [&, id] () mutable {
+ auto initialShift = getShift(id);
+ {
+ auto startInstant = GetCpuInstant();
+ Sleep(initialShift);
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+
+ while (progresses[id] < work + initialShift) {
+ {
+ auto guard = Guard(lock);
+
+ if (numThreads == 1) {
+ auto min = TDuration::Max();
+ for (size_t id = 0; id < numWorkers; ++id) {
+ if (progresses[id] < min && progresses[id] < work + getShift(id)) {
+ min = progresses[id];
+ }
+ }
+
+ if (progresses[id].MilliSeconds() != min.MilliSeconds()) {
+ YT_LOG_ERROR("Progresses time: [%v]",
+ MakeFormattableView(
+ progresses,
+ [&] (TStringBuilderBase* builder, const auto& progress) {
+ builder->AppendFormat("%v", progress.MilliSeconds());
+ }));
+ }
+
+ EXPECT_EQ(progresses[id].MilliSeconds(), min.MilliSeconds());
+ }
+ }
+
+ {
+ auto startInstant = GetCpuInstant();
+ Sleep(SleepQuantum * (id + numWorkers) / numWorkers);
+ auto guard = Guard(lock);
+ auto elapsedTime = CpuDurationToDuration(GetCpuInstant() - startInstant);
+ progresses[id] += elapsedTime;
+ }
+
+ Yield();
+ }
+ };
+
+ auto result = BIND(worker)
+ .AsyncVia(invoker)
+ .Run();
+
+ futures.push_back(result);
+ }
+
+ WaitFor(AllSucceeded(futures))
+ .ThrowOnError();
+}
+
+const auto FSWorkTime = SleepQuantum * 10;
+
+INSTANTIATE_TEST_SUITE_P(
+ Test,
+ TFairShareSchedulerTest,
+ ::testing::Values(
+ std::make_tuple(1, 5, 1, FSWorkTime),
+ std::make_tuple(1, 7, 3, FSWorkTime),
+ std::make_tuple(5, 7, 1, FSWorkTime),
+ std::make_tuple(5, 7, 3, FSWorkTime)
+ ));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/suspendable_action_queue_ut.cpp b/yt/yt/core/concurrency/unittests/suspendable_action_queue_ut.cpp
new file mode 100644
index 0000000000..58aac2557e
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/suspendable_action_queue_ut.cpp
@@ -0,0 +1,253 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/suspendable_action_queue.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <yt/yt/core/utilex/random.h>
+
+#include <random>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSuspendableActionQueueTest
+ : public ::testing::Test
+{
+protected:
+ ISuspendableActionQueuePtr Queue_ = CreateSuspendableActionQueue("TestQueue");
+ IInvokerPtr Invoker_ = Queue_->GetInvoker();
+
+ void RandomSleep()
+ {
+ if (RandomNumber<ui64>(5) == 0u) {
+ return;
+ }
+
+ TDelayedExecutor::WaitForDuration(RandomDuration(TDuration::MilliSeconds(15)));
+ }
+};
+
+TEST_F(TSuspendableActionQueueTest, Simple)
+{
+ std::atomic<i64> x = 0;
+ BIND([&x] { ++x; })
+ .AsyncVia(Invoker_)
+ .Run()
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_EQ(x, 1);
+}
+
+TEST_F(TSuspendableActionQueueTest, SuspendResume)
+{
+ std::atomic<i64> x = 0;
+ auto future = BIND([&x] {
+ while (true) {
+ ++x;
+ Yield();
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+
+ TDelayedExecutor::WaitForDuration(RandomDuration(TDuration::MilliSeconds(15)));
+
+ Queue_->Suspend(/*immediate*/ true)
+ .Get()
+ .ThrowOnError();
+
+ i64 x1 = x;
+ EXPECT_GT(x1, 0);
+
+ TDelayedExecutor::WaitForDuration(RandomDuration(TDuration::MilliSeconds(15)));
+
+ i64 x2 = x;
+ EXPECT_EQ(x2, x1);
+
+ Queue_->Resume();
+
+ TDelayedExecutor::WaitForDuration(RandomDuration(TDuration::MilliSeconds(15)));
+
+ i64 x3 = x;
+ EXPECT_GT(x3, x2);
+
+ future.Cancel(TError("Test ended"));
+}
+
+TEST_F(TSuspendableActionQueueTest, SuspendEmptyQueue)
+{
+ Queue_->Suspend(/*immedidate*/ true)
+ .Get()
+ .ThrowOnError();
+ Queue_->Resume();
+
+ Queue_->Suspend(/*immedidate*/ false)
+ .Get()
+ .ThrowOnError();
+ Queue_->Resume();
+
+ int x = 0;
+ BIND([&x] {++x; })
+ .AsyncVia(Invoker_)
+ .Run()
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_EQ(x, 1);
+}
+
+TEST_F(TSuspendableActionQueueTest, NotImmediateSuspend)
+{
+ std::atomic<i64> x = 0;
+ Invoker_->Invoke(BIND([&x] {
+ for (int iteration = 0; iteration < 50; ++iteration) {
+ ++x;
+
+ Sleep(TDuration::MilliSeconds(10));
+
+ Yield();
+ }
+ }));
+
+ auto future = Queue_->Suspend(/*immedidate*/ false);
+
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+
+ EXPECT_FALSE(future.IsSet());
+
+ future
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_EQ(x, 50);
+
+ Queue_->Resume();
+}
+
+TEST_F(TSuspendableActionQueueTest, PromoteSuspendToImmediate)
+{
+ i64 x = 0;
+ auto future = BIND([&x] {
+ while (true) {
+ ++x;
+ Yield();
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+
+ auto suspendFuture = Queue_->Suspend(/*immedidate*/ false);
+
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+
+ EXPECT_FALSE(suspendFuture.IsSet());
+
+ Queue_->Suspend(/*immediately*/ true)
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_TRUE(suspendFuture.IsSet());
+ EXPECT_GT(x, 0);
+
+ Queue_->Resume();
+
+ future.Cancel(TError("Test ended"));
+}
+
+TEST_F(TSuspendableActionQueueTest, StressTest1)
+{
+ std::atomic<i64> x = 0;
+
+ std::vector<TFuture<void>> futures;
+ for (int index = 0; index < 100; ++index) {
+ auto future = BIND([&x] {
+ while (true) {
+ ++x;
+ Yield();
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+ futures.push_back(future);
+ }
+
+ i64 lastX = 0;
+ for (int iteration = 0; iteration < 100; ++iteration) {
+ RandomSleep();
+
+ Queue_->Suspend(/*immedidate*/ true)
+ .Get()
+ .ThrowOnError();
+
+ i64 currentX = x;
+ EXPECT_GE(currentX, lastX);
+ lastX = currentX;
+
+ RandomSleep();
+
+ currentX = x;
+ EXPECT_EQ(currentX, lastX);
+
+ Queue_->Resume();
+ }
+
+ for (auto& future : futures) {
+ future.Cancel(TError("Test ended"));
+ }
+}
+
+TEST_F(TSuspendableActionQueueTest, StressTest2)
+{
+ std::atomic<i64> x = 0;
+
+ std::vector<TFuture<void>> futures;
+ for (int index = 0; index < 100; ++index) {
+ auto future = BIND([&x] {
+ for (int iteration = 0; iteration < 1000; ++iteration) {
+ ++x;
+ Yield();
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+ futures.push_back(future);
+ }
+
+ i64 lastX = 0;
+ for (int iteration = 0; iteration < 100; ++iteration) {
+ RandomSleep();
+
+ Queue_->Suspend(/*immedidate*/ true)
+ .Get()
+ .ThrowOnError();
+
+ i64 currentX = x;
+ EXPECT_GE(currentX, lastX);
+ lastX = currentX;
+
+ RandomSleep();
+
+ currentX = x;
+ EXPECT_EQ(currentX, lastX);
+
+ Queue_->Resume();
+ }
+
+ for (auto& future : futures) {
+ future.Get();
+ }
+
+ EXPECT_EQ(x, 100'000);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/thread_affinity_ut.cpp b/yt/yt/core/concurrency/unittests/thread_affinity_ut.cpp
new file mode 100644
index 0000000000..01f1564489
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/thread_affinity_ut.cpp
@@ -0,0 +1,146 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMyObject
+{
+ DECLARE_THREAD_AFFINITY_SLOT(FirstThread);
+ DECLARE_THREAD_AFFINITY_SLOT(SecondThread);
+
+public:
+ void A()
+ {
+ VERIFY_THREAD_AFFINITY(FirstThread);
+ }
+
+ void B()
+ {
+ VERIFY_THREAD_AFFINITY(SecondThread);
+ }
+
+ void C()
+ {
+ VERIFY_THREAD_AFFINITY(FirstThread);
+ }
+};
+
+#define PROLOGUE() \
+ auto queue1 = New<TActionQueue>(); \
+ auto queue2 = New<TActionQueue>(); \
+ auto invoker1 = queue1->GetInvoker(); \
+ auto invoker2 = queue2->GetInvoker(); \
+
+void SingleThreadedAccess(TMyObject* object)
+{
+ PROLOGUE();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker1).Run().Get();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker1).Run().Get();
+}
+
+void UntangledThreadAccess(TMyObject* object)
+{
+ PROLOGUE();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+}
+
+void UntangledThreadAccessToSharedSlot(TMyObject* object)
+{
+ PROLOGUE();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+ BIND(&TMyObject::C, object).AsyncVia(invoker1).Run().Get();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+ BIND(&TMyObject::C, object).AsyncVia(invoker1).Run().Get();
+}
+
+[[maybe_unused]] void TangledThreadAccess1(TMyObject* object)
+{
+ PROLOGUE();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker1).Run().Get();
+}
+
+[[maybe_unused]] void TangledThreadAccess2(TMyObject* object)
+{
+ PROLOGUE();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker1).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+
+ BIND(&TMyObject::A, object).AsyncVia(invoker2).Run().Get();
+ BIND(&TMyObject::B, object).AsyncVia(invoker2).Run().Get();
+}
+
+#undef PROLOGUE
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TThreadAffinityTest, SingleThreadedAccess)
+{
+ TMyObject object;
+ SingleThreadedAccess(&object);
+
+ SUCCEED();
+}
+
+TEST(TThreadAffinityTest, UntangledThreadAccess)
+{
+ TMyObject object;
+ UntangledThreadAccess(&object);
+
+ SUCCEED();
+}
+
+TEST(TThreadAffinityTest, UntangledThreadAccessToSharedSlot)
+{
+ TMyObject object;
+ UntangledThreadAccessToSharedSlot(&object);
+
+ SUCCEED();
+}
+
+#ifndef NDEBUG
+
+TEST(TThreadAffinityDeathTest, DISABLED_TangledThreadAccess1)
+{
+ TMyObject object;
+ ASSERT_DEATH({ TangledThreadAccess1(&object); }, ".*");
+}
+
+TEST(TThreadAffinityDeathTest, DISABLED_TangledThreadAccess2)
+{
+ TMyObject object;
+ ASSERT_DEATH({ TangledThreadAccess2(&object); }, ".*");
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/thread_pool_poller_ut.cpp b/yt/yt/core/concurrency/unittests/thread_pool_poller_ut.cpp
new file mode 100644
index 0000000000..9f939a971b
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/thread_pool_poller_ut.cpp
@@ -0,0 +1,210 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <util/system/env.h>
+
+#include <thread>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void ExpectSuccessfullySetFuture(const TFuture<T>& future)
+{
+ YT_VERIFY(future.WithTimeout(TDuration::Seconds(15)).Get().IsOK());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPollableMock
+ : public IPollable
+{
+public:
+ explicit TPollableMock(TString loggingTag = {})
+ : LoggingTag_(std::move(loggingTag))
+ { }
+
+ void SetCookie(IPollable::TCookiePtr cookie) override
+ {
+ Cookie_ = std::move(cookie);
+ }
+
+ void* GetCookie() const override
+ {
+ return static_cast<void*>(Cookie_.Get());
+ }
+
+ const TString& GetLoggingTag() const override
+ {
+ return LoggingTag_;
+ }
+
+ EPollablePriority GetPriority() const override
+ {
+ return EPollablePriority::Normal;
+ }
+
+ void OnEvent(EPollControl control) override
+ {
+ // NB: Retry is the only event we trigger in this unittest via |IPoller::Retry|.
+ YT_VERIFY(control == EPollControl::Retry);
+ YT_VERIFY(!ShutdownPromise_.IsSet());
+ RetryPromise_.Set();
+ }
+
+ void OnShutdown() override
+ {
+ ShutdownPromise_.Set();
+ }
+
+ TFuture<void> GetRetryFuture() const
+ {
+ return RetryPromise_;
+ }
+
+ TFuture<void> GetShutdownFuture() const
+ {
+ return ShutdownPromise_;
+ }
+
+private:
+ const TString LoggingTag_;
+
+ const TPromise<void> RetryPromise_ = NewPromise<void>();
+ const TPromise<void> ShutdownPromise_ = NewPromise<void>();
+
+ IPollable::TCookiePtr Cookie_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThreadPoolPollerTest
+ : public ::testing::Test
+{
+public:
+ void SetUp() override
+ {
+ Poller = CreateThreadPoolPoller(InitialThreadCount, "TestPoller");
+ }
+
+ void TearDown() override
+ {
+ Poller->Shutdown();
+ }
+
+protected:
+ const int InitialThreadCount = 4;
+
+ IThreadPoolPollerPtr Poller;
+};
+
+TEST_F(TThreadPoolPollerTest, SimplePollable)
+{
+ auto pollable = New<TPollableMock>();
+ EXPECT_TRUE(Poller->TryRegister(pollable));
+ Poller->Retry(pollable);
+
+ ExpectSuccessfullySetFuture(pollable->GetRetryFuture());
+
+ std::vector<TFuture<void>> futures{
+ Poller->Unregister(pollable),
+ pollable->GetShutdownFuture()
+ };
+ ExpectSuccessfullySetFuture(AllSucceeded(futures));
+}
+
+TEST_F(TThreadPoolPollerTest, SimpleCallback)
+{
+ auto promise = NewPromise<void>();
+ auto callback = BIND([=] { promise.Set(); });
+
+ Poller->GetInvoker()->Invoke(callback);
+
+ ExpectSuccessfullySetFuture(promise.ToFuture());
+}
+
+TEST_F(TThreadPoolPollerTest, SimpleReconfigure)
+{
+ auto pollable = New<TPollableMock>();
+ EXPECT_TRUE(Poller->TryRegister(pollable));
+
+ Poller->Reconfigure(InitialThreadCount * 2);
+
+ std::vector<TFuture<void>> futures{
+ Poller->Unregister(pollable),
+ pollable->GetShutdownFuture()
+ };
+ ExpectSuccessfullySetFuture(AllSucceeded(futures));
+}
+
+TEST_F(TThreadPoolPollerTest, Stress)
+{
+ std::vector<std::thread> threads;
+
+ auto envIterCount = GetEnv("ITER_COUNT");
+#if defined(__aarch64__) || defined(__arm64__)
+ constexpr int defaultIterCount = 10'000;
+#else
+ constexpr int defaultIterCount = 20'000;
+#endif
+ int iterCount = envIterCount.empty() ? defaultIterCount : FromString<int>(envIterCount);
+
+ std::vector<std::thread> auxThreads;
+ auxThreads.emplace_back([&] {
+ for (int i = 0; i < 10; ++i) {
+ threads.emplace_back([&, i] {
+ std::vector<TIntrusivePtr<TPollableMock>> pollables;
+ for (int j = 0; j < iterCount; ++j) {
+ pollables.push_back(New<TPollableMock>(Format("%v/%05d", i, j)));
+ EXPECT_TRUE(Poller->TryRegister(pollables.back()));
+ }
+
+ std::this_thread::yield();
+
+ std::vector<TFuture<void>> retryFutures;
+ std::vector<TFuture<void>> unregisterFutures;
+ for (int j = 0; j < iterCount; j += 2) {
+ Poller->Retry(pollables[j]);
+ retryFutures.push_back(pollables[j]->GetRetryFuture());
+
+ Poller->Retry(pollables[j + 1]);
+ std::vector<TFuture<void>> futures{
+ Poller->Unregister(pollables[j + 1]),
+ pollables[j + 1]->GetShutdownFuture()
+ };
+ unregisterFutures.push_back(AllSucceeded(futures));
+ }
+
+ ExpectSuccessfullySetFuture(AllSucceeded(retryFutures));
+ ExpectSuccessfullySetFuture(AllSucceeded(unregisterFutures));
+ });
+
+ std::this_thread::yield();
+ }
+ });
+ auxThreads.emplace_back([&] {
+ for (int j = 0; j < 10; ++j) {
+ for (int i = 1, sign = -1; i < 10; ++i, sign *= -1) {
+ Poller->Reconfigure(InitialThreadCount + sign * i);
+ }
+ std::this_thread::yield();
+ }
+ });
+
+ for (auto& thread : auxThreads) {
+ thread.join();
+ }
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/thread_pool_ut.cpp b/yt/yt/core/concurrency/unittests/thread_pool_ut.cpp
new file mode 100644
index 0000000000..19cdb60f88
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/thread_pool_ut.cpp
@@ -0,0 +1,51 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+#include <yt/yt/core/actions/invoker.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <util/random/random.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TThreadPoolTest, Configure)
+{
+ auto threadPool = CreateThreadPool(1, "Test");
+ auto counter = std::make_shared<std::atomic<int>>();
+ auto callback = BIND([=] { ++*counter; });
+ std::vector<TFuture<void>> futures;
+
+ const int N = 10000;
+ int threadCount = 0;
+ for (int i = 0; i < N; ++i) {
+ futures.push_back(callback.AsyncVia(threadPool->GetInvoker()).Run());
+ if (i % 100 == 0) {
+ threadCount = RandomNumber<size_t>(10) + 1;
+ threadPool->Configure(threadCount);
+ EXPECT_EQ(threadPool->GetThreadCount(), threadCount);
+ }
+ }
+
+ AllSucceeded(std::move(futures))
+ .Get();
+
+ // Thread pool doesn't contain less than one thread whatever you configured.
+ threadPool->Configure(0);
+ EXPECT_EQ(threadPool->GetThreadCount(), 1);
+
+ // Thread pool doesn't contain more than maximal threads count whatever you configured.
+ threadPool->Configure(1e8);
+ EXPECT_LE(threadPool->GetThreadCount(), 1e8);
+
+ threadPool->Shutdown();
+ EXPECT_EQ(N, counter->load());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/throughput_throttler_ut.cpp b/yt/yt/core/concurrency/unittests/throughput_throttler_ut.cpp
new file mode 100644
index 0000000000..3364e553e8
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/throughput_throttler_ut.cpp
@@ -0,0 +1,601 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/throughput_throttler.h>
+#include <yt/yt/core/concurrency/config.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <random>
+#include <thread>
+#include <vector>
+
+namespace NYT::NConcurrency {
+namespace {
+
+using namespace NLogging;
+
+using namespace testing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TLogger Logger("Test");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TReconfigurableThroughputThrottlerTest, TestNoLimit)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ New<TThroughputThrottlerConfig>());
+
+ NProfiling::TWallTimer timer;
+ for (int i = 0; i < 1000; ++i) {
+ throttler->Throttle(1).Get().ThrowOnError();
+ }
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 100u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestLimit)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+ throttler->Throttle(1).Get().ThrowOnError();
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 50u);
+
+ throttler->Throttle(1).Get().ThrowOnError();
+ throttler->Throttle(1).Get().ThrowOnError();
+
+ auto duration = timer.GetElapsedTime().MilliSeconds();
+ EXPECT_GE(duration, 1000u);
+ EXPECT_LE(duration, 3000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestScheduleUpdate)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+
+ throttler->Throttle(3).Get().ThrowOnError();
+
+ throttler->Throttle(1).Get().ThrowOnError();
+ throttler->Throttle(1).Get().ThrowOnError();
+ throttler->Throttle(1).Get().ThrowOnError();
+
+ auto duration = timer.GetElapsedTime().MilliSeconds();
+ EXPECT_GE(duration, 3000u);
+ EXPECT_LE(duration, 6000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestUpdate)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+
+ throttler->Throttle(1).Get().ThrowOnError();
+ Sleep(TDuration::Seconds(1));
+ throttler->Throttle(1).Get().ThrowOnError();
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 2000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestCancel)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+
+ throttler->Throttle(5).Get().ThrowOnError();
+ auto future = throttler->Throttle(1);
+ future.Cancel(TError("Error"));
+ auto result = future.Get();
+
+ EXPECT_FALSE(result.IsOK());
+ EXPECT_TRUE(result.GetCode() == NYT::EErrorCode::Canceled);
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 100u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestReconfigureSchedulesUpdatesProperly)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+
+ std::vector<TFuture<void>> scheduled;
+ for (int i = 0; i < 4; ++i) {
+ scheduled.push_back(throttler->Throttle(100));
+ }
+
+ throttler->Reconfigure(TThroughputThrottlerConfig::Create(100));
+
+ for (const auto& future : scheduled) {
+ future.Get().ThrowOnError();
+ }
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 5000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestSetLimit)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ NProfiling::TWallTimer timer;
+
+ std::vector<TFuture<void>> scheduled;
+ for (int i = 0; i < 4; ++i) {
+ scheduled.push_back(throttler->Throttle(100));
+ }
+
+ throttler->SetLimit(100);
+
+ for (const auto& future : scheduled) {
+ future.Get().ThrowOnError();
+ }
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 5000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestReconfigureMustRescheduleUpdate)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(1));
+
+ throttler->Acquire(1); // drains throttler to zero
+
+ NProfiling::TWallTimer timer;
+
+ auto scheduled1 = throttler->Throttle(100); // sits in front of the queue
+ auto scheduled2 = throttler->Throttle(100); // also waits in the queue
+
+ throttler->Reconfigure(TThroughputThrottlerConfig::Create(100));
+
+ EXPECT_FALSE(scheduled2.IsSet()); // must remain waiting in the queue after Reconfigure
+
+ scheduled2.Get().ThrowOnError();
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 3000u); // Reconfigure must have rescheduled the update
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestOverdraft)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(100));
+
+ throttler->Throttle(150).Get().ThrowOnError();
+
+ EXPECT_TRUE(throttler->IsOverdraft());
+ Sleep(TDuration::Seconds(2));
+ EXPECT_FALSE(throttler->IsOverdraft());
+}
+
+#if !defined(_asan_enabled_) && !defined(_msan_enabled_) && !defined(_tsan_enabled_)
+
+TEST(TReconfigurableThroughputThrottlerTest, StressTest)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(100));
+
+ const int N = 30;
+ const int M = 10;
+
+ NProfiling::TWallTimer timer;
+
+ std::vector<std::thread> threads;
+ for (int i = 0; i < N; i++) {
+ threads.emplace_back([&] {
+ for (int j = 0; j < M; ++j) {
+ throttler->Throttle(1).Get().ThrowOnError();
+ }
+ });
+ }
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 3000u);
+}
+
+#endif
+
+TEST(TReconfigurableThroughputThrottlerTest, TestFractionalLimit)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(0.5));
+
+ NProfiling::TWallTimer timer;
+ for (int i = 0; i < 2; ++i) {
+ throttler->Throttle(1).Get().ThrowOnError();
+ }
+ auto duration = timer.GetElapsedTime().MilliSeconds();
+ EXPECT_GE(duration, 1500u);
+ EXPECT_LE(duration, 4000u);
+}
+
+TEST(TReconfigurableThroughputThrottlerTest, TestZeroLimit)
+{
+ auto throttler = CreateReconfigurableThroughputThrottler(
+ TThroughputThrottlerConfig::Create(0));
+
+ throttler->SetLimit(100);
+
+ NProfiling::TWallTimer timer;
+
+ std::vector<TFuture<void>> scheduled;
+ for (int i = 0; i < 4; ++i) {
+ scheduled.push_back(throttler->Throttle(10));
+ }
+
+ for (const auto& future : scheduled) {
+ future.Get().ThrowOnError();
+ }
+
+ EXPECT_LE(timer.GetElapsedTime().MilliSeconds(), 1000u);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TMockThrottler)
+
+class TMockThrottler
+ : public IThroughputThrottler
+{
+public:
+ MOCK_METHOD(TFuture<void>, Throttle, (i64 /*amount*/), (override));
+
+ bool TryAcquire(i64 /*amount*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ i64 TryAcquireAvailable(i64 /*amount*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ void Acquire(i64 /*amount*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ bool IsOverdraft() override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ i64 GetQueueTotalAmount() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ TDuration GetEstimatedOverdraftDuration() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ i64 GetAvailable() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TMockThrottler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrefetchingThrottlerExponentialGrowthTest
+ : public ::testing::Test
+{
+public:
+ void SetUp() override
+ {
+ Config_ = New<TPrefetchingThrottlerConfig>();
+ Config_->TargetRps = 1.0;
+ Config_->MinPrefetchAmount = 1;
+ Config_->MaxPrefetchAmount = 1 << 30;
+ Config_->Window = TDuration::MilliSeconds(100);
+
+ Throttler_ = CreatePrefetchingThrottler(Config_, Underlying_, Logger);
+ }
+
+protected:
+ TPrefetchingThrottlerConfigPtr Config_;
+ TMockThrottlerPtr Underlying_ = New<TMockThrottler>();
+ IThroughputThrottlerPtr Throttler_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TPrefetchingThrottlerExponentialGrowthTest, OneRequest)
+{
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .Times(1)
+ .WillRepeatedly(Return(VoidFuture));
+
+ EXPECT_TRUE(Throttler_->Throttle(1).Get().IsOK());
+}
+
+TEST_F(TPrefetchingThrottlerExponentialGrowthTest, ManyRequests)
+{
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .Times(4)
+ .WillRepeatedly(Return(VoidFuture));
+
+ for (int i = 0; i < 1'000; ++i) {
+ EXPECT_TRUE(Throttler_->Throttle(1).Get().IsOK());
+ }
+}
+
+TEST_F(TPrefetchingThrottlerExponentialGrowthTest, SpikeAmount)
+{
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .Times(4)
+ .WillRepeatedly(Return(VoidFuture));
+
+ for (int i = 0; i < 3; ++i) {
+ EXPECT_TRUE(Throttler_->Throttle(1).Get().IsOK());
+ }
+
+ EXPECT_TRUE(Throttler_->Throttle(10'000'000).Get().IsOK());
+
+ for (int i = 3; i < 1'000; ++i) {
+ EXPECT_TRUE(Throttler_->Throttle(1).Get().IsOK());
+ }
+}
+
+TEST_F(TPrefetchingThrottlerExponentialGrowthTest, DoNotOverloadUnderlyingWhenTheQuotaIsExceeded)
+{
+ std::vector<TPromise<void>> requests;
+ std::vector<TFuture<void>> replies;
+ TPromise<void> lastRequest;
+
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .Times(AtMost(9))
+ .WillRepeatedly(DoAll(
+ [&] () { lastRequest = requests.emplace_back(NewPromise<void>()); },
+ ReturnPointee(&lastRequest)
+ ));
+
+ for (int i = 0; i < 100; ++i) {
+ replies.emplace_back(Throttler_->Throttle(1));
+ }
+
+ for (auto& request : requests) {
+ request.Set();
+ }
+}
+
+TEST_F(TPrefetchingThrottlerExponentialGrowthTest, DoNotHangUpAfterAnError)
+{
+ std::vector<TPromise<void>> requests;
+ TPromise<void> lastRequest;
+
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .Times(AtLeast(2))
+ .WillRepeatedly(DoAll(
+ [&] () { lastRequest = requests.emplace_back(NewPromise<void>()); },
+ ReturnPointee(&lastRequest)
+ ));
+
+ auto failedRequest = Throttler_->Throttle(10);
+ requests[0].Set(TError(NYT::EErrorCode::Generic, "Test error"));
+ EXPECT_FALSE(failedRequest.Get().IsOK());
+
+ Throttler_->Throttle(1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TStressParameters
+{
+ TDuration TestDuration;
+ TDuration IterationDuration = TDuration::MilliSeconds(100);
+ int RequestsPerIteration = 10'000;
+ int IterationsPerStep = 0;
+ int PassCount = 1;
+ double StepMultiplier = 1.0;
+ double MaxUnderlyingAmountMultiplier = 2.0;
+ double AllowedRpsOverflowMultiplier;
+ double TryAcquireAllProbability = -1.0;
+ double ErrorProbability = -1.0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPrefetchingStressTest
+ : public ::testing::TestWithParam<TStressParameters>
+{
+public:
+ void SetUp() override
+ {
+ Config_ = New<TPrefetchingThrottlerConfig>();
+ Config_->TargetRps = 10.0;
+ Config_->MinPrefetchAmount = 1;
+ Config_->MaxPrefetchAmount = 1 << 30;
+ Config_->Window = TDuration::Seconds(1);
+
+ Throttler_ = CreatePrefetchingThrottler(Config_, Underlying_, Logger);
+ }
+
+protected:
+ TPrefetchingThrottlerConfigPtr Config_;
+ TMockThrottlerPtr Underlying_ = New<TMockThrottler>();
+ IThroughputThrottlerPtr Throttler_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_P(TPrefetchingStressTest, Stress)
+{
+ auto parameters = GetParam();
+
+ std::mt19937 engine(314159265);
+ std::uniform_int_distribution<int> underlyingResponses(0, 2);
+ std::uniform_int_distribution<int> incomingAmount(1, 1 << 15);
+ std::uniform_real_distribution<double> incomingSpikeLbAmount(15.0, 20.0);
+ std::uniform_real_distribution<double> probabilisticOutcome(0.0, 1.0);
+
+ double tryAcquireProbability = 0.000'1;
+ double acquireProbability = 0.000'1;
+
+ for (int pass = 0; pass < parameters.PassCount; ++pass) {
+ i64 lastUnderlyingAmount = 0;
+ i64 iterationUnderlyingAmount = 0;
+ int underlyingRequestCount = 0;
+
+ std::deque<TPromise<void>> requests;
+ std::vector<TFuture<void>> replies;
+ TPromise<void> lastRequest;
+ int processedUnderlyingRequests = 0;
+
+ EXPECT_CALL(*Underlying_, Throttle(_))
+ .WillRepeatedly(DoAll(
+ SaveArg<0>(&lastUnderlyingAmount),
+ [&] () {
+ lastRequest = requests.emplace_back(NewPromise<void>());
+ ++underlyingRequestCount;
+ iterationUnderlyingAmount += lastUnderlyingAmount;
+ },
+ ReturnPointee(&lastRequest)
+ ));
+
+ auto processUnderlyingRequest = [&](double errorProbability) {
+ if (!requests.empty()) {
+ if (probabilisticOutcome(engine) < errorProbability) {
+ requests.front().Set(TError(NYT::EErrorCode::Generic, "Test error"));
+ } else {
+ requests.front().Set();
+ }
+ requests.pop_front();
+ ++processedUnderlyingRequests;
+ }
+ };
+
+ int iteration = 0;
+ auto requestsPerIteration = parameters.RequestsPerIteration;
+
+ auto start = TInstant::Now();
+ TInstant iterationStart;
+
+ while ((iterationStart = TInstant::Now()) < start + parameters.TestDuration / parameters.PassCount) {
+ iterationUnderlyingAmount = 0;
+ underlyingRequestCount = 0;
+ i64 iterationIncomingAmount = 0;
+
+ auto spikeProbability = 1.0 / requestsPerIteration;
+
+ for (int i = 0; i < requestsPerIteration; ++i) {
+ if (probabilisticOutcome(engine) < parameters.TryAcquireAllProbability) {
+ Throttler_->TryAcquireAvailable(std::numeric_limits<i64>::max());
+ }
+
+ i64 amount = 0;
+ if (probabilisticOutcome(engine) < spikeProbability) {
+ amount = std::pow(2.0, incomingSpikeLbAmount(engine));
+ } else {
+ amount = incomingAmount(engine);
+ }
+ iterationIncomingAmount += amount;
+ auto outcome = probabilisticOutcome(engine);
+ if (outcome < acquireProbability) {
+ Throttler_->Acquire(amount);
+ } else if (outcome < acquireProbability + tryAcquireProbability) {
+ Throttler_->TryAcquire(amount);
+ } else {
+ replies.emplace_back(Throttler_->Throttle(amount));
+ }
+
+ auto requestFinish = TInstant::Now();
+ auto realDuration = requestFinish - iterationStart;
+ if (realDuration < i * parameters.IterationDuration / requestsPerIteration) {
+ Sleep(i * parameters.IterationDuration / requestsPerIteration - realDuration);
+ }
+ }
+
+ auto iterationFinish = TInstant::Now();
+ auto realDuration = iterationFinish - iterationStart;
+ if (realDuration < parameters.IterationDuration) {
+ Sleep(parameters.IterationDuration - realDuration);
+ }
+
+ int underlyingResponseCount = underlyingResponses(engine);
+ for (int i = underlyingResponseCount; i > 0; --i) {
+ processUnderlyingRequest(parameters.ErrorProbability);
+ }
+
+ ++iteration;
+ if (parameters.IterationsPerStep > 0 && iteration % parameters.IterationsPerStep == 0) {
+ if (iterationIncomingAmount != 0) {
+ EXPECT_LE(iterationUnderlyingAmount / iterationIncomingAmount, parameters.MaxUnderlyingAmountMultiplier);
+ }
+
+ requestsPerIteration *= parameters.StepMultiplier;
+ }
+ }
+
+ while (!requests.empty()) {
+ processUnderlyingRequest(-1.0);
+ }
+
+ auto finish = TInstant::Now();
+ auto totalDuration = finish - start;
+ auto averageUnderlyingRps = processedUnderlyingRequests / totalDuration.SecondsFloat();
+
+ EXPECT_LE(averageUnderlyingRps / Config_->TargetRps, parameters.AllowedRpsOverflowMultiplier);
+
+ for (auto& reply : replies) {
+ if (parameters.ErrorProbability > 0.0) {
+ reply.Get();
+ } else {
+ EXPECT_TRUE(reply.Get().IsOK());
+ }
+ }
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(Stress,
+ TPrefetchingStressTest,
+ testing::Values(
+ TStressParameters {
+ .TestDuration = TDuration::Seconds(50),
+ .IterationsPerStep = 100,
+ .StepMultiplier = 0.1,
+ .AllowedRpsOverflowMultiplier = 3.0,
+ },
+ TStressParameters {
+ .TestDuration = TDuration::Seconds(10),
+ .AllowedRpsOverflowMultiplier = 5.0,
+ .TryAcquireAllProbability = 0.000'1,
+ },
+ TStressParameters{
+ .TestDuration = TDuration::Seconds(1),
+ .AllowedRpsOverflowMultiplier = 20.0,
+ .TryAcquireAllProbability = 0.000'1,
+ },
+ TStressParameters {
+ .TestDuration = TDuration::Seconds(100),
+ .PassCount = 10,
+ .AllowedRpsOverflowMultiplier = 5.0,
+ .TryAcquireAllProbability = 0.000'1,
+ .ErrorProbability = 0.01,
+ },
+ TStressParameters{
+ .TestDuration = TDuration::Seconds(10),
+ .PassCount = 10,
+ .AllowedRpsOverflowMultiplier = 20.0,
+ .TryAcquireAllProbability = 0.000'1,
+ .ErrorProbability = 0.01,
+ }));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/concurrency/unittests/two_level_fair_share_thread_pool_ut.cpp b/yt/yt/core/concurrency/unittests/two_level_fair_share_thread_pool_ut.cpp
new file mode 100644
index 0000000000..66e9c6a3ee
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/two_level_fair_share_thread_pool_ut.cpp
@@ -0,0 +1,45 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/two_level_fair_share_thread_pool.h>
+#include <yt/yt/core/concurrency/new_fair_share_thread_pool.h>
+
+#include <yt/yt/core/actions/invoker.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <util/random/random.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTwoLevelFairShareThreadPoolTest, Configure)
+{
+ auto threadPool = CreateNewTwoLevelFairShareThreadPool(1, "Test");
+ auto counter = std::make_shared<std::atomic<int>>();
+ auto callback = BIND([=] { ++*counter; });
+ std::vector<TFuture<void>> futures;
+
+ const int N = 10000;
+ for (int i = 0; i < N; ++i) {
+ auto invoker = threadPool->GetInvoker(
+ ToString(RandomNumber<size_t>(10)),
+ ToString(RandomNumber<size_t>(10)));
+ futures.push_back(callback.AsyncVia(invoker).Run());
+ if (i % 100 == 0) {
+ threadPool->Configure(RandomNumber<size_t>(10) + 1);
+ }
+ }
+
+ AllSucceeded(std::move(futures))
+ .Get();
+
+ threadPool->Shutdown();
+ EXPECT_EQ(N, counter->load());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
+
diff --git a/yt/yt/core/concurrency/unittests/ya.make b/yt/yt/core/concurrency/unittests/ya.make
new file mode 100644
index 0000000000..710a53e040
--- /dev/null
+++ b/yt/yt/core/concurrency/unittests/ya.make
@@ -0,0 +1,64 @@
+GTEST(unittester-core-concurrency)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ async_barrier_ut.cpp
+ async_rw_lock_ut.cpp
+ async_stream_pipe_ut.cpp
+ async_stream_ut.cpp
+ async_yson_writer_ut.cpp
+ coroutines_ut.cpp
+ count_down_latch_ut.cpp
+ delayed_executor_ut.cpp
+ fair_share_invoker_pool_ut.cpp
+ fair_share_thread_pool_ut.cpp
+ fair_throttler_ut.cpp
+ fls_ut.cpp
+ invoker_alarm_ut.cpp
+ invoker_pool_ut.cpp
+ nonblocking_batch_ut.cpp
+ nonblocking_queue_ut.cpp
+ periodic_ut.cpp
+ propagating_storage_ut.cpp
+ quantized_executor_ut.cpp
+ scheduler_ut.cpp
+ suspendable_action_queue_ut.cpp
+ thread_affinity_ut.cpp
+ thread_pool_ut.cpp
+ thread_pool_poller_ut.cpp
+ throughput_throttler_ut.cpp
+ two_level_fair_share_thread_pool_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/crypto/config.cpp b/yt/yt/core/crypto/config.cpp
new file mode 100644
index 0000000000..1face17aca
--- /dev/null
+++ b/yt/yt/core/crypto/config.cpp
@@ -0,0 +1,38 @@
+#include "config.h"
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPemBlobConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("file_name", &TThis::FileName)
+ .Optional();
+ registrar.Parameter("value", &TThis::Value)
+ .Optional();
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->FileName && config->Value) {
+ THROW_ERROR_EXCEPTION("Cannot specify both \"file_name\" and \"value\"");
+ }
+ if (!config->FileName && !config->Value) {
+ THROW_ERROR_EXCEPTION("Must specify either \"file_name\" or \"value\"");
+ }
+ });
+}
+
+TString TPemBlobConfig::LoadBlob() const
+{
+ if (FileName) {
+ return TFileInput(*FileName).ReadAll();
+ } else if (Value) {
+ return *Value;
+ } else {
+ THROW_ERROR_EXCEPTION("Neither \"file_name\" nor \"value\" is given");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
+
diff --git a/yt/yt/core/crypto/config.h b/yt/yt/core/crypto/config.h
new file mode 100644
index 0000000000..41b79a751b
--- /dev/null
+++ b/yt/yt/core/crypto/config.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Either an inlined value or a file reference.
+class TPemBlobConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<TString> FileName;
+ std::optional<TString> Value;
+
+ TString LoadBlob() const;
+
+ REGISTER_YSON_STRUCT(TPemBlobConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TPemBlobConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
diff --git a/yt/yt/core/crypto/crypto.cpp b/yt/yt/core/crypto/crypto.cpp
new file mode 100644
index 0000000000..b3f35df114
--- /dev/null
+++ b/yt/yt/core/crypto/crypto.cpp
@@ -0,0 +1,331 @@
+#include "crypto.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/string/hex.h>
+
+#include <openssl/hmac.h>
+#include <openssl/md5.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/sha.h>
+#include <openssl/rand.h>
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMD5Hash MD5FromString(TStringBuf data)
+{
+ TMD5Hash hash;
+ if (data.size() != hash.size()) {
+ THROW_ERROR_EXCEPTION("Invalid MD5 hash size")
+ << TErrorAttribute("expected", hash.size())
+ << TErrorAttribute("actual", data.size());
+ }
+
+ std::copy(data.begin(), data.end(), hash.begin());
+ return hash;
+}
+
+static_assert(
+ sizeof(MD5_CTX) == sizeof(TMD5Hasher),
+ "TMD5Hasher size must be exactly equal to that of MD5_CTX");
+
+TMD5Hasher::TMD5Hasher()
+{
+ MD5_Init(reinterpret_cast<MD5_CTX*>(State_.data()));
+}
+
+TMD5Hasher::TMD5Hasher(const TMD5State& data)
+{
+ State_ = data;
+}
+
+TMD5Hasher& TMD5Hasher::Append(TStringBuf data)
+{
+ MD5_Update(reinterpret_cast<MD5_CTX*>(State_.data()), data.data(), data.size());
+ return *this;
+}
+
+TMD5Hasher& TMD5Hasher::Append(TRef data)
+{
+ MD5_Update(reinterpret_cast<MD5_CTX*>(State_.data()), data.Begin(), data.Size());
+ return *this;
+}
+
+TMD5Hash TMD5Hasher::GetDigest()
+{
+ TMD5Hash hash;
+ MD5_Final(
+ reinterpret_cast<unsigned char*>(hash.data()),
+ reinterpret_cast<MD5_CTX*>(State_.data()));
+ return hash;
+}
+
+TString TMD5Hasher::GetHexDigestUpperCase()
+{
+ auto md5 = GetDigest();
+ return HexEncode(md5.data(), md5.size());
+}
+
+TString TMD5Hasher::GetHexDigestLowerCase()
+{
+ return to_lower(GetHexDigestUpperCase());
+}
+
+void TMD5Hasher::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, State_);
+}
+
+const TMD5State& TMD5Hasher::GetState() const
+{
+ return State_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetMD5HexDigestUpperCase(TStringBuf data)
+{
+ TMD5Hasher hasher;
+ hasher.Append(data);
+ return hasher.GetHexDigestUpperCase();
+}
+
+TString GetMD5HexDigestLowerCase(TStringBuf data)
+{
+ TMD5Hasher hasher;
+ hasher.Append(data);
+ return hasher.GetHexDigestLowerCase();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSha1Hash Sha1FromString(TStringBuf data)
+{
+ TSha1Hash hash;
+ if (data.size() != hash.size()) {
+ THROW_ERROR_EXCEPTION("Invalid Sha1 hash size")
+ << TErrorAttribute("expected", hash.size())
+ << TErrorAttribute("actual", data.size());
+ }
+
+ std::copy(data.begin(), data.end(), hash.begin());
+ return hash;
+}
+
+static_assert(
+ sizeof(SHA_CTX) == sizeof(TSha1Hasher),
+ "TSha1Hasher size must be exactly equal to that of SHA1_CTX");
+
+TSha1Hasher::TSha1Hasher()
+{
+ SHA1_Init(reinterpret_cast<SHA_CTX*>(CtxStorage_.data()));
+}
+
+TSha1Hasher& TSha1Hasher::Append(TStringBuf data)
+{
+ SHA1_Update(reinterpret_cast<SHA_CTX*>(CtxStorage_.data()), data.data(), data.size());
+ return *this;
+}
+
+TSha1Hash TSha1Hasher::GetDigest()
+{
+ TSha1Hash hash;
+ SHA1_Final(
+ reinterpret_cast<unsigned char*>(hash.data()),
+ reinterpret_cast<SHA_CTX*>(CtxStorage_.data()));
+ return hash;
+}
+
+TString TSha1Hasher::GetHexDigestUpperCase()
+{
+ auto sha1 = GetDigest();
+ return HexEncode(sha1.data(), sha1.size());
+}
+
+TString TSha1Hasher::GetHexDigestLowerCase()
+{
+ return to_lower(GetHexDigestUpperCase());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSha256Hasher::TSha256Hasher()
+{
+ static_assert(
+ sizeof(CtxStorage_) == sizeof(SHA256_CTX),
+ "Invalid ctx storage size");
+
+ SHA256_Init(reinterpret_cast<SHA256_CTX*>(CtxStorage_.data()));
+}
+
+TSha256Hasher& TSha256Hasher::Append(TStringBuf data)
+{
+ SHA256_Update(
+ reinterpret_cast<SHA256_CTX*>(CtxStorage_.data()),
+ data.data(),
+ data.size());
+ return *this;
+}
+
+TSha256Hasher::TDigest TSha256Hasher::GetDigest()
+{
+ TDigest digest;
+ SHA256_Final(
+ reinterpret_cast<unsigned char*>(digest.data()),
+ reinterpret_cast<SHA256_CTX*>(CtxStorage_.data()));
+ return digest;
+}
+
+TString TSha256Hasher::GetHexDigestUpperCase()
+{
+ auto digest = GetDigest();
+ return HexEncode(digest.data(), digest.size());
+}
+
+TString TSha256Hasher::GetHexDigestLowerCase()
+{
+ return to_lower(GetHexDigestUpperCase());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetSha256HexDigestUpperCase(TStringBuf data)
+{
+ TSha256Hasher hasher;
+ hasher.Append(data);
+ return hasher.GetHexDigestUpperCase();
+}
+
+TString GetSha256HexDigestLowerCase(TStringBuf data)
+{
+ TSha256Hasher hasher;
+ hasher.Append(data);
+ return hasher.GetHexDigestLowerCase();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TSha256Hmac = std::array<char, 256 / 8>;
+
+TSha256Hmac CreateSha256HmacImpl(const TString& key, const TString& message)
+{
+ TSha256Hmac hmac;
+ unsigned int opensslIsInsane;
+ auto result = HMAC(
+ EVP_sha256(),
+ key.data(),
+ key.size(),
+ reinterpret_cast<const unsigned char*>(message.data()),
+ message.size(),
+ reinterpret_cast<unsigned char*>(hmac.data()),
+ &opensslIsInsane);
+ YT_VERIFY(nullptr != result);
+ return hmac;
+}
+
+TString CreateSha256Hmac(const TString& key, const TString& message)
+{
+ auto hmac = CreateSha256HmacImpl(key, message);
+ return to_lower(HexEncode(hmac.data(), hmac.size()));
+}
+
+TString CreateSha256HmacRaw(const TString& key, const TString& message)
+{
+ auto hmac = CreateSha256HmacImpl(key, message);
+ return TString(hmac.data(), hmac.size());
+}
+
+bool ConstantTimeCompare(const TString& trusted, const TString& untrusted)
+{
+ int total = 0;
+
+ size_t i = 0;
+ size_t j = 0;
+ while (true) {
+ total |= trusted[i] ^ untrusted[j];
+
+ if (i == untrusted.size()) {
+ break;
+ }
+
+ ++i;
+
+ if (j < trusted.size()) {
+ ++j;
+ } else {
+ total |= 1;
+ }
+
+ }
+
+ return total == 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString HashPassword(const TString& password, const TString& salt)
+{
+ auto passwordSha256 = GetSha256HexDigestLowerCase(password);
+ return HashPasswordSha256(passwordSha256, salt);
+}
+
+TString HashPasswordSha256(const TString& passwordSha256, const TString& salt)
+{
+ auto saltedPassword = salt + passwordSha256;
+ return GetSha256HexDigestLowerCase(saltedPassword);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GenerateCryptoStrongRandomString(int length)
+{
+ std::vector<unsigned char> bytes(length);
+ if (RAND_bytes(bytes.data(), bytes.size())) {
+ auto data = reinterpret_cast<char*>(bytes.data());
+ return TString{data, static_cast<size_t>(length)};
+ } else {
+ THROW_ERROR_EXCEPTION("Failed to generate %v random bytes")
+ << TErrorAttribute("openssl_error_code", ERR_get_error());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+void ToProto(NCrypto::NProto::TMD5Hasher* protoHasher, const std::optional<NYT::NCrypto::TMD5Hasher>& hasher)
+{
+ auto* outputBytes = protoHasher->mutable_state();
+ outputBytes->clear();
+ if (!hasher) {
+ return;
+ }
+
+ const auto& state = hasher->GetState();
+ outputBytes->assign(state.begin(), state.end());
+}
+
+void FromProto(std::optional<NYT::NCrypto::TMD5Hasher>* hasher, const NCrypto::NProto::TMD5Hasher& protoHasher)
+{
+ const auto& inputBytes = protoHasher.state();
+ if (inputBytes.empty()) {
+ return;
+ }
+
+ TMD5State state;
+ std::copy(inputBytes.begin(), inputBytes.end(), state.data());
+
+ hasher->emplace(state);
+}
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
+
diff --git a/yt/yt/core/crypto/crypto.h b/yt/yt/core/crypto/crypto.h
new file mode 100644
index 0000000000..5ed38394aa
--- /dev/null
+++ b/yt/yt/core/crypto/crypto.h
@@ -0,0 +1,128 @@
+#pragma once
+
+#include <yt/yt_proto/yt/core/crypto/proto/crypto.pb.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/generic/strbuf.h>
+
+#include <array>
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+typedef std::array<char, 16> TMD5Hash;
+typedef std::array<char, 92> TMD5State;
+
+TMD5Hash MD5FromString(TStringBuf data);
+
+class TMD5Hasher
+{
+public:
+ TMD5Hasher();
+ explicit TMD5Hasher(const TMD5State& data);
+
+ TMD5Hasher& Append(TStringBuf data);
+ TMD5Hasher& Append(TRef data);
+
+ TMD5Hash GetDigest();
+ TString GetHexDigestLowerCase();
+ TString GetHexDigestUpperCase();
+
+ const TMD5State& GetState() const;
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ //! Erasing openssl struct type... brutally.
+ TMD5State State_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetMD5HexDigestUpperCase(TStringBuf data);
+TString GetMD5HexDigestLowerCase(TStringBuf data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+typedef std::array<char, 20> TSha1Hash;
+
+TSha1Hash Sha1FromString(TStringBuf data);
+
+class TSha1Hasher
+{
+public:
+ TSha1Hasher();
+
+ TSha1Hasher& Append(TStringBuf data);
+
+ TSha1Hash GetDigest();
+ TString GetHexDigestLowerCase();
+ TString GetHexDigestUpperCase();
+
+private:
+ std::array<char, 96> CtxStorage_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSha256Hasher
+{
+public:
+ TSha256Hasher();
+
+ TSha256Hasher& Append(TStringBuf data);
+
+ using TDigest = std::array<char, 32>;
+ TDigest GetDigest();
+
+ TString GetHexDigestLowerCase();
+ TString GetHexDigestUpperCase();
+
+private:
+ constexpr static int CtxSize = 112;
+ std::array<char, CtxSize> CtxStorage_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetSha256HexDigestUpperCase(TStringBuf data);
+TString GetSha256HexDigestLowerCase(TStringBuf data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString CreateSha256Hmac(const TString& key, const TString& message);
+TString CreateSha256HmacRaw(const TString& key, const TString& message);
+
+bool ConstantTimeCompare(const TString& trusted, const TString& untrusted);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Hashes password with given (random) salt.
+// BEWARE: Think twice before changing this function's semantics!
+TString HashPassword(const TString& password, const TString& salt);
+
+//! Hashes SHA256-hashed password with given (random) salt.
+TString HashPasswordSha256(const TString& passwordSha256, const TString& salt);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns string of given length filled with random bytes fetched
+//! from cryptographically strong generator.
+// NB: May throw on RNG failure.
+TString GenerateCryptoStrongRandomString(int length);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+void ToProto(NCrypto::NProto::TMD5Hasher* protoHasher, const std::optional<NYT::NCrypto::TMD5Hasher>& hasher);
+void FromProto(std::optional<NYT::NCrypto::TMD5Hasher>* hasher, const NCrypto::NProto::TMD5Hasher& protoHasher);
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
diff --git a/yt/yt/core/crypto/public.h b/yt/yt/core/crypto/public.h
new file mode 100644
index 0000000000..2b524c79ef
--- /dev/null
+++ b/yt/yt/core/crypto/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/misc/intrusive_ptr.h>
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TPemBlobConfig)
+DECLARE_REFCOUNTED_CLASS(TSslContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
diff --git a/yt/yt/core/crypto/tls.cpp b/yt/yt/core/crypto/tls.cpp
new file mode 100644
index 0000000000..26c89a0823
--- /dev/null
+++ b/yt/yt/core/crypto/tls.cpp
@@ -0,0 +1,720 @@
+#include "tls.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/poller.h>
+
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/dialer.h>
+#include <yt/yt/core/net/listener.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/openssl/io/stream.h>
+
+#include <openssl/bio.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+
+namespace NYT::NCrypto {
+
+using namespace NNet;
+using namespace NConcurrency;
+using namespace NLogging;
+
+static const TLogger Logger{"Tls"};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TErrorAttribute GetLastSslError()
+{
+ char errorStr[256];
+ ERR_error_string_n(ERR_get_error(), errorStr, sizeof(errorStr));
+ return TErrorAttribute("ssl_error", TString(errorStr));
+}
+
+constexpr auto TlsBufferSize = 1_MB;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSslContextImpl
+ : public TRefCounted
+{
+ SSL_CTX* Ctx = nullptr;
+
+ TSslContextImpl()
+ {
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ Ctx = SSL_CTX_new(TLS_method());
+ if (!Ctx) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_new(TLS_method()) failed")
+ << GetLastSslError();
+ }
+ if (SSL_CTX_set_min_proto_version(Ctx, TLS1_2_VERSION) == 0) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_set_min_proto_version failed")
+ << GetLastSslError();
+ }
+ if (SSL_CTX_set_max_proto_version(Ctx, TLS1_2_VERSION) == 0) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_set_max_proto_version failed")
+ << GetLastSslError();
+ }
+#else
+ Ctx = SSL_CTX_new(TLSv1_2_method());
+ if (!Ctx) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_new(TLSv1_2_method()) failed")
+ << GetLastSslError();
+ }
+#endif
+ }
+
+ ~TSslContextImpl()
+ {
+ if (Ctx) {
+ SSL_CTX_free(Ctx);
+ }
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSslContextImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTlsBufferTag
+{ };
+
+class TTlsConnection
+ : public IConnection
+{
+public:
+ TTlsConnection(
+ TSslContextImplPtr ctx,
+ IPollerPtr poller,
+ IConnectionPtr connection)
+ : Ctx_(std::move(ctx))
+ , Invoker_(CreateSerializedInvoker(poller->GetInvoker(), "crypto_tls_connection"))
+ , Underlying_(std::move(connection))
+ {
+ Ssl_ = SSL_new(Ctx_->Ctx);
+ if (!Ssl_) {
+ THROW_ERROR_EXCEPTION("SSL_new failed")
+ << GetLastSslError();
+ }
+
+ InputBIO_ = BIO_new(BIO_s_mem());
+ YT_VERIFY(InputBIO_);
+ // Makes InputBIO_ non-blocking.
+
+ BIO_set_mem_eof_return(InputBIO_, -1);
+ OutputBIO_ = BIO_new(BIO_s_mem());
+ YT_VERIFY(OutputBIO_);
+
+ SSL_set_bio(Ssl_, InputBIO_, OutputBIO_);
+
+ InputBuffer_ = TSharedMutableRef::Allocate<TTlsBufferTag>(TlsBufferSize);
+ OutputBuffer_ = TSharedMutableRef::Allocate<TTlsBufferTag>(TlsBufferSize);
+ }
+
+ ~TTlsConnection()
+ {
+ SSL_free(Ssl_);
+ }
+
+ void StartClient()
+ {
+ SSL_set_connect_state(Ssl_);
+ auto sslResult = SSL_do_handshake(Ssl_);
+ sslResult = SSL_get_error(Ssl_, sslResult);
+ YT_VERIFY(sslResult == SSL_ERROR_WANT_READ);
+
+ Invoker_->Invoke(BIND(&TTlsConnection::DoRun, MakeStrong(this)));
+ }
+
+ void StartServer()
+ {
+ SSL_set_accept_state(Ssl_);
+
+ Invoker_->Invoke(BIND(&TTlsConnection::DoRun, MakeStrong(this)));
+ }
+
+ int GetHandle() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ i64 GetReadByteCount() const override
+ {
+ return Underlying_->GetReadByteCount();
+ }
+
+ TConnectionStatistics GetReadStatistics() const override
+ {
+ return Underlying_->GetReadStatistics();
+ }
+
+ i64 GetWriteByteCount() const override
+ {
+ return Underlying_->GetWriteByteCount();
+ }
+
+ const TNetworkAddress& LocalAddress() const override
+ {
+ return Underlying_->LocalAddress();
+ }
+
+ const TNetworkAddress& RemoteAddress() const override
+ {
+ return Underlying_->RemoteAddress();
+ }
+
+ TConnectionStatistics GetWriteStatistics() const override
+ {
+ return Underlying_->GetWriteStatistics();
+ }
+
+ void SetReadDeadline(std::optional<TInstant> deadline) override
+ {
+ Underlying_->SetReadDeadline(deadline);
+ }
+
+ void SetWriteDeadline(std::optional<TInstant> deadline) override
+ {
+ Underlying_->SetWriteDeadline(deadline);
+ }
+
+ bool SetNoDelay() override
+ {
+ return Underlying_->SetNoDelay();
+ }
+
+ bool SetKeepAlive() override
+ {
+ return Underlying_->SetKeepAlive();
+ }
+
+ TFuture<size_t> Read(const TSharedMutableRef& buffer) override
+ {
+ auto promise = NewPromise<size_t>();
+ ++ActiveIOCount_;
+ Invoker_->Invoke(BIND([this, this_ = MakeStrong(this), promise, buffer] () {
+ ReadBuffer_ = buffer;
+ ReadPromise_ = promise;
+
+ YT_VERIFY(!ReadActive_);
+ ReadActive_ = true;
+
+ DoRun();
+ }));
+ return promise.ToFuture();
+ }
+
+ TFuture<void> Write(const TSharedRef& buffer) override
+ {
+ return WriteV(TSharedRefArray(buffer));
+ }
+
+ TFuture<void> WriteV(const TSharedRefArray& buffer) override
+ {
+ auto promise = NewPromise<void>();
+ ++ActiveIOCount_;
+ Invoker_->Invoke(BIND([this, this_ = MakeStrong(this), promise, buffer] () {
+ WriteBuffer_ = buffer;
+ WritePromise_ = promise;
+
+ YT_VERIFY(!WriteActive_);
+ WriteActive_ = true;
+
+ DoRun();
+ }));
+ return promise.ToFuture();
+ }
+
+ TFuture<void> CloseRead() override
+ {
+ // TLS does not support half-open connection state.
+ return Close();
+ }
+
+ TFuture<void> CloseWrite() override
+ {
+ // TLS does not support half-open connection state.
+ return Close();
+ }
+
+ TFuture<void> Close() override
+ {
+ ++ActiveIOCount_;
+ return BIND([this, this_ = MakeStrong(this)] () {
+ CloseRequested_ = true;
+
+ DoRun();
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+ bool IsIdle() const override
+ {
+ return ActiveIOCount_ == 0 && !Failed_;
+ }
+
+ TFuture<void> Abort() override
+ {
+ return BIND([this, this_ = MakeStrong(this)] () {
+ if (Error_.IsOK()) {
+ Error_ = TError("TLS connection aborted");
+ CheckError();
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+ void SubscribePeerDisconnect(TCallback<void ()> cb) override
+ {
+ return Underlying_->SubscribePeerDisconnect(std::move(cb));
+ }
+
+private:
+ const TSslContextImplPtr Ctx_;
+ const IInvokerPtr Invoker_;
+ const IConnectionPtr Underlying_;
+
+ SSL* Ssl_ = nullptr;
+ BIO* InputBIO_ = nullptr;
+ BIO* OutputBIO_ = nullptr;
+
+ // This counter gets stuck after streams encounters an error.
+ std::atomic<int> ActiveIOCount_ = {0};
+ std::atomic<bool> Failed_ = {false};
+
+ // FSM
+ TError Error_;
+ bool HandshakeInProgress_ = true;
+ bool CloseRequested_ = false;
+ bool ReadActive_ = false;
+ bool WriteActive_ = false;
+ bool UnderlyingReadActive_ = false;
+ bool UnderlyingWriteActive_ = false;
+
+ TSharedMutableRef InputBuffer_;
+ TSharedMutableRef OutputBuffer_;
+
+ // Active read
+ TSharedMutableRef ReadBuffer_;
+ TPromise<size_t> ReadPromise_;
+
+ // Active write
+ TSharedRefArray WriteBuffer_;
+ TPromise<void> WritePromise_;
+
+
+ void CheckError()
+ {
+ if (Error_.IsOK()) {
+ return;
+ }
+
+ if (ReadActive_) {
+ Failed_ = true;
+ ReadPromise_.Set(Error_);
+ ReadActive_ = false;
+ }
+
+ if (WriteActive_) {
+ Failed_ = true;
+ WritePromise_.Set(Error_);
+ WriteActive_ = false;
+ }
+ }
+
+ template <class T>
+ void HandleUnderlyingIOResult(TFuture<T> future, TCallback<void(const TErrorOr<T>&)> handler)
+ {
+ future.Subscribe(BIND([handler = std::move(handler), invoker = Invoker_] (const TErrorOr<T>& result) {
+ GuardedInvoke(
+ std::move(invoker),
+ BIND(handler, result),
+ BIND([=] {
+ TError error("Poller terminated");
+ handler(error);
+ }));
+ }));
+ }
+
+ void MaybeStartUnderlyingIO(bool sslWantRead)
+ {
+ if (!UnderlyingReadActive_ && sslWantRead) {
+ UnderlyingReadActive_ = true;
+ HandleUnderlyingIOResult(
+ Underlying_->Read(InputBuffer_),
+ BIND([this, this_ = MakeStrong(this)] (const TErrorOr<size_t>& result) {
+ UnderlyingReadActive_ = false;
+ if (result.IsOK()) {
+ if (result.Value() > 0) {
+ int count = BIO_write(InputBIO_, InputBuffer_.Begin(), result.Value());
+ YT_VERIFY(count >= 0);
+ YT_VERIFY(static_cast<size_t>(count) == result.Value());
+ } else {
+ BIO_set_mem_eof_return(InputBIO_, 0);
+ }
+ } else {
+ Error_ = result;
+ }
+
+ DoRun();
+ MaybeStartUnderlyingIO(false);
+ }));
+ }
+
+ if (!UnderlyingWriteActive_ && BIO_ctrl_pending(OutputBIO_)) {
+ UnderlyingWriteActive_ = true;
+
+ int count = BIO_read(OutputBIO_, OutputBuffer_.Begin(), OutputBuffer_.Size());
+ YT_VERIFY(count > 0);
+
+ HandleUnderlyingIOResult(
+ Underlying_->Write(OutputBuffer_.Slice(0, count)),
+ BIND([this, this_ = MakeStrong(this)] (const TError& result) {
+ UnderlyingWriteActive_ = false;
+ if (result.IsOK()) {
+ // Hooray!
+ } else {
+ Error_ = result;
+ }
+
+ DoRun();
+ }));
+ }
+ }
+
+ void DoRun()
+ {
+ CheckError();
+
+ if (CloseRequested_ && !HandshakeInProgress_) {
+ SSL_shutdown(Ssl_);
+ MaybeStartUnderlyingIO(false);
+ }
+
+ // NB: We should check for an error here, because Underylying_ might have failed already, and then
+ // we will loop on SSL_ERROR_WANT_READ forever.
+ if (HandshakeInProgress_ && Error_.IsOK()) {
+ int sslResult = SSL_do_handshake(Ssl_);
+ if (sslResult == 1) {
+ HandshakeInProgress_ = false;
+ } else {
+ int sslError = SSL_get_error(Ssl_, sslResult);
+ if (sslError == SSL_ERROR_WANT_READ) {
+ MaybeStartUnderlyingIO(true);
+ } else {
+ Error_ = TError("SSL_do_handshake failed")
+ << GetLastSslError();
+ YT_LOG_DEBUG(Error_, "TLS handshake failed");
+ CheckError();
+ return;
+ }
+ }
+ }
+
+ if (HandshakeInProgress_) {
+ return;
+ }
+
+ // Second condition acts as a poor-man backpressure.
+ if (WriteActive_ && !UnderlyingWriteActive_) {
+ for (const auto& ref : WriteBuffer_) {
+ int count = SSL_write(Ssl_, ref.Begin(), ref.Size());
+
+ if (count < 0) {
+ Error_ = TError("SSL_write failed")
+ << GetLastSslError();
+ YT_LOG_DEBUG(Error_, "TLS write failed");
+ CheckError();
+ return;
+ }
+
+ YT_VERIFY(count == std::ssize(ref));
+ }
+
+ MaybeStartUnderlyingIO(false);
+
+ WriteActive_ = false;
+ WriteBuffer_.Reset();
+ WritePromise_.Set();
+ WritePromise_.Reset();
+ --ActiveIOCount_;
+ }
+
+ if (ReadActive_) {
+ int count = SSL_read(Ssl_, ReadBuffer_.Begin(), ReadBuffer_.Size());
+ if (count >= 0) {
+ ReadActive_ = false;
+ ReadPromise_.Set(count);
+ ReadPromise_.Reset();
+ ReadBuffer_.Reset();
+ --ActiveIOCount_;
+ } else {
+ int sslError = SSL_get_error(Ssl_, count);
+ if (sslError == SSL_ERROR_WANT_READ) {
+ MaybeStartUnderlyingIO(true);
+ } else {
+ Error_ = TError("SSL_read failed")
+ << GetLastSslError();
+ YT_LOG_DEBUG(Error_, "TLS read failed");
+ CheckError();
+ return;
+ }
+ }
+ }
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTlsConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTlsDialer
+ : public IDialer
+{
+public:
+ TTlsDialer(
+ TSslContextImplPtr ctx,
+ IDialerPtr dialer,
+ IPollerPtr poller)
+ : Ctx_(std::move(ctx))
+ , Underlying_(std::move(dialer))
+ , Poller_(std::move(poller))
+ { }
+
+ TFuture<IConnectionPtr> Dial(const TNetworkAddress& remote) override
+ {
+ return Underlying_->Dial(remote).Apply(BIND([ctx = Ctx_, poller = Poller_] (const IConnectionPtr& underlying) -> IConnectionPtr {
+ auto connection = New<TTlsConnection>(ctx, poller, underlying);
+ connection->StartClient();
+ return connection;
+ }));
+ }
+
+private:
+ const TSslContextImplPtr Ctx_;
+ const IDialerPtr Underlying_;
+ const IPollerPtr Poller_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTlsListener
+ : public IListener
+{
+public:
+ TTlsListener(
+ TSslContextImplPtr ctx,
+ IListenerPtr listener,
+ IPollerPtr poller)
+ : Ctx_(std::move(ctx))
+ , Underlying_(std::move(listener))
+ , Poller_(std::move(poller))
+ { }
+
+ const TNetworkAddress& GetAddress() const override
+ {
+ return Underlying_->GetAddress();
+ }
+
+ TFuture<IConnectionPtr> Accept() override
+ {
+ return Underlying_->Accept().Apply(
+ BIND([ctx = Ctx_, poller = Poller_] (const IConnectionPtr& underlying) -> IConnectionPtr {
+ auto connection = New<TTlsConnection>(ctx, poller, underlying);
+ connection->StartServer();
+ return connection;
+ }));
+ }
+
+ void Shutdown() override
+ {
+ Underlying_->Shutdown();
+ }
+
+private:
+ const TSslContextImplPtr Ctx_;
+ const IListenerPtr Underlying_;
+ const IPollerPtr Poller_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSslContext::TSslContext()
+ : Impl_(New<TSslContextImpl>())
+{ }
+
+void TSslContext::UseBuiltinOpenSslX509Store()
+{
+ SSL_CTX_set_cert_store(Impl_->Ctx, GetBuiltinOpenSslX509Store().Release());
+}
+
+void TSslContext::SetCipherList(const TString& list)
+{
+ if (SSL_CTX_set_cipher_list(Impl_->Ctx, list.data()) == 0) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_set_cipher_list failed")
+ << TErrorAttribute("cipher_list", list)
+ << GetLastSslError();
+ }
+}
+
+void TSslContext::AddCertificateFromFile(const TString& path)
+{
+ if (SSL_CTX_use_certificate_file(Impl_->Ctx, path.c_str(), SSL_FILETYPE_PEM) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_certificate_file failed")
+ << TErrorAttribute("path", path)
+ << GetLastSslError();
+ }
+}
+
+void TSslContext::AddCertificateChainFromFile(const TString& path)
+{
+ if (SSL_CTX_use_certificate_chain_file(Impl_->Ctx, path.c_str()) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_certificate_chain_file failed")
+ << TErrorAttribute("path", path)
+ << GetLastSslError();
+ }
+}
+
+void TSslContext::AddPrivateKeyFromFile(const TString& path)
+{
+ if (SSL_CTX_use_PrivateKey_file(Impl_->Ctx, path.c_str(), SSL_FILETYPE_PEM) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_PrivateKey_file failed")
+ << TErrorAttribute("path", path)
+ << GetLastSslError();
+ }
+}
+
+void TSslContext::AddCertificateChain(const TString& certificateChain)
+{
+ auto bio = BIO_new_mem_buf(certificateChain.c_str(), certificateChain.size());
+ YT_VERIFY(bio);
+ auto freeBio = Finally([&] {
+ BIO_free(bio);
+ });
+
+ auto certificateObject = PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr);
+ if (!certificateObject) {
+ THROW_ERROR_EXCEPTION("PEM_read_bio_X509_AUX failed")
+ << GetLastSslError();
+ }
+ auto freeCertificate = Finally([&] {
+ X509_free(certificateObject);
+ });
+
+ if (SSL_CTX_use_certificate(Impl_->Ctx, certificateObject) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_certificate failed")
+ << GetLastSslError();
+ }
+
+ SSL_CTX_clear_chain_certs(Impl_->Ctx);
+ while (true) {
+ auto chainCertificateObject = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
+ if (!chainCertificateObject) {
+ int err = ERR_peek_last_error();
+ if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
+ ERR_clear_error();
+ break;
+ }
+
+ THROW_ERROR_EXCEPTION("PEM_read_bio_X509")
+ << GetLastSslError();
+ }
+
+ int result = SSL_CTX_add0_chain_cert(Impl_->Ctx, chainCertificateObject);
+ if (!result) {
+ X509_free(chainCertificateObject);
+ THROW_ERROR_EXCEPTION("SSL_CTX_add0_chain_cert")
+ << GetLastSslError();
+ }
+ }
+}
+
+void TSslContext::AddCertificate(const TString& certificate)
+{
+ auto bio = BIO_new_mem_buf(certificate.c_str(), certificate.size());
+ YT_VERIFY(bio);
+ auto freeBio = Finally([&] {
+ BIO_free(bio);
+ });
+
+ auto certificateObject = PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr);
+ if (!certificateObject) {
+ THROW_ERROR_EXCEPTION("PEM_read_bio_X509_AUX")
+ << GetLastSslError();
+ }
+ auto freeCertificate = Finally([&] {
+ X509_free(certificateObject);
+ });
+
+ if (SSL_CTX_use_certificate(Impl_->Ctx, certificateObject) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_certificate failed")
+ << GetLastSslError();
+ }
+}
+
+void TSslContext::AddPrivateKey(const TString& privateKey)
+{
+ auto bio = BIO_new_mem_buf(privateKey.c_str(), privateKey.size());
+ YT_VERIFY(bio);
+ auto freeBio = Finally([&] {
+ BIO_free(bio);
+ });
+
+ auto privateKeyObject = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
+ if (!privateKeyObject) {
+ THROW_ERROR_EXCEPTION("PEM_read_bio_PrivateKey failed")
+ << GetLastSslError();
+ }
+ auto freePrivateKey = Finally([&] {
+ EVP_PKEY_free(privateKeyObject);
+ });
+
+ if (SSL_CTX_use_PrivateKey(Impl_->Ctx, privateKeyObject) != 1) {
+ THROW_ERROR_EXCEPTION("SSL_CTX_use_PrivateKey failed")
+ << GetLastSslError();
+ }
+}
+
+IDialerPtr TSslContext::CreateDialer(
+ const TDialerConfigPtr& config,
+ const IPollerPtr& poller,
+ const TLogger& logger)
+{
+ auto dialer = NNet::CreateDialer(config, poller, logger);
+ return New<TTlsDialer>(Impl_, dialer, poller);
+}
+
+IListenerPtr TSslContext::CreateListener(
+ const TNetworkAddress& at,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor)
+{
+ auto listener = NNet::CreateListener(at, poller, acceptor);
+ return New<TTlsListener>(Impl_, listener, poller);
+}
+
+IListenerPtr TSslContext::CreateListener(
+ const IListenerPtr& underlying,
+ const IPollerPtr& poller)
+{
+ return New<TTlsListener>(Impl_, underlying, poller);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
diff --git a/yt/yt/core/crypto/tls.h b/yt/yt/core/crypto/tls.h
new file mode 100644
index 0000000000..5844d3f021
--- /dev/null
+++ b/yt/yt/core/crypto/tls.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/logging/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+namespace NYT::NCrypto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TSslContextImpl)
+
+class TSslContext
+ : public TRefCounted
+{
+public:
+ TSslContext();
+
+ void UseBuiltinOpenSslX509Store();
+
+ void SetCipherList(const TString& list);
+
+ void AddCertificateFromFile(const TString& path);
+ void AddCertificateChainFromFile(const TString& path);
+ void AddPrivateKeyFromFile(const TString& path);
+
+ void AddCertificate(const TString& certificate);
+ void AddCertificateChain(const TString& certificateChain);
+ void AddPrivateKey(const TString& privateKey);
+
+ NNet::IDialerPtr CreateDialer(
+ const NNet::TDialerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller,
+ const NLogging::TLogger& logger);
+
+ NNet::IListenerPtr CreateListener(
+ const NNet::TNetworkAddress& at,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor);
+
+ NNet::IListenerPtr CreateListener(
+ const NNet::IListenerPtr& underlying,
+ const NConcurrency::IPollerPtr& poller);
+
+private:
+ const TIntrusivePtr<TSslContextImpl> Impl_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSslContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrypto
diff --git a/yt/yt/core/crypto/unittests/crypto_ut.cpp b/yt/yt/core/crypto/unittests/crypto_ut.cpp
new file mode 100644
index 0000000000..ec45f1aed3
--- /dev/null
+++ b/yt/yt/core/crypto/unittests/crypto_ut.cpp
@@ -0,0 +1,83 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/crypto/crypto.h>
+
+#include <limits>
+
+namespace NYT::NCrypto {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSha1Test, Simple)
+{
+ EXPECT_EQ("da39a3ee5e6b4b0d3255bfef95601890afd80709", TSha1Hasher().GetHexDigestLowerCase());
+ EXPECT_EQ("da39a3ee5e6b4b0d3255bfef95601890afd80709", TSha1Hasher().Append("").GetHexDigestLowerCase());
+
+ EXPECT_EQ("a9993e364706816aba3e25717850c26c9cd0d89d", TSha1Hasher().Append("abc").GetHexDigestLowerCase());
+}
+
+TEST(TSha256Test, Simple)
+{
+ EXPECT_EQ(
+ "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b",
+ GetSha256HexDigestLowerCase("secret"));
+ EXPECT_EQ(
+ "2BB80D537B1DA3E38BD30361AA855686BDE0EACD7162FEF6A25FE97BF527A25B",
+ GetSha256HexDigestUpperCase("secret"));
+ EXPECT_EQ(
+ "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721",
+ TSha256Hasher().Append("abc").Append("def").GetHexDigestLowerCase());
+}
+
+TEST(TMD5Test, Simple)
+{
+ EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", TMD5Hasher().GetHexDigestLowerCase());
+ EXPECT_EQ("d41d8cd98f00b204e9800998ecf8427e", TMD5Hasher().Append("").GetHexDigestLowerCase());
+
+ EXPECT_EQ("900150983cd24fb0d6963f7d28e17f72", TMD5Hasher().Append("abc").GetHexDigestLowerCase());
+
+ auto state = TMD5Hasher().Append("abacaba").GetState();
+ TMD5State md5State = {
+ 1, 35, 69, 103, -119, -85, -51, -17, -2, -36, -70, -104, 118, 84, 50,
+ 16, 56, 0, 0, 0, 0, 0, 0, 0, 97, 98, 97, 99, 97, 98, 97, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0,
+ };
+ EXPECT_EQ(state, md5State);
+}
+
+TEST(TEncryptPasswordTest, Simple)
+{
+ // Some canonical values.
+ EXPECT_EQ(
+ "711a9ab5749d53639ed06e66110dd2fe680f977610a1b56b3814d04cbd9e3c51",
+ HashPassword(/*password*/ "pass", /*salt*/ "salt"));
+ EXPECT_EQ(
+ "1679f6a1d0e1fcc1771bfa819e6d3171f73abf7c5aec9d64099f2ada6396414f",
+ HashPassword(/*password*/ "pass", /*salt*/ "another_salt"));
+ EXPECT_EQ(
+ "e821ff94d0202254a1d7dd492e79f82a4c38dfc5782570a560feff066129385e",
+ HashPassword(/*password*/ "another_pass", /*salt*/ "salt"));
+
+ auto passwordSha256 = GetSha256HexDigestLowerCase("pass");
+ EXPECT_EQ(HashPassword("pass", "salt"), HashPasswordSha256(passwordSha256, "salt"));
+}
+
+TEST(TRngTest, Simple)
+{
+ auto firstString = GenerateCryptoStrongRandomString(32);
+ EXPECT_EQ(32u, firstString.size());
+
+ auto secondString = GenerateCryptoStrongRandomString(32);
+ EXPECT_EQ(32u, secondString.size());
+
+ EXPECT_FALSE(firstString == secondString);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NCrypto
+
diff --git a/yt/yt/core/crypto/unittests/tls_ut.cpp b/yt/yt/core/crypto/unittests/tls_ut.cpp
new file mode 100644
index 0000000000..2981b24350
--- /dev/null
+++ b/yt/yt/core/crypto/unittests/tls_ut.cpp
@@ -0,0 +1,114 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/test_key.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/dialer.h>
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/listener.h>
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/private.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <yt/yt/core/rpc/grpc/dispatcher.h>
+
+#include <yt/yt/core/crypto/tls.h>
+
+namespace NYT {
+namespace {
+
+using namespace NCrypto;
+using namespace NRpc;
+using namespace NNet;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTlsTest
+ : public ::testing::Test
+{
+public:
+ TTlsTest()
+ {
+ // Piggybacking on openssl initialization in grpc.
+ GrpcLock = NRpc::NGrpc::TDispatcher::Get()->GetLibraryLock();
+ Context = New<TSslContext>();
+
+ Context->AddCertificate(TestCertificate);
+ Context->AddPrivateKey(TestCertificate);
+
+ Poller = CreateThreadPoolPoller(2, "TlsTest");
+ }
+
+ ~TTlsTest()
+ {
+ Poller->Shutdown();
+ }
+
+ NRpc::NGrpc::TGrpcLibraryLockPtr GrpcLock;
+ TSslContextPtr Context;
+ IPollerPtr Poller;
+};
+
+TEST_F(TTlsTest, CreateContext)
+{
+ // Not that trivial as it seems!
+}
+
+TEST_F(TTlsTest, CreateListener)
+{
+ auto localhost = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = Context->CreateListener(localhost, Poller, Poller);
+}
+
+TEST_F(TTlsTest, CreateDialer)
+{
+ auto config = New<TDialerConfig>();
+ config->SetDefaults();
+ auto dialer = Context->CreateDialer(config, Poller, NetLogger);
+}
+
+TEST_F(TTlsTest, SimplePingPong)
+{
+ auto localhost = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = Context->CreateListener(localhost, Poller, Poller);
+
+ auto config = New<TDialerConfig>();
+ config->SetDefaults();
+ auto dialer = Context->CreateDialer(config, Poller, NetLogger);
+
+ auto asyncFirstSide = dialer->Dial(listener->GetAddress());
+ auto asyncSecondSide = listener->Accept();
+
+ auto firstSide = asyncFirstSide.Get().ValueOrThrow();
+ auto secondSide = asyncSecondSide.Get().ValueOrThrow();
+
+ auto buffer = TSharedRef::FromString(TString("ping"));
+ auto outputBuffer = TSharedMutableRef::Allocate(4);
+
+ auto result = firstSide->Write(buffer).Get();
+ ASSERT_EQ(secondSide->Read(outputBuffer).Get().ValueOrThrow(), 4u);
+ result.ThrowOnError();
+ ASSERT_EQ(ToString(outputBuffer), ToString(buffer));
+
+ secondSide->Write(buffer).Get().ThrowOnError();
+ ASSERT_EQ(firstSide->Read(outputBuffer).Get().ValueOrThrow(), 4u);
+ ASSERT_EQ(ToString(outputBuffer), ToString(buffer));
+
+ WaitFor(firstSide->Close())
+ .ThrowOnError();
+ ASSERT_EQ(secondSide->Read(outputBuffer).Get().ValueOrThrow(), 0u);
+}
+
+TEST(TTlsTestWithoutFixture, LoadCertificateChain)
+{
+ auto grpcLock = NRpc::NGrpc::TDispatcher::Get()->GetLibraryLock();
+ auto context = New<TSslContext>();
+ context->AddCertificateChain(TestCertificateChain);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/crypto/unittests/ya.make b/yt/yt/core/crypto/unittests/ya.make
new file mode 100644
index 0000000000..94604793b3
--- /dev/null
+++ b/yt/yt/core/crypto/unittests/ya.make
@@ -0,0 +1,42 @@
+GTEST(unittester-core-crypto)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ crypto_ut.cpp
+ tls_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/crypto
+ yt/yt/core/rpc/grpc
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/crypto/ya.make b/yt/yt/core/crypto/ya.make
new file mode 100644
index 0000000000..9862acecd8
--- /dev/null
+++ b/yt/yt/core/crypto/ya.make
@@ -0,0 +1,17 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ config.cpp
+ crypto.cpp
+ tls.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+ contrib/libs/openssl
+ library/cpp/openssl/io
+)
+
+END()
diff --git a/yt/yt/core/dns/ares_dns_resolver.cpp b/yt/yt/core/dns/ares_dns_resolver.cpp
new file mode 100644
index 0000000000..dc128f67bf
--- /dev/null
+++ b/yt/yt/core/dns/ares_dns_resolver.cpp
@@ -0,0 +1,545 @@
+#include "dns_resolver.h"
+#include "private.h"
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/invoker.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/moody_camel_concurrent_queue.h>
+
+#include <yt/yt/core/threading/thread.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/threading/notification_handle.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <ares.h>
+
+#include <cmath>
+
+#ifdef _unix_
+ #ifdef _linux_
+ #define YT_DNS_RESOLVER_USE_EPOLL
+ #endif
+
+ #include <arpa/inet.h>
+ #include <netdb.h>
+ #include <netinet/in.h>
+ #include <sys/socket.h>
+
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ #include <sys/epoll.h>
+ #else
+ #include <sys/select.h>
+ #endif
+#endif
+
+#ifdef _win_
+ #include <winsock2.h>
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(NYT::TStringBuilderBase* builder, struct hostent* hostent, TStringBuf /*spec*/)
+{
+ bool empty = true;
+
+ auto appendIf = [&] (bool condition, auto function) {
+ if (condition) {
+ if (!empty) {
+ builder->AppendString(", ");
+ }
+ function();
+ empty = false;
+ }
+ };
+
+ builder->AppendString("{");
+
+ appendIf(hostent->h_name, [&] {
+ builder->AppendString("Canonical: ");
+ builder->AppendString(hostent->h_name);
+ });
+
+ appendIf(hostent->h_aliases, [&] {
+ builder->AppendString("Aliases: {");
+ for (int i = 0; hostent->h_aliases[i]; ++i) {
+ if (i > 0) {
+ builder->AppendString(", ");
+ }
+ builder->AppendString(hostent->h_aliases[i]);
+ }
+ builder->AppendString("}");
+ });
+
+ appendIf(hostent->h_addr_list, [&] {
+ auto stringSize = 0;
+ if (hostent->h_addrtype == AF_INET) {
+ stringSize = INET_ADDRSTRLEN;
+ }
+ if (hostent->h_addrtype == AF_INET6) {
+ stringSize = INET6_ADDRSTRLEN;
+ }
+ builder->AppendString("Addresses: {");
+ for (int i = 0; hostent->h_addr_list[i]; ++i) {
+ if (i > 0) {
+ builder->AppendString(", ");
+ }
+ auto string = builder->Preallocate(stringSize);
+ ares_inet_ntop(
+ hostent->h_addrtype,
+ hostent->h_addr_list[i],
+ string,
+ stringSize);
+ builder->Advance(strlen(string));
+ }
+ builder->AppendString("}");
+ });
+
+ builder->AppendString("}");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT::NDns {
+
+using namespace NConcurrency;
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = DnsLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAresDnsResolver
+ : public IDnsResolver
+{
+public:
+ TAresDnsResolver(
+ int retries,
+ TDuration resolveTimeout,
+ TDuration maxResolveTimeout,
+ TDuration warningTimeout,
+ std::optional<double> jitter)
+ : Retries_(retries)
+ , ResolveTimeout_(resolveTimeout)
+ , MaxResolveTimeout_(maxResolveTimeout)
+ , WarningTimeout_(warningTimeout)
+ , Jitter_(jitter)
+ , ResolverThread_(New<TResolverThread>(this))
+ {
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ EpollFD_ = HandleEintr(epoll_create1, EPOLL_CLOEXEC);
+ YT_VERIFY(EpollFD_ >= 0);
+ #endif
+
+ int wakeupFD = WakeupHandle_.GetFD();
+ OnSocketCreated(wakeupFD, AF_UNSPEC, this);
+ OnSocketStateChanged(this, wakeupFD, 1, 0);
+
+ // Init library globals.
+ // c-ares 1.10+ provides recursive behaviour of init/cleanup.
+ YT_VERIFY(ares_library_init(ARES_LIB_INIT_ALL) == ARES_SUCCESS);
+
+ Zero(Channel_);
+ Zero(Options_);
+
+ // See https://c-ares.haxx.se/ares_init_options.html for full details.
+ int mask = 0;
+ Options_.flags |= ARES_FLAG_STAYOPEN;
+ mask |= ARES_OPT_FLAGS;
+ Options_.timeout = static_cast<int>(ResolveTimeout_.MilliSeconds());
+ mask |= ARES_OPT_TIMEOUTMS;
+
+ // ARES_OPT_MAXTIMEOUTMS and ARES_OPT_JITTER are options from
+ // yandex privately patched (05-ttl, 07-timeouts) version of c-ares.
+ #ifdef ARES_OPT_MAXTIMEOUTMS
+ Options_.maxtimeout = static_cast<int>(MaxResolveTimeout_.MilliSeconds());
+ mask |= ARES_OPT_MAXTIMEOUTMS;
+ #endif
+
+ #ifdef ARES_OPT_JITTER
+ if (Jitter_) {
+ Options_.jitter = llround(*Jitter_ * 1000.0);
+ Options_.jitter_rand_seed = TGuid::Create().Parts32[0];
+ mask |= ARES_OPT_JITTER;
+ }
+ #endif
+
+ Options_.tries = Retries_;
+ mask |= ARES_OPT_TRIES;
+
+ Options_.sock_state_cb = &TAresDnsResolver::OnSocketStateChanged;
+ Options_.sock_state_cb_data = this;
+ mask |= ARES_OPT_SOCK_STATE_CB;
+
+ // Disable lookups from /etc/hosts file. Otherwise, cares re-reads file on every dns query.
+ // That causes issues with memory management, because c-areas uses fopen(), and fopen() calls mmap().
+ Options_.lookups = const_cast<char*>("b");
+
+ YT_VERIFY(ares_init_options(&Channel_, &Options_, mask) == ARES_SUCCESS);
+
+ ares_set_socket_callback(Channel_, &TAresDnsResolver::OnSocketCreated, this);
+ }
+
+ ~TAresDnsResolver()
+ {
+ ResolverThread_->Stop();
+
+ ares_destroy(Channel_);
+
+ // Cleanup library globals.
+ // c-ares 1.10+ provides recursive behaviour of init/cleanup.
+ ares_library_cleanup();
+
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ YT_VERIFY(HandleEintr(close, EpollFD_) == 0);
+ #endif
+ }
+
+ TFuture<TNetworkAddress> Resolve(
+ const TString& hostName,
+ const TDnsResolveOptions& options) override
+ {
+ auto promise = NewPromise<TNetworkAddress>();
+ auto future = promise.ToFuture();
+
+ auto requestId = TGuid::Create();
+ auto timeoutCookie = TDelayedExecutor::Submit(
+ BIND([promise, requestId] {
+ YT_LOG_WARNING("Ares DNS resolve timed out (RequestId: %v)",
+ requestId);
+ promise.TrySet(TError(NNet::EErrorCode::ResolveTimedOut, "Ares DNS resolve timed out"));
+ }),
+ MaxResolveTimeout_);
+
+ RequestCounter_.Increment();
+
+ YT_LOG_DEBUG("Started Ares DNS resolve (RequestId: %v, HostName: %v, Options: %v)",
+ requestId,
+ hostName,
+ options);
+
+ auto request = std::unique_ptr<TResolveRequest>{new TResolveRequest{
+ this,
+ requestId,
+ std::move(promise),
+ hostName,
+ options,
+ {},
+ std::move(timeoutCookie)
+ }};
+
+ Queue_.enqueue(std::move(request));
+
+ WakeupHandle_.Raise();
+
+ if (!ResolverThread_->Start()) {
+ std::unique_ptr<TResolveRequest> request;
+ while (Queue_.try_dequeue(request)) {
+ request->Promise.Set(TError(NYT::EErrorCode::Canceled, "Ares DNS resolver is stopped"));
+ }
+ }
+
+ return future;
+ }
+
+private:
+ const int Retries_;
+ const TDuration ResolveTimeout_;
+ const TDuration MaxResolveTimeout_;
+ const TDuration WarningTimeout_;
+ const std::optional<double> Jitter_;
+
+ const NProfiling::TProfiler Profiler_ = DnsProfiler.WithPrefix("/ares_resolver");
+ const NProfiling::TCounter RequestCounter_ = Profiler_.Counter("/request_count");
+ const NProfiling::TCounter FailureCounter_ = Profiler_.Counter("/failure_count");
+ const NProfiling::TCounter TimeoutCounter_ = Profiler_.Counter("/timeout_count");
+ const NProfiling::TTimeGauge RequestTimeGauge_ = Profiler_.TimeGauge("/request_time");
+
+ struct TResolveRequest
+ {
+ TAresDnsResolver* Owner;
+ TGuid RequestId;
+ TPromise<TNetworkAddress> Promise;
+ TString HostName;
+ TDnsResolveOptions Options;
+ NProfiling::TWallTimer Timer;
+ TDelayedExecutorCookie TimeoutCookie;
+ };
+
+ moodycamel::ConcurrentQueue<std::unique_ptr<TResolveRequest>> Queue_;
+
+#ifdef YT_DNS_RESOLVER_USE_EPOLL
+ int EpollFD_ = -1;
+#endif
+ NThreading::TNotificationHandle WakeupHandle_;
+
+ ares_channel Channel_;
+ ares_options Options_;
+
+ class TResolverThread
+ : public NThreading::TThread
+ {
+ public:
+ explicit TResolverThread(TAresDnsResolver* owner)
+ : TThread("AresDnsResolver")
+ , Owner_(owner)
+ { }
+
+ private:
+ TAresDnsResolver* const Owner_;
+
+ void StopPrologue() override
+ {
+ Owner_->WakeupHandle_.Raise();
+ }
+
+ void ThreadMain() override
+ {
+ constexpr size_t MaxRequestsPerDrain = 100;
+
+ auto drainQueue = [&] {
+ for (size_t iteration = 0; iteration < MaxRequestsPerDrain; ++iteration) {
+ std::unique_ptr<TResolveRequest> request;
+ if (!Owner_->Queue_.try_dequeue(request)) {
+ return true;
+ }
+
+ // Try to reduce number of lookups to save some time.
+ int family = AF_UNSPEC;
+ if (request->Options.EnableIPv4 && !request->Options.EnableIPv6) {
+ family = AF_INET;
+ }
+ if (request->Options.EnableIPv6 && !request->Options.EnableIPv4) {
+ family = AF_INET6;
+ }
+
+ ares_gethostbyname(
+ Owner_->Channel_,
+ request->HostName.c_str(),
+ family,
+ &OnNamedResolvedThunk,
+ request.get());
+
+ // Releasing unique_ptr on a separate line,
+ // because argument evaluation order is not specified.
+ Y_UNUSED(request.release());
+ }
+ return false;
+ };
+
+ while (!IsStopping()) {
+ bool drain = false;
+ constexpr int PollTimeoutMs = 1000;
+
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ constexpr size_t MaxEventsPerPoll = 10;
+ struct epoll_event events[MaxEventsPerPoll];
+ int count = HandleEintr(epoll_wait, Owner_->EpollFD_, events, MaxEventsPerPoll, PollTimeoutMs);
+ YT_VERIFY(count >= 0);
+
+ if (count == 0) {
+ ares_process_fd(Owner_->Channel_, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
+ } else {
+ // According to c-ares implementation this loop would cost O(#dns-servers-total * #dns-servers-active).
+ // Hope that we are not creating too many connections!
+ for (int i = 0; i < count; ++i) {
+ int triggeredFD = events[i].data.fd;
+ if (triggeredFD == Owner_->WakeupHandle_.GetFD()) {
+ drain = true;
+ } else {
+ // If the error events were returned, process both EPOLLIN and EPOLLOUT.
+ int readFD = (events[i].events & (EPOLLIN | EPOLLERR | EPOLLHUP)) ? triggeredFD : ARES_SOCKET_BAD;
+ int writeFD = (events[i].events & (EPOLLOUT | EPOLLERR | EPOLLHUP)) ? triggeredFD : ARES_SOCKET_BAD;
+ ares_process_fd(Owner_->Channel_, readFD, writeFD);
+ }
+ }
+ }
+ #else
+ fd_set readFDs, writeFDs;
+ FD_ZERO(&readFDs);
+ FD_ZERO(&writeFDs);
+
+ int wakeupFD = Owner_->WakeupHandle_.GetFD();
+ int nFDs = ares_fds(Owner_->Channel_, &readFDs, &writeFDs);
+ nFDs = std::max(nFDs, 1 + wakeupFD);
+ FD_SET(wakeupFD, &readFDs);
+
+ // Windows has no such limitation since fd_set there implemented as an array, not as bit fields.
+ // https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
+ #ifndef _win_
+ YT_VERIFY(nFDs <= FD_SETSIZE); // This is inherent limitation by select().
+ #endif
+
+ timeval timeout;
+ timeout.tv_sec = PollTimeoutMs / 1000;
+ timeout.tv_usec = (PollTimeoutMs % 1000) * 1000;
+
+ int result = select(nFDs, &readFDs, &writeFDs, nullptr, &timeout);
+ YT_VERIFY(result >= 0);
+
+ ares_process(Owner_->Channel_, &readFDs, &writeFDs);
+
+ if (FD_ISSET(wakeupFD, &readFDs)) {
+ drain = true;
+ }
+ #endif
+
+ if (drain && drainQueue()) {
+ Owner_->WakeupHandle_.Clear();
+ while (!drainQueue());
+ }
+ }
+
+ // Make sure that there are no more enqueued requests.
+ // We have observed `IsStopping() == true` previously,
+ // which implies that producers no longer pushing to the queue.
+ while (!drainQueue());
+ // Cancel out all pending requests.
+ ares_cancel(Owner_->Channel_);
+ }
+ };
+
+ using TResolverThreadPtr = TIntrusivePtr<TResolverThread>;
+
+ const TResolverThreadPtr ResolverThread_;
+
+
+ static int OnSocketCreated(ares_socket_t socket, int /*type*/, void* opaque)
+ {
+ int result = 0;
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ auto this_ = static_cast<TAresDnsResolver*>(opaque);
+ struct epoll_event event{0, {0}};
+ event.data.fd = socket;
+ result = epoll_ctl(this_->EpollFD_, EPOLL_CTL_ADD, socket, &event);
+ if (result != 0) {
+ YT_LOG_WARNING(TError::FromSystem(), "epoll_ctl() failed in Ares DNS resolver");
+ result = -1;
+ }
+ #else
+ Y_UNUSED(opaque);
+ #ifndef _win_
+ if (socket >= FD_SETSIZE) {
+ YT_LOG_WARNING("File descriptor is out of valid range (FD: %v, Limit: %v)",
+ socket,
+ FD_SETSIZE);
+ result = -1;
+ }
+ #endif
+ #endif
+ return result;
+ }
+
+ static void OnSocketStateChanged(void* opaque, ares_socket_t socket, int readable, int writable)
+ {
+ #ifdef YT_DNS_RESOLVER_USE_EPOLL
+ auto this_ = static_cast<TAresDnsResolver*>(opaque);
+ struct epoll_event event{0, {0}};
+ event.data.fd = socket;
+ int op = EPOLL_CTL_MOD;
+ if (readable) {
+ event.events |= EPOLLIN;
+ }
+ if (writable) {
+ event.events |= EPOLLOUT;
+ }
+ if (!readable && !writable) {
+ op = EPOLL_CTL_DEL;
+ }
+ YT_VERIFY(epoll_ctl(this_->EpollFD_, op, socket, &event) == 0);
+ #else
+ Y_UNUSED(opaque, readable, writable);
+ #ifndef _win_
+ YT_VERIFY(socket < FD_SETSIZE);
+ #endif
+ #endif
+ }
+
+ static void OnNamedResolvedThunk(
+ void* opaque,
+ int status,
+ int timeouts,
+ struct hostent* hostent)
+ {
+ auto* request = static_cast<TResolveRequest*>(opaque);
+ request->Owner->OnNamedResolved(
+ std::unique_ptr<TResolveRequest>(request),
+ status,
+ timeouts,
+ hostent);
+ }
+
+ void OnNamedResolved(
+ std::unique_ptr<TResolveRequest> request,
+ int status,
+ int timeouts,
+ struct hostent* hostent)
+ {
+ TDelayedExecutor::CancelAndClear(request->TimeoutCookie);
+
+ auto elapsed = request->Timer.GetElapsedTime();
+ RequestTimeGauge_.Update(elapsed);
+
+ if (elapsed > WarningTimeout_ || timeouts > 0) {
+ YT_LOG_WARNING("Ares DNS resolve took too long (RequestId: %v, HostName: %v, Timeouts: %v, Elapsed: %v)",
+ request->RequestId,
+ request->HostName,
+ timeouts,
+ elapsed);
+ }
+
+ if (status != ARES_SUCCESS) {
+ YT_LOG_WARNING("Ares DNS resolve failed (RequestId: %v, HostName: %v)",
+ request->RequestId,
+ request->HostName);
+ request->Promise.TrySet(TError("Ares DNS resolve failed for %Qv",
+ request->HostName)
+ << TError(ares_strerror(status)));
+ FailureCounter_.Increment();
+ return;
+ }
+
+ YT_VERIFY(hostent->h_addrtype == AF_INET || hostent->h_addrtype == AF_INET6);
+ YT_VERIFY(hostent->h_addr_list && hostent->h_addr_list[0]);
+
+ TNetworkAddress result(hostent->h_addrtype, hostent->h_addr, hostent->h_length);
+ YT_LOG_DEBUG("Ares DNS resolve completed (RequestId: %v, HostName: %v, Result: %v, Hostent: %v, Elapsed: %v)",
+ request->RequestId,
+ request->HostName,
+ result,
+ hostent,
+ elapsed);
+
+ request->Promise.TrySet(result);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDnsResolverPtr CreateAresDnsResolver(
+ int retries,
+ TDuration resolveTimeout,
+ TDuration maxResolveTimeout,
+ TDuration warningTimeout,
+ std::optional<double> jitter)
+{
+ return New<TAresDnsResolver>(
+ retries,
+ resolveTimeout,
+ maxResolveTimeout,
+ warningTimeout,
+ jitter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
diff --git a/yt/yt/core/dns/ares_dns_resolver.h b/yt/yt/core/dns/ares_dns_resolver.h
new file mode 100644
index 0000000000..43772c47a7
--- /dev/null
+++ b/yt/yt/core/dns/ares_dns_resolver.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDnsResolverPtr CreateAresDnsResolver(
+ int retries,
+ TDuration resolveTimeout,
+ TDuration maxResolveTimeout,
+ TDuration warningTimeout,
+ std::optional<double> jitter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
+
diff --git a/yt/yt/core/dns/dns_resolver.cpp b/yt/yt/core/dns/dns_resolver.cpp
new file mode 100644
index 0000000000..f12d4cf117
--- /dev/null
+++ b/yt/yt/core/dns/dns_resolver.cpp
@@ -0,0 +1,22 @@
+#include "dns_resolver.h"
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TDnsResolveOptions options, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("{EnableIPv4: %v, EnableIPv6: %v}",
+ options.EnableIPv4,
+ options.EnableIPv6);
+}
+
+TString ToString(const TDnsResolveOptions& options)
+{
+ return ToStringViaBuilder(options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
+
diff --git a/yt/yt/core/dns/dns_resolver.h b/yt/yt/core/dns/dns_resolver.h
new file mode 100644
index 0000000000..46ed2f76ab
--- /dev/null
+++ b/yt/yt/core/dns/dns_resolver.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/net/address.h>
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDnsResolveOptions
+{
+ bool EnableIPv4 = true;
+ bool EnableIPv6 = true;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TDnsResolveOptions options, TStringBuf spec);
+TString ToString(const TDnsResolveOptions& options);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IDnsResolver
+ : public TRefCounted
+{
+ virtual TFuture<NNet::TNetworkAddress> Resolve(
+ const TString& hostName,
+ const TDnsResolveOptions& options) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IDnsResolver)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
+
diff --git a/yt/yt/core/dns/private.h b/yt/yt/core/dns/private.h
new file mode 100644
index 0000000000..30869f3a40
--- /dev/null
+++ b/yt/yt/core/dns/private.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger DnsLogger("Dns");
+inline const NProfiling::TProfiler DnsProfiler("/dns");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
diff --git a/yt/yt/core/dns/public.h b/yt/yt/core/dns/public.h
new file mode 100644
index 0000000000..b5db58ebba
--- /dev/null
+++ b/yt/yt/core/dns/public.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NDns {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDnsResolveOptions;
+
+DECLARE_REFCOUNTED_STRUCT(IDnsResolver)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDns
diff --git a/yt/yt/core/http/client.cpp b/yt/yt/core/http/client.cpp
new file mode 100644
index 0000000000..40237c0fcb
--- /dev/null
+++ b/yt/yt/core/http/client.cpp
@@ -0,0 +1,324 @@
+#include "client.h"
+#include "connection_pool.h"
+#include "connection_reuse_helpers.h"
+#include "http.h"
+#include "config.h"
+#include "stream.h"
+#include "private.h"
+
+#include <yt/yt/core/net/dialer.h>
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/connection.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <util/string/cast.h>
+
+namespace NYT::NHttp {
+
+using namespace NConcurrency;
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClient
+ : public IClient
+{
+public:
+ TClient(
+ const TClientConfigPtr& config,
+ const IDialerPtr& dialer,
+ const IInvokerPtr& invoker)
+ : Config_(config)
+ , Dialer_(dialer)
+ , Invoker_(invoker)
+ , ConnectionPool_(New<TConnectionPool>(dialer, config, invoker))
+ { }
+
+ TFuture<IResponsePtr> Get(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Request(EMethod::Get, url, std::nullopt, headers);
+ }
+
+ TFuture<IResponsePtr> Post(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Request(EMethod::Post, url, TSharedRef{body}, headers);
+ }
+
+ TFuture<IResponsePtr> Patch(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Request(EMethod::Patch, url, TSharedRef{body}, headers);
+ }
+
+ TFuture<IResponsePtr> Put(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Request(EMethod::Put, url, TSharedRef{body}, headers);
+ }
+
+ TFuture<IResponsePtr> Delete(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Request(EMethod::Delete, url, std::nullopt, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPost(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return StartRequest(EMethod::Post, url, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPatch(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return StartRequest(EMethod::Patch, url, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPut(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return StartRequest(EMethod::Put, url, headers);
+ }
+
+private:
+ const TClientConfigPtr Config_;
+ const IDialerPtr Dialer_;
+ const IInvokerPtr Invoker_;
+ TConnectionPoolPtr ConnectionPool_;
+
+ static int GetDefaultPort(const TUrlRef& parsedUrl)
+ {
+ if (parsedUrl.Protocol == "https") {
+ return 443;
+ } else {
+ return 80;
+ }
+ }
+
+ TNetworkAddress GetAddress(const TUrlRef& parsedUrl)
+ {
+ auto host = parsedUrl.Host;
+ TNetworkAddress address;
+
+ auto tryIP = TNetworkAddress::TryParse(host);
+ if (tryIP.IsOK()) {
+ address = tryIP.Value();
+ } else {
+ auto asyncAddress = TAddressResolver::Get()->Resolve(ToString(host));
+ address = WaitFor(asyncAddress)
+ .ValueOrThrow();
+ }
+
+ return TNetworkAddress(address, parsedUrl.Port.value_or(GetDefaultPort(parsedUrl)));
+ }
+
+ std::pair<THttpOutputPtr, THttpInputPtr> OpenHttp(const TNetworkAddress& address)
+ {
+ // TODO(aleexfi): Enable connection pool by default
+ if (Config_->MaxIdleConnections == 0) {
+ auto connection = WaitFor(Dialer_->Dial(address)).ValueOrThrow();
+
+ auto input = New<THttpInput>(
+ connection,
+ address,
+ Invoker_,
+ EMessageType::Response,
+ Config_);
+
+ auto output = New<THttpOutput>(
+ connection,
+ EMessageType::Request,
+ Config_);
+
+ return {std::move(output), std::move(input)};
+ } else {
+ auto connection = WaitFor(ConnectionPool_->Connect(address)).ValueOrThrow();
+
+ auto reuseSharedState = New<NDetail::TReusableConnectionState>(connection, ConnectionPool_);
+
+ auto input = New<NDetail::TConnectionReuseWrapper<THttpInput>>(
+ connection,
+ address,
+ Invoker_,
+ EMessageType::Response,
+ Config_);
+ input->SetReusableState(reuseSharedState);
+
+ auto output = New<NDetail::TConnectionReuseWrapper<THttpOutput>>(
+ connection,
+ EMessageType::Request,
+ Config_);
+ output->SetReusableState(reuseSharedState);
+
+ return {std::move(output), std::move(input)};
+ }
+ }
+
+ TString SanitizeUrl(const TString& url)
+ {
+ // Do not expose URL parameters in error attributes.
+ auto urlRef = ParseUrl(url);
+ if (urlRef.PortStr.empty()) {
+ return TString(urlRef.Host) + urlRef.Path;
+ } else {
+ return Format("%v:%v%v", urlRef.Host, urlRef.PortStr, urlRef.Path);
+ }
+ }
+
+ template <typename T>
+ TFuture<T> WrapError(const TString& url, TCallback<T()> action)
+ {
+ return BIND([=, this, this_ = MakeStrong(this)] {
+ try {
+ return action();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("HTTP request failed")
+ << TErrorAttribute("url", SanitizeUrl(url))
+ << ex;
+ }
+ })
+ .AsyncVia(Invoker_)
+ .Run();
+ }
+
+ class TActiveRequest
+ : public IActiveRequest
+ {
+ public:
+ TActiveRequest(
+ THttpOutputPtr request,
+ THttpInputPtr response,
+ TIntrusivePtr<TClient> client,
+ TString url
+ )
+ : Request_(std::move(request))
+ , Response_(std::move(response))
+ , Client_(std::move(client))
+ , Url_(std::move(url))
+ { }
+
+ TFuture<IResponsePtr> Finish() override
+ {
+ return Client_->WrapError(Url_, BIND([this, this_ = MakeStrong(this)] {
+ WaitFor(Request_->Close())
+ .ThrowOnError();
+
+ // Waits for response headers internally.
+ Response_->GetStatusCode();
+
+ return IResponsePtr{Response_};
+ }));
+ }
+
+ NConcurrency::IAsyncOutputStreamPtr GetRequestStream() override
+ {
+ return Request_;
+ }
+
+ IResponsePtr GetResponse() override
+ {
+ return Response_;
+ }
+
+ private:
+ THttpOutputPtr Request_;
+ THttpInputPtr Response_;
+ TIntrusivePtr<TClient> Client_;
+ TString Url_;
+ };
+
+ std::pair<THttpOutputPtr, THttpInputPtr> StartAndWriteHeaders(
+ EMethod method,
+ const TString& url,
+ const THeadersPtr& headers)
+ {
+ THttpOutputPtr request;
+ THttpInputPtr response;
+
+ auto urlRef = ParseUrl(url);
+ auto address = GetAddress(urlRef);
+ std::tie(request, response) = OpenHttp(address);
+
+ request->SetHost(urlRef.Host, urlRef.PortStr);
+ if (headers) {
+ request->SetHeaders(headers);
+ }
+
+ auto requestPath = Format("%v?%v", urlRef.Path, urlRef.RawQuery);
+ request->WriteRequest(method, requestPath);
+
+ return {std::move(request), std::move(response)};
+ }
+
+ TFuture<IActiveRequestPtr> StartRequest(
+ EMethod method,
+ const TString& url,
+ const THeadersPtr& headers)
+ {
+ return WrapError(url, BIND([=, this, this_ = MakeStrong(this)] {
+ auto [request, response] = StartAndWriteHeaders(method, url, headers);
+ return IActiveRequestPtr{New<TActiveRequest>(request, response, this_, url)};
+ }));
+ }
+
+ TFuture<IResponsePtr> Request(
+ EMethod method,
+ const TString& url,
+ const std::optional<TSharedRef>& body,
+ const THeadersPtr& headers)
+ {
+ return WrapError(url, BIND([=, this, this_ = MakeStrong(this)] {
+ auto [request, response] = StartAndWriteHeaders(method, url, headers);
+
+ if (body) {
+ WaitFor(request->WriteBody(*body))
+ .ThrowOnError();
+ } else {
+ WaitFor(request->Close())
+ .ThrowOnError();
+ }
+
+ // Waits for response headers internally.
+ response->GetStatusCode();
+
+ return IResponsePtr(response);
+ }));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const IDialerPtr& dialer,
+ const IInvokerPtr& invoker)
+{
+ return New<TClient>(config, dialer, invoker);
+}
+
+IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const IPollerPtr& poller)
+{
+ return CreateClient(
+ config,
+ CreateDialer(New<TDialerConfig>(), poller, HttpLogger),
+ poller->GetInvoker());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/client.h b/yt/yt/core/http/client.h
new file mode 100644
index 0000000000..d768be22c7
--- /dev/null
+++ b/yt/yt/core/http/client.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class IActiveRequest
+ : public virtual TRefCounted
+{
+public:
+ virtual TFuture<IResponsePtr> Finish() = 0;
+ virtual NConcurrency::IAsyncOutputStreamPtr GetRequestStream() = 0;
+ virtual IResponsePtr GetResponse() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IActiveRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IClient
+ : public virtual TRefCounted
+{
+ virtual TFuture<IResponsePtr> Get(
+ const TString& url,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IResponsePtr> Post(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IResponsePtr> Patch(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IResponsePtr> Put(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IResponsePtr> Delete(
+ const TString& url,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IActiveRequestPtr> StartPost(
+ const TString& url,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IActiveRequestPtr> StartPatch(
+ const TString& url,
+ const THeadersPtr& headers = nullptr) = 0;
+
+ virtual TFuture<IActiveRequestPtr> StartPut(
+ const TString& url,
+ const THeadersPtr& headers = nullptr) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IClient)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const NNet::IDialerPtr& dialer,
+ const IInvokerPtr& invoker);
+IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/config.cpp b/yt/yt/core/http/config.cpp
new file mode 100644
index 0000000000..91b6acce91
--- /dev/null
+++ b/yt/yt/core/http/config.cpp
@@ -0,0 +1,81 @@
+#include "config.h"
+
+#include <yt/yt/core/net/config.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THttpIOConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("read_buffer_size", &TThis::ReadBufferSize)
+ .Default(128_KB);
+
+ registrar.Parameter("connection_idle_timeout", &TThis::ConnectionIdleTimeout)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("header_read_timeout", &TThis::HeaderReadTimeout)
+ .Default(TDuration::Seconds(30));
+
+ registrar.Parameter("body_read_idle_timeout", &TThis::BodyReadIdleTimeout)
+ .Default(TDuration::Minutes(5));
+
+ registrar.Parameter("write_idle_timeout", &TThis::WriteIdleTimeout)
+ .Default(TDuration::Minutes(5));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("port", &TThis::Port)
+ .Default(80);
+
+ registrar.Parameter("max_simultaneous_connections", &TThis::MaxSimultaneousConnections)
+ .Default(50000);
+
+ registrar.Parameter("max_backlog_size", &TThis::MaxBacklogSize)
+ .Default(8192);
+
+ registrar.Parameter("bind_retry_count", &TThis::BindRetryCount)
+ .Default(5);
+
+ registrar.Parameter("bind_retry_backoff", &TThis::BindRetryBackoff)
+ .Default(TDuration::Seconds(1));
+
+ registrar.Parameter("enable_keep_alive", &TThis::EnableKeepAlive)
+ .Default(true);
+
+ registrar.Parameter("cancel_fiber_on_connection_close", &TThis::CancelFiberOnConnectionClose)
+ .Default(false);
+
+ registrar.Parameter("nodelay", &TThis::NoDelay)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TClientConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_idle_connections", &TThis::MaxIdleConnections)
+ .Default(0)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("dialer", &TThis::Dialer)
+ .DefaultNew();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCorsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("disable_cors_check", &TThis::DisableCorsCheck)
+ .Default(false);
+ registrar.Parameter("host_allow_list", &TThis::HostAllowList)
+ .Default({"localhost"});
+ registrar.Parameter("host_suffix_allow_list", &TThis::HostSuffixAllowList)
+ .Default({".yandex-team.ru"});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/config.h b/yt/yt/core/http/config.h
new file mode 100644
index 0000000000..a18a480ff8
--- /dev/null
+++ b/yt/yt/core/http/config.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpIOConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ int ReadBufferSize;
+
+ TDuration ConnectionIdleTimeout;
+
+ TDuration HeaderReadTimeout;
+ TDuration BodyReadIdleTimeout;
+
+ TDuration WriteIdleTimeout;
+
+ REGISTER_YSON_STRUCT(THttpIOConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THttpIOConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerConfig
+ : public THttpIOConfig
+{
+public:
+ //! If zero then the port is chosen automatically.
+ int Port;
+
+ //! Limit for number of open TCP connections.
+ int MaxSimultaneousConnections;
+ int MaxBacklogSize;
+
+ int BindRetryCount;
+ TDuration BindRetryBackoff;
+
+ bool EnableKeepAlive;
+
+ bool CancelFiberOnConnectionClose;
+
+ //! Disables Nagle's algorithm.
+ bool NoDelay;
+
+ //! This field is not accessible from config.
+ bool IsHttps = false;
+
+ //! Used for thread naming.
+ //! CamelCase identifiers are preferred.
+ //! This field is not accessible from config.
+ TString ServerName = "Http";
+
+ REGISTER_YSON_STRUCT(TServerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientConfig
+ : public THttpIOConfig
+{
+public:
+ int MaxIdleConnections;
+ NNet::TDialerConfigPtr Dialer;
+
+ REGISTER_YSON_STRUCT(TClientConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCorsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool DisableCorsCheck;
+ std::vector<TString> HostAllowList;
+ std::vector<TString> HostSuffixAllowList;
+
+ REGISTER_YSON_STRUCT(TCorsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TCorsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/connection_pool.cpp b/yt/yt/core/http/connection_pool.cpp
new file mode 100644
index 0000000000..b49afe57ec
--- /dev/null
+++ b/yt/yt/core/http/connection_pool.cpp
@@ -0,0 +1,95 @@
+#include "connection_pool.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/net/connection.h>
+
+namespace NYT::NHttp {
+
+using namespace NNet;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDuration TIdleConnection::GetIdleTime() const
+{
+ return TInstant::Now() - InsertionTime;
+}
+
+bool TIdleConnection::IsOK() const
+{
+ return Connection->IsIdle();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConnectionPool::TConnectionPool(
+ IDialerPtr dialer,
+ TClientConfigPtr config,
+ IInvokerPtr invoker)
+ : Dialer_(std::move(dialer))
+ , Config_(std::move(config))
+ , Connections_(Config_->MaxIdleConnections)
+ , ExpiredConnectionsCollector_(
+ New<TPeriodicExecutor>(
+ std::move(invoker),
+ BIND([weakThis = MakeWeak(this)] {
+ auto this_ = weakThis.Lock();
+ if (this_) {
+ this_->DropExpiredConnections();
+ }
+ }),
+ TPeriodicExecutorOptions::WithJitter(
+ Config_->ConnectionIdleTimeout)))
+{
+ if (Config_->MaxIdleConnections > 0) {
+ ExpiredConnectionsCollector_->Start();
+ }
+}
+
+TConnectionPool::~TConnectionPool()
+{
+ YT_UNUSED_FUTURE(ExpiredConnectionsCollector_->Stop());
+}
+
+TFuture<IConnectionPtr> TConnectionPool::Connect(const TNetworkAddress& address)
+{
+ {
+ auto guard = Guard(SpinLock_);
+
+ while (auto item = Connections_.Extract(address)) {
+ if (item->GetIdleTime() < Config_->ConnectionIdleTimeout && item->IsOK()) {
+ return MakeFuture<IConnectionPtr>(std::move(item->Connection));
+ }
+ }
+ }
+
+ return Dialer_->Dial(address);
+}
+
+void TConnectionPool::Release(const IConnectionPtr& connection)
+{
+ auto guard = Guard(SpinLock_);
+ Connections_.Insert(connection->RemoteAddress(), {connection, TInstant::Now()});
+}
+
+void TConnectionPool::DropExpiredConnections()
+{
+ auto guard = Guard(SpinLock_);
+
+ TMultiLruCache<TNetworkAddress, TIdleConnection> validConnections(
+ Config_->MaxIdleConnections);
+
+ while (Connections_.GetSize() > 0) {
+ auto idleConnection = Connections_.Pop();
+ if (idleConnection.GetIdleTime() < Config_->ConnectionIdleTimeout && idleConnection.IsOK()) {
+ validConnections.Insert(idleConnection.Connection->RemoteAddress(), idleConnection);
+ }
+ }
+
+ Connections_ = std::move(validConnections);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/connection_pool.h b/yt/yt/core/http/connection_pool.h
new file mode 100644
index 0000000000..b7c06245bd
--- /dev/null
+++ b/yt/yt/core/http/connection_pool.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "config.h"
+#include "stream.h"
+
+#include <yt/yt/core/misc/sync_cache.h>
+
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/dialer.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TIdleConnection
+{
+ NNet::IConnectionPtr Connection;
+ TInstant InsertionTime;
+
+ TDuration GetIdleTime() const;
+ bool IsOK() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConnectionPool
+ : public TRefCounted
+{
+public:
+ TConnectionPool(
+ NNet::IDialerPtr dialer,
+ TClientConfigPtr config,
+ IInvokerPtr invoker);
+
+ ~TConnectionPool();
+
+ TFuture<NNet::IConnectionPtr> Connect(const NNet::TNetworkAddress& address);
+
+ void Release(const NNet::IConnectionPtr& connection);
+
+private:
+ const NNet::IDialerPtr Dialer_;
+ const TClientConfigPtr Config_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TMultiLruCache<NNet::TNetworkAddress, TIdleConnection> Connections_;
+ NConcurrency::TPeriodicExecutorPtr ExpiredConnectionsCollector_;
+
+ void DropExpiredConnections();
+};
+
+DEFINE_REFCOUNTED_TYPE(TConnectionPool)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/connection_reuse_helpers-inl.h b/yt/yt/core/http/connection_reuse_helpers-inl.h
new file mode 100644
index 0000000000..0b39855ecf
--- /dev/null
+++ b/yt/yt/core/http/connection_reuse_helpers-inl.h
@@ -0,0 +1,29 @@
+#ifndef CONNECTION_REUSE_HELPERS_INL_H
+#error "Direct inclusion of this file is not allowed, include connection_reuse_helpers.h"
+// For the sake of sane code completion.
+#include "connection_reuse_helpers.h"
+#endif
+
+namespace NYT::NHttp::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TConnectionReuseWrapper<T>::~TConnectionReuseWrapper()
+{
+ if (T::IsSafeToReuse()) {
+ T::Reset();
+ } else if (ReusableState_) {
+ ReusableState_->Reusable = false;
+ }
+}
+
+template <class T>
+void TConnectionReuseWrapper<T>::SetReusableState(TReusableConnectionStatePtr reusableState)
+{
+ ReusableState_ = std::move(reusableState);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp::NDetail
diff --git a/yt/yt/core/http/connection_reuse_helpers.cpp b/yt/yt/core/http/connection_reuse_helpers.cpp
new file mode 100644
index 0000000000..2f500046c4
--- /dev/null
+++ b/yt/yt/core/http/connection_reuse_helpers.cpp
@@ -0,0 +1,27 @@
+#include "connection_reuse_helpers.h"
+
+#include "connection_pool.h"
+
+#include <yt/yt/core/net/connection.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT::NHttp::NDetail {
+
+TReusableConnectionState::TReusableConnectionState(
+ NNet::IConnectionPtr connection,
+ TConnectionPoolPtr owningPool)
+ : Connection(std::move(connection))
+ , OwningPool(std::move(owningPool))
+{ }
+
+TReusableConnectionState::~TReusableConnectionState()
+{
+ if (Reusable && OwningPool && Connection->IsIdle()) {
+ OwningPool->Release(std::move(Connection));
+ }
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/http/connection_reuse_helpers.h b/yt/yt/core/http/connection_reuse_helpers.h
new file mode 100644
index 0000000000..5ac736166c
--- /dev/null
+++ b/yt/yt/core/http/connection_reuse_helpers.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/public.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT::NHttp::NDetail {
+
+//! Responsible for returning the connection to the owning pool
+//! if it could be reused
+struct TReusableConnectionState final
+{
+ std::atomic<bool> Reusable = true;
+ NNet::IConnectionPtr Connection;
+ TConnectionPoolPtr OwningPool;
+
+ TReusableConnectionState(NNet::IConnectionPtr connection, TConnectionPoolPtr owningPool);
+ ~TReusableConnectionState();
+};
+
+using TReusableConnectionStatePtr = TIntrusivePtr<TReusableConnectionState>;
+
+//! Reports to the shared state whether the connection could be reused
+//! (by calling T::IsSafeToReuse() in the destructor)
+template <class T>
+class TConnectionReuseWrapper
+ : public T
+{
+public:
+ using T::T;
+
+ ~TConnectionReuseWrapper() override;
+
+ void SetReusableState(TReusableConnectionStatePtr reusableState);
+
+private:
+ TReusableConnectionStatePtr ReusableState_;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define CONNECTION_REUSE_HELPERS_INL_H
+#include "connection_reuse_helpers-inl.h"
+#undef CONNECTION_REUSE_HELPERS_INL_H
diff --git a/yt/yt/core/http/helpers.cpp b/yt/yt/core/http/helpers.cpp
new file mode 100644
index 0000000000..71f96dc28e
--- /dev/null
+++ b/yt/yt/core/http/helpers.cpp
@@ -0,0 +1,466 @@
+#include "helpers.h"
+
+#include "http.h"
+#include "private.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/json/json_writer.h>
+#include <yt/yt/core/json/json_parser.h>
+#include <yt/yt/core/json/config.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <util/stream/buffer.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/string/strip.h>
+#include <util/string/join.h>
+#include <util/string/cast.h>
+#include <util/string/split.h>
+
+namespace NYT::NHttp {
+
+static const auto& Logger = HttpLogger;
+
+using namespace NJson;
+using namespace NYson;
+using namespace NYTree;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TString XYTErrorHeaderName("X-YT-Error");
+static const TString XYTResponseCodeHeaderName("X-YT-Response-Code");
+static const TString XYTResponseMessageHeaderName("X-YT-Response-Message");
+static const TString AccessControlAllowCredentialsHeaderName("Access-Control-Allow-Credentials");
+static const TString AccessControlAllowOriginHeaderName("Access-Control-Allow-Origin");
+static const TString AccessControlAllowMethodsHeaderName("Access-Control-Allow-Methods");
+static const TString AccessControlMaxAgeHeaderName("Access-Control-Max-Age");
+static const TString AccessControlAllowHeadersHeaderName("Access-Control-Allow-Headers");
+static const TString AccessControlExposeHeadersHeaderName("Access-Control-Expose-Headers");
+static const TString XSourcePortYHeaderName("X-Source-Port-Y");
+static const TString XForwardedForYHeaderName("X-Forwarded-For-Y");
+static const TString ContentTypeHeaderName("Content-Type");
+static const TString ContentRangeHeaderName("Content-Range");
+static const TString PragmaHeaderName("Pragma");
+static const TString RangeHeaderName("Range");
+static const TString ExpiresHeaderName("Expires");
+static const TString CacheControlHeaderName("Cache-Control");
+static const TString XContentTypeOptionsHeaderName("X-Content-Type-Options");
+static const TString XFrameOptionsHeaderName("X-Frame-Options");
+static const TString XDnsPrefetchControlHeaderName("X-DNS-Prefetch-Control");
+static const TString XYTRequestIdHeaderName("X-YT-Request-Id");
+static const TString XYTTraceIdHeaderName("X-YT-Trace-Id");
+static const TString XYTSpanIdHeaderName("X-YT-Span-Id");
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FillYTError(const THeadersPtr& headers, const TError& error)
+{
+ TString errorJson;
+ TStringOutput errorJsonOutput(errorJson);
+ auto jsonWriter = CreateJsonConsumer(&errorJsonOutput);
+ Serialize(error, jsonWriter.get());
+ jsonWriter->Flush();
+
+ headers->Add(XYTErrorHeaderName, errorJson);
+ headers->Add(XYTResponseCodeHeaderName, ToString(static_cast<int>(error.GetCode())));
+ headers->Add(XYTResponseMessageHeaderName, EscapeHeaderValue(error.GetMessage()));
+}
+
+void FillYTErrorHeaders(const IResponseWriterPtr& rsp, const TError& error)
+{
+ FillYTError(rsp->GetHeaders(), error);
+}
+
+void FillYTErrorTrailers(const IResponseWriterPtr& rsp, const TError& error)
+{
+ FillYTError(rsp->GetTrailers(), error);
+}
+
+TError ParseYTError(const IResponsePtr& rsp, bool fromTrailers)
+{
+ TString source;
+ const TString* errorHeader;
+ if (fromTrailers) {
+ static const TString TrailerSource("trailer");
+ source = TrailerSource;
+ errorHeader = rsp->GetTrailers()->Find(XYTErrorHeaderName);
+ } else {
+ static const TString HeaderSource("header");
+ source = HeaderSource;
+ errorHeader = rsp->GetHeaders()->Find(XYTErrorHeaderName);
+ }
+
+ TString errorJson;
+ if (errorHeader) {
+ errorJson = *errorHeader;
+ } else {
+ static const TString BodySource("body");
+ source = BodySource;
+ errorJson = ToString(rsp->ReadAll());
+ }
+
+ TStringInput errorJsonInput(errorJson);
+ std::unique_ptr<IBuildingYsonConsumer<TError>> buildingConsumer;
+ CreateBuildingYsonConsumer(&buildingConsumer, EYsonType::Node);
+ try {
+ ParseJson(&errorJsonInput, buildingConsumer.get());
+ } catch (const std::exception& ex) {
+ return TError("Failed to parse error from response")
+ << TErrorAttribute("source", source)
+ << ex;
+ }
+ return buildingConsumer->Finish();
+}
+
+class TErrorWrappingHttpHandler
+ : public virtual IHttpHandler
+{
+public:
+ explicit TErrorWrappingHttpHandler(IHttpHandlerPtr underlying)
+ : Underlying_(std::move(underlying))
+ { }
+
+ void HandleRequest(
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp) override
+ {
+ try {
+ Underlying_->HandleRequest(req, rsp);
+ } catch(const std::exception& ex) {
+ TError error(ex);
+
+ YT_LOG_DEBUG(error, "Error handling HTTP request (Path: %v)",
+ req->GetUrl().Path);
+
+ FillYTErrorHeaders(rsp, error);
+ rsp->SetStatus(EStatusCode::InternalServerError);
+
+ WaitFor(rsp->Close())
+ .ThrowOnError();
+ }
+ }
+
+private:
+ const IHttpHandlerPtr Underlying_;
+};
+
+IHttpHandlerPtr WrapYTException(IHttpHandlerPtr underlying)
+{
+ return New<TErrorWrappingHttpHandler>(std::move(underlying));
+}
+
+static const auto HeadersWhitelist = JoinSeq(", ", std::vector<TString>{
+ "Authorization",
+ "Origin",
+ "Content-Type",
+ "Accept",
+ "Cache-Control",
+ "X-Csrf-Token",
+ "X-YT-Parameters",
+ "X-YT-Parameters0",
+ "X-YT-Parameters-0",
+ "X-YT-Parameters1",
+ "X-YT-Parameters-1",
+ "X-YT-Response-Parameters",
+ "X-YT-Input-Format",
+ "X-YT-Input-Format0",
+ "X-YT-Input-Format-0",
+ "X-YT-Output-Format",
+ "X-YT-Output-Format0",
+ "X-YT-Output-Format-0",
+ "X-YT-Header-Format",
+ "X-YT-Suppress-Redirect",
+ "X-YT-Omit-Trailers",
+ "X-YT-Request-Format-Options",
+ "X-YT-Response-Format-Options",
+ "X-YT-Request-Id",
+ "X-YT-Error",
+ "X-YT-Response-Code",
+ "X-YT-Response-Message",
+ "X-YT-Trace-Id",
+ "X-YT-User-Tag",
+});
+
+bool MaybeHandleCors(
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp,
+ const TCorsConfigPtr& config)
+{
+ auto origin = req->GetHeaders()->Find("Origin");
+ if (origin) {
+ auto url = ParseUrl(*origin);
+
+ bool allow = false;
+ if (config->DisableCorsCheck) {
+ allow = true;
+ }
+
+ for (const auto& host : config->HostAllowList) {
+ if (host == url.Host) {
+ allow = true;
+ }
+ }
+
+ for (const auto& suffix : config->HostSuffixAllowList) {
+ if (url.Host.EndsWith(suffix)) {
+ allow = true;
+ }
+ }
+
+ if (allow) {
+ rsp->GetHeaders()->Add(AccessControlAllowCredentialsHeaderName, "true");
+ rsp->GetHeaders()->Add(AccessControlAllowOriginHeaderName, *origin);
+ rsp->GetHeaders()->Add(AccessControlAllowMethodsHeaderName, "POST, PUT, GET, OPTIONS");
+ rsp->GetHeaders()->Add(AccessControlMaxAgeHeaderName, "3600");
+
+ if (req->GetMethod() == EMethod::Options) {
+ rsp->GetHeaders()->Add(AccessControlAllowHeadersHeaderName, HeadersWhitelist);
+ rsp->SetStatus(EStatusCode::OK);
+ WaitFor(rsp->Close())
+ .ThrowOnError();
+ return true;
+ } else {
+ rsp->GetHeaders()->Add(AccessControlExposeHeadersHeaderName, HeadersWhitelist);
+ }
+ }
+ }
+
+ return false;
+}
+
+THashMap<TString, TString> ParseCookies(TStringBuf cookies)
+{
+ THashMap<TString, TString> map;
+ size_t index = 0;
+ while (index < cookies.size()) {
+ auto nameStartIndex = index;
+ auto nameEndIndex = cookies.find('=', index);
+ if (nameEndIndex == TString::npos) {
+ THROW_ERROR_EXCEPTION("Malformed cookies");
+ }
+ auto name = StripString(cookies.substr(nameStartIndex, nameEndIndex - nameStartIndex));
+
+ auto valueStartIndex = nameEndIndex + 1;
+ auto valueEndIndex = cookies.find(';', valueStartIndex);
+ if (valueEndIndex == TString::npos) {
+ valueEndIndex = cookies.size();
+ }
+ auto value = StripString(cookies.substr(valueStartIndex, valueEndIndex - valueStartIndex));
+
+ map.emplace(TString(name), TString(value));
+
+ index = valueEndIndex + 1;
+ }
+ return map;
+}
+
+void ProtectCsrfToken(const IResponseWriterPtr& rsp)
+{
+ const auto& headers = rsp->GetHeaders();
+
+ headers->Set(PragmaHeaderName, "nocache");
+ headers->Set(ExpiresHeaderName, "Thu, 01 Jan 1970 00:00:01 GMT");
+ headers->Set(CacheControlHeaderName, "max-age=0, must-revalidate, proxy-revalidate, no-cache, no-store, private");
+ headers->Set(XContentTypeOptionsHeaderName, "nosniff");
+ headers->Set(XFrameOptionsHeaderName, "SAMEORIGIN");
+ headers->Set(XDnsPrefetchControlHeaderName, "off");
+}
+
+std::optional<TString> FindHeader(const IRequestPtr& req, const TString& headerName)
+{
+ auto header = req->GetHeaders()->Find(headerName);
+ return header ? std::make_optional(*header) : std::nullopt;
+}
+
+std::optional<TString> FindBalancerRequestId(const IRequestPtr& req)
+{
+ return FindHeader(req, "X-Req-Id");
+}
+
+std::optional<TString> FindBalancerRealIP(const IRequestPtr& req)
+{
+ const auto& headers = req->GetHeaders();
+
+ auto forwardedFor = headers->Find(XForwardedForYHeaderName);
+ auto sourcePort = headers->Find(XSourcePortYHeaderName);
+
+ if (forwardedFor && sourcePort) {
+ return Format("[%v]:%v", *forwardedFor, *sourcePort);
+ }
+
+ return {};
+}
+
+std::optional<TString> FindUserAgent(const IRequestPtr& req)
+{
+ return FindHeader(req, "User-Agent");
+}
+
+void SetUserAgent(const THeadersPtr& headers, const TString& value)
+{
+ headers->Set("User-Agent", value);
+}
+
+void ReplyJson(const IResponseWriterPtr& rsp, std::function<void(NYson::IYsonConsumer*)> producer)
+{
+ rsp->GetHeaders()->Set(ContentTypeHeaderName, "application/json");
+
+ TBufferOutput out;
+
+ auto json = NJson::CreateJsonConsumer(&out);
+ producer(json.get());
+ json->Flush();
+
+ TString body;
+ out.Buffer().AsString(body);
+ WaitFor(rsp->WriteBody(TSharedRef::FromString(body)))
+ .ThrowOnError();
+}
+
+void ReplyError(const IResponseWriterPtr& response, const TError& error)
+{
+ FillYTErrorHeaders(response, error);
+ ReplyJson(response, [&] (NYson::IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .Value(error);
+ });
+}
+
+NTracing::TTraceId GetTraceId(const IRequestPtr& req)
+{
+ const auto& headers = req->GetHeaders();
+ auto id = headers->Find(XYTTraceIdHeaderName);
+ if (!id) {
+ return NTracing::InvalidTraceId;
+ }
+
+ NTracing::TTraceId traceId;
+ if (!NTracing::TTraceId::FromString(*id, &traceId)) {
+ return NTracing::InvalidTraceId;
+ }
+ return traceId;
+}
+
+void SetTraceId(const IResponseWriterPtr& rsp, NTracing::TTraceId traceId)
+{
+ if (traceId != NTracing::InvalidTraceId) {
+ rsp->GetHeaders()->Set(XYTTraceIdHeaderName, ToString(traceId));
+ }
+}
+
+void SetRequestId(const IResponseWriterPtr& rsp, NRpc::TRequestId requestId)
+{
+ if (requestId) {
+ rsp->GetHeaders()->Set(XYTRequestIdHeaderName, ToString(requestId));
+ }
+}
+
+NTracing::TSpanId GetSpanId(const IRequestPtr& req)
+{
+ const auto& headers = req->GetHeaders();
+ auto id = headers->Find(XYTSpanIdHeaderName);
+ if (!id) {
+ return NTracing::InvalidSpanId;
+ }
+ return IntFromString<NTracing::TSpanId, 16>(*id);
+}
+
+bool TryParseTraceParent(const TString& traceParent, NTracing::TSpanContext& spanContext)
+{
+ // An adaptation of https://github.com/census-instrumentation/opencensus-go/blob/ae11cd04b/plugin/ochttp/propagation/tracecontext/propagation.go#L49-L106
+
+ auto parts = StringSplitter(traceParent).Split('-').ToList<TString>();
+ if (parts.size() < 3 || parts.size() > 4) {
+ return false;
+ }
+
+ // NB: we support three-part form in which version is assumed to be zero.
+ ui8 version = 0;
+ if (parts.size() == 4) {
+ if (parts[0].size() != 2) {
+ return false;
+ }
+ if (!TryIntFromString<10>(parts[0], version)) {
+ return false;
+ }
+ parts.erase(parts.begin());
+ }
+
+ // Now we have exactly three parts: traceId-spanId-options.
+
+ // Parse trace context.
+ if (!TGuid::FromStringHex32(parts[0], &spanContext.TraceId)) {
+ return false;
+ }
+
+ if (parts[1].size() != 16) {
+ return false;
+ }
+ if (!TryIntFromString<16>(parts[1], spanContext.SpanId)) {
+ return false;
+ }
+
+ ui8 options = 0;
+ if (!TryIntFromString<16>(parts[2], options)) {
+ return false;
+ }
+ spanContext.Sampled = static_cast<bool>(options & 1u);
+ spanContext.Debug = static_cast<bool>(options & 2u);
+
+ return true;
+}
+
+NTracing::TTraceContextPtr GetOrCreateTraceContext(const IRequestPtr& req)
+{
+ const auto& headers = req->GetHeaders();
+ NTracing::TTraceContextPtr traceContext;
+ if (auto* traceParent = headers->Find("traceparent")) {
+ NTracing::TSpanContext parentSpan;
+ if (TryParseTraceParent(*traceParent, parentSpan)) {
+ traceContext = NTracing::TTraceContext::NewChildFromSpan(parentSpan, "HttpServer");
+ }
+ }
+
+ if (!traceContext) {
+ // Generate new trace context from scratch.
+ traceContext = NTracing::TTraceContext::NewRoot("HttpServer");
+ }
+
+ traceContext->SetRecorded();
+ return traceContext;
+}
+
+std::optional<std::pair<i64, i64>> FindBytesRange(const THeadersPtr& headers)
+{
+ auto range = headers->Find(RangeHeaderName);
+ if (!range) {
+ return {};
+ }
+
+ const TString bytesPrefix = "bytes=";
+ if (!range->StartsWith(bytesPrefix)) {
+ THROW_ERROR_EXCEPTION("Invalid range header format")
+ << TErrorAttribute("range", *range);
+ }
+
+ auto indices = range->substr(bytesPrefix.size());
+ std::pair<i64, i64> rangeValue;
+ StringSplitter(indices).Split('-').CollectInto(&rangeValue.first, &rangeValue.second);
+ return rangeValue;
+}
+
+void SetBytesRange(const THeadersPtr& headers, std::pair<i64, i64> range)
+{
+ headers->Set(ContentRangeHeaderName, Format("bytes %v-%v/*", range.first, range.second));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/helpers.h b/yt/yt/core/http/helpers.h
new file mode 100644
index 0000000000..09216c651e
--- /dev/null
+++ b/yt/yt/core/http/helpers.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FillYTErrorHeaders(const IResponseWriterPtr& rsp, const TError& error);
+void FillYTErrorTrailers(const IResponseWriterPtr& rsp, const TError& error);
+
+TError ParseYTError(const IResponsePtr& rsp, bool fromTrailers = false);
+
+//! Catches exception thrown from underlying handler body and
+//! translates it into HTTP error.
+IHttpHandlerPtr WrapYTException(IHttpHandlerPtr underlying);
+
+bool MaybeHandleCors(
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp,
+ const TCorsConfigPtr& config = New<TCorsConfig>());
+
+THashMap<TString, TString> ParseCookies(TStringBuf cookies);
+
+void ProtectCsrfToken(const IResponseWriterPtr& rsp);
+
+std::optional<TString> FindHeader(const IRequestPtr& req, const TString& headerName);
+std::optional<TString> FindBalancerRequestId(const IRequestPtr& req);
+std::optional<TString> FindBalancerRealIP(const IRequestPtr& req);
+
+std::optional<TString> FindUserAgent(const IRequestPtr& req);
+void SetUserAgent(const THeadersPtr& headers, const TString& value);
+
+void ReplyJson(const IResponseWriterPtr& rsp, std::function<void(NYson::IYsonConsumer*)> producer);
+
+void ReplyError(const IResponseWriterPtr& response, const TError& error);
+
+NTracing::TTraceId GetTraceId(const IRequestPtr& req);
+void SetTraceId(const IResponseWriterPtr& rsp, NTracing::TTraceId traceId);
+
+void SetRequestId(const IResponseWriterPtr& rsp, NRpc::TRequestId requestId);
+
+NTracing::TSpanId GetSpanId(const IRequestPtr& req);
+
+NTracing::TTraceContextPtr GetOrCreateTraceContext(const IRequestPtr& req);
+
+std::optional<std::pair<i64, i64>> FindBytesRange(const THeadersPtr& headers);
+void SetBytesRange(const THeadersPtr& headers, std::pair<i64, i64> range);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/http.cpp b/yt/yt/core/http/http.cpp
new file mode 100644
index 0000000000..666799d830
--- /dev/null
+++ b/yt/yt/core/http/http.cpp
@@ -0,0 +1,209 @@
+#include "http.h"
+
+#include <contrib/restricted/http-parser/http_parser.h>
+
+namespace NYT::NHttp {
+
+using namespace NYTree;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStringBuf ToHttpString(EMethod method)
+{
+ switch(method) {
+#define XX(num, name, string) case EMethod::name: return #string;
+ YT_HTTP_METHOD_MAP(XX)
+#undef XX
+ default: THROW_ERROR_EXCEPTION("Invalid method %v", method);
+ }
+}
+
+TStringBuf ToHttpString(EStatusCode code)
+{
+ switch(code) {
+#define XX(num, name, string) case EStatusCode::name: return #string;
+ YT_HTTP_STATUS_MAP(XX)
+#undef XX
+ default:
+ THROW_ERROR_EXCEPTION("Invalid status code %v", code);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUrlRef ParseUrl(TStringBuf url)
+{
+ TUrlRef urlRef;
+
+ http_parser_url parsed;
+ if (0 != http_parser_parse_url(url.data(), url.size(), false, &parsed)) {
+ THROW_ERROR_EXCEPTION("Invalid URL")
+ << TErrorAttribute("url", url);
+ }
+
+ auto convertField = [&] (int flag) -> TStringBuf {
+ if (parsed.field_set & (1 << flag)) {
+ const auto& data = parsed.field_data[flag];
+ return url.SubString(data.off, data.len);
+ }
+
+ return TStringBuf();
+ };
+
+ urlRef.Protocol = convertField(UF_SCHEMA);
+ urlRef.User = convertField(UF_USERINFO);
+ urlRef.Host = convertField(UF_HOST);
+ urlRef.PortStr = convertField(UF_PORT);
+ urlRef.Path = convertField(UF_PATH);
+ urlRef.RawQuery = convertField(UF_QUERY);
+
+ if (parsed.field_set & (1 << UF_PORT)) {
+ urlRef.Port = parsed.port;
+ }
+
+ return urlRef;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THeaders::Add(const TString& header, TString value)
+{
+ ValidateHeaderValue(header, value);
+
+ auto& entry = NameToEntry_[header];
+ entry.OriginalHeaderName = header;
+ entry.Values.push_back(std::move(value));
+}
+
+void THeaders::Remove(TStringBuf header)
+{
+ NameToEntry_.erase(header);
+}
+
+void THeaders::Set(const TString& header, TString value)
+{
+ ValidateHeaderValue(header, value);
+
+ NameToEntry_[header] = {header, {std::move(value)}};
+}
+
+const TString* THeaders::Find(TStringBuf header) const
+{
+ auto it = NameToEntry_.find(header);
+ if (it == NameToEntry_.end()) {
+ return nullptr;
+ }
+
+ // Actually impossible, but just in case.
+ if (it->second.Values.empty()) {
+ return nullptr;
+ }
+
+ return &it->second.Values[0];
+}
+
+void THeaders::RemoveOrThrow(TStringBuf header)
+{
+ auto it = NameToEntry_.find(header);
+ if (it == NameToEntry_.end()) {
+ THROW_ERROR_EXCEPTION("Header %Qv not found", header);
+ }
+ NameToEntry_.erase(it);
+}
+
+TString THeaders::GetOrThrow(TStringBuf header) const
+{
+ auto value = Find(header);
+ if (!value) {
+ THROW_ERROR_EXCEPTION("Header %Qv not found", header);
+ }
+ return *value;
+}
+
+const TCompactVector<TString, 1>& THeaders::GetAll(TStringBuf header) const
+{
+ auto it = NameToEntry_.find(header);
+ if (it == NameToEntry_.end()) {
+ THROW_ERROR_EXCEPTION("Header %Qv not found", header);
+ }
+
+ return it->second.Values;
+}
+
+void THeaders::WriteTo(
+ IOutputStream* out,
+ const THashSet<TString, TCaseInsensitiveStringHasher, TCaseInsensitiveStringEqualityComparer>* filtered) const
+{
+ for (const auto& [name, entry] : NameToEntry_) {
+ // TODO(prime): sanitize headers
+ const auto& header = entry.OriginalHeaderName;
+ const auto& values = entry.Values;
+
+ if (filtered && filtered->find(header) != filtered->end()) {
+ continue;
+ }
+
+ for (const auto& value : values) {
+ *out << header << ": " << value << "\r\n";
+ }
+ }
+}
+
+THeadersPtr THeaders::Duplicate() const
+{
+ auto headers = New<THeaders>();
+ headers->NameToEntry_ = NameToEntry_;
+ return headers;
+}
+
+void THeaders::MergeFrom(const THeadersPtr& headers)
+{
+ for (const auto& [name, entry] : headers->NameToEntry_) {
+ for (const auto& value : entry.Values) {
+ Add(entry.OriginalHeaderName, value);
+ }
+ }
+}
+
+std::vector<std::pair<TString, TString>> THeaders::Dump() const
+{
+ std::vector<std::pair<TString, TString>> result;
+
+ for (const auto& [name, entry] : NameToEntry_) {
+ for (const auto& value : entry.Values) {
+ result.emplace_back(entry.OriginalHeaderName, value);
+ }
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString EscapeHeaderValue(TStringBuf value)
+{
+ TString result;
+ result.reserve(value.length());
+ for (auto ch : value) {
+ if (ch == '\n') {
+ result.append("\\n");
+ } else {
+ result.append(ch);
+ }
+ }
+ return result;
+}
+
+void ValidateHeaderValue(TStringBuf header, TStringBuf value)
+{
+ if (value.find('\n') != TString::npos) {
+ THROW_ERROR_EXCEPTION("Header value should not contain newline symbol")
+ << TErrorAttribute("header", header)
+ << TErrorAttribute("value", value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/http.h b/yt/yt/core/http/http.h
new file mode 100644
index 0000000000..73633444e6
--- /dev/null
+++ b/yt/yt/core/http/http.h
@@ -0,0 +1,294 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/datetime/base.h>
+#include <util/stream/zerocopy.h>
+
+#include <optional>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Copy-pasted from http_parser.h in order to avoid exposing it in our headers.
+//! Please keep in sync, LOL.
+
+#define YT_HTTP_STATUS_MAP(XX) \
+ XX(100, Continue, Continue) \
+ XX(101, SwitchingProtocols, Switching Protocols) \
+ XX(102, Processing, Processing) \
+ XX(200, OK, OK) \
+ XX(201, Created, Created) \
+ XX(202, Accepted, Accepted) \
+ XX(203, NonAuthoritativeInformation, Non-Authoritative Information) \
+ XX(204, NoContent, No Content) \
+ XX(205, ResetContent, Reset Content) \
+ XX(206, PartialContent, Partial Content) \
+ XX(207, MultiStatus, Multi-Status) \
+ XX(208, AlreadyReported, Already Reported) \
+ XX(226, ImUsed, IM Used) \
+ XX(300, MultipleChoices, Multiple Choices) \
+ XX(301, MovedPermanently, Moved Permanently) \
+ XX(302, Found, Found) \
+ XX(303, SeeOther, See Other) \
+ XX(304, NotModified, Not Modified) \
+ XX(305, UseProxy, Use Proxy) \
+ XX(307, TemporaryRedirect, Temporary Redirect) \
+ XX(308, PermanentRedirect, Permanent Redirect) \
+ XX(400, BadRequest, Bad Request) \
+ XX(401, Unauthorized, Unauthorized) \
+ XX(402, PaymentRequired, Payment Required) \
+ XX(403, Forbidden, Forbidden) \
+ XX(404, NotFound, Not Found) \
+ XX(405, MethodNotAllowed, Method Not Allowed) \
+ XX(406, NotAcceptable, Not Acceptable) \
+ XX(407, ProxyAuthenticationRequired, Proxy Authentication Required) \
+ XX(408, RequestTimeout, Request Timeout) \
+ XX(409, Conflict, Conflict) \
+ XX(410, Gone, Gone) \
+ XX(411, LengthRequired, Length Required) \
+ XX(412, PreconditionFailed, Precondition Failed) \
+ XX(413, PayloadTooLarge, Payload Too Large) \
+ XX(414, UriTooLong, URI Too Long) \
+ XX(415, UnsupportedMediaType, Unsupported Media Type) \
+ XX(416, RangeNotSatisfiable, Range Not Satisfiable) \
+ XX(417, ExpectationFailed, Expectation Failed) \
+ XX(421, MisdirectedRequest, Misdirected Request) \
+ XX(422, UnprocessableEntity, Unprocessable Entity) \
+ XX(423, Locked, Locked) \
+ XX(424, FailedDependency, Failed Dependency) \
+ XX(426, UpgradeRequired, Upgrade Required) \
+ XX(428, PreconditionRequired, Precondition Required) \
+ XX(429, TooManyRequests, Too Many Requests) \
+ XX(431, RequestHeaderFieldsTooLarge, Request Header Fields Too Large) \
+ XX(451, UnavailableForLegalReasons, Unavailable For Legal Reasons) \
+ XX(499, ClientClosedRequest, Client Closed Request) \
+ XX(500, InternalServerError, Internal Server Error) \
+ XX(501, NotImplemented, Not Implemented) \
+ XX(502, BadGateway, Bad Gateway) \
+ XX(503, ServiceUnavailable, Service Unavailable) \
+ XX(504, GatewayTimeout, Gateway Timeout) \
+ XX(505, HttpVersionNotSupported, HTTP Version Not Supported) \
+ XX(506, VariantAlsoNegotiates, Variant Also Negotiates) \
+ XX(507, InsufficientStorage, Insufficient Storage) \
+ XX(508, LoopDetected, Loop Detected) \
+ XX(510, NotExtended, Not Extended) \
+ XX(511, NetworkAuthenticationRequired, Network Authentication Required) \
+
+/* Request Methods */
+#define YT_HTTP_METHOD_MAP(XX) \
+ XX(0, Delete, DELETE) \
+ XX(1, Get, GET) \
+ XX(2, Head, HEAD) \
+ XX(3, Post, POST) \
+ XX(4, Put, PUT) \
+ /* pathological */ \
+ XX(5, Connect, CONNECT) \
+ XX(6, Options, OPTIONS) \
+ XX(7, Trace, TRACE) \
+ /* WebDAV */ \
+ XX(8, Copy, COPY) \
+ XX(9, Lock, LOCK) \
+ XX(10, Mkcol, MKCOL) \
+ XX(11, Move, MOVE) \
+ XX(12, Propfind, PROPFIND) \
+ XX(13, Proppatch, PROPPATCH) \
+ XX(14, Search, SEARCH) \
+ XX(15, Unlock, UNLOCK) \
+ XX(16, Bind, BIND) \
+ XX(17, Rebind, REBIND) \
+ XX(18, Unbind, UNBIND) \
+ XX(19, Acl, ACL) \
+ /* subversion */ \
+ XX(20, Report, REPORT) \
+ XX(21, Mkactivity, MKACTIVITY) \
+ XX(22, Checkout, CHECKOUT) \
+ XX(23, Merge, MERGE) \
+ /* upnp */ \
+ XX(24, Msearch, M-SEARCH) \
+ XX(25, Notify, NOTIFY) \
+ XX(26, Subscribe, SUBSCRIBE) \
+ XX(27, Unsubscribe, UNSUBSCRIBE) \
+ /* RFC-5789 */ \
+ XX(28, Patch, PATCH) \
+ XX(29, Purge, PURGE) \
+ /* CalDAV */ \
+ XX(30, Mkcalendar, MKCALENDAR) \
+ /* RFC-2068, section 19.6.1.2 */ \
+ XX(31, Link, LINK) \
+ XX(32, Unlink, UNLINK) \
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define XX(num, name, string) (name)
+DEFINE_ENUM(EMethod,
+ YT_HTTP_METHOD_MAP(XX)
+);
+#undef XX
+
+#define XX(num, name, string) ((name) (num))
+DEFINE_ENUM(EStatusCode,
+ YT_HTTP_STATUS_MAP(XX)
+);
+#undef XX
+
+//! YT enum doesn't support specifying custom string conversion, so we
+//! define our own.
+
+TStringBuf ToHttpString(EMethod method);
+TStringBuf ToHttpString(EStatusCode code);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! {Protocol}://{User}@{Host}:{Port}{Path}?{RawQuery}
+struct TUrlRef
+{
+ std::optional<ui16> Port;
+
+ TStringBuf Protocol;
+ TStringBuf User;
+ //! If host is IPv6 address, Host field will contain address without square brackets.
+ //! E.g. http://[::1]:80/ -> Host == "::1"
+ TStringBuf Host;
+ TStringBuf PortStr;
+ TStringBuf Path;
+ TStringBuf RawQuery;
+};
+
+TUrlRef ParseUrl(TStringBuf url);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THeaders
+ : public virtual TRefCounted
+{
+public:
+ void Add(const TString& header, TString value);
+ void Set(const TString& header, TString value);
+ void Remove(TStringBuf header);
+
+ const TString* Find(TStringBuf header) const;
+
+ void RemoveOrThrow(TStringBuf header);
+
+ //! Returns first header value, if any. Throws otherwise.
+ TString GetOrThrow(TStringBuf header) const;
+
+ const TCompactVector<TString, 1>& GetAll(TStringBuf header) const;
+
+ void WriteTo(
+ IOutputStream* out,
+ const THashSet<TString, TCaseInsensitiveStringHasher, TCaseInsensitiveStringEqualityComparer>* filtered = nullptr) const;
+
+ THeadersPtr Duplicate() const;
+
+ void MergeFrom(const THeadersPtr& headers);
+
+ std::vector<std::pair<TString, TString>> Dump() const;
+
+private:
+ struct TEntry
+ {
+ TString OriginalHeaderName;
+ TCompactVector<TString, 1> Values;
+ };
+
+ THashMap<TString, TEntry, TCaseInsensitiveStringHasher, TCaseInsensitiveStringEqualityComparer> NameToEntry_;
+};
+
+DEFINE_REFCOUNTED_TYPE(THeaders)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString EscapeHeaderValue(TStringBuf value);
+void ValidateHeaderValue(TStringBuf header, TStringBuf value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IRequest
+ : public virtual TRefCounted
+ , public virtual NConcurrency::IAsyncZeroCopyInputStream
+{
+ virtual std::pair<int, int> GetVersion() = 0;
+
+ virtual EMethod GetMethod() = 0;
+
+ virtual const TUrlRef& GetUrl() = 0;
+
+ virtual const THeadersPtr& GetHeaders() = 0;
+
+ virtual const NNet::TNetworkAddress& GetRemoteAddress() const = 0;
+
+ virtual TGuid GetConnectionId() const = 0;
+ virtual TGuid GetRequestId() const = 0;
+ virtual i64 GetReadByteCount() const = 0;
+ virtual TInstant GetStartTime() const = 0;
+
+ virtual bool IsHttps() const { return false; }
+
+ virtual int GetPort() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IResponseWriter
+ : public virtual TRefCounted
+ , public virtual NConcurrency::IFlushableAsyncOutputStream
+{
+ virtual const THeadersPtr& GetHeaders() = 0;
+ virtual const THeadersPtr& GetTrailers() = 0;
+ virtual bool AreHeadersFlushed() const = 0;
+
+ virtual std::optional<EStatusCode> GetStatus() const = 0;
+ virtual void SetStatus(EStatusCode status) = 0;
+ virtual void AddConnectionCloseHeader() = 0;
+ virtual i64 GetWriteByteCount() const = 0;
+
+ virtual TFuture<void> WriteBody(const TSharedRef& smallBody) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IResponseWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IResponse
+ : public virtual NConcurrency::IAsyncZeroCopyInputStream
+{
+ virtual EStatusCode GetStatusCode() = 0;
+ virtual const THeadersPtr& GetHeaders() = 0;
+ virtual const THeadersPtr& GetTrailers() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IResponse)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IHttpHandler
+ : public virtual TRefCounted
+{
+ virtual void HandleRequest(
+ const IRequestPtr& req,
+ const IResponseWriterPtr& rsp) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IHttpHandler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/mock/http.cpp b/yt/yt/core/http/mock/http.cpp
new file mode 100644
index 0000000000..8d9845978b
--- /dev/null
+++ b/yt/yt/core/http/mock/http.cpp
@@ -0,0 +1,47 @@
+#include "http.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRequestMock::TRequestMock()
+ : Headers(New<THeaders>())
+{
+ ON_CALL(*this, GetMethod()).WillByDefault(::testing::Return(EMethod::Get));
+ ON_CALL(*this, GetUrl()).WillByDefault(::testing::ReturnRef(Url));
+ ON_CALL(*this, GetHeaders()).WillByDefault(::testing::ReturnRef(Headers));
+ ON_CALL(*this, GetRemoteAddress()).WillByDefault(::testing::ReturnRef(Address));
+ ON_CALL(*this, GetStartTime()).WillByDefault(::testing::Return(TInstant::Now()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResponseWriterMock::TResponseWriterMock()
+ : Headers(New<THeaders>())
+ , Trailers(New<THeaders>())
+{
+ ON_CALL(*this, GetHeaders()).WillByDefault(::testing::ReturnRef(Headers));
+ ON_CALL(*this, GetTrailers()).WillByDefault(::testing::ReturnRef(Trailers));
+
+ ON_CALL(*this, GetStatus()).WillByDefault(::testing::Return(std::make_optional(EStatusCode::InternalServerError)));
+ ON_CALL(*this, Write(::testing::_)).WillByDefault(::testing::Return(VoidFuture));
+ ON_CALL(*this, WriteBody(::testing::_)).WillByDefault(::testing::Return(VoidFuture));
+ ON_CALL(*this, Flush()).WillByDefault(::testing::Return(VoidFuture));
+ ON_CALL(*this, Close()).WillByDefault(::testing::Return(VoidFuture));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TResponseMock::TResponseMock()
+ : Headers(New<THeaders>())
+ , Trailers(New<THeaders>())
+{
+ ON_CALL(*this, GetHeaders()).WillByDefault(::testing::ReturnRef(Headers));
+ ON_CALL(*this, GetTrailers()).WillByDefault(::testing::ReturnRef(Trailers));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/mock/http.h b/yt/yt/core/http/mock/http.h
new file mode 100644
index 0000000000..8406afdcda
--- /dev/null
+++ b/yt/yt/core/http/mock/http.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <yt/yt/core/http/http.h>
+#include <yt/yt/core/net/address.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRequestMock
+ : public IRequest
+{
+public:
+ TRequestMock();
+
+ MOCK_METHOD(TFuture<TSharedRef>, Read, (), (override));
+ MOCK_METHOD((std::pair<int, int>), GetVersion, (), (override));
+ MOCK_METHOD(EMethod, GetMethod, (), (override));
+ MOCK_METHOD(const TUrlRef&, GetUrl, (), (override));
+ MOCK_METHOD(const THeadersPtr&, GetHeaders, (), (override));
+ MOCK_METHOD(const NNet::TNetworkAddress&, GetRemoteAddress, (), (const, override));
+ MOCK_METHOD(TGuid, GetConnectionId, (), (const, override));
+ MOCK_METHOD(TGuid, GetRequestId, (), (const, override));
+ MOCK_METHOD(i64, GetReadByteCount, (), (const, override));
+ MOCK_METHOD(TInstant, GetStartTime, (), (const, override));
+ MOCK_METHOD(int, GetPort, (), (const, override));
+
+ TUrlRef Url;
+ THeadersPtr Headers;
+ NNet::TNetworkAddress Address;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResponseWriterMock
+ : public IResponseWriter
+{
+public:
+ TResponseWriterMock();
+
+ MOCK_METHOD(TFuture<void>, Write, (const TSharedRef&), (override));
+ MOCK_METHOD(TFuture<void>, Flush, (), (override));
+ MOCK_METHOD(TFuture<void>, Close, (), (override));
+
+ MOCK_METHOD(const THeadersPtr&, GetHeaders, (), (override));
+ MOCK_METHOD(const THeadersPtr&, GetTrailers, (), (override));
+ MOCK_METHOD(bool, AreHeadersFlushed, (), (const, override));
+ MOCK_METHOD(std::optional<EStatusCode>, GetStatus, (), (const, override));
+ MOCK_METHOD(void, SetStatus, (EStatusCode), (override));
+ MOCK_METHOD(void, AddConnectionCloseHeader, (), (override));
+ MOCK_METHOD(i64, GetWriteByteCount, (), (const, override));
+ MOCK_METHOD(TFuture<void>, WriteBody, (const TSharedRef&), (override));
+
+ THeadersPtr Headers;
+ THeadersPtr Trailers;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResponseMock
+ : public IResponse
+{
+ TResponseMock();
+
+ MOCK_METHOD(EStatusCode, GetStatusCode, (), (override));
+ MOCK_METHOD(THeadersPtr&, GetHeaders, (), (override));
+ MOCK_METHOD(THeadersPtr&, GetTrailers, (), (override));
+
+ THeadersPtr Headers;
+ THeadersPtr Trailers;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/mock/ya.make b/yt/yt/core/http/mock/ya.make
new file mode 100644
index 0000000000..94e890be45
--- /dev/null
+++ b/yt/yt/core/http/mock/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ http.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ yt/yt/core/http
+)
+
+END()
diff --git a/yt/yt/core/http/private.h b/yt/yt/core/http/private.h
new file mode 100644
index 0000000000..15cecc2ff9
--- /dev/null
+++ b/yt/yt/core/http/private.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "http.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger HttpLogger("Http");
+inline const NProfiling::TProfiler HttpProfiler = NProfiling::TProfiler{"/http"}.WithHot();
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THttpInput)
+DECLARE_REFCOUNTED_CLASS(THttpOutput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/public.h b/yt/yt/core/http/public.h
new file mode 100644
index 0000000000..93148a0d92
--- /dev/null
+++ b/yt/yt/core/http/public.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THeaders)
+
+DECLARE_REFCOUNTED_STRUCT(IRequest)
+DECLARE_REFCOUNTED_STRUCT(IResponse)
+DECLARE_REFCOUNTED_STRUCT(IResponseWriter)
+DECLARE_REFCOUNTED_CLASS(IActiveRequest)
+
+DECLARE_REFCOUNTED_STRUCT(IServer)
+DECLARE_REFCOUNTED_STRUCT(IClient)
+
+DECLARE_REFCOUNTED_STRUCT(IHttpHandler)
+
+DECLARE_REFCOUNTED_CLASS(THttpIOConfig)
+DECLARE_REFCOUNTED_CLASS(TServerConfig)
+DECLARE_REFCOUNTED_CLASS(TClientConfig)
+DECLARE_REFCOUNTED_CLASS(TCorsConfig)
+DECLARE_REFCOUNTED_CLASS(TConnectionPool)
+DECLARE_REFCOUNTED_CLASS(IRequestPathMatcher)
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const TString DefaultServer;
+extern const TString DefaultUserAgent;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/server.cpp b/yt/yt/core/http/server.cpp
new file mode 100644
index 0000000000..84924f28e2
--- /dev/null
+++ b/yt/yt/core/http/server.cpp
@@ -0,0 +1,520 @@
+#include "server.h"
+#include "http.h"
+#include "config.h"
+#include "stream.h"
+#include "private.h"
+#include "helpers.h"
+
+#include <yt/yt/core/net/listener.h>
+#include <yt/yt/core/net/connection.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NHttp {
+
+using namespace NConcurrency;
+using namespace NProfiling;
+using namespace NNet;
+
+static const auto& Logger = HttpLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCallbackHandler::TCallbackHandler(TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler)
+ : Handler_(std::move(handler))
+{ }
+
+void TCallbackHandler::HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp)
+{
+ Handler_(req, rsp);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IServer::AddHandler(
+ const TString& pattern,
+ TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler)
+{
+ AddHandler(pattern, New<TCallbackHandler>(handler));
+}
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServer
+ : public IServer
+{
+public:
+ TServer(
+ const TServerConfigPtr& config,
+ const IListenerPtr& listener,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor,
+ const IInvokerPtr& invoker,
+ const IRequestPathMatcherPtr& handlers,
+ bool ownPoller = false)
+ : Config_(config)
+ , Listener_(listener)
+ , Poller_(poller)
+ , Acceptor_(acceptor)
+ , Invoker_(invoker)
+ , Handlers_(handlers)
+ , OwnPoller_(ownPoller)
+ { }
+
+ void AddHandler(const TString& path, const IHttpHandlerPtr& handler) override
+ {
+ YT_VERIFY(!Started_);
+ Handlers_->Add(path, handler);
+ }
+
+ const TNetworkAddress& GetAddress() const override
+ {
+ return Listener_->GetAddress();
+ }
+
+ void Start() override
+ {
+ YT_VERIFY(!Started_);
+ Started_ = true;
+
+ YT_LOG_INFO("Server started");
+
+ AsyncAcceptConnection();
+ }
+
+ void Stop() override
+ {
+ Stopped_.store(true);
+
+ if (OwnPoller_) {
+ Poller_->Shutdown();
+ }
+
+ YT_LOG_INFO("Server stopped");
+ }
+
+ void SetPathMatcher(const IRequestPathMatcherPtr& matcher) override
+ {
+ YT_VERIFY(Handlers_->IsEmpty());
+ Handlers_ = matcher;
+ YT_LOG_INFO("Changed path matcher");
+ }
+
+ IRequestPathMatcherPtr GetPathMatcher() override
+ {
+ return Handlers_;
+ }
+
+private:
+ const TServerConfigPtr Config_;
+ const IListenerPtr Listener_;
+ const IPollerPtr Poller_;
+ const IPollerPtr Acceptor_;
+ const IInvokerPtr Invoker_;
+ IRequestPathMatcherPtr Handlers_;
+ bool OwnPoller_ = false;
+
+ bool Started_ = false;
+ std::atomic<bool> Stopped_ = {false};
+
+
+ std::atomic<ui64> ActiveConnections_{0};
+ TGauge ConnectionsActive_ = HttpProfiler.Gauge("/connections_active");
+ TCounter ConnectionsAccepted_ = HttpProfiler.Counter("/connections_accepted");
+ TCounter ConnectionsDropped_ = HttpProfiler.Counter("/connections_dropped");
+
+ void AsyncAcceptConnection()
+ {
+ Listener_->Accept().Subscribe(
+ BIND(&TServer::OnConnectionAccepted, MakeWeak(this))
+ .Via(Acceptor_->GetInvoker()));
+ }
+
+ void OnConnectionAccepted(const TErrorOr<IConnectionPtr>& connectionOrError)
+ {
+ if (Stopped_.load()) {
+ return;
+ }
+
+ AsyncAcceptConnection();
+
+ if (!connectionOrError.IsOK()) {
+ YT_LOG_INFO(connectionOrError, "Error accepting connection");
+ return;
+ }
+
+ auto connection = connectionOrError.ValueOrThrow();
+
+ auto count = ActiveConnections_.fetch_add(1) + 1;
+ if (count >= static_cast<ui64>(Config_->MaxSimultaneousConnections)) {
+ ConnectionsDropped_.Increment();
+ ActiveConnections_--;
+ YT_LOG_WARNING("Server is over max active connection limit (RemoteAddress: %v)",
+ connection->RemoteAddress());
+ return;
+ }
+ ConnectionsActive_.Update(count);
+ ConnectionsAccepted_.Increment();
+
+ auto connectionId = TGuid::Create();
+ YT_LOG_DEBUG("Connection accepted (ConnectionId: %v, RemoteAddress: %v, LocalAddress: %v)",
+ connectionId,
+ connection->RemoteAddress(),
+ connection->LocalAddress());
+
+ Invoker_->Invoke(
+ BIND(&TServer::HandleConnection, MakeStrong(this), std::move(connection), connectionId));
+ }
+
+ bool HandleRequest(const THttpInputPtr& request, const THttpOutputPtr& response)
+ {
+ response->SetStatus(EStatusCode::InternalServerError);
+
+ bool closeResponse = true;
+ try {
+ if (!request->ReceiveHeaders()) {
+ return false;
+ }
+
+ const auto& path = request->GetUrl().Path;
+
+ NProfiling::TWallTimer timer;
+
+ YT_LOG_DEBUG("Received HTTP request (ConnectionId: %v, RequestId: %v, Method: %v, Path: %v, L7RequestId: %v, L7RealIP: %v, UserAgent: %v)",
+ request->GetConnectionId(),
+ request->GetRequestId(),
+ request->GetMethod(),
+ path,
+ FindBalancerRequestId(request),
+ FindBalancerRealIP(request),
+ FindUserAgent(request));
+
+ auto handler = Handlers_->Match(path);
+ if (handler) {
+ closeResponse = false;
+
+ if (request->IsExpecting100Continue()) {
+ response->Flush100Continue();
+ }
+
+ auto traceContext = GetOrCreateTraceContext(request);
+ NTracing::TTraceContextGuard guard(traceContext);
+ SetTraceId(response, traceContext->GetTraceId());
+
+ SetRequestId(response, request->GetRequestId());
+
+ handler->HandleRequest(request, response);
+
+ NTracing::FlushCurrentTraceContextElapsedTime();
+
+ YT_LOG_DEBUG("Finished handling HTTP request (RequestId: %v, WallTime: %v, CpuTime: %v)",
+ request->GetRequestId(),
+ timer.GetElapsedTime(),
+ traceContext->GetElapsedTime());
+ } else {
+ YT_LOG_INFO("Missing HTTP handler for given URL (RequestId: %v, Path: %v)",
+ request->GetRequestId(),
+ path);
+
+ response->SetStatus(EStatusCode::NotFound);
+ }
+ } catch (const std::exception& ex) {
+ closeResponse = true;
+ YT_LOG_INFO(ex, "Error handling HTTP request (RequestId: %v)",
+ request->GetRequestId());
+
+ if (!response->AreHeadersFlushed()) {
+ response->SetStatus(EStatusCode::InternalServerError);
+ }
+ }
+
+ try {
+ if (closeResponse) {
+ WaitFor(response->Close())
+ .ThrowOnError();
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_INFO(ex, "Error flushing HTTP response stream (RequestId: %v)",
+ request->GetRequestId());
+ }
+
+ return true;
+ }
+
+ void HandleConnection(const IConnectionPtr& connection, TGuid connectionId)
+ {
+ try {
+ connection->SubscribePeerDisconnect(BIND([config=Config_, canceler=GetCurrentFiberCanceler(), connectionId=connectionId] {
+ YT_LOG_DEBUG("Client closed TCP socket (ConnectionId: %v)", connectionId);
+
+ if (config->CancelFiberOnConnectionClose) {
+ canceler(TError("Client closed TCP socket; HTTP connection closed"));
+ }
+ }));
+
+ if (Config_->NoDelay) {
+ connection->SetNoDelay();
+ }
+
+ DoHandleConnection(connection, connectionId);
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Unhandled exception (ConnectionId: %v)");
+ }
+ }
+
+ void DoHandleConnection(const IConnectionPtr& connection, TGuid connectionId)
+ {
+ auto finally = Finally([&] {
+ auto count = ActiveConnections_.fetch_sub(1) - 1;
+ ConnectionsActive_.Update(count);
+ });
+
+ auto request = New<THttpInput>(
+ connection,
+ connection->RemoteAddress(),
+ GetCurrentInvoker(),
+ EMessageType::Request,
+ Config_);
+
+ if (Config_->IsHttps) {
+ request->SetHttps();
+ }
+
+ request->SetPort(Config_->Port);
+
+ auto response = New<THttpOutput>(
+ connection,
+ EMessageType::Response,
+ Config_);
+
+ request->SetConnectionId(connectionId);
+ response->SetConnectionId(connectionId);
+
+ while (true) {
+ auto requestId = TGuid::Create();
+ request->SetRequestId(requestId);
+ response->SetRequestId(requestId);
+
+ bool ok = HandleRequest(request, response);
+ if (!ok) {
+ break;
+ }
+
+ auto logDrop = [&] (auto reason) {
+ YT_LOG_DEBUG("Dropping HTTP connection (ConnectionId: %v, Reason: %v)",
+ connectionId,
+ reason);
+ };
+
+ if (!Config_->EnableKeepAlive) {
+ break;
+ }
+
+ // Arcadia decompressors might return eof earlier than
+ // underlying stream. From HTTP server standpoint that
+ // looks like request that wasn't fully consumed, even if
+ // next Read() on that request would have returned eof.
+ //
+ // So we perform one last Read() here and check that
+ // there is no data left inside stream.
+ bool bodyConsumed = false;
+ try {
+ auto chunk = WaitFor(request->Read())
+ .ValueOrThrow();
+ bodyConsumed = chunk.Empty();
+ } catch (const std::exception& ) { }
+ if (!bodyConsumed) {
+ logDrop("Body is not fully consumed by the handler");
+ break;
+ }
+
+ if (request->IsSafeToReuse()) {
+ request->Reset();
+ } else {
+ logDrop("Request is not safe to reuse");
+ break;
+ }
+
+ if (response->IsSafeToReuse()) {
+ response->Reset();
+ } else {
+ logDrop("Response is not safe to reuse");
+ break;
+ }
+
+ if (!connection->IsIdle()) {
+ logDrop("Connection not idle");
+ break;
+ }
+ }
+
+ auto connectionResult = WaitFor(connection->Close());
+ if (connectionResult.IsOK()) {
+ YT_LOG_DEBUG("HTTP connection closed (ConnectionId: %v)",
+ connectionId);
+ } else {
+ YT_LOG_DEBUG(connectionResult, "Error closing HTTP connection (ConnectionId: %v)",
+ connectionId);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const IListenerPtr& listener,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor,
+ const IInvokerPtr& invoker,
+ bool ownPoller)
+{
+ auto handlers = New<TRequestPathMatcher>();
+ return New<TServer>(config, listener, poller, acceptor, invoker, handlers, ownPoller);
+}
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor,
+ const IInvokerPtr& invoker,
+ bool ownPoller)
+{
+ auto address = TNetworkAddress::CreateIPv6Any(config->Port);
+ for (int i = 0;; ++i) {
+ try {
+ auto listener = CreateListener(address, poller, acceptor, config->MaxBacklogSize);
+ return CreateServer(config, listener, poller, acceptor, invoker, ownPoller);
+ } catch (const std::exception& ex) {
+ if (i + 1 == config->BindRetryCount) {
+ throw;
+ } else {
+ YT_LOG_ERROR(ex, "HTTP server bind failed");
+ Sleep(config->BindRetryBackoff);
+ }
+ }
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const IListenerPtr& listener,
+ const IPollerPtr& poller)
+{
+ return CreateServer(config, listener, poller, poller, poller->GetInvoker(), false);
+}
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const IListenerPtr& listener,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor)
+{
+ return CreateServer(config, listener, poller, acceptor, poller->GetInvoker(), false);
+}
+
+IServerPtr CreateServer(const TServerConfigPtr& config, const IPollerPtr& poller, const IPollerPtr& acceptor)
+{
+ return CreateServer(config, poller, acceptor, poller->GetInvoker(), false);
+}
+
+IServerPtr CreateServer(const TServerConfigPtr& config, const IPollerPtr& poller)
+{
+ return CreateServer(config, poller, poller);
+}
+
+IServerPtr CreateServer(int port, const IPollerPtr& poller)
+{
+ auto config = New<TServerConfig>();
+ config->Port = port;
+ return CreateServer(config, poller);
+}
+
+IServerPtr CreateServer(const TServerConfigPtr& config, int pollerThreadCount)
+{
+ auto poller = CreateThreadPoolPoller(pollerThreadCount, config->ServerName);
+ return CreateServer(config, poller, poller, poller->GetInvoker(), true);
+}
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller,
+ const IInvokerPtr& invoker)
+{
+ return CreateServer(config, poller, poller, invoker, false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRequestPathMatcher::Add(const TString& pattern, const IHttpHandlerPtr& handler)
+{
+ if (pattern.empty()) {
+ THROW_ERROR_EXCEPTION("Empty pattern is invalid");
+ }
+
+ if (pattern.back() == '/') {
+ Subtrees_[pattern] = handler;
+
+ auto withoutSlash = pattern.substr(0, pattern.size() - 1);
+ Subtrees_[withoutSlash] = handler;
+ } else {
+ Exact_[pattern] = handler;
+ }
+}
+
+void TRequestPathMatcher::Add(const TString& pattern, TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler)
+{
+ Add(pattern, New<TCallbackHandler>(handler));
+}
+
+IHttpHandlerPtr TRequestPathMatcher::Match(TStringBuf path)
+{
+ {
+ auto it = Exact_.find(path);
+ if (it != Exact_.end()) {
+ return it->second;
+ }
+ }
+
+ while (true) {
+ auto it = Subtrees_.find(path);
+ if (it != Subtrees_.end()) {
+ return it->second;
+ }
+
+ if (path.empty()) {
+ break;
+ }
+
+ path.Chop(1);
+ while (!path.empty() && path.back() != '/') {
+ path.Chop(1);
+ }
+ }
+
+ return nullptr;
+}
+
+bool TRequestPathMatcher::IsEmpty() const
+{
+ return Exact_.empty() && Subtrees_.empty();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/server.h b/yt/yt/core/http/server.h
new file mode 100644
index 0000000000..933c5359c0
--- /dev/null
+++ b/yt/yt/core/http/server.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "public.h"
+#include "http.h"
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCallbackHandler
+ : public IHttpHandler
+{
+public:
+ explicit TCallbackHandler(TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler);
+
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp) override;
+
+private:
+ TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> Handler_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * Thread affinity: single-threaded
+ */
+struct IServer
+ : public virtual TRefCounted
+{
+ //! Attaches a new handler.
+ /*!
+ * Path matching semantic is copied from go standard library.
+ * See https://golang.org/pkg/net/http/#ServeMux
+ */
+ virtual void AddHandler(
+ const TString& pattern,
+ const IHttpHandlerPtr& handler) = 0;
+
+ //! Returns the address this server listens at.
+ virtual const NNet::TNetworkAddress& GetAddress() const = 0;
+
+ //! Starts the server.
+ /*!
+ * Must be called at most once.
+ * All #AddHandler calls must happen prior to start.
+ */
+ virtual void Start() = 0;
+
+ //! Stops the server.
+ /*!
+ * Can be called multiple times (and even if not started).
+ */
+ virtual void Stop() = 0;
+
+ //! Sets path matcher
+ /*!
+ * Must be called before adding callbacks.
+ * @see IRequestPathMatcher
+ */
+ virtual void SetPathMatcher(const IRequestPathMatcherPtr& matcher) = 0;
+ virtual IRequestPathMatcherPtr GetPathMatcher() = 0;
+
+
+ // Extension methods
+ void AddHandler(
+ const TString& pattern,
+ TCallback<void(const IRequestPtr& req, const IResponseWriterPtr& rsp)> handler);
+};
+
+DEFINE_REFCOUNTED_TYPE(IServer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NNet::IListenerPtr& listener,
+ const NConcurrency::IPollerPtr& poller);
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NNet::IListenerPtr& listener,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor);
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller);
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor);
+IServerPtr CreateServer(
+ int port,
+ const NConcurrency::IPollerPtr& poller);
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ int pollerThreadCount = 1);
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller,
+ const IInvokerPtr& invoker);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! IRequestPathMatcher is responsible for storing handlers and giving them back by path
+class IRequestPathMatcher
+ : public virtual TRefCounted
+{
+public:
+ virtual void Add(const TString& pattern, const IHttpHandlerPtr& handler) = 0;
+ virtual void Add(const TString& pattern, TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler) = 0;
+ virtual IHttpHandlerPtr Match(TStringBuf path) = 0;
+ virtual bool IsEmpty() const = 0;
+
+};
+
+DEFINE_REFCOUNTED_TYPE(IRequestPathMatcher)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRequestPathMatcher
+ : public IRequestPathMatcher
+{
+public:
+ void Add(const TString& pattern, const IHttpHandlerPtr& handler) override;
+ void Add(const TString& pattern, TCallback<void(const IRequestPtr&, const IResponseWriterPtr&)> handler) override;
+ IHttpHandlerPtr Match(TStringBuf path) override;
+ bool IsEmpty() const override;
+
+
+private:
+ THashMap<TString, IHttpHandlerPtr> Exact_;
+ THashMap<TString, IHttpHandlerPtr> Subtrees_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/stream.cpp b/yt/yt/core/http/stream.cpp
new file mode 100644
index 0000000000..6637fb0b35
--- /dev/null
+++ b/yt/yt/core/http/stream.cpp
@@ -0,0 +1,857 @@
+#include "stream.h"
+#include "private.h"
+
+#include <yt/yt/core/net/connection.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/string/escape.h>
+
+namespace NYT::NHttp {
+
+using namespace NConcurrency;
+using namespace NNet;
+
+static const auto& Logger = HttpLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+http_parser_settings THttpParser::GetParserSettings()
+{
+ http_parser_settings settings;
+ http_parser_settings_init(&settings);
+
+ settings.on_url = &OnUrl;
+ settings.on_status = &OnStatus;
+ settings.on_header_field = &OnHeaderField;
+ settings.on_header_value = &OnHeaderValue;
+ settings.on_headers_complete = &OnHeadersComplete;
+ settings.on_body = &OnBody;
+ settings.on_message_complete = &OnMessageComplete;
+
+ return settings;
+}
+
+const http_parser_settings ParserSettings = THttpParser::GetParserSettings();
+
+THttpParser::THttpParser(http_parser_type parserType)
+ : Headers_(New<THeaders>())
+{
+ http_parser_init(&Parser_, parserType);
+ Parser_.data = reinterpret_cast<void*>(this);
+}
+
+EParserState THttpParser::GetState() const
+{
+ return State_;
+}
+
+void THttpParser::Reset()
+{
+ Headers_ = New<THeaders>();
+ Trailers_.Reset();
+
+ ShouldKeepAlive_ = false;
+ HeaderBuffered_ = false;
+ State_ = EParserState::Initialized;
+
+ FirstLine_.Reset();
+ NextField_.Reset();
+ NextValue_.Reset();
+ LastBodyChunk_ = {};
+ YT_VERIFY(FirstLine_.GetLength() == 0);
+ YT_VERIFY(NextField_.GetLength() == 0);
+ YT_VERIFY(NextValue_.GetLength() == 0);
+}
+
+TSharedRef THttpParser::Feed(const TSharedRef& input)
+{
+ InputBuffer_ = &input;
+ auto finally = Finally([&] {
+ InputBuffer_ = nullptr;
+ });
+
+ size_t read = http_parser_execute(&Parser_, &ParserSettings, input.Begin(), input.Size());
+ auto http_errno = static_cast<enum http_errno>(Parser_.http_errno);
+ if (http_errno != 0 && http_errno != HPE_PAUSED) {
+ // 64 bytes before error
+ size_t contextStart = read - std::min<size_t>(read, 64);
+
+ // and 64 bytes after error
+ size_t contextEnd = std::min(read + 64, input.Size());
+
+ TString errorContext(input.Begin() + contextStart, contextEnd - contextStart);
+
+ THROW_ERROR_EXCEPTION("HTTP parse error: %v", http_errno_description(http_errno))
+ << TErrorAttribute("parser_error_name", http_errno_name(http_errno))
+ << TErrorAttribute("error_context", EscapeC(errorContext));
+ }
+
+ if (http_errno == HPE_PAUSED) {
+ http_parser_pause(&Parser_, 0);
+ }
+
+ return input.Slice(read, input.Size());
+}
+
+std::pair<int, int> THttpParser::GetVersion() const
+{
+ return std::make_pair<int, int>(Parser_.http_major, Parser_.http_minor);
+}
+
+EStatusCode THttpParser::GetStatusCode() const
+{
+ return EStatusCode(Parser_.status_code);
+}
+
+EMethod THttpParser::GetMethod() const
+{
+ return EMethod(Parser_.method);
+}
+
+TString THttpParser::GetFirstLine()
+{
+ return FirstLine_.Flush();
+}
+
+const THeadersPtr& THttpParser::GetHeaders() const
+{
+ return Headers_;
+}
+
+const THeadersPtr& THttpParser::GetTrailers() const
+{
+ return Trailers_;
+}
+
+TSharedRef THttpParser::GetLastBodyChunk()
+{
+ auto chunk = LastBodyChunk_;
+ LastBodyChunk_ = TSharedRef::MakeEmpty();
+ return chunk;
+}
+
+bool THttpParser::ShouldKeepAlive() const
+{
+ return ShouldKeepAlive_;
+}
+
+void THttpParser::MaybeFlushHeader(bool trailer)
+{
+ if (!HeaderBuffered_) {
+ return;
+ }
+
+ HeaderBuffered_ = false;
+ if (NextField_.GetLength() == 0) {
+ return;
+ }
+
+ if (trailer) {
+ if (!Trailers_) {
+ Trailers_ = New<THeaders>();
+ }
+ Trailers_->Set(NextField_.Flush(), NextValue_.Flush());
+ } else {
+ Headers_->Set(NextField_.Flush(), NextValue_.Flush());
+ }
+}
+
+int THttpParser::OnUrl(http_parser* parser, const char* at, size_t length)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->FirstLine_.AppendString(TStringBuf(at, length));
+
+ return 0;
+}
+
+int THttpParser::OnStatus(http_parser* /*parser*/, const char* /*at*/, size_t /*length*/)
+{
+ return 0;
+}
+
+int THttpParser::OnHeaderField(http_parser* parser, const char* at, size_t length)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->MaybeFlushHeader(that->State_ == EParserState::HeadersFinished);
+
+ that->NextField_.AppendString(TStringBuf(at, length));
+ return 0;
+}
+
+int THttpParser::OnHeaderValue(http_parser* parser, const char* at, size_t length)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->NextValue_.AppendString(TStringBuf(at, length));
+ that->HeaderBuffered_ = true;
+
+ return 0;
+}
+
+int THttpParser::OnHeadersComplete(http_parser* parser)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->MaybeFlushHeader(that->State_ == EParserState::HeadersFinished);
+
+ that->State_ = EParserState::HeadersFinished;
+
+ return 0;
+}
+
+int THttpParser::OnBody(http_parser* parser, const char* at, size_t length)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->LastBodyChunk_ = that->InputBuffer_->Slice(at, at + length);
+ http_parser_pause(parser, 1);
+ return 0;
+}
+
+int THttpParser::OnMessageComplete(http_parser* parser)
+{
+ auto that = reinterpret_cast<THttpParser*>(parser->data);
+ that->MaybeFlushHeader(that->State_ == EParserState::HeadersFinished);
+
+ that->State_ = EParserState::MessageFinished;
+ that->ShouldKeepAlive_ = http_should_keep_alive(parser);
+ http_parser_pause(parser, 1);
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THttpParserTag
+{ };
+
+THttpInput::THttpInput(
+ const IConnectionPtr& connection,
+ const TNetworkAddress& remoteAddress,
+ const IInvokerPtr& readInvoker,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config)
+ : Connection_(connection)
+ , RemoteAddress_(remoteAddress)
+ , MessageType_(messageType)
+ , Config_(config)
+ , InputBuffer_(TSharedMutableRef::Allocate<THttpParserTag>(Config_->ReadBufferSize))
+ , Parser_(messageType == EMessageType::Request ? HTTP_REQUEST : HTTP_RESPONSE)
+ , StartByteCount_(connection->GetReadByteCount())
+ , StartStatistics_(connection->GetReadStatistics())
+ , LastProgressLogTime_(TInstant::Now())
+ , ReadInvoker_(readInvoker)
+{ }
+
+std::pair<int, int> THttpInput::GetVersion()
+{
+ EnsureHeadersReceived();
+ return Parser_.GetVersion();
+}
+
+EMethod THttpInput::GetMethod()
+{
+ YT_VERIFY(MessageType_ == EMessageType::Request);
+
+ EnsureHeadersReceived();
+ return Parser_.GetMethod();
+}
+
+const TUrlRef& THttpInput::GetUrl()
+{
+ YT_VERIFY(MessageType_ == EMessageType::Request);
+
+ EnsureHeadersReceived();
+ return Url_;
+}
+
+const THeadersPtr& THttpInput::GetHeaders()
+{
+ EnsureHeadersReceived();
+ return Headers_;
+}
+
+EStatusCode THttpInput::GetStatusCode()
+{
+ EnsureHeadersReceived();
+ return Parser_.GetStatusCode();
+}
+
+const THeadersPtr& THttpInput::GetTrailers()
+{
+ if (Parser_.GetState() != EParserState::MessageFinished) {
+ THROW_ERROR_EXCEPTION("Cannot access trailers while body is not fully consumed");
+ }
+
+ const auto& trailers = Parser_.GetTrailers();
+ if (!trailers) {
+ static THeadersPtr emptyTrailers = New<THeaders>();
+ return emptyTrailers;
+ }
+ return trailers;
+}
+
+const TNetworkAddress& THttpInput::GetRemoteAddress() const
+{
+ return RemoteAddress_;
+}
+
+TGuid THttpInput::GetConnectionId() const
+{
+ return ConnectionId_;
+}
+
+void THttpInput::SetConnectionId(TGuid connectionId)
+{
+ ConnectionId_ = connectionId;
+}
+
+TGuid THttpInput::GetRequestId() const
+{
+ return RequestId_;
+}
+
+void THttpInput::SetRequestId(TGuid requestId)
+{
+ RequestId_ = requestId;
+}
+
+bool THttpInput::IsExpecting100Continue() const
+{
+ auto expectHeader = Headers_->Find("Expect");
+ return expectHeader && *expectHeader == "100-continue";
+}
+
+bool THttpInput::IsSafeToReuse() const
+{
+ return SafeToReuse_;
+}
+
+void THttpInput::Reset()
+{
+ HeadersReceived_ = false;
+ Headers_.Reset();
+ Parser_.Reset();
+ RawUrl_ = {};
+ Url_ = {};
+ SafeToReuse_ = false;
+ LastProgressLogTime_ = TInstant::Now();
+ StartTime_ = TInstant::Zero();
+
+ StartByteCount_ = Connection_->GetReadByteCount();
+ StartStatistics_ = Connection_->GetReadStatistics();
+}
+
+void THttpInput::FinishHeaders()
+{
+ HeadersReceived_ = true;
+ Headers_ = Parser_.GetHeaders();
+
+ if (MessageType_ == EMessageType::Request) {
+ RawUrl_ = Parser_.GetFirstLine();
+ Url_ = ParseUrl(RawUrl_);
+ }
+}
+
+void THttpInput::EnsureHeadersReceived()
+{
+ if (!ReceiveHeaders()) {
+ THROW_ERROR_EXCEPTION("Connection was closed before the first byte of HTTP message");
+ }
+}
+
+bool THttpInput::ReceiveHeaders()
+{
+ if (HeadersReceived_) {
+ return true;
+ }
+
+ bool idleConnection = MessageType_ == EMessageType::Request;
+ TInstant start = TInstant::Now();
+
+ if (idleConnection) {
+ Connection_->SetReadDeadline(start + Config_->ConnectionIdleTimeout);
+ } else {
+ Connection_->SetReadDeadline(start + Config_->HeaderReadTimeout);
+ }
+
+ while (true) {
+ MaybeLogSlowProgress();
+
+ bool eof = false;
+ TErrorOr<size_t> readResult;
+ if (UnconsumedData_.Empty()) {
+ auto asyncReadResult = Connection_->Read(InputBuffer_);
+ readResult = WaitFor(asyncReadResult);
+ if (readResult.IsOK()) {
+ UnconsumedData_ = InputBuffer_.Slice(0, readResult.ValueOrThrow());
+ if (!StartTime_) {
+ StartTime_ = TInstant::Now();
+ }
+ } else {
+ UnconsumedData_ = InputBuffer_.Slice(static_cast<size_t>(0), static_cast<size_t>(0));
+ }
+ eof = UnconsumedData_.Size() == 0;
+ }
+
+ try {
+ UnconsumedData_ = Parser_.Feed(UnconsumedData_);
+ } catch (const std::exception& ex) {
+ if (!readResult.IsOK()) {
+ THROW_ERROR_EXCEPTION(ex) << readResult;
+ } else {
+ throw;
+ }
+ }
+
+ if (Parser_.GetState() != EParserState::Initialized) {
+ FinishHeaders();
+ if (Parser_.GetState() == EParserState::MessageFinished) {
+ FinishMessage();
+ }
+ Connection_->SetReadDeadline(std::nullopt);
+ return true;
+ }
+
+ // HTTP parser does not treat EOF at message start as error.
+ if (eof) {
+ return false;
+ }
+
+ if (idleConnection) {
+ idleConnection = false;
+ Connection_->SetReadDeadline(StartTime_ + Config_->HeaderReadTimeout);
+ }
+ }
+}
+
+void THttpInput::FinishMessage()
+{
+ SafeToReuse_ = Parser_.ShouldKeepAlive();
+
+ auto stats = Connection_->GetReadStatistics();
+ if (MessageType_ == EMessageType::Request) {
+ YT_LOG_DEBUG("Finished reading HTTP request body (RequestId: %v, BytesIn: %v, IdleDuration: %v, BusyDuration: %v, Keep-Alive: %v)",
+ RequestId_,
+ GetReadByteCount(),
+ stats.IdleDuration - StartStatistics_.IdleDuration,
+ stats.BusyDuration - StartStatistics_.BusyDuration,
+ Parser_.ShouldKeepAlive());
+ }
+}
+
+TFuture<TSharedRef> THttpInput::Read()
+{
+ return BIND(&THttpInput::DoRead, MakeStrong(this))
+ .AsyncVia(ReadInvoker_)
+ .Run();
+}
+
+i64 THttpInput::GetReadByteCount() const
+{
+ return Connection_->GetReadByteCount() - StartByteCount_;
+}
+
+TInstant THttpInput::GetStartTime() const
+{
+ return StartTime_;
+}
+
+bool THttpInput::IsHttps() const
+{
+ return IsHttps_;
+}
+
+void THttpInput::SetHttps()
+{
+ IsHttps_ = true;
+}
+
+int THttpInput::GetPort() const
+{
+ return Port_;
+}
+
+void THttpInput::SetPort(int port)
+{
+ Port_ = port;
+}
+
+TSharedRef THttpInput::DoRead()
+{
+ if (Parser_.GetState() == EParserState::MessageFinished) {
+ return TSharedRef::MakeEmpty();
+ }
+
+ Connection_->SetReadDeadline(TInstant::Now() + Config_->BodyReadIdleTimeout);
+ while (true) {
+ MaybeLogSlowProgress();
+
+ auto chunk = Parser_.GetLastBodyChunk();
+ if (!chunk.Empty()) {
+ Connection_->SetReadDeadline(std::nullopt);
+ return chunk;
+ }
+
+ bool eof = false;
+ if (UnconsumedData_.Empty()) {
+ auto asyncRead = Connection_->Read(InputBuffer_);
+ UnconsumedData_ = InputBuffer_.Slice(0, WaitFor(asyncRead).ValueOrThrow());
+ eof = UnconsumedData_.Size() == 0;
+ }
+
+ UnconsumedData_ = Parser_.Feed(UnconsumedData_);
+ if (Parser_.GetState() == EParserState::MessageFinished) {
+ FinishMessage();
+
+ Connection_->SetReadDeadline(std::nullopt);
+ return TSharedRef::MakeEmpty();
+ }
+
+ // EOF must be handled by HTTP parser.
+ YT_VERIFY(!eof);
+ }
+}
+
+void THttpInput::MaybeLogSlowProgress()
+{
+ auto now = TInstant::Now();
+ if (LastProgressLogTime_ + Config_->BodyReadIdleTimeout < now) {
+ YT_LOG_DEBUG("Reading HTTP message (RequestId: %v, BytesIn: %v)",
+ RequestId_,
+ GetReadByteCount());
+ LastProgressLogTime_ = now;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THttpOutput::THttpOutput(
+ const THeadersPtr& headers,
+ const IConnectionPtr& connection,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config)
+ : Connection_(connection)
+ , MessageType_(messageType)
+ , Config_(config)
+ , OnWriteFinish_(BIND_NO_PROPAGATE(&THttpOutput::OnWriteFinish, MakeWeak(this)))
+ , StartByteCount_(connection->GetWriteByteCount())
+ , StartStatistics_(connection->GetWriteStatistics())
+ , LastProgressLogTime_(TInstant::Now())
+ , Headers_(headers)
+{ }
+
+THttpOutput::THttpOutput(
+ const IConnectionPtr& connection,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config)
+ : THttpOutput(New<THeaders>(), connection, messageType, config)
+{ }
+
+const THeadersPtr& THttpOutput::GetHeaders()
+{
+ return Headers_;
+}
+
+void THttpOutput::SetHost(TStringBuf host, TStringBuf port)
+{
+ if (!port.empty()) {
+ HostHeader_ = Format("%v:%v", host, port);
+ } else {
+ HostHeader_ = TString(host);
+ }
+}
+
+void THttpOutput::SetHeaders(const THeadersPtr& headers)
+{
+ Headers_ = headers;
+}
+
+bool THttpOutput::AreHeadersFlushed() const
+{
+ return HeadersFlushed_;
+}
+
+const THeadersPtr& THttpOutput::GetTrailers()
+{
+ if (!Trailers_) {
+ Trailers_ = New<THeaders>();
+ }
+ return Trailers_;
+}
+
+void THttpOutput::AddConnectionCloseHeader()
+{
+ YT_VERIFY(MessageType_ == EMessageType::Response);
+ ConnectionClose_ = true;
+}
+
+bool THttpOutput::IsSafeToReuse() const
+{
+ return MessageFinished_ && !ConnectionClose_;
+}
+
+void THttpOutput::Reset()
+{
+ StartByteCount_ = Connection_->GetWriteByteCount();
+ StartStatistics_ = Connection_->GetWriteStatistics();
+ HeadersLogged_ = false;
+
+ ConnectionClose_ = false;
+ Headers_ = New<THeaders>();
+
+ Status_.reset();
+ Method_.reset();
+ HostHeader_.reset();
+ Path_.clear();
+
+ HeadersFlushed_ = false;
+ MessageFinished_ = false;
+ LastProgressLogTime_ = TInstant::Now();
+
+ Trailers_.Reset();
+}
+
+void THttpOutput::SetConnectionId(TGuid connectionId)
+{
+ ConnectionId_ = connectionId;
+}
+
+void THttpOutput::SetRequestId(TGuid requestId)
+{
+ RequestId_ = requestId;
+}
+
+void THttpOutput::WriteRequest(EMethod method, const TString& path)
+{
+ YT_VERIFY(MessageType_ == EMessageType::Request);
+
+ Method_ = method;
+ Path_ = path;
+}
+
+std::optional<EStatusCode> THttpOutput::GetStatus() const
+{
+ return Status_;
+}
+
+void THttpOutput::SetStatus(EStatusCode status)
+{
+ YT_VERIFY(MessageType_ == EMessageType::Response);
+
+ Status_ = status;
+}
+
+TSharedRef THttpOutput::GetHeadersPart(std::optional<size_t> contentLength)
+{
+ TBufferOutput messageHeaders;
+ if (MessageType_ == EMessageType::Request) {
+ YT_VERIFY(Method_);
+
+ messageHeaders << ToHttpString(*Method_) << " " << Path_ << " HTTP/1.1\r\n";
+ } else {
+ if (!Status_) {
+ Status_ = EStatusCode::OK;
+ }
+
+ messageHeaders << "HTTP/1.1 " << static_cast<int>(*Status_) << " " << ToHttpString(*Status_) << "\r\n";
+ }
+
+ bool methodNeedsContentLength = Method_ && *Method_ != EMethod::Get && *Method_ != EMethod::Head;
+
+ if (contentLength) {
+ if (MessageType_ == EMessageType::Response ||
+ (MessageType_ == EMessageType::Request && methodNeedsContentLength)) {
+ messageHeaders << "Content-Length: " << *contentLength << "\r\n";
+ }
+ } else {
+ messageHeaders << "Transfer-Encoding: chunked\r\n";
+ }
+
+ if (ConnectionClose_) {
+ messageHeaders << "Connection: close\r\n";
+ }
+
+ if (HostHeader_) {
+ messageHeaders << "Host: " << *HostHeader_ << "\r\n";
+ }
+
+ Headers_->WriteTo(&messageHeaders, &FilteredHeaders_);
+
+ TString headers;
+ messageHeaders.Buffer().AsString(headers);
+ return TSharedRef::FromString(headers);
+}
+
+TSharedRef THttpOutput::GetTrailersPart()
+{
+ TBufferOutput messageTrailers;
+
+ Trailers_->WriteTo(&messageTrailers, &FilteredHeaders_);
+
+ TString trailers;
+ messageTrailers.Buffer().AsString(trailers);
+ return TSharedRef::FromString(trailers);
+}
+
+TSharedRef THttpOutput::GetChunkHeader(size_t size)
+{
+ return TSharedRef::FromString(Format("%llX\r\n", size));
+}
+
+void THttpOutput::Flush100Continue()
+{
+ if (HeadersFlushed_) {
+ THROW_ERROR_EXCEPTION("Cannot send 100 Continue after headers");
+ }
+
+ Connection_->SetWriteDeadline(TInstant::Now() + Config_->WriteIdleTimeout);
+ WaitFor(Connection_->Write(Http100Continue).Apply(OnWriteFinish_))
+ .ThrowOnError();
+}
+
+TFuture<void> THttpOutput::Write(const TSharedRef& data)
+{
+ if (MessageFinished_) {
+ THROW_ERROR_EXCEPTION("Cannot write to finished HTTP message");
+ }
+
+ std::vector<TSharedRef> writeRefs;
+ if (!HeadersFlushed_) {
+ HeadersFlushed_ = true;
+ writeRefs.emplace_back(GetHeadersPart(std::nullopt));
+ writeRefs.emplace_back(CrLf);
+ }
+
+ if (data.Size() != 0) {
+ writeRefs.emplace_back(GetChunkHeader(data.Size()));
+ writeRefs.emplace_back(data);
+ writeRefs.push_back(CrLf);
+ }
+
+ Connection_->SetWriteDeadline(TInstant::Now() + Config_->WriteIdleTimeout);
+ return Connection_->WriteV(TSharedRefArray(std::move(writeRefs), TSharedRefArray::TMoveParts{}))
+ .Apply(OnWriteFinish_);
+}
+
+TFuture<void> THttpOutput::Flush()
+{
+ return VoidFuture;
+}
+
+TFuture<void> THttpOutput::Close()
+{
+ if (MessageFinished_) {
+ return VoidFuture;
+ }
+
+ if (!HeadersFlushed_) {
+ return WriteBody(TSharedRef::MakeEmpty());
+ }
+
+ return FinishChunked();
+}
+
+TFuture<void> THttpOutput::FinishChunked()
+{
+ std::vector<TSharedRef> writeRefs;
+
+ if (Trailers_) {
+ writeRefs.emplace_back(ZeroCrLf);
+ writeRefs.emplace_back(GetTrailersPart());
+ writeRefs.emplace_back(CrLf);
+ } else {
+ writeRefs.emplace_back(ZeroCrLfCrLf);
+ }
+
+ MessageFinished_ = true;
+ Connection_->SetWriteDeadline(TInstant::Now() + Config_->WriteIdleTimeout);
+ return Connection_->WriteV(TSharedRefArray(std::move(writeRefs), TSharedRefArray::TMoveParts{}))
+ .Apply(OnWriteFinish_);
+}
+
+TFuture<void> THttpOutput::WriteBody(const TSharedRef& smallBody)
+{
+ if (HeadersFlushed_ || MessageFinished_) {
+ THROW_ERROR_EXCEPTION("Cannot write body to partially flushed HTTP message");
+ }
+
+ TSharedRefArray writeRefs;
+ if (Trailers_) {
+ writeRefs = TSharedRefArray(
+ std::array<TSharedRef, 4>{
+ GetHeadersPart(smallBody.Size()),
+ GetTrailersPart(),
+ CrLf,
+ smallBody
+ },
+ TSharedRefArray::TCopyParts{});
+ } else {
+ writeRefs = TSharedRefArray(
+ std::array<TSharedRef, 3>{
+ GetHeadersPart(smallBody.Size()),
+ CrLf,
+ smallBody
+ },
+ TSharedRefArray::TCopyParts{});
+ }
+
+ HeadersFlushed_ = true;
+ MessageFinished_ = true;
+ Connection_->SetWriteDeadline(TInstant::Now() + Config_->WriteIdleTimeout);
+ return Connection_->WriteV(writeRefs)
+ .Apply(OnWriteFinish_);
+}
+
+i64 THttpOutput::GetWriteByteCount() const
+{
+ return Connection_->GetWriteByteCount() - StartByteCount_;
+}
+
+void THttpOutput::OnWriteFinish()
+{
+ Connection_->SetWriteDeadline({});
+
+ auto now = TInstant::Now();
+ auto stats = Connection_->GetWriteStatistics();
+ if (LastProgressLogTime_ + Config_->WriteIdleTimeout < now) {
+ YT_LOG_DEBUG("Writing HTTP message (Requestid: %v, BytesOut: %v, IdleDuration: %v, BusyDuration: %v)",
+ RequestId_,
+ GetWriteByteCount(),
+ stats.IdleDuration - StartStatistics_.IdleDuration,
+ stats.BusyDuration - StartStatistics_.BusyDuration);
+ LastProgressLogTime_ = now;
+ }
+
+ if (MessageType_ == EMessageType::Response) {
+ if (HeadersFlushed_ && !HeadersLogged_) {
+ HeadersLogged_ = true;
+ YT_LOG_DEBUG("Finished writing HTTP headers (RequestId: %v, StatusCode: %v)",
+ RequestId_,
+ Status_);
+ }
+
+ if (MessageFinished_) {
+ YT_LOG_DEBUG("Finished writing HTTP response (RequestId: %v, BytesOut: %v)",
+ RequestId_,
+ GetWriteByteCount());
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const THashSet<TString, TCaseInsensitiveStringHasher, TCaseInsensitiveStringEqualityComparer> THttpOutput::FilteredHeaders_ = {
+ "transfer-encoding",
+ "content-length",
+ "connection",
+ "host",
+};
+
+const TSharedRef THttpOutput::Http100Continue = TSharedRef::FromString("HTTP/1.1 100 Continue\r\n\r\n");
+const TSharedRef THttpOutput::CrLf = TSharedRef::FromString("\r\n");
+const TSharedRef THttpOutput::ZeroCrLf = TSharedRef::FromString("0\r\n");
+const TSharedRef THttpOutput::ZeroCrLfCrLf = TSharedRef::FromString("0\r\n\r\n");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/stream.h b/yt/yt/core/http/stream.h
new file mode 100644
index 0000000000..44783c1013
--- /dev/null
+++ b/yt/yt/core/http/stream.h
@@ -0,0 +1,269 @@
+#pragma once
+
+#include "http.h"
+#include "config.h"
+
+#include <yt/yt/core/net/public.h>
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/address.h>
+
+#include <contrib/restricted/http-parser/http_parser.h>
+
+#include <util/stream/buffer.h>
+
+namespace NYT::NHttp {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMessageType,
+ (Request)
+ (Response)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EParserState,
+ (Initialized)
+ (HeadersFinished)
+ (MessageFinished)
+);
+
+class THttpParser
+{
+public:
+ explicit THttpParser(http_parser_type parserType);
+
+ static http_parser_settings GetParserSettings();
+
+ std::pair<int, int> GetVersion() const;
+ EMethod GetMethod() const;
+ EStatusCode GetStatusCode() const;
+ TString GetFirstLine();
+
+ const THeadersPtr& GetHeaders() const;
+ const THeadersPtr& GetTrailers() const;
+
+ void Reset();
+ bool ShouldKeepAlive() const;
+
+ EParserState GetState() const;
+ TSharedRef GetLastBodyChunk();
+ TSharedRef Feed(const TSharedRef& buf);
+
+private:
+ http_parser Parser_{};
+
+ TStringBuilder FirstLine_;
+ TStringBuilder NextField_;
+ TStringBuilder NextValue_;
+
+ THeadersPtr Headers_;
+ THeadersPtr Trailers_;
+
+ EParserState State_ = EParserState::Initialized;
+
+ const TSharedRef* InputBuffer_ = nullptr;
+ TSharedRef LastBodyChunk_;
+
+ bool ShouldKeepAlive_ = false;
+ bool HeaderBuffered_ = false;
+
+ void MaybeFlushHeader(bool trailer);
+
+ static int OnUrl(http_parser* parser, const char *at, size_t length);
+ static int OnStatus(http_parser* parser, const char *at, size_t length);
+ static int OnHeaderField(http_parser* parser, const char *at, size_t length);
+ static int OnHeaderValue(http_parser* parser, const char *at, size_t length);
+ static int OnHeadersComplete(http_parser* parser);
+ static int OnBody(http_parser* parser, const char *at, size_t length);
+ static int OnMessageComplete(http_parser* parser);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpInput
+ : public IRequest
+ , public IResponse
+{
+public:
+ THttpInput(
+ const NNet::IConnectionPtr& connection,
+ const NNet::TNetworkAddress& peerAddress,
+ const IInvokerPtr& readInvoker,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config);
+
+ EMethod GetMethod() override;
+ const TUrlRef& GetUrl() override;
+ std::pair<int, int> GetVersion() override;
+ const THeadersPtr& GetHeaders() override;
+
+ EStatusCode GetStatusCode() override;
+ const THeadersPtr& GetTrailers() override;
+
+ TFuture<TSharedRef> Read() override;
+
+ const NNet::TNetworkAddress& GetRemoteAddress() const override;
+
+ TGuid GetConnectionId() const override;
+ void SetConnectionId(TGuid connectionId);
+
+ TGuid GetRequestId() const override;
+ void SetRequestId(TGuid requestId);
+
+ i64 GetReadByteCount() const override;
+
+ bool IsExpecting100Continue() const;
+
+ bool IsSafeToReuse() const;
+ void Reset();
+
+ // Returns false if connection was closed before receiving first byte.
+ bool ReceiveHeaders();
+
+ TInstant GetStartTime() const override;
+
+ bool IsHttps() const override;
+ void SetHttps();
+
+ int GetPort() const override;
+ void SetPort(int port);
+
+private:
+ const NNet::IConnectionPtr Connection_;
+ const NNet::TNetworkAddress RemoteAddress_;
+ const EMessageType MessageType_;
+ const THttpIOConfigPtr Config_;
+
+ TSharedMutableRef InputBuffer_;
+ TSharedRef UnconsumedData_;
+
+ bool HeadersReceived_ = false;
+ THttpParser Parser_;
+
+ TString RawUrl_;
+ TUrlRef Url_;
+ int Port_;
+ THeadersPtr Headers_;
+
+ // Debug.
+ TGuid ConnectionId_;
+ TGuid RequestId_;
+ i64 StartByteCount_ = 0;
+ NNet::TConnectionStatistics StartStatistics_;
+ TInstant LastProgressLogTime_;
+ TInstant StartTime_;
+
+ bool SafeToReuse_ = false;
+ bool IsHttps_ = false;
+
+ void FinishHeaders();
+ void FinishMessage();
+ void EnsureHeadersReceived();
+
+ IInvokerPtr ReadInvoker_;
+
+ TSharedRef DoRead();
+
+ void MaybeLogSlowProgress();
+};
+
+DEFINE_REFCOUNTED_TYPE(THttpInput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpOutput
+ : public IResponseWriter
+{
+public:
+ THttpOutput(
+ const THeadersPtr& headers,
+ const NNet::IConnectionPtr& connection,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config);
+
+ THttpOutput(
+ const NNet::IConnectionPtr& connection,
+ EMessageType messageType,
+ const THttpIOConfigPtr& config);
+
+ const THeadersPtr& GetHeaders() override;
+ void SetHeaders(const THeadersPtr& headers);
+ void SetHost(TStringBuf host, TStringBuf port);
+ bool AreHeadersFlushed() const override;
+
+ const THeadersPtr& GetTrailers() override;
+
+ void Flush100Continue();
+
+ void WriteRequest(EMethod method, const TString& path);
+ std::optional<EStatusCode> GetStatus() const override;
+ void SetStatus(EStatusCode status) override;
+
+ TFuture<void> Write(const TSharedRef& data) override;
+ TFuture<void> Flush() override;
+ TFuture<void> Close() override;
+
+ TFuture<void> WriteBody(const TSharedRef& smallBody) override;
+
+ void AddConnectionCloseHeader() override;
+
+ bool IsSafeToReuse() const;
+ void Reset();
+
+ void SetConnectionId(TGuid connectionId);
+ void SetRequestId(TGuid requestId);
+
+ i64 GetWriteByteCount() const override;
+
+private:
+ const NNet::IConnectionPtr Connection_;
+ const EMessageType MessageType_;
+ const THttpIOConfigPtr Config_;
+
+ TClosure OnWriteFinish_;
+
+ //! Debugging.
+ TGuid ConnectionId_;
+ TGuid RequestId_;
+ i64 StartByteCount_ = 0;
+ NNet::TConnectionStatistics StartStatistics_;
+ bool HeadersLogged_ = false;
+ TInstant LastProgressLogTime_;
+
+ static const THashSet<TString, TCaseInsensitiveStringHasher, TCaseInsensitiveStringEqualityComparer> FilteredHeaders_;
+
+ bool ConnectionClose_ = false;
+
+ //! Headers.
+ THeadersPtr Headers_;
+ std::optional<EStatusCode> Status_;
+ std::optional<EMethod> Method_;
+ std::optional<TString> HostHeader_;
+ TString Path_;
+ bool HeadersFlushed_ = false;
+ bool MessageFinished_ = false;
+
+ //! Trailers.
+ THeadersPtr Trailers_;
+
+ TFuture<void> FinishChunked();
+
+ TSharedRef GetHeadersPart(std::optional<size_t> contentLength);
+ TSharedRef GetTrailersPart();
+
+ static TSharedRef GetChunkHeader(size_t size);
+
+ static const TSharedRef Http100Continue;
+ static const TSharedRef CrLf;
+ static const TSharedRef ZeroCrLf;
+ static const TSharedRef ZeroCrLfCrLf;
+
+ void OnWriteFinish();
+};
+
+DEFINE_REFCOUNTED_TYPE(THttpOutput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/unittests/http_ut.cpp b/yt/yt/core/http/unittests/http_ut.cpp
new file mode 100644
index 0000000000..13dd7dbdcf
--- /dev/null
+++ b/yt/yt/core/http/unittests/http_ut.cpp
@@ -0,0 +1,1398 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/test_key.h>
+
+#include <yt/yt/core/http/server.h>
+#include <yt/yt/core/http/client.h>
+#include <yt/yt/core/http/private.h>
+#include <yt/yt/core/http/http.h>
+#include <yt/yt/core/http/stream.h>
+#include <yt/yt/core/http/config.h>
+#include <yt/yt/core/http/helpers.h>
+#include <yt/yt/core/http/connection_pool.h>
+
+#include <yt/yt/core/https/server.h>
+#include <yt/yt/core/https/client.h>
+#include <yt/yt/core/https/config.h>
+
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/listener.h>
+#include <yt/yt/core/net/dialer.h>
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/mock/dialer.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/crypto/tls.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/finally.h>
+#include <yt/yt/core/https/config.h>
+
+#include <library/cpp/testing/common/network.h>
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT::NHttp {
+namespace {
+
+using namespace NYT::NConcurrency;
+using namespace NYT::NNet;
+using namespace NYT::NCrypto;
+using namespace NYT::NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THttpUrlParse, Simple)
+{
+ TString example = "https://user@google.com:12345/a/b/c?foo=bar&zog=%20";
+ auto url = ParseUrl(example);
+
+ ASSERT_EQ(url.Protocol, TStringBuf("https"));
+ ASSERT_EQ(url.Host, TStringBuf("google.com"));
+ ASSERT_EQ(url.User, TStringBuf("user"));
+ ASSERT_EQ(url.PortStr, TStringBuf("12345"));
+ ASSERT_TRUE(url.Port);
+ ASSERT_EQ(*url.Port, 12345);
+ ASSERT_EQ(url.Path, TStringBuf("/a/b/c"));
+ ASSERT_EQ(url.RawQuery, TStringBuf("foo=bar&zog=%20"));
+
+ ASSERT_THROW(ParseUrl(TStringBuf("\0", 1)), TErrorException);
+}
+
+TEST(THttpUrlParse, IPv4)
+{
+ TString example = "https://1.2.3.4:12345/";
+ auto url = ParseUrl(example);
+
+ ASSERT_EQ(url.Host, TStringBuf("1.2.3.4"));
+ ASSERT_EQ(*url.Port, 12345);
+}
+
+TEST(THttpUrlParse, IPv6)
+{
+ TString example = "https://[::1]:12345/";
+ auto url = ParseUrl(example);
+
+ ASSERT_EQ(url.Host, TStringBuf("::1"));
+ ASSERT_EQ(*url.Port, 12345);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THttpCookie, ParseCookie)
+{
+ TString cookieString = "yandexuid=706216621492423338; yandex_login=prime; _ym_d=1529669659; Cookie_check=1; _ym_isad=1;some_cookie_name= some_cookie_value ; abracadabra=";
+ auto cookie = ParseCookies(cookieString);
+
+ ASSERT_EQ("706216621492423338", cookie.at("yandexuid"));
+ ASSERT_EQ("prime", cookie.at("yandex_login"));
+ ASSERT_EQ("1529669659", cookie.at("_ym_d"));
+ ASSERT_EQ("1", cookie.at("_ym_isad"));
+ ASSERT_EQ("some_cookie_value", cookie.at("some_cookie_name"));
+ ASSERT_EQ("", cookie.at("abracadabra"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TString> ToVector(const TCompactVector<TString, 1>& v)
+{
+ return std::vector<TString>(v.begin(), v.end());
+}
+
+TEST(THttpHeaders, Simple)
+{
+ auto headers = New<THeaders>();
+
+ headers->Set("X-Test", "F");
+
+ ASSERT_EQ(std::vector<TString>{{"F"}}, ToVector(headers->GetAll("X-Test")));
+ ASSERT_EQ(TString{"F"}, headers->GetOrThrow("X-Test"));
+ ASSERT_EQ(TString{"F"}, *headers->Find("X-Test"));
+
+ ASSERT_THROW(headers->GetAll("X-Test2"), TErrorException);
+ ASSERT_THROW(headers->GetOrThrow("X-Test2"), TErrorException);
+ ASSERT_FALSE(headers->Find("X-Test2"));
+
+ headers->Add("X-Test", "H");
+ std::vector<TString> expected = {"F", "H"};
+ ASSERT_EQ(expected, ToVector(headers->GetAll("X-Test")));
+
+ headers->Set("X-Test", "J");
+ ASSERT_EQ(std::vector<TString>{{"J"}}, ToVector(headers->GetAll("X-Test")));
+}
+
+TEST(THttpHeaders, HeaderCaseIsIrrelevant)
+{
+ auto headers = New<THeaders>();
+
+ headers->Set("x-tEsT", "F");
+ ASSERT_EQ(TString("F"), headers->GetOrThrow("x-test"));
+ ASSERT_EQ(TString("F"), headers->GetOrThrow("X-Test"));
+
+ TString buffer;
+ TStringOutput output(buffer);
+ headers->WriteTo(&output);
+
+ TString expected = "x-tEsT: F\r\n";
+ ASSERT_EQ(expected, buffer);
+}
+
+
+TEST(THttpHeaders, MessedUpHeaderValuesAreNotAllowed)
+{
+ auto headers = New<THeaders>();
+
+ EXPECT_THROW(headers->Set("X-Newlines", "aaa\r\nbbb\nccc"), TErrorException);
+ EXPECT_THROW(headers->Add("X-Newlines", "aaa\r\nbbb\nccc"), TErrorException);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFakeConnection
+ : public IConnection
+{
+ TString Input;
+ TString Output;
+
+ bool SetNoDelay() override
+ {
+ return true;
+ }
+
+ bool SetKeepAlive() override
+ {
+ return true;
+ }
+
+ TFuture<size_t> Read(const TSharedMutableRef& ref) override
+ {
+ size_t toCopy = std::min(ref.Size(), Input.size());
+ std::copy_n(Input.data(), toCopy, ref.Begin());
+ Input = Input.substr(toCopy);
+ return MakeFuture(toCopy);
+ }
+
+ TFuture<void> Write(const TSharedRef& ref) override
+ {
+ Output += TString(ref.Begin(), ref.Size());
+ return VoidFuture;
+ }
+
+ TFuture<void> WriteV(const TSharedRefArray& refs) override
+ {
+ for (const auto& ref : refs) {
+ Output += TString(ref.Begin(), ref.Size());
+ }
+ return VoidFuture;
+ }
+
+ TFuture<void> Close() override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ bool IsIdle() const override
+ {
+ return true;
+ }
+
+ TFuture<void> Abort() override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ TFuture<void> CloseRead() override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ TFuture<void> CloseWrite() override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ const TNetworkAddress& LocalAddress() const override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ const TNetworkAddress& RemoteAddress() const override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ int GetHandle() const override
+ {
+ THROW_ERROR_EXCEPTION("Not implemented");
+ }
+
+ TConnectionStatistics GetReadStatistics() const override
+ {
+ return {};
+ }
+
+ TConnectionStatistics GetWriteStatistics() const override
+ {
+ return {};
+ }
+
+ i64 GetReadByteCount() const override
+ {
+ return 0;
+ }
+
+ i64 GetWriteByteCount() const override
+ {
+ return 0;
+ }
+
+ void SetReadDeadline(std::optional<TInstant> /*deadline*/) override
+ { }
+
+ void SetWriteDeadline(std::optional<TInstant> /*deadline*/) override
+ { }
+
+ void SubscribePeerDisconnect(TCallback<void()> /*cb*/) override
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFakeConnection)
+
+void FinishBody(THttpOutput* out)
+{
+ WaitFor(out->Close()).ThrowOnError();
+}
+
+void WriteChunk(THttpOutput* out, TStringBuf chunk)
+{
+ WaitFor(out->Write(TSharedRef::FromString(TString(chunk)))).ThrowOnError();
+}
+
+void WriteBody(THttpOutput* out, TStringBuf body)
+{
+ WaitFor(out->WriteBody(TSharedRef::FromString(TString(body)))).ThrowOnError();
+}
+
+TEST(THttpOutputTest, Full)
+{
+ typedef std::tuple<EMessageType, TString, std::function<void(THttpOutput*)>> TTestCase;
+ std::vector<TTestCase> table = {
+ TTestCase{
+ EMessageType::Request,
+ "GET / HTTP/1.1\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->WriteRequest(EMethod::Get, "/");
+ FinishBody(out);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "POST / HTTP/1.1\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->WriteRequest(EMethod::Post, "/");
+ FinishBody(out);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "POST / HTTP/1.1\r\n"
+ "Content-Length: 1\r\n"
+ "\r\n"
+ "x",
+ [] (THttpOutput* out) {
+ out->WriteRequest(EMethod::Post, "/");
+ WriteBody(out, TStringBuf("x"));
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "POST / HTTP/1.1\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "1\r\n"
+ "X\r\n"
+ "A\r\n" // hex(10)
+ "0123456789\r\n"
+ "0\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->WriteRequest(EMethod::Post, "/");
+
+ WriteChunk(out, TStringBuf("X"));
+ WriteChunk(out, TStringBuf("0123456789"));
+ FinishBody(out);
+ }
+ },
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->SetStatus(EStatusCode::OK);
+ FinishBody(out);
+ }
+ },
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 400 Bad Request\r\n"
+ "Content-Length: 0\r\n"
+ "X-YT-Response-Code: 500\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->SetStatus(EStatusCode::BadRequest);
+ out->GetTrailers()->Add("X-YT-Response-Code", "500");
+ FinishBody(out);
+ }
+ },
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "Content-Length: 4\r\n"
+ "\r\n"
+ "fail",
+ [] (THttpOutput* out) {
+ out->SetStatus(EStatusCode::InternalServerError);
+ WriteBody(out, TStringBuf("fail"));
+ }
+ },
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 200 OK\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "1\r\n"
+ "X\r\n"
+ "A\r\n" // hex(10)
+ "0123456789\r\n"
+ "0\r\n"
+ "\r\n",
+ [] (THttpOutput* out) {
+ out->SetStatus(EStatusCode::OK);
+
+ WriteChunk(out, TStringBuf("X"));
+ WriteChunk(out, TStringBuf("0123456789"));
+ FinishBody(out);
+ }
+ },
+ };
+
+ for (auto [messageType, expected, callback]: table) {
+ auto fake = New<TFakeConnection>();
+ auto config = New<THttpIOConfig>();
+ auto output = New<THttpOutput>(fake, messageType, config);
+
+ try {
+ callback(output.Get());
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Failed to write output"
+ << expected
+ << ex.what();
+ }
+ ASSERT_EQ(fake->Output, expected);
+ }
+}
+
+TEST(THttpOutputTest, LargeResponse)
+{
+#ifdef _unix_
+ constexpr ui64 SizeGib = 4;
+#else
+ constexpr ui64 SizeGib = 1;
+#endif
+
+ constexpr ui64 Size = (SizeGib << 30) + 1;
+ const auto body = TString(Size, 'x');
+
+ struct TLargeFakeConnection
+ : public TFakeConnection
+ {
+ TFuture<void> WriteV(const TSharedRefArray& refs) override
+ {
+ for (const auto& ref : refs) {
+ if (ref.Size() == Size) {
+ LargeRef = ref;
+ } else {
+ Output += TString(ref.Begin(), ref.Size());
+ }
+ }
+ return VoidFuture;
+ }
+
+ TSharedRef LargeRef;
+ };
+
+ auto fake = New<TLargeFakeConnection>();
+ auto config = New<THttpIOConfig>();
+ auto output = New<THttpOutput>(fake, EMessageType::Response, config);
+
+ output->SetStatus(EStatusCode::OK);
+ WriteChunk(output.Get(), body);
+ FinishBody(output.Get());
+
+ // The large part is skipped and saved in LargeRef field.
+ ASSERT_EQ(fake->Output, Format(
+ "HTTP/1.1 200 OK\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "%llX\r\n"
+ "\r\n"
+ "0\r\n"
+ "\r\n",
+ Size
+ ));
+
+ if (TStringBuf(fake->LargeRef.Begin(), fake->LargeRef.Size()) != body) {
+ ADD_FAILURE() << "Wrong large chunk";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+void ExpectBodyPart(THttpInput* in, TStringBuf chunk)
+{
+ ASSERT_EQ(chunk, ToString(WaitFor(in->Read()).ValueOrThrow()));
+}
+
+void ExpectBodyEnd(THttpInput* in)
+{
+ ASSERT_EQ(0u, WaitFor(in->Read()).ValueOrThrow().Size());
+}
+
+TEST(THttpInputTest, Simple)
+{
+ typedef std::tuple<EMessageType, TString, std::function<void(THttpInput*)>> TTestCase;
+ std::vector<TTestCase> table = {
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 200 OK\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetStatusCode(), EStatusCode::OK);
+ ExpectBodyEnd(in);
+ }
+ },
+ TTestCase{
+ EMessageType::Response,
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetStatusCode(), EStatusCode::InternalServerError);
+ ExpectBodyEnd(in);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "GET / HTTP/1.1\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetMethod(), EMethod::Get);
+ EXPECT_EQ(in->GetUrl().Path, TStringBuf("/"));
+ ExpectBodyEnd(in);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "GET / HTTP/1.1\r\n"
+ "X-Foo: test\r\n"
+ "X-Foo0: test-test-test\r\n"
+ "X-FooFooFoo: test-test-test\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetMethod(), EMethod::Get);
+ EXPECT_EQ(in->GetUrl().Path, TStringBuf("/"));
+ auto headers = in->GetHeaders();
+
+ ASSERT_EQ(TString("test"), headers->GetOrThrow("X-Foo"));
+ ASSERT_EQ(TString("test-test-test"), headers->GetOrThrow("X-Foo0"));
+ ASSERT_EQ(TString("test-test-test"), headers->GetOrThrow("X-FooFooFoo"));
+ ExpectBodyEnd(in);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "POST / HTTP/1.1\r\n"
+ "Content-Length: 6\r\n"
+ "\r\n"
+ "foobar",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetMethod(), EMethod::Post);
+ ExpectBodyPart(in, "foobar");
+ ExpectBodyEnd(in);
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "POST /chunked_w_trailing_headers HTTP/1.1\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "X-Foo: test\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "5\r\nhello\r\n"
+ "6\r\n world\r\n"
+ "0\r\n"
+ "Vary: *\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(in->GetMethod(), EMethod::Post);
+ EXPECT_EQ(in->GetUrl().Path, TStringBuf("/chunked_w_trailing_headers"));
+
+ auto headers = in->GetHeaders();
+ ASSERT_EQ(TString("test"), headers->GetOrThrow("X-Foo"));
+
+ ASSERT_THROW(in->GetTrailers(), TErrorException);
+
+ ExpectBodyPart(in, "hell");
+ ExpectBodyPart(in, "o");
+ ExpectBodyPart(in, " world");
+ ExpectBodyEnd(in);
+
+ auto trailers = in->GetTrailers();
+ ASSERT_EQ(TString("*"), trailers->GetOrThrow("Vary"));
+ ASSERT_EQ(TString("text/plain"), trailers->GetOrThrow("Content-Type"));
+ }
+ },
+ TTestCase{
+ EMessageType::Request,
+ "GET http://yt/foo HTTP/1.1\r\n"
+ "\r\n",
+ [] (THttpInput* in) {
+ EXPECT_EQ(TStringBuf("yt"), in->GetUrl().Host);
+ }
+ }
+ };
+
+ for (auto testCase : table) {
+ auto fake = New<TFakeConnection>();
+ fake->Input = std::get<1>(testCase);
+ auto config = New<THttpIOConfig>();
+ config->ReadBufferSize = 16;
+
+ auto input = New<THttpInput>(fake, TNetworkAddress(), GetSyncInvoker(), std::get<0>(testCase), config);
+
+ try {
+ std::get<2>(testCase)(input.Get());
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "Failed to parse input:"
+ << std::endl << "==============" << std::endl
+ << std::get<1>(testCase)
+ << std::endl << "==============" << std::endl
+ << ex.what();
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THttpServerTest
+ : public ::testing::TestWithParam<bool>
+{
+protected:
+ IPollerPtr Poller;
+ TServerConfigPtr ServerConfig;
+ IServerPtr Server;
+ IClientPtr Client;
+
+ NTesting::TPortHolder TestPort;
+ TString TestUrl;
+
+private:
+ void SetupServer(const NHttp::TServerConfigPtr& config)
+ {
+ config->Port = TestPort;
+ }
+
+ void SetupClient(const NHttp::TClientConfigPtr& /*config*/)
+ { }
+
+ void SetUp() override
+ {
+ TestPort = NTesting::GetFreePort();
+ TestUrl = Format("http://localhost:%v", TestPort);
+ Poller = CreateThreadPoolPoller(4, "HttpTest");
+ if (!GetParam()) {
+ ServerConfig = New<NHttp::TServerConfig>();
+ SetupServer(ServerConfig);
+ Server = NHttp::CreateServer(ServerConfig, Poller);
+
+ auto clientConfig = New<NHttp::TClientConfig>();
+ SetupClient(clientConfig);
+ Client = NHttp::CreateClient(clientConfig, Poller);
+ } else {
+ auto serverConfig = New<NHttps::TServerConfig>();
+ serverConfig->Credentials = New<NHttps::TServerCredentialsConfig>();
+ serverConfig->Credentials->PrivateKey = New<TPemBlobConfig>();
+ serverConfig->Credentials->PrivateKey->Value = TestCertificate;
+ serverConfig->Credentials->CertChain = New<TPemBlobConfig>();
+ serverConfig->Credentials->CertChain->Value = TestCertificate;
+ SetupServer(serverConfig);
+ ServerConfig = serverConfig;
+ Server = NHttps::CreateServer(serverConfig, Poller);
+
+ auto clientConfig = New<NHttps::TClientConfig>();
+ clientConfig->Credentials = New<NHttps::TClientCredentialsConfig>();
+ clientConfig->Credentials->PrivateKey = New<TPemBlobConfig>();
+ clientConfig->Credentials->PrivateKey->Value = TestCertificate;
+ clientConfig->Credentials->CertChain = New<TPemBlobConfig>();
+ clientConfig->Credentials->CertChain->Value = TestCertificate;
+ SetupClient(clientConfig);
+ Client = NHttps::CreateClient(clientConfig, Poller);
+ }
+ }
+
+ void TearDown() override
+ {
+ Server->Stop();
+ Server.Reset();
+ Poller->Shutdown();
+ Poller.Reset();
+ TestPort.Reset();
+ }
+};
+
+class TOKHttpHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+ rsp->SetStatus(EStatusCode::OK);
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+};
+
+TEST_P(THttpServerTest, SimpleRequest)
+{
+ Server->AddHandler("/ok", New<TOKHttpHandler>());
+ Server->Start();
+
+ auto rsp = WaitFor(Client->Get(TestUrl + "/ok")).ValueOrThrow();
+ ASSERT_EQ(EStatusCode::OK, rsp->GetStatusCode());
+}
+
+class TEchoHttpHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp) override
+ {
+ rsp->SetStatus(EStatusCode::OK);
+ while (true) {
+ auto data = WaitFor(req->Read()).ValueOrThrow();
+ if (data.Size() == 0) {
+ break;
+ }
+ WaitFor(rsp->Write(data)).ThrowOnError();
+ }
+
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+};
+
+TString ReadAll(const IAsyncZeroCopyInputStreamPtr& in)
+{
+ TString buf;
+ while (true) {
+ auto data = WaitFor(in->Read()).ValueOrThrow();
+ if (data.Size() == 0) {
+ break;
+ }
+
+ buf += ToString(data);
+ }
+
+ return buf;
+}
+
+
+TEST_P(THttpServerTest, TransferSmallBody)
+{
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto reqBody = TSharedMutableRef::Allocate(1024);
+ std::fill(reqBody.Begin(), reqBody.End(), 0xab);
+
+ auto rsp = WaitFor(Client->Post(TestUrl + "/echo", reqBody)).ValueOrThrow();
+ ASSERT_EQ(EStatusCode::OK, rsp->GetStatusCode());
+
+ auto rspBody = ReadAll(rsp);
+ ASSERT_EQ(TString(reqBody.Begin(), reqBody.Size()), rspBody);
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+TEST_P(THttpServerTest, TransferSmallBodyUsingStreaming)
+{
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto reqBody = TSharedMutableRef::Allocate(1024);
+ std::fill(reqBody.Begin(), reqBody.End(), 0xab);
+
+ auto activeRequest = WaitFor(Client->StartPost(TestUrl + "/echo")).ValueOrThrow();
+ WaitFor(activeRequest->GetRequestStream()->Write(reqBody)).ThrowOnError();
+ auto rsp = WaitFor(activeRequest->Finish()).ValueOrThrow();
+
+ ASSERT_EQ(EStatusCode::OK, rsp->GetStatusCode());
+
+ auto rspBody = ReadAll(rsp);
+ ASSERT_EQ(TString(reqBody.Begin(), reqBody.Size()), rspBody);
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TTestStatusCodeHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+ rsp->SetStatus(Code);
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+
+ EStatusCode Code = EStatusCode::OK;
+};
+
+TEST_P(THttpServerTest, StatusCode)
+{
+ auto handler = New<TTestStatusCodeHandler>();
+ Server->AddHandler("/code", handler);
+ Server->Start();
+
+ handler->Code = EStatusCode::NotFound;
+ ASSERT_EQ(EStatusCode::NotFound,
+ WaitFor(Client->Get(TestUrl + "/code"))
+ .ValueOrThrow()
+ ->GetStatusCode());
+
+ handler->Code = EStatusCode::Forbidden;
+ ASSERT_EQ(EStatusCode::Forbidden,
+ WaitFor(Client->Get(TestUrl + "/code"))
+ .ValueOrThrow()
+ ->GetStatusCode());
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TTestHeadersHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp) override
+ {
+ for (const auto& header : ExpectedHeaders) {
+ EXPECT_EQ(header.second, req->GetHeaders()->GetOrThrow(header.first));
+ }
+
+ for (const auto& header : ReplyHeaders) {
+ rsp->GetHeaders()->Add(header.first, header.second);
+ }
+
+ rsp->SetStatus(EStatusCode::OK);
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+
+ std::vector<std::pair<TString, TString>> ReplyHeaders, ExpectedHeaders;
+};
+
+TEST_P(THttpServerTest, HeadersTest)
+{
+ auto handler = New<TTestHeadersHandler>();
+ handler->ExpectedHeaders = {
+ { "X-Yt-Test", "foo; bar; zog" },
+ { "Accept-Charset", "utf-8" }
+ };
+ handler->ReplyHeaders = {
+ { "Content-Type", "test/plain; charset=utf-8" },
+ { "Cache-Control", "nocache" }
+ };
+
+ Server->AddHandler("/headers", handler);
+ Server->Start();
+
+ auto headers = New<THeaders>();
+ headers->Add("X-Yt-Test", "foo; bar; zog");
+ headers->Add("Accept-Charset", "utf-8");
+
+ auto rsp = WaitFor(Client->Get(TestUrl + "/headers", headers)).ValueOrThrow();
+ EXPECT_EQ("nocache", rsp->GetHeaders()->GetOrThrow("Cache-Control"));
+ EXPECT_EQ("test/plain; charset=utf-8", rsp->GetHeaders()->GetOrThrow("Content-Type"));
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TTestTrailersHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+ WaitFor(rsp->Write(TSharedRef::FromString("test"))).ThrowOnError();
+
+ rsp->GetTrailers()->Set("X-Yt-Test", "foo; bar");
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+};
+
+TEST_P(THttpServerTest, TrailersTest)
+{
+ auto handler = New<TTestTrailersHandler>();
+
+ Server->AddHandler("/trailers", handler);
+ Server->Start();
+
+ auto rsp = WaitFor(Client->Get(TestUrl + "/trailers")).ValueOrThrow();
+ auto body = ReadAll(rsp);
+ EXPECT_EQ("foo; bar", rsp->GetTrailers()->GetOrThrow("X-Yt-Test"));
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class THangingHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& /*rsp*/) override
+ { }
+};
+
+class TImpatientHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+ WaitFor(rsp->Write(TSharedRef::FromString("body"))).ThrowOnError();
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+};
+
+class TForgetfulHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+ rsp->SetStatus(EStatusCode::OK);
+ }
+};
+
+TEST_P(THttpServerTest, WeirdHandlers)
+{
+ auto hanging = New<THangingHandler>();
+ auto impatient = New<TImpatientHandler>();
+ auto forgetful = New<TForgetfulHandler>();
+
+ Server->AddHandler("/hanging", hanging);
+ Server->AddHandler("/impatient", impatient);
+ Server->AddHandler("/forgetful", forgetful);
+ Server->Start();
+
+ EXPECT_THROW(
+ WaitFor(Client->Get(TestUrl + "/hanging"))
+ .ValueOrThrow()
+ ->GetStatusCode(),
+ TErrorException);
+ EXPECT_EQ(
+ WaitFor(Client->Get(TestUrl + "/impatient"))
+ .ValueOrThrow()
+ ->GetStatusCode(),
+ EStatusCode::InternalServerError);
+ EXPECT_THROW(
+ WaitFor(Client->Get(TestUrl + "/forgetful"))
+ .ValueOrThrow()
+ ->GetStatusCode(),
+ TErrorException);
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TThrowingHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& /*rsp*/) override
+ {
+ THROW_ERROR_EXCEPTION("Your request is bad");
+ }
+};
+
+TEST_P(THttpServerTest, ThrowingHandler)
+{
+ auto throwing = New<TThrowingHandler>();
+
+ Server->AddHandler("/throwing", throwing);
+ Server->Start();
+
+ ASSERT_EQ(EStatusCode::InternalServerError,
+ WaitFor(Client->Get(TestUrl + "/throwing"))
+ .ValueOrThrow()
+ ->GetStatusCode());
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TConsumingHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp) override
+ {
+ while (WaitFor(req->Read()).ValueOrThrow().Size() != 0)
+ { }
+
+ rsp->SetStatus(EStatusCode::OK);
+ WaitFor(rsp->Close()).ThrowOnError();
+ }
+};
+
+TEST_P(THttpServerTest, RequestStreaming)
+{
+ Server->AddHandler("/consuming", New<TConsumingHandler>());
+ Server->Start();
+
+#ifdef _win_
+ constexpr int BodySizeMib = 1;
+#else
+ constexpr int BodySizeMib = 128;
+#endif
+
+ auto body = TSharedMutableRef::Allocate(BodySizeMib * 1024 * 1024);
+ ASSERT_EQ(EStatusCode::OK,
+ WaitFor(Client->Post(TestUrl + "/consuming", body))
+ .ValueOrThrow()->GetStatusCode());
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+class TStreamingHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& rsp) override
+ {
+#ifdef _win_
+ constexpr int BodySizeKib = 64;
+#else
+ constexpr int BodySizeKib = 16 * 1024;
+#endif
+
+ rsp->SetStatus(EStatusCode::OK);
+ auto data = TSharedRef::FromString(TString(1024, 'f'));
+ for (int i = 0; i < BodySizeKib; i++) {
+ WaitFor(rsp->Write(data))
+ .ThrowOnError();
+ }
+
+ WaitFor(rsp->Close())
+ .ThrowOnError();
+ }
+};
+
+TEST_P(THttpServerTest, ResponseStreaming)
+{
+ Server->AddHandler("/streaming", New<TStreamingHandler>());
+ Server->Start();
+
+#ifdef _win_
+ constexpr int BodySizeKib = 64;
+#else
+ constexpr int BodySizeKib = 16 * 1024;
+#endif
+
+ auto rsp = WaitFor(Client->Get(TestUrl + "/streaming")).ValueOrThrow();
+ ASSERT_EQ(BodySizeKib * 1024, std::ssize(ReadAll(rsp)));
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+}
+
+const auto& Logger = HttpLogger;
+
+class TCancelingHandler
+ : public IHttpHandler
+{
+public:
+ TPromise<void> Canceled = NewPromise<void>();
+
+ void HandleRequest(const IRequestPtr& /*req*/, const IResponseWriterPtr& /*rsp*/) override
+ {
+ auto finally = Finally([this] {
+ YT_LOG_DEBUG("Running finally block");
+ Canceled.Set();
+ });
+
+ auto p = NewPromise<void>();
+ p.OnCanceled(BIND([p] (const TError& error) {
+ YT_LOG_INFO(error, "Promise is canceled");
+ p.Set(error);
+ }));
+
+ YT_LOG_DEBUG("Blocking on promise");
+ WaitFor(p.ToFuture())
+ .ThrowOnError();
+ }
+};
+
+TEST_P(THttpServerTest, RequestCancel)
+{
+ if (GetParam()) {
+ return;
+ }
+
+#if defined(_darwin_) || defined(_win_)
+ return;
+#endif
+
+ auto handler = New<TCancelingHandler>();
+
+ ServerConfig->CancelFiberOnConnectionClose = true;
+ Server->AddHandler("/cancel", handler);
+ Server->Start();
+
+ auto dialer = CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+ auto connection = WaitFor(dialer->Dial(TNetworkAddress::CreateIPv6Loopback(TestPort)))
+ .ValueOrThrow();
+ WaitFor(connection->Write(TSharedRef::FromString("POST /cancel HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")))
+ .ThrowOnError();
+
+ Sleep(TDuration::Seconds(1));
+ YT_LOG_DEBUG("Closing client connection");
+ WaitFor(connection->CloseWrite())
+ .ThrowOnError();
+
+ WaitFor(handler->Canceled.ToFuture())
+ .ThrowOnError();
+}
+
+class TValidateErrorHandler
+ : public IHttpHandler
+{
+public:
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& /*rsp*/) override
+ {
+ ASSERT_THROW(ReadAll(req), TErrorException);
+ Ok = true;
+ }
+
+ bool Ok = false;
+};
+
+TEST_P(THttpServerTest, RequestHangUp)
+{
+ if (GetParam()) {
+ // This test is not TLS-specific.
+ return;
+ }
+
+ auto validating = New<TValidateErrorHandler>();
+ Server->AddHandler("/validating", validating);
+ Server->Start();
+
+ auto dialer = CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+ auto connection = WaitFor(dialer->Dial(TNetworkAddress::CreateIPv6Loopback(TestPort)))
+ .ValueOrThrow();
+ WaitFor(connection->Write(TSharedRef::FromString("POST /validating HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")))
+ .ThrowOnError();
+ WaitFor(connection->CloseWrite())
+ .ThrowOnError();
+ auto bytesRead = WaitFor(connection->Read(TSharedMutableRef::Allocate(1)))
+ .ValueOrThrow();
+ ASSERT_EQ(0u, bytesRead);
+
+ Server->Stop();
+ Sleep(TDuration::MilliSeconds(10));
+
+ EXPECT_TRUE(validating->Ok);
+}
+
+TEST_P(THttpServerTest, ConnectionKeepAlive)
+{
+ if (GetParam()) {
+ // This test is not TLS-specific.
+ return;
+ }
+
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto dialer = CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+
+ // Many requests.
+ {
+ auto connection = WaitFor(dialer->Dial(TNetworkAddress::CreateIPv6Loopback(TestPort)))
+ .ValueOrThrow();
+
+ auto request = New<THttpOutput>(
+ connection,
+ EMessageType::Request,
+ New<THttpIOConfig>());
+
+ auto response = New<THttpInput>(
+ connection,
+ connection->RemoteAddress(),
+ Poller->GetInvoker(),
+ EMessageType::Response,
+ New<THttpIOConfig>());
+
+ for (int i = 0; i < 10; ++i) {
+ request->WriteRequest(EMethod::Post, "/echo");
+ WaitFor(request->Write(TSharedRef::FromString("foo")))
+ .ThrowOnError();
+ WaitFor(request->Close())
+ .ThrowOnError();
+
+ response->GetStatusCode();
+ auto body = response->ReadAll();
+
+ ASSERT_TRUE(response->IsSafeToReuse());
+ ASSERT_TRUE(request->IsSafeToReuse());
+ response->Reset();
+ request->Reset();
+ }
+ }
+
+ // Pipelining
+ {
+ auto connection = WaitFor(dialer->Dial(TNetworkAddress::CreateIPv6Loopback(TestPort)))
+ .ValueOrThrow();
+
+ auto request = New<THttpOutput>(
+ connection,
+ EMessageType::Request,
+ New<THttpIOConfig>());
+
+ auto response = New<THttpInput>(
+ connection,
+ connection->RemoteAddress(),
+ Poller->GetInvoker(),
+ EMessageType::Response,
+ New<THttpIOConfig>());
+
+ for (int i = 0; i < 10; ++i) {
+ request->WriteRequest(EMethod::Post, "/echo");
+ WaitFor(request->Write(TSharedRef::FromString("foo")))
+ .ThrowOnError();
+ WaitFor(request->Close())
+ .ThrowOnError();
+
+ ASSERT_TRUE(request->IsSafeToReuse());
+ request->Reset();
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ response->GetStatusCode();
+ auto body = response->ReadAll();
+
+ ASSERT_TRUE(response->IsSafeToReuse());
+ response->Reset();
+ }
+ }
+}
+
+TEST_P(THttpServerTest, ReuseConnections)
+{
+ if (GetParam()) {
+ // This test is not TLS-specific.
+ return;
+ }
+
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto dialer = NNet::CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+ auto dialerMock = New<TDialerMock>(dialer);
+ auto clientConfig = New<NHttp::TClientConfig>();
+ clientConfig->MaxIdleConnections = 2;
+ auto client = CreateClient(clientConfig, dialerMock, Poller->GetInvoker());
+
+ EXPECT_CALL(*dialerMock, Dial).Times(2);
+
+ auto reqBody = TSharedMutableRef::Allocate(1024);
+
+ for (int i = 0; i < 5; ++i) {
+ std::fill(reqBody.Begin(), reqBody.End(), i);
+
+ auto rsp1 = WaitFor(client->Post(TestUrl + "/echo", reqBody)).ValueOrThrow();
+ auto rsp2 = WaitFor(client->Post(TestUrl + "/echo", reqBody)).ValueOrThrow();
+ ASSERT_EQ(EStatusCode::OK, rsp1->GetStatusCode());
+ ASSERT_EQ(EStatusCode::OK, rsp2->GetStatusCode());
+
+ auto rsp1Body = ReadAll(rsp1);
+ auto rsp2Body = ReadAll(rsp2);
+ ASSERT_EQ(TString(reqBody.Begin(), reqBody.Size()), rsp1Body);
+ ASSERT_EQ(TString(reqBody.Begin(), reqBody.Size()), rsp2Body);
+ }
+}
+
+TEST_P(THttpServerTest, DropConnectionsByTimeout)
+{
+ if (GetParam()) {
+ // This test is not TLS-specific.
+ return;
+ }
+
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto dialer = NNet::CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+ auto dialerMock = New<TDialerMock>(dialer);
+ auto clientConfig = New<NHttp::TClientConfig>();
+ clientConfig->MaxIdleConnections = 1;
+ clientConfig->ConnectionIdleTimeout = TDuration::MilliSeconds(300);
+
+ auto client = CreateClient(clientConfig, dialerMock, Poller->GetInvoker());
+
+ auto reqBody = TSharedMutableRef::Allocate(1024);
+
+ for (int i = 0; i < 5; ++i) {
+ if (i > 0) {
+ Sleep(TDuration::MilliSeconds(300));
+ }
+
+ std::fill(reqBody.Begin(), reqBody.End(), i);
+
+ EXPECT_CALL(*dialerMock, Dial);
+ auto rsp = WaitFor(client->Post(TestUrl + "/echo", reqBody)).ValueOrThrow();
+ ASSERT_EQ(EStatusCode::OK, rsp->GetStatusCode());
+
+ auto rspBody = ReadAll(rsp);
+ ASSERT_EQ(TString(reqBody.Begin(), reqBody.Size()), rspBody);
+ }
+}
+
+
+TEST_P(THttpServerTest, ConnectionsDropRoutine)
+{
+ if (GetParam()) {
+ // This test is not TLS-specific.
+ return;
+ }
+
+ Server->AddHandler("/echo", New<TEchoHttpHandler>());
+ Server->Start();
+
+ auto dialer = NNet::CreateDialer(New<TDialerConfig>(), Poller, HttpLogger);
+ auto dialerMock = New<TDialerMock>(dialer);
+ auto clientConfig = New<NHttp::TClientConfig>();
+ clientConfig->MaxIdleConnections = 1;
+ clientConfig->ConnectionIdleTimeout = TDuration::MilliSeconds(100);
+
+ auto pool = New<TConnectionPool>(dialerMock, clientConfig, Poller->GetInvoker());
+
+ auto url = ParseUrl(TestUrl + "/echo");
+ auto address = TNetworkAddress::CreateIPv6Loopback(*url.Port);
+ pool->Release(WaitFor(dialer->Dial(address)).ValueOrThrow());
+
+ Sleep(TDuration::MilliSeconds(220));
+
+ EXPECT_CALL(*dialerMock, Dial).WillOnce(testing::Return(MakeFuture<IConnectionPtr>(nullptr)));
+ pool->Connect(address);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+INSTANTIATE_TEST_SUITE_P(WithoutTls, THttpServerTest, ::testing::Values(false));
+INSTANTIATE_TEST_SUITE_P(WithTls, THttpServerTest, ::testing::Values(true));
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THttpServerTest, TestOwnPoller)
+{
+ auto port = NTesting::GetFreePort();
+ auto url = Format("http://localhost:%v", port);
+
+ auto config = New<NHttp::TServerConfig>();
+ config->Port = port;
+ auto server = NHttp::CreateServer(config);
+ server->Start();
+ server->Stop();
+ // this test will cause memory leak w/o calling shutdown for IPoller in server
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THttpHandlerMatchingTest, Simple)
+{
+ auto h1 = New<TOKHttpHandler>();
+ auto h2 = New<TOKHttpHandler>();
+ auto h3 = New<TOKHttpHandler>();
+
+ auto handlers = New<TRequestPathMatcher>();
+ ASSERT_TRUE(handlers->IsEmpty());
+ handlers->Add("/", h1);
+ handlers->Add("/a", h2);
+ handlers->Add("/a/b", h3);
+ ASSERT_FALSE(handlers->IsEmpty());
+
+ EXPECT_EQ(h1.Get(), handlers->Match(TStringBuf("/")).Get());
+ EXPECT_EQ(h1.Get(), handlers->Match(TStringBuf("/c")).Get());
+
+ EXPECT_EQ(h2.Get(), handlers->Match(TStringBuf("/a")).Get());
+ EXPECT_EQ(h1.Get(), handlers->Match(TStringBuf("/a/")).Get());
+
+ EXPECT_EQ(h3.Get(), handlers->Match(TStringBuf("/a/b")).Get());
+ EXPECT_EQ(h1.Get(), handlers->Match(TStringBuf("/a/b/")).Get());
+
+ auto handlers2 = New<TRequestPathMatcher>();
+ handlers2->Add("/a/", h2);
+ EXPECT_FALSE(handlers2->Match(TStringBuf("/")).Get());
+ EXPECT_EQ(h2.Get(), handlers2->Match(TStringBuf("/a")).Get());
+ EXPECT_EQ(h2.Get(), handlers2->Match(TStringBuf("/a/")).Get());
+ EXPECT_EQ(h2.Get(), handlers2->Match(TStringBuf("/a/b")).Get());
+
+ auto handlers3 = New<TRequestPathMatcher>();
+ handlers3->Add("/a/", h2);
+ handlers3->Add("/a", h3);
+
+ EXPECT_EQ(h3.Get(), handlers3->Match(TStringBuf("/a")).Get());
+ EXPECT_EQ(h2.Get(), handlers3->Match(TStringBuf("/a/")).Get());
+ EXPECT_EQ(h2.Get(), handlers3->Match(TStringBuf("/a/b")).Get());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TRangeHeadersTest, Test)
+{
+ auto headers = New<THeaders>();
+ EXPECT_EQ(FindBytesRange(headers), std::nullopt);
+
+ headers->Set("Range", "bytes=2-1234");
+ std::pair<i64, i64> result{2, 1234};
+ EXPECT_EQ(FindBytesRange(headers), result);
+
+ headers->Set("Range", "bytes=1234-");
+ EXPECT_ANY_THROW(FindBytesRange(headers));
+
+ headers->Set("Range", "bytes=junk");
+ EXPECT_ANY_THROW(FindBytesRange(headers));
+
+ headers->Set("Range", "bytes=1-2, 3-");
+ EXPECT_ANY_THROW(FindBytesRange(headers));
+
+ headers->Set("Range", "bytes=-2");
+ EXPECT_ANY_THROW(FindBytesRange(headers));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/http/unittests/ya.make b/yt/yt/core/http/unittests/ya.make
new file mode 100644
index 0000000000..3a810e504b
--- /dev/null
+++ b/yt/yt/core/http/unittests/ya.make
@@ -0,0 +1,45 @@
+GTEST(unittester-core-http)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ http_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ library/cpp/testing/common
+ yt/yt/core
+ yt/yt/core/http
+ yt/yt/core/https
+ yt/yt/core/net/mock
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ ram:32
+ ram:32
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+REQUIREMENTS(ram:32)
+
+END()
diff --git a/yt/yt/core/http/ya.make b/yt/yt/core/http/ya.make
new file mode 100644
index 0000000000..c3cf704d56
--- /dev/null
+++ b/yt/yt/core/http/ya.make
@@ -0,0 +1,21 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ client.cpp
+ config.cpp
+ connection_pool.cpp
+ connection_reuse_helpers.cpp
+ http.cpp
+ server.cpp
+ stream.cpp
+ helpers.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+ contrib/restricted/http-parser
+)
+
+END()
diff --git a/yt/yt/core/https/client.cpp b/yt/yt/core/https/client.cpp
new file mode 100644
index 0000000000..2f4b415b89
--- /dev/null
+++ b/yt/yt/core/https/client.cpp
@@ -0,0 +1,139 @@
+#include "client.h"
+#include "config.h"
+
+#include <yt/yt/core/http/client.h>
+#include <yt/yt/core/http/private.h>
+
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/crypto/tls.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+
+#include <library/cpp/openssl/io/stream.h>
+
+namespace NYT::NHttps {
+
+using namespace NNet;
+using namespace NHttp;
+using namespace NCrypto;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClient
+ : public IClient
+{
+public:
+ explicit TClient(IClientPtr underlying)
+ : Underlying_(std::move(underlying))
+ { }
+
+ TFuture<IResponsePtr> Get(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->Get(url, headers);
+ }
+
+ TFuture<IResponsePtr> Post(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->Post(url, body, headers);
+ }
+
+ TFuture<IResponsePtr> Patch(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->Patch(url, body, headers);
+ }
+
+ TFuture<IResponsePtr> Put(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->Put(url, body, headers);
+ }
+
+ TFuture<IResponsePtr> Delete(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->Delete(url, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPost(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->StartPost(url, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPatch(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->StartPatch(url, headers);
+ }
+
+ TFuture<IActiveRequestPtr> StartPut(
+ const TString& url,
+ const THeadersPtr& headers) override
+ {
+ return Underlying_->StartPut(url, headers);
+ }
+
+private:
+ const IClientPtr Underlying_;
+};
+
+IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const IPollerPtr& poller)
+{
+ auto sslContext = New<TSslContext>();
+ if (config->Credentials) {
+ if (config->Credentials->CertChain) {
+ if (config->Credentials->CertChain->FileName) {
+ sslContext->AddCertificateChainFromFile(*config->Credentials->CertChain->FileName);
+ } else if (config->Credentials->CertChain->Value) {
+ sslContext->AddCertificateChain(*config->Credentials->CertChain->Value);
+ } else {
+ THROW_ERROR_EXCEPTION("Neither \"file_name\" nor \"value\" is given for client certificate chain");
+ }
+ }
+ if (config->Credentials->PrivateKey) {
+ if (config->Credentials->PrivateKey->FileName) {
+ sslContext->AddPrivateKeyFromFile(*config->Credentials->PrivateKey->FileName);
+ } else if (config->Credentials->PrivateKey->Value) {
+ sslContext->AddPrivateKey(*config->Credentials->PrivateKey->Value);
+ } else {
+ THROW_ERROR_EXCEPTION("Neither \"file_name\" nor \"value\" is given for client private key");
+ }
+ }
+ } else {
+ sslContext->UseBuiltinOpenSslX509Store();
+ }
+
+ auto tlsDialer = sslContext->CreateDialer(
+ New<TDialerConfig>(),
+ poller,
+ HttpLogger);
+
+ auto httpClient = NHttp::CreateClient(
+ config,
+ tlsDialer,
+ poller->GetInvoker());
+
+ return New<TClient>(std::move(httpClient));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/client.h b/yt/yt/core/https/client.h
new file mode 100644
index 0000000000..2061974f00
--- /dev/null
+++ b/yt/yt/core/https/client.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/http/public.h>
+
+namespace NYT::NHttps {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NHttp::IClientPtr CreateClient(
+ const TClientConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/config.cpp b/yt/yt/core/https/config.cpp
new file mode 100644
index 0000000000..49c59d5258
--- /dev/null
+++ b/yt/yt/core/https/config.cpp
@@ -0,0 +1,42 @@
+#include "config.h"
+
+namespace NYT::NHttps {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerCredentialsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("private_key", &TThis::PrivateKey)
+ .Optional();
+ registrar.Parameter("cert_chain", &TThis::CertChain)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("credentials", &TThis::Credentials);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TClientCredentialsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("private_key", &TThis::PrivateKey)
+ .Optional();
+ registrar.Parameter("cert_chain", &TThis::CertChain)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TClientConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("credentials", &TThis::Credentials)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/config.h b/yt/yt/core/https/config.h
new file mode 100644
index 0000000000..fe4decc26e
--- /dev/null
+++ b/yt/yt/core/https/config.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/http/config.h>
+
+#include <yt/yt/core/crypto/config.h>
+
+namespace NYT::NHttps {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerCredentialsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NCrypto::TPemBlobConfigPtr PrivateKey;
+ NCrypto::TPemBlobConfigPtr CertChain;
+
+ REGISTER_YSON_STRUCT(TServerCredentialsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerCredentialsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerConfig
+ : public NHttp::TServerConfig
+{
+public:
+ TServerCredentialsConfigPtr Credentials;
+
+ REGISTER_YSON_STRUCT(TServerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientCredentialsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NCrypto::TPemBlobConfigPtr PrivateKey;
+ NCrypto::TPemBlobConfigPtr CertChain;
+
+ REGISTER_YSON_STRUCT(TClientCredentialsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientCredentialsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientConfig
+ : public NHttp::TClientConfig
+{
+public:
+ // If missing then builtin certificate store is used.
+ TClientCredentialsConfigPtr Credentials;
+
+ REGISTER_YSON_STRUCT(TClientConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/public.h b/yt/yt/core/https/public.h
new file mode 100644
index 0000000000..effb3a114e
--- /dev/null
+++ b/yt/yt/core/https/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+namespace NYT::NHttps {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TServerCredentialsConfig)
+DECLARE_REFCOUNTED_CLASS(TClientCredentialsConfig)
+DECLARE_REFCOUNTED_CLASS(TServerConfig)
+DECLARE_REFCOUNTED_CLASS(TClientConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/server.cpp b/yt/yt/core/https/server.cpp
new file mode 100644
index 0000000000..e53f132c04
--- /dev/null
+++ b/yt/yt/core/https/server.cpp
@@ -0,0 +1,105 @@
+#include "server.h"
+#include "config.h"
+
+#include <yt/yt/core/http/server.h>
+
+#include <yt/yt/core/crypto/tls.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+
+namespace NYT::NHttps {
+
+using namespace NNet;
+using namespace NHttp;
+using namespace NCrypto;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServer
+ : public IServer
+{
+public:
+ explicit TServer(IServerPtr underlying)
+ : Underlying_(std::move(underlying))
+ { }
+
+ void AddHandler(
+ const TString& pattern,
+ const IHttpHandlerPtr& handler) override
+ {
+ Underlying_->AddHandler(pattern, handler);
+ }
+
+ const TNetworkAddress& GetAddress() const override
+ {
+ return Underlying_->GetAddress();
+ }
+
+ //! Starts the server.
+ void Start() override
+ {
+ Underlying_->Start();
+ }
+
+ //! Stops the server.
+ void Stop() override
+ {
+ Underlying_->Stop();
+ }
+
+ void SetPathMatcher(const IRequestPathMatcherPtr& matcher) override
+ {
+ Underlying_->SetPathMatcher(matcher);
+ }
+
+ IRequestPathMatcherPtr GetPathMatcher() override
+ {
+ return Underlying_->GetPathMatcher();
+ }
+
+private:
+ const IServerPtr Underlying_;
+};
+
+IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const IPollerPtr& poller,
+ const IPollerPtr& acceptor)
+{
+ auto sslContext = New<TSslContext>();
+ if (config->Credentials->CertChain->FileName) {
+ sslContext->AddCertificateChainFromFile(*config->Credentials->CertChain->FileName);
+ } else if (config->Credentials->CertChain->Value) {
+ sslContext->AddCertificateChain(*config->Credentials->CertChain->Value);
+ } else {
+ YT_ABORT();
+ }
+ if (config->Credentials->PrivateKey->FileName) {
+ sslContext->AddPrivateKeyFromFile(*config->Credentials->PrivateKey->FileName);
+ } else if (config->Credentials->PrivateKey->Value) {
+ sslContext->AddPrivateKey(*config->Credentials->PrivateKey->Value);
+ } else {
+ YT_ABORT();
+ }
+
+ auto address = TNetworkAddress::CreateIPv6Any(config->Port);
+ auto tlsListener = sslContext->CreateListener(address, poller, acceptor);
+
+ auto configCopy = CloneYsonStruct(config);
+ configCopy->IsHttps = true;
+ auto httpServer = NHttp::CreateServer(configCopy, tlsListener, poller, acceptor);
+
+ return New<TServer>(std::move(httpServer));
+}
+
+IServerPtr CreateServer(const TServerConfigPtr& config, const IPollerPtr& poller)
+{
+ return CreateServer(config, poller, poller);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/server.h b/yt/yt/core/https/server.h
new file mode 100644
index 0000000000..e07dc0f722
--- /dev/null
+++ b/yt/yt/core/https/server.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/http/public.h>
+
+namespace NYT::NHttps {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NHttp::IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller);
+
+NHttp::IServerPtr CreateServer(
+ const TServerConfigPtr& config,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHttps
diff --git a/yt/yt/core/https/ya.make b/yt/yt/core/https/ya.make
new file mode 100644
index 0000000000..7615301ece
--- /dev/null
+++ b/yt/yt/core/https/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ config.cpp
+ client.cpp
+ server.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/http
+ yt/yt/core/crypto
+ library/cpp/http/io
+)
+
+END()
diff --git a/yt/yt/core/json/config.cpp b/yt/yt/core/json/config.cpp
new file mode 100644
index 0000000000..bffb0e8f4e
--- /dev/null
+++ b/yt/yt/core/json/config.cpp
@@ -0,0 +1,42 @@
+#include "config.h"
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TJsonFormatConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("format", &TThis::Format)
+ .Default(EJsonFormat::Text);
+ registrar.Parameter("attributes_mode", &TThis::AttributesMode)
+ .Default(EJsonAttributesMode::OnDemand);
+ registrar.Parameter("plain", &TThis::Plain)
+ .Default(false);
+ registrar.Parameter("encode_utf8", &TThis::EncodeUtf8)
+ .Default(true);
+ registrar.Parameter("string_length_limit", &TThis::StringLengthLimit)
+ .Default();
+ registrar.Parameter("stringify", &TThis::Stringify)
+ .Default(false);
+ registrar.Parameter("annotate_with_types", &TThis::AnnotateWithTypes)
+ .Default(false);
+ registrar.Parameter("support_infinity", &TThis::SupportInfinity)
+ .Default(false);
+ registrar.Parameter("stringify_nan_and_infinity", &TThis::StringifyNanAndInfinity)
+ .Default(false);
+ registrar.Parameter("buffer_size", &TThis::BufferSize)
+ .Default(16 * 1024);
+ registrar.Parameter("skip_null_values", &TThis::SkipNullValues)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->SupportInfinity && config->StringifyNanAndInfinity) {
+ THROW_ERROR_EXCEPTION("\"support_infinity\" and \"stringify_nan_and_infinity\" "
+ "cannot be specified simultaneously");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/config.h b/yt/yt/core/json/config.h
new file mode 100644
index 0000000000..8d9a951631
--- /dev/null
+++ b/yt/yt/core/json/config.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EJsonFormat,
+ (Text)
+ (Pretty)
+);
+
+DEFINE_ENUM(EJsonAttributesMode,
+ (Always)
+ (Never)
+ (OnDemand)
+);
+
+class TJsonFormatConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ EJsonFormat Format;
+ EJsonAttributesMode AttributesMode;
+ bool Plain;
+ bool EncodeUtf8;
+ i64 MemoryLimit = 256_MB;
+
+ std::optional<int> StringLengthLimit;
+
+ bool Stringify;
+ bool AnnotateWithTypes;
+
+ bool SupportInfinity;
+ bool StringifyNanAndInfinity;
+
+ // Size of buffer used read out input stream in parser.
+ // NB: in case of parsing long string yajl holds in memory whole string prefix and copy it on every parse call.
+ // Therefore parsing long strings works faster with larger buffer.
+ int BufferSize;
+
+ //! Only works for tabular data.
+ bool SkipNullValues;
+
+ REGISTER_YSON_STRUCT(TJsonFormatConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TJsonFormatConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/helpers.cpp b/yt/yt/core/json/helpers.cpp
new file mode 100644
index 0000000000..78b0d7f7d0
--- /dev/null
+++ b/yt/yt/core/json/helpers.cpp
@@ -0,0 +1,14 @@
+#include "helpers.h"
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSpecialJsonKey(TStringBuf key)
+{
+ return key.size() > 0 && key[0] == '$';
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/helpers.h b/yt/yt/core/json/helpers.h
new file mode 100644
index 0000000000..0e7706ad47
--- /dev/null
+++ b/yt/yt/core/json/helpers.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSpecialJsonKey(TStringBuf key);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_callbacks.cpp b/yt/yt/core/json/json_callbacks.cpp
new file mode 100644
index 0000000000..1edea77312
--- /dev/null
+++ b/yt/yt/core/json/json_callbacks.cpp
@@ -0,0 +1,414 @@
+#include "json_callbacks.h"
+#include "helpers.h"
+
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NJson {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJsonCallbacksBuildingNodesImpl::TJsonCallbacksBuildingNodesImpl(
+ IYsonConsumer* consumer,
+ NYson::EYsonType ysonType,
+ const TUtf8Transcoder& utf8Transcoder,
+ i64 memoryLimit,
+ NJson::EJsonAttributesMode attributesMode)
+ : Consumer_(consumer)
+ , YsonType_(ysonType)
+ , Utf8Transcoder_(utf8Transcoder)
+ , MemoryLimit_(memoryLimit)
+ , AttributesMode_(attributesMode)
+ , TreeBuilder_(CreateBuilderFromFactory(GetEphemeralNodeFactory()))
+{
+ TreeBuilder_->BeginTree();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnStringScalar(TStringBuf value)
+{
+ AccountMemory(value.size());
+ OnItemStarted();
+ TreeBuilder_->OnStringScalar(Utf8Transcoder_.Decode(value));
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnInt64Scalar(i64 value)
+{
+ AccountMemory(sizeof(value));
+ OnItemStarted();
+ TreeBuilder_->OnInt64Scalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnUint64Scalar(ui64 value)
+{
+ AccountMemory(sizeof(value));
+ OnItemStarted();
+ TreeBuilder_->OnUint64Scalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnDoubleScalar(double value)
+{
+ AccountMemory(sizeof(value));
+ OnItemStarted();
+ TreeBuilder_->OnDoubleScalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnBooleanScalar(bool value)
+{
+ AccountMemory(sizeof(value));
+ OnItemStarted();
+ TreeBuilder_->OnBooleanScalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnEntity()
+{
+ AccountMemory(0);
+ OnItemStarted();
+ TreeBuilder_->OnEntity();
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnBeginList()
+{
+ AccountMemory(0);
+ OnItemStarted();
+ TreeBuilder_->OnBeginList();
+ Stack_.push_back(EJsonCallbacksNodeType::List);
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnEndList()
+{
+ TreeBuilder_->OnEndList();
+ Stack_.pop_back();
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnBeginMap()
+{
+ AccountMemory(0);
+ OnItemStarted();
+ TreeBuilder_->OnBeginMap();
+ Stack_.push_back(EJsonCallbacksNodeType::Map);
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnKeyedItem(TStringBuf key)
+{
+ AccountMemory(sizeof(key.size()));
+ TreeBuilder_->OnKeyedItem(Utf8Transcoder_.Decode(key));
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnEndMap()
+{
+ TreeBuilder_->OnEndMap();
+ Stack_.pop_back();
+ OnItemFinished();
+}
+
+void TJsonCallbacksBuildingNodesImpl::AccountMemory(i64 memory)
+{
+ memory += sizeof(NYTree::INodePtr);
+ if (ConsumedMemory_ + memory > MemoryLimit_) {
+ THROW_ERROR_EXCEPTION(
+ "Memory limit exceeded while parsing JSON: allocated %v, limit %v",
+ ConsumedMemory_ + memory,
+ MemoryLimit_);
+ }
+ ConsumedMemory_ += memory;
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnItemStarted()
+{
+ if (!Stack_.empty() && Stack_.back() == EJsonCallbacksNodeType::List)
+ {
+ TreeBuilder_->OnListItem();
+ }
+}
+
+void TJsonCallbacksBuildingNodesImpl::OnItemFinished()
+{
+ if (Stack_.empty()) {
+ if (YsonType_ == EYsonType::ListFragment) {
+ Consumer_->OnListItem();
+ }
+ ConsumeNode(TreeBuilder_->EndTree());
+ TreeBuilder_->BeginTree();
+ ConsumedMemory_ = 0;
+ }
+}
+
+void TJsonCallbacksBuildingNodesImpl::ConsumeNode(INodePtr node)
+{
+ switch (node->GetType()) {
+ case ENodeType::Int64:
+ Consumer_->OnInt64Scalar(node->AsInt64()->GetValue());
+ break;
+ case ENodeType::Uint64:
+ Consumer_->OnUint64Scalar(node->AsUint64()->GetValue());
+ break;
+ case ENodeType::Double:
+ Consumer_->OnDoubleScalar(node->AsDouble()->GetValue());
+ break;
+ case ENodeType::Boolean:
+ Consumer_->OnBooleanScalar(node->AsBoolean()->GetValue());
+ break;
+ case ENodeType::Entity:
+ Consumer_->OnEntity();
+ break;
+ case ENodeType::String:
+ Consumer_->OnStringScalar(node->AsString()->GetValue());
+ break;
+ case ENodeType::Map:
+ ConsumeNode(node->AsMap());
+ break;
+ case ENodeType::List:
+ ConsumeNode(node->AsList());
+ break;
+ default:
+ YT_ABORT();
+ break;
+ };
+}
+
+void TJsonCallbacksBuildingNodesImpl::ConsumeMapFragment(IMapNodePtr map)
+{
+ for (const auto& [key, value] : map->GetChildren()) {
+ auto adjustedKey = TStringBuf(key);
+ if (AttributesMode_ != EJsonAttributesMode::Never && IsSpecialJsonKey(adjustedKey)) {
+ if (adjustedKey.size() < 2 || key[1] != '$') {
+ THROW_ERROR_EXCEPTION(
+ "Key \"%v\" starts with single \"$\"; use \"$%v\" "
+ "to encode this key in JSON format",
+ adjustedKey,
+ adjustedKey);
+ }
+ adjustedKey = adjustedKey.substr(1);
+ }
+ Consumer_->OnKeyedItem(adjustedKey);
+ ConsumeNode(value);
+ }
+}
+
+void TJsonCallbacksBuildingNodesImpl::ConsumeNode(IMapNodePtr map)
+{
+ auto node = map->FindChild("$value");
+ if (node) {
+ auto attributes = map->FindChild("$attributes");
+ if (attributes) {
+ if (attributes->GetType() != ENodeType::Map) {
+ THROW_ERROR_EXCEPTION("Value of \"$attributes\" must be a map");
+ }
+ Consumer_->OnBeginAttributes();
+ ConsumeMapFragment(attributes->AsMap());
+ Consumer_->OnEndAttributes();
+ }
+
+ auto type = map->FindChild("$type");
+
+ if (type) {
+ if (type->GetType() != ENodeType::String) {
+ THROW_ERROR_EXCEPTION("Value of \"$type\" must be a string");
+ }
+ auto typeString = type->AsString()->GetValue();
+ ENodeType expectedType;
+ if (typeString == "string") {
+ expectedType = ENodeType::String;
+ } else if (typeString == "int64") {
+ expectedType = ENodeType::Int64;
+ } else if (typeString == "uint64") {
+ expectedType = ENodeType::Uint64;
+ } else if (typeString == "double") {
+ expectedType = ENodeType::Double;
+ } else if (typeString == "boolean") {
+ expectedType = ENodeType::Boolean;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected \"$type\" value %Qv", typeString);
+ }
+
+ if (node->GetType() == expectedType) {
+ ConsumeNode(node);
+ } else if (node->GetType() == ENodeType::String) {
+ auto nodeAsString = node->AsString()->GetValue();
+ switch (expectedType) {
+ case ENodeType::Int64:
+ Consumer_->OnInt64Scalar(FromString<i64>(nodeAsString));
+ break;
+ case ENodeType::Uint64:
+ Consumer_->OnUint64Scalar(FromString<ui64>(nodeAsString));
+ break;
+ case ENodeType::Double:
+ Consumer_->OnDoubleScalar(FromString<double>(nodeAsString));
+ break;
+ case ENodeType::Boolean: {
+ if (nodeAsString == "true") {
+ Consumer_->OnBooleanScalar(true);
+ } else if (nodeAsString == "false") {
+ Consumer_->OnBooleanScalar(false);
+ } else {
+ THROW_ERROR_EXCEPTION("Invalid boolean string %Qv", nodeAsString);
+ }
+ break;
+ }
+ default:
+ YT_ABORT();
+ break;
+ }
+ } else if (node->GetType() == ENodeType::Int64) {
+ auto nodeAsInt = node->AsInt64()->GetValue();
+ switch (expectedType) {
+ case ENodeType::Int64:
+ Consumer_->OnInt64Scalar(nodeAsInt);
+ break;
+ case ENodeType::Uint64:
+ Consumer_->OnUint64Scalar(nodeAsInt);
+ break;
+ case ENodeType::Double:
+ Consumer_->OnDoubleScalar(nodeAsInt);
+ break;
+ case ENodeType::Boolean:
+ case ENodeType::String:
+ THROW_ERROR_EXCEPTION("Type mismatch in JSON")
+ << TErrorAttribute("expected_type", expectedType)
+ << TErrorAttribute("actual_type", node->GetType());
+ break;
+ default:
+ YT_ABORT();
+ break;
+ }
+ } else {
+ THROW_ERROR_EXCEPTION("Type mismatch in JSON")
+ << TErrorAttribute("expected_type", expectedType)
+ << TErrorAttribute("actual_type", node->GetType());
+ }
+ } else {
+ ConsumeNode(node);
+ }
+ } else {
+ if (map->FindChild("$attributes")) {
+ THROW_ERROR_EXCEPTION("Found key \"$attributes\" without key \"$value\"");
+ }
+ Consumer_->OnBeginMap();
+ ConsumeMapFragment(map);
+ Consumer_->OnEndMap();
+ }
+}
+
+void TJsonCallbacksBuildingNodesImpl::ConsumeNode(IListNodePtr list)
+{
+ Consumer_->OnBeginList();
+ for (int i = 0; i < list->GetChildCount(); ++i) {
+ Consumer_->OnListItem();
+ ConsumeNode(list->GetChildOrThrow(i));
+ }
+ Consumer_->OnEndList();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJsonCallbacksForwardingImpl::TJsonCallbacksForwardingImpl(
+ IYsonConsumer* consumer,
+ NYson::EYsonType ysonType,
+ const TUtf8Transcoder& utf8Transcoder)
+ : Consumer_(consumer)
+ , YsonType_(ysonType)
+ , Utf8Transcoder_(utf8Transcoder)
+{ }
+
+void TJsonCallbacksForwardingImpl::OnStringScalar(TStringBuf value)
+{
+ OnItemStarted();
+ Consumer_->OnStringScalar(Utf8Transcoder_.Decode(value));
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnInt64Scalar(i64 value)
+{
+ OnItemStarted();
+ Consumer_->OnInt64Scalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnUint64Scalar(ui64 value)
+{
+ OnItemStarted();
+ Consumer_->OnUint64Scalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnDoubleScalar(double value)
+{
+ OnItemStarted();
+ Consumer_->OnDoubleScalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnBooleanScalar(bool value)
+{
+ OnItemStarted();
+ Consumer_->OnBooleanScalar(value);
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnEntity()
+{
+ OnItemStarted();
+ Consumer_->OnEntity();
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnBeginList()
+{
+ OnItemStarted();
+ Stack_.push_back(EJsonCallbacksNodeType::List);
+ Consumer_->OnBeginList();
+}
+
+void TJsonCallbacksForwardingImpl::OnEndList()
+{
+ Consumer_->OnEndList();
+ Stack_.pop_back();
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnBeginMap()
+{
+ OnItemStarted();
+ Stack_.push_back(EJsonCallbacksNodeType::Map);
+ Consumer_->OnBeginMap();
+}
+
+void TJsonCallbacksForwardingImpl::OnKeyedItem(TStringBuf key)
+{
+ Consumer_->OnKeyedItem(Utf8Transcoder_.Decode(key));
+}
+
+void TJsonCallbacksForwardingImpl::OnEndMap()
+{
+ Consumer_->OnEndMap();
+ Stack_.pop_back();
+ OnItemFinished();
+}
+
+void TJsonCallbacksForwardingImpl::OnItemStarted()
+{
+ if ((Stack_.empty() && YsonType_ == EYsonType::ListFragment) || (!Stack_.empty() && Stack_.back() == EJsonCallbacksNodeType::List))
+ {
+ Consumer_->OnListItem();
+ }
+}
+
+void TJsonCallbacksForwardingImpl::OnItemFinished()
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_callbacks.h b/yt/yt/core/json/json_callbacks.h
new file mode 100644
index 0000000000..ffae11185c
--- /dev/null
+++ b/yt/yt/core/json/json_callbacks.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/public.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+
+#include <yt/yt/core/json/config.h>
+#include <yt/yt/core/misc/utf8_decoder.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <queue>
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EJsonCallbacksNodeType,
+ (List)
+ (Map)
+);
+
+class TJsonCallbacks
+{
+public:
+ virtual void OnStringScalar(TStringBuf value) = 0;
+ virtual void OnInt64Scalar(i64 value) = 0;
+ virtual void OnUint64Scalar(ui64 value) = 0;
+ virtual void OnDoubleScalar(double value) = 0;
+ virtual void OnBooleanScalar(bool value) = 0;
+ virtual void OnEntity() = 0;
+ virtual void OnBeginList() = 0;
+ virtual void OnEndList() = 0;
+ virtual void OnBeginMap() = 0;
+ virtual void OnKeyedItem(TStringBuf key) = 0;
+ virtual void OnEndMap() = 0;
+
+ virtual ~TJsonCallbacks()
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJsonCallbacksBuildingNodesImpl
+ : public TJsonCallbacks
+{
+public:
+ TJsonCallbacksBuildingNodesImpl(
+ NYson::IYsonConsumer* consumer,
+ NYson::EYsonType ysonType,
+ const TUtf8Transcoder& utf8Transcoder,
+ i64 memoryLimit,
+ NJson::EJsonAttributesMode attributesMode);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+private:
+ // Memory accounted approximately
+ void AccountMemory(i64 memory);
+ void OnItemStarted();
+ void OnItemFinished();
+
+ void ConsumeNode(NYTree::INodePtr node);
+ void ConsumeNode(NYTree::IMapNodePtr map);
+ void ConsumeNode(NYTree::IListNodePtr list);
+ void ConsumeMapFragment(NYTree::IMapNodePtr map);
+
+ NYson::IYsonConsumer* Consumer_;
+ NYson::EYsonType YsonType_;
+ TUtf8Transcoder Utf8Transcoder_;
+ i64 ConsumedMemory_ = 0;
+ const i64 MemoryLimit_;
+ const NJson::EJsonAttributesMode AttributesMode_;
+
+ TCompactVector<EJsonCallbacksNodeType, 4> Stack_;
+
+ const std::unique_ptr<NYTree::ITreeBuilder> TreeBuilder_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJsonCallbacksForwardingImpl
+ : public TJsonCallbacks
+{
+public:
+ TJsonCallbacksForwardingImpl(
+ NYson::IYsonConsumer* consumer,
+ NYson::EYsonType ysonType,
+ const TUtf8Transcoder& utf8Transcoder);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+private:
+ void OnItemStarted();
+ void OnItemFinished();
+
+ NYson::IYsonConsumer* Consumer_;
+ NYson::EYsonType YsonType_;
+ TUtf8Transcoder Utf8Transcoder_;
+
+ TCompactVector<EJsonCallbacksNodeType, 4> Stack_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_parser.cpp b/yt/yt/core/json/json_parser.cpp
new file mode 100644
index 0000000000..55aedb9917
--- /dev/null
+++ b/yt/yt/core/json/json_parser.cpp
@@ -0,0 +1,242 @@
+#include "json_parser.h"
+#include "json_callbacks.h"
+
+#include <yt/yt/core/misc/utf8_decoder.h>
+#include <yt/yt/core/misc/error.h>
+
+#include <array>
+
+#include <contrib/libs/yajl/api/yajl_parse.h>
+
+namespace NYT::NJson {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+static int OnNull(void* ctx)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnEntity();
+ return 1;
+}
+
+static int OnBoolean(void *ctx, int boolean)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnBooleanScalar(boolean);
+ return 1;
+}
+
+static int OnInteger(void *ctx, long long value)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnInt64Scalar(value);
+ return 1;
+}
+
+static int OnUnsignedInteger(void *ctx, unsigned long long value)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnUint64Scalar(value);
+ return 1;
+}
+
+static int OnDouble(void *ctx, double value)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnDoubleScalar(value);
+ return 1;
+}
+
+static int OnString(void *ctx, const unsigned char *val, size_t len)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnStringScalar(TStringBuf((const char *)val, len));
+ return 1;
+}
+
+static int OnStartMap(void *ctx)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnBeginMap();
+ return 1;
+}
+
+static int OnMapKey(void *ctx, const unsigned char *val, size_t len)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnKeyedItem(TStringBuf((const char *)val, len));
+ return 1;
+}
+
+static int OnEndMap(void *ctx)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnEndMap();
+ return 1;
+}
+
+static int OnStartArray(void *ctx)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnBeginList();
+ return 1;
+}
+
+static int OnEndArray(void *ctx)
+{
+ static_cast<TJsonCallbacks*>(ctx)->OnEndList();
+ return 1;
+}
+
+static yajl_callbacks YajlCallbacks = {
+ OnNull,
+ OnBoolean,
+ OnInteger,
+ OnUnsignedInteger,
+ OnDouble,
+ nullptr,
+ OnString,
+ OnStartMap,
+ OnMapKey,
+ OnEndMap,
+ OnStartArray,
+ OnEndArray
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TJsonParserBufferTag
+{ };
+
+class TJsonParser::TImpl
+{
+public:
+ TImpl(IYsonConsumer* consumer, TJsonFormatConfigPtr config, EYsonType type)
+ : Consumer_(consumer)
+ , Config_(config ? config : New<TJsonFormatConfig>())
+ , Type_(type)
+ , YajlHandle_(nullptr, yajl_free)
+ {
+ YT_VERIFY(Type_ != EYsonType::MapFragment);
+
+ if (Config_->Format == EJsonFormat::Pretty && Type_ == EYsonType::ListFragment) {
+ THROW_ERROR_EXCEPTION("Pretty JSON format is not supported for list fragments");
+ }
+
+ if (Config_->Plain) {
+ Callbacks_ = std::make_unique<TJsonCallbacksForwardingImpl>(
+ Consumer_,
+ Type_,
+ TUtf8Transcoder(Config_->EncodeUtf8));
+ } else {
+ Callbacks_ = std::make_unique<TJsonCallbacksBuildingNodesImpl>(
+ Consumer_,
+ Type_,
+ TUtf8Transcoder(Config_->EncodeUtf8),
+ Config_->MemoryLimit,
+ Config_->AttributesMode);
+ }
+ YajlHandle_.reset(yajl_alloc(&YajlCallbacks, nullptr, Callbacks_.get()));
+
+ if (Type_ == EYsonType::ListFragment) {
+ yajl_config(YajlHandle_.get(), yajl_allow_multiple_values, 1);
+ // To allow empty list fragment
+ yajl_config(YajlHandle_.get(), yajl_allow_partial_values, 1);
+ }
+ yajl_set_memory_limit(YajlHandle_.get(), Config_->MemoryLimit);
+
+ Buffer_ = TSharedMutableRef::Allocate<TJsonParserBufferTag>(Config_->BufferSize, {.InitializeStorage = false});
+ }
+
+ void Read(TStringBuf data)
+ {
+ if (yajl_parse(
+ YajlHandle_.get(),
+ reinterpret_cast<const unsigned char*>(data.data()),
+ data.size()) == yajl_status_error)
+ {
+ OnError(data.data(), data.size());
+ }
+ }
+
+ void Finish()
+ {
+ if (yajl_complete_parse(YajlHandle_.get()) == yajl_status_error) {
+ OnError(nullptr, 0);
+ }
+ }
+
+ void Parse(IInputStream* input)
+ {
+ while (true) {
+ auto readLength = input->Read(Buffer_.Begin(), Config_->BufferSize);
+ if (readLength == 0) {
+ break;
+ }
+ Read(TStringBuf(Buffer_.begin(), readLength));
+ }
+ Finish();
+ }
+
+private:
+ IYsonConsumer* const Consumer_;
+ const TJsonFormatConfigPtr Config_;
+ const EYsonType Type_;
+
+ std::unique_ptr<TJsonCallbacks> Callbacks_;
+
+ TSharedMutableRef Buffer_;
+
+ std::unique_ptr<yajl_handle_t, decltype(&yajl_free)> YajlHandle_;
+
+ void OnError(const char* data, int len)
+ {
+ unsigned char* errorMessage = yajl_get_error(
+ YajlHandle_.get(),
+ 1,
+ reinterpret_cast<const unsigned char*>(data),
+ len);
+ auto error = TError("Error parsing JSON") << TError((char*) errorMessage);
+ yajl_free_error(YajlHandle_.get(), errorMessage);
+ THROW_ERROR_EXCEPTION(error);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJsonParser::TJsonParser(
+ IYsonConsumer* consumer,
+ TJsonFormatConfigPtr config,
+ EYsonType type)
+ : Impl_(new TImpl(consumer, config, type))
+{ }
+
+TJsonParser::~TJsonParser() = default;
+
+void TJsonParser::Read(TStringBuf data)
+{
+ Impl_->Read(data);
+}
+
+void TJsonParser::Finish()
+{
+ Impl_->Finish();
+}
+
+void TJsonParser::Parse(IInputStream* input)
+{
+ Impl_->Parse(input);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseJson(
+ IInputStream* input,
+ IYsonConsumer* consumer,
+ TJsonFormatConfigPtr config,
+ EYsonType type)
+{
+ TJsonParser jsonParser(consumer, config, type);
+ jsonParser.Parse(input);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_parser.h b/yt/yt/core/json/json_parser.h
new file mode 100644
index 0000000000..4ca598f5c5
--- /dev/null
+++ b/yt/yt/core/json/json_parser.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NJson {
+
+// See json_writer.h for details on how YSON is mapped to JSON.
+// This implementation of TJsonParser is DOM-based (and is thus suboptimal).
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJsonParser
+{
+public:
+ explicit TJsonParser(
+ NYson::IYsonConsumer* consumer,
+ TJsonFormatConfigPtr config = nullptr,
+ NYson::EYsonType type = NYson::EYsonType::Node);
+ ~TJsonParser();
+
+ void Read(TStringBuf data);
+ void Finish();
+
+ void Parse(IInputStream* input);
+
+private:
+ class TImpl;
+ const std::unique_ptr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseJson(
+ IInputStream* input,
+ NYson::IYsonConsumer* consumer,
+ TJsonFormatConfigPtr config = nullptr,
+ NYson::EYsonType type = NYson::EYsonType::Node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_writer.cpp b/yt/yt/core/json/json_writer.cpp
new file mode 100644
index 0000000000..20adbb4786
--- /dev/null
+++ b/yt/yt/core/json/json_writer.cpp
@@ -0,0 +1,684 @@
+#include "json_writer.h"
+#include "config.h"
+#include "helpers.h"
+
+#include <yt/yt/core/misc/utf8_decoder.h>
+
+#include <contrib/libs/yajl/api/yajl_gen.h>
+
+#include <cmath>
+#include <iostream>
+
+namespace NYT::NJson {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TJsonWriter
+ : public IJsonWriter
+{
+public:
+ TJsonWriter(IOutputStream* output, bool isPretty);
+ virtual ~TJsonWriter() override;
+
+ void Flush() override;
+ void OnStringScalar(TStringBuf value) override;
+
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+
+ void OnBeginList() override;
+
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+
+ void OnKeyedItem(TStringBuf name) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+
+ void OnEndAttributes() override;
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+
+ void StartNextValue() override;
+
+ ui64 GetWrittenByteCount() const override;
+
+private:
+ void GenerateString(TStringBuf value);
+ TStringBuf GetBuffer() const;
+
+private:
+ yajl_gen Handle;
+ IOutputStream* Output;
+ ui64 WrittenToOutputByteCount_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ENanInfinityMode,
+ (NotSupported)
+ (WriteInfinitiesUnquoted)
+ (WriteAllQuouted)
+);
+
+class TJsonConsumer
+ : public TYsonConsumerBase
+ , public IJsonConsumer
+{
+public:
+ TJsonConsumer(
+ IJsonWriter* jsonWriter,
+ EYsonType type,
+ TJsonFormatConfigPtr config);
+
+ TJsonConsumer(
+ std::unique_ptr<IJsonWriter> jsonWriter,
+ EYsonType type,
+ TJsonFormatConfigPtr config);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+
+ void OnEntity() override;
+
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+ void SetAnnotateWithTypesParameter(bool value) override;
+
+ void OnStringScalarWeightLimited(TStringBuf value, std::optional<i64> weightLimit) override;
+ void OnNodeWeightLimited(TStringBuf yson, std::optional<i64> weightLimit) override;
+
+ void Flush() override;
+
+private:
+ void WriteStringScalar(TStringBuf value);
+ void WriteStringScalarWithAttributes(TStringBuf value, TStringBuf type, bool incomplete);
+
+ void EnterNode();
+ void LeaveNode();
+ bool IsWriteAllowed();
+
+private:
+ IJsonWriter* const JsonWriter;
+ std::unique_ptr<IJsonWriter> JsonWriterHolder_;
+
+ const EYsonType Type;
+ const TJsonFormatConfigPtr Config;
+ ENanInfinityMode NanInfinityMode_;
+
+ TUtf8Transcoder Utf8Transcoder;
+
+ std::vector<bool> HasUnfoldedStructureStack;
+ int InAttributesBalance = 0;
+ bool HasAttributes = false;
+ int Depth = 0;
+ bool CheckLimit = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void CheckYajlCode(int yajlCode)
+{
+ if (yajlCode == yajl_gen_status_ok) {
+ return;
+ }
+
+ TString errorMessage;
+ switch (yajlCode)
+ {
+ case yajl_gen_keys_must_be_strings:
+ errorMessage = "JSON key must be a string";
+ break;
+ case yajl_max_depth_exceeded:
+ errorMessage = Format("JSON maximal depth exceeded %v", YAJL_MAX_DEPTH);
+ break;
+ case yajl_gen_in_error_state:
+ errorMessage = "JSON: a generator function (yajl_gen_XXX) was called while in an error state";
+ break;
+ case yajl_gen_generation_complete:
+ errorMessage = "Attempt to alter already completed JSON document";
+ break;
+ case yajl_gen_invalid_number:
+ errorMessage = "Invalid floating point value in JSON";
+ break;
+ case yajl_gen_invalid_string:
+ errorMessage = "Invalid UTF-8 string in JSON";
+ break;
+ default:
+ errorMessage = Format("Yajl writer failed with code %v", yajlCode);
+ }
+ THROW_ERROR_EXCEPTION(errorMessage);
+}
+
+TJsonWriter::TJsonWriter(IOutputStream* output, bool isPretty)
+ : Output(output)
+{
+ Handle = yajl_gen_alloc(nullptr);
+ yajl_gen_config(Handle, yajl_gen_beautify, isPretty ? 1 : 0);
+ yajl_gen_config(Handle, yajl_gen_skip_final_newline, 0);
+
+ yajl_gen_config(Handle, yajl_gen_support_infinity, 1);
+
+ yajl_gen_config(Handle, yajl_gen_disable_yandex_double_format, 1);
+ yajl_gen_config(Handle, yajl_gen_validate_utf8, 1);
+}
+
+TJsonWriter::~TJsonWriter()
+{
+ yajl_gen_free(Handle);
+}
+
+void TJsonWriter::GenerateString(TStringBuf value)
+{
+ CheckYajlCode(yajl_gen_string(Handle, (const unsigned char*) value.data(), value.size()));
+}
+
+TStringBuf TJsonWriter::GetBuffer() const
+{
+ size_t len = 0;
+ const unsigned char* buf = nullptr;
+ CheckYajlCode(yajl_gen_get_buf(Handle, &buf, &len));
+ return TStringBuf(static_cast<const char*>(static_cast<const void*>(buf)), len);
+}
+
+void TJsonWriter::Flush()
+{
+ auto buf = GetBuffer();
+ Output->Write(buf);
+ WrittenToOutputByteCount_ += buf.Size();
+ yajl_gen_clear(Handle);
+}
+
+void TJsonWriter::StartNextValue()
+{
+ Flush();
+ yajl_gen_reset(Handle, nullptr);
+ Output->Write('\n');
+}
+
+void TJsonWriter::OnBeginMap()
+{
+ CheckYajlCode(yajl_gen_map_open(Handle));
+}
+
+void TJsonWriter::OnKeyedItem(TStringBuf name)
+{
+ GenerateString(name);
+}
+
+void TJsonWriter::OnEndMap()
+{
+ CheckYajlCode(yajl_gen_map_close(Handle));
+}
+
+void TJsonWriter::OnBeginList()
+{
+ CheckYajlCode(yajl_gen_array_open(Handle));
+}
+
+void TJsonWriter::OnListItem()
+{ }
+
+void TJsonWriter::OnEndList()
+{
+ CheckYajlCode(yajl_gen_array_close(Handle));
+}
+
+void TJsonWriter::OnStringScalar(TStringBuf value)
+{
+ GenerateString(value);
+}
+
+void TJsonWriter::OnEntity()
+{
+ CheckYajlCode(yajl_gen_null(Handle));
+}
+
+void TJsonWriter::OnDoubleScalar(double value)
+{
+ CheckYajlCode(yajl_gen_double(Handle, value));
+}
+
+void TJsonWriter::OnInt64Scalar(i64 value)
+{
+ CheckYajlCode(yajl_gen_integer(Handle, value));
+}
+
+void TJsonWriter::OnUint64Scalar(ui64 value)
+{
+ CheckYajlCode(yajl_gen_uinteger(Handle, value));
+}
+
+void TJsonWriter::OnBooleanScalar(bool value)
+{
+ CheckYajlCode(yajl_gen_bool(Handle, value ? 1 : 0));
+}
+
+void TJsonWriter::OnBeginAttributes()
+{
+ THROW_ERROR_EXCEPTION("TJsonWriter does not support attributes");
+}
+
+void TJsonWriter::OnEndAttributes()
+{
+ THROW_ERROR_EXCEPTION("TJsonWriter does not support attributes");
+}
+
+void TJsonWriter::OnRaw(TStringBuf /*yson*/, NYT::NYson::EYsonType /*type*/)
+{
+ THROW_ERROR_EXCEPTION("TJsonWriter does not support OnRaw()");
+}
+
+ui64 TJsonWriter::GetWrittenByteCount() const
+{
+ return GetBuffer().Size() + WrittenToOutputByteCount_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TJsonConsumer::TJsonConsumer(
+ IJsonWriter* jsonWriter,
+ EYsonType type,
+ TJsonFormatConfigPtr config)
+ : JsonWriter(jsonWriter)
+ , Type(type)
+ , Config(std::move(config))
+ , Utf8Transcoder(Config->EncodeUtf8)
+{
+ if (Type == EYsonType::MapFragment) {
+ THROW_ERROR_EXCEPTION("Map fragments are not supported by JSON");
+ }
+
+ NanInfinityMode_ = ENanInfinityMode::NotSupported;
+ if (Config->SupportInfinity) {
+ NanInfinityMode_ = ENanInfinityMode::WriteInfinitiesUnquoted;
+ } else if (Config->StringifyNanAndInfinity) {
+ NanInfinityMode_ = ENanInfinityMode::WriteAllQuouted;
+ }
+}
+
+TJsonConsumer::TJsonConsumer(
+ std::unique_ptr<IJsonWriter> jsonWriter,
+ EYsonType type,
+ TJsonFormatConfigPtr config)
+ : TJsonConsumer(jsonWriter.get(), type, std::move(config))
+{
+ JsonWriterHolder_ = std::move(jsonWriter);
+}
+
+void TJsonConsumer::EnterNode()
+{
+ if (Config->AttributesMode == EJsonAttributesMode::Never) {
+ HasAttributes = false;
+ } else if (Config->AttributesMode == EJsonAttributesMode::OnDemand) {
+ // Do nothing
+ } else if (Config->AttributesMode == EJsonAttributesMode::Always) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ JsonWriter->OnKeyedItem(TStringBuf("$attributes"));
+ JsonWriter->OnBeginMap();
+ JsonWriter->OnEndMap();
+ HasAttributes = true;
+ }
+ }
+ HasUnfoldedStructureStack.push_back(HasAttributes);
+
+ if (HasAttributes) {
+ JsonWriter->OnKeyedItem(TStringBuf("$value"));
+ HasAttributes = false;
+ }
+
+ Depth += 1;
+}
+
+void TJsonConsumer::LeaveNode()
+{
+ YT_VERIFY(!HasUnfoldedStructureStack.empty());
+ if (HasUnfoldedStructureStack.back()) {
+ // Close map of the {$attributes, $value}
+ JsonWriter->OnEndMap();
+ }
+ HasUnfoldedStructureStack.pop_back();
+
+ Depth -= 1;
+
+ if (Depth == 0 && Type == EYsonType::ListFragment && InAttributesBalance == 0) {
+ JsonWriter->StartNextValue();
+ }
+}
+
+bool TJsonConsumer::IsWriteAllowed()
+{
+ if (Config->AttributesMode == EJsonAttributesMode::Never) {
+ return InAttributesBalance == 0;
+ }
+ return true;
+}
+
+void TJsonConsumer::OnStringScalar(TStringBuf value)
+{
+ TStringBuf writeValue = value;
+ bool incomplete = false;
+ if (Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (CheckLimit && Config->StringLengthLimit && std::ssize(value) > *Config->StringLengthLimit) {
+ writeValue = value.substr(0, *Config->StringLengthLimit);
+ incomplete = true;
+ }
+ }
+
+ WriteStringScalarWithAttributes(writeValue, TStringBuf("string"), incomplete);
+}
+
+void TJsonConsumer::OnInt64Scalar(i64 value)
+{
+ if (IsWriteAllowed()) {
+ if (Config->AnnotateWithTypes && Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+ JsonWriter->OnKeyedItem(TStringBuf("$type"));
+ JsonWriter->OnStringScalar(TStringBuf("int64"));
+ }
+ EnterNode();
+ if (Config->Stringify) {
+ WriteStringScalar(::ToString(value));
+ } else {
+ JsonWriter->OnInt64Scalar(value);
+ }
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnUint64Scalar(ui64 value)
+{
+ if (IsWriteAllowed()) {
+ if (Config->AnnotateWithTypes && Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+ JsonWriter->OnKeyedItem(TStringBuf("$type"));
+ JsonWriter->OnStringScalar(TStringBuf("uint64"));
+ }
+ EnterNode();
+ if (Config->Stringify) {
+ WriteStringScalar(::ToString(value));
+ } else {
+ JsonWriter->OnUint64Scalar(value);
+ }
+ LeaveNode();
+
+ }
+}
+
+void TJsonConsumer::OnDoubleScalar(double value)
+{
+ if (IsWriteAllowed()) {
+ if (Config->AnnotateWithTypes && Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+ JsonWriter->OnKeyedItem(TStringBuf("$type"));
+ JsonWriter->OnStringScalar(TStringBuf("double"));
+ }
+ EnterNode();
+ if (Config->Stringify) {
+ char buf[256];
+ auto str = TStringBuf(buf, FloatToString(value, buf, sizeof(buf)));
+ WriteStringScalar(str);
+ } else {
+ switch (NanInfinityMode_) {
+ case ENanInfinityMode::WriteAllQuouted:
+ if (std::isnan(value)) {
+ JsonWriter->OnStringScalar(TStringBuf("nan"));
+ } else if (std::isinf(value)) {
+ if (value < 0) {
+ JsonWriter->OnStringScalar(TStringBuf("-inf"));
+ } else {
+ JsonWriter->OnStringScalar(TStringBuf("inf"));
+ }
+ } else {
+ JsonWriter->OnDoubleScalar(value);
+ }
+ break;
+ case ENanInfinityMode::WriteInfinitiesUnquoted:
+ if (std::isnan(value)) {
+ THROW_ERROR_EXCEPTION(
+ "Unexpected NaN encountered during JSON writing; "
+ "consider \"stringify_nan_and_infinity\" config option");
+ }
+ JsonWriter->OnDoubleScalar(value);
+ break;
+ case ENanInfinityMode::NotSupported:
+ if (std::isnan(value) || std::isinf(value)) {
+ THROW_ERROR_EXCEPTION(
+ "Unexpected NaN or infinity encountered during JSON writing; "
+ "consider using either \"support_infinity\" or \"stringify_nan_and_infinity\" config options");
+ }
+ JsonWriter->OnDoubleScalar(value);
+ break;
+ }
+ }
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnBooleanScalar(bool value)
+{
+ if (IsWriteAllowed()) {
+ if (Config->AnnotateWithTypes && Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+ JsonWriter->OnKeyedItem(TStringBuf("$type"));
+ JsonWriter->OnStringScalar(TStringBuf("boolean"));
+ }
+ EnterNode();
+ if (Config->Stringify) {
+ WriteStringScalar(FormatBool(value));
+ } else {
+ JsonWriter->OnBooleanScalar(value);
+ }
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnEntity()
+{
+ if (IsWriteAllowed()) {
+ EnterNode();
+ JsonWriter->OnEntity();
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnBeginList()
+{
+ if (IsWriteAllowed()) {
+ EnterNode();
+ JsonWriter->OnBeginList();
+ }
+}
+
+void TJsonConsumer::OnListItem()
+{
+ if (IsWriteAllowed()) {
+ JsonWriter->OnListItem();
+ }
+}
+
+void TJsonConsumer::OnEndList()
+{
+ if (IsWriteAllowed()) {
+ JsonWriter->OnEndList();
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnBeginMap()
+{
+ if (IsWriteAllowed()) {
+ EnterNode();
+ JsonWriter->OnBeginMap();
+ }
+}
+
+void TJsonConsumer::OnKeyedItem(TStringBuf name)
+{
+ if (IsWriteAllowed()) {
+ if (IsSpecialJsonKey(name)) {
+ JsonWriter->OnKeyedItem(Utf8Transcoder.Encode(TString("$") + name));
+ } else {
+ JsonWriter->OnKeyedItem(Utf8Transcoder.Encode(name));
+ }
+ }
+}
+
+void TJsonConsumer::OnEndMap()
+{
+ if (IsWriteAllowed()) {
+ JsonWriter->OnEndMap();
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::OnBeginAttributes()
+{
+ InAttributesBalance += 1;
+ if (Config->AttributesMode != EJsonAttributesMode::Never) {
+ JsonWriter->OnBeginMap();
+ JsonWriter->OnKeyedItem(TStringBuf("$attributes"));
+ JsonWriter->OnBeginMap();
+ }
+}
+
+void TJsonConsumer::OnEndAttributes()
+{
+ InAttributesBalance -= 1;
+ if (Config->AttributesMode != EJsonAttributesMode::Never) {
+ JsonWriter->OnEndMap();
+ HasAttributes = true;
+ }
+}
+
+void TJsonConsumer::Flush()
+{
+ JsonWriter->Flush();
+}
+
+void TJsonConsumer::WriteStringScalar(TStringBuf value)
+{
+ JsonWriter->OnStringScalar(Utf8Transcoder.Encode(value));
+}
+
+void TJsonConsumer::WriteStringScalarWithAttributes(
+ TStringBuf value,
+ TStringBuf type,
+ bool incomplete)
+{
+ if (IsWriteAllowed()) {
+ if (Config->AttributesMode != EJsonAttributesMode::Never) {
+ if (incomplete) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+
+ JsonWriter->OnKeyedItem(TStringBuf("$incomplete"));
+ JsonWriter->OnBooleanScalar(true);
+ }
+
+ if (Config->AnnotateWithTypes) {
+ if (!HasAttributes) {
+ JsonWriter->OnBeginMap();
+ HasAttributes = true;
+ }
+
+ JsonWriter->OnKeyedItem(TStringBuf("$type"));
+ JsonWriter->OnStringScalar(type);
+ }
+ }
+
+ EnterNode();
+ WriteStringScalar(value);
+ LeaveNode();
+ }
+}
+
+void TJsonConsumer::SetAnnotateWithTypesParameter(bool value)
+{
+ Config->AnnotateWithTypes = value;
+}
+
+void TJsonConsumer::OnStringScalarWeightLimited(TStringBuf value, std::optional<i64> weightLimit)
+{
+ TStringBuf writeValue = value;
+ bool incomplete = false;
+ if (CheckLimit && weightLimit && std::ssize(value) > *weightLimit) {
+ writeValue = value.substr(0, *weightLimit);
+ incomplete = true;
+ }
+
+ WriteStringScalarWithAttributes(writeValue, TStringBuf("string"), incomplete);
+}
+
+void TJsonConsumer::OnNodeWeightLimited(TStringBuf yson, std::optional<i64> weightLimit)
+{
+ if (CheckLimit && weightLimit && std::ssize(yson) > *weightLimit) {
+ WriteStringScalarWithAttributes({}, TStringBuf("any"), true);
+ return;
+ }
+
+ OnRaw(yson, EYsonType::Node);
+}
+
+std::unique_ptr<IJsonConsumer> CreateJsonConsumer(
+ IOutputStream* output,
+ EYsonType type,
+ TJsonFormatConfigPtr config)
+{
+ auto jsonWriter = CreateJsonWriter(
+ output,
+ /* pretty */ config->Format == EJsonFormat::Pretty);
+ return std::make_unique<TJsonConsumer>(std::move(jsonWriter), type, std::move(config));
+}
+
+std::unique_ptr<IJsonConsumer> CreateJsonConsumer(
+ IJsonWriter* jsonWriter,
+ EYsonType type,
+ TJsonFormatConfigPtr config)
+{
+ return std::make_unique<TJsonConsumer>(jsonWriter, type, std::move(config));
+}
+
+std::unique_ptr<IJsonWriter> CreateJsonWriter(
+ IOutputStream* output,
+ bool pretty)
+{
+ return std::make_unique<TJsonWriter>(output, pretty);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/json_writer.h b/yt/yt/core/json/json_writer.h
new file mode 100644
index 0000000000..a8482c4ebf
--- /dev/null
+++ b/yt/yt/core/json/json_writer.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include "public.h"
+#include "config.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// YSON-to-JSON Mapping Conventions.
+// * Attributes are not supported.
+// * Mapping:
+// YSON --> JSON
+// * List --> Array
+// * Map --> Object
+// * Int64 --> Number
+// * Uint64 --> Number
+// * Double (NaNs are not supported) --> Number
+// * String (MUST be UTF-8) --> String
+// * Entity --> null
+struct IJsonWriter
+ : public NYson::IFlushableYsonConsumer
+{
+ // Finish writing top-level value and start the next one from new line.
+ // No check for value completeness are guaranteed.
+ virtual void StartNextValue() = 0;
+
+ virtual ui64 GetWrittenByteCount() const = 0;
+};
+
+std::unique_ptr<IJsonWriter> CreateJsonWriter(
+ IOutputStream* output,
+ bool pretty = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// THIS INTERFACE IS YT JSON FORMAT SPECIFIC, IT SHOULD NOT BE USED ELSEWHERE.
+//
+// YSON-to-JSON Mapping Conventions
+//
+// * List fragment corresponds to new-line-delimited JSON.
+// * Map fragment (which exists in YSON) is not supported.
+// * Other types (without attributes) are mapped almost as is:
+// YSON --> JSON
+// * List --> Array
+// * Map --> Object
+// * Int64 --> Number
+// * Uint64 --> Number
+// * Double --> Number
+// * String (s) --> String (t):
+// * If s is ASCII: t := s
+// * else: t := UTF-8 string with code points corresponding to bytes in s.
+// * Entity --> null
+// * Nodes with attributes are mapped to the following JSON map:
+// {
+// '$attributes': (attributes map),
+// '$value': (value, as explained above),
+// '$type': (type name, if type annotation is enabled)
+// }
+
+//! Translates YSON events into a series of calls to underlying |IJsonWriter|
+//! thus enabling to transform YSON into JSON.
+/*!
+ * \note
+ * Entities are translated to nulls.
+ *
+ * Attributes are only supported for entities and maps.
+ * They are written as an inner "$attributes" map.
+ *
+ * Explicit #Flush calls should be made when finished writing via the adapter.
+ */
+struct IJsonConsumer
+ : public NYson::IFlushableYsonConsumer
+{
+ virtual void SetAnnotateWithTypesParameter(bool value) = 0;
+
+ virtual void OnStringScalarWeightLimited(TStringBuf value, std::optional<i64> weightLimit) = 0;
+ virtual void OnNodeWeightLimited(TStringBuf yson, std::optional<i64> weightLimit) = 0;
+};
+
+// If |type == ListFragment|, additionally call |underlying->StartNextValue| after each complete value.
+std::unique_ptr<IJsonConsumer> CreateJsonConsumer(
+ IOutputStream* output,
+ NYson::EYsonType type = NYson::EYsonType::Node,
+ TJsonFormatConfigPtr config = New<TJsonFormatConfig>());
+
+// If |type == ListFragment|, additionally call |underlying->StartNextValue| after each complete value.
+std::unique_ptr<IJsonConsumer> CreateJsonConsumer(
+ IJsonWriter* underlying,
+ NYson::EYsonType type = NYson::EYsonType::Node,
+ TJsonFormatConfigPtr config = New<TJsonFormatConfig>());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/public.h b/yt/yt/core/json/public.h
new file mode 100644
index 0000000000..860acdc88c
--- /dev/null
+++ b/yt/yt/core/json/public.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NJson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TJsonFormatConfig)
+
+struct IJsonConsumer;
+struct IJsonWriter;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/unittests/parser_ut.cpp b/yt/yt/core/json/unittests/parser_ut.cpp
new file mode 100644
index 0000000000..b9e24ec128
--- /dev/null
+++ b/yt/yt/core/json/unittests/parser_ut.cpp
@@ -0,0 +1,806 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/json/json_parser.h>
+
+namespace NYT::NJson {
+namespace {
+
+using namespace NYson;
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::NiceMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TString SurroundWithQuotes(const TString& s)
+{
+ TString quote = "\"";
+ return quote + s + quote;
+}
+
+// Basic types:
+TEST(TJsonParserTest, List)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("aaa"));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(3.5)));
+ EXPECT_CALL(Mock, OnEndList());
+
+ TString input = "[1,\"aaa\",3.5]";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Map)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"hello\":\"world\",\"foo\":\"bar\"}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Integer1)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnInt64Scalar(1ll << 62));
+
+ TString input = "4611686018427387904";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Integer2)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnInt64Scalar(std::numeric_limits<i64>::max()));
+
+ TString input = "9223372036854775807";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, UnsignedInteger)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnUint64Scalar((1ull << 63) + (1ull << 62)));
+
+ TString input = "13835058055282163712";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, UnsignedInteger2)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnUint64Scalar((1ull << 63)));
+
+ TString input = "9223372036854775808";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Infinity)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnDoubleScalar(std::numeric_limits<double>::infinity()));
+
+ TString input = "inf";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, MinusInfinity)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnDoubleScalar(-std::numeric_limits<double>::infinity()));
+
+ TString input = "-inf";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, IncorrectInfinity)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ TString input = "[0, -in, 1.0]";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, UnsignedInteger3)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ //InSequence dummy; // order in map is not specified
+
+ EXPECT_CALL(Mock, OnUint64Scalar(std::numeric_limits<ui64>::max()));
+
+ TString input = "18446744073709551615";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Entity)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnEntity());
+
+ TString input = "null";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, EmptyString)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnStringScalar(""));
+
+ TString input = SurroundWithQuotes("");
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+
+TEST(TJsonParserTest, OutOfRangeUnicodeSymbols)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ TString input = SurroundWithQuotes("\\u0100");
+ TStringInput stream(input);
+
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, EscapedUnicodeSymbols)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ TString s = TString("\x80\n\xFF", 3);
+ EXPECT_CALL(Mock, OnStringScalar(s));
+
+ TString input = SurroundWithQuotes("\\u0080\\u000A\\u00FF");
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Boolean)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ TString input = "true";
+
+ EXPECT_CALL(Mock, OnBooleanScalar(true));
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, InvalidJson)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ TString input = "{\"hello\" = \"world\"}"; // YSon style instead of json
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, Embedded)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "{"
+ "\"a\":{\"foo\":\"bar\"}"
+ ","
+ "\"b\":[1]"
+ "}";
+ TStringInput stream(input);
+
+ ParseJson(&stream, &Mock);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TJsonParserPlainTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("aaa"));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(3.5)));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnEntity());
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnEndList());
+
+ TString input = "[1,\"aaa\",3.5,{\"a\": null}]";
+
+ TStringInput stream(input);
+
+ auto config = New<TJsonFormatConfig>();
+ config->Plain = true;
+
+ ParseJson(&stream, &Mock, config);
+}
+
+TEST(TJsonParserPlainTest, Incorrect)
+{
+ InSequence dummy;
+
+ StrictMock<TMockYsonConsumer> Mock;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("$attributes"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnKeyedItem("$value"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":[1]"
+ "}";
+ TStringInput stream(input);
+
+ auto config = New<TJsonFormatConfig>();
+ config->Plain = true;
+
+ ParseJson(&stream, &Mock, config);
+}
+
+TEST(TJsonParserPlainTest, ListFragment)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"hello\":\"world\"}\n{\"foo\":\"bar\"}\n";
+
+ auto config = New<TJsonFormatConfig>();
+ config->Plain = true;
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock, config, EYsonType::ListFragment);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Values with attributes:
+TEST(TJsonParserTest, ListWithAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnEndList());
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":[1]"
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, MapWithAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("spam"));
+ EXPECT_CALL(Mock, OnStringScalar("bad"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":{\"spam\":\"bad\"}"
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, Int64WithAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":42"
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, EntityWithAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnEntity());
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":null"
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, StringWithAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnStringScalar("some_string"));
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":\"some_string\""
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, DoubleAttributes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("another_foo"));
+ EXPECT_CALL(Mock, OnStringScalar("another_bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnStringScalar("some_string"));
+
+ TString input =
+ "{"
+ "\"$attributes\":{\"foo\":"
+ "{"
+ "\"$attributes\":{\"another_foo\":\"another_bar\"}"
+ ","
+ "\"$value\":\"bar\"}"
+ "}"
+ ","
+ "\"$value\":\"some_string\""
+ "}";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, AnnotateWithTypes)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnUint64Scalar(123));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(71.6)));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBooleanScalar(true));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBooleanScalar(false));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("aaa"));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnUint64Scalar(123));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(71.6)));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBooleanScalar(true));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBooleanScalar(false));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(89));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnUint64Scalar(89));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(89)));
+ EXPECT_CALL(Mock, OnEndList());
+
+ TString input =
+ "["
+ "{\"$value\":\"42\",\"$type\":\"int64\"},"
+ "{\"$value\":\"123\",\"$type\":\"uint64\"},"
+ "{\"$value\":\"71.6\",\"$type\":\"double\"},"
+ "{\"$value\":\"true\",\"$type\":\"boolean\"},"
+ "{\"$value\":\"false\",\"$type\":\"boolean\"},"
+ "{\"$value\":\"aaa\",\"$type\":\"string\"},"
+
+ "{\"$value\":42,\"$type\":\"int64\"},"
+ "{\"$value\":123,\"$type\":\"uint64\"},"
+ "{\"$value\":71.6,\"$type\":\"double\"},"
+ "{\"$value\":true,\"$type\":\"boolean\"},"
+ "{\"$value\":false,\"$type\":\"boolean\"},"
+
+ "{\"$value\":89,\"$type\":\"int64\"},"
+ "{\"$value\":89,\"$type\":\"uint64\"},"
+ "{\"$value\":89,\"$type\":\"double\"}"
+ "]";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, SomeHackyTest)
+{
+ TString input = "{\"$value\": \"yamr\", \"$attributes\": {\"lenval\": \"false\", \"has_subkey\": \"false\"}}";
+
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("lenval"));
+ EXPECT_CALL(Mock, OnStringScalar("false"));
+ EXPECT_CALL(Mock, OnKeyedItem("has_subkey"));
+ EXPECT_CALL(Mock, OnStringScalar("false"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnStringScalar("yamr"));
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, EmptyListFragment)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ TString empty;
+ TStringInput stream(empty);
+ ParseJson(&stream, &Mock, nullptr, EYsonType::ListFragment);
+}
+
+TEST(TJsonParserTest, ListFragment)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"hello\":\"world\"}\n{\"foo\":\"bar\"}\n";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock, nullptr, EYsonType::ListFragment);
+}
+
+TEST(TJsonParserTest, SpecialKeys)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("$$value"));
+ EXPECT_CALL(Mock, OnStringScalar("10"));
+ EXPECT_CALL(Mock, OnKeyedItem("$attributes"));
+ EXPECT_CALL(Mock, OnStringScalar("20"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"$$$value\":\"10\",\"$$attributes\":\"20\"}\n";
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock, nullptr, EYsonType::ListFragment);
+}
+
+TEST(TJsonParserTest, AttributesWithoutValue)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ TString input = "{\"$attributes\":\"20\"}";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, Trash)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ TString input = "fdslfsdhfkajsdhf";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, TrailingTrash)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("b"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"a\":\"b\"} fdslfsdhfkajsdhf";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, MultipleValues)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnStringScalar("b"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"a\":\"b\"}{\"a\":\"b\"}";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, ReservedKeyName)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ EXPECT_CALL(Mock, OnBeginMap());
+
+ TString input = "{\"$other\":\"20\"}";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock)
+ );
+}
+
+TEST(TJsonParserTest, MemoryLimit1)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ auto config = New<TJsonFormatConfig>();
+ config->MemoryLimit = 10;
+
+ TString input = "{\"my_string\":\"" + TString(100000, 'X') + "\"}";
+
+ TStringInput stream(input);
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock, config)
+ );
+}
+
+TEST(TJsonParserTest, MemoryLimit2)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("my_string"));
+ TString expectedString(100000, 'X');
+ EXPECT_CALL(Mock, OnStringScalar(expectedString));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input = "{\"my_string\":\"" + TString(100000, 'X') + "\"}";
+
+ auto config = New<TJsonFormatConfig>();
+ config->MemoryLimit = 500000;
+
+ TStringInput stream(input);
+ ParseJson(&stream, &Mock);
+}
+
+TEST(TJsonParserTest, MemoryLimit3)
+{
+ StrictMock<TMockYsonConsumer> Mock;
+
+ auto config = New<TJsonFormatConfig>();
+ config->MemoryLimit = 1000;
+
+ int keyCount = 100;
+ TStringStream stream;
+ stream << "{";
+ for (int i = 0; i < keyCount; ++i) {
+ stream << "\"key" << ToString(i) << "\": \"value\"";
+ if (i + 1 < keyCount) {
+ stream << ",";
+ }
+ }
+ stream << "}";
+
+ EXPECT_ANY_THROW(
+ ParseJson(&stream, &Mock, config)
+ );
+}
+
+TEST(TJsonParserTest, MemoryLimit4)
+{
+ NiceMock<TMockYsonConsumer> Mock;
+
+ auto config = New<TJsonFormatConfig>();
+ config->MemoryLimit = 200000;
+ config->BufferSize = 512;
+
+ int rowCount = 1000;
+ int keyCount = 100;
+
+ TStringStream stream;
+ for (int j = 0; j < rowCount; ++j) {
+ stream << "{";
+ for (int i = 0; i < keyCount; ++i) {
+ stream << "\"key" << ToString(i) << "\": \"value\"";
+ if (i + 1 < keyCount) {
+ stream << ",";
+ }
+ }
+ stream << "}\n";
+ }
+
+ // Not throw, because of total memory occupied by all rows is greater than MemoryLimit,
+ // but memory occuied by individual row is much lower than MemoryLimit.
+ ParseJson(&stream, &Mock, config, EYsonType::ListFragment);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/unittests/writer_ut.cpp b/yt/yt/core/json/unittests/writer_ut.cpp
new file mode 100644
index 0000000000..247ae3b900
--- /dev/null
+++ b/yt/yt/core/json/unittests/writer_ut.cpp
@@ -0,0 +1,844 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/json/json_writer.h>
+#include <yt/yt/core/json/config.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <util/string/strip.h>
+
+namespace NYT::NJson {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TString SurroundWithQuotes(const TString& s)
+{
+ TString quote = "\"";
+ return quote + s + quote;
+}
+
+TEST(TJsonWriterTest, Basic)
+{
+ auto getWrittenJson = [] (bool pretty) {
+ TStringStream outputStream;
+ auto writer = CreateJsonWriter(&outputStream, pretty);
+
+ BuildYsonFluently(writer.get())
+ .BeginList()
+ .Item().Value(123)
+ .Item().Value(-56)
+ .Item()
+ .BeginMap()
+ .Item("key").Value(true)
+ .Item("entity").Entity()
+ .Item("value").Value(4.25)
+ .EndMap()
+ .Item().Value(std::numeric_limits<double>::infinity())
+ .EndList();
+ writer->Flush();
+ return outputStream.Str();
+ };
+
+ EXPECT_EQ(getWrittenJson(false), R"([123,-56,{"key":true,"entity":null,"value":4.25},inf])");
+ EXPECT_EQ(Strip(getWrittenJson(true)),
+ "[\n"
+ " 123,\n"
+ " -56,\n"
+ " {\n"
+ " \"key\": true,\n"
+ " \"entity\": null,\n"
+ " \"value\": 4.25\n"
+ " },\n"
+ " inf\n"
+ "]");
+}
+
+TEST(TJsonWriterTest, StartNextValue)
+{
+ TStringStream outputStream;
+ {
+ auto writer = CreateJsonWriter(&outputStream);
+
+ BuildYsonFluently(writer.get())
+ .BeginList()
+ .Item().Value(123)
+ .Item().Value("hello")
+ .EndList();
+
+ writer->StartNextValue();
+
+ BuildYsonFluently(writer.get())
+ .BeginMap()
+ .Item("abc").Value(true)
+ .Item("def").Value(-1.5)
+ .EndMap();
+
+ writer->Flush();
+ }
+
+ EXPECT_EQ(outputStream.Str(), "[123,\"hello\"]\n{\"abc\":true,\"def\":-1.5}");
+}
+
+TEST(TJsonWriterTest, Errors)
+{
+ TStringStream outputStream;
+
+ {
+ auto writer = CreateJsonWriter(&outputStream);
+ // Non-UTF-8.
+ EXPECT_THROW(writer->OnStringScalar("\xFF\xFE\xFC"), TErrorException);
+ }
+ {
+ auto writer = CreateJsonWriter(&outputStream);
+ EXPECT_THROW(writer->OnBeginAttributes(), TErrorException);
+ }
+ {
+ auto writer = CreateJsonWriter(&outputStream);
+ EXPECT_THROW(writer->OnRaw("{x=3}", EYsonType::Node), TErrorException);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Basic types.
+TEST(TJsonConsumerTest, List)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ consumer->OnInt64Scalar(1);
+ consumer->OnListItem();
+ consumer->OnStringScalar("aaa");
+ consumer->OnListItem();
+ consumer->OnDoubleScalar(3.5);
+ consumer->OnEndList();
+ consumer->Flush();
+
+ TString output = "[1,\"aaa\",3.5]";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, Map)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar("world");
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":\"world\",\"foo\":\"bar\"}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, DoubleMap)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream, NYson::EYsonType::ListFragment);
+
+ consumer->OnListItem();
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar("world");
+ consumer->OnEndMap();
+ consumer->OnListItem();
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":\"world\"}\n{\"foo\":\"bar\"}\n";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, ListFragmentWithEntity)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream, NYson::EYsonType::ListFragment);
+
+ consumer->OnListItem();
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("x");
+ consumer->OnStringScalar("y");
+ consumer->OnEndAttributes();
+ consumer->OnEntity();
+ consumer->OnListItem();
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar("world");
+ consumer->OnEndMap();
+ consumer->OnListItem();
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"$attributes\":{\"x\":\"y\"},\"$value\":null}\n{\"hello\":\"world\"}\n{\"foo\":\"bar\"}\n";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, Entity)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnEntity();
+ consumer->Flush();
+
+ TString output = "null";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, SupportInfinity)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->SupportInfinity = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("a");
+ consumer->OnDoubleScalar(-std::numeric_limits<double>::infinity());
+ consumer->OnKeyedItem("b");
+ consumer->OnDoubleScalar(std::numeric_limits<double>::infinity());
+ consumer->OnEndMap();
+
+ consumer->Flush();
+
+ TString output = "{\"a\":-inf,\"b\":inf}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, StringifyNanAndInfinity)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->StringifyNanAndInfinity = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("a");
+ consumer->OnDoubleScalar(-std::numeric_limits<double>::infinity());
+ consumer->OnKeyedItem("b");
+ consumer->OnDoubleScalar(std::numeric_limits<double>::infinity());
+ consumer->OnKeyedItem("c");
+ consumer->OnDoubleScalar(std::numeric_limits<double>::quiet_NaN());
+ consumer->OnEndMap();
+
+ consumer->Flush();
+
+ TString output = "{\"a\":\"-inf\",\"b\":\"inf\",\"c\":\"nan\"}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, EmptyString)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnStringScalar("");
+ consumer->Flush();
+
+ TString output = SurroundWithQuotes("");
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, AsciiString)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ TString s = TString("\x7F\x32", 2);
+ consumer->OnStringScalar(s);
+ consumer->Flush();
+
+ TString output = SurroundWithQuotes(s);
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+
+TEST(TJsonConsumerTest, NonAsciiString)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ TString s = TString("\xFF\x00\x80", 3);
+ consumer->OnStringScalar(s);
+ consumer->Flush();
+
+ TString output = SurroundWithQuotes("\xC3\xBF\\u0000\xC2\x80");
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, NonAsciiStringWithoutEscaping)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->EncodeUtf8 = false;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ TString s = TString("\xC3\xBF", 2);
+ consumer->OnStringScalar(s);
+ consumer->Flush();
+
+ TString output = SurroundWithQuotes(TString("\xC3\xBF", 2));
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, IncorrectUtfWithoutEscaping)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->EncodeUtf8 = false;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ TString s = TString("\xFF", 1);
+ EXPECT_ANY_THROW(
+ consumer->OnStringScalar(s);
+ );
+}
+
+TEST(TJsonConsumerTest, StringStartingWithSpecialSymbol)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ TString s = "&some_string";
+ consumer->OnStringScalar(s);
+ consumer->Flush();
+
+ TString output = SurroundWithQuotes(s);
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Values with attributes.
+TEST(TJsonConsumerTest, ListWithAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnBeginList();
+ consumer->OnListItem();
+ consumer->OnInt64Scalar(1);
+ consumer->OnEndList();
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":[1]"
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, MapWithAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("spam");
+ consumer->OnStringScalar("bad");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":{\"spam\":\"bad\"}"
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, Int64WithAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnInt64Scalar(42);
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":42"
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, Uint64)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnUint64Scalar(42);
+ consumer->Flush();
+
+ TString output = "42";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, EntityWithAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnEntity();
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":null"
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, StringWithAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnStringScalar("some_string");
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":\"bar\"}"
+ ","
+ "\"$value\":\"some_string\""
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, DoubleAttributes)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("another_foo");
+ consumer->OnStringScalar("another_bar");
+ consumer->OnEndAttributes();
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnStringScalar("some_string");
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":"
+ "{"
+ "\"$attributes\":{\"another_foo\":\"another_bar\"}"
+ ","
+ "\"$value\":\"bar\"}"
+ "}"
+ ","
+ "\"$value\":\"some_string\""
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TJsonConsumerTest, NeverAttributes)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AttributesMode = EJsonAttributesMode::Never;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("answer");
+ consumer->OnInt64Scalar(42);
+
+ consumer->OnKeyedItem("question");
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+ consumer->OnStringScalar("strange question");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"answer\":42,"
+ "\"question\":\"strange question\""
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, AlwaysAttributes)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AttributesMode = EJsonAttributesMode::Always;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("answer");
+ consumer->OnInt64Scalar(42);
+
+ consumer->OnKeyedItem("question");
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("foo");
+ consumer->OnStringScalar("bar");
+ consumer->OnEndAttributes();
+ consumer->OnStringScalar("strange question");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output =
+ "{"
+ "\"$attributes\":{\"foo\":{\"$attributes\":{},\"$value\":\"bar\"}},"
+ "\"$value\":"
+ "{"
+ "\"answer\":{\"$attributes\":{},\"$value\":42},"
+ "\"question\":"
+ "{"
+ "\"$attributes\":{\"foo\":{\"$attributes\":{},\"$value\":\"bar\"}},"
+ "\"$value\":\"strange question\""
+ "}"
+ "}"
+ "}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, SpecialKeys)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("$value");
+ consumer->OnStringScalar("foo");
+ consumer->OnKeyedItem("$$attributes");
+ consumer->OnStringScalar("bar");
+ consumer->OnKeyedItem("$other");
+ consumer->OnInt64Scalar(42);
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"$$value\":\"foo\",\"$$$attributes\":\"bar\",\"$$other\":42}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestStringLengthLimit)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->StringLengthLimit = 2;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar(TString(10000, 'A'));
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$incomplete\":true,\"$value\":\"AA\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestAnnotateWithTypes)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar("world");
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$type\":\"string\",\"$value\":\"world\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestAnnotateWithTypesStringify)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ config->Stringify = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnUint64Scalar(-1);
+ consumer->OnKeyedItem("world");
+ consumer->OnDoubleScalar(1.7976931348623157e+308);
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$type\":\"uint64\",\"$value\":\"18446744073709551615\"},"
+ "\"world\":{\"$type\":\"double\",\"$value\":\"1.7976931348623157e+308\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, SeveralOptions)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->StringLengthLimit = 2;
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnStringScalar(TString(10000, 'A'));
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$incomplete\":true,\"$type\":\"string\",\"$value\":\"AA\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, SeveralOptions2)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->StringLengthLimit = 4;
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("mood");
+ consumer->OnInt64Scalar(42);
+ consumer->OnEndAttributes();
+ consumer->OnStringScalar(TString(10000, 'A'));
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$attributes\":{\"mood\":{\"$type\":\"int64\",\"$value\":42}},"
+ "\"$incomplete\":true,\"$type\":\"string\",\"$value\":\"AAAA\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, SeveralOptionsFlushBuffer)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->StringLengthLimit = 2;
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::ListFragment, config);
+
+ consumer->OnListItem();
+ consumer->OnStringScalar(TString(10000, 'A'));
+ consumer->Flush();
+
+ TString output = "{\"$incomplete\":true,\"$type\":\"string\",\"$value\":\"AA\"}\n";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestPrettyFormat)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->Format = EJsonFormat::Pretty;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnInt64Scalar(1);
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\n"
+ " \"hello\": 1\n"
+ "}";
+ EXPECT_EQ(output, StripInPlace(outputStream.Str()));
+}
+
+TEST(TJsonConsumerTest, TestNodeWeightLimitAccepted)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, std::move(config));
+
+ TString yson = "<\"attr\"=123>\"456\"";
+ consumer->OnNodeWeightLimited(yson, yson.Size() - 1);
+ consumer->Flush();
+
+ TString expectedOutput =
+ "{"
+ "\"$incomplete\":true,"
+ "\"$type\":\"any\","
+ "\"$value\":\"\""
+ "}";
+ EXPECT_EQ(expectedOutput, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestNodeWeightLimitRejected)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ TString yson = "<\"attr\"=123>\"456\"";
+ consumer->OnNodeWeightLimited(yson, yson.Size());
+ consumer->Flush();
+
+ TString expectedOutput =
+ "{"
+ "\"$attributes\":{"
+ "\"attr\":123"
+ "},"
+ "\"$value\":\"456\""
+ "}";
+ EXPECT_EQ(expectedOutput, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestStringScalarWeightLimitAccepted)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, std::move(config));
+
+ consumer->OnStringScalarWeightLimited("1234567", 5);
+ consumer->Flush();
+
+ TString expectedOutput =
+ "{"
+ "\"$incomplete\":true,"
+ "\"$type\":\"string\","
+ "\"$value\":\"12345\""
+ "}";
+ EXPECT_EQ(expectedOutput, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestStringScalarWeightLimitRejected)
+{
+ TStringStream outputStream;
+ auto consumer = CreateJsonConsumer(&outputStream);
+
+ TString value = "1234567";
+ consumer->OnStringScalarWeightLimited(value, value.size());
+ consumer->Flush();
+
+ EXPECT_EQ(SurroundWithQuotes(value), outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, TestSetAnnotateWithTypesParameter)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ auto consumer = CreateJsonConsumer(&outputStream, EYsonType::Node, std::move(config));
+
+ consumer->OnBeginList();
+
+ consumer->OnListItem();
+ consumer->OnStringScalar("1234567");
+
+ consumer->SetAnnotateWithTypesParameter(false);
+ consumer->OnListItem();
+ consumer->OnStringScalar("1234567");
+
+ consumer->OnEndList();
+
+ consumer->Flush();
+
+ TString expectedOutput =
+ "["
+ "{"
+ "\"$type\":\"string\","
+ "\"$value\":\"1234567\""
+ "},"
+ "\"1234567\""
+ "]";
+
+ EXPECT_EQ(expectedOutput, outputStream.Str());
+}
+
+TEST(TJsonConsumerTest, ThroughJsonWriter)
+{
+ TStringStream outputStream;
+ auto config = New<TJsonFormatConfig>();
+ config->AnnotateWithTypes = true;
+ config->Stringify = true;
+
+ auto writer = CreateJsonWriter(&outputStream);
+ auto consumer = CreateJsonConsumer(writer.get(), EYsonType::Node, config);
+
+ consumer->OnBeginMap();
+ consumer->OnKeyedItem("hello");
+ consumer->OnUint64Scalar(-1);
+ consumer->OnKeyedItem("world");
+ consumer->OnDoubleScalar(1.7976931348623157e+308);
+ consumer->OnEndMap();
+ consumer->Flush();
+
+ TString output = "{\"hello\":{\"$type\":\"uint64\",\"$value\":\"18446744073709551615\"},"
+ "\"world\":{\"$type\":\"double\",\"$value\":\"1.7976931348623157e+308\"}}";
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NJson
diff --git a/yt/yt/core/json/unittests/ya.make b/yt/yt/core/json/unittests/ya.make
new file mode 100644
index 0000000000..5b174a52c9
--- /dev/null
+++ b/yt/yt/core/json/unittests/ya.make
@@ -0,0 +1,40 @@
+GTEST(unittester-core-json)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ parser_ut.cpp
+ writer_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/logging/compression.cpp b/yt/yt/core/logging/compression.cpp
new file mode 100644
index 0000000000..0fc42506eb
--- /dev/null
+++ b/yt/yt/core/logging/compression.cpp
@@ -0,0 +1,150 @@
+#include "compression.h"
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+namespace NYT::NLogging {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAppendableCompressedFile::TAppendableCompressedFile(
+ TFile file,
+ ILogCompressionCodecPtr codec,
+ IInvokerPtr compressInvoker,
+ bool writeTruncateMessage)
+ : Codec_(std::move(codec))
+ , CompressInvoker_(std::move(compressInvoker))
+ , SerializedInvoker_(CreateSerializedInvoker(CompressInvoker_, NProfiling::TTagSet({{"invoker", "appendable_compressed_file"}, {"file_name", file.GetName()}})))
+ , MaxBlockSize_(static_cast<size_t>(Codec_->GetMaxBlockSize()))
+ , File_(std::move(file))
+{
+ i64 fileSize = File_.GetLength();
+ Codec_->Repair(&File_, OutputPosition_);
+ if (OutputPosition_ != fileSize && writeTruncateMessage) {
+ TStringBuilder message;
+ message.AppendFormat("Truncated %v bytes due to zstd repair.\n", fileSize - OutputPosition_);
+ TString messageStr = message.Flush();
+
+ Input_.Append(messageStr.Data(), messageStr.Size());
+ Flush();
+ }
+}
+
+void TAppendableCompressedFile::DoWrite(const void* buf, size_t len)
+{
+ const char* in = reinterpret_cast<const char*>(buf);
+ while (len > 0) {
+ size_t toWrite = len;
+ if (Input_.Size() >= MaxBlockSize_) {
+ toWrite = 0;
+ } else if (Input_.Size() + len >= MaxBlockSize_) {
+ toWrite = MaxBlockSize_ - Input_.Size();
+ }
+
+ Input_.Append(in, toWrite);
+ in += toWrite;
+ len -= toWrite;
+
+ while (Input_.Size() >= MaxBlockSize_) {
+ EnqueueOneFrame();
+ }
+ }
+}
+
+void TAppendableCompressedFile::EnqueueOneFrame()
+{
+ if (Input_.Empty()) {
+ return;
+ }
+
+ size_t toWrite = std::min(Input_.Size(), MaxBlockSize_);
+ EnqueueBuffer(TBuffer(Input_.Data(), toWrite));
+ Input_.ChopHead(toWrite);
+}
+
+void TAppendableCompressedFile::EnqueueBuffer(TBuffer buffer)
+{
+ i64 bufferId = EnqueuedBuffersCount_;
+ ++EnqueuedBuffersCount_;
+
+ BIND([this_ = MakeStrong(this), this, buffer = std::move(buffer)] {
+ TBuffer output;
+ Codec_->Compress(buffer, output);
+ return output;
+ })
+ .AsyncVia(CompressInvoker_)
+ .Run()
+ .Subscribe(BIND([this_ = MakeStrong(this), this, bufferId] (TErrorOr<TBuffer> result) {
+ YT_VERIFY(result.IsOK());
+
+ CompressedBuffers_[bufferId] = std::move(result.Value());
+
+ auto guard = Guard(FlushSpinLock_);
+ ++CompressedBuffersCount_;
+ if (CompressedBuffersCount_ == BuffersToFlushCount_) {
+ ReadyToFlushEvent_.Set();
+ }
+ })
+ .Via(SerializedInvoker_));
+}
+
+void TAppendableCompressedFile::DoFlush()
+{
+ while (!Input_.Empty()) {
+ EnqueueOneFrame();
+ }
+ FlushOutput();
+}
+
+void TAppendableCompressedFile::FlushOutput()
+{
+ TFuture<void> readyToFlushFuture;
+
+ {
+ auto guard = Guard(FlushSpinLock_);
+ BuffersToFlushCount_ = EnqueuedBuffersCount_;
+ if (BuffersToFlushCount_ == CompressedBuffersCount_) {
+ readyToFlushFuture = VoidFuture;
+ } else {
+ ReadyToFlushEvent_ = NewPromise<void>();
+ readyToFlushFuture = ReadyToFlushEvent_.ToFuture();
+ }
+ }
+
+ auto asyncOutputBuffer = readyToFlushFuture
+ .Apply(BIND([this_ = MakeStrong(this), this, outputPosition = OutputPosition_] {
+ TBuffer output;
+
+ while (!CompressedBuffers_.empty()) {
+ auto it = CompressedBuffers_.find(WrittenBuffersCount_);
+ YT_VERIFY(it != CompressedBuffers_.end());
+
+ output.Append(it->second.Data(), it->second.Size());
+ Codec_->AddSyncTag(outputPosition + output.Size(), output);
+
+ CompressedBuffers_.erase(it);
+ ++WrittenBuffersCount_;
+ }
+
+ return output;
+ })
+ .AsyncVia(SerializedInvoker_));
+
+ // We use .Get() here instead of WaitFor() here to ensure that this method doesn't do context switches.
+ // Otherwise, flush events in TLogManager may intersect, because another event could start while we are
+ // waiting in WaitFor().
+ auto outputBuffer = asyncOutputBuffer.Get().ValueOrThrow();
+ File_.Pwrite(outputBuffer.Data(), outputBuffer.Size(), OutputPosition_);
+ OutputPosition_ += outputBuffer.Size();
+}
+
+void TAppendableCompressedFile::DoFinish()
+{
+ Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/compression.h b/yt/yt/core/logging/compression.h
new file mode 100644
index 0000000000..857add43e4
--- /dev/null
+++ b/yt/yt/core/logging/compression.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "public.h"
+
+#include "stream_output.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <util/generic/buffer.h>
+
+#include <util/stream/file.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Base interface for compression codec in logs. It is different from ICodec in yt/yt/core/compression,
+//! as we must have the possibility to repair the corrupted log file if the process died unexpectedly.
+struct ILogCompressionCodec
+ : public TRefCounted
+{
+ // NB. All the methods in this interface must be thread-safe.
+
+ virtual i64 GetMaxBlockSize() const = 0;
+ virtual void Compress(const TBuffer& input, TBuffer& output) = 0;
+ virtual void AddSyncTag(i64 offset, TBuffer& output) = 0;
+ virtual void Repair(TFile* file, i64& outputPosition) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ILogCompressionCodec)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAppendableCompressedFile
+ : public IStreamLogOutput
+{
+public:
+ TAppendableCompressedFile(
+ TFile file,
+ ILogCompressionCodecPtr codec,
+ IInvokerPtr compressInvoker,
+ bool writeTruncateMessage);
+
+private:
+ const ILogCompressionCodecPtr Codec_;
+ const IInvokerPtr CompressInvoker_;
+ const IInvokerPtr SerializedInvoker_;
+ const size_t MaxBlockSize_;
+
+ TFile File_;
+
+ // These fields are read and updated in the thread that owns this TAppendableCompressedFile.
+ TBuffer Input_;
+ i64 EnqueuedBuffersCount_ = 0;
+ i64 OutputPosition_ = 0;
+
+ // These fields are read and updated in SerializedInvoker_.
+ THashMap<i64, TBuffer> CompressedBuffers_;
+ i64 WrittenBuffersCount_ = 0;
+
+ // These fields are read and updated under FlushSpinLock_.
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, FlushSpinLock_);
+ i64 BuffersToFlushCount_ = 0;
+ i64 CompressedBuffersCount_ = 0;
+ TPromise<void> ReadyToFlushEvent_;
+
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFlush() override;
+ void DoFinish() override;
+
+ void EnqueueBuffer(TBuffer buffer);
+ void EnqueueOneFrame();
+
+ void FlushOutput();
+};
+
+DECLARE_REFCOUNTED_TYPE(TAppendableCompressedFile)
+DEFINE_REFCOUNTED_TYPE(TAppendableCompressedFile)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/config-inl.h b/yt/yt/core/logging/config-inl.h
new file mode 100644
index 0000000000..52b2010df2
--- /dev/null
+++ b/yt/yt/core/logging/config-inl.h
@@ -0,0 +1,26 @@
+#ifndef CONFIG_INL_H_
+#error "Direct inclusion of this file is not allowed, include config.h"
+// For the sake of sane code completion.
+#include "config.h"
+#endif
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTypedConfigPtr>
+NYTree::IMapNodePtr TLogWriterConfig::BuildFullConfig(const TTypedConfigPtr& typedConfig)
+{
+ auto result = NYTree::GetEphemeralNodeFactory()->CreateMap();
+ for (const auto& [key, value] : NYTree::ConvertToNode(this)->AsMap()->GetChildren()) {
+ result->AddChild(key, value);
+ }
+ for (const auto& [key, value] : NYTree::ConvertToNode(typedConfig)->AsMap()->GetChildren()) {
+ result->AddChild(key, value);
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/config.cpp b/yt/yt/core/logging/config.cpp
new file mode 100644
index 0000000000..83c6533884
--- /dev/null
+++ b/yt/yt/core/logging/config.cpp
@@ -0,0 +1,483 @@
+#include "config.h"
+#include "private.h"
+#include "file_log_writer.h"
+#include "stream_log_writer.h"
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <util/string/vector.h>
+#include <util/system/env.h>
+
+#include <library/cpp/iterator/functools.h>
+
+namespace NYT::NLogging {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+std::optional<ELogLevel> GetLogLevelFromEnv()
+{
+ auto logLevelStr = GetEnv("YT_LOG_LEVEL");
+ if (logLevelStr.empty()) {
+ return {};
+ }
+
+ // This handles most typical casings like "DEBUG", "debug", "Debug".
+ logLevelStr.to_title();
+ return TEnumTraits<ELogLevel>::FromString(logLevelStr);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TFileLogWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("file_name", &TThis::FileName);
+ registrar.Parameter("enable_compression", &TThis::EnableCompression)
+ .Default(false);
+ registrar.Parameter("compression_method", &TThis::CompressionMethod)
+ .Default(ECompressionMethod::Gzip);
+ registrar.Parameter("compression_level", &TThis::CompressionLevel)
+ .Default(6);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->CompressionMethod == ECompressionMethod::Gzip && (config->CompressionLevel < 0 || config->CompressionLevel > 9)) {
+ THROW_ERROR_EXCEPTION("Invalid \"compression_level\" attribute for \"gzip\" compression method");
+ } else if (config->CompressionMethod == ECompressionMethod::Zstd && config->CompressionLevel > 22) {
+ THROW_ERROR_EXCEPTION("Invalid \"compression_level\" attribute for \"zstd\" compression method");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TLogWriterConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("type", &TThis::Type);
+ registrar.Parameter("format", &TThis::Format)
+ .Alias("accepted_message_format")
+ .Default(ELogFormat::PlainText);
+ registrar.Parameter("rate_limit", &TThis::RateLimit)
+ .Default();
+ registrar.Parameter("common_fields", &TThis::CommonFields)
+ .Default();
+ registrar.Parameter("enable_system_messages", &TThis::EnableSystemMessages)
+ .Alias("enable_control_messages")
+ .Default();
+ registrar.Parameter("enable_source_location", &TThis::EnableSourceLocation)
+ .Default(false);
+ registrar.Parameter("json_format", &TThis::JsonFormat)
+ .Default();
+
+ registrar.Postprocessor([] (TThis* config) {
+ // COMPAT(max42).
+ if (config->Format == ELogFormat::Structured) {
+ config->Format = ELogFormat::Json;
+ }
+ });
+}
+
+ELogFamily TLogWriterConfig::GetFamily() const
+{
+ return Format == ELogFormat::PlainText ? ELogFamily::PlainText : ELogFamily::Structured;
+}
+
+bool TLogWriterConfig::AreSystemMessagesEnabled() const
+{
+ return EnableSystemMessages.value_or(GetFamily() == ELogFamily::PlainText);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRuleConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("include_categories", &TThis::IncludeCategories)
+ .Default();
+ registrar.Parameter("exclude_categories", &TThis::ExcludeCategories)
+ .Default();
+ registrar.Parameter("min_level", &TThis::MinLevel)
+ .Default(ELogLevel::Minimum);
+ registrar.Parameter("max_level", &TThis::MaxLevel)
+ .Default(ELogLevel::Maximum);
+ registrar.Parameter("family", &TThis::Family)
+ .Alias("message_format")
+ .Default(ELogFamily::PlainText);
+ registrar.Parameter("writers", &TThis::Writers)
+ .NonEmpty();
+}
+
+bool TRuleConfig::IsApplicable(TStringBuf category, ELogFamily family) const
+{
+ return
+ Family == family &&
+ ExcludeCategories.find(category) == ExcludeCategories.end() &&
+ (!IncludeCategories || IncludeCategories->find(category) != IncludeCategories->end());
+}
+
+bool TRuleConfig::IsApplicable(TStringBuf category, ELogLevel level, ELogFamily family) const
+{
+ return
+ IsApplicable(category, family) &&
+ MinLevel <= level && level <= MaxLevel;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TLogManagerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("flush_period", &TThis::FlushPeriod)
+ .Default();
+ registrar.Parameter("watch_period", &TThis::WatchPeriod)
+ .Default();
+ registrar.Parameter("check_space_period", &TThis::CheckSpacePeriod)
+ .Default();
+ registrar.Parameter("min_disk_space", &TThis::MinDiskSpace)
+ .GreaterThanOrEqual(0)
+ .Default(5_GB);
+ registrar.Parameter("high_backlog_watermark", &TThis::HighBacklogWatermark)
+ .GreaterThanOrEqual(0)
+ .Default(10'000'000);
+ registrar.Parameter("low_backlog_watermark", &TThis::LowBacklogWatermark)
+ .GreaterThanOrEqual(0)
+ .Default(1'000'000);
+ registrar.Parameter("shutdown_grace_timeout", &TThis::ShutdownGraceTimeout)
+ .Default(TDuration::Seconds(1));
+
+ registrar.Parameter("writers", &TThis::Writers);
+ registrar.Parameter("rules", &TThis::Rules);
+ registrar.Parameter("suppressed_messages", &TThis::SuppressedMessages)
+ .Default();
+ registrar.Parameter("category_rate_limits", &TThis::CategoryRateLimits)
+ .Default();
+
+ registrar.Parameter("request_suppression_timeout", &TThis::RequestSuppressionTimeout)
+ .Alias("trace_suppression_timeout")
+ .Default(TDuration::Zero());
+
+ registrar.Parameter("enable_anchor_profiling", &TThis::EnableAnchorProfiling)
+ .Default(false);
+ registrar.Parameter("min_logged_message_rate_to_profile", &TThis::MinLoggedMessageRateToProfile)
+ .Default(1.0);
+
+ registrar.Parameter("abort_on_alert", &TThis::AbortOnAlert)
+ .Default(false);
+
+ registrar.Parameter("structured_validation_sampling_rate", &TThis::StructuredValidationSamplingRate)
+ .Default(0.01)
+ .InRange(0.0, 1.0);
+
+ registrar.Parameter("compression_thread_count", &TThis::CompressionThreadCount)
+ .Default(1);
+
+ registrar.Postprocessor([] (TThis* config) {
+ THashMap<TString, ELogFamily> writerNameToFamily;
+ for (const auto& [writerName, writerConfig] : config->Writers) {
+ try {
+ auto typedWriterConfig = ConvertTo<TLogWriterConfigPtr>(writerConfig);
+ EmplaceOrCrash(writerNameToFamily, writerName, typedWriterConfig->GetFamily());
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Malformed configuration of writer %Qv",
+ writerName)
+ << ex;
+ }
+ }
+
+ for (const auto& [ruleIndex, rule] : Enumerate(config->Rules)) {
+ for (const auto& writerName : rule->Writers) {
+ auto it = writerNameToFamily.find(writerName);
+ if (it == writerNameToFamily.end()) {
+ THROW_ERROR_EXCEPTION("Unknown writer %Qv", writerName);
+ }
+ if (rule->Family != it->second) {
+ THROW_ERROR_EXCEPTION("Writer %Qv has family %Qlv while rule %v has family %Qlv",
+ writerName,
+ it->second,
+ ruleIndex,
+ rule->Family);
+ }
+ }
+ }
+ });
+}
+
+TLogManagerConfigPtr TLogManagerConfig::ApplyDynamic(const TLogManagerDynamicConfigPtr& dynamicConfig) const
+{
+ auto mergedConfig = New<TLogManagerConfig>();
+ mergedConfig->FlushPeriod = FlushPeriod;
+ mergedConfig->WatchPeriod = WatchPeriod;
+ mergedConfig->CheckSpacePeriod = CheckSpacePeriod;
+ mergedConfig->MinDiskSpace = dynamicConfig->MinDiskSpace.value_or(MinDiskSpace);
+ mergedConfig->HighBacklogWatermark = dynamicConfig->HighBacklogWatermark.value_or(HighBacklogWatermark);
+ mergedConfig->LowBacklogWatermark = dynamicConfig->LowBacklogWatermark.value_or(LowBacklogWatermark);
+ mergedConfig->ShutdownGraceTimeout = ShutdownGraceTimeout;
+ mergedConfig->Rules = CloneYsonStructs(dynamicConfig->Rules.value_or(Rules));
+ mergedConfig->Writers = CloneYsonStructs(Writers);
+ mergedConfig->SuppressedMessages = dynamicConfig->SuppressedMessages.value_or(SuppressedMessages);
+ mergedConfig->CategoryRateLimits = dynamicConfig->CategoryRateLimits.value_or(CategoryRateLimits);
+ mergedConfig->RequestSuppressionTimeout = dynamicConfig->RequestSuppressionTimeout.value_or(RequestSuppressionTimeout);
+ mergedConfig->EnableAnchorProfiling = dynamicConfig->EnableAnchorProfiling.value_or(EnableAnchorProfiling);
+ mergedConfig->MinLoggedMessageRateToProfile = dynamicConfig->MinLoggedMessageRateToProfile.value_or(MinLoggedMessageRateToProfile);
+ mergedConfig->AbortOnAlert = dynamicConfig->AbortOnAlert.value_or(AbortOnAlert);
+ mergedConfig->StructuredValidationSamplingRate = dynamicConfig->StructuredValidationSamplingRate.value_or(StructuredValidationSamplingRate);
+ mergedConfig->CompressionThreadCount = dynamicConfig->CompressionThreadCount.value_or(CompressionThreadCount);
+ mergedConfig->Postprocess();
+ return mergedConfig;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateLogFile(const TString& path)
+{
+ auto rule = New<TRuleConfig>();
+ rule->MinLevel = ELogLevel::Trace;
+ rule->Writers.push_back(TString(DefaultFileWriterName));
+
+ auto writerConfig = New<TLogWriterConfig>();
+ writerConfig->Type = TFileLogWriterConfig::Type;
+
+ auto fileWriterConfig = New<TFileLogWriterConfig>();
+ fileWriterConfig->FileName = NFS::NormalizePathSeparators(path);
+
+ auto config = New<TLogManagerConfig>();
+ config->Rules.push_back(rule);
+ EmplaceOrCrash(config->Writers, DefaultFileWriterName, writerConfig->BuildFullConfig(fileWriterConfig));
+ config->MinDiskSpace = 0;
+ config->HighBacklogWatermark = 100'000;
+ config->LowBacklogWatermark = 100'000;
+
+ config->Postprocess();
+ return config;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateStderrLogger(ELogLevel logLevel)
+{
+ auto rule = New<TRuleConfig>();
+ rule->MinLevel = logLevel;
+ rule->Writers.push_back(TString(DefaultStderrWriterName));
+
+ auto writerConfig = New<TLogWriterConfig>();
+ writerConfig->Type = TStderrLogWriterConfig::Type;
+
+ auto stderrWriterConfig = New<TStderrLogWriterConfig>();
+
+ auto config = New<TLogManagerConfig>();
+ config->Rules.push_back(rule);
+ config->Writers.emplace(DefaultStderrWriterName, writerConfig->BuildFullConfig(stderrWriterConfig));
+ config->MinDiskSpace = 0;
+ config->HighBacklogWatermark = 100'000;
+ config->LowBacklogWatermark = 100'000;
+
+ config->Postprocess();
+ return config;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateDefault()
+{
+ return CreateStderrLogger(DefaultStderrMinLevel);
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateQuiet()
+{
+ return CreateStderrLogger(DefaultStderrQuietLevel);
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateSilent()
+{
+ auto config = New<TLogManagerConfig>();
+ config->MinDiskSpace = 0;
+ config->HighBacklogWatermark = 0;
+ config->LowBacklogWatermark = 0;
+
+ config->Postprocess();
+ return config;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateYTServer(
+ const TString& componentName,
+ const TString& directory,
+ const THashMap<TString, TString>& structuredCategoryToWriterName)
+{
+ auto config = New<TLogManagerConfig>();
+
+ auto logLevel = GetLogLevelFromEnv().value_or(ELogLevel::Debug);
+
+ static const std::vector logLevels{
+ ELogLevel::Trace,
+ ELogLevel::Debug,
+ ELogLevel::Info,
+ ELogLevel::Error
+ };
+
+ for (auto currentLogLevel : logLevels) {
+ if (currentLogLevel < logLevel) {
+ continue;
+ }
+
+ auto rule = New<TRuleConfig>();
+ // Due to historical reasons, error logs usually contain warning messages.
+ rule->MinLevel = currentLogLevel == ELogLevel::Error ? ELogLevel::Warning : currentLogLevel;
+ rule->Writers.push_back(ToString(currentLogLevel));
+
+ auto writerConfig = New<TLogWriterConfig>();
+ writerConfig->Type = TFileLogWriterConfig::Type;
+
+ auto fileWriterConfig = New<TFileLogWriterConfig>();
+ auto fileName = Format(
+ "%v/%v%v.log",
+ directory,
+ componentName,
+ currentLogLevel == ELogLevel::Info ? "" : "." + FormatEnum(currentLogLevel));
+ fileWriterConfig->FileName = NFS::NormalizePathSeparators(fileName);
+
+ config->Rules.push_back(rule);
+ config->Writers.emplace(ToString(currentLogLevel), writerConfig->BuildFullConfig(fileWriterConfig));
+ }
+
+ for (const auto& [category, writerName] : structuredCategoryToWriterName) {
+ auto rule = New<TRuleConfig>();
+ rule->MinLevel = ELogLevel::Info;
+ rule->Writers.emplace_back(writerName);
+ rule->Family = ELogFamily::Structured;
+ rule->IncludeCategories = {category};
+
+ auto writerConfig = New<TLogWriterConfig>();
+ writerConfig->Type = TFileLogWriterConfig::Type;
+ writerConfig->Format = ELogFormat::Yson;
+
+ auto fileWriterConfig = New<TFileLogWriterConfig>();
+ auto fileName = Format(
+ "%v/%v.yson.%v.log",
+ directory,
+ componentName,
+ writerName);
+ fileWriterConfig->FileName = NFS::NormalizePathSeparators(fileName);
+
+ config->Rules.push_back(rule);
+ config->Writers.emplace(writerName, writerConfig->BuildFullConfig(fileWriterConfig));
+ }
+
+ config->Postprocess();
+ return config;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateFromFile(const TString& file, const NYPath::TYPath& path)
+{
+ NYTree::INodePtr node;
+ {
+ TIFStream stream(file);
+ node = NYTree::ConvertToNode(&stream);
+ }
+ return CreateFromNode(std::move(node), path);
+}
+
+TLogManagerConfigPtr TLogManagerConfig::CreateFromNode(NYTree::INodePtr node, const NYPath::TYPath& path)
+{
+ auto config = New<TLogManagerConfig>();
+ config->Load(node, true, true, path);
+ return config;
+}
+
+TLogManagerConfigPtr TLogManagerConfig::TryCreateFromEnv()
+{
+ auto logLevel = GetLogLevelFromEnv();
+ if (!logLevel) {
+ return nullptr;
+ }
+
+ auto logExcludeCategoriesStr = GetEnv("YT_LOG_EXCLUDE_CATEGORIES");
+ auto logIncludeCategoriesStr = GetEnv("YT_LOG_INCLUDE_CATEGORIES");
+
+ auto rule = New<TRuleConfig>();
+ rule->Writers.push_back(TString(DefaultStderrWriterName));
+ rule->MinLevel = *logLevel;
+
+ std::vector<TString> logExcludeCategories;
+ if (logExcludeCategoriesStr) {
+ logExcludeCategories = SplitString(logExcludeCategoriesStr, ",");
+ }
+
+ for (const auto& excludeCategory : logExcludeCategories) {
+ rule->ExcludeCategories.insert(excludeCategory);
+ }
+
+ std::vector<TString> logIncludeCategories;
+ if (logIncludeCategoriesStr) {
+ logIncludeCategories = SplitString(logIncludeCategoriesStr, ",");
+ }
+
+ if (!logIncludeCategories.empty()) {
+ rule->IncludeCategories.emplace();
+ for (const auto& includeCategory : logIncludeCategories) {
+ rule->IncludeCategories->insert(includeCategory);
+ }
+ }
+
+ auto writerConfig = New<TLogWriterConfig>();
+ writerConfig->Type = TStderrLogWriterConfig::Type;
+
+ auto stderrWriterConfig = New<TStderrLogWriterConfig>();
+
+ auto config = New<TLogManagerConfig>();
+ config->Rules.push_back(std::move(rule));
+ config->MinDiskSpace = 0;
+ config->HighBacklogWatermark = std::numeric_limits<int>::max();
+ config->LowBacklogWatermark = 0;
+ EmplaceOrCrash(config->Writers, DefaultStderrWriterName, writerConfig->BuildFullConfig(stderrWriterConfig));
+
+ config->Postprocess();
+ return config;
+}
+
+void TLogManagerConfig::UpdateWriters(
+ const std::function<IMapNodePtr(const IMapNodePtr&)> updater)
+{
+ THashMap<TString, IMapNodePtr> updatedWriters;
+ for (const auto& [name, configNode] : Writers) {
+ if (auto updatedConfigNode = updater(configNode)) {
+ EmplaceOrCrash(updatedWriters, name, updatedConfigNode);
+ }
+ }
+ Writers = std::move(updatedWriters);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TLogManagerDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("min_disk_space", &TThis::MinDiskSpace)
+ .Optional();
+ registrar.Parameter("high_backlog_watermark", &TThis::HighBacklogWatermark)
+ .Optional();
+ registrar.Parameter("low_backlog_watermark", &TThis::LowBacklogWatermark)
+ .Optional();
+
+ registrar.Parameter("rules", &TThis::Rules)
+ .Optional();
+ registrar.Parameter("suppressed_messages", &TThis::SuppressedMessages)
+ .Optional();
+ registrar.Parameter("category_rate_limits", &TThis::CategoryRateLimits)
+ .Optional();
+
+ registrar.Parameter("request_suppression_timeout", &TThis::RequestSuppressionTimeout)
+ .Optional();
+
+ registrar.Parameter("enable_anchor_profiling", &TThis::EnableAnchorProfiling)
+ .Optional();
+ registrar.Parameter("min_logged_message_rate_to_profile", &TThis::MinLoggedMessageRateToProfile)
+ .Optional();
+
+ registrar.Parameter("abort_on_alert", &TThis::AbortOnAlert)
+ .Optional();
+ registrar.Parameter("structured_validation_sampling_rate", &TThis::StructuredValidationSamplingRate)
+ .Optional();
+
+ registrar.Parameter("compression_thread_count", &TThis::CompressionThreadCount)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/config.h b/yt/yt/core/logging/config.h
new file mode 100644
index 0000000000..d091e912a9
--- /dev/null
+++ b/yt/yt/core/logging/config.h
@@ -0,0 +1,212 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/json/public.h>
+#include <yt/yt/core/json/config.h>
+
+#include <yt/yt/core/ytree/public.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileLogWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ static constexpr const TStringBuf Type = "file";
+
+ TString FileName;
+ bool EnableCompression;
+ ECompressionMethod CompressionMethod;
+ int CompressionLevel;
+
+ REGISTER_YSON_STRUCT(TFileLogWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TFileLogWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStderrLogWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ static constexpr TStringBuf Type = "stderr";
+
+ REGISTER_YSON_STRUCT(TStderrLogWriterConfig);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+DEFINE_REFCOUNTED_TYPE(TStderrLogWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogWriterConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString Type;
+
+ ELogFormat Format;
+
+ std::optional<i64> RateLimit;
+
+ //! Common formatter options.
+ std::optional<bool> EnableSystemMessages;
+
+ //! Plain text formatter options.
+ bool EnableSourceLocation;
+
+ //! Structured formatter options.
+ THashMap<TString, NYTree::INodePtr> CommonFields;
+ NJson::TJsonFormatConfigPtr JsonFormat;
+
+ ELogFamily GetFamily() const;
+ bool AreSystemMessagesEnabled() const;
+
+ //! Constructs a full config by combining parameters from this one and #typedConfig.
+ template <class TTypedConfigPtr>
+ NYTree::IMapNodePtr BuildFullConfig(const TTypedConfigPtr& typedConfig);
+
+ REGISTER_YSON_STRUCT(TLogWriterConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TLogWriterConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRuleConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<THashSet<TString>> IncludeCategories;
+ THashSet<TString> ExcludeCategories;
+
+ ELogLevel MinLevel;
+ ELogLevel MaxLevel;
+
+ ELogFamily Family;
+
+ std::vector<TString> Writers;
+
+ bool IsApplicable(TStringBuf category, ELogFamily family) const;
+ bool IsApplicable(TStringBuf category, ELogLevel level, ELogFamily family) const;
+
+ REGISTER_YSON_STRUCT(TRuleConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRuleConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogManagerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<TDuration> FlushPeriod;
+ std::optional<TDuration> WatchPeriod;
+ std::optional<TDuration> CheckSpacePeriod;
+
+ i64 MinDiskSpace;
+
+ int HighBacklogWatermark;
+ int LowBacklogWatermark;
+
+ TDuration ShutdownGraceTimeout;
+
+ std::vector<TRuleConfigPtr> Rules;
+ THashMap<TString, NYTree::IMapNodePtr> Writers;
+ std::vector<TString> SuppressedMessages;
+ THashMap<TString, i64> CategoryRateLimits;
+
+ TDuration RequestSuppressionTimeout;
+
+ bool EnableAnchorProfiling;
+ double MinLoggedMessageRateToProfile;
+
+ bool AbortOnAlert;
+
+ double StructuredValidationSamplingRate;
+
+ int CompressionThreadCount;
+
+ TLogManagerConfigPtr ApplyDynamic(const TLogManagerDynamicConfigPtr& dynamicConfig) const;
+
+ static TLogManagerConfigPtr CreateStderrLogger(ELogLevel logLevel);
+ static TLogManagerConfigPtr CreateLogFile(const TString& path);
+ static TLogManagerConfigPtr CreateDefault();
+ static TLogManagerConfigPtr CreateQuiet();
+ static TLogManagerConfigPtr CreateSilent();
+ //! Create logging config a-la YT server config: #directory/#componentName{,.debug,.error}.log.
+ //! Also allows adding structured logs. For example, pair ("RpcProxyStructuredMain", "main") would
+ //! make structured messages with RpcProxyStructuredMain category go to #directory/#componentName.yson.main.log.
+ static TLogManagerConfigPtr CreateYTServer(
+ const TString& componentName,
+ const TString& directory = ".",
+ const THashMap<TString, TString>& structuredCategoryToWriterName = {});
+ static TLogManagerConfigPtr CreateFromFile(const TString& file, const NYPath::TYPath& path = "");
+ static TLogManagerConfigPtr CreateFromNode(NYTree::INodePtr node, const NYPath::TYPath& path = "");
+ static TLogManagerConfigPtr TryCreateFromEnv();
+
+ //! Applies #updater to each writer config in #Writers.
+ //! If null is returned then the writer is removed, otherwise it is replaced.
+ void UpdateWriters(const std::function<NYTree::IMapNodePtr(const NYTree::IMapNodePtr&)> updater);
+
+ REGISTER_YSON_STRUCT(TLogManagerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TLogManagerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogManagerDynamicConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<i64> MinDiskSpace;
+
+ std::optional<int> HighBacklogWatermark;
+ std::optional<int> LowBacklogWatermark;
+
+ std::optional<std::vector<TRuleConfigPtr>> Rules;
+ std::optional<std::vector<TString>> SuppressedMessages;
+ std::optional<THashMap<TString, i64>> CategoryRateLimits;
+
+ std::optional<TDuration> RequestSuppressionTimeout;
+
+ std::optional<bool> EnableAnchorProfiling;
+ std::optional<double> MinLoggedMessageRateToProfile;
+
+ std::optional<bool> AbortOnAlert;
+
+ std::optional<double> StructuredValidationSamplingRate;
+
+ std::optional<int> CompressionThreadCount;
+
+ REGISTER_YSON_STRUCT(TLogManagerDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TLogManagerDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
+
+#define CONFIG_INL_H_
+#include "config-inl.h"
+#undef CONFIG_INL_H_
diff --git a/yt/yt/core/logging/file_log_writer.cpp b/yt/yt/core/logging/file_log_writer.cpp
new file mode 100644
index 0000000000..e957048855
--- /dev/null
+++ b/yt/yt/core/logging/file_log_writer.cpp
@@ -0,0 +1,254 @@
+#include "file_log_writer.h"
+
+#include "log_writer_detail.h"
+#include "config.h"
+#include "private.h"
+#include "log.h"
+#include "random_access_gzip.h"
+#include "stream_output.h"
+#include "compression.h"
+#include "log_writer_factory.h"
+#include "zstd_compression.h"
+#include "formatter.h"
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <library/cpp/yt/memory/leaky_ref_counted_singleton.h>
+
+namespace NYT::NLogging {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TLogger Logger(SystemLoggingCategoryName);
+static constexpr size_t BufferSize = 64_KB;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileLogWriter
+ : public TStreamLogWriterBase
+ , public IFileLogWriter
+{
+public:
+ TFileLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ TFileLogWriterConfigPtr config,
+ ILogWriterHost* host)
+ : TStreamLogWriterBase(std::move(formatter), std::move(name))
+ , Config_(std::move(config))
+ , Host_(host)
+ {
+ Open();
+ }
+
+ void Reload() override
+ {
+ Close();
+ Open();
+ }
+
+ const TString& GetFileName() const override
+ {
+ return Config_->FileName;
+ }
+
+ void CheckSpace(i64 minSpace) override
+ {
+ try {
+ auto directoryName = NFS::GetDirectoryName(Config_->FileName);
+ auto statistics = NFS::GetDiskSpaceStatistics(directoryName);
+ if (statistics.AvailableSpace < minSpace) {
+ if (!Disabled_.load(std::memory_order::acquire)) {
+ Disabled_ = true;
+ YT_LOG_ERROR("Log file disabled: not enough space available (FileName: %v, AvailableSpace: %v, MinSpace: %v)",
+ directoryName,
+ statistics.AvailableSpace,
+ minSpace);
+
+ Close();
+ }
+ } else {
+ if (Disabled_.load(std::memory_order::acquire)) {
+ Reload(); // Reinitialize all descriptors.
+
+ YT_LOG_INFO("Log file enabled: space check passed (FileName: %v)",
+ Config_->FileName);
+ Disabled_ = false;
+ }
+ }
+ } catch (const std::exception& ex) {
+ Disabled_ = true;
+ YT_LOG_ERROR(ex, "Log file disabled: space check failed (FileName: %v)",
+ Config_->FileName);
+
+ Close();
+ }
+ }
+
+protected:
+ IOutputStream* GetOutputStream() const noexcept override
+ {
+ if (Y_UNLIKELY(Disabled_.load(std::memory_order::acquire))) {
+ return nullptr;
+ }
+ return OutputStream_.Get();
+ }
+
+ void OnException(const std::exception& ex) override
+ {
+ Disabled_ = true;
+ YT_LOG_ERROR(ex, "Disabled log file (FileName: %v)",
+ Config_->FileName);
+
+ Close();
+ }
+
+private:
+ const TFileLogWriterConfigPtr Config_;
+ ILogWriterHost* const Host_;
+
+ std::atomic<bool> Disabled_ = false;
+
+ std::unique_ptr<TFile> File_;
+ IStreamLogOutputPtr OutputStream_;
+
+
+ void Open()
+ {
+ Disabled_ = false;
+ try {
+ NFS::MakeDirRecursive(NFS::GetDirectoryName(Config_->FileName));
+
+ TFlags<EOpenModeFlag> openMode;
+ if (Config_->EnableCompression) {
+ openMode = OpenAlways|RdWr|CloseOnExec;
+ } else {
+ openMode = OpenAlways|ForAppend|WrOnly|Seq|CloseOnExec;
+ }
+
+ File_.reset(new TFile(Config_->FileName, openMode));
+
+ if (Config_->EnableCompression) {
+ switch (Config_->CompressionMethod) {
+ case ECompressionMethod::Zstd:
+ OutputStream_ = New<TAppendableCompressedFile>(
+ *File_,
+ CreateZstdCompressionCodec(Config_->CompressionLevel),
+ Host_->GetCompressionInvoker(),
+ /*writeTruncateMessage*/ true);
+ break;
+
+ case ECompressionMethod::Gzip:
+ OutputStream_ = New<TRandomAccessGZipFile>(
+ *File_,
+ Config_->CompressionLevel);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ } else {
+ OutputStream_ = New<TFixedBufferFileOutput>(
+ *File_,
+ BufferSize);
+ }
+
+ // Emit a delimiter for ease of navigation.
+ if (File_->GetLength() > 0) {
+ Formatter_->WriteLogReopenSeparator(GetOutputStream());
+ }
+
+ Formatter_->WriteLogStartEvent(GetOutputStream());
+
+ ResetCurrentSegment(File_->GetLength());
+ } catch (const std::exception& ex) {
+ Disabled_ = true;
+ YT_LOG_ERROR(ex, "Failed to open log file (FileName: %v)",
+ Config_->FileName);
+
+ Close();
+ } catch (...) {
+ YT_ABORT();
+ }
+ }
+
+ void Close()
+ {
+ try {
+ if (OutputStream_) {
+ OutputStream_->Finish();
+ OutputStream_.Reset();
+ }
+
+ if (File_) {
+ File_->Close();
+ File_.reset();
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Failed to close log file; ignored (FileName: %v)",
+ Config_->FileName);
+ } catch (...) {
+ YT_ABORT();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFileLogWriterPtr CreateFileLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ TFileLogWriterConfigPtr config,
+ ILogWriterHost* host)
+{
+ return New<TFileLogWriter>(
+ std::move(formatter),
+ std::move(name),
+ std::move(config),
+ host);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFileLogWriterFactory
+ : public ILogWriterFactory
+{
+public:
+ void ValidateConfig(
+ const NYTree::IMapNodePtr& configNode) override
+ {
+ ParseConfig(configNode);
+ }
+
+ ILogWriterPtr CreateWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ const NYTree::IMapNodePtr& configNode,
+ ILogWriterHost* host) noexcept override
+ {
+ return CreateFileLogWriter(
+ std::move(formatter),
+ std::move(name),
+ ParseConfig(configNode),
+ host);
+ }
+
+private:
+ static TFileLogWriterConfigPtr ParseConfig(const NYTree::IMapNodePtr& configNode)
+ {
+ return ConvertTo<TFileLogWriterConfigPtr>(configNode);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ILogWriterFactoryPtr GetFileLogWriterFactory()
+{
+ return LeakyRefCountedSingleton<TFileLogWriterFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/file_log_writer.h b/yt/yt/core/logging/file_log_writer.h
new file mode 100644
index 0000000000..f125688e7c
--- /dev/null
+++ b/yt/yt/core/logging/file_log_writer.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "private.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IFileLogWriterPtr CreateFileLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ TFileLogWriterConfigPtr config,
+ ILogWriterHost* host);
+
+ILogWriterFactoryPtr GetFileLogWriterFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/fluent_log-inl.h b/yt/yt/core/logging/fluent_log-inl.h
new file mode 100644
index 0000000000..896829cda2
--- /dev/null
+++ b/yt/yt/core/logging/fluent_log-inl.h
@@ -0,0 +1,58 @@
+#ifndef FLUENT_LOG_INL_H_
+#error "Direct inclusion of this file is not allowed, include log.h"
+// For the sake of sane code completion.
+#include "log.h"
+#include "fluent_log.h" // it makes CLion happy
+#endif
+#undef FLUENT_LOG_INL_H_
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TParent>
+TOneShotFluentLogEventImpl<TParent>::TOneShotFluentLogEventImpl(
+ TStatePtr state,
+ const NLogging::TLogger& logger,
+ NLogging::ELogLevel level)
+ : TBase(state->GetConsumer())
+ , State_(std::move(state))
+ , Logger_(&logger)
+ , Level_(level)
+{
+ for (const auto& [key, value] : logger.GetStructuredTags()) {
+ (*this).Item(key).Value(value);
+ }
+}
+
+template <class TParent>
+NYTree::TFluentYsonBuilder::TAny<TOneShotFluentLogEventImpl<TParent>&&> TOneShotFluentLogEventImpl<TParent>::Item(TStringBuf key)
+{
+ this->Consumer->OnKeyedItem(key);
+ return NYTree::TFluentYsonBuilder::TAny<TThis&&>(this->Consumer, std::move(*this));
+}
+
+template <class TParent>
+TOneShotFluentLogEventImpl<TParent>::~TOneShotFluentLogEventImpl()
+{
+ if (State_ && *Logger_) {
+ LogStructuredEvent(*Logger_, State_->GetValue(), Level_);
+ }
+}
+
+template <class TParent>
+template <class T, class... TExtraArgs>
+typename TOneShotFluentLogEventImpl<TParent>::TThis& TOneShotFluentLogEventImpl<TParent>::OptionalItem(TStringBuf key, const T& optionalValue, TExtraArgs&&... extraArgs)
+{
+ using NYTree::Serialize;
+
+ if (optionalValue) {
+ this->Consumer->OnKeyedItem(key);
+ Serialize(optionalValue, this->Consumer, std::forward<TExtraArgs>(extraArgs)...);
+ }
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/fluent_log.cpp b/yt/yt/core/logging/fluent_log.cpp
new file mode 100644
index 0000000000..2e43c78496
--- /dev/null
+++ b/yt/yt/core/logging/fluent_log.cpp
@@ -0,0 +1,71 @@
+#include "fluent_log.h"
+
+namespace NYT::NLogging {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NLogging;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TOneShotFluentLogEvent LogStructuredEventFluently(const TLogger& logger, ELogLevel level)
+{
+ return TOneShotFluentLogEvent(
+ New<TFluentYsonWriterState>(EYsonFormat::Binary, EYsonType::MapFragment),
+ logger,
+ level);
+}
+
+TOneShotFluentLogEvent LogStructuredEventFluentlyToNowhere()
+{
+ static const TLogger NullLogger;
+ return TOneShotFluentLogEvent(
+ New<TFluentYsonWriterState>(EYsonFormat::Binary, EYsonType::MapFragment),
+ NullLogger,
+ ELogLevel::Debug);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructuredLogBatcher::TStructuredLogBatcher(TLogger logger, i64 maxBatchSize, ELogLevel level)
+ : Logger(std::move(logger))
+ , MaxBatchSize_(maxBatchSize)
+ , Level_(level)
+{ }
+
+TStructuredLogBatcher::TFluent TStructuredLogBatcher::AddItemFluently()
+{
+ if (std::ssize(BatchYson_) >= MaxBatchSize_) {
+ Flush();
+ }
+ ++BatchItemCount_;
+
+ return BuildYsonListFragmentFluently(&BatchYsonWriter_)
+ .Item();
+}
+
+void TStructuredLogBatcher::Flush()
+{
+ if (BatchItemCount_ == 0) {
+ return;
+ }
+ BatchYsonWriter_.Flush();
+ LogStructuredEventFluently(Logger, Level_)
+ .Item("batch")
+ .BeginList()
+ .Do([&] (TFluentList fluent) {
+ fluent.GetConsumer()->OnRaw(TYsonString(std::move(BatchYson_), EYsonType::ListFragment));
+ })
+ .EndList();
+ BatchYson_.clear();
+ BatchItemCount_ = 0;
+}
+
+TStructuredLogBatcher::~TStructuredLogBatcher()
+{
+ Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/fluent_log.h b/yt/yt/core/logging/fluent_log.h
new file mode 100644
index 0000000000..f2328b75f8
--- /dev/null
+++ b/yt/yt/core/logging/fluent_log.h
@@ -0,0 +1,87 @@
+#pragma once
+
+#include <yt/yt/core/yson/public.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TParent>
+class TOneShotFluentLogEventImpl
+ : public NYTree::TFluentYsonBuilder::TFluentFragmentBase<TOneShotFluentLogEventImpl, TParent, NYTree::TFluentMap>
+{
+public:
+ using TThis = TOneShotFluentLogEventImpl;
+ using TBase = NYTree::TFluentYsonBuilder::TFluentFragmentBase<NLogging::TOneShotFluentLogEventImpl, TParent, NYTree::TFluentMap>;
+ using TStatePtr = TIntrusivePtr<NYTree::TFluentYsonWriterState>;
+
+ TOneShotFluentLogEventImpl(TStatePtr state, const NLogging::TLogger& logger, NLogging::ELogLevel level);
+ TOneShotFluentLogEventImpl(TOneShotFluentLogEventImpl&& other) = default;
+ TOneShotFluentLogEventImpl(const TOneShotFluentLogEventImpl& other) = delete;
+
+ ~TOneShotFluentLogEventImpl();
+
+ TOneShotFluentLogEventImpl& operator=(TOneShotFluentLogEventImpl&& other) = default;
+ TOneShotFluentLogEventImpl& operator=(const TOneShotFluentLogEventImpl& other) = delete;
+
+ // TODO(max42): why these two methods must be re-implemented here? Maybe it is enough to replace TFluentYsonVoid with TFluentMap below?
+
+ NYTree::TFluentYsonBuilder::TAny<TThis&&> Item(TStringBuf key);
+
+ template <class T, class... TExtraArgs>
+ TThis& OptionalItem(TStringBuf key, const T& optionalValue, TExtraArgs&&... extraArgs);
+
+private:
+ TStatePtr State_;
+ const NLogging::TLogger* Logger_;
+ NLogging::ELogLevel Level_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TOneShotFluentLogEvent = TOneShotFluentLogEventImpl<NYTree::TFluentYsonVoid>;
+
+TOneShotFluentLogEvent LogStructuredEventFluently(const NLogging::TLogger& logger, NLogging::ELogLevel level);
+
+TOneShotFluentLogEvent LogStructuredEventFluentlyToNowhere();
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStructuredLogBatcher
+{
+public:
+ explicit TStructuredLogBatcher(
+ TLogger logger,
+ i64 maxBatchSize = 10_KBs,
+ ELogLevel level = ELogLevel::Info);
+
+ using TFluent = decltype(NYTree::BuildYsonListFragmentFluently(nullptr).Item());
+
+ TFluent AddItemFluently();
+
+ ~TStructuredLogBatcher();
+
+private:
+ const NLogging::TLogger Logger;
+ const i64 MaxBatchSize_;
+ const ELogLevel Level_;
+
+ TString BatchYson_;
+ TStringOutput BatchOutputStream_{BatchYson_};
+ NYson::TYsonWriter BatchYsonWriter_{
+ &BatchOutputStream_,
+ NYson::EYsonFormat::Binary,
+ NYson::EYsonType::ListFragment};
+ int BatchItemCount_ = 0;
+
+ void Flush();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
+
+#define FLUENT_LOG_INL_H_
+#include "fluent_log-inl.h"
+#undef FLUENT_LOG_INL_H_
diff --git a/yt/yt/core/logging/formatter.cpp b/yt/yt/core/logging/formatter.cpp
new file mode 100644
index 0000000000..83d658c148
--- /dev/null
+++ b/yt/yt/core/logging/formatter.cpp
@@ -0,0 +1,285 @@
+#include "formatter.h"
+#include "private.h"
+#include "log.h"
+
+#include <yt/yt/build/build.h>
+
+#include <yt/yt/core/json/json_writer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <util/stream/length.h>
+
+namespace NYT::NLogging {
+
+using namespace NProfiling;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TLogger Logger(SystemLoggingCategoryName);
+
+namespace {
+
+TLogEvent GetStartLogEvent()
+{
+ TLogEvent event;
+ event.Instant = GetCpuInstant();
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Info;
+ event.MessageRef = TSharedRef::FromString(Format("Logging started (Version: %v, BuildHost: %v, BuildTime: %v)",
+ GetVersion(),
+ GetBuildHost(),
+ GetBuildTime()));
+ event.MessageKind = ELogMessageKind::Unstructured;
+ return event;
+}
+
+TLogEvent GetStartLogStructuredEvent()
+{
+ TLogEvent event;
+ event.Instant = GetCpuInstant();
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Info;
+ event.MessageRef = BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
+ .Item("message").Value("Logging started")
+ .Item("version").Value(GetVersion())
+ .Item("build_host").Value(GetBuildHost())
+ .Item("build_time").Value(GetBuildTime())
+ .Finish()
+ .ToSharedRef();
+ event.MessageKind = ELogMessageKind::Structured;
+ return event;
+}
+
+TLogEvent GetSkippedLogEvent(i64 count, TStringBuf skippedBy)
+{
+ TLogEvent event;
+ event.Instant = GetCpuInstant();
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Info;
+ event.MessageRef = TSharedRef::FromString(Format("Skipped log records in last second (Count: %v, SkippedBy: %v)",
+ count,
+ skippedBy));
+ event.MessageKind = ELogMessageKind::Unstructured;
+ return event;
+}
+
+TLogEvent GetSkippedLogStructuredEvent(i64 count, TStringBuf skippedBy)
+{
+ TLogEvent event;
+ event.Instant = GetCpuInstant();
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Info;
+ event.MessageRef = BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
+ .Item("message").Value("Events skipped")
+ .Item("skipped_by").Value(skippedBy)
+ .Item("events_skipped").Value(count)
+ .Finish()
+ .ToSharedRef();
+ event.MessageKind = ELogMessageKind::Structured;
+ return event;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCachingDateFormatter::Format(TBaseFormatter* buffer, TInstant dateTime, bool printMicroseconds)
+{
+ auto currentSecond = dateTime.Seconds();
+ if (CachedSecond_ != currentSecond) {
+ Cached_.Reset();
+ FormatDateTime(&Cached_, dateTime);
+ CachedSecond_ = currentSecond;
+ }
+
+ buffer->AppendString(Cached_.GetBuffer());
+ buffer->AppendChar(',');
+ if (printMicroseconds) {
+ FormatMicroseconds(buffer, dateTime);
+ } else {
+ FormatMilliseconds(buffer, dateTime);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPlainTextLogFormatter::TPlainTextLogFormatter(
+ bool enableSystemMessages,
+ bool enableSourceLocation)
+ : Buffer_(std::make_unique<TRawFormatter<MessageBufferSize>>())
+ , CachingDateFormatter_(std::make_unique<TCachingDateFormatter>())
+ , EnableSystemMessages_(enableSystemMessages && Logger)
+ , EnableSourceLocation_(enableSourceLocation)
+{ }
+
+i64 TPlainTextLogFormatter::WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) const
+{
+ if (!outputStream) {
+ return 0;
+ }
+
+ auto* buffer = Buffer_.get();
+ buffer->Reset();
+
+ CachingDateFormatter_->Format(buffer, CpuInstantToInstant(event.Instant), true);
+
+ buffer->AppendChar('\t');
+
+ FormatLevel(buffer, event.Level);
+
+ buffer->AppendChar('\t');
+
+ buffer->AppendString(event.Category->Name);
+
+ buffer->AppendChar('\t');
+
+ FormatMessage(buffer, event.MessageRef.ToStringBuf());
+
+ buffer->AppendChar('\t');
+
+ if (event.ThreadName.Length > 0) {
+ buffer->AppendString(TStringBuf(event.ThreadName.Buffer.data(), event.ThreadName.Length));
+ } else if (event.ThreadId != NThreading::InvalidThreadId) {
+ buffer->AppendNumber(event.ThreadId, 16);
+ }
+
+ buffer->AppendChar('\t');
+
+ if (event.FiberId != NConcurrency::InvalidFiberId) {
+ buffer->AppendNumber(event.FiberId, 16);
+ }
+
+ buffer->AppendChar('\t');
+
+ if (event.TraceId != NTracing::InvalidTraceId) {
+ buffer->AppendGuid(event.TraceId);
+ }
+
+ if (EnableSourceLocation_) {
+ buffer->AppendChar('\t');
+ if (event.SourceFile) {
+ auto sourceFile = event.SourceFile;
+ buffer->AppendString(sourceFile.RNextTok(LOCSLASH_C));
+ buffer->AppendChar(':');
+ buffer->AppendNumber(event.SourceLine);
+ }
+ }
+
+ buffer->AppendChar('\n');
+
+ outputStream->Write(buffer->GetData(), buffer->GetBytesWritten());
+
+ return buffer->GetBytesWritten();
+}
+
+void TPlainTextLogFormatter::WriteLogReopenSeparator(IOutputStream* outputStream) const
+{
+ *outputStream << Endl;
+}
+
+void TPlainTextLogFormatter::WriteLogStartEvent(IOutputStream* outputStream) const
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetStartLogEvent());
+ }
+}
+
+void TPlainTextLogFormatter::WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) const
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetSkippedLogEvent(count, skippedBy));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructuredLogFormatter::TStructuredLogFormatter(
+ ELogFormat format,
+ THashMap<TString, NYTree::INodePtr> commonFields,
+ bool enableSystemMessages,
+ NJson::TJsonFormatConfigPtr jsonFormat)
+ : Format_(format)
+ , CachingDateFormatter_(std::make_unique<TCachingDateFormatter>())
+ , CommonFields_(std::move(commonFields))
+ , EnableSystemMessages_(enableSystemMessages)
+ , JsonFormat_(!jsonFormat && (Format_ == ELogFormat::Json)
+ ? New<NJson::TJsonFormatConfig>()
+ : std::move(jsonFormat))
+{ }
+
+i64 TStructuredLogFormatter::WriteFormatted(IOutputStream* stream, const TLogEvent& event) const
+{
+ if (!stream) {
+ return 0;
+ }
+
+ auto countingStream = TCountingOutput(stream);
+ std::unique_ptr<IFlushableYsonConsumer> consumer;
+
+ switch (Format_) {
+ case ELogFormat::Json:
+ YT_VERIFY(JsonFormat_);
+ consumer = NJson::CreateJsonConsumer(&countingStream, EYsonType::Node, JsonFormat_);
+ break;
+ case ELogFormat::Yson:
+ consumer = std::make_unique<TYsonWriter>(&countingStream, EYsonFormat::Text);
+ break;
+ default:
+ YT_ABORT();
+ }
+
+ TRawFormatter<DateTimeBufferSize> dateTimeBuffer;
+ CachingDateFormatter_->Format(&dateTimeBuffer, CpuInstantToInstant(event.Instant));
+
+ BuildYsonFluently(consumer.get())
+ .BeginMap()
+ .DoFor(CommonFields_, [] (auto fluent, auto item) {
+ fluent.Item(item.first).Value(item.second);
+ })
+ .DoIf(event.MessageKind == ELogMessageKind::Structured, [&] (auto fluent) {
+ fluent.Items(TYsonString(event.MessageRef, EYsonType::MapFragment));
+ })
+ .DoIf(event.MessageKind == ELogMessageKind::Unstructured, [&] (auto fluent) {
+ fluent.Item("message").Value(event.MessageRef.ToStringBuf());
+ })
+ .Item("instant").Value(dateTimeBuffer.GetBuffer())
+ .Item("level").Value(FormatEnum(event.Level))
+ .Item("category").Value(event.Category->Name)
+ .EndMap();
+ consumer->Flush();
+
+ if (Format_ == ELogFormat::Yson) {
+ // In order to obtain proper list fragment, we must manually insert trailing semicolon in each line.
+ countingStream.Write(';');
+ }
+
+ countingStream.Write('\n');
+
+ return countingStream.Counter();
+}
+
+void TStructuredLogFormatter::WriteLogReopenSeparator(IOutputStream* /*outputStream*/) const
+{ }
+
+void TStructuredLogFormatter::WriteLogStartEvent(IOutputStream* outputStream) const
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetStartLogStructuredEvent());
+ }
+}
+
+void TStructuredLogFormatter::WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) const
+{
+ if (EnableSystemMessages_) {
+ WriteFormatted(outputStream, GetSkippedLogStructuredEvent(count, skippedBy));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/formatter.h b/yt/yt/core/logging/formatter.h
new file mode 100644
index 0000000000..6f86116610
--- /dev/null
+++ b/yt/yt/core/logging/formatter.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include "config.h"
+#include "pattern.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCachingDateFormatter
+{
+public:
+ void Format(TBaseFormatter* buffer, TInstant dateTime, bool printMicroseconds = false);
+
+private:
+ ui64 CachedSecond_ = 0;
+ TRawFormatter<DateTimeBufferSize> Cached_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogFormatter
+{
+ virtual ~ILogFormatter() = default;
+
+ virtual i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) const = 0;
+ virtual void WriteLogReopenSeparator(IOutputStream* outputStream) const = 0;
+ virtual void WriteLogStartEvent(IOutputStream* outputStream) const = 0;
+ virtual void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPlainTextLogFormatter
+ : public ILogFormatter
+{
+public:
+ explicit TPlainTextLogFormatter(
+ bool enableControlMessages = true,
+ bool enableSourceLocation = false);
+
+ i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) const override;
+ void WriteLogReopenSeparator(IOutputStream* outputStream) const override;
+ void WriteLogStartEvent(IOutputStream* outputStream) const override;
+ void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) const override;
+
+private:
+ const std::unique_ptr<TRawFormatter<MessageBufferSize>> Buffer_;
+ const std::unique_ptr<TCachingDateFormatter> CachingDateFormatter_;
+ const bool EnableSystemMessages_;
+ const bool EnableSourceLocation_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStructuredLogFormatter
+ : public ILogFormatter
+{
+public:
+ TStructuredLogFormatter(
+ ELogFormat format,
+ THashMap<TString, NYTree::INodePtr> commonFields,
+ bool enableControlMessages = true,
+ NJson::TJsonFormatConfigPtr jsonFormat = nullptr);
+
+ i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) const override;
+ void WriteLogReopenSeparator(IOutputStream* outputStream) const override;
+ void WriteLogStartEvent(IOutputStream* outputStream) const override;
+ void WriteLogSkippedEvent(IOutputStream* outputStream, i64 count, TStringBuf skippedBy) const override;
+
+private:
+ const ELogFormat Format_;
+ const std::unique_ptr<TCachingDateFormatter> CachingDateFormatter_;
+ const THashMap<TString, NYTree::INodePtr> CommonFields_;
+ const bool EnableSystemMessages_;
+ const NJson::TJsonFormatConfigPtr JsonFormat_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log-inl.h b/yt/yt/core/logging/log-inl.h
new file mode 100644
index 0000000000..72a7cffaed
--- /dev/null
+++ b/yt/yt/core/logging/log-inl.h
@@ -0,0 +1,35 @@
+#ifndef LOG_INL_H_
+#error "Direct inclusion of this file is not allowed, include log.h"
+// For the sake of sane code completion.
+#include "log.h"
+#endif
+#undef LOG_INL_H_
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <size_t Length, class... TArgs>
+TLogMessage BuildLogMessage(
+ const TLoggingContext& loggingContext,
+ const TLogger& logger,
+ const TError& error,
+ const char (&format)[Length],
+ TArgs&&... args)
+{
+ TMessageStringBuilder builder;
+ AppendLogMessageWithFormat(&builder, loggingContext, logger, format, std::forward<TArgs>(args)...);
+ builder.AppendChar('\n');
+ FormatValue(&builder, error, TStringBuf());
+ return {builder.Flush(), format};
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log.cpp b/yt/yt/core/logging/log.cpp
new file mode 100644
index 0000000000..e562cbd0ff
--- /dev/null
+++ b/yt/yt/core/logging/log.cpp
@@ -0,0 +1,37 @@
+#include "log.h"
+#include "log_manager.h"
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <library/cpp/yt/misc/thread_name.h>
+
+#include <util/system/thread.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef _win_
+
+TLoggingContext GetLoggingContext()
+{
+ auto* traceContext = NTracing::TryGetCurrentTraceContext();
+
+ return TLoggingContext{
+ .Instant = GetCpuInstant(),
+ .ThreadId = TThread::CurrentThreadId(),
+ .ThreadName = GetCurrentThreadName(),
+ .FiberId = NConcurrency::GetCurrentFiberId(),
+ .TraceId = traceContext ? traceContext->GetTraceId() : TTraceId{},
+ .RequestId = traceContext ? traceContext->GetRequestId() : NTracing::TRequestId(),
+ .TraceLoggingTag = traceContext ? traceContext->GetLoggingTag() : TStringBuf(),
+ };
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log.h b/yt/yt/core/logging/log.h
new file mode 100644
index 0000000000..9b7d519bc0
--- /dev/null
+++ b/yt/yt/core/logging/log.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/logging/logger.h>
+
+#define LOG_INL_H_
+#include "log-inl.h"
+#undef LOG_INL_H_
diff --git a/yt/yt/core/logging/log_manager.cpp b/yt/yt/core/logging/log_manager.cpp
new file mode 100644
index 0000000000..7f9465368d
--- /dev/null
+++ b/yt/yt/core/logging/log_manager.cpp
@@ -0,0 +1,1597 @@
+#include "log_manager.h"
+
+#include "private.h"
+#include "config.h"
+#include "log.h"
+#include "log_writer.h"
+#include "log_writer_factory.h"
+#include "formatter.h"
+#include "file_log_writer.h"
+#include "stream_log_writer.h"
+
+#include <yt/yt/core/concurrency/profiling_helpers.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/scheduler_thread.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/invoker_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+#include <yt/yt/core/misc/fs.h>
+#include <yt/yt/core/misc/spsc_queue.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+#include <yt/yt/core/misc/pattern_formatter.h>
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/property.h>
+#include <yt/yt/core/misc/shutdown.h>
+#include <yt/yt/core/misc/ref_counted_tracker.h>
+#include <yt/yt/core/misc/signal_registry.h>
+#include <yt/yt/core/misc/shutdown.h>
+#include <yt/yt/core/misc/heap.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_service.h>
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/library/profiling/producer.h>
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/misc/hash.h>
+#include <library/cpp/yt/misc/variant.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <util/system/defaults.h>
+#include <util/system/sigset.h>
+#include <util/system/yield.h>
+
+#include <util/generic/algorithm.h>
+
+#include <atomic>
+#include <mutex>
+
+#ifdef _win_
+ #include <io.h>
+#else
+ #include <unistd.h>
+#endif
+
+#ifdef _linux_
+ #include <sys/inotify.h>
+#endif
+
+#include <errno.h>
+
+namespace NYT::NLogging {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NFS;
+using namespace NProfiling;
+using namespace NTracing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TLogger Logger(SystemLoggingCategoryName);
+
+static constexpr auto DiskProfilingPeriod = TDuration::Minutes(5);
+static constexpr auto AnchorProfilingPeriod = TDuration::Seconds(15);
+static constexpr auto DequeuePeriod = TDuration::MilliSeconds(30);
+
+static const TStringBuf StderrSystemWriterName("stderr");
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator == (const TLogWriterCacheKey& lhs, const TLogWriterCacheKey& rhs)
+{
+ return lhs.Category == rhs.Category && lhs.LogLevel == rhs.LogLevel && lhs.Family == rhs.Family;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNotificationHandle
+ : private TNonCopyable
+{
+public:
+ TNotificationHandle()
+ : FD_(-1)
+ {
+#ifdef _linux_
+ FD_ = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
+ YT_VERIFY(FD_ >= 0);
+#endif
+ }
+
+ ~TNotificationHandle()
+ {
+#ifdef _linux_
+ YT_VERIFY(FD_ >= 0);
+ ::close(FD_);
+#endif
+ }
+
+ int Poll()
+ {
+#ifdef _linux_
+ YT_VERIFY(FD_ >= 0);
+
+ char buffer[sizeof(struct inotify_event) + NAME_MAX + 1];
+ ssize_t rv = HandleEintr(::read, FD_, buffer, sizeof(buffer));
+
+ if (rv < 0) {
+ if (errno != EAGAIN) {
+ YT_LOG_ERROR(
+ TError::FromSystem(errno),
+ "Unable to poll inotify() descriptor %v",
+ FD_);
+ }
+ } else if (rv > 0) {
+ YT_ASSERT(rv >= static_cast<ssize_t>(sizeof(struct inotify_event)));
+ struct inotify_event* event = (struct inotify_event*)buffer;
+
+ if (event->mask & IN_ATTRIB) {
+ YT_LOG_TRACE(
+ "Watch %v has triggered metadata change (IN_ATTRIB)",
+ event->wd);
+ }
+ if (event->mask & IN_DELETE_SELF) {
+ YT_LOG_TRACE(
+ "Watch %v has triggered a deletion (IN_DELETE_SELF)",
+ event->wd);
+ }
+ if (event->mask & IN_MOVE_SELF) {
+ YT_LOG_TRACE(
+ "Watch %v has triggered a movement (IN_MOVE_SELF)",
+ event->wd);
+ }
+
+ return event->wd;
+ } else {
+ // Do nothing.
+ }
+#endif
+ return 0;
+ }
+
+ DEFINE_BYVAL_RO_PROPERTY(int, FD);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNotificationWatch
+ : private TNonCopyable
+{
+public:
+ TNotificationWatch(
+ TNotificationHandle* handle,
+ const TString& path,
+ TClosure callback)
+ : FD_(handle->GetFD())
+ , WD_(-1)
+ , Path_(path)
+ , Callback_(std::move(callback))
+
+ {
+ FD_ = handle->GetFD();
+ YT_VERIFY(FD_ >= 0);
+
+ CreateWatch();
+ }
+
+ ~TNotificationWatch()
+ {
+ DropWatch();
+ }
+
+ DEFINE_BYVAL_RO_PROPERTY(int, FD);
+ DEFINE_BYVAL_RO_PROPERTY(int, WD);
+
+ bool IsValid() const
+ {
+ return WD_ >= 0;
+ }
+
+ void Run()
+ {
+ Callback_();
+ // Reinitialize watch to hook to the newly created file.
+ DropWatch();
+ CreateWatch();
+ }
+
+private:
+ void CreateWatch()
+ {
+ YT_VERIFY(WD_ <= 0);
+#ifdef _linux_
+ WD_ = inotify_add_watch(
+ FD_,
+ Path_.c_str(),
+ IN_ATTRIB | IN_DELETE_SELF | IN_MOVE_SELF);
+
+ if (WD_ < 0) {
+ YT_LOG_ERROR(TError::FromSystem(errno), "Error registering watch for %v",
+ Path_);
+ WD_ = -1;
+ } else if (WD_ > 0) {
+ YT_LOG_TRACE("Registered watch %v for %v",
+ WD_,
+ Path_);
+ } else {
+ YT_ABORT();
+ }
+#else
+ WD_ = -1;
+#endif
+ }
+
+ void DropWatch()
+ {
+#ifdef _linux_
+ if (WD_ > 0) {
+ YT_LOG_TRACE("Unregistering watch %v for %v",
+ WD_,
+ Path_);
+ inotify_rm_watch(FD_, WD_);
+ }
+#endif
+ WD_ = -1;
+ }
+
+private:
+ TString Path_;
+ TClosure Callback_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TElement>
+class TExpiringSet
+{
+public:
+ TExpiringSet()
+ {
+ Reconfigure(TDuration::Zero());
+ }
+
+ explicit TExpiringSet(TDuration lifetime)
+ {
+ Reconfigure(lifetime);
+ }
+
+ void Update(std::vector<TElement> elements)
+ {
+ RemoveExpired();
+ Insert(std::move(elements));
+ }
+
+ bool Contains(const TElement& element)
+ {
+ return Set_.contains(element);
+ }
+
+ void Reconfigure(TDuration lifetime)
+ {
+ Lifetime_ = DurationToCpuDuration(lifetime);
+ }
+
+ void Clear()
+ {
+ Set_.clear();
+ ExpirationQueue_ = std::priority_queue<TPack>();
+ }
+
+private:
+ struct TPack
+ {
+ std::vector<TElement> Elements;
+ TCpuInstant ExpirationTime;
+
+ bool operator<(const TPack& other) const
+ {
+ // Reversed ordering for the priority queue.
+ return ExpirationTime > other.ExpirationTime;
+ }
+ };
+
+ TCpuDuration Lifetime_;
+ THashSet<TElement> Set_;
+ std::priority_queue<TPack> ExpirationQueue_;
+
+
+ void Insert(std::vector<TElement> elements)
+ {
+ for (const auto& element : elements) {
+ Set_.insert(element);
+ }
+
+ ExpirationQueue_.push(TPack{std::move(elements), GetCpuInstant() + Lifetime_});
+ }
+
+ void RemoveExpired()
+ {
+ auto now = GetCpuInstant();
+ while (!ExpirationQueue_.empty() && ExpirationQueue_.top().ExpirationTime < now) {
+ for (const auto& element : ExpirationQueue_.top().Elements) {
+ Set_.erase(element);
+ }
+
+ ExpirationQueue_.pop();
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConfigEvent
+{
+ TCpuInstant Instant = 0;
+ TLogManagerConfigPtr Config;
+ bool FromEnv;
+ TPromise<void> Promise = NewPromise<void>();
+};
+
+using TLoggerQueueItem = std::variant<
+ TLogEvent,
+ TConfigEvent
+>;
+
+TCpuInstant GetEventInstant(const TLoggerQueueItem& item)
+{
+ return Visit(item,
+ [&] (const TConfigEvent& event) {
+ return event.Instant;
+ },
+ [&] (const TLogEvent& event) {
+ return event.Instant;
+ });
+}
+
+using TThreadLocalQueue = TSpscQueue<TLoggerQueueItem>;
+
+static constexpr uintptr_t ThreadQueueDestroyedSentinel = -1;
+static thread_local TThreadLocalQueue* PerThreadQueue;
+
+/////////////////////////////////////////////////////////////////////////////
+
+class TLogManager::TImpl
+ : public ISensorProducer
+ , public ILogWriterHost
+{
+public:
+ friend struct TLocalQueueReclaimer;
+
+ TImpl()
+ : EventQueue_(New<TMpscInvokerQueue>(
+ EventCount_,
+ NConcurrency::GetThreadTags("Profiling")))
+ , LoggingThread_(New<TThread>(this))
+ , SystemWriters_({
+ CreateStderrLogWriter(
+ std::make_unique<TPlainTextLogFormatter>(),
+ TString(StderrSystemWriterName))
+ })
+ , DiskProfilingExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::OnDiskProfiling, MakeWeak(this)),
+ DiskProfilingPeriod))
+ , AnchorProfilingExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::OnAnchorProfiling, MakeWeak(this)),
+ AnchorProfilingPeriod))
+ , DequeueExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::OnDequeue, MakeStrong(this)),
+ DequeuePeriod))
+ , FlushExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::FlushWriters, MakeStrong(this)),
+ std::nullopt))
+ , WatchExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::WatchWriters, MakeStrong(this)),
+ std::nullopt))
+ , CheckSpaceExecutor_(New<TPeriodicExecutor>(
+ EventQueue_,
+ BIND(&TImpl::CheckSpace, MakeStrong(this)),
+ std::nullopt))
+ , CompressionThreadPool_(CreateThreadPool(
+ /*threadCount*/ 1,
+ /*threadNamePrefix*/ "LogCompress"))
+ {
+ RegisterWriterFactory(TString(TFileLogWriterConfig::Type), GetFileLogWriterFactory());
+ RegisterWriterFactory(TString(TStderrLogWriterConfig::Type), GetStderrLogWriterFactory());
+ }
+
+ void Initialize()
+ {
+ std::call_once(Initialized_, [&] {
+ // NB: Cannot place this logic inside ctor since it may boot up Compression threads unexpected
+ // and these will try to access TLogManager instance causing a deadlock.
+ try {
+ if (auto config = TLogManagerConfig::TryCreateFromEnv()) {
+ DoUpdateConfig(config, /*fromEnv*/ true);
+ }
+ } catch (const std::exception& ex) {
+ fprintf(stderr, "Error configuring logging from environment variables\n%s\n",
+ ex.what());
+ }
+
+ if (!IsConfiguredFromEnv()) {
+ DoUpdateConfig(TLogManagerConfig::CreateDefault(), /*fromEnv*/ false);
+ }
+
+ SystemCategory_ = GetCategory(SystemLoggingCategoryName);
+ });
+ }
+
+ void Configure(INodePtr node)
+ {
+ Configure(
+ TLogManagerConfig::CreateFromNode(node),
+ /*fromEnv*/ false,
+ /*sync*/ true);
+ }
+
+ void Configure(TLogManagerConfigPtr config, bool fromEnv, bool sync)
+ {
+ if (LoggingThread_->IsStopping()) {
+ return;
+ }
+
+ EnsureStarted();
+
+ TConfigEvent event{
+ .Instant = GetCpuInstant(),
+ .Config = std::move(config),
+ .FromEnv = fromEnv
+ };
+
+ auto future = event.Promise.ToFuture();
+
+ PushEvent(std::move(event));
+
+ DequeueExecutor_->ScheduleOutOfBand();
+
+ if (sync) {
+ future.Get().ThrowOnError();
+ }
+ }
+
+ void ConfigureFromEnv()
+ {
+ if (auto config = TLogManagerConfig::TryCreateFromEnv()) {
+ Configure(
+ std::move(config),
+ /*fromEnv*/ true,
+ /*sync*/ true);
+ }
+ }
+
+ bool IsConfiguredFromEnv()
+ {
+ return ConfiguredFromEnv_.load();
+ }
+
+ void Shutdown()
+ {
+ ShutdownRequested_.store(true);
+
+ if (LoggingThread_->GetThreadId() == GetCurrentThreadId()) {
+ FlushWriters();
+ } else {
+ // Wait for all previously enqueued messages to be flushed
+ // but no more than ShutdownGraceTimeout to prevent hanging.
+ Synchronize(TInstant::Now() + Config_->ShutdownGraceTimeout);
+ }
+
+ EventQueue_->Shutdown();
+ LoggingThread_->Stop();
+ }
+
+ /*!
+ * In some cases (when configuration is being updated at the same time),
+ * the actual version is greater than the version returned by this method.
+ */
+ int GetVersion() const
+ {
+ return Version_.load();
+ }
+
+ bool GetAbortOnAlert() const
+ {
+ return AbortOnAlert_.load();
+ }
+
+ const TLoggingCategory* GetCategory(TStringBuf categoryName)
+ {
+ if (!categoryName) {
+ return nullptr;
+ }
+
+ auto guard = Guard(SpinLock_);
+ auto it = NameToCategory_.find(categoryName);
+ if (it == NameToCategory_.end()) {
+ auto category = std::make_unique<TLoggingCategory>();
+ category->Name = categoryName;
+ category->ActualVersion = &Version_;
+ it = NameToCategory_.emplace(categoryName, std::move(category)).first;
+ DoUpdateCategory(it->second.get());
+ }
+ return it->second.get();
+ }
+
+ void UpdateCategory(TLoggingCategory* category)
+ {
+ auto guard = Guard(SpinLock_);
+ DoUpdateCategory(category);
+ }
+
+ void UpdateAnchor(TLoggingAnchor* anchor)
+ {
+ auto guard = Guard(SpinLock_);
+ bool enabled = true;
+ for (const auto& prefix : Config_->SuppressedMessages) {
+ if (anchor->AnchorMessage.StartsWith(prefix)) {
+ enabled = false;
+ break;
+ }
+ }
+
+ anchor->Enabled.store(enabled, std::memory_order::relaxed);
+ anchor->CurrentVersion.store(GetVersion(), std::memory_order::relaxed);
+ }
+
+ void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation sourceLocation, TStringBuf message)
+ {
+ if (anchor->Registered.exchange(true)) {
+ return;
+ }
+
+ auto guard = Guard(SpinLock_);
+ anchor->SourceLocation = sourceLocation;
+ anchor->AnchorMessage = BuildAnchorMessage(sourceLocation, message);
+ DoRegisterAnchor(anchor);
+ }
+
+ TLoggingAnchor* RegisterDynamicAnchor(TString anchorMessage)
+ {
+ auto guard = Guard(SpinLock_);
+ if (auto it = AnchorMap_.find(anchorMessage)) {
+ return it->second;
+ }
+ auto anchor = std::make_unique<TLoggingAnchor>();
+ anchor->Registered = true;
+ anchor->AnchorMessage = std::move(anchorMessage);
+ auto* rawAnchor = anchor.get();
+ DynamicAnchors_.push_back(std::move(anchor));
+ DoRegisterAnchor(rawAnchor);
+ return rawAnchor;
+ }
+
+ void RegisterWriterFactory(const TString& typeName, const ILogWriterFactoryPtr& factory)
+ {
+ auto guard = Guard(SpinLock_);
+ EmplaceOrCrash(TypeNameToWriterFactory_, typeName, factory);
+ }
+
+ void UnregisterWriterFactory(const TString& typeName)
+ {
+ auto guard = Guard(SpinLock_);
+ EraseOrCrash(TypeNameToWriterFactory_, typeName);
+ }
+
+ void Enqueue(TLogEvent&& event)
+ {
+ if (event.Level == ELogLevel::Fatal) {
+ bool shutdown = false;
+ if (!ShutdownRequested_.compare_exchange_strong(shutdown, true)) {
+ // Fatal events should not get out of this call.
+ Sleep(TDuration::Max());
+ }
+
+ // Collect last-minute information.
+ TRawFormatter<1024> formatter;
+ formatter.AppendString("\n*** Fatal error ***\n");
+ formatter.AppendString(event.MessageRef.ToStringBuf());
+ formatter.AppendString("\n*** Aborting ***\n");
+
+ HandleEintr(::write, 2, formatter.GetData(), formatter.GetBytesWritten());
+
+ // Add fatal message to log and notify event log queue.
+ PushEvent(std::move(event));
+
+ // Flush everything and die.
+ Shutdown();
+
+ std::terminate();
+ }
+
+ if (ShutdownRequested_) {
+ ++DroppedEvents_;
+ return;
+ }
+
+ if (LoggingThread_->IsStopping()) {
+ ++DroppedEvents_;
+ return;
+ }
+
+ EnsureStarted();
+
+ // Order matters here; inherent race may lead to negative backlog and integer overflow.
+ ui64 writtenEvents = WrittenEvents_.load();
+ ui64 enqueuedEvents = EnqueuedEvents_.load();
+ ui64 backlogEvents = enqueuedEvents - writtenEvents;
+
+ // NB: This is somewhat racy but should work fine as long as more messages keep coming.
+ auto lowBacklogWatermark = LowBacklogWatermark_.load(std::memory_order::relaxed);
+ auto highBacklogWatermark = HighBacklogWatermark_.load(std::memory_order::relaxed);
+ if (Suspended_.load(std::memory_order::relaxed)) {
+ if (backlogEvents < lowBacklogWatermark) {
+ Suspended_.store(false, std::memory_order::relaxed);
+ YT_LOG_INFO("Backlog size has dropped below low watermark, logging resumed (LowBacklogWatermark: %v)",
+ lowBacklogWatermark);
+ }
+ } else {
+ if (backlogEvents >= lowBacklogWatermark && !ScheduledOutOfBand_.exchange(true)) {
+ DequeueExecutor_->ScheduleOutOfBand();
+ }
+
+ if (backlogEvents >= highBacklogWatermark) {
+ Suspended_.store(true, std::memory_order::relaxed);
+ YT_LOG_WARNING("Backlog size has exceeded high watermark, logging suspended (HighBacklogWatermark: %v)",
+ highBacklogWatermark);
+ }
+ }
+
+ // NB: Always allow system messages to pass through.
+ if (Suspended_ && event.Category != SystemCategory_ && !event.Essential) {
+ ++DroppedEvents_;
+ return;
+ }
+
+ PushEvent(std::move(event));
+ }
+
+ void Reopen()
+ {
+ ReopenRequested_.store(true);
+ }
+
+ void EnableReopenOnSighup()
+ {
+#ifdef _unix_
+ TSignalRegistry::Get()->PushCallback(
+ SIGHUP,
+ [this] { Reopen(); });
+#endif
+ }
+
+ void SuppressRequest(TRequestId requestId)
+ {
+ if (!RequestSuppressionEnabled_) {
+ return;
+ }
+
+ SuppressedRequestIdQueue_.Enqueue(requestId);
+ }
+
+ void Synchronize(TInstant deadline = TInstant::Max())
+ {
+ auto enqueuedEvents = EnqueuedEvents_.load();
+ while (enqueuedEvents > FlushedEvents_.load() && TInstant::Now() < deadline) {
+ SchedYield();
+ }
+ }
+
+ // ILogWriterHost implementation
+ IInvokerPtr GetCompressionInvoker() override
+ {
+ return CompressionThreadPool_->GetInvoker();
+ }
+
+private:
+ class TThread
+ : public TSchedulerThread
+ {
+ public:
+ explicit TThread(TImpl* owner)
+ : TSchedulerThread(
+ owner->EventCount_,
+ "Logging",
+ "Logging")
+ , Owner_(owner)
+ { }
+
+ private:
+ TImpl* const Owner_;
+
+ TClosure BeginExecute() override
+ {
+ return Owner_->BeginExecute();
+ }
+
+ void EndExecute() override
+ {
+ Owner_->EndExecute();
+ }
+ };
+
+ TClosure BeginExecute()
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ return EventQueue_->BeginExecute(&CurrentAction_);
+ }
+
+ void EndExecute()
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ EventQueue_->EndExecute(&CurrentAction_);
+ }
+
+ void EnsureStarted()
+ {
+ std::call_once(Started_, [&] {
+ if (LoggingThread_->IsStopping()) {
+ return;
+ }
+
+ LoggingThread_->Start();
+ EventQueue_->SetThreadId(LoggingThread_->GetThreadId());
+ DiskProfilingExecutor_->Start();
+ AnchorProfilingExecutor_->Start();
+ DequeueExecutor_->Start();
+ FlushExecutor_->Start();
+ WatchExecutor_->Start();
+ CheckSpaceExecutor_->Start();
+ });
+ }
+
+ const std::vector<ILogWriterPtr>& GetWriters(const TLogEvent& event)
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ if (event.Category == SystemCategory_) {
+ return SystemWriters_;
+ }
+
+ TLogWriterCacheKey cacheKey{event.Category->Name, event.Level, event.Family};
+ auto it = KeyToCachedWriter_.find(cacheKey);
+ if (it != KeyToCachedWriter_.end()) {
+ return it->second;
+ }
+
+ THashSet<TString> writerNames;
+ for (const auto& rule : Config_->Rules) {
+ if (rule->IsApplicable(event.Category->Name, event.Level, event.Family)) {
+ writerNames.insert(rule->Writers.begin(), rule->Writers.end());
+ }
+ }
+
+ std::vector<ILogWriterPtr> writers;
+ for (const auto& name : writerNames) {
+ writers.push_back(GetOrCrash(NameToWriter_, name));
+ }
+
+ return EmplaceOrCrash(KeyToCachedWriter_, cacheKey, writers)->second;
+ }
+
+ std::unique_ptr<TNotificationWatch> CreateNotificationWatch(
+ const TLogManagerConfigPtr& config,
+ const IFileLogWriterPtr& writer)
+ {
+#ifdef _linux_
+ if (config->WatchPeriod) {
+ if (!NotificationHandle_) {
+ NotificationHandle_ = std::make_unique<TNotificationHandle>();
+ }
+ return std::unique_ptr<TNotificationWatch>(
+ new TNotificationWatch(
+ NotificationHandle_.get(),
+ writer->GetFileName().c_str(),
+ BIND(&ILogWriter::Reload, writer)));
+ }
+#else
+ Y_UNUSED(config, writer);
+#endif
+ return nullptr;
+ }
+
+ void UpdateConfig(const TConfigEvent& event)
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ if (ShutdownRequested_) {
+ return;
+ }
+
+ if (LoggingThread_->IsStopping()) {
+ return;
+ }
+
+ AbortOnAlert_.store(event.Config->AbortOnAlert);
+
+ EnsureStarted();
+
+ FlushWriters();
+
+ try {
+ DoUpdateConfig(event.Config, event.FromEnv);
+ event.Promise.Set();
+ } catch (const std::exception& ex) {
+ event.Promise.Set(ex);
+ }
+ }
+
+ std::unique_ptr<ILogFormatter> CreateFormatter(const TLogWriterConfigPtr& writerConfig)
+ {
+ switch (writerConfig->Format) {
+ case ELogFormat::PlainText:
+ return std::make_unique<TPlainTextLogFormatter>(
+ writerConfig->AreSystemMessagesEnabled(),
+ writerConfig->EnableSourceLocation);
+
+ case ELogFormat::Json: [[fallthrough]];
+ case ELogFormat::Yson:
+ return std::make_unique<TStructuredLogFormatter>(
+ writerConfig->Format,
+ writerConfig->CommonFields,
+ writerConfig->AreSystemMessagesEnabled(),
+ writerConfig->JsonFormat);
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void DoUpdateConfig(const TLogManagerConfigPtr& config, bool fromEnv)
+ {
+ if (AreNodesEqual(ConvertToNode(Config_), ConvertToNode(config))) {
+ return;
+ }
+
+ THashMap<TString, ILogWriterFactoryPtr> typeNameToWriterFactory;
+ {
+ auto guard = Guard(SpinLock_);
+ for (const auto& [name, writerConfig] : config->Writers) {
+ auto typedWriterConfig = ConvertTo<TLogWriterConfigPtr>(writerConfig);
+ if (typeNameToWriterFactory.contains(typedWriterConfig->Type)) {
+ continue;
+ }
+ auto it = TypeNameToWriterFactory_.find(typedWriterConfig->Type);
+ if (it == TypeNameToWriterFactory_.end()) {
+ THROW_ERROR_EXCEPTION("Unknown log writer type %Qv", typedWriterConfig->Type);
+ }
+ const auto& writerFactory = it->second;
+ writerFactory->ValidateConfig(writerConfig);
+ EmplaceOrCrash(typeNameToWriterFactory, typedWriterConfig->Type, writerFactory);
+ }
+ }
+
+ NameToWriter_.clear();
+ KeyToCachedWriter_.clear();
+ WDToNotificationWatch_.clear();
+ NotificationWatches_.clear();
+
+ for (const auto& [name, writerConfig] : config->Writers) {
+ auto typedWriterConfig = ConvertTo<TLogWriterConfigPtr>(writerConfig);
+ auto formatter = CreateFormatter(typedWriterConfig);
+ auto writerFactory = GetOrCrash(typeNameToWriterFactory, typedWriterConfig->Type);
+ auto writer = writerFactory->CreateWriter(
+ std::move(formatter),
+ name,
+ writerConfig,
+ this);
+
+ writer->SetRateLimit(typedWriterConfig->RateLimit);
+ writer->SetCategoryRateLimits(config->CategoryRateLimits);
+
+ EmplaceOrCrash(NameToWriter_, name, writer);
+
+ if (auto fileWriter = DynamicPointerCast<IFileLogWriter>(writer)) {
+ auto watch = CreateNotificationWatch(config, fileWriter);
+ if (watch && watch->IsValid()) {
+ // Watch can fail to initialize if the writer is disabled
+ // e.g. due to the lack of space.
+ EmplaceOrCrash(WDToNotificationWatch_, watch->GetWD(), watch.get());
+ }
+ NotificationWatches_.push_back(std::move(watch));
+ }
+ }
+ for (const auto& [_, category] : NameToCategory_) {
+ category->StructuredValidationSamplingRate.store(config->StructuredValidationSamplingRate, std::memory_order::relaxed);
+ }
+
+ Config_ = config;
+ ConfiguredFromEnv_.store(fromEnv);
+ HighBacklogWatermark_.store(Config_->HighBacklogWatermark);
+ LowBacklogWatermark_.store(Config_->LowBacklogWatermark);
+ RequestSuppressionEnabled_.store(Config_->RequestSuppressionTimeout != TDuration::Zero());
+
+ CompressionThreadPool_->Configure(Config_->CompressionThreadCount);
+
+ if (RequestSuppressionEnabled_) {
+ SuppressedRequestIdSet_.Reconfigure((Config_->RequestSuppressionTimeout + DequeuePeriod) * 2);
+ } else {
+ SuppressedRequestIdSet_.Clear();
+ SuppressedRequestIdQueue_.DequeueAll();
+ }
+
+ FlushExecutor_->SetPeriod(Config_->FlushPeriod);
+ WatchExecutor_->SetPeriod(Config_->WatchPeriod);
+ CheckSpaceExecutor_->SetPeriod(Config_->CheckSpacePeriod);
+
+ Version_++;
+ }
+
+ void WriteEvent(const TLogEvent& event)
+ {
+ if (ReopenRequested_.exchange(false)) {
+ ReloadWriters();
+ }
+
+ GetWrittenEventsCounter(event).Increment();
+
+ for (const auto& writer : GetWriters(event)) {
+ writer->Write(event);
+ }
+ }
+
+ void FlushWriters()
+ {
+ for (const auto& [name, writer] : NameToWriter_) {
+ writer->Flush();
+ }
+ FlushedEvents_ = WrittenEvents_.load();
+ }
+
+ void ReloadWriters()
+ {
+ Version_++;
+ for (const auto& [name, writer] : NameToWriter_) {
+ writer->Reload();
+ }
+ }
+
+ void CheckSpace()
+ {
+ for (const auto& [name, writer] : NameToWriter_) {
+ if (auto fileWriter = DynamicPointerCast<IFileLogWriter>(writer)) {
+ fileWriter->CheckSpace(Config_->MinDiskSpace);
+ }
+ }
+ }
+
+ void WatchWriters()
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ if (!NotificationHandle_) {
+ return;
+ }
+
+ int previousWD = -1, currentWD = -1;
+ while ((currentWD = NotificationHandle_->Poll()) > 0) {
+ if (currentWD == previousWD) {
+ continue;
+ }
+ auto it = WDToNotificationWatch_.find(currentWD);
+ auto jt = WDToNotificationWatch_.end();
+ if (it == jt) {
+ continue;
+ }
+
+ auto* watch = it->second;
+ watch->Run();
+
+ if (watch->GetWD() != currentWD) {
+ WDToNotificationWatch_.erase(it);
+ if (watch->GetWD() >= 0) {
+ // Watch can fail to initialize if the writer is disabled
+ // e.g. due to the lack of space.
+ EmplaceOrCrash(WDToNotificationWatch_, watch->GetWD(), watch);
+ }
+ }
+
+ previousWD = currentWD;
+ }
+ }
+
+ void PushEvent(TLoggerQueueItem&& event)
+ {
+ if (!PerThreadQueue) {
+ PerThreadQueue = new TThreadLocalQueue();
+ RegisteredLocalQueues_.Enqueue(PerThreadQueue);
+ }
+
+ ++EnqueuedEvents_;
+ if (PerThreadQueue == reinterpret_cast<TThreadLocalQueue*>(ThreadQueueDestroyedSentinel)) {
+ GlobalQueue_.Enqueue(std::move(event));
+ } else {
+ PerThreadQueue->Push(std::move(event));
+ }
+ }
+
+ const TCounter& GetWrittenEventsCounter(const TLogEvent& event)
+ {
+ auto key = std::make_pair(event.Category->Name, event.Level);
+ auto it = WrittenEventsCounters_.find(key);
+
+ if (it == WrittenEventsCounters_.end()) {
+ // TODO(prime@): optimize sensor count
+ auto counter = Profiler
+ .WithSparse()
+ .WithTag("category", TString{event.Category->Name})
+ .WithTag("level", FormatEnum(event.Level))
+ .Counter("/written_events");
+
+ it = WrittenEventsCounters_.emplace(key, counter).first;
+ }
+ return it->second;
+ }
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ auto writtenEvents = WrittenEvents_.load();
+ auto enqueuedEvents = EnqueuedEvents_.load();
+ auto suppressedEvents = SuppressedEvents_.load();
+ auto droppedEvents = DroppedEvents_.load();
+ auto messageBuffersSize = TRefCountedTracker::Get()->GetBytesAlive(GetRefCountedTypeKey<NDetail::TMessageBufferTag>());
+
+ writer->AddCounter("/enqueued_events", enqueuedEvents);
+ writer->AddGauge("/backlog_events", enqueuedEvents - writtenEvents);
+ writer->AddCounter("/dropped_events", droppedEvents);
+ writer->AddCounter("/suppressed_events", suppressedEvents);
+ writer->AddGauge("/message_buffers_size", messageBuffersSize);
+ }
+
+ void OnDiskProfiling()
+ {
+ try {
+ auto minLogStorageAvailableSpace = std::numeric_limits<i64>::max();
+ auto minLogStorageFreeSpace = std::numeric_limits<i64>::max();
+
+ for (const auto& [name, writer] : NameToWriter_) {
+ if (auto fileWriter = DynamicPointerCast<IFileLogWriter>(writer)) {
+ auto logStorageDiskSpaceStatistics = GetDiskSpaceStatistics(GetDirectoryName(fileWriter->GetFileName()));
+ minLogStorageAvailableSpace = std::min<i64>(minLogStorageAvailableSpace, logStorageDiskSpaceStatistics.AvailableSpace);
+ minLogStorageFreeSpace = std::min<i64>(minLogStorageFreeSpace, logStorageDiskSpaceStatistics.FreeSpace);
+ }
+ }
+
+ if (minLogStorageAvailableSpace != std::numeric_limits<i64>::max()) {
+ MinLogStorageAvailableSpace_.Update(minLogStorageAvailableSpace);
+ }
+ if (minLogStorageFreeSpace != std::numeric_limits<i64>::max()) {
+ MinLogStorageFreeSpace_.Update(minLogStorageFreeSpace);
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_WARNING(ex, "Failed to get log storage disk statistics");
+ }
+ }
+
+ struct TLoggingAnchorStat
+ {
+ TLoggingAnchor* Anchor;
+ double MessageRate;
+ double ByteRate;
+ };
+
+ std::vector<TLoggingAnchorStat> CaptureAnchorStats()
+ {
+ auto now = TInstant::Now();
+ auto deltaSeconds = (now - LastAnchorStatsCaptureTime_).SecondsFloat();
+ LastAnchorStatsCaptureTime_ = now;
+
+ std::vector<TLoggingAnchorStat> result;
+ auto* currentAnchor = FirstAnchor_.load();
+ while (currentAnchor) {
+ auto getRate = [&] (auto& counter) {
+ auto current = counter.Current.load(std::memory_order::relaxed);
+ auto rate = (current - counter.Previous) / deltaSeconds;
+ counter.Previous = current;
+ return rate;
+ };
+
+ auto messageRate = getRate(currentAnchor->MessageCounter);
+ auto byteRate = getRate(currentAnchor->ByteCounter);
+ result.push_back({
+ currentAnchor,
+ messageRate,
+ byteRate
+ });
+
+ currentAnchor = currentAnchor->NextAnchor;
+ }
+ return result;
+ }
+
+ void OnAnchorProfiling()
+ {
+ if (Config_->EnableAnchorProfiling && !AnchorBufferedProducer_) {
+ AnchorBufferedProducer_ = New<TBufferedProducer>();
+ Profiler
+ .WithSparse()
+ .WithDefaultDisabled()
+ .WithProducerRemoveSupport()
+ .AddProducer("/anchors", AnchorBufferedProducer_);
+ } else if (!Config_->EnableAnchorProfiling && AnchorBufferedProducer_) {
+ AnchorBufferedProducer_.Reset();
+ }
+
+ if (!AnchorBufferedProducer_) {
+ return;
+ }
+
+ auto stats = CaptureAnchorStats();
+
+ TSensorBuffer sensorBuffer;
+ for (const auto& stat : stats) {
+ if (stat.MessageRate < Config_->MinLoggedMessageRateToProfile) {
+ continue;
+ }
+ TWithTagGuard tagGuard(&sensorBuffer, "message", stat.Anchor->AnchorMessage);
+ sensorBuffer.AddGauge("/logged_messages/rate", stat.MessageRate);
+ sensorBuffer.AddGauge("/logged_bytes/rate", stat.ByteRate);
+ }
+
+ AnchorBufferedProducer_->Update(std::move(sensorBuffer));
+ }
+
+ void OnDequeue()
+ {
+ VERIFY_THREAD_AFFINITY(LoggingThread);
+
+ ScheduledOutOfBand_.store(false);
+
+ auto currentInstant = GetCpuInstant();
+
+ RegisteredLocalQueues_.DequeueAll(true, [&] (TThreadLocalQueue* item) {
+ InsertOrCrash(LocalQueues_, item);
+ });
+
+ struct THeapItem
+ {
+ TThreadLocalQueue* Queue;
+
+ explicit THeapItem(TThreadLocalQueue* queue)
+ : Queue(queue)
+ { }
+
+ TLoggerQueueItem* Front() const
+ {
+ return Queue->Front();
+ }
+
+ void Pop()
+ {
+ Queue->Pop();
+ }
+
+ TCpuInstant GetInstant() const
+ {
+ auto* front = Front();
+ if (Y_LIKELY(front)) {
+ return GetEventInstant(*front);
+ } else {
+ return std::numeric_limits<TCpuInstant>::max();
+ }
+ }
+
+ bool operator < (const THeapItem& other) const
+ {
+ return GetInstant() < other.GetInstant();
+ }
+ };
+
+ std::vector<THeapItem> heap;
+ for (auto* localQueue : LocalQueues_) {
+ if (localQueue->Front()) {
+ heap.emplace_back(localQueue);
+ }
+ }
+
+ if (!heap.empty()) {
+ // NB: Messages are not totally ordered because of race around high/low watermark check.
+
+ MakeHeap(heap.begin(), heap.end());
+ ExtractHeap(heap.begin(), heap.end());
+ THeapItem topItem = heap.back();
+ heap.pop_back();
+
+ while (!heap.empty()) {
+ // Increment front instant by one to avoid live lock when there are two queueus
+ // with equal front instants.
+ auto nextInstant = heap.front().GetInstant() < currentInstant
+ ? heap.front().GetInstant() + 1
+ : currentInstant;
+
+ // TODO(lukyan): Use exponential search to determine last element.
+ // Use batch extraction from queue.
+ while (topItem.GetInstant() < nextInstant) {
+ TimeOrderedBuffer_.emplace_back(std::move(*topItem.Front()));
+ topItem.Pop();
+ }
+
+ std::swap(topItem, heap.front());
+
+ if (heap.front().GetInstant() < currentInstant) {
+ AdjustHeapFront(heap.begin(), heap.end());
+ } else {
+ ExtractHeap(heap.begin(), heap.end());
+ heap.pop_back();
+ }
+ }
+
+ while (topItem.GetInstant() < currentInstant) {
+ TimeOrderedBuffer_.emplace_back(std::move(*topItem.Front()));
+ topItem.Pop();
+ }
+ }
+
+ UnregisteredLocalQueues_.DequeueAll(true, [&] (TThreadLocalQueue* item) {
+ if (item->IsEmpty()) {
+ EraseOrCrash(LocalQueues_, item);
+ delete item;
+ } else {
+ UnregisteredLocalQueues_.Enqueue(item);
+ }
+ });
+
+ // TODO(lukyan): To achieve total order of messages copy them from GlobalQueue to
+ // separate TThreadLocalQueue sort it and merge it with LocalQueues
+ // TODO(lukyan): Reuse nextEvents
+ // NB: Messages from global queue are not sorted
+ std::vector<TLoggerQueueItem> nextEvents;
+ while (GlobalQueue_.DequeueAll(true, [&] (TLoggerQueueItem& event) {
+ if (GetEventInstant(event) < currentInstant) {
+ TimeOrderedBuffer_.emplace_back(std::move(event));
+ } else {
+ nextEvents.push_back(std::move(event));
+ }
+ }))
+ { }
+
+ for (auto& event : nextEvents) {
+ GlobalQueue_.Enqueue(std::move(event));
+ }
+
+ auto eventsWritten = ProcessTimeOrderedBuffer();
+
+ if (eventsWritten == 0) {
+ return;
+ }
+
+ WrittenEvents_ += eventsWritten;
+
+ if (!Config_->FlushPeriod || ShutdownRequested_) {
+ FlushWriters();
+ }
+ }
+
+ int ProcessTimeOrderedBuffer()
+ {
+ int eventsWritten = 0;
+ int eventsSuppressed = 0;
+
+ SuppressedRequestIdSet_.Update(SuppressedRequestIdQueue_.DequeueAll());
+
+ auto requestSuppressionEnabled = RequestSuppressionEnabled_.load(std::memory_order::relaxed);
+ auto deadline = GetCpuInstant() - DurationToCpuDuration(Config_->RequestSuppressionTimeout);
+
+ while (!TimeOrderedBuffer_.empty()) {
+ const auto& event = TimeOrderedBuffer_.front();
+
+ if (requestSuppressionEnabled && GetEventInstant(event) > deadline) {
+ break;
+ }
+
+ ++eventsWritten;
+
+ Visit(event,
+ [&] (const TConfigEvent& event) {
+ return UpdateConfig(event);
+ },
+ [&] (const TLogEvent& event) {
+ if (requestSuppressionEnabled && event.RequestId && SuppressedRequestIdSet_.Contains(event.RequestId)) {
+ ++eventsSuppressed;
+ } else {
+ WriteEvent(event);
+ }
+ });
+
+ TimeOrderedBuffer_.pop_front();
+ }
+
+ SuppressedEvents_ += eventsSuppressed;
+
+ return eventsWritten;
+ }
+
+ void DoUpdateCategory(TLoggingCategory* category)
+ {
+ auto minPlainTextLevel = ELogLevel::Maximum;
+ for (const auto& rule : Config_->Rules) {
+ if (rule->IsApplicable(category->Name, ELogFamily::PlainText)) {
+ minPlainTextLevel = std::min(minPlainTextLevel, rule->MinLevel);
+ }
+ }
+
+ category->MinPlainTextLevel.store(minPlainTextLevel, std::memory_order::relaxed);
+ category->CurrentVersion.store(GetVersion(), std::memory_order::relaxed);
+ category->StructuredValidationSamplingRate.store(Config_->StructuredValidationSamplingRate, std::memory_order::relaxed);
+ }
+
+ void DoRegisterAnchor(TLoggingAnchor* anchor)
+ {
+ // NB: Duplicates are not desirable but possible.
+ AnchorMap_.emplace(anchor->AnchorMessage, anchor);
+ anchor->NextAnchor = FirstAnchor_;
+ FirstAnchor_.store(anchor);
+ }
+
+ static TString BuildAnchorMessage(::TSourceLocation sourceLocation, TStringBuf message)
+ {
+ if (message) {
+ auto index = message.find_first_of('(');
+ return Strip(TString(message.substr(0, index)));
+ } else {
+ return Format("%v:%v",
+ sourceLocation.File,
+ sourceLocation.Line);
+ }
+ }
+
+private:
+ const TIntrusivePtr<NThreading::TEventCount> EventCount_ = New<NThreading::TEventCount>();
+ const TMpscInvokerQueuePtr EventQueue_;
+ const TIntrusivePtr<TThread> LoggingThread_;
+ const TShutdownCookie ShutdownCookie_ = RegisterShutdownCallback(
+ "LogManager",
+ BIND_NO_PROPAGATE(&TImpl::Shutdown, MakeWeak(this)),
+ /*priority*/ 200);
+
+ DECLARE_THREAD_AFFINITY_SLOT(LoggingThread);
+
+ TEnqueuedAction CurrentAction_;
+
+ // Configuration.
+ NThreading::TForkAwareSpinLock SpinLock_;
+ // Version forces this very module's Logger object to update to our own
+ // default configuration (default level etc.).
+ std::atomic<int> Version_ = 0;
+ std::atomic<bool> AbortOnAlert_ = false;
+ TLogManagerConfigPtr Config_;
+ std::atomic<bool> ConfiguredFromEnv_ = false;
+ THashMap<TString, std::unique_ptr<TLoggingCategory>> NameToCategory_;
+ THashMap<TString, ILogWriterFactoryPtr> TypeNameToWriterFactory_;
+ const TLoggingCategory* SystemCategory_;
+ // These are just copies from Config_.
+ // The values are being read from arbitrary threads but stale values are fine.
+ std::atomic<ui64> HighBacklogWatermark_ = Max<ui64>();
+ std::atomic<ui64> LowBacklogWatermark_ = Max<ui64>();
+
+ std::once_flag Initialized_;
+ std::once_flag Started_;
+ std::atomic<bool> Suspended_ = false;
+ std::atomic<bool> ScheduledOutOfBand_ = false;
+
+ THashSet<TThreadLocalQueue*> LocalQueues_;
+ TMpscStack<TThreadLocalQueue*> RegisteredLocalQueues_;
+ TMpscStack<TThreadLocalQueue*> UnregisteredLocalQueues_;
+
+ TMpscStack<TLoggerQueueItem> GlobalQueue_;
+ TMpscStack<TRequestId> SuppressedRequestIdQueue_;
+
+ std::deque<TLoggerQueueItem> TimeOrderedBuffer_;
+ TExpiringSet<TRequestId> SuppressedRequestIdSet_;
+
+ using TEventProfilingKey = std::pair<TString, ELogLevel>;
+ THashMap<TEventProfilingKey, TCounter> WrittenEventsCounters_;
+
+ const TProfiler Profiler{"/logging"};
+
+ TGauge MinLogStorageAvailableSpace_ = Profiler.Gauge("/min_log_storage_available_space");
+ TGauge MinLogStorageFreeSpace_ = Profiler.Gauge("/min_log_storage_free_space");
+
+ TBufferedProducerPtr AnchorBufferedProducer_;
+ TInstant LastAnchorStatsCaptureTime_;
+
+ std::atomic<ui64> EnqueuedEvents_ = 0;
+ std::atomic<ui64> WrittenEvents_ = 0;
+ std::atomic<ui64> FlushedEvents_ = 0;
+ std::atomic<ui64> SuppressedEvents_ = 0;
+ std::atomic<ui64> DroppedEvents_ = 0;
+
+ THashMap<TString, ILogWriterPtr> NameToWriter_;
+ THashMap<TLogWriterCacheKey, std::vector<ILogWriterPtr>> KeyToCachedWriter_;
+ const std::vector<ILogWriterPtr> SystemWriters_;
+
+ std::atomic<bool> ReopenRequested_ = false;
+ std::atomic<bool> ShutdownRequested_ = false;
+ std::atomic<bool> RequestSuppressionEnabled_ = false;
+
+ const TPeriodicExecutorPtr DiskProfilingExecutor_;
+ const TPeriodicExecutorPtr AnchorProfilingExecutor_;
+ const TPeriodicExecutorPtr DequeueExecutor_;
+ const TPeriodicExecutorPtr FlushExecutor_;
+ const TPeriodicExecutorPtr WatchExecutor_;
+ const TPeriodicExecutorPtr CheckSpaceExecutor_;
+
+ const IThreadPoolPtr CompressionThreadPool_;
+
+ std::unique_ptr<TNotificationHandle> NotificationHandle_;
+ std::vector<std::unique_ptr<TNotificationWatch>> NotificationWatches_;
+ THashMap<int, TNotificationWatch*> WDToNotificationWatch_;
+
+ THashMap<TString, TLoggingAnchor*> AnchorMap_;
+ std::atomic<TLoggingAnchor*> FirstAnchor_ = nullptr;
+ std::vector<std::unique_ptr<TLoggingAnchor>> DynamicAnchors_;
+};
+
+/////////////////////////////////////////////////////////////////////////////
+
+struct TLocalQueueReclaimer
+{
+ ~TLocalQueueReclaimer()
+ {
+ if (PerThreadQueue) {
+ auto logManager = TLogManager::Get()->Impl_;
+ logManager->UnregisteredLocalQueues_.Enqueue(PerThreadQueue);
+ PerThreadQueue = reinterpret_cast<TThreadLocalQueue*>(ThreadQueueDestroyedSentinel);
+ }
+ }
+};
+
+static thread_local TLocalQueueReclaimer LocalQueueReclaimer;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLogManager::TLogManager()
+ : Impl_(New<TImpl>())
+{
+ // NB: TLogManager is instantiated before main. We can't rely on global variables here.
+ TProfiler{""}.AddProducer("/logging", Impl_);
+}
+
+TLogManager::~TLogManager() = default;
+
+TLogManager* TLogManager::Get()
+{
+ auto* logManager = LeakySingleton<TLogManager>();
+ logManager->Initialize();
+ return logManager;
+}
+
+void TLogManager::Configure(TLogManagerConfigPtr config, bool sync)
+{
+ Impl_->Configure(std::move(config), /*fromEnv*/ false, sync);
+}
+
+void TLogManager::ConfigureFromEnv()
+{
+ Impl_->ConfigureFromEnv();
+}
+
+bool TLogManager::IsConfiguredFromEnv()
+{
+ return Impl_->IsConfiguredFromEnv();
+}
+
+void TLogManager::Shutdown()
+{
+ Impl_->Shutdown();
+}
+
+int TLogManager::GetVersion() const
+{
+ return Impl_->GetVersion();
+}
+
+bool TLogManager::GetAbortOnAlert() const
+{
+ return Impl_->GetAbortOnAlert();
+}
+
+const TLoggingCategory* TLogManager::GetCategory(TStringBuf categoryName)
+{
+ return Impl_->GetCategory(categoryName);
+}
+
+void TLogManager::UpdateCategory(TLoggingCategory* category)
+{
+ Impl_->UpdateCategory(category);
+}
+
+void TLogManager::UpdateAnchor(TLoggingAnchor* anchor)
+{
+ Impl_->UpdateAnchor(anchor);
+}
+
+void TLogManager::RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation sourceLocation, TStringBuf anchorMessage)
+{
+ Impl_->RegisterStaticAnchor(anchor, sourceLocation, anchorMessage);
+}
+
+TLoggingAnchor* TLogManager::RegisterDynamicAnchor(TString anchorMessage)
+{
+ return Impl_->RegisterDynamicAnchor(std::move(anchorMessage));
+}
+
+void TLogManager::RegisterWriterFactory(const TString& typeName, const ILogWriterFactoryPtr& factory)
+{
+ Impl_->RegisterWriterFactory(typeName, factory);
+}
+
+void TLogManager::UnregisterWriterFactory(const TString& typeName)
+{
+ Impl_->UnregisterWriterFactory(typeName);
+}
+
+void TLogManager::Enqueue(TLogEvent&& event)
+{
+ Impl_->Enqueue(std::move(event));
+}
+
+void TLogManager::Reopen()
+{
+ Impl_->Reopen();
+}
+
+void TLogManager::EnableReopenOnSighup()
+{
+ Impl_->EnableReopenOnSighup();
+}
+
+void TLogManager::SuppressRequest(TRequestId requestId)
+{
+ Impl_->SuppressRequest(requestId);
+}
+
+void TLogManager::Synchronize(TInstant deadline)
+{
+ Impl_->Synchronize(deadline);
+}
+
+void TLogManager::Initialize()
+{
+ Impl_->Initialize();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFiberMinLogLevelGuard::TFiberMinLogLevelGuard(ELogLevel minLogLevel)
+ : OldMinLogLevel_(GetThreadMinLogLevel())
+{
+ SetThreadMinLogLevel(minLogLevel);
+}
+
+TFiberMinLogLevelGuard::~TFiberMinLogLevelGuard()
+{
+ SetThreadMinLogLevel(OldMinLogLevel_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef _win_
+
+ILogManager* GetDefaultLogManager()
+{
+ return TLogManager::Get();
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log_manager.h b/yt/yt/core/logging/log_manager.h
new file mode 100644
index 0000000000..08598d3080
--- /dev/null
+++ b/yt/yt/core/logging/log_manager.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include "log.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TLogWriterCacheKey
+{
+ TStringBuf Category;
+ ELogLevel LogLevel;
+ ELogFamily Family;
+};
+
+bool operator == (const TLogWriterCacheKey& lhs, const TLogWriterCacheKey& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogManager
+ : public ILogManager
+{
+public:
+ friend struct TLocalQueueReclaimer;
+
+ ~TLogManager();
+
+ static TLogManager* Get();
+
+ void Configure(TLogManagerConfigPtr config, bool sync = true);
+
+ void ConfigureFromEnv();
+ bool IsConfiguredFromEnv();
+
+ void Shutdown();
+
+ const TLoggingCategory* GetCategory(TStringBuf categoryName) override;
+ void UpdateCategory(TLoggingCategory* category) override;
+
+ void RegisterStaticAnchor(
+ TLoggingAnchor* position,
+ ::TSourceLocation sourceLocation,
+ TStringBuf anchorMessage) override;
+ TLoggingAnchor* RegisterDynamicAnchor(TString anchorMessage);
+ void UpdateAnchor(TLoggingAnchor* position) override;
+
+ void RegisterWriterFactory(const TString& typeName, const ILogWriterFactoryPtr& factory);
+ void UnregisterWriterFactory(const TString& typeName);
+
+ int GetVersion() const;
+ bool GetAbortOnAlert() const override;
+
+ void Enqueue(TLogEvent&& event) override;
+
+ void Reopen();
+ void EnableReopenOnSighup();
+
+ void SuppressRequest(NTracing::TRequestId requestId);
+
+ void Synchronize(TInstant deadline = TInstant::Max());
+
+private:
+ TLogManager();
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+
+ void Initialize();
+
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Sets the minimum logging level for all messages in current fiber.
+class TFiberMinLogLevelGuard
+{
+public:
+ explicit TFiberMinLogLevelGuard(ELogLevel minLogLevel);
+ ~TFiberMinLogLevelGuard();
+
+private:
+ const ELogLevel OldMinLogLevel_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
+
+template <>
+struct TSingletonTraits<NYT::NLogging::TLogManager>
+{
+ enum
+ {
+ Priority = 2048
+ };
+};
+
+template <>
+struct THash<NYT::NLogging::TLogWriterCacheKey>
+{
+ size_t operator () (const NYT::NLogging::TLogWriterCacheKey& obj) const
+ {
+ size_t hash = 0;
+ NYT::HashCombine(hash, THash<TString>()(obj.Category));
+ NYT::HashCombine(hash, static_cast<size_t>(obj.LogLevel));
+ NYT::HashCombine(hash, static_cast<size_t>(obj.Family));
+ return hash;
+ }
+};
diff --git a/yt/yt/core/logging/log_writer.h b/yt/yt/core/logging/log_writer.h
new file mode 100644
index 0000000000..6737ec379f
--- /dev/null
+++ b/yt/yt/core/logging/log_writer.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogWriter
+ : public virtual TRefCounted
+{
+ virtual void Write(const TLogEvent& event) = 0;
+ virtual void Flush() = 0;
+
+ virtual void Reload() = 0;
+
+ virtual void SetRateLimit(std::optional<i64> limit) = 0;
+ virtual void SetCategoryRateLimits(const THashMap<TString, i64>& categoryRateLimits) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ILogWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IFileLogWriter
+ : public virtual ILogWriter
+{
+ virtual const TString& GetFileName() const = 0;
+ virtual void CheckSpace(i64 minSpace) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IFileLogWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log_writer_detail.cpp b/yt/yt/core/logging/log_writer_detail.cpp
new file mode 100644
index 0000000000..ce7228aa38
--- /dev/null
+++ b/yt/yt/core/logging/log_writer_detail.cpp
@@ -0,0 +1,200 @@
+#include "log_writer_detail.h"
+
+#include "log.h"
+#include "formatter.h"
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+namespace NYT::NLogging {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto RateLimitUpdatePeriod = TDuration::Seconds(1);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRateLimitCounter::TRateLimitCounter(
+ std::optional<i64> rateLimit,
+ NProfiling::TCounter bytesCounter,
+ NProfiling::TCounter skippedEventsCounter)
+ : RateLimit_(rateLimit)
+ , BytesCounter_(std::move(bytesCounter))
+ , SkippedEventsCounter_(std::move(skippedEventsCounter))
+ , LastUpdate_(TInstant::Now())
+{ }
+
+void TRateLimitCounter::SetRateLimit(std::optional<i64> rateLimit)
+{
+ RateLimit_ = rateLimit;
+ LastUpdate_ = TInstant::Now();
+ BytesWritten_ = 0;
+}
+
+bool TRateLimitCounter::IsLimitReached()
+{
+ if (!RateLimit_) {
+ return false;
+ }
+
+ if (BytesWritten_ >= *RateLimit_) {
+ SkippedEvents_++;
+ SkippedEventsCounter_.Increment();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool TRateLimitCounter::IsIntervalPassed()
+{
+ auto now = TInstant::Now();
+ if (now - LastUpdate_ >= RateLimitUpdatePeriod) {
+ LastUpdate_ = now;
+ BytesWritten_ = 0;
+ return true;
+ }
+ return false;
+}
+
+void TRateLimitCounter::UpdateCounter(i64 bytesWritten)
+{
+ BytesWritten_ += bytesWritten;
+ BytesCounter_.Increment(bytesWritten);
+}
+
+i64 TRateLimitCounter::GetAndResetLastSkippedEventsCount()
+{
+ i64 old = SkippedEvents_;
+ SkippedEvents_ = 0;
+ return old;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStreamLogWriterBase::TStreamLogWriterBase(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name)
+ : Formatter_(std::move(formatter))
+ , Name_(std::move(name))
+ , RateLimit_(
+ std::nullopt,
+ {},
+ TProfiler{"/logging"}.WithSparse().WithTag("writer", Name_).Counter("/events_skipped_by_global_limit"))
+ , CurrentSegmentSizeGauge_(
+ TProfiler{"/logging"}.WithSparse().WithTag("writer", Name_).Gauge("/current_segment_size"))
+{ }
+
+void TStreamLogWriterBase::Write(const TLogEvent& event)
+{
+ auto* stream = GetOutputStream();
+ if (!stream) {
+ return;
+ }
+
+ try {
+ auto* categoryRateLimit = GetCategoryRateLimitCounter(event.Category->Name);
+ if (RateLimit_.IsIntervalPassed()) {
+ auto eventsSkipped = RateLimit_.GetAndResetLastSkippedEventsCount();
+ if (eventsSkipped > 0) {
+ Formatter_->WriteLogSkippedEvent(stream, eventsSkipped, Name_);
+ }
+ }
+ if (categoryRateLimit->IsIntervalPassed()) {
+ auto eventsSkipped = categoryRateLimit->GetAndResetLastSkippedEventsCount();
+ if (eventsSkipped > 0) {
+ Formatter_->WriteLogSkippedEvent(stream, eventsSkipped, event.Category->Name);
+ }
+ }
+ if (!RateLimit_.IsLimitReached() && !categoryRateLimit->IsLimitReached()) {
+ auto bytesWritten = Formatter_->WriteFormatted(stream, event);
+ CurrentSegmentSize_ += bytesWritten;
+ CurrentSegmentSizeGauge_.Update(CurrentSegmentSize_);
+ RateLimit_.UpdateCounter(bytesWritten);
+ categoryRateLimit->UpdateCounter(bytesWritten);
+ }
+ } catch (const std::exception& ex) {
+ OnException(ex);
+ }
+}
+
+void TStreamLogWriterBase::Flush()
+{
+ auto* stream = GetOutputStream();
+ if (!stream) {
+ return;
+ }
+
+ try {
+ stream->Flush();
+ } catch (const std::exception& ex) {
+ OnException(ex);
+ }
+}
+
+void TStreamLogWriterBase::Reload()
+{ }
+
+void TStreamLogWriterBase::OnException(const std::exception& ex)
+{
+ // Fail with drama by default.
+ TRawFormatter<1024> formatter;
+ formatter.AppendString("\n*** Unhandled exception in log writer: ");
+ formatter.AppendString(ex.what());
+ formatter.AppendString("\n*** Aborting ***\n");
+#ifdef _unix_
+ HandleEintr(::write, 2, formatter.GetData(), formatter.GetBytesWritten());
+#else
+ ::WriteFile(
+ GetStdHandle(STD_ERROR_HANDLE),
+ formatter.GetData(),
+ formatter.GetBytesWritten(),
+ /*lpNumberOfBytesWritten*/ nullptr,
+ /*lpOverlapped*/ nullptr);
+#endif
+ _exit(100);
+ YT_ABORT();
+}
+
+void TStreamLogWriterBase::ResetCurrentSegment(i64 size)
+{
+ CurrentSegmentSize_ = size;
+ CurrentSegmentSizeGauge_.Update(CurrentSegmentSize_);
+}
+
+void TStreamLogWriterBase::SetRateLimit(std::optional<i64> limit)
+{
+ RateLimit_.SetRateLimit(limit);
+}
+
+void TStreamLogWriterBase::SetCategoryRateLimits(const THashMap<TString, i64>& categoryRateLimits)
+{
+ CategoryToRateLimit_.clear();
+ for (const auto& it : categoryRateLimits) {
+ GetCategoryRateLimitCounter(it.first)->SetRateLimit(it.second);
+ }
+}
+
+TRateLimitCounter* TStreamLogWriterBase::GetCategoryRateLimitCounter(TStringBuf category)
+{
+ auto it = CategoryToRateLimit_.find(category);
+ if (it == CategoryToRateLimit_.end()) {
+ auto r = TProfiler{"/logging"}
+ .WithSparse()
+ .WithTag("writer", Name_)
+ .WithTag("category", TString{category}, -1);
+
+ // TODO(prime@): optimize sensor count
+ it = CategoryToRateLimit_.insert({category, TRateLimitCounter(
+ std::nullopt,
+ r.Counter("/bytes_written"),
+ r.Counter("/events_skipped_by_category_limit")
+ )}).first;
+ }
+ return &it->second;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log_writer_detail.h b/yt/yt/core/logging/log_writer_detail.h
new file mode 100644
index 0000000000..4435d7c33e
--- /dev/null
+++ b/yt/yt/core/logging/log_writer_detail.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "private.h"
+#include "log_writer.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRateLimitCounter
+{
+public:
+ TRateLimitCounter(
+ std::optional<i64> rateLimit,
+ NProfiling::TCounter bytesCounter,
+ NProfiling::TCounter skippedEventsCounter);
+
+ void SetRateLimit(std::optional<i64> rateLimit);
+ bool IsLimitReached();
+ bool IsIntervalPassed();
+ i64 GetAndResetLastSkippedEventsCount();
+
+ void UpdateCounter(i64 bytesWritten);
+
+private:
+ std::optional<i64> RateLimit_;
+
+ NProfiling::TCounter BytesCounter_;
+ NProfiling::TCounter SkippedEventsCounter_;
+
+ i64 BytesWritten_ = 0;
+ i64 SkippedEvents_ = 0;
+
+ TInstant LastUpdate_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamLogWriterBase
+ : public virtual ILogWriter
+{
+public:
+ TStreamLogWriterBase(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name);
+
+ void Write(const TLogEvent& event) override;
+ void Flush() override;
+ void Reload() override;
+
+ void SetRateLimit(std::optional<i64> limit) override;
+ void SetCategoryRateLimits(const THashMap<TString, i64>& categoryRateLimits) override;
+
+protected:
+ virtual IOutputStream* GetOutputStream() const noexcept = 0;
+ virtual void OnException(const std::exception& ex);
+
+ void ResetCurrentSegment(i64 size);
+
+ TRateLimitCounter* GetCategoryRateLimitCounter(TStringBuf category);
+
+ const std::unique_ptr<ILogFormatter> Formatter_;
+ const TString Name_;
+
+ TRateLimitCounter RateLimit_;
+ THashMap<TStringBuf, TRateLimitCounter> CategoryToRateLimit_;
+
+ i64 CurrentSegmentSize_ = 0;
+ NProfiling::TGauge CurrentSegmentSizeGauge_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/log_writer_factory.h b/yt/yt/core/logging/log_writer_factory.h
new file mode 100644
index 0000000000..8ba6e2c69b
--- /dev/null
+++ b/yt/yt/core/logging/log_writer_factory.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogWriterHost
+{
+ virtual ~ILogWriterHost() = default;
+
+ //! Returns an invoker to be used for background compression
+ //! of log data.
+ /*!
+ * The threads are being spawned lazily; one should avoid unneeded
+ * invocations of this method.
+ */
+ virtual IInvokerPtr GetCompressionInvoker() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ILogWriterFactory
+ : public virtual TRefCounted
+{
+ //! Checks that #configNode can be deserialized into
+ //! a valid configuration for this particular log writer.
+ virtual void ValidateConfig(
+ const NYTree::IMapNodePtr& configNode) = 0;
+
+ //! Creates a log writer.
+ /*
+ * #configNode is checked by #ValidateConfig prior to this call.
+ *
+ * This call must not throw.
+ */
+ virtual ILogWriterPtr CreateWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ const NYTree::IMapNodePtr& configNode,
+ ILogWriterHost* host) noexcept = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ILogWriterFactory)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/logger_owner.cpp b/yt/yt/core/logging/logger_owner.cpp
new file mode 100644
index 0000000000..ee5abca554
--- /dev/null
+++ b/yt/yt/core/logging/logger_owner.cpp
@@ -0,0 +1,32 @@
+#include "logger_owner.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TLoggerOwner::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ Save(context, Logger);
+}
+
+void TLoggerOwner::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+
+ Load(context, Logger);
+}
+
+void TLoggerOwner::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/logger_owner.h b/yt/yt/core/logging/logger_owner.h
new file mode 100644
index 0000000000..c4815eaa06
--- /dev/null
+++ b/yt/yt/core/logging/logger_owner.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "serializable_logger.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Typically serves as a virtual base for classes that need a member logger.
+class TLoggerOwner
+{
+protected:
+ TSerializableLogger Logger;
+
+ TLoggerOwner() = default;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+ void Persist(const TStreamPersistenceContext& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/pattern.cpp b/yt/yt/core/logging/pattern.cpp
new file mode 100644
index 0000000000..99dd894023
--- /dev/null
+++ b/yt/yt/core/logging/pattern.cpp
@@ -0,0 +1,144 @@
+#include "pattern.h"
+
+#ifdef YT_USE_SSE42
+ #include <emmintrin.h>
+ #include <pmmintrin.h>
+#endif
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+// Ultra-fast specialized versions of AppendNumber.
+void AppendDigit(TBaseFormatter* out, ui32 value)
+{
+ out->AppendChar('0' + value);
+}
+
+void AppendNumber2(TBaseFormatter* out, ui32 value)
+{
+ AppendDigit(out, value / 10);
+ AppendDigit(out, value % 10);
+}
+
+void AppendNumber3(TBaseFormatter* out, ui32 value)
+{
+ AppendDigit(out, value / 100);
+ AppendDigit(out, (value / 10) % 10);
+ AppendDigit(out, value % 10);
+}
+
+void AppendNumber4(TBaseFormatter* out, ui32 value)
+{
+ AppendDigit(out, value / 1000);
+ AppendDigit(out, (value / 100) % 10);
+ AppendDigit(out, (value / 10) % 10);
+ AppendDigit(out, value % 10);
+}
+
+void AppendNumber6(TBaseFormatter* out, ui32 value)
+{
+ AppendDigit(out, value / 100000);
+ AppendDigit(out, (value / 10000) % 10);
+ AppendDigit(out, (value / 1000) % 10);
+ AppendDigit(out, (value / 100) % 10);
+ AppendDigit(out, (value / 10) % 10);
+ AppendDigit(out, value % 10);
+}
+
+} // namespace
+
+void FormatDateTime(TBaseFormatter* out, TInstant dateTime)
+{
+ tm localTime;
+ dateTime.LocalTime(&localTime);
+ AppendNumber4(out, localTime.tm_year + 1900);
+ out->AppendChar('-');
+ AppendNumber2(out, localTime.tm_mon + 1);
+ out->AppendChar('-');
+ AppendNumber2(out, localTime.tm_mday);
+ out->AppendChar(' ');
+ AppendNumber2(out, localTime.tm_hour);
+ out->AppendChar(':');
+ AppendNumber2(out, localTime.tm_min);
+ out->AppendChar(':');
+ AppendNumber2(out, localTime.tm_sec);
+}
+
+void FormatMilliseconds(TBaseFormatter* out, TInstant dateTime)
+{
+ AppendNumber3(out, dateTime.MilliSecondsOfSecond());
+}
+
+void FormatMicroseconds(TBaseFormatter* out, TInstant dateTime)
+{
+ AppendNumber6(out, dateTime.MicroSecondsOfSecond());
+}
+
+void FormatLevel(TBaseFormatter* out, ELogLevel level)
+{
+ static char chars[] = "?TDIWEAF?";
+ out->AppendChar(chars[static_cast<int>(level)]);
+}
+
+void FormatMessage(TBaseFormatter* out, TStringBuf message)
+{
+ auto current = message.begin();
+
+#ifdef YT_USE_SSE42
+ auto vectorLow = _mm_set1_epi8(PrintableASCIILow);
+ auto vectorHigh = _mm_set1_epi8(PrintableASCIIHigh);
+#endif
+
+ auto appendChar = [&] {
+ char ch = *current;
+ if (ch == '\n') {
+ out->AppendString("\\n");
+ } else if (ch == '\t') {
+ out->AppendString("\\t");
+ } else if (ch < PrintableASCIILow || ch > PrintableASCIIHigh) {
+ unsigned char unsignedCh = ch;
+ out->AppendString("\\x");
+ out->AppendChar(IntToHexLowercase[unsignedCh >> 4]);
+ out->AppendChar(IntToHexLowercase[unsignedCh & 15]);
+ } else {
+ out->AppendChar(ch);
+ }
+ ++current;
+ };
+
+ while (current < message.end()) {
+#ifdef YT_USE_SSE42
+ // Use SSE for optimization.
+ if (current + 16 > message.end()) {
+ appendChar();
+ } else if (out->GetBytesRemaining() < MessageBufferWatermarkSize) {
+ out->AppendString(TStringBuf("...<message truncated>"));
+ break;
+ } else {
+ const void* inPtr = &(*current);
+ void* outPtr = out->GetCursor();
+ auto value = _mm_lddqu_si128(static_cast<const __m128i*>(inPtr));
+ if (_mm_movemask_epi8(_mm_cmplt_epi8(value, vectorLow)) ||
+ _mm_movemask_epi8(_mm_cmpgt_epi8(value, vectorHigh))) {
+ for (int index = 0; index < 16; ++index) {
+ appendChar();
+ }
+ } else {
+ _mm_storeu_si128(static_cast<__m128i*>(outPtr), value);
+ out->Advance(16);
+ current += 16;
+ }
+ }
+#else
+ // Unoptimized version.
+ appendChar();
+#endif
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/pattern.h b/yt/yt/core/logging/pattern.h
new file mode 100644
index 0000000000..7733f8da90
--- /dev/null
+++ b/yt/yt/core/logging/pattern.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <util/generic/size_literals.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int DateTimeBufferSize = 64;
+constexpr int MessageBufferSize = 64_KB;
+constexpr int MessageBufferWatermarkSize = 256;
+
+void FormatMessage(TBaseFormatter* out, TStringBuf message);
+void FormatDateTime(TBaseFormatter* out, TInstant dateTime);
+void FormatMilliseconds(TBaseFormatter* out, TInstant dateTime);
+void FormatMicroseconds(TBaseFormatter* out, TInstant dateTime);
+void FormatLevel(TBaseFormatter* out, ELogLevel level);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NLogging::NYT
diff --git a/yt/yt/core/logging/private.h b/yt/yt/core/logging/private.h
new file mode 100644
index 0000000000..a8160cbdc1
--- /dev/null
+++ b/yt/yt/core/logging/private.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr TStringBuf SystemLoggingCategoryName = "Logging";
+
+constexpr TStringBuf DefaultStderrWriterName = "Stderr";
+constexpr TStringBuf DefaultFileWriterName = "File";
+
+constexpr ELogLevel DefaultStderrMinLevel = ELogLevel::Info;
+constexpr ELogLevel DefaultStderrQuietLevel = ELogLevel::Error;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_TYPE(TFixedBufferFileOutput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/public.h b/yt/yt/core/logging/public.h
new file mode 100644
index 0000000000..dce165235b
--- /dev/null
+++ b/yt/yt/core/logging/public.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <library/cpp/yt/logging/public.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ELogFormat,
+ (PlainText)
+ (Json)
+ // Legacy alias for JSON.
+ (Structured)
+ (Yson)
+);
+
+DEFINE_ENUM(ECompressionMethod,
+ (Gzip)
+ (Zstd)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TLogManagerConfig)
+DECLARE_REFCOUNTED_CLASS(TLogManagerDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TFormatterConfig)
+DECLARE_REFCOUNTED_CLASS(TLogWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TRuleConfig)
+DECLARE_REFCOUNTED_CLASS(TFileLogWriterConfig)
+DECLARE_REFCOUNTED_CLASS(TStderrLogWriterConfig)
+
+struct ILogFormatter;
+struct ILogWriterHost;
+DECLARE_REFCOUNTED_STRUCT(ILogWriterFactory)
+DECLARE_REFCOUNTED_STRUCT(ILogWriter)
+DECLARE_REFCOUNTED_STRUCT(IFileLogWriter)
+DECLARE_REFCOUNTED_STRUCT(IStreamLogOutput)
+DECLARE_REFCOUNTED_STRUCT(ILogCompressionCodec)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/random_access_gzip.cpp b/yt/yt/core/logging/random_access_gzip.cpp
new file mode 100644
index 0000000000..a8fc487f08
--- /dev/null
+++ b/yt/yt/core/logging/random_access_gzip.cpp
@@ -0,0 +1,119 @@
+#include "random_access_gzip.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+namespace NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#pragma pack(push, 1)
+
+struct TGZipFixedHeader
+{
+ ui8 Id[2];
+ char Uninteresting0[1];
+ char Flags;
+ char Uninteresting1[6];
+};
+
+struct TGZipExtendedHeader
+{
+ TGZipFixedHeader FixedHeader;
+ ui16 XLen;
+ char SubfieldId[2];
+ ui16 SubfieldLength;
+ ui32 SmuggledBlockSize;
+};
+
+constexpr int HeaderGrowth = sizeof(TGZipExtendedHeader) - sizeof(TGZipFixedHeader);
+constexpr int ExtraFlag = 1 << 2;
+
+#pragma pack(pop)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRandomAccessGZipFile::TRandomAccessGZipFile(TFile file, size_t compressionLevel, size_t /*blockSize*/)
+ : CompressionLevel_(compressionLevel)
+ , File_(std::move(file))
+{
+ Repair();
+ Reset();
+}
+
+void TRandomAccessGZipFile::Repair()
+{
+ auto fileSize = File_.GetLength();
+ if (fileSize == 0) {
+ return;
+ }
+
+ while (OutputPosition_ != fileSize) {
+ TGZipExtendedHeader header;
+ if (fileSize - OutputPosition_ < static_cast<int>(sizeof(header))) {
+ File_.Resize(OutputPosition_);
+ return;
+ }
+
+ File_.Pread(&header, sizeof(header), OutputPosition_);
+ // Wrong magic.
+ if (header.FixedHeader.Id[0] != 0x1f || header.FixedHeader.Id[1] != 0x8b) {
+ File_.Resize(OutputPosition_);
+ return;
+ }
+
+ // Block is not fully flushed.
+ if (OutputPosition_ + header.SmuggledBlockSize > fileSize || header.SmuggledBlockSize == 0) {
+ File_.Resize(OutputPosition_);
+ return;
+ }
+
+ OutputPosition_ += header.SmuggledBlockSize;
+ }
+}
+
+void TRandomAccessGZipFile::Reset()
+{
+ Output_.Buffer().Resize(HeaderGrowth);
+ Compressor_.reset(new TZLibCompress(&Output_, ZLib::GZip, CompressionLevel_));
+}
+
+void TRandomAccessGZipFile::DoWrite(const void* buf, size_t len)
+{
+ Compressor_->Write(buf, len);
+}
+
+void TRandomAccessGZipFile::DoFlush()
+{
+ Compressor_->Finish();
+ auto buffer = Output_.Buffer();
+
+ TGZipExtendedHeader header;
+ memcpy(&header.FixedHeader, buffer.Data() + HeaderGrowth, sizeof(header.FixedHeader));
+
+ YT_VERIFY(header.FixedHeader.Id[0] == 0x1f);
+ YT_VERIFY(header.FixedHeader.Id[1] == 0x8b);
+ YT_VERIFY((header.FixedHeader.Flags & ExtraFlag) == 0);
+ header.FixedHeader.Flags |= ExtraFlag;
+ header.XLen = 8;
+ header.SubfieldId[0] = 'Y';
+ header.SubfieldId[1] = 'T';
+ header.SubfieldLength = 4;
+ header.SmuggledBlockSize = buffer.Size();
+
+ memcpy(buffer.Data(), &header, sizeof(header));
+
+ File_.Pwrite(buffer.Data(), buffer.Size(), OutputPosition_);
+ OutputPosition_ += buffer.Size();
+ Reset();
+}
+
+void TRandomAccessGZipFile::DoFinish()
+{
+ Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NLogging
+} // namespace NYT
diff --git a/yt/yt/core/logging/random_access_gzip.h b/yt/yt/core/logging/random_access_gzip.h
new file mode 100644
index 0000000000..f4d564a016
--- /dev/null
+++ b/yt/yt/core/logging/random_access_gzip.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "stream_output.h"
+
+#include <memory>
+
+#include <util/generic/buffer.h>
+#include <util/generic/size_literals.h>
+
+#include <util/stream/buffer.h>
+#include <util/stream/file.h>
+#include <util/stream/zlib.h>
+
+namespace NYT {
+namespace NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRandomAccessGZipFile
+ : public IStreamLogOutput
+{
+public:
+ explicit TRandomAccessGZipFile(TFile file, size_t compressionLevel = 6, size_t blockSize = 256_KB);
+
+private:
+ const size_t CompressionLevel_;
+
+ TFile File_;
+
+ i64 OutputPosition_ = 0;
+
+ TBufferOutput Output_;
+ std::unique_ptr<TZLibCompress> Compressor_;
+
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFlush() override;
+ void DoFinish() override;
+
+ void Repair();
+ void Reset();
+};
+
+DECLARE_REFCOUNTED_TYPE(TRandomAccessGZipFile)
+DEFINE_REFCOUNTED_TYPE(TRandomAccessGZipFile)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NLogging
+} // namespace NYT
diff --git a/yt/yt/core/logging/serializable_logger.cpp b/yt/yt/core/logging/serializable_logger.cpp
new file mode 100644
index 0000000000..229638be79
--- /dev/null
+++ b/yt/yt/core/logging/serializable_logger.cpp
@@ -0,0 +1,60 @@
+#include "serializable_logger.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSerializableLogger::TSerializableLogger(TLogger logger)
+ : TLogger(std::move(logger))
+{ }
+
+void TSerializableLogger::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ if (Category_) {
+ Save(context, true);
+ Save(context, TString(Category_->Name));
+ } else {
+ Save(context, false);
+ }
+
+ Save(context, Essential_);
+ Save(context, MinLevel_);
+ Save(context, Tag_);
+ TVectorSerializer<TTupleSerializer<TStructuredTag, 2>>::Save(context, StructuredTags_);
+}
+
+void TSerializableLogger::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+
+ TString categoryName;
+
+ bool categoryPresent = false;
+ Load(context, categoryPresent);
+ if (categoryPresent) {
+ Load(context, categoryName);
+ LogManager_ = GetDefaultLogManager();
+ Category_ = LogManager_->GetCategory(categoryName.data());
+ } else {
+ Category_ = nullptr;
+ }
+
+ Load(context, Essential_);
+ Load(context, MinLevel_);
+ Load(context, Tag_);
+
+ // COMPAT(max42); 300616 is a version for StructuredTagsInLogger in CA snapshot.
+ if (context.GetVersion() >= 300616) {
+ TVectorSerializer<TTupleSerializer<TStructuredTag, 2>>::Load(context, StructuredTags_);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/serializable_logger.h b/yt/yt/core/logging/serializable_logger.h
new file mode 100644
index 0000000000..8d4c4fa9d2
--- /dev/null
+++ b/yt/yt/core/logging/serializable_logger.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "log.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializableLogger
+ : public TLogger
+{
+public:
+ using TLogger::TLogger;
+ TSerializableLogger(TLogger logger);
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/stream_log_writer.cpp b/yt/yt/core/logging/stream_log_writer.cpp
new file mode 100644
index 0000000000..337d2bb92d
--- /dev/null
+++ b/yt/yt/core/logging/stream_log_writer.cpp
@@ -0,0 +1,94 @@
+#include "stream_log_writer.h"
+
+#include "log_writer_detail.h"
+#include "log_writer_factory.h"
+#include "formatter.h"
+
+#include <library/cpp/yt/memory/leaky_ref_counted_singleton.h>
+
+namespace NYT::NLogging {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamLogWriter
+ : public TStreamLogWriterBase
+{
+public:
+ TStreamLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ IOutputStream* stream)
+ : TStreamLogWriterBase(
+ std::move(formatter),
+ std::move(name))
+ , Stream_(stream)
+ { }
+
+private:
+ IOutputStream* const Stream_;
+
+ IOutputStream* GetOutputStream() const noexcept override
+ {
+ return Stream_;
+ }
+};
+
+ILogWriterPtr CreateStreamLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ IOutputStream* stream)
+{
+ return New<TStreamLogWriter>(
+ std::move(formatter),
+ std::move(name),
+ stream);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ILogWriterPtr CreateStderrLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name)
+{
+ return CreateStreamLogWriter(
+ std::move(formatter),
+ std::move(name),
+ &Cerr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStderrLogWriterFactory
+ : public ILogWriterFactory
+{
+public:
+ void ValidateConfig(
+ const NYTree::IMapNodePtr& configNode) override
+ {
+ ConvertTo<TStderrLogWriterConfigPtr>(configNode);
+ }
+
+ ILogWriterPtr CreateWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ const NYTree::IMapNodePtr& /*configNode*/,
+ ILogWriterHost* /*host*/) noexcept override
+ {
+ return CreateStderrLogWriter(
+ std::move(formatter),
+ std::move(name));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ILogWriterFactoryPtr GetStderrLogWriterFactory()
+{
+ return LeakyRefCountedSingleton<TStderrLogWriterFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/stream_log_writer.h b/yt/yt/core/logging/stream_log_writer.h
new file mode 100644
index 0000000000..717b6a621c
--- /dev/null
+++ b/yt/yt/core/logging/stream_log_writer.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ILogWriterPtr CreateStreamLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name,
+ IOutputStream* stream);
+
+ILogWriterPtr CreateStderrLogWriter(
+ std::unique_ptr<ILogFormatter> formatter,
+ TString name);
+
+ILogWriterFactoryPtr GetStderrLogWriterFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/stream_output.cpp b/yt/yt/core/logging/stream_output.cpp
new file mode 100644
index 0000000000..6c8349051a
--- /dev/null
+++ b/yt/yt/core/logging/stream_output.cpp
@@ -0,0 +1,32 @@
+#include "stream_output.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFixedBufferFileOutput::TFixedBufferFileOutput(
+ TFile file,
+ size_t bufferSize)
+ : Underlying_(bufferSize, file)
+{
+ Underlying_.SetFinishPropagateMode(true);
+}
+
+void TFixedBufferFileOutput::DoWrite(const void* buf, size_t len)
+{
+ Underlying_.Write(buf, len);
+}
+
+void TFixedBufferFileOutput::DoFlush()
+{
+ Underlying_.Flush();
+}
+
+void TFixedBufferFileOutput::DoFinish()
+{
+ Underlying_.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/stream_output.h b/yt/yt/core/logging/stream_output.h
new file mode 100644
index 0000000000..6373a59999
--- /dev/null
+++ b/yt/yt/core/logging/stream_output.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/stream/file.h>
+#include <util/stream/output.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IStreamLogOutput
+ : public IOutputStream
+ , public virtual TRefCounted
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IStreamLogOutput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFixedBufferFileOutput
+ : public IStreamLogOutput
+{
+public:
+ TFixedBufferFileOutput(TFile file, size_t bufferSize);
+
+private:
+ TBuffered<TUnbufferedFileOutput> Underlying_;
+
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFlush() override;
+ void DoFinish() override;
+};
+
+DEFINE_REFCOUNTED_TYPE(TFixedBufferFileOutput)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/unittests/logging_ut.cpp b/yt/yt/core/logging/unittests/logging_ut.cpp
new file mode 100644
index 0000000000..49a5019d2d
--- /dev/null
+++ b/yt/yt/core/logging/unittests/logging_ut.cpp
@@ -0,0 +1,1224 @@
+#include <gtest/gtest-spi.h>
+
+#include "yt/yt/core/misc/string_builder.h"
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/logging/log.h>
+#include <yt/yt/core/logging/log_manager.h>
+#include <yt/yt/core/logging/log_writer.h>
+#include <yt/yt/core/logging/log_writer_factory.h>
+#include <yt/yt/core/logging/file_log_writer.h>
+#include <yt/yt/core/logging/fluent_log.h>
+#include <yt/yt/core/logging/stream_log_writer.h>
+#include <yt/yt/core/logging/random_access_gzip.h>
+#include <yt/yt/core/logging/compression.h>
+#include <yt/yt/core/logging/zstd_compression.h>
+#include <yt/yt/core/logging/config.h>
+#include <yt/yt/core/logging/formatter.h>
+
+#include <yt/yt/core/json/json_parser.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/misc/range_formatters.h>
+
+#include <library/cpp/streams/zstd/zstd.h>
+
+#include <util/system/fs.h>
+#include <util/system/tempfile.h>
+
+#include <util/stream/zlib.h>
+
+#include <cmath>
+#include <thread>
+
+#ifdef _unix_
+#include <unistd.h>
+#endif
+
+namespace NYT::NLogging {
+namespace {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NJson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TLogger Logger("Test");
+
+TString GenerateLogFileName()
+{
+ return GenerateRandomFileName("log");
+}
+
+class TLoggingTest
+ : public ::testing::Test
+ , public ILogWriterHost
+{
+protected:
+ const int DateLength = ToString("2014-04-24 23:41:09,804000").length();
+
+ IInvokerPtr GetCompressionInvoker() override
+ {
+ return GetCurrentInvoker();
+ }
+
+ IMapNodePtr DeserializeStructuredEvent(const TString& source, ELogFormat format)
+ {
+ switch (format) {
+ case ELogFormat::Json: {
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ TStringStream stream(source);
+ ParseJson(&stream, builder.get());
+ return builder->EndTree()->AsMap();
+ }
+ case ELogFormat::Yson: {
+ // Each line ends with a semicolon, so it must be treated as a list fragment.
+ auto listFragment = ConvertTo<std::vector<IMapNodePtr>>(TYsonStringBuf(source, EYsonType::ListFragment));
+ EXPECT_EQ(1, std::ssize(listFragment));
+ return listFragment.front();
+ }
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void WritePlainTextEvent(const ILogWriterPtr& writer)
+ {
+ TLogEvent event;
+ event.Family = ELogFamily::PlainText;
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Debug;
+ event.MessageRef = TSharedRef::FromString("message");
+ event.MessageKind = ELogMessageKind::Unstructured;
+ event.ThreadId = 0xba;
+ WriteEvent(writer, event);
+ }
+
+ void ExpectPlainTextEvent(const TString& line)
+ {
+ EXPECT_EQ(
+ Format("\tD\t%v\t%v\t%v\t\t\n",
+ Logger.GetCategory()->Name,
+ "message",
+ "ba"),
+ line.substr(DateLength));
+ }
+
+ void WriteEvent(const ILogWriterPtr& writer, const TLogEvent& event)
+ {
+ writer->Write(event);
+ writer->Flush();
+ }
+
+ std::vector<TString> ReadPlainTextEvents(
+ const TString& fileName,
+ std::optional<ECompressionMethod> compressionMethod = {})
+ {
+ auto splitLines = [&] (IInputStream *input) {
+ TString line;
+ std::vector<TString> lines;
+ while (input->ReadLine(line)) {
+ if (line.Contains(Logger.GetCategory()->Name)) {
+ lines.push_back(line + "\n");
+ }
+ }
+ return lines;
+ };
+
+ TUnbufferedFileInput rawInput(fileName);
+ if (!compressionMethod) {
+ return splitLines(&rawInput);
+ } else if (compressionMethod == ECompressionMethod::Gzip) {
+ TZLibDecompress input(&rawInput);
+ return splitLines(&input);
+ } else if (compressionMethod == ECompressionMethod::Zstd) {
+ TZstdDecompress input(&rawInput);
+ return splitLines(&input);
+ } else {
+ EXPECT_TRUE(false);
+ return {};
+ }
+ }
+
+ bool CheckPlainTextLogFileContains(const TString& fileName, const TString& message)
+ {
+ if (!NFs::Exists(fileName)) {
+ return false;
+ }
+
+ auto lines = ReadPlainTextEvents(fileName);
+ for (const auto& line : lines) {
+ if (line.Contains(message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void Configure(const TString& configYson)
+ {
+ auto configNode = ConvertToNode(TYsonString(configYson));
+ auto config = ConvertTo<TLogManagerConfigPtr>(configNode);
+ TLogManager::Get()->Configure(config, /*sync*/ true);
+ }
+
+ void DoTestCompression(ECompressionMethod method, int compressionLevel)
+ {
+ TTempFile logFile(GenerateLogFileName() + ".gz");
+
+ auto writerConfig = New<TFileLogWriterConfig>();
+ writerConfig->FileName = logFile.Name();
+ writerConfig->EnableCompression = true;
+ writerConfig->CompressionMethod = method;
+ writerConfig->CompressionLevel = compressionLevel;
+
+ auto writer = CreateFileLogWriter(
+ std::make_unique<TPlainTextLogFormatter>(),
+ "test_writer",
+ writerConfig,
+ this);
+
+ WritePlainTextEvent(writer);
+
+ writer->Reload();
+
+ WritePlainTextEvent(writer);
+
+ {
+ auto lines = ReadPlainTextEvents(logFile.Name(), method);
+ EXPECT_EQ(2, std::ssize(lines));
+ ExpectPlainTextEvent(lines[0]);
+ ExpectPlainTextEvent(lines[1]);
+ }
+ }
+};
+
+#ifdef _unix_
+
+TEST_F(TLoggingTest, ReloadOnSighup)
+{
+ TTempFile logFile(GenerateLogFileName());
+ TTempFile rotatedLogFile(logFile.Name() + ".1");
+
+ Cerr << "Configuring logging" << Endl;
+
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", logFile.Name()));
+
+ WaitForPredicate([&] {
+ TString message("Message1");
+ YT_LOG_INFO(message);
+ return CheckPlainTextLogFileContains(logFile.Name(), message);
+ });
+
+ Cerr << "Renaming logfile" << Endl;
+
+ NFs::Rename(logFile.Name(), rotatedLogFile.Name());
+
+ Cerr << "Sending SIGHUP" << Endl;
+
+ ::kill(::getpid(), SIGHUP);
+
+ Cerr << "Waiting for message 2" << Endl;
+
+ WaitForPredicate([&] {
+ TString message("Message2");
+ YT_LOG_INFO(message);
+ return CheckPlainTextLogFileContains(logFile.Name(), message);
+ });
+
+ Cerr << "Success" << Endl;
+}
+
+TEST_F(TLoggingTest, ReloadOnRename)
+{
+ TTempFile logFile(GenerateLogFileName());
+ TTempFile rotatedLogFile(logFile.Name() + ".1");
+
+ Cerr << "Configuring logging" << Endl;
+
+ Configure(Format(R"({
+ watch_period = 1000;
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", logFile.Name()));
+
+ Cerr << "Waiting for message 1" << Endl;
+
+ WaitForPredicate([&] {
+ TString message("Message1");
+ YT_LOG_INFO(message);
+ return CheckPlainTextLogFileContains(logFile.Name(), message);
+ });
+
+ Cerr << "Renaming logfile" << Endl;
+
+ NFs::Rename(logFile.Name(), rotatedLogFile.Name());
+
+ Cerr << "Waiting for message 2" << Endl;
+
+ WaitForPredicate([&] {
+ TString message("Message2");
+ YT_LOG_INFO(message);
+ return CheckPlainTextLogFileContains(logFile.Name(), message);
+ });
+
+ Cerr << "Success" << Endl;
+}
+
+#endif
+
+TEST_F(TLoggingTest, FileWriter)
+{
+ TTempFile logFile(GenerateLogFileName());
+
+ auto writerConfig = New<TFileLogWriterConfig>();
+ writerConfig->FileName = logFile.Name();
+
+ auto writer = CreateFileLogWriter(
+ std::make_unique<TPlainTextLogFormatter>(),
+ "test_writer",
+ writerConfig,
+ this);
+
+ WritePlainTextEvent(writer);
+
+ {
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(1, std::ssize(lines));
+ ExpectPlainTextEvent(lines[0]);
+ }
+
+ writer->Reload();
+ WritePlainTextEvent(writer);
+
+ {
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(2, std::ssize(lines));
+ ExpectPlainTextEvent(lines[0]);
+ ExpectPlainTextEvent(lines[1]);
+ }
+}
+
+TEST_F(TLoggingTest, GzipCompression)
+{
+ // No compression.
+ DoTestCompression(ECompressionMethod::Gzip, /*compressionLevel*/ 0);
+
+ // Default compression.
+ DoTestCompression(ECompressionMethod::Gzip, /*compressionLevel*/ 6);
+
+ // Maximum compression.
+ DoTestCompression(ECompressionMethod::Gzip, /*compressionLevel*/ 9);
+}
+
+TEST_F(TLoggingTest, ZstdCompression)
+{
+ // Default compression.
+ DoTestCompression(ECompressionMethod::Zstd, /*compressionLevel*/ 0);
+
+ // Fast compression (--fast=<...>).
+ DoTestCompression(ECompressionMethod::Zstd, /*compressionLevel*/ -2);
+
+ // Fast compression.
+ DoTestCompression(ECompressionMethod::Zstd, /*compressionLevel*/ 1);
+
+ // Maximum compression.
+ DoTestCompression(ECompressionMethod::Zstd, /*compressionLevel*/ 22);
+}
+
+TEST_F(TLoggingTest, StreamWriter)
+{
+ TStringStream stringOutput;
+ auto writer = CreateStreamLogWriter(
+ std::make_unique<TPlainTextLogFormatter>(),
+ "test_writer",
+ &stringOutput);
+
+ WritePlainTextEvent(writer);
+ ExpectPlainTextEvent(stringOutput.Str());
+}
+
+TEST_F(TLoggingTest, Rule)
+{
+ auto rule = New<TRuleConfig>();
+ rule->Load(ConvertToNode(TYsonString(TStringBuf(
+ R"({
+ exclude_categories = [ bus ];
+ min_level = info;
+ writers = [ some_writer ];
+ })"))));
+
+ EXPECT_TRUE(rule->IsApplicable("some_service", ELogFamily::PlainText));
+ EXPECT_FALSE(rule->IsApplicable("bus", ELogFamily::PlainText));
+ EXPECT_FALSE(rule->IsApplicable("bus", ELogLevel::Debug, ELogFamily::PlainText));
+ EXPECT_FALSE(rule->IsApplicable("some_service", ELogLevel::Debug, ELogFamily::PlainText));
+ EXPECT_TRUE(rule->IsApplicable("some_service", ELogLevel::Warning, ELogFamily::PlainText));
+ EXPECT_TRUE(rule->IsApplicable("some_service", ELogLevel::Info, ELogFamily::PlainText));
+}
+
+TEST_F(TLoggingTest, LogManager)
+{
+ TTempFile infoFile(GenerateLogFileName());
+ TTempFile errorFile(GenerateLogFileName());
+
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ {
+ "min_level" = "error";
+ "writers" = [ "error" ];
+ };
+ ];
+ "writers" = {
+ "error" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", errorFile.Name(), infoFile.Name()));
+
+ YT_LOG_DEBUG("Debug message");
+ YT_LOG_INFO("Info message");
+ YT_LOG_ERROR("Error message");
+
+ TLogManager::Get()->Synchronize();
+
+ {
+ auto infoLines = ReadPlainTextEvents(infoFile.Name());
+ EXPECT_EQ(2, std::ssize(infoLines));
+ }
+
+ {
+ auto errorLines = ReadPlainTextEvents(errorFile.Name());
+ EXPECT_EQ(1, std::ssize(errorLines));
+ }
+}
+
+TEST_F(TLoggingTest, ThreadMinLogLevel)
+{
+ TTempFile debugFile(GenerateLogFileName());
+
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "debug";
+ "writers" = [ "debug" ];
+ };
+ ];
+ "writers" = {
+ "debug" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", debugFile.Name()));
+
+ YT_LOG_DEBUG("Debug message 1");
+
+ SetThreadMinLogLevel(ELogLevel::Info);
+ YT_LOG_DEBUG("Debug message 2");
+ YT_LOG_INFO("Info message 1");
+
+ TLogManager::Get()->Synchronize();
+
+ {
+ auto infoLines = ReadPlainTextEvents(debugFile.Name());
+ EXPECT_EQ(2, std::ssize(infoLines));
+ }
+}
+
+TEST_F(TLoggingTest, StructuredLogging)
+{
+ TLogEvent event;
+ event.Family = ELogFamily::Structured;
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Debug;
+ event.MessageRef = BuildYsonStringFluently<EYsonType::MapFragment>()
+ .Item("message").Value("test_message")
+ .Finish()
+ .ToSharedRef();
+ event.MessageKind = ELogMessageKind::Structured;
+
+ for (auto format : {ELogFormat::Yson, ELogFormat::Json}) {
+ TTempFile logFile(GenerateLogFileName());
+
+ auto writerConfig = New<TFileLogWriterConfig>();
+ writerConfig->FileName = logFile.Name();
+
+ auto writer = CreateFileLogWriter(
+ std::make_unique<TStructuredLogFormatter>(format, THashMap<TString, INodePtr>{}),
+ "test_writer",
+ writerConfig,
+ this);
+
+ WriteEvent(writer, event);
+ TLogManager::Get()->Synchronize();
+
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(1, std::ssize(lines));
+
+ auto message = DeserializeStructuredEvent(lines[0], format);
+ EXPECT_EQ(message->GetChildOrThrow("message")->AsString()->GetValue(), "test_message");
+ EXPECT_EQ(message->GetChildOrThrow("level")->AsString()->GetValue(), "debug");
+ EXPECT_EQ(message->GetChildOrThrow("category")->AsString()->GetValue(), Logger.GetCategory()->Name);
+ }
+}
+
+TEST_F(TLoggingTest, UnstructuredLogging)
+{
+ TLogEvent event;
+ event.Family = ELogFamily::Structured;
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Debug;
+ event.MessageRef = TSharedRef::FromString("test_message");
+ event.MessageKind = ELogMessageKind::Unstructured;
+
+ for (auto format : {ELogFormat::Yson, ELogFormat::Json}) {
+ TTempFile logFile(GenerateLogFileName());
+
+ auto writerConfig = New<TFileLogWriterConfig>();
+ writerConfig->FileName = logFile.Name();
+
+ auto writer = CreateFileLogWriter(
+ std::make_unique<TStructuredLogFormatter>(format, THashMap<TString, INodePtr>{}),
+ "test_writer",
+ writerConfig,
+ this);
+
+ WriteEvent(writer, event);
+ TLogManager::Get()->Synchronize();
+
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(1, std::ssize(lines));
+
+ auto message = DeserializeStructuredEvent(lines[0], format);
+ EXPECT_EQ(message->GetChildOrThrow("message")->AsString()->GetValue(), "test_message");
+ EXPECT_EQ(message->GetChildOrThrow("level")->AsString()->GetValue(), FormatEnum(ELogLevel::Debug));
+ EXPECT_EQ(message->GetChildOrThrow("category")->AsString()->GetValue(), Logger.GetCategory()->Name);
+ }
+}
+
+TEST_F(TLoggingTest, StructuredLoggingJsonFormat)
+{
+ TString longString(1000, 'a');
+ TString longStringPrefix(100, 'a');
+
+ TLogEvent event;
+ event.Family = ELogFamily::Structured;
+ event.Category = Logger.GetCategory();
+ event.Level = ELogLevel::Debug;
+ event.MessageRef = BuildYsonStringFluently<EYsonType::MapFragment>()
+ .Item("message").Value("test_message")
+ .Item("nan_value").Value(std::nan("1"))
+ .Item("long_string_value").Value(longString)
+ .Finish()
+ .ToSharedRef();
+ event.MessageKind = ELogMessageKind::Structured;
+
+ auto jsonFormat = New<TJsonFormatConfig>();
+ jsonFormat->StringifyNanAndInfinity = true;
+ jsonFormat->StringLengthLimit = 100;
+
+ TTempFile logFile(GenerateLogFileName());
+
+ auto writerConfig = New<TFileLogWriterConfig>();
+ writerConfig->FileName = logFile.Name();
+
+ auto formatter = std::make_unique<TStructuredLogFormatter>(
+ ELogFormat::Json,
+ /*commonFields*/ THashMap<TString, INodePtr>{},
+ /*enableControlMessages*/ true,
+ jsonFormat);
+
+ auto writer = CreateFileLogWriter(
+ std::move(formatter),
+ "test_writer",
+ writerConfig,
+ this);
+
+ WriteEvent(writer, event);
+ TLogManager::Get()->Synchronize();
+
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(1, std::ssize(lines));
+
+ auto message = DeserializeStructuredEvent(lines[0], ELogFormat::Json);
+ EXPECT_EQ(message->GetChildOrThrow("message")->AsString()->GetValue(), "test_message");
+ EXPECT_EQ(message->GetChildOrThrow("nan_value")->AsString()->GetValue(), "nan");
+ EXPECT_EQ(message->GetChildOrThrow("long_string_value")->AsString()->GetValue(), longStringPrefix);
+ EXPECT_EQ(message->GetChildOrThrow("level")->AsString()->GetValue(), FormatEnum(ELogLevel::Debug));
+ EXPECT_EQ(message->GetChildOrThrow("category")->AsString()->GetValue(), Logger.GetCategory()->Name);
+}
+
+TEST_F(TLoggingTest, StructuredLoggingWithValidator)
+{
+ TTempFile logFile(GenerateLogFileName());
+ Configure(Format(R"({
+ rules = [
+ {
+ "family" = "structured";
+ "min_level" = "info";
+ "writers" = [ "test" ];
+ };
+ ];
+ "writers" = {
+ "test" = {
+ "format" = "structured";
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ "structured_validation_sampling_rate" = 1.0;
+ })", logFile.Name()));
+
+ auto logger = Logger.WithStructuredValidator([] (const TYsonString& yson) {
+ auto message = ConvertToNode(yson)->AsMap();
+ auto testField = message->FindChild("test_field");
+ if (!testField) {
+ ADD_FAILURE();
+ }
+ });
+
+ auto sendValidMessage = [&logger] {
+ LogStructuredEventFluently(logger, ELogLevel::Info)
+ .Item("test_field").Value("test_value");
+ };
+ sendValidMessage();
+
+ auto sendInvalidMessage = [&logger] {
+ LogStructuredEventFluently(logger, ELogLevel::Info);
+ };
+ EXPECT_NONFATAL_FAILURE(sendInvalidMessage(), "");
+
+ TLogManager::Get()->Synchronize();
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(2, std::ssize(lines));
+}
+
+TEST_F(TLoggingTest, StructuredValidationWithSamplingRate)
+{
+ int counter = 0;
+ auto logger = Logger.WithStructuredValidator([&counter] (const TYsonString& /*yson*/) {
+ counter++;
+ });
+
+ TTempFile logFile(GenerateLogFileName());
+ Configure(Format(R"({
+ rules = [
+ {
+ "family" = "structured";
+ "min_level" = "info";
+ "writers" = [ "test" ];
+ };
+ ];
+ "writers" = {
+ "test" = {
+ "file_name" = "%v";
+ "format" = "structured";
+ "type" = "file";
+ }
+ };
+ "structured_validation_sampling_rate" = 0.5;
+ })", logFile.Name()));
+ EXPECT_NEAR(TLogManager::Get()->GetCategory("Test")->StructuredValidationSamplingRate, 0.5, 0.001);
+
+ int iterations = 100;
+ for (int i = 0; i < iterations; i++) {
+ LogStructuredEventFluently(logger, ELogLevel::Info);
+ }
+
+ EXPECT_LT(counter, iterations);
+ EXPECT_GT(counter, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAppendableZstdFileTest
+ : public ::testing::Test
+{
+protected:
+ TTempFile GetLogFile()
+ {
+ return {GenerateLogFileName() + ".zst"};
+ }
+
+ TAppendableCompressedFilePtr CreateAppendableZstdFile(TFile rawFile, bool writeTruncateMessage)
+ {
+ return New<TAppendableCompressedFile>(
+ std::move(rawFile),
+ CreateZstdCompressionCodec(),
+ GetCurrentInvoker(),
+ writeTruncateMessage);
+ }
+
+ void WriteTestFile(const TString& filename, i64 addBytes, bool writeTruncateMessage)
+ {
+ {
+ TFile rawFile(filename, OpenAlways|RdWr|CloseOnExec);
+ auto file = CreateAppendableZstdFile(rawFile, writeTruncateMessage);
+ *file << "foo\n";
+ file->Flush();
+ *file << "bar\n";
+ file->Finish();
+
+ rawFile.Resize(rawFile.GetLength() + addBytes);
+ }
+ {
+ TFile rawFile(filename, OpenAlways|RdWr|CloseOnExec);
+ auto file = CreateAppendableZstdFile(rawFile, writeTruncateMessage);
+ *file << "zog\n";
+ file->Flush();
+ }
+ }
+
+ std::vector<char> GenerateIncompressibleData(i64 size)
+ {
+ std::vector<char> data(size);
+ for (int index = 0; index < size; index++) {
+ data[index] = rand();
+ }
+ return data;
+ }
+};
+
+TEST_F(TAppendableZstdFileTest, Write)
+{
+ auto logFile = GetLogFile();
+ WriteTestFile(logFile.Name(), 0, false);
+
+ TUnbufferedFileInput file(logFile.Name());
+ TZstdDecompress decompress(&file);
+ EXPECT_EQ("foo\nbar\nzog\n", decompress.ReadAll());
+}
+
+TEST_F(TAppendableZstdFileTest, WriteMultipleFramesPerFlush)
+{
+ auto logFile = GetLogFile();
+ auto data = GenerateIncompressibleData(5 * MaxZstdFrameUncompressedLength);
+
+ {
+ TFile rawFile(logFile.Name(), OpenAlways|RdWr|CloseOnExec);
+ auto file = CreateAppendableZstdFile(rawFile, true);
+ file->Write(data.data(), data.size());
+ file->Finish();
+ }
+
+ TUnbufferedFileInput file(logFile.Name());
+ TZstdDecompress decompress(&file);
+ auto decompressed = decompress.ReadAll();
+
+ EXPECT_EQ(data.size(), decompressed.size());
+ EXPECT_TRUE(std::equal(data.begin(), data.end(), decompressed.begin()));
+}
+
+TEST_F(TAppendableZstdFileTest, RepairSmall)
+{
+ auto logFile = GetLogFile();
+ WriteTestFile(logFile.Name(), -1, false);
+
+ TUnbufferedFileInput file(logFile.Name());
+ TZstdDecompress decompress(&file);
+ EXPECT_EQ("foo\nzog\n", decompress.ReadAll());
+}
+
+TEST_F(TAppendableZstdFileTest, RepairLarge)
+{
+ auto logFile = GetLogFile();
+ WriteTestFile(logFile.Name(), 10_MB, true);
+
+ TUnbufferedFileInput file(logFile.Name());
+ TZstdDecompress decompress(&file);
+
+ TStringBuilder expected;
+ expected.AppendFormat("foo\nbar\nTruncated %v bytes due to zstd repair.\nzog\n", 10_MB);
+ EXPECT_EQ(expected.Flush(), decompress.ReadAll());
+}
+
+TEST(TRandomAccessGZipTest, Write)
+{
+ TTempFile logFile(GenerateLogFileName() + ".gz");
+
+ {
+ TFile rawFile(logFile.Name(), OpenAlways|RdWr|CloseOnExec);
+ auto file = New<TRandomAccessGZipFile>(rawFile);
+ *file << "foo\n";
+ file->Flush();
+ *file << "bar\n";
+ file->Finish();
+ }
+ {
+ TFile rawFile(logFile.Name(), OpenAlways|RdWr|CloseOnExec);
+ auto file = New<TRandomAccessGZipFile>(rawFile);
+ *file << "zog\n";
+ file->Finish();
+ }
+
+ auto input = TUnbufferedFileInput(logFile.Name());
+ TZLibDecompress decompress(&input);
+ EXPECT_EQ("foo\nbar\nzog\n", decompress.ReadAll());
+}
+
+TEST(TRandomAccessGZipTest, RepairIncompleteBlocks)
+{
+ TTempFile logFile(GenerateLogFileName() + ".gz");
+
+ {
+ TFile rawFile(logFile.Name(), OpenAlways|RdWr|CloseOnExec);
+ auto file = New<TRandomAccessGZipFile>(rawFile);
+ *file << "foo\n";
+ file->Flush();
+ *file << "bar\n";
+ file->Finish();
+ }
+
+ i64 fullSize;
+ {
+ TFile file(logFile.Name(), OpenAlways|RdWr);
+ fullSize = file.GetLength();
+ file.Resize(fullSize - 1);
+ }
+
+ {
+ TFile rawFile(logFile.Name(), OpenAlways | RdWr | CloseOnExec);
+ auto file = New<TRandomAccessGZipFile>(rawFile);
+ }
+
+ {
+ TFile file(logFile.Name(), OpenAlways|RdWr);
+ EXPECT_LE(file.GetLength(), fullSize - 1);
+ }
+}
+
+// This test is for manual check of YT_LOG_FATAL
+TEST_F(TLoggingTest, DISABLED_LogFatal)
+{
+ TTempFile logFile(GenerateLogFileName());
+
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", logFile.Name()));
+
+ YT_LOG_INFO("Info message");
+
+ Sleep(TDuration::MilliSeconds(100));
+
+ YT_LOG_INFO("Info message");
+ YT_LOG_FATAL("FATAL");
+}
+
+// Windows does not support request tracing for now.
+#ifndef _win_
+TEST_F(TLoggingTest, RequestSuppression)
+{
+ TTempFile logFile(GenerateLogFileName());
+
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ "request_suppression_timeout" = 100;
+ })", logFile.Name()));
+
+ {
+ auto requestId = NTracing::TRequestId::Create();
+ auto traceContext = NTracing::TTraceContext::NewRoot("Test");
+ traceContext->SetRequestId(requestId);
+ NTracing::TTraceContextGuard guard(traceContext);
+
+ YT_LOG_INFO("Traced message");
+
+ TLogManager::Get()->SuppressRequest(requestId);
+ }
+
+ YT_LOG_INFO("Info message");
+
+ TLogManager::Get()->Synchronize();
+
+ auto lines = ReadPlainTextEvents(logFile.Name());
+ EXPECT_EQ(1, std::ssize(lines));
+ EXPECT_TRUE(lines[0].find("Info message") != TString::npos);
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLoggingTagsTest
+ : public ::testing::TestWithParam<std::tuple<bool, bool, bool, TString>>
+{ };
+
+TEST_P(TLoggingTagsTest, All)
+{
+ auto hasMessageTag = std::get<0>(GetParam());
+ auto hasLoggerTag = std::get<1>(GetParam());
+ auto hasTraceContext = std::get<2>(GetParam());
+ auto expected = std::get<3>(GetParam());
+
+ auto loggingContext = NLogging::GetLoggingContext();
+ if (hasTraceContext) {
+ loggingContext.TraceLoggingTag = TStringBuf("TraceContextTag");
+ }
+
+ auto logger = TLogger("Test");
+ if (hasLoggerTag) {
+ logger = logger.WithTag("LoggerTag");
+ }
+
+ if (hasMessageTag) {
+ EXPECT_EQ(
+ expected,
+ ToString(NLogging::NDetail::BuildLogMessage(
+ loggingContext,
+ logger,
+ "Log message (Value: %v)",
+ 123).MessageRef));
+ } else {
+ EXPECT_EQ(
+ expected,
+ ToString(NLogging::NDetail::BuildLogMessage(
+ loggingContext,
+ logger,
+ "Log message").MessageRef));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(ValueParametrized, TLoggingTagsTest,
+ ::testing::Values(
+ std::make_tuple(false, false, false, "Log message"),
+ std::make_tuple(false, false, true, "Log message (TraceContextTag)"),
+ std::make_tuple(false, true, false, "Log message (LoggerTag)"),
+ std::make_tuple(false, true, true, "Log message (LoggerTag, TraceContextTag)"),
+ std::make_tuple( true, false, false, "Log message (Value: 123)"),
+ std::make_tuple( true, false, true, "Log message (Value: 123, TraceContextTag)"),
+ std::make_tuple( true, true, false, "Log message (Value: 123, LoggerTag)"),
+ std::make_tuple( true, true, true, "Log message (Value: 123, LoggerTag, TraceContextTag)")));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLongMessagesTest
+ : public TLoggingTest
+{
+protected:
+ static constexpr int N = 500;
+ std::vector<TString> Chunks_;
+
+ TLongMessagesTest()
+ {
+ for (int i = 0; i < N; ++i) {
+ Chunks_.push_back(Format("PayloadPayloadPayloadPayloadPayload%v", i));
+ }
+ }
+
+ void ConfigureForLongMessages(const TString& fileName)
+ {
+ Configure(Format(R"({
+ rules = [
+ {
+ "min_level" = "info";
+ "max_level" = "info";
+ "writers" = [ "info" ];
+ };
+ ];
+ "writers" = {
+ "info" = {
+ "file_name" = "%v";
+ "type" = "file";
+ };
+ };
+ })", fileName));
+ }
+
+ void LogLongMessages()
+ {
+ for (int i = 0; i < N; ++i) {
+ YT_LOG_INFO("%v", MakeRange(Chunks_.data(), Chunks_.data() + i));
+ }
+ }
+
+ void CheckLongMessages(const TString& fileName)
+ {
+ TLogManager::Get()->Synchronize();
+
+ auto lines = ReadPlainTextEvents(fileName);
+ EXPECT_EQ(N, std::ssize(lines));
+ for (int i = 0; i < N; ++i) {
+ auto expected = Format("%v", MakeRange(Chunks_.data(), Chunks_.data() + i));
+ auto actual = lines[i];
+ EXPECT_NE(TString::npos, actual.find(expected));
+ }
+ }
+};
+
+TEST_F(TLongMessagesTest, WithPerThreadCache)
+{
+ TTempFile logFile(GenerateLogFileName());
+ ConfigureForLongMessages(logFile.Name());
+ LogLongMessages();
+ CheckLongMessages(logFile.Name());
+}
+
+TEST_F(TLongMessagesTest, WithoutPerThreadCache)
+{
+ TTempFile logFile(GenerateLogFileName());
+ ConfigureForLongMessages(logFile.Name());
+ std::thread thread([&] {
+ NLogging::NDetail::TMessageStringBuilder::DisablePerThreadCache();
+ LogLongMessages();
+ });
+ thread.join();
+ CheckLongMessages(logFile.Name());
+}
+
+TEST_F(TLoggingTest, Anchors)
+{
+ NLogging::TLogger logger;
+ NLogging::TLoggingContext context{};
+ EXPECT_EQ(NLogging::NDetail::BuildLogMessage(context, logger, "Simple message").Anchor, "Simple message");
+ EXPECT_EQ(NLogging::NDetail::BuildLogMessage(context, logger, "Simple message (Param: %v)", 1).Anchor, "Simple message (Param: %v)");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TTestWriterConfig)
+
+class TTestWriterConfig
+ : public TYsonStruct
+{
+public:
+ int Padding;
+
+ REGISTER_YSON_STRUCT(TTestWriterConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("padding", &TThis::Padding)
+ .GreaterThanOrEqual(0)
+ .Default(0);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTestWriterConfig)
+
+DECLARE_REFCOUNTED_CLASS(TTestWriter)
+
+class TTestWriter
+ : public ILogWriter
+{
+public:
+ explicit TTestWriter(TTestWriterConfigPtr config)
+ : Config_(std::move(config))
+ { }
+
+ void Write(const TLogEvent& event) override
+ {
+ if (event.Category == Logger.GetCategory()) {
+ Messages_.push_back(TString(Config_->Padding, ' ') + event.MessageRef.ToStringBuf());
+ }
+ }
+
+ void Flush() override
+ { }
+
+ void Reload() override
+ { }
+
+ void SetRateLimit(std::optional<i64> /*limit*/) override
+ { }
+
+ void SetCategoryRateLimits(const THashMap<TString, i64>& /*categoryRateLimits*/) override
+ { }
+
+ const std::vector<TString>& GetMessages() const
+ {
+ return Messages_;
+ }
+
+private:
+ const TTestWriterConfigPtr Config_;
+
+ std::vector<TString> Messages_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTestWriter)
+
+DECLARE_REFCOUNTED_CLASS(TTestWriterFactory)
+
+class TTestWriterFactory
+ : public ILogWriterFactory
+{
+public:
+ void ValidateConfig(const IMapNodePtr& configNode) override
+ {
+ ParseConfig(configNode);
+ }
+
+ ILogWriterPtr CreateWriter(
+ std::unique_ptr<ILogFormatter> /*formatter*/,
+ TString /*name*/,
+ const IMapNodePtr& configNode,
+ ILogWriterHost* /*host*/) noexcept override
+ {
+ EXPECT_FALSE(Writer_.operator bool());
+ Writer_ = New<TTestWriter>(ParseConfig(configNode));
+ return Writer_;
+ }
+
+ TTestWriterPtr GetWriter()
+ {
+ EXPECT_TRUE(Writer_.operator bool());
+ return Writer_;
+ }
+
+private:
+ TTestWriterPtr Writer_;
+
+ static TTestWriterConfigPtr ParseConfig(const IMapNodePtr& configNode)
+ {
+ return ConvertTo<TTestWriterConfigPtr>(configNode);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TTestWriterFactory)
+
+class TCustomWriterTest
+ : public TLoggingTest
+{
+protected:
+ static inline const TString CustomWriterType = "custom";
+ const TTestWriterFactoryPtr WriterFactory_ = New<TTestWriterFactory>();
+
+ void SetUp() override
+ {
+ TLogManager::Get()->RegisterWriterFactory(CustomWriterType, WriterFactory_);
+ }
+
+ void TearDown() override
+ {
+ TLogManager::Get()->UnregisterWriterFactory(CustomWriterType);
+ }
+};
+
+TEST_F(TCustomWriterTest, UnknownWriterType)
+{
+ EXPECT_THROW_WITH_SUBSTRING(
+ {
+ Configure(R"({
+ "rules" = [];
+ "writers" = {
+ "custom" = {
+ "type" = "unknown";
+ };
+ };
+ })");
+ },
+ "Unknown log writer type");
+}
+
+TEST_F(TCustomWriterTest, WriterConfigValidation)
+{
+ EXPECT_THROW_WITH_SUBSTRING(
+ {
+ Configure(Format(R"({
+ "rules" = [];
+ "writers" = {
+ "custom" = {
+ "type" = "%v";
+ "padding" = -10;
+ };
+ };
+ })", CustomWriterType));
+ },
+ "Expected >= 0, found -10");
+}
+
+TEST_F(TCustomWriterTest, Write)
+{
+ Configure(Format(R"({
+ "rules" = [
+ {
+ "min_level" = "info";
+ "writers" = [ "custom" ];
+ }
+ ];
+ "writers" = {
+ "custom" = {
+ "type" = "%v";
+ "padding" = 2;
+ };
+ };
+ })", CustomWriterType));
+
+ YT_LOG_INFO("first");
+ YT_LOG_INFO("second");
+ YT_LOG_INFO("third");
+
+ TLogManager::Get()->Synchronize();
+
+ auto writer = WriterFactory_->GetWriter();
+ const auto& messages = writer->GetMessages();
+ EXPECT_EQ(3, std::ssize(messages));
+ EXPECT_EQ(" first", messages[0]);
+ EXPECT_EQ(" second", messages[1]);
+ EXPECT_EQ(" third", messages[2]);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/unittests/ya.make b/yt/yt/core/logging/unittests/ya.make
new file mode 100644
index 0000000000..aa7c15d923
--- /dev/null
+++ b/yt/yt/core/logging/unittests/ya.make
@@ -0,0 +1,40 @@
+GTEST(unittester-core-logging)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ logging_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+ library/cpp/streams/zstd
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/logging/zstd_compression.cpp b/yt/yt/core/logging/zstd_compression.cpp
new file mode 100644
index 0000000000..fe7a3306f5
--- /dev/null
+++ b/yt/yt/core/logging/zstd_compression.cpp
@@ -0,0 +1,157 @@
+#include "zstd_compression.h"
+
+#include "compression.h"
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <contrib/libs/zstd/include/zstd.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// ZstdSyncTag is the constant part of a skippable frame appended after each zstd frame.
+// It is ignored by tools and allows positioning after last fully written frame upon file opening.
+constexpr const char ZstdSyncTag[] = {
+ '\x50', '\x2a', '\x4d', '\x18', // zstd skippable frame magic number
+ '\x18', '\x00', '\x00', '\x00', // data size: 128-bit ID + 64-bit offset
+
+ // 128-bit sync tag ID
+ '\xf6', '\x79', '\x9c', '\x4e', '\xd1', '\x09', '\x90', '\x7e',
+ '\x29', '\x91', '\xd9', '\xe6', '\xbe', '\xe4', '\x84', '\x40'
+
+ // 64-bit offset is written separately.
+};
+
+constexpr i64 MaxZstdFrameLength = ZSTD_COMPRESSBOUND(MaxZstdFrameUncompressedLength);
+constexpr i64 ZstdSyncTagLength = sizeof(ZstdSyncTag) + sizeof(ui64);
+constexpr i64 TailScanLength = MaxZstdFrameLength + 2 * ZstdSyncTagLength;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static std::optional<i64> FindSyncTag(const char* buf, size_t size, i64 offset)
+{
+ const char* syncTag = nullptr;
+ TStringBuf data(buf, size);
+ TStringBuf zstdSyncTagView(ZstdSyncTag, sizeof(ZstdSyncTag));
+ while (true) {
+ size_t tagPos = data.find(zstdSyncTagView);
+ if (tagPos == TStringBuf::npos) {
+ break;
+ }
+
+ const char* tag = data.data() + tagPos;
+ data.remove_prefix(tagPos + 1);
+
+ if (ZstdSyncTagLength - 1 > data.size()) {
+ continue;
+ }
+
+ ui64 tagOffset = *reinterpret_cast<const ui64*>(tag + sizeof(ZstdSyncTag));
+ ui64 tagOffsetExpected = offset + (tag - buf);
+
+ if (tagOffset == tagOffsetExpected) {
+ syncTag = tag;
+ }
+ }
+
+ if (!syncTag) {
+ return {};
+ }
+
+ return offset + (syncTag - buf);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZstdLogCompressionCodec
+ : public ILogCompressionCodec
+{
+public:
+ explicit TZstdLogCompressionCodec(int compressionLevel)
+ : CompressionLevel_(compressionLevel)
+ { }
+
+ i64 GetMaxBlockSize() const override
+ {
+ return MaxZstdFrameUncompressedLength;
+ }
+
+ void Compress(const TBuffer& input, TBuffer& output) override
+ {
+ auto context = ZSTD_createCCtx();
+ auto contextGuard = Finally([&] () {
+ ZSTD_freeCCtx(context);
+ });
+
+ auto frameLength = ZSTD_COMPRESSBOUND(std::min<i64>(MaxZstdFrameUncompressedLength, input.Size()));
+
+ output.Reserve(output.Size() + frameLength + ZstdSyncTagLength);
+ size_t size = ZSTD_compressCCtx(
+ context,
+ output.Data() + output.Size(),
+ frameLength,
+ input.Data(),
+ input.Size(),
+ CompressionLevel_);
+
+ if (ZSTD_isError(size)) {
+ THROW_ERROR_EXCEPTION("ZSTD_compressCCtx() failed")
+ << TErrorAttribute("zstd_error", ZSTD_getErrorName(size));
+ }
+ output.Advance(size);
+ }
+
+ void AddSyncTag(i64 offset, TBuffer& output) override
+ {
+ output.Append(ZstdSyncTag, sizeof(ZstdSyncTag));
+ output.Append(reinterpret_cast<const char*>(&offset), sizeof(offset));
+ }
+
+ void Repair(TFile* file, i64& outputPosition) override
+ {
+ constexpr i64 scanOverlap = ZstdSyncTagLength - 1;
+
+ i64 fileSize = file->GetLength();
+ i64 bufSize = fileSize;
+ i64 pos = Max<i64>(bufSize - TailScanLength, 0);
+ bufSize -= pos;
+
+ TBuffer buffer;
+
+ outputPosition = 0;
+ while (bufSize >= ZstdSyncTagLength) {
+ buffer.Resize(0);
+ buffer.Reserve(bufSize);
+
+ size_t sz = file->Pread(buffer.Data(), bufSize, pos);
+ buffer.Resize(sz);
+
+ std::optional<i64> off = FindSyncTag(buffer.Data(), buffer.Size(), pos);
+ if (off.has_value()) {
+ outputPosition = *off + ZstdSyncTagLength;
+ break;
+ }
+
+ i64 newPos = Max<i64>(pos - TailScanLength, 0);
+ bufSize = Max<i64>(pos + scanOverlap - newPos, 0);
+ pos = newPos;
+ }
+ file->Resize(outputPosition);
+ }
+
+private:
+ int CompressionLevel_;
+};
+
+DECLARE_REFCOUNTED_TYPE(TZstdLogCompressionCodec)
+DEFINE_REFCOUNTED_TYPE(TZstdLogCompressionCodec)
+
+ILogCompressionCodecPtr CreateZstdCompressionCodec(int compressionLevel)
+{
+ return New<TZstdLogCompressionCodec>(compressionLevel);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/logging/zstd_compression.h b/yt/yt/core/logging/zstd_compression.h
new file mode 100644
index 0000000000..c53e4f032c
--- /dev/null
+++ b/yt/yt/core/logging/zstd_compression.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/size_literals.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr i64 MaxZstdFrameUncompressedLength = 5_MBs;
+constexpr const int DefaultZstdCompressionLevel = 3;
+
+ILogCompressionCodecPtr CreateZstdCompressionCodec(int compressionLevel = DefaultZstdCompressionLevel);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/yt/yt/core/misc/arithmetic_formula.cpp b/yt/yt/core/misc/arithmetic_formula.cpp
new file mode 100644
index 0000000000..2134f4970a
--- /dev/null
+++ b/yt/yt/core/misc/arithmetic_formula.cpp
@@ -0,0 +1,1139 @@
+#include "arithmetic_formula.h"
+#include "phoenix.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/node.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+#include <util/generic/hash.h>
+
+namespace NYT {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EEvaluationContext,
+ (Boolean)
+ (Arithmetic)
+);
+
+bool IsSymbolAllowedInName(char c, EEvaluationContext context, bool isFirst)
+{
+ const static THashSet<char> extraAllowedBooleanVariableTokens{'/', '-', '.', ':'};
+
+ if (std::isalpha(c) || c == '_') {
+ return true;
+ }
+ if (isFirst) {
+ return false;
+ }
+ if (std::isdigit(c)) {
+ return true;
+ }
+ if (context == EEvaluationContext::Boolean && extraAllowedBooleanVariableTokens.contains(c)) {
+ return true;
+ }
+ return false;
+}
+
+void ValidateFormulaVariable(const TString& variable, EEvaluationContext context)
+{
+ if (variable.empty()) {
+ THROW_ERROR_EXCEPTION("Variable should not be empty");
+ }
+ for (char c : variable) {
+ if (!IsSymbolAllowedInName(c, context, false)) {
+ THROW_ERROR_EXCEPTION("Invalid character %Qv in variable %Qv", c, variable);
+ }
+ }
+ if (!IsSymbolAllowedInName(variable[0], context, true)) {
+ THROW_ERROR_EXCEPTION("Invalid first character in variable %Qv", variable);
+ }
+ if (variable == "in") {
+ THROW_ERROR_EXCEPTION("Invalid variable name %Qv", variable);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateArithmeticFormulaVariable(const TString& variable)
+{
+ ValidateFormulaVariable(variable, EEvaluationContext::Arithmetic);
+}
+
+void ValidateBooleanFormulaVariable(const TString& variable)
+{
+ ValidateFormulaVariable(variable, EEvaluationContext::Boolean);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void ThrowError(const TString& formula, int position, const TString& message, EEvaluationContext evaluationContext)
+{
+ const static int maxContextSize = 30;
+
+ int contextStart = std::max(0, position - maxContextSize / 2);
+ TString context = formula.substr(contextStart, maxContextSize);
+ int contextPosition = std::min(position, maxContextSize / 2);
+
+ TStringBuilder builder;
+ builder.AppendFormat(
+ "Error while parsing %v formula:\n%v\n",
+ evaluationContext == EEvaluationContext::Arithmetic ? "arithmetic" : "boolean",
+ formula);
+ builder.AppendChar(' ', position);
+ builder.AppendFormat("^\n%v", message);
+ THROW_ERROR_EXCEPTION(builder.Flush())
+ << TErrorAttribute("context", context)
+ << TErrorAttribute("context_pos", contextPosition);
+}
+
+// NB: 'Set' type cannot appear in parsed/tokenized formula, it is needed only for CheckTypeConsistency.
+#define FOR_EACH_TOKEN(func) \
+ func(0, Variable) \
+ func(0, Number) \
+ func(0, BooleanLiteral) \
+ func(0, Set) \
+ func(0, LeftBracket) \
+ func(0, RightBracket) \
+ func(1, In) \
+ func(2, Comma) \
+ func(3, LogicalOr) \
+ func(4, LogicalAnd) \
+ func(5, BitwiseOr) \
+ func(6, BitwiseXor) \
+ func(7, BitwiseAnd) \
+ func(8, Equals) \
+ func(8, NotEquals) \
+ func(9, Less) \
+ func(9, Greater) \
+ func(9, LessOrEqual) \
+ func(9, GreaterOrEqual) \
+ func(10, Plus) \
+ func(10, Minus) \
+ func(11, Multiplies) \
+ func(11, Divides) \
+ func(11, Modulus) \
+ func(12, LogicalNot)
+
+#define EXTRACT_FIELD_NAME(x, y) (y)
+#define EXTRACT_PRECEDENCE(x, y) x,
+
+DEFINE_ENUM(EFormulaTokenType,
+ FOR_EACH_TOKEN(EXTRACT_FIELD_NAME)
+);
+
+static int Precedence(EFormulaTokenType type) {
+ constexpr static int precedence[] =
+ {
+ FOR_EACH_TOKEN(EXTRACT_PRECEDENCE)
+ };
+ int index = static_cast<int>(type);
+ YT_VERIFY(0 <= index && index < static_cast<int>(sizeof(precedence) / sizeof(*precedence)));
+ return precedence[index];
+}
+
+#undef FOR_EACH_TOKEN
+#undef EXTRACT_FIELD_NAME
+#undef EXTRACT_PRECEDENCE
+
+struct TFormulaToken
+{
+ EFormulaTokenType Type;
+ int Position;
+ TString Name;
+ i64 Number = 0;
+};
+
+bool operator==(const TFormulaToken& lhs, const TFormulaToken& rhs)
+{
+ return lhs.Type == rhs.Type && lhs.Name == rhs.Name && lhs.Number == rhs.Number;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGenericFormulaImpl
+ : public TRefCounted
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(TString, Formula);
+ DEFINE_BYVAL_RO_PROPERTY(size_t, Hash);
+
+public:
+ TGenericFormulaImpl(const TString& formula, size_t hash, std::vector<TFormulaToken> parsedFormula);
+
+ bool operator==(const TGenericFormulaImpl& other) const;
+
+ bool IsEmpty() const;
+
+ int Size() const;
+
+ i64 Eval(const THashMap<TString, i64>& values, EEvaluationContext context) const;
+
+ THashSet<TString> GetVariables() const;
+
+private:
+ std::vector<TFormulaToken> ParsedFormula_;
+
+ static std::vector<TFormulaToken> Tokenize(const TString& formula, EEvaluationContext context);
+ static std::vector<TFormulaToken> Parse(
+ const TString& formula,
+ const std::vector<TFormulaToken>& tokens,
+ EEvaluationContext context);
+ static size_t CalculateHash(const std::vector<TFormulaToken>& tokens);
+ static void CheckTypeConsistency(
+ const TString& formula,
+ const std::vector<TFormulaToken>& tokens,
+ EEvaluationContext context);
+
+ friend TIntrusivePtr<TGenericFormulaImpl> MakeGenericFormulaImpl(const TString& formula, EEvaluationContext context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGenericFormulaImpl::TGenericFormulaImpl(
+ const TString& formula,
+ size_t hash,
+ std::vector<TFormulaToken> parsedFormula)
+ : Formula_(formula)
+ , Hash_(hash)
+ , ParsedFormula_(std::move(parsedFormula))
+{ }
+
+bool TGenericFormulaImpl::operator==(const TGenericFormulaImpl& other) const
+{
+ return std::equal(
+ ParsedFormula_.begin(),
+ ParsedFormula_.end(),
+ other.ParsedFormula_.begin(),
+ other.ParsedFormula_.end());
+}
+
+bool TGenericFormulaImpl::IsEmpty() const
+{
+ return ParsedFormula_.empty();
+}
+
+int TGenericFormulaImpl::Size() const
+{
+ return ParsedFormula_.size();
+}
+
+i64 TGenericFormulaImpl::Eval(const THashMap<TString, i64>& values, EEvaluationContext context) const
+{
+ auto variableValue = [&] (const TString& var) -> i64 {
+ auto iter = values.find(var);
+ if (iter == values.end()) {
+ if (context == EEvaluationContext::Boolean) {
+ return 0;
+ } else {
+ THROW_ERROR_EXCEPTION("Undefined variable %Qv", var)
+ << TErrorAttribute("formula", Formula_)
+ << TErrorAttribute("values", values);
+ }
+ }
+ return iter->second;
+ };
+
+#define APPLY_BINARY_OP(op) \
+ YT_VERIFY(stack.size() >= 2); \
+ { \
+ YT_VERIFY(std::holds_alternative<i64>(stack.back())); \
+ auto top = std::get<i64>(stack.back()); \
+ stack.pop_back(); \
+ YT_VERIFY(std::holds_alternative<i64>(stack.back())); \
+ stack.back() = static_cast<i64>(std::get<i64>(stack.back()) op top); \
+ }
+
+ std::vector<std::variant<i64, std::vector<i64>>> stack;
+ for (const auto& token : ParsedFormula_) {
+ switch (token.Type) {
+ case EFormulaTokenType::Variable:
+ stack.push_back(variableValue(token.Name));
+ break;
+ case EFormulaTokenType::Number:
+ case EFormulaTokenType::BooleanLiteral:
+ stack.push_back(token.Number);
+ break;
+ case EFormulaTokenType::LogicalNot:
+ YT_VERIFY(!stack.empty());
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ stack.back() = static_cast<i64>(!std::get<i64>(stack.back()));
+ break;
+ case EFormulaTokenType::LogicalOr:
+ APPLY_BINARY_OP(||);
+ break;
+ case EFormulaTokenType::LogicalAnd:
+ APPLY_BINARY_OP(&&);
+ break;
+ case EFormulaTokenType::BitwiseOr:
+ APPLY_BINARY_OP(|);
+ break;
+ case EFormulaTokenType::BitwiseXor:
+ APPLY_BINARY_OP(^);
+ break;
+ case EFormulaTokenType::BitwiseAnd:
+ APPLY_BINARY_OP(&);
+ break;
+ case EFormulaTokenType::Equals:
+ APPLY_BINARY_OP(==);
+ break;
+ case EFormulaTokenType::NotEquals:
+ APPLY_BINARY_OP(!=);
+ break;
+ case EFormulaTokenType::Less:
+ APPLY_BINARY_OP(<);
+ break;
+ case EFormulaTokenType::Greater:
+ APPLY_BINARY_OP(>);
+ break;
+ case EFormulaTokenType::LessOrEqual:
+ APPLY_BINARY_OP(<=);
+ break;
+ case EFormulaTokenType::GreaterOrEqual:
+ APPLY_BINARY_OP(>=);
+ break;
+ case EFormulaTokenType::Plus:
+ APPLY_BINARY_OP(+);
+ break;
+ case EFormulaTokenType::Minus:
+ APPLY_BINARY_OP(-);
+ break;
+ case EFormulaTokenType::Multiplies:
+ APPLY_BINARY_OP(*);
+ break;
+ case EFormulaTokenType::Divides:
+ case EFormulaTokenType::Modulus:
+ YT_VERIFY(stack.size() >= 2);
+ {
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ i64 top = std::get<i64>(stack.back());
+ if (top == 0) {
+ THROW_ERROR_EXCEPTION("Division by zero in formula %Qv", Formula_)
+ << TErrorAttribute("values", values);
+ }
+ stack.pop_back();
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ if (std::get<i64>(stack.back()) == std::numeric_limits<i64>::min() && top == -1) {
+ THROW_ERROR_EXCEPTION("Division of INT64_MIN by -1 in formula %Qv", Formula_)
+ << TErrorAttribute("values", values);
+ }
+ if (token.Type == EFormulaTokenType::Divides) {
+ stack.back() = std::get<i64>(stack.back()) / top;
+ } else {
+ stack.back() = std::get<i64>(stack.back()) % top;
+ }
+ }
+ break;
+ case EFormulaTokenType::Comma:
+ YT_VERIFY(stack.size() >= 2);
+ {
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ i64 top = std::get<i64>(stack.back());
+ stack.pop_back();
+ Visit(stack.back(),
+ [&] (i64 v) {
+ std::vector<i64> vector{v, top};
+ stack.back() = vector;
+ },
+ [&] (std::vector<i64>& v) {
+ v.push_back(top);
+ });
+ }
+ break;
+ case EFormulaTokenType::In:
+ YT_VERIFY(stack.size() >= 2);
+ {
+ auto set = std::holds_alternative<i64>(stack.back())
+ ? std::vector<i64>{std::get<i64>(stack.back())}
+ : std::move(std::get<std::vector<i64>>(stack.back()));
+ stack.pop_back();
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ i64 element = std::get<i64>(stack.back());
+ stack.pop_back();
+ stack.push_back(static_cast<i64>(std::find(
+ set.begin(),
+ set.end(),
+ element) != set.end()));
+ }
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+ if (stack.empty()) {
+ if (context == EEvaluationContext::Arithmetic) {
+ THROW_ERROR_EXCEPTION("Empty arithmetic formula cannot be evaluated");
+ }
+ return true;
+ }
+ YT_VERIFY(stack.size() == 1);
+ YT_VERIFY(std::holds_alternative<i64>(stack.back()));
+ return std::get<i64>(stack[0]);
+
+#undef APPLY_BINARY_OP
+}
+
+THashSet<TString> TGenericFormulaImpl::GetVariables() const
+{
+ THashSet<TString> variables;
+ for (const auto& token : ParsedFormula_) {
+ if (token.Type == EFormulaTokenType::Variable) {
+ variables.insert(token.Name);
+ }
+ }
+ return variables;
+}
+
+std::vector<TFormulaToken> TGenericFormulaImpl::Tokenize(const TString& formula, EEvaluationContext context)
+{
+ std::vector<TFormulaToken> result;
+ size_t pos = 0;
+
+ auto throwError = [&] (int position, const TString& message) {
+ ThrowError(formula, position, message, context);
+ };
+
+ auto skipWhitespace = [&] {
+ while (pos < formula.size() && std::isspace(formula[pos])) {
+ ++pos;
+ }
+ };
+
+ auto extractSpecialToken = [&] () -> std::optional<EFormulaTokenType> {
+ char first = formula[pos];
+ char second = pos + 1 < formula.size() ? formula[pos + 1] : '\0';
+ if (first == 'i' && second == 'n') {
+ char third = pos + 2 < formula.size() ? formula[pos + 2] : '\0';
+ if (IsSymbolAllowedInName(third, context, false)) {
+ return std::nullopt;
+ } else {
+ pos += 2;
+ return EFormulaTokenType::In;
+ }
+ }
+ switch (first) {
+ case '^':
+ ++pos;
+ return EFormulaTokenType::BitwiseXor;
+ case '+':
+ ++pos;
+ return EFormulaTokenType::Plus;
+ case '-':
+ ++pos;
+ return EFormulaTokenType::Minus;
+ case '*':
+ ++pos;
+ return EFormulaTokenType::Multiplies;
+ case '/':
+ ++pos;
+ return EFormulaTokenType::Divides;
+ case '%':
+ ++pos;
+ return EFormulaTokenType::Modulus;
+ case '(':
+ ++pos;
+ return EFormulaTokenType::LeftBracket;
+ case ')':
+ ++pos;
+ return EFormulaTokenType::RightBracket;
+ case ',':
+ ++pos;
+ return EFormulaTokenType::Comma;
+ case '=':
+ if (second != '=') {
+ throwError(pos + 1, "Unexpected character");
+ }
+ pos += 2;
+ return EFormulaTokenType::Equals;
+ case '!':
+ switch (second) {
+ case '=':
+ pos += 2;
+ return EFormulaTokenType::NotEquals;
+ default:
+ ++pos;
+ return EFormulaTokenType::LogicalNot;
+ }
+ case '&':
+ switch (second) {
+ case '&':
+ pos += 2;
+ return EFormulaTokenType::LogicalAnd;
+ default:
+ ++pos;
+ return EFormulaTokenType::BitwiseAnd;
+ }
+ case '|':
+ switch (second) {
+ case '|':
+ pos += 2;
+ return EFormulaTokenType::LogicalOr;
+ default:
+ ++pos;
+ return EFormulaTokenType::BitwiseOr;
+ }
+ case '<':
+ switch (second) {
+ case '=':
+ pos += 2;
+ return EFormulaTokenType::LessOrEqual;
+ default:
+ ++pos;
+ return EFormulaTokenType::Less;
+ }
+ case '>':
+ switch (second) {
+ case '=':
+ pos += 2;
+ return EFormulaTokenType::GreaterOrEqual;
+ default:
+ ++pos;
+ return EFormulaTokenType::Greater;
+ }
+ default:
+ return std::nullopt;
+ }
+ };
+
+ auto extractNumber = [&] {
+ size_t start = pos;
+ if (formula[pos] == '-') {
+ ++pos;
+ }
+ if (pos == formula.size() || !std::isdigit(formula[pos])) {
+ throwError(pos, "Expected digit");
+ }
+ while (pos < formula.size() && std::isdigit(formula[pos])) {
+ ++pos;
+ }
+ return IntFromString<i64, 10>(TStringBuf(formula, start, pos - start));
+ };
+
+ auto extractVariable = [&] {
+ TString name;
+ while (pos < formula.size() && IsSymbolAllowedInName(formula[pos], context, name.empty())) {
+ name += formula[pos++];
+ }
+ return name;
+ };
+
+ auto extractBooleanLiteral = [&] {
+ YT_VERIFY(formula[pos] == '%');
+ ++pos;
+
+ size_t start = pos;
+ while (pos < formula.size() && std::isalpha(formula[pos])) {
+ ++pos;
+ }
+
+ TStringBuf buf(formula, start, pos - start);
+ if (buf == "false") {
+ return 0;
+ } else if (buf == "true") {
+ return 1;
+ } else {
+ throwError(pos, Format("Invalid literal %Qv", buf));
+ YT_ABORT();
+ }
+ };
+
+ bool expectBinaryOperator = false;
+
+ while (pos < formula.size()) {
+ char c = formula[pos];
+ if (std::isspace(c)) {
+ skipWhitespace();
+ if (pos == formula.size()) {
+ break;
+ }
+ c = formula[pos];
+ }
+
+ TFormulaToken token;
+ token.Position = pos;
+
+ if (std::isdigit(c) || (c == '-' && !expectBinaryOperator)) {
+ token.Type = EFormulaTokenType::Number;
+ token.Number = extractNumber();
+ expectBinaryOperator = true;
+ } else if (!expectBinaryOperator && c == '%') {
+ token.Type = EFormulaTokenType::BooleanLiteral;
+ token.Number = extractBooleanLiteral();
+ expectBinaryOperator = true;
+ } else if (auto optionalType = extractSpecialToken()) {
+ token.Type = *optionalType;
+ expectBinaryOperator = token.Type == EFormulaTokenType::RightBracket;
+ } else if (IsSymbolAllowedInName(formula[pos], context, true)) {
+ token.Type = EFormulaTokenType::Variable;
+ token.Name = extractVariable();
+ expectBinaryOperator = true;
+ } else {
+ throwError(pos, "Unexpected character");
+ }
+
+ result.push_back(token);
+ }
+
+ return result;
+}
+
+std::vector<TFormulaToken> TGenericFormulaImpl::Parse(
+ const TString& formula,
+ const std::vector<TFormulaToken>& tokens,
+ EEvaluationContext context)
+{
+ std::vector<TFormulaToken> result;
+ std::vector<TFormulaToken> stack;
+ bool expectSubformula = true;
+
+ if (tokens.empty()) {
+ return result;
+ }
+
+ auto throwError = [&] (int position, const TString& message) {
+ ThrowError(formula, position, message, context);
+ };
+
+ auto finishSubformula = [&] () {
+ while (!stack.empty() && stack.back().Type != EFormulaTokenType::LeftBracket) {
+ result.push_back(stack.back());
+ stack.pop_back();
+ }
+ };
+
+ auto processBinaryOp = [&] (const TFormulaToken& token) {
+ while (!stack.empty() && Precedence(stack.back().Type) >= Precedence(token.Type)) {
+ result.push_back(stack.back());
+ stack.pop_back();
+ }
+ };
+
+ for (const auto& token : tokens) {
+ switch (token.Type) {
+ case EFormulaTokenType::Variable:
+ case EFormulaTokenType::Number:
+ case EFormulaTokenType::BooleanLiteral:
+ if (!expectSubformula) {
+ throwError(token.Position, "Unexpected variable");
+ }
+ result.push_back(token);
+ expectSubformula = false;
+ break;
+
+ case EFormulaTokenType::LogicalNot:
+ case EFormulaTokenType::LeftBracket:
+ if (!expectSubformula) {
+ throwError(token.Position, "Unexpected token");
+ }
+ stack.push_back(token);
+ break;
+
+ case EFormulaTokenType::RightBracket:
+ if (expectSubformula) {
+ throwError(token.Position, "Unexpected token");
+ }
+ finishSubformula();
+ if (stack.empty()) {
+ throwError(token.Position, "Unmatched ')'");
+ }
+ stack.pop_back();
+ break;
+
+ default:
+ if (expectSubformula) {
+ throwError(token.Position, "Unexpected token");
+ }
+ processBinaryOp(token);
+ stack.push_back(token);
+ expectSubformula = true;
+ break;
+ }
+ }
+
+ if (expectSubformula) {
+ throwError(formula.size(), "Unfinished formula");
+ }
+ finishSubformula();
+ if (!stack.empty()) {
+ throwError(stack.back().Position, "Unmatched '('");
+ }
+
+ if (context == EEvaluationContext::Boolean) {
+ for (const auto& token : result) {
+ switch (token.Type) {
+ case EFormulaTokenType::BitwiseAnd:
+ case EFormulaTokenType::BitwiseOr:
+ case EFormulaTokenType::LogicalNot:
+ case EFormulaTokenType::Variable:
+ case EFormulaTokenType::BooleanLiteral:
+ break;
+ default:
+ throwError(
+ token.Position,
+ "Invalid token in boolean formula (only '!', '&', '|', '(', ')', '%false', '%true' are allowed)");
+ YT_ABORT();
+ }
+ }
+ }
+
+ CheckTypeConsistency(formula, result, context);
+
+ return result;
+}
+
+size_t TGenericFormulaImpl::CalculateHash(const std::vector<TFormulaToken>& tokens)
+{
+ size_t result = 0x18a92ea497f9bb1e;
+
+ for (const auto& token : tokens) {
+ HashCombine(result, static_cast<size_t>(token.Type));
+ HashCombine(result, ComputeHash(token.Name));
+ HashCombine(result, static_cast<size_t>(token.Number));
+ }
+
+ return result;
+}
+
+void TGenericFormulaImpl::CheckTypeConsistency(
+ const TString& formula,
+ const std::vector<TFormulaToken>& tokens,
+ EEvaluationContext context)
+{
+ auto validateIsNumber = [&] (EFormulaTokenType type, int position) {
+ if (type != EFormulaTokenType::Number) {
+ ThrowError(formula, position, "Type mismatch: expected \"number\", got \"set\"", context);
+ }
+ };
+
+ std::vector<EFormulaTokenType> stack;
+ for (const auto& token : tokens) {
+ switch (token.Type) {
+ case EFormulaTokenType::Variable:
+ case EFormulaTokenType::Number:
+ case EFormulaTokenType::BooleanLiteral:
+ stack.push_back(EFormulaTokenType::Number);
+ break;
+ case EFormulaTokenType::LogicalNot:
+ YT_VERIFY(!stack.empty());
+ validateIsNumber(stack.back(), token.Position);
+ break;
+ case EFormulaTokenType::LogicalOr:
+ case EFormulaTokenType::LogicalAnd:
+ case EFormulaTokenType::BitwiseOr:
+ case EFormulaTokenType::BitwiseXor:
+ case EFormulaTokenType::BitwiseAnd:
+ case EFormulaTokenType::Equals:
+ case EFormulaTokenType::NotEquals:
+ case EFormulaTokenType::Less:
+ case EFormulaTokenType::Greater:
+ case EFormulaTokenType::LessOrEqual:
+ case EFormulaTokenType::GreaterOrEqual:
+ case EFormulaTokenType::Plus:
+ case EFormulaTokenType::Minus:
+ case EFormulaTokenType::Multiplies:
+ case EFormulaTokenType::Divides:
+ case EFormulaTokenType::Modulus:
+ YT_VERIFY(stack.size() >= 2);
+ validateIsNumber(stack.back(), token.Position);
+ stack.pop_back();
+ validateIsNumber(stack.back(), token.Position);
+ break;
+ case EFormulaTokenType::Comma:
+ YT_VERIFY(stack.size() >= 2);
+ validateIsNumber(stack.back(), token.Position);
+ stack.pop_back();
+ stack.back() = EFormulaTokenType::Set;
+ break;
+ case EFormulaTokenType::In:
+ YT_VERIFY(stack.size() >= 2);
+ stack.pop_back();
+ validateIsNumber(stack.back(), token.Position);
+ break;
+ default:
+ YT_ABORT();
+ }
+ }
+ if (!stack.empty()) {
+ validateIsNumber(stack.back(), 0);
+ }
+}
+
+TIntrusivePtr<TGenericFormulaImpl> MakeGenericFormulaImpl(const TString& formula, EEvaluationContext context)
+{
+ auto tokens = TGenericFormulaImpl::Tokenize(formula, context);
+ auto parsed = TGenericFormulaImpl::Parse(formula, tokens, context);
+ auto hash = TGenericFormulaImpl::CalculateHash(parsed);
+ return New<TGenericFormulaImpl>(formula, hash, parsed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TArithmeticFormula::TArithmeticFormula()
+ : Impl_(MakeGenericFormulaImpl(TString(), EEvaluationContext::Arithmetic))
+{ }
+
+TArithmeticFormula::TArithmeticFormula(TIntrusivePtr<TGenericFormulaImpl> impl)
+ : Impl_(std::move(impl))
+{ }
+
+TArithmeticFormula::TArithmeticFormula(const TArithmeticFormula& other) = default;
+TArithmeticFormula::TArithmeticFormula(TArithmeticFormula&& other) = default;
+TArithmeticFormula& TArithmeticFormula::operator=(const TArithmeticFormula& other) = default;
+TArithmeticFormula& TArithmeticFormula::operator=(TArithmeticFormula&& other) = default;
+TArithmeticFormula::~TArithmeticFormula() = default;
+
+bool TArithmeticFormula::operator==(const TArithmeticFormula& other) const
+{
+ return *Impl_ == *other.Impl_;
+}
+
+bool TArithmeticFormula::IsEmpty() const
+{
+ return Impl_->IsEmpty();
+}
+
+int TArithmeticFormula::Size() const
+{
+ return Impl_->Size();
+}
+
+size_t TArithmeticFormula::GetHash() const
+{
+ return Impl_->GetHash();
+}
+
+TString TArithmeticFormula::GetFormula() const
+{
+ return Impl_->GetFormula();
+}
+
+i64 TArithmeticFormula::Eval(const THashMap<TString, i64>& values) const
+{
+ return Impl_->Eval(values, EEvaluationContext::Arithmetic);
+}
+
+THashSet<TString> TArithmeticFormula::GetVariables() const
+{
+ return Impl_->GetVariables();
+}
+
+TArithmeticFormula MakeArithmeticFormula(const TString& formula)
+{
+ auto impl = MakeGenericFormulaImpl(formula, EEvaluationContext::Arithmetic);
+ return TArithmeticFormula(std::move(impl));
+}
+
+void Serialize(const TArithmeticFormula& arithmeticFormula, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .Value(arithmeticFormula.GetFormula());
+}
+
+void Deserialize(TArithmeticFormula& arithmeticFormula, NYTree::INodePtr node)
+{
+ arithmeticFormula = MakeArithmeticFormula(node->AsString()->GetValue());
+}
+
+void TArithmeticFormula::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, GetFormula());
+}
+
+void TArithmeticFormula::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ auto formula = Load<TString>(context);
+ Impl_ = MakeGenericFormulaImpl(formula, EEvaluationContext::Arithmetic);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBooleanFormulaTags::TBooleanFormulaTags(THashSet<TString> tags)
+ : Tags_(std::move(tags))
+{
+ for (const auto& key: Tags_) {
+ PreparedTags_[key] = 1;
+ }
+}
+
+const THashSet<TString>& TBooleanFormulaTags::GetSourceTags() const
+{
+ return Tags_;
+}
+
+void TBooleanFormulaTags::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, Tags_);
+}
+
+void TBooleanFormulaTags::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ *this = TBooleanFormulaTags(Load<THashSet<TString>>(context));
+}
+
+bool TBooleanFormulaTags::operator==(const TBooleanFormulaTags& other) const
+{
+ return Tags_ == other.Tags_;
+}
+
+bool TBooleanFormulaTags::operator!=(const TBooleanFormulaTags& other) const
+{
+ return !operator==(other);
+}
+
+void Serialize(const TBooleanFormulaTags& tags, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .Value(tags.GetSourceTags());
+}
+
+void Deserialize(TBooleanFormulaTags& tags, NYTree::INodePtr node)
+{
+ tags = TBooleanFormulaTags(ConvertTo<THashSet<TString>>(node));
+}
+
+TString ToString(const TBooleanFormulaTags& tags)
+{
+ return ToStringViaBuilder(tags);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TBooleanFormulaTags& tags, TStringBuf /* format */)
+{
+ builder->AppendFormat("%v", tags.GetSourceTags());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBooleanFormula::TBooleanFormula()
+ : Impl_(MakeGenericFormulaImpl(TString(), EEvaluationContext::Boolean))
+{ }
+
+TBooleanFormula::TBooleanFormula(TIntrusivePtr<TGenericFormulaImpl> impl)
+ : Impl_(std::move(impl))
+{ }
+
+TBooleanFormula::TBooleanFormula(const TBooleanFormula& other) = default;
+TBooleanFormula::TBooleanFormula(TBooleanFormula&& other) = default;
+TBooleanFormula& TBooleanFormula::operator=(const TBooleanFormula& other) = default;
+TBooleanFormula& TBooleanFormula::operator=(TBooleanFormula&& other) = default;
+TBooleanFormula::~TBooleanFormula() = default;
+
+bool TBooleanFormula::operator==(const TBooleanFormula& other) const
+{
+ return *Impl_ == *other.Impl_;
+}
+
+bool TBooleanFormula::IsEmpty() const
+{
+ return Impl_->IsEmpty();
+}
+
+int TBooleanFormula::Size() const
+{
+ return Impl_->Size();
+}
+
+size_t TBooleanFormula::GetHash() const
+{
+ return Impl_->GetHash();
+}
+
+TString TBooleanFormula::GetFormula() const
+{
+ return Impl_->GetFormula();
+}
+
+bool TBooleanFormula::IsSatisfiedBy(const std::vector<TString>& value) const
+{
+ THashMap<TString, i64> values;
+ for (const auto& key: value) {
+ values[key] = 1;
+ }
+ return Impl_->Eval(values, EEvaluationContext::Boolean);
+}
+
+bool TBooleanFormula::IsSatisfiedBy(const THashSet<TString>& value) const
+{
+ return IsSatisfiedBy(std::vector<TString>(value.begin(), value.end()));
+}
+
+bool TBooleanFormula::IsSatisfiedBy(const TBooleanFormulaTags& tags) const
+{
+ return Impl_->Eval(tags.PreparedTags_, EEvaluationContext::Boolean);
+}
+
+TBooleanFormula MakeBooleanFormula(const TString& formula)
+{
+ auto impl = MakeGenericFormulaImpl(formula, EEvaluationContext::Boolean);
+ return TBooleanFormula(std::move(impl));
+}
+
+TBooleanFormula operator&(const TBooleanFormula& lhs, const TBooleanFormula& rhs)
+{
+ if (lhs.IsEmpty()) {
+ return rhs;
+ }
+ if (rhs.IsEmpty()) {
+ return lhs;
+ }
+ return MakeBooleanFormula(Format("(%v) & (%v)", lhs.GetFormula(), rhs.GetFormula()));
+}
+
+TBooleanFormula operator|(const TBooleanFormula& lhs, const TBooleanFormula& rhs)
+{
+ if (lhs.IsEmpty() || rhs.IsEmpty()) {
+ return MakeBooleanFormula("%true");
+ }
+ return MakeBooleanFormula(Format("(%v) | (%v)", lhs.GetFormula(), rhs.GetFormula()));
+}
+
+TBooleanFormula operator!(const TBooleanFormula& formula)
+{
+ if (formula.IsEmpty()) {
+ return MakeBooleanFormula("%false");
+ }
+ return MakeBooleanFormula(Format("!(%v)", formula.GetFormula()));
+}
+
+void Serialize(const TBooleanFormula& booleanFormula, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .Value(booleanFormula.GetFormula());
+}
+
+void Deserialize(TBooleanFormula& booleanFormula, NYTree::INodePtr node)
+{
+ booleanFormula = MakeBooleanFormula(node->AsString()->GetValue());
+}
+
+void Deserialize(TBooleanFormula& booleanFormula, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("TBooleanFormula", *cursor, EYsonItemType::StringValue);
+ booleanFormula = MakeBooleanFormula(ExtractTo<TString>(cursor));
+}
+
+void TBooleanFormula::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, GetFormula());
+}
+
+void TBooleanFormula::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ auto formula = Load<TString>(context);
+ Impl_ = MakeGenericFormulaImpl(formula, EEvaluationContext::Boolean);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTimeFormula::TTimeFormula() = default;
+TTimeFormula::TTimeFormula(const TTimeFormula& other) = default;
+TTimeFormula::TTimeFormula(TTimeFormula&& other) = default;
+TTimeFormula& TTimeFormula::operator=(const TTimeFormula& other) = default;
+TTimeFormula& TTimeFormula::operator=(TTimeFormula&& other) = default;
+TTimeFormula::~TTimeFormula() = default;
+
+bool TTimeFormula::operator==(const TTimeFormula& other) const
+{
+ return Formula_ == other.Formula_;
+}
+
+bool TTimeFormula::IsEmpty() const
+{
+ return Formula_.IsEmpty();
+}
+
+int TTimeFormula::Size() const
+{
+ return Formula_.Size();
+}
+
+size_t TTimeFormula::GetHash() const
+{
+ return Formula_.GetHash();
+}
+
+TString TTimeFormula::GetFormula() const
+{
+ return Formula_.GetFormula();
+}
+
+bool TTimeFormula::IsSatisfiedBy(TInstant time) const
+{
+ struct tm tm;
+ time.LocalTime(&tm);
+ return Formula_.Eval({
+ {"hours", tm.tm_hour},
+ {"minutes", tm.tm_min}}) != 0;
+}
+
+TTimeFormula::TTimeFormula(TArithmeticFormula&& arithmeticFormula)
+ : Formula_(std::move(arithmeticFormula))
+{ }
+
+TTimeFormula MakeTimeFormula(const TString& formula)
+{
+ const static THashSet<TString> allowedVariables{"minutes", "hours"};
+
+ auto arithmeticFormula = MakeArithmeticFormula(formula);
+
+ for (const auto& variable : arithmeticFormula.GetVariables()) {
+ if (!allowedVariables.contains(variable)) {
+ THROW_ERROR_EXCEPTION("Invalid variable %Qv in time formula %Qv",
+ variable,
+ formula);
+ }
+ }
+ return TTimeFormula{std::move(arithmeticFormula)};
+}
+
+void Serialize(const TTimeFormula& timeFormula, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .Value(timeFormula.GetFormula());
+}
+
+void Deserialize(TTimeFormula& timeFormula, INodePtr node)
+{
+ timeFormula = MakeTimeFormula(node->AsString()->GetValue());
+}
+
+void Deserialize(TTimeFormula& timeFormula, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("TTimeFormula", *cursor, EYsonItemType::StringValue);
+ timeFormula = MakeTimeFormula(ExtractTo<TString>(cursor));
+}
+
+void TTimeFormula::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+ Save(context, Formula_);
+}
+
+void TTimeFormula::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+ Formula_ = Load<TArithmeticFormula>(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/arithmetic_formula.h b/yt/yt/core/misc/arithmetic_formula.h
new file mode 100644
index 0000000000..09f99767ab
--- /dev/null
+++ b/yt/yt/core/misc/arithmetic_formula.h
@@ -0,0 +1,208 @@
+#pragma once
+
+#include "property.h"
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Validates that a string is a correct arithmetic formula variable name.
+void ValidateArithmeticFormulaVariable(const TString& variable);
+
+//! Validates that a string is a correct boolean formula variable name.
+void ValidateBooleanFormulaVariable(const TString& variable);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGenericFormulaImpl;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TArithmeticFormula
+{
+public:
+ // TODO(ifsmirnov): remove default ctor (first must make std::optional<T> serializable
+ // for non-default-constructible T)
+ TArithmeticFormula();
+ TArithmeticFormula(const TArithmeticFormula& other);
+ TArithmeticFormula(TArithmeticFormula&& other);
+ TArithmeticFormula& operator=(const TArithmeticFormula& other);
+ TArithmeticFormula& operator=(TArithmeticFormula&& other);
+ ~TArithmeticFormula();
+
+ bool operator==(const TArithmeticFormula& other) const;
+
+ //! Returns true if formula is empty.
+ bool IsEmpty() const;
+
+ //! Returns number of tokens in parsed formula.
+ int Size() const;
+
+ //! Returns hash based on parsed formula.
+ size_t GetHash() const;
+
+ //! Returns a human-readable representation of the formula.
+ TString GetFormula() const;
+
+ //! Evaluate the formula given values of variables.
+ i64 Eval(const THashMap<TString, i64>& values) const;
+
+ //! Returns the list of variables used in the formula.
+ THashSet<TString> GetVariables() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ TIntrusivePtr<TGenericFormulaImpl> Impl_;
+
+ explicit TArithmeticFormula(TIntrusivePtr<TGenericFormulaImpl> impl);
+
+ friend TArithmeticFormula MakeArithmeticFormula(const TString& formula);
+};
+
+//! Parse string and return arithmetic formula.
+TArithmeticFormula MakeArithmeticFormula(const TString& formula);
+
+void Serialize(const TArithmeticFormula& arithmeticFormula, NYson::IYsonConsumer* consumer);
+void Deserialize(TArithmeticFormula& arithmeticFormula, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBooleanFormulaTags
+{
+public:
+ TBooleanFormulaTags() = default;
+ explicit TBooleanFormulaTags(THashSet<TString> tags);
+
+ const THashSet<TString>& GetSourceTags() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+ bool operator==(const TBooleanFormulaTags& other) const;
+ bool operator!=(const TBooleanFormulaTags& other) const;
+
+private:
+ THashSet<TString> Tags_;
+ THashMap<TString, i64> PreparedTags_;
+
+ friend class TBooleanFormula;
+};
+
+void Serialize(const TBooleanFormulaTags& tags, NYson::IYsonConsumer* consumer);
+void Deserialize(TBooleanFormulaTags& tags, NYTree::INodePtr node);
+
+TString ToString(const TBooleanFormulaTags& tags);
+void FormatValue(TStringBuilderBase* builder, const TBooleanFormulaTags& tags, TStringBuf /* format */);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBooleanFormula
+{
+public:
+ TBooleanFormula();
+ TBooleanFormula(const TBooleanFormula& other);
+ TBooleanFormula(TBooleanFormula&& other);
+ TBooleanFormula& operator=(const TBooleanFormula& other);
+ TBooleanFormula& operator=(TBooleanFormula&& other);
+ ~TBooleanFormula();
+
+ bool operator==(const TBooleanFormula& other) const;
+
+ //! Returns true if formula is empty.
+ bool IsEmpty() const;
+
+ //! Returns number of tokens in parsed formula.
+ int Size() const;
+
+ //! Returns hash based on parsed formula.
+ size_t GetHash() const;
+
+ //! Returns a human-readable representation of the formula.
+ TString GetFormula() const;
+
+ //! Check that a given set of true-variables satisfies the formula.
+ bool IsSatisfiedBy(const std::vector<TString>& value) const;
+ bool IsSatisfiedBy(const THashSet<TString>& value) const;
+ bool IsSatisfiedBy(const TBooleanFormulaTags& tags) const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ TIntrusivePtr<TGenericFormulaImpl> Impl_;
+
+ explicit TBooleanFormula(TIntrusivePtr<TGenericFormulaImpl> impl);
+
+ friend TBooleanFormula MakeBooleanFormula(const TString& formula);
+};
+
+//! Parse string and return boolean formula.
+TBooleanFormula MakeBooleanFormula(const TString& formula);
+
+//! Make conjunction, disjunction and negation of formulas.
+TBooleanFormula operator&(const TBooleanFormula& lhs, const TBooleanFormula& rhs);
+TBooleanFormula operator|(const TBooleanFormula& lhs, const TBooleanFormula& rhs);
+TBooleanFormula operator!(const TBooleanFormula& formula);
+
+void Serialize(const TBooleanFormula& booleanFormula, NYson::IYsonConsumer* consumer);
+void Deserialize(TBooleanFormula& booleanFormula, NYTree::INodePtr node);
+void Deserialize(TBooleanFormula& booleanFormula, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeFormula
+{
+public:
+ TTimeFormula();
+ TTimeFormula(const TTimeFormula& other);
+ TTimeFormula(TTimeFormula&& other);
+ TTimeFormula& operator=(const TTimeFormula& other);
+ TTimeFormula& operator=(TTimeFormula&& other);
+ ~TTimeFormula();
+
+ bool operator==(const TTimeFormula& other) const;
+
+ //! Returns true if formula is empty.
+ bool IsEmpty() const;
+
+ //! Returns number of tokens in parsed formula.
+ int Size() const;
+
+ //! Returns hash based on parsed formula.
+ size_t GetHash() const;
+
+ //! Returns a human-readable representation of the formula.
+ TString GetFormula() const;
+
+ //! Check that given time satisfies the formula.
+ bool IsSatisfiedBy(TInstant time) const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ TArithmeticFormula Formula_;
+
+ explicit TTimeFormula(TArithmeticFormula&& arithmeticFormula);
+
+ friend TTimeFormula MakeTimeFormula(const TString& formula);
+};
+
+//! Parse string and return time formula.
+TTimeFormula MakeTimeFormula(const TString& formula);
+
+void Serialize(const TTimeFormula& timeFormula, NYson::IYsonConsumer* consumer);
+void Deserialize(TTimeFormula& timeFormula, NYTree::INodePtr node);
+void Deserialize(TTimeFormula& timeFormula, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/assert.cpp b/yt/yt/core/misc/assert.cpp
new file mode 100644
index 0000000000..3fe7a2863d
--- /dev/null
+++ b/yt/yt/core/misc/assert.cpp
@@ -0,0 +1,65 @@
+#include "assert.h"
+
+#include "proc.h"
+
+#include <yt/yt/core/logging/log_manager.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#ifdef _win_
+ #include <io.h>
+#else
+ #include <unistd.h>
+#endif
+
+#include <errno.h>
+
+namespace NYT::NDetail {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_WEAK void MaybeThrowSafeAssertionException(const char* /*message*/, int /*length*/)
+{
+ // A default implementation has no means of safety.
+ // Actual implementation lives in yt/yt/library/safe_assert.
+}
+
+void AssertTrapImpl(
+ TStringBuf trapType,
+ TStringBuf expr,
+ TStringBuf file,
+ int line,
+ TStringBuf function)
+{
+ TRawFormatter<1024> formatter;
+ formatter.AppendString(trapType);
+ formatter.AppendString("(");
+ formatter.AppendString(expr);
+ formatter.AppendString(") at ");
+ formatter.AppendString(file);
+ formatter.AppendString(":");
+ formatter.AppendNumber(line);
+ if (function) {
+ formatter.AppendString(" in ");
+ formatter.AppendString(function);
+ formatter.AppendString("\n");
+ }
+
+ MaybeThrowSafeAssertionException(formatter.GetData(), formatter.GetBytesWritten());
+
+ HandleEintr(::write, 2, formatter.GetData(), formatter.GetBytesWritten());
+
+ NLogging::TLogManager::Get()->Shutdown();
+
+ YT_BUILTIN_TRAP();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/yt/core/misc/async_expiring_cache-inl.h b/yt/yt/core/misc/async_expiring_cache-inl.h
new file mode 100644
index 0000000000..96039eee96
--- /dev/null
+++ b/yt/yt/core/misc/async_expiring_cache-inl.h
@@ -0,0 +1,691 @@
+#ifndef EXPIRING_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include async_expiring_cache.h"
+// For the sake of sane code completion.
+#include "async_expiring_cache.h"
+#endif
+
+#include "config.h"
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue>
+TAsyncExpiringCache<TKey, TValue>::TEntry::TEntry(NProfiling::TCpuInstant accessDeadline)
+ : AccessDeadline(accessDeadline)
+ , UpdateDeadline(std::numeric_limits<NProfiling::TCpuInstant>::max())
+ , Promise(NewPromise<TValue>())
+ , Future(Promise.ToFuture().ToUncancelable())
+{ }
+
+template <class TKey, class TValue>
+bool TAsyncExpiringCache<TKey, TValue>::TEntry::IsExpired(NProfiling::TCpuInstant now) const
+{
+ return now > AccessDeadline || now > UpdateDeadline;
+}
+
+template <class TKey, class TValue>
+TAsyncExpiringCache<TKey, TValue>::TAsyncExpiringCache(
+ TAsyncExpiringCacheConfigPtr config,
+ NLogging::TLogger logger,
+ NProfiling::TProfiler profiler)
+ : Logger_(std::move(logger))
+ , Config_(std::move(config))
+ , HitCounter_(profiler.Counter("/hit"))
+ , MissedCounter_(profiler.Counter("/miss"))
+ , SizeCounter_(profiler.Gauge("/size"))
+{
+ if (Config_->BatchUpdate && Config_->RefreshTime && *Config_->RefreshTime) {
+ NConcurrency::TDelayedExecutor::Submit(
+ BIND(&TAsyncExpiringCache::UpdateAll, MakeWeak(this)),
+ *Config_->RefreshTime);
+ }
+}
+
+template <class TKey, class TValue>
+TAsyncExpiringCacheConfigPtr TAsyncExpiringCache<TKey, TValue>::GetConfig() const
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return Config_;
+}
+
+template <class TKey, class TValue>
+typename TAsyncExpiringCache<TKey, TValue>::TExtendedGetResult TAsyncExpiringCache<TKey, TValue>::GetExtended(
+ const TKey& key)
+{
+ const auto& Logger = Logger_;
+ auto now = NProfiling::GetCpuInstant();
+
+ // Fast path.
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& entry = it->second;
+ if (!entry->IsExpired(now)) {
+ HitCounter_.Increment();
+ entry->AccessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ if (!entry->Future.IsSet()) {
+ YT_LOG_DEBUG("Waiting for cache entry (Key: %v)",
+ key);
+ }
+ return {entry->Future, false};
+ }
+ }
+ }
+
+ // Slow path.
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ auto& entry = it->second;
+ if (entry->Promise.IsSet() && entry->IsExpired(now)) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(entry->ProbationCookie);
+ Map_.erase(it);
+ OnRemoved(key);
+ } else {
+ HitCounter_.Increment();
+ entry->AccessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ if (!entry->Future.IsSet()) {
+ YT_LOG_DEBUG("Waiting for cache entry (Key: %v)",
+ key);
+ }
+ return {entry->Future, false};
+ }
+ }
+
+ MissedCounter_.Increment();
+ auto accessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ auto entry = New<TEntry>(accessDeadline);
+ auto future = entry->Future;
+ YT_VERIFY(Map_.emplace(key, entry).second);
+ OnAdded(key);
+ SizeCounter_.Update(Map_.size());
+ guard.Release();
+ YT_LOG_DEBUG("Populating cache entry (Key: %v)",
+ key);
+
+ DoGet(key, nullptr, EUpdateReason::InitialFetch)
+ .Subscribe(BIND([=, weakEntry = MakeWeak(entry), this, this_ = MakeStrong(this)] (const TErrorOr<TValue>& valueOrError) {
+ SetResult(weakEntry, key, valueOrError, false);
+ }));
+
+ return {future, true};
+ }
+}
+
+template <class TKey, class TValue>
+TFuture<TValue> TAsyncExpiringCache<TKey, TValue>::Get(const TKey& key)
+{
+ return GetExtended(key).Future;
+}
+
+template <class TKey, class TValue>
+TFuture<std::vector<TErrorOr<TValue>>> TAsyncExpiringCache<TKey, TValue>::GetMany(
+ const std::vector<TKey>& keys)
+{
+ const auto& Logger = Logger_;
+ auto now = NProfiling::GetCpuInstant();
+
+ std::vector<TFuture<TValue>> results(keys.size());
+ std::vector<TKey> keysToWaitFor;
+
+ auto handleHit = [&] (size_t index, const TEntryPtr& entry) {
+ const auto& key = keys[index];
+ HitCounter_.Increment();
+ results[index] = entry->Future;
+ if (!entry->Future.IsSet()) {
+ keysToWaitFor.push_back(key);
+ }
+ entry->AccessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ };
+
+ std::vector<size_t> preliminaryIndexesToPopulate;
+
+ // Fast path.
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ for (size_t index = 0; index < keys.size(); ++index) {
+ const auto& key = keys[index];
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& entry = it->second;
+ if (!entry->IsExpired(now)) {
+ handleHit(index, entry);
+ continue;
+ }
+ }
+ preliminaryIndexesToPopulate.push_back(index);
+ }
+ }
+
+ // Slow path.
+ if (!preliminaryIndexesToPopulate.empty()) {
+ std::vector<size_t> finalIndexesToPopulate;
+ std::vector<TWeakPtr<TEntry>> entriesToPopulate;
+
+ auto guard = WriterGuard(SpinLock_);
+
+ for (auto index : preliminaryIndexesToPopulate) {
+ const auto& key = keys[index];
+ if (auto it = Map_.find(keys[index]); it != Map_.end()) {
+ auto& entry = it->second;
+ if (entry->Promise.IsSet() && entry->IsExpired(now)) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(entry->ProbationCookie);
+ Map_.erase(it);
+ OnRemoved(key);
+ } else {
+ handleHit(index, entry);
+ continue;
+ }
+ }
+
+ MissedCounter_.Increment();
+
+ auto accessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ auto entry = New<TEntry>(accessDeadline);
+
+ finalIndexesToPopulate.push_back(index);
+ entriesToPopulate.push_back(entry);
+ results[index] = entry->Future;
+
+ YT_VERIFY(Map_.emplace(key, std::move(entry)).second);
+ OnAdded(key);
+ }
+
+ SizeCounter_.Update(Map_.size());
+
+ std::vector<TKey> keysToPopulate;
+ for (auto index : finalIndexesToPopulate) {
+ keysToPopulate.push_back(keys[index]);
+ }
+
+ guard.Release();
+
+ if (!keysToWaitFor.empty()) {
+ YT_LOG_DEBUG("Waiting for cache entries (Keys: %v)",
+ keysToWaitFor);
+ }
+
+ if (!keysToPopulate.empty()) {
+ YT_LOG_DEBUG("Populating cache entries (Keys: %v)",
+ keysToPopulate);
+ InvokeGetMany(entriesToPopulate, keysToPopulate, /* periodicRefreshTime */ std::nullopt);
+ }
+ }
+
+ return AllSet(results);
+}
+
+template <class TKey, class TValue>
+std::optional<TErrorOr<TValue>> TAsyncExpiringCache<TKey, TValue>::Find(const TKey& key)
+{
+ auto now = NProfiling::GetCpuInstant();
+
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& entry = it->second;
+ if (!entry->IsExpired(now) && entry->Promise.IsSet()) {
+ HitCounter_.Increment();
+ entry->AccessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ return entry->Future.Get();
+ }
+ }
+
+ MissedCounter_.Increment();
+ return std::nullopt;
+}
+
+template <class TKey, class TValue>
+std::vector<std::optional<TErrorOr<TValue>>> TAsyncExpiringCache<TKey, TValue>::FindMany(const std::vector<TKey>& keys)
+{
+ auto now = NProfiling::GetCpuInstant();
+
+ std::vector<std::optional<TErrorOr<TValue>>> results(keys.size());
+ std::vector<size_t> indexesToPopulate;
+
+ auto guard = ReaderGuard(SpinLock_);
+
+ for (size_t index = 0; index < keys.size(); ++index) {
+ const auto& key = keys[index];
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& entry = it->second;
+ if (!entry->IsExpired(now) && entry->Promise.IsSet()) {
+ HitCounter_.Increment();
+ results[index] = entry->Future.Get();
+ entry->AccessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ } else {
+ MissedCounter_.Increment();
+ }
+ } else {
+ MissedCounter_.Increment();
+ }
+ }
+ return results;
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::InvalidateActive(const TKey& key)
+{
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ if (auto it = Map_.find(key); it == Map_.end() || !it->second->Promise.IsSet()) {
+ return;
+ }
+ }
+
+ auto guard = WriterGuard(SpinLock_);
+ if (auto it = Map_.find(key); it != Map_.end() && it->second->Promise.IsSet()) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(it->second->ProbationCookie);
+ Map_.erase(it);
+ OnRemoved(key);
+ SizeCounter_.Update(Map_.size());
+ }
+}
+
+template <class TKey, class TValue>
+template <class T>
+void TAsyncExpiringCache<TKey, TValue>::InvalidateValue(const TKey& key, const T& value)
+{
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ if (auto it = Map_.find(key); it != Map_.end() && it->second->Promise.IsSet()) {
+ auto valueOrError = it->second->Promise.Get();
+ if (!valueOrError.IsOK() || valueOrError.Value() != value) {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ auto guard = WriterGuard(SpinLock_);
+ if (auto it = Map_.find(key); it != Map_.end() && it->second->Promise.IsSet()) {
+ auto valueOrError = it->second->Promise.Get();
+ if (valueOrError.IsOK() && valueOrError.Value() == value) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(it->second->ProbationCookie);
+ Map_.erase(it);
+ OnRemoved(key);
+ SizeCounter_.Update(Map_.size());
+ }
+ }
+}
+
+template <class TKey, class TValue>
+template <class T>
+void TAsyncExpiringCache<TKey, TValue>::ForceRefresh(const TKey& key, const T& value)
+{
+ auto now = NProfiling::GetCpuInstant();
+
+ auto guard = WriterGuard(SpinLock_);
+ if (auto it = Map_.find(key); it != Map_.end() && it->second->Promise.IsSet()) {
+ auto valueOrError = it->second->Promise.Get();
+ if (valueOrError.IsOK() && valueOrError.Value() == value) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(it->second->ProbationCookie);
+
+ auto accessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ auto newEntry = New<TEntry>(accessDeadline);
+ Map_[key] = newEntry;
+ guard.Release();
+
+ DoGet(key, &valueOrError, EUpdateReason::ForcedUpdate)
+ .Subscribe(BIND([=, weakEntry = MakeWeak(newEntry), this, this_ = MakeStrong(this)] (const TErrorOr<TValue>& valueOrError) {
+ SetResult(weakEntry, key, valueOrError, false);
+ }));
+ }
+ }
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::Set(const TKey& key, TErrorOr<TValue> valueOrError)
+{
+ auto isValueOK = valueOrError.IsOK();
+ auto now = NProfiling::GetCpuInstant();
+
+ TPromise<TValue> promise;
+
+ auto guard = WriterGuard(SpinLock_);
+
+ auto accessDeadline = now + NProfiling::DurationToCpuDuration(Config()->ExpireAfterAccessTime);
+ auto expirationTime = isValueOK ? Config()->ExpireAfterSuccessfulUpdateTime : Config()->ExpireAfterFailedUpdateTime;
+ auto updateDeadline = now + NProfiling::DurationToCpuDuration(expirationTime);
+
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& entry = it->second;
+ if (entry->Promise.IsSet()) {
+ entry->Promise = MakePromise(std::move(valueOrError));
+ entry->Future = entry->Promise.ToFuture();
+ } else {
+ promise = entry->Promise;
+ }
+ if (expirationTime == TDuration::Zero()) {
+ Map_.erase(it);
+ OnRemoved(key);
+ } else {
+ entry->AccessDeadline = accessDeadline;
+ entry->UpdateDeadline = updateDeadline;
+ }
+ } else if (expirationTime != TDuration::Zero()) {
+ auto entry = New<TEntry>(accessDeadline);
+ entry->UpdateDeadline = updateDeadline;
+ entry->Promise = MakePromise(std::move(valueOrError));
+ entry->Future = entry->Promise.ToFuture();
+ YT_VERIFY(Map_.emplace(key, std::move(entry)).second);
+ OnAdded(key);
+
+ if (isValueOK && !Config()->BatchUpdate) {
+ ScheduleEntryRefresh(entry, key, Config()->RefreshTime);
+ }
+ }
+
+ SizeCounter_.Update(Map_.size());
+ guard.Release();
+
+ if (!promise) {
+ return;
+ }
+
+ // This is deliberately racy: during concurrent sets some updates may be not visible.
+ promise.TrySet(std::move(valueOrError));
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::ScheduleEntryRefresh(
+ const TEntryPtr& entry,
+ const TKey& key,
+ std::optional<TDuration> refreshTime)
+{
+ if (refreshTime && *refreshTime) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(entry->ProbationCookie);
+ entry->ProbationCookie = NConcurrency::TDelayedExecutor::Submit(
+ BIND_NO_PROPAGATE(
+ &TAsyncExpiringCache::InvokeGet,
+ MakeWeak(this),
+ entry,
+ key),
+ *refreshTime);
+ }
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::Clear()
+{
+ auto guard = WriterGuard(SpinLock_);
+
+ if (!Config()->BatchUpdate) {
+ for (const auto& [key, entry] : Map_) {
+ if (entry->Promise.IsSet()) {
+ NConcurrency::TDelayedExecutor::CancelAndClear(entry->ProbationCookie);
+ }
+ }
+ }
+
+ for (const auto& [key, value] : Map_) {
+ OnRemoved(key);
+ }
+ Map_.clear();
+ SizeCounter_.Update(0);
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::SetResult(
+ const TWeakPtr<TEntry>& weakEntry,
+ const TKey& key,
+ const TErrorOr<TValue>& valueOrError,
+ bool isPeriodicUpdate)
+{
+ auto entry = weakEntry.Lock();
+ if (!entry) {
+ return;
+ }
+
+ // Ignore cancelation errors during periodic update.
+ if (isPeriodicUpdate && valueOrError.FindMatching(NYT::EErrorCode::Canceled)) {
+ if (valueOrError.IsOK()) {
+ auto guard = ReaderGuard(SpinLock_);
+ if (!Config()->BatchUpdate) {
+ auto refreshTime = Config()->RefreshTime;
+ guard.Release();
+ ScheduleEntryRefresh(entry, key, refreshTime);
+ }
+ }
+ return;
+ }
+
+ bool canCacheEntry = valueOrError.IsOK() || CanCacheError(valueOrError);
+
+ auto promise = GetPromise(entry);
+ auto entryUpdated = promise.TrySet(valueOrError);
+
+ auto now = NProfiling::GetCpuInstant();
+ auto guard = WriterGuard(SpinLock_);
+
+ if (!entryUpdated && !entry->Promise.IsSet()) {
+ // Someone has replaced the original promise with a new one,
+ // since we attempted to set it. We retire a let a concurrent writer to do the job.
+ return;
+ }
+
+ auto it = Map_.find(key);
+ if (it == Map_.end() || it->second != entry) {
+ return;
+ }
+
+ if (!entryUpdated && canCacheEntry) {
+ entry->Promise = MakePromise(valueOrError);
+ entry->Future = entry->Promise.ToFuture();
+ entryUpdated = true;
+ }
+
+ auto expirationTime = TDuration::Zero();
+ if (canCacheEntry) {
+ expirationTime = valueOrError.IsOK()
+ ? Config()->ExpireAfterSuccessfulUpdateTime
+ : Config()->ExpireAfterFailedUpdateTime;
+ }
+
+ auto updateDeadline = NProfiling::GetCpuInstant() + NProfiling::DurationToCpuDuration(expirationTime);
+
+ if (entryUpdated) {
+ entry->UpdateDeadline = updateDeadline;
+ }
+
+ if (entry->IsExpired(now) || (entryUpdated && expirationTime == TDuration::Zero())) {
+ Map_.erase(it);
+ OnRemoved(key);
+ SizeCounter_.Update(Map_.size());
+ return;
+ }
+
+ if (valueOrError.IsOK() && !Config()->BatchUpdate) {
+ ScheduleEntryRefresh(entry, key, Config()->RefreshTime);
+ }
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::InvokeGet(
+ const TEntryPtr& entry,
+ const TKey& key)
+{
+ if (TryEraseExpired(entry, key)) {
+ return;
+ }
+
+ YT_VERIFY(entry->Future.IsSet());
+ const auto& oldValue = entry->Future.Get();
+
+ DoGet(key, &oldValue, EUpdateReason::PeriodicUpdate)
+ .Subscribe(BIND([=, weakEntry = MakeWeak(entry), this, this_ = MakeStrong(this)] (const TErrorOr<TValue>& valueOrError) {
+ SetResult(weakEntry, key, valueOrError, true);
+ }));
+}
+
+template <class TKey, class TValue>
+TFuture<TValue> TAsyncExpiringCache<TKey, TValue>::DoGet(
+ const TKey& key,
+ const TErrorOr<TValue>* /* oldValue */,
+ EUpdateReason reason) noexcept
+{
+ return DoGet(key, reason != EUpdateReason::InitialFetch);
+}
+
+template <class TKey, class TValue>
+bool TAsyncExpiringCache<TKey, TValue>::TryEraseExpired(const TEntryPtr& entry, const TKey& key)
+{
+ auto now = NProfiling::GetCpuInstant();
+
+ if (now > entry->AccessDeadline) {
+ auto writerGuard = WriterGuard(SpinLock_);
+
+ if (auto it = Map_.find(key); it != Map_.end() && entry == it->second && now > it->second->AccessDeadline) {
+ Map_.erase(it);
+ OnRemoved(key);
+ SizeCounter_.Update(Map_.size());
+ }
+ return true;
+ }
+ return false;
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::InvokeGetMany(
+ const std::vector<TWeakPtr<TEntry>>& entries,
+ const std::vector<TKey>& keys,
+ std::optional<TDuration> periodicRefreshTime)
+{
+ auto isPeriodicUpdate = periodicRefreshTime.has_value();
+
+ DoGetMany(keys, isPeriodicUpdate)
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TErrorOr<std::vector<TErrorOr<TValue>>>& valuesOrError) {
+ for (size_t index = 0; index < keys.size(); ++index) {
+ SetResult(
+ entries[index],
+ keys[index],
+ valuesOrError.IsOK() ? valuesOrError.Value()[index] : TErrorOr<TValue>(TError(valuesOrError)),
+ isPeriodicUpdate);
+ }
+
+ if (isPeriodicUpdate) {
+ NConcurrency::TDelayedExecutor::Submit(
+ BIND(&TAsyncExpiringCache::UpdateAll, MakeWeak(this)),
+ *periodicRefreshTime);
+ }
+ }));
+}
+
+template <class TKey, class TValue>
+TFuture<std::vector<TErrorOr<TValue>>> TAsyncExpiringCache<TKey, TValue>::DoGetMany(
+ const std::vector<TKey>& keys,
+ bool isPeriodicUpdate) noexcept
+{
+ std::vector<TFuture<TValue>> results;
+ for (const auto& key : keys) {
+ results.push_back(DoGet(key, isPeriodicUpdate));
+ }
+ return AllSet(std::move(results));
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::OnAdded(const TKey& /*key*/) noexcept
+{ }
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::OnRemoved(const TKey& /*key*/) noexcept
+{ }
+
+template <class TKey, class TValue>
+bool TAsyncExpiringCache<TKey, TValue>::CanCacheError(const TError& /*error*/) noexcept
+{
+ return true;
+}
+
+template <class TKey, class TValue>
+TPromise<TValue> TAsyncExpiringCache<TKey, TValue>::GetPromise(const TEntryPtr& entry) noexcept
+{
+ auto guard = ReaderGuard(SpinLock_);
+ return entry->Promise;
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::UpdateAll()
+{
+ std::vector<TWeakPtr<TEntry>> entries;
+ std::vector<TKey> keys;
+ std::vector<TKey> expiredKeys;
+
+ auto now = NProfiling::GetCpuInstant();
+ TDuration refreshTime;
+
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ refreshTime = *Config()->RefreshTime;
+
+ for (const auto& [key, entry] : Map_) {
+ if (entry->Promise.IsSet()) {
+ if (now > entry->AccessDeadline) {
+ expiredKeys.push_back(key);
+ } else if (entry->Future.Get().IsOK()) {
+ keys.push_back(key);
+ entries.push_back(MakeWeak(entry));
+ }
+ }
+ }
+ }
+
+ if (!expiredKeys.empty()) {
+ auto guard = WriterGuard(SpinLock_);
+
+ for (const auto& key : expiredKeys) {
+ if (auto it = Map_.find(key); it != Map_.end()) {
+ const auto& [_, entry] = *it;
+ if (entry->Promise.IsSet()) {
+ if (now > entry->AccessDeadline) {
+ Map_.erase(it);
+ OnRemoved(key);
+ SizeCounter_.Update(Map_.size());
+ } else if (entry->Future.Get().IsOK()) {
+ keys.push_back(key);
+ entries.push_back(MakeWeak(entry));
+ }
+ }
+ }
+ }
+ }
+
+ if (entries.empty()) {
+ NConcurrency::TDelayedExecutor::Submit(
+ BIND(&TAsyncExpiringCache::UpdateAll, MakeWeak(this)),
+ refreshTime);
+ } else {
+ InvokeGetMany(entries, keys, refreshTime);
+ }
+}
+
+template <class TKey, class TValue>
+void TAsyncExpiringCache<TKey, TValue>::Reconfigure(TAsyncExpiringCacheConfigPtr config)
+{
+ auto guard = WriterGuard(SpinLock_);
+ if (Config_->BatchUpdate != config->BatchUpdate) {
+ // TODO(akozhikhov): Support this.
+ THROW_ERROR_EXCEPTION("Cannot change 'BatchUpdate' option");
+ }
+ Config_.Swap(config);
+}
+
+template <class TKey, class TValue>
+const TAsyncExpiringCacheConfigPtr& TAsyncExpiringCache<TKey, TValue>::Config() const
+{
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ return Config_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/async_expiring_cache.h b/yt/yt/core/misc/async_expiring_cache.h
new file mode 100644
index 0000000000..137df01f32
--- /dev/null
+++ b/yt/yt/core/misc/async_expiring_cache.h
@@ -0,0 +1,171 @@
+#pragma once
+
+#include "public.h"
+#include "cache_config.h"
+
+#include <type_traits>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue>
+class TAsyncExpiringCache
+ : public virtual TRefCounted
+{
+public:
+ using KeyType = TKey;
+ using ValueType = TValue;
+
+ struct TExtendedGetResult
+ {
+ TFuture<TValue> Future;
+ bool RequestInitialized;
+ };
+
+ explicit TAsyncExpiringCache(
+ TAsyncExpiringCacheConfigPtr config,
+ NLogging::TLogger logger = {},
+ NProfiling::TProfiler profiler = {});
+
+ TFuture<TValue> Get(const TKey& key);
+ TExtendedGetResult GetExtended(const TKey& key);
+ TFuture<std::vector<TErrorOr<TValue>>> GetMany(const std::vector<TKey>& keys);
+
+ std::optional<TErrorOr<TValue>> Find(const TKey& key);
+ std::vector<std::optional<TErrorOr<TValue>>> FindMany(const std::vector<TKey>& keys);
+
+ //! InvalidateActive removes key from the cache, if it's value is currently set.
+ void InvalidateActive(const TKey& key);
+
+ //! InvalidateValue removes key from the cache, if it's value is equal to provided.
+ template <class T>
+ void InvalidateValue(const TKey& key, const T& value);
+
+ //! ForceRefresh marks current value as outdated, forcing value update.
+ template <class T>
+ void ForceRefresh(const TKey& key, const T& value);
+
+ void Set(const TKey& key, TErrorOr<TValue> valueOrError);
+
+ void Clear();
+
+ void Reconfigure(TAsyncExpiringCacheConfigPtr config);
+
+ enum EUpdateReason
+ {
+ InitialFetch,
+ PeriodicUpdate,
+ ForcedUpdate,
+ };
+
+protected:
+ TAsyncExpiringCacheConfigPtr GetConfig() const;
+
+ virtual TFuture<TValue> DoGet(
+ const TKey& key,
+ bool isPeriodicUpdate) noexcept = 0;
+
+ virtual TFuture<TValue> DoGet(
+ const TKey& key,
+ const TErrorOr<TValue>* oldValue,
+ EUpdateReason reason) noexcept;
+
+ virtual TFuture<std::vector<TErrorOr<TValue>>> DoGetMany(
+ const std::vector<TKey>& keys,
+ bool isPeriodicUpdate) noexcept;
+
+ //! Called under write lock.
+ virtual void OnAdded(const TKey& key) noexcept;
+
+ //! Called under write lock.
+ virtual void OnRemoved(const TKey& key) noexcept;
+
+ virtual bool CanCacheError(const TError& error) noexcept;
+
+private:
+ const NLogging::TLogger Logger_;
+
+ struct TEntry
+ : public TRefCounted
+ {
+ //! When this entry must be evicted with respect to access timeout.
+ std::atomic<NProfiling::TCpuInstant> AccessDeadline;
+
+ //! When this entry must be evicted with respect to update timeout.
+ NProfiling::TCpuInstant UpdateDeadline;
+
+ //! Some latest known value (possibly not yet set).
+ TPromise<TValue> Promise;
+
+ //! Uncancelable version of #Promise.
+ TFuture<TValue> Future;
+
+ //! Corresponds to a future probation request.
+ NConcurrency::TDelayedExecutorCookie ProbationCookie;
+
+ //! Constructs a fresh entry.
+ explicit TEntry(NProfiling::TCpuInstant accessDeadline);
+
+ //! Check that entry is expired with respect to either access or update.
+ bool IsExpired(NProfiling::TCpuInstant now) const;
+ };
+
+ using TEntryPtr = TIntrusivePtr<TEntry>;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ THashMap<TKey, TEntryPtr> Map_;
+ TAsyncExpiringCacheConfigPtr Config_;
+
+ NProfiling::TCounter HitCounter_;
+ NProfiling::TCounter MissedCounter_;
+ NProfiling::TGauge SizeCounter_;
+
+ void SetResult(
+ const TWeakPtr<TEntry>& entry,
+ const TKey& key,
+ const TErrorOr<TValue>& valueOrError,
+ bool isPeriodicUpdate);
+
+ void InvokeGetMany(
+ const std::vector<TWeakPtr<TEntry>>& entries,
+ const std::vector<TKey>& keys,
+ std::optional<TDuration> periodicRefreshTime);
+
+ void InvokeGet(
+ const TEntryPtr& entry,
+ const TKey& key);
+
+ bool TryEraseExpired(
+ const TEntryPtr& Entry,
+ const TKey& key);
+
+ void UpdateAll();
+
+ void ScheduleEntryRefresh(
+ const TEntryPtr& entry,
+ const TKey& key,
+ std::optional<TDuration> refreshTime);
+
+ TPromise<TValue> GetPromise(const TEntryPtr& entry) noexcept;
+
+ const TAsyncExpiringCacheConfigPtr& Config() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define EXPIRING_CACHE_INL_H_
+#include "async_expiring_cache-inl.h"
+#undef EXPIRING_CACHE_INL_H_
+
diff --git a/yt/yt/core/misc/async_slru_cache-inl.h b/yt/yt/core/misc/async_slru_cache-inl.h
new file mode 100644
index 0000000000..9ed8c1c98b
--- /dev/null
+++ b/yt/yt/core/misc/async_slru_cache-inl.h
@@ -0,0 +1,1410 @@
+#ifndef ASYNC_SLRU_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include async_slru_cache.h"
+// For the sake of sane code completion.
+#include "async_slru_cache.h"
+#endif
+
+#include <util/system/yield.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TItem::TItem()
+ : ValuePromise(NewPromise<TValuePtr>())
+{ }
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TItem::TItem(TValuePtr value)
+ : ValuePromise(MakePromise(value))
+ , Value(std::move(value))
+{ }
+
+template <class TKey, class TValue, class THash>
+auto TAsyncSlruCacheBase<TKey, TValue, THash>::TItem::GetValueFuture() const -> TValueFuture
+{
+ return ValuePromise
+ .ToFuture()
+ .ToUncancelable();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::PushToYounger(TItem* item, i64 weight)
+{
+ YT_ASSERT(item->Empty());
+ YoungerLruList.PushFront(item);
+ item->CachedWeight = weight;
+ YoungerWeightCounter += weight;
+ AsDerived()->OnYoungerUpdated(1, weight);
+ item->Younger = true;
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::MoveToYounger(TItem* item)
+{
+ YT_ASSERT(!item->Empty());
+ item->Unlink();
+ YoungerLruList.PushFront(item);
+ if (!item->Younger) {
+ i64 weight = item->CachedWeight;
+ OlderWeightCounter -= weight;
+ AsDerived()->OnOlderUpdated(-1, -weight);
+ YoungerWeightCounter += weight;
+ AsDerived()->OnYoungerUpdated(1, weight);
+ item->Younger = true;
+ }
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::MoveToOlder(TItem* item)
+{
+ YT_ASSERT(!item->Empty());
+ item->Unlink();
+ OlderLruList.PushFront(item);
+ if (item->Younger) {
+ i64 weight = item->CachedWeight;
+ YoungerWeightCounter -= weight;
+ AsDerived()->OnYoungerUpdated(-1, -weight);
+ OlderWeightCounter += weight;
+ AsDerived()->OnOlderUpdated(1, weight);
+ item->Younger = false;
+ }
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::PopFromLists(TItem* item)
+{
+ if (item->Empty()) {
+ return;
+ }
+
+ YT_VERIFY(TouchBufferPosition.load() == 0);
+
+ i64 weight = item->CachedWeight;
+ if (item->Younger) {
+ YoungerWeightCounter -= weight;
+ AsDerived()->OnYoungerUpdated(-1, -weight);
+ } else {
+ OlderWeightCounter -= weight;
+ AsDerived()->OnOlderUpdated(-1, -weight);
+ }
+ item->Unlink();
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::UpdateWeight(TItem* item, i64 weightDelta)
+{
+ YT_VERIFY(!item->Empty());
+ if (item->Younger) {
+ YoungerWeightCounter += weightDelta;
+ AsDerived()->OnYoungerUpdated(0, weightDelta);
+ } else {
+ OlderWeightCounter += weightDelta;
+ AsDerived()->OnOlderUpdated(0, weightDelta);
+ }
+ item->CachedWeight += weightDelta;
+}
+
+template <class TItem, class TDerived>
+TIntrusiveListWithAutoDelete<TItem, TDelete> TAsyncSlruCacheListManager<TItem, TDerived>::TrimNoDelete()
+{
+ // Move from older to younger.
+ auto capacity = Capacity.load();
+ auto youngerSizeFraction = YoungerSizeFraction.load();
+ while (!OlderLruList.Empty() && OlderWeightCounter > capacity * (1 - youngerSizeFraction)) {
+ auto* item = &*(--OlderLruList.End());
+ MoveToYounger(item);
+ }
+
+ // Evict from younger.
+ TIntrusiveListWithAutoDelete<TItem, TDelete> evictedItems;
+ while (!YoungerLruList.Empty() && static_cast<i64>(YoungerWeightCounter + OlderWeightCounter) > capacity) {
+ auto* item = &*(--YoungerLruList.End());
+ PopFromLists(item);
+ evictedItems.PushBack(item);
+ }
+
+ return evictedItems;
+}
+
+template <class TItem, class TDerived>
+bool TAsyncSlruCacheListManager<TItem, TDerived>::TouchItem(TItem* item)
+{
+ if (item->Empty()) {
+ return false;
+ }
+
+ int capacity = std::ssize(TouchBuffer);
+ int index = TouchBufferPosition++;
+ if (index >= capacity) {
+ // Drop touch request due to buffer overflow.
+ // NB: We still return false since the other thread is already responsible for
+ // draining the buffer.
+ return false;
+ }
+
+ TouchBuffer[index] = item;
+ return index == capacity - 1;
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::DrainTouchBuffer()
+{
+ int count = std::min<int>(TouchBufferPosition.load(), std::ssize(TouchBuffer));
+ for (int index = 0; index < count; ++index) {
+ MoveToOlder(TouchBuffer[index]);
+ }
+ TouchBufferPosition = 0;
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::Reconfigure(i64 capacity, double youngerSizeFraction)
+{
+ Capacity = capacity;
+ YoungerSizeFraction = youngerSizeFraction;
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::SetTouchBufferCapacity(i64 touchBufferCapacity)
+{
+ TouchBuffer.resize(touchBufferCapacity);
+}
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::OnYoungerUpdated(i64 /*deltaCount*/, i64 /*deltaWeight*/)
+{ }
+
+template <class TItem, class TDerived>
+void TAsyncSlruCacheListManager<TItem, TDerived>::OnOlderUpdated(i64 /*deltaCount*/, i64 /*deltaWeight*/)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+const TKey& TAsyncCacheValueBase<TKey, TValue, THash>::GetKey() const
+{
+ return Key_;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncCacheValueBase<TKey, TValue, THash>::UpdateWeight() const
+{
+ if (auto cache = Cache_.Lock()) {
+ cache->UpdateWeight(GetKey());
+ }
+}
+
+template <class TKey, class TValue, class THash>
+TAsyncCacheValueBase<TKey, TValue, THash>::TAsyncCacheValueBase(const TKey& key)
+ : Key_(key)
+{ }
+
+template <class TKey, class TValue, class THash>
+NYT::TAsyncCacheValueBase<TKey, TValue, THash>::~TAsyncCacheValueBase()
+{
+ if (auto cache = Cache_.Lock()) {
+ cache->Unregister(Key_);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TCounters::TCounters(
+ const NProfiling::TProfiler& profiler)
+{
+ MissedWeightCounter = profiler.Counter("/missed_weight");
+ MissedCounter = profiler.Counter("/missed_count");
+
+ auto profilerWithSyncTag = profiler.WithTag("hit_type", "sync");
+ SyncHitWeightCounter = profilerWithSyncTag.Counter("/hit_weight");
+ SyncHitCounter = profilerWithSyncTag.Counter("/hit_count");
+
+ auto profilerWithAsyncTag = profiler.WithTag("hit_type", "async");
+ AsyncHitWeightCounter = profilerWithAsyncTag.Counter("/hit_weight");
+ AsyncHitCounter = profilerWithAsyncTag.Counter("/hit_count");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TAsyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ const NProfiling::TProfiler& profiler)
+ : Config_(std::move(config))
+ , Capacity_(Config_->Capacity)
+ , Counters_(profiler)
+ , SmallGhostCounters_(profiler.WithPrefix("/small_ghost_cache"))
+ , LargeGhostCounters_(profiler.WithPrefix("/large_ghost_cache"))
+{
+ static_assert(
+ std::is_base_of_v<TAsyncCacheValueBase<TKey, TValue, THash>, TValue>,
+ "TValue must be derived from TAsyncCacheValueBase");
+
+ auto youngerSegmentProfiler = profiler.WithTag("segment", "younger");
+ auto olderSegmentProfiler = profiler.WithTag("segment", "older");
+
+ youngerSegmentProfiler.AddFuncGauge("/weight", MakeStrong(this), [this] {
+ return YoungerWeightCounter_.load();
+ });
+ olderSegmentProfiler.AddFuncGauge("/weight", MakeStrong(this), [this] {
+ return OlderWeightCounter_.load();
+ });
+ youngerSegmentProfiler.AddFuncGauge("/size", MakeStrong(this), [this] {
+ return YoungerSizeCounter_.load();
+ });
+ olderSegmentProfiler.AddFuncGauge("/size", MakeStrong(this), [this] {
+ return OlderSizeCounter_.load();
+ });
+
+ GhostCachesEnabled_.store(Config_->EnableGhostCaches);
+
+ YT_VERIFY(IsPowerOf2(Config_->ShardCount));
+ Shards_.reset(new TShard[Config_->ShardCount]);
+
+ i64 shardCapacity = std::max<i64>(1, Config_->Capacity / Config_->ShardCount);
+ i64 touchBufferCapacity = Config_->TouchBufferCapacity / Config_->ShardCount;
+ for (int index = 0; index < Config_->ShardCount; ++index) {
+ auto& shard = Shards_[index];
+
+ shard.SetTouchBufferCapacity(touchBufferCapacity);
+ shard.Reconfigure(shardCapacity, Config_->YoungerSizeFraction);
+
+ if (GhostCachesEnabled_.load()) {
+ shard.SmallGhost.SetCounters(&SmallGhostCounters_);
+ shard.LargeGhost.SetCounters(&LargeGhostCounters_);
+
+ shard.SmallGhost.SetTouchBufferCapacity(touchBufferCapacity);
+ shard.LargeGhost.SetTouchBufferCapacity(touchBufferCapacity);
+
+ shard.SmallGhost.Reconfigure(
+ static_cast<i64>(shardCapacity * Config_->SmallGhostCacheRatio),
+ Config_->YoungerSizeFraction);
+ shard.LargeGhost.Reconfigure(
+ static_cast<i64>(shardCapacity * Config_->LargeGhostCacheRatio),
+ Config_->YoungerSizeFraction);
+ }
+
+ shard.Parent = this;
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(const TSlruCacheDynamicConfigPtr& config)
+{
+ i64 capacity = config->Capacity.value_or(Config_->Capacity);
+ i64 shardCapacity = std::max<i64>(1, capacity / Config_->ShardCount);
+ double youngerSizeFraction = config->YoungerSizeFraction.value_or(Config_->YoungerSizeFraction);
+
+ Capacity_.store(capacity);
+
+ if (!config->EnableGhostCaches) {
+ GhostCachesEnabled_.store(false);
+ }
+
+ for (int shardIndex = 0; shardIndex < Config_->ShardCount; ++shardIndex) {
+ auto& shard = Shards_[shardIndex];
+
+ if (GhostCachesEnabled_.load()) {
+ shard.SmallGhost.Reconfigure(
+ static_cast<i64>(shardCapacity * Config_->SmallGhostCacheRatio),
+ youngerSizeFraction);
+ shard.LargeGhost.Reconfigure(
+ static_cast<i64>(shardCapacity * Config_->LargeGhostCacheRatio),
+ youngerSizeFraction);
+ }
+
+ auto writerGuard = WriterGuard(shard.SpinLock);
+ shard.Reconfigure(shardCapacity, youngerSizeFraction);
+ shard.DrainTouchBuffer();
+ NotifyOnTrim(shard.Trim(writerGuard), nullptr);
+ }
+}
+
+template <class TKey, class TValue, class THash>
+typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValuePtr
+TAsyncSlruCacheBase<TKey, TValue, THash>::Find(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.Find(key);
+ shard->LargeGhost.Find(key);
+ }
+
+ auto readerGuard = ReaderGuard(shard->SpinLock);
+
+ auto itemIt = shard->ItemMap.find(key);
+ if (itemIt == shard->ItemMap.end()) {
+ Counters_.MissedCounter.Increment();
+ return nullptr;
+ }
+
+ auto* item = itemIt->second;
+ auto value = item->Value;
+ if (!value) {
+ Counters_.MissedCounter.Increment();
+ return nullptr;
+ }
+
+ bool needToDrain = shard->TouchItem(item);
+
+ Counters_.SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_.SyncHitCounter.Increment();
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(shard->SpinLock);
+ shard->DrainTouchBuffer();
+ }
+
+ return value;
+}
+
+template <class TKey, class TValue, class THash>
+int TAsyncSlruCacheBase<TKey, TValue, THash>::GetSize() const
+{
+ return Size_.load();
+}
+
+template <class TKey, class TValue, class THash>
+i64 TAsyncSlruCacheBase<TKey, TValue, THash>::GetCapacity() const
+{
+ return Capacity_.load();
+}
+
+template <class TKey, class TValue, class THash>
+std::vector<typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValuePtr>
+TAsyncSlruCacheBase<TKey, TValue, THash>::GetAll()
+{
+ std::vector<TValuePtr> result;
+ result.reserve(GetSize());
+
+ for (int shardIndex = 0; shardIndex < Config_->ShardCount; ++shardIndex) {
+ const auto& shard = Shards_[shardIndex];
+
+ auto readerGuard = ReaderGuard(shard.SpinLock);
+ for (const auto& [key, rawValue] : shard.ValueMap) {
+ if (auto value = DangerousGetPtr<TValue>(rawValue)) {
+ result.push_back(value);
+ }
+ }
+ }
+ return result;
+}
+
+template <class TKey, class TValue, class THash>
+typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValueFuture
+TAsyncSlruCacheBase<TKey, TValue, THash>::Lookup(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.Lookup(key);
+ shard->LargeGhost.Lookup(key);
+ }
+
+ auto valueFuture = DoLookup(shard, key);
+ if (!valueFuture) {
+ Counters_.MissedCounter.Increment();
+ }
+ return valueFuture;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::Touch(const TValuePtr& value)
+{
+ auto* shard = GetShardByKey(value->GetKey());
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.Touch(value);
+ shard->LargeGhost.Touch(value);
+ }
+
+ auto readerGuard = ReaderGuard(shard->SpinLock);
+
+ if (value->Cache_.Lock() != this || !value->Item_) {
+ return;
+ }
+
+ auto needToDrain = shard->TouchItem(value->Item_);
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(shard->SpinLock);
+ shard->DrainTouchBuffer();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValueFuture
+TAsyncSlruCacheBase<TKey, TValue, THash>::DoLookup(TShard* shard, const TKey& key)
+{
+ auto readerGuard = ReaderGuard(shard->SpinLock);
+
+ auto& itemMap = shard->ItemMap;
+ const auto& valueMap = shard->ValueMap;
+
+ if (auto itemIt = itemMap.find(key); itemIt != itemMap.end()) {
+ auto* item = itemIt->second;
+ bool needToDrain = shard->TouchItem(item);
+ auto valueFuture = item->GetValueFuture();
+
+ if (item->Value) {
+ Counters_.SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_.SyncHitCounter.Increment();
+ } else {
+ Counters_.AsyncHitCounter.Increment();
+ item->AsyncHitCount.fetch_add(1);
+ }
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(shard->SpinLock);
+ shard->DrainTouchBuffer();
+ }
+
+ return valueFuture;
+ }
+
+ auto valueIt = valueMap.find(key);
+ if (valueIt == valueMap.end()) {
+ return {};
+ }
+
+ auto value = DangerousGetPtr(valueIt->second);
+ if (!value) {
+ return {};
+ }
+
+ readerGuard.Release();
+
+ auto writerGuard = WriterGuard(shard->SpinLock);
+
+ if (auto itemIt = itemMap.find(key); itemIt != itemMap.end()) {
+ auto* item = itemIt->second;
+
+ shard->TouchItem(item);
+ auto valueFuture = item->GetValueFuture();
+
+ if (item->Value) {
+ Counters_.SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_.SyncHitCounter.Increment();
+ } else {
+ Counters_.AsyncHitCounter.Increment();
+ item->AsyncHitCount.fetch_add(1);
+ }
+
+ shard->DrainTouchBuffer();
+
+ return valueFuture;
+ }
+
+ shard->DrainTouchBuffer();
+
+ {
+ auto* item = new TItem(value);
+ value->Item_ = item;
+
+ auto valueFuture = item->GetValueFuture();
+
+ YT_VERIFY(itemMap.emplace(key, item).second);
+ ++Size_;
+
+ i64 weight = GetWeight(item->Value);
+ shard->PushToYounger(item, weight);
+ Counters_.SyncHitWeightCounter.Increment(weight);
+ Counters_.SyncHitCounter.Increment();
+
+ // NB: Releases the lock.
+ NotifyOnTrim(shard->Trim(writerGuard), value);
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.Resurrect(value, weight);
+ shard->LargeGhost.Resurrect(value, weight);
+ }
+
+ return valueFuture;
+ }
+}
+
+template <class TKey, class TValue, class THash>
+auto TAsyncSlruCacheBase<TKey, TValue, THash>::BeginInsert(const TKey& key) -> TInsertCookie
+{
+ auto* shard = GetShardByKey(key);
+
+ if (auto valueFuture = DoLookup(shard, key)) {
+ if (GhostCachesEnabled_.load()) {
+ if (valueFuture.IsSet() && valueFuture.Get().IsOK()) {
+ bool smallInserted = shard->SmallGhost.BeginInsert(key);
+ bool largeInserted = shard->LargeGhost.BeginInsert(key);
+ if (smallInserted || largeInserted) {
+ const auto& value = valueFuture.Get().Value();
+ i64 weight = GetWeight(value);
+ if (smallInserted) {
+ shard->SmallGhost.EndInsert(value, weight);
+ }
+ if (largeInserted) {
+ shard->LargeGhost.EndInsert(value, weight);
+ }
+ }
+ } else {
+ shard->SmallGhost.Lookup(key);
+ shard->LargeGhost.Lookup(key);
+ }
+ }
+
+ return TInsertCookie(
+ key,
+ nullptr,
+ std::move(valueFuture),
+ false);
+ }
+
+ while (true) {
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ auto& itemMap = shard->ItemMap;
+ auto& valueMap = shard->ValueMap;
+
+ auto itemIt = itemMap.find(key);
+ if (itemIt != itemMap.end()) {
+ auto* item = itemIt->second;
+ shard->TouchItem(item);
+ auto valueFuture = item->GetValueFuture();
+
+ if (item->Value) {
+ Counters_.SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_.SyncHitCounter.Increment();
+ } else {
+ Counters_.AsyncHitCounter.Increment();
+ item->AsyncHitCount.fetch_add(1);
+ }
+
+ auto value = item->Value;
+ i64 weight = item->CachedWeight;
+
+ guard.Release();
+
+ if (GhostCachesEnabled_.load()) {
+ if (value) {
+ if (shard->SmallGhost.BeginInsert(key)) {
+ shard->SmallGhost.EndInsert(value, weight);
+ }
+ if (shard->LargeGhost.BeginInsert(key)) {
+ shard->LargeGhost.EndInsert(value, weight);
+ }
+ } else {
+ shard->SmallGhost.Lookup(key);
+ shard->LargeGhost.Lookup(key);
+ }
+ }
+
+ return TInsertCookie(
+ key,
+ nullptr,
+ std::move(valueFuture),
+ false);
+ }
+
+ auto valueIt = valueMap.find(key);
+ if (valueIt == valueMap.end()) {
+ auto* item = new TItem();
+ auto valueFuture = item->GetValueFuture();
+
+ YT_VERIFY(itemMap.emplace(key, item).second);
+ ++Size_;
+
+ Counters_.MissedCounter.Increment();
+
+ guard.Release();
+
+ auto insertCookie = TInsertCookie(
+ key,
+ this,
+ std::move(valueFuture),
+ true);
+
+ if (GhostCachesEnabled_.load()) {
+ insertCookie.InsertedIntoSmallGhost_ = shard->SmallGhost.BeginInsert(key);
+ insertCookie.InsertedIntoLargeGhost_ = shard->LargeGhost.BeginInsert(key);
+ }
+
+ return insertCookie;
+ }
+
+ if (auto value = DangerousGetPtr(valueIt->second)) {
+ auto* item = new TItem(value);
+ value->Item_ = item;
+
+ YT_VERIFY(itemMap.emplace(key, item).second);
+ ++Size_;
+
+ i64 weight = GetWeight(item->Value);
+ shard->PushToYounger(item, weight);
+ Counters_.SyncHitWeightCounter.Increment(weight);
+ Counters_.SyncHitCounter.Increment();
+
+ // NB: Releases the lock.
+ NotifyOnTrim(shard->Trim(guard), value);
+
+ guard.Release();
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.Resurrect(value, weight);
+ shard->LargeGhost.Resurrect(value, weight);
+ }
+
+ return TInsertCookie(
+ key,
+ nullptr,
+ MakeFuture(value),
+ false);
+ }
+
+ // Back off.
+ // Hopefully the object we had just extracted will be destroyed soon
+ // and thus vanish from ValueMap.
+ guard.Release();
+ ThreadYield();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::EndInsert(const TInsertCookie& insertCookie, TValuePtr value)
+{
+ YT_VERIFY(value);
+ auto key = value->GetKey();
+
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ value->Cache_ = MakeWeak(this);
+
+ auto* item = GetOrCrash(shard->ItemMap, key);
+ item->Value = value;
+ value->Item_ = item;
+ auto promise = item->ValuePromise;
+
+ YT_VERIFY(shard->ValueMap.emplace(key, value.Get()).second);
+
+ i64 weight = GetWeight(item->Value);
+ shard->PushToYounger(item, weight);
+ // MissedCounter and AsyncHitCounter have already been incremented in BeginInsert.
+ Counters_.MissedWeightCounter.Increment(weight);
+ Counters_.AsyncHitWeightCounter.Increment(weight * item->AsyncHitCount.load());
+
+ // NB: Releases the lock.
+ NotifyOnTrim(shard->Trim(guard), value);
+
+ // We do not want to break the ghost cache invariants, according to which either EndInsert
+ // or CancelInsert must be called for each item in Inserting state. So we end the insertion
+ // even after ghost cache is disabled.
+ if (insertCookie.InsertedIntoSmallGhost_) {
+ shard->SmallGhost.EndInsert(value, weight);
+ }
+ if (insertCookie.InsertedIntoLargeGhost_) {
+ shard->LargeGhost.EndInsert(value, weight);
+ }
+
+ promise.Set(value);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::CancelInsert(const TInsertCookie& insertCookie, const TError& error)
+{
+ const auto& key = insertCookie.Key_;
+ auto* shard = GetShardByKey(key);
+
+ // We do not want to break the ghost cache invariants, according to which either EndInsert
+ // or CancelInsert must be called for each item in Inserting state. So we end the insertion
+ // even after ghost cache is disabled.
+ if (insertCookie.InsertedIntoSmallGhost_) {
+ shard->SmallGhost.CancelInsert(key);
+ }
+ if (insertCookie.InsertedIntoLargeGhost_) {
+ shard->LargeGhost.CancelInsert(key);
+ }
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ auto& itemMap = shard->ItemMap;
+ auto itemIt = itemMap.find(key);
+ YT_VERIFY(itemIt != itemMap.end());
+
+ auto* item = itemIt->second;
+ auto promise = item->ValuePromise;
+
+ itemMap.erase(itemIt);
+ --Size_;
+
+ YT_VERIFY(!item->Value);
+
+ delete item;
+
+ guard.Release();
+
+ promise.Set(error);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::Unregister(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ YT_VERIFY(shard->ItemMap.find(key) == shard->ItemMap.end());
+ YT_VERIFY(shard->ValueMap.erase(key) == 1);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TryRemove(const TKey& key, bool forbidResurrection)
+{
+ DoTryRemove(key, nullptr, forbidResurrection);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TryRemoveValue(const TValuePtr& value, bool forbidResurrection)
+{
+ DoTryRemove(value->GetKey(), value, forbidResurrection);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::DoTryRemove(
+ const TKey& key,
+ const TValuePtr& value,
+ bool forbidResurrection)
+{
+ auto* shard = GetShardByKey(key);
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.TryRemove(key, value);
+ shard->LargeGhost.TryRemove(key, value);
+ }
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ auto& itemMap = shard->ItemMap;
+ auto& valueMap = shard->ValueMap;
+
+ auto valueIt = valueMap.find(key);
+ if (valueIt == valueMap.end()) {
+ return;
+ }
+
+ if (value && valueIt->second != value) {
+ return;
+ }
+
+ if (forbidResurrection || !IsResurrectionSupported()) {
+ valueIt->second->Cache_.Reset();
+ valueMap.erase(valueIt);
+ }
+
+ auto itemIt = itemMap.find(key);
+ if (itemIt == itemMap.end()) {
+ return;
+ }
+
+ auto* item = itemIt->second;
+ auto actualValue = item->Value;
+ if (!actualValue) {
+ return;
+ }
+
+ itemMap.erase(itemIt);
+ --Size_;
+
+ shard->PopFromLists(item);
+
+ YT_VERIFY(actualValue->Item_ == item);
+ actualValue->Item_ = nullptr;
+
+ delete item;
+
+ guard.Release();
+
+ OnRemoved(actualValue);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::UpdateWeight(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ shard->DrainTouchBuffer();
+
+ auto itemIt = shard->ItemMap.find(key);
+ if (itemIt == shard->ItemMap.end()) {
+ return;
+ }
+
+ auto item = itemIt->second;
+ if (!item->Value) {
+ return;
+ }
+
+ i64 newWeight = GetWeight(item->Value);
+ i64 weightDelta = newWeight - item->CachedWeight;
+
+ shard->UpdateWeight(item, weightDelta);
+
+ // If item weight increases, it means that some parts of the item were missing in cache,
+ // so add delta to missed weight.
+ if (weightDelta > 0) {
+ Counters_.MissedWeightCounter.Increment(weightDelta);
+ }
+
+ NotifyOnTrim(shard->Trim(guard), nullptr);
+
+ if (GhostCachesEnabled_.load()) {
+ shard->SmallGhost.UpdateWeight(key, newWeight);
+ shard->LargeGhost.UpdateWeight(key, newWeight);
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::UpdateWeight(const TValuePtr& value)
+{
+ UpdateWeight(value->GetKey());
+}
+
+template <class TKey, class TValue, class THash>
+auto TAsyncSlruCacheBase<TKey, TValue, THash>::GetShardByKey(const TKey& key) const -> TShard*
+{
+ return &Shards_[THash()(key) & (Config_->ShardCount - 1)];
+}
+
+template <class TKey, class TValue, class THash>
+i64 TAsyncSlruCacheBase<TKey, TValue, THash>::GetWeight(const TValuePtr& /*value*/) const
+{
+ return 1;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::OnAdded(const TValuePtr& /*value*/)
+{ }
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::OnRemoved(const TValuePtr& /*value*/)
+{ }
+
+template <class TKey, class TValue, class THash>
+bool TAsyncSlruCacheBase<TKey, TValue, THash>::IsResurrectionSupported() const
+{
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+auto TAsyncSlruCacheBase<TKey, TValue, THash>::GetSmallGhostCounters() const -> const TCounters&
+{
+ return SmallGhostCounters_;
+}
+
+template <class TKey, class TValue, class THash>
+auto TAsyncSlruCacheBase<TKey, TValue, THash>::GetLargeGhostCounters() const -> const TCounters&
+{
+ return LargeGhostCounters_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+bool TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::DoLookup(const TKey& key, bool allowAsyncHits)
+{
+ auto readerGuard = ReaderGuard(SpinLock);
+
+ auto itemIt = ItemMap_.find(key);
+ if (itemIt == ItemMap_.end()) {
+ return false;
+ }
+
+ auto* item = itemIt->second;
+ if (!allowAsyncHits && !item->Inserted) {
+ return false;
+ }
+
+ bool needToDrain = this->TouchItem(item);
+
+ if (item->Inserted) {
+ Counters_->SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_->SyncHitCounter.Increment();
+ } else {
+ Counters_->AsyncHitCounter.Increment();
+ item->AsyncHitCount.fetch_add(1);
+ }
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(SpinLock);
+ this->DrainTouchBuffer();
+ }
+
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Find(const TKey& key)
+{
+ if (!DoLookup(key, false)) {
+ Counters_->MissedCounter.Increment();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Lookup(const TKey& key)
+{
+ if (!DoLookup(key, true)) {
+ Counters_->MissedCounter.Increment();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Touch(const TValuePtr& value)
+{
+ auto readerGuard = ReaderGuard(SpinLock);
+
+ if (!value) {
+ return;
+ }
+
+ auto itemIt = ItemMap_.find(value->GetKey());
+ if (itemIt == ItemMap_.end() || itemIt->second->Value.Lock() != value) {
+ return;
+ }
+ auto* item = itemIt->second;
+
+ auto needToDrain = this->TouchItem(item);
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(SpinLock);
+ this->DrainTouchBuffer();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+bool TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::BeginInsert(const TKey& key)
+{
+ if (DoLookup(key, true)) {
+ return false;
+ }
+
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto itemIt = ItemMap_.find(key);
+ if (itemIt != ItemMap_.end()) {
+ auto* item = itemIt->second;
+ this->TouchItem(item);
+
+ if (item->Inserted) {
+ Counters_->SyncHitWeightCounter.Increment(item->CachedWeight);
+ Counters_->SyncHitCounter.Increment();
+ } else {
+ Counters_->AsyncHitCounter.Increment();
+ item->AsyncHitCount.fetch_add(1);
+ }
+
+ return false;
+ }
+
+ auto* item = new TGhostItem(key);
+ Counters_->MissedCounter.Increment();
+ YT_VERIFY(ItemMap_.emplace(key, item).second);
+
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::CancelInsert(const TKey& key)
+{
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto itemIt = ItemMap_.find(key);
+ YT_VERIFY(itemIt != ItemMap_.end());
+
+ auto* item = itemIt->second;
+ YT_VERIFY(!item->Inserted);
+
+ ItemMap_.erase(itemIt);
+
+ delete item;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::EndInsert(const TValuePtr& value, i64 weight)
+{
+ YT_VERIFY(value);
+ auto key = value->GetKey();
+
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto* item = GetOrCrash(ItemMap_, key);
+
+ YT_VERIFY(!item->Inserted);
+
+ item->Value = value;
+ item->Inserted = true;
+
+ this->PushToYounger(item, weight);
+ // MissedCounter and AsyncHitCounter have already been incremented in BeginInsert.
+ Counters_->MissedWeightCounter.Increment(weight);
+ Counters_->AsyncHitWeightCounter.Increment(weight * item->AsyncHitCount.load());
+
+ // NB: Releases the lock.
+ Trim(guard);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Resurrect(const TValuePtr& value, i64 weight)
+{
+ YT_VERIFY(value);
+ auto key = value->GetKey();
+
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto itemIt = ItemMap_.find(key);
+ if (itemIt != ItemMap_.end()) {
+ return;
+ }
+
+ auto* item = new TGhostItem(key);
+ item->Value = value;
+ item->Inserted = true;
+
+ YT_VERIFY(ItemMap_.emplace(key, item).second);
+
+ this->PushToYounger(item, weight);
+
+ Counters_->SyncHitWeightCounter.Increment(weight);
+ Counters_->SyncHitCounter.Increment();
+
+ // NB: Releases the lock.
+ Trim(guard);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::TryRemove(const TKey& key, const TValuePtr& value)
+{
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto itemIt = ItemMap_.find(key);
+ if (itemIt == ItemMap_.end()) {
+ return;
+ }
+
+ auto* item = itemIt->second;
+ if (!item->Inserted) {
+ return;
+ }
+ auto actualValue = item->Value.Lock();
+ // If value is null, it means that we don't care about the removed value and remove just by key.
+ // If actualValue is null, then it refers to the value removed from the main cache, and always
+ // doesn't match our provided value. Otherwise, just compare the values. Note that the condition
+ // can be simplified just to (value && value != actualValue), but is retained as-is to make the
+ // intention more clear.
+ if (value && (!actualValue || value != actualValue)) {
+ return;
+ }
+ actualValue.Reset();
+
+ ItemMap_.erase(itemIt);
+
+ this->PopFromLists(item);
+
+ delete item;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::UpdateWeight(const TKey& key, i64 newWeight)
+{
+ auto guard = WriterGuard(SpinLock);
+
+ this->DrainTouchBuffer();
+
+ auto itemIt = ItemMap_.find(key);
+ if (itemIt == ItemMap_.end()) {
+ return;
+ }
+
+ auto item = itemIt->second;
+ if (!item->Inserted) {
+ return;
+ }
+
+ i64 weightDelta = newWeight - item->CachedWeight;
+
+ TAsyncSlruCacheListManager<TGhostItem, TGhostShard>::UpdateWeight(item, weightDelta);
+
+ // If item weight increases, it means that some parts of the item were missing in cache,
+ // so add delta to missed weight.
+ if (weightDelta > 0) {
+ Counters_->MissedWeightCounter.Increment(weightDelta);
+ }
+
+ // NB: Releases the lock.
+ Trim(guard);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Reconfigure(i64 capacity, double youngerSizeFraction)
+{
+ auto writerGuard = WriterGuard(SpinLock);
+ TAsyncSlruCacheListManager<TGhostItem, TGhostShard>::Reconfigure(capacity, youngerSizeFraction);
+ this->DrainTouchBuffer();
+ Trim(writerGuard);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TGhostShard::Trim(NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard)
+{
+ auto evictedItems = this->TrimNoDelete();
+ for (const auto& item : evictedItems) {
+ YT_VERIFY(ItemMap_.erase(item.Key) == 1);
+ }
+
+ // NB. Evicted items must die outside of critical section.
+ guard.Release();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+std::vector<typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValuePtr>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TShard::Trim(NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard)
+{
+ auto evictedItems = this->TrimNoDelete();
+
+ Parent->Size_ -= static_cast<int>(evictedItems.Size());
+
+ std::vector<TValuePtr> evictedValues;
+ for (const auto& item : evictedItems) {
+ auto value = item.Value;
+
+ YT_VERIFY(ItemMap.erase(value->GetKey()) == 1);
+
+ if (!Parent->IsResurrectionSupported()) {
+ YT_VERIFY(ValueMap.erase(value->GetKey()) == 1);
+ value->Cache_.Reset();
+ }
+
+ YT_VERIFY(value->Item_ == &item);
+ value->Item_ = nullptr;
+
+ evictedValues.push_back(std::move(value));
+ }
+
+ // NB. Evicted items must die outside of critical section.
+ guard.Release();
+
+ return evictedValues;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TShard::OnYoungerUpdated(i64 deltaCount, i64 deltaWeight)
+{
+ Parent->YoungerSizeCounter_ += deltaCount;
+ Parent->YoungerWeightCounter_ += deltaWeight;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TShard::OnOlderUpdated(i64 deltaCount, i64 deltaWeight)
+{
+ Parent->OlderSizeCounter_ += deltaCount;
+ Parent->OlderWeightCounter_ += deltaWeight;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::NotifyOnTrim(
+ const std::vector<TValuePtr>& evictedValues,
+ const TValuePtr& insertedValue)
+{
+ if (insertedValue) {
+ OnAdded(insertedValue);
+ }
+ for (const auto& value : evictedValues) {
+ OnRemoved(value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::TInsertCookie()
+ : Active_(false)
+{ }
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::TInsertCookie(const TKey& key)
+ : Key_(key)
+ , Active_(false)
+{ }
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::TInsertCookie(TInsertCookie&& other)
+ : Key_(std::move(other.Key_))
+ , Cache_(std::move(other.Cache_))
+ , ValueFuture_(std::move(other.ValueFuture_))
+ , Active_(other.Active_.exchange(false))
+ , InsertedIntoSmallGhost_(std::exchange(other.InsertedIntoSmallGhost_, false))
+ , InsertedIntoLargeGhost_(std::exchange(other.InsertedIntoLargeGhost_, false))
+{ }
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::~TInsertCookie()
+{
+ Abort();
+}
+
+template <class TKey, class TValue, class THash>
+typename TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie& TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::operator =(TInsertCookie&& other)
+{
+ if (this != &other) {
+ Abort();
+ Key_ = std::move(other.Key_);
+ Cache_ = std::move(other.Cache_);
+ ValueFuture_ = std::move(other.ValueFuture_);
+ Active_ = other.Active_.exchange(false);
+ InsertedIntoSmallGhost_ = std::exchange(other.InsertedIntoSmallGhost_, false);
+ InsertedIntoLargeGhost_ = std::exchange(other.InsertedIntoLargeGhost_, false);
+ }
+ return *this;
+}
+
+template <class TKey, class TValue, class THash>
+const TKey& TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::GetKey() const
+{
+ return Key_;
+}
+
+template <class TKey, class TValue, class THash>
+typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValueFuture
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::GetValue() const
+{
+ YT_ASSERT(ValueFuture_);
+ return ValueFuture_;
+}
+
+template <class TKey, class TValue, class THash>
+bool TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::IsActive() const
+{
+ return Active_;
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::Cancel(const TError& error)
+{
+ auto expected = true;
+ if (!Active_.compare_exchange_strong(expected, false)) {
+ return;
+ }
+
+ Cache_->CancelInsert(*this, error);
+}
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::EndInsert(TValuePtr value)
+{
+ auto expected = true;
+ if (!Active_.compare_exchange_strong(expected, false)) {
+ return;
+ }
+
+ Cache_->EndInsert(*this, value);
+}
+
+template <class TKey, class TValue, class THash>
+TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::TInsertCookie(
+ const TKey& key,
+ TIntrusivePtr<TAsyncSlruCacheBase> cache,
+ TValueFuture valueFuture,
+ bool active)
+ : Key_(key)
+ , Cache_(std::move(cache))
+ , ValueFuture_(std::move(valueFuture))
+ , Active_(active)
+{ }
+
+template <class TKey, class TValue, class THash>
+void TAsyncSlruCacheBase<TKey, TValue, THash>::TInsertCookie::Abort()
+{
+ Cancel(TError(NYT::EErrorCode::Canceled, "Cache item insertion aborted"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TMemoryTrackingAsyncSlruCacheBase<TKey, TValue, THash>::TMemoryTrackingAsyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ IMemoryUsageTrackerPtr memoryTracker,
+ const NProfiling::TProfiler& profiler)
+ : TAsyncSlruCacheBase<TKey, TValue, THash>(
+ std::move(config),
+ profiler)
+ , MemoryTracker_(std::move(memoryTracker))
+{
+ MemoryTracker_->SetLimit(this->GetCapacity());
+}
+
+template <class TKey, class TValue, class THash>
+TMemoryTrackingAsyncSlruCacheBase<TKey, TValue, THash>::~TMemoryTrackingAsyncSlruCacheBase()
+{
+ MemoryTracker_->SetLimit(0);
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingAsyncSlruCacheBase<TKey, TValue, THash>::OnAdded(const TValuePtr& value)
+{
+ MemoryTracker_->Acquire(this->GetWeight(value));
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingAsyncSlruCacheBase<TKey, TValue, THash>::OnRemoved(const TValuePtr& value)
+{
+ MemoryTracker_->Release(this->GetWeight(value));
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingAsyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(const TSlruCacheDynamicConfigPtr& config)
+{
+ if (auto newCapacity = config->Capacity) {
+ MemoryTracker_->SetLimit(*newCapacity);
+ }
+ TAsyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/async_slru_cache.h b/yt/yt/core/misc/async_slru_cache.h
new file mode 100644
index 0000000000..501c5e178a
--- /dev/null
+++ b/yt/yt/core/misc/async_slru_cache.h
@@ -0,0 +1,478 @@
+#pragma once
+
+#include "public.h"
+#include "cache_config.h"
+#include "memory_usage_tracker.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+class TAsyncSlruCacheBase;
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TAsyncCacheValueBase
+ : public virtual TRefCounted
+{
+public:
+ virtual ~TAsyncCacheValueBase();
+
+ const TKey& GetKey() const;
+
+ void UpdateWeight() const;
+
+protected:
+ explicit TAsyncCacheValueBase(const TKey& key);
+
+private:
+ using TCache = TAsyncSlruCacheBase<TKey, TValue, THash>;
+ friend class TAsyncSlruCacheBase<TKey, TValue, THash>;
+
+ TWeakPtr<TCache> Cache_;
+ TKey Key_;
+ typename TCache::TItem* Item_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Manages lists for TAsyncSlruCacheBase. It contains two lists, Younger and Older,
+//! and TouchBuffer.
+//!
+//! TouchBuffer is a temporary buffer to keep items which were touched recently. It is
+//! used for optimization, to allow touching items without holding a write lock. The
+//! following important invariant must hold: any item it TouchBuffer must be also present
+//! in either Younger or Older. In particular, TouchBuffer must not contain items that
+//! were already freed. So, it's IMPORTANT to call DrainTouchBuffer() before removing
+//! anything.
+//!
+//! Younger and Older store the items in a linked list. The rules are as follows:
+//! - When an item is inserted, it's pushed to the head Younger.
+//! - When an item is touched, it's moved to the head of Older.
+//! - Total weight of Older cannot exceed (capacity * (1 - youngerSizeFraction)).
+//! - Total weight of all items cannot exceed capacity.
+//! - The items from the tail of Older are evicted to the head of Younger.
+//! - The items from the tail of Younger are removed from this ListManager.
+template <class TItem, class TDerived>
+class TAsyncSlruCacheListManager
+{
+public:
+ void PushToYounger(TItem* item, i64 weight);
+
+ void MoveToYounger(TItem* item);
+ void MoveToOlder(TItem* item);
+
+ void PopFromLists(TItem* item);
+
+ void UpdateWeight(TItem* item, i64 weightDelta);
+
+ TIntrusiveListWithAutoDelete<TItem, TDelete> TrimNoDelete();
+
+ bool TouchItem(TItem* item);
+
+ //! Drains touch buffer. You MUST call this function before trying to remove anything from the
+ //! lists (i.e. calling PopFromLists() or TrimNoDelete()), otherwise you may catch use-after-free
+ //! bugs. It's not necessary to call it when moving from Younger to Older or from Older to Younger,
+ //! though.
+ void DrainTouchBuffer();
+
+ void Reconfigure(i64 capacity, double youngerSizeFraction);
+
+ void SetTouchBufferCapacity(i64 touchBufferCapacity);
+
+protected:
+ TDerived* AsDerived()
+ {
+ return static_cast<TDerived*>(this);
+ }
+
+ const TDerived* AsDerived() const
+ {
+ return static_cast<const TDerived*>(this);
+ }
+
+ // Callbacks to be overloaded in derived classes.
+ void OnYoungerUpdated(i64 deltaCount, i64 deltaWeight);
+ void OnOlderUpdated(i64 deltaCount, i64 deltaWeight);
+
+private:
+ TIntrusiveListWithAutoDelete<TItem, TDelete> YoungerLruList;
+ TIntrusiveListWithAutoDelete<TItem, TDelete> OlderLruList;
+
+ std::vector<TItem*> TouchBuffer;
+ std::atomic<int> TouchBufferPosition = 0;
+
+ size_t YoungerWeightCounter = 0;
+ size_t OlderWeightCounter = 0;
+
+ std::atomic<i64> Capacity;
+ std::atomic<double> YoungerSizeFraction;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Base class for asynchronous caches.
+//!
+//! The cache is asynchronous. It means that the items are not inserted immediately. Instead,
+//! one may call BeginInsert() to indicate that the item is being inserted. Then, the caller
+//! may either insert the item or cancel the insertion.
+//!
+//! It is divided onto shards. Each shard behaves as a small cache independent of others. The
+//! item is put into a shard according to hash of its key.
+//!
+//! The cache also optionally supports resurrection. If you try to lookup a value in the cache
+//! which is already evicted but it still present in memory (because someone else holds a strong
+//! pointer to this value), it returns back to the cache. This behavior may be overloaded by
+//! overriding IsResurrectionSupported() function.
+//!
+//! This cache is quite complex and has many invariants. Read about them below and change the
+//! code carefully.
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TAsyncSlruCacheBase
+ : public virtual TRefCounted
+{
+public:
+ using TValuePtr = TIntrusivePtr<TValue>;
+ using TValueFuture = TFuture<TValuePtr>;
+ using TValuePromise = TPromise<TValuePtr>;
+
+ class TInsertCookie
+ {
+ public:
+ TInsertCookie();
+ explicit TInsertCookie(const TKey& key);
+ TInsertCookie(TInsertCookie&& other);
+ TInsertCookie(const TInsertCookie& other) = delete;
+ ~TInsertCookie();
+
+ TInsertCookie& operator = (TInsertCookie&& other);
+ TInsertCookie& operator = (const TInsertCookie& other) = delete;
+
+ const TKey& GetKey() const;
+ TValueFuture GetValue() const;
+ bool IsActive() const;
+
+ void Cancel(const TError& error);
+ void EndInsert(TValuePtr value);
+
+ private:
+ friend class TAsyncSlruCacheBase;
+
+ TKey Key_;
+ TIntrusivePtr<TAsyncSlruCacheBase> Cache_;
+ TValueFuture ValueFuture_;
+ std::atomic<bool> Active_;
+ bool InsertedIntoSmallGhost_ = false;
+ bool InsertedIntoLargeGhost_ = false;
+
+ TInsertCookie(
+ const TKey& key,
+ TIntrusivePtr<TAsyncSlruCacheBase> cache,
+ TValueFuture valueFuture,
+ bool active);
+
+ void Abort();
+ };
+
+ // NB: Shards store reference to the cache, so the cache cannot be simply copied or moved.
+ TAsyncSlruCacheBase(const TAsyncSlruCacheBase&) = delete;
+ TAsyncSlruCacheBase(TAsyncSlruCacheBase&&) = delete;
+ TAsyncSlruCacheBase& operator=(const TAsyncSlruCacheBase&) = delete;
+ TAsyncSlruCacheBase& operator=(TAsyncSlruCacheBase&&) = delete;
+
+ int GetSize() const;
+ i64 GetCapacity() const;
+
+ std::vector<TValuePtr> GetAll();
+
+ TValuePtr Find(const TKey& key);
+ TValueFuture Lookup(const TKey& key);
+ void Touch(const TValuePtr& value);
+
+ TInsertCookie BeginInsert(const TKey& key);
+ void TryRemove(const TKey& key, bool forbidResurrection = false);
+ void TryRemoveValue(const TValuePtr& value, bool forbidResurrection = false);
+
+ void UpdateWeight(const TKey& key);
+ void UpdateWeight(const TValuePtr& value);
+
+ virtual void Reconfigure(const TSlruCacheDynamicConfigPtr& config);
+
+protected:
+ const TSlruCacheConfigPtr Config_;
+
+ explicit TAsyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ const NProfiling::TProfiler& profiler = {});
+
+ // Called once when the value is inserted to the cache.
+ // If item weight ever changes, UpdateWeight() should be called to apply the changes.
+ virtual i64 GetWeight(const TValuePtr& value) const;
+
+ virtual void OnAdded(const TValuePtr& value);
+ virtual void OnRemoved(const TValuePtr& value);
+
+ //! Returns true if resurrection is supported. Note that the function must always returns the same value.
+ virtual bool IsResurrectionSupported() const;
+
+protected:
+ /*!
+ * Every request counts to one of the following metric types:
+ *
+ * SyncHit* - Item is present in the cache and contains the value.
+ *
+ * AsyncHit* - Item is present in the cache and contains the value future.
+ * Caller should wait till the concurrent request sets the value.
+ *
+ * Missed* - Item is missing in the cache and should be requested.
+ *
+ * Hit/Missed counters are updated immediately, while the update of
+ * all Weight* metrics can be delayed till the EndInsert call,
+ * because we do not know the weight of the object before it arrives.
+ */
+ struct TCounters
+ {
+ explicit TCounters(const NProfiling::TProfiler& profiler);
+
+ NProfiling::TCounter SyncHitWeightCounter;
+ NProfiling::TCounter AsyncHitWeightCounter;
+ NProfiling::TCounter MissedWeightCounter;
+ NProfiling::TCounter SyncHitCounter;
+ NProfiling::TCounter AsyncHitCounter;
+ NProfiling::TCounter MissedCounter;
+ };
+
+ //! For testing purposes only.
+ const TCounters& GetSmallGhostCounters() const;
+ const TCounters& GetLargeGhostCounters() const;
+
+private:
+ friend class TAsyncCacheValueBase<TKey, TValue, THash>;
+
+ struct TItem
+ : public TIntrusiveListItem<TItem>
+ {
+ TItem();
+ explicit TItem(TValuePtr value);
+
+ TValueFuture GetValueFuture() const;
+
+ TValuePromise ValuePromise;
+ TValuePtr Value;
+ i64 CachedWeight;
+ //! Counter for accurate calculation of AsyncHitWeight.
+ //! It can be updated concurrently under the ReadLock.
+ std::atomic<int> AsyncHitCount = 0;
+ bool Younger = false;
+ };
+
+ struct TGhostItem
+ : public TIntrusiveListItem<TGhostItem>
+ {
+ explicit TGhostItem(TKey key)
+ : Key(std::move(key))
+ { }
+
+ TKey Key;
+ //! The value associated with this item. If Inserted == true and Value is null, then we refer to some
+ //! old item freed from the memory. If the main cache was bigger, than the item would be present in it.
+ //! So, we still need to keep such items in ghost shards.
+ TWeakPtr<TValue> Value;
+ i64 CachedWeight;
+ //! Counter for accurate calculation of AsyncHitWeight.
+ //! It can be updated concurrently under the ReadLock.
+ std::atomic<int> AsyncHitCount = 0;
+ bool Younger = false;
+ bool Inserted = false;
+ };
+
+ //! Ghost shard does not store any values really. They just simulate the cache with a different capacity and
+ //! update the corresponding counters. It is used to estimate what would happen if the cache had different
+ //! capacity, thus helping to tweak cache capacity. See YT-15782.
+ //!
+ //! Ghost shards are lighter than ordinary shards and don't contain ValueMap, only ItemMap. Each value in
+ //! the ghost shard is in one of the following states:
+ //!
+ //! 1. Inserting. The item is present in ItemMap, but not present in the lists. The item in ItemMap has
+ //! Inserted == false. You MUST NOT remove or update the item in this state in any place except CancelInsert()
+ //! or EndInsert(). If CancelInsert() is called, then the state becomes Destroyed. If EndInsert() is called,
+ //! then the state becomes Inserted.
+ //! 2. Inserted. The value is present in both ItemMap and lists. The item in ItemMap has Inserted == true.
+ //! Its state can be changed only to Destroyed. The value is stored as weak pointer here, so ghost shards do
+ //! not hold values. If the value is removed, then we just assume that there was some old value with the
+ //! given key, but don't know which one.
+ //! 3. Destroyed. The value is not present in ItemMap, and the corresponding item is removed from the lists and
+ //! freed.
+ //!
+ //! Ghost shards do not support resurrection.
+ class TGhostShard
+ : private TAsyncSlruCacheListManager<TGhostItem, TGhostShard>
+ {
+ public:
+ using TValuePtr = TIntrusivePtr<TValue>;
+
+ void Find(const TKey& key);
+ void Lookup(const TKey& key);
+ void Touch(const TValuePtr& value);
+
+ //! If BeginInsert() returns true, then it must be paired with either CancelInsert() or EndInsert()
+ //! called with the same key. Do not call CancelInsert() or EndInsert() without matching BeginInsert().
+ bool BeginInsert(const TKey& key);
+ void CancelInsert(const TKey& key);
+ void EndInsert(const TValuePtr& value, i64 weight);
+
+ //! Inserts the value back to the cache immediately. Called when the value is resurected in the
+ //! main cache.
+ void Resurrect(const TValuePtr& value, i64 weight);
+
+ //! If value is null, remove by key. Otherwise, remove by value. Note that value.GetKey() == key
+ //! must hold in the latter case.
+ void TryRemove(const TKey& key, const TValuePtr& value);
+
+ void UpdateWeight(const TKey& key, i64 newWeight);
+
+ using TAsyncSlruCacheListManager<TGhostItem, TGhostShard>::SetTouchBufferCapacity;
+
+ void Reconfigure(i64 capacity, double youngerSizeFraction);
+
+ DEFINE_BYVAL_RW_PROPERTY(TCounters*, Counters);
+
+ private:
+ friend class TAsyncSlruCacheListManager<TGhostItem, TGhostShard>;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock);
+
+ THashMap<TKey, TGhostItem*, THash> ItemMap_;
+
+ bool DoLookup(const TKey& key, bool allowAsyncHits);
+ void Trim(NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard);
+ };
+
+ //! Cache shard. Each shard is a small cache that can store a subset of keys. It consists of lists (see
+ //! TAsyncSlruCacheListManager), ValueMap and ItemMap (see below).
+ //!
+ //! The values in the shard may be in various states:
+ //!
+ //! 1. Inserting. The item is present in ItemMap, but isn't added into the lists. ValueMap doesn't contain
+ //! the value corresponding to this item, as there's no such value yet. Value in the item is null. You
+ //! MUST NOT remove or update the item in this state in any place except CancelInsert() or EndInsert().
+ //! If CancelInsert() is called, then the state becomes Destroyed. If EndInsert() is called, then the state
+ //! becomes Inserted.
+ //! 2. Inserted. The item is present in ItemMap and in the lists, and the corresponding value is present in
+ //! ValueMap. Value in the item must be non-null. Value->Item_ and Value->Cache_ must be set. The state
+ //! can be only changed to Ready for Resurrection or Destroying.
+ //! 3. Ready for Resurrection. The item is freed and removed from ItemMap and the lists. ValueMap holds the
+ //! corresponding value. value->Cache_ must be non-null, and value->Item_ must be null. The state can be
+ //! changed to Inserted if resurrection happens, or Destroying if it didn't happen when refcount of the
+ //! value reached zero.
+ //! 4. Destroying. The refcount of the value reached zero. It's not present in ItemMap and the lists, but is
+ //! still present in ValueMap. It's not allowed to return such value into the cache, and its state can be
+ //! only changed to Destroyed. To distinguish between Ready for Resurrection and Destroying, one may use
+ //! DangerousGetPtr() on the pointer from ValueMap. If it returned null, then the state is Destroying.
+ //! 5. Destroyed. The value and its corresponding item are freed and are not present anywhere.
+ class TShard
+ : public TAsyncSlruCacheListManager<TItem, TShard>
+ {
+ public:
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock);
+
+ //! Holds pointers to values for any given key. They are stored to allow resurrection. When the value
+ //! is freed, it will be removed from ValueMap. When the value is in Destroying state, the value will still
+ //! reside in ValueMap, you need to be careful with it.
+ THashMap<TKey, TValue*, THash> ValueMap;
+
+ //! Holds pointers to items in the lists for any given key.
+ THashMap<TKey, TItem*, THash> ItemMap;
+
+ TAsyncSlruCacheBase* Parent;
+
+ TGhostShard SmallGhost;
+ TGhostShard LargeGhost;
+
+ //! Trims the lists and releases the guard. Returns the list of evicted items.
+ std::vector<TValuePtr> Trim(NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard);
+
+ protected:
+ void OnYoungerUpdated(i64 deltaCount, i64 deltaWeight);
+ void OnOlderUpdated(i64 deltaCount, i64 deltaWeight);
+
+ friend class TAsyncSlruCacheListManager<TItem, TShard>;
+ };
+
+ friend class TShard;
+
+ std::unique_ptr<TShard[]> Shards_;
+
+ std::atomic<int> Size_ = 0;
+ std::atomic<i64> Capacity_;
+
+ TCounters Counters_;
+ TCounters SmallGhostCounters_;
+ TCounters LargeGhostCounters_;
+
+ std::atomic<i64> YoungerWeightCounter_ = 0;
+ std::atomic<i64> OlderWeightCounter_ = 0;
+ std::atomic<i64> YoungerSizeCounter_ = 0;
+ std::atomic<i64> OlderSizeCounter_ = 0;
+
+ std::atomic<bool> GhostCachesEnabled_;
+
+ TShard* GetShardByKey(const TKey& key) const;
+
+ TValueFuture DoLookup(TShard* shard, const TKey& key);
+
+ void DoTryRemove(const TKey& key, const TValuePtr& value, bool forbidResurrection);
+
+ //! Calls OnAdded on OnRemoved for the values evicted with Trim(). If the trim was caused by insertion, then
+ //! insertedValue must be the value, insertion of which caused trim. Otherwise, insertedValue must be nullptr.
+ void NotifyOnTrim(const std::vector<TValuePtr>& evictedValues, const TValuePtr& insertedValue);
+
+ void EndInsert(const TInsertCookie& insertCookie, TValuePtr value);
+ void CancelInsert(const TInsertCookie& insertCookie, const TError& error);
+ void Unregister(const TKey& key);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TMemoryTrackingAsyncSlruCacheBase
+ : public TAsyncSlruCacheBase<TKey, TValue, THash>
+{
+public:
+ explicit TMemoryTrackingAsyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ IMemoryUsageTrackerPtr memoryTracker,
+ const NProfiling::TProfiler& profiler = {});
+ ~TMemoryTrackingAsyncSlruCacheBase();
+
+ void Reconfigure(const TSlruCacheDynamicConfigPtr& config) override;
+
+protected:
+ using TValuePtr = typename TAsyncSlruCacheBase<TKey, TValue, THash>::TValuePtr;
+
+ void OnAdded(const TValuePtr& value) override;
+ void OnRemoved(const TValuePtr& value) override;
+
+private:
+ const IMemoryUsageTrackerPtr MemoryTracker_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ASYNC_SLRU_CACHE_INL_H_
+#include "async_slru_cache-inl.h"
+#undef ASYNC_SLRU_CACHE_INL_H_
diff --git a/yt/yt/core/misc/atomic_object-inl.h b/yt/yt/core/misc/atomic_object-inl.h
new file mode 100644
index 0000000000..f75722cb32
--- /dev/null
+++ b/yt/yt/core/misc/atomic_object-inl.h
@@ -0,0 +1,89 @@
+#ifndef ATOMIC_OBJECT_INL_H_
+#error "Direct inclusion of this file is not allowed, include atomic_object.h"
+// For the sake of sane code completion.
+#include "atomic_object.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+template <class U>
+TAtomicObject<T>::TAtomicObject(U&& u)
+ : Object_(std::forward<U>(u))
+{ }
+
+template <class T>
+template <class U>
+void TAtomicObject<T>::Store(U&& u)
+{
+ // NB: Using exchange to avoid destructing the old object while holding the lock.
+ std::ignore = Exchange(std::forward<U>(u));
+}
+
+template <class T>
+template <class U>
+T TAtomicObject<T>::Exchange(U&& u)
+{
+ T tmpObject = std::forward<U>(u);
+ {
+ auto guard = WriterGuard(Spinlock_);
+ std::swap(Object_, tmpObject);
+ }
+ return tmpObject;
+}
+
+template <class T>
+bool TAtomicObject<T>::CompareExchange(T& expected, const T& desired)
+{
+ auto guard = WriterGuard(Spinlock_);
+ if (Object_ == expected) {
+ auto oldObject = std::move(Object_);
+ Y_UNUSED(oldObject);
+ Object_ = desired;
+ guard.Release();
+ return true;
+ } else {
+ auto oldExpected = std::move(expected);
+ Y_UNUSED(oldExpected);
+ expected = Object_;
+ guard.Release();
+ return false;
+ }
+}
+
+template <class T>
+template <class F>
+void TAtomicObject<T>::Transform(const F& func)
+{
+ auto guard = WriterGuard(Spinlock_);
+ func(Object_);
+}
+
+template <class T>
+T TAtomicObject<T>::Load() const
+{
+ auto guard = ReaderGuard(Spinlock_);
+ return Object_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOriginal, class TSerialized>
+void ToProto(TSerialized* serialized, const TAtomicObject<TOriginal>& original)
+{
+ ToProto(serialized, original.Load());
+}
+
+template <class TOriginal, class TSerialized>
+void FromProto(TAtomicObject<TOriginal>* original, const TSerialized& serialized)
+{
+ TOriginal data;
+ FromProto(&data, serialized);
+ original->Store(std::move(data));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/atomic_object.h b/yt/yt/core/misc/atomic_object.h
new file mode 100644
index 0000000000..d75e9cfd7d
--- /dev/null
+++ b/yt/yt/core/misc/atomic_object.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A synchronization object to load and store nontrivial object.
+//! It looks like atomics but for objects.
+template <class T>
+class TAtomicObject
+{
+public:
+ TAtomicObject() = default;
+
+ template <class U>
+ TAtomicObject(U&& u);
+
+ template <class U>
+ void Store(U&& u);
+
+ //! Atomically replaces the old value with the new one and returns the old value.
+ template <class U>
+ T Exchange(U&& u);
+
+ //! Atomically checks if then current value equals #expected.
+ //! If so, replaces it with #desired and returns |true|.
+ //! Otherwise, copies it into #expected and returns |false|.
+ bool CompareExchange(T& expected, const T& desired);
+
+ //! Atomically transforms the value with function #func.
+ template <class F>
+ void Transform(const F& func);
+
+ T Load() const;
+
+private:
+ T Object_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, Spinlock_);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TOriginal, class TSerialized>
+void ToProto(TSerialized* serialized, const TAtomicObject<TOriginal>& original);
+
+template <class TOriginal, class TSerialized>
+void FromProto(TAtomicObject<TOriginal>* original, const TSerialized& serialized);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ATOMIC_OBJECT_INL_H_
+#include "atomic_object-inl.h"
+#undef ATOMIC_OBJECT_INL_H_
diff --git a/yt/yt/core/misc/atomic_ptr-inl.h b/yt/yt/core/misc/atomic_ptr-inl.h
new file mode 100644
index 0000000000..f0d4477ef2
--- /dev/null
+++ b/yt/yt/core/misc/atomic_ptr-inl.h
@@ -0,0 +1,216 @@
+#ifndef ATOMIC_PTR_INL_H_
+#error "Direct inclusion of this file is not allowed, include atomic_ptr.h"
+// For the sake of sane code completion.
+#include "atomic_ptr.h"
+#endif
+#undef ATOMIC_PTR_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> MakeStrong(const THazardPtr<T>& ptr)
+{
+ if (!ptr) {
+ return nullptr;
+ }
+
+ if (!GetRefCounter(ptr.Get())->TryRef()) {
+ static const auto& Logger = LockFreePtrLogger;
+ YT_LOG_TRACE("Failed to acquire intrusive ptr from hazard ptr (Ptr: %v)",
+ ptr.Get());
+ return nullptr;
+ }
+
+ return TIntrusivePtr<T>(ptr.Get(), false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::TAtomicPtr(std::nullptr_t)
+{ }
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::TAtomicPtr(TIntrusivePtr<T> other)
+ : Ptr_(other.Release())
+{ }
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::TAtomicPtr(TAtomicPtr&& other)
+ : Ptr_(other.Ptr_)
+{
+ other.Ptr_ = nullptr;
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::TAtomicPtr(T* ptr)
+ : Ptr_(ptr)
+{ }
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::~TAtomicPtr()
+{
+ Drop(Ptr_.load());
+}
+
+template <class T, bool EnableAcquireHazard>
+void TAtomicPtr<T, EnableAcquireHazard>::Drop(T* ptr)
+{
+ if (ptr) {
+ if constexpr (EnableAcquireHazard) {
+ RetireHazardPointer(ptr, [] (T* ptr) {
+ Unref(ptr);
+ });
+ } else {
+ Unref(ptr);
+ }
+ }
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>& TAtomicPtr<T, EnableAcquireHazard>::operator=(TIntrusivePtr<T> other)
+{
+ Exchange(std::move(other));
+ return *this;
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>& TAtomicPtr<T, EnableAcquireHazard>::operator=(std::nullptr_t)
+{
+ Exchange(TIntrusivePtr<T>());
+ return *this;
+}
+
+template <class T, bool EnableAcquireHazard>
+void TAtomicPtr<T, EnableAcquireHazard>::Reset()
+{
+ Exchange(TIntrusivePtr<T>());
+}
+
+template <class T, bool EnableAcquireHazard>
+THazardPtr<T> TAtomicPtr<T, EnableAcquireHazard>::AcquireHazard() const
+{
+ static_assert(EnableAcquireHazard, "EnableAcquireHazard must be true");
+ return DoAcquireHazard();
+}
+
+template <class T, bool EnableAcquireHazard>
+THazardPtr<T> TAtomicPtr<T, EnableAcquireHazard>::DoAcquireHazard() const
+{
+ return THazardPtr<T>::Acquire([&] {
+ return Ptr_.load(std::memory_order::acquire);
+ });
+}
+
+template <class T, bool EnableAcquireHazard>
+TIntrusivePtr<T> TAtomicPtr<T, EnableAcquireHazard>::AcquireWeak() const
+{
+ return MakeStrong(DoAcquireHazard());
+}
+
+template <class T, bool EnableAcquireHazard>
+TIntrusivePtr<T> TAtomicPtr<T, EnableAcquireHazard>::Acquire() const
+{
+ while (auto hazardPtr = DoAcquireHazard()) {
+ if (auto ptr = MakeStrong(hazardPtr)) {
+ return ptr;
+ }
+ }
+ return nullptr;
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard> TAtomicPtr<T, EnableAcquireHazard>::Exchange(TIntrusivePtr<T> other)
+{
+ auto* oldPtr = Ptr_.exchange(other.Release());
+ return TAtomicPtr<T, EnableAcquireHazard>(oldPtr);
+}
+
+template <class T, bool EnableAcquireHazard>
+void TAtomicPtr<T, EnableAcquireHazard>::Store(TIntrusivePtr<T> other)
+{
+ Exchange(std::move(other));
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard> TAtomicPtr<T, EnableAcquireHazard>::SwapIfCompare(THazardPtr<T>& compare, TIntrusivePtr<T> target)
+{
+ auto* comparePtr = compare.Get();
+ auto* targetPtr = target.Get();
+ if (Ptr_.compare_exchange_strong(comparePtr, targetPtr)) {
+ target.Release();
+ return TAtomicPtr<T, EnableAcquireHazard>(comparePtr);
+ } else {
+ compare.Reset();
+ compare = THazardPtr<T>::Acquire([&] {
+ return Ptr_.load(std::memory_order::acquire);
+ }, comparePtr);
+ }
+ return {};
+}
+
+template <class T, bool EnableAcquireHazard>
+bool TAtomicPtr<T, EnableAcquireHazard>::SwapIfCompare(T* comparePtr, TIntrusivePtr<T> target)
+{
+ static const auto& Logger = LockFreePtrLogger;
+
+ auto* targetPtr = target.Get();
+ auto* savedPtr = comparePtr;
+ if (!Ptr_.compare_exchange_strong(comparePtr, targetPtr)) {
+ YT_LOG_TRACE("CAS failed (Current: %v, Compare: %v, Target: %v)",
+ comparePtr,
+ savedPtr,
+ targetPtr);
+ return false;
+ }
+
+ YT_LOG_TRACE("CAS succeeded (Compare: %v, Target: %v)",
+ comparePtr,
+ targetPtr);
+ target.Release();
+ Drop(comparePtr);
+ return true;
+}
+
+template <class T, bool EnableAcquireHazard>
+bool TAtomicPtr<T, EnableAcquireHazard>::SwapIfCompare(const TIntrusivePtr<T>& compare, TIntrusivePtr<T> target)
+{
+ return SwapIfCompare(compare.Get(), std::move(target));
+}
+
+template <class T, bool EnableAcquireHazard>
+bool TAtomicPtr<T, EnableAcquireHazard>::SwapIfCompare(const TIntrusivePtr<T>& compare, TIntrusivePtr<T>* target)
+{
+ auto* ptr = compare.Get();
+ if (Ptr_.compare_exchange_strong(ptr, target->Ptr_)) {
+ target->Ptr_ = ptr;
+ return true;
+ }
+ return false;
+}
+
+template <class T, bool EnableAcquireHazard>
+TAtomicPtr<T, EnableAcquireHazard>::operator bool() const
+{
+ return Ptr_.load() != nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, bool EnableAcquireHazard>
+bool operator==(const TAtomicPtr<T, EnableAcquireHazard>& lhs, const TIntrusivePtr<T>& rhs)
+{
+ return lhs.Ptr_.load() == rhs.Get();
+}
+
+template <class T, bool EnableAcquireHazard>
+bool operator==(const TIntrusivePtr<T>& lhs, const TAtomicPtr<T, EnableAcquireHazard>& rhs)
+{
+ return lhs.Get() == rhs.Ptr_.load();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/atomic_ptr.h b/yt/yt/core/misc/atomic_ptr.h
new file mode 100644
index 0000000000..89104ad7bb
--- /dev/null
+++ b/yt/yt/core/misc/atomic_ptr.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "hazard_ptr.h"
+#include "intrusive_ptr.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Overload for TIntrusivePtr<T> MakeStrong(T* p).
+template <class T>
+TIntrusivePtr<T> MakeStrong(const THazardPtr<T>& ptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Holds an atomic pointer to an instance of ref-counted type |T| enabling concurrent
+//! read and write access.
+template <class T, bool EnableAcquireHazard = false>
+class TAtomicPtr
+{
+public:
+ TAtomicPtr() = default;
+ TAtomicPtr(std::nullptr_t);
+ explicit TAtomicPtr(TIntrusivePtr<T> other);
+ TAtomicPtr(TAtomicPtr&& other);
+
+ ~TAtomicPtr();
+
+ TAtomicPtr& operator=(TIntrusivePtr<T> other);
+ TAtomicPtr& operator=(std::nullptr_t);
+
+ void Reset();
+
+ //! Acquires a hazard pointer.
+ /*!
+ *
+ * Returning a hazard pointer avoids contention on ref-counter in read-heavy scenarios.
+ * The user, however, must not keep this hazard pointer alive for longer
+ * than needed. Currently there are limits on the number of HPs that a thread
+ * may concurrently maintain.
+ */
+ THazardPtr<T> AcquireHazard() const;
+
+ //! Attempts to acquire an intrusive pointer.
+ //! May return null in case of a race.
+ TIntrusivePtr<T> AcquireWeak() const;
+
+ //! Acquires an intrusive pointer.
+ TIntrusivePtr<T> Acquire() const;
+
+ TAtomicPtr<T, EnableAcquireHazard> Exchange(TIntrusivePtr<T> other);
+ void Store(TIntrusivePtr<T> other);
+
+ TAtomicPtr<T, EnableAcquireHazard> SwapIfCompare(THazardPtr<T>& compare, TIntrusivePtr<T> target);
+ bool SwapIfCompare(T* comparePtr, TIntrusivePtr<T> target);
+ bool SwapIfCompare(const TIntrusivePtr<T>& compare, TIntrusivePtr<T> target);
+ bool SwapIfCompare(const TIntrusivePtr<T>& compare, TIntrusivePtr<T>* target);
+
+ explicit operator bool() const;
+
+private:
+ explicit TAtomicPtr(T* ptr);
+
+ template <class T_, bool EnableAcquireHazard_>
+ friend bool operator==(const TAtomicPtr<T_, EnableAcquireHazard_>& lhs, const TIntrusivePtr<T_>& rhs);
+
+ template <class T_, bool EnableAcquireHazard_>
+ friend bool operator==(const TIntrusivePtr<T_>& lhs, const TAtomicPtr<T_, EnableAcquireHazard_>& rhs);
+
+ std::atomic<T*> Ptr_ = nullptr;
+
+ THazardPtr<T> DoAcquireHazard() const;
+ void Drop(T* ptr);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ATOMIC_PTR_INL_H_
+#include "atomic_ptr-inl.h"
+#undef ATOMIC_PTR_INL_H_
diff --git a/yt/yt/core/misc/backoff_strategy.cpp b/yt/yt/core/misc/backoff_strategy.cpp
new file mode 100644
index 0000000000..98494d012b
--- /dev/null
+++ b/yt/yt/core/misc/backoff_strategy.cpp
@@ -0,0 +1,69 @@
+#include "backoff_strategy.h"
+
+#include <util/random/normal.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConstantBackoffOptions::operator TExponentialBackoffOptions() const
+{
+ return TExponentialBackoffOptions{
+ .RetryCount = RetryCount,
+ .MinBackoff = Backoff,
+ .MaxBackoff = Backoff,
+ .BackoffMultiplier = 1.0,
+ .BackoffJitter = BackoffJitter
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBackoffStrategy::TBackoffStrategy(const TExponentialBackoffOptions& options)
+ : Options_(options)
+{
+ Restart();
+}
+
+void TBackoffStrategy::Restart()
+{
+ RetryIndex_ = 0;
+ Backoff_ = Options_.MinBackoff;
+ ApplyJitter();
+}
+
+bool TBackoffStrategy::NextRetry()
+{
+ if (RetryIndex_ > 0) {
+ Backoff_ = std::min(Backoff_ * Options_.BackoffMultiplier, Options_.MaxBackoff);
+ ApplyJitter();
+ }
+ return ++RetryIndex_ < Options_.RetryCount;
+}
+
+int TBackoffStrategy::GetRetryIndex() const
+{
+ return RetryIndex_;
+}
+
+int TBackoffStrategy::GetRetryCount() const
+{
+ return Options_.RetryCount;
+}
+
+TDuration TBackoffStrategy::GetBackoff() const
+{
+ return BackoffWithJitter_;
+}
+
+void TBackoffStrategy::ApplyJitter()
+{
+ auto rnd = StdNormalRandom<double>();
+ bool isNegative = rnd < 0;
+ auto jitter = std::abs(rnd) * Options_.BackoffJitter * Backoff_;
+ BackoffWithJitter_ = isNegative ? Backoff_ - jitter : Backoff_ + jitter;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/backoff_strategy.h b/yt/yt/core/misc/backoff_strategy.h
new file mode 100644
index 0000000000..1e04cb2d79
--- /dev/null
+++ b/yt/yt/core/misc/backoff_strategy.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "backoff_strategy_api.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TExponentialBackoffOptions
+{
+ static constexpr int DefaultRetryCount = 10;
+ static constexpr auto DefaultMinBackoff = TDuration::Seconds(1);
+ static constexpr auto DefaultMaxBackoff = TDuration::Seconds(5);
+ static constexpr double DefaultBackoffMultiplier = 1.5;
+ static constexpr double DefaultBackoffJitter = 0.1;
+
+ int RetryCount = DefaultRetryCount;
+ TDuration MinBackoff = DefaultMinBackoff;
+ TDuration MaxBackoff = DefaultMaxBackoff;
+ double BackoffMultiplier = DefaultBackoffMultiplier;
+ double BackoffJitter = DefaultBackoffJitter;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConstantBackoffOptions
+{
+ static constexpr int DefaultRetryCount = 10;
+ static constexpr auto DefaultBackoff = TDuration::Seconds(3);
+ static constexpr double DefaultBackoffJitter = 0.1;
+
+ int RetryCount = DefaultRetryCount;
+ TDuration Backoff = DefaultBackoff;
+ double BackoffJitter = DefaultBackoffJitter;
+
+ operator TExponentialBackoffOptions() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Implements exponential backoffs with jitter.
+class TBackoffStrategy
+{
+public:
+ explicit TBackoffStrategy(const TExponentialBackoffOptions& options);
+
+ void Restart();
+ bool NextRetry();
+
+ int GetRetryIndex() const;
+ int GetRetryCount() const;
+
+ TDuration GetBackoff() const;
+
+private:
+ const TExponentialBackoffOptions Options_;
+
+ int RetryIndex_;
+ TDuration Backoff_;
+ TDuration BackoffWithJitter_;
+
+ void ApplyJitter();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/backoff_strategy_api.h b/yt/yt/core/misc/backoff_strategy_api.h
new file mode 100644
index 0000000000..5be149a32c
--- /dev/null
+++ b/yt/yt/core/misc/backoff_strategy_api.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TExponentialBackoffOptions;
+struct TConstantBackoffOptions;
+
+DECLARE_REFCOUNTED_CLASS(TSerializableExponentialBackoffOptions)
+DECLARE_REFCOUNTED_CLASS(TSerializableConstantlBackoffOptions)
+
+class TBackoffStrategy;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/backoff_strategy_config.cpp b/yt/yt/core/misc/backoff_strategy_config.cpp
new file mode 100644
index 0000000000..f4f0e69dd8
--- /dev/null
+++ b/yt/yt/core/misc/backoff_strategy_config.cpp
@@ -0,0 +1,35 @@
+#include "backoff_strategy_config.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSerializableExponentialBackoffOptions::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("retry_count", &TThis::RetryCount)
+ .Default(DefaultRetryCount);
+ registrar.BaseClassParameter("min_backoff", &TThis::MinBackoff)
+ .Default(DefaultMinBackoff);
+ registrar.BaseClassParameter("max_backoff", &TThis::MaxBackoff)
+ .Default(DefaultMaxBackoff);
+ registrar.BaseClassParameter("backoff_multiplier", &TThis::BackoffMultiplier)
+ .Default(DefaultBackoffMultiplier);
+ registrar.BaseClassParameter("backoff_jitter", &TThis::BackoffJitter)
+ .Default(DefaultBackoffJitter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSerializableConstantBackoffOptions::Register(TRegistrar registrar)
+{
+ registrar.BaseClassParameter("retry_count", &TThis::RetryCount)
+ .Default(DefaultRetryCount);
+ registrar.BaseClassParameter("backoff", &TThis::Backoff)
+ .Default(DefaultBackoff);
+ registrar.BaseClassParameter("backoff_jitter", &TThis::BackoffJitter)
+ .Default(DefaultBackoffJitter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/backoff_strategy_config.h b/yt/yt/core/misc/backoff_strategy_config.h
new file mode 100644
index 0000000000..cb9d31d299
--- /dev/null
+++ b/yt/yt/core/misc/backoff_strategy_config.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "backoff_strategy.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializableExponentialBackoffOptions
+ : public virtual NYTree::TYsonStruct
+ , public TExponentialBackoffOptions
+{
+public:
+ REGISTER_YSON_STRUCT(TSerializableExponentialBackoffOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableExponentialBackoffOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializableConstantBackoffOptions
+ : public virtual NYTree::TYsonStruct
+ , public TConstantBackoffOptions
+{
+public:
+ REGISTER_YSON_STRUCT(TSerializableConstantBackoffOptions);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSerializableConstantBackoffOptions)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bit_packed_unsigned_vector-inl.h b/yt/yt/core/misc/bit_packed_unsigned_vector-inl.h
new file mode 100644
index 0000000000..fac8faad7b
--- /dev/null
+++ b/yt/yt/core/misc/bit_packed_unsigned_vector-inl.h
@@ -0,0 +1,338 @@
+#ifndef BIT_PACKED_UNSIGNED_VECTOR_INL_H_
+#error "Direct inclusion of this file is not allowed, include bit_packed_unsigned_vector.h"
+// For the sake of sane code completion.
+#include "bit_packed_unsigned_vector.h"
+#endif
+
+#include <util/generic/bitops.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline ui64 GetWidth(ui64 value)
+{
+ return (value == 0) ? 0 : MostSignificantBit(value) + 1;
+}
+
+inline size_t CompressedUnsignedVectorSizeInWords(ui64 maxValue, size_t count)
+{
+ // One word for the header.
+ return 1 + ((GetWidth(maxValue) * count + 63ULL) >> 6ULL);
+}
+
+inline size_t CompressedUnsignedVectorSizeInBytes(ui64 maxValue, size_t count)
+{
+ static size_t wordSize = sizeof(ui64);
+ return CompressedUnsignedVectorSizeInWords(maxValue, count) * wordSize;
+}
+
+template <class T>
+typename std::enable_if<std::is_unsigned<T>::value, size_t>::type
+BitPackUnsignedVector(TRange<T> values, ui64 maxValue, ui64* dst)
+{
+ ui64 width = GetWidth(maxValue);
+ ui64 header = values.Size();
+
+ // Check that most significant byte is empty.
+ YT_VERIFY((MaskLowerBits(8, 56) & header) == 0);
+ header |= width << 56;
+
+ // Save header.
+ *dst = header;
+
+ if (maxValue == 0) {
+ // All values are zeros.
+ return 1;
+ }
+
+ ui64* word = dst + 1;
+
+ ui8 offset = 0;
+ if (width < 64) {
+ for (auto value : values) {
+ // Cast to ui64 to do proper shifts.
+ ui64 x = value;
+ if (offset + width < 64) {
+ *word |= (x << offset);
+ offset += width;
+ } else {
+ *word |= (x << offset);
+ offset = offset + width;
+ offset &= 0x3F;
+ ++word;
+ x >>= width - offset;
+ if (x > 0) {
+ // This is important not to overstep allocated boundaries.
+ *word |= x;
+ }
+ }
+ }
+ } else {
+ // Custom path for 64-bits (especially useful since right shift on 64 bits does nothing).
+ for (auto value : values) {
+ *word = value;
+ ++word;
+ }
+ }
+
+ return (offset == 0 ? 0 : 1) + word - dst;
+}
+
+
+template <class T>
+typename std::enable_if<std::is_unsigned<T>::value, TSharedRef>::type
+BitPackUnsignedVector(TRange<T> values, ui64 maxValue)
+{
+ struct TCompressedUnsignedVectorTag {};
+
+ size_t size = CompressedUnsignedVectorSizeInBytes(maxValue, values.Size());
+ auto data = TSharedMutableRef::Allocate<TCompressedUnsignedVectorTag>(size);
+ auto actualSize = BitPackUnsignedVector(values, maxValue, reinterpret_cast<ui64*>(data.Begin()));
+ YT_VERIFY(size == actualSize * sizeof(ui64));
+
+ return data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, bool Scan>
+TBitPackedUnsignedVectorReader<T, Scan>::TBitPackedUnsignedVectorReader(const ui64* data)
+ : Data_(data + 1)
+ , Size_(*data & MaskLowerBits(56))
+ , Width_(*data >> 56)
+{
+ if (Scan) {
+ UnpackValues();
+ }
+}
+
+template <class T, bool Scan>
+TBitPackedUnsignedVectorReader<T, Scan>::TBitPackedUnsignedVectorReader()
+ : Data_(nullptr)
+ , Size_(0)
+ , Width_(0)
+{ }
+
+template <class T, bool Scan>
+inline T TBitPackedUnsignedVectorReader<T, Scan>::operator[] (size_t index) const
+{
+ YT_ASSERT(index < Size_);
+ if (Scan) {
+ return Values_[index];
+ } else {
+ return GetValue(index);
+ }
+}
+
+template <class T, bool Scan>
+inline size_t TBitPackedUnsignedVectorReader<T, Scan>::GetSize() const
+{
+ return Size_;
+}
+
+template <class T, bool Scan>
+inline size_t TBitPackedUnsignedVectorReader<T, Scan>::GetByteSize() const
+{
+ if (Data_) {
+ return (1 + ((Width_ * Size_ + 63ULL) >> 6ULL)) * sizeof(ui64);
+ } else {
+ return 0;
+ }
+}
+
+template <class T, bool Scan>
+TRange<T> TBitPackedUnsignedVectorReader<T, Scan>::GetData() const
+{
+ return MakeRange(Values_, Values_ + Size_);
+}
+
+template <class T, bool Scan>
+T TBitPackedUnsignedVectorReader<T, Scan>::GetValue(size_t index) const
+{
+ if (Width_ == 0) {
+ return 0;
+ }
+
+ ui64 bitIndex = index * Width_;
+ const ui64* word = Data_ + (bitIndex >> 6);
+ ui8 offset = bitIndex & 0x3F;
+
+ ui64 w1 = (*word) >> offset;
+ if (offset + Width_ > 64) {
+ ++word;
+ ui64 w2 = (*word & MaskLowerBits((offset + Width_) & 0x3F)) << (64 - offset);
+ return static_cast<T>(w1 | w2);
+ } else {
+ return static_cast<T>(w1 & MaskLowerBits(Width_));
+ }
+}
+
+namespace {
+
+template <class T, int Width, int Remaining>
+struct TCompressedUnsignedVectorUnrolledReader
+{
+ static Y_FORCE_INLINE void Do(ui64& data, T*& output, ui64 mask)
+ {
+ *output++ = static_cast<T>(data & mask);
+ data >>= Width;
+ TCompressedUnsignedVectorUnrolledReader<T, Width, Remaining - 1>::Do(data, output, mask);
+ }
+};
+
+template <class T, int Width>
+struct TCompressedUnsignedVectorUnrolledReader<T, Width, 0>
+{
+ static Y_FORCE_INLINE void Do(ui64& /*data*/, T*& /*output*/, ui64 /*mask*/)
+ { }
+};
+
+} // namespace
+
+template <class T, bool Scan>
+template <int Width>
+void TBitPackedUnsignedVectorReader<T, Scan>::UnpackValuesUnrolled()
+{
+ constexpr bool Aligned = (Width % 64 == 0);
+ constexpr int UnrollFactor = (64 / Width) - (Aligned ? 0 : 1);
+
+ const ui64* input = Data_;
+ auto* output = ValuesHolder_.get();
+ auto* outputEnd = output + Size_;
+ ui8 offset = 0;
+ ui64 mask = MaskLowerBits(Width_);
+
+ ui64 data = *input++;
+ while (output < outputEnd) {
+ TCompressedUnsignedVectorUnrolledReader<T, Width, UnrollFactor>::Do(data, output, mask);
+ offset += UnrollFactor * Width;
+ if (!Aligned && offset + Width <= 64) {
+ TCompressedUnsignedVectorUnrolledReader<T, Width, 1>::Do(data, output, mask);
+ offset += Width;
+ }
+ if (output >= outputEnd) {
+ break;
+ }
+ if (offset == 64) {
+ offset = 0;
+ data = *input++;
+ } else {
+ ui64 nextData = *input++;
+ ui8 nextOffset = (offset + Width) & 0x3F;
+ *output++ = static_cast<T>(((nextData & MaskLowerBits(nextOffset)) << (64 - offset)) | data);
+ data = nextData;
+ offset = nextOffset;
+ data >>= offset;
+ }
+ }
+}
+
+template <class T, bool Scan>
+void TBitPackedUnsignedVectorReader<T, Scan>::UnpackValuesFallback()
+{
+ const ui64* input = Data_;
+ auto* output = ValuesHolder_.get();
+ auto* outputEnd = output + Size_;
+ ui8 offset = 0;
+ ui64 mask = MaskLowerBits(Width_);
+ while (output != outputEnd) {
+ ui64 w1 = *input >> offset;
+ if (offset + Width_ > 64) {
+ ++input;
+ ui64 w2 = (*input & MaskLowerBits((offset + Width_) & 0x3F)) << (64 - offset);
+ *output = static_cast<T>(w1 | w2);
+ } else {
+ *output = static_cast<T>(w1 & mask);
+ }
+
+ offset = (offset + Width_) & 0x3F;
+ if (offset == 0) {
+ ++input;
+ }
+
+ ++output;
+ }
+}
+
+template <class T, bool Scan>
+template <class S>
+void TBitPackedUnsignedVectorReader<T, Scan>::UnpackValuesAligned()
+{
+ const auto* input = reinterpret_cast<const S*>(Data_);
+ auto* output = ValuesHolder_.get();
+ auto* outputEnd = output + Size_;
+ while (output != outputEnd) {
+ *output++ = *input++;
+ }
+}
+
+template <class T, bool Scan>
+void TBitPackedUnsignedVectorReader<T, Scan>::UnpackValues()
+{
+ if (Size_ == 0) {
+ return;
+ }
+
+ // Zero-copy path.
+ if (Width_ == 8 && sizeof(T) == 1 ||
+ Width_ == 16 && sizeof(T) == 2 ||
+ Width_ == 32 && sizeof(T) == 4 ||
+ Width_ == 64 && sizeof(T) == 8)
+ {
+ Values_ = reinterpret_cast<const T*>(Data_);
+ return;
+ }
+
+ // NB: Unrolled loop may unpack more values than actually needed.
+ // Make sure we have enough room for them.
+ auto valuesSize = Size_ + ((Width_ > 0) ? (64 / Width_ + 1) : 0);
+ ValuesHolder_.reset(new T[valuesSize]);
+ Values_ = ValuesHolder_.get();
+
+ switch (Width_) {
+ case 0: std::fill(ValuesHolder_.get(), ValuesHolder_.get() + Size_, 0); break;
+ #define UNROLLED(width) case width: UnpackValuesUnrolled<width>(); break;
+ #define ALIGNED(width, type) case width: UnpackValuesAligned<type>(); break;
+ UNROLLED( 1)
+ UNROLLED( 2)
+ UNROLLED( 3)
+ UNROLLED( 4)
+ UNROLLED( 5)
+ UNROLLED( 6)
+ UNROLLED( 7)
+ ALIGNED ( 8, ui8)
+ UNROLLED( 9)
+ UNROLLED(10)
+ UNROLLED(11)
+ UNROLLED(12)
+ UNROLLED(13)
+ UNROLLED(14)
+ UNROLLED(15)
+ ALIGNED (16, ui16)
+ UNROLLED(17)
+ UNROLLED(18)
+ UNROLLED(19)
+ UNROLLED(20)
+ UNROLLED(21)
+ UNROLLED(22)
+ UNROLLED(23)
+ UNROLLED(24)
+ UNROLLED(25)
+ UNROLLED(26)
+ UNROLLED(27)
+ UNROLLED(28)
+ UNROLLED(29)
+ UNROLLED(30)
+ UNROLLED(31)
+ ALIGNED (32, ui32)
+ // NB: ALIGNED(64, ui64) is redundant, cf. zero-copy path above.
+ #undef UNROLLED
+ #undef ALIGNED
+ default: UnpackValuesFallback(); break;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bit_packed_unsigned_vector.cpp b/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
new file mode 100644
index 0000000000..e75766b3bc
--- /dev/null
+++ b/yt/yt/core/misc/bit_packed_unsigned_vector.cpp
@@ -0,0 +1,32 @@
+#include "bit_packed_unsigned_vector.h"
+#include "numeric_helpers.h"
+
+#include <library/cpp/yt/coding/zig_zag.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrepareDiffFromExpected(std::vector<ui32>* values, ui32* expected, ui32* maxDiff)
+{
+ if (values->empty()) {
+ *expected = 0;
+ *maxDiff = 0;
+ return;
+ }
+
+ *expected = DivRound<int>(values->back(), values->size());
+
+ *maxDiff = 0;
+ i64 expectedValue = 0;
+ for (int i = 0; i < std::ssize(*values); ++i) {
+ expectedValue += *expected;
+ i32 diff = values->at(i) - expectedValue;
+ (*values)[i] = ZigZagEncode32(diff);
+ *maxDiff = std::max(*maxDiff, (*values)[i]);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bit_packed_unsigned_vector.h b/yt/yt/core/misc/bit_packed_unsigned_vector.h
new file mode 100644
index 0000000000..5a12aaba56
--- /dev/null
+++ b/yt/yt/core/misc/bit_packed_unsigned_vector.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "range.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+typename std::enable_if<std::is_unsigned<T>::value, TSharedRef>::type
+BitPackUnsignedVector(TRange<T> values, ui64 maxValue);
+
+/*!
+ * \note Memory allocated under #dst must be initialized with zeroes.
+ */
+template <class T>
+typename std::enable_if<std::is_unsigned<T>::value, size_t>::type
+BitPackUnsignedVector(TRange<T> values, ui64 maxValue, ui64* dst);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, bool Scan = true>
+class TBitPackedUnsignedVectorReader
+{
+public:
+ TBitPackedUnsignedVectorReader();
+ explicit TBitPackedUnsignedVectorReader(const ui64* data);
+
+ T operator[] (size_t index) const;
+
+ //! Number of elements in the vector.
+ size_t GetSize() const;
+
+ //! Number of bytes occupied by the vector.
+ size_t GetByteSize() const;
+
+ //! Returns the raw values.
+ TRange<T> GetData() const;
+
+private:
+ const ui64* Data_;
+ size_t Size_;
+ ui8 Width_;
+
+ const T* Values_;
+ std::unique_ptr<T[]> ValuesHolder_;
+
+ T GetValue(size_t index) const;
+ void UnpackValues();
+ template <int Width>
+ void UnpackValuesUnrolled();
+ template <class S>
+ void UnpackValuesAligned();
+ void UnpackValuesFallback();
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrepareDiffFromExpected(std::vector<ui32>* values, ui32* expected, ui32* maxDiff);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define BIT_PACKED_UNSIGNED_VECTOR_INL_H_
+#include "bit_packed_unsigned_vector-inl.h"
+#undef BIT_PACKED_UNSIGNED_VECTOR_INL_H_
diff --git a/yt/yt/core/misc/bit_packing-inl.h b/yt/yt/core/misc/bit_packing-inl.h
new file mode 100644
index 0000000000..fc878287e0
--- /dev/null
+++ b/yt/yt/core/misc/bit_packing-inl.h
@@ -0,0 +1,150 @@
+#ifndef BIT_PACKING_INL_H_
+#error "Direct inclusion of this file is not allowed, include bit_packing.h"
+// For the sake of sane code completion.
+#include "bit_packing.h"
+#endif
+#undef BIT_PACKING_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+size_t UnpackBitVector(TCompressedVectorView view, std::vector<T>* container)
+{
+ container->resize(view.GetSize());
+ view.UnpackTo(container->data());
+ return view.GetSizeInWords();
+}
+
+template <class T>
+size_t UnpackBitVector(const ui64* input, std::vector<T>* container)
+{
+ TCompressedVectorView view(input);
+ return UnpackBitVector(view, container);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline size_t GetCompressedVectorSize(const void* ptr)
+{
+ return *static_cast<const ui64*>(ptr) & MaskLowerBits(56);
+}
+
+inline size_t GetCompressedVectorWidth(const void* ptr)
+{
+ return *static_cast<const ui64*>(ptr) >> 56;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TCompressedViewBase::TCompressedViewBase(ui32 size, ui8 width)
+ : Size_(size)
+ , Width_(width)
+{ }
+
+inline size_t TCompressedViewBase::GetSize() const
+{
+ return Size_;
+}
+
+inline size_t TCompressedViewBase::GetWidth() const
+{
+ return Width_;
+}
+
+inline size_t TCompressedViewBase::GetSizeInWords() const
+{
+ return 1 + (GetWidth() * GetSize() + WordSize - 1) / WordSize;
+}
+
+inline size_t TCompressedViewBase::GetSizeInBytes() const
+{
+ return GetSizeInWords() * sizeof(ui64);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TCompressedVectorView::TCompressedVectorView(const ui64* ptr, ui32 size, ui8 width)
+ // Considering width here helps to avoid extra branch when extracting element.
+ : TCompressedViewBase(size, width)
+ , Ptr_(ptr + (width != 0))
+{
+ YT_ASSERT(GetCompressedVectorSize(ptr) == size);
+ YT_ASSERT(GetCompressedVectorWidth(ptr) == width);
+}
+
+inline TCompressedVectorView::TCompressedVectorView(const ui64* ptr)
+ : TCompressedVectorView(ptr, GetCompressedVectorSize(ptr), GetCompressedVectorWidth(ptr))
+{ }
+
+inline void TCompressedVectorView::Prefetch(size_t index) const
+{
+ YT_ASSERT(index < GetSize());
+ auto width = GetWidth();
+ auto bitIndex = index * width;
+ const auto* data = Ptr_ + bitIndex / WordSize;
+ // Prefetch data into all levels of the cache hierarchy.
+ Y_PREFETCH_READ(data, 3);
+}
+
+inline TCompressedVectorView::TWord TCompressedVectorView::operator[] (size_t index) const
+{
+ YT_ASSERT(index < GetSize());
+ auto width = GetWidth();
+ auto bitIndex = index * width;
+
+ const auto* data = Ptr_ + bitIndex / WordSize;
+ ui8 offset = bitIndex % WordSize;
+
+ TWord w = data[0] >> offset;
+ if (offset + width > WordSize) {
+ w |= data[1] << (WordSize - offset);
+ }
+
+ return w & MaskLowerBits(width);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TCompressedVectorView32::TCompressedVectorView32(const ui64* ptr, ui32 size, ui8 width)
+ // Considering width here helps to avoid extra branch when extracting element.
+ : TCompressedViewBase(size, width)
+ , Ptr_(ptr + 1)
+{
+ YT_ASSERT(GetCompressedVectorSize(ptr) == size);
+ YT_ASSERT(GetCompressedVectorWidth(ptr) == width);
+}
+
+inline TCompressedVectorView32::TCompressedVectorView32(const ui64* ptr)
+ : TCompressedVectorView32(ptr, GetCompressedVectorSize(ptr), GetCompressedVectorWidth(ptr))
+{ }
+
+void TCompressedVectorView32::Prefetch(size_t index) const
+{
+ YT_ASSERT(index < GetSize());
+ auto width = GetWidth();
+ auto bitIndexEnd = (index + 1) * width;
+ const auto* data = reinterpret_cast<const ui8*>(Ptr_) + (bitIndexEnd + 7) / 8;
+ // Prefetch data into all levels of the cache hierarchy.
+ Y_PREFETCH_READ(reinterpret_cast<const TWord*>(data) - 1, 3);
+}
+
+ui32 TCompressedVectorView32::operator[] (size_t index) const
+{
+ YT_ASSERT(index < GetSize());
+ auto width = GetWidth();
+ // Read without crossing upper memory bound of compressed view.
+ auto bitIndexEnd = (index + 1) * width;
+ auto byteIndexEnd = (bitIndexEnd + 7) / 8;
+
+ const auto* data = reinterpret_cast<const ui8*>(Ptr_) + byteIndexEnd;
+ // Unaligned read is used to eliminate extra branch.
+ size_t offset = WordSize - width + bitIndexEnd - 8 * byteIndexEnd;
+ TWord w = reinterpret_cast<const TWord*>(data)[-1] >> offset;
+ return w & MaskLowerBits(width);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bit_packing.cpp b/yt/yt/core/misc/bit_packing.cpp
new file mode 100644
index 0000000000..81a466240d
--- /dev/null
+++ b/yt/yt/core/misc/bit_packing.cpp
@@ -0,0 +1,341 @@
+#include "bit_packing.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class TInput, class TOutput>
+void DoUnpackSimple(const TInput* input, TOutput* output, TOutput* outputEnd, ui8 width, ui8 bitOffset)
+{
+ constexpr ui8 Bits = sizeof(TInput) * 8;
+ YT_VERIFY(width != 0);
+
+ TInput mask = MaskLowerBits(width);
+ while (output != outputEnd) {
+ TInput word = input[0] >> bitOffset;
+
+ if (bitOffset + width > Bits) {
+ word |= input[1] << (Bits - bitOffset);
+ }
+
+ *output++ = static_cast<TOutput>(word & mask);
+
+ bitOffset += width;
+ input += bitOffset / Bits;
+ bitOffset &= Bits - 1;
+ }
+}
+
+template <class TOutput, int Width, int Remaining>
+struct TBitPackedUnroller
+{
+ static constexpr ui64 Mask = (1ULL << Width) - 1;
+ static constexpr ui8 UnrollFactor = 64 / Width;
+ static constexpr ui8 Index = UnrollFactor - Remaining;
+
+ static Y_FORCE_INLINE void Do(ui64 data, TOutput* output)
+ {
+ output[Index] = static_cast<TOutput>((data >> (Width * Index)) & Mask);
+ TBitPackedUnroller<TOutput, Width, Remaining - 1>::Do(data, output);
+ }
+};
+
+template <class TOutput, int Width>
+struct TBitPackedUnroller<TOutput, Width, 0>
+{
+ static Y_FORCE_INLINE void Do(ui64 /*data*/, TOutput* /*output*/)
+ { }
+};
+
+template <int Width, class TInput, class TOutput>
+void DoUnpackUnrolled(const TInput* input, TOutput* output, TOutput* outputEnd, ui8 bitOffset)
+{
+ constexpr ui8 Bits = sizeof(TInput) * 8;
+ constexpr TInput Mask = (1ULL << Width) - 1;
+
+ constexpr ui8 UnrollFactor = Bits / Width;
+ constexpr ui8 UnrollWidth = UnrollFactor * Width;
+
+ while (output + UnrollFactor < outputEnd) {
+ TInput word = input[0] >> bitOffset;
+
+ if (bitOffset + UnrollWidth > Bits) {
+ word |= input[1] << (Bits - bitOffset);
+ }
+
+ bitOffset += UnrollWidth;
+ input += bitOffset / Bits;
+ bitOffset &= Bits - 1;
+
+ for (int i = 0; i < static_cast<int>(UnrollFactor); ++i) {
+ *output++ = static_cast<TOutput>(word & Mask);
+ word >>= Width;
+ }
+ }
+
+ DoUnpackSimple(input, output, outputEnd, Width, bitOffset);
+}
+
+template <int Width, class TInput, class TOutput>
+Y_NO_INLINE void UnpackValues(const TInput* input, TOutput* output, TOutput* outputEnd, size_t startOffset = 0)
+{
+ constexpr ui8 Bits = sizeof(TInput) * 8;
+ static_assert(Width != 0);
+
+ ui8 bitOffset = startOffset * Width % Bits;
+ input += startOffset * Width / Bits;
+
+ DoUnpackUnrolled<Width>(input, output, outputEnd, bitOffset);
+}
+
+template <class TInput, class TOutput>
+Y_NO_INLINE void UnpackValuesFallback(
+ const TInput* input,
+ TOutput* output,
+ TOutput* outputEnd,
+ ui8 width,
+ size_t startOffset = 0)
+{
+ constexpr ui8 Bits = sizeof(TInput) * 8;
+ YT_VERIFY(width != 0);
+
+ ui8 bitOffset = startOffset * width % Bits;
+ input += startOffset * width / Bits;
+
+ DoUnpackSimple(input, output, outputEnd, width, bitOffset);
+}
+
+template <class TInput, class TOutput, ui8 Width, ui8 Remaining>
+struct TUnroller
+{
+ static constexpr ui8 Bits = sizeof(TInput) * 8;
+ static constexpr ui8 UnrollFactor = Bits / std::gcd(Width, Bits);
+ static constexpr TInput Mask = (1ULL << Width) - 1;
+
+ static Y_FORCE_INLINE void Do(const TInput* input, TOutput* output)
+ {
+ constexpr ui8 Index = UnrollFactor - Remaining;
+ constexpr ui8 Offset = Index * Width % Bits;
+
+ auto value = input[Index * Width / Bits] >> Offset;
+ if constexpr (Offset + Width > Bits) {
+ value |= input[(Index + 1) * Width / Bits] << (Bits - Offset);
+ }
+
+ output[Index] = static_cast<TOutput>(value & Mask);
+
+ TUnroller<TInput, TOutput, Width, Remaining - 1>::Do(input, output);
+ }
+};
+
+template <class TInput, class TOutput, ui8 Width>
+struct TUnroller<TInput, TOutput, Width, 0>
+{
+ static Y_FORCE_INLINE void Do(const TInput* /*input*/, TOutput* /*output*/)
+ { }
+};
+
+template <int Width, class TInput, class TOutput>
+Y_NO_INLINE void UnpackFullyUnrolled(const TInput* input, TOutput* output, TOutput* outputEnd)
+{
+ constexpr ui8 Bits = sizeof(TInput) * 8;
+ constexpr ui8 Gcd = std::gcd(Width, Bits);
+ constexpr ui8 UnrollFactor = Bits / Gcd;
+ constexpr ui8 UnrollBatch = Width / Gcd;
+
+ while (output + UnrollFactor < outputEnd) {
+ TUnroller<TInput, TOutput, Width, UnrollFactor>::Do(input, output);
+ output += UnrollFactor;
+ input += UnrollBatch;
+ }
+
+ DoUnpackSimple(input, output, outputEnd, Width, 0);
+}
+
+template <class TInput, class TOutput>
+void UnpackAligned(const TInput* input, TOutput* output, TOutput* outputEnd)
+{
+ while (output != outputEnd) {
+ *output++ = *input++;
+ }
+}
+
+} // namespace NDetail
+
+template <class TOutput>
+void TCompressedVectorView::UnpackTo(TOutput* output)
+{
+ auto size = GetSize();
+ auto width = GetWidth();
+ auto input = Ptr_;
+
+ YT_VERIFY(width <= 8 * sizeof(TOutput));
+
+ switch (width) {
+ case 0: std::fill(output, output + size, 0); break;
+ // Cast to const ui32* produces less instructions.
+ #define UNROLLED(width, type) \
+ case width: \
+ NDetail::UnpackFullyUnrolled<width>(reinterpret_cast<const type*>(input), output, output + size); \
+ break;
+ #define ALIGNED(width, type) \
+ case width: \
+ NDetail::UnpackAligned(reinterpret_cast<const type*>(input), output, output + size); \
+ break;
+ UNROLLED( 1, ui8)
+ UNROLLED( 2, ui8)
+ UNROLLED( 3, ui32)
+ UNROLLED( 4, ui8)
+ UNROLLED( 5, ui32)
+ UNROLLED( 6, ui32)
+ UNROLLED( 7, ui32)
+
+ UNROLLED( 9, ui32)
+ UNROLLED(10, ui32)
+ UNROLLED(11, ui32)
+ UNROLLED(12, ui32)
+ UNROLLED(13, ui32)
+ UNROLLED(14, ui32)
+ UNROLLED(15, ui32)
+
+ UNROLLED(17, ui32)
+ UNROLLED(18, ui32)
+ UNROLLED(19, ui32)
+ UNROLLED(20, ui32)
+ UNROLLED(21, ui32)
+ UNROLLED(22, ui32)
+ UNROLLED(23, ui32)
+ UNROLLED(24, ui32)
+ UNROLLED(25, ui32)
+ UNROLLED(26, ui32)
+ UNROLLED(27, ui32)
+ UNROLLED(28, ui32)
+ UNROLLED(29, ui32)
+ UNROLLED(30, ui32)
+ UNROLLED(31, ui32)
+
+ ALIGNED( 8, ui8)
+ ALIGNED(16, ui16)
+ ALIGNED(32, ui32)
+ ALIGNED(64, ui64)
+
+ #undef UNROLLED
+ #undef ALIGNED
+ default:
+ NDetail::UnpackValuesFallback(input, output, output + size, width);
+ break;
+ }
+}
+
+template <class TOutput>
+void TCompressedVectorView::UnpackTo(TOutput* output, ui32 start, ui32 end)
+{
+ auto size = GetSize();
+ auto width = GetWidth();
+ auto input = Ptr_;
+
+ YT_VERIFY(end <= size);
+
+ YT_VERIFY(width <= 8 * sizeof(TOutput));
+
+ if (width == 0) {
+ std::fill(output, output + end - start, 0);
+ } else {
+ switch (width) {
+ case 0: std::fill(output, output + end - start, 0); break;
+ // Cast to const ui32* produces less instructions.
+ #define UNROLLED(Width, type) \
+ case Width: \
+ NDetail::UnpackValues<Width>(reinterpret_cast<const type*>(input), output, output + end - start, start); \
+ break;
+ #define ALIGNED(width, type) \
+ case width: \
+ NDetail::UnpackAligned(reinterpret_cast<const type*>(input) + start, output, output + end - start); \
+ break;
+ UNROLLED( 1, ui32)
+ UNROLLED( 2, ui8)
+ UNROLLED( 3, ui32)
+ UNROLLED( 4, ui8)
+ UNROLLED( 5, ui32)
+ UNROLLED( 6, ui32)
+ UNROLLED( 7, ui32)
+
+ UNROLLED( 9, ui64)
+ UNROLLED(10, ui64)
+ UNROLLED(11, ui64)
+ UNROLLED(12, ui64)
+ UNROLLED(13, ui64)
+ UNROLLED(14, ui64)
+ UNROLLED(15, ui64)
+
+ UNROLLED(17, ui64)
+ UNROLLED(18, ui64)
+ UNROLLED(19, ui64)
+ UNROLLED(20, ui64)
+ UNROLLED(21, ui64)
+ UNROLLED(22, ui64)
+ UNROLLED(23, ui64)
+ UNROLLED(24, ui64)
+ UNROLLED(25, ui64)
+ UNROLLED(26, ui64)
+ UNROLLED(27, ui64)
+ UNROLLED(28, ui64)
+ UNROLLED(29, ui64)
+ UNROLLED(30, ui64)
+ UNROLLED(31, ui64)
+
+ ALIGNED( 8, ui8)
+ ALIGNED(16, ui16)
+ ALIGNED(32, ui32)
+ ALIGNED(64, ui64)
+
+ #undef UNROLLED
+ #undef ALIGNED
+ default:
+ NDetail::UnpackValuesFallback(input, output, output + end - start, width, start);
+ break;
+ }
+
+#ifndef NDEBUG
+ for (int i = 0; i < int(end - start); ++i) {
+ YT_VERIFY(TWord(output[i]) == (*this)[i + start]);
+ }
+#endif
+ }
+}
+
+template
+void TCompressedVectorView::UnpackTo<ui8>(ui8* output);
+
+template
+void TCompressedVectorView::UnpackTo<ui16>(ui16* output);
+
+template
+void TCompressedVectorView::UnpackTo<ui32>(ui32* output);
+
+template
+void TCompressedVectorView::UnpackTo<ui64>(ui64* output);
+
+template
+void TCompressedVectorView::UnpackTo<i64>(i64* output);
+
+template
+void TCompressedVectorView::UnpackTo<ui8>(ui8* output, ui32 start, ui32 end);
+
+template
+void TCompressedVectorView::UnpackTo<ui16>(ui16* output, ui32 start, ui32 end);
+
+template
+void TCompressedVectorView::UnpackTo<ui32>(ui32* output, ui32 start, ui32 end);
+
+template
+void TCompressedVectorView::UnpackTo<ui64>(ui64* output, ui32 start, ui32 end);
+
+template
+void TCompressedVectorView::UnpackTo<i64>(i64* output, ui32 start, ui32 end);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bit_packing.h b/yt/yt/core/misc/bit_packing.h
new file mode 100644
index 0000000000..3433bfa708
--- /dev/null
+++ b/yt/yt/core/misc/bit_packing.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <numeric>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompressedViewBase
+{
+public:
+ using TWord = ui64;
+ static constexpr ui8 WordSize = sizeof(TWord) * 8;
+
+ TCompressedViewBase() = default;
+
+ TCompressedViewBase(ui32 size, ui8 width);
+
+ size_t GetSize() const;
+ size_t GetWidth() const;
+ size_t GetSizeInWords() const;
+ size_t GetSizeInBytes() const;
+
+private:
+ ui32 Size_ = 0;
+ ui8 Width_ = 0;
+};
+
+class TCompressedVectorView
+ : public TCompressedViewBase
+{
+public:
+ TCompressedVectorView() = default;
+
+ TCompressedVectorView(const ui64* ptr, ui32 size, ui8 width);
+
+ explicit TCompressedVectorView(const ui64* ptr);
+
+ Y_FORCE_INLINE void Prefetch(size_t index) const;
+
+ Y_FORCE_INLINE TWord operator[] (size_t index) const;
+
+ template <class T>
+ void UnpackTo(T* output);
+
+ template <class T>
+ void UnpackTo(T* output, ui32 start, ui32 end);
+
+protected:
+ static constexpr int WidthBitsOffset = 56;
+
+ const ui64* Ptr_ = nullptr;
+};
+
+class TCompressedVectorView32
+ : public TCompressedViewBase
+{
+public:
+ TCompressedVectorView32() = default;
+
+ TCompressedVectorView32(const ui64* ptr, ui32 size, ui8 width);
+
+ explicit TCompressedVectorView32(const ui64* ptr);
+
+ Y_FORCE_INLINE void Prefetch(size_t index) const;
+
+ Y_FORCE_INLINE ui32 operator[] (size_t index) const;
+
+private:
+ const ui64* Ptr_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+size_t UnpackBitVector(TCompressedVectorView view, std::vector<T>* container);
+
+template <class T>
+size_t UnpackBitVector(const ui64* input, std::vector<T>* container);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define BIT_PACKING_INL_H_
+#include "bit_packing-inl.h"
+#undef BIT_PACKING_INL_H_
diff --git a/yt/yt/core/misc/bitmap.cpp b/yt/yt/core/misc/bitmap.cpp
new file mode 100644
index 0000000000..9f3ac77608
--- /dev/null
+++ b/yt/yt/core/misc/bitmap.cpp
@@ -0,0 +1,84 @@
+#include "bitmap.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CopyBitmap(void* dst, ui32 dstOffset, const void* src, ui32 srcOffset, ui32 count)
+{
+ if (!count) {
+ return;
+ }
+
+ auto dstOffsetSave = dstOffset;
+ auto srcOffsetSave = srcOffset;
+ Y_UNUSED(srcOffsetSave);
+ Y_UNUSED(dstOffsetSave);
+
+ using TWord = ui64;
+
+ constexpr ui8 Bits = 8 * sizeof(TWord);
+
+ auto* dstPtr = static_cast<TWord*>(dst);
+ auto* srcPtr = static_cast<const TWord*>(src);
+
+ dstPtr += dstOffset / Bits;
+ dstOffset %= Bits;
+
+ srcPtr += srcOffset / Bits;
+ srcOffset %= Bits;
+
+ auto* srcEnd = srcPtr + (srcOffset + count + Bits - 1) / Bits;
+ auto* dstEnd = dstPtr + (dstOffset + count + Bits - 1) / Bits;
+
+ {
+ // Read min(count, Bits - dstOffset).
+ auto word = srcPtr[0] >> srcOffset;
+ if (srcOffset + count > Bits && srcOffset > dstOffset) {
+ word |= srcPtr[1] << (Bits - srcOffset);
+ }
+
+ TWord firstWordMask = (TWord(1) << dstOffset) - 1;
+ *dstPtr++ = (*dstPtr & firstWordMask) | (word << dstOffset);
+
+ srcOffset += Bits - dstOffset;
+ // Now dstOffset is zero.
+
+ srcPtr += srcOffset / Bits;
+ srcOffset %= Bits;
+ }
+
+ if (srcOffset) {
+ if (srcPtr != srcEnd) {
+ auto srcWord = *srcPtr++;
+ auto dstWord = srcWord >> srcOffset;
+ while (srcPtr != srcEnd) {
+ srcWord = *srcPtr++;
+ dstWord |= srcWord << (Bits - srcOffset);
+ *dstPtr++ = dstWord;
+ dstWord = srcWord >> srcOffset;
+ }
+
+ if (dstPtr != dstEnd) {
+ *dstPtr++ = dstWord;
+ }
+ }
+ } else {
+ while (srcPtr != srcEnd) {
+ *dstPtr++ = *srcPtr++;
+ }
+ }
+
+#ifndef NDEBUG
+ TBitmap srcBitmap(src);
+ TBitmap dstBitmap(dst);
+
+ for (int index = 0; index < static_cast<int>(count); ++index) {
+ YT_VERIFY(srcBitmap[srcOffsetSave + index] == dstBitmap[dstOffsetSave + index]);
+ }
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bitmap.h b/yt/yt/core/misc/bitmap.h
new file mode 100644
index 0000000000..eec263a295
--- /dev/null
+++ b/yt/yt/core/misc/bitmap.h
@@ -0,0 +1,223 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <util/system/align.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NBitmapDetail {
+ // No need to use word size other than ui8.
+ // Performance is same for ui8, ui16, ui32 and ui64 and is equal to 200 Mb/s.
+
+ // BM_Bitmap_ui8 153148 ns 153108 ns 4492 bytes_per_second=204.105M/s
+ // BM_Bitmap_ui16 151758 ns 151720 ns 4462 bytes_per_second=205.971M/s
+ // BM_Bitmap_ui32 150381 ns 150352 ns 4672 bytes_per_second=207.846M/s
+ // BM_Bitmap_ui64 152476 ns 152442 ns 4668 bytes_per_second=204.996M/s
+
+ // We do not want to force alignment when reading bitmaps, hence we should prefer ui8.
+ // Alignment/padding in serialization should be used explicitly.
+
+ using TByte = ui8;
+ constexpr static size_t Bits = 8 * sizeof(TByte);
+ constexpr size_t SerializationAlignment = 8;
+
+ constexpr static TByte GetBitMask(size_t index)
+ {
+ return TByte(1) << (index % Bits);
+ }
+
+ constexpr static size_t GetWordIndex(size_t index)
+ {
+ return index / Bits;
+ }
+
+ constexpr static size_t GetByteSize(size_t size)
+ {
+ return (size + Bits - 1) / Bits;
+ }
+} // namespace NBitmapDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMutableBitmap
+{
+public:
+ using TByte = NBitmapDetail::TByte;
+
+ explicit TMutableBitmap(void* ptr = nullptr)
+ : Ptr_(static_cast<TByte*>(ptr))
+ { }
+
+ bool operator [] (size_t index) const
+ {
+ return Ptr_[NBitmapDetail::GetWordIndex(index)] & NBitmapDetail::GetBitMask(index);
+ }
+
+ void Set(size_t index)
+ {
+ Ptr_[NBitmapDetail::GetWordIndex(index)] |= NBitmapDetail::GetBitMask(index);
+ }
+
+ void Set(size_t index, bool value)
+ {
+ auto mask = NBitmapDetail::GetBitMask(index);
+ auto& word = Ptr_[NBitmapDetail::GetWordIndex(index)];
+ word = (word & ~mask) | (-value & mask);
+ }
+
+ TByte* GetData()
+ {
+ return Ptr_;
+ }
+
+ const TByte* GetData() const
+ {
+ return Ptr_;
+ }
+
+private:
+ TByte* Ptr_;
+};
+
+class TBitmap
+{
+public:
+ using TByte = NBitmapDetail::TByte;
+
+ TBitmap(const TMutableBitmap& bitmap)
+ : Ptr_(bitmap.GetData())
+ { }
+
+ explicit TBitmap(const void* ptr = nullptr)
+ : Ptr_(static_cast<const TByte*>(ptr))
+ { }
+
+ bool operator [] (size_t index) const
+ {
+ return Ptr_[NBitmapDetail::GetWordIndex(index)] & NBitmapDetail::GetBitMask(index);
+ }
+
+ void Prefetch(size_t index) const
+ {
+ // Prefetch data into all levels of the cache hierarchy.
+ Y_PREFETCH_READ(Ptr_ + NBitmapDetail::GetWordIndex(index), 3);
+ }
+
+ const TByte* GetData() const
+ {
+ return Ptr_;
+ }
+
+private:
+ const TByte* Ptr_;
+};
+
+static_assert(sizeof(TBitmap) == sizeof(void*), "Do not modify TBitmap. Write your own class.");
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CopyBitmap(void* dst, ui32 dstOffset, const void* src, ui32 srcOffset, ui32 count);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Like TBlobOutput.
+class TBitmapOutput
+{
+public:
+ using TByte = NBitmapDetail::TByte;
+
+ explicit TBitmapOutput(size_t bitCapacity = 0)
+ {
+ if (bitCapacity) {
+ Chunks_.reserve(
+ AlignUp(NBitmapDetail::GetByteSize(bitCapacity), NBitmapDetail::SerializationAlignment));
+ }
+ }
+
+ void Append(bool value)
+ {
+ if (Chunks_.size() * NBitmapDetail::Bits == BitSize_) {
+ Chunks_.resize(Chunks_.size() + NBitmapDetail::SerializationAlignment, 0);
+ }
+ TMutableBitmap(Chunks_.data()).Set(BitSize_++, value);
+ }
+
+ bool operator[](size_t bitIndex) const
+ {
+ YT_ASSERT(bitIndex < BitSize_);
+ return Chunks_[NBitmapDetail::GetWordIndex(bitIndex)] & NBitmapDetail::GetBitMask(bitIndex);
+ }
+
+ template <class TTag>
+ TSharedRef Flush()
+ {
+ YT_ASSERT(Chunks_.size() == GetByteSize());
+ return TSharedRef::MakeCopy<TTag>(TRef(GetData(), GetByteSize()));
+ }
+
+ const TByte* GetData() const
+ {
+ return Chunks_.data();
+ }
+
+ size_t GetBitSize() const
+ {
+ return BitSize_;
+ }
+
+ size_t GetByteSize() const
+ {
+ return AlignUp(Chunks_.size() * sizeof(TByte), NBitmapDetail::SerializationAlignment);
+ }
+
+private:
+ TCompactVector<TByte, NBitmapDetail::SerializationAlignment> Chunks_;
+ size_t BitSize_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadOnlyBitmap
+ : public TBitmap
+{
+public:
+ using TByte = NBitmapDetail::TByte;
+
+ TReadOnlyBitmap() = default;
+
+ TReadOnlyBitmap(const void* chunks, size_t bitSize)
+ : TBitmap(chunks)
+ , BitSize_(bitSize)
+ { }
+
+ void Reset(const void* chunks, size_t bitSize)
+ {
+ YT_VERIFY(chunks);
+ static_cast<TBitmap&>(*this) = TBitmap(chunks);
+ BitSize_ = bitSize;
+ }
+
+ size_t GetByteSize() const
+ {
+ return NBitmapDetail::GetByteSize(BitSize_);
+ }
+
+ TRef GetData() const
+ {
+ return TRef(TBitmap::GetData(), GetByteSize());
+ }
+
+private:
+ size_t BitSize_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/blob.h b/yt/yt/core/misc/blob.h
new file mode 100644
index 0000000000..173a38df50
--- /dev/null
+++ b/yt/yt/core/misc/blob.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/blob.h>
diff --git a/yt/yt/core/misc/blob_output.cpp b/yt/yt/core/misc/blob_output.cpp
new file mode 100644
index 0000000000..04df3f7b37
--- /dev/null
+++ b/yt/yt/core/misc/blob_output.cpp
@@ -0,0 +1,105 @@
+#include "blob_output.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr size_t InitialBlobOutputCapacity = 16;
+static constexpr double BlobOutputCapacityMultiplier = 1.5;
+
+TBlobOutput::TBlobOutput(
+ size_t capacity,
+ bool pageAligned,
+ TRefCountedTypeCookie tagCookie)
+ : Blob_(
+ tagCookie,
+ /*size*/ 0,
+ /*initializeStorage*/ true,
+ pageAligned)
+{
+ Reserve(capacity);
+}
+
+size_t TBlobOutput::DoNext(void** ptr)
+{
+ if (Blob_.Size() == Blob_.Capacity()) {
+ if (Blob_.Capacity() >= InitialBlobOutputCapacity) {
+ Reserve(static_cast<size_t>(Blob_.Capacity() * BlobOutputCapacityMultiplier));
+ } else {
+ Reserve(InitialBlobOutputCapacity);
+ }
+ }
+ auto previousSize = Blob_.Size();
+ Blob_.Resize(Blob_.Capacity(), /*initializeStorage*/ false);
+ *ptr = Blob_.Begin() + previousSize;
+ return Blob_.Size() - previousSize;
+}
+
+void TBlobOutput::DoUndo(size_t len)
+{
+ YT_VERIFY(len <= Blob_.Size());
+ Blob_.Resize(Blob_.Size() - len);
+}
+
+void TBlobOutput::DoWrite(const void* buffer, size_t length)
+{
+ Blob_.Append(buffer, length);
+}
+
+void TBlobOutput::Reserve(size_t capacity)
+{
+ Blob_.Reserve(RoundUpToPage(capacity));
+}
+
+void TBlobOutput::Clear()
+{
+ Blob_.Clear();
+}
+
+TSharedRef TBlobOutput::Flush()
+{
+ auto result = TSharedRef::FromBlob(std::move(Blob_));
+ Blob_.Clear();
+ return result;
+}
+
+void swap(TBlobOutput& left, TBlobOutput& right)
+{
+ if (&left != &right) {
+ swap(left.Blob_, right.Blob_);
+ }
+}
+
+TBlob& TBlobOutput::Blob()
+{
+ return Blob_;
+}
+
+const TBlob& TBlobOutput::Blob() const
+{
+ return Blob_;
+}
+
+const char* TBlobOutput::Begin() const
+{
+ return Blob_.Begin();
+}
+
+size_t TBlobOutput::Size() const
+{
+ return Blob_.Size();
+}
+
+size_t TBlobOutput::size() const
+{
+ return Blob_.Size();
+}
+
+size_t TBlobOutput::Capacity() const
+{
+ return Blob_.Capacity();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/blob_output.h b/yt/yt/core/misc/blob_output.h
new file mode 100644
index 0000000000..b9c7666c58
--- /dev/null
+++ b/yt/yt/core/misc/blob_output.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "blob.h"
+
+#include <util/stream/zerocopy_output.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBlobOutputTag
+{ };
+
+class TBlobOutput
+ : public IZeroCopyOutput
+{
+public:
+ explicit TBlobOutput(
+ size_t capacity = 0,
+ bool pageAligned = false,
+ TRefCountedTypeCookie tagCookie = GetRefCountedTypeCookie<TBlobOutputTag>());
+
+ TBlob& Blob();
+ const TBlob& Blob() const;
+
+ const char* Begin() const;
+ size_t Size() const;
+ size_t size() const;
+ size_t Capacity() const;
+
+ void Reserve(size_t capacity);
+ void Clear();
+ TSharedRef Flush();
+
+ friend void swap(TBlobOutput& left, TBlobOutput& right);
+
+private:
+ size_t DoNext(void** ptr) override;
+ void DoUndo(size_t len) override;
+ void DoWrite(const void* buf, size_t len) override;
+
+ TBlob Blob_;
+};
+
+void swap(TBlobOutput& left, TBlobOutput& right);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/bloom_filter.cpp b/yt/yt/core/misc/bloom_filter.cpp
new file mode 100644
index 0000000000..500d88e7b3
--- /dev/null
+++ b/yt/yt/core/misc/bloom_filter.cpp
@@ -0,0 +1,222 @@
+#include "bloom_filter.h"
+#include "blob.h"
+#include "error.h"
+
+#include <yt/yt_proto/yt/core/misc/proto/bloom_filter.pb.h>
+
+#include <cmath>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBloomFilterBase::TBloomFilterBase(int hashCount, int size)
+ : HashCount_(hashCount)
+ , LogSize_(Log2(size))
+{ }
+
+int TBloomFilterBase::GetVersion()
+{
+ return 1;
+}
+
+i64 TBloomFilterBase::Size() const
+{
+ return 1LL << LogSize_;
+}
+
+void TBloomFilterBase::VerifySize(ui64 size) const
+{
+ if (size != (1ULL << LogSize_)) {
+ THROW_ERROR_EXCEPTION("Incorrect Bloom Filter size: expected a power of two, got %v",
+ size);
+ }
+}
+
+int TBloomFilterBase::BitPosition(int position) const
+{
+ return position & 7;
+}
+
+int TBloomFilterBase::BytePosition(int position) const
+{
+ return (position >> 3) & (Size() - 1);
+}
+
+int TBloomFilterBase::Log2(int size)
+{
+ int result = 0;
+ while (size >> 1 != 0) {
+ size >>= 1;
+ ++result;
+ }
+ return result;
+}
+
+int TBloomFilterBase::HashCountFromRate(double falsePositiveRate)
+{
+ YT_VERIFY(falsePositiveRate > 0 && falsePositiveRate <= 1);
+ int hashCount = std::log2(1.0 / falsePositiveRate);
+ return hashCount > 0 ? hashCount : 1;
+}
+
+double TBloomFilterBase::BitsPerItemFromRate(double falsePositiveRate)
+{
+ YT_VERIFY(falsePositiveRate > 0 && falsePositiveRate <= 1);
+ return 1.44 * std::log2(1.0 / falsePositiveRate);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBloomFilterBuilderTag { };
+
+TBloomFilterBuilder::TBloomFilterBuilder(i64 capacity, double falsePositiveRate)
+ : TBloomFilterBuilder(
+ capacity,
+ HashCountFromRate(falsePositiveRate),
+ BitsPerItemFromRate(falsePositiveRate))
+{ }
+
+TBloomFilterBuilder::TBloomFilterBuilder(i64 capacity, int hashCount, double bitsPerItem)
+ : TBloomFilterBuilder(
+ TSharedMutableRef::Allocate<TBloomFilterBuilderTag>(1ULL << Log2(capacity * bitsPerItem / 8)),
+ hashCount,
+ bitsPerItem)
+{ }
+
+TBloomFilterBuilder::TBloomFilterBuilder(TSharedMutableRef data, int hashCount, double bitsPerItem)
+ : TBloomFilterBase(hashCount, data.Size())
+ , BitsPerItem_(bitsPerItem)
+ , Data_(std::move(data))
+{
+ VerifySize(Data_.Size());
+}
+
+TSharedRef TBloomFilterBuilder::Bitmap() const
+{
+ return Data_.Slice(0, Size());
+}
+
+void TBloomFilterBuilder::Insert(TFingerprint fingerprint)
+{
+ YT_VERIFY(BitsPerItem_ > 0 && Size() > 0);
+
+ int hash1 = static_cast<int>(fingerprint >> 32);
+ int hash2 = static_cast<int>(fingerprint);
+
+ for (int index = 0; index < HashCount_; ++index) {
+ SetBit(hash1 + index * hash2);
+ }
+
+ ++InsertionCount_;
+}
+
+void TBloomFilterBuilder::SetBit(int position)
+{
+ Data_.Begin()[BytePosition(position)] |= (1 << BitPosition(position));
+}
+
+bool TBloomFilterBuilder::IsValid() const
+{
+ return !Data_.Empty() && InsertionCount_ * HashCount_ <= Size() * 8;
+}
+
+int TBloomFilterBuilder::EstimateLogSize() const
+{
+ i64 size = Size();
+ int sizeLog = LogSize_;
+ while ((size << 2) >= InsertionCount_ * BitsPerItem_) {
+ size >>= 1;
+ --sizeLog;
+ }
+ return sizeLog;
+}
+
+i64 TBloomFilterBuilder::EstimateSize() const
+{
+ return 1ULL << EstimateLogSize();
+}
+
+void TBloomFilterBuilder::Shrink()
+{
+ int sizeLog = EstimateLogSize();
+ i64 size = 1LL << sizeLog;
+
+ if (sizeLog < LogSize_) {
+ auto sizeMask = size - 1;
+
+ for (int index = size; index < Size(); ++index) {
+ Data_.Begin()[index & sizeMask] |= Data_.Begin()[index];
+ }
+
+ LogSize_ = sizeLog;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBloomFilter::TBloomFilter(TSharedRef data, int hashCount)
+ : TBloomFilterBase(hashCount, data.Size())
+ , Data_(std::move(data))
+{
+ VerifySize(Data_.Size());
+}
+
+TBloomFilter::TBloomFilter(TBloomFilter&& other)
+{
+ *this = std::move(other);
+}
+
+TBloomFilter& TBloomFilter::operator=(TBloomFilter&& other)
+{
+ HashCount_ = other.HashCount_;
+ LogSize_ = other.LogSize_;
+ Data_ = std::move(other.Data_);
+ return *this;
+}
+
+bool TBloomFilter::Contains(TFingerprint fingerprint) const
+{
+ int hash1 = static_cast<int>(fingerprint >> 32);
+ int hash2 = static_cast<int>(fingerprint);
+
+ for (int index = 0; index < HashCount_; ++index) {
+ if (!IsBitSet(hash1 + index * hash2)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool TBloomFilter::IsBitSet(int position) const
+{
+ return Data_.Begin()[BytePosition(position)] & (1 << BitPosition(position));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TBloomFilter* protoBloomFilter, const TBloomFilterBuilder& bloomFilter)
+{
+ protoBloomFilter->set_version(bloomFilter.GetVersion());
+ protoBloomFilter->set_hash_count(bloomFilter.GetHashCount());
+ protoBloomFilter->set_bitmap(ToString(bloomFilter.Bitmap()));
+}
+
+void FromProto(TBloomFilter* bloomFilter, const NProto::TBloomFilter& protoBloomFilter)
+{
+ if (protoBloomFilter.version() != TBloomFilter::GetVersion()) {
+ THROW_ERROR_EXCEPTION("Bloom filter version mismatch: expected %v, got %v",
+ TBloomFilter::GetVersion(),
+ protoBloomFilter.version());
+ }
+
+ auto data = TSharedMutableRef::Allocate(protoBloomFilter.bitmap().size());
+ ::memcpy(data.Begin(), protoBloomFilter.bitmap().data(), protoBloomFilter.bitmap().size());
+ *bloomFilter = TBloomFilter(std::move(data), protoBloomFilter.hash_count());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/bloom_filter.h b/yt/yt/core/misc/bloom_filter.h
new file mode 100644
index 0000000000..006c560f88
--- /dev/null
+++ b/yt/yt/core/misc/bloom_filter.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include "public.h"
+#include "farm_hash.h"
+#include "property.h"
+#include "protobuf_helpers.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/generic/noncopyable.h>
+
+#include <util/system/defaults.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBloomFilterBase
+ : private TNonCopyable
+{
+public:
+ TBloomFilterBase() = default;
+ TBloomFilterBase(int hashCount, int size);
+
+ static int GetVersion();
+
+ i64 Size() const;
+
+ DEFINE_BYVAL_RO_PROPERTY(int, HashCount);
+
+protected:
+ int LogSize_;
+
+ void VerifySize(ui64 size) const;
+
+ int BitPosition(int position) const;
+ int BytePosition(int position) const;
+
+ static int Log2(int size);
+ static int HashCountFromRate(double falsePositiveRate);
+ static double BitsPerItemFromRate(double falsePositiveRate);
+};
+
+class TBloomFilterBuilder
+ : public TBloomFilterBase
+{
+public:
+ TBloomFilterBuilder(i64 capacity, double falsePositiveRate);
+ TBloomFilterBuilder(i64 capacity, int hashCount, double bitsPerItem);
+ TBloomFilterBuilder(TSharedMutableRef data, int hashCount, double bitsPerItem);
+
+ void Insert(TFingerprint fingerprint);
+
+ i64 EstimateSize() const;
+
+ bool IsValid() const;
+ void Shrink();
+
+ DEFINE_BYVAL_RO_PROPERTY(double, BitsPerItem);
+
+protected:
+ int InsertionCount_ = 0;
+ TSharedMutableRef Data_;
+
+ void SetBit(int position);
+ int EstimateLogSize() const;
+ TSharedRef Bitmap() const;
+
+ friend void ToProto(NProto::TBloomFilter* protoBloomFilter, const TBloomFilterBuilder& bloomFilter);
+};
+
+class TBloomFilter
+ : public TBloomFilterBase
+{
+public:
+ TBloomFilter() = default;
+ TBloomFilter(TBloomFilter&& other);
+ TBloomFilter(TSharedRef data, int hashCount);
+
+ TBloomFilter& operator=(TBloomFilter&& other);
+
+ bool Contains(TFingerprint fingerprint) const;
+
+protected:
+ TSharedRef Data_;
+
+ bool IsBitSet(int position) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TBloomFilter* protoBloomFilter, const TBloomFilterBuilder& bloomFilter);
+void FromProto(TBloomFilter* bloomFilter, const NProto::TBloomFilter& protoBloomFilter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/cache_config.cpp b/yt/yt/core/misc/cache_config.cpp
new file mode 100644
index 0000000000..9e6e57fff9
--- /dev/null
+++ b/yt/yt/core/misc/cache_config.cpp
@@ -0,0 +1,125 @@
+#include "cache_config.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSlruCacheConfigPtr TSlruCacheConfig::CreateWithCapacity(i64 capacity)
+{
+ auto result = New<TSlruCacheConfig>();
+ result->Capacity = capacity;
+ return result;
+}
+
+void TSlruCacheConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("capacity", &TThis::Capacity)
+ .Default(0)
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("younger_size_fraction", &TThis::YoungerSizeFraction)
+ .Default(0.25)
+ .InRange(0.0, 1.0);
+ registrar.Parameter("shard_count", &TThis::ShardCount)
+ .Default(16)
+ .GreaterThan(0);
+ registrar.Parameter("touch_buffer_capacity", &TThis::TouchBufferCapacity)
+ .Default(65536)
+ .GreaterThan(0);
+ registrar.Parameter("small_ghost_cache_ratio", &TThis::SmallGhostCacheRatio)
+ .Default(0.5)
+ .GreaterThanOrEqual(0.0);
+ registrar.Parameter("large_ghost_cache_ratio", &TThis::LargeGhostCacheRatio)
+ .Default(2.0)
+ .GreaterThanOrEqual(0.0);
+ registrar.Parameter("enable_ghost_caches", &TThis::EnableGhostCaches)
+ .Default(true);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (!IsPowerOf2(config->ShardCount)) {
+ THROW_ERROR_EXCEPTION("\"shard_count\" must be power of two, actual: %v",
+ config->ShardCount);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSlruCacheDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("capacity", &TThis::Capacity)
+ .Optional()
+ .GreaterThanOrEqual(0);
+ registrar.Parameter("younger_size_fraction", &TThis::YoungerSizeFraction)
+ .Optional()
+ .InRange(0.0, 1.0);
+ registrar.Parameter("enable_ghost_caches", &TThis::EnableGhostCaches)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAsyncExpiringCacheConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("expire_after_access_time", &TThis::ExpireAfterAccessTime)
+ .Default(TDuration::Seconds(300));
+ registrar.Parameter("expire_after_successful_update_time", &TThis::ExpireAfterSuccessfulUpdateTime)
+ .Alias("success_expiration_time")
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("expire_after_failed_update_time", &TThis::ExpireAfterFailedUpdateTime)
+ .Alias("failure_expiration_time")
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("refresh_time", &TThis::RefreshTime)
+ .Alias("success_probation_time")
+ .Default(TDuration::Seconds(10));
+ registrar.Parameter("batch_update", &TThis::BatchUpdate)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->RefreshTime && *config->RefreshTime && *config->RefreshTime > config->ExpireAfterSuccessfulUpdateTime) {
+ THROW_ERROR_EXCEPTION("\"refresh_time\" must be less than \"expire_after_successful_update_time\"")
+ << TErrorAttribute("refresh_time", config->RefreshTime)
+ << TErrorAttribute("expire_after_successful_update_time", config->ExpireAfterSuccessfulUpdateTime);
+ }
+ });
+}
+
+void TAsyncExpiringCacheConfig::ApplyDynamicInplace(
+ const TAsyncExpiringCacheDynamicConfigPtr& dynamicConfig)
+{
+ ExpireAfterAccessTime = dynamicConfig->ExpireAfterAccessTime.value_or(ExpireAfterAccessTime);
+ ExpireAfterSuccessfulUpdateTime = dynamicConfig->ExpireAfterSuccessfulUpdateTime.value_or(ExpireAfterSuccessfulUpdateTime);
+ ExpireAfterFailedUpdateTime = dynamicConfig->ExpireAfterFailedUpdateTime.value_or(ExpireAfterFailedUpdateTime);
+ RefreshTime = dynamicConfig->RefreshTime.has_value()
+ ? dynamicConfig->RefreshTime
+ : RefreshTime;
+ BatchUpdate = dynamicConfig->BatchUpdate.value_or(BatchUpdate);
+}
+
+TAsyncExpiringCacheConfigPtr TAsyncExpiringCacheConfig::ApplyDynamic(
+ const TAsyncExpiringCacheDynamicConfigPtr& dynamicConfig) const
+{
+ auto mergedConfig = CloneYsonStruct(MakeStrong(this));
+ mergedConfig->ApplyDynamicInplace(dynamicConfig);
+ mergedConfig->Postprocess();
+ return mergedConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAsyncExpiringCacheDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("expire_after_access_time", &TThis::ExpireAfterAccessTime)
+ .Optional();
+ registrar.Parameter("expire_after_successful_update_time", &TThis::ExpireAfterSuccessfulUpdateTime)
+ .Optional();
+ registrar.Parameter("expire_after_failed_update_time", &TThis::ExpireAfterFailedUpdateTime)
+ .Optional();
+ registrar.Parameter("refresh_time", &TThis::RefreshTime)
+ .Optional();
+ registrar.Parameter("batch_update", &TThis::BatchUpdate)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/cache_config.h b/yt/yt/core/misc/cache_config.h
new file mode 100644
index 0000000000..9ac7c9063c
--- /dev/null
+++ b/yt/yt/core/misc/cache_config.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSlruCacheConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! The maximum number of weight units cached items are allowed to occupy.
+ //! Zero means that no items are cached.
+ i64 Capacity;
+
+ //! The fraction of total capacity given to the younger segment.
+ double YoungerSizeFraction;
+
+ //! Number of shards.
+ int ShardCount;
+
+ //! Capacity of internal buffer used to amortize and de-contend touch operations.
+ int TouchBufferCapacity;
+
+ //! Multiplier for ghost cache capacities. Ghost caches do not really store elements
+ //! actually, they just export the counters used to tweak the cache sizes.
+ double SmallGhostCacheRatio;
+ double LargeGhostCacheRatio;
+
+ //! Set to true if ghost caches are enabled. Once disabled, ghost caches cannot be
+ //! re-enabled again (i.e. the value of this field is ignored).
+ bool EnableGhostCaches;
+
+ static TSlruCacheConfigPtr CreateWithCapacity(i64 capacity);
+
+ REGISTER_YSON_STRUCT(TSlruCacheConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSlruCacheConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSlruCacheDynamicConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! The maximum number of weight units cached items are allowed to occupy.
+ //! Zero means that no items are cached.
+ std::optional<i64> Capacity;
+
+ //! The fraction of total capacity given to the younger segment.
+ std::optional<double> YoungerSizeFraction;
+
+ //! Set to true if ghost caches are enabled. Once disabled, ghost caches cannot be
+ //! re-enabled again (i.e. the value of this field is ignored).
+ bool EnableGhostCaches;
+
+ REGISTER_YSON_STRUCT(TSlruCacheDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSlruCacheDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Cache which removes entries after a while.
+/*!
+ * TAsyncExpiringCache acts like a proxy between a client and a remote service:
+ * requests are sent to the service and responses are saved in the cache as entries.
+ * Next time the client makes a request, the response can be taken from the cache
+ * unless it is expired.
+ *
+ * An entry is considered expired if at least one of the following conditions is true:
+ * 1) last access was more than ExpireAfterAccessTime ago,
+ * 2) last update was more than ExpireAfter*UpdateTime ago.
+ *
+ * To avoid client awaiting time on subsequent requests and keep the response
+ * up to date, the cache updates entries in the background:
+ * If request was successful, the cache performs the same request after RefreshTime
+ * and updates the entry.
+ * If request was unsuccessful, the entry (which contains error response) will be expired
+ * after ExpireAfterFailedUpdateTime.
+ */
+class TAsyncExpiringCacheConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Time since last finished Get() after which an entry is removed.
+ TDuration ExpireAfterAccessTime;
+
+ //! Time since last update, if succeeded, after which an entry is removed.
+ TDuration ExpireAfterSuccessfulUpdateTime;
+
+ //! Time since last update, if it failed, after which an entry is removed.
+ TDuration ExpireAfterFailedUpdateTime;
+
+ //! Time before next (background) update.
+ std::optional<TDuration> RefreshTime;
+
+ //! If set to true, cache will invoke DoGetMany once instead of DoGet on every entry during an update.
+ bool BatchUpdate;
+
+ TAsyncExpiringCacheConfigPtr ApplyDynamic(const TAsyncExpiringCacheDynamicConfigPtr& dynamicConfig) const;
+
+ REGISTER_YSON_STRUCT(TAsyncExpiringCacheConfig);
+
+ static void Register(TRegistrar registrar);
+
+protected:
+ void ApplyDynamicInplace(const TAsyncExpiringCacheDynamicConfigPtr& dynamicConfig);
+};
+
+DEFINE_REFCOUNTED_TYPE(TAsyncExpiringCacheConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncExpiringCacheDynamicConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ std::optional<TDuration> ExpireAfterAccessTime;
+ std::optional<TDuration> ExpireAfterSuccessfulUpdateTime;
+ std::optional<TDuration> ExpireAfterFailedUpdateTime;
+ std::optional<TDuration> RefreshTime;
+
+ std::optional<bool> BatchUpdate;
+
+ REGISTER_YSON_STRUCT(TAsyncExpiringCacheDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TAsyncExpiringCacheDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/checksum.cpp b/yt/yt/core/misc/checksum.cpp
new file mode 100644
index 0000000000..2b3aba5347
--- /dev/null
+++ b/yt/yt/core/misc/checksum.cpp
@@ -0,0 +1,796 @@
+#include "checksum.h"
+#include "checksum_helpers.h"
+
+#include <yt/yt/core/misc/isa_crc64/checksum.h>
+
+#ifdef YT_USE_SSE42
+ #include <util/system/cpu_id.h>
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef YT_USE_SSE42
+
+namespace NCrcNative0xE543279765927881 {
+
+using namespace NCrc;
+
+// http://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf
+
+constexpr ui64 Poly = ULL(0xE543279765927881);
+constexpr ui64 Mu = ULL(0x9d9034581c0766b0); // DivXPow_64(128, poly)
+
+constexpr ui64 XPow64ModPoly = ULL(0xe543279765927881); // ModXPow_64(64, poly)
+constexpr ui64 XPow128ModPoly = ULL(0xf9c49baae9f0beb0); // ModXPow_64(128, poly)
+constexpr ui64 XPow192ModPoly = ULL(0xd0665f166605dcc4); // ModXPow_64(128 + 64, poly)
+constexpr ui64 XPow512ModPoly = ULL(0x209670022e7af509); // ModXPow_64(512, poly)
+constexpr ui64 XPow576ModPoly = ULL(0xd85c929ddd7a7d69); // ModXPow_64(512 + 64, poly)
+
+__m128i FoldTo128(const __m128i* buf128, size_t buflen, __m128i result)
+{
+ const __m128i FoldBy128_128 = _mm_set_epi64x(XPow192ModPoly, XPow128ModPoly);
+ const __m128i FoldBy512_128 = _mm_set_epi64x(XPow576ModPoly, XPow512ModPoly);
+
+ if (buflen >= 3) {
+ __m128i result0, result1, result2, result3;
+
+ result0 = result;
+ result1 = AlignedLoad(buf128++);
+ result2 = AlignedLoad(buf128++);
+ result3 = AlignedLoad(buf128++);
+ buflen -= 3;
+
+ size_t count = buflen / 4;
+ buflen = buflen % 4;
+
+ while (count--) {
+ result0 = Fold(result0, AlignedLoad(buf128 + 0), FoldBy512_128);
+ result1 = Fold(result1, AlignedLoad(buf128 + 1), FoldBy512_128);
+ result2 = Fold(result2, AlignedLoad(buf128 + 2), FoldBy512_128);
+ result3 = Fold(result3, AlignedLoad(buf128 + 3), FoldBy512_128);
+ buf128 += 4;
+ }
+
+ result1 = Fold(result0, result1, FoldBy128_128);
+ result2 = Fold(result1, result2, FoldBy128_128);
+ result = Fold(result2, result3, FoldBy128_128);
+ }
+
+ while (buflen--) {
+ result = Fold(result, AlignedLoad(buf128++), FoldBy128_128);
+ }
+
+ return result;
+}
+
+ui64 Crc(const void* buf, size_t buflen, ui64 seed) Y_NO_SANITIZE("memory")
+{
+ const ui8* ptr = reinterpret_cast<const ui8*>(buf);
+
+ const __m128i FoldBy64_128 = _mm_set_epi64x(XPow128ModPoly, XPow64ModPoly);
+ const __m128i FoldBy128_128 = _mm_set_epi64x(XPow192ModPoly, XPow128ModPoly);
+
+ __m128i result = _mm_set_epi64x(seed, 0);
+
+ if (buflen >= 16) {
+ result = _mm_xor_si128(result, UnalignedLoad(ptr));
+ buflen -= 16;
+ ptr += 16;
+
+ if (buflen >= 16) {
+ if (size_t offset = 16 - (reinterpret_cast<size_t>(ptr) % 16)) {
+ result = FoldTail(result, UnalignedLoad(ptr, offset), FoldBy128_128, offset);
+ buflen -= offset;
+ ptr += offset;
+ }
+
+ size_t rest = buflen % 16;
+ result = FoldTo128(reinterpret_cast<const __m128i*>(ptr), buflen / 16, result);
+ ptr += buflen - rest;
+ buflen = rest;
+ }
+
+ if (buflen) {
+ result = FoldTail(result, UnalignedLoad(ptr, buflen), FoldBy128_128, buflen);
+ buflen -= buflen;
+ ptr += buflen;
+ }
+
+ result = Fold(result, FoldBy64_128);
+ } else if (buflen) {
+ __m128i tail = _mm_shift_right_si128(_mm_shift_left_si128(result, buflen), 8);
+ result = _mm_shift_right_si128(_mm_xor_si128(result, UnalignedLoad(ptr, buflen)), 16 - buflen);
+ result = Fold(result, tail, FoldBy64_128);
+ } else {
+ result = _mm_shift_right_si128(result, 8);
+ }
+
+ return BarretReduction(result, Poly, Mu);
+}
+
+} // namespace NCrcNative0xE543279765927881
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NCrcTable0xE543279765927881 {
+
+ui64 CrcLookup[8][256] = {
+ {
+ ULL(0x0000000000000000), ULL(0x81789265972743e5), ULL(0x8389b6aeb968c52f), ULL(0x02f124cb2e4f86ca),
+ ULL(0x06136d5d73d18a5f), ULL(0x876bff38e4f6c9ba), ULL(0x859adbf3cab94f70), ULL(0x04e249965d9e0c95),
+ ULL(0x0c26dabae6a215bf), ULL(0x8d5e48df7185565a), ULL(0x8faf6c145fcad090), ULL(0x0ed7fe71c8ed9375),
+ ULL(0x0a35b7e795739fe0), ULL(0x8b4d25820254dc05), ULL(0x89bc01492c1b5acf), ULL(0x08c4932cbb3c192a),
+ ULL(0x993426105a62689b), ULL(0x184cb475cd452b7e), ULL(0x1abd90bee30aadb4), ULL(0x9bc502db742dee51),
+ ULL(0x9f274b4d29b3e2c4), ULL(0x1e5fd928be94a121), ULL(0x1caefde390db27eb), ULL(0x9dd66f8607fc640e),
+ ULL(0x9512fcaabcc07d24), ULL(0x146a6ecf2be73ec1), ULL(0x169b4a0405a8b80b), ULL(0x97e3d861928ffbee),
+ ULL(0x930191f7cf11f77b), ULL(0x127903925836b49e), ULL(0x1088275976793254), ULL(0x91f0b53ce15e71b1),
+ ULL(0xb311de4523e393d3), ULL(0x32694c20b4c4d036), ULL(0x309868eb9a8b56fc), ULL(0xb1e0fa8e0dac1519),
+ ULL(0xb502b3185032198c), ULL(0x347a217dc7155a69), ULL(0x368b05b6e95adca3), ULL(0xb7f397d37e7d9f46),
+ ULL(0xbf3704ffc541866c), ULL(0x3e4f969a5266c589), ULL(0x3cbeb2517c294343), ULL(0xbdc62034eb0e00a6),
+ ULL(0xb92469a2b6900c33), ULL(0x385cfbc721b74fd6), ULL(0x3aaddf0c0ff8c91c), ULL(0xbbd54d6998df8af9),
+ ULL(0x2a25f8557981fb48), ULL(0xab5d6a30eea6b8ad), ULL(0xa9ac4efbc0e93e67), ULL(0x28d4dc9e57ce7d82),
+ ULL(0x2c3695080a507117), ULL(0xad4e076d9d7732f2), ULL(0xafbf23a6b338b438), ULL(0x2ec7b1c3241ff7dd),
+ ULL(0x260322ef9f23eef7), ULL(0xa77bb08a0804ad12), ULL(0xa58a9441264b2bd8), ULL(0x24f20624b16c683d),
+ ULL(0x20104fb2ecf264a8), ULL(0xa168ddd77bd5274d), ULL(0xa399f91c559aa187), ULL(0x22e16b79c2bde262),
+ ULL(0xe75b2eeed1e16442), ULL(0x6623bc8b46c627a7), ULL(0x64d298406889a16d), ULL(0xe5aa0a25ffaee288),
+ ULL(0xe14843b3a230ee1d), ULL(0x6030d1d63517adf8), ULL(0x62c1f51d1b582b32), ULL(0xe3b967788c7f68d7),
+ ULL(0xeb7df454374371fd), ULL(0x6a056631a0643218), ULL(0x68f442fa8e2bb4d2), ULL(0xe98cd09f190cf737),
+ ULL(0xed6e99094492fba2), ULL(0x6c160b6cd3b5b847), ULL(0x6ee72fa7fdfa3e8d), ULL(0xef9fbdc26add7d68),
+ ULL(0x7e6f08fe8b830cd9), ULL(0xff179a9b1ca44f3c), ULL(0xfde6be5032ebc9f6), ULL(0x7c9e2c35a5cc8a13),
+ ULL(0x787c65a3f8528686), ULL(0xf904f7c66f75c563), ULL(0xfbf5d30d413a43a9), ULL(0x7a8d4168d61d004c),
+ ULL(0x7249d2446d211966), ULL(0xf3314021fa065a83), ULL(0xf1c064ead449dc49), ULL(0x70b8f68f436e9fac),
+ ULL(0x745abf191ef09339), ULL(0xf5222d7c89d7d0dc), ULL(0xf7d309b7a7985616), ULL(0x76ab9bd230bf15f3),
+ ULL(0x544af0abf202f791), ULL(0xd53262ce6525b474), ULL(0xd7c346054b6a32be), ULL(0x56bbd460dc4d715b),
+ ULL(0x52599df681d37dce), ULL(0xd3210f9316f43e2b), ULL(0xd1d02b5838bbb8e1), ULL(0x50a8b93daf9cfb04),
+ ULL(0x586c2a1114a0e22e), ULL(0xd914b8748387a1cb), ULL(0xdbe59cbfadc82701), ULL(0x5a9d0eda3aef64e4),
+ ULL(0x5e7f474c67716871), ULL(0xdf07d529f0562b94), ULL(0xddf6f1e2de19ad5e), ULL(0x5c8e6387493eeebb),
+ ULL(0xcd7ed6bba8609f0a), ULL(0x4c0644de3f47dcef), ULL(0x4ef7601511085a25), ULL(0xcf8ff270862f19c0),
+ ULL(0xcb6dbbe6dbb11555), ULL(0x4a1529834c9656b0), ULL(0x48e40d4862d9d07a), ULL(0xc99c9f2df5fe939f),
+ ULL(0xc1580c014ec28ab5), ULL(0x40209e64d9e5c950), ULL(0x42d1baaff7aa4f9a), ULL(0xc3a928ca608d0c7f),
+ ULL(0xc74b615c3d1300ea), ULL(0x4633f339aa34430f), ULL(0x44c2d7f2847bc5c5), ULL(0xc5ba4597135c8620),
+ ULL(0xceb75cdca3c3c984), ULL(0x4fcfceb934e48a61), ULL(0x4d3eea721aab0cab), ULL(0xcc4678178d8c4f4e),
+ ULL(0xc8a43181d01243db), ULL(0x49dca3e44735003e), ULL(0x4b2d872f697a86f4), ULL(0xca55154afe5dc511),
+ ULL(0xc29186664561dc3b), ULL(0x43e91403d2469fde), ULL(0x411830c8fc091914), ULL(0xc060a2ad6b2e5af1),
+ ULL(0xc482eb3b36b05664), ULL(0x45fa795ea1971581), ULL(0x470b5d958fd8934b), ULL(0xc673cff018ffd0ae),
+ ULL(0x57837accf9a1a11f), ULL(0xd6fbe8a96e86e2fa), ULL(0xd40acc6240c96430), ULL(0x55725e07d7ee27d5),
+ ULL(0x519017918a702b40), ULL(0xd0e885f41d5768a5), ULL(0xd219a13f3318ee6f), ULL(0x5361335aa43fad8a),
+ ULL(0x5ba5a0761f03b4a0), ULL(0xdadd32138824f745), ULL(0xd82c16d8a66b718f), ULL(0x595484bd314c326a),
+ ULL(0x5db6cd2b6cd23eff), ULL(0xdcce5f4efbf57d1a), ULL(0xde3f7b85d5bafbd0), ULL(0x5f47e9e0429db835),
+ ULL(0x7da6829980205a57), ULL(0xfcde10fc170719b2), ULL(0xfe2f343739489f78), ULL(0x7f57a652ae6fdc9d),
+ ULL(0x7bb5efc4f3f1d008), ULL(0xfacd7da164d693ed), ULL(0xf83c596a4a991527), ULL(0x7944cb0fddbe56c2),
+ ULL(0x7180582366824fe8), ULL(0xf0f8ca46f1a50c0d), ULL(0xf209ee8ddfea8ac7), ULL(0x73717ce848cdc922),
+ ULL(0x7793357e1553c5b7), ULL(0xf6eba71b82748652), ULL(0xf41a83d0ac3b0098), ULL(0x756211b53b1c437d),
+ ULL(0xe492a489da4232cc), ULL(0x65ea36ec4d657129), ULL(0x671b1227632af7e3), ULL(0xe6638042f40db406),
+ ULL(0xe281c9d4a993b893), ULL(0x63f95bb13eb4fb76), ULL(0x61087f7a10fb7dbc), ULL(0xe070ed1f87dc3e59),
+ ULL(0xe8b47e333ce02773), ULL(0x69ccec56abc76496), ULL(0x6b3dc89d8588e25c), ULL(0xea455af812afa1b9),
+ ULL(0xeea7136e4f31ad2c), ULL(0x6fdf810bd816eec9), ULL(0x6d2ea5c0f6596803), ULL(0xec5637a5617e2be6),
+ ULL(0x29ec72327222adc6), ULL(0xa894e057e505ee23), ULL(0xaa65c49ccb4a68e9), ULL(0x2b1d56f95c6d2b0c),
+ ULL(0x2fff1f6f01f32799), ULL(0xae878d0a96d4647c), ULL(0xac76a9c1b89be2b6), ULL(0x2d0e3ba42fbca153),
+ ULL(0x25caa8889480b879), ULL(0xa4b23aed03a7fb9c), ULL(0xa6431e262de87d56), ULL(0x273b8c43bacf3eb3),
+ ULL(0x23d9c5d5e7513226), ULL(0xa2a157b0707671c3), ULL(0xa050737b5e39f709), ULL(0x2128e11ec91eb4ec),
+ ULL(0xb0d854222840c55d), ULL(0x31a0c647bf6786b8), ULL(0x3351e28c91280072), ULL(0xb22970e9060f4397),
+ ULL(0xb6cb397f5b914f02), ULL(0x37b3ab1accb60ce7), ULL(0x35428fd1e2f98a2d), ULL(0xb43a1db475dec9c8),
+ ULL(0xbcfe8e98cee2d0e2), ULL(0x3d861cfd59c59307), ULL(0x3f773836778a15cd), ULL(0xbe0faa53e0ad5628),
+ ULL(0xbaede3c5bd335abd), ULL(0x3b9571a02a141958), ULL(0x3964556b045b9f92), ULL(0xb81cc70e937cdc77),
+ ULL(0x9afdac7751c13e15), ULL(0x1b853e12c6e67df0), ULL(0x19741ad9e8a9fb3a), ULL(0x980c88bc7f8eb8df),
+ ULL(0x9ceec12a2210b44a), ULL(0x1d96534fb537f7af), ULL(0x1f6777849b787165), ULL(0x9e1fe5e10c5f3280),
+ ULL(0x96db76cdb7632baa), ULL(0x17a3e4a82044684f), ULL(0x1552c0630e0bee85), ULL(0x942a5206992cad60),
+ ULL(0x90c81b90c4b2a1f5), ULL(0x11b089f55395e210), ULL(0x1341ad3e7dda64da), ULL(0x92393f5beafd273f),
+ ULL(0x03c98a670ba3568e), ULL(0x82b118029c84156b), ULL(0x80403cc9b2cb93a1), ULL(0x0138aeac25ecd044),
+ ULL(0x05dae73a7872dcd1), ULL(0x84a2755fef559f34), ULL(0x86535194c11a19fe), ULL(0x072bc3f1563d5a1b),
+ ULL(0x0fef50dded014331), ULL(0x8e97c2b87a2600d4), ULL(0x8c66e6735469861e), ULL(0x0d1e7416c34ec5fb),
+ ULL(0x09fc3d809ed0c96e), ULL(0x8884afe509f78a8b), ULL(0x8a758b2e27b80c41), ULL(0x0b0d194bb09f4fa4)
+ }, {
+ ULL(0x0000000000000000), ULL(0x1d172bddd0a0d0ec), ULL(0xbb56c4df3666e23c), ULL(0xa641ef02e6c632d0),
+ ULL(0x76ad88bf6dccc479), ULL(0x6bbaa362bd6c1495), ULL(0xcdfb4c605baa2645), ULL(0xd0ec67bd8b0af6a9),
+ ULL(0xec5a117fdb9889f3), ULL(0xf14d3aa20b38591f), ULL(0x570cd5a0edfe6bcf), ULL(0x4a1bfe7d3d5ebb23),
+ ULL(0x9af799c0b6544d8a), ULL(0x87e0b21d66f49d66), ULL(0x21a15d1f8032afb6), ULL(0x3cb676c250927f5a),
+ ULL(0x59cdb09b21165002), ULL(0x44da9b46f1b680ee), ULL(0xe29b74441770b23e), ULL(0xff8c5f99c7d062d2),
+ ULL(0x2f6038244cda947b), ULL(0x327713f99c7a4497), ULL(0x9436fcfb7abc7647), ULL(0x8921d726aa1ca6ab),
+ ULL(0xb597a1e4fa8ed9f1), ULL(0xa8808a392a2e091d), ULL(0x0ec1653bcce83bcd), ULL(0x13d64ee61c48eb21),
+ ULL(0xc33a295b97421d88), ULL(0xde2d028647e2cd64), ULL(0x786ced84a124ffb4), ULL(0x657bc65971842f58),
+ ULL(0xb29a6137432ca004), ULL(0xaf8d4aea938c70e8), ULL(0x09cca5e8754a4238), ULL(0x14db8e35a5ea92d4),
+ ULL(0xc437e9882ee0647d), ULL(0xd920c255fe40b491), ULL(0x7f612d5718868641), ULL(0x6276068ac82656ad),
+ ULL(0x5ec0704898b429f7), ULL(0x43d75b954814f91b), ULL(0xe596b497aed2cbcb), ULL(0xf8819f4a7e721b27),
+ ULL(0x286df8f7f578ed8e), ULL(0x357ad32a25d83d62), ULL(0x933b3c28c31e0fb2), ULL(0x8e2c17f513bedf5e),
+ ULL(0xeb57d1ac623af006), ULL(0xf640fa71b29a20ea), ULL(0x50011573545c123a), ULL(0x4d163eae84fcc2d6),
+ ULL(0x9dfa59130ff6347f), ULL(0x80ed72cedf56e493), ULL(0x26ac9dcc3990d643), ULL(0x3bbbb611e93006af),
+ ULL(0x070dc0d3b9a279f5), ULL(0x1a1aeb0e6902a919), ULL(0xbc5b040c8fc49bc9), ULL(0xa14c2fd15f644b25),
+ ULL(0x71a0486cd46ebd8c), ULL(0x6cb763b104ce6d60), ULL(0xcaf68cb3e2085fb0), ULL(0xd7e1a76e32a88f5c),
+ ULL(0x6435c36e86584009), ULL(0x7922e8b356f890e5), ULL(0xdf6307b1b03ea235), ULL(0xc2742c6c609e72d9),
+ ULL(0x12984bd1eb948470), ULL(0x0f8f600c3b34549c), ULL(0xa9ce8f0eddf2664c), ULL(0xb4d9a4d30d52b6a0),
+ ULL(0x886fd2115dc0c9fa), ULL(0x9578f9cc8d601916), ULL(0x333916ce6ba62bc6), ULL(0x2e2e3d13bb06fb2a),
+ ULL(0xfec25aae300c0d83), ULL(0xe3d57173e0acdd6f), ULL(0x45949e71066aefbf), ULL(0x5883b5acd6ca3f53),
+ ULL(0x3df873f5a74e100b), ULL(0x20ef582877eec0e7), ULL(0x86aeb72a9128f237), ULL(0x9bb99cf7418822db),
+ ULL(0x4b55fb4aca82d472), ULL(0x5642d0971a22049e), ULL(0xf0033f95fce4364e), ULL(0xed1414482c44e6a2),
+ ULL(0xd1a2628a7cd699f8), ULL(0xccb54957ac764914), ULL(0x6af4a6554ab07bc4), ULL(0x77e38d889a10ab28),
+ ULL(0xa70fea35111a5d81), ULL(0xba18c1e8c1ba8d6d), ULL(0x1c592eea277cbfbd), ULL(0x014e0537f7dc6f51),
+ ULL(0xd6afa259c574e00d), ULL(0xcbb8898415d430e1), ULL(0x6df96686f3120231), ULL(0x70ee4d5b23b2d2dd),
+ ULL(0xa0022ae6a8b82474), ULL(0xbd15013b7818f498), ULL(0x1b54ee399edec648), ULL(0x0643c5e44e7e16a4),
+ ULL(0x3af5b3261eec69fe), ULL(0x27e298fbce4cb912), ULL(0x81a377f9288a8bc2), ULL(0x9cb45c24f82a5b2e),
+ ULL(0x4c583b997320ad87), ULL(0x514f1044a3807d6b), ULL(0xf70eff4645464fbb), ULL(0xea19d49b95e69f57),
+ ULL(0x8f6212c2e462b00f), ULL(0x9275391f34c260e3), ULL(0x3434d61dd2045233), ULL(0x2923fdc002a482df),
+ ULL(0xf9cf9a7d89ae7476), ULL(0xe4d8b1a0590ea49a), ULL(0x42995ea2bfc8964a), ULL(0x5f8e757f6f6846a6),
+ ULL(0x633803bd3ffa39fc), ULL(0x7e2f2860ef5ae910), ULL(0xd86ec762099cdbc0), ULL(0xc579ecbfd93c0b2c),
+ ULL(0x15958b025236fd85), ULL(0x0882a0df82962d69), ULL(0xaec34fdd64501fb9), ULL(0xb3d46400b4f0cf55),
+ ULL(0xc86a86dd0cb18012), ULL(0xd57dad00dc1150fe), ULL(0x733c42023ad7622e), ULL(0x6e2b69dfea77b2c2),
+ ULL(0xbec70e62617d446b), ULL(0xa3d025bfb1dd9487), ULL(0x0591cabd571ba657), ULL(0x1886e16087bb76bb),
+ ULL(0x243097a2d72909e1), ULL(0x3927bc7f0789d90d), ULL(0x9f66537de14febdd), ULL(0x827178a031ef3b31),
+ ULL(0x529d1f1dbae5cd98), ULL(0x4f8a34c06a451d74), ULL(0xe9cbdbc28c832fa4), ULL(0xf4dcf01f5c23ff48),
+ ULL(0x91a736462da7d010), ULL(0x8cb01d9bfd0700fc), ULL(0x2af1f2991bc1322c), ULL(0x37e6d944cb61e2c0),
+ ULL(0xe70abef9406b1469), ULL(0xfa1d952490cbc485), ULL(0x5c5c7a26760df655), ULL(0x414b51fba6ad26b9),
+ ULL(0x7dfd2739f63f59e3), ULL(0x60ea0ce4269f890f), ULL(0xc6abe3e6c059bbdf), ULL(0xdbbcc83b10f96b33),
+ ULL(0x0b50af869bf39d9a), ULL(0x1647845b4b534d76), ULL(0xb0066b59ad957fa6), ULL(0xad1140847d35af4a),
+ ULL(0x7af0e7ea4f9d2016), ULL(0x67e7cc379f3df0fa), ULL(0xc1a6233579fbc22a), ULL(0xdcb108e8a95b12c6),
+ ULL(0x0c5d6f552251e46f), ULL(0x114a4488f2f13483), ULL(0xb70bab8a14370653), ULL(0xaa1c8057c497d6bf),
+ ULL(0x96aaf6959405a9e5), ULL(0x8bbddd4844a57909), ULL(0x2dfc324aa2634bd9), ULL(0x30eb199772c39b35),
+ ULL(0xe0077e2af9c96d9c), ULL(0xfd1055f72969bd70), ULL(0x5b51baf5cfaf8fa0), ULL(0x464691281f0f5f4c),
+ ULL(0x233d57716e8b7014), ULL(0x3e2a7cacbe2ba0f8), ULL(0x986b93ae58ed9228), ULL(0x857cb873884d42c4),
+ ULL(0x5590dfce0347b46d), ULL(0x4887f413d3e76481), ULL(0xeec61b1135215651), ULL(0xf3d130cce58186bd),
+ ULL(0xcf67460eb513f9e7), ULL(0xd2706dd365b3290b), ULL(0x743182d183751bdb), ULL(0x6926a90c53d5cb37),
+ ULL(0xb9caceb1d8df3d9e), ULL(0xa4dde56c087fed72), ULL(0x029c0a6eeeb9dfa2), ULL(0x1f8b21b33e190f4e),
+ ULL(0xac5f45b38ae9c01b), ULL(0xb1486e6e5a4910f7), ULL(0x1709816cbc8f2227), ULL(0x0a1eaab16c2ff2cb),
+ ULL(0xdaf2cd0ce7250462), ULL(0xc7e5e6d13785d48e), ULL(0x61a409d3d143e65e), ULL(0x7cb3220e01e336b2),
+ ULL(0x400554cc517149e8), ULL(0x5d127f1181d19904), ULL(0xfb5390136717abd4), ULL(0xe644bbceb7b77b38),
+ ULL(0x36a8dc733cbd8d91), ULL(0x2bbff7aeec1d5d7d), ULL(0x8dfe18ac0adb6fad), ULL(0x90e93371da7bbf41),
+ ULL(0xf592f528abff9019), ULL(0xe885def57b5f40f5), ULL(0x4ec431f79d997225), ULL(0x53d31a2a4d39a2c9),
+ ULL(0x833f7d97c6335460), ULL(0x9e28564a1693848c), ULL(0x3869b948f055b65c), ULL(0x257e929520f566b0),
+ ULL(0x19c8e457706719ea), ULL(0x04dfcf8aa0c7c906), ULL(0xa29e20884601fbd6), ULL(0xbf890b5596a12b3a),
+ ULL(0x6f656ce81dabdd93), ULL(0x72724735cd0b0d7f), ULL(0xd433a8372bcd3faf), ULL(0xc92483eafb6def43),
+ ULL(0x1ec52484c9c5601f), ULL(0x03d20f591965b0f3), ULL(0xa593e05bffa38223), ULL(0xb884cb862f0352cf),
+ ULL(0x6868ac3ba409a466), ULL(0x757f87e674a9748a), ULL(0xd33e68e4926f465a), ULL(0xce29433942cf96b6),
+ ULL(0xf29f35fb125de9ec), ULL(0xef881e26c2fd3900), ULL(0x49c9f124243b0bd0), ULL(0x54dedaf9f49bdb3c),
+ ULL(0x8432bd447f912d95), ULL(0x99259699af31fd79), ULL(0x3f64799b49f7cfa9), ULL(0x2273524699571f45),
+ ULL(0x4708941fe8d3301d), ULL(0x5a1fbfc23873e0f1), ULL(0xfc5e50c0deb5d221), ULL(0xe1497b1d0e1502cd),
+ ULL(0x31a51ca0851ff464), ULL(0x2cb2377d55bf2488), ULL(0x8af3d87fb3791658), ULL(0x97e4f3a263d9c6b4),
+ ULL(0xab528560334bb9ee), ULL(0xb645aebde3eb6902), ULL(0x100441bf052d5bd2), ULL(0x0d136a62d58d8b3e),
+ ULL(0xddff0ddf5e877d97), ULL(0xc0e826028e27ad7b), ULL(0x66a9c90068e19fab), ULL(0x7bbee2ddb8414f47)
+ }, {
+ ULL(0x0000000000000000), ULL(0x90d50cbb19620125), ULL(0x20ab197633c4024a), ULL(0xb07e15cd2aa6036f),
+ ULL(0x405633ec66880594), ULL(0xd0833f577fea04b1), ULL(0x60fd2a9a554c07de), ULL(0xf02826214c2e06fb),
+ ULL(0x01d4f4bd5a3748cd), ULL(0x9101f806435549e8), ULL(0x217fedcb69f34a87), ULL(0xb1aae17070914ba2),
+ ULL(0x4182c7513cbf4d59), ULL(0xd157cbea25dd4c7c), ULL(0x6129de270f7b4f13), ULL(0xf1fcd29c16194e36),
+ ULL(0x83d07b1e2249d37f), ULL(0x130577a53b2bd25a), ULL(0xa37b6268118dd135), ULL(0x33ae6ed308efd010),
+ ULL(0xc38648f244c1d6eb), ULL(0x535344495da3d7ce), ULL(0xe32d51847705d4a1), ULL(0x73f85d3f6e67d584),
+ ULL(0x82048fa3787e9bb2), ULL(0x12d18318611c9a97), ULL(0xa2af96d54bba99f8), ULL(0x327a9a6e52d898dd),
+ ULL(0xc252bc4f1ef69e26), ULL(0x5287b0f407949f03), ULL(0xe2f9a5392d329c6c), ULL(0x722ca98234509d49),
+ ULL(0x06a1f73c4492a6ff), ULL(0x9674fb875df0a7da), ULL(0x260aee4a7756a4b5), ULL(0xb6dfe2f16e34a590),
+ ULL(0x46f7c4d0221aa36b), ULL(0xd622c86b3b78a24e), ULL(0x665cdda611dea121), ULL(0xf689d11d08bca004),
+ ULL(0x077503811ea5ee32), ULL(0x97a00f3a07c7ef17), ULL(0x27de1af72d61ec78), ULL(0xb70b164c3403ed5d),
+ ULL(0x4723306d782deba6), ULL(0xd7f63cd6614fea83), ULL(0x6788291b4be9e9ec), ULL(0xf75d25a0528be8c9),
+ ULL(0x85718c2266db7580), ULL(0x15a480997fb974a5), ULL(0xa5da9554551f77ca), ULL(0x350f99ef4c7d76ef),
+ ULL(0xc527bfce00537014), ULL(0x55f2b37519317131), ULL(0xe58ca6b83397725e), ULL(0x7559aa032af5737b),
+ ULL(0x84a5789f3cec3d4d), ULL(0x14707424258e3c68), ULL(0xa40e61e90f283f07), ULL(0x34db6d52164a3e22),
+ ULL(0xc4f34b735a6438d9), ULL(0x542647c8430639fc), ULL(0xe458520569a03a93), ULL(0x748d5ebe70c23bb6),
+ ULL(0x8d3a7d1c1f030e1a), ULL(0x1def71a706610f3f), ULL(0xad91646a2cc70c50), ULL(0x3d4468d135a50d75),
+ ULL(0xcd6c4ef0798b0b8e), ULL(0x5db9424b60e90aab), ULL(0xedc757864a4f09c4), ULL(0x7d125b3d532d08e1),
+ ULL(0x8cee89a1453446d7), ULL(0x1c3b851a5c5647f2), ULL(0xac4590d776f0449d), ULL(0x3c909c6c6f9245b8),
+ ULL(0xccb8ba4d23bc4343), ULL(0x5c6db6f63ade4266), ULL(0xec13a33b10784109), ULL(0x7cc6af80091a402c),
+ ULL(0x0eea06023d4add65), ULL(0x9e3f0ab92428dc40), ULL(0x2e411f740e8edf2f), ULL(0xbe9413cf17ecde0a),
+ ULL(0x4ebc35ee5bc2d8f1), ULL(0xde69395542a0d9d4), ULL(0x6e172c986806dabb), ULL(0xfec220237164db9e),
+ ULL(0x0f3ef2bf677d95a8), ULL(0x9febfe047e1f948d), ULL(0x2f95ebc954b997e2), ULL(0xbf40e7724ddb96c7),
+ ULL(0x4f68c15301f5903c), ULL(0xdfbdcde818979119), ULL(0x6fc3d82532319276), ULL(0xff16d49e2b539353),
+ ULL(0x8b9b8a205b91a8e5), ULL(0x1b4e869b42f3a9c0), ULL(0xab3093566855aaaf), ULL(0x3be59fed7137ab8a),
+ ULL(0xcbcdb9cc3d19ad71), ULL(0x5b18b577247bac54), ULL(0xeb66a0ba0eddaf3b), ULL(0x7bb3ac0117bfae1e),
+ ULL(0x8a4f7e9d01a6e028), ULL(0x1a9a722618c4e10d), ULL(0xaae467eb3262e262), ULL(0x3a316b502b00e347),
+ ULL(0xca194d71672ee5bc), ULL(0x5acc41ca7e4ce499), ULL(0xeab2540754eae7f6), ULL(0x7a6758bc4d88e6d3),
+ ULL(0x084bf13e79d87b9a), ULL(0x989efd8560ba7abf), ULL(0x28e0e8484a1c79d0), ULL(0xb835e4f3537e78f5),
+ ULL(0x481dc2d21f507e0e), ULL(0xd8c8ce6906327f2b), ULL(0x68b6dba42c947c44), ULL(0xf863d71f35f67d61),
+ ULL(0x099f058323ef3357), ULL(0x994a09383a8d3272), ULL(0x29341cf5102b311d), ULL(0xb9e1104e09493038),
+ ULL(0x49c9366f456736c3), ULL(0xd91c3ad45c0537e6), ULL(0x69622f1976a33489), ULL(0xf9b723a26fc135ac),
+ ULL(0x1a75fa383e061c34), ULL(0x8aa0f68327641d11), ULL(0x3adee34e0dc21e7e), ULL(0xaa0beff514a01f5b),
+ ULL(0x5a23c9d4588e19a0), ULL(0xcaf6c56f41ec1885), ULL(0x7a88d0a26b4a1bea), ULL(0xea5ddc1972281acf),
+ ULL(0x1ba10e85643154f9), ULL(0x8b74023e7d5355dc), ULL(0x3b0a17f357f556b3), ULL(0xabdf1b484e975796),
+ ULL(0x5bf73d6902b9516d), ULL(0xcb2231d21bdb5048), ULL(0x7b5c241f317d5327), ULL(0xeb8928a4281f5202),
+ ULL(0x99a581261c4fcf4b), ULL(0x09708d9d052dce6e), ULL(0xb90e98502f8bcd01), ULL(0x29db94eb36e9cc24),
+ ULL(0xd9f3b2ca7ac7cadf), ULL(0x4926be7163a5cbfa), ULL(0xf958abbc4903c895), ULL(0x698da7075061c9b0),
+ ULL(0x9871759b46788786), ULL(0x08a479205f1a86a3), ULL(0xb8da6ced75bc85cc), ULL(0x280f60566cde84e9),
+ ULL(0xd827467720f08212), ULL(0x48f24acc39928337), ULL(0xf88c5f0113348058), ULL(0x685953ba0a56817d),
+ ULL(0x1cd40d047a94bacb), ULL(0x8c0101bf63f6bbee), ULL(0x3c7f14724950b881), ULL(0xacaa18c95032b9a4),
+ ULL(0x5c823ee81c1cbf5f), ULL(0xcc573253057ebe7a), ULL(0x7c29279e2fd8bd15), ULL(0xecfc2b2536babc30),
+ ULL(0x1d00f9b920a3f206), ULL(0x8dd5f50239c1f323), ULL(0x3dabe0cf1367f04c), ULL(0xad7eec740a05f169),
+ ULL(0x5d56ca55462bf792), ULL(0xcd83c6ee5f49f6b7), ULL(0x7dfdd32375eff5d8), ULL(0xed28df986c8df4fd),
+ ULL(0x9f04761a58dd69b4), ULL(0x0fd17aa141bf6891), ULL(0xbfaf6f6c6b196bfe), ULL(0x2f7a63d7727b6adb),
+ ULL(0xdf5245f63e556c20), ULL(0x4f87494d27376d05), ULL(0xfff95c800d916e6a), ULL(0x6f2c503b14f36f4f),
+ ULL(0x9ed082a702ea2179), ULL(0x0e058e1c1b88205c), ULL(0xbe7b9bd1312e2333), ULL(0x2eae976a284c2216),
+ ULL(0xde86b14b646224ed), ULL(0x4e53bdf07d0025c8), ULL(0xfe2da83d57a626a7), ULL(0x6ef8a4864ec42782),
+ ULL(0x974f87242105122e), ULL(0x079a8b9f3867130b), ULL(0xb7e49e5212c11064), ULL(0x273192e90ba31141),
+ ULL(0xd719b4c8478d17ba), ULL(0x47ccb8735eef169f), ULL(0xf7b2adbe744915f0), ULL(0x6767a1056d2b14d5),
+ ULL(0x969b73997b325ae3), ULL(0x064e7f2262505bc6), ULL(0xb6306aef48f658a9), ULL(0x26e566545194598c),
+ ULL(0xd6cd40751dba5f77), ULL(0x46184cce04d85e52), ULL(0xf66659032e7e5d3d), ULL(0x66b355b8371c5c18),
+ ULL(0x149ffc3a034cc151), ULL(0x844af0811a2ec074), ULL(0x3434e54c3088c31b), ULL(0xa4e1e9f729eac23e),
+ ULL(0x54c9cfd665c4c4c5), ULL(0xc41cc36d7ca6c5e0), ULL(0x7462d6a05600c68f), ULL(0xe4b7da1b4f62c7aa),
+ ULL(0x154b0887597b899c), ULL(0x859e043c401988b9), ULL(0x35e011f16abf8bd6), ULL(0xa5351d4a73dd8af3),
+ ULL(0x551d3b6b3ff38c08), ULL(0xc5c837d026918d2d), ULL(0x75b6221d0c378e42), ULL(0xe5632ea615558f67),
+ ULL(0x91ee70186597b4d1), ULL(0x013b7ca37cf5b5f4), ULL(0xb145696e5653b69b), ULL(0x219065d54f31b7be),
+ ULL(0xd1b843f4031fb145), ULL(0x416d4f4f1a7db060), ULL(0xf1135a8230dbb30f), ULL(0x61c6563929b9b22a),
+ ULL(0x903a84a53fa0fc1c), ULL(0x00ef881e26c2fd39), ULL(0xb0919dd30c64fe56), ULL(0x204491681506ff73),
+ ULL(0xd06cb7495928f988), ULL(0x40b9bbf2404af8ad), ULL(0xf0c7ae3f6aecfbc2), ULL(0x6012a284738efae7),
+ ULL(0x123e0b0647de67ae), ULL(0x82eb07bd5ebc668b), ULL(0x32951270741a65e4), ULL(0xa2401ecb6d7864c1),
+ ULL(0x526838ea2156623a), ULL(0xc2bd34513834631f), ULL(0x72c3219c12926070), ULL(0xe2162d270bf06155),
+ ULL(0x13eaffbb1de92f63), ULL(0x833ff300048b2e46), ULL(0x3341e6cd2e2d2d29), ULL(0xa394ea76374f2c0c),
+ ULL(0x53bccc577b612af7), ULL(0xc369c0ec62032bd2), ULL(0x7317d52148a528bd), ULL(0xe3c2d99a51c72998)
+ }, {
+ ULL(0x0000000000000000), ULL(0x34eaf4717c0c3868), ULL(0x68d4e9e3f81870d0), ULL(0x5c3e1d92841448b8),
+ ULL(0x51d041a26616a345), ULL(0x653ab5d31a1a9b2d), ULL(0x3904a8419e0ed395), ULL(0x0dee5c30e202ebfd),
+ ULL(0xa2a08344cd2c468b), ULL(0x964a7735b1207ee3), ULL(0xca746aa73534365b), ULL(0xfe9e9ed649380e33),
+ ULL(0xf370c2e6ab3ae5ce), ULL(0xc79a3697d736dda6), ULL(0x9ba42b055322951e), ULL(0xaf4edf742f2ead76),
+ ULL(0xc53995ec0d7ecff3), ULL(0xf1d3619d7172f79b), ULL(0xaded7c0ff566bf23), ULL(0x9907887e896a874b),
+ ULL(0x94e9d44e6b686cb6), ULL(0xa003203f176454de), ULL(0xfc3d3dad93701c66), ULL(0xc8d7c9dcef7c240e),
+ ULL(0x679916a8c0528978), ULL(0x5373e2d9bc5eb110), ULL(0x0f4dff4b384af9a8), ULL(0x3ba70b3a4446c1c0),
+ ULL(0x3649570aa6442a3d), ULL(0x02a3a37bda481255), ULL(0x5e9dbee95e5c5aed), ULL(0x6a774a9822506285),
+ ULL(0x0b0bb8bc8cdbdd02), ULL(0x3fe14ccdf0d7e56a), ULL(0x63df515f74c3add2), ULL(0x5735a52e08cf95ba),
+ ULL(0x5adbf91eeacd7e47), ULL(0x6e310d6f96c1462f), ULL(0x320f10fd12d50e97), ULL(0x06e5e48c6ed936ff),
+ ULL(0xa9ab3bf841f79b89), ULL(0x9d41cf893dfba3e1), ULL(0xc17fd21bb9efeb59), ULL(0xf595266ac5e3d331),
+ ULL(0xf87b7a5a27e138cc), ULL(0xcc918e2b5bed00a4), ULL(0x90af93b9dff9481c), ULL(0xa44567c8a3f57074),
+ ULL(0xce322d5081a512f1), ULL(0xfad8d921fda92a99), ULL(0xa6e6c4b379bd6221), ULL(0x920c30c205b15a49),
+ ULL(0x9fe26cf2e7b3b1b4), ULL(0xab0898839bbf89dc), ULL(0xf73685111fabc164), ULL(0xc3dc716063a7f90c),
+ ULL(0x6c92ae144c89547a), ULL(0x58785a6530856c12), ULL(0x044647f7b49124aa), ULL(0x30acb386c89d1cc2),
+ ULL(0x3d42efb62a9ff73f), ULL(0x09a81bc75693cf57), ULL(0x55960655d28787ef), ULL(0x617cf224ae8bbf87),
+ ULL(0x1616707919b7bb05), ULL(0x22fc840865bb836d), ULL(0x7ec2999ae1afcbd5), ULL(0x4a286deb9da3f3bd),
+ ULL(0x47c631db7fa11840), ULL(0x732cc5aa03ad2028), ULL(0x2f12d83887b96890), ULL(0x1bf82c49fbb550f8),
+ ULL(0xb4b6f33dd49bfd8e), ULL(0x805c074ca897c5e6), ULL(0xdc621ade2c838d5e), ULL(0xe888eeaf508fb536),
+ ULL(0xe566b29fb28d5ecb), ULL(0xd18c46eece8166a3), ULL(0x8db25b7c4a952e1b), ULL(0xb958af0d36991673),
+ ULL(0xd32fe59514c974f6), ULL(0xe7c511e468c54c9e), ULL(0xbbfb0c76ecd10426), ULL(0x8f11f80790dd3c4e),
+ ULL(0x82ffa43772dfd7b3), ULL(0xb61550460ed3efdb), ULL(0xea2b4dd48ac7a763), ULL(0xdec1b9a5f6cb9f0b),
+ ULL(0x718f66d1d9e5327d), ULL(0x456592a0a5e90a15), ULL(0x195b8f3221fd42ad), ULL(0x2db17b435df17ac5),
+ ULL(0x205f2773bff39138), ULL(0x14b5d302c3ffa950), ULL(0x488bce9047ebe1e8), ULL(0x7c613ae13be7d980),
+ ULL(0x1d1dc8c5956c6607), ULL(0x29f73cb4e9605e6f), ULL(0x75c921266d7416d7), ULL(0x4123d55711782ebf),
+ ULL(0x4ccd8967f37ac542), ULL(0x78277d168f76fd2a), ULL(0x241960840b62b592), ULL(0x10f394f5776e8dfa),
+ ULL(0xbfbd4b815840208c), ULL(0x8b57bff0244c18e4), ULL(0xd769a262a058505c), ULL(0xe3835613dc546834),
+ ULL(0xee6d0a233e5683c9), ULL(0xda87fe52425abba1), ULL(0x86b9e3c0c64ef319), ULL(0xb25317b1ba42cb71),
+ ULL(0xd8245d299812a9f4), ULL(0xeccea958e41e919c), ULL(0xb0f0b4ca600ad924), ULL(0x841a40bb1c06e14c),
+ ULL(0x89f41c8bfe040ab1), ULL(0xbd1ee8fa820832d9), ULL(0xe120f568061c7a61), ULL(0xd5ca01197a104209),
+ ULL(0x7a84de6d553eef7f), ULL(0x4e6e2a1c2932d717), ULL(0x1250378ead269faf), ULL(0x26bac3ffd12aa7c7),
+ ULL(0x2b549fcf33284c3a), ULL(0x1fbe6bbe4f247452), ULL(0x4380762ccb303cea), ULL(0x776a825db73c0482),
+ ULL(0x2c2ce0f2326e770b), ULL(0x18c614834e624f63), ULL(0x44f80911ca7607db), ULL(0x7012fd60b67a3fb3),
+ ULL(0x7dfca1505478d44e), ULL(0x491655212874ec26), ULL(0x152848b3ac60a49e), ULL(0x21c2bcc2d06c9cf6),
+ ULL(0x8e8c63b6ff423180), ULL(0xba6697c7834e09e8), ULL(0xe6588a55075a4150), ULL(0xd2b27e247b567938),
+ ULL(0xdf5c2214995492c5), ULL(0xebb6d665e558aaad), ULL(0xb788cbf7614ce215), ULL(0x83623f861d40da7d),
+ ULL(0xe915751e3f10b8f8), ULL(0xddff816f431c8090), ULL(0x81c19cfdc708c828), ULL(0xb52b688cbb04f040),
+ ULL(0xb8c534bc59061bbd), ULL(0x8c2fc0cd250a23d5), ULL(0xd011dd5fa11e6b6d), ULL(0xe4fb292edd125305),
+ ULL(0x4bb5f65af23cfe73), ULL(0x7f5f022b8e30c61b), ULL(0x23611fb90a248ea3), ULL(0x178bebc87628b6cb),
+ ULL(0x1a65b7f8942a5d36), ULL(0x2e8f4389e826655e), ULL(0x72b15e1b6c322de6), ULL(0x465baa6a103e158e),
+ ULL(0x2727584ebeb5aa09), ULL(0x13cdac3fc2b99261), ULL(0x4ff3b1ad46addad9), ULL(0x7b1945dc3aa1e2b1),
+ ULL(0x76f719ecd8a3094c), ULL(0x421ded9da4af3124), ULL(0x1e23f00f20bb799c), ULL(0x2ac9047e5cb741f4),
+ ULL(0x8587db0a7399ec82), ULL(0xb16d2f7b0f95d4ea), ULL(0xed5332e98b819c52), ULL(0xd9b9c698f78da43a),
+ ULL(0xd4579aa8158f4fc7), ULL(0xe0bd6ed9698377af), ULL(0xbc83734bed973f17), ULL(0x8869873a919b077f),
+ ULL(0xe21ecda2b3cb65fa), ULL(0xd6f439d3cfc75d92), ULL(0x8aca24414bd3152a), ULL(0xbe20d03037df2d42),
+ ULL(0xb3ce8c00d5ddc6bf), ULL(0x87247871a9d1fed7), ULL(0xdb1a65e32dc5b66f), ULL(0xeff0919251c98e07),
+ ULL(0x40be4ee67ee72371), ULL(0x7454ba9702eb1b19), ULL(0x286aa70586ff53a1), ULL(0x1c805374faf36bc9),
+ ULL(0x116e0f4418f18034), ULL(0x2584fb3564fdb85c), ULL(0x79bae6a7e0e9f0e4), ULL(0x4d5012d69ce5c88c),
+ ULL(0x3a3a908b2bd9cc0e), ULL(0x0ed064fa57d5f466), ULL(0x52ee7968d3c1bcde), ULL(0x66048d19afcd84b6),
+ ULL(0x6bead1294dcf6f4b), ULL(0x5f00255831c35723), ULL(0x033e38cab5d71f9b), ULL(0x37d4ccbbc9db27f3),
+ ULL(0x989a13cfe6f58a85), ULL(0xac70e7be9af9b2ed), ULL(0xf04efa2c1eedfa55), ULL(0xc4a40e5d62e1c23d),
+ ULL(0xc94a526d80e329c0), ULL(0xfda0a61cfcef11a8), ULL(0xa19ebb8e78fb5910), ULL(0x95744fff04f76178),
+ ULL(0xff03056726a703fd), ULL(0xcbe9f1165aab3b95), ULL(0x97d7ec84debf732d), ULL(0xa33d18f5a2b34b45),
+ ULL(0xaed344c540b1a0b8), ULL(0x9a39b0b43cbd98d0), ULL(0xc607ad26b8a9d068), ULL(0xf2ed5957c4a5e800),
+ ULL(0x5da38623eb8b4576), ULL(0x6949725297877d1e), ULL(0x35776fc0139335a6), ULL(0x019d9bb16f9f0dce),
+ ULL(0x0c73c7818d9de633), ULL(0x389933f0f191de5b), ULL(0x64a72e62758596e3), ULL(0x504dda130989ae8b),
+ ULL(0x31312837a702110c), ULL(0x05dbdc46db0e2964), ULL(0x59e5c1d45f1a61dc), ULL(0x6d0f35a5231659b4),
+ ULL(0x60e16995c114b249), ULL(0x540b9de4bd188a21), ULL(0x08358076390cc299), ULL(0x3cdf74074500faf1),
+ ULL(0x9391ab736a2e5787), ULL(0xa77b5f0216226fef), ULL(0xfb45429092362757), ULL(0xcfafb6e1ee3a1f3f),
+ ULL(0xc241ead10c38f4c2), ULL(0xf6ab1ea07034ccaa), ULL(0xaa950332f4208412), ULL(0x9e7ff743882cbc7a),
+ ULL(0xf408bddbaa7cdeff), ULL(0xc0e249aad670e697), ULL(0x9cdc54385264ae2f), ULL(0xa836a0492e689647),
+ ULL(0xa5d8fc79cc6a7dba), ULL(0x91320808b06645d2), ULL(0xcd0c159a34720d6a), ULL(0xf9e6e1eb487e3502),
+ ULL(0x56a83e9f67509874), ULL(0x6242caee1b5ca01c), ULL(0x3e7cd77c9f48e8a4), ULL(0x0a96230de344d0cc),
+ ULL(0x07787f3d01463b31), ULL(0x33928b4c7d4a0359), ULL(0x6fac96def95e4be1), ULL(0x5b4662af85527389)
+ }, {
+ ULL(0x0000000000000000), ULL(0x5858c0e565dcee16), ULL(0xb0b080cbcbb8dd2d), ULL(0xe8e8402eae64333b),
+ ULL(0x606101979771bb5b), ULL(0x3839c172f2ad554d), ULL(0xd0d1815c5cc96676), ULL(0x888941b939158860),
+ ULL(0xc0c2022e2fe376b7), ULL(0x989ac2cb4a3f98a1), ULL(0x707282e5e45bab9a), ULL(0x282a42008187458c),
+ ULL(0xa0a303b9b892cdec), ULL(0xf8fbc35cdd4e23fa), ULL(0x10138372732a10c1), ULL(0x484b439716f6fed7),
+ ULL(0x01fd9739c9e1ae8b), ULL(0x59a557dcac3d409d), ULL(0xb14d17f2025973a6), ULL(0xe915d71767859db0),
+ ULL(0x619c96ae5e9015d0), ULL(0x39c4564b3b4cfbc6), ULL(0xd12c16659528c8fd), ULL(0x8974d680f0f426eb),
+ ULL(0xc13f9517e602d83c), ULL(0x996755f283de362a), ULL(0x718f15dc2dba0511), ULL(0x29d7d5394866eb07),
+ ULL(0xa15e948071736367), ULL(0xf906546514af8d71), ULL(0x11ee144bbacbbe4a), ULL(0x49b6d4aedf17505c),
+ ULL(0x8382bd1605e41ef2), ULL(0xdbda7df36038f0e4), ULL(0x33323dddce5cc3df), ULL(0x6b6afd38ab802dc9),
+ ULL(0xe3e3bc819295a5a9), ULL(0xbbbb7c64f7494bbf), ULL(0x53533c4a592d7884), ULL(0x0b0bfcaf3cf19692),
+ ULL(0x4340bf382a076845), ULL(0x1b187fdd4fdb8653), ULL(0xf3f03ff3e1bfb568), ULL(0xaba8ff1684635b7e),
+ ULL(0x2321beafbd76d31e), ULL(0x7b797e4ad8aa3d08), ULL(0x93913e6476ce0e33), ULL(0xcbc9fe811312e025),
+ ULL(0x827f2a2fcc05b079), ULL(0xda27eacaa9d95e6f), ULL(0x32cfaae407bd6d54), ULL(0x6a976a0162618342),
+ ULL(0xe21e2bb85b740b22), ULL(0xba46eb5d3ea8e534), ULL(0x52aeab7390ccd60f), ULL(0x0af66b96f5103819),
+ ULL(0x42bd2801e3e6c6ce), ULL(0x1ae5e8e4863a28d8), ULL(0xf20da8ca285e1be3), ULL(0xaa55682f4d82f5f5),
+ ULL(0x22dc299674977d95), ULL(0x7a84e973114b9383), ULL(0x926ca95dbf2fa0b8), ULL(0xca3469b8daf34eae),
+ ULL(0x877de9489def7e01), ULL(0xdf2529adf8339017), ULL(0x37cd69835657a32c), ULL(0x6f95a966338b4d3a),
+ ULL(0xe71ce8df0a9ec55a), ULL(0xbf44283a6f422b4c), ULL(0x57ac6814c1261877), ULL(0x0ff4a8f1a4faf661),
+ ULL(0x47bfeb66b20c08b6), ULL(0x1fe72b83d7d0e6a0), ULL(0xf70f6bad79b4d59b), ULL(0xaf57ab481c683b8d),
+ ULL(0x27deeaf1257db3ed), ULL(0x7f862a1440a15dfb), ULL(0x976e6a3aeec56ec0), ULL(0xcf36aadf8b1980d6),
+ ULL(0x86807e71540ed08a), ULL(0xded8be9431d23e9c), ULL(0x3630feba9fb60da7), ULL(0x6e683e5ffa6ae3b1),
+ ULL(0xe6e17fe6c37f6bd1), ULL(0xbeb9bf03a6a385c7), ULL(0x5651ff2d08c7b6fc), ULL(0x0e093fc86d1b58ea),
+ ULL(0x46427c5f7beda63d), ULL(0x1e1abcba1e31482b), ULL(0xf6f2fc94b0557b10), ULL(0xaeaa3c71d5899506),
+ ULL(0x26237dc8ec9c1d66), ULL(0x7e7bbd2d8940f370), ULL(0x9693fd032724c04b), ULL(0xcecb3de642f82e5d),
+ ULL(0x04ff545e980b60f3), ULL(0x5ca794bbfdd78ee5), ULL(0xb44fd49553b3bdde), ULL(0xec171470366f53c8),
+ ULL(0x649e55c90f7adba8), ULL(0x3cc6952c6aa635be), ULL(0xd42ed502c4c20685), ULL(0x8c7615e7a11ee893),
+ ULL(0xc43d5670b7e81644), ULL(0x9c659695d234f852), ULL(0x748dd6bb7c50cb69), ULL(0x2cd5165e198c257f),
+ ULL(0xa45c57e72099ad1f), ULL(0xfc04970245454309), ULL(0x14ecd72ceb217032), ULL(0x4cb417c98efd9e24),
+ ULL(0x0502c36751eace78), ULL(0x5d5a03823436206e), ULL(0xb5b243ac9a521355), ULL(0xedea8349ff8efd43),
+ ULL(0x6563c2f0c69b7523), ULL(0x3d3b0215a3479b35), ULL(0xd5d3423b0d23a80e), ULL(0x8d8b82de68ff4618),
+ ULL(0xc5c0c1497e09b8cf), ULL(0x9d9801ac1bd556d9), ULL(0x75704182b5b165e2), ULL(0x2d288167d06d8bf4),
+ ULL(0xa5a1c0dee9780394), ULL(0xfdf9003b8ca4ed82), ULL(0x1511401522c0deb9), ULL(0x4d4980f0471c30af),
+ ULL(0x0efbd2913adffd02), ULL(0x56a312745f031314), ULL(0xbe4b525af167202f), ULL(0xe61392bf94bbce39),
+ ULL(0x6e9ad306adae4659), ULL(0x36c213e3c872a84f), ULL(0xde2a53cd66169b74), ULL(0x8672932803ca7562),
+ ULL(0xce39d0bf153c8bb5), ULL(0x9661105a70e065a3), ULL(0x7e895074de845698), ULL(0x26d19091bb58b88e),
+ ULL(0xae58d128824d30ee), ULL(0xf60011cde791def8), ULL(0x1ee851e349f5edc3), ULL(0x46b091062c2903d5),
+ ULL(0x0f0645a8f33e5389), ULL(0x575e854d96e2bd9f), ULL(0xbfb6c56338868ea4), ULL(0xe7ee05865d5a60b2),
+ ULL(0x6f67443f644fe8d2), ULL(0x373f84da019306c4), ULL(0xdfd7c4f4aff735ff), ULL(0x878f0411ca2bdbe9),
+ ULL(0xcfc44786dcdd253e), ULL(0x979c8763b901cb28), ULL(0x7f74c74d1765f813), ULL(0x272c07a872b91605),
+ ULL(0xafa546114bac9e65), ULL(0xf7fd86f42e707073), ULL(0x1f15c6da80144348), ULL(0x474d063fe5c8ad5e),
+ ULL(0x8d796f873f3be3f0), ULL(0xd521af625ae70de6), ULL(0x3dc9ef4cf4833edd), ULL(0x65912fa9915fd0cb),
+ ULL(0xed186e10a84a58ab), ULL(0xb540aef5cd96b6bd), ULL(0x5da8eedb63f28586), ULL(0x05f02e3e062e6b90),
+ ULL(0x4dbb6da910d89547), ULL(0x15e3ad4c75047b51), ULL(0xfd0bed62db60486a), ULL(0xa5532d87bebca67c),
+ ULL(0x2dda6c3e87a92e1c), ULL(0x7582acdbe275c00a), ULL(0x9d6aecf54c11f331), ULL(0xc5322c1029cd1d27),
+ ULL(0x8c84f8bef6da4d7b), ULL(0xd4dc385b9306a36d), ULL(0x3c3478753d629056), ULL(0x646cb89058be7e40),
+ ULL(0xece5f92961abf620), ULL(0xb4bd39cc04771836), ULL(0x5c5579e2aa132b0d), ULL(0x040db907cfcfc51b),
+ ULL(0x4c46fa90d9393bcc), ULL(0x141e3a75bce5d5da), ULL(0xfcf67a5b1281e6e1), ULL(0xa4aebabe775d08f7),
+ ULL(0x2c27fb074e488097), ULL(0x747f3be22b946e81), ULL(0x9c977bcc85f05dba), ULL(0xc4cfbb29e02cb3ac),
+ ULL(0x89863bd9a7308303), ULL(0xd1defb3cc2ec6d15), ULL(0x3936bb126c885e2e), ULL(0x616e7bf70954b038),
+ ULL(0xe9e73a4e30413858), ULL(0xb1bffaab559dd64e), ULL(0x5957ba85fbf9e575), ULL(0x010f7a609e250b63),
+ ULL(0x494439f788d3f5b4), ULL(0x111cf912ed0f1ba2), ULL(0xf9f4b93c436b2899), ULL(0xa1ac79d926b7c68f),
+ ULL(0x292538601fa24eef), ULL(0x717df8857a7ea0f9), ULL(0x9995b8abd41a93c2), ULL(0xc1cd784eb1c67dd4),
+ ULL(0x887bace06ed12d88), ULL(0xd0236c050b0dc39e), ULL(0x38cb2c2ba569f0a5), ULL(0x6093eccec0b51eb3),
+ ULL(0xe81aad77f9a096d3), ULL(0xb0426d929c7c78c5), ULL(0x58aa2dbc32184bfe), ULL(0x00f2ed5957c4a5e8),
+ ULL(0x48b9aece41325b3f), ULL(0x10e16e2b24eeb529), ULL(0xf8092e058a8a8612), ULL(0xa051eee0ef566804),
+ ULL(0x28d8af59d643e064), ULL(0x70806fbcb39f0e72), ULL(0x98682f921dfb3d49), ULL(0xc030ef777827d35f),
+ ULL(0x0a0486cfa2d49df1), ULL(0x525c462ac70873e7), ULL(0xbab40604696c40dc), ULL(0xe2ecc6e10cb0aeca),
+ ULL(0x6a65875835a526aa), ULL(0x323d47bd5079c8bc), ULL(0xdad50793fe1dfb87), ULL(0x828dc7769bc11591),
+ ULL(0xcac684e18d37eb46), ULL(0x929e4404e8eb0550), ULL(0x7a76042a468f366b), ULL(0x222ec4cf2353d87d),
+ ULL(0xaaa785761a46501d), ULL(0xf2ff45937f9abe0b), ULL(0x1a1705bdd1fe8d30), ULL(0x424fc558b4226326),
+ ULL(0x0bf911f66b35337a), ULL(0x53a1d1130ee9dd6c), ULL(0xbb49913da08dee57), ULL(0xe31151d8c5510041),
+ ULL(0x6b981061fc448821), ULL(0x33c0d08499986637), ULL(0xdb2890aa37fc550c), ULL(0x8370504f5220bb1a),
+ ULL(0xcb3b13d844d645cd), ULL(0x9363d33d210aabdb), ULL(0x7b8b93138f6e98e0), ULL(0x23d353f6eab276f6),
+ ULL(0xab5a124fd3a7fe96), ULL(0xf302d2aab67b1080), ULL(0x1bea9284181f23bb), ULL(0x43b252617dc3cdad)
+ }, {
+ ULL(0x0000000000000000), ULL(0x1cf6a52375befb05), ULL(0x38ec4b47ea7cf70b), ULL(0x241aee649fc20c0e),
+ ULL(0x70d8978ed4f9ee17), ULL(0x6c2e32ada1471512), ULL(0x4834dcc93e85191c), ULL(0x54c279ea4b3be219),
+ ULL(0xe0b02f1da9f3dd2f), ULL(0xfc468a3edc4d262a), ULL(0xd85c645a438f2a24), ULL(0xc4aac1793631d121),
+ ULL(0x9068b8937d0a3338), ULL(0x8c9e1db008b4c83d), ULL(0xa884f3d49776c433), ULL(0xb47256f7e2c83f36),
+ ULL(0xc0615f3a52e7bb5f), ULL(0xdc97fa192759405a), ULL(0xf88d147db89b4c54), ULL(0xe47bb15ecd25b751),
+ ULL(0xb0b9c8b4861e5548), ULL(0xac4f6d97f3a0ae4d), ULL(0x885583f36c62a243), ULL(0x94a326d019dc5946),
+ ULL(0x20d17027fb146670), ULL(0x3c27d5048eaa9d75), ULL(0x183d3b601168917b), ULL(0x04cb9e4364d66a7e),
+ ULL(0x5009e7a92fed8867), ULL(0x4cff428a5a537362), ULL(0x68e5aceec5917f6c), ULL(0x741309cdb02f8469),
+ ULL(0x80c3be74a4ce77bf), ULL(0x9c351b57d1708cba), ULL(0xb82ff5334eb280b4), ULL(0xa4d950103b0c7bb1),
+ ULL(0xf01b29fa703799a8), ULL(0xeced8cd9058962ad), ULL(0xc8f762bd9a4b6ea3), ULL(0xd401c79eeff595a6),
+ ULL(0x607391690d3daa90), ULL(0x7c85344a78835195), ULL(0x589fda2ee7415d9b), ULL(0x44697f0d92ffa69e),
+ ULL(0x10ab06e7d9c44487), ULL(0x0c5da3c4ac7abf82), ULL(0x28474da033b8b38c), ULL(0x34b1e88346064889),
+ ULL(0x40a2e14ef629cce0), ULL(0x5c54446d839737e5), ULL(0x784eaa091c553beb), ULL(0x64b80f2a69ebc0ee),
+ ULL(0x307a76c022d022f7), ULL(0x2c8cd3e3576ed9f2), ULL(0x08963d87c8acd5fc), ULL(0x146098a4bd122ef9),
+ ULL(0xa012ce535fda11cf), ULL(0xbce46b702a64eaca), ULL(0x98fe8514b5a6e6c4), ULL(0x84082037c0181dc1),
+ ULL(0xd0ca59dd8b23ffd8), ULL(0xcc3cfcfefe9d04dd), ULL(0xe826129a615f08d3), ULL(0xf4d0b7b914e1f3d6),
+ ULL(0x81ffef8cdfbaac9b), ULL(0x9d094aafaa04579e), ULL(0xb913a4cb35c65b90), ULL(0xa5e501e84078a095),
+ ULL(0xf12778020b43428c), ULL(0xedd1dd217efdb989), ULL(0xc9cb3345e13fb587), ULL(0xd53d966694814e82),
+ ULL(0x614fc091764971b4), ULL(0x7db965b203f78ab1), ULL(0x59a38bd69c3586bf), ULL(0x45552ef5e98b7dba),
+ ULL(0x1197571fa2b09fa3), ULL(0x0d61f23cd70e64a6), ULL(0x297b1c5848cc68a8), ULL(0x358db97b3d7293ad),
+ ULL(0x419eb0b68d5d17c4), ULL(0x5d681595f8e3ecc1), ULL(0x7972fbf16721e0cf), ULL(0x65845ed2129f1bca),
+ ULL(0x3146273859a4f9d3), ULL(0x2db0821b2c1a02d6), ULL(0x09aa6c7fb3d80ed8), ULL(0x155cc95cc666f5dd),
+ ULL(0xa12e9fab24aecaeb), ULL(0xbdd83a88511031ee), ULL(0x99c2d4ecced23de0), ULL(0x853471cfbb6cc6e5),
+ ULL(0xd1f60825f05724fc), ULL(0xcd00ad0685e9dff9), ULL(0xe91a43621a2bd3f7), ULL(0xf5ece6416f9528f2),
+ ULL(0x013c51f87b74db24), ULL(0x1dcaf4db0eca2021), ULL(0x39d01abf91082c2f), ULL(0x2526bf9ce4b6d72a),
+ ULL(0x71e4c676af8d3533), ULL(0x6d126355da33ce36), ULL(0x49088d3145f1c238), ULL(0x55fe2812304f393d),
+ ULL(0xe18c7ee5d287060b), ULL(0xfd7adbc6a739fd0e), ULL(0xd96035a238fbf100), ULL(0xc59690814d450a05),
+ ULL(0x9154e96b067ee81c), ULL(0x8da24c4873c01319), ULL(0xa9b8a22cec021f17), ULL(0xb54e070f99bce412),
+ ULL(0xc15d0ec22993607b), ULL(0xddababe15c2d9b7e), ULL(0xf9b14585c3ef9770), ULL(0xe547e0a6b6516c75),
+ ULL(0xb185994cfd6a8e6c), ULL(0xad733c6f88d47569), ULL(0x8969d20b17167967), ULL(0x959f772862a88262),
+ ULL(0x21ed21df8060bd54), ULL(0x3d1b84fcf5de4651), ULL(0x19016a986a1c4a5f), ULL(0x05f7cfbb1fa2b15a),
+ ULL(0x5135b65154995343), ULL(0x4dc313722127a846), ULL(0x69d9fd16bee5a448), ULL(0x752f5835cb5b5f4d),
+ ULL(0x83874d7c28521ad2), ULL(0x9f71e85f5dece1d7), ULL(0xbb6b063bc22eedd9), ULL(0xa79da318b79016dc),
+ ULL(0xf35fdaf2fcabf4c5), ULL(0xefa97fd189150fc0), ULL(0xcbb391b516d703ce), ULL(0xd74534966369f8cb),
+ ULL(0x6337626181a1c7fd), ULL(0x7fc1c742f41f3cf8), ULL(0x5bdb29266bdd30f6), ULL(0x472d8c051e63cbf3),
+ ULL(0x13eff5ef555829ea), ULL(0x0f1950cc20e6d2ef), ULL(0x2b03bea8bf24dee1), ULL(0x37f51b8bca9a25e4),
+ ULL(0x43e612467ab5a18d), ULL(0x5f10b7650f0b5a88), ULL(0x7b0a590190c95686), ULL(0x67fcfc22e577ad83),
+ ULL(0x333e85c8ae4c4f9a), ULL(0x2fc820ebdbf2b49f), ULL(0x0bd2ce8f4430b891), ULL(0x17246bac318e4394),
+ ULL(0xa3563d5bd3467ca2), ULL(0xbfa09878a6f887a7), ULL(0x9bba761c393a8ba9), ULL(0x874cd33f4c8470ac),
+ ULL(0xd38eaad507bf92b5), ULL(0xcf780ff6720169b0), ULL(0xeb62e192edc365be), ULL(0xf79444b1987d9ebb),
+ ULL(0x0344f3088c9c6d6d), ULL(0x1fb2562bf9229668), ULL(0x3ba8b84f66e09a66), ULL(0x275e1d6c135e6163),
+ ULL(0x739c64865865837a), ULL(0x6f6ac1a52ddb787f), ULL(0x4b702fc1b2197471), ULL(0x57868ae2c7a78f74),
+ ULL(0xe3f4dc15256fb042), ULL(0xff02793650d14b47), ULL(0xdb189752cf134749), ULL(0xc7ee3271baadbc4c),
+ ULL(0x932c4b9bf1965e55), ULL(0x8fdaeeb88428a550), ULL(0xabc000dc1beaa95e), ULL(0xb736a5ff6e54525b),
+ ULL(0xc325ac32de7bd632), ULL(0xdfd30911abc52d37), ULL(0xfbc9e77534072139), ULL(0xe73f425641b9da3c),
+ ULL(0xb3fd3bbc0a823825), ULL(0xaf0b9e9f7f3cc320), ULL(0x8b1170fbe0fecf2e), ULL(0x97e7d5d89540342b),
+ ULL(0x2395832f77880b1d), ULL(0x3f63260c0236f018), ULL(0x1b79c8689df4fc16), ULL(0x078f6d4be84a0713),
+ ULL(0x534d14a1a371e50a), ULL(0x4fbbb182d6cf1e0f), ULL(0x6ba15fe6490d1201), ULL(0x7757fac53cb3e904),
+ ULL(0x0278a2f0f7e8b649), ULL(0x1e8e07d382564d4c), ULL(0x3a94e9b71d944142), ULL(0x26624c94682aba47),
+ ULL(0x72a0357e2311585e), ULL(0x6e56905d56afa35b), ULL(0x4a4c7e39c96daf55), ULL(0x56badb1abcd35450),
+ ULL(0xe2c88ded5e1b6b66), ULL(0xfe3e28ce2ba59063), ULL(0xda24c6aab4679c6d), ULL(0xc6d26389c1d96768),
+ ULL(0x92101a638ae28571), ULL(0x8ee6bf40ff5c7e74), ULL(0xaafc5124609e727a), ULL(0xb60af4071520897f),
+ ULL(0xc219fdcaa50f0d16), ULL(0xdeef58e9d0b1f613), ULL(0xfaf5b68d4f73fa1d), ULL(0xe60313ae3acd0118),
+ ULL(0xb2c16a4471f6e301), ULL(0xae37cf6704481804), ULL(0x8a2d21039b8a140a), ULL(0x96db8420ee34ef0f),
+ ULL(0x22a9d2d70cfcd039), ULL(0x3e5f77f479422b3c), ULL(0x1a459990e6802732), ULL(0x06b33cb3933edc37),
+ ULL(0x52714559d8053e2e), ULL(0x4e87e07aadbbc52b), ULL(0x6a9d0e1e3279c925), ULL(0x766bab3d47c73220),
+ ULL(0x82bb1c845326c1f6), ULL(0x9e4db9a726983af3), ULL(0xba5757c3b95a36fd), ULL(0xa6a1f2e0cce4cdf8),
+ ULL(0xf2638b0a87df2fe1), ULL(0xee952e29f261d4e4), ULL(0xca8fc04d6da3d8ea), ULL(0xd679656e181d23ef),
+ ULL(0x620b3399fad51cd9), ULL(0x7efd96ba8f6be7dc), ULL(0x5ae778de10a9ebd2), ULL(0x4611ddfd651710d7),
+ ULL(0x12d3a4172e2cf2ce), ULL(0x0e2501345b9209cb), ULL(0x2a3fef50c45005c5), ULL(0x36c94a73b1eefec0),
+ ULL(0x42da43be01c17aa9), ULL(0x5e2ce69d747f81ac), ULL(0x7a3608f9ebbd8da2), ULL(0x66c0adda9e0376a7),
+ ULL(0x3202d430d53894be), ULL(0x2ef47113a0866fbb), ULL(0x0aee9f773f4463b5), ULL(0x16183a544afa98b0),
+ ULL(0xa26a6ca3a832a786), ULL(0xbe9cc980dd8c5c83), ULL(0x9a8627e4424e508d), ULL(0x867082c737f0ab88),
+ ULL(0xd2b2fb2d7ccb4991), ULL(0xce445e0e0975b294), ULL(0xea5eb06a96b7be9a), ULL(0xf6a81549e309459f)
+ }, {
+ ULL(0x0000000000000000), ULL(0x8777099dc7837741), ULL(0x0eef123a8f07ef82), ULL(0x89981ba7488498c3),
+ ULL(0x9da6b71189289de0), ULL(0x1ad1be8c4eabeaa1), ULL(0x9349a52b062f7262), ULL(0x143eacb6c1ac0523),
+ ULL(0xbb35fd4685767924), ULL(0x3c42f4db42f50e65), ULL(0xb5daef7c0a7196a6), ULL(0x32ade6e1cdf2e1e7),
+ ULL(0x26934a570c5ee4c4), ULL(0xa1e443cacbdd9385), ULL(0x287c586d83590b46), ULL(0xaf0b51f044da7c07),
+ ULL(0x766bfa8d0aedf248), ULL(0xf11cf310cd6e8509), ULL(0x7884e8b785ea1dca), ULL(0xfff3e12a42696a8b),
+ ULL(0xebcd4d9c83c56fa8), ULL(0x6cba4401444618e9), ULL(0xe5225fa60cc2802a), ULL(0x6255563bcb41f76b),
+ ULL(0xcd5e07cb8f9b8b6c), ULL(0x4a290e564818fc2d), ULL(0xc3b115f1009c64ee), ULL(0x44c61c6cc71f13af),
+ ULL(0x50f8b0da06b3168c), ULL(0xd78fb947c13061cd), ULL(0x5e17a2e089b4f90e), ULL(0xd960ab7d4e378e4f),
+ ULL(0xecd6f41b15dae591), ULL(0x6ba1fd86d25992d0), ULL(0xe239e6219add0a13), ULL(0x654eefbc5d5e7d52),
+ ULL(0x7170430a9cf27871), ULL(0xf6074a975b710f30), ULL(0x7f9f513013f597f3), ULL(0xf8e858add476e0b2),
+ ULL(0x57e3095d90ac9cb5), ULL(0xd09400c0572febf4), ULL(0x590c1b671fab7337), ULL(0xde7b12fad8280476),
+ ULL(0xca45be4c19840155), ULL(0x4d32b7d1de077614), ULL(0xc4aaac769683eed7), ULL(0x43dda5eb51009996),
+ ULL(0x9abd0e961f3717d9), ULL(0x1dca070bd8b46098), ULL(0x94521cac9030f85b), ULL(0x1325153157b38f1a),
+ ULL(0x071bb987961f8a39), ULL(0x806cb01a519cfd78), ULL(0x09f4abbd191865bb), ULL(0x8e83a220de9b12fa),
+ ULL(0x2188f3d09a416efd), ULL(0xa6fffa4d5dc219bc), ULL(0x2f67e1ea1546817f), ULL(0xa810e877d2c5f63e),
+ ULL(0xbc2e44c11369f31d), ULL(0x3b594d5cd4ea845c), ULL(0xb2c156fb9c6e1c9f), ULL(0x35b65f665bed6bde),
+ ULL(0x59d57b52bd9388c6), ULL(0xdea272cf7a10ff87), ULL(0x573a696832946744), ULL(0xd04d60f5f5171005),
+ ULL(0xc473cc4334bb1526), ULL(0x4304c5def3386267), ULL(0xca9cde79bbbcfaa4), ULL(0x4debd7e47c3f8de5),
+ ULL(0xe2e0861438e5f1e2), ULL(0x65978f89ff6686a3), ULL(0xec0f942eb7e21e60), ULL(0x6b789db370616921),
+ ULL(0x7f463105b1cd6c02), ULL(0xf8313898764e1b43), ULL(0x71a9233f3eca8380), ULL(0xf6de2aa2f949f4c1),
+ ULL(0x2fbe81dfb77e7a8e), ULL(0xa8c9884270fd0dcf), ULL(0x215193e53879950c), ULL(0xa6269a78fffae24d),
+ ULL(0xb21836ce3e56e76e), ULL(0x356f3f53f9d5902f), ULL(0xbcf724f4b15108ec), ULL(0x3b802d6976d27fad),
+ ULL(0x948b7c99320803aa), ULL(0x13fc7504f58b74eb), ULL(0x9a646ea3bd0fec28), ULL(0x1d13673e7a8c9b69),
+ ULL(0x092dcb88bb209e4a), ULL(0x8e5ac2157ca3e90b), ULL(0x07c2d9b2342771c8), ULL(0x80b5d02ff3a40689),
+ ULL(0xb5038f49a8496d57), ULL(0x327486d46fca1a16), ULL(0xbbec9d73274e82d5), ULL(0x3c9b94eee0cdf594),
+ ULL(0x28a538582161f0b7), ULL(0xafd231c5e6e287f6), ULL(0x264a2a62ae661f35), ULL(0xa13d23ff69e56874),
+ ULL(0x0e36720f2d3f1473), ULL(0x89417b92eabc6332), ULL(0x00d96035a238fbf1), ULL(0x87ae69a865bb8cb0),
+ ULL(0x9390c51ea4178993), ULL(0x14e7cc836394fed2), ULL(0x9d7fd7242b106611), ULL(0x1a08deb9ec931150),
+ ULL(0xc36875c4a2a49f1f), ULL(0x441f7c596527e85e), ULL(0xcd8767fe2da3709d), ULL(0x4af06e63ea2007dc),
+ ULL(0x5ecec2d52b8c02ff), ULL(0xd9b9cb48ec0f75be), ULL(0x5021d0efa48bed7d), ULL(0xd756d97263089a3c),
+ ULL(0x785d888227d2e63b), ULL(0xff2a811fe051917a), ULL(0x76b29ab8a8d509b9), ULL(0xf1c593256f567ef8),
+ ULL(0xe5fb3f93aefa7bdb), ULL(0x628c360e69790c9a), ULL(0xeb142da921fd9459), ULL(0x6c632434e67ee318),
+ ULL(0x33d265c1ed005268), ULL(0xb4a56c5c2a832529), ULL(0x3d3d77fb6207bdea), ULL(0xba4a7e66a584caab),
+ ULL(0xae74d2d06428cf88), ULL(0x2903db4da3abb8c9), ULL(0xa09bc0eaeb2f200a), ULL(0x27ecc9772cac574b),
+ ULL(0x88e7988768762b4c), ULL(0x0f90911aaff55c0d), ULL(0x86088abde771c4ce), ULL(0x017f832020f2b38f),
+ ULL(0x15412f96e15eb6ac), ULL(0x9236260b26ddc1ed), ULL(0x1bae3dac6e59592e), ULL(0x9cd93431a9da2e6f),
+ ULL(0x45b99f4ce7eda020), ULL(0xc2ce96d1206ed761), ULL(0x4b568d7668ea4fa2), ULL(0xcc2184ebaf6938e3),
+ ULL(0xd81f285d6ec53dc0), ULL(0x5f6821c0a9464a81), ULL(0xd6f03a67e1c2d242), ULL(0x518733fa2641a503),
+ ULL(0xfe8c620a629bd904), ULL(0x79fb6b97a518ae45), ULL(0xf0637030ed9c3686), ULL(0x771479ad2a1f41c7),
+ ULL(0x632ad51bebb344e4), ULL(0xe45ddc862c3033a5), ULL(0x6dc5c72164b4ab66), ULL(0xeab2cebca337dc27),
+ ULL(0xdf0491daf8dab7f9), ULL(0x587398473f59c0b8), ULL(0xd1eb83e077dd587b), ULL(0x569c8a7db05e2f3a),
+ ULL(0x42a226cb71f22a19), ULL(0xc5d52f56b6715d58), ULL(0x4c4d34f1fef5c59b), ULL(0xcb3a3d6c3976b2da),
+ ULL(0x64316c9c7daccedd), ULL(0xe3466501ba2fb99c), ULL(0x6ade7ea6f2ab215f), ULL(0xeda9773b3528561e),
+ ULL(0xf997db8df484533d), ULL(0x7ee0d2103307247c), ULL(0xf778c9b77b83bcbf), ULL(0x700fc02abc00cbfe),
+ ULL(0xa96f6b57f23745b1), ULL(0x2e1862ca35b432f0), ULL(0xa780796d7d30aa33), ULL(0x20f770f0bab3dd72),
+ ULL(0x34c9dc467b1fd851), ULL(0xb3bed5dbbc9caf10), ULL(0x3a26ce7cf41837d3), ULL(0xbd51c7e1339b4092),
+ ULL(0x125a961177413c95), ULL(0x952d9f8cb0c24bd4), ULL(0x1cb5842bf846d317), ULL(0x9bc28db63fc5a456),
+ ULL(0x8ffc2100fe69a175), ULL(0x088b289d39ead634), ULL(0x8113333a716e4ef7), ULL(0x06643aa7b6ed39b6),
+ ULL(0x6a071e935093daae), ULL(0xed70170e9710adef), ULL(0x64e80ca9df94352c), ULL(0xe39f05341817426d),
+ ULL(0xf7a1a982d9bb474e), ULL(0x70d6a01f1e38300f), ULL(0xf94ebbb856bca8cc), ULL(0x7e39b225913fdf8d),
+ ULL(0xd132e3d5d5e5a38a), ULL(0x5645ea481266d4cb), ULL(0xdfddf1ef5ae24c08), ULL(0x58aaf8729d613b49),
+ ULL(0x4c9454c45ccd3e6a), ULL(0xcbe35d599b4e492b), ULL(0x427b46fed3cad1e8), ULL(0xc50c4f631449a6a9),
+ ULL(0x1c6ce41e5a7e28e6), ULL(0x9b1bed839dfd5fa7), ULL(0x1283f624d579c764), ULL(0x95f4ffb912fab025),
+ ULL(0x81ca530fd356b506), ULL(0x06bd5a9214d5c247), ULL(0x8f2541355c515a84), ULL(0x085248a89bd22dc5),
+ ULL(0xa7591958df0851c2), ULL(0x202e10c5188b2683), ULL(0xa9b60b62500fbe40), ULL(0x2ec102ff978cc901),
+ ULL(0x3affae495620cc22), ULL(0xbd88a7d491a3bb63), ULL(0x3410bc73d92723a0), ULL(0xb367b5ee1ea454e1),
+ ULL(0x86d1ea8845493f3f), ULL(0x01a6e31582ca487e), ULL(0x883ef8b2ca4ed0bd), ULL(0x0f49f12f0dcda7fc),
+ ULL(0x1b775d99cc61a2df), ULL(0x9c0054040be2d59e), ULL(0x15984fa343664d5d), ULL(0x92ef463e84e53a1c),
+ ULL(0x3de417cec03f461b), ULL(0xba931e5307bc315a), ULL(0x330b05f44f38a999), ULL(0xb47c0c6988bbded8),
+ ULL(0xa042a0df4917dbfb), ULL(0x2735a9428e94acba), ULL(0xaeadb2e5c6103479), ULL(0x29dabb7801934338),
+ ULL(0xf0ba10054fa4cd77), ULL(0x77cd19988827ba36), ULL(0xfe55023fc0a322f5), ULL(0x79220ba2072055b4),
+ ULL(0x6d1ca714c68c5097), ULL(0xea6bae89010f27d6), ULL(0x63f3b52e498bbf15), ULL(0xe484bcb38e08c854),
+ ULL(0x4b8fed43cad2b453), ULL(0xccf8e4de0d51c312), ULL(0x4560ff7945d55bd1), ULL(0xc217f6e482562c90),
+ ULL(0xd6295a5243fa29b3), ULL(0x515e53cf84795ef2), ULL(0xd8c64868ccfdc631), ULL(0x5fb141f50b7eb170)
+ }, {
+ ULL(0x0000000000000000), ULL(0x66a4cb82db01a4d0), ULL(0x4d30056020240b44), ULL(0x2b94cee2fb25af94),
+ ULL(0x9a600ac040481688), ULL(0xfcc4c1429b49b258), ULL(0xd7500fa0606c1dcc), ULL(0xb1f4c422bb6db91c),
+ ULL(0xb5b986e516b76ff5), ULL(0xd31d4d67cdb6cb25), ULL(0xf8898385369364b1), ULL(0x9e2d4807ed92c061),
+ ULL(0x2fd98c2556ff797d), ULL(0x497d47a78dfeddad), ULL(0x62e9894576db7239), ULL(0x044d42c7addad6e9),
+ ULL(0xeb0b9faeba499c0f), ULL(0x8daf542c614838df), ULL(0xa63b9ace9a6d974b), ULL(0xc09f514c416c339b),
+ ULL(0x716b956efa018a87), ULL(0x17cf5eec21002e57), ULL(0x3c5b900eda2581c3), ULL(0x5aff5b8c01242513),
+ ULL(0x5eb2194bacfef3fa), ULL(0x3816d2c977ff572a), ULL(0x13821c2b8cdaf8be), ULL(0x7526d7a957db5c6e),
+ ULL(0xc4d2138becb6e572), ULL(0xa276d80937b741a2), ULL(0x89e216ebcc92ee36), ULL(0xef46dd6917934ae6),
+ ULL(0xd6173e5d7593381f), ULL(0xb0b3f5dfae929ccf), ULL(0x9b273b3d55b7335b), ULL(0xfd83f0bf8eb6978b),
+ ULL(0x4c77349d35db2e97), ULL(0x2ad3ff1feeda8a47), ULL(0x014731fd15ff25d3), ULL(0x67e3fa7fcefe8103),
+ ULL(0x63aeb8b8632457ea), ULL(0x050a733ab825f33a), ULL(0x2e9ebdd843005cae), ULL(0x483a765a9801f87e),
+ ULL(0xf9ceb278236c4162), ULL(0x9f6a79faf86de5b2), ULL(0xb4feb71803484a26), ULL(0xd25a7c9ad849eef6),
+ ULL(0x3d1ca1f3cfdaa410), ULL(0x5bb86a7114db00c0), ULL(0x702ca493effeaf54), ULL(0x16886f1134ff0b84),
+ ULL(0xa77cab338f92b298), ULL(0xc1d860b154931648), ULL(0xea4cae53afb6b9dc), ULL(0x8ce865d174b71d0c),
+ ULL(0x88a52716d96dcbe5), ULL(0xee01ec94026c6f35), ULL(0xc5952276f949c0a1), ULL(0xa331e9f422486471),
+ ULL(0x12c52dd69925dd6d), ULL(0x7461e654422479bd), ULL(0x5ff528b6b901d629), ULL(0x3951e334620072f9),
+ ULL(0xac2f7cbaea26713e), ULL(0xca8bb7383127d5ee), ULL(0xe11f79daca027a7a), ULL(0x87bbb2581103deaa),
+ ULL(0x364f767aaa6e67b6), ULL(0x50ebbdf8716fc366), ULL(0x7b7f731a8a4a6cf2), ULL(0x1ddbb898514bc822),
+ ULL(0x1996fa5ffc911ecb), ULL(0x7f3231dd2790ba1b), ULL(0x54a6ff3fdcb5158f), ULL(0x320234bd07b4b15f),
+ ULL(0x83f6f09fbcd90843), ULL(0xe5523b1d67d8ac93), ULL(0xcec6f5ff9cfd0307), ULL(0xa8623e7d47fca7d7),
+ ULL(0x4724e314506fed31), ULL(0x218028968b6e49e1), ULL(0x0a14e674704be675), ULL(0x6cb02df6ab4a42a5),
+ ULL(0xdd44e9d41027fbb9), ULL(0xbbe02256cb265f69), ULL(0x9074ecb43003f0fd), ULL(0xf6d02736eb02542d),
+ ULL(0xf29d65f146d882c4), ULL(0x9439ae739dd92614), ULL(0xbfad609166fc8980), ULL(0xd909ab13bdfd2d50),
+ ULL(0x68fd6f310690944c), ULL(0x0e59a4b3dd91309c), ULL(0x25cd6a5126b49f08), ULL(0x4369a1d3fdb53bd8),
+ ULL(0x7a3842e79fb54921), ULL(0x1c9c896544b4edf1), ULL(0x37084787bf914265), ULL(0x51ac8c056490e6b5),
+ ULL(0xe0584827dffd5fa9), ULL(0x86fc83a504fcfb79), ULL(0xad684d47ffd954ed), ULL(0xcbcc86c524d8f03d),
+ ULL(0xcf81c402890226d4), ULL(0xa9250f8052038204), ULL(0x82b1c162a9262d90), ULL(0xe4150ae072278940),
+ ULL(0x55e1cec2c94a305c), ULL(0x33450540124b948c), ULL(0x18d1cba2e96e3b18), ULL(0x7e750020326f9fc8),
+ ULL(0x9133dd4925fcd52e), ULL(0xf79716cbfefd71fe), ULL(0xdc03d82905d8de6a), ULL(0xbaa713abded97aba),
+ ULL(0x0b53d78965b4c3a6), ULL(0x6df71c0bbeb56776), ULL(0x4663d2e94590c8e2), ULL(0x20c7196b9e916c32),
+ ULL(0x248a5bac334bbadb), ULL(0x422e902ee84a1e0b), ULL(0x69ba5ecc136fb19f), ULL(0x0f1e954ec86e154f),
+ ULL(0xbeea516c7303ac53), ULL(0xd84e9aeea8020883), ULL(0xf3da540c5327a717), ULL(0x957e9f8e882603c7),
+ ULL(0x585ff874d54de27c), ULL(0x3efb33f60e4c46ac), ULL(0x156ffd14f569e938), ULL(0x73cb36962e684de8),
+ ULL(0xc23ff2b49505f4f4), ULL(0xa49b39364e045024), ULL(0x8f0ff7d4b521ffb0), ULL(0xe9ab3c566e205b60),
+ ULL(0xede67e91c3fa8d89), ULL(0x8b42b51318fb2959), ULL(0xa0d67bf1e3de86cd), ULL(0xc672b07338df221d),
+ ULL(0x7786745183b29b01), ULL(0x1122bfd358b33fd1), ULL(0x3ab67131a3969045), ULL(0x5c12bab378973495),
+ ULL(0xb35467da6f047e73), ULL(0xd5f0ac58b405daa3), ULL(0xfe6462ba4f207537), ULL(0x98c0a9389421d1e7),
+ ULL(0x29346d1a2f4c68fb), ULL(0x4f90a698f44dcc2b), ULL(0x6404687a0f6863bf), ULL(0x02a0a3f8d469c76f),
+ ULL(0x06ede13f79b31186), ULL(0x60492abda2b2b556), ULL(0x4bdde45f59971ac2), ULL(0x2d792fdd8296be12),
+ ULL(0x9c8debff39fb070e), ULL(0xfa29207de2faa3de), ULL(0xd1bdee9f19df0c4a), ULL(0xb719251dc2dea89a),
+ ULL(0x8e48c629a0deda63), ULL(0xe8ec0dab7bdf7eb3), ULL(0xc378c34980fad127), ULL(0xa5dc08cb5bfb75f7),
+ ULL(0x1428cce9e096cceb), ULL(0x728c076b3b97683b), ULL(0x5918c989c0b2c7af), ULL(0x3fbc020b1bb3637f),
+ ULL(0x3bf140ccb669b596), ULL(0x5d558b4e6d681146), ULL(0x76c145ac964dbed2), ULL(0x10658e2e4d4c1a02),
+ ULL(0xa1914a0cf621a31e), ULL(0xc735818e2d2007ce), ULL(0xeca14f6cd605a85a), ULL(0x8a0584ee0d040c8a),
+ ULL(0x654359871a97466c), ULL(0x03e79205c196e2bc), ULL(0x28735ce73ab34d28), ULL(0x4ed79765e1b2e9f8),
+ ULL(0xff2353475adf50e4), ULL(0x998798c581def434), ULL(0xb21356277afb5ba0), ULL(0xd4b79da5a1faff70),
+ ULL(0xd0fadf620c202999), ULL(0xb65e14e0d7218d49), ULL(0x9dcada022c0422dd), ULL(0xfb6e1180f705860d),
+ ULL(0x4a9ad5a24c683f11), ULL(0x2c3e1e2097699bc1), ULL(0x07aad0c26c4c3455), ULL(0x610e1b40b74d9085),
+ ULL(0xf47084ce3f6b9342), ULL(0x92d44f4ce46a3792), ULL(0xb94081ae1f4f9806), ULL(0xdfe44a2cc44e3cd6),
+ ULL(0x6e108e0e7f2385ca), ULL(0x08b4458ca422211a), ULL(0x23208b6e5f078e8e), ULL(0x458440ec84062a5e),
+ ULL(0x41c9022b29dcfcb7), ULL(0x276dc9a9f2dd5867), ULL(0x0cf9074b09f8f7f3), ULL(0x6a5dccc9d2f95323),
+ ULL(0xdba908eb6994ea3f), ULL(0xbd0dc369b2954eef), ULL(0x96990d8b49b0e17b), ULL(0xf03dc60992b145ab),
+ ULL(0x1f7b1b6085220f4d), ULL(0x79dfd0e25e23ab9d), ULL(0x524b1e00a5060409), ULL(0x34efd5827e07a0d9),
+ ULL(0x851b11a0c56a19c5), ULL(0xe3bfda221e6bbd15), ULL(0xc82b14c0e54e1281), ULL(0xae8fdf423e4fb651),
+ ULL(0xaac29d85939560b8), ULL(0xcc6656074894c468), ULL(0xe7f298e5b3b16bfc), ULL(0x8156536768b0cf2c),
+ ULL(0x30a29745d3dd7630), ULL(0x56065cc708dcd2e0), ULL(0x7d929225f3f97d74), ULL(0x1b3659a728f8d9a4),
+ ULL(0x2267ba934af8ab5d), ULL(0x44c3711191f90f8d), ULL(0x6f57bff36adca019), ULL(0x09f37471b1dd04c9),
+ ULL(0xb807b0530ab0bdd5), ULL(0xdea37bd1d1b11905), ULL(0xf537b5332a94b691), ULL(0x93937eb1f1951241),
+ ULL(0x97de3c765c4fc4a8), ULL(0xf17af7f4874e6078), ULL(0xdaee39167c6bcfec), ULL(0xbc4af294a76a6b3c),
+ ULL(0x0dbe36b61c07d220), ULL(0x6b1afd34c70676f0), ULL(0x408e33d63c23d964), ULL(0x262af854e7227db4),
+ ULL(0xc96c253df0b13752), ULL(0xafc8eebf2bb09382), ULL(0x845c205dd0953c16), ULL(0xe2f8ebdf0b9498c6),
+ ULL(0x530c2ffdb0f921da), ULL(0x35a8e47f6bf8850a), ULL(0x1e3c2a9d90dd2a9e), ULL(0x7898e11f4bdc8e4e),
+ ULL(0x7cd5a3d8e60658a7), ULL(0x1a71685a3d07fc77), ULL(0x31e5a6b8c62253e3), ULL(0x57416d3a1d23f733),
+ ULL(0xe6b5a918a64e4e2f), ULL(0x8011629a7d4feaff), ULL(0xab85ac78866a456b), ULL(0xcd2167fa5d6be1bb)
+ }
+};
+
+ui64 ReverseBytes(ui64 v)
+{
+ ui64 r = v;
+ int s = sizeof(v) - 1;
+
+ for (v >>= 8; v; v >>= 8)
+ {
+ r <<= 8;
+ r |= v & 0xff;
+ s--;
+ }
+
+ r <<= 8 * s;
+ return r;
+}
+
+ui64 Crc(const void* buf, size_t buflen, ui64 crcinit)
+{
+ crcinit = ReverseBytes(crcinit);
+
+ const unsigned char * ptrChar = (const unsigned char *) buf;
+
+ while ((reinterpret_cast<size_t>(ptrChar) & 0x7) && buflen) {
+ crcinit = CrcLookup[0][(crcinit ^ *ptrChar++) & 0xff] ^ (crcinit >> 8);
+ --buflen;
+ }
+
+ const ui64* ptr = (const ui64 *) ptrChar;
+
+ for(; buflen >= 8; buflen -= 8) {
+ ui64 val = crcinit ^ *ptr++;
+
+ crcinit =
+ CrcLookup[7][ val & 0xff] ^
+ CrcLookup[6][(val >> 8) & 0xff] ^
+ CrcLookup[5][(val >> 16) & 0xff] ^
+ CrcLookup[4][(val >> 24) & 0xff] ^
+ CrcLookup[3][(val >> 32) & 0xff] ^
+ CrcLookup[2][(val >> 40) & 0xff] ^
+ CrcLookup[1][(val >> 48) & 0xff] ^
+ CrcLookup[0][ val >> 56 ];
+ }
+
+ ptrChar = (const unsigned char *) ptr;
+
+ while (buflen--) {
+ crcinit = CrcLookup[0][(crcinit ^ *ptrChar++) & 0xff] ^ (crcinit >> 8);
+ }
+
+ return ReverseBytes(crcinit);
+}
+
+} // namespace NCrcTable0xE543279765927881
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+// COMPAT(akozhikhov): drop this code after a while
+ui64 CrcImplOld(const void* data, size_t length, ui64 seed)
+{
+#ifdef YT_USE_SSE42
+ static const bool Native = NX86::CachedHaveSSE42() && NX86::CachedHavePCLMUL();
+ if (Native) {
+ return NCrcNative0xE543279765927881::Crc(data, length, seed);
+ }
+#endif
+ return NCrcTable0xE543279765927881::Crc(data, length, seed);
+}
+
+ui64 CrcImpl(const void* data, size_t length, ui64 seed)
+{
+#ifdef YT_USE_SSE42
+ static const bool Native = NX86::CachedHaveSSE42() && NX86::CachedHavePCLMUL();
+ if (Native) {
+ return NIsaCrc64::CrcImplFast(data, length, seed);
+ }
+#endif
+ return NIsaCrc64::CrcImplBase(data, length, seed);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChecksum CombineChecksums(const std::vector<TChecksum>& blockChecksums)
+{
+ TChecksum combined = NullChecksum;
+ for (auto checksum : blockChecksums) {
+ HashCombine(combined, checksum);
+ }
+ return combined;
+}
+
+TChecksum GetChecksumOld(TRef data, TChecksum seed)
+{
+ return CrcImplOld(data.Begin(), data.Size(), seed);
+}
+
+TChecksum GetChecksum(TRef data, TChecksum seed)
+{
+ return CrcImpl(data.Begin(), data.Size(), seed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChecksumInput::TChecksumInput(IInputStream* input)
+ : Input_(input)
+{ }
+
+TChecksum TChecksumInput::GetChecksum() const
+{
+ return Checksum_;
+}
+
+size_t TChecksumInput::DoRead(void* buf, size_t len)
+{
+ size_t res = Input_->Read(buf, len);
+ Checksum_ = CrcImpl(buf, res, Checksum_);
+ return res;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChecksumOutput::TChecksumOutput(IOutputStream* output)
+ : Output_(output)
+{ }
+
+TChecksum TChecksumOutput::GetChecksum() const
+{
+ return Checksum_;
+}
+
+void TChecksumOutput::DoWrite(const void* buf, size_t len)
+{
+ Output_->Write(buf, len);
+ Checksum_ = CrcImpl(buf, len, Checksum_);
+}
+
+void TChecksumOutput::DoFlush()
+{
+ Output_->Flush();
+}
+
+void TChecksumOutput::DoFinish()
+{
+ Output_->Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/checksum.h b/yt/yt/core/misc/checksum.h
new file mode 100644
index 0000000000..ff597f50e1
--- /dev/null
+++ b/yt/yt/core/misc/checksum.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/stream/input.h>
+#include <util/stream/output.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChecksum GetChecksum(TRef data, TChecksum seed = 0);
+// COMPAT(akozhikhov)
+TChecksum GetChecksumOld(TRef data, TChecksum seed = 0);
+
+TChecksum CombineChecksums(const std::vector<TChecksum>& blockChecksums);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChecksumInput
+ : public IInputStream
+{
+public:
+ explicit TChecksumInput(IInputStream* input);
+ TChecksum GetChecksum() const;
+
+protected:
+ size_t DoRead(void* buf, size_t len) override;
+
+private:
+ IInputStream* const Input_;
+ TChecksum Checksum_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChecksumOutput
+ : public IOutputStream
+{
+public:
+ explicit TChecksumOutput(IOutputStream* output);
+ TChecksum GetChecksum() const;
+
+protected:
+ void DoWrite(const void* buf, size_t len) override;
+ void DoFlush() override;
+ void DoFinish() override;
+
+private:
+ IOutputStream* const Output_;
+ TChecksum Checksum_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/checksum_helpers.h b/yt/yt/core/misc/checksum_helpers.h
new file mode 100644
index 0000000000..1f7e0bf229
--- /dev/null
+++ b/yt/yt/core/misc/checksum_helpers.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include "public.h"
+
+#ifdef YT_USE_SSE42
+ #include <tmmintrin.h>
+ #include <nmmintrin.h>
+ #include <wmmintrin.h>
+#endif
+
+namespace NYT::NCrc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef YT_USE_SSE42
+
+inline __m128i _mm_shift_right_si128(__m128i v, ui8 offset)
+{
+ static const ui8 RotateMask[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80
+ };
+
+ return _mm_shuffle_epi8(v, _mm_loadu_si128((__m128i *) (RotateMask + offset)));
+}
+
+inline __m128i _mm_shift_left_si128(__m128i v, ui8 offset)
+{
+ static const ui8 RotateMask[] = {
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
+ };
+
+ return _mm_shuffle_epi8(v, _mm_loadu_si128((__m128i *) (RotateMask + 16 - offset)));
+}
+
+inline __m128i ReverseBytes(__m128i value)
+{
+ return _mm_shuffle_epi8(value,
+ _mm_setr_epi8(0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0));
+}
+
+inline __m128i Fold(__m128i value, __m128i foldFactor)
+{
+ __m128i high = _mm_clmulepi64_si128(value, foldFactor, 0x11);
+ __m128i low = _mm_clmulepi64_si128(value, foldFactor, 0x00);
+ return _mm_xor_si128(high, low);
+}
+
+inline __m128i Fold(__m128i value, __m128i data, __m128i foldFactor)
+{
+ return _mm_xor_si128(data, Fold(value, foldFactor));
+}
+
+YT_ATTRIBUTE_NO_SANITIZE_ADDRESS inline __m128i AlignedPrefixLoad(const void* p, size_t* length)
+{
+ size_t offset = (size_t)p & 15; *length = 16 - offset;
+ return _mm_shift_right_si128(_mm_load_si128((__m128i*)((char*)p - offset)), offset);
+}
+
+YT_ATTRIBUTE_NO_SANITIZE_ADDRESS inline __m128i UnalignedLoad(const void* buf, size_t expectedLength = 16)
+{
+ size_t length;
+ __m128i result = AlignedPrefixLoad(buf, &length);
+
+ if (length < expectedLength) {
+ result = _mm_loadu_si128((__m128i*) buf);
+ }
+
+ return ReverseBytes(result);
+}
+
+inline __m128i AlignedLoad(const void* buf)
+{
+ return ReverseBytes(_mm_load_si128((__m128i*) buf));
+}
+
+inline __m128i FoldTail(__m128i result, __m128i tail, __m128i foldFactor, size_t tailLength)
+{
+ tail = _mm_or_si128(_mm_shift_right_si128(tail, 16 - tailLength), _mm_shift_left_si128(result, tailLength));
+ result = _mm_shift_right_si128(result, 16 - tailLength);
+ return Fold(result, tail, foldFactor);
+}
+
+static ui64 BarretReduction(__m128i chunk, ui64 poly, ui64 mu)
+{
+ __m128i muAndPoly = _mm_set_epi64x(poly, mu);
+ __m128i high = _mm_set_epi64x(_mm_cvtsi128_si64(_mm_srli_si128(chunk, 8)), 0);
+
+ // T1(x) = (R(x) div x^64) clmul mu
+ // mu is 65 bit polynomial
+ __m128i t1 = _mm_xor_si128(_mm_clmulepi64_si128(high, muAndPoly, 0x01), high);
+
+ // T2(x) = (T1(x) div x^64) clmul p(x)
+ // p(x) is 65 bit polynomial, so we have to do xor of high 64 bits after carry-less multiplication
+ // but since the next operation is (R(x) xor T2(x)) mod x^64, xor is unnecessary
+ __m128i t2 = _mm_clmulepi64_si128(t1, muAndPoly, 0x11);
+
+ // return (R(x) xor T2(x)) mod x^64
+ return _mm_cvtsi128_si64(_mm_xor_si128(chunk, t2)); // .m128i_u64[0];
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NCrc
diff --git a/yt/yt/core/misc/collection_helpers-inl.h b/yt/yt/core/misc/collection_helpers-inl.h
new file mode 100644
index 0000000000..e43af8bbb5
--- /dev/null
+++ b/yt/yt/core/misc/collection_helpers-inl.h
@@ -0,0 +1,351 @@
+#ifndef COLLECTION_HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include collection_helpers.h"
+// For the sake of sane code completion.
+#include "collection_helpers.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+template <bool IsSet>
+struct TKeyLess;
+
+template <>
+struct TKeyLess<true>
+{
+ template<typename T>
+ bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs < rhs;
+ }
+};
+
+template <>
+struct TKeyLess<false>
+{
+ template<typename T>
+ bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs.first < rhs.first;
+ }
+};
+
+template <class TItem, class T, class TGetter>
+std::vector<TItem> GetIthsImpl(const T& collection, size_t sizeLimit, const TGetter& getter)
+{
+ std::vector<TItem> result;
+ result.reserve(std::min(collection.size(), sizeLimit));
+ for (const auto& item : collection) {
+ if (result.size() >= sizeLimit)
+ break;
+ result.push_back(getter(item));
+ }
+ return result;
+}
+
+} // namespace
+
+template <class T, class C>
+std::vector<typename T::const_iterator> GetSortedIterators(const T& collection, C comp)
+{
+ using TIterator = typename T::const_iterator;
+ std::vector<TIterator> iterators;
+ iterators.reserve(collection.size());
+ for (auto it = collection.cbegin(); it != collection.cend(); ++it) {
+ iterators.emplace_back(it);
+ }
+
+ std::sort(
+ iterators.begin(),
+ iterators.end(),
+ [&] (auto lhsIt, auto rhtIt) {
+ return comp(*lhsIt, *rhtIt);
+ });
+
+ return iterators;
+}
+
+template <class T>
+std::vector<typename T::const_iterator> GetSortedIterators(const T& collection)
+{
+ using TIsSet = std::is_same<typename T::key_type, typename T::value_type>;
+ return GetSortedIterators(collection, TKeyLess<TIsSet::value>());
+}
+
+template <class T>
+std::vector<typename T::key_type> GetKeys(const T& collection, size_t sizeLimit)
+{
+ return GetIthsImpl<typename T::key_type>(
+ collection,
+ sizeLimit,
+ [] (const auto& item) {
+ return std::get<0u>(item);
+ });
+}
+
+template <class T>
+std::vector<typename T::mapped_type> GetValues(const T& collection, size_t sizeLimit)
+{
+ return GetIthsImpl<typename T::mapped_type>(
+ collection,
+ sizeLimit,
+ [] (const auto& item) {
+ return std::get<1u>(item);
+ });
+}
+
+template <class T>
+std::vector<typename T::value_type> GetItems(const T& collection, size_t sizeLimit)
+{
+ return GetIthsImpl<typename T::value_type>(
+ collection,
+ sizeLimit,
+ [] (const auto& item) {
+ return item;
+ });
+}
+
+template <size_t I, class T>
+std::vector<typename std::tuple_element<I, typename T::value_type>::type> GetIths(const T& collection, size_t sizeLimit)
+{
+ return GetIthsImpl<typename std::tuple_element<I, typename T::value_type>::type>(
+ collection,
+ sizeLimit,
+ [] (const auto& item) {
+ return std::get<I>(item);
+ });
+}
+
+template <class T>
+bool ShrinkHashTable(T&& collection)
+{
+ if (collection.bucket_count() <= 4 * collection.size() || collection.bucket_count() <= 16) {
+ return false;
+ }
+
+ typename std::decay_t<decltype(collection)> collectionCopy(collection.begin(), collection.end());
+ collectionCopy.swap(collection);
+ return true;
+}
+
+template <class TSource, class TTarget>
+void MergeFrom(TTarget* target, const TSource& source)
+{
+ for (const auto& item : source) {
+ target->insert(item);
+ }
+}
+
+template <class TMap, class TKeySet>
+void DropMissingKeys(TMap&& map, const TKeySet& set)
+{
+ for (auto it = map.begin(); it != map.end(); ) {
+ if (!set.contains(it->first)) {
+ map.erase(it++);
+ } else {
+ ++it;
+ }
+ }
+}
+
+template <class TMap, class TKey>
+auto GetIteratorOrCrash(TMap&& map, const TKey& key)
+{
+ auto it = map.find(key);
+ YT_VERIFY(it != map.end());
+ return it;
+}
+
+template <class TMap, class TKey>
+const auto& GetOrCrash(const TMap& map, const TKey& key)
+{
+ return GetIteratorOrCrash(map, key)->second;
+}
+
+template <class TMap, class TKey>
+auto& GetOrCrash(TMap&& map, const TKey& key)
+{
+ return GetIteratorOrCrash(map, key)->second;
+}
+
+template <class TMap, class TKey>
+void EraseOrCrash(TMap&& map, const TKey& key)
+{
+ YT_VERIFY(map.erase(key) > 0);
+}
+
+template <class TContainer, class TArg>
+auto InsertOrCrash(TContainer&& container, TArg&& arg)
+{
+ auto [it, inserted] = container.insert(std::forward<TArg>(arg));
+ YT_VERIFY(inserted);
+ return it;
+}
+
+template <class TContainer, class... TArgs>
+auto EmplaceOrCrash(TContainer&& container, TArgs&&... args)
+{
+ auto [it, emplaced] = container.emplace(std::forward<TArgs>(args)...);
+ YT_VERIFY(emplaced);
+ return it;
+}
+
+template <class T, class... TVariantArgs>
+T& GetOrCrash(std::variant<TVariantArgs...>& variant)
+{
+ auto* item = get_if<T>(&variant);
+ YT_VERIFY(item);
+ return *item;
+}
+
+template <class T, class... TVariantArgs>
+const T& GetOrCrash(const std::variant<TVariantArgs...>& variant)
+{
+ const auto* item = get_if<T>(&variant);
+ YT_VERIFY(item);
+ return *item;
+}
+
+template <class TMap, class TKey>
+typename TMap::mapped_type GetOrDefault(
+ const TMap& map,
+ const TKey& key,
+ const typename TMap::mapped_type& defaultValue)
+{
+ auto it = map.find(key);
+ return it == map.end() ? defaultValue : it->second;
+}
+
+template <class TMap, class TKey, class TCtor>
+auto& GetOrInsert(TMap&& map, const TKey& key, TCtor&& ctor)
+{
+ if constexpr (requires {typename TMap::insert_ctx;}) {
+ typename TMap::insert_ctx context;
+ auto it = map.find(key, context);
+ if (it == map.end()) {
+ it = map.emplace_direct(context, key, ctor()).first;
+ }
+ return it->second;
+ } else {
+ auto it = map.find(key);
+ if (it == map.end()) {
+ it = map.emplace(key, ctor()).first;
+ }
+ return it->second;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// See https://stackoverflow.com/questions/23439221/variadic-template-function-to-concatenate-stdvector-containers.
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Nice syntax to allow in-order expansion of parameter packs.
+struct TDoInOrder
+{
+ template <class T>
+ TDoInOrder(std::initializer_list<T>&&) { }
+};
+
+// const& version.
+template <class TVector>
+void AppendVector(TVector& destination, const TVector& source)
+{
+ destination.insert(destination.end(), source.begin(), source.end());
+}
+
+// && version.
+template <class TVector>
+void AppendVector(TVector& destination, TVector&& source)
+{
+ destination.insert(
+ destination.end(),
+ std::make_move_iterator(source.begin()),
+ std::make_move_iterator(source.end()));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+template <class TVector, class... TArgs>
+TVector ConcatVectors(TVector first, TArgs&&... rest)
+{
+ // We need to put results somewhere; that's why we accept first parameter by value and use it as a resulting vector.
+ // First, calculate total size of the result.
+ std::size_t totalSize = first.size();
+ NDetail::TDoInOrder { totalSize += rest.size() ... };
+ first.reserve(totalSize);
+ // Then, append all remaining arguments to first. Note that depending on rvalue-ness of the argument,
+ // suitable overload of AppendVector will be used.
+ NDetail::TDoInOrder { (NDetail::AppendVector(first, std::forward<TArgs>(rest)), 0)... };
+ // Not quite sure why, but in the original article result is explicitly moved.
+ return std::move(first);
+}
+
+template <class T>
+void SortByFirst(T begin, T end)
+{
+ std::sort(begin, end, [] (const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
+}
+
+template <class T>
+void SortByFirst(T&& collection)
+{
+ SortByFirst(collection.begin(), collection.end());
+}
+
+template <class T>
+std::vector<std::pair<typename T::key_type, typename T::mapped_type>> SortHashMapByKeys(const T& hashMap)
+{
+ std::vector<std::pair<typename T::key_type, typename T::mapped_type>> vector;
+ vector.reserve(hashMap.size());
+ for (const auto& [key, value] : hashMap) {
+ vector.emplace_back(key, value);
+ }
+ SortByFirst(vector);
+ return vector;
+}
+
+template <class T>
+void EnsureVectorSize(std::vector<T>& vector, ssize_t size, const T& defaultValue)
+{
+ if (static_cast<ssize_t>(vector.size()) < size) {
+ vector.resize(size, defaultValue);
+ }
+}
+
+template <class T>
+void EnsureVectorIndex(std::vector<T>& vector, ssize_t index, const T& defaultValue)
+{
+ EnsureVectorSize(vector, index + 1, defaultValue);
+}
+
+template <class T>
+void AssignVectorAt(std::vector<T>& vector, ssize_t index, const T& value, const T& defaultValue)
+{
+ EnsureVectorIndex(vector, index, defaultValue);
+ vector[index] = value;
+}
+
+template <class T>
+void AssignVectorAt(std::vector<T>& vector, ssize_t index, T&& value, const T& defaultValue)
+{
+ EnsureVectorIndex(vector, index, defaultValue);
+ vector[index] = std::move(value);
+}
+
+template <class T>
+const T& VectorAtOr(const std::vector<T>& vector, ssize_t index, const T& defaultValue)
+{
+ return index < static_cast<ssize_t>(vector.size()) ? vector[index] : defaultValue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/collection_helpers.h b/yt/yt/core/misc/collection_helpers.h
new file mode 100644
index 0000000000..20bd861e2c
--- /dev/null
+++ b/yt/yt/core/misc/collection_helpers.h
@@ -0,0 +1,161 @@
+#pragma once
+
+#include "common.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+std::vector<typename T::const_iterator> GetSortedIterators(const T& collection);
+
+template <class T, class C>
+std::vector<typename T::const_iterator> GetSortedIterators(const T& collection, C comp);
+
+template <class T>
+std::vector<typename T::key_type> GetKeys(
+ const T& collection,
+ size_t sizeLimit = std::numeric_limits<size_t>::max());
+
+template <class T>
+std::vector<typename T::mapped_type> GetValues(
+ const T& collection,
+ size_t sizeLimit = std::numeric_limits<size_t>::max());
+
+template <class T>
+std::vector<typename T::value_type> GetItems(
+ const T& collection,
+ size_t sizeLimit = std::numeric_limits<size_t>::max());
+
+template <size_t N, class T>
+std::vector<typename std::tuple_element<N, typename T::value_type>::type> GetIths(
+ const T& collection,
+ size_t sizeLimit = std::numeric_limits<size_t>::max());
+
+template <class T>
+bool ShrinkHashTable(T&& collection);
+
+template <class TSource, class TTarget>
+void MergeFrom(TTarget* target, const TSource& source);
+
+template <class TMap, class TKeySet>
+void DropMissingKeys(TMap&& map, const TKeySet& set);
+
+/*!
+ * This function is supposed to replace a frequent pattern
+ * auto it = map.find(key);
+ * YT_VERIFY(it != map.end());
+ * use it;
+ * with
+ * use GetIteratorOrCrash(map, key);
+ */
+template <class TMap, class TKey>
+auto GetIteratorOrCrash(TMap&& map, const TKey& key);
+
+/*!
+ * This function is supposed to replace a frequent pattern
+ * auto it = map.find(key);
+ * YT_VERIFY(it != map.end());
+ * use it->second;
+ * with
+ * use GetOrCrash(map, key);
+ */
+template <class TMap, class TKey>
+const auto& GetOrCrash(const TMap& map, const TKey& key);
+
+template <class TMap, class TKey>
+auto& GetOrCrash(TMap&& map, const TKey& key);
+
+/*!
+ * This function is supposed to replace a frequent pattern
+ * YT_VERIFY(map.erase(key) > 0);
+ * with
+ * EraseOrCrash(map, key);
+ */
+template <class TMap, class TKey>
+void EraseOrCrash(TMap&& map, const TKey& key);
+
+/*!
+ * This function is supposed to replace a frequent pattern
+ * YT_VERIFY(map.insert(pair).second);
+ * with
+ * InsertOrCrash(map, pair);
+ */
+template <class TContainer, class TArg>
+auto InsertOrCrash(TContainer&& container, TArg&& arg);
+
+/*!
+ * This function is supposed to replace a frequent pattern
+ * YT_VERIFY(map.emplace(key, value).second);
+ * with
+ * EmplaceOrCrash(map, key, value);
+ */
+template <class TContainer, class... TArgs>
+auto EmplaceOrCrash(TContainer&& container, TArgs&&... args);
+
+/*!
+ * This function is supposed to replace std::get<T>(variant)
+ * for those cases when exception should not be thrown.
+ */
+template <class T, class... TVariantArgs>
+T& GetOrCrash(std::variant<TVariantArgs...>& variant);
+
+template <class T, class... TVariantArgs>
+const T& GetOrCrash(const std::variant<TVariantArgs...>& variant);
+
+/*!
+ * Returns the copy of the value in #map if #key is present
+ * of the copy of #defaultValue otherwise.
+ */
+template <class TMap, class TKey>
+typename TMap::mapped_type GetOrDefault(
+ const TMap& map,
+ const TKey& key,
+ const typename TMap::mapped_type& defaultValue = {});
+
+template <class TMap, class TKey, class TCtor>
+auto& GetOrInsert(TMap&& map, const TKey& key, TCtor&& ctor);
+
+template <class TVector, class... TArgs>
+TVector ConcatVectors(TVector first, TArgs&&... rest);
+
+template <class T>
+void SortByFirst(T begin, T end);
+
+template <class T>
+void SortByFirst(T&& collection);
+
+template <class T>
+std::vector<std::pair<typename T::key_type, typename T::mapped_type>> SortHashMapByKeys(const T& hashMap);
+
+// Below follow helpers for representing a map (small unsigned integer) -> T over std::vector<T> using keys as indices.
+
+//! If vector size is less than provided size, resize vector up to provided size.
+template <class T>
+void EnsureVectorSize(std::vector<T>& vector, ssize_t size, const T& defaultValue = T());
+
+//! If vector size is not enough for vector[index] to exist, resize vector up to index + 1.
+template <class T>
+void EnsureVectorIndex(std::vector<T>& vector, ssize_t index, const T& defaultValue = T());
+
+//! If vector size is not enough for vector[size] to exist, resize vector to size + 1.
+//! After that perform assignment vector[size] = value. Const reference version.
+template <class T>
+void AssignVectorAt(std::vector<T>& vector, ssize_t index, const T& value, const T& defaultValue = T());
+
+//! If vector size is not enough for vector[size] to exist, resize vector to size + 1.
+//! After that perform assignment vector[size] = std::move(value). Rvalue reference version.
+template <class T>
+void AssignVectorAt(std::vector<T>& vector, ssize_t index, T&& value, const T& defaultValue = T());
+
+//! If vector size is not enough for vector[size] to exist, return defaultValue, otherwise return vector[size].
+template <class T>
+const T& VectorAtOr(const std::vector<T>& vector, ssize_t index, const T& defaultValue = T());
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define COLLECTION_HELPERS_INL_H_
+#include "collection_helpers-inl.h"
+#undef COLLECTION_HELPERS_INL_H_
diff --git a/yt/yt/core/misc/common.h b/yt/yt/core/misc/common.h
new file mode 100644
index 0000000000..bbc9a9ae02
--- /dev/null
+++ b/yt/yt/core/misc/common.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <library/cpp/yt/misc/port.h>
+#include <library/cpp/yt/misc/hash.h>
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/memory/intrusive_ptr.h>
+#include <library/cpp/yt/memory/weak_ptr.h>
+#include <library/cpp/yt/memory/new.h>
+#include <library/cpp/yt/memory/ref_counted.h>
+
+#include <util/datetime/base.h>
+
+#include <util/generic/hash.h>
+#include <util/generic/hash_set.h>
+#include <util/generic/string.h>
+
+#include <util/system/compiler.h>
+#include <util/system/defaults.h>
+
+#include <list>
+#include <map>
+#include <queue>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/concurrent_cache-inl.h b/yt/yt/core/misc/concurrent_cache-inl.h
new file mode 100644
index 0000000000..f6ca30b6ab
--- /dev/null
+++ b/yt/yt/core/misc/concurrent_cache-inl.h
@@ -0,0 +1,212 @@
+#ifndef CONCURRENT_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include concurrent_cache.h"
+// For the sake of sane code completion.
+#include "concurrent_cache.h"
+#endif
+#undef CONCURRENT_CACHE_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TConcurrentCache<T>::TLookupTable final
+ : public THashTable
+ , public TRefTracked<TLookupTable>
+{
+ static constexpr bool EnableHazard = true;
+
+ const size_t Capacity;
+ std::atomic<size_t> Size = 0;
+ TAtomicPtr<TLookupTable> Next;
+
+ explicit TLookupTable(size_t capacity)
+ : THashTable(capacity)
+ , Capacity(capacity)
+ { }
+
+ bool Insert(TValuePtr item)
+ {
+ auto fingerprint = THash<T>()(item.Get());
+ if (THashTable::Insert(fingerprint, std::move(item))) {
+ ++Size;
+ return true;
+ }
+ return false;
+ }
+};
+
+template <class T>
+TIntrusivePtr<typename TConcurrentCache<T>::TLookupTable>
+TConcurrentCache<T>::RenewTable(const TIntrusivePtr<TLookupTable>& head, size_t capacity)
+{
+ if (head != Head_) {
+ return Head_.Acquire();
+ }
+
+ // Rotate lookup table.
+ auto newHead = New<TLookupTable>(capacity);
+ newHead->Next = head;
+
+ if (Head_.SwapIfCompare(head, newHead)) {
+ static const auto& Logger = LockFreePtrLogger;
+ YT_LOG_DEBUG("Concurrent cache lookup table rotated (LoadFactor: %v)",
+ head->Size.load());
+
+ // Head_ swapped, remove third lookup table.
+ head->Next.Reset();
+ return newHead;
+ } else {
+ return Head_.Acquire();
+ }
+}
+
+template <class T>
+TConcurrentCache<T>::TConcurrentCache(size_t capacity)
+ : Capacity_(capacity)
+ , Head_(New<TLookupTable>(capacity))
+{
+ YT_VERIFY(capacity > 0);
+}
+
+template <class T>
+TConcurrentCache<T>::~TConcurrentCache()
+{
+ auto head = Head_.Acquire();
+
+ static const auto& Logger = LockFreePtrLogger;
+ YT_LOG_DEBUG("Concurrent cache head statistics (ElementCount: %v)",
+ head->Size.load());
+}
+
+template <class T>
+TConcurrentCache<T>::TCachedItemRef::TCachedItemRef(typename THashTable::TItemRef ref, TLookupTable* origin)
+ : TConcurrentCache<T>::THashTable::TItemRef(ref)
+ , Origin(origin)
+{ }
+
+template <class T>
+typename TConcurrentCache<T>::TLookuper& TConcurrentCache<T>::TLookuper::operator= (TLookuper&& other)
+{
+ Parent_ = std::move(other.Parent_);
+ Primary_ = std::move(other.Primary_);
+ Secondary_ = std::move(other.Secondary_);
+
+ return *this;
+}
+
+template <class T>
+TConcurrentCache<T>::TLookuper::TLookuper(
+ TConcurrentCache* parent,
+ TIntrusivePtr<TLookupTable> primary,
+ TIntrusivePtr<TLookupTable> secondary)
+ : Parent_(parent)
+ , Primary_(std::move(primary))
+ , Secondary_(std::move(secondary))
+{ }
+
+template <class T>
+template <class TKey>
+typename TConcurrentCache<T>::TCachedItemRef TConcurrentCache<T>::TLookuper::operator() (const TKey& key)
+{
+ auto fingerprint = THash<T>()(key);
+
+ // Use fixed lookup tables. No need to read head.
+
+ if (auto item = Primary_->FindRef(fingerprint, key)) {
+ return TCachedItemRef(item, Primary_.Get());
+ }
+
+ if (!Secondary_) {
+ return TCachedItemRef();
+ }
+
+ return TCachedItemRef(Secondary_->FindRef(fingerprint, key), Secondary_.Get());
+}
+
+template <class T>
+TConcurrentCache<T>::TLookuper::operator bool ()
+{
+ return Parent_;
+}
+
+template <class T>
+typename TConcurrentCache<T>::TLookupTable* TConcurrentCache<T>::TLookuper::GetPrimary()
+{
+ return Primary_.Get();
+}
+
+template <class T>
+typename TConcurrentCache<T>::TLookupTable* TConcurrentCache<T>::TLookuper::GetSecondary()
+{
+ return Secondary_.Get();
+}
+
+template <class T>
+typename TConcurrentCache<T>::TLookuper TConcurrentCache<T>::GetLookuper()
+{
+ auto primary = Head_.Acquire();
+ auto secondary = primary ? primary->Next.Acquire() : nullptr;
+
+ return TLookuper(this, std::move(primary), std::move(secondary));
+}
+
+template <class T>
+typename TConcurrentCache<T>::TLookuper TConcurrentCache<T>::GetSecondaryLookuper()
+{
+ auto primary = Head_.Acquire();
+ auto secondary = primary ? primary->Next.Acquire() : nullptr;
+
+ return TLookuper(this, secondary, nullptr);
+}
+
+template <class T>
+typename TConcurrentCache<T>::TInserter& TConcurrentCache<T>::TInserter::operator= (TInserter&& other)
+{
+ Parent_ = std::move(other.Parent_);
+ Primary_ = std::move(other.Primary_);
+
+ return *this;
+}
+
+template <class T>
+TConcurrentCache<T>::TInserter::TInserter(
+ TConcurrentCache* parent,
+ TIntrusivePtr<TLookupTable> primary)
+ : Parent_(parent)
+ , Primary_(std::move(primary))
+{ }
+
+template <class T>
+typename TConcurrentCache<T>::TLookupTable* TConcurrentCache<T>::TInserter::GetTable()
+{
+ auto targetCapacity = Parent_->Capacity_.load(std::memory_order::acquire);
+ if (Primary_->Size >= std::min(targetCapacity, Primary_->Capacity)) {
+ Primary_ = Parent_->RenewTable(Primary_, targetCapacity);
+ }
+
+ return Primary_.Get();
+}
+
+template <class T>
+typename TConcurrentCache<T>::TInserter TConcurrentCache<T>::GetInserter()
+{
+ auto primary = Head_.Acquire();
+ return TInserter(this, std::move(primary));
+}
+
+template <class T>
+void TConcurrentCache<T>::SetCapacity(size_t capacity)
+{
+ YT_VERIFY(capacity > 0);
+ Capacity_.store(capacity, std::memory_order::release);
+
+ auto primary = Head_.Acquire();
+ if (primary->Size >= std::min(capacity, primary->Capacity)) {
+ RenewTable(primary, capacity);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/concurrent_cache.h b/yt/yt/core/misc/concurrent_cache.h
new file mode 100644
index 0000000000..158a358eef
--- /dev/null
+++ b/yt/yt/core/misc/concurrent_cache.h
@@ -0,0 +1,106 @@
+#pragma once
+
+#include "public.h"
+#include "atomic_ptr.h"
+#include "lock_free_hash_table.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TConcurrentCache
+{
+private:
+ using THashTable = TLockFreeHashTable<T>;
+
+ struct TLookupTable;
+
+ TIntrusivePtr<TLookupTable> RenewTable(const TIntrusivePtr<TLookupTable>& head, size_t capacity);
+
+public:
+ using TValuePtr = TIntrusivePtr<T>;
+
+ explicit TConcurrentCache(size_t maxElementCount);
+
+ ~TConcurrentCache();
+
+ struct TCachedItemRef
+ : public THashTable::TItemRef
+ {
+ TCachedItemRef() = default;
+
+ TCachedItemRef(typename THashTable::TItemRef ref, TLookupTable* origin);
+
+ TLookupTable* const Origin = nullptr;
+ };
+
+ class TLookuper
+ {
+ public:
+ TLookuper() = default;
+
+ TLookuper(TLookuper&& other) = default;
+
+ TLookuper& operator= (TLookuper&& other);
+
+ TLookuper(
+ TConcurrentCache* parent,
+ TIntrusivePtr<TLookupTable> primary,
+ TIntrusivePtr<TLookupTable> secondary);
+
+ template <class TKey>
+ TCachedItemRef operator() (const TKey& key);
+
+ explicit operator bool ();
+
+ TLookupTable* GetPrimary();
+ TLookupTable* GetSecondary();
+
+ private:
+ TConcurrentCache* Parent_ = nullptr;
+ TIntrusivePtr<TLookupTable> Primary_;
+ TIntrusivePtr<TLookupTable> Secondary_;
+ };
+
+ TLookuper GetLookuper();
+
+ TLookuper GetSecondaryLookuper();
+
+ class TInserter
+ {
+ public:
+ TInserter() = default;
+
+ TInserter(TInserter&& other) = default;
+
+ TInserter& operator= (TInserter&& other);
+
+ TInserter(
+ TConcurrentCache* parent,
+ TIntrusivePtr<TLookupTable> primary);
+
+ TLookupTable* GetTable();
+
+ private:
+ TConcurrentCache* Parent_ = nullptr;
+ TIntrusivePtr<TLookupTable> Primary_;
+ };
+
+ TInserter GetInserter();
+
+ void SetCapacity(size_t capacity);
+
+private:
+ std::atomic<size_t> Capacity_;
+ TAtomicPtr<TLookupTable> Head_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define CONCURRENT_CACHE_INL_H_
+#include "concurrent_cache-inl.h"
+#undef CONCURRENT_CACHE_INL_H_
diff --git a/yt/yt/core/misc/config.cpp b/yt/yt/core/misc/config.cpp
new file mode 100644
index 0000000000..147b8be0ce
--- /dev/null
+++ b/yt/yt/core/misc/config.cpp
@@ -0,0 +1,128 @@
+#include "config.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TLogDigestConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("relative_precision", &TThis::RelativePrecision)
+ .Default(0.01)
+ .GreaterThan(0);
+
+ registrar.Parameter("lower_bound", &TThis::LowerBound)
+ .GreaterThan(0);
+
+ registrar.Parameter("upper_bound", &TThis::UpperBound)
+ .GreaterThan(0);
+
+ registrar.Parameter("default_value", &TThis::DefaultValue)
+ .Default();
+
+ registrar.Postprocessor([] (TLogDigestConfig* config) {
+ // If there are more than 1000 buckets, the implementation of TLogDigest
+ // becomes inefficient since it stores information about at least that many buckets.
+ const int MaxBucketCount = 1000;
+ double bucketCount = log(config->UpperBound / config->LowerBound) / log(1 + config->RelativePrecision);
+ if (bucketCount > MaxBucketCount) {
+ THROW_ERROR_EXCEPTION("Bucket count is too large")
+ << TErrorAttribute("bucket_count", bucketCount)
+ << TErrorAttribute("max_bucket_count", MaxBucketCount);
+ }
+ if (config->DefaultValue && (*config->DefaultValue < config->LowerBound || *config->DefaultValue > config->UpperBound)) {
+ THROW_ERROR_EXCEPTION("Default value should be between lower bound and upper bound")
+ << TErrorAttribute("default_value", *config->DefaultValue)
+ << TErrorAttribute("lower_bound", config->LowerBound)
+ << TErrorAttribute("upper_bound", config->UpperBound);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THistogramDigestConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("absolute_precision", &TThis::AbsolutePrecision)
+ .Default(0.01)
+ .GreaterThan(0);
+
+ registrar.Parameter("lower_bound", &TThis::LowerBound)
+ .Default(0.0);
+
+ registrar.Parameter("upper_bound", &TThis::UpperBound)
+ .Default(1.0);
+
+ registrar.Parameter("default_value", &TThis::DefaultValue)
+ .Default();
+
+ registrar.Postprocessor([] (THistogramDigestConfig* config) {
+ if (config->UpperBound < config->LowerBound) {
+ THROW_ERROR_EXCEPTION("Upper bound should be greater than or equal to lower bound")
+ << TErrorAttribute("lower_bound", config->LowerBound)
+ << TErrorAttribute("upper_bound", config->UpperBound);
+ }
+
+ // If there are more buckets, the implementation of THistogramDigest
+ // becomes inefficient since it stores information about at least that many buckets.
+ const int MaxBucketCount = 10000;
+ double bucketCount = (config->UpperBound - config->LowerBound) / config->AbsolutePrecision;
+ if (bucketCount > MaxBucketCount) {
+ THROW_ERROR_EXCEPTION("Bucket count is too large")
+ << TErrorAttribute("bucket_count", bucketCount)
+ << TErrorAttribute("max_bucket_count", MaxBucketCount);
+ }
+
+ if (config->DefaultValue && (*config->DefaultValue < config->LowerBound || *config->DefaultValue > config->UpperBound)) {
+ THROW_ERROR_EXCEPTION("Default value should be between lower bound and upper bound")
+ << TErrorAttribute("default_value", *config->DefaultValue)
+ << TErrorAttribute("lower_bound", config->LowerBound)
+ << TErrorAttribute("upper_bound", config->UpperBound);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THistoricUsageConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("aggregation_mode", &TThis::AggregationMode)
+ .Default(EHistoricUsageAggregationMode::None);
+
+ registrar.Parameter("ema_alpha", &TThis::EmaAlpha)
+ // TODO(eshcherbin): Adjust.
+ .Default(1.0 / (24.0 * 60.0 * 60.0))
+ .GreaterThanOrEqual(0.0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAdaptiveHedgingManagerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_backup_request_ratio", &TThis::MaxBackupRequestRatio)
+ .GreaterThan(0.)
+ .Optional();
+
+ registrar.Parameter("tick_period", &TThis::TickPeriod)
+ .GreaterThan(TDuration::Zero())
+ .Default(TDuration::Seconds(1));
+
+ registrar.Parameter("hedging_delay_tune_factor", &TThis::HedgingDelayTuneFactor)
+ .GreaterThanOrEqual(1.)
+ .Default(1.05);
+ registrar.Parameter("min_hedging_delay", &TThis::MinHedgingDelay)
+ .Default(TDuration::Zero());
+ registrar.Parameter("max_hedging_delay", &TThis::MaxHedgingDelay)
+ .Default(TDuration::Seconds(10));
+
+ registrar.Postprocessor([] (TAdaptiveHedgingManagerConfig* config) {
+ if (config->MinHedgingDelay > config->MaxHedgingDelay) {
+ THROW_ERROR_EXCEPTION("\"min_hedging_delay\" cannot be greater than \"max_hedging_delay\"")
+ << TErrorAttribute("min_hedging_delay", config->MinHedgingDelay)
+ << TErrorAttribute("max_hedging_delay", config->MaxHedgingDelay);
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/config.h b/yt/yt/core/misc/config.h
new file mode 100644
index 0000000000..7417a41ff8
--- /dev/null
+++ b/yt/yt/core/misc/config.h
@@ -0,0 +1,114 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <cmath>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogDigestConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ // We will round each sample x to the range from [(1 - RelativePrecision)*x, (1 + RelativePrecision)*x].
+ // This parameter affects the memory usage of the digest, it is proportional to
+ // log(UpperBound / LowerBound) / log(1 + RelativePrecision).
+ double RelativePrecision;
+
+ // The bounds of the range operated by the class.
+ double LowerBound;
+ double UpperBound;
+
+ // The value that is returned when there are no samples in the digest.
+ std::optional<double> DefaultValue;
+
+ REGISTER_YSON_STRUCT(TLogDigestConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TLogDigestConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogramDigestConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ // We will round each sample x to a value from [x - AbsolutePrecision / 2, x + AbsolutePrecision / 2].
+ // More precisely, size of each bucket in the histogram will be equal to AbsolutePrecision.
+ // This parameter affects the memory usage of the digest, it is proportional to ((UpperBound - LowerBound) / AbsolutePrecision).
+ double AbsolutePrecision;
+
+ // The bounds of the range operated by the class.
+ double LowerBound;
+ double UpperBound;
+
+ // The value that is returned when there are no samples in the digest.
+ std::optional<double> DefaultValue;
+
+ REGISTER_YSON_STRUCT(THistogramDigestConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THistogramDigestConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EHistoricUsageAggregationMode,
+ ((None) (0))
+ ((ExponentialMovingAverage) (1))
+);
+
+class THistoricUsageConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ EHistoricUsageAggregationMode AggregationMode;
+
+ //! Parameter of exponential moving average (EMA) of the aggregated usage.
+ //! Roughly speaking, it means that current usage ratio is twice as relevant for the
+ //! historic usage as the usage ratio alpha seconds ago.
+ //! EMA for unevenly spaced time series was adapted from here: https://clck.ru/HaGZs
+ double EmaAlpha;
+
+ REGISTER_YSON_STRUCT(THistoricUsageConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THistoricUsageConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAdaptiveHedgingManagerConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Percentage of primary requests that should have a hedging counterpart.
+ //! Null is for disabled hedging.
+ std::optional<double> MaxBackupRequestRatio;
+
+ //! Period for hedging delay tuning and profiling.
+ TDuration TickPeriod;
+
+ //! Each tick hedging delay is tuned according to |MaxBackupRequestRatio| by |HedgingDelayTuneFactor|.
+ double HedgingDelayTuneFactor;
+ TDuration MinHedgingDelay;
+ TDuration MaxHedgingDelay;
+
+ REGISTER_YSON_STRUCT(TAdaptiveHedgingManagerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TAdaptiveHedgingManagerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/copyable_atomic.h b/yt/yt/core/misc/copyable_atomic.h
new file mode 100644
index 0000000000..241287ffd0
--- /dev/null
+++ b/yt/yt/core/misc/copyable_atomic.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TCopyableAtomic
+{
+public:
+ T Load() const
+ {
+ return T_.load();
+ }
+
+ void Store(T v)
+ {
+ T_.store(v);
+ }
+
+ TCopyableAtomic() = default;
+
+ TCopyableAtomic(T v)
+ : T_(v)
+ { }
+
+ TCopyableAtomic(const TCopyableAtomic& other)
+ : T_(other.T_.load())
+ { }
+
+ TCopyableAtomic& operator = (const TCopyableAtomic& other)
+ {
+ T_ = other.T_.load();
+ return *this;
+ }
+
+ TCopyableAtomic(TCopyableAtomic&& other)
+ : T_(other.T_.load())
+ { }
+
+ TCopyableAtomic& operator = (TCopyableAtomic&& other)
+ {
+ T_ = other.T_.load();
+ return *this;
+ }
+
+private:
+ std::atomic<T> T_ = {};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/coro_pipe.cpp b/yt/yt/core/misc/coro_pipe.cpp
new file mode 100644
index 0000000000..3b2c504c3d
--- /dev/null
+++ b/yt/yt/core/misc/coro_pipe.cpp
@@ -0,0 +1,82 @@
+#include "coro_pipe.h"
+
+#include <yt/yt/core/actions/bind.h>
+
+namespace NYT {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCoroStream
+ : public IZeroCopyInput
+{
+public:
+ TCoroStream(TCoroutine<void(TStringBuf)>* coroutine, TStringBuf data)
+ : Coroutine_(coroutine)
+ , PendingData_(data)
+ , Finished_(data.empty())
+ { }
+
+ size_t DoNext(const void** ptr, size_t len) override
+ {
+ if (PendingData_.empty()) {
+ if (Finished_) {
+ *ptr = nullptr;
+ return 0;
+ }
+ std::tie(PendingData_) = Coroutine_->Yield();
+ if (PendingData_.Empty()) {
+ Finished_ = true;
+ *ptr = nullptr;
+ return 0;
+ }
+ }
+ *ptr = PendingData_.Data();
+ len = Min(len, PendingData_.Size());
+ PendingData_.Skip(len);
+ return len;
+ }
+
+ void Complete()
+ {
+ if (!Finished_) {
+ const void* ptr;
+ if (!PendingData_.Empty() || DoNext(&ptr, 1)) {
+ THROW_ERROR_EXCEPTION("Stray data in stream");
+ }
+ }
+ }
+
+private:
+ TCoroutine<void(TStringBuf)>* const Coroutine_;
+ TStringBuf PendingData_;
+ bool Finished_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCoroPipe::TCoroPipe(std::function<void(IZeroCopyInput*)> streamReader)
+ : Coroutine_(BIND(
+ [streamReader=std::move(streamReader)] (TCoroutine& self, TStringBuf data) {
+ TCoroStream stream(&self, data);
+ streamReader(&stream);
+ stream.Complete();
+ }))
+{ }
+
+void TCoroPipe::Feed(TStringBuf data)
+{
+ if (data) {
+ Coroutine_.Run(data);
+ }
+}
+
+void TCoroPipe::Finish()
+{
+ Coroutine_.Run(TStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/coro_pipe.h b/yt/yt/core/misc/coro_pipe.h
new file mode 100644
index 0000000000..fbad5924d6
--- /dev/null
+++ b/yt/yt/core/misc/coro_pipe.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <yt/yt/core/concurrency/coroutine.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// Class launches function processing pull-based stream in coroutine
+// and allows to feed it with data in push-based manner.
+class TCoroPipe
+{
+public:
+ TCoroPipe(std::function<void(IZeroCopyInput*)> streamReader);
+
+ void Feed(TStringBuf data);
+ void Finish();
+
+private:
+ using TCoroutine = NConcurrency::TCoroutine<void(TStringBuf)>;
+ TCoroutine Coroutine_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/crash_handler-inl.h b/yt/yt/core/misc/crash_handler-inl.h
new file mode 100644
index 0000000000..f3e0d463ab
--- /dev/null
+++ b/yt/yt/core/misc/crash_handler-inl.h
@@ -0,0 +1,41 @@
+#ifndef CRASH_HANDLER_INL_H_
+#error "Direct inclusion of this file is not allowed, include crash_handler.h"
+// For the sake of sane code completion.
+#include "crash_handler.h"
+#endif
+
+#include <library/cpp/yt/backtrace/backtrace.h>
+
+#include <algorithm>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+using TStackTrace = TRange<const void*>;
+using TStackTraceBuffer = std::array<const void*, 99>; // 99 is to keep formatting :)
+TStackTrace GetStackTrace(TStackTraceBuffer* buffer);
+
+} // namespace NDetail
+
+template <class TCallback>
+Y_NO_INLINE void DumpStackTrace(TCallback writeCallback, void* startPC)
+{
+ NDetail::TStackTraceBuffer buffer;
+ auto frames = NDetail::GetStackTrace(&buffer);
+ if (frames.empty()) {
+ writeCallback(TStringBuf("<stack trace is not available>"));
+ } else {
+ NDetail::TStackTraceBuffer::const_iterator it;
+ if (startPC && (it = std::find(frames.Begin(), frames.End(), startPC)) != frames.End()) {
+ frames = frames.Slice(it - frames.Begin(), frames.Size());
+ }
+ NBacktrace::SymbolizeBacktrace(frames, writeCallback);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/crash_handler.cpp b/yt/yt/core/misc/crash_handler.cpp
new file mode 100644
index 0000000000..90c1e11ec9
--- /dev/null
+++ b/yt/yt/core/misc/crash_handler.cpp
@@ -0,0 +1,622 @@
+#include "crash_handler.h"
+#include "signal_registry.h"
+
+#include <yt/yt/core/logging/log_manager.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/concurrency/fls.h>
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <yt/yt/library/undumpable/undumpable.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/string/raw_formatter.h>
+
+#include <library/cpp/yt/backtrace/backtrace.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#ifdef _unix_
+#include <library/cpp/yt/backtrace/cursors/libunwind/libunwind_cursor.h>
+#else
+#include <library/cpp/yt/backtrace/cursors/dummy/dummy_cursor.h>
+#endif
+
+#include <util/system/defaults.h>
+
+#include <signal.h>
+#include <time.h>
+
+#include <yt/yt/build/config.h>
+
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+#ifdef HAVE_UCONTEXT_H
+#ifdef _linux_
+# include <ucontext.h>
+#endif
+#endif
+#ifdef HAVE_SYS_UCONTEXT_H
+# include <sys/ucontext.h>
+#endif
+#ifdef _win_
+# include <io.h>
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WriteToStderr(const char* buffer, int length)
+{
+ // Ignore errors.
+#ifdef _win_
+ HandleEintr(::write, 2, buffer, length);
+#else
+ HandleEintr(write, 2, buffer, length);
+#endif
+}
+
+void WriteToStderr(TStringBuf buffer)
+{
+ WriteToStderr(buffer.begin(), buffer.length());
+}
+
+void WriteToStderr(const char* buffer)
+{
+ WriteToStderr(buffer, ::strlen(buffer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+Y_NO_INLINE TStackTrace GetStackTrace(TStackTraceBuffer* buffer)
+{
+#ifdef _unix_
+ NBacktrace::TLibunwindCursor cursor;
+#else
+ NBacktrace::TDummyCursor cursor;
+#endif
+ return NBacktrace::GetBacktrace(
+ &cursor,
+ MakeMutableRange(*buffer),
+ /*framesToSkip*/ 2);
+}
+
+using NYT::WriteToStderr;
+
+#ifdef _unix_
+
+// See http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_04.html
+// for a list of async signal safe functions.
+
+//! Returns the program counter from a signal context, NULL if unknown.
+void* GetPC(void* uc)
+{
+ // TODO(sandello): Merge with code from Bind() internals.
+#if (defined(HAVE_UCONTEXT_H) || defined(HAVE_SYS_UCONTEXT_H)) && defined(PC_FROM_UCONTEXT) && defined(_linux_)
+ if (uc) {
+ const auto* context = reinterpret_cast<ucontext_t*>(uc);
+ return reinterpret_cast<void*>(context->PC_FROM_UCONTEXT);
+ }
+#else
+ Y_UNUSED(uc);
+#endif
+ return nullptr;
+}
+
+using TFormatter = TRawFormatter<1024>;
+
+void WriteToStderr(const TBaseFormatter& formatter)
+{
+ WriteToStderr(formatter.GetData(), formatter.GetBytesWritten());
+}
+
+//! Dumps time information.
+/*!
+ * We do not dump human-readable time information with localtime()
+ * as it is not guaranteed to be async signal safe.
+ */
+void DumpTimeInfo()
+{
+ auto timeSinceEpoch = ::time(nullptr);
+
+ TFormatter formatter;
+ formatter.AppendString("*** Aborted at ");
+ formatter.AppendNumber(timeSinceEpoch);
+ formatter.AppendString(" (Unix time); Try \"date -d @");
+ formatter.AppendNumber(timeSinceEpoch, 10);
+ formatter.AppendString("\" if you are using GNU date ***\n");
+ WriteToStderr(formatter);
+}
+
+using TCodicilStack = std::vector<TString>;
+
+NConcurrency::TFlsSlot<TCodicilStack>& CodicilStackSlot()
+{
+ static NConcurrency::TFlsSlot<TCodicilStack> Slot;
+ return Slot;
+}
+
+//! Dump codicils.
+void DumpCodicils()
+{
+ // NB: Avoid constructing FLS slot to avoid allocations; these may lead to deadlocks if the
+ // program crashes during an allocation itself.
+ if (CodicilStackSlot().IsInitialized() && !CodicilStackSlot()->empty()) {
+ WriteToStderr("*** Begin codicils ***\n");
+ for (const auto& data : *CodicilStackSlot()) {
+ TFormatter formatter;
+ formatter.AppendString(data.c_str());
+ formatter.AppendString("\n");
+ WriteToStderr(formatter);
+ }
+ WriteToStderr("*** End codicils ***\n");
+ }
+}
+
+// We will install the failure signal handler for signals SIGSEGV, SIGILL, SIGFPE, SIGABRT, SIGBUS
+// We could use strsignal() to get signal names, but we do not use it to avoid
+// introducing yet another #ifdef complication.
+const char* GetSignalName(int signo)
+{
+#define XX(name, message) case name: return #name " (" message ")";
+
+ switch (signo) {
+ XX(SIGILL, "Illegal instruction")
+ XX(SIGFPE, "Floating-point exception")
+ XX(SIGSEGV, "Segmentation violation")
+ XX(SIGBUS, "BUS error")
+ XX(SIGABRT, "Abort")
+ XX(SIGTRAP, "Trace trap")
+ XX(SIGCHLD, "Child status has changed")
+#if 0
+ XX(SIGPOLL, "Pollable event occurred")
+#endif
+ default: return nullptr;
+ }
+
+#undef XX
+}
+
+#ifdef _unix_
+
+const char* GetSignalCodeName(int signo, int code)
+{
+#define XX(name, message) case name: return #name " (" message ")";
+
+ switch (signo) {
+ case SIGILL: switch (code) {
+ XX(ILL_ILLOPC, "Illegal opcode.")
+ XX(ILL_ILLOPN, "Illegal operand.")
+ XX(ILL_ILLADR, "Illegal addressing mode.")
+ XX(ILL_ILLTRP, "Illegal trap.")
+ XX(ILL_PRVOPC, "Privileged opcode.")
+ XX(ILL_PRVREG, "Privileged register.")
+ XX(ILL_COPROC, "Coprocessor error.")
+ XX(ILL_BADSTK, "Internal stack error.")
+ default: return nullptr;
+ }
+ case SIGFPE: switch (code) {
+ XX(FPE_INTDIV, "Integer divide by zero.")
+ XX(FPE_INTOVF, "Integer overflow.")
+ XX(FPE_FLTDIV, "Floating point divide by zero.")
+ XX(FPE_FLTOVF, "Floating point overflow.")
+ XX(FPE_FLTUND, "Floating point underflow.")
+ XX(FPE_FLTRES, "Floating point inexact result.")
+ XX(FPE_FLTINV, "Floating point invalid operation.")
+ XX(FPE_FLTSUB, "Subscript out of range.")
+ default: return nullptr;
+ }
+ case SIGSEGV: switch (code) {
+ XX(SEGV_MAPERR, "Address not mapped to object.")
+ XX(SEGV_ACCERR, "Invalid permissions for mapped object.")
+ default: return nullptr;
+ }
+ case SIGBUS: switch (code) {
+ XX(BUS_ADRALN, "Invalid address alignment.")
+ XX(BUS_ADRERR, "Non-existent physical address.")
+ XX(BUS_OBJERR, "Object specific hardware error.")
+#if 0
+ XX(BUS_MCEERR_AR, "Hardware memory error: action required.")
+ XX(BUS_MCEERR_AO, "Hardware memory error: action optional.")
+#endif
+ default: return nullptr;
+ }
+
+ case SIGTRAP: switch (code) {
+ XX(TRAP_BRKPT, "Process breakpoint.")
+ XX(TRAP_TRACE, "Process trace trap.")
+ default: return nullptr;
+ }
+
+ case SIGCHLD: switch (code) {
+ XX(CLD_EXITED, "Child has exited." )
+ XX(CLD_KILLED, "Child was killed.")
+ XX(CLD_DUMPED, "Child terminated abnormally.")
+ XX(CLD_TRAPPED, "Traced child has trapped.")
+ XX(CLD_STOPPED, "Child has stopped.")
+ XX(CLD_CONTINUED, "Stopped child has continued.")
+ default: return nullptr;
+ }
+#if 0
+ case SIGPOLL: switch (code) {
+ XX(POLL_IN, "Data input available.")
+ XX(POLL_OUT, "Output buffers available.")
+ XX(POLL_MSG, "Input message available.")
+ XX(POLL_ERR, "I/O error.")
+ XX(POLL_PRI, "High priority input available.")
+ XX(POLL_HUP, "Device disconnected.")
+ default: return nullptr;
+ }
+#endif
+ default: return nullptr;
+ }
+
+#undef XX
+}
+
+#endif
+
+#ifdef _x86_64_
+
+// From include/asm/traps.h
+
+[[maybe_unused]]
+const char* GetTrapName(int trapno)
+{
+#define XX(name, value, message) case value: return #name " (" message ")";
+
+ switch (trapno) {
+ XX(X86_TRAP_DE, 0, "Divide-by-zero")
+ XX(X86_TRAP_DB, 1, "Debug")
+ XX(X86_TRAP_NMI, 2, "Non-maskable Interrupt")
+ XX(X86_TRAP_BP, 3, "Breakpoint")
+ XX(X86_TRAP_OF, 4, "Overflow")
+ XX(X86_TRAP_BR, 5, "Bound Range Exceeded")
+ XX(X86_TRAP_UD, 6, "Invalid Opcode")
+ XX(X86_TRAP_NM, 7, "Device Not Available")
+ XX(X86_TRAP_DF, 8, "Double Fault")
+ XX(X86_TRAP_OLD_MF, 9, "Coprocessor Segment Overrun")
+ XX(X86_TRAP_TS, 10, "Invalid TSS")
+ XX(X86_TRAP_NP, 11, "Segment Not Present")
+ XX(X86_TRAP_SS, 12, "Stack Segment Fault")
+ XX(X86_TRAP_GP, 13, "General Protection Fault")
+ XX(X86_TRAP_PF, 14, "Page Fault")
+ XX(X86_TRAP_SPURIOUS, 15, "Spurious Interrupt")
+ XX(X86_TRAP_MF, 16, "x87 Floating-Point Exception")
+ XX(X86_TRAP_AC, 17, "Alignment Check")
+ XX(X86_TRAP_MC, 18, "Machine Check")
+ XX(X86_TRAP_XF, 19, "SIMD Floating-Point Exception")
+ XX(X86_TRAP_IRET, 32, "IRET Exception")
+ default: return nullptr;
+ }
+
+#undef XX
+}
+
+[[maybe_unused]]
+void FormatErrorCodeName(TBaseFormatter* formatter, int codeno)
+{
+ /*
+ * Page fault error code bits:
+ *
+ * bit 0 == 0: no page found 1: protection fault
+ * bit 1 == 0: read access 1: write access
+ * bit 2 == 0: kernel-mode access 1: user-mode access
+ * bit 3 == 1: use of reserved bit detected
+ * bit 4 == 1: fault was an instruction fetch
+ * bit 5 == 1: protection keys block access
+ */
+ enum x86_pf_error_code {
+ X86_PF_PROT = 1 << 0,
+ X86_PF_WRITE = 1 << 1,
+ X86_PF_USER = 1 << 2,
+ X86_PF_RSVD = 1 << 3,
+ X86_PF_INSTR = 1 << 4,
+ X86_PF_PK = 1 << 5,
+ };
+
+ formatter->AppendString(codeno & X86_PF_PROT ? "protection fault" : "no page found");
+ formatter->AppendString(codeno & X86_PF_WRITE ? " write" : " read");
+ formatter->AppendString(codeno & X86_PF_USER ? " user-mode" : " kernel-mode");
+ formatter->AppendString( " access");
+
+ if (codeno & X86_PF_RSVD) {
+ formatter->AppendString(", use of reserved bit detected");
+ }
+
+ if (codeno & X86_PF_INSTR) {
+ formatter->AppendString(", fault was an instruction fetch");
+ }
+
+ if (codeno & X86_PF_PK) {
+ formatter->AppendString(", protection keys block access");
+ }
+}
+
+#endif // _x86_64_
+
+//! Dumps information about the signal.
+void DumpSignalInfo(siginfo_t* si)
+{
+ TFormatter formatter;
+
+ formatter.AppendString("*** ");
+ if (const char* name = GetSignalName(si->si_signo)) {
+ formatter.AppendString(name);
+ } else {
+ // Use the signal number if the name is unknown. The signal name
+ // should be known, but just in case.
+ formatter.AppendString("Signal ");
+ formatter.AppendNumber(si->si_signo);
+ }
+
+ formatter.AppendString(" (@0x");
+ formatter.AppendNumber(reinterpret_cast<uintptr_t>(si->si_addr), 16);
+ formatter.AppendString(")");
+ formatter.AppendString(" received by PID ");
+ formatter.AppendNumber(getpid());
+
+ formatter.AppendString(" (FID 0x");
+ formatter.AppendNumber(NConcurrency::GetCurrentFiberId(), 16);
+ formatter.AppendString(" TID 0x");
+ // We assume pthread_t is an integral number or a pointer, rather
+ // than a complex struct. In some environments, pthread_self()
+ // returns an uint64 but in some other environments pthread_self()
+ // returns a pointer. Hence we use C-style cast here, rather than
+ // reinterpret/static_cast, to support both types of environments.
+ formatter.AppendNumber((uintptr_t)pthread_self(), 16);
+ formatter.AppendString(") ");
+ // Only linux has the PID of the signal sender in si_pid.
+#ifdef _unix_
+ formatter.AppendString("from PID ");
+ formatter.AppendNumber(si->si_pid);
+ formatter.AppendString(" ");
+ formatter.AppendString("code ");
+
+ if (const char* codeMessage = GetSignalCodeName(si->si_signo, si->si_code)) {
+ formatter.AppendString(codeMessage);
+ } else {
+ formatter.AppendNumber(si->si_code);
+ }
+
+ formatter.AppendString(" ");
+#endif
+ formatter.AppendString("***\n");
+
+ WriteToStderr(formatter);
+}
+
+void DumpSigcontext(void* uc)
+{
+#if (defined(HAVE_UCONTEXT_H) || defined(HAVE_SYS_UCONTEXT_H)) && defined(PC_FROM_UCONTEXT) && defined(_linux_) && defined(_x86_64_)
+ ucontext_t* context = reinterpret_cast<ucontext_t*>(uc);
+
+ TFormatter formatter;
+
+ formatter.AppendString("\nERR ");
+ FormatErrorCodeName(&formatter, context->uc_mcontext.gregs[REG_ERR]);
+
+ formatter.AppendString("\nTRAPNO ");
+ if (const char* trapName = GetTrapName(context->uc_mcontext.gregs[REG_TRAPNO])) {
+ formatter.AppendString(trapName);
+ } else {
+ formatter.AppendString("0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_TRAPNO], 16);
+ }
+
+ formatter.AppendString("\nR8 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R8], 16);
+ formatter.AppendString("\nR9 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R9], 16);
+ formatter.AppendString("\nR10 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R10], 16);
+ formatter.AppendString("\nR11 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R11], 16);
+ formatter.AppendString("\nR12 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R12], 16);
+ formatter.AppendString("\nR13 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R13], 16);
+ formatter.AppendString("\nR14 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R14], 16);
+ formatter.AppendString("\nR15 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_R15], 16);
+ formatter.AppendString("\nRDI 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RDI], 16);
+ formatter.AppendString("\nRSI 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RSI], 16);
+ formatter.AppendString("\nRBP 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RBP], 16);
+ formatter.AppendString("\nRBX 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RBX], 16);
+ formatter.AppendString("\nRDX 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RDX], 16);
+ formatter.AppendString("\nRAX 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RAX], 16);
+ formatter.AppendString("\nRCX 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RCX], 16);
+ formatter.AppendString("\nRSP 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RSP], 16);
+ formatter.AppendString("\nRIP 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_RIP], 16);
+ formatter.AppendString("\nEFL 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_EFL], 16);
+ formatter.AppendString("\nCSGSFS 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_CSGSFS], 16);
+ formatter.AppendString("\nOLDMASK 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_OLDMASK], 16);
+ formatter.AppendString("\nCR2 0x");
+ formatter.AppendNumber(context->uc_mcontext.gregs[REG_CR2], 16);
+ formatter.AppendChar('\n');
+
+ WriteToStderr(formatter);
+#else
+ Y_UNUSED(uc);
+#endif
+}
+
+void CrashTimeoutHandler(int /*signal*/)
+{
+ WriteToStderr("*** Process hung during crash ***\n");
+ _exit(1);
+}
+
+void DumpUndumpableBlocksInfo()
+{
+ auto cutInfo = CutUndumpableRegionsFromCoredump();
+
+ {
+ TFormatter formatter;
+ formatter.AppendString("*** Marked memory regions of total size ");
+ formatter.AppendNumber(cutInfo.MarkedSize / 1_MB);
+ formatter.AppendString(" MB as undumpable ***\n");
+ WriteToStderr(formatter);
+ }
+
+ for (const auto& record : cutInfo.FailedToMarkMemory) {
+ if (record.ErrorCode == 0) {
+ break;
+ }
+
+ TFormatter formatter;
+ formatter.AppendString("*** Failed to mark ");
+ formatter.AppendNumber(record.Size / 1_MB);
+ formatter.AppendString(" MB with error code ");
+ formatter.AppendNumber(record.ErrorCode);
+ formatter.AppendString(" ***\n");
+ WriteToStderr(formatter);
+ }
+}
+
+#endif
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef _unix_
+
+// Dumps signal, stack frame information and codicils.
+void CrashSignalHandler(int /*signal*/, siginfo_t* si, void* uc)
+{
+ // All code here _MUST_ be async signal safe unless specified otherwise.
+
+ // When did the crash happen?
+ NDetail::DumpTimeInfo();
+
+ // Dump codicils.
+ NDetail::DumpCodicils();
+
+ // Where did the crash happen?
+ {
+ std::array<const void*, 1> frames{NDetail::GetPC(uc)};
+ NBacktrace::SymbolizeBacktrace(
+ MakeRange(frames),
+ [] (TStringBuf info) {
+ info.SkipPrefix(" 1. ");
+ WriteToStderr(info);
+ });
+ }
+
+ NDetail::DumpSignalInfo(si);
+
+ NDetail::DumpSigcontext(uc);
+
+ // The easiest way to choose proper overload...
+ DumpStackTrace([] (TStringBuf str) { WriteToStderr(str); }, NDetail::GetPC(uc));
+
+ NDetail::DumpUndumpableBlocksInfo();
+
+ WriteToStderr("*** Waiting for logger to shut down ***\n");
+
+ // Actually, it is not okay to hang.
+ ::signal(SIGALRM, NDetail::CrashTimeoutHandler);
+ ::alarm(5);
+
+ NLogging::TLogManager::Get()->Shutdown();
+
+ WriteToStderr("*** Terminating ***\n");
+}
+
+#else
+
+void CrashSignalHandler(int /*signal*/)
+{ }
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PushCodicil(const TString& data)
+{
+#ifdef _unix_
+ NDetail::CodicilStackSlot()->push_back(data);
+#else
+ Y_UNUSED(data);
+#endif
+}
+
+void PopCodicil()
+{
+#ifdef _unix_
+ YT_VERIFY(!NDetail::CodicilStackSlot()->empty());
+ NDetail::CodicilStackSlot()->pop_back();
+#endif
+}
+
+std::vector<TString> GetCodicils()
+{
+#ifdef _unix_
+ return *NDetail::CodicilStackSlot();
+#else
+ return {};
+#endif
+}
+
+TCodicilGuard::TCodicilGuard()
+ : Active_(false)
+{ }
+
+TCodicilGuard::TCodicilGuard(const TString& data)
+ : Active_(true)
+{
+ PushCodicil(data);
+}
+
+TCodicilGuard::~TCodicilGuard()
+{
+ Release();
+}
+
+TCodicilGuard::TCodicilGuard(TCodicilGuard&& other)
+ : Active_(other.Active_)
+{
+ other.Active_ = false;
+}
+
+TCodicilGuard& TCodicilGuard::operator=(TCodicilGuard&& other)
+{
+ if (this != &other) {
+ Release();
+ Active_ = other.Active_;
+ other.Active_ = false;
+ }
+ return *this;
+}
+
+void TCodicilGuard::Release()
+{
+ if (Active_) {
+ PopCodicil();
+ Active_ = false;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/crash_handler.h b/yt/yt/core/misc/crash_handler.h
new file mode 100644
index 0000000000..94f903bd30
--- /dev/null
+++ b/yt/yt/core/misc/crash_handler.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+#ifdef _unix_
+#include <signal.h>
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Writes the given buffer with the length to the standard error.
+void WriteToStderr(const char* buffer, int length);
+//! Writes the given zero-terminated buffer to the standard error.
+void WriteToStderr(const char* buffer);
+//! Same for TStringBuf.
+void WriteToStderr(TStringBuf buffer);
+
+#ifdef _unix_
+// Dumps signal, stack frame information and codicils.
+void CrashSignalHandler(int signal, siginfo_t* si, void* uc);
+#else
+void CrashSignalHandler(int signal);
+#endif
+
+template <class TCallback>
+void DumpStackTrace(TCallback flushCallback, void* startPC = nullptr);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// "Codicils" are short human- and machine-readable strings organized into a per-fiber stack.
+// When the crash handler is invoked, it dumps (alongside with the other
+// useful stuff like backtrace) the content of the latter stack.
+
+//! Installs a new codicil into the stack.
+void PushCodicil(const TString& data);
+
+//! Removes the top codicils from the stack.
+void PopCodicil();
+
+//! Returns the list of the currently installed codicils.
+std::vector<TString> GetCodicils();
+
+//! Invokes #PushCodicil in ctor and #PopCodicil in dtor.
+class TCodicilGuard
+{
+public:
+ TCodicilGuard();
+ explicit TCodicilGuard(const TString& data);
+ ~TCodicilGuard();
+
+ TCodicilGuard(const TCodicilGuard& other) = delete;
+ TCodicilGuard(TCodicilGuard&& other);
+
+ TCodicilGuard& operator=(const TCodicilGuard& other) = delete;
+ TCodicilGuard& operator=(TCodicilGuard&& other);
+
+private:
+ bool Active_;
+
+ void Release();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define CRASH_HANDLER_INL_H_
+#include "crash_handler-inl.h"
+#undef CRASH_HANDLER_INL_H_
diff --git a/yt/yt/core/misc/default_map-inl.h b/yt/yt/core/misc/default_map-inl.h
new file mode 100644
index 0000000000..e4eb0fd496
--- /dev/null
+++ b/yt/yt/core/misc/default_map-inl.h
@@ -0,0 +1,28 @@
+#ifndef DEFAULT_MAP_INL_H_
+#error "Direct inclusion of this file is not allowed, include default_map.h"
+// For the sake of sane code completion.
+#include "default_map.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TUnderlying>
+TDefaultMap<TUnderlying>::TDefaultMap(mapped_type defaultValue)
+ : DefaultValue_(std::move(defaultValue))
+{ }
+
+template <class TUnderlying>
+template <class K>
+typename TDefaultMap<TUnderlying>::mapped_type& TDefaultMap<TUnderlying>::operator[](const K& key)
+{
+ if (auto it = TUnderlying::find(key); it != TUnderlying::end()) {
+ return it->second;
+ }
+ return TUnderlying::insert({key, DefaultValue_}).first->second;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/default_map.h b/yt/yt/core/misc/default_map.h
new file mode 100644
index 0000000000..dec954c09b
--- /dev/null
+++ b/yt/yt/core/misc/default_map.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <utility>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TUnderlying>
+class TDefaultMap
+ : public TUnderlying
+{
+public:
+ using mapped_type = typename TUnderlying::mapped_type;
+
+ explicit TDefaultMap(mapped_type defaultValue);
+
+ template <class K>
+ mapped_type& operator[](const K& key);
+
+private:
+ mapped_type DefaultValue_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define DEFAULT_MAP_INL_H_
+#include "default_map-inl.h"
+#undef DEFAULT_MAP_INL_H_
diff --git a/yt/yt/core/misc/digest.cpp b/yt/yt/core/misc/digest.cpp
new file mode 100644
index 0000000000..0ecdb4a37e
--- /dev/null
+++ b/yt/yt/core/misc/digest.cpp
@@ -0,0 +1,173 @@
+#include "digest.h"
+#include "config.h"
+
+#include <yt/yt/core/misc/phoenix.h>
+
+namespace NYT {
+
+using namespace NPhoenix;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogDigest
+ : public IPersistentDigest
+ , public NPhoenix::TFactoryTag<NPhoenix::TSimpleFactory>
+{
+public:
+ TLogDigest(TLogDigestConfigPtr config)
+ : Step_(1 + config->RelativePrecision)
+ , LogStep_(log(Step_))
+ , LowerBound_(config->LowerBound)
+ , UpperBound_(config->UpperBound)
+ , DefaultValue_(config->DefaultValue ? *config->DefaultValue : config->LowerBound)
+ , BucketCount_(std::max(1, static_cast<int>(ceil(log(UpperBound_ / LowerBound_) / LogStep_))))
+ , Buckets_(BucketCount_)
+ { }
+
+ TLogDigest() = default;
+
+ void AddSample(double value) override
+ {
+ double bucketId = log(value / LowerBound_) / LogStep_;
+ if (std::isnan(bucketId) || bucketId < std::numeric_limits<i32>::min() || bucketId > std::numeric_limits<i32>::max()) {
+ // Discard all incorrect values (those that are non-positive, too small or too large).
+ return;
+ }
+ ++Buckets_[std::clamp(static_cast<int>(bucketId), 0, BucketCount_ - 1)];
+ ++SampleCount_;
+ }
+
+ double GetQuantile(double alpha) const override
+ {
+ if (SampleCount_ == 0) {
+ return DefaultValue_;
+ }
+ double value = LowerBound_;
+ i64 sum = 0;
+ for (int index = 0; index < BucketCount_; ++index) {
+ if (sum >= alpha * SampleCount_) {
+ return value;
+ }
+ sum += Buckets_[index];
+ value *= Step_;
+ }
+ return UpperBound_;
+ }
+
+ void Reset() override
+ {
+ SampleCount_ = 0;
+ std::fill(Buckets_.begin(), Buckets_.end(), 0);
+ }
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ Persist(context, Step_);
+ Persist(context, LogStep_);
+ Persist(context, LowerBound_);
+ Persist(context, UpperBound_);
+ Persist(context, DefaultValue_);
+ Persist(context, BucketCount_);
+ Persist(context, SampleCount_);
+ Persist(context, Buckets_);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(TLogDigest, 0x42424243);
+
+private:
+ double Step_;
+ double LogStep_;
+
+ double LowerBound_;
+ double UpperBound_;
+ double DefaultValue_;
+
+ int BucketCount_;
+
+ i64 SampleCount_ = 0;
+
+ std::vector<i64> Buckets_;
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(TLogDigest);
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPersistentDigestPtr CreateLogDigest(TLogDigestConfigPtr config)
+{
+ return New<TLogDigest>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogramDigest
+ : public IDigest
+{
+public:
+ explicit THistogramDigest(THistogramDigestConfigPtr config)
+ : Config_(std::move(config))
+ , Step_(Config_->AbsolutePrecision)
+ , BucketCount_(static_cast<int>(std::round((Config_->UpperBound - Config_->LowerBound) / Step_)) + 1)
+ , Buckets_(BucketCount_)
+ { }
+
+ void AddSample(double value) override
+ {
+ if (!std::isfinite(value)) {
+ return;
+ }
+
+ value = std::clamp(value, Config_->LowerBound, Config_->UpperBound);
+ int bucketId = static_cast<int>(std::round((value - Config_->LowerBound) / Step_));
+ YT_ASSERT(bucketId < BucketCount_);
+
+ // Note that due to round, i-th bucket corresponds to range [LowerBound + i*Step - Step/2; LowerBound + i*Step + Step/2).
+ ++Buckets_[bucketId];
+ ++SampleCount_;
+ }
+
+ double GetQuantile(double alpha) const override
+ {
+ if (SampleCount_ == 0) {
+ return Config_->DefaultValue.value_or(Config_->LowerBound);
+ }
+
+ double value = Config_->LowerBound;
+ i64 sum = 0;
+ for (int index = 0; index < BucketCount_; ++index) {
+ sum += Buckets_[index];
+ if (sum >= alpha * SampleCount_) {
+ return value;
+ }
+ value += Step_;
+ }
+
+ return Config_->UpperBound;
+ }
+
+ void Reset() override
+ {
+ SampleCount_ = 0;
+ std::fill(Buckets_.begin(), Buckets_.end(), 0);
+ }
+
+private:
+ THistogramDigestConfigPtr Config_;
+ const double Step_;
+ const int BucketCount_;
+
+ i64 SampleCount_ = 0;
+ std::vector<i64> Buckets_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDigestPtr CreateHistogramDigest(THistogramDigestConfigPtr config)
+{
+ return New<THistogramDigest>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/digest.h b/yt/yt/core/misc/digest.h
new file mode 100644
index 0000000000..afa8054284
--- /dev/null
+++ b/yt/yt/core/misc/digest.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/phoenix.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This class holds a compact representation of a set of samples.
+//! #IDigest::GetQuantile|(alpha)| returns a lower bound |X| such that the number
+//! of samples less than |X| is no less than |alpha|.
+// TODO(max42): add methods GetCDF(X) -> alpha (inverse to GetQuantile).
+// TODO(max42): add support for serialization/deserialization.
+// TODO(max42): implement Q-Digest (https://github.com/addthis/stream-lib/blob/master/src/main/java/com/clearspring/analytics/stream/quantile/QDigest.java)
+// and T-Digest (https://github.com/tdunning/t-digest) algorithms and compare them with TLogDigest.
+struct IDigest
+ : public TRefCounted
+{
+ virtual void AddSample(double value) = 0;
+
+ virtual double GetQuantile(double alpha) const = 0;
+
+ virtual void Reset() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IDigest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPersistentDigest
+ : public IDigest
+ , public NPhoenix::IPersistent
+{
+ void Persist(const NPhoenix::TPersistenceContext& context) override = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPersistentDigest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPersistentDigestPtr CreateLogDigest(TLogDigestConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDigestPtr CreateHistogramDigest(THistogramDigestConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/dnf.cpp b/yt/yt/core/misc/dnf.cpp
new file mode 100644
index 0000000000..a51fdc34bc
--- /dev/null
+++ b/yt/yt/core/misc/dnf.cpp
@@ -0,0 +1,212 @@
+#include "dnf.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/node.h>
+
+#include <util/generic/hash.h>
+
+namespace NYT {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TConjunctiveClause::TConjunctiveClause(const std::vector<TString>& include, const std::vector<TString>& exclude)
+ : Include_(include)
+ , Exclude_(exclude)
+{
+ std::sort(Include_.begin(), Include_.end());
+ std::sort(Exclude_.begin(), Exclude_.end());
+ // Exception can be thrown here.
+ Validate();
+}
+
+void TConjunctiveClause::Validate() const
+{
+ for (const auto& includeItem : Include_) {
+ if (std::binary_search(Exclude_.begin(), Exclude_.end(), includeItem)) {
+ THROW_ERROR_EXCEPTION("Include and exclude sets must be disjoint, but item %Qv is present in both",
+ includeItem);
+ }
+ }
+}
+
+template <class TContainer>
+bool TConjunctiveClause::IsSatisfiedByImpl(const TContainer& value) const
+{
+ int includeCount = 0;
+ for (const auto& item : value) {
+ if (std::binary_search(Exclude_.begin(), Exclude_.end(), item)) {
+ return false;
+ }
+ if (std::binary_search(Include_.begin(), Include_.end(), item)) {
+ ++includeCount;
+ }
+ }
+ return includeCount == std::ssize(Include_);
+}
+
+bool TConjunctiveClause::IsSatisfiedBy(const std::vector<TString>& value) const
+{
+ return IsSatisfiedByImpl(value);
+}
+
+bool TConjunctiveClause::IsSatisfiedBy(const THashSet<TString>& value) const
+{
+ return IsSatisfiedByImpl(value);
+}
+
+size_t TConjunctiveClause::GetHash() const
+{
+ const size_t multiplier = 1000003;
+
+ auto hashOfSet = [] (const std::vector<TString>& container) {
+ const size_t multiplier = 67;
+
+ size_t result = 0;
+ for (const auto& str : container) {
+ result = result * multiplier + ComputeHash(str);
+ }
+ return result;
+ };
+
+ return multiplier * hashOfSet(Include_) + hashOfSet(Exclude_);
+}
+
+void Serialize(const TConjunctiveClause& clause, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("include").Value(clause.Include())
+ .Item("exclude").Value(clause.Exclude())
+ .EndMap();
+}
+
+void Deserialize(TConjunctiveClause& clause, INodePtr node)
+{
+ if (node->GetType() == ENodeType::String) {
+ clause.Include() = std::vector<TString>({node->AsString()->GetValue()});
+ } else if (node->GetType() == ENodeType::Map) {
+ auto mapNode = node->AsMap();
+ auto includeNode = mapNode->FindChild("include");
+ if (includeNode) {
+ if (includeNode->GetType() != ENodeType::List) {
+ THROW_ERROR_EXCEPTION("Conjunction include item must be \"list\"");
+ }
+ clause.Include() = ConvertTo<std::vector<TString>>(includeNode);
+ }
+
+ auto excludeNode = mapNode->FindChild("exclude");
+ if (excludeNode) {
+ if (excludeNode->GetType() != ENodeType::List) {
+ THROW_ERROR_EXCEPTION("Conjunction exclude item must be \"list\"");
+ }
+ clause.Exclude() = ConvertTo<std::vector<TString>>(excludeNode);
+ }
+ } else {
+ THROW_ERROR_EXCEPTION("Conjunction clause can only be parsed from \"string\" or \"map\"");
+ }
+}
+
+bool operator==(const TConjunctiveClause& lhs, const TConjunctiveClause& rhs)
+{
+ return lhs.Include() == rhs.Include() &&
+ lhs.Exclude() == rhs.Exclude();
+}
+
+bool operator<(const TConjunctiveClause& lhs, const TConjunctiveClause& rhs)
+{
+ if (lhs.Include() != rhs.Include()) {
+ return lhs.Include() < rhs.Include();
+ }
+ return lhs.Exclude() < rhs.Exclude();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDnfFormula::TDnfFormula(const std::vector<TConjunctiveClause>& clauses)
+ : Clauses_(clauses)
+{
+ std::sort(Clauses_.begin(), Clauses_.end());
+}
+
+template <class TContainer>
+bool TDnfFormula::IsSatisfiedByImpl(const TContainer& value) const
+{
+ for (const auto& clause : Clauses_) {
+ if (clause.IsSatisfiedBy(value)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TDnfFormula::IsSatisfiedBy(const std::vector<TString>& value) const
+{
+ return IsSatisfiedByImpl(value);
+}
+
+bool TDnfFormula::IsSatisfiedBy(const THashSet<TString>& value) const
+{
+ return IsSatisfiedByImpl(value);
+}
+
+size_t TDnfFormula::GetHash() const
+{
+ const size_t multiplier = 424243;
+
+ size_t result = 0;
+ for (const auto& clause : Clauses_) {
+ result = result * multiplier + clause.GetHash();
+ }
+ return result;
+}
+
+bool operator<(const TDnfFormula& lhs, const TDnfFormula& rhs)
+{
+ return lhs.Clauses() < rhs.Clauses();
+}
+
+bool operator==(const TDnfFormula& lhs, const TDnfFormula& rhs)
+{
+ return lhs.Clauses() == rhs.Clauses();
+}
+
+void Serialize(const TDnfFormula& dnf, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .DoListFor(dnf.Clauses(), [=] (TFluentList fluent, const TConjunctiveClause& clause) {
+ fluent
+ .Item().Value(clause);
+ });
+}
+
+void Deserialize(TDnfFormula& dnf, NYTree::INodePtr node)
+{
+ auto clauses = ConvertTo<std::vector<TConjunctiveClause>>(node);
+ dnf = TDnfFormula(clauses);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t THash<NYT::TConjunctiveClause>::operator()(const NYT::TConjunctiveClause& clause) const
+{
+ return clause.GetHash();
+}
+
+size_t THash<NYT::TDnfFormula>::operator()(const NYT::TDnfFormula& dnf) const
+{
+ return dnf.GetHash();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/yt/yt/core/misc/dnf.h b/yt/yt/core/misc/dnf.h
new file mode 100644
index 0000000000..c54f7d1c33
--- /dev/null
+++ b/yt/yt/core/misc/dnf.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "property.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConjunctiveClause
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TString>, Include);
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TString>, Exclude);
+
+public:
+ TConjunctiveClause() = default;
+ TConjunctiveClause(const std::vector<TString>& include, const std::vector<TString>& exclude);
+
+ bool IsSatisfiedBy(const std::vector<TString>& value) const;
+ bool IsSatisfiedBy(const THashSet<TString>& value) const;
+
+ size_t GetHash() const;
+
+private:
+ void Validate() const;
+
+ template<class TContainer>
+ bool IsSatisfiedByImpl(const TContainer& value) const;
+};
+
+bool operator<(const TConjunctiveClause& lhs, const TConjunctiveClause& rhs);
+bool operator==(const TConjunctiveClause& lhs, const TConjunctiveClause& rhs);
+
+void Serialize(const TConjunctiveClause& rule, NYson::IYsonConsumer* consumer);
+void Deserialize(TConjunctiveClause& rule, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDnfFormula
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TConjunctiveClause>, Clauses);
+
+public:
+ explicit TDnfFormula(const std::vector<TConjunctiveClause>& clauses = {});
+
+ bool IsSatisfiedBy(const std::vector<TString>& value) const;
+ bool IsSatisfiedBy(const THashSet<TString>& value) const;
+
+ size_t GetHash() const;
+
+private:
+ template<class TContainer>
+ bool IsSatisfiedByImpl(const TContainer& value) const;
+};
+
+bool operator<(const TDnfFormula& lhs, const TDnfFormula& rhs);
+bool operator==(const TDnfFormula& lhs, const TDnfFormula& rhs);
+
+void Serialize(const TDnfFormula& rule, NYson::IYsonConsumer* consumer);
+void Deserialize(TDnfFormula& rule, NYTree::INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+struct THash<NYT::TConjunctiveClause>
+{
+ size_t operator()(const NYT::TConjunctiveClause& clause) const;
+};
+
+template <>
+struct THash<NYT::TDnfFormula>
+{
+ size_t operator()(const NYT::TDnfFormula& dnf) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/misc/ema_counter.cpp b/yt/yt/core/misc/ema_counter.cpp
new file mode 100644
index 0000000000..aba58bf508
--- /dev/null
+++ b/yt/yt/core/misc/ema_counter.cpp
@@ -0,0 +1,96 @@
+#include "ema_counter.h"
+
+#include <cmath>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEmaCounter::TEmaCounter(TWindowDurations windowDurations)
+ : WindowDurations(std::move(windowDurations))
+ , WindowRates(WindowDurations.size())
+{ }
+
+void TEmaCounter::Update(i64 newCount, TInstant newTimestamp)
+{
+ if (!LastTimestamp) {
+ // Just set current value, we do not know enough information to deal with rates.
+ Count = newCount;
+ LastTimestamp = newTimestamp;
+ StartTimestamp = newTimestamp;
+ return;
+ }
+
+ if (newTimestamp <= *LastTimestamp) {
+ // Ignore obsolete update.
+ return;
+ }
+
+ auto timeDelta = (newTimestamp - *LastTimestamp).SecondsFloat();
+ i64 countDelta = std::max(Count, newCount) - Count;
+ auto newRate = countDelta / timeDelta;
+
+ Count = newCount;
+ ImmediateRate = newRate;
+ LastTimestamp = newTimestamp;
+
+ for (int windowIndex = 0; windowIndex < std::ssize(WindowDurations); ++windowIndex) {
+ auto exp = std::exp(-timeDelta / (WindowDurations[windowIndex].SecondsFloat() / 2.0));
+ auto& rate = WindowRates[windowIndex];
+ rate = newRate * (1 - exp) + rate * exp;
+ }
+}
+
+std::optional<double> TEmaCounter::GetRate(int windowIndex, TInstant currentTimestamp) const
+{
+ if (!StartTimestamp) {
+ return {};
+ }
+
+ if (*StartTimestamp + WindowDurations[windowIndex] > currentTimestamp) {
+ return {};
+ }
+
+ return WindowRates[windowIndex];
+}
+
+TEmaCounter operator+(const TEmaCounter& lhs, const TEmaCounter& rhs)
+{
+ TEmaCounter result = lhs;
+ result += rhs;
+ return result;
+}
+
+TEmaCounter& operator+=(TEmaCounter& lhs, const TEmaCounter& rhs)
+{
+ YT_VERIFY(lhs.WindowDurations == rhs.WindowDurations);
+ lhs.LastTimestamp = std::max(lhs.LastTimestamp, rhs.LastTimestamp);
+ lhs.StartTimestamp = std::max(lhs.StartTimestamp, rhs.StartTimestamp);
+ lhs.Count += rhs.Count;
+ lhs.ImmediateRate += rhs.ImmediateRate;
+ for (int windowIndex = 0; windowIndex < std::ssize(lhs.WindowDurations); ++windowIndex) {
+ lhs.WindowRates[windowIndex] += rhs.WindowRates[windowIndex];
+ }
+ return lhs;
+}
+
+TEmaCounter& operator*=(TEmaCounter& lhs, double coefficient)
+{
+ lhs.Count *= coefficient;
+ lhs.ImmediateRate *= coefficient;
+ for (auto& rate : lhs.WindowRates) {
+ rate *= coefficient;
+ }
+ return lhs;
+}
+
+TEmaCounter operator*(const TEmaCounter& lhs, double coefficient)
+{
+ TEmaCounter result = lhs;
+ result *= coefficient;
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ema_counter.h b/yt/yt/core/misc/ema_counter.h
new file mode 100644
index 0000000000..77e86e49bd
--- /dev/null
+++ b/yt/yt/core/misc/ema_counter.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A helper structure for maintaining a monotonic counter and
+//! estimating its average rate over a set of configured time windows
+//! using EMA (exponential moving average) technique.
+struct TEmaCounter
+{
+ //! Current value of the counter.
+ i64 Count = 0;
+ //! Last update time.
+ std::optional<TInstant> LastTimestamp;
+ //! First update time.
+ std::optional<TInstant> StartTimestamp;
+ //! Rate (measured in units per second) calculated based on the last update,
+ //! i.e. #Count delta divided by the time delta measured in seconds
+ //! according to the last update.
+ double ImmediateRate = 0.0;
+
+ //! A typical number of configured time windows.
+ static constexpr int TypicalWindowCount = 2;
+ using TWindowDurations = TCompactVector<TDuration, TypicalWindowCount>;
+ using TWindowRates = TCompactVector<double, TypicalWindowCount>;
+
+ //! Durations of configured time windows.
+ TWindowDurations WindowDurations;
+ //! Estimates of a rate over corresponding time windows.
+ TWindowRates WindowRates;
+
+ explicit TEmaCounter(TWindowDurations windowDurations);
+
+ //! Set new value of counter, optionally providing a current timestamp.
+ void Update(i64 newCount, TInstant newTimestamp = TInstant::Now());
+
+ //! Returns the rate for the given window after enough time has passed
+ //! for the values to be accurate (at least the duration of the window itself).
+ //! Optionally a current timestamp can be provided.
+ std::optional<double> GetRate(int windowIndex, TInstant currentTimestamp = TInstant::Now()) const;
+};
+
+// Operators for linear transformations (addition, scaling) of counters over the fixed set of windows.
+
+TEmaCounter operator+(const TEmaCounter& lhs, const TEmaCounter& rhs);
+TEmaCounter& operator+=(TEmaCounter& lhs, const TEmaCounter& rhs);
+TEmaCounter& operator*=(TEmaCounter& lhs, double coefficient);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/error-inl.h b/yt/yt/core/misc/error-inl.h
new file mode 100644
index 0000000000..b6745b09ff
--- /dev/null
+++ b/yt/yt/core/misc/error-inl.h
@@ -0,0 +1,279 @@
+#ifndef ERROR_INL_H_
+#error "Direct inclusion of this file is not allowed, include error.h"
+// For the sake of sane code completion.
+#include "error.h"
+#endif
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr TErrorCode::TErrorCode()
+ : Value_(static_cast<int>(NYT::EErrorCode::OK))
+{ }
+
+inline constexpr TErrorCode::TErrorCode(int value)
+ : Value_(value)
+{ }
+
+template <class E>
+requires std::is_enum_v<E>
+constexpr TErrorCode::TErrorCode(E value)
+ : Value_(static_cast<int>(value))
+{ }
+
+inline constexpr TErrorCode::operator int() const
+{
+ return Value_;
+}
+
+template <class E>
+requires std::is_enum_v<E>
+constexpr bool operator == (TErrorCode lhs, E rhs)
+{
+ return static_cast<int>(lhs) == static_cast<int>(rhs);
+}
+
+constexpr inline bool operator == (TErrorCode lhs, TErrorCode rhs)
+{
+ return static_cast<int>(lhs) == static_cast<int>(rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <size_t Length, class... TArgs>
+TString FormatErrorMessage(const char (&format)[Length], TArgs&&... args)
+{
+ return Format(format, std::forward<TArgs>(args)...);
+}
+
+template <size_t Length>
+TString FormatErrorMessage(const char (&message)[Length])
+{
+ return TString(message);
+}
+
+} // namespace NDetail
+
+template <size_t Length, class... TArgs>
+TError::TErrorOr(const char (&messageOrFormat)[Length], TArgs&&... args)
+ : TErrorOr(NYT::EErrorCode::Generic, NYT::NDetail::FormatErrorMessage(messageOrFormat, std::forward<TArgs>(args)...))
+{ }
+
+template <size_t Length, class... TArgs>
+TError::TErrorOr(TErrorCode code, const char (&messageOrFormat)[Length], TArgs&&... args)
+ : TErrorOr(code, NYT::NDetail::FormatErrorMessage(messageOrFormat, std::forward<TArgs>(args)...))
+{ }
+
+template <class... TArgs>
+TError TError::Wrap(TArgs&&... args) const
+{
+ return TError(std::forward<TArgs>(args)...) << *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TErrorOr<T>::TErrorOr()
+{
+ Value_.emplace();
+}
+
+template <class T>
+TErrorOr<T>::TErrorOr(T&& value) noexcept
+ : Value_(std::move(value))
+{ }
+
+template <class T>
+TErrorOr<T>::TErrorOr(const T& value)
+ : Value_(value)
+{ }
+
+template <class T>
+TErrorOr<T>::TErrorOr(const TError& other)
+ : TError(other)
+{
+ YT_VERIFY(!IsOK());
+}
+
+template <class T>
+TErrorOr<T>::TErrorOr(TError&& other) noexcept
+ : TError(std::move(other))
+{
+ YT_VERIFY(!IsOK());
+}
+
+template <class T>
+TErrorOr<T>::TErrorOr(const TErrorOr<T>& other)
+ : TError(other)
+{
+ if (IsOK()) {
+ Value_ = other.Value();
+ }
+}
+
+template <class T>
+TErrorOr<T>::TErrorOr(TErrorOr<T>&& other) noexcept
+ : TError(std::move(other))
+{
+ if (IsOK()) {
+ Value_ = std::move(other.Value());
+ }
+}
+
+template <class T>
+template <class U>
+TErrorOr<T>::TErrorOr(const TErrorOr<U>& other)
+ : TError(other)
+{
+ if (IsOK()) {
+ Value_ = other.Value();
+ }
+}
+
+template <class T>
+template <class U>
+TErrorOr<T>::TErrorOr(TErrorOr<U>&& other) noexcept
+ : TError(other)
+{
+ if (IsOK()) {
+ Value_ = std::move(other.Value());
+ }
+}
+
+template <class T>
+TErrorOr<T>::TErrorOr(const std::exception& ex)
+ : TError(ex)
+{ }
+
+template <class T>
+TErrorOr<T>& TErrorOr<T>::operator = (const TErrorOr<T>& other)
+{
+ static_cast<TError&>(*this) = static_cast<const TError&>(other);
+ Value_ = other.Value_;
+ return *this;
+}
+
+template <class T>
+TErrorOr<T>& TErrorOr<T>::operator = (TErrorOr<T>&& other) noexcept
+{
+ static_cast<TError&>(*this) = std::move(other);
+ Value_ = std::move(other.Value_);
+ return *this;
+}
+
+template <class T>
+T&& TErrorOr<T>::ValueOrThrow() &&
+{
+ if (!IsOK()) {
+ THROW_ERROR std::move(*this);
+ }
+ return std::move(*Value_);
+}
+
+template <class T>
+T& TErrorOr<T>::ValueOrThrow() &
+{
+ if (!IsOK()) {
+ THROW_ERROR *this;
+ }
+ return *Value_;
+}
+
+template <class T>
+const T& TErrorOr<T>::ValueOrThrow() const &
+{
+ if (!IsOK()) {
+ THROW_ERROR *this;
+ }
+ return *Value_;
+}
+
+template <class T>
+T&& TErrorOr<T>::Value() &&
+{
+ YT_ASSERT(IsOK());
+ return std::move(*Value_);
+}
+
+template <class T>
+T& TErrorOr<T>::Value() &
+{
+ YT_ASSERT(IsOK());
+ return *Value_;
+}
+
+template <class T>
+const T& TErrorOr<T>::Value() const &
+{
+ YT_ASSERT(IsOK());
+ return *Value_;
+}
+
+template <class T>
+const T& TErrorOr<T>::ValueOrDefault(const T& defaultValue) const &
+{
+ return IsOK() ? *Value_ : defaultValue;
+}
+
+template <class T>
+T& TErrorOr<T>::ValueOrDefault(T& defaultValue) &
+{
+ return IsOK() ? *Value_ : defaultValue;
+}
+
+template <class T>
+constexpr T TErrorOr<T>::ValueOrDefault(T&& defaultValue) const &
+{
+ return IsOK()
+ ? *Value_
+ : std::forward<T>(defaultValue);
+}
+
+template <class T>
+constexpr T TErrorOr<T>::ValueOrDefault(T&& defaultValue) &&
+{
+ return IsOK()
+ ? std::move(*Value_)
+ : std::forward<T>(defaultValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringBuf spec)
+{
+ FormatValue(builder, static_cast<const TError&>(error), spec);
+}
+
+template <class T>
+TString ToString(const TErrorOr<T>& valueOrError)
+{
+ return ToString(TError(valueOrError));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TException>
+TException&& operator <<= (TException&& ex, const TError& error)
+{
+ YT_VERIFY(!error.IsOK());
+ ex.Error() = error;
+ return std::move(ex);
+}
+
+template <class TException>
+TException&& operator <<= (TException&& ex, TError&& error)
+{
+ YT_VERIFY(!error.IsOK());
+ ex.Error() = std::move(error);
+ return std::move(ex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/error.cpp b/yt/yt/core/misc/error.cpp
new file mode 100644
index 0000000000..a72b069416
--- /dev/null
+++ b/yt/yt/core/misc/error.cpp
@@ -0,0 +1,1241 @@
+#include "error.h"
+#include "serialize.h"
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt_proto/yt/core/misc/proto/error.pb.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/net/local_address.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/yson/tokenizer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <library/cpp/yt/exception/exception.h>
+
+#include <library/cpp/yt/misc/thread_name.h>
+
+#include <util/string/subst.h>
+
+#include <util/system/error.h>
+#include <util/system/thread.h>
+
+namespace NYT {
+
+using namespace NYTree;
+using namespace NYson;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TErrorCode::Save(TStreamSaveContext& context) const
+{
+ NYT::Save(context, Value_);
+}
+
+void TErrorCode::Load(TStreamLoadContext& context)
+{
+ NYT::Load(context, Value_);
+}
+
+void FormatValue(TStringBuilderBase* builder, TErrorCode code, TStringBuf spec)
+{
+ FormatValue(builder, static_cast<int>(code), spec);
+}
+
+TString ToString(TErrorCode code)
+{
+ return ToStringViaBuilder(code);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+thread_local bool ErrorSanitizerEnabled = false;
+thread_local TInstant ErrorSanitizerDatetimeOverride = {};
+
+TErrorSanitizerGuard::TErrorSanitizerGuard(TInstant datetimeOverride)
+ : SavedEnabled_(ErrorSanitizerEnabled)
+ , SavedDatetimeOverride_(ErrorSanitizerDatetimeOverride)
+{
+ ErrorSanitizerEnabled = true;
+ ErrorSanitizerDatetimeOverride = datetimeOverride;
+}
+
+TErrorSanitizerGuard::~TErrorSanitizerGuard()
+{
+ YT_ASSERT(ErrorSanitizerEnabled);
+
+ ErrorSanitizerEnabled = SavedEnabled_;
+ ErrorSanitizerDatetimeOverride = SavedDatetimeOverride_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TError::TImpl
+{
+public:
+ TImpl()
+ : Code_(NYT::EErrorCode::OK)
+ { }
+
+ TImpl(const TError::TImpl& other)
+ : Code_(other.Code_)
+ , Message_(other.Message_)
+ , Host_(other.Host_)
+ , HostHolder_(other.HostHolder_)
+ , Datetime_(other.Datetime_)
+ , Pid_(other.Pid_)
+ , Tid_(other.Tid_)
+ , ThreadName_(other.ThreadName_)
+ , Fid_(other.Fid_)
+ , TraceId_(other.TraceId_)
+ , SpanId_(other.SpanId_)
+ , Attributes_(other.Attributes_ ? other.Attributes_->Clone() : nullptr)
+ , InnerErrors_(other.InnerErrors_)
+ { }
+
+ explicit TImpl(TString message)
+ : Code_(NYT::EErrorCode::Generic)
+ , Message_(std::move(message))
+ {
+ CaptureOriginAttributes();
+ }
+
+ TImpl(TErrorCode code, TString message)
+ : Code_(code)
+ , Message_(std::move(message))
+ {
+ if (!IsOK()) {
+ CaptureOriginAttributes();
+ }
+ }
+
+ TErrorCode GetCode() const
+ {
+ return Code_;
+ }
+
+ void SetCode(TErrorCode code)
+ {
+ Code_ = code;
+ }
+
+ const TString& GetMessage() const
+ {
+ return Message_;
+ }
+
+ void SetMessage(TString message)
+ {
+ Message_ = std::move(message);
+ }
+
+ bool HasOriginAttributes() const
+ {
+ return Host_.operator bool();
+ }
+
+ TStringBuf GetHost() const
+ {
+ return Host_;
+ }
+
+ bool HasDatetime() const
+ {
+ return Datetime_ != TInstant();
+ }
+
+ TInstant GetDatetime() const
+ {
+ return Datetime_;
+ }
+
+ void SetDatetime(TInstant datetime)
+ {
+ Datetime_ = datetime;
+ }
+
+ TProcessId GetPid() const
+ {
+ return Pid_;
+ }
+
+ NThreading::TThreadId GetTid() const
+ {
+ return Tid_;
+ }
+
+ TStringBuf GetThreadName() const
+ {
+ return ThreadName_.ToStringBuf();
+ }
+
+ NConcurrency::TFiberId GetFid() const
+ {
+ return Fid_;
+ }
+
+ bool HasTracingAttributes() const
+ {
+ return TraceId_ != NTracing::InvalidTraceId;
+ }
+
+ NTracing::TTraceId GetTraceId() const
+ {
+ return TraceId_;
+ }
+
+ NTracing::TSpanId GetSpanId() const
+ {
+ return SpanId_;
+ }
+
+ const IAttributeDictionary& Attributes() const
+ {
+ if (!Attributes_) {
+ return EmptyAttributes();
+ }
+ return *Attributes_;
+ }
+
+ IAttributeDictionary* MutableAttributes()
+ {
+ if (!Attributes_) {
+ Attributes_ = CreateEphemeralAttributes();
+ }
+ return Attributes_.Get();
+ }
+
+ bool HasAttributes() const
+ {
+ return Attributes_.operator bool();
+ }
+
+ void SetAttributes(NYTree::IAttributeDictionaryPtr attributes)
+ {
+ Attributes_ = std::move(attributes);
+ ExtractSystemAttributes();
+ }
+
+ const std::vector<TError>& InnerErrors() const
+ {
+ return InnerErrors_;
+ }
+
+ std::vector<TError>* MutableInnerErrors()
+ {
+ return &InnerErrors_;
+ }
+
+ bool IsOK() const
+ {
+ return Code_ == NYT::EErrorCode::OK;
+ }
+
+ void CopyBuiltinAttributesFrom(const TError::TImpl& other)
+ {
+ Host_ = other.Host_;
+ HostHolder_ = other.HostHolder_;
+ Datetime_ = other.Datetime_;
+ Pid_ = other.Pid_;
+ Tid_ = other.Tid_;
+ ThreadName_ = other.ThreadName_;
+ Fid_ = other.Fid_;
+ TraceId_ = other.TraceId_;
+ SpanId_ = other.SpanId_;
+ }
+
+private:
+ TErrorCode Code_;
+ TString Message_;
+ // Most errors are local; for these Host_ refers to a static buffer and HostHolder_ is not used.
+ // This saves one allocation on TError construction.
+ TStringBuf Host_;
+ // HostHolder_ optionally stores data of Host_, and this pointer connection survives move of containing object.
+ TSharedRef HostHolder_;
+ TInstant Datetime_;
+ TProcessId Pid_ = 0;
+ NThreading::TThreadId Tid_ = NThreading::InvalidThreadId;
+ TThreadName ThreadName_;
+ NConcurrency::TFiberId Fid_ = NConcurrency::InvalidFiberId;
+ NTracing::TTraceId TraceId_ = NTracing::InvalidTraceId;
+ NTracing::TSpanId SpanId_ = NTracing::InvalidSpanId;
+ NYTree::IAttributeDictionaryPtr Attributes_;
+ std::vector<TError> InnerErrors_;
+
+
+ void CaptureOriginAttributes()
+ {
+ if (ErrorSanitizerEnabled) {
+ Datetime_ = ErrorSanitizerDatetimeOverride;
+ return;
+ }
+
+ Host_ = NNet::ReadLocalHostName();
+ Datetime_ = TInstant::Now();
+ Pid_ = GetPID();
+ Tid_ = TThread::CurrentThreadId();
+ ThreadName_ = GetCurrentThreadName();
+ Fid_ = NConcurrency::GetCurrentFiberId();
+ if (const auto* traceContext = NTracing::TryGetCurrentTraceContext()) {
+ TraceId_ = traceContext->GetTraceId();
+ SpanId_ = traceContext->GetSpanId();
+ }
+ }
+
+ void ExtractSystemAttributes()
+ {
+ if (!Attributes_) {
+ return;
+ }
+
+ static const TString HostKey("host");
+ HostHolder_ = TSharedRef::FromString(Attributes_->GetAndRemove<TString>(HostKey, TString()));
+ Host_ = HostHolder_.empty() ? TStringBuf() : TStringBuf(HostHolder_.Begin(), HostHolder_.End());
+
+ static const TString DatetimeKey("datetime");
+ Datetime_ = Attributes_->GetAndRemove<TInstant>(DatetimeKey, TInstant());
+
+ static const TString PidKey("pid");
+ Pid_ = Attributes_->GetAndRemove<TProcessId>(PidKey, 0);
+
+ static const TString TidKey("tid");
+ Tid_ = Attributes_->GetAndRemove<NThreading::TThreadId>(TidKey, NThreading::InvalidThreadId);
+
+ static const TString ThreadNameKey("thread");
+ ThreadName_ = Attributes_->GetAndRemove<TString>(ThreadNameKey, TString());
+
+ static const TString FidKey("fid");
+ Fid_ = Attributes_->GetAndRemove<NConcurrency::TFiberId>(FidKey, NConcurrency::InvalidFiberId);
+
+ static const TString TraceIdKey("trace_id");
+ // COMPAT(babenko): some older versions use uint64 for trace id.
+ try {
+ TraceId_ = Attributes_->GetAndRemove<NTracing::TTraceId>(TraceIdKey, NTracing::InvalidTraceId);
+ } catch (const std::exception&) {
+ TraceId_ = NTracing::TTraceId(Attributes_->GetAndRemove<ui64>(TraceIdKey, 0), 0);
+ }
+
+ static const TString SpanIdKey("span_id");
+ SpanId_ = Attributes_->GetAndRemove<NTracing::TSpanId>(SpanIdKey, NTracing::InvalidSpanId);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TError::TErrorOr() = default;
+
+TError::~TErrorOr() = default;
+
+TError::TErrorOr(const TError& other)
+{
+ if (!other.IsOK()) {
+ Impl_ = std::make_unique<TImpl>(*other.Impl_);
+ }
+}
+
+TError::TErrorOr(TError&& other) noexcept
+ : Impl_(std::move(other.Impl_))
+{ }
+
+TError::TErrorOr(const std::exception& ex)
+{
+ if (const auto* compositeException = dynamic_cast<const TCompositeException*>(&ex)) {
+ try {
+ std::rethrow_exception(compositeException->GetInnerException());
+ } catch (const std::exception& innerEx) {
+ *this = TError(NYT::EErrorCode::Generic, compositeException->GetMessage())
+ << TError(innerEx);
+ }
+ } else if (const auto* errorEx = dynamic_cast<const TErrorException*>(&ex)) {
+ *this = errorEx->Error();
+ } else {
+ *this = TError(NYT::EErrorCode::Generic, ex.what());
+ }
+ YT_VERIFY(!IsOK());
+}
+
+TError::TErrorOr(TString message)
+ : Impl_(std::make_unique<TImpl>(std::move(message)))
+{ }
+
+TError::TErrorOr(TErrorCode code, TString message)
+ : Impl_(std::make_unique<TImpl>(code, std::move(message)))
+{ }
+
+TError& TError::operator = (const TError& other)
+{
+ if (other.IsOK()) {
+ Impl_.reset();
+ } else {
+ Impl_ = std::make_unique<TImpl>(*other.Impl_);
+ }
+ return *this;
+}
+
+TError& TError::operator = (TError&& other) noexcept
+{
+ Impl_ = std::move(other.Impl_);
+ return *this;
+}
+
+TError TError::FromSystem()
+{
+ return FromSystem(LastSystemError());
+}
+
+TError TError::FromSystem(int error)
+{
+ return TError(TErrorCode(LinuxErrorCodeBase + error), LastSystemErrorText(error)) <<
+ TErrorAttribute("errno", error);
+}
+
+TError TError::FromSystem(const TSystemError& error)
+{
+ return FromSystem(error.Status());
+}
+
+TErrorCode TError::GetCode() const
+{
+ if (!Impl_) {
+ return NYT::EErrorCode::OK;
+ }
+ return Impl_->GetCode();
+}
+
+TError& TError::SetCode(TErrorCode code)
+{
+ MakeMutable();
+ Impl_->SetCode(code);
+ return *this;
+}
+
+TErrorCode TError::GetNonTrivialCode() const
+{
+ if (!Impl_) {
+ return NYT::EErrorCode::OK;
+ }
+
+ if (GetCode() != NYT::EErrorCode::Generic) {
+ return GetCode();
+ }
+
+ for (const auto& innerError : InnerErrors()) {
+ auto innerCode = innerError.GetNonTrivialCode();
+ if (innerCode != NYT::EErrorCode::Generic) {
+ return innerCode;
+ }
+ }
+
+ return GetCode();
+}
+
+THashSet<TErrorCode> TError::GetDistinctNonTrivialErrorCodes() const
+{
+ THashSet<TErrorCode> result;
+ TraverseError(*this, [&result] (const TError& error, int /*depth*/) {
+ if (auto errorCode = error.GetCode(); errorCode != NYT::EErrorCode::OK) {
+ result.insert(errorCode);
+ }
+ });
+ return result;
+}
+
+const TString& TError::GetMessage() const
+{
+ if (!Impl_) {
+ static const TString Result;
+ return Result;
+ }
+ return Impl_->GetMessage();
+}
+
+TError& TError::SetMessage(TString message)
+{
+ MakeMutable();
+ Impl_->SetMessage(std::move(message));
+ return *this;
+}
+
+bool TError::HasOriginAttributes() const
+{
+ if (!Impl_) {
+ return false;
+ }
+ return Impl_->HasOriginAttributes();
+}
+
+TStringBuf TError::GetHost() const
+{
+ if (!Impl_) {
+ return {};
+ }
+ return Impl_->GetHost();
+}
+
+bool TError::HasDatetime() const
+{
+ if (!Impl_) {
+ return false;
+ }
+ return Impl_->HasDatetime();
+}
+
+TInstant TError::GetDatetime() const
+{
+ if (!Impl_) {
+ return {};
+ }
+ return Impl_->GetDatetime();
+}
+
+TProcessId TError::GetPid() const
+{
+ if (!Impl_) {
+ return 0;
+ }
+ return Impl_->GetPid();
+}
+
+NThreading::TThreadId TError::GetTid() const
+{
+ if (!Impl_) {
+ return NThreading::InvalidThreadId;
+ }
+ return Impl_->GetTid();
+}
+
+TStringBuf TError::GetThreadName() const
+{
+ if (!Impl_) {
+ static TString empty;
+ return empty;
+ }
+ return Impl_->GetThreadName();
+}
+
+NConcurrency::TFiberId TError::GetFid() const
+{
+ if (!Impl_) {
+ return NConcurrency::InvalidFiberId;
+ }
+ return Impl_->GetFid();
+}
+
+bool TError::HasTracingAttributes() const
+{
+ if (!Impl_) {
+ return false;
+ }
+ return Impl_->HasTracingAttributes();
+}
+
+NTracing::TTraceId TError::GetTraceId() const
+{
+ if (!Impl_) {
+ return NTracing::InvalidTraceId;
+ }
+ return Impl_->GetTraceId();
+}
+
+NTracing::TSpanId TError::GetSpanId() const
+{
+ if (!Impl_) {
+ return NTracing::InvalidSpanId;
+ }
+ return Impl_->GetSpanId();
+}
+
+const IAttributeDictionary& TError::Attributes() const
+{
+ if (!Impl_) {
+ return EmptyAttributes();
+ }
+ return Impl_->Attributes();
+}
+
+IAttributeDictionary* TError::MutableAttributes()
+{
+ MakeMutable();
+ return Impl_->MutableAttributes();
+}
+
+const std::vector<TError>& TError::InnerErrors() const
+{
+ if (!Impl_) {
+ static const std::vector<TError> Result;
+ return Result;
+ }
+ return Impl_->InnerErrors();
+}
+
+std::vector<TError>* TError::MutableInnerErrors()
+{
+ MakeMutable();
+ return Impl_->MutableInnerErrors();
+}
+
+TError TError::Sanitize() const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ auto result = std::make_unique<TImpl>();
+ result->SetCode(GetCode());
+ result->SetMessage(GetMessage());
+ if (Impl_->HasAttributes()) {
+ result->SetAttributes(Impl_->Attributes().Clone());
+ }
+ for (const auto& innerError : Impl_->InnerErrors()) {
+ result->MutableInnerErrors()->push_back(innerError.Sanitize());
+ }
+
+ return TError(std::move(result));
+}
+
+TError TError::Truncate(int maxInnerErrorCount, i64 stringLimit) const
+{
+ static const TString InnerErrorsTruncatedKey("inner_errors_truncated");
+
+ if (!Impl_) {
+ return TError();
+ }
+
+ auto truncateInnerError = [=] (const TError& innerError) {
+ return innerError.Truncate(maxInnerErrorCount, stringLimit);
+ };
+
+ auto truncateString = [stringLimit] (TString string) {
+ if (std::ssize(string) > stringLimit) {
+ return Format("%v...<message truncated>", string.substr(0, stringLimit));
+ }
+ return string;
+ };
+
+ auto truncateAttributes = [stringLimit] (const IAttributeDictionary& attributes) {
+ auto clonedAttributes = attributes.Clone();
+ for (const auto& key : clonedAttributes->ListKeys()) {
+ if (std::ssize(clonedAttributes->FindYson(key).AsStringBuf()) > stringLimit) {
+ clonedAttributes->SetYson(
+ key,
+ BuildYsonStringFluently()
+ .Value("...<attribute truncated>..."));
+ }
+ }
+ return clonedAttributes;
+ };
+
+ auto result = std::make_unique<TImpl>();
+ result->SetCode(GetCode());
+ result->SetMessage(truncateString(GetMessage()));
+ if (Impl_->HasAttributes()) {
+ result->SetAttributes(truncateAttributes(Impl_->Attributes()));
+ }
+ result->CopyBuiltinAttributesFrom(*Impl_);
+
+ if (std::ssize(InnerErrors()) <= maxInnerErrorCount) {
+ for (const auto& innerError : InnerErrors()) {
+ result->MutableInnerErrors()->push_back(truncateInnerError(innerError));
+ }
+ } else {
+ result->MutableAttributes()->Set(InnerErrorsTruncatedKey, true);
+ for (int i = 0; i + 1 < maxInnerErrorCount; ++i) {
+ result->MutableInnerErrors()->push_back(truncateInnerError(InnerErrors()[i]));
+ }
+ result->MutableInnerErrors()->push_back(truncateInnerError(InnerErrors().back()));
+ }
+
+ return TError(std::move(result));
+}
+
+bool TError::IsOK() const
+{
+ if (!Impl_) {
+ return true;
+ }
+ return Impl_->IsOK();
+}
+
+void TError::ThrowOnError() const
+{
+ if (!IsOK()) {
+ THROW_ERROR *this;
+ }
+}
+
+TError TError::Wrap() const
+{
+ return *this;
+}
+
+Y_WEAK TString GetErrorSkeleton(const TError& /*error*/)
+{
+ // Proper implementation resides in yt/yt/library/error_skeleton/skeleton.cpp.
+ THROW_ERROR_EXCEPTION("Error skeleton implementation library is not linked; consider PEERDIR'ing yt/yt/library/error_skeleton");
+}
+
+TString TError::GetSkeleton() const
+{
+ return GetErrorSkeleton(*this);
+}
+
+void TError::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ if (!Impl_) {
+ // Fast path.
+ Save(context, TErrorCode(NYT::EErrorCode::OK)); // code
+ Save(context, TStringBuf()); // message
+ Save(context, IAttributeDictionaryPtr()); // attributes
+ Save(context, std::vector<TError>()); // inner errors
+ return;
+ }
+
+ Save(context, GetCode());
+ Save(context, GetMessage());
+
+ // Cf. TAttributeDictionaryValueSerializer.
+ auto attributePairs = Attributes().ListPairs();
+ size_t attributeCount = attributePairs.size();
+ if (HasOriginAttributes()) {
+ attributeCount += 5;
+ }
+ if (HasDatetime()) {
+ attributeCount += 1;
+ }
+ if (HasTracingAttributes()) {
+ attributeCount += 2;
+ }
+
+ if (attributeCount > 0) {
+ // Cf. TAttributeDictionaryRefSerializer.
+ Save(context, true);
+
+ TSizeSerializer::Save(context, attributeCount);
+
+ auto saveAttribute = [&] (const TString& key, const auto& value) {
+ Save(context, key);
+ Save(context, ConvertToYsonString(value));
+ };
+
+ if (HasOriginAttributes()) {
+ static const TString HostKey("host");
+ saveAttribute(HostKey, GetHost());
+
+ static const TString PidKey("pid");
+ saveAttribute(PidKey, GetPid());
+
+ static const TString TidKey("tid");
+ saveAttribute(TidKey, GetTid());
+
+ static const TString ThreadNameKey("thread");
+ saveAttribute(ThreadNameKey, GetThreadName());
+
+ static const TString FidKey("fid");
+ saveAttribute(FidKey, GetFid());
+ }
+
+ if (HasDatetime()) {
+ static const TString DatetimeKey("datetime");
+ saveAttribute(DatetimeKey, GetDatetime());
+ }
+
+ if (HasTracingAttributes()) {
+ static const TString TraceIdKey("trace_id");
+ saveAttribute(TraceIdKey, GetTraceId());
+
+ static const TString SpanIdKey("span_id");
+ saveAttribute(SpanIdKey, GetSpanId());
+ }
+
+ std::sort(attributePairs.begin(), attributePairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ for (const auto& [key, value] : attributePairs) {
+ Save(context, key);
+ Save(context, value);
+ }
+ } else {
+ Save(context, false);
+ }
+
+ Save(context, InnerErrors());
+}
+
+void TError::Load(TStreamLoadContext& context)
+{
+ Impl_.reset();
+
+ using NYT::Load;
+
+ auto code = Load<TErrorCode>(context);
+ auto message = Load<TString>(context);
+
+ IAttributeDictionaryPtr attributes;
+ if (Load<bool>(context)) {
+ attributes = CreateEphemeralAttributes();
+ TAttributeDictionarySerializer::LoadNonNull(context, attributes);
+ }
+
+ auto innerErrors = Load<std::vector<TError>>(context);
+
+ if (code == NYT::EErrorCode::OK) {
+ // Fast path.
+ // Note that there were no allocations above.
+ return;
+ }
+
+ auto impl = std::make_unique<TImpl>();
+ impl->SetCode(code);
+ impl->SetMessage(std::move(message));
+ impl->SetAttributes(std::move(attributes));
+ *impl->MutableInnerErrors() = std::move(innerErrors);
+ Impl_ = std::move(impl);
+}
+
+std::optional<TError> TError::FindMatching(TErrorCode code) const
+{
+ if (!Impl_) {
+ return {};
+ }
+
+ if (GetCode() == code) {
+ return *this;
+ }
+
+ for (const auto& innerError : InnerErrors()) {
+ auto innerResult = innerError.FindMatching(code);
+ if (innerResult) {
+ return innerResult;
+ }
+ }
+
+ return {};
+}
+
+TError::TErrorOr(std::unique_ptr<TImpl> impl)
+ : Impl_(std::move(impl))
+{ }
+
+void TError::MakeMutable()
+{
+ if (!Impl_) {
+ Impl_ = std::make_unique<TImpl>();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+void AppendIndent(TStringBuilderBase* builer, int indent)
+{
+ builer->AppendChar(' ', indent);
+}
+
+void AppendAttribute(TStringBuilderBase* builder, const TString& key, const TString& value, int indent)
+{
+ AppendIndent(builder, indent + 4);
+ if (!value.Contains('\n')) {
+ builder->AppendFormat("%-15s %s", key, value);
+ } else {
+ builder->AppendString(key);
+ TString indentedValue = "\n" + value;
+ SubstGlobal(indentedValue, "\n", "\n" + TString{static_cast<size_t>(indent + 8), ' '});
+ // Now first line in indentedValue is empty and every other line is indented by 8 spaces.
+ builder->AppendString(indentedValue);
+ }
+ builder->AppendChar('\n');
+}
+
+void AppendError(TStringBuilderBase* builder, const TError& error, int indent)
+{
+ if (error.IsOK()) {
+ builder->AppendString("OK");
+ return;
+ }
+
+ AppendIndent(builder, indent);
+ builder->AppendString(error.GetMessage());
+ builder->AppendChar('\n');
+
+ if (error.GetCode() != NYT::EErrorCode::Generic) {
+ AppendAttribute(builder, "code", ToString(static_cast<int>(error.GetCode())), indent);
+ }
+
+ // Pretty-print origin.
+ if (error.HasOriginAttributes()) {
+ AppendAttribute(
+ builder,
+ "origin",
+ Format("%v (pid %v, thread %v, fid %x)",
+ error.GetHost(),
+ error.GetPid(),
+ (!error.GetThreadName().empty() ? error.GetThreadName() : ToString(error.GetTid())),
+ error.GetFid()),
+ indent);
+ }
+
+ if (error.HasDatetime()) {
+ AppendAttribute(
+ builder,
+ "datetime",
+ Format("%v", error.GetDatetime()),
+ indent);
+ }
+
+ for (const auto& [key, value] : error.Attributes().ListPairs()) {
+ TTokenizer tokenizer(value.AsStringBuf());
+ YT_VERIFY(tokenizer.ParseNext());
+ switch (tokenizer.GetCurrentType()) {
+ case ETokenType::String:
+ AppendAttribute(builder, key, TString(tokenizer.CurrentToken().GetStringValue()), indent);
+ break;
+ case ETokenType::Int64:
+ AppendAttribute(builder, key, ToString(tokenizer.CurrentToken().GetInt64Value()), indent);
+ break;
+ case ETokenType::Uint64:
+ AppendAttribute(builder, key, ToString(tokenizer.CurrentToken().GetUint64Value()), indent);
+ break;
+ case ETokenType::Double:
+ AppendAttribute(builder, key, ToString(tokenizer.CurrentToken().GetDoubleValue()), indent);
+ break;
+ case ETokenType::Boolean:
+ AppendAttribute(builder, key, TString(FormatBool(tokenizer.CurrentToken().GetBooleanValue())), indent);
+ break;
+ default:
+ AppendAttribute(builder, key, ConvertToYsonString(value, EYsonFormat::Text).ToString(), indent);
+ break;
+ }
+ }
+
+ for (const auto& innerError : error.InnerErrors()) {
+ builder->AppendChar('\n');
+ AppendError(builder, innerError, indent + 2);
+ }
+}
+
+} // namespace
+
+bool operator == (const TError& lhs, const TError& rhs)
+{
+ if (!lhs.Impl_ && !rhs.Impl_) {
+ return true;
+ }
+ return
+ lhs.GetCode() == rhs.GetCode() &&
+ lhs.GetMessage() == rhs.GetMessage() &&
+ lhs.GetHost() == rhs.GetHost() &&
+ lhs.GetDatetime() == rhs.GetDatetime() &&
+ lhs.GetPid() == rhs.GetPid() &&
+ lhs.GetTid() == rhs.GetTid() &&
+ lhs.GetFid() == rhs.GetFid() &&
+ lhs.GetTraceId() == rhs.GetTraceId() &&
+ lhs.GetSpanId() == rhs.GetSpanId() &&
+ lhs.Attributes() == rhs.Attributes() &&
+ lhs.InnerErrors() == rhs.InnerErrors();
+}
+
+bool operator != (const TError& lhs, const TError& rhs)
+{
+ return !(lhs == rhs);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TError& error, TStringBuf /*spec*/)
+{
+ AppendError(builder, error, 0);
+}
+
+TString ToString(const TError& error)
+{
+ TStringBuilder builder;
+ AppendError(&builder, error, 0);
+ return builder.Flush();
+}
+
+void ToProto(NYT::NProto::TError* protoError, const TError& error)
+{
+ if (!error.Impl_) {
+ protoError->set_code(static_cast<int>(NYT::EErrorCode::OK));
+ protoError->clear_message();
+ return;
+ }
+
+ protoError->set_code(error.GetCode());
+ protoError->set_message(error.GetMessage());
+
+ protoError->clear_attributes();
+ if (error.Impl_->HasAttributes()) {
+ ToProto(protoError->mutable_attributes(), error.Attributes());
+ }
+
+ auto addAttribute = [&] (const TString& key, const auto& value) {
+ auto* protoItem = protoError->mutable_attributes()->add_attributes();
+ protoItem->set_key(key);
+ protoItem->set_value(ConvertToYsonString(value).ToString());
+ };
+
+ if (error.HasOriginAttributes()) {
+ static const TString HostKey("host");
+ addAttribute(HostKey, error.GetHost());
+
+ static const TString PidKey("pid");
+ addAttribute(PidKey, error.GetPid());
+
+ static const TString TidKey("tid");
+ addAttribute(TidKey, error.GetTid());
+
+ static const TString ThreadName("thread");
+ addAttribute(ThreadName, error.GetThreadName());
+
+ static const TString FidKey("fid");
+ addAttribute(FidKey, error.GetFid());
+ }
+
+ if (error.HasDatetime()) {
+ static const TString DatetimeKey("datetime");
+ addAttribute(DatetimeKey, error.GetDatetime());
+ }
+
+ if (error.HasTracingAttributes()) {
+ static const TString TraceIdKey("trace_id");
+ addAttribute(TraceIdKey, error.GetTraceId());
+
+ static const TString SpanIdKey("span_id");
+ addAttribute(SpanIdKey, error.GetSpanId());
+ }
+
+ protoError->clear_inner_errors();
+ for (const auto& innerError : error.InnerErrors()) {
+ ToProto(protoError->add_inner_errors(), innerError);
+ }
+}
+
+void FromProto(TError* error, const NYT::NProto::TError& protoError)
+{
+ *error = {};
+
+ if (protoError.code() == static_cast<int>(NYT::EErrorCode::OK)) {
+ return;
+ }
+
+ error->SetCode(TErrorCode(protoError.code()));
+ error->SetMessage(protoError.message());
+ if (protoError.has_attributes()) {
+ error->Impl_->SetAttributes(FromProto(protoError.attributes()));
+ } else {
+ error->Impl_->SetAttributes(nullptr);
+ }
+ *error->MutableInnerErrors() = FromProto<std::vector<TError>>(protoError.inner_errors());
+}
+
+void TraverseError(const TError& error, const TErrorVisitor& visitor, int depth)
+{
+ visitor(error, depth);
+ for (const auto& inner : error.InnerErrors()) {
+ TraverseError(inner, visitor, depth + 1);
+ }
+}
+
+namespace {
+
+// Errors whose depth exceeds |ErrorSerializationDepthLimit| are serialized
+// as children of their ancestor on depth |ErrorSerializationDepthLimit - 1|.
+void SerializeInnerErrors(TFluentMap fluent, const TError& error, int depth)
+{
+ if (depth >= ErrorSerializationDepthLimit) {
+ // Ignore deep inner errors.
+ return;
+ }
+
+ auto visit = [&] (auto fluent, const TError& error, int depth) {
+ fluent
+ .Item().Do([&] (auto fluent) {
+ Serialize(error, fluent.GetConsumer(), /* valueProduce */ nullptr, depth);
+ });
+ };
+
+ fluent
+ .Item("inner_errors").DoListFor(error.InnerErrors(), [&] (auto fluent, const TError& innerError) {
+ if (depth < ErrorSerializationDepthLimit - 1) {
+ visit(fluent, innerError, depth + 1);
+ } else {
+ YT_VERIFY(depth == ErrorSerializationDepthLimit - 1);
+ TraverseError(
+ innerError,
+ [&] (const TError& e, int depth) {
+ visit(fluent, e, depth);
+ },
+ depth + 1);
+ }
+ });
+}
+
+} // namespace
+
+void Serialize(
+ const TError& error,
+ IYsonConsumer* consumer,
+ const std::function<void(IYsonConsumer*)>* valueProducer,
+ int depth)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("code").Value(error.GetCode())
+ .Item("message").Value(error.GetMessage())
+ .Item("attributes").DoMap([&] (auto fluent) {
+ if (error.HasOriginAttributes()) {
+ fluent
+ .Item("host").Value(error.GetHost())
+ .Item("pid").Value(error.GetPid())
+ .Item("tid").Value(error.GetTid())
+ .Item("thread").Value(error.GetThreadName())
+ .Item("fid").Value(error.GetFid());
+ }
+ if (error.HasDatetime()) {
+ fluent
+ .Item("datetime").Value(error.GetDatetime());
+ }
+ if (error.HasTracingAttributes()) {
+ fluent
+ .Item("trace_id").Value(error.GetTraceId())
+ .Item("span_id").Value(error.GetSpanId());
+ }
+ if (depth > ErrorSerializationDepthLimit) {
+ fluent
+ .Item("original_error_depth").Value(depth);
+ }
+ for (const auto& [key, value] : error.Attributes().ListPairs()) {
+ fluent
+ .Item(key).Value(value);
+ }
+ })
+ .DoIf(!error.InnerErrors().empty(), [&] (auto fluent) {
+ SerializeInnerErrors(fluent, error, depth);
+ })
+ .DoIf(valueProducer != nullptr, [&] (auto fluent) {
+ auto* consumer = fluent.GetConsumer();
+ // NB: we are forced to deal with a bare consumer here because
+ // we can't use void(TFluentMap) in a function signature as it
+ // will lead to the inclusion of fluent.h in error.h and a cyclic
+ // inclusion error.h -> fluent.h -> callback.h -> error.h
+ consumer->OnKeyedItem(TStringBuf("value"));
+ (*valueProducer)(consumer);
+ })
+ .EndMap();
+
+}
+
+void Deserialize(TError& error, const NYTree::INodePtr& node)
+{
+ error = {};
+
+ auto mapNode = node->AsMap();
+
+ static const TString CodeKey("code");
+ auto code = TErrorCode(mapNode->GetChildValueOrThrow<i64>(CodeKey));
+ if (code == NYT::EErrorCode::OK) {
+ return;
+ }
+
+ auto result = std::make_unique<TError::TImpl>();
+ result->SetCode(code);
+
+ static const TString MessageKey("message");
+ result->SetMessage(mapNode->GetChildValueOrThrow<TString>(MessageKey));
+
+ static const TString AttributesKey("attributes");
+ result->SetAttributes(IAttributeDictionary::FromMap(mapNode->GetChildOrThrow(AttributesKey)->AsMap()));
+
+ static const TString InnerErrorsKey("inner_errors");
+ if (auto innerErrorsNode = mapNode->FindChild(InnerErrorsKey)) {
+ for (const auto& innerErrorNode : innerErrorsNode->AsList()->GetChildren()) {
+ result->MutableInnerErrors()->push_back(ConvertTo<TError>(innerErrorNode));
+ }
+ }
+
+ error = TError(std::move(result));
+}
+
+void Deserialize(TError& error, NYson::TYsonPullParserCursor* cursor)
+{
+ Deserialize(error, ExtractTo<INodePtr>(cursor));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TError operator << (TError error, const TErrorAttribute& attribute)
+{
+ error.MutableAttributes()->SetYson(attribute.Key, attribute.Value);
+ return error;
+}
+
+TError operator << (TError error, const std::vector<TErrorAttribute>& attributes)
+{
+ for (const auto& attribute : attributes) {
+ error.MutableAttributes()->SetYson(attribute.Key, attribute.Value);
+ }
+ return error;
+}
+
+TError operator << (TError error, const TError& innerError)
+{
+ error.MutableInnerErrors()->push_back(innerError);
+ return error;
+}
+
+TError operator << (TError error, TError&& innerError)
+{
+ error.MutableInnerErrors()->push_back(std::move(innerError));
+ return error;
+}
+
+TError operator << (TError error, const std::vector<TError>& innerErrors)
+{
+ error.MutableInnerErrors()->insert(
+ error.MutableInnerErrors()->end(),
+ innerErrors.begin(),
+ innerErrors.end());
+ return error;
+}
+
+TError operator << (TError error, std::vector<TError>&& innerErrors)
+{
+ error.MutableInnerErrors()->insert(
+ error.MutableInnerErrors()->end(),
+ std::make_move_iterator(innerErrors.begin()),
+ std::make_move_iterator(innerErrors.end()));
+ return error;
+}
+
+TError operator << (TError error, const NYTree::IAttributeDictionary& attributes)
+{
+ error.MutableAttributes()->MergeFrom(attributes);
+ return error;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const char* TErrorException::what() const noexcept
+{
+ if (CachedWhat_.empty()) {
+ CachedWhat_ = ToString(Error_);
+ }
+ return CachedWhat_.data();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/error.h b/yt/yt/core/misc/error.h
new file mode 100644
index 0000000000..c99893e4a5
--- /dev/null
+++ b/yt/yt/core/misc/error.h
@@ -0,0 +1,381 @@
+#pragma once
+
+#include "public.h"
+#include "property.h"
+#include "optional.h"
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/attributes.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/threading/public.h>
+
+#include <library/cpp/yt/yson_string/convert.h>
+
+#include <util/system/getpid.h>
+
+#include <util/generic/size_literals.h>
+
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque wrapper around |int| value capable of conversions from |int|s and
+//! arbitrary enum types.
+class TErrorCode
+{
+public:
+ using TUnderlying = int;
+
+ constexpr TErrorCode();
+ explicit constexpr TErrorCode(int value);
+ template <class E>
+ requires std::is_enum_v<E>
+ constexpr TErrorCode(E value);
+
+ constexpr operator int() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ int Value_;
+};
+
+void FormatValue(TStringBuilderBase* builder, TErrorCode code, TStringBuf spec);
+TString ToString(TErrorCode code);
+
+template <class E>
+requires std::is_enum_v<E>
+constexpr bool operator == (TErrorCode lhs, E rhs);
+
+constexpr bool operator == (TErrorCode lhs, TErrorCode rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int ErrorSerializationDepthLimit = 16;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! When this guard is set, newly created errors do not have non-deterministic
+//! system attributes and have "datetime" attribute overridden with a given value.
+class TErrorSanitizerGuard
+ : public TNonCopyable
+{
+public:
+ explicit TErrorSanitizerGuard(TInstant datetimeOverride);
+ ~TErrorSanitizerGuard();
+
+private:
+ const bool SavedEnabled_;
+ const TInstant SavedDatetimeOverride_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class [[nodiscard]] TErrorOr<void>
+{
+public:
+ TErrorOr();
+ ~TErrorOr();
+
+ TErrorOr(const TError& other);
+ TErrorOr(TError&& other) noexcept;
+
+ TErrorOr(const std::exception& ex);
+
+ explicit TErrorOr(TString message);
+ TErrorOr(TErrorCode code, TString message);
+
+ template <size_t Length, class... TArgs>
+ explicit TErrorOr(
+ const char (&messageOrFormat)[Length],
+ TArgs&&... arg);
+
+ template <size_t Length, class... TArgs>
+ TErrorOr(
+ TErrorCode code,
+ const char (&messageOrFormat)[Length],
+ TArgs&&... args);
+
+ TError& operator = (const TError& other);
+ TError& operator = (TError&& other) noexcept;
+
+ static TError FromSystem();
+ static TError FromSystem(int error);
+ static TError FromSystem(const TSystemError& error);
+
+ TErrorCode GetCode() const;
+ TError& SetCode(TErrorCode code);
+
+ TErrorCode GetNonTrivialCode() const;
+ THashSet<TErrorCode> GetDistinctNonTrivialErrorCodes() const;
+
+ const TString& GetMessage() const;
+ TError& SetMessage(TString message);
+
+ bool HasOriginAttributes() const;
+ TStringBuf GetHost() const;
+ TProcessId GetPid() const;
+ TStringBuf GetThreadName() const;
+ NThreading::TThreadId GetTid() const;
+ NConcurrency::TFiberId GetFid() const;
+
+ bool HasDatetime() const;
+ TInstant GetDatetime() const;
+
+ bool HasTracingAttributes() const;
+ NTracing::TTraceId GetTraceId() const;
+ NTracing::TSpanId GetSpanId() const;
+
+ const NYTree::IAttributeDictionary& Attributes() const;
+ NYTree::IAttributeDictionary* MutableAttributes();
+
+ const std::vector<TError>& InnerErrors() const;
+ std::vector<TError>* MutableInnerErrors();
+
+ // TODO(gritukan): This method is used only outside of YT. Remove it.
+ TError Sanitize() const;
+
+ TError Truncate(int maxInnerErrorCount = 2, i64 stringLimit = 16_KB) const;
+
+ bool IsOK() const;
+
+ void ThrowOnError() const;
+
+ std::optional<TError> FindMatching(TErrorCode code) const;
+
+ template <class... TArgs>
+ TError Wrap(TArgs&&... args) const;
+ TError Wrap() const;
+
+ //! Perform recursive aggregation of error codes and messages over the error tree.
+ //! Result of this aggregation is suitable for error clustering in groups of
+ //! "similar" errors. Refer to yt/yt/library/error_skeleton/skeleton_ut.cpp for examples.
+ //!
+ //! This method builds skeleton from scratch by doing complete error tree traversal,
+ //! so calling it in computationally hot code is discouraged.
+ //!
+ //! In order to prevent core -> re2 dependency, implementation belongs to a separate library
+ //! yt/yt/library/error_skeleton. Calling this method without PEERDIR'ing implementation
+ //! results in an exception.
+ TString GetSkeleton() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ class TImpl;
+ std::unique_ptr<TImpl> Impl_;
+
+ explicit TErrorOr(std::unique_ptr<TImpl> impl);
+
+ void MakeMutable();
+
+ friend bool operator == (const TError& lhs, const TError& rhs);
+ friend bool operator != (const TError& lhs, const TError& rhs);
+
+ friend void ToProto(NProto::TError* protoError, const TError& error);
+ friend void FromProto(TError* error, const NProto::TError& protoError);
+
+ friend void Serialize(
+ const TError& error,
+ NYson::IYsonConsumer* consumer,
+ const std::function<void(NYson::IYsonConsumer*)>* valueProducer,
+ int depth);
+ friend void Deserialize(TError& error, const NYTree::INodePtr& node);
+ friend void Deserialize(TError& error, NYson::TYsonPullParserCursor* cursor);
+};
+
+bool operator == (const TError& lhs, const TError& rhs);
+bool operator != (const TError& lhs, const TError& rhs);
+
+void ToProto(NProto::TError* protoError, const TError& error);
+void FromProto(TError* error, const NProto::TError& protoError);
+
+using TErrorVisitor = std::function<void(const TError&, int depth)>;
+
+//! Traverses the error tree in DFS order.
+void TraverseError(
+ const TError& error,
+ const TErrorVisitor& visitor,
+ int depth = 0);
+
+void Serialize(
+ const TError& error,
+ NYson::IYsonConsumer* consumer,
+ const std::function<void(NYson::IYsonConsumer*)>* valueProducer = nullptr,
+ int depth = 0);
+void Deserialize(
+ TError& error,
+ const NYTree::INodePtr& node);
+
+void FormatValue(TStringBuilderBase* builder, const TError& error, TStringBuf spec);
+TString ToString(const TError& error);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TErrorTraits
+{
+ using TWrapped = TErrorOr<T>;
+ using TUnwrapped = T;
+};
+
+template <class T>
+struct TErrorTraits<TErrorOr<T>>
+{
+ using TUnderlying = T;
+ using TWrapped = TErrorOr<T>;
+ using TUnwrapped = T;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TErrorAttribute
+{
+ template <class T>
+ TErrorAttribute(const TString& key, const T& value)
+ : Key(key)
+ , Value(NYson::ConvertToYsonString(value))
+ { }
+
+ TErrorAttribute(const TString& key, const NYson::TYsonString& value)
+ : Key(key)
+ , Value(value)
+ { }
+
+ TString Key;
+ NYson::TYsonString Value;
+};
+
+TError operator << (TError error, const TErrorAttribute& attribute);
+TError operator << (TError error, const std::vector<TErrorAttribute>& attributes);
+TError operator << (TError error, const TError& innerError);
+TError operator << (TError error, TError&& innerError);
+TError operator << (TError error, const std::vector<TError>& innerErrors);
+TError operator << (TError error, std::vector<TError>&& innerErrors);
+TError operator << (TError error, const NYTree::IAttributeDictionary& attributes);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TErrorException
+ : public std::exception
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(TError, Error);
+
+public:
+ TErrorException() = default;
+ TErrorException(const TErrorException& other) = default;
+ TErrorException(TErrorException&& other) = default;
+
+ const char* what() const noexcept override;
+
+private:
+ mutable TString CachedWhat_;
+
+};
+
+// Make these templates to avoid type erasure during throw.
+template <class TException>
+TException&& operator <<= (TException&& ex, const TError& error);
+template <class TException>
+TException&& operator <<= (TException&& ex, TError&& error);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define THROW_ERROR \
+ throw ::NYT::TErrorException() <<=
+
+#define THROW_ERROR_EXCEPTION(head, ...) \
+ THROW_ERROR ::NYT::TError(head __VA_OPT__(,) __VA_ARGS__)
+
+#define THROW_ERROR_EXCEPTION_IF_FAILED(error, ...) \
+ if (const auto& error__ ## __LINE__ = (error); error__ ## __LINE__ .IsOK()) { \
+ } else { \
+ THROW_ERROR error__ ## __LINE__.Wrap(__VA_ARGS__); \
+ }
+
+#define THROW_ERROR_EXCEPTION_UNLESS(condition, head, ...) \
+ if ((condition)) {\
+ } else { \
+ THROW_ERROR ::NYT::TError(head __VA_OPT__(,) __VA_ARGS__); \
+ }
+
+#define THROW_ERROR_EXCEPTION_IF(condition, head, ...) \
+ THROW_ERROR_EXCEPTION_UNLESS(!(condition), head, __VA_ARGS__)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class [[nodiscard]] TErrorOr
+ : public TError
+{
+public:
+ TErrorOr();
+
+ TErrorOr(const T& value);
+ TErrorOr(T&& value) noexcept;
+
+ TErrorOr(const TErrorOr<T>& other);
+ TErrorOr(TErrorOr<T>&& other) noexcept;
+
+ TErrorOr(const TError& other);
+ TErrorOr(TError&& other) noexcept;
+
+ TErrorOr(const std::exception& ex);
+
+ template <class U>
+ TErrorOr(const TErrorOr<U>& other);
+ template <class U>
+ TErrorOr(TErrorOr<U>&& other) noexcept;
+
+ TErrorOr<T>& operator = (const TErrorOr<T>& other);
+ TErrorOr<T>& operator = (TErrorOr<T>&& other) noexcept;
+
+ const T& Value() const &;
+ T& Value() &;
+ T&& Value() &&;
+
+ const T& ValueOrThrow() const &;
+ T& ValueOrThrow() &;
+ T&& ValueOrThrow() &&;
+
+ const T& ValueOrDefault(const T& defaultValue) const &;
+ T& ValueOrDefault(T& defaultValue) &;
+ constexpr T ValueOrDefault(T&& defaultValue) const &;
+ constexpr T ValueOrDefault(T&& defaultValue) &&;
+
+private:
+ std::optional<T> Value_;
+};
+
+template <class T>
+void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringBuf spec);
+
+template <class T>
+TString ToString(const TErrorOr<T>& valueOrError);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class F, class... As>
+auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...))
+{
+ return functor(std::forward<As>(args)...);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ERROR_INL_H_
+#include "error-inl.h"
+#undef ERROR_INL_H_
diff --git a/yt/yt/core/misc/error_code.cpp b/yt/yt/core/misc/error_code.cpp
new file mode 100644
index 0000000000..daf93d634d
--- /dev/null
+++ b/yt/yt/core/misc/error_code.cpp
@@ -0,0 +1,160 @@
+#include "error_code.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <util/string/split.h>
+
+#include <util/system/type_name.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(achulkov2): Remove this once we find all duplicate error codes.
+static NLogging::TLogger GetLogger()
+{
+ static NLogging::TLogger logger("ErrorCode");
+ return logger;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TErrorCodeRegistry::TErrorCodeInfo::operator==(const TErrorCodeInfo& rhs) const
+{
+ return Namespace == rhs.Namespace && Name == rhs.Name;
+}
+
+TErrorCodeRegistry* TErrorCodeRegistry::Get()
+{
+ return LeakySingleton<TErrorCodeRegistry>();
+}
+
+TErrorCodeRegistry::TErrorCodeInfo TErrorCodeRegistry::Get(int code) const
+{
+ auto it = CodeToInfo_.find(code);
+ if (it != CodeToInfo_.end()) {
+ return it->second;
+ }
+ for (const auto& range : ErrorCodeRanges_) {
+ if (range.Contains(code)) {
+ return range.Get(code);
+ }
+ }
+ return {"NUnknown", Format("ErrorCode%v", code)};
+}
+
+THashMap<int, TErrorCodeRegistry::TErrorCodeInfo> TErrorCodeRegistry::GetAllErrorCodes() const
+{
+ return CodeToInfo_;
+}
+
+std::vector<TErrorCodeRegistry::TErrorCodeRangeInfo> TErrorCodeRegistry::GetAllErrorCodeRanges() const
+{
+ return ErrorCodeRanges_;
+}
+
+void TErrorCodeRegistry::RegisterErrorCode(int code, const TErrorCodeInfo& errorCodeInfo)
+{
+ if (!CodeToInfo_.insert({code, errorCodeInfo}).second) {
+ // TODO(achulkov2): Deal with duplicate TransportError in NRpc and NBus.
+ if (code == 100) {
+ return;
+ }
+ auto Logger = GetLogger();
+ YT_LOG_FATAL(
+ "Duplicate error code (Code: %v, StoredCodeInfo: %v, NewCodeInfo: %v)",
+ code,
+ CodeToInfo_[code],
+ errorCodeInfo);
+ }
+}
+
+TErrorCodeRegistry::TErrorCodeInfo TErrorCodeRegistry::TErrorCodeRangeInfo::Get(int code) const
+{
+ return {Namespace, Formatter(code)};
+}
+
+bool TErrorCodeRegistry::TErrorCodeRangeInfo::Intersects(const TErrorCodeRangeInfo& other) const
+{
+ return std::max(From, other.From) <= std::min(To, other.To);
+}
+
+bool TErrorCodeRegistry::TErrorCodeRangeInfo::Contains(int value) const
+{
+ return From <= value && value <= To;
+}
+
+void TErrorCodeRegistry::RegisterErrorCodeRange(int from, int to, TString namespaceName, std::function<TString(int)> formatter)
+{
+ YT_VERIFY(from <= to);
+
+ TErrorCodeRangeInfo newRange{from, to, std::move(namespaceName), std::move(formatter)};
+ auto Logger = GetLogger();
+ for (const auto& range : ErrorCodeRanges_) {
+ YT_LOG_FATAL_IF(
+ range.Intersects(newRange),
+ "Intersecting error code ranges registered (FirstRange: %v, SecondRange: %v)",
+ range,
+ newRange);
+ }
+ ErrorCodeRanges_.push_back(std::move(newRange));
+ CheckCodesAgainstRanges();
+}
+
+void TErrorCodeRegistry::CheckCodesAgainstRanges() const
+{
+ auto Logger = GetLogger();
+ for (const auto& [code, info] : CodeToInfo_) {
+ for (const auto& range : ErrorCodeRanges_) {
+ YT_LOG_FATAL_IF(
+ range.Contains(code),
+ "Error code range contains another registered code "
+ "(Range: %v, Code: %v, RangeCodeInfo: %v, StandaloneCodeInfo: %v)",
+ range,
+ code,
+ range.Get(code),
+ info);
+ }
+ }
+}
+
+TString TErrorCodeRegistry::ParseNamespace(const std::type_info& errorCodeEnumTypeInfo)
+{
+ TString name;
+ // Ensures that "EErrorCode" is found as a substring in the type name and stores the prefix before
+ // the first occurrence into #name.
+ YT_VERIFY(StringSplitter(
+ TypeName(errorCodeEnumTypeInfo)).SplitByString("EErrorCode").Limit(2).TryCollectInto(&name, &std::ignore));
+
+ // TypeName returns name in form "enum ErrorCode" on Windows
+ if (name.StartsWith("enum ")) {
+ name.remove(0, 5);
+ }
+
+ // If the enum was declared directly in the global namespace, #name should be empty.
+ // Otherwise, #name should end with "::".
+ if (!name.empty()) {
+ YT_VERIFY(name.EndsWith("::"));
+ name.resize(name.size() - 2);
+ }
+ return name;
+}
+
+TString ToString(const TErrorCodeRegistry::TErrorCodeInfo& errorCodeInfo)
+{
+ if (errorCodeInfo.Namespace.empty()) {
+ return Format("EErrorCode::%v", errorCodeInfo.Name);
+ }
+ return Format("%v::EErrorCode::%v", errorCodeInfo.Namespace, errorCodeInfo.Name);
+}
+
+TString ToString(const TErrorCodeRegistry::TErrorCodeRangeInfo& errorCodeRangeInfo)
+{
+ return Format("%v-%v", errorCodeRangeInfo.From, errorCodeRangeInfo.To);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/error_code.h b/yt/yt/core/misc/error_code.h
new file mode 100644
index 0000000000..3ab42c092a
--- /dev/null
+++ b/yt/yt/core/misc/error_code.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include <library/cpp/yt/misc/enum.h>
+#include <library/cpp/yt/misc/port.h>
+
+#include <util/generic/hash_set.h>
+
+#include <library/cpp/yt/misc/preprocessor.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TErrorCodeRegistry
+{
+public:
+ static TErrorCodeRegistry* Get();
+
+ struct TErrorCodeInfo
+ {
+ TString Namespace;
+ //! Human-readable error code name.
+ TString Name;
+
+ bool operator==(const TErrorCodeInfo& rhs) const;
+ };
+
+ struct TErrorCodeRangeInfo
+ {
+ int From;
+ int To;
+ TString Namespace;
+ std::function<TString(int code)> Formatter;
+
+ TErrorCodeInfo Get(int code) const;
+ bool Intersects(const TErrorCodeRangeInfo& other) const;
+ bool Contains(int value) const;
+ };
+
+ //! Retrieves info from registered codes and code ranges.
+ TErrorCodeInfo Get(int code) const;
+
+ //! Retrieves information about registered codes.
+ THashMap<int, TErrorCodeInfo> GetAllErrorCodes() const;
+
+ //! Retrieves information about registered code ranges.
+ std::vector<TErrorCodeRangeInfo> GetAllErrorCodeRanges() const;
+
+ //! Registers a single error code.
+ void RegisterErrorCode(int code, const TErrorCodeInfo& errorCodeInfo);
+
+ //! Registers a range of error codes given a human-readable code to name formatter.
+ void RegisterErrorCodeRange(int from, int to, TString namespaceName, std::function<TString(int code)> formatter);
+
+ static TString ParseNamespace(const std::type_info& errorCodeEnumTypeInfo);
+
+private:
+ THashMap<int, TErrorCodeInfo> CodeToInfo_;
+ std::vector<TErrorCodeRangeInfo> ErrorCodeRanges_;
+
+ void CheckCodesAgainstRanges() const;
+};
+
+TString ToString(const TErrorCodeRegistry::TErrorCodeInfo& errorCodeInfo);
+TString ToString(const TErrorCodeRegistry::TErrorCodeRangeInfo& errorCodeInfo);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define YT_DEFINE_ERROR_ENUM(seq) \
+ DEFINE_ENUM(EErrorCode, seq); \
+ YT_ATTRIBUTE_USED inline const void* ErrorEnum_EErrorCode = [] { \
+ for (auto errorCode : ::NYT::TEnumTraits<EErrorCode>::GetDomainValues()) { \
+ ::NYT::TErrorCodeRegistry::Get()->RegisterErrorCode( \
+ static_cast<int>(errorCode), \
+ {::NYT::TErrorCodeRegistry::ParseNamespace(typeid(EErrorCode)), ToString(errorCode)}); \
+ } \
+ return nullptr; \
+ } ()
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! NB: This macro should only by used in cpp files.
+#define YT_DEFINE_ERROR_CODE_RANGE(from, to, namespaceName, formatter) \
+ YT_ATTRIBUTE_USED static const void* PP_ANONYMOUS_VARIABLE(RegisterErrorCodeRange) = [] { \
+ ::NYT::TErrorCodeRegistry::Get()->RegisterErrorCodeRange(from, to, namespaceName, formatter); \
+ return nullptr; \
+ } ()
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/fair_scheduler-inl.h b/yt/yt/core/misc/fair_scheduler-inl.h
new file mode 100644
index 0000000000..fff2a2c26c
--- /dev/null
+++ b/yt/yt/core/misc/fair_scheduler-inl.h
@@ -0,0 +1,150 @@
+#pragma once
+#ifndef FAIR_SCHEDULER_INL_H_
+#error "Direct inclusion of this file is not allowed, include fair_scheduler.h"
+// For the sake of sane code completion.
+#include "fair_scheduler.h"
+#endif
+
+#include "heap.h"
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTask>
+class TFairScheduler
+ : public IFairScheduler<TTask>
+{
+public:
+ void Enqueue(TTask task, const TString& user) override
+ {
+ auto guard = Guard(Lock_);
+
+ auto* bucket = GetOrCreateBucket(user);
+ // Insert the bucket into the heap if this is its first task.
+ if (!bucket->InHeap) {
+ BucketHeap_.push_back(bucket);
+ AdjustHeapBack(BucketHeap_.begin(), BucketHeap_.end(), TUserBucketComparer());
+ bucket->InHeap = true;
+ }
+ bucket->Tasks.push(std::move(task));
+ }
+
+ TTask Dequeue() override
+ {
+ auto guard = Guard(Lock_);
+
+ while (true) {
+ YT_VERIFY(!BucketHeap_.empty());
+
+ auto* bucket = BucketHeap_.front();
+ YT_ASSERT(bucket->InHeap);
+
+ auto actualExcessTime = std::max(bucket->ExcessTime, ExcessBaseline_);
+
+ // Account for charged time possibly reordering the heap.
+ if (bucket->HeapKey != actualExcessTime) {
+ YT_ASSERT(bucket->HeapKey < actualExcessTime);
+ bucket->HeapKey = actualExcessTime;
+ AdjustHeapFront(BucketHeap_.begin(), BucketHeap_.end(), TUserBucketComparer());
+ continue;
+ }
+
+ auto& tasks = bucket->Tasks;
+ YT_VERIFY(!tasks.empty());
+
+ // Extract the task.
+ auto task = std::move(bucket->Tasks.front());
+ bucket->Tasks.pop();
+
+ // Remove the bucket from the heap if no tasks are pending.
+ if (bucket->Tasks.empty()) {
+ ExtractHeap(BucketHeap_.begin(), BucketHeap_.end(), TUserBucketComparer());
+ BucketHeap_.pop_back();
+ bucket->InHeap = false;
+ }
+
+ // Promote the baseline.
+ ExcessBaseline_ = actualExcessTime;
+
+ return task;
+ }
+
+ YT_ABORT();
+ }
+
+ bool IsEmpty() const override
+ {
+ auto guard = Guard(Lock_);
+
+ return BucketHeap_.empty();
+ }
+
+ void ChargeUser(const TString& user, TDuration time) override
+ {
+ auto guard = Guard(Lock_);
+
+ auto* bucket = GetOrCreateBucket(user);
+ // Just charge the bucket, do not reorder it in the heap.
+ auto actualExcessTime = std::max(bucket->ExcessTime, ExcessBaseline_);
+ bucket->ExcessTime = actualExcessTime + time;
+ }
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ struct TUserBucket
+ {
+ explicit TUserBucket(TString userName)
+ : UserName(std::move(userName))
+ { }
+
+ TString UserName;
+ TDuration ExcessTime;
+ //! Typically equals ExcessTime; however when a user is charged we just update ExcessTime
+ //! and leave HeapKey intact. Upon extracting heap's top we check if its ExcessTime matches its HeapKey
+ //! and if not then readjust the heap.
+ TDuration HeapKey;
+ std::queue<TTask> Tasks;
+ bool InHeap = false;
+ };
+
+ struct TUserBucketComparer
+ {
+ bool operator ()(TUserBucket* lhs, TUserBucket* rhs) const
+ {
+ return lhs->HeapKey < rhs->HeapKey;
+ }
+ };
+
+ THashMap<TString, TUserBucket> NameToUserBucket_;
+ TDuration ExcessBaseline_;
+
+ //! Min-heap ordered by TUserBucket::ExcessTime.
+ //! A bucket is only present here iff it has at least one task.
+ std::vector<TUserBucket*> BucketHeap_;
+
+ TUserBucket* GetOrCreateBucket(const TString& userName)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ auto [it, inserted] = NameToUserBucket_.emplace(userName, TUserBucket(userName));
+ return &it->second;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTask>
+IFairSchedulerPtr<TTask> CreateFairScheduler()
+{
+ return New<TFairScheduler<TTask>>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/fair_scheduler.h b/yt/yt/core/misc/fair_scheduler.h
new file mode 100644
index 0000000000..7f7d607667
--- /dev/null
+++ b/yt/yt/core/misc/fair_scheduler.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTask>
+struct IFairScheduler
+ : public TRefCounted
+{
+ virtual void Enqueue(TTask task, const TString& user) = 0;
+
+ virtual TTask Dequeue() = 0;
+
+ virtual bool IsEmpty() const = 0;
+
+ virtual void ChargeUser(const TString& user, TDuration time) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTask>
+IFairSchedulerPtr<TTask> CreateFairScheduler();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define FAIR_SCHEDULER_INL_H_
+#include "fair_scheduler-inl.h"
+#undef FAIR_SCHEDULER_INL_H_
diff --git a/yt/yt/core/misc/farm_hash.h b/yt/yt/core/misc/farm_hash.h
new file mode 100644
index 0000000000..b945bd97a5
--- /dev/null
+++ b/yt/yt/core/misc/farm_hash.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/farmhash/farm_hash.h>
diff --git a/yt/yt/core/misc/fenwick_tree-inl.h b/yt/yt/core/misc/fenwick_tree-inl.h
new file mode 100644
index 0000000000..095f016591
--- /dev/null
+++ b/yt/yt/core/misc/fenwick_tree-inl.h
@@ -0,0 +1,173 @@
+#ifndef FENWICK_TREE_INL_H_
+#error "Direct inclusion of this file is not allowed, include fenwick_tree.h"
+// For the sake of sane code completion.
+#include "fenwick_tree.h"
+#endif
+
+#include "serialize.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * Fenwick tree is based on partial sum array FenwickSums_.
+ *
+ * FenwickSums_[i] contains a partial sum from index (i&(i+1)) to index i inclusive.
+ * Note that if i = xxx0111b, then (i&(i+1)) is xxx0000b.
+ * Thus FenwickSums_[i] contains a sum of elements whose index equals i except
+ for several least significant 1's of i.
+ *
+ * With FenwickSums_ we can calculate a prefix sum in O(log n):
+ * FenwickSums_[i] gives us a partial sum from i = xxx0111b down to j = xxx0000b.
+ * Let j = xx10000b, then j-1 is xx01111b. We add FenwickSums_[j-1] to the result and so on.
+ * Note that after each iteration the number of least significant zeros of j increases,
+ * so the complexity follows.
+ *
+ * To update an element with index i we need to update all FenwickSums_[j] which contain i.
+ * Let j be xxx0111b. Then xxx0 part must be exactly the same as in i. Thus to find such j
+ * we need to iteratively replace 0 by 1 in i starting from the least significant 0.
+ *
+ * To insert a new element with index i = xx01111b we add FenwickSums_[xx01110b],
+ * FenwickSums_[xx01101b], FenwickSums_[xx01011b] and FenwickSums_[xx00111b]
+ * with the new element, obtaining FenwickSums_[i].
+ */
+
+template <class TItem>
+void TFenwickTree<TItem>::PushBack(const TItem& item)
+{
+ int newItemIndex = Size();
+ YT_VERIFY(Size() <= MaxLength);
+ FenwickSums_.push_back(CalculateIncompleteFenwickSum(newItemIndex) + item);
+}
+
+template <class TItem>
+template <class... TArgs>
+void TFenwickTree<TItem>::EmplaceBack(TArgs&&... args)
+{
+ int newItemIndex = Size();
+ YT_VERIFY(Size() <= MaxLength);
+ FenwickSums_.push_back(CalculateIncompleteFenwickSum(newItemIndex) + TItem(std::forward<TArgs>(args)...));
+}
+
+template <class TItem>
+void TFenwickTree<TItem>::SetValue(int index, const TItem& item)
+{
+ YT_VERIFY(index >= 0);
+ YT_VERIFY(index < Size());
+ Increment(index, item - GetValue(index));
+}
+
+template <class TItem>
+void TFenwickTree<TItem>::PopBack()
+{
+ YT_VERIFY(Size() > 0);
+ FenwickSums_.pop_back();
+}
+
+template <class TItem>
+TItem TFenwickTree<TItem>::GetCumulativeSum(int index) const
+{
+ YT_VERIFY(index >= 0);
+ YT_VERIFY(index <= Size());
+
+ --index;
+ TItem result{};
+ while (index >= 0) {
+ result = result + FenwickSums_[index];
+ index = (index & (index + 1)) - 1;
+ }
+ return result;
+}
+
+template <class TItem>
+template <class TValue, class TComparer>
+int TFenwickTree<TItem>::LowerBound(const TValue& sum, TComparer&& comparer) const
+{
+ int resultIndex = 0;
+ TItem foundSum{};
+
+ if (!comparer(foundSum, sum)) {
+ return resultIndex;
+ }
+
+ int step = 1;
+ while (step <= Size()) {
+ step *= 2;
+ }
+
+ while (step >= 1) {
+ int newIndex = resultIndex + step - 1;
+ if (newIndex < Size() && comparer(foundSum + FenwickSums_[newIndex], sum)) {
+ foundSum = foundSum + FenwickSums_[newIndex];
+ resultIndex += step;
+ }
+ step /= 2;
+ }
+
+ return resultIndex + 1;
+}
+
+template <class TItem>
+template <class TValue, class TComparer>
+int TFenwickTree<TItem>::UpperBound(const TValue& sum, TComparer&& comparer) const
+{
+ return LowerBound(sum, [&comparer](const auto& lhs, const auto& rhs) {
+ return !comparer(rhs, lhs);
+ });
+}
+
+template <class TItem>
+i64 TFenwickTree<TItem>::Size() const
+{
+ return FenwickSums_.size();
+}
+
+template <class TItem>
+void TFenwickTree<TItem>::Clear()
+{
+ FenwickSums_.clear();
+}
+
+template <class TItem>
+void TFenwickTree<TItem>::Persist(const NYT::TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+ Persist(context, FenwickSums_);
+}
+
+template <class TItem>
+void TFenwickTree<TItem>::Increment(int index, const TItem& delta)
+{
+ while (index < Size()) {
+ FenwickSums_[index] = FenwickSums_[index] + delta;
+ index |= index + 1;
+ }
+}
+
+template <class TItem>
+TItem TFenwickTree<TItem>::CalculateIncompleteFenwickSum(int index) const
+{
+ YT_VERIFY(index >= 0);
+ YT_VERIFY(index <= Size());
+ TItem sum{};
+ int bit = 1;
+ while (index & bit) {
+ sum = sum + FenwickSums_[index - 1];
+ index ^= bit;
+ bit <<= 1;
+ }
+ return sum;
+}
+
+template <class TItem>
+TItem TFenwickTree<TItem>::GetValue(int index) const
+{
+ YT_VERIFY(index >= 0);
+ YT_VERIFY(index < Size());
+ return FenwickSums_[index] - CalculateIncompleteFenwickSum(index);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/fenwick_tree.h b/yt/yt/core/misc/fenwick_tree.h
new file mode 100644
index 0000000000..a6a142ec66
--- /dev/null
+++ b/yt/yt/core/misc/fenwick_tree.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A data structure for storing dynamic cumulative sums.
+//! NB: TItem::operator+ is assumed to be associative and commutative.
+template <class TItem>
+class TFenwickTree
+{
+public:
+ void PushBack(const TItem& item);
+
+ template <class... TArgs>
+ void EmplaceBack(TArgs&&... args);
+
+ //! |index| must be in [0, Size()).
+ void SetValue(int index, const TItem& item);
+
+ //! Add |delta| to the value at the position |index|.
+ //! |index| must be in [0, Size()).
+ void Increment(int index, const TItem& delta);
+
+ void PopBack();
+
+ //! Returns cumulative sum of all items in [0, |index|).
+ //! |index| must be in [0, Size()].
+ TItem GetCumulativeSum(int index) const;
+
+ //! Returns the smallest index such that the sum of items in [0, index) is not less than |sum|.
+ //! If the comparer is provided, returns the smallest index such that
+ //! comparer(sum([0, index), |sum|) is false.
+ //!
+ //! Is equivalent to std::lower_bound on the array of cumulative sums.
+ //!
+ //! NB: Works in assumption that all values are nonnegative.
+ template <class TValue, class TComparer = std::less<>>
+ int LowerBound(const TValue& sum, TComparer&& comparer = TComparer{}) const;
+
+ //! Returns the smallest index such that the sum of items in [0, index) is greater than |sum|.
+ //! If the comparer is provided, returns the smallest index such that
+ //! comparer(|sum|, sum([0, index)) is true.
+ //!
+ //! Is equivalent to std::upper_bound on the array of cumulative sums.
+ //!
+ //! NB: Works in assumption that all values are nonnegative.
+ template <class TValue, class TComparer = std::less<>>
+ int UpperBound(const TValue& sum, TComparer&& comparer = TComparer{}) const;
+
+ i64 Size() const;
+
+ void Clear();
+
+ void Persist(const NYT::TStreamPersistenceContext& context);
+
+private:
+ constexpr static ssize_t MaxLength = 1<<30;
+
+ std::vector<TItem> FenwickSums_;
+
+ //! Returns the value that would be stored in FenwickSums_[index] if only
+ //! values up to |index|, exclusive, were present.
+ TItem CalculateIncompleteFenwickSum(int index) const;
+
+ TItem GetValue(int index) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define FENWICK_TREE_INL_H_
+#include "fenwick_tree-inl.h"
+#undef FENWICK_TREE_INL_H_
diff --git a/yt/yt/core/misc/finally.h b/yt/yt/core/misc/finally.h
new file mode 100644
index 0000000000..fa6d098d40
--- /dev/null
+++ b/yt/yt/core/misc/finally.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <utility>
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A simple guard that executes a given function at the end of scope.
+
+template <class TCallback>
+class TFinallyGuard
+{
+public:
+ template <class T>
+ explicit TFinallyGuard(T&& finally)
+ : Finally_(std::forward<T>(finally))
+ { }
+
+ TFinallyGuard(TFinallyGuard&& guard)
+ : Released_(guard.Released_)
+ , Finally_(std::move(guard.Finally_))
+ {
+ guard.Release();
+ }
+
+ TFinallyGuard(const TFinallyGuard&) = delete;
+ TFinallyGuard& operator=(const TFinallyGuard&) = delete;
+ TFinallyGuard& operator=(TFinallyGuard&&) = delete;
+
+ void Release()
+ {
+ Released_ = true;
+ }
+
+ ~TFinallyGuard()
+ {
+ if (!Released_) {
+ Finally_();
+ }
+ }
+
+private:
+ bool Released_ = false;
+ TCallback Finally_;
+};
+
+template <class TCallback>
+[[nodiscard]] TFinallyGuard<typename std::decay<TCallback>::type> Finally(TCallback&& callback);
+
+template <class TCallback>
+TFinallyGuard<typename std::decay<TCallback>::type> Finally(TCallback&& callback)
+{
+ return TFinallyGuard<typename std::decay<TCallback>::type>(std::forward<TCallback>(callback));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/fs.cpp b/yt/yt/core/misc/fs.cpp
new file mode 100644
index 0000000000..794e17bc38
--- /dev/null
+++ b/yt/yt/core/misc/fs.cpp
@@ -0,0 +1,1165 @@
+#include "fs.h"
+#include "finally.h"
+
+#include <yt/yt/core/logging/log.h>
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <util/folder/dirut.h>
+#include <util/folder/iterator.h>
+#include <util/folder/filelist.h>
+#include <util/string/split.h>
+#include <util/system/shellcommand.h>
+
+#include <array>
+
+#if defined(_unix_)
+ #include <sys/mount.h>
+ #include <sys/stat.h>
+ #include <fcntl.h>
+#endif
+
+#if defined(_linux_)
+ #include <mntent.h>
+ #include <sys/vfs.h>
+ #include <sys/quota.h>
+ #include <sys/types.h>
+ #include <sys/sendfile.h>
+#elif defined(_freebsd_) || defined(_darwin_)
+ #include <sys/param.h>
+ #include <sys/mount.h>
+#elif defined (_win_)
+ #include <comutil.h>
+ #include <shlobj.h>
+#endif
+
+namespace NYT::NFS {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static inline const NLogging::TLogger Logger("FS");
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+[[maybe_unused]] void ThrowNotSupported()
+{
+ THROW_ERROR_EXCEPTION("Unsupported platform");
+}
+
+} // namespace
+
+bool Exists(const TString& path)
+{
+#ifdef _win32_
+ return GetFileAttributesA(path.data()) != 0xFFFFFFFF;
+#else
+ return access(path.data(), F_OK) == 0;
+#endif
+}
+
+bool IsDirEmpty(const TString& path)
+{
+ if (!IsDir(path)) {
+ THROW_ERROR_EXCEPTION("%v is not a directory",
+ path);
+ }
+
+ TDirIterator dir(path, TDirIterator::TOptions(FTS_NOSTAT));
+ for (auto it = dir.begin(); it != dir.end(); ++it) {
+ switch (it->fts_info) {
+ case FTS_F:
+ case FTS_DEFAULT:
+ case FTS_DP:
+ case FTS_SL:
+ case FTS_SLNONE:
+ if (it->fts_level > 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void Remove(const TString& path)
+{
+ bool ok;
+#ifdef _win_
+ ok = DeleteFileA(path.data());
+#else
+ struct stat sb;
+ ok = lstat(path.data(), &sb) == 0;
+ if (ok) {
+ if (S_ISDIR(sb.st_mode)) {
+ ok = rmdir(path.data()) == 0;
+ } else {
+ ok = remove(path.data()) == 0;
+ }
+ }
+#endif
+ if (!ok) {
+ THROW_ERROR_EXCEPTION("Cannot remove %v",
+ path)
+ << TError::FromSystem();
+ }
+}
+
+void Replace(const TString& source, const TString& destination)
+{
+ if (NFS::Exists(destination)) {
+ NFS::Remove(destination);
+ }
+ NFS::Rename(source, destination);
+}
+
+void RemoveRecursive(const TString& path)
+{
+ RemoveDirWithContents(path);
+}
+
+void Rename(const TString& source, const TString& destination)
+{
+ bool ok;
+#if defined(_win_)
+ ok = MoveFileEx(source.data(), destination.data(), MOVEFILE_REPLACE_EXISTING) != 0;
+#else
+ ok = rename(source.data(), destination.data()) == 0;
+#endif
+ if (!ok) {
+ THROW_ERROR_EXCEPTION("Cannot rename %v to %v",
+ source,
+ destination)
+ << TError::FromSystem();
+ }
+}
+
+TString GetFileName(const TString& path)
+{
+ size_t slashPosition = path.find_last_of(LOCSLASH_C);
+ if (slashPosition == TString::npos) {
+ return path;
+ }
+ return path.substr(slashPosition + 1);
+}
+
+TString GetDirectoryName(const TString& path)
+{
+ auto absPath = CombinePaths(NFs::CurrentWorkingDirectory(), path);
+ size_t slashPosition = absPath.find_last_of(LOCSLASH_C);
+ if (slashPosition == 0) {
+ // Root.
+ return TString(1, LOCSLASH_C);
+ } else {
+ return absPath.substr(0, slashPosition);
+ }
+}
+
+TString GetRealPath(const TString& path)
+{
+ auto curPath = CombinePaths(NFs::CurrentWorkingDirectory(), path);
+ std::vector<TString> parts;
+ while (!Exists(curPath)) {
+ auto filename = GetFileName(curPath);
+ if (filename == ".") {
+ // Do nothing.
+ } else if (filename == ".." || parts.empty() || parts.back() != "..") {
+ parts.push_back(filename);
+ } else {
+ parts.pop_back();
+ }
+ curPath = GetDirectoryName(curPath);
+ if (curPath.empty()) {
+ break;
+ }
+ }
+ if (!curPath.empty()) {
+ parts.push_back(RealPath(curPath));
+ } else {
+ parts.push_back(LOCSLASH_S);
+ }
+
+ Reverse(parts.begin(), parts.end());
+ return CombinePaths(parts);
+}
+
+bool IsPathRelativeAndInvolvesNoTraversal(const TString& path)
+{
+ auto normalizedPath = NormalizePathSeparators(path);
+ if (normalizedPath.StartsWith(LOCSLASH_C)) {
+ return false;
+ }
+
+ TStringBuf currentPath(normalizedPath);
+ int depth = 0;
+ while (!currentPath.empty()) {
+ size_t slashPosition = currentPath.find_first_of(LOCSLASH_C);
+ if (slashPosition == 0) {
+ currentPath = currentPath.substr(1);
+ continue;
+ }
+ auto part = slashPosition == TString::npos ? currentPath : currentPath.substr(0, slashPosition);
+ if (part == "..") {
+ --depth;
+ if (depth < 0) {
+ return false;
+ }
+ } else if (normalizedPath == ".") {
+ // Do nothing.
+ } else {
+ ++depth;
+ }
+ if (slashPosition == TString::npos) {
+ break;
+ }
+ currentPath = currentPath.substr(slashPosition + 1);
+ }
+ return true;
+}
+
+TString GetFileExtension(const TString& path)
+{
+ size_t dotPosition = path.find_last_of('.');
+ if (dotPosition == TString::npos) {
+ return "";
+ }
+ size_t slashPosition = path.find_last_of(LOCSLASH_C);
+ if (slashPosition != TString::npos && dotPosition < slashPosition) {
+ return "";
+ }
+ return path.substr(dotPosition + 1);
+}
+
+TString GetFileNameWithoutExtension(const TString& path)
+{
+ auto fileName = GetFileName(path);
+ size_t dotPosition = fileName.find_last_of('.');
+ if (dotPosition == TString::npos) {
+ return fileName;
+ }
+ return fileName.substr(0, dotPosition);
+}
+
+void CleanTempFiles(const TString& path)
+{
+ YT_LOG_INFO("Cleaning temp files in %v", path);
+
+ // TODO(ignat): specify suffix in EnumerateFiles.
+ auto entries = EnumerateFiles(path, std::numeric_limits<int>::max());
+ for (const auto& entry : entries) {
+ if (entry.EndsWith(TempFileSuffix)) {
+ auto fileName = NFS::CombinePaths(path, entry);
+ YT_LOG_DEBUG("Removing file (FileName: %v)",
+ fileName);
+ NFS::Remove(fileName);
+ }
+ }
+}
+
+std::vector<TString> EnumerateFiles(const TString& path, int depth)
+{
+ std::vector<TString> result;
+ if (NFS::Exists(path)) {
+ TFileList list;
+ list.Fill(path, TStringBuf(), TStringBuf(), depth);
+ int size = list.Size();
+ for (int i = 0; i < size; ++i) {
+ result.push_back(list.Next());
+ }
+ }
+ return result;
+}
+
+std::vector<TString> EnumerateDirectories(const TString& path, int depth)
+{
+ std::vector<TString> result;
+ if (NFS::Exists(path)) {
+ TDirsList list;
+ list.Fill(path, TStringBuf(), TStringBuf(), depth);
+ int size = list.Size();
+ for (int i = 0; i < size; ++i) {
+ result.push_back(list.Next());
+ }
+ }
+ return result;
+}
+
+TString GetRelativePath(const TString& from, const TString& to)
+{
+ std::vector<TString> tokensFrom;
+ StringSplitter(GetRealPath(from)).Split(LOCSLASH_C).Collect(&tokensFrom);
+ std::vector<TString> tokensTo;
+ StringSplitter(GetRealPath(to)).Split(LOCSLASH_C).Collect(&tokensTo);
+
+ int commonPrefixLength = 0;
+ while (commonPrefixLength < std::min(std::ssize(tokensFrom), std::ssize(tokensTo)) &&
+ tokensFrom[commonPrefixLength] == tokensTo[commonPrefixLength])
+ {
+ ++commonPrefixLength;
+ }
+
+ std::vector<TString> relativePathTokens;
+ relativePathTokens.reserve(tokensFrom.size() + tokensTo.size() - 2 * commonPrefixLength);
+ for (int index = 0; index < std::ssize(tokensFrom) - commonPrefixLength; ++index) {
+ relativePathTokens.push_back("..");
+ }
+ for (int index = commonPrefixLength; index < std::ssize(tokensTo); ++index) {
+ relativePathTokens.push_back(tokensTo[index]);
+ }
+
+ if (relativePathTokens.empty()) {
+ return ".";
+ }
+
+ return CombinePaths(relativePathTokens);
+}
+
+TString GetRelativePath(const TString& path)
+{
+ return GetRelativePath(NFs::CurrentWorkingDirectory(), path);
+}
+
+TString GetShortestPath(const TString& path)
+{
+ auto absolutePath = GetRealPath(path);
+ auto relativePath = GetRelativePath(path);
+ if (absolutePath.length() < relativePath.length()) {
+ return absolutePath;
+ } else {
+ return relativePath;
+ }
+}
+
+TDiskSpaceStatistics GetDiskSpaceStatistics(const TString& path)
+{
+ TDiskSpaceStatistics result;
+ bool ok;
+#ifdef _win_
+ ok = GetDiskFreeSpaceEx(
+ path.data(),
+ (PULARGE_INTEGER) &result.AvailableSpace,
+ (PULARGE_INTEGER) &result.TotalSpace,
+ (PULARGE_INTEGER) &result.FreeSpace) != 0;
+#else
+ struct statfs fsData;
+ ok = statfs(path.data(), &fsData) == 0;
+ result.TotalSpace = (i64) fsData.f_blocks * fsData.f_bsize;
+ result.AvailableSpace = (i64) fsData.f_bavail * fsData.f_bsize;
+ result.FreeSpace = (i64) fsData.f_bfree * fsData.f_bsize;
+#endif
+
+ if (!ok) {
+ THROW_ERROR_EXCEPTION("Failed to get disk space statistics for %v",
+ path)
+ << TError::FromSystem();
+ }
+
+ return result;
+}
+
+void MakeDirRecursive(const TString& path, int mode)
+{
+ MakePathIfNotExist(path.data(), mode);
+}
+
+TPathStatistics GetPathStatistics(const TString& path)
+{
+#ifdef _unix_
+ TPathStatistics statistics;
+
+ struct stat fileStat;
+ int result = ::stat(path.data(), &fileStat);
+
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Failed to get statistics for %v",
+ path)
+ << TError::FromSystem();
+ }
+
+ statistics.Size = static_cast<i64>(fileStat.st_size);
+ statistics.ModificationTime = TInstant::Seconds(fileStat.st_mtime);
+ statistics.AccessTime = TInstant::Seconds(fileStat.st_atime);
+ statistics.INode = fileStat.st_ino;
+ statistics.DeviceId = fileStat.st_dev;
+
+ return statistics;
+#else
+ ThrowNotSupported();
+ // Suppress clang's error about reaching end of non-void function without return.
+ Y_UNREACHABLE();
+#endif
+}
+
+i64 GetDirectorySize(const TString& path, bool ignoreUnavailableFiles, bool deduplicateByINodes, bool checkDeviceId)
+{
+ auto wrapNoEntryError = [&] (std::function<void()> func) {
+ try {
+ func();
+ } catch (const TSystemError& ex) { // For util functions.
+ if (ignoreUnavailableFiles && ex.Status() == ENOENT) {
+ // Do nothing
+ } else {
+ throw;
+ }
+ } catch (const TErrorException& ex) { // For YT functions.
+ if (ignoreUnavailableFiles && ex.Error().FindMatching(ELinuxErrorCode::NOENT)) {
+ // Do nothing
+ } else {
+ throw;
+ }
+ }
+ };
+
+ std::queue<TString> directories;
+ directories.push(path);
+
+ TPathStatistics rootDirStatistics;
+ wrapNoEntryError([&] {
+ rootDirStatistics = GetPathStatistics(path);
+ });
+
+ THashSet<ui64> visitedInodes;
+
+ i64 size = 0;
+
+
+ while (!directories.empty()) {
+ const auto& directory = directories.front();
+
+ wrapNoEntryError([&] {
+ auto subdirectories = EnumerateDirectories(directory);
+ for (const auto& subdirectory : subdirectories) {
+ directories.push(CombinePaths(directory, subdirectory));
+ }
+ });
+
+ std::vector<TString> files;
+ wrapNoEntryError([&] {
+ files = EnumerateFiles(directory);
+ });
+
+ for (const auto& file : files) {
+ wrapNoEntryError([&] {
+ auto fileStatistics = GetPathStatistics(CombinePaths(directory, file));
+ if (deduplicateByINodes) {
+ auto insertResult = visitedInodes.insert(fileStatistics.INode);
+ if (!insertResult.second) { // File already visited
+ return;
+ }
+ }
+ if (checkDeviceId && fileStatistics.DeviceId != rootDirStatistics.DeviceId) {
+ return;
+ }
+ if (fileStatistics.Size > 0) {
+ size += fileStatistics.Size;
+ }
+ });
+ }
+
+ directories.pop();
+ }
+
+ return size;
+}
+
+void Touch(const TString& path)
+{
+#ifdef _unix_
+ int result = ::utimes(path.data(), nullptr);
+ if (result != 0) {
+ THROW_ERROR_EXCEPTION("Failed to touch %v",
+ path)
+ << TError::FromSystem();
+ }
+#else
+ ThrowNotSupported();
+#endif
+}
+
+namespace {
+
+#ifdef _win_
+ const char PATH_DELIM = '\\';
+ const char PATH_DELIM2 = '/';
+#else
+ const char PATH_DELIM = '/';
+ const char PATH_DELIM2 = 0;
+#endif
+
+bool IsAbsolutePath(const TString& path)
+{
+ if (path.empty())
+ return false;
+ if (path[0] == PATH_DELIM)
+ return true;
+#ifdef _win_
+ if (path[0] == PATH_DELIM2)
+ return true;
+ if (path[0] > 0 && isalpha(path[0]) && path[1] == ':')
+ return true;
+#endif
+ return false;
+}
+
+} // namespace
+
+TString CombinePaths(const TString& path1, const TString& path2)
+{
+ return IsAbsolutePath(path2) ? NormalizePathSeparators(path2) : JoinPaths(path1, path2);
+}
+
+TString CombinePaths(const std::vector<TString>& paths)
+{
+ YT_VERIFY(!paths.empty());
+ if (paths.size() == 1) {
+ return paths[0];
+ }
+ auto result = CombinePaths(paths[0], paths[1]);
+ for (int index = 2; index < std::ssize(paths); ++index) {
+ result = CombinePaths(result, paths[index]);
+ }
+ return result;
+}
+
+TString JoinPaths(const TString& path1, const TString& path2)
+{
+ if (path1.empty())
+ return path2;
+ if (path2.empty())
+ return path1;
+
+ auto path = path1;
+ int delim = 0;
+ if (path1.back() == PATH_DELIM || path1.back() == PATH_DELIM2)
+ ++delim;
+ if (path2[0] == PATH_DELIM || path2[0] == PATH_DELIM2)
+ ++delim;
+ if (delim == 0)
+ path.append(1, PATH_DELIM);
+ path.append(path2, delim == 2 ? 1 : 0, TString::npos);
+ return NormalizePathSeparators(path);
+}
+
+TString NormalizePathSeparators(const TString& path)
+{
+#ifdef _unix_
+ constexpr char platformPathSeparator = '/';
+ constexpr char foreignPathSeparator = '\\';
+#else
+ constexpr char platformPathSeparator = '\\';
+ constexpr char foreignPathSeparator = '/';
+#endif
+ TString result;
+ result.reserve(path.length());
+ for (int i = 0; i < std::ssize(path); ++i) {
+ if (path[i] == foreignPathSeparator) {
+ result.append(platformPathSeparator);
+ } else {
+ result.append(path[i]);
+ }
+ }
+ return result;
+}
+
+void SetPermissions(const TString& path, int permissions)
+{
+#ifdef _linux_
+ auto res = HandleEintr(::chmod, path.data(), permissions);
+ if (res == -1) {
+ THROW_ERROR_EXCEPTION("Failed to set permissions for descriptor")
+ << TErrorAttribute("path", path)
+ << TErrorAttribute("permissions", permissions)
+ << TError::FromSystem();
+ }
+#else
+ Y_UNUSED(path, permissions);
+#endif
+}
+
+void SetPermissions(int fd, int permissions)
+{
+ const auto& procPath = Format("/proc/self/fd/%v", fd);
+ SetPermissions(procPath, permissions);
+}
+
+void MakeSymbolicLink(const TString& filePath, const TString& linkPath)
+{
+#ifdef _win_
+ // From MSDN: If the function succeeds, the return value is nonzero.
+ // If the function fails, the return value is zero. To get extended error information, call GetLastError.
+ bool ok = CreateSymbolicLink(linkPath.data(), filePath.data(), 0) != 0;
+#else
+ bool ok = symlink(filePath.data(), linkPath.data()) == 0;
+#endif
+
+ if (!ok) {
+ THROW_ERROR_EXCEPTION(
+ "Failed to link %v to %v",
+ filePath,
+ linkPath)
+ << TError::FromSystem();
+ }
+}
+
+bool AreInodesIdentical(const TString& lhsPath, const TString& rhsPath)
+{
+#ifdef _unix_
+ auto checkedStat = [] (const TString& path, struct stat* buffer) {
+ auto result = stat(path.data(), buffer);
+ if (result) {
+ THROW_ERROR_EXCEPTION(
+ "Failed to check for identical inodes: stat failed for %v",
+ path)
+ << TError::FromSystem();
+ }
+ };
+
+ struct stat lhsBuffer, rhsBuffer;
+ checkedStat(lhsPath, &lhsBuffer);
+ checkedStat(rhsPath, &rhsBuffer);
+
+ return
+ lhsBuffer.st_dev == rhsBuffer.st_dev &&
+ lhsBuffer.st_ino == rhsBuffer.st_ino;
+#else
+ return false;
+#endif
+}
+
+TString GetHomePath()
+{
+#ifdef _win_
+ std::array<char, 1024> buffer;
+ SHGetSpecialFolderPath(0, buffer.data(), CSIDL_PROFILE, 0);
+ return TString(buffer.data());
+#else
+ return std::getenv("HOME");
+#endif
+}
+
+void FlushDirectory(const TString& path)
+{
+#ifdef _unix_
+ int fd = ::open(path.data(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd < 0) {
+ THROW_ERROR_EXCEPTION("Failed to open directory %v", path)
+ << TError::FromSystem();
+ }
+
+ int result = ::fsync(fd);
+ if (result < 0) {
+ SafeClose(fd, false);
+ THROW_ERROR_EXCEPTION("Failed to flush directory %v", path)
+ << TError::FromSystem();
+ }
+
+ SafeClose(fd, false);
+#else
+ // No-op.
+#endif
+}
+
+std::vector<TMountPoint> GetMountPoints(const TString& mountsFile)
+{
+#ifdef _linux_
+ std::unique_ptr<FILE, decltype(&endmntent)> file(::setmntent(mountsFile.data(), "r"), endmntent);
+
+ if (!file.get()) {
+ THROW_ERROR_EXCEPTION("Failed to open mounts file %v", mountsFile);
+ }
+
+ std::vector<TMountPoint> mountPoints;
+
+ ::mntent* entry;
+ while ((entry = getmntent(file.get()))) {
+ TMountPoint point;
+ point.Name = entry->mnt_fsname;
+ point.Path = entry->mnt_dir;
+ mountPoints.push_back(point);
+ }
+
+ return mountPoints;
+#else
+ Y_UNUSED(mountsFile);
+ ThrowNotSupported();
+ YT_ABORT();
+#endif
+}
+
+void MountTmpfs(const TString& path, int userId, i64 size)
+{
+#ifdef _linux_
+ auto opts = Format("mode=0777,uid=%v,size=%v", userId, size);
+ int result = ::mount("none", path.data(), "tmpfs", 0, opts.data());
+ if (result < 0) {
+ THROW_ERROR_EXCEPTION("Failed to mount tmpfs at %v", path)
+ << TErrorAttribute("user_id", userId)
+ << TErrorAttribute("size", size)
+ << TError::FromSystem();
+ }
+#else
+ Y_UNUSED(path, userId, size);
+ ThrowNotSupported();
+ YT_ABORT();
+#endif
+}
+
+void Umount(const TString& path, bool detach)
+{
+#ifdef _linux_
+ int flags = 0;
+ if (detach) {
+ flags |= MNT_DETACH;
+ }
+ int result = ::umount2(path.data(), flags);
+ // EINVAL for ::umount means that nothing mounted at this point.
+ // ENOENT means 'No such file or directory'.
+ if (result < 0 && LastSystemError() != EINVAL && LastSystemError() != ENOENT) {
+ auto error = TError("Failed to umount %v", path)
+ << TError::FromSystem();
+ if (LastSystemError() == EBUSY) {
+ error = AttachLsofOutput(error, path);
+ error = AttachFindOutput(error, path);
+ }
+ THROW_ERROR error;
+ }
+
+#else
+ Y_UNUSED(path, detach);
+ ThrowNotSupported();
+#endif
+}
+
+struct stat Stat(TStringBuf path)
+{
+ struct stat statInfo;
+ int result = ::stat(path.data(), &statInfo);
+ if (result != 0) {
+ THROW_ERROR_EXCEPTION("Failed to execute ::stat for %v", path)
+ << TError::FromSystem();
+ }
+ return statInfo;
+}
+
+i64 GetBlockSize(TStringBuf device)
+{
+#ifdef _unix_
+ struct stat statInfo = Stat(device);
+ return static_cast<i64>(statInfo.st_blksize);
+#else
+ ThrowNotSupported();
+ Y_UNREACHABLE();
+#endif
+}
+
+TString GetFilesystemName(TStringBuf path)
+{
+ struct stat statInfo = Stat(path);
+ auto dev = statInfo.st_dev;
+
+ for (const auto& mountPoint : GetMountPoints()) {
+ struct stat currentStatInfo;
+ if (::stat(mountPoint.Path.c_str(), &currentStatInfo) != 0) {
+ continue;
+ }
+
+ if (currentStatInfo.st_dev == dev) {
+ return mountPoint.Name;
+ }
+ }
+
+ THROW_ERROR_EXCEPTION("Failed to find mount point for %v", path);
+}
+
+void SetQuota(
+ int userId,
+ TStringBuf path,
+ std::optional<i64> diskSpaceLimit,
+ std::optional<i64> inodeLimit)
+{
+#ifdef _linux_
+ dqblk info;
+ const i64 blockSize = GetBlockSize(path);
+ const auto filesystem = GetFilesystemName(path);
+ u_int32_t flags = 0;
+ if (diskSpaceLimit) {
+ const auto diskSpaceLimitValue = (*diskSpaceLimit + blockSize - 1) / blockSize;
+ info.dqb_bhardlimit = static_cast<u_int64_t>(diskSpaceLimitValue);
+ info.dqb_bsoftlimit = info.dqb_bhardlimit;
+ flags |= QIF_BLIMITS;
+ }
+ if (inodeLimit) {
+ info.dqb_ihardlimit = static_cast<u_int64_t>(*inodeLimit);
+ info.dqb_isoftlimit = info.dqb_ihardlimit;
+ flags |= QIF_ILIMITS;
+ }
+ info.dqb_valid = flags;
+ int result = ::quotactl(
+ QCMD(Q_SETQUOTA, USRQUOTA),
+ filesystem.c_str(),
+ userId,
+ reinterpret_cast<caddr_t>(&info));
+ if (result < 0) {
+ THROW_ERROR_EXCEPTION("Failed to set FS quota for user")
+ << TErrorAttribute("user_id", userId)
+ << TErrorAttribute("disk_space_limit", diskSpaceLimit.value_or(0))
+ << TErrorAttribute("inode_limit", inodeLimit.value_or(0))
+ << TErrorAttribute("path", path)
+ << TError::FromSystem();
+ }
+#else
+ Y_UNUSED(userId, path, diskSpaceLimit, inodeLimit);
+ ThrowNotSupported();
+#endif
+}
+
+void WrapIOErrors(std::function<void()> func)
+{
+ try {
+ func();
+ } catch (const TSystemError& ex) {
+ auto status = ex.Status();
+ switch (status) {
+ case ENOMEM:
+ fprintf(stderr, "Out-of-memory condition detected during I/O operation; terminating\n");
+ _exit(9);
+ break;
+
+ case EIO:
+ case ENOSPC:
+ case EROFS:
+ case EWOULDBLOCK: // aka EAGAIN
+#ifdef _linux_
+ case EUCLEAN:
+#endif
+ THROW_ERROR_EXCEPTION(NFS::EErrorCode::IOError, "I/O error")
+ << TErrorAttribute("status", status)
+ << TError(ex);
+
+ default: {
+ TError error(ex);
+ YT_LOG_FATAL(error, "Unexpected exception thrown during I/O operation");
+ break;
+ }
+ }
+ }
+}
+
+void Chmod(const TString& path, int mode)
+{
+#ifdef _linux_
+ int result = ::Chmod(path.data(), mode);
+ if (result < 0) {
+ THROW_ERROR_EXCEPTION("Failed to change mode of %v", path)
+ << TErrorAttribute("mode", Format("%04o", mode))
+ << TError::FromSystem();
+ }
+#else
+ Y_UNUSED(path, mode);
+ ThrowNotSupported();
+#endif
+}
+
+void SendfileChunkedCopy(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ try {
+ TFile src(existingPath, OpenExisting | RdOnly | Seq | CloseOnExec);
+ TFile dst(newPath, CreateAlways | WrOnly | Seq | CloseOnExec);
+ dst.Flock(LOCK_EX);
+ SendfileChunkedCopy(src, dst, chunkSize);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to copy %v to %v",
+ existingPath,
+ newPath)
+ << ex;
+ }
+#else
+ Y_UNUSED(existingPath, newPath, chunkSize);
+ ThrowNotSupported();
+#endif
+}
+
+void SendfileChunkedCopy(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ try {
+ i64 srcSize = source.GetLength();
+ if (srcSize == -1) {
+ THROW_ERROR_EXCEPTION("Cannot get source file length: stat failed for %v",
+ destination.GetName())
+ << TError::FromSystem();
+ }
+
+ int srcFd = source.GetHandle();
+ int dstFd = destination.GetHandle();
+
+ while (true) {
+ i64 currentChunkSize = 0;
+ while (currentChunkSize < chunkSize && srcSize > 0) {
+ auto size = sendfile(dstFd, srcFd, nullptr, chunkSize);
+ if (size == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing chunked copy: sendfile failed")
+ << TError::FromSystem();
+ }
+ currentChunkSize += size;
+ srcSize -= size;
+ }
+
+ if (srcSize == 0) {
+ break;
+ }
+
+ NConcurrency::Yield();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to copy %v to %v",
+ source.GetName(),
+ destination.GetName())
+ << ex;
+ }
+#else
+ Y_UNUSED(source, destination, chunkSize);
+ ThrowNotSupported();
+#endif
+}
+
+TFuture<void> ReadBuffer(
+ int fromFd,
+ int toFd,
+ std::vector<ui8> buffer,
+ int bufferSize)
+{
+ YT_VERIFY(bufferSize);
+
+ auto readSize = read(fromFd, buffer.data(), bufferSize);
+
+ if (readSize == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing read")
+ << TError::FromSystem();
+ }
+
+ if (readSize == 0) {
+ return VoidFuture;
+ }
+
+ return BIND(&WriteBuffer)
+ .AsyncVia(GetCurrentInvoker())
+ .Run(fromFd, toFd, std::move(buffer), bufferSize, readSize);
+}
+
+TFuture<void> WriteBuffer(
+ int fromFd,
+ int toFd,
+ std::vector<ui8> buffer,
+ int bufferSize,
+ int readSize)
+{
+ YT_VERIFY(readSize);
+ YT_VERIFY(bufferSize);
+
+ auto size = write(toFd, buffer.data(), readSize);
+
+ if (size == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing write")
+ << TError::FromSystem();
+ }
+
+ return BIND(&ReadBuffer)
+ .AsyncVia(GetCurrentInvoker())
+ .Run(fromFd, toFd, std::move(buffer), bufferSize);
+}
+
+TFuture<void> ReadWriteCopyAsync(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ try {
+ TFile src(existingPath, OpenExisting | RdOnly | Seq | CloseOnExec);
+ TFile dst(newPath, CreateAlways | WrOnly | Seq | CloseOnExec);
+ dst.Flock(LOCK_EX);
+ return ReadWriteCopyAsync(src, dst, chunkSize);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to copy %v to %v",
+ existingPath,
+ newPath)
+ << ex;
+ }
+#else
+ Y_UNUSED(existingPath, newPath, chunkSize);
+ ThrowNotSupported();
+ return VoidFuture;
+#endif
+}
+
+TFuture<void> ReadWriteCopyAsync(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ int srcFd = source.GetHandle();
+ int dstFd = destination.GetHandle();
+ std::vector<ui8> buffer(chunkSize);
+
+ return ReadBuffer(srcFd, dstFd, std::move(buffer), chunkSize)
+ .Apply(BIND([=] (const TErrorOr<void>& result) {
+ THROW_ERROR_EXCEPTION_IF_FAILED(result,
+ TError("Failed to copy %v to %v",
+ source.GetName(),
+ destination.GetName()));
+ }));
+#else
+ Y_UNUSED(source, destination, chunkSize);
+ ThrowNotSupported();
+ return VoidFuture;
+#endif
+}
+
+void ReadWriteCopySync(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ try {
+ TFile src(existingPath, OpenExisting | RdOnly | Seq | CloseOnExec);
+ TFile dst(newPath, CreateAlways | WrOnly | Seq | CloseOnExec);
+ dst.Flock(LOCK_EX);
+ ReadWriteCopySync(src, dst, chunkSize);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to copy %v to %v",
+ existingPath,
+ newPath)
+ << ex;
+ }
+#else
+ Y_UNUSED(existingPath, newPath, chunkSize);
+ ThrowNotSupported();
+#endif
+}
+
+void ReadWriteCopySync(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ int srcFd = source.GetHandle();
+ int dstFd = destination.GetHandle();
+ std::vector<ui8> buffer(chunkSize);
+
+ while (true) {
+ auto readSize = read(srcFd, buffer.data(), chunkSize);
+
+ if (readSize == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing read")
+ << TError::FromSystem();
+ }
+
+ if (readSize == 0) {
+ return;
+ }
+
+ auto size = write(dstFd, buffer.data(), readSize);
+
+ if (size == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing write")
+ << TError::FromSystem();
+ }
+ }
+#else
+ Y_UNUSED(source, destination, chunkSize);
+ ThrowNotSupported();
+#endif
+}
+
+void Splice(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize)
+{
+#ifdef _linux_
+ try {
+ int srcFd = source.GetHandle();
+ int dstFd = destination.GetHandle();
+
+ loff_t offset = 0;
+
+ bool completed = false;
+ while (!completed) {
+ i64 currentChunkSize = 0;
+ while (currentChunkSize < chunkSize) {
+ auto size = splice(srcFd, nullptr, dstFd, &offset, chunkSize, SPLICE_F_MOVE | SPLICE_F_MORE);
+ if (size == -1) {
+ THROW_ERROR_EXCEPTION("Error while doing splice")
+ << TErrorAttribute("source_path", source.GetName())
+ << TErrorAttribute("destination_path", destination.GetName())
+ << TError::FromSystem();
+ } else if (size == 0) {
+ completed = true;
+ break;
+ } else {
+ currentChunkSize += size;
+ }
+ }
+
+ NConcurrency::Yield();
+ }
+
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Failed to copy %v to %v via splice",
+ source.GetName(),
+ destination.GetName())
+ << ex;
+ }
+#else
+ Y_UNUSED(source, destination, chunkSize);
+ ThrowNotSupported();
+#endif
+}
+
+TError AttachLsofOutput(TError error, const TString& path)
+{
+ auto lsofOutput = TShellCommand("lsof", {path})
+ .Run()
+ .Wait()
+ .GetOutput();
+ return error
+ << TErrorAttribute("lsof_output", lsofOutput);
+}
+
+TError AttachFindOutput(TError error, const TString& path)
+{
+ auto findOutput = TShellCommand("find", {path, "-name", "*"})
+ .Run()
+ .Wait()
+ .GetOutput();
+ return error
+ << TErrorAttribute("find_output", findOutput);
+}
+
+int GetDeviceId(const TString& path)
+{
+#ifdef _linux_
+ return Stat(path).st_dev;
+#else
+ Y_UNUSED(path);
+ YT_UNIMPLEMENTED();
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFS
diff --git a/yt/yt/core/misc/fs.h b/yt/yt/core/misc/fs.h
new file mode 100644
index 0000000000..2a5779ad8d
--- /dev/null
+++ b/yt/yt/core/misc/fs.h
@@ -0,0 +1,242 @@
+#pragma once
+
+/*!
+ * \file fs.h
+ * \brief File system functions
+ */
+
+#include "common.h"
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/system/file.h>
+
+namespace NYT::NFS {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((IOError)(19000))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! File suffix for temporary files.
+constexpr auto TempFileSuffix = TStringBuf("~");
+
+//! Returns |true| if a given path points to an existing file or directory.
+bool Exists(const TString& path);
+
+//! Returns |true| if a given path to an empty directory.
+bool IsDirEmpty(const TString& path);
+
+//! Removes a given file or directory.
+void Remove(const TString& path);
+
+//! Removes #destination if it exists. Then renames #destination into #source.
+void Replace(const TString& source, const TString& destination);
+
+//! Removes a given directory recursively.
+void RemoveRecursive(const TString& path);
+
+//! Renames a given file or directory.
+void Rename(const TString& source, const TString& destination);
+
+//! Returns name of file.
+TString GetFileName(const TString& path);
+
+//! Returns extension of file.
+TString GetFileExtension(const TString& path);
+
+//! Returns name of file without extension.
+TString GetFileNameWithoutExtension(const TString& path);
+
+//! Returns path of directory containing the file.
+TString GetDirectoryName(const TString& path);
+
+//! Returns the absolute path for the given (possibly relative) path.
+TString GetRealPath(const TString& path);
+
+//! Checks that given path is relative and points somewhere inside the root directory.
+bool IsPathRelativeAndInvolvesNoTraversal(const TString& path);
+
+//! Combines two strings into a path. Returns second path if it is absolute.
+TString CombinePaths(const TString& path1, const TString& path2);
+
+//! Appends second path to the first one, handling delimiters.
+TString JoinPaths(const TString& path1, const TString& path2);
+
+//! Combines a bunch of strings into a path.
+TString CombinePaths(const std::vector<TString>& paths);
+
+//! Deletes all files with extension #TempFileSuffix in a given directory.
+void CleanTempFiles(const TString& path);
+
+//! Returns all files in a given directory.
+std::vector<TString> EnumerateFiles(const TString& path, int depth = 1);
+
+//! Returns all directories in a given directory.
+std::vector<TString> EnumerateDirectories(const TString& path, int depth = 1);
+
+//! Returns path to `to` relative to `from`.
+TString GetRelativePath(const TString& from, const TString& to);
+
+//! Returns path to `path` relative to working directory.
+TString GetRelativePath(const TString& path);
+
+//! Returns the shortest among absolute and relative to working directory path to `path`.
+TString GetShortestPath(const TString& path);
+
+//! Describes total, free, and available space on a disk drive.
+struct TDiskSpaceStatistics
+{
+ i64 TotalSpace;
+ i64 FreeSpace;
+ i64 AvailableSpace;
+};
+
+//! Computes the space statistics for disk drive containing #path.
+TDiskSpaceStatistics GetDiskSpaceStatistics(const TString& path);
+
+//! Creates the #path and parent directories if they don't exists.
+void MakeDirRecursive(const TString& path, int mode = 0777);
+
+struct TPathStatistics
+{
+ i64 Size = -1;
+ ui64 INode;
+ int DeviceId;
+ TInstant ModificationTime;
+ TInstant AccessTime;
+};
+
+//! Returns the path statistics.
+TPathStatistics GetPathStatistics(const TString& path);
+
+//! Recursively calculates size of all regular files inside the directory.
+i64 GetDirectorySize(
+ const TString& path,
+ bool ignoreUnavailableFiles = true,
+ bool deduplicateByINodes = false,
+ bool checkDeviceId = false);
+
+//! Sets the access and modification times to now.
+void Touch(const TString& path);
+
+//! Converts all path separators to platform path separators.
+TString NormalizePathSeparators(const TString& path);
+
+//! Sets permissions for a file.
+void SetPermissions(const TString& path, int permissions);
+
+//! Sets permissions for an fd.
+void SetPermissions(int fd, int permissions);
+
+//! Makes a symbolic link on file #fileName with #linkName.
+void MakeSymbolicLink(const TString& filePath, const TString& linkPath);
+
+//! Returns |true| if given paths refer to the same inode.
+//! Always returns |false| under Windows.
+bool AreInodesIdentical(const TString& lhsPath, const TString& rhsPath);
+
+//! Returns the home directory of the current user.
+//! Interestingly, implemented for both Windows and *nix.
+TString GetHomePath();
+
+//! Flushes the directory's metadata. Useful for, e.g., committing renames happened in #path.
+void FlushDirectory(const TString& path);
+
+struct TMountPoint
+{
+ TString Name;
+ TString Path;
+};
+
+std::vector<TMountPoint> GetMountPoints(const TString& mountsFile = "/proc/mounts");
+
+//! Mount tmpfs at given path.
+void MountTmpfs(const TString& path, int userId, i64 size);
+
+//! Unmount given path.
+void Umount(const TString& path, bool detach);
+
+//! Set disk space and inodes quota for given user on filesystem determined by pathInFs.
+//! The filesystem must be mounted with quotas enabled.
+void SetQuota(
+ int userId,
+ TStringBuf path,
+ std::optional<i64> diskSpaceLimit,
+ std::optional<i64> inodeLimit);
+
+//! Wraps a given #func in with try/catch; makes sure that only IO-related
+//! exceptions are being thrown. For all other exceptions, immediately terminates
+//! with fatal error.
+void WrapIOErrors(std::function<void()> func);
+
+//! Sets a given mode on the path.
+void Chmod(const TString& path, int mode);
+
+//! Copies file chunk after chunk, releasing thread between chunks.
+void SendfileChunkedCopy(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize);
+
+void SendfileChunkedCopy(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize);
+
+TFuture<void> ReadBuffer(
+ int fromFd,
+ int toFd,
+ std::vector<ui8> buffer,
+ int bufferSize);
+
+TFuture<void> WriteBuffer(
+ int fromFd,
+ int toFd,
+ std::vector<ui8> buffer,
+ int bufferSize,
+ int readSize);
+
+TFuture<void> ReadWriteCopyAsync(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize);
+
+TFuture<void> ReadWriteCopyAsync(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize);
+
+void ReadWriteCopySync(
+ const TString& existingPath,
+ const TString& newPath,
+ i64 chunkSize);
+
+void ReadWriteCopySync(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize);
+
+//! Copies file chunk after chunk via splice syscall,
+//! releasing thread between chunks.
+void Splice(
+ const TFile& source,
+ const TFile& destination,
+ i64 chunkSize);
+
+TError AttachLsofOutput(TError error, const TString& path);
+TError AttachFindOutput(TError error, const TString& path);
+
+//! Returns id of device path belongs to.
+int GetDeviceId(const TString& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NFS
diff --git a/yt/yt/core/misc/guid-inl.h b/yt/yt/core/misc/guid-inl.h
new file mode 100644
index 0000000000..6002a1cfa7
--- /dev/null
+++ b/yt/yt/core/misc/guid-inl.h
@@ -0,0 +1,35 @@
+#ifndef GUID_INL_H_
+#error "Direct inclusion of this file is not allowed, include guid.h"
+// For the sake of sane code completion.
+#include "guid.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void ToProto(NProto::TGuid* protoGuid, TGuid guid)
+{
+ protoGuid->set_first(guid.Parts64[0]);
+ protoGuid->set_second(guid.Parts64[1]);
+}
+
+Y_FORCE_INLINE void FromProto(TGuid* guid, const NYT::NProto::TGuid& protoGuid)
+{
+ guid->Parts64[0] = protoGuid.first();
+ guid->Parts64[1] = protoGuid.second();
+}
+
+Y_FORCE_INLINE void ToProto(TProtoStringType* protoGuid, TGuid guid)
+{
+ *protoGuid = guid ? ToString(guid) : TString();
+}
+
+Y_FORCE_INLINE void FromProto(TGuid* guid, const TProtoStringType& protoGuid)
+{
+ *guid = protoGuid ? TGuid::FromString(protoGuid) : TGuid();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/guid.cpp b/yt/yt/core/misc/guid.cpp
new file mode 100644
index 0000000000..9e58c8bbcc
--- /dev/null
+++ b/yt/yt/core/misc/guid.cpp
@@ -0,0 +1,16 @@
+#include "guid.h"
+
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NYTree::Serialize;
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_REPRESENTATION(NProto::TGuid, TGuid)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/guid.h b/yt/yt/core/misc/guid.h
new file mode 100644
index 0000000000..66ab218880
--- /dev/null
+++ b/yt/yt/core/misc/guid.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt_proto/yt/core/misc/proto/guid.pb.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <library/cpp/yt/string/guid.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TGuid* protoGuid, TGuid guid);
+void FromProto(TGuid* guid, const NProto::TGuid& protoGuid);
+
+void ToProto(TProtoStringType* protoGuid, TGuid guid);
+void FromProto(TGuid* guid, const TProtoStringType& protoGuid);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define GUID_INL_H_
+#include "guid-inl.h"
+#undef GUID_INL_H_
diff --git a/yt/yt/core/misc/hazard_ptr-inl.h b/yt/yt/core/misc/hazard_ptr-inl.h
new file mode 100644
index 0000000000..4e5a4fc04c
--- /dev/null
+++ b/yt/yt/core/misc/hazard_ptr-inl.h
@@ -0,0 +1,192 @@
+#ifndef HAZARD_PTR_INL_H_
+#error "Direct inclusion of this file is not allowed, include hazard_ptr.h"
+// For the sake of sane code completion.
+#include "hazard_ptr.h"
+#endif
+#undef HAZARD_PTR_INL_H_
+
+#include <array>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRefCountedBase;
+
+namespace NDetail {
+
+constexpr int MaxHazardPointersPerThread = 2;
+using THazardPointerSet = std::array<std::atomic<void*>, MaxHazardPointersPerThread>;
+
+extern thread_local THazardPointerSet HazardPointers;
+
+struct THazardThreadState;
+extern thread_local THazardThreadState* HazardThreadState;
+
+void InitHazardThreadState();
+
+template <class T, bool = std::is_base_of_v<TRefCountedBase, T>>
+struct THazardPtrTraits
+{
+ Y_FORCE_INLINE static void* GetBasePtr(T* object)
+ {
+ return object;
+ }
+};
+
+template <class T>
+struct THazardPtrTraits<T, true>
+{
+ Y_FORCE_INLINE static void* GetBasePtr(TRefCountedBase* object)
+ {
+ return object;
+ }
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class TReclaimer>
+void RetireHazardPointer(T* ptr, TReclaimer /*reclaimer*/)
+{
+ RetireHazardPointer(
+ reinterpret_cast<TPackedPtr>(ptr),
+ [] (TPackedPtr packedPtr) { TReclaimer()(reinterpret_cast<T*>(packedPtr)); });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+THazardPtr<T>::THazardPtr(THazardPtr&& other)
+ : Ptr_(other.Ptr_)
+ , HazardPtr_(other.HazardPtr_)
+{
+ other.Ptr_ = nullptr;
+ other.HazardPtr_ = nullptr;
+}
+
+template <class T>
+THazardPtr<T>& THazardPtr<T>::operator=(THazardPtr&& other)
+{
+ if (this != &other) {
+ Reset();
+ Ptr_ = other.Ptr_;
+ HazardPtr_ = other.HazardPtr_;
+ other.Ptr_ = nullptr;
+ other.HazardPtr_ = nullptr;
+ }
+ return *this;
+}
+
+template <class T>
+template <class TPtrLoader>
+THazardPtr<T> THazardPtr<T>::Acquire(TPtrLoader&& ptrLoader, T* ptr)
+{
+ if (!ptr) {
+ return {};
+ }
+
+ auto* hazardPtr = [] {
+ for (auto it = NYT::NDetail::HazardPointers.begin(); it != NYT::NDetail::HazardPointers.end(); ++it) {
+ auto& ptr = *it;
+ if (!ptr.load(std::memory_order::relaxed)) {
+ return &ptr;
+ }
+ }
+ // Too many hazard pointers are being used in a single thread concurrently.
+ // Try increasing MaxHazardPointersPerThread.
+ YT_ABORT();
+ }();
+
+ if (Y_UNLIKELY(!NYT::NDetail::HazardThreadState)) {
+ NYT::NDetail::InitHazardThreadState();
+ }
+
+ void* checkPtr;
+ do {
+ hazardPtr->store(NYT::NDetail::THazardPtrTraits<T>::GetBasePtr(ptr), std::memory_order::release);
+ std::atomic_thread_fence(std::memory_order::seq_cst);
+ checkPtr = ptr;
+ ptr = ptrLoader();
+ } while (ptr != checkPtr);
+
+ return THazardPtr(ptr, hazardPtr);
+}
+
+template <class T>
+template <class TPtrLoader>
+THazardPtr<T> THazardPtr<T>::Acquire(TPtrLoader&& ptrLoader)
+{
+ return Acquire(std::forward<TPtrLoader>(ptrLoader), ptrLoader());
+}
+
+template <class T>
+void THazardPtr<T>::Reset()
+{
+ if (Ptr_) {
+#ifdef NDEBUG
+ HazardPtr_->store(nullptr, std::memory_order::release);
+#else
+ YT_VERIFY(HazardPtr_->exchange(nullptr) == NYT::NDetail::THazardPtrTraits<T>::GetBasePtr(Ptr_));
+#endif
+ Ptr_ = nullptr;
+ HazardPtr_ = nullptr;
+ }
+}
+
+template <class T>
+THazardPtr<T>::~THazardPtr()
+{
+ Reset();
+}
+
+template <class T>
+T* THazardPtr<T>::Get() const
+{
+ return Ptr_;
+}
+
+template <class T>
+T& THazardPtr<T>::operator*() const
+{
+ YT_ASSERT(Ptr_);
+ return *Ptr_;
+}
+
+template <class T>
+T* THazardPtr<T>::operator->() const
+{
+ YT_ASSERT(Ptr_);
+ return Ptr_;
+}
+
+template <class T>
+THazardPtr<T>::operator bool() const
+{
+ return Ptr_ != nullptr;
+}
+
+template <class T>
+THazardPtr<T>::THazardPtr(T* ptr, std::atomic<void*>* hazardPtr)
+ : Ptr_(ptr)
+ , HazardPtr_(hazardPtr)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class U>
+bool operator==(const THazardPtr<U>& lhs, const U* rhs)
+{
+ return lhs.Get() == rhs;
+}
+
+template <class U>
+bool operator!=(const THazardPtr<U>& lhs, const U* rhs)
+{
+ return lhs.Get() != rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/hazard_ptr.cpp b/yt/yt/core/misc/hazard_ptr.cpp
new file mode 100644
index 0000000000..0c52c68bd4
--- /dev/null
+++ b/yt/yt/core/misc/hazard_ptr.cpp
@@ -0,0 +1,447 @@
+#include "hazard_ptr.h"
+
+#include <yt/yt/core/misc/singleton.h>
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/ring_queue.h>
+#include <yt/yt/core/misc/shutdown.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <library/cpp/yt/threading/at_fork.h>
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+#include <library/cpp/yt/containers/intrusive_linked_list.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/yt/memory/free_list.h>
+
+namespace NYT {
+
+using namespace NConcurrency;
+
+/////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger LockFreePtrLogger("LockFree");
+static const auto& Logger = LockFreePtrLogger;
+
+////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////
+
+thread_local THazardPointerSet HazardPointers;
+
+//! A simple container based on free list which supports only Enqueue and DequeueAll.
+template <class T>
+class TRetireQueue
+{
+private:
+ struct TNode
+ : public TFreeListItemBase<TNode>
+ {
+ T Value;
+
+ TNode() = default;
+
+ explicit TNode(T&& value)
+ : Value(std::move(value))
+ { }
+ };
+
+ TFreeList<TNode> Impl_;
+
+ void EraseList(TNode* node)
+ {
+ while (node) {
+ auto* next = node->Next.load(std::memory_order::acquire);
+ delete node;
+ node = next;
+ }
+ }
+
+public:
+ ~TRetireQueue()
+ {
+ EraseList(Impl_.ExtractAll());
+ }
+
+ template <typename TCallback>
+ void DequeueAll(TCallback&& callback)
+ {
+ auto* head = Impl_.ExtractAll();
+
+ auto cleanup = Finally([this, head] {
+ EraseList(head);
+ });
+
+ auto* ptr = head;
+ while (ptr) {
+ callback(ptr->Value);
+ ptr = ptr->Next;
+ }
+ }
+
+ void Enqueue(T&& value)
+ {
+ Impl_.Put(new TNode(std::forward<T>(value)));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRetiredPtr
+{
+ TPackedPtr PackedPtr;
+ THazardPtrReclaimer Reclaimer;
+};
+
+struct THazardThreadState
+{
+ THazardPointerSet* const HazardPointers;
+
+ TIntrusiveLinkedListNode<THazardThreadState> RegistryNode;
+ TRingQueue<TRetiredPtr> RetireList;
+ TCompactVector<void*, 64> ProtectedPointers;
+ bool Reclaiming = false;
+
+ explicit THazardThreadState(THazardPointerSet* hazardPointers)
+ : HazardPointers(hazardPointers)
+ { }
+};
+
+thread_local THazardThreadState* HazardThreadState;
+thread_local bool HazardThreadStateDestroyed;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THazardPointerManager
+{
+public:
+ struct THazardThreadStateToRegistryNode
+ {
+ auto operator() (THazardThreadState* state) const
+ {
+ return &state->RegistryNode;
+ }
+ };
+
+ static THazardPointerManager* Get()
+ {
+ return LeakySingleton<THazardPointerManager>();
+ }
+
+ void InitThreadState();
+
+ void RetireHazardPointer(TPackedPtr packedPtr, THazardPtrReclaimer reclaimer);
+
+ void ReclaimHazardPointers(bool flush);
+
+private:
+ std::atomic<int> ThreadCount_ = 0;
+
+ TRetireQueue<TRetiredPtr> RetireQueue_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, ThreadRegistryLock_);
+ TIntrusiveLinkedList<THazardThreadState, THazardThreadStateToRegistryNode> ThreadRegistry_;
+
+ THazardPointerManager();
+
+ void Shutdown();
+
+ bool TryReclaimHazardPointers();
+ bool DoReclaimHazardPointers(THazardThreadState* threadState);
+
+ THazardThreadState* AllocateThreadState();
+ void DestroyThreadState(THazardThreadState* ptr);
+
+ void BeforeFork();
+ void AfterForkParent();
+ void AfterForkChild();
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+};
+
+/////////////////////////////////////////////////////////////////////////////
+
+static void* HazardPointerManagerInitializer = [] {
+ THazardPointerManager::Get();
+ return nullptr;
+}();
+
+/////////////////////////////////////////////////////////////////////////////
+
+THazardPointerManager::THazardPointerManager()
+{
+ NThreading::RegisterAtForkHandlers(
+ [this] { BeforeFork(); },
+ [this] { AfterForkParent(); },
+ [this] { AfterForkChild(); });
+}
+
+void THazardPointerManager::Shutdown()
+{
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Hazard Pointer Manager shutdown started (ThreadCount: %d)\n",
+ ThreadCount_.load());
+ }
+
+ int count = 0;
+ RetireQueue_.DequeueAll([&] (TRetiredPtr& item) {
+ item.Reclaimer(item.PackedPtr);
+ ++count;
+ });
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Hazard Pointer Manager shutdown completed (DeletedPtrCount: %d)\n",
+ count);
+ }
+}
+
+void THazardPointerManager::RetireHazardPointer(TPackedPtr packedPtr, THazardPtrReclaimer reclaimer)
+{
+ auto* threadState = HazardThreadState;
+ if (Y_UNLIKELY(!threadState)) {
+ if (HazardThreadStateDestroyed) {
+ // Looks like a global shutdown.
+ reclaimer(packedPtr);
+ return;
+ }
+ InitThreadState();
+ threadState = HazardThreadState;
+ }
+
+ threadState->RetireList.push({packedPtr, reclaimer});
+
+ if (threadState->Reclaiming) {
+ return;
+ }
+
+ int threadCount = ThreadCount_.load(std::memory_order::relaxed);
+ while (std::ssize(threadState->RetireList) >= std::max(2 * threadCount, 1)) {
+ DoReclaimHazardPointers(threadState);
+ }
+}
+
+bool THazardPointerManager::TryReclaimHazardPointers()
+{
+ auto* threadState = HazardThreadState;
+ if (!threadState || threadState->RetireList.empty()) {
+ return false;
+ }
+
+ YT_VERIFY(!threadState->Reclaiming);
+
+ bool hasNewPointers = DoReclaimHazardPointers(threadState);
+ int threadCount = ThreadCount_.load(std::memory_order::relaxed);
+ return
+ hasNewPointers ||
+ std::ssize(threadState->RetireList) > threadCount;
+}
+
+void THazardPointerManager::ReclaimHazardPointers(bool flush)
+{
+ if (flush) {
+ while (TryReclaimHazardPointers());
+ } else {
+ TryReclaimHazardPointers();
+ }
+}
+
+void THazardPointerManager::InitThreadState()
+{
+ if (!HazardThreadState) {
+ YT_VERIFY(!HazardThreadStateDestroyed);
+ HazardThreadState = AllocateThreadState();
+ }
+}
+
+THazardThreadState* THazardPointerManager::AllocateThreadState()
+{
+ auto* threadState = new THazardThreadState(&HazardPointers);
+
+ struct THazardThreadStateDestroyer
+ {
+ THazardThreadState* ThreadState;
+
+ ~THazardThreadStateDestroyer()
+ {
+ THazardPointerManager::Get()->DestroyThreadState(ThreadState);
+ }
+ };
+
+ // Unregisters thread from hazard ptr manager on thread exit.
+ static thread_local THazardThreadStateDestroyer destroyer{threadState};
+
+ {
+ auto guard = WriterGuard(ThreadRegistryLock_);
+ ThreadRegistry_.PushBack(threadState);
+ ++ThreadCount_;
+ }
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Hazard Pointer Manager thread state allocated (ThreadId: %" PRISZT ")\n",
+ GetCurrentThreadId());
+ }
+
+ return threadState;
+}
+
+bool THazardPointerManager::DoReclaimHazardPointers(THazardThreadState* threadState)
+{
+ threadState->Reclaiming = true;
+
+ // Collect protected pointers.
+ auto& protectedPointers = threadState->ProtectedPointers;
+ YT_VERIFY(protectedPointers.empty());
+
+ {
+ auto guard = ForkFriendlyReaderGuard(ThreadRegistryLock_);
+ for (
+ auto* current = ThreadRegistry_.GetFront();
+ current;
+ current = current->RegistryNode.Next)
+ {
+ for (const auto& hazardPointer : *current->HazardPointers) {
+ if (auto* ptr = hazardPointer.load()) {
+ protectedPointers.push_back(ptr);
+ }
+ }
+ }
+ }
+
+ std::sort(protectedPointers.begin(), protectedPointers.end());
+
+ auto& retireList = threadState->RetireList;
+
+ // Append global RetireQueue_ to local retireList.
+ RetireQueue_.DequeueAll([&] (auto item) {
+ retireList.push(item);
+ });
+
+ YT_LOG_TRACE_IF(
+ !protectedPointers.empty(),
+ "Scanning hazard pointers (Candidates: %v, Protected: %v)",
+ MakeFormattableView(TRingQueueIterableWrapper(retireList), [&] (auto* builder, auto item) {
+ builder->AppendFormat("%v", TTaggedPtr<void>::Unpack(item.PackedPtr).Ptr);
+ }),
+ MakeFormattableView(protectedPointers, [&] (auto* builder, auto ptr) {
+ builder->AppendFormat("%v", ptr);
+ }));
+
+ size_t pushedCount = 0;
+ auto popCount = retireList.size();
+ while (popCount-- > 0) {
+ auto item = std::move(retireList.front());
+ retireList.pop();
+
+ void* ptr = TTaggedPtr<void>::Unpack(item.PackedPtr).Ptr;
+ if (std::binary_search(protectedPointers.begin(), protectedPointers.end(), ptr)) {
+ retireList.push(item);
+ ++pushedCount;
+ } else {
+ item.Reclaimer(item.PackedPtr);
+ }
+ }
+
+ protectedPointers.clear();
+
+ threadState->Reclaiming = false;
+
+ YT_VERIFY(pushedCount <= retireList.size());
+ return pushedCount < retireList.size();
+}
+
+void THazardPointerManager::DestroyThreadState(THazardThreadState* threadState)
+{
+ {
+ auto guard = WriterGuard(ThreadRegistryLock_);
+ ThreadRegistry_.Remove(threadState);
+ --ThreadCount_;
+ }
+
+ // Scan threadState->RetireList and move to blocked elements to global RetireQueue_.
+
+ DoReclaimHazardPointers(threadState);
+
+ int count = 0;
+ while (!threadState->RetireList.empty()) {
+ RetireQueue_.Enqueue(std::move(threadState->RetireList.front()));
+ threadState->RetireList.pop();
+ ++count;
+ }
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Hazard Pointer Manager thread state destroyed (ThreadId: %" PRISZT ", RetiredPtrCount: %d)\n",
+ GetCurrentThreadId(),
+ count);
+ }
+
+ delete threadState;
+
+ HazardThreadState = nullptr;
+ HazardThreadStateDestroyed = true;
+}
+
+void THazardPointerManager::BeforeFork()
+{
+ ThreadRegistryLock_.AcquireWriter();
+}
+
+void THazardPointerManager::AfterForkParent()
+{
+ ThreadRegistryLock_.ReleaseWriter();
+}
+
+void THazardPointerManager::AfterForkChild()
+{
+ ThreadRegistry_.Clear();
+ ThreadCount_ = 0;
+
+ if (HazardThreadState) {
+ ThreadRegistry_.PushBack(HazardThreadState);
+ ThreadCount_ = 1;
+ }
+
+ ThreadRegistryLock_.ReleaseWriter();
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+void InitHazardThreadState()
+{
+ THazardPointerManager::Get()->InitThreadState();
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+void RetireHazardPointer(TPackedPtr packedPtr, THazardPtrReclaimer reclaimer)
+{
+ NYT::NDetail::THazardPointerManager::Get()->RetireHazardPointer(packedPtr, reclaimer);
+}
+
+void ReclaimHazardPointers(bool flush)
+{
+ NYT::NDetail::THazardPointerManager::Get()->ReclaimHazardPointers(flush);
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+THazardPtrReclaimGuard::~THazardPtrReclaimGuard()
+{
+ ReclaimHazardPointers();
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+THazardPtrReclaimOnContextSwitchGuard::THazardPtrReclaimOnContextSwitchGuard()
+ : NConcurrency::TContextSwitchGuard(
+ [] { ReclaimHazardPointers(); },
+ nullptr)
+{ }
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/hazard_ptr.h b/yt/yt/core/misc/hazard_ptr.h
new file mode 100644
index 0000000000..431d09c208
--- /dev/null
+++ b/yt/yt/core/misc/hazard_ptr.h
@@ -0,0 +1,92 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const NLogging::TLogger LockFreePtrLogger;
+
+void ReclaimHazardPointers(bool flush = true);
+
+using THazardPtrReclaimer = void(*)(TPackedPtr packedPtr);
+void RetireHazardPointer(TPackedPtr packedPtr, THazardPtrReclaimer reclaimer);
+
+//! NB: #reclaimer must be stateless.
+template <class T, class TReclaimer>
+void RetireHazardPointer(T* ptr, TReclaimer reclaimer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Relcaims hazard pointers on destruction.
+struct THazardPtrReclaimGuard
+{
+ ~THazardPtrReclaimGuard();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Relcaims hazard pointers on destruction and also on context switch.
+struct THazardPtrReclaimOnContextSwitchGuard
+ : public THazardPtrReclaimGuard
+ , public NConcurrency::TContextSwitchGuard
+{
+ THazardPtrReclaimOnContextSwitchGuard();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Protects an object from destruction (or deallocation) before CAS.
+//! Destruction or deallocation depends on delete callback in ScheduleObjectDeletion.
+template <class T>
+class THazardPtr
+{
+public:
+ static_assert(T::EnableHazard, "T::EnableHazard must be true.");
+
+ THazardPtr() = default;
+ THazardPtr(const THazardPtr&) = delete;
+ THazardPtr(THazardPtr&& other);
+
+ THazardPtr& operator=(const THazardPtr&) = delete;
+ THazardPtr& operator=(THazardPtr&& other);
+
+ template <class TPtrLoader>
+ static THazardPtr Acquire(TPtrLoader&& ptrLoader, T* ptr);
+ template <class TPtrLoader>
+ static THazardPtr Acquire(TPtrLoader&& ptrLoader);
+
+ void Reset();
+
+ ~THazardPtr();
+
+ T* Get() const;
+
+ // Operators * and -> are allowed to use only when hazard ptr protects from object
+ // destruction (ref count decrementation). Not memory deallocation.
+ T& operator*() const;
+ T* operator->() const;
+
+ explicit operator bool() const;
+
+private:
+ THazardPtr(T* ptr, std::atomic<void*>* hazardPtr);
+
+ T* Ptr_ = nullptr;
+ std::atomic<void*>* HazardPtr_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define HAZARD_PTR_INL_H_
+#include "hazard_ptr-inl.h"
+#undef HAZARD_PTR_INL_H_
diff --git a/yt/yt/core/misc/heap-inl.h b/yt/yt/core/misc/heap-inl.h
new file mode 100644
index 0000000000..c26cf45d53
--- /dev/null
+++ b/yt/yt/core/misc/heap-inl.h
@@ -0,0 +1,255 @@
+#ifndef HEAP_INL_H_
+#error "Direct inclusion of this file is not allowed, include heap.h"
+// For the sake of sane code completion.
+#include "heap.h"
+#endif
+
+#include <iterator>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TIterator, class TComparer, class TOnAssign>
+void SiftDown(TIterator begin, TIterator end, TIterator current, TComparer comparer, TOnAssign onAssign)
+{
+ size_t size = std::distance(begin, end);
+ size_t offset = std::distance(begin, current);
+
+ auto value = std::move(begin[offset]);
+ while (true) {
+ size_t left = 2 * offset + 1;
+
+ if (left >= size) {
+ break;
+ }
+
+ size_t right = left + 1;
+ size_t min;
+
+ if (right >= size) {
+ min = left;
+ } else {
+ min = comparer(begin[left], begin[right]) ? left : right;
+ }
+
+ auto&& minValue = begin[min];
+ if (comparer(value, minValue)) {
+ break;
+ }
+
+ begin[offset] = std::move(minValue);
+ onAssign(offset);
+ offset = min;
+ }
+ begin[offset] = std::move(value);
+ onAssign(offset);
+}
+
+template <class TIterator, class TComparer>
+void SiftDown(TIterator begin, TIterator end, TIterator current, TComparer comparer)
+{
+ SiftDown(std::move(begin), std::move(end), std::move(current), comparer, [] (size_t) {});
+}
+
+template <class TIterator>
+void SiftDown(TIterator begin, TIterator end, TIterator current)
+{
+ SiftDown(std::move(begin), std::move(end), std::move(current), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void SiftUp(TIterator begin, TIterator /*end*/, TIterator current, TComparer comparer, TOnAssign onAssign)
+{
+ auto value = std::move(*current);
+ while (current != begin) {
+ size_t dist = std::distance(begin, current);
+ auto parent = begin + (dist - 1) / 2;
+ auto&& parentValue = *parent;
+ if (comparer(parentValue, value)) {
+ break;
+ }
+
+ *current = std::move(parentValue);
+ onAssign(dist);
+ current = parent;
+ }
+ *current = std::move(value);
+ onAssign(std::distance(begin, current));
+}
+
+template <class TIterator, class TComparer>
+void SiftUp(TIterator begin, TIterator end, TIterator current, TComparer comparer)
+{
+ SiftUp(std::move(begin), std::move(end), std::move(current), comparer, [] (size_t) {});
+}
+
+template <class TIterator>
+void SiftUp(TIterator begin, TIterator end, TIterator current)
+{
+ SiftUp(std::move(begin), std::move(end), std::move(current), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void MakeHeap(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign)
+{
+ size_t size = std::distance(begin, end);
+ if (size > 1) {
+ for (size_t current = size / 2; current > 0; ) {
+ --current;
+ SiftDown(begin, end, begin + current, comparer, onAssign);
+ }
+ }
+}
+
+template <class TIterator, class TComparer>
+void MakeHeap(TIterator begin, TIterator end, TComparer comparer)
+{
+ MakeHeap(std::move(begin), std::move(end), std::move(comparer), [] (size_t) {});
+}
+
+template <class TIterator>
+void MakeHeap(TIterator begin, TIterator end)
+{
+ MakeHeap(std::move(begin), std::move(end), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapFront(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign)
+{
+ if (std::distance(begin, end) > 1) {
+ SiftDown(begin, end, begin, std::move(comparer), std::move(onAssign));
+ }
+}
+
+template <class TIterator, class TComparer>
+void AdjustHeapFront(TIterator begin, TIterator end, TComparer comparer)
+{
+ AdjustHeapFront(std::move(begin), std::move(end), std::move(comparer), [] (size_t) {});
+}
+
+template <class TIterator>
+void AdjustHeapFront(TIterator begin, TIterator end)
+{
+ AdjustHeapFront(std::move(begin), std::move(end), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapBack(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign)
+{
+ if (std::distance(begin, end) > 1) {
+ SiftUp(begin, end, end - 1, std::move(comparer), std::move(onAssign));
+ }
+}
+
+template <class TIterator, class TComparer>
+void AdjustHeapBack(TIterator begin, TIterator end, TComparer comparer)
+{
+ AdjustHeapBack(std::move(begin), std::move(end), std::move(comparer), [] (size_t) {});
+}
+
+template <class TIterator>
+void AdjustHeapBack(TIterator begin, TIterator end)
+{
+ AdjustHeapBack(std::move(begin), std::move(end), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void ExtractHeap(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign)
+{
+ YT_ASSERT(begin != end);
+ auto newEnd = end - 1;
+ std::swap(*begin, *newEnd);
+ onAssign(0);
+ onAssign(std::distance(begin, newEnd));
+ SiftDown(begin, newEnd, begin, comparer, onAssign);
+}
+
+template <class TIterator, class TComparer>
+void ExtractHeap(TIterator begin, TIterator end, TComparer comparer)
+{
+ ExtractHeap(std::move(begin), std::move(end), comparer, [] (size_t) {});
+}
+
+template <class TIterator>
+void ExtractHeap(TIterator begin, TIterator end)
+{
+ ExtractHeap(std::move(begin), std::move(end), std::less<>(), [] (size_t) {});
+}
+
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current, TComparer comparer, TOnAssign onAssign)
+{
+ // It intentionally duplicates SiftUp and SiftDown code for optimization reasons.
+ bool hasSiftedUp = false;
+ {
+ auto value = std::move(*current);
+ while (current != begin) {
+ size_t dist = std::distance(begin, current);
+ auto parent = begin + (dist - 1) / 2;
+ auto&& parentValue = *parent;
+ if (comparer(parentValue, value)) {
+ break;
+ }
+
+ hasSiftedUp = true;
+
+ *current = std::move(parentValue);
+ onAssign(dist);
+ current = parent;
+ }
+ *current = std::move(value);
+ }
+
+ if (hasSiftedUp) {
+ onAssign(std::distance(begin, current));
+ } else {
+ size_t size = std::distance(begin, end);
+ size_t offset = std::distance(begin, current);
+
+ auto value = std::move(begin[offset]);
+ while (true) {
+ size_t left = 2 * offset + 1;
+
+ if (left >= size) {
+ break;
+ }
+
+ size_t right = left + 1;
+ size_t min;
+
+ if (right >= size) {
+ min = left;
+ } else {
+ min = comparer(begin[left], begin[right]) ? left : right;
+ }
+
+ auto&& minValue = begin[min];
+ if (comparer(value, minValue)) {
+ break;
+ }
+
+ begin[offset] = std::move(minValue);
+ onAssign(offset);
+ offset = min;
+ }
+ begin[offset] = std::move(value);
+ onAssign(offset);
+ }
+}
+
+template <class TIterator, class TComparer>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current, TComparer comparer)
+{
+ AdjustHeapItem(std::move(begin), std::move(end), std::move(current), comparer, [] (size_t) {});
+}
+
+template <class TIterator>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current)
+{
+ AdjustHeapItem(std::move(begin), std::move(end), std::move(current), std::less<>(), [] (size_t) {});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/heap.h b/yt/yt/core/misc/heap.h
new file mode 100644
index 0000000000..6d65e38cbd
--- /dev/null
+++ b/yt/yt/core/misc/heap.h
@@ -0,0 +1,70 @@
+#pragma once
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Constructs a min-heap on |[begin, end)|.
+template <class TIterator, class TComparer, class TOnAssign>
+void MakeHeap(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void MakeHeap(TIterator begin, TIterator end, TComparer comparer);
+template <class TIterator>
+void MakeHeap(TIterator begin, TIterator end);
+
+//! Readjusts the min-heap on |[begin, end)| by pushing its front item down if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapFront(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void AdjustHeapFront(TIterator begin, TIterator end, TComparer comparer);
+template <class TIterator>
+void AdjustHeapFront(TIterator begin, TIterator end);
+
+//! Readjusts the min-heap on |[begin, end)| by pushing its back item up if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapBack(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void AdjustHeapBack(TIterator begin, TIterator end, TComparer comparer);
+template <class TIterator>
+void AdjustHeapBack(TIterator begin, TIterator end);
+
+//! Extracts the front from the heap on |[begin, end)| by moving
+//! its back to the front and then pushing it down if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void ExtractHeap(TIterator begin, TIterator end, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void ExtractHeap(TIterator begin, TIterator end, TComparer comparer);
+template <class TIterator>
+void ExtractHeap(TIterator begin, TIterator end);
+
+//! Readjusts the min-heap on |[begin, end)| by pushing current item down if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void SiftDown(TIterator begin, TIterator end, TIterator current, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void SiftDown(TIterator begin, TIterator end, TIterator current, TComparer comparer);
+template <class TIterator>
+void SiftDown(TIterator begin, TIterator end, TIterator current);
+
+//! Readjusts the min-heap on |[begin, end)| by pushing current item up if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void SiftUp(TIterator begin, TIterator end, TIterator current, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void SiftUp(TIterator begin, TIterator end, TIterator current, TComparer comparer);
+template <class TIterator>
+void SiftUp(TIterator begin, TIterator end, TIterator current);
+
+//! Readjusts the min-heap on |[begin, end)| by pushing current item up or down if needed.
+template <class TIterator, class TComparer, class TOnAssign>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current, TComparer comparer, TOnAssign onAssign);
+template <class TIterator, class TComparer>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current, TComparer comparer);
+template <class TIterator>
+void AdjustHeapItem(TIterator begin, TIterator end, TIterator current);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define HEAP_INL_H_
+#include "heap-inl.h"
+#undef HEAP_INL_H_
diff --git a/yt/yt/core/misc/hedging_manager.cpp b/yt/yt/core/misc/hedging_manager.cpp
new file mode 100644
index 0000000000..16a232cacd
--- /dev/null
+++ b/yt/yt/core/misc/hedging_manager.cpp
@@ -0,0 +1,189 @@
+#include "hedging_manager.h"
+#include "config.h"
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAdaptiveHedgingManager
+ : public IHedgingManager
+{
+public:
+ TAdaptiveHedgingManager(
+ TAdaptiveHedgingManagerConfigPtr config,
+ const NProfiling::TProfiler& profiler)
+ : Config_(std::move(config))
+ , HedgingStatistics_(New<THedgingStatistics>(
+ Config_->MaxHedgingDelay))
+ , PrimaryRequestCount_(profiler.Counter("/primary_request_count"))
+ , BackupAttemptCount_(profiler.Counter("/backup_attempt_count"))
+ , BackupRequestCount_(profiler.Counter("/backup_request_count"))
+ , HedgingDelay_(profiler.TimeGauge("/hedging_delay"))
+ {
+ YT_VERIFY(Config_->MaxBackupRequestRatio);
+ }
+
+ TDuration OnPrimaryRequestsStarted(int requestCount) override
+ {
+ auto statistics = AcquireHedgingStatistics();
+ statistics->PrimaryRequestCount.fetch_add(requestCount, std::memory_order::relaxed);
+
+ return statistics->HedgingDelay;
+ }
+
+ bool OnHedgingDelayPassed(int attemptCount) override
+ {
+ auto statistics = AcquireHedgingStatistics();
+
+ double previousStatisticsWeight = 1. - (GetInstant() - statistics->StartInstant) / Config_->TickPeriod;
+ previousStatisticsWeight = std::max(0., std::min(1., previousStatisticsWeight));
+
+ auto backupRequestCount = statistics->BackupRequestCount.load(std::memory_order::relaxed);
+ auto primaryRequestCount = statistics->PrimaryRequestCount.load(std::memory_order::relaxed);
+
+ if (auto previousStatistics = statistics->PreviousStatistics.Acquire()) {
+ backupRequestCount += previousStatisticsWeight *
+ previousStatistics->BackupRequestCount.load(std::memory_order::relaxed);
+ primaryRequestCount += previousStatisticsWeight *
+ previousStatistics->PrimaryRequestCount.load(std::memory_order::relaxed);
+ }
+
+ statistics->BackupAttemptCount.fetch_add(attemptCount, std::memory_order::relaxed);
+
+ bool hedgingApproved = !IsBackupRequestLimitExceeded(primaryRequestCount, backupRequestCount);
+ if (hedgingApproved) {
+ statistics->BackupRequestCount.fetch_add(attemptCount, std::memory_order::relaxed);
+ }
+
+ return hedgingApproved;
+ }
+
+private:
+ const TAdaptiveHedgingManagerConfigPtr Config_;
+
+ struct THedgingStatistics;
+ using THedgingStatisticsPtr = TIntrusivePtr<THedgingStatistics>;
+
+ struct THedgingStatistics final
+ {
+ explicit THedgingStatistics(
+ TDuration hedgingDelay,
+ THedgingStatisticsPtr previousStatistics = nullptr)
+ : StartInstant(GetInstant())
+ , HedgingDelay(hedgingDelay)
+ , PreviousStatistics(std::move(previousStatistics))
+ { }
+
+ const TInstant StartInstant;
+ const TDuration HedgingDelay;
+
+ std::atomic<i64> PrimaryRequestCount = 0;
+ std::atomic<i64> BackupAttemptCount = 0;
+ std::atomic<i64> BackupRequestCount = 0;
+
+ TAtomicIntrusivePtr<THedgingStatistics> PreviousStatistics;
+ };
+
+ TAtomicIntrusivePtr<THedgingStatistics> HedgingStatistics_;
+
+ NProfiling::TCounter PrimaryRequestCount_;
+ NProfiling::TCounter BackupAttemptCount_;
+ NProfiling::TCounter BackupRequestCount_;
+ NProfiling::TTimeGauge HedgingDelay_;
+
+
+ THedgingStatisticsPtr TrySwitchStatisticsAndTuneHedgingDelay(
+ const THedgingStatisticsPtr& currentStatistics)
+ {
+ auto newHedgingDelay = currentStatistics->HedgingDelay;
+ auto primaryRequestCount = currentStatistics->PrimaryRequestCount.load(std::memory_order::relaxed);
+ auto backupAttemptCount = currentStatistics->BackupAttemptCount.load(std::memory_order::relaxed);
+ if (IsBackupRequestLimitExceeded(primaryRequestCount, backupAttemptCount)) {
+ newHedgingDelay *= Config_->HedgingDelayTuneFactor;
+ } else {
+ newHedgingDelay /= Config_->HedgingDelayTuneFactor;
+ }
+ newHedgingDelay = std::max(Config_->MinHedgingDelay, std::min(Config_->MaxHedgingDelay, newHedgingDelay));
+
+ auto newStatistics = New<THedgingStatistics>(newHedgingDelay, currentStatistics);
+
+ void* expectedStatistics = currentStatistics.Get();
+ if (!HedgingStatistics_.CompareAndSwap(expectedStatistics, newStatistics)) {
+ return HedgingStatistics_.Acquire();
+ }
+
+ // NB: Skip profiling in case of very low RPS.
+ if (newStatistics->StartInstant - currentStatistics->StartInstant <= 2 * Config_->TickPeriod) {
+ PrimaryRequestCount_.Increment(currentStatistics->PrimaryRequestCount.load(std::memory_order::relaxed));
+ BackupAttemptCount_.Increment(currentStatistics->BackupAttemptCount.load(std::memory_order::relaxed));
+ BackupRequestCount_.Increment(currentStatistics->BackupRequestCount.load(std::memory_order::relaxed));
+ HedgingDelay_.Update(currentStatistics->HedgingDelay);
+ }
+
+ currentStatistics->PreviousStatistics.Reset();
+
+ return newStatistics;
+ }
+
+ THedgingStatisticsPtr AcquireHedgingStatistics()
+ {
+ auto statistics = HedgingStatistics_.Acquire();
+
+ if (GetInstant() - statistics->StartInstant <= Config_->TickPeriod) {
+ return statistics;
+ }
+
+ return TrySwitchStatisticsAndTuneHedgingDelay(statistics);
+ }
+
+ bool IsBackupRequestLimitExceeded(i64 primaryRequestCount, i64 backupRequestCount) const
+ {
+ return backupRequestCount >= static_cast<i64>(std::ceil(primaryRequestCount * *Config_->MaxBackupRequestRatio));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IHedgingManagerPtr CreateAdaptiveHedgingManager(
+ TAdaptiveHedgingManagerConfigPtr config,
+ const NProfiling::TProfiler& profiler)
+{
+ return New<TAdaptiveHedgingManager>(std::move(config), profiler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleHedgingManager
+ : public IHedgingManager
+{
+public:
+ explicit TSimpleHedgingManager(TDuration hedgingDelay)
+ : HedgingDelay_(hedgingDelay)
+ { }
+
+ TDuration OnPrimaryRequestsStarted(int /*requestCount*/) override
+ {
+ return HedgingDelay_;
+ }
+
+ bool OnHedgingDelayPassed(int /*attemptCount*/) override
+ {
+ return true;
+ }
+
+private:
+ const TDuration HedgingDelay_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IHedgingManagerPtr CreateSimpleHedgingManager(TDuration hedgingDelay)
+{
+ return New<TSimpleHedgingManager>(hedgingDelay);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/hedging_manager.h b/yt/yt/core/misc/hedging_manager.h
new file mode 100644
index 0000000000..144d0837ca
--- /dev/null
+++ b/yt/yt/core/misc/hedging_manager.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Hedging manager determines hedging policy and may be used to control request hedging.
+/*!
+ * There are two policies:
+ * - Simple one with constant hedging delay.
+ * - Adaptive one with adaptive hedging delay to satisfy ratio of backup requests.
+ * This policy also restrains sending backup requests if there were too many recently.
+ */
+struct IHedgingManager
+ : public TRefCounted
+{
+ //! This function is called upon start of #requestCount requests that may be hedged.
+ //! Returns delay to initiate backup requests.
+ virtual TDuration OnPrimaryRequestsStarted(int requestCount) = 0;
+
+ //! This function is called if #requestCount primary requests failed to finish within the determined delay.
+ //! Returns whether hedging may actually be performed according to policy-based restraints.
+ virtual bool OnHedgingDelayPassed(int requestCount) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IHedgingManager)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IHedgingManagerPtr CreateSimpleHedgingManager(
+ TDuration hedgingDelay);
+
+IHedgingManagerPtr CreateAdaptiveHedgingManager(
+ TAdaptiveHedgingManagerConfigPtr config,
+ const NProfiling::TProfiler& profiler = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/histogram.cpp b/yt/yt/core/misc/histogram.cpp
new file mode 100644
index 0000000000..2d59450832
--- /dev/null
+++ b/yt/yt/core/misc/histogram.cpp
@@ -0,0 +1,191 @@
+#include "histogram.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NPhoenix;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogram
+ : public IHistogram
+ , public NPhoenix::TFactoryTag<NPhoenix::TSimpleFactory>
+{
+public:
+ THistogram() = default;
+
+ explicit THistogram(int defaultBuckets)
+ : MaxBuckets_(defaultBuckets * HistogramViewReserveFactor)
+ { }
+
+ void AddValue(i64 value, i64 count) override
+ {
+ YT_VERIFY(value >= 0);
+
+ Items_.emplace_back(TItem{value, count});
+ ValueMin_ = std::min(ValueMin_, value);
+ ValueMax_ = std::max(ValueMax_, value);
+ if (IsValid() && HasBucket(value)) {
+ View_.Count[GetBucketIndex(value)] += count;
+ }
+ }
+
+ void RemoveValue(i64 value, i64 count) override
+ {
+ Items_.emplace_back(TItem{value, -count});
+ if (IsValid() && HasBucket(value)) {
+ auto& valueCount = View_.Count[GetBucketIndex(value)];
+ valueCount -= count;
+ YT_VERIFY(valueCount >= 0);
+ }
+ }
+
+ void BuildHistogramView() override
+ {
+ if (Items_.size() > 1 && !IsValid()) {
+ RebuildView();
+ }
+ }
+
+ THistogramView GetHistogramView() const override
+ {
+ THistogramView result;
+ if (Items_.empty()) {
+ return result;
+ }
+ if (Items_.size() == 1) {
+ result.Min = ValueMin_;
+ result.Max = ValueMax_;
+ result.Count.assign(1, Items_[0].Count);
+ return result;
+ }
+
+ YT_VERIFY(IsValid());
+ i64 firstBucket = GetBucketIndex(ValueMin_);
+ i64 lastBucket = GetBucketIndex(ValueMax_) + 1;
+ result.Min = View_.Min + BucketWidth_ * firstBucket;
+ result.Max = View_.Min + BucketWidth_ * lastBucket;
+ result.Count.assign(View_.Count.begin() + firstBucket, View_.Count.begin() + lastBucket);
+ return result;
+ }
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ Persist(context, MaxBuckets_);
+ Persist(context, ValueMin_);
+ Persist(context, ValueMax_);
+ Persist(context, Items_);
+ }
+
+private:
+ struct TItem {
+ i64 Value;
+ i64 Count;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, Value);
+ Persist(context, Count);
+ }
+ };
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(THistogram, 0x636d76d7);
+
+ static const i64 HistogramViewReserveFactor = 2;
+
+ i64 MaxBuckets_ = 0;
+ i64 ValueMin_ = std::numeric_limits<i64>::max();
+ i64 ValueMax_ = std::numeric_limits<i64>::min();
+ i64 BucketWidth_ = 0;
+ THistogramView View_;
+ std::vector<TItem> Items_;
+
+ bool IsValid() const
+ {
+ return BucketWidth_ != 0 && HasBucket(ValueMin_) && HasBucket(ValueMax_);
+ }
+
+ bool HasBucket(i64 value) const
+ {
+ return value >= View_.Min && value < View_.Max;
+ }
+
+ i64 GetBucketIndex(i64 value) const
+ {
+ YT_VERIFY(HasBucket(value));
+ return (value - View_.Min) / BucketWidth_;
+ }
+
+ void RebuildView()
+ {
+ YT_VERIFY(Items_.size() > 1);
+ // Make a view with a range twice largen that current and mean value in place.
+ BucketWidth_ = (HistogramViewReserveFactor * (ValueMax_ + 1 - ValueMin_) + MaxBuckets_ - 1) / MaxBuckets_;
+ if (BucketWidth_ == 0) {
+ BucketWidth_ = 1;
+ }
+ View_.Min = ValueMin_ - BucketWidth_ * (MaxBuckets_ / 4);
+ View_.Max = View_.Min + BucketWidth_ * MaxBuckets_;
+ View_.Min = std::max<i64>(View_.Min, 0);
+ View_.Count.assign(MaxBuckets_, 0);
+ for (const auto& item : Items_) {
+ View_.Count[GetBucketIndex(item.Value)] += item.Count;
+ }
+ }
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(THistogram);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IHistogram> CreateHistogram(int maxBuckets)
+{
+ return std::unique_ptr<IHistogram>(new THistogram(maxBuckets));
+}
+
+void Serialize(const IHistogram& histogram, IYsonConsumer* consumer)
+{
+ auto view = histogram.GetHistogramView();
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("min").Value(view.Min)
+ .Item("max").Value(view.Max)
+ .Item("count").Value(view.Count)
+ .EndMap();
+}
+
+THistogramQuartiles ComputeHistogramQuartiles(const THistogramView& histogramView)
+{
+ YT_VERIFY(histogramView.Count.size() > 0);
+
+ int currentBucketIndex = 0;
+ i64 partialBucketSum = 0;
+ i64 totalSum = std::accumulate(histogramView.Count.begin(), histogramView.Count.end(), 0LL);
+ i64 bucketSize = (histogramView.Max - histogramView.Min) / histogramView.Count.size();
+
+ auto computeNextQuartile = [&] (double quartile) {
+ while (currentBucketIndex < std::ssize(histogramView.Count) && partialBucketSum < quartile * totalSum) {
+ partialBucketSum += histogramView.Count[currentBucketIndex];
+ ++currentBucketIndex;
+ }
+ return histogramView.Min + currentBucketIndex * bucketSize;
+ };
+
+ THistogramQuartiles result;
+ result.Q25 = computeNextQuartile(0.25);
+ result.Q50 = computeNextQuartile(0.50);
+ result.Q75 = computeNextQuartile(0.75);
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/histogram.h b/yt/yt/core/misc/histogram.h
new file mode 100644
index 0000000000..8230790e85
--- /dev/null
+++ b/yt/yt/core/misc/histogram.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "public.h"
+#include "serialize.h"
+
+#include <yt/yt/core/misc/phoenix.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const int DefaultHistogramViewBuckets = 100;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THistogramView
+{
+ i64 Min = 0;
+ i64 Max = 0;
+ std::vector<i64> Count;
+};
+
+struct IHistogram
+ : public virtual NPhoenix::IPersistent
+{
+ virtual void AddValue(i64 value, i64 count = 1) = 0;
+ virtual void RemoveValue(i64 value, i64 count = 1) = 0;
+ virtual void BuildHistogramView() = 0;
+ virtual THistogramView GetHistogramView() const = 0;
+};
+
+std::unique_ptr<IHistogram> CreateHistogram(int defaultBuckets = DefaultHistogramViewBuckets);
+void Serialize(const IHistogram& histogram, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THistogramQuartiles
+{
+ i64 Q25 = 0;
+ i64 Q50 = 0;
+ i64 Q75 = 0;
+};
+
+THistogramQuartiles ComputeHistogramQuartiles(const THistogramView& histogramView);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/misc/historic_usage_aggregator.cpp b/yt/yt/core/misc/historic_usage_aggregator.cpp
new file mode 100644
index 0000000000..04b6d530d5
--- /dev/null
+++ b/yt/yt/core/misc/historic_usage_aggregator.cpp
@@ -0,0 +1,124 @@
+#include "historic_usage_aggregator.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/generic/ymath.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+THistoricUsageAggregationParameters::THistoricUsageAggregationParameters(
+ EHistoricUsageAggregationMode mode,
+ double emaAlpha)
+ : Mode(mode)
+ , EmaAlpha(emaAlpha)
+{ }
+
+THistoricUsageAggregationParameters::THistoricUsageAggregationParameters(
+ const THistoricUsageConfigPtr& config)
+ : Mode(config->AggregationMode)
+ , EmaAlpha(config->EmaAlpha)
+{ }
+
+bool THistoricUsageAggregationParameters::operator==(const THistoricUsageAggregationParameters& other) const
+{
+ return Mode == other.Mode && EmaAlpha == other.EmaAlpha;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THistoricUsageAggregator::THistoricUsageAggregator()
+{
+ Reset();
+}
+
+void THistoricUsageAggregator::UpdateParameters(
+ const THistoricUsageAggregationParameters& newParameters)
+{
+ if (Parameters_ == newParameters) {
+ return;
+ }
+
+ Parameters_ = newParameters;
+ Reset();
+}
+
+void THistoricUsageAggregator::Reset()
+{
+ ExponentialMovingAverage_ = 0.0;
+ LastExponentialMovingAverageUpdateTime_ = TInstant::Zero();
+}
+
+void THistoricUsageAggregator::UpdateAt(TInstant now, double value)
+{
+ YT_VERIFY(now >= LastExponentialMovingAverageUpdateTime_);
+
+ // If LastExponentialMovingAverageUpdateTime_ is zero, this is the first update (after most
+ // recent reset) and we just want to leave EMA = 0.0, as if there was no previous usage.
+ if (Parameters_.Mode == EHistoricUsageAggregationMode::ExponentialMovingAverage &&
+ LastExponentialMovingAverageUpdateTime_ != TInstant::Zero())
+ {
+ auto sinceLast = now - LastExponentialMovingAverageUpdateTime_;
+ auto w = Exp2(-1. * Parameters_.EmaAlpha * sinceLast.SecondsFloat());
+ ExponentialMovingAverage_ = w * ExponentialMovingAverage_ + (1 - w) * value;
+ }
+
+ LastExponentialMovingAverageUpdateTime_ = now;
+}
+
+double THistoricUsageAggregator::GetHistoricUsage() const
+{
+ return ExponentialMovingAverage_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAverageHistoricUsageAggregator::TAverageHistoricUsageAggregator(TDuration period)
+ : Period_(period)
+{ }
+
+void TAverageHistoricUsageAggregator::UpdateParameters(THistoricUsageAggregationParameters params)
+{
+ HistoricUsageAggregator_.UpdateParameters(params);
+}
+
+double TAverageHistoricUsageAggregator::GetHistoricUsage()
+{
+ auto now = NProfiling::GetInstant();
+ MaybeFlush(now);
+ return HistoricUsageAggregator_.GetHistoricUsage();
+}
+
+void TAverageHistoricUsageAggregator::UpdateAt(TInstant now, double value)
+{
+ MaybeFlush(now);
+ CurrentUsage_ += value;
+}
+
+void TAverageHistoricUsageAggregator::MaybeFlush(TInstant now)
+{
+ if (!IntervalStart_ || now < IntervalStart_) {
+ IntervalStart_ = now;
+ return;
+ }
+
+ auto diff = now - IntervalStart_;
+ if (diff < Period_) {
+ return;
+ }
+
+ auto ratio = diff / Period_;
+ auto usagePerPeriod = CurrentUsage_ / ratio;
+
+ HistoricUsageAggregator_.UpdateAt(now, usagePerPeriod);
+
+ IntervalStart_ = now;
+ CurrentUsage_ = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/historic_usage_aggregator.h b/yt/yt/core/misc/historic_usage_aggregator.h
new file mode 100644
index 0000000000..6d20d8d019
--- /dev/null
+++ b/yt/yt/core/misc/historic_usage_aggregator.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "config.h"
+
+#include <util/datetime/base.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THistoricUsageAggregationParameters
+{
+ THistoricUsageAggregationParameters() = default;
+ explicit THistoricUsageAggregationParameters(EHistoricUsageAggregationMode mode, double emaAlpha = 0.0);
+ explicit THistoricUsageAggregationParameters(const THistoricUsageConfigPtr& config);
+
+ bool operator==(const THistoricUsageAggregationParameters& other) const;
+
+ EHistoricUsageAggregationMode Mode = EHistoricUsageAggregationMode::None;
+
+ //! Parameter of exponential moving average (EMA) of the aggregated usage.
+ //! Roughly speaking, it means that current usage ratio is twice as relevant for the
+ //! historic usage as the usage ratio alpha seconds ago.
+ //! EMA for unevenly spaced time series was adapted from here: https://clck.ru/HaGZs
+ double EmaAlpha = 0.0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistoricUsageAggregator
+{
+public:
+ THistoricUsageAggregator();
+ THistoricUsageAggregator(const THistoricUsageAggregator& other) = default;
+ THistoricUsageAggregator& operator=(const THistoricUsageAggregator& other) = default;
+
+ //! Update the parameters. If the parameters have changed, resets the state.
+ void UpdateParameters(const THistoricUsageAggregationParameters& newParameters);
+
+ void Reset();
+
+ void UpdateAt(TInstant now, double value);
+
+ double GetHistoricUsage() const;
+
+private:
+ THistoricUsageAggregationParameters Parameters_;
+
+ double ExponentialMovingAverage_;
+
+ TInstant LastExponentialMovingAverageUpdateTime_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAverageHistoricUsageAggregator
+{
+public:
+ explicit TAverageHistoricUsageAggregator(TDuration period = TDuration::Seconds(1));
+
+ void UpdateParameters(THistoricUsageAggregationParameters params);
+
+ double GetHistoricUsage();
+
+ void UpdateAt(TInstant now, double value);
+
+private:
+ const TDuration Period_;
+
+ TInstant IntervalStart_ = TInstant::Zero();
+ double CurrentUsage_ = 0;
+
+ THistoricUsageAggregator HistoricUsageAggregator_;
+
+ void MaybeFlush(TInstant now);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/hr_timer.cpp b/yt/yt/core/misc/hr_timer.cpp
new file mode 100644
index 0000000000..18a6c1019c
--- /dev/null
+++ b/yt/yt/core/misc/hr_timer.cpp
@@ -0,0 +1,95 @@
+#include "hr_timer.h"
+
+#if defined(_linux_)
+#include <time.h>
+#elif defined(_darwin_)
+#include <mach/mach_time.h>
+#elif defined(_win_)
+// Any other includes?
+#endif
+
+#include <limits>
+
+namespace NYT::NHRTimer {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const ui64 NumberOfNsInS = 1000000000UL;
+static const i64 NumberOfSamples = 1000UL;
+
+void GetHRInstant(THRInstant* instant)
+{
+#if defined(_linux_)
+ // See:
+ // http://stackoverflow.com/questions/6814792/why-is-clock-gettime-so-erratic
+ // http://stackoverflow.com/questions/7935518/is-clock-gettime-adequate-for-submicrosecond-timing
+ struct timespec* ts = reinterpret_cast<struct timespec*>(instant);
+ YT_VERIFY(clock_gettime(CLOCK_REALTIME, ts) == 0);
+#elif defined(_darwin_)
+ // See http://lists.mysql.com/commits/70966
+ static mach_timebase_info_data_t info = { 0, 0 };
+ if (Y_UNLIKELY(info.denom == 0)) {
+ YT_VERIFY(mach_timebase_info(&info) == 0);
+ }
+ ui64 time;
+ YT_VERIFY((time = mach_absolute_time()));
+ time *= info.numer;
+ time /= info.denom;
+ instant->Seconds = time / NumberOfNsInS;
+ instant->Nanoseconds = time % NumberOfNsInS;
+#elif defined(_win_)
+ static LARGE_INTEGER frequency = { 0 };
+ if (Y_UNLIKELY(frequency.QuadPart == 0)) {
+ YT_VERIFY(QueryPerformanceFrequency(&frequency));
+
+ }
+ LARGE_INTEGER time;
+ YT_VERIFY((QueryPerformanceCounter(&time)));
+ instant->Seconds = time.QuadPart / frequency.QuadPart;
+ instant->Nanoseconds = (time.QuadPart % frequency.QuadPart) * NumberOfNsInS / frequency.QuadPart;
+#else
+ #error "Unsupported architecture"
+#endif
+}
+
+THRDuration GetHRDuration(const THRInstant& begin, const THRInstant& end)
+{
+ if (end.Seconds == begin.Seconds) {
+ YT_ASSERT(end.Nanoseconds >= begin.Nanoseconds);
+ return end.Nanoseconds - begin.Nanoseconds;
+ }
+
+ YT_ASSERT(
+ end.Seconds > begin.Seconds &&
+ end.Seconds - begin.Seconds <
+ static_cast<i64>(std::numeric_limits<THRDuration>::max() / NumberOfNsInS));
+
+ return
+ ( end.Seconds - begin.Seconds ) * NumberOfNsInS
+ + end.Nanoseconds - begin.Nanoseconds;
+}
+
+THRDuration GetHRResolution()
+{
+ static THRDuration result = 0;
+ if (Y_LIKELY(result)) {
+ return result;
+ }
+
+ std::vector<THRDuration> samples(NumberOfSamples);
+ THRInstant begin;
+ THRInstant end;
+ for (int i = 0; i < NumberOfSamples; ++i) {
+ GetHRInstant(&begin);
+ GetHRInstant(&end);
+ samples[i] = GetHRDuration(begin, end);
+ }
+
+ std::sort(samples.begin(), samples.end());
+ result = samples[samples.size() / 2];
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHRTimer
diff --git a/yt/yt/core/misc/hr_timer.h b/yt/yt/core/misc/hr_timer.h
new file mode 100644
index 0000000000..a39223f2c4
--- /dev/null
+++ b/yt/yt/core/misc/hr_timer.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "common.h"
+
+#include <util/system/datetime.h>
+
+namespace NYT::NHRTimer {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Returns CPU internal cycle counter.
+// On modern systems, cycle counters are consistent across cores and cycle rate
+// can be considered constant for practical purposes.
+Y_FORCE_INLINE ui64 GetRdtsc()
+{
+ return GetCycleCount();
+}
+
+// Represents an offset from an arbitrary point in the past;
+// it should be used only for relative measurements.
+struct THRInstant
+{
+ i64 Seconds;
+ i64 Nanoseconds;
+};
+
+// Represents a duration in nano-seconds.
+using THRDuration = ui64;
+
+#ifdef _linux_
+static_assert(
+ sizeof(THRInstant) == sizeof(struct timespec),
+ "THRInstant should be ABI-compatible with struct timespec");
+static_assert(
+ offsetof(THRInstant, Seconds) == offsetof(struct timespec, tv_sec),
+ "THRInstant should be ABI-compatible with struct timespec");
+static_assert(
+ offsetof(THRInstant, Nanoseconds) == offsetof(struct timespec, tv_nsec),
+ "THRInstant should be ABI-compatible with struct timespec");
+#endif
+
+// Returns instant.
+void GetHRInstant(THRInstant* instant);
+
+// Returns time difference in nanoseconds.
+THRDuration GetHRDuration(const THRInstant& begin, const THRInstant& end);
+
+// Returns instant resolution.
+THRDuration GetHRResolution();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NHRTimer
diff --git a/yt/yt/core/misc/hyperloglog.h b/yt/yt/core/misc/hyperloglog.h
new file mode 100644
index 0000000000..88f9228ddd
--- /dev/null
+++ b/yt/yt/core/misc/hyperloglog.h
@@ -0,0 +1,139 @@
+#pragma once
+
+#include "public.h"
+#include "hyperloglog_bias.h"
+#include "farm_hash.h"
+
+#include <cmath>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <int Precision>
+class THyperLogLog
+{
+ static_assert(
+ Precision >= 4 && Precision <= 18,
+ "Precision is out of range (expected to be within 4..18)");
+
+public:
+ THyperLogLog();
+
+ void Add(TFingerprint fingerprint);
+
+ void Merge(const THyperLogLog& that);
+
+ ui64 EstimateCardinality() const;
+
+ static ui64 EstimateCardinality(const std::vector<ui64>& values);
+
+private:
+ static constexpr ui64 RegisterCount = (ui64)1 << Precision;
+ static constexpr ui64 PrecisionMask = RegisterCount - 1;
+ static constexpr double Threshold = NDetail::Thresholds[Precision - 4];
+ static constexpr int Size = NDetail::Sizes[Precision - 4];
+ static constexpr auto RawEstimates = NDetail::RawEstimates[Precision - 4];
+ static constexpr auto BiasData = NDetail::BiasData[Precision - 4];
+ std::array<ui8, RegisterCount> ZeroCounts_;
+
+ void AddHash(ui64 hash);
+ double EstimateBias(double cardinality) const;
+};
+
+template <int Precision>
+THyperLogLog<Precision>::THyperLogLog()
+{
+ std::fill(ZeroCounts_.begin(), ZeroCounts_.end(), 0);
+}
+
+template <int Precision>
+void THyperLogLog<Precision>::Add(TFingerprint fingerprint)
+{
+ fingerprint |= ((ui64)1 << 63);
+ auto zeroesPlusOne = __builtin_ctzl(fingerprint >> Precision) + 1;
+
+ auto index = fingerprint & PrecisionMask;
+ if (ZeroCounts_[index] < zeroesPlusOne) {
+ ZeroCounts_[index] = zeroesPlusOne;
+ }
+}
+
+template <int Precision>
+void THyperLogLog<Precision>::Merge(const THyperLogLog<Precision>& that)
+{
+ for (size_t i = 0; i < RegisterCount; i++) {
+ auto thatCount = that.ZeroCounts_[i];
+ if (ZeroCounts_[i] < thatCount) {
+ ZeroCounts_[i] = thatCount;
+ }
+ }
+}
+
+template <int Precision>
+double THyperLogLog<Precision>::EstimateBias(double cardinality) const
+{
+ auto upperEstimate = std::lower_bound(RawEstimates, RawEstimates + Size, cardinality);
+ int index = upperEstimate - RawEstimates;
+ if (*upperEstimate == cardinality) {
+ return BiasData[index];
+ }
+
+ if (index == 0) {
+ return BiasData[0];
+ } else if (index >= Size) {
+ return BiasData[Size];
+ } else {
+ double w1 = cardinality - RawEstimates[index - 1];
+ double w2 = RawEstimates[index] - cardinality;
+ return (BiasData[index - 1] * w1 + BiasData[index] * w2) / (w1 + w2);
+ }
+}
+
+template <int Precision>
+ui64 THyperLogLog<Precision>::EstimateCardinality() const
+{
+ auto zeroRegisters = 0;
+ double sum = 0;
+ for (auto count : ZeroCounts_) {
+ if (count == 0) {
+ zeroRegisters++;
+ } else {
+ sum += 1.0 / ((ui64)1 << count);
+ }
+ }
+ sum += zeroRegisters;
+
+ double alpha = 0.7213 / (1.0 + 1.079 / RegisterCount);
+ double m = RegisterCount;
+ double raw = (1.0 / sum) * m * m * alpha;
+
+ if (raw < 5 * m) {
+ raw -= EstimateBias(raw);
+ }
+
+ double smallCardinality = raw;
+ if (zeroRegisters != 0) {
+ smallCardinality = m * log(m / zeroRegisters);
+ }
+
+ if (smallCardinality <= Threshold) {
+ return smallCardinality;
+ } else {
+ return raw;
+ }
+}
+
+template <int Precision>
+ui64 THyperLogLog<Precision>::EstimateCardinality(const std::vector<ui64>& values)
+{
+ auto state = THyperLogLog<Precision>();
+ for (auto v : values) {
+ state.Add(v);
+ }
+ return state.EstimateCardinality();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/hyperloglog_bias.h b/yt/yt/core/misc/hyperloglog_bias.h
new file mode 100644
index 0000000000..61f768e13c
--- /dev/null
+++ b/yt/yt/core/misc/hyperloglog_bias.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include "public.h"
+
+#include <array>
+
+namespace NYT::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr double RawEstimates[15][201] = {
+ // precision 4
+ { 11, 11.717, 12.207, 12.7896, 13.2882, 13.8204, 14.3772, 14.9342, 15.5202, 16.161, 16.7722, 17.4636, 18.0396, 18.6766, 19.3566, 20.0454, 20.7936, 21.4856, 22.2666, 22.9946, 23.766, 24.4692, 25.3638, 26.0764, 26.7864, 27.7602, 28.4814, 29.433, 30.2926, 31.0664, 31.9996, 32.7956, 33.5366, 34.5894, 35.5738, 36.2698, 37.3682, 38.0544, 39.2342, 40.0108, 40.7966, 41.9298, 42.8704, 43.6358, 44.5194, 45.773, 46.6772, 47.6174, 48.4888, 49.3304, 50.2506, 51.4996, 52.3824, 53.3078, 54.3984, 55.5838, 56.6618, 57.2174, 58.3514, 59.0802, 60.1482, 61.0376, 62.3598, 62.8078, 63.9744, 64.914, 65.781, 67.1806, 68.0594, 68.8446, 69.7928, 70.8248, 71.8324, 72.8598, 73.6246, 74.7014, 75.393, 76.6708, 77.2394, },
+ // precision 5
+ { 23, 23.1194, 23.8208, 24.2318, 24.77, 25.2436, 25.7774, 26.2848, 26.8224, 27.3742, 27.9336, 28.503, 29.0494, 29.6292, 30.2124, 30.798, 31.367, 31.9728, 32.5944, 33.217, 33.8438, 34.3696, 35.0956, 35.7044, 36.324, 37.0668, 37.6698, 38.3644, 39.049, 39.6918, 40.4146, 41.082, 41.687, 42.5398, 43.2462, 43.857, 44.6606, 45.4168, 46.1248, 46.9222, 47.6804, 48.447, 49.3454, 49.9594, 50.7636, 51.5776, 52.331, 53.19, 53.9676, 54.7564, 55.5314, 56.4442, 57.3708, 57.9774, 58.9624, 59.8796, 60.755, 61.472, 62.2076, 63.1024, 63.8908, 64.7338, 65.7728, 66.629, 67.413, 68.3266, 69.1524, 70.2642, 71.1806, 72.0566, 72.9192, 73.7598, 74.3516, 75.5802, 76.4386, 77.4916, 78.1524, 79.1892, 79.8414, 80.8798, 81.8376, 82.4698, 83.7656, 84.331, 85.5914, 86.6012, 87.7016, 88.5582, 89.3394, 90.3544, 91.4912, 92.308, 93.3552, 93.9746, 95.2052, 95.727, 97.1322, 98.3944, 98.7588, 100.242, 101.1914, 102.2538, 102.8776, 103.6292, 105.1932, 105.9152, 107.0868, 107.6728, 108.7144, 110.3114, 110.8716, 111.245, 112.7908, 113.7064, 114.636, 115.7464, 116.1788, 117.7464, 118.4896, 119.6166, 120.5082, 121.7798, 122.9028, 123.4426, 124.8854, 125.705, 126.4652, 128.3464, 128.3462, 130.0398, 131.0342, 131.0042, 132.4766, 133.511, 134.7252, 135.425, 136.5172, 138.0572, 138.6694, 139.3712, 140.8598, 141.4594, 142.554, 143.4006, 144.7374, 146.1634, 146.8994, 147.605, 147.9304, 149.1636, 150.2468, 151.5876, 152.2096, 153.7032, 154.7146, 155.807, 156.9228, 157.0372, 158.5852, },
+ // precision 6
+ { 46, 46.1902, 47.271, 47.8358, 48.8142, 49.2854, 50.317, 51.354, 51.8924, 52.9436, 53.4596, 54.5262, 55.6248, 56.1574, 57.2822, 57.837, 58.9636, 60.074, 60.7042, 61.7976, 62.4772, 63.6564, 64.7942, 65.5004, 66.686, 67.291, 68.5672, 69.8556, 70.4982, 71.8204, 72.4252, 73.7744, 75.0786, 75.8344, 77.0294, 77.8098, 79.0794, 80.5732, 81.1878, 82.5648, 83.2902, 84.6784, 85.3352, 86.8946, 88.3712, 89.0852, 90.499, 91.2686, 92.6844, 94.2234, 94.9732, 96.3356, 97.2286, 98.7262, 100.3284, 101.1048, 102.5962, 103.3562, 105.1272, 106.4184, 107.4974, 109.0822, 109.856, 111.48, 113.2834, 114.0208, 115.637, 116.5174, 118.0576, 119.7476, 120.427, 122.1326, 123.2372, 125.2788, 126.6776, 127.7926, 129.1952, 129.9564, 131.6454, 133.87, 134.5428, 136.2, 137.0294, 138.6278, 139.6782, 141.792, 143.3516, 144.2832, 146.0394, 147.0748, 148.4912, 150.849, 151.696, 153.5404, 154.073, 156.3714, 157.7216, 158.7328, 160.4208, 161.4184, 163.9424, 165.2772, 166.411, 168.1308, 168.769, 170.9258, 172.6828, 173.7502, 175.706, 176.3886, 179.0186, 180.4518, 181.927, 183.4172, 184.4114, 186.033, 188.5124, 189.5564, 191.6008, 192.4172, 193.8044, 194.997, 197.4548, 198.8948, 200.2346, 202.3086, 203.1548, 204.8842, 206.6508, 206.6772, 209.7254, 210.4752, 212.7228, 214.6614, 215.1676, 217.793, 218.0006, 219.9052, 221.66, 223.5588, 225.1636, 225.6882, 227.7126, 229.4502, 231.1978, 232.9756, 233.1654, 236.727, 238.1974, 237.7474, 241.1346, 242.3048, 244.1948, 245.3134, 246.879, 249.1204, 249.853, 252.6792, 253.857, 254.4486, 257.2362, 257.9534, 260.0286, 260.5632, 262.663, 264.723, 265.7566, 267.2566, 267.1624, 270.62, 272.8216, 273.2166, 275.2056, 276.2202, 278.3726, 280.3344, 281.9284, 283.9728, 284.1924, 286.4872, 287.587, 289.807, 291.1206, 292.769, 294.8708, 296.665, 297.1182, 299.4012, 300.6352, 302.1354, 304.1756, 306.1606, 307.3462, 308.5214, 309.4134, 310.8352, 313.9684, 315.837, 316.7796, 318.9858, },
+ // precision 7
+ { 92, 93.4934, 94.9758, 96.4574, 97.9718, 99.4954, 101.5302, 103.0756, 104.6374, 106.1782, 107.7888, 109.9522, 111.592, 113.2532, 114.9086, 116.5938, 118.9474, 120.6796, 122.4394, 124.2176, 125.9768, 128.4214, 130.2528, 132.0102, 133.8658, 135.7278, 138.3044, 140.1316, 142.093, 144.0032, 145.9092, 148.6306, 150.5294, 152.5756, 154.6508, 156.662, 159.552, 161.3724, 163.617, 165.5754, 167.7872, 169.8444, 172.7988, 174.8606, 177.2118, 179.3566, 181.4476, 184.5882, 186.6816, 189.0824, 191.0258, 193.6048, 196.4436, 198.7274, 200.957, 203.147, 205.4364, 208.7592, 211.3386, 213.781, 215.8028, 218.656, 221.6544, 223.996, 226.4718, 229.1544, 231.6098, 234.5956, 237.0616, 239.5758, 242.4878, 244.5244, 248.2146, 250.724, 252.8722, 255.5198, 258.0414, 261.941, 264.9048, 266.87, 269.4304, 272.028, 274.4708, 278.37, 281.0624, 283.4668, 286.5532, 289.4352, 293.2564, 295.2744, 298.2118, 300.7472, 304.1456, 307.2928, 309.7504, 312.5528, 315.979, 318.2102, 322.1834, 324.3494, 327.325, 330.6614, 332.903, 337.2544, 339.9042, 343.215, 345.2864, 348.0814, 352.6764, 355.301, 357.139, 360.658, 363.1732, 366.5902, 369.9538, 373.0828, 375.922, 378.9902, 382.7328, 386.4538, 388.1136, 391.2234, 394.0878, 396.708, 401.1556, 404.1852, 406.6372, 409.6822, 412.7796, 416.6078, 418.4916, 422.131, 424.5376, 428.1988, 432.211, 434.4502, 438.5282, 440.912, 444.0448, 447.7432, 450.8524, 453.7988, 456.7858, 458.8868, 463.9886, 466.5064, 468.9124, 472.6616, 475.4682, 478.582, 481.304, 485.2738, 488.6894, 490.329, 496.106, 497.6908, 501.1374, 504.5322, 506.8848, 510.3324, 513.4512, 516.179, 520.4412, 522.6066, 526.167, 528.7794, 533.379, 536.067, 538.46, 542.9116, 545.692, 547.9546, 552.493, 555.2722, 557.335, 562.449, 564.2014, 569.0738, 571.0974, 574.8564, 578.2996, 581.409, 583.9704, 585.8098, 589.6528, 594.5998, 595.958, 600.068, 603.3278, 608.2016, 609.9632, 612.864, 615.43, 620.7794, 621.272, 625.8644, 629.206, 633.219, 634.5154, 638.6102, },
+ // precision 8
+ { 184.2152, 187.2454, 190.2096, 193.6652, 196.6312, 199.6822, 203.249, 206.3296, 210.0038, 213.2074, 216.4612, 220.27, 223.5178, 227.4412, 230.8032, 234.1634, 238.1688, 241.6074, 245.6946, 249.2664, 252.8228, 257.0432, 260.6824, 264.9464, 268.6268, 272.2626, 276.8376, 280.4034, 284.8956, 288.8522, 292.7638, 297.3552, 301.3556, 305.7526, 309.9292, 313.8954, 318.8198, 322.7668, 327.298, 331.6688, 335.9466, 340.9746, 345.1672, 349.3474, 354.3028, 358.8912, 364.114, 368.4646, 372.9744, 378.4092, 382.6022, 387.843, 392.5684, 397.1652, 402.5426, 407.4152, 412.5388, 417.3592, 422.1366, 427.486, 432.3918, 437.5076, 442.509, 447.3834, 453.3498, 458.0668, 463.7346, 469.1228, 473.4528, 479.7, 484.644, 491.0518, 495.5774, 500.9068, 506.432, 512.1666, 517.434, 522.6644, 527.4894, 533.6312, 538.3804, 544.292, 550.5496, 556.0234, 562.8206, 566.6146, 572.4188, 579.117, 583.6762, 590.6576, 595.7864, 601.509, 607.5334, 612.9204, 619.772, 624.2924, 630.8654, 636.1836, 642.745, 649.1316, 655.0386, 660.0136, 666.6342, 671.6196, 678.1866, 684.4282, 689.3324, 695.4794, 702.5038, 708.129, 713.528, 720.3204, 726.463, 732.7928, 739.123, 744.7418, 751.2192, 756.5102, 762.6066, 769.0184, 775.2224, 781.4014, 787.7618, 794.1436, 798.6506, 805.6378, 811.766, 819.7514, 824.5776, 828.7322, 837.8048, 843.6302, 849.9336, 854.4798, 861.3388, 867.9894, 873.8196, 880.3136, 886.2308, 892.4588, 899.0816, 905.4076, 912.0064, 917.3878, 923.619, 929.998, 937.3482, 943.9506, 947.991, 955.1144, 962.203, 968.8222, 975.7324, 981.7826, 988.7666, 994.2648, 1000.3128, 1007.4082, 1013.7536, 1020.3376, 1026.7156, 1031.7478, 1037.4292, 1045.393, 1051.2278, 1058.3434, 1062.8726, 1071.884, 1076.806, 1082.9176, 1089.1678, 1095.5032, 1102.525, 1107.2264, 1115.315, 1120.93, 1127.252, 1134.1496, 1139.0408, 1147.5448, 1153.3296, 1158.1974, 1166.5262, 1174.3328, 1175.657, 1184.4222, 1190.9172, 1197.1292, 1204.4606, 1210.4578, 1218.8728, 1225.3336, 1226.6592, 1236.5768, 1241.363, 1249.4074, 1254.6566, 1260.8014, 1266.5454, 1274.5192, },
+ // precision 9
+ { 369, 374.8294, 381.2452, 387.6698, 394.1464, 400.2024, 406.8782, 413.6598, 420.462, 427.2826, 433.7102, 440.7416, 447.9366, 455.1046, 462.285, 469.0668, 476.306, 483.8448, 491.301, 498.9886, 506.2422, 513.8138, 521.7074, 529.7428, 537.8402, 545.1664, 553.3534, 561.594, 569.6886, 577.7876, 585.65, 594.228, 602.8036, 611.1666, 620.0818, 628.0824, 637.2574, 646.302, 655.1644, 664.0056, 672.3802, 681.7192, 690.5234, 700.2084, 708.831, 718.485, 728.1112, 737.4764, 746.76, 756.3368, 766.5538, 775.5058, 785.2646, 795.5902, 804.3818, 814.8998, 824.9532, 835.2062, 845.2798, 854.4728, 864.9582, 875.3292, 886.171, 896.781, 906.5716, 916.7048, 927.5322, 937.875, 949.3972, 958.3464, 969.7274, 980.2834, 992.1444, 1003.4264, 1013.0166, 1024.018, 1035.0438, 1046.34, 1057.6856, 1068.9836, 1079.0312, 1091.677, 1102.3188, 1113.4846, 1124.4424, 1135.739, 1147.1488, 1158.9202, 1169.406, 1181.5342, 1193.2834, 1203.8954, 1216.3286, 1226.2146, 1239.6684, 1251.9946, 1262.123, 1275.4338, 1285.7378, 1296.076, 1308.9692, 1320.4964, 1333.0998, 1343.9864, 1357.7754, 1368.3208, 1380.4838, 1392.7388, 1406.0758, 1416.9098, 1428.9728, 1440.9228, 1453.9292, 1462.617, 1476.05, 1490.2996, 1500.6128, 1513.7392, 1524.5174, 1536.6322, 1548.2584, 1562.3766, 1572.423, 1587.1232, 1596.5164, 1610.5938, 1622.5972, 1633.1222, 1647.7674, 1658.5044, 1671.57, 1683.7044, 1695.4142, 1708.7102, 1720.6094, 1732.6522, 1747.841, 1756.4072, 1769.9786, 1782.3276, 1797.5216, 1808.3186, 1819.0694, 1834.354, 1844.575, 1856.2808, 1871.1288, 1880.7852, 1893.9622, 1906.3418, 1920.6548, 1932.9302, 1945.8584, 1955.473, 1968.8248, 1980.6446, 1995.9598, 2008.349, 2019.8556, 2033.0334, 2044.0206, 2059.3956, 2069.9174, 2082.6084, 2093.7036, 2106.6108, 2118.9124, 2132.301, 2144.7628, 2159.8422, 2171.0212, 2183.101, 2193.5112, 2208.052, 2221.3194, 2233.3282, 2247.295, 2257.7222, 2273.342, 2286.5638, 2299.6786, 2310.8114, 2322.3312, 2335.516, 2349.874, 2363.5968, 2373.865, 2387.1918, 2401.8328, 2414.8496, 2424.544, 2436.7592, 2447.1682, 2464.1958, 2474.3438, 2489.0006, 2497.4526, 2513.6586, 2527.19, 2540.7028, 2553.768, },
+ // precision 10
+ { 738.1256, 750.4234, 763.1064, 775.4732, 788.4636, 801.0644, 814.488, 827.9654, 841.0832, 854.7864, 868.1992, 882.2176, 896.5228, 910.1716, 924.7752, 938.899, 953.6126, 968.6492, 982.9474, 998.5214, 1013.1064, 1028.6364, 1044.2468, 1059.4588, 1075.3832, 1091.0584, 1106.8606, 1123.3868, 1139.5062, 1156.1862, 1172.463, 1189.339, 1206.1936, 1223.1292, 1240.1854, 1257.2908, 1275.3324, 1292.8518, 1310.5204, 1328.4854, 1345.9318, 1364.552, 1381.4658, 1400.4256, 1419.849, 1438.152, 1456.8956, 1474.8792, 1494.118, 1513.62, 1532.5132, 1551.9322, 1570.7726, 1590.6086, 1610.5332, 1630.5918, 1650.4294, 1669.7662, 1690.4106, 1710.7338, 1730.9012, 1750.4486, 1770.1556, 1791.6338, 1812.7312, 1833.6264, 1853.9526, 1874.8742, 1896.8326, 1918.1966, 1939.5594, 1961.07, 1983.037, 2003.1804, 2026.071, 2047.4884, 2070.0848, 2091.2944, 2114.333, 2135.9626, 2158.2902, 2181.0814, 2202.0334, 2224.4832, 2246.39, 2269.7202, 2292.1714, 2314.2358, 2338.9346, 2360.891, 2384.0264, 2408.3834, 2430.1544, 2454.8684, 2476.9896, 2501.4368, 2522.8702, 2548.0408, 2570.6738, 2593.5208, 2617.0158, 2640.2302, 2664.0962, 2687.4986, 2714.2588, 2735.3914, 2759.6244, 2781.8378, 2808.0072, 2830.6516, 2856.2454, 2877.2136, 2903.4546, 2926.785, 2951.2294, 2976.468, 3000.867, 3023.6508, 3049.91, 3073.5984, 3098.162, 3121.5564, 3146.2328, 3170.9484, 3195.5902, 3221.3346, 3242.7032, 3271.6112, 3296.5546, 3317.7376, 3345.072, 3369.9518, 3394.326, 3418.1818, 3444.6926, 3469.086, 3494.2754, 3517.8698, 3544.248, 3565.3768, 3588.7234, 3616.979, 3643.7504, 3668.6812, 3695.72, 3719.7392, 3742.6224, 3770.4456, 3795.6602, 3819.9058, 3844.002, 3869.517, 3895.6824, 3920.8622, 3947.1364, 3973.985, 3995.4772, 4021.62, 4046.628, 4074.65, 4096.2256, 4121.831, 4146.6406, 4173.276, 4195.0744, 4223.9696, 4251.3708, 4272.9966, 4300.8046, 4326.302, 4353.1248, 4374.312, 4403.0322, 4426.819, 4450.0598, 4478.5206, 4504.8116, 4528.8928, 4553.9584, 4578.8712, 4603.8384, 4632.3872, 4655.5128, 4675.821, 4704.6222, 4731.9862, 4755.4174, 4781.2628, 4804.332, 4832.3048, 4862.8752, 4883.4148, 4906.9544, 4935.3516, 4954.3532, 4984.0248, 5011.217, 5035.3258, 5057.3672, 5084.1828, },
+ // precision 11
+ { 1477, 1501.6014, 1526.5802, 1551.7942, 1577.3042, 1603.2062, 1629.8402, 1656.2292, 1682.9462, 1709.9926, 1737.3026, 1765.4252, 1793.0578, 1821.6092, 1849.626, 1878.5568, 1908.527, 1937.5154, 1967.1874, 1997.3878, 2027.37, 2058.1972, 2089.5728, 2120.1012, 2151.9668, 2183.292, 2216.0772, 2247.8578, 2280.6562, 2313.041, 2345.714, 2380.3112, 2414.1806, 2447.9854, 2481.656, 2516.346, 2551.5154, 2586.8378, 2621.7448, 2656.6722, 2693.5722, 2729.1462, 2765.4124, 2802.8728, 2838.898, 2876.408, 2913.4926, 2951.4938, 2989.6776, 3026.282, 3065.7704, 3104.1012, 3143.7388, 3181.6876, 3221.1872, 3261.5048, 3300.0214, 3339.806, 3381.409, 3421.4144, 3461.4294, 3502.2286, 3544.651, 3586.6156, 3627.337, 3670.083, 3711.1538, 3753.5094, 3797.01, 3838.6686, 3882.1678, 3922.8116, 3967.9978, 4009.9204, 4054.3286, 4097.5706, 4140.6014, 4185.544, 4229.5976, 4274.583, 4316.9438, 4361.672, 4406.2786, 4451.8628, 4496.1834, 4543.505, 4589.1816, 4632.5188, 4678.2294, 4724.8908, 4769.0194, 4817.052, 4861.4588, 4910.1596, 4956.4344, 5002.5238, 5048.13, 5093.6374, 5142.8162, 5187.7894, 5237.3984, 5285.6078, 5331.0858, 5379.1036, 5428.6258, 5474.6018, 5522.7618, 5571.5822, 5618.59, 5667.9992, 5714.88, 5763.454, 5808.6982, 5860.3644, 5910.2914, 5953.571, 6005.9232, 6055.1914, 6104.5882, 6154.5702, 6199.7036, 6251.1764, 6298.7596, 6350.0302, 6398.061, 6448.4694, 6495.933, 6548.0474, 6597.7166, 6646.9416, 6695.9208, 6742.6328, 6793.5276, 6842.1934, 6894.2372, 6945.3864, 6996.9228, 7044.2372, 7094.1374, 7142.2272, 7192.2942, 7238.8338, 7288.9006, 7344.0908, 7394.8544, 7443.5176, 7490.4148, 7542.9314, 7595.6738, 7641.9878, 7694.3688, 7743.0448, 7797.522, 7845.53, 7899.594, 7950.3132, 7996.455, 8050.9442, 8092.9114, 8153.1374, 8197.4472, 8252.8278, 8301.8728, 8348.6776, 8401.4698, 8453.551, 8504.6598, 8553.8944, 8604.1276, 8657.6514, 8710.3062, 8758.908, 8807.8706, 8862.1702, 8910.4668, 8960.77, 9007.2766, 9063.164, 9121.0534, 9164.1354, 9218.1594, 9267.767, 9319.0594, 9372.155, 9419.7126, 9474.3722, 9520.1338, 9572.368, 9622.7702, 9675.8448, 9726.5396, 9778.7378, 9827.6554, 9878.1922, 9928.7782, 9978.3984, 10026.578, 10076.5626, 10137.1618, 10177.5244, 10229.9176, },
+ // precision 12
+ { 2954, 3003.4782, 3053.3568, 3104.3666, 3155.324, 3206.9598, 3259.648, 3312.539, 3366.1474, 3420.2576, 3474.8376, 3530.6076, 3586.451, 3643.38, 3700.4104, 3757.5638, 3815.9676, 3875.193, 3934.838, 3994.8548, 4055.018, 4117.1742, 4178.4482, 4241.1294, 4304.4776, 4367.4044, 4431.8724, 4496.3732, 4561.4304, 4627.5326, 4693.949, 4761.5532, 4828.7256, 4897.6182, 4965.5186, 5034.4528, 5104.865, 5174.7164, 5244.6828, 5316.6708, 5387.8312, 5459.9036, 5532.476, 5604.8652, 5679.6718, 5753.757, 5830.2072, 5905.2828, 5980.0434, 6056.6264, 6134.3192, 6211.5746, 6290.0816, 6367.1176, 6447.9796, 6526.5576, 6606.1858, 6686.9144, 6766.1142, 6847.0818, 6927.9664, 7010.9096, 7091.0816, 7175.3962, 7260.3454, 7344.018, 7426.4214, 7511.3106, 7596.0686, 7679.8094, 7765.818, 7852.4248, 7936.834, 8022.363, 8109.5066, 8200.4554, 8288.5832, 8373.366, 8463.4808, 8549.7682, 8642.0522, 8728.3288, 8820.9528, 8907.727, 9001.0794, 9091.2522, 9179.988, 9269.852, 9362.6394, 9453.642, 9546.9024, 9640.6616, 9732.6622, 9824.3254, 9917.7484, 10007.9392, 10106.7508, 10196.2152, 10289.8114, 10383.5494, 10482.3064, 10576.8734, 10668.7872, 10764.7156, 10862.0196, 10952.793, 11049.9748, 11146.0702, 11241.4492, 11339.2772, 11434.2336, 11530.741, 11627.6136, 11726.311, 11821.5964, 11918.837, 12015.3724, 12113.0162, 12213.0424, 12306.9804, 12408.4518, 12504.8968, 12604.586, 12700.9332, 12798.705, 12898.5142, 12997.0488, 13094.788, 13198.475, 13292.7764, 13392.9698, 13486.8574, 13590.1616, 13686.5838, 13783.6264, 13887.2638, 13992.0978, 14081.0844, 14189.9956, 14280.0912, 14382.4956, 14486.4384, 14588.1082, 14686.2392, 14782.276, 14888.0284, 14985.1864, 15088.8596, 15187.0998, 15285.027, 15383.6694, 15495.8266, 15591.3736, 15694.2008, 15790.3246, 15898.4116, 15997.4522, 16095.5014, 16198.8514, 16291.7492, 16402.6424, 16499.1266, 16606.2436, 16697.7186, 16796.3946, 16902.3376, 17005.7672, 17100.814, 17206.8282, 17305.8262, 17416.0744, 17508.4092, 17617.0178, 17715.4554, 17816.758, 17920.1748, 18012.9236, 18119.7984, 18223.2248, 18324.2482, 18426.6276, 18525.0932, 18629.8976, 18733.2588, 18831.0466, 18940.1366, 19032.2696, 19131.729, 19243.4864, 19349.6932, 19442.866, 19547.9448, 19653.2798, 19754.4034, 19854.0692, 19965.1224, 20065.1774, 20158.2212, 20253.353, 20366.3264, 20463.22, },
+ // precision 13
+ { 5908.5052, 6007.2672, 6107.347, 6208.5794, 6311.2622, 6414.5514, 6519.3376, 6625.6952, 6732.5988, 6841.3552, 6950.5972, 7061.3082, 7173.5646, 7287.109, 7401.8216, 7516.4344, 7633.3802, 7751.2962, 7870.3784, 7990.292, 8110.79, 8233.4574, 8356.6036, 8482.2712, 8607.7708, 8735.099, 8863.1858, 8993.4746, 9123.8496, 9255.6794, 9388.5448, 9522.7516, 9657.3106, 9792.6094, 9930.5642, 10068.794, 10206.7256, 10347.81, 10490.3196, 10632.0778, 10775.9916, 10920.4662, 11066.124, 11213.073, 11358.0362, 11508.1006, 11659.1716, 11808.7514, 11959.4884, 12112.1314, 12265.037, 12420.3756, 12578.933, 12734.311, 12890.0006, 13047.2144, 13207.3096, 13368.5144, 13528.024, 13689.847, 13852.7528, 14018.3168, 14180.5372, 14346.9668, 14513.5074, 14677.867, 14846.2186, 15017.4186, 15184.9716, 15356.339, 15529.2972, 15697.3578, 15871.8686, 16042.187, 16216.4094, 16389.4188, 16565.9126, 16742.3272, 16919.0042, 17094.7592, 17273.965, 17451.8342, 17634.4254, 17810.5984, 17988.9242, 18171.051, 18354.7938, 18539.466, 18721.0408, 18904.9972, 19081.867, 19271.9118, 19451.8694, 19637.9816, 19821.2922, 20013.1292, 20199.3858, 20387.8726, 20572.9514, 20770.7764, 20955.1714, 21144.751, 21329.9952, 21520.709, 21712.7016, 21906.3868, 22096.2626, 22286.0524, 22475.051, 22665.5098, 22862.8492, 23055.5294, 23249.6138, 23437.848, 23636.273, 23826.093, 24020.3296, 24213.3896, 24411.7392, 24602.9614, 24805.7952, 24998.1552, 25193.9588, 25389.0166, 25585.8392, 25780.6976, 25981.2728, 26175.977, 26376.5252, 26570.1964, 26773.387, 26962.9812, 27163.0586, 27368.164, 27565.0534, 27758.7428, 27961.1276, 28163.2324, 28362.3816, 28565.7668, 28758.644, 28956.9768, 29163.4722, 29354.7026, 29561.1186, 29767.9948, 29959.9986, 30164.0492, 30366.9818, 30562.5338, 30762.9928, 30976.1592, 31166.274, 31376.722, 31570.3734, 31770.809, 31974.8934, 32179.5286, 32387.5442, 32582.3504, 32794.076, 32989.9528, 33191.842, 33392.4684, 33595.659, 33801.8672, 34000.3414, 34200.0922, 34402.6792, 34610.0638, 34804.0084, 35011.13, 35218.669, 35418.6634, 35619.0792, 35830.6534, 36028.4966, 36229.7902, 36438.6422, 36630.7764, 36833.3102, 37048.6728, 37247.3916, 37453.5904, 37669.3614, 37854.5526, 38059.305, 38268.0936, 38470.2516, 38674.7064, 38876.167, 39068.3794, 39281.9144, 39492.8566, 39684.8628, 39898.4108, 40093.1836, 40297.6858, 40489.7086, 40717.2424, },
+ // precision 14
+ { 11817.475, 12015.0046, 12215.3792, 12417.7504, 12623.1814, 12830.0086, 13040.0072, 13252.503, 13466.178, 13683.2738, 13902.0344, 14123.9798, 14347.394, 14573.7784, 14802.6894, 15033.6824, 15266.9134, 15502.8624, 15741.4944, 15980.7956, 16223.8916, 16468.6316, 16715.733, 16965.5726, 17217.204, 17470.666, 17727.8516, 17986.7886, 18247.6902, 18510.9632, 18775.304, 19044.7486, 19314.4408, 19587.202, 19862.2576, 20135.924, 20417.0324, 20697.9788, 20979.6112, 21265.0274, 21550.723, 21841.6906, 22132.162, 22428.1406, 22722.127, 23020.5606, 23319.7394, 23620.4014, 23925.2728, 24226.9224, 24535.581, 24845.505, 25155.9618, 25470.3828, 25785.9702, 26103.7764, 26420.4132, 26742.0186, 27062.8852, 27388.415, 27714.6024, 28042.296, 28365.4494, 28701.1526, 29031.8008, 29364.2156, 29704.497, 30037.1458, 30380.111, 30723.8168, 31059.5114, 31404.9498, 31751.6752, 32095.2686, 32444.7792, 32794.767, 33145.204, 33498.4226, 33847.6502, 34209.006, 34560.849, 34919.4838, 35274.9778, 35635.1322, 35996.3266, 36359.1394, 36722.8266, 37082.8516, 37447.7354, 37815.9606, 38191.0692, 38559.4106, 38924.8112, 39294.6726, 39663.973, 40042.261, 40416.2036, 40779.2036, 41161.6436, 41540.9014, 41921.1998, 42294.7698, 42678.5264, 43061.3464, 43432.375, 43818.432, 44198.6598, 44583.0138, 44970.4794, 45353.924, 45729.858, 46118.2224, 46511.5724, 46900.7386, 47280.6964, 47668.1472, 48055.6796, 48446.9436, 48838.7146, 49217.7296, 49613.7796, 50010.7508, 50410.0208, 50793.7886, 51190.2456, 51583.1882, 51971.0796, 52376.5338, 52763.319, 53165.5534, 53556.5594, 53948.2702, 54346.352, 54748.7914, 55138.577, 55543.4824, 55941.1748, 56333.7746, 56745.1552, 57142.7944, 57545.2236, 57935.9956, 58348.5268, 58737.5474, 59158.5962, 59542.6896, 59958.8004, 60349.3788, 60755.0212, 61147.6144, 61548.194, 61946.0696, 62348.6042, 62763.603, 63162.781, 63560.635, 63974.3482, 64366.4908, 64771.5876, 65176.7346, 65597.3916, 65995.915, 66394.0384, 66822.9396, 67203.6336, 67612.2032, 68019.0078, 68420.0388, 68821.22, 69235.8388, 69640.0724, 70055.155, 70466.357, 70863.4266, 71276.2482, 71677.0306, 72080.2006, 72493.0214, 72893.5952, 73314.5856, 73714.9852, 74125.3022, 74521.2122, 74933.6814, 75341.5904, 75743.0244, 76166.0278, 76572.1322, 76973.1028, 77381.6284, 77800.6092, 78189.328, 78607.0962, 79012.2508, 79407.8358, 79825.725, 80238.701, 80646.891, 81035.6436, 81460.0448, 81876.3884, },
+ // precision 15
+ { 23635.0036, 24030.8034, 24431.4744, 24837.1524, 25246.7928, 25661.326, 26081.3532, 26505.2806, 26933.9892, 27367.7098, 27805.318, 28248.799, 28696.4382, 29148.8244, 29605.5138, 30066.8668, 30534.2344, 31006.32, 31480.778, 31962.2418, 32447.3324, 32938.0232, 33432.731, 33930.728, 34433.9896, 34944.1402, 35457.5588, 35974.5958, 36497.3296, 37021.9096, 37554.326, 38088.0826, 38628.8816, 39171.3192, 39723.2326, 40274.5554, 40832.3142, 41390.613, 41959.5908, 42532.5466, 43102.0344, 43683.5072, 44266.694, 44851.2822, 45440.7862, 46038.0586, 46640.3164, 47241.064, 47846.155, 48454.7396, 49076.9168, 49692.542, 50317.4778, 50939.65, 51572.5596, 52210.2906, 52843.7396, 53481.3996, 54127.236, 54770.406, 55422.6598, 56078.7958, 56736.7174, 57397.6784, 58064.5784, 58730.308, 59404.9784, 60077.0864, 60751.9158, 61444.1386, 62115.817, 62808.7742, 63501.4774, 64187.5454, 64883.6622, 65582.7468, 66274.5318, 66976.9276, 67688.7764, 68402.138, 69109.6274, 69822.9706, 70543.6108, 71265.5202, 71983.3848, 72708.4656, 73433.384, 74158.4664, 74896.4868, 75620.9564, 76362.1434, 77098.3204, 77835.7662, 78582.6114, 79323.9902, 80067.8658, 80814.9246, 81567.0136, 82310.8536, 83061.9952, 83821.4096, 84580.8608, 85335.547, 86092.5802, 86851.6506, 87612.311, 88381.2016, 89146.3296, 89907.8974, 90676.846, 91451.4152, 92224.5518, 92995.8686, 93763.5066, 94551.2796, 95315.1944, 96096.1806, 96881.0918, 97665.679, 98442.68, 99229.3002, 100011.0994, 100790.6386, 101580.1564, 102377.7484, 103152.1392, 103944.2712, 104730.216, 105528.6336, 106324.9398, 107117.6706, 107890.3988, 108695.2266, 109485.238, 110294.7876, 111075.0958, 111878.0496, 112695.2864, 113464.5486, 114270.0474, 115068.608, 115884.3626, 116673.2588, 117483.3716, 118275.097, 119085.4092, 119879.2808, 120687.5868, 121499.9944, 122284.916, 123095.9254, 123912.5038, 124709.0454, 125503.7182, 126323.259, 127138.9412, 127943.8294, 128755.646, 129556.5354, 130375.3298, 131161.4734, 131971.1962, 132787.5458, 133588.1056, 134431.351, 135220.2906, 136023.398, 136846.6558, 137667.0004, 138463.663, 139283.7154, 140074.6146, 140901.3072, 141721.8548, 142543.2322, 143356.1096, 144173.7412, 144973.0948, 145794.3162, 146609.5714, 147420.003, 148237.9784, 149050.5696, 149854.761, 150663.1966, 151494.0754, 152313.1416, 153112.6902, 153935.7206, 154746.9262, 155559.547, 156401.9746, 157228.7036, 158008.7254, 158820.75, 159646.9184, 160470.4458, 161279.5348, 162093.3114, 162918.542, 163729.2842, },
+ // precision 16
+ { 47271, 48062.3584, 48862.7074, 49673.152, 50492.8416, 51322.9514, 52161.03, 53009.407, 53867.6348, 54734.206, 55610.5144, 56496.2096, 57390.795, 58297.268, 59210.6448, 60134.665, 61068.0248, 62010.4472, 62962.5204, 63923.5742, 64895.0194, 65876.4182, 66862.6136, 67862.6968, 68868.8908, 69882.8544, 70911.271, 71944.0924, 72990.0326, 74040.692, 75100.6336, 76174.7826, 77252.5998, 78340.2974, 79438.2572, 80545.4976, 81657.2796, 82784.6336, 83915.515, 85059.7362, 86205.9368, 87364.4424, 88530.3358, 89707.3744, 90885.9638, 92080.197, 93275.5738, 94479.391, 95695.918, 96919.2236, 98148.4602, 99382.3474, 100625.6974, 101878.0284, 103141.6278, 104409.4588, 105686.2882, 106967.5402, 108261.6032, 109548.1578, 110852.0728, 112162.231, 113479.0072, 114806.2626, 116137.9072, 117469.5048, 118813.5186, 120165.4876, 121516.2556, 122875.766, 124250.5444, 125621.2222, 127003.2352, 128387.848, 129775.2644, 131181.7776, 132577.3086, 133979.9458, 135394.1132, 136800.9078, 138233.217, 139668.5308, 141085.212, 142535.2122, 143969.0684, 145420.2872, 146878.1542, 148332.7572, 149800.3202, 151269.66, 152743.6104, 154213.0948, 155690.288, 157169.4246, 158672.1756, 160160.059, 161650.6854, 163145.7772, 164645.6726, 166159.1952, 167682.1578, 169177.3328, 170700.0118, 172228.8964, 173732.6664, 175265.5556, 176787.799, 178317.111, 179856.6914, 181400.865, 182943.4612, 184486.742, 186033.4698, 187583.7886, 189148.1868, 190688.4526, 192250.1926, 193810.9042, 195354.2972, 196938.7682, 198493.5898, 200079.2824, 201618.912, 203205.5492, 204765.5798, 206356.1124, 207929.3064, 209498.7196, 211086.229, 212675.1324, 214256.7892, 215826.2392, 217412.8474, 218995.6724, 220618.6038, 222207.1166, 223781.0364, 225387.4332, 227005.7928, 228590.4336, 230217.8738, 231805.1054, 233408.9, 234995.3432, 236601.4956, 238190.7904, 239817.2548, 241411.2832, 243002.4066, 244640.1884, 246255.3128, 247849.3508, 249479.9734, 251106.8822, 252705.027, 254332.9242, 255935.129, 257526.9014, 259154.772, 260777.625, 262390.253, 264004.4906, 265643.59, 267255.4076, 268873.426, 270470.7252, 272106.4804, 273722.4456, 275337.794, 276945.7038, 278592.9154, 280204.3726, 281841.1606, 283489.171, 285130.1716, 286735.3362, 288364.7164, 289961.1814, 291595.5524, 293285.683, 294899.6668, 296499.3434, 298128.0462, 299761.8946, 301394.2424, 302997.6748, 304615.1478, 306269.7724, 307886.114, 309543.1028, 311153.2862, 312782.8546, 314421.2008, 316033.2438, 317692.9636, 319305.2648, 320948.7406, 322566.3364, 324228.4224, 325847.1542, },
+ // precision 17
+ { 94542, 96125.811, 97728.019, 99348.558, 100987.9705, 102646.7565, 104324.5125, 106021.7435, 107736.7865, 109469.272, 111223.9465, 112995.219, 114787.432, 116593.152, 118422.71, 120267.2345, 122134.6765, 124020.937, 125927.2705, 127851.255, 129788.9485, 131751.016, 133726.8225, 135722.592, 137736.789, 139770.568, 141821.518, 143891.343, 145982.1415, 148095.387, 150207.526, 152355.649, 154515.6415, 156696.05, 158887.7575, 161098.159, 163329.852, 165569.053, 167837.4005, 170121.6165, 172420.4595, 174732.6265, 177062.77, 179412.502, 181774.035, 184151.939, 186551.6895, 188965.691, 191402.8095, 193857.949, 196305.0775, 198774.6715, 201271.2585, 203764.78, 206299.3695, 208818.1365, 211373.115, 213946.7465, 216532.076, 219105.541, 221714.5375, 224337.5135, 226977.5125, 229613.0655, 232270.2685, 234952.2065, 237645.3555, 240331.1925, 243034.517, 245756.0725, 248517.6865, 251232.737, 254011.3955, 256785.995, 259556.44, 262368.335, 265156.911, 267965.266, 270785.583, 273616.0495, 276487.4835, 279346.639, 282202.509, 285074.3885, 287942.2855, 290856.018, 293774.0345, 296678.5145, 299603.6355, 302552.6575, 305492.9785, 308466.8605, 311392.581, 314347.538, 317319.4295, 320285.9785, 323301.7325, 326298.3235, 329301.3105, 332301.987, 335309.791, 338370.762, 341382.923, 344431.1265, 347464.1545, 350507.28, 353619.2345, 356631.2005, 359685.203, 362776.7845, 365886.488, 368958.2255, 372060.6825, 375165.4335, 378237.935, 381328.311, 384430.5225, 387576.425, 390683.242, 393839.648, 396977.8425, 400101.9805, 403271.296, 406409.8425, 409529.5485, 412678.7, 415847.423, 419020.8035, 422157.081, 425337.749, 428479.6165, 431700.902, 434893.1915, 438049.582, 441210.5415, 444379.2545, 447577.356, 450741.931, 453959.548, 457137.0935, 460329.846, 463537.4815, 466732.3345, 469960.5615, 473164.681, 476347.6345, 479496.173, 482813.1645, 486025.6995, 489249.4885, 492460.1945, 495675.8805, 498908.0075, 502131.802, 505374.3855, 508550.9915, 511806.7305, 515026.776, 518217.0005, 521523.9855, 524705.9855, 527950.997, 531210.0265, 534472.497, 537750.7315, 540926.922, 544207.094, 547429.4345, 550666.3745, 553975.3475, 557150.7185, 560399.6165, 563662.697, 566916.7395, 570146.1215, 573447.425, 576689.6245, 579874.5745, 583202.337, 586503.0255, 589715.635, 592910.161, 596214.3885, 599488.035, 602740.92, 605983.0685, 609248.67, 612491.3605, 615787.912, 619107.5245, 622307.9555, 625577.333, 628840.4385, 632085.2155, 635317.6135, 638691.7195, 641887.467, 645139.9405, 648441.546, 651666.252, 654941.845, },
+ // precision 18
+ { 189084, 192250.913, 195456.774, 198696.946, 201977.762, 205294.444, 208651.754, 212042.099, 215472.269, 218941.91, 222443.912, 225996.845, 229568.199, 233193.568, 236844.457, 240543.233, 244279.475, 248044.27, 251854.588, 255693.2, 259583.619, 263494.621, 267445.385, 271454.061, 275468.769, 279549.456, 283646.446, 287788.198, 291966.099, 296181.164, 300431.469, 304718.618, 309024.004, 313393.508, 317760.803, 322209.731, 326675.061, 331160.627, 335654.47, 340241.442, 344841.833, 349467.132, 354130.629, 358819.432, 363574.626, 368296.587, 373118.482, 377914.93, 382782.301, 387680.669, 392601.981, 397544.323, 402529.115, 407546.018, 412593.658, 417638.657, 422762.865, 427886.169, 433017.167, 438213.273, 443441.254, 448692.421, 453937.533, 459239.049, 464529.569, 469910.083, 475274.03, 480684.473, 486070.26, 491515.237, 496995.651, 502476.617, 507973.609, 513497.19, 519083.233, 524726.509, 530305.505, 535945.728, 541584.404, 547274.055, 552967.236, 558667.862, 564360.216, 570128.148, 575965.08, 581701.952, 587532.523, 593361.144, 599246.128, 605033.418, 610958.779, 616837.117, 622772.818, 628672.04, 634675.369, 640574.831, 646585.739, 652574.547, 658611.217, 664642.684, 670713.914, 676737.681, 682797.313, 688837.897, 694917.874, 701009.882, 707173.648, 713257.254, 719415.392, 725636.761, 731710.697, 737906.209, 744103.074, 750313.39, 756504.185, 762712.579, 768876.985, 775167.859, 781359, 787615.959, 793863.597, 800245.477, 806464.582, 812785.294, 819005.925, 825403.057, 831676.197, 837936.284, 844266.968, 850642.711, 856959.756, 863322.774, 869699.931, 876102.478, 882355.787, 888694.463, 895159.952, 901536.143, 907872.631, 914293.672, 920615.14, 927130.974, 933409.404, 939922.178, 946331.47, 952745.93, 959209.264, 965590.224, 972077.284, 978501.961, 984953.19, 991413.271, 997817.479, 1004222.658, 1010725.676, 1017177.138, 1023612.529, 1030098.236, 1036493.719, 1043112.207, 1049537.036, 1056008.096, 1062476.184, 1068942.337, 1075524.95, 1081932.864, 1088426.025, 1094776.005, 1101327.448, 1107901.673, 1114423.639, 1120884.602, 1127324.923, 1133794.24, 1140328.886, 1146849.376, 1153346.682, 1159836.502, 1166478.703, 1172953.304, 1179391.502, 1185950.982, 1192544.052, 1198913.41, 1205430.994, 1212015.525, 1218674.042, 1225121.683, 1231551.101, 1238126.379, 1244673.795, 1251260.649, 1257697.86, 1264320.983, 1270736.319, 1277274.694, 1283804.95, 1290211.514, 1296858.568, 1303455.691, }
+};
+
+static constexpr double BiasData[15][201] = {
+ // precision 4
+ { 10, 9.717, 9.207, 8.7896, 8.2882, 7.8204, 7.3772, 6.9342, 6.5202, 6.161, 5.7722, 5.4636, 5.0396, 4.6766, 4.3566, 4.0454, 3.7936, 3.4856, 3.2666, 2.9946, 2.766, 2.4692, 2.3638, 2.0764, 1.7864, 1.7602, 1.4814, 1.433, 1.2926, 1.0664, 0.999600000000001, 0.7956, 0.5366, 0.589399999999998, 0.573799999999999, 0.269799999999996, 0.368200000000002, 0.0544000000000011, 0.234200000000001, 0.0108000000000033, -0.203400000000002, -0.0701999999999998, -0.129600000000003, -0.364199999999997, -0.480600000000003, -0.226999999999997, -0.322800000000001, -0.382599999999996, -0.511200000000002, -0.669600000000003, -0.749400000000001, -0.500399999999999, -0.617600000000003, -0.6922, -0.601599999999998, -0.416200000000003, -0.338200000000001, -0.782600000000002, -0.648600000000002, -0.919800000000002, -0.851799999999997, -0.962400000000002, -0.6402, -1.1922, -1.0256, -1.086, -1.21899999999999, -0.819400000000002, -0.940600000000003, -1.1554, -1.2072, -1.1752, -1.16759999999999, -1.14019999999999, -1.3754, -1.29859999999999, -1.607, -1.3292, -1.7606, },
+ // precision 5
+ { 22, 21.1194, 20.8208, 20.2318, 19.77, 19.2436, 18.7774, 18.2848, 17.8224, 17.3742, 16.9336, 16.503, 16.0494, 15.6292, 15.2124, 14.798, 14.367, 13.9728, 13.5944, 13.217, 12.8438, 12.3696, 12.0956, 11.7044, 11.324, 11.0668, 10.6698, 10.3644, 10.049, 9.6918, 9.4146, 9.082, 8.687, 8.5398, 8.2462, 7.857, 7.6606, 7.4168, 7.1248, 6.9222, 6.6804, 6.447, 6.3454, 5.9594, 5.7636, 5.5776, 5.331, 5.19, 4.9676, 4.7564, 4.5314, 4.4442, 4.3708, 3.9774, 3.9624, 3.8796, 3.755, 3.472, 3.2076, 3.1024, 2.8908, 2.7338, 2.7728, 2.629, 2.413, 2.3266, 2.1524, 2.2642, 2.1806, 2.0566, 1.9192, 1.7598, 1.3516, 1.5802, 1.43859999999999, 1.49160000000001, 1.1524, 1.1892, 0.841399999999993, 0.879800000000003, 0.837599999999995, 0.469800000000006, 0.765600000000006, 0.331000000000003, 0.591399999999993, 0.601200000000006, 0.701599999999999, 0.558199999999999, 0.339399999999998, 0.354399999999998, 0.491200000000006, 0.308000000000007, 0.355199999999996, -0.0254000000000048, 0.205200000000005, -0.272999999999996, 0.132199999999997, 0.394400000000005, -0.241200000000006, 0.242000000000004, 0.191400000000002, 0.253799999999998, -0.122399999999999, -0.370800000000003, 0.193200000000004, -0.0848000000000013, 0.0867999999999967, -0.327200000000005, -0.285600000000002, 0.311400000000006, -0.128399999999999, -0.754999999999995, -0.209199999999996, -0.293599999999998, -0.364000000000004, -0.253600000000006, -0.821200000000005, -0.253600000000006, -0.510400000000004, -0.383399999999995, -0.491799999999998, -0.220200000000006, -0.0972000000000008, -0.557400000000001, -0.114599999999996, -0.295000000000002, -0.534800000000004, 0.346399999999988, -0.65379999999999, 0.0398000000000138, 0.0341999999999985, -0.995800000000003, -0.523400000000009, -0.489000000000004, -0.274799999999999, -0.574999999999989, -0.482799999999997, 0.0571999999999946, -0.330600000000004, -0.628800000000012, -0.140199999999993, -0.540600000000012, -0.445999999999998, -0.599400000000003, -0.262599999999992, 0.163399999999996, -0.100599999999986, -0.39500000000001, -1.06960000000001, -0.836399999999998, -0.753199999999993, -0.412399999999991, -0.790400000000005, -0.29679999999999, -0.28540000000001, -0.193000000000012, -0.0772000000000048, -0.962799999999987, -0.414800000000014, },
+ // precision 6
+ { 45, 44.1902, 43.271, 42.8358, 41.8142, 41.2854, 40.317, 39.354, 38.8924, 37.9436, 37.4596, 36.5262, 35.6248, 35.1574, 34.2822, 33.837, 32.9636, 32.074, 31.7042, 30.7976, 30.4772, 29.6564, 28.7942, 28.5004, 27.686, 27.291, 26.5672, 25.8556, 25.4982, 24.8204, 24.4252, 23.7744, 23.0786, 22.8344, 22.0294, 21.8098, 21.0794, 20.5732, 20.1878, 19.5648, 19.2902, 18.6784, 18.3352, 17.8946, 17.3712, 17.0852, 16.499, 16.2686, 15.6844, 15.2234, 14.9732, 14.3356, 14.2286, 13.7262, 13.3284, 13.1048, 12.5962, 12.3562, 12.1272, 11.4184, 11.4974, 11.0822, 10.856, 10.48, 10.2834, 10.0208, 9.637, 9.51739999999999, 9.05759999999999, 8.74760000000001, 8.42700000000001, 8.1326, 8.2372, 8.2788, 7.6776, 7.79259999999999, 7.1952, 6.9564, 6.6454, 6.87, 6.5428, 6.19999999999999, 6.02940000000001, 5.62780000000001, 5.6782, 5.792, 5.35159999999999, 5.28319999999999, 5.0394, 5.07480000000001, 4.49119999999999, 4.84899999999999, 4.696, 4.54040000000001, 4.07300000000001, 4.37139999999999, 3.7216, 3.7328, 3.42080000000001, 3.41839999999999, 3.94239999999999, 3.27719999999999, 3.411, 3.13079999999999, 2.76900000000001, 2.92580000000001, 2.68279999999999, 2.75020000000001, 2.70599999999999, 2.3886, 3.01859999999999, 2.45179999999999, 2.92699999999999, 2.41720000000001, 2.41139999999999, 2.03299999999999, 2.51240000000001, 2.5564, 2.60079999999999, 2.41720000000001, 1.80439999999999, 1.99700000000001, 2.45480000000001, 1.8948, 2.2346, 2.30860000000001, 2.15479999999999, 1.88419999999999, 1.6508, 0.677199999999999, 1.72540000000001, 1.4752, 1.72280000000001, 1.66139999999999, 1.16759999999999, 1.79300000000001, 1.00059999999999, 0.905200000000008, 0.659999999999997, 1.55879999999999, 1.1636, 0.688199999999995, 0.712600000000009, 0.450199999999995, 1.1978, 0.975599999999986, 0.165400000000005, 1.727, 1.19739999999999, -0.252600000000001, 1.13460000000001, 1.3048, 1.19479999999999, 0.313400000000001, 0.878999999999991, 1.12039999999999, 0.853000000000009, 1.67920000000001, 0.856999999999999, 0.448599999999999, 1.2362, 0.953399999999988, 1.02859999999998, 0.563199999999995, 0.663000000000011, 0.723000000000013, 0.756599999999992, 0.256599999999992, -0.837600000000009, 0.620000000000005, 0.821599999999989, 0.216600000000028, 0.205600000000004, 0.220199999999977, 0.372599999999977, 0.334400000000016, 0.928400000000011, 0.972800000000007, 0.192400000000021, 0.487199999999973, -0.413000000000011, 0.807000000000016, 0.120600000000024, 0.769000000000005, 0.870799999999974, 0.66500000000002, 0.118200000000002, 0.401200000000017, 0.635199999999998, 0.135400000000004, 0.175599999999974, 1.16059999999999, 0.34620000000001, 0.521400000000028, -0.586599999999976, -1.16480000000001, 0.968399999999974, 0.836999999999989, 0.779600000000016, 0.985799999999983, },
+ // precision 7
+ { 91, 89.4934, 87.9758, 86.4574, 84.9718, 83.4954, 81.5302, 80.0756, 78.6374, 77.1782, 75.7888, 73.9522, 72.592, 71.2532, 69.9086, 68.5938, 66.9474, 65.6796, 64.4394, 63.2176, 61.9768, 60.4214, 59.2528, 58.0102, 56.8658, 55.7278, 54.3044, 53.1316, 52.093, 51.0032, 49.9092, 48.6306, 47.5294, 46.5756, 45.6508, 44.662, 43.552, 42.3724, 41.617, 40.5754, 39.7872, 38.8444, 37.7988, 36.8606, 36.2118, 35.3566, 34.4476, 33.5882, 32.6816, 32.0824, 31.0258, 30.6048, 29.4436, 28.7274, 27.957, 27.147, 26.4364, 25.7592, 25.3386, 24.781, 23.8028, 23.656, 22.6544, 21.996, 21.4718, 21.1544, 20.6098, 19.5956, 19.0616, 18.5758, 18.4878, 17.5244, 17.2146, 16.724, 15.8722, 15.5198, 15.0414, 14.941, 14.9048, 13.87, 13.4304, 13.028, 12.4708, 12.37, 12.0624, 11.4668, 11.5532, 11.4352, 11.2564, 10.2744, 10.2118, 9.74720000000002, 10.1456, 9.2928, 8.75040000000001, 8.55279999999999, 8.97899999999998, 8.21019999999999, 8.18340000000001, 7.3494, 7.32499999999999, 7.66140000000001, 6.90300000000002, 7.25439999999998, 6.9042, 7.21499999999997, 6.28640000000001, 6.08139999999997, 6.6764, 6.30099999999999, 5.13900000000001, 5.65800000000002, 5.17320000000001, 4.59019999999998, 4.9538, 5.08280000000002, 4.92200000000003, 4.99020000000002, 4.7328, 5.4538, 4.11360000000002, 4.22340000000003, 4.08780000000002, 3.70800000000003, 4.15559999999999, 4.18520000000001, 3.63720000000001, 3.68220000000002, 3.77960000000002, 3.6078, 2.49160000000001, 3.13099999999997, 2.5376, 3.19880000000001, 3.21100000000001, 2.4502, 3.52820000000003, 2.91199999999998, 3.04480000000001, 2.7432, 2.85239999999999, 2.79880000000003, 2.78579999999999, 1.88679999999999, 2.98860000000002, 2.50639999999999, 1.91239999999999, 2.66160000000002, 2.46820000000002, 1.58199999999999, 1.30399999999997, 2.27379999999999, 2.68939999999998, 1.32900000000001, 3.10599999999999, 1.69080000000002, 2.13740000000001, 2.53219999999999, 1.88479999999998, 1.33240000000001, 1.45119999999997, 1.17899999999997, 2.44119999999998, 1.60659999999996, 2.16700000000003, 0.77940000000001, 2.37900000000002, 2.06700000000001, 1.46000000000004, 2.91160000000002, 1.69200000000001, 0.954600000000028, 2.49300000000005, 2.2722, 1.33500000000004, 2.44899999999996, 1.20140000000004, 3.07380000000001, 2.09739999999999, 2.85640000000001, 2.29960000000005, 2.40899999999999, 1.97040000000004, 0.809799999999996, 1.65279999999996, 2.59979999999996, 0.95799999999997, 2.06799999999998, 2.32780000000002, 4.20159999999998, 1.96320000000003, 1.86400000000003, 1.42999999999995, 3.77940000000001, 1.27200000000005, 1.86440000000005, 2.20600000000002, 3.21900000000005, 1.5154, 2.61019999999996, },
+ // precision 8
+ { 183.2152, 180.2454, 177.2096, 173.6652, 170.6312, 167.6822, 164.249, 161.3296, 158.0038, 155.2074, 152.4612, 149.27, 146.5178, 143.4412, 140.8032, 138.1634, 135.1688, 132.6074, 129.6946, 127.2664, 124.8228, 122.0432, 119.6824, 116.9464, 114.6268, 112.2626, 109.8376, 107.4034, 104.8956, 102.8522, 100.7638, 98.3552, 96.3556, 93.7526, 91.9292, 89.8954, 87.8198, 85.7668, 83.298, 81.6688, 79.9466, 77.9746, 76.1672, 74.3474, 72.3028, 70.8912, 69.114, 67.4646, 65.9744, 64.4092, 62.6022, 60.843, 59.5684, 58.1652, 56.5426, 55.4152, 53.5388, 52.3592, 51.1366, 49.486, 48.3918, 46.5076, 45.509, 44.3834, 43.3498, 42.0668, 40.7346, 40.1228, 38.4528, 37.7, 36.644, 36.0518, 34.5774, 33.9068, 32.432, 32.1666, 30.434, 29.6644, 28.4894, 27.6312, 26.3804, 26.292, 25.5496000000001, 25.0234, 24.8206, 22.6146, 22.4188, 22.117, 20.6762, 20.6576, 19.7864, 19.509, 18.5334, 17.9204, 17.772, 16.2924, 16.8654, 15.1836, 15.745, 15.1316, 15.0386, 14.0136, 13.6342, 12.6196, 12.1866, 12.4281999999999, 11.3324, 10.4794000000001, 11.5038, 10.129, 9.52800000000002, 10.3203999999999, 9.46299999999997, 9.79280000000006, 9.12300000000005, 8.74180000000001, 9.2192, 7.51020000000005, 7.60659999999996, 7.01840000000004, 7.22239999999999, 7.40139999999997, 6.76179999999999, 7.14359999999999, 5.65060000000005, 5.63779999999997, 5.76599999999996, 6.75139999999999, 5.57759999999996, 3.73220000000003, 5.8048, 5.63019999999995, 4.93359999999996, 3.47979999999995, 4.33879999999999, 3.98940000000005, 3.81960000000004, 3.31359999999995, 3.23080000000004, 3.4588, 3.08159999999998, 3.4076, 3.00639999999999, 2.38779999999997, 2.61900000000003, 1.99800000000005, 3.34820000000002, 2.95060000000001, 0.990999999999985, 2.11440000000005, 2.20299999999997, 2.82219999999995, 2.73239999999998, 2.7826, 3.76660000000004, 2.26480000000004, 2.31280000000004, 2.40819999999997, 2.75360000000001, 3.33759999999995, 2.71559999999999, 1.7478000000001, 1.42920000000004, 2.39300000000003, 2.22779999999989, 2.34339999999997, 0.87259999999992, 3.88400000000001, 1.80600000000004, 1.91759999999999, 1.16779999999994, 1.50320000000011, 2.52500000000009, 0.226400000000012, 2.31500000000005, 0.930000000000064, 1.25199999999995, 2.14959999999996, 0.0407999999999902, 2.5447999999999, 1.32960000000003, 0.197400000000016, 2.52620000000002, 3.33279999999991, -1.34300000000007, 0.422199999999975, 0.917200000000093, 1.12920000000008, 1.46060000000011, 1.45779999999991, 2.8728000000001, 3.33359999999993, -1.34079999999994, 1.57680000000005, 0.363000000000056, 1.40740000000005, 0.656600000000026, 0.801400000000058, -0.454600000000028, 1.51919999999996, },
+ // precision 9
+ { 368, 361.8294, 355.2452, 348.6698, 342.1464, 336.2024, 329.8782, 323.6598, 317.462, 311.2826, 305.7102, 299.7416, 293.9366, 288.1046, 282.285, 277.0668, 271.306, 265.8448, 260.301, 254.9886, 250.2422, 244.8138, 239.7074, 234.7428, 229.8402, 225.1664, 220.3534, 215.594, 210.6886, 205.7876, 201.65, 197.228, 192.8036, 188.1666, 184.0818, 180.0824, 176.2574, 172.302, 168.1644, 164.0056, 160.3802, 156.7192, 152.5234, 149.2084, 145.831, 142.485, 139.1112, 135.4764, 131.76, 129.3368, 126.5538, 122.5058, 119.2646, 116.5902, 113.3818, 110.8998, 107.9532, 105.2062, 102.2798, 99.4728, 96.9582, 94.3292, 92.171, 89.7809999999999, 87.5716, 84.7048, 82.5322, 79.875, 78.3972, 75.3464, 73.7274, 71.2834, 70.1444, 68.4263999999999, 66.0166, 64.018, 62.0437999999999, 60.3399999999999, 58.6856, 57.9836, 55.0311999999999, 54.6769999999999, 52.3188, 51.4846, 49.4423999999999, 47.739, 46.1487999999999, 44.9202, 43.4059999999999, 42.5342000000001, 41.2834, 38.8954000000001, 38.3286000000001, 36.2146, 36.6684, 35.9946, 33.123, 33.4338, 31.7378000000001, 29.076, 28.9692, 27.4964, 27.0998, 25.9864, 26.7754, 24.3208, 23.4838, 22.7388000000001, 24.0758000000001, 21.9097999999999, 20.9728, 19.9228000000001, 19.9292, 16.617, 17.05, 18.2996000000001, 15.6128000000001, 15.7392, 14.5174, 13.6322, 12.2583999999999, 13.3766000000001, 11.423, 13.1232, 9.51639999999998, 10.5938000000001, 9.59719999999993, 8.12220000000002, 9.76739999999995, 7.50440000000003, 7.56999999999994, 6.70440000000008, 6.41419999999994, 6.71019999999999, 5.60940000000005, 4.65219999999999, 6.84099999999989, 3.4072000000001, 3.97859999999991, 3.32760000000007, 5.52160000000003, 3.31860000000006, 2.06940000000009, 4.35400000000004, 1.57500000000005, 0.280799999999999, 2.12879999999996, -0.214799999999968, -0.0378000000000611, -0.658200000000079, 0.654800000000023, -0.0697999999999865, 0.858400000000074, -2.52700000000004, -2.1751999999999, -3.35539999999992, -1.04019999999991, -0.651000000000067, -2.14439999999991, -1.96659999999997, -3.97939999999994, -0.604400000000169, -3.08260000000018, -3.39159999999993, -5.29640000000018, -5.38920000000007, -5.08759999999984, -4.69900000000007, -5.23720000000003, -3.15779999999995, -4.97879999999986, -4.89899999999989, -7.48880000000008, -5.94799999999987, -5.68060000000014, -6.67180000000008, -4.70499999999993, -7.27779999999984, -4.6579999999999, -4.4362000000001, -4.32139999999981, -5.18859999999995, -6.66879999999992, -6.48399999999992, -5.1260000000002, -4.4032000000002, -6.13500000000022, -5.80819999999994, -4.16719999999987, -4.15039999999999, -7.45600000000013, -7.24080000000004, -9.83179999999993, -5.80420000000004, -8.6561999999999, -6.99940000000015, -10.5473999999999, -7.34139999999979, -6.80999999999995, -6.29719999999998, -6.23199999999997, },
+ // precision 10
+ { 737.1256, 724.4234, 711.1064, 698.4732, 685.4636, 673.0644, 660.488, 647.9654, 636.0832, 623.7864, 612.1992, 600.2176, 588.5228, 577.1716, 565.7752, 554.899, 543.6126, 532.6492, 521.9474, 511.5214, 501.1064, 490.6364, 480.2468, 470.4588, 460.3832, 451.0584, 440.8606, 431.3868, 422.5062, 413.1862, 404.463, 395.339, 386.1936, 378.1292, 369.1854, 361.2908, 353.3324, 344.8518, 337.5204, 329.4854, 321.9318, 314.552, 306.4658, 299.4256, 292.849, 286.152, 278.8956, 271.8792, 265.118, 258.62, 252.5132, 245.9322, 239.7726, 233.6086, 227.5332, 222.5918, 216.4294, 210.7662, 205.4106, 199.7338, 194.9012, 188.4486, 183.1556, 178.6338, 173.7312, 169.6264, 163.9526, 159.8742, 155.8326, 151.1966, 147.5594, 143.07, 140.037, 134.1804, 131.071, 127.4884, 124.0848, 120.2944, 117.333, 112.9626, 110.2902, 107.0814, 103.0334, 99.4832000000001, 96.3899999999999, 93.7202000000002, 90.1714000000002, 87.2357999999999, 85.9346, 82.8910000000001, 80.0264000000002, 78.3834000000002, 75.1543999999999, 73.8683999999998, 70.9895999999999, 69.4367999999999, 64.8701999999998, 65.0408000000002, 61.6738, 59.5207999999998, 57.0158000000001, 54.2302, 53.0962, 50.4985999999999, 52.2588000000001, 47.3914, 45.6244000000002, 42.8377999999998, 43.0072, 40.6516000000001, 40.2453999999998, 35.2136, 36.4546, 33.7849999999999, 33.2294000000002, 32.4679999999998, 30.8670000000002, 28.6507999999999, 28.9099999999999, 27.5983999999999, 26.1619999999998, 24.5563999999999, 23.2328000000002, 21.9484000000002, 21.5902000000001, 21.3346000000001, 17.7031999999999, 20.6111999999998, 19.5545999999999, 15.7375999999999, 17.0720000000001, 16.9517999999998, 15.326, 13.1817999999998, 14.6925999999999, 13.0859999999998, 13.2754, 10.8697999999999, 11.248, 7.3768, 4.72339999999986, 7.97899999999981, 8.7503999999999, 7.68119999999999, 9.7199999999998, 7.73919999999998, 5.6224000000002, 7.44560000000001, 6.6601999999998, 5.9058, 4.00199999999995, 4.51699999999983, 4.68240000000014, 3.86220000000003, 5.13639999999987, 5.98500000000013, 2.47719999999981, 2.61999999999989, 1.62800000000016, 4.65000000000009, 0.225599999999758, 0.831000000000131, -0.359400000000278, 1.27599999999984, -2.92559999999958, -0.0303999999996449, 2.37079999999969, -2.0033999999996, 0.804600000000391, 0.30199999999968, 1.1247999999996, -2.6880000000001, 0.0321999999996478, -1.18099999999959, -3.9402, -1.47940000000017, -0.188400000000001, -2.10720000000038, -2.04159999999956, -3.12880000000041, -4.16160000000036, -0.612799999999879, -3.48719999999958, -8.17900000000009, -5.37780000000021, -4.01379999999972, -5.58259999999973, -5.73719999999958, -7.66799999999967, -5.69520000000011, -1.1247999999996, -5.58520000000044, -8.04560000000038, -4.64840000000004, -11.6468000000004, -7.97519999999986, -5.78300000000036, -7.67420000000038, -10.6328000000003, -9.81720000000041, },
+ // precision 11
+ { 1476, 1449.6014, 1423.5802, 1397.7942, 1372.3042, 1347.2062, 1321.8402, 1297.2292, 1272.9462, 1248.9926, 1225.3026, 1201.4252, 1178.0578, 1155.6092, 1132.626, 1110.5568, 1088.527, 1066.5154, 1045.1874, 1024.3878, 1003.37, 982.1972, 962.5728, 942.1012, 922.9668, 903.292, 884.0772, 864.8578, 846.6562, 828.041, 809.714, 792.3112, 775.1806, 757.9854, 740.656, 724.346, 707.5154, 691.8378, 675.7448, 659.6722, 645.5722, 630.1462, 614.4124, 600.8728, 585.898, 572.408, 558.4926, 544.4938, 531.6776, 517.282, 505.7704, 493.1012, 480.7388, 467.6876, 456.1872, 445.5048, 433.0214, 420.806, 411.409, 400.4144, 389.4294, 379.2286, 369.651, 360.6156, 350.337, 342.083, 332.1538, 322.5094, 315.01, 305.6686, 298.1678, 287.8116, 280.9978, 271.9204, 265.3286, 257.5706, 249.6014, 242.544, 235.5976, 229.583, 220.9438, 214.672, 208.2786, 201.8628, 195.1834, 191.505, 186.1816, 178.5188, 172.2294, 167.8908, 161.0194, 158.052, 151.4588, 148.1596, 143.4344, 138.5238, 133.13, 127.6374, 124.8162, 118.7894, 117.3984, 114.6078, 109.0858, 105.1036, 103.6258, 98.6018000000004, 95.7618000000002, 93.5821999999998, 88.5900000000001, 86.9992000000002, 82.8800000000001, 80.4539999999997, 74.6981999999998, 74.3644000000004, 73.2914000000001, 65.5709999999999, 66.9232000000002, 65.1913999999997, 62.5882000000001, 61.5702000000001, 55.7035999999998, 56.1764000000003, 52.7596000000003, 53.0302000000001, 49.0609999999997, 48.4694, 44.933, 46.0474000000004, 44.7165999999997, 41.9416000000001, 39.9207999999999, 35.6328000000003, 35.5276000000003, 33.1934000000001, 33.2371999999996, 33.3864000000003, 33.9228000000003, 30.2371999999996, 29.1373999999996, 25.2272000000003, 24.2942000000003, 19.8338000000003, 18.9005999999999, 23.0907999999999, 21.8544000000002, 19.5176000000001, 15.4147999999996, 16.9314000000004, 18.6737999999996, 12.9877999999999, 14.3688000000002, 12.0447999999997, 15.5219999999999, 12.5299999999997, 14.5940000000001, 14.3131999999996, 9.45499999999993, 12.9441999999999, 3.91139999999996, 13.1373999999996, 5.44720000000052, 9.82779999999912, 7.87279999999919, 3.67760000000089, 5.46980000000076, 5.55099999999948, 5.65979999999945, 3.89439999999922, 3.1275999999998, 5.65140000000065, 6.3062000000009, 3.90799999999945, 1.87060000000019, 5.17020000000048, 2.46680000000015, 0.770000000000437, -3.72340000000077, 1.16400000000067, 8.05340000000069, 0.135399999999208, 2.15940000000046, 0.766999999999825, 1.0594000000001, 3.15500000000065, -0.287399999999252, 2.37219999999979, -2.86620000000039, -1.63199999999961, -2.22979999999916, -0.15519999999924, -1.46039999999994, -0.262199999999211, -2.34460000000036, -2.8078000000005, -3.22179999999935, -5.60159999999996, -8.42200000000048, -9.43740000000071, 0.161799999999857, -10.4755999999998, -10.0823999999993, },
+ // precision 12
+ { 2953, 2900.4782, 2848.3568, 2796.3666, 2745.324, 2694.9598, 2644.648, 2595.539, 2546.1474, 2498.2576, 2450.8376, 2403.6076, 2357.451, 2311.38, 2266.4104, 2221.5638, 2176.9676, 2134.193, 2090.838, 2048.8548, 2007.018, 1966.1742, 1925.4482, 1885.1294, 1846.4776, 1807.4044, 1768.8724, 1731.3732, 1693.4304, 1657.5326, 1621.949, 1586.5532, 1551.7256, 1517.6182, 1483.5186, 1450.4528, 1417.865, 1385.7164, 1352.6828, 1322.6708, 1291.8312, 1260.9036, 1231.476, 1201.8652, 1173.6718, 1145.757, 1119.2072, 1092.2828, 1065.0434, 1038.6264, 1014.3192, 988.5746, 965.0816, 940.1176, 917.9796, 894.5576, 871.1858, 849.9144, 827.1142, 805.0818, 783.9664, 763.9096, 742.0816, 724.3962, 706.3454, 688.018, 667.4214, 650.3106, 633.0686, 613.8094, 597.818, 581.4248, 563.834, 547.363, 531.5066, 520.455400000001, 505.583199999999, 488.366, 476.480799999999, 459.7682, 450.0522, 434.328799999999, 423.952799999999, 408.727000000001, 399.079400000001, 387.252200000001, 373.987999999999, 360.852000000001, 351.6394, 339.642, 330.902400000001, 322.661599999999, 311.662200000001, 301.3254, 291.7484, 279.939200000001, 276.7508, 263.215200000001, 254.811400000001, 245.5494, 242.306399999999, 234.8734, 223.787200000001, 217.7156, 212.0196, 200.793, 195.9748, 189.0702, 182.449199999999, 177.2772, 170.2336, 164.741, 158.613600000001, 155.311, 147.5964, 142.837, 137.3724, 132.0162, 130.0424, 121.9804, 120.451800000001, 114.8968, 111.585999999999, 105.933199999999, 101.705, 98.5141999999996, 95.0488000000005, 89.7880000000005, 91.4750000000004, 83.7764000000006, 80.9698000000008, 72.8574000000008, 73.1615999999995, 67.5838000000003, 62.6263999999992, 63.2638000000006, 66.0977999999996, 52.0843999999997, 58.9956000000002, 47.0912000000008, 46.4956000000002, 48.4383999999991, 47.1082000000006, 43.2392, 37.2759999999998, 40.0283999999992, 35.1864000000005, 35.8595999999998, 32.0998, 28.027, 23.6694000000007, 33.8266000000003, 26.3736000000008, 27.2008000000005, 21.3245999999999, 26.4115999999995, 23.4521999999997, 19.5013999999992, 19.8513999999996, 10.7492000000002, 18.6424000000006, 13.1265999999996, 18.2436000000016, 6.71860000000015, 3.39459999999963, 6.33759999999893, 7.76719999999841, 0.813999999998487, 3.82819999999992, 0.826199999999517, 8.07440000000133, -1.59080000000176, 5.01780000000144, 0.455399999998917, -0.24199999999837, 0.174800000000687, -9.07640000000174, -4.20160000000033, -3.77520000000004, -4.75179999999818, -5.3724000000002, -8.90680000000066, -6.10239999999976, -5.74120000000039, -9.95339999999851, -3.86339999999836, -13.7304000000004, -16.2710000000006, -7.51359999999841, -3.30679999999847, -13.1339999999982, -10.0551999999989, -6.72019999999975, -8.59660000000076, -10.9307999999983, -1.8775999999998, -4.82259999999951, -13.7788, -21.6470000000008, -10.6735999999983, -15.7799999999988, },
+ // precision 13
+ { 5907.5052, 5802.2672, 5697.347, 5593.5794, 5491.2622, 5390.5514, 5290.3376, 5191.6952, 5093.5988, 4997.3552, 4902.5972, 4808.3082, 4715.5646, 4624.109, 4533.8216, 4444.4344, 4356.3802, 4269.2962, 4183.3784, 4098.292, 4014.79, 3932.4574, 3850.6036, 3771.2712, 3691.7708, 3615.099, 3538.1858, 3463.4746, 3388.8496, 3315.6794, 3244.5448, 3173.7516, 3103.3106, 3033.6094, 2966.5642, 2900.794, 2833.7256, 2769.81, 2707.3196, 2644.0778, 2583.9916, 2523.4662, 2464.124, 2406.073, 2347.0362, 2292.1006, 2238.1716, 2182.7514, 2128.4884, 2077.1314, 2025.037, 1975.3756, 1928.933, 1879.311, 1831.0006, 1783.2144, 1738.3096, 1694.5144, 1649.024, 1606.847, 1564.7528, 1525.3168, 1482.5372, 1443.9668, 1406.5074, 1365.867, 1329.2186, 1295.4186, 1257.9716, 1225.339, 1193.2972, 1156.3578, 1125.8686, 1091.187, 1061.4094, 1029.4188, 1000.9126, 972.3272, 944.004199999999, 915.7592, 889.965, 862.834200000001, 840.4254, 812.598399999999, 785.924200000001, 763.050999999999, 741.793799999999, 721.466, 699.040799999999, 677.997200000002, 649.866999999998, 634.911800000002, 609.8694, 591.981599999999, 570.2922, 557.129199999999, 538.3858, 521.872599999999, 502.951400000002, 495.776399999999, 475.171399999999, 459.751, 439.995200000001, 426.708999999999, 413.7016, 402.3868, 387.262599999998, 372.0524, 357.050999999999, 342.5098, 334.849200000001, 322.529399999999, 311.613799999999, 295.848000000002, 289.273000000001, 274.093000000001, 263.329600000001, 251.389599999999, 245.7392, 231.9614, 229.7952, 217.155200000001, 208.9588, 199.016599999999, 190.839199999999, 180.6976, 176.272799999999, 166.976999999999, 162.5252, 151.196400000001, 149.386999999999, 133.981199999998, 130.0586, 130.164000000001, 122.053400000001, 110.7428, 108.1276, 106.232400000001, 100.381600000001, 98.7668000000012, 86.6440000000002, 79.9768000000004, 82.4722000000002, 68.7026000000005, 70.1186000000016, 71.9948000000004, 58.998599999999, 59.0492000000013, 56.9818000000014, 47.5338000000011, 42.9928, 51.1591999999982, 37.2740000000013, 42.7220000000016, 31.3734000000004, 26.8090000000011, 25.8934000000008, 26.5286000000015, 29.5442000000003, 19.3503999999994, 26.0760000000009, 17.9527999999991, 14.8419999999969, 10.4683999999979, 8.65899999999965, 9.86720000000059, 4.34139999999752, -0.907800000000861, -3.32080000000133, -0.936199999996461, -11.9916000000012, -8.87000000000262, -6.33099999999831, -11.3366000000024, -15.9207999999999, -9.34659999999712, -15.5034000000014, -19.2097999999969, -15.357799999998, -28.2235999999975, -30.6898000000001, -19.3271999999997, -25.6083999999973, -24.409599999999, -13.6385999999984, -33.4473999999973, -32.6949999999997, -28.9063999999998, -31.7483999999968, -32.2935999999972, -35.8329999999987, -47.620600000002, -39.0855999999985, -33.1434000000008, -46.1371999999974, -37.5892000000022, -46.8164000000033, -47.3142000000007, -60.2914000000019, -37.7575999999972, },
+ // precision 14
+ { 11816.475, 11605.0046, 11395.3792, 11188.7504, 10984.1814, 10782.0086, 10582.0072, 10384.503, 10189.178, 9996.2738, 9806.0344, 9617.9798, 9431.394, 9248.7784, 9067.6894, 8889.6824, 8712.9134, 8538.8624, 8368.4944, 8197.7956, 8031.8916, 7866.6316, 7703.733, 7544.5726, 7386.204, 7230.666, 7077.8516, 6926.7886, 6778.6902, 6631.9632, 6487.304, 6346.7486, 6206.4408, 6070.202, 5935.2576, 5799.924, 5671.0324, 5541.9788, 5414.6112, 5290.0274, 5166.723, 5047.6906, 4929.162, 4815.1406, 4699.127, 4588.5606, 4477.7394, 4369.4014, 4264.2728, 4155.9224, 4055.581, 3955.505, 3856.9618, 3761.3828, 3666.9702, 3575.7764, 3482.4132, 3395.0186, 3305.8852, 3221.415, 3138.6024, 3056.296, 2970.4494, 2896.1526, 2816.8008, 2740.2156, 2670.497, 2594.1458, 2527.111, 2460.8168, 2387.5114, 2322.9498, 2260.6752, 2194.2686, 2133.7792, 2074.767, 2015.204, 1959.4226, 1898.6502, 1850.006, 1792.849, 1741.4838, 1687.9778, 1638.1322, 1589.3266, 1543.1394, 1496.8266, 1447.8516, 1402.7354, 1361.9606, 1327.0692, 1285.4106, 1241.8112, 1201.6726, 1161.973, 1130.261, 1094.2036, 1048.2036, 1020.6436, 990.901400000002, 961.199800000002, 924.769800000002, 899.526400000002, 872.346400000002, 834.375, 810.432000000001, 780.659800000001, 756.013800000001, 733.479399999997, 707.923999999999, 673.858, 652.222399999999, 636.572399999997, 615.738599999997, 586.696400000001, 564.147199999999, 541.679600000003, 523.943599999999, 505.714599999999, 475.729599999999, 461.779600000002, 449.750800000002, 439.020799999998, 412.7886, 400.245600000002, 383.188199999997, 362.079599999997, 357.533799999997, 334.319000000003, 327.553399999997, 308.559399999998, 291.270199999999, 279.351999999999, 271.791400000002, 252.576999999997, 247.482400000001, 236.174800000001, 218.774599999997, 220.155200000001, 208.794399999999, 201.223599999998, 182.995600000002, 185.5268, 164.547400000003, 176.5962, 150.689599999998, 157.8004, 138.378799999999, 134.021200000003, 117.614399999999, 108.194000000003, 97.0696000000025, 89.6042000000016, 95.6030000000028, 84.7810000000027, 72.635000000002, 77.3482000000004, 59.4907999999996, 55.5875999999989, 50.7346000000034, 61.3916000000027, 50.9149999999936, 39.0384000000049, 58.9395999999979, 29.633600000001, 28.2032000000036, 26.0078000000067, 17.0387999999948, 9.22000000000116, 13.8387999999977, 8.07240000000456, 14.1549999999988, 15.3570000000036, 3.42660000000615, 6.24820000000182, -2.96940000000177, -8.79940000000352, -5.97860000000219, -14.4048000000039, -3.4143999999942, -13.0148000000045, -11.6977999999945, -25.7878000000055, -22.3185999999987, -24.409599999999, -31.9756000000052, -18.9722000000038, -22.8678000000073, -30.8972000000067, -32.3715999999986, -22.3907999999938, -43.6720000000059, -35.9038, -39.7492000000057, -54.1641999999993, -45.2749999999942, -42.2989999999991, -44.1089999999967, -64.3564000000042, -49.9551999999967, -42.6116000000038, },
+ // precision 15
+ { 23634.0036, 23210.8034, 22792.4744, 22379.1524, 21969.7928, 21565.326, 21165.3532, 20770.2806, 20379.9892, 19994.7098, 19613.318, 19236.799, 18865.4382, 18498.8244, 18136.5138, 17778.8668, 17426.2344, 17079.32, 16734.778, 16397.2418, 16063.3324, 15734.0232, 15409.731, 15088.728, 14772.9896, 14464.1402, 14157.5588, 13855.5958, 13559.3296, 13264.9096, 12978.326, 12692.0826, 12413.8816, 12137.3192, 11870.2326, 11602.5554, 11340.3142, 11079.613, 10829.5908, 10583.5466, 10334.0344, 10095.5072, 9859.694, 9625.2822, 9395.7862, 9174.0586, 8957.3164, 8738.064, 8524.155, 8313.7396, 8116.9168, 7913.542, 7718.4778, 7521.65, 7335.5596, 7154.2906, 6968.7396, 6786.3996, 6613.236, 6437.406, 6270.6598, 6107.7958, 5945.7174, 5787.6784, 5635.5784, 5482.308, 5337.9784, 5190.0864, 5045.9158, 4919.1386, 4771.817, 4645.7742, 4518.4774, 4385.5454, 4262.6622, 4142.74679999999, 4015.5318, 3897.9276, 3790.7764, 3685.13800000001, 3573.6274, 3467.9706, 3368.61079999999, 3271.5202, 3170.3848, 3076.4656, 2982.38400000001, 2888.4664, 2806.4868, 2711.9564, 2634.1434, 2551.3204, 2469.7662, 2396.61139999999, 2318.9902, 2243.8658, 2171.9246, 2105.01360000001, 2028.8536, 1960.9952, 1901.4096, 1841.86079999999, 1777.54700000001, 1714.5802, 1654.65059999999, 1596.311, 1546.2016, 1492.3296, 1433.8974, 1383.84600000001, 1339.4152, 1293.5518, 1245.8686, 1193.50659999999, 1162.27959999999, 1107.19439999999, 1069.18060000001, 1035.09179999999, 999.679000000004, 957.679999999993, 925.300199999998, 888.099400000006, 848.638600000006, 818.156400000007, 796.748399999997, 752.139200000005, 725.271200000003, 692.216, 671.633600000001, 647.939799999993, 621.670599999998, 575.398799999995, 561.226599999995, 532.237999999998, 521.787599999996, 483.095799999996, 467.049599999998, 465.286399999997, 415.548599999995, 401.047399999996, 380.607999999993, 377.362599999993, 347.258799999996, 338.371599999999, 310.096999999994, 301.409199999995, 276.280799999993, 265.586800000005, 258.994399999996, 223.915999999997, 215.925399999993, 213.503800000006, 191.045400000003, 166.718200000003, 166.259000000005, 162.941200000001, 148.829400000002, 141.645999999993, 123.535399999993, 122.329800000007, 89.473399999988, 80.1962000000058, 77.5457999999926, 59.1056000000099, 83.3509999999951, 52.2906000000075, 36.3979999999865, 40.6558000000077, 42.0003999999899, 19.6630000000005, 19.7153999999864, -8.38539999999921, -0.692799999989802, 0.854800000000978, 3.23219999999856, -3.89040000000386, -5.25880000001052, -24.9052000000083, -22.6837999999989, -26.4286000000138, -34.997000000003, -37.0216000000073, -43.430400000012, -58.2390000000014, -68.8034000000043, -56.9245999999985, -57.8583999999973, -77.3097999999882, -73.2793999999994, -81.0738000000129, -87.4530000000086, -65.0254000000132, -57.296399999992, -96.2746000000043, -103.25, -96.081600000005, -91.5542000000132, -102.465200000006, -107.688599999994, -101.458000000013, -109.715800000005, },
+ // precision 16
+ { 47270, 46423.3584, 45585.7074, 44757.152, 43938.8416, 43130.9514, 42330.03, 41540.407, 40759.6348, 39988.206, 39226.5144, 38473.2096, 37729.795, 36997.268, 36272.6448, 35558.665, 34853.0248, 34157.4472, 33470.5204, 32793.5742, 32127.0194, 31469.4182, 30817.6136, 30178.6968, 29546.8908, 28922.8544, 28312.271, 27707.0924, 27114.0326, 26526.692, 25948.6336, 25383.7826, 24823.5998, 24272.2974, 23732.2572, 23201.4976, 22674.2796, 22163.6336, 21656.515, 21161.7362, 20669.9368, 20189.4424, 19717.3358, 19256.3744, 18795.9638, 18352.197, 17908.5738, 17474.391, 17052.918, 16637.2236, 16228.4602, 15823.3474, 15428.6974, 15043.0284, 14667.6278, 14297.4588, 13935.2882, 13578.5402, 13234.6032, 12882.1578, 12548.0728, 12219.231, 11898.0072, 11587.2626, 11279.9072, 10973.5048, 10678.5186, 10392.4876, 10105.2556, 9825.766, 9562.5444, 9294.2222, 9038.2352, 8784.848, 8533.2644, 8301.7776, 8058.30859999999, 7822.94579999999, 7599.11319999999, 7366.90779999999, 7161.217, 6957.53080000001, 6736.212, 6548.21220000001, 6343.06839999999, 6156.28719999999, 5975.15419999999, 5791.75719999999, 5621.32019999999, 5451.66, 5287.61040000001, 5118.09479999999, 4957.288, 4798.4246, 4662.17559999999, 4512.05900000001, 4364.68539999999, 4220.77720000001, 4082.67259999999, 3957.19519999999, 3842.15779999999, 3699.3328, 3583.01180000001, 3473.8964, 3338.66639999999, 3233.55559999999, 3117.799, 3008.111, 2909.69140000001, 2814.86499999999, 2719.46119999999, 2624.742, 2532.46979999999, 2444.7886, 2370.1868, 2272.45259999999, 2196.19260000001, 2117.90419999999, 2023.2972, 1969.76819999999, 1885.58979999999, 1833.2824, 1733.91200000001, 1682.54920000001, 1604.57980000001, 1556.11240000001, 1491.3064, 1421.71960000001, 1371.22899999999, 1322.1324, 1264.7892, 1196.23920000001, 1143.8474, 1088.67240000001, 1073.60380000001, 1023.11660000001, 959.036400000012, 927.433199999999, 906.792799999996, 853.433599999989, 841.873800000001, 791.1054, 756.899999999994, 704.343200000003, 672.495599999995, 622.790399999998, 611.254799999995, 567.283200000005, 519.406599999988, 519.188400000014, 495.312800000014, 451.350799999986, 443.973399999988, 431.882199999993, 392.027000000002, 380.924200000009, 345.128999999986, 298.901400000002, 287.771999999997, 272.625, 247.253000000026, 222.490600000019, 223.590000000026, 196.407599999977, 176.425999999978, 134.725199999986, 132.4804, 110.445599999977, 86.7939999999944, 56.7038000000175, 64.915399999998, 38.3726000000024, 37.1606000000029, 46.170999999973, 49.1716000000015, 15.3362000000197, 6.71639999997569, -34.8185999999987, -39.4476000000141, 12.6830000000191, -12.3331999999937, -50.6565999999875, -59.9538000000175, -65.1054000000004, -70.7576000000117, -106.325200000021, -126.852200000023, -110.227599999984, -132.885999999999, -113.897200000007, -142.713800000027, -151.145399999979, -150.799200000009, -177.756200000003, -156.036399999983, -182.735199999996, -177.259399999981, -198.663600000029, -174.577600000019, -193.84580000001, },
+ // precision 17
+ { 94541, 92848.811, 91174.019, 89517.558, 87879.9705, 86262.7565, 84663.5125, 83083.7435, 81521.7865, 79977.272, 78455.9465, 76950.219, 75465.432, 73994.152, 72546.71, 71115.2345, 69705.6765, 68314.937, 66944.2705, 65591.255, 64252.9485, 62938.016, 61636.8225, 60355.592, 59092.789, 57850.568, 56624.518, 55417.343, 54231.1415, 53067.387, 51903.526, 50774.649, 49657.6415, 48561.05, 47475.7575, 46410.159, 45364.852, 44327.053, 43318.4005, 42325.6165, 41348.4595, 40383.6265, 39436.77, 38509.502, 37594.035, 36695.939, 35818.6895, 34955.691, 34115.8095, 33293.949, 32465.0775, 31657.6715, 30877.2585, 30093.78, 29351.3695, 28594.1365, 27872.115, 27168.7465, 26477.076, 25774.541, 25106.5375, 24452.5135, 23815.5125, 23174.0655, 22555.2685, 21960.2065, 21376.3555, 20785.1925, 20211.517, 19657.0725, 19141.6865, 18579.737, 18081.3955, 17578.995, 17073.44, 16608.335, 16119.911, 15651.266, 15194.583, 14749.0495, 14343.4835, 13925.639, 13504.509, 13099.3885, 12691.2855, 12328.018, 11969.0345, 11596.5145, 11245.6355, 10917.6575, 10580.9785, 10277.8605, 9926.58100000001, 9605.538, 9300.42950000003, 8989.97850000003, 8728.73249999998, 8448.3235, 8175.31050000002, 7898.98700000002, 7629.79100000003, 7413.76199999999, 7149.92300000001, 6921.12650000001, 6677.1545, 6443.28000000003, 6278.23450000002, 6014.20049999998, 5791.20299999998, 5605.78450000001, 5438.48800000001, 5234.2255, 5059.6825, 4887.43349999998, 4682.935, 4496.31099999999, 4322.52250000002, 4191.42499999999, 4021.24200000003, 3900.64799999999, 3762.84250000003, 3609.98050000001, 3502.29599999997, 3363.84250000003, 3206.54849999998, 3079.70000000001, 2971.42300000001, 2867.80349999998, 2727.08100000001, 2630.74900000001, 2496.6165, 2440.902, 2356.19150000002, 2235.58199999999, 2120.54149999999, 2012.25449999998, 1933.35600000003, 1820.93099999998, 1761.54800000001, 1663.09350000002, 1578.84600000002, 1509.48149999999, 1427.3345, 1379.56150000001, 1306.68099999998, 1212.63449999999, 1084.17300000001, 1124.16450000001, 1060.69949999999, 1007.48849999998, 941.194499999983, 879.880500000028, 836.007500000007, 782.802000000025, 748.385499999975, 647.991500000004, 626.730500000005, 570.776000000013, 484.000500000024, 513.98550000001, 418.985499999952, 386.996999999974, 370.026500000036, 355.496999999974, 356.731499999994, 255.92200000002, 259.094000000041, 205.434499999974, 165.374500000034, 197.347500000033, 95.718499999959, 67.6165000000037, 54.6970000000438, 31.7395000000251, -15.8784999999916, 8.42500000004657, -26.3754999999655, -118.425500000012, -66.6629999999423, -42.9745000000112, -107.364999999991, -189.839000000036, -162.611499999999, -164.964999999967, -189.079999999958, -223.931499999948, -235.329999999958, -269.639500000048, -249.087999999989, -206.475499999942, -283.04449999996, -290.667000000016, -304.561499999953, -336.784499999951, -380.386500000022, -283.280499999993, -364.533000000054, -389.059499999974, -364.454000000027, -415.748000000021, -417.155000000028, },
+ // precision 18
+ { 189083, 185696.913, 182348.774, 179035.946, 175762.762, 172526.444, 169329.754, 166166.099, 163043.269, 159958.91, 156907.912, 153906.845, 150924.199, 147996.568, 145093.457, 142239.233, 139421.475, 136632.27, 133889.588, 131174.2, 128511.619, 125868.621, 123265.385, 120721.061, 118181.769, 115709.456, 113252.446, 110840.198, 108465.099, 106126.164, 103823.469, 101556.618, 99308.004, 97124.508, 94937.803, 92833.731, 90745.061, 88677.627, 86617.47, 84650.442, 82697.833, 80769.132, 78879.629, 77014.432, 75215.626, 73384.587, 71652.482, 69895.93, 68209.301, 66553.669, 64921.981, 63310.323, 61742.115, 60205.018, 58698.658, 57190.657, 55760.865, 54331.169, 52908.167, 51550.273, 50225.254, 48922.421, 47614.533, 46362.049, 45098.569, 43926.083, 42736.03, 41593.473, 40425.26, 39316.237, 38243.651, 37170.617, 36114.609, 35084.19, 34117.233, 33206.509, 32231.505, 31318.728, 30403.404, 29540.0550000001, 28679.236, 27825.862, 26965.216, 26179.148, 25462.08, 24645.952, 23922.523, 23198.144, 22529.128, 21762.4179999999, 21134.779, 20459.117, 19840.818, 19187.04, 18636.3689999999, 17982.831, 17439.7389999999, 16874.547, 16358.2169999999, 15835.684, 15352.914, 14823.681, 14329.313, 13816.897, 13342.874, 12880.882, 12491.648, 12021.254, 11625.392, 11293.7610000001, 10813.697, 10456.209, 10099.074, 9755.39000000001, 9393.18500000006, 9047.57900000003, 8657.98499999999, 8395.85900000005, 8033, 7736.95900000003, 7430.59699999995, 7258.47699999996, 6924.58200000005, 6691.29399999999, 6357.92500000005, 6202.05700000003, 5921.19700000004, 5628.28399999999, 5404.96799999999, 5226.71100000001, 4990.75600000005, 4799.77399999998, 4622.93099999998, 4472.478, 4171.78700000001, 3957.46299999999, 3868.95200000005, 3691.14300000004, 3474.63100000005, 3341.67200000002, 3109.14000000001, 3071.97400000005, 2796.40399999998, 2756.17799999996, 2611.46999999997, 2471.93000000005, 2382.26399999997, 2209.22400000005, 2142.28399999999, 2013.96100000001, 1911.18999999994, 1818.27099999995, 1668.47900000005, 1519.65800000005, 1469.67599999998, 1367.13800000004, 1248.52899999998, 1181.23600000003, 1022.71900000004, 1088.20700000005, 959.03600000008, 876.095999999903, 791.183999999892, 703.337000000058, 731.949999999953, 586.86400000006, 526.024999999907, 323.004999999888, 320.448000000091, 340.672999999952, 309.638999999966, 216.601999999955, 102.922999999952, 19.2399999999907, -0.114000000059605, -32.6240000000689, -89.3179999999702, -153.497999999905, -64.2970000000205, -143.695999999996, -259.497999999905, -253.017999999924, -213.948000000091, -397.590000000084, -434.006000000052, -403.475000000093, -297.958000000101, -404.317000000039, -528.898999999976, -506.621000000043, -513.205000000075, -479.351000000024, -596.139999999898, -527.016999999993, -664.681000000099, -680.306000000099, -704.050000000047, -850.486000000034, -757.43200000003, -713.308999999892, }
+};
+
+static constexpr int Sizes[] = { 79, 159, 200, 200, 200, 201, 200, 201, 201, 200, 201, 201, 200, 201, 200 };
+
+static constexpr ui64 Thresholds[] = { 10, 20, 40, 80, 220, 400, 900, 1800, 3100, 6500, 11500, 20000, 50000, 120000, 350000 };
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NDetail
diff --git a/yt/yt/core/misc/id_generator.cpp b/yt/yt/core/misc/id_generator.cpp
new file mode 100644
index 0000000000..30d4133af3
--- /dev/null
+++ b/yt/yt/core/misc/id_generator.cpp
@@ -0,0 +1,31 @@
+#include "id_generator.h"
+#include "serialize.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui64 TIdGenerator::Next()
+{
+ return Current_++;
+}
+
+void TIdGenerator::Reset()
+{
+ Current_ = 0;
+}
+
+void TIdGenerator::Save(TStreamSaveContext& context) const
+{
+ NYT::Save(context, Current_.load());
+}
+
+void TIdGenerator::Load(TStreamLoadContext& context)
+{
+ Current_ = NYT::Load<ui64>(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/id_generator.h b/yt/yt/core/misc/id_generator.h
new file mode 100644
index 0000000000..dd1d5d9a95
--- /dev/null
+++ b/yt/yt/core/misc/id_generator.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A thread-safe generator producing increasing sequence of numbers.
+class TIdGenerator
+{
+public:
+ ui64 Next();
+ void Reset();
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ std::atomic<ui64> Current_ = 0;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/intern_registry-inl.h b/yt/yt/core/misc/intern_registry-inl.h
new file mode 100644
index 0000000000..80d49c4531
--- /dev/null
+++ b/yt/yt/core/misc/intern_registry-inl.h
@@ -0,0 +1,233 @@
+#ifndef INTERN_REGISTRY_INL_H_
+#error "Direct inclusion of this file is not allowed, include intern_registry.h"
+// For the sake of sane code completion.
+#include "intern_registry.h"
+#endif
+
+#include "serialize.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+template <class U>
+TInternedObjectDataPtr<T> TInternRegistryTraits<T>::ConstructData(
+ TInternRegistryPtr<T> registry,
+ U&& data)
+{
+ return New<TInternedObjectData<T>>(
+ std::forward<U>(data),
+ std::move(registry));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+template <class U>
+TInternedObject<T> TInternRegistry<T>::Intern(U&& data)
+{
+ if (TInternRegistry<T>::GetDefault()->GetData() == data) {
+ return TInternedObject<T>();
+ }
+ auto guard = Guard(Lock_);
+ typename TProfilerSet::insert_ctx context;
+ if (auto it = Registry_.find(data, context)) {
+ return TInternedObject<T>(MakeStrong(*it));
+ } else {
+ auto internedData = TInternRegistryTraits<T>::ConstructData(this, std::forward<U>(data));
+ it = Registry_.insert_direct(internedData.Get(), context);
+ internedData->Iterator_ = it;
+ return TInternedObject<T>(std::move(internedData));
+ }
+}
+
+template <class T>
+int TInternRegistry<T>::GetSize() const
+{
+ auto guard = Guard(Lock_);
+ return static_cast<int>(Registry_.size());
+}
+
+template <class T>
+TInternedObjectDataPtr<T> TInternRegistry<T>::GetDefault()
+{
+ static const auto Default = New<TInternedObjectData<T>>(T(), nullptr);
+ return Default;
+}
+
+template <class T>
+void TInternRegistry<T>::OnInternedDataDestroyed(TInternedObjectData<T>* data)
+{
+ auto guard = Guard(Lock_);
+ Registry_.erase(data->Iterator_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+size_t TInternRegistry<T>::THash::operator()(const TInternedObjectData<T>* internedData) const
+{
+ return internedData->GetHash();
+}
+
+template <class T>
+size_t TInternRegistry<T>::THash::operator()(const T& data) const
+{
+ return ::THash<T>()(data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+bool TInternRegistry<T>::TEqual::operator()(
+ const TInternedObjectData<T>* lhs,
+ const TInternedObjectData<T>* rhs) const
+{
+ return lhs == rhs;
+}
+
+template <class T>
+bool TInternRegistry<T>::TEqual::operator()(
+ const TInternedObjectData<T>* lhs,
+ const T& rhs) const
+{
+ return lhs->GetData() == rhs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TInternedObjectData<T>::~TInternedObjectData()
+{
+ if (Registry_) {
+ Registry_->OnInternedDataDestroyed(this);
+ }
+}
+
+template <class T>
+const T& TInternedObjectData<T>::GetData() const
+{
+ return Data_;
+}
+
+template <class T>
+size_t TInternedObjectData<T>::GetHash() const
+{
+ return Hash_;
+}
+
+template <class T>
+TInternedObjectData<T>::TInternedObjectData(T&& data, TInternRegistryPtr<T> registry)
+ : Data_(std::move(data))
+ , Hash_(THash<T>()(Data_))
+ , Registry_(std::move(registry))
+{ }
+
+template <class T>
+TInternedObjectData<T>::TInternedObjectData(const T& data, TInternRegistryPtr<T> registry)
+ : Data_(data)
+ , Hash_(THash<T>()(Data_))
+ , Registry_(std::move(registry))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TInternedObject<T>::TInternedObject()
+ : Data_(TInternRegistry<T>::GetDefault())
+{ }
+
+template <class T>
+TInternedObject<T>::operator bool() const
+{
+ return Data_.operator bool();
+}
+
+template <class T>
+const T& TInternedObject<T>::operator*() const
+{
+ return Data_->GetData();
+}
+
+template <class T>
+const T* TInternedObject<T>::operator->() const
+{
+ return &Data_->GetData();
+}
+
+template <class T>
+TInternedObjectDataPtr<T> TInternedObject<T>::ToDataPtr() const
+{
+ return Data_;
+}
+
+template <class T>
+TInternedObject<T> TInternedObject<T>::FromDataPtr(TInternedObjectDataPtr<T> data)
+{
+ return TInternedObject<T>(std::move(data));
+}
+
+template<class T>
+bool TInternedObject<T>::RefEqual(const TInternedObject <T>& lhs, const TInternedObject <T>& rhs)
+{
+ return lhs.Data_ == rhs.Data_;
+}
+
+template <class T>
+TInternedObject<T>::TInternedObject(TInternedObjectDataPtr<T> data)
+ : Data_(std::move(data))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TInternedObjectSerializer
+{
+ template <class C, class T>
+ static void Save(C& context, const TInternedObject<T>& object)
+ {
+ using NYT::Save;
+
+ auto key = context.RegisterRefCountedEntity(object.ToDataPtr());
+ Save(context, key);
+ if (key == TEntityStreamSaveContext::InlineKey) {
+ Save(context, *object);
+ }
+ }
+
+ template <class C, class T>
+ static void Load(C& context, TInternedObject<T>& object)
+ {
+ using NYT::Load;
+
+ auto key = NYT::LoadSuspended<TEntitySerializationKey>(context);
+ if (key == TEntityStreamSaveContext::InlineKey) {
+ SERIALIZATION_DUMP_INDENT(context) {
+ auto value = Load<T>(context);
+ const auto& registry = context.template GetInternRegistry<T>();
+ object = registry->Intern(std::move(value));
+ auto loadedKey = context.RegisterRefCountedEntity(object.ToDataPtr());
+ SERIALIZATION_DUMP_WRITE(context, "objref %v", loadedKey.Index);
+ }
+ } else {
+ object = TInternedObject<T>::FromDataPtr(context.template GetRefCountedEntity<TInternedObjectData<T>>(key));
+ SERIALIZATION_DUMP_WRITE(context, "objref %v", key.Index);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class C>
+struct TSerializerTraits<
+ TInternedObject<T>,
+ C,
+ void
+>
+{
+ using TSerializer = TInternedObjectSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/intern_registry.h b/yt/yt/core/misc/intern_registry.h
new file mode 100644
index 0000000000..07f8a9e35d
--- /dev/null
+++ b/yt/yt/core/misc/intern_registry.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/hash_set.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TInternRegistryTraits
+{
+ template <class U>
+ static TInternedObjectDataPtr<T> ConstructData(
+ TInternRegistryPtr<T> registry,
+ U&& data);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TInternRegistry
+ : public TRefCounted
+{
+public:
+ template <class U>
+ TInternedObject<T> Intern(U&& data);
+
+ int GetSize() const;
+ void Clear();
+
+ static TInternedObjectDataPtr<T> GetDefault();
+
+private:
+ friend class TInternedObjectData<T>;
+
+ void OnInternedDataDestroyed(TInternedObjectData<T>* data);
+
+ struct THash
+ {
+ size_t operator() (const TInternedObjectData<T>* internedData) const;
+ size_t operator() (const T& data) const;
+ };
+
+ struct TEqual
+ {
+ bool operator() (const TInternedObjectData<T>* lhs, const TInternedObjectData<T>* rhs) const;
+ bool operator() (const TInternedObjectData<T>* lhs, const T& rhs) const;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+
+ using TProfilerSet = THashSet<TInternedObjectData<T>*, THash, TEqual>;
+ TProfilerSet Registry_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TInternedObjectData
+ : public TRefCounted
+{
+public:
+ ~TInternedObjectData();
+
+ const T& GetData() const;
+ size_t GetHash() const;
+
+private:
+ friend class TInternRegistry<T>;
+ DECLARE_NEW_FRIEND()
+
+ const T Data_;
+ const size_t Hash_;
+ const TInternRegistryPtr<T> Registry_;
+ typename TInternRegistry<T>::TProfilerSet::iterator Iterator_;
+
+ TInternedObjectData(const T& data, TInternRegistryPtr<T> registry);
+ TInternedObjectData(T&& data, TInternRegistryPtr<T> registry);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TInternedObject
+{
+public:
+ TInternedObject();
+
+ TInternedObject(const TInternedObject<T>& other) = default;
+ TInternedObject(TInternedObject<T>&& other) = default;
+
+ TInternedObject<T>& operator=(const TInternedObject<T>& other) = default;
+ TInternedObject<T>& operator=(TInternedObject<T>&& other) = default;
+
+ explicit operator bool() const;
+
+ const T& operator*() const;
+ const T* operator->() const;
+
+ TInternedObjectDataPtr<T> ToDataPtr() const;
+ static TInternedObject<T> FromDataPtr(TInternedObjectDataPtr<T> data);
+
+ static bool RefEqual(const TInternedObject<T>& lhs, const TInternedObject<T>& rhs);
+
+private:
+ friend class TInternRegistry<T>;
+
+ TInternedObjectDataPtr<T> Data_;
+
+ explicit TInternedObject(TInternedObjectDataPtr<T> data);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define INTERN_REGISTRY_INL_H_
+#include "intern_registry-inl.h"
+#undef INTERN_REGISTRY_INL_H_
diff --git a/yt/yt/core/misc/intrusive_ptr.h b/yt/yt/core/misc/intrusive_ptr.h
new file mode 100644
index 0000000000..0a9b9ab14d
--- /dev/null
+++ b/yt/yt/core/misc/intrusive_ptr.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/intrusive_ptr.h>
diff --git a/yt/yt/core/misc/isa_crc64/checksum.cpp b/yt/yt/core/misc/isa_crc64/checksum.cpp
new file mode 100644
index 0000000000..6bf5c65283
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/checksum.cpp
@@ -0,0 +1,85 @@
+#include "checksum.h"
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern "C" uint64_t crc64_yt_norm_base(
+ uint64_t init_crc, //!< initial CRC value, 64 bits
+ const unsigned char *buf, //!< buffer to calculate CRC on
+ uint64_t len //!< buffer length in bytes (64-bit data)
+);
+
+uint64_t IsaCrcImplBase(const void* data, size_t length, uint64_t seed)
+{
+ seed = ~seed;
+
+ uint64_t checksum = crc64_yt_norm_base(seed, reinterpret_cast<const unsigned char*>(data), length);
+
+ return ~checksum;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+
+namespace NYT::NIsaCrc64
+{
+
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t CrcImplBase(const void* data, size_t length, uint64_t seed)
+{
+ return IsaCrcImplBase(data, length, seed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NIsaCrc64
+
+#ifdef ISA_CRC64_FAST_IMPL_SUPPORTED
+
+namespace
+{
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern "C" uint64_t crc64_yt_norm_by8(
+ uint64_t init_crc, //!< initial CRC value, 64 bits
+ const unsigned char *buf, //!< buffer to calculate CRC on
+ uint64_t len //!< buffer length in bytes (64-bit data)
+);
+
+uint64_t IsaCrcImplFast(const void* data, size_t length, uint64_t seed)
+{
+ // These manipulations with seed are necessary in order to maintain compatibility.
+ // Comparing YT's old implenetation to ISA's one, the latter performs `NOT` operations
+ // before and after the function call, so we use `NOT` here to neutralize it.
+
+ seed = ~seed;
+
+ uint64_t checksum = crc64_yt_norm_by8(seed, reinterpret_cast<const unsigned char*>(data), length);
+
+ return ~checksum;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+namespace NYT::NIsaCrc64
+{
+
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t CrcImplFast(const void* data, size_t length, uint64_t seed)
+{
+ return IsaCrcImplFast(data, length, seed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NIsaCrc64
+
+#endif // ISA_CRC64_FAST_IMPL_SUPPORTED
diff --git a/yt/yt/core/misc/isa_crc64/checksum.h b/yt/yt/core/misc/isa_crc64/checksum.h
new file mode 100644
index 0000000000..4d3aea36bd
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/checksum.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <cstdint>
+#include <cstddef>
+
+/*
+ This file presents implementation of CRC64 with polynomial
+ that was previously specified in YT (0xE543279765927881).
+
+ Implementation is based on one from ISA-l (contrib/libs/isa-l/crc) with assembly and lots of pclmulqdq.
+ In ISA-l there are 6 of them: (iso poly, ecma poly, jones poly) x (normal form, reflected form).
+
+ In order to support our own polynomial, we had to patch their asm file. There are few details to mention:
+ 1) The implementation, supported in YT before, was one with normal form.
+
+ 2) All 3 normal form implementations only differ in rk01, ... rk20 constants.
+ Their meaning is described in `crc64_yt_norm_by8.asm`. You may also refer to https://github.com/intel/isa-l/issues/88.
+ If you want to generate such constants for your polynomial utilities/caclulations_for_matlab.py may be helpful.
+*/
+
+#ifdef __x86_64__
+ #define ISA_CRC64_FAST_IMPL_SUPPORTED
+#endif
+
+namespace NYT::NIsaCrc64 {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef ISA_CRC64_FAST_IMPL_SUPPORTED
+uint64_t CrcImplFast(const void* data, size_t length, uint64_t seed);
+#endif
+
+uint64_t CrcImplBase(const void* data, size_t length, uint64_t seed);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NIsaCrc64
diff --git a/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm b/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm
new file mode 100644
index 0000000000..744f8fccde
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/crc64_yt_norm_by8.asm
@@ -0,0 +1,589 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2016 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+; Function API:
+; uint64_t crc64_ecma_norm_by8(
+; uint64_t init_crc, //initial CRC value, 64 bits
+; const unsigned char *buf, //buffer pointer to calculate CRC on
+; uint64_t len //buffer length in bytes (64-bit data)
+; );
+;
+; yasm -f x64 -f elf64 -X gnu -g dwarf2 crc64_ecma_norm_by8
+%include "reg_sizes.asm"
+
+%define fetch_dist 1024
+
+[bits 64]
+default rel
+
+section .text
+
+%ifidn __OUTPUT_FORMAT__, win64
+ %xdefine arg1 rcx
+ %xdefine arg2 rdx
+ %xdefine arg3 r8
+%else
+ %xdefine arg1 rdi
+ %xdefine arg2 rsi
+ %xdefine arg3 rdx
+%endif
+
+%define TMP 16*0
+%ifidn __OUTPUT_FORMAT__, win64
+ %define XMM_SAVE 16*2
+ %define VARIABLE_OFFSET 16*10+8
+%else
+ %define VARIABLE_OFFSET 16*2+8
+%endif
+align 16
+global crc64_yt_norm_by8:ISAL_SYM_TYPE_FUNCTION
+crc64_yt_norm_by8:
+
+; fix for darwin builds
+%ifidn __OUTPUT_FORMAT__, macho64
+global _crc64_yt_norm_by8:ISAL_SYM_TYPE_FUNCTION
+_crc64_yt_norm_by8:
+%endif
+
+ not arg1 ;~init_crc
+
+ sub rsp,VARIABLE_OFFSET
+
+%ifidn __OUTPUT_FORMAT__, win64
+ ; push the xmm registers into the stack to maintain
+ movdqa [rsp + XMM_SAVE + 16*0], xmm6
+ movdqa [rsp + XMM_SAVE + 16*1], xmm7
+ movdqa [rsp + XMM_SAVE + 16*2], xmm8
+ movdqa [rsp + XMM_SAVE + 16*3], xmm9
+ movdqa [rsp + XMM_SAVE + 16*4], xmm10
+ movdqa [rsp + XMM_SAVE + 16*5], xmm11
+ movdqa [rsp + XMM_SAVE + 16*6], xmm12
+ movdqa [rsp + XMM_SAVE + 16*7], xmm13
+%endif
+
+
+ ; check if smaller than 256
+ cmp arg3, 256
+
+ ; for sizes less than 256, we can't fold 128B at a time...
+ jl _less_than_256
+
+
+ ; load the initial crc value
+ movq xmm10, arg1 ; initial crc
+
+ ; crc value does not need to be byte-reflected, but it needs to be moved to the high part of the register.
+ ; because data will be byte-reflected and will align with initial crc at correct place.
+ pslldq xmm10, 8
+
+ movdqa xmm11, [SHUF_MASK]
+ ; receive the initial 128B data, xor the initial crc value
+ movdqu xmm0, [arg2+16*0]
+ movdqu xmm1, [arg2+16*1]
+ movdqu xmm2, [arg2+16*2]
+ movdqu xmm3, [arg2+16*3]
+ movdqu xmm4, [arg2+16*4]
+ movdqu xmm5, [arg2+16*5]
+ movdqu xmm6, [arg2+16*6]
+ movdqu xmm7, [arg2+16*7]
+
+ pshufb xmm0, xmm11
+ ; XOR the initial_crc value
+ pxor xmm0, xmm10
+ pshufb xmm1, xmm11
+ pshufb xmm2, xmm11
+ pshufb xmm3, xmm11
+ pshufb xmm4, xmm11
+ pshufb xmm5, xmm11
+ pshufb xmm6, xmm11
+ pshufb xmm7, xmm11
+
+ movdqa xmm10, [rk3] ;xmm10 has rk3 and rk4
+ ;imm value of pclmulqdq instruction will determine which constant to use
+ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+ ; we subtract 256 instead of 128 to save one instruction from the loop
+ sub arg3, 256
+
+ ; at this section of the code, there is 128*x+y (0<=y<128) bytes of buffer. The _fold_128_B_loop
+ ; loop will fold 128B at a time until we have 128+y Bytes of buffer
+
+
+ ; fold 128B at a time. This section of the code folds 8 xmm registers in parallel
+_fold_128_B_loop:
+
+ ; update the buffer pointer
+ add arg2, 128 ; buf += 128;
+
+ prefetchnta [arg2+fetch_dist+0]
+ movdqu xmm9, [arg2+16*0]
+ movdqu xmm12, [arg2+16*1]
+ pshufb xmm9, xmm11
+ pshufb xmm12, xmm11
+ movdqa xmm8, xmm0
+ movdqa xmm13, xmm1
+ pclmulqdq xmm0, xmm10, 0x0
+ pclmulqdq xmm8, xmm10 , 0x11
+ pclmulqdq xmm1, xmm10, 0x0
+ pclmulqdq xmm13, xmm10 , 0x11
+ pxor xmm0, xmm9
+ xorps xmm0, xmm8
+ pxor xmm1, xmm12
+ xorps xmm1, xmm13
+
+ prefetchnta [arg2+fetch_dist+32]
+ movdqu xmm9, [arg2+16*2]
+ movdqu xmm12, [arg2+16*3]
+ pshufb xmm9, xmm11
+ pshufb xmm12, xmm11
+ movdqa xmm8, xmm2
+ movdqa xmm13, xmm3
+ pclmulqdq xmm2, xmm10, 0x0
+ pclmulqdq xmm8, xmm10 , 0x11
+ pclmulqdq xmm3, xmm10, 0x0
+ pclmulqdq xmm13, xmm10 , 0x11
+ pxor xmm2, xmm9
+ xorps xmm2, xmm8
+ pxor xmm3, xmm12
+ xorps xmm3, xmm13
+
+ prefetchnta [arg2+fetch_dist+64]
+ movdqu xmm9, [arg2+16*4]
+ movdqu xmm12, [arg2+16*5]
+ pshufb xmm9, xmm11
+ pshufb xmm12, xmm11
+ movdqa xmm8, xmm4
+ movdqa xmm13, xmm5
+ pclmulqdq xmm4, xmm10, 0x0
+ pclmulqdq xmm8, xmm10 , 0x11
+ pclmulqdq xmm5, xmm10, 0x0
+ pclmulqdq xmm13, xmm10 , 0x11
+ pxor xmm4, xmm9
+ xorps xmm4, xmm8
+ pxor xmm5, xmm12
+ xorps xmm5, xmm13
+
+ prefetchnta [arg2+fetch_dist+96]
+ movdqu xmm9, [arg2+16*6]
+ movdqu xmm12, [arg2+16*7]
+ pshufb xmm9, xmm11
+ pshufb xmm12, xmm11
+ movdqa xmm8, xmm6
+ movdqa xmm13, xmm7
+ pclmulqdq xmm6, xmm10, 0x0
+ pclmulqdq xmm8, xmm10 , 0x11
+ pclmulqdq xmm7, xmm10, 0x0
+ pclmulqdq xmm13, xmm10 , 0x11
+ pxor xmm6, xmm9
+ xorps xmm6, xmm8
+ pxor xmm7, xmm12
+ xorps xmm7, xmm13
+
+ sub arg3, 128
+
+ ; check if there is another 128B in the buffer to be able to fold
+ jge _fold_128_B_loop
+ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+ add arg2, 128
+ ; at this point, the buffer pointer is pointing at the last y Bytes of the buffer, where 0 <= y < 128
+ ; the 128B of folded data is in 8 of the xmm registers: xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7
+
+
+ ; fold the 8 xmm registers to 1 xmm register with different constants
+
+ movdqa xmm10, [rk9]
+ movdqa xmm8, xmm0
+ pclmulqdq xmm0, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ xorps xmm7, xmm0
+
+ movdqa xmm10, [rk11]
+ movdqa xmm8, xmm1
+ pclmulqdq xmm1, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ xorps xmm7, xmm1
+
+ movdqa xmm10, [rk13]
+ movdqa xmm8, xmm2
+ pclmulqdq xmm2, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ pxor xmm7, xmm2
+
+ movdqa xmm10, [rk15]
+ movdqa xmm8, xmm3
+ pclmulqdq xmm3, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ xorps xmm7, xmm3
+
+ movdqa xmm10, [rk17]
+ movdqa xmm8, xmm4
+ pclmulqdq xmm4, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ pxor xmm7, xmm4
+
+ movdqa xmm10, [rk19]
+ movdqa xmm8, xmm5
+ pclmulqdq xmm5, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ xorps xmm7, xmm5
+
+ movdqa xmm10, [rk1] ;xmm10 has rk1 and rk2
+
+ movdqa xmm8, xmm6
+ pclmulqdq xmm6, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ pxor xmm7, xmm6
+
+
+ ; instead of 128, we add 112 to the loop counter to save 1 instruction from the loop
+ ; instead of a cmp instruction, we use the negative flag with the jl instruction
+ add arg3, 128-16
+ jl _final_reduction_for_128
+
+ ; now we have 16+y bytes left to reduce. 16 Bytes is in register xmm7 and the rest is in memory
+ ; we can fold 16 bytes at a time if y>=16
+ ; continue folding 16B at a time
+
+_16B_reduction_loop:
+ movdqa xmm8, xmm7
+ pclmulqdq xmm7, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ movdqu xmm0, [arg2]
+ pshufb xmm0, xmm11
+ pxor xmm7, xmm0
+ add arg2, 16
+ sub arg3, 16
+ ; instead of a cmp instruction, we utilize the flags with the jge instruction
+ ; equivalent of: cmp arg3, 16-16
+ ; check if there is any more 16B in the buffer to be able to fold
+ jge _16B_reduction_loop
+
+ ;now we have 16+z bytes left to reduce, where 0<= z < 16.
+ ;first, we reduce the data in the xmm7 register
+
+
+_final_reduction_for_128:
+ ; check if any more data to fold. If not, compute the CRC of the final 128 bits
+ add arg3, 16
+ je _128_done
+
+ ; here we are getting data that is less than 16 bytes.
+ ; since we know that there was data before the pointer, we can offset the input pointer before the actual point, to receive exactly 16 bytes.
+ ; after that the registers need to be adjusted.
+_get_last_two_xmms:
+ movdqa xmm2, xmm7
+
+ movdqu xmm1, [arg2 - 16 + arg3]
+ pshufb xmm1, xmm11
+
+ ; get rid of the extra data that was loaded before
+ ; load the shift constant
+ lea rax, [pshufb_shf_table + 16]
+ sub rax, arg3
+ movdqu xmm0, [rax]
+
+ ; shift xmm2 to the left by arg3 bytes
+ pshufb xmm2, xmm0
+
+ ; shift xmm7 to the right by 16-arg3 bytes
+ pxor xmm0, [mask1]
+ pshufb xmm7, xmm0
+ pblendvb xmm1, xmm2 ;xmm0 is implicit
+
+ ; fold 16 Bytes
+ movdqa xmm2, xmm1
+ movdqa xmm8, xmm7
+ pclmulqdq xmm7, xmm10, 0x11
+ pclmulqdq xmm8, xmm10, 0x0
+ pxor xmm7, xmm8
+ pxor xmm7, xmm2
+
+_128_done:
+ ; compute crc of a 128-bit value
+ movdqa xmm10, [rk5] ; rk5 and rk6 in xmm10
+ movdqa xmm0, xmm7
+
+ ;64b fold
+ pclmulqdq xmm7, xmm10, 0x01 ; H*L
+ pslldq xmm0, 8
+ pxor xmm7, xmm0
+
+ ;barrett reduction
+_barrett:
+ movdqa xmm10, [rk7] ; rk7 and rk8 in xmm10
+ movdqa xmm0, xmm7
+
+ movdqa xmm1, xmm7
+ pand xmm1, [mask3]
+ pclmulqdq xmm7, xmm10, 0x01
+ pxor xmm7, xmm1
+
+ pclmulqdq xmm7, xmm10, 0x11
+ pxor xmm7, xmm0
+ pextrq rax, xmm7, 0
+
+_cleanup:
+ not rax
+%ifidn __OUTPUT_FORMAT__, win64
+ movdqa xmm6, [rsp + XMM_SAVE + 16*0]
+ movdqa xmm7, [rsp + XMM_SAVE + 16*1]
+ movdqa xmm8, [rsp + XMM_SAVE + 16*2]
+ movdqa xmm9, [rsp + XMM_SAVE + 16*3]
+ movdqa xmm10, [rsp + XMM_SAVE + 16*4]
+ movdqa xmm11, [rsp + XMM_SAVE + 16*5]
+ movdqa xmm12, [rsp + XMM_SAVE + 16*6]
+ movdqa xmm13, [rsp + XMM_SAVE + 16*7]
+%endif
+ add rsp, VARIABLE_OFFSET
+ ret
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+align 16
+_less_than_256:
+
+ ; check if there is enough buffer to be able to fold 16B at a time
+ cmp arg3, 32
+ jl _less_than_32
+ movdqa xmm11, [SHUF_MASK]
+
+ ; if there is, load the constants
+ movdqa xmm10, [rk1] ; rk1 and rk2 in xmm10
+
+ movq xmm0, arg1 ; get the initial crc value
+ pslldq xmm0, 8 ; align it to its correct place
+ movdqu xmm7, [arg2] ; load the plaintext
+ pshufb xmm7, xmm11 ; byte-reflect the plaintext
+ pxor xmm7, xmm0
+
+
+ ; update the buffer pointer
+ add arg2, 16
+
+ ; update the counter. subtract 32 instead of 16 to save one instruction from the loop
+ sub arg3, 32
+
+ jmp _16B_reduction_loop
+align 16
+_less_than_32:
+ ; mov initial crc to the return value. this is necessary for zero-length buffers.
+ mov rax, arg1
+ test arg3, arg3
+ je _cleanup
+
+ movdqa xmm11, [SHUF_MASK]
+
+ movq xmm0, arg1 ; get the initial crc value
+ pslldq xmm0, 8 ; align it to its correct place
+
+ cmp arg3, 16
+ je _exact_16_left
+ jl _less_than_16_left
+
+ movdqu xmm7, [arg2] ; load the plaintext
+ pshufb xmm7, xmm11 ; byte-reflect the plaintext
+ pxor xmm7, xmm0 ; xor the initial crc value
+ add arg2, 16
+ sub arg3, 16
+ movdqa xmm10, [rk1] ; rk1 and rk2 in xmm10
+ jmp _get_last_two_xmms
+align 16
+_less_than_16_left:
+ ; use stack space to load data less than 16 bytes, zero-out the 16B in memory first.
+ pxor xmm1, xmm1
+ mov r11, rsp
+ movdqa [r11], xmm1
+
+ ; backup the counter value
+ mov r9, arg3
+ cmp arg3, 8
+ jl _less_than_8_left
+
+ ; load 8 Bytes
+ mov rax, [arg2]
+ mov [r11], rax
+ add r11, 8
+ sub arg3, 8
+ add arg2, 8
+_less_than_8_left:
+
+ cmp arg3, 4
+ jl _less_than_4_left
+
+ ; load 4 Bytes
+ mov eax, [arg2]
+ mov [r11], eax
+ add r11, 4
+ sub arg3, 4
+ add arg2, 4
+_less_than_4_left:
+
+ cmp arg3, 2
+ jl _less_than_2_left
+
+ ; load 2 Bytes
+ mov ax, [arg2]
+ mov [r11], ax
+ add r11, 2
+ sub arg3, 2
+ add arg2, 2
+_less_than_2_left:
+ cmp arg3, 1
+ jl _zero_left
+
+ ; load 1 Byte
+ mov al, [arg2]
+ mov [r11], al
+_zero_left:
+ movdqa xmm7, [rsp]
+ pshufb xmm7, xmm11
+ pxor xmm7, xmm0 ; xor the initial crc value
+
+ ; shl r9, 4
+ lea rax, [pshufb_shf_table + 16]
+ sub rax, r9
+
+ cmp r9, 8
+ jl _end_1to7
+
+_end_8to15:
+ movdqu xmm0, [rax]
+ pxor xmm0, [mask1]
+
+ pshufb xmm7, xmm0
+ jmp _128_done
+
+_end_1to7:
+ ; Right shift (8-length) bytes in XMM
+ add rax, 8
+ movdqu xmm0, [rax]
+ pshufb xmm7,xmm0
+
+ jmp _barrett
+align 16
+_exact_16_left:
+ movdqu xmm7, [arg2]
+ pshufb xmm7, xmm11
+ pxor xmm7, xmm0 ; xor the initial crc value
+
+ jmp _128_done
+
+section .data
+
+; precomputed constants
+align 16
+
+rk1 :
+DQ 0xf9c49baae9f0beb0 ; 2^(64*2) mod P(x)
+rk2 :
+DQ 0xd0665f166605dcc4 ; 2^(64*3) mod P(x)
+rk3 :
+DQ 0x43de7c94f68ae581 ; 2^(64*16) mod P(x)
+rk4 :
+DQ 0xa9e338db73b74f41 ; 2^(64*17) mod P(x)
+rk5 :
+DQ 0xf9c49baae9f0beb0 ; 2^(64*2) mod P(x)
+rk6 :
+DQ 0x0000000000000000
+rk7 :
+DQ 0x9d9034581c0766b0 ; floor(2^(64*2) / P(x))
+rk8 :
+DQ 0xe543279765927881 ; P(x)
+rk9 :
+DQ 0xd7ce9a41dd19144a ; 2^(64*14) mod P(x)
+rk10 :
+DQ 0x6218f07e3b0cb7c5 ; 2^(64*15) mod P(x)
+rk11 :
+DQ 0x5b6cab2d2af9508f ; 2^(64*12) mod P(x)
+rk12 :
+DQ 0x3d20387928bfc54f ; 2^(64*13) mod P(x)
+rk13 :
+DQ 0xe3bba77656be23ef ; 2^(64*10) mod P(x)
+rk14 :
+DQ 0xcab3ead699658082 ; 2^(64*11) mod P(x)
+rk15 :
+DQ 0x209670022e7af509 ; 2^(64*8) mod P(x)
+rk16 :
+DQ 0xd85c929ddd7a7d69 ; 2^(64*9) mod P(x)
+rk17 :
+DQ 0x0a012188851e7f4d ; 2^(64*6) mod P(x)
+rk18 :
+DQ 0xa7135f5784dedf59 ; 2^(64*7) mod P(x)
+rk19 :
+DQ 0x5ebb29b972edf1a9 ; 2^(64*4) mod P(x)
+rk20 :
+DQ 0x6472813464933a8c ; 2^(64*5) mod P(x)
+
+
+mask1:
+dq 0x8080808080808080, 0x8080808080808080
+mask2:
+dq 0xFFFFFFFFFFFFFFFF, 0x00000000FFFFFFFF
+mask3:
+dq 0x0000000000000000, 0xFFFFFFFFFFFFFFFF
+
+SHUF_MASK:
+dq 0x08090A0B0C0D0E0F, 0x0001020304050607
+
+pshufb_shf_table:
+; use these values for shift constants for the pshufb instruction
+; different alignments result in values as shown:
+; dq 0x8887868584838281, 0x008f8e8d8c8b8a89 ; shl 15 (16-1) / shr1
+; dq 0x8988878685848382, 0x01008f8e8d8c8b8a ; shl 14 (16-3) / shr2
+; dq 0x8a89888786858483, 0x0201008f8e8d8c8b ; shl 13 (16-4) / shr3
+; dq 0x8b8a898887868584, 0x030201008f8e8d8c ; shl 12 (16-4) / shr4
+; dq 0x8c8b8a8988878685, 0x04030201008f8e8d ; shl 11 (16-5) / shr5
+; dq 0x8d8c8b8a89888786, 0x0504030201008f8e ; shl 10 (16-6) / shr6
+; dq 0x8e8d8c8b8a898887, 0x060504030201008f ; shl 9 (16-7) / shr7
+; dq 0x8f8e8d8c8b8a8988, 0x0706050403020100 ; shl 8 (16-8) / shr8
+; dq 0x008f8e8d8c8b8a89, 0x0807060504030201 ; shl 7 (16-9) / shr9
+; dq 0x01008f8e8d8c8b8a, 0x0908070605040302 ; shl 6 (16-10) / shr10
+; dq 0x0201008f8e8d8c8b, 0x0a09080706050403 ; shl 5 (16-11) / shr11
+; dq 0x030201008f8e8d8c, 0x0b0a090807060504 ; shl 4 (16-12) / shr12
+; dq 0x04030201008f8e8d, 0x0c0b0a0908070605 ; shl 3 (16-13) / shr13
+; dq 0x0504030201008f8e, 0x0d0c0b0a09080706 ; shl 2 (16-14) / shr14
+; dq 0x060504030201008f, 0x0e0d0c0b0a090807 ; shl 1 (16-15) / shr15
+dq 0x8786858483828100, 0x8f8e8d8c8b8a8988
+dq 0x0706050403020100, 0x0f0e0d0c0b0a0908
+dq 0x8080808080808080, 0x0f0e0d0c0b0a0908
+dq 0x8080808080808080, 0x8080808080808080
+
+;;; func core, ver, snum
+slversion crc64_ecma_norm_by8, 01, 00, 001a
diff --git a/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c b/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
new file mode 100644
index 0000000000..b794eed62b
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c
@@ -0,0 +1,151 @@
+/**********************************************************************
+ Copyright(c) 2011-2016 Intel Corporation All rights reserved.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+#pragma GCC diagnostic ignored "-Wunused-function"
+
+#include <stdint.h>
+
+#define MAX_ITER 8
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+/*
+ This file contains base and reference c-based implementations of CRC64,
+ which also originate from ISA-l with the following modifications:
+
+ 1) `crc64_yt_norm_ref` is the same as `crc64_*_norm_ref` functions from `crc64_ref.c`,
+ but with yt-specific polynomial variable.
+
+ 2) `crc64_yt_norm_base` is the same as `crc64_*_norm_base` functions from `crc64_base.c`,
+ but with yt-specific lookup table. example of code which generates such table may be found at
+ `yt/yt/experiments/benchmark_crc/main.cpp`
+*/
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+static inline uint64_t crc64_yt_norm_ref(uint64_t seed, const uint8_t * buf, uint64_t len)
+{
+ uint64_t rem = ~seed;
+ unsigned int i, j;
+
+ uint64_t poly = 0xE543279765927881; // YT-specific polynomial
+
+ for (i = 0; i < len; i++) {
+ rem = rem ^ ((uint64_t) buf[i] << 56);
+ for (j = 0; j < MAX_ITER; j++) {
+ rem = (rem & 0x8000000000000000ULL ? poly : 0) ^ (rem << 1);
+ }
+ }
+ return ~rem;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t crc64_yt_norm_base(uint64_t seed, const uint8_t * buf, uint64_t len);
+
+static const uint64_t crc64_yt_norm_table[256] = {
+ 0x0000000000000000ULL, 0xe543279765927881ULL, 0x2fc568b9aeb68983ULL, 0xca864f2ecb24f102ULL,
+ 0x5f8ad1735d6d1306ULL, 0xbac9f6e438ff6b87ULL, 0x704fb9caf3db9a85ULL, 0x950c9e5d9649e204ULL,
+ 0xbf15a2e6bada260cULL, 0x5a568571df485e8dULL, 0x90d0ca5f146caf8fULL, 0x7593edc871fed70eULL,
+ 0xe09f7395e7b7350aULL, 0x05dc540282254d8bULL, 0xcf5a1b2c4901bc89ULL, 0x2a193cbb2c93c408ULL,
+ 0x9b68625a10263499ULL, 0x7e2b45cd75b44c18ULL, 0xb4ad0ae3be90bd1aULL, 0x51ee2d74db02c59bULL,
+ 0xc4e2b3294d4b279fULL, 0x21a194be28d95f1eULL, 0xeb27db90e3fdae1cULL, 0x0e64fc07866fd69dULL,
+ 0x247dc0bcaafc1295ULL, 0xc13ee72bcf6e6a14ULL, 0x0bb8a805044a9b16ULL, 0xeefb8f9261d8e397ULL,
+ 0x7bf711cff7910193ULL, 0x9eb4365892037912ULL, 0x5432797659278810ULL, 0xb1715ee13cb5f091ULL,
+ 0xd393e32345de11b3ULL, 0x36d0c4b4204c6932ULL, 0xfc568b9aeb689830ULL, 0x1915ac0d8efae0b1ULL,
+ 0x8c19325018b302b5ULL, 0x695a15c77d217a34ULL, 0xa3dc5ae9b6058b36ULL, 0x469f7d7ed397f3b7ULL,
+ 0x6c8641c5ff0437bfULL, 0x89c566529a964f3eULL, 0x4343297c51b2be3cULL, 0xa6000eeb3420c6bdULL,
+ 0x330c90b6a26924b9ULL, 0xd64fb721c7fb5c38ULL, 0x1cc9f80f0cdfad3aULL, 0xf98adf98694dd5bbULL,
+ 0x48fb817955f8252aULL, 0xadb8a6ee306a5dabULL, 0x673ee9c0fb4eaca9ULL, 0x827dce579edcd428ULL,
+ 0x1771500a0895362cULL, 0xf232779d6d074eadULL, 0x38b438b3a623bfafULL, 0xddf71f24c3b1c72eULL,
+ 0xf7ee239fef220326ULL, 0x12ad04088ab07ba7ULL, 0xd82b4b2641948aa5ULL, 0x3d686cb12406f224ULL,
+ 0xa864f2ecb24f1020ULL, 0x4d27d57bd7dd68a1ULL, 0x87a19a551cf999a3ULL, 0x62e2bdc2796be122ULL,
+ 0x4264e1d1ee2e5be7ULL, 0xa727c6468bbc2366ULL, 0x6da189684098d264ULL, 0x88e2aeff250aaae5ULL,
+ 0x1dee30a2b34348e1ULL, 0xf8ad1735d6d13060ULL, 0x322b581b1df5c162ULL, 0xd7687f8c7867b9e3ULL,
+ 0xfd71433754f47debULL, 0x183264a03166056aULL, 0xd2b42b8efa42f468ULL, 0x37f70c199fd08ce9ULL,
+ 0xa2fb924409996eedULL, 0x47b8b5d36c0b166cULL, 0x8d3efafda72fe76eULL, 0x687ddd6ac2bd9fefULL,
+ 0xd90c838bfe086f7eULL, 0x3c4fa41c9b9a17ffULL, 0xf6c9eb3250bee6fdULL, 0x138acca5352c9e7cULL,
+ 0x868652f8a3657c78ULL, 0x63c5756fc6f704f9ULL, 0xa9433a410dd3f5fbULL, 0x4c001dd668418d7aULL,
+ 0x6619216d44d24972ULL, 0x835a06fa214031f3ULL, 0x49dc49d4ea64c0f1ULL, 0xac9f6e438ff6b870ULL,
+ 0x3993f01e19bf5a74ULL, 0xdcd0d7897c2d22f5ULL, 0x165698a7b709d3f7ULL, 0xf315bf30d29bab76ULL,
+ 0x91f702f2abf04a54ULL, 0x74b42565ce6232d5ULL, 0xbe326a4b0546c3d7ULL, 0x5b714ddc60d4bb56ULL,
+ 0xce7dd381f69d5952ULL, 0x2b3ef416930f21d3ULL, 0xe1b8bb38582bd0d1ULL, 0x04fb9caf3db9a850ULL,
+ 0x2ee2a014112a6c58ULL, 0xcba1878374b814d9ULL, 0x0127c8adbf9ce5dbULL, 0xe464ef3ada0e9d5aULL,
+ 0x716871674c477f5eULL, 0x942b56f029d507dfULL, 0x5ead19dee2f1f6ddULL, 0xbbee3e4987638e5cULL,
+ 0x0a9f60a8bbd67ecdULL, 0xefdc473fde44064cULL, 0x255a08111560f74eULL, 0xc0192f8670f28fcfULL,
+ 0x5515b1dbe6bb6dcbULL, 0xb056964c8329154aULL, 0x7ad0d962480de448ULL, 0x9f93fef52d9f9cc9ULL,
+ 0xb58ac24e010c58c1ULL, 0x50c9e5d9649e2040ULL, 0x9a4faaf7afbad142ULL, 0x7f0c8d60ca28a9c3ULL,
+ 0xea00133d5c614bc7ULL, 0x0f4334aa39f33346ULL, 0xc5c57b84f2d7c244ULL, 0x20865c139745bac5ULL,
+ 0x84c9c3a3dc5cb7ceULL, 0x618ae434b9cecf4fULL, 0xab0cab1a72ea3e4dULL, 0x4e4f8c8d177846ccULL,
+ 0xdb4312d08131a4c8ULL, 0x3e003547e4a3dc49ULL, 0xf4867a692f872d4bULL, 0x11c55dfe4a1555caULL,
+ 0x3bdc6145668691c2ULL, 0xde9f46d20314e943ULL, 0x141909fcc8301841ULL, 0xf15a2e6bada260c0ULL,
+ 0x6456b0363beb82c4ULL, 0x811597a15e79fa45ULL, 0x4b93d88f955d0b47ULL, 0xaed0ff18f0cf73c6ULL,
+ 0x1fa1a1f9cc7a8357ULL, 0xfae2866ea9e8fbd6ULL, 0x3064c94062cc0ad4ULL, 0xd527eed7075e7255ULL,
+ 0x402b708a91179051ULL, 0xa568571df485e8d0ULL, 0x6fee18333fa119d2ULL, 0x8aad3fa45a336153ULL,
+ 0xa0b4031f76a0a55bULL, 0x45f724881332dddaULL, 0x8f716ba6d8162cd8ULL, 0x6a324c31bd845459ULL,
+ 0xff3ed26c2bcdb65dULL, 0x1a7df5fb4e5fcedcULL, 0xd0fbbad5857b3fdeULL, 0x35b89d42e0e9475fULL,
+ 0x575a20809982a67dULL, 0xb2190717fc10defcULL, 0x789f483937342ffeULL, 0x9ddc6fae52a6577fULL,
+ 0x08d0f1f3c4efb57bULL, 0xed93d664a17dcdfaULL, 0x2715994a6a593cf8ULL, 0xc256bedd0fcb4479ULL,
+ 0xe84f826623588071ULL, 0x0d0ca5f146caf8f0ULL, 0xc78aeadf8dee09f2ULL, 0x22c9cd48e87c7173ULL,
+ 0xb7c553157e359377ULL, 0x528674821ba7ebf6ULL, 0x98003bacd0831af4ULL, 0x7d431c3bb5116275ULL,
+ 0xcc3242da89a492e4ULL, 0x2971654dec36ea65ULL, 0xe3f72a6327121b67ULL, 0x06b40df4428063e6ULL,
+ 0x93b893a9d4c981e2ULL, 0x76fbb43eb15bf963ULL, 0xbc7dfb107a7f0861ULL, 0x593edc871fed70e0ULL,
+ 0x7327e03c337eb4e8ULL, 0x9664c7ab56eccc69ULL, 0x5ce288859dc83d6bULL, 0xb9a1af12f85a45eaULL,
+ 0x2cad314f6e13a7eeULL, 0xc9ee16d80b81df6fULL, 0x036859f6c0a52e6dULL, 0xe62b7e61a53756ecULL,
+ 0xc6ad22723272ec29ULL, 0x23ee05e557e094a8ULL, 0xe9684acb9cc465aaULL, 0x0c2b6d5cf9561d2bULL,
+ 0x9927f3016f1fff2fULL, 0x7c64d4960a8d87aeULL, 0xb6e29bb8c1a976acULL, 0x53a1bc2fa43b0e2dULL,
+ 0x79b8809488a8ca25ULL, 0x9cfba703ed3ab2a4ULL, 0x567de82d261e43a6ULL, 0xb33ecfba438c3b27ULL,
+ 0x263251e7d5c5d923ULL, 0xc3717670b057a1a2ULL, 0x09f7395e7b7350a0ULL, 0xecb41ec91ee12821ULL,
+ 0x5dc540282254d8b0ULL, 0xb88667bf47c6a031ULL, 0x720028918ce25133ULL, 0x97430f06e97029b2ULL,
+ 0x024f915b7f39cbb6ULL, 0xe70cb6cc1aabb337ULL, 0x2d8af9e2d18f4235ULL, 0xc8c9de75b41d3ab4ULL,
+ 0xe2d0e2ce988efebcULL, 0x0793c559fd1c863dULL, 0xcd158a773638773fULL, 0x2856ade053aa0fbeULL,
+ 0xbd5a33bdc5e3edbaULL, 0x5819142aa071953bULL, 0x929f5b046b556439ULL, 0x77dc7c930ec71cb8ULL,
+ 0x153ec15177acfd9aULL, 0xf07de6c6123e851bULL, 0x3afba9e8d91a7419ULL, 0xdfb88e7fbc880c98ULL,
+ 0x4ab410222ac1ee9cULL, 0xaff737b54f53961dULL, 0x6571789b8477671fULL, 0x80325f0ce1e51f9eULL,
+ 0xaa2b63b7cd76db96ULL, 0x4f684420a8e4a317ULL, 0x85ee0b0e63c05215ULL, 0x60ad2c9906522a94ULL,
+ 0xf5a1b2c4901bc890ULL, 0x10e29553f589b011ULL, 0xda64da7d3ead4113ULL, 0x3f27fdea5b3f3992ULL,
+ 0x8e56a30b678ac903ULL, 0x6b15849c0218b182ULL, 0xa193cbb2c93c4080ULL, 0x44d0ec25acae3801ULL,
+ 0xd1dc72783ae7da05ULL, 0x349f55ef5f75a284ULL, 0xfe191ac194515386ULL, 0x1b5a3d56f1c32b07ULL,
+ 0x314301eddd50ef0fULL, 0xd400267ab8c2978eULL, 0x1e86695473e6668cULL, 0xfbc54ec316741e0dULL,
+ 0x6ec9d09e803dfc09ULL, 0x8b8af709e5af8488ULL, 0x410cb8272e8b758aULL, 0xa44f9fb04b190d0bULL
+};
+
+uint64_t crc64_yt_norm_base(uint64_t seed, const uint8_t * buf, uint64_t len)
+{
+ uint64_t i, crc = ~seed;
+
+ for (i = 0; i < len; i++) {
+ uint8_t byte = buf[i];
+ crc = crc64_yt_norm_table[((crc >> 56) ^ byte) & 0xff] ^ (crc << 8);
+ }
+
+ return ~crc;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/misc/isa_crc64/include/reg_sizes.asm b/yt/yt/core/misc/isa_crc64/include/reg_sizes.asm
new file mode 100644
index 0000000000..a22f9b3dcd
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/include/reg_sizes.asm
@@ -0,0 +1,248 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; Copyright(c) 2011-2015 Intel Corporation All rights reserved.
+;
+; Redistribution and use in source and binary forms, with or without
+; modification, are permitted provided that the following conditions
+; are met:
+; * Redistributions of source code must retain the above copyright
+; notice, this list of conditions and the following disclaimer.
+; * Redistributions in binary form must reproduce the above copyright
+; notice, this list of conditions and the following disclaimer in
+; the documentation and/or other materials provided with the
+; distribution.
+; * Neither the name of Intel Corporation nor the names of its
+; contributors may be used to endorse or promote products derived
+; from this software without specific prior written permission.
+;
+; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+%ifndef _REG_SIZES_ASM_
+%define _REG_SIZES_ASM_
+
+%ifdef __NASM_VER__
+%ifidn __OUTPUT_FORMAT__, win64
+%error nasm not supported in windows
+%else
+%define endproc_frame
+%endif
+%endif
+
+%ifndef AS_FEATURE_LEVEL
+%define AS_FEATURE_LEVEL 4
+%endif
+
+%define EFLAGS_HAS_CPUID (1<<21)
+%define FLAG_CPUID1_ECX_CLMUL (1<<1)
+%define FLAG_CPUID1_EDX_SSE2 (1<<26)
+%define FLAG_CPUID1_ECX_SSE3 (1)
+%define FLAG_CPUID1_ECX_SSE4_1 (1<<19)
+%define FLAG_CPUID1_ECX_SSE4_2 (1<<20)
+%define FLAG_CPUID1_ECX_POPCNT (1<<23)
+%define FLAG_CPUID1_ECX_AESNI (1<<25)
+%define FLAG_CPUID1_ECX_OSXSAVE (1<<27)
+%define FLAG_CPUID1_ECX_AVX (1<<28)
+%define FLAG_CPUID1_EBX_AVX2 (1<<5)
+
+%define FLAG_CPUID7_EBX_AVX2 (1<<5)
+%define FLAG_CPUID7_EBX_AVX512F (1<<16)
+%define FLAG_CPUID7_EBX_AVX512DQ (1<<17)
+%define FLAG_CPUID7_EBX_AVX512IFMA (1<<21)
+%define FLAG_CPUID7_EBX_AVX512PF (1<<26)
+%define FLAG_CPUID7_EBX_AVX512ER (1<<27)
+%define FLAG_CPUID7_EBX_AVX512CD (1<<28)
+%define FLAG_CPUID7_EBX_AVX512BW (1<<30)
+%define FLAG_CPUID7_EBX_AVX512VL (1<<31)
+
+%define FLAG_CPUID7_ECX_AVX512VBMI (1<<1)
+%define FLAG_CPUID7_ECX_AVX512VBMI2 (1 << 6)
+%define FLAG_CPUID7_ECX_GFNI (1 << 8)
+%define FLAG_CPUID7_ECX_VAES (1 << 9)
+%define FLAG_CPUID7_ECX_VPCLMULQDQ (1 << 10)
+%define FLAG_CPUID7_ECX_VNNI (1 << 11)
+%define FLAG_CPUID7_ECX_BITALG (1 << 12)
+%define FLAG_CPUID7_ECX_VPOPCNTDQ (1 << 14)
+
+%define FLAGS_CPUID7_EBX_AVX512_G1 (FLAG_CPUID7_EBX_AVX512F | FLAG_CPUID7_EBX_AVX512VL | FLAG_CPUID7_EBX_AVX512BW | FLAG_CPUID7_EBX_AVX512CD | FLAG_CPUID7_EBX_AVX512DQ)
+%define FLAGS_CPUID7_ECX_AVX512_G2 (FLAG_CPUID7_ECX_AVX512VBMI2 | FLAG_CPUID7_ECX_GFNI | FLAG_CPUID7_ECX_VAES | FLAG_CPUID7_ECX_VPCLMULQDQ | FLAG_CPUID7_ECX_VNNI | FLAG_CPUID7_ECX_BITALG | FLAG_CPUID7_ECX_VPOPCNTDQ)
+
+%define FLAG_XGETBV_EAX_XMM (1<<1)
+%define FLAG_XGETBV_EAX_YMM (1<<2)
+%define FLAG_XGETBV_EAX_XMM_YMM 0x6
+%define FLAG_XGETBV_EAX_ZMM_OPM 0xe0
+
+%define FLAG_CPUID1_EAX_AVOTON 0x000406d0
+%define FLAG_CPUID1_EAX_STEP_MASK 0xfffffff0
+
+; define d and w variants for registers
+
+%define raxd eax
+%define raxw ax
+%define raxb al
+
+%define rbxd ebx
+%define rbxw bx
+%define rbxb bl
+
+%define rcxd ecx
+%define rcxw cx
+%define rcxb cl
+
+%define rdxd edx
+%define rdxw dx
+%define rdxb dl
+
+%define rsid esi
+%define rsiw si
+%define rsib sil
+
+%define rdid edi
+%define rdiw di
+%define rdib dil
+
+%define rbpd ebp
+%define rbpw bp
+%define rbpb bpl
+
+%define ymm0x xmm0
+%define ymm1x xmm1
+%define ymm2x xmm2
+%define ymm3x xmm3
+%define ymm4x xmm4
+%define ymm5x xmm5
+%define ymm6x xmm6
+%define ymm7x xmm7
+%define ymm8x xmm8
+%define ymm9x xmm9
+%define ymm10x xmm10
+%define ymm11x xmm11
+%define ymm12x xmm12
+%define ymm13x xmm13
+%define ymm14x xmm14
+%define ymm15x xmm15
+
+%define zmm0x xmm0
+%define zmm1x xmm1
+%define zmm2x xmm2
+%define zmm3x xmm3
+%define zmm4x xmm4
+%define zmm5x xmm5
+%define zmm6x xmm6
+%define zmm7x xmm7
+%define zmm8x xmm8
+%define zmm9x xmm9
+%define zmm10x xmm10
+%define zmm11x xmm11
+%define zmm12x xmm12
+%define zmm13x xmm13
+%define zmm14x xmm14
+%define zmm15x xmm15
+%define zmm16x xmm16
+%define zmm17x xmm17
+%define zmm18x xmm18
+%define zmm19x xmm19
+%define zmm20x xmm20
+%define zmm21x xmm21
+%define zmm22x xmm22
+%define zmm23x xmm23
+%define zmm24x xmm24
+%define zmm25x xmm25
+%define zmm26x xmm26
+%define zmm27x xmm27
+%define zmm28x xmm28
+%define zmm29x xmm29
+%define zmm30x xmm30
+%define zmm31x xmm31
+
+%define zmm0y ymm0
+%define zmm1y ymm1
+%define zmm2y ymm2
+%define zmm3y ymm3
+%define zmm4y ymm4
+%define zmm5y ymm5
+%define zmm6y ymm6
+%define zmm7y ymm7
+%define zmm8y ymm8
+%define zmm9y ymm9
+%define zmm10y ymm10
+%define zmm11y ymm11
+%define zmm12y ymm12
+%define zmm13y ymm13
+%define zmm14y ymm14
+%define zmm15y ymm15
+%define zmm16y ymm16
+%define zmm17y ymm17
+%define zmm18y ymm18
+%define zmm19y ymm19
+%define zmm20y ymm20
+%define zmm21y ymm21
+%define zmm22y ymm22
+%define zmm23y ymm23
+%define zmm24y ymm24
+%define zmm25y ymm25
+%define zmm26y ymm26
+%define zmm27y ymm27
+%define zmm28y ymm28
+%define zmm29y ymm29
+%define zmm30y ymm30
+%define zmm31y ymm31
+
+%define DWORD(reg) reg %+ d
+%define WORD(reg) reg %+ w
+%define BYTE(reg) reg %+ b
+
+%define XWORD(reg) reg %+ x
+
+%ifidn __OUTPUT_FORMAT__,elf32
+section .note.GNU-stack noalloc noexec nowrite progbits
+section .text
+%endif
+%ifidn __OUTPUT_FORMAT__,elf64
+section .note.GNU-stack noalloc noexec nowrite progbits
+section .text
+%endif
+
+%ifdef REL_TEXT
+ %define WRT_OPT
+%elifidn __OUTPUT_FORMAT__, elf64
+ %define WRT_OPT wrt ..plt
+%else
+ %define WRT_OPT
+%endif
+
+%ifidn __OUTPUT_FORMAT__, macho64
+ %define elf64 macho64
+ mac_equ equ 1
+ %ifdef __NASM_VER__
+ %define ISAL_SYM_TYPE_FUNCTION
+ %define ISAL_SYM_TYPE_DATA_INTERNAL
+ %else
+ %define ISAL_SYM_TYPE_FUNCTION function
+ %define ISAL_SYM_TYPE_DATA_INTERNAL data internal
+ %endif
+%else
+ %define ISAL_SYM_TYPE_FUNCTION function
+ %define ISAL_SYM_TYPE_DATA_INTERNAL data internal
+%endif
+
+%macro slversion 4
+ section .text
+ global %1_slver_%2%3%4
+ global %1_slver
+ %1_slver:
+ %1_slver_%2%3%4:
+ dw 0x%4
+ db 0x%3, 0x%2
+%endmacro
+
+%endif ; ifndef _REG_SIZES_ASM_ \ No newline at end of file
diff --git a/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/crc64_reference_test.c b/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/crc64_reference_test.c
new file mode 100644
index 0000000000..aaa1d1c841
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/crc64_reference_test.c
@@ -0,0 +1,308 @@
+/**********************************************************************
+ Copyright(c) 2011-2016 Intel Corporation All rights reserved.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Intel Corporation nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**********************************************************************/
+
+#include <yt/yt/core/misc/isa_crc64/crc64_yt_norm_refs.c>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+#ifndef TEST_SEED
+# define TEST_SEED 0x1234
+#endif
+
+#define MAX_BUF 4096
+#define TEST_SIZE 32
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ This file is a patched version of `crc64_funcs_test.c` from ISA-l.
+
+ Here we test that 3 implementations of CRC64 (fast assembly, slow with lookup table and very slow)
+ with YT-specific polynomial all give same result.
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t crc64_yt_norm_ref(
+ uint64_t init_crc,
+ const unsigned char *buf,
+ uint64_t len);
+
+uint64_t crc64_yt_norm_base(
+ uint64_t init_crc,
+ const unsigned char *buf,
+ uint64_t len);
+
+uint64_t crc64_yt_norm_by8(
+ uint64_t init_crc,
+ const unsigned char *buf,
+ uint64_t len);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Generates pseudo-random data
+
+void rand_buffer(unsigned char *buf, long buffer_size)
+{
+ long i;
+ for (i = 0; i < buffer_size; i++)
+ buf[i] = rand();
+}
+
+// Test cases
+int zeros_test();
+int simple_pattern_test();
+int seeds_sizes_test();
+int eob_test();
+int update_test();
+
+int verbose = 0;
+void *buf_alloc = NULL;
+
+int main(int argc, char *argv[])
+{
+ int fail = 0, fail_case = 0;
+ int i, ret;
+
+ verbose = argc - 1;
+
+ // Align to 32B boundary
+ ret = posix_memalign(&buf_alloc, TEST_SIZE, MAX_BUF * TEST_SIZE);
+ if (ret) {
+ printf("alloc error: Fail");
+ return -1;
+ }
+ srand(TEST_SEED);
+ printf("CRC64 Tests\n");
+
+ fail_case += zeros_test();
+ fail_case += simple_pattern_test();
+ fail_case += seeds_sizes_test();
+ fail_case += eob_test();
+ fail_case += update_test();
+ printf(" done: %s\n", fail_case ? "Fail" : "Pass");
+
+ if (fail_case) {
+ printf("\nFailed %d tests\n", fail_case);
+ fail++;
+ }
+
+ printf("CRC64 Tests all done: %s\n", fail ? "Fail" : "Pass");
+
+ return fail;
+}
+
+// Test of all zeros
+int zeros_test()
+{
+ uint64_t crc_ref, crc_base, crc;
+ int fail = 0;
+ unsigned char *buf = NULL;
+
+ buf = (unsigned char *)buf_alloc;
+ memset(buf, 0, MAX_BUF * 10);
+ crc_ref = crc64_yt_norm_ref(TEST_SEED, buf, MAX_BUF * 10);
+ crc_base = crc64_yt_norm_base(TEST_SEED, buf, MAX_BUF * 10);
+ crc = crc64_yt_norm_by8(TEST_SEED, buf, MAX_BUF * 10);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref)) {
+ fail++;
+ printf("\n opt ref\n");
+ printf(" ------ ------\n");
+ printf("crc zero = 0x%16lx 0x%16lx 0x%16lx \n", crc_ref, crc_base, crc);
+ } else
+ printf(".");
+
+ return fail;
+}
+
+// Another simple test pattern
+int simple_pattern_test()
+{
+ uint64_t crc_ref, crc_base, crc;
+ int fail = 0;
+ unsigned char *buf = NULL;
+
+ buf = (unsigned char *)buf_alloc;
+ memset(buf, 0x8a, MAX_BUF);
+ crc_ref = crc64_yt_norm_ref(TEST_SEED, buf, MAX_BUF);
+ crc_base = crc64_yt_norm_base(TEST_SEED, buf, MAX_BUF);
+ crc = crc64_yt_norm_by8(TEST_SEED, buf, MAX_BUF);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref))
+ fail++;
+ if (verbose)
+ printf("crc all 8a = 0x%16lx 0x%16lx 0x%16lx\n", crc_ref, crc_base, crc);
+ else
+ printf(".");
+
+ return fail;
+}
+
+int seeds_sizes_test()
+{
+ uint64_t crc_ref, crc_base, crc;
+ int fail = 0;
+ int i;
+ uint64_t r, s;
+ unsigned char *buf = NULL;
+
+ // Do a few random tests
+ buf = (unsigned char *)buf_alloc; //reset buf
+ r = rand();
+ rand_buffer(buf, MAX_BUF * TEST_SIZE);
+
+ for (i = 0; i < TEST_SIZE; i++) {
+ crc_ref = crc64_yt_norm_ref(r, buf, MAX_BUF);
+ crc_base = crc64_yt_norm_base(r, buf, MAX_BUF);
+ crc = crc64_yt_norm_by8(r, buf, MAX_BUF);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref))
+ fail++;
+ if (verbose)
+ printf("crc rand%3d = 0x%16lx 0x%16lx 0x%16lx\n", i, crc_ref, crc_base,
+ crc);
+ else if (i % (TEST_SIZE / 8) == 0)
+ printf(".");
+ buf += MAX_BUF;
+ }
+
+ // Do a few random sizes
+ buf = (unsigned char *)buf_alloc; //reset buf
+ r = rand();
+
+ for (i = MAX_BUF; i >= 0; i--) {
+ crc_ref = crc64_yt_norm_ref(r, buf, i);
+ crc_base = crc64_yt_norm_base(r, buf, i);
+ crc = crc64_yt_norm_by8(r, buf, i);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref)) {
+ fail++;
+ printf("fail random size%i 0x%16lx 0x%16lx 0x%16lx\n", i, crc_ref,
+ crc_base, crc);
+ } else if (i % (MAX_BUF / 8) == 0)
+ printf(".");
+ }
+
+ // Try different seeds
+ for (s = 0; s < 20; s++) {
+ buf = (unsigned char *)buf_alloc; //reset buf
+
+ r = rand(); // just to get a new seed
+ rand_buffer(buf, MAX_BUF * TEST_SIZE); // new pseudo-rand data
+
+ if (verbose)
+ printf("seed = 0x%lx\n", r);
+
+ for (i = 0; i < TEST_SIZE; i++) {
+ crc_ref = crc64_yt_norm_ref(r, buf, MAX_BUF);
+ crc_base = crc64_yt_norm_base(r, buf, MAX_BUF);
+ crc = crc64_yt_norm_by8(r, buf, MAX_BUF);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref))
+ fail++;
+ if (verbose)
+ printf("crc rand%3d = 0x%16lx 0x%16lx 0x%16lx\n", i, crc_ref,
+ crc_base, crc);
+ else if (i % (TEST_SIZE * 20 / 8) == 0)
+ printf(".");
+ buf += MAX_BUF;
+ }
+ }
+
+ return fail;
+}
+
+// Run tests at end of buffer
+int eob_test()
+{
+ uint64_t crc_ref, crc_base, crc;
+ int fail = 0;
+ int i;
+ unsigned char *buf = NULL;
+
+ // Null test
+ if (0 != crc64_yt_norm_by8(0, NULL, 0)) {
+ fail++;
+ printf("crc null test fail\n");
+ }
+
+ buf = (unsigned char *)buf_alloc; //reset buf
+ buf = buf + ((MAX_BUF - 1) * TEST_SIZE); //Line up TEST_SIZE from end
+ for (i = 0; i <= TEST_SIZE; i++) {
+ crc_ref = crc64_yt_norm_ref(TEST_SEED, buf + i, TEST_SIZE - i);
+ crc_base = crc64_yt_norm_base(TEST_SEED, buf + i, TEST_SIZE - i);
+ crc = crc64_yt_norm_by8(TEST_SEED, buf + i, TEST_SIZE - i);
+
+ if ((crc_base != crc_ref) || (crc != crc_ref))
+ fail++;
+ if (verbose)
+ printf("crc eob rand%3d = 0x%16lx 0x%16lx 0x%16lx\n", i, crc_ref,
+ crc_base, crc);
+ else if (i % (TEST_SIZE / 8) == 0)
+ printf(".");
+ }
+
+ return fail;
+}
+
+int update_test()
+{
+ uint64_t crc_ref, crc_base, crc;
+ int fail = 0;
+ int i;
+ uint64_t r;
+ unsigned char *buf = NULL;
+
+ buf = (unsigned char *)buf_alloc; //reset buf
+ r = rand();
+ // Process the whole buf with reference func single call.
+ crc_ref = crc64_yt_norm_ref(r, buf, MAX_BUF * TEST_SIZE);
+ crc_base = crc64_yt_norm_base(r, buf, MAX_BUF * TEST_SIZE);
+ // Process buf with update method.
+ for (i = 0; i < TEST_SIZE; i++) {
+ crc = crc64_yt_norm_by8(r, buf, MAX_BUF);
+ // Update crc seeds and buf pointer.
+ r = crc;
+ buf += MAX_BUF;
+ }
+
+ if ((crc_base != crc_ref) || (crc != crc_ref))
+ fail++;
+ if (verbose)
+ printf("crc rand%3d = 0x%16lx 0x%16lx 0x%16lx\n", i, crc_ref, crc_base, crc);
+ else
+ printf(".");
+
+ return fail;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/ya.make b/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/ya.make
new file mode 100644
index 0000000000..88ae7150b1
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test/ya.make
@@ -0,0 +1,17 @@
+PROGRAM()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+NO_UTIL()
+
+NO_COMPILER_WARNINGS()
+
+PEERDIR(
+ yt/yt/core/misc/isa_crc64
+)
+
+SRCS(
+ crc64_reference_test.c
+)
+
+END()
diff --git a/yt/yt/core/misc/isa_crc64/unittests/ya.make b/yt/yt/core/misc/isa_crc64/unittests/ya.make
new file mode 100644
index 0000000000..0745ec4159
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/unittests/ya.make
@@ -0,0 +1,23 @@
+EXECTEST(crc64_reference_test)
+
+NO_UTIL()
+
+SIZE(
+ SMALL
+)
+
+RUN(
+ crc64_reference_test
+)
+
+DEPENDS(
+ yt/yt/core/misc/isa_crc64/unittests/crc64_reference_test
+)
+
+REQUIREMENTS(ram:12)
+
+END()
+
+RECURSE_FOR_TESTS(
+ crc64_reference_test
+)
diff --git a/yt/yt/core/misc/isa_crc64/ya.make b/yt/yt/core/misc/isa_crc64/ya.make
new file mode 100644
index 0000000000..442891b5a6
--- /dev/null
+++ b/yt/yt/core/misc/isa_crc64/ya.make
@@ -0,0 +1,29 @@
+LIBRARY(isa-l_crc_yt_patch)
+
+LICENSE(BSD-3-Clause)
+
+VERSION(2.28)
+
+NO_UTIL()
+
+IF (ARCH_X86_64)
+ # pclmul is required for fast crc computation
+ CFLAGS(-mpclmul)
+ SRCS(crc64_yt_norm_by8.asm)
+ENDIF()
+
+ADDINCL(
+ FOR asm yt/yt/core/misc/isa_crc64/include # for reg_sizes.asm
+)
+
+SRCS(
+ crc64_yt_norm_refs.c
+
+ checksum.cpp
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests
+)
diff --git a/yt/yt/core/misc/lazy_ptr.h b/yt/yt/core/misc/lazy_ptr.h
new file mode 100644
index 0000000000..c0c510b0aa
--- /dev/null
+++ b/yt/yt/core/misc/lazy_ptr.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "common.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+const TCallback<TIntrusivePtr<T>()>& DefaultRefCountedFactory()
+{
+ static auto result = BIND([] () -> TIntrusivePtr<T> {
+ return New<T>();
+ });
+ return result;
+}
+
+//! Intrusive ptr with lazy creation and double-checked locking.
+template <class T, class TLock = NThreading::TSpinLock>
+class TLazyIntrusivePtr
+ : public TPointerCommon<TLazyIntrusivePtr<T, TLock>, T>
+{
+public:
+ using TFactory = TCallback<TIntrusivePtr<T>()>;
+
+ explicit TLazyIntrusivePtr(TFactory factory = DefaultRefCountedFactory<T>())
+ : Factory_(std::move(factory))
+ { }
+
+ T* Get() const noexcept
+ {
+ MaybeInitialize();
+ return Value_.Get();
+ }
+
+ const TIntrusivePtr<T>& Value() const noexcept
+ {
+ MaybeInitialize();
+ return Value_;
+ }
+
+ bool HasValue() const noexcept
+ {
+ return Initialized_.load();
+ }
+
+private:
+ YT_DECLARE_SPIN_LOCK(TLock, Lock_);
+ TFactory Factory_;
+ mutable TIntrusivePtr<T> Value_;
+ mutable std::atomic<bool> Initialized_ = false;
+
+ void MaybeInitialize() const noexcept
+ {
+ if (!HasValue()) {
+ TGuard<TLock> guard(Lock_);
+ if (!HasValue()) {
+ NTracing::TNullTraceContextGuard guard;
+ Value_ = Factory_();
+ Initialized_.store(true);
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/linear_probe-inl.h b/yt/yt/core/misc/linear_probe-inl.h
new file mode 100644
index 0000000000..ed41a1e2f2
--- /dev/null
+++ b/yt/yt/core/misc/linear_probe-inl.h
@@ -0,0 +1,43 @@
+#ifndef LINEAR_PROBE_INL_H_
+#error "Direct inclusion of this file is not allowed, include linear_probe.h"
+// For the sake of sane code completion.
+#include "linear_probe.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <size_t N>
+void TLinearProbeHashTable::Find(TFingerprint fingerprint, TCompactVector<TValue, N>* result) const
+{
+ DoFind(IndexFromFingerprint(fingerprint), StampFromFingerprint(fingerprint), result);
+}
+
+template <size_t N>
+void TLinearProbeHashTable::DoFind(ui64 index, TStamp stamp, TCompactVector<TValue, N>* result) const
+{
+ YT_ASSERT(stamp != 0);
+
+ ui64 wrappedIndex = index % HashTable_.size();
+ for (int currentIndex = 0; currentIndex < std::ssize(HashTable_); ++currentIndex) {
+ auto tableEntry = HashTable_[wrappedIndex].load(std::memory_order::relaxed);
+ auto tableStamp = StampFromEntry(tableEntry);
+
+ if (tableStamp == 0) {
+ break;
+ }
+ if (tableStamp == stamp) {
+ result->push_back(ValueFromEntry(tableEntry));
+ }
+
+ ++wrappedIndex;
+ if (wrappedIndex == HashTable_.size()) {
+ wrappedIndex = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/linear_probe.cpp b/yt/yt/core/misc/linear_probe.cpp
new file mode 100644
index 0000000000..724e251279
--- /dev/null
+++ b/yt/yt/core/misc/linear_probe.cpp
@@ -0,0 +1,88 @@
+#include "linear_probe.h"
+
+#include <util/system/types.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLinearProbeHashTable::TLinearProbeHashTable(size_t maxElementCount)
+ : HashTable_(maxElementCount * HashTableExpansionParameter)
+{ }
+
+bool TLinearProbeHashTable::Insert(TFingerprint fingerprint, TValue value)
+{
+ return DoInsert(IndexFromFingerprint(fingerprint), StampFromFingerprint(fingerprint), value);
+}
+
+bool TLinearProbeHashTable::DoInsert(ui64 index, TStamp stamp, TValue value)
+{
+ YT_VERIFY(stamp != 0);
+ YT_VERIFY((value >> ValueLog) == 0);
+
+ ui64 wrappedIndex = index % HashTable_.size();
+ auto entry = MakeEntry(stamp, value);
+ for (int currentIndex = 0; currentIndex < std::ssize(HashTable_); ++currentIndex) {
+ auto tableEntry = HashTable_[wrappedIndex].load(std::memory_order::relaxed);
+ auto tableStamp = StampFromEntry(tableEntry);
+
+ if (tableStamp == 0) {
+ auto success = HashTable_[wrappedIndex].compare_exchange_strong(
+ tableEntry,
+ entry,
+ std::memory_order::release,
+ std::memory_order::relaxed);
+ if (success) {
+ return true;
+ }
+ }
+
+ ++wrappedIndex;
+ if (wrappedIndex == HashTable_.size()) {
+ wrappedIndex = 0;
+ }
+ }
+
+ return false;
+}
+
+size_t TLinearProbeHashTable::GetByteSize() const
+{
+ return sizeof(std::atomic<TEntry>) * HashTable_.size();
+}
+
+TLinearProbeHashTable::TStamp TLinearProbeHashTable::StampFromEntry(TLinearProbeHashTable::TEntry entry)
+{
+ return entry >> ValueLog;
+}
+
+TLinearProbeHashTable::TValue TLinearProbeHashTable::ValueFromEntry(TLinearProbeHashTable::TEntry entry)
+{
+ return entry & ((1ULL << ValueLog) - 1);
+}
+
+TLinearProbeHashTable::TEntry TLinearProbeHashTable::MakeEntry(TLinearProbeHashTable::TStamp stamp, TLinearProbeHashTable::TValue value)
+{
+ return (static_cast<TLinearProbeHashTable::TEntry>(stamp) << ValueLog) | value;
+}
+
+ui64 TLinearProbeHashTable::IndexFromFingerprint(TFingerprint fingerprint)
+{
+ return fingerprint;
+}
+
+TLinearProbeHashTable::TStamp TLinearProbeHashTable::StampFromFingerprint(TFingerprint fingerprint)
+{
+ TLinearProbeHashTable::TStamp stamp = fingerprint;
+ if (stamp == 0) {
+ stamp = 1;
+ }
+ return stamp;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/linear_probe.h b/yt/yt/core/misc/linear_probe.h
new file mode 100644
index 0000000000..0810ec3c9c
--- /dev/null
+++ b/yt/yt/core/misc/linear_probe.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "farm_hash.h"
+#include "public.h"
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <util/system/types.h>
+
+#include <atomic>
+#include <vector>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLinearProbeHashTable
+{
+public:
+ // 64-bit hash table entry contains 16-bit stamp and 48-bit value.
+ using TStamp = ui16;
+ using TValue = ui64;
+ using TEntry = ui64;
+
+ explicit TLinearProbeHashTable(size_t maxElementCount);
+
+ bool Insert(TFingerprint fingerprint, TValue value);
+
+ template <size_t N>
+ void Find(TFingerprint fingerprint, TCompactVector<TValue, N>* result) const;
+
+ size_t GetByteSize() const;
+
+private:
+ constexpr static int HashTableExpansionParameter = 2;
+ constexpr static int ValueLog = 48;
+
+ std::vector<std::atomic<TEntry>> HashTable_;
+
+ bool DoInsert(ui64 index, TStamp stamp, TValue value);
+
+ template <size_t N>
+ void DoFind(ui64 index, TStamp stamp, TCompactVector<TValue, N>* result) const;
+
+ static TStamp StampFromEntry(TEntry entry);
+ static TValue ValueFromEntry(TEntry entry);
+ static TEntry MakeEntry(TStamp stamp, TValue value);
+
+ static ui64 IndexFromFingerprint(TFingerprint fingerprint);
+ static TStamp StampFromFingerprint(TFingerprint fingerprint);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define LINEAR_PROBE_INL_H_
+#include "linear_probe-inl.h"
+#undef LINEAR_PROBE_INL_H_
diff --git a/yt/yt/core/misc/lock_free_hash_table-inl.h b/yt/yt/core/misc/lock_free_hash_table-inl.h
new file mode 100644
index 0000000000..4f36d33831
--- /dev/null
+++ b/yt/yt/core/misc/lock_free_hash_table-inl.h
@@ -0,0 +1,210 @@
+#ifndef LOCK_FREE_HASH_TABLE_INL_H_
+#error "Direct inclusion of this file is not allowed, include lock_free_hash_table.h"
+// For the sake of sane code completion.
+#include "lock_free_hash_table.h"
+#endif
+#undef LOCK_FREE_HASH_TABLE_INL_H_
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TLockFreeHashTable<T>::TLockFreeHashTable(size_t maxElementCount)
+ : Size_(maxElementCount * HashTableExpansionFactor)
+ , HashTable_(new std::atomic<TEntry>[Size_]())
+{ }
+
+template <class T>
+TLockFreeHashTable<T>::~TLockFreeHashTable()
+{
+ for (size_t index = 0; index < Size_; ++index) {
+ auto tableEntry = HashTable_[index].load(std::memory_order::relaxed);
+ auto stamp = StampFromEntry(tableEntry);
+ if (stamp != 0) {
+ RetireHazardPointer(ValueFromEntry(tableEntry), [] (auto* ptr) {
+ Unref(ptr);
+ });
+ }
+ }
+}
+
+template <class T>
+template <class TCallback>
+void TLockFreeHashTable<T>::ForEach(TCallback callback)
+{
+ for (size_t index = 0; index < Size_; ++index) {
+ auto tableEntry = HashTable_[index].load(std::memory_order::relaxed);
+ auto stamp = StampFromEntry(tableEntry);
+ if (stamp != 0) {
+ callback(TItemRef(&HashTable_[index]));
+ }
+ }
+}
+
+template <class T>
+size_t TLockFreeHashTable<T>::GetByteSize() const
+{
+ return sizeof(std::atomic<TEntry>) * Size_;
+}
+
+template <class T>
+bool TLockFreeHashTable<T>::Insert(TFingerprint fingerprint, TValuePtr value)
+{
+ auto index = IndexFromFingerprint(fingerprint) % Size_;
+ auto stamp = StampFromFingerprint(fingerprint);
+
+ auto entry = MakeEntry(stamp, value.Get());
+
+ for (size_t probeCount = Size_; probeCount != 0;) {
+ auto tableEntry = HashTable_[index].load(std::memory_order::acquire);
+ auto tableStamp = StampFromEntry(tableEntry);
+
+ if (tableStamp == 0) {
+ auto success = HashTable_[index].compare_exchange_strong(
+ tableEntry,
+ entry,
+ std::memory_order::release,
+ std::memory_order::acquire);
+ if (success) {
+ value.Release();
+ return true;
+ }
+ }
+
+ // This hazard ptr protects from Unref. We do not want to change ref count so frequently.
+ auto item = THazardPtr<T>::Acquire([&] {
+ return ValueFromEntry(HashTable_[index].load(std::memory_order::acquire));
+ }, ValueFromEntry(tableEntry));
+
+ if (TEqualTo<T>()(item.Get(), value.Get())) {
+ return false;
+ }
+
+ ++index;
+ if (index == Size_) {
+ index = 0;
+ }
+ --probeCount;
+ }
+
+ return false;
+}
+
+template <class T>
+template <class TKey>
+TIntrusivePtr<T> TLockFreeHashTable<T>::Find(TFingerprint fingerprint, const TKey& key)
+{
+ auto index = IndexFromFingerprint(fingerprint) % Size_;
+ auto stamp = StampFromFingerprint(fingerprint);
+
+ for (size_t probeCount = Size_; probeCount != 0;) {
+ auto tableEntry = HashTable_[index].load(std::memory_order::acquire);
+ auto tableStamp = StampFromEntry(tableEntry);
+
+ if (tableStamp == 0) {
+ break;
+ }
+
+ if (tableStamp == stamp) {
+ // This hazard ptr protects from Unref. We do not want to change ref count so frequently.
+ // TIntrusivePtr::AcquireUnchecked could be used outside this function.
+
+ auto item = THazardPtr<T>::Acquire([&] {
+ return ValueFromEntry(HashTable_[index].load(std::memory_order::acquire));
+ }, ValueFromEntry(tableEntry));
+
+ if (TEqualTo<T>()(item.Get(), key)) {
+ return TValuePtr(item.Get());
+ }
+ }
+
+ ++index;
+ if (index == Size_) {
+ index = 0;
+ }
+ --probeCount;
+ }
+
+ return nullptr;
+}
+
+template <class T>
+template <class TKey>
+typename TLockFreeHashTable<T>::TItemRef TLockFreeHashTable<T>::FindRef(TFingerprint fingerprint, const TKey& key)
+{
+ using TItemRef = typename TLockFreeHashTable<T>::TItemRef;
+
+ auto index = IndexFromFingerprint(fingerprint) % Size_;
+ auto stamp = StampFromFingerprint(fingerprint);
+
+ for (size_t probeCount = Size_; probeCount != 0;) {
+ auto tableEntry = HashTable_[index].load(std::memory_order::relaxed);
+ auto tableStamp = StampFromEntry(tableEntry);
+
+ if (tableStamp == 0) {
+ break;
+ }
+
+ if (tableStamp == stamp) {
+ // This hazard ptr protects from Unref. We do not want to change ref count so frequently.
+ // TIntrusivePtr::AcquireUnchecked could be used outside this function.
+
+ auto item = THazardPtr<T>::Acquire([&] {
+ return ValueFromEntry(HashTable_[index].load(std::memory_order::relaxed));
+ }, ValueFromEntry(tableEntry));
+
+ if (TEqualTo<T>()(item.Get(), key)) {
+ return TItemRef(&HashTable_[index]);
+ }
+ }
+
+ ++index;
+ if (index == Size_) {
+ index = 0;
+ }
+ --probeCount;
+ }
+
+ return TItemRef();
+}
+
+template <class T>
+typename TLockFreeHashTable<T>::TStamp
+ TLockFreeHashTable<T>::StampFromEntry(TEntry entry)
+{
+ return entry >> ValueLog;
+}
+
+template <class T>
+T* TLockFreeHashTable<T>::ValueFromEntry(TEntry entry)
+{
+ return reinterpret_cast<T*>(entry & ((1ULL << ValueLog) - 1));
+}
+
+template <class T>
+typename TLockFreeHashTable<T>::TEntry
+ TLockFreeHashTable<T>::MakeEntry(TStamp stamp, T* value)
+{
+ YT_ASSERT(stamp != 0);
+ YT_ASSERT(StampFromEntry(reinterpret_cast<TEntry>(value)) == 0);
+ return (static_cast<TEntry>(stamp) << ValueLog) | reinterpret_cast<TEntry>(value);
+}
+
+template <class T>
+size_t TLockFreeHashTable<T>::IndexFromFingerprint(TFingerprint fingerprint)
+{
+ // TODO(lukyan): Use higher bits of fingerprint. Lower are used by stamp.
+ return fingerprint;
+}
+
+template <class T>
+typename TLockFreeHashTable<T>::TStamp
+ TLockFreeHashTable<T>::StampFromFingerprint(TFingerprint fingerprint)
+{
+ return (fingerprint << 1) | 1ULL;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/lock_free_hash_table.h b/yt/yt/core/misc/lock_free_hash_table.h
new file mode 100644
index 0000000000..b837dc1811
--- /dev/null
+++ b/yt/yt/core/misc/lock_free_hash_table.h
@@ -0,0 +1,142 @@
+#pragma once
+
+#include "public.h"
+#include "atomic_ptr.h"
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TLockFreeHashTable
+{
+public:
+ //! 64-bit hash table entry contains 16-bit stamp and 48-bit value.
+ using TStamp = ui16;
+ using TEntry = ui64;
+ using TValuePtr = TIntrusivePtr<T>;
+
+ template <class TCallback>
+ void ForEach(TCallback callback);
+
+ class TItemRef
+ {
+ public:
+ using TEntry = ui64;
+ using TValuePtr = TIntrusivePtr<T>;
+
+ TItemRef(const TItemRef&) = default;
+
+ explicit TItemRef(std::atomic<TEntry>* entry = nullptr)
+ : Entry_(entry)
+ { }
+
+ explicit operator bool () const
+ {
+ return Entry_;
+ }
+
+ // TODO(lukyan): Implement Get returns hazard ptr.
+ TValuePtr Get() const
+ {
+ if (!Entry_) {
+ return nullptr;
+ }
+ auto item = THazardPtr<T>::Acquire([&] {
+ return ValueFromEntry(Entry_->load(std::memory_order::acquire));
+ });
+
+ return TValuePtr(item.Get());
+ }
+
+ //! Updates existing element.
+ void Update(TValuePtr value)
+ {
+ // Fingerprint must be equal.
+ auto stamp = StampFromEntry(Entry_->load(std::memory_order::acquire));
+
+ auto entry = MakeEntry(stamp, value.Release());
+ // TODO(lukyan): Keep dereferenced value and update via CAS.
+ auto oldEntry = Entry_->exchange(entry);
+
+ DeleteEntry(oldEntry);
+ }
+
+ bool Update(TValuePtr value, const T* expected)
+ {
+ if (value.Get() == expected) {
+ return false;
+ }
+
+ auto currentEntry = Entry_->load(std::memory_order::acquire);
+ // Fingerprint must be equal.
+ auto stamp = StampFromEntry(currentEntry);
+
+ if (ValueFromEntry(currentEntry) != expected) {
+ return false;
+ }
+
+ auto entry = MakeEntry(stamp, value.Get());
+
+ if (!Entry_->compare_exchange_strong(currentEntry, entry)) {
+ return false;
+ }
+
+ value.Release();
+ DeleteEntry(currentEntry);
+ return true;
+ }
+
+ private:
+ std::atomic<TEntry>* Entry_ = nullptr;
+
+ static void DeleteEntry(TEntry entry)
+ {
+ RetireHazardPointer(ValueFromEntry(entry), [] (auto* ptr) {
+ Unref(ptr);
+ });
+ }
+ };
+
+ explicit TLockFreeHashTable(size_t maxElementCount);
+
+ ~TLockFreeHashTable();
+
+ size_t GetByteSize() const;
+
+ //! Inserts element. Called concurrently from multiple threads.
+ bool Insert(TFingerprint fingerprint, TValuePtr value);
+
+ template <class TKey>
+ TIntrusivePtr<T> Find(TFingerprint fingerprint, const TKey& key);
+
+ template <class TKey>
+ TItemRef FindRef(TFingerprint fingerprint, const TKey& key);
+
+private:
+ const size_t Size_;
+ std::unique_ptr<std::atomic<TEntry>[]> HashTable_;
+
+ static constexpr int HashTableExpansionFactor = 2;
+ static constexpr int ValueLog = 48;
+
+ static TStamp StampFromEntry(TEntry entry);
+
+ static T* ValueFromEntry(TEntry entry);
+
+ static TEntry MakeEntry(TStamp stamp, T* value);
+
+ static size_t IndexFromFingerprint(TFingerprint fingerprint);
+
+ static TStamp StampFromFingerprint(TFingerprint fingerprint);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define LOCK_FREE_HASH_TABLE_INL_H_
+#include "lock_free_hash_table-inl.h"
+#undef LOCK_FREE_HASH_TABLE_INL_H_
diff --git a/yt/yt/core/misc/maybe_inf-inl.h b/yt/yt/core/misc/maybe_inf-inl.h
new file mode 100644
index 0000000000..4c0111e337
--- /dev/null
+++ b/yt/yt/core/misc/maybe_inf-inl.h
@@ -0,0 +1,89 @@
+#ifndef MAYBE_INF_INL_H_
+#error "Direct inclusion of this file is not allowed, include maybe_inf.h"
+// For the sake of sane code completion.
+#include "maybe_inf.h"
+#endif
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <std::unsigned_integral T>
+TMaybeInf<T>::TMaybeInf(T value) noexcept
+ : Value_(value)
+{
+ YT_ASSERT(value != TTraits::InfiniteValue);
+}
+
+template <std::unsigned_integral T>
+TMaybeInf<T> TMaybeInf<T>::Infinity() noexcept
+{
+ // NB: Avoid using public constructor which prohibits infinity.
+ TMaybeInf result;
+ result.Value_ = TTraits::InfiniteValue;
+ return result;
+}
+
+template <std::unsigned_integral T>
+T TMaybeInf<T>::ToUnderlying() const noexcept
+{
+ YT_ASSERT(!IsInfinity());
+ return Value_;
+}
+
+template <std::unsigned_integral T>
+bool TMaybeInf<T>::IsInfinity() const noexcept
+{
+ return Value_ == TTraits::InfiniteValue;
+}
+
+template <std::unsigned_integral T>
+bool TMaybeInf<T>::CanBeIncreased(TMaybeInf delta) const noexcept
+{
+ return (!IsInfinity() && !delta.IsInfinity()) || Value_ == 0 || delta.Value_ == 0;
+}
+
+template <std::unsigned_integral T>
+bool TMaybeInf<T>::CanBeDecreased(TMaybeInf delta) const noexcept
+{
+ return
+ !delta.IsInfinity() &&
+ (IsInfinity() ? delta.Value_ == 0 : Value_ >= delta.Value_);
+}
+
+template <std::unsigned_integral T>
+void TMaybeInf<T>::IncreaseBy(TMaybeInf delta) noexcept
+{
+ YT_ASSERT(CanBeIncreased(delta));
+
+ if (IsInfinity() || delta.IsInfinity()) {
+ Value_ = TTraits::InfiniteValue;
+ } else {
+ auto newValue = Value_ + delta.ToUnderlying();
+ // Check overflow.
+ YT_ASSERT(newValue != TTraits::InfiniteValue);
+ YT_ASSERT(newValue >= Value_);
+ Value_ = newValue;
+ }
+}
+
+template <std::unsigned_integral T>
+void TMaybeInf<T>::DecreaseBy(TMaybeInf delta) noexcept
+{
+ YT_ASSERT(CanBeDecreased(delta));
+
+ Value_ -= delta.Value_;
+}
+
+template <std::unsigned_integral T>
+std::strong_ordering TMaybeInf<T>::operator<=>(TMaybeInf that) const noexcept
+{
+ // It works because infinity is represented as `numeric_limits<T>::max()`.
+ return Value_ <=> that.Value_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/maybe_inf.h b/yt/yt/core/misc/maybe_inf.h
new file mode 100644
index 0000000000..b40e6bb8f8
--- /dev/null
+++ b/yt/yt/core/misc/maybe_inf.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <compare>
+#include <concepts>
+#include <limits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <std::unsigned_integral T>
+struct TMaybeInfTraits
+{
+ constexpr static T InfiniteValue = std::numeric_limits<T>::max();
+};
+
+template <std::unsigned_integral T>
+class TMaybeInf
+{
+public:
+ using TTraits = TMaybeInfTraits<T>;
+
+ TMaybeInf() noexcept = default;
+
+ explicit TMaybeInf(T value) noexcept;
+ TMaybeInf(const TMaybeInf&) noexcept = default;
+ TMaybeInf& operator=(const TMaybeInf&) noexcept = default;
+
+ static TMaybeInf Infinity() noexcept;
+
+ T ToUnderlying() const noexcept;
+
+ bool IsInfinity() const noexcept;
+
+ bool CanBeIncreased(TMaybeInf delta) const noexcept;
+ bool CanBeDecreased(TMaybeInf delta) const noexcept;
+
+ void IncreaseBy(TMaybeInf delta) noexcept;
+ void DecreaseBy(TMaybeInf delta) noexcept;
+
+ std::strong_ordering operator<=>(TMaybeInf that) const noexcept;
+
+protected:
+ T Value_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define MAYBE_INF_INL_H_
+#include "maybe_inf-inl.h"
+#undef MAYBE_INF_INL_H_
diff --git a/yt/yt/core/misc/memory_reference_tracker.cpp b/yt/yt/core/misc/memory_reference_tracker.cpp
new file mode 100644
index 0000000000..4fce3b0d2a
--- /dev/null
+++ b/yt/yt/core/misc/memory_reference_tracker.cpp
@@ -0,0 +1,35 @@
+#include "memory_reference_tracker.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef TrackMemory(
+ const IMemoryReferenceTrackerPtr& tracker,
+ TSharedRef reference,
+ bool keepExistingTracking)
+{
+ if (!tracker || !reference) {
+ return reference;
+ }
+ return tracker->Track(reference, keepExistingTracking);
+}
+
+TSharedRefArray TrackMemory(
+ const IMemoryReferenceTrackerPtr& tracker,
+ TSharedRefArray array,
+ bool keepExistingTracking)
+{
+ if (!tracker || !array) {
+ return array;
+ }
+ TSharedRefArrayBuilder builder(array.Size());
+ for (const auto& part : array) {
+ builder.Add(tracker->Track(part, keepExistingTracking));
+ }
+ return builder.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/memory_reference_tracker.h b/yt/yt/core/misc/memory_reference_tracker.h
new file mode 100644
index 0000000000..dda9f38c53
--- /dev/null
+++ b/yt/yt/core/misc/memory_reference_tracker.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Tracks memory used by references.
+/*!
+ * Memory tracking is implemented by specific shared ref holders.
+ * #Track returns reference with a holder that wraps the old one and also
+ * enables accounting memory in memory tracker's internal state.
+
+ * Subsequent #Track calls for this reference drop memory reference tracker's
+ * holder unless #keepExistingTracking is true.
+ */
+struct IMemoryReferenceTracker
+ : public TRefCounted
+{
+ //! Tracks reference in a tracker while the reference itself is alive.
+ virtual TSharedRef Track(
+ TSharedRef reference,
+ bool keepExistingTracking = false) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IMemoryReferenceTracker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef TrackMemory(
+ const IMemoryReferenceTrackerPtr& tracker,
+ TSharedRef reference,
+ bool keepExistingTracking = false);
+TSharedRefArray TrackMemory(
+ const IMemoryReferenceTrackerPtr& tracker,
+ TSharedRefArray array,
+ bool keepExistingTracking = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/memory_usage_tracker.cpp b/yt/yt/core/misc/memory_usage_tracker.cpp
new file mode 100644
index 0000000000..b5b627a302
--- /dev/null
+++ b/yt/yt/core/misc/memory_usage_tracker.cpp
@@ -0,0 +1,184 @@
+#include "memory_usage_tracker.h"
+#include "singleton.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullMemoryUsageTracker
+ : public IMemoryUsageTracker
+{
+public:
+ TError TryAcquire(i64 /*size*/) override
+ {
+ return {};
+ }
+
+ TError TryChange(i64 /*size*/) override
+ {
+ return {};
+ }
+
+ void Acquire(i64 /*size*/) override
+ { }
+
+ void Release(i64 /*size*/) override
+ { }
+
+ void SetLimit(i64 /*size*/) override
+ { }
+};
+
+IMemoryUsageTrackerPtr GetNullMemoryUsageTracker()
+{
+ return LeakyRefCountedSingleton<TNullMemoryUsageTracker>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMemoryUsageTrackerGuard::TMemoryUsageTrackerGuard(TMemoryUsageTrackerGuard&& other)
+{
+ MoveFrom(std::move(other));
+}
+
+TMemoryUsageTrackerGuard::~TMemoryUsageTrackerGuard()
+{
+ Release();
+}
+
+TMemoryUsageTrackerGuard& TMemoryUsageTrackerGuard::operator=(TMemoryUsageTrackerGuard&& other)
+{
+ if (this != &other) {
+ Release();
+ MoveFrom(std::move(other));
+ }
+ return *this;
+}
+
+void TMemoryUsageTrackerGuard::MoveFrom(TMemoryUsageTrackerGuard&& other)
+{
+ Tracker_ = other.Tracker_;
+ Size_ = other.Size_;
+ AcquiredSize_ = other.AcquiredSize_;
+ Granularity_ = other.Granularity_;
+
+ other.Tracker_ = nullptr;
+ other.Size_ = 0;
+ other.AcquiredSize_ = 0;
+ other.Granularity_ = 0;
+}
+
+TMemoryUsageTrackerGuard TMemoryUsageTrackerGuard::Acquire(
+ IMemoryUsageTrackerPtr tracker,
+ i64 size,
+ i64 granularity)
+{
+ if (!tracker) {
+ return {};
+ }
+
+ YT_VERIFY(size >= 0);
+ TMemoryUsageTrackerGuard guard;
+ guard.Tracker_ = tracker;
+ guard.Size_ = size;
+ guard.Granularity_ = granularity;
+ if (size >= granularity) {
+ guard.AcquiredSize_ = size;
+ tracker->Acquire(size);
+ }
+ return guard;
+}
+
+TErrorOr<TMemoryUsageTrackerGuard> TMemoryUsageTrackerGuard::TryAcquire(
+ IMemoryUsageTrackerPtr tracker,
+ i64 size,
+ i64 granularity)
+{
+ YT_VERIFY(size >= 0);
+ YT_VERIFY(tracker);
+
+ auto error = tracker->TryAcquire(size);
+ if (!error.IsOK()) {
+ return error;
+ }
+ TMemoryUsageTrackerGuard guard;
+ guard.Tracker_ = tracker;
+ guard.Size_ = size;
+ guard.AcquiredSize_ = size;
+ guard.Granularity_ = granularity;
+ return std::move(guard);
+}
+
+void TMemoryUsageTrackerGuard::Release()
+{
+ if (Tracker_) {
+ if (AcquiredSize_) {
+ Tracker_->Release(AcquiredSize_);
+ }
+
+ ReleaseNoReclaim();
+ }
+}
+
+void TMemoryUsageTrackerGuard::ReleaseNoReclaim()
+{
+ Tracker_.Reset();
+ Size_ = 0;
+ AcquiredSize_ = 0;
+ Granularity_ = 0;
+}
+
+TMemoryUsageTrackerGuard::operator bool() const
+{
+ return Tracker_.operator bool();
+}
+
+i64 TMemoryUsageTrackerGuard::GetSize() const
+{
+ return Size_;
+}
+
+void TMemoryUsageTrackerGuard::SetSize(i64 size)
+{
+ if (!Tracker_) {
+ return;
+ }
+
+ YT_VERIFY(size >= 0);
+ Size_ = size;
+ if (std::abs(Size_ - AcquiredSize_) >= Granularity_) {
+ if (Size_ > AcquiredSize_) {
+ Tracker_->Acquire(Size_ - AcquiredSize_);
+ } else {
+ Tracker_->Release(AcquiredSize_ - Size_);
+ }
+ AcquiredSize_ = Size_;
+ }
+}
+
+void TMemoryUsageTrackerGuard::IncrementSize(i64 sizeDelta)
+{
+ SetSize(Size_ + sizeDelta);
+}
+
+TMemoryUsageTrackerGuard TMemoryUsageTrackerGuard::TransferMemory(i64 size)
+{
+ YT_VERIFY(Size_ >= size);
+
+ auto acquiredDelta = std::min(AcquiredSize_, size);
+
+ Size_ -= size;
+ AcquiredSize_ -= acquiredDelta;
+
+ TMemoryUsageTrackerGuard guard;
+ guard.Tracker_ = Tracker_;
+ guard.Size_ = size;
+ guard.AcquiredSize_ = acquiredDelta;
+ guard.Granularity_ = Granularity_;
+ return std::move(guard);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/memory_usage_tracker.h b/yt/yt/core/misc/memory_usage_tracker.h
new file mode 100644
index 0000000000..897ffe4dc5
--- /dev/null
+++ b/yt/yt/core/misc/memory_usage_tracker.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "error.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IMemoryUsageTracker
+ : public TRefCounted
+{
+ virtual TError TryAcquire(i64 size) = 0;
+ virtual TError TryChange(i64 size) = 0;
+ virtual void Acquire(i64 size) = 0;
+ virtual void Release(i64 size) = 0;
+ virtual void SetLimit(i64 size) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IMemoryUsageTracker)
+
+IMemoryUsageTrackerPtr GetNullMemoryUsageTracker();
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMemoryUsageTrackerGuard
+ : private TNonCopyable
+{
+public:
+ TMemoryUsageTrackerGuard() = default;
+ TMemoryUsageTrackerGuard(const TMemoryUsageTrackerGuard& other) = delete;
+ TMemoryUsageTrackerGuard(TMemoryUsageTrackerGuard&& other);
+ ~TMemoryUsageTrackerGuard();
+
+ TMemoryUsageTrackerGuard& operator=(const TMemoryUsageTrackerGuard& other) = delete;
+ TMemoryUsageTrackerGuard& operator=(TMemoryUsageTrackerGuard&& other);
+
+ static TMemoryUsageTrackerGuard Acquire(
+ IMemoryUsageTrackerPtr tracker,
+ i64 size,
+ i64 granularity = 1);
+ static TErrorOr<TMemoryUsageTrackerGuard> TryAcquire(
+ IMemoryUsageTrackerPtr tracker,
+ i64 size,
+ i64 granularity = 1);
+
+ void Release();
+
+ //! Releases the guard but does not return memory to the tracker.
+ //! The caller should care about releasing memory itself.
+ void ReleaseNoReclaim();
+
+ explicit operator bool() const;
+
+ i64 GetSize() const;
+ void SetSize(i64 size);
+ void IncrementSize(i64 sizeDelta);
+ TMemoryUsageTrackerGuard TransferMemory(i64 size);
+
+private:
+ IMemoryUsageTrackerPtr Tracker_;
+ i64 Size_ = 0;
+ i64 AcquiredSize_ = 0;
+ i64 Granularity_ = 0;
+
+ void MoveFrom(TMemoryUsageTrackerGuard&& other);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/mpl.h b/yt/yt/core/misc/mpl.h
new file mode 100644
index 0000000000..be3b00ebbc
--- /dev/null
+++ b/yt/yt/core/misc/mpl.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <util/generic/typetraits.h>
+
+#include <tuple>
+#include <type_traits>
+
+// See the following references for an inspiration:
+// * http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/type_traits?revision=HEAD&view=markup
+// * http://www.boost.org/doc/libs/1_48_0/libs/type_traits/doc/html/index.html
+// * http://www.boost.org/doc/libs/1_48_0/libs/mpl/doc/index.html
+
+namespace NYT::NMpl {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T, bool isPrimitive>
+struct TCallTraitsHelper
+{ };
+
+template <class T>
+struct TCallTraitsHelper<T, true>
+{
+ using TType = T;
+};
+
+template <class T>
+struct TCallTraitsHelper<T, false>
+{
+ using TType = const T&;
+};
+
+template <template <class...> class TTemplate, class... TArgs>
+void DerivedFromSpecializationImpl(const TTemplate<TArgs...>&);
+
+} // namespace NDetail
+
+//! A trait for choosing appropriate argument and return types for functions.
+/*!
+ * All types except for primitive ones should be passed to functions
+ * and returned from const getters by const ref.
+ */
+template <class T>
+struct TCallTraits
+ : public NDetail::TCallTraitsHelper<T, !std::is_class<T>::value>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TIsPod
+ : std::integral_constant<bool, ::TTypeTraits<T>::IsPod>
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TDerived, template <class...> class TTemplatedBase>
+concept DerivedFromSpecializationOf = requires(const TDerived& instance)
+{
+ NDetail::DerivedFromSpecializationImpl<TTemplatedBase>(instance);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Inspired by https://stackoverflow.com/questions/51032671/idiomatic-way-to-write-concept-that-says-that-type-is-a-stdvector
+template<class, template<class...> class>
+inline constexpr bool IsSpecialization = false;
+template<template<class...> class T, class... Args>
+inline constexpr bool IsSpecialization<T<Args...>, T> = true;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NMpl
diff --git a/yt/yt/core/misc/mpsc_fair_share_queue-inl.h b/yt/yt/core/misc/mpsc_fair_share_queue-inl.h
new file mode 100644
index 0000000000..523128cfcf
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_fair_share_queue-inl.h
@@ -0,0 +1,392 @@
+#ifndef MPSC_FAIR_SHARE_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include mpsc_fair_share_queue.h"
+// For the sake of sane code completion.
+#include "mpsc_fair_share_queue.h"
+#endif
+
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NMpscFSQueue {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TCookie = uintptr_t;
+
+// For yandex smart pointer types
+template <typename T>
+ requires requires(T t) { t.Get(); }
+TCookie ToCookie(const T& value)
+{
+ return reinterpret_cast<TCookie>(value.Get());
+}
+
+// For std smart pointers
+template <typename T>
+ requires requires(T t) { t.get(); }
+TCookie ToCookie(const T& value)
+{
+ return reinterpret_cast<TCookie>(value.get());
+}
+
+// For types that are directly convertible to uintptr_t, like integral and pointer
+template <typename T>
+ requires std::is_convertible_v<T, TCookie>
+TCookie ToCookie(T value)
+{
+ return value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+TBucketGeneric<TPoolId, TItem, TFairShareTag>::TBucketGeneric(TExecutorPoolPtr pool, TFairShareTag tag)
+ : Pool(std::move(pool))
+ , Tag(std::move(tag))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+THeapItemGeneric<TPoolId, TItem, TFairShareTag>::THeapItemGeneric(TBucketPtr bucket)
+ : Bucket(std::move(bucket))
+{
+ AdjustBackReference(this);
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+THeapItemGeneric<TPoolId, TItem, TFairShareTag>::THeapItemGeneric(THeapItemGeneric&& other) noexcept
+ : Bucket(std::move(other.Bucket))
+{
+ AdjustBackReference(this);
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+auto THeapItemGeneric<TPoolId, TItem, TFairShareTag>::operator=(THeapItemGeneric&& other) noexcept -> THeapItemGeneric&
+{
+ Bucket = std::move(other.Bucket);
+ AdjustBackReference(this);
+
+ return *this;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void THeapItemGeneric<TPoolId, TItem, TFairShareTag>::AdjustBackReference(THeapItemGeneric* iterator)
+{
+ if (Bucket) {
+ Bucket->HeapIterator = iterator;
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+THeapItemGeneric<TPoolId, TItem, TFairShareTag>::~THeapItemGeneric()
+{
+ if (Bucket) {
+ Bucket->HeapIterator = nullptr;
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+bool THeapItemGeneric<TPoolId, TItem, TFairShareTag>::operator<(const THeapItemGeneric& rhs) const
+{
+ return std::tie(Bucket->ExcessTime, Bucket->RunningTaskCount) <
+ std::tie(rhs.Bucket->ExcessTime, rhs.Bucket->RunningTaskCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>::TExecutionPoolGeneric(TPoolId poolId)
+ : PoolId(std::move(poolId))
+{ }
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+auto TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>::TryDequeue() -> std::optional<TExecutingTask>
+{
+ auto now = GetCpuInstant();
+ std::optional<TExecutingTask> result;
+ while (!Heap.empty() && !result) {
+ const auto& bucket = Heap.front().Bucket;
+
+ if (!bucket->Queue.empty()) {
+ result = TExecutingTask{
+ std::move(bucket->Queue.front()),
+ bucket,
+ now,
+ };
+
+ ++bucket->RunningTaskCount;
+ ++RunningTaskCount;
+
+ bucket->Queue.pop();
+ }
+
+ if (bucket->Queue.empty()) {
+ ExtractHeap(Heap.begin(), Heap.end());
+ Heap.pop_back();
+ }
+ }
+
+ return result;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+auto TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>::GetOrAddBucket(
+ const TEnqueuedTask& scheduled,
+ const TIntrusivePtr<TExecutionPoolGeneric>& pool) -> TBucketPtr
+{
+ TBucketPtr bucket;
+
+ auto it = TagToBucket.find(scheduled.FairShareTag);
+ if (it != TagToBucket.end()) {
+ bucket = it->second;
+ YT_VERIFY(bucket);
+ } else {
+ bucket = New<TBucket>(pool, scheduled.FairShareTag);
+ if (!Heap.empty()) {
+ bucket->ExcessTime = Heap.front().Bucket->ExcessTime;
+ }
+ }
+
+ if (!bucket->HeapIterator) {
+ TagToBucket[scheduled.FairShareTag] = bucket;
+ Heap.emplace_back(bucket);
+ AdjustHeapBack(Heap.begin(), Heap.end());
+ }
+ YT_VERIFY(bucket->HeapIterator);
+
+ return bucket;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+bool TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>::operator<(const TExecutionPoolGeneric& pool) const
+{
+ return std::tie(ExcessTime, RunningTaskCount) < std::tie(pool.ExcessTime, pool.RunningTaskCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NMpscFSQueue
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::~TMpscFairShareQueue()
+{
+ Cleanup();
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::Enqueue(TEnqueuedTask task)
+{
+ Prequeue_.Enqueue(std::move(task));
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::EnqueueMany(std::vector<TEnqueuedTask>&& tasks)
+{
+ Prequeue_.EnqueueMany(std::move(tasks));
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::PrepareDequeue()
+{
+ AccountCurrentlyExecutingBuckets();
+ TruncatePoolExcessTime();
+ Prequeue_.ConsumeAll([&] (auto& batch) {
+ MoveToFairQueue(batch);
+ });
+}
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::TruncatePoolExcessTime()
+{
+ auto minPool = GetStarvingPool();
+ if (!minPool) {
+ return;
+ }
+
+ for (const auto& pool : Pools_) {
+ if (pool) {
+ pool->ExcessTime = std::max<TCpuDuration>(0, pool->ExcessTime - minPool->ExcessTime);
+ }
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+TItem TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::TryDequeue()
+{
+ auto pool = GetStarvingPool();
+ if (!pool) {
+ return {};
+ }
+
+ if (auto runningTask = pool->TryDequeue(); runningTask) {
+ auto result = std::move(runningTask->Task.Item);
+ auto cookie = NMpscFSQueue::ToCookie(result);
+ Executing_[cookie] = std::move(*runningTask);
+ return result;
+ }
+ return {};
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::MarkFinished(const TItem& item, TCpuDuration finishedTime)
+{
+ auto cookie = NMpscFSQueue::ToCookie(item);
+ auto it = Executing_.find(cookie);
+ YT_VERIFY(it != Executing_.end());
+ const auto& bucket = it->second.Bucket;
+
+ YT_VERIFY(--bucket->RunningTaskCount >= 0);
+ YT_VERIFY(--bucket->Pool->RunningTaskCount >= 0);
+
+ UpdateExcessTime(it->second, finishedTime);
+
+ Executing_.erase(it);
+
+ static constexpr int CleanupPeriod = 50'000;
+
+ if (++DequeCounter_ == CleanupPeriod) {
+ Cleanup();
+ DequeCounter_ = 0;
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+int TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::GetPoolCount()
+{
+ int count = 0;
+ for (const auto& pool : Pools_) {
+ if (pool) {
+ ++count;
+ }
+ }
+
+ return count;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::Cleanup()
+{
+ for (auto& pool : Pools_) {
+ if (!pool) {
+ continue;
+ }
+
+ std::vector<TFairShareTag> erasedTags;
+
+ for (const auto& [tag, bucket] : pool->TagToBucket) {
+ if (bucket->RunningTaskCount == 0 && bucket->Queue.empty()) {
+ YT_VERIFY(!bucket->HeapIterator);
+ erasedTags.push_back(tag);
+ }
+ }
+
+ for (const auto& tag : erasedTags) {
+ pool->TagToBucket.erase(tag);
+ }
+
+ if (pool->TagToBucket.empty()) {
+ YT_VERIFY(pool->RunningTaskCount == 0);
+ PoolIdToPoolIndex_.erase(pool->PoolId);
+ pool.Reset();
+ }
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+int TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::GetShardSize()
+{
+ return Prequeue_.GetShardSize();
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::MoveToFairQueue(std::vector<TEnqueuedTask>& tasks)
+{
+ for (auto& task : tasks) {
+ const auto& pool = GetOrAddPool(task);
+ auto bucket = pool->GetOrAddBucket(task, pool);
+ bucket->Queue.push(std::move(task));
+ }
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+int TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::GetEmptyPoolIndex()
+{
+ for (int index = 0; index < std::ssize(Pools_); ++index) {
+ if (!Pools_[index]) {
+ return index;
+ }
+ }
+
+ Pools_.emplace_back();
+ return std::ssize(Pools_) - 1;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+auto TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::GetOrAddPool(const TEnqueuedTask& task) -> const TExecutionPoolPtr&
+{
+ auto it = PoolIdToPoolIndex_.find(task.PoolId);
+ if (it == PoolIdToPoolIndex_.end()) {
+ auto index = GetEmptyPoolIndex();
+ Pools_[index] = New<TExecutionPool>(task.PoolId);
+ it = PoolIdToPoolIndex_.emplace(task.PoolId, index).first;
+ }
+
+ const auto& pool = Pools_[it->second];
+ pool->Weight = task.PoolWeight;
+ return pool;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+auto TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::GetStarvingPool() -> TExecutionPoolPtr
+{
+ TExecutionPoolPtr result;
+
+ for (const auto& pool : Pools_) {
+ if (!pool || pool->Heap.empty()) {
+ continue;
+ }
+ if (!result || *pool < *result) {
+ result = pool;
+ }
+ }
+
+ return result;
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::UpdateExcessTime(TExecutingTask& executing, TCpuDuration now)
+{
+ auto delta = now - executing.LastAccounted;
+
+ executing.Bucket->ExcessTime += delta;
+ const auto& pool = executing.Bucket->Pool;
+ pool->ExcessTime += delta / pool->Weight;
+
+ executing.LastAccounted = now;
+
+ auto heapIterator = executing.Bucket->HeapIterator;
+ if (!heapIterator) {
+ return;
+ }
+
+ auto heapIndex = heapIterator - pool->Heap.data();
+ YT_VERIFY(heapIndex < std::ssize(pool->Heap));
+ SiftDown(pool->Heap.begin(), pool->Heap.end(), pool->Heap.begin() + heapIndex, std::less<>());
+}
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+void TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>::AccountCurrentlyExecutingBuckets()
+{
+ auto now = GetCpuInstant();
+
+ for (auto& [_, executing] : Executing_) {
+ UpdateExcessTime(executing, now);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/mpsc_fair_share_queue.h b/yt/yt/core/misc/mpsc_fair_share_queue.h
new file mode 100644
index 0000000000..8aca4e82ba
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_fair_share_queue.h
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "mpsc_sharded_queue.h"
+
+#include <yt/yt/core/threading/thread.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/misc/heap.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NMpscFSQueue {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct TEnqueuedTaskGeneric
+{
+ TItem Item;
+
+ TPoolId PoolId = {};
+ double PoolWeight = 1;
+ TFairShareTag FairShareTag = {};
+};
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct THeapItemGeneric;
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct TExecutionPoolGeneric;
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct TBucketGeneric final
+{
+ using TExecutorPoolPtr = TIntrusivePtr<TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>>;
+ using TEnqueuedTask = TEnqueuedTaskGeneric<TPoolId, TItem, TFairShareTag>;
+ using THeapItem = THeapItemGeneric<TPoolId, TItem, TFairShareTag>;
+
+ const TExecutorPoolPtr Pool;
+ const TFairShareTag Tag;
+
+ TRingQueue<TEnqueuedTask> Queue;
+ THeapItem* HeapIterator = nullptr;
+ TCpuDuration ExcessTime = 0;
+ int RunningTaskCount = 0;
+
+ TBucketGeneric(TExecutorPoolPtr pool, TFairShareTag tag);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct THeapItemGeneric
+{
+ using TBucketPtr = TIntrusivePtr<TBucketGeneric<TPoolId, TItem, TFairShareTag>>;
+
+ TBucketPtr Bucket;
+
+ THeapItemGeneric(const THeapItemGeneric&) = delete;
+ THeapItemGeneric& operator=(const THeapItemGeneric&) = delete;
+
+ explicit THeapItemGeneric(TBucketPtr bucket);
+
+ THeapItemGeneric(THeapItemGeneric&& other) noexcept;
+
+ THeapItemGeneric& operator=(THeapItemGeneric&& other) noexcept;
+
+ void AdjustBackReference(THeapItemGeneric* iterator);
+
+ ~THeapItemGeneric();
+
+ bool operator<(const THeapItemGeneric& rhs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+struct TExecutionPoolGeneric final
+{
+ using TBucket = TBucketGeneric<TPoolId, TItem, TFairShareTag>;
+ using TBucketPtr = TIntrusivePtr<TBucket>;
+
+ using TEnqueuedTask = TEnqueuedTaskGeneric<TPoolId, TItem, TFairShareTag>;
+ using THeapItem = THeapItemGeneric<TPoolId, TItem, TFairShareTag>;
+
+ struct TExecutingTask
+ {
+ TEnqueuedTask Task;
+ TBucketPtr Bucket;
+ TCpuDuration LastAccounted = 0;
+ };
+
+ const TPoolId PoolId;
+ double Weight = 1.0;
+
+ TCpuDuration ExcessTime = 0;
+ int RunningTaskCount = 0;
+
+ std::vector<THeapItem> Heap;
+ THashMap<TFairShareTag, TBucketPtr> TagToBucket;
+
+
+ explicit TExecutionPoolGeneric(TPoolId poolId);
+
+ std::optional<TExecutingTask> TryDequeue();
+
+ TBucketPtr GetOrAddBucket(const TEnqueuedTask& scheduled, const TIntrusivePtr<TExecutionPoolGeneric>& pool);
+
+ bool operator<(const TExecutionPoolGeneric& pool) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NMpscFSQueue
+
+template <typename TPoolId, typename TItem, typename TFairShareTag>
+class TMpscFairShareQueue
+{
+public:
+ using TEnqueuedTask = NMpscFSQueue::TEnqueuedTaskGeneric<TPoolId, TItem, TFairShareTag>;
+ using TExecutionPool = NMpscFSQueue::TExecutionPoolGeneric<TPoolId, TItem, TFairShareTag>;
+ using TExecutingTask = typename TExecutionPool::TExecutingTask;
+
+ using TBucket = NMpscFSQueue::TBucketGeneric<TPoolId, TItem, TFairShareTag>;
+ using TBucketPtr = TIntrusivePtr<TBucket>;
+ using TExecutionPoolPtr = TIntrusivePtr<TExecutionPool>;
+
+ ~TMpscFairShareQueue();
+
+ void Enqueue(TEnqueuedTask task);
+
+ void EnqueueMany(std::vector<TEnqueuedTask>&& tasks);
+
+ void PrepareDequeue();
+
+ TItem TryDequeue();
+
+ void MarkFinished(const TItem& item, TCpuDuration finishedTime);
+
+ int GetPoolCount();
+
+ void Cleanup();
+
+ int GetShardSize();
+
+private:
+ using TCookie = uintptr_t;
+
+ TMpscShardedQueue<TEnqueuedTask> Prequeue_;
+
+ THashMap<TPoolId, int> PoolIdToPoolIndex_;
+ std::vector<TExecutionPoolPtr> Pools_;
+
+ THashMap<TCookie, TExecutingTask> Executing_;
+ int DequeCounter_ = 0;
+
+
+ void MoveToFairQueue(std::vector<TEnqueuedTask>& tasks);
+ int GetEmptyPoolIndex();
+ const TExecutionPoolPtr& GetOrAddPool(const TEnqueuedTask& task);
+
+ TExecutionPoolPtr GetStarvingPool();
+
+ void UpdateExcessTime(TExecutingTask& executing, TCpuDuration now);
+
+ void AccountCurrentlyExecutingBuckets();
+ void TruncatePoolExcessTime();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define MPSC_FAIR_SHARE_QUEUE_INL_H_
+#include "mpsc_fair_share_queue-inl.h"
+#undef MPSC_FAIR_SHARE_QUEUE_INL_H_
diff --git a/yt/yt/core/misc/mpsc_queue-inl.h b/yt/yt/core/misc/mpsc_queue-inl.h
new file mode 100644
index 0000000000..3a10e825b3
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_queue-inl.h
@@ -0,0 +1,116 @@
+#ifndef MPSC_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include mpsc_queue.h"
+// For the sake of sane code completion.
+#include "mpsc_queue.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TMpscQueue<T>::TNode
+{
+ T Value;
+ TNode* Next = nullptr;
+
+ explicit TNode(const T& value)
+ : Value(value)
+ { }
+
+ explicit TNode(T&& value)
+ : Value(std::move(value))
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TMpscQueue<T>::~TMpscQueue()
+{
+ DeleteNodeList(Head_.load());
+ DeleteNodeList(Tail_);
+}
+
+template <class T>
+void TMpscQueue<T>::Enqueue(const T& value)
+{
+ DoEnqueue(new TNode(value));
+}
+
+template <class T>
+void TMpscQueue<T>::Enqueue(T&& value)
+{
+ DoEnqueue(new TNode(std::move(value)));
+}
+
+template <class T>
+void TMpscQueue<T>::DoEnqueue(TNode* node)
+{
+ auto* expectedHead = Head_.load(std::memory_order::relaxed);
+ do {
+ node->Next = expectedHead;
+ } while (!Head_.compare_exchange_weak(expectedHead, node));
+}
+
+template <class T>
+bool TMpscQueue<T>::TryDequeue(T* value)
+{
+ if (!Tail_) {
+ Tail_ = Head_.exchange(nullptr);
+ if (auto* current = Tail_) {
+ auto* next = current->Next;
+ current->Next = nullptr;
+ while (next) {
+ auto* second = next->Next;
+ next->Next = current;
+ current = next;
+ next = second;
+ }
+ Tail_ = current;
+ }
+ }
+
+ if (!Tail_) {
+ return false;
+ }
+
+ *value = std::move(Tail_->Value);
+ delete std::exchange(Tail_, Tail_->Next);
+
+ return true;
+}
+
+template <class T>
+bool TMpscQueue<T>::IsEmpty() const
+{
+ return !Tail_ && !Head_.load();
+}
+
+template <class T>
+void TMpscQueue<T>::DrainConsumer()
+{
+ DeleteNodeList(std::exchange(Tail_, nullptr));
+ DrainProducer();
+}
+
+template <class T>
+void TMpscQueue<T>::DrainProducer()
+{
+ while (auto* head = Head_.exchange(nullptr)) {
+ DeleteNodeList(head);
+ }
+}
+
+template <class T>
+void TMpscQueue<T>::DeleteNodeList(TNode* node)
+{
+ auto* current = node;
+ while (current) {
+ delete std::exchange(current, current->Next);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/mpsc_queue.h b/yt/yt/core/misc/mpsc_queue.h
new file mode 100644
index 0000000000..a067495516
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_queue.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "public.h"
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Multiple producer single consumer lock-free queue.
+/*!
+ * Internally implemented by a pair of lock-free stack (head) and
+ * linked-list of popped-but-not-yet-dequeued items (tail).
+ *
+ * Additionally supports draining during shutdown.
+ *
+ * The proper shutdown sequence is as follows:
+ * 1) #DrainConsumer must be called by the consumer thread.
+ * 2) #DrainProducer must be called by each producer thread
+ * that has just enqueued some items into the queue.
+ */
+template <class T>
+class TMpscQueue final
+{
+public:
+ TMpscQueue(const TMpscQueue&) = delete;
+ void operator=(const TMpscQueue&) = delete;
+
+ TMpscQueue() = default;
+ ~TMpscQueue();
+
+ void Enqueue(const T& value);
+ void Enqueue(T&& value);
+
+ bool TryDequeue(T* value);
+
+ bool IsEmpty() const;
+
+ void DrainConsumer();
+ void DrainProducer();
+
+private:
+ struct TNode;
+
+ std::atomic<TNode*> Head_ = nullptr;
+ TNode* Tail_ = nullptr;
+
+ void DoEnqueue(TNode* node);
+ void DeleteNodeList(TNode* node);
+};
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define MPSC_QUEUE_INL_H_
+#include "mpsc_queue-inl.h"
+#undef MPSC_QUEUE_INL_H_
diff --git a/yt/yt/core/misc/mpsc_sharded_queue-inl.h b/yt/yt/core/misc/mpsc_sharded_queue-inl.h
new file mode 100644
index 0000000000..c876982f20
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_sharded_queue-inl.h
@@ -0,0 +1,92 @@
+#ifndef MPSC_SHARDED_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include mpsc_sharded_queue.h"
+// For the sake of sane code completion.
+#include "mpsc_sharded_queue.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TItem>
+void TSimpleMpscSpinLockQueue<TItem>::Enqueue(TItem item)
+{
+ auto guard = Guard(QueueLock_);
+ Queue_.push_back(std::move(item));
+}
+
+template <typename TItem>
+void TSimpleMpscSpinLockQueue<TItem>::EnqueueMany(std::vector<TItem>&& items)
+{
+ auto guard = Guard(QueueLock_);
+ Queue_.insert(
+ Queue_.end(),
+ std::make_move_iterator(items.begin()),
+ std::make_move_iterator(items.end()));
+}
+
+template <typename TItem>
+std::vector<TItem>& TSimpleMpscSpinLockQueue<TItem>::DequeueAll()
+{
+ Dequeued_.clear();
+
+ {
+ auto guard = Guard(QueueLock_);
+ Queue_.swap(Dequeued_);
+ }
+
+ return Dequeued_;
+}
+
+template <typename TItem>
+int TSimpleMpscSpinLockQueue<TItem>::GetSize()
+{
+ auto guard = Guard(QueueLock_);
+ return std::ssize(Queue_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TItem>
+void TMpscShardedQueue<TItem>::Enqueue(TItem item)
+{
+ auto tscp = NProfiling::TTscp::Get();
+ auto& shardQueue = Shards_[tscp.ProcessorId].Queue;
+ shardQueue.Enqueue(std::move(item));
+}
+
+template <typename TItem>
+void TMpscShardedQueue<TItem>::EnqueueMany(std::vector<TItem>&& items)
+{
+ auto tscp = NProfiling::TTscp::Get();
+ auto& shardQueue = Shards_[tscp.ProcessorId].Queue;
+ shardQueue.EnqueueMany(std::move(items));
+}
+
+template <typename TItem>
+template <typename TConsumer>
+i64 TMpscShardedQueue<TItem>::ConsumeAll(TConsumer consumer)
+{
+ i64 consumedCount = 0;
+
+ for (auto& shard : Shards_) {
+ auto& batch = shard.Queue.DequeueAll();
+
+ consumedCount += std::ssize(batch);
+ consumer(batch);
+ }
+
+ return consumedCount;
+}
+
+template <typename TItem>
+int TMpscShardedQueue<TItem>::GetShardSize()
+{
+ auto tscp = NProfiling::TTscp::Get();
+ auto& shardQueue = Shards_[tscp.ProcessorId].Queue;
+ return shardQueue.GetSize();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/mpsc_sharded_queue.h b/yt/yt/core/misc/mpsc_sharded_queue.h
new file mode 100644
index 0000000000..81c16b8f2b
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_sharded_queue.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <yt/yt/core/profiling/tscp.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <vector>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TItem>
+class TSimpleMpscSpinLockQueue
+{
+public:
+ void Enqueue(TItem item);
+ void EnqueueMany(std::vector<TItem>&& items);
+ std::vector<TItem>& DequeueAll();
+
+ int GetSize();
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, QueueLock_);
+ std::vector<TItem> Queue_;
+
+ std::vector<TItem> Dequeued_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TItem>
+class TMpscShardedQueue
+{
+public:
+ void Enqueue(TItem item);
+ void EnqueueMany(std::vector<TItem>&& items);
+
+ // TConsumer is a functor with a signature: void (vector<TItem>& batch)
+ template <typename TConsumer>
+ i64 ConsumeAll(TConsumer consumer);
+
+ int GetShardSize();
+
+private:
+ struct alignas(2 * CacheLineSize) TShard
+ {
+ TSimpleMpscSpinLockQueue<TItem> Queue;
+ };
+
+ std::array<TShard, NProfiling::TTscp::MaxProcessorId> Shards_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define MPSC_SHARDED_QUEUE_INL_H_
+#include "mpsc_sharded_queue-inl.h"
+#undef MPSC_SHARDED_QUEUE_INL_H_
diff --git a/yt/yt/core/misc/mpsc_stack-inl.h b/yt/yt/core/misc/mpsc_stack-inl.h
new file mode 100644
index 0000000000..4f14d8ba5c
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_stack-inl.h
@@ -0,0 +1,120 @@
+#ifndef MPSC_STACK_INL_H_
+#error "Direct inclusion of this file is not allowed, include mpsc_stack.h"
+// For the sake of sane code completion.
+#include "mpsc_stack.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TMpscStack<T>::TNode
+{
+ T Value;
+ TNode* Next = nullptr;
+
+ explicit TNode(const T& value)
+ : Value(value)
+ { }
+
+ explicit TNode(T&& value)
+ : Value(std::move(value))
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TMpscStack<T>::~TMpscStack()
+{
+ auto* current = Head_.load();
+ while (current) {
+ auto* next = current->Next;
+ delete current;
+ current = next;
+ }
+}
+
+template <class T>
+void TMpscStack<T>::Enqueue(const T& value)
+{
+ DoEnqueue(new TNode(value));
+}
+
+template <class T>
+void TMpscStack<T>::Enqueue(T&& value)
+{
+ DoEnqueue(new TNode(std::move(value)));
+}
+
+template <class T>
+void TMpscStack<T>::DoEnqueue(TNode* node)
+{
+ auto* expected = Head_.load(std::memory_order::relaxed);
+ do {
+ node->Next = expected;
+ } while (!Head_.compare_exchange_weak(expected, node));
+}
+
+template <class T>
+bool TMpscStack<T>::TryDequeue(T* value)
+{
+ auto* expected = Head_.load();
+ do {
+ if (!expected) {
+ return false;
+ }
+ } while (!Head_.compare_exchange_weak(expected, expected->Next));
+
+ *value = std::move(expected->Value);
+ delete expected;
+ return true;
+}
+
+template <class T>
+std::vector<T> TMpscStack<T>::DequeueAll(bool reverse)
+{
+ std::vector<T> results;
+ DequeueAll(reverse, [&results] (T& value) {
+ results.push_back(std::move(value));
+ });
+ return results;
+}
+
+template <class T>
+template <class F>
+bool TMpscStack<T>::DequeueAll(bool reverse, F&& functor)
+{
+ auto* current = Head_.exchange(nullptr);
+ if (!current) {
+ return false;
+ }
+ if (reverse) {
+ auto* next = current->Next;
+ current->Next = nullptr;
+ while (next) {
+ auto* second = next->Next;
+ next->Next = current;
+ current = next;
+ next = second;
+ }
+ }
+ while (current) {
+ functor(current->Value);
+ auto* next = current->Next;
+ delete current;
+ current = next;
+ }
+ return true;
+}
+
+template <class T>
+bool TMpscStack<T>::IsEmpty() const
+{
+ return !Head_.load();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/mpsc_stack.h b/yt/yt/core/misc/mpsc_stack.h
new file mode 100644
index 0000000000..7907c5ba14
--- /dev/null
+++ b/yt/yt/core/misc/mpsc_stack.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "public.h"
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Multiple producer single consumer lock-free stack.
+template <class T>
+class TMpscStack
+{
+public:
+ TMpscStack(const TMpscStack&) = delete;
+ void operator=(const TMpscStack&) = delete;
+
+ TMpscStack() = default;
+ ~TMpscStack();
+
+ void Enqueue(const T& value);
+ void Enqueue(T&& value);
+
+ bool TryDequeue(T* value);
+ std::vector<T> DequeueAll(bool reverse = false);
+ template <class F>
+ bool DequeueAll(bool reverse, F&& functor);
+
+ bool IsEmpty() const;
+
+private:
+ struct TNode;
+
+ std::atomic<TNode*> Head_ = nullptr;
+
+ void DoEnqueue(TNode* node);
+};
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define MPSC_STACK_INL_H_
+#include "mpsc_stack-inl.h"
+#undef MPSC_STACK_INL_H_
diff --git a/yt/yt/core/misc/numeric_helpers-inl.h b/yt/yt/core/misc/numeric_helpers-inl.h
new file mode 100644
index 0000000000..055b8b9ec9
--- /dev/null
+++ b/yt/yt/core/misc/numeric_helpers-inl.h
@@ -0,0 +1,44 @@
+#ifndef NUMERIC_HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include numeric_helpers.h"
+// For the sake of sane code completion.
+#include "numeric_helpers.h"
+#endif
+
+#include <cstdlib>
+#include <cinttypes>
+#include <algorithm>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T DivCeil(const T& numerator, const T& denominator)
+{
+ YT_VERIFY(denominator != 0);
+ auto res = std::div(numerator, denominator);
+ return res.quot + (res.rem > static_cast<T>(0) ? static_cast<T>(1) : static_cast<T>(0));
+}
+
+template <typename T>
+T DivRound(const T& numerator, const T& denominator)
+{
+ auto res = std::div(numerator, denominator);
+ return res.quot + (res.rem >= (denominator + 1) / 2 ? static_cast<T>(1) : static_cast<T>(0));
+}
+
+template <class T>
+T RoundUp(const T& numerator, const T& denominator)
+{
+ return DivCeil(numerator, denominator) * denominator;
+}
+
+template <class T>
+T RoundDown(const T& numerator, const T& denominator)
+{
+ return (numerator / denominator) * denominator;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/numeric_helpers.h b/yt/yt/core/misc/numeric_helpers.h
new file mode 100644
index 0000000000..dec7b7688c
--- /dev/null
+++ b/yt/yt/core/misc/numeric_helpers.h
@@ -0,0 +1,28 @@
+#pragma once
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T DivCeil(const T& numerator, const T& denominator);
+
+//! A version of division that is a bit less noisy around the situation
+//! when numerator is almost divisible by denominator. Round up if the remainder
+//! is at least half of denominator, otherwise round down.
+template<class T>
+T DivRound(const T& numerator, const T& denominator);
+
+template <class T>
+T RoundUp(const T& numerator, const T& denominator);
+
+template <class T>
+T RoundDown(const T& numerator, const T& denominator);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define NUMERIC_HELPERS_INL_H_
+#include "numeric_helpers-inl.h"
+#undef NUMERIC_HELPERS_INL_H_
diff --git a/yt/yt/core/misc/object_pool-inl.h b/yt/yt/core/misc/object_pool-inl.h
new file mode 100644
index 0000000000..68b64c2a69
--- /dev/null
+++ b/yt/yt/core/misc/object_pool-inl.h
@@ -0,0 +1,111 @@
+#ifndef OBJECT_POOL_INL_H_
+#error "Direct inclusion of this file is not allowed, include object_pool.h"
+// For the sake of sane code completion.
+#include "object_pool.h"
+#endif
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class TTraits>
+TObjectPool<T, TTraits>::~TObjectPool()
+{
+ T* obj;
+ while (PooledObjects_.Dequeue(&obj)) {
+ FreeInstance(obj);
+ }
+}
+
+template <class T, class TTraits>
+auto TObjectPool<T, TTraits>::Allocate() -> TObjectPtr
+{
+ T* obj = nullptr;
+ if (PooledObjects_.Dequeue(&obj)) {
+ --PoolSize_;
+ }
+
+ if (!obj) {
+ obj = TTraits::Allocate();
+ }
+
+ return TObjectPtr(obj, [] (T* obj) {
+ ObjectPool<T, TTraits>().Reclaim(obj);
+ });
+}
+
+template <class T, class TTraits>
+void TObjectPool<T, TTraits>::Reclaim(T* obj)
+{
+ TTraits::Clean(obj);
+
+ while (true) {
+ auto poolSize = PoolSize_.load();
+ if (poolSize >= TTraits::GetMaxPoolSize()) {
+ FreeInstance(obj);
+ break;
+ } else if (PoolSize_.compare_exchange_strong(poolSize, poolSize + 1)) {
+ PooledObjects_.Enqueue(obj);
+ break;
+ }
+ }
+
+ if (PoolSize_ > TTraits::GetMaxPoolSize()) {
+ T* objToDestroy;
+ if (PooledObjects_.Dequeue(&objToDestroy)) {
+ --PoolSize_;
+ FreeInstance(objToDestroy);
+ }
+ }
+}
+
+template <class T, class TTraits>
+int TObjectPool<T, TTraits>::GetSize() const
+{
+ return PoolSize_;
+}
+
+template <class T, class TTraits>
+void TObjectPool<T, TTraits>::Release(int count)
+{
+ T* obj;
+ while (count > 0 && PooledObjects_.Dequeue(&obj)) {
+ --PoolSize_;
+ FreeInstance(obj);
+ --count;
+ }
+}
+
+template <class T, class TTraits>
+void TObjectPool<T, TTraits>::FreeInstance(T* obj)
+{
+ delete obj;
+}
+
+template <class T, class TTraits>
+TObjectPool<T, TTraits>& ObjectPool()
+{
+ return *Singleton<TObjectPool<T, TTraits>>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TPooledObjectTraits<
+ T,
+ typename std::enable_if_t<
+ std::is_convertible_v<T&, ::google::protobuf::MessageLite&>
+ >
+>
+ : public TPooledObjectTraitsBase<T>
+{
+ static void Clean(T* message)
+ {
+ message->Clear();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/object_pool.h b/yt/yt/core/misc/object_pool.h
new file mode 100644
index 0000000000..8dd997b9af
--- /dev/null
+++ b/yt/yt/core/misc/object_pool.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include "common.h"
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <util/generic/singleton.h>
+
+#include <util/thread/lfstack.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides various traits for pooled objects of type |T|.
+/*!
+ * |Clean| method is called before an object is put into the pool.
+ *
+ * |GetMaxPoolSize| method is called to determine the maximum number of
+ * objects allowed to be pooled.
+ */
+template <class TObject, class = void>
+struct TPooledObjectTraits
+{ };
+
+//! Basic version of traits. Others may consider inheriting from it.
+template <class TObject>
+struct TPooledObjectTraitsBase
+{
+ static TObject* Allocate()
+ {
+ return new TObject();
+ }
+
+ static void Clean(TObject*)
+ { }
+
+ static int GetMaxPoolSize()
+ {
+ return 256;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A pool for reusable objects.
+/*
+ * Instances are tracked via shared pointers with a special deleter
+ * that returns spare instances back to the pool.
+ *
+ * Both the pool and the references are thread-safe.
+ *
+ */
+
+template <class TObject, class TTraits = TPooledObjectTraits<TObject>>
+class TObjectPool
+{
+public:
+ using TObjectPtr = std::shared_ptr<TObject>;
+
+ ~TObjectPool();
+
+ //! Either creates a fresh instance or returns a pooled one.
+ TObjectPtr Allocate();
+
+ int GetSize() const;
+
+ void Release(int count);
+
+private:
+ TLockFreeStack<TObject*> PooledObjects_;
+ std::atomic<int> PoolSize_ = {0};
+
+ //! Calls #TPooledObjectTraits::Clean and returns the instance back into the pool.
+ void Reclaim(TObject* obj);
+
+ void FreeInstance(TObject* obj);
+
+ Y_DECLARE_SINGLETON_FRIEND()
+};
+
+template <class TObject, class TTraits = TPooledObjectTraits<TObject>>
+TObjectPool<TObject, TTraits>& ObjectPool();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define OBJECT_POOL_INL_H_
+#include "object_pool-inl.h"
+#undef OBJECT_POOL_INL_H_
diff --git a/yt/yt/core/misc/optional.h b/yt/yt/core/misc/optional.h
new file mode 100644
index 0000000000..cfae5c36d5
--- /dev/null
+++ b/yt/yt/core/misc/optional.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include <util/string/cast.h>
+
+#include <optional>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TOptionalTraits
+{
+ using TOptional = std::optional<T>;
+ using TValue = T;
+};
+
+template <class T>
+struct TOptionalTraits<std::optional<T>>
+{
+ using TOptional = std::optional<T>;
+ using TValue = T;
+};
+
+template <class T>
+struct TOptionalTraits<T*>
+{
+ using TOptional = T*;
+ using TValue = T*;
+};
+
+template <class T>
+struct TOptionalTraits<TIntrusivePtr<T>>
+{
+ using TOptional = TIntrusivePtr<T>;
+ using TValue = TIntrusivePtr<T>;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TStdOptionalTraits
+{
+ static constexpr bool IsStdOptional = false;
+ using TValueType = T;
+};
+
+template <class T>
+struct TStdOptionalTraits<std::optional<T>>
+{
+ static constexpr bool IsStdOptional = true;
+ using TValueType = T;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+template <class T>
+TString ToString(const std::optional<T>& nullable)
+{
+ return nullable ? ToString(*nullable) : "<Null>";
+}
+
+template <class T>
+struct THash<std::optional<T>>
+{
+ size_t operator()(const std::optional<T>& nullable) const
+ {
+ return nullable ? THash<T>()(*nullable) : 0;
+ }
+};
diff --git a/yt/yt/core/misc/parser_helpers.cpp b/yt/yt/core/misc/parser_helpers.cpp
new file mode 100644
index 0000000000..f218cc3eea
--- /dev/null
+++ b/yt/yt/core/misc/parser_helpers.cpp
@@ -0,0 +1,37 @@
+#include "parser_helpers.h"
+#include "common.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSpace(char ch)
+{
+ static const ui8 lookupTable[] =
+ {
+ 0,0,0,0,0,0,0,0, 0,1,1,1,1,1,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
+ };
+ return lookupTable[static_cast<ui8>(ch)];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/parser_helpers.h b/yt/yt/core/misc/parser_helpers.h
new file mode 100644
index 0000000000..3fd58ea95f
--- /dev/null
+++ b/yt/yt/core/misc/parser_helpers.h
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSpace(char ch);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/pattern_formatter.cpp b/yt/yt/core/misc/pattern_formatter.cpp
new file mode 100644
index 0000000000..666459e7ee
--- /dev/null
+++ b/yt/yt/core/misc/pattern_formatter.cpp
@@ -0,0 +1,54 @@
+#include "pattern_formatter.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/stream/str.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const char Dollar = '$';
+static const char LeftParen = '(';
+static const char RightParen = ')';
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPatternFormatter::AddProperty(const TString& name, const TString& value)
+{
+ PropertyMap[name] = value;
+}
+
+TString TPatternFormatter::Format(const TString& pattern)
+{
+ TString result;
+
+ for (size_t pos = 0; pos < pattern.size(); ++pos) {
+ if (pattern[pos] == Dollar && (pos + 1 < pattern.size() && pattern[pos + 1] == LeftParen)) {
+ auto left = pos + 2;
+ auto right = left;
+ while (right < pattern.size() && pattern[right] != RightParen) {
+ right += 1;
+ }
+
+ if (right < pattern.size()) {
+ auto property = pattern.substr(left, right - left);
+
+ auto it = PropertyMap.find(property);
+ if (it != PropertyMap.end()) {
+ result.append(it->second);
+ pos = right;
+ continue;
+ }
+ }
+ }
+
+ result.append(pattern[pos]);
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/pattern_formatter.h b/yt/yt/core/misc/pattern_formatter.h
new file mode 100644
index 0000000000..2fd4475387
--- /dev/null
+++ b/yt/yt/core/misc/pattern_formatter.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "common.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPatternFormatter
+ : private TNonCopyable
+{
+public:
+ void AddProperty(const TString& name, const TString& value);
+ TString Format(const TString& pattern);
+
+private:
+ using TPropertyMap = THashMap<TString, TString>;
+ TPropertyMap PropertyMap;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/persistent_queue-inl.h b/yt/yt/core/misc/persistent_queue-inl.h
new file mode 100644
index 0000000000..5f277db695
--- /dev/null
+++ b/yt/yt/core/misc/persistent_queue-inl.h
@@ -0,0 +1,194 @@
+#ifndef PERSISTENT_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include persistent_queue.h"
+// For the sake of sane code completion.
+#include "persistent_queue.h"
+#endif
+
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, size_t ChunkSize>
+TPersistentQueueIterator<T, ChunkSize>::TPersistentQueueIterator()
+{ }
+
+template <class T, size_t ChunkSize>
+TPersistentQueueIterator<T, ChunkSize>& TPersistentQueueIterator<T, ChunkSize>::operator++()
+{
+ YT_ASSERT(CurrentChunk_);
+ YT_ASSERT(CurrentIndex_ >= 0 && CurrentIndex_ < ChunkSize);
+
+ ++CurrentIndex_;
+ if (CurrentIndex_ == ChunkSize) {
+ CurrentChunk_ = CurrentChunk_->Next;
+ CurrentIndex_ = 0;
+ }
+
+ return *this;
+}
+
+template <class T, size_t ChunkSize>
+TPersistentQueueIterator<T, ChunkSize> TPersistentQueueIterator<T, ChunkSize>::operator++(int)
+{
+ auto result = *this;
+ ++(*this);
+ return result;
+}
+
+template <class T, size_t ChunkSize>
+const T& TPersistentQueueIterator<T, ChunkSize>::operator*() const
+{
+ return CurrentChunk_->Elements[CurrentIndex_];
+}
+
+template <class T, size_t ChunkSize>
+bool TPersistentQueueIterator<T, ChunkSize>::operator==(const TPersistentQueueIterator& other) const
+{
+ return CurrentChunk_ == other.CurrentChunk_ && CurrentIndex_ == other.CurrentIndex_;
+}
+
+template <class T, size_t ChunkSize>
+bool TPersistentQueueIterator<T, ChunkSize>::operator!=(const TPersistentQueueIterator& other) const
+{
+ return !(*this == other);
+}
+
+template <class T, size_t ChunkSize>
+TPersistentQueueIterator<T, ChunkSize>::TPersistentQueueIterator(
+ TChunkPtr chunk,
+ size_t index)
+ : CurrentChunk_(std::move(chunk))
+ , CurrentIndex_(index)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, size_t ChunkSize>
+size_t TPersistentQueueBase<T, ChunkSize>::Size() const
+{
+ return Size_;
+}
+
+template <class T, size_t ChunkSize>
+bool TPersistentQueueBase<T, ChunkSize>::Empty() const
+{
+ return Size_ == 0;
+}
+
+template <class T, size_t ChunkSize>
+auto TPersistentQueueBase<T, ChunkSize>::Begin() const -> TIterator
+{
+ return Tail_;
+}
+
+template <class T, size_t ChunkSize>
+auto TPersistentQueueBase<T, ChunkSize>::End() const -> TIterator
+{
+ return Head_;
+}
+
+template <class T, size_t ChunkSize>
+auto TPersistentQueueBase<T, ChunkSize>::begin() const -> TIterator
+{
+ return Begin();
+}
+
+template <class T, size_t ChunkSize>
+auto TPersistentQueueBase<T, ChunkSize>::end() const -> TIterator
+{
+ return End();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, size_t ChunkSize>
+template <class C>
+void TPersistentQueueSnapshot<T, ChunkSize>::Save(C& context) const
+{
+ using NYT::Save;
+ TSizeSerializer::Save(context, this->Size());
+ for (const auto& value : *this) {
+ Save(context, value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, size_t ChunkSize>
+void TPersistentQueue<T, ChunkSize>::Enqueue(T value)
+{
+ auto& head = this->Head_;
+ auto& tail = this->Tail_;
+ auto& size = this->Size_;
+
+ if (!head.CurrentChunk_) {
+ auto chunk = New<TChunk>();
+ head.CurrentChunk_ = tail.CurrentChunk_ = chunk;
+ head.CurrentIndex_ = tail.CurrentIndex_ = 0;
+ }
+
+ head.CurrentChunk_->Elements[head.CurrentIndex_++] = std::move(value);
+ ++size;
+
+ if (head.CurrentIndex_ == ChunkSize) {
+ auto chunk = New<TChunk>();
+ head.CurrentChunk_->Next = chunk;
+ head.CurrentChunk_ = chunk;
+ head.CurrentIndex_ = 0;
+ }
+}
+
+template <class T, size_t ChunkSize>
+T TPersistentQueue<T, ChunkSize>::Dequeue()
+{
+ auto& tail = this->Tail_;
+ auto& size = this->Size_;
+
+ YT_ASSERT(size != 0);
+
+ auto result = std::move(tail.CurrentChunk_->Elements[tail.CurrentIndex_++]);
+ --size;
+
+ if (tail.CurrentIndex_ == ChunkSize) {
+ tail.CurrentChunk_ = tail.CurrentChunk_->Next;
+ tail.CurrentIndex_ = 0;
+ }
+
+ return result;
+}
+
+template <class T, size_t ChunkSize>
+void TPersistentQueue<T, ChunkSize>::Clear()
+{
+ this->Head_ = TPersistentQueueIterator<T, ChunkSize>();
+ this->Tail_ = TPersistentQueueIterator<T, ChunkSize>();
+ this->Size_ = 0;
+}
+
+template <class T, size_t ChunkSize>
+auto TPersistentQueue<T, ChunkSize>::MakeSnapshot() const -> TSnapshot
+{
+ TSnapshot snapshot;
+ snapshot.Head_ = this->Head_;
+ snapshot.Tail_ = this->Tail_;
+ snapshot.Size_ = this->Size_;
+ return snapshot;
+}
+
+template <class T, size_t ChunkSize>
+template <class C>
+void TPersistentQueue<T, ChunkSize>::Load(C& context)
+{
+ using NYT::Load;
+ YT_VERIFY(this->Empty());
+ auto size = TSizeSerializer::Load(context);
+ for (size_t index = 0; index < size; ++index) {
+ Enqueue(Load<T>(context));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/persistent_queue.h b/yt/yt/core/misc/persistent_queue.h
new file mode 100644
index 0000000000..32329cecf0
--- /dev/null
+++ b/yt/yt/core/misc/persistent_queue.h
@@ -0,0 +1,135 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+// Forward declarations.
+
+template <class T, size_t ChunkSize>
+struct TPersistentQueueChunk;
+
+template <class T, size_t ChunkSize>
+class TPersistentQueueBase;
+
+template <class T, size_t ChunkSize>
+class TPersistentQueueIterator;
+
+template <class T, size_t ChunkSize>
+class TPersistentQueue;
+
+////////////////////////////////////////////////////////////////////////////////
+// Implementation.
+
+template <class T, size_t ChunkSize>
+struct TPersistentQueueChunk
+ : public TRefCounted
+{
+ TIntrusivePtr<TPersistentQueueChunk<T, ChunkSize>> Next;
+ T Elements[ChunkSize];
+};
+
+template <class T, size_t ChunkSize>
+class TPersistentQueueIterator
+{
+public:
+ TPersistentQueueIterator();
+
+ TPersistentQueueIterator& operator++(); // prefix
+ TPersistentQueueIterator operator++(int); // postfix
+
+ const T& operator * () const;
+
+ bool operator == (const TPersistentQueueIterator& other) const;
+ bool operator != (const TPersistentQueueIterator& other) const;
+
+private:
+ using TChunk = TPersistentQueueChunk<T, ChunkSize>;
+ using TChunkPtr = TIntrusivePtr<TChunk>;
+
+ friend class TPersistentQueueBase<T, ChunkSize>;
+ friend class TPersistentQueue<T, ChunkSize>;
+
+ TPersistentQueueIterator(TChunkPtr chunk, size_t index);
+
+ TChunkPtr CurrentChunk_;
+ size_t CurrentIndex_ = 0;
+
+};
+
+template <class T, size_t ChunkSize>
+class TPersistentQueueBase
+{
+public:
+ using TIterator = TPersistentQueueIterator<T, ChunkSize>;
+
+ size_t Size() const;
+ bool Empty() const;
+
+ TIterator Begin() const;
+ TIterator End() const;
+
+ // STL interop.
+ TIterator begin() const;
+ TIterator end() const;
+
+protected:
+ using TChunk = TPersistentQueueChunk<T, ChunkSize>;
+ using TChunkPtr = TIntrusivePtr<TChunk>;
+
+ friend class TPersistentQueue<T, ChunkSize>;
+
+ size_t Size_ = 0;
+ TIterator Head_;
+ TIterator Tail_;
+
+};
+
+template <class T, size_t ChunkSize>
+class TPersistentQueueSnapshot
+ : public TPersistentQueueBase<T, ChunkSize>
+{
+public:
+ template <class C>
+ void Save(C& context) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Interface.
+
+//! A partially persistent queue.
+/*!
+ * Implemented as a linked-list of chunks each carrying #ChunkSize elements.
+ *
+ * Can be modified from a single thread.
+ * Snapshots can be read from arbitrary threads.
+ */
+template <class T, size_t ChunkSize>
+class TPersistentQueue
+ : public TPersistentQueueBase<T, ChunkSize>
+{
+public:
+ void Enqueue(T value);
+ T Dequeue();
+ void Clear();
+
+ using TSnapshot = TPersistentQueueSnapshot<T, ChunkSize>;
+ TSnapshot MakeSnapshot() const;
+
+ template <class C>
+ void Load(C& context);
+
+private:
+ using TChunk = TPersistentQueueChunk<T, ChunkSize>;
+ using TChunkPtr = TIntrusivePtr<TChunk>;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define PERSISTENT_QUEUE_INL_H_
+#include "persistent_queue-inl.h"
+#undef PERSISTENT_QUEUE_INL_H_
diff --git a/yt/yt/core/misc/phoenix-inl.h b/yt/yt/core/misc/phoenix-inl.h
new file mode 100644
index 0000000000..aae4214aab
--- /dev/null
+++ b/yt/yt/core/misc/phoenix-inl.h
@@ -0,0 +1,297 @@
+#ifndef PHOENIX_INL_H_
+#error "Direct inclusion of this file is not allowed, include phoenix.h"
+// For the sake of sane code completion.
+#include "phoenix.h"
+#endif
+
+#include <type_traits>
+
+namespace NYT::NPhoenix {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void* TProfiler::DoInstantiate()
+{
+ using TFactory = typename TFactoryTraits<T>::TFactory;
+ using TBase = typename TPolymorphicTraits<T>::TBase;
+
+ T* ptr = TFactory::template Instantiate<T>();
+ TBase* basePtr = static_cast<TBase*>(ptr);
+ return basePtr;
+}
+
+template <class T>
+void TProfiler::Register(ui32 tag)
+{
+ using TIdClass = typename TIdClass<T>::TType;
+
+ auto pair = TagToEntry_.emplace(tag, TEntry());
+ YT_VERIFY(pair.second);
+ auto& entry = pair.first->second;
+ entry.Tag = tag;
+ entry.TypeInfo = &typeid(TIdClass);
+ entry.Factory = std::bind(&DoInstantiate<T>);
+ YT_VERIFY(TypeInfoToEntry_.emplace(entry.TypeInfo, &entry).second);
+}
+
+template <class T>
+T* TProfiler::Instantiate(ui32 tag)
+{
+ using TBase = typename TPolymorphicTraits<T>::TBase;
+ TBase* basePtr = static_cast<TBase*>(GetEntry(tag).Factory());
+ return dynamic_cast<T*>(basePtr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class>
+struct TInstantiatedRegistrar
+{
+ static void Do(TLoadContext& /*context*/, T* /*rawPtr*/)
+ { }
+};
+
+template <class T>
+struct TInstantiatedRegistrar<
+ T,
+ std::enable_if_t<
+ std::is_convertible_v<T&, TRefCounted&>
+ >
+>
+{
+ static void Do(TLoadContext& context, T* rawPtr)
+ {
+ context.Deletors_.push_back([=] { Unref(rawPtr); });
+ }
+};
+
+template <class T>
+void TLoadContext::RegisterInstantiatedObject(T* rawPtr)
+{
+ TInstantiatedRegistrar<T>::Do(*this, rawPtr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const TIntrusivePtr<T>& ptr)
+ {
+ SaveImpl(context, ptr.Get());
+ }
+
+ template <class T, class C>
+ static void Save(C& context, const std::unique_ptr<T>& ptr)
+ {
+ SaveImpl(context, ptr.get());
+ }
+
+ template <class T, class C>
+ static void Save(C& context, T* ptr)
+ {
+ SaveImpl(context, ptr);
+ }
+
+ template <class T, class C>
+ static void SaveImpl(C& context, T* ptr)
+ {
+ using TBase = typename TPolymorphicTraits<T>::TBase;
+ using NYT::Save;
+
+ if (ptr) {
+ TBase* basePtr = ptr;
+ bool dynamic = TPolymorphicTraits<T>::Dynamic;
+ const auto* typeInfo = dynamic ? &typeid(*ptr) : nullptr;
+ bool saveBody = false;
+ ui32 id = context.FindId(basePtr, typeInfo);
+ if (!id) {
+ id = context.GenerateId(basePtr, typeInfo);
+ saveBody = true;
+ }
+
+ Save(context, saveBody ? (id | InlineObjectIdMask) : id);
+
+ if (saveBody) {
+ if (dynamic) {
+ ui32 tag = TProfiler::Get()->GetTag(*typeInfo);
+ Save(context, tag);
+ }
+ Save(context, *ptr);
+ }
+ } else {
+ Save(context, NullObjectId);
+ }
+ }
+
+
+ template <class T, class C>
+ static void Load(C& context, TIntrusivePtr<T>& ptr)
+ {
+ T* rawPtr = nullptr;
+ LoadImpl(context, rawPtr, false);
+ ptr.Reset(rawPtr);
+ }
+
+ template <class T, class C>
+ static void InplaceLoad(C& context, const TIntrusivePtr<T>& ptr)
+ {
+ T* rawPtr = ptr.Get();
+ LoadImpl(context, rawPtr, true);
+ }
+
+ template <class T, class C>
+ static void Load(C& context, std::unique_ptr<T>& ptr)
+ {
+ T* rawPtr = nullptr;
+ LoadImpl(context, rawPtr, false);
+ ptr.reset(rawPtr);
+ }
+
+ template <class T, class C>
+ static void InplaceLoad(C& context, const std::unique_ptr<T>& ptr)
+ {
+ T* rawPtr = ptr.get();
+ LoadImpl(context, rawPtr, true);
+ }
+
+ template <class T, class C>
+ static void Load(C& context, T*& rawPtr)
+ {
+ rawPtr = nullptr;
+ LoadImpl(context, rawPtr, false);
+ }
+
+ template <class T, class C>
+ static void InplaceLoad(C& context, T* rawPtr)
+ {
+ LoadImpl(context, rawPtr, true);
+ }
+
+ template <class T, class C>
+ static void LoadImpl(C& context, T*& rawPtr, bool inplace)
+ {
+ using TBase = typename TPolymorphicTraits<T>::TBase;
+ using NYT::Load;
+
+ ui32 id = Load<ui32>(context);
+ if (id & InlineObjectIdMask) {
+ if (inplace) {
+ YT_VERIFY(rawPtr);
+ TInstantiator<T, C, TPolymorphicTraits<T>::Dynamic>::ValidateTag(context, rawPtr);
+ } else {
+ rawPtr = TInstantiator<T, C, TPolymorphicTraits<T>::Dynamic>::Instantiate(context);
+ context.RegisterInstantiatedObject(rawPtr);
+ }
+
+ TBase* basePtr = rawPtr;
+ context.RegisterObject(id & ~InlineObjectIdMask, basePtr);
+
+ Load(context, *rawPtr);
+ } else {
+ if (id) {
+ auto* basePtr = static_cast<TBase*>(context.GetObject(id));
+ rawPtr = dynamic_cast<T*>(basePtr);
+ } else {
+ rawPtr = nullptr;
+ }
+ }
+ }
+
+
+ template <class T, class C, bool dynamic>
+ struct TInstantiator
+ { };
+
+ template <class T, class C>
+ struct TInstantiator<T, C, true>
+ {
+ static T* Instantiate(C& context)
+ {
+ using NYT::Load;
+ ui32 tag = Load<ui32>(context);
+ return TProfiler::Get()->Instantiate<T>(tag);
+ }
+
+ static void ValidateTag(C& context, T* rawPtr)
+ {
+ using NYT::Load;
+ ui32 streamTag = Load<ui32>(context);
+ ui32 runtimeTag = TProfiler::Get()->GetTag(typeid (*rawPtr));
+ YT_VERIFY(streamTag == runtimeTag);
+ }
+ };
+
+ template <class T, class C>
+ struct TInstantiator<T, C, false>
+ {
+ static T* Instantiate(const C& /*context*/)
+ {
+ using TFactory = typename TFactoryTraits<T>::TFactory;
+ return TFactory::template Instantiate<T>();
+ }
+
+ static void ValidateTag(const C& /*context*/, T* /*rawPtr*/)
+ { }
+ };
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NPhoenix
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ std::enable_if_t<
+ std::conjunction_v<
+ std::is_convertible<T&, TIntrusivePtr<typename T::TUnderlying>&>,
+ std::is_convertible<C&, NPhoenix::TContextBase&>
+ >
+ >
+>
+{
+ using TSerializer = NPhoenix::TSerializer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ std::enable_if_t<
+ std::conjunction_v<
+ std::is_convertible<T&, std::unique_ptr<typename T::element_type>&>,
+ std::is_convertible<C&, NPhoenix::TContextBase&>
+ >
+ >
+>
+{
+ using TSerializer = NPhoenix::TSerializer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ std::enable_if_t<
+ std::conjunction_v<
+ std::is_pointer<T>,
+ std::is_convertible<C&, NPhoenix::TContextBase&>
+ >
+ >
+>
+{
+ using TSerializer = NPhoenix::TSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/phoenix.cpp b/yt/yt/core/misc/phoenix.cpp
new file mode 100644
index 0000000000..a3b7a7ef06
--- /dev/null
+++ b/yt/yt/core/misc/phoenix.cpp
@@ -0,0 +1,85 @@
+#include "phoenix.h"
+
+#include "collection_helpers.h"
+
+namespace NYT::NPhoenix {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProfiler::TProfiler()
+{ }
+
+TProfiler* TProfiler::Get()
+{
+ return Singleton<TProfiler>();
+}
+
+ui32 TProfiler::GetTag(const std::type_info& typeInfo)
+{
+ return GetEntry(typeInfo).Tag;
+}
+
+const TProfiler::TEntry& TProfiler::GetEntry(ui32 tag)
+{
+ return GetOrCrash(TagToEntry_, tag);
+}
+
+const TProfiler::TEntry& TProfiler::GetEntry(const std::type_info& typeInfo)
+{
+ return *GetOrCrash(TypeInfoToEntry_, &typeInfo);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSaveContext::TSaveContext(IZeroCopyOutput* output, int version)
+ : TStreamSaveContext(output, version)
+{
+ // Zero id is reserved for nullptr.
+ IdGenerator_.Next();
+}
+
+ui32 TSaveContext::FindId(void* basePtr, const std::type_info* typeInfo) const
+{
+ auto it = PtrToEntry_.find(basePtr);
+ if (it == PtrToEntry_.end()) {
+ return NullObjectId;
+ } else {
+ const auto& entry = it->second;
+ // Failure here means an attempt was made to serialize a polymorphic type
+ // not marked with TDynamicTag.
+ YT_VERIFY(entry.TypeInfo == typeInfo);
+ return entry.Id;
+ }
+}
+
+ui32 TSaveContext::GenerateId(void* basePtr, const std::type_info* typeInfo)
+{
+ TEntry entry;
+ entry.Id = static_cast<ui32>(IdGenerator_.Next());
+ entry.TypeInfo = typeInfo;
+ YT_VERIFY(PtrToEntry_.emplace(basePtr, entry).second);
+ return entry.Id;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLoadContext::~TLoadContext()
+{
+ for (const auto& deletor : Deletors_) {
+ deletor();
+ }
+}
+
+void TLoadContext::RegisterObject(ui32 id, void* basePtr)
+{
+ YT_VERIFY(IdToPtr_.emplace(id, basePtr).second);
+}
+
+void* TLoadContext::GetObject(ui32 id) const
+{
+ return GetOrCrash(IdToPtr_, id);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NPhoenix
diff --git a/yt/yt/core/misc/phoenix.h b/yt/yt/core/misc/phoenix.h
new file mode 100644
index 0000000000..e0900ec64b
--- /dev/null
+++ b/yt/yt/core/misc/phoenix.h
@@ -0,0 +1,310 @@
+#pragma once
+
+#include "id_generator.h"
+#include "mpl.h"
+#include "serialize.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <typeinfo>
+
+namespace NYT::NPhoenix {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr ui32 InlineObjectIdMask = 0x80000000;
+constexpr ui32 NullObjectId = 0x00000000;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDynamicTag
+{
+ virtual ~TDynamicTag() = default;
+};
+
+template <class TFactory>
+struct TFactoryTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializer;
+
+class TContextBase
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TPolymorphicTraits
+{
+ static const bool Dynamic = false;
+ using TBase = T;
+};
+
+template <class T>
+struct TPolymorphicTraits<
+ T,
+ typename std::enable_if_t<
+ std::is_convertible_v<T&, TDynamicTag&>
+ >
+>
+{
+ static const bool Dynamic = true;
+ using TBase = TDynamicTag;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TNullFactory
+{
+ template <class T>
+ static T* Instantiate()
+ {
+ YT_ABORT();
+ }
+};
+
+struct TSimpleFactory
+{
+ template <class T>
+ static T* Instantiate()
+ {
+ return new T();
+ }
+};
+
+struct TRefCountedFactory
+{
+ template <class T>
+ static T* Instantiate()
+ {
+ return New<T>().Release();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TFactoryTraits
+{
+ using TFactory = TSimpleFactory;
+};
+
+template <class T>
+struct TFactoryTraits<
+ T,
+ typename std::enable_if_t<
+ std::conjunction_v<
+ std::is_convertible<T*, NYT::TRefCountedBase*>,
+ std::negation<std::is_convertible<T*, TFactoryTag<TNullFactory>*>>
+ >
+ >
+>
+{
+ using TFactory = TRefCountedFactory;
+};
+
+template <class T>
+struct TFactoryTraits<
+ T,
+ typename std::enable_if_t<
+ std::is_convertible_v<T*, TFactoryTag<TNullFactory>*>
+ >
+>
+{
+ using TFactory = TNullFactory;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TIdClass
+{
+ using TType = T;
+};
+
+template <class T>
+struct TIdClass<
+ T,
+ typename std::enable_if_t<
+ std::is_convertible_v<T*, NYT::TRefCountedBase*>
+ >
+>
+{
+ using TType = NYT::TRefCountedWrapper<T>;
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProfiler
+{
+public:
+ static TProfiler* Get();
+
+ ui32 GetTag(const std::type_info& typeInfo);
+
+ template <class T>
+ T* Instantiate(ui32 tag);
+
+ template <class T>
+ void Register(ui32 tag);
+
+private:
+ struct TEntry
+ {
+ const std::type_info* TypeInfo;
+ ui32 Tag;
+ std::function<void*()> Factory;
+ };
+
+ THashMap<const std::type_info*, TEntry*> TypeInfoToEntry_;
+ THashMap<ui32, TEntry> TagToEntry_;
+
+ TProfiler();
+
+ const TEntry& GetEntry(ui32 tag);
+ const TEntry& GetEntry(const std::type_info& typeInfo);
+
+ template <class T>
+ static void* DoInstantiate();
+
+ Y_DECLARE_SINGLETON_FRIEND()
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ class TType,
+ ui32 tag,
+ class TFactory = typename TFactoryTraits<TType>::TFactory
+>
+struct TDynamicInitializer
+{
+ TDynamicInitializer()
+ {
+ TProfiler::Get()->Register<TType>(tag);
+ }
+};
+
+template <
+ class TType,
+ ui32 tag
+>
+struct TDynamicInitializer<TType, tag, TRefCountedFactory>
+{
+ TDynamicInitializer()
+ {
+ TProfiler::Get()->Register<TType>(tag);
+ }
+};
+
+#define DECLARE_DYNAMIC_PHOENIX_TYPE(...) \
+ static ::NYT::NPhoenix::TDynamicInitializer<__VA_ARGS__> \
+ DynamicPhoenixInitializer
+
+// __VA_ARGS__ are used because sometimes we want a template type
+// to be an argument but the single macro argument may not contain
+// commas. Dat preprocessor :/
+#define DEFINE_DYNAMIC_PHOENIX_TYPE(...) \
+ decltype(__VA_ARGS__::DynamicPhoenixInitializer) \
+ __VA_ARGS__::DynamicPhoenixInitializer
+
+#define INHERIT_DYNAMIC_PHOENIX_TYPE(baseType, type, tag) \
+class type \
+ : public baseType \
+{ \
+public: \
+ using baseType::baseType; \
+ \
+private: \
+ DECLARE_DYNAMIC_PHOENIX_TYPE(type, tag); \
+}
+
+#define INHERIT_DYNAMIC_PHOENIX_TYPE_TEMPLATED(baseType, type, tag, ...) \
+class type \
+ : public baseType<__VA_ARGS__> \
+{ \
+public: \
+ using baseType::baseType; \
+ \
+private: \
+ DECLARE_DYNAMIC_PHOENIX_TYPE(type, tag); \
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSaveContext
+ : public TContextBase
+ , public TStreamSaveContext
+{
+public:
+ explicit TSaveContext(
+ IZeroCopyOutput* output,
+ int version = 0);
+
+ ui32 FindId(void* basePtr, const std::type_info* typeInfo) const;
+ ui32 GenerateId(void* basePtr, const std::type_info* typeInfo);
+
+private:
+ mutable TIdGenerator IdGenerator_;
+
+ struct TEntry
+ {
+ ui32 Id;
+ const std::type_info* TypeInfo;
+ };
+
+ mutable THashMap<void*, TEntry> PtrToEntry_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class = void>
+struct TInstantiatedRegistrar;
+
+class TLoadContext
+ : public TContextBase
+ , public TStreamLoadContext
+{
+public:
+ using TStreamLoadContext::TStreamLoadContext;
+ ~TLoadContext();
+
+ void RegisterObject(ui32 id, void* basePtr);
+ void* GetObject(ui32 id) const;
+
+ template <class T>
+ void RegisterInstantiatedObject(T* rawPtr);
+
+private:
+ THashMap<ui32, void*> IdToPtr_;
+
+ template <class T, class>
+ friend struct TInstantiatedRegistrar;
+
+ std::vector<std::function<void()>> Deletors_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class C>
+struct ICustomPersistent
+ : public virtual TDynamicTag
+{
+ virtual ~ICustomPersistent() = default;
+ virtual void Persist(const C& context) = 0;
+};
+
+using TPersistenceContext = TCustomPersistenceContext<TSaveContext, TLoadContext>;
+using IPersistent = ICustomPersistent<TPersistenceContext>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NPhoenix
+
+#define PHOENIX_INL_H_
+#include "phoenix-inl.h"
+#undef PHOENIX_INL_H_
diff --git a/yt/yt/core/misc/pool_allocator-inl.h b/yt/yt/core/misc/pool_allocator-inl.h
new file mode 100644
index 0000000000..b2be895ea2
--- /dev/null
+++ b/yt/yt/core/misc/pool_allocator-inl.h
@@ -0,0 +1,110 @@
+#ifndef POOL_ALLOCATOR_INL_H_
+#error "Direct inclusion of this file is not allowed, include pool_allocator.h"
+// For the sake of sane code completion.
+#include "pool_allocator.h"
+#endif
+
+#include <util/system/align.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TPoolAllocator::TAllocatedBlockHeader
+{
+ explicit TAllocatedBlockHeader(TPoolAllocator* pool)
+ : Pool(pool)
+ { }
+
+ TPoolAllocator* Pool;
+};
+
+struct TPoolAllocator::TFreeBlockHeader
+{
+ explicit TFreeBlockHeader(TFreeBlockHeader* next)
+ : Next(next)
+ { }
+
+ TFreeBlockHeader* Next;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TPoolAllocator::TPoolAllocator(
+ size_t blockSize,
+ size_t blockAlignment,
+ size_t chunkSize,
+ TRefCountedTypeCookie cookie)
+ : BlockSize_(blockSize)
+ , BlockAlignment_(blockAlignment)
+ , ChunkSize_(chunkSize)
+ , Cookie_(cookie)
+{ }
+
+inline void* TPoolAllocator::Allocate() noexcept
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ if (Y_UNLIKELY(!FirstFree_)) {
+ AllocateChunk();
+ }
+
+ auto* freeHeader = FirstFree_;
+ FirstFree_ = freeHeader->Next;
+
+ auto* ptr = reinterpret_cast<char*>(freeHeader + 1);
+ auto* allocatedHeader = reinterpret_cast<TAllocatedBlockHeader*>(ptr) - 1;
+ new(allocatedHeader) TAllocatedBlockHeader(this);
+ return ptr;
+}
+
+inline void TPoolAllocator::Free(void* ptr) noexcept
+{
+ auto* header = static_cast<TAllocatedBlockHeader*>(ptr) - 1;
+ header->Pool->DoFree(ptr);
+}
+
+template <class T, class... TArgs>
+std::unique_ptr<T> TPoolAllocator::New(TArgs&&... args)
+{
+ struct TChunkTag
+ { };
+ constexpr auto ChunkSize = 64_KB;
+ static thread_local TPoolAllocator Allocator(
+ sizeof(T),
+ alignof(T),
+ ChunkSize,
+ GetRefCountedTypeCookie<TChunkTag>());
+
+ return std::unique_ptr<T>(new(&Allocator) T(std::forward<TArgs>(args)...));
+}
+
+inline void TPoolAllocator::DoFree(void* ptr)
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ auto* header = static_cast<TFreeBlockHeader*>(ptr) - 1;
+ new(header) TFreeBlockHeader(FirstFree_);
+ FirstFree_ = header;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void* TPoolAllocator::TObjectBase::operator new(size_t /*size*/, TPoolAllocator* allocator) noexcept
+{
+ return allocator->Allocate();
+}
+
+inline void* TPoolAllocator::TObjectBase::operator new(size_t /*size*/, void* where) noexcept
+{
+ return where;
+}
+
+inline void TPoolAllocator::TObjectBase::operator delete(void* ptr) noexcept
+{
+ TPoolAllocator::Free(ptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/pool_allocator.cpp b/yt/yt/core/misc/pool_allocator.cpp
new file mode 100644
index 0000000000..dbb7c2ea73
--- /dev/null
+++ b/yt/yt/core/misc/pool_allocator.cpp
@@ -0,0 +1,49 @@
+#include "pool_allocator.h"
+
+#include <util/system/align.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TPoolAllocator::AllocateChunk()
+{
+ VERIFY_THREAD_AFFINITY(HomeThread);
+
+ auto alignment = Max(
+ alignof(TAllocatedBlockHeader),
+ alignof(TFreeBlockHeader),
+ BlockAlignment_);
+ auto alignedHeaderSize = Max(
+ AlignUp(sizeof(TAllocatedBlockHeader), alignment),
+ AlignUp(sizeof(TFreeBlockHeader), alignment));
+ auto alignedBlockSize = AlignUp(BlockSize_, alignment);
+ auto fullBlockSize = alignedHeaderSize + alignedBlockSize;
+
+ auto blocksPerChunk = ChunkSize_ < fullBlockSize + alignment ? 1 : (ChunkSize_ - alignment) / fullBlockSize;
+ auto chunkSize = blocksPerChunk * fullBlockSize;
+ auto chunk = TSharedMutableRef::Allocate(
+ chunkSize,
+ {.InitializeStorage = false, .ExtendToUsableSize = true},
+ Cookie_);
+ Chunks_.push_back(chunk);
+
+ auto* current = AlignUp(chunk.Begin(), alignment);
+ while (true) {
+ auto* blockBegin = current + alignedHeaderSize;
+ auto* blockEnd = blockBegin + alignedBlockSize;
+ if (blockEnd > chunk.End()) {
+ break;
+ }
+
+ auto* header = reinterpret_cast<TFreeBlockHeader*>(blockBegin) - 1;
+ new(header) TFreeBlockHeader(FirstFree_);
+ FirstFree_ = header;
+
+ current += fullBlockSize;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/pool_allocator.h b/yt/yt/core/misc/pool_allocator.h
new file mode 100644
index 0000000000..16837572d8
--- /dev/null
+++ b/yt/yt/core/misc/pool_allocator.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/generic/size_literals.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Allocates blocks of given fixed size with given alignment.
+//! The underlying memory is acquired in chunks of fixed size.
+/*!
+ * \note
+ * Thread affinity: single-threaded.
+ */
+class TPoolAllocator
+{
+public:
+ //! Initializes a new allocator.
+ /*!
+ * All allocated blocks will be #blockSize bytes and will obey #blockAlignment.
+ * Chunks of #chunkSize (approximately) tagged with #cookie will be used
+ * as an underlying storage.
+ */
+ TPoolAllocator(
+ size_t blockSize,
+ size_t blockAlignment,
+ size_t chunkSize,
+ TRefCountedTypeCookie cookie);
+
+ //! Allocates a new block.
+ void* Allocate() noexcept;
+
+ //! Frees a block previously allocated via #Allocate.
+ /*!
+ * The call must take place within the same thread the block was allocated.
+ */
+ static void Free(void* ptr) noexcept;
+
+ //! Allocates a new object of type #T (typically derived from #TPoolAllocator::TObjectBase)
+ //! from a per-thread pool.
+ /*!
+ * \note
+ * The object's disposal via |operator delete| must take place within
+ * the same thread the object was created.
+ */
+ template <class T, class... TArgs>
+ static std::unique_ptr<T> New(TArgs&&... args);
+
+ //! A base for objects instantiated via #TPoolAllocator::New.
+ class TObjectBase
+ {
+ public:
+ void* operator new(size_t size, TPoolAllocator* allocator) noexcept;
+ void* operator new(size_t size, void* where) noexcept;
+ void operator delete(void* ptr) noexcept;
+
+ [[deprecated("Use TPoolAllocator::New for instantiation")]]
+ void* operator new(size_t size) noexcept;
+ [[deprecated("Vectorized allocations are not supported")]]
+ void* operator new[](size_t size) noexcept;
+ [[deprecated("Vectorized allocations are not supported")]]
+ void operator delete[](void* ptr) noexcept;
+ };
+
+private:
+ const size_t BlockSize_;
+ const size_t BlockAlignment_;
+ const size_t ChunkSize_;
+ const TRefCountedTypeCookie Cookie_;
+
+ DECLARE_THREAD_AFFINITY_SLOT(HomeThread);
+
+ struct TAllocatedBlockHeader;
+ struct TFreeBlockHeader;
+
+ std::vector<TSharedRef> Chunks_;
+ TFreeBlockHeader* FirstFree_ = nullptr;
+
+ void DoFree(void* ptr);
+ void AllocateChunk();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define POOL_ALLOCATOR_INL_H_
+#include "pool_allocator-inl.h"
+#undef POOL_ALLOCATOR_INL_H_
diff --git a/yt/yt/core/misc/proc.cpp b/yt/yt/core/misc/proc.cpp
new file mode 100644
index 0000000000..1b7aefe466
--- /dev/null
+++ b/yt/yt/core/misc/proc.cpp
@@ -0,0 +1,1587 @@
+#include "proc.h"
+#include "common.h"
+#include "string.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/common.h>
+#include <yt/yt/core/misc/error_code.h>
+#include <yt/yt/core/misc/fs.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <util/stream/file.h>
+
+#include <util/string/split.h>
+#include <util/string/strip.h>
+#include <util/string/vector.h>
+
+#include <util/system/info.h>
+#include <util/system/fs.h>
+#include <util/system/fstat.h>
+#include <util/folder/iterator.h>
+#include <util/folder/filelist.h>
+
+#ifdef _unix_
+ #include <stdio.h>
+ #include <dirent.h>
+ #include <errno.h>
+ #include <pwd.h>
+ #include <sys/ioctl.h>
+ #include <sys/types.h>
+ #include <sys/resource.h>
+ #include <sys/stat.h>
+ #include <sys/syscall.h>
+ #include <sys/ttydefaults.h>
+ #include <unistd.h>
+#endif
+#ifdef _linux_
+ #include <pty.h>
+ #include <pwd.h>
+ #include <grp.h>
+ #include <utmp.h>
+ #include <sys/prctl.h>
+ #include <sys/sysmacros.h>
+ #include <sys/ttydefaults.h>
+ #include <sys/utsname.h>
+#endif
+#ifdef _darwin_
+ #include <util.h>
+ #include <pthread.h>
+#endif
+
+#ifdef _linux_
+extern "C" int memfd_create(const char *name, unsigned flags);
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static inline const NLogging::TLogger Logger("Proc");
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString LinuxErrorCodeFormatter(int code)
+{
+ return TEnumTraits<ELinuxErrorCode>::ToString(static_cast<ELinuxErrorCode>(code));
+}
+
+YT_DEFINE_ERROR_CODE_RANGE(4200, 4399, "NYT::ELinuxErrorCode", LinuxErrorCodeFormatter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSystemErrorCode(TErrorCode errorCode)
+{
+ return errorCode >= LinuxErrorCodeBase && errorCode < LinuxErrorCodeBase + LinuxErrorCodeCount;
+}
+
+bool IsSystemError(const TError& error)
+{
+ if (IsSystemErrorCode(error.GetCode())) {
+ return true;
+ }
+ for (const auto& innerError : error.InnerErrors()) {
+ if (IsSystemError(innerError)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<int> GetParentPid(int pid)
+{
+ TFileInput in(Format("/proc/%v/status", pid));
+ TString line;
+ while (in.ReadLine(line)) {
+ const TString ppidMarker = "PPid:\t";
+ if (line.StartsWith(ppidMarker)) {
+ line = line.substr(ppidMarker.size());
+ return FromString<int>(line);
+ }
+ }
+
+ return {};
+}
+
+std::vector<int> GetNamespacePids(int pid)
+{
+ TFileInput in(Format("/proc/%v/status", pid));
+ TString line;
+ while (in.ReadLine(line)) {
+ const TString nstgidMarker = "NStgid:\t";
+ if (line.StartsWith(nstgidMarker)) {
+ line = line.substr(nstgidMarker.size());
+ auto pidFields = SplitString(line, " ");
+ std::vector<int> pids;
+ for (const auto& field : pidFields) {
+ pids.push_back(FromString<int>(field));
+ }
+ return pids;
+ }
+ }
+ return {};
+}
+
+std::vector<int> ListPids()
+{
+#ifdef _linux_
+ std::vector<int> pids;
+
+ for (const auto& entry : TDirIterator("/proc", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_info != FTS_D) {
+ continue;
+ }
+ const char* begin = entry.fts_name;
+ char* end = nullptr;
+ int pid = static_cast<int>(strtol(begin, &end, 10));
+ if (begin == end) {
+ // Not a pid.
+ continue;
+ }
+ pids.push_back(pid);
+ }
+
+ return pids;
+#else
+ return {};
+#endif
+}
+
+std::optional<int> GetPidByChildNamespacePid(int childNamespacePid)
+{
+#ifdef _linux_
+ for (const auto& entry : TDirIterator("/proc", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_info != FTS_D) {
+ continue;
+ }
+ const char* begin = entry.fts_name;
+ char* end = nullptr;
+ int pid = static_cast<int>(strtol(begin, &end, 10));
+ if (begin == end) {
+ // Not a pid.
+ continue;
+ }
+
+ try {
+ auto namespacePids = GetNamespacePids(pid);
+ if (namespacePids.size() >= 2 && namespacePids[1] == childNamespacePid) {
+ return pid;
+ }
+ } catch(...) {
+ // Assume that the process has already completed.
+ continue;
+ }
+ }
+ return {};
+#else
+ Y_UNUSED(childNamespacePid);
+ return {};
+#endif
+}
+
+std::vector<int> GetPidsByUid(int uid)
+{
+#ifdef _linux_
+ std::vector<int> result;
+
+ for (const auto& entry : TDirIterator("/proc", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_info != FTS_D) {
+ continue;
+ }
+ const char* begin = entry.fts_name;
+ char* end = nullptr;
+ int pid = static_cast<int>(strtol(begin, &end, 10));
+ if (begin == end) {
+ // Not a pid.
+ continue;
+ }
+
+ YT_VERIFY(entry.fts_statp);
+ if (static_cast<int>(entry.fts_statp->st_uid) == uid || uid == -1) {
+ result.push_back(pid);
+ }
+ }
+
+ return result;
+#else
+ Y_UNUSED(uid);
+ return std::vector<int>();
+#endif
+}
+
+std::vector<int> GetPidsUnderParent(int targetPid)
+{
+#ifdef _linux_
+ std::vector<int> result;
+ std::map<int, int> parents;
+
+ for (const auto& entry : TDirIterator("/proc", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_info != FTS_D) {
+ continue;
+ }
+ const char* begin = entry.fts_name;
+ char* end = nullptr;
+ int pid = static_cast<int>(strtol(begin, &end, 10));
+ if (begin == end) {
+ // Not a pid.
+ continue;
+ }
+
+ try {
+ auto ppid = GetParentPid(pid);
+ if (ppid) {
+ parents[pid] = *ppid;
+ }
+ } catch(...) {
+ // Assume that the process has already completed.
+ continue;
+ }
+ }
+
+ for (auto [pid, ppid] : parents) {
+ while (true) {
+ if (ppid == targetPid) {
+ result.push_back(pid);
+ }
+
+ auto it = parents.find(ppid);
+ if (it == parents.end()) {
+ break;
+ } else {
+ ppid = it->second;
+ }
+ }
+ }
+
+ return result;
+#else
+ Y_UNUSED(targetPid);
+ return {};
+#endif
+}
+
+size_t GetCurrentProcessId()
+{
+#if defined(_linux_)
+ return getpid();
+#else
+ YT_ABORT();
+#endif
+}
+
+size_t GetCurrentThreadId()
+{
+#if defined(_linux_)
+ return static_cast<size_t>(::syscall(SYS_gettid));
+#elif defined(_darwin_)
+ uint64_t tid;
+ YT_VERIFY(pthread_threadid_np(nullptr, &tid) == 0);
+ return static_cast<size_t>(tid);
+#else
+ return ::GetCurrentThreadId();
+#endif
+}
+
+std::vector<size_t> GetCurrentProcessThreadIds()
+{
+#ifdef __linux__
+ std::vector<size_t> result;
+ TFileEntitiesList fileList(TFileEntitiesList::EM_DIRS);
+ try {
+ fileList.Fill("/proc/self/task");
+ while (const char* name = fileList.Next()) {
+ if (auto optionalId = TryFromString<size_t>(name)) {
+ result.push_back(*optionalId);
+ }
+ }
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error listing /proc/self/task");
+ return {};
+ }
+ return result;
+#else
+ return {};
+#endif
+}
+
+void ChownChmodDirectory(const TString& path, const std::optional<uid_t>& userId, const std::optional<int>& permissions)
+{
+#ifdef _unix_
+ if (userId) {
+ auto res = HandleEintr(::chown, path.data(), *userId, -1);
+ if (res != 0) {
+ THROW_ERROR_EXCEPTION("Failed to change owner for directory %v", path)
+ << TErrorAttribute("owner_uid", *userId)
+ << TError::FromSystem();
+ }
+ }
+
+ if (permissions) {
+ auto res = HandleEintr(::chmod, path.data(), *permissions);
+ if (res != 0) {
+ THROW_ERROR_EXCEPTION("Failed to set permissions for directory %v", path)
+ << TErrorAttribute("permissions", *permissions)
+ << TError::FromSystem();
+ }
+ }
+#else
+ YT_ABORT();
+#endif
+}
+
+void ChownChmodDirectoriesRecursively(const TString& path, const std::optional<uid_t>& userId, const std::optional<int>& permissions)
+{
+#ifdef _unix_
+ for (const auto& directoryPath : NFS::EnumerateDirectories(path)) {
+ auto nestedPath = NFS::CombinePaths(path, directoryPath);
+ ChownChmodDirectoriesRecursively(nestedPath, userId, permissions);
+ }
+
+ ChownChmodDirectory(path, userId, permissions);
+#else
+ YT_ABORT();
+#endif
+}
+
+void SetThreadPriority(int tid, int priority)
+{
+#ifdef _unix_
+ auto res = ::setpriority(PRIO_PROCESS, tid, priority);
+ if (res != 0) {
+ THROW_ERROR_EXCEPTION("Failed to set priority for thread %v",
+ tid) << TError::FromSystem();
+ }
+#else
+ YT_ABORT();
+#endif
+}
+
+TMemoryUsage GetProcessMemoryUsage(int pid)
+{
+#ifdef _linux_
+ TString path = "/proc/self/statm";
+ if (pid != -1) {
+ path = Format("/proc/%v/statm", pid);
+ }
+
+ TIFStream memoryStatFile(path);
+ auto memoryStatFields = SplitString(memoryStatFile.ReadLine(), " ");
+ return TMemoryUsage {
+ FromString<ui64>(memoryStatFields[1]) * NSystemInfo::GetPageSize(),
+ FromString<ui64>(memoryStatFields[2]) * NSystemInfo::GetPageSize(),
+ };
+#else
+ Y_UNUSED(pid);
+ return TMemoryUsage{0, 0};
+#endif
+}
+
+std::vector<TProcessCgroup> GetProcessCgroups(int pid)
+{
+#ifdef _linux_
+ TString path = "/proc/self/cgroup";
+ if (pid != -1) {
+ path = Format("/proc/%v/cgroup", pid);
+ }
+
+ std::vector<TProcessCgroup> groups;
+
+ TIFStream cgroupFile(path);
+ for (TString line; cgroupFile.ReadLine(line); ) {
+ if (line.empty()) {
+ continue;
+ }
+
+ auto fields = SplitString(line, ":", 3, KEEP_EMPTY_TOKENS);
+ if (fields.size() != 3) {
+ THROW_ERROR_EXCEPTION("Failed parse process cgroups")
+ << TErrorAttribute("line", line)
+ << TErrorAttribute("fields", fields);
+ }
+
+ TProcessCgroup group;
+ group.HierarchyId = FromString<ui64>(fields[0]);
+ group.ControllersName = fields[1];
+ group.Controllers = SplitString(fields[1], ",");
+ group.Path = fields[2];
+
+ groups.push_back(group);
+ }
+
+ return groups;
+#else
+ Y_UNUSED(pid);
+ return {};
+#endif
+}
+
+TCgroupCpuStat GetCgroupCpuStat(
+ const TString& controllerName,
+ const TString& cgroupPath,
+ const TString& cgroupMountPoint)
+{
+#ifdef _linux_
+ TString path = cgroupMountPoint + "/" + controllerName + cgroupPath + "/cpu.stat";
+
+ TCgroupCpuStat stat;
+
+ TIFStream cgroupFile(path);
+ for (TString line; cgroupFile.ReadLine(line); ) {
+ if (line.empty()) {
+ continue;
+ }
+
+ auto fields = SplitString(line, " ", 2);
+ if (fields.size() != 2) {
+ continue;
+ }
+ if (fields[0] == "nr_periods") {
+ stat.NrPeriods = FromString<ui64>(fields[1]);
+ } else if (fields[0] == "nr_throttled") {
+ stat.NrThrottled = FromString<ui64>(fields[1]);
+ } else if (fields[0] == "throttled_time") {
+ stat.ThrottledTime = FromString<ui64>(fields[1]);
+ } else if (fields[0] == "wait_sum") {
+ stat.WaitTime = FromString<ui64>(fields[1]);
+ }
+ }
+
+ return stat;
+#else
+ Y_UNUSED(controllerName, cgroupPath, cgroupMountPoint);
+ return {};
+#endif
+}
+
+TCgroupMemoryStat GetCgroupMemoryStat(
+ const TString& cgroupPath,
+ const TString& cgroupMountPoint)
+{
+#ifdef _linux_
+ TString path = cgroupMountPoint + "/memory" + cgroupPath + "/memory.stat";
+
+ TCgroupMemoryStat stat;
+
+ TIFStream cgroupFile(path);
+ for (TString line; cgroupFile.ReadLine(line); ) {
+ if (line.empty()) {
+ continue;
+ }
+
+ auto fields = SplitString(line, " ", 2);
+ if (fields.size() != 2) {
+ continue;
+ }
+
+ auto tryParse = [&] (auto field, auto name) {
+ if (fields[0] == name) {
+ *field = FromString<ui64>(fields[1]);
+ }
+ };
+
+ tryParse(&stat.HierarchicalMemoryLimit, "hierarchical_memory_limit");
+ tryParse(&stat.Cache, "cache");
+ tryParse(&stat.Rss, "rss");
+ tryParse(&stat.RssHuge, "rss_huge");
+ tryParse(&stat.MappedFile, "mapped_file");
+ tryParse(&stat.Dirty, "dirty");
+ tryParse(&stat.Writeback, "writeback");
+ }
+
+ return stat;
+#else
+ Y_UNUSED(cgroupPath, cgroupMountPoint);
+ return {};
+#endif
+}
+
+THashMap<TString, i64> GetVmstat()
+{
+#ifdef _linux_
+ THashMap<TString, i64> result;
+ TString path = "/proc/vmstat";
+ TFileInput vmstatFile(path);
+ auto data = vmstatFile.ReadAll();
+ auto lines = SplitString(data, "\n");
+ for (const auto& line : lines) {
+ auto strippedLine = Strip(line);
+ if (strippedLine.empty()) {
+ continue;
+ }
+ auto fields = SplitString(line, " ");
+ result[fields[0]] = NSystemInfo::GetPageSize() * FromString<i64>(fields[1]);
+ }
+ return result;
+#else
+ return {};
+#endif
+}
+
+ui64 GetProcessCumulativeMajorPageFaults(int pid)
+{
+#ifdef _linux_
+ TString path = "/proc/self/stat";
+ if (pid != -1) {
+ path = Format("/proc/%v/stat", pid);
+ }
+
+ TIFStream statFile(path);
+ auto statFields = SplitString(statFile.ReadLine(), " ");
+ return FromString<ui64>(statFields[11]) + FromString<ui64>(statFields[12]);
+#else
+ Y_UNUSED(pid);
+ return 0;
+#endif
+}
+
+TString GetProcessName(int pid)
+{
+#ifdef _linux_
+ TString path = Format("/proc/%v/comm", pid);
+ return Trim(TUnbufferedFileInput(path).ReadAll(), "\n");
+#else
+ Y_UNUSED(pid);
+ return "";
+#endif
+}
+
+std::vector<TString> GetProcessCommandLine(int pid)
+{
+#ifdef _linux_
+ TString path = Format("/proc/%v/cmdline", pid);
+ auto raw = TUnbufferedFileInput(path).ReadAll();
+ std::vector<TString> result;
+ auto begin = 0;
+ while (begin < std::ssize(raw)) {
+ auto end = raw.find('\0', begin);
+ if (end == TString::npos) {
+ result.push_back(raw.substr(begin));
+ begin = raw.length();
+ } else {
+ result.push_back(raw.substr(begin, end - begin));
+ begin = end + 1;
+ }
+ }
+
+ return result;
+#else
+ Y_UNUSED(pid);
+ return std::vector<TString>();
+#endif
+}
+
+void SafeClose(TFileDescriptor fd, bool ignoreBadFD)
+{
+ if (!TryClose(fd, ignoreBadFD)) {
+ THROW_ERROR TError::FromSystem();
+ }
+}
+
+#ifdef _unix_
+
+TError StatusToError(int status)
+{
+ if (WIFEXITED(status) && (WEXITSTATUS(status) == 0)) {
+ return TError();
+ } else if (WIFSIGNALED(status)) {
+ int signalNumber = WTERMSIG(status);
+ return TError(
+ EProcessErrorCode::Signal,
+ "Process terminated by signal %v",
+ signalNumber)
+ << TErrorAttribute("signal", signalNumber);
+ } else if (WIFSTOPPED(status)) {
+ int signalNumber = WSTOPSIG(status);
+ return TError(
+ EProcessErrorCode::Signal,
+ "Process stopped by signal %v",
+ signalNumber)
+ << TErrorAttribute("signal", signalNumber);
+ } else if (WIFEXITED(status)) {
+ int exitCode = WEXITSTATUS(status);
+ return TError(
+ EProcessErrorCode::NonZeroExitCode,
+ "Process exited with code %v",
+ exitCode)
+ << TErrorAttribute("exit_code", exitCode);
+ } else {
+ return TError("Unknown status %v", status);
+ }
+}
+
+#ifdef _unix_
+TError ProcessInfoToError(const siginfo_t& processInfo)
+{
+ switch (processInfo.si_code) {
+ case CLD_EXITED: {
+ auto exitCode = processInfo.si_status;
+ if (exitCode == 0) {
+ return TError();
+ } else {
+ return TError(
+ EProcessErrorCode::NonZeroExitCode,
+ "Process exited with code %v",
+ exitCode)
+ << TErrorAttribute("exit_code", exitCode);
+ }
+ }
+
+ case CLD_KILLED:
+ case CLD_DUMPED: {
+ int signal = processInfo.si_status;
+ return TError(
+ EProcessErrorCode::Signal,
+ "Process terminated by signal %v",
+ signal)
+ << TErrorAttribute("signal", signal)
+ << TErrorAttribute("core_dumped", processInfo.si_code == CLD_DUMPED);
+ }
+
+ default:
+ return TError("Unknown signal code %v",
+ processInfo.si_code);
+ }
+}
+#endif
+
+bool TryExecve(const char* path, const char* const* argv, const char* const* env)
+{
+ ::execve(
+ path,
+ const_cast<char* const*>(argv),
+ const_cast<char* const*>(env));
+ // If we are still here, it's an error.
+ return false;
+}
+
+bool TryDup2(int oldFD, int newFD)
+{
+ while (true) {
+ auto res = HandleEintr(::dup2, oldFD, newFD);
+
+ if (res != -1) {
+ return true;
+ }
+
+ if (errno == EBUSY) {
+ continue;
+ }
+
+ return false;
+ }
+}
+
+bool TryClose(int fd, bool ignoreBadFD)
+{
+ while (true) {
+ auto res = ::close(fd);
+ if (res != -1) {
+ return true;
+ }
+
+ switch (errno) {
+ // Please read
+ // http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html and
+ // http://rb.yandex-team.ru/arc/r/44030/
+ // before editing.
+ case EINTR:
+ return true;
+ case EBADF:
+ return ignoreBadFD;
+ default:
+ return false;
+ }
+ }
+}
+
+void SafeDup2(int oldFD, int newFD)
+{
+ if (!TryDup2(oldFD, newFD)) {
+ THROW_ERROR_EXCEPTION("dup2 failed")
+ << TErrorAttribute("old_fd", oldFD)
+ << TErrorAttribute("new_fd", newFD)
+ << TError::FromSystem();
+ }
+}
+
+void SafeSetCloexec(int fd)
+{
+ int getResult = ::fcntl(fd, F_GETFD);
+ if (getResult == -1) {
+ THROW_ERROR_EXCEPTION("Error creating pipe: fcntl failed to get descriptor flags")
+ << TError::FromSystem();
+ }
+
+ int setResult = ::fcntl(fd, F_SETFD, getResult | FD_CLOEXEC);
+ if (setResult == -1) {
+ THROW_ERROR_EXCEPTION("Error creating pipe: fcntl failed to set descriptor flags")
+ << TError::FromSystem();
+ }
+}
+
+void SetUid(int uid)
+{
+ // Set unprivileged uid for user process.
+ if (setuid(0) != 0) {
+ THROW_ERROR_EXCEPTION("Unable to set zero uid")
+ << TError::FromSystem();
+ }
+
+ errno = 0;
+#ifdef _linux_
+ const auto* passwd = getpwuid(uid);
+ int gid = (passwd && errno == 0)
+ ? passwd->pw_gid
+ : uid; // fallback value.
+
+ if (setresgid(gid, gid, gid) != 0) {
+ THROW_ERROR_EXCEPTION("Unable to set gids")
+ << TErrorAttribute("uid", uid)
+ << TErrorAttribute("gid", gid)
+ << TError::FromSystem();
+ }
+
+ if (setresuid(uid, uid, uid) != 0) {
+ THROW_ERROR_EXCEPTION("Unable to set uids")
+ << TErrorAttribute("uid", uid)
+ << TError::FromSystem();
+ }
+#else
+ if (setuid(uid) != 0) {
+ THROW_ERROR_EXCEPTION("Unable to set uid")
+ << TErrorAttribute("uid", uid)
+ << TError::FromSystem();
+ }
+
+ if (setgid(uid) != 0) {
+ THROW_ERROR_EXCEPTION("Unable to set gid")
+ << TErrorAttribute("gid", uid)
+ << TError::FromSystem();
+ }
+#endif
+}
+
+void SafePipe(int fd[2])
+{
+#ifdef _linux_
+ auto result = ::pipe2(fd, O_CLOEXEC);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error creating pipe")
+ << TError::FromSystem();
+ }
+#else
+ {
+ int result = ::pipe(fd);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error creating pipe")
+ << TError::FromSystem();
+ }
+ }
+ SafeSetCloexec(fd[0]);
+ SafeSetCloexec(fd[1]);
+#endif
+}
+
+int SafeDup(int fd)
+{
+ auto result = ::dup(fd);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error duplicating fd")
+ << TError::FromSystem();
+ }
+ return result;
+}
+
+void SafeOpenPty(int* masterFD, int* slaveFD, int height, int width)
+{
+#ifdef _linux_
+ {
+ struct termios tt = {};
+ tt.c_iflag = TTYDEF_IFLAG & ~ISTRIP;
+ tt.c_oflag = TTYDEF_OFLAG;
+ tt.c_lflag = TTYDEF_LFLAG;
+ tt.c_cflag = (TTYDEF_CFLAG & ~(CS7 | PARENB | HUPCL)) | CS8;
+ tt.c_cc[VERASE] = '\x7F';
+ cfsetispeed(&tt, B38400);
+ cfsetospeed(&tt, B38400);
+
+ struct winsize ws = {};
+ struct winsize* wsPtr = nullptr;
+ if (height > 0 && width > 0) {
+ ws.ws_row = height;
+ ws.ws_col = width;
+ wsPtr = &ws;
+ }
+
+ int result = ::openpty(masterFD, slaveFD, nullptr, &tt, wsPtr);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error creating pty: pty creation failed")
+ << TError::FromSystem();
+ }
+ }
+ SafeSetCloexec(*masterFD);
+#else
+ Y_UNUSED(masterFD, slaveFD, height, width);
+ THROW_ERROR_EXCEPTION("Unsupported");
+#endif
+}
+
+void SafeLoginTty(int slaveFD)
+{
+#ifdef _linux_
+ int result = ::login_tty(slaveFD);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error attaching pty to standard streams")
+ << TError::FromSystem();
+ }
+#else
+ Y_UNUSED(slaveFD);
+ THROW_ERROR_EXCEPTION("Unsupported");
+#endif
+}
+
+void SafeSetTtyWindowSize(int fd, int height, int width)
+{
+ if (height > 0 && width > 0) {
+ struct winsize ws;
+ int result = ::ioctl(fd, TIOCGWINSZ, &ws);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error reading tty window size")
+ << TError::FromSystem();
+ }
+ if (ws.ws_row != height || ws.ws_col != width) {
+ ws.ws_row = height;
+ ws.ws_col = width;
+ result = ::ioctl(fd, TIOCSWINSZ, &ws);
+ if (result == -1) {
+ THROW_ERROR_EXCEPTION("Error setting tty window size")
+ << TError::FromSystem();
+ }
+ }
+ }
+}
+
+bool TryMakeNonblocking(int fd)
+{
+ auto res = fcntl(fd, F_GETFL);
+
+ if (res == -1) {
+ return false;
+ }
+
+ res = fcntl(fd, F_SETFL, res | O_NONBLOCK);
+
+ if (res == -1) {
+ return false;
+ }
+
+ return true;
+}
+
+void SafeMakeNonblocking(int fd)
+{
+ if (!TryMakeNonblocking(fd)) {
+ THROW_ERROR_EXCEPTION("Failed to set nonblocking mode for descriptor %v", fd)
+ << TError::FromSystem();
+ }
+}
+
+bool TrySetUid(int uid)
+{
+#ifdef _linux_
+ // NB(psushin): setting real uid is really important, e.g. for access() call.
+ if (setresuid(uid, uid, uid) != 0) {
+ return false;
+ }
+#else
+ if (setuid(uid) != 0) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+void SafeSetUid(int uid)
+{
+ if (!TrySetUid(uid)) {
+ THROW_ERROR_EXCEPTION("Failed to set uid to %v", uid)
+ << TError::FromSystem();
+ }
+}
+
+TString SafeGetUsernameByUid(int uid)
+{
+ int bufferSize = ::sysconf(_SC_GETPW_R_SIZE_MAX);
+ if (bufferSize < 0) {
+ THROW_ERROR_EXCEPTION("Failed to get username, sysconf(_SC_GETPW_R_SIZE_MAX) failed")
+ << TError::FromSystem();
+ }
+ char buffer[bufferSize];
+ struct passwd pwd, * pwdptr = nullptr;
+ int result = getpwuid_r(uid, &pwd, buffer, bufferSize, &pwdptr);
+ if (result != 0 || pwdptr == nullptr) {
+ // Return #uid in case of absent uid in the system.
+ return "#" + ToString(uid);
+ }
+ return pwdptr->pw_name;
+}
+
+#else
+
+bool TryClose(TFileDescriptor fd, bool ignoreBadFD)
+{
+ if (::closesocket(fd) != SOCKET_ERROR) {
+ return true;
+ }
+
+ if (WSAGetLastError() == WSAENOTSOCK) {
+ return ignoreBadFD;
+ }
+
+ return false;
+}
+
+bool TryDup2(TFileDescriptor /* oldFD */, TFileDescriptor /* newFD */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeDup2(TFileDescriptor /* oldFD */, TFileDescriptor /* newFD */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeSetCloexec(TFileDescriptor /* fd */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+bool TryExecve(const char /* *path */, const char* /* argv[] */, const char* /* env[] */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TError StatusToError(int /* status */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void CloseAllDescriptors()
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafePipe(TFileDescriptor /* fd */ [2])
+{
+ YT_UNIMPLEMENTED();
+}
+
+TFileDescriptor SafeDup(TFileDescriptor /* fd */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeOpenPty(TFileDescriptor* /* masterFD */, TFileDescriptor* /* slaveFD */, int /* height */, int /* width */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeLoginTty(TFileDescriptor /* slaveFD */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeSetTtyWindowSize(TFileDescriptor /* slaveFD */, int /* height */, int /* width */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+bool TryMakeNonblocking(TFileDescriptor /* fd */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeMakeNonblocking(TFileDescriptor /* fd */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+void SafeSetUid(int /* uid */)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TString SafeGetUsernameByUid(int /* uid */)
+{
+ YT_UNIMPLEMENTED();
+}
+#endif
+
+void CloseAllDescriptors(const std::vector<int>& exceptFor)
+{
+#ifdef _linux_
+ std::vector<int> fds;
+ for (const auto& entry : TDirIterator("/proc/self/fd", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_type != FTS_SL) {
+ continue;
+ }
+ const char* begin = entry.fts_name;
+ char* end = nullptr;
+ int fd = static_cast<int>(strtol(begin, &end, 10));
+ if (begin == end ||
+ (std::find(exceptFor.begin(), exceptFor.end(), fd) != exceptFor.end()))
+ {
+ continue;
+ }
+ // We can add descriptor from TDirIterator here but it will be ignored with ignoreBadFD flag.
+ fds.push_back(fd);
+ }
+
+ bool ignoreBadFD = true;
+ for (int fd : fds) {
+ YT_VERIFY(TryClose(fd, ignoreBadFD));
+ }
+#else
+ Y_UNUSED(exceptFor);
+#endif
+}
+
+int GetFileDescriptorCount()
+{
+ int descriptorCount = 0;
+#ifdef __linux__
+ TFileEntitiesList fileList(TFileEntitiesList::EM_SLINKS);
+ try {
+ fileList.Fill("/proc/self/fd");
+ while (fileList.Next() != nullptr) {
+ ++descriptorCount;
+ }
+ // Don't count opened /proc/self/fd.
+ --descriptorCount;
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error listing /proc/self/fd");
+ }
+#endif
+ return descriptorCount;
+}
+
+void SafeCreateStderrFile(TString fileName)
+{
+#ifdef _unix_
+ if (freopen(fileName.data(), "a", stderr) == nullptr) {
+ auto lastError = TError::FromSystem();
+ THROW_ERROR_EXCEPTION("Stderr redirection failed")
+ << lastError;
+ }
+#endif
+}
+
+bool HasRootPermissions()
+{
+#ifdef _unix_
+ uid_t ruid, euid, suid;
+#ifdef _linux_
+ YT_VERIFY(getresuid(&ruid, &euid, &suid) == 0);
+#else
+ ruid = getuid();
+ euid = geteuid();
+ setuid(0);
+ suid = getuid();
+ YT_VERIFY(seteuid(euid) == 0);
+ YT_VERIFY(setruid(ruid) == 0);
+#endif
+ return suid == 0;
+#else // not _unix_
+ return false;
+#endif
+}
+
+TNetworkInterfaceStatisticsMap GetNetworkInterfaceStatistics()
+{
+#ifdef _linux_
+ // According to https://www.kernel.org/doc/Documentation/filesystems/proc.txt,
+ // using /proc/net/dev is a stable (and seemingly easiest, despite being nasty)
+ // way to access per-interface network statistics.
+
+ TFileInput procNetDev("/proc/net/dev");
+ // First two lines are header.
+ Y_UNUSED(procNetDev.ReadLine());
+ Y_UNUSED(procNetDev.ReadLine());
+ TNetworkInterfaceStatisticsMap interfaceToStatistics;
+ for (TString line; procNetDev.ReadLine(line) != 0; ) {
+ TNetworkInterfaceStatistics statistics;
+ TVector<TString> lineParts = StringSplitter(line).SplitBySet(": ").SkipEmpty();
+ YT_VERIFY(lineParts.size() == 1 + sizeof(TNetworkInterfaceStatistics) / sizeof(ui64));
+ auto interfaceName = lineParts[0];
+
+ int index = 1;
+#define XX(field) statistics.field = FromString<ui64>(lineParts[index++])
+ XX(Rx.Bytes);
+ XX(Rx.Packets);
+ XX(Rx.Errs);
+ XX(Rx.Drop);
+ XX(Rx.Fifo);
+ XX(Rx.Frame);
+ XX(Rx.Compressed);
+ XX(Rx.Multicast);
+ XX(Tx.Bytes);
+ XX(Tx.Packets);
+ XX(Tx.Errs);
+ XX(Tx.Drop);
+ XX(Tx.Fifo);
+ XX(Tx.Colls);
+ XX(Tx.Carrier);
+ XX(Tx.Compressed);
+#undef XX
+ // NB: data is racy; duplicates are possible; just deal with it.
+ interfaceToStatistics.emplace(interfaceName, statistics);
+ }
+ return interfaceToStatistics;
+#else
+ return {};
+#endif
+}
+
+void SendSignal(const std::vector<int>& pids, const TString& signalName)
+{
+#ifdef _unix_
+ ValidateSignalName(signalName);
+ auto sig = FindSignalIdBySignalName(signalName);
+ for (int pid : pids) {
+ if (kill(pid, *sig) != 0 && errno != ESRCH) {
+ THROW_ERROR_EXCEPTION("Unable to kill process %d", pid)
+ << TError::FromSystem();
+ }
+ }
+#else
+ YT_UNIMPLEMENTED();
+#endif
+}
+
+std::optional<int> FindSignalIdBySignalName(const TString& signalName)
+{
+ static const THashMap<TString, int> SignalNameToNumber{
+ { "SIGTERM", SIGTERM },
+ { "SIGINT", SIGINT },
+ { "SIGALRM", SIGALRM },
+ { "SIGKILL", SIGKILL },
+#ifdef _unix_
+ { "SIGHUP", SIGHUP },
+ { "SIGUSR1", SIGUSR1 },
+ { "SIGUSR2", SIGUSR2 },
+ { "SIGURG", SIGURG },
+#endif
+ };
+
+ auto it = SignalNameToNumber.find(signalName);
+ return it == SignalNameToNumber.end() ? std::nullopt : std::make_optional(it->second);
+}
+
+void ValidateSignalName(const TString& signalName)
+{
+ auto signal = FindSignalIdBySignalName(signalName);
+ if (!signal) {
+ THROW_ERROR_EXCEPTION("Unsupported signal name %Qv", signalName);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMemoryMappingStatistics& TMemoryMappingStatistics::operator+=(const TMemoryMappingStatistics& rhs)
+{
+ Size += rhs.Size;
+ KernelPageSize += rhs.KernelPageSize;
+ MMUPageSize += rhs.MMUPageSize;
+ Rss += rhs.Rss;
+ Pss += rhs.Pss;
+ SharedClean += rhs.SharedClean;
+ SharedDirty += rhs.SharedDirty;
+ PrivateClean += rhs.PrivateClean;
+ PrivateDirty += rhs.PrivateDirty;
+ Referenced += rhs.Referenced;
+ Anonymous += rhs.Anonymous;
+ LazyFree += rhs.LazyFree;
+ AnonHugePages += rhs.AnonHugePages;
+ ShmemPmdMapped += rhs.ShmemPmdMapped;
+ SharedHugetlb += rhs.SharedHugetlb;
+ PrivateHugetlb += rhs.PrivateHugetlb;
+ Swap += rhs.Swap;
+ SwapPss += rhs.SwapPss;
+ Locked += rhs.Locked;
+
+ return *this;
+}
+
+TMemoryMappingStatistics operator+(TMemoryMappingStatistics lhs, const TMemoryMappingStatistics& rhs)
+{
+ lhs += rhs;
+ return lhs;
+}
+
+std::vector<TMemoryMapping> ParseMemoryMappings(const TString& rawSMaps)
+{
+ auto parseMemoryAmount = [] (const TString& strValue, const TString& unit) {
+ YT_VERIFY(unit == "kB");
+ auto value = FromString<ui64>(strValue);
+ return value * 1_KB;
+ };
+
+ std::vector<TMemoryMapping> memoryMappings;
+ for (const auto& token : StringSplitter(rawSMaps).Split('\n')) {
+ auto line = token.Token();
+ if (line.empty()) {
+ continue;
+ }
+
+ // TODO(gritukan): Remove it when smaps tracker will be more stable.
+ auto verify = [&] (bool condition) {
+ if (!condition) {
+ Cerr << "Failed to parse smaps: " << rawSMaps << Endl;
+ Cerr << "Failed line: " << line << Endl;
+ YT_LOG_ERROR("Failed to parse smaps (SMaps: %v)", rawSMaps);
+ YT_LOG_ERROR("Failed line (Line: %v)", line);
+ YT_ABORT();
+ }
+ };
+
+ std::vector<TString> words;
+ StringSplitter(line).SplitBySet(" \t").SkipEmpty().Collect(&words);
+
+ // Memory mapping description starts with boundary addresses which consists of lowercase
+ // letters and digits. Memory mapping properties descriptions starts with uppercase letter.
+ if (std::isupper(line[0])) {
+ auto property = words[0];
+ verify(property.back() == ':');
+ property.pop_back();
+
+ verify(!memoryMappings.empty());
+ auto& mapping = memoryMappings.back();
+ auto& statistics = mapping.Statistics;
+
+ if (property == "Size") {
+ verify(words.size() == 3);
+ statistics.Size = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "KernelPageSize") {
+ verify(words.size() == 3);
+ statistics.KernelPageSize = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "MMUPageSize") {
+ verify(words.size() == 3);
+ statistics.MMUPageSize = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Rss") {
+ verify(words.size() == 3);
+ statistics.Rss = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Pss") {
+ verify(words.size() == 3);
+ statistics.Pss = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Shared_Clean") {
+ verify(words.size() == 3);
+ statistics.SharedClean = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Shared_Dirty") {
+ verify(words.size() == 3);
+ statistics.SharedDirty = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Private_Clean") {
+ verify(words.size() == 3);
+ statistics.PrivateClean = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Private_Dirty") {
+ verify(words.size() == 3);
+ statistics.PrivateDirty = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Referenced") {
+ verify(words.size() == 3);
+ statistics.Referenced = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Anonymous") {
+ verify(words.size() == 3);
+ statistics.Anonymous = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "LazyFree") {
+ verify(words.size() == 3);
+ statistics.LazyFree = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "AnonHugePages") {
+ verify(words.size() == 3);
+ statistics.AnonHugePages = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "ShmemPmdMapped") {
+ verify(words.size() == 3);
+ statistics.ShmemPmdMapped = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Shared_Hugetlb") {
+ verify(words.size() == 3);
+ statistics.SharedHugetlb = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Private_Hugetlb") {
+ verify(words.size() == 3);
+ statistics.PrivateHugetlb = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Swap") {
+ verify(words.size() == 3);
+ statistics.Swap = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "SwapPss") {
+ verify(words.size() == 3);
+ statistics.SwapPss = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "Locked") {
+ verify(words.size() == 3);
+ statistics.Locked = parseMemoryAmount(words[1], words[2]);
+ } else if (property == "ProtectionKey") {
+ verify(words.size() == 2);
+ mapping.ProtectionKey = FromString<ui64>(words[1]);
+ } else if (property == "VmFlags") {
+ for (const auto& flag : words) {
+ if (auto optionalEnumFlag = TEnumTraits<EVMFlag>::FindValueByLiteral(to_upper(flag))) {
+ mapping.VMFlags |= *optionalEnumFlag;
+ } else {
+ // Unknown flag, do not crash.
+ }
+ }
+ } else {
+ // Unknown property, do not crash.
+ }
+ } else {
+ verify(words.size() >= 5);
+ TMemoryMapping memoryMapping;
+ {
+ TStringBuf addressRange = words[0];
+ TStringBuf start;
+ TStringBuf end;
+ verify(addressRange.TrySplit('-', start, end));
+ verify(TryIntFromString<16>(start, memoryMapping.Start));
+ verify(TryIntFromString<16>(end, memoryMapping.End));
+ }
+ {
+ const auto& permissions = words[1];
+ verify(permissions.size() == 4);
+ if (permissions[0] == 'r') {
+ memoryMapping.Permissions |= EMemoryMappingPermission::Read;
+ } else {
+ verify(permissions[0] == '-');
+ }
+ if (permissions[1] == 'w') {
+ memoryMapping.Permissions |= EMemoryMappingPermission::Write;
+ } else {
+ verify(permissions[1] == '-');
+ }
+ if (permissions[2] == 'x') {
+ memoryMapping.Permissions |= EMemoryMappingPermission::Execute;
+ } else {
+ verify(permissions[2] == '-');
+ }
+ if (permissions[3] == 'p') {
+ memoryMapping.Permissions |= EMemoryMappingPermission::Private;
+ } else {
+ verify(permissions[3] == 's');
+ memoryMapping.Permissions |= EMemoryMappingPermission::Shared;
+ }
+ }
+
+ verify(TryIntFromString<16>(words[2], memoryMapping.Offset));
+
+ {
+ TStringBuf device = words[3];
+ TStringBuf majorStr;
+ TStringBuf minorStr;
+ verify(device.TrySplit(':', majorStr, minorStr));
+ ui16 major;
+ ui16 minor;
+ verify(TryIntFromString<16>(majorStr, major));
+ verify(TryIntFromString<16>(minorStr, minor));
+ if (major != 0 || minor != 0) {
+#ifdef _linux_
+ memoryMapping.DeviceId = makedev(major, minor);
+#endif
+ }
+ }
+
+ if (words[4] != "0") {
+ // NB: Decimal number.
+ memoryMapping.INode = FromString<ui64>(words[4]);
+ }
+
+ if (words.size() >= 6) {
+ memoryMapping.Path = words[5];
+ }
+
+ memoryMappings.push_back(memoryMapping);
+ }
+ }
+
+ return memoryMappings;
+}
+
+std::vector<TMemoryMapping> GetProcessMemoryMappings(int pid)
+{
+#ifdef _linux_
+ auto rawSMaps = TFileInput{Format("/proc/%v/smaps", pid)}.ReadAll();
+ return ParseMemoryMappings(rawSMaps);
+#else
+ Y_UNUSED(pid);
+ return {};
+#endif
+}
+
+template <typename TField>
+static bool TryParseField(const TVector<TString>& fields, int index, TField& field)
+{
+ if (std::ssize(fields) <= index) {
+ return false;
+ }
+ return TryFromString(fields[index], field);
+}
+
+static bool TryParseField(const TVector<TString>& fields, int index, TDuration& field)
+{
+ i64 value = 0;
+ if (TryParseField(fields, index, value)) {
+ field = TDuration::MilliSeconds(value);
+ return true;
+ }
+ return false;
+}
+
+TDiskStat ParseDiskStat(const TString& statLine)
+{
+ auto buffer = SplitString(statLine, " ");
+ TDiskStat result;
+ TryParseField(buffer, 0, result.MajorNumber);
+ TryParseField(buffer, 1, result.MinorNumber);
+ TryParseField(buffer, 2, result.DeviceName);
+ TryParseField(buffer, 3, result.ReadsCompleted);
+ TryParseField(buffer, 4, result.ReadsMerged);
+ TryParseField(buffer, 5, result.SectorsRead);
+ TryParseField(buffer, 6, result.TimeSpentReading);
+ TryParseField(buffer, 7, result.WritesCompleted);
+ TryParseField(buffer, 8, result.WritesMerged);
+ TryParseField(buffer, 9, result.SectorsWritten);
+ TryParseField(buffer, 10, result.TimeSpentWriting);
+ TryParseField(buffer, 11, result.IOCurrentlyInProgress);
+ TryParseField(buffer, 12, result.TimeSpentDoingIO);
+ TryParseField(buffer, 13, result.WeightedTimeSpentDoingIO);
+ TryParseField(buffer, 14, result.DiscardsCompleted);
+ TryParseField(buffer, 15, result.DiscardsMerged);
+ TryParseField(buffer, 16, result.SectorsDiscarded);
+ TryParseField(buffer, 17, result.TimeSpentDiscarding);
+ return result;
+}
+
+THashMap<TString, TDiskStat> GetDiskStats()
+{
+#ifdef _linux_
+ THashMap<TString, TDiskStat> result;
+ static const TString path("/proc/diskstats");
+ TFileInput diskStatsFile(path);
+ auto data = diskStatsFile.ReadAll();
+ auto lines = SplitString(data, "\n");
+
+ for (const auto& line : lines) {
+ auto strippedLine = Strip(line);
+ if (strippedLine.empty()) {
+ continue;
+ }
+ auto parsed = ParseDiskStat(line);
+ result[parsed.DeviceName] = parsed;
+ }
+
+ return result;
+#else
+ return {};
+#endif
+}
+
+std::vector<TString> ListDisks()
+{
+#ifdef _linux_
+ std::vector<TString> disks;
+
+ for (const auto& entry : TDirIterator("/sys/block", TDirIterator::TOptions().SetMaxLevel(1))) {
+ if (entry.fts_info == FTS_D || entry.fts_info == FTS_DP) {
+ continue;
+ }
+ disks.push_back(entry.fts_name);
+ }
+
+ return disks;
+#else
+ return {};
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTaskDiskStatistics GetSelfThreadTaskDiskStatistics()
+{
+#ifdef _linux_
+ static const TString path = "/proc/thread-self/io";
+
+ TTaskDiskStatistics stat;
+
+ TIFStream ioFile(path);
+ for (TString line; ioFile.ReadLine(line); ) {
+ if (line.empty()) {
+ continue;
+ }
+
+ auto fields = SplitString(line, " ", 2);
+ if (fields.size() != 2) {
+ continue;
+ }
+
+ if (fields[0] == "read_bytes:") {
+ TryFromString(fields[1], stat.ReadBytes);
+ } else if (fields[0] == "write_bytes:") {
+ TryFromString(fields[1], stat.ReadBytes);
+ }
+ }
+
+ return stat;
+#else
+ return {};
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFile MemfdCreate(const TString& name)
+{
+#ifdef _linux_
+ int fd = memfd_create(name.c_str(), 0);
+ if (fd == -1) {
+ THROW_ERROR_EXCEPTION("Unable to create memfd")
+ << TError::FromSystem();
+ }
+
+ return TFile{fd};
+#else
+ Y_UNUSED(name);
+
+ THROW_ERROR_EXCEPTION("Not implemented");
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& GetLinuxKernelVersion()
+{
+#ifdef _linux_
+ static TString release = []() -> TString {
+ utsname buf{};
+ if (uname(&buf) != 0) {
+ return "unknown";
+ }
+
+ // buf.release is a '\0' terminated string.
+ return buf.release;
+ }();
+
+ return release;
+#else
+ static TString release = "unknown";
+ return release;
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/proc.h b/yt/yt/core/misc/proc.h
new file mode 100644
index 0000000000..2d52d0d209
--- /dev/null
+++ b/yt/yt/core/misc/proc.h
@@ -0,0 +1,367 @@
+#pragma once
+
+#include "common.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+#include <errno.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! NYT::TError::FromSystem adds this value to a system errno. The enum
+//! below lists several errno's that are used in our code.
+constexpr int LinuxErrorCodeBase = 4200;
+constexpr int LinuxErrorCodeCount = 2000;
+
+DEFINE_ENUM(ELinuxErrorCode,
+ ((NOENT) ((LinuxErrorCodeBase + ENOENT)))
+ ((IO) ((LinuxErrorCodeBase + EIO)))
+ ((ACCESS) ((LinuxErrorCodeBase + EACCES)))
+ ((NFILE) ((LinuxErrorCodeBase + ENFILE)))
+ ((MFILE) ((LinuxErrorCodeBase + EMFILE)))
+ ((NOSPC) ((LinuxErrorCodeBase + ENOSPC)))
+ ((PIPE) ((LinuxErrorCodeBase + EPIPE)))
+ ((CONNRESET) ((LinuxErrorCodeBase + ECONNRESET)))
+ ((TIMEDOUT) ((LinuxErrorCodeBase + ETIMEDOUT)))
+ ((CONNREFUSED) ((LinuxErrorCodeBase + ECONNREFUSED)))
+ ((DQUOT) ((LinuxErrorCodeBase + EDQUOT)))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsSystemErrorCode(TErrorCode errorCode);
+bool IsSystemError(const TError& error);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef _win_
+ using TFileDescriptor = SOCKET;
+ using uid_t = i64;
+#else
+ using TFileDescriptor = int;
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<int> ListPids();
+std::vector<int> GetPidsByUid(int uid = -1);
+std::vector<int> GetPidsUnderParent(int targetPid);
+std::optional<int> GetPidByChildNamespacePid(int childNamespacePid);
+
+//! Gets the resident set size of a process.
+/*!
+ \note If |pid == -1| then self RSS is returned.
+ */
+
+struct TMemoryUsage
+{
+ ui64 Rss;
+ ui64 Shared;
+};
+
+TMemoryUsage GetProcessMemoryUsage(int pid = -1);
+
+struct TProcessCgroup
+{
+ int HierarchyId;
+ TString ControllersName;
+ std::vector<TString> Controllers;
+ TString Path;
+};
+
+std::vector<TProcessCgroup> GetProcessCgroups(int pid = -1);
+
+struct TCgroupCpuStat
+{
+ ui64 NrPeriods = 0;
+ ui64 NrThrottled = 0;
+ ui64 ThrottledTime = 0;
+ ui64 WaitTime = 0;
+};
+
+TCgroupCpuStat GetCgroupCpuStat(
+ const TString& controllerName,
+ const TString& cgroupPath,
+ const TString& cgroupMountPoint = "/sys/fs/cgroup");
+
+struct TCgroupMemoryStat
+{
+ ui64 HierarchicalMemoryLimit = 0;
+
+ ui64 Cache = 0;
+ ui64 Rss = 0;
+ ui64 RssHuge = 0;
+ ui64 MappedFile = 0;
+ ui64 Dirty = 0;
+ ui64 Writeback = 0;
+};
+
+TCgroupMemoryStat GetCgroupMemoryStat(
+ const TString& cgroupPath,
+ const TString& cgroupMountPoint = "/sys/fs/cgroup");
+
+THashMap<TString, i64> GetVmstat();
+
+ui64 GetProcessCumulativeMajorPageFaults(int pid = -1);
+size_t GetCurrentProcessId();
+size_t GetCurrentThreadId();
+std::vector<size_t> GetCurrentProcessThreadIds();
+
+void ChownChmodDirectory(
+ const TString& path,
+ const std::optional<uid_t>& userId,
+ const std::optional<int>& permissions);
+
+void ChownChmodDirectoriesRecursively(
+ const TString& path,
+ const std::optional<uid_t>& userId,
+ const std::optional<int>& permissions);
+
+void SetThreadPriority(int tid, int priority);
+
+TString GetProcessName(int pid);
+std::vector<TString> GetProcessCommandLine(int pid);
+
+TError StatusToError(int status);
+
+#ifdef _unix_
+TError ProcessInfoToError(const siginfo_t& processInfo);
+#endif
+
+bool TryClose(TFileDescriptor fd, bool ignoreBadFD = true);
+void SafeClose(TFileDescriptor fd, bool ignoreBadFD = true);
+
+bool TryDup2(TFileDescriptor oldFD, TFileDescriptor newFD);
+void SafeDup2(TFileDescriptor oldFD, TFileDescriptor newFD);
+
+void SafeSetCloexec(TFileDescriptor fd);
+
+bool TryExecve(const char* path, const char* const* argv, const char* const* env);
+
+void SafeCreateStderrFile(TString fileName);
+
+//! Returns a pipe with CLOSE_EXEC flag.
+void SafePipe(TFileDescriptor fd[2]);
+
+TFileDescriptor SafeDup(TFileDescriptor fd);
+
+//! Returns a pty with CLOSE_EXEC flag on master channel.
+void SafeOpenPty(TFileDescriptor* masterFD, TFileDescriptor* slaveFD, int height, int width);
+void SafeLoginTty(TFileDescriptor fd);
+void SafeSetTtyWindowSize(TFileDescriptor slaveFD, int height, int width);
+
+bool TryMakeNonblocking(TFileDescriptor fd);
+void SafeMakeNonblocking(TFileDescriptor fd);
+
+bool TrySetUid(int uid);
+void SafeSetUid(int uid);
+
+TString SafeGetUsernameByUid(int uid);
+
+void SetUid(int uid);
+
+void CloseAllDescriptors(const std::vector<int>& exceptFor = std::vector<int>());
+
+int GetFileDescriptorCount();
+
+//! Return true iff ytserver was started with root permissions (e.g. via sudo or with suid bit).
+bool HasRootPermissions();
+
+struct TNetworkInterfaceStatistics
+{
+ struct TReceiveStatistics
+ {
+ ui64 Bytes = 0;
+ ui64 Packets = 0;
+ ui64 Errs = 0;
+ ui64 Drop = 0;
+ ui64 Fifo = 0;
+ ui64 Frame = 0;
+ ui64 Compressed = 0;
+ ui64 Multicast = 0;
+ };
+ struct TTransmitStatistics
+ {
+ ui64 Bytes = 0;
+ ui64 Packets = 0;
+ ui64 Errs = 0;
+ ui64 Drop = 0;
+ ui64 Fifo = 0;
+ ui64 Colls = 0;
+ ui64 Carrier = 0;
+ ui64 Compressed = 0;
+ };
+
+ TReceiveStatistics Rx;
+ TTransmitStatistics Tx;
+};
+
+using TNetworkInterfaceStatisticsMap = THashMap<TString, TNetworkInterfaceStatistics>;
+//! Returns a mapping from interface name to network statistics.
+TNetworkInterfaceStatisticsMap GetNetworkInterfaceStatistics();
+
+void SendSignal(const std::vector<int>& pids, const TString& signalName);
+std::optional<int> FindSignalIdBySignalName(const TString& signalName);
+void ValidateSignalName(const TString& signalName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! The following structures represents content of /proc/[PID]/smaps.
+//! Look into 'man 5 /proc' for the description.
+struct TMemoryMappingStatistics
+{
+ ui64 Size = 0;
+ ui64 KernelPageSize = 0;
+ ui64 MMUPageSize = 0;
+ ui64 Rss = 0;
+ ui64 Pss = 0;
+ ui64 SharedClean = 0;
+ ui64 SharedDirty = 0;
+ ui64 PrivateClean = 0;
+ ui64 PrivateDirty = 0;
+ ui64 Referenced = 0;
+ ui64 Anonymous = 0;
+ ui64 LazyFree = 0;
+ ui64 AnonHugePages = 0;
+ ui64 ShmemPmdMapped = 0;
+ ui64 SharedHugetlb = 0;
+ ui64 PrivateHugetlb = 0;
+ ui64 Swap = 0;
+ ui64 SwapPss = 0;
+ ui64 Locked = 0;
+
+ TMemoryMappingStatistics& operator+=(const TMemoryMappingStatistics& rhs);
+};
+
+TMemoryMappingStatistics operator+(TMemoryMappingStatistics lhs, const TMemoryMappingStatistics& rhs);
+
+DEFINE_BIT_ENUM(EMemoryMappingPermission,
+ ((None) (0x0000))
+ ((Read) (0x0001))
+ ((Write) (0x0002))
+ ((Execute) (0x0004))
+ ((Private) (0x0008))
+ ((Shared) (0x0010))
+);
+
+DEFINE_BIT_ENUM(EVMFlag,
+ ((None) (0x000000000))
+ ((RD) (0x000000001))
+ ((WR) (0x000000002))
+ ((EX) (0x000000004))
+ ((SH) (0x000000008))
+ ((MR) (0x000000010))
+ ((MW) (0x000000020))
+ ((ME) (0x000000040))
+ ((MS) (0x000000080))
+ ((GD) (0x000000100))
+ ((PF) (0x000000200))
+ ((DW) (0x000000400))
+ ((LO) (0x000000800))
+ ((IO) (0x000001000))
+ ((SR) (0x000002000))
+ ((RR) (0x000004000))
+ ((DC) (0x000008000))
+ ((DE) (0x000010000))
+ ((AC) (0x000020000))
+ ((NR) (0x000040000))
+ ((HT) (0x000080000))
+ ((NL) (0x000100000))
+ ((AR) (0x000200000))
+ ((DD) (0x000400000))
+ ((SD) (0x000800000))
+ ((MM) (0x001000000))
+ ((HG) (0x002000000))
+ ((NH) (0x004000000))
+ ((MG) (0x008000000))
+);
+
+struct TMemoryMapping
+{
+ ui64 Start = 0;
+ ui64 End = 0;
+
+ EMemoryMappingPermission Permissions = EMemoryMappingPermission::None;
+
+ ui64 Offset = 0;
+
+ std::optional<int> DeviceId;
+
+ std::optional<ui64> INode;
+
+ std::optional<TString> Path;
+
+ TMemoryMappingStatistics Statistics;
+
+ EVMFlag VMFlags = EVMFlag::None;
+
+ ui64 ProtectionKey = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TMemoryMapping> ParseMemoryMappings(const TString& rawSMaps);
+std::vector<TMemoryMapping> GetProcessMemoryMappings(int pid);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// See https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
+// for fields info.
+struct TDiskStat
+{
+ i32 MajorNumber = 0;
+ i32 MinorNumber = 0;
+ TString DeviceName;
+
+ i64 ReadsCompleted = 0;
+ i64 ReadsMerged = 0;
+ i64 SectorsRead = 0;
+ TDuration TimeSpentReading;
+
+ i64 WritesCompleted = 0;
+ i64 WritesMerged = 0;
+ i64 SectorsWritten = 0;
+ TDuration TimeSpentWriting;
+
+ i64 IOCurrentlyInProgress = 0;
+ TDuration TimeSpentDoingIO;
+ TDuration WeightedTimeSpentDoingIO;
+
+ i64 DiscardsCompleted = 0;
+ i64 DiscardsMerged = 0;
+ i64 SectorsDiscarded = 0;
+ TDuration TimeSpentDiscarding;
+};
+
+TDiskStat ParseDiskStat(const TString& statLine);
+
+//! DeviceName to stat info
+THashMap<TString, TDiskStat> GetDiskStats();
+std::vector<TString> ListDisks();
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTaskDiskStatistics
+{
+ i64 ReadBytes = 0;
+ i64 WriteBytes = 0;
+};
+
+TTaskDiskStatistics GetSelfThreadTaskDiskStatistics();
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFile MemfdCreate(const TString& name);
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& GetLinuxKernelVersion();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/property.h b/yt/yt/core/misc/property.h
new file mode 100644
index 0000000000..99c8a5b9cb
--- /dev/null
+++ b/yt/yt/core/misc/property.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/misc/property.h>
diff --git a/yt/yt/core/misc/protobuf_helpers-inl.h b/yt/yt/core/misc/protobuf_helpers-inl.h
new file mode 100644
index 0000000000..92a2af9f72
--- /dev/null
+++ b/yt/yt/core/misc/protobuf_helpers-inl.h
@@ -0,0 +1,586 @@
+#ifndef PROTOBUF_HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include protobuf_helpers.h"
+// For the sake of sane code completion.
+#include "protobuf_helpers.h"
+#endif
+
+#include "error.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DEFINE_TRIVIAL_PROTO_CONVERSIONS(type) \
+ inline void ToProto(type* serialized, type original) \
+ { \
+ *serialized = original; \
+ } \
+ \
+ inline void FromProto(type* original, type serialized) \
+ { \
+ *original = serialized; \
+ }
+
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(TString)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(i8)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(ui8)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(i16)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(ui16)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(i32)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(ui32)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(i64)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(ui64)
+DEFINE_TRIVIAL_PROTO_CONVERSIONS(bool)
+
+#undef DEFINE_TRIVIAL_PROTO_CONVERSIONS
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::int64* serialized, TDuration original)
+{
+ *serialized = original.MicroSeconds();
+}
+
+inline void FromProto(TDuration* original, ::google::protobuf::int64 serialized)
+{
+ *original = TDuration::MicroSeconds(serialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::int64* serialized, TInstant original)
+{
+ *serialized = original.MicroSeconds();
+}
+
+inline void FromProto(TInstant* original, ::google::protobuf::int64 serialized)
+{
+ *original = TInstant::MicroSeconds(serialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::uint64* serialized, TInstant original)
+{
+ *serialized = original.MicroSeconds();
+}
+
+inline void FromProto(TInstant* original, ::google::protobuf::uint64 serialized)
+{
+ *original = TInstant::MicroSeconds(serialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+typename std::enable_if_t<std::is_convertible_v<T*, ::google::protobuf::MessageLite*>> ToProto(
+ T* serialized,
+ const T& original)
+{
+ *serialized = original;
+}
+
+template <class T>
+typename std::enable_if_t<std::is_convertible_v<T*, ::google::protobuf::MessageLite*>> FromProto(
+ T* original,
+ const T& serialized)
+{
+ *original = serialized;
+}
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum && (!TEnumTraits<T>::IsBitEnum)
+void ToProto(int* serialized, T original)
+{
+ *serialized = static_cast<int>(original);
+}
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum && (!TEnumTraits<T>::IsBitEnum)
+void FromProto(T* original, int serialized)
+{
+ *original = static_cast<T>(serialized);
+}
+
+template <class T>
+ requires TEnumTraits<T>::IsBitEnum
+void ToProto(ui64* serialized, T original)
+{
+ *serialized = static_cast<ui64>(original);
+}
+
+template <class T>
+ requires TEnumTraits<T>::IsBitEnum
+void FromProto(T* original, ui64 serialized)
+{
+ *original = static_cast<T>(serialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T GetProtoExtension(const NProto::TExtensionSet& extensions)
+{
+ // Intentionally complex to take benefit of RVO.
+ T result;
+ i32 tag = TProtoExtensionTag<T>::Value;
+ bool found = false;
+ for (const auto& extension : extensions.extensions()) {
+ if (extension.tag() == tag) {
+ const auto& data = extension.data();
+ DeserializeProto(&result, TRef::FromString(data));
+ found = true;
+ break;
+ }
+ }
+ YT_VERIFY(found);
+ return result;
+}
+
+template <class T>
+bool HasProtoExtension(const NProto::TExtensionSet& extensions)
+{
+ i32 tag = TProtoExtensionTag<T>::Value;
+ for (const auto& extension : extensions.extensions()) {
+ if (extension.tag() == tag) {
+ return true;
+ }
+ }
+ return false;
+}
+
+template <class T>
+std::optional<T> FindProtoExtension(const NProto::TExtensionSet& extensions)
+{
+ std::optional<T> result;
+ i32 tag = TProtoExtensionTag<T>::Value;
+ for (const auto& extension : extensions.extensions()) {
+ if (extension.tag() == tag) {
+ const auto& data = extension.data();
+ result.emplace();
+ DeserializeProto(&*result, TRef::FromString(data));
+ break;
+ }
+ }
+ return result;
+}
+
+template <class T>
+void SetProtoExtension(NProto::TExtensionSet* extensions, const T& value)
+{
+ i32 tag = TProtoExtensionTag<T>::Value;
+ NYT::NProto::TExtension* extension = nullptr;
+ for (auto& currentExtension : *extensions->mutable_extensions()) {
+ if (currentExtension.tag() == tag) {
+ extension = &currentExtension;
+ break;
+ }
+ }
+ if (!extension) {
+ extension = extensions->add_extensions();
+ }
+
+ ui64 longSize = value.ByteSizeLong();
+ YT_VERIFY(longSize <= std::numeric_limits<ui32>::max());
+ ui32 size = static_cast<ui32>(longSize);
+ TString str;
+ str.resize(size);
+ YT_VERIFY(value.SerializeToArray(str.begin(), size));
+ extension->set_data(str);
+ extension->set_tag(tag);
+}
+
+template <class TProto, class TValue>
+std::enable_if_t<!std::is_same_v<TProto, TValue>, void>
+SetProtoExtension(NProto::TExtensionSet* extensions, const TValue& value)
+{
+ TProto proto;
+ ToProto(&proto, value);
+ SetProtoExtension(extensions, proto);
+}
+
+template <class T>
+bool RemoveProtoExtension(NProto::TExtensionSet* extensions)
+{
+ i32 tag = TProtoExtensionTag<T>::Value;
+ for (int index = 0; index < extensions->extensions_size(); ++index) {
+ const auto& currentExtension = extensions->extensions(index);
+ if (currentExtension.tag() == tag) {
+ // Make it the last one.
+ extensions->mutable_extensions()->SwapElements(index, extensions->extensions_size() - 1);
+ // And then drop.
+ extensions->mutable_extensions()->RemoveLast();
+ return true;
+ }
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class TSerializedArray, class TOriginalArray>
+void ToProtoArrayImpl(
+ TSerializedArray* serializedArray,
+ const TOriginalArray& originalArray)
+{
+ serializedArray->Clear();
+ serializedArray->Reserve(originalArray.size());
+ for (const auto& item : originalArray) {
+ ToProto(serializedArray->Add(), item);
+ }
+}
+
+template <class TOriginalArray, class TSerializedArray>
+void FromProtoArrayImpl(
+ TOriginalArray* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ originalArray->clear();
+ originalArray->resize(serializedArray.size());
+ for (int i = 0; i < serializedArray.size(); ++i) {
+ FromProto(&(*originalArray)[i], serializedArray.Get(i));
+ }
+}
+
+template <class TProtoPair, class TValue>
+typename std::enable_if_t<!std::is_trivial_v<TValue>> SetPairValueImpl(TProtoPair& pair, const TValue& value)
+{
+ ToProto(pair->mutable_value(), value);
+}
+
+template <class TProtoPair, class TValue>
+typename std::enable_if_t<std::is_trivial_v<TValue>> SetPairValueImpl(TProtoPair& pair, const TValue& value)
+{
+ pair->set_value(value);
+}
+
+template <class TSerializedArray, class T, class E, E Min, E Max>
+void ToProtoArrayImpl(
+ TSerializedArray* serializedArray,
+ const TEnumIndexedVector<E, T, Min, Max>& originalArray)
+{
+ serializedArray->Clear();
+ for (auto key : TEnumTraits<E>::GetDomainValues()) {
+ if (originalArray.IsDomainValue(key)) {
+ const auto& value = originalArray[key];
+ auto* pair = serializedArray->Add();
+ pair->set_key(static_cast<i32>(key));
+ SetPairValueImpl(pair, value);
+ }
+ }
+}
+
+template <class T, class E, E Min, E Max, class TSerializedArray>
+void FromProtoArrayImpl(
+ TEnumIndexedVector<E, T, Min, Max>* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ for (auto key : TEnumTraits<E>::GetDomainValues()) {
+ if (originalArray->IsDomainValue(key)) {
+ (*originalArray)[key] = T{};
+ }
+ }
+ for (const auto& pair : serializedArray) {
+ const auto& key = static_cast<E>(pair.key());
+ if (originalArray->IsDomainValue(key)) {
+ FromProto(&(*originalArray)[key], pair.value());
+ }
+ }
+}
+
+// Does not check for duplicates.
+template <class TOriginal, class TSerializedArray>
+void FromProtoArrayImpl(
+ THashSet<TOriginal>* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ originalArray->clear();
+ originalArray->reserve(serializedArray.size());
+ for (int i = 0; i < serializedArray.size(); ++i) {
+ originalArray->emplace(
+ FromProto<TOriginal>(serializedArray.Get(i)));
+ }
+}
+
+template <class TOriginal, class TSerializedArray>
+void CheckedFromProtoArrayImpl(
+ THashSet<TOriginal>* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ FromProtoArrayImpl(originalArray, serializedArray);
+
+ if (std::ssize(*originalArray) != serializedArray.size()) {
+ THROW_ERROR_EXCEPTION("Duplicate elements in a serialized hash set")
+ << TErrorAttribute("unique_element_count", originalArray->size())
+ << TErrorAttribute("total_element_count", serializedArray.size());
+ }
+}
+
+template <class TOriginal, class TSerializedArray>
+void FromProtoArrayImpl(
+ TMutableRange<TOriginal>* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ std::fill(originalArray->begin(), originalArray->end(), TOriginal());
+ // NB: Only takes items with known indexes. Be careful when protocol is changed.
+ for (int i = 0; i < std::ssize(serializedArray) && i < std::ssize(*originalArray); ++i) {
+ FromProto(&(*originalArray)[i], serializedArray.Get(i));
+ }
+}
+
+template <class TOriginal, class TSerializedArray, size_t N>
+void FromProtoArrayImpl(
+ std::array<TOriginal, N>* originalArray,
+ const TSerializedArray& serializedArray)
+{
+ for (int i = 0; i < std::ssize(serializedArray) && i < std::ssize(*originalArray); ++i) {
+ FromProto(&(*originalArray)[i], serializedArray.Get(i));
+ }
+}
+
+} // namespace NDetail
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const std::vector<TOriginal>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const std::vector<TOriginal>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal, size_t N>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const std::array<TOriginal, N>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal, size_t N>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const std::array<TOriginal, N>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal, size_t Size>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const TCompactVector<TOriginal, Size>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal, size_t Size>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const TCompactVector<TOriginal, Size>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ TRange<TOriginal> originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ TRange<TOriginal> originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class T, class E, E Min, E Max>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const TEnumIndexedVector<E, T, Min, Max>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class T, class E, E Min, E Max>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const TEnumIndexedVector<E, T, Min, Max>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const THashSet<TOriginal>& originalArray)
+{
+ NYT::NDetail::ToProtoArrayImpl(serializedArray, originalArray);
+}
+
+template <class TOriginalArray, class TSerialized>
+void FromProto(
+ TOriginalArray* originalArray,
+ const ::google::protobuf::RepeatedPtrField<TSerialized>& serializedArray)
+{
+ NYT::NDetail::FromProtoArrayImpl(originalArray, serializedArray);
+}
+
+template <class TOriginalArray, class TSerialized>
+void FromProto(
+ TOriginalArray* originalArray,
+ const ::google::protobuf::RepeatedField<TSerialized>& serializedArray)
+{
+ NYT::NDetail::FromProtoArrayImpl(originalArray, serializedArray);
+}
+
+// Throws if duplicate elements are found.
+template <class TOriginal, class TSerialized>
+void CheckedHashSetFromProto(
+ THashSet<TOriginal>* originalHashSet,
+ const ::google::protobuf::RepeatedPtrField<TSerialized>& serializedHashSet)
+{
+ NYT::NDetail::CheckedFromProtoArrayImpl(originalHashSet, serializedHashSet);
+}
+
+template <class TOriginal, class TSerialized>
+void CheckedHashSetFromProto(
+ THashSet<TOriginal>* originalHashSet,
+ const ::google::protobuf::RepeatedField<TSerialized>& serializedHashSet)
+{
+ NYT::NDetail::CheckedFromProtoArrayImpl(originalHashSet, serializedHashSet);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSerialized, class TOriginal, class... TArgs>
+TSerialized ToProto(const TOriginal& original, TArgs&&... args)
+{
+ TSerialized serialized;
+ ToProto(&serialized, original, std::forward<TArgs>(args)...);
+ return serialized;
+}
+
+template <class TOriginal, class TSerialized, class... TArgs>
+TOriginal FromProto(const TSerialized& serialized, TArgs&&... args)
+{
+ TOriginal original;
+ FromProto(&original, serialized, std::forward<TArgs>(args)...);
+ return original;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TProto>
+TRefCountedProto<TProto>::TRefCountedProto(const TRefCountedProto<TProto>& other)
+{
+ TProto::CopyFrom(other);
+ RegisterExtraSpace();
+}
+
+template <class TProto>
+TRefCountedProto<TProto>::TRefCountedProto(TRefCountedProto<TProto>&& other)
+{
+ TProto::Swap(&other);
+ RegisterExtraSpace();
+}
+
+template <class TProto>
+TRefCountedProto<TProto>::TRefCountedProto(const TProto& other)
+{
+ TProto::CopyFrom(other);
+ RegisterExtraSpace();
+}
+
+template <class TProto>
+TRefCountedProto<TProto>::TRefCountedProto(TProto&& other)
+{
+ TProto::Swap(&other);
+ RegisterExtraSpace();
+}
+
+template <class TProto>
+TRefCountedProto<TProto>::~TRefCountedProto()
+{
+ UnregisterExtraSpace();
+}
+
+template <class TProto>
+void TRefCountedProto<TProto>::RegisterExtraSpace()
+{
+ auto spaceUsed = TProto::SpaceUsed();
+ YT_ASSERT(static_cast<size_t>(spaceUsed) >= sizeof(TProto));
+ YT_ASSERT(ExtraSpace_ == 0);
+ ExtraSpace_ = TProto::SpaceUsed() - sizeof (TProto);
+ auto cookie = GetRefCountedTypeCookie<TRefCountedProto<TProto>>();
+ TRefCountedTrackerFacade::AllocateSpace(cookie, ExtraSpace_);
+}
+
+template <class TProto>
+void TRefCountedProto<TProto>::UnregisterExtraSpace()
+{
+ if (ExtraSpace_ != 0) {
+ auto cookie = GetRefCountedTypeCookie<TRefCountedProto<TProto>>();
+ TRefCountedTrackerFacade::FreeSpace(cookie, ExtraSpace_);
+ }
+}
+
+template <class TProto>
+i64 TRefCountedProto<TProto>::GetSize() const
+{
+ return sizeof(*this) + ExtraSpace_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// RepeatedField formatter
+template <class T>
+struct TValueFormatter<::google::protobuf::RepeatedField<T>>
+{
+ static void Do(TStringBuilderBase* builder, const ::google::protobuf::RepeatedField<T>& collection, TStringBuf /*format*/)
+ {
+ FormatRange(builder, collection, TDefaultFormatter());
+ }
+};
+
+// RepeatedPtrField formatter
+template <class T>
+struct TValueFormatter<::google::protobuf::RepeatedPtrField<T>>
+{
+ static void Do(TStringBuilderBase* builder, const ::google::protobuf::RepeatedPtrField<T>& collection, TStringBuf /*format*/)
+ {
+ FormatRange(builder, collection, TDefaultFormatter());
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSerialized, class T, class TTag>
+void FromProto(TStrongTypedef<T, TTag>* original, const TSerialized& serialized)
+{
+ FromProto(&original->Underlying(), serialized);
+}
+
+template <class TSerialized, class T, class TTag>
+void ToProto(TSerialized* serialized, const TStrongTypedef<T, TTag>& original)
+{
+ ToProto(serialized, original.Underlying());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/protobuf_helpers.cpp b/yt/yt/core/misc/protobuf_helpers.cpp
new file mode 100644
index 0000000000..3d03d784d8
--- /dev/null
+++ b/yt/yt/core/misc/protobuf_helpers.cpp
@@ -0,0 +1,542 @@
+#include "protobuf_helpers.h"
+#include "mpl.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/util/time_util.h>
+
+namespace NYT {
+
+using namespace google::protobuf::io;
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const NLogging::TLogger Logger("Serialize");
+
+struct TSerializedMessageTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+i32 CheckedCastToI32(ui64 length)
+{
+ if (length >= std::numeric_limits<i32>::max()) {
+ THROW_ERROR_EXCEPTION("Protobuf message size exceeds 2GB")
+ << TErrorAttribute("length", length);
+ }
+ return static_cast<i32>(length);
+}
+
+} // namespace
+
+void SerializeProtoToRef(
+ const google::protobuf::MessageLite& message,
+ TMutableRef ref,
+ [[maybe_unused]] bool partial)
+{
+#ifdef YT_VALIDATE_REQUIRED_PROTO_FIELDS
+ if (!partial && !message.IsInitialized()) {
+ YT_LOG_FATAL("Missing required protobuf fields (Error: %v)",
+ message.InitializationErrorString());
+ }
+#endif
+ auto* begin = reinterpret_cast<google::protobuf::uint8*>(ref.begin());
+ auto* end = reinterpret_cast<google::protobuf::uint8*>(ref.end());
+ YT_VERIFY(message.SerializeWithCachedSizesToArray(begin) == end);
+}
+
+TSharedRef SerializeProtoToRef(
+ const google::protobuf::MessageLite& message,
+ bool partial)
+{
+ auto size = CheckedCastToI32(message.ByteSizeLong());
+ auto data = TSharedMutableRef::Allocate<TSerializedMessageTag>(size, {.InitializeStorage = false});
+ SerializeProtoToRef(message, data, partial);
+ return data;
+}
+
+TString SerializeProtoToString(
+ const google::protobuf::MessageLite& message,
+ bool partial)
+{
+ auto size = CheckedCastToI32(message.ByteSizeLong());
+ auto data = TString::Uninitialized(size);
+ SerializeProtoToRef(message, TMutableRef(data.begin(), size), partial);
+ return data;
+}
+
+bool TryDeserializeProto(google::protobuf::MessageLite* message, TRef data)
+{
+ // See comments to CodedInputStream::SetTotalBytesLimit (libs/protobuf/io/coded_stream.h)
+ // to find out more about protobuf message size limits.
+ CodedInputStream codedInputStream(
+ reinterpret_cast<const ui8*>(data.Begin()),
+ static_cast<int>(data.Size()));
+ codedInputStream.SetTotalBytesLimit(data.Size() + 1);
+
+ // Raise recursion limit.
+ codedInputStream.SetRecursionLimit(1024);
+
+ return message->ParsePartialFromCodedStream(&codedInputStream);
+}
+
+void DeserializeProto(google::protobuf::MessageLite* message, TRef data)
+{
+ YT_VERIFY(TryDeserializeProto(message, data));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef SerializeProtoToRefWithEnvelope(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId,
+ bool partial)
+{
+ NYT::NProto::TSerializedMessageEnvelope envelope;
+ if (codecId != NCompression::ECodec::None) {
+ envelope.set_codec(static_cast<int>(codecId));
+ }
+
+ auto serializedMessage = SerializeProtoToRef(message, partial);
+
+ auto codec = NCompression::GetCodec(codecId);
+ auto compressedMessage = codec->Compress(serializedMessage);
+
+ TEnvelopeFixedHeader fixedHeader;
+ fixedHeader.EnvelopeSize = CheckedCastToI32(envelope.ByteSizeLong());
+ fixedHeader.MessageSize = static_cast<ui32>(compressedMessage.Size());
+
+ size_t totalSize =
+ sizeof (TEnvelopeFixedHeader) +
+ fixedHeader.EnvelopeSize +
+ fixedHeader.MessageSize;
+
+ auto data = TSharedMutableRef::Allocate<TSerializedMessageTag>(totalSize, {.InitializeStorage = false});
+
+ char* targetFixedHeader = data.Begin();
+ char* targetHeader = targetFixedHeader + sizeof (TEnvelopeFixedHeader);
+ char* targetMessage = targetHeader + fixedHeader.EnvelopeSize;
+
+ memcpy(targetFixedHeader, &fixedHeader, sizeof (fixedHeader));
+ YT_VERIFY(envelope.SerializeToArray(targetHeader, fixedHeader.EnvelopeSize));
+ memcpy(targetMessage, compressedMessage.Begin(), fixedHeader.MessageSize);
+
+ return data;
+}
+
+TString SerializeProtoToStringWithEnvelope(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId,
+ bool partial)
+{
+ if (codecId != NCompression::ECodec::None) {
+ // TODO(babenko): see YT-7865 for a related issue
+ return ToString(SerializeProtoToRefWithEnvelope(message, codecId, partial));
+ }
+
+ NYT::NProto::TSerializedMessageEnvelope envelope;
+
+ TEnvelopeFixedHeader fixedHeader;
+ fixedHeader.EnvelopeSize = CheckedCastToI32(envelope.ByteSizeLong());
+ fixedHeader.MessageSize = CheckedCastToI32(message.ByteSizeLong());
+
+ auto totalSize =
+ sizeof (fixedHeader) +
+ fixedHeader.EnvelopeSize +
+ fixedHeader.MessageSize;
+
+ auto data = TString::Uninitialized(totalSize);
+ char* ptr = data.begin();
+ ::memcpy(ptr, &fixedHeader, sizeof (fixedHeader));
+ ptr += sizeof (fixedHeader);
+ ptr = reinterpret_cast<char*>(envelope.SerializeWithCachedSizesToArray(reinterpret_cast<ui8*>(ptr)));
+ ptr = reinterpret_cast<char*>(message.SerializeWithCachedSizesToArray(reinterpret_cast<ui8*>(ptr)));
+ YT_ASSERT(ptr == data.end());
+
+ return data;
+}
+
+bool TryDeserializeProtoWithEnvelope(
+ google::protobuf::MessageLite* message,
+ TRef data)
+{
+ if (data.Size() < sizeof (TEnvelopeFixedHeader)) {
+ return false;
+ }
+
+ const auto* fixedHeader = reinterpret_cast<const TEnvelopeFixedHeader*>(data.Begin());
+ const char* sourceHeader = data.Begin() + sizeof (TEnvelopeFixedHeader);
+ if (fixedHeader->EnvelopeSize + sizeof (*fixedHeader) > data.Size()) {
+ return false;
+ }
+
+ const char* sourceMessage = sourceHeader + fixedHeader->EnvelopeSize;
+
+ NYT::NProto::TSerializedMessageEnvelope envelope;
+ if (!envelope.ParseFromArray(sourceHeader, fixedHeader->EnvelopeSize)) {
+ return false;
+ }
+
+ NCompression::ECodec codecId;
+ if (!TryEnumCast(envelope.codec(), &codecId)) {
+ return false;
+ }
+
+ if (fixedHeader->MessageSize + fixedHeader->EnvelopeSize + sizeof (*fixedHeader) > data.Size()) {
+ return false;
+ }
+
+ auto compressedMessage = TSharedRef(sourceMessage, fixedHeader->MessageSize, nullptr);
+
+ auto* codec = NCompression::GetCodec(codecId);
+ try {
+ auto serializedMessage = codec->Decompress(compressedMessage);
+
+ return TryDeserializeProto(message, serializedMessage);
+ } catch (const std::exception& ex) {
+ return false;
+ }
+}
+
+void DeserializeProtoWithEnvelope(
+ google::protobuf::MessageLite* message,
+ TRef data)
+{
+ YT_VERIFY(TryDeserializeProtoWithEnvelope(message, data));
+}
+
+TSharedRef SerializeProtoToRefWithCompression(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId,
+ bool partial)
+{
+ auto serializedMessage = SerializeProtoToRef(message, partial);
+ auto codec = NCompression::GetCodec(codecId);
+ return codec->Compress(serializedMessage);
+}
+
+bool TryDeserializeProtoWithCompression(
+ google::protobuf::MessageLite* message,
+ TRef data,
+ NCompression::ECodec codecId)
+{
+ auto compressedMessage = TSharedRef(data.Begin(), data.Size(), nullptr);
+ auto* codec = NCompression::GetCodec(codecId);
+ try {
+ auto serializedMessage = codec->Decompress(compressedMessage);
+ return TryDeserializeProto(message, serializedMessage);
+ } catch (const std::exception& ex) {
+ return false;
+ }
+}
+
+void DeserializeProtoWithCompression(
+ google::protobuf::MessageLite* message,
+ TRef data,
+ NCompression::ECodec codecId)
+{
+ YT_VERIFY(TryDeserializeProtoWithCompression(message, data, codecId));
+}
+
+TSharedRef PopEnvelope(const TSharedRef& data)
+{
+ TEnvelopeFixedHeader header;
+ if (data.Size() < sizeof(header)) {
+ THROW_ERROR_EXCEPTION("Fixed header is missing");
+ }
+
+ memcpy(&header, data.Begin(), sizeof(header));
+ if (header.EnvelopeSize != 0) {
+ THROW_ERROR_EXCEPTION("Envelope is not empty");
+ }
+
+ return data.Slice(sizeof(TEnvelopeFixedHeader), data.Size());
+}
+
+TSharedRef PushEnvelope(const TSharedRef& data)
+{
+ TEnvelopeFixedHeader header;
+ header.EnvelopeSize = 0;
+ header.MessageSize = data.Size();
+
+ auto headerRef = TSharedMutableRef::Allocate(sizeof(header));
+ memcpy(headerRef.Begin(), &header, sizeof(header));
+
+ return MergeRefsToRef<TDefaultSharedBlobTag>(std::vector<TSharedRef>{headerRef, data});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufExtensionRegistry
+ : public IProtobufExtensionRegistry
+{
+public:
+ void AddAction(TRegisterAction action) override
+ {
+ YT_VERIFY(State_ == EState::Uninitialized);
+
+ Actions_.push_back(std::move(action));
+ }
+
+ void RegisterDescriptor(const TProtobufExtensionDescriptor& descriptor) override
+ {
+ YT_VERIFY(State_ == EState::Initializing);
+
+ EmplaceOrCrash(ExtensionTagToExtensionDescriptor_, descriptor.Tag, descriptor);
+ EmplaceOrCrash(ExtensionNameToExtensionDescriptor_, descriptor.Name, descriptor);
+ }
+
+ const TProtobufExtensionDescriptor* FindDescriptorByTag(int tag) override
+ {
+ EnsureInitialized();
+
+ auto it = ExtensionTagToExtensionDescriptor_.find(tag);
+ return it == ExtensionTagToExtensionDescriptor_.end() ? nullptr : &it->second;
+ }
+
+ const TProtobufExtensionDescriptor* FindDescriptorByName(const TString& name) override
+ {
+ EnsureInitialized();
+
+ auto it = ExtensionNameToExtensionDescriptor_.find(name);
+ return it == ExtensionNameToExtensionDescriptor_.end() ? nullptr : &it->second;
+ }
+
+private:
+ enum class EState
+ {
+ Uninitialized,
+ Initializing,
+ Initialized
+ };
+
+ EState State_ = EState::Uninitialized;
+
+ THashMap<int, TProtobufExtensionDescriptor> ExtensionTagToExtensionDescriptor_;
+ THashMap<TString, TProtobufExtensionDescriptor> ExtensionNameToExtensionDescriptor_;
+
+ std::vector<TRegisterAction> Actions_;
+
+ void EnsureInitialized()
+ {
+ if (State_ == EState::Initialized) {
+ return;
+ }
+
+ YT_VERIFY(State_ == EState::Uninitialized);
+ State_ = EState::Initializing;
+
+ for (const auto& action : Actions_) {
+ action();
+ }
+ Actions_.clear();
+
+ State_ = EState::Initialized;
+ }
+};
+
+IProtobufExtensionRegistry* IProtobufExtensionRegistry::Get()
+{
+ return LeakySingleton<TProtobufExtensionRegistry>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Intermediate extension representation for proto<->yson converter.
+struct TExtension
+{
+ //! Extension tag.
+ int Tag;
+
+ //! Serialized extension message.
+ TString Data;
+};
+
+//! Intermediate extension set representation for proto<->yson converter.
+struct TExtensionSet
+{
+ std::vector<TExtension> Extensions;
+};
+
+void FromProto(TExtensionSet* extensionSet, const NYT::NProto::TExtensionSet& protoExtensionSet)
+{
+ for (const auto& protoExtension : protoExtensionSet.extensions()) {
+ // Do not parse unknown extensions.
+ if (IProtobufExtensionRegistry::Get()->FindDescriptorByTag(protoExtension.tag())) {
+ TExtension extension{
+ .Tag = protoExtension.tag(),
+ .Data = protoExtension.data()
+ };
+ extensionSet->Extensions.push_back(std::move(extension));
+ }
+ }
+}
+
+void ToProto(NYT::NProto::TExtensionSet* protoExtensionSet, const TExtensionSet& extensionSet)
+{
+ for (const auto& extension : extensionSet.Extensions) {
+ auto* protoExtension = protoExtensionSet->add_extensions();
+ protoExtension->set_tag(extension.Tag);
+ protoExtension->set_data(extension.Data);
+ }
+}
+
+void Serialize(const TExtensionSet& extensionSet, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .DoMapFor(extensionSet.Extensions, [&] (TFluentMap fluent, const TExtension& extension) {
+ const auto* extensionDescriptor = IProtobufExtensionRegistry::Get()->FindDescriptorByTag(extension.Tag);
+ YT_VERIFY(extensionDescriptor);
+
+ fluent
+ .Item(extensionDescriptor->Name)
+ .Do([&] (TFluentAny fluent) {
+ const auto& data = extension.Data;
+ ArrayInputStream inputStream(data.data(), data.size());
+ ParseProtobuf(
+ fluent.GetConsumer(),
+ &inputStream,
+ ReflectProtobufMessageType(extensionDescriptor->MessageDescriptor));
+ });
+ });
+}
+
+void Deserialize(TExtensionSet& extensionSet, NYTree::INodePtr node)
+{
+ auto mapNode = node->AsMap();
+ for (const auto& [name, value] : mapNode->GetChildren()) {
+ const auto* extensionDescriptor = IProtobufExtensionRegistry::Get()->FindDescriptorByName(name);
+ // Do not parse unknown extensions.
+ if (!extensionDescriptor) {
+ continue;
+ }
+ auto& extension = extensionSet.Extensions.emplace_back();
+ extension.Tag = extensionDescriptor->Tag;
+
+ StringOutputStream stream(&extension.Data);
+ auto writer = CreateProtobufWriter(
+ &stream,
+ ReflectProtobufMessageType(extensionDescriptor->MessageDescriptor));
+ VisitTree(value, writer.get(), /*stable=*/false);
+ }
+}
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_REPRESENTATION(NYT::NProto::TExtensionSet, TExtensionSet)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBinaryProtoSerializer::Save(TStreamSaveContext& context, const ::google::protobuf::Message& message)
+{
+ auto data = SerializeProtoToRefWithEnvelope(message);
+ TSizeSerializer::Save(context, data.Size());
+ TRangeSerializer::Save(context, data);
+}
+
+namespace {
+
+TString DumpProto(::google::protobuf::Message& message)
+{
+ ::google::protobuf::TextFormat::Printer printer;
+ printer.SetSingleLineMode(true);
+ TString result;
+ YT_VERIFY(printer.PrintToString(message, &result));
+ return result;
+}
+
+} // namespace
+
+void TBinaryProtoSerializer::Load(TStreamLoadContext& context, ::google::protobuf::Message& message)
+{
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ auto data = TSharedMutableRef::Allocate(size, {.InitializeStorage = false});
+
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ TRangeSerializer::Load(context, data);
+ }
+
+ DeserializeProtoWithEnvelope(&message, data);
+
+ SERIALIZATION_DUMP_WRITE(context, "proto[%v] %v", size, DumpProto(message));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FilterProtoExtensions(
+ NYT::NProto::TExtensionSet* target,
+ const NYT::NProto::TExtensionSet& source,
+ const THashSet<int>& tags)
+{
+ target->Clear();
+ for (const auto& extension : source.extensions()) {
+ if (tags.find(extension.tag()) != tags.end()) {
+ *target->add_extensions() = extension;
+ }
+ }
+}
+
+void FilterProtoExtensions(
+ NYT::NProto::TExtensionSet* inplace,
+ const THashSet<int>& tags)
+{
+ auto tmp = std::move(*inplace);
+ FilterProtoExtensions(inplace, tmp, tags);
+}
+
+NYT::NProto::TExtensionSet FilterProtoExtensions(
+ const NYT::NProto::TExtensionSet& source,
+ const THashSet<int>& tags)
+{
+ NYT::NProto::TExtensionSet target;
+ FilterProtoExtensions(&target, source, tags);
+ return target;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashSet<int> GetExtensionTagSet(const NYT::NProto::TExtensionSet& source)
+{
+ THashSet<int> tags;
+ for (const auto& extension : source.extensions()) {
+ tags.insert(extension.tag());
+ }
+ return tags;
+}
+
+std::optional<TString> FindExtensionName(int tag)
+{
+ const auto* extensionDescriptor = IProtobufExtensionRegistry::Get()->FindDescriptorByTag(tag);
+ if (!extensionDescriptor) {
+ return std::nullopt;
+ }
+ return extensionDescriptor->Name;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+google::protobuf::Timestamp GetProtoNow()
+{
+ // Unfortunately TimeUtil::GetCurrentTime provides only one second accuracy, so we use TInstant::Now.
+ return google::protobuf::util::TimeUtil::MicrosecondsToTimestamp(TInstant::Now().MicroSeconds());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/protobuf_helpers.h b/yt/yt/core/misc/protobuf_helpers.h
new file mode 100644
index 0000000000..4c8c8532cc
--- /dev/null
+++ b/yt/yt/core/misc/protobuf_helpers.h
@@ -0,0 +1,436 @@
+#pragma once
+
+#include "guid.h"
+#include "mpl.h"
+#include "optional.h"
+#include "object_pool.h"
+#include "range.h"
+#include "serialize.h"
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt_proto/yt/core/misc/proto/guid.pb.h>
+#include <yt/yt_proto/yt/core/misc/proto/protobuf_helpers.pb.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/misc/preprocessor.h>
+
+#include <google/protobuf/message.h>
+#include <google/protobuf/repeated_field.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::int64* serialized, TDuration original);
+inline void FromProto(TDuration* original, ::google::protobuf::int64 serialized);
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::int64* serialized, TInstant original);
+inline void FromProto(TInstant* original, ::google::protobuf::int64 serialized);
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void ToProto(::google::protobuf::uint64* serialized, TInstant original);
+inline void FromProto(TInstant* original, ::google::protobuf::uint64 serialized);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+typename std::enable_if<std::is_convertible_v<T*, ::google::protobuf::MessageLite*>, void>::type ToProto(
+ T* serialized,
+ const T& original);
+template <class T>
+typename std::enable_if<std::is_convertible_v<T*, ::google::protobuf::MessageLite*>, void>::type FromProto(
+ T* original,
+ const T& serialized);
+
+template <class T>
+ requires TEnumTraits<T>::IsEnum && (!TEnumTraits<T>::IsBitEnum)
+void ToProto(int* serialized, T original);
+template <class T>
+ requires TEnumTraits<T>::IsEnum && (!TEnumTraits<T>::IsBitEnum)
+void FromProto(T* original, int serialized);
+
+template <class T>
+ requires TEnumTraits<T>::IsBitEnum
+void ToProto(ui64* serialized, T original);
+template <class T>
+ requires TEnumTraits<T>::IsBitEnum
+void FromProto(T* original, ui64 serialized);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const std::vector<TOriginal>& originalArray);
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const std::vector<TOriginal>& originalArray);
+
+template <class TSerialized, class TOriginal, size_t N>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const std::array<TOriginal, N>& originalArray);
+
+template <class TSerialized, class TOriginal, size_t N>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const std::array<TOriginal, N>& originalArray);
+
+template <class TSerialized, class TOriginal, size_t Size>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const TCompactVector<TOriginal, Size>& originalArray);
+
+template <class TSerialized, class TOriginal, size_t Size>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const TCompactVector<TOriginal, Size>& originalArray);
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const THashSet<TOriginal>& originalArray);
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const THashSet<TOriginal>& originalArray);
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ TRange<TOriginal> originalArray);
+
+template <class TSerialized, class TOriginal>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ TRange<TOriginal> originalArray);
+
+template <class TOriginalArray, class TSerialized>
+void FromProto(
+ TOriginalArray* originalArray,
+ const ::google::protobuf::RepeatedPtrField<TSerialized>& serializedArray);
+
+template <class TOriginalArray, class TSerialized>
+void FromProto(
+ TOriginalArray* originalArray,
+ const ::google::protobuf::RepeatedField<TSerialized>& serializedArray);
+
+template <class TOriginal, class TSerialized>
+void CheckedHashSetFromProto(
+ THashSet<TOriginal>* originalHashSet,
+ const ::google::protobuf::RepeatedPtrField<TSerialized>& serializedHashSet);
+
+template <class TOriginal, class TSerialized>
+void CheckedHashSetFromProto(
+ THashSet<TOriginal>* originalHashSet,
+ const ::google::protobuf::RepeatedField<TSerialized>& serializedHashSet);
+
+template <class TSerialized, class T, class E, E Min, E Max>
+void ToProto(
+ ::google::protobuf::RepeatedPtrField<TSerialized>* serializedArray,
+ const TEnumIndexedVector<E, T, Min, Max>& originalArray);
+
+template <class TSerialized, class T, class E, E Min, E Max>
+void ToProto(
+ ::google::protobuf::RepeatedField<TSerialized>* serializedArray,
+ const TEnumIndexedVector<E, T, Min, Max>& originalArray);
+
+template <class TSerialized, class T, class TTag>
+void FromProto(TStrongTypedef<T, TTag>* original, const TSerialized& serialized);
+
+template <class TSerialized, class T, class TTag>
+void ToProto(TSerialized* serialized, const TStrongTypedef<T, TTag>& original);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSerialized, class TOriginal, class... TArgs>
+TSerialized ToProto(const TOriginal& original, TArgs&&... args);
+
+template <class TOriginal, class TSerialized, class... TArgs>
+TOriginal FromProto(const TSerialized& serialized, TArgs&&... args);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#pragma pack(push, 4)
+
+struct TEnvelopeFixedHeader
+{
+ ui32 EnvelopeSize;
+ ui32 MessageSize;
+};
+
+#pragma pack(pop)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Serializes a protobuf message to pre-allocated TMutableRef.
+//! The caller is responsible for providing #ref of a suitable size.
+//! Fails on error.
+void SerializeProtoToRef(
+ const google::protobuf::MessageLite& message,
+ TMutableRef ref,
+ bool partial = true);
+
+//! Serializes a protobuf message to TSharedRef.
+//! Fails on error.
+TSharedRef SerializeProtoToRef(
+ const google::protobuf::MessageLite& message,
+ bool partial = true);
+
+//! Serializes a protobuf message to TString.
+//! Fails on error.
+TString SerializeProtoToString(
+ const google::protobuf::MessageLite& message,
+ bool partial = true);
+
+//! Deserializes a chunk of memory into a protobuf message.
+//! Returns |true| iff everything went well.
+bool TryDeserializeProto(
+ google::protobuf::MessageLite* message,
+ TRef data);
+
+//! Deserializes a chunk of memory into a protobuf message.
+//! Fails on error.
+void DeserializeProto(
+ google::protobuf::MessageLite* message,
+ TRef data);
+
+//! Serializes a given protobuf message and wraps it with envelope.
+//! Optionally compresses the serialized message.
+//! Fails on error.
+TSharedRef SerializeProtoToRefWithEnvelope(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId = NCompression::ECodec::None,
+ bool partial = true);
+
+//! \see SerializeProtoToRefWithEnvelope
+TString SerializeProtoToStringWithEnvelope(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId = NCompression::ECodec::None,
+ bool partial = true);
+
+//! Unwraps a chunk of memory obtained from #SerializeProtoToRefWithEnvelope
+//! and deserializes it into a protobuf message.
+//! Returns |true| iff everything went well.
+bool TryDeserializeProtoWithEnvelope(
+ google::protobuf::MessageLite* message,
+ TRef data);
+
+//! Unwraps a chunk of memory obtained from #SerializeProtoToRefWithEnvelope
+//! and deserializes it into a protobuf message.
+//! Fails on error.
+void DeserializeProtoWithEnvelope(
+ google::protobuf::MessageLite* message,
+ TRef data);
+
+//! Serializes a given protobuf message.
+//! Optionally compresses the serialized message.
+//! Fails on error.
+TSharedRef SerializeProtoToRefWithCompression(
+ const google::protobuf::MessageLite& message,
+ NCompression::ECodec codecId = NCompression::ECodec::None,
+ bool partial = true);
+
+//! Unwraps a chunk of memory obtained from #SerializeProtoToRefWithCompression,
+//! decompresses it with a given codec and deserializes it into a protobuf message.
+//! Returns |true| iff everything went well.
+bool TryDeserializeProtoWithCompression(
+ google::protobuf::MessageLite* message,
+ TRef data,
+ NCompression::ECodec codecId = NCompression::ECodec::None);
+
+//! Unwraps a chunk of memory obtained from #SerializeProtoToRefWithCompression,
+//! decompresses it with a given codec and deserializes it into a protobuf message.
+//! Fails on error.
+void DeserializeProtoWithCompression(
+ google::protobuf::MessageLite* message,
+ TRef data,
+ NCompression::ECodec codecId = NCompression::ECodec::None);
+
+TSharedRef PushEnvelope(const TSharedRef& data);
+TSharedRef PopEnvelope(const TSharedRef& data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBinaryProtoSerializer
+{
+ //! Serializes a given protobuf message into a given stream.
+ //! Throws an exception in case of error.
+ static void Save(TStreamSaveContext& context, const ::google::protobuf::Message& message);
+
+ //! Reads from a given stream protobuf message.
+ //! Throws an exception in case of error.
+ static void Load(TStreamLoadContext& context, ::google::protobuf::Message& message);
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ typename std::enable_if_t<std::is_convertible_v<T&, ::google::protobuf::Message&>>>
+{
+ using TSerializer = TBinaryProtoSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * YT Extension Set is a collection of |(tag, data)| pairs.
+ *
+ * Here |tag| is a unique integer identifier and |data| is a protobuf-serialized
+ * embedded message.
+ *
+ * In contrast to native Protobuf Extensions, ours are deserialized on-demand.
+ */
+
+//! Used to obtain an integer tag for a given type.
+/*!
+ * Specialized versions of this traits are generated with |DECLARE_PROTO_EXTENSION|.
+ */
+template <class T>
+struct TProtoExtensionTag;
+
+#define DECLARE_PROTO_EXTENSION(type, tag) \
+ template <> \
+ struct TProtoExtensionTag<type> \
+ { \
+ static constexpr i32 Value = tag; \
+ };
+
+struct TProtobufExtensionDescriptor
+{
+ const google::protobuf::Descriptor* MessageDescriptor;
+ const int Tag;
+ const TString Name;
+};
+
+struct IProtobufExtensionRegistry
+{
+ using TRegisterAction = std::function<void()>;
+
+ //! This method is assumed to be called during static initialization only.
+ //! We defer running actions until static protobuf descriptors are ready.
+ //! Accessing type descriptors during static initialization phase may cause UB.
+ virtual void AddAction(TRegisterAction action) = 0;
+
+ //! Registers protobuf extension for further conversions. Do not call
+ //! this method explicitly; use REGISTER_PROTO_EXTENSION macro that defers invocation
+ //! until static protobuf descriptors are ready.
+ virtual void RegisterDescriptor(const TProtobufExtensionDescriptor& descriptor) = 0;
+
+ //! Finds a descriptor by tag value.
+ virtual const TProtobufExtensionDescriptor* FindDescriptorByTag(int tag) = 0;
+
+ //! Finds a descriptor by name.
+ virtual const TProtobufExtensionDescriptor* FindDescriptorByName(const TString& name) = 0;
+
+ //! Returns the singleton instance.
+ static IProtobufExtensionRegistry* Get();
+};
+
+#define REGISTER_PROTO_EXTENSION(type, tag, name) \
+ YT_ATTRIBUTE_USED static const void* PP_ANONYMOUS_VARIABLE(RegisterProtoExtension) = [] { \
+ NYT::IProtobufExtensionRegistry::Get()->AddAction([] { \
+ const auto* descriptor = type::default_instance().GetDescriptor(); \
+ NYT::IProtobufExtensionRegistry::Get()->RegisterDescriptor({ \
+ .MessageDescriptor = descriptor, \
+ .Tag = tag, \
+ .Name = #name \
+ });\
+ }); \
+ return nullptr; \
+ } ();
+
+//! Finds and deserializes an extension of the given type. Fails if no matching
+//! extension is found.
+template <class T>
+T GetProtoExtension(const NYT::NProto::TExtensionSet& extensions);
+
+// Returns |true| iff an extension of a given type is present.
+template <class T>
+bool HasProtoExtension(const NYT::NProto::TExtensionSet& extensions);
+
+//! Finds and deserializes an extension of the given type. Returns null if no matching
+//! extension is found.
+template <class T>
+std::optional<T> FindProtoExtension(const NYT::NProto::TExtensionSet& extensions);
+
+//! Serializes and stores an extension.
+//! Overwrites any extension with the same tag (if exists).
+template <class T>
+void SetProtoExtension(NProto::TExtensionSet* extensions, const T& value);
+template <class TProto, class TValue>
+std::enable_if_t<!std::is_same_v<TProto, TValue>, void>
+SetProtoExtension(NProto::TExtensionSet* extensions, const TValue& value);
+
+//! Tries to remove the extension.
+//! Returns |true| iff the proper extension is found.
+template <class T>
+bool RemoveProtoExtension(NProto::TExtensionSet* extensions);
+
+//! Filters extensions leaving only those matching #tags set.
+void FilterProtoExtensions(
+ NYT::NProto::TExtensionSet* target,
+ const NYT::NProto::TExtensionSet& source,
+ const THashSet<int>& tags);
+void FilterProtoExtensions(
+ NYT::NProto::TExtensionSet* inplace,
+ const THashSet<int>& tags);
+NYT::NProto::TExtensionSet FilterProtoExtensions(
+ const NYT::NProto::TExtensionSet& source,
+ const THashSet<int>& tags);
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashSet<int> GetExtensionTagSet(const NYT::NProto::TExtensionSet& source);
+std::optional<TString> FindExtensionName(int tag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Wrapper that makes proto message ref-counted.
+template <class TProto>
+class TRefCountedProto
+ : public TRefCounted
+ , public TProto
+{
+public:
+ TRefCountedProto() = default;
+ TRefCountedProto(const TRefCountedProto<TProto>& other);
+ TRefCountedProto(TRefCountedProto<TProto>&& other);
+ explicit TRefCountedProto(const TProto& other);
+ explicit TRefCountedProto(TProto&& other);
+ ~TRefCountedProto();
+
+ i64 GetSize() const;
+
+private:
+ size_t ExtraSpace_ = 0;
+
+ void RegisterExtraSpace();
+ void UnregisterExtraSpace();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+google::protobuf::Timestamp GetProtoNow();
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This macro may be used to extract std::optional<T> from protobuf message field of type T.
+#define YT_PROTO_OPTIONAL(message, field) (((message).has_##field()) ? std::make_optional((message).field()) : std::nullopt)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define PROTOBUF_HELPERS_INL_H_
+#include "protobuf_helpers-inl.h"
+#undef PROTOBUF_HELPERS_INL_H_
diff --git a/yt/yt/core/misc/public.cpp b/yt/yt/core/misc/public.cpp
new file mode 100644
index 0000000000..54920e5a1f
--- /dev/null
+++ b/yt/yt/core/misc/public.cpp
@@ -0,0 +1,20 @@
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString YTCoreNoteName = "YT";
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ProcessErrorCodeFormatter(int code)
+{
+ return TEnumTraits<EProcessErrorCode>::ToString(static_cast<EProcessErrorCode>(code));
+}
+
+YT_DEFINE_ERROR_CODE_RANGE(10000, 10255, "NYT::EProcessErrorCode", ProcessErrorCodeFormatter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/public.h b/yt/yt/core/misc/public.h
new file mode 100644
index 0000000000..556fb71a02
--- /dev/null
+++ b/yt/yt/core/misc/public.h
@@ -0,0 +1,175 @@
+#pragma once
+
+#include "common.h"
+#include "error_code.h"
+
+// Google Protobuf forward declarations.
+namespace google::protobuf {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class Descriptor;
+class EnumDescriptor;
+class MessageLite;
+class Message;
+
+template <class Element>
+class RepeatedField;
+template <class Element>
+class RepeatedPtrField;
+
+class Timestamp;
+
+namespace io {
+
+class ZeroCopyInputStream;
+class ZeroCopyOutputStream;
+
+} // namespace io
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace google::protobuf
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TError;
+class TBloomFilter;
+class TDataStatistics;
+class TExtensionSet;
+
+} // namespace NProto
+
+struct TGuid;
+
+template <class T>
+class TErrorOr;
+
+using TError = TErrorOr<void>;
+
+template <class T>
+struct TErrorTraits;
+
+class TStreamLoadContext;
+class TStreamSaveContext;
+
+struct TEntitySerializationContext;
+class TEntityStreamSaveContext;
+class TEntityStreamLoadContext;
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion = int>
+class TCustomPersistenceContext;
+
+using TStreamPersistenceContext = TCustomPersistenceContext<
+ TStreamSaveContext,
+ TStreamLoadContext
+>;
+
+struct TValueBoundComparer;
+struct TValueBoundSerializer;
+
+template <class T, class C, class = void>
+struct TSerializerTraits;
+
+template <class TKey, class TComparer>
+class TSkipList;
+
+class TBlobOutput;
+
+class TStringBuilderBase;
+class TStringBuilder;
+
+DECLARE_REFCOUNTED_STRUCT(IDigest)
+DECLARE_REFCOUNTED_STRUCT(IPersistentDigest)
+
+DECLARE_REFCOUNTED_CLASS(TSlruCacheDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TSlruCacheConfig)
+
+DECLARE_REFCOUNTED_CLASS(TAsyncExpiringCacheDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TAsyncExpiringCacheConfig)
+
+DECLARE_REFCOUNTED_CLASS(TLogDigestConfig)
+DECLARE_REFCOUNTED_CLASS(THistogramDigestConfig)
+
+DECLARE_REFCOUNTED_CLASS(THistoricUsageConfig)
+
+class TSignalRegistry;
+
+class TBloomFilterBuilder;
+class TBloomFilter;
+
+using TChecksum = ui64;
+
+constexpr TChecksum NullChecksum = 0;
+
+template <class T, size_t N>
+class TCompactVector;
+
+class TRef;
+class TMutableRef;
+
+template <class TProto>
+class TRefCountedProto;
+
+DECLARE_REFCOUNTED_CLASS(TProcessBase)
+
+const ui32 YTCoreNoteType = 0x5f59545f; // = hex("_YT_") ;)
+extern const TString YTCoreNoteName;
+
+template <class T>
+class TInternRegistry;
+
+template <class T>
+using TInternRegistryPtr = TIntrusivePtr<TInternRegistry<T>>;
+
+template <class T>
+class TInternedObjectData;
+
+template <class T>
+using TInternedObjectDataPtr = TIntrusivePtr<TInternedObjectData<T>>;
+
+template <class T>
+class TInternedObject;
+
+DECLARE_REFCOUNTED_STRUCT(IMemoryUsageTracker)
+
+class TStatistics;
+class TSummary;
+
+template <class TTask>
+struct IFairScheduler;
+
+template <class TTask>
+using IFairSchedulerPtr = TIntrusivePtr<IFairScheduler<TTask>>;
+
+DECLARE_REFCOUNTED_CLASS(TAdaptiveHedgingManagerConfig)
+DECLARE_REFCOUNTED_STRUCT(IHedgingManager)
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((OK) (0))
+ ((Generic) (1))
+ ((Canceled) (2))
+ ((Timeout) (3))
+ ((FutureCombinerFailure) (4))
+ ((FutureCombinerShortcut)(5))
+);
+
+DEFINE_ENUM(EProcessErrorCode,
+ ((NonZeroExitCode) (10000))
+ ((Signal) (10001))
+ ((CannotResolveBinary)(10002))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(IMemoryReferenceTracker)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/random-inl.h b/yt/yt/core/misc/random-inl.h
new file mode 100644
index 0000000000..7a116ed56c
--- /dev/null
+++ b/yt/yt/core/misc/random-inl.h
@@ -0,0 +1,72 @@
+#ifndef RANDOM_INL_H_
+#error "Direct inclusion of this file is not allowed, include random.h"
+// For the sake of sane code completion.
+#include "random.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TRandomGenerator::TRandomGenerator()
+ : Current_(0)
+{ }
+
+inline TRandomGenerator::TRandomGenerator(ui64 seed)
+ : Current_(seed)
+{ }
+
+template <>
+inline double TRandomGenerator::Generate()
+{
+ return GenerateDouble();
+}
+
+template <>
+inline float TRandomGenerator::Generate()
+{
+ return GenerateDouble();
+}
+
+template <>
+inline long double TRandomGenerator::Generate()
+{
+ return GenerateDouble();
+}
+
+template <class T>
+T TRandomGenerator::Generate()
+{
+ return static_cast<T>(GenerateInteger());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TForwardIterator, class TOutputIterator, class TGenerator>
+TOutputIterator RandomSampleN(
+ TForwardIterator begin,
+ TForwardIterator end,
+ TOutputIterator output,
+ size_t n,
+ TGenerator&& generator)
+{
+ size_t remaining = std::distance(begin, end);
+ size_t m = Min(n, remaining);
+
+ while (m > 0) {
+ if (generator(remaining) < m) {
+ *output = *begin;
+ ++output;
+ --m;
+ }
+
+ --remaining;
+ ++begin;
+ }
+
+ return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/random.cpp b/yt/yt/core/misc/random.cpp
new file mode 100644
index 0000000000..aeaa634e1b
--- /dev/null
+++ b/yt/yt/core/misc/random.cpp
@@ -0,0 +1,23 @@
+#include "random.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui64 TRandomGenerator::GenerateInteger()
+{
+ // Parameters are taken from http://en.wikipedia.org/wiki/Linear_congruential_generator
+ // Taking modulo 2^64 is implemented via overflow in ui64
+ Current_ = 6364136223846793005ll * Current_ + 1442695040888963407ll;
+ return Current_;
+}
+
+double TRandomGenerator::GenerateDouble()
+{
+ // This formula is taken from util/random/mersenne64.h
+ return (GenerateInteger() >> 11) * (1.0 / 9007199254740992.0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/random.h b/yt/yt/core/misc/random.h
new file mode 100644
index 0000000000..a8373958a3
--- /dev/null
+++ b/yt/yt/core/misc/random.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "common.h"
+
+#include <util/generic/noncopyable.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A fully deterministic pseudo-random number generator.
+class TRandomGenerator final
+ : public TNonCopyable
+{
+public:
+ TRandomGenerator();
+ explicit TRandomGenerator(ui64 seed);
+
+ template <class T>
+ T Generate();
+
+private:
+ ui64 Current_;
+
+ ui64 GenerateInteger();
+ double GenerateDouble();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TGenerator is supposed to implement operator(), which returns
+// uniformly-distributed integers in the range [0, #max) given argument #max.
+template <class TForwardIterator, class TOutputIterator, class TGenerator>
+TOutputIterator RandomSampleN(
+ TForwardIterator begin,
+ TForwardIterator end,
+ TOutputIterator output,
+ size_t n,
+ TGenerator&& generator);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define RANDOM_INL_H_
+#include "random-inl.h"
+#undef RANDOM_INL_H_
diff --git a/yt/yt/core/misc/range.h b/yt/yt/core/misc/range.h
new file mode 100644
index 0000000000..6b08660f75
--- /dev/null
+++ b/yt/yt/core/misc/range.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/range.h>
diff --git a/yt/yt/core/misc/range_formatters.h b/yt/yt/core/misc/range_formatters.h
new file mode 100644
index 0000000000..4bfdd13a2c
--- /dev/null
+++ b/yt/yt/core/misc/range_formatters.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "range.h"
+#include "shared_range.h"
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TRange formatter
+template <class T>
+struct TValueFormatter<TRange<T>, void>
+{
+ static void Do(TStringBuilderBase* builder, TRange<T> range, TStringBuf /*format*/)
+ {
+ FormatRange(builder, range, TDefaultFormatter());
+ }
+};
+
+// TSharedRange formatter
+template <class T>
+struct TValueFormatter<TSharedRange<T>>
+{
+ static void Do(TStringBuilderBase* builder, const TSharedRange<T>& range, TStringBuf /*format*/)
+ {
+ FormatRange(builder, range, TDefaultFormatter());
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_counted.h b/yt/yt/core/misc/ref_counted.h
new file mode 100644
index 0000000000..9d5675f4ff
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/ref_counted.h>
diff --git a/yt/yt/core/misc/ref_counted_tracker-inl.h b/yt/yt/core/misc/ref_counted_tracker-inl.h
new file mode 100644
index 0000000000..e7c4b6c52b
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker-inl.h
@@ -0,0 +1,159 @@
+#ifndef REF_COUNTED_TRACKER_INL_H_
+#error "Direct inclusion of this file is not allowed, include ref_counted_tracker.h"
+// For the sake of sane code completion.
+#include "ref_counted_tracker.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ENUMERATE_SLOT_FIELDS() \
+ XX(ObjectsAllocated) \
+ XX(ObjectsFreed) \
+ XX(TagObjectsAllocated) \
+ XX(TagObjectsFreed) \
+ XX(SpaceSizeAllocated) \
+ XX(SpaceSizeFreed)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRefCountedTracker::TLocalSlot
+{
+ #define XX(name) size_t name = 0;
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRefCountedTracker::TGlobalSlot
+{
+ #define XX(name) std::atomic<size_t> name = {0};
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+
+ TGlobalSlot() = default;
+
+ TGlobalSlot(TGlobalSlot&& other)
+ {
+ #define XX(name) name = other.name.load();
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+ }
+
+ TGlobalSlot& operator += (const TLocalSlot& rhs)
+ {
+ #define XX(name) name += rhs.name;
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+ return *this;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRefCountedTracker::TNamedSlot
+{
+public:
+ TNamedSlot(const TKey& key, size_t objectSize);
+
+ TRefCountedTypeKey GetTypeKey() const;
+ const TSourceLocation& GetLocation() const;
+
+ TString GetTypeName() const;
+ TString GetFullName() const;
+
+ size_t GetObjectsAllocated() const;
+ size_t GetObjectsFreed() const;
+ size_t GetObjectsAlive() const;
+
+ size_t GetBytesAllocated() const;
+ size_t GetBytesFreed() const;
+ size_t GetBytesAlive() const;
+
+ TRefCountedTrackerStatistics::TNamedSlotStatistics GetStatistics() const;
+
+ TNamedSlot& operator += (const TLocalSlot& rhs)
+ {
+ #define XX(name) name ## _ += rhs.name;
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+ return *this;
+ }
+
+ TNamedSlot& operator += (const TGlobalSlot& rhs)
+ {
+ #define XX(name) name ## _ += rhs.name.load();
+ ENUMERATE_SLOT_FIELDS()
+ #undef XX
+ return *this;
+ }
+
+private:
+ TKey Key_;
+ size_t ObjectSize_;
+
+ size_t ObjectsAllocated_ = 0;
+ size_t ObjectsFreed_ = 0;
+ size_t TagObjectsAllocated_ = 0;
+ size_t TagObjectsFreed_ = 0;
+ size_t SpaceSizeAllocated_ = 0;
+ size_t SpaceSizeFreed_ = 0;
+
+ static size_t ClampNonnegative(size_t allocated, size_t freed);
+};
+
+#undef ENUMERATE_SLOT_FIELDS
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TRefCountedTracker* TRefCountedTracker::Get()
+{
+ return LeakySingleton<TRefCountedTracker>();
+}
+
+#define INCREMENT_COUNTER(fallback, name, delta) \
+ auto index = cookie.Underlying(); \
+ YT_ASSERT(index >= 0); \
+ if (Y_UNLIKELY(index >= LocalSlotsSize_)) { \
+ Get()->fallback; \
+ } else { \
+ LocalSlotsBegin_[index].name += delta; \
+ }
+
+Y_FORCE_INLINE void TRefCountedTracker::AllocateInstance(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER(AllocateInstanceSlow(cookie), ObjectsAllocated, 1)
+}
+
+Y_FORCE_INLINE void TRefCountedTracker::FreeInstance(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER(FreeInstanceSlow(cookie), ObjectsFreed, 1)
+}
+
+Y_FORCE_INLINE void TRefCountedTracker::AllocateTagInstance(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER(AllocateTagInstanceSlow(cookie), TagObjectsAllocated, 1)
+}
+
+Y_FORCE_INLINE void TRefCountedTracker::FreeTagInstance(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER(FreeTagInstanceSlow(cookie), TagObjectsFreed, 1)
+}
+
+Y_FORCE_INLINE void TRefCountedTracker::AllocateSpace(TRefCountedTypeCookie cookie, size_t space)
+{
+ INCREMENT_COUNTER(AllocateSpaceSlow(cookie, space), SpaceSizeAllocated, space)
+}
+
+Y_FORCE_INLINE void TRefCountedTracker::FreeSpace(TRefCountedTypeCookie cookie, size_t space)
+{
+ INCREMENT_COUNTER(FreeSpaceSlow(cookie, space), SpaceSizeFreed, space)
+}
+
+#undef INCREMENT_COUNTER
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_counted_tracker.cpp b/yt/yt/core/misc/ref_counted_tracker.cpp
new file mode 100644
index 0000000000..617ce2cd26
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker.cpp
@@ -0,0 +1,467 @@
+#include "ref_counted_tracker.h"
+
+#include <util/system/type_name.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <library/cpp/yt/string/format.h>
+
+#include <library/cpp/yt/memory/memory_tag.h>
+
+#include <algorithm>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRefCountedTrackerStatistics::TStatistics& TRefCountedTrackerStatistics::TStatistics::operator+= (
+ const TRefCountedTrackerStatistics::TStatistics& rhs)
+{
+ ObjectsAllocated += rhs.ObjectsAllocated;
+ ObjectsFreed += rhs.ObjectsFreed;
+ ObjectsAlive += rhs.ObjectsAlive;
+ BytesAllocated += rhs.BytesAllocated;
+ BytesFreed += rhs.BytesFreed;
+ BytesAlive += rhs.BytesAlive;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TRefCountedTracker::TKey::operator==(const TKey& other) const
+{
+ return
+ TypeKey == other.TypeKey &&
+ Location == other.Location;
+}
+
+bool TRefCountedTracker::TKey::operator<(const TKey& other) const
+{
+ if (TypeKey < other.TypeKey) {
+ return true;
+ }
+ if (other.TypeKey < TypeKey) {
+ return false;
+ }
+ if (Location < other.Location) {
+ return true;
+ }
+ if (other.Location < Location) {
+ return false;
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRefCountedTracker::TNamedSlot::TNamedSlot(const TKey& key, size_t objectSize)
+ : Key_(key)
+ , ObjectSize_(objectSize)
+{ }
+
+TRefCountedTypeKey TRefCountedTracker::TNamedSlot::GetTypeKey() const
+{
+ return Key_.TypeKey;
+}
+
+const TSourceLocation& TRefCountedTracker::TNamedSlot::GetLocation() const
+{
+ return Key_.Location;
+}
+
+TString TRefCountedTracker::TNamedSlot::GetTypeName() const
+{
+ return TypeName(*GetTypeKey().Underlying());
+}
+
+TString TRefCountedTracker::TNamedSlot::GetFullName() const
+{
+ const auto& location = Key_.Location;
+ return location.IsValid()
+ ? Format("%v at %v:%v", GetTypeName(), location.GetFileName(), location.GetLine())
+ : GetTypeName();
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetObjectsAllocated() const
+{
+ return ObjectsAllocated_ + TagObjectsAllocated_;
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetObjectsFreed() const
+{
+ return ObjectsFreed_ + TagObjectsFreed_;
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetObjectsAlive() const
+{
+ return
+ ClampNonnegative(ObjectsAllocated_, ObjectsFreed_) +
+ ClampNonnegative(TagObjectsAllocated_, TagObjectsFreed_);
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetBytesAllocated() const
+{
+ return
+ ObjectsAllocated_ * ObjectSize_ +
+ SpaceSizeAllocated_;
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetBytesFreed() const
+{
+ return
+ ObjectsFreed_ * ObjectSize_ +
+ SpaceSizeFreed_;
+}
+
+size_t TRefCountedTracker::TNamedSlot::GetBytesAlive() const
+{
+ return
+ ClampNonnegative(ObjectsAllocated_, ObjectsFreed_) * ObjectSize_ +
+ ClampNonnegative(SpaceSizeAllocated_, SpaceSizeFreed_);
+}
+
+TRefCountedTrackerStatistics::TNamedSlotStatistics TRefCountedTracker::TNamedSlot::GetStatistics() const
+{
+ TRefCountedTrackerStatistics::TNamedSlotStatistics result;
+ result.FullName = GetFullName();
+ result.ObjectsAllocated = GetObjectsAllocated();
+ result.ObjectsFreed = GetObjectsFreed();
+ result.ObjectsAlive = GetObjectsAlive();
+ result.BytesAllocated = GetBytesAllocated();
+ result.BytesFreed = GetBytesFreed();
+ result.BytesAlive = GetBytesAlive();
+ return result;
+}
+
+size_t TRefCountedTracker::TNamedSlot::ClampNonnegative(size_t allocated, size_t freed)
+{
+ return allocated >= freed ? allocated - freed : 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// nullptr if not initialized or already destroyed
+thread_local TRefCountedTracker::TLocalSlots* TRefCountedTracker::LocalSlots_;
+
+// nullptr if not initialized or already destroyed
+thread_local TRefCountedTracker::TLocalSlot* TRefCountedTracker::LocalSlotsBegin_;
+
+// 0 if not initialized
+// -1 if already destroyed
+thread_local int TRefCountedTracker::LocalSlotsSize_;
+
+int TRefCountedTracker::GetTrackedThreadCount() const
+{
+ auto guard = Guard(SpinLock_);
+ return std::ssize(AllLocalSlots_);
+}
+
+TRefCountedTypeCookie TRefCountedTracker::GetCookie(
+ TRefCountedTypeKey typeKey,
+ size_t objectSize,
+ const TSourceLocation& location)
+{
+ auto guard = Guard(SpinLock_);
+
+ TypeKeyToObjectSize_.emplace(typeKey, objectSize);
+
+ TKey key{typeKey, location};
+ auto it = KeyToCookie_.find(key);
+ if (it != KeyToCookie_.end()) {
+ return it->second;
+ }
+
+ auto cookie = TRefCountedTypeCookie(std::ssize(CookieToKey_));
+ KeyToCookie_.emplace(key, cookie);
+ CookieToKey_.push_back(key);
+
+ return cookie;
+}
+
+TRefCountedTracker::TNamedStatistics TRefCountedTracker::GetSnapshot() const
+{
+ auto guard = Guard(SpinLock_);
+
+ TNamedStatistics result;
+ for (const auto& key : CookieToKey_) {
+ result.emplace_back(key, GetObjectSize(key.TypeKey));
+ }
+
+ auto accumulateResult = [&] (const auto& slots) {
+ for (auto index = 0; index < std::ssize(result) && index < std::ssize(slots); ++index) {
+ result[index] += slots[index];
+ }
+ };
+
+ accumulateResult(GlobalSlots_);
+ for (const auto* slots : AllLocalSlots_) {
+ accumulateResult(*slots);
+ }
+
+ return result;
+}
+
+void TRefCountedTracker::SortSnapshot(TNamedStatistics* snapshot, int sortByColumn)
+{
+ std::function<bool(const TNamedSlot& lhs, const TNamedSlot& rhs)> predicate;
+ switch (sortByColumn) {
+ case 0:
+ predicate = [] (const TNamedSlot& lhs, const TNamedSlot& rhs) {
+ return lhs.GetObjectsAlive() > rhs.GetObjectsAlive();
+ };
+ break;
+
+ case 1:
+ predicate = [] (const TNamedSlot& lhs, const TNamedSlot& rhs) {
+ return lhs.GetObjectsAllocated() > rhs.GetObjectsAllocated();
+ };
+ break;
+
+ case 2:
+ default:
+ predicate = [] (const TNamedSlot& lhs, const TNamedSlot& rhs) {
+ return lhs.GetBytesAlive() > rhs.GetBytesAlive();
+ };
+ break;
+
+ case 3:
+ predicate = [] (const TNamedSlot& lhs, const TNamedSlot& rhs) {
+ return lhs.GetBytesAllocated() > rhs.GetBytesAllocated();
+ };
+ break;
+
+ case 4:
+ predicate = [] (const TNamedSlot& lhs, const TNamedSlot& rhs) {
+ return lhs.GetTypeName() < rhs.GetTypeName();
+ };
+ break;
+ }
+ std::sort(snapshot->begin(), snapshot->end(), predicate);
+}
+
+TString TRefCountedTracker::GetDebugInfo(int sortByColumn) const
+{
+ auto snapshot = GetSnapshot();
+ SortSnapshot(&snapshot, sortByColumn);
+
+ TStringBuilder builder;
+
+ size_t totalObjectsAlive = 0;
+ size_t totalObjectsAllocated = 0;
+ size_t totalBytesAlive = 0;
+ size_t totalBytesAllocated = 0;
+
+ builder.AppendFormat(
+ "%10s %10s %15s %15s %s\n",
+ "ObjAlive",
+ "ObjAllocated",
+ "BytesAlive",
+ "BytesAllocated",
+ "Name");
+
+ builder.AppendString("-------------------------------------------------------------------------------------------------------------\n");
+
+ for (const auto& slot : snapshot) {
+ totalObjectsAlive += slot.GetObjectsAlive();
+ totalObjectsAllocated += slot.GetObjectsAllocated();
+ totalBytesAlive += slot.GetBytesAlive();
+ totalBytesAllocated += slot.GetBytesAllocated();
+
+ builder.AppendFormat(
+ "%10" PRISZT " %10" PRISZT " %15" PRISZT " %15" PRISZT " %s\n",
+ slot.GetObjectsAlive(),
+ slot.GetObjectsAllocated(),
+ slot.GetBytesAlive(),
+ slot.GetBytesAllocated(),
+ slot.GetFullName().data());
+ }
+
+ builder.AppendString("-------------------------------------------------------------------------------------------------------------\n");
+ builder.AppendFormat(
+ "%10" PRISZT " %10" PRISZT " %15" PRISZT " %15" PRISZT " %s\n",
+ totalObjectsAlive,
+ totalObjectsAllocated,
+ totalBytesAlive,
+ totalBytesAllocated,
+ "Total");
+
+ return builder.Flush();
+}
+
+TRefCountedTrackerStatistics TRefCountedTracker::GetStatistics() const
+{
+ auto slots = GetSnapshot();
+ SortSnapshot(&slots, -1);
+
+ TRefCountedTrackerStatistics result;
+ result.NamedStatistics.reserve(slots.size());
+
+ for (const auto& slot : slots) {
+ auto statistics = slot.GetStatistics();
+ result.NamedStatistics.push_back(statistics);
+ result.TotalStatistics += statistics;
+ }
+
+ return result;
+}
+
+size_t TRefCountedTracker::GetObjectsAllocated(TRefCountedTypeKey typeKey) const
+{
+ return GetSlot(typeKey).GetObjectsAllocated();
+}
+
+size_t TRefCountedTracker::GetObjectsAlive(TRefCountedTypeKey typeKey) const
+{
+ return GetSlot(typeKey).GetObjectsAlive();
+}
+
+size_t TRefCountedTracker::GetBytesAllocated(TRefCountedTypeKey typeKey) const
+{
+ return GetSlot(typeKey).GetBytesAllocated();
+}
+
+size_t TRefCountedTracker::GetBytesAlive(TRefCountedTypeKey typeKey) const
+{
+ return GetSlot(typeKey).GetBytesAlive();
+}
+
+size_t TRefCountedTracker::GetObjectSize(TRefCountedTypeKey typeKey) const
+{
+ auto it = TypeKeyToObjectSize_.find(typeKey);
+ return it == TypeKeyToObjectSize_.end() ? 0 : it->second;
+}
+
+TRefCountedTracker::TNamedSlot TRefCountedTracker::GetSlot(TRefCountedTypeKey typeKey) const
+{
+ auto guard = Guard(SpinLock_);
+
+ TKey key{typeKey, TSourceLocation()};
+
+ TNamedSlot result(key, GetObjectSize(typeKey));
+ auto it = KeyToCookie_.lower_bound(key);
+ while (it != KeyToCookie_.end() && it->first.TypeKey == typeKey) {
+ auto cookie = it->second;
+ auto index = cookie.Underlying();
+ auto accumulateResult = [&] (const auto& slots) {
+ if (index < std::ssize(slots)) {
+ result += slots[index];
+ }
+ };
+ accumulateResult(GlobalSlots_);
+ for (auto* slots : AllLocalSlots_) {
+ accumulateResult(*slots);
+ }
+ ++it;
+ }
+
+ return result;
+}
+
+#define INCREMENT_COUNTER_SLOW(name, delta) \
+ if (LocalSlotsSize_ < 0) { \
+ auto guard = Guard(SpinLock_); \
+ GetGlobalSlot(cookie)->name += delta; \
+ } else { \
+ GetLocalSlot(cookie)->name += delta; \
+ }
+
+void TRefCountedTracker::AllocateInstanceSlow(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER_SLOW(ObjectsAllocated, 1)
+}
+
+void TRefCountedTracker::FreeInstanceSlow(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER_SLOW(ObjectsFreed, 1)
+}
+
+void TRefCountedTracker::AllocateTagInstanceSlow(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER_SLOW(TagObjectsAllocated, 1)
+}
+
+void TRefCountedTracker::FreeTagInstanceSlow(TRefCountedTypeCookie cookie)
+{
+ INCREMENT_COUNTER_SLOW(TagObjectsFreed, 1)
+}
+
+void TRefCountedTracker::AllocateSpaceSlow(TRefCountedTypeCookie cookie, size_t space)
+{
+ INCREMENT_COUNTER_SLOW(SpaceSizeAllocated, space)
+}
+
+void TRefCountedTracker::FreeSpaceSlow(TRefCountedTypeCookie cookie, size_t space)
+{
+ INCREMENT_COUNTER_SLOW(SpaceSizeFreed, space)
+}
+
+#undef INCREMENT_COUNTER_SLOW
+
+TRefCountedTracker::TLocalSlot* TRefCountedTracker::GetLocalSlot(TRefCountedTypeCookie cookie)
+{
+ TMemoryTagGuard memoryTagGuard(NullMemoryTag);
+
+ struct TReclaimer
+ {
+ ~TReclaimer()
+ {
+ auto* this_ = TRefCountedTracker::Get();
+
+ auto guard = Guard(this_->SpinLock_);
+
+ if (this_->GlobalSlots_.size() < LocalSlots_->size()) {
+ this_->GlobalSlots_.resize(std::max(LocalSlots_->size(), this_->GlobalSlots_.size()));
+ }
+
+ for (auto index = 0; index < std::ssize(*LocalSlots_); ++index) {
+ this_->GlobalSlots_[index] += (*LocalSlots_)[index];
+ }
+
+ YT_VERIFY(this_->AllLocalSlots_.erase(LocalSlots_) == 1);
+
+ delete LocalSlots_;
+ LocalSlots_ = nullptr;
+ LocalSlotsBegin_ = nullptr;
+ LocalSlotsSize_ = -1;
+ }
+ };
+
+ static thread_local TReclaimer Reclaimer;
+
+ YT_VERIFY(LocalSlotsSize_ >= 0);
+
+ auto guard = Guard(SpinLock_);
+
+ if (!LocalSlots_) {
+ LocalSlots_ = new TLocalSlots();
+ YT_VERIFY(AllLocalSlots_.insert(LocalSlots_).second);
+ }
+
+ auto index = cookie.Underlying();
+ if (index >= std::ssize(*LocalSlots_)) {
+ LocalSlots_->resize(static_cast<size_t>(index) + 1);
+ }
+
+ LocalSlotsBegin_ = LocalSlots_->data();
+ LocalSlotsSize_ = std::ssize(*LocalSlots_);
+
+ return LocalSlotsBegin_ + index;
+}
+
+TRefCountedTracker::TGlobalSlot* TRefCountedTracker::GetGlobalSlot(TRefCountedTypeCookie cookie)
+{
+ TMemoryTagGuard memoryTagGuard(NullMemoryTag);
+
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+ auto index = cookie.Underlying();
+ if (index >= std::ssize(GlobalSlots_)) {
+ GlobalSlots_.resize(static_cast<size_t>(index) + 1);
+ }
+ return &GlobalSlots_[index];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/ref_counted_tracker.h b/yt/yt/core/misc/ref_counted_tracker.h
new file mode 100644
index 0000000000..fd5bb0a887
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker.h
@@ -0,0 +1,143 @@
+#pragma once
+
+#include "public.h"
+#include "source_location.h"
+#include "singleton.h"
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRefCountedTrackerStatistics
+{
+ struct TStatistics
+ {
+ size_t ObjectsAllocated = 0;
+ size_t ObjectsFreed = 0;
+ size_t ObjectsAlive = 0;
+ size_t BytesAllocated = 0;
+ size_t BytesFreed = 0;
+ size_t BytesAlive = 0;
+
+ TStatistics& operator+= (const TStatistics& rhs);
+ };
+
+ struct TNamedSlotStatistics
+ : public TStatistics
+ {
+ TString FullName;
+ };
+
+ std::vector<TNamedSlotStatistics> NamedStatistics;
+ TStatistics TotalStatistics;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Reference tracking relies on uniqueness of std::type_info objects.
+// Without uniqueness reference tracking is still functional but lacks precision
+// (i. e. some types may have duplicate slots in the accumulated table).
+// GCC guarantees std::type_info uniqueness starting from version 3.0
+// due to the so-called vague linking.
+//
+// See also: http://gcc.gnu.org/faq.html#dso
+// See also: http://www.codesourcery.com/public/cxx-abi/
+class TRefCountedTracker
+ : private TNonCopyable
+{
+public:
+ static TRefCountedTracker* Get();
+
+ TRefCountedTypeCookie GetCookie(
+ TRefCountedTypeKey typeKey,
+ size_t objectSize,
+ const TSourceLocation& location = TSourceLocation());
+
+ static void AllocateInstance(TRefCountedTypeCookie cookie);
+ static void FreeInstance(TRefCountedTypeCookie cookie);
+
+ static void AllocateTagInstance(TRefCountedTypeCookie cookie);
+ static void FreeTagInstance(TRefCountedTypeCookie cookie);
+
+ static void AllocateSpace(TRefCountedTypeCookie cookie, size_t size);
+ static void FreeSpace(TRefCountedTypeCookie cookie, size_t size);
+
+ TString GetDebugInfo(int sortByColumn = -1) const;
+ TRefCountedTrackerStatistics GetStatistics() const;
+
+ size_t GetObjectsAllocated(TRefCountedTypeKey typeKey) const;
+ size_t GetObjectsAlive(TRefCountedTypeKey typeKey) const;
+ size_t GetBytesAllocated(TRefCountedTypeKey typeKey) const;
+ size_t GetBytesAlive(TRefCountedTypeKey typeKey) const;
+
+ int GetTrackedThreadCount() const;
+
+private:
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+
+ struct TLocalSlotsHolder;
+
+ struct TKey
+ {
+ TRefCountedTypeKey TypeKey;
+ TSourceLocation Location;
+
+ bool operator < (const TKey& other) const;
+ bool operator == (const TKey& other) const;
+ };
+
+ struct TLocalSlot;
+ using TLocalSlots = std::vector<TLocalSlot>;
+
+ struct TGlobalSlot;
+ using TGlobalSlots = std::vector<TGlobalSlot>;
+
+ class TNamedSlot;
+ using TNamedStatistics = std::vector<TNamedSlot>;
+
+ // nullptr if not initialized or already destroyed
+ static thread_local TLocalSlots* LocalSlots_;
+
+ // nullptr if not initialized or already destroyed
+ static thread_local TLocalSlot* LocalSlotsBegin_;
+
+ // 0 if not initialized
+ // -1 if already destroyed
+ static thread_local int LocalSlotsSize_;
+
+ mutable NThreading::TForkAwareSpinLock SpinLock_;
+ std::map<TKey, TRefCountedTypeCookie> KeyToCookie_;
+ std::map<TRefCountedTypeKey, size_t> TypeKeyToObjectSize_;
+ std::vector<TKey> CookieToKey_;
+ std::vector<TGlobalSlot> GlobalSlots_;
+ THashSet<TLocalSlots*> AllLocalSlots_;
+
+ TNamedStatistics GetSnapshot() const;
+ static void SortSnapshot(TNamedStatistics* snapshot, int sortByColumn);
+
+ size_t GetObjectSize(TRefCountedTypeKey typeKey) const;
+
+ TNamedSlot GetSlot(TRefCountedTypeKey typeKey) const;
+
+ void AllocateInstanceSlow(TRefCountedTypeCookie cookie);
+ void FreeInstanceSlow(TRefCountedTypeCookie cookie);
+
+ void AllocateTagInstanceSlow(TRefCountedTypeCookie cookie);
+ void FreeTagInstanceSlow(TRefCountedTypeCookie cookie);
+
+ void AllocateSpaceSlow(TRefCountedTypeCookie cookie, size_t size);
+ void FreeSpaceSlow(TRefCountedTypeCookie cookie, size_t size);
+
+ TLocalSlot* GetLocalSlot(TRefCountedTypeCookie cookie);
+ TGlobalSlot* GetGlobalSlot(TRefCountedTypeCookie cookie);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define REF_COUNTED_TRACKER_INL_H_
+#include "ref_counted_tracker-inl.h"
+#undef REF_COUNTED_TRACKER_INL_H_
diff --git a/yt/yt/core/misc/ref_counted_tracker_profiler.cpp b/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
new file mode 100644
index 0000000000..be5d007bbd
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker_profiler.cpp
@@ -0,0 +1,44 @@
+#include "ref_counted_tracker_profiler.h"
+#include "ref_counted_tracker.h"
+#include "singleton.h"
+
+#include <yt/yt/library/profiling/producer.h>
+
+namespace NYT {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRefCountedTrackerProfiler
+ : public ISensorProducer
+{
+public:
+ TRefCountedTrackerProfiler()
+ {
+ TProfiler registry{"/ref_counted_tracker"};
+ registry.AddProducer("/total", MakeStrong(this));
+ }
+
+ void CollectSensors(ISensorWriter* writer) override
+ {
+ auto statistics = TRefCountedTracker::Get()->GetStatistics().TotalStatistics;
+
+ writer->AddCounter("/objects_allocated", statistics.ObjectsAllocated);
+ writer->AddCounter("/objects_freed", statistics.ObjectsFreed);
+ writer->AddGauge("/objects_alive", statistics.ObjectsAlive);
+
+ writer->AddCounter("/bytes_allocated", statistics.BytesAllocated);
+ writer->AddCounter("/bytes_freed", statistics.BytesFreed);
+ writer->AddGauge("/bytes_alive", statistics.BytesAlive);
+ }
+};
+
+void EnableRefCountedTrackerProfiling()
+{
+ LeakyRefCountedSingleton<TRefCountedTrackerProfiler>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_counted_tracker_profiler.h b/yt/yt/core/misc/ref_counted_tracker_profiler.h
new file mode 100644
index 0000000000..f20ad7765e
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker_profiler.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void EnableRefCountedTrackerProfiling();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp b/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
new file mode 100644
index 0000000000..e6b3212506
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker_statistics_producer.cpp
@@ -0,0 +1,42 @@
+#include "ref_counted_tracker_statistics_producer.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonProducer CreateRefCountedTrackerStatisticsProducer()
+{
+ return BIND([] (IYsonConsumer* consumer) {
+ auto statistics = TRefCountedTracker::Get()->GetStatistics();
+
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("statistics")
+ .DoListFor(statistics.NamedStatistics, [] (TFluentList fluent, const auto& namedSlotStatistics) {
+ fluent
+ .Item().BeginMap()
+ .Item("name").Value(namedSlotStatistics.FullName)
+ .Item("objects_alive").Value(namedSlotStatistics.ObjectsAlive)
+ .Item("objects_allocated").Value(namedSlotStatistics.ObjectsAllocated)
+ .Item("bytes_alive").Value(namedSlotStatistics.BytesAlive)
+ .Item("bytes_allocated").Value(namedSlotStatistics.BytesAllocated)
+ .EndMap();
+ })
+ .Item("total").BeginMap()
+ .Item("objects_alive").Value(statistics.TotalStatistics.ObjectsAlive)
+ .Item("objects_allocated").Value(statistics.TotalStatistics.ObjectsAllocated)
+ .Item("bytes_alive").Value(statistics.TotalStatistics.BytesAlive)
+ .Item("bytes_allocated").Value(statistics.TotalStatistics.BytesAllocated)
+ .EndMap()
+ .EndMap();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_counted_tracker_statistics_producer.h b/yt/yt/core/misc/ref_counted_tracker_statistics_producer.h
new file mode 100644
index 0000000000..92b2a8b62c
--- /dev/null
+++ b/yt/yt/core/misc/ref_counted_tracker_statistics_producer.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "ref_counted_tracker.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYson::TYsonProducer CreateRefCountedTrackerStatisticsProducer();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_tracked.cpp b/yt/yt/core/misc/ref_tracked.cpp
new file mode 100644
index 0000000000..3d7551a8f6
--- /dev/null
+++ b/yt/yt/core/misc/ref_tracked.cpp
@@ -0,0 +1,56 @@
+#include "ref_tracked.h"
+#include "ref_counted_tracker.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRefCountedTypeCookie TRefCountedTrackerFacade::GetCookie(
+ TRefCountedTypeKey typeKey,
+ size_t instanceSize,
+ const TSourceLocation& location)
+{
+ return TRefCountedTracker::Get()->GetCookie(
+ typeKey,
+ instanceSize,
+ location);
+}
+
+void TRefCountedTrackerFacade::AllocateInstance(TRefCountedTypeCookie cookie)
+{
+ TRefCountedTracker::AllocateInstance(cookie);
+}
+
+void TRefCountedTrackerFacade::FreeInstance(TRefCountedTypeCookie cookie)
+{
+ TRefCountedTracker::FreeInstance(cookie);
+}
+
+void TRefCountedTrackerFacade::AllocateTagInstance(TRefCountedTypeCookie cookie)
+{
+ TRefCountedTracker::AllocateTagInstance(cookie);
+}
+
+void TRefCountedTrackerFacade::FreeTagInstance(TRefCountedTypeCookie cookie)
+{
+ TRefCountedTracker::FreeTagInstance(cookie);
+}
+
+void TRefCountedTrackerFacade::AllocateSpace(TRefCountedTypeCookie cookie, size_t size)
+{
+ TRefCountedTracker::AllocateSpace(cookie, size);
+}
+
+void TRefCountedTrackerFacade::FreeSpace(TRefCountedTypeCookie cookie, size_t size)
+{
+ TRefCountedTracker::FreeSpace(cookie, size);
+}
+
+void TRefCountedTrackerFacade::Dump()
+{
+ fprintf(stderr, "%s", TRefCountedTracker::Get()->GetDebugInfo().data());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/ref_tracked.h b/yt/yt/core/misc/ref_tracked.h
new file mode 100644
index 0000000000..4d94188f9a
--- /dev/null
+++ b/yt/yt/core/misc/ref_tracked.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/ref_tracked.h>
diff --git a/yt/yt/core/misc/relaxed_mpsc_queue-inl.h b/yt/yt/core/misc/relaxed_mpsc_queue-inl.h
new file mode 100644
index 0000000000..9fdf42e4df
--- /dev/null
+++ b/yt/yt/core/misc/relaxed_mpsc_queue-inl.h
@@ -0,0 +1,77 @@
+#ifndef RELAXED_MPSC_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include relaxed_mpsc_queue.h"
+// For the sake of sane code completion.
+#include "relaxed_mpsc_queue.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+TRelaxedIntrusiveMpscQueue<T, Hook>::~TRelaxedIntrusiveMpscQueue()
+{
+ while (auto item = TryDequeue()) {
+ }
+}
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+void TRelaxedIntrusiveMpscQueue<T, Hook>::Enqueue(std::unique_ptr<T> node)
+{
+ EnqueueImpl(HookFromNode(node.release()));
+}
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+std::unique_ptr<T> TRelaxedIntrusiveMpscQueue<T, Hook>::TryDequeue()
+{
+ return std::unique_ptr<T>(NodeFromHook(TryDequeueImpl()));
+}
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+TRelaxedMpscQueueHook* TRelaxedIntrusiveMpscQueue<T, Hook>::HookFromNode(T* node) noexcept
+{
+ if (!node) {
+ return nullptr;
+ }
+ return &(node->*Hook);
+}
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+T* TRelaxedIntrusiveMpscQueue<T, Hook>::NodeFromHook(TRelaxedMpscQueueHook* hook) noexcept
+{
+ if (!hook) {
+ return nullptr;
+ }
+ const T* const fakeNode = nullptr;
+ const char* const fakeHook = static_cast<const char*>(static_cast<const void*>(&(fakeNode->*Hook)));
+ const size_t offset = fakeHook - static_cast<const char*>(static_cast<const void*>(fakeNode));
+ return static_cast<T*>(static_cast<void*>(static_cast<char*>(static_cast<void*>(hook)) - offset));
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TRelaxedMpscQueue<T>::TNode::TNode(T&& value)
+ : Value(std::move(value))
+{ }
+
+template <class T>
+void TRelaxedMpscQueue<T>::Enqueue(T&& value)
+{
+ Impl_.Enqueue(std::make_unique<TNode>(std::move(value)));
+}
+
+template <class T>
+bool TRelaxedMpscQueue<T>::TryDequeue(T* value)
+{
+ auto node = Impl_.TryDequeue();
+ if (!node) {
+ return false;
+ }
+ *value = std::move(node->Value);
+ return true;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/misc/relaxed_mpsc_queue.cpp b/yt/yt/core/misc/relaxed_mpsc_queue.cpp
new file mode 100644
index 0000000000..c5eca5c337
--- /dev/null
+++ b/yt/yt/core/misc/relaxed_mpsc_queue.cpp
@@ -0,0 +1,71 @@
+#include "relaxed_mpsc_queue.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRelaxedMpscQueueBase::TRelaxedMpscQueueBase()
+ : Head_(&Stub_)
+ , Tail_(&Stub_)
+{ }
+
+TRelaxedMpscQueueBase::~TRelaxedMpscQueueBase()
+{
+ // Check that queue is empty. Derived classes must ensure that the queue is empty.
+ YT_VERIFY(Head_ == Tail_);
+ YT_VERIFY(Head_ == &Stub_);
+ YT_VERIFY(!Head_.load()->Next.load());
+}
+
+void TRelaxedMpscQueueBase::EnqueueImpl(TRelaxedMpscQueueHook* node) noexcept
+{
+ node->Next.store(nullptr, std::memory_order::release);
+ auto* prev = Head_.exchange(node, std::memory_order::acq_rel);
+ prev->Next.store(node, std::memory_order::release);
+}
+
+TRelaxedMpscQueueHook* TRelaxedMpscQueueBase::TryDequeueImpl() noexcept
+{
+ auto* tail = Tail_;
+ auto* next = tail->Next.load(std::memory_order::acquire);
+
+ // Handle stub node.
+ if (tail == &Stub_) {
+ if (!next) {
+ return nullptr;
+ }
+ Tail_ = next;
+ // Save tail-recursive call by updating local variables.
+ tail = next;
+ next = next->Next.load(std::memory_order::acquire);
+ }
+
+ // No producer-consumer race.
+ if (next) {
+ Tail_ = next;
+ return tail;
+ }
+
+ auto* head = Head_.load(std::memory_order::acquire);
+
+ // Concurrent producer was blocked, bail out.
+ if (tail != head) {
+ return nullptr;
+ }
+
+ // Decouple (future) producers and consumer by barriering via stub node.
+ EnqueueImpl(&Stub_);
+ next = tail->Next.load(std::memory_order::acquire);
+
+ if (next) {
+ Tail_ = next;
+ return tail;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/relaxed_mpsc_queue.h b/yt/yt/core/misc/relaxed_mpsc_queue.h
new file mode 100644
index 0000000000..d52d34ce9a
--- /dev/null
+++ b/yt/yt/core/misc/relaxed_mpsc_queue.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <library/cpp/yt/memory/public.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// A non-linearizable multiple-producer single-consumer queue.
+//
+// Based on http://www.1024cores.net/home/lock-free-algorithms/queues/non-intrusive-mpsc-node-based-queue
+//
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRelaxedMpscQueueHook
+{
+ std::atomic<TRelaxedMpscQueueHook*> Next = nullptr;
+};
+
+class TRelaxedMpscQueueBase
+{
+protected:
+ TRelaxedMpscQueueBase();
+ ~TRelaxedMpscQueueBase();
+
+ //! Pushes the (detached) node to the queue.
+ //! Node ownership is transferred to the queue.
+ void EnqueueImpl(TRelaxedMpscQueueHook* node) noexcept;
+
+ //! Pops and detaches a node from the queue. Node ownership is transferred to the caller.
+ //! When null is returned then the queue is either empty or blocked.
+ //! This method does not distinguish between these two cases.
+ TRelaxedMpscQueueHook* TryDequeueImpl() noexcept;
+
+private:
+ alignas(CacheLineSize) TRelaxedMpscQueueHook Stub_;
+
+ //! Producer-side.
+ alignas(CacheLineSize) std::atomic<TRelaxedMpscQueueHook*> Head_;
+
+ //! Consumer-side.
+ alignas(CacheLineSize) TRelaxedMpscQueueHook* Tail_;
+};
+
+template <class T, TRelaxedMpscQueueHook T::*Hook>
+class TRelaxedIntrusiveMpscQueue
+ : public TRelaxedMpscQueueBase
+{
+public:
+ TRelaxedIntrusiveMpscQueue() = default;
+ TRelaxedIntrusiveMpscQueue(const TRelaxedIntrusiveMpscQueue&) = delete;
+ TRelaxedIntrusiveMpscQueue(TRelaxedIntrusiveMpscQueue&&) = delete;
+
+ ~TRelaxedIntrusiveMpscQueue();
+
+ void Enqueue(std::unique_ptr<T> node);
+ std::unique_ptr<T> TryDequeue();
+
+private:
+ static TRelaxedMpscQueueHook* HookFromNode(T* node) noexcept;
+ static T* NodeFromHook(TRelaxedMpscQueueHook* hook) noexcept;
+};
+
+template <class T>
+class TRelaxedMpscQueue
+{
+public:
+ TRelaxedMpscQueue() = default;
+ TRelaxedMpscQueue(const TRelaxedMpscQueue&) = delete;
+ TRelaxedMpscQueue(TRelaxedMpscQueue&&) = delete;
+
+ void Enqueue(T&& value);
+ bool TryDequeue(T* value);
+
+private:
+ struct TNode
+ {
+ explicit TNode(T&& value);
+
+ T Value;
+ TRelaxedMpscQueueHook Hook;
+ };
+
+ TRelaxedIntrusiveMpscQueue<TNode, &TNode::Hook> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define RELAXED_MPSC_QUEUE_INL_H_
+#include "relaxed_mpsc_queue-inl.h"
+#undef RELAXED_MPSC_QUEUE_INL_H_
+
diff --git a/yt/yt/core/misc/ring_queue.h b/yt/yt/core/misc/ring_queue.h
new file mode 100644
index 0000000000..e0463e2f45
--- /dev/null
+++ b/yt/yt/core/misc/ring_queue.h
@@ -0,0 +1,379 @@
+#pragma once
+
+#include "common.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A simple and high-performant drop-in replacement for |std::queue|.
+/*!
+ * Things to keep in mind:
+ * - Capacity is doubled each time it is exhausted and is never shrunk back.
+ * - Iteration is supported but iterator movements involve calling |move_forward| and |move_backward|.
+ * - |T| must be nothrow move constructable.
+ */
+template <class T, class TAllocator = std::allocator<T>>
+class TRingQueue
+{
+public:
+ using value_type = T;
+ using reference = T&;
+ using const_reference = const T&;
+ using pointer = T*;
+ using const_pointer = const T*;
+ using size_type = size_t;
+
+ static_assert(std::is_nothrow_move_constructible<T>::value, "T must be nothrow move constructable.");
+
+ class TIterator
+ {
+ public:
+ TIterator() = default;
+ TIterator(const TIterator&) = default;
+
+ const T& operator* () const
+ {
+ return *Ptr_;
+ }
+
+ T& operator* ()
+ {
+ return *Ptr_;
+ }
+
+ const T* operator-> () const
+ {
+ return Ptr_;
+ }
+
+ T* operator-> ()
+ {
+ return Ptr_;
+ }
+
+ bool operator == (TIterator other) const
+ {
+ return Ptr_ == other.Ptr_;
+ }
+
+ bool operator != (TIterator other) const
+ {
+ return Ptr_ != other.Ptr_;
+ }
+
+ TIterator& operator = (TIterator other)
+ {
+ Ptr_ = other.Ptr_;
+ return *this;
+ }
+
+ private:
+ friend class TRingQueue<T>;
+
+ explicit TIterator(T* ptr)
+ : Ptr_(ptr)
+ { }
+
+ T* Ptr_;
+ };
+
+ TRingQueue()
+ : TRingQueue(TAllocator())
+ { }
+
+ explicit TRingQueue(const TAllocator& allocator)
+ : Allocator_(allocator)
+ {
+ Capacity_ = InitialCapacity;
+ Begin_ = Allocator_.allocate(Capacity_);
+ End_ = Begin_ + Capacity_;
+
+ Size_ = 0;
+ Head_ = Tail_ = Begin_;
+ }
+
+ TRingQueue(TRingQueue&& other)
+ : Allocator_(std::move(other.Allocator_))
+ {
+ Capacity_ = other.Capacity_;
+ Begin_ = other.Begin_;
+ End_ = other.End_;
+
+ Size_ = other.Size_;
+ Head_ = other.Head_;
+ Tail_ = other.Tail_;
+
+ other.Capacity_ = other.Size_ = 0;
+ other.Begin_ = other.End_ = other.Head_ = other.Tail_ = nullptr;
+ }
+
+ TRingQueue(const TRingQueue& other) = delete;
+
+ ~TRingQueue()
+ {
+ DestroyElements();
+ if (Begin_) {
+ Allocator_.deallocate(Begin_, Capacity_);
+ }
+ }
+
+
+ T& front()
+ {
+ return *Head_;
+ }
+
+ const T& front() const
+ {
+ return *Head_;
+ }
+
+ T& back()
+ {
+ if (Tail_ == Begin_) {
+ return *(End_ - 1);
+ } else {
+ return *(Tail_ - 1);
+ }
+ }
+
+ const T& back() const
+ {
+ if (Tail_ == Begin_) {
+ return *(End_ - 1);
+ } else {
+ return *(Tail_ - 1);
+ }
+ }
+
+
+ // For performance reasons iterators do not provide their own operator++ and operator--.
+ // move_forward and move_backward are provided instead.
+ TIterator begin()
+ {
+ return TIterator(Head_);
+ }
+
+ TIterator end()
+ {
+ return TIterator(Tail_);
+ }
+
+ void move_forward(TIterator& it) const
+ {
+ ++it.Ptr_;
+ if (it.Ptr_ == End_) {
+ it.Ptr_ = Begin_;
+ }
+ }
+
+ void move_backward(TIterator& it) const
+ {
+ if (it.Ptr_ == Begin_) {
+ it.Ptr_ = End_ - 1;
+ } else {
+ --it.Ptr_;
+ }
+ }
+
+
+ T& operator[](size_t index)
+ {
+ YT_ASSERT(index < size());
+ auto headToEnd = static_cast<size_t>(End_ - Head_);
+ return index < headToEnd
+ ? Head_[index]
+ : Begin_[index - headToEnd];
+ }
+
+ const T& operator[](size_t index) const
+ {
+ return const_cast<TRingQueue*>(this)->operator[](index);
+ }
+
+
+ size_t size() const
+ {
+ return Size_;
+ }
+
+ bool empty() const
+ {
+ return Size_ == 0;
+ }
+
+
+ void push(const T& value)
+ {
+ BeforePush();
+ new(Tail_) T(value);
+ AfterPush();
+ }
+
+ void push(T&& value)
+ {
+ BeforePush();
+ new(Tail_) T(std::move(value));
+ AfterPush();
+ }
+
+ template <class... TArgs>
+ T& emplace(TArgs&&... args)
+ {
+ BeforePush();
+ auto* ptr = Tail_;
+ new (ptr) T(std::forward<TArgs>(args)...);
+ AfterPush();
+ return *ptr;
+ }
+
+ void pop()
+ {
+ YT_ASSERT(Size_ > 0);
+ Head_->T::~T();
+ ++Head_;
+ if (Head_ == End_) {
+ Head_ = Begin_;
+ }
+ --Size_;
+ }
+
+ void clear()
+ {
+ DestroyElements();
+ Size_ = 0;
+ Head_ = Tail_ = Begin_;
+ }
+
+private:
+ TAllocator Allocator_;
+
+ size_t Capacity_;
+ T* Begin_;
+ T* End_;
+
+ size_t Size_;
+ T* Head_;
+ T* Tail_;
+
+ static const size_t InitialCapacity = 16;
+
+
+ void DestroyElements()
+ {
+ if (Head_ <= Tail_) {
+ DestroyRange(Head_, Tail_);
+ } else {
+ DestroyRange(Head_, End_);
+ DestroyRange(Begin_, Tail_);
+ }
+ }
+
+ static void DestroyRange(T* begin, T* end)
+ {
+ if (!std::is_trivially_destructible<T>::value) {
+ for (auto* current = begin; current != end; ++current) {
+ current->T::~T();
+ }
+ }
+ }
+
+ static void MoveRange(T* begin, T* end, T* result)
+ {
+ if (std::is_trivially_move_constructible<T>::value) {
+ ::memcpy(result, begin, sizeof (T) * (end - begin));
+ } else {
+ for (auto* current = begin; current != end; ++current) {
+ new(result++) T(std::move(*current));
+ current->T::~T();
+ }
+ }
+ }
+
+ void BeforePush() noexcept
+ {
+ // NB: Avoid filling Items_ completely and collapsing Head_ with Tail_.
+ if (Size_ == Capacity_ - 1) {
+ auto newCapacity = Capacity_ * 2;
+ auto* newBegin = Allocator_.allocate(newCapacity);
+
+ if (Head_ <= Tail_) {
+ MoveRange(Head_, Tail_, newBegin);
+ } else {
+ MoveRange(Head_, End_, newBegin);
+ MoveRange(Begin_, Tail_, newBegin + (End_ - Head_));
+ }
+
+ Allocator_.deallocate(Begin_, Capacity_);
+
+ Capacity_ = newCapacity;
+ Begin_ = newBegin;
+ End_ = Begin_ + newCapacity;
+
+ Head_ = Begin_;
+ Tail_ = Head_ + Size_;
+ }
+ }
+
+ void AfterPush()
+ {
+ if (++Tail_ == End_) {
+ Tail_ = Begin_;
+ }
+ ++Size_;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class TAllocator = std::allocator<T>>
+class TRingQueueIterableWrapper
+{
+public:
+ using TContainer = TRingQueue<T, TAllocator>;
+ using TBaseIterator = typename TContainer::TIterator;
+
+ class TIterator
+ : public TBaseIterator
+ {
+ public:
+ TIterator(TBaseIterator baseIterator, TContainer& container)
+ : TBaseIterator(std::move(baseIterator))
+ , Container_(container)
+ { }
+
+ void operator++()
+ {
+ Container_.move_forward(*this);
+ }
+
+ private:
+ TRingQueue<T, TAllocator>& Container_;
+ };
+
+ TIterator begin() const
+ {
+ auto& container = const_cast<TContainer&>(Container_);
+ return TIterator(container.begin(), container);
+ }
+
+ TIterator end() const
+ {
+ auto& container = const_cast<TContainer&>(Container_);
+ return TIterator(container.end(), container);
+ }
+
+ TRingQueueIterableWrapper(TContainer& container)
+ : Container_(container)
+ { }
+
+private:
+ TContainer& Container_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/serialize-inl.h b/yt/yt/core/misc/serialize-inl.h
new file mode 100644
index 0000000000..8183c34656
--- /dev/null
+++ b/yt/yt/core/misc/serialize-inl.h
@@ -0,0 +1,1998 @@
+#ifndef SERIALIZE_INL_H_
+#error "Direct inclusion of this file is not allowed, include serialize.h"
+// For the sake of sane code completion.
+#include "serialize.h"
+#endif
+
+#include "collection_helpers.h"
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+#include <library/cpp/yt/small_containers/compact_flat_map.h>
+#include <library/cpp/yt/small_containers/compact_set.h>
+
+#include <optional>
+#include <variant>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TInput>
+void ReadRef(TInput& input, TMutableRef ref)
+{
+ auto bytesLoaded = input.Load(ref.Begin(), ref.Size());
+ if (bytesLoaded != ref.Size()) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("Premature end-of-stream")
+ << TErrorAttribute("bytes_loaded", bytesLoaded)
+ << TErrorAttribute("bytes_expected", ref.Size());
+ }
+}
+
+inline void ReadRef(const char*& ptr, TMutableRef ref)
+{
+ memcpy(ref.Begin(), ptr, ref.Size());
+ ptr += ref.Size();
+}
+
+template <class TOutput>
+void WriteRef(TOutput& output, TRef ref)
+{
+ output.Write(ref.Begin(), ref.Size());
+}
+
+inline void WriteRef(char*& ptr, TRef ref)
+{
+ memcpy(ptr, ref.Begin(), ref.Size());
+ ptr += ref.Size();
+}
+
+template <class TOutput, class T>
+void WritePod(TOutput& output, const T& obj)
+{
+ static_assert(TTypeTraits<T>::IsPod || std::is_trivial_v<T>, "T must be a pod-type.");
+ static_assert(
+ std::has_unique_object_representations_v<T> ||
+ std::is_same_v<T, float> ||
+ std::is_same_v<T, double>,
+ "T must have unique representations.");
+
+ output.Write(&obj, sizeof(obj));
+}
+
+template <class T>
+void WritePod(char*& ptr, const T& obj)
+{
+ static_assert(TTypeTraits<T>::IsPod || std::is_trivial_v<T>, "T must be a pod-type.");
+ memcpy(ptr, &obj, sizeof(obj));
+ ptr += sizeof(obj);
+}
+
+template <class TOutput>
+void WriteZeroes(TOutput& output, size_t count)
+{
+ size_t bytesWritten = 0;
+ while (bytesWritten < count) {
+ size_t bytesToWrite = Min(ZeroBufferSize, count - bytesWritten);
+ output.Write(ZeroBuffer.data(), bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ YT_VERIFY(bytesWritten == count);
+}
+
+inline void WriteZeroes(char*& ptr, size_t count)
+{
+ memset(ptr, 0, count);
+ ptr += count;
+}
+
+template <class TOutput>
+void WritePadding(TOutput& output, size_t sizeToPad)
+{
+ output.Write(ZeroBuffer.data(), AlignUpSpace(sizeToPad, SerializationAlignment));
+}
+
+inline void WritePadding(char*& ptr, size_t sizeToPad)
+{
+ size_t count = AlignUpSpace(sizeToPad, SerializationAlignment);
+ memset(ptr, 0, count);
+ ptr += count;
+}
+
+template <class TInput>
+void ReadPadding(TInput& input, size_t sizeToPad)
+{
+ auto bytesToSkip = AlignUpSpace(sizeToPad, SerializationAlignment);
+ auto bytesSkipped = input.Skip(bytesToSkip);
+ if (bytesSkipped != bytesToSkip) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("Premature end-of-stream")
+ << TErrorAttribute("bytes_skipped", bytesSkipped)
+ << TErrorAttribute("bytes_expected", bytesToSkip);
+ }
+}
+
+inline void ReadPadding(const char*& ptr, size_t sizeToPad)
+{
+ ptr += AlignUpSpace(sizeToPad, SerializationAlignment);
+}
+
+template <class TInput, class T>
+void ReadPod(TInput& input, T& obj)
+{
+ static_assert(TTypeTraits<T>::IsPod || std::is_trivial_v<T>, "T must be a pod-type.");
+ ReadRef(input, TMutableRef::FromPod(obj));
+}
+
+template <class T>
+void ReadPod(char*& ptr, T& obj)
+{
+ static_assert(TTypeTraits<T>::IsPod || std::is_trivial_v<T>, "T must be a pod-type.");
+ memcpy(&obj, ptr, sizeof(obj));
+ ptr += sizeof(obj);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TSharedRef PackRefs(const T& parts)
+{
+ size_t size = 0;
+
+ // Number of bytes to hold vector size.
+ size += sizeof(i32);
+ // Number of bytes to hold ref sizes.
+ size += sizeof(i64) * parts.size();
+ // Number of bytes to hold refs.
+ for (const auto& ref : parts) {
+ size += ref.Size();
+ }
+
+ struct TPackedRefsTag { };
+ auto result = TSharedMutableRef::Allocate<TPackedRefsTag>(size, {.InitializeStorage = false});
+ TMemoryOutput output(result.Begin(), result.Size());
+
+ WritePod(output, static_cast<i32>(parts.size()));
+ for (const auto& ref : parts) {
+ WritePod(output, static_cast<i64>(ref.Size()));
+ WriteRef(output, ref);
+ }
+
+ return result;
+}
+
+template <class T>
+void UnpackRefs(const TSharedRef& packedRef, T* parts);
+
+template <class T>
+void UnpackRefs(const TSharedRef& packedRef, T* parts);
+
+template <class TTag, class TParts>
+TSharedRef MergeRefsToRef(const TParts& parts)
+{
+ size_t size = GetByteSize(parts);
+ auto mergedRef = TSharedMutableRef::Allocate<TTag>(size, {.InitializeStorage = false});
+ MergeRefsToRef(parts, mergedRef);
+ return mergedRef;
+}
+
+template <class TParts>
+void MergeRefsToRef(const TParts& parts, TMutableRef dst)
+{
+ char* current = dst.Begin();
+ for (const auto& part : parts) {
+ std::copy(part.Begin(), part.End(), current);
+ current += part.Size();
+ }
+ YT_VERIFY(current == dst.End());
+}
+
+template <class TParts>
+TString MergeRefsToString(const TParts& parts)
+{
+ size_t size = GetByteSize(parts);
+ TString packedString;
+ packedString.reserve(size);
+ for (const auto& part : parts) {
+ packedString.append(part.Begin(), part.End());
+ }
+ return packedString;
+}
+
+template <class T>
+void UnpackRefs(const TSharedRef& packedRef, T* parts)
+{
+ TMemoryInput input(packedRef.Begin(), packedRef.Size());
+
+ i32 size = 0;
+ ReadPod(input, size);
+ if (size < 0) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("Packed ref size is negative")
+ << TErrorAttribute("size", size);
+ }
+
+ parts->clear();
+ parts->reserve(size);
+
+ for (int index = 0; index < size; ++index) {
+ i64 partSize = 0;
+ ReadPod(input, partSize);
+ if (partSize < 0) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("A part of a packed ref has negative size")
+ << TErrorAttribute("index", index)
+ << TErrorAttribute("size", partSize);
+ }
+ if (packedRef.End() - input.Buf() < partSize) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("A part of a packed ref is too large")
+ << TErrorAttribute("index", index)
+ << TErrorAttribute("size", partSize)
+ << TErrorAttribute("bytes_left", packedRef.End() - input.Buf());
+ }
+
+ parts->push_back(packedRef.Slice(input.Buf(), input.Buf() + partSize));
+
+ input.Skip(partSize);
+ }
+
+ if (input.Buf() < packedRef.End()) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("Packed ref is too large")
+ << TErrorAttribute("extra_bytes", packedRef.End() - input.Buf());
+ }
+}
+
+inline std::vector<TSharedRef> UnpackRefs(const TSharedRef& packedRef)
+{
+ std::vector<TSharedRef> parts;
+ UnpackRefs(packedRef, &parts);
+ return parts;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void TSaveContextStream::Write(const void* buf, size_t len)
+{
+ if (Y_LIKELY(BufferRemaining_ >= len)) {
+ ::memcpy(BufferPtr_, buf, len);
+ BufferPtr_ += len;
+ BufferRemaining_ -= len;
+ } else {
+ WriteSlow(buf, len);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TSaveContextStream* TStreamSaveContext::GetOutput()
+{
+ return &Output_;
+}
+
+Y_FORCE_INLINE int TStreamSaveContext::GetVersion() const
+{
+ return Version_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE size_t TLoadContextStream::Load(void* buf, size_t len)
+{
+ if (Y_LIKELY(BufferRemaining_ >= len)) {
+ ::memcpy(buf, BufferPtr_, len);
+ BufferPtr_ += len;
+ BufferRemaining_ -= len;
+ return len;
+ } else {
+ return LoadSlow(buf, len);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TLoadContextStream* TStreamLoadContext::GetInput()
+{
+ return &Input_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr TEntitySerializationKey::TEntitySerializationKey()
+ : Index(-1)
+{ }
+
+inline constexpr TEntitySerializationKey::TEntitySerializationKey(int index)
+ : Index(index)
+{ }
+
+inline constexpr bool TEntitySerializationKey::operator == (TEntitySerializationKey rhs) const
+{
+ return Index == rhs.Index;
+}
+
+inline constexpr bool TEntitySerializationKey::operator != (TEntitySerializationKey rhs) const
+{
+ return !(*this == rhs);
+}
+
+inline constexpr TEntitySerializationKey::operator bool() const
+{
+ return Index != -1;
+}
+
+inline void TEntitySerializationKey::Save(TEntityStreamSaveContext& context) const
+{
+ NYT::Save(context, Index);
+}
+
+inline void TEntitySerializationKey::Load(TEntityStreamLoadContext& context)
+{
+ NYT::Load(context, Index);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TEntitySerializationKey TEntityStreamSaveContext::GenerateSerializationKey()
+{
+ YT_VERIFY(!ParentContext_);
+ return TEntitySerializationKey(SerializationKeyIndex_++);
+}
+
+template <class T>
+TEntitySerializationKey TEntityStreamSaveContext::RegisterRawEntity(T* entity)
+{
+ if (ParentContext_) {
+ return GetOrCrash(ParentContext_->RawPtrs_, entity);
+ }
+
+ if (auto it = RawPtrs_.find(entity)) {
+ return it->second;
+ }
+ auto key = GenerateSerializationKey();
+ EmplaceOrCrash(RawPtrs_, entity, key);
+ return InlineKey;
+}
+
+template <class T>
+TEntitySerializationKey TEntityStreamSaveContext::RegisterRefCountedEntity(const TIntrusivePtr<T>& entity)
+{
+ if (ParentContext_) {
+ return GetOrCrash(ParentContext_->RefCountedPtrs_, entity);
+ }
+
+ if (auto it = RefCountedPtrs_.find(entity)) {
+ return it->second;
+ }
+
+ auto key = GenerateSerializationKey();
+ EmplaceOrCrash(RefCountedPtrs_, entity, key);
+ return InlineKey;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+inline TEntitySerializationKey TEntityStreamLoadContext::RegisterRawEntity(T* entity)
+{
+ auto key = TEntitySerializationKey(std::ssize(RawPtrs_));
+ RawPtrs_.push_back(entity);
+ return key;
+}
+
+template <class T>
+TEntitySerializationKey TEntityStreamLoadContext::RegisterRefCountedEntity(const TIntrusivePtr<T>& entity)
+{
+ auto* ptr = entity.Get();
+ RefCountedPtrs_.push_back(entity);
+ return RegisterRawEntity(ptr);
+}
+
+template <class T>
+T* TEntityStreamLoadContext::GetRawEntity(TEntitySerializationKey key) const
+{
+ YT_ASSERT(key.Index >= 0);
+ YT_ASSERT(key.Index < std::ssize(RawPtrs_));
+ return static_cast<T*>(RawPtrs_[key.Index]);
+}
+
+template <class T>
+TIntrusivePtr<T> TEntityStreamLoadContext::GetRefCountedEntity(TEntitySerializationKey key) const
+{
+ return TIntrusivePtr<T>(GetRawEntity<T>(key));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::TCustomPersistenceContext(TSaveContext& saveContext)
+ : SaveContext_(&saveContext)
+{ }
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::TCustomPersistenceContext(TLoadContext& loadContext)
+ : LoadContext_(&loadContext)
+{ }
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+bool TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::IsSave() const
+{
+ return SaveContext_ != nullptr;
+}
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+TSaveContext& TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::SaveContext() const
+{
+ YT_ASSERT(SaveContext_);
+ return *SaveContext_;
+}
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+bool TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::IsLoad() const
+{
+ return LoadContext_ != nullptr;
+}
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+TLoadContext& TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::LoadContext() const
+{
+ YT_ASSERT(LoadContext_);
+ return *LoadContext_;
+}
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+template <class TOtherContext>
+TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::operator TOtherContext() const
+{
+ return IsSave() ? TOtherContext(*SaveContext_) : TOtherContext(*LoadContext_);
+}
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+TSnapshotVersion TCustomPersistenceContext<TSaveContext, TLoadContext, TSnapshotVersion>::GetVersion() const
+{
+ return IsSave() ? SaveContext().GetVersion() : LoadContext().GetVersion();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class C, class... TArgs>
+void Save(C& context, const T& value, TArgs&&... args)
+{
+ TSerializerTraits<T, C>::TSerializer::Save(context, value, std::forward<TArgs>(args)...);
+}
+
+template <class T, class C, class... TArgs>
+void Load(C& context, T& value, TArgs&&... args)
+{
+ TSerializerTraits<T, C>::TSerializer::Load(context, value, std::forward<TArgs>(args)...);
+}
+
+template <class T, class C, class... TArgs>
+T Load(C& context, TArgs&&... args)
+{
+ T value{};
+ Load(context, value, std::forward<TArgs>(args)...);
+ return value;
+}
+
+template <class T, class C, class... TArgs>
+T LoadSuspended(C& context, TArgs&&... args)
+{
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ return Load<T, C, TArgs...>(context, std::forward<TArgs>(args)...);
+ }
+}
+
+template <class S, class T, class C>
+void Persist(const C& context, T& value)
+{
+ if (context.IsSave()) {
+ S::Save(context.SaveContext(), value);
+ } else if (context.IsLoad()) {
+ S::Load(context.LoadContext(), value);
+ } else {
+ YT_ABORT();
+ }
+}
+
+struct TDefaultSerializer;
+
+template <class T, class C>
+void Persist(const C& context, T& value)
+{
+ Persist<TDefaultSerializer, T, C>(context, value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TPersistMemberTraits
+{
+ template <class S>
+ struct TSignatureCracker
+ { };
+
+ template <class U, class C>
+ struct TSignatureCracker<void (U::*)(C&)>
+ {
+ using TContext = C;
+ };
+
+ using TContext = typename TSignatureCracker<decltype(&T::Persist)>::TContext;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Simple types
+
+struct TValueBoundSerializer
+{
+ template <class T, class C, class = void>
+ struct TSaver
+ { };
+
+ template <class T, class C, class = void>
+ struct TLoader
+ { };
+
+ template <class T, class C>
+ struct TSaver<
+ T,
+ C,
+ decltype(std::declval<const T&>().Save(std::declval<C&>()), void())
+ >
+ {
+ static void Do(C& context, const T& value)
+ {
+ value.Save(context);
+ }
+ };
+
+ template <class T, class C>
+ struct TLoader<
+ T,
+ C,
+ decltype(std::declval<T&>().Load(std::declval<C&>()), void())>
+ {
+ static void Do(C& context, T& value)
+ {
+ value.Load(context);
+ }
+ };
+
+ template <class T, class C>
+ struct TSaver<
+ T,
+ C,
+ decltype(std::declval<T&>().Persist(std::declval<C&>()), void())>
+ {
+ static void Do(C& context, const T& value)
+ {
+ const_cast<T&>(value).Persist(context);
+ }
+ };
+
+ template <class T, class C>
+ struct TLoader<
+ T,
+ C,
+ decltype(std::declval<T&>().Persist(std::declval<C&>()), void())>
+ {
+ static void Do(C& context, T& value)
+ {
+ value.Persist(context);
+ }
+ };
+
+
+ template <class T, class C>
+ static void Save(C& context, const T& value)
+ {
+ // If you see an "ambiguous partial specializations of 'TSaver'" error then
+ // you've probably defined both Persist and Save methods. Remove one.
+ //
+ // If you see a "no member named 'Do' in 'TSaver'" then
+ // - either you're missing both Persist and Save methods on T
+ // - or the method arguments are wrong (hint: Persist takes a const ref).
+ TSaver<T, C>::Do(context, value);
+ }
+
+ template <class T, class C>
+ static void Load(C& context, T& value)
+ {
+ // If you see an "ambiguous partial specializations of TLoader" error then
+ // you've probably defined both Persist and Load methods. Remove one.
+ //
+ // If you see a "no member named 'Do' in 'TLoader'" then
+ // - either you're missing both Persist and Load methods on T
+ // - or the method arguments are wrong (hint: Persist takes a const ref).
+ TLoader<T, C>::Do(context, value);
+ }
+};
+
+struct TDefaultSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const T& value)
+ {
+ NYT::Save(context, value);
+ }
+
+ template <class T, class C>
+ static void Load(C& context, T& value)
+ {
+ NYT::Load(context, value);
+ }
+};
+
+struct TRangeSerializer
+{
+ template <class C>
+ static void Save(C& context, TRef value)
+ {
+ auto* output = context.GetOutput();
+ output->Write(value.Begin(), value.Size());
+ }
+
+ template <class C>
+ static void Load(C& context, const TMutableRef& value)
+ {
+ auto* input = context.GetInput();
+ ReadRef(*input, value);
+ SERIALIZATION_DUMP_WRITE(context, "raw[%v] %v", value.Size(), DumpRangeToHex(value));
+ }
+};
+
+struct TPodSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const T& value)
+ {
+ TRangeSerializer::Save(context, TRef::FromPod(value));
+ }
+
+ template <class T, class C>
+ static void Load(C& context, T& value)
+ {
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ TRangeSerializer::Load(context, TMutableRef::FromPod(value));
+ }
+ TSerializationDumpPodWriter<T>::Do(context, value);
+ }
+};
+
+struct TSizeSerializer
+{
+ template <class C>
+ static void Save(C& context, size_t value)
+ {
+ ui32 fixedValue = static_cast<ui32>(value);
+ TPodSerializer::Save(context, fixedValue);
+ }
+
+ template <class C>
+ static void Load(C& context, size_t& value)
+ {
+ ui32 fixedValue = 0;
+ TPodSerializer::Load(context, fixedValue);
+ value = static_cast<size_t>(fixedValue);
+ }
+
+
+ // Helpers.
+ template <class C>
+ static size_t Load(C& context)
+ {
+ size_t value = 0;
+ Load(context, value);
+ return value;
+ }
+
+ template <class C>
+ static size_t LoadSuspended(C& context)
+ {
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ return Load(context);
+ }
+ }
+};
+
+struct TSharedRefSerializer
+{
+ template <class C>
+ static void Save(C& context, const TSharedRef& value)
+ {
+ TSizeSerializer::Save(context, value.Size());
+ auto* output = context.GetOutput();
+ output->Write(value.Begin(), value.Size());
+ }
+
+ template <class C>
+ static void Load(C& context, TSharedRef& value)
+ {
+ return Load(context, value, TDefaultSharedBlobTag());
+ }
+
+ template <class C, class TTag>
+ static void Load(C& context, TSharedRef& value, TTag)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ auto mutableValue = TSharedMutableRef::Allocate<TTag>(size, {.InitializeStorage = false});
+
+ auto* input = context.GetInput();
+ ReadRef(*input, mutableValue);
+ value = mutableValue;
+
+ SERIALIZATION_DUMP_WRITE(context, "TSharedRef %v", DumpRangeToHex(value));
+ }
+};
+
+struct TSharedRefArraySerializer
+{
+ template <class C>
+ static void Save(C& context, const TSharedRefArray& value)
+ {
+ TSizeSerializer::Save(context, value.Size());
+
+ for (const auto& part : value) {
+ NYT::Save(context, part);
+ }
+ }
+
+ template <class C>
+ static void Load(C& context, TSharedRefArray& value)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ std::vector<TSharedRef> parts(size);
+
+ SERIALIZATION_DUMP_WRITE(context, "TSharedRefArray[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (int index = 0; index < static_cast<int>(size); ++index) {
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ NYT::Load(context, parts[index]);
+ }
+ SERIALIZATION_DUMP_WRITE(context, "%v => %v", index, DumpRangeToHex(parts[index]));
+ }
+ }
+
+ value = TSharedRefArray(std::move(parts), TSharedRefArray::TMoveParts{});
+ }
+};
+
+struct TEnumSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const T& value)
+ {
+ NYT::Save(context, static_cast<i32>(value));
+ }
+
+ template <class T, class C>
+ static void Load(C& context, T& value)
+ {
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ value = T(NYT::Load<i32>(context));
+ }
+
+ SERIALIZATION_DUMP_WRITE(context, "%v %v", TEnumTraits<T>::GetTypeName(), value);
+ }
+};
+
+struct TStringSerializer
+{
+ template <class C>
+ static void Save(C& context, TStringBuf value)
+ {
+ TSizeSerializer::Save(context, value.size());
+
+ TRangeSerializer::Save(context, TRef::FromStringBuf(value));
+ }
+
+ template <class C>
+ static void Load(C& context, TString& value)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ value.resize(size);
+
+ SERIALIZATION_DUMP_SUSPEND(context) {
+ TRangeSerializer::Load(context, TMutableRef::FromString(value));
+ }
+
+ SERIALIZATION_DUMP_WRITE(context, "TString %Qv", value);
+ }
+};
+
+template <class TUnderlyingSerializer = TDefaultSerializer>
+struct TOptionalSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const std::optional<T>& optional)
+ {
+ using NYT::Save;
+
+ Save(context, optional.operator bool());
+
+ if (optional) {
+ TUnderlyingSerializer::Save(context, *optional);
+ }
+ }
+
+ template <class T, class C>
+ static void Load(C& context, std::optional<T>& optional)
+ {
+ using NYT::Load;
+
+ bool hasValue = LoadSuspended<bool>(context);
+
+ if (hasValue) {
+ T temp{};
+ TUnderlyingSerializer::Load(context, temp);
+ optional = std::move(temp);
+ } else {
+ optional.reset();
+ SERIALIZATION_DUMP_WRITE(context, "null");
+ }
+ }
+};
+
+template <size_t Index, class... Ts>
+struct TVariantSerializerTraits;
+
+template <size_t Index, class T, class... Ts>
+struct TVariantSerializerTraits<Index, T, Ts...>
+{
+ template <class C, class V>
+ static void Save(C& context, const V& variant)
+ {
+ if (Index == variant.index()) {
+ NYT::Save(context, std::get<Index>(variant));
+ } else {
+ TVariantSerializerTraits<Index + 1, Ts...>::Save(context, variant);
+ }
+ }
+
+ template <class C, class V>
+ static void Load(C& context, size_t index, V& variant)
+ {
+ if (Index == index) {
+ variant = V(std::in_place_index_t<Index>());
+ NYT::Load(context, std::get<Index>(variant));
+ } else {
+ TVariantSerializerTraits<Index + 1, Ts...>::Load(context, index, variant);
+ }
+ }
+};
+
+template <size_t Index>
+struct TVariantSerializerTraits<Index>
+{
+ template <class C, class V>
+ static void Save(C& /*context*/, const V& /*variant*/)
+ {
+ // Invalid variant index.
+ YT_ABORT();
+ }
+
+ template <class C, class V>
+ static void Load(C& /*context*/, size_t /*index*/, V& /*variant*/)
+ {
+ // Invalid variant index.
+ YT_ABORT();
+ }
+};
+
+struct TVariantSerializer
+{
+ template <class... Ts, class C>
+ static void Save(C& context, const std::variant<Ts...>& variant)
+ {
+ NYT::Save<ui32>(context, variant.index());
+ TVariantSerializerTraits<0, Ts...>::Save(context, variant);
+ }
+
+ template <class... Ts, class C>
+ static void Load(C& context, std::variant<Ts...>& variant)
+ {
+ size_t index = NYT::Load<ui32>(context);
+ TVariantSerializerTraits<0, Ts...>::Load(context, index, variant);
+ }
+};
+
+template <class TUnderlyingSerializer = TDefaultSerializer>
+struct TAtomicSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const std::atomic<T>& value)
+ {
+ TUnderlyingSerializer::Save(context, value.load());
+ }
+
+ template <class T, class C>
+ static void Load(C& context, std::atomic<T>& value)
+ {
+ T temp;
+ TUnderlyingSerializer::Load(context, temp);
+ value.store(std::move(temp));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Sorters
+
+template <class T, class C>
+class TNoopSorter
+{
+public:
+ using TIterator = typename T::const_iterator;
+
+ explicit TNoopSorter(const T& any)
+ : Any_(any)
+ { }
+
+ TIterator begin()
+ {
+ return Any_.begin();
+ }
+
+ TIterator end()
+ {
+ return Any_.end();
+ }
+
+private:
+ const T& Any_;
+
+};
+
+template <class C>
+struct TValueSorterComparer
+{
+ template <class TIterator>
+ static bool Compare(TIterator lhs, TIterator rhs)
+ {
+ using T = typename std::remove_const<typename std::remove_reference<decltype(*lhs)>::type>::type;
+ using TComparer = typename TSerializerTraits<T, C>::TComparer;
+ return TComparer::Compare(*lhs, *rhs);
+ }
+};
+
+template <class C>
+struct TKeySorterComparer
+{
+ template <class TIterator>
+ static bool Compare(TIterator lhs, TIterator rhs)
+ {
+ using T = typename std::remove_const<typename std::remove_reference<decltype(lhs->first)>::type>::type;
+ using TComparer = typename TSerializerTraits<T, C>::TComparer;
+ return TComparer::Compare(lhs->first, rhs->first);
+ }
+};
+
+template <class C>
+struct TKeyValueSorterComparer
+{
+ template <class TIterator>
+ static bool Compare(TIterator lhs, TIterator rhs)
+ {
+ using TKey = typename std::remove_const<typename std::remove_reference<decltype(lhs->first)>::type>::type;
+ using TValue = typename std::remove_const<typename std::remove_reference<decltype(lhs->second)>::type>::type;
+ using TKeyComparer = typename TSerializerTraits<TKey, C>::TComparer;
+ using TValueComparer = typename TSerializerTraits<TValue, C>::TComparer;
+ if (TKeyComparer::Compare(lhs->first, rhs->first)) {
+ return true;
+ }
+ if (TKeyComparer::Compare(rhs->first, lhs->first)) {
+ return false;
+ }
+ return TValueComparer::Compare(lhs->second, rhs->second);
+ }
+};
+
+template <class T, class Q>
+class TCollectionSorter
+{
+public:
+ using TIterator = typename T::const_iterator;
+ using TIterators = TCompactVector<TIterator, 16>;
+
+ class TIteratorWrapper
+ {
+ public:
+ TIteratorWrapper(const TIterators* iterators, size_t index)
+ : Iterators_(iterators)
+ , Index_(index)
+ { }
+
+ const typename T::value_type& operator * ()
+ {
+ return *((*Iterators_)[Index_]);
+ }
+
+ TIteratorWrapper& operator ++ ()
+ {
+ ++Index_;
+ return *this;
+ }
+
+ bool operator == (const TIteratorWrapper& other) const
+ {
+ return Index_ == other.Index_;
+ }
+
+ bool operator != (const TIteratorWrapper& other) const
+ {
+ return Index_ != other.Index_;
+ }
+
+ private:
+ const TIterators* const Iterators_;
+ size_t Index_;
+
+ };
+
+ explicit TCollectionSorter(const T& set)
+ {
+ Iterators_.reserve(set.size());
+ for (auto it = set.begin(); it != set.end(); ++it) {
+ Iterators_.push_back(it);
+ }
+
+ std::sort(
+ Iterators_.begin(),
+ Iterators_.end(),
+ [] (TIterator lhs, TIterator rhs) {
+ return Q::Compare(lhs, rhs);
+ });
+ }
+
+ TIteratorWrapper begin() const
+ {
+ return TIteratorWrapper(&Iterators_, 0);
+ }
+
+ TIteratorWrapper end() const
+ {
+ return TIteratorWrapper(&Iterators_, Iterators_.size());
+ }
+
+private:
+ TIterators Iterators_;
+};
+
+struct TSortedTag { };
+struct TUnsortedTag { };
+
+template <class T, class C, class TTag>
+struct TSorterSelector
+{ };
+
+template <class T, class C>
+struct TSorterSelector<T, C, TUnsortedTag>
+{
+ using TSorter = TNoopSorter<T, C>;
+};
+
+template <class T, class C>
+struct TSorterSelector<std::vector<T>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<std::vector<T>, TValueSorterComparer<C>>;
+};
+
+template <class T, class C, size_t N>
+struct TSorterSelector<TCompactVector<T, N>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<TCompactVector<T, N>, TValueSorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::set<T...>, C, TSortedTag>
+{
+ using TSorter = TNoopSorter<std::set<T...>, C>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::map<T...>, C, TSortedTag>
+{
+ using TSorter = TNoopSorter<std::map<T...>, C>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::unordered_set<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<std::unordered_set<T...>, TValueSorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<THashSet<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<THashSet<T...>, TValueSorterComparer<C>>;
+};
+
+template <class C, class T, size_t N, class Q>
+struct TSorterSelector<TCompactSet<T, N, Q>, C, TSortedTag>
+{
+ typedef TNoopSorter<TCompactSet<T, N, Q>, C> TSorter;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::unordered_multiset<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<std::unordered_multiset<T...>, TValueSorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<THashMultiSet<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<THashMultiSet<T...>, TValueSorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::unordered_map<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<std::unordered_map<T...>, TKeySorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<THashMap<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<THashMap<T...>, TKeySorterComparer<C>>;
+};
+
+template <class K, class V, size_t N, class C>
+struct TSorterSelector<TCompactFlatMap<K, V, N>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<TCompactFlatMap<K, V, N>, TKeySorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<std::unordered_multimap<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<std::unordered_map<T...>, TKeyValueSorterComparer<C>>;
+};
+
+template <class C, class... T>
+struct TSorterSelector<THashMultiMap<T...>, C, TSortedTag>
+{
+ using TSorter = TCollectionSorter<THashMultiMap<T...>, TKeyValueSorterComparer<C>>;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Ordered collections
+
+template <
+ class TItemSerializer = TDefaultSerializer,
+ class TSortTag = TUnsortedTag
+>
+struct TVectorSerializer
+{
+ template <class TVectorType, class C>
+ static void Save(C& context, const TVectorType& objects)
+ {
+ TSizeSerializer::Save(context, objects.size());
+
+ typename TSorterSelector<TVectorType, C, TSortTag>::TSorter sorter(objects);
+ for (const auto& object : sorter) {
+ TItemSerializer::Save(context, object);
+ }
+ }
+
+ template <class TVectorType, class C>
+ static void Load(C& context, TVectorType& objects)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ objects.resize(size);
+
+ SERIALIZATION_DUMP_WRITE(context, "vector[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, objects[index]);
+ }
+ }
+ }
+ }
+};
+
+template <
+ class TItemSerializer = TDefaultSerializer,
+ class TSortTag = TUnsortedTag
+>
+struct TOptionalVectorSerializer
+{
+ template <class TVectorType, class C>
+ static void Save(C& context, const std::unique_ptr<TVectorType>& objects)
+ {
+ if (objects) {
+ TVectorSerializer<TItemSerializer, TSortTag>::Save(context, *objects);
+ } else {
+ TSizeSerializer::Save(context, 0);
+ }
+ }
+
+ template <class TVectorType, class C>
+ static void Load(C& context, std::unique_ptr<TVectorType>& objects)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ if (size == 0) {
+ objects.reset();
+ return;
+ }
+
+ objects.reset(new TVectorType());
+ objects->resize(size);
+
+ SERIALIZATION_DUMP_WRITE(context, "vector[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, (*objects)[index]);
+ }
+ }
+ }
+ }
+};
+
+template <class TItemSerializer = TDefaultSerializer>
+struct TListSerializer
+{
+ template <class TListType, class C>
+ static void Save(C& context, const TListType& objects)
+ {
+ TSizeSerializer::Save(context, objects.size());
+
+ for (const auto& object : objects) {
+ TItemSerializer::Save(context, object);
+ }
+ }
+
+ template <class TListType, class C>
+ static void Load(C& context, TListType& objects)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ objects.clear();
+
+ SERIALIZATION_DUMP_WRITE(context, "list[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ typename TListType::value_type obj;
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, obj);
+ }
+ objects.push_back(obj);
+ }
+ }
+ }
+};
+
+template <class TItemSerializer = TDefaultSerializer>
+struct TArraySerializer
+{
+ template <class TArray, class C>
+ static void Save(C& context, const TArray& objects)
+ {
+ TSizeSerializer::Save(context, objects.size());
+
+ for (const auto& object : objects) {
+ TItemSerializer::Save(context, object);
+ }
+ }
+
+ template <
+ class C,
+ class T,
+ std::size_t N,
+ template <typename U, std::size_t S> class TArray
+ >
+ static void Load(C& context, TArray<T,N>& objects)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+ if (size > N) {
+ TCrashOnDeserializationErrorGuard::OnError();
+ THROW_ERROR_EXCEPTION("Array size is too large: expected <= %v, actual %v",
+ N,
+ size);
+ }
+
+ SERIALIZATION_DUMP_WRITE(context, "array[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, objects[index]);
+ }
+ }
+ }
+ }
+};
+
+template <class TItemSerializer = TDefaultSerializer>
+struct TOptionalListSerializer
+{
+ template <class TListType, class C>
+ static void Save(C& context, const std::unique_ptr<TListType>& objects)
+ {
+ using NYT::Save;
+ if (objects) {
+ TListSerializer<TItemSerializer>::Save(context, *objects);
+ } else {
+ TSizeSerializer::Save(context, 0);
+ }
+ }
+
+ template <class TListType, class C>
+ static void Load(C& context, std::unique_ptr<TListType>& objects)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ SERIALIZATION_DUMP_WRITE(context, "list[%v]", size);
+
+ if (size == 0) {
+ objects.reset();
+ return;
+ }
+
+ objects.reset(new TListType());
+
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ typename TListType::value_type obj;
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, obj);
+ }
+ objects->push_back(obj);
+ }
+ }
+ }
+};
+
+template <class TItemSerializer = TDefaultSerializer>
+struct TEnumIndexedVectorSerializer
+{
+ template <class E, class T, class C, E Min, E Max>
+ static void Save(C& context, const TEnumIndexedVector<E, T, Min, Max>& vector)
+ {
+ using NYT::Save;
+
+ auto keys = TEnumTraits<E>::GetDomainValues();
+ size_t count = 0;
+ for (auto key : keys) {
+ if (!vector.IsDomainValue(key)) {
+ continue;
+ }
+ ++count;
+ }
+
+ TSizeSerializer::Save(context, count);
+
+ for (auto key : keys) {
+ if (!vector.IsDomainValue(key)) {
+ continue;
+ }
+ Save(context, key);
+ TItemSerializer::Save(context, vector[key]);
+ }
+ }
+
+ template <class E, class T, class C, E Min, E Max>
+ static void Load(C& context, TEnumIndexedVector<E, T, Min, Max>& vector)
+ {
+ if constexpr (std::is_copy_assignable_v<T>) {
+ std::fill(vector.begin(), vector.end(), T());
+ } else {
+ // Use move assignment.
+ for (auto& item : vector) {
+ item = T();
+ }
+ }
+
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ SERIALIZATION_DUMP_WRITE(context, "vector[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index != size; ++index) {
+ auto key = LoadSuspended<E>(context);
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", key);
+ SERIALIZATION_DUMP_INDENT(context) {
+ if (!vector.IsDomainValue(key)) {
+ T dummy;
+ TItemSerializer::Load(context, dummy);
+ } else {
+ TItemSerializer::Load(context, vector[key]);
+ }
+ }
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Possibly unordered collections
+
+template <
+ class TItemSerializer = TDefaultSerializer,
+ class TSortTag = TSortedTag
+>
+struct TSetSerializer
+{
+ template <class TSetType, class C>
+ static void Save(C& context, const TSetType& set)
+ {
+ TSizeSerializer::Save(context, set.size());
+
+ typename TSorterSelector<TSetType, C, TSortTag>::TSorter sorter(set);
+ for (const auto& item : sorter) {
+ TItemSerializer::Save(context, item);
+ }
+ }
+
+ template <class TSetType, class C>
+ static void Load(C& context, TSetType& set)
+ {
+ using TKey = typename TSetType::key_type;
+
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ set.clear();
+
+ SERIALIZATION_DUMP_WRITE(context, "set[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index < size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+
+ TKey key;
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, key);
+ }
+
+ InsertOrCrash(set, key);
+ }
+ }
+ }
+};
+
+template <
+ class TItemSerializer = TDefaultSerializer,
+ class TSortTag = TSortedTag
+>
+struct TMultiSetSerializer
+{
+ template <class TSetType, class C>
+ static void Save(C& context, const TSetType& set)
+ {
+ TSetSerializer<TItemSerializer, TSortTag>::Save(context, set);
+ }
+
+ template <class TSetType, class C>
+ static void Load(C& context, TSetType& set)
+ {
+ using TKey = typename TSetType::key_type;
+
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ set.clear();
+
+ SERIALIZATION_DUMP_WRITE(context, "multiset[%v]", size);
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index < size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+
+ TKey key;
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, key);
+ }
+
+ set.emplace(std::move(key));
+ }
+ }
+ }
+};
+
+template <
+ class TItemSerializer = TDefaultSerializer,
+ class TSortTag = TSortedTag
+>
+struct TOptionalSetSerializer
+{
+ template <class TSetType, class C>
+ static void Save(C& context, const std::unique_ptr<TSetType>& set)
+ {
+ if (set) {
+ TSetSerializer<TItemSerializer, TSortTag>::Save(context, *set);
+ } else {
+ TSizeSerializer::Save(context, 0);
+ }
+ }
+
+ template <class TSetType, class C>
+ static void Load(C& context, std::unique_ptr<TSetType>& set)
+ {
+ using TKey = typename TSetType::key_type;
+
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ SERIALIZATION_DUMP_WRITE(context, "set[%v]", size);
+
+ if (size == 0) {
+ set.reset();
+ return;
+ }
+
+ set.reset(new TSetType());
+
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index < size; ++index) {
+ SERIALIZATION_DUMP_WRITE(context, "%v =>", index);
+
+ TKey key;
+ SERIALIZATION_DUMP_INDENT(context) {
+ TItemSerializer::Load(context, key);
+ }
+
+ InsertOrCrash(*set, key);
+ }
+ }
+ }
+};
+
+template <
+ class TKeySerializer = TDefaultSerializer,
+ class TValueSerializer = TDefaultSerializer,
+ class TSortTag = TSortedTag
+>
+struct TMapSerializer
+{
+ template <class TMapType, class C>
+ static void Save(C& context, const TMapType& map)
+ {
+ TSizeSerializer::Save(context, map.size());
+
+ typename TSorterSelector<TMapType, C, TSortTag>::TSorter sorter(map);
+ for (const auto& [key, value] : sorter) {
+ TKeySerializer::Save(context, key);
+ TValueSerializer::Save(context, value);
+ }
+ }
+
+ template <class TMapType, class C>
+ static void Load(C& context, TMapType& map)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ SERIALIZATION_DUMP_WRITE(context, "map[%v]", size);
+
+ map.clear();
+
+ SERIALIZATION_DUMP_INDENT(context) {
+ for (size_t index = 0; index < size; ++index) {
+ typename TMapType::key_type key{};
+ TKeySerializer::Load(context, key);
+
+ SERIALIZATION_DUMP_WRITE(context, "=>");
+
+ typename TMapType::mapped_type value{};
+ SERIALIZATION_DUMP_INDENT(context) {
+ TValueSerializer::Load(context, value);
+ }
+
+ EmplaceOrCrash(map, std::move(key), std::move(value));
+ }
+ }
+ }
+};
+
+template <
+ class TKeySerializer = TDefaultSerializer,
+ class TValueSerializer = TDefaultSerializer,
+ class TSortTag = TSortedTag
+>
+struct TMultiMapSerializer
+{
+ template <class TMapType, class C>
+ static void Save(C& context, const TMapType& map)
+ {
+ TMapSerializer<
+ TDefaultSerializer,
+ TDefaultSerializer,
+ TSortTag
+ >::Save(context, map);
+ }
+
+ template <class TMapType, class C>
+ static void Load(C& context, TMapType& map)
+ {
+ size_t size = TSizeSerializer::LoadSuspended(context);
+
+ SERIALIZATION_DUMP_WRITE(context, "multimap[%v]", size);
+
+ map.clear();
+
+ for (size_t index = 0; index < size; ++index) {
+ typename TMapType::key_type key{};
+ TKeySerializer::Load(context, key);
+
+ SERIALIZATION_DUMP_WRITE(context, "=>");
+
+ typename TMapType::mapped_type value{};
+ SERIALIZATION_DUMP_INDENT(context) {
+ TValueSerializer::Load(context, value);
+ }
+
+ map.emplace(std::move(key), std::move(value));
+ }
+ }
+};
+
+template <class T, size_t Size = std::tuple_size<T>::value>
+struct TTupleSerializer;
+
+template <class T>
+struct TTupleSerializer<T, 0U>
+{
+ template<class C>
+ static void Save(C&, const T&) {}
+
+ template<class C>
+ static void Load(C&, T&) {}
+};
+
+template <class T, size_t Size>
+struct TTupleSerializer
+{
+ template<class C>
+ static void Save(C& context, const T& tuple)
+ {
+ TTupleSerializer<T, Size - 1U>::Save(context, tuple);
+ NYT::Save(context, std::get<Size - 1U>(tuple));
+ }
+
+ template<class C>
+ static void Load(C& context, T& tuple)
+ {
+ TTupleSerializer<T, Size - 1U>::Load(context, tuple);
+ NYT::Load(context, std::get<Size - 1U>(tuple));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TValueBoundComparer
+{
+ template <class T>
+ static bool Compare(const T& lhs, const T& rhs)
+ {
+ return lhs < rhs;
+ }
+};
+
+template <class T, class C, class>
+struct TSerializerTraits
+{
+ using TSerializer = TValueBoundSerializer;
+ using TComparer = TValueBoundComparer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TUnderlyingSerializer = TDefaultSerializer>
+struct TUniquePtrSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const std::unique_ptr<T>& ptr)
+ {
+ using NYT::Save;
+ if (ptr) {
+ Save(context, true);
+ TUnderlyingSerializer::Save(context, *ptr);
+ } else {
+ Save(context, false);
+ }
+ }
+
+ template <class T, class C>
+ static void Load(C& context, std::unique_ptr<T>& ptr)
+ {
+ if (LoadSuspended<bool>(context)) {
+ ptr = std::make_unique<T>();
+ TUnderlyingSerializer::Load(context, *ptr);
+ } else {
+ ptr.reset();
+ SERIALIZATION_DUMP_WRITE(context, "nullptr");
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TUnderlyingSerializer = TDefaultSerializer>
+struct TNonNullableIntrusivePtrSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const TIntrusivePtr<T>& ptr)
+ {
+ using NYT::Save;
+ TUnderlyingSerializer::Save(context, *ptr);
+ }
+
+ template <class T, class C>
+ static void Load(C& context, TIntrusivePtr<T>& ptr)
+ {
+ ptr = New<T>();
+ TUnderlyingSerializer::Load(context, *ptr);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TUnderlyingSerializer = TDefaultSerializer>
+struct TNullableIntrusivePtrSerializer
+{
+ template <class T, class C>
+ static void Save(C& context, const TIntrusivePtr<T>& ptr)
+ {
+ using NYT::Save;
+
+ Save(context, ptr.operator bool());
+
+ if (ptr) {
+ TUnderlyingSerializer::Save(context, *ptr);
+ }
+ }
+
+ template <class T, class C>
+ static void Load(C& context, TIntrusivePtr<T>& ptr)
+ {
+ using NYT::Load;
+
+ auto hasValue = LoadSuspended<bool>(context);
+
+ if (hasValue) {
+ ptr = New<T>();
+ TUnderlyingSerializer::Load(context, *ptr);
+ } else {
+ ptr.Reset();
+ SERIALIZATION_DUMP_WRITE(context, "nullptr");
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class C>
+struct TSerializerTraits<TSharedRef, C, void>
+{
+ using TSerializer = TSharedRefSerializer;
+};
+
+template <class C>
+struct TSerializerTraits<TSharedRefArray, C, void>
+{
+ using TSerializer = TSharedRefArraySerializer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ typename std::enable_if<NMpl::TIsPod<T>::value && !std::is_pointer<T>::value>::type
+>
+{
+ using TSerializer = TPodSerializer;
+ using TComparer = TValueBoundComparer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ typename TEnumTraits<T>::TType
+>
+{
+ using TSerializer = TEnumSerializer;
+ using TComparer = TValueBoundComparer;
+};
+
+template <class C>
+struct TSerializerTraits<TString, C, void>
+{
+ using TSerializer = TStringSerializer;
+ using TComparer = TValueBoundComparer;
+};
+
+// For save only.
+template <class C>
+struct TSerializerTraits<TStringBuf, C, void>
+{
+ using TSerializer = TStringSerializer;
+ using TComparer = TValueBoundComparer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<std::optional<T>, C, void>
+{
+ using TSerializer = TOptionalSerializer<>;
+ using TComparer = TValueBoundComparer;
+};
+
+template <class... Ts, class C>
+struct TSerializerTraits<std::variant<Ts...>, C, void>
+{
+ using TSerializer = TVariantSerializer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<std::atomic<T>, C, void>
+{
+ using TSerializer = TAtomicSerializer<>;
+};
+
+template <class T, class A, class C>
+struct TSerializerTraits<std::vector<T, A>, C, void>
+{
+ using TSerializer = TVectorSerializer<>;
+};
+
+template <class T, size_t N, class C>
+struct TSerializerTraits<TCompactVector<T, N>, C, void>
+{
+ using TSerializer = TVectorSerializer<>;
+};
+
+template <class T, std::size_t size, class C>
+struct TSerializerTraits<std::array<T, size>, C, void>
+{
+ using TSerializer = TArraySerializer<>;
+};
+
+template <class T, class A, class C>
+struct TSerializerTraits<std::list<T, A>, C, void>
+{
+ using TSerializer = TListSerializer<>;
+};
+
+template <class T, class C>
+struct TSerializerTraits<std::deque<T>, C, void>
+{
+ using TSerializer = TListSerializer<>;
+};
+
+template <class T, class Q, class A, class C>
+struct TSerializerTraits<std::set<T, Q, A>, C, void>
+{
+ using TSerializer = TSetSerializer<>;
+};
+template <class T, class H, class P, class A, class C>
+struct TSerializerTraits<std::unordered_set<T, H, P, A>, C, void>
+{
+ using TSerializer = TSetSerializer<>;
+};
+
+template <class T, class H, class E, class A, class C>
+struct TSerializerTraits<THashSet<T, H, E, A>, C, void>
+{
+ using TSerializer = TSetSerializer<>;
+};
+
+template <class T, size_t N, class Q, class C>
+struct TSerializerTraits<TCompactSet<T, N, Q>, C, void>
+{
+ typedef TSetSerializer<> TSerializer;
+};
+
+template <class T, class C>
+struct TSerializerTraits<THashMultiSet<T>, C, void>
+{
+ using TSerializer = TMultiSetSerializer<>;
+};
+
+template <class T, class A, class C>
+struct TSerializerTraits<std::unique_ptr<std::vector<T, A>>, C, void>
+{
+ using TSerializer = TOptionalVectorSerializer<>;
+};
+
+template <class T, size_t size, class C>
+struct TSerializerTraits<std::unique_ptr<TCompactVector<T, size>>, C, void>
+{
+ using TSerializer = TOptionalVectorSerializer<>;
+};
+
+template <class T, class A, class C>
+struct TSerializerTraits<std::unique_ptr<std::list<T, A>>, C, void>
+{
+ using TSerializer = TOptionalListSerializer<>;
+};
+
+template <class T, class Q, class A, class C>
+struct TSerializerTraits<std::unique_ptr<std::set<T, Q, A>>, C, void>
+{
+ using TSerializer = TOptionalSetSerializer<>;
+};
+
+template <class T, class H, class P, class A, class C>
+struct TSerializerTraits<std::unique_ptr<std::unordered_set<T, H, P, A>>, C, void>
+{
+ using TSerializer = TOptionalSetSerializer<>;
+};
+
+template <class T, class H, class E, class A, class C>
+struct TSerializerTraits<std::unique_ptr<THashSet<T, H, E, A>>, C, void>
+{
+ using TSerializer = TOptionalSetSerializer<>;
+};
+
+template <class K, class V, class Q, class A, class C>
+struct TSerializerTraits<std::map<K, V, Q, A>, C, void>
+{
+ using TSerializer = TMapSerializer<>;
+};
+
+template <class K, class V, class H, class P, class A, class C>
+struct TSerializerTraits<std::unordered_map<K, V, H, P, A>, C, void>
+{
+ using TSerializer = TMapSerializer<>;
+};
+
+template <class K, class V, class Q, class A, class C>
+struct TSerializerTraits<THashMap<K, V, Q, A>, C, void>
+{
+ using TSerializer = TMapSerializer<>;
+};
+
+template <class K, class V, size_t N, class C>
+struct TSerializerTraits<TCompactFlatMap<K, V, N>, C, void>
+{
+ using TSerializer = TMapSerializer<>;
+};
+
+template <class K, class V, class Q, class A, class C>
+struct TSerializerTraits<std::multimap<K, V, Q, A>, C, void>
+{
+ using TSerializer = TMultiMapSerializer<>;
+};
+
+template <class K, class V, class C>
+struct TSerializerTraits<THashMultiMap<K, V>, C, void>
+{
+ using TSerializer = TMultiMapSerializer<>;
+};
+
+template <class E, class T, class C, E Min, E Max>
+struct TSerializerTraits<TEnumIndexedVector<E, T, Min, Max>, C, void>
+{
+ using TSerializer = TEnumIndexedVectorSerializer<>;
+};
+
+template <class F, class S, class C>
+struct TSerializerTraits<std::pair<F, S>, C, typename std::enable_if<!NMpl::TIsPod<std::pair<F, S>>::value>::type>
+{
+ using TSerializer = TTupleSerializer<std::pair<F, S>>;
+ using TComparer = TValueBoundComparer;
+};
+
+template <class T, class TTag, class C>
+struct TSerializerTraits<TStrongTypedef<T, TTag>, C, void>
+{
+ struct TSerializer
+ {
+ static void Save(C& context, const TStrongTypedef<T, TTag>& value)
+ {
+ NYT::Save(context, value.Underlying());
+ }
+
+ static void Load(C& context, TStrongTypedef<T, TTag>& value)
+ {
+ NYT::Load(context, value.Underlying());
+ }
+ };
+
+ using TComparer = TValueBoundComparer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/serialize.cpp b/yt/yt/core/misc/serialize.cpp
new file mode 100644
index 0000000000..483d4e93e8
--- /dev/null
+++ b/yt/yt/core/misc/serialize.cpp
@@ -0,0 +1,181 @@
+#include "serialize.h"
+
+#include <yt/yt/core/concurrency/fls.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::array<ui8, ZeroBufferSize> ZeroBuffer{};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void AssertSerializationAligned(i64 byteSize)
+{
+ YT_ASSERT(AlignUpSpace<i64>(byteSize, SerializationAlignment) == 0);
+}
+
+void VerifySerializationAligned(i64 byteSize)
+{
+ YT_VERIFY(AlignUpSpace<i64>(byteSize, SerializationAlignment) == 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+int& CrashOnErrorDepth()
+{
+ static NConcurrency::TFlsSlot<int> Slot;
+ return *Slot;
+}
+
+} // namespace
+
+TCrashOnDeserializationErrorGuard::TCrashOnDeserializationErrorGuard()
+{
+ ++CrashOnErrorDepth();
+}
+
+TCrashOnDeserializationErrorGuard::~TCrashOnDeserializationErrorGuard()
+{
+ YT_VERIFY(--CrashOnErrorDepth() >= 0);
+}
+
+void TCrashOnDeserializationErrorGuard::OnError()
+{
+ YT_VERIFY(CrashOnErrorDepth() == 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSaveContextStream::TSaveContextStream(IOutputStream* output)
+ : BufferedOutput_(output)
+ , Output_(&*BufferedOutput_)
+{ }
+
+TSaveContextStream::TSaveContextStream(IZeroCopyOutput* output)
+ : Output_(output)
+{ }
+
+void TSaveContextStream::FlushBuffer()
+{
+ Output_->Undo(BufferRemaining_);
+ BufferPtr_ = nullptr;
+ BufferRemaining_ = 0;
+ Output_->Flush();
+ if (BufferedOutput_) {
+ BufferedOutput_->Flush();
+ }
+}
+
+void TSaveContextStream::WriteSlow(const void* buf, size_t len)
+{
+ auto bufPtr = static_cast<const char*>(buf);
+ auto toWrite = len;
+ while (toWrite > 0) {
+ if (BufferRemaining_ == 0) {
+ BufferRemaining_ = Output_->Next(&BufferPtr_);
+ }
+ YT_ASSERT(BufferRemaining_ > 0);
+ auto toCopy = std::min(toWrite, BufferRemaining_);
+ ::memcpy(BufferPtr_, bufPtr, toCopy);
+ BufferPtr_ += toCopy;
+ BufferRemaining_ -= toCopy;
+ bufPtr += toCopy;
+ toWrite -= toCopy;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStreamSaveContext::TStreamSaveContext(
+ IOutputStream* output,
+ int version)
+ : Output_(output)
+ , Version_(version)
+{ }
+
+TStreamSaveContext::TStreamSaveContext(
+ IZeroCopyOutput* output,
+ int version)
+ : Output_(output)
+ , Version_(version)
+{ }
+
+void TStreamSaveContext::Finish()
+{
+ Output_.FlushBuffer();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLoadContextStream::TLoadContextStream(IInputStream* input)
+ : Input_(input)
+{ }
+
+TLoadContextStream::TLoadContextStream(IZeroCopyInput* input)
+ : ZeroCopyInput_(input)
+{ }
+
+void TLoadContextStream::ClearBuffer()
+{
+ if (BufferRemaining_ > 0) {
+ BufferPtr_ = nullptr;
+ BufferRemaining_ = 0;
+ }
+}
+
+size_t TLoadContextStream::LoadSlow(void* buf, size_t len)
+{
+ if (ZeroCopyInput_) {
+ auto bufPtr = static_cast<char*>(buf);
+ auto toRead = len;
+ while (toRead > 0) {
+ if (BufferRemaining_ == 0) {
+ BufferRemaining_ = ZeroCopyInput_->Next(&BufferPtr_);
+ if (BufferRemaining_ == 0) {
+ break;
+ }
+ }
+ YT_ASSERT(BufferRemaining_ > 0);
+ auto toCopy = std::min(toRead, BufferRemaining_);
+ ::memcpy(bufPtr, BufferPtr_, toCopy);
+ BufferPtr_ += toCopy;
+ BufferRemaining_ -= toCopy;
+ bufPtr += toCopy;
+ toRead -= toCopy;
+ }
+ return len - toRead;
+ } else {
+ return Input_->Load(buf, len);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStreamLoadContext::TStreamLoadContext(IInputStream* input)
+ : Input_(input)
+{ }
+
+TStreamLoadContext::TStreamLoadContext(IZeroCopyInput* input)
+ : Input_(input)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEntityStreamSaveContext::TEntityStreamSaveContext(
+ IZeroCopyOutput* output,
+ TEntityStreamSaveContext* parentContext)
+ : TStreamSaveContext(
+ output,
+ parentContext->GetVersion())
+ , ParentContext_(parentContext)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/serialize.h b/yt/yt/core/misc/serialize.h
new file mode 100644
index 0000000000..d633597104
--- /dev/null
+++ b/yt/yt/core/misc/serialize.h
@@ -0,0 +1,299 @@
+#pragma once
+
+#include "public.h"
+#include "error.h"
+#include "mpl.h"
+#include "property.h"
+#include "serialize_dump.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/stream/buffered.h>
+#include <util/stream/file.h>
+#include <util/stream/zerocopy_output.h>
+
+#include <util/system/align.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Alignment size; measured in bytes and must be a power of two.
+constexpr size_t SerializationAlignment = 8;
+static_assert(
+ (SerializationAlignment & (SerializationAlignment - 1)) == 0,
+ "SerializationAlignment should be a power of two");
+
+//! The size of the zero buffer used by #WriteZeroes and #WritePadding.
+constexpr size_t ZeroBufferSize = 64_KB;
+static_assert(
+ ZeroBufferSize >= SerializationAlignment,
+ "ZeroBufferSize < SerializationAlignment");
+extern const std::array<ui8, ZeroBufferSize> ZeroBuffer;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! When active, causes the process on crash on a deserialization error is
+//! encountered. (The default is to throw an exception.)
+class TCrashOnDeserializationErrorGuard
+{
+public:
+ TCrashOnDeserializationErrorGuard();
+ ~TCrashOnDeserializationErrorGuard();
+
+ TCrashOnDeserializationErrorGuard(const TCrashOnDeserializationErrorGuard&) = delete;
+ TCrashOnDeserializationErrorGuard(TCrashOnDeserializationErrorGuard&&) = delete;
+
+ static void OnError();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TInput>
+void ReadRef(TInput& input, TMutableRef ref);
+void ReadRef(const char*& ptr, TMutableRef ref);
+template <class TOutput>
+void WriteRef(TOutput& output, TRef ref);
+void WriteRef(char*& ptr, TRef ref);
+
+template <class TInput, class T>
+void ReadPod(TInput& input, T& obj);
+template <class TInput, class T>
+void ReadPod(const char*& ptr, T& obj);
+template <class TOutput, class T>
+void WritePod(TOutput& output, const T& obj);
+template <class T>
+void WritePod(char*& ptr, const T& obj);
+
+template <class TOutput>
+void WriteZeroes(TOutput& output, size_t count);
+void WriteZeroes(char*& ptr, size_t count);
+
+template <class TOutput>
+void WritePadding(TOutput& output, size_t sizeToPad);
+void WritePadding(char*& ptr, size_t sizeToPad);
+
+template <class TInput>
+void ReadPadding(TInput& input, size_t sizeToPad);
+void ReadPadding(const char*& ptr, size_t sizeToPad);
+
+template <class T>
+TSharedRef PackRefs(const T& parts);
+template <class T>
+void UnpackRefs(const TSharedRef& packedRef, T* parts);
+std::vector<TSharedRef> UnpackRefs(const TSharedRef& packedRef);
+
+template <class TTag, class TParts>
+TSharedRef MergeRefsToRef(const TParts& parts);
+template <class TParts>
+void MergeRefsToRef(const TParts& parts, TMutableRef dst);
+template <class TParts>
+TString MergeRefsToString(const TParts& parts);
+
+void AssertSerializationAligned(i64 byteSize);
+void VerifySerializationAligned(i64 byteSize);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSaveContextStream
+{
+public:
+ explicit TSaveContextStream(IOutputStream* output);
+ explicit TSaveContextStream(IZeroCopyOutput* output);
+
+ void Write(const void* buf, size_t len);
+ void FlushBuffer();
+
+private:
+ std::optional<TBufferedOutput> BufferedOutput_;
+ IZeroCopyOutput* const Output_;
+
+ char* BufferPtr_ = nullptr;
+ size_t BufferRemaining_ = 0;
+
+ void WriteSlow(const void* buf, size_t len);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamSaveContext
+{
+public:
+ explicit TStreamSaveContext(
+ IOutputStream* output,
+ int version = 0);
+ explicit TStreamSaveContext(
+ IZeroCopyOutput* output,
+ int version = 0);
+
+ virtual ~TStreamSaveContext() = default;
+
+ TSaveContextStream* GetOutput();
+ int GetVersion() const;
+
+ void Finish();
+
+protected:
+ TSaveContextStream Output_;
+ const int Version_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLoadContextStream
+{
+public:
+ explicit TLoadContextStream(IInputStream* input);
+ explicit TLoadContextStream(IZeroCopyInput* input);
+
+ size_t Load(void* buf, size_t len);
+ void ClearBuffer();
+
+private:
+ IInputStream* const Input_ = nullptr;
+ IZeroCopyInput* const ZeroCopyInput_ = nullptr;
+
+ char* BufferPtr_ = nullptr;
+ size_t BufferRemaining_ = 0;
+
+ size_t LoadSlow(void* buf, size_t len);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStreamLoadContext
+{
+public:
+ DEFINE_BYVAL_RW_PROPERTY(int, Version);
+ DEFINE_BYREF_RW_PROPERTY(TSerializationDumper, Dumper);
+ DEFINE_BYVAL_RW_PROPERTY(bool, EnableTotalWriteCountReport);
+
+public:
+ explicit TStreamLoadContext(IInputStream* input);
+ explicit TStreamLoadContext(IZeroCopyInput* input);
+
+ virtual ~TStreamLoadContext() = default;
+
+ TLoadContextStream* GetInput();
+
+protected:
+ TLoadContextStream Input_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TSaveContext, class TLoadContext, class TSnapshotVersion>
+class TCustomPersistenceContext
+{
+public:
+ // Deliberately not explicit.
+ TCustomPersistenceContext(TSaveContext& saveContext);
+ TCustomPersistenceContext(TLoadContext& loadContext);
+
+ bool IsSave() const;
+ TSaveContext& SaveContext() const;
+
+ bool IsLoad() const;
+ TLoadContext& LoadContext() const;
+
+ template <class TOtherContext>
+ operator TOtherContext() const;
+
+ TSnapshotVersion GetVersion() const;
+
+private:
+ TSaveContext* const SaveContext_ = nullptr;
+ TLoadContext* const LoadContext_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEntitySerializationKey
+{
+ constexpr TEntitySerializationKey();
+ constexpr explicit TEntitySerializationKey(int index);
+
+ constexpr bool operator == (TEntitySerializationKey rhs) const;
+ constexpr bool operator != (TEntitySerializationKey rhs) const;
+
+ constexpr explicit operator bool() const;
+
+ void Save(TEntityStreamSaveContext& context) const;
+ void Load(TEntityStreamLoadContext& context);
+
+ int Index;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEntityStreamSaveContext
+ : public TStreamSaveContext
+{
+public:
+ using TStreamSaveContext::TStreamSaveContext;
+
+ TEntityStreamSaveContext(
+ IZeroCopyOutput* output,
+ TEntityStreamSaveContext* parentContext);
+
+ TEntitySerializationKey GenerateSerializationKey();
+
+ static inline const TEntitySerializationKey InlineKey = TEntitySerializationKey(-3);
+
+ template <class T>
+ TEntitySerializationKey RegisterRawEntity(T* entity);
+ template <class T>
+ TEntitySerializationKey RegisterRefCountedEntity(const TIntrusivePtr<T>& entity);
+
+private:
+ const TEntityStreamSaveContext* const ParentContext_ = nullptr;
+
+ int SerializationKeyIndex_ = 0;
+ THashMap<void*, TEntitySerializationKey> RawPtrs_;
+ THashMap<TRefCountedPtr, TEntitySerializationKey> RefCountedPtrs_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEntityStreamLoadContext
+ : public TStreamLoadContext
+{
+public:
+ using TStreamLoadContext::TStreamLoadContext;
+
+ template <class T>
+ TEntitySerializationKey RegisterRawEntity(T* entity);
+ template <class T>
+ TEntitySerializationKey RegisterRefCountedEntity(const TIntrusivePtr<T>& entity);
+
+ template <class T>
+ T* GetRawEntity(TEntitySerializationKey key) const;
+ template <class T>
+ TIntrusivePtr<T> GetRefCountedEntity(TEntitySerializationKey key) const;
+
+private:
+ std::vector<void*> RawPtrs_;
+ std::vector<TIntrusivePtr<TRefCounted>> RefCountedPtrs_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class C, class... TArgs>
+void Save(C& context, const T& value, TArgs&&... args);
+
+template <class T, class C, class... TArgs>
+void Load(C& context, T& value, TArgs&&... args);
+
+template <class T, class C, class... TArgs>
+T Load(C& context, TArgs&&... args);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SERIALIZE_INL_H_
+#include "serialize-inl.h"
+#undef SERIALIZE_INL_H_
+
diff --git a/yt/yt/core/misc/serialize_dump.h b/yt/yt/core/misc/serialize_dump.h
new file mode 100644
index 0000000000..fef82b5219
--- /dev/null
+++ b/yt/yt/core/misc/serialize_dump.h
@@ -0,0 +1,261 @@
+#pragma once
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializationDumper
+{
+public:
+ Y_FORCE_INLINE bool IsEnabled() const
+ {
+ return Enabled_;
+ }
+
+ Y_FORCE_INLINE void SetEnabled(bool value)
+ {
+ Enabled_ = value;
+ }
+
+ Y_FORCE_INLINE void SetLowerWriteCountDumpLimit(i64 lowerLimit)
+ {
+ LowerWriteCountDumpLimit_ = lowerLimit;
+ }
+
+ Y_FORCE_INLINE void SetUpperWriteCountDumpLimit(i64 upperLimit)
+ {
+ UpperWriteCountDumpLimit_ = upperLimit;
+ }
+
+ Y_FORCE_INLINE void Indent()
+ {
+ ++IndentCount_;
+ }
+
+ Y_FORCE_INLINE void Unindent()
+ {
+ --IndentCount_;
+ }
+
+
+ Y_FORCE_INLINE void Suspend()
+ {
+ ++SuspendCount_;
+ }
+
+ Y_FORCE_INLINE void Resume()
+ {
+ --SuspendCount_;
+ }
+
+ Y_FORCE_INLINE bool IsSuspended() const
+ {
+ return SuspendCount_ > 0;
+ }
+
+
+ Y_FORCE_INLINE bool IsActive() const
+ {
+ return IsEnabled() && !IsSuspended();
+ }
+
+
+ template <class... TArgs>
+ void Write(const char* format, const TArgs&... args)
+ {
+ if (!IsActive())
+ return;
+
+ if (WriteCount_ < LowerWriteCountDumpLimit_) {
+ ++WriteCount_;
+ return;
+ }
+ if (WriteCount_ >= UpperWriteCountDumpLimit_) {
+ SetEnabled(false);
+ return;
+ }
+
+ TStringBuilder builder;
+ builder.AppendString("DUMP ");
+ builder.AppendChar(' ', IndentCount_ * 2);
+ builder.AppendFormat(format, args...);
+ builder.AppendChar('\n');
+ auto buffer = builder.GetBuffer();
+ fwrite(buffer.begin(), buffer.length(), 1, stderr);
+
+ ++WriteCount_;
+ }
+
+ void IncrementWriteCountIfNotSuspended()
+ {
+ if (!IsSuspended()) {
+ ++WriteCount_;
+ }
+ }
+
+ void ReportWriteCount()
+ {
+ TStringBuilder builder;
+ builder.AppendFormat("%v\n", WriteCount_);
+ auto buffer = builder.GetBuffer();
+ fwrite(buffer.begin(), buffer.length(), 1, stdout);
+ fflush(stdout);
+ }
+
+private:
+ bool Enabled_ = false;
+ int IndentCount_ = 0;
+ int SuspendCount_ = 0;
+
+ i64 WriteCount_ = 0;
+ i64 LowerWriteCountDumpLimit_ = 0;
+ i64 UpperWriteCountDumpLimit_ = std::numeric_limits<i64>::max();
+};
+
+class TSerializeDumpIndentGuard
+ : private TNonCopyable
+{
+public:
+ explicit TSerializeDumpIndentGuard(TSerializationDumper* dumper)
+ : Dumper_(dumper)
+ {
+ Dumper_->Indent();
+ }
+
+ TSerializeDumpIndentGuard(TSerializeDumpIndentGuard&& other)
+ : Dumper_(other.Dumper_)
+ {
+ other.Dumper_ = nullptr;
+ }
+
+ ~TSerializeDumpIndentGuard()
+ {
+ if (Dumper_) {
+ Dumper_->Unindent();
+ }
+ }
+
+ //! Needed for SERIALIZATION_DUMP_INDENT.
+ explicit operator bool() const
+ {
+ return false;
+ }
+
+private:
+ TSerializationDumper* Dumper_;
+
+};
+
+class TSerializeDumpSuspendGuard
+ : private TNonCopyable
+{
+public:
+ explicit TSerializeDumpSuspendGuard(TSerializationDumper* dumper)
+ : Dumper_(dumper)
+ {
+ Dumper_->Suspend();
+ }
+
+ TSerializeDumpSuspendGuard(TSerializeDumpSuspendGuard&& other)
+ : Dumper_(other.Dumper_)
+ {
+ other.Dumper_ = nullptr;
+ }
+
+ ~TSerializeDumpSuspendGuard()
+ {
+ if (Dumper_) {
+ Dumper_->Resume();
+ }
+ }
+
+ //! Needed for SERIALIZATION_DUMP_SUSPEND.
+ explicit operator bool() const
+ {
+ return false;
+ }
+
+private:
+ TSerializationDumper* Dumper_;
+
+};
+
+#define SERIALIZATION_DUMP_WRITE(context, ...) \
+ if (Y_LIKELY(!(context).Dumper().IsActive())) \
+ { \
+ if ((context).GetEnableTotalWriteCountReport()) \
+ (context).Dumper().IncrementWriteCountIfNotSuspended(); \
+ } \
+ else \
+ (context).Dumper().Write(__VA_ARGS__)
+
+#define SERIALIZATION_DUMP_INDENT(context) \
+ if (auto SERIALIZATION_DUMP_INDENT__Guard = NYT::TSerializeDumpIndentGuard(&(context).Dumper())) \
+ { YT_ABORT(); } \
+ else
+
+#define SERIALIZATION_DUMP_SUSPEND(context) \
+ if (auto SERIALIZATION_DUMP_SUSPEND__Guard = NYT::TSerializeDumpSuspendGuard(&(context).Dumper())) \
+ { YT_ABORT(); } \
+ else
+
+inline TString DumpRangeToHex(TRef data)
+{
+ TStringBuilder builder;
+ builder.AppendChar('<');
+ for (const char* ptr = data.Begin(); ptr != data.End(); ++ptr) {
+ ui8 ch = *ptr;
+ builder.AppendChar(IntToHexLowercase[ch >> 4]);
+ builder.AppendChar(IntToHexLowercase[ch & 0xf]);
+ }
+ builder.AppendChar('>');
+ return builder.Flush();
+}
+
+template <class T>
+struct TSerializationDumpPodWriter
+{
+ template <class C>
+ static void Do(C& context, const T& value)
+ {
+ if constexpr(TFormatTraits<T>::HasCustomFormatValue) {
+ SERIALIZATION_DUMP_WRITE(context, "pod %v", value);
+ } else {
+ SERIALIZATION_DUMP_WRITE(context, "pod[%v] %v", sizeof(T), DumpRangeToHex(TRef::FromPod(value)));
+ }
+ }
+};
+
+#define XX(type) \
+ template <> \
+ struct TSerializationDumpPodWriter<type> \
+ { \
+ template <class C> \
+ static void Do(C& context, const type& value) \
+ { \
+ SERIALIZATION_DUMP_WRITE(context, #type " %v", value); \
+ } \
+ };
+
+XX(i8)
+XX(ui8)
+XX(i16)
+XX(ui16)
+XX(i32)
+XX(ui32)
+XX(i64)
+XX(ui64)
+XX(float)
+XX(double)
+XX(char)
+XX(bool)
+XX(TInstant)
+XX(TDuration)
+
+#undef XX
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/shared_range.h b/yt/yt/core/misc/shared_range.h
new file mode 100644
index 0000000000..db37476ad3
--- /dev/null
+++ b/yt/yt/core/misc/shared_range.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/memory/shared_range.h>
diff --git a/yt/yt/core/misc/shutdown.cpp b/yt/yt/core/misc/shutdown.cpp
new file mode 100644
index 0000000000..e029e96f30
--- /dev/null
+++ b/yt/yt/core/misc/shutdown.cpp
@@ -0,0 +1,271 @@
+#include "shutdown.h"
+
+#include <yt/yt/core/misc/collection_helpers.h>
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+#include <library/cpp/yt/threading/event_count.h>
+
+#include <util/generic/algorithm.h>
+
+#include <util/system/env.h>
+#include <util/system/thread.h>
+
+#include <thread>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TShutdownManager
+{
+public:
+ static TShutdownManager* Get()
+ {
+ return LeakySingleton<TShutdownManager>();
+ }
+
+ TShutdownCookie RegisterShutdownCallback(
+ TString name,
+ TClosure callback,
+ int priority)
+ {
+ auto guard = Guard(Lock_);
+
+ if (ShutdownStarted_.load()) {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Attempt to register shutdown callback when shutdown is already in progress (Name: %s)\n",
+ name.c_str());
+ }
+ return nullptr;
+ }
+
+ auto registeredCallback = New<TRefCountedRegisteredCallback>();
+ registeredCallback->Name = std::move(name);
+ registeredCallback->Callback = std::move(callback);
+ registeredCallback->Priority = priority;
+ InsertOrCrash(RegisteredCallbacks_, registeredCallback.Get());
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Shutdown callback registered (Name: %s, Priority: %d)\n",
+ registeredCallback->Name.c_str(),
+ registeredCallback->Priority);
+ }
+
+ return registeredCallback;
+ }
+
+ void Shutdown(const TShutdownOptions& options)
+ {
+ std::vector<TRegisteredCallback> registeredCallbacks;
+
+ {
+ auto guard = Guard(Lock_);
+
+ if (ShutdownStarted_.load()) {
+ return;
+ }
+
+ ShutdownStarted_.store(true);
+ ShutdownThreadId_.store(GetCurrentThreadId());
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Shutdown started (ThreadId: %" PRISZT ")\n",
+ GetCurrentThreadId());
+ }
+
+ for (auto* registeredCallback : RegisteredCallbacks_) {
+ registeredCallbacks.push_back(*registeredCallback);
+ }
+ }
+
+ SortBy(registeredCallbacks, [] (const auto& registeredCallback) {
+ return registeredCallback.Priority;
+ });
+
+ // Starting threads in exit handlers on Windows causes immediate calling exit
+ // so the routine will not be executed. Moreover, if we try to join this thread we'll get deadlock
+ // because this thread will try to acquire atexit lock which is owned by this thread
+ #ifndef _win_
+ NThreading::TEvent shutdownCompleteEvent;
+ std::thread watchdogThread([&] {
+ ::TThread::SetCurrentThreadName("ShutdownWD");
+ if (!shutdownCompleteEvent.Wait(options.GraceTimeout)) {
+ if (options.AbortOnHang) {
+ ::fprintf(stderr, "*** Shutdown hung, aborting\n");
+ YT_ABORT();
+ } else {
+ ::fprintf(stderr, "*** Shutdown hung, exiting\n");
+ ::_exit(options.HungExitCode);
+ }
+ }
+ });
+ #endif
+
+ for (auto it = registeredCallbacks.rbegin(); it != registeredCallbacks.rend(); it++) {
+ const auto& registeredCallback = *it;
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Running callback (Name: %s, Priority: %d)\n",
+ registeredCallback.Name.c_str(),
+ registeredCallback.Priority);
+ }
+ registeredCallback.Callback();
+ }
+
+ #ifndef _win_
+ shutdownCompleteEvent.NotifyOne();
+ watchdogThread.join();
+ #endif
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Shutdown completed\n");
+ }
+ }
+
+ bool IsShutdownStarted()
+ {
+ return ShutdownStarted_.load();
+ }
+
+ void EnableShutdownLoggingToStderr()
+ {
+ ShutdownLogFile_.store(stderr);
+ }
+
+ void EnableShutdownLoggingToFile(const TString& fileName)
+ {
+ auto* file = fopen(fileName.c_str(), "w");
+ if (!file) {
+ ::fprintf(stderr, "*** Could not open the shutdown logging file\n");
+ return;
+ }
+ // Although POSIX guarantees fprintf always to be thread-safe (see fprintf(2)),
+ // it seems to be a good idea to disable buffering for the log file.
+ ::setvbuf(file, nullptr, _IONBF, 0);
+ ShutdownLogFile_.store(file);
+ }
+
+ FILE* TryGetShutdownLogFile()
+ {
+ return ShutdownLogFile_.load();
+ }
+
+ size_t GetShutdownThreadId()
+ {
+ return ShutdownThreadId_.load();
+ }
+
+private:
+ std::atomic<FILE*> ShutdownLogFile_ = IsShutdownLoggingEnabledImpl() ? stderr : nullptr;
+
+ NThreading::TForkAwareSpinLock Lock_;
+
+ struct TRegisteredCallback
+ {
+ TString Name;
+ TClosure Callback;
+ int Priority;
+ };
+
+ struct TRefCountedRegisteredCallback
+ : public TRegisteredCallback
+ , public TRefCounted
+ {
+ ~TRefCountedRegisteredCallback()
+ {
+ TShutdownManager::Get()->UnregisterShutdownCallback(this);
+ }
+ };
+
+ std::unordered_set<TRefCountedRegisteredCallback*> RegisteredCallbacks_;
+ std::atomic<bool> ShutdownStarted_ = false;
+ std::atomic<size_t> ShutdownThreadId_ = 0;
+
+
+ static bool IsShutdownLoggingEnabledImpl()
+ {
+ auto value = GetEnv("YT_ENABLE_SHUTDOWN_LOGGING");
+ value.to_lower();
+ return value == "1" || value == "true";
+ }
+
+ void UnregisterShutdownCallback(TRefCountedRegisteredCallback* registeredCallback)
+ {
+ auto guard = Guard(Lock_);
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Shutdown callback unregistered (Name: %s, Priority: %d)\n",
+ registeredCallback->Name.c_str(),
+ registeredCallback->Priority);
+ }
+ EraseOrCrash(RegisteredCallbacks_, registeredCallback);
+ }
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TShutdownCookie RegisterShutdownCallback(
+ TString name,
+ TClosure callback,
+ int priority)
+{
+ return TShutdownManager::Get()->RegisterShutdownCallback(
+ std::move(name),
+ std::move(callback),
+ priority);
+}
+
+void Shutdown(const TShutdownOptions& options)
+{
+ TShutdownManager::Get()->Shutdown(options);
+}
+
+bool IsShutdownStarted()
+{
+ return TShutdownManager::Get()->IsShutdownStarted();
+}
+
+void EnableShutdownLoggingToStderr()
+{
+ TShutdownManager::Get()->EnableShutdownLoggingToStderr();
+}
+
+void EnableShutdownLoggingToFile(const TString& fileName)
+{
+ TShutdownManager::Get()->EnableShutdownLoggingToFile(fileName);
+}
+
+FILE* TryGetShutdownLogFile()
+{
+ return TShutdownManager::Get()->TryGetShutdownLogFile();
+}
+
+size_t GetShutdownThreadId()
+{
+ return TShutdownManager::Get()->GetShutdownThreadId();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const void* ShutdownGuardInitializer = [] {
+ class TShutdownGuard
+ {
+ public:
+ ~TShutdownGuard()
+ {
+ if (auto* logFile = TShutdownManager::Get()->TryGetShutdownLogFile()) {
+ fprintf(logFile, "*** Shutdown guard destructed\n");
+ }
+ Shutdown();
+ }
+ };
+
+ static thread_local TShutdownGuard Guard;
+ return nullptr;
+}();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/shutdown.h b/yt/yt/core/misc/shutdown.h
new file mode 100644
index 0000000000..59f261a156
--- /dev/null
+++ b/yt/yt/core/misc/shutdown.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque ref-counted entity representing a registered shutdown callback.
+using TShutdownCookie = TIntrusivePtr<TRefCounted>;
+
+//! Registers a new callback to be called at global shutdown.
+/*!
+ * If null is returned then the shutdown has already been started
+ * and #callback is not registered.
+ *
+ * When the returned cookie is lost, the callback is automatically
+ * unregistered.
+ */
+[[nodiscard]]
+TShutdownCookie RegisterShutdownCallback(
+ TString name,
+ TClosure callback,
+ int priority = 0);
+
+
+struct TShutdownOptions
+{
+ //! The amount of time to wait for all background threads to finish gracefully.
+ TDuration GraceTimeout = TDuration::Seconds(60);
+
+ //! Controls shutdown behavior when #GraceTimeout expires but some background
+ //! threads are still runnining.
+ //! If true then aborts the program (typically producing a core dump).
+ //! If false then |_exit|s the program with #HungExitCode.
+ bool AbortOnHang = true;
+
+ //! Exit code to use in case on nongraceful exit.
+ int HungExitCode = 0;
+};
+
+//! Starts the global shutdown.
+/*!
+ * Invokes all registered shutdown callbacks in the order of
+ * decreasing priority.
+ *
+ * Safe to call multiple times. All calls after the first one are,
+ * however, no ops.
+ *
+ * This call happens automatically on program exit but on some legacy
+ * systems (e.g. Ubuntu 12) it may be sequenced too late (i.e. when the
+ * destructors of global static objects already started running).
+ * Hence calling it manually at a proper place is always a viable option.
+ */
+void Shutdown(const TShutdownOptions& options = {});
+
+//! Returns true if the global shutdown has already been started
+//! (and is possibly already completed).
+bool IsShutdownStarted();
+
+//! Enables logging shutdown messages to stderr.
+void EnableShutdownLoggingToStderr();
+
+//! Enables logging shutdown messages to the given file.
+void EnableShutdownLoggingToFile(const TString& fileName);
+
+//! Returns the pointer to the log file if shutdown logging has been enabled or nullptr otherwise.
+FILE* TryGetShutdownLogFile();
+
+//! In case the global shutdown has been started, returns
+//! the id of the thread invoking shutdown callbacks.
+size_t GetShutdownThreadId();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/shutdown_priorities.h b/yt/yt/core/misc/shutdown_priorities.h
new file mode 100644
index 0000000000..65594acafd
--- /dev/null
+++ b/yt/yt/core/misc/shutdown_priorities.h
@@ -0,0 +1,14 @@
+#pragma once
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int GrpcDispatcherThreadShutdownPriority = 0;
+constexpr int GrpcServerShutdownPriority = 100;
+
+static_assert(GrpcServerShutdownPriority > GrpcDispatcherThreadShutdownPriority);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/signal_registry.cpp b/yt/yt/core/misc/signal_registry.cpp
new file mode 100644
index 0000000000..27fe3b250d
--- /dev/null
+++ b/yt/yt/core/misc/signal_registry.cpp
@@ -0,0 +1,189 @@
+#include "signal_registry.h"
+
+#include <yt/yt/build/config.h>
+
+#include <library/cpp/yt/system/thread_id.h>
+
+#include <signal.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<int> CrashSignals = {
+ SIGSEGV,
+ SIGILL,
+ SIGFPE,
+ SIGABRT,
+#ifdef _unix_
+ SIGBUS,
+#endif
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This variable is used for protecting signal handlers for crash signals from
+// dumping stuff while another thread is already doing that. Our policy is to let
+// the first thread dump stuff and make other threads wait.
+std::atomic<TSequentialThreadId> CrashingThreadId = InvalidSequentialThreadId;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSignalRegistry* TSignalRegistry::Get()
+{
+ return Singleton<TSignalRegistry>();
+}
+
+void TSignalRegistry::SetupSignal(int signal, int flags)
+{
+#ifdef _unix_
+ DispatchMultiSignal(signal, [&] (int signal) {
+ // Why would you like to use SIGALRM? It is used in crash handler
+ // to prevent program hunging, do not interfere.
+ YT_VERIFY(signal != SIGALRM);
+
+ if (!OverrideNonDefaultSignalHandlers_) {
+ struct sigaction oldact;
+ YT_VERIFY(sigaction(signal, NULL, &oldact) == 0);
+ if (reinterpret_cast<void*>(oldact.sa_sigaction) != SIG_DFL) {
+ return;
+ }
+ }
+
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = flags | SA_SIGINFO;
+ sa.sa_sigaction = &Handle;
+ YT_VERIFY(sigaction(signal, &sa, NULL) == 0);
+ Signals_[signal].SetUp = true;
+ });
+#else
+ DispatchMultiSignal(signal, [&] (int signal) {
+ _crt_signal_t oldact = ::signal(signal, static_cast<_crt_signal_t>(&Handle));
+ YT_VERIFY(oldact != SIG_ERR);
+ if (!OverrideNonDefaultSignalHandlers_ && oldact != SIG_DFL) {
+ YT_VERIFY(::signal(signal, oldact) != SIG_ERR);
+ return;
+ }
+ Signals_[signal].SetUp = true;
+ });
+#endif
+}
+
+void TSignalRegistry::PushCallback(int signal, TSignalRegistry::TSignalHandler callback)
+{
+ DispatchMultiSignal(signal, [&] (int signal) {
+ if (!Signals_[signal].SetUp) {
+ SetupSignal(signal);
+ }
+ Signals_[signal].Callbacks.emplace_back(callback);
+ });
+}
+
+#ifdef _unix_
+void TSignalRegistry::PushCallback(int signal, std::function<void(int)> callback)
+{
+ PushCallback(signal, [callback = std::move(callback)] (int signal, siginfo_t* /* siginfo */, void* /* ucontext */) {
+ callback(signal);
+ });
+}
+#endif
+
+void TSignalRegistry::PushCallback(int signal, std::function<void(void)> callback)
+{
+#ifdef _unix_
+ PushCallback(signal, [callback = std::move(callback)] (int /* signal */, siginfo_t* /* siginfo */, void* /* ucontext */) {
+ callback();
+ });
+#else
+ PushCallback(signal, [callback = std::move(callback)] (int /* signal */) {
+ callback();
+ });
+#endif
+}
+
+void TSignalRegistry::PushDefaultSignalHandler(int signal)
+{
+ PushCallback(signal, [] (int signal) {
+ #ifdef _unix_
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sigemptyset(&sa.sa_mask);
+ sa.sa_handler = SIG_DFL;
+ YT_VERIFY(sigaction(signal, &sa, nullptr) == 0);
+
+ YT_VERIFY(raise(signal) == 0);
+ #else
+ YT_VERIFY(::signal(signal, SIG_DFL) != SIG_ERR);
+ YT_VERIFY(::raise(signal) == 0);
+ #endif
+ });
+}
+
+#ifdef _unix_
+void TSignalRegistry::Handle(int signal, siginfo_t* siginfo, void* ucontext)
+#else
+void TSignalRegistry::Handle(int signal)
+#endif
+{
+ auto* self = Get();
+
+ if (self->EnableCrashSignalProtection_ &&
+ std::find(CrashSignals.begin(), CrashSignals.end(), signal) != CrashSignals.end()) {
+ // For crash signals we try pretty hard to prevent simultaneous execution of
+ // several crash handlers.
+
+ auto currentThreadId = GetSequentialThreadId();
+ auto expectedCrashingThreadId = InvalidSequentialThreadId;
+
+ if (!CrashingThreadId.compare_exchange_strong(expectedCrashingThreadId, currentThreadId)) {
+ // We've already entered the signal handler. What should we do?
+ if (currentThreadId == expectedCrashingThreadId) {
+ // It looks the current thread is reentering the signal handler.
+ // Something must be going wrong (maybe we are reentering by another
+ // type of signal?). Simply return from here and hope that the default signal handler
+ // (which is going to be executed after us by TSignalRegistry) will succeed in killing us.
+ // Otherwise, we will probably end up running out of stack entering
+ // CrashSignalHandler over and over again. Not a bad thing, after all.
+ return;
+ } else {
+ // Another thread is dumping stuff. Let's wait until that thread
+ // finishes the job and kills the process.
+ while (true) {
+ sleep(1);
+ }
+ }
+ }
+
+ // This is the first time we enter the signal handler.
+ // Let the rest of the handlers do their interesting stuff.
+ }
+
+ for (const auto& callback : self->Signals_[signal].Callbacks) {
+ #ifdef _unix_
+ callback(signal, siginfo, ucontext);
+ #else
+ callback(signal);
+ #endif
+ }
+}
+
+template <class TCallback>
+void TSignalRegistry::DispatchMultiSignal(int multiSignal, const TCallback& callback)
+{
+ std::vector<int> signals;
+ if (multiSignal == AllCrashSignals) {
+ signals = CrashSignals;
+ } else {
+ signals = {multiSignal};
+ }
+
+ for (int signal : signals) {
+ callback(signal);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/signal_registry.h b/yt/yt/core/misc/signal_registry.h
new file mode 100644
index 0000000000..c0fe057289
--- /dev/null
+++ b/yt/yt/core/misc/signal_registry.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/property.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Shorthand for all crash signals (SIGSEGV, SIGILL, SIGFPE, SIGABRT, SIGBUS).
+//! May be used instead of signal in all public methods of signal registry.
+constexpr int AllCrashSignals = -1;
+
+//! Singleton class which provides convenient interface for signal handler registration.
+class TSignalRegistry
+{
+public:
+ //! Flag enabling mechanism which protects multiple crash signal handlers from simultaneous
+ //! execution.
+ DEFINE_BYVAL_RW_PROPERTY(bool, EnableCrashSignalProtection, true);
+
+ //! Flag preventing us to override user custom signal handlers.
+ DEFINE_BYVAL_RW_PROPERTY(bool, OverrideNonDefaultSignalHandlers, true);
+
+#ifdef _unix_
+ using TSignalHandler = std::function<void(int, siginfo_t*, void*)>;
+#else
+ using TSignalHandler = std::function<void(int)>;
+#endif
+
+public:
+ static TSignalRegistry* Get();
+
+ //! Setup our handler that invokes registered callbacks in order.
+ //! Flags has same meaning as sa_flags in sigaction(2). Use this method if you need certain flags.
+ //! By default any signal touched by PushCallback(...) will be set up with default flags.
+ void SetupSignal(int signal, int flags = 0);
+
+ //! Add simple callback which should be called for signal. Different signatures are supported for convenience.
+ void PushCallback(int signal, std::function<void(void)> callback);
+#ifdef _unix_
+ void PushCallback(int signal, std::function<void(int)> callback);
+#endif
+ void PushCallback(int signal, TSignalHandler callback);
+
+ //! Add default signal handler which is called after invoking our custom handlers.
+ //! NB: this handler restores default signal handler as a side-effect. Use it only
+ //! when default handler terminates the program.
+ void PushDefaultSignalHandler(int signal);
+
+private:
+ static constexpr int SignalRange = 64;
+
+ struct TSignalSetup
+ {
+ std::vector<TSignalHandler> Callbacks;
+ bool SetUp = false;
+ };
+ std::array<TSignalSetup, SignalRange> Signals_;
+
+#ifdef _unix_
+ static void Handle(int signal, siginfo_t* siginfo, void* ucontext);
+#else
+ static void Handle(int signal);
+#endif
+
+ //! Invoke something for `multisignal` which may either be some real signal or signal set like `AllCrashSignals`.
+ template <class TCallback>
+ void DispatchMultiSignal(int multiSignal, const TCallback& callback);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/singleton.h b/yt/yt/core/misc/singleton.h
new file mode 100644
index 0000000000..ea9fe85aac
--- /dev/null
+++ b/yt/yt/core/misc/singleton.h
@@ -0,0 +1,2 @@
+#include <library/cpp/yt/memory/leaky_ref_counted_singleton.h>
+#include <library/cpp/yt/memory/leaky_singleton.h>
diff --git a/yt/yt/core/misc/skip_list-inl.h b/yt/yt/core/misc/skip_list-inl.h
new file mode 100644
index 0000000000..8cf5152c17
--- /dev/null
+++ b/yt/yt/core/misc/skip_list-inl.h
@@ -0,0 +1,306 @@
+#ifndef SKIP_LIST_INL_H_
+#error "Direct inclusion of this file is not allowed, include skip_list.h"
+// For the sake of sane code completion.
+#include "skip_list.h"
+#endif
+
+#include <library/cpp/yt/memory/chunked_memory_pool.h>
+
+#include <util/random/random.h>
+
+#include <type_traits>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::TIterator::TIterator()
+ : Head_(nullptr)
+ , Current_(nullptr)
+{ }
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::TIterator::TIterator(
+ const TSkipList* owner,
+ const TNode* current)
+ : Head_(owner->Head_)
+ , Current_(current)
+{ }
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::TIterator::TIterator(const TIterator& other)
+ : Head_(other.Head_)
+ , Current_(other.Current_)
+{ }
+
+template <class TKey, class TComparer>
+const TKey& TSkipList<TKey, TComparer>::TIterator::GetCurrent() const
+{
+ YT_ASSERT(IsValid());
+ return Current_->GetKey();
+}
+
+template <class TKey, class TComparer>
+bool TSkipList<TKey, TComparer>::TIterator::IsValid() const
+{
+ return Current_ != Head_;
+}
+
+template <class TKey, class TComparer>
+void TSkipList<TKey, TComparer>::TIterator::MovePrev()
+{
+ YT_ASSERT(IsValid());
+ Current_ = Current_->GetPrev(0);
+}
+
+template <class TKey, class TComparer>
+void TSkipList<TKey, TComparer>::TIterator::MoveNext()
+{
+ YT_ASSERT(IsValid());
+ Current_ = Current_->GetNext(0);
+}
+
+template <class TKey, class TComparer>
+typename TSkipList<TKey, TComparer>::TIterator& TSkipList<TKey, TComparer>::TIterator::operator=(const TIterator& other)
+{
+ Head_ = other.Head_;
+ Current_ = other.Current_;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::TNode::TNode(const TKey& key)
+ : Key_(key)
+{ }
+
+template <class TKey, class TComparer>
+const TKey& TSkipList<TKey, TComparer>::TNode::GetKey() const
+{
+ return Key_;
+}
+
+template <class TKey, class TComparer>
+typename TSkipList<TKey, TComparer>::TNode* TSkipList<TKey, TComparer>::TNode::GetPrev(int height) const
+{
+ return Link_[height].first;
+}
+
+template <class TKey, class TComparer>
+typename TSkipList<TKey, TComparer>::TNode* TSkipList<TKey, TComparer>::TNode::GetNext(int height) const
+{
+ return Link_[height].second;
+}
+
+template <class TKey, class TComparer>
+void TSkipList<TKey, TComparer>::TNode::SetPrev(int height, TNode* next)
+{
+ Link_[height].first = next;
+}
+
+template <class TKey, class TComparer>
+void TSkipList<TKey, TComparer>::TNode::SetNext(int height, TNode* next)
+{
+ Link_[height].second = next;
+}
+
+template <class TKey, class TComparer>
+void TSkipList<TKey, TComparer>::TNode::InsertAfter(int height, TNode** prevs)
+{
+ for (int index = 0; index < height; ++index) {
+ // NB: GetPrev->GetPrev->GetNext may return this, not Prev.
+ auto next = prevs[index]->GetNext(index);
+ SetNext(index, next);
+ SetPrev(index, prevs[index]);
+ next->SetPrev(index, this);
+ prevs[index]->SetNext(index, this);
+ }
+}
+
+template <class TKey, class TComparer>
+size_t TSkipList<TKey, TComparer>::TNode::GetByteSize(int height)
+{
+ // -1 since Link_ is of size 1.
+ return sizeof(TNode) + sizeof(TLink) * (height - 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::TSkipList(
+ TChunkedMemoryPool* pool,
+ const TComparer& comparer)
+ : Pool_(pool)
+ , Comparer_(comparer)
+ , Head_(AllocateHeadNode())
+{
+ for (int index = 0; index < MaxHeight; ++index) {
+ Prevs_[index] = Head_;
+ }
+}
+
+template <class TKey, class TComparer>
+TSkipList<TKey, TComparer>::~TSkipList()
+{
+ if (!std::is_trivially_destructible<TKey>::value) {
+ auto* current = Head_;
+ while (current) {
+ auto* next = current->GetNext(0);
+ current->~TNode();
+ current = next;
+ }
+ }
+}
+
+template <class TKey, class TComparer>
+int TSkipList<TKey, TComparer>::GetSize() const
+{
+ return Size_;
+}
+
+template <class TKey, class TComparer>
+template <class TPivot, class TNewKeyProvider, class TExistingKeyConsumer>
+void TSkipList<TKey, TComparer>::Insert(
+ const TPivot& pivot,
+ const TNewKeyProvider& newKeyProvider,
+ const TExistingKeyConsumer& existingKeyConsumer)
+{
+ auto* lastInserted = Prevs_[0];
+ auto* next = lastInserted->GetNext(0);
+
+ if ((lastInserted != Head_ && Comparer_(lastInserted->GetKey(), pivot) >= 0) ||
+ (next != Head_ && Comparer_(next->GetKey(), pivot) < 0))
+ {
+ next = DoFindGreaterThanOrEqualTo(pivot, Prevs_.data());
+ }
+
+ if (next != Head_ && Comparer_(next->GetKey(), pivot) == 0) {
+ existingKeyConsumer(next->GetKey());
+ return;
+ }
+
+ int currentHeight = Height_;
+ int randomHeight = GenerateHeight();
+
+ // Upgrade current height if needed.
+ if (randomHeight > currentHeight) {
+ for (int index = currentHeight; index < randomHeight; ++index) {
+ Prevs_[index] = Head_;
+ }
+ Height_ = randomHeight;
+ }
+
+ // Insert a new node.
+ auto* node = AllocateNode(newKeyProvider(), randomHeight);
+ node->InsertAfter(randomHeight, Prevs_.data());
+ ++Size_;
+
+ for (int index = 0; index < randomHeight; ++index) {
+ Prevs_[index] = node;
+ }
+}
+
+template <class TKey, class TComparer>
+bool TSkipList<TKey, TComparer>::Insert(const TKey& key)
+{
+ bool result = true;
+ Insert(
+ key,
+ [&] () { return key; },
+ [&] (const TKey& /*key*/) { result = false; });
+ return result;
+}
+
+template <class TKey, class TComparer>
+template <class TPivot>
+typename TSkipList<TKey, TComparer>::TIterator TSkipList<TKey, TComparer>::FindLessThanOrEqualTo(const TPivot& pivot) const
+{
+ auto* lowerBound = DoFindGreaterThanOrEqualTo(pivot, nullptr);
+ if (lowerBound != Head_ && Comparer_(lowerBound->GetKey(), pivot) == 0) {
+ return TIterator(this, lowerBound);
+ }
+ lowerBound = lowerBound->GetPrev(0);
+ return lowerBound != Head_
+ ? TIterator(this, lowerBound)
+ : TIterator();
+}
+
+template <class TKey, class TComparer>
+template <class TPivot>
+typename TSkipList<TKey, TComparer>::TIterator TSkipList<TKey, TComparer>::FindGreaterThanOrEqualTo(const TPivot& pivot) const
+{
+ auto* lowerBound = DoFindGreaterThanOrEqualTo(pivot, nullptr);
+ return lowerBound != Head_
+ ? TIterator(this, lowerBound)
+ : TIterator();
+}
+
+template <class TKey, class TComparer>
+template <class TPivot>
+typename TSkipList<TKey, TComparer>::TIterator TSkipList<TKey, TComparer>::FindEqualTo(const TPivot& pivot) const
+{
+ auto* lowerBound = DoFindGreaterThanOrEqualTo(pivot, nullptr);
+ return lowerBound != Head_ && Comparer_(lowerBound->GetKey(), pivot) == 0
+ ? TIterator(this, lowerBound)
+ : TIterator();
+}
+
+template <class TKey, class TComparer>
+int TSkipList<TKey, TComparer>::GenerateHeight()
+{
+ int height = 1;
+ while (height < MaxHeight && (RandomNumber<unsigned int>() % InverseProbability) == 0) {
+ ++height;
+ }
+ return height;
+}
+
+template <class TKey, class TComparer>
+typename TSkipList<TKey, TComparer>::TNode* TSkipList<TKey, TComparer>::AllocateNode(const TKey& key, int height)
+{
+ auto* buffer = Pool_->AllocateAligned(TNode::GetByteSize(height));
+ new (buffer) TNode(key);
+ return reinterpret_cast<TNode*>(buffer);
+}
+
+template <class TKey, class TComparer>
+typename TSkipList<TKey, TComparer>::TNode* TSkipList<TKey, TComparer>::AllocateHeadNode()
+{
+ auto head = AllocateNode(TKey(), MaxHeight);
+ for (int index = 0; index < MaxHeight; ++index) {
+ head->SetNext(index, head);
+ head->SetPrev(index, head);
+ }
+ return head;
+}
+
+template <class TKey, class TComparer>
+template <class TPivot>
+typename TSkipList<TKey, TComparer>::TNode* TSkipList<TKey, TComparer>::DoFindGreaterThanOrEqualTo(const TPivot& pivot, TNode** prevs) const
+{
+ auto* current = Head_;
+ auto* lastChecked = Head_;
+ int height = Height_ - 1;
+ while (true) {
+ auto* next = current->GetNext(height);
+ if (next != Head_ && next != lastChecked && Comparer_(next->GetKey(), pivot) < 0) {
+ current = next;
+ } else {
+ if (prevs) {
+ prevs[height] = current;
+ }
+ if (height > 0) {
+ lastChecked = next;
+ --height;
+ } else {
+ return next;
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/skip_list.h b/yt/yt/core/misc/skip_list.h
new file mode 100644
index 0000000000..46f93978a2
--- /dev/null
+++ b/yt/yt/core/misc/skip_list.h
@@ -0,0 +1,148 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/public.h>
+
+#include <atomic>
+#include <array>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An insert-only concurrent skip-list.
+/*!
+ * All mutating methods (including ctor and dtor) must be called from a single (writer) thread.
+ * All const methods can be called from arbitrary (reader) threads.
+ *
+ * Kudos to Yandex RTMR Team :)
+ */
+template <class TKey, class TComparer>
+class TSkipList
+{
+private:
+ class TNode;
+
+public:
+ TSkipList(
+ TChunkedMemoryPool* pool,
+ const TComparer& comparer);
+
+ ~TSkipList();
+
+ //! Returns the number of distinct keys in the list.
+ int GetSize() const;
+
+ //! Tries to insert a new key.
+ //! If a key equivalent to |pivot| is already present then invokes |existingKeyConsumer| passing that key.
+ //! Otherwise invokes |newKeyProvider| to obtain the actual key and inserts that key.
+ template <class TPivot, class TNewKeyProvider, class TExistingKeyConsumer>
+ void Insert(
+ const TPivot& pivot,
+ const TNewKeyProvider& newKeyProvider,
+ const TExistingKeyConsumer& existingKeyConsumer);
+
+ //! Tries to insert a key.
+ //! Returns |false| if a key equivalent to |key| is already present.
+ //! Otherwise returns |true| and inserts |key|.
+ bool Insert(const TKey& key);
+
+
+ class TIterator
+ {
+ public:
+ TIterator();
+ TIterator(const TSkipList* owner, const TNode* current);
+ TIterator(const TIterator& other);
+
+ TIterator& operator = (const TIterator& other);
+
+ //! Advances the iterator to the previous item.
+ void MovePrev();
+
+ //! Advances the iterator to the next item.
+ void MoveNext();
+
+ //! Returns |true| if the iterator points to a valid item.
+ bool IsValid() const;
+
+ //! Returns the key the iterator points to.
+ const TKey& GetCurrent() const;
+
+ private:
+ const TNode* Head_;
+ const TNode* Current_;
+
+ };
+
+ //! Tries to find a key equivalent to |pivot|.
+ //! If succeeds then returns an iterator pointing to that key.
+ //! Otherwise returns an invalid iterator.
+ template <class TPivot>
+ TIterator FindEqualTo(const TPivot& pivot) const;
+
+ //! Returns an iterator pointing to the smallest key that compares greater than or
+ //! equal to |pivot|. If no such key is found then returns an invalid iterator.
+ template <class TPivot>
+ TIterator FindGreaterThanOrEqualTo(const TPivot& pivot) const;
+
+ //! Returns an iterator pointing to the largest key that compares less than or
+ //! equal to |pivot|. If no such key is found then returns an invalid iterator.
+ template <class TPivot>
+ TIterator FindLessThanOrEqualTo(const TPivot& pivot) const;
+
+private:
+ static const int MaxHeight = 12;
+ static const int InverseProbability = 4;
+
+ class TNode
+ {
+ public:
+ explicit TNode(const TKey& key);
+
+ const TKey& GetKey() const;
+
+ TNode* GetPrev(int height) const;
+ TNode* GetNext(int height) const;
+ void SetPrev(int height, TNode* next);
+ void SetNext(int height, TNode* next);
+
+ void InsertAfter(int height, TNode** prevs);
+
+ static size_t GetByteSize(int height);
+
+ private:
+ const TKey Key_;
+ using TLink = std::pair<std::atomic<TNode*>, std::atomic<TNode*>>;
+ TLink Link_[1]; // variable-size array with actual size up to MaxHeight
+ };
+
+ static_assert(sizeof(std::atomic<TNode*>) == sizeof(intptr_t), "std::atomic<TNode*> does not seem to be lock-free.");
+
+private:
+ TChunkedMemoryPool* const Pool_;
+ const TComparer Comparer_;
+ TNode* const Head_;
+ std::array<TNode*, MaxHeight> Prevs_;
+
+ std::atomic<int> Size_ = {0};
+ std::atomic<int> Height_ = {1};
+
+private:
+ static int GenerateHeight();
+
+ TNode* AllocateNode(const TKey& key, int height);
+ TNode* AllocateHeadNode();
+
+ template <class TPivot>
+ TNode* DoFindGreaterThanOrEqualTo(const TPivot& pivot, TNode** prevs) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SKIP_LIST_INL_H_
+#include "skip_list-inl.h"
+#undef SKIP_LIST_INL_H_
diff --git a/yt/yt/core/misc/slab_allocator.cpp b/yt/yt/core/misc/slab_allocator.cpp
new file mode 100644
index 0000000000..8d0fbd18ee
--- /dev/null
+++ b/yt/yt/core/misc/slab_allocator.cpp
@@ -0,0 +1,463 @@
+#include "slab_allocator.h"
+
+#include <yt/yt/core/misc/atomic_ptr.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/malloc/malloc.h>
+
+namespace NYT {
+
+/////////////////////////////////////////////////////////////////////////////
+
+static_assert(TSlabAllocator::SegmentSize >= NYTAlloc::LargeAllocationSizeThreshold, "Segment size violation");
+
+static_assert(TSlabAllocator::AcquireMemoryGranularity % 2 == 0, "Must be divisible by 2");
+
+struct TArenaCounters
+{
+ TArenaCounters() = default;
+
+ explicit TArenaCounters(const NProfiling::TProfiler& profiler)
+ : AllocatedItems(profiler.Counter("/lookup/allocated_items"))
+ , FreedItems(profiler.Counter("/lookup/freed_items"))
+ , AliveItems(profiler.Gauge("/lookup/alive_items"))
+ , ArenaSize(profiler.Gauge("/lookup/arena_size"))
+ { }
+
+ NProfiling::TCounter AllocatedItems;
+ NProfiling::TCounter FreedItems;
+ NProfiling::TGauge AliveItems;
+ NProfiling::TGauge ArenaSize;
+};
+
+class TSmallArena final
+ : public TRefTracked<TSmallArena>
+ , public TArenaCounters
+{
+public:
+ static constexpr bool EnableHazard = true;
+
+ struct TFreeListItem
+ : public TFreeListItemBase<TFreeListItem>
+ { };
+
+ using TSimpleFreeList = TFreeList<TFreeListItem>;
+
+ TSmallArena(
+ size_t rank,
+ size_t segmentSize,
+ IMemoryUsageTrackerPtr memoryTracker,
+ const NProfiling::TProfiler& profiler)
+ : TArenaCounters(profiler.WithTag("rank", ToString(rank)))
+ , ObjectSize_(NYTAlloc::SmallRankToSize[rank])
+ , ObjectCount_(segmentSize / ObjectSize_)
+ , MemoryTracker_(std::move(memoryTracker))
+ {
+ YT_VERIFY(ObjectCount_ > 0);
+ }
+
+ void* Allocate()
+ {
+ auto* obj = FreeList_.Extract();
+ if (Y_LIKELY(obj)) {
+ AllocatedItems.Increment();
+ AliveItems.Update(GetRefCounter(this)->GetRefCount() + 1);
+ // Fast path.
+ return obj;
+ }
+
+ return AllocateSlow();
+ }
+
+ void Free(void* obj)
+ {
+ FreedItems.Increment();
+ AliveItems.Update(GetRefCounter(this)->GetRefCount() - 1);
+ FreeList_.Put(static_cast<TFreeListItem*>(obj));
+ Unref(this);
+ }
+
+ ~TSmallArena()
+ {
+ static const auto& Logger = LockFreePtrLogger;
+
+ FreeList_.ExtractAll();
+
+ size_t segmentCount = 0;
+ auto* segment = Segments_.ExtractAll();
+ while (segment) {
+ auto* next = segment->Next.load(std::memory_order::acquire);
+ NYTAlloc::Free(segment);
+ segment = next;
+ ++segmentCount;
+ }
+
+ YT_VERIFY(static_cast<ssize_t>(segmentCount) == SegmentCount_.load());
+
+ size_t totalSize = segmentCount * (sizeof(TFreeListItem) + ObjectSize_ * ObjectCount_);
+
+ YT_LOG_TRACE("Destroying arena (ObjectSize: %v, TotalSize: %v)",
+ ObjectSize_,
+ totalSize);
+
+ if (MemoryTracker_) {
+ MemoryTracker_->Release(totalSize);
+ }
+
+ #ifdef YT_ENABLE_REF_COUNTED_TRACKING
+ TRefCountedTrackerFacade::FreeSpace(GetRefCountedTypeCookie<TSmallArena>(), totalSize);
+ #endif
+ }
+
+ bool IsReallocationNeeded() const
+ {
+ auto refCount = GetRefCounter(this)->GetRefCount();
+ auto segmentCount = SegmentCount_.load();
+
+ auto maxRefCount = static_cast<ssize_t>(segmentCount * ObjectCount_) + 4;
+ return segmentCount > 1 && refCount * 2 < maxRefCount || segmentCount == 1 && refCount == 2;
+ }
+
+ IMemoryUsageTrackerPtr GetMemoryTracker() const
+ {
+ return MemoryTracker_;
+ }
+
+private:
+ const size_t ObjectSize_;
+ const size_t ObjectCount_;
+
+ TSimpleFreeList FreeList_;
+ TSimpleFreeList Segments_;
+ std::atomic<int> SegmentCount_ = 0;
+ const IMemoryUsageTrackerPtr MemoryTracker_;
+
+ std::pair<TFreeListItem*, TFreeListItem*> BuildFreeList(char* ptr)
+ {
+ auto head = reinterpret_cast<TFreeListItem*>(ptr);
+
+ // Build chain of chunks.
+ auto objectCount = ObjectCount_;
+ auto objectSize = ObjectSize_;
+
+ YT_VERIFY(objectCount > 0);
+ YT_VERIFY(objectSize > 0);
+ auto lastPtr = ptr + objectSize * (objectCount - 1);
+
+ while (objectCount-- > 1) {
+ auto* current = reinterpret_cast<TFreeListItem*>(ptr);
+ ptr += objectSize;
+
+ current->Next.store(reinterpret_cast<TFreeListItem*>(ptr), std::memory_order::release);
+ }
+
+ YT_VERIFY(ptr == lastPtr);
+
+ auto* current = reinterpret_cast<TFreeListItem*>(ptr);
+ current->Next.store(nullptr, std::memory_order::release);
+
+ return {head, current};
+ }
+
+ void* AllocateSlow()
+ {
+ // For large chunks it is better to allocate SegmentSize + sizeof(TFreeListItem) space
+ // than allocate SegmentSize and use ObjectCount_ - 1.
+ auto totalSize = sizeof(TFreeListItem) + ObjectSize_ * ObjectCount_;
+
+ if (MemoryTracker_ && !MemoryTracker_->TryAcquire(totalSize).IsOK()) {
+ return nullptr;
+ }
+
+ auto segmentCount = SegmentCount_.load();
+ auto refCount = GetRefCounter(this)->GetRefCount();
+ static const auto& Logger = LockFreePtrLogger;
+
+ YT_LOG_TRACE("Allocating segment (ObjectSize: %v, RefCount: %v, SegmentCount: %v, TotalObjectCapacity: %v, TotalSize: %v)",
+ ObjectSize_,
+ refCount,
+ segmentCount,
+ segmentCount * ObjectCount_,
+ segmentCount * totalSize);
+
+#ifdef YT_ENABLE_REF_COUNTED_TRACKING
+ TRefCountedTrackerFacade::AllocateSpace(GetRefCountedTypeCookie<TSmallArena>(), totalSize);
+#endif
+
+ auto* ptr = NYTAlloc::Allocate(totalSize);
+
+ // Save segments in list to free them in destructor.
+ Segments_.Put(static_cast<TFreeListItem*>(ptr));
+
+ ++SegmentCount_;
+
+ AllocatedItems.Increment();
+ AliveItems.Update(refCount + 1);
+ ArenaSize.Update((segmentCount + 1) * totalSize);
+
+ auto [head, tail] = BuildFreeList(static_cast<char*>(ptr) + sizeof(TFreeListItem));
+
+ // Extract one element.
+ auto* next = head->Next.load();
+ FreeList_.Put(next, tail);
+ return head;
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSmallArena)
+
+/////////////////////////////////////////////////////////////////////////////
+
+class TLargeArena
+ : public TRefTracked<TLargeArena>
+ , public TArenaCounters
+{
+public:
+ TLargeArena(IMemoryUsageTrackerPtr memoryTracker, const NProfiling::TProfiler& profiler)
+ : TArenaCounters(profiler.WithTag("rank", "large"))
+ , MemoryTracker_(std::move(memoryTracker))
+ { }
+
+ void* Allocate(size_t size)
+ {
+ auto allocatedSize = size + sizeof(TSizeHeader);
+ if (!TryAcquireMemory(allocatedSize)) {
+ return nullptr;
+ }
+
+ auto itemCount = ++RefCount_;
+ auto ptr = malloc(allocatedSize);
+
+ auto header = reinterpret_cast<TSizeHeader*>(ptr);
+ header->Size = allocatedSize;
+
+ AllocatedItems.Increment();
+ AliveItems.Update(itemCount);
+
+ return reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) + sizeof(TSizeHeader));
+ }
+
+ void Free(void* ptr)
+ {
+ ptr = reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) - sizeof(TSizeHeader));
+
+ auto allocatedSize = reinterpret_cast<TSizeHeader*>(ptr)->Size;
+ ReleaseMemory(allocatedSize);
+ free(ptr);
+ FreedItems.Increment();
+ AliveItems.Update(RefCount_.load() - 1);
+ Unref();
+ }
+
+ size_t Unref()
+ {
+ auto count = --RefCount_;
+ if (count == 0) {
+ delete this;
+ }
+ return count;
+ }
+
+ bool TryAcquireMemory(size_t size)
+ {
+ if (!MemoryTracker_) {
+ return true;
+ }
+
+ auto overheadMemory = OverheadMemory_.load();
+ do {
+ if (overheadMemory < size) {
+ auto targetAcquire = std::max(TSlabAllocator::AcquireMemoryGranularity, size);
+ auto result = MemoryTracker_->TryAcquire(targetAcquire);
+ if (result.IsOK()) {
+ OverheadMemory_.fetch_add(targetAcquire - size);
+ auto arenaSize = AcquiredMemory_.fetch_add(targetAcquire) + targetAcquire;
+ ArenaSize.Update(arenaSize);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ } while (!OverheadMemory_.compare_exchange_weak(overheadMemory, overheadMemory - size));
+
+ return true;
+ }
+
+ void ReleaseMemory(size_t size)
+ {
+ if (!MemoryTracker_) {
+ return;
+ }
+
+ auto overheadMemory = OverheadMemory_.load();
+
+ while (overheadMemory + size > TSlabAllocator::AcquireMemoryGranularity) {
+ auto halfMemoryGranularity = TSlabAllocator::AcquireMemoryGranularity / 2;
+ if (OverheadMemory_.compare_exchange_weak(overheadMemory, halfMemoryGranularity)) {
+ auto releasedMemory = overheadMemory + size - halfMemoryGranularity;
+ MemoryTracker_->Release(releasedMemory);
+ auto arenaSize = AcquiredMemory_.fetch_sub(releasedMemory) - releasedMemory;
+ ArenaSize.Update(arenaSize);
+
+ return;
+ }
+ }
+
+ OverheadMemory_.fetch_add(size);
+ }
+
+private:
+ struct TSizeHeader
+ {
+ i64 Size;
+ i64 Dummy;
+ };
+
+ const IMemoryUsageTrackerPtr MemoryTracker_;
+ // One ref from allocator plus refs from allocated objects.
+ std::atomic<size_t> RefCount_ = 1;
+ std::atomic<size_t> OverheadMemory_ = 0;
+ std::atomic<size_t> AcquiredMemory_ = 0;
+};
+
+/////////////////////////////////////////////////////////////////////////////
+
+TSlabAllocator::TSlabAllocator(
+ const NProfiling::TProfiler& profiler,
+ IMemoryUsageTrackerPtr memoryTracker)
+ : Profiler_(profiler)
+{
+ for (size_t rank = 1; rank < NYTAlloc::SmallRankCount; ++rank) {
+ // There is no std::make_unique overload with custom deleter.
+ SmallArenas_[rank].Store(New<TSmallArena>(rank, TSlabAllocator::SegmentSize, memoryTracker, Profiler_));
+ }
+
+ LargeArena_.reset(new TLargeArena(memoryTracker, profiler));
+}
+
+namespace {
+
+TLargeArena* TryGetLargeArenaFromTag(uintptr_t tag)
+{
+ return tag & 1ULL ? reinterpret_cast<TLargeArena*>(tag & ~1ULL) : nullptr;
+}
+
+TSmallArena* GetSmallArenaFromTag(uintptr_t tag)
+{
+ return reinterpret_cast<TSmallArena*>(tag);
+}
+
+uintptr_t MakeTagFromArena(TLargeArena* arena)
+{
+ auto result = reinterpret_cast<uintptr_t>(arena);
+ YT_ASSERT((result & 1ULL) == 0);
+ return result | 1ULL;
+}
+
+uintptr_t MakeTagFromArena(TSmallArena* segment)
+{
+ auto result = reinterpret_cast<uintptr_t>(segment);
+ YT_ASSERT((result & 1ULL) == 0);
+ return result & ~1ULL;
+}
+
+const uintptr_t* GetHeaderFromPtr(const void* ptr)
+{
+ return static_cast<const uintptr_t*>(ptr) - 1;
+}
+
+uintptr_t* GetHeaderFromPtr(void* ptr)
+{
+ return static_cast<uintptr_t*>(ptr) - 1;
+}
+
+} // namespace
+
+void TSlabAllocator::TLargeArenaDeleter::operator() (TLargeArena* arena)
+{
+ arena->Unref();
+}
+
+void* TSlabAllocator::Allocate(size_t size)
+{
+ size += sizeof(uintptr_t);
+
+ uintptr_t tag = 0;
+ void* ptr = nullptr;
+ if (size < NYTAlloc::LargeAllocationSizeThreshold) {
+ auto rank = NYTAlloc::SizeToSmallRank(size);
+
+ auto arena = SmallArenas_[rank].Acquire();
+ YT_VERIFY(arena);
+ ptr = arena->Allocate();
+ if (ptr) {
+ auto* arenaPtr = arena.Release();
+ tag = MakeTagFromArena(arenaPtr);
+ }
+ } else {
+ ptr = LargeArena_->Allocate(size);
+ tag = MakeTagFromArena(LargeArena_.get());
+ }
+
+ if (!ptr) {
+ return nullptr;
+ }
+
+ // Mutes TSAN data race with write Next in TFreeList::Push.
+ auto* header = static_cast<std::atomic<uintptr_t>*>(ptr);
+ header->store(tag, std::memory_order::release);
+
+ return header + 1;
+}
+
+bool TSlabAllocator::IsReallocationNeeded() const
+{
+ for (size_t rank = 1; rank < NYTAlloc::SmallRankCount; ++rank) {
+ auto arena = SmallArenas_[rank].Acquire();
+ if (arena->IsReallocationNeeded()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TSlabAllocator::ReallocateArenasIfNeeded()
+{
+ bool hasReallocatedArenas = false;
+ for (size_t rank = 1; rank < NYTAlloc::SmallRankCount; ++rank) {
+ auto arena = SmallArenas_[rank].Acquire();
+ if (arena->IsReallocationNeeded()) {
+ SmallArenas_[rank].SwapIfCompare(
+ arena,
+ New<TSmallArena>(rank, TSlabAllocator::SegmentSize, arena->GetMemoryTracker(), Profiler_));
+ hasReallocatedArenas = true;
+ }
+ }
+ return hasReallocatedArenas;
+}
+
+void TSlabAllocator::Free(void* ptr)
+{
+ YT_ASSERT(ptr);
+ auto* header = GetHeaderFromPtr(ptr);
+ auto tag = *header;
+
+ if (auto* largeArena = TryGetLargeArenaFromTag(tag)) {
+ largeArena->Free(header);
+ } else {
+ auto* arenaPtr = GetSmallArenaFromTag(tag);
+ arenaPtr->Free(header);
+ }
+}
+
+bool IsReallocationNeeded(const void* ptr)
+{
+ auto tag = *GetHeaderFromPtr(ptr);
+ return !TryGetLargeArenaFromTag(tag) && GetSmallArenaFromTag(tag)->IsReallocationNeeded();
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/slab_allocator.h b/yt/yt/core/misc/slab_allocator.h
new file mode 100644
index 0000000000..2313ad2550
--- /dev/null
+++ b/yt/yt/core/misc/slab_allocator.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "common.h"
+#include "error.h"
+#include "memory_usage_tracker.h"
+
+#include <yt/yt/core/misc/atomic_ptr.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/ytalloc/api/ytalloc.h>
+
+#include <library/cpp/yt/memory/free_list.h>
+
+#include <array>
+
+namespace NYT {
+
+/////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSmallArena)
+
+class TLargeArena;
+
+/////////////////////////////////////////////////////////////////////////////
+
+class TSlabAllocator
+{
+public:
+ explicit TSlabAllocator(
+ const NProfiling::TProfiler& profiler = {},
+ IMemoryUsageTrackerPtr memoryTracker = nullptr);
+
+ void* Allocate(size_t size);
+ static void Free(void* ptr);
+
+ bool IsReallocationNeeded() const;
+ bool ReallocateArenasIfNeeded();
+
+ static constexpr size_t SegmentSize = 64_KB;
+ static constexpr size_t AcquireMemoryGranularity = 500_KB;
+
+private:
+ const NProfiling::TProfiler Profiler_;
+
+ struct TLargeArenaDeleter
+ {
+ void operator() (TLargeArena* arena);
+ };
+
+ using TLargeArenaPtr = std::unique_ptr<TLargeArena, TLargeArenaDeleter>;
+
+ TAtomicPtr<TSmallArena> SmallArenas_[NYTAlloc::SmallRankCount];
+ TLargeArenaPtr LargeArena_;
+};
+
+bool IsReallocationNeeded(const void* ptr);
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/sliding_window-inl.h b/yt/yt/core/misc/sliding_window-inl.h
new file mode 100644
index 0000000000..30a3e67f5b
--- /dev/null
+++ b/yt/yt/core/misc/sliding_window-inl.h
@@ -0,0 +1,99 @@
+#ifndef SLIDING_WINDOW_INL_H_
+#error "Direct inclusion of this file is not allowed, include sliding_window.h"
+// For the sake of sane code completion.
+#include "sliding_window.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TPacket>
+TSlidingWindow<TPacket>::TSlidingWindow(ssize_t maxSize)
+ : MaxSize_(maxSize)
+{ }
+
+template <class TPacket>
+template <class TCallback>
+void TSlidingWindow<TPacket>::AddPacket(
+ ssize_t sequenceNumber,
+ TPacket&& packet,
+ const TCallback& callback)
+{
+ if (sequenceNumber < GetNextSequenceNumber()) {
+ THROW_ERROR_EXCEPTION("Packet sequence number is too small")
+ << TErrorAttribute("sequence_number", sequenceNumber)
+ << TErrorAttribute("min_sequence_number", GetNextSequenceNumber());
+ }
+
+ if (Window_.find(sequenceNumber) != Window_.end()) {
+ THROW_ERROR_EXCEPTION("Packet with this sequence number is already queued")
+ << TErrorAttribute("sequence_number", sequenceNumber);
+ }
+
+ if (std::ssize(Window_) >= MaxSize_) {
+ THROW_ERROR_EXCEPTION("Packet window overflow")
+ << TErrorAttribute("max_size", MaxSize_);
+ }
+
+ Window_[sequenceNumber] = std::move(packet);
+
+ for (auto it = Window_.find(NextPacketSequenceNumber_);
+ it != Window_.end();
+ it = Window_.find(++NextPacketSequenceNumber_))
+ {
+ callback(std::move(it->second));
+ Window_.erase(it);
+ }
+}
+
+template <class TPacket>
+ssize_t TSlidingWindow<TPacket>::GetNextSequenceNumber() const
+{
+ return NextPacketSequenceNumber_;
+}
+
+template <class TPacket>
+bool TSlidingWindow<TPacket>::IsEmpty() const
+{
+ return Window_.empty();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TPacket>
+TMultiSlidingWindow<TPacket>::TMultiSlidingWindow(ssize_t maxSize)
+ : MaxSize_(maxSize)
+{ }
+
+template <class TPacket>
+template <class TCallback>
+void TMultiSlidingWindow<TPacket>::AddPacket(
+ TMultiSlidingWindowSequenceNumber sequenceNumber,
+ TPacket&& packet,
+ const TCallback& callback)
+{
+ auto it = WindowPerSource_.find(sequenceNumber.SourceId);
+ if (it == WindowPerSource_.end()) {
+ it = WindowPerSource_.emplace(sequenceNumber.SourceId, TSlidingWindow<TPacket>(MaxSize_)).first;
+ }
+ it->second.AddPacket(sequenceNumber.Value, std::forward<TPacket>(packet), callback);
+}
+
+template <class TPacket>
+std::optional<TMultiSlidingWindowSequenceNumber> TMultiSlidingWindow<TPacket>::TryGetMissingSequenceNumber() const
+{
+ for (const auto& [sourceId, window] : WindowPerSource_) {
+ if (!window.IsEmpty()) {
+ return TMultiSlidingWindowSequenceNumber{
+ .SourceId = sourceId,
+ .Value = window.GetNextSequenceNumber()
+ };
+ }
+ }
+ return std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/sliding_window.h b/yt/yt/core/misc/sliding_window.h
new file mode 100644
index 0000000000..5473e9a7e8
--- /dev/null
+++ b/yt/yt/core/misc/sliding_window.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "error.h"
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A classic sliding window implementation.
+/*!
+ * Can defer up to #windowSize "packets" (abstract movable objects) and reorder
+ * them according to their sequence numbers. If #windowSize is not set, it's
+ * assumed to be infinity.
+ *
+ * Once a packet is received from the outside world, the user should call
+ * #AddPacket, providing packet's sequence number.
+ *
+ * The #callback is called once for each packet when it's about to be popped
+ * out of the window. Specifically, a packet leaves the window when no
+ * packets preceding it are missing.
+ *
+ * #callback mustn't throw.
+ */
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TPacket>
+class TSlidingWindow
+{
+public:
+ //! Constructs the sliding window.
+ explicit TSlidingWindow(ssize_t maxSize);
+
+ //! Informs the window that the packet has been received.
+ /*!
+ * May cause #callback to be called for deferred packets (up to
+ * #WindowSize times).
+ *
+ * Throws if a packet with the specified sequence number has already been
+ * set.
+ * Throws if the sequence number was already slid over (i.e. it's too
+ * small).
+ * Throws if setting this packet would exceed the window size (i.e. the
+ * sequence number is too large).
+ */
+ template <class TCallback>
+ void AddPacket(ssize_t sequenceNumber, TPacket&& packet, const TCallback& callback);
+
+ //! Checks whether the window stores no packets.
+ bool IsEmpty() const;
+
+ //! Returns the first missing sequence number.
+ ssize_t GetNextSequenceNumber() const;
+
+private:
+ const ssize_t MaxSize_;
+
+ ssize_t NextPacketSequenceNumber_ = 0;
+ THashMap<ssize_t, TPacket> Window_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMultiSlidingWindowSequenceNumber
+{
+ ssize_t SourceId;
+ ssize_t Value;
+};
+
+//! Incorporates several independent sliding windows indexed by source id.
+template <class TPacket>
+class TMultiSlidingWindow
+{
+public:
+ explicit TMultiSlidingWindow(ssize_t maxSize);
+
+ template <class TCallback>
+ void AddPacket(
+ TMultiSlidingWindowSequenceNumber sequenceNumber,
+ TPacket&& packet,
+ const TCallback& callback);
+
+ //! Returns null if every window is empty.
+ //! Otherwise returns next sequence number within some non-empty window,
+ //! so effectively returns any missing sequence number.
+ std::optional<TMultiSlidingWindowSequenceNumber> TryGetMissingSequenceNumber() const;
+
+private:
+ const ssize_t MaxSize_;
+
+ THashMap<ssize_t, TSlidingWindow<TPacket>> WindowPerSource_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SLIDING_WINDOW_INL_H_
+#include "sliding_window-inl.h"
+#undef SLIDING_WINDOW_INL_H_
diff --git a/yt/yt/core/misc/source_location.h b/yt/yt/core/misc/source_location.h
new file mode 100644
index 0000000000..c01b3aed6e
--- /dev/null
+++ b/yt/yt/core/misc/source_location.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/misc/source_location.h>
diff --git a/yt/yt/core/misc/spsc_queue-inl.h b/yt/yt/core/misc/spsc_queue-inl.h
new file mode 100644
index 0000000000..a567981c29
--- /dev/null
+++ b/yt/yt/core/misc/spsc_queue-inl.h
@@ -0,0 +1,95 @@
+#ifndef SPSC_QUEUE_INL_H_
+#error "Direct inclusion of this file is not allowed, include spsc_queue.h"
+// For the sake of sane code completion.
+#include "spsc_queue.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TSpscQueue<T>::TNode
+{
+ std::atomic<TNode*> Next = nullptr;
+ size_t Offset = 0;
+ T Data[BufferSize];
+};
+
+template <class T>
+TSpscQueue<T>::TSpscQueue()
+{
+ auto* initialNode = new TNode();
+ Head_ = initialNode;
+ Tail_ = initialNode;
+}
+
+template <class T>
+TSpscQueue<T>::~TSpscQueue()
+{
+ auto current = Head_;
+ while (current) {
+ auto next = current->Next.load(std::memory_order::acquire);
+ delete current;
+ current = next;
+ }
+}
+
+template <class T>
+void TSpscQueue<T>::Push(T&& element)
+{
+ auto count = Count_.load(std::memory_order::acquire);
+ size_t position = count - Tail_->Offset;
+
+ if (Y_UNLIKELY(position == BufferSize)) {
+ auto oldTail = Tail_;
+ Tail_ = new TNode();
+ Tail_->Offset = count;
+ oldTail->Next.store(Tail_);
+ position = 0;
+ }
+
+ Tail_->Data[position] = std::move(element);
+ Count_.store(count + 1, std::memory_order::release);
+}
+
+template <class T>
+T* TSpscQueue<T>::Front() const
+{
+ if (Y_UNLIKELY(Offset_ >= CachedCount_)) {
+ auto count = Count_.load(std::memory_order::acquire);
+ CachedCount_ = count;
+
+ if (Offset_ >= count) {
+ return nullptr;
+ }
+ }
+
+ while (Y_UNLIKELY(Offset_ >= Head_->Offset + BufferSize)) {
+ auto next = Head_->Next.load(std::memory_order::acquire);
+ YT_VERIFY(next);
+ delete Head_;
+ Head_ = next;
+ }
+
+ auto position = Offset_ - Head_->Offset;
+ return &Head_->Data[position];
+}
+
+template <class T>
+void TSpscQueue<T>::Pop()
+{
+ ++Offset_;
+}
+
+template <class T>
+bool TSpscQueue<T>::IsEmpty() const
+{
+ auto count = Count_.load();
+ YT_ASSERT(Offset_ <= count);
+ return Offset_ == count;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/spsc_queue.h b/yt/yt/core/misc/spsc_queue.h
new file mode 100644
index 0000000000..b382938ab7
--- /dev/null
+++ b/yt/yt/core/misc/spsc_queue.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/public.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Single producer single consumer lock-free queue.
+template <class T>
+class TSpscQueue
+{
+public:
+ TSpscQueue(const TSpscQueue&) = delete;
+ void operator=(const TSpscQueue&) = delete;
+
+ TSpscQueue();
+ ~TSpscQueue();
+
+ Y_FORCE_INLINE void Push(T&& element);
+
+ Y_FORCE_INLINE T* Front() const;
+ Y_FORCE_INLINE void Pop();
+
+ Y_FORCE_INLINE bool IsEmpty() const;
+
+private:
+ struct TNode;
+
+ static constexpr size_t BufferSize = 128;
+
+ mutable TNode* Head_;
+ TNode* Tail_;
+ size_t Offset_ = 0;
+ mutable size_t CachedCount_ = 0;
+
+ // Avoid false sharing.
+ char Padding[CacheLineSize - 4 * sizeof(void*)];
+
+ std::atomic<size_t> Count_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SPSC_QUEUE_INL_H_
+#include "spsc_queue-inl.h"
+#undef SPSC_QUEUE_INL_H_
diff --git a/yt/yt/core/misc/statistics-inl.h b/yt/yt/core/misc/statistics-inl.h
new file mode 100644
index 0000000000..567b22f47a
--- /dev/null
+++ b/yt/yt/core/misc/statistics-inl.h
@@ -0,0 +1,205 @@
+#ifndef STATISTICS_INL_H_
+#error "Direct inclusion of this file is not allowed, include statistics.h"
+// For the sake of sane code completion.
+#include "statistics.h"
+#endif
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TStatistics::AddSample(const NYPath::TYPath& path, const T& sample)
+{
+ AddSample(path, NYTree::ConvertToNode(sample));
+}
+
+template <class T>
+void TStatistics::ReplacePathWithSample(const NYPath::TYPath& path, const T& sample)
+{
+ ReplacePathWithSample(path, NYTree::ConvertToNode(sample));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTags>
+void TTaggedStatistics<TTags>::AppendStatistics(const TStatistics& statistics, TTags tags)
+{
+ for (const auto& [path, summary] : statistics.Data()) {
+ auto& pathSummaries = Data_[path];
+ auto it = pathSummaries.find(tags);
+ if (it == pathSummaries.end()) {
+ pathSummaries.emplace(tags, summary);
+ } else {
+ it->second.Merge(summary);
+ }
+ }
+}
+
+template <class TTags>
+void TTaggedStatistics<TTags>::AppendTaggedSummary(const NYPath::TYPath& path, const TTaggedStatistics<TTags>::TTaggedSummaries& taggedSummaries)
+{
+ auto taggedSummariesIt = Data_.find(path);
+ if (taggedSummariesIt == Data_.end()) {
+ Data_[path] = taggedSummaries;
+ return;
+ }
+
+ auto& currentTaggedSummaries = taggedSummariesIt->second;
+ for (const auto& [tags, summary] : taggedSummaries) {
+ if (auto summaryIt = currentTaggedSummaries.find(tags); summaryIt == currentTaggedSummaries.end()) {
+ currentTaggedSummaries.insert(std::make_pair(tags, summary));
+ } else {
+ summaryIt->second.Merge(summary);
+ }
+ }
+}
+
+template <class TTags>
+const THashMap<TTags, TSummary>* TTaggedStatistics<TTags>::FindTaggedSummaries(const NYPath::TYPath& path) const
+{
+ auto it = Data_.find(path);
+ if (it != Data_.end()) {
+ return &it->second;
+ }
+ return nullptr;
+}
+
+template <class TTags>
+const typename TTaggedStatistics<TTags>::TSummaryMap& TTaggedStatistics<TTags>::GetData() const
+{
+ return Data_;
+}
+
+template <class TTags>
+void TTaggedStatistics<TTags>::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Data_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTags>
+void Serialize(const TTaggedStatistics<TTags>& statistics, NYson::IYsonConsumer* consumer)
+{
+ SerializeYsonPathsMap<THashMap<TTags, TSummary>>(
+ statistics.GetData(),
+ consumer,
+ [] (const THashMap<TTags, TSummary>& summaries, NYson::IYsonConsumer* consumer) {
+ NYTree::BuildYsonFluently(consumer)
+ .DoListFor(summaries, [] (NYTree::TFluentList fluentList, const auto& pair) {
+ fluentList.Item()
+ .BeginMap()
+ .Item("tags").Value(pair.first)
+ .Item("summary").Value(pair.second)
+ .EndMap();
+ });
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE int SkipEqualTokens(NYPath::TTokenizer& first, NYPath::TTokenizer& second)
+{
+ int commonDepth = 0;
+ while (true) {
+ first.Advance();
+ second.Advance();
+ // Note that both tokenizers can't reach EndOfStream token, because it would mean that
+ // currentKey is prefix of prefixKey or vice versa that is prohibited in TStatistics.
+ first.Expect(NYPath::ETokenType::Slash);
+ second.Expect(NYPath::ETokenType::Slash);
+
+ first.Advance();
+ second.Advance();
+ first.Expect(NYPath::ETokenType::Literal);
+ second.Expect(NYPath::ETokenType::Literal);
+ if (first.GetLiteralValue() == second.GetLiteralValue()) {
+ ++commonDepth;
+ } else {
+ break;
+ }
+ }
+
+ return commonDepth;
+}
+
+template <class TMapValue>
+void SerializeYsonPathsMap(
+ const std::map<NYTree::TYPath, TMapValue>& map,
+ NYson::IYsonConsumer* consumer,
+ const std::function<void(const TMapValue&, NYson::IYsonConsumer*)>& valueSerializer)
+{
+ using NYT::Serialize;
+
+ consumer->OnBeginMap();
+
+ // Depth of the previous key defined as a number of nested maps before the summary itself.
+ int previousDepth = 0;
+ NYPath::TYPath previousPath;
+ for (const auto& [currentPath, value] : map) {
+ NYPath::TTokenizer previousTokenizer(previousPath);
+ NYPath::TTokenizer currentTokenizer(currentPath);
+
+ // The depth of the common part of two keys, needed to determine the number of maps to close.
+ int commonDepth = 0;
+
+ if (previousPath) {
+ // First we find the position in which current key is different from the
+ // previous one in order to close necessary number of maps.
+ commonDepth = SkipEqualTokens(currentTokenizer, previousTokenizer);
+
+ // Close all redundant maps.
+ while (previousDepth > commonDepth) {
+ consumer->OnEndMap();
+ --previousDepth;
+ }
+ } else {
+ currentTokenizer.Advance();
+ currentTokenizer.Expect(NYPath::ETokenType::Slash);
+ currentTokenizer.Advance();
+ currentTokenizer.Expect(NYPath::ETokenType::Literal);
+ }
+
+ int currentDepth = commonDepth;
+ // Open all newly appeared maps.
+ while (true) {
+ consumer->OnKeyedItem(currentTokenizer.GetLiteralValue());
+ currentTokenizer.Advance();
+ if (currentTokenizer.GetType() == NYPath::ETokenType::Slash) {
+ consumer->OnBeginMap();
+ ++currentDepth;
+ currentTokenizer.Advance();
+ currentTokenizer.Expect(NYPath::ETokenType::Literal);
+ } else if (currentTokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ break;
+ } else {
+ THROW_ERROR_EXCEPTION("Wrong token type in statistics path")
+ << TErrorAttribute("token_type", currentTokenizer.GetType())
+ << TErrorAttribute("statistics_path", currentPath);
+ }
+ }
+ // Serialize value.
+ valueSerializer(value, consumer);
+
+ previousDepth = currentDepth;
+ previousPath = currentPath;
+ }
+ while (previousDepth > 0) {
+ consumer->OnEndMap();
+ --previousDepth;
+ }
+
+ // This OnEndMap is complementary to the OnBeginMap before the main loop.
+ consumer->OnEndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/statistics.cpp b/yt/yt/core/misc/statistics.cpp
new file mode 100644
index 0000000000..3b52203b6c
--- /dev/null
+++ b/yt/yt/core/misc/statistics.cpp
@@ -0,0 +1,469 @@
+#include "statistics.h"
+
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/helpers.h>
+#include <yt/yt/core/ytree/serialize.h>
+
+namespace NYT {
+
+using namespace NYTree;
+using namespace NYson;
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSummary::TSummary()
+{
+ Reset();
+}
+
+TSummary::TSummary(i64 sum, i64 count, i64 min, i64 max, std::optional<i64> last)
+ : Sum_(sum)
+ , Count_(count)
+ , Min_(min)
+ , Max_(max)
+ , Last_(last)
+{ }
+
+void TSummary::AddSample(i64 sample)
+{
+ Sum_ += sample;
+ Count_ += 1;
+ Min_ = std::min(Min_, sample);
+ Max_ = std::max(Max_, sample);
+ Last_ = sample;
+}
+
+void TSummary::Merge(const TSummary& summary)
+{
+ Sum_ += summary.GetSum();
+ Count_ += summary.GetCount();
+ Min_ = std::min(Min_, summary.GetMin());
+ Max_ = std::max(Max_, summary.GetMax());
+ // This method used to aggregate summaries of different objects. Last is intentionally dropped.
+ Last_ = std::nullopt;
+}
+
+void TSummary::Reset()
+{
+ Sum_ = 0;
+ Count_ = 0;
+ Min_ = std::numeric_limits<i64>::max();
+ Max_ = std::numeric_limits<i64>::min();
+ Last_ = std::nullopt;
+}
+
+void TSummary::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Sum_);
+ Persist(context, Count_);
+ Persist(context, Min_);
+ Persist(context, Max_);
+ // COMPAT(ignat)
+ if (context.IsLoad() && context.GetVersion() < /* ESnapshotVersion::LastFieldInStatistics */ 300610) {
+ Last_ = std::nullopt;
+ } else {
+ Persist(context, Last_);
+ }
+}
+
+void Serialize(const TSummary& summary, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("sum").Value(summary.GetSum())
+ .Item("count").Value(summary.GetCount())
+ .Item("min").Value(summary.GetMin())
+ .Item("max").Value(summary.GetMax())
+ .OptionalItem("last", summary.GetLast())
+ .EndMap();
+}
+
+bool TSummary::operator ==(const TSummary& other) const
+{
+ return
+ Sum_ == other.Sum_ &&
+ Count_ == other.Count_ &&
+ Min_ == other.Min_ &&
+ Max_ == other.Max_ &&
+ Last_ == other.Last_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSummary& TStatistics::GetSummary(const NYPath::TYPath& path)
+{
+ auto result = Data_.emplace(path, TSummary());
+ auto it = result.first;
+ if (result.second) {
+ // This is a new statistic, check validity.
+ if (it != Data_.begin()) {
+ auto prev = std::prev(it);
+ if (HasPrefix(it->first, prev->first)) {
+ Data_.erase(it);
+ THROW_ERROR_EXCEPTION(
+ "Incompatible statistic paths: old %v, new %v",
+ prev->first,
+ path);
+ }
+ }
+ auto next = std::next(it);
+ if (next != Data_.end()) {
+ if (HasPrefix(next->first, it->first)) {
+ Data_.erase(it);
+ THROW_ERROR_EXCEPTION(
+ "Incompatible statistic paths: old %v, new %v",
+ next->first,
+ path);
+ }
+ }
+ }
+
+ return it->second;
+}
+
+void TStatistics::AddSample(const NYPath::TYPath& path, i64 sample)
+{
+ GetSummary(path).AddSample(sample);
+}
+
+void TStatistics::AddSample(const NYPath::TYPath& path, const INodePtr& sample)
+{
+ ProcessNodeWithCallback(path, sample, [this] (const auto&... args) {
+ AddSample(args...);
+ });
+}
+
+void TStatistics::ReplacePathWithSample(const NYPath::TYPath& path, const i64 sample)
+{
+ auto& summary = GetSummary(path);
+ summary.Reset();
+ summary.AddSample(sample);
+}
+
+void TStatistics::ReplacePathWithSample(const NYPath::TYPath& path, const INodePtr& sample)
+{
+ ProcessNodeWithCallback(path, sample, [this] (const auto&... args) {
+ ReplacePathWithSample(args...);
+ });
+}
+
+template <class TCallback>
+void TStatistics::ProcessNodeWithCallback(const NYPath::TYPath& path, const NYTree::INodePtr& sample, TCallback callback)
+{
+ switch (sample->GetType()) {
+ case ENodeType::Int64:
+ callback(path, sample->AsInt64()->GetValue());
+ break;
+
+ case ENodeType::Uint64:
+ callback(path, static_cast<i64>(sample->AsUint64()->GetValue()));
+ break;
+
+ case ENodeType::Map:
+ for (const auto& [key, child] : sample->AsMap()->GetChildren()) {
+ if (key.empty()) {
+ THROW_ERROR_EXCEPTION("Statistic cannot have an empty name")
+ << TErrorAttribute("path_prefix", path);
+ }
+ callback(path + "/" + ToYPathLiteral(key), child);
+ }
+ break;
+
+ default:
+ THROW_ERROR_EXCEPTION(
+ "Invalid statistics type: expected map or integral type but found %v of type %v",
+ ConvertToYsonString(sample, EYsonFormat::Text).AsStringBuf(),
+ sample->GetType());
+ }
+}
+
+void TStatistics::Merge(const TStatistics& statistics)
+{
+ for (const auto& [path, summary] : statistics.Data()) {
+ GetSummary(path).Merge(summary);
+ }
+}
+
+void TStatistics::MergeWithOverride(const TStatistics& statistics)
+{
+ const auto& otherData = statistics.Data();
+ Data_.insert(otherData.begin(), otherData.end());
+}
+
+TStatistics::TSummaryRange TStatistics::GetRangeByPrefix(const TString& prefix) const
+{
+ auto begin = Data().lower_bound(prefix + '/');
+ // This will effectively return an iterator to the first path not starting with "`prefix`/".
+ auto end = Data().lower_bound(prefix + ('/' + 1));
+ return TSummaryRange(begin, end);
+}
+
+void TStatistics::RemoveRangeByPrefix(const TString& prefixPath)
+{
+ auto begin = Data().lower_bound(prefixPath + '/');
+ // This will effectively return an iterator to the first path not starting with "`prefix`/".
+ auto end = Data().lower_bound(prefixPath + ('/' + 1));
+ Data_.erase(begin, end);
+}
+
+void TStatistics::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Data_);
+}
+
+void Serialize(const TStatistics& statistics, IYsonConsumer* consumer)
+{
+ using NYT::Serialize;
+
+ if (statistics.GetTimestamp()) {
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("timestamp");
+ NYTree::Serialize(*statistics.GetTimestamp(), consumer);
+ consumer->OnEndAttributes();
+ }
+
+ SerializeYsonPathsMap<TSummary>(
+ statistics.Data(),
+ consumer,
+ [] (const TSummary& summary, IYsonConsumer* consumer) {
+ Serialize(summary, consumer);
+ });
+}
+
+// Helper function for GetNumericValue.
+i64 GetSum(const TSummary& summary)
+{
+ return summary.GetSum();
+}
+
+i64 GetNumericValue(const TStatistics& statistics, const TString& path)
+{
+ auto value = FindNumericValue(statistics, path);
+ if (!value) {
+ THROW_ERROR_EXCEPTION("Statistics %v is not present",
+ path);
+ } else {
+ return *value;
+ }
+}
+
+std::optional<i64> FindNumericValue(const TStatistics& statistics, const TString& path)
+{
+ auto summary = FindSummary(statistics, path);
+ return summary ? std::make_optional(summary->GetSum()) : std::nullopt;
+}
+
+std::optional<TSummary> FindSummary(const TStatistics& statistics, const TString& path)
+{
+ const auto& data = statistics.Data();
+ auto iterator = data.lower_bound(path);
+ if (iterator != data.end() && iterator->first != path && HasPrefix(iterator->first, path)) {
+ THROW_ERROR_EXCEPTION("Invalid statistics type: cannot get summary of %v since it is a map",
+ path);
+ } else if (iterator == data.end() || iterator->first != path) {
+ return std::nullopt;
+ } else {
+ return iterator->second;
+ }
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatisticsBuildingConsumer
+ : public TYsonConsumerBase
+ , public IBuildingYsonConsumer<TStatistics>
+{
+public:
+ void OnStringScalar(TStringBuf value) override
+ {
+ if (!AtAttributes_) {
+ THROW_ERROR_EXCEPTION("String scalars are not allowed for statistics");
+ }
+ Statistics_.SetTimestamp(ConvertTo<TInstant>(value));
+ }
+
+ void OnInt64Scalar(i64 value) override
+ {
+ if (AtAttributes_) {
+ THROW_ERROR_EXCEPTION("Timestamp should have string type");
+ }
+ bool isFieldKnown = true;
+ AtSummaryMap_ = true;
+ if (LastKey_ == "sum") {
+ CurrentSummary_.Sum_ = value;
+ } else if (LastKey_ == "count") {
+ CurrentSummary_.Count_ = value;
+ } else if (LastKey_ == "min") {
+ CurrentSummary_.Min_ = value;
+ } else if (LastKey_ == "max") {
+ CurrentSummary_.Max_ = value;
+ } else if (LastKey_ == "last") {
+ CurrentSummary_.Last_ = value;
+ LastFound_ = true;
+ } else {
+ isFieldKnown = false;
+ }
+
+ if (isFieldKnown) {
+ ++FilledSummaryFields_;
+ }
+ }
+
+ void OnUint64Scalar(ui64 /*value*/) override
+ {
+ THROW_ERROR_EXCEPTION("Uint64 scalars are not allowed for statistics");
+ }
+
+ void OnDoubleScalar(double /*value*/) override
+ {
+ THROW_ERROR_EXCEPTION("Double scalars are not allowed for statistics");
+ }
+
+ void OnBooleanScalar(bool /*value*/) override
+ {
+ THROW_ERROR_EXCEPTION("Boolean scalars are not allowed for statistics");
+ }
+
+ void OnEntity() override
+ {
+ THROW_ERROR_EXCEPTION("Entities are not allowed for statistics");
+ }
+
+ void OnBeginList() override
+ {
+ THROW_ERROR_EXCEPTION("Lists are not allowed for statistics");
+ }
+
+ void OnListItem() override
+ {
+ THROW_ERROR_EXCEPTION("Lists are not allowed for statistics");
+ }
+
+ void OnEndList() override
+ {
+ THROW_ERROR_EXCEPTION("Lists are not allowed for statistics");
+ }
+
+ void OnBeginMap() override
+ {
+ // If we are here, we are either:
+ // * at the root (then do nothing)
+ // * at some directory (then the last key was the directory name)
+ if (!LastKey_.empty()) {
+ DirectoryNameLengths_.push_back(LastKey_.size());
+ CurrentPath_.append('/');
+ CurrentPath_.append(LastKey_);
+ LastKey_.clear();
+ } else {
+ if (!CurrentPath_.empty()) {
+ THROW_ERROR_EXCEPTION("Empty keys are not allowed for statistics");
+ }
+ }
+ }
+
+ void OnKeyedItem(TStringBuf key) override
+ {
+ if (AtAttributes_) {
+ if (key != "timestamp") {
+ THROW_ERROR_EXCEPTION("Attributes other than \"timestamp\" are not allowed");
+ }
+ } else {
+ LastKey_ = ToYPathLiteral(key);
+ }
+ }
+
+ void OnEndMap() override
+ {
+ if (AtSummaryMap_) {
+ int requiredFilledSummaryFields = FilledSummaryFields_ - (LastFound_ ? 1 : 0);
+ if (requiredFilledSummaryFields != 4) {
+ THROW_ERROR_EXCEPTION("All 4 required summary fields must be filled for statistics, but found %v",
+ requiredFilledSummaryFields);
+ }
+ if (!LastFound_) {
+ CurrentSummary_.Last_ = std::nullopt;
+ }
+ Statistics_.Data_[CurrentPath_] = CurrentSummary_;
+ FilledSummaryFields_ = 0;
+ LastFound_ = false;
+ AtSummaryMap_ = false;
+ }
+
+ if (!CurrentPath_.empty()) {
+ // We need to go to the parent.
+ CurrentPath_.resize(CurrentPath_.size() - DirectoryNameLengths_.back() - 1);
+ DirectoryNameLengths_.pop_back();
+ }
+ }
+
+ void OnBeginAttributes() override
+ {
+ if (!CurrentPath_.empty()) {
+ THROW_ERROR_EXCEPTION("Attributes are not allowed for statistics");
+ }
+ AtAttributes_ = true;
+ }
+
+ void OnEndAttributes() override
+ {
+ AtAttributes_ = false;
+ }
+
+ TStatistics Finish() override
+ {
+ return Statistics_;
+ }
+
+private:
+ TStatistics Statistics_;
+
+ TString CurrentPath_;
+ std::vector<int> DirectoryNameLengths_;
+
+ TSummary CurrentSummary_;
+ i64 FilledSummaryFields_ = 0;
+ bool LastFound_ = false;
+
+ TString LastKey_;
+
+ bool AtSummaryMap_ = false;
+ bool AtAttributes_ = false;
+};
+
+void CreateBuildingYsonConsumer(std::unique_ptr<IBuildingYsonConsumer<TStatistics>>* buildingConsumer, EYsonType ysonType)
+{
+ YT_VERIFY(ysonType == EYsonType::Node);
+ *buildingConsumer = std::make_unique<TStatisticsBuildingConsumer>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStatisticsConsumer::TStatisticsConsumer(TSampleHandler sampleHandler)
+ : TreeBuilder_(CreateBuilderFromFactory(GetEphemeralNodeFactory()))
+ , SampleHandler_(sampleHandler)
+{ }
+
+void TStatisticsConsumer::OnMyListItem()
+{
+ TreeBuilder_->BeginTree();
+ Forward(
+ TreeBuilder_.get(),
+ [this] {
+ auto node = TreeBuilder_->EndTree();
+ SampleHandler_.Run(node);
+ },
+ EYsonType::Node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/statistics.h b/yt/yt/core/misc/statistics.h
new file mode 100644
index 0000000000..9467477600
--- /dev/null
+++ b/yt/yt/core/misc/statistics.h
@@ -0,0 +1,169 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/forwarding_consumer.h>
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/building_consumer.h>
+
+#include <yt/yt/core/ytree/tree_builder.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/property.h>
+
+#include <util/generic/iterator_range.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSummary
+{
+public:
+ TSummary();
+
+ TSummary(i64 sum, i64 count, i64 min, i64 max, std::optional<i64> last);
+
+ void AddSample(i64 sample);
+
+ void Merge(const TSummary& summary);
+
+ void Reset();
+
+ DEFINE_BYVAL_RO_PROPERTY(i64, Sum);
+ DEFINE_BYVAL_RO_PROPERTY(i64, Count);
+ DEFINE_BYVAL_RO_PROPERTY(i64, Min);
+ DEFINE_BYVAL_RO_PROPERTY(i64, Max);
+ DEFINE_BYVAL_RO_PROPERTY(std::optional<i64>, Last);
+
+ void Persist(const TStreamPersistenceContext& context);
+
+ bool operator == (const TSummary& other) const;
+
+ friend class TStatisticsBuildingConsumer;
+};
+
+void Serialize(const TSummary& summary, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatistics
+{
+public:
+ using TSummaryMap = std::map<NYPath::TYPath, TSummary>;
+ using TSummaryRange = TIteratorRange<TSummaryMap::const_iterator>;
+ DEFINE_BYREF_RO_PROPERTY(TSummaryMap, Data);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TInstant>, Timestamp);
+
+public:
+ void AddSample(const NYPath::TYPath& path, i64 sample);
+
+ void AddSample(const NYPath::TYPath& path, const NYTree::INodePtr& sample);
+
+ template <class T>
+ void AddSample(const NYPath::TYPath& path, const T& sample);
+
+ void ReplacePathWithSample(const NYPath::TYPath& path, i64 sample);
+
+ void ReplacePathWithSample(const NYPath::TYPath& path, const NYTree::INodePtr& sample);
+
+ template <class T>
+ void ReplacePathWithSample(const NYPath::TYPath& path, const T& sample);
+
+ //! Merge statistics by merging summaries for each common statistics path.
+ void Merge(const TStatistics& statistics);
+ //! Merge statistics by taking summary from #statistics for each common statistics path.
+ void MergeWithOverride(const TStatistics& statistics);
+
+ //! Get range of all elements whose path starts with a given strict prefix path (possibly empty).
+ /*!
+ * Pre-requisites: `prefixPath` must not have terminating slash.
+ * Examples: /a/b is a prefix path for /a/b/hij but not for /a/bcd/efg nor /a/b itself.
+ */
+ TSummaryRange GetRangeByPrefix(const TString& prefixPath) const;
+
+ //! Remove all the elements starting from prefixPath.
+ //! The requirements for prefixPath are the same as in GetRangeByPrefix.
+ void RemoveRangeByPrefix(const TString& prefixPath);
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ template <class TCallback>
+ void ProcessNodeWithCallback(const NYPath::TYPath& path, const NYTree::INodePtr& sample, TCallback callback);
+
+ TSummary& GetSummary(const NYPath::TYPath& path);
+
+ friend class TStatisticsBuildingConsumer;
+};
+
+i64 GetNumericValue(const TStatistics& statistics, const TString& path);
+
+std::optional<i64> FindNumericValue(const TStatistics& statistics, const TString& path);
+std::optional<TSummary> FindSummary(const TStatistics& statistics, const TString& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TStatistics& statistics, NYson::IYsonConsumer* consumer);
+
+void CreateBuildingYsonConsumer(std::unique_ptr<NYson::IBuildingYsonConsumer<TStatistics>>* buildingConsumer, NYson::EYsonType ysonType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatisticsConsumer
+ : public NYson::TForwardingYsonConsumer
+{
+public:
+ using TSampleHandler = TCallback<void(const NYTree::INodePtr& sample)>;
+ explicit TStatisticsConsumer(TSampleHandler consumer);
+
+private:
+ const std::unique_ptr<NYTree::ITreeBuilder> TreeBuilder_;
+ const TSampleHandler SampleHandler_;
+
+ void OnMyListItem() override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTags>
+class TTaggedStatistics
+{
+public:
+ using TTaggedSummaries = THashMap<TTags, TSummary>;
+ using TSummaryMap = std::map<NYPath::TYPath, TTaggedSummaries>;
+
+ void AppendStatistics(const TStatistics& statistics, TTags tags);
+ void AppendTaggedSummary(const NYPath::TYPath& path, const TTaggedSummaries& taggedSummaries);
+
+ const TTaggedSummaries* FindTaggedSummaries(const NYPath::TYPath& path) const;
+ const TSummaryMap& GetData() const;
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ TSummaryMap Data_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTags>
+void Serialize(const TTaggedStatistics<TTags>& statistics, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+void SerializeYsonPathsMap(
+ const std::map<NYTree::TYPath, TValue>& map,
+ NYson::IYsonConsumer* consumer,
+ const std::function<void(const TValue&, NYson::IYsonConsumer*)>& valueSerializer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define STATISTICS_INL_H_
+#include "statistics-inl.h"
+#undef STATISTICS_INL_H_
diff --git a/yt/yt/core/misc/string_builder.h b/yt/yt/core/misc/string_builder.h
new file mode 100644
index 0000000000..2593b6924f
--- /dev/null
+++ b/yt/yt/core/misc/string_builder.h
@@ -0,0 +1 @@
+#include <library/cpp/yt/string/string_builder.h>
diff --git a/yt/yt/core/misc/sync_cache-inl.h b/yt/yt/core/misc/sync_cache-inl.h
new file mode 100644
index 0000000000..bd80cfa1e0
--- /dev/null
+++ b/yt/yt/core/misc/sync_cache-inl.h
@@ -0,0 +1,676 @@
+#ifndef SYNC_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include sync_cache.h"
+// For the sake of sane code completion.
+#include "sync_cache.h"
+#endif
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+const TKey& TSyncCacheValueBase<TKey, TValue, THash>::GetKey() const
+{
+ return Key_;
+}
+
+template <class TKey, class TValue, class THash>
+TSyncCacheValueBase<TKey, TValue, THash>::TSyncCacheValueBase(const TKey& key)
+ : Key_(key)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TSyncSlruCacheBase<TKey, TValue, THash>::TItem::TItem(TValuePtr value)
+ : Value(std::move(value))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TSyncSlruCacheBase<TKey, TValue, THash>::TSyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ const NProfiling::TProfiler& profiler)
+ : Config_(std::move(config))
+ , Capacity_(Config_->Capacity)
+ , YoungerSizeFraction_(Config_->YoungerSizeFraction)
+ , HitWeightCounter_(profiler.Counter("/hit"))
+ , MissedWeightCounter_(profiler.Counter("/missed"))
+ , DroppedWeightCounter_(profiler.Counter("/dropped"))
+{
+ profiler.AddFuncGauge("/younger", MakeStrong(this), [this] {
+ return YoungerWeightCounter_.load();
+ });
+ profiler.AddFuncGauge("/older", MakeStrong(this), [this] {
+ return OlderWeightCounter_.load();
+ });
+
+ Shards_.reset(new TShard[Config_->ShardCount]);
+
+ int touchBufferCapacity = Config_->TouchBufferCapacity / Config_->ShardCount;
+ for (int index = 0; index < Config_->ShardCount; ++index) {
+ Shards_[index].TouchBuffer.resize(touchBufferCapacity);
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::Clear()
+{
+ for (int i = 0; i < Config_->ShardCount; ++i) {
+ auto& shard = Shards_[i];
+ auto guard = WriterGuard(shard.SpinLock);
+
+ shard.TouchBufferPosition = 0;
+
+ shard.ItemMap.clear();
+
+ TIntrusiveListWithAutoDelete<TItem, TDelete> youngerLruList;
+ shard.YoungerLruList.Swap(youngerLruList);
+
+ TIntrusiveListWithAutoDelete<TItem, TDelete> olderLruList;
+ shard.OlderLruList.Swap(olderLruList);
+
+ int totalItemCount = 0;
+ i64 totalYoungerWeight = 0;
+ i64 totalOlderWeight = 0;
+ for (const auto& item : youngerLruList) {
+ totalYoungerWeight += GetWeight(item.Value);
+ ++totalItemCount;
+ }
+ for (const auto& item : olderLruList) {
+ totalOlderWeight += GetWeight(item.Value);
+ ++totalItemCount;
+ }
+
+ shard.YoungerWeightCounter -= totalYoungerWeight;
+ shard.OlderWeightCounter -= totalOlderWeight;
+ YoungerWeightCounter_.fetch_sub(totalYoungerWeight, std::memory_order::relaxed);
+ OlderWeightCounter_.fetch_sub(totalOlderWeight, std::memory_order::relaxed);
+ Size_ -= totalItemCount;
+
+ // NB: Lists must die outside the critical section.
+ guard.Release();
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(const TSlruCacheDynamicConfigPtr& config)
+{
+ Capacity_.store(config->Capacity.value_or(Config_->Capacity));
+ YoungerSizeFraction_.store(config->YoungerSizeFraction.value_or(Config_->YoungerSizeFraction));
+
+ for (int index = 0; index < Config_->ShardCount; ++index) {
+ auto* shard = &Shards_[index];
+ auto guard = WriterGuard(shard->SpinLock);
+ DrainTouchBuffer(shard);
+ Trim(shard, guard);
+ }
+}
+
+template <class TKey, class TValue, class THash>
+typename TSyncSlruCacheBase<TKey, TValue, THash>::TValuePtr
+TSyncSlruCacheBase<TKey, TValue, THash>::Find(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ auto readerGuard = ReaderGuard(shard->SpinLock);
+
+ auto itemIt = shard->ItemMap.find(key);
+ if (itemIt == shard->ItemMap.end()) {
+ return nullptr;
+ }
+
+ auto* item = itemIt->second;
+ bool needToDrain = Touch(shard, item);
+ auto value = item->Value;
+
+ auto weight = GetWeight(item->Value);
+ HitWeightCounter_.Increment(weight);
+
+ readerGuard.Release();
+
+ if (needToDrain) {
+ auto writerGuard = WriterGuard(shard->SpinLock);
+ DrainTouchBuffer(shard);
+ }
+
+ return value;
+}
+
+template <class TKey, class TValue, class THash>
+std::vector<typename TSyncSlruCacheBase<TKey, TValue, THash>::TValuePtr>
+TSyncSlruCacheBase<TKey, TValue, THash>::GetAll()
+{
+ std::vector<TValuePtr> result;
+ result.reseve(GetSize());
+ for (const auto& shard : Shards_) {
+ auto guard = ReaderGuard(shard->SpinLock);
+ for (const auto& [key, item] : shard->ItemMap) {
+ result.push_back(item->Value);
+ }
+ }
+ return result;
+}
+
+template <class TKey, class TValue, class THash>
+bool TSyncSlruCacheBase<TKey, TValue, THash>::TryInsert(const TValuePtr& value, TValuePtr* existingValue)
+{
+ const auto& key = value->GetKey();
+ auto weight = GetWeight(value);
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ DrainTouchBuffer(shard);
+
+ auto itemIt = shard->ItemMap.find(key);
+ if (itemIt != shard->ItemMap.end()) {
+ DroppedWeightCounter_.Increment(weight);
+ if (existingValue) {
+ *existingValue = itemIt->second->Value;
+ }
+ return false;
+ }
+
+ auto* item = new TItem(value);
+ YT_VERIFY(shard->ItemMap.emplace(key, item).second);
+ ++Size_;
+
+ MissedWeightCounter_.Increment(weight);
+
+ PushToYounger(shard, item);
+
+ // NB: Releases the lock.
+ Trim(shard, guard);
+
+ OnAdded(value);
+
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::Trim(TShard* shard, NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard)
+{
+ auto capacity = Capacity_.load();
+ auto youngerSizeFraction = YoungerSizeFraction_.load();
+
+ // Move from older to younger.
+ while (!shard->OlderLruList.Empty() &&
+ Config_->ShardCount * shard->OlderWeightCounter > capacity * (1 - youngerSizeFraction))
+ {
+ auto* item = &*(--shard->OlderLruList.End());
+ MoveToYounger(shard, item);
+ }
+
+ // Evict from younger.
+ std::vector<TValuePtr> evictedValues;
+ while (!shard->YoungerLruList.Empty() &&
+ static_cast<i64>(Config_->ShardCount * (shard->YoungerWeightCounter + shard->OlderWeightCounter)) > capacity)
+ {
+ auto* item = &*(--shard->YoungerLruList.End());
+ auto value = item->Value;
+
+ Pop(shard, item);
+
+ YT_VERIFY(shard->ItemMap.erase(value->GetKey()) == 1);
+ --Size_;
+
+ evictedValues.emplace_back(std::move(item->Value));
+
+ delete item;
+ }
+
+ guard.Release();
+
+ for (const auto& value : evictedValues) {
+ OnRemoved(value);
+ }
+}
+
+template <class TKey, class TValue, class THash>
+bool TSyncSlruCacheBase<TKey, TValue, THash>::TryRemove(const TKey& key)
+{
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ DrainTouchBuffer(shard);
+
+ auto it = shard->ItemMap.find(key);
+ if (it == shard->ItemMap.end()) {
+ return false;
+ }
+
+ auto* item = it->second;
+ auto value = item->Value;
+
+ shard->ItemMap.erase(it);
+ --Size_;
+
+ Pop(shard, item);
+
+ delete item;
+
+ guard.Release();
+
+ OnRemoved(value);
+
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+bool TSyncSlruCacheBase<TKey, TValue, THash>::TryRemove(const TValuePtr& value)
+{
+ const auto& key = value->GetKey();
+ auto* shard = GetShardByKey(key);
+
+ auto guard = WriterGuard(shard->SpinLock);
+
+ DrainTouchBuffer(shard);
+
+ auto itemIt = shard->ItemMap.find(key);
+ if (itemIt == shard->ItemMap.end()) {
+ return false;
+ }
+
+ auto* item = itemIt->second;
+ if (item->Value != value) {
+ return false;
+ }
+
+ shard->ItemMap.erase(itemIt);
+ --Size_;
+
+ Pop(shard, item);
+
+ delete item;
+
+ guard.Release();
+
+ OnRemoved(value);
+
+ return true;
+}
+
+template <class TKey, class TValue, class THash>
+auto TSyncSlruCacheBase<TKey, TValue, THash>::GetShardByKey(const TKey& key) const -> TShard*
+{
+ return &Shards_[THash()(key) % Config_->ShardCount];
+}
+
+template <class TKey, class TValue, class THash>
+bool TSyncSlruCacheBase<TKey, TValue, THash>::Touch(TShard* shard, TItem* item)
+{
+ int capacity = shard->TouchBuffer.size();
+ int index = shard->TouchBufferPosition++;
+ if (index >= capacity) {
+ // Drop touch request due to buffer overflow.
+ // NB: We still return false since the other thread is already responsible for
+ // draining the buffer.
+ return false;
+ }
+
+ shard->TouchBuffer[index] = item;
+ return index == capacity - 1;
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::DrainTouchBuffer(TShard* shard)
+{
+ int count = std::min(
+ shard->TouchBufferPosition.load(),
+ static_cast<int>(shard->TouchBuffer.size()));
+ for (int index = 0; index < count; ++index) {
+ MoveToOlder(shard, shard->TouchBuffer[index]);
+ }
+ shard->TouchBufferPosition = 0;
+}
+
+template <class TKey, class TValue, class THash>
+i64 TSyncSlruCacheBase<TKey, TValue, THash>::GetWeight(const TValuePtr& /*value*/) const
+{
+ return 1;
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::OnAdded(const TValuePtr& /*value*/)
+{ }
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::OnRemoved(const TValuePtr& /*value*/)
+{ }
+
+template <class TKey, class TValue, class THash>
+int TSyncSlruCacheBase<TKey, TValue, THash>::GetSize() const
+{
+ return Size_.load(std::memory_order::relaxed);
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::PushToYounger(TShard* shard, TItem* item)
+{
+ YT_ASSERT(item->Empty());
+ shard->YoungerLruList.PushFront(item);
+ auto weight = GetWeight(item->Value);
+ shard->YoungerWeightCounter += weight;
+ YoungerWeightCounter_.fetch_add(weight, std::memory_order::relaxed);
+ item->Younger = true;
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::MoveToYounger(TShard* shard, TItem* item)
+{
+ YT_ASSERT(!item->Empty());
+ item->Unlink();
+ shard->YoungerLruList.PushFront(item);
+ if (!item->Younger) {
+ auto weight = GetWeight(item->Value);
+ shard->YoungerWeightCounter += weight;
+ shard->OlderWeightCounter -= weight;
+ OlderWeightCounter_.fetch_sub(weight, std::memory_order::relaxed);
+ YoungerWeightCounter_.fetch_add(weight, std::memory_order::relaxed);
+ item->Younger = true;
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::MoveToOlder(TShard* shard, TItem* item)
+{
+ YT_ASSERT(!item->Empty());
+ item->Unlink();
+ shard->OlderLruList.PushFront(item);
+ if (item->Younger) {
+ auto weight = GetWeight(item->Value);
+ shard->YoungerWeightCounter -= weight;
+ shard->OlderWeightCounter += weight;
+ YoungerWeightCounter_.fetch_sub(weight, std::memory_order::relaxed);
+ OlderWeightCounter_.fetch_add(weight, std::memory_order::relaxed);
+ item->Younger = false;
+ }
+}
+
+template <class TKey, class TValue, class THash>
+void TSyncSlruCacheBase<TKey, TValue, THash>::Pop(TShard* shard, TItem* item)
+{
+ if (item->Empty()) {
+ return;
+ }
+ auto weight = GetWeight(item->Value);
+ if (item->Younger) {
+ shard->YoungerWeightCounter -= weight;
+ YoungerWeightCounter_.fetch_sub(weight, std::memory_order::relaxed);
+ } else {
+ shard->OlderWeightCounter -= weight;
+ OlderWeightCounter_.fetch_sub(weight, std::memory_order::relaxed);
+ }
+ item->Unlink();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TMemoryTrackingSyncSlruCacheBase<TKey, TValue, THash>::TMemoryTrackingSyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ IMemoryUsageTrackerPtr memoryTracker,
+ const NProfiling::TProfiler& profiler)
+ : TSyncSlruCacheBase<TKey, TValue, THash>(
+ std::move(config),
+ profiler)
+ , MemoryTracker_(std::move(memoryTracker))
+{
+ MemoryTracker_->SetLimit(this->Capacity_.load());
+}
+
+template <class TKey, class TValue, class THash>
+TMemoryTrackingSyncSlruCacheBase<TKey, TValue, THash>::~TMemoryTrackingSyncSlruCacheBase()
+{
+ MemoryTracker_->SetLimit(0);
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingSyncSlruCacheBase<TKey, TValue, THash>::OnAdded(const TValuePtr& value)
+{
+ MemoryTracker_->Acquire(this->GetWeight(value));
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingSyncSlruCacheBase<TKey, TValue, THash>::OnRemoved(const TValuePtr& value)
+{
+ MemoryTracker_->Release(this->GetWeight(value));
+}
+
+template <class TKey, class TValue, class THash>
+void TMemoryTrackingSyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(const TSlruCacheDynamicConfigPtr& config)
+{
+ TSyncSlruCacheBase<TKey, TValue, THash>::Reconfigure(config);
+ MemoryTracker_->SetLimit(this->Capacity_.load());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TSimpleLruCache<TKey, TValue, THash>::TSimpleLruCache(size_t maxWeight)
+ : MaxWeight_(maxWeight)
+{ }
+
+template <class TKey, class TValue, class THash>
+size_t TSimpleLruCache<TKey, TValue, THash>::GetSize() const
+{
+ return ItemMap_.size();
+}
+
+template <class TKey, class TValue, class THash>
+const TValue& TSimpleLruCache<TKey, TValue, THash>::Get(const TKey& key)
+{
+ auto it = ItemMap_.find(key);
+ YT_VERIFY(it != ItemMap_.end());
+ UpdateLruList(it);
+ return it->second.Value;
+}
+
+template <class TKey, class TValue, class THash>
+TValue* TSimpleLruCache<TKey, TValue, THash>::Find(const TKey& key)
+{
+ auto it = ItemMap_.find(key);
+ if (it == ItemMap_.end()) {
+ return nullptr;
+ }
+ UpdateLruList(it);
+ return &(it->second.Value);
+}
+
+template <class TKey, class TValue, class THash>
+TValue* TSimpleLruCache<TKey, TValue, THash>::FindNoTouch(const TKey& key)
+{
+ auto it = ItemMap_.find(key);
+ if (it == ItemMap_.end()) {
+ return nullptr;
+ }
+ return &(it->second.Value);
+}
+
+template <class TKey, class TValue, class THash>
+TValue* TSimpleLruCache<TKey, TValue, THash>::Insert(const TKey& key, TValue value, size_t weight)
+{
+ {
+ auto mapIt = ItemMap_.find(key);
+ if (mapIt != ItemMap_.end()) {
+ LruList_.erase(mapIt->second.LruListIterator);
+ CurrentWeight_ -= mapIt->second.Weight;
+ ItemMap_.erase(mapIt);
+ }
+ }
+
+ auto item = TItem(std::move(value), weight);
+ while (GetSize() > 0 && CurrentWeight_ + weight > MaxWeight_) {
+ Pop();
+ }
+ auto insertResult = ItemMap_.emplace(key, std::move(item));
+ YT_VERIFY(insertResult.second);
+ auto mapIt = insertResult.first;
+ LruList_.push_front(mapIt);
+ mapIt->second.LruListIterator = LruList_.begin();
+ CurrentWeight_ += weight;
+
+ return &mapIt->second.Value;
+}
+
+template <class TKey, class TValue, class THash>
+void TSimpleLruCache<TKey, TValue, THash>::SetMaxWeight(size_t maxWeight)
+{
+ MaxWeight_ = maxWeight;
+}
+
+template <class TKey, class TValue, class THash>
+void TSimpleLruCache<TKey, TValue, THash>::Clear()
+{
+ ItemMap_.clear();
+ LruList_.clear();
+ CurrentWeight_ = 0;
+}
+
+template <class TKey, class TValue, class THash>
+void TSimpleLruCache<TKey, TValue, THash>::Pop()
+{
+ auto mapIt = LruList_.back();
+ CurrentWeight_ -= mapIt->second.Weight;
+ ItemMap_.erase(mapIt);
+ LruList_.pop_back();
+}
+
+template <class TKey, class TValue, class THash>
+void TSimpleLruCache<TKey, TValue, THash>::UpdateLruList(typename TItemMap::iterator mapIt)
+{
+ LruList_.erase(mapIt->second.LruListIterator);
+ LruList_.push_front(mapIt);
+ mapIt->second.LruListIterator = LruList_.begin();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+TMultiLruCache<TKey, TValue, THash>::TMultiLruCache(size_t maxWeight)
+ : MaxWeight_(maxWeight)
+{ }
+
+template <class TKey, class TValue, class THash>
+size_t TMultiLruCache<TKey, TValue, THash>::GetSize() const
+{
+ return LruList_.size();
+}
+
+template <class TKey, class TValue, class THash>
+const TValue& TMultiLruCache<TKey, TValue, THash>::Get(const TKey& key)
+{
+ auto* value = Find(key);
+ YT_VERIFY(value != nullptr);
+ return *value;
+}
+
+template <class TKey, class TValue, class THash>
+TValue* TMultiLruCache<TKey, TValue, THash>::Find(const TKey& key)
+{
+ auto it = ItemMap_.find(key);
+ if (it == ItemMap_.end()) {
+ return nullptr;
+ }
+
+ auto item = std::move(it->second.back());
+ it->second.pop_back();
+ it->second.push_front(std::move(item));
+
+ UpdateLruList(it->second.begin());
+ return &(it->second.front().Value);
+
+}
+
+template <class TKey, class TValue, class THash>
+TValue* TMultiLruCache<TKey, TValue, THash>::Insert(const TKey& key, TValue value, size_t weight)
+{
+ YT_VERIFY(weight <= MaxWeight_);
+
+ while (GetSize() > 0 && CurrentWeight_ + weight > MaxWeight_) {
+ Pop();
+ }
+
+ CurrentWeight_ += weight;
+
+ auto mapIt = ItemMap_.emplace(key, std::deque<TItem>()).first;
+ mapIt->second.emplace_front(std::move(value), weight);
+ mapIt->second.front().LruListIterator = LruList_.insert(LruList_.begin(), mapIt);
+ return &(mapIt->second.front().Value);
+}
+
+template <class TKey, class TValue, class THash>
+std::optional<TValue> TMultiLruCache<TKey, TValue, THash>::Extract(const TKey& key)
+{
+ auto mapIt = ItemMap_.find(key);
+ if (mapIt == ItemMap_.end()) {
+ return std::nullopt;
+ }
+
+ auto item = std::move(mapIt->second.back());
+ CurrentWeight_ -= item.Weight;
+
+ mapIt->second.pop_back();
+ if (mapIt->second.empty()) {
+ ItemMap_.erase(mapIt);
+ }
+
+ auto lruListIt = item.LruListIterator;
+ LruList_.erase(lruListIt);
+
+ return std::move(item.Value);
+}
+
+template <class TKey, class TValue, class THash>
+TValue TMultiLruCache<TKey, TValue, THash>::Pop()
+{
+ auto listIt = std::prev(LruList_.end());
+ auto mapIt = *listIt;
+
+ YT_VERIFY(!mapIt->second.empty());
+ YT_VERIFY(mapIt->second.back().LruListIterator == listIt);
+
+ auto item = std::move(mapIt->second.back());
+ CurrentWeight_ -= item.Weight;
+
+ mapIt->second.pop_back();
+ if (mapIt->second.empty()) {
+ ItemMap_.erase(mapIt);
+ }
+
+ LruList_.erase(listIt);
+
+ return std::move(item.Value);
+}
+
+template <class TKey, class TValue, class THash>
+void TMultiLruCache<TKey, TValue, THash>::Clear()
+{
+ ItemMap_.clear();
+ LruList_.clear();
+ CurrentWeight_ = 0;
+}
+
+template <class TKey, class TValue, class THash>
+TMultiLruCache<TKey, TValue, THash>::TItem::TItem(TValue value, size_t weight)
+ : Value(std::move(value))
+ , Weight(weight)
+{ }
+
+template <class TKey, class TValue, class THash>
+void TMultiLruCache<TKey, TValue, THash>::UpdateLruList(typename std::deque<TItem>::iterator dequeIt)
+{
+ auto mapIt = *dequeIt->LruListIterator;
+ LruList_.erase(dequeIt->LruListIterator);
+ LruList_.push_front(std::move(mapIt));
+ dequeIt->LruListIterator = LruList_.begin();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/sync_cache.h b/yt/yt/core/misc/sync_cache.h
new file mode 100644
index 0000000000..399dec86be
--- /dev/null
+++ b/yt/yt/core/misc/sync_cache.h
@@ -0,0 +1,240 @@
+#pragma once
+
+#include "public.h"
+#include "cache_config.h"
+#include "memory_usage_tracker.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+#include <atomic>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash>
+class TSyncSlruCacheBase;
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TSyncCacheValueBase
+ : public virtual TRefCounted
+{
+public:
+ const TKey& GetKey() const;
+
+protected:
+ explicit TSyncCacheValueBase(const TKey& key);
+
+private:
+ const TKey Key_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TSyncSlruCacheBase
+ : public virtual TRefCounted
+{
+public:
+ using TValuePtr = TIntrusivePtr<TValue>;
+
+ int GetSize() const;
+ std::vector<TValuePtr> GetAll();
+
+ TValuePtr Find(const TKey& key);
+
+ bool TryInsert(const TValuePtr& value, TValuePtr* existingValue = nullptr);
+ bool TryRemove(const TKey& key);
+ bool TryRemove(const TValuePtr& value);
+ void Clear();
+
+ virtual void Reconfigure(const TSlruCacheDynamicConfigPtr& config);
+
+protected:
+ const TSlruCacheConfigPtr Config_;
+
+ std::atomic<i64> Capacity_;
+ std::atomic<double> YoungerSizeFraction_;
+
+ explicit TSyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ const NProfiling::TProfiler& profiler = {});
+
+ virtual i64 GetWeight(const TValuePtr& value) const;
+ virtual void OnAdded(const TValuePtr& value);
+ virtual void OnRemoved(const TValuePtr& value);
+
+private:
+ struct TItem
+ : public TIntrusiveListItem<TItem>
+ {
+ explicit TItem(TValuePtr value);
+
+ TValuePtr Value;
+ bool Younger;
+ };
+
+ struct TShard
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock);
+
+ TIntrusiveListWithAutoDelete<TItem, TDelete> YoungerLruList;
+ TIntrusiveListWithAutoDelete<TItem, TDelete> OlderLruList;
+
+ size_t YoungerWeightCounter = 0;
+ size_t OlderWeightCounter = 0;
+
+ THashMap<TKey, TItem*, THash> ItemMap;
+
+ std::vector<TItem*> TouchBuffer;
+ std::atomic<int> TouchBufferPosition = {0};
+ };
+
+ std::unique_ptr<TShard[]> Shards_;
+
+ std::atomic<int> Size_ = 0;
+
+ NProfiling::TCounter HitWeightCounter_;
+ NProfiling::TCounter MissedWeightCounter_;
+ NProfiling::TCounter DroppedWeightCounter_;
+ std::atomic<i64> YoungerWeightCounter_ = 0;
+ std::atomic<i64> OlderWeightCounter_ = 0;
+
+ TShard* GetShardByKey(const TKey& key) const;
+
+ bool Touch(TShard* shard, TItem* item);
+ void DrainTouchBuffer(TShard* shard);
+
+ void Trim(TShard* shard, NThreading::TWriterGuard<NThreading::TReaderWriterSpinLock>& guard);
+
+ void PushToYounger(TShard* shard, TItem* item);
+ void MoveToYounger(TShard* shard, TItem* item);
+ void MoveToOlder(TShard* shard, TItem* item);
+ void Pop(TShard* shard, TItem* item);
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TMemoryTrackingSyncSlruCacheBase
+ : public TSyncSlruCacheBase<TKey, TValue, THash>
+{
+public:
+ explicit TMemoryTrackingSyncSlruCacheBase(
+ TSlruCacheConfigPtr config,
+ IMemoryUsageTrackerPtr memoryTracker,
+ const NProfiling::TProfiler& profiler = {});
+ ~TMemoryTrackingSyncSlruCacheBase();
+
+ void Reconfigure(const TSlruCacheDynamicConfigPtr& config) override;
+
+protected:
+ using TValuePtr = typename TSyncSlruCacheBase<TKey, TValue, THash>::TValuePtr;
+
+ void OnAdded(const TValuePtr& value) override;
+ void OnRemoved(const TValuePtr& value) override;
+
+private:
+ const IMemoryUsageTrackerPtr MemoryTracker_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TSimpleLruCache
+{
+public:
+ explicit TSimpleLruCache(size_t maxWeight);
+
+ size_t GetSize() const;
+
+ const TValue& Get(const TKey& key);
+ TValue* Find(const TKey& key);
+ TValue* FindNoTouch(const TKey& key);
+ TValue* Insert(const TKey& key, TValue value, size_t weight = 1);
+
+ void SetMaxWeight(size_t maxWeight);
+
+ void Clear();
+
+private:
+ struct TItem
+ {
+ TItem(TValue value, size_t weight)
+ : Value(std::move(value))
+ , Weight(weight)
+ { }
+
+ TValue Value;
+ size_t Weight;
+ typename std::list<typename THashMap<TKey, TItem, THash>::iterator>::iterator LruListIterator;
+ };
+
+ size_t MaxWeight_ = 0;
+ size_t CurrentWeight_ = 0;
+
+ using TItemMap = THashMap<TKey, TItem, THash>;
+ TItemMap ItemMap_;
+ mutable std::list<typename TItemMap::iterator> LruList_;
+
+ void Pop();
+ void UpdateLruList(typename TItemMap::iterator);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, class THash = THash<TKey>>
+class TMultiLruCache
+{
+public:
+ explicit TMultiLruCache(size_t maxWeight);
+
+ size_t GetSize() const;
+
+ const TValue& Get(const TKey& key);
+ TValue* Find(const TKey& key);
+
+ TValue* Insert(const TKey& key, TValue value, size_t weight = 1);
+ std::optional<TValue> Extract(const TKey& key);
+
+ TValue Pop();
+
+ void Clear();
+
+private:
+ struct TItem;
+
+ using TItemMap = THashMap<TKey, std::deque<TItem>, THash>;
+ using TLruList = typename std::list<typename TItemMap::iterator>;
+
+ struct TItem
+ {
+ TItem(TValue value, size_t weight);
+
+ TValue Value;
+ size_t Weight;
+
+ typename TLruList::iterator LruListIterator;
+ };
+
+ void UpdateLruList(typename std::deque<TItem>::iterator listIt);
+
+private:
+ size_t MaxWeight_ = 0;
+ size_t CurrentWeight_ = 0;
+
+ TItemMap ItemMap_;
+ TLruList LruList_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SYNC_CACHE_INL_H_
+#include "sync_cache-inl.h"
+#undef SYNC_CACHE_INL_H_
diff --git a/yt/yt/core/misc/sync_expiring_cache-inl.h b/yt/yt/core/misc/sync_expiring_cache-inl.h
new file mode 100644
index 0000000000..d259667be4
--- /dev/null
+++ b/yt/yt/core/misc/sync_expiring_cache-inl.h
@@ -0,0 +1,262 @@
+#ifndef SYNC_EXPIRING_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include sync_expiring_cache.h"
+// For the sake of sane code completion.
+#include "sync_expiring_cache.h"
+#endif
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue>
+TSyncExpiringCache<TKey, TValue>::TEntry::TEntry(
+ NProfiling::TCpuInstant lastAccessTime,
+ NProfiling::TCpuInstant lastUpdateTime,
+ TValue value)
+ : LastAccessTime(lastAccessTime)
+ , LastUpdateTime(lastUpdateTime)
+ , Value(std::move(value))
+{ }
+
+template <class TKey, class TValue>
+TSyncExpiringCache<TKey, TValue>::TEntry::TEntry(TSyncExpiringCache<TKey, TValue>::TEntry&& entry)
+ : LastAccessTime(entry.LastAccessTime.load())
+ , LastUpdateTime(entry.LastUpdateTime)
+ , Value(std::move(entry.Value))
+{ }
+
+template <class TKey, class TValue>
+typename TSyncExpiringCache<TKey, TValue>::TEntry&
+TSyncExpiringCache<TKey, TValue>::TEntry::operator=(typename TSyncExpiringCache<TKey, TValue>::TEntry&& other)
+{
+ LastAccessTime = other.LastAccessTime.load();
+ LastUpdateTime = other.LastUpdateTime;
+ Value = std::move(other.Value);
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue>
+TSyncExpiringCache<TKey, TValue>::TSyncExpiringCache(
+ TCallback<TValue(const TKey&)> calculateValueAction,
+ std::optional<TDuration> expirationTimeout,
+ IInvokerPtr invoker)
+ : CalculateValueAction_(std::move(calculateValueAction))
+ , EvictionExecutor_(New<NConcurrency::TPeriodicExecutor>(
+ invoker,
+ BIND(&TSyncExpiringCache::DeleteExpiredItems, MakeWeak(this))))
+{
+ SetExpirationTimeout(expirationTimeout);
+ EvictionExecutor_->Start();
+}
+
+template <class TKey, class TValue>
+std::optional<TValue> TSyncExpiringCache<TKey, TValue>::Find(const TKey& key)
+{
+ auto now = NProfiling::GetCpuInstant();
+ auto deadline = now - ExpirationTimeout_.load();
+
+ auto guard = ReaderGuard(MapLock_);
+
+ auto it = Map_.find(key);
+ if (it != Map_.end()) {
+ auto& entry = it->second;
+ if (entry.LastUpdateTime >= deadline) {
+ entry.LastAccessTime = now;
+ return entry.Value;
+ }
+ }
+
+ return std::nullopt;
+}
+
+template <class TKey, class TValue>
+TValue TSyncExpiringCache<TKey, TValue>::Get(const TKey& key)
+{
+ auto now = NProfiling::GetCpuInstant();
+ auto deadline = now - ExpirationTimeout_.load();
+
+ {
+ auto guard = ReaderGuard(MapLock_);
+
+ auto it = Map_.find(key);
+ if (it != Map_.end()) {
+ auto& entry = it->second;
+ if (entry.LastUpdateTime >= deadline) {
+ entry.LastAccessTime = now;
+ return entry.Value;
+ }
+ }
+ }
+
+ auto result = CalculateValueAction_(key);
+
+ {
+ auto guard = WriterGuard(MapLock_);
+
+ auto it = Map_.find(key);
+ if (it != Map_.end()) {
+ it->second = {now, now, std::move(result)};
+ } else {
+ auto emplaceResult = Map_.emplace(
+ key,
+ TEntry(now, now, std::move(result)));
+ YT_VERIFY(emplaceResult.second);
+ it = emplaceResult.first;
+ }
+
+ return it->second.Value;
+ }
+}
+
+template <class TKey, class TValue>
+std::vector<TValue> TSyncExpiringCache<TKey, TValue>::Get(const std::vector<TKey>& keys)
+{
+ auto now = NProfiling::GetCpuInstant();
+ auto deadline = now - ExpirationTimeout_.load();
+
+ std::vector<TValue> foundValues;
+ std::vector<int> missingValueIndexes;
+
+ {
+ auto guard = ReaderGuard(MapLock_);
+
+ for (int i = 0; i < std::ssize(keys); ++i) {
+ auto it = Map_.find(keys[i]);
+ if (it != Map_.end()) {
+ auto& entry = it->second;
+ if (entry.LastUpdateTime >= deadline) {
+ entry.LastAccessTime = now;
+ foundValues.push_back(entry.Value);
+ continue;
+ }
+ }
+
+ missingValueIndexes.push_back(i);
+ }
+ }
+
+ if (missingValueIndexes.empty()) {
+ return foundValues;
+ }
+
+ std::vector<TValue> results;
+ results.reserve(keys.size());
+
+ int missingIndex = 0;
+ for (int keyIndex = 0; keyIndex < std::ssize(keys); ++keyIndex) {
+ if (missingIndex < std::ssize(missingValueIndexes) &&
+ missingValueIndexes[missingIndex] == keyIndex)
+ {
+ results.push_back(CalculateValueAction_(keys[keyIndex]));
+ ++missingIndex;
+ } else {
+ results.push_back(std::move(foundValues[keyIndex - missingIndex]));
+ }
+ }
+
+ {
+ auto guard = WriterGuard(MapLock_);
+
+ for (auto index : missingValueIndexes) {
+ if (auto it = Map_.find(keys[index]); it != Map_.end()) {
+ it->second = {now, now, results[index]};
+ } else {
+ YT_VERIFY(Map_.emplace(
+ keys[index],
+ TEntry(now, now, results[index]))
+ .second);
+ }
+ }
+ }
+
+ return results;
+}
+
+template <class TKey, class TValue>
+void TSyncExpiringCache<TKey, TValue>::Set(const TKey& key, TValue value)
+{
+ auto now = NProfiling::GetCpuInstant();
+
+ auto guard = WriterGuard(MapLock_);
+
+ if (auto it = Map_.find(key);
+ it != Map_.end())
+ {
+ it->second = {now, now, std::move(value)};
+ } else
+ {
+ YT_VERIFY(Map_.emplace(
+ key,
+ TEntry(now, now, std::move(value)))
+ .second);
+ }
+}
+
+template <class TKey, class TValue>
+void TSyncExpiringCache<TKey, TValue>::Invalidate(const TKey& key)
+{
+ auto guard = WriterGuard(MapLock_);
+
+ Map_.erase(key);
+}
+
+template <class TKey, class TValue>
+void TSyncExpiringCache<TKey, TValue>::Clear()
+{
+ decltype(Map_) map;
+ {
+ auto guard = WriterGuard(MapLock_);
+ Map_.swap(map);
+ }
+}
+
+template <class TKey, class TValue>
+void TSyncExpiringCache<TKey, TValue>::SetExpirationTimeout(std::optional<TDuration> expirationTimeout)
+{
+ EvictionExecutor_->SetPeriod(expirationTimeout);
+ ExpirationTimeout_.store(expirationTimeout
+ ? NProfiling::DurationToCpuDuration(*expirationTimeout)
+ : std::numeric_limits<NProfiling::TCpuDuration>::max() / 2); // effective (but safer) infinity
+}
+
+template <class TKey, class TValue>
+void TSyncExpiringCache<TKey, TValue>::DeleteExpiredItems()
+{
+ auto deadline = NProfiling::GetCpuInstant() - ExpirationTimeout_.load();
+
+ std::vector<TKey> keysToRemove;
+ {
+ auto guard = ReaderGuard(MapLock_);
+ for (const auto& [key, entry] : Map_) {
+ if (entry.LastAccessTime < deadline) {
+ keysToRemove.push_back(key);
+ }
+ }
+ }
+
+ if (keysToRemove.empty()) {
+ return;
+ }
+
+ std::vector<TValue> valuesToRemove;
+ valuesToRemove.reserve(keysToRemove.size());
+ {
+ auto guard = WriterGuard(MapLock_);
+ for (const auto& key : keysToRemove) {
+ auto it = Map_.find(key);
+ auto& entry = it->second;
+ if (entry.LastAccessTime < deadline) {
+ valuesToRemove.push_back(std::move(entry.Value));
+ Map_.erase(it);
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/sync_expiring_cache.h b/yt/yt/core/misc/sync_expiring_cache.h
new file mode 100644
index 0000000000..1bc7cc5874
--- /dev/null
+++ b/yt/yt/core/misc/sync_expiring_cache.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue>
+class TSyncExpiringCache
+ : public TRefCounted
+{
+public:
+ TSyncExpiringCache(
+ TCallback<TValue(const TKey&)> calculateValueAction,
+ std::optional<TDuration> expirationTimeout,
+ IInvokerPtr invoker);
+
+ std::optional<TValue> Find(const TKey& key);
+
+ TValue Get(const TKey& key);
+ std::vector<TValue> Get(const std::vector<TKey>& keys);
+
+ void Set(const TKey& key, TValue value);
+ void Invalidate(const TKey& key);
+ void Clear();
+
+ void SetExpirationTimeout(std::optional<TDuration> expirationTimeout);
+
+private:
+ struct TEntry
+ {
+ TEntry(
+ NProfiling::TCpuInstant lastAccessTime,
+ NProfiling::TCpuInstant lastUpdateTime,
+ TValue value);
+
+ TEntry(TEntry&& entry);
+ TEntry& operator=(TEntry&& other);
+
+ std::atomic<NProfiling::TCpuInstant> LastAccessTime;
+ NProfiling::TCpuInstant LastUpdateTime;
+ TValue Value;
+ };
+
+ const TCallback<TValue(const TKey&)> CalculateValueAction_;
+ const NConcurrency::TPeriodicExecutorPtr EvictionExecutor_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, MapLock_);
+ THashMap<TKey, TEntry> Map_;
+
+ std::atomic<NProfiling::TCpuDuration> ExpirationTimeout_;
+
+
+ void DeleteExpiredItems();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SYNC_EXPIRING_CACHE_INL_H_
+#include "sync_expiring_cache-inl.h"
+#undef SYNC_EXPIRING_CACHE_INL_H_
diff --git a/yt/yt/core/misc/tls_expiring_cache-inl.h b/yt/yt/core/misc/tls_expiring_cache-inl.h
new file mode 100644
index 0000000000..579112f19f
--- /dev/null
+++ b/yt/yt/core/misc/tls_expiring_cache-inl.h
@@ -0,0 +1,108 @@
+#ifndef TLS_EXPIRING_CACHE_INL_H_
+#error "Direct inclusion of this file is not allowed, include tls_expiring_cache.h"
+// For the sake of sane code completion.
+#include "tls_expiring_cache.h"
+#endif
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TKey, class TValue, size_t NumShards>
+TThreadLocalExpiringCache<TKey, TValue, NumShards>::TThreadLocalExpiringCache(
+ TDuration expirationTimeout)
+ : ExpirationTimeout_(expirationTimeout)
+{ }
+
+template <class TKey, class TValue, size_t NumShards>
+std::optional<TValue> TThreadLocalExpiringCache<TKey, TValue, NumShards>::Get(const TKey& key)
+{
+ return Cache_.GetRef(ExpirationTimeout_).Get(key);
+}
+
+template <class TKey, class TValue, size_t NumShards>
+void TThreadLocalExpiringCache<TKey, TValue, NumShards>::Set(const TKey& key, TValue value)
+{
+ Cache_.GetRef(ExpirationTimeout_).Set(key, std::move(value));
+}
+
+template <class TKey, class TValue, size_t NumShards>
+TThreadLocalExpiringCache<TKey, TValue, NumShards>::TCache::TCache(TDuration expirationTimeout)
+ : ExpirationTimeout_(DurationToCpuDuration(expirationTimeout))
+{ }
+
+template <class TKey, class TValue, size_t NumShards>
+std::optional<TValue> TThreadLocalExpiringCache<TKey, TValue, NumShards>::TCache::Get(const TKey& key)
+{
+ TryCleanup();
+
+ auto it = Entries_.find(key);
+ if (it == Entries_.end()) {
+ return std::nullopt;
+ }
+
+ auto now = GetCpuInstant();
+ auto deadline = now - ExpirationTimeout_;
+ if (it->second.LastUpdateTime < deadline) {
+ Entries_.erase(it);
+ return std::nullopt;
+ }
+
+ auto result = it->second.Value;
+ return result;
+}
+
+template <class TKey, class TValue, size_t NumShards>
+void TThreadLocalExpiringCache<TKey, TValue, NumShards>::TCache::Set(const TKey& key, TValue value)
+{
+ TryCleanup();
+
+ auto now = GetCpuInstant();
+
+ TEntry newEntry{
+ .Value = std::move(value),
+ .LastUpdateTime = now,
+ };
+
+ auto it = Entries_.find(key);
+ if (it == Entries_.end()) {
+ Entries_.emplace(key, std::move(newEntry));
+ } else {
+ it->second = std::move(newEntry);
+ }
+}
+
+template <class TKey, class TValue, size_t NumShards>
+void TThreadLocalExpiringCache<TKey, TValue, NumShards>::TCache::Cleanup()
+{
+ IterationsSinceCleanup_ = 0;
+
+ if (Entries_.empty()) {
+ return;
+ }
+
+ auto now = GetCpuInstant();
+ auto deadline = now - ExpirationTimeout_;
+
+ for (auto it = Entries_.begin(); it != Entries_.end();) {
+ if (it->second.LastUpdateTime < deadline) {
+ Entries_.erase(it++);
+ } else {
+ ++it;
+ }
+ }
+}
+
+template <class TKey, class TValue, size_t NumShards>
+void TThreadLocalExpiringCache<TKey, TValue, NumShards>::TCache::TryCleanup()
+{
+ if (IterationsSinceCleanup_ > CleanupAmortizationFactor * std::ssize(Entries_)) {
+ Cleanup();
+ } else {
+ ++IterationsSinceCleanup_;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/tls_expiring_cache.h b/yt/yt/core/misc/tls_expiring_cache.h
new file mode 100644
index 0000000000..685bf05dd5
--- /dev/null
+++ b/yt/yt/core/misc/tls_expiring_cache.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/threading/thread_local/thread_local.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An expiring cache with separate storage for each thread.
+//!
+//! It is useful when some cache entries are used very frequently. So, with a
+//! regular cache for this task, we might spend more on lock contention inside
+//! the cache rather than on cache misses.
+template <class TKey, class TValue, size_t ShardCount = 128>
+class TThreadLocalExpiringCache
+{
+public:
+ explicit TThreadLocalExpiringCache(TDuration expirationTimeout);
+
+ std::optional<TValue> Get(const TKey& key);
+ void Set(const TKey& key, TValue value);
+
+private:
+ struct TEntry
+ {
+ TValue Value;
+ TCpuInstant LastUpdateTime;
+ };
+
+ class TCache
+ {
+ public:
+ explicit TCache(TDuration expirationTimeout);
+
+ std::optional<TValue> Get(const TKey& key);
+ void Set(const TKey& key, TValue value);
+
+ private:
+ THashMap<TKey, TEntry> Entries_;
+ i64 IterationsSinceCleanup_ = 0;
+ NProfiling::TCpuDuration ExpirationTimeout_;
+
+ static constexpr int CleanupAmortizationFactor = 3;
+
+ void TryCleanup();
+ void Cleanup();
+ };
+
+ const TDuration ExpirationTimeout_;
+ ::NThreading::TThreadLocalValue<TCache, ::NThreading::EThreadLocalImpl::HotSwap, ShardCount> Cache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define TLS_EXPIRING_CACHE_INL_H_
+#include "tls_expiring_cache-inl.h"
+#undef TLS_EXPIRING_CACHE_INL_H_
diff --git a/yt/yt/core/misc/topological_ordering-inl.h b/yt/yt/core/misc/topological_ordering-inl.h
new file mode 100644
index 0000000000..f2832d496d
--- /dev/null
+++ b/yt/yt/core/misc/topological_ordering-inl.h
@@ -0,0 +1,86 @@
+#ifndef TOPOLOGICAL_ORDERING_INL_H_
+#error "Direct inclusion of this file is not allowed, include topological_ordering.h"
+// For the sake of sane code completion.
+#include "topological_ordering.h"
+#endif
+
+#include "topological_ordering.h"
+
+#include "serialize.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TVertexDescriptor>
+const std::vector<TVertexDescriptor>& TIncrementalTopologicalOrdering<TVertexDescriptor>::GetOrdering() const
+{
+ return TopologicalOrdering_;
+}
+
+template <typename TVertexDescriptor>
+void TIncrementalTopologicalOrdering<TVertexDescriptor>::AddEdge(const TVertexDescriptor& from, const TVertexDescriptor& to)
+{
+ if (OutgoingEdges_[from].insert(to).second) {
+ Rebuild();
+ }
+}
+
+template <typename TVertexDescriptor>
+void TIncrementalTopologicalOrdering<TVertexDescriptor>::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, OutgoingEdges_);
+ Persist(context, TopologicalOrdering_);
+}
+
+template <typename TVertexDescriptor>
+void TIncrementalTopologicalOrdering<TVertexDescriptor>::Rebuild()
+{
+ std::queue<TVertexDescriptor> queue;
+ THashMap<TVertexDescriptor, int> inDegree;
+
+ // Initialize in-degrees of all vertices.
+ for (const auto& [srcVertex, dstVertices] : OutgoingEdges_) {
+ // Make an entry for the vertex appear in the inDegree.
+ inDegree[srcVertex];
+ for (const auto& dstVertex : dstVertices) {
+ ++inDegree[dstVertex];
+ }
+ }
+
+ // Put all sources in the queue.
+ for (const auto& [vertex, degree] : inDegree) {
+ if (degree == 0) {
+ queue.push(vertex);
+ }
+ }
+
+ TopologicalOrdering_.clear();
+
+ // Extract sources and put them into the ordering while graph is non-empty.
+ while (!queue.empty()) {
+ const auto& vertex = queue.front();
+ queue.pop();
+
+ auto it = inDegree.find(vertex);
+ YT_VERIFY(it != inDegree.end() && it->second == 0);
+ inDegree.erase(it);
+
+ TopologicalOrdering_.push_back(vertex);
+
+ for (const auto& nextVertex : OutgoingEdges_[vertex]) {
+ if (--inDegree[nextVertex] == 0) {
+ queue.push(nextVertex);
+ }
+ }
+ }
+
+ // Check that all vertices are visited.
+ YT_VERIFY(inDegree.empty());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/topological_ordering.h b/yt/yt/core/misc/topological_ordering.h
new file mode 100644
index 0000000000..0213cb7d1b
--- /dev/null
+++ b/yt/yt/core/misc/topological_ordering.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Class representing a directed acyclic graph with its topological ordering
+//! allowing you to insert new edges in the graph.
+/*!
+ * Vertices of graph are represented with integers.
+ * TODO(max42): http://people.cs.georgetown.edu/~jfineman/papers/topsort.pdf :)
+ */
+template <typename TVertexDescriptor>
+class TIncrementalTopologicalOrdering
+{
+public:
+ TIncrementalTopologicalOrdering() = default;
+
+ const std::vector<TVertexDescriptor>& GetOrdering() const;
+
+ void Persist(const TStreamPersistenceContext& context);
+
+ void AddEdge(const TVertexDescriptor& from, const TVertexDescriptor& to);
+
+private:
+ std::vector<TVertexDescriptor> TopologicalOrdering_;
+ THashMap<TVertexDescriptor, THashSet<TVertexDescriptor>> OutgoingEdges_;
+
+ void Rebuild();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define TOPOLOGICAL_ORDERING_INL_H_
+#include "topological_ordering-inl.h"
+#undef TOPOLOGICAL_ORDERING_INL_H_
diff --git a/yt/yt/core/misc/unittests/algorithm_helpers_ut.cpp b/yt/yt/core/misc/unittests/algorithm_helpers_ut.cpp
new file mode 100644
index 0000000000..f7dc0f91ca
--- /dev/null
+++ b/yt/yt/core/misc/unittests/algorithm_helpers_ut.cpp
@@ -0,0 +1,108 @@
+#include <yt/yt/library/numeric/algorithm_helpers.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TIter, class TPredicate>
+TIter OrderedSearch(TIter begin, TIter end, TPredicate pred)
+{
+ auto result1 = BinarySearch(begin, end, pred);
+ auto result2 = ExponentialSearch(begin, end, pred);
+
+ EXPECT_EQ(result1, result2);
+ return result1;
+}
+
+template <class TIter, class TPredicate>
+std::reverse_iterator<TIter> BinarySearchReverse(
+ TIter begin,
+ TIter end,
+ TPredicate pred)
+{
+ auto result1 = std::make_reverse_iterator(OrderedSearch(begin, end, pred));
+ auto result2 = OrderedSearch(std::make_reverse_iterator(end), std::make_reverse_iterator(begin), [&] (auto it) {
+ return !pred(it);
+ });
+
+ EXPECT_EQ(result1, result2);
+ return result1;
+}
+
+TEST(TAlgorithmHelpersTest, BinarySearch)
+{
+ {
+ std::vector<TString> v;
+ auto it = NYT::LowerBound(v.begin(), v.end(), "test");
+ EXPECT_EQ(it, v.end());
+ }
+
+ {
+ int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ auto it = NYT::LowerBound(data, data + Y_ARRAY_SIZE(data), 2);
+ EXPECT_EQ(it - data, 1);
+
+ it = NYT::LowerBound(data, data + Y_ARRAY_SIZE(data), 10);
+ EXPECT_EQ(it, data + Y_ARRAY_SIZE(data));
+ }
+
+ {
+ std::vector<size_t> data = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+
+ {
+ auto it = OrderedSearch(data.begin(), data.end(), [] (auto it) {
+ return *it > 9;
+ });
+ EXPECT_EQ(it, data.begin() + 1);
+ }
+ {
+ auto it = OrderedSearch(data.begin(), data.end(), [] (auto it) {
+ return *it > 11;
+ });
+ EXPECT_EQ(it, data.begin());
+ }
+ {
+ auto it = NYT::LowerBound(data.rbegin(), data.rend(), 1u);
+ EXPECT_EQ(it, data.rbegin() + 1);
+ }
+ }
+}
+
+TEST(TAlgorithmHelpersTest, BinarySearchReverse)
+{
+ {
+ int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ for (int i = 0; i < 11; ++i) {
+ // Return iterator to last element less than i.
+ // If there is no such element result is equal to data.rend().
+ auto it = BinarySearchReverse(data, data + Y_ARRAY_SIZE(data), [&] (auto it) {
+ return *it < i;
+ });
+
+ auto expected = std::make_reverse_iterator(data + (i > 0 ? i - 1 : 0));
+ EXPECT_EQ(it, expected);
+ }
+ }
+}
+
+TEST(TAlgorithmHelpersTest, ExponentialSearch)
+{
+ {
+ int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ for (int i = 0; i < 11; ++i) {
+ auto it = ExpLowerBound(data, data + Y_ARRAY_SIZE(data), i);
+ EXPECT_EQ(it, data + (i > 0 ? i - 1 : 0));
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/arithmetic_formula_ut.cpp b/yt/yt/core/misc/unittests/arithmetic_formula_ut.cpp
new file mode 100644
index 0000000000..3664ed1984
--- /dev/null
+++ b/yt/yt/core/misc/unittests/arithmetic_formula_ut.cpp
@@ -0,0 +1,312 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/arithmetic_formula.h>
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TArithmeticFormulaTestWithoutVariables
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<const char*, i64>>
+{ };
+
+TEST_P(TArithmeticFormulaTestWithoutVariables, Test)
+{
+ const auto& args = GetParam();
+ const auto& formula = std::get<0>(args);
+ const auto& expected = std::get<1>(args);
+
+ auto expr = MakeArithmeticFormula(formula);
+
+ EXPECT_EQ(expected, expr.Eval({}))
+ << "formula: " << formula << std::endl
+ << "expected value: " << expected;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TArithmeticFormulaTestWithoutVariables1,
+ TArithmeticFormulaTestWithoutVariables,
+ ::testing::Values(
+ // +, -, few /%, parens
+ std::make_tuple("(((35)-35)+(47)+(12))/((13+(31))-(24-(37)-23)+(30-30-33+24-39))", 1),
+ std::make_tuple("((49)-34+(44)-35+(11-28)-(40)%46+20+22)+((19-50-40)-13+(17))", -58),
+ std::make_tuple("17%25-(23)-24+(45)+34-32+23-34-(10)", -4),
+ std::make_tuple("31%26-42+22+(19+48-(40+28)+(27))-(45-21)+(15)+10+26%43", 38),
+ std::make_tuple("((11)+46)-10%14-(29+43)-33-47+27+48+50-((19-18-20)+(46)-(29))", 22),
+ std::make_tuple("(10)-28-13%21+32+34+(40-48+(32)%41)+(32%44)+(35)%37+50-20+(31)", 187),
+ std::make_tuple("(48-29)-(44-(14)-35+28-16-((48)/13-(43)/22)-((12-41+32/45)+32+33-50-39))", -39),
+
+ // all except /%
+ std::make_tuple("(33-(35))*13|31<=43*37||(17+38)==!(34*(16)!=28!=15)<=50<17", 1),
+ std::make_tuple("(12&&49&(17)|27)==20!=35&&46==34>=(!!(16)-41<(10)==11||11*11*19!=((30)<=(40)<=48))", 0),
+ std::make_tuple("!!(37)||(27)<39&28<=25^16|(!40+40)^(47<15<25)<47^45+16==23!=17&&22<=!38!=14", 1),
+ std::make_tuple("!!27!=15==(35||23||!!(40)&&18<=((17>=34)-38==(47))&&22>13|40*31!=40+(!38^43)&(34)|(27))", 1),
+ std::make_tuple("(!(28)-(25)||49^(23>(21)==(36<=17)!=23>(27<=33))!=!!(47)||46^22+19<11>=15)==46", 0),
+ std::make_tuple("(!24-(12)+24&(45)|(43))<=!28==(50&&(46)<21<(17))<=28-(20)<=29&&(28)==!(24)!=24<=(20<=37)", 0),
+ std::make_tuple("16<46&40*30", 0),
+ std::make_tuple("38>=(48)|(13>=30)|!!31!=21|(49!=(31)>=(36)==(33))>=((!(30)==24)*25||48)||!47!=18", 1),
+ std::make_tuple("!22<23^((49)*33)!=50==!(32)||(49)||!(36)+(35)-26>=18>(!(31)>=21-(10||39)||(11)>=25)", 1),
+
+ // spaces
+ std::make_tuple("( 14!=24 )< 27 <=44== 15*22", 0),
+ std::make_tuple(" 24<36 >=( 41*48)<= ( !23!=16 )<=( 49- 34)", 1),
+ std::make_tuple(" (34 *13 )!=(48 >= 37 ) <=( 43!=( 19 ) )- 34* 45", 1),
+ std::make_tuple(" 13 * 50 +(14) <43 == 17 ==( !18 < 47 ) ", 0),
+ std::make_tuple(" 44!=18 > 43 -( 41 )>=(34 )!= 30", 1),
+ std::make_tuple(" (( 14)*(15 ) )+43 -!(23 )>= 36 > 33 <=(24 )", 1),
+ std::make_tuple(" !24 != 43<(!50-34>= 49 )", 0),
+ std::make_tuple(" ! 11>=43 > 19< 12 <((32)>39 )", 0),
+ std::make_tuple("!14 ==46 <=22 !=(15 )< 31", 0),
+ std::make_tuple("(( 38)>= 16 > 20>=(26) )<= 50 -33", 1),
+ std::make_tuple(" 36> 37 == 12< !(11 )> 32 > 25+14 ", 1),
+ std::make_tuple(" (26)>=38 +( !16> 28 )", 0),
+ std::make_tuple(" 30 >(25) * 23 +26-( 20 )", 0),
+ std::make_tuple("! 26<= 11<! 13 + 10 >( 26==36 )", 1),
+ std::make_tuple("!10* ! 33 <16 <= 11", 1),
+ std::make_tuple("(34 )- 50 * 11- 33 !=(26 )<= 16", 1),
+ std::make_tuple(" !( 27)> 30 >=36== 44 != ! 18 <= 23 *35", 1),
+ std::make_tuple("( 29 >= 12)<=( ! 35==17)", 0),
+ std::make_tuple("!16 <= 23 ==! 47 <27 <=( (24)< 23 >= 36< 31 )", 1),
+ std::make_tuple(" ! 12 >=17+ ( 25+ 10)<15 < 36", 1)
+));
+
+INSTANTIATE_TEST_SUITE_P(
+ TArithmeticFormulaTestWithoutVariables2,
+ TArithmeticFormulaTestWithoutVariables,
+ ::testing::Values(
+ // some manual stuff
+ std::make_tuple("1+1", 2),
+ std::make_tuple("1+1+1", 3),
+ std::make_tuple("10-1", 9),
+ std::make_tuple("10-5-3", 2),
+ std::make_tuple( "10 -5 --2", 7),
+ std::make_tuple("( 1) + -2 ",-1),
+ std::make_tuple("(((((5*5)))))", 25),
+ std::make_tuple( "2 * 2 + 10 / 5 ", 6),
+ std::make_tuple("7 % 4 * 4", 12),
+ std::make_tuple("7 % (4 * 4)", 7),
+ std::make_tuple("1&&2&&3||6", 1),
+ std::make_tuple("1&2&3|6", 6),
+ std::make_tuple("!!(1||(2))", 1),
+ std::make_tuple("(1)-1", 0),
+ std::make_tuple("(1) --1", 2),
+ std::make_tuple("9223372036854775807", std::numeric_limits<i64>::max()),
+ std::make_tuple("-9223372036854775808", std::numeric_limits<i64>::min()),
+ std::make_tuple("10 in (10, 20, 30)", 1),
+ std::make_tuple("10 in 10, 20, 30", 1),
+ std::make_tuple("5+10 in 12+3", 1),
+ std::make_tuple("5+(10 in 12)+3", 8),
+ std::make_tuple("10 in 5, 6, 7+4, 2+8", 1),
+ std::make_tuple("11 in 5, 6, 7+4, 2+8", 1),
+
+ std::make_tuple("1 + %true", 2),
+ std::make_tuple("%true+1", 2),
+ std::make_tuple("10%%true", 0),
+ std::make_tuple("10 % %true", 0)
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TArithmeticFormulaTestParseError
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<const char*>
+{ };
+
+TEST_P(TArithmeticFormulaTestParseError, Test)
+{
+ const auto& args = GetParam();
+ const auto& formula = args;
+
+ EXPECT_THROW(MakeArithmeticFormula(formula), TErrorException);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TArithmeticFormulaTestParseError,
+ TArithmeticFormulaTestParseError,
+ ::testing::Values(
+ "()",
+ "1 1",
+ ")",
+ "(",
+ "(1)---1",
+ "+1",
+ "--",
+ "- 1",
+ "1var",
+ "(-)",
+ "(1+)",
+ "++a",
+ "-(1+2)", // unary minus is not yet supported
+ "2//2",
+ "(^_^)", // q^_^p is a valid expression btw
+ "2=3",
+ "!= 10",
+ "a = = b",
+ "a > = b",
+ "a =- b",
+ "in + 1",
+ "in in",
+ "5, 6,",
+ ",",
+ ", 5",
+ "in 2",
+ "10 in",
+ "10 in ()",
+ "1, 1",
+ "1, 1 in 1",
+ "1, 1 in (1, 1)",
+ "(1, 1) in (1, 1)",
+ "1, 1 + 2",
+ "in/",
+ "1,(1,1)",
+ "%%true",
+ "%true.foo",
+ "1%% true",
+ "1 %in b"
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TArithmeticFormulaTest, Misc)
+{
+ EXPECT_THROW(MakeArithmeticFormula("9223372036854775808"), TFromStringException);
+
+ auto divideByZero = MakeArithmeticFormula("1/0");
+ EXPECT_THROW(divideByZero.Eval({}), TErrorException);
+
+ auto modulusByZero = MakeArithmeticFormula("1%(10-5*2)");
+ EXPECT_THROW(modulusByZero.Eval({}), TErrorException);
+
+ auto divideMinIntByMinusOne = MakeArithmeticFormula("a / -1");
+ EXPECT_THROW(divideMinIntByMinusOne.Eval({{"a", std::numeric_limits<i64>::min()}}), TErrorException);
+
+ auto minIntModuloMinusOne = MakeArithmeticFormula("a % -1");
+ EXPECT_THROW(minIntModuloMinusOne.Eval({{"a", std::numeric_limits<i64>::min()}}), TErrorException);
+
+ auto formulaWithVariables = MakeArithmeticFormula("( a+ b)/c");
+
+ // divide by zero
+ EXPECT_THROW(formulaWithVariables.Eval({{"a", 1}, {"b", 2}, {"c", 0}}), TErrorException);
+
+ // not enough variables
+ EXPECT_THROW(formulaWithVariables.Eval({{"a", 1}, {"b", 2}}), TErrorException);
+
+ EXPECT_EQ(10, formulaWithVariables.Eval({{"a", 20}, {"b", 30}, {"c", 5}}));
+
+ auto longVars = MakeArithmeticFormula("abacaba ^ variable");
+ EXPECT_EQ(435, longVars.Eval({{"variable", 123}, {"abacaba", 456}}));
+
+ auto schedule = MakeArithmeticFormula("hours % 2 == 1 && minutes % 5 == 2");
+ int cnt = 0;
+ for (int hour = 0; hour < 24; ++hour) {
+ for (int minute = 0; minute < 60; ++minute) {
+ if (schedule.Eval({{"hours", hour}, {"minutes", minute}})) {
+ ++cnt;
+ }
+ }
+ }
+ EXPECT_EQ(cnt, (24 / 2) * (60 / 5));
+
+ auto emptyFormula = MakeArithmeticFormula("");
+ EXPECT_TRUE(emptyFormula.IsEmpty());
+ EXPECT_THROW(emptyFormula.Eval({}), TErrorException);
+
+ auto modulusByTrue = MakeArithmeticFormula("123 %true");
+ EXPECT_EQ(modulusByTrue.Eval({{"true", 100}}), 23);
+ EXPECT_THROW(modulusByTrue.Eval({}), TErrorException);
+}
+
+TEST(TArithmeticFormulaTest, InExpression)
+{
+ auto formulaWithIn = MakeArithmeticFormula("inf in (1000*1000*1000, 10, 20, 30)");
+ EXPECT_EQ(1, formulaWithIn.Eval({{"inf", 1'000'000'000}}));
+
+ auto multilevelIn = MakeArithmeticFormula("a in b in c");
+ EXPECT_EQ(1, multilevelIn.Eval({{"a", 1}, {"b", 1}, {"c", 1}}));
+ EXPECT_EQ(0, multilevelIn.Eval({{"a", 0}, {"b", 1}, {"c", 1}}));
+ // Check associativity.
+ EXPECT_EQ(1, multilevelIn.Eval({{"a", 1}, {"b", 2}, {"c", 0}}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TArithmeticFormulaTest, Equality)
+{
+ auto f1 = MakeArithmeticFormula("a+b");
+ auto f2 = MakeArithmeticFormula("a + b");
+ auto f3 = MakeArithmeticFormula("(a + (b))");
+ auto f4 = MakeArithmeticFormula("b+a");
+ auto f5 = MakeArithmeticFormula("a");
+
+ auto fn1 = MakeArithmeticFormula("1");
+ auto fn2 = MakeArithmeticFormula("2");
+
+ auto assertComparison = [](const TArithmeticFormula& lhs, const TArithmeticFormula& rhs, bool equal) {
+ if (equal) {
+ EXPECT_TRUE(lhs == rhs);
+ EXPECT_EQ(lhs.GetHash(), rhs.GetHash());
+ } else {
+ EXPECT_FALSE(lhs == rhs);
+ EXPECT_NE(lhs.GetHash(), rhs.GetHash());
+ }
+ };
+
+ assertComparison(f1, f2, true);
+ assertComparison(f1, f3, true);
+ assertComparison(f1, f4, false);
+ assertComparison(f1, f5, false);
+ assertComparison(f2, f1, true);
+ assertComparison(f2, f3, true);
+ assertComparison(f2, f4, false);
+ assertComparison(f2, f5, false);
+ assertComparison(f3, f1, true);
+ assertComparison(f3, f2, true);
+ assertComparison(f3, f4, false);
+ assertComparison(f3, f5, false);
+ assertComparison(f4, f1, false);
+ assertComparison(f4, f2, false);
+ assertComparison(f4, f3, false);
+ assertComparison(f4, f5, false);
+ assertComparison(f5, f1, false);
+ assertComparison(f5, f2, false);
+ assertComparison(f5, f3, false);
+ assertComparison(f5, f4, false);
+
+ assertComparison(fn1, fn2, false);
+ assertComparison(fn1, f5, false);
+
+ assertComparison(MakeArithmeticFormula("1"), MakeArithmeticFormula("%true"), false);
+ assertComparison(MakeArithmeticFormula("%false"), MakeArithmeticFormula("%true"), false);
+}
+
+TEST(TArithmeticFormulaTest, TestValidateVariable)
+{
+ ValidateArithmeticFormulaVariable("abc");
+ ValidateArithmeticFormulaVariable("abc123");
+ ValidateArithmeticFormulaVariable("ABc_123");
+ ValidateArithmeticFormulaVariable("IN");
+ ValidateArithmeticFormulaVariable("iN");
+ ValidateArithmeticFormulaVariable("In");
+
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("123abc"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable(" abc"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable(" abc"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("ab c"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable(""), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("dollar$"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("a+b"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("in"), TErrorException);
+
+ // OK for boolean, but parse error for arithmetic
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("var/2"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("var-var-var"), TErrorException);
+ EXPECT_THROW(ValidateArithmeticFormulaVariable("tablet_common/news-queue"), TErrorException);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/async_expiring_cache_ut.cpp b/yt/yt/core/misc/unittests/async_expiring_cache_ut.cpp
new file mode 100644
index 0000000000..053f75ab2a
--- /dev/null
+++ b/yt/yt/core/misc/unittests/async_expiring_cache_ut.cpp
@@ -0,0 +1,377 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/misc/async_expiring_cache.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleExpiringCache
+ : public TAsyncExpiringCache<int, int>
+{
+public:
+ explicit TSimpleExpiringCache(TAsyncExpiringCacheConfigPtr config, float successProbability = 1.0)
+ : TAsyncExpiringCache<int, int>(std::move(config))
+ , Generator_(RandomDevice_())
+ , Bernoulli_(successProbability)
+ { }
+
+ int GetCount()
+ {
+ return Count_;
+ }
+
+ TAsyncExpiringCacheConfigPtr GetConfig() const
+ {
+ return TAsyncExpiringCache::GetConfig();
+ }
+
+protected:
+ TFuture<int> DoGet(const int& /*key*/, bool /*isPeriodicUpdate*/) noexcept override
+ {
+ ++Count_;
+
+ return Bernoulli_(Generator_)
+ ? MakeFuture<int>(0)
+ : MakeFuture<int>(TErrorOr<int>(TError("error")));
+ }
+
+private:
+ std::random_device RandomDevice_;
+ std::mt19937 Generator_;
+ std::bernoulli_distribution Bernoulli_;
+ std::atomic<int> Count_ = {0};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TFuture<T> MakeDelayedFuture(const TDuration& duration, T x)
+{
+ return TDelayedExecutor::MakeDelayed(duration)
+ .Apply(BIND([=] () { return x; }));
+}
+
+class TDelayedExpiringCache
+ : public TAsyncExpiringCache<int, int>
+{
+public:
+ TDelayedExpiringCache(TAsyncExpiringCacheConfigPtr config, const TDuration& delay)
+ : TAsyncExpiringCache<int, int>(std::move(config))
+ , Delay_(delay)
+ { }
+
+ int GetCount()
+ {
+ return Count_;
+ }
+
+protected:
+ TFuture<int> DoGet(const int& /*key*/, bool /*isPeriodicUpdate*/) noexcept override
+ {
+ int count = ++Count_;
+ return MakeDelayedFuture(Delay_, count);
+ }
+
+private:
+ TDuration Delay_;
+ std::atomic<int> Count_ = {0};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TAsyncExpiringCacheTest, TestBackgroundUpdate)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(100);
+ auto cache = New<TSimpleExpiringCache>(config);
+
+ auto start = Now();
+ cache->Get(0);
+ Sleep(TDuration::MilliSeconds(500));
+ int actual = cache->GetCount();
+ auto end = Now();
+
+ int duration = (end - start).MilliSeconds();
+ int expected = duration / config->RefreshTime->MilliSeconds();
+
+ EXPECT_LE(std::abs(expected - actual), 50);
+}
+
+TEST(TAsyncExpiringCacheTest, TestConcurrentAccess)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(100);
+ auto cache = New<TSimpleExpiringCache>(config, 0.9);
+
+ auto threadPool = CreateThreadPool(10, "CacheAccessor");
+ std::vector<TFuture<void>> asyncResult;
+
+ for (int i = 0; i < 10; ++i) {
+ auto callback = BIND([=] () {
+ for (int j = 0; j < 1000; ++j) {
+ cache->Get(0);
+
+ if (rand() % 20 == 0) {
+ cache->InvalidateActive(0);
+ }
+ }
+ });
+ asyncResult.push_back(
+ callback
+ .AsyncVia(threadPool->GetInvoker())
+ .Run());
+ }
+
+ config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(50);
+ config->ExpireAfterAccessTime = TDuration::MilliSeconds(10);
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::MilliSeconds(10);
+ config->ExpireAfterFailedUpdateTime = TDuration::MilliSeconds(10);
+
+ cache->Reconfigure(config);
+
+ WaitFor(AllSucceeded(asyncResult))
+ .ThrowOnError();
+
+ Sleep(TDuration::MilliSeconds(200));
+
+ auto start = Now();
+ int begin = cache->GetCount();
+ Sleep(TDuration::Seconds(1));
+ int actual = cache->GetCount() - begin;
+ auto end = Now();
+
+ int duration = (end - start).MilliSeconds();
+ int expected = duration / 100;
+
+ EXPECT_GE(expected, actual);
+}
+
+TEST(TAsyncExpiringCacheTest, TestReconfigure)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(100);
+ auto cache = New<TSimpleExpiringCache>(config, 0.9);
+
+ EXPECT_EQ(cache->GetConfig()->RefreshTime, TDuration::MilliSeconds(100));
+
+ config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(50);
+ config->ExpireAfterAccessTime = TDuration::MilliSeconds(10);
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::MilliSeconds(10);
+ config->ExpireAfterFailedUpdateTime = TDuration::MilliSeconds(10);
+
+ cache->Reconfigure(config);
+
+ EXPECT_EQ(cache->GetConfig()->RefreshTime, TDuration::MilliSeconds(50));
+ EXPECT_EQ(cache->GetConfig()->ExpireAfterAccessTime, TDuration::MilliSeconds(10));
+ EXPECT_EQ(cache->GetConfig()->ExpireAfterSuccessfulUpdateTime, TDuration::MilliSeconds(10));
+ EXPECT_EQ(cache->GetConfig()->ExpireAfterFailedUpdateTime, TDuration::MilliSeconds(10));
+}
+
+TEST(TAsyncExpiringCacheTest, TestAccessTime1)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(200);
+ config->ExpireAfterAccessTime = TDuration::Zero();
+ auto cache = New<TSimpleExpiringCache>(config);
+
+ EXPECT_TRUE(cache->Get(0).IsSet());
+ Sleep(TDuration::MilliSeconds(300));
+
+ EXPECT_EQ(1, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestAccessTime2)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterAccessTime = TDuration::MilliSeconds(100);
+ auto cache = New<TSimpleExpiringCache>(config);
+
+ for (int i = 0; i < 10; ++i) {
+ cache->Get(0);
+ Sleep(TDuration::MilliSeconds(50));
+ }
+
+
+ EXPECT_EQ(1, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestAccessTime3)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterAccessTime = TDuration::MilliSeconds(50);
+ auto cache = New<TSimpleExpiringCache>(config);
+
+ for (int i = 0; i < 10; ++i) {
+ cache->Get(0);
+ Sleep(TDuration::MilliSeconds(100));
+ }
+
+ EXPECT_EQ(10, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, CacheDoesntRefreshExpiredItem)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(200);
+ config->ExpireAfterAccessTime = TDuration::MilliSeconds(100);
+ auto cache = New<TSimpleExpiringCache>(config);
+
+ EXPECT_TRUE(cache->Get(0).IsSet());
+ Sleep(TDuration::MilliSeconds(500));
+
+ EXPECT_EQ(1, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestUpdateTime1)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::MilliSeconds(50);
+ auto cache = New<TSimpleExpiringCache>(config, 1.0);
+
+ for (int i = 0; i < 10; ++i) {
+ cache->Get(0);
+ Sleep(TDuration::MilliSeconds(100));
+ }
+
+ EXPECT_EQ(10, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestUpdateTime2)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterFailedUpdateTime = TDuration::MilliSeconds(50);
+ auto cache = New<TSimpleExpiringCache>(config, 0.0);
+
+ for (int i = 0; i < 10; ++i) {
+ cache->Get(0);
+ Sleep(TDuration::MilliSeconds(100));
+ }
+
+ EXPECT_EQ(10, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestZeroCache1)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterAccessTime = TDuration::Zero();
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::Zero();
+ config->ExpireAfterFailedUpdateTime = TDuration::Zero();
+
+ auto cache = New<TDelayedExpiringCache>(config, TDuration::MilliSeconds(200));
+ for (int i = 0; i < 10; ++i) {
+ auto future = cache->Get(0);
+ EXPECT_EQ(i + 1, cache->GetCount());
+ Sleep(TDuration::MilliSeconds(100));
+ auto valueOrError = future.Get();
+ EXPECT_TRUE(valueOrError.IsOK());
+ EXPECT_EQ(i + 1, valueOrError.Value());
+ Sleep(TDuration::MilliSeconds(100));
+ }
+
+ EXPECT_EQ(10, cache->GetCount());
+}
+
+TEST(TAsyncExpiringCacheTest, TestZeroCache2)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->ExpireAfterAccessTime = TDuration::Zero();
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::Zero();
+ config->ExpireAfterFailedUpdateTime = TDuration::Zero();
+
+ auto cache = New<TDelayedExpiringCache>(config, TDuration::MilliSeconds(100));
+ std::vector<TFuture<int>> futures;
+ for (int i = 0; i < 10; ++i) {
+ futures.push_back(cache->Get(0));
+ }
+
+ for (const auto& future : futures) {
+ auto result = future.Get();
+ EXPECT_TRUE(result.IsOK());
+ EXPECT_EQ(1, result.Value());
+ }
+
+ EXPECT_EQ(1, cache->GetCount());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRevisionCache
+ : public TAsyncExpiringCache<int, int>
+{
+public:
+ TRevisionCache(const TAsyncExpiringCacheConfigPtr& config)
+ : TAsyncExpiringCache<int, int>(config)
+ { }
+
+ std::atomic<int> InitialFetchCount = {0};
+ std::atomic<int> PeriodicUpdateCount = {0};
+ std::atomic<int> ForcedUpdateCount = {0};
+
+private:
+ virtual TFuture<int> DoGet(
+ const int& /* key */,
+ bool /* isPeriodicUpdate */) noexcept
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ virtual TFuture<int> DoGet(
+ const int& /* key */,
+ const TErrorOr<int>* oldValue,
+ EUpdateReason reason) noexcept
+ {
+ if (reason == EUpdateReason::ForcedUpdate) {
+ ForcedUpdateCount++;
+ return MakeDelayedFuture(TDuration::MilliSeconds(100), oldValue->Value() + 1);
+ } else if (reason == EUpdateReason::PeriodicUpdate) {
+ PeriodicUpdateCount++;
+ return MakeDelayedFuture(TDuration::MilliSeconds(100), oldValue->Value());
+ } else {
+ InitialFetchCount++;
+ return MakeDelayedFuture(TDuration::MilliSeconds(100), 0);
+ }
+ }
+};
+
+TEST(TAsyncExpiringCacheTest, ForceUpdate)
+{
+ auto config = New<TAsyncExpiringCacheConfig>();
+ config->RefreshTime = TDuration::MilliSeconds(100);
+
+ auto cache = New<TRevisionCache>(config);
+
+ auto rev0 = cache->Get(0);
+ ASSERT_EQ(rev0.Get().Value(), 0);
+
+ Sleep(TDuration::MilliSeconds(500));
+
+ rev0 = cache->Get(0);
+ ASSERT_EQ(rev0.Get().Value(), 0);
+ ASSERT_GE(cache->PeriodicUpdateCount.load(), 0);
+
+ cache->ForceRefresh(0, 0);
+ auto rev1 = cache->Get(0);
+ ASSERT_EQ(rev1.Get().Value(), 1);
+ ASSERT_EQ(cache->ForcedUpdateCount.load(), 1);
+
+ cache->ForceRefresh(0, 0);
+ rev1 = cache->Get(0);
+ ASSERT_EQ(rev1.Get().Value(), 1);
+ ASSERT_EQ(cache->ForcedUpdateCount.load(), 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/async_slru_cache_ut.cpp b/yt/yt/core/misc/unittests/async_slru_cache_ut.cpp
new file mode 100644
index 0000000000..79256217af
--- /dev/null
+++ b/yt/yt/core/misc/unittests/async_slru_cache_ut.cpp
@@ -0,0 +1,1115 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/async_slru_cache.h>
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/library/profiling/testing.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+using namespace NProfiling;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSimpleCachedValue)
+
+class TSimpleCachedValue
+ : public TAsyncCacheValueBase<int, TSimpleCachedValue>
+{
+public:
+ explicit TSimpleCachedValue(int key, int value, int weight = 1)
+ : TAsyncCacheValueBase(key)
+ , Value(value)
+ , Weight(weight)
+ { }
+
+ int Value;
+ int Weight;
+};
+
+DEFINE_REFCOUNTED_TYPE(TSimpleCachedValue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TSimpleSlruCache)
+
+class TSimpleSlruCache
+ : public TAsyncSlruCacheBase<int, TSimpleCachedValue>
+{
+public:
+ explicit TSimpleSlruCache(TSlruCacheConfigPtr config, TProfiler profiler = {})
+ : TAsyncSlruCacheBase(std::move(config), std::move(profiler))
+ { }
+
+ struct TCountersState
+ {
+ i64 SyncHitWeight;
+ i64 AsyncHitWeight;
+ i64 MissedWeight;
+ i64 SyncHit;
+ i64 AsyncHit;
+ i64 Missed;
+
+ TCountersState operator -(const TCountersState& other) const
+ {
+ return TCountersState {
+ .SyncHitWeight = SyncHitWeight - other.SyncHitWeight,
+ .AsyncHitWeight = AsyncHitWeight - other.AsyncHitWeight,
+ .MissedWeight = MissedWeight - other.MissedWeight,
+ .SyncHit = SyncHit - other.SyncHit,
+ .AsyncHit = AsyncHit - other.AsyncHit,
+ .Missed = Missed - other.Missed
+ };
+ }
+ };
+
+ TCountersState ReadSmallGhostCounters() const
+ {
+ return ReadCounters(GetSmallGhostCounters());
+ }
+
+ TCountersState ReadLargeGhostCounters() const
+ {
+ return ReadCounters(GetLargeGhostCounters());
+ }
+
+protected:
+ i64 GetWeight(const TSimpleCachedValuePtr& value) const override
+ {
+ return value->Weight;
+ }
+
+ static TCountersState ReadCounters(const TCounters& counters)
+ {
+ return TCountersState {
+ .SyncHitWeight = TTesting::ReadCounter(counters.SyncHitWeightCounter),
+ .AsyncHitWeight = TTesting::ReadCounter(counters.AsyncHitWeightCounter),
+ .MissedWeight = TTesting::ReadCounter(counters.MissedWeightCounter),
+ .SyncHit = TTesting::ReadCounter(counters.SyncHitCounter),
+ .AsyncHit = TTesting::ReadCounter(counters.AsyncHitCounter),
+ .Missed = TTesting::ReadCounter(counters.MissedCounter)
+ };
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TSimpleSlruCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TCountingSlruCache)
+
+class TCountingSlruCache
+ : public TAsyncSlruCacheBase<int, TSimpleCachedValue>
+{
+public:
+ explicit TCountingSlruCache(TSlruCacheConfigPtr config, bool enableResurrection = true)
+ : TAsyncSlruCacheBase(std::move(config)), EnableResurrection_(enableResurrection)
+ { }
+
+ DEFINE_BYVAL_RO_PROPERTY(int, ItemCount, 0);
+ DEFINE_BYVAL_RO_PROPERTY(int, TotalAdded, 0);
+ DEFINE_BYVAL_RO_PROPERTY(int, TotalRemoved, 0);
+
+protected:
+ i64 GetWeight(const TSimpleCachedValuePtr& value) const override
+ {
+ return value->Weight;
+ }
+
+ void OnAdded(const TSimpleCachedValuePtr& /*value*/) override
+ {
+ ++ItemCount_;
+ ++TotalAdded_;
+ }
+
+ void OnRemoved(const TSimpleCachedValuePtr& /*value*/) override
+ {
+ --ItemCount_;
+ ++TotalRemoved_;
+ EXPECT_GE(ItemCount_, 0);
+ }
+ bool IsResurrectionSupported() const override
+ {
+ return EnableResurrection_;
+ }
+
+private:
+ bool EnableResurrection_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCountingSlruCache)
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<int> GetAllKeys(const TSimpleSlruCachePtr& cache)
+{
+ std::vector<int> result;
+
+ for (const auto& cachedValue : cache->GetAll()) {
+ result.emplace_back(cachedValue->GetKey());
+ }
+ std::sort(result.begin(), result.end());
+
+ return result;
+}
+
+std::vector<int> GetKeysFromRanges(std::vector<std::pair<int, int>> ranges)
+{
+ std::vector<int> result;
+
+ for (const auto& [from, to] : ranges) {
+ for (int i = from; i < to; ++i) {
+ result.push_back(i);
+ }
+ }
+ std::sort(result.begin(), result.end());
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSlruCacheConfigPtr CreateCacheConfig(i64 cacheSize)
+{
+ auto config = TSlruCacheConfig::CreateWithCapacity(cacheSize);
+ config->ShardCount = 1;
+
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TAsyncSlruCacheTest, Simple)
+{
+ const int cacheSize = 10;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(config);
+
+ for (int i = 0; i < 2 * cacheSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i));
+ }
+
+ // Cache size is small, so on the second pass every element should be missing too.
+ for (int i = 0; i < 2 * cacheSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i * 2));
+ }
+
+ // Only last cacheSize items.
+ EXPECT_EQ(GetAllKeys(cache), GetKeysFromRanges({{cacheSize, 2 * cacheSize}}));
+
+ // Check that Find() works as expected.
+ for (int i = 0; i < cacheSize; ++i) {
+ auto cachedValue = cache->Find(i);
+ EXPECT_EQ(cachedValue, nullptr);
+ }
+ for (int i = cacheSize; i < 2 * cacheSize; ++i) {
+ auto cachedValue = cache->Find(i);
+ ASSERT_NE(cachedValue, nullptr);
+ EXPECT_EQ(cachedValue->GetKey(), i);
+ EXPECT_EQ(cachedValue->Value, i * 2);
+ }
+}
+
+TEST(TAsyncSlruCacheTest, Youngest)
+{
+ const int cacheSize = 10;
+ const int oldestSize = 5;
+ auto config = CreateCacheConfig(cacheSize);
+ config->YoungerSizeFraction = 0.5;
+ auto cache = New<TSimpleSlruCache>(config);
+
+ for (int i = 0; i < oldestSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i));
+ // Move to oldest.
+ cache->Find(i);
+ }
+
+ for (int i = cacheSize; i < 2 * cacheSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i));
+ }
+
+ EXPECT_EQ(GetAllKeys(cache), GetKeysFromRanges({{0, oldestSize}, {cacheSize + oldestSize, 2 * cacheSize}}));
+}
+
+TEST(TAsyncSlruCacheTest, Resurrection)
+{
+ const int cacheSize = 10;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(config);
+
+ std::vector<TSimpleCachedValuePtr> values;
+
+ for (int i = 0; i < 2 * cacheSize; ++i) {
+ auto value = New<TSimpleCachedValue>(i, i);
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(value);
+ values.push_back(value);
+ }
+
+ EXPECT_EQ(cache->GetSize(), cacheSize);
+ // GetAll() returns values which are in cache or can be resurrected.
+ EXPECT_EQ(GetAllKeys(cache), GetKeysFromRanges({{0, 2 * cacheSize}}));
+
+ for (int i = 0; i < 2 * cacheSize; ++i) {
+ // It's expired because our cache is too small.
+ EXPECT_EQ(cache->Find(i), nullptr);
+ // But lookup can find and restore it (and make some other values expired)
+ // because the value is alive in 'values' vector.
+ EXPECT_EQ(cache->Lookup(i).Get().ValueOrThrow(), values[i]);
+ }
+}
+
+TEST(TAsyncSlruCacheTest, LookupBetweenBeginAndEndInsert)
+{
+ const int cacheSize = 10;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(config);
+
+ auto cookie = cache->BeginInsert(1);
+ EXPECT_TRUE(cookie.IsActive());
+
+ EXPECT_FALSE(cache->Find(1).operator bool ());
+
+ auto future = cache->Lookup(1);
+ EXPECT_TRUE(future.operator bool());
+ EXPECT_FALSE(future.IsSet());
+
+ auto value = New<TSimpleCachedValue>(1, 10);
+ cookie.EndInsert(value);
+
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(future.Get().IsOK());
+ EXPECT_EQ(value, future.Get().Value());
+}
+
+TEST(TAsyncSlruCacheTest, UpdateWeight)
+{
+ const int cacheSize = 10;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(config);
+
+ for (int i = 0; i < cacheSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i));
+ }
+
+ // All values fit in cache.
+ for (int i = 0; i < cacheSize; ++i) {
+ auto value = cache->Find(i);
+ EXPECT_NE(value, nullptr);
+ EXPECT_EQ(value->GetKey(), i);
+ EXPECT_EQ(value->Value, i);
+ }
+
+ {
+ // When we search '0' again, it goes to the end of the queue to be deleted.
+ auto value = cache->Find(0);
+ value->Weight = cacheSize;
+ cache->UpdateWeight(value);
+ // It should not be deleted.
+ EXPECT_EQ(cache->Find(0), value);
+ }
+
+ for (int i = 1; i < cacheSize; ++i) {
+ EXPECT_EQ(cache->Find(i), nullptr);
+ }
+
+ {
+ auto value = New<TSimpleCachedValue>(1, 1);
+ auto cookie = cache->BeginInsert(1);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(value);
+
+ // After first insert we can not find value '1' because '0' was in 'oldest' segment.
+ EXPECT_EQ(cache->Find(1), nullptr);
+ // But now '0' should be moved to 'youngest' after Trim() call.
+ // Second insert should delete '0' and insert '1' because it's newer.
+ cookie = cache->BeginInsert(1);
+ // Cookie is not active because we still hold value and it can be resurrected.
+ EXPECT_FALSE(cookie.IsActive());
+
+ // '0' is deleted, because it is too big.
+ EXPECT_EQ(cache->Find(0), nullptr);
+ EXPECT_EQ(cache->Find(1), value);
+ }
+}
+
+TEST(TAsyncSlruCacheTest, Touch)
+{
+ const int cacheSize = 2;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(config);
+
+ std::vector<TSimpleCachedValuePtr> values;
+
+ for (int i = 0; i < cacheSize; ++i) {
+ values.push_back(New<TSimpleCachedValue>(i, i));
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(values.back());
+ }
+
+ // Move v0 to touch buffer.
+ cache->Touch(values[0]);
+
+ values.push_back(New<TSimpleCachedValue>(cacheSize, cacheSize));
+ auto cookie = cache->BeginInsert(cacheSize);
+ EXPECT_TRUE(cookie.IsActive());
+ // Move v0 to older, evict v1 and insert v2.
+ cookie.EndInsert(values.back());
+
+ EXPECT_EQ(cache->Find(0), values[0]);
+ EXPECT_EQ(cache->Find(1), nullptr);
+ EXPECT_EQ(cache->Find(2), values[2]);
+}
+
+TEST(TAsyncSlruCacheTest, AddRemoveWithResurrection)
+{
+ constexpr int cacheSize = 2;
+ constexpr int valueCount = 10;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TCountingSlruCache>(std::move(config));
+
+ std::vector<TSimpleCachedValuePtr> values;
+ for (int i = 0; i < valueCount; ++i) {
+ values.push_back(New<TSimpleCachedValue>(i, i));
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(values.back());
+ EXPECT_EQ(cache->GetItemCount(), std::min(2, i + 1));
+ EXPECT_EQ(cache->GetItemCount(), cache->GetSize());
+ }
+
+ for (int iter = 0; iter < 5; ++iter) {
+ for (int i = 0; i < valueCount; ++i) {
+ auto value = cache->Lookup(i)
+ .Get()
+ .ValueOrThrow();
+ EXPECT_EQ(value->Value, i);
+ EXPECT_EQ(cache->GetItemCount(), 2);
+ EXPECT_EQ(cache->GetItemCount(), cache->GetSize());
+ }
+ for (int i = 0; i < valueCount; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(!cookie.IsActive());
+ auto value = cookie.GetValue()
+ .Get()
+ .ValueOrThrow();
+ EXPECT_EQ(value->Value, i);
+ EXPECT_EQ(cache->GetItemCount(), 2);
+ EXPECT_EQ(cache->GetItemCount(), cache->GetSize());
+ }
+ }
+}
+
+TEST(TAsyncSlruCacheTest, AddThenImmediatelyRemove)
+{
+ constexpr int cacheSize = 1;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TCountingSlruCache>(std::move(config));
+
+ auto persistentValue = New<TSimpleCachedValue>(
+ /* key */ 0,
+ /* value */ 42,
+ /* weight */ 100);
+
+ {
+ auto cookie = cache->BeginInsert(0);
+ cookie.EndInsert(persistentValue);
+ EXPECT_EQ(cache->GetItemCount(), 0);
+ EXPECT_EQ(cache->GetTotalAdded(), 1);
+ EXPECT_EQ(cache->GetTotalRemoved(), 1);
+ }
+
+ {
+ auto cookie = cache->BeginInsert(1);
+ auto temporaryValue = New<TSimpleCachedValue>(
+ /* key */ 1,
+ /* value */ 43,
+ /* weight */ 100);
+ cookie.EndInsert(temporaryValue);
+ temporaryValue.Reset();
+ EXPECT_EQ(cache->GetItemCount(), 0);
+ EXPECT_EQ(cache->GetTotalAdded(), 2);
+ EXPECT_EQ(cache->GetTotalRemoved(), 2);
+ }
+
+ {
+ auto value = cache->Lookup(0)
+ .Get()
+ .ValueOrThrow();
+ EXPECT_EQ(cache->GetItemCount(), 0);
+ EXPECT_EQ(value->Value, 42);
+ }
+
+ {
+ auto value = cache->Lookup(1);
+ EXPECT_EQ(cache->GetItemCount(), 0);
+ ASSERT_FALSE(static_cast<bool>(value));
+ }
+}
+
+TEST(TAsyncSlruCacheTest, TouchRemovedValue)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TCountingSlruCache>(std::move(config), /*enableResurrection*/ true);
+
+ auto value = New<TSimpleCachedValue>(
+ /*key*/ 1,
+ /*value*/ 1,
+ /*weight*/ 1);
+ {
+ auto insertCookie = cache->BeginInsert(value->GetKey());
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.EndInsert(value);
+ }
+ cache->TryRemove(value->GetKey());
+
+ cache->Touch(value);
+
+ auto value2 = New<TSimpleCachedValue>(
+ /*key*/ 2,
+ /*value*/ 2,
+ /*weight*/ 1);
+ {
+ auto insertCookie = cache->BeginInsert(value2->GetKey());
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.EndInsert(value2);
+ }
+ cache->TryRemove(value2->GetKey(), /*forbidResurrection*/ true);
+
+ cache->Touch(value2);
+
+ // Start and cancel insertion to forcefully drain touch buffer. If touch buffer
+ // contains already freed items due to bug, they will be put into the main linked
+ // list, and the bug won't be hidden. See also YT-15976.
+ {
+ auto insertCookie = cache->BeginInsert(3);
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.Cancel(TError("Cancelled"));
+ }
+}
+
+TEST(TAsyncSlruCacheTest, TouchEvictedValue)
+{
+ constexpr int cacheSize = 1;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TCountingSlruCache>(std::move(config));
+
+ auto value = New<TSimpleCachedValue>(
+ /*key*/ 1,
+ /*value*/ 1,
+ /*weight*/ 1);
+ {
+ auto insertCookie = cache->BeginInsert(value->GetKey());
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.EndInsert(value);
+ }
+
+ // Evict value.
+ auto value2 = New<TSimpleCachedValue>(
+ /*key*/ 2,
+ /*value*/ 2,
+ /*weight*/ 1);
+ {
+ auto insertCookie = cache->BeginInsert(value2->GetKey());
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.EndInsert(value2);
+ }
+
+ cache->Touch(value);
+
+ // Start and cancel insertion to forcefully drain touch buffer. If touch buffer
+ // contains already freed items due to bug, they will be put into the main linked
+ // list, and the bug won't be hidden. See also YT-15976.
+ {
+ auto insertCookie = cache->BeginInsert(3);
+ ASSERT_TRUE(insertCookie.IsActive());
+ insertCookie.Cancel(TError("Cancelled"));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Profiling is not supported on Windows for now.
+#ifdef _unix_
+
+TEST(TAsyncSlruGhostCacheTest, InsertSmall)
+{
+ constexpr int cacheSize = 10;
+ constexpr int numStages = 3;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ for (int stage = 0; stage < numStages; ++stage) {
+ for (int index = 0; index < cacheSize / 2; ++index) {
+ auto cookie = cache->BeginInsert(index);
+ if (!cookie.IsActive()) {
+ ASSERT_NE(stage, 0);
+ continue;
+ }
+ ASSERT_EQ(stage, 0);
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 1));
+ }
+ }
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, cacheSize / 2 * (numStages - 1));
+ EXPECT_EQ(smallCount.Missed, cacheSize / 2);
+ EXPECT_EQ(largeCount.SyncHit, cacheSize / 2 * (numStages - 1));
+ EXPECT_EQ(largeCount.Missed, cacheSize / 2);
+}
+
+TEST(TAsyncSlruGhostCacheTest, InsertLarge)
+{
+ constexpr int cacheSize = 10;
+ constexpr int numStages = 3;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ for (int stage = 0; stage < numStages; ++stage) {
+ for (int index = 0; index < 2 * cacheSize; ++index) {
+ auto cookie = cache->BeginInsert(index);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 1));
+ }
+ }
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 0);
+ EXPECT_EQ(smallCount.Missed, 2 * cacheSize * numStages);
+ EXPECT_EQ(largeCount.SyncHit, 2 * cacheSize * (numStages - 1));
+ EXPECT_EQ(largeCount.Missed, 2 * cacheSize);
+}
+
+TEST(TAsyncSlruGhostCacheTest, Weights)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ auto value = New<TSimpleCachedValue>(
+ /*key*/ 1,
+ /*value*/ 42,
+ /*weight*/ 64);
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ auto firstCookie = cache->BeginInsert(1);
+ ASSERT_TRUE(firstCookie.IsActive());
+
+ auto secondCookie = cache->BeginInsert(1);
+ ASSERT_TRUE(!secondCookie.IsActive());
+
+ firstCookie.EndInsert(value);
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 0);
+ EXPECT_EQ(smallCount.AsyncHit, 1);
+ EXPECT_EQ(smallCount.AsyncHitWeight, 64);
+ EXPECT_EQ(smallCount.Missed, 1);
+ EXPECT_EQ(smallCount.MissedWeight, 64);
+
+ EXPECT_EQ(largeCount.SyncHit, 0);
+ EXPECT_EQ(largeCount.AsyncHit, 1);
+ EXPECT_EQ(largeCount.AsyncHitWeight, 64);
+ EXPECT_EQ(largeCount.Missed, 1);
+ EXPECT_EQ(largeCount.MissedWeight, 64);
+ }
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ value->Weight = 90;
+ value->UpdateWeight();
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 0);
+ EXPECT_EQ(smallCount.AsyncHit, 0);
+ EXPECT_EQ(smallCount.AsyncHitWeight, 0);
+ EXPECT_EQ(smallCount.Missed, 0);
+ EXPECT_EQ(smallCount.MissedWeight, 0);
+
+ EXPECT_EQ(largeCount.SyncHit, 0);
+ EXPECT_EQ(largeCount.AsyncHit, 0);
+ EXPECT_EQ(largeCount.AsyncHitWeight, 0);
+ EXPECT_EQ(largeCount.Missed, 0);
+ EXPECT_EQ(largeCount.MissedWeight, 26);
+ }
+}
+
+TEST(TAsyncSlruGhostCacheTest, Lookups)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ for (int index = 0; index < 6; ++index) {
+ auto cookie = cache->BeginInsert(index);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 50));
+ }
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ for (int index = 0; index < 6; ++index) {
+ cache->Lookup(index);
+ }
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 1);
+ EXPECT_EQ(smallCount.SyncHitWeight, 50);
+ EXPECT_EQ(smallCount.Missed, 5);
+
+ EXPECT_EQ(largeCount.SyncHit, 4);
+ EXPECT_EQ(largeCount.SyncHitWeight, 200);
+ EXPECT_EQ(largeCount.Missed, 2);
+ }
+}
+
+TEST(TAsyncSlruGhostCacheTest, MoveConstructCookie)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ for (int index = 0; index < 5; ++index) {
+ auto originalCookie = cache->BeginInsert(index);
+ ASSERT_TRUE(originalCookie.IsActive());
+
+ auto newCookie = std::move(originalCookie);
+ ASSERT_FALSE(originalCookie.IsActive());
+ ASSERT_TRUE(newCookie.IsActive());
+
+ newCookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 1));
+ }
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ for (int index = 0; index < 5; ++index) {
+ cache->Lookup(index);
+ }
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 5);
+ EXPECT_EQ(smallCount.AsyncHit, 0);
+ EXPECT_EQ(smallCount.Missed, 0);
+
+ EXPECT_EQ(largeCount.SyncHit, 5);
+ EXPECT_EQ(largeCount.AsyncHit, 0);
+ EXPECT_EQ(largeCount.Missed, 0);
+ }
+}
+
+TEST(TAsyncSlruGhostCacheTest, MoveAssignCookie)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ // Ensure that all the necessary items are present in large ghost, but absent in main
+ // cache and small ghost.
+ for (int index = 0; index < 5; ++index) {
+ auto cookie = cache->BeginInsert(index);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 1));
+ }
+ {
+ auto cookie = cache->BeginInsert(43);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ 43,
+ /*value*/ 100500,
+ /*weight*/ 101));
+ }
+
+ for (int index = 0; index < 5; ++index) {
+ auto otherCookie = cache->BeginInsert(index);
+ ASSERT_TRUE(otherCookie.IsActive());
+
+ auto cookie = cache->BeginInsert(42);
+ ASSERT_TRUE(cookie.IsActive());
+
+ cookie = std::move(otherCookie);
+ ASSERT_FALSE(otherCookie.IsActive());
+ ASSERT_TRUE(cookie.IsActive());
+
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ index,
+ /*value*/ 42,
+ /*weight*/ 1));
+ }
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ for (int index = 0; index < 5; ++index) {
+ cache->Lookup(index);
+ }
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 5);
+ EXPECT_EQ(smallCount.AsyncHit, 0);
+ EXPECT_EQ(smallCount.Missed, 0);
+
+ EXPECT_EQ(largeCount.SyncHit, 5);
+ EXPECT_EQ(largeCount.AsyncHit, 0);
+ EXPECT_EQ(largeCount.Missed, 0);
+ }
+}
+
+TEST(TAsyncSlruGhostCacheTest, Disable)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ auto cookie = cache->BeginInsert(1);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ 1,
+ /*value*/ 42,
+ /*weight*/ 1));
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 0);
+ EXPECT_EQ(smallCount.AsyncHit, 0);
+ EXPECT_EQ(smallCount.Missed, 1);
+
+ EXPECT_EQ(largeCount.SyncHit, 0);
+ EXPECT_EQ(largeCount.AsyncHit, 0);
+ EXPECT_EQ(largeCount.Missed, 1);
+ }
+
+ auto dynamicConfig = New<TSlruCacheDynamicConfig>();
+ dynamicConfig->EnableGhostCaches = false;
+ cache->Reconfigure(dynamicConfig);
+
+ {
+ auto oldSmallCounters = cache->ReadSmallGhostCounters();
+ auto oldLargeCounters = cache->ReadLargeGhostCounters();
+
+ auto cookie = cache->BeginInsert(2);
+ ASSERT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(
+ /*key*/ 2,
+ /*value*/ 57,
+ /*weight*/ 1));
+
+ auto value1 = cache->Find(1);
+ ASSERT_NE(value1, nullptr);
+ ASSERT_EQ(value1->Value, 42);
+
+ auto value2 = cache->Lookup(2);
+ ASSERT_TRUE(value2.IsSet());
+ ASSERT_TRUE(value2.Get().IsOK());
+ ASSERT_EQ(value2.Get().Value()->Value, 57);
+
+ auto smallCount = cache->ReadSmallGhostCounters() - oldSmallCounters;
+ auto largeCount = cache->ReadLargeGhostCounters() - oldLargeCounters;
+
+ EXPECT_EQ(smallCount.SyncHit, 0);
+ EXPECT_EQ(smallCount.AsyncHit, 0);
+ EXPECT_EQ(smallCount.Missed, 0);
+
+ EXPECT_EQ(largeCount.SyncHit, 0);
+ EXPECT_EQ(largeCount.AsyncHit, 0);
+ EXPECT_EQ(largeCount.Missed, 0);
+ }
+}
+
+TEST(TAsyncSlruGhostCacheTest, ReconfigureTrim)
+{
+ constexpr int cacheSize = 100;
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TSimpleSlruCache>(std::move(config), TProfiler{"/cache"});
+
+ for (int i = 0; i < cacheSize; ++i) {
+ auto cookie = cache->BeginInsert(i);
+ EXPECT_TRUE(cookie.IsActive());
+ cookie.EndInsert(New<TSimpleCachedValue>(i, i));
+ }
+
+ EXPECT_EQ(cacheSize, cache->GetSize());
+ EXPECT_EQ(cacheSize, cache->GetCapacity());
+
+ const int newCacheSize = 30;
+ auto dynamicConfig = New<TSlruCacheDynamicConfig>();
+ dynamicConfig->Capacity = newCacheSize;
+ cache->Reconfigure(dynamicConfig);
+
+ EXPECT_EQ(newCacheSize, cache->GetSize());
+ EXPECT_EQ(newCacheSize, cache->GetCapacity());
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EStressOperation,
+ ((Find) (0))
+ ((Lookup) (1))
+ ((Touch) (2))
+ ((BeginInsert) (3))
+ ((CancelInsert) (4))
+ ((EndInsert) (5))
+ ((TryRemove) (6))
+ ((UpdateWeight) (7))
+ ((ReleaseValue) (8))
+ ((Reconfigure) (9))
+);
+
+class TAsyncSlruCacheStressTest
+ : public ::testing::TestWithParam<bool>
+{ };
+
+TEST_P(TAsyncSlruCacheStressTest, Stress)
+{
+ constexpr int cacheSize = 100;
+ constexpr int stepCount = 1'000'000;
+ constexpr double forbidResurrectionProbability = 0.25;
+
+ const bool enableResurrection = GetParam();
+
+ auto config = CreateCacheConfig(cacheSize);
+ auto cache = New<TCountingSlruCache>(std::move(config), enableResurrection);
+
+ // Use a fixed-seed random generator for deterministic testing.
+ std::mt19937 randomGenerator(142857);
+
+ auto operationDomainValues = TEnumTraits<EStressOperation>::GetDomainValues();
+ std::vector<EStressOperation> operations(operationDomainValues.begin(), operationDomainValues.end());
+ if (!enableResurrection) {
+ operations.erase(
+ std::find(operations.begin(), operations.end(), EStressOperation::ReleaseValue));
+ }
+
+ std::uniform_int_distribution<int> weightDistribution(1, 10);
+ std::uniform_int_distribution<int> keyDistribution(1, 20);
+ std::uniform_int_distribution<int> capacityDistribution(50, 150);
+ std::uniform_real_distribution<double> youngerSizeFractionDistribution(0.0, 1.0);
+
+ std::vector<TCountingSlruCache::TInsertCookie> activeInsertCookies;
+
+ // Pointers to all the values that are either present in cache now or were in cache
+ // earlier. We hold weak pointers, allowing the values to be deleted to prevent their
+ // resurrection.
+ std::vector<TWeakPtr<TSimpleCachedValue>> cacheValues;
+
+ // Holds references to some of the values. This is needed to allow resurrection. Used
+ // only if enableResurrection is true.
+ std::vector<TSimpleCachedValuePtr> heldValues;
+
+ // For each key, stores the last inserted value with key. Can be null if we are sure
+ // that there is no value with the given key in the cache.
+ THashMap<int, TWeakPtr<TSimpleCachedValue>> lastInsertedValues;
+
+ auto pickCacheValue = [&] () -> TSimpleCachedValuePtr {
+ while (!cacheValues.empty()) {
+ size_t cacheValueIndex = randomGenerator() % cacheValues.size();
+ std::swap(cacheValues[cacheValueIndex], cacheValues.back());
+ auto value = cacheValues.back().Lock();
+ if (value) {
+ return value;
+ }
+ cacheValues.pop_back();
+ }
+ return nullptr;
+ };
+
+ for (int step = 0; step < stepCount; ++step) {
+ auto operation = operations[randomGenerator() % operations.size()];
+
+ switch (operation) {
+ case EStressOperation::Find: {
+ auto value = cache->Find(keyDistribution(randomGenerator));
+ if (value) {
+ ASSERT_EQ(lastInsertedValues[value->GetKey()].Lock(), value);
+ }
+ break;
+ }
+ case EStressOperation::Lookup: {
+ auto key = keyDistribution(randomGenerator);
+ auto valueFuture = cache->Lookup(key);
+ if (!valueFuture) {
+ break;
+ }
+ if (valueFuture.IsSet()) {
+ ASSERT_TRUE(valueFuture.Get().IsOK());
+ const auto& value = valueFuture.Get().Value();
+ ASSERT_EQ(lastInsertedValues[key].Lock(), value);
+ } else {
+ // The value insertion is in progress, so lastInsertedValues must contain nullptr
+ // for our key.
+ ASSERT_EQ(lastInsertedValues[key].Lock(), nullptr);
+ }
+ break;
+ }
+ case EStressOperation::Touch: {
+ auto value = pickCacheValue();
+ if (!value) {
+ break;
+ }
+ cache->Touch(value);
+ break;
+ }
+ case EStressOperation::BeginInsert: {
+ int key = keyDistribution(randomGenerator);
+ auto cookie = cache->BeginInsert(key);
+ if (cookie.IsActive()) {
+ activeInsertCookies.emplace_back(std::move(cookie));
+ lastInsertedValues[key] = nullptr;
+ } else {
+ auto valueFuture = cookie.GetValue();
+ ASSERT_TRUE(static_cast<bool>(valueFuture));
+ if (valueFuture.IsSet()) {
+ ASSERT_TRUE(valueFuture.Get().IsOK());
+ const auto& value = valueFuture.Get().Value();
+ ASSERT_EQ(lastInsertedValues[value->GetKey()].Lock(), value);
+ } else {
+ // The value insertion is in progress, so lastInsertedValues must contain nullptr
+ // for our key.
+ ASSERT_EQ(lastInsertedValues[key].Lock(), nullptr);
+ }
+ }
+ break;
+ }
+ case EStressOperation::EndInsert: {
+ if (activeInsertCookies.empty()) {
+ break;
+ }
+ size_t cookieIndex = randomGenerator() % activeInsertCookies.size();
+ std::swap(activeInsertCookies[cookieIndex], activeInsertCookies.back());
+ auto value = New<TSimpleCachedValue>(
+ /*key*/ activeInsertCookies.back().GetKey(),
+ /*value*/ step,
+ /*weight*/ weightDistribution(randomGenerator));
+ cacheValues.emplace_back(value);
+ if (enableResurrection) {
+ heldValues.push_back(value);
+ }
+ lastInsertedValues[value->GetKey()] = value;
+ ASSERT_TRUE(activeInsertCookies.back().IsActive());
+ activeInsertCookies.back().EndInsert(std::move(value));
+ activeInsertCookies.pop_back();
+ break;
+ }
+ case EStressOperation::CancelInsert: {
+ if (activeInsertCookies.empty()) {
+ break;
+ }
+ size_t cookieIndex = randomGenerator() % activeInsertCookies.size();
+ std::swap(activeInsertCookies[cookieIndex], activeInsertCookies.back());
+ ASSERT_TRUE(activeInsertCookies.back().IsActive());
+ activeInsertCookies.back().Cancel(TError("Cancelled"));
+ activeInsertCookies.pop_back();
+ break;
+ }
+ case EStressOperation::TryRemove: {
+ std::bernoulli_distribution distribution(forbidResurrectionProbability);
+ bool forbidResurrection = distribution(randomGenerator);
+ auto key = keyDistribution(randomGenerator);
+ cache->TryRemove(key, forbidResurrection);
+ if (!enableResurrection || forbidResurrection) {
+ lastInsertedValues[key] = nullptr;
+ }
+ break;
+ }
+ case EStressOperation::UpdateWeight: {
+ auto value = pickCacheValue();
+ if (!value) {
+ break;
+ }
+ value->Weight = weightDistribution(randomGenerator);
+ value->UpdateWeight();
+ break;
+ }
+ case EStressOperation::ReleaseValue: {
+ if (heldValues.empty()) {
+ break;
+ }
+ size_t valueIndex = randomGenerator() % heldValues.size();
+ std::swap(heldValues[valueIndex], heldValues.back());
+ heldValues.pop_back();
+ break;
+ }
+ case EStressOperation::Reconfigure: {
+ auto config = New<TSlruCacheDynamicConfig>();
+ config->Capacity = capacityDistribution(randomGenerator);
+ config->YoungerSizeFraction = youngerSizeFractionDistribution(randomGenerator);
+ cache->Reconfigure(std::move(config));
+ break;
+ }
+ }
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(Stress, TAsyncSlruCacheStressTest, ::testing::Values(false, true));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/atomic_ptr_ut.cpp b/yt/yt/core/misc/unittests/atomic_ptr_ut.cpp
new file mode 100644
index 0000000000..e75610ea05
--- /dev/null
+++ b/yt/yt/core/misc/unittests/atomic_ptr_ut.cpp
@@ -0,0 +1,83 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/atomic_ptr.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TObject>
+class TAtomicPtrTest
+ : public ::testing::Test
+{ };
+
+struct TFinalObject final
+{
+ static constexpr bool EnableHazard = true;
+};
+
+struct TRefCountedObject
+ : public TRefCounted
+{
+ static constexpr bool EnableHazard = true;
+};
+
+struct TVirtualRefCountedObject
+ : public virtual TRefCounted
+{
+ static constexpr bool EnableHazard = true;
+};
+
+using TObjectTypes = ::testing::Types<
+ TFinalObject,
+ TRefCountedObject,
+ TVirtualRefCountedObject
+>;
+
+TYPED_TEST_SUITE(TAtomicPtrTest, TObjectTypes);
+
+TYPED_TEST(TAtomicPtrTest, Simple)
+{
+ using TObject = TypeParam;
+
+ TAtomicPtr<TObject> atomicPtr;
+ EXPECT_FALSE(atomicPtr.Acquire());
+
+ auto obj1 = New<TObject>();
+ atomicPtr.Store(obj1);
+
+ auto obj2 = atomicPtr.Acquire();
+ EXPECT_EQ(obj1, obj2);
+
+ atomicPtr.Reset();
+ EXPECT_FALSE(atomicPtr.Acquire());
+}
+
+TEST(TAtomicPtrTest, AcquireHazard)
+{
+ TAtomicPtr<TRefCountedObject, /*EnableAcquireHazard*/ true> atomicPtr;
+
+ EXPECT_FALSE(atomicPtr.Acquire());
+
+ auto obj = New<TRefCountedObject>();
+ atomicPtr.Store(obj);
+
+ auto hazardObj = atomicPtr.AcquireHazard();
+ EXPECT_EQ(hazardObj.Get(), obj.Get());
+ EXPECT_EQ(obj->GetRefCount(), 2);
+
+ atomicPtr.Reset();
+ ReclaimHazardPointers();
+
+ EXPECT_EQ(obj->GetRefCount(), 2);
+
+ hazardObj.Reset();
+ ReclaimHazardPointers();
+
+ EXPECT_EQ(obj->GetRefCount(), 1);
+}
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/bind_ut.cpp b/yt/yt/core/misc/unittests/bind_ut.cpp
new file mode 100644
index 0000000000..a44dd05f0c
--- /dev/null
+++ b/yt/yt/core/misc/unittests/bind_ut.cpp
@@ -0,0 +1,1158 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT {
+namespace {
+
+using ::testing::Mock;
+using ::testing::Return;
+using ::testing::AllOf;
+using ::testing::StrictMock;
+using ::testing::TProbe;
+using ::testing::TProbeState;
+using ::testing::TCoercibleToProbe;
+using ::testing::HasCopyMoveCounts;
+using ::testing::NoCopies;
+using ::testing::NoAssignments;
+
+////////////////////////////////////////////////////////////////////////////////
+// Auxiliary types and functions.
+
+// An incomplete type (really).
+class TIncompleteType;
+
+// A simple mock.
+class TObject
+{
+public:
+ TObject() = default;
+
+ MOCK_METHOD(void, VoidMethod0, (), ());
+ MOCK_METHOD(void, VoidConstMethod0, (), (const));
+
+ MOCK_METHOD(int, IntMethod0, (), ());
+ MOCK_METHOD(int, IntConstMethod0, (), (const));
+
+private:
+ // Explicitly non-copyable and non-movable.
+ // Particularly important in this test to ensure that no copies are made.
+ TObject(const TObject&);
+ TObject(TObject&&);
+ TObject& operator=(const TObject&);
+ TObject& operator=(TObject&&);
+};
+
+// A simple mock which also mocks Ref()/Unref() hence mocking reference counting
+// behaviour.
+class TObjectWithRC
+ : public TObject
+{
+public:
+ TObjectWithRC() = default;
+
+ MOCK_METHOD(void, Ref, (), (const));
+ MOCK_METHOD(void, Unref, (), (const));
+
+private:
+ // Explicitly non-copyable and non-movable.
+ // Particularly important in this test to ensure that no copies are made.
+ TObjectWithRC(const TObjectWithRC&);
+ TObjectWithRC(TObjectWithRC&&);
+ TObjectWithRC& operator=(const TObjectWithRC&);
+ TObjectWithRC& operator=(TObjectWithRC&&);
+};
+
+void Ref(TObjectWithRC* obj)
+{
+ obj->Ref();
+}
+
+void Unref(TObjectWithRC* obj)
+{
+ obj->Unref();
+}
+
+using TObjectWithRCPtr = TIntrusivePtr<TObjectWithRC>;
+
+// A simple mock object which mocks Ref()/Unref() and prohibits
+// public destruction.
+class TObjectWithRCAndPrivateDtor
+ : public TObjectWithRC
+{
+private:
+ ~TObjectWithRCAndPrivateDtor() = default;
+};
+
+// A simple mock object with weak ptr support.
+class TObjectWithFullRC
+ : public TObject
+ , public TRefCounted
+{ };
+
+using TObjectWithExtrinsicRCPtr = TIntrusivePtr<TObjectWithFullRC>;
+using TObjectWithExtrinsicRCConstPtr = TIntrusivePtr<const TObjectWithFullRC>;
+using TObjectWithExtrinsicRCWkPtr = TWeakPtr<TObjectWithFullRC>;
+using TObjectWithExtrinsicRCConstWkPtr = TWeakPtr<const TObjectWithFullRC>;
+
+// Below there is a series of either reference-counted or not classes
+// with simple inheritance and both virtual and non-virtual methods.
+
+static const int SomeParentValue = 1;
+static const int SomeChildValue = 2;
+
+class TRefParent
+{
+public:
+ // Stub methods for reference counting.
+ void Ref()
+ { }
+
+ void Unref()
+ { }
+
+ virtual void VirtualSet()
+ {
+ Value = SomeParentValue;
+ }
+
+ void NonVirtualSet()
+ {
+ Value = SomeParentValue;
+ }
+
+ int Value;
+};
+
+class TRefChild
+ : public TRefParent
+{
+public:
+ void VirtualSet() override
+ {
+ Value = SomeChildValue;
+ }
+
+ void NonVirtualSet()
+ {
+ Value = SomeChildValue;
+ }
+};
+
+class TNoRefParent
+{
+public:
+ virtual void VirtualSet()
+ {
+ Value = SomeParentValue;
+ }
+
+ void NonVirtualSet()
+ {
+ Value = SomeParentValue;
+ }
+
+ int Value;
+};
+
+class NoRefChild
+ : public TNoRefParent
+{
+ void VirtualSet() override
+ {
+ Value = SomeChildValue;
+ }
+
+ void NonVirtualSet()
+ {
+ Value = SomeChildValue;
+ }
+};
+
+int UnwrapNoRefParent(TNoRefParent p)
+{
+ return p.Value;
+}
+
+int UnwrapNoRefParentPtr(TNoRefParent* p)
+{
+ return p->Value;
+}
+
+int UnwrapNoRefParentConstRef(const TNoRefParent& p)
+{
+ return p.Value;
+}
+
+// Various functions for testing purposes.
+
+int IntegerIdentity(int n)
+{
+ return n;
+}
+
+const char* StringIdentity(const char* s)
+{
+ return s;
+}
+
+template <class T>
+T PolymorphicIdentity(T t)
+{
+ return t; // Copy
+}
+
+template <class T>
+T PolymorphicPassThrough(T&& t)
+{
+ return std::move(t); // Move
+}
+
+template <class T>
+void VoidPolymorphic1(T t)
+{
+ Y_UNUSED(t);
+}
+
+int ArrayGet(const int array[], int n)
+{
+ return array[n];
+}
+
+int Sum(int a, int b, int c, int d, int e, int f)
+{
+ // Sum(1, 2, 3, 4, 5, 6) -> 123456.
+ return f + 10 * (e + 10 * (d + 10 * (c + 10 * (b + 10 * a))));
+}
+
+void SetIntViaRef(int& n)
+{
+ n = 2012;
+}
+
+void SetIntViaPtr(int* n)
+{
+ *n = 2012;
+}
+
+template <class T>
+int FunctionWithWeakParam(TWeakPtr<T> /*ptr*/, int n)
+{
+ return n;
+}
+
+void InvokeClosure(const TClosure& callback)
+{
+ callback();
+}
+
+void Touch(const TProbe& probe) {
+ probe.Touch();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test fixture.
+
+class TBindTest
+ : public ::testing::Test
+{
+public:
+ TBindTest() = default;
+
+ void SetUp() override
+ {
+ ConstObjectWithRCPtr = &ObjectWithRC;
+ ConstObjectPtr = &Object;
+ StaticObjectPtr = &StaticObject;
+ }
+
+ // Helper methods.
+ static void StaticVoidFunc0()
+ {
+ StaticObjectPtr->VoidMethod0();
+ }
+
+ static int StaticIntFunc0()
+ {
+ return StaticObjectPtr->IntMethod0();
+ }
+
+protected:
+ StrictMock<TObject> Object;
+ StrictMock<TObjectWithRC> ObjectWithRC;
+
+ const TObjectWithRC* ConstObjectWithRCPtr;
+ const TObject* ConstObjectPtr;
+
+ StrictMock<TObject> StaticObject;
+
+ // Used by the static functions.
+ static StrictMock<TObject>* StaticObjectPtr;
+
+private:
+ // Explicitly non-copyable and non-movable.
+ // Thus we prevent BIND() from taking copy of the target (i. e. this fixture).
+ TBindTest(const TBindTest&);
+ TBindTest(TBindTest&&);
+ TBindTest& operator=(const TBindTest&);
+ TBindTest& operator=(TBindTest&&);
+};
+
+StrictMock<TObject>* TBindTest::StaticObjectPtr = 0;
+
+////////////////////////////////////////////////////////////////////////////////
+// Test definitions.
+
+// Sanity check that we can instantiate a callback for each arity.
+TEST_F(TBindTest, ArityTest)
+{
+ TCallback<int()> c0 = BIND(&Sum, 5, 4, 3, 2, 1, 0);
+ EXPECT_EQ(543210, c0());
+
+ TCallback<int(int)> c1 = BIND(&Sum, 5, 4, 3, 2, 1);
+ EXPECT_EQ(543219, c1(9));
+
+ TCallback<int(int,int)> c2 = BIND(&Sum, 5, 4, 3, 2);
+ EXPECT_EQ(543298, c2(9, 8));
+
+ TCallback<int(int,int,int)> c3 = BIND(&Sum, 5, 4, 3);
+ EXPECT_EQ(543987, c3(9, 8, 7));
+
+ TCallback<int(int,int,int,int)> c4 = BIND(&Sum, 5, 4);
+ EXPECT_EQ(549876, c4(9, 8, 7, 6));
+
+ TCallback<int(int,int,int,int,int)> c5 = BIND(&Sum, 5);
+ EXPECT_EQ(598765, c5(9, 8, 7, 6, 5));
+
+ TCallback<int(int,int,int,int,int,int)> c6 = BIND(&Sum);
+ EXPECT_EQ(987654, c6(9, 8, 7, 6, 5, 4));
+}
+
+// Test the currying ability of the BIND().
+TEST_F(TBindTest, CurryingTest)
+{
+ TCallback<int(int,int,int,int,int,int)> c6 = BIND(&Sum);
+ EXPECT_EQ(987654, c6(9, 8, 7, 6, 5, 4));
+
+ TCallback<int(int,int,int,int,int)> c5 = BIND(c6, 5);
+ EXPECT_EQ(598765, c5(9, 8, 7, 6, 5));
+
+ TCallback<int(int,int,int,int)> c4 = BIND(c5, 4);
+ EXPECT_EQ(549876, c4(9, 8, 7, 6));
+
+ TCallback<int(int,int,int)> c3 = BIND(c4, 3);
+ EXPECT_EQ(543987, c3(9, 8, 7));
+
+ TCallback<int(int,int)> c2 = BIND(c3, 2);
+ EXPECT_EQ(543298, c2(9, 8));
+
+ TCallback<int(int)> c1 = BIND(c2, 1);
+ EXPECT_EQ(543219, c1(9));
+
+ TCallback<int()> c0 = BIND(c1, 0);
+ EXPECT_EQ(543210, c0());
+}
+
+// Test that currying the rvalue result of another BIND() works correctly.
+// - Rvalue should be usable as an argument to the BIND().
+// - Multiple runs of resulting TCallback remain valid.
+TEST_F(TBindTest, CurryingRvalueResultOfBind)
+{
+ int x;
+ TClosure cb = BIND(&InvokeClosure, BIND(&SetIntViaPtr, &x));
+
+ // If we implement BIND() such that the return value has auto_ptr-like
+ // semantics, the second call here will fail because ownership of
+ // the internal BindState<> would have been transferred to a *temporary*
+ // constructon of a TCallback object on the first call.
+ x = 0;
+ cb();
+ EXPECT_EQ(2012, x);
+
+ x = 0;
+ cb();
+ EXPECT_EQ(2012, x);
+}
+
+// Now we have to test that there are proper instantinations for various use cases.
+// The following test cases try to cover most of the used cases.
+
+// Function type support.
+// - Normal function.
+// - Normal function bound with a non-refcounted first argument.
+// - Method bound to an object via raw pointer.
+// - Method bound to an object via intrusive pointer.
+// - Const method bound to a non-const object.
+// - Const method bound to a const object.
+// - Derived classes can be used with pointers to non-virtual base functions.
+// - Derived classes can be used with pointers to virtual base functions
+// (preserves virtual dispatch).
+TEST_F(TBindTest, FunctionTypeSupport)
+{
+ EXPECT_CALL(StaticObject, VoidMethod0());
+
+ EXPECT_CALL(ObjectWithRC, Ref()).Times(1);
+ EXPECT_CALL(ObjectWithRC, Unref()).Times(1);
+
+ EXPECT_CALL(ObjectWithRC, VoidMethod0()).Times(2);
+ EXPECT_CALL(ObjectWithRC, VoidConstMethod0()).Times(2);
+
+ // Normal functions.
+ TClosure normalFunc =
+ BIND(&StaticVoidFunc0);
+ TCallback<TObject*()> normalFuncNonRC =
+ BIND(&PolymorphicIdentity<TObject*>, &Object);
+
+ normalFunc();
+ EXPECT_EQ(&Object, normalFuncNonRC());
+
+ // Bound methods.
+ TClosure boundMethodViaRawPtr =
+ BIND(&TObjectWithRC::VoidMethod0, &ObjectWithRC); // (NoRef)
+ TClosure boundMethodViaRefPtr =
+ BIND(&TObjectWithRC::VoidMethod0, TObjectWithRCPtr(&ObjectWithRC)); // (Ref)
+
+ boundMethodViaRawPtr();
+ boundMethodViaRefPtr();
+
+ // Const-methods.
+ TClosure constMethodNonConstObject =
+ BIND(&TObjectWithRC::VoidConstMethod0, &ObjectWithRC); // (NoRef)
+ TClosure constMethodConstObject =
+ BIND(&TObjectWithRC::VoidConstMethod0, ConstObjectWithRCPtr); // (NoRef)
+
+ constMethodNonConstObject();
+ constMethodConstObject();
+
+ // Virtual calls.
+ TRefChild child;
+
+ child.Value = 0;
+ TClosure virtualSet = BIND(&TRefParent::VirtualSet, &child);
+ virtualSet();
+ EXPECT_EQ(SomeChildValue, child.Value);
+
+ child.Value = 0;
+ TClosure nonVirtualSet = BIND(&TRefParent::NonVirtualSet, &child);
+ nonVirtualSet();
+ EXPECT_EQ(SomeParentValue, child.Value);
+}
+
+// Return value support.
+// - Function with a return value.
+// - Method with a return value.
+// - Const method with a return value.
+TEST_F(TBindTest, ReturnValuesSupport)
+{
+ EXPECT_CALL(StaticObject, IntMethod0()).WillOnce(Return(13));
+
+ EXPECT_CALL(ObjectWithRC, Ref()).Times(0);
+ EXPECT_CALL(ObjectWithRC, Unref()).Times(0);
+
+ EXPECT_CALL(ObjectWithRC, IntMethod0()).WillOnce(Return(17));
+ EXPECT_CALL(ObjectWithRC, IntConstMethod0())
+ .WillOnce(Return(19))
+ .WillOnce(Return(23));
+
+ TCallback<int()> normalFunc =
+ BIND(&StaticIntFunc0);
+ TCallback<int()> boundMethod =
+ BIND(&TObjectWithRC::IntMethod0, &ObjectWithRC); // (NoRef)
+
+ EXPECT_EQ(13, normalFunc());
+ EXPECT_EQ(17, boundMethod());
+
+ TCallback<int()> constMethodNonConstObject =
+ BIND(&TObjectWithRC::IntConstMethod0, &ObjectWithRC); // (NoRef)
+ TCallback<int()> constMethodConstObject =
+ BIND(&TObjectWithRC::IntConstMethod0, ConstObjectWithRCPtr); // (NoRef)
+
+ EXPECT_EQ(19, constMethodNonConstObject());
+ EXPECT_EQ(23, constMethodConstObject());
+}
+
+// An ability to ignore returned value.
+// - Function with a return value.
+// - Method with a return value.
+// - Const Method with a return value.
+// - Method with a return value bound to a weak pointer.
+// - Const Method with a return value bound to a weak pointer.
+TEST_F(TBindTest, IgnoreResultWrapper)
+{
+ EXPECT_CALL(StaticObject, IntMethod0()).WillOnce(Return(13));
+
+ EXPECT_CALL(ObjectWithRC, Ref()).Times(0);
+ EXPECT_CALL(ObjectWithRC, Unref()).Times(0);
+
+ EXPECT_CALL(ObjectWithRC, IntMethod0()).WillOnce(Return(17));
+ EXPECT_CALL(ObjectWithRC, IntConstMethod0()).WillOnce(Return(19));
+
+ TClosure normalFunc =
+ BIND(IgnoreResult(&StaticIntFunc0));
+ normalFunc();
+
+ TClosure boundMethod =
+ BIND(IgnoreResult(&TObjectWithRC::IntMethod0), &ObjectWithRC); // (NoRef)
+ boundMethod();
+
+ TClosure constBoundMethod =
+ BIND(IgnoreResult(&TObjectWithRC::IntConstMethod0), &ObjectWithRC); // (NoRef)
+ constBoundMethod();
+}
+
+// Argument binding tests.
+// - Argument binding to a primitive.
+// - Argument binding to a primitive pointer.
+// - Argument binding to a literal integer.
+// - Argument binding to a literal string.
+// - Argument binding with template function.
+// - Argument binding to an object.
+// - Argument binding to a pointer to an incomplete type.
+// - Argument upcasts when required.
+TEST_F(TBindTest, ArgumentBindingSupport)
+{
+ int n = 1;
+
+ TCallback<int()> primitiveBind =
+ BIND(&IntegerIdentity, n);
+ EXPECT_EQ(n, primitiveBind());
+
+ TCallback<int*()> primitivePointerBind =
+ BIND(&PolymorphicIdentity<int*>, &n);
+ EXPECT_EQ(&n, primitivePointerBind());
+
+ TCallback<int()> literalIntegerBind
+ = BIND(&IntegerIdentity, 2);
+ EXPECT_EQ(2, literalIntegerBind());
+
+ TCallback<const char*()> literalStringBind =
+ BIND(&StringIdentity, "Dire Straits");
+ EXPECT_STREQ("Dire Straits", literalStringBind());
+
+ TCallback<int()> templateFunctionBind =
+ BIND(&PolymorphicIdentity<int>, 3);
+ EXPECT_EQ(3, templateFunctionBind());
+
+ TNoRefParent p;
+ p.Value = 4;
+
+ TCallback<int()> objectBind = BIND(&UnwrapNoRefParent, p);
+ EXPECT_EQ(4, objectBind());
+
+ TIncompleteType* dummyPtr = reinterpret_cast<TIncompleteType*>(123);
+ TCallback<TIncompleteType*()> incompleteTypeBind =
+ BIND(&PolymorphicIdentity<TIncompleteType*>, dummyPtr);
+ EXPECT_EQ(dummyPtr, incompleteTypeBind());
+
+ NoRefChild c;
+
+ c.Value = 5;
+ TCallback<int()> upcastBind =
+ BIND(&UnwrapNoRefParent, c);
+ EXPECT_EQ(5, upcastBind());
+
+ c.Value = 6;
+ TCallback<int()> upcastPtrBind =
+ BIND(&UnwrapNoRefParentPtr, &c);
+ EXPECT_EQ(6, upcastPtrBind());
+
+ c.Value = 7;
+ TCallback<int()> upcastConstRefBind =
+ BIND(&UnwrapNoRefParentConstRef, c);
+ EXPECT_EQ(7, upcastConstRefBind());
+}
+
+// Unbound argument type support tests.
+// - Unbound value.
+// - Unbound pointer.
+// - Unbound reference.
+// - Unbound const reference.
+// - Unbound unsized array.
+// - Unbound sized array.
+// - Unbound array-of-arrays.
+TEST_F(TBindTest, UnboundArgumentTypeSupport)
+{
+ // Check only for valid instatination.
+ TCallback<void(int)> unboundValue =
+ BIND(&VoidPolymorphic1<int>);
+ TCallback<void(int*)> unboundPtr =
+ BIND(&VoidPolymorphic1<int*>);
+ TCallback<void(int&)> unboundRef =
+ BIND(&VoidPolymorphic1<int&>);
+ TCallback<void(const int&)> unboundConstRef =
+ BIND(&VoidPolymorphic1<const int&>);
+ TCallback<void(int[])> unboundUnsizedArray =
+ BIND(&VoidPolymorphic1<int[]>);
+ TCallback<void(int[3])> unboundSizedArray =
+ BIND(&VoidPolymorphic1<int[3]>);
+ TCallback<void(int[][3])> unboundArrayOfArrays =
+ BIND(&VoidPolymorphic1<int[][3]>);
+
+ SUCCEED();
+}
+
+// Function with unbound reference parameter.
+// - Original parameter is modified by the callback.
+TEST_F(TBindTest, UnboundReference)
+{
+ int n = 0;
+ TCallback<void(int&)> unboundRef = BIND(&SetIntViaRef);
+ unboundRef(n);
+ EXPECT_EQ(2012, n);
+}
+
+// Functions that take reference parameters.
+// - Forced reference parameter type still stores a copy.
+// - Forced const reference parameter type still stores a copy.
+TEST_F(TBindTest, ReferenceArgumentBinding)
+{
+ int myInt = 1;
+ int& myIntRef = myInt;
+ const int& myIntConstRef = myInt;
+
+ TCallback<int()> firstAction =
+ BIND(&IntegerIdentity, myIntRef);
+ EXPECT_EQ(1, firstAction());
+ myInt++;
+ EXPECT_EQ(1, firstAction());
+
+ TCallback<int()> secondAction =
+ BIND(&IntegerIdentity, myIntConstRef);
+ EXPECT_EQ(2, secondAction());
+ myInt++;
+ EXPECT_EQ(2, secondAction());
+
+ EXPECT_EQ(3, myInt);
+}
+
+// Check that we can pass in arrays and have them be stored as a pointer.
+// - Array of values stores a pointer.
+// - Array of const values stores a pointer.
+TEST_F(TBindTest, ArrayArgumentBinding)
+{
+ int array[4] = { 1, 2, 3, 4 };
+ const int (*constArrayPtr)[4] = &array;
+
+ TCallback<int(int)> arrayPolyGet = BIND(&ArrayGet, array);
+ EXPECT_EQ(1, arrayPolyGet(0));
+ EXPECT_EQ(2, arrayPolyGet(1));
+ EXPECT_EQ(3, arrayPolyGet(2));
+ EXPECT_EQ(4, arrayPolyGet(3));
+
+ TCallback<int()> arrayGet = BIND(&ArrayGet, array, 1);
+ EXPECT_EQ(2, arrayGet());
+
+ TCallback<int()> constArrayGet = BIND(&ArrayGet, *constArrayPtr, 1);
+ EXPECT_EQ(2, constArrayGet());
+
+ array[1] = 7;
+ EXPECT_EQ(7, arrayGet());
+ EXPECT_EQ(7, constArrayGet());
+}
+
+// Unretained() wrapper support.
+// - Method bound to Unretained() non-const object.
+// - Const method bound to Unretained() non-const object.
+// - Const method bound to Unretained() const object.
+TEST_F(TBindTest, UnretainedWrapper)
+{
+ EXPECT_CALL(Object, VoidMethod0()).Times(1);
+ EXPECT_CALL(Object, VoidConstMethod0()).Times(2);
+
+ EXPECT_CALL(ObjectWithRC, Ref()).Times(0);
+ EXPECT_CALL(ObjectWithRC, Unref()).Times(0);
+ EXPECT_CALL(ObjectWithRC, VoidMethod0()).Times(1);
+ EXPECT_CALL(ObjectWithRC, VoidConstMethod0()).Times(2);
+
+ TCallback<void()> boundMethod =
+ BIND(&TObject::VoidMethod0, Unretained(&Object));
+ boundMethod();
+
+ TCallback<void()> constMethodNonConstObject =
+ BIND(&TObject::VoidConstMethod0, Unretained(&Object));
+ constMethodNonConstObject();
+
+ TCallback<void()> constMethodConstObject =
+ BIND(&TObject::VoidConstMethod0, Unretained(ConstObjectPtr));
+ constMethodConstObject();
+
+ TCallback<void()> boundMethodWithoutRC =
+ BIND(&TObjectWithRC::VoidMethod0, Unretained(&ObjectWithRC)); // (NoRef)
+ boundMethodWithoutRC();
+
+ TCallback<void()> constMethodNonConstObjectWithoutRC =
+ BIND(&TObjectWithRC::VoidConstMethod0, Unretained(&ObjectWithRC)); // (NoRef)
+ constMethodNonConstObjectWithoutRC();
+
+ TCallback<void()> constMethodConstObjectWithoutRC =
+ BIND(&TObjectWithRC::VoidConstMethod0, Unretained(ConstObjectWithRCPtr)); // (NoRef)
+ constMethodConstObjectWithoutRC();
+}
+
+// Weak pointer support.
+// - Method bound to a weak pointer to a non-const object.
+// - Const method bound to a weak pointer to a non-const object.
+// - Const method bound to a weak pointer to a const object.
+// - Normal Function with WeakPtr<> as P1 can have return type and is
+// not canceled.
+TEST_F(TBindTest, WeakPtr)
+{
+ TObjectWithExtrinsicRCPtr object = New<TObjectWithFullRC>();
+ TObjectWithExtrinsicRCWkPtr objectWk(object);
+
+ EXPECT_CALL(*object, VoidMethod0());
+ EXPECT_CALL(*object, VoidConstMethod0()).Times(2);
+
+ TClosure boundMethod =
+ BIND(
+ &TObjectWithFullRC::VoidMethod0,
+ TObjectWithExtrinsicRCWkPtr(object));
+ boundMethod();
+
+ TClosure constMethodNonConstObject =
+ BIND(
+ &TObject::VoidConstMethod0,
+ TObjectWithExtrinsicRCWkPtr(object));
+ constMethodNonConstObject();
+
+ TClosure constMethodConstObject =
+ BIND(
+ &TObject::VoidConstMethod0,
+ TObjectWithExtrinsicRCConstWkPtr(object));
+ constMethodConstObject();
+
+ TCallback<int(int)> normalFunc =
+ BIND(
+ &FunctionWithWeakParam<TObjectWithFullRC>,
+ TObjectWithExtrinsicRCWkPtr(object));
+
+ EXPECT_EQ(1, normalFunc(1));
+
+ object.Reset();
+ ASSERT_TRUE(objectWk.IsExpired());
+
+ boundMethod();
+ constMethodNonConstObject();
+ constMethodConstObject();
+
+ EXPECT_EQ(2, normalFunc(2));
+}
+
+// ConstRef() wrapper support.
+// - Binding without ConstRef() takes a copy.
+// - Binding with a ConstRef() takes a reference.
+// - Binding ConstRef() to a function that accepts const reference does not copy on invoke.
+TEST_F(TBindTest, ConstRefWrapper)
+{
+ int n = 1;
+
+ TCallback<int()> withoutConstRef =
+ BIND(&IntegerIdentity, n);
+ TCallback<int()> withConstRef =
+ BIND(&IntegerIdentity, ConstRef(n));
+
+ EXPECT_EQ(1, withoutConstRef());
+ EXPECT_EQ(1, withConstRef());
+ n++;
+ EXPECT_EQ(1, withoutConstRef());
+ EXPECT_EQ(2, withConstRef());
+
+ TProbeState state;
+ TProbe probe(&state);
+
+ TClosure everywhereConstRef =
+ BIND(&Touch, ConstRef(probe));
+ everywhereConstRef();
+
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ EXPECT_THAT(probe, NoAssignments());
+}
+
+// Owned() wrapper support.
+TEST_F(TBindTest, OwnedWrapper)
+{
+ TProbeState state;
+ TProbe* probe;
+
+ // If we don't capture, delete happens on TCallback destruction/reset.
+ // return the same value.
+ state.Reset();
+ probe = new TProbe(&state);
+
+ TCallback<TProbe*()> capturedArgument =
+ BIND(&PolymorphicIdentity<TProbe*>, Owned(probe));
+
+ ASSERT_EQ(probe, capturedArgument());
+ ASSERT_EQ(probe, capturedArgument());
+ EXPECT_EQ(0, state.Destructors);
+ capturedArgument.Reset(); // This should trigger a delete.
+ EXPECT_EQ(1, state.Destructors);
+
+ state.Reset();
+ probe = new TProbe(&state);
+ TCallback<void()> capturedTarget =
+ BIND(&TProbe::Touch, Owned(probe));
+
+ capturedTarget();
+ EXPECT_EQ(0, state.Destructors);
+ capturedTarget.Reset();
+ EXPECT_EQ(1, state.Destructors);
+}
+
+// Passed() wrapper support.
+// - Using Passed() gives TCallback ownership.
+// - Ownership is transferred from TCallback to callee on the first Run().
+TEST_F(TBindTest, PassedWrapper1)
+{
+ TProbeState state;
+ TProbe probe(&state);
+
+ TCallback<TProbe()> cb =
+ BIND(
+ &PolymorphicPassThrough<TProbe>,
+ Passed(std::move(probe)));
+
+ // The argument has been passed.
+ EXPECT_FALSE(probe.IsValid());
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+
+ {
+ // Check that ownership can be transferred back out.
+ int n = state.MoveConstructors;
+ TProbe result = cb();
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_LT(n, state.MoveConstructors);
+ EXPECT_THAT(state, NoCopies());
+
+ // Resetting does not delete since ownership was transferred.
+ cb.Reset();
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+ }
+
+ // Ensure that we actually did get ownership (from the last scope).
+ EXPECT_EQ(1, state.Destructors);
+}
+
+TEST_F(TBindTest, PassedWrapper2)
+{
+ TProbeState state;
+ TProbe probe(&state);
+
+ TCallback<TProbe()> cb =
+ BIND(
+ &PolymorphicIdentity<TProbe>,
+ Passed(std::move(probe)));
+
+ // The argument has been passed.
+ EXPECT_FALSE(probe.IsValid());
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+
+ {
+ // Check that ownership can be transferred back out.
+ int n = state.MoveConstructors;
+ TProbe result = cb();
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_LT(n, state.MoveConstructors);
+ EXPECT_THAT(state, NoCopies());
+
+ // Resetting does not delete since ownership was transferred.
+ cb.Reset();
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+ }
+
+ // Ensure that we actually did get ownership (from the last scope).
+ EXPECT_EQ(1, state.Destructors);
+}
+
+TEST_F(TBindTest, PassedWrapper3)
+{
+ TProbeState state;
+ TProbe sender(&state);
+ TProbe receiver(TProbe::ExplicitlyCreateInvalidProbe());
+
+ TCallback<TProbe(TProbe&&)> cb =
+ BIND(&PolymorphicPassThrough<TProbe>);
+
+ EXPECT_TRUE(sender.IsValid());
+ EXPECT_FALSE(receiver.IsValid());
+
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+
+ receiver = cb(std::move(sender));
+
+ EXPECT_FALSE(sender.IsValid());
+ EXPECT_TRUE(receiver.IsValid());
+
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+}
+
+TEST_F(TBindTest, PassedWrapper4)
+{
+ TProbeState state;
+ TProbe sender(&state);
+ TProbe receiver(TProbe::ExplicitlyCreateInvalidProbe());
+
+ TCallback<TProbe(TProbe)> cb =
+ BIND(&PolymorphicIdentity<TProbe>);
+
+ EXPECT_TRUE(sender.IsValid());
+ EXPECT_FALSE(receiver.IsValid());
+
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+
+ receiver = cb(std::move(sender));
+
+ EXPECT_FALSE(sender.IsValid());
+ EXPECT_TRUE(receiver.IsValid());
+
+ EXPECT_EQ(0, state.Destructors);
+ EXPECT_THAT(state, NoCopies());
+}
+
+
+// Argument constructor usage for non-reference and const reference parameters.
+TEST_F(TBindTest, ArgumentProbing)
+{
+ TProbeState state;
+ TProbe probe(&state);
+
+ TProbe& probeRef = probe;
+ const TProbe& probeConstRef = probe;
+
+ // {T, T&, const T&, T&&} -> T
+ {
+ // Bind T
+ state.Reset();
+ TClosure boundValue =
+ BIND(&VoidPolymorphic1<TProbe>, probe);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundValue();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(2, 0), NoAssignments()));
+
+ // Bind T&
+ state.Reset();
+ TClosure boundRef =
+ BIND(&VoidPolymorphic1<TProbe>, probeRef);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(2, 0), NoAssignments()));
+
+ // Bind const T&
+ state.Reset();
+ TClosure boundConstRef =
+ BIND(&VoidPolymorphic1<TProbe>, probeConstRef);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundConstRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(2, 0), NoAssignments()));
+
+ // Bind T&&
+ state.Reset();
+ TClosure boundRvRef =
+ BIND(&VoidPolymorphic1<TProbe>, static_cast<TProbe&&>(TProbe(&state)));
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(0, 1), NoAssignments()));
+ boundRvRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 1), NoAssignments()));
+
+ // Pass all of above as a forwarded argument.
+ // We expect almost perfect forwarding (copy + move)
+ state.Reset();
+ TCallback<void(TProbe)> forward = BIND(&VoidPolymorphic1<TProbe>);
+
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ forward(probe);
+ EXPECT_THAT(probe, HasCopyMoveCounts(1, 1));
+ forward(probeRef);
+ EXPECT_THAT(probe, HasCopyMoveCounts(2, 2));
+ forward(probeConstRef);
+ EXPECT_THAT(probe, HasCopyMoveCounts(3, 3));
+ forward(TProbe(&state));
+ EXPECT_THAT(probe, HasCopyMoveCounts(3, 4));
+
+ EXPECT_THAT(probe, NoAssignments());
+ }
+
+ // {T, T&, const T&, T&&} -> const T&
+ {
+ // Bind T
+ state.Reset();
+ TClosure boundValue =
+ BIND(&VoidPolymorphic1<const TProbe&>, probe);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundValue();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+
+ // Bind T&
+ state.Reset();
+ TClosure boundRef =
+ BIND(&VoidPolymorphic1<const TProbe&>, probeRef);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+
+ // Bind const T&
+ state.Reset();
+ TClosure boundConstRef =
+ BIND(&VoidPolymorphic1<const TProbe&>, probeConstRef);
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+ boundConstRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(1, 0), NoAssignments()));
+
+ // Bind T&&
+ state.Reset();
+ TClosure boundRvRef =
+ BIND(&VoidPolymorphic1<const TProbe&>, TProbe(&state));
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(0, 1), NoAssignments()));
+ boundRvRef();
+ EXPECT_THAT(probe, AllOf(HasCopyMoveCounts(0, 1), NoAssignments()));
+
+ // Pass all of above as a forwarded argument.
+ // We expect perfect forwarding.
+ state.Reset();
+ TCallback<void(const TProbe&)> forward = BIND(&VoidPolymorphic1<const TProbe&>);
+
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ forward(probe);
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ forward(probeRef);
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ forward(probeConstRef);
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+ forward(static_cast<TProbe&&>(TProbe(&state)));
+ EXPECT_THAT(probe, HasCopyMoveCounts(0, 0));
+
+ EXPECT_THAT(probe, NoAssignments());
+ }
+}
+
+// Argument constructor usage for non-reference and const reference parameters.
+TEST_F(TBindTest, CoercibleArgumentProbing)
+{
+ TProbeState state;
+ TCoercibleToProbe probe(&state);
+
+ TCoercibleToProbe& probeRef = probe;
+ const TCoercibleToProbe& probeConstRef = probe;
+
+ // Pass {T, T&, const T&, T&&} as a forwarded argument.
+ // We expect almost perfect forwarding (copy + move).
+ state.Reset();
+ TCallback<void(TProbe)> forward = BIND(&VoidPolymorphic1<TProbe>);
+
+ EXPECT_THAT(state, HasCopyMoveCounts(0, 0));
+ forward(probe);
+ EXPECT_THAT(state, HasCopyMoveCounts(1, 1));
+ forward(probeRef);
+ EXPECT_THAT(state, HasCopyMoveCounts(2, 2));
+ forward(probeConstRef);
+ EXPECT_THAT(state, HasCopyMoveCounts(3, 3));
+ forward(TProbe(&state));
+ EXPECT_THAT(state, HasCopyMoveCounts(3, 4));
+
+ EXPECT_THAT(state, NoAssignments());
+}
+
+// TCallback construction and assignment tests.
+// - Construction from an InvokerStorageHolder should not cause ref/deref.
+// - Assignment from other callback should only cause one ref
+//
+// TODO(ajwong): Is there actually a way to test this?
+
+// Lambda support.
+// - Should be able to bind C++11 lambdas without any arguments.
+// - Should be able to bind C++11 lambdas with free arguments.
+TEST_F(TBindTest, LambdaSupport)
+{
+ int n = 1;
+
+ TClosure closure = BIND([&n] () { ++n; });
+ EXPECT_EQ(1, n);
+ closure();
+ EXPECT_EQ(2, n);
+ closure();
+ EXPECT_EQ(3, n);
+
+ TCallback<int()> cb1 = BIND([ ] () -> int { return 42; });
+ TCallback<int()> cb2 = BIND([&n] () -> int { return ++n; });
+
+ EXPECT_EQ(42, cb1());
+ EXPECT_EQ( 4, cb2());
+ EXPECT_EQ( 4, n);
+ EXPECT_EQ( 5, cb2());
+ EXPECT_EQ( 5, n);
+
+ TCallback<int(int, int)> plus = BIND([] (int a, int b) -> int { return a + b; });
+ TCallback<int(int)> plus5 = BIND([] (int a, int b) -> int { return a + b; }, 5);
+
+ EXPECT_EQ(3, plus. Run(1, 2));
+ EXPECT_EQ(6, plus5(1));
+}
+
+TEST_F(TBindTest, MutableLambdaSupport)
+{
+ auto f = BIND([n = 1] () mutable { ++n; return n; });
+ EXPECT_EQ(2, f());
+ EXPECT_EQ(3, f());
+ EXPECT_EQ(4, f());
+}
+
+TEST_F(TBindTest, ConstLambdaSupport)
+{
+ int n = 1;
+ const auto l = [&n] () { ++n; return n; };
+ const auto f = BIND(l);
+ EXPECT_EQ(2, f());
+ EXPECT_EQ(3, f());
+ EXPECT_EQ(4, f());
+}
+
+TEST_F(TBindTest, MoveOnlyLambdaSupport)
+{
+ class TMoveOnly
+ {
+ public:
+ TMoveOnly(size_t v)
+ : Value_(v)
+ { }
+
+ TMoveOnly(const TMoveOnly&) = delete;
+
+ TMoveOnly(TMoveOnly&& rhs)
+ : Value_(rhs.Value_)
+ {
+ rhs.Value_ = 0;
+ }
+
+ size_t Value() const
+ {
+ return Value_;
+ }
+
+ private:
+ size_t Value_ = 0;
+ };
+
+ TMoveOnly outer(5);
+ EXPECT_EQ(5u, outer.Value());
+
+ auto f = BIND([inner = std::move(outer)] () { return inner.Value(); });
+ EXPECT_EQ(0u, outer.Value());
+
+ // Call twice just in case
+ EXPECT_EQ(5u, f());
+ EXPECT_EQ(5u, f());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/bit_packed_integer_vector_ut.cpp b/yt/yt/core/misc/unittests/bit_packed_integer_vector_ut.cpp
new file mode 100644
index 0000000000..eaa209a13b
--- /dev/null
+++ b/yt/yt/core/misc/unittests/bit_packed_integer_vector_ut.cpp
@@ -0,0 +1,152 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/bit_packed_unsigned_vector.h>
+#include <yt/yt/core/misc/bit_packing.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+size_t Compress(const std::vector<T> &data, std::vector<ui64> *buffer)
+{
+ T maxValue = 0;
+ if (!data.empty()) {
+ auto it = std::max_element(data.begin(), data.end());
+ maxValue = *it;
+ }
+
+ auto size = CompressedUnsignedVectorSizeInWords(maxValue, data.size());
+ // NB: initialize with zeros!
+ buffer->resize(size, 0);
+
+ return BitPackUnsignedVector(MakeRange(data), maxValue, buffer->data());
+}
+
+template <class T, class TReader>
+void Validate(const std::vector<T>& data, const TReader& reader)
+{
+ EXPECT_EQ(data.size(), reader.GetSize());
+
+ for (int i = 0; i < std::ssize(data); ++i) {
+ EXPECT_EQ(data[i], reader[i]);
+ }
+}
+
+template <class T>
+class TNewBitReader
+ : public std::unique_ptr<T[]>
+{
+public:
+ explicit TNewBitReader(TCompressedVectorView view)
+ : std::unique_ptr<T[]>(new T[view.GetSize()])
+ , Size_(view.GetSize())
+ {
+ view.UnpackTo(this->get());
+ }
+
+ explicit TNewBitReader(const ui64* data)
+ : TNewBitReader(TCompressedVectorView(data))
+ { }
+
+ size_t GetSize() const
+ {
+ return Size_;
+ }
+
+private:
+ size_t Size_;
+};
+
+template <class T>
+void DoTest(T value, size_t count)
+{
+ std::vector<T> data(count, value);
+ std::vector<ui64> buffer;
+
+ size_t size = Compress(data, &buffer);
+ EXPECT_EQ(CompressedUnsignedVectorSizeInWords(value, count), size);
+
+ auto reader = TBitPackedUnsignedVectorReader<T>(buffer.data());
+ Validate(data, reader);
+
+ auto newReader = TNewBitReader<T>(buffer.data());
+ Validate(data, newReader);
+
+ TCompressedVectorView view(buffer.data());
+ Validate(data, view);
+
+ if constexpr (sizeof(T) <= sizeof(ui32)) {
+ TCompressedVectorView32 view(buffer.data());
+ Validate(data, view);
+ }
+}
+
+template <class T>
+void DoTestZero()
+{
+ T value = 0;
+ DoTest(value, 0);
+ DoTest(value, 10);
+}
+
+TEST(TCompressedIntegerVectorTest, TestZero)
+{
+ DoTestZero<ui8>();
+ DoTestZero<ui16>();
+ DoTestZero<ui32>();
+ DoTestZero<ui64>();
+}
+
+template <class T>
+void DoTestMax()
+{
+ T value = std::numeric_limits<T>::max();
+ DoTest(value, 10);
+}
+
+TEST(TCompressedIntegerVectorTest, TestMax)
+{
+ DoTestMax<ui8>();
+ DoTestMax<ui16>();
+ DoTestMax<ui32>();
+ DoTestMax<ui64>();
+}
+
+template <class T>
+void DoTestPowerOfTwo()
+{
+ // Set half of the bits.
+ T value = MaskLowerBits(1, sizeof(T) * 4);
+ DoTest(value, 1000);
+}
+
+TEST(TCompressedIntegerVectorTest, TestPowerOfTwo)
+{
+ DoTestPowerOfTwo<ui8>();
+ DoTestPowerOfTwo<ui16>();
+ DoTestPowerOfTwo<ui32>();
+ DoTestPowerOfTwo<ui64>();
+}
+
+template <class T>
+void DoTestOdd()
+{
+ // 00 ... 010 ... 01
+ T value = (1ULL << (sizeof(T) * 4)) + 1;
+ DoTest(value, 1000);
+}
+
+TEST(TCompressedIntegerVectorTest, TestOdd)
+{
+ DoTestOdd<ui8>();
+ DoTestOdd<ui16>();
+ DoTestOdd<ui32>();
+ DoTestOdd<ui64>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/boolean_formula_ut.cpp b/yt/yt/core/misc/unittests/boolean_formula_ut.cpp
new file mode 100644
index 0000000000..8b3cad6858
--- /dev/null
+++ b/yt/yt/core/misc/unittests/boolean_formula_ut.cpp
@@ -0,0 +1,201 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/arithmetic_formula.h>
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBooleanFormulaTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ const char*,
+ std::vector<TString>,
+ bool>>
+{ };
+
+TEST_P(TBooleanFormulaTest, Test)
+{
+ const auto& args = GetParam();
+ const auto& formula = std::get<0>(args);
+ const auto& values = std::get<1>(args);
+ bool expected = std::get<2>(args);
+
+ auto filter = MakeBooleanFormula(formula);
+
+ EXPECT_EQ(expected, filter.IsSatisfiedBy(values))
+ << "formula: " << formula << std::endl
+ << "true variables: " << ::testing::PrintToString(values) << std::endl
+ << "expected: " << expected;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TBooleanFormulaTest,
+ TBooleanFormulaTest,
+ ::testing::Values(
+ std::make_tuple("", std::vector<TString>{}, true),
+ std::make_tuple("", std::vector<TString>{"b"}, true),
+ std::make_tuple("a", std::vector<TString>{"b"}, false),
+ std::make_tuple("!a", std::vector<TString>{"b"}, true),
+ std::make_tuple("b", std::vector<TString>{"b"}, true),
+ std::make_tuple("a|b", std::vector<TString>{"b"}, true),
+ std::make_tuple("a & b", std::vector<TString>{"b"}, false),
+ std::make_tuple("(b)", std::vector<TString>{"b"}, true),
+ std::make_tuple("a|(a|b)", std::vector<TString>{"b"}, true),
+ std::make_tuple("(a|b)&(!a&b)", std::vector<TString>{"b"}, true),
+ std::make_tuple("a&b", std::vector<TString>{"a", "b"}, true),
+ std::make_tuple("(a|c)&(b|c)", std::vector<TString>{"a", "b"}, true),
+ std::make_tuple("(a|b)&c", std::vector<TString>{"a", "b"}, false),
+ std::make_tuple("a|b|c", std::vector<TString>{"b"}, true),
+ std::make_tuple("!a & b & !c", std::vector<TString>{"b"}, true),
+ std::make_tuple("var-1 | !var/2", std::vector<TString>{"var-1"}, true),
+ std::make_tuple("var-1 | !var/2", std::vector<TString>{"var/2"}, false),
+ std::make_tuple("var-1 | !var/2", std::vector<TString>{}, true),
+ std::make_tuple("!in-", std::vector<TString>{}, true),
+ std::make_tuple("in/|x", std::vector<TString>{"in/"}, true),
+ std::make_tuple("%true", std::vector<TString>{""}, true),
+ std::make_tuple("%false", std::vector<TString>{"false"}, false),
+ std::make_tuple("%true|%false", std::vector<TString>{""}, true),
+ std::make_tuple("a.b.c-d.e:1234", std::vector<TString>{"a.b.c-d.e:1234"}, true),
+ std::make_tuple("!a.b.c-d.e:1234", std::vector<TString>{"a.b.c-d.e:1234"}, false)
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBooleanFormulaParseErrorTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<const char*>
+{ };
+
+TEST_P(TBooleanFormulaParseErrorTest, Test)
+{
+ const auto& formula = GetParam();
+
+ EXPECT_THROW(MakeBooleanFormula(formula), std::exception)
+ << "formula: " << formula;
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TBooleanFormulaParseErrorTest,
+ TBooleanFormulaParseErrorTest,
+ ::testing::Values(
+ "!",
+ "&",
+ "|",
+ "(",
+ ")",
+ "()",
+ "()|a",
+ "a&()",
+ "a&(",
+ "a|)",
+ "&a",
+ "a&",
+ "a!",
+ "a!b",
+ "a|c!",
+ "a!|c",
+ "a|(c!)",
+ "a|(c&)",
+ "a|(|c)",
+ "1",
+ "a||b",
+ "a&&b",
+ "a+b",
+ "a^b",
+ "a==b",
+ "a!=b",
+ "a>=b",
+ "a<=b",
+ "a>b",
+ "a<b",
+ "a*b",
+ "a%b",
+ "/a",
+ "-",
+ "a /b",
+ "a & /b",
+ "in",
+ "% true",
+ "%something",
+ "%true.abc"
+));
+
+class TBooleanFormulaTest2
+ : public ::testing::Test
+{ };
+
+TEST_F(TBooleanFormulaTest2, Equality)
+{
+ auto formulaA = MakeBooleanFormula("internal");
+ auto formulaB = MakeBooleanFormula("internal");
+ auto formulaC = MakeBooleanFormula("cloud");
+
+ EXPECT_TRUE(formulaA == formulaB);
+ EXPECT_FALSE(formulaA == formulaC);
+
+ EXPECT_TRUE(formulaA.GetHash() == formulaB.GetHash());
+ EXPECT_FALSE(formulaA.GetHash() == formulaC.GetHash());
+}
+
+TEST(TBooleanFormulaTest, ValidateVariable)
+{
+ ValidateBooleanFormulaVariable("var");
+ ValidateBooleanFormulaVariable("var/2");
+ ValidateBooleanFormulaVariable("var-var-var");
+ ValidateBooleanFormulaVariable("tablet_common/news-queue");
+ ValidateBooleanFormulaVariable("VAR123VAR");
+ ValidateBooleanFormulaVariable("IN");
+
+ EXPECT_THROW(ValidateBooleanFormulaVariable("2var"), TErrorException);
+ EXPECT_THROW(ValidateBooleanFormulaVariable("foo bar"), TErrorException);
+ EXPECT_THROW(ValidateBooleanFormulaVariable(""), TErrorException);
+ EXPECT_THROW(ValidateBooleanFormulaVariable("a+b"), TErrorException);
+ EXPECT_THROW(ValidateBooleanFormulaVariable("in"), TErrorException);
+}
+
+TEST(TBooleanFormulaTest, ExternalOperators)
+{
+ auto formulaA = MakeBooleanFormula("a");
+ auto formulaB = MakeBooleanFormula("b");
+ auto aAndB = formulaA & formulaB;
+ auto aOrB = formulaA | formulaB;
+ auto notA = !formulaA;
+
+ for (auto vars : std::vector<std::vector<TString>>{{}, {"a"}, {"b"}, {"a", "b"}}) {
+ bool resA = formulaA.IsSatisfiedBy(vars);
+ bool resB = formulaB.IsSatisfiedBy(vars);
+
+ EXPECT_EQ(resA & resB, aAndB.IsSatisfiedBy(vars));
+ EXPECT_EQ(resA | resB, aOrB.IsSatisfiedBy(vars));
+ EXPECT_EQ(!resA, notA.IsSatisfiedBy(vars));
+ }
+
+ EXPECT_FALSE((!MakeBooleanFormula("a | b"))
+ .IsSatisfiedBy(std::vector<TString>{"b"}));
+
+ EXPECT_EQ((formulaA & formulaB).GetFormula(), "(a) & (b)");
+ EXPECT_EQ((formulaA | formulaB).GetFormula(), "(a) | (b)");
+ EXPECT_EQ((!formulaA).GetFormula(), "!(a)");
+
+ auto empty = MakeBooleanFormula("");
+ EXPECT_TRUE(empty.IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_FALSE((!empty).IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_TRUE((empty | !empty).IsSatisfiedBy(THashSet<TString>{}));
+
+ EXPECT_TRUE((empty | formulaA).IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_TRUE((empty | formulaA).IsSatisfiedBy(THashSet<TString>{"a"}));
+ EXPECT_TRUE((formulaA | empty).IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_TRUE((formulaA | empty).IsSatisfiedBy(THashSet<TString>{"a"}));
+ EXPECT_FALSE((empty & formulaA).IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_TRUE((empty & formulaA).IsSatisfiedBy(THashSet<TString>{"a"}));
+ EXPECT_FALSE((formulaA & empty).IsSatisfiedBy(THashSet<TString>{}));
+ EXPECT_TRUE((formulaA & empty).IsSatisfiedBy(THashSet<TString>{"a"}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/callback_ut.cpp b/yt/yt/core/misc/unittests/callback_ut.cpp
new file mode 100644
index 0000000000..cbc3578181
--- /dev/null
+++ b/yt/yt/core/misc/unittests/callback_ut.cpp
@@ -0,0 +1,179 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/callback.h>
+#include <yt/yt/core/actions/callback_internal.h>
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// White-box testpoint.
+struct TFakeInvoker
+{
+ using TSignature = void(NDetail::TBindStateBase*);
+ static void Run(NDetail::TBindStateBase*)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ bool CaptureTraceContext,
+ class TRunnable,
+ class TSignature,
+ class TBoundArgs
+>
+struct TBindState;
+
+// White-box injection into a #TCallback<> object for checking
+// comparators and emptiness APIs. Use a #TBindState<> that is specialized
+// based on a type we declared in the anonymous namespace above to remove any
+// chance of colliding with another instantiation and breaking the
+// one-definition-rule.
+template <>
+struct TBindState<true, void(), void(), void(TFakeInvoker)>
+ : public NDetail::TBindStateBase
+{
+public:
+ using TInvokerType = TFakeInvoker;
+ TBindState()
+ : TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ FROM_HERE
+#endif
+ )
+ { }
+};
+
+template <>
+struct TBindState<true, void(), void(), void(TFakeInvoker, TFakeInvoker)>
+ : public NDetail::TBindStateBase
+{
+ using TInvokerType = TFakeInvoker;
+ TBindState()
+ : TBindStateBase(
+#ifdef YT_ENABLE_BIND_LOCATION_TRACKING
+ FROM_HERE
+#endif
+ )
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <bool CaptureTraceContext, class TRunnable, class TSignature, class TBoundArgs>
+TCallback<TSignature> MakeCallback(
+ TIntrusivePtr<NYT::TBindState<CaptureTraceContext, TRunnable, TSignature, TBoundArgs>>&& bindState)
+{
+ auto invokeFunction = &NYT::TBindState<CaptureTraceContext, TRunnable, TSignature, TBoundArgs>::TInvokerType::Run;
+ return TCallback<TSignature>(std::move(bindState), invokeFunction);
+}
+
+// TODO(sandello): Implement accurate check on the number of Ref() and Unref()s.
+
+using TFakeBindState1 = TBindState<true, void(), void(), void(TFakeInvoker)>;
+using TFakeBindState2 = TBindState<true, void(), void(), void(TFakeInvoker, TFakeInvoker)>;
+
+class TCallbackTest
+ : public ::testing::Test
+{
+public:
+ TCallbackTest()
+ : FirstCallback(MakeCallback(New<TFakeBindState1>()))
+ , SecondCallback(MakeCallback(New<TFakeBindState2>()))
+ { }
+
+ virtual ~TCallbackTest()
+ { }
+
+protected:
+ TCallback<void()> FirstCallback;
+ const TCallback<void()> SecondCallback;
+
+ TCallback<void()> NullCallback;
+};
+
+// Ensure we can create unbound callbacks. We need this to be able to store
+// them in class members that can be initialized later.
+TEST_F(TCallbackTest, DefaultConstruction)
+{
+ TCallback<void()> c0;
+
+ TCallback<void(int)> c1;
+ TCallback<void(int,int)> c2;
+ TCallback<void(int,int,int)> c3;
+ TCallback<void(int,int,int,int)> c4;
+ TCallback<void(int,int,int,int,int)> c5;
+ TCallback<void(int,int,int,int,int,int)> c6;
+
+ EXPECT_FALSE(c0);
+ EXPECT_FALSE(c1);
+ EXPECT_FALSE(c2);
+ EXPECT_FALSE(c3);
+ EXPECT_FALSE(c4);
+ EXPECT_FALSE(c5);
+ EXPECT_FALSE(c6);
+}
+
+TEST_F(TCallbackTest, IsNull)
+{
+ EXPECT_FALSE(NullCallback);
+ EXPECT_TRUE(FirstCallback);
+ EXPECT_TRUE(SecondCallback);
+}
+
+TEST_F(TCallbackTest, Move)
+{
+ EXPECT_TRUE(FirstCallback);
+
+ TCallback<void()> localCallback(std::move(FirstCallback));
+ TCallback<void()> anotherCallback;
+
+ EXPECT_FALSE(FirstCallback);
+ EXPECT_TRUE(localCallback);
+ EXPECT_FALSE(anotherCallback);
+
+ anotherCallback = std::move(localCallback);
+
+ EXPECT_FALSE(FirstCallback);
+ EXPECT_FALSE(localCallback);
+ EXPECT_TRUE(anotherCallback);
+}
+
+TEST_F(TCallbackTest, Equals)
+{
+ EXPECT_EQ(FirstCallback, FirstCallback);
+ EXPECT_NE(FirstCallback, SecondCallback);
+ EXPECT_NE(SecondCallback, FirstCallback);
+
+ // We should compare based on instance, not type.
+ TCallback<void()> localCallback(MakeCallback(New<TFakeBindState1>()));
+ TCallback<void()> anotherCallback = FirstCallback;
+
+ EXPECT_EQ(FirstCallback, anotherCallback);
+ EXPECT_NE(FirstCallback, localCallback);
+
+ // Empty, however, is always equal to empty.
+ TCallback<void()> localNullCallback;
+ EXPECT_EQ(NullCallback, localNullCallback);
+}
+
+TEST_F(TCallbackTest, Reset)
+{
+ // Resetting should bring us back to empty.
+ ASSERT_TRUE(FirstCallback);
+ ASSERT_NE(FirstCallback, NullCallback);
+
+ FirstCallback.Reset();
+
+ EXPECT_FALSE(FirstCallback);
+ EXPECT_EQ(FirstCallback, NullCallback);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/checksum_ut.cpp b/yt/yt/core/misc/unittests/checksum_ut.cpp
new file mode 100644
index 0000000000..a9de7092ce
--- /dev/null
+++ b/yt/yt/core/misc/unittests/checksum_ut.cpp
@@ -0,0 +1,126 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/blob.h>
+
+#include <yt/yt/core/misc/checksum.h>
+
+#include <util/random/random.h>
+#include <util/stream/mem.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCrcTestCase
+{
+ ui64 Iso;
+ ui64 Ecma;
+ ui64 Ours;
+ TString Data;
+};
+
+static std::vector<TCrcTestCase> Cases = {
+ {
+ 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
+ ""
+ }, {
+ 0x3420000000000000, 0x330284772e652b05, 0x74b42565ce6232d5,
+ "a"
+ }, {
+ 0x36c4200000000000, 0xbc6573200e84b046, 0x5f02be5e81cf7b1c,
+ "ab"
+ }, {
+ 0x3776c42000000000, 0x2cd8094a1a277627, 0xaadaac6d7d340c20,
+ "abc"
+ }, {
+ 0x336776c420000000, 0x3c9d28596e5960ba, 0xd35b54234f7f70a0,
+ "abcd"
+ }, {
+ 0x32d36776c4200000, 0x040bdf58fb0895f2, 0xe729d85f050fa861,
+ "abcde"
+ }, {
+ 0x3002d36776c42000, 0xd08e9f8545a700f4, 0x4852bb31b666ae4f,
+ "abcdef"
+ }, {
+ 0x31b002d36776c420, 0xec20a3a8cc710e66, 0xab31ee2e0fe39abb,
+ "abcdefg"
+ }, {
+ 0xe21b002d36776c40, 0x67b4f30a647a0c59, 0x3dc543531acca62b,
+ "abcdefgh"
+ }, {
+ 0x8b6e21b002d36776, 0x9966f6c89d56ef8e, 0x43c501e26fc35778,
+ "abcdefghi"
+ }, {
+ 0x7f5b6e21b002d367, 0x32093a2ecd5773f4, 0x4cc4843d59c1373e,
+ "abcdefghij"
+ }, {
+ 0xa7b9d53ea87eb82f, 0x561cc0cfa235ac68, 0x481ac76eee0d3ebd,
+ "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"
+ },
+};
+
+TEST(TChecksumTest, Ours)
+{
+ for (size_t i = 0; i < Cases.size(); ++i) {
+ auto data = TRef::FromString(Cases[i].Data);
+
+ auto isaCrc = GetChecksum(data);
+ auto oldCrc = GetChecksumOld(data);
+
+ EXPECT_EQ(oldCrc, Cases[i].Ours);
+ EXPECT_EQ(isaCrc, Cases[i].Ours);
+ }
+}
+
+
+static constexpr int IterCount = 1000;
+static constexpr int BufMaxSize = 100000;
+
+TEST(TChecksumTest, Reference)
+{
+ for (size_t iter = 0; iter < IterCount; ++iter) {
+ auto bufSize = RandomNumber<ui64>(BufMaxSize);
+ auto buffer = TSharedMutableRef::Allocate(bufSize);
+
+ for(size_t index = 0; index < bufSize; ++index) {
+ buffer.Begin()[index] = RandomNumber<unsigned char>();
+ }
+
+ auto isaCrc = GetChecksum(TRef(buffer));
+ auto oldCrc = GetChecksumOld(TRef(buffer));
+
+ EXPECT_EQ(isaCrc, oldCrc);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TChecksumTest, TestStreams)
+{
+ auto size = 0;
+ for (const auto& test : Cases) {
+ size += test.Data.Size();
+ }
+ TBlob blob(GetRefCountedTypeCookie<TDefaultBlobTag>(), size);
+
+ auto memoryOutputStream = TMemoryOutput(blob.Begin(), blob.Size());
+ auto outputStream = TChecksumOutput(&memoryOutputStream);
+ for (const auto& test : Cases) {
+ outputStream.Write(TStringBuf(test.Data));
+ }
+ auto outputChecksum = outputStream.GetChecksum();
+
+ auto memoryInputStream = TMemoryInput(blob.Begin(), blob.Size());
+ auto inputStream = TChecksumInput(&memoryInputStream);
+ char v;
+ while (inputStream.Read(&v, 1)) { }
+ auto inputChecksum = inputStream.GetChecksum();
+
+ EXPECT_EQ(outputChecksum, inputChecksum);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/codicil_ut.cpp b/yt/yt/core/misc/unittests/codicil_ut.cpp
new file mode 100644
index 0000000000..94d81af601
--- /dev/null
+++ b/yt/yt/core/misc/unittests/codicil_ut.cpp
@@ -0,0 +1,46 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/crash_handler.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCodicilTest, Simple)
+{
+ const auto codicil1 = TString("codicil1");
+ const auto codicil2 = TString("codicil2");
+ TCodicilGuard guard1(codicil1);
+ TCodicilGuard guard2(codicil2);
+ EXPECT_EQ(GetCodicils(), (std::vector{codicil1, codicil2}));
+}
+
+TEST(TCodicilTest, CodicilGuardedInvoker)
+{
+ const auto codicil = TString("codicil");
+ auto actionQueue = New<TActionQueue>("ActionQueue");
+ auto invoker = CreateCodicilGuardedInvoker(actionQueue->GetInvoker(), codicil);
+ BIND([&] {
+ EXPECT_EQ(GetCodicils(), (std::vector{codicil}));
+ TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(100));
+ EXPECT_EQ(GetCodicils(), (std::vector{codicil}));
+ })
+ .AsyncVia(invoker)
+ .Run()
+ .Get();
+ actionQueue->Shutdown();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/concurrent_cache_ut.cpp b/yt/yt/core/misc/unittests/concurrent_cache_ut.cpp
new file mode 100644
index 0000000000..007b51c103
--- /dev/null
+++ b/yt/yt/core/misc/unittests/concurrent_cache_ut.cpp
@@ -0,0 +1,150 @@
+#include "lock_free_hash_table_and_concurrent_cache_helpers.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/bind.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/slab_allocator.h>
+#include <yt/yt/core/misc/concurrent_cache.h>
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TElement final
+{
+ ui64 Hash;
+ ui32 Size;
+ char Data[0];
+
+ using TAllocator = TSlabAllocator;
+ static constexpr bool EnableHazard = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
+template <>
+struct THash<NYT::TElement>
+{
+ size_t operator()(const NYT::TElement* value) const
+ {
+ return value->Hash;
+ }
+};
+
+template <>
+struct TEqualTo<NYT::TElement>
+{
+ bool operator()(const NYT::TElement* lhs, const NYT::TElement* rhs) const
+ {
+ return lhs->Hash == rhs->Hash &&
+ lhs->Size == rhs->Size &&
+ memcmp(lhs->Data, rhs->Data, lhs->Size) == 0;
+ }
+};
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYT;
+using namespace NYT::NConcurrency;
+
+class TConcurrentCacheTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<
+ int /*keyColumnCount*/,
+ int /*threadCount*/,
+ int /*iterations*/,
+ bool /*reinsert*/>>
+{ };
+
+TEST_P(TConcurrentCacheTest, Stress)
+{
+ size_t keyColumnCount = std::get<0>(GetParam());
+ size_t threadCount = std::get<1>(GetParam());
+ size_t iterations = std::get<2>(GetParam());
+ bool reinsert = std::get<3>(GetParam());
+
+ size_t columnCount = keyColumnCount;
+ size_t distinctElements = std::pow('z' - 'a' + 1 + '9' - '0' + 1, keyColumnCount);
+ size_t tableSize = distinctElements / 2 + 1;
+
+ YT_VERIFY(keyColumnCount <= columnCount);
+
+ THazardPtrReclaimOnContextSwitchGuard flushGuard;
+ TSlabAllocator allocator;
+ TConcurrentCache<TElement> concurrentCache(tableSize);
+
+ auto threadPool = CreateThreadPool(threadCount, "Workers");
+ std::vector<TFuture<size_t>> asyncResults;
+
+ for (size_t threadId = 0; threadId < threadCount; ++threadId) {
+ asyncResults.push_back(BIND([&, threadId] () -> size_t {
+ THazardPtrReclaimOnContextSwitchGuard flushGuard;
+ TRandomCharGenerator randomChar(threadId);
+
+ size_t insertCount = 0;
+
+ auto keyBuffer = std::make_unique<char[]>(sizeof(TElement) + columnCount);
+ auto* key = reinterpret_cast<TElement*>(keyBuffer.get());
+
+ for (size_t index = 0; index < iterations * tableSize; ++index) {
+ key->Size = keyColumnCount;
+ for (size_t pos = 0; pos < columnCount; ++pos) {
+ key->Data[pos] = randomChar();
+ }
+
+ key->Hash = THash<TStringBuf>{}(TStringBuf(&key->Data[0], keyColumnCount));
+
+ auto lookuper = concurrentCache.GetLookuper();
+ auto inserter = concurrentCache.GetInserter();
+ auto foundRef = lookuper(key);
+
+ if (!foundRef) {
+ auto value = NewWithExtraSpace<TElement>(&allocator, columnCount);
+ memcpy(value.Get(), key, sizeof(TElement) + columnCount);
+ bool inserted = inserter.GetTable()->Insert(std::move(value));
+
+ insertCount += inserted;
+ } else if (reinsert) {
+ auto table = inserter.GetTable();
+ if (table != foundRef.Origin) {
+ table->Insert(foundRef.Get());
+ }
+ }
+ }
+
+ return insertCount;
+ })
+ .AsyncVia(threadPool->GetInvoker())
+ .Run());
+ }
+
+ auto results = AllSucceeded(asyncResults).Get().Value();
+
+ EXPECT_LE(distinctElements, std::accumulate(results.begin(), results.end(), 0u));
+ threadPool->Shutdown();
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Simple,
+ TConcurrentCacheTest,
+ ::testing::Values(
+ std::make_tuple(2, 1, 1000, false),
+ std::make_tuple(2, 5, 1000, false),
+ std::make_tuple(2, 5, 1000, true)
+ ));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/default_map_ut.cpp b/yt/yt/core/misc/unittests/default_map_ut.cpp
new file mode 100644
index 0000000000..f40d3719f1
--- /dev/null
+++ b/yt/yt/core/misc/unittests/default_map_ut.cpp
@@ -0,0 +1,26 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/default_map.h>
+
+#include <util/generic/hash_table.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TDefaultMap, Common)
+{
+ TDefaultMap<THashMap<int, TString>> defaultMap("Hello");
+ EXPECT_EQ(defaultMap[1], "Hello");
+ defaultMap[1].append(", World");
+ EXPECT_EQ(defaultMap[1], "Hello, World");
+ defaultMap.insert({2, "abc"});
+ EXPECT_EQ(defaultMap[2], "abc");
+ EXPECT_EQ(defaultMap.find(3), defaultMap.end());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/digest_ut.cpp b/yt/yt/core/misc/unittests/digest_ut.cpp
new file mode 100644
index 0000000000..ad57883e36
--- /dev/null
+++ b/yt/yt/core/misc/unittests/digest_ut.cpp
@@ -0,0 +1,252 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/digest.h>
+#include <yt/yt/core/misc/config.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLogDigestTest
+ : public ::testing::Test
+{
+protected:
+ void CreateStandardLogDigest()
+ {
+ auto config = New<TLogDigestConfig>();
+ config->LowerBound = 0.5;
+ config->UpperBound = 1.0;
+ config->RelativePrecision = Epsilon;
+ LogDigest_ = CreateLogDigest(config);
+ }
+
+ bool LogNear(double a, double b)
+ {
+ return a < b * (1 + Epsilon) * (1 + Epsilon) && b < a * (1 + Epsilon) * (1 + Epsilon);
+ }
+
+ static constexpr double Epsilon = 0.01;
+ static constexpr int SampleCount = 10000;
+
+ IDigestPtr LogDigest_;
+};
+
+TEST_F(TLogDigestTest, TestStrictFixtureInRange)
+{
+ CreateStandardLogDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(0.77);
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.0), 0.5));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 0.77));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(1.0), 0.77));
+}
+
+TEST_F(TLogDigestTest, TestStrictFixtureBelowRange)
+{
+ CreateStandardLogDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(0.17);
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.0), 0.5));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 0.5));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(1.0), 0.5));
+}
+
+TEST_F(TLogDigestTest, TestStrictFixtureAboveRange)
+{
+ CreateStandardLogDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(1.17);
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.0), 0.5));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(1.0), 1.0));
+}
+
+TEST_F(TLogDigestTest, TestNormalDistributionFixture)
+{
+ CreateStandardLogDigest();
+
+ std::mt19937 generator(42 /* seed */);
+ std::normal_distribution<double> distribution(0.77, 0.05);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 0.77));
+ // Theoretical 95% quantile.
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.95), 0.852));
+}
+
+TEST_F(TLogDigestTest, TestUniformRandomFixture)
+{
+ CreateStandardLogDigest();
+
+ std::mt19937 generator(42 /* seed */);
+ std::uniform_real_distribution<double> distribution(0.25, 1.25);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(1.0), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.75), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 0.75));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.25), 0.5));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.0), 0.5));
+}
+
+TEST_F(TLogDigestTest, TestCoincidingBounds)
+{
+ auto config = New<TLogDigestConfig>();
+ config->LowerBound = 1.0;
+ config->UpperBound = 1.0;
+ config->RelativePrecision = Epsilon;
+ LogDigest_ = CreateLogDigest(config);
+
+ std::mt19937 generator(42 /* seed */);
+ std::uniform_real_distribution<double> distribution(0.5, 1.5);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ LogDigest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(1.0), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.75), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.5), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.25), 1.0));
+ EXPECT_TRUE(LogNear(LogDigest_->GetQuantile(0.0), 1.0));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogramDigestTest
+ : public ::testing::Test
+{
+protected:
+ void CreateStandardHistogramDigest()
+ {
+ auto config = New<THistogramDigestConfig>();
+ config->LowerBound = 0.0;
+ config->UpperBound = 1.0;
+ config->AbsolutePrecision = Epsilon;
+ Digest_ = CreateHistogramDigest(config);
+ }
+
+ static constexpr double Epsilon = 0.01;
+ static constexpr int SampleCount = 10000;
+ static constexpr int Seed = 225;
+
+ IDigestPtr Digest_;
+};
+
+TEST_F(THistogramDigestTest, TestStrictFixtureInRange)
+{
+ CreateStandardHistogramDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(0.77);
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(0.0), 0.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 0.77, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(1.0), 0.77, Epsilon);
+}
+
+TEST_F(THistogramDigestTest, TestStrictFixtureBelowRange)
+{
+ CreateStandardHistogramDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(-0.117);
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(0.0), 0.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 0.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(1.0), 0.0, Epsilon);
+}
+
+TEST_F(THistogramDigestTest, TestStrictFixtureAboveRange)
+{
+ CreateStandardHistogramDigest();
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(1.17);
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(0.0), 0.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(1.0), 1.0, Epsilon);
+}
+
+TEST_F(THistogramDigestTest, TestNormalDistributionFixture)
+{
+ CreateStandardHistogramDigest();
+
+ std::mt19937 generator(Seed);
+ std::normal_distribution<double> distribution(0.77, 0.05);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 0.77, Epsilon);
+ // Theoretical 95% quantile.
+ EXPECT_NEAR(Digest_->GetQuantile(0.95), 0.852, Epsilon);
+}
+
+TEST_F(THistogramDigestTest, TestUniformRandomFixture)
+{
+ CreateStandardHistogramDigest();
+
+ std::mt19937 generator(Seed);
+ std::uniform_real_distribution<double> distribution(-0.5, 1.5);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(1.0), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.75), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 0.5, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.25), 0.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.0), 0.0, Epsilon);
+}
+
+TEST_F(THistogramDigestTest, TestCoincidingBounds)
+{
+ auto config = New<THistogramDigestConfig>();
+ config->LowerBound = 1.0;
+ config->UpperBound = 1.0;
+ config->AbsolutePrecision = Epsilon;
+ Digest_ = CreateHistogramDigest(std::move(config));
+
+ std::mt19937 generator(Seed);
+ std::uniform_real_distribution<double> distribution(0.5, 1.5);
+
+ for (int i = 0; i < SampleCount; ++i) {
+ Digest_->AddSample(distribution(generator));
+ }
+
+ EXPECT_NEAR(Digest_->GetQuantile(1.0), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.75), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.5), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.25), 1.0, Epsilon);
+ EXPECT_NEAR(Digest_->GetQuantile(0.0), 1.0, Epsilon);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/dnf_ut.cpp b/yt/yt/core/misc/unittests/dnf_ut.cpp
new file mode 100644
index 0000000000..df007eb379
--- /dev/null
+++ b/yt/yt/core/misc/unittests/dnf_ut.cpp
@@ -0,0 +1,80 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/dnf.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+using namespace NYson;
+
+TEST(TDnfTest, Conjunctions)
+{
+ TConjunctiveClause clause;
+ EXPECT_TRUE(clause.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_TRUE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+
+ clause = TConjunctiveClause({"aaa"}, {});
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_TRUE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+
+ clause = TConjunctiveClause({"aaa", "bbb"}, {});
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+ EXPECT_TRUE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb")})));
+
+ clause = TConjunctiveClause({"aaa", "bbb"}, {"ccc"});
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+ EXPECT_TRUE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb")})));
+ EXPECT_FALSE(clause.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb"), TString("ccc")})));
+}
+
+TEST(TDnfTest, Dnf)
+{
+ TDnfFormula dnf;
+
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+
+ dnf.Clauses().push_back(TConjunctiveClause({"aaa", "bbb"}, {"ccc"}));
+
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+ EXPECT_TRUE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb")})));
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb"), TString("ccc")})));
+
+ dnf.Clauses().push_back(TConjunctiveClause({"ccc"}, {}));
+
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>()));
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa")})));
+ EXPECT_TRUE(dnf.IsSatisfiedBy(std::vector<TString>({TString("aaa"), TString("bbb")})));
+ EXPECT_TRUE(dnf.IsSatisfiedBy(std::vector<TString>({TString("ccc")})));
+ EXPECT_FALSE(dnf.IsSatisfiedBy(std::vector<TString>({TString("bbb")})));
+}
+
+TEST(TDnfTest, Serialization)
+{
+ auto clause = TConjunctiveClause({"aaa", "bbb"}, {"ccc"});
+ auto conjunctionString = ConvertToYsonString(clause, EYsonFormat::Text);
+ EXPECT_EQ("{\"include\"=[\"aaa\";\"bbb\";];\"exclude\"=[\"ccc\";];}", conjunctionString.AsStringBuf());
+ EXPECT_EQ(clause, ConvertTo<TConjunctiveClause>(conjunctionString));
+
+ auto dnf = TDnfFormula({
+ TConjunctiveClause({"aaa", "bbb"}, {"ccc"}),
+ TConjunctiveClause({"ccc"}, {})});
+ auto dnfString = ConvertToYsonString(dnf, EYsonFormat::Text);
+ EXPECT_EQ("[{\"include\"=[\"aaa\";\"bbb\";];\"exclude\"=[\"ccc\";];};{\"include\"=[\"ccc\";];\"exclude\"=[];};]", dnfString.AsStringBuf());
+ EXPECT_EQ(dnf, ConvertTo<TDnfFormula>(dnfString));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/ema_counter_ut.cpp b/yt/yt/core/misc/unittests/ema_counter_ut.cpp
new file mode 100644
index 0000000000..667a9b6c0d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/ema_counter_ut.cpp
@@ -0,0 +1,139 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/ema_counter.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TEmaCounterTest, Simple)
+{
+ const auto min = TDuration::Minutes(1);
+
+ TEmaCounter counter({min});
+
+ EXPECT_EQ(std::nullopt, counter.LastTimestamp);
+ EXPECT_EQ(std::nullopt, counter.StartTimestamp);
+ EXPECT_EQ(0, counter.Count);
+ EXPECT_EQ(0.0, counter.ImmediateRate);
+ EXPECT_EQ(0.0, counter.WindowRates[0]);
+
+ counter.Update(10, TInstant::Zero());
+
+ EXPECT_EQ(TInstant::Zero(), counter.LastTimestamp);
+ EXPECT_EQ(TInstant::Zero(), counter.StartTimestamp);
+ EXPECT_EQ(10, counter.Count);
+ // Still no information about rates.
+ EXPECT_EQ(0.0, counter.ImmediateRate);
+ EXPECT_EQ(0.0, counter.WindowRates[0]);
+
+ counter.Update(20, TInstant::Zero() + min);
+
+ EXPECT_EQ(TInstant::Zero() + min, counter.LastTimestamp);
+ EXPECT_EQ(TInstant::Zero(), counter.StartTimestamp);
+ EXPECT_EQ(20, counter.Count);
+ EXPECT_DOUBLE_EQ(10.0 / 60.0, counter.ImmediateRate);
+ // New rate should be considered with weight 1 - e^{-2}, new one with 1/e^{-2}.
+ EXPECT_DOUBLE_EQ(10.0 / 60.0 * (1 - std::exp(-2)) + 0.0 * std::exp(-2), counter.WindowRates[0]);
+}
+
+TEST(TEmaCounterTest, MockTime)
+{
+ const auto sec = TDuration::Seconds(1), min = TDuration::Minutes(1);
+
+ TEmaCounter counter({min});
+
+ int obsoleteRate = 1;
+ int actualRate = 10;
+
+ // Set up some history.
+
+ i64 currentCount = 0;
+ TInstant currentTimestamp = TInstant::Zero();
+
+ for (int index = 0; index < 300; ++index, currentTimestamp += sec) {
+ currentCount += obsoleteRate;
+ counter.Update(currentCount, currentTimestamp);
+
+ if (index < 60) {
+ EXPECT_FALSE(counter.GetRate(0, TInstant::Zero() + index * sec));
+ }
+ }
+
+ EXPECT_DOUBLE_EQ(1.0, counter.ImmediateRate);
+ // Result should be almost 1 (recall that the initial rate value of 0
+ // is remembered by EMA for some time).
+ EXPECT_NEAR(1.0, counter.WindowRates[0], 1e-3);
+ EXPECT_TRUE(counter.GetRate(0, currentTimestamp));
+
+ for (int index = 300; index < 360; ++index, currentTimestamp += sec) {
+ currentCount += actualRate;
+ counter.Update(currentCount, currentTimestamp);
+ }
+
+ EXPECT_DOUBLE_EQ(10.0, counter.ImmediateRate);
+ // Actual value would be 8.78, which is quite close to 10.0.
+ EXPECT_NEAR(10.0, counter.WindowRates[0], 2.0);
+ EXPECT_TRUE(counter.GetRate(0, currentTimestamp));
+
+ for (int index = 360; index < 420; ++index, currentTimestamp += sec) {
+ currentCount += actualRate;
+ counter.Update(currentCount, currentTimestamp);
+ }
+
+ EXPECT_DOUBLE_EQ(10.0, counter.ImmediateRate);
+ // Actual value would be 9.83, which is notably close to 10.0.
+ EXPECT_NEAR(10.0, counter.WindowRates[0], 0.2);
+ EXPECT_TRUE(counter.GetRate(0, currentTimestamp));
+}
+
+TEST(TEmaCounterTest, RealTime)
+{
+ const auto quant = TDuration::MilliSeconds(10), sec = TDuration::Seconds(1);
+
+ TEmaCounter counter({sec});
+
+ const int valueCount = 200;
+ std::mt19937 generator(/*seed*/ 42);
+ const int maxValue = 200'000;
+ std::uniform_int_distribution<int> valueDistribution(0, maxValue);
+ std::vector<i64> values;
+ values.reserve(valueCount);
+ for (int index = 0; index < valueCount; ++index) {
+ values.push_back(valueDistribution(generator));
+ }
+ std::sort(values.begin(), values.end());
+
+ auto start = TInstant::Now();
+
+ for (int index = 0; index < valueCount; ++index) {
+ counter.Update(values[index]);
+ Sleep(quant);
+ if (TInstant::Now() - start < sec * 0.9) {
+ EXPECT_FALSE(counter.GetRate(0));
+ }
+ }
+
+ auto end = TInstant::Now();
+
+ auto testDuration = end - start;
+ auto expectedRate = counter.Count / (testDuration).SecondsFloat();
+
+ const double relativeTolerance = 0.2;
+
+ Cerr << "Test duration = " << testDuration << " sec" << Endl;
+ Cerr << "Expected rate = " << expectedRate << Endl;
+ Cerr << "Window rate = " << counter.WindowRates[0] << Endl;
+ Cerr << "Relative error = " << counter.WindowRates[0] / expectedRate - 1.0 << Endl;
+
+ EXPECT_NEAR(1, counter.WindowRates[0] / expectedRate, relativeTolerance);
+ EXPECT_TRUE(counter.GetRate(0));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/enum_ut.cpp b/yt/yt/core/misc/unittests/enum_ut.cpp
new file mode 100644
index 0000000000..44d55436f9
--- /dev/null
+++ b/yt/yt/core/misc/unittests/enum_ut.cpp
@@ -0,0 +1,47 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EColor,
+ ((Red) (10))
+ ((Green)(20))
+ ((Blue) (30))
+ (Black)
+ (White)
+);
+
+TEST(TEnumTest, SaveAndLoad)
+{
+ TStringStream stream;
+ TStreamSaveContext saveContext(&stream);
+ TStreamLoadContext loadContext(&stream);
+
+ auto first = EColor::Red;
+ auto second = EColor::Black;
+ auto third = EColor(0);
+ auto fourth = EColor(0);
+
+ Save(saveContext, first);
+ Save(saveContext, second);
+
+ saveContext.Finish();
+
+ Load(loadContext, third);
+ Load(loadContext, fourth);
+
+ EXPECT_EQ(first, third);
+ EXPECT_EQ(second, fourth);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/error_code_ut.cpp b/yt/yt/core/misc/unittests/error_code_ut.cpp
new file mode 100644
index 0000000000..4670966cd1
--- /dev/null
+++ b/yt/yt/core/misc/unittests/error_code_ut.cpp
@@ -0,0 +1,120 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/error_code.h>
+
+#include <library/cpp/yt/string/format.h>
+
+#include <ostream>
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((Global1) (-5))
+ ((Global2) (-6))
+);
+
+namespace NExternalWorld {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((X) (-11))
+ ((Y) (-22))
+ ((Z) (-33))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NExternalWorld
+
+namespace NYT {
+
+void PrintTo(const TErrorCodeRegistry::TErrorCodeInfo& errorCodeInfo, std::ostream* os)
+{
+ *os << ToString(errorCodeInfo);
+}
+
+namespace NInternalLittleWorld {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((A) (-1))
+ ((B) (-2))
+ ((C) (-3))
+ ((D) (-4))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NMyOwnLittleWorld
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((Kek) (-57))
+ ((Haha) (-179))
+ ((Muahaha) (-1543))
+ ((Kukarek) (-2007))
+);
+
+TString TestErrorCodeFormatter(int code)
+{
+ return Format("formatted%v", code);
+}
+
+YT_DEFINE_ERROR_CODE_RANGE(-4399, -4200, "NYT::Test", TestErrorCodeFormatter);
+
+DEFINE_ENUM(EDifferentTestErrorCode,
+ ((ErrorNumberOne) (-10000))
+ ((ErrorNumberTwo) (-10001))
+ ((ErrorNumberThree) (-10002))
+);
+
+TString DifferentTestErrorCodeFormatter(int code)
+{
+ return TEnumTraits<EDifferentTestErrorCode>::ToString(static_cast<EDifferentTestErrorCode>(code));
+}
+
+YT_DEFINE_ERROR_CODE_RANGE(-10005, -10000, "NYT::DifferentTest", DifferentTestErrorCodeFormatter);
+
+TEST(TErrorCodeRegistryTest, Basic)
+{
+#ifdef _unix_
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-1543),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::(anonymous namespace)", "Muahaha"}));
+#else
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-1543),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::`anonymous namespace'", "Muahaha"}));
+#endif
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-3),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::NInternalLittleWorld", "C"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-33),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NExternalWorld", "Z"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-5),
+ (TErrorCodeRegistry::TErrorCodeInfo{"", "Global1"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-4300),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::Test", "formatted-4300"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-10002),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::DifferentTest", "ErrorNumberThree"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-10005),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NYT::DifferentTest", "EDifferentTestErrorCode(-10005)"}));
+ EXPECT_EQ(
+ TErrorCodeRegistry::Get()->Get(-111),
+ (TErrorCodeRegistry::TErrorCodeInfo{"NUnknown", "ErrorCode-111"}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/error_ut.cpp b/yt/yt/core/misc/unittests/error_ut.cpp
new file mode 100644
index 0000000000..53bd4d434c
--- /dev/null
+++ b/yt/yt/core/misc/unittests/error_ut.cpp
@@ -0,0 +1,219 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <util/stream/str.h>
+#include <util/string/join.h>
+#include <util/string/split.h>
+
+namespace NYT {
+namespace {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TErrorTest, SerializationDepthLimit)
+{
+ constexpr int Depth = 1000;
+ auto error = TError(TErrorCode(Depth), "error");
+ for (int i = Depth - 1; i >= 0; --i) {
+ error = TError(TErrorCode(i), "error") << std::move(error);
+ }
+
+ // Use intermediate conversion to test YSON parser depth limit simultaneously.
+ auto errorYson = ConvertToYsonString(error);
+ auto errorNode = ConvertTo<IMapNodePtr>(errorYson);
+
+ for (int i = 0; i < ErrorSerializationDepthLimit - 1; ++i) {
+ ASSERT_EQ(errorNode->GetChildValueOrThrow<i64>("code"), i);
+ ASSERT_EQ(errorNode->GetChildValueOrThrow<TString>("message"), "error");
+ ASSERT_FALSE(errorNode->GetChildOrThrow("attributes")->AsMap()->FindChild("original_error_depth"));
+ auto innerErrors = errorNode->GetChildOrThrow("inner_errors")->AsList()->GetChildren();
+ ASSERT_EQ(innerErrors.size(), 1u);
+ errorNode = innerErrors[0]->AsMap();
+ }
+ auto innerErrors = errorNode->GetChildOrThrow("inner_errors")->AsList();
+ const auto& children = innerErrors->GetChildren();
+ ASSERT_EQ(std::ssize(children), Depth - ErrorSerializationDepthLimit + 1);
+ for (int i = 0; i < std::ssize(children); ++i) {
+ auto child = children[i]->AsMap();
+ ASSERT_EQ(child->GetChildValueOrThrow<i64>("code"), i + ErrorSerializationDepthLimit);
+ ASSERT_EQ(child->GetChildValueOrThrow<TString>("message"), "error");
+ auto originalErrorDepth = child->GetChildOrThrow("attributes")->AsMap()->FindChild("original_error_depth");
+ if (i > 0) {
+ ASSERT_TRUE(originalErrorDepth);
+ ASSERT_EQ(originalErrorDepth->GetValue<i64>(), i + ErrorSerializationDepthLimit);
+ } else {
+ ASSERT_FALSE(originalErrorDepth);
+ }
+ }
+}
+
+TEST(TErrorTest, ErrorSkeletonStubImplementation)
+{
+ TError error("foo");
+ EXPECT_THROW(error.GetSkeleton(), std::exception);
+}
+
+TEST(TErrorTest, FormatCtor)
+{
+ EXPECT_EQ("Some error %v", TError("Some error %v").GetMessage());
+ EXPECT_EQ("Some error hello", TError("Some error %v", "hello").GetMessage());
+}
+
+TEST(TErrorTest, TruncateSimple)
+{
+ auto error = TError("Some error")
+ << TErrorAttribute("my_attr", "Attr value");
+ auto truncatedError = error.Truncate();
+ EXPECT_EQ(error.GetCode(), truncatedError.GetCode());
+ EXPECT_EQ(error.GetMessage(), truncatedError.GetMessage());
+ EXPECT_EQ(error.GetPid(), truncatedError.GetPid());
+ EXPECT_EQ(error.GetTid(), truncatedError.GetTid());
+ EXPECT_EQ(error.GetSpanId(), truncatedError.GetSpanId());
+ EXPECT_EQ(error.GetDatetime(), truncatedError.GetDatetime());
+ EXPECT_EQ(error.Attributes().Get<TString>("my_attr"), truncatedError.Attributes().Get<TString>("my_attr"));
+}
+
+TEST(TErrorTest, TruncateLarge)
+{
+ auto error = TError("Some long long error");
+ error.MutableAttributes()->Set("my_attr", "Some long long attr");
+
+ auto truncatedError = error.Truncate(/*maxInnerErrorCount*/ 2, /*stringLimit*/ 10);
+ EXPECT_EQ(error.GetCode(), truncatedError.GetCode());
+ EXPECT_EQ("Some long ...<message truncated>", truncatedError.GetMessage());
+ EXPECT_EQ("...<attribute truncated>...", truncatedError.Attributes().Get<TString>("my_attr"));
+}
+
+TEST(TErrorTest, YTExceptionToError)
+{
+ try {
+ throw TSimpleException("message");
+ } catch (const std::exception& ex) {
+ TError error(ex);
+ EXPECT_EQ(NYT::EErrorCode::Generic, error.GetCode());
+ EXPECT_EQ("message", error.GetMessage());
+ }
+}
+
+TEST(TErrorTest, CompositeYTExceptionToError)
+{
+ try {
+ try {
+ throw TSimpleException("inner message");
+ } catch (const std::exception& ex) {
+ throw TCompositeException(ex, "outer message");
+ }
+ } catch (const std::exception& ex) {
+ TError outerError(ex);
+ EXPECT_EQ(NYT::EErrorCode::Generic, outerError.GetCode());
+ EXPECT_EQ("outer message", outerError.GetMessage());
+ EXPECT_EQ(1, std::ssize(outerError.InnerErrors()));
+ const auto& innerError = outerError.InnerErrors()[0];
+ EXPECT_EQ(NYT::EErrorCode::Generic, innerError.GetCode());
+ EXPECT_EQ("inner message", innerError.GetMessage());
+ }
+}
+
+TEST(TErrorTest, ErrorSanitizer)
+{
+ auto checkSantizied = [&] (const TError& error) {
+ EXPECT_FALSE(error.HasOriginAttributes());
+ EXPECT_FALSE(error.HasTracingAttributes());
+
+ EXPECT_EQ("", error.GetHost());
+ EXPECT_EQ(0, error.GetPid());
+ EXPECT_EQ(NThreading::InvalidThreadId, error.GetTid());
+ EXPECT_EQ(NConcurrency::InvalidFiberId, error.GetFid());
+ EXPECT_EQ(NTracing::InvalidTraceId, error.GetTraceId());
+ EXPECT_EQ(NTracing::InvalidSpanId, error.GetSpanId());
+ };
+
+ auto checkNotSanitized = [&] (const TError& error) {
+ EXPECT_TRUE(error.HasOriginAttributes());
+
+ EXPECT_FALSE(error.GetHost() == "");
+ EXPECT_FALSE(error.GetPid() == 0);
+
+ auto now = TInstant::Now();
+ EXPECT_GE(error.GetDatetime() + TDuration::Minutes(1), now);
+ };
+
+ auto error1 = TError("error1");
+ checkNotSanitized(error1);
+
+ {
+ auto instant1 = TInstant::Days(123);
+ TErrorSanitizerGuard guard1(instant1);
+
+ auto error2 = TError("error2");
+ checkSantizied(error2);
+ EXPECT_EQ(instant1, error2.GetDatetime());
+
+ {
+ auto instant2 = TInstant::Days(234);
+ TErrorSanitizerGuard guard2(instant2);
+
+ auto error3 = TError("error3");
+ checkSantizied(error3);
+ EXPECT_EQ(instant2, error3.GetDatetime());
+ }
+
+ auto error4 = TError("error4");
+ checkSantizied(error4);
+ EXPECT_EQ(instant1, error4.GetDatetime());
+ }
+
+ auto error5 = TError("error5");
+ checkNotSanitized(error5);
+}
+
+TEST(TErrorTest, SimpleLoadAfterSave)
+{
+ TStringStream stream;
+
+ TStreamSaveContext saveContext(&stream);
+ TError savedError("error");
+ savedError.Save(saveContext);
+
+ TStreamLoadContext loadContext(&stream);
+ TError loadedError;
+ loadedError.Load(loadContext);
+
+ EXPECT_EQ(ToString(savedError), ToString(loadedError));
+}
+
+TEST(TErrorTest, AttributeSerialization)
+{
+ auto getWeededText = [](const TError& err) {
+ std::vector<TString> lines;
+ for (const auto& line : StringSplitter(ToString(err)).Split('\n')) {
+ if (!line.Contains("origin") && !line.Contains("datetime")) {
+ lines.push_back(TString{line});
+ }
+ }
+ return JoinSeq("\n", lines);
+ };
+
+ EXPECT_EQ(getWeededText(TError("E1") << TErrorAttribute("A1", "V1")), TString(
+ "E1\n"
+ " A1 V1\n"));
+ EXPECT_EQ(getWeededText(TError("E1") << TErrorAttribute("A1", "L1\nL2\nL3")), TString(
+ "E1\n"
+ " A1\n"
+ " L1\n"
+ " L2\n"
+ " L3\n"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/fair_scheduler_ut.cpp b/yt/yt/core/misc/unittests/fair_scheduler_ut.cpp
new file mode 100644
index 0000000000..4e517cd22d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/fair_scheduler_ut.cpp
@@ -0,0 +1,78 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/fair_scheduler.h>
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFairSchedulerTest
+ : public ::testing::Test
+{
+protected:
+ IFairSchedulerPtr<TString> Scheduler_ = CreateFairScheduler<TString>();
+};
+
+TEST_F(TFairSchedulerTest, Simple)
+{
+ Scheduler_->Enqueue("T1", "John");
+ Scheduler_->Enqueue("T2", "John");
+
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "T1");
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "T2");
+ EXPECT_TRUE(Scheduler_->IsEmpty());
+}
+
+TEST_F(TFairSchedulerTest, Fairness1)
+{
+ Scheduler_->ChargeUser("Bob", TDuration::Seconds(1));
+ Scheduler_->Enqueue("A1", "Alice");
+ Scheduler_->Enqueue("A2", "Alice");
+ Scheduler_->Enqueue("B1", "Bob");
+ Scheduler_->Enqueue("B2", "Bob");
+
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "A1");
+ Scheduler_->ChargeUser("Alice", TDuration::Seconds(2));
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "B1");
+ Scheduler_->ChargeUser("Bob", TDuration::Seconds(2));
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "A2");
+ Scheduler_->ChargeUser("Alice", TDuration::Seconds(2));
+ EXPECT_FALSE(Scheduler_->IsEmpty());
+ EXPECT_EQ(Scheduler_->Dequeue(), "B2");
+ Scheduler_->ChargeUser("Bob", TDuration::Seconds(2));
+ EXPECT_TRUE(Scheduler_->IsEmpty());
+}
+
+TEST_F(TFairSchedulerTest, Fairness2)
+{
+ Scheduler_->ChargeUser("Bob", TDuration::Seconds(1));
+ for (int index = 1; index <= 10; ++index) {
+ Scheduler_->Enqueue(Format("A%v", index), "Alice");
+ Scheduler_->Enqueue(Format("B%v", index), "Bob");
+ }
+
+ EXPECT_EQ(Scheduler_->Dequeue(), "A1");
+ Scheduler_->ChargeUser("Alice", TDuration::Seconds(100500));
+
+ for (int index = 1; index <= 10; ++index) {
+ EXPECT_EQ(Scheduler_->Dequeue(), Format("B%v", index));
+ Scheduler_->ChargeUser("Bob", TDuration::Seconds(1));
+ }
+
+ for (int index = 2; index <= 10; ++index) {
+ EXPECT_EQ(Scheduler_->Dequeue(), Format("A%v", index));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/fenwick_tree_ut.cpp b/yt/yt/core/misc/unittests/fenwick_tree_ut.cpp
new file mode 100644
index 0000000000..c05552fd09
--- /dev/null
+++ b/yt/yt/core/misc/unittests/fenwick_tree_ut.cpp
@@ -0,0 +1,191 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/fenwick_tree.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/random.h>
+
+#include <algorithm>
+#include <vector>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFenwickTreeTest, TPushBackTest)
+{
+ TFenwickTree<int> tree;
+
+ for (int i = 0; i < 10; ++i) {
+ tree.PushBack(i + 1);
+ }
+ EXPECT_EQ(tree.Size(), 10);
+
+ for (int i = 0; i <= tree.Size(); ++i) {
+ EXPECT_EQ(i * (i + 1) / 2, tree.GetCumulativeSum(i));
+ }
+
+ for (int i = 0; i < 5; ++i) {
+ tree.PopBack();
+ }
+ EXPECT_EQ(tree.Size(), 5);
+
+ for (int i = 0; i <= tree.Size(); ++i) {
+ EXPECT_EQ(i * (i + 1) / 2, tree.GetCumulativeSum(i));
+ }
+}
+
+TEST(TFenwickTreeTest, TSetValueTest)
+{
+ TFenwickTree<i64> tree;
+
+ for (int i = 0; i < 10; ++i) {
+ tree.EmplaceBack();
+ }
+
+ for (int i = 0; i <= tree.Size(); ++i) {
+ EXPECT_EQ(0, tree.GetCumulativeSum(i));
+ }
+
+ for (int i = 0; i < tree.Size(); ++i) {
+ // Setting values in pseudo-random order. Note that gcd(3, 10) = 1.
+ int index = (i * 3) % tree.Size();
+
+ tree.SetValue(index, index + 1);
+ }
+
+ for (int i = 0; i <= tree.Size(); ++i) {
+ EXPECT_EQ(i * (i + 1) / 2, tree.GetCumulativeSum(i));
+ }
+}
+
+TEST(TFenwickTreeTest, TLowerBoundTest1)
+{
+ TFenwickTree<int> tree;
+
+ tree.PushBack(1);
+ tree.PushBack(2);
+ tree.PushBack(0);
+ tree.PushBack(0);
+ tree.PushBack(5);
+
+ EXPECT_EQ(0, tree.LowerBound(0));
+ EXPECT_EQ(1, tree.LowerBound(1));
+ EXPECT_EQ(2, tree.LowerBound(2));
+ EXPECT_EQ(2, tree.LowerBound(3));
+ EXPECT_EQ(5, tree.LowerBound(4));
+ EXPECT_EQ(5, tree.LowerBound(5));
+ EXPECT_EQ(5, tree.LowerBound(6));
+ EXPECT_EQ(5, tree.LowerBound(7));
+ EXPECT_EQ(5, tree.LowerBound(8));
+ EXPECT_EQ(6, tree.LowerBound(9));
+ EXPECT_EQ(6, tree.LowerBound(10000));
+}
+
+TEST(TFenwickTreeTest, TLowerBoundTest2)
+{
+ TFenwickTree<int> tree;
+ std::vector<int> prefixSums{0};
+
+ TRandomGenerator generator;
+
+ for (int i = 0; i < 16; ++i) {
+ int value = generator.Generate<ui32>() % 100;
+ tree.PushBack(value);
+ prefixSums.push_back(prefixSums.back() + value);
+ }
+
+ for (int i = 0; i < 1600; ++i) {
+ EXPECT_EQ(tree.LowerBound(i), std::lower_bound(prefixSums.begin(), prefixSums.end(), i) - prefixSums.begin());
+ }
+}
+
+TEST(TFenwickTreeTest, TUpperBoundTest)
+{
+ TFenwickTree<int> tree;
+ std::vector<int> prefixSums{0};
+
+ TRandomGenerator generator;
+
+ for (int i = 0; i < 16; ++i) {
+ int value = generator.Generate<ui32>() % 100;
+ tree.PushBack(value);
+ prefixSums.push_back(prefixSums.back() + value);
+ }
+
+ for (int i = 0; i < 1600; ++i) {
+ EXPECT_EQ(tree.UpperBound(i), std::upper_bound(prefixSums.begin(), prefixSums.end(), i) - prefixSums.begin());
+ }
+}
+
+TEST(TFenwickTreeTest, TFloatTest)
+{
+ TFenwickTree<double> tree;
+
+ tree.PushBack(0.5);
+ tree.PushBack(0.1);
+ tree.PushBack(0.4);
+
+ EXPECT_FLOAT_EQ(tree.GetCumulativeSum(0), 0.0);
+ EXPECT_FLOAT_EQ(tree.GetCumulativeSum(1), 0.5);
+ EXPECT_FLOAT_EQ(tree.GetCumulativeSum(2), 0.6);
+ EXPECT_FLOAT_EQ(tree.GetCumulativeSum(3), 1.0);
+}
+
+TEST(TFenwickTreeTest, TCustomStructureTest)
+{
+ struct TItem
+ {
+ int X = 0;
+ int Y = 0;
+
+ TItem() {}
+
+ TItem(int x, int y)
+ : X(x)
+ , Y(y)
+ { }
+
+ TItem operator+(const TItem& other) const
+ {
+ return {X + other.X, Y + other.Y};
+ }
+
+ bool operator==(const TItem& other) const
+ {
+ return std::tie(X, Y) == std::tie(other.X, other.Y);
+ }
+ };
+
+ TFenwickTree<TItem> tree;
+
+ tree.PushBack({1, 1});
+ tree.EmplaceBack(2, 2);
+
+ EXPECT_EQ(tree.GetCumulativeSum(2), TItem(3, 3));
+
+ auto cmp = [](TItem lhs, TItem rhs) {
+ return std::tie(lhs.X, lhs.Y) < std::tie(rhs.X, rhs.Y);
+ };
+
+ EXPECT_EQ(tree.LowerBound(TItem(1, 1), cmp), 1);
+ EXPECT_EQ(tree.UpperBound(TItem(1, 1), cmp), 2);
+
+ auto cmpLower = [](TItem lhs, int rhs) {
+ return lhs.X < rhs;
+ };
+
+ EXPECT_EQ(tree.LowerBound(1, cmpLower), 1);
+
+ auto cmpUpper = [](int rhs, TItem lhs) {
+ return rhs < lhs.X;
+ };
+
+ EXPECT_EQ(tree.UpperBound(1, cmpUpper), 2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/finally_ut.cpp b/yt/yt/core/misc/unittests/finally_ut.cpp
new file mode 100644
index 0000000000..4157bd6579
--- /dev/null
+++ b/yt/yt/core/misc/unittests/finally_ut.cpp
@@ -0,0 +1,57 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFinallyTest, Common)
+{
+ bool executed = false;
+ {
+ auto f = Finally(
+ [&] {
+ executed = true;
+ });
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+TEST(TFinallyTest, Exception)
+{
+ bool executed = false;
+ try {
+ auto f = Finally(
+ [&] {
+ executed = true;
+ });
+ EXPECT_FALSE(executed);
+ throw 1;
+ } catch (...) {
+ EXPECT_TRUE(executed);
+ }
+}
+
+TEST(TFinallyTest, Move)
+{
+ int executedCounter = 0;
+ {
+ auto f = Finally(
+ [&] {
+ ++executedCounter;
+ });
+ {
+ auto f2 = std::move(f);
+ }
+ EXPECT_EQ(1, executedCounter);
+ }
+ EXPECT_EQ(1, executedCounter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/format_ut.cpp b/yt/yt/core/misc/unittests/format_ut.cpp
new file mode 100644
index 0000000000..cfbce43656
--- /dev/null
+++ b/yt/yt/core/misc/unittests/format_ut.cpp
@@ -0,0 +1,17 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static_assert(!TFormatTraits<TIntrusivePtr<TRefCounted>>::HasCustomFormatValue);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/fs_ut.cpp b/yt/yt/core/misc/unittests/fs_ut.cpp
new file mode 100644
index 0000000000..8808b43598
--- /dev/null
+++ b/yt/yt/core/misc/unittests/fs_ut.cpp
@@ -0,0 +1,87 @@
+#include "gtest/gtest.h"
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <util/folder/dirut.h>
+
+namespace NYT::NFS {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TFSTest, TestGetRealPath)
+{
+ auto cwd = NFs::CurrentWorkingDirectory();
+ auto rootPrefix = cwd.substr(0, cwd.find(LOCSLASH_C));
+ EXPECT_EQ(CombinePaths(cwd, "dir"), GetRealPath("dir"));
+ EXPECT_EQ(cwd, GetRealPath("dir/.."));
+ EXPECT_EQ(CombinePaths(cwd, "dir"), GetRealPath("dir/./a/b/../../."));
+ EXPECT_EQ(GetRealPath("/a"), rootPrefix + NormalizePathSeparators("/a"));
+ EXPECT_EQ(GetRealPath("/a/b"), rootPrefix + NormalizePathSeparators("/a/b"));
+ EXPECT_EQ(GetRealPath("/a/b/c/.././../d/."), rootPrefix + NormalizePathSeparators("/a/d"));
+}
+
+TEST(TFSTest, TestGetDirectoryName)
+{
+ auto cwd = NFs::CurrentWorkingDirectory();
+ EXPECT_EQ(GetDirectoryName("/a/b/c"), NormalizePathSeparators("/a/b"));
+ EXPECT_EQ(GetDirectoryName("a/b/c"), NormalizePathSeparators(cwd + "/a/b"));
+ EXPECT_EQ(GetDirectoryName("."), NormalizePathSeparators(cwd));
+ EXPECT_EQ(GetDirectoryName("/"), NormalizePathSeparators("/"));
+ EXPECT_EQ(GetDirectoryName("/a"), NormalizePathSeparators("/"));
+}
+
+TEST(TFSTest, TestIsDirEmpty)
+{
+ auto cwd = NFs::CurrentWorkingDirectory();
+ auto dir = CombinePaths(cwd, "test");
+ MakeDirRecursive(dir);
+ EXPECT_TRUE(IsDirEmpty(dir));
+ MakeDirRecursive(CombinePaths(dir, "nested"));
+ EXPECT_FALSE(IsDirEmpty(dir));
+ RemoveRecursive(dir);
+}
+
+TEST(TFSTest, TestIsPathRelativeAndInvolvesNoTraversal)
+{
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal(""));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("some"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("some/file"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("."));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("./some/file"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("./some/./file"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("./some/.."));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("a/../b"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("a/./../b"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("some/"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("some//"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("a//b"));
+ EXPECT_TRUE(NFS::IsPathRelativeAndInvolvesNoTraversal("a/b/"));
+
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("/"));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("//"));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("/some"));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal(".."));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("../some"));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("a/../.."));
+ EXPECT_FALSE(NFS::IsPathRelativeAndInvolvesNoTraversal("a/../../b"));
+}
+
+TEST(TFSTest, TestGetRelativePath)
+{
+ EXPECT_EQ(GetRelativePath("/a", "/a/b"), "b");
+ EXPECT_EQ(GetRelativePath("/a/b", "/a"), "..");
+ EXPECT_EQ(GetRelativePath("/a/b/c", "/d/e"), NormalizePathSeparators("../../../d/e"));
+ EXPECT_EQ(GetRelativePath("/a/b/c/d", "/a/b"), NormalizePathSeparators("../.."));
+ EXPECT_EQ(GetRelativePath("/a/b/c/d/e", "/a/b/c/f/g/h"), NormalizePathSeparators("../../f/g/h"));
+ EXPECT_EQ(GetRelativePath("a/b/c", "d/e"), NormalizePathSeparators("../../../d/e"));
+ EXPECT_EQ(GetRelativePath("/a/b", "/a/b"), ".");
+
+ EXPECT_EQ(GetRelativePath(CombinePaths(NFs::CurrentWorkingDirectory(), "dir")), "dir");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NFS
diff --git a/yt/yt/core/misc/unittests/future_ut.cpp b/yt/yt/core/misc/unittests/future_ut.cpp
new file mode 100644
index 0000000000..2c7666c5ca
--- /dev/null
+++ b/yt/yt/core/misc/unittests/future_ut.cpp
@@ -0,0 +1,1614 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/cancelable_context.h>
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/misc/ref_counted_tracker.h>
+#include <yt/yt/core/misc/mpsc_stack.h>
+
+#include <util/system/thread.h>
+
+#include <thread>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto SleepQuantum = TDuration::MilliSeconds(50);
+
+class TFutureTest
+ : public ::testing::Test
+{ };
+
+TEST_F(TFutureTest, NoncopyableGet)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ EXPECT_TRUE(f.IsSet());
+ EXPECT_TRUE(f.Get().IsOK());
+ EXPECT_EQ(1, *f.Get().Value());
+ auto result = f.GetUnique();
+ EXPECT_TRUE(result.IsOK());
+ EXPECT_EQ(1, *result.Value());
+}
+
+TEST_F(TFutureTest, NoncopyableApply1)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ auto g = f.ApplyUnique(BIND([] (TErrorOr<std::unique_ptr<int>>&& ptrOrError) {
+ EXPECT_TRUE(ptrOrError.IsOK());
+ EXPECT_EQ(1, *ptrOrError.Value());
+ }));
+ EXPECT_TRUE(g.IsSet());
+ EXPECT_TRUE(g.Get().IsOK());
+}
+
+TEST_F(TFutureTest, NoncopyableApply2)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ auto g = f.ApplyUnique(BIND([] (TErrorOr<std::unique_ptr<int>>&& ptrOrError) -> TErrorOr<int> {
+ EXPECT_TRUE(ptrOrError.IsOK());
+ EXPECT_EQ(1, *ptrOrError.Value());
+ return 2;
+ }));
+ EXPECT_TRUE(g.IsSet());
+ EXPECT_TRUE(g.Get().IsOK());
+ EXPECT_EQ(2, g.Get().Value());
+}
+
+TEST_F(TFutureTest, NoncopyableApply3)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ auto g = f.ApplyUnique(BIND([] (TErrorOr<std::unique_ptr<int>>&& ptrOrError) -> TFuture<int> {
+ EXPECT_TRUE(ptrOrError.IsOK());
+ EXPECT_EQ(1, *ptrOrError.Value());
+ return MakeFuture(2);
+ }));
+ EXPECT_TRUE(g.IsSet());
+ EXPECT_TRUE(g.Get().IsOK());
+ EXPECT_EQ(2, g.Get().Value());
+}
+
+TEST_F(TFutureTest, NoncopyableApply4)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ auto g = f.ApplyUnique(BIND([] (std::unique_ptr<int>&& ptr) {
+ EXPECT_EQ(1, *ptr);
+ }));
+ EXPECT_TRUE(g.IsSet());
+ EXPECT_TRUE(g.Get().IsOK());
+}
+
+TEST_F(TFutureTest, NoncopyableApply5)
+{
+ auto f = MakeFuture<std::unique_ptr<int>>(std::make_unique<int>(1));
+ auto g = f.ApplyUnique(BIND([] (std::unique_ptr<int>&& ptr) -> TFuture<int> {
+ EXPECT_EQ(1, *ptr);
+ return MakeFuture(2);
+ }));
+ EXPECT_TRUE(g.IsSet());
+ EXPECT_TRUE(g.Get().IsOK());
+ EXPECT_EQ(2, g.Get().Value());
+}
+
+TEST_F(TFutureTest, Unsubscribe)
+{
+ auto p = NewPromise<int>();
+ auto f = p.ToFuture();
+
+ bool f1 = false;
+ auto c1 = f.AsVoid().Subscribe(BIND([&] (const TError&) { f1 = true; }));
+
+ bool f2 = false;
+ auto c2 = f.Subscribe(BIND([&] (const TErrorOr<int>&) { f2 = true; }));
+
+ EXPECT_NE(c1, c2);
+
+ f.Unsubscribe(c1);
+ f.Unsubscribe(c2);
+
+ bool f3 = false;
+ auto c3 = f.AsVoid().Subscribe(BIND([&] (const TError&) { f3 = true; }));
+ EXPECT_EQ(c1, c3);
+
+ bool f4 = false;
+ auto c4 = f.Subscribe(BIND([&] (const TErrorOr<int>&) { f4 = true; }));
+ EXPECT_EQ(c2, c4);
+
+ p.Set(1);
+
+ EXPECT_FALSE(f1);
+ EXPECT_FALSE(f2);
+ EXPECT_TRUE(f3);
+ EXPECT_TRUE(f4);
+
+ bool f5 = false;
+ EXPECT_EQ(NullFutureCallbackCookie, f.Subscribe(BIND([&] (const TError&) { f5 = true; })));
+ EXPECT_TRUE(f5);
+
+ bool f6 = false;
+ EXPECT_EQ(NullFutureCallbackCookie, f.Subscribe(BIND([&] (const TErrorOr<int>&) { f6 = true; })));
+ EXPECT_TRUE(f6);
+}
+
+TEST_F(TFutureTest, IsNull)
+{
+ TFuture<int> empty;
+ TFuture<int> nonEmpty = MakeFuture(42);
+
+ EXPECT_FALSE(empty);
+ EXPECT_TRUE(nonEmpty);
+
+ empty = std::move(nonEmpty);
+
+ EXPECT_TRUE(empty);
+ EXPECT_FALSE(nonEmpty);
+
+ swap(empty, nonEmpty);
+
+ EXPECT_FALSE(empty);
+ EXPECT_TRUE(nonEmpty);
+}
+
+TEST_F(TFutureTest, IsNullVoid)
+{
+ TFuture<void> empty;
+ TFuture<void> nonEmpty = VoidFuture;
+
+ EXPECT_FALSE(empty);
+ EXPECT_TRUE(nonEmpty);
+
+ empty = std::move(nonEmpty);
+
+ EXPECT_TRUE(empty);
+ EXPECT_FALSE(nonEmpty);
+
+ swap(empty, nonEmpty);
+
+ EXPECT_FALSE(empty);
+ EXPECT_TRUE(nonEmpty);
+}
+
+TEST_F(TFutureTest, Reset)
+{
+ auto foo = MakeFuture(42);
+
+ EXPECT_TRUE(foo);
+ foo.Reset();
+ EXPECT_FALSE(foo);
+}
+
+TEST_F(TFutureTest, IsSet)
+{
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+
+ EXPECT_FALSE(future.IsSet());
+ EXPECT_FALSE(promise.IsSet());
+ promise.Set(42);
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(promise.IsSet());
+}
+
+TEST_F(TFutureTest, SetAndGet)
+{
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+
+ promise.Set(57);
+ EXPECT_EQ(57, future.Get().Value());
+ EXPECT_EQ(57, future.Get().Value()); // Second Get() should also work.
+}
+
+TEST_F(TFutureTest, SetAndTryGet)
+{
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+
+ {
+ auto result = future.TryGet();
+ EXPECT_FALSE(result);
+ }
+
+ promise.Set(42);
+
+ {
+ auto result = future.TryGet();
+ EXPECT_TRUE(result);
+ EXPECT_EQ(42, result->Value());
+ }
+}
+
+class TMock
+{
+public:
+ MOCK_METHOD(void, Tacke, (int), ());
+};
+
+TEST_F(TFutureTest, Subscribe)
+{
+ TMock firstMock;
+ TMock secondMock;
+
+ EXPECT_CALL(firstMock, Tacke(42)).Times(1);
+ EXPECT_CALL(secondMock, Tacke(42)).Times(1);
+
+ auto firstSubscriber = BIND([&] (const TErrorOr<int>& x) { firstMock.Tacke(x.Value()); });
+ auto secondSubscriber = BIND([&] (const TErrorOr<int>& x) { secondMock.Tacke(x.Value()); });
+
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+
+ future.Subscribe(firstSubscriber);
+ promise.Set(42);
+ future.Subscribe(secondSubscriber);
+}
+
+TEST_F(TFutureTest, GetUnique)
+{
+ auto promise = NewPromise<std::vector<int>>();
+ auto future = promise.ToFuture();
+
+ EXPECT_FALSE(future.IsSet());
+
+ std::vector v{1, 2, 3};
+ promise.Set(v);
+
+ EXPECT_TRUE(future.IsSet());
+ auto w = future.GetUnique();
+ EXPECT_TRUE(w.IsOK());
+ EXPECT_EQ(v, w.Value());
+ EXPECT_TRUE(future.IsSet());
+}
+
+TEST_F(TFutureTest, TryGetUnique)
+{
+ auto promise = NewPromise<std::vector<int>>();
+ auto future = promise.ToFuture();
+
+ EXPECT_FALSE(future.IsSet());
+ EXPECT_FALSE(future.TryGetUnique());
+
+ std::vector v{1, 2, 3};
+ promise.Set(v);
+
+ EXPECT_TRUE(future.IsSet());
+ auto w = future.TryGetUnique();
+ EXPECT_TRUE(w);
+ EXPECT_TRUE(w->IsOK());
+ EXPECT_EQ(v, w->Value());
+ EXPECT_TRUE(future.IsSet());
+}
+
+TEST_F(TFutureTest, SubscribeUniqueBeforeSet)
+{
+ std::vector v{1, 2, 3};
+
+ auto promise = NewPromise<std::vector<int>>();
+ auto future = promise.ToFuture();
+
+ std::vector<int> vv;
+ future.SubscribeUnique(BIND([&] (TErrorOr<std::vector<int>>&& arg) {
+ EXPECT_TRUE(arg.IsOK());
+ vv = std::move(arg.Value());
+ }));
+
+ EXPECT_FALSE(future.IsSet());
+ promise.Set(v);
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_EQ(v, vv);
+}
+
+TEST_F(TFutureTest, SubscribeUniqueAfterSet)
+{
+ std::vector v{1, 2, 3};
+
+ auto promise = NewPromise<std::vector<int>>();
+ auto future = promise.ToFuture();
+
+ EXPECT_FALSE(future.IsSet());
+ promise.Set(v);
+ EXPECT_TRUE(future.IsSet());
+
+ std::vector<int> vv;
+ future.SubscribeUnique(BIND([&] (TErrorOr<std::vector<int>>&& arg) {
+ EXPECT_TRUE(arg.IsOK());
+ vv = std::move(arg.Value());
+ }));
+
+ EXPECT_EQ(v, vv);
+ EXPECT_TRUE(future.IsSet());
+}
+
+static void* AsynchronousIntSetter(void* param)
+{
+ Sleep(SleepQuantum);
+
+ auto* promise = reinterpret_cast<TPromise<int>*>(param);
+ promise->Set(42);
+
+ return nullptr;
+}
+
+static void* AsynchronousVoidSetter(void* param)
+{
+ Sleep(SleepQuantum);
+
+ auto* promise = reinterpret_cast<TPromise<void>*>(param);
+ promise->Set();
+
+ return nullptr;
+}
+
+TEST_F(TFutureTest, SubscribeWithAsynchronousSet)
+{
+ TMock firstMock;
+ TMock secondMock;
+
+ EXPECT_CALL(firstMock, Tacke(42)).Times(1);
+ EXPECT_CALL(secondMock, Tacke(42)).Times(1);
+
+ auto firstSubscriber = BIND([&] (const TErrorOr<int>& x) { firstMock.Tacke(x.Value()); });
+ auto secondSubscriber = BIND([&] (const TErrorOr<int>& x) { secondMock.Tacke(x.Value()); });
+
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+
+ future.Subscribe(firstSubscriber);
+
+ ::TThread thread(&AsynchronousIntSetter, &promise);
+ thread.Start();
+ thread.Join();
+
+ future.Subscribe(secondSubscriber);
+}
+
+TEST_F(TFutureTest, CascadedApply)
+{
+ auto kicker = NewPromise<bool>();
+
+ auto left = NewPromise<int>();
+ auto right = NewPromise<int>();
+
+ ::TThread thread(&AsynchronousIntSetter, &left);
+
+ auto leftPrime =
+ kicker.ToFuture()
+ .Apply(BIND([=, &thread] (bool /*f*/) -> TFuture<int> {
+ thread.Start();
+ return left.ToFuture();
+ }))
+ .Apply(BIND([=] (int xv) -> int {
+ return xv + 8;
+ }));
+ auto rightPrime =
+ right.ToFuture()
+ .Apply(BIND([=] (int xv) -> TFuture<int> {
+ return MakeFuture(xv + 4);
+ }));
+
+ int accumulator = 0;
+ auto accumulate = BIND([&] (const TErrorOr<int>& x) { accumulator += x.Value(); });
+
+ leftPrime.Subscribe(accumulate);
+ rightPrime.Subscribe(accumulate);
+
+ // Ensure that thread was not started.
+ Sleep(SleepQuantum * 2);
+
+ // Initial computation condition.
+ EXPECT_FALSE(left.IsSet()); EXPECT_FALSE(leftPrime.IsSet());
+ EXPECT_FALSE(right.IsSet()); EXPECT_FALSE(rightPrime.IsSet());
+ EXPECT_EQ(0, accumulator);
+
+ // Kick off!
+ kicker.Set(true);
+ EXPECT_FALSE(left.IsSet()); EXPECT_FALSE(leftPrime.IsSet());
+ EXPECT_FALSE(right.IsSet()); EXPECT_FALSE(rightPrime.IsSet());
+ EXPECT_EQ(0, accumulator);
+
+ // Kick off!
+ right.Set(1);
+
+ EXPECT_FALSE(left.IsSet()); EXPECT_FALSE(leftPrime.IsSet());
+ EXPECT_TRUE(right.IsSet()); EXPECT_TRUE(rightPrime.IsSet());
+ EXPECT_EQ( 5, accumulator);
+ EXPECT_EQ( 1, right.Get().Value());
+ EXPECT_EQ( 5, rightPrime.Get().Value());
+
+ // This will sleep for a while until left branch will be evaluated.
+ thread.Join();
+
+ EXPECT_TRUE(left.IsSet()); EXPECT_TRUE(leftPrime.IsSet());
+ EXPECT_TRUE(right.IsSet()); EXPECT_TRUE(rightPrime.IsSet());
+ EXPECT_EQ(55, accumulator);
+ EXPECT_EQ(42, left.Get().Value());
+ EXPECT_EQ(50, leftPrime.Get().Value());
+}
+
+TEST_F(TFutureTest, ApplyVoidToVoid)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<void>();
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] () -> void { ++state; }));
+
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ kicker.Set();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+}
+
+TEST_F(TFutureTest, ApplyVoidToFutureVoid)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<void>();
+ auto setter = NewPromise<void>();
+
+ ::TThread thread(&AsynchronousVoidSetter, &setter);
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] () -> TFuture<void> {
+ ++state;
+ thread.Start();
+ return setter.ToFuture();
+ }));
+
+ // Ensure that thread was not started.
+ Sleep(SleepQuantum * 2);
+
+ // Initial computation condition.
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // Kick off!
+ kicker.Set();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // This will sleep for a while until evaluation completion.
+ thread.Join();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+}
+
+TEST_F(TFutureTest, ApplyVoidToInt)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<void>();
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] () -> int {
+ ++state;
+ return 17;
+ }));
+
+ // Initial computation condition.
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // Kick off!
+ kicker.Set();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+
+ EXPECT_EQ(17, target.Get().Value());
+}
+
+TEST_F(TFutureTest, ApplyVoidToFutureInt)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<void>();
+ auto setter = NewPromise<int>();
+
+ ::TThread thread(&AsynchronousIntSetter, &setter);
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] () -> TFuture<int> {
+ ++state;
+ thread.Start();
+ return setter.ToFuture();
+ }));
+
+ // Ensure that thread was not started.
+ Sleep(SleepQuantum * 2);
+
+ // Initial computation condition.
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // Kick off!
+ kicker.Set();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // This will sleep for a while until evaluation completion.
+ thread.Join();
+
+ EXPECT_EQ(1, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+
+ EXPECT_EQ(42, target.Get().Value());
+}
+
+TEST_F(TFutureTest, ApplyIntToVoid)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<int>();
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] (int x) -> void { state += x; }));
+
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ kicker.Set(21);
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+
+ EXPECT_EQ(21, source.Get().Value());
+}
+
+TEST_F(TFutureTest, ApplyIntToFutureVoid)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<int>();
+ auto setter = NewPromise<void>();
+
+ ::TThread thread(&AsynchronousVoidSetter, &setter);
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] (int x) -> TFuture<void> {
+ state += x;
+ thread.Start();
+ return setter.ToFuture();
+ }));
+
+ // Ensure that thread was not started.
+ Sleep(SleepQuantum * 2);
+
+ // Initial computation condition.
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // Kick off!
+ kicker.Set(21);
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ EXPECT_EQ(21, source.Get().Value());
+
+ // This will sleep for a while until evaluation completion.
+ thread.Join();
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+}
+
+TEST_F(TFutureTest, ApplyIntToInt)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<int>();
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] (int x) -> int {
+ state += x;
+ return x * 2;
+ }));
+
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ kicker.Set(21);
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+
+ EXPECT_EQ(21, source.Get().Value());
+ EXPECT_EQ(42, target.Get().Value());
+}
+
+TEST_F(TFutureTest, ApplyIntToFutureInt)
+{
+ int state = 0;
+
+ auto kicker = NewPromise<int>();
+ auto setter = NewPromise<int>();
+
+ ::TThread thread(&AsynchronousIntSetter, &setter);
+
+ auto source = kicker.ToFuture();
+ auto target = source
+ .Apply(BIND([&] (int x) -> TFuture<int> {
+ state += x;
+ thread.Start();
+ return setter.ToFuture();
+ }));
+
+ // Ensure that thread was not started.
+ Sleep(SleepQuantum * 2);
+
+ // Initial computation condition.
+ EXPECT_EQ(0, state);
+ EXPECT_FALSE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ // Kick off!
+ kicker.Set(21);
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_FALSE(target.IsSet());
+
+ EXPECT_EQ(21, source.Get().Value());
+
+ // This will sleep for a while until evaluation completion.
+ thread.Join();
+
+ EXPECT_EQ(21, state);
+ EXPECT_TRUE(source.IsSet());
+ EXPECT_TRUE(target.IsSet());
+
+ EXPECT_EQ(21, source.Get().Value());
+ EXPECT_EQ(42, target.Get().Value());
+}
+
+TEST_F(TFutureTest, TestCancelDelayed)
+{
+ auto future = NConcurrency::TDelayedExecutor::MakeDelayed(TDuration::Seconds(10));
+ future.Cancel(TError("Canceled"));
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_FALSE(future.Get().IsOK());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TFutureTest, AnyCombiner)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(2);
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ auto result = resultOrError.Value();
+ EXPECT_EQ(2, result);
+}
+
+TEST_F(TFutureTest, AnyCombinerRetainError)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySet(futures);
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_FALSE(resultOrError.IsOK());
+}
+
+TEST_F(TFutureTest, AnyCombinerEmpty)
+{
+ std::vector<TFuture<int>> futures;
+ auto error = AnySucceeded(futures).Get();
+ EXPECT_EQ(NYT::EErrorCode::FutureCombinerFailure, error.GetCode());
+}
+
+TEST_F(TFutureTest, AnyCombinerSkipError)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnySucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ EXPECT_FALSE(p2.IsCanceled());
+ p1.Set(TError("oops"));
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(123);
+ EXPECT_TRUE(f.IsSet());
+ auto result = f.Get();
+ EXPECT_TRUE(result.IsOK());
+ EXPECT_EQ(123, result.Value());
+ EXPECT_TRUE(p3.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyCombinerSuccessShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ EXPECT_FALSE(p2.IsCanceled());
+ p1.Set(1);
+ EXPECT_TRUE(f.IsSet());
+ auto result = f.Get();
+ EXPECT_TRUE(result.IsOK());
+ EXPECT_EQ(1, result.Value());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyCombinerDontCancelOnShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySucceeded(
+ futures,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ p1.Set(1);
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyCombinerPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySucceeded(futures);
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_TRUE(p1.IsCanceled());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyCombinerDontPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnySucceeded(
+ futures,
+ TFutureCombinerOptions{.PropagateCancelationToInput = false});
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyCombiner1)
+{
+ auto promise = NewPromise<int>();
+ auto future = promise.ToFuture();
+ std::vector<TFuture<int>> futures{
+ future
+ };
+ EXPECT_EQ(future, AnySucceeded(futures));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TFutureTest, AllCombinerEmpty)
+{
+ std::vector<TFuture<int>> futures{};
+ auto resultOrError = AllSucceeded(futures).Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ const auto& result = resultOrError.Value();
+ EXPECT_TRUE(result.empty());
+}
+
+TEST_F(TFutureTest, AllCombiner)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ p1.Set(2);
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(10);
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ const auto& result = resultOrError.Value();
+ EXPECT_EQ(2, std::ssize(result));
+ EXPECT_EQ(2, result[0]);
+ EXPECT_EQ(10, result[1]);
+}
+
+TEST_F(TFutureTest, AllCombinerError)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ p1.Set(2);
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_FALSE(resultOrError.IsOK());
+}
+
+TEST_F(TFutureTest, AllCombinerFailureShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ EXPECT_FALSE(p2.IsCanceled());
+ p1.Set(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ auto result = f.Get();
+ EXPECT_FALSE(result.IsOK());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AllCombinerDontCancelOnShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(
+ futures,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ p1.Set(TError("oops"));
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AllCombinerCancel)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AllSucceeded(futures);
+ EXPECT_FALSE(f.IsSet());
+ f.Cancel(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ const auto& result = f.Get();
+ EXPECT_EQ(NYT::EErrorCode::Canceled, result.GetCode());
+}
+
+TEST_F(TFutureTest, AllCombinerVoid0)
+{
+ std::vector<TFuture<void>> futures;
+ EXPECT_EQ(VoidFuture, AllSucceeded(futures));
+}
+
+TEST_F(TFutureTest, AllCombinerVoid1)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ std::vector<TFuture<void>> futures{
+ future
+ };
+ EXPECT_EQ(future, AllSucceeded(futures));
+}
+
+TEST_F(TFutureTest, AllCombinerRetainError)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ };
+ auto f = AllSet(futures);
+ EXPECT_FALSE(f.IsSet());
+ p1.Set(2);
+ EXPECT_FALSE(f.IsSet());
+ p2.Set(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ const auto& result = resultOrError.Value();
+ EXPECT_EQ(2, std::ssize(result));
+ EXPECT_TRUE(result[0].IsOK());
+ EXPECT_EQ(2, result[0].Value());
+ EXPECT_FALSE(result[1].IsOK());
+}
+
+TEST_F(TFutureTest, AllCombinerPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(futures);
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_TRUE(p1.IsCanceled());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AllCombinerDontPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSucceeded(
+ futures,
+ TFutureCombinerOptions{.PropagateCancelationToInput = false});
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TFutureTest, AllSetWithTimeoutWorks)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ auto p3 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AllSetWithTimeout(futures, TDuration::MilliSeconds(100));
+
+ p1.Set();
+ EXPECT_FALSE(f.IsSet());
+
+ Sleep(TDuration::MilliSeconds(20));
+ p3.Set(TError("oops"));
+ EXPECT_FALSE(f.IsSet());
+
+ Sleep(TDuration::MilliSeconds(300));
+ EXPECT_TRUE(f.IsSet());
+ const auto& resultOrError = f.Get();
+
+ EXPECT_TRUE(p1.IsSet());
+ EXPECT_TRUE(resultOrError.Value()[0].IsOK());
+
+ EXPECT_TRUE(p2.IsSet());
+ EXPECT_EQ(resultOrError.Value()[1].GetCode(), EErrorCode::Timeout);
+
+ EXPECT_TRUE(p3.IsSet());
+ EXPECT_FALSE(resultOrError.Value()[2].IsOK());
+}
+
+TEST_F(TFutureTest, AllSetWithTimeoutFuturesAreReleased)
+{
+ TWeakPtr<TRefCounted> wip1;
+ TWeakPtr<TRefCounted> wip2;
+ {
+ auto p1 = NewPromise<TRefCountedPtr>();
+ auto p2 = NewPromise<TRefCountedPtr>();
+ std::vector<TFuture<TRefCountedPtr>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AllSetWithTimeout(futures, TDuration::MilliSeconds(100));
+
+ auto ip1 = New<TRefCounted>();
+ wip1 = MakeWeak(ip1);
+ p1.Set(std::move(ip1));
+ EXPECT_FALSE(f.IsSet());
+
+ auto ip2 = New<TRefCounted>();
+ wip2 = MakeWeak(ip2);
+ p2.Set(std::move(ip2));
+ EXPECT_TRUE(f.IsSet());
+ }
+
+ // The WaitFor below ensures that the internal delayed executor cookie in AllSetWithTimeout is actually cancelled.
+ WaitFor(TDelayedExecutor::MakeDelayed(TDuration::Zero()))
+ .ThrowOnError();
+ EXPECT_TRUE(wip1.IsExpired());
+ EXPECT_TRUE(wip2.IsExpired());
+}
+
+TEST_F(TFutureTest, AllSetWithTimeoutCancellation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ auto p3 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AllSetWithTimeout(futures, TDuration::MilliSeconds(100));
+
+ Sleep(TDuration::MilliSeconds(20));
+ p3.Set();
+
+ Sleep(TDuration::MilliSeconds(20));
+ f.Cancel(TError("oops"));
+
+ EXPECT_TRUE(p1.IsCanceled());
+ EXPECT_TRUE(p2.IsCanceled());
+ EXPECT_TRUE(p3.IsSet());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TFutureTest, AnyNCombinerEmpty)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnyNSucceeded(futures, 0);
+ EXPECT_TRUE(f.IsSet());
+ const auto& resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ const auto& result = resultOrError.Value();
+ EXPECT_TRUE(result.empty());
+ EXPECT_TRUE(p1.IsCanceled());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontCancelOnEmptyShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 0,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerInsufficientInputs)
+{
+ auto p1 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture()
+ };
+ auto f = AnyNSucceeded(futures, 2);
+ EXPECT_TRUE(f.IsSet());
+ const auto& resultOrError = f.Get();
+ EXPECT_EQ(NYT::EErrorCode::FutureCombinerFailure, resultOrError.GetCode());
+ EXPECT_TRUE(p1.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontCancelOnInsufficientInputsShortcut)
+{
+ auto p1 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 2,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ EXPECT_FALSE(p1.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerTooManyFailures)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSucceeded(futures, 2);
+ EXPECT_FALSE(f.IsSet());
+ EXPECT_FALSE(p3.IsCanceled());
+ p1.Set(TError("oops1"));
+ p2.Set(TError("oops2"));
+ EXPECT_TRUE(f.IsSet());
+ const auto& resultOrError = f.Get();
+ EXPECT_EQ(NYT::EErrorCode::FutureCombinerFailure, resultOrError.GetCode());
+ EXPECT_TRUE(p3.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontCancelOnTooManyFailuresShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 2,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ p1.Set(TError("oops1"));
+ p2.Set(TError("oops2"));
+ EXPECT_FALSE(p3.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombiner)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSucceeded(futures, 2);
+ EXPECT_FALSE(f.IsSet());
+ EXPECT_FALSE(p1.IsCanceled());
+ p2.Set(1);
+ p3.Set(2);
+ EXPECT_TRUE(f.IsSet());
+ const auto& resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ auto result = resultOrError.Value();
+ std::sort(result.begin(), result.end());
+ EXPECT_EQ(2, std::ssize(result));
+ EXPECT_EQ(1, result[0]);
+ EXPECT_EQ(2, result[1]);
+ EXPECT_TRUE(p1.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontCancelOnShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 2,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ p2.Set(1);
+ p3.Set(2);
+ EXPECT_FALSE(p1.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontCancelOnPropagateErrorShortcut)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 2,
+ TFutureCombinerOptions{.CancelInputOnShortcut = false});
+ p3.Set(TError("oops"));
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerVoid1)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ std::vector<TFuture<void>> futures{
+ future
+ };
+ EXPECT_EQ(future, AnyNSucceeded(futures, 1));
+}
+
+TEST_F(TFutureTest, AnyNCombinerRetainError)
+{
+ auto p1 = NewPromise<int>();
+ auto p2 = NewPromise<int>();
+ auto p3 = NewPromise<int>();
+ std::vector<TFuture<int>> futures{
+ p1.ToFuture(),
+ p2.ToFuture(),
+ p3.ToFuture()
+ };
+ auto f = AnyNSet(futures, 2);
+ EXPECT_FALSE(f.IsSet());
+ p1.Set(2);
+ EXPECT_FALSE(f.IsSet());
+ p3.Set(TError("oops"));
+ EXPECT_TRUE(f.IsSet());
+ auto resultOrError = f.Get();
+ EXPECT_TRUE(resultOrError.IsOK());
+ const auto& result = resultOrError.Value();
+ EXPECT_EQ(2, std::ssize(result));
+ EXPECT_TRUE(result[0].IsOK());
+ EXPECT_EQ(2, result[0].Value());
+ EXPECT_FALSE(result[1].IsOK());
+}
+
+TEST_F(TFutureTest, AnyNCombinerPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnyNSucceeded(futures, 1);
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_TRUE(p1.IsCanceled());
+ EXPECT_TRUE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AnyNCombinerDontPropagateCancelation)
+{
+ auto p1 = NewPromise<void>();
+ auto p2 = NewPromise<void>();
+ std::vector<TFuture<void>> futures{
+ p1.ToFuture(),
+ p2.ToFuture()
+ };
+ auto f = AnyNSucceeded(
+ futures,
+ 1,
+ TFutureCombinerOptions{.PropagateCancelationToInput = false});
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+ f.Cancel(TError("oops"));
+ EXPECT_FALSE(p1.IsCanceled());
+ EXPECT_FALSE(p2.IsCanceled());
+}
+
+TEST_F(TFutureTest, AsyncViaCanceledInvoker)
+{
+ auto context = New<TCancelableContext>();
+ auto invoker = context->CreateInvoker(GetSyncInvoker());
+ auto generator = BIND([] () {}).AsyncVia(invoker);
+ context->Cancel(TError("oops"));
+ auto future = generator();
+ auto error = future.Get();
+ ASSERT_EQ(NYT::EErrorCode::Canceled, error.GetCode());
+}
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TFutureTest, LastPromiseDied)
+{
+ TFuture<void> future;
+ {
+ auto promise = NewPromise<void>();
+ future = promise;
+ EXPECT_FALSE(future.IsSet());
+ }
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_EQ(NYT::EErrorCode::Canceled, future.Get().GetCode());
+}
+
+TEST_F(TFutureTest, PropagateErrorSync)
+{
+ auto p = NewPromise<int>();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.Apply(BIND([] (int x) { return x + 1; }));
+ p.Set(TError("Oops"));
+ EXPECT_TRUE(f2.IsSet());
+ EXPECT_FALSE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, PropagateErrorAsync)
+{
+ auto p = NewPromise<int>();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.Apply(BIND([] (int x) { return MakeFuture(x + 1);}));
+ p.Set(TError("Oops"));
+ EXPECT_TRUE(f2.IsSet());
+ EXPECT_FALSE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, WithDeadlineSuccess)
+{
+ auto p = NewPromise<void>();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.WithDeadline(TInstant::Now() + TDuration::MilliSeconds(100));
+ Sleep(TDuration::MilliSeconds(10));
+ p.Set();
+ EXPECT_TRUE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, WithDeadlineOnSet)
+{
+ auto p = NewPromise<void>();
+ p.Set();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.WithDeadline(TInstant::Now());
+ EXPECT_TRUE(f1.Get().IsOK());
+ EXPECT_TRUE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, WithDeadlineFail)
+{
+ auto p = NewPromise<int>();
+ auto f1 = p.ToFuture();
+ auto deadline = TInstant::Now() + SleepQuantum;
+ auto f2 = f1.WithDeadline(deadline);
+ EXPECT_EQ(NYT::EErrorCode::Timeout, f2.Get().GetCode());
+ EXPECT_EQ(NYson::ConvertToYsonString(deadline), f2.Get().Attributes().FindYson("deadline"));
+}
+
+TEST_F(TFutureTest, WithTimeoutSuccess)
+{
+ auto p = NewPromise<void>();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.WithTimeout(TDuration::MilliSeconds(100));
+ Sleep(TDuration::MilliSeconds(10));
+ p.Set();
+ EXPECT_TRUE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, WithTimeoutOnSet)
+{
+ auto p = NewPromise<void>();
+ p.Set();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.WithTimeout(TDuration::MilliSeconds(0));
+ EXPECT_TRUE(f1.Get().IsOK());
+ EXPECT_TRUE(f2.Get().IsOK());
+}
+
+TEST_F(TFutureTest, WithTimeoutFail)
+{
+ auto p = NewPromise<int>();
+ auto f1 = p.ToFuture();
+ auto f2 = f1.WithTimeout(SleepQuantum);
+ EXPECT_EQ(NYT::EErrorCode::Timeout, f2.Get().GetCode());
+ EXPECT_EQ(NYson::ConvertToYsonString(SleepQuantum), f2.Get().Attributes().FindYson("timeout"));
+}
+
+TEST_W(TFutureTest, Holder)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ {
+ TFutureHolder<void> holder(future);
+ }
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(promise.IsCanceled());
+}
+
+TEST_F(TFutureTest, JustAbandon)
+{
+ Y_UNUSED(NewPromise<void>());
+}
+
+TEST_F(TFutureTest, AbandonIsSet)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.Reset();
+ EXPECT_TRUE(future.IsSet());
+}
+
+TEST_F(TFutureTest, AbandonTryGet)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.Reset();
+ EXPECT_EQ(EErrorCode::Canceled, future.TryGet()->GetCode());
+}
+
+TEST_F(TFutureTest, AbandonGet)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.Reset();
+ EXPECT_EQ(EErrorCode::Canceled, future.Get().GetCode());
+}
+
+TEST_F(TFutureTest, AbandonSubscribe)
+{
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.Reset();
+ bool called = false;
+ future.Subscribe(BIND([&] (const TError&) mutable { called = true; }));
+ EXPECT_TRUE(called);
+}
+
+TEST_F(TFutureTest, SubscribeAbandon)
+{
+ bool called = false;
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ future.Subscribe(BIND([&] (const TError&) mutable {
+ VERIFY_INVOKER_AFFINITY(GetFinalizerInvoker());
+ called = true;
+ }));
+ promise.Reset();
+ Sleep(SleepQuantum);
+ EXPECT_TRUE(called);
+}
+
+TEST_F(TFutureTest, OnCanceledAbandon)
+{
+ bool called = false;
+ auto promise = NewPromise<void>();
+ auto future = promise.ToFuture();
+ promise.OnCanceled(BIND([&] (const TError& /*error*/) {
+ called = true;
+ }));
+ promise.Reset();
+ Sleep(SleepQuantum);
+ EXPECT_FALSE(called);
+}
+
+TEST_F(TFutureTest, OnCanceledResult)
+{
+ {
+ auto promise = NewPromise<void>();
+ EXPECT_TRUE(promise.OnCanceled(BIND([&] (const TError& /*error*/) {})));
+ }
+
+ {
+ auto promise = NewPromise<void>();
+ promise.Set();
+ EXPECT_FALSE(promise.OnCanceled(BIND([&] (const TError& /*error*/) {})));
+ }
+}
+
+TString OnCallResult(const TErrorOr<int>& /*callResult*/)
+{
+ THROW_ERROR_EXCEPTION("Call failed");
+}
+
+TEST_F(TFutureTest, LtoCrash)
+{
+ auto future = MakeFuture<int>(0);
+ auto nextFuture = future.Apply(BIND(OnCallResult));
+}
+
+struct S
+{
+ static int DestroyedCounter;
+
+ ~S()
+ {
+ ++DestroyedCounter;
+ }
+};
+
+int S::DestroyedCounter = 0;
+
+TEST_F(TFutureTest, CancelableDoesNotProhibitDestruction)
+{
+ auto promise = NewPromise<S>();
+ promise.Set(S());
+
+ auto cancelable = promise.ToFuture().AsCancelable();
+
+ auto before = S::DestroyedCounter;
+ promise.Reset();
+ auto after = S::DestroyedCounter;
+ EXPECT_EQ(1, after - before);
+}
+
+TEST_F(TFutureTest, AbandonCancel)
+{
+ TMpscStack<TFuture<void>> queue;
+ std::thread producer([&] {
+ for (int i = 0; i < 10000; i++) {
+ auto p = NewPromise<void>();
+ queue.Enqueue(p.ToFuture());
+ Sleep(TDuration::MicroSeconds(1));
+ }
+
+ queue.Enqueue(TFuture<void>());
+ });
+
+ bool stop = false;
+ while (!stop) {
+ for (auto future : queue.DequeueAll(true)) {
+ if (!future) {
+ stop = true;
+ break;
+ }
+
+ future.Cancel(TError("Cancel"));
+ }
+ }
+
+ producer.join();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/guid_ut.cpp b/yt/yt/core/misc/unittests/guid_ut.cpp
new file mode 100644
index 0000000000..89701750e5
--- /dev/null
+++ b/yt/yt/core/misc/unittests/guid_ut.cpp
@@ -0,0 +1,22 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/guid.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TGuidTest, SerializationToProto)
+{
+ auto guid = TGuid::Create();
+ auto protoGuid = ToProto<NProto::TGuid>(guid);
+ auto deserializedGuid = FromProto<TGuid>(protoGuid);
+ EXPECT_EQ(guid, deserializedGuid);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/hash_filter_ut.cpp b/yt/yt/core/misc/unittests/hash_filter_ut.cpp
new file mode 100644
index 0000000000..e7dbc6e8be
--- /dev/null
+++ b/yt/yt/core/misc/unittests/hash_filter_ut.cpp
@@ -0,0 +1,122 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/bloom_filter.h>
+#include <yt/yt_proto/yt/core/misc/proto/bloom_filter.pb.h>
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+#include <random>
+#include <unordered_set>
+#include <vector>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ui64 Random()
+{
+ static std::random_device random;
+ static std::mt19937_64 generator(random());
+ static std::uniform_int_distribution<ui64> uniform;
+ return uniform(generator);
+}
+
+TBloomFilter FromBuilder(const TBloomFilterBuilder& bloomBuilder)
+{
+ NProto::TBloomFilter bloomProto;
+ TBloomFilter bloomFilter;
+ ToProto(&bloomProto, bloomBuilder);
+ FromProto(&bloomFilter, bloomProto);
+ return bloomFilter;
+}
+
+TEST(TBloomFilterTest, Null)
+{
+ auto bloom = FromBuilder(TBloomFilterBuilder(65636, 0.03));
+
+ for (int index = 0; index < 1000; ++index){
+ EXPECT_FALSE(bloom.Contains(Random()));
+ }
+}
+
+TEST(TBloomFilterTest, Simple)
+{
+ TBloomFilterBuilder bloomBuilder(65636, 0.03);
+ auto items = std::vector<ui64>{0,1,2};
+
+ for (const auto item : items) {
+ bloomBuilder.Insert(item);
+ }
+
+ auto bloom = FromBuilder(bloomBuilder);
+
+ for (const auto item : items) {
+ EXPECT_TRUE(bloom.Contains(item));
+ }
+
+ auto size = bloomBuilder.EstimateSize();
+ EXPECT_EQ(size, 4);
+ bloomBuilder.Shrink();
+ EXPECT_EQ(bloomBuilder.Size(), size);
+
+ bloom = FromBuilder(bloomBuilder);
+
+ for (const auto item : items) {
+ EXPECT_TRUE(bloom.Contains(item));
+ }
+}
+
+TEST(TBloomFilterTest, FalsePositiveRate)
+{
+ TBloomFilterBuilder bloomBuilder(65636, 0.03);
+ auto items = std::unordered_set<ui64>();
+
+ for (int index = 0; index < 1000; ++index){
+ auto item = Random();
+ items.insert(item);
+ bloomBuilder.Insert(item);
+ }
+
+ auto bloom = FromBuilder(bloomBuilder);
+
+ for (const auto item : items) {
+ EXPECT_TRUE(bloom.Contains(item));
+ }
+
+ bloom = FromBuilder(bloomBuilder);
+
+ int falsePositiveCount = 0;
+ int lookupCount = 10000;
+ for (int index = 0; index < lookupCount; ++index) {
+ auto item = Random();
+ if (items.find(item) == items.end() && bloom.Contains(item)) {
+ ++falsePositiveCount;
+ }
+ }
+
+ EXPECT_LT(static_cast<double>(falsePositiveCount) / lookupCount, 0.05);
+
+ bloomBuilder.Shrink();
+
+ EXPECT_LE(bloomBuilder.Size(), 8192);
+
+ bloom = FromBuilder(bloomBuilder);
+
+ falsePositiveCount = 0;
+ lookupCount = 10000;
+ for (int index = 0; index < lookupCount; ++index) {
+ auto item = Random();
+ if (items.find(item) == items.end() && bloom.Contains(item)) {
+ ++falsePositiveCount;
+ }
+ }
+
+ EXPECT_LT(static_cast<double>(falsePositiveCount) / lookupCount, 0.05);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/hazard_ptr_ut.cpp b/yt/yt/core/misc/unittests/hazard_ptr_ut.cpp
new file mode 100644
index 0000000000..d0fec52d50
--- /dev/null
+++ b/yt/yt/core/misc/unittests/hazard_ptr_ut.cpp
@@ -0,0 +1,381 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/public.h>
+#include <yt/yt/core/misc/hazard_ptr.h>
+#include <yt/yt/core/misc/atomic_ptr.h>
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+
+#include <library/cpp/yt/memory/new.h>
+
+#include <util/system/thread.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestAllocator
+{
+public:
+ explicit TTestAllocator(IOutputStream* output)
+ : Output_(output)
+ { }
+
+ void* Allocate(size_t size)
+ {
+ *Output_ << 'A';
+ ++AllocatedCount_;
+
+ size += sizeof(void*);
+ auto* ptr = ::malloc(size);
+ auto* header = static_cast<TTestAllocator**>(ptr);
+ *header = this;
+ return header + 1;
+ }
+
+ static void Free(void* ptr)
+ {
+ auto* header = static_cast<TTestAllocator**>(ptr) - 1;
+ auto* allocator = *header;
+
+ *allocator->Output_ << 'F';
+ ++allocator->DeallocatedCount_;
+
+ ::free(header);
+ }
+
+ ~TTestAllocator()
+ {
+ YT_VERIFY(AllocatedCount_ == DeallocatedCount_);
+ }
+
+private:
+ IOutputStream* const Output_;
+ size_t AllocatedCount_ = 0;
+ size_t DeallocatedCount_ = 0;
+};
+
+class TSampleObject final
+{
+public:
+ using TAllocator = TTestAllocator;
+ static constexpr bool EnableHazard = true;
+
+ explicit TSampleObject(IOutputStream* output)
+ : Output_(output)
+ {
+ *Output_ << 'C';
+ }
+
+ ~TSampleObject()
+ {
+ *Output_ << 'D';
+ }
+
+ void DoSomething()
+ {
+ *Output_ << '!';
+ }
+
+private:
+ IOutputStream* const Output_;
+
+};
+
+TEST(THazardPtrTest, RefCountedPtrBehavior)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ {
+ auto ptr = New<TSampleObject>(&allocator, &output);
+ {
+ auto anotherPtr = ptr;
+ anotherPtr->DoSomething();
+ }
+ {
+ auto anotherPtr = ptr;
+ anotherPtr->DoSomething();
+ }
+ ptr->DoSomething();
+ }
+
+ EXPECT_STREQ("AC!!!D", output.Str().c_str());
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!!!DF", output.Str().c_str());
+}
+
+TEST(THazardPtrTest, DelayedDeallocation)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ auto ptr = New<TSampleObject>(&allocator, &output);
+ ptr->DoSomething();
+
+ auto hazardPtr = THazardPtr<TSampleObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ ptr = nullptr;
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ EXPECT_TRUE(hazardPtr);
+ EXPECT_FALSE(MakeStrong(hazardPtr));
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ hazardPtr.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+}
+
+TEST(THazardPtrTest, DelayedDeallocationWithMultipleHPs)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ auto ptr = New<TSampleObject>(&allocator, &output);
+ ptr->DoSomething();
+
+ auto hazardPtr1 = THazardPtr<TSampleObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ auto hazardPtr2 = THazardPtr<TSampleObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ ptr = nullptr;
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ EXPECT_TRUE(hazardPtr1);
+ EXPECT_FALSE(MakeStrong(hazardPtr1));
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ hazardPtr1.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+ hazardPtr2.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+}
+
+TEST(THazardPtrTest, CombinedLogic)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ auto ptr = New<TSampleObject>(&allocator, &output);
+ ptr->DoSomething();
+
+ auto ptrCopy = ptr;
+ auto rawPtr = ptrCopy.Release();
+
+ auto hazardPtr = THazardPtr<TSampleObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ ptr = nullptr;
+
+ EXPECT_STREQ("AC!", output.Str().c_str());
+
+ RetireHazardPointer(rawPtr, [] (auto* ptr) {
+ Unref(ptr);
+ });
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!", output.Str().c_str());
+
+ {
+ hazardPtr.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+ }
+
+ {
+ auto hazardPtr = THazardPtr<TSampleObject>::Acquire([&] {
+ return rawPtr;
+ });
+
+ ReclaimHazardPointers(/*flush*/ false);
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+ }
+
+ {
+ ReclaimHazardPointers(/*flush*/ false);
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSamplePolymorphicObject
+ : public TRefCounted
+{
+public:
+ using TAllocator = TTestAllocator;
+ static constexpr bool EnableHazard = true;
+
+ explicit TSamplePolymorphicObject(IOutputStream* output)
+ : Output_(output)
+ {
+ *Output_ << 'C';
+ }
+
+ ~TSamplePolymorphicObject()
+ {
+ *Output_ << 'D';
+ }
+
+ void DoSomething()
+ {
+ *Output_ << '!';
+ }
+
+private:
+ IOutputStream* const Output_;
+
+};
+
+TEST(THazardPtrTest, DelayedDeallocationPolymorphic)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ auto ptr = New<TSamplePolymorphicObject>(&allocator, &output);
+ ptr->DoSomething();
+
+ auto hazardPtr = THazardPtr<TSamplePolymorphicObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ ptr = nullptr;
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ hazardPtr.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+}
+
+NThreading::TEvent Started;
+NThreading::TEvent Finish;
+
+#ifndef _win_
+TEST(THazardPtrTest, SupportFork)
+{
+ // Ensure that delete list is empty.
+ ReclaimHazardPointers();
+
+ TStringStream output;
+ TTestAllocator allocator(&output);
+
+ auto ptr = New<TSamplePolymorphicObject>(&allocator, &output);
+ ptr->DoSomething();
+
+ auto hazardPtr = THazardPtr<TSamplePolymorphicObject>::Acquire([&] {
+ return ptr.Get();
+ });
+
+ TThread thread1([] (void* opaque) -> void* {
+ auto ptrRef = static_cast<TIntrusivePtr<TSamplePolymorphicObject>*>(opaque);
+ auto hazardPtr = THazardPtr<TSamplePolymorphicObject>::Acquire([&] {
+ return ptrRef->Get();
+ });
+
+ EXPECT_TRUE(hazardPtr);
+ hazardPtr.Reset();
+
+ Started.NotifyOne();
+ Finish.Wait();
+
+ return nullptr;
+ }, &ptr);
+
+ thread1.Start();
+ Started.Wait();
+
+ ptr = nullptr;
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ auto childPid = fork();
+ if (childPid < 0) {
+ THROW_ERROR_EXCEPTION("fork failed")
+ << TError::FromSystem();
+ }
+
+ if (childPid == 0) {
+ thread1.Detach();
+
+ EXPECT_TRUE(hazardPtr);
+
+ ReclaimHazardPointers(/*flush*/ false);
+ EXPECT_STREQ("AC!D", output.Str().c_str());
+
+ hazardPtr.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+
+ // Do not test hazard pointer manager shutdown
+ // because of broken (after fork) NYT::Shutdown.
+ ::_exit(0);
+ } else {
+ Sleep(TDuration::Seconds(1));
+ hazardPtr.Reset();
+ ReclaimHazardPointers(/*flush*/ false);
+
+ EXPECT_STREQ("AC!DF", output.Str().c_str());
+
+ Finish.NotifyOne();
+ thread1.Join();
+ }
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/heap_ut.cpp b/yt/yt/core/misc/unittests/heap_ut.cpp
new file mode 100644
index 0000000000..6d969765c1
--- /dev/null
+++ b/yt/yt/core/misc/unittests/heap_ut.cpp
@@ -0,0 +1,123 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/heap.h>
+
+#include <vector>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetRandomString(int len)
+{
+ TString str;
+ for (int i = 0; i < len; ++i) {
+ str.push_back('a' + rand() % 26);
+ }
+ return str;
+}
+
+TEST(THeapTest, MakeThenExtract)
+{
+ srand(0);
+
+ std::vector<TString> words;
+ for (int i = 0; i < 10000; ++i) {
+ words.push_back(GetRandomString(10));
+ }
+
+ auto sorted = words;
+ std::sort(sorted.begin(), sorted.end());
+
+ NYT::MakeHeap(words.begin(), words.end(), std::greater<TString>());
+ auto end = words.end();
+ while (end != words.begin()) {
+ NYT::ExtractHeap(words.begin(), end, std::greater<TString>());
+ --end;
+ }
+
+ EXPECT_EQ(sorted, words);
+}
+
+TEST(THeapTest, MakeChangeThenExtract)
+{
+ srand(0);
+
+ std::vector<i64> values;
+ for (int i = 0; i < 10000; ++i) {
+ values.push_back(rand());
+ }
+
+ NYT::MakeHeap(values.begin(), values.end(), std::less<i64>());
+
+ for (int i = 0; i < 10000; ++i) {
+ int delta = rand();
+
+ if (delta > 0) {
+ SiftDown(values.begin(), values.end(), values.begin() + 1, std::less<i64>());
+ } else {
+ SiftUp(values.begin(), values.end(), values.begin() + 1, std::less<i64>());
+ }
+ }
+
+ i64 last = std::numeric_limits<i64>::min();
+
+ auto end = values.end();
+ while (end != values.begin()) {
+ EXPECT_LE(last, values.front());
+ last = values.front();
+ NYT::ExtractHeap(values.begin(), end, std::less<i64>());
+ --end;
+ }
+}
+
+TEST(THeapTest, MakeThenExtractMoveOnly)
+{
+ srand(0);
+
+ std::vector<std::unique_ptr<TString>> words;
+ for (int i = 0; i < 10000; ++i) {
+ words.push_back(std::make_unique<TString>(GetRandomString(10)));
+ }
+
+ NYT::MakeHeap(words.begin(), words.end(), [] (const auto& lhs, const auto& rhs) { return *lhs > *rhs; });
+ auto end = words.end();
+ while (end != words.begin()) {
+ NYT::ExtractHeap(words.begin(), end, [] (const auto& lhs, const auto& rhs) { return *lhs > *rhs; });
+ --end;
+ }
+
+ EXPECT_TRUE(std::is_sorted(words.begin(), words.end(), [] (const auto& lhs, const auto& rhs) { return *lhs < *rhs; }));
+}
+
+
+TEST(THeapTest, InsertThenExtract)
+{
+ srand(0);
+
+ std::vector<TString> words;
+ for (int i = 0; i < 10000; ++i) {
+ words.push_back(GetRandomString(10));
+ }
+
+ auto sorted = words;
+ std::sort(sorted.begin(), sorted.end());
+
+ for (auto it = words.begin(); it != words.end(); ++it) {
+ NYT::AdjustHeapBack(words.begin(), it, std::greater<TString>());
+ }
+
+ auto end = words.end();
+ while (end != words.begin()) {
+ NYT::ExtractHeap(words.begin(), end, std::greater<TString>());
+ --end;
+ }
+
+ EXPECT_EQ(sorted, words);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/hedging_manager_ut.cpp b/yt/yt/core/misc/unittests/hedging_manager_ut.cpp
new file mode 100644
index 0000000000..e31e9f4539
--- /dev/null
+++ b/yt/yt/core/misc/unittests/hedging_manager_ut.cpp
@@ -0,0 +1,109 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/config.h>
+#include <yt/yt/core/misc/hedging_manager.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAdaptiveHedgingManagerTest
+ : public ::testing::Test
+{
+protected:
+ const TAdaptiveHedgingManagerConfigPtr Config_ = New<TAdaptiveHedgingManagerConfig>();
+
+ IHedgingManagerPtr HedgingManager_;
+
+
+ void CreateHedgingManager()
+ {
+ Config_->Postprocess();
+ HedgingManager_ = CreateAdaptiveHedgingManager(Config_);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TAdaptiveHedgingManagerTest, Simple)
+{
+ Config_->MaxHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxBackupRequestRatio = 0.1;
+ CreateHedgingManager();
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+}
+
+TEST_F(TAdaptiveHedgingManagerTest, RatioGreaterThanOne)
+{
+ Config_->MaxHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxBackupRequestRatio = 2.0;
+ CreateHedgingManager();
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+}
+
+TEST_F(TAdaptiveHedgingManagerTest, RestrainHedging)
+{
+ Config_->MaxHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxBackupRequestRatio = 0.5;
+ CreateHedgingManager();
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+ EXPECT_FALSE(HedgingManager_->OnHedgingDelayPassed(1));
+}
+
+TEST_F(TAdaptiveHedgingManagerTest, ApproveHedging)
+{
+ Config_->MinHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxBackupRequestRatio = 0.5;
+ Config_->TickPeriod = TDuration::Seconds(1);
+ CreateHedgingManager();
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+
+ Sleep(TDuration::Seconds(1));
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+}
+
+TEST_F(TAdaptiveHedgingManagerTest, TuneHedgingDelay)
+{
+ Config_->MinHedgingDelay = TDuration::MilliSeconds(1);
+ Config_->MaxHedgingDelay = TDuration::Seconds(1);
+ Config_->MaxBackupRequestRatio = 0.1;
+ Config_->HedgingDelayTuneFactor = 1e9;
+ Config_->TickPeriod = TDuration::Seconds(1);
+ CreateHedgingManager();
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+
+ Sleep(TDuration::Seconds(1));
+
+ EXPECT_EQ(TDuration::MilliSeconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+ EXPECT_TRUE(HedgingManager_->OnHedgingDelayPassed(1));
+
+ Sleep(TDuration::Seconds(1));
+
+ EXPECT_EQ(TDuration::Seconds(1), HedgingManager_->OnPrimaryRequestsStarted(1));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/histogram_ut.cpp b/yt/yt/core/misc/unittests/histogram_ut.cpp
new file mode 100644
index 0000000000..9fde3e48fb
--- /dev/null
+++ b/yt/yt/core/misc/unittests/histogram_ut.cpp
@@ -0,0 +1,55 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/histogram.h>
+
+#include <numeric>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(THistogramTest, Simple)
+{
+ auto h = CreateHistogram();
+
+ for (i64 i : {1, 10, 100, 100}) {
+ h->AddValue(i);
+ }
+ h->BuildHistogramView();
+ auto v = h->GetHistogramView();
+ EXPECT_EQ(1, v.Min);
+ EXPECT_EQ(101, v.Max);
+ EXPECT_EQ(100, std::ssize(v.Count));
+ EXPECT_EQ(4, std::accumulate(v.Count.begin(), v.Count.end(), 0));
+ EXPECT_EQ(1, v.Count.front());
+ EXPECT_EQ(2, v.Count.back());
+ for (i64 i : {49, 48, 149, 150}) {
+ h->AddValue(i);
+ }
+ h->BuildHistogramView();
+ v = h->GetHistogramView();
+ EXPECT_EQ(1, v.Min);
+ EXPECT_EQ(151, v.Max);
+ EXPECT_EQ(150, std::ssize(v.Count));
+ EXPECT_EQ(8, std::accumulate(v.Count.begin(), v.Count.end(), 0));
+ EXPECT_EQ(1, v.Count.front());
+ EXPECT_EQ(1, v.Count.back());
+ for (i64 i : {150, 151}) {
+ h->AddValue(i);
+ }
+ h->BuildHistogramView();
+ v = h->GetHistogramView();
+ EXPECT_EQ(0, v.Min);
+ EXPECT_EQ(152, v.Max);
+ EXPECT_EQ(76, std::ssize(v.Count));
+ EXPECT_EQ(10, std::accumulate(v.Count.begin(), v.Count.end(), 0));
+ EXPECT_EQ(1, v.Count.front());
+ EXPECT_EQ(3, v.Count.back());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/historic_usage_aggregator_ut.cpp b/yt/yt/core/misc/unittests/historic_usage_aggregator_ut.cpp
new file mode 100644
index 0000000000..d664aba639
--- /dev/null
+++ b/yt/yt/core/misc/unittests/historic_usage_aggregator_ut.cpp
@@ -0,0 +1,105 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/historic_usage_aggregator.h>
+
+#include <util/generic/ymath.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistoricUsageAggregatorTest
+ : public testing::Test
+{
+public:
+ void InitializeNoneMode()
+ {
+ HistoricUsageAggregator_.Reset();
+ HistoricUsageAggregator_.UpdateParameters(THistoricUsageAggregationParameters(
+ EHistoricUsageAggregationMode::None
+ ));
+ }
+
+ void InitializeEmaMode(double emaAlpha)
+ {
+ HistoricUsageAggregator_.Reset();
+ HistoricUsageAggregator_.UpdateParameters(THistoricUsageAggregationParameters(
+ EHistoricUsageAggregationMode::ExponentialMovingAverage,
+ emaAlpha
+ ));
+ }
+
+protected:
+ THistoricUsageAggregator HistoricUsageAggregator_;
+};
+
+TEST_F(THistoricUsageAggregatorTest, TestNoneMode)
+{
+ InitializeNoneMode();
+
+ TInstant now = TInstant::Zero();
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), 0.0);
+
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), 0.0);
+
+ now += TDuration::Seconds(1);
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), 0.0);
+}
+
+TEST_F(THistoricUsageAggregatorTest, TestExponentialMovingAverageModeUpdateAt)
+{
+ InitializeEmaMode(0.1);
+
+ auto now = TInstant::Seconds(1);
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), 0.0);
+
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), 0.0);
+
+ now += TDuration::Seconds(1);
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ auto currentHistoricUsage = 1.0 - Exp2(-0.1);
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), currentHistoricUsage);
+
+ now += TDuration::Seconds(1);
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ currentHistoricUsage = Exp2(-0.1) * currentHistoricUsage + (1.0 - Exp2(-0.1));
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), currentHistoricUsage);
+}
+
+TEST_F(THistoricUsageAggregatorTest, TestExponentialMovingAverageModeUpdateParametersAndResetIfNecessary)
+{
+ InitializeEmaMode(0.1);
+
+ // Aggregate some usage.
+ auto now = TInstant::Seconds(1);
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ now += TDuration::Seconds(1);
+ HistoricUsageAggregator_.UpdateAt(now, 1.0);
+ auto currentHistoricUsage = 1.0 - Exp2(-0.1);
+
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), currentHistoricUsage);
+
+ // No reset if parameters haven't changed.
+ HistoricUsageAggregator_.UpdateParameters(THistoricUsageAggregationParameters(
+ EHistoricUsageAggregationMode::ExponentialMovingAverage,
+ 0.1
+ ));
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), currentHistoricUsage);
+
+ // Reset if parameters have changed.
+ HistoricUsageAggregator_.UpdateParameters(THistoricUsageAggregationParameters(
+ EHistoricUsageAggregationMode::ExponentialMovingAverage,
+ 0.05
+ ));
+ currentHistoricUsage = 0.0;
+ ASSERT_DOUBLE_EQ(HistoricUsageAggregator_.GetHistoricUsage(), currentHistoricUsage);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/hyperloglog_ut.cpp b/yt/yt/core/misc/unittests/hyperloglog_ut.cpp
new file mode 100644
index 0000000000..00eec04ede
--- /dev/null
+++ b/yt/yt/core/misc/unittests/hyperloglog_ut.cpp
@@ -0,0 +1,78 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/hyperloglog.h>
+#include <yt/yt/core/misc/random.h>
+
+#include <library/cpp/yt/farmhash/farm_hash.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THyperLogLogTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<int, int, int, int>>
+{ };
+
+std::pair<THyperLogLog<8>, int> GenerateHyperLogLog(
+ TRandomGenerator& rng,
+ int size,
+ int targetCardinality)
+{
+ auto hll = THyperLogLog<8>();
+
+ int cardinality = 1;
+ ui64 n = 0;
+ hll.Add(FarmFingerprint(n));
+ for (int i = 0; i < size; i++) {
+ if (static_cast<int>(rng.Generate<ui64>() % size) < targetCardinality) {
+ cardinality++;
+ n += 1 + (rng.Generate<ui32>() % 100);
+ }
+
+ hll.Add(FarmFingerprint(n));
+ }
+
+ return std::make_pair(hll, cardinality);
+}
+
+TEST_P(THyperLogLogTest, Random)
+{
+ auto param = GetParam();
+ auto seed = std::get<0>(param);
+ auto size = std::get<1>(param);
+ auto targetCardinality = std::get<2>(param);
+ auto iterations = std::get<3>(param);
+ auto rng = TRandomGenerator(seed);
+
+ auto error = 0.0;
+
+ for (int i = 0; i < iterations; i++) {
+ auto hll = GenerateHyperLogLog(
+ rng,
+ size,
+ targetCardinality);
+ auto err = ((double)hll.first.EstimateCardinality() - hll.second) / hll.second;
+ error += err;
+ }
+
+ auto meanError = error / iterations;
+
+ EXPECT_NEAR(meanError, 0, 0.05);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ HyperLogLogTest,
+ THyperLogLogTest,
+ ::testing::Values(
+ std::tuple<int, int, int, int>(123, 100000, 200, 10),
+ std::tuple<int, int, int, int>(324, 100000, 1000, 10),
+ std::tuple<int, int, int, int>(653, 100000, 5000, 10),
+ std::tuple<int, int, int, int>(890, 100000, 10000, 10),
+ std::tuple<int, int, int, int>(278, 100000, 50000, 10)));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/intern_registry_ut.cpp b/yt/yt/core/misc/unittests/intern_registry_ut.cpp
new file mode 100644
index 0000000000..f49069e928
--- /dev/null
+++ b/yt/yt/core/misc/unittests/intern_registry_ut.cpp
@@ -0,0 +1,75 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/intern_registry.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TStringRegistry = TInternRegistry<TString>;
+using TInternedString = TInternedObject<TString>;
+
+TEST(TInternRegistry, TestEmptyRegistry)
+{
+ auto registry = New<TStringRegistry>();
+ EXPECT_EQ(0, registry->GetSize());
+}
+
+TEST(TInternRegistry, TestEmptyInstance)
+{
+ TInternedString s;
+ EXPECT_EQ(0u, s->length());
+}
+
+TEST(TInternRegistry, Simple)
+{
+ auto registry = New<TStringRegistry>();
+ EXPECT_EQ(0, registry->GetSize());
+
+ auto s1 = registry->Intern(TString("hello"));
+ EXPECT_EQ(1, registry->GetSize());
+
+ auto s2 = registry->Intern(TString("world"));
+ EXPECT_EQ(2, registry->GetSize());
+
+ auto s3 = registry->Intern(TString("hello"));
+ EXPECT_EQ(2, registry->GetSize());
+
+ EXPECT_TRUE(*s1 == *s3);
+ EXPECT_FALSE(*s1 == *s2);
+
+ auto s4 = registry->Intern(TString("test"));
+ EXPECT_EQ(3, registry->GetSize());
+
+ s4 = TInternedString();
+ EXPECT_EQ(2, registry->GetSize());
+
+ s3 = TInternedString();
+ EXPECT_EQ(2, registry->GetSize());
+
+ s2 = TInternedString();
+ EXPECT_EQ(1, registry->GetSize());
+
+ s1 = TInternedString();
+ EXPECT_EQ(0, registry->GetSize());
+}
+
+
+TEST(TInternRegistry, Default)
+{
+ auto registry = New<TStringRegistry>();
+ EXPECT_EQ(0, registry->GetSize());
+
+ auto s1 = TInternedString();
+
+ auto s2 = registry->Intern(TString());
+ EXPECT_EQ(0, registry->GetSize());
+
+ EXPECT_TRUE(*s1 == *s2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/job_signaler_ut.cpp b/yt/yt/core/misc/unittests/job_signaler_ut.cpp
new file mode 100644
index 0000000000..02813e3f89
--- /dev/null
+++ b/yt/yt/core/misc/unittests/job_signaler_ut.cpp
@@ -0,0 +1,71 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#ifdef _unix_
+#include <signal.h>
+#include <unistd.h>
+#include <fcntl.h>
+#endif
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#ifdef _unix_
+
+static int PipeDescriptors[2];
+
+static void SignalHandler(int /*signum*/)
+{
+ Y_UNUSED(::write(PipeDescriptors[1], "got signal\n", 11));
+}
+
+TEST(TJobSignaler, Basic)
+{
+ ASSERT_EQ(0, ::pipe(PipeDescriptors));
+ ASSERT_EQ(0, ::fcntl(PipeDescriptors[0], F_SETFL, ::fcntl(PipeDescriptors[0], F_GETFL) | O_NONBLOCK));
+
+ int pid = ::fork();
+ ASSERT_TRUE(pid >= 0);
+
+ if (pid == 0) {
+ struct sigaction new_action;
+ new_action.sa_handler = SignalHandler;
+ sigemptyset(&new_action.sa_mask);
+ new_action.sa_flags = 0;
+ ::sigaction(SIGUSR1, &new_action, nullptr);
+
+ while (true) {
+ Sleep(TDuration::MilliSeconds(100));
+ }
+
+ _exit(0);
+ }
+
+ Sleep(TDuration::MilliSeconds(100));
+ SendSignal({pid}, "SIGUSR1");
+ Sleep(TDuration::MilliSeconds(100));
+
+ char buffer[256];
+ ASSERT_EQ(11, ::read(PipeDescriptors[0], buffer, sizeof(buffer)));
+ ASSERT_EQ(0, ::memcmp(buffer, "got signal\n", 11));
+
+ ASSERT_EQ(0, ::kill(pid, SIGKILL));
+
+ ASSERT_EQ(pid, ::waitpid(pid, nullptr, 0));
+}
+
+TEST(TJobSignaler, UnknownSignal)
+{
+ int pid = ::getpid();
+ ASSERT_THROW(SendSignal({pid}, "SIGUNKNOWN"), std::exception);
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/lock_free_hash_table_and_concurrent_cache_helpers.h b/yt/yt/core/misc/unittests/lock_free_hash_table_and_concurrent_cache_helpers.h
new file mode 100644
index 0000000000..c636ee3edb
--- /dev/null
+++ b/yt/yt/core/misc/unittests/lock_free_hash_table_and_concurrent_cache_helpers.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <yt/yt/core/misc/common.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TRandomCharGenerator
+{
+ std::default_random_engine Engine;
+ std::uniform_int_distribution<char> Uniform;
+
+ explicit TRandomCharGenerator(size_t seed)
+ : Engine(seed)
+ , Uniform(0, 'z' - 'a' + '9' - '0' + 1)
+ { }
+
+ char operator() ()
+ {
+ char symbol = Uniform(Engine);
+ auto result = symbol >= 10 ? symbol - 10 + 'a' : symbol + '0';
+ YT_VERIFY((result <= 'z' && result >= 'a') || (result <= '9' && result >= '0'));
+ return result;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/lock_free_hash_table_ut.cpp b/yt/yt/core/misc/unittests/lock_free_hash_table_ut.cpp
new file mode 100644
index 0000000000..6c196f1bce
--- /dev/null
+++ b/yt/yt/core/misc/unittests/lock_free_hash_table_ut.cpp
@@ -0,0 +1,139 @@
+#include "lock_free_hash_table_and_concurrent_cache_helpers.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/lock_free_hash_table.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TElement final
+{
+ ui64 Hash;
+ ui32 Size;
+ char Data[0];
+
+ static constexpr bool EnableHazard = true;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
+template <>
+struct THash<NYT::TElement>
+{
+ size_t operator()(const NYT::TElement* value) const
+ {
+ return value->Hash;
+ }
+};
+
+template <>
+struct TEqualTo<NYT::TElement>
+{
+ bool operator()(const NYT::TElement* lhs, const NYT::TElement* rhs) const
+ {
+ return lhs->Hash == rhs->Hash &&
+ lhs->Size == rhs->Size &&
+ memcmp(lhs->Data, rhs->Data, lhs->Size) == 0;
+ }
+};
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLockFreeHashTableTest, Simple)
+{
+ size_t keyColumnCount = 3;
+ size_t columnCount = 5;
+
+ TLockFreeHashTable<TElement> table(1000);
+
+ THash<TElement> hash;
+ TEqualTo<TElement> equalTo;
+
+ std::vector<TIntrusivePtr<TElement>> checkTable;
+
+ size_t iterations = 1000;
+
+ TRandomCharGenerator randomChar(0);
+
+ for (size_t index = 0; index < iterations; ++index) {
+ auto item = NewWithExtraSpace<TElement>(columnCount);
+ {
+ item->Size = keyColumnCount;
+ for (size_t pos = 0; pos < columnCount; ++pos) {
+ item->Data[pos] = randomChar();
+ }
+ item->Hash = THash<TStringBuf>{}(TStringBuf(&item->Data[0], keyColumnCount));
+ }
+ checkTable.push_back(item);
+ }
+
+ std::sort(checkTable.begin(), checkTable.end(), [] (
+ const TIntrusivePtr<TElement>& lhs,
+ const TIntrusivePtr<TElement>& rhs)
+ {
+ return memcmp(lhs->Data, rhs->Data, lhs->Size) < 0;
+ });
+
+ auto it = std::unique(checkTable.begin(), checkTable.end(), [&] (
+ const TIntrusivePtr<TElement>& lhs,
+ const TIntrusivePtr<TElement>& rhs)
+ {
+ return equalTo(lhs.Get(), rhs.Get());
+ });
+
+ checkTable.erase(it, checkTable.end());
+
+ std::random_shuffle(checkTable.begin(), checkTable.end());
+
+ for (const auto& item : checkTable) {
+ auto fingerprint = hash(item.Get());
+ table.Insert(fingerprint, item);
+ }
+
+ for (const auto& item : checkTable) {
+ auto fingerprint = hash(item.Get());
+ auto found = table.Find(fingerprint, item.Get());
+
+ EXPECT_TRUE(found);
+ }
+
+ std::vector<TIntrusivePtr<TElement>> updateTable;
+ for (size_t index = 0; index < checkTable.size(); index += 2) {
+ const auto& current = checkTable[index];
+
+ auto item = NewWithExtraSpace<TElement>(columnCount);
+ memcpy(item.Get(), current.Get(), sizeof(TElement) + columnCount);
+ std::swap(item->Data[columnCount - 1], item->Data[columnCount - 2]);
+
+ updateTable.push_back(item);
+ }
+
+ for (const auto& item : updateTable) {
+ auto fingerprint = hash(item.Get());
+ auto foundRef = table.FindRef(fingerprint, item.Get());
+ EXPECT_TRUE(static_cast<bool>(foundRef));
+ foundRef.Update(item);
+ }
+
+ for (const auto& item : checkTable) {
+ auto fingerprint = hash(item.Get());
+ auto found = table.Find(fingerprint, item.Get());
+
+ EXPECT_TRUE(found);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/lru_cache_ut.cpp b/yt/yt/core/misc/unittests/lru_cache_ut.cpp
new file mode 100644
index 0000000000..da46d7a332
--- /dev/null
+++ b/yt/yt/core/misc/unittests/lru_cache_ut.cpp
@@ -0,0 +1,146 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/sync_cache.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSimpleLruCache, Common)
+{
+ TSimpleLruCache<TString, int> cache(2);
+ cache.Insert("a", 1);
+ cache.Insert("b", 2);
+
+ EXPECT_TRUE(cache.Find("a"));
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_FALSE(cache.Find("c"));
+ EXPECT_EQ(cache.Get("a"), 1);
+ EXPECT_EQ(cache.Get("b"), 2);
+
+ cache.Insert("c", 3);
+
+ EXPECT_FALSE(cache.Find("a"));
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_TRUE(cache.Find("c"));
+ EXPECT_EQ(cache.Get("b"), 2);
+ EXPECT_EQ(cache.Get("c"), 3);
+
+ cache.Insert("b", 4);
+
+ EXPECT_FALSE(cache.Find("a"));
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_TRUE(cache.Find("c"));
+ EXPECT_EQ(cache.Get("c"), 3);
+ EXPECT_EQ(cache.Get("b"), 4);
+
+ cache.Insert("a", 5);
+
+ EXPECT_TRUE(cache.Find("a"));
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_FALSE(cache.Find("c"));
+ EXPECT_EQ(cache.Get("a"), 5);
+ EXPECT_EQ(cache.Get("b"), 4);
+}
+
+TEST(TSimpleLruCache, Clear)
+{
+ TSimpleLruCache<TString, int> cache(2);
+ cache.Insert("a", 1);
+ cache.Insert("b", 2);
+
+ cache.Clear();
+ EXPECT_FALSE(cache.Find("a"));
+ EXPECT_FALSE(cache.Find("b"));
+
+ cache.Insert("c", 3);
+ cache.Insert("d", 4);
+ cache.Insert("e", 5);
+
+ EXPECT_FALSE(cache.Find("c"));
+ EXPECT_TRUE(cache.Find("d"));
+ EXPECT_TRUE(cache.Find("e"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMultiLruCache, InsertAndFind)
+{
+ TMultiLruCache<TString, int> cache(3);
+
+ EXPECT_EQ(cache.GetSize(), 0u);
+
+ cache.Insert("a", 1);
+ cache.Insert("b", 2);
+ cache.Insert("a", 3);
+
+ EXPECT_EQ(cache.GetSize(), 3u);
+
+ ASSERT_TRUE(cache.Find("a"));
+ ASSERT_TRUE(cache.Find("b"));
+ EXPECT_FALSE(cache.Find("c"));
+
+ EXPECT_EQ(cache.Get("a"), 3);
+ EXPECT_EQ(cache.Get("a"), 1);
+ EXPECT_EQ(cache.Get("b"), 2);
+ EXPECT_EQ(cache.Get("b"), 2);
+
+ cache.Insert("b", 4);
+
+ EXPECT_EQ(cache.GetSize(), 3u);
+ ASSERT_TRUE(cache.Find("a"));
+ ASSERT_TRUE(cache.Find("b"));
+ EXPECT_FALSE(cache.Find("c"));
+
+ EXPECT_EQ(cache.Get("a"), 1);
+ EXPECT_EQ(cache.Get("a"), 1);
+ EXPECT_EQ(cache.Get("b"), 4);
+ EXPECT_EQ(cache.Get("b"), 2);
+
+ EXPECT_TRUE(cache.Find("a"));
+
+ cache.Insert("c", 5);
+
+ EXPECT_EQ(cache.GetSize(), 3u);
+ EXPECT_TRUE(cache.Find("a"));
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_TRUE(cache.Find("c"));
+
+ cache.Clear();
+ EXPECT_EQ(cache.GetSize(), 0u);
+}
+
+TEST(TMultiLruCache, Extract)
+{
+ TMultiLruCache<TString, int> cache(3);
+
+ cache.Insert("a", 1);
+ cache.Insert("b", 2);
+ cache.Insert("a", 3);
+
+ EXPECT_FALSE(cache.Extract("c"));
+
+ EXPECT_EQ(*cache.Extract("a"), 1);
+ EXPECT_EQ(cache.GetSize(), 2u);
+
+ cache.Insert("b", 4);
+
+ EXPECT_EQ(*cache.Extract("a"), 3);
+ EXPECT_EQ(cache.GetSize(), 2u);
+
+ cache.Insert("c", 1);
+
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_EQ(*cache.Extract("b"), 4);
+ EXPECT_EQ(cache.GetSize(), 2u);
+ EXPECT_TRUE(cache.Find("b"));
+ EXPECT_EQ(*cache.Extract("b"), 2);
+
+ EXPECT_FALSE(cache.Extract("b"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/maybe_inf_ut.cpp b/yt/yt/core/misc/unittests/maybe_inf_ut.cpp
new file mode 100644
index 0000000000..ac2d3ce9d8
--- /dev/null
+++ b/yt/yt/core/misc/unittests/maybe_inf_ut.cpp
@@ -0,0 +1,43 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/maybe_inf.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMaybeInf, IncreaseBy)
+{
+ using TLimit = TMaybeInf<ui32>;
+ TLimit a(1);
+ EXPECT_FALSE(a.CanBeIncreased(TLimit::Infinity()));
+}
+
+TEST(TMaybeInf, DecreaseBy)
+{
+ using TLimit = TMaybeInf<ui32>;
+ TLimit a(1);
+ auto b = TLimit::Infinity();
+ EXPECT_FALSE(a.CanBeDecreased(b));
+ a = TLimit::Infinity();
+ EXPECT_FALSE(a.CanBeDecreased(b));
+ b = TLimit();
+ EXPECT_TRUE(a.CanBeDecreased(b));
+ b = TLimit(1);
+ EXPECT_FALSE(a.CanBeDecreased(b));
+ a = TLimit(4);
+ EXPECT_TRUE(a.CanBeDecreased(b));
+ b = TLimit(4);
+ EXPECT_TRUE(a.CanBeDecreased(b));
+ a.DecreaseBy(b);
+ EXPECT_EQ(a.ToUnderlying(), 0u);
+ a = TLimit(4);
+ b = TLimit(5);
+ EXPECT_FALSE(a.CanBeDecreased(b));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/memory_tag_ut.cpp b/yt/yt/core/misc/unittests/memory_tag_ut.cpp
new file mode 100644
index 0000000000..346cb86e0b
--- /dev/null
+++ b/yt/yt/core/misc/unittests/memory_tag_ut.cpp
@@ -0,0 +1,244 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <library/cpp/yt/memory/memory_tag.h>
+
+#include <util/random/random.h>
+
+#include <util/system/compiler.h>
+
+// These tests do not work under MSAN and ASAN.
+#if !defined(_msan_enabled_) and !defined(_asan_enabled_) and defined(_linux_) and defined(YT_ALLOC_ENABLED)
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Used for fake side effects to disable compiler optimizations.
+volatile const void* FakeSideEffectVolatileVariable = nullptr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+using namespace NConcurrency;
+using namespace ::testing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMemoryTagTest
+ : public TestWithParam<void(*)()>
+{
+public:
+ TMemoryTagTest() = default;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Allocate vector that results in exactly `size` memory usage considering the 16-byte header.
+std::vector<char> MakeAllocation(size_t size)
+{
+ YT_VERIFY(IsPowerOf2(size));
+
+ auto result = std::vector<char>(size);
+
+ // We make fake side effect to prevent any compiler optimizations here.
+ // (Clever compilers like to throw away our unused allocations).
+ FakeSideEffectVolatileVariable = result.data();
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestStackingGuards()
+{
+ TMemoryTagGuard guard1(1);
+ EXPECT_EQ(GetMemoryUsageForTag(1), 0u);
+ auto allocation1 = MakeAllocation(1 << 5);
+ EXPECT_EQ(GetMemoryUsageForTag(1), 1u << 5);
+ {
+ TMemoryTagGuard guard2(2);
+ auto allocation2 = MakeAllocation(1 << 6);
+ EXPECT_EQ(GetMemoryUsageForTag(1), 1u << 5);
+ EXPECT_EQ(GetMemoryUsageForTag(2), 1u << 6);
+ }
+ EXPECT_EQ(GetMemoryUsageForTag(1), 1u << 5);
+ EXPECT_EQ(GetMemoryUsageForTag(2), 0u);
+ {
+ TMemoryTagGuard guard2(std::move(guard1));
+ auto allocation2 = MakeAllocation(1 << 7);
+ EXPECT_EQ(GetMemoryUsageForTag(1), (1u << 5) + (1u << 7));
+ EXPECT_EQ(GetMemoryUsageForTag(2), 0u);
+ }
+ EXPECT_EQ(GetMemoryUsageForTag(1), (1u << 5));
+ EXPECT_EQ(GetMemoryUsageForTag(2), 0u);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Action1()
+{
+ TMemoryTagGuard guard(1);
+ Yield();
+ auto allocation1 = MakeAllocation(1 << 5);
+ EXPECT_EQ(GetMemoryUsageForTag(1), 1u << 5);
+ Yield();
+ auto allocation2 = MakeAllocation(1 << 7);
+ EXPECT_EQ(GetMemoryUsageForTag(1), (1u << 5) + (1u << 7));
+ Yield();
+ auto allocation3 = MakeAllocation(1 << 9);
+ EXPECT_EQ(GetMemoryUsageForTag(1), (1u << 5) + (1u << 7) + (1u << 9));
+}
+
+void Action2()
+{
+ TMemoryTagGuard guard(2);
+ Yield();
+ auto allocation1 = MakeAllocation(1 << 6);
+ EXPECT_EQ(GetMemoryUsageForTag(2), 1u << 6);
+ Yield();
+ auto allocation2 = MakeAllocation(1 << 8);
+ EXPECT_EQ(GetMemoryUsageForTag(2), (1u << 6) + (1u << 8));
+ Yield();
+ auto allocation3 = MakeAllocation(1 << 10);
+ EXPECT_EQ(GetMemoryUsageForTag(2), (1u << 6) + (1u << 8) + (1u << 10));
+}
+
+void TestSwitchingFibers()
+{
+ auto future1 = BIND(&Action1)
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ auto future2 = BIND(&Action2)
+ .AsyncVia(GetCurrentInvoker())
+ .Run();
+ WaitFor(AllSucceeded(std::vector<TFuture<void>>{future1, future2}))
+ .ThrowOnError();
+ EXPECT_EQ(GetMemoryUsageForTag(1), 0u);
+ EXPECT_EQ(GetMemoryUsageForTag(2), 0u);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMiniController
+ : public TRefCounted
+{
+public:
+ TMiniController(IInvokerPtr controlInvoker, TMemoryTag memoryTag)
+ : MemoryTag_(memoryTag)
+ , Invoker_(CreateMemoryTaggingInvoker(CreateSerializedInvoker(std::move(controlInvoker)), MemoryTag_))
+ { }
+
+ ssize_t GetMemoryUsage() const
+ {
+ return GetMemoryUsageForTag(MemoryTag_);
+ }
+
+ IInvokerPtr GetControlInvoker() const
+ {
+ return Invoker_;
+ }
+
+ std::vector<std::vector<char>>& Allocations()
+ {
+ return Allocations_;
+ }
+
+private:
+ TMemoryTag MemoryTag_;
+ IInvokerPtr Invoker_;
+ std::vector<std::vector<char>> Allocations_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TMiniController)
+DECLARE_REFCOUNTED_CLASS(TMiniController)
+
+void Action3(TMiniControllerPtr controller)
+{
+ controller->Allocations().emplace_back(MakeAllocation(128_MB));
+}
+
+void TestMemoryTaggingInvoker()
+{
+ auto queue = New<TActionQueue>();
+ auto controller = New<TMiniController>(queue->GetInvoker(), 1);
+ EXPECT_EQ(controller->GetMemoryUsage(), 0);
+
+ WaitFor(BIND(&Action3, controller)
+ .AsyncVia(controller->GetControlInvoker())
+ .Run())
+ .ThrowOnError();
+ EXPECT_NEAR(controller->GetMemoryUsage(), 128_MB, 1_MB);
+
+ controller->Allocations().clear();
+ controller->Allocations().shrink_to_fit();
+
+ EXPECT_NEAR(GetMemoryUsageForTag(1), 0, 1_MB);
+}
+
+void TestControllersInThreadPool()
+{
+ std::vector<TMiniControllerPtr> controllers;
+ constexpr int controllerCount = 1000;
+ auto pool = CreateThreadPool(16, "TestPool");
+ for (int index = 0; index < controllerCount; ++index) {
+ controllers.emplace_back(New<TMiniController>(pool->GetInvoker(), index + 1));
+ }
+ constexpr int actionCount = 100 * 1000;
+ std::vector<TFuture<void>> futures;
+ std::vector<int> memoryUsages(controllerCount);
+ srand(42);
+ for (int index = 0; index < actionCount; ++index) {
+ int controllerIndex = rand() % controllerCount;
+ auto allocationSize = 1 << (5 + rand() % 10);
+ memoryUsages[controllerIndex] += allocationSize;
+ const auto& controller = controllers[controllerIndex];
+ futures.emplace_back(
+ BIND([] (TMiniControllerPtr controller, int allocationSize) {
+ controller->Allocations().emplace_back(MakeAllocation(allocationSize));
+ }, controller, allocationSize)
+ .AsyncVia(controller->GetControlInvoker())
+ .Run());
+ }
+ WaitFor(AllSucceeded(futures))
+ .ThrowOnError();
+ for (int index = 0; index < controllerCount; ++index) {
+ EXPECT_NEAR(memoryUsages[index], controllers[index]->GetMemoryUsage(), 10_KB);
+ }
+ controllers.clear();
+ for (int index = 0; index < controllerCount; ++index) {
+ EXPECT_NEAR(GetMemoryUsageForTag(index + 1), 0, 10_KB);
+ EXPECT_GE(GetMemoryUsageForTag(index + 1), 0u);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_P(TMemoryTagTest, Test)
+{
+ // We wrap anything with an outer action queue to make
+ // fiber-friendly environment.
+ auto outerQueue = New<TActionQueue>();
+ WaitFor(BIND(GetParam())
+ .AsyncVia(outerQueue->GetInvoker())
+ .Run())
+ .ThrowOnError();
+}
+
+INSTANTIATE_TEST_SUITE_P(MemoryTagTest, TMemoryTagTest, Values(
+ &TestStackingGuards,
+ &TestSwitchingFibers,
+ &TestMemoryTaggingInvoker,
+ &TestControllersInThreadPool));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
+#endif // !defined(_msan_enabled_)
diff --git a/yt/yt/core/misc/unittests/mpl_ut.cpp b/yt/yt/core/misc/unittests/mpl_ut.cpp
new file mode 100644
index 0000000000..8ed7c51986
--- /dev/null
+++ b/yt/yt/core/misc/unittests/mpl_ut.cpp
@@ -0,0 +1,23 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/mpl.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMetaProgrammingTest, IsPod)
+{
+ EXPECT_TRUE (( NMpl::TIsPod<char>::value ));
+ EXPECT_TRUE (( NMpl::TIsPod<int>::value ));
+ EXPECT_TRUE (( NMpl::TIsPod<short>::value ));
+ EXPECT_TRUE (( NMpl::TIsPod<long>::value ));
+ EXPECT_TRUE (( NMpl::TIsPod<float>::value ));
+ EXPECT_TRUE (( NMpl::TIsPod<double>::value ));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/mpsc_fair_share_queue_ut.cpp b/yt/yt/core/misc/unittests/mpsc_fair_share_queue_ut.cpp
new file mode 100644
index 0000000000..d669bc482d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/mpsc_fair_share_queue_ut.cpp
@@ -0,0 +1,392 @@
+#include <yt/yt/core/misc/mpsc_fair_share_queue.h>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/moody_camel_concurrent_queue.h>
+
+#include <util/random/shuffle.h>
+
+#include <numeric>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMockTask final
+{
+ explicit TMockTask(int taskId)
+ : TaskId(taskId)
+ { }
+
+ int TaskId = 0;
+};
+
+using TMockTaskPtr = TIntrusivePtr<TMockTask>;
+
+using TTestFairShareQueue = TMpscFairShareQueue<int, TMockTaskPtr, std::string>;
+
+template<typename TPoolId, typename TItem, typename TFairShareTag>
+std::vector<TItem> DequeueMany(int count, TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>& queue)
+{
+ std::vector<TItem> result;
+ result.reserve(count);
+
+ queue.PrepareDequeue();
+
+ for (int i = 0; i < count; ++i) {
+ if (auto task = queue.TryDequeue()) {
+ result.push_back(std::move(task));
+ } else {
+ break;
+ }
+ }
+
+ return result;
+}
+
+template<typename TPoolId, typename TItem, typename TFairShareTag>
+void MarkFinishedMany(TMpscFairShareQueue<TPoolId, TItem, TFairShareTag>& queue, const std::vector<TItem>& requests)
+{
+ auto now = GetCpuInstant();
+
+ for (const auto& request : requests) {
+ queue.MarkFinished(request, now);
+ }
+}
+
+std::string ToString(const THashSet<int>& expected)
+{
+ std::ostringstream stream;
+
+ for (auto x : expected) {
+ stream << x << ",";
+ }
+
+ return stream.str();
+}
+
+bool CheckEqual(const auto& tasks, const THashSet<int>& expected)
+{
+ THashSet<int> actual;
+ for (const auto& task : tasks) {
+ actual.insert(task->TaskId);
+ }
+
+ EXPECT_TRUE(actual == expected) <<
+ "Expected: {" << ToString(expected) <<
+ "} Actual: {" << ToString(actual) << "}";
+
+ return actual == expected;
+}
+
+TEST(TMpscFairShareQueueTest, Simple)
+{
+ TTestFairShareQueue queue;
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(1),
+ .PoolId = 1,
+ .PoolWeight = 4,
+ .FairShareTag = "request_1",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(2),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_3",
+ });
+
+ std::vector<TMockTaskPtr> running;
+ {
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {1, 2}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+ }
+
+ {
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_EQ(std::ssize(tasks), 0);
+ }
+
+ Sleep(TDuration::MilliSeconds(10));
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(5),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_3",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(4),
+ .PoolId = 1,
+ .PoolWeight = 4,
+ .FairShareTag = "request_2",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(3),
+ .PoolId = 1,
+ .PoolWeight = 4,
+ .FairShareTag = "request_1",
+ });
+
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {3, 4}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+
+ MarkFinishedMany(queue, running);
+
+ tasks = DequeueMany(10, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {5}));
+ MarkFinishedMany(queue, tasks);
+
+ tasks = DequeueMany(100, queue);
+ MarkFinishedMany(queue, tasks);
+
+ queue.Cleanup();
+ EXPECT_EQ(0, queue.GetPoolCount());
+}
+
+TEST(TMpscFairShareQueueTest, StdSmartPtr)
+{
+ TMpscFairShareQueue<int, std::unique_ptr<TMockTask>, std::string> queue;
+
+ queue.Enqueue({
+ .Item = std::make_unique<TMockTask>(1),
+ .PoolId = 1,
+ .PoolWeight = 4,
+ .FairShareTag = "request_1",
+ });
+
+ queue.Enqueue({
+ .Item = std::make_unique<TMockTask>(2),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_3",
+ });
+
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {1, 2}));
+
+ MarkFinishedMany(queue, tasks);
+}
+
+TEST(TMpscFairShareQueueTest, FairNewBucket)
+{
+ TTestFairShareQueue queue;
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(1),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_1",
+ });
+
+ std::vector<TMockTaskPtr> running;
+ {
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {1}));
+ running.push_back(tasks.front());
+ }
+
+ Sleep(TDuration::MilliSeconds(100));
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(2),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_1",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(3),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+ queue.Enqueue({
+ .Item = New<TMockTask>(4),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ {
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {3, 4}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+ }
+
+ Sleep(TDuration::MilliSeconds(10));
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(6),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(7),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ {
+ auto tasks = DequeueMany(1, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {2}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+ }
+
+ auto tasks = DequeueMany(10, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {6, 7}));
+ MarkFinishedMany(queue, tasks);
+
+ MarkFinishedMany(queue, running);
+
+ tasks = DequeueMany(100, queue);
+ MarkFinishedMany(queue, tasks);
+
+ queue.Cleanup();
+ EXPECT_EQ(0, queue.GetPoolCount());
+}
+
+TEST(TMpscFairShareQueueTest, FairNewPool)
+{
+ TTestFairShareQueue queue;
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(1),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_1",
+ });
+
+ std::vector<TMockTaskPtr> running;
+ {
+ auto tasks = DequeueMany(2, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {1}));
+ running.push_back(tasks.front());
+ }
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(2),
+ .PoolId = 1,
+ .PoolWeight = 1,
+ .FairShareTag = "request_1",
+ });
+
+ Sleep(TDuration::MilliSeconds(100));
+ queue.Enqueue({
+ .Item = New<TMockTask>(3),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+ queue.Enqueue({
+ .Item = New<TMockTask>(4),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ {
+ auto tasks = DequeueMany(1, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {3}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+ }
+
+ Sleep(TDuration::MilliSeconds(10));
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(6),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ queue.Enqueue({
+ .Item = New<TMockTask>(7),
+ .PoolId = 2,
+ .PoolWeight = 1,
+ .FairShareTag = "request_2",
+ });
+
+ {
+ auto tasks = DequeueMany(1, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {4}));
+ running.insert(running.end(), tasks.begin(), tasks.end());
+ }
+ auto tasks = DequeueMany(10, queue);
+ EXPECT_TRUE(CheckEqual(tasks, {6, 7, 2}));
+ MarkFinishedMany(queue, tasks);
+
+ MarkFinishedMany(queue, running);
+
+ tasks = DequeueMany(100, queue);
+ MarkFinishedMany(queue, tasks);
+
+ queue.Cleanup();
+ EXPECT_EQ(0, queue.GetPoolCount());
+}
+
+TEST(TMpscFairShareQueueTest, Bench)
+{
+ const int TaskCount = 1'000'000;
+ const int PoolsCount = 10;
+ const int BucketsCount = 100;
+
+ using TTestFairShareQueue = TMpscFairShareQueue<int, i64, int>;
+
+ TTestFairShareQueue queue;
+
+ int expectedRunningCount = 4096;
+
+ std::vector<i64> running;
+ int tasksIndex = 1;
+ while (tasksIndex < TaskCount) {
+ int tasksToSchedule = RandomNumber<ui32>(expectedRunningCount);
+ for (int i = 0; i < tasksToSchedule; ++i) {
+ queue.Enqueue({
+ .Item = tasksIndex,
+ .PoolId = tasksIndex % PoolsCount,
+ .PoolWeight = double(tasksIndex % PoolsCount) + 1,
+ .FairShareTag = int(RandomNumber<ui32>(BucketsCount)),
+ });
+ ++tasksIndex;
+ }
+
+ int dequeCount = RandomNumber<ui32>(expectedRunningCount);
+ auto tasks = DequeueMany(dequeCount, queue);
+ running.insert(running.end(), tasks.begin(), tasks.end());
+
+ auto finishedCount = RandomNumber<ui32>(expectedRunningCount);
+ {
+ Shuffle(running.begin(), running.end());
+ int splitSize = std::min<ui32>(finishedCount, std::ssize(running));
+ std::vector<i64> finished(running.begin() + splitSize, running.end());
+ running.resize(std::ssize(running) - std::ssize(finished));
+
+ YT_VERIFY(std::ssize(running) == splitSize);
+ MarkFinishedMany(queue, finished);
+ }
+ }
+
+ EXPECT_TRUE(std::ssize(running) < expectedRunningCount * 10);
+ MarkFinishedMany(queue, running);
+
+ auto tasks = DequeueMany(TaskCount, queue);
+ MarkFinishedMany(queue, tasks);
+
+ queue.Cleanup();
+ EXPECT_EQ(0, queue.GetPoolCount());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/mpsc_queue_ut.cpp b/yt/yt/core/misc/unittests/mpsc_queue_ut.cpp
new file mode 100644
index 0000000000..7534e0d045
--- /dev/null
+++ b/yt/yt/core/misc/unittests/mpsc_queue_ut.cpp
@@ -0,0 +1,140 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/mpsc_queue.h>
+
+#include <thread>
+#include <array>
+
+namespace NYT {
+namespace {
+
+using ::testing::TProbeState;
+using ::testing::TProbe;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMpscQueueTest, SingleThreaded1)
+{
+ TMpscQueue<int> queue;
+
+ queue.Enqueue(1);
+ queue.Enqueue(2);
+ queue.Enqueue(3);
+
+ int value;
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(1, value);
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(2, value);
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(3, value);
+
+ EXPECT_FALSE(queue.TryDequeue(&value));
+};
+
+TEST(TMpscQueueTest, SingleThreaded2)
+{
+ TMpscQueue<int> queue;
+
+ queue.Enqueue(1);
+ queue.Enqueue(2);
+
+ int value;
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(1, value);
+
+ queue.Enqueue(3);
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(2, value);
+
+ EXPECT_TRUE(queue.TryDequeue(&value));
+ EXPECT_EQ(3, value);
+
+ EXPECT_FALSE(queue.TryDequeue(&value));
+};
+
+TEST(TMpscQueueTest, MultiThreaded)
+{
+ TMpscQueue<int> queue;
+
+ constexpr int N = 10000;
+ constexpr int T = 4;
+
+ auto barrier = NewPromise<void>();
+
+ auto producer = [&] {
+ barrier.ToFuture().Get();
+ for (int i = 0; i < N; ++i) {
+ queue.Enqueue(i);
+ }
+ };
+
+ auto consumer = [&] {
+ std::array<int, N> counts{};
+ barrier.ToFuture().Get();
+ for (int i = 0; i < N * T; ++i) {
+ int item;
+ while (!queue.TryDequeue(&item));
+ counts[item]++;
+ }
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(counts[i], T);
+ }
+ };
+
+ std::vector<std::thread> threads;
+
+ for (int i = 0; i < T; ++i) {
+ threads.emplace_back(producer);
+ }
+ threads.emplace_back(consumer);
+
+ barrier.Set();
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+}
+
+TEST(TMpscQueueTest, Drain)
+{
+ TProbeState probeState;
+ TMpscQueue<TProbe> queue;
+
+ queue.Enqueue(TProbe(&probeState));
+ queue.Enqueue(TProbe(&probeState));
+ queue.Enqueue(TProbe(&probeState));
+ {
+ auto probe = TProbe::ExplicitlyCreateInvalidProbe();
+ EXPECT_TRUE(queue.TryDequeue(&probe));
+ }
+ EXPECT_EQ(3, probeState.Constructors);
+ EXPECT_EQ(1, probeState.Destructors);
+ EXPECT_EQ(5, probeState.ShadowDestructors);
+ EXPECT_EQ(3, probeState.MoveConstructors);
+
+ queue.DrainConsumer();
+ EXPECT_EQ(3, probeState.Constructors);
+ EXPECT_EQ(3, probeState.Destructors);
+ EXPECT_EQ(7, probeState.ShadowDestructors);
+ EXPECT_EQ(3, probeState.MoveConstructors);
+
+ queue.Enqueue(TProbe(&probeState));
+ queue.DrainProducer();
+ EXPECT_EQ(4, probeState.Constructors);
+ EXPECT_EQ(4, probeState.Destructors);
+ EXPECT_EQ(9, probeState.ShadowDestructors);
+ EXPECT_EQ(4, probeState.MoveConstructors);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/mpsc_stack_ut.cpp b/yt/yt/core/misc/unittests/mpsc_stack_ut.cpp
new file mode 100644
index 0000000000..fe3aa5878f
--- /dev/null
+++ b/yt/yt/core/misc/unittests/mpsc_stack_ut.cpp
@@ -0,0 +1,119 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/mpsc_stack.h>
+
+#include <thread>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TMpscStackTest, Simple)
+{
+ TMpscStack<int> stack;
+ stack.Enqueue(1);
+ stack.Enqueue(2);
+
+ int x;
+ EXPECT_FALSE(stack.IsEmpty());
+ EXPECT_TRUE(stack.TryDequeue(&x));
+ EXPECT_EQ(2, x);
+ EXPECT_FALSE(stack.IsEmpty());
+ EXPECT_TRUE(stack.TryDequeue(&x));
+ EXPECT_EQ(1, x);
+ EXPECT_TRUE(stack.IsEmpty());
+ EXPECT_FALSE(stack.TryDequeue(&x));
+}
+
+TEST(TMpscStackTest, DequeueAll)
+{
+ {
+ TMpscStack<int> stack;
+ for (int i = 0; i < 5; ++i) {
+ stack.Enqueue(i);
+ }
+
+ auto values = stack.DequeueAll(/*reverse*/ false);
+ EXPECT_TRUE(stack.IsEmpty());
+ EXPECT_EQ(values, std::vector<int>({4, 3, 2, 1, 0}));
+ }
+ {
+ TMpscStack<int> stack;
+ for (int i = 0; i < 5; ++i) {
+ stack.Enqueue(i);
+ }
+
+ auto values = stack.DequeueAll(/*reverse*/ true);
+ EXPECT_TRUE(stack.IsEmpty());
+ EXPECT_EQ(values, std::vector<int>({0, 1, 2, 3, 4}));
+ }
+}
+
+TEST(TMpscStackTest, ConcurrentTryDequeue)
+{
+ constexpr i64 Size = 100'000;
+
+ TMpscStack<int> stack;
+
+ auto run = [&] {
+ Sleep(TDuration::MilliSeconds(50));
+ for (int i = 0; i < Size; ++i) {
+ stack.Enqueue(i);
+ }
+ };
+
+ std::thread t1(run);
+ std::thread t2(run);
+
+ i64 sum = 0;
+ for (int i = 0; i < 2 * Size; ++i) {
+ int x = -1;
+ while (!stack.TryDequeue(&x));
+ sum += x;
+ }
+
+ EXPECT_EQ(sum, Size * (Size - 1));
+
+ t1.join();
+ t2.join();
+ EXPECT_TRUE(stack.IsEmpty());
+}
+
+TEST(TMpscStackTest, ConcurrentTryDequeueAll)
+{
+ constexpr i64 Size = 100'000;
+
+ TMpscStack<int> stack;
+
+ auto run = [&] {
+ Sleep(TDuration::MilliSeconds(50));
+ for (int i = 0; i < Size; ++i) {
+ stack.Enqueue(i);
+ }
+ };
+
+ std::thread t1(run);
+ std::thread t2(run);
+
+ int count = 0;
+ i64 sum = 0;
+ while (count < 2 * Size) {
+ auto values = stack.DequeueAll(/*reverse*/ rand() % 2 == 0);
+ count += values.size();
+ for (int x : values) {
+ sum += x;
+ }
+ }
+
+ EXPECT_EQ(sum, Size * (Size - 1));
+
+ t1.join();
+ t2.join();
+ EXPECT_TRUE(stack.IsEmpty());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/pattern_formatter_ut.cpp b/yt/yt/core/misc/unittests/pattern_formatter_ut.cpp
new file mode 100644
index 0000000000..ae4c43e633
--- /dev/null
+++ b/yt/yt/core/misc/unittests/pattern_formatter_ut.cpp
@@ -0,0 +1,94 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/pattern_formatter.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPatternFormatterTest
+ : public ::testing::Test
+{
+protected:
+ TPatternFormatter Formatter;
+};
+
+#define EXPECT_FORMAT(pattern, expected) \
+ EXPECT_EQ((expected), Formatter.Format(pattern))
+
+TEST_F(TPatternFormatterTest, EmptyPattern)
+{
+ Formatter.AddProperty("key", "value");
+ EXPECT_FORMAT("", "");
+}
+
+TEST_F(TPatternFormatterTest, PatternWithoutProperties)
+{
+ Formatter.AddProperty("key", "value");
+ EXPECT_FORMAT("some text", "some text");
+}
+
+TEST_F(TPatternFormatterTest, PropertyWithEmptyName1)
+{
+ Formatter.AddProperty("", "");
+ EXPECT_FORMAT("<<-$()->>", "<<-->>");
+}
+
+TEST_F(TPatternFormatterTest, PropertyWithEmptyName2)
+{
+ Formatter.AddProperty("", "foobar");
+ EXPECT_FORMAT("<<-$()->>", "<<-foobar->>");
+}
+
+TEST_F(TPatternFormatterTest, CommonCase)
+{
+ Formatter.AddProperty("a", "1");
+ Formatter.AddProperty("b", "10");
+ Formatter.AddProperty("c", "100");
+ EXPECT_FORMAT("$(a)", "1");
+ EXPECT_FORMAT(
+ "Hello! You own me $(a)$(b) dollars; not $(c)!",
+ "Hello! You own me 110 dollars; not 100!");
+
+}
+
+TEST_F(TPatternFormatterTest, MultiplePropertyDeclaration)
+{
+ Formatter.AddProperty("x", "1");
+ EXPECT_FORMAT("<$(x)>", "<1>");
+ Formatter.AddProperty("x", "2");
+ EXPECT_FORMAT("<$(x)>", "<2>");
+ Formatter.AddProperty("x", "3");
+ EXPECT_FORMAT("<$(x)>", "<3>");
+}
+
+TEST_F(TPatternFormatterTest, MultiplePropertyUsage)
+{
+ Formatter.AddProperty("x", "2");
+ EXPECT_FORMAT("$(x) = 2", "2 = 2");
+ EXPECT_FORMAT("$(x) + $(x) = 4", "2 + 2 = 4");
+ EXPECT_FORMAT("$(x) + $(x) + $(x) = 6", "2 + 2 + 2 = 6");
+}
+
+TEST_F(TPatternFormatterTest, UndefinedValue)
+{
+ Formatter.AddProperty("a", "b");
+ Formatter.AddProperty("key", "value");
+ EXPECT_FORMAT("$(a) $(kkeeyy)", "b $(kkeeyy)");
+ EXPECT_FORMAT("$$(a)", "$b");
+}
+
+TEST_F(TPatternFormatterTest, NoRightParenthesis)
+{
+ Formatter.AddProperty("a", "b");
+ EXPECT_FORMAT("$(a", "$(a");
+}
+
+#undef EXPECT_FORMAT
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/persistent_queue_ut.cpp b/yt/yt/core/misc/unittests/persistent_queue_ut.cpp
new file mode 100644
index 0000000000..bdb934d816
--- /dev/null
+++ b/yt/yt/core/misc/unittests/persistent_queue_ut.cpp
@@ -0,0 +1,133 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/persistent_queue.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TQueueType = TPersistentQueue<int, 10>;
+using TSnapshot = TPersistentQueueSnapshot<int, 10>;
+
+TEST(TPersistentQueue, Empty)
+{
+ TQueueType queue;
+ EXPECT_EQ(0u, queue.Size());
+ EXPECT_TRUE(queue.Empty());
+ EXPECT_EQ(queue.Begin(), queue.End());
+
+ auto snapshot = queue.MakeSnapshot();
+ EXPECT_EQ(0u, snapshot.Size());
+ EXPECT_TRUE(snapshot.Empty());
+ EXPECT_EQ(snapshot.Begin(), snapshot.End());
+}
+
+TEST(TPersistentQueue, EnqueueDequeue)
+{
+ TQueueType queue;
+
+ const int N = 100;
+
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(i, static_cast<ssize_t>(queue.Size()));
+ queue.Enqueue(i);
+ }
+
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(N - i, static_cast<ssize_t>(queue.Size()));
+ EXPECT_EQ(i, queue.Dequeue());
+ }
+}
+
+TEST(TPersistentQueue, Iterate)
+{
+ TQueueType queue;
+
+ const int N = 100;
+
+ for (int i = 0; i < 2 * N; ++i) {
+ queue.Enqueue(i);
+ }
+
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(i, queue.Dequeue());
+ }
+
+ int expected = N;
+ for (int x : queue) {
+ EXPECT_EQ(expected, x);
+ ++expected;
+ }
+}
+
+TEST(TPersistentQueue, Snapshot1)
+{
+ TQueueType queue;
+
+ const int N = 100;
+ std::vector<TSnapshot> snapshots;
+
+ for (int i = 0; i < N; ++i) {
+ snapshots.push_back(queue.MakeSnapshot());
+ queue.Enqueue(i);
+ }
+
+ for (int i = 0; i < N; ++i) {
+ const auto& snapshot = snapshots[i];
+ EXPECT_EQ(i, static_cast<ssize_t>(snapshot.Size()));
+ int expected = 0;
+ for (int x : snapshot) {
+ EXPECT_EQ(expected, x);
+ ++expected;
+ }
+ }
+}
+
+TEST(TPersistentQueue, Snapshot2)
+{
+ TQueueType queue;
+
+ const int N = 100;
+ std::vector<TSnapshot> snapshots;
+
+ for (int i = 0; i < N; ++i) {
+ queue.Enqueue(i);
+ }
+
+ for (int i = 0; i < N; ++i) {
+ snapshots.push_back(queue.MakeSnapshot());
+ EXPECT_EQ(i, queue.Dequeue());
+ }
+
+ for (int i = 0; i < N; ++i) {
+ const auto& snapshot = snapshots[i];
+ EXPECT_EQ(i, static_cast<ssize_t>(N - snapshot.Size()));
+ int expected = i;
+ for (int x : snapshot) {
+ EXPECT_EQ(expected, x);
+ ++expected;
+ }
+ }
+}
+
+TEST(TPersistentQueue, Clear)
+{
+ TQueueType queue;
+
+ queue.Enqueue(1);
+
+ EXPECT_EQ(1u, queue.Size());
+
+ queue.Clear();
+
+ EXPECT_EQ(0u, queue.Size());
+
+ auto snapshot = queue.MakeSnapshot();
+ EXPECT_EQ(snapshot.Begin(), snapshot.End());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/phoenix_ut.cpp b/yt/yt/core/misc/unittests/phoenix_ut.cpp
new file mode 100644
index 0000000000..1d41675444
--- /dev/null
+++ b/yt/yt/core/misc/unittests/phoenix_ut.cpp
@@ -0,0 +1,625 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/blob_output.h>
+#include <yt/yt/core/misc/phoenix.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TSaveContext = NPhoenix::TSaveContext;
+using TLoadContext = NPhoenix::TLoadContext;
+using IPersistent = NPhoenix::IPersistent;
+using TPersistenceContext = NPhoenix::TPersistenceContext;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TSharedRef Serialize(const T& value)
+{
+ TBlobOutput output;
+ TSaveContext context(&output);
+ Save(context, value);
+ context.Finish();
+ return output.Flush();
+}
+
+template <class T>
+void Deserialize(T& value, TRef ref)
+{
+ TMemoryInput input(ref.Begin(), ref.Size());
+ TLoadContext context(&input);
+ Load(context, value);
+}
+
+template <class T>
+void InplaceDeserialize(TIntrusivePtr<T> value, TRef ref)
+{
+ TMemoryInput input(ref.Begin(), ref.Size());
+ TLoadContext context(&input);
+ NPhoenix::TSerializer::InplaceLoad(context, value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NSimple {
+
+TEST(TPhoenixTest, Scalar)
+{
+ struct C
+ {
+ C()
+ : A(-1)
+ , B(-1)
+ { }
+
+ int A;
+ double B;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, A);
+ Persist(context, B);
+ }
+ };
+
+ C c1;
+ c1.A = 10;
+ c1.B = 3.14;
+
+ C c2;
+ Deserialize(c2, Serialize(c1));
+
+ EXPECT_EQ(c1.A, c2.A);
+ EXPECT_EQ(c1.B, c2.B);
+}
+
+TEST(TPhoenixTest, Guid)
+{
+ TGuid g1(1, 2, 3, 4);
+
+ TGuid g2(5, 6, 7, 8);
+ Deserialize(g2, Serialize(g1));
+
+ EXPECT_EQ(g1, g2);
+}
+
+TEST(TPhoenixTest, Vector)
+{
+ std::vector<int> v1;
+ v1.push_back(1);
+ v1.push_back(2);
+ v1.push_back(3);
+
+ std::vector<int> v2;
+ v2.push_back(33);
+ Deserialize(v2, Serialize(v1));
+
+ EXPECT_EQ(v1, v2);
+}
+
+} // namespace NSimple
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef1 {
+
+struct A;
+struct B;
+
+struct A
+ : public TRefCounted
+{
+ TIntrusivePtr<B> X;
+ TIntrusivePtr<B> Y;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ Persist(context, Y);
+ }
+
+};
+
+struct B
+ : public TRefCounted
+{
+ B()
+ : V(-1)
+ { }
+
+ int V;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, V);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref1)
+{
+ auto a1 = New<A>();
+ a1->X = New<B>();
+ a1->X->V = 1;
+ a1->Y = New<B>();
+ a1->Y->V = 2;
+
+ TIntrusivePtr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->GetRefCount(), 1);
+ EXPECT_EQ(a2->X->GetRefCount(), 1);
+ EXPECT_EQ(a2->X->V, 1);
+ EXPECT_EQ(a2->Y->GetRefCount(), 1);
+ EXPECT_EQ(a2->Y->V, 2);
+}
+
+} // namespace NRef1
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef2 {
+
+struct A
+ : public TRefCounted
+{
+ TIntrusivePtr<A> X;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref2)
+{
+ auto a1 = New<A>();
+
+ TIntrusivePtr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->GetRefCount(), 1);
+ EXPECT_FALSE(a2->X.operator bool());
+}
+
+} // namespace NRef2
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef3 {
+
+struct A
+ : public TRefCounted
+{
+ TIntrusivePtr<A> X;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref3)
+{
+ auto a1 = New<A>();
+ a1->X = a1;
+
+ TIntrusivePtr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->GetRefCount(), 2);
+ EXPECT_EQ(a2->X, a2);
+
+ a1->X.Reset();
+ a2->X.Reset();
+}
+
+} // namespace NRef3
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef4 {
+
+struct A
+ : public TRefCounted
+{
+ A()
+ : X(nullptr)
+ { }
+
+ A* X;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref4)
+{
+ auto a1 = New<A>();
+ a1->X = a1.Get();
+
+ TIntrusivePtr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->GetRefCount(), 1);
+ EXPECT_EQ(a2->X, a2);
+}
+
+} // namespace NRef4
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef5 {
+
+struct A;
+struct B;
+
+struct A
+ : public TRefCounted
+{
+ A()
+ : X(nullptr)
+ , Y(nullptr)
+ { }
+
+ B* X;
+ TIntrusivePtr<B> Y;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ Persist(context, Y);
+ }
+};
+
+struct B
+ : public TRefCounted
+{
+ B()
+ : V(-1)
+ { }
+
+ int V;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, V);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref5)
+{
+ auto a1 = New<A>();
+ a1->Y = New<B>();
+ a1->Y->V = 7;
+ a1->X = a1->Y.Get();
+
+ TIntrusivePtr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->GetRefCount(), 1);
+ EXPECT_EQ(a2->Y->GetRefCount(), 1);
+ EXPECT_EQ(a2->Y->V, 7);
+ EXPECT_EQ(a2->X, a2->Y);
+}
+
+} // namespace NRef5
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef6 {
+
+struct TBase
+ : public TRefCounted
+ , public IPersistent
+{ };
+
+struct TDerived1
+ : public TBase
+{
+ TDerived1()
+ : V(-1)
+ { }
+
+ int V;
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ Persist(context, V);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(TDerived1, 0x71297841);
+
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(TDerived1);
+
+struct TDerived2
+ : public TBase
+{
+ TDerived2()
+ : V(-1)
+ { }
+
+ double V;
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ Persist(context, V);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(TDerived2, 0x62745629);
+
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(TDerived2);
+
+TEST(TPhoenixTest, Ref6)
+{
+ auto derived1 = New<TDerived1>();
+ derived1->V = 5;
+ TIntrusivePtr<TBase> base1(derived1);
+
+ TIntrusivePtr<TBase> base2;
+ Deserialize(base2, Serialize(base1));
+
+ EXPECT_EQ(base2->GetRefCount(), 1);
+ auto* derived2 = dynamic_cast<TDerived1*>(base2.Get());
+ EXPECT_NE(derived2, nullptr);
+ EXPECT_EQ(derived2->V, 5);
+}
+
+} // namespace NRef6
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef7 {
+
+struct TNonConstructable
+ : public TRefCounted
+ , public NPhoenix::TFactoryTag<NPhoenix::TNullFactory>
+{
+ explicit TNonConstructable(int x)
+ : X(x)
+ { }
+
+ int X;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(TNonConstructable, 0x14712618);
+
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(TNonConstructable);
+
+TEST(TPhoenixTest, Ref7)
+{
+ auto obj1 = New<TNonConstructable>(123);
+ EXPECT_EQ(obj1->X, 123);
+
+ auto obj2 = New<TNonConstructable>(456);
+
+ InplaceDeserialize(obj2, Serialize(obj1));
+
+ EXPECT_EQ(obj2->X, 123);
+}
+
+} // namespace NRef7
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef8 {
+
+struct A;
+struct B;
+
+struct A
+ : public NPhoenix::TFactoryTag<NPhoenix::TSimpleFactory>
+{
+ int X;
+ std::unique_ptr<B> T;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ Persist(context, T);
+ }
+
+};
+
+struct B
+ : public NPhoenix::TFactoryTag<NPhoenix::TSimpleFactory>
+{
+ int Y;
+ A* Z;
+
+ void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, Y);
+ Persist(context, Z);
+ }
+
+};
+
+TEST(TPhoenixTest, Ref8)
+{
+ std::unique_ptr<A> a1(new A());
+ a1->X = 123;
+ a1->T.reset(new B());
+ a1->T->Y = 456;
+ a1->T->Z = a1.get();
+
+ std::unique_ptr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ EXPECT_EQ(a2->X, 123);
+ EXPECT_EQ(a2->T->Y, 456);
+ EXPECT_EQ(a2->T->Z, a2.get());
+}
+
+} // namespace NRef8
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef9 {
+
+struct A;
+struct B;
+
+struct A
+ : public IPersistent
+{
+ explicit A(int x)
+ : X(x)
+ { }
+
+ int X;
+
+ virtual void Foo() = 0;
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+
+};
+
+struct B
+ : public A
+ , public NPhoenix::TFactoryTag<NPhoenix::TSimpleFactory>
+{
+ B()
+ : A(0)
+ , Y(0)
+ , Z(nullptr)
+ { }
+
+ int Y;
+ A* Z;
+
+ void Foo() override
+ { }
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ using NYT::Persist;
+ A::Persist(context);
+ Persist(context, Y);
+ Persist(context, Z);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(B, 0x54717818);
+
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(B);
+
+TEST(TPhoenixTest, Ref9)
+{
+ std::unique_ptr<B> b1(new B());
+ b1->X = 123;
+ b1->Y = 456;
+ b1->Z = b1.get();
+
+ std::unique_ptr<A> a1(b1.release());
+
+ std::unique_ptr<A> a2;
+ Deserialize(a2, Serialize(a1));
+
+ B* b2 = dynamic_cast<B*>(a2.get());
+ EXPECT_NE(b2, nullptr);
+ EXPECT_EQ(b2->X, 123);
+ EXPECT_EQ(b2->Y, 456);
+ EXPECT_EQ(b2->Z, b2);
+}
+
+} // namespace NRef9
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NRef10 {
+
+struct TBase
+ : public TRefCounted
+ , public NPhoenix::TDynamicTag
+{
+ TBase()
+ : X (0)
+ { }
+
+ int X;
+
+ virtual void Persist(const TPersistenceContext& context)
+ {
+ using NYT::Persist;
+ Persist(context, X);
+ }
+};
+
+struct TDerived
+ : public TBase
+{
+ TDerived()
+ { }
+
+ int Y;
+
+ void Persist(const TPersistenceContext& context) override
+ {
+ TBase::Persist(context);
+
+ using NYT::Persist;
+ Persist(context, Y);
+ }
+
+ DECLARE_DYNAMIC_PHOENIX_TYPE(TDerived, 0x57818795);
+
+};
+
+DEFINE_DYNAMIC_PHOENIX_TYPE(TDerived);
+
+TEST(TPhoenixTest, Ref10)
+{
+ auto obj1 = New<TDerived>();
+ obj1->X = 123;
+ obj1->Y = 456;
+
+ auto obj2 = New<TDerived>();
+
+ InplaceDeserialize(obj2, Serialize(TIntrusivePtr<TBase>(obj1)));
+
+ EXPECT_EQ(obj2->X, 123);
+ EXPECT_EQ(obj2->Y, 456);
+}
+
+} // namespace NRef10
+
+////////////////////////////////////////////////////////////////////////////////
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/pool_allocator_ut.cpp b/yt/yt/core/misc/unittests/pool_allocator_ut.cpp
new file mode 100644
index 0000000000..38a9697b08
--- /dev/null
+++ b/yt/yt/core/misc/unittests/pool_allocator_ut.cpp
@@ -0,0 +1,71 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/pool_allocator.h>
+
+namespace NYT {
+namespace {
+
+using ::testing::TProbe;
+using ::testing::TProbeState;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TPoolAllocator, Alignment16)
+{
+ struct TTag
+ { };
+ TPoolAllocator allocator(48, 16, 64_KB, GetRefCountedTypeCookie<TTag>());
+
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_EQ(0ULL, reinterpret_cast<uintptr_t>(allocator.Allocate()) % 16);
+ }
+}
+
+TEST(TPoolAllocator, Reuse)
+{
+ struct TTag
+ { };
+ TPoolAllocator allocator(256, 1, 64_KB, GetRefCountedTypeCookie<TTag>());
+
+ THashSet<void*> ptrs;
+
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_TRUE(ptrs.insert(allocator.Allocate()).second);
+ }
+
+ for (auto* ptr : ptrs) {
+ TPoolAllocator::Free(ptr);
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_TRUE(ptrs.contains(allocator.Allocate()));
+ }
+
+ EXPECT_FALSE(ptrs.contains(allocator.Allocate()));
+}
+
+TEST(TPoolAllocator, Object)
+{
+ TProbeState state;
+
+ class TObject
+ : public TPoolAllocator::TObjectBase
+ , public TProbe
+ {
+ public:
+ explicit TObject(TProbeState* state)
+ : TProbe(state)
+ { }
+ };
+
+ auto obj = TPoolAllocator::New<TObject>(&state);
+ EXPECT_EQ(1, state.Constructors);
+
+ obj.reset();
+ EXPECT_EQ(1, state.Destructors);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/proc_ut.cpp b/yt/yt/core/misc/unittests/proc_ut.cpp
new file mode 100644
index 0000000000..f18e462aef
--- /dev/null
+++ b/yt/yt/core/misc/unittests/proc_ut.cpp
@@ -0,0 +1,193 @@
+#ifdef __linux__
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TProcTest, TestParseMemoryMappings)
+{
+ const TString rawSMaps =
+ "7fbb7b24d000-7fbb7b251000 rw-s 00000000 00:00 0 \n"
+ "Size: 1 kB\n"
+ "KernelPageSize: 2 kB\n"
+ "MMUPageSize: 3 kB\n"
+ "Rss: 4 kB\n"
+ "Pss: 5 kB\n"
+ "Shared_Clean: 6 kB\n"
+ "Shared_Dirty: 7 kB\n"
+ "Private_Clean: 8 kB\n"
+ "Private_Dirty: 9 kB\n"
+ "Referenced: 10 kB\n"
+ "Anonymous: 11 kB\n"
+ "LazyFree: 12 kB\n"
+ "AnonHugePages: 13 kB\n"
+ "ShmemPmdMapped: 14 kB\n"
+ "Shared_Hugetlb: 15 kB\n"
+ "Private_Hugetlb: 16 kB\n"
+ "Swap: 17 kB\n"
+ "SwapPss: 18 kB\n"
+ "Locked: 19 kB\n"
+ "ProtectionKey: 20\n"
+ "VmFlags: rd wr mg\n"
+ "7fbb7b251000-7fbb7b278000 r-xp 000000ff 00:13d 406536 /lib/x86_64-linux-gnu/ld-2.28.so (deleted)\n"
+ "Size: 156 kB\n"
+ "KernelPageSize: 4 kB\n"
+ "MMUPageSize: 4 kB\n"
+ "ProtectionKey: 0\n"
+ "VmFlags: \n";
+
+ auto smaps = ParseMemoryMappings(rawSMaps);
+
+ EXPECT_EQ(std::ssize(smaps), 2);
+
+ EXPECT_EQ(smaps[0].Start, 0x7fbb7b24d000u);
+ EXPECT_EQ(smaps[0].End, 0x7fbb7b251000u);
+ EXPECT_EQ(smaps[0].Permissions, EMemoryMappingPermission::Read | EMemoryMappingPermission::Write | EMemoryMappingPermission::Shared);
+ EXPECT_EQ(smaps[0].Offset, 0u);
+ EXPECT_EQ(static_cast<bool>(smaps[0].DeviceId), false);
+ EXPECT_EQ(static_cast<bool>(smaps[0].INode), false);
+ EXPECT_EQ(static_cast<bool>(smaps[0].Path), false);
+ EXPECT_EQ(smaps[0].Statistics.Size, 1_KB);
+ EXPECT_EQ(smaps[0].Statistics.KernelPageSize, 2_KB);
+ EXPECT_EQ(smaps[0].Statistics.MMUPageSize, 3_KB);
+ EXPECT_EQ(smaps[0].Statistics.Rss, 4_KB);
+ EXPECT_EQ(smaps[0].Statistics.Pss, 5_KB);
+ EXPECT_EQ(smaps[0].Statistics.SharedClean, 6_KB);
+ EXPECT_EQ(smaps[0].Statistics.SharedDirty, 7_KB);
+ EXPECT_EQ(smaps[0].Statistics.PrivateClean, 8_KB);
+ EXPECT_EQ(smaps[0].Statistics.PrivateDirty, 9_KB);
+ EXPECT_EQ(smaps[0].Statistics.Referenced, 10_KB);
+ EXPECT_EQ(smaps[0].Statistics.Anonymous, 11_KB);
+ EXPECT_EQ(smaps[0].Statistics.LazyFree, 12_KB);
+ EXPECT_EQ(smaps[0].Statistics.AnonHugePages, 13_KB);
+ EXPECT_EQ(smaps[0].Statistics.ShmemPmdMapped, 14_KB);
+ EXPECT_EQ(smaps[0].Statistics.SharedHugetlb, 15_KB);
+ EXPECT_EQ(smaps[0].Statistics.PrivateHugetlb, 16_KB);
+ EXPECT_EQ(smaps[0].Statistics.Swap, 17_KB);
+ EXPECT_EQ(smaps[0].Statistics.SwapPss, 18_KB);
+ EXPECT_EQ(smaps[0].Statistics.Locked, 19_KB);
+ EXPECT_EQ(smaps[0].ProtectionKey, 20u);
+ EXPECT_EQ(smaps[0].VMFlags, EVMFlag::RD | EVMFlag::WR | EVMFlag::MG);
+
+ EXPECT_EQ(smaps[1].Start, 0x7fbb7b251000u);
+ EXPECT_EQ(smaps[1].End, 0x7fbb7b278000u);
+ EXPECT_EQ(smaps[1].Permissions, EMemoryMappingPermission::Read | EMemoryMappingPermission::Execute | EMemoryMappingPermission::Private);
+ EXPECT_EQ(smaps[1].Offset, 0xffu);
+ EXPECT_EQ(smaps[1].DeviceId, 1048637);
+ EXPECT_EQ(*smaps[1].INode, 406536u);
+ EXPECT_EQ(*smaps[1].Path, "/lib/x86_64-linux-gnu/ld-2.28.so");
+ EXPECT_EQ(smaps[1].Statistics.Size, 156_KB);
+ EXPECT_EQ(smaps[1].Statistics.KernelPageSize, 4_KB);
+ EXPECT_EQ(smaps[1].Statistics.MMUPageSize, 4_KB);
+ EXPECT_EQ(smaps[1].Statistics.Rss, 0_KB);
+ EXPECT_EQ(smaps[1].ProtectionKey, 0u);
+ EXPECT_EQ(smaps[1].VMFlags, EVMFlag::None);
+}
+
+TEST(TProcTest, TestGetSelfMemoryMappings)
+{
+ auto pid = GetCurrentProcessId();
+ auto memoryMappings = GetProcessMemoryMappings(pid);
+
+ TMemoryMappingStatistics statistics;
+ for (const auto& mapping : memoryMappings) {
+ statistics += mapping.Statistics;
+ }
+
+ auto memoryUsage = GetProcessMemoryUsage();
+
+ // Memory usage could change slightly between measurings.
+ EXPECT_LE(statistics.Rss, 1.1 * memoryUsage.Rss);
+ EXPECT_GE(statistics.Rss, 0.9 * memoryUsage.Rss);
+}
+
+TEST(TProcTest, CgroupList)
+{
+ auto cgroups = GetProcessCgroups();
+ ASSERT_FALSE(cgroups.empty());
+
+ for (const auto& group : cgroups) {
+ if (group.HierarchyId == 0) {
+ continue;
+ }
+
+ ASSERT_FALSE(group.Controllers.empty());
+ ASSERT_NE(group.Path, "");
+
+ for (const auto& controller : group.Controllers) {
+ if (controller == "cpu") {
+ GetCgroupCpuStat(group.ControllersName, group.Path);
+ }
+ }
+ }
+}
+
+TEST(TProcTest, DiskStat)
+{
+ {
+ auto parsed = ParseDiskStat("259 1 nvme0n1 372243 70861 50308550 175935 635314 559065 105338106 2777004 0 415304 3236956 38920 4 80436632 905059");
+ EXPECT_EQ(parsed.MajorNumber, 259);
+ EXPECT_EQ(parsed.MinorNumber, 1);
+ EXPECT_EQ(parsed.DeviceName, "nvme0n1");
+
+ EXPECT_EQ(parsed.ReadsCompleted, 372243);
+ EXPECT_EQ(parsed.ReadsMerged, 70861);
+ EXPECT_EQ(parsed.SectorsRead, 50308550);
+ EXPECT_EQ(parsed.TimeSpentReading, TDuration::MilliSeconds(175935));
+
+ EXPECT_EQ(parsed.WritesCompleted, 635314);
+
+ EXPECT_EQ(parsed.DiscardsCompleted, 38920);
+ EXPECT_EQ(parsed.DiscardsMerged, 4);
+ EXPECT_EQ(parsed.SectorsDiscarded, 80436632);
+ EXPECT_EQ(parsed.TimeSpentDiscarding, TDuration::MilliSeconds(905059));
+ }
+ {
+ auto parsed = ParseDiskStat("259 1 nvme0n1 372243 trash 50308550 trash");
+ EXPECT_EQ(parsed.MajorNumber, 259);
+ EXPECT_EQ(parsed.MinorNumber, 1);
+ EXPECT_EQ(parsed.DeviceName, "nvme0n1");
+
+ EXPECT_EQ(parsed.ReadsCompleted, 372243);
+ EXPECT_EQ(parsed.ReadsMerged, 0);
+ EXPECT_EQ(parsed.SectorsRead, 50308550);
+ EXPECT_EQ(parsed.TimeSpentReading, TDuration::MilliSeconds(0));
+ }
+ {
+ auto stats = GetDiskStats();
+ for (const TString& disk : ListDisks()) {
+ EXPECT_TRUE(IsIn(stats, disk));
+ }
+ }
+}
+
+TEST(TProcTest, FileDescriptorCount)
+{
+ auto initialCount = GetFileDescriptorCount();
+ EXPECT_GE(initialCount, 3);
+
+ int newDescriptorCount = 139;
+ std::vector<TFile> files;
+ for (int i = 0; i < newDescriptorCount; ++i) {
+ files.emplace_back("/dev/null", EOpenModeFlag::WrOnly);
+ }
+ EXPECT_EQ(GetFileDescriptorCount(), initialCount + newDescriptorCount);
+
+ files.clear();
+ EXPECT_EQ(GetFileDescriptorCount(), initialCount);
+}
+
+TEST(TProcTest, SelfIO)
+{
+ GetSelfThreadTaskDiskStatistics();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+#endif
diff --git a/yt/yt/core/misc/unittests/proto/ref_counted_tracker_ut.proto b/yt/yt/core/misc/unittests/proto/ref_counted_tracker_ut.proto
new file mode 100644
index 0000000000..ec601dcc7d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/proto/ref_counted_tracker_ut.proto
@@ -0,0 +1,14 @@
+package NYT.NProto;
+
+message TPlainMessage
+{
+ required int32 a = 1;
+}
+
+message TRefCountedMessage
+{
+ required string a = 1;
+ optional int64 b = 2;
+ required TPlainMessage c = 3;
+ repeated TPlainMessage d = 4;
+} \ No newline at end of file
diff --git a/yt/yt/core/misc/unittests/random_ut.cpp b/yt/yt/core/misc/unittests/random_ut.cpp
new file mode 100644
index 0000000000..924b061b74
--- /dev/null
+++ b/yt/yt/core/misc/unittests/random_ut.cpp
@@ -0,0 +1,37 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/random.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TRandomGeneratorTest, DifferentTypes)
+{
+ TRandomGenerator rg1(100500);
+ TRandomGenerator rg2(100500);
+
+ EXPECT_EQ(rg1.Generate<ui64>(), rg2.Generate<ui64>());
+ EXPECT_EQ(rg1.Generate<i64>(), rg2.Generate<i64>());
+ EXPECT_EQ(rg1.Generate<ui32>(), rg2.Generate<ui32>());
+ EXPECT_EQ(rg1.Generate<i32>(), rg2.Generate<i32>());
+ EXPECT_EQ(rg1.Generate<char>(), rg2.Generate<char>());
+
+ EXPECT_EQ(rg1.Generate<double>(), rg2.Generate<double>());
+}
+
+TEST(TRandomGeneratorTest, Many)
+{
+ TRandomGenerator rg1(100500);
+ TRandomGenerator rg2(100500);
+
+ for (int i = 0; i < 1000; ++i) {
+ EXPECT_EQ(rg1.Generate<ui64>(), rg2.Generate<ui64>());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/ref_counted_tracker_ut.cpp b/yt/yt/core/misc/unittests/ref_counted_tracker_ut.cpp
new file mode 100644
index 0000000000..538f03989f
--- /dev/null
+++ b/yt/yt/core/misc/unittests/ref_counted_tracker_ut.cpp
@@ -0,0 +1,300 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/unittests/proto/ref_counted_tracker_ut.pb.h>
+
+#define YT_ENABLE_REF_COUNTED_TRACKING
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/ref_counted.h>
+#include <yt/yt/core/misc/ref_counted_tracker.h>
+#include <yt/yt/core/misc/ref_tracked.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <library/cpp/yt/memory/new.h>
+
+namespace NYT {
+namespace {
+
+using namespace NYT::NProto;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+size_t GetAliveCount()
+{
+ return TRefCountedTracker::Get()->GetObjectsAlive(GetRefCountedTypeKey<T>());
+}
+
+template <class T>
+size_t GetAliveBytes()
+{
+ return TRefCountedTracker::Get()->GetBytesAlive(GetRefCountedTypeKey<T>());
+}
+
+template <class T>
+size_t GetAllocatedCount()
+{
+ return TRefCountedTracker::Get()->GetObjectsAllocated(GetRefCountedTypeKey<T>());
+}
+
+template <class T>
+size_t GetAllocatedBytes()
+{
+ return TRefCountedTracker::Get()->GetBytesAllocated(GetRefCountedTypeKey<T>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TRefCountedTraits;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleRefCountedObject
+ : public TRefCounted
+{ };
+
+template <>
+class TRefCountedTraits<TSimpleRefCountedObject>
+{
+public:
+ static TIntrusivePtr<TSimpleRefCountedObject> Create()
+ {
+ return New<TSimpleRefCountedObject>();
+ }
+
+ static size_t GetInstanceSize()
+ {
+ return sizeof(TSimpleRefCountedObject);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TProtoRefCountedObject = TRefCountedProto<TRefCountedMessage>;
+
+template <>
+class TRefCountedTraits<TProtoRefCountedObject>
+{
+public:
+ static TIntrusivePtr<TProtoRefCountedObject> Create()
+ {
+ static auto message = CreateMessage();
+ return New<TProtoRefCountedObject>(message);
+ }
+
+ static size_t GetInstanceSize()
+ {
+ static auto message = CreateMessage();
+ return message.SpaceUsed() - sizeof(TRefCountedMessage) + sizeof(TProtoRefCountedObject);
+ }
+
+private:
+ static TRefCountedMessage CreateMessage()
+ {
+ TRefCountedMessage message;
+ message.set_a("string");
+ message.mutable_c()->set_a(10);
+ message.add_d()->set_a(1);
+ message.add_d()->set_a(2);
+ message.add_d()->set_a(3);
+
+ return message;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+class TRefCountedTrackerTest
+ : public ::testing::Test
+{ };
+
+using TypeList = ::testing::Types<TSimpleRefCountedObject, TProtoRefCountedObject>;
+TYPED_TEST_SUITE(TRefCountedTrackerTest, TypeList);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPED_TEST(TRefCountedTrackerTest, SinglethreadedRefCounted)
+{
+ const auto instanceSize = TRefCountedTraits<TypeParam>::GetInstanceSize();
+ auto create = [] () {
+ return TRefCountedTraits<TypeParam>::Create();
+ };
+
+ auto countBase = GetAllocatedCount<TypeParam>();
+ auto bytesBase = GetAllocatedBytes<TypeParam>();
+
+ std::vector<TIntrusivePtr<TypeParam>> container;
+ container.reserve(2000);
+
+ EXPECT_EQ(0u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(0u, GetAliveBytes<TypeParam>());
+ EXPECT_EQ(countBase, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(bytesBase, GetAllocatedBytes<TypeParam>());
+
+ for (size_t i = 0; i < 1000; ++i) {
+ container.push_back(create());
+ }
+
+ EXPECT_EQ(1000u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(1000u * instanceSize, GetAliveBytes<TypeParam>());
+ EXPECT_EQ(countBase + 1000u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(bytesBase + 1000u * instanceSize, GetAllocatedBytes<TypeParam>());
+
+ for (size_t i = 0; i < 1000; ++i) {
+ container.push_back(create());
+ }
+
+ EXPECT_EQ(2000u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(countBase + 2000u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(2000u * instanceSize, GetAliveBytes<TypeParam>());
+ EXPECT_EQ(bytesBase + 2000u * instanceSize, GetAllocatedBytes<TypeParam>());
+
+ container.resize(1000);
+
+ EXPECT_EQ(1000u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(countBase + 2000u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(1000u * instanceSize, GetAliveBytes<TypeParam>());
+ EXPECT_EQ(bytesBase + 2000u * instanceSize, GetAllocatedBytes<TypeParam>());
+
+ container.resize(0);
+
+ EXPECT_EQ(0u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(countBase + 2000u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(0u, GetAliveBytes<TypeParam>());
+ EXPECT_EQ(bytesBase + 2000u * instanceSize, GetAllocatedBytes<TypeParam>());
+}
+
+TYPED_TEST(TRefCountedTrackerTest, MultithreadedRefCounted)
+{
+ const auto instanceSize = TRefCountedTraits<TypeParam>::GetInstanceSize();
+ auto create = [] () {
+ return TRefCountedTraits<TypeParam>::Create();
+ };
+
+ auto countBase = GetAllocatedCount<TypeParam>();
+ auto bytesBase = GetAllocatedBytes<TypeParam>();
+
+ auto obj1 = create();
+
+ auto queue = New<TActionQueue>();
+ BIND([&] () {
+ auto obj2 = create();
+ EXPECT_EQ(countBase + 2u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(2u, GetAliveCount<TypeParam>());
+ })
+ .AsyncVia(queue->GetInvoker())
+ .Run()
+ .Get();
+ queue->Shutdown();
+
+ EXPECT_EQ(countBase + 2u, GetAllocatedCount<TypeParam>());
+ EXPECT_EQ(1u, GetAliveCount<TypeParam>());
+ EXPECT_EQ(bytesBase + 2u * instanceSize, GetAllocatedBytes<TypeParam>());
+ EXPECT_EQ(instanceSize, GetAliveBytes<TypeParam>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBlobTag
+{ };
+
+TEST(TRefCountedTrackerTest, TBlobAllocatedMemoryTracker)
+{
+ auto allocatedBytesBase = GetAllocatedBytes<TBlobTag>();
+ auto allocatedObjectsBase = GetAllocatedCount<TBlobTag>();
+
+ EXPECT_EQ(0u, GetAliveBytes<TBlobTag>());
+ EXPECT_EQ(0u, GetAliveCount<TBlobTag>());
+ EXPECT_EQ(allocatedBytesBase, GetAllocatedBytes<TBlobTag>());
+ EXPECT_EQ(allocatedObjectsBase, GetAllocatedCount<TBlobTag>());
+
+ auto blob = TBlob(GetRefCountedTypeCookie<TBlobTag>(), 1);
+ auto blobCapacity1 = blob.Capacity();
+
+ EXPECT_EQ(blobCapacity1, GetAliveBytes<TBlobTag>());
+ EXPECT_EQ(1u, GetAliveCount<TBlobTag>());
+ EXPECT_EQ(allocatedBytesBase + blobCapacity1, GetAllocatedBytes<TBlobTag>());
+ EXPECT_EQ(allocatedObjectsBase + 1, GetAllocatedCount<TBlobTag>());
+
+ blob.Resize(3000);
+ auto blobCapacity2 = blob.Capacity();
+
+ EXPECT_EQ(blobCapacity2, GetAliveBytes<TBlobTag>());
+ EXPECT_EQ(1u, GetAliveCount<TBlobTag>());
+ EXPECT_EQ(allocatedBytesBase + blobCapacity1 + blobCapacity2, GetAllocatedBytes<TBlobTag>());
+ EXPECT_EQ(allocatedObjectsBase + 1, GetAllocatedCount<TBlobTag>());
+
+ blob = TBlob(GetRefCountedTypeCookie<TBlobTag>());
+
+ EXPECT_EQ(0u, GetAliveBytes<TBlobTag>());
+ EXPECT_EQ(0u, GetAliveCount<TBlobTag>());
+ EXPECT_EQ(allocatedBytesBase + blobCapacity1 + blobCapacity2, GetAllocatedBytes<TBlobTag>());
+ EXPECT_EQ(allocatedObjectsBase + 1, GetAllocatedCount<TBlobTag>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TThrowingConstructorObject)
+TThrowingConstructorObjectPtr GlobalObject;
+
+class TThrowingConstructorObject
+ : public TRefCounted
+{
+public:
+ explicit TThrowingConstructorObject(bool passThisToSomebodyElse)
+ {
+ if (passThisToSomebodyElse) {
+ GlobalObject = this;
+ }
+ THROW_ERROR_EXCEPTION("Some error");
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TThrowingConstructorObject)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TRefCountedTrackerTest, ThrowingExceptionsInConstructor)
+{
+ TThrowingConstructorObjectPtr object;
+ EXPECT_THROW(object = New<TThrowingConstructorObject>(false), std::exception);
+ // TODO(max42): enable this when death tests are allowed in unittests.
+ // ASSERT_DEATH(object = New<TThrowingConstructorObject>(true), "YT_VERIFY\\(GetRefCount\\(\\) == 1\\).*");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleRefTrackedObject
+ : public TRefTracked<TSimpleRefTrackedObject>
+{ };
+
+TEST(TRefCountedTrackerTest, RefTracked)
+{
+ auto countBase = GetAllocatedCount<TSimpleRefTrackedObject>();
+ auto bytesBase = GetAllocatedBytes<TSimpleRefTrackedObject>();
+
+ {
+ TSimpleRefTrackedObject obj;
+ EXPECT_EQ(countBase + 1, GetAllocatedCount<TSimpleRefTrackedObject>());
+ EXPECT_EQ(bytesBase + sizeof(TSimpleRefTrackedObject), GetAllocatedBytes<TSimpleRefTrackedObject>());
+ EXPECT_EQ(1u, GetAliveCount<TSimpleRefTrackedObject>());
+ EXPECT_EQ(sizeof(TSimpleRefTrackedObject), GetAliveBytes<TSimpleRefTrackedObject>());
+ }
+
+ EXPECT_EQ(countBase + 1, GetAllocatedCount<TSimpleRefTrackedObject>());
+ EXPECT_EQ(bytesBase + sizeof(TSimpleRefTrackedObject), GetAllocatedBytes<TSimpleRefTrackedObject>());
+ EXPECT_EQ(0u, GetAliveCount<TSimpleRefTrackedObject>());
+ EXPECT_EQ(0u, GetAliveBytes<TSimpleRefTrackedObject>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/relaxed_mpsc_queue_ut.cpp b/yt/yt/core/misc/unittests/relaxed_mpsc_queue_ut.cpp
new file mode 100644
index 0000000000..b3f7d72b0d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/relaxed_mpsc_queue_ut.cpp
@@ -0,0 +1,92 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/relaxed_mpsc_queue.h>
+
+#include <thread>
+#include <array>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TIntNode
+{
+ int Value;
+ TRelaxedMpscQueueHook Hook;
+
+ explicit TIntNode(int value)
+ : Value(value)
+ { }
+};
+
+TEST(TRelaxedMpscQueueTest, SimpleSingleThreaded)
+{
+ TRelaxedIntrusiveMpscQueue<TIntNode, &TIntNode::Hook> queue;
+
+ queue.Enqueue(std::make_unique<TIntNode>(1));
+ queue.Enqueue(std::make_unique<TIntNode>(2));
+ queue.Enqueue(std::make_unique<TIntNode>(3));
+
+ auto n1 = queue.TryDequeue();
+ EXPECT_EQ(1, n1->Value);
+ auto n2 = queue.TryDequeue();
+ EXPECT_EQ(2, n2->Value);
+ auto n3 = queue.TryDequeue();
+ EXPECT_EQ(3, n3->Value);
+
+ EXPECT_FALSE(static_cast<bool>(queue.TryDequeue()));
+};
+
+TEST(TRelaxedMpscQueueTest, SimpleMultiThreaded)
+{
+ TRelaxedIntrusiveMpscQueue<TIntNode, &TIntNode::Hook> queue;
+
+ constexpr int N = 10000;
+ constexpr int T = 4;
+
+ auto barrier = NewPromise<void>();
+
+ auto producer = [&] {
+ barrier.ToFuture().Get();
+ for (int i = 0; i < N; ++i) {
+ queue.Enqueue(std::make_unique<TIntNode>(i));
+ }
+ };
+
+ auto consumer = [&] {
+ std::array<int, N> counts{};
+ barrier.ToFuture().Get();
+ for (int i = 0; i < N * T; ++i) {
+ while (true) {
+ if (auto item = queue.TryDequeue()) {
+ counts[item->Value]++;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(counts[i], T);
+ }
+ };
+
+ std::vector<std::thread> threads;
+
+ for (int i = 0; i < T; ++i) {
+ threads.emplace_back(producer);
+ }
+ threads.emplace_back(consumer);
+
+ barrier.Set();
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/ring_queue_ut.cpp b/yt/yt/core/misc/unittests/ring_queue_ut.cpp
new file mode 100644
index 0000000000..5adc6c70d4
--- /dev/null
+++ b/yt/yt/core/misc/unittests/ring_queue_ut.cpp
@@ -0,0 +1,127 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/common.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TRingQueueTest, PodRandomOperations)
+{
+ TRingQueue<int> queue;
+ EXPECT_EQ(0, std::ssize(queue));
+ EXPECT_TRUE(queue.empty());
+ EXPECT_EQ(queue.begin(), queue.end());
+
+ const int N = 100000;
+ // We perform same sequence of operations on this deque.
+ std::deque<int> deque;
+
+ for (int i = 0; i < N; ++i) {
+ EXPECT_EQ(queue.size(), deque.size());
+ EXPECT_EQ(queue.empty(), deque.empty());
+ if (!queue.empty()) {
+ EXPECT_EQ(queue.back(), deque.back());
+ EXPECT_EQ(queue.front(), deque.front());
+ }
+
+ int r = rand() % 100;
+ if (r == 0) {
+ queue.clear();
+ deque.clear();
+ EXPECT_EQ(0, std::ssize(queue));
+ EXPECT_TRUE(queue.empty());
+ EXPECT_EQ(queue.begin(), queue.end());
+ } else if (r < 10) {
+ auto queueIterator = queue.begin();
+ auto dequeIterator = deque.begin();
+ while (dequeIterator != deque.end()) {
+ EXPECT_NE(queueIterator, queue.end());
+ EXPECT_EQ(*queueIterator, *dequeIterator);
+ queue.move_forward(queueIterator);
+ ++dequeIterator;
+ }
+ EXPECT_EQ(queueIterator, queue.end());
+ } else if (r < 55 && !queue.empty()) {
+ queue.pop();
+ deque.pop_front();
+ } else {
+ int value = rand();
+ queue.push(value);
+ deque.push_back(value);
+ }
+ }
+}
+
+class TFoo
+ : public virtual TRefCounted
+{
+public:
+ explicit TFoo(int& counter)
+ : Counter_(counter)
+ {
+ ++Counter_;
+ }
+
+ ~TFoo()
+ {
+ --Counter_;
+ }
+private:
+ int& Counter_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TFoo)
+DECLARE_REFCOUNTED_TYPE(TFoo)
+
+TEST(TRingQueueTest, TestLifetimeWithRefCount)
+{
+ const int N = 100000;
+ int counter = 0;
+
+ {
+ TRingQueue<TFooPtr> queue;
+
+ for (int i = 0; i < N; ++i) {
+ queue.push(New<TFoo>(counter));
+ EXPECT_EQ(counter, std::ssize(queue));
+ }
+ queue.clear();
+ EXPECT_EQ(counter, 0);
+ }
+
+ {
+ TRingQueue<TFooPtr> queue;
+
+ for (int i = 0; i < N; ++i) {
+ queue.push(New<TFoo>(counter));
+ EXPECT_EQ(counter, std::ssize(queue));
+ queue.pop();
+ EXPECT_EQ(counter, std::ssize(queue));
+ }
+ queue.clear();
+ EXPECT_EQ(counter, 0);
+ }
+
+ {
+ TRingQueue<TFooPtr> queue;
+
+ for (int i = 0; i < N; ++i) {
+ if (!queue.empty() && rand() % 2 == 0) {
+ queue.pop();
+ } else {
+ queue.push(New<TFoo>(counter));
+ }
+ EXPECT_EQ(counter, std::ssize(queue));
+ }
+ queue.clear();
+ EXPECT_EQ(counter, 0);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/skip_list_ut.cpp b/yt/yt/core/misc/unittests/skip_list_ut.cpp
new file mode 100644
index 0000000000..faa4dc15bd
--- /dev/null
+++ b/yt/yt/core/misc/unittests/skip_list_ut.cpp
@@ -0,0 +1,147 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/skip_list.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TComparer
+{
+ int operator() (int lhs, int rhs) const
+ {
+ if (lhs < rhs) {
+ return -1;
+ }
+ if (lhs > rhs) {
+ return +1;
+ }
+ return 0;
+ }
+};
+
+class TSkipListTest
+ : public ::testing::Test
+{
+public:
+ TChunkedMemoryPool Pool;
+ TSkipList<int, TComparer> List;
+
+ TSkipListTest()
+ : List(&Pool, TComparer())
+ { }
+
+};
+
+TEST_F(TSkipListTest, Empty)
+{
+ EXPECT_EQ(List.GetSize(), 0);
+
+ EXPECT_FALSE(List.FindEqualTo(1).IsValid());
+
+ EXPECT_FALSE(List.FindGreaterThanOrEqualTo(1).IsValid());
+}
+
+TEST_F(TSkipListTest, Singleton)
+{
+ EXPECT_TRUE(List.Insert(0));
+ EXPECT_EQ(List.GetSize(), 1);
+
+ EXPECT_FALSE(List.FindEqualTo(1).IsValid());
+
+ EXPECT_FALSE(List.FindGreaterThanOrEqualTo(1).IsValid());
+
+ {
+ auto it = List.FindGreaterThanOrEqualTo(-1);
+ EXPECT_TRUE(it.IsValid());
+ EXPECT_EQ(it.GetCurrent(), 0);
+ it.MoveNext();
+ EXPECT_FALSE(it.IsValid());
+ }
+
+ {
+ auto it = List.FindGreaterThanOrEqualTo(0);
+ EXPECT_TRUE(it.IsValid());
+ EXPECT_EQ(it.GetCurrent(), 0);
+ it.MoveNext();
+ EXPECT_FALSE(it.IsValid());
+ }
+}
+
+TEST_F(TSkipListTest, 1to10)
+{
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_TRUE(List.Insert(i));
+ }
+ EXPECT_EQ(List.GetSize(), 10);
+
+ for (int i = 0; i < 10; ++i) {
+ auto it = List.FindGreaterThanOrEqualTo(i);
+ for (int j = i; j < 10; ++j) {
+ EXPECT_TRUE(it.IsValid());
+ EXPECT_EQ(it.GetCurrent(), j);
+ it.MoveNext();
+ }
+ EXPECT_FALSE(it.IsValid());
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_TRUE(List.FindEqualTo(i).IsValid());
+ }
+
+ EXPECT_FALSE(List.FindEqualTo(-1).IsValid());
+ EXPECT_FALSE(List.FindEqualTo(11).IsValid());
+}
+
+TEST_F(TSkipListTest, 20to0skip2)
+{
+ for (int i = 20; i > 0; i -=2) {
+ EXPECT_TRUE(List.Insert(i));
+ }
+ EXPECT_EQ(List.GetSize(), 10);
+
+ for (int i = 3; i < 21; i+=2) {
+ auto it = List.FindLessThanOrEqualTo(i);
+ for (int j = i-1; j > 0; j -= 2) {
+ EXPECT_TRUE(it.IsValid());
+ EXPECT_EQ(it.GetCurrent(), j);
+ it.MovePrev();
+ }
+ EXPECT_FALSE(it.IsValid());
+ }
+
+ for (int i = 2; i < 21; i += 2) {
+ EXPECT_TRUE(List.FindEqualTo(i).IsValid());
+ }
+
+ EXPECT_FALSE(List.FindEqualTo(-1).IsValid());
+ EXPECT_FALSE(List.FindEqualTo(21).IsValid());
+}
+
+TEST_F(TSkipListTest, Random100000)
+{
+ srand(42);
+ std::set<int> set;
+ for (int i = 0; i < 100000; ++i) {
+ int value = rand() % 1000;
+ EXPECT_EQ(List.Insert(value), set.insert(value).second);
+ }
+ EXPECT_EQ(List.GetSize(), std::ssize(set));
+
+ for (int value : set) {
+ EXPECT_TRUE(List.FindEqualTo(value).IsValid());
+ }
+
+ auto it = List.FindGreaterThanOrEqualTo(*set.begin());
+ for (int value : set) {
+ EXPECT_TRUE(it.IsValid());
+ EXPECT_EQ(it.GetCurrent(), value);
+ it.MoveNext();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/slab_allocator_ut.cpp b/yt/yt/core/misc/unittests/slab_allocator_ut.cpp
new file mode 100644
index 0000000000..6dd1c5b782
--- /dev/null
+++ b/yt/yt/core/misc/unittests/slab_allocator_ut.cpp
@@ -0,0 +1,63 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/slab_allocator.h>
+
+#include <library/cpp/yt/malloc/malloc.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSlabAllocatorTest, Reallocate)
+{
+ constexpr int objSize = 8;
+
+ TSlabAllocator allocator;
+ EXPECT_FALSE(allocator.IsReallocationNeeded());
+
+ void* ptr = allocator.Allocate(objSize);
+ EXPECT_FALSE(allocator.IsReallocationNeeded());
+
+ TSlabAllocator::Free(ptr);
+ EXPECT_TRUE(allocator.IsReallocationNeeded());
+ ptr = allocator.Allocate(objSize);
+
+ void* ptr0 = allocator.Allocate(objSize);
+ EXPECT_FALSE(allocator.IsReallocationNeeded());
+
+ int objectsInSegment = TSlabAllocator::SegmentSize / nallocx(objSize + sizeof(void*), 0);
+
+ std::vector<void*> ptrs;
+ // Fill segment.
+ for (int i = 0; i < objectsInSegment - 2; ++i) {
+ ptrs.push_back(allocator.Allocate(objSize));
+ }
+
+ EXPECT_FALSE(allocator.IsReallocationNeeded());
+ void* ptr1 = allocator.Allocate(objSize);
+
+ TSlabAllocator::Free(ptr);
+ TSlabAllocator::Free(ptr0);
+ TSlabAllocator::Free(ptr1);
+
+ EXPECT_TRUE(allocator.IsReallocationNeeded());
+
+ allocator.ReallocateArenasIfNeeded();
+
+ for (auto& ptr : ptrs) {
+ TSlabAllocator::Free(ptr);
+ ptr = allocator.Allocate(objSize);
+ }
+
+ EXPECT_FALSE(allocator.IsReallocationNeeded());
+
+ for (auto* ptr : ptrs) {
+ TSlabAllocator::Free(ptr);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/sliding_window_ut.cpp b/yt/yt/core/misc/unittests/sliding_window_ut.cpp
new file mode 100644
index 0000000000..61641bf797
--- /dev/null
+++ b/yt/yt/core/misc/unittests/sliding_window_ut.cpp
@@ -0,0 +1,120 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/sliding_window.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <size_t Size>
+class TSlidingWindowTest
+ : public ::testing::Test
+{
+public:
+ void SetUp() override
+ {
+ Window_ = std::make_unique<TSlidingWindow<size_t>>(Size);
+ }
+
+ void PushElement(size_t value)
+ {
+ Window_->AddPacket(value, size_t(value), [&] (size_t&& value) {
+ ReceivedElements_.push_back(value);
+ });
+ }
+
+ bool CheckElements(size_t minValue, size_t expectedSize)
+ {
+ if (ReceivedElements_.size() != expectedSize ||
+ Window_->GetNextSequenceNumber() != static_cast<ssize_t>(expectedSize))
+ {
+ return false;
+ }
+
+ for (size_t value = minValue; value < expectedSize; ++value) {
+ if (ReceivedElements_[value] != value) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+protected:
+ std::unique_ptr<TSlidingWindow<size_t>> Window_;
+ std::vector<size_t> ReceivedElements_;
+};
+
+using TSingleSlidingWindowTest = TSlidingWindowTest<1>;
+
+TEST_F(TSingleSlidingWindowTest, SingleTest)
+{
+ EXPECT_TRUE(CheckElements(0, 0));
+ EXPECT_TRUE(Window_->IsEmpty());
+
+ PushElement(0);
+ PushElement(1);
+ EXPECT_ANY_THROW(PushElement(1));
+ PushElement(2);
+ EXPECT_TRUE(CheckElements(0, 3));
+ EXPECT_TRUE(Window_->IsEmpty());
+}
+
+constexpr ssize_t WindowSize = 1024;
+using TFiniteSlidingWindowTest = TSlidingWindowTest<WindowSize>;
+
+TEST_F(TFiniteSlidingWindowTest, FiniteTest)
+{
+ EXPECT_TRUE(CheckElements(0, 0));
+ EXPECT_TRUE(Window_->IsEmpty());
+
+ PushElement(0);
+ for (auto element = WindowSize; element > 1; --element) {
+ PushElement(element);
+ }
+ EXPECT_TRUE(CheckElements(0, 1));
+ EXPECT_FALSE(Window_->IsEmpty());
+ EXPECT_ANY_THROW(PushElement(2));
+ EXPECT_ANY_THROW(PushElement(WindowSize));
+
+ PushElement(1);
+ EXPECT_TRUE(CheckElements(1, WindowSize + 1));
+ EXPECT_TRUE(Window_->IsEmpty());
+
+ PushElement(2 * WindowSize);
+ EXPECT_TRUE(CheckElements(WindowSize + 1, WindowSize + 1));
+ EXPECT_FALSE(Window_->IsEmpty());
+}
+
+using TInfiniteSlidingWindowTest = TSlidingWindowTest<std::numeric_limits<ssize_t>::max()>;
+
+TEST_F(TInfiniteSlidingWindowTest, TInfiniteTest)
+{
+ EXPECT_TRUE(CheckElements(0, 0));
+ EXPECT_TRUE(Window_->IsEmpty());
+
+ PushElement(WindowSize);
+ PushElement(0);
+ for (auto element = WindowSize - 1; element > 1; --element) {
+ PushElement(element);
+ }
+ EXPECT_TRUE(CheckElements(0, 1));
+ EXPECT_FALSE(Window_->IsEmpty());
+ EXPECT_ANY_THROW(PushElement(2));
+ EXPECT_ANY_THROW(PushElement(WindowSize));
+
+ PushElement(1);
+ EXPECT_TRUE(CheckElements(1, WindowSize + 1));
+ EXPECT_TRUE(Window_->IsEmpty());
+
+ PushElement(std::numeric_limits<ssize_t>::max());
+ EXPECT_ANY_THROW(PushElement(std::numeric_limits<ssize_t>::max()));
+ EXPECT_TRUE(CheckElements(WindowSize + 1, WindowSize + 1));
+ EXPECT_FALSE(Window_->IsEmpty());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/spsc_queue_ut.cpp b/yt/yt/core/misc/unittests/spsc_queue_ut.cpp
new file mode 100644
index 0000000000..b514753639
--- /dev/null
+++ b/yt/yt/core/misc/unittests/spsc_queue_ut.cpp
@@ -0,0 +1,104 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/spsc_queue.h>
+
+#include <util/system/thread.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSpscQueueTest
+ : public ::testing::Test
+{
+protected:
+ TSpscQueue<int> A;
+ TSpscQueue<int> B;
+
+ std::atomic<bool> Stopped = {false};
+
+ TSpscQueueTest()
+ { }
+
+ void AtoB()
+ {
+ while (auto item = A.Front()) {
+ if (Stopped.load()) {
+ break;
+ }
+ B.Push(std::move(*item));
+ A.Pop();
+ }
+ }
+
+ void BtoA()
+ {
+ while (auto item = B.Front()) {
+ if (Stopped.load()) {
+ break;
+ }
+ A.Push(std::move(*item));
+ B.Pop();
+ }
+ }
+
+};
+
+TEST_F(TSpscQueueTest, TwoThreads)
+{
+ constexpr int N = 1000;
+
+ using TThis = typename std::remove_reference<decltype(*this)>::type;
+ TThread thread1([] (void* opaque) -> void* {
+ auto this_ = static_cast<TThis*>(opaque);
+ this_->AtoB();
+ return nullptr;
+ }, this);
+ TThread thread2([] (void* opaque) -> void* {
+ auto this_ = static_cast<TThis*>(opaque);
+ this_->BtoA();
+ return nullptr;
+ }, this);
+
+ for (size_t i = 1; i <= N; ++i) {
+ if (i & 1) {
+ A.Push(i);
+ } else {
+ B.Push(i);
+ }
+ }
+
+ thread1.Start();
+ thread2.Start();
+
+ Sleep(TDuration::Seconds(2));
+
+ Stopped.store(true);
+
+ thread1.Join();
+ thread2.Join();
+
+ std::vector<int> elements;
+ while (auto item = A.Front()) {
+ elements.push_back(*item);
+ A.Pop();
+ }
+ while (auto item = B.Front()) {
+ elements.push_back(*item);
+ B.Pop();
+ }
+
+ std::sort(elements.begin(), elements.end());
+
+ EXPECT_EQ(std::ssize(elements), N);
+ for (ssize_t i = 1; i <= N; ++i) {
+ EXPECT_EQ(elements[i - 1], i);
+ }
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/statistics_ut.cpp b/yt/yt/core/misc/unittests/statistics_ut.cpp
new file mode 100644
index 0000000000..1e59f78585
--- /dev/null
+++ b/yt/yt/core/misc/unittests/statistics_ut.cpp
@@ -0,0 +1,184 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/statistics.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT {
+
+using namespace NYTree;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TStatistics, Summary)
+{
+ TSummary summary;
+
+ summary.AddSample(10);
+ summary.AddSample(20);
+
+ EXPECT_EQ(10, summary.GetMin());
+ EXPECT_EQ(20, summary.GetMax());
+ EXPECT_EQ(2, summary.GetCount());
+ EXPECT_EQ(30, summary.GetSum());
+ EXPECT_EQ(20, summary.GetLast());
+
+ summary.AddSample(15);
+ EXPECT_EQ(10, summary.GetMin());
+ EXPECT_EQ(20, summary.GetMax());
+ EXPECT_EQ(3, summary.GetCount());
+ EXPECT_EQ(45, summary.GetSum());
+ EXPECT_EQ(15, summary.GetLast());
+
+ summary.Merge(summary);
+ EXPECT_EQ(10, summary.GetMin());
+ EXPECT_EQ(20, summary.GetMax());
+ EXPECT_EQ(6, summary.GetCount());
+ EXPECT_EQ(90, summary.GetSum());
+ EXPECT_EQ(std::nullopt, summary.GetLast());
+
+ summary.Reset();
+ EXPECT_EQ(0, summary.GetCount());
+ EXPECT_EQ(0, summary.GetSum());
+ EXPECT_EQ(std::nullopt, summary.GetLast());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStatistics CreateStatistics(std::initializer_list<std::pair<NYPath::TYPath, i64>> data)
+{
+ TStatistics result;
+ for (const auto& item : data) {
+ result.AddSample(item.first, item.second);
+ }
+ return result;
+}
+
+TEST(TStatistics, AddSample)
+{
+ std::map<TString, int> origin = {{"x", 5}, {"y", 7}};
+
+ TStatistics statistics;
+ statistics.AddSample(
+ "/key/subkey",
+ origin);
+
+ EXPECT_EQ(5, GetNumericValue(statistics, "/key/subkey/x"));
+ EXPECT_EQ(7, GetNumericValue(statistics, "/key/subkey/y"));
+
+ statistics.AddSample("/key/sub", 42);
+ EXPECT_EQ(42, GetNumericValue(statistics, "/key/sub"));
+
+ // Cannot add sample to the map node.
+ EXPECT_THROW(statistics.AddSample("/key/subkey", 24), std::exception);
+
+ statistics.Merge(CreateStatistics({
+ {"/key/subkey/x", 5},
+ {"/key/subkey/z", 9}}));
+
+ EXPECT_EQ(10, GetNumericValue(statistics, "/key/subkey/x"));
+ EXPECT_EQ(7, GetNumericValue(statistics, "/key/subkey/y"));
+ EXPECT_EQ(9, GetNumericValue(statistics, "/key/subkey/z"));
+
+ EXPECT_THROW(
+ statistics.Merge(CreateStatistics({{"/key", 5}})),
+ std::exception);
+
+ statistics.AddSample("/key/subkey/x", 10);
+ EXPECT_EQ(20, GetNumericValue(statistics, "/key/subkey/x"));
+
+ auto ysonStatistics = ConvertToYsonString(statistics);
+ auto deserializedStatistics = ConvertTo<TStatistics>(ysonStatistics);
+
+ EXPECT_EQ(20, GetNumericValue(deserializedStatistics, "/key/subkey/x"));
+ EXPECT_EQ(42, GetNumericValue(deserializedStatistics, "/key/sub"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatisticsUpdater
+{
+public:
+ void AddSample(const INodePtr& node)
+ {
+ Statistics_.AddSample("/custom", node);
+ }
+
+ const TStatistics& GetStatistics()
+ {
+ return Statistics_;
+ }
+
+private:
+ TStatistics Statistics_;
+};
+
+TEST(TStatistics, Consumer)
+{
+ TStatisticsUpdater statisticsUpdater;
+ TStatisticsConsumer consumer(BIND(&TStatisticsUpdater::AddSample, &statisticsUpdater));
+ BuildYsonListFragmentFluently(&consumer)
+ .Item()
+ .BeginMap()
+ .Item("k1").Value(4)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("k2").Value(-7)
+ .EndMap()
+ .Item()
+ .BeginMap()
+ .Item("key")
+ .BeginMap()
+ .Item("subkey")
+ .Value(42)
+ .EndMap()
+ .EndMap();
+
+ const auto& statistics = statisticsUpdater.GetStatistics();
+ EXPECT_EQ(4, GetNumericValue(statistics, "/custom/k1"));
+ EXPECT_EQ(-7, GetNumericValue(statistics, "/custom/k2"));
+ EXPECT_EQ(42, GetNumericValue(statistics, "/custom/key/subkey"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TStatistics, BuildingConsumer)
+{
+ TYsonString statisticsYson(TStringBuf(
+ "{"
+ "abc="
+ "{"
+ "def="
+ "{"
+ "sum=42; count=3; min=5; max=21; last=10;"
+ "};"
+ "degh="
+ "{"
+ "sum=27; count=1; min=27; max=27; last=27;"
+ "};"
+ "};"
+ "xyz="
+ "{"
+ "sum=50; count=5; min=8; max=12;"
+ "};"
+ "}"));
+ auto statistics = ConvertTo<TStatistics>(statisticsYson);
+ auto data = statistics.Data();
+
+ std::map<TString, TSummary> expectedData {
+ { "/abc/def", TSummary(42, 3, 5, 21, 10) },
+ { "/abc/degh", TSummary(27, 1, 27, 27, 27) },
+ { "/xyz", TSummary(50, 5, 8, 12, std::nullopt) },
+ };
+
+ EXPECT_EQ(expectedData, data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/string_ut.cpp b/yt/yt/core/misc/unittests/string_ut.cpp
new file mode 100644
index 0000000000..b2ffbf4218
--- /dev/null
+++ b/yt/yt/core/misc/unittests/string_ut.cpp
@@ -0,0 +1,52 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <library/cpp/yt/string/enum.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EColor,
+ (Red)
+ (BlackAndWhite)
+);
+
+DEFINE_BIT_ENUM(ELangs,
+ ((None) (0x00))
+ ((Cpp) (0x01))
+ ((Go) (0x02))
+ ((Rust) (0x04))
+ ((Python) (0x08))
+ ((JavaScript) (0x10))
+);
+
+TEST(TStringTest, Enum)
+{
+ EXPECT_EQ("red", FormatEnum(EColor::Red));
+ EXPECT_EQ(ParseEnum<EColor>("red"), EColor::Red);
+
+ EXPECT_EQ("black_and_white", FormatEnum(EColor::BlackAndWhite));
+ EXPECT_EQ(ParseEnum<EColor>("black_and_white"), EColor::BlackAndWhite);
+
+ EXPECT_EQ("EColor(100)", FormatEnum(EColor(100)));
+
+ EXPECT_EQ("java_script", FormatEnum(ELangs::JavaScript));
+ EXPECT_EQ(ParseEnum<ELangs>("java_script"), ELangs::JavaScript);
+
+ EXPECT_EQ("none", FormatEnum(ELangs::None));
+ EXPECT_EQ(ParseEnum<ELangs>("none"), ELangs::None);
+
+ EXPECT_EQ("cpp | go", FormatEnum(ELangs::Cpp | ELangs::Go));
+ EXPECT_EQ(ParseEnum<ELangs>("cpp | go"), ELangs::Cpp | ELangs::Go);
+
+ auto four = ELangs::Cpp | ELangs::Go | ELangs::Python | ELangs::JavaScript;
+ EXPECT_EQ("cpp | go | python | java_script", FormatEnum(four));
+ EXPECT_EQ(ParseEnum<ELangs>("cpp | go | python | java_script"), four);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
+
diff --git a/yt/yt/core/misc/unittests/sync_cache_ut.cpp b/yt/yt/core/misc/unittests/sync_cache_ut.cpp
new file mode 100644
index 0000000000..ea3b68604d
--- /dev/null
+++ b/yt/yt/core/misc/unittests/sync_cache_ut.cpp
@@ -0,0 +1,61 @@
+#include <gtest/gtest.h>
+
+#include <yt/yt/core/misc/sync_cache.h>
+
+namespace NYT::NConcurrency {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TTestValue)
+
+struct TTestValue
+ : TSyncCacheValueBase<TString, TTestValue>
+{
+public:
+ using TSyncCacheValueBase::TSyncCacheValueBase;
+
+ i64 Weight = 1;
+};
+
+DEFINE_REFCOUNTED_TYPE(TTestValue)
+
+class TTestCache
+ : public TSyncSlruCacheBase<TString, TTestValue>
+{
+public:
+ using TSyncSlruCacheBase::TSyncSlruCacheBase;
+
+ virtual i64 GetWeight(const TTestValuePtr& value) const
+ {
+ return value->Weight;
+ }
+};
+
+TEST(TSyncSlruCache, DownsizeSegfault)
+{
+ auto config = New<TSlruCacheConfig>();
+ config->Capacity = 100;
+
+ auto cache = New<TTestCache>(config);
+ for (int i = 0; i < 100; i++) {
+ cache->TryInsert(New<TTestValue>(ToString(i)));
+ }
+
+ for (int i = 0; i < 10; i++) {
+ cache->Find(ToString(i));
+ }
+
+ auto smallConfig = New<TSlruCacheDynamicConfig>();
+ smallConfig->Capacity = 50;
+ cache->Reconfigure(smallConfig);
+
+ for (int i = 0; i < 100; i++) {
+ cache->TryInsert(New<TTestValue>(ToString(i + 100)));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NConcurrency
diff --git a/yt/yt/core/misc/unittests/sync_expiring_cache_ut.cpp b/yt/yt/core/misc/unittests/sync_expiring_cache_ut.cpp
new file mode 100644
index 0000000000..103bb81ee0
--- /dev/null
+++ b/yt/yt/core/misc/unittests/sync_expiring_cache_ut.cpp
@@ -0,0 +1,92 @@
+#include <thread>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/misc/sync_expiring_cache.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TSyncExpiringCacheTest, MultipleGet_RaceCondition)
+{
+ auto cache = New<TSyncExpiringCache<int, int>>(
+ BIND([] (int x) {
+ Sleep(TDuration::MilliSeconds(1));
+ return x;
+ }),
+ TDuration::Seconds(1),
+ GetSyncInvoker());
+
+ std::thread thread1([&] { cache->Get(1); });
+ std::thread thread2([&] { cache->Get(1); });
+
+ thread1.join();
+ thread2.join();
+}
+
+TEST(TSyncExpiringCacheTest, FindSetClear)
+{
+ auto cache = New<TSyncExpiringCache<int, int>>(
+ BIND([] (int x) { return x; }),
+ TDuration::Seconds(1),
+ GetSyncInvoker());
+
+ EXPECT_EQ(std::nullopt, cache->Find(1));
+
+ cache->Set(1, 2);
+
+ EXPECT_EQ(2, cache->Find(1));
+
+ cache->Clear();
+
+ EXPECT_EQ(std::nullopt, cache->Find(1));
+}
+
+TEST(TSyncExpiringCacheTest, SetExpirationTimeout)
+{
+ auto cache = New<TSyncExpiringCache<int, int>>(
+ BIND([] (int x) { return x; }),
+ TDuration::Seconds(1),
+ GetSyncInvoker());
+
+ cache->Set(1, 2);
+
+ cache->SetExpirationTimeout(TDuration::MilliSeconds(100));
+
+ Sleep(TDuration::MilliSeconds(200));
+
+ EXPECT_EQ(std::nullopt, cache->Find(1));
+
+ cache->Set(1, 3);
+
+ cache->SetExpirationTimeout(std::nullopt);
+
+ Sleep(TDuration::MilliSeconds(200));
+
+ EXPECT_EQ(3, cache->Find(1));
+}
+
+TEST(TSyncExpiringCacheTest, GetMany)
+{
+ auto cache = New<TSyncExpiringCache<int, int>>(
+ BIND([] (int x) { return x; }),
+ TDuration::Seconds(1),
+ GetSyncInvoker());
+
+ EXPECT_EQ(1, cache->Get({1}));
+ EXPECT_EQ(1, cache->Get({1}));
+
+ std::vector<int> expected{0, 1, 2};
+ EXPECT_EQ(expected, cache->Get({0, 1, 2}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/time_formula_ut.cpp b/yt/yt/core/misc/unittests/time_formula_ut.cpp
new file mode 100644
index 0000000000..08378c2e2e
--- /dev/null
+++ b/yt/yt/core/misc/unittests/time_formula_ut.cpp
@@ -0,0 +1,97 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/arithmetic_formula.h>
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimeFormulaParseTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<const char*, bool>>
+{ };
+
+TEST_P(TTimeFormulaParseTest, Test)
+{
+ const auto& args = GetParam();
+ const auto& formula = std::get<0>(args);
+ bool wellformed = std::get<1>(args);
+
+ if (wellformed) {
+ MakeTimeFormula(formula);
+ } else {
+ EXPECT_THROW(MakeTimeFormula(formula), TErrorException);
+ }
+}
+
+
+INSTANTIATE_TEST_SUITE_P(
+ TTimeFormulaParseTest,
+ TTimeFormulaParseTest,
+ ::testing::Values(
+ std::make_tuple("", true),
+ std::make_tuple("hours == 0", true),
+ std::make_tuple("minutes == 0", true),
+ std::make_tuple("hours % 2 == 0 && minutes == 1", true),
+ std::make_tuple("hours * 100 + minutes >= 1030", true),
+ std::make_tuple("seconds < 10", false),
+ std::make_tuple("HOURS > 10", false),
+ std::make_tuple("hours ++ 1", false)
+));
+
+class TTimeFormulaCorrectnessTest
+ : public ::testing::Test
+ , public ::testing::WithParamInterface<std::tuple<const char*, int>>
+{ };
+
+TEST_P(TTimeFormulaCorrectnessTest, Test)
+{
+ const auto& args = GetParam();
+ const auto& formula = std::get<0>(args);
+ int expectedCount = std::get<1>(args);
+
+ auto timeFormula = MakeTimeFormula(formula);
+
+
+ struct timeval tv;
+ tv.tv_usec = 0;
+ tv.tv_sec = 1527777898;
+ TInstant timePoint(tv);
+
+ int count = 0;
+ for (int i = 0; i < 24 * 60; ++i) {
+ count += timeFormula.IsSatisfiedBy(timePoint);
+ timePoint += TDuration::Minutes(1);
+ }
+
+ EXPECT_EQ(count, expectedCount);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TTimeFormulaCorrectnessTest,
+ TTimeFormulaCorrectnessTest,
+ ::testing::Values(
+ std::make_tuple("1", 24 * 60),
+ std::make_tuple("hours == 0", 60),
+ std::make_tuple("minutes == 0", 24),
+ std::make_tuple("hours % 2 == 0 && minutes == 1", 12),
+ std::make_tuple("hours * 100 + minutes >= 1030", 24 * 60 - (10 * 60 + 30)),
+ std::make_tuple("minutes % 5 == 3 || minutes % 5 == 0", 24 * 60 / 5 * 2)
+));
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TTimeFormulaTest, Misc)
+{
+ auto formula1 = MakeTimeFormula("");
+ auto formula2 = MakeTimeFormula("hours/0");
+ EXPECT_THROW(formula1.IsSatisfiedBy(TInstant{}), TErrorException);
+ EXPECT_THROW(formula2.IsSatisfiedBy(TInstant{}), TErrorException);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/tls_expiring_cache_ut.cpp b/yt/yt/core/misc/unittests/tls_expiring_cache_ut.cpp
new file mode 100644
index 0000000000..e3f4415e72
--- /dev/null
+++ b/yt/yt/core/misc/unittests/tls_expiring_cache_ut.cpp
@@ -0,0 +1,66 @@
+#include <thread>
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/invoker_util.h>
+
+#include <yt/yt/core/misc/tls_expiring_cache.h>
+
+namespace NYT {
+namespace {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TThreadLocalExpiringCacheTest, Simple)
+{
+ TThreadLocalExpiringCache<TString, int> cache(TDuration::Hours(1));
+
+ EXPECT_EQ(std::nullopt, cache.Get("hello"));
+ EXPECT_EQ(std::nullopt, cache.Get("hello"));
+
+ cache.Set("hello", 42);
+ EXPECT_EQ(42, cache.Get("hello"));
+}
+
+TEST(TThreadLocalExpiringCacheTest, ThreadLocal)
+{
+ TThreadLocalExpiringCache<TString, int> cache(TDuration::Hours(1));
+
+ std::vector<std::thread> threads;
+ for (int i = 0; i < 16; ++i) {
+ threads.emplace_back([&] {
+ EXPECT_EQ(std::nullopt, cache.Get("hello"));
+ EXPECT_EQ(std::nullopt, cache.Get("hello"));
+
+ cache.Set("hello", 42);
+ EXPECT_EQ(42, cache.Get("hello"));
+ });
+ }
+
+ for (auto& thread : threads) {
+ thread.join();
+ }
+}
+
+TEST(TThreadLocalExpiringCacheTest, Expiration)
+{
+ auto duration = TDuration::MilliSeconds(100);
+ auto cpuDuration = DurationToCpuDuration(duration);
+
+ TThreadLocalExpiringCache<TString, int> cache(duration);
+ cache.Set("hello", 42);
+ EXPECT_EQ(42, cache.Get("hello"));
+ auto now = GetCpuInstant();
+
+ while (GetCpuInstant() < now + cpuDuration) {
+ Sleep(TDuration::MilliSeconds(15));
+ }
+ EXPECT_EQ(std::nullopt, cache.Get("hello"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/topological_ordering_ut.cpp b/yt/yt/core/misc/unittests/topological_ordering_ut.cpp
new file mode 100644
index 0000000000..1c95aba99b
--- /dev/null
+++ b/yt/yt/core/misc/unittests/topological_ordering_ut.cpp
@@ -0,0 +1,112 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/topological_ordering.h>
+
+#include <random>
+
+namespace NYT {
+namespace {
+
+using namespace ::testing;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTopologicalOrderingTest
+ : public Test
+{
+protected:
+ std::vector<std::pair<int, int>> CurrentEdges_;
+ TIncrementalTopologicalOrdering<int> IncrementalOrdering_;
+
+ void AddEdgeAndValidate(int from, int to)
+ {
+ CurrentEdges_.emplace_back(from, to);
+ IncrementalOrdering_.AddEdge(from, to);
+ ValidateOrdering();
+ }
+
+ void ValidateOrdering()
+ {
+ const auto& ordering = IncrementalOrdering_.GetOrdering();
+ THashMap<int, int> positionInOrdering;
+ for (int index = 0; index < std::ssize(ordering); ++index) {
+ positionInOrdering[ordering[index]] = index;
+ }
+ for (const auto& edge : CurrentEdges_) {
+ EXPECT_TRUE(positionInOrdering.contains(edge.first));
+ EXPECT_TRUE(positionInOrdering.contains(edge.second));
+ EXPECT_LT(positionInOrdering[edge.first], positionInOrdering[edge.second]);
+ }
+ }
+
+ void Clear() {
+ CurrentEdges_.clear();
+ IncrementalOrdering_ = TIncrementalTopologicalOrdering<int>();
+ }
+
+ void TearDown() override
+ {
+ Clear();
+ }
+};
+
+TEST_F(TTopologicalOrderingTest, Simple)
+{
+ AddEdgeAndValidate(2, 0);
+ EXPECT_THAT(IncrementalOrdering_.GetOrdering(), ElementsAre(2, 0));
+
+ AddEdgeAndValidate(0, 3);
+ EXPECT_THAT(IncrementalOrdering_.GetOrdering(), ElementsAre(2, 0, 3));
+
+ // Adding duplicating edge doesn't change anything.
+ AddEdgeAndValidate(2, 0);
+ EXPECT_THAT(IncrementalOrdering_.GetOrdering(), ElementsAre(2, 0, 3));
+
+ AddEdgeAndValidate(2, 1);
+ // We can't be sure if topological ordering is {2, 0, 3, 1}, {2, 0, 1, 3} or {2, 1, 0, 3} now.
+
+ AddEdgeAndValidate(1, 3);
+ // Now topological ordering is either {2, 0, 1, 3} or {2, 1, 0, 3}.
+
+ AddEdgeAndValidate(1, 0);
+ EXPECT_THAT(IncrementalOrdering_.GetOrdering(), ElementsAre(2, 1, 0, 3));
+}
+
+TEST_F(TTopologicalOrderingTest, RandomizedTest)
+{
+ const int iterationCount = 1000;
+ const int maxVertexCount = 15;
+ const int maxVertexValue = 1000 * 1000 * 1000;
+ std::mt19937 gen;
+ for (int iteration = 0; iteration < iterationCount; ++iteration) {
+ int vertexCount = std::uniform_int_distribution<>(2, maxVertexCount)(gen);
+ // Generate the vertex values.
+ THashSet<int> vertices;
+ while (std::ssize(vertices) < vertexCount) {
+ vertices.insert(std::uniform_int_distribution<>(0, maxVertexValue)(gen));
+ }
+
+ // Generate the desired topological ordering. We will extract edges from it and
+ // feed to the incremental ordering.
+ std::vector<int> desiredTopologicalOrdering(vertices.begin(), vertices.end());
+ std::shuffle(desiredTopologicalOrdering.begin(), desiredTopologicalOrdering.end(), gen);
+
+ std::vector<std::pair<int, int>> edges;
+ for (int fromIndex = 0; fromIndex < vertexCount; ++fromIndex) {
+ for (int toIndex = fromIndex + 1; toIndex < vertexCount; ++toIndex) {
+ int from = desiredTopologicalOrdering[fromIndex];
+ int to = desiredTopologicalOrdering[toIndex];
+ AddEdgeAndValidate(from, to);
+ }
+ }
+
+ EXPECT_EQ(IncrementalOrdering_.GetOrdering(), desiredTopologicalOrdering);
+
+ Clear();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/ya.make b/yt/yt/core/misc/unittests/ya.make
new file mode 100644
index 0000000000..62b75b637a
--- /dev/null
+++ b/yt/yt/core/misc/unittests/ya.make
@@ -0,0 +1,105 @@
+GTEST(unittester-core-misc)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ algorithm_helpers_ut.cpp
+ arithmetic_formula_ut.cpp
+ atomic_ptr_ut.cpp
+ async_expiring_cache_ut.cpp
+ async_slru_cache_ut.cpp
+ bind_ut.cpp
+ bit_packed_integer_vector_ut.cpp
+ boolean_formula_ut.cpp
+ callback_ut.cpp
+ checksum_ut.cpp
+ codicil_ut.cpp
+ concurrent_cache_ut.cpp
+ default_map_ut.cpp
+ dnf_ut.cpp
+ digest_ut.cpp
+ ema_counter_ut.cpp
+ enum_ut.cpp
+ error_code_ut.cpp
+ error_ut.cpp
+ fair_scheduler_ut.cpp
+ fenwick_tree_ut.cpp
+ finally_ut.cpp
+ format_ut.cpp
+ fs_ut.cpp
+ future_ut.cpp
+ guid_ut.cpp
+ hash_filter_ut.cpp
+ hazard_ptr_ut.cpp
+ heap_ut.cpp
+ histogram_ut.cpp
+ historic_usage_aggregator_ut.cpp
+ hyperloglog_ut.cpp
+ intern_registry_ut.cpp
+ job_signaler_ut.cpp
+ lock_free_hash_table_ut.cpp
+ lru_cache_ut.cpp
+ maybe_inf_ut.cpp
+ memory_tag_ut.cpp
+ mpsc_fair_share_queue_ut.cpp
+ mpsc_stack_ut.cpp
+ mpsc_queue_ut.cpp
+ relaxed_mpsc_queue_ut.cpp
+ mpl_ut.cpp
+ pattern_formatter_ut.cpp
+ persistent_queue_ut.cpp
+ phoenix_ut.cpp
+ pool_allocator_ut.cpp
+ proc_ut.cpp
+ random_ut.cpp
+ ref_counted_tracker_ut.cpp
+ ring_queue_ut.cpp
+ skip_list_ut.cpp
+ slab_allocator_ut.cpp
+ sliding_window_ut.cpp
+ sync_cache_ut.cpp
+ spsc_queue_ut.cpp
+ statistics_ut.cpp
+ string_ut.cpp
+ sync_expiring_cache_ut.cpp
+ time_formula_ut.cpp
+ tls_expiring_cache_ut.cpp
+ topological_ordering_ut.cpp
+ yverify_ut.cpp
+ zerocopy_output_writer_ut.cpp
+ hedging_manager_ut.cpp
+
+ proto/ref_counted_tracker_ut.proto
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/misc/unittests/yverify_ut.cpp b/yt/yt/core/misc/unittests/yverify_ut.cpp
new file mode 100644
index 0000000000..d392204f3c
--- /dev/null
+++ b/yt/yt/core/misc/unittests/yverify_ut.cpp
@@ -0,0 +1,60 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/generic/yexception.h>
+
+namespace NYT {
+namespace {
+
+using ::testing::_;
+using ::testing::A;
+using ::testing::NiceMock;
+using ::testing::ReturnArg;
+using ::testing::Throw;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCallee
+{
+public:
+ virtual ~TCallee() = default;
+ virtual bool F(bool passThrough, const char* comment) = 0;
+};
+
+class TMockCallee
+ : public TCallee
+{
+public:
+ TMockCallee()
+ {
+ ON_CALL(*this, F(A<bool>(), _))
+ .WillByDefault(ReturnArg<0>());
+ }
+
+ MOCK_METHOD(bool, F, (bool passThrough, const char* comment), (override));
+};
+
+TEST(TVerifyDeathTest, DISABLED_NoCrashForTruthExpression)
+{
+ TMockCallee callee;
+ EXPECT_CALL(callee, F(true, _)).Times(1);
+
+ YT_VERIFY(callee.F(true, "This should be okay."));
+ SUCCEED();
+}
+
+TEST(TVerifyDeathTest, DISABLED_CrashForFalseExpression)
+{
+ NiceMock<TMockCallee> callee;
+
+ ASSERT_DEATH(
+ { YT_VERIFY(callee.F(false, "Cheshire Cat")); },
+ ".*Cheshire Cat.*"
+ );
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/unittests/zerocopy_output_writer_ut.cpp b/yt/yt/core/misc/unittests/zerocopy_output_writer_ut.cpp
new file mode 100644
index 0000000000..4cecd2bf26
--- /dev/null
+++ b/yt/yt/core/misc/unittests/zerocopy_output_writer_ut.cpp
@@ -0,0 +1,145 @@
+#include <yt/yt/core/test_framework/fixed_growth_string_output.h>
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/zerocopy_output_writer.h>
+
+#include <util/random/fast.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TZeroCopyOutputStreamWriter, TestBasic)
+{
+ constexpr int GrowthSize = 7;
+ TString string;
+ auto buffer1 = TString("abcdef");
+ auto buffer2 = TString("kinda long buffer");
+
+ TFixedGrowthStringOutput stream(&string, GrowthSize);
+ {
+ TZeroCopyOutputStreamWriter writer(&stream);
+
+ EXPECT_EQ(static_cast<ssize_t>(writer.RemainingBytes()), 0);
+ EXPECT_EQ(writer.GetTotalWrittenSize(), 0u);
+
+ writer.Write(buffer1.data(), buffer1.size());
+ EXPECT_EQ(writer.RemainingBytes(), 7u);
+ EXPECT_EQ(writer.GetTotalWrittenSize(), buffer1.length());
+
+ writer.UndoRemaining();
+ EXPECT_EQ(writer.RemainingBytes(), 0u);
+ EXPECT_EQ(writer.GetTotalWrittenSize(), buffer1.length());
+
+ writer.Write(buffer2.data(), buffer2.length());
+ EXPECT_EQ(writer.GetTotalWrittenSize(), buffer1.length() + buffer2.length());
+ EXPECT_EQ(static_cast<ssize_t>(writer.RemainingBytes()), GrowthSize);
+ }
+
+ stream.Finish();
+ EXPECT_EQ(string, buffer1 + buffer2);
+}
+
+TEST(TZeroCopyOutputStreamWriter, TestStress)
+{
+ TString string;
+ constexpr auto StringSize = 1000;
+ TFastRng64 gen(42);
+ for (int i = 0; i < StringSize; ++i) {
+ string.push_back('a' + gen.Uniform(0, 26));
+ }
+
+ for (int growthSize = 1; growthSize < 20; ++growthSize) {
+ auto writtenBytes = 0;
+ TString outputString;
+ TFixedGrowthStringOutput stream(&outputString, growthSize);
+ TZeroCopyOutputStreamWriter writer(&stream);
+ while (true) {
+ auto toWrite = gen.Uniform(1, 3 * growthSize);
+ toWrite = Min(toWrite, string.size() - writtenBytes);
+ writer.Write(string.data() + writtenBytes, toWrite);
+ writtenBytes += toWrite;
+
+ if (writtenBytes == std::ssize(string)) {
+ break;
+ }
+
+ if (writer.RemainingBytes() > 0) {
+ *writer.Current() = string[writtenBytes];
+ ++writtenBytes;
+ writer.Advance(1);
+ }
+ }
+ writer.UndoRemaining();
+ stream.Finish();
+ EXPECT_EQ(string, outputString) << "test for growth size " << growthSize << " failed";
+ }
+}
+
+TEST(TZeroCopyOutputStreamWriter, TestVarInt)
+{
+ constexpr auto GrowthSize = 4;
+ TString outputString;
+ TFixedGrowthStringOutput stream(&outputString, GrowthSize);
+ TZeroCopyOutputStreamWriter writer(&stream);
+
+ EXPECT_EQ(1, WriteVarInt(&writer, static_cast<i32>(-17)));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+ EXPECT_EQ(1, WriteVarInt32(&writer, static_cast<i32>(-18)));
+ EXPECT_EQ(3u, writer.RemainingBytes());
+ EXPECT_EQ(5, WriteVarInt(&writer, std::numeric_limits<i32>::min()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+ EXPECT_EQ(5, WriteVarInt32(&writer, std::numeric_limits<i32>::min()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+
+ EXPECT_EQ(1, WriteVarInt(&writer, static_cast<ui32>(10)));
+ EXPECT_EQ(3u, writer.RemainingBytes());
+ EXPECT_EQ(1, WriteVarUint32(&writer, static_cast<ui32>(11)));
+ EXPECT_EQ(2u, writer.RemainingBytes());
+ EXPECT_EQ(5, WriteVarInt(&writer, std::numeric_limits<ui32>::max()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+ EXPECT_EQ(5, WriteVarUint32(&writer, std::numeric_limits<ui32>::max()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+
+ EXPECT_EQ(1, WriteVarInt(&writer, static_cast<i64>(-23)));
+ EXPECT_EQ(3u, writer.RemainingBytes());
+ EXPECT_EQ(1, WriteVarInt64(&writer, static_cast<i64>(-22)));
+ EXPECT_EQ(2u, writer.RemainingBytes());
+ EXPECT_EQ(10, WriteVarInt(&writer, std::numeric_limits<i64>::min()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+ EXPECT_EQ(10, WriteVarInt64(&writer, std::numeric_limits<i64>::min()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+
+ EXPECT_EQ(2, WriteVarInt(&writer, static_cast<ui64>(200)));
+ EXPECT_EQ(2u, writer.RemainingBytes());
+ EXPECT_EQ(2, WriteVarUint64(&writer, static_cast<ui64>(211)));
+ EXPECT_EQ(0u, writer.RemainingBytes());
+ EXPECT_EQ(10, WriteVarInt(&writer, std::numeric_limits<ui64>::max()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+ EXPECT_EQ(10, WriteVarUint64(&writer, std::numeric_limits<ui64>::max()));
+ EXPECT_EQ(4u, writer.RemainingBytes());
+
+ writer.UndoRemaining();
+ stream.Finish();
+
+ EXPECT_EQ(
+ outputString,
+ "\x21\x23"
+ "\xFF\xFF\xFF\xFF\x0F"
+ "\xFF\xFF\xFF\xFF\x0F"
+ "\x0A\x0B"
+ "\xFF\xFF\xFF\xFF\x0F"
+ "\xFF\xFF\xFF\xFF\x0F"
+ "\x2D\x2B"
+ "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"
+ "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"
+ "\xC8\x01" "\xD3\x01"
+ "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"
+ "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/misc/utf8_decoder.cpp b/yt/yt/core/misc/utf8_decoder.cpp
new file mode 100644
index 0000000000..6acb70b69b
--- /dev/null
+++ b/yt/yt/core/misc/utf8_decoder.cpp
@@ -0,0 +1,83 @@
+#include "utf8_decoder.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUtf8Transcoder::TUtf8Transcoder(bool enableEncoding)
+ : EnableEncoding_(enableEncoding)
+{ }
+
+TStringBuf TUtf8Transcoder::Encode(TStringBuf str)
+{
+ if (!EnableEncoding_) {
+ return str;
+ }
+
+ Buffer_.clear();
+
+ bool isAscii = true;
+ for (int i = 0; i < std::ssize(str); ++i) {
+ if (ui8(str[i]) < 128) {
+ if (!isAscii) {
+ Buffer_.push_back(str[i]);
+ }
+ } else {
+ if (isAscii) {
+ Buffer_.resize(i);
+ std::copy(str.data(), str.data() + i, Buffer_.data());
+ isAscii = false;
+ }
+ Buffer_.push_back('\xC0' | (ui8(str[i]) >> 6));
+ Buffer_.push_back('\x80' | (ui8(str[i]) & ~'\xC0'));
+ }
+ }
+
+ if (isAscii) {
+ return str;
+ } else {
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+}
+
+TStringBuf TUtf8Transcoder::Decode(TStringBuf str)
+{
+ if (!EnableEncoding_) {
+ return str;
+ }
+
+ Buffer_.clear();
+
+ bool isAscii = true;
+ for (int i = 0; i < std::ssize(str); ++i) {
+ if (ui8(str[i]) < 128) {
+ if (!isAscii) {
+ Buffer_.push_back(str[i]);
+ }
+ } else if ((str[i] & '\xFC') == '\xC0' && i + 1 < std::ssize(str)) {
+ if (isAscii) {
+ Buffer_.resize(i);
+ std::copy(str.data(), str.data() + i, Buffer_.data());
+ isAscii = false;
+ }
+ Buffer_.push_back(((str[i] & '\x03') << 6) | (str[i + 1] & '\x3F'));
+ i += 1;
+ } else {
+ THROW_ERROR_EXCEPTION("Unicode symbols with codes greater than 255 are not supported. "
+ "Please refer to https://wiki.yandex-team.ru/yt/userdoc/formats/#json and "
+ "consider using encode_utf8=false in format options");
+ }
+ }
+
+ if (isAscii) {
+ return str;
+ } else {
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/utf8_decoder.h b/yt/yt/core/misc/utf8_decoder.h
new file mode 100644
index 0000000000..3128fd3eb6
--- /dev/null
+++ b/yt/yt/core/misc/utf8_decoder.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUtf8Transcoder
+{
+public:
+ explicit TUtf8Transcoder(bool enableEncoding = true);
+
+ TStringBuf Encode(TStringBuf str);
+ TStringBuf Decode(TStringBuf str);
+
+private:
+ bool EnableEncoding_;
+ std::vector<char> Buffer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/zerocopy_output_writer-inl.h b/yt/yt/core/misc/zerocopy_output_writer-inl.h
new file mode 100644
index 0000000000..47f8fc0634
--- /dev/null
+++ b/yt/yt/core/misc/zerocopy_output_writer-inl.h
@@ -0,0 +1,120 @@
+#ifndef ZEROCOPY_OUTPUT_WRITER_INL_H_
+#error "Direct inclusion of this file is not allowed, include zerocopy_output_writer.h"
+// For the sake of sane code completion.
+#include "zerocopy_output_writer.h"
+#endif
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/coding/varint.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+char* TZeroCopyOutputStreamWriter::Current() const
+{
+ return Current_;
+}
+
+ui64 TZeroCopyOutputStreamWriter::RemainingBytes() const
+{
+ return RemainingBytes_;
+}
+
+void TZeroCopyOutputStreamWriter::Advance(size_t bytes)
+{
+ YT_VERIFY(bytes <= RemainingBytes_);
+ Current_ += bytes;
+ RemainingBytes_ -= bytes;
+}
+
+void TZeroCopyOutputStreamWriter::Write(const void* buffer, size_t length)
+{
+ if (length > RemainingBytes_) {
+ UndoRemaining();
+ Output_->Write(buffer, length);
+ TotalWrittenBlockSize_ += length;
+ ObtainNextBlock();
+ } else {
+ memcpy(Current_, buffer, length);
+ Advance(length);
+ }
+}
+
+ui64 TZeroCopyOutputStreamWriter::GetTotalWrittenSize() const
+{
+ return TotalWrittenBlockSize_ - RemainingBytes_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+Y_FORCE_INLINE int WriteVarInt(char* output, T value)
+{
+ if constexpr (std::is_same_v<T, i32>) {
+ return WriteVarInt32(output, value);
+ } else if constexpr (std::is_same_v<T, ui32>) {
+ return WriteVarUint32(output, value);
+ } else if constexpr (std::is_same_v<T, i64>) {
+ return WriteVarInt64(output, value);
+ } else if constexpr (std::is_same_v<T, ui64>) {
+ return WriteVarUint64(output, value);
+ } else {
+ static_assert(TDependentFalse<T>);
+ }
+}
+
+template <typename T>
+constexpr auto MaxVarIntSize = [] {
+ if constexpr (std::is_same_v<T, i32>) {
+ return MaxVarInt32Size;
+ } else if constexpr (std::is_same_v<T, ui32>) {
+ return MaxVarUint32Size;
+ } else if constexpr (std::is_same_v<T, i64>) {
+ return MaxVarInt64Size;
+ } else if constexpr (std::is_same_v<T, ui64>) {
+ return MaxVarUint64Size;
+ } else {
+ static_assert(TDependentFalse<T>);
+ }
+}();
+
+template <typename T>
+int WriteVarInt(TZeroCopyOutputStreamWriter* writer, T value)
+{
+ int size;
+ if (writer->RemainingBytes() >= MaxVarIntSize<T>) {
+ size = WriteVarInt(writer->Current(), value);
+ writer->Advance(size);
+ } else {
+ char result[MaxVarIntSize<T>];
+ size = WriteVarInt(result, value);
+ writer->Write(result, size);
+ }
+ return size;
+}
+
+int WriteVarUint32(TZeroCopyOutputStreamWriter* writer, ui32 value)
+{
+ return WriteVarInt(writer, value);
+}
+
+int WriteVarUint64(TZeroCopyOutputStreamWriter* writer, ui64 value)
+{
+ return WriteVarInt(writer, value);
+}
+
+int WriteVarInt32(TZeroCopyOutputStreamWriter* writer, i32 value)
+{
+ return WriteVarInt(writer, value);
+}
+
+int WriteVarInt64(TZeroCopyOutputStreamWriter* writer, i64 value)
+{
+ return WriteVarInt(writer, value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/zerocopy_output_writer.cpp b/yt/yt/core/misc/zerocopy_output_writer.cpp
new file mode 100644
index 0000000000..ca23f4f2d7
--- /dev/null
+++ b/yt/yt/core/misc/zerocopy_output_writer.cpp
@@ -0,0 +1,42 @@
+#include "zerocopy_output_writer.h"
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TZeroCopyOutputStreamWriter::TZeroCopyOutputStreamWriter(IZeroCopyOutput* output)
+ : Output_(output)
+{
+ ObtainNextBlock();
+ // Need to have a "not dirty" stream after creating TZeroCopyOutputStreamWriter.
+ // This is for when we want to reuse a writer and clear the stream at the beginning of each iteration.
+ // But we want to have a valid (not null) Current_ pointer as not to break the current behavior.
+ UndoRemaining();
+}
+
+TZeroCopyOutputStreamWriter::~TZeroCopyOutputStreamWriter()
+{
+ if (RemainingBytes_ > 0) {
+ UndoRemaining();
+ }
+}
+
+void TZeroCopyOutputStreamWriter::ObtainNextBlock()
+{
+ if (RemainingBytes_ > 0) {
+ UndoRemaining();
+ }
+ RemainingBytes_ = Output_->Next(&Current_);
+ TotalWrittenBlockSize_ += RemainingBytes_;
+}
+
+void TZeroCopyOutputStreamWriter::UndoRemaining()
+{
+ Output_->Undo(RemainingBytes_);
+ TotalWrittenBlockSize_ -= RemainingBytes_;
+ RemainingBytes_ = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/misc/zerocopy_output_writer.h b/yt/yt/core/misc/zerocopy_output_writer.h
new file mode 100644
index 0000000000..3365305fb9
--- /dev/null
+++ b/yt/yt/core/misc/zerocopy_output_writer.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <util/stream/zerocopy_output.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Simple wrapper around
+class TZeroCopyOutputStreamWriter
+ : private TNonCopyable
+{
+public:
+ explicit TZeroCopyOutputStreamWriter(IZeroCopyOutput* output);
+
+ ~TZeroCopyOutputStreamWriter();
+
+ Y_FORCE_INLINE char* Current() const;
+ Y_FORCE_INLINE ui64 RemainingBytes() const;
+ Y_FORCE_INLINE void Advance(size_t bytes);
+ void UndoRemaining();
+ Y_FORCE_INLINE void Write(const void* buffer, size_t length);
+ Y_FORCE_INLINE ui64 GetTotalWrittenSize() const;
+
+private:
+ void ObtainNextBlock();
+
+private:
+ IZeroCopyOutput* Output_;
+ char* Current_ = nullptr;
+ ui64 RemainingBytes_ = 0;
+ ui64 TotalWrittenBlockSize_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Writes varint to |writer| and returned number of written bytes.
+template <typename T>
+Y_FORCE_INLINE int WriteVarInt(TZeroCopyOutputStreamWriter* writer, T value);
+
+Y_FORCE_INLINE int WriteVarUint32(TZeroCopyOutputStreamWriter* writer, ui32 value);
+Y_FORCE_INLINE int WriteVarUint64(TZeroCopyOutputStreamWriter* writer, ui64 value);
+Y_FORCE_INLINE int WriteVarInt32(TZeroCopyOutputStreamWriter* writer, i32 value);
+Y_FORCE_INLINE int WriteVarInt64(TZeroCopyOutputStreamWriter* writer, i64 value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define ZEROCOPY_OUTPUT_WRITER_INL_H_
+#include "zerocopy_output_writer-inl.h"
+#undef ZEROCOPY_OUTPUT_WRITER_INL_H_
diff --git a/yt/yt/core/net/address.cpp b/yt/yt/core/net/address.cpp
new file mode 100644
index 0000000000..563cea35b0
--- /dev/null
+++ b/yt/yt/core/net/address.cpp
@@ -0,0 +1,1275 @@
+#include "address.h"
+
+#include "local_address.h"
+#include "config.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/dns/dns_resolver.h>
+#include <yt/yt/core/dns/ares_dns_resolver.h>
+#include <yt/yt/core/dns/private.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/async_expiring_cache.h>
+#include <yt/yt/core/misc/fs.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+#include <util/generic/singleton.h>
+
+#include <util/network/interface.h>
+
+#include <util/string/hex.h>
+
+#ifdef _win_
+ #include <ws2ipdef.h>
+ #include <winsock2.h>
+#endif
+
+#ifdef _unix_
+ #include <ifaddrs.h>
+ #include <netdb.h>
+ #include <netinet/in.h>
+ #include <arpa/inet.h>
+ #include <sys/socket.h>
+ #include <sys/un.h>
+ #include <unistd.h>
+#endif
+
+namespace NYT::NNet {
+
+using namespace NConcurrency;
+using namespace NYson;
+using namespace NYTree;
+using namespace NDns;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = NetLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+// These are implemented in local_address.cpp.
+
+// Update* function interacts with the system to determine actual hostname
+// of the local machine (by calling `gethostname` and `getaddrinfo`).
+// On success, calls Write* with the hostname, throws on errors.
+void UpdateLocalHostName(const TAddressResolverConfigPtr& config);
+
+// Updates the loopback address based on available configuration.
+void UpdateLoopbackAddress(const TAddressResolverConfigPtr& config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString BuildServiceAddress(TStringBuf hostName, int port)
+{
+ return Format("%v:%v", hostName, port);
+}
+
+void ParseServiceAddress(TStringBuf address, TStringBuf* hostName, int* port)
+{
+ auto colonIndex = address.find_last_of(':');
+ if (colonIndex == TString::npos) {
+ THROW_ERROR_EXCEPTION("Service address %Qv is malformed, <host>:<port> format is expected",
+ address);
+ }
+
+ if (hostName) {
+ *hostName = address.substr(0, colonIndex);
+ }
+
+ if (port) {
+ try {
+ *port = FromString<int>(address.substr(colonIndex + 1));
+ } catch (const std::exception) {
+ THROW_ERROR_EXCEPTION("Port number in service address %Qv is malformed",
+ address);
+ }
+ }
+}
+
+int GetServicePort(TStringBuf address)
+{
+ int result;
+ ParseServiceAddress(address, nullptr, &result);
+ return result;
+}
+
+TStringBuf GetServiceHostName(TStringBuf address)
+{
+ TStringBuf result;
+ ParseServiceAddress(address, &result, nullptr);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TNetworkAddress NullNetworkAddress;
+
+TNetworkAddress::TNetworkAddress()
+{
+ Storage_ = {};
+ Storage_.ss_family = AF_UNSPEC;
+ Length_ = sizeof(Storage_);
+}
+
+TNetworkAddress::TNetworkAddress(const TNetworkAddress& other, int port)
+{
+ memcpy(&Storage_, &other.Storage_, sizeof(Storage_));
+ switch (Storage_.ss_family) {
+ case AF_INET:
+ reinterpret_cast<sockaddr_in*>(&Storage_)->sin_port = htons(port);
+ Length_ = sizeof(sockaddr_in);
+ break;
+ case AF_INET6:
+ reinterpret_cast<sockaddr_in6*>(&Storage_)->sin6_port = htons(port);
+ Length_ = sizeof(sockaddr_in6);
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Unknown network address family")
+ << TErrorAttribute("family", Storage_.ss_family);
+ }
+}
+
+TNetworkAddress::TNetworkAddress(const sockaddr& other, socklen_t length)
+{
+ Length_ = length == 0 ? GetGenericLength(other) : length;
+ memcpy(&Storage_, &other, Length_);
+}
+
+TNetworkAddress::TNetworkAddress(int family, const char* addr, size_t size)
+{
+ Storage_ = {};
+ Storage_.ss_family = family;
+ switch (Storage_.ss_family) {
+ case AF_INET: {
+ auto* typedSockAddr = reinterpret_cast<sockaddr_in*>(&Storage_);
+ if (size > sizeof(sockaddr_in)) {
+ THROW_ERROR_EXCEPTION("Wrong size of AF_INET address")
+ << TErrorAttribute("size", size);
+ }
+ memcpy(&typedSockAddr->sin_addr, addr, size);
+ Length_ = sizeof(sockaddr_in);
+ break;
+ }
+ case AF_INET6: {
+ auto* typedSockAddr = reinterpret_cast<sockaddr_in6*>(&Storage_);
+ if (size > sizeof(sockaddr_in6)) {
+ THROW_ERROR_EXCEPTION("Wrong size of AF_INET6 address")
+ << TErrorAttribute("size", size);
+ }
+ memcpy(&typedSockAddr->sin6_addr, addr, size);
+ Length_ = sizeof(sockaddr_in6);
+ break;
+ }
+ default:
+ THROW_ERROR_EXCEPTION("Unknown network address family")
+ << TErrorAttribute("family", family);
+ }
+}
+
+sockaddr* TNetworkAddress::GetSockAddr()
+{
+ return reinterpret_cast<sockaddr*>(&Storage_);
+}
+
+const sockaddr* TNetworkAddress::GetSockAddr() const
+{
+ return reinterpret_cast<const sockaddr*>(&Storage_);
+}
+
+socklen_t TNetworkAddress::GetGenericLength(const sockaddr& sockAddr)
+{
+ switch (sockAddr.sa_family) {
+#ifdef _unix_
+ case AF_UNIX:
+ return sizeof(sockaddr_un);
+#endif
+ case AF_INET:
+ return sizeof(sockaddr_in);
+ case AF_INET6:
+ return sizeof(sockaddr_in6);
+ default:
+ // Don't know its actual size, report the maximum possible.
+ return sizeof(sockaddr_storage);
+ }
+}
+
+int TNetworkAddress::GetPort() const
+{
+ switch (Storage_.ss_family) {
+ case AF_INET:
+ return ntohs(reinterpret_cast<const sockaddr_in*>(&Storage_)->sin_port);
+ case AF_INET6:
+ return ntohs(reinterpret_cast<const sockaddr_in6*>(&Storage_)->sin6_port);
+ default:
+ THROW_ERROR_EXCEPTION("Address has no port");
+ }
+}
+
+bool TNetworkAddress::IsUnix() const
+{
+ return Storage_.ss_family == AF_UNIX;
+}
+
+bool TNetworkAddress::IsIP() const
+{
+ return IsIP4() || IsIP6();
+}
+
+bool TNetworkAddress::IsIP4() const
+{
+ return Storage_.ss_family == AF_INET;
+}
+
+bool TNetworkAddress::IsIP6() const
+{
+ return Storage_.ss_family == AF_INET6;
+}
+
+TIP6Address TNetworkAddress::ToIP6Address() const
+{
+ if (Storage_.ss_family != AF_INET6) {
+ THROW_ERROR_EXCEPTION("Address is not an IPv6 address");
+ }
+
+ auto addr = reinterpret_cast<const sockaddr_in6*>(&Storage_)->sin6_addr;
+ std::reverse(addr.s6_addr, addr.s6_addr + sizeof(addr));
+ return TIP6Address::FromRawBytes(addr.s6_addr);
+}
+
+socklen_t TNetworkAddress::GetLength() const
+{
+ return Length_;
+}
+
+socklen_t* TNetworkAddress::GetLengthPtr()
+{
+ return &Length_;
+}
+
+TErrorOr<TNetworkAddress> TNetworkAddress::TryParse(TStringBuf address)
+{
+ TString ipAddress(address);
+ std::optional<int> port;
+
+ auto closingBracketIndex = address.find(']');
+ if (closingBracketIndex != TString::npos) {
+ if (closingBracketIndex == TString::npos || address.empty() || address[0] != '[') {
+ return TError("Address %Qv is malformed, expected [<addr>]:<port> or [<addr>] format",
+ address);
+ }
+
+ auto colonIndex = address.find(':', closingBracketIndex + 1);
+ if (colonIndex != TString::npos) {
+ try {
+ port = FromString<int>(address.substr(colonIndex + 1));
+ } catch (const std::exception) {
+ return TError("Port number in address %Qv is malformed",
+ address);
+ }
+ }
+
+ ipAddress = TString(address.substr(1, closingBracketIndex - 1));
+ } else {
+ if (address.find('.') != TString::npos) {
+ auto colonIndex = address.find(':', closingBracketIndex + 1);
+ if (colonIndex != TString::npos) {
+ try {
+ port = FromString<int>(address.substr(colonIndex + 1));
+ ipAddress = TString(address.substr(0, colonIndex));
+ } catch (const std::exception) {
+ return TError("Port number in address %Qv is malformed",
+ address);
+ }
+ }
+ }
+ }
+
+ {
+ // Try to parse as IPv4.
+ struct sockaddr_in sa = {};
+ if (inet_pton(AF_INET, ipAddress.c_str(), &sa.sin_addr) == 1) {
+ if (port) {
+ sa.sin_port = htons(*port);
+ }
+ sa.sin_family = AF_INET;
+ return TNetworkAddress(*reinterpret_cast<sockaddr*>(&sa));
+ }
+ }
+ {
+ // Try to parse as IPv6.
+ struct sockaddr_in6 sa = {};
+ if (inet_pton(AF_INET6, ipAddress.c_str(), &(sa.sin6_addr))) {
+ if (port) {
+ sa.sin6_port = htons(*port);
+ }
+ sa.sin6_family = AF_INET6;
+ return TNetworkAddress(*reinterpret_cast<sockaddr*>(&sa));
+ }
+ }
+
+ return TError("Address %Qv is neither a valid IPv4 nor a valid IPv6 address",
+ ipAddress);
+}
+
+TNetworkAddress TNetworkAddress::CreateIPv6Any(int port)
+{
+ sockaddr_in6 serverAddress = {};
+ serverAddress.sin6_family = AF_INET6;
+ serverAddress.sin6_addr = in6addr_any;
+ serverAddress.sin6_port = htons(port);
+
+ return TNetworkAddress(reinterpret_cast<const sockaddr&>(serverAddress), sizeof(serverAddress));
+}
+
+TNetworkAddress TNetworkAddress::CreateIPv6Loopback(int port)
+{
+ sockaddr_in6 serverAddress = {};
+ serverAddress.sin6_family = AF_INET6;
+ serverAddress.sin6_addr = in6addr_loopback;
+ serverAddress.sin6_port = htons(port);
+
+ return TNetworkAddress(reinterpret_cast<const sockaddr&>(serverAddress), sizeof(serverAddress));
+}
+
+TNetworkAddress TNetworkAddress::CreateUnixDomainSocketAddress(const TString& socketPath)
+{
+#ifdef _linux_
+ // Abstract unix sockets are supported only on Linux.
+ sockaddr_un sockAddr = {};
+ if (socketPath.size() > sizeof(sockAddr.sun_path)) {
+ THROW_ERROR_EXCEPTION("Unix domain socket path is too long")
+ << TErrorAttribute("socket_path", socketPath)
+ << TErrorAttribute("max_socket_path_length", sizeof(sockAddr.sun_path));
+ }
+
+ sockAddr.sun_family = AF_UNIX;
+ memcpy(sockAddr.sun_path, socketPath.data(), socketPath.length());
+ return TNetworkAddress(
+ *reinterpret_cast<sockaddr*>(&sockAddr),
+ sizeof(sockAddr.sun_family) +
+ socketPath.length());
+#else
+ Y_UNUSED(socketPath);
+ YT_ABORT();
+#endif
+}
+
+TNetworkAddress TNetworkAddress::CreateAbstractUnixDomainSocketAddress(const TString& socketName)
+{
+ return CreateUnixDomainSocketAddress(TString("\0", 1) + socketName);
+}
+
+TNetworkAddress TNetworkAddress::Parse(TStringBuf address)
+{
+ return TryParse(address).ValueOrThrow();
+}
+
+void ToProto(TString* protoAddress, const TNetworkAddress& address)
+{
+ *protoAddress = TString(reinterpret_cast<const char*>(&address.Storage_), address.Length_);
+}
+
+void FromProto(TNetworkAddress* address, const TString& protoAddress)
+{
+ if (protoAddress.size() > sizeof(address->Storage_)) {
+ THROW_ERROR_EXCEPTION("Network address size is too big")
+ << TErrorAttribute("size", protoAddress.size());
+ }
+ address->Storage_ = {};
+ memcpy(&address->Storage_, protoAddress.data(), protoAddress.size());
+ address->Length_ = protoAddress.size();
+}
+
+TString ToString(const TNetworkAddress& address, const TNetworkAddressFormatOptions& options)
+{
+ const auto& sockAddr = address.GetSockAddr();
+
+ const void* ipAddr;
+ int port = 0;
+ bool ipv6 = false;
+ switch (sockAddr->sa_family) {
+#ifdef _unix_
+ case AF_UNIX: {
+ const auto* typedAddr = reinterpret_cast<const sockaddr_un*>(sockAddr);
+ //! See `man unix`.
+ if (address.GetLength() == sizeof(sa_family_t)) {
+ return "unix://[*unnamed*]";
+ } else if (typedAddr->sun_path[0] == 0) {
+ auto addressRef = TStringBuf(typedAddr->sun_path + 1, address.GetLength() - 1 - sizeof(sa_family_t));
+ auto quoted = Format("%Qv", addressRef);
+ return Format("unix://[%v]", quoted.substr(1, quoted.size() - 2));
+ } else {
+ auto addressRef = TString(typedAddr->sun_path, address.GetLength() - sizeof(sa_family_t));
+ return Format("unix://%v", NFS::GetRealPath(addressRef));
+ }
+ }
+#endif
+ case AF_INET: {
+ const auto* typedAddr = reinterpret_cast<const sockaddr_in*>(sockAddr);
+ ipAddr = &typedAddr->sin_addr;
+ port = typedAddr->sin_port;
+ ipv6 = false;
+ break;
+ }
+ case AF_INET6: {
+ const auto* typedAddr = reinterpret_cast<const sockaddr_in6*>(sockAddr);
+ ipAddr = &typedAddr->sin6_addr;
+ port = typedAddr->sin6_port;
+ ipv6 = true;
+ break;
+ }
+ default:
+ return Format("unknown://family(%v)", sockAddr->sa_family);
+ }
+
+ std::array<char, std::max(INET6_ADDRSTRLEN, INET_ADDRSTRLEN)> buffer;
+ YT_VERIFY(inet_ntop(
+ sockAddr->sa_family,
+ const_cast<void*>(ipAddr),
+ buffer.data(),
+ buffer.size()));
+
+ TStringBuilder result;
+ if (options.IncludeTcpProtocol) {
+ result.AppendString(TStringBuf("tcp://"));
+ }
+
+ bool withBrackets = ipv6 && (options.IncludeTcpProtocol || options.IncludePort);
+ if (withBrackets) {
+ result.AppendChar('[');
+ }
+
+ result.AppendString(buffer.data());
+
+ if (withBrackets) {
+ result.AppendChar(']');
+ }
+
+ if (options.IncludePort) {
+ result.AppendFormat(":%v", ntohs(port));
+ }
+
+ return result.Flush();
+}
+
+bool operator == (const TNetworkAddress& lhs, const TNetworkAddress& rhs)
+{
+ auto lhsAddr = lhs.GetSockAddr();
+ auto lhsSize = lhs.GetLength();
+
+ auto rhsAddr = rhs.GetSockAddr();
+ auto rhsSize = rhs.GetLength();
+
+ TStringBuf rawLhs{reinterpret_cast<const char*>(lhsAddr), static_cast<size_t>(lhsSize)};
+ TStringBuf rawRhs{reinterpret_cast<const char*>(rhsAddr), static_cast<size_t>(rhsSize)};
+
+ return rawLhs == rawRhs;
+}
+
+bool operator != (const TNetworkAddress& lhs, const TNetworkAddress& rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+//! Parse project id notation as described here:
+//! https://wiki.yandex-team.ru/noc/newnetwork/hbf/projectid/#faervolnajazapissetevogoprefiksasprojectidirabotasnim
+bool ParseProjectId(TStringBuf* str, std::optional<ui32>* projectId)
+{
+ auto pos = str->find('@');
+ if (pos == TStringBuf::npos) {
+ // Project id not specified.
+ return true;
+ }
+
+ if (pos == 0 || pos > 8) {
+ // Project id occupies 32 bits of address, so it must be between 1 and 8 hex digits.
+ return false;
+ }
+
+ ui32 value = 0;
+ for (int i = 0; i < static_cast<int>(pos); ++i) {
+ int digit = Char2DigitTable[static_cast<unsigned char>((*str)[i])];
+ if (digit == '\xff') {
+ return false;
+ }
+
+ value <<= 4;
+ value += digit;
+ }
+
+ *projectId = value;
+ str->Skip(pos + 1);
+ return true;
+}
+
+bool ParseIP6Address(TStringBuf* str, TIP6Address* address)
+{
+ auto tokenizeWord = [&] (ui16* word) -> bool {
+ int partLen = 0;
+ ui16 wordValue = 0;
+
+ if (str->empty()) {
+ return false;
+ }
+
+ while (partLen < 4 && !str->empty()) {
+ int digit = Char2DigitTable[static_cast<unsigned char>((*str)[0])];
+ if (digit == '\xff' && partLen == 0) {
+ return false;
+ }
+ if (digit == '\xff') {
+ break;
+ }
+
+ str->Skip(1);
+ wordValue <<= 4;
+ wordValue += digit;
+ ++partLen;
+ }
+
+ *word = wordValue;
+ return true;
+ };
+
+ bool beforeAbbrev = true;
+ int wordIndex = 0;
+ int wordsPushed = 0;
+
+ auto words = address->GetRawWords();
+ std::fill_n(address->GetRawBytes(), TIP6Address::ByteSize, 0);
+
+ auto isEnd = [&] () {
+ return str->empty() || (*str)[0] == '/';
+ };
+
+ auto tokenizeAbbrev = [&] () {
+ if (str->size() >= 2 && (*str)[0] == ':' && (*str)[1] == ':') {
+ str->Skip(2);
+ return true;
+ }
+ return false;
+ };
+
+ if (tokenizeAbbrev()) {
+ beforeAbbrev = false;
+ ++wordIndex;
+ }
+
+ if (isEnd() && !beforeAbbrev) {
+ return true;
+ }
+
+ while (true) {
+ if (beforeAbbrev) {
+ ui16 newWord = 0;
+ if (!tokenizeWord(&newWord)) {
+ return false;
+ }
+
+ words[7 - wordIndex] = newWord;
+ ++wordIndex;
+ } else {
+ ui16 newWord = 0;
+ if (!tokenizeWord(&newWord)) {
+ return false;
+ }
+
+ std::copy_backward(words, words + wordsPushed, words + wordsPushed + 1);
+ words[0] = newWord;
+ ++wordsPushed;
+ }
+
+ // end of full address
+ if (wordIndex + wordsPushed == 8) {
+ break;
+ }
+
+ // end of abbreviated address
+ if (isEnd() && !beforeAbbrev) {
+ break;
+ }
+
+ // ':' or '::'
+ if (beforeAbbrev && tokenizeAbbrev()) {
+ beforeAbbrev = false;
+ ++wordIndex;
+
+ if (isEnd()) {
+ break;
+ }
+ } else if (!str->empty() && (*str)[0] == ':') {
+ str->Skip(1);
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ParseMask(TStringBuf buf, int* maskSize)
+{
+ if (buf.size() < 2 || buf[0] != '/') {
+ return false;
+ }
+
+ *maskSize = 0;
+ for (int i = 1; i < 4; ++i) {
+ if (i == std::ssize(buf)) {
+ return true;
+ }
+
+ if (buf[i] < '0' || '9' < buf[i]) {
+ return false;
+ }
+
+ *maskSize = (*maskSize * 10) + (buf[i] - '0');
+ }
+
+ return buf.size() == 4 && *maskSize <= 128;
+}
+
+} // namespace
+
+const ui8* TIP6Address::GetRawBytes() const
+{
+ return Raw_.data();
+}
+
+ui8* TIP6Address::GetRawBytes()
+{
+ return Raw_.data();
+}
+
+const ui16* TIP6Address::GetRawWords() const
+{
+ return reinterpret_cast<const ui16*>(GetRawBytes());
+}
+
+ui16* TIP6Address::GetRawWords()
+{
+ return reinterpret_cast<ui16*>(GetRawBytes());
+}
+
+const ui32* TIP6Address::GetRawDWords() const
+{
+ return reinterpret_cast<const ui32*>(GetRawBytes());
+}
+
+ui32* TIP6Address::GetRawDWords()
+{
+ return reinterpret_cast<ui32*>(GetRawBytes());
+}
+
+TIP6Address TIP6Address::FromRawBytes(const ui8* raw)
+{
+ TIP6Address result;
+ ::memcpy(result.Raw_.data(), raw, ByteSize);
+ return result;
+}
+
+TIP6Address TIP6Address::FromRawWords(const ui16* raw)
+{
+ return FromRawBytes(reinterpret_cast<const ui8*>(raw));
+}
+
+TIP6Address TIP6Address::FromRawDWords(const ui32* raw)
+{
+ return FromRawBytes(reinterpret_cast<const ui8*>(raw));
+}
+
+TIP6Address TIP6Address::FromString(TStringBuf str)
+{
+ TIP6Address result;
+ if (!FromString(str, &result)) {
+ THROW_ERROR_EXCEPTION("Error parsing IP6 address %Qv", str);
+ }
+ return result;
+}
+
+bool TIP6Address::FromString(TStringBuf str, TIP6Address* address)
+{
+ TStringBuf buf = str;
+ if (!ParseIP6Address(&buf, address) || !buf.empty()) {
+ return false;
+ }
+ return true;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TIP6Address& address, TStringBuf /*spec*/)
+{
+ const auto* parts = reinterpret_cast<const ui16*>(address.GetRawBytes());
+ std::pair<int, int> maxRun = {-1, -1};
+ int start = -1;
+ int end = -1;
+ auto endRun = [&] () {
+ if ((end - start) >= (maxRun.second - maxRun.first) && (end - start) > 1) {
+ maxRun = {start, end};
+ }
+
+ start = -1;
+ end = -1;
+ };
+
+ for (int index = 0; index < 8; ++index) {
+ if (parts[index] == 0) {
+ if (start == -1) {
+ start = index;
+ }
+ end = index + 1;
+ } else {
+ endRun();
+ }
+ }
+ endRun();
+
+ for (int index = 7; index >= 0; --index) {
+ if (maxRun.first <= index && index < maxRun.second) {
+ if (index == maxRun.first) {
+ builder->AppendChar(':');
+ builder->AppendChar(':');
+ }
+ } else {
+ if (index != 7 && index + 1 != maxRun.first) {
+ builder->AppendChar(':');
+ }
+ builder->AppendFormat("%x", parts[index]);
+ }
+ }
+}
+
+TString ToString(const TIP6Address& address)
+{
+ return ToStringViaBuilder(address);
+}
+
+bool operator == (const TIP6Address& lhs, const TIP6Address& rhs)
+{
+ return ::memcmp(lhs.GetRawBytes(), rhs.GetRawBytes(), TIP6Address::ByteSize) == 0;
+}
+
+bool operator != (const TIP6Address& lhs, const TIP6Address& rhs)
+{
+ return !(lhs == rhs);
+}
+
+TIP6Address operator|(const TIP6Address& lhs, const TIP6Address& rhs)
+{
+ auto result = lhs;
+ result |= rhs;
+ return result;
+}
+
+TIP6Address operator&(const TIP6Address& lhs, const TIP6Address& rhs)
+{
+ auto result = lhs;
+ result &= rhs;
+ return result;
+}
+
+TIP6Address& operator|=(TIP6Address& lhs, const TIP6Address& rhs)
+{
+ *reinterpret_cast<ui64*>(lhs.GetRawBytes()) |= *reinterpret_cast<const ui64*>(rhs.GetRawBytes());
+ *reinterpret_cast<ui64*>(lhs.GetRawBytes() + 8) |= *reinterpret_cast<const ui64*>(rhs.GetRawBytes() + 8);
+ return lhs;
+}
+
+TIP6Address& operator&=(TIP6Address& lhs, const TIP6Address& rhs)
+{
+ *reinterpret_cast<ui64*>(lhs.GetRawBytes()) &= *reinterpret_cast<const ui64*>(rhs.GetRawBytes());
+ *reinterpret_cast<ui64*>(lhs.GetRawBytes() + 8) &= *reinterpret_cast<const ui64*>(rhs.GetRawBytes() + 8);
+ return lhs;
+}
+
+void Deserialize(TIP6Address& value, INodePtr node)
+{
+ value = TIP6Address::FromString(node->AsString()->GetValue());
+}
+
+void Deserialize(TIP6Address& value, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("TIP6Address", *cursor, EYsonItemType::StringValue);
+ value = TIP6Address::FromString((*cursor)->UncheckedAsString());
+ cursor->Next();
+}
+
+void Serialize(const TIP6Address& value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(ToString(value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TIP6Network::TIP6Network(const TIP6Address& network, const TIP6Address& mask)
+ : Network_(network)
+ , Mask_(mask)
+{ }
+
+const TIP6Address& TIP6Network::GetAddress() const
+{
+ return Network_;
+}
+
+const TIP6Address& TIP6Network::GetMask() const
+{
+ return Mask_;
+}
+
+int TIP6Network::GetMaskSize() const
+{
+ int size = 0;
+ const auto* parts = Mask_.GetRawDWords();
+ for (size_t partIndex = 0; partIndex < 4; ++partIndex) {
+ size += __builtin_popcount(parts[partIndex]);
+ }
+ return size;
+}
+
+bool TIP6Network::Contains(const TIP6Address& address) const
+{
+ TIP6Address masked = address;
+ masked &= Mask_;
+ return masked == Network_;
+}
+
+TIP6Network TIP6Network::FromString(TStringBuf str)
+{
+ TIP6Network network;
+ if (!FromString(str, &network)) {
+ THROW_ERROR_EXCEPTION("Error parsing IP6 network %Qv", str);
+ }
+ return network;
+}
+
+bool TIP6Network::FromString(TStringBuf str, TIP6Network* network)
+{
+ auto buf = str;
+ std::optional<ui32> projectId = std::nullopt;
+ if (!ParseProjectId(&buf, &projectId)) {
+ return false;
+ }
+
+ if (!ParseIP6Address(&buf, &network->Network_)) {
+ return false;
+ }
+
+ int maskSize = 0;
+ if (!ParseMask(buf, &maskSize)) {
+ return false;
+ }
+
+ if (projectId) {
+ network->Network_.GetRawWords()[2] = *projectId;
+ network->Network_.GetRawWords()[3] = *projectId >> 16;
+ }
+
+ network->Mask_ = TIP6Address();
+ auto bytes = network->Mask_.GetRawBytes();
+ for (int i = 0; i < static_cast<int>(TIP6Address::ByteSize * 8); ++i) {
+ if (i >= static_cast<int>(TIP6Address::ByteSize * 8) - maskSize) {
+ *(bytes + i / 8) |= (1 << (i % 8));
+ } else {
+ *(bytes + i / 8) &= ~(1 << (i % 8));
+ }
+ }
+
+ if (projectId) {
+ static_assert(TIP6Address::ByteSize == 16);
+ network->Mask_.GetRawDWords()[1] = 0xffffffff;
+ }
+
+ return true;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TIP6Network& network, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v/%v",
+ network.GetAddress(),
+ network.GetMaskSize());
+}
+
+TString ToString(const TIP6Network& network)
+{
+ return ToStringViaBuilder(network);
+}
+
+void Deserialize(TIP6Network& value, INodePtr node)
+{
+ value = TIP6Network::FromString(node->AsString()->GetValue());
+}
+
+void Deserialize(TIP6Network& value, NYson::TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("TIP6Network", *cursor, EYsonItemType::StringValue);
+ value = TIP6Network::FromString((*cursor)->UncheckedAsString());
+ cursor->Next();
+}
+
+void Serialize(const TIP6Network& value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(ToString(value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Performs asynchronous host name resolution.
+class TAddressResolver::TImpl
+ : public virtual TRefCounted
+ , private TAsyncExpiringCache<TString, TNetworkAddress>
+{
+public:
+ explicit TImpl(TAddressResolverConfigPtr config)
+ : TAsyncExpiringCache(
+ config,
+ /*logger*/ {},
+ DnsProfiler.WithPrefix("/resolve_cache"))
+ {
+ SetDnsResolver(CreateAresDnsResolver(
+ config->Retries,
+ config->ResolveTimeout,
+ config->MaxResolveTimeout,
+ config->WarningTimeout,
+ config->Jitter));
+ Configure(std::move(config));
+ }
+
+ TFuture<TNetworkAddress> Resolve(const TString& hostName)
+ {
+ // Check if |address| parses into a valid IPv4 or IPv6 address.
+ if (auto result = TNetworkAddress::TryParse(hostName); result.IsOK()) {
+ return MakeFuture(result);
+ }
+
+ // Run async resolution.
+ return Get(hostName);
+ }
+
+ IDnsResolverPtr GetDnsResolver()
+ {
+ return DnsResolver_.Acquire();
+ }
+
+ void SetDnsResolver(IDnsResolverPtr dnsResolver)
+ {
+ DnsResolver_.Store(std::move(dnsResolver));
+ }
+
+ void EnsureLocalHostName()
+ {
+ if (Config_->LocalHostNameOverride) {
+ return;
+ }
+
+ UpdateLocalHostName(Config_);
+
+ YT_LOG_INFO("Localhost name determined via system call (LocalHostName: %v, ResolveHostNameIntoFqdn: %v)",
+ GetLocalHostName(),
+ Config_->ResolveHostNameIntoFqdn);
+ }
+
+ bool IsLocalAddress(const TNetworkAddress& address)
+ {
+ TNetworkAddress localIP{address, 0};
+
+ const auto& localAddresses = GetLocalAddresses();
+ auto&& it = std::find(localAddresses.begin(), localAddresses.end(), localIP);
+ auto jt = localAddresses.end();
+ return it != jt;
+ }
+
+ void PurgeCache()
+ {
+ Clear();
+ YT_LOG_INFO("Address cache purged");
+ }
+
+ void Configure(TAddressResolverConfigPtr config)
+ {
+ Config_ = std::move(config);
+
+ if (Config_->LocalHostNameOverride) {
+ WriteLocalHostName(*Config_->LocalHostNameOverride);
+ YT_LOG_INFO("Localhost name configured via config override (LocalHostName: %v)",
+ Config_->LocalHostNameOverride);
+ }
+
+ UpdateLoopbackAddress(Config_);
+ }
+
+private:
+ TAddressResolverConfigPtr Config_;
+
+ std::atomic<bool> HasCachedLocalAddresses_ = false;
+ std::vector<TNetworkAddress> CachedLocalAddresses_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, CacheLock_);
+
+ const TActionQueuePtr Queue_ = New<TActionQueue>("AddressResolver");
+
+ TAtomicIntrusivePtr<IDnsResolver> DnsResolver_;
+
+ TFuture<TNetworkAddress> DoGet(const TString& hostName, bool /*isPeriodicUpdate*/) noexcept override
+ {
+ TDnsResolveOptions options{
+ .EnableIPv4 = Config_->EnableIPv4,
+ .EnableIPv6 = Config_->EnableIPv6,
+ };
+ return GetDnsResolver()->Resolve(hostName, options)
+ .Apply(BIND([=] (const TErrorOr<TNetworkAddress>& result) {
+ // Empty callback just to forward future callbacks into proper thread.
+ return result.ValueOrThrow();
+ })
+ .AsyncVia(Queue_->GetInvoker()));
+ }
+
+ const std::vector<TNetworkAddress>& GetLocalAddresses()
+ {
+ if (HasCachedLocalAddresses_) {
+ return CachedLocalAddresses_;
+ }
+
+ std::vector<TNetworkAddress> localAddresses;
+ for (const auto& interface : NAddr::GetNetworkInterfaces()) {
+ localAddresses.push_back(TNetworkAddress(*interface.Address->Addr()));
+ }
+
+ {
+ auto guard = WriterGuard(CacheLock_);
+ // NB: Only update CachedLocalAddresses_ once.
+ if (!HasCachedLocalAddresses_) {
+ CachedLocalAddresses_ = std::move(localAddresses);
+ HasCachedLocalAddresses_ = true;
+ }
+ }
+
+ return CachedLocalAddresses_;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAddressResolver::TAddressResolver()
+ : Impl_(New<TImpl>(New<TAddressResolverConfig>()))
+{ }
+
+TAddressResolver* TAddressResolver::Get()
+{
+ return LeakySingleton<TAddressResolver>();
+}
+
+TFuture<TNetworkAddress> TAddressResolver::Resolve(const TString& address)
+{
+ return Impl_->Resolve(address);
+}
+
+IDnsResolverPtr TAddressResolver::GetDnsResolver()
+{
+ return Impl_->GetDnsResolver();
+}
+
+void TAddressResolver::SetDnsResolver(IDnsResolverPtr dnsResolver)
+{
+ Impl_->SetDnsResolver(std::move(dnsResolver));
+}
+
+void TAddressResolver::EnsureLocalHostName()
+{
+ return Impl_->EnsureLocalHostName();
+}
+
+bool TAddressResolver::IsLocalAddress(const TNetworkAddress& address)
+{
+ return Impl_->IsLocalAddress(address);
+}
+
+void TAddressResolver::PurgeCache()
+{
+ return Impl_->PurgeCache();
+}
+
+void TAddressResolver::Configure(TAddressResolverConfigPtr config)
+{
+ return Impl_->Configure(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMtnAddress::TMtnAddress(TIP6Address address)
+ : Address_(address)
+{ }
+
+ui64 TMtnAddress::GetPrefix() const
+{
+ return GetBytesRangeValue(PrefixOffsetInBytes, TotalLenInBytes);
+}
+
+TMtnAddress& TMtnAddress::SetPrefix(ui64 prefix)
+{
+ SetBytesRangeValue(PrefixOffsetInBytes, TotalLenInBytes, prefix);
+ return *this;
+}
+
+ui64 TMtnAddress::GetGeo() const
+{
+ return GetBytesRangeValue(GeoOffsetInBytes, PrefixOffsetInBytes);
+}
+
+TMtnAddress& TMtnAddress::SetGeo(ui64 geo)
+{
+ SetBytesRangeValue(GeoOffsetInBytes, PrefixOffsetInBytes, geo);
+ return *this;
+}
+
+ui64 TMtnAddress::GetProjectId() const
+{
+ return GetBytesRangeValue(ProjectIdOffsetInBytes, GeoOffsetInBytes);
+}
+
+TMtnAddress& TMtnAddress::SetProjectId(ui64 projectId)
+{
+ SetBytesRangeValue(ProjectIdOffsetInBytes, GeoOffsetInBytes, projectId);
+ return *this;
+}
+
+ui64 TMtnAddress::GetHost() const
+{
+ return GetBytesRangeValue(HostOffsetInBytes, ProjectIdOffsetInBytes);
+}
+
+TMtnAddress& TMtnAddress::SetHost(ui64 host)
+{
+ SetBytesRangeValue(HostOffsetInBytes, ProjectIdOffsetInBytes, host);
+ return *this;
+}
+
+const TIP6Address& TMtnAddress::ToIP6Address() const
+{
+ return Address_;
+}
+
+ui64 TMtnAddress::GetBytesRangeValue(int leftIndex, int rightIndex) const
+{
+ if (leftIndex > rightIndex) {
+ THROW_ERROR_EXCEPTION("Left index is greater than right index (LeftIndex: %v, RightIndex: %v)",
+ leftIndex,
+ rightIndex);
+ }
+
+ const auto* addressBytes = Address_.GetRawBytes();
+
+ ui64 result = 0;
+ for (int index = rightIndex - 1; index >= leftIndex; --index) {
+ result = (result << 8) | addressBytes[index];
+ }
+ return result;
+}
+
+void TMtnAddress::SetBytesRangeValue(int leftIndex, int rightIndex, ui64 value)
+{
+ if (leftIndex > rightIndex) {
+ THROW_ERROR_EXCEPTION("Left index is greater than right index (LeftIndex: %v, RightIndex: %v)",
+ leftIndex,
+ rightIndex);
+ }
+
+ auto bytesInRange = rightIndex - leftIndex;
+ if (value >= (1ull << (8 * bytesInRange))) {
+ THROW_ERROR_EXCEPTION("Value is too large to be set in [leftIndex; rightIndex) interval (LeftIndex: %v, RightIndex: %v, Value %v)",
+ leftIndex,
+ rightIndex,
+ value);
+ }
+
+ auto* addressBytes = Address_.GetRawBytes();
+ for (int index = leftIndex; index < rightIndex; ++index) {
+ addressBytes[index] = value & 255;
+ value >>= 8;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr size_t MaxYPClusterNameSize = 32;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+std::optional<TStringBuf> InferYPClusterFromHostNameRaw(TStringBuf hostName)
+{
+ auto start = hostName.find_first_of('.');
+ if (start == TStringBuf::npos) {
+ return {};
+ }
+ auto end = hostName.find_first_of('.', start + 1);
+ if (end == TStringBuf::npos) {
+ return {};
+ }
+ auto cluster = hostName.substr(start + 1, end - start - 1);
+ if (cluster.empty()) {
+ return {};
+ }
+ if (cluster.length() > MaxYPClusterNameSize) {
+ return {};
+ }
+ return {cluster};
+}
+
+std::optional<TString> InferYPClusterFromHostName(TStringBuf hostName)
+{
+ if (auto rawResult = InferYPClusterFromHostNameRaw(hostName)) {
+ return TString{*rawResult};
+ }
+ return {};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TStringBuf> InferYTClusterFromClusterUrlRaw(TStringBuf clusterUrl)
+{
+ clusterUrl.SkipPrefix("http://");
+ clusterUrl.ChopSuffix(".yt.yandex.net");
+
+ if (clusterUrl.find("localhost") != TStringBuf::npos || clusterUrl.find_first_of(".:/") != TStringBuf::npos) {
+ return {};
+ }
+
+ return clusterUrl;
+}
+
+std::optional<TString> InferYTClusterFromClusterUrl(TStringBuf clusterUrl)
+{
+ if (auto rawResult = InferYTClusterFromClusterUrlRaw(clusterUrl)) {
+ return TString{*rawResult};
+ }
+ return {};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
+
+size_t THash<NYT::NNet::TNetworkAddress>::operator()(const NYT::NNet::TNetworkAddress& address) const
+{
+ TStringBuf rawAddress{
+ reinterpret_cast<const char*>(address.GetSockAddr()),
+ static_cast<size_t>(address.GetLength())};
+ return ComputeHash(rawAddress);
+}
diff --git a/yt/yt/core/net/address.h b/yt/yt/core/net/address.h
new file mode 100644
index 0000000000..6d28a01efd
--- /dev/null
+++ b/yt/yt/core/net/address.h
@@ -0,0 +1,279 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/dns/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <util/generic/hash.h>
+
+#ifdef _WIN32
+ #include <ws2tcpip.h>
+#else
+ #include <sys/socket.h>
+#endif
+
+#include <array>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Constructs an address of the form |hostName:port|.
+TString BuildServiceAddress(TStringBuf hostName, int port);
+
+//! Parses service address into host name and port number.
+//! Both #hostName and #port can be |NULL|.
+//! Throws if the address is malformed.
+void ParseServiceAddress(
+ TStringBuf address,
+ TStringBuf* hostName,
+ int* port);
+
+//! Extracts port number from a service address.
+//! Throws if the address is malformed.
+int GetServicePort(TStringBuf address);
+
+//! Extracts host name from a service address.
+TStringBuf GetServiceHostName(TStringBuf address);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIP6Address;
+
+//! An opaque wrapper for |sockaddr| type.
+class TNetworkAddress
+{
+public:
+ TNetworkAddress();
+ TNetworkAddress(const TNetworkAddress& other, int port);
+ explicit TNetworkAddress(const sockaddr& other, socklen_t length = 0);
+ TNetworkAddress(int family, const char* addr, size_t size);
+
+ sockaddr* GetSockAddr();
+ const sockaddr* GetSockAddr() const;
+ socklen_t GetLength() const;
+ socklen_t* GetLengthPtr();
+ int GetPort() const;
+
+ bool IsUnix() const;
+ bool IsIP() const;
+ bool IsIP4() const;
+ bool IsIP6() const;
+
+ static TErrorOr<TNetworkAddress> TryParse(TStringBuf address);
+ static TNetworkAddress Parse(TStringBuf address);
+
+ static TNetworkAddress CreateIPv6Any(int port);
+ static TNetworkAddress CreateIPv6Loopback(int port);
+ static TNetworkAddress CreateUnixDomainSocketAddress(const TString& socketPath);
+ static TNetworkAddress CreateAbstractUnixDomainSocketAddress(const TString& socketName);
+
+ TIP6Address ToIP6Address() const;
+
+private:
+ sockaddr_storage Storage_;
+ socklen_t Length_;
+
+ static socklen_t GetGenericLength(const sockaddr& sockAddr);
+
+ friend void ToProto(TString* protoAddress, const TNetworkAddress& address);
+ friend void FromProto(TNetworkAddress* address, const TString& protoAddress);
+};
+
+extern const TNetworkAddress NullNetworkAddress;
+
+struct TNetworkAddressFormatOptions
+{
+ bool IncludePort = true;
+ bool IncludeTcpProtocol = true;
+};
+
+TString ToString(const TNetworkAddress& address, const TNetworkAddressFormatOptions& options = {});
+
+bool operator == (const TNetworkAddress& lhs, const TNetworkAddress& rhs);
+bool operator != (const TNetworkAddress& lhs, const TNetworkAddress& rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIP6Address
+{
+public:
+ static constexpr size_t ByteSize = 16;
+
+ TIP6Address() = default;
+
+ static TIP6Address FromString(TStringBuf str);
+ static bool FromString(TStringBuf str, TIP6Address* address);
+
+ static TIP6Address FromRawBytes(const ui8* raw);
+ static TIP6Address FromRawWords(const ui16* raw);
+ static TIP6Address FromRawDWords(const ui32* raw);
+
+ const ui8* GetRawBytes() const;
+ ui8* GetRawBytes();
+
+ const ui16* GetRawWords() const;
+ ui16* GetRawWords();
+
+ const ui32* GetRawDWords() const;
+ ui32* GetRawDWords();
+
+private:
+ std::array<ui8, ByteSize> Raw_ = {};
+};
+
+void FormatValue(TStringBuilderBase* builder, const TIP6Address& address, TStringBuf spec);
+TString ToString(const TIP6Address& address);
+
+bool operator == (const TIP6Address& lhs, const TIP6Address& rhs);
+bool operator != (const TIP6Address& lhs, const TIP6Address& rhs);
+
+TIP6Address operator & (const TIP6Address& lhs, const TIP6Address& rhs);
+TIP6Address operator | (const TIP6Address& lhs, const TIP6Address& rhs);
+TIP6Address& operator &= (TIP6Address& lhs, const TIP6Address& rhs);
+TIP6Address& operator |= (TIP6Address& lhs, const TIP6Address& rhs);
+
+void Deserialize(TIP6Address& value, NYTree::INodePtr node);
+void Deserialize(TIP6Address& value, NYson::TYsonPullParserCursor* cursor);
+void Serialize(const TIP6Address& value, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TIP6Network
+{
+public:
+ TIP6Network() = default;
+ TIP6Network(const TIP6Address& network, const TIP6Address& mask);
+
+ static TIP6Network FromString(TStringBuf str);
+ static bool FromString(TStringBuf str, TIP6Network* network);
+
+ bool Contains(const TIP6Address& address) const;
+
+ const TIP6Address& GetAddress() const;
+ const TIP6Address& GetMask() const;
+ int GetMaskSize() const;
+
+private:
+ TIP6Address Network_;
+ TIP6Address Mask_;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TIP6Network& network, TStringBuf spec);
+TString ToString(const TIP6Network& network);
+
+void Deserialize(TIP6Network& value, NYTree::INodePtr node);
+void Deserialize(TIP6Network& value, NYson::TYsonPullParserCursor* cursor);
+void Serialize(const TIP6Network& value, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Performs asynchronous host name resolution.
+class TAddressResolver
+{
+public:
+ //! Returns the singleton instance.
+ static TAddressResolver* Get();
+
+ //! Resolves #address asynchronously.
+ /*!
+ * Calls |getaddrinfo| and returns the first entry belonging to |AF_INET| or |AF_INET6| family.
+ * Caches successful resolutions.
+ */
+ TFuture<TNetworkAddress> Resolve(const TString& address);
+
+ //! Returns the currently installed global DNS resolver.
+ NDns::IDnsResolverPtr GetDnsResolver();
+
+ //! Sets the global DNS resolver.
+ void SetDnsResolver(NDns::IDnsResolverPtr dnsResolver);
+
+ //! Sets localhost name up if it was not provided via |localhost_name_override| config section.
+ //! Depending on |resolve_localhost_into_fqdn| option, localhost name is either additionally resolved
+ //! into FQDN or not.
+ void EnsureLocalHostName();
+
+ //! Returns |true| if #address matches one of local host addresses.
+ bool IsLocalAddress(const TNetworkAddress& address);
+
+ //! Removes all cached resolutions.
+ void PurgeCache();
+
+ //! Updates resolver configuration.
+ void Configure(TAddressResolverConfigPtr config);
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+
+ TAddressResolver();
+
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Class representing IPv6 address in MTN model.
+//! Refer to (https://wiki.yandex-team.ru/noc/newnetwork/hbf/projectid/)
+//! for model description.
+class TMtnAddress
+{
+public:
+ TMtnAddress() = default;
+
+ TMtnAddress(TIP6Address address);
+
+ ui64 GetPrefix() const;
+ TMtnAddress& SetPrefix(ui64 prefix);
+
+ ui64 GetGeo() const;
+ TMtnAddress& SetGeo(ui64 geo);
+
+ ui64 GetProjectId() const;
+ TMtnAddress& SetProjectId(ui64 projectId);
+
+ ui64 GetHost() const;
+ TMtnAddress& SetHost(ui64 host);
+
+ const TIP6Address& ToIP6Address() const;
+
+private:
+ ui64 GetBytesRangeValue(int leftIndex, int rightIndex) const;
+
+ void SetBytesRangeValue(int leftIndex, int rightIndex, ui64 value);
+
+ static constexpr int HostOffsetInBytes = 0;
+ static constexpr int ProjectIdOffsetInBytes = 4;
+ static constexpr int GeoOffsetInBytes = 8;
+ static constexpr int PrefixOffsetInBytes = 11;
+ static constexpr int TotalLenInBytes = 16;
+
+ TIP6Address Address_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Expected format: sas1-5535-9d7.sas-test.yp.gencfg-c.yandex.net, or noqpmfiudzbb4hvs.man.yp-c.yandex.net.
+// YP pod id must not contain a '.' in its name.
+std::optional<TStringBuf> InferYPClusterFromHostNameRaw(TStringBuf hostName);
+std::optional<TString> InferYPClusterFromHostName(TStringBuf hostName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TStringBuf> InferYTClusterFromClusterUrlRaw(TStringBuf clusterUrl);
+std::optional<TString> InferYTClusterFromClusterUrl(TStringBuf clusterUrl);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
+
+template <>
+struct THash<NYT::NNet::TNetworkAddress>
+{
+ size_t operator()(const NYT::NNet::TNetworkAddress& address) const;
+};
diff --git a/yt/yt/core/net/config.cpp b/yt/yt/core/net/config.cpp
new file mode 100644
index 0000000000..21429eccb7
--- /dev/null
+++ b/yt/yt/core/net/config.cpp
@@ -0,0 +1,65 @@
+#include "config.h"
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDialerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_no_delay", &TThis::EnableNoDelay)
+ .Default(true);
+ registrar.Parameter("enable_aggressive_reconnect", &TThis::EnableAggressiveReconnect)
+ .Default(false);
+ registrar.Parameter("min_rto", &TThis::MinRto)
+ .Default(TDuration::MilliSeconds(100));
+ registrar.Parameter("max_rto", &TThis::MaxRto)
+ .Default(TDuration::Seconds(30));
+ registrar.Parameter("rto_scale", &TThis::RtoScale)
+ .GreaterThan(0.0)
+ .Default(2.0);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MaxRto < config->MinRto) {
+ THROW_ERROR_EXCEPTION("\"max_rto\" should be greater than or equal to \"min_rto\"");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAddressResolverConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_ipv4", &TThis::EnableIPv4)
+ .Default(false);
+ registrar.Parameter("enable_ipv6", &TThis::EnableIPv6)
+ .Default(true);
+ registrar.Parameter("localhost_name_override", &TThis::LocalHostNameOverride)
+ .Alias("localhost_fqdn")
+ .Default();
+ registrar.Parameter("resolve_hostname_into_fqdn", &TThis::ResolveHostNameIntoFqdn)
+ .Default(true);
+ registrar.Parameter("retries", &TThis::Retries)
+ .Default(25);
+ registrar.Parameter("retry_delay", &TThis::RetryDelay)
+ .Default(TDuration::MilliSeconds(200));
+ registrar.Parameter("resolve_timeout", &TThis::ResolveTimeout)
+ .Default(TDuration::Seconds(1));
+ registrar.Parameter("max_resolve_timeout", &TThis::MaxResolveTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("warning_timeout", &TThis::WarningTimeout)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("jitter", &TThis::Jitter)
+ .Default(0.5);
+ registrar.Parameter("expected_localhost_name", &TThis::ExpectedLocalHostName)
+ .Default();
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->RefreshTime = TDuration::Seconds(60);
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::Seconds(120);
+ config->ExpireAfterFailedUpdateTime = TDuration::Seconds(30);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/config.h b/yt/yt/core/net/config.h
new file mode 100644
index 0000000000..976fd1d38b
--- /dev/null
+++ b/yt/yt/core/net/config.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/core/misc/cache_config.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDialerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool EnableNoDelay;
+ bool EnableAggressiveReconnect;
+
+ TDuration MinRto;
+ TDuration MaxRto;
+ double RtoScale;
+
+ REGISTER_YSON_STRUCT(TDialerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDialerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Configuration for TAddressResolver singleton.
+class TAddressResolverConfig
+ : public TAsyncExpiringCacheConfig
+{
+public:
+ bool EnableIPv4;
+ bool EnableIPv6;
+ //! If true, when determining local host name, it will additionally be resolved
+ //! into FQDN by calling |getaddrinfo|. Setting this option to false may be
+ //! useful in MTN environment, in which hostnames are barely resolvable.
+ //! NB: Set this option to false only if you are sure that process is not being
+ //! exposed under localhost name to anyone; in particular, any kind of discovery
+ //! should be done using some other kind of addresses.
+ bool ResolveHostNameIntoFqdn;
+ //! If set, localhost name will be forcefully set to the given value rather
+ //! than retrieved via |NYT::NNet::UpdateLocalHostName|.
+ std::optional<TString> LocalHostNameOverride;
+ int Retries;
+ TDuration RetryDelay;
+ TDuration ResolveTimeout;
+ TDuration MaxResolveTimeout;
+ double Jitter;
+ TDuration WarningTimeout;
+ //! Used to check that bootstrap is being initialized from a correct container.
+ std::optional<TString> ExpectedLocalHostName;
+
+ REGISTER_YSON_STRUCT(TAddressResolverConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TAddressResolverConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/connection.cpp b/yt/yt/core/net/connection.cpp
new file mode 100644
index 0000000000..c0f47f1ead
--- /dev/null
+++ b/yt/yt/core/net/connection.cpp
@@ -0,0 +1,1309 @@
+#include "connection.h"
+#include "packet_connection.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/pollable_detail.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/net/socket.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <util/network/pollerimpl.h>
+
+#include <errno.h>
+
+#ifdef _win_
+ #include <util/network/socket.h>
+ #include <util/network/pair.h>
+
+ #include <winsock2.h>
+
+ #include <sys/uio.h>
+ #include <fcntl.h>
+
+ #define SHUT_RD SD_RECEIVE
+ #define SHUT_WR SD_SEND
+ #define SHUT_RDWR SD_BOTH
+
+ #define EWOULDBLOCK WSAEWOULDBLOCK
+#endif
+
+namespace NYT::NNet {
+
+using namespace NConcurrency;
+using namespace NProfiling;
+
+#ifdef _unix_
+ using TIOVecBasePtr = void*;
+#else
+ using TIOVecBasePtr = char*;
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+int GetLastNetworkError()
+{
+#ifdef _win_
+ return WSAGetLastError();
+#else
+ return errno;
+#endif
+}
+
+ssize_t ReadFromFD(TFileDescriptor fd, char* buffer, size_t length)
+{
+#ifdef _win_
+ return ::recv(
+ fd,
+ buffer,
+ length,
+ /*flags*/ 0);
+#else
+ return HandleEintr(
+ ::read,
+ fd,
+ buffer,
+ length);
+#endif
+}
+
+ssize_t WriteToFD(TFileDescriptor fd, const char* buffer, size_t length)
+{
+#ifdef _win_
+ return ::send(
+ fd,
+ buffer,
+ length,
+ /*flags*/ 0);
+#else
+ return HandleEintr(
+ ::write,
+ fd,
+ buffer,
+ length);
+#endif
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TFDConnectionImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TIOResult
+{
+ bool Retry;
+ size_t ByteCount;
+};
+
+struct IIOOperation
+{
+ virtual ~IIOOperation() = default;
+
+ virtual TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) = 0;
+
+ virtual void Abort(const TError& error) = 0;
+
+ virtual void SetResult() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadOperation
+ : public IIOOperation
+{
+public:
+ explicit TReadOperation(const TSharedMutableRef& buffer)
+ : Buffer_(buffer)
+ { }
+
+ TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) override
+ {
+ size_t bytesRead = 0;
+ while (Position_ < Buffer_.Size()) {
+ ssize_t size = ReadFromFD(
+ fd,
+ Buffer_.Begin() + Position_,
+ Buffer_.Size() - Position_);
+ if (size == -1) {
+ if (GetLastNetworkError() == EWOULDBLOCK || bytesRead > 0) {
+ return TIOResult{.Retry = Position_ == 0, .ByteCount = bytesRead};
+ }
+
+ return TError("Read failed")
+ << TError::FromSystem();
+ }
+ if (size == 0) {
+ break;
+ }
+
+ bytesRead += size;
+ Position_ += size;
+ }
+ return TIOResult{.Retry = false, .ByteCount = bytesRead};
+ }
+
+ void Abort(const TError& error) override
+ {
+ ResultPromise_.Set(error);
+ }
+
+ void SetResult() override
+ {
+ ResultPromise_.Set(Position_);
+ }
+
+ TFuture<size_t> ToFuture() const
+ {
+ return ResultPromise_.ToFuture();
+ }
+
+private:
+ TSharedMutableRef Buffer_;
+ size_t Position_ = 0;
+
+ TPromise<size_t> ResultPromise_ = NewPromise<size_t>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReceiveFromOperation
+ : public IIOOperation
+{
+public:
+ explicit TReceiveFromOperation(const TSharedMutableRef& buffer)
+ : Buffer_(buffer)
+ { }
+
+ TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) override
+ {
+ ssize_t size = HandleEintr(
+ ::recvfrom,
+ fd,
+ Buffer_.Begin(),
+ Buffer_.Size(),
+ /*flags*/ 0,
+ RemoteAddress_.GetSockAddr(),
+ RemoteAddress_.GetLengthPtr());
+
+ if (size == -1) {
+ if (GetLastNetworkError() == EWOULDBLOCK) {
+ return TIOResult{.Retry = true, .ByteCount = 0};
+ }
+
+ return TError("Read failed")
+ << TError::FromSystem();
+ }
+
+ Position_ += size;
+
+ return TIOResult{.Retry = false, .ByteCount = static_cast<size_t>(size)};
+ }
+
+ void Abort(const TError& error) override
+ {
+ ResultPromise_.Set(error);
+ }
+
+ void SetResult() override
+ {
+ ResultPromise_.Set(std::make_pair(Position_, RemoteAddress_));
+ }
+
+ TFuture<std::pair<size_t, TNetworkAddress>> ToFuture() const
+ {
+ return ResultPromise_.ToFuture();
+ }
+
+private:
+ TSharedMutableRef Buffer_;
+ size_t Position_ = 0;
+ TNetworkAddress RemoteAddress_;
+
+ TPromise<std::pair<size_t, TNetworkAddress>> ResultPromise_ = NewPromise<std::pair<size_t, TNetworkAddress>>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteOperation
+ : public IIOOperation
+{
+public:
+ explicit TWriteOperation(const TSharedRef& buffer)
+ : Buffer_(buffer)
+ { }
+
+ TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) override
+ {
+ size_t bytesWritten = 0;
+ while (Position_ < Buffer_.Size()) {
+ ssize_t size = WriteToFD(
+ fd,
+ Buffer_.Begin() + Position_,
+ Buffer_.Size() - Position_);
+ if (size == -1) {
+ if (GetLastNetworkError() == EWOULDBLOCK) {
+ return TIOResult{.Retry = true, .ByteCount = bytesWritten};
+ }
+ return TError("Write failed")
+ << TError::FromSystem();
+ }
+
+ YT_VERIFY(size > 0);
+ bytesWritten += size;
+ Position_ += size;
+ }
+ return TIOResult{.Retry = false, .ByteCount = bytesWritten};
+ }
+
+ void Abort(const TError& error) override
+ {
+ ResultPromise_.Set(error);
+ }
+
+ void SetResult() override
+ {
+ ResultPromise_.Set();
+ }
+
+ TFuture<void> ToFuture() const
+ {
+ return ResultPromise_.ToFuture();
+ }
+
+private:
+ TSharedRef Buffer_;
+ size_t Position_ = 0;
+
+ TPromise<void> ResultPromise_ = NewPromise<void>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TWriteVOperation
+ : public IIOOperation
+{
+public:
+ explicit TWriteVOperation(const TSharedRefArray& buffers)
+ : Buffers_(buffers)
+ { }
+
+ TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) override
+ {
+ size_t bytesWritten = 0;
+ while (Index_ < Buffers_.Size()) {
+ constexpr int MaxEntries = 128;
+ iovec ioVectors[MaxEntries];
+
+ ioVectors[0].iov_base = reinterpret_cast<TIOVecBasePtr>(const_cast<char*>(Buffers_[Index_].Begin() + Position_));
+ ioVectors[0].iov_len = Buffers_[Index_].Size() - Position_;
+
+ size_t ioVectorsCount = 1;
+ for (; ioVectorsCount < MaxEntries && ioVectorsCount + Index_ < Buffers_.Size(); ++ioVectorsCount) {
+ const auto& ref = Buffers_[Index_ + ioVectorsCount];
+
+ ioVectors[ioVectorsCount].iov_base = reinterpret_cast<TIOVecBasePtr>(const_cast<char*>(ref.Begin()));
+ ioVectors[ioVectorsCount].iov_len = ref.Size();
+ }
+
+ ssize_t size = HandleEintr(::writev, fd, ioVectors, ioVectorsCount);
+
+ if (size == -1) {
+ if (GetLastNetworkError() == EWOULDBLOCK) {
+ return TIOResult{.Retry = true, .ByteCount = bytesWritten};
+ }
+
+ return TError("Write failed")
+ << TError::FromSystem();
+ }
+
+ YT_VERIFY(size > 0);
+ bytesWritten += size;
+ Position_ += size;
+
+ while (Index_ != Buffers_.Size() && Position_ >= Buffers_[Index_].Size()) {
+ Position_ -= Buffers_[Index_].Size();
+ Index_++;
+ }
+ }
+ return TIOResult{.Retry = false, .ByteCount = bytesWritten};
+ }
+
+ void Abort(const TError& error) override
+ {
+ ResultPromise_.Set(error);
+ }
+
+ void SetResult() override
+ {
+ ResultPromise_.Set();
+ }
+
+ TFuture<void> ToFuture() const
+ {
+ return ResultPromise_.ToFuture();
+ }
+
+private:
+ TSharedRefArray Buffers_;
+ size_t Index_ = 0;
+ size_t Position_ = 0;
+
+ TPromise<void> ResultPromise_ = NewPromise<void>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TShutdownOperation
+ : public IIOOperation
+{
+public:
+ explicit TShutdownOperation(bool shutdownRead)
+ : ShutdownRead_(shutdownRead)
+ { }
+
+ TErrorOr<TIOResult> PerformIO(TFileDescriptor fd) override
+ {
+ int res = HandleEintr(::shutdown, fd, ShutdownRead_ ? SHUT_RD : SHUT_WR);
+ if (res == -1) {
+ return TError("Shutdown failed")
+ << TError::FromSystem();
+ }
+ return TIOResult{.Retry = false, .ByteCount = 0};
+ }
+
+ void Abort(const TError& error) override
+ {
+ ResultPromise_.Set(error);
+ }
+
+ void SetResult() override
+ {
+ ResultPromise_.Set();
+ }
+
+ TFuture<void> ToFuture() const
+ {
+ return ResultPromise_.ToFuture();
+ }
+
+private:
+ const bool ShutdownRead_;
+ TPromise<void> ResultPromise_ = NewPromise<void>();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFDConnectionImpl
+ : public TPollableBase
+{
+public:
+ static TFDConnectionImplPtr Create(
+ TFileDescriptor fd,
+ const TString& filePath,
+ const IPollerPtr& poller)
+ {
+ auto impl = New<TFDConnectionImpl>(fd, filePath, poller);
+ impl->Init();
+ return impl;
+ }
+
+ static TFDConnectionImplPtr Create(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const TNetworkAddress& remoteAddress,
+ const IPollerPtr& poller)
+ {
+ auto impl = New<TFDConnectionImpl>(fd, localAddress, remoteAddress, poller);
+ impl->Init();
+ return impl;
+ }
+
+ const TString& GetLoggingTag() const override
+ {
+ return LoggingTag_;
+ }
+
+ void OnEvent(EPollControl control) override
+ {
+ DoIO(&WriteDirection_, Any(control & EPollControl::Write));
+ DoIO(&ReadDirection_, Any(control & EPollControl::Read));
+
+ if (Any(control & EPollControl::ReadHup)) {
+ std::vector<TCallback<void()>> callbacks;
+ {
+ auto guard = Guard(Lock_);
+ PeerDisconnected_ = true;
+ callbacks = std::move(OnPeerDisconnected_);
+ }
+ for (const auto& cb : callbacks) {
+ cb();
+ }
+ }
+ }
+
+ void OnShutdown() override
+ {
+ // Poller guarantees that OnShutdown is never executed concurrently with OnEvent()
+ {
+ auto guard = Guard(Lock_);
+
+ YT_VERIFY(!ReadDirection_.Running);
+ YT_VERIFY(!WriteDirection_.Running);
+
+ auto shutdownError = TError("Connection is shut down");
+ if (WriteError_.IsOK()) {
+ WriteError_ = shutdownError;
+ }
+ if (ReadError_.IsOK()) {
+ ReadError_ = shutdownError;
+ }
+
+ ShutdownRequested_ = true;
+
+ TDelayedExecutor::CancelAndClear(WriteTimeoutCookie_);
+ TDelayedExecutor::CancelAndClear(ReadTimeoutCookie_);
+
+ if (SynchronousIOCount_ > 0) {
+ return;
+ }
+ }
+
+ if (ReadDirection_.Operation) {
+ ReadDirection_.Operation->Abort(ReadError_);
+ ReadDirection_.Operation.reset();
+ }
+ if (WriteDirection_.Operation) {
+ WriteDirection_.Operation->Abort(WriteError_);
+ WriteDirection_.Operation.reset();
+ }
+
+ YT_VERIFY(TryClose(FD_, false));
+ FD_ = -1;
+
+ ReadDirection_.OnShutdown();
+ WriteDirection_.OnShutdown();
+
+ ShutdownPromise_.Set();
+ }
+
+ TFuture<size_t> Read(const TSharedMutableRef& data)
+ {
+ auto read = std::make_unique<TReadOperation>(data);
+ auto future = read->ToFuture();
+ StartIO(&ReadDirection_, std::move(read));
+ return future;
+ }
+
+ TFuture<std::pair<size_t, TNetworkAddress>> ReceiveFrom(const TSharedMutableRef& buffer)
+ {
+ auto receive = std::make_unique<TReceiveFromOperation>(buffer);
+ auto future = receive->ToFuture();
+ StartIO(&ReadDirection_, std::move(receive));
+ return future;
+ }
+
+ void SendTo(const TSharedRef& buffer, const TNetworkAddress& address)
+ {
+ auto guard = TSynchronousIOGuard(this);
+ auto res = HandleEintr(
+ ::sendto,
+ FD_,
+ buffer.Begin(),
+ buffer.Size(),
+ 0, // flags
+ address.GetSockAddr(),
+ address.GetLength());
+ if (res == -1) {
+ THROW_ERROR_EXCEPTION("Write failed")
+ << TError::FromSystem();
+ }
+ }
+
+ bool SetNoDelay()
+ {
+ auto guard = TSynchronousIOGuard(this);
+ return TrySetSocketNoDelay(FD_);
+ }
+
+ bool SetKeepAlive()
+ {
+ auto guard = TSynchronousIOGuard(this);
+ return TrySetSocketKeepAlive(FD_);
+ }
+
+ TFuture<void> Write(const TSharedRef& data)
+ {
+ auto write = std::make_unique<TWriteOperation>(data);
+ auto future = write->ToFuture();
+ StartIO(&WriteDirection_, std::move(write));
+ return future;
+ }
+
+ TFuture<void> WriteV(const TSharedRefArray& data)
+ {
+ auto writeV = std::make_unique<TWriteVOperation>(data);
+ auto future = writeV->ToFuture();
+ StartIO(&WriteDirection_, std::move(writeV));
+ return future;
+ }
+
+ TFuture<void> Close()
+ {
+ auto error = TError("Connection closed")
+ << TErrorAttribute("connection", Name_);
+ return AbortIO(error);
+ }
+
+ bool IsIdle()
+ {
+ auto guard = Guard(Lock_);
+ return
+ ReadError_.IsOK() &&
+ WriteError_.IsOK() &&
+ !WriteDirection_.Operation &&
+ !ReadDirection_.Operation &&
+ SynchronousIOCount_ == 0;
+ }
+
+ TFuture<void> Abort(const TError& error)
+ {
+ return AbortIO(error);
+ }
+
+ TFuture<void> CloseRead()
+ {
+ auto shutdownRead = std::make_unique<TShutdownOperation>(true);
+ auto future = shutdownRead->ToFuture();
+ StartIO(&ReadDirection_, std::move(shutdownRead));
+ return future;
+ }
+
+ TFuture<void> CloseWrite()
+ {
+ auto shutdownWrite = std::make_unique<TShutdownOperation>(false);
+ auto future = shutdownWrite->ToFuture();
+ StartIO(&WriteDirection_, std::move(shutdownWrite));
+ return future;
+ }
+
+ const TNetworkAddress& LocalAddress() const
+ {
+ return LocalAddress_;
+ }
+
+ const TNetworkAddress& RemoteAddress() const
+ {
+ return RemoteAddress_;
+ }
+
+ TFileDescriptor GetHandle() const
+ {
+ return FD_;
+ }
+
+ i64 GetReadByteCount() const
+ {
+ return ReadDirection_.BytesTransferred;
+ }
+
+ i64 GetWriteByteCount() const
+ {
+ return WriteDirection_.BytesTransferred;
+ }
+
+ TConnectionStatistics GetReadStatistics() const
+ {
+ auto guard = Guard(Lock_);
+ return ReadDirection_.GetStatistics();
+ }
+
+ TConnectionStatistics GetWriteStatistics() const
+ {
+ auto guard = Guard(Lock_);
+ return WriteDirection_.GetStatistics();
+ }
+
+ void SetReadDeadline(std::optional<TInstant> deadline)
+ {
+ auto guard = Guard(Lock_);
+
+ if (ShutdownRequested_) {
+ return;
+ }
+
+ TDelayedExecutor::CancelAndClear(ReadTimeoutCookie_);
+
+ if (deadline) {
+ ReadTimeoutCookie_ = TDelayedExecutor::Submit(AbortFromReadTimeout_, *deadline);
+ }
+ }
+
+ void SetWriteDeadline(std::optional<TInstant> deadline)
+ {
+ auto guard = Guard(Lock_);
+
+ if (ShutdownRequested_) {
+ return;
+ }
+
+ TDelayedExecutor::CancelAndClear(WriteTimeoutCookie_);
+
+ if (deadline) {
+ WriteTimeoutCookie_ = TDelayedExecutor::Submit(AbortFromWriteTimeout_, *deadline);
+ }
+ }
+
+ void SubscribePeerDisconnect(TCallback<void()> cb)
+ {
+ {
+ auto guard = Guard(Lock_);
+ if (!PeerDisconnected_) {
+ OnPeerDisconnected_.push_back(std::move(cb));
+ return;
+ }
+ }
+
+ cb();
+ }
+
+private:
+ const TString Name_;
+ const TString LoggingTag_;
+ const TNetworkAddress LocalAddress_;
+ const TNetworkAddress RemoteAddress_;
+ TFileDescriptor FD_ = -1;
+ const IPollerPtr Poller_;
+
+
+ TFDConnectionImpl(
+ TFileDescriptor fd,
+ const TString& filePath,
+ const IPollerPtr& poller)
+ : Name_(Format("File{%v}", filePath))
+ , FD_(fd)
+ , Poller_(poller)
+ { }
+
+ TFDConnectionImpl(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const TNetworkAddress& remoteAddress,
+ const IPollerPtr& poller)
+ : Name_(Format("FD{%v<->%v}", localAddress, remoteAddress))
+ , LoggingTag_(Format("ConnectionId: %v", Name_))
+ , LocalAddress_(localAddress)
+ , RemoteAddress_(remoteAddress)
+ , FD_(fd)
+ , Poller_(poller)
+ { }
+
+ DECLARE_NEW_FRIEND()
+
+ class TSynchronousIOGuard
+ {
+ public:
+ explicit TSynchronousIOGuard(TFDConnectionImplPtr owner)
+ : Owner_(std::move(owner))
+ {
+ auto guard = Guard(Owner_->Lock_);
+ Owner_->WriteError_.ThrowOnError();
+ Owner_->ReadError_.ThrowOnError();
+ ++Owner_->SynchronousIOCount_;
+ }
+
+ ~TSynchronousIOGuard()
+ {
+ if (Owner_) {
+ auto guard = Guard(Owner_->Lock_);
+ YT_VERIFY(Owner_->SynchronousIOCount_ > 0);
+ if (--Owner_->SynchronousIOCount_ == 0 &&
+ Owner_->ShutdownRequested_)
+ {
+ guard.Release();
+ Owner_->OnShutdown();
+ }
+ }
+ }
+
+ TSynchronousIOGuard(const TSynchronousIOGuard&) = delete;
+ TSynchronousIOGuard(TSynchronousIOGuard&&) = default;
+
+ TSynchronousIOGuard& operator=(const TSynchronousIOGuard&) = delete;
+ TSynchronousIOGuard& operator=(TSynchronousIOGuard&&) = default;
+
+ private:
+ const TFDConnectionImplPtr Owner_;
+ };
+
+ enum class EDirection
+ {
+ Read,
+ Write
+ };
+
+ struct TIODirection
+ {
+ explicit TIODirection(EDirection direction)
+ : Direction(direction)
+ { }
+
+ std::unique_ptr<IIOOperation> Operation;
+ std::atomic<i64> BytesTransferred = 0;
+ TDuration IdleDuration;
+ TDuration BusyDuration;
+ TCpuInstant StartTime = GetCpuInstant();
+ std::optional<TCpuInstant> EndTime;
+ EDirection Direction;
+ bool Pending = false;
+ bool Running = false;
+
+ void StartBusyTimer()
+ {
+ auto now = GetCpuInstant();
+ IdleDuration += CpuDurationToDuration(now - StartTime);
+ StartTime = now;
+ }
+
+ void StopBusyTimer()
+ {
+ auto now = GetCpuInstant();
+ BusyDuration += CpuDurationToDuration(now - StartTime);
+ StartTime = now;
+ }
+
+ void OnShutdown()
+ {
+ EndTime = GetCpuInstant();
+ }
+
+ TConnectionStatistics GetStatistics() const
+ {
+ TConnectionStatistics statistics{IdleDuration, BusyDuration};
+ auto lastEventTime = EndTime.value_or(GetCpuInstant());
+ (Operation ? statistics.BusyDuration : statistics.IdleDuration) += CpuDurationToDuration(lastEventTime - StartTime);
+ return statistics;
+ }
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TIODirection ReadDirection_{EDirection::Read};
+ TIODirection WriteDirection_{EDirection::Write};
+ bool ShutdownRequested_ = false;
+ int SynchronousIOCount_ = 0;
+ TError WriteError_;
+ TError ReadError_;
+ const TPromise<void> ShutdownPromise_ = NewPromise<void>();
+
+ bool PeerDisconnected_ = false;
+ std::vector<TCallback<void()>> OnPeerDisconnected_;
+
+ TClosure AbortFromReadTimeout_;
+ TClosure AbortFromWriteTimeout_;
+
+ TDelayedExecutorCookie ReadTimeoutCookie_;
+ TDelayedExecutorCookie WriteTimeoutCookie_;
+
+ void Init()
+ {
+ AbortFromReadTimeout_ = BIND(&TFDConnectionImpl::AbortFromReadTimeout, MakeWeak(this));
+ AbortFromWriteTimeout_ = BIND(&TFDConnectionImpl::AbortFromWriteTimeout, MakeWeak(this));
+
+ if (!Poller_->TryRegister(this)) {
+ WriteError_ = TError("Cannot register connection pollable");
+ ReadError_ = WriteError_;
+ return;
+ }
+
+ Arm();
+ }
+
+ void Arm(EPollControl additionalFlags = {})
+ {
+ auto control = EPollControl::Read | EPollControl::Write | EPollControl::EdgeTriggered | EPollControl::ReadHup;
+ Poller_->Arm(FD_, this, control | additionalFlags);
+ }
+
+ TError GetCurrentError(EDirection direction)
+ {
+ switch (direction) {
+ case EDirection::Read:
+ return ReadError_;
+ case EDirection::Write: {
+ // We want to read if there were write errors before, but we don't want to write if there were read errors,
+ // because it looks useless.
+ auto error = WriteError_;
+ if (error.IsOK() && !ReadError_.IsOK()) {
+ error = ReadError_;
+ }
+ return error;
+ }
+ }
+ }
+
+ void StartIO(TIODirection* direction, std::unique_ptr<IIOOperation> operation)
+ {
+ TError error;
+ bool needRetry = false;
+
+ {
+ auto guard = Guard(Lock_);
+
+ error = GetCurrentError(direction->Direction);
+
+ if (error.IsOK()) {
+ if (direction->Operation) {
+ THROW_ERROR_EXCEPTION("Another IO operation is in progress")
+ << TErrorAttribute("connection", Name_);
+ }
+
+ YT_VERIFY(!direction->Running);
+ direction->Operation = std::move(operation);
+ direction->StartBusyTimer();
+ // Start operation only if this direction already has pending
+ // event otherwise reading from FIFO before opening by writer
+ // will return EOF immediately.
+ needRetry = direction->Pending;
+ }
+ }
+
+ if (!error.IsOK()) {
+ operation->Abort(error);
+ return;
+ }
+
+ if (needRetry) {
+ Poller_->Retry(this);
+ }
+ }
+
+ void DoIO(TIODirection* direction, bool event)
+ {
+ {
+ auto guard = Guard(Lock_);
+
+ if (!event && !direction->Pending) {
+ return;
+ }
+
+ auto error = GetCurrentError(direction->Direction);
+ if (!error.IsOK()) {
+ return;
+ }
+
+ if (!direction->Operation || direction->Running) {
+ direction->Pending |= event;
+ return;
+ }
+
+ direction->Pending = false;
+ direction->Running = true;
+ }
+
+ auto result = direction->Operation->PerformIO(FD_);
+ if (result.IsOK()) {
+ direction->BytesTransferred += result.Value().ByteCount;
+ } else {
+ result = result << TErrorAttribute("connection", Name_);
+ }
+
+ bool needUnregister = false;
+ bool needRetry = false;
+ bool needRearm = false;
+ std::unique_ptr<IIOOperation> operation;
+ {
+ auto guard = Guard(Lock_);
+ direction->Running = false;
+
+ auto error = GetCurrentError(direction->Direction);
+
+ if (!result.IsOK()) {
+ // IO finished with error.
+ operation = std::move(direction->Operation);
+ auto& directionError = direction->Direction == EDirection::Read ? ReadError_ : WriteError_;
+ if (directionError.IsOK()) {
+ directionError = result;
+ if (direction->Direction == EDirection::Read) {
+ Poller_->Unarm(FD_, this);
+ needUnregister = true;
+ }
+ }
+ direction->StopBusyTimer();
+ } else if (!error.IsOK()) {
+ // IO was aborted.
+ operation = std::move(direction->Operation);
+ // Avoid aborting completed IO.
+ if (result.Value().Retry) {
+ result = error;
+ }
+ direction->Pending = true;
+ direction->StopBusyTimer();
+ } else if (result.Value().Retry) {
+ // IO not completed. Retry if have pending backlog.
+ // If dont have pending backlog, just subscribe for further notifications.
+ if (direction->Pending) {
+ needRetry = true;
+ } else {
+ needRearm = true;
+ }
+ } else {
+ // IO finished successfully.
+ operation = std::move(direction->Operation);
+ // TODO not set pending if no backlog after short read/write
+ direction->Pending = true;
+ direction->StopBusyTimer();
+ }
+
+ if (needRearm) {
+ YT_VERIFY(!needRetry && !needUnregister);
+ Arm(EPollControl::BacklogEmpty);
+ }
+ }
+
+ if (!result.IsOK()) {
+ operation->Abort(result);
+ } else if (!result.Value().Retry) {
+ operation->SetResult();
+ } else if (needRetry) {
+ Poller_->Retry(this, false);
+ }
+
+ if (needUnregister) {
+ YT_UNUSED_FUTURE(Poller_->Unregister(this));
+ }
+ }
+
+ TFuture<void> AbortIO(const TError& error)
+ {
+ auto guard = Guard(Lock_);
+ // In case of read errors we have called Unarm and Unregister already.
+ bool needUnarmAndUnregister = ReadError_.IsOK();
+ if (WriteError_.IsOK()) {
+ WriteError_ = error;
+ }
+ if (ReadError_.IsOK()) {
+ ReadError_ = error;
+ }
+ if (needUnarmAndUnregister) {
+ Poller_->Unarm(FD_, this);
+ guard.Release();
+ YT_UNUSED_FUTURE(Poller_->Unregister(this));
+ }
+ return ShutdownPromise_.ToFuture();
+ }
+
+ void AbortFromReadTimeout()
+ {
+ YT_UNUSED_FUTURE(Abort(TError("Read timeout")));
+ }
+
+ void AbortFromWriteTimeout()
+ {
+ YT_UNUSED_FUTURE(Abort(TError("Write timeout")));
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TFDConnectionImpl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// The sole purpose of this class is to call Abort on Impl in dtor.
+class TFDConnection
+ : public IConnection
+{
+public:
+ TFDConnection(
+ TFileDescriptor fd,
+ const TString& pipePath,
+ const IPollerPtr& poller,
+ TRefCountedPtr pipeHolder = nullptr)
+ : Impl_(TFDConnectionImpl::Create(fd, pipePath, poller))
+ , PipeHolder_(std::move(pipeHolder))
+ { }
+
+ TFDConnection(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const TNetworkAddress& remoteAddress,
+ const IPollerPtr& poller)
+ : Impl_(TFDConnectionImpl::Create(fd, localAddress, remoteAddress, poller))
+ { }
+
+ ~TFDConnection()
+ {
+ YT_UNUSED_FUTURE(Impl_->Abort(TError("Connection is abandoned")));
+ }
+
+ const TNetworkAddress& LocalAddress() const override
+ {
+ return Impl_->LocalAddress();
+ }
+
+ const TNetworkAddress& RemoteAddress() const override
+ {
+ return Impl_->RemoteAddress();
+ }
+
+ int GetHandle() const override
+ {
+ return Impl_->GetHandle();
+ }
+
+ TFuture<size_t> Read(const TSharedMutableRef& data) override
+ {
+ return Impl_->Read(data);
+ }
+
+ TFuture<void> Write(const TSharedRef& data) override
+ {
+ return Impl_->Write(data);
+ }
+
+ TFuture<void> WriteV(const TSharedRefArray& data) override
+ {
+ return Impl_->WriteV(data);
+ }
+
+ TFuture<void> Close() override
+ {
+ return Impl_->Close();
+ }
+
+ bool IsIdle() const override
+ {
+ return Impl_->IsIdle();
+ }
+
+ TFuture<void> Abort() override
+ {
+ return Impl_->Abort(TError(EErrorCode::Aborted, "Connection aborted"));
+ }
+
+ TFuture<void> CloseRead() override
+ {
+ return Impl_->CloseRead();
+ }
+
+ TFuture<void> CloseWrite() override
+ {
+ return Impl_->CloseWrite();
+ }
+
+ i64 GetReadByteCount() const override
+ {
+ return Impl_->GetReadByteCount();
+ }
+
+ i64 GetWriteByteCount() const override
+ {
+ return Impl_->GetWriteByteCount();
+ }
+
+ TConnectionStatistics GetReadStatistics() const override
+ {
+ return Impl_->GetReadStatistics();
+ }
+
+ TConnectionStatistics GetWriteStatistics() const override
+ {
+ return Impl_->GetWriteStatistics();
+ }
+
+ void SetReadDeadline(std::optional<TInstant> deadline) override
+ {
+ Impl_->SetReadDeadline(deadline);
+ }
+
+ void SetWriteDeadline(std::optional<TInstant> deadline) override
+ {
+ Impl_->SetWriteDeadline(deadline);
+ }
+
+ bool SetNoDelay() override
+ {
+ return Impl_->SetNoDelay();
+ }
+
+ bool SetKeepAlive() override
+ {
+ return Impl_->SetKeepAlive();
+ }
+
+ void SubscribePeerDisconnect(TCallback<void()> cb) override
+ {
+ return Impl_->SubscribePeerDisconnect(std::move(cb));
+ }
+
+private:
+ const TFDConnectionImplPtr Impl_;
+ TRefCountedPtr PipeHolder_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::pair<IConnectionPtr, IConnectionPtr> CreateConnectionPair(const IPollerPtr& poller)
+{
+ SOCKET fds[2];
+
+#ifdef _unix_
+ int flags = SOCK_STREAM;
+
+ #ifdef _linux_
+ flags |= SOCK_NONBLOCK | SOCK_CLOEXEC;
+ #endif
+
+ if (HandleEintr(::socketpair, AF_LOCAL, flags, 0, fds) == -1) {
+ THROW_ERROR_EXCEPTION("Failed to create socket pair")
+ << TError::FromSystem();
+ }
+#else
+ if (SocketPair(fds, /*overlapped*/ false, /*cloexec*/ true) == SOCKET_ERROR) {
+ THROW_ERROR_EXCEPTION("Failed to create socket pair")
+ << TError::FromSystem();
+ }
+
+ SetNonBlock(fds[0]);
+ SetNonBlock(fds[1]);
+#endif
+
+ try {
+ auto address0 = GetSocketName(fds[0]);
+ auto address1 = GetSocketName(fds[1]);
+
+ auto first = New<TFDConnection>(fds[0], address0, address1, poller);
+ auto second = New<TFDConnection>(fds[1], address1, address0, poller);
+ return std::make_pair(std::move(first), std::move(second));
+ } catch (...) {
+ YT_VERIFY(TryClose(fds[0], false));
+ YT_VERIFY(TryClose(fds[1], false));
+ throw;
+ }
+}
+
+IConnectionPtr CreateConnectionFromFD(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const TNetworkAddress& remoteAddress,
+ const IPollerPtr& poller)
+{
+ return New<TFDConnection>(fd, localAddress, remoteAddress, poller);
+}
+
+IConnectionReaderPtr CreateInputConnectionFromFD(
+ TFileDescriptor fd,
+ const TString& pipePath,
+ const IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder)
+{
+ return New<TFDConnection>(fd, pipePath, poller, pipeHolder);
+}
+
+IConnectionReaderPtr CreateInputConnectionFromPath(
+ const TString& pipePath,
+ const IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder)
+{
+#ifdef _unix_
+ int flags = O_RDONLY | O_CLOEXEC | O_NONBLOCK;
+ int fd = HandleEintr(::open, pipePath.c_str(), flags);
+ if (fd == -1) {
+ THROW_ERROR_EXCEPTION("Failed to open named pipe")
+ << TError::FromSystem()
+ << TErrorAttribute("path", pipePath);
+ }
+
+ return New<TFDConnection>(fd, pipePath, poller, pipeHolder);
+#else
+ THROW_ERROR_EXCEPTION("Unsupported platform");
+#endif
+}
+
+IConnectionWriterPtr CreateOutputConnectionFromPath(
+ const TString& pipePath,
+ const IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder)
+{
+#ifdef _unix_
+ int flags = O_WRONLY | O_CLOEXEC;
+ int fd = HandleEintr(::open, pipePath.c_str(), flags);
+ if (fd == -1) {
+ THROW_ERROR_EXCEPTION("Failed to open named pipe")
+ << TError::FromSystem()
+ << TErrorAttribute("path", pipePath);
+ }
+
+ try {
+ SafeMakeNonblocking(fd);
+ } catch (...) {
+ SafeClose(fd, false);
+ throw;
+ }
+ return New<TFDConnection>(fd, pipePath, poller, pipeHolder);
+#else
+ THROW_ERROR_EXCEPTION("Unsupported platform");
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPacketConnection
+ : public IPacketConnection
+{
+public:
+ TPacketConnection(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const IPollerPtr& poller)
+ : Impl_(TFDConnectionImpl::Create(fd, localAddress, TNetworkAddress{}, poller))
+ { }
+
+ ~TPacketConnection()
+ {
+ YT_UNUSED_FUTURE(Abort());
+ }
+
+ TFuture<std::pair<size_t, TNetworkAddress>> ReceiveFrom(
+ const TSharedMutableRef& buffer) override
+ {
+ return Impl_->ReceiveFrom(buffer);
+ }
+
+ void SendTo(const TSharedRef& buffer, const TNetworkAddress& address) override
+ {
+ Impl_->SendTo(buffer, address);
+ }
+
+ TFuture<void> Abort() override
+ {
+ return Impl_->Abort(TError("Connection is abandoned"));
+ }
+
+private:
+ TFDConnectionImplPtr Impl_;
+};
+
+IPacketConnectionPtr CreatePacketConnection(
+ const TNetworkAddress& at,
+ const NConcurrency::IPollerPtr& poller)
+{
+ auto fd = CreateUdpSocket();
+ try {
+ SetReuseAddrFlag(fd);
+ BindSocket(fd, at);
+ } catch (...) {
+ SafeClose(fd, false);
+ throw;
+ }
+
+ return New<TPacketConnection>(fd, at, poller);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/connection.h b/yt/yt/core/net/connection.h
new file mode 100644
index 0000000000..e52de86abb
--- /dev/null
+++ b/yt/yt/core/net/connection.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt/core/misc/ref_counted.h>
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/net/address.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TConnectionStatistics
+{
+ TDuration IdleDuration;
+ TDuration BusyDuration;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IConnectionReader
+ : public NConcurrency::IAsyncInputStream
+{
+ virtual TFuture<void> CloseRead() = 0;
+
+ virtual TFuture<void> Abort() = 0;
+
+ virtual int GetHandle() const = 0;
+
+ virtual i64 GetReadByteCount() const = 0;
+
+ virtual void SetReadDeadline(std::optional<TInstant> deadline) = 0;
+
+ virtual TConnectionStatistics GetReadStatistics() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IConnectionReader)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IConnectionWriter
+ : public NConcurrency::IAsyncOutputStream
+{
+ virtual TFuture<void> WriteV(const TSharedRefArray& data) = 0;
+
+ virtual TFuture<void> CloseWrite() = 0;
+
+ virtual TFuture<void> Abort() = 0;
+
+ virtual int GetHandle() const = 0;
+
+ virtual i64 GetWriteByteCount() const = 0;
+
+ virtual void SetWriteDeadline(std::optional<TInstant> deadline) = 0;
+
+ virtual TConnectionStatistics GetWriteStatistics() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IConnectionWriter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IConnection
+ : public IConnectionReader
+ , public IConnectionWriter
+{
+ virtual const TNetworkAddress& LocalAddress() const = 0;
+ virtual const TNetworkAddress& RemoteAddress() const = 0;
+
+ // Returns true if connection is not is failed state and has no
+ // active IO operations.
+ virtual bool IsIdle() const = 0;
+
+ virtual bool SetNoDelay() = 0;
+ virtual bool SetKeepAlive() = 0;
+
+ TFuture<void> Abort() override = 0;
+
+ // SubscribePeerDisconnect is best effort and is not guaranteed to fire.
+ virtual void SubscribePeerDisconnect(TCallback<void()> cb) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::pair<IConnectionPtr, IConnectionPtr> CreateConnectionPair(const NConcurrency::IPollerPtr& poller);
+
+//! File descriptor must be in nonblocking mode.
+IConnectionPtr CreateConnectionFromFD(
+ TFileDescriptor fd,
+ const TNetworkAddress& localAddress,
+ const TNetworkAddress& remoteAddress,
+ const NConcurrency::IPollerPtr& poller);
+
+IConnectionReaderPtr CreateInputConnectionFromFD(
+ TFileDescriptor fd,
+ const TString& pipePath,
+ const NConcurrency::IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder);
+
+IConnectionReaderPtr CreateInputConnectionFromPath(
+ const TString& pipePath,
+ const NConcurrency::IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder);
+
+IConnectionWriterPtr CreateOutputConnectionFromPath(
+ const TString& pipePath,
+ const NConcurrency::IPollerPtr& poller,
+ const TRefCountedPtr& pipeHolder);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/dialer.cpp b/yt/yt/core/net/dialer.cpp
new file mode 100644
index 0000000000..1a23091b91
--- /dev/null
+++ b/yt/yt/core/net/dialer.cpp
@@ -0,0 +1,418 @@
+#include "dialer.h"
+#include "connection.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/pollable_detail.h>
+
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/net/socket.h>
+
+#include <util/random/random.h>
+
+namespace NYT::NNet {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDialSession
+ : public TRefCounted
+{
+public:
+ TDialSession(
+ const TNetworkAddress& remoteAddress,
+ const IAsyncDialerPtr& asyncDialer,
+ IPollerPtr poller)
+ : Name_(Format("dialer[%v]", remoteAddress))
+ , RemoteAddress_(remoteAddress)
+ , Poller_(std::move(poller))
+ , Session_(asyncDialer->CreateSession(
+ remoteAddress,
+ BIND(&TDialSession::OnDialerFinished, MakeWeak(this))))
+ {
+ Session_->Dial();
+
+ Promise_.OnCanceled(BIND([this, this_ = MakeStrong(this)] (const TError& error) {
+ Promise_.TrySet(TError(NYT::EErrorCode::Canceled, "Dial canceled")
+ << TErrorAttribute("dialer", Name_)
+ << error);
+ }));
+ }
+
+ TFuture<IConnectionPtr> GetFuture() const
+ {
+ return Promise_.ToFuture();
+ }
+
+private:
+ const TString Name_;
+ const TNetworkAddress RemoteAddress_;
+ const IPollerPtr Poller_;
+ const IAsyncDialerSessionPtr Session_;
+
+ const TPromise<IConnectionPtr> Promise_ = NewPromise<IConnectionPtr>();
+
+ void OnDialerFinished(const TErrorOr<SOCKET>& socketOrError)
+ {
+ if (socketOrError.IsOK()) {
+ auto socket = socketOrError.Value();
+ Promise_.TrySet(CreateConnectionFromFD(
+ socket,
+ GetSocketName(socket),
+ RemoteAddress_,
+ Poller_));
+ } else {
+ Promise_.TrySet(socketOrError
+ << TErrorAttribute("dialer", Name_));
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDialer
+ : public IDialer
+{
+public:
+ TDialer(
+ TDialerConfigPtr config,
+ IPollerPtr poller,
+ const NLogging::TLogger& logger)
+ : AsyncDialer_(CreateAsyncDialer(
+ std::move(config),
+ poller,
+ logger))
+ , Poller_(std::move(poller))
+ { }
+
+ TFuture<IConnectionPtr> Dial(const TNetworkAddress& remote) override
+ {
+ auto session = New<TDialSession>(
+ remote,
+ AsyncDialer_,
+ Poller_);
+ return session->GetFuture();
+ }
+
+private:
+ const IAsyncDialerPtr AsyncDialer_;
+ const IPollerPtr Poller_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TDialer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IDialerPtr CreateDialer(
+ TDialerConfigPtr config,
+ IPollerPtr poller,
+ const NLogging::TLogger& logger)
+{
+ return New<TDialer>(
+ std::move(config),
+ std::move(poller),
+ logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncDialerSession
+ : public IAsyncDialerSession
+{
+public:
+ TAsyncDialerSession(
+ TDialerConfigPtr config,
+ IPollerPtr poller,
+ const NLogging::TLogger& logger,
+ const TNetworkAddress& address,
+ TAsyncDialerCallback onFinished)
+ : Config_(std::move(config))
+ , Poller_(std::move(poller))
+ , Address_(address)
+ , OnFinished_(std::move(onFinished))
+ , Id_(TGuid::Create())
+ , Logger(logger.WithTag("AsyncDialerSession: %v", Id_))
+ , Timeout_(Config_->MinRto * GetRandomVariation())
+ { }
+
+ ~TAsyncDialerSession()
+ {
+ auto guard = Guard(SpinLock_);
+
+ Finished_ = true;
+ CloseSocket();
+ }
+
+ void Dial() override
+ {
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(!Dialed_);
+ Dialed_ = true;
+
+ Connect(guard);
+ }
+
+private:
+ class TPollable
+ : public TPollableBase
+ {
+ public:
+ TPollable(TAsyncDialerSession* owner, TGuid id, SOCKET socket)
+ : Owner_(MakeWeak(owner))
+ , LoggingTag_(Format("AsyncDialerSession{%v:%v}", id, socket))
+ { }
+
+ const TString& GetLoggingTag() const override
+ {
+ return LoggingTag_;
+ }
+
+ void OnEvent(EPollControl /*control*/) override
+ {
+ if (auto owner = Owner_.Lock()) {
+ owner->OnConnected(this);
+ }
+ }
+
+ void OnShutdown() override
+ {
+ if (auto owner = Owner_.Lock()) {
+ owner->OnShutdown(this);
+ }
+ }
+
+ private:
+ const NLogging::TLogger Logger;
+ const TWeakPtr<TAsyncDialerSession> Owner_;
+ const TString LoggingTag_;
+ };
+
+ using TPollablePtr = TIntrusivePtr<TPollable>;
+
+ const TDialerConfigPtr Config_;
+ const IPollerPtr Poller_;
+ const TNetworkAddress Address_;
+ const TAsyncDialerCallback OnFinished_;
+ const TGuid Id_;
+ const NLogging::TLogger Logger;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ SOCKET Socket_ = INVALID_SOCKET;
+ bool Dialed_ = false;
+ bool Finished_ = false;
+ TDuration Timeout_;
+ TDelayedExecutorCookie TimeoutCookie_;
+ TPollablePtr Pollable_;
+
+ void CloseSocket()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (Socket_ != INVALID_SOCKET) {
+ YT_VERIFY(TryClose(Socket_));
+ Socket_ = INVALID_SOCKET;
+ }
+ }
+
+ bool TryRegisterPollable()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ auto pollable = New<TPollable>(this, Id_, Socket_);
+ if (!Poller_->TryRegister(pollable)) {
+ return false;
+ }
+
+ YT_VERIFY(!Pollable_);
+ Pollable_ = std::move(pollable);
+ Poller_->Arm(Socket_, Pollable_, EPollControl::Read | EPollControl::Write | EPollControl::EdgeTriggered);
+ return true;
+ }
+
+ void UnregisterPollable()
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ YT_VERIFY(Socket_ != INVALID_SOCKET);
+ Poller_->Unarm(Socket_, Pollable_);
+
+ YT_VERIFY(Pollable_);
+ YT_UNUSED_FUTURE(Poller_->Unregister(Pollable_));
+ Pollable_.Reset();
+ }
+
+ void Connect(TGuard<NThreading::TSpinLock>& guard)
+ {
+ VERIFY_SPINLOCK_AFFINITY(SpinLock_);
+
+ try {
+ auto family = Address_.GetSockAddr()->sa_family;
+
+ YT_VERIFY(Socket_ == INVALID_SOCKET);
+ if (family == AF_UNIX) {
+ Socket_ = CreateUnixClientSocket();
+ } else {
+ Socket_ = CreateTcpClientSocket(family);
+ if (Config_->EnableNoDelay && !TrySetSocketNoDelay(Socket_)) {
+ YT_LOG_DEBUG("Failed to set socket no delay option");
+ }
+ if (!TrySetSocketKeepAlive(Socket_)) {
+ YT_LOG_DEBUG("Failed to set socket keep alive option");
+ }
+ }
+
+ if (ConnectSocket(Socket_, Address_) == 0) {
+ // Connection was established synchronously.
+ SOCKET socket = Socket_;
+ Socket_ = INVALID_SOCKET;
+ Finished_ = true;
+ guard.Release();
+ OnFinished_(socket);
+ return;
+ }
+
+ if (!TryRegisterPollable()) {
+ THROW_ERROR_EXCEPTION("Cannot register dailer pollable");
+ }
+ } catch (const std::exception& ex) {
+ Finished_ = true;
+ CloseSocket();
+ guard.Release();
+ OnFinished_(ex);
+ return;
+ }
+
+ if (Config_->EnableAggressiveReconnect) {
+ TimeoutCookie_ = TDelayedExecutor::Submit(
+ BIND(&TAsyncDialerSession::OnTimeout, MakeWeak(this)),
+ Timeout_);
+ }
+ }
+
+ void OnConnected(TPollable* pollable)
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (Finished_ || pollable != Pollable_) {
+ return;
+ }
+
+ Finished_ = true;
+
+ UnregisterPollable();
+
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+
+ SOCKET socket = Socket_;
+ YT_VERIFY(socket != INVALID_SOCKET);
+
+ if (int socketError = GetSocketError(socket); socketError == 0) {
+ Socket_ = INVALID_SOCKET;
+ guard.Release();
+ OnFinished_(socket);
+ } else {
+ auto error = TError(NRpc::EErrorCode::TransportError, "Connect error")
+ << TError::FromSystem(socketError);
+ CloseSocket();
+ guard.Release();
+ OnFinished_(error);
+ }
+ }
+
+
+ void OnShutdown(TPollable* pollable)
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (Finished_ || pollable != Pollable_) {
+ return;
+ }
+
+ Finished_ = true;
+
+ Pollable_.Reset();
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+
+ guard.Release();
+
+ OnFinished_(TError("Dialer session was shut down"));
+ }
+
+ void OnTimeout()
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (Finished_) {
+ return;
+ }
+
+ UnregisterPollable();
+
+ CloseSocket();
+
+ if (Timeout_ < Config_->MaxRto) {
+ Timeout_ *= Config_->RtoScale * GetRandomVariation();
+ }
+
+ YT_LOG_DEBUG("Connect timeout; trying to reconnect (Timeout: %v)",
+ Timeout_);
+
+ Connect(guard);
+ }
+
+ static float GetRandomVariation()
+ {
+ return (0.9 + RandomNumber<float>() / 5);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncDialer
+ : public IAsyncDialer
+{
+public:
+ TAsyncDialer(
+ TDialerConfigPtr config,
+ IPollerPtr poller,
+ const NLogging::TLogger& logger)
+ : Config_(std::move(config))
+ , Poller_(std::move(poller))
+ , Logger(logger)
+ { }
+
+ IAsyncDialerSessionPtr CreateSession(
+ const TNetworkAddress& address,
+ TAsyncDialerCallback onFinished) override
+ {
+ return New<TAsyncDialerSession>(
+ Config_,
+ Poller_,
+ Logger,
+ address,
+ std::move(onFinished));
+ }
+
+private:
+ const TDialerConfigPtr Config_;
+ const IPollerPtr Poller_;
+ const NLogging::TLogger Logger;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IAsyncDialerPtr CreateAsyncDialer(
+ TDialerConfigPtr config,
+ IPollerPtr poller,
+ const NLogging::TLogger& logger)
+{
+ return New<TAsyncDialer>(
+ std::move(config),
+ std::move(poller),
+ logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/dialer.h b/yt/yt/core/net/dialer.h
new file mode 100644
index 0000000000..b5a6ce3b2a
--- /dev/null
+++ b/yt/yt/core/net/dialer.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/address.h>
+
+#include <library/cpp/yt/logging/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/network/init.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Dialer establishes connection to a (resolved) network address.
+
+struct IDialer
+ : public virtual TRefCounted
+{
+ virtual TFuture<IConnectionPtr> Dial(const TNetworkAddress& remote) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IDialer)
+
+IDialerPtr CreateDialer(
+ TDialerConfigPtr config,
+ NConcurrency::IPollerPtr poller,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Async dialer notifies caller via callback for better performance.
+using TAsyncDialerCallback = TCallback<void(const TErrorOr<SOCKET>&)>;
+
+//! Dialer session interface.
+//! Caller should hold a reference to a session until callback is called.
+//! When caller releases the reference, session is dropped.
+struct IAsyncDialerSession
+ : public virtual TRefCounted
+{
+ //! Activate session. This method should be called no more than once.
+ virtual void Dial() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncDialerSession)
+
+//! Async dialer interface.
+struct IAsyncDialer
+ : public virtual TRefCounted
+{
+ //! Create dialer session to establish connection to a specific address.
+ virtual IAsyncDialerSessionPtr CreateSession(
+ const TNetworkAddress& address,
+ TAsyncDialerCallback onFinished) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAsyncDialer)
+
+IAsyncDialerPtr CreateAsyncDialer(
+ TDialerConfigPtr config,
+ NConcurrency::IPollerPtr poller,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/helpers.cpp b/yt/yt/core/net/helpers.cpp
new file mode 100644
index 0000000000..3a1abf6906
--- /dev/null
+++ b/yt/yt/core/net/helpers.cpp
@@ -0,0 +1,65 @@
+#include "helpers.h"
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <yt/yt/core/net/socket.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<int> AllocateFreePorts(
+ int portCount,
+ const THashSet<int>& availablePorts,
+ const NLogging::TLogger& logger)
+{
+ if (portCount == 0) {
+ return {};
+ }
+
+ const auto& Logger = logger;
+
+ // Here goes our best effort to make sure we provide free ports to user job.
+ // No doubt there may still be race conditions in which user job will still not be
+ // able to bind to the port, but it should happen pretty rarely.
+ std::vector<int> allocatedPorts;
+
+ for (int port : availablePorts) {
+ SOCKET socket = INVALID_SOCKET;
+
+ try {
+ socket = CreateTcpServerSocket();
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error while creating a socket for preliminary port bind")
+ << ex;
+ }
+
+ YT_VERIFY(socket != INVALID_SOCKET);
+
+ try {
+ YT_LOG_DEBUG("Making a preliminary port bind (Port: %v, Socket: %v)", port, socket);
+ BindSocket(socket, TNetworkAddress::CreateIPv6Any(port));
+ } catch (const std::exception& ex) {
+ SafeClose(socket, false /* ignoreBadFD */);
+ YT_LOG_DEBUG(ex, "Error while trying making a preliminary port bind, skipping it (Port: %v, Socket: %v)", port, socket);
+ continue;
+ }
+
+ SafeClose(socket, false /* ignoreBadFD */);
+ YT_LOG_DEBUG("Socket used in preliminary bind is closed (Port: %v, Socket: %v)", port, socket);
+
+ allocatedPorts.push_back(port);
+
+ if (std::ssize(allocatedPorts) >= portCount) {
+ break;
+ }
+ }
+
+ YT_VERIFY(std::ssize(allocatedPorts) <= portCount);
+
+ return allocatedPorts;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/helpers.h b/yt/yt/core/net/helpers.h
new file mode 100644
index 0000000000..069612e23d
--- /dev/null
+++ b/yt/yt/core/net/helpers.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+#include "public.h"
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Choose `portCount` ports among `availablePorts` such that they are free _right now_ and
+//! return them. If there are less ports available, helper returns as many ports as possible.
+//! This method makes best effort to check that each port is free by making a preliminary bind
+//! and immediately releasing it; if this procedure fails, port is skipped.
+std::vector<int> AllocateFreePorts(
+ int portCount,
+ const THashSet<int>& availablePorts,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/listener.cpp b/yt/yt/core/net/listener.cpp
new file mode 100644
index 0000000000..533c0aa50c
--- /dev/null
+++ b/yt/yt/core/net/listener.cpp
@@ -0,0 +1,236 @@
+#include "listener.h"
+#include "connection.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/pollable_detail.h>
+
+#include <yt/yt/core/net/socket.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <util/network/pollerimpl.h>
+
+namespace NYT::NNet {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = NetLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListener
+ : public TPollableBase
+ , public IListener
+{
+public:
+ TListener(
+ SOCKET serverSocket,
+ const TNetworkAddress& address,
+ const TString& name,
+ IPollerPtr poller,
+ IPollerPtr acceptor)
+ : Name_(name)
+ , Address_(address)
+ , ServerSocket_(serverSocket)
+ , Poller_(poller)
+ , Acceptor_(acceptor)
+ { }
+
+ // IPollable implementation
+ const TString& GetLoggingTag() const override
+ {
+ return Name_;
+ }
+
+ void OnEvent(EPollControl /*control*/) override
+ {
+ try {
+ while (true) {
+ TPromise<IConnectionPtr> promise;
+
+ {
+ auto guard = Guard(Lock_);
+ if (!Error_.IsOK()) {
+ break;
+ }
+ if (Queue_.empty()) {
+ Pending_ = true;
+ break;
+ }
+ promise = std::move(Queue_.front());
+ Queue_.pop_front();
+ Pending_ = false;
+ }
+
+ if (!TryAccept(promise)) {
+ auto guard = Guard(Lock_);
+ Queue_.push_back(promise);
+ if (!Pending_) {
+ break;
+ }
+ }
+ }
+ } catch (const TErrorException& ex) {
+ auto error = ex << TErrorAttribute("listener", Name_);
+ Abort(error);
+ YT_LOG_FATAL(error, "Listener crashed with fatal error");
+ }
+
+ auto guard = Guard(Lock_);
+ if (Error_.IsOK() && !Pending_) {
+ Acceptor_->Arm(ServerSocket_, this, EPollControl::Read | EPollControl::EdgeTriggered | EPollControl::BacklogEmpty);
+ }
+ }
+
+ void OnShutdown() override
+ {
+ decltype(Queue_) queue;
+ {
+ auto guard = Guard(Lock_);
+ if (Error_.IsOK()) {
+ Error_ = TError("Listener is shut down");
+ }
+ std::swap(Queue_, queue);
+ YT_VERIFY(TryClose(ServerSocket_, false));
+ }
+
+ for (auto& promise : queue) {
+ promise.Set(Error_);
+ }
+ }
+
+ const TNetworkAddress& GetAddress() const override
+ {
+ return Address_;
+ }
+
+ // IListener implementation
+ TFuture<IConnectionPtr> Accept() override
+ {
+ auto promise = NewPromise<IConnectionPtr>();
+
+ if (!Pending_ || !TryAccept(promise)) {
+ auto guard = Guard(Lock_);
+ if (Error_.IsOK()) {
+ Queue_.push_back(promise);
+ if (Pending_) {
+ Pending_ = false;
+ Acceptor_->Retry(this);
+ }
+ } else {
+ promise.Set(Error_);
+ }
+ }
+
+ promise.OnCanceled(BIND([promise, this, thisWeak_ = MakeWeak(this)] (const TError& error) {
+ if (auto this_ = thisWeak_.Lock()) {
+ auto guard = Guard(Lock_);
+ auto it = std::find(Queue_.begin(), Queue_.end(), promise);
+ if (it != Queue_.end()) {
+ Queue_.erase(it);
+ }
+ }
+ promise.TrySet(TError(NYT::EErrorCode::Canceled, "Accept canceled")
+ << error);
+ }));
+
+ return promise.ToFuture();
+ }
+
+ void Shutdown() override
+ {
+ Abort(TError("Listener is shut down"));
+ }
+
+private:
+ const TString Name_;
+ const TNetworkAddress Address_;
+ const SOCKET ServerSocket_;
+ const IPollerPtr Poller_;
+ const IPollerPtr Acceptor_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ std::atomic<bool> Pending_ = false;
+ std::deque<TPromise<IConnectionPtr>> Queue_;
+ TError Error_;
+
+
+ void Abort(const TError& error)
+ {
+ YT_VERIFY(!error.IsOK());
+
+ auto guard = Guard(Lock_);
+
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ Pending_ = false;
+ Error_ = error
+ << TErrorAttribute("listener", Name_);
+ Acceptor_->Unarm(ServerSocket_, this);
+ YT_UNUSED_FUTURE(Acceptor_->Unregister(this));
+ }
+
+ bool TryAccept(TPromise<IConnectionPtr> &promise)
+ {
+ TNetworkAddress clientAddress;
+ auto clientSocket = AcceptSocket(ServerSocket_, &clientAddress);
+ if (clientSocket == INVALID_SOCKET) {
+ return false;
+ }
+
+ auto localAddress = GetSocketName(clientSocket);
+ promise.TrySet(CreateConnectionFromFD(
+ clientSocket,
+ localAddress,
+ clientAddress,
+ Poller_));
+
+ return true;
+ }
+};
+
+DECLARE_REFCOUNTED_CLASS(TListener)
+DEFINE_REFCOUNTED_TYPE(TListener)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IListenerPtr CreateListener(
+ const TNetworkAddress& address,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor,
+ int maxBacklogSize)
+{
+ auto serverSocket = address.GetSockAddr()->sa_family == AF_UNIX
+ ? CreateUnixServerSocket()
+ : CreateTcpServerSocket();
+
+ try {
+ BindSocket(serverSocket, address);
+ // Client might have specified port == 0, find real address.
+ auto realAddress = GetSocketName(serverSocket);
+
+ ListenSocket(serverSocket, maxBacklogSize);
+ auto listener = New<TListener>(
+ serverSocket,
+ realAddress,
+ Format("Listener{%v}", realAddress),
+ poller,
+ acceptor);
+ if (!acceptor->TryRegister(listener)) {
+ THROW_ERROR_EXCEPTION("Cannot register listener pollable");
+ }
+ acceptor->Arm(serverSocket, listener, EPollControl::Read | EPollControl::EdgeTriggered);
+ return listener;
+ } catch (...) {
+ YT_VERIFY(TryClose(serverSocket, false));
+ throw;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/listener.h b/yt/yt/core/net/listener.h
new file mode 100644
index 0000000000..d4cdcf96a6
--- /dev/null
+++ b/yt/yt/core/net/listener.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/address.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IListener
+ : public virtual TRefCounted
+{
+ virtual const TNetworkAddress& GetAddress() const = 0;
+ virtual TFuture<IConnectionPtr> Accept() = 0;
+ virtual void Shutdown() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IListener)
+
+IListenerPtr CreateListener(
+ const TNetworkAddress& address,
+ const NConcurrency::IPollerPtr& poller,
+ const NConcurrency::IPollerPtr& acceptor,
+ int maxBacklogSize = 8192);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/local_address.cpp b/yt/yt/core/net/local_address.cpp
new file mode 100644
index 0000000000..b862607ccd
--- /dev/null
+++ b/yt/yt/core/net/local_address.cpp
@@ -0,0 +1,183 @@
+#include "local_address.h"
+#include "address.h"
+
+#include "config.h"
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#ifdef _unix_
+ #include <sys/types.h>
+ #include <sys/socket.h>
+ #include <netdb.h>
+ #include <errno.h>
+#endif
+
+#ifdef _win_
+ #include <winsock2.h>
+#endif
+
+#include <array>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+constexpr size_t MaxLocalFieldLength = 256;
+constexpr size_t MaxLocalFieldDataSize = 1024;
+
+// All static variables below must be const-initialized.
+// - char[] is a POD, so it must be const-initialized.
+// - std::atomic has constexpr value constructors.
+// However, there is no way to enforce in compile-time that these variables
+// are really const-initialized, so please double-check it with `objdump -s`.
+char LocalHostNameData[MaxLocalFieldDataSize] = "(unknown)";
+std::atomic<char*> LocalHostNamePtr;
+
+char LocalYPClusterData[MaxLocalFieldDataSize] = "(unknown)";
+std::atomic<char*> LocalYPClusterPtr;
+
+std::atomic<bool> IPv6Enabled_ = false;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+const char* ReadLocalHostName() noexcept
+{
+ // Writer-side imposes AcqRel ordering, so all preceding writes must be visible.
+ char* ptr = LocalHostNamePtr.load(std::memory_order::relaxed);
+ return ptr ? ptr : LocalHostNameData;
+}
+
+const char* ReadLocalYPCluster() noexcept
+{
+ // Writer-side imposes AcqRel ordering, so all preceding writes must be visible.
+ char* ptr = LocalYPClusterPtr.load(std::memory_order::relaxed);
+ return ptr ? ptr : LocalYPClusterData;
+}
+
+void GuardedWriteString(std::atomic<char*>& storage, char* initial, TStringBuf string)
+{
+ char* ptr = storage.load(std::memory_order::relaxed);
+ ptr = ptr ? ptr : initial;
+
+ if (::strncmp(ptr, string.data(), string.length()) == 0) {
+ return; // No changes; just return.
+ }
+
+ ptr = ptr + strlen(ptr) + 1;
+
+ if (ptr + string.length() + 1 >= initial + MaxLocalFieldDataSize) {
+ ::abort(); // Once we crash here, we can start reusing space.
+ }
+
+ ::memcpy(ptr, string.data(), string.length());
+ *(ptr + string.length()) = 0;
+
+ storage.store(ptr, std::memory_order::seq_cst);
+}
+
+void WriteLocalHostName(TStringBuf hostName) noexcept
+{
+ static NThreading::TForkAwareSpinLock Lock;
+ auto guard = Guard(Lock);
+
+ GuardedWriteString(LocalHostNamePtr, LocalHostNameData, hostName);
+
+ if (auto ypCluster = InferYPClusterFromHostNameRaw(hostName)) {
+ GuardedWriteString(LocalYPClusterPtr, LocalYPClusterData, *ypCluster);
+ }
+}
+
+TString GetLocalHostName()
+{
+ return {ReadLocalHostName()};
+}
+
+TString GetLocalYPCluster()
+{
+ return {ReadLocalYPCluster()};
+}
+
+void UpdateLocalHostName(const TAddressResolverConfigPtr& config)
+{
+ std::array<char, MaxLocalFieldLength> hostName;
+ hostName.fill(0);
+
+ auto onFail = [&] (const std::vector<TError>& errors) {
+ THROW_ERROR_EXCEPTION("Failed to update localhost name") << errors;
+ };
+
+ auto runWithRetries = [&] (std::function<int()> func, std::function<TError(int /*result*/)> onError) {
+ std::vector<TError> errors;
+
+ for (int retryIndex = 0; retryIndex < config->Retries; ++retryIndex) {
+ auto result = func();
+ if (result == 0) {
+ return;
+ }
+
+ errors.push_back(onError(result));
+
+ if (retryIndex + 1 == config->Retries) {
+ onFail(errors);
+ } else {
+ Sleep(config->RetryDelay);
+ }
+ }
+ };
+
+ runWithRetries(
+ [&] { return HandleEintr(::gethostname, hostName.data(), hostName.size() - 1); },
+ [&] (int /*result*/) { return TError("gethostname failed: %v", strerror(errno)); });
+
+ if (!config->ResolveHostNameIntoFqdn) {
+ WriteLocalHostName(TStringBuf(hostName.data()));
+ return;
+ }
+
+ addrinfo request;
+ ::memset(&request, 0, sizeof(request));
+ request.ai_family = AF_UNSPEC;
+ request.ai_socktype = SOCK_STREAM;
+ request.ai_flags |= AI_CANONNAME;
+
+ addrinfo* response = nullptr;
+
+ runWithRetries(
+ [&] { return getaddrinfo(hostName.data(), nullptr, &request, &response); },
+ [&] (int result) { return TError("getaddrinfo failed: %v", gai_strerror(result)); });
+
+ std::unique_ptr<addrinfo, void(*)(addrinfo*)> holder(response, &::freeaddrinfo);
+
+ if (!response->ai_canonname) {
+ auto error = TError("getaddrinfo failed: no canonical hostname");
+ onFail({error});
+ }
+
+ WriteLocalHostName(TStringBuf(response->ai_canonname));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TString& GetLoopbackAddress()
+{
+ static const TString ipv4result("[127.0.1.1]");
+ static const TString ipv6result("[::1]");
+ return IPv6Enabled_.load(std::memory_order::relaxed) ? ipv6result : ipv4result;
+}
+
+void UpdateLoopbackAddress(const TAddressResolverConfigPtr& config)
+{
+ IPv6Enabled_ = config->EnableIPv6;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/local_address.h b/yt/yt/core/net/local_address.h
new file mode 100644
index 0000000000..3665feaa4d
--- /dev/null
+++ b/yt/yt/core/net/local_address.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * Provides a bunch of functions to work with local hostname.
+ *
+ * If you are willing to change / extend these functions, please do it carefully.
+ * Read the comments, read the implementation, consult with the authors.
+ */
+
+/*
+ * Read* & Write* functions work with statically allocated buffer.
+ * These functions are carefully engineered to be as safe and as robust
+ * as possible, because they may be called at any time (e. g. during
+ * invocation of atexit() hooks; or during static initialization).
+ * They must never throw / crash / corrupt program.
+ *
+ * Read* returns a pointer to a null-terminated string. Pointer is always valid
+ * (because it is a static memory!), string content never changes.
+ * Read* may be called concurrently, it is a lock-free function.
+ *
+ * Write* checks if the new value differs from the latest one, and saves
+ * new value to the static memory if so. New value becomes visible for future Read*s.
+ *
+ * Write* may be called concurrently, but it blocks.
+ * It also updates the stored local YP cluster.
+ */
+const char* ReadLocalHostName() noexcept;
+void WriteLocalHostName(TStringBuf hostName) noexcept;
+
+const char* ReadLocalYPCluster() noexcept;
+
+//! Get* & Set* wrap Read* & Write* for more convenient usage.
+//! The price is a dynamically allocated string.
+TString GetLocalHostName();
+TString GetLocalYPCluster();
+
+// Returns the loopback address (either IPv4 or IPv6, depending on the configuration).
+const TString& GetLoopbackAddress();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/mock/dialer.cpp b/yt/yt/core/net/mock/dialer.cpp
new file mode 100644
index 0000000000..6e30a087a5
--- /dev/null
+++ b/yt/yt/core/net/mock/dialer.cpp
@@ -0,0 +1,19 @@
+#include "dialer.h"
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDialerMock::TDialerMock(IDialerPtr underlying)
+ : Underlying_(std::move(underlying))
+{
+ ON_CALL(*this, Dial).WillByDefault([this] (const TNetworkAddress& address) {
+ return Underlying_->Dial(address);
+ });
+}
+
+DECLARE_REFCOUNTED_CLASS(TDialerMock)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/mock/dialer.h b/yt/yt/core/net/mock/dialer.h
new file mode 100644
index 0000000000..707a7b3dad
--- /dev/null
+++ b/yt/yt/core/net/mock/dialer.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <yt/yt/core/net/dialer.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDialerMock
+ : public IDialer
+{
+public:
+ explicit TDialerMock(IDialerPtr underlying);
+
+ MOCK_METHOD(TFuture<IConnectionPtr>, Dial, (const TNetworkAddress& remote), (override));
+
+private:
+ const IDialerPtr Underlying_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TDialerMock)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/mock/ya.make b/yt/yt/core/net/mock/ya.make
new file mode 100644
index 0000000000..b405e9d862
--- /dev/null
+++ b/yt/yt/core/net/mock/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ dialer.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ yt/yt/core
+)
+
+END()
diff --git a/yt/yt/core/net/packet_connection.h b/yt/yt/core/net/packet_connection.h
new file mode 100644
index 0000000000..36c21175c1
--- /dev/null
+++ b/yt/yt/core/net/packet_connection.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/ref_counted.h>
+
+#include <yt/yt/core/net/address.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IPacketConnection
+ : public virtual TRefCounted
+{
+ virtual TFuture<std::pair<size_t, TNetworkAddress>> ReceiveFrom(const TSharedMutableRef& buffer) = 0;
+
+ // NOTE: This method is synchronous
+ virtual void SendTo(const TSharedRef& buffer, const TNetworkAddress& address) = 0;
+
+ virtual TFuture<void> Abort() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IPacketConnection)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IPacketConnectionPtr CreatePacketConnection(
+ const TNetworkAddress& at,
+ const NConcurrency::IPollerPtr& poller);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/private.h b/yt/yt/core/net/private.h
new file mode 100644
index 0000000000..04dcc63e14
--- /dev/null
+++ b/yt/yt/core/net/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger NetLogger("Net");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/public.cpp b/yt/yt/core/net/public.cpp
new file mode 100644
index 0000000000..bf60f50f96
--- /dev/null
+++ b/yt/yt/core/net/public.cpp
@@ -0,0 +1,9 @@
+#include "public.h"
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/public.h b/yt/yt/core/net/public.h
new file mode 100644
index 0000000000..bb1b74c615
--- /dev/null
+++ b/yt/yt/core/net/public.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/misc/intrusive_ptr.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNetworkAddress;
+class TIP6Address;
+class TIP6Network;
+
+DECLARE_REFCOUNTED_STRUCT(IConnection)
+DECLARE_REFCOUNTED_STRUCT(IPacketConnection)
+DECLARE_REFCOUNTED_STRUCT(IConnectionReader)
+DECLARE_REFCOUNTED_STRUCT(IConnectionWriter)
+DECLARE_REFCOUNTED_STRUCT(IListener)
+DECLARE_REFCOUNTED_STRUCT(IDialer)
+DECLARE_REFCOUNTED_STRUCT(IAsyncDialer)
+DECLARE_REFCOUNTED_STRUCT(IAsyncDialerSession)
+
+DECLARE_REFCOUNTED_CLASS(TDialerConfig)
+DECLARE_REFCOUNTED_CLASS(TAddressResolverConfig)
+
+YT_DEFINE_ERROR_ENUM(
+ ((Aborted) (1500))
+ ((ResolveTimedOut) (1501))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/socket.cpp b/yt/yt/core/net/socket.cpp
new file mode 100644
index 0000000000..26790df13e
--- /dev/null
+++ b/yt/yt/core/net/socket.cpp
@@ -0,0 +1,528 @@
+#include "socket.h"
+#include "address.h"
+
+#include <yt/yt/core/misc/proc.h>
+
+#include <library/cpp/yt/system/handle_eintr.h>
+
+#include <util/system/env.h>
+#include <util/system/shellcommand.h>
+
+#ifdef _unix_
+ #include <netinet/ip.h>
+ #include <netinet/tcp.h>
+ #include <sys/socket.h>
+ #include <sys/un.h>
+ #include <sys/types.h>
+ #include <sys/stat.h>
+#endif
+
+#ifdef _win_
+ #include <util/network/socket.h>
+
+ #include <winsock2.h>
+#endif
+
+#ifdef _linux_
+ #include <linux/filter.h>
+#endif
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetReuseAddrFlag(SOCKET socket)
+{
+ int flag = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (const char*) &flag, sizeof(flag)) != 0) {
+ auto lastError = LastSystemError();
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to configure socket address reuse")
+ << TError::FromSystem(lastError);
+ }
+}
+
+void SetReusePortFlag(SOCKET socket)
+{
+#ifdef SO_REUSEPORT
+ int flag = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_REUSEPORT, (const char*) &flag, sizeof(flag)) != 0) {
+ auto lastError = LastSystemError();
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to configure socket port reuse")
+ << TError::FromSystem(lastError);
+ }
+#endif
+}
+
+SOCKET CreateTcpServerSocket()
+{
+ int type = SOCK_STREAM;
+
+#ifdef _linux_
+ type |= SOCK_CLOEXEC;
+ type |= SOCK_NONBLOCK;
+#endif
+
+ SOCKET serverSocket = socket(AF_INET6, type, IPPROTO_TCP);
+ if (serverSocket == INVALID_SOCKET) {
+ auto lastError = LastSystemError();
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to create a server socket")
+ << TError::FromSystem(lastError);
+ }
+
+#ifdef _win_
+ SetNonBlock(serverSocket);
+#endif
+
+#if defined _unix_ && !defined _linux_
+ {
+ int flags = fcntl(serverSocket, F_GETFL);
+ int result = fcntl(serverSocket, F_SETFL, flags | O_NONBLOCK);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(serverSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable nonblocking mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+ {
+ int flags = fcntl(serverSocket, F_GETFD);
+ int result = fcntl(serverSocket, F_SETFD, flags | FD_CLOEXEC);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(serverSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable close-on-exec mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+#endif
+
+ {
+ int flag = 0;
+ if (setsockopt(serverSocket, IPPROTO_IPV6, IPV6_V6ONLY, (const char*) &flag, sizeof(flag)) != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(serverSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to configure IPv6 protocol")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+ try {
+ SetReuseAddrFlag(serverSocket);
+ // SO_REUSEPORT is necessary for Darwin build.
+ // More details here: https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ
+#ifdef _darwin_
+ SetReusePortFlag(serverSocket);
+#endif
+ } catch (...) {
+ SafeClose(serverSocket, false);
+ throw;
+ }
+
+ return serverSocket;
+}
+
+SOCKET CreateUnixServerSocket()
+{
+ int type = SOCK_STREAM;
+
+#ifdef _linux_
+ type |= SOCK_CLOEXEC;
+ type |= SOCK_NONBLOCK;
+#endif
+
+ SOCKET serverSocket = socket(AF_UNIX, type, 0);
+ if (serverSocket == INVALID_SOCKET) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to create a local server socket")
+ << TError::FromSystem();
+ }
+
+#ifdef _win_
+ SetNonBlock(serverSocket);
+#endif
+
+ return serverSocket;
+}
+
+SOCKET CreateTcpClientSocket(int family)
+{
+ YT_VERIFY(family == AF_INET6 || family == AF_INET);
+
+ int protocol = IPPROTO_TCP;
+ int type = SOCK_STREAM;
+
+#ifdef _linux_
+ type |= SOCK_CLOEXEC;
+ type |= SOCK_NONBLOCK;
+#endif
+
+ SOCKET clientSocket = socket(family, type, protocol);
+ if (clientSocket == INVALID_SOCKET) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to create client socket")
+ << TError::FromSystem();
+ }
+
+ if (family == AF_INET6) {
+ int value = 0;
+ if (setsockopt(clientSocket, IPPROTO_IPV6, IPV6_V6ONLY, (const char*) &value, sizeof(value)) != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(clientSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to configure IPv6 protocol")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+#if defined _unix_ && !defined _linux_
+ {
+ int flags = fcntl(clientSocket, F_GETFL);
+ int result = fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(clientSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable nonblocking mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+ {
+ int flags = fcntl(clientSocket, F_GETFD);
+ int result = fcntl(clientSocket, F_SETFD, flags | FD_CLOEXEC);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(clientSocket, false);
+ THROW_ERROR_EXCEPTION("Failed to enable close-on-exec mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+#elif defined _win_
+ SetNonBlock(clientSocket);
+#endif
+
+ return clientSocket;
+}
+
+SOCKET CreateUnixClientSocket()
+{
+ int family = AF_UNIX;
+ int protocol = 0;
+ int type = SOCK_STREAM;
+
+#ifdef _linux_
+ type |= SOCK_CLOEXEC;
+ type |= SOCK_NONBLOCK;
+#endif
+
+ SOCKET clientSocket = socket(family, type, protocol);
+ if (clientSocket == INVALID_SOCKET) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to create client socket")
+ << TError::FromSystem();
+ }
+
+#if defined _unix_ && !defined _linux_
+ {
+ int flags = fcntl(clientSocket, F_GETFL);
+ int result = fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(clientSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable nonblocking mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+ {
+ int flags = fcntl(clientSocket, F_GETFD);
+ int result = fcntl(clientSocket, F_SETFD, flags | FD_CLOEXEC);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(clientSocket, false);
+ THROW_ERROR_EXCEPTION("Failed to enable close-on-exec mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+#elif defined _win_
+ SetNonBlock(clientSocket);
+#endif
+
+ return clientSocket;
+}
+
+SOCKET CreateUdpSocket()
+{
+ int type = SOCK_DGRAM;
+
+#ifdef _linux_
+ type |= SOCK_CLOEXEC;
+ type |= SOCK_NONBLOCK;
+#endif
+
+ SOCKET udpSocket = socket(AF_INET6, type, 0);
+ if (udpSocket == INVALID_SOCKET) {
+ auto lastError = LastSystemError();
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to create a server socket")
+ << TError::FromSystem(lastError);
+ }
+
+#ifdef _win_
+ SetNonBlock(udpSocket);
+#endif
+
+ return udpSocket;
+}
+
+int ConnectSocket(SOCKET clientSocket, const TNetworkAddress& address)
+{
+ int result = HandleEintr(connect, clientSocket, address.GetSockAddr(), address.GetLength());
+ if (result != 0) {
+ int error = LastSystemError();
+ if (error != EAGAIN && error != EWOULDBLOCK && error != EINPROGRESS) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Error connecting to %v",
+ address)
+ << TError::FromSystem(error);
+ }
+ }
+
+ return result;
+}
+
+void BindSocket(SOCKET serverSocket, const TNetworkAddress& address)
+{
+ if (bind(serverSocket, address.GetSockAddr(), address.GetLength()) != 0) {
+ if (GetEnv("YT_DEBUG_TAKEN_PORT")) {
+ try {
+ TShellCommand cmd("ss -tlpn");
+ cmd.Run();
+ Cerr << cmd.GetOutput() << Endl;
+
+ TShellCommand cmd2("ss -tpn");
+ cmd2.Run();
+ Cerr << cmd2.GetOutput() << Endl;
+ } catch (const std::exception& ex) {
+ Cerr << ex.what() << Endl;
+ }
+ }
+
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to bind a server socket to %v",
+ address)
+ << TError::FromSystem();
+ }
+}
+
+int AcceptSocket(SOCKET serverSocket, TNetworkAddress* clientAddress)
+{
+ SOCKET clientSocket;
+
+#ifdef _linux_
+ clientSocket = accept4(
+ serverSocket,
+ clientAddress->GetSockAddr(),
+ clientAddress->GetLengthPtr(),
+ SOCK_CLOEXEC | SOCK_NONBLOCK);
+#else
+ clientSocket = accept(
+ serverSocket,
+ clientAddress->GetSockAddr(),
+ clientAddress->GetLengthPtr());
+#endif
+
+ if (clientSocket == INVALID_SOCKET) {
+ if (LastSystemError() != EAGAIN && LastSystemError() != EWOULDBLOCK && LastSystemError() != ECONNABORTED) {
+ // ECONNABORTED means, that a socket on the listen
+ // queue was closed before we Accept()ed it; ignore it.
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Error accepting connection")
+ << TError::FromSystem();
+ }
+
+ return clientSocket;
+ }
+
+#if defined _unix_ && !defined _linux_
+ {
+ int flags = fcntl(clientSocket, F_GETFL);
+ int result = fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(serverSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable nonblocking mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+
+ {
+ int flags = fcntl(clientSocket, F_GETFD);
+ int result = fcntl(clientSocket, F_SETFD, flags | FD_CLOEXEC);
+ if (result != 0) {
+ auto lastError = LastSystemError();
+ SafeClose(serverSocket, false);
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to enable close-on-exec mode")
+ << TError::FromSystem(lastError);
+ }
+ }
+#elif defined _win_
+ SetNonBlock(clientSocket);
+#endif
+
+ return clientSocket;
+}
+
+void ListenSocket(SOCKET serverSocket, int backlog)
+{
+ if (listen(serverSocket, backlog) == -1) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to listen to server socket")
+ << TError::FromSystem();
+ }
+}
+
+void CloseSocket(SOCKET socket)
+{
+ YT_VERIFY(close(socket) == 0);
+}
+
+int GetSocketError(SOCKET socket)
+{
+ int error;
+ socklen_t errorLen = sizeof (error);
+ getsockopt(socket, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&error), &errorLen);
+ return error;
+}
+
+TNetworkAddress GetSocketName(SOCKET socket)
+{
+ TNetworkAddress address;
+ auto lengthPtr = address.GetLengthPtr();
+ int result = getsockname(socket, address.GetSockAddr(), lengthPtr);
+ if (result != 0) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to get socket name")
+ << TError::FromSystem();
+ }
+
+ return address;
+}
+
+TNetworkAddress GetSocketPeerName(SOCKET socket)
+{
+ TNetworkAddress address;
+ auto lengthPtr = address.GetLengthPtr();
+ int result = getpeername(socket, address.GetSockAddr(), lengthPtr);
+ if (result != 0) {
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::TransportError,
+ "Failed to get socket peer name")
+ << TError::FromSystem();
+ }
+
+ return address;
+}
+
+bool TrySetSocketNoDelay(SOCKET socket)
+{
+ int value = 1;
+ if (setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, (const char*) &value, sizeof(value)) != 0) {
+ return false;
+ }
+ return true;
+}
+
+bool TrySetSocketKeepAlive(SOCKET socket)
+{
+#ifdef _linux_
+ int value = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, (const char*) &value, sizeof(value)) != 0) {
+ return false;
+ }
+#else
+ Y_UNUSED(socket);
+#endif
+ return true;
+}
+
+bool TrySetSocketEnableQuickAck(SOCKET socket)
+{
+#ifdef _linux_
+ int value = 1;
+ if (setsockopt(socket, IPPROTO_TCP, TCP_QUICKACK, (const char*) &value, sizeof(value)) != 0) {
+ return false;
+ }
+#else
+ Y_UNUSED(socket);
+#endif
+ return true;
+}
+
+bool TrySetSocketTosLevel(SOCKET socket, int tosLevel)
+{
+#ifdef _unix_
+ if (setsockopt(socket, IPPROTO_IP, IP_TOS, &tosLevel, sizeof(tosLevel)) != 0) {
+ return false;
+ }
+ if (setsockopt(socket, IPPROTO_IPV6, IPV6_TCLASS, &tosLevel, sizeof(tosLevel)) != 0) {
+ return false;
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool TrySetSocketInputFilter(SOCKET socket, bool drop)
+{
+#ifdef _linux_
+ static struct sock_filter filter_code[] = {
+ BPF_STMT(BPF_RET, 0),
+ };
+ static const struct sock_fprog filter = {
+ .len = Y_ARRAY_SIZE(filter_code),
+ .filter = filter_code,
+ };
+
+ return setsockopt(socket, SOL_SOCKET, drop ? SO_ATTACH_FILTER : SO_DETACH_FILTER, &filter, sizeof(filter)) == 0;
+#else
+ Y_UNUSED(socket);
+ Y_UNUSED(drop);
+ return false;
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/socket.h b/yt/yt/core/net/socket.h
new file mode 100644
index 0000000000..79c7199b41
--- /dev/null
+++ b/yt/yt/core/net/socket.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "address.h"
+
+#include <yt/yt/core/misc/public.h>
+
+#include <util/network/init.h>
+
+namespace NYT::NNet {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Create NONBLOCKING server socket, set IPV6_ONLY and REUSEADD flags.
+SOCKET CreateTcpServerSocket();
+SOCKET CreateUnixServerSocket();
+SOCKET CreateTcpClientSocket(int family);
+SOCKET CreateUnixClientSocket();
+SOCKET CreateUdpSocket();
+
+//! Start connect on the socket. Any errors other than EWOULDBLOCK,
+//! EAGAIN and EINPROGRESS are thrown as exceptions.
+int ConnectSocket(SOCKET clientSocket, const TNetworkAddress& address);
+
+void SetReuseAddrFlag(SOCKET socket);
+
+//! Try binding socket to address. Error is thrown as exception.
+void BindSocket(SOCKET serverSocket, const TNetworkAddress& address);
+
+//! Try to accept client on non-blocking socket. Any errors other than
+//! EWOULDBLOCK or EAGAIN are thrown as exceptions.
+//! Returned socket has CLOEXEC and NONBLOCK flags set.
+int AcceptSocket(SOCKET serverSocket, TNetworkAddress* clientAddress);
+
+void ListenSocket(SOCKET serverSocket, int backlog);\
+
+void CloseSocket(SOCKET socket);
+
+int GetSocketError(SOCKET socket);
+
+TNetworkAddress GetSocketName(SOCKET socket);
+TNetworkAddress GetSocketPeer(SOCKET socket);
+
+bool TrySetSocketNoDelay(SOCKET socket);
+bool TrySetSocketKeepAlive(SOCKET socket);
+bool TrySetSocketEnableQuickAck(SOCKET socket);
+bool TrySetSocketTosLevel(SOCKET socket, int tosLevel);
+bool TrySetSocketInputFilter(SOCKET socket, bool drop);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/unittests/local_address_ut.cpp b/yt/yt/core/net/unittests/local_address_ut.cpp
new file mode 100644
index 0000000000..d840b0d024
--- /dev/null
+++ b/yt/yt/core/net/unittests/local_address_ut.cpp
@@ -0,0 +1,30 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/net/local_address.h>
+
+namespace NYT::NNet {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TLocalYPClusterTest, Basic)
+{
+ TString ypHostNameMan = "noqpmfiudzbb4hvs.man.yp-c.yandex.net";
+ TString ypHostNameSas = "hellodarknessmyoldfriend.sas.yp-c.yandex.net";
+
+ WriteLocalHostName(ypHostNameMan);
+ TStringBuf localYPCluster1{ReadLocalYPCluster()};
+ EXPECT_EQ(localYPCluster1, "man");
+ EXPECT_EQ(GetLocalYPCluster(), "man");
+
+ WriteLocalHostName(ypHostNameSas);
+ TStringBuf localYPCluster2{ReadLocalYPCluster()};
+ EXPECT_EQ(localYPCluster1, "man");
+ EXPECT_EQ(localYPCluster2, "sas");
+ EXPECT_EQ(GetLocalYPCluster(), "sas");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/unittests/net_ut.cpp b/yt/yt/core/net/unittests/net_ut.cpp
new file mode 100644
index 0000000000..65fa6ad04e
--- /dev/null
+++ b/yt/yt/core/net/unittests/net_ut.cpp
@@ -0,0 +1,377 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/net/connection.h>
+#include <yt/yt/core/net/listener.h>
+#include <yt/yt/core/net/dialer.h>
+#include <yt/yt/core/net/config.h>
+#include <yt/yt/core/net/private.h>
+
+#include <yt/yt/core/concurrency/poller.h>
+#include <yt/yt/core/concurrency/thread_pool_poller.h>
+
+#include <util/network/pollerimpl.h>
+
+namespace NYT::NNet {
+namespace {
+
+using namespace NYT::NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNetTest
+ : public ::testing::Test
+{
+protected:
+ IThreadPoolPollerPtr Poller_;
+
+ void SetUp() override
+ {
+ Poller_ = CreateThreadPoolPoller(2, "nettest");
+ }
+
+ void TearDown() override
+ {
+ Poller_->Shutdown();
+ }
+
+ IDialerPtr CreateDialer()
+ {
+ return NNet::CreateDialer(
+ New<TDialerConfig>(),
+ Poller_,
+ NetLogger);
+ }
+};
+
+TEST_F(TNetTest, ReconfigurePoller)
+{
+ for (int i = 1; i <= 10; ++i) {
+ Poller_->Reconfigure(i);
+ }
+ for (int i = 9; i >= 10; --i) {
+ Poller_->Reconfigure(i);
+ }
+}
+
+TEST_F(TNetTest, CreateConnectionPair)
+{
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+}
+
+TEST_F(TNetTest, TransferFourBytes)
+{
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ a->Write(TSharedRef::FromString("ping")).Get();
+
+ auto buffer = TSharedMutableRef::Allocate(10);
+ ASSERT_EQ(4u, b->Read(buffer).Get().ValueOrThrow());
+ ASSERT_EQ(ToString(buffer.Slice(0, 4)), TString("ping"));
+}
+
+TEST_F(TNetTest, TransferFourBytesUsingWriteV)
+{
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ a->WriteV(TSharedRefArray(std::vector<TSharedRef>{
+ TSharedRef::FromString("p"),
+ TSharedRef::FromString("i"),
+ TSharedRef::FromString("n"),
+ TSharedRef::FromString("g")
+ }, TSharedRefArray::TMoveParts{})).Get().ThrowOnError();
+
+ auto buffer = TSharedMutableRef::Allocate(10);
+ ASSERT_EQ(4u, b->Read(buffer).Get().ValueOrThrow());
+ ASSERT_EQ(ToString(buffer.Slice(0, 4)), TString("ping"));
+}
+
+TEST_F(TNetTest, BigTransfer)
+{
+// Select-based poller implementation is much slower there.
+#if defined(HAVE_EPOLL_POLLER)
+ const int N = 1024, K = 256 * 1024;
+#else
+ const int N = 32, K = 256 * 1024;
+#endif
+
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ auto sender = BIND([=] {
+ auto buffer = TSharedRef::FromString(TString(K, 'f'));
+ for (int i = 0; i < N; ++i) {
+ WaitFor(a->Write(buffer)).ThrowOnError();
+ }
+
+ WaitFor(a->CloseWrite()).ThrowOnError();
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+
+ auto receiver = BIND([=] {
+ auto buffer = TSharedMutableRef::Allocate(3 * K);
+ ssize_t received = 0;
+ while (true) {
+ int res = WaitFor(b->Read(buffer)).ValueOrThrow();
+ if (res == 0) break;
+ received += res;
+ }
+
+ EXPECT_EQ(N * K, received);
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+
+ sender.Get().ThrowOnError();
+ receiver.Get().ThrowOnError();
+}
+
+TEST_F(TNetTest, BidirectionalTransfer)
+{
+// Select-based poller implementation is much slower there.
+#if defined(HAVE_EPOLL_POLLER)
+ const int N = 1024, K = 256 * 1024;
+#else
+ const int N = 32, K = 256 * 1024;
+#endif
+
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ auto startSender = [&] (IConnectionPtr conn) {
+ return BIND([=] {
+ auto buffer = TSharedRef::FromString(TString(K, 'f'));
+ for (int i = 0; i < N; ++i) {
+ WaitFor(conn->Write(buffer)).ThrowOnError();
+ }
+
+ WaitFor(conn->CloseWrite()).ThrowOnError();
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+ };
+
+ auto startReceiver = [&] (IConnectionPtr conn) {
+ return BIND([=] {
+ auto buffer = TSharedMutableRef::Allocate(K * 4);
+ ssize_t received = 0;
+ while (true) {
+ int res = WaitFor(conn->Read(buffer)).ValueOrThrow();
+ if (res == 0) break;
+ received += res;
+ }
+
+ EXPECT_EQ(N * K, received);
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+ };
+
+ std::vector<TFuture<void>> futures = {
+ startSender(a),
+ startReceiver(a),
+ startSender(b),
+ startReceiver(b)
+ };
+
+ AllSucceeded(futures).Get().ThrowOnError();
+}
+
+class TContinueReadInCaseOfWriteErrorsTest
+ : public TNetTest
+ , public testing::WithParamInterface<bool>
+{ };
+
+TEST_P(TContinueReadInCaseOfWriteErrorsTest, ContinueReadInCaseOfWriteErrors)
+{
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ auto data = TSharedRef::FromString(TString(16 * 1024, 'f'));
+ bool gracefulConnectionClose = GetParam();
+ // If server closes the connection without reading the entire request,
+ // it causes an error 'Connection reset by peer' on client's side right after reading response.
+ if (!gracefulConnectionClose) {
+ b->Write(data).Get().ThrowOnError();
+ }
+ a->Write(data).Get().ThrowOnError();
+ a->Close().Get().ThrowOnError();
+
+ {
+ auto data = TSharedRef::FromString(TString(16 * 1024, 'a'));
+ #ifndef _win_
+ EXPECT_THROW(b->Write(data).Get().ThrowOnError(), TErrorException);
+ #endif
+ }
+
+ auto buffer = TSharedMutableRef::Allocate(32 * 1024);
+ auto read = b->Read(buffer).Get().ValueOrThrow();
+
+ EXPECT_EQ(data.size(), read);
+ ASSERT_EQ(ToString(buffer.Slice(0, 4)), TString("ffff"));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TContinueReadInCaseOfWriteErrorsTest,
+ TContinueReadInCaseOfWriteErrorsTest,
+ testing::Values(
+ false,
+ true
+ )
+);
+
+TEST_F(TNetTest, StressConcurrentClose)
+{
+ for (int i = 0; i < 10; i++) {
+ IConnectionPtr a, b;
+ std::tie(a, b) = CreateConnectionPair(Poller_);
+
+ auto runSender = [&] (IConnectionPtr conn) {
+ return BIND([=] {
+ auto buffer = TSharedRef::FromString(TString(16 * 1024, 'f'));
+ while (true) {
+ WaitFor(conn->Write(buffer)).ThrowOnError();
+ }
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+ };
+
+ auto runReceiver = [&] (IConnectionPtr conn) {
+ return BIND([=] {
+ auto buffer = TSharedMutableRef::Allocate(16 * 1024);
+ while (true) {
+ int res = WaitFor(conn->Read(buffer)).ValueOrThrow();
+ if (res == 0) break;
+ }
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run();
+ };
+
+ runSender(a);
+ runReceiver(a);
+ runSender(b);
+ runReceiver(b);
+
+ Sleep(TDuration::MilliSeconds(10));
+ a->Close().Get().ThrowOnError();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TNetTest, Bind)
+{
+ // TODO(aleexfi): test is broken.
+ #ifdef _win_
+ return;
+ #endif
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = CreateListener(address, Poller_, Poller_);
+
+ // On Windows option SO_REUSEADDR (which is enabled by CreateTcpSocket) behaves exactly like combination
+ // SO_REUSEADDR | SO_REUSEPORT, so consecutive binds on the same address will succeed.
+ // https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ
+ #ifdef _linux_
+ EXPECT_THROW(CreateListener(listener->GetAddress(), Poller_, Poller_), TErrorException);
+ #else
+ EXPECT_NO_THROW(CreateListener(listener->GetAddress(), Poller_, Poller_));
+ #endif
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TNetTest, DialError)
+{
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(4000);
+ auto dialer = CreateDialer();
+ EXPECT_THROW(dialer->Dial(address).Get().ValueOrThrow(), TErrorException);
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TNetTest, DialSuccess)
+{
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = CreateListener(address, Poller_, Poller_);
+ auto dialer = CreateDialer();
+
+ auto futureDial = dialer->Dial(listener->GetAddress());
+ auto futureAccept = listener->Accept();
+
+ WaitFor(futureDial).ValueOrThrow();
+ WaitFor(futureAccept).ValueOrThrow();
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TNetTest, ManyDials)
+{
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = CreateListener(address, Poller_, Poller_);
+ auto dialer = CreateDialer();
+
+ auto futureAccept1 = listener->Accept();
+ auto futureAccept2 = listener->Accept();
+
+ auto futureDial1 = dialer->Dial(listener->GetAddress());
+ auto futureDial2 = dialer->Dial(listener->GetAddress());
+
+ WaitFor(AllSucceeded(std::vector<TFuture<IConnectionPtr>>{futureDial1, futureDial2, futureAccept1, futureAccept2})).ValueOrThrow();
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TNetTest, AbandonDial)
+{
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = CreateListener(address, Poller_, Poller_);
+ auto dialer = CreateDialer();
+
+ dialer->Dial(listener->GetAddress());
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+TEST_F(TNetTest, AbandonAccept)
+{
+ BIND([&] {
+ auto address = TNetworkAddress::CreateIPv6Loopback(0);
+ auto listener = CreateListener(address, Poller_, Poller_);
+
+ listener->Accept();
+ })
+ .AsyncVia(Poller_->GetInvoker())
+ .Run()
+ .Get()
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/unittests/network_address_ut.cpp b/yt/yt/core/net/unittests/network_address_ut.cpp
new file mode 100644
index 0000000000..6d295fae96
--- /dev/null
+++ b/yt/yt/core/net/unittests/network_address_ut.cpp
@@ -0,0 +1,362 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/socket.h>
+
+#include <util/system/fs.h>
+
+#ifdef _unix_
+ #include <sys/types.h>
+ #include <sys/socket.h>
+#endif
+
+namespace NYT::NNet {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TNetworkAddressTest, ParseGoodIPv4)
+{
+ TNetworkAddressFormatOptions options{
+ .IncludePort = false
+ };
+
+ auto address = TNetworkAddress::Parse("[192.0.2.33]");
+ EXPECT_EQ("tcp://192.0.2.33", ToString(address, options));
+
+ address = TNetworkAddress::Parse("192.0.2.33");
+ EXPECT_EQ("tcp://192.0.2.33", ToString(address, options));
+}
+
+TEST(TNetworkAddressTest, ParseGoodIPv4WithPort)
+{
+ auto address = TNetworkAddress::Parse("[192.0.2.33]:1000");
+ EXPECT_EQ("tcp://192.0.2.33:1000", ToString(address));
+
+ address = TNetworkAddress::Parse("192.0.2.33:1000");
+ EXPECT_EQ("tcp://192.0.2.33:1000", ToString(address));
+}
+
+TEST(TNetworkAddressTest, ParseBadIPv4Address)
+{
+ EXPECT_ANY_THROW(TNetworkAddress::Parse("[192.0.XXX.33]")); // extra symbols
+ EXPECT_ANY_THROW(TNetworkAddress::Parse("[192.0.2.33]:")); // no port after colon
+}
+
+TEST(TNetworkAddressTest, ParseGoodIPv6)
+{
+ TNetworkAddressFormatOptions options{
+ .IncludePort = false
+ };
+
+ auto address = TNetworkAddress::Parse("[2001:db8:8714:3a90::12]");
+ EXPECT_EQ("tcp://[2001:db8:8714:3a90::12]", ToString(address, options));
+}
+
+TEST(TNetworkAddressTest, IP6Conversion)
+{
+ auto address = TNetworkAddress::Parse("[2001:db8:8714:3a90::12]");
+ auto ip6Address = address.ToIP6Address();
+
+ EXPECT_EQ("2001:db8:8714:3a90::12", ToString(ip6Address));
+}
+
+TEST(TNetworkAddressTest, ParseGoodIPv6WithPort)
+{
+ auto address = TNetworkAddress::Parse("[2001:db8:8714:3a90::12]:1000");
+ EXPECT_EQ("tcp://[2001:db8:8714:3a90::12]:1000", ToString(address));
+}
+
+TEST(TNetworkAddressTest, ParseBadIPv6Address)
+{
+ EXPECT_ANY_THROW(TNetworkAddress::Parse("[2001:db8:SOME_STRING:3a90::12]")); // extra symbols
+ EXPECT_ANY_THROW(TNetworkAddress::Parse("[2001:db8:8714:3a90::12]:")); // no port after colon
+}
+
+TEST(TNetworkAddressTest, FormatParseGoodIPv4NoTcpNoPort)
+{
+ TNetworkAddressFormatOptions options{
+ .IncludePort = false,
+ .IncludeTcpProtocol = false
+ };
+
+ auto address = TNetworkAddress::Parse("127.0.0.1");
+ EXPECT_EQ("127.0.0.1", ToString(address, options));
+}
+
+TEST(TNetworkAddressTest, FormatParseGoodIPv6NoTcpNoPort)
+{
+ TNetworkAddressFormatOptions options{
+ .IncludePort = false,
+ .IncludeTcpProtocol = false
+ };
+
+ auto address = TNetworkAddress::Parse("2001:db8:8714:3a90::12");
+ EXPECT_EQ("2001:db8:8714:3a90::12", ToString(address, options));
+}
+
+#ifdef _linux_
+
+TEST(TNetworkAddressTest, UnixSocketName)
+{
+ int fds[2];
+ YT_VERIFY(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ EXPECT_EQ("unix://[*unnamed*]", ToString(GetSocketName(fds[0])));
+
+ close(fds[0]);
+ close(fds[1]);
+
+ auto address = TNetworkAddress::CreateUnixDomainSocketAddress("abc");
+ EXPECT_EQ(Format("unix://%v/abc", NFs::CurrentWorkingDirectory()), ToString(address));
+
+ auto absctractAddress = TNetworkAddress::CreateAbstractUnixDomainSocketAddress("abc");
+ EXPECT_EQ("unix://[abc]", ToString(absctractAddress));
+
+ auto binaryString = TString("a\0c", 3);
+ auto binaryAbstractAddress = TNetworkAddress::CreateAbstractUnixDomainSocketAddress(binaryString);
+
+ EXPECT_EQ(
+ Format("%Qv", TString("unix://[a\\x00c]")),
+ Format("%Qv", ToString(binaryAbstractAddress)));
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TIP6AddressTest, ToString)
+{
+ {
+ TIP6Address address;
+ ASSERT_EQ("::", ToString(address));
+ }
+
+ {
+ TIP6Address address;
+ address.GetRawWords()[0] = 3;
+ ASSERT_EQ("::3", ToString(address));
+ }
+
+ {
+ TIP6Address address;
+ address.GetRawWords()[7] = 0xfff1;
+ ASSERT_EQ("fff1::", ToString(address));
+ }
+}
+
+TEST(TIP6AddressTest, InvalidAddress)
+{
+ for (const auto& addr : std::vector<TString>{
+ ":::",
+ "1::1::1",
+ "0:1:2:3:4:5:6:7:8",
+ "0:1:2:3:4:5:67777",
+ "0:1:2:3:4:5:6:7:",
+ ":0:1:2:3:4:5:6:7",
+ ":1:2:3:4:5:6:7",
+ "1:2:3:4:5:6:7:"
+ }) {
+ EXPECT_THROW(TIP6Address::FromString(addr), TErrorException)
+ << addr;
+ }
+}
+
+std::array<ui16, 8> AddressToWords(const TIP6Address& addr) {
+ std::array<ui16, 8> buf;
+ std::copy(addr.GetRawWords(), addr.GetRawWords() + 8, buf.begin());
+ return buf;
+}
+
+TEST(TIP6AddressTest, FromString)
+{
+ typedef std::pair<TString, std::array<ui16, 8>> TTestCase;
+
+ for (const auto& testCase : {
+ TTestCase{"0:0:0:0:0:0:0:0", {0, 0, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"0:0:0:0:0:0:0:3", {3, 0, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"0:0:0:0::0:3", {3, 0, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"fff1:1:3:4:5:6:7:8", {8, 7, 6, 5, 4, 3, 1_KB / 1_KB, 0xfff1}},
+ TTestCase{"::", {0, 0, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"::1", {1, 0, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"::1:2", {2, 1, 0, 0, 0, 0, 0, 0}},
+ TTestCase{"1::", {0, 0, 0, 0, 0, 0, 0, 1}},
+ TTestCase{"0:1::", {0, 0, 0, 0, 0, 0, 1, 0}},
+ TTestCase{"0:1::1:0:0", {0, 0, 1, 0, 0, 0, 1, 0}},
+ TTestCase{"ffab:3:0::1234:6", {0x6, 0x1234, 0, 0, 0, 0, 0x3, 0xffab}}
+ }) {
+ auto address = TIP6Address::FromString(testCase.first);
+ EXPECT_EQ(AddressToWords(address), testCase.second);
+ }
+}
+
+TEST(TIP6AddressTest, CanonicalText)
+{
+ for (const auto& str : std::vector<TString>{
+ "::",
+ "::1",
+ "1::",
+ "2001:db8::1:0:0:1",
+ "::2001:db8:1:0:0:1",
+ "::2001:db8:1:1:0:0",
+ "2001:db8::1:1:0:0",
+ "1:2:3:4:5:6:7:8"
+ }) {
+ auto address = TIP6Address::FromString(str);
+ EXPECT_EQ(str, ToString(address));
+ }
+}
+
+TEST(TIP6AddressTest, NetworkMask)
+{
+ using TTestCase = std::tuple<const char*, std::array<ui16, 8>, int>;
+ for (const auto& testCase : {
+ TTestCase{"::/1", {0, 0, 0, 0, 0, 0, 0, 0x8000}, 1},
+ TTestCase{"::/0", {0, 0, 0, 0, 0, 0, 0, 0}, 0},
+ TTestCase{"::/24", {0, 0, 0, 0, 0, 0, 0xff00, 0xffff}, 24},
+ TTestCase{"::/32", {0, 0, 0, 0, 0, 0, 0xffff, 0xffff}, 32},
+ TTestCase{"::/64", {0, 0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff}, 64},
+ TTestCase{"::/128", {0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff}, 128},
+ }) {
+ auto network = TIP6Network::FromString(std::get<0>(testCase));
+ EXPECT_EQ(AddressToWords(network.GetMask()), std::get<1>(testCase));
+ EXPECT_EQ(network.GetMaskSize(), std::get<2>(testCase));
+ EXPECT_EQ(ToString(network), std::get<0>(testCase));
+ }
+
+ EXPECT_THROW(TIP6Network::FromString("::/129"), TErrorException);
+ EXPECT_THROW(TIP6Network::FromString("::/1291"), TErrorException);
+}
+
+TEST(TIP6AddressTest, ProjectId)
+{
+ using TTestCase = std::tuple<const char*, const char*, std::array<ui16, 8>, std::array<ui16, 8>>;
+ for (const auto& testCase : {
+ TTestCase{
+ "10ec9bd@2a02:6b8:fc00::/40",
+ "2a02:6b8:fc17:3204:10e:c9bd:0:320f",
+ {0, 0, 0xc9bd, 0x10e, 0, 0xfc00, 0x6b8, 0x2a02},
+ {0, 0, 0xffff, 0xffff, 0, 0xff00, 0xffff, 0xffff}
+ },
+ TTestCase{
+ "102bcfb@2a02:6b8:c00::/40",
+ "2a02:6b8:c13:3120:102:bcfb:0:2334",
+ {0, 0, 0xbcfb, 0x102, 0, 0x0c00, 0x6b8, 0x2a02},
+ {0, 0, 0xffff, 0xffff, 0, 0xff00, 0xffff, 0xffff}
+ },
+ }) {
+ auto network = TIP6Network::FromString(std::get<0>(testCase));
+ auto testAddress = TIP6Address::FromString(std::get<1>(testCase));
+ EXPECT_EQ(AddressToWords(network.GetAddress()), std::get<2>(testCase));
+ EXPECT_EQ(AddressToWords(network.GetMask()), std::get<3>(testCase));
+ EXPECT_TRUE(network.Contains(testAddress) );
+ }
+
+ EXPECT_THROW(TIP6Network::FromString("@1::1"), TErrorException);
+ EXPECT_THROW(TIP6Network::FromString("123456789@1::1"), TErrorException);
+}
+
+TEST(TIP6AddressTest, InvalidInput)
+{
+ for (const auto& testCase : std::vector<TString>{
+ "",
+ ":",
+ "::/",
+ "::/1",
+ ":::",
+ "::1::",
+ "1",
+ "1:1",
+ "11111::",
+ "g::",
+ "1:::1",
+ "fff1:1:3:4:5:6:7:8:9",
+ "fff1:1:3:4:5:6:7:8::",
+ "::fff1:1:3:4:5:6:7:8"
+ }) {
+ EXPECT_THROW(TIP6Address::FromString(testCase), TErrorException)
+ << Format("input = %Qv", testCase);
+
+ auto network = testCase + "/32";
+ EXPECT_THROW(TIP6Network::FromString(network), TErrorException)
+ << Format("input = %Qv", network);
+ }
+}
+
+TEST(TIP6AddressTest, ToStringFromStringRandom)
+{
+ for (int i = 0; i < 100; ++i) {
+ ui8 bytes[TIP6Address::ByteSize];
+ for (int j = 0; j < static_cast<ssize_t>(TIP6Address::ByteSize); ++j) {
+ bytes[j] = RandomNumber<ui8>();
+ }
+
+ auto address = TIP6Address::FromRawBytes(bytes);
+ ASSERT_EQ(address, TIP6Address::FromString(ToString(address)));
+ }
+}
+
+TEST(TMtnAddressTest, SimpleTest)
+{
+ TMtnAddress address(TIP6Address::FromString("1361:24ad:4326:bda1:8432:a3fe:3f6c:4b38"));
+ EXPECT_EQ(address.GetPrefix(), 0x136124ad43u);
+ EXPECT_EQ(address.GetGeo(), 0x26bda1u);
+ EXPECT_EQ(address.GetProjectId(), 0x8432a3feu);
+ EXPECT_EQ(address.GetHost(), 0x3f6c4b38u);
+
+ address.SetPrefix(0x123456789a);
+ EXPECT_EQ(ToString(address.ToIP6Address()), "1234:5678:9a26:bda1:8432:a3fe:3f6c:4b38");
+
+ address.SetGeo(0x123456);
+ EXPECT_EQ(ToString(address.ToIP6Address()), "1234:5678:9a12:3456:8432:a3fe:3f6c:4b38");
+
+ address.SetProjectId(0x12345678);
+ EXPECT_EQ(ToString(address.ToIP6Address()), "1234:5678:9a12:3456:1234:5678:3f6c:4b38");
+
+ address.SetHost(0x12345678);
+ EXPECT_EQ(ToString(address.ToIP6Address()), "1234:5678:9a12:3456:1234:5678:1234:5678");
+
+ EXPECT_THROW(address.SetPrefix(1ull << 41), TErrorException);
+ EXPECT_THROW(address.SetGeo(1ull << 25), TErrorException);
+ EXPECT_THROW(address.SetProjectId(1ull << 33), TErrorException);
+ EXPECT_THROW(address.SetHost(1ull << 33), TErrorException);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(InferYPCluster, ValidFqdns)
+{
+ TString gencfgHostName = "sas1-5535-9d7.sas-test.yp.gencfg-c.yandex.net";
+ TString ypHostName = "noqpmfiudzbb4hvs.man.yp-c.yandex.net";
+
+ EXPECT_EQ(InferYPClusterFromHostName(gencfgHostName), "sas-test");
+ EXPECT_EQ(InferYPClusterFromHostName(ypHostName), "man");
+}
+
+TEST(InferYPCluster, InvalidFqdn)
+{
+ TString hostName = "noqpmfiudzbb4hvs..yp-c.yandex.net";
+
+ EXPECT_EQ(InferYPClusterFromHostName(hostName), std::nullopt);
+ EXPECT_EQ(InferYPClusterFromHostName("localhost"), std::nullopt);
+ EXPECT_EQ(InferYPClusterFromHostName("noqpmfiudzbb4hvs."), std::nullopt);
+ EXPECT_EQ(InferYPClusterFromHostName("yandex.net"), std::nullopt);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(InferYTCluster, ClusterUrls)
+{
+ EXPECT_EQ(InferYTClusterFromClusterUrl("hume"), "hume");
+ EXPECT_EQ(InferYTClusterFromClusterUrl("http://yp-sas.yt.yandex.net"), "yp-sas");
+ EXPECT_EQ(InferYTClusterFromClusterUrl("seneca-man.yt.yandex.net"), "seneca-man");
+ EXPECT_EQ(InferYTClusterFromClusterUrl("seneca-man..yt.yandex.net"), std::nullopt);
+ EXPECT_EQ(InferYTClusterFromClusterUrl("localhost:1245"), std::nullopt);
+ EXPECT_EQ(InferYTClusterFromClusterUrl("kek:1245"), std::nullopt);
+ EXPECT_EQ(InferYTClusterFromClusterUrl("localhost"), std::nullopt);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NNet
diff --git a/yt/yt/core/net/unittests/ya.make b/yt/yt/core/net/unittests/ya.make
new file mode 100644
index 0000000000..2fdcc268b5
--- /dev/null
+++ b/yt/yt/core/net/unittests/ya.make
@@ -0,0 +1,43 @@
+GTEST(unittester-core-net)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ local_address_ut.cpp
+ net_ut.cpp
+ network_address_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+ library/cpp/streams/zstd
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TIMEOUT(120)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/profiling/public.h b/yt/yt/core/profiling/public.h
new file mode 100644
index 0000000000..94bd2a4fcf
--- /dev/null
+++ b/yt/yt/core/profiling/public.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/library/profiling/tag.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Generic value for samples.
+using TValue = i64;
+
+using TCpuInstant = NYT::TCpuInstant;
+using TCpuDuration = NYT::TCpuDuration;
+
+//! Enumeration of metric types.
+/*
+ * - Counter: A counter is a cumulative metric that represents a single numerical
+ * value that only ever goes up. A counter is typically used to count requests served,
+ * tasks completed, errors occurred, etc.. Counters should not be used to expose current
+ * counts of items whose number can also go down, e.g. the number of currently running
+ * goroutines. Use gauges for this use case.
+ *
+ * - Gauge: A gauge is a metric that represents a single numerical value that can
+ * arbitrarily go up and down. Gauges are typically used for measured values like
+ * temperatures or current memory usage, but also "counts" that can go up and down,
+ * like the number of running goroutines.
+ */
+DEFINE_ENUM(EMetricType,
+ ((Counter) (0))
+ ((Gauge) (1))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/timing-inl.h b/yt/yt/core/profiling/timing-inl.h
new file mode 100644
index 0000000000..81cc08303a
--- /dev/null
+++ b/yt/yt/core/profiling/timing-inl.h
@@ -0,0 +1,53 @@
+#ifndef TIMING_INL_H_
+#error "Direct inclusion of this file is not allowed, include timing.h"
+// For the sake of sane code completion.
+#include "timing.h"
+#endif
+#undef TIMING_INL_H_
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTimer>
+TValueIncrementingTimingGuard<TTimer>::TValueIncrementingTimingGuard(TDuration* value)
+ : Value_(value)
+{ }
+
+template <class TTimer>
+TValueIncrementingTimingGuard<TTimer>::~TValueIncrementingTimingGuard()
+{
+ *Value_ += Timer_.GetElapsedTime();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TCpuDurationIncrementingGuard::TCpuDurationIncrementingGuard(TCpuDuration* value)
+ : Value_(value)
+ , StartInstant_(GetCpuInstant())
+{ }
+
+inline TCpuDurationIncrementingGuard::~TCpuDurationIncrementingGuard()
+{
+ *Value_ += GetCpuInstant() - StartInstant_;
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTimer>
+TTimerGuard<TTimer>::TTimerGuard(TTimer* timer)
+ : Timer_(timer)
+{
+ Timer_->Start();
+}
+
+template <class TTimer>
+TTimerGuard<TTimer>::~TTimerGuard()
+{
+ Timer_->Stop();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/timing.cpp b/yt/yt/core/profiling/timing.cpp
new file mode 100644
index 0000000000..b5963138b0
--- /dev/null
+++ b/yt/yt/core/profiling/timing.cpp
@@ -0,0 +1,128 @@
+#include "timing.h"
+
+#include <yt/yt/core/misc/public.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <util/system/hp_timer.h>
+
+#include <array>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TValue DurationToValue(TDuration duration)
+{
+ return duration.MicroSeconds();
+}
+
+TDuration ValueToDuration(TValue value)
+{
+ // TDuration is unsigned and thus does not support negative values.
+ if (value < 0) {
+ value = 0;
+ }
+ return TDuration::MicroSeconds(static_cast<ui64>(value));
+}
+
+TValue CpuDurationToValue(TCpuDuration cpuDuration)
+{
+ return cpuDuration > 0
+ ? DurationToValue(CpuDurationToDuration(cpuDuration))
+ : -DurationToValue(CpuDurationToDuration(-cpuDuration));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TWallTimer::TWallTimer(bool start)
+{
+ if (start) {
+ Start();
+ }
+}
+
+TInstant TWallTimer::GetStartTime() const
+{
+ return CpuInstantToInstant(GetStartCpuTime());
+}
+
+TDuration TWallTimer::GetElapsedTime() const
+{
+ return CpuDurationToDuration(GetElapsedCpuTime());
+}
+
+TCpuInstant TWallTimer::GetStartCpuTime() const
+{
+ return StartTime_;
+}
+
+TCpuDuration TWallTimer::GetElapsedCpuTime() const
+{
+ return Duration_ + GetCurrentCpuDuration();
+}
+
+TValue TWallTimer::GetElapsedValue() const
+{
+ return DurationToValue(GetElapsedTime());
+}
+
+void TWallTimer::Start()
+{
+ StartTime_ = GetCpuInstant();
+ Active_ = true;
+}
+
+void TWallTimer::StartIfNotActive()
+{
+ if (!Active_) {
+ Start();
+ }
+}
+
+void TWallTimer::Stop()
+{
+ Duration_ += GetCurrentCpuDuration();
+ StartTime_ = 0;
+ Active_ = false;
+}
+
+void TWallTimer::Restart()
+{
+ Duration_ = 0;
+ Start();
+}
+
+TCpuDuration TWallTimer::GetCurrentCpuDuration() const
+{
+ return Active_
+ ? Max<TCpuDuration>(GetCpuInstant() - StartTime_, 0)
+ : 0;
+}
+
+void TWallTimer::Persist(const TStreamPersistenceContext& context)
+{
+ using NYT::Persist;
+
+ Persist(context, Active_);
+ if (context.IsSave()) {
+ auto duration = GetElapsedCpuTime();
+ Persist(context, duration);
+ } else {
+ Persist(context, Duration_);
+ StartTime_ = Active_ ? GetCpuInstant() : 0;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFiberWallTimer::TFiberWallTimer()
+ : NConcurrency::TContextSwitchGuard(
+ [this] () noexcept { Stop(); },
+ [this] () noexcept { Start(); })
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/timing.h b/yt/yt/core/profiling/timing.h
new file mode 100644
index 0000000000..e293038a0c
--- /dev/null
+++ b/yt/yt/core/profiling/timing.h
@@ -0,0 +1,130 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NYT::GetCpuInstant;
+using NYT::GetInstant;
+using NYT::CpuDurationToDuration;
+using NYT::DurationToCpuDuration;
+using NYT::CpuInstantToInstant;
+using NYT::InstantToCpuInstant;
+
+//! Converts a duration to TValue suitable for profiling.
+/*!
+ * The current implementation just returns microseconds.
+ */
+TValue DurationToValue(TDuration duration);
+
+//! Converts a TValue to duration.
+/*!
+ * The current implementation assumes that #value is given in microseconds.
+ */
+TDuration ValueToDuration(TValue value);
+
+//! Converts a CPU duration into TValue suitable for profiling.
+TValue CpuDurationToValue(TCpuDuration cpuDuration);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Continuously tracks the wall time passed since construction.
+class TWallTimer
+{
+public:
+ TWallTimer(bool start = true);
+
+ TInstant GetStartTime() const;
+ TDuration GetElapsedTime() const;
+ TValue GetElapsedValue() const;
+
+ TCpuInstant GetStartCpuTime() const;
+ TCpuDuration GetElapsedCpuTime() const;
+
+ void Start();
+ void StartIfNotActive();
+ void Stop();
+ void Restart();
+
+ void Persist(const TStreamPersistenceContext& context);
+
+private:
+ TCpuDuration GetCurrentCpuDuration() const;
+
+ TCpuInstant StartTime_ = 0;
+ TCpuDuration Duration_ = 0;
+ bool Active_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Upon destruction, increments the value by the elapsed time (measured by the timer)
+//! passed since construction.
+template <class TTimer>
+class TValueIncrementingTimingGuard
+{
+public:
+ explicit TValueIncrementingTimingGuard(TDuration* value);
+ ~TValueIncrementingTimingGuard();
+
+ TValueIncrementingTimingGuard(const TValueIncrementingTimingGuard&) = delete;
+ TValueIncrementingTimingGuard& operator=(const TValueIncrementingTimingGuard&) = delete;
+
+private:
+ TDuration* const Value_;
+ TTimer Timer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCpuDurationIncrementingGuard
+{
+public:
+ explicit TCpuDurationIncrementingGuard(TCpuDuration* value);
+ ~TCpuDurationIncrementingGuard();
+
+private:
+ TCpuDuration* Value_;
+ TCpuInstant StartInstant_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Similar to TWallTimer but excludes the time passed while the fiber was inactive.
+class TFiberWallTimer
+ : public TWallTimer
+ , private NConcurrency::TContextSwitchGuard
+{
+public:
+ TFiberWallTimer();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Calls TTimer::Start() on construction and TTimer::Stop() on destruction.
+template <class TTimer>
+class TTimerGuard
+{
+public:
+ explicit TTimerGuard(TTimer* timer);
+ ~TTimerGuard();
+
+private:
+ TTimer* const Timer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
+
+#define TIMING_INL_H_
+#include "timing-inl.h"
+#undef TIMING_INL_H_
diff --git a/yt/yt/core/profiling/tscp-inl.h b/yt/yt/core/profiling/tscp-inl.h
new file mode 100644
index 0000000000..f13fe4cceb
--- /dev/null
+++ b/yt/yt/core/profiling/tscp-inl.h
@@ -0,0 +1,33 @@
+#ifndef TSCP_INL_H_
+#error "Direct inclusion of this file is not allowed, include tscp.h"
+// For the sake of sane code completion.
+#include "tscp.h"
+#endif
+#undef TSCP_INL_H_
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TTscp TTscp::Get()
+{
+#if defined(__x86_64__)
+ ui64 rax, rcx, rdx;
+ asm volatile ( "rdtscp\n" : "=a" (rax), "=c" (rcx), "=d" (rdx) : : );
+ ui64 t = (rdx << 32) + rax;
+ ui64 c = rcx;
+#elif defined(__arm64__) || defined(__aarch64__)
+ ui64 c;
+ __asm__ volatile("mrs %x0, tpidrro_el0" : "=r"(c));
+ c = c & 0x07u;
+ TCpuInstant t = GetCpuInstant();
+#endif
+ return TTscp{
+ .Instant = static_cast<TCpuInstant>(t),
+ .ProcessorId = static_cast<int>(c) & (MaxProcessorId - 1)
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/tscp.h b/yt/yt/core/profiling/tscp.h
new file mode 100644
index 0000000000..354aad6b3c
--- /dev/null
+++ b/yt/yt/core/profiling/tscp.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+
+#include <util/generic/bitops.h>
+
+namespace NYT::NProfiling {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Timestamp counter + processor id.
+struct TTscp
+{
+ //! Defines the range for TTscp::ProcessorId.
+ //! Always a power of 2.
+ static constexpr int MaxProcessorId = 64;
+ static_assert(IsPowerOf2(MaxProcessorId), "MaxProcessorId must be a power of 2.");
+
+ //! The moment this TTscp instance was created.
+ TCpuInstant Instant;
+
+ //! "Processor id", always taken modulo #MaxProcessorId.
+ //! There's no guarantee that same value indicates the same phisycal core.
+ int ProcessorId;
+
+ static TTscp Get();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NProfiling
+
+#define TSCP_INL_H_
+#include "tscp-inl.h"
+#undef TSCP_INL_H_
diff --git a/yt/yt/core/profiling/unittests/timer_ut.cpp b/yt/yt/core/profiling/unittests/timer_ut.cpp
new file mode 100644
index 0000000000..6c7720288c
--- /dev/null
+++ b/yt/yt/core/profiling/unittests/timer_ut.cpp
@@ -0,0 +1,133 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+#include <yt/yt/core/misc/blob_output.h>
+#include <yt/yt/core/misc/phoenix.h>
+
+#include <util/generic/cast.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+using namespace NConcurrency;
+using namespace NPhoenix;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto SleepQuantum = TDuration::MilliSeconds(100);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PersistWaitRestore(TWallTimer& timer)
+{
+ TBlobOutput output;
+ TSaveContext saveContext(&output);
+ Save(saveContext, timer);
+ saveContext.Finish();
+ auto blob = output.Flush();
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ TMemoryInput input(blob.Begin(), blob.Size());
+ TLoadContext loadContext(&input);
+ Load(loadContext, timer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTimerTest
+ : public ::testing::Test
+{
+protected:
+ TLazyIntrusivePtr<TActionQueue> Queue_;
+
+ virtual void TearDown()
+ {
+ if (Queue_.HasValue()) {
+ Queue_->Shutdown();
+ }
+ }
+};
+
+TEST_F(TTimerTest, CpuEmpty)
+{
+ TValue cpu = 0;
+ BIND([&] {
+ TFiberWallTimer cpuTimer;
+ cpu = cpuTimer.GetElapsedValue();
+ })
+ .AsyncVia(Queue_->GetInvoker())
+ .Run()
+ .Get();
+
+ EXPECT_LT(cpu, 10'000);
+}
+
+TEST_F(TTimerTest, CpuWallCompare)
+{
+ TValue cpu = 0;
+ TValue wall = 0;
+ BIND([&] {
+ TFiberWallTimer cpuTimer;
+ TWallTimer wallTimer;
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ cpu = cpuTimer.GetElapsedValue();
+ wall = wallTimer.GetElapsedValue();
+ })
+ .AsyncVia(Queue_->GetInvoker())
+ .Run()
+ .Get();
+
+ EXPECT_LT(cpu, 10'000);
+ EXPECT_GT(wall, 80'000);
+ EXPECT_LT(wall, 120'000);
+}
+
+TEST_F(TTimerTest, PersistAndRestoreActive)
+{
+ TWallTimer wallTimer;
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ PersistWaitRestore(wallTimer);
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ auto wall = wallTimer.GetElapsedValue();
+
+ EXPECT_GT(wall, 150'000);
+ EXPECT_LT(wall, 250'000);
+}
+
+TEST_F(TTimerTest, PersistAndRestoreNonActive)
+{
+ TWallTimer wallTimer;
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ wallTimer.Stop();
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ PersistWaitRestore(wallTimer);
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ wallTimer.Start();
+
+ TDelayedExecutor::WaitForDuration(SleepQuantum);
+
+ auto wall = wallTimer.GetElapsedValue();
+
+ EXPECT_GT(wall, 150'000);
+ EXPECT_LT(wall, 250'000);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/unittests/timing_ut.cpp b/yt/yt/core/profiling/unittests/timing_ut.cpp
new file mode 100644
index 0000000000..4bd23b88e3
--- /dev/null
+++ b/yt/yt/core/profiling/unittests/timing_ut.cpp
@@ -0,0 +1,52 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/profiling/timing.h>
+#include <yt/yt/core/profiling/tscp.h>
+
+namespace NYT::NProfiling {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+i64 DiffMS(T a, T b)
+{
+ return a >= b
+ ? static_cast<i64>(a.MilliSeconds()) - static_cast<i64>(b.MilliSeconds())
+ : DiffMS(b ,a);
+}
+
+TEST(TTimingTest, GetInstant)
+{
+ EXPECT_LE(DiffMS(GetInstant(), TInstant::Now()), 10);
+}
+
+TEST(TTimingTest, InstantVSCpuInstant)
+{
+ auto instant1 = TInstant::Now();
+ auto cpuInstant = InstantToCpuInstant(instant1);
+ auto instant2 = CpuInstantToInstant(cpuInstant);
+ EXPECT_LE(DiffMS(instant1, instant2), 10);
+}
+
+TEST(TTimingTest, DurationVSCpuDuration)
+{
+ auto cpuInstant1 = GetCpuInstant();
+ constexpr auto duration1 = TDuration::MilliSeconds(100);
+ Sleep(duration1);
+ auto cpuInstant2 = GetCpuInstant();
+ auto duration2 = CpuDurationToDuration(cpuInstant2 - cpuInstant1);
+ EXPECT_LE(DiffMS(duration1, duration2), 10);
+}
+
+TEST(TTimingTest, TTscp_Get)
+{
+ auto cpuInstant = GetCpuInstant();
+ auto tscp = TTscp::Get();
+ EXPECT_GE(tscp.Instant, cpuInstant);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NProfiling
diff --git a/yt/yt/core/profiling/unittests/ya.make b/yt/yt/core/profiling/unittests/ya.make
new file mode 100644
index 0000000000..bbe2bd3ce5
--- /dev/null
+++ b/yt/yt/core/profiling/unittests/ya.make
@@ -0,0 +1,40 @@
+GTEST(unittester-core-profiling)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ timer_ut.cpp
+ timing_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/rpc/authentication_identity.cpp b/yt/yt/core/rpc/authentication_identity.cpp
new file mode 100644
index 0000000000..73687cfdb7
--- /dev/null
+++ b/yt/yt/core/rpc/authentication_identity.cpp
@@ -0,0 +1,140 @@
+#include "authentication_identity.h"
+
+#include <yt/yt/core/concurrency/fls.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NRpc {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAuthenticationIdentity::TAuthenticationIdentity(TString user, TString userTag)
+ : User(std::move(user))
+ , UserTag(std::move(userTag))
+{ }
+
+bool TAuthenticationIdentity::operator==(const TAuthenticationIdentity& other) const
+{
+ return
+ User == other.User &&
+ UserTag == other.UserTag;
+}
+
+bool TAuthenticationIdentity::operator!=(const TAuthenticationIdentity& other) const
+{
+ return !(*this == other);
+}
+
+void Serialize(const TAuthenticationIdentity& identity, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("user").Value(identity.User)
+ .DoIf(!identity.UserTag.empty() && identity.UserTag != identity.User, [&] (TFluentMap fluent) {
+ fluent
+ .Item("user_tag").Value(identity.UserTag);
+ })
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+const TAuthenticationIdentity** GetCurrentAuthenticationIdentityPtr()
+{
+ static NConcurrency::TFlsSlot<const TAuthenticationIdentity*> Slot;
+ return Slot.GetOrCreate();
+}
+
+} // namespace
+
+const TAuthenticationIdentity& GetCurrentAuthenticationIdentity()
+{
+ const auto* identity = *GetCurrentAuthenticationIdentityPtr();
+ return identity ? *identity : GetRootAuthenticationIdentity();
+}
+
+const TAuthenticationIdentity& GetRootAuthenticationIdentity()
+{
+ static const TAuthenticationIdentity RootIdentity{
+ RootUserName,
+ RootUserName
+ };
+ return RootIdentity;
+}
+
+void SetCurrentAuthenticationIdentity(const TAuthenticationIdentity* identity)
+{
+ *GetCurrentAuthenticationIdentityPtr() = identity;
+}
+
+void FormatValue(TStringBuilderBase* builder, const TAuthenticationIdentity& value, TStringBuf /*format*/)
+{
+ builder->AppendFormat("{User: %v", value.User);
+ if (!value.UserTag.Empty() && value.UserTag != value.User) {
+ builder->AppendFormat(", UserTag: %v", value.UserTag);
+ }
+ builder->AppendChar('}');
+}
+
+TString ToString(const TAuthenticationIdentity& value)
+{
+ return ToStringViaBuilder(value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCurrentAuthenticationIdentityGuard::TCurrentAuthenticationIdentityGuard()
+ : OldIdentity_(nullptr)
+{ }
+
+TCurrentAuthenticationIdentityGuard::TCurrentAuthenticationIdentityGuard(
+ TCurrentAuthenticationIdentityGuard&& other)
+ : OldIdentity_(other.OldIdentity_)
+{
+ other.OldIdentity_ = nullptr;
+}
+
+TCurrentAuthenticationIdentityGuard::TCurrentAuthenticationIdentityGuard(
+ const TAuthenticationIdentity* newIdentity)
+{
+ OldIdentity_ = &GetCurrentAuthenticationIdentity();
+ SetCurrentAuthenticationIdentity(newIdentity);
+}
+
+TCurrentAuthenticationIdentityGuard::~TCurrentAuthenticationIdentityGuard()
+{
+ Release();
+}
+
+TCurrentAuthenticationIdentityGuard& TCurrentAuthenticationIdentityGuard::operator=(
+ TCurrentAuthenticationIdentityGuard&& other)
+{
+ Release();
+ OldIdentity_ = other.OldIdentity_;
+ other.OldIdentity_ = nullptr;
+ return *this;
+}
+
+void TCurrentAuthenticationIdentityGuard::Release()
+{
+ if (OldIdentity_) {
+ *GetCurrentAuthenticationIdentityPtr() = OldIdentity_;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+size_t THash<NYT::NRpc::TAuthenticationIdentity>::operator()(const NYT::NRpc::TAuthenticationIdentity& value) const
+{
+ size_t result = 0;
+ NYT::HashCombine(result, value.User);
+ NYT::HashCombine(result, value.UserTag);
+ return result;
+}
diff --git a/yt/yt/core/rpc/authentication_identity.h b/yt/yt/core/rpc/authentication_identity.h
new file mode 100644
index 0000000000..284ffe90b3
--- /dev/null
+++ b/yt/yt/core/rpc/authentication_identity.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAuthenticationIdentity
+{
+ TAuthenticationIdentity() = default;
+ explicit TAuthenticationIdentity(TString user, TString userTag = {});
+
+ bool operator==(const TAuthenticationIdentity& other) const;
+ bool operator!=(const TAuthenticationIdentity& other) const;
+
+ TString User;
+ TString UserTag;
+};
+
+//! Returns the current identity.
+//! If none is explicitly set then returns the root identity.
+const TAuthenticationIdentity& GetCurrentAuthenticationIdentity();
+
+//! Sets the current identity.
+//! The passed identity is not copied, just a pointer is updated.
+//! The caller must ensure a proper lifetime of the installed identity.
+void SetCurrentAuthenticationIdentity(const TAuthenticationIdentity* identity);
+
+//! Returns the root identity, which is the default one.
+const TAuthenticationIdentity& GetRootAuthenticationIdentity();
+
+void FormatValue(TStringBuilderBase* builder, const TAuthenticationIdentity& value, TStringBuf format);
+TString ToString(const TAuthenticationIdentity& value);
+
+void Serialize(const TAuthenticationIdentity& identity, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCurrentAuthenticationIdentityGuard
+ : private TNonCopyable
+{
+public:
+ TCurrentAuthenticationIdentityGuard();
+ explicit TCurrentAuthenticationIdentityGuard(const TAuthenticationIdentity* newIdentity);
+ TCurrentAuthenticationIdentityGuard(TCurrentAuthenticationIdentityGuard&& other);
+ ~TCurrentAuthenticationIdentityGuard();
+
+ TCurrentAuthenticationIdentityGuard& operator=(TCurrentAuthenticationIdentityGuard&& other);
+
+private:
+ const TAuthenticationIdentity* OldIdentity_;
+
+ void Release();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+template <>
+struct THash<NYT::NRpc::TAuthenticationIdentity>
+{
+ size_t operator()(const NYT::NRpc::TAuthenticationIdentity& value) const;
+};
diff --git a/yt/yt/core/rpc/authenticator.cpp b/yt/yt/core/rpc/authenticator.cpp
new file mode 100644
index 0000000000..a93fe2bc8b
--- /dev/null
+++ b/yt/yt/core/rpc/authenticator.cpp
@@ -0,0 +1,85 @@
+#include "authenticator.h"
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompositeAuthenticator
+ : public IAuthenticator
+{
+public:
+ explicit TCompositeAuthenticator(std::vector<IAuthenticatorPtr> authenticators)
+ : Authenticators_(std::move(authenticators))
+ { }
+
+ bool CanAuthenticate(const TAuthenticationContext& context) override
+ {
+ for (const auto& authenticator : Authenticators_) {
+ if (authenticator->CanAuthenticate(context)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ TFuture<TAuthenticationResult> AsyncAuthenticate(
+ const TAuthenticationContext& context) override
+ {
+ for (const auto& authenticator : Authenticators_) {
+ if (authenticator->CanAuthenticate(context)) {
+ return authenticator->AsyncAuthenticate(context);
+ }
+ }
+ // Hypothetically some authenticator may change its opinion on whether it can authenticate request (e.g.
+ // due to dynamic configuration change), so we report an error instead of crashing.
+ return MakeFuture<TAuthenticationResult>(TError(
+ NYT::NRpc::EErrorCode::AuthenticationError,
+ "Request is missing credentials"));
+ }
+
+private:
+ const std::vector<IAuthenticatorPtr> Authenticators_;
+};
+
+IAuthenticatorPtr CreateCompositeAuthenticator(
+ std::vector<IAuthenticatorPtr> authenticators)
+{
+ return New<TCompositeAuthenticator>(std::move(authenticators));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNoopAuthenticator
+ : public IAuthenticator
+{
+public:
+ bool CanAuthenticate(const TAuthenticationContext& /*context*/) override
+ {
+ return true;
+ }
+
+ TFuture<TAuthenticationResult> AsyncAuthenticate(
+ const TAuthenticationContext& context) override
+ {
+ static const auto Realm = TString("noop");
+ static const auto UserTicket = TString();
+ TAuthenticationResult result{
+ context.Header->has_user() ? context.Header->user() : RootUserName,
+ Realm,
+ UserTicket
+ };
+ return MakeFuture<TAuthenticationResult>(result);
+ }
+};
+
+IAuthenticatorPtr CreateNoopAuthenticator()
+{
+ return New<TNoopAuthenticator>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
diff --git a/yt/yt/core/rpc/authenticator.h b/yt/yt/core/rpc/authenticator.h
new file mode 100644
index 0000000000..3648e23c2b
--- /dev/null
+++ b/yt/yt/core/rpc/authenticator.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TAuthenticationResult
+{
+ TString User;
+ TString Realm;
+ TString UserTicket;
+};
+
+struct TAuthenticationContext
+{
+ const NRpc::NProto::TRequestHeader* Header;
+ NNet::TNetworkAddress UserIP;
+ //! True iff the request comes from the current process.
+ bool IsLocal;
+};
+
+struct IAuthenticator
+ : public virtual TRefCounted
+{
+ //! Returns true if #context contains credentials that can be parsed by
+ //! this authenticator.
+ //!
+ //! If this method returns true, AsyncAuthenticate may still return an error,
+ //! although in such case the composite authenticator will not proceed with
+ //! another underlying authenticator.
+ //!
+ //! Also, this method may be invoked multiple times for the same context.
+ //! Moreover, the returned values are allowed to be different which is useful
+ //! in rare occasions, e.g. in event of dynamic configuration change.
+ virtual bool CanAuthenticate(const TAuthenticationContext& context) = 0;
+
+ //! Validates authentication credentials in #context.
+ //! Returns either an error or authentication result containing
+ //! the actual (and validated) username.
+ //!
+ //! Should be called only after CanAuthenticate(context) returns true.
+ virtual TFuture<TAuthenticationResult> AsyncAuthenticate(
+ const TAuthenticationContext& context) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAuthenticator)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the authenticator that sequentially forwards the request to
+//! all elements of #authenticators until a non-null response arrives.
+//! If no such response is discovered, an error is returned.
+IAuthenticatorPtr CreateCompositeAuthenticator(
+ std::vector<IAuthenticatorPtr> authenticators);
+
+//! Returns the authenticator that accepts any request.
+IAuthenticatorPtr CreateNoopAuthenticator();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/balancing_channel.cpp b/yt/yt/core/rpc/balancing_channel.cpp
new file mode 100644
index 0000000000..82ee29216f
--- /dev/null
+++ b/yt/yt/core/rpc/balancing_channel.cpp
@@ -0,0 +1,327 @@
+#include "balancing_channel.h"
+#include "channel.h"
+#include "caching_channel_factory.h"
+#include "config.h"
+#include "roaming_channel.h"
+#include "dynamic_channel_pool.h"
+#include "dispatcher.h"
+#include "hedging_channel.h"
+
+#include <yt/yt/core/service_discovery/service_discovery.h>
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/misc/hedging_manager.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT::NRpc {
+
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NServiceDiscovery;
+using namespace NNet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TBalancingChannelSubprovider)
+DECLARE_REFCOUNTED_CLASS(TBalancingChannelProvider)
+
+class TBalancingChannelSubprovider
+ : public TRefCounted
+{
+public:
+ TBalancingChannelSubprovider(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TString serviceName,
+ TDiscoverRequestHook discoverRequestHook)
+ : Config_(std::move(config))
+ , EndpointDescription_(endpointDescription)
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Items(*endpointAttributes)
+ .Item("service").Value(serviceName)
+ .EndMap()))
+ , ServiceName_(std::move(serviceName))
+ , DiscoverRequestHook_(std::move(discoverRequestHook))
+ , Pool_(New<TDynamicChannelPool>(
+ Config_,
+ std::move(channelFactory),
+ EndpointDescription_,
+ EndpointAttributes_,
+ ServiceName_,
+ DiscoverRequestHook_))
+ {
+ if (Config_->Addresses) {
+ ConfigureFromAddresses();
+ } else if (Config_->Endpoints) {
+ ConfigureFromEndpoints();
+ } else {
+ // Must not happen for a properly validated configuration.
+ Pool_->SetPeerDiscoveryError(TError("No endpoints configured"));
+ }
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& request)
+ {
+ auto hedgingOptions = Config_->HedgingDelay
+ ? std::make_optional(
+ THedgingChannelOptions{
+ .HedgingManager = CreateSimpleHedgingManager(*Config_->HedgingDelay),
+ .CancelPrimaryOnHedging = Config_->CancelPrimaryRequestOnHedging,
+ })
+ : std::nullopt;
+ return Pool_->GetChannel(request, hedgingOptions);
+ }
+
+ TFuture<IChannelPtr> GetChannel()
+ {
+ return Pool_->GetRandomChannel();
+ }
+
+ void Terminate(const TError& error)
+ {
+ Pool_->Terminate(error);
+ }
+
+private:
+ const TBalancingChannelConfigPtr Config_;
+ const TString EndpointDescription_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+ const TString ServiceName_;
+ const TDiscoverRequestHook DiscoverRequestHook_;
+
+ const TDynamicChannelPoolPtr Pool_;
+
+ IServiceDiscoveryPtr ServiceDiscovery_;
+ TPeriodicExecutorPtr EndpointsUpdateExecutor_;
+
+ void ConfigureFromAddresses()
+ {
+ Pool_->SetPeers(*Config_->Addresses);
+ }
+
+ void ConfigureFromEndpoints()
+ {
+ ServiceDiscovery_ = TDispatcher::Get()->GetServiceDiscovery();
+ if (!ServiceDiscovery_) {
+ Pool_->SetPeerDiscoveryError(TError("No Service Discovery is configured"));
+ return;
+ }
+
+ EndpointsUpdateExecutor_ = New<TPeriodicExecutor>(
+ TDispatcher::Get()->GetHeavyInvoker(),
+ BIND(&TBalancingChannelSubprovider::UpdateEndpoints, MakeWeak(this)),
+ Config_->Endpoints->UpdatePeriod);
+ EndpointsUpdateExecutor_->Start();
+ }
+
+ void UpdateEndpoints()
+ {
+ std::vector<TFuture<TEndpointSet>> asyncEndpointSets;
+ for (const auto& cluster : Config_->Endpoints->Clusters) {
+ asyncEndpointSets.push_back(ServiceDiscovery_->ResolveEndpoints(
+ cluster,
+ Config_->Endpoints->EndpointSetId));
+ }
+
+ AllSet(asyncEndpointSets)
+ .Subscribe(BIND(&TBalancingChannelSubprovider::OnEndpointsResolved, MakeWeak(this)));
+ }
+
+ void OnEndpointsResolved(const TErrorOr<std::vector<TErrorOr<TEndpointSet>>>& endpointSetsOrError)
+ {
+ const auto& endpointSets = endpointSetsOrError.ValueOrThrow();
+
+ std::vector<TString> allAddresses;
+ std::vector<TError> errors;
+ for (const auto& endpointSetOrError : endpointSets) {
+ if (!endpointSetOrError.IsOK()) {
+ errors.push_back(endpointSetOrError);
+ YT_LOG_WARNING(endpointSetOrError, "Could not resolve endpoints from cluster");
+ continue;
+ }
+
+ auto addresses = AddressesFromEndpointSet(endpointSetOrError.Value());
+ allAddresses.insert(allAddresses.end(), addresses.begin(), addresses.end());
+ }
+
+ if (errors.size() == endpointSets.size()) {
+ Pool_->SetPeerDiscoveryError(
+ TError("Endpoints could not be resolved in any cluster") << errors);
+ return;
+ }
+
+ Pool_->SetPeers(allAddresses);
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TBalancingChannelSubprovider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBalancingChannelProvider
+ : public IRoamingChannelProvider
+{
+public:
+ TBalancingChannelProvider(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TDiscoverRequestHook discoverRequestHook)
+ : Config_(std::move(config))
+ , ChannelFactory_(std::move(channelFactory))
+ , DiscoverRequestHook_(std::move(discoverRequestHook))
+ , EndpointDescription_(Format("%v%v",
+ endpointDescription,
+ Config_->Addresses))
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("addresses").Value(Config_->Addresses)
+ .Items(*endpointAttributes)
+ .EndMap()))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription_;
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& request) override
+ {
+ if (Config_->Addresses && Config_->Addresses->size() == 1) {
+ // Disable discovery and balancing when just one address is given.
+ // This is vital for jobs since node's redirector is incapable of handling
+ // Discover requests properly.
+ return MakeFuture(ChannelFactory_->CreateChannel((*Config_->Addresses)[0]));
+ } else {
+ return GetSubprovider(request->GetService())->GetChannel(request);
+ }
+ }
+
+ TFuture<IChannelPtr> GetChannel(const TString& serviceName) override
+ {
+ return GetSubprovider(serviceName)->GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel() override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ void Terminate(const TError& error) override
+ {
+ std::vector<TBalancingChannelSubproviderPtr> subproviders;
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ for (const auto& [_, subprovider] : SubproviderMap_) {
+ subproviders.push_back(subprovider);
+ }
+ }
+
+ for (const auto& subprovider : subproviders) {
+ subprovider->Terminate(error);
+ }
+ }
+
+private:
+ const TBalancingChannelConfigPtr Config_;
+ const IChannelFactoryPtr ChannelFactory_;
+ const TDiscoverRequestHook DiscoverRequestHook_;
+
+ const TString EndpointDescription_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ THashMap<TString, TBalancingChannelSubproviderPtr> SubproviderMap_;
+
+
+ TBalancingChannelSubproviderPtr GetSubprovider(const TString& serviceName)
+ {
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ auto it = SubproviderMap_.find(serviceName);
+ if (it != SubproviderMap_.end()) {
+ return it->second;
+ }
+ }
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+ auto it = SubproviderMap_.find(serviceName);
+ if (it != SubproviderMap_.end()) {
+ return it->second;
+ }
+
+ auto subprovider = New<TBalancingChannelSubprovider>(
+ Config_,
+ ChannelFactory_,
+ EndpointDescription_,
+ EndpointAttributes_,
+ serviceName,
+ DiscoverRequestHook_);
+ YT_VERIFY(SubproviderMap_.emplace(serviceName, subprovider).second);
+ return subprovider;
+ }
+ }
+
+};
+
+DEFINE_REFCOUNTED_TYPE(TBalancingChannelProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateBalancingChannel(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TDiscoverRequestHook discoverRequestHook)
+{
+ auto channelProvider = CreateBalancingChannelProvider(
+ std::move(config),
+ std::move(channelFactory),
+ std::move(endpointDescription),
+ std::move(endpointAttributes),
+ std::move(discoverRequestHook));
+ return CreateRoamingChannel(channelProvider);
+}
+
+IRoamingChannelProviderPtr CreateBalancingChannelProvider(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TDiscoverRequestHook discoverRequestHook)
+{
+ YT_VERIFY(config);
+ YT_VERIFY(channelFactory);
+
+ return New<TBalancingChannelProvider>(
+ std::move(config),
+ std::move(channelFactory),
+ std::move(endpointDescription),
+ std::move(endpointAttributes),
+ std::move(discoverRequestHook));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/balancing_channel.h b/yt/yt/core/rpc/balancing_channel.h
new file mode 100644
index 0000000000..2241bcffb1
--- /dev/null
+++ b/yt/yt/core/rpc/balancing_channel.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateBalancingChannel(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ NYTree::IAttributeDictionaryPtr endpointAttributes,
+ TDiscoverRequestHook discoverRequestHook = {});
+
+IRoamingChannelProviderPtr CreateBalancingChannelProvider(
+ TBalancingChannelConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ NYTree::IAttributeDictionaryPtr endpointAttributes,
+ TDiscoverRequestHook discoverRequestHook = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/bus/channel.cpp b/yt/yt/core/rpc/bus/channel.cpp
new file mode 100644
index 0000000000..d9162b0336
--- /dev/null
+++ b/yt/yt/core/rpc/bus/channel.cpp
@@ -0,0 +1,1259 @@
+#include "channel.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/rpc/channel_detail.h>
+#include <yt/yt/core/rpc/client.h>
+#include <yt/yt/core/rpc/dispatcher.h>
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt/core/rpc/stream.h>
+#include <yt/yt/core/rpc/private.h>
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/bus/tcp/config.h>
+#include <yt/yt/core/bus/tcp/client.h>
+#include <yt/yt/core/bus/tcp/dispatcher.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/core/misc/finally.h>
+#include <yt/yt/core/misc/atomic_object.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <array>
+
+namespace NYT::NRpc::NBus {
+
+using namespace NYT::NBus;
+using namespace NYPath;
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusChannel
+ : public IChannel
+{
+public:
+ explicit TBusChannel(IBusClientPtr client)
+ : Client_(std::move(client))
+ {
+ YT_VERIFY(Client_);
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return Client_->GetEndpointDescription();
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return Client_->GetEndpointAttributes();
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ TSessionPtr session;
+
+ try {
+ session = GetOrCreateSession(options);
+ } catch (const std::exception& ex) {
+ responseHandler->HandleError(TError(ex));
+ return nullptr;
+ }
+
+ return session->SendRequest(
+ std::move(request),
+ std::move(responseHandler),
+ options);
+ }
+
+ void Terminate(const TError& error) override
+ {
+ YT_VERIFY(!error.IsOK());
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (TerminationFlag_.exchange(true)) {
+ return;
+ }
+
+ TerminationError_.Store(error);
+
+ std::vector<TSessionPtr> sessions;
+ for (auto& bucket : Buckets_) {
+ auto guard = WriterGuard(bucket.Lock);
+
+ for (auto&& session : bucket.Sessions) {
+ sessions.push_back(std::move(session));
+ }
+ bucket.Sessions.clear();
+
+ bucket.Terminated = true;
+ }
+
+ for (const auto& session : sessions) {
+ session->Terminate(error);
+ }
+
+ Terminated_.Fire(error);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Subscribe(callback);
+ }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Unsubscribe(callback);
+ }
+
+private:
+ class TSession;
+ using TSessionPtr = TIntrusivePtr<TSession>;
+
+ class TClientRequestControl;
+ using TClientRequestControlPtr = TIntrusivePtr<TClientRequestControl>;
+
+ const IBusClientPtr Client_;
+
+ TSingleShotCallbackList<void(const TError&)> Terminated_;
+
+ struct TBandBucket
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, Lock);
+ std::atomic<size_t> CurrentSessionIndex = 0;
+ std::vector<TSessionPtr> Sessions;
+ bool Terminated = false;
+ };
+
+ TEnumIndexedVector<EMultiplexingBand, TBandBucket> Buckets_;
+
+ std::atomic<bool> TerminationFlag_ = false;
+ TAtomicObject<TError> TerminationError_;
+
+ TSessionPtr GetOrCreateSession(const TSendOptions& options)
+ {
+ auto& bucket = Buckets_[options.MultiplexingBand];
+ auto index = options.MultiplexingParallelism <= 1 ? 0 : bucket.CurrentSessionIndex++ % options.MultiplexingParallelism;
+
+ // Fast path.
+ {
+ auto guard = ReaderGuard(bucket.Lock);
+ if (bucket.Sessions.size() > index) {
+ return bucket.Sessions[index];
+ }
+ }
+
+ std::vector<std::pair<IBusPtr, TSessionPtr>> results;
+
+ // Slow path.
+ {
+ auto guard = WriterGuard(bucket.Lock);
+
+ if (bucket.Sessions.size() > index) {
+ return bucket.Sessions[index];
+ }
+
+ if (bucket.Terminated) {
+ guard.Release();
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::TransportError, "Channel terminated")
+ << TerminationError_.Load();
+ }
+
+ bucket.Sessions.reserve(options.MultiplexingParallelism);
+ while (bucket.Sessions.size() <= index) {
+ auto session = New<TSession>(options.MultiplexingBand);
+ auto messageHandler = New<TMessageHandler>(session);
+ auto bus = Client_->CreateBus(
+ messageHandler,
+ {
+ .MultiplexingBand = options.MultiplexingBand
+ });
+ session->Initialize(bus);
+ bucket.Sessions.push_back(session);
+ results.emplace_back(std::move(bus), std::move(session));
+ }
+ }
+
+ for (const auto& [bus, session] : results) {
+ bus->SubscribeTerminated(BIND_NO_PROPAGATE(
+ &TBusChannel::OnBusTerminated,
+ MakeWeak(this),
+ MakeWeak(session),
+ options.MultiplexingBand));
+ }
+
+ return results.back().second;
+ }
+
+ void OnBusTerminated(const TWeakPtr<TSession>& session, EMultiplexingBand band, const TError& error)
+ {
+ auto session_ = session.Lock();
+ if (!session_) {
+ return;
+ }
+
+ auto& bucket = Buckets_[band];
+
+ {
+ auto guard = WriterGuard(bucket.Lock);
+
+ for (size_t index = 0; index < bucket.Sessions.size(); ++index) {
+ if (bucket.Sessions[index] == session_) {
+ std::swap(bucket.Sessions[index], bucket.Sessions.back());
+ bucket.Sessions.pop_back();
+ break;
+ }
+ }
+ }
+
+ session_->Terminate(error);
+ }
+
+ //! Provides a weak wrapper around a session and breaks the cycle
+ //! between the session and its underlying bus.
+ class TMessageHandler
+ : public IMessageHandler
+ {
+ public:
+ explicit TMessageHandler(TSessionPtr session)
+ : Session_(std::move(session))
+ { }
+
+ void HandleMessage(TSharedRefArray message, IBusPtr replyBus) noexcept override
+ {
+ auto session_ = Session_.Lock();
+ if (session_) {
+ session_->HandleMessage(std::move(message), std::move(replyBus));
+ }
+ }
+
+ private:
+ const TWeakPtr<TSession> Session_;
+
+ };
+
+ //! Directs requests sent via a channel to go through its underlying bus.
+ //! Terminates when the underlying bus does so.
+ class TSession
+ : public IMessageHandler
+ {
+ public:
+ explicit TSession(EMultiplexingBand band)
+ : TosLevel_(TTcpDispatcher::Get()->GetTosLevelForBand(band))
+ { }
+
+ void Initialize(IBusPtr bus)
+ {
+ YT_ASSERT(bus);
+ Bus_ = std::move(bus);
+ Bus_->SetTosLevel(TosLevel_);
+ }
+
+ void Terminate(const TError& error)
+ {
+ YT_VERIFY(!error.IsOK());
+
+ if (TerminationFlag_.exchange(true)) {
+ return;
+ }
+
+ TerminationError_.Store(error);
+
+ std::vector<std::tuple<TClientRequestControlPtr, IClientResponseHandlerPtr>> existingRequests;
+
+ // Mark the channel as terminated to disallow any further usage.
+ for (auto& bucket : RequestBuckets_) {
+ auto guard = Guard(bucket.Lock);
+
+ bucket.Terminated = true;
+
+ existingRequests.reserve(bucket.ActiveRequestMap.size());
+ for (auto& [requestId, requestControl] : bucket.ActiveRequestMap) {
+ auto responseHandler = requestControl->Finalize(guard);
+ existingRequests.emplace_back(std::move(requestControl), std::move(responseHandler));
+ }
+
+ bucket.ActiveRequestMap.clear();
+ }
+
+ for (const auto& existingRequest : existingRequests) {
+ NotifyError(
+ std::get<0>(existingRequest),
+ std::get<1>(existingRequest),
+ TStringBuf("Request failed due to channel termination"),
+ error);
+ }
+ }
+
+ IClientRequestControlPtr SendRequest(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options)
+ {
+ YT_VERIFY(request);
+ YT_VERIFY(responseHandler);
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto requestControl = New<TClientRequestControl>(
+ this,
+ request,
+ options,
+ std::move(responseHandler));
+
+ {
+ // NB: Requests without timeout are rare but may occur.
+ // For these requests we still need to register a timeout cookie with TDelayedExecutor
+ // since this also provides proper cleanup and cancelation when global shutdown happens.
+ auto effectiveTimeout = options.Timeout.value_or(TDuration::Hours(24));
+ auto timeoutCookie = TDelayedExecutor::Submit(
+ BIND(&TSession::HandleTimeout, MakeWeak(this), requestControl),
+ effectiveTimeout,
+ TDispatcher::Get()->GetHeavyInvoker());
+ requestControl->SetTimeoutCookie(std::move(timeoutCookie));
+ }
+
+ if (auto readyFuture = GetBusReadyFuture()) {
+ YT_LOG_DEBUG("Waiting for bus to become ready (RequestId: %v, Method: %v.%v)",
+ requestControl->GetRequestId(),
+ requestControl->GetService(),
+ requestControl->GetMethod());
+
+ readyFuture.Subscribe(BIND(
+ [
+ =,
+ this,
+ this_ = MakeStrong(this),
+ request = std::move(request)
+ ] (const TError& error) {
+ if (!BusReady_.exchange(true)) {
+ YT_LOG_DEBUG(error, "Bus has become ready (Endpoint: %v)",
+ Bus_->GetEndpointDescription());
+ }
+ DoSendRequest(
+ std::move(request),
+ requestControl,
+ options);
+ })
+ .Via(TDispatcher::Get()->GetHeavyInvoker()));
+ } else {
+ DoSendRequest(
+ std::move(request),
+ requestControl,
+ options);
+ }
+
+ return requestControl;
+ }
+
+ void Cancel(const TClientRequestControlPtr& requestControl)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto requestId = requestControl->GetRequestId();
+ auto* bucket = GetBucketForRequest(requestId);
+
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it == bucket->ActiveRequestMap.end()) {
+ YT_LOG_DEBUG("Attempt to cancel an unknown request, ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ if (requestControl != it->second) {
+ YT_LOG_DEBUG("Attempt to cancel a resent request, ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ requestControl->ProfileCancel();
+ responseHandler = requestControl->Finalize(guard);
+ bucket->ActiveRequestMap.erase(it);
+ }
+
+ // YT-1639: Avoid long chain of recursive calls.
+ thread_local int Depth = 0;
+ constexpr int MaxDepth = 10;
+ if (Depth < MaxDepth) {
+ ++Depth;
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request canceled"),
+ TError(NYT::EErrorCode::Canceled, "Request canceled"));
+ --Depth;
+ } else {
+ TDispatcher::Get()->GetHeavyInvoker()->Invoke(BIND(
+ &TSession::NotifyError,
+ MakeStrong(this),
+ requestControl,
+ responseHandler,
+ TStringBuf("Request canceled"),
+ TError(NYT::EErrorCode::Canceled, "Request canceled")));
+ }
+
+ if (TerminationFlag_.load()) {
+ return;
+ }
+
+ const auto& realmId = requestControl->GetRealmId();
+ const auto& service = requestControl->GetService();
+ const auto& method = requestControl->GetMethod();
+
+ NProto::TRequestCancelationHeader header;
+ ToProto(header.mutable_request_id(), requestId);
+ header.set_service(service);
+ header.set_method(method);
+ if (realmId) {
+ ToProto(header.mutable_realm_id(), realmId);
+ }
+
+ auto message = CreateRequestCancelationMessage(header);
+ YT_UNUSED_FUTURE(Bus_->Send(std::move(message), NBus::TSendOptions(EDeliveryTrackingLevel::None)));
+ }
+
+ TFuture<void> SendStreamingPayload(
+ const TClientRequestControlPtr& requestControl,
+ const TStreamingPayload& payload)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (TerminationFlag_.load()) {
+ return MakeFuture(TError(NRpc::EErrorCode::TransportError, "Session is terminated"));
+ }
+
+ auto requestId = requestControl->GetRequestId();
+ const auto& realmId = requestControl->GetRealmId();
+ const auto& service = requestControl->GetService();
+ const auto& method = requestControl->GetMethod();
+
+ NProto::TStreamingPayloadHeader header;
+ ToProto(header.mutable_request_id(), requestId);
+ header.set_service(service);
+ header.set_method(method);
+ if (realmId) {
+ ToProto(header.mutable_realm_id(), realmId);
+ }
+ header.set_sequence_number(payload.SequenceNumber);
+ header.set_codec(static_cast<int>(payload.Codec));
+
+ auto message = CreateStreamingPayloadMessage(header, payload.Attachments);
+ NBus::TSendOptions options;
+ options.TrackingLevel = EDeliveryTrackingLevel::Full;
+ return Bus_->Send(std::move(message), options);
+ }
+
+ TFuture<void> SendStreamingFeedback(
+ const TClientRequestControlPtr& requestControl,
+ const TStreamingFeedback& feedback)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (TerminationFlag_.load()) {
+ return MakeFuture(TError(NRpc::EErrorCode::TransportError, "Session is terminated"));
+ }
+
+ auto requestId = requestControl->GetRequestId();
+ const auto& realmId = requestControl->GetRealmId();
+ const auto& service = requestControl->GetService();
+ const auto& method = requestControl->GetMethod();
+
+ NProto::TStreamingFeedbackHeader header;
+ ToProto(header.mutable_request_id(), requestId);
+ header.set_service(service);
+ header.set_method(method);
+ if (realmId) {
+ ToProto(header.mutable_realm_id(), realmId);
+ }
+ header.set_read_position(feedback.ReadPosition);
+
+ auto message = CreateStreamingFeedbackMessage(header);
+ NBus::TSendOptions options;
+ options.TrackingLevel = EDeliveryTrackingLevel::Full;
+ return Bus_->Send(std::move(message), options);
+ }
+
+ void HandleTimeout(const TClientRequestControlPtr& requestControl, bool aborted)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto requestId = requestControl->GetRequestId();
+ auto* bucket = GetBucketForRequest(requestId);
+
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ if (!requestControl->IsActive(guard)) {
+ return;
+ }
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it != bucket->ActiveRequestMap.end() && requestControl == it->second) {
+ bucket->ActiveRequestMap.erase(it);
+ } else {
+ YT_LOG_DEBUG("Timeout occurred for an unknown or resent request (RequestId: %v)",
+ requestId);
+ }
+
+ requestControl->ProfileTimeout();
+ responseHandler = requestControl->Finalize(guard);
+ }
+
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request timed out"),
+ TError(NYT::EErrorCode::Timeout, aborted
+ ? "Request timed out or timer was aborted"
+ : "Request timed out"));
+ }
+
+ void HandleAcknowledgementTimeout(const TClientRequestControlPtr& requestControl, bool aborted)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (aborted) {
+ return;
+ }
+
+ auto requestId = requestControl->GetRequestId();
+ auto* bucket = GetBucketForRequest(requestId);
+
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ if (!requestControl->IsActive(guard)) {
+ return;
+ }
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it != bucket->ActiveRequestMap.end() && requestControl == it->second) {
+ bucket->ActiveRequestMap.erase(it);
+ } else {
+ YT_LOG_DEBUG("Acknowledgement timeout occurred for an unknown or resent request (RequestId: %v)",
+ requestId);
+ }
+
+ requestControl->ProfileTimeout();
+ responseHandler = requestControl->Finalize(guard);
+ }
+
+ auto error = TError(NYT::EErrorCode::Timeout, "Request acknowledgement timed out");
+
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request acknowledgement timed out"),
+ error);
+
+ if (TerminationFlag_.load()) {
+ return;
+ }
+
+ Bus_->Terminate(error);
+ }
+
+ void HandleMessage(TSharedRefArray message, IBusPtr /*replyBus*/) noexcept override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto messageType = GetMessageType(message);
+ switch (messageType) {
+ case EMessageType::Response:
+ OnResponseMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingPayload:
+ OnStreamingPayloadMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingFeedback:
+ OnStreamingFeedbackMessage(std::move(message));
+ break;
+
+ default:
+ YT_LOG_ERROR("Incoming message has invalid type, ignored (Type: %x)",
+ static_cast<ui32>(messageType));
+ break;
+ }
+ }
+
+ private:
+ const TTosLevel TosLevel_;
+
+ IBusPtr Bus_;
+ std::atomic<bool> BusReady_ = false;
+
+ struct TBucket
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ IBusPtr Bus;
+ bool Terminated = false;
+ THashMap<TRequestId, TClientRequestControlPtr> ActiveRequestMap;
+ };
+
+ static constexpr size_t BucketCount = 64;
+
+ std::array<TBucket, BucketCount> RequestBuckets_;
+
+ std::atomic<bool> TerminationFlag_ = false;
+ TAtomicObject<TError> TerminationError_;
+
+
+ TFuture<void> GetBusReadyFuture()
+ {
+ if (Y_LIKELY(BusReady_.load(std::memory_order::relaxed))) {
+ return {};
+ }
+
+ auto future = Bus_->GetReadyFuture();
+ if (future.IsSet()) {
+ BusReady_.store(true);
+ return {};
+ }
+
+ return future;
+ }
+
+ void DoSendRequest(
+ IClientRequestPtr request,
+ TClientRequestControlPtr requestControl,
+ const TSendOptions& options)
+ {
+ auto& header = request->Header();
+ header.set_start_time(ToProto<i64>(TInstant::Now()));
+ if (options.Timeout) {
+ header.set_timeout(ToProto<i64>(*options.Timeout));
+ } else {
+ header.clear_timeout();
+ }
+
+ if (options.RequestHeavy) {
+ BIND(&IClientRequest::Serialize, request)
+ .AsyncVia(TDispatcher::Get()->GetHeavyInvoker())
+ .Run()
+ .Subscribe(BIND(
+ &TSession::OnRequestSerialized,
+ MakeStrong(this),
+ std::move(requestControl),
+ options));
+ } else {
+ try {
+ auto requestMessage = request->Serialize();
+ OnRequestSerialized(
+ std::move(requestControl),
+ options,
+ std::move(requestMessage));
+ } catch (const std::exception& ex) {
+ OnRequestSerialized(
+ std::move(requestControl),
+ options,
+ TError(ex));
+ }
+ }
+ }
+
+
+ TBucket* GetBucketForRequest(TRequestId requestId)
+ {
+ return &RequestBuckets_[requestId.Parts32[0] % BucketCount];
+ }
+
+
+ std::pair<IClientResponseHandlerPtr, NTracing::TCurrentTraceContextGuard> FindResponseHandlerAndTraceContextGuard(TRequestId requestId)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto* bucket = GetBucketForRequest(requestId);
+ auto guard = Guard(bucket->Lock);
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it == bucket->ActiveRequestMap.end()) {
+ return {nullptr, NTracing::TCurrentTraceContextGuard(nullptr)};
+ }
+
+ const auto& requestControl = it->second;
+ return {requestControl->GetResponseHandler(guard), requestControl->GetTraceContextGuard()};
+ }
+
+
+ void OnRequestSerialized(
+ const TClientRequestControlPtr& requestControl,
+ const TSendOptions& options,
+ TErrorOr<TSharedRefArray> requestMessageOrError)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (requestMessageOrError.IsOK()) {
+ auto requestMessageError = CheckBusMessageLimits(requestMessageOrError.Value());
+ if (!requestMessageError.IsOK()){
+ requestMessageOrError = requestMessageError;
+ }
+ }
+
+ auto requestId = requestControl->GetRequestId();
+ auto* bucket = GetBucketForRequest(requestId);
+
+ TClientRequestControlPtr existingRequestControl;
+ IClientResponseHandlerPtr existingResponseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ if (!requestControl->IsActive(guard)) {
+ return;
+ }
+
+ if (!requestMessageOrError.IsOK()) {
+ auto responseHandler = requestControl->Finalize(guard);
+ guard.Release();
+
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request serialization failed"),
+ TError(NRpc::EErrorCode::TransportError, "Request serialization failed")
+ << requestMessageOrError);
+ return;
+ }
+
+ if (bucket->Terminated) {
+ auto responseHandler = requestControl->Finalize(guard);
+ guard.Release();
+
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request is dropped because channel is terminated"),
+ TError(NRpc::EErrorCode::TransportError, "Channel terminated")
+ << TerminationError_.Load());
+ return;
+ }
+
+ // NB: We're OK with duplicate request ids.
+ auto [it, inserted] = bucket->ActiveRequestMap.emplace(requestId, requestControl);
+ if (!inserted) {
+ existingRequestControl = std::move(it->second);
+ existingResponseHandler = existingRequestControl->Finalize(guard);
+ it->second = requestControl;
+ }
+
+ if (options.AcknowledgementTimeout) {
+ auto timeoutCookie = TDelayedExecutor::Submit(
+ BIND(&TSession::HandleAcknowledgementTimeout, MakeWeak(this), requestControl),
+ *options.AcknowledgementTimeout,
+ TDispatcher::Get()->GetHeavyInvoker());
+ requestControl->SetAcknowledgementTimeoutCookie(std::move(timeoutCookie));
+ }
+ }
+
+ if (existingResponseHandler) {
+ NotifyError(
+ existingRequestControl,
+ existingResponseHandler,
+ "Request resent",
+ TError(NRpc::EErrorCode::TransportError, "Request resent"));
+ }
+
+ if (options.SendDelay) {
+ Sleep(*options.SendDelay);
+ }
+
+ const auto& requestMessage = requestMessageOrError.Value();
+
+ NBus::TSendOptions busOptions;
+ busOptions.TrackingLevel = options.AcknowledgementTimeout
+ ? EDeliveryTrackingLevel::Full
+ : EDeliveryTrackingLevel::ErrorOnly;
+ busOptions.ChecksummedPartCount = options.GenerateAttachmentChecksums
+ ? NBus::TSendOptions::AllParts
+ : 2; // RPC header + request body
+ Bus_->Send(requestMessage, busOptions).Subscribe(BIND(
+ &TSession::OnAcknowledgement,
+ MakeStrong(this),
+ options.AcknowledgementTimeout.has_value(),
+ requestId));
+
+ requestControl->ProfileRequest(requestMessage);
+
+ YT_LOG_DEBUG("Request sent (RequestId: %v, Method: %v.%v, Timeout: %v, TrackingLevel: %v, "
+ "ChecksummedPartCount: %v, MultiplexingBand: %v, Endpoint: %v, BodySize: %v, AttachmentsSize: %v)",
+ requestId,
+ requestControl->GetService(),
+ requestControl->GetMethod(),
+ requestControl->GetTimeout(),
+ busOptions.TrackingLevel,
+ busOptions.ChecksummedPartCount,
+ options.MultiplexingBand,
+ Bus_->GetEndpointDescription(),
+ GetMessageBodySize(requestMessage),
+ GetTotalMessageAttachmentSize(requestMessage));
+ }
+
+
+ void OnResponseMessage(TSharedRefArray message)
+ {
+ NProto::TResponseHeader header;
+ if (!TryParseResponseHeader(message, &header)) {
+ YT_LOG_ERROR("Error parsing response header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto* bucket = GetBucketForRequest(requestId);
+
+ TClientRequestControlPtr requestControl;
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ if (bucket->Terminated) {
+ YT_LOG_WARNING("Response received via a terminated channel (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it == bucket->ActiveRequestMap.end()) {
+ // This may happen when the other party responds to an already timed-out request.
+ YT_LOG_DEBUG("Response for an incorrect or obsolete request received (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ requestControl = std::move(it->second);
+ requestControl->ProfileReply(message);
+ responseHandler = requestControl->Finalize(guard);
+ bucket->ActiveRequestMap.erase(it);
+ }
+
+ const auto traceContextGuard = requestControl->GetTraceContextGuard();
+
+ {
+ TError error;
+ if (header.has_error()) {
+ error = FromProto<TError>(header.error());
+ }
+ if (error.IsOK()) {
+ NotifyResponse(
+ requestId,
+ requestControl,
+ responseHandler,
+ std::move(message));
+ } else {
+ requestControl->ProfileError(error);
+ if (error.GetCode() == EErrorCode::PoisonPill) {
+ YT_LOG_FATAL(error, "Poison pill received");
+ }
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request failed"),
+ error);
+ }
+ }
+ }
+
+ void OnStreamingPayloadMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingPayloadHeader header;
+ if (!ParseStreamingPayloadHeader(message, &header)) {
+ YT_LOG_ERROR("Error parsing streaming payload header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto sequenceNumber = header.sequence_number();
+ auto attachments = std::vector<TSharedRef>(message.Begin() + 1, message.End());
+
+ auto [responseHandler, traceContextGuard] = FindResponseHandlerAndTraceContextGuard(requestId);
+
+ if (!responseHandler) {
+ YT_LOG_ERROR("Received streaming payload for an unknown request; ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ if (attachments.empty()) {
+ responseHandler->HandleError(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Streaming payload without attachments"));
+ return;
+ }
+
+ NCompression::ECodec codec;
+ int intCodec = header.codec();
+ if (!TryEnumCast(intCodec, &codec)) {
+ responseHandler->HandleError(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Streaming payload codec %v is not supported",
+ intCodec));
+ return;
+ }
+
+ YT_LOG_DEBUG("Response streaming payload received (RequestId: %v, SequenceNumber: %v, Sizes: %v, "
+ "Codec: %v, Closed: %v)",
+ requestId,
+ sequenceNumber,
+ MakeFormattableView(attachments, [] (auto* builder, const auto& attachment) {
+ builder->AppendFormat("%v", GetStreamingAttachmentSize(attachment));
+ }),
+ codec,
+ !attachments.back());
+
+ TStreamingPayload payload{
+ codec,
+ sequenceNumber,
+ std::move(attachments)
+ };
+ responseHandler->HandleStreamingPayload(payload);
+ }
+
+ void OnStreamingFeedbackMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingFeedbackHeader header;
+ if (!ParseStreamingFeedbackHeader(message, &header)) {
+ YT_LOG_ERROR("Error parsing streaming feedback header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto readPosition = header.read_position();
+
+ auto [responseHandler, traceContextGuard] = FindResponseHandlerAndTraceContextGuard(requestId);
+
+ if (!responseHandler) {
+ YT_LOG_DEBUG("Received streaming feedback for an unknown request; ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ YT_LOG_DEBUG("Response streaming feedback received (RequestId: %v, ReadPosition: %v)",
+ requestId,
+ readPosition);
+
+ TStreamingFeedback feedback{
+ readPosition
+ };
+ responseHandler->HandleStreamingFeedback(feedback);
+ }
+
+ void OnAcknowledgement(bool requestAcknowledgement, TRequestId requestId, const TError& error)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (!requestAcknowledgement && error.IsOK()) {
+ return;
+ }
+
+ auto* bucket = GetBucketForRequest(requestId);
+
+ TClientRequestControlPtr requestControl;
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(bucket->Lock);
+
+ auto it = bucket->ActiveRequestMap.find(requestId);
+ if (it == bucket->ActiveRequestMap.end()) {
+ // This one may easily get the actual response before the acknowledgment.
+ YT_LOG_DEBUG(error, "Acknowledgment received for an unknown request, ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ requestControl = it->second;
+ requestControl->ResetAcknowledgementTimeoutCookie();
+ if (!error.IsOK()) {
+ responseHandler = requestControl->Finalize(guard);
+ bucket->ActiveRequestMap.erase(it);
+ } else {
+ requestControl->ProfileAcknowledgement();
+ responseHandler = requestControl->GetResponseHandler(guard);
+ }
+ }
+
+ if (error.IsOK()) {
+ NotifyAcknowledgement(requestId, responseHandler);
+ } else {
+ NotifyError(
+ requestControl,
+ responseHandler,
+ TStringBuf("Request acknowledgment failed"),
+ TError(NRpc::EErrorCode::TransportError, "Request acknowledgment failed")
+ << error);
+ }
+ }
+
+ void NotifyError(
+ const TClientRequestControlPtr& requestControl,
+ const IClientResponseHandlerPtr& responseHandler,
+ TStringBuf reason,
+ const TError& error) noexcept
+ {
+ YT_VERIFY(responseHandler);
+
+ auto detailedError = error
+ << TErrorAttribute("realm_id", requestControl->GetRealmId())
+ << TErrorAttribute("service", requestControl->GetService())
+ << TErrorAttribute("method", requestControl->GetMethod())
+ << TErrorAttribute("request_id", requestControl->GetRequestId())
+ << Bus_->GetEndpointAttributes();
+
+ if (requestControl->GetTimeout()) {
+ detailedError = detailedError
+ << TErrorAttribute("timeout", *requestControl->GetTimeout());
+ }
+
+ YT_LOG_DEBUG(detailedError, "%v (RequestId: %v)",
+ reason,
+ requestControl->GetRequestId());
+
+ responseHandler->HandleError(detailedError);
+ }
+
+ void NotifyAcknowledgement(
+ TRequestId requestId,
+ const IClientResponseHandlerPtr& responseHandler) noexcept
+ {
+ YT_LOG_DEBUG("Request acknowledged (RequestId: %v)", requestId);
+
+ responseHandler->HandleAcknowledgement();
+ }
+
+ void NotifyResponse(
+ TRequestId requestId,
+ const TClientRequestControlPtr& requestControl,
+ const IClientResponseHandlerPtr& responseHandler,
+ TSharedRefArray message) noexcept
+ {
+ YT_LOG_DEBUG("Response received (RequestId: %v, Method: %v.%v, TotalTime: %v, AttachmentsSize: %v)",
+ requestId,
+ requestControl->GetService(),
+ requestControl->GetMethod(),
+ requestControl->GetTotalTime(),
+ GetTotalMessageAttachmentSize(message));
+
+ responseHandler->HandleResponse(std::move(message), Bus_->GetEndpointAddress());
+ }
+ };
+
+ //! Controls a sent request.
+ class TClientRequestControl
+ : public TClientRequestPerformanceProfiler
+ {
+ public:
+ TClientRequestControl(
+ TSessionPtr session,
+ IClientRequestPtr request,
+ const TSendOptions& options,
+ IClientResponseHandlerPtr responseHandler)
+ : TClientRequestPerformanceProfiler(request->GetService(), request->GetMethod())
+ , Session_(std::move(session))
+ , RealmId_(request->GetRealmId())
+ , Service_(request->GetService())
+ , Method_(request->GetMethod())
+ , RequestId_(request->GetRequestId())
+ , Options_(options)
+ , ResponseHandler_(std::move(responseHandler))
+ , TraceContext_()
+ { }
+
+ ~TClientRequestControl()
+ {
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+ TDelayedExecutor::CancelAndClear(AcknowledgementTimeoutCookie_);
+ }
+
+ TRealmId GetRealmId() const
+ {
+ return RealmId_;
+ }
+
+ const TString& GetService() const
+ {
+ return Service_;
+ }
+
+ const TString& GetMethod() const
+ {
+ return Method_;
+ }
+
+ TRequestId GetRequestId() const
+ {
+ return RequestId_;
+ }
+
+ std::optional<TDuration> GetTimeout() const
+ {
+ return Options_.Timeout;
+ }
+
+ TDuration GetTotalTime() const
+ {
+ return TotalTime_;
+ }
+
+ NTracing::TCurrentTraceContextGuard GetTraceContextGuard() const
+ {
+ return TraceContext_.MakeTraceContextGuard();
+ }
+
+ bool IsActive(const TGuard<NThreading::TSpinLock>&) const
+ {
+ return static_cast<bool>(ResponseHandler_);
+ }
+
+ void SetTimeoutCookie(TDelayedExecutorCookie cookie)
+ {
+ YT_ASSERT(!TimeoutCookie_);
+ TimeoutCookie_ = std::move(cookie);
+ }
+
+ void SetAcknowledgementTimeoutCookie(TDelayedExecutorCookie cookie)
+ {
+ YT_ASSERT(!AcknowledgementTimeoutCookie_);
+ AcknowledgementTimeoutCookie_ = std::move(cookie);
+ }
+
+ void ResetAcknowledgementTimeoutCookie()
+ {
+ TDelayedExecutor::CancelAndClear(AcknowledgementTimeoutCookie_);
+ }
+
+ IClientResponseHandlerPtr GetResponseHandler(const TGuard<NThreading::TSpinLock>&)
+ {
+ return ResponseHandler_;
+ }
+
+ IClientResponseHandlerPtr Finalize(const TGuard<NThreading::TSpinLock>&)
+ {
+ TotalTime_ = ProfileComplete();
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+ TDelayedExecutor::CancelAndClear(AcknowledgementTimeoutCookie_);
+ return std::move(ResponseHandler_);
+ }
+
+ // IClientRequestControl overrides
+ void Cancel() override
+ {
+ Session_->Cancel(this);
+ }
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& payload) override
+ {
+ return Session_->SendStreamingPayload(this, payload);
+ }
+
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& feedback) override
+ {
+ return Session_->SendStreamingFeedback(this, feedback);
+ }
+
+ private:
+ const TSessionPtr Session_;
+ const TRealmId RealmId_;
+ const TString Service_;
+ const TString Method_;
+ const TRequestId RequestId_;
+ const TSendOptions Options_;
+
+ TDelayedExecutorCookie TimeoutCookie_;
+ TDelayedExecutorCookie AcknowledgementTimeoutCookie_;
+ IClientResponseHandlerPtr ResponseHandler_;
+
+ TDuration TotalTime_;
+
+ NYT::NTracing::TTraceContextHandler TraceContext_;
+ };
+};
+
+IChannelPtr CreateBusChannel(IBusClientPtr client)
+{
+ YT_VERIFY(client);
+
+ return New<TBusChannel>(std::move(client));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTcpBusChannelFactory
+ : public IChannelFactory
+{
+public:
+ explicit TTcpBusChannelFactory(TBusConfigPtr config)
+ : Config_(ConvertToNode(std::move(config)))
+ { }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto config = TBusClientConfig::CreateTcp(address);
+ config->Load(Config_, /*postprocess*/ true, /*setDefaults*/ false);
+ auto client = CreateBusClient(std::move(config));
+ return CreateBusChannel(std::move(client));
+ }
+
+private:
+ const INodePtr Config_;
+};
+
+IChannelFactoryPtr CreateTcpBusChannelFactory(TBusConfigPtr config)
+{
+ return New<TTcpBusChannelFactory>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUdsBusChannelFactory
+ : public IChannelFactory
+{
+public:
+ explicit TUdsBusChannelFactory(TBusConfigPtr config)
+ : Config_(ConvertToNode(std::move(config)))
+ { }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto config = TBusClientConfig::CreateUds(address);
+ config->Load(Config_, /*postprocess*/ true, /*setDefaults*/ false);
+ auto client = CreateBusClient(std::move(config));
+ return CreateBusChannel(std::move(client));
+ }
+
+private:
+ const INodePtr Config_;
+};
+
+IChannelFactoryPtr CreateUdsBusChannelFactory(TBusConfigPtr config)
+{
+ return New<TUdsBusChannelFactory>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NBus
diff --git a/yt/yt/core/rpc/bus/channel.h b/yt/yt/core/rpc/bus/channel.h
new file mode 100644
index 0000000000..aaf02a8241
--- /dev/null
+++ b/yt/yt/core/rpc/bus/channel.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/tcp/public.h>
+
+namespace NYT::NRpc::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a channel implemented via Bus.
+IChannelPtr CreateBusChannel(NYT::NBus::IBusClientPtr client);
+
+//! Creates a factory for creating TCP Bus channels.
+IChannelFactoryPtr CreateTcpBusChannelFactory(NYT::NBus::TBusConfigPtr config);
+
+//! Creates a factory for creating Unix domain socket (UDS) Bus channels.
+IChannelFactoryPtr CreateUdsBusChannelFactory(NYT::NBus::TBusConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NBus
diff --git a/yt/yt/core/rpc/bus/public.h b/yt/yt/core/rpc/bus/public.h
new file mode 100644
index 0000000000..930ecd7144
--- /dev/null
+++ b/yt/yt/core/rpc/bus/public.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NRpc::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NBus
diff --git a/yt/yt/core/rpc/bus/server.cpp b/yt/yt/core/rpc/bus/server.cpp
new file mode 100644
index 0000000000..7dad52d91b
--- /dev/null
+++ b/yt/yt/core/rpc/bus/server.cpp
@@ -0,0 +1,286 @@
+#include "server.h"
+
+#include <yt/yt/core/rpc/server_detail.h>
+#include <yt/yt/core/rpc/private.h>
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/server.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt/core/rpc/stream.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NRpc::NBus {
+
+using namespace NConcurrency;
+using namespace NYT::NBus;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBusServer
+ : public TServerBase
+ , public IMessageHandler
+{
+public:
+ explicit TBusServer(IBusServerPtr busServer)
+ : TServerBase(RpcServerLogger.WithTag("BusServerId: %v", TGuid::Create()))
+ , BusServer_(std::move(busServer))
+ { }
+
+private:
+ IBusServerPtr BusServer_;
+
+
+ void DoStart() override
+ {
+ BusServer_->Start(this);
+ TServerBase::DoStart();
+ }
+
+ TFuture<void> DoStop(bool graceful) override
+ {
+ return TServerBase::DoStop(graceful)
+ .Apply(BIND([this, this_ = MakeStrong(this)] (const TError& error) {
+ // NB: Stop the bus server anyway.
+ auto future = BusServer_->Stop();
+ BusServer_.Reset();
+ error.ThrowOnError();
+ return future;
+ }));
+ }
+
+ void HandleMessage(TSharedRefArray message, IBusPtr replyBus) noexcept override
+ {
+ auto messageType = GetMessageType(message);
+ switch (messageType) {
+ case EMessageType::Request:
+ OnRequestMessage(std::move(message), std::move(replyBus));
+ break;
+
+ case EMessageType::RequestCancelation:
+ OnRequestCancelationMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingPayload:
+ OnStreamingPayloadMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingFeedback:
+ OnStreamingFeedbackMessage(std::move(message));
+ break;
+
+ default:
+ // Unable to reply, no request id is known.
+ // Let's just drop the message.
+ YT_LOG_ERROR("Incoming message has invalid type, ignored (Type: %x)",
+ static_cast<ui32>(messageType));
+ break;
+ }
+ }
+
+ void OnRequestMessage(TSharedRefArray message, IBusPtr replyBus)
+ {
+ auto header = std::make_unique<NProto::TRequestHeader>();
+ if (!ParseRequestHeader(message, header.get())) {
+ // Unable to reply, no request id is known.
+ // Let's just drop the message.
+ YT_LOG_ERROR("Error parsing request header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header->request_id());
+ const auto& serviceName = header->service();
+ auto realmId = FromProto<TRealmId>(header->realm_id());
+ auto tosLevel = header->tos_level();
+
+ if (message.Size() < 2) {
+ YT_LOG_ERROR("Too few request parts: expected >= 2, actual %v (RequestId: %v)",
+ message.Size(),
+ requestId);
+ return;
+ }
+
+ YT_LOG_DEBUG("Request received (RequestId: %v, Endpoint: %v)",
+ requestId,
+ replyBus->GetEndpointDescription());
+
+ auto replyWithError = [&] (const TError& error) {
+ YT_LOG_DEBUG(error);
+ auto response = CreateErrorResponseMessage(requestId, error);
+ YT_UNUSED_FUTURE(replyBus->Send(std::move(response), NBus::TSendOptions(EDeliveryTrackingLevel::None)));
+ };
+
+ if (!Started_) {
+ replyWithError(TError(
+ NRpc::EErrorCode::Unavailable,
+ "Server is not started")
+ << TErrorAttribute("realm_id", realmId)
+ << TErrorAttribute("endpoint", replyBus->GetEndpointDescription()));
+ return;
+ }
+
+ TServiceId serviceId(serviceName, realmId);
+ IServicePtr service;
+ try {
+ service = GetServiceOrThrow(serviceId);
+ } catch (const TErrorException& ex) {
+ replyWithError(TError(ex));
+ return;
+ }
+
+ replyBus->SetTosLevel(tosLevel);
+
+ YT_VERIFY(service);
+ service->HandleRequest(
+ std::move(header),
+ std::move(message),
+ std::move(replyBus));
+ }
+
+ void OnRequestCancelationMessage(TSharedRefArray message)
+ {
+ NProto::TRequestCancelationHeader header;
+ if (!ParseRequestCancelationHeader(message, &header)) {
+ // Unable to reply, no request id is known.
+ // Let's just drop the message.
+ YT_LOG_ERROR("Error parsing request cancelation header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ const auto& serviceName = header.service();
+ const auto& methodName = header.method();
+ auto realmId = FromProto<TRealmId>(header.realm_id());
+
+ TServiceId serviceId(serviceName, realmId);
+ auto service = FindService(serviceId);
+ if (!service) {
+ YT_LOG_DEBUG("Service is not registered (Service: %v, RealmId: %v, RequestId: %v)",
+ serviceName,
+ realmId,
+ requestId);
+ return;
+ }
+
+ YT_LOG_DEBUG("Request cancelation received (Method: %v.%v, RealmId: %v, RequestId: %v)",
+ serviceName,
+ methodName,
+ realmId,
+ requestId);
+
+ service->HandleRequestCancellation(requestId);
+ }
+
+ void OnStreamingPayloadMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingPayloadHeader header;
+ if (!ParseStreamingPayloadHeader(message, &header)) {
+ // Unable to reply, no request id is known.
+ // Let's just drop the message.
+ YT_LOG_ERROR("Error parsing request streaming payload header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto sequenceNumber = header.sequence_number();
+ auto attachments = std::vector<TSharedRef>(message.Begin() + 1, message.End());
+ const auto& serviceName = header.service();
+ auto realmId = FromProto<TRealmId>(header.realm_id());
+
+ TServiceId serviceId(serviceName, realmId);
+ auto service = FindService(serviceId);
+ if (!service) {
+ YT_LOG_DEBUG("Service is not registered (Service: %v, RealmId: %v, RequestId: %v)",
+ serviceName,
+ realmId,
+ requestId);
+ return;
+ }
+
+ if (attachments.empty()) {
+ YT_LOG_WARNING("Streaming payload without attachments; canceling request (RequestId: %v)",
+ requestId);
+ service->HandleRequestCancellation(requestId);
+ return;
+ }
+
+ NCompression::ECodec codec;
+ int intCodec = header.codec();
+ if (!TryEnumCast(intCodec, &codec)) {
+ YT_LOG_WARNING("Streaming payload codec is not supported; canceling request (RequestId: %v, Codec: %v)",
+ requestId,
+ intCodec);
+ service->HandleRequestCancellation(requestId);
+ return;
+ }
+
+ YT_LOG_DEBUG("Request streaming payload received (RequestId: %v, SequenceNumber: %v, Sizes: %v, "
+ "Codec: %v, Closed: %v)",
+ requestId,
+ sequenceNumber,
+ MakeFormattableView(attachments, [] (auto* builder, const auto& attachment) {
+ builder->AppendFormat("%v", GetStreamingAttachmentSize(attachment));
+ }),
+ codec,
+ !attachments.back());
+
+ TStreamingPayload payload{
+ codec,
+ sequenceNumber,
+ std::move(attachments)
+ };
+ service->HandleStreamingPayload(requestId, payload);
+ }
+
+ void OnStreamingFeedbackMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingFeedbackHeader header;
+ if (!ParseStreamingFeedbackHeader(message, &header)) {
+ // Unable to reply, no request id is known.
+ // Let's just drop the message.
+ YT_LOG_ERROR("Error parsing request streaming feedback header");
+ return;
+ }
+
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto readPosition = header.read_position();
+ auto attachments = std::vector<TSharedRef>(message.Begin() + 1, message.End());
+ const auto& serviceName = header.service();
+ auto realmId = FromProto<TRealmId>(header.realm_id());
+
+ TServiceId serviceId(serviceName, realmId);
+ auto service = FindService(serviceId);
+ if (!service) {
+ YT_LOG_DEBUG("Service is not registered (Service: %v, RealmId: %v, RequestId: %v)",
+ serviceName,
+ realmId,
+ requestId);
+ return;
+ }
+
+ YT_LOG_DEBUG("Request streaming feedback received (RequestId: %v, ReadPosition: %v)",
+ requestId,
+ readPosition);
+
+ TStreamingFeedback feedback{
+ readPosition
+ };
+ service->HandleStreamingFeedback(requestId, feedback);
+ }
+};
+
+IServerPtr CreateBusServer(NBus::IBusServerPtr busServer)
+{
+ return New<TBusServer>(busServer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NBus
diff --git a/yt/yt/core/rpc/bus/server.h b/yt/yt/core/rpc/bus/server.h
new file mode 100644
index 0000000000..43c11ed199
--- /dev/null
+++ b/yt/yt/core/rpc/bus/server.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/public.h>
+
+namespace NYT::NRpc::NBus {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServerPtr CreateBusServer(NYT::NBus::IBusServerPtr busServer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NBus
diff --git a/yt/yt/core/rpc/caching_channel_factory.cpp b/yt/yt/core/rpc/caching_channel_factory.cpp
new file mode 100644
index 0000000000..256077e714
--- /dev/null
+++ b/yt/yt/core/rpc/caching_channel_factory.cpp
@@ -0,0 +1,277 @@
+#include "caching_channel_factory.h"
+#include "channel.h"
+#include "channel_detail.h"
+#include "client.h"
+#include "dispatcher.h"
+#include "private.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/misc/mpsc_stack.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+using namespace NYTree;
+using namespace NYT::NBus;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto ExpirationCheckInterval = TDuration::Seconds(15);
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCachedChannel
+ : public TChannelWrapper
+{
+public:
+ TCachedChannel(
+ TCachingChannelFactory* factory,
+ IChannelPtr underlyingChannel,
+ const TString& address)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , Factory_(factory)
+ , Address_(address)
+ , LastActivityTime_(TInstant::Now())
+ {
+ UnderlyingChannel_->SubscribeTerminated(BIND(&TCachedChannel::OnTerminated, MakeWeak(this)));
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ Touch();
+ return TChannelWrapper::Send(
+ std::move(request),
+ std::move(responseHandler),
+ options);
+ }
+
+ void Touch()
+ {
+ LastActivityTime_.store(TInstant::Now());
+ }
+
+ TInstant GetLastActivityTime() const
+ {
+ return LastActivityTime_.load();
+ }
+
+private:
+ const TWeakPtr<TCachingChannelFactory> Factory_;
+ const TString Address_;
+
+ std::atomic<TInstant> LastActivityTime_;
+
+ void OnTerminated(const TError& error);
+};
+
+DECLARE_REFCOUNTED_CLASS(TCachedChannel)
+DEFINE_REFCOUNTED_TYPE(TCachedChannel)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCachingChannelFactory
+ : public IChannelFactory
+{
+public:
+ TCachingChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration idleChannelTtl)
+ : UnderlyingFactory_(std::move(underlyingFactory))
+ , IdleChannelTtl_(idleChannelTtl)
+ , ExpirationExecutor_(New<TPeriodicExecutor>(
+ TDispatcher::Get()->GetHeavyInvoker(),
+ BIND(&TCachingChannelFactory::CheckExpiredChannels, MakeWeak(this)),
+ std::min(ExpirationCheckInterval, IdleChannelTtl_)))
+ { }
+
+ void Initialize()
+ {
+ ExpirationExecutor_->Start();
+ }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ return DoCreateChannel(
+ address,
+ [&] { return UnderlyingFactory_->CreateChannel(address); });
+ }
+
+ void EvictChannel(const TString& address, IChannel* evictableChannel)
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ YT_LOG_DEBUG("Cached channel evicted (Endpoint: %v)",
+ evictableChannel->GetEndpointDescription());
+
+ if (auto it = WeakChannelMap_.find(address); it != WeakChannelMap_.end()) {
+ if (auto existingChannel = it->second.Lock(); existingChannel.Get() == evictableChannel) {
+ WeakChannelMap_.erase(it);
+ }
+ }
+
+ if (auto it = StrongChannelMap_.find(address); it != StrongChannelMap_.end()) {
+ if (const auto& existingChannel = it->second; existingChannel.Get() == evictableChannel) {
+ StrongChannelMap_.erase(it);
+ }
+ }
+ }
+
+private:
+ const IChannelFactoryPtr UnderlyingFactory_;
+ const TDuration IdleChannelTtl_;
+
+ TPeriodicExecutorPtr ExpirationExecutor_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ THashMap<TString, TCachedChannelPtr> StrongChannelMap_;
+ THashMap<TString, TWeakPtr<TCachedChannel>> WeakChannelMap_;
+
+ using TTtlItem = std::pair<TString, TWeakPtr<TCachedChannel>>;
+ TMpscStack<TTtlItem> TtlRegisterQueue_;
+ std::vector<TTtlItem> TtlCheckQueue_;
+
+ template <class TFactory>
+ IChannelPtr DoCreateChannel(const TString& address, const TFactory& factory)
+ {
+ {
+ auto readerGuard = ReaderGuard(SpinLock_);
+
+ if (auto it = StrongChannelMap_.find(address)) {
+ auto channel = it->second;
+ channel->Touch();
+ return channel;
+ }
+
+ if (auto it = WeakChannelMap_.find(address)) {
+ const auto& weakChannel = it->second;
+ if (auto channel = weakChannel.Lock()) {
+ readerGuard.Release();
+
+ {
+ auto writerGuard = WriterGuard(SpinLock_);
+ // Check if the weak map still contains the same channel.
+ if (auto jt = WeakChannelMap_.find(address); jt != WeakChannelMap_.end() && jt->second == weakChannel) {
+ StrongChannelMap_.emplace(address, channel);
+ RegisterChannelForTtlChecks(address, channel);
+ }
+ }
+
+ channel->Touch();
+ return channel;
+ }
+ }
+ }
+
+ auto underlyingChannel = factory();
+ auto wrappedChannel = New<TCachedChannel>(this, underlyingChannel, address);
+
+ {
+ auto writerGuard = WriterGuard(SpinLock_);
+ // Check if another channel has been inserted while the lock was released.
+ if (auto it = WeakChannelMap_.find(address)) {
+ const auto& weakChannel = it->second;
+ if (auto channel = weakChannel.Lock()) {
+ StrongChannelMap_.emplace(address, channel);
+ channel->Touch();
+ return channel;
+ }
+ }
+
+ WeakChannelMap_.emplace(address, wrappedChannel);
+ StrongChannelMap_.emplace(address, wrappedChannel);
+ RegisterChannelForTtlChecks(address, wrappedChannel);
+
+ YT_LOG_DEBUG("Cached channel registered (Endpoint: %v)",
+ wrappedChannel->GetEndpointDescription());
+
+ return wrappedChannel;
+ }
+ }
+
+ void RegisterChannelForTtlChecks(const TString& address, const TCachedChannelPtr& channel)
+ {
+ TtlRegisterQueue_.Enqueue({address, channel});
+ }
+
+ void CheckExpiredChannels()
+ {
+ TtlRegisterQueue_.DequeueAll(false, [&] (auto&& item) {
+ TtlCheckQueue_.push_back(std::move(item));
+ });
+
+ auto deadline = TInstant::Now() - IdleChannelTtl_;
+
+ std::vector<std::pair<TString, TCachedChannelPtr>> expiredItems;
+ auto it = TtlCheckQueue_.begin();
+ while (it != TtlCheckQueue_.end()) {
+ auto channel = it->second.Lock();
+ auto lastActivityTime = channel ? std::make_optional(channel->GetLastActivityTime()) : std::nullopt;
+ if (!lastActivityTime || *lastActivityTime < deadline) {
+ YT_LOG_DEBUG("Cached channel expired (Address: %v, Endpoint: %v, LastActivityTime: %v, Ttl: %v)",
+ it->first,
+ channel ? std::make_optional(channel->GetEndpointDescription()) : std::nullopt,
+ lastActivityTime,
+ IdleChannelTtl_);
+ expiredItems.emplace_back(std::move(it->first), std::move(channel));
+ *it = std::move(TtlCheckQueue_.back());
+ TtlCheckQueue_.pop_back();
+ } else {
+ ++it;
+ }
+ }
+
+ if (!expiredItems.empty()) {
+ auto guard = WriterGuard(SpinLock_);
+ for (auto& item : expiredItems) {
+ if (auto it = StrongChannelMap_.find(item.first)) {
+ if (it->second == item.second) {
+ StrongChannelMap_.erase(it);
+ }
+ }
+ item.second.Reset();
+ if (auto it = WeakChannelMap_.find(item.first)) {
+ if (it->second.IsExpired()) {
+ WeakChannelMap_.erase(it);
+ }
+ }
+ }
+ }
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TCachingChannelFactory)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCachedChannel::OnTerminated(const TError& /*error*/)
+{
+ if (auto factory = Factory_.Lock()) {
+ factory->EvictChannel(Address_, this);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelFactoryPtr CreateCachingChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration idleChannelTtl)
+{
+ YT_VERIFY(underlyingFactory);
+
+ auto factory = New<TCachingChannelFactory>(
+ std::move(underlyingFactory),
+ idleChannelTtl);
+ factory->Initialize();
+ return factory;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/caching_channel_factory.h b/yt/yt/core/rpc/caching_channel_factory.h
new file mode 100644
index 0000000000..7af503a61a
--- /dev/null
+++ b/yt/yt/core/rpc/caching_channel_factory.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a channel factory that wraps another channel factory
+//! and caches its channels by address.
+//! These channels expire after some time of inactivity.
+IChannelFactoryPtr CreateCachingChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration idleChannelTtl = TDuration::Minutes(5));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/channel.h b/yt/yt/core/rpc/channel.h
new file mode 100644
index 0000000000..9ae2a664ee
--- /dev/null
+++ b/yt/yt/core/rpc/channel.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include "public.h"
+
+#include "protocol_version.h"
+
+#include <yt/yt/core/actions/future.h>
+#include <yt/yt/core/actions/signal.h>
+
+#include <yt/yt/core/bus/client.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Encapsulates a portion of streaming data.
+struct TStreamingPayload
+{
+ NCompression::ECodec Codec;
+ int SequenceNumber;
+ std::vector<TSharedRef> Attachments;
+};
+
+//! Encapsulates a progress on reading streaming data.
+struct TStreamingFeedback
+{
+ ssize_t ReadPosition;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Controls how attachments are being streamed between clients and services.
+struct TStreamingParameters
+{
+ //! Approximate limit for the number of bytes in flight.
+ ssize_t WindowSize = 16_MB;
+ //! Timeout for reads from attachments input stream.
+ std::optional<TDuration> ReadTimeout;
+ //! Timeout for writes to attachments output stream.
+ std::optional<TDuration> WriteTimeout;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Controls the lifetime of a request sent via IChannel::Send.
+struct IClientRequestControl
+ : public virtual TRefCounted
+{
+ //! Cancels the request.
+ /*!
+ * An implementation is free to ignore cancellations.
+ */
+ virtual void Cancel() = 0;
+
+ //! Sends the streaming request attachments to the service.
+ virtual TFuture<void> SendStreamingPayload(const TStreamingPayload& payload) = 0;
+
+ //! Notifies the service of the client's progress in reading the streaming response attachments.
+ virtual TFuture<void> SendStreamingFeedback(const TStreamingFeedback& feedback) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IClientRequestControl)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSendOptions
+{
+ std::optional<TDuration> Timeout;
+ std::optional<TDuration> AcknowledgementTimeout;
+ bool GenerateAttachmentChecksums = true;
+ bool RequestHeavy = false;
+ EMultiplexingBand MultiplexingBand = EMultiplexingBand::Default;
+ int MultiplexingParallelism = 1;
+ // For testing purposes only.
+ std::optional<TDuration> SendDelay;
+};
+
+//! An interface for exchanging request-response pairs.
+/*!
+ * \note Thread affinity: any.
+ */
+struct IChannel
+ : public virtual TRefCounted
+{
+ //! Returns a textual representation of the channel's endpoint.
+ //! Typically used for logging.
+ virtual const TString& GetEndpointDescription() const = 0;
+
+ //! Returns the channel's endpoint attributes.
+ //! Typically used for constructing errors.
+ virtual const NYTree::IAttributeDictionary& GetEndpointAttributes() const = 0;
+
+ //! Sends a request via the channel.
+ /*!
+ * \param request A request to send.
+ * \param responseHandler An object that will handle a response.
+ * \param timeout Request processing timeout.
+ * \returns an object controlling the lifetime of the request;
+ * the latter could be |nullptr| if no control is supported by the implementation in general
+ * or for this particular request.
+ */
+ virtual IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) = 0;
+
+ //! Shuts down the channel.
+ /*!
+ * It is safe to call this method multiple times.
+ * After the first call the instance is no longer usable.
+ */
+ virtual void Terminate(const TError& error) = 0;
+
+ //! Raised whenever the channel is terminated.
+ DECLARE_INTERFACE_SIGNAL(void(const TError&), Terminated);
+};
+
+DEFINE_REFCOUNTED_TYPE(IChannel)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides means for parsing addresses and creating channels.
+struct IChannelFactory
+ : public virtual TRefCounted
+{
+ virtual IChannelPtr CreateChannel(const TString& address) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IChannelFactory)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/channel_detail.cpp b/yt/yt/core/rpc/channel_detail.cpp
new file mode 100644
index 0000000000..7a297f20c4
--- /dev/null
+++ b/yt/yt/core/rpc/channel_detail.cpp
@@ -0,0 +1,261 @@
+#include "channel_detail.h"
+
+#include "message.h"
+#include "private.h"
+
+#include <library/cpp/yt/memory/leaky_singleton.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TChannelWrapper::TChannelWrapper(IChannelPtr underlyingChannel)
+ : UnderlyingChannel_(std::move(underlyingChannel))
+{
+ YT_ASSERT(UnderlyingChannel_);
+}
+
+const TString& TChannelWrapper::GetEndpointDescription() const
+{
+ return UnderlyingChannel_->GetEndpointDescription();
+}
+
+const NYTree::IAttributeDictionary& TChannelWrapper::GetEndpointAttributes() const
+{
+ return UnderlyingChannel_->GetEndpointAttributes();
+}
+
+IClientRequestControlPtr TChannelWrapper::Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options)
+{
+ return UnderlyingChannel_->Send(
+ std::move(request),
+ std::move(responseHandler),
+ options);
+}
+
+void TChannelWrapper::Terminate(const TError& error)
+{
+ UnderlyingChannel_->Terminate(error);
+}
+
+void TChannelWrapper::SubscribeTerminated(const TCallback<void(const TError&)>& callback)
+{
+ UnderlyingChannel_->SubscribeTerminated(callback);
+}
+
+void TChannelWrapper::UnsubscribeTerminated(const TCallback<void(const TError&)>& callback)
+{
+ UnderlyingChannel_->UnsubscribeTerminated(callback);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TClientRequestControlThunk::SetUnderlying(IClientRequestControlPtr underlying)
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (!underlying) {
+ return;
+ }
+
+ auto guard = Guard(SpinLock_);
+
+ // NB: SetUnderlying can only be invoked once.
+ // This protects from races on unguarded reads since once Underlying_ is non-null, it never changes.
+ YT_VERIFY(!Underlying_);
+ Underlying_ = std::move(underlying);
+
+ auto canceled = UnderlyingCanceled_ = Canceled_;
+ auto streamingPayloads = std::move(PendingStreamingPayloads_);
+ auto streamingFeedback = PendingStreamingFeedback_;
+
+ guard.Release();
+
+ if (canceled) {
+ Underlying_->Cancel();
+ }
+
+ for (auto& payload : streamingPayloads) {
+ payload.Promise.SetFrom(Underlying_->SendStreamingPayload(payload.Payload));
+ }
+
+ if (streamingFeedback.Feedback.ReadPosition >= 0) {
+ streamingFeedback.Promise.SetFrom(Underlying_->SendStreamingFeedback(streamingFeedback.Feedback));
+ }
+}
+
+void TClientRequestControlThunk::Cancel()
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(SpinLock_);
+
+ if (Canceled_) {
+ return;
+ }
+
+ Canceled_ = true;
+
+ if (Underlying_ && !UnderlyingCanceled_) {
+ UnderlyingCanceled_ = true;
+ guard.Release();
+ Underlying_->Cancel();
+ }
+}
+
+TFuture<void> TClientRequestControlThunk::SendStreamingPayload(const TStreamingPayload& payload)
+{
+ auto guard = Guard(SpinLock_);
+
+ if (Underlying_) {
+ guard.Release();
+ return Underlying_->SendStreamingPayload(payload);
+ }
+
+ auto promise = NewPromise<void>();
+ PendingStreamingPayloads_.push_back({
+ payload,
+ promise
+ });
+ return promise.ToFuture();
+}
+
+TFuture<void> TClientRequestControlThunk::SendStreamingFeedback(const TStreamingFeedback& feedback)
+{
+ auto guard = Guard(SpinLock_);
+
+ if (Underlying_) {
+ guard.Release();
+ return Underlying_->SendStreamingFeedback(feedback);
+ }
+
+ if (!PendingStreamingFeedback_.Promise) {
+ PendingStreamingFeedback_.Promise = NewPromise<void>();
+ }
+ auto promise = PendingStreamingFeedback_.Promise;
+
+ PendingStreamingFeedback_.Feedback = TStreamingFeedback{
+ std::max(PendingStreamingFeedback_.Feedback.ReadPosition, feedback.ReadPosition)
+ };
+
+ return promise;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TClientRequestPerformanceProfiler::TPerformanceCounters
+{
+ TPerformanceCounters(const NProfiling::TProfiler& profiler)
+ : AckTimeCounter(profiler.Timer("/request_time/ack"))
+ , ReplyTimeCounter(profiler.Timer("/request_time/reply"))
+ , TimeoutTimeCounter(profiler.Timer("/request_time/timeout"))
+ , CancelTimeCounter(profiler.Timer("/request_time/cancel"))
+ , TotalTimeCounter(profiler.Timer("/request_time/total"))
+ , RequestCounter(profiler.Counter("/request_count"))
+ , FailedRequestCounter(profiler.Counter("/failed_request_count"))
+ , TimedOutRequestCounter(profiler.Counter("/timed_out_request_count"))
+ , CancelledRequestCounter(profiler.Counter("/cancelled_request_count"))
+ , RequestMessageBodySizeCounter(profiler.Counter("/request_message_body_bytes"))
+ , RequestMessageAttachmentSizeCounter(profiler.Counter("/request_message_attachment_bytes"))
+ , ResponseMessageBodySizeCounter(profiler.Counter("/response_message_body_bytes"))
+ , ResponseMessageAttachmentSizeCounter(profiler.Counter("/response_message_attachment_bytes"))
+ { }
+
+ NProfiling::TEventTimer AckTimeCounter;
+ NProfiling::TEventTimer ReplyTimeCounter;
+ NProfiling::TEventTimer TimeoutTimeCounter;
+ NProfiling::TEventTimer CancelTimeCounter;
+ NProfiling::TEventTimer TotalTimeCounter;
+
+ NProfiling::TCounter RequestCounter;
+ NProfiling::TCounter FailedRequestCounter;
+ NProfiling::TCounter TimedOutRequestCounter;
+ NProfiling::TCounter CancelledRequestCounter;
+ NProfiling::TCounter RequestMessageBodySizeCounter;
+ NProfiling::TCounter RequestMessageAttachmentSizeCounter;
+ NProfiling::TCounter ResponseMessageBodySizeCounter;
+ NProfiling::TCounter ResponseMessageAttachmentSizeCounter;
+};
+
+auto TClientRequestPerformanceProfiler::GetPerformanceCounters(
+ const TString& service,
+ const TString& method) -> const TPerformanceCounters*
+{
+ using TCountersMap = NConcurrency::TSyncMap<std::pair<TString, TString>, TPerformanceCounters>;
+
+ auto [counter, _] = LeakySingleton<TCountersMap>()->FindOrInsert(std::pair(service, method), [&] {
+ auto profiler = RpcClientProfiler
+ .WithHot()
+ .WithTag("yt_service", service)
+ .WithTag("method", method, -1);
+ return TPerformanceCounters(profiler);
+ });
+ return counter;
+}
+
+TClientRequestPerformanceProfiler::TClientRequestPerformanceProfiler(const TString& service, const TString& method)
+ : MethodCounters_(GetPerformanceCounters(service, method))
+{ }
+
+void TClientRequestPerformanceProfiler::ProfileRequest(const TSharedRefArray& requestMessage)
+{
+ MethodCounters_->RequestCounter.Increment();
+ MethodCounters_->RequestMessageBodySizeCounter.Increment(GetMessageBodySize(requestMessage));
+ MethodCounters_->RequestMessageAttachmentSizeCounter.Increment(GetTotalMessageAttachmentSize(requestMessage));
+}
+
+void TClientRequestPerformanceProfiler::ProfileReply(const TSharedRefArray& responseMessage)
+{
+ MethodCounters_->ReplyTimeCounter.Record(Timer_.GetElapsedTime());
+ MethodCounters_->ResponseMessageBodySizeCounter.Increment(GetMessageBodySize(responseMessage));
+ MethodCounters_->ResponseMessageAttachmentSizeCounter.Increment(GetTotalMessageAttachmentSize(responseMessage));
+}
+
+void TClientRequestPerformanceProfiler::ProfileAcknowledgement()
+{
+ MethodCounters_->AckTimeCounter.Record(Timer_.GetElapsedTime());
+}
+
+void TClientRequestPerformanceProfiler::ProfileCancel()
+{
+ MethodCounters_->CancelTimeCounter.Record(Timer_.GetElapsedTime());
+ MethodCounters_->CancelledRequestCounter.Increment();
+}
+
+void TClientRequestPerformanceProfiler::ProfileTimeout()
+{
+ MethodCounters_->TimeoutTimeCounter.Record(Timer_.GetElapsedTime());
+ MethodCounters_->TimedOutRequestCounter.Increment();
+}
+
+void TClientRequestPerformanceProfiler::ProfileError(const TError& error)
+{
+ const auto errorCode = error.GetCode();
+ if (errorCode == NYT::EErrorCode::Canceled) {
+ ProfileCancel();
+ } else if (errorCode == NYT::EErrorCode::Timeout) {
+ ProfileTimeout();
+ } else {
+ MethodCounters_->FailedRequestCounter.Increment();
+ }
+}
+
+TDuration TClientRequestPerformanceProfiler::ProfileComplete()
+{
+ auto elapsed = Timer_.GetElapsedTime();
+ MethodCounters_->TotalTimeCounter.Record(elapsed);
+ return elapsed;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/channel_detail.h b/yt/yt/core/rpc/channel_detail.h
new file mode 100644
index 0000000000..a89568d100
--- /dev/null
+++ b/yt/yt/core/rpc/channel_detail.h
@@ -0,0 +1,103 @@
+#pragma once
+
+#include "channel.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChannelWrapper
+ : public virtual IChannel
+{
+public:
+ explicit TChannelWrapper(IChannelPtr underlyingChannel);
+
+ const TString& GetEndpointDescription() const override;
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override;
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override;
+
+ void Terminate(const TError& error) override;
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override;
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override;
+
+protected:
+ const IChannelPtr UnderlyingChannel_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TChannelWrapper)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientRequestControlThunk
+ : public IClientRequestControl
+{
+public:
+ void SetUnderlying(IClientRequestControlPtr underlying);
+
+ void Cancel() override;
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& payload) override;
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& feedback) override;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+
+ bool Canceled_ = false;
+
+ struct TPendingStreamingPayload
+ {
+ TStreamingPayload Payload;
+ TPromise<void> Promise;
+ };
+ std::vector<TPendingStreamingPayload> PendingStreamingPayloads_;
+
+ struct TPendingStreamingFeedback
+ {
+ TStreamingFeedback Feedback{-1};
+ TPromise<void> Promise;
+ };
+ TPendingStreamingFeedback PendingStreamingFeedback_;
+
+ bool UnderlyingCanceled_ = false;
+
+ IClientRequestControlPtr Underlying_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientRequestControlThunk)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientRequestPerformanceProfiler
+ : public IClientRequestControl
+{
+public:
+ TClientRequestPerformanceProfiler(const TString& service, const TString& method);
+
+ void ProfileRequest(const TSharedRefArray& requestMessage);
+ void ProfileAcknowledgement();
+ void ProfileReply(const TSharedRefArray& responseMessage);
+ void ProfileCancel();
+ void ProfileTimeout();
+ void ProfileError(const TError& error);
+
+ TDuration ProfileComplete();
+
+private:
+ struct TPerformanceCounters;
+
+ const TPerformanceCounters* const MethodCounters_;
+ NProfiling::TWallTimer Timer_;
+
+ static const TPerformanceCounters* GetPerformanceCounters(const TString& service, const TString& method);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/client-inl.h b/yt/yt/core/rpc/client-inl.h
new file mode 100644
index 0000000000..f90de1f717
--- /dev/null
+++ b/yt/yt/core/rpc/client-inl.h
@@ -0,0 +1,166 @@
+#ifndef CLIENT_INL_H_
+#error "Direct inclusion of this file is not allowed, include client.h"
+// For the sake of sane code completion.
+#include "client.h"
+#endif
+
+#include "helpers.h"
+#include "stream.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+TServiceDescriptor& TServiceDescriptor::SetFeaturesType()
+{
+ static const std::function<std::optional<TStringBuf>(int featureId)> Formatter = [] (int featureId) {
+ return TEnumTraits<E>::FindLiteralByValue(static_cast<E>(featureId));
+ };
+ FeatureIdFormatter = &Formatter;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+void IClientRequest::DeclareClientFeature(E featureId)
+{
+ DeclareClientFeature(FeatureIdToInt(featureId));
+}
+
+template <class E>
+void IClientRequest::RequireServerFeature(E featureId)
+{
+ RequireServerFeature(FeatureIdToInt(featureId));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class TResponse>
+TTypedClientRequest<TRequestMessage, TResponse>::TTypedClientRequest(
+ IChannelPtr channel,
+ const TServiceDescriptor& serviceDescriptor,
+ const TMethodDescriptor& methodDescriptor)
+ : TClientRequest(
+ std::move(channel),
+ serviceDescriptor,
+ methodDescriptor)
+{ }
+
+template <class TRequestMessage, class TResponse>
+TFuture<typename TResponse::TResult> TTypedClientRequest<TRequestMessage, TResponse>::Invoke()
+{
+ auto context = CreateClientContext();
+ auto requestAttachmentsStream = context->GetRequestAttachmentsStream();
+ auto responseAttachmentsStream = context->GetResponseAttachmentsStream();
+ typename TResponse::TResult response;
+ {
+ TMemoryTagGuard guard(context->GetResponseMemoryTag());
+ response = New<TResponse>(std::move(context));
+ }
+ auto promise = response->GetPromise();
+ auto requestControl = Send(std::move(response));
+ if (requestControl) {
+ auto subscribeToStreamAbort = [&] (const auto& stream) {
+ if (stream) {
+ stream->SubscribeAborted(BIND([=] {
+ requestControl->Cancel();
+ }));
+ }
+ };
+ subscribeToStreamAbort(requestAttachmentsStream);
+ subscribeToStreamAbort(responseAttachmentsStream);
+ promise.OnCanceled(BIND([=] (const TError& /*error*/) {
+ requestControl->Cancel();
+ }));
+ }
+ return promise.ToFuture();
+}
+
+template <class TRequestMessage, class TResponse>
+TSharedRefArray TTypedClientRequest<TRequestMessage, TResponse>::SerializeHeaderless() const
+{
+ TSharedRefArrayBuilder builder(Attachments().size() + 1);
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ builder.Add(EnableLegacyRpcCodecs_
+ ? SerializeProtoToRefWithEnvelope(*this, RequestCodec_, false)
+ : SerializeProtoToRefWithCompression(*this, RequestCodec_, false));
+
+ auto attachmentCodecId = EnableLegacyRpcCodecs_
+ ? NCompression::ECodec::None
+ : RequestCodec_;
+ auto compressedAttachments = CompressAttachments(Attachments(), attachmentCodecId);
+ for (auto&& attachment : compressedAttachments) {
+ builder.Add(std::move(attachment));
+ }
+
+ return builder.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResponseMessage>
+TTypedClientResponse<TResponseMessage>::TTypedClientResponse(TClientContextPtr clientContext)
+ : TClientResponse(std::move(clientContext))
+{ }
+
+template <class TResponseMessage>
+auto TTypedClientResponse<TResponseMessage>::GetPromise() -> TPromise<TResult>
+{
+ return Promise_;
+}
+
+template <class TResponseMessage>
+void TTypedClientResponse<TResponseMessage>::SetPromise(const TError& error)
+{
+ if (error.IsOK()) {
+ Promise_.Set(this);
+ } else {
+ Promise_.Set(error);
+ }
+ Promise_.Reset();
+}
+
+template <class TResponseMessage>
+bool TTypedClientResponse<TResponseMessage>::TryDeserializeBody(TRef data, std::optional<NCompression::ECodec> codecId)
+{
+ TMemoryTagGuard guard(ClientContext_->GetResponseMemoryTag());
+
+ return codecId
+ ? TryDeserializeProtoWithCompression(this, data, *codecId)
+ // COMPAT(kiselyovp): legacy RPC codecs
+ : TryDeserializeProtoWithEnvelope(this, data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> TProxyBase::CreateRequest(const TMethodDescriptor& methodDescriptor)
+{
+ auto request = New<T>(
+ Channel_,
+ ServiceDescriptor_,
+ methodDescriptor);
+ request->SetTimeout(DefaultTimeout_);
+ request->SetAcknowledgementTimeout(DefaultAcknowledgementTimeout_);
+ request->SetRequestCodec(DefaultRequestCodec_);
+ request->SetResponseCodec(DefaultResponseCodec_);
+ request->SetEnableLegacyRpcCodecs(DefaultEnableLegacyRpcCodecs_);
+ request->SetMultiplexingBand(methodDescriptor.MultiplexingBand);
+ request->SetResponseMemoryTag(GetCurrentMemoryTag());
+
+ if (methodDescriptor.StreamingEnabled) {
+ request->ClientAttachmentsStreamingParameters() =
+ DefaultClientAttachmentsStreamingParameters_;
+ request->ServerAttachmentsStreamingParameters() =
+ DefaultServerAttachmentsStreamingParameters_;
+ }
+
+ return request;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/client.cpp b/yt/yt/core/rpc/client.cpp
new file mode 100644
index 0000000000..30758a48b2
--- /dev/null
+++ b/yt/yt/core/rpc/client.cpp
@@ -0,0 +1,783 @@
+#include "client.h"
+#include "private.h"
+#include "dispatcher.h"
+#include "message.h"
+#include "stream.h"
+
+#include <yt/yt/core/net/local_address.h>
+
+#include <yt/yt/core/misc/checksum.h>
+#include <yt/yt/core/misc/memory_reference_tracker.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/bus/tcp/dispatcher.h>
+
+#include <yt/yt/build/ya_version.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+using namespace NYTree;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+static const auto LightInvokerDurationWarningThreshold = TDuration::MilliSeconds(10);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientContext::TClientContext(
+ TRequestId requestId,
+ NTracing::TTraceContextPtr traceContext,
+ const TString& service,
+ const TString& method,
+ TFeatureIdFormatter featureIdFormatter,
+ bool responseIsHeavy,
+ TAttachmentsOutputStreamPtr requestAttachmentsStream,
+ TAttachmentsInputStreamPtr responseAttachmentsStream,
+ TMemoryTag responseMemoryTag)
+ : RequestId_(requestId)
+ , TraceContext_(std::move(traceContext))
+ , Service_(service)
+ , Method_(method)
+ , FeatureIdFormatter_(featureIdFormatter)
+ , ResponseHeavy_(responseIsHeavy)
+ , RequestAttachmentsStream_(std::move(requestAttachmentsStream))
+ , ResponseAttachmentsStream_(std::move(responseAttachmentsStream))
+ , ResponseMemoryTag_(responseMemoryTag)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientRequest::TClientRequest(
+ IChannelPtr channel,
+ const TServiceDescriptor& serviceDescriptor,
+ const TMethodDescriptor& methodDescriptor)
+ : Channel_(std::move(channel))
+ , StreamingEnabled_(methodDescriptor.StreamingEnabled)
+ , SendBaggage_(serviceDescriptor.AcceptsBaggage)
+ , FeatureIdFormatter_(serviceDescriptor.FeatureIdFormatter)
+{
+ YT_ASSERT(Channel_);
+
+ Header_.set_service(serviceDescriptor.GetFullServiceName());
+ Header_.set_method(methodDescriptor.MethodName);
+ Header_.set_protocol_version_major(serviceDescriptor.ProtocolVersion.Major);
+ Header_.set_protocol_version_minor(serviceDescriptor.ProtocolVersion.Minor);
+
+ ToProto(Header_.mutable_request_id(), TRequestId::Create());
+}
+
+TClientRequest::TClientRequest(const TClientRequest& other)
+ : Attachments_(other.Attachments_)
+ , Timeout_(other.Timeout_)
+ , AcknowledgementTimeout_(other.AcknowledgementTimeout_)
+ , RequestHeavy_(other.RequestHeavy_)
+ , ResponseHeavy_(other.ResponseHeavy_)
+ , RequestCodec_(other.RequestCodec_)
+ , ResponseCodec_(other.ResponseCodec_)
+ , GenerateAttachmentChecksums_(other.GenerateAttachmentChecksums_)
+ , Channel_(other.Channel_)
+ , StreamingEnabled_(other.StreamingEnabled_)
+ , SendBaggage_(other.SendBaggage_)
+ , FeatureIdFormatter_(other.FeatureIdFormatter_)
+ , Header_(other.Header_)
+ , MultiplexingBand_(other.MultiplexingBand_)
+ , ClientAttachmentsStreamingParameters_(other.ClientAttachmentsStreamingParameters_)
+ , ServerAttachmentsStreamingParameters_(other.ServerAttachmentsStreamingParameters_)
+ , User_(other.User_)
+ , UserTag_(other.UserTag_)
+{ }
+
+TSharedRefArray TClientRequest::Serialize()
+{
+ bool retry = Serialized_.exchange(true);
+
+ PrepareHeader();
+
+ auto headerlessMessage = GetHeaderlessMessage();
+
+ if (!retry) {
+ auto output = CreateRequestMessage(Header_, headerlessMessage);
+ return TrackMemory(std::move(output));
+ }
+
+ if (StreamingEnabled_) {
+ THROW_ERROR_EXCEPTION("Retries are not supported for requests with streaming");
+ }
+
+ auto patchedHeader = Header_;
+ patchedHeader.set_retry(true);
+
+ auto output = CreateRequestMessage(patchedHeader, headerlessMessage);
+ return TrackMemory(std::move(output));
+}
+
+IClientRequestControlPtr TClientRequest::Send(IClientResponseHandlerPtr responseHandler)
+{
+ TSendOptions options{
+ .Timeout = Timeout_,
+ .AcknowledgementTimeout = AcknowledgementTimeout_,
+ .GenerateAttachmentChecksums = GenerateAttachmentChecksums_,
+ .RequestHeavy = RequestHeavy_,
+ .MultiplexingBand = MultiplexingBand_,
+ .MultiplexingParallelism = MultiplexingParallelism_,
+ .SendDelay = SendDelay_
+ };
+ auto control = Channel_->Send(
+ this,
+ std::move(responseHandler),
+ options);
+ RequestControl_ = control;
+ return control;
+}
+
+NProto::TRequestHeader& TClientRequest::Header()
+{
+ return Header_;
+}
+
+const NProto::TRequestHeader& TClientRequest::Header() const
+{
+ return Header_;
+}
+
+bool TClientRequest::IsStreamingEnabled() const
+{
+ return StreamingEnabled_;
+}
+
+const TStreamingParameters& TClientRequest::ClientAttachmentsStreamingParameters() const
+{
+ return ClientAttachmentsStreamingParameters_;
+}
+
+TStreamingParameters& TClientRequest::ClientAttachmentsStreamingParameters()
+{
+ return ClientAttachmentsStreamingParameters_;
+}
+
+const TStreamingParameters& TClientRequest::ServerAttachmentsStreamingParameters() const
+{
+ return ServerAttachmentsStreamingParameters_;
+}
+
+TStreamingParameters& TClientRequest::ServerAttachmentsStreamingParameters()
+{
+ return ServerAttachmentsStreamingParameters_;
+}
+
+NConcurrency::IAsyncZeroCopyOutputStreamPtr TClientRequest::GetRequestAttachmentsStream() const
+{
+ if (!RequestAttachmentsStream_) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::StreamingNotSupported, "Streaming is not supported");
+ }
+ return RequestAttachmentsStream_;
+}
+
+NConcurrency::IAsyncZeroCopyInputStreamPtr TClientRequest::GetResponseAttachmentsStream() const
+{
+ if (!ResponseAttachmentsStream_) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::StreamingNotSupported, "Streaming is not supported");
+ }
+ return ResponseAttachmentsStream_;
+}
+
+TRequestId TClientRequest::GetRequestId() const
+{
+ return FromProto<TRequestId>(Header_.request_id());
+}
+
+TRealmId TClientRequest::GetRealmId() const
+{
+ return FromProto<TRealmId>(Header_.realm_id());
+}
+
+const TString& TClientRequest::GetService() const
+{
+ return Header_.service();
+}
+
+const TString& TClientRequest::GetMethod() const
+{
+ return Header_.method();
+}
+
+void TClientRequest::DeclareClientFeature(int featureId)
+{
+ Header_.add_declared_client_feature_ids(featureId);
+}
+
+void TClientRequest::RequireServerFeature(int featureId)
+{
+ Header_.add_required_server_feature_ids(featureId);
+}
+
+const TString& TClientRequest::GetUser() const
+{
+ return User_;
+}
+
+void TClientRequest::SetUser(const TString& user)
+{
+ User_ = user;
+}
+
+const TString& TClientRequest::GetUserTag() const
+{
+ return UserTag_;
+}
+
+void TClientRequest::SetUserTag(const TString& tag)
+{
+ UserTag_ = tag;
+}
+
+void TClientRequest::SetUserAgent(const TString& userAgent)
+{
+ Header_.set_user_agent(userAgent);
+}
+
+bool TClientRequest::GetRetry() const
+{
+ return Header_.retry();
+}
+
+void TClientRequest::SetRetry(bool value)
+{
+ Header_.set_retry(value);
+}
+
+TMutationId TClientRequest::GetMutationId() const
+{
+ return NRpc::GetMutationId(Header_);
+}
+
+void TClientRequest::SetMutationId(TMutationId id)
+{
+ if (id) {
+ ToProto(Header_.mutable_mutation_id(), id);
+ } else {
+ Header_.clear_mutation_id();
+ }
+}
+
+size_t TClientRequest::GetHash() const
+{
+ auto hash = Hash_.load(std::memory_order::relaxed);
+ if (hash == UnknownHash) {
+ hash = ComputeHash();
+ auto oldHash = Hash_.exchange(hash);
+ YT_VERIFY(oldHash == UnknownHash || oldHash == hash);
+ }
+ return hash;
+}
+
+EMultiplexingBand TClientRequest::GetMultiplexingBand() const
+{
+ return MultiplexingBand_;
+}
+
+void TClientRequest::SetMultiplexingBand(EMultiplexingBand band)
+{
+ MultiplexingBand_ = band;
+ Header_.set_tos_level(TTcpDispatcher::Get()->GetTosLevelForBand(band));
+}
+
+int TClientRequest::GetMultiplexingParallelism() const
+{
+ return MultiplexingParallelism_;
+}
+
+void TClientRequest::SetMultiplexingParallelism(int parallelism)
+{
+ MultiplexingParallelism_ = parallelism;
+}
+
+size_t TClientRequest::ComputeHash() const
+{
+ size_t hash = 0;
+ for (const auto& part : GetHeaderlessMessage()) {
+ HashCombine(hash, GetChecksum(part));
+ }
+ return hash;
+}
+
+TClientContextPtr TClientRequest::CreateClientContext()
+{
+ auto traceContext = CreateCallTraceContext(GetService(), GetMethod());
+ if (traceContext) {
+ auto* tracingExt = Header().MutableExtension(NRpc::NProto::TRequestHeader::tracing_ext);
+ ToProto(tracingExt, traceContext);
+ if (!SendBaggage_) {
+ tracingExt->clear_baggage();
+ }
+ if (traceContext->IsSampled()) {
+ TraceRequest(traceContext);
+ }
+ }
+
+ // If user-agent was not specified explicitly, generate it from build information.
+ if (!Header().has_user_agent()) {
+ Header().set_user_agent(GetRpcUserAgent());
+ }
+
+ if (StreamingEnabled_) {
+ RequestAttachmentsStream_ = New<TAttachmentsOutputStream>(
+ RequestCodec_,
+ TDispatcher::Get()->GetCompressionPoolInvoker(),
+ BIND(&TClientRequest::OnPullRequestAttachmentsStream, MakeWeak(this)),
+ ClientAttachmentsStreamingParameters_.WindowSize,
+ ClientAttachmentsStreamingParameters_.WriteTimeout);
+ ResponseAttachmentsStream_ = New<TAttachmentsInputStream>(
+ BIND(&TClientRequest::OnResponseAttachmentsStreamRead, MakeWeak(this)),
+ TDispatcher::Get()->GetCompressionPoolInvoker(),
+ ClientAttachmentsStreamingParameters_.ReadTimeout);
+ }
+
+ return New<TClientContext>(
+ GetRequestId(),
+ std::move(traceContext),
+ GetService(),
+ GetMethod(),
+ FeatureIdFormatter_,
+ ResponseHeavy_,
+ RequestAttachmentsStream_,
+ ResponseAttachmentsStream_,
+ GetResponseMemoryTag().value_or(GetCurrentMemoryTag()));
+}
+
+void TClientRequest::OnPullRequestAttachmentsStream()
+{
+ auto payload = RequestAttachmentsStream_->TryPull();
+ if (!payload) {
+ return;
+ }
+
+ auto control = RequestControl_.Lock();
+ if (!control) {
+ RequestAttachmentsStream_->Abort(TError("Client request control is finalized")
+ << TErrorAttribute("request_id", GetRequestId()));
+ return;
+ }
+
+ YT_LOG_DEBUG("Request streaming attachments pulled (RequestId: %v, SequenceNumber: %v, Sizes: %v, Closed: %v)",
+ GetRequestId(),
+ payload->SequenceNumber,
+ MakeFormattableView(payload->Attachments, [] (auto* builder, const auto& attachment) {
+ builder->AppendFormat("%v", GetStreamingAttachmentSize(attachment));
+ }),
+ !payload->Attachments.back());
+
+ control->SendStreamingPayload(*payload).Subscribe(
+ BIND(&TClientRequest::OnRequestStreamingPayloadAcked, MakeStrong(this), payload->SequenceNumber));
+}
+
+void TClientRequest::OnRequestStreamingPayloadAcked(int sequenceNumber, const TError& error)
+{
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Request streaming payload delivery acknowledged (RequestId: %v, SequenceNumber: %v)",
+ GetRequestId(),
+ sequenceNumber);
+ } else {
+ YT_LOG_DEBUG(error, "Response streaming payload delivery failed (RequestId: %v, SequenceNumber: %v)",
+ GetRequestId(),
+ sequenceNumber);
+ RequestAttachmentsStream_->Abort(error);
+ }
+}
+
+void TClientRequest::OnResponseAttachmentsStreamRead()
+{
+ auto feedback = ResponseAttachmentsStream_->GetFeedback();
+
+ auto control = RequestControl_.Lock();
+ if (!control) {
+ RequestAttachmentsStream_->Abort(TError("Client request control is finalized")
+ << TErrorAttribute("request_id", GetRequestId()));
+ return;
+ }
+
+ YT_LOG_DEBUG("Response streaming attachments read (RequestId: %v, ReadPosition: %v)",
+ GetRequestId(),
+ feedback.ReadPosition);
+
+ control->SendStreamingFeedback(feedback).Subscribe(
+ BIND(&TClientRequest::OnResponseStreamingFeedbackAcked, MakeStrong(this), feedback));
+}
+
+void TClientRequest::OnResponseStreamingFeedbackAcked(const TStreamingFeedback& feedback, const TError& error)
+{
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Response streaming feedback delivery acknowledged (RequestId: %v, ReadPosition: %v)",
+ GetRequestId(),
+ feedback.ReadPosition);
+ } else {
+ YT_LOG_DEBUG(error, "Response streaming feedback delivery failed (RequestId: %v)",
+ GetRequestId());
+ ResponseAttachmentsStream_->Abort(error);
+ }
+}
+
+void TClientRequest::TraceRequest(const NTracing::TTraceContextPtr& traceContext)
+{
+ traceContext->AddTag(RequestIdAnnotation, GetRequestId());
+ traceContext->AddTag(EndpointAnnotation, Channel_->GetEndpointDescription());
+}
+
+void TClientRequest::PrepareHeader()
+{
+ if (HeaderPrepared_.load()) {
+ return;
+ }
+
+ auto guard = Guard(HeaderPreparationLock_);
+
+ if (HeaderPrepared_.load()) {
+ return;
+ }
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ if (!EnableLegacyRpcCodecs_) {
+ Header_.set_request_codec(ToProto<int>(RequestCodec_));
+ Header_.set_response_codec(ToProto<int>(ResponseCodec_));
+ }
+
+ if (StreamingEnabled_) {
+ ToProto(Header_.mutable_server_attachments_streaming_parameters(), ServerAttachmentsStreamingParameters_);
+ }
+
+ if (User_ && User_ != RootUserName) {
+ Header_.set_user(User_);
+ }
+
+ if (UserTag_ && UserTag_ != Header_.user()) {
+ Header_.set_user_tag(UserTag_);
+ }
+
+ HeaderPrepared_.store(true);
+}
+
+bool TClientRequest::IsLegacyRpcCodecsEnabled()
+{
+ return EnableLegacyRpcCodecs_;
+}
+
+TSharedRefArray TClientRequest::GetHeaderlessMessage() const
+{
+ if (SerializedHeaderlessMessageSet_.load()) {
+ return SerializedHeaderlessMessage_;
+ }
+ auto message = SerializeHeaderless();
+ if (!SerializedHeaderlessMessageLatch_.exchange(true)) {
+ SerializedHeaderlessMessage_ = message;
+ SerializedHeaderlessMessageSet_.store(true);
+ }
+ return message;
+}
+
+bool IsRequestSticky(const IClientRequestPtr& request)
+{
+ if (!request) {
+ return false;
+ }
+ const auto& balancingExt = request->Header().GetExtension(NProto::TBalancingExt::balancing_ext);
+ return balancingExt.enable_stickiness();
+}
+
+TSharedRefArray TClientRequest::TrackMemory(TSharedRefArray array) const
+{
+ return NYT::TrackMemory(MemoryReferenceTracker_, std::move(array));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TClientResponse::TClientResponse(TClientContextPtr clientContext)
+ : StartTime_(NProfiling::GetInstant())
+ , ClientContext_(std::move(clientContext))
+{ }
+
+const TString& TClientResponse::GetAddress() const
+{
+ return Address_;
+}
+
+const NProto::TResponseHeader& TClientResponse::Header() const
+{
+ return Header_;
+}
+
+TSharedRefArray TClientResponse::GetResponseMessage() const
+{
+ YT_ASSERT(ResponseMessage_);
+ return ResponseMessage_;
+}
+
+size_t TClientResponse::GetTotalSize() const
+{
+ YT_ASSERT(ResponseMessage_);
+ return ResponseMessage_.ByteSize();
+}
+
+void TClientResponse::HandleError(const TError& error)
+{
+ auto prevState = State_.exchange(EState::Done);
+ if (prevState == EState::Done) {
+ // Ignore the error.
+ // Most probably this is a late timeout.
+ return;
+ }
+
+ auto invokeHandler = [&] (const TError& error) {
+ GetInvoker()->Invoke(
+ BIND(&TClientResponse::DoHandleError, MakeStrong(this), error));
+ };
+
+ auto optionalEnrichedError = TryEnrichClientRequestErrorWithFeatureName(
+ error,
+ ClientContext_->GetFeatureIdFormatter());
+ if (optionalEnrichedError) {
+ invokeHandler(*optionalEnrichedError);
+ } else {
+ invokeHandler(error);
+ }
+}
+
+void TClientResponse::DoHandleError(const TError& error)
+{
+ NProfiling::TWallTimer timer;
+
+ Finish(error);
+
+ if (!ClientContext_->GetResponseHeavy() && timer.GetElapsedTime() > LightInvokerDurationWarningThreshold) {
+ YT_LOG_DEBUG("Handling light request error took too long (RequestId: %v, Duration: %v)",
+ ClientContext_->GetRequestId(),
+ timer.GetElapsedTime());
+ }
+}
+
+void TClientResponse::Finish(const TError& error)
+{
+ TraceResponse();
+
+ if (const auto& requestAttachmentsStream = ClientContext_->GetRequestAttachmentsStream()) {
+ // Abort the request stream unconditionally since there is no chance
+ // the server will receive any of newly written data.
+ requestAttachmentsStream->AbortUnlessClosed(error, false);
+ }
+
+ if (const auto& responseAttachmentsStream = ClientContext_->GetResponseAttachmentsStream()) {
+ // Only abort the response stream in case of an error.
+ // When request finishes successfully the client may still be reading the output stream.
+ if (!error.IsOK()) {
+ responseAttachmentsStream->AbortUnlessClosed(error, false);
+ }
+ }
+
+ SetPromise(error);
+}
+
+void TClientResponse::TraceResponse()
+{
+ if (const auto& traceContext = ClientContext_->GetTraceContext()) {
+ traceContext->Finish();
+ }
+}
+
+const IInvokerPtr& TClientResponse::GetInvoker()
+{
+ return ClientContext_->GetResponseHeavy()
+ ? TDispatcher::Get()->GetHeavyInvoker()
+ : TDispatcher::Get()->GetLightInvoker();
+}
+
+void TClientResponse::Deserialize(TSharedRefArray responseMessage)
+{
+ YT_ASSERT(responseMessage);
+ YT_ASSERT(!ResponseMessage_);
+
+ ResponseMessage_ = std::move(responseMessage);
+
+ if (ResponseMessage_.Size() < 2) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Too few response message parts: %v < 2",
+ ResponseMessage_.Size());
+ }
+
+ if (!TryParseResponseHeader(ResponseMessage_, &Header_)) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Error deserializing response header");
+ }
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ std::optional<NCompression::ECodec> bodyCodecId;
+ NCompression::ECodec attachmentCodecId;
+ if (Header_.has_codec()) {
+ bodyCodecId = attachmentCodecId = CheckedEnumCast<NCompression::ECodec>(Header_.codec());
+ } else {
+ bodyCodecId = std::nullopt;
+ attachmentCodecId = NCompression::ECodec::None;
+ }
+
+ if (!TryDeserializeBody(ResponseMessage_[1], bodyCodecId)) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Error deserializing response body");
+ }
+
+ auto compressedAttachments = MakeRange(ResponseMessage_.Begin() + 2, ResponseMessage_.End());
+ if (attachmentCodecId == NCompression::ECodec::None) {
+ Attachments_.clear();
+ Attachments_.reserve(compressedAttachments.Size());
+ for (auto& attachment : compressedAttachments) {
+ struct TCopiedAttachmentTag
+ { };
+ auto copiedAttachment = TSharedMutableRef::MakeCopy<TCopiedAttachmentTag>(attachment);
+ Attachments_.push_back(std::move(copiedAttachment));
+ }
+ } else {
+ Attachments_ = DecompressAttachments(compressedAttachments, attachmentCodecId);
+ }
+}
+
+void TClientResponse::HandleAcknowledgement()
+{
+ // NB: Handle without switching to another invoker.
+ auto expected = EState::Sent;
+ State_.compare_exchange_strong(expected, EState::Ack);
+}
+
+void TClientResponse::HandleResponse(TSharedRefArray message, TString address)
+{
+ auto prevState = State_.exchange(EState::Done);
+ YT_ASSERT(prevState == EState::Sent || prevState == EState::Ack);
+
+ GetInvoker()->Invoke(BIND(&TClientResponse::DoHandleResponse,
+ MakeStrong(this),
+ Passed(std::move(message)),
+ Passed(std::move(address))));
+}
+
+void TClientResponse::DoHandleResponse(TSharedRefArray message, TString address)
+{
+ NProfiling::TWallTimer timer;
+
+ Address_ = std::move(address);
+
+ try {
+ Deserialize(std::move(message));
+ Finish({});
+ } catch (const std::exception& ex) {
+ Finish(ex);
+ }
+
+ if (!ClientContext_->GetResponseHeavy() && timer.GetElapsedTime() > LightInvokerDurationWarningThreshold) {
+ YT_LOG_DEBUG("Handling light response took too long (RequestId: %v, Duration: %v)",
+ ClientContext_->GetRequestId(),
+ timer.GetElapsedTime());
+ }
+}
+
+void TClientResponse::HandleStreamingPayload(const TStreamingPayload& payload)
+{
+ const auto& stream = ClientContext_->GetResponseAttachmentsStream();
+ if (!stream) {
+ YT_LOG_DEBUG("Received streaming attachments payload for request with disabled streaming; ignored (RequestId: %v)",
+ ClientContext_->GetRequestId());
+ return;
+ }
+
+ stream->EnqueuePayload(payload);
+}
+
+void TClientResponse::HandleStreamingFeedback(const TStreamingFeedback& feedback)
+{
+ const auto& stream = ClientContext_->GetRequestAttachmentsStream();
+ if (!stream) {
+ YT_LOG_DEBUG("Received streaming attachments feedback for request with disabled streaming; ignored (RequestId: %v)",
+ ClientContext_->GetRequestId());
+ return;
+ }
+
+ stream->HandleFeedback(feedback);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceDescriptor::TServiceDescriptor(const TString& serviceName)
+ : ServiceName(serviceName)
+{ }
+
+TServiceDescriptor& TServiceDescriptor::SetProtocolVersion(int majorVersion)
+{
+ auto version = DefaultProtocolVersion;
+ version.Major = majorVersion;
+ ProtocolVersion = version;
+ return *this;
+}
+
+TServiceDescriptor& TServiceDescriptor::SetProtocolVersion(TProtocolVersion version)
+{
+ ProtocolVersion = version;
+ return *this;
+}
+
+TServiceDescriptor& TServiceDescriptor::SetNamespace(const TString& value)
+{
+ Namespace = value;
+ return *this;
+}
+
+TServiceDescriptor& TServiceDescriptor::SetAcceptsBaggage(bool value)
+{
+ AcceptsBaggage = value;
+ return *this;
+}
+
+TString TServiceDescriptor::GetFullServiceName() const
+{
+ return Namespace ? Namespace + "." + ServiceName : ServiceName;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMethodDescriptor::TMethodDescriptor(const TString& methodName)
+ : MethodName(methodName)
+{ }
+
+TMethodDescriptor& TMethodDescriptor::SetMultiplexingBand(EMultiplexingBand value)
+{
+ MultiplexingBand = value;
+ return *this;
+}
+
+TMethodDescriptor& TMethodDescriptor::SetStreamingEnabled(bool value)
+{
+ StreamingEnabled = value;
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProxyBase::TProxyBase(
+ IChannelPtr channel,
+ const TServiceDescriptor& descriptor)
+ : Channel_(std::move(channel))
+ , ServiceDescriptor_(descriptor)
+{
+ YT_VERIFY(Channel_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGenericProxy::TGenericProxy(
+ IChannelPtr channel,
+ const TServiceDescriptor& descriptor)
+ : TProxyBase(std::move(channel), descriptor)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/client.h b/yt/yt/core/rpc/client.h
new file mode 100644
index 0000000000..0a0285337c
--- /dev/null
+++ b/yt/yt/core/rpc/client.h
@@ -0,0 +1,508 @@
+#pragma once
+
+#include "public.h"
+#include "channel.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/bus/client.h>
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <yt/yt/core/misc/property.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/rpc/helpers.h>
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <atomic>
+#include <optional>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*!
+ * \note
+ * Thread affinity: single-threaded.
+ * Notable exceptions are IClientRequest::Serialize and IClientRequest::GetHash.
+ * Once the request is fully configured (from a single thread), these could be
+ * invoked from arbitrary threads concurrently.
+ */
+struct IClientRequest
+ : public virtual TRefCounted
+{
+ virtual TSharedRefArray Serialize() = 0;
+
+ virtual const NProto::TRequestHeader& Header() const = 0;
+ virtual NProto::TRequestHeader& Header() = 0;
+
+ virtual bool IsStreamingEnabled() const = 0;
+
+ virtual const TStreamingParameters& ClientAttachmentsStreamingParameters() const = 0;
+ virtual TStreamingParameters& ClientAttachmentsStreamingParameters() = 0;
+
+ virtual const TStreamingParameters& ServerAttachmentsStreamingParameters() const = 0;
+ virtual TStreamingParameters& ServerAttachmentsStreamingParameters() = 0;
+
+ virtual NConcurrency::IAsyncZeroCopyOutputStreamPtr GetRequestAttachmentsStream() const = 0;
+ virtual NConcurrency::IAsyncZeroCopyInputStreamPtr GetResponseAttachmentsStream() const = 0;
+
+ virtual TRequestId GetRequestId() const = 0;
+ virtual TRealmId GetRealmId() const = 0;
+ virtual const TString& GetService() const = 0;
+ virtual const TString& GetMethod() const = 0;
+
+ virtual void DeclareClientFeature(int featureId) = 0;
+ virtual void RequireServerFeature(int featureId) = 0;
+
+ virtual const TString& GetUser() const = 0;
+ virtual void SetUser(const TString& user) = 0;
+
+ virtual const TString& GetUserTag() const = 0;
+ virtual void SetUserTag(const TString& tag) = 0;
+
+ virtual void SetUserAgent(const TString& userAgent) = 0;
+
+ virtual bool GetRetry() const = 0;
+ virtual void SetRetry(bool value) = 0;
+
+ virtual TMutationId GetMutationId() const = 0;
+ virtual void SetMutationId(TMutationId id) = 0;
+
+ virtual bool IsLegacyRpcCodecsEnabled() = 0;
+
+ virtual size_t GetHash() const = 0;
+
+ // Extension methods.
+ template <class E>
+ void DeclareClientFeature(E featureId);
+ template <class E>
+ void RequireServerFeature(E featureId);
+};
+
+DEFINE_REFCOUNTED_TYPE(IClientRequest)
+
+bool IsRequestSticky(const IClientRequestPtr& request);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientContext
+ : public TRefCounted
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(TRequestId, RequestId);
+ DEFINE_BYVAL_RO_PROPERTY(NTracing::TTraceContextPtr, TraceContext);
+ DEFINE_BYVAL_RO_PROPERTY(TString, Service);
+ DEFINE_BYVAL_RO_PROPERTY(TString, Method);
+ DEFINE_BYVAL_RO_PROPERTY(TFeatureIdFormatter, FeatureIdFormatter);
+ DEFINE_BYVAL_RO_PROPERTY(bool, ResponseHeavy);
+ DEFINE_BYVAL_RO_PROPERTY(TAttachmentsOutputStreamPtr, RequestAttachmentsStream);
+ DEFINE_BYVAL_RO_PROPERTY(TAttachmentsInputStreamPtr, ResponseAttachmentsStream);
+ DEFINE_BYVAL_RO_PROPERTY(TMemoryTag, ResponseMemoryTag);
+
+public:
+ TClientContext(
+ TRequestId requestId,
+ NTracing::TTraceContextPtr traceContext,
+ const TString& service,
+ const TString& method,
+ TFeatureIdFormatter featureIdFormatter,
+ bool heavy,
+ TAttachmentsOutputStreamPtr requestAttachmentsStream,
+ TAttachmentsInputStreamPtr responseAttachmentsStream,
+ TMemoryTag responseMemoryTag);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TClientRequest
+ : public IClientRequest
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TSharedRef>, Attachments);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TDuration>, Timeout);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TDuration>, AcknowledgementTimeout);
+ //! If |true| then the request will be serialized in RPC heavy thread pool.
+ DEFINE_BYVAL_RW_PROPERTY(bool, RequestHeavy);
+ //! If |true| then the response will be deserialized and the response handler will
+ //! be invoked in RPC heavy thread pool.
+ DEFINE_BYVAL_RW_PROPERTY(bool, ResponseHeavy);
+ DEFINE_BYVAL_RW_PROPERTY(NCompression::ECodec, RequestCodec, NCompression::ECodec::None);
+ DEFINE_BYVAL_RW_PROPERTY(NCompression::ECodec, ResponseCodec, NCompression::ECodec::None);
+ DEFINE_BYVAL_RW_PROPERTY(bool, EnableLegacyRpcCodecs, true);
+ DEFINE_BYVAL_RW_PROPERTY(bool, GenerateAttachmentChecksums, true);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TMemoryTag>, ResponseMemoryTag);
+ DEFINE_BYVAL_RW_PROPERTY(IMemoryReferenceTrackerPtr, MemoryReferenceTracker);
+ // For testing purposes only.
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TDuration>, SendDelay);
+
+public:
+ TSharedRefArray Serialize() override;
+
+ NProto::TRequestHeader& Header() override;
+ const NProto::TRequestHeader& Header() const override;
+
+ bool IsStreamingEnabled() const override;
+
+ const TStreamingParameters& ClientAttachmentsStreamingParameters() const override;
+ TStreamingParameters& ClientAttachmentsStreamingParameters() override;
+
+ const TStreamingParameters& ServerAttachmentsStreamingParameters() const override;
+ TStreamingParameters& ServerAttachmentsStreamingParameters() override;
+
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr GetRequestAttachmentsStream() const override;
+ NConcurrency::IAsyncZeroCopyInputStreamPtr GetResponseAttachmentsStream() const override;
+
+ TRequestId GetRequestId() const override;
+ TRealmId GetRealmId() const override;
+ const TString& GetService() const override;
+ const TString& GetMethod() const override;
+
+ using NRpc::IClientRequest::DeclareClientFeature;
+ using NRpc::IClientRequest::RequireServerFeature;
+
+ void DeclareClientFeature(int featureId) override;
+ void RequireServerFeature(int featureId) override;
+
+ const TString& GetUser() const override;
+ void SetUser(const TString& user) override;
+
+ const TString& GetUserTag() const override;
+ void SetUserTag(const TString& tag) override;
+
+ void SetUserAgent(const TString& userAgent) override;
+
+ bool GetRetry() const override;
+ void SetRetry(bool value) override;
+
+ TMutationId GetMutationId() const override;
+ void SetMutationId(TMutationId id) override;
+
+ size_t GetHash() const override;
+
+ bool IsLegacyRpcCodecsEnabled() override;
+
+ EMultiplexingBand GetMultiplexingBand() const;
+ void SetMultiplexingBand(EMultiplexingBand band);
+
+ int GetMultiplexingParallelism() const;
+ void SetMultiplexingParallelism(int parallelism);
+
+protected:
+ const IChannelPtr Channel_;
+ const bool StreamingEnabled_;
+ const bool SendBaggage_;
+ const TFeatureIdFormatter FeatureIdFormatter_;
+
+ TClientRequest(
+ IChannelPtr channel,
+ const TServiceDescriptor& serviceDescriptor,
+ const TMethodDescriptor& methodDescriptor);
+ TClientRequest(const TClientRequest& other);
+
+ virtual TSharedRefArray SerializeHeaderless() const = 0;
+ virtual size_t ComputeHash() const;
+
+ TClientContextPtr CreateClientContext();
+
+ IClientRequestControlPtr Send(IClientResponseHandlerPtr responseHandler);
+
+private:
+ std::atomic<bool> Serialized_ = false;
+
+ std::atomic<bool> HeaderPrepared_ = false;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, HeaderPreparationLock_);
+ NProto::TRequestHeader Header_;
+
+ mutable TSharedRefArray SerializedHeaderlessMessage_;
+ mutable std::atomic<bool> SerializedHeaderlessMessageLatch_ = false;
+ mutable std::atomic<bool> SerializedHeaderlessMessageSet_ = false;
+
+ static constexpr auto UnknownHash = static_cast<size_t>(-1);
+ mutable std::atomic<size_t> Hash_ = UnknownHash;
+
+ EMultiplexingBand MultiplexingBand_ = EMultiplexingBand::Default;
+ int MultiplexingParallelism_ = 1;
+
+ TStreamingParameters ClientAttachmentsStreamingParameters_;
+ TStreamingParameters ServerAttachmentsStreamingParameters_;
+
+ TAttachmentsOutputStreamPtr RequestAttachmentsStream_;
+ TAttachmentsInputStreamPtr ResponseAttachmentsStream_;
+
+ TString User_;
+ TString UserTag_;
+
+ TWeakPtr<IClientRequestControl> RequestControl_;
+
+ void OnPullRequestAttachmentsStream();
+ void OnRequestStreamingPayloadAcked(int sequenceNumber, const TError& error);
+ void OnResponseAttachmentsStreamRead();
+ void OnResponseStreamingFeedbackAcked(const TStreamingFeedback& feedback, const TError& error);
+
+ void TraceRequest(const NTracing::TTraceContextPtr& traceContext);
+
+ void PrepareHeader();
+ TSharedRefArray GetHeaderlessMessage() const;
+
+ TSharedRefArray TrackMemory(TSharedRefArray array) const;
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class TResponse>
+class TTypedClientRequest
+ : public TClientRequest
+ , public TRequestMessage
+{
+public:
+ using TThisPtr = TIntrusivePtr<TTypedClientRequest>;
+
+ TTypedClientRequest(
+ IChannelPtr channel,
+ const TServiceDescriptor& serviceDescriptor,
+ const TMethodDescriptor& methodDescriptor);
+
+ TFuture<typename TResponse::TResult> Invoke();
+
+private:
+ TSharedRefArray SerializeHeaderless() const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Handles the outcome of a single RPC request.
+struct IClientResponseHandler
+ : public virtual TRefCounted
+{
+ //! Called when request delivery is acknowledged.
+ virtual void HandleAcknowledgement() = 0;
+
+ //! Called if the request is replied with #EErrorCode::OK.
+ /*!
+ * \param message A message containing the response.
+ * \param address Address of the response sender. Empty if it is not supported by the underlying RPC stack.
+ */
+ virtual void HandleResponse(TSharedRefArray message, TString address) = 0;
+
+ //! Called if the request fails.
+ /*!
+ * \param error An error that has occurred.
+ */
+ virtual void HandleError(const TError& error) = 0;
+
+ //! Enables passing streaming data from the service to clients.
+ virtual void HandleStreamingPayload(const TStreamingPayload& payload) = 0;
+
+ //! Enables the service to notify clients about its progress in receiving streaming data.
+ virtual void HandleStreamingFeedback(const TStreamingFeedback& feedback) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IClientResponseHandler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EClientResponseState,
+ (Sent)
+ (Ack)
+ (Done)
+);
+
+//! Provides a common base for both one-way and two-way responses.
+class TClientResponse
+ : public IClientResponseHandler
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(TInstant, StartTime);
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TSharedRef>, Attachments);
+
+ //! Returns address of the response sender, as it was provided by the channel configuration (FQDN, IP address, etc).
+ //! Empty if it is not supported by the underlying RPC stack or the OK response has not been received yet.
+ //! Note: complex channels choose destination dynamically (hedging, roaming), so the address is not known beforehand.
+ const TString& GetAddress() const;
+
+ const NProto::TResponseHeader& Header() const;
+
+ TSharedRefArray GetResponseMessage() const;
+
+ //! Returns total size: response message size plus attachments.
+ size_t GetTotalSize() const;
+
+protected:
+ const TClientContextPtr ClientContext_;
+
+ using EState = EClientResponseState;
+ std::atomic<EState> State_ = {EState::Sent};
+
+
+ explicit TClientResponse(TClientContextPtr clientContext);
+
+ virtual bool TryDeserializeBody(TRef data, std::optional<NCompression::ECodec> codecId = {}) = 0;
+
+ // IClientResponseHandler implementation.
+ void HandleError(const TError& error) override;
+ void HandleAcknowledgement() override;
+ void HandleResponse(TSharedRefArray message, TString address) override;
+ void HandleStreamingPayload(const TStreamingPayload& payload) override;
+ void HandleStreamingFeedback(const TStreamingFeedback& feedback) override;
+
+ void Finish(const TError& error);
+
+ virtual void SetPromise(const TError& error) = 0;
+
+ const IInvokerPtr& GetInvoker();
+
+private:
+ TString Address_;
+ NProto::TResponseHeader Header_;
+ TSharedRefArray ResponseMessage_;
+
+ void TraceResponse();
+ void DoHandleError(const TError& error);
+
+ void DoHandleResponse(TSharedRefArray message, TString address);
+ void Deserialize(TSharedRefArray responseMessage);
+};
+
+DEFINE_REFCOUNTED_TYPE(TClientResponse)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResponseMessage>
+class TTypedClientResponse
+ : public TClientResponse
+ , public TResponseMessage
+{
+public:
+ using TResult = TIntrusivePtr<TTypedClientResponse>;
+
+ explicit TTypedClientResponse(TClientContextPtr clientContext);
+
+ TPromise<TResult> GetPromise();
+
+private:
+ TPromise<TResult> Promise_ = NewPromise<TResult>();
+
+
+ void SetPromise(const TError& error) override;
+ bool TryDeserializeBody(TRef data, std::optional<NCompression::ECodec> codecId = {}) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TServiceDescriptor
+{
+ TString ServiceName;
+ TString Namespace;
+ TProtocolVersion ProtocolVersion = DefaultProtocolVersion;
+ TFeatureIdFormatter FeatureIdFormatter = nullptr;
+ bool AcceptsBaggage = true;
+
+ explicit TServiceDescriptor(const TString& serviceName);
+
+ TServiceDescriptor& SetProtocolVersion(int majorVersion);
+ TServiceDescriptor& SetProtocolVersion(TProtocolVersion version);
+ TServiceDescriptor& SetNamespace(const TString& value);
+ TServiceDescriptor& SetAcceptsBaggage(bool value);
+ template <class E>
+ TServiceDescriptor& SetFeaturesType();
+
+ TString GetFullServiceName() const;
+};
+
+#define DEFINE_RPC_PROXY(type, name, ...) \
+ static const ::NYT::NRpc::TServiceDescriptor& GetDescriptor() \
+ { \
+ static const auto Descriptor = ::NYT::NRpc::TServiceDescriptor(#name) __VA_ARGS__; \
+ return Descriptor; \
+ } \
+ \
+ explicit type(::NYT::NRpc::IChannelPtr channel) \
+ : ::NYT::NRpc::TProxyBase(std::move(channel), GetDescriptor()) \
+ { } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMethodDescriptor
+{
+ TString MethodName;
+ EMultiplexingBand MultiplexingBand = EMultiplexingBand::Default;
+ bool StreamingEnabled = false;
+
+ explicit TMethodDescriptor(const TString& methodName);
+
+ TMethodDescriptor& SetMultiplexingBand(EMultiplexingBand value);
+ TMethodDescriptor& SetStreamingEnabled(bool value);
+};
+
+#define DEFINE_RPC_PROXY_METHOD(ns, method, ...) \
+ using TRsp##method = ::NYT::NRpc::TTypedClientResponse<ns::TRsp##method>; \
+ using TReq##method = ::NYT::NRpc::TTypedClientRequest<ns::TReq##method, TRsp##method>; \
+ using TRsp##method##Ptr = ::NYT::TIntrusivePtr<TRsp##method>; \
+ using TReq##method##Ptr = ::NYT::TIntrusivePtr<TReq##method>; \
+ using TErrorOrRsp##method##Ptr = ::NYT::TErrorOr<TRsp##method##Ptr>; \
+ \
+ TReq##method##Ptr method() \
+ { \
+ static const auto Descriptor = ::NYT::NRpc::TMethodDescriptor(#method) __VA_ARGS__; \
+ return CreateRequest<TReq##method>(Descriptor); \
+ } \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProxyBase
+{
+public:
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TDuration>, DefaultTimeout);
+ DEFINE_BYVAL_RW_PROPERTY(std::optional<TDuration>, DefaultAcknowledgementTimeout);
+ DEFINE_BYVAL_RW_PROPERTY(NCompression::ECodec, DefaultRequestCodec, NCompression::ECodec::None);
+ DEFINE_BYVAL_RW_PROPERTY(NCompression::ECodec, DefaultResponseCodec, NCompression::ECodec::None);
+ DEFINE_BYVAL_RW_PROPERTY(bool, DefaultEnableLegacyRpcCodecs, true);
+
+ DEFINE_BYREF_RW_PROPERTY(TStreamingParameters, DefaultClientAttachmentsStreamingParameters);
+ DEFINE_BYREF_RW_PROPERTY(TStreamingParameters, DefaultServerAttachmentsStreamingParameters);
+
+protected:
+ const IChannelPtr Channel_;
+ const TServiceDescriptor ServiceDescriptor_;
+
+ TProxyBase(
+ IChannelPtr channel,
+ const TServiceDescriptor& descriptor);
+
+ template <class T>
+ TIntrusivePtr<T> CreateRequest(const TMethodDescriptor& methodDescriptor);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGenericProxy
+ : public TProxyBase
+{
+public:
+ TGenericProxy(
+ IChannelPtr channel,
+ const TServiceDescriptor& descriptor);
+
+ DEFINE_RPC_PROXY_METHOD(NProto, Discover);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+#define CLIENT_INL_H_
+#include "client-inl.h"
+#undef CLIENT_INL_H_
diff --git a/yt/yt/core/rpc/config.cpp b/yt/yt/core/rpc/config.cpp
new file mode 100644
index 0000000000..bb1519dafe
--- /dev/null
+++ b/yt/yt/core/rpc/config.cpp
@@ -0,0 +1,306 @@
+#include "config.h"
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+using namespace NYTree;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THistogramExponentialBounds::Register(TRegistrar registrar)
+{
+ registrar.Parameter("min", &TThis::Min).Default(TDuration::Zero());
+ registrar.Parameter("max", &TThis::Max).Default(TDuration::Seconds(2));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THistogramConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("exponential_bounds", &TThis::ExponentialBounds).Optional();
+ registrar.Parameter("custom_bounds", &TThis::CustomBounds).Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServiceCommonConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_per_user_profiling", &TThis::EnablePerUserProfiling)
+ .Default(false);
+ registrar.Parameter("histogram_timer_profiling", &TThis::HistogramTimerProfiling)
+ .Default();
+ registrar.Parameter("code_counting", &TThis::EnableErrorCodeCounting)
+ .Default(false);
+ registrar.Parameter("tracing_mode", &TThis::TracingMode)
+ .Default(ERequestTracingMode::Enable);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("services", &TThis::Services)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServiceConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_per_user_profiling", &TThis::EnablePerUserProfiling)
+ .Optional();
+ registrar.Parameter("code_counting", &TThis::EnableErrorCodeCounting)
+ .Optional();
+ registrar.Parameter("histogram_timer_profiling", &TThis::HistogramTimerProfiling)
+ .Default();
+ registrar.Parameter("tracing_mode", &TThis::TracingMode)
+ .Optional();
+ registrar.Parameter("methods", &TThis::Methods)
+ .Optional();
+ registrar.Parameter("authentication_queue_size_limit", &TThis::AuthenticationQueueSizeLimit)
+ .Alias("max_authentication_queue_size")
+ .Optional();
+ registrar.Parameter("pending_payloads_timeout", &TThis::PendingPayloadsTimeout)
+ .Optional();
+ registrar.Parameter("pooled", &TThis::Pooled)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TMethodConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("heavy", &TThis::Heavy)
+ .Optional();
+ registrar.Parameter("queue_size_limit", &TThis::QueueSizeLimit)
+ .Alias("max_queue_size")
+ .Optional();
+ registrar.Parameter("concurrency_limit", &TThis::ConcurrencyLimit)
+ .Alias("max_concurrency")
+ .Optional();
+ registrar.Parameter("log_level", &TThis::LogLevel)
+ .Optional();
+ registrar.Parameter("request_bytes_throttler", &TThis::RequestBytesThrottler)
+ .Default();
+ registrar.Parameter("request_weight_throttler", &TThis::RequestWeightThrottler)
+ .Default();
+ registrar.Parameter("logging_suppression_timeout", &TThis::LoggingSuppressionTimeout)
+ .Optional();
+ registrar.Parameter("logging_suppression_failed_request_throttler", &TThis::LoggingSuppressionFailedRequestThrottler)
+ .Optional();
+ registrar.Parameter("tracing_mode", &TThis::TracingMode)
+ .Optional();
+ registrar.Parameter("pooled", &TThis::Pooled)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TRetryingChannelConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("retry_backoff_time", &TThis::RetryBackoffTime)
+ .Default(TDuration::Seconds(3));
+ registrar.Parameter("retry_attempts", &TThis::RetryAttempts)
+ .GreaterThanOrEqual(1)
+ .Default(10);
+ registrar.Parameter("retry_timeout", &TThis::RetryTimeout)
+ .GreaterThanOrEqual(TDuration::Zero())
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBalancingChannelConfigBase::Register(TRegistrar registrar)
+{
+ registrar.Parameter("discover_timeout", &TThis::DiscoverTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("acknowledgement_timeout", &TThis::AcknowledgementTimeout)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("rediscover_period", &TThis::RediscoverPeriod)
+ .Default(TDuration::Seconds(60));
+ registrar.Parameter("rediscover_splay", &TThis::RediscoverSplay)
+ .Default(TDuration::Seconds(15));
+ registrar.Parameter("hard_backoff_time", &TThis::HardBackoffTime)
+ .Default(TDuration::Seconds(60));
+ registrar.Parameter("soft_backoff_time", &TThis::SoftBackoffTime)
+ .Default(TDuration::Seconds(15));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TViablePeerRegistryConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_peer_count", &TThis::MaxPeerCount)
+ .GreaterThan(1)
+ .Default(100);
+ registrar.Parameter("hashes_per_peer", &TThis::HashesPerPeer)
+ .GreaterThan(0)
+ .Default(10);
+ registrar.Parameter("peer_priority_strategy", &TThis::PeerPriorityStrategy)
+ .Default(EPeerPriorityStrategy::None);
+ registrar.Parameter("min_peer_count_for_priority_awareness", &TThis::MinPeerCountForPriorityAwareness)
+ .GreaterThanOrEqual(0)
+ .Default(0);
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->MinPeerCountForPriorityAwareness > config->MaxPeerCount) {
+ THROW_ERROR_EXCEPTION(
+ "Value of \"min_peer_count_for_priority_awareness\" cannot be bigger than \"max_peer_count\": %v > %v; please read the corresponding comment",
+ config->MinPeerCountForPriorityAwareness,
+ config->MaxPeerCount);
+ }
+ });
+}
+
+void TDynamicChannelPoolConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("max_concurrent_discover_requests", &TThis::MaxConcurrentDiscoverRequests)
+ .GreaterThan(0)
+ .Default(10);
+ registrar.Parameter("random_peer_eviction_period", &TThis::RandomPeerEvictionPeriod)
+ .Default(TDuration::Seconds(1));
+
+ registrar.Parameter("enable_peer_polling", &TThis::EnablePeerPolling)
+ .Default(false);
+ registrar.Parameter("peer_polling_period", &TThis::PeerPollingPeriod)
+ .Default(TDuration::Seconds(60));
+ registrar.Parameter("peer_polling_period_splay", &TThis::PeerPollingPeriodSplay)
+ .Default(TDuration::Seconds(10));
+ registrar.Parameter("peer_polling_request_timeout", &TThis::PeerPollingRequestTimeout)
+ .Default(TDuration::Seconds(15));
+
+ registrar.Parameter("discovery_session_timeout", &TThis::DiscoverySessionTimeout)
+ .Default(TDuration::Minutes(5))
+ .DontSerializeDefault();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServiceDiscoveryEndpointsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("cluster", &TThis::Cluster)
+ .Default();
+ registrar.Parameter("clusters", &TThis::Clusters)
+ .Default();
+ registrar.Parameter("endpoint_set_id", &TThis::EndpointSetId);
+ registrar.Parameter("update_period", &TThis::UpdatePeriod)
+ .Default(TDuration::Seconds(60));
+
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->Cluster.has_value() == !config->Clusters.empty()) {
+ THROW_ERROR_EXCEPTION("Exactly one of \"cluster\" and \"clusters\" field must be set");
+ }
+
+ if (config->Clusters.empty()) {
+ config->Clusters.push_back(*config->Cluster);
+ config->Cluster.reset();
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBalancingChannelConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("addresses", &TThis::Addresses)
+ .Optional();
+ registrar.Parameter("endpoints", &TThis::Endpoints)
+ .Optional();
+ registrar.Parameter("hedging_delay", &TThis::HedgingDelay)
+ .Optional();
+ registrar.Parameter("cancel_primary_request_on_hedging", &TThis::CancelPrimaryRequestOnHedging)
+ .Default(false);
+
+ registrar.Postprocessor([] (TThis* config) {
+ int endpointConfigCount = 0;
+ if (config->Addresses) {
+ ++endpointConfigCount;
+ }
+ if (config->Endpoints) {
+ ++endpointConfigCount;
+ }
+ if (endpointConfigCount != 1) {
+ THROW_ERROR_EXCEPTION("Exactly one of \"addresses\" and \"endpoints\" must be specified");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TThrottlingChannelConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("rate_limit", &TThis::RateLimit)
+ .GreaterThan(0)
+ .Default(10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TThrottlingChannelDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("rate_limit", &TThis::RateLimit)
+ .GreaterThan(0)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TResponseKeeperConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("expiration_time", &TThis::ExpirationTime)
+ .Default(TDuration::Minutes(5));
+ registrar.Parameter("max_eviction_busy_time", &TThis::MaxEvictionTickTime)
+ .Default(TDuration::MilliSeconds(10));
+ registrar.Parameter("enable_warmup", &TThis::EnableWarmup)
+ .Default(true);
+ registrar.Parameter("warmup_time", &TThis::WarmupTime)
+ .Default(TDuration::Minutes(6));
+ registrar.Postprocessor([] (TThis* config) {
+ if (config->EnableWarmup && config->WarmupTime < config->ExpirationTime) {
+ THROW_ERROR_EXCEPTION("\"warmup_time\" cannot be less than \"expiration_time\"");
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDispatcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("heavy_pool_size", &TThis::HeavyPoolSize)
+ .Default(DefaultHeavyPoolSize)
+ .GreaterThan(0);
+ registrar.Parameter("compression_pool_size", &TThis::CompressionPoolSize)
+ .Default(DefaultCompressionPoolSize)
+ .GreaterThan(0);
+ registrar.Parameter("alert_on_missing_request_info", &TThis::AlertOnMissingRequestInfo)
+ .Default(false);
+}
+
+TDispatcherConfigPtr TDispatcherConfig::ApplyDynamic(const TDispatcherDynamicConfigPtr& dynamicConfig) const
+{
+ auto mergedConfig = CloneYsonStruct(MakeStrong(this));
+ UpdateYsonStructField(mergedConfig->HeavyPoolSize, dynamicConfig->HeavyPoolSize);
+ UpdateYsonStructField(mergedConfig->CompressionPoolSize, dynamicConfig->CompressionPoolSize);
+ UpdateYsonStructField(mergedConfig->AlertOnMissingRequestInfo, dynamicConfig->AlertOnMissingRequestInfo);
+ mergedConfig->Postprocess();
+ return mergedConfig;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDispatcherDynamicConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("heavy_pool_size", &TThis::HeavyPoolSize)
+ .Optional()
+ .GreaterThan(0);
+ registrar.Parameter("compression_pool_size", &TThis::CompressionPoolSize)
+ .Optional()
+ .GreaterThan(0);
+ registrar.Parameter("alert_on_missing_request_info", &TThis::AlertOnMissingRequestInfo)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/config.h b/yt/yt/core/rpc/config.h
new file mode 100644
index 0000000000..6659daf9e2
--- /dev/null
+++ b/yt/yt/core/rpc/config.h
@@ -0,0 +1,409 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <yt/yt/core/concurrency/config.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <vector>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ERequestTracingMode,
+ (Enable) // Propagation only.
+ (Disable) // Neither creates, nor propagates trace further.
+ (Force) // Forces trace creation.
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogramExponentialBounds
+ : public NYTree::TYsonStruct
+{
+public:
+ TDuration Min;
+ TDuration Max;
+
+ REGISTER_YSON_STRUCT(THistogramExponentialBounds);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THistogramExponentialBounds)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THistogramConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<THistogramExponentialBoundsPtr> ExponentialBounds;
+ std::optional<std::vector<TDuration>> CustomBounds;
+
+ REGISTER_YSON_STRUCT(THistogramConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(THistogramConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Common options shared between all services in one server.
+class TServiceCommonConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool EnablePerUserProfiling;
+ THistogramConfigPtr HistogramTimerProfiling;
+ bool EnableErrorCodeCounting;
+ ERequestTracingMode TracingMode;
+
+ REGISTER_YSON_STRUCT(TServiceCommonConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceCommonConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerConfig
+ : public TServiceCommonConfig
+{
+public:
+ THashMap<TString, NYTree::INodePtr> Services;
+
+ REGISTER_YSON_STRUCT(TServerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<bool> EnablePerUserProfiling;
+ std::optional<bool> EnableErrorCodeCounting;
+ std::optional<ERequestTracingMode> TracingMode;
+ THistogramConfigPtr HistogramTimerProfiling;
+ THashMap<TString, TMethodConfigPtr> Methods;
+ std::optional<int> AuthenticationQueueSizeLimit;
+ std::optional<TDuration> PendingPayloadsTimeout;
+ std::optional<bool> Pooled;
+
+ REGISTER_YSON_STRUCT(TServiceConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMethodConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<bool> Heavy;
+ std::optional<int> QueueSizeLimit;
+ std::optional<int> ConcurrencyLimit;
+ std::optional<NLogging::ELogLevel> LogLevel;
+ std::optional<TDuration> LoggingSuppressionTimeout;
+ NConcurrency::TThroughputThrottlerConfigPtr RequestBytesThrottler;
+ NConcurrency::TThroughputThrottlerConfigPtr RequestWeightThrottler;
+ NConcurrency::TThroughputThrottlerConfigPtr LoggingSuppressionFailedRequestThrottler;
+ std::optional<ERequestTracingMode> TracingMode;
+ std::optional<bool> Pooled;
+
+ REGISTER_YSON_STRUCT(TMethodConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TMethodConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetryingChannelConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Time to wait between consequent attempts.
+ TDuration RetryBackoffTime;
+
+ //! Maximum number of retry attempts to make.
+ int RetryAttempts;
+
+ //! Maximum time to spend while retrying.
+ //! If null then no limit is enforced.
+ std::optional<TDuration> RetryTimeout;
+
+ REGISTER_YSON_STRUCT(TRetryingChannelConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TRetryingChannelConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBalancingChannelConfigBase
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Timeout for |Discover| requests.
+ TDuration DiscoverTimeout;
+
+ //! Timeout for acknowledgement of all RPC requests going through the channel.
+ TDuration AcknowledgementTimeout;
+
+ //! Interval between automatic rediscovery of active peers.
+ /*!
+ * Discovery is started automatically if no active peers are known.
+ * In some cases, however, this is not enough.
+ * E.g. a follower may become active and thus eligible for load balancing.
+ * This setting controls the period of time after which the channel
+ * starts rediscovering peers even if an active one is known.
+ */
+ TDuration RediscoverPeriod;
+
+ //! A random duration from 0 to #RediscoverSplay is added to #RediscoverPeriod on each
+ //! rediscovery attempt.
+ TDuration RediscoverSplay;
+
+ //! Time between consequent attempts to reconnect to a peer, which
+ //! returns a hard failure (i.e. non-OK response) to |Discover| request.
+ TDuration HardBackoffTime;
+
+ //! Time between consequent attempts to reconnect to a peer, which
+ //! returns a soft failure (i.e. "down" response) to |Discover| request.
+ TDuration SoftBackoffTime;
+
+ REGISTER_YSON_STRUCT(TBalancingChannelConfigBase);
+
+ static void Register(TRegistrar registrar);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EPeerPriorityStrategy,
+ (None)
+ (PreferLocal)
+);
+
+class TViablePeerRegistryConfig
+ : public TBalancingChannelConfigBase
+{
+public:
+ //! In case too many peers are known, the registry will only maintain this many peers active.
+ int MaxPeerCount;
+
+ //! For sticky mode: number of consistent hash tokens to assign to each peer.
+ int HashesPerPeer;
+
+ //! Configures how random channels are selected.
+ EPeerPriorityStrategy PeerPriorityStrategy;
+
+ //! If set to a positive value, this number of active peers with the smallest priority will be required
+ //! for priority to be taken into account when choosing a random peer according to the peer priority strategy.
+ //! If it is not satisfied, peers will be chosen randomly from the whole pool of active peers.
+ //!
+ //! In practice: if EPeerPriorityStrategy::PreferLocal is set, it will only have an effect if there are at least
+ //! MinPeerCountForPriorityAwareness active local peers, otherwise peers will be chosen uniformly from the whole set of active peers.
+ //!
+ //! NB: Please note that MaxPeerCount respects priorities, e.g. given EPeerPriorityStrategy::PreferLocal and
+ //! MaxPeerCount = 100, if there are 200 available local and 400 available non-local peers, all active peers will be local.
+ //! This means that setting MinPeerCountForPriorityAwareness close to MaxPeerCount is practically useless.
+ //! If you want to set bigger values, you must also increase MaxPeerCount to accommodate more peers.
+ int MinPeerCountForPriorityAwareness;
+
+ REGISTER_YSON_STRUCT(TViablePeerRegistryConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TViablePeerRegistryConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDynamicChannelPoolConfig
+ : public TViablePeerRegistryConfig
+{
+public:
+ //! Maximum number of peers to query in parallel when locating alive ones.
+ int MaxConcurrentDiscoverRequests;
+
+ //! To avoid being stuck with the same peer set forever,
+ //! one random peer could be evicted after #RandomPeerEvictionPeriod.
+ TDuration RandomPeerEvictionPeriod;
+
+ bool EnablePeerPolling;
+ TDuration PeerPollingPeriod;
+ TDuration PeerPollingPeriodSplay;
+ TDuration PeerPollingRequestTimeout;
+
+ TDuration DiscoverySessionTimeout;
+
+ REGISTER_YSON_STRUCT(TDynamicChannelPoolConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDynamicChannelPoolConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceDiscoveryEndpointsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<TString> Cluster;
+ //! NB: If empty (default) this vector is filled with the cluster above.
+ std::vector<TString> Clusters;
+ TString EndpointSetId;
+ TDuration UpdatePeriod;
+
+ REGISTER_YSON_STRUCT(TServiceDiscoveryEndpointsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceDiscoveryEndpointsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBalancingChannelConfig
+ : public TDynamicChannelPoolConfig
+{
+public:
+ //! First option: static list of addresses.
+ std::optional<std::vector<TString>> Addresses;
+
+ //! Second option: SD endpoints.
+ TServiceDiscoveryEndpointsConfigPtr Endpoints;
+
+ //! Delay before sending a hedged request. If null then hedging is disabled.
+ std::optional<TDuration> HedgingDelay;
+
+ //! Whether to cancel the primary request when backup one is sent.
+ bool CancelPrimaryRequestOnHedging;
+
+ REGISTER_YSON_STRUCT(TBalancingChannelConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TBalancingChannelConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThrottlingChannelConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ //! Maximum allowed number of requests per second.
+ int RateLimit;
+
+ REGISTER_YSON_STRUCT(TThrottlingChannelConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TThrottlingChannelConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThrottlingChannelDynamicConfig
+ : public virtual NYTree::TYsonStruct
+{
+public:
+ std::optional<int> RateLimit;
+
+ REGISTER_YSON_STRUCT(TThrottlingChannelDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TThrottlingChannelDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResponseKeeperConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ //! For how long responses are kept in memory.
+ TDuration ExpirationTime;
+
+ //! Maximum time an eviction tick can spend.
+ TDuration MaxEvictionTickTime;
+
+ //! If |true| then initial warmup is enabled. In particular, #WarmupTime and #ExpirationTime are
+ //! checked against each other. If |false| then initial warmup is disabled and #WarmupTime is ignored.
+ bool EnableWarmup;
+
+ //! For how long the keeper remains passive after start and merely collects all responses.
+ TDuration WarmupTime;
+
+ REGISTER_YSON_STRUCT(TResponseKeeperConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TResponseKeeperConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcherConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ static constexpr int DefaultHeavyPoolSize = 16;
+ static constexpr int DefaultCompressionPoolSize = 8;
+ int HeavyPoolSize;
+ int CompressionPoolSize;
+
+ bool AlertOnMissingRequestInfo;
+
+ TDispatcherConfigPtr ApplyDynamic(const TDispatcherDynamicConfigPtr& dynamicConfig) const;
+
+ REGISTER_YSON_STRUCT(TDispatcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDispatcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcherDynamicConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<int> HeavyPoolSize;
+ std::optional<int> CompressionPoolSize;
+
+ std::optional<bool> AlertOnMissingRequestInfo;
+
+ REGISTER_YSON_STRUCT(TDispatcherDynamicConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDispatcherDynamicConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/dispatcher.cpp b/yt/yt/core/rpc/dispatcher.cpp
new file mode 100644
index 0000000000..eb6ef5d6c9
--- /dev/null
+++ b/yt/yt/core/rpc/dispatcher.cpp
@@ -0,0 +1,152 @@
+#include "dispatcher.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/fair_share_thread_pool.h>
+
+#include <yt/yt/core/misc/lazy_ptr.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+using namespace NBus;
+using namespace NServiceDiscovery;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcher::TImpl
+{
+public:
+ TImpl()
+ : CompressionPoolInvoker_(BIND([this] {
+ return CreatePrioritizedInvoker(CompressionPool_->GetInvoker(), "rpc_dispatcher");
+ }))
+ { }
+
+ void Configure(const TDispatcherConfigPtr& config)
+ {
+ HeavyPool_->Configure(config->HeavyPoolSize);
+ CompressionPool_->Configure(config->CompressionPoolSize);
+ FairShareCompressionPool_->Configure(config->CompressionPoolSize);
+ AlertOnMissingRequestInfo_.store(config->AlertOnMissingRequestInfo);
+ }
+
+ const IInvokerPtr& GetLightInvoker()
+ {
+ return LightQueue_->GetInvoker();
+ }
+
+ const IInvokerPtr& GetHeavyInvoker()
+ {
+ return HeavyPool_->GetInvoker();
+ }
+
+ const IPrioritizedInvokerPtr& GetPrioritizedCompressionPoolInvoker()
+ {
+ return CompressionPoolInvoker_.Value();
+ }
+
+ const IFairShareThreadPoolPtr& GetFairShareCompressionThreadPool()
+ {
+ return FairShareCompressionPool_;
+ }
+
+ bool ShouldAlertOnMissingRequestInfo()
+ {
+ return AlertOnMissingRequestInfo_.load(std::memory_order::relaxed);
+ }
+
+ const IInvokerPtr& GetCompressionPoolInvoker()
+ {
+ return CompressionPool_->GetInvoker();
+ }
+
+ IServiceDiscoveryPtr GetServiceDiscovery()
+ {
+ return ServiceDiscovery_.Acquire();
+ }
+
+ void SetServiceDiscovery(IServiceDiscoveryPtr serviceDiscovery)
+ {
+ ServiceDiscovery_.Store(std::move(serviceDiscovery));
+ }
+
+private:
+ const TActionQueuePtr LightQueue_ = New<TActionQueue>("RpcLight");
+ const IThreadPoolPtr HeavyPool_ = CreateThreadPool(TDispatcherConfig::DefaultHeavyPoolSize, "RpcHeavy");
+ const IThreadPoolPtr CompressionPool_ = CreateThreadPool(TDispatcherConfig::DefaultCompressionPoolSize, "Compression");
+ const IFairShareThreadPoolPtr FairShareCompressionPool_ = CreateFairShareThreadPool(TDispatcherConfig::DefaultCompressionPoolSize, "FSCompression");
+
+ TLazyIntrusivePtr<IPrioritizedInvoker> CompressionPoolInvoker_;
+
+ std::atomic<bool> AlertOnMissingRequestInfo_;
+
+ TAtomicIntrusivePtr<IServiceDiscovery> ServiceDiscovery_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDispatcher::TDispatcher()
+ : Impl_(std::make_unique<TImpl>())
+{ }
+
+TDispatcher::~TDispatcher() = default;
+
+TDispatcher* TDispatcher::Get()
+{
+ return LeakySingleton<TDispatcher>();
+}
+
+void TDispatcher::Configure(const TDispatcherConfigPtr& config)
+{
+ Impl_->Configure(config);
+}
+
+const IInvokerPtr& TDispatcher::GetLightInvoker()
+{
+ return Impl_->GetLightInvoker();
+}
+
+const IInvokerPtr& TDispatcher::GetHeavyInvoker()
+{
+ return Impl_->GetHeavyInvoker();
+}
+
+const IPrioritizedInvokerPtr& TDispatcher::GetPrioritizedCompressionPoolInvoker()
+{
+ return Impl_->GetPrioritizedCompressionPoolInvoker();
+}
+
+const IInvokerPtr& TDispatcher::GetCompressionPoolInvoker()
+{
+ return Impl_->GetCompressionPoolInvoker();
+}
+
+const IFairShareThreadPoolPtr& TDispatcher::GetFairShareCompressionThreadPool()
+{
+ return Impl_->GetFairShareCompressionThreadPool();
+}
+
+bool TDispatcher::ShouldAlertOnMissingRequestInfo()
+{
+ return Impl_->ShouldAlertOnMissingRequestInfo();
+}
+
+IServiceDiscoveryPtr TDispatcher::GetServiceDiscovery()
+{
+ return Impl_->GetServiceDiscovery();
+}
+
+void TDispatcher::SetServiceDiscovery(IServiceDiscoveryPtr serviceDiscovery)
+{
+ Impl_->SetServiceDiscovery(std::move(serviceDiscovery));
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/dispatcher.h b/yt/yt/core/rpc/dispatcher.h
new file mode 100644
index 0000000000..e351946563
--- /dev/null
+++ b/yt/yt/core/rpc/dispatcher.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/service_discovery/public.h>
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcher
+{
+public:
+ TDispatcher();
+ ~TDispatcher();
+
+ static TDispatcher* Get();
+
+ void Configure(const TDispatcherConfigPtr& config);
+
+ //! Returns the invoker for the single thread used to dispatch light callbacks
+ //! (e.g. discovery or request cancelation).
+ const IInvokerPtr& GetLightInvoker();
+ //! Returns the invoker for the thread pool used to dispatch heavy callbacks
+ //! (e.g. serialization).
+ const IInvokerPtr& GetHeavyInvoker();
+
+ //! Returns the invoker for the thread pool used to dispatch compression callbacks.
+ const IInvokerPtr& GetCompressionPoolInvoker();
+ //! Returns the prioritized invoker for the thread pool used to
+ //! dispatch compression callbacks. This invoker is a wrapper around compression pool invoker.
+ const IPrioritizedInvokerPtr& GetPrioritizedCompressionPoolInvoker();
+ //! Returns the fair-share thread pool with the similar semantics as previous two.
+ //! NB: this thread pool is different from the underlying thread pool beneath two previous invokers.
+ const NConcurrency::IFairShareThreadPoolPtr& GetFairShareCompressionThreadPool();
+
+ //! Returns true if alert must be issued when a request is missing request info.
+ bool ShouldAlertOnMissingRequestInfo();
+
+ NServiceDiscovery::IServiceDiscoveryPtr GetServiceDiscovery();
+ void SetServiceDiscovery(NServiceDiscovery::IServiceDiscoveryPtr serviceDiscovery);
+
+private:
+ class TImpl;
+ const std::unique_ptr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/dynamic_channel_pool.cpp b/yt/yt/core/rpc/dynamic_channel_pool.cpp
new file mode 100644
index 0000000000..a5fc835472
--- /dev/null
+++ b/yt/yt/core/rpc/dynamic_channel_pool.cpp
@@ -0,0 +1,913 @@
+#include "dynamic_channel_pool.h"
+
+#include "dispatcher.h"
+#include "client.h"
+#include "config.h"
+#include "private.h"
+#include "viable_peer_registry.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/misc/random.h>
+
+#include <yt/yt/core/utilex/random.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+#include <library/cpp/yt/small_containers/compact_set.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+
+#include <util/random/shuffle.h>
+
+#include <util/generic/algorithm.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+using namespace NThreading;
+using namespace NYTree;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDynamicChannelPool::TImpl
+ : public TRefCounted
+{
+public:
+ TImpl(
+ TDynamicChannelPoolConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TString serviceName,
+ TDiscoverRequestHook discoverRequestHook)
+ : Config_(std::move(config))
+ , ChannelFactory_(std::move(channelFactory))
+ , EndpointDescription_(std::move(endpointDescription))
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Items(*endpointAttributes)
+ .Item("service").Value(serviceName)
+ .EndMap()))
+ , ServiceName_(std::move(serviceName))
+ , DiscoverRequestHook_(std::move(discoverRequestHook))
+ , Logger(RpcClientLogger.WithTag(
+ "ChannelId: %v, Endpoint: %v, Service: %v",
+ TGuid::Create(),
+ EndpointDescription_,
+ ServiceName_))
+ , ViablePeerRegistry_(CreateViablePeerRegistry(Config_, BIND(&TImpl::CreateChannel, Unretained(this)), Logger))
+ , RandomPeerRotationExecutor_(New<TPeriodicExecutor>(
+ TDispatcher::Get()->GetLightInvoker(),
+ BIND(&TDynamicChannelPool::TImpl::MaybeEvictRandomPeer, MakeWeak(this)),
+ Config_->RandomPeerEvictionPeriod))
+ {
+ RandomPeerRotationExecutor_->Start();
+ }
+
+ TFuture<IChannelPtr> GetRandomChannel()
+ {
+ return GetChannel(
+ /*request*/ nullptr,
+ /*hedgingOptions*/ std::nullopt);
+ }
+
+ TFuture<IChannelPtr> GetChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions)
+ {
+ if (auto channel = PickViableChannel(request, hedgingOptions)) {
+ return MakeFuture(channel);
+ }
+
+ auto sessionOrError = RunDiscoverySession();
+ if (!sessionOrError.IsOK()) {
+ return MakeFuture<IChannelPtr>(TError(sessionOrError));
+ }
+
+ const auto& session = sessionOrError.Value();
+
+ // TODO(achulkov2): kill GetFinished.
+ auto future = IsRequestSticky(request)
+ ? session->GetFinished()
+ : ViablePeerRegistry_->GetPeersAvailable();
+ YT_LOG_DEBUG_IF(!future.IsSet(), "Channel requested, waiting on peers to become available");
+ return future.Apply(BIND([this_ = MakeWeak(this), request, hedgingOptions] {
+ if (auto strongThis = this_.Lock()) {
+ auto channel = strongThis->PickViableChannel(request, hedgingOptions);
+ if (!channel) {
+ // Not very likely but possible in theory.
+ THROW_ERROR strongThis->MakeNoAlivePeersError();
+ }
+ return channel;
+ } else {
+ THROW_ERROR_EXCEPTION("Cannot get channel, dynamic channel pool is being destroyed");
+ }
+ }));
+ }
+
+ void SetPeers(std::vector<TString> addresses)
+ {
+ SortUnique(addresses);
+ Shuffle(addresses.begin(), addresses.end());
+ THashSet<TString> addressSet(addresses.begin(), addresses.end());
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ std::vector<TString> addressesToRemove;
+
+ for (const auto& address : ActiveAddresses_) {
+ if (!addressSet.contains(address)) {
+ addressesToRemove.push_back(address);
+ }
+ }
+
+ for (const auto& address : BannedAddresses_) {
+ if (!addressSet.contains(address)) {
+ addressesToRemove.push_back(address);
+ }
+ }
+
+ for (const auto& address : addressesToRemove) {
+ RemovePeer(address);
+ }
+
+ DoAddPeers(addresses);
+ }
+
+ PeersSetPromise_.TrySet();
+ }
+
+ void SetPeerDiscoveryError(const TError& error)
+ {
+ {
+ auto guard = WriterGuard(SpinLock_);
+ PeerDiscoveryError_ = error;
+ }
+
+ PeersSetPromise_.TrySet();
+ }
+
+ void Terminate(const TError& error)
+ {
+ // Holds a weak reference to this class and the callback has no meaningful side-effects,
+ // so not waiting on this future is OK.
+ YT_UNUSED_FUTURE(RandomPeerRotationExecutor_->Stop());
+
+ std::vector<IChannelPtr> activeChannels;
+
+ {
+ auto guard = WriterGuard(SpinLock_);
+ Terminated_ = true;
+ TerminationError_ = error;
+ activeChannels = ViablePeerRegistry_->GetActiveChannels();
+ ViablePeerRegistry_->Clear();
+ }
+
+ for (auto& channel : activeChannels) {
+ channel->Terminate(error);
+ }
+ }
+
+private:
+ class TDiscoverySession;
+ using TDiscoverySessionPtr = TIntrusivePtr<TDiscoverySession>;
+
+ class TPeerPoller;
+ using TPeerPollerPtr = TIntrusivePtr<TPeerPoller>;
+
+ const TDynamicChannelPoolConfigPtr Config_;
+ const IChannelFactoryPtr ChannelFactory_;
+ const TString EndpointDescription_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+ const TString ServiceName_;
+ const TDiscoverRequestHook DiscoverRequestHook_;
+
+ const NLogging::TLogger Logger;
+
+ const TPromise<void> PeersSetPromise_ = NewPromise<void>();
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ bool Terminated_ = false;
+ TDiscoverySessionPtr CurrentDiscoverySession_;
+ TDelayedExecutorCookie RediscoveryCookie_;
+ TError TerminationError_;
+ TError PeerDiscoveryError_;
+
+ THashSet<TString> ActiveAddresses_;
+ THashSet<TString> BannedAddresses_;
+
+ THashMap<TString, TPeerPollerPtr> AddressToPoller_;
+
+ IViablePeerRegistryPtr ViablePeerRegistry_;
+
+ const TPeriodicExecutorPtr RandomPeerRotationExecutor_;
+
+ struct TTooManyConcurrentRequests { };
+ struct TNoMorePeers { };
+
+ using TPickPeerResult = std::variant<
+ TString,
+ TTooManyConcurrentRequests,
+ TNoMorePeers>;
+
+ class TDiscoverySession
+ : public TRefCounted
+ {
+ public:
+ explicit TDiscoverySession(TImpl* owner)
+ : Owner_(owner)
+ , Config_(owner->Config_)
+ , Logger(owner->Logger)
+ { }
+
+ TFuture<void> GetFinished()
+ {
+ return FinishedPromise_;
+ }
+
+ void Run()
+ {
+ YT_LOG_DEBUG("Starting peer discovery");
+ TDispatcher::Get()->GetLightInvoker()->Invoke(BIND(&TDiscoverySession::DoRun, MakeStrong(this)));
+ }
+
+ void OnPeerDiscovered(const TString& address)
+ {
+ AddViablePeer(address);
+ Success_.store(true);
+ }
+
+ private:
+ const TWeakPtr<TImpl> Owner_;
+ const TDynamicChannelPoolConfigPtr Config_;
+ const NLogging::TLogger Logger;
+
+ const TPromise<void> FinishedPromise_ = NewPromise<void>();
+ std::atomic<bool> Finished_ = false;
+ std::atomic<bool> Success_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ THashSet<TString> RequestedAddresses_;
+ THashSet<TString> RequestingAddresses_;
+
+ constexpr static int MaxDiscoveryErrorsToKeep = 100;
+ std::deque<TError> DiscoveryErrors_;
+
+ void DoRun()
+ {
+ auto deadline = TInstant::Now() + Config_->DiscoverySessionTimeout;
+
+ while (true) {
+ if (TInstant::Now() > deadline) {
+ OnFinished();
+ break;
+ }
+
+ auto mustBreak = false;
+ auto pickResult = PickPeer();
+ Visit(pickResult,
+ [&] (TTooManyConcurrentRequests) {
+ mustBreak = true;
+ },
+ [&] (TNoMorePeers) {
+ if (!HasOutstandingQueries()) {
+ OnFinished();
+ }
+ mustBreak = true;
+ },
+ [&] (const TString& address) {
+ QueryPeer(address);
+ });
+
+ if (mustBreak) {
+ break;
+ }
+ }
+ }
+
+ void QueryPeer(const TString& address)
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Querying peer (Address: %v)", address);
+
+ auto channel = owner->ChannelFactory_->CreateChannel(address);
+ auto proxy = owner->CreateGenericProxy(channel);
+ proxy.SetDefaultTimeout(owner->Config_->DiscoverTimeout);
+
+ auto req = proxy.Discover();
+ if (owner->DiscoverRequestHook_) {
+ owner->DiscoverRequestHook_.Run(req.Get());
+ }
+
+ // NB: Via prevents stack overflow due to QueryPeer -> OnResponse -> DoRun loop in
+ // case when Invoke() is immediately set.
+ req->Invoke().Subscribe(BIND(
+ &TDiscoverySession::OnResponse,
+ MakeStrong(this),
+ address)
+ .Via(TDispatcher::Get()->GetLightInvoker()));
+ }
+
+ void OnResponse(
+ const TString& address,
+ const TGenericProxy::TErrorOrRspDiscoverPtr& rspOrError)
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ // COMPAT(babenko): drop this once all RPC proxies support unauthenticated Discover requests
+ bool authError = rspOrError.GetCode() == NRpc::EErrorCode::AuthenticationError;
+ YT_LOG_DEBUG_IF(authError, "Peer has reported authentication error on discovery (Address: %v)",
+ address);
+ if (rspOrError.IsOK() || authError) {
+ // const auto& rsp = rspOrError.Value();
+ // bool up = rsp->up();
+ // auto suggestedAddresses = FromProto<std::vector<TString>>(rsp->suggested_addresses());
+ bool up = authError ? true : rspOrError.Value()->up();
+ auto suggestedAddresses = authError ? std::vector<TString>() : FromProto<std::vector<TString>>(rspOrError.Value()->suggested_addresses());
+
+ if (!suggestedAddresses.empty()) {
+ YT_LOG_DEBUG("Peers suggested (SuggestorAddress: %v, SuggestedAddresses: %v)",
+ address,
+ suggestedAddresses);
+ owner->AddPeers(suggestedAddresses);
+ }
+
+ YT_LOG_DEBUG("Peer has reported its state (Address: %v, Up: %v)",
+ address,
+ up);
+
+ if (up) {
+ OnPeerDiscovered(address);
+ } else {
+ auto error = owner->MakePeerDownError(address);
+ BanPeer(address, error, owner->Config_->SoftBackoffTime);
+ InvalidatePeer(address);
+ }
+ } else {
+ YT_LOG_DEBUG(rspOrError, "Peer discovery request failed (Address: %v)",
+ address);
+ auto error = owner->MakePeerDiscoveryFailedError(address, rspOrError);
+ BanPeer(address, error, owner->Config_->HardBackoffTime);
+ InvalidatePeer(address);
+ }
+
+ OnPeerQueried(address);
+ DoRun();
+ }
+
+ TPickPeerResult PickPeer()
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return TNoMorePeers();
+ }
+
+ auto guard = Guard(SpinLock_);
+ return owner->PickPeer(&RequestingAddresses_, &RequestedAddresses_);
+ }
+
+ void OnPeerQueried(const TString& address)
+ {
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(RequestingAddresses_.erase(address) == 1);
+ }
+
+ bool HasOutstandingQueries()
+ {
+ auto guard = Guard(SpinLock_);
+ return !RequestingAddresses_.empty();
+ }
+
+ void BanPeer(const TString& address, const TError& error, TDuration backoffTime)
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(RequestedAddresses_.erase(address) == 1);
+
+ DiscoveryErrors_.push_back(error);
+ while (std::ssize(DiscoveryErrors_) > MaxDiscoveryErrorsToKeep) {
+ DiscoveryErrors_.pop_front();
+ }
+ }
+
+ owner->BanPeer(address, backoffTime);
+ }
+
+ std::vector<TError> GetDiscoveryErrors()
+ {
+ auto guard = Guard(SpinLock_);
+ return {DiscoveryErrors_.begin(), DiscoveryErrors_.end()};
+ }
+
+ void AddViablePeer(const TString& address)
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ owner->AddViablePeer(address);
+ }
+
+ void InvalidatePeer(const TString& address)
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ owner->InvalidatePeer(address);
+ }
+
+ void OnFinished()
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ if (Finished_.exchange(true)) {
+ return;
+ }
+
+ if (Success_.load()) {
+ FinishedPromise_.Set();
+ } else {
+ auto error = owner->MakeNoAlivePeersError()
+ << GetDiscoveryErrors();
+ YT_LOG_DEBUG(error, "Error performing peer discovery");
+ owner->ViablePeerRegistry_->SetError(error);
+ FinishedPromise_.Set(error);
+ }
+ }
+ };
+
+ class TPeerPoller
+ : public TRefCounted
+ {
+ public:
+ TPeerPoller(TImpl* owner, TString peerAddress)
+ : Owner_(owner)
+ , Logger(owner->Logger.WithTag("Address: %v", peerAddress))
+ , PeerAddress_(std::move(peerAddress))
+ { }
+
+ void Run()
+ {
+ YT_LOG_DEBUG("Starting peer poller");
+ TDispatcher::Get()->GetLightInvoker()->Invoke(BIND(&TPeerPoller::DoRun, MakeStrong(this)));
+ }
+
+ void Stop()
+ {
+ YT_LOG_DEBUG("Stopping peer poller");
+ Stopped_ = true;
+ }
+
+ private:
+ const TWeakPtr<TImpl> Owner_;
+ const NLogging::TLogger Logger;
+
+ const TString PeerAddress_;
+
+ std::atomic<bool> Stopped_ = false;
+
+ TInstant LastRequestStart_ = TInstant::Zero();
+
+ void DoRun()
+ {
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ auto delay = RandomDuration(owner->Config_->PeerPollingPeriodSplay);
+ YT_LOG_DEBUG("Sleeping before peer polling start (Delay: %v)",
+ delay);
+ TDelayedExecutor::WaitForDuration(delay);
+ }
+
+ DoPollPeer();
+ }
+
+ void DoPollPeer(TDuration lastPeerPollingPeriod = TDuration::Zero())
+ {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ if (Stopped_) {
+ return;
+ }
+
+ auto now = TInstant::Now();
+ if (LastRequestStart_ + lastPeerPollingPeriod > now) {
+ auto delay = LastRequestStart_ + lastPeerPollingPeriod - now;
+ YT_LOG_DEBUG("Sleeping before peer polling (Delay: %v)",
+ delay);
+ TDelayedExecutor::WaitForDuration(delay);
+ }
+
+ LastRequestStart_ = now;
+ auto peerPollingPeriod = owner->Config_->PeerPollingPeriod + RandomDuration(owner->Config_->PeerPollingPeriodSplay);
+
+ auto channel = owner->ChannelFactory_->CreateChannel(PeerAddress_);
+ auto proxy = owner->CreateGenericProxy(channel);
+
+ auto requestTimeout = peerPollingPeriod + owner->Config_->PeerPollingRequestTimeout;
+ auto req = proxy.Discover();
+ req->set_reply_delay(peerPollingPeriod.GetValue());
+ req->SetTimeout(requestTimeout);
+ if (owner->DiscoverRequestHook_) {
+ owner->DiscoverRequestHook_.Run(req.Get());
+ }
+
+ YT_LOG_DEBUG("Polling peer (PollingPeriod: %v, RequestTimeout: %v)",
+ peerPollingPeriod,
+ requestTimeout);
+
+ owner.Reset();
+
+ req->Invoke()
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TGenericProxy::TErrorOrRspDiscoverPtr& rspOrError) {
+ auto owner = Owner_.Lock();
+ if (!owner) {
+ return;
+ }
+
+ if (rspOrError.IsOK()) {
+ auto isUp = rspOrError.Value()->up();
+ if (isUp) {
+ YT_LOG_DEBUG("Peer is up");
+ owner->UnbanPeer(PeerAddress_);
+ auto discoverySessionOrError = owner->RunDiscoverySession();
+ if (discoverySessionOrError.IsOK()) {
+ discoverySessionOrError.Value()->OnPeerDiscovered(PeerAddress_);
+ } else {
+ YT_LOG_DEBUG(discoverySessionOrError, "Failed to get discovery session");
+ }
+ } else {
+ YT_LOG_DEBUG("Peer is down");
+ }
+ } else {
+ YT_LOG_DEBUG(rspOrError, "Failed to poll peer");
+ }
+
+ DoPollPeer(peerPollingPeriod);
+ }).Via(TDispatcher::Get()->GetLightInvoker()));
+ }
+ };
+
+ IChannelPtr PickViableChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions)
+ {
+ return IsRequestSticky(request)
+ ? ViablePeerRegistry_->PickStickyChannel(request)
+ : ViablePeerRegistry_->PickRandomChannel(request, hedgingOptions);
+ }
+
+ TErrorOr<TDiscoverySessionPtr> RunDiscoverySession()
+ {
+ TDiscoverySessionPtr session;
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ if (Terminated_) {
+ return TError(
+ NRpc::EErrorCode::TransportError,
+ "Channel terminated")
+ << *EndpointAttributes_
+ << TerminationError_;
+ }
+
+ if (CurrentDiscoverySession_) {
+ return CurrentDiscoverySession_;
+ }
+
+ if (!ActiveAddresses_.empty() || !PeersSetPromise_.IsSet()) {
+ session = CurrentDiscoverySession_ = New<TDiscoverySession>(this);
+ }
+ }
+
+ if (!session) {
+ return MakeNoAlivePeersError();
+ }
+
+ PeersSetPromise_.ToFuture().Subscribe(
+ BIND(&TImpl::OnPeersSet, MakeWeak(this)));
+ session->GetFinished().Subscribe(
+ BIND(&TImpl::OnDiscoverySessionFinished, MakeWeak(this)));
+ return session;
+ }
+
+ TError MakeNoAlivePeersError()
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ if (PeerDiscoveryError_.IsOK()) {
+ return TError(NRpc::EErrorCode::Unavailable, "No alive peers found")
+ << *EndpointAttributes_;
+ } else {
+ return PeerDiscoveryError_;
+ }
+ }
+
+ TError MakePeerDownError(const TString& address)
+ {
+ return TError("Peer %v is down", address)
+ << *EndpointAttributes_;
+ }
+
+ TError MakePeerDiscoveryFailedError(const TString& address, const TError& error)
+ {
+ return TError("Discovery request failed for peer %v", address)
+ << *EndpointAttributes_
+ << error;
+ }
+
+ void OnPeersSet(const TError& /*error*/)
+ {
+ NTracing::TNullTraceContextGuard nullTraceContext;
+
+ TDiscoverySessionPtr session;
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ YT_VERIFY(CurrentDiscoverySession_);
+ session = CurrentDiscoverySession_;
+ }
+
+ session->Run();
+ }
+
+ void OnDiscoverySessionFinished(const TError& /*error*/)
+ {
+ NTracing::TNullTraceContextGuard nullTraceContext;
+ auto guard = WriterGuard(SpinLock_);
+
+ YT_VERIFY(CurrentDiscoverySession_);
+ CurrentDiscoverySession_.Reset();
+
+ TDelayedExecutor::CancelAndClear(RediscoveryCookie_);
+ RediscoveryCookie_ = TDelayedExecutor::Submit(
+ BIND(&TImpl::OnRediscovery, MakeWeak(this)),
+ Config_->RediscoverPeriod + RandomDuration(Config_->RediscoverSplay));
+ }
+
+ void OnRediscovery(bool aborted)
+ {
+ if (aborted) {
+ return;
+ }
+
+ Y_UNUSED(RunDiscoverySession());
+ }
+
+ void AddPeers(const std::vector<TString>& addresses)
+ {
+ auto guard = WriterGuard(SpinLock_);
+ DoAddPeers(addresses);
+ }
+
+ void DoAddPeers(const std::vector<TString>& addresses)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ PeerDiscoveryError_ = {};
+
+ std::vector<TString> newAddresses;
+ for (const auto& address : addresses) {
+ if (!BannedAddresses_.contains(address) && !ActiveAddresses_.contains(address)) {
+ newAddresses.push_back(address);
+ }
+ }
+
+ for (const auto& address : newAddresses) {
+ AddPeer(address);
+ }
+ }
+
+ void MaybeEvictRandomPeer()
+ {
+ ViablePeerRegistry_->MaybeRotateRandomPeer();
+ }
+
+ void AddPeer(const TString& address)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ YT_VERIFY(ActiveAddresses_.insert(address).second);
+
+ if (Config_->EnablePeerPolling) {
+ auto poller = New<TPeerPoller>(this, address);
+ poller->Run();
+ YT_VERIFY(AddressToPoller_.emplace(address, std::move(poller)).second);
+ }
+
+ YT_LOG_DEBUG("Peer added (Address: %v)", address);
+ }
+
+ void RemovePeer(const TString& address)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (ActiveAddresses_.erase(address) == 0 && BannedAddresses_.erase(address) == 0) {
+ return;
+ }
+
+ ViablePeerRegistry_->UnregisterPeer(address);
+
+ if (Config_->EnablePeerPolling) {
+ const auto& poller = GetOrCrash(AddressToPoller_, address);
+ poller->Stop();
+
+ YT_VERIFY(AddressToPoller_.erase(address));
+ }
+
+ YT_LOG_DEBUG("Peer removed (Address: %v)", address);
+ }
+
+ TPickPeerResult PickPeer(
+ THashSet<TString>* requestingAddresses,
+ THashSet<TString>* requestedAddresses)
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (std::ssize(*requestingAddresses) >= Config_->MaxConcurrentDiscoverRequests) {
+ return TTooManyConcurrentRequests();
+ }
+
+ std::vector<TString> candidates;
+ candidates.reserve(ActiveAddresses_.size());
+
+ for (const auto& address : ActiveAddresses_) {
+ if (requestingAddresses->find(address) == requestingAddresses->end() &&
+ requestedAddresses->find(address) == requestedAddresses->end())
+ {
+ candidates.push_back(address);
+ }
+ }
+
+ if (candidates.empty()) {
+ return TNoMorePeers();
+ }
+
+ const auto& result = candidates[RandomNumber(candidates.size())];
+ YT_VERIFY(requestedAddresses->insert(result).second);
+ YT_VERIFY(requestingAddresses->insert(result).second);
+ return result;
+ }
+
+ void BanPeer(const TString& address, TDuration backoffTime)
+ {
+ {
+ auto guard = WriterGuard(SpinLock_);
+ if (ActiveAddresses_.erase(address) != 1) {
+ return;
+ }
+ BannedAddresses_.insert(address);
+ }
+
+ YT_LOG_DEBUG("Peer banned (Address: %v, BackoffTime: %v)",
+ address,
+ backoffTime);
+
+ TDelayedExecutor::Submit(
+ BIND(&TImpl::OnPeerBanTimeout, MakeWeak(this), address),
+ backoffTime);
+ }
+
+ void UnbanPeer(const TString& address)
+ {
+ auto guard = WriterGuard(SpinLock_);
+ if (BannedAddresses_.erase(address) != 1) {
+ return;
+ }
+ ActiveAddresses_.insert(address);
+
+ YT_LOG_DEBUG("Peer unbanned (Address: %v)", address);
+ }
+
+ void OnPeerBanTimeout(const TString& address, bool aborted)
+ {
+ if (aborted) {
+ // If we are terminating -- do not unban anyone to prevent infinite retries.
+ return;
+ }
+
+ UnbanPeer(address);
+ }
+
+ void AddViablePeer(const TString& address)
+ {
+ bool added = ViablePeerRegistry_->RegisterPeer(address);
+
+ YT_LOG_DEBUG("Peer is viable (Address: %v, Added: %v)",
+ address,
+ added);
+ }
+
+ void InvalidatePeer(const TString& address)
+ {
+ ViablePeerRegistry_->UnregisterPeer(address);
+ }
+
+ void OnChannelFailed(const TString& address, const IChannelPtr& channel, const TError& error)
+ {
+ bool evicted = ViablePeerRegistry_->UnregisterChannel(address, channel);
+
+ YT_LOG_DEBUG(
+ error,
+ "Peer is no longer viable due to channel failure (Address: %v, Evicted: %v)",
+ address,
+ evicted);
+ }
+
+ TGenericProxy CreateGenericProxy(IChannelPtr peerChannel)
+ {
+ auto serviceDescriptor = TServiceDescriptor(ServiceName_)
+ .SetProtocolVersion(GenericProtocolVersion);
+ return TGenericProxy(std::move(peerChannel), serviceDescriptor);
+ }
+
+ IChannelPtr CreateChannel(const TString& address)
+ {
+ return CreateFailureDetectingChannel(
+ ChannelFactory_->CreateChannel(address),
+ Config_->AcknowledgementTimeout,
+ BIND(&TImpl::OnChannelFailed, MakeWeak(this), address));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDynamicChannelPool::TDynamicChannelPool(
+ TDynamicChannelPoolConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ IAttributeDictionaryPtr endpointAttributes,
+ TString serviceName,
+ TDiscoverRequestHook discoverRequestHook)
+ : Impl_(New<TImpl>(
+ std::move(config),
+ std::move(channelFactory),
+ std::move(endpointDescription),
+ std::move(endpointAttributes),
+ std::move(serviceName),
+ std::move(discoverRequestHook)))
+{ }
+
+TDynamicChannelPool::~TDynamicChannelPool() = default;
+
+TFuture<IChannelPtr> TDynamicChannelPool::GetRandomChannel()
+{
+ return Impl_->GetRandomChannel();
+}
+
+TFuture<IChannelPtr> TDynamicChannelPool::GetChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions)
+{
+ return Impl_->GetChannel(request, hedgingOptions);
+}
+
+void TDynamicChannelPool::SetPeers(const std::vector<TString>& addresses)
+{
+ Impl_->SetPeers(addresses);
+}
+
+void TDynamicChannelPool::SetPeerDiscoveryError(const TError& error)
+{
+ Impl_->SetPeerDiscoveryError(error);
+}
+
+void TDynamicChannelPool::Terminate(const TError& error)
+{
+ Impl_->Terminate(error);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/dynamic_channel_pool.h b/yt/yt/core/rpc/dynamic_channel_pool.h
new file mode 100644
index 0000000000..30483e339c
--- /dev/null
+++ b/yt/yt/core/rpc/dynamic_channel_pool.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "public.h"
+#include "hedging_channel.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: The internal mechanism performs different kinds of polling on the provided addresses.
+// This includes sequentially sending Discover requests to potentially all of them.
+// Thus, you should avoid passing an aggressively-caching channel factory to the pool if the amount of peers is large
+// and your system's file descriptor limit is low.
+// In any case, since this itself is a caching-like structure, adding another caching layer is pointless (and even dangerous).
+class TDynamicChannelPool
+ : public TRefCounted
+{
+public:
+ TDynamicChannelPool(
+ TDynamicChannelPoolConfigPtr config,
+ IChannelFactoryPtr channelFactory,
+ TString endpointDescription,
+ NYTree::IAttributeDictionaryPtr endpointAttributes,
+ TString serviceName,
+ TDiscoverRequestHook discoverRequestHook = {});
+ ~TDynamicChannelPool();
+
+ TFuture<IChannelPtr> GetRandomChannel();
+ TFuture<IChannelPtr> GetChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions = std::nullopt);
+
+ void SetPeers(const std::vector<TString>& addresses);
+ void SetPeerDiscoveryError(const TError& error);
+
+ void Terminate(const TError& error);
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TDynamicChannelPool)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/grpc/channel.cpp b/yt/yt/core/rpc/grpc/channel.cpp
new file mode 100644
index 0000000000..0bb8504c88
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/channel.cpp
@@ -0,0 +1,728 @@
+#include "channel.h"
+#include "config.h"
+#include "dispatcher.h"
+#include "helpers.h"
+
+#include <yt/yt/core/misc/singleton.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/rpc/channel.h>
+#include <yt/yt/core/rpc/channel_detail.h>
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc.h>
+#include <contrib/libs/grpc/src/core/lib/channel/call_tracer.h>
+#include <contrib/libs/grpc/src/core/lib/surface/call.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <array>
+
+namespace NYT::NRpc::NGrpc {
+namespace {
+
+using namespace NRpc;
+using namespace NYTree;
+using namespace NYson;
+using namespace NConcurrency;
+using namespace NBus;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcCallTracer final
+ : public grpc_core::CallTracer
+{
+public:
+ CallAttemptTracer* StartNewAttempt(bool /*is_transparent_retry*/) override
+ {
+ return &AttemptTracer_;
+ }
+
+ TError GetError()
+ {
+ return AttemptTracer_.GetError();
+ }
+
+private:
+ class TAttemptTracer
+ : public CallAttemptTracer
+ {
+ public:
+ TAttemptTracer()
+ : Error_(GRPC_ERROR_NONE)
+ { }
+
+ ~TAttemptTracer() override
+ {
+ GRPC_ERROR_UNREF(Error_.get());
+ }
+
+ void RecordSendInitialMetadata(
+ grpc_metadata_batch* /*send_initial_metadata*/) override
+ { }
+
+ void RecordOnDoneSendInitialMetadata(gpr_atm* /*peer_string*/) override
+ { }
+
+ void RecordSendTrailingMetadata(grpc_metadata_batch* /*send_trailing_metadata*/) override
+ { }
+
+ void RecordSendMessage(const grpc_core::SliceBuffer& /*send_message*/) override
+ { }
+
+ void RecordReceivedInitialMetadata(
+ grpc_metadata_batch* /*recv_initial_metadata*/,
+ uint32_t /*flags*/) override
+ { }
+
+ void RecordReceivedMessage(const grpc_core::SliceBuffer& /*recv_message*/) override
+ { }
+
+ void RecordReceivedTrailingMetadata(
+ y_absl::Status /*status*/,
+ grpc_metadata_batch* /*recv_trailing_metadata*/,
+ const grpc_transport_stream_stats* /*transport_stream_stats*/) override
+ { }
+
+ void RecordCancel(grpc_error_handle cancelError) override
+ {
+ auto current = Error_.get();
+ Error_.set(cancelError);
+ GRPC_ERROR_UNREF(current);
+ }
+
+ void RecordEnd(const gpr_timespec& /*latency*/) override
+ { }
+
+ TError GetError()
+ {
+ auto error = Error_.get();
+ intptr_t statusCode;
+ if (!grpc_error_get_int(error, GRPC_ERROR_INT_GRPC_STATUS, &statusCode)) {
+ statusCode = GRPC_STATUS_UNKNOWN;
+ }
+ TString statusDetail;
+ if (!grpc_error_get_str(error, GRPC_ERROR_STR_DESCRIPTION, &statusDetail)) {
+ statusDetail = "Unknown error";
+ }
+
+ return TError(StatusCodeToErrorCode(static_cast<grpc_status_code>(statusCode)), statusDetail)
+ << TErrorAttribute("status_code", statusCode);
+ }
+
+ private:
+ AtomicError Error_;
+ };
+
+ TAttemptTracer AttemptTracer_;
+};
+
+DECLARE_REFCOUNTED_TYPE(TGrpcCallTracer)
+DEFINE_REFCOUNTED_TYPE(TGrpcCallTracer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TChannel)
+
+DEFINE_ENUM(EClientCallStage,
+ (SendingRequest)
+ (ReceivingInitialMetadata)
+ (ReceivingResponse)
+);
+
+class TChannel
+ : public IChannel
+{
+public:
+ explicit TChannel(TChannelConfigPtr config)
+ : Config_(std::move(config))
+ , EndpointAddress_(Config_->Address)
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("address").Value(EndpointAddress_)
+ .EndMap()))
+ , Credentials_(
+ Config_->Credentials ?
+ LoadChannelCredentials(Config_->Credentials) :
+ TGrpcChannelCredentialsPtr(grpc_insecure_credentials_create()))
+ {
+ TGrpcChannelArgs args(Config_->GrpcArguments);
+ Channel_ = TGrpcChannelPtr(grpc_channel_create(
+ Config_->Address.c_str(),
+ Credentials_.Unwrap(),
+ args.Unwrap()));
+ }
+
+ // IChannel implementation.
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointAddress_;
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+ if (!TerminationError_.IsOK()) {
+ auto error = TerminationError_;
+ guard.Release();
+ responseHandler->HandleError(error);
+ return nullptr;
+ }
+ return New<TCallHandler>(
+ this,
+ options,
+ std::move(request),
+ std::move(responseHandler));
+ }
+
+ void Terminate(const TError& error) override
+ {
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ if (!TerminationError_.IsOK()) {
+ return;
+ }
+
+ TerminationError_ = error;
+ LibraryLock_.Reset();
+ Channel_.Reset();
+ }
+
+ Terminated_.Fire(TerminationError_);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Subscribe(callback);
+ }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Unsubscribe(callback);
+ }
+
+ // Custom methods.
+ const TString& GetEndpointAddress() const
+ {
+ return EndpointAddress_;
+ }
+
+private:
+ const TChannelConfigPtr Config_;
+ const TString EndpointAddress_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+
+ TSingleShotCallbackList<void(const TError&)> Terminated_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+ TError TerminationError_;
+ TGrpcLibraryLockPtr LibraryLock_ = TDispatcher::Get()->GetLibraryLock();
+ TGrpcChannelPtr Channel_;
+ TGrpcChannelCredentialsPtr Credentials_;
+
+
+ class TCallHandler
+ : public TCompletionQueueTag
+ , public TClientRequestPerformanceProfiler
+ {
+ public:
+ TCallHandler(
+ TChannelPtr owner,
+ const TSendOptions& options,
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler)
+ : TClientRequestPerformanceProfiler(request->GetService(), request->GetMethod())
+ , Owner_(std::move(owner))
+ , Options_(options)
+ , Request_(std::move(request))
+ , ResponseHandler_(std::move(responseHandler))
+ , GuardedCompletionQueue_(TDispatcher::Get()->PickRandomGuardedCompletionQueue())
+ , Logger(GrpcLogger)
+ {
+ YT_LOG_DEBUG("Sending request (RequestId: %v, Method: %v.%v, Timeout: %v)",
+ Request_->GetRequestId(),
+ Request_->GetService(),
+ Request_->GetMethod(),
+ Options_.Timeout);
+
+ {
+ auto completionQueueGuard = GuardedCompletionQueue_->TryLock();
+ if (!completionQueueGuard) {
+ NotifyError("Failed to initialize request call", TError{"Completion queue is shut down"});
+ return;
+ }
+
+ auto methodSlice = BuildGrpcMethodString();
+ Call_ = TGrpcCallPtr(grpc_channel_create_call(
+ Owner_->Channel_.Unwrap(),
+ nullptr,
+ 0,
+ completionQueueGuard->Unwrap(),
+ methodSlice,
+ nullptr,
+ GetDeadline(),
+ nullptr));
+ grpc_slice_unref(methodSlice);
+
+ Tracer_ = New<TGrpcCallTracer>();
+ grpc_call_context_set(Call_.Unwrap(), GRPC_CONTEXT_CALL_TRACER, Tracer_.Get(), [] (void* ptr) {
+ NYT::Unref(static_cast<TGrpcCallTracer*>(ptr));
+ });
+ NYT::Ref(Tracer_.Get());
+ }
+ InitialMetadataBuilder_.Add(RequestIdMetadataKey, ToString(Request_->GetRequestId()));
+ InitialMetadataBuilder_.Add(UserMetadataKey, Request_->GetUser());
+ if (Request_->GetUserTag()) {
+ InitialMetadataBuilder_.Add(UserTagMetadataKey, Request_->GetUserTag());
+ }
+
+ TProtocolVersion protocolVersion{
+ Request_->Header().protocol_version_major(),
+ Request_->Header().protocol_version_minor()
+ };
+
+ InitialMetadataBuilder_.Add(ProtocolVersionMetadataKey, ToString(protocolVersion));
+
+ if (Request_->Header().HasExtension(NRpc::NProto::TCredentialsExt::credentials_ext)) {
+ const auto& credentialsExt = Request_->Header().GetExtension(NRpc::NProto::TCredentialsExt::credentials_ext);
+ if (credentialsExt.has_token()) {
+ InitialMetadataBuilder_.Add(AuthTokenMetadataKey, credentialsExt.token());
+ }
+ if (credentialsExt.has_session_id()) {
+ InitialMetadataBuilder_.Add(AuthSessionIdMetadataKey, credentialsExt.session_id());
+ }
+ if (credentialsExt.has_ssl_session_id()) {
+ InitialMetadataBuilder_.Add(AuthSslSessionIdMetadataKey, credentialsExt.ssl_session_id());
+ }
+ if (credentialsExt.has_user_ticket()) {
+ InitialMetadataBuilder_.Add(AuthUserTicketMetadataKey, credentialsExt.user_ticket());
+ }
+ }
+
+ if (const auto traceContext = NTracing::TryGetCurrentTraceContext()) {
+ InitialMetadataBuilder_.Add(TracingTraceIdMetadataKey, ToString(traceContext->GetTraceId()));
+ InitialMetadataBuilder_.Add(TracingSpanIdMetadataKey, ToString(traceContext->GetSpanId()));
+ if (traceContext->IsSampled()) {
+ InitialMetadataBuilder_.Add(TracingSampledMetadataKey, "1");
+ }
+ if (traceContext->IsDebug()) {
+ InitialMetadataBuilder_.Add(TracingDebugMetadataKey, "1");
+ }
+ }
+
+ try {
+ RequestBody_ = Request_->Serialize();
+ } catch (const std::exception& ex) {
+ auto responseHandler = TryAcquireResponseHandler();
+ YT_VERIFY(responseHandler);
+ responseHandler->HandleError(TError(NRpc::EErrorCode::TransportError, "Request serialization failed")
+ << ex);
+ return;
+ }
+
+ YT_VERIFY(RequestBody_.Size() >= 2);
+ TMessageWithAttachments messageWithAttachments;
+ if (Request_->IsLegacyRpcCodecsEnabled()) {
+ messageWithAttachments.Message = ExtractMessageFromEnvelopedMessage(RequestBody_[1]);
+ } else {
+ messageWithAttachments.Message = RequestBody_[1];
+ }
+
+ for (int index = 2; index < std::ssize(RequestBody_); ++index) {
+ messageWithAttachments.Attachments.push_back(RequestBody_[index]);
+ }
+
+ RequestBodyBuffer_ = MessageWithAttachmentsToByteBuffer(messageWithAttachments);
+ if (!messageWithAttachments.Attachments.empty()) {
+ InitialMetadataBuilder_.Add(MessageBodySizeMetadataKey, ToString(messageWithAttachments.Message.Size()));
+ }
+
+ Ref();
+ Stage_ = EClientCallStage::SendingRequest;
+
+ std::array<grpc_op, 3> ops;
+
+ ops[0].op = GRPC_OP_SEND_INITIAL_METADATA;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.send_initial_metadata.maybe_compression_level.is_set = false;
+ ops[0].data.send_initial_metadata.metadata = InitialMetadataBuilder_.Unwrap();
+ ops[0].data.send_initial_metadata.count = InitialMetadataBuilder_.GetSize();
+
+ ops[1].op = GRPC_OP_SEND_MESSAGE;
+ ops[1].data.send_message.send_message = RequestBodyBuffer_.Unwrap();
+ ops[1].flags = 0;
+ ops[1].reserved = nullptr;
+
+ ops[2].op = GRPC_OP_SEND_CLOSE_FROM_CLIENT;
+ ops[2].flags = 0;
+ ops[2].reserved = nullptr;
+
+ if (!TryStartBatch(ops)) {
+ Unref();
+ }
+ }
+
+ // TCompletionQueueTag overrides
+ void Run(bool success, int /*cookie*/) override
+ {
+ const auto guard = TraceContext_.MakeTraceContextGuard();
+
+ auto error = success ? TError() : Tracer_->GetError();
+
+ switch (Stage_) {
+ case EClientCallStage::SendingRequest:
+ OnRequestSent(error);
+ break;
+
+ case EClientCallStage::ReceivingInitialMetadata:
+ OnInitialMetadataReceived(error);
+ break;
+
+ case EClientCallStage::ReceivingResponse:
+ OnResponseReceived(error);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ // IClientRequestControl overrides
+ void Cancel() override
+ {
+ auto result = grpc_call_cancel(Call_.Unwrap(), nullptr);
+ YT_VERIFY(result == GRPC_CALL_OK);
+
+ YT_LOG_DEBUG("Request canceled (RequestId: %v)", Request_->GetRequestId());
+
+ NotifyError(
+ TStringBuf("Request canceled"),
+ TError(NYT::EErrorCode::Canceled, "Request canceled"));
+ }
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& /*payload*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& /*feedback*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ private:
+ const TChannelPtr Owner_;
+ const TSendOptions Options_;
+ const IClientRequestPtr Request_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ResponseHandlerLock_);
+ IClientResponseHandlerPtr ResponseHandler_;
+
+ // Completion queue must be accessed under read lock
+ // in order to prohibit creating new requests after shutting completion queue down.
+ TGuardedGrpcCompletionQueuePtr* GuardedCompletionQueue_;
+ const NLogging::TLogger& Logger;
+
+ NYT::NTracing::TTraceContextHandler TraceContext_;
+
+ TGrpcCallPtr Call_;
+ TGrpcCallTracerPtr Tracer_;
+ TSharedRefArray RequestBody_;
+ TGrpcByteBufferPtr RequestBodyBuffer_;
+ TGrpcMetadataArray ResponseInitialMetadata_;
+ TGrpcByteBufferPtr ResponseBodyBuffer_;
+ TGrpcMetadataArray ResponseFinalMetadata_;
+ grpc_status_code ResponseStatusCode_ = GRPC_STATUS_UNKNOWN;
+ TGrpcSlice ResponseStatusDetails_;
+
+ EClientCallStage Stage_;
+
+ TGrpcMetadataArrayBuilder InitialMetadataBuilder_;
+
+
+ IClientResponseHandlerPtr TryAcquireResponseHandler()
+ {
+ IClientResponseHandlerPtr result;
+
+ auto guard = Guard(ResponseHandlerLock_);
+
+ // NB! Reset response handler explicitly.
+ // Implicit destruction in ~TCallHandler is impossible
+ // because of the following cycle dependency:
+ // futureState -> TCallHandler -> responseHandler -> futureState.
+ result.Swap(ResponseHandler_);
+
+ return result;
+ }
+
+ //! Builds /<service>/<method> string.
+ grpc_slice BuildGrpcMethodString()
+ {
+ auto length =
+ 1 + // slash
+ Request_->GetService().length() +
+ 1 + // slash
+ Request_->GetMethod().length();
+ auto slice = grpc_slice_malloc(length);
+ auto* ptr = GRPC_SLICE_START_PTR(slice);
+ *ptr++ = '/';
+ ::memcpy(ptr, Request_->GetService().c_str(), Request_->GetService().length());
+ ptr += Request_->GetService().length();
+ *ptr++ = '/';
+ ::memcpy(ptr, Request_->GetMethod().c_str(), Request_->GetMethod().length());
+ ptr += Request_->GetMethod().length();
+ YT_ASSERT(ptr == GRPC_SLICE_END_PTR(slice));
+ return slice;
+ }
+
+ gpr_timespec GetDeadline() const
+ {
+ return Options_.Timeout
+ ? gpr_time_add(
+ gpr_now(GPR_CLOCK_REALTIME),
+ gpr_time_from_micros(Options_.Timeout->MicroSeconds(), GPR_TIMESPAN))
+ : gpr_inf_future(GPR_CLOCK_REALTIME);
+ }
+
+ void OnRequestSent(const TError& error)
+ {
+ if (!error.IsOK()) {
+ NotifyError(TStringBuf("Failed to send request"), error);
+ Unref();
+ return;
+ }
+
+ YT_LOG_DEBUG("Request sent (RequestId: %v, Method: %v.%v)",
+ Request_->GetRequestId(),
+ Request_->GetService(),
+ Request_->GetMethod());
+
+ ProfileRequest(RequestBody_);
+
+ Stage_ = EClientCallStage::ReceivingInitialMetadata;
+
+ std::array<grpc_op, 1> ops;
+
+ ops[0].op = GRPC_OP_RECV_INITIAL_METADATA;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.recv_initial_metadata.recv_initial_metadata = ResponseInitialMetadata_.Unwrap();
+
+ if (!TryStartBatch(ops)) {
+ Unref();
+ }
+ }
+
+ void OnInitialMetadataReceived(const TError& error)
+ {
+ if (!error.IsOK()) {
+ NotifyError(TStringBuf("Failed to receive initial response metadata"), error);
+ Unref();
+ return;
+ }
+
+ ProfileAcknowledgement();
+ YT_LOG_DEBUG("Initial response metadata received (RequestId: %v)",
+ Request_->GetRequestId());
+
+ Stage_ = EClientCallStage::ReceivingResponse;
+
+ std::array<grpc_op, 2> ops;
+
+ ops[0].op = GRPC_OP_RECV_MESSAGE;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.recv_message.recv_message = ResponseBodyBuffer_.GetPtr();
+
+ ops[1].op = GRPC_OP_RECV_STATUS_ON_CLIENT;
+ ops[1].flags = 0;
+ ops[1].reserved = nullptr;
+ ops[1].data.recv_status_on_client.trailing_metadata = ResponseFinalMetadata_.Unwrap();
+ ops[1].data.recv_status_on_client.status = &ResponseStatusCode_;
+ ops[1].data.recv_status_on_client.status_details = ResponseStatusDetails_.Unwrap();
+ ops[1].data.recv_status_on_client.error_string = nullptr;
+
+ if (!TryStartBatch(ops)) {
+ Unref();
+ }
+ }
+
+ void OnResponseReceived(const TError& error)
+ {
+ auto guard = Finally([this] { Unref(); });
+
+ if (!error.IsOK()) {
+ NotifyError(TStringBuf("Failed to receive response"), error);
+ return;
+ }
+
+ if (ResponseStatusCode_ != GRPC_STATUS_OK) {
+ TError error;
+ auto serializedError = ResponseFinalMetadata_.Find(ErrorMetadataKey);
+ if (serializedError) {
+ error = DeserializeError(serializedError);
+ } else {
+ error = TError(StatusCodeToErrorCode(ResponseStatusCode_), ResponseStatusDetails_.AsString())
+ << TErrorAttribute("status_code", ResponseStatusCode_);
+ }
+ NotifyError(TStringBuf("Request failed"), error);
+ return;
+ }
+
+ if (!ResponseBodyBuffer_) {
+ auto error = TError(NRpc::EErrorCode::ProtocolError, "Empty response body");
+ NotifyError(TStringBuf("Request failed"), error);
+ return;
+ }
+
+ std::optional<ui32> messageBodySize;
+
+ auto messageBodySizeString = ResponseFinalMetadata_.Find(MessageBodySizeMetadataKey);
+ if (messageBodySizeString) {
+ try {
+ messageBodySize = FromString<ui32>(messageBodySizeString);
+ } catch (const std::exception& ex) {
+ auto error = TError(NRpc::EErrorCode::TransportError, "Failed to parse response message body size")
+ << ex;
+ NotifyError(TStringBuf("Failed to parse response message body size"), error);
+ return;
+ }
+ }
+
+ TMessageWithAttachments messageWithAttachments;
+ try {
+ messageWithAttachments = ByteBufferToMessageWithAttachments(
+ ResponseBodyBuffer_.Unwrap(),
+ messageBodySize);
+ } catch (const std::exception& ex) {
+ auto error = TError(NRpc::EErrorCode::TransportError, "Failed to receive request body") << ex;
+ NotifyError(TStringBuf("Failed to receive request body"), error);
+ return;
+ }
+
+ NRpc::NProto::TResponseHeader responseHeader;
+ ToProto(responseHeader.mutable_request_id(), Request_->GetRequestId());
+
+ auto responseMessage = CreateResponseMessage(
+ responseHeader,
+ messageWithAttachments.Message,
+ messageWithAttachments.Attachments);
+
+ ProfileReply(responseMessage);
+ NotifyResponse(std::move(responseMessage));
+ }
+
+ template <class TOps>
+ bool TryStartBatch(const TOps& ops)
+ {
+
+ auto completionQueueGuard = GuardedCompletionQueue_->TryLock();
+ if (!completionQueueGuard) {
+ NotifyError("Failed to enqueue request operations batch", TError{"Completion queue is shut down"});
+ return false;
+ }
+
+ auto result = grpc_call_start_batch(
+ Call_.Unwrap(),
+ ops.data(),
+ ops.size(),
+ GetTag(),
+ nullptr);
+ YT_VERIFY(result == GRPC_CALL_OK);
+ return true;
+ }
+
+ void NotifyError(TStringBuf reason, const TError& error)
+ {
+ auto responseHandler = TryAcquireResponseHandler();
+ if (!responseHandler) {
+ return;
+ }
+
+ auto detailedError = error
+ << TErrorAttribute("realm_id", Request_->GetRealmId())
+ << TErrorAttribute("service", Request_->GetService())
+ << TErrorAttribute("method", Request_->GetMethod())
+ << TErrorAttribute("request_id", Request_->GetRequestId())
+ << Owner_->GetEndpointAttributes();
+ if (Options_.Timeout) {
+ detailedError = detailedError
+ << TErrorAttribute("timeout", Options_.Timeout);
+ }
+
+ ProfileError(error);
+ YT_LOG_DEBUG(detailedError, "%v (RequestId: %v)",
+ reason,
+ Request_->GetRequestId());
+
+ responseHandler->HandleError(detailedError);
+ }
+
+ void NotifyResponse(TSharedRefArray message)
+ {
+ auto responseHandler = TryAcquireResponseHandler();
+ if (!responseHandler) {
+ return;
+ }
+
+ auto elapsed = ProfileComplete();
+ YT_LOG_DEBUG("Response received (RequestId: %v, Method: %v.%v, TotalTime: %v)",
+ Request_->GetRequestId(),
+ Request_->GetService(),
+ Request_->GetMethod(),
+ elapsed);
+
+ responseHandler->HandleResponse(
+ std::move(message),
+ /*address*/ Owner_->GetEndpointAddress());
+ }
+ };
+};
+
+DEFINE_REFCOUNTED_TYPE(TChannel)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChannelFactory
+ : public IChannelFactory
+{
+public:
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto config = New<TChannelConfig>();
+ config->Address = address;
+ return CreateGrpcChannel(config);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+IChannelPtr CreateGrpcChannel(TChannelConfigPtr config)
+{
+ return New<TChannel>(std::move(config));
+}
+
+IChannelFactoryPtr GetGrpcChannelFactory()
+{
+ return LeakyRefCountedSingleton<TChannelFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/channel.h b/yt/yt/core/rpc/grpc/channel.h
new file mode 100644
index 0000000000..1a11b35254
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/channel.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a channel implemented via GRPC.
+NRpc::IChannelPtr CreateGrpcChannel(TChannelConfigPtr config);
+
+//! Returns the factory for creating GRPC channels.
+NRpc::IChannelFactoryPtr GetGrpcChannelFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/config.cpp b/yt/yt/core/rpc/grpc/config.cpp
new file mode 100644
index 0000000000..7a4a4a0d36
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/config.cpp
@@ -0,0 +1,88 @@
+#include "config.h"
+
+#include <yt/yt/core/crypto/config.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TDispatcherConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("dispatcher_thread_count", &TThis::DispatcherThreadCount)
+ .Alias("thread_count")
+ .GreaterThan(0)
+ .Default(4);
+
+ registrar.Parameter("grpc_thread_count", &TThis::GrpcThreadCount)
+ .GreaterThan(0)
+ .Default(4);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSslPemKeyCertPairConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("private_key", &TThis::PrivateKey)
+ .Optional();
+ registrar.Parameter("cert_chain", &TThis::CertChain)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerCredentialsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("pem_root_certs", &TThis::PemRootCerts)
+ .Optional();
+ registrar.Parameter("pem_key_cert_pairs", &TThis::PemKeyCertPairs);
+ registrar.Parameter("client_certificate_request", &TThis::ClientCertificateRequest)
+ .Default(EClientCertificateRequest::RequestAndRequireClientCertificateAndVerify);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerAddressConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("address", &TThis::Address);
+ registrar.Parameter("credentials", &TThis::Credentials)
+ .Optional();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("profiling_name", &TThis::ProfilingName)
+ .Default("none");
+ registrar.Parameter("addresses", &TThis::Addresses);
+ registrar.Parameter("grpc_arguments", &TThis::GrpcArguments)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChannelCredentialsConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("pem_root_certs", &TThis::PemRootCerts)
+ .Optional();
+ registrar.Parameter("pem_key_cert_pair", &TThis::PemKeyCertPair)
+ .Optional();
+ registrar.Parameter("verify_server_cert", &TThis::VerifyServerCert)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TChannelConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("address", &TThis::Address)
+ .Optional();
+ registrar.Parameter("credentials", &TThis::Credentials)
+ .Optional();
+ registrar.Parameter("grpc_arguments", &TThis::GrpcArguments)
+ .Default();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/config.h b/yt/yt/core/rpc/grpc/config.h
new file mode 100644
index 0000000000..1575e46901
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/config.h
@@ -0,0 +1,142 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/crypto/public.h>
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc_security_constants.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcherConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ int DispatcherThreadCount;
+ int GrpcThreadCount;
+
+ REGISTER_YSON_STRUCT(TDispatcherConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TDispatcherConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSslPemKeyCertPairConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NCrypto::TPemBlobConfigPtr PrivateKey;
+ NCrypto::TPemBlobConfigPtr CertChain;
+
+ REGISTER_YSON_STRUCT(TSslPemKeyCertPairConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TSslPemKeyCertPairConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EClientCertificateRequest,
+ ((DontRequestClientCertificate)(GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE))
+ ((RequestClientCertificateButDontVerify)(GRPC_SSL_REQUEST_CLIENT_CERTIFICATE_BUT_DONT_VERIFY))
+ ((RequestClientCertificateAndVerify)(GRPC_SSL_REQUEST_CLIENT_CERTIFICATE_AND_VERIFY))
+ ((RequestAndRequireClientCertificateButDontVerify)(GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_BUT_DONT_VERIFY))
+ ((RequestAndRequireClientCertificateAndVerify)(GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerCredentialsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NCrypto::TPemBlobConfigPtr PemRootCerts;
+ std::vector<TSslPemKeyCertPairConfigPtr> PemKeyCertPairs;
+ EClientCertificateRequest ClientCertificateRequest;
+
+ REGISTER_YSON_STRUCT(TServerCredentialsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerCredentialsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerAddressConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString Address;
+ TServerCredentialsConfigPtr Credentials;
+
+ REGISTER_YSON_STRUCT(TServerAddressConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerAddressConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString ProfilingName;
+
+ std::vector<TServerAddressConfigPtr> Addresses;
+ THashMap<TString, NYTree::INodePtr> GrpcArguments;
+
+ REGISTER_YSON_STRUCT(TServerConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServerConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChannelCredentialsConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ NCrypto::TPemBlobConfigPtr PemRootCerts;
+ TSslPemKeyCertPairConfigPtr PemKeyCertPair;
+ bool VerifyServerCert;
+
+ REGISTER_YSON_STRUCT(TChannelCredentialsConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChannelCredentialsConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TChannelConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ TString Address;
+ TChannelCredentialsConfigPtr Credentials;
+ THashMap<TString, NYTree::INodePtr> GrpcArguments;
+
+ REGISTER_YSON_STRUCT(TChannelConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TChannelConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/dispatcher.cpp b/yt/yt/core/rpc/grpc/dispatcher.cpp
new file mode 100644
index 0000000000..a0ef4511e1
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/dispatcher.cpp
@@ -0,0 +1,230 @@
+#include "dispatcher.h"
+
+#include "config.h"
+
+#include <yt/yt/core/threading/thread.h>
+
+#include <yt/yt/core/misc/shutdown_priorities.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc.h>
+
+#include <contrib/libs/grpc/src/core/lib/iomgr/executor.h>
+
+#include <atomic>
+
+namespace NYT::NRpc::NGrpc {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = GrpcLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void* TCompletionQueueTag::GetTag(int cookie)
+{
+ YT_ASSERT(cookie >= 0 && cookie < 8);
+ return reinterpret_cast<void*>(reinterpret_cast<intptr_t>(this) | cookie);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGrpcLibraryLock::TGrpcLibraryLock()
+{
+ YT_LOG_INFO("Initializing GRPC library");
+ grpc_init_openssl();
+ grpc_init();
+}
+
+TGrpcLibraryLock::~TGrpcLibraryLock()
+{
+ YT_LOG_INFO("Shutting down GRPC library");
+ grpc_shutdown();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcher::TImpl
+{
+public:
+ [[nodiscard]] bool IsConfigured() const noexcept
+ {
+ return Configured_.load();
+ }
+
+ void Configure(const TDispatcherConfigPtr& config)
+ {
+ auto guard = Guard(ConfigureLock_);
+
+ if (IsConfigured()) {
+ THROW_ERROR_EXCEPTION("GPRC dispatcher is already configured");
+ }
+
+ DoConfigure(config);
+ }
+
+ TGrpcLibraryLockPtr GetLibraryLock()
+ {
+ EnsureConfigured();
+ auto grpcLock = LibraryLock_.Lock();
+ YT_VERIFY(grpcLock);
+ return grpcLock;
+ }
+
+ TGuardedGrpcCompletionQueuePtr* PickRandomGuardedCompletionQueue()
+ {
+ EnsureConfigured();
+ return Threads_[RandomNumber<size_t>() % Threads_.size()]->GetGuardedCompletionQueue();
+ }
+
+private:
+ class TDispatcherThread
+ : public NThreading::TThread
+ {
+ public:
+ TDispatcherThread(TGrpcLibraryLockPtr libraryLock, int index)
+ : TThread(
+ Format("Grpc/%v", index),
+ NThreading::EThreadPriority::Normal,
+ GrpcDispatcherThreadShutdownPriority)
+ , LibraryLock_(std::move(libraryLock))
+ , GuardedCompletionQueue_(TGrpcCompletionQueuePtr(grpc_completion_queue_create_for_next(nullptr)))
+ {
+ Start();
+ }
+
+ TGuardedGrpcCompletionQueuePtr* GetGuardedCompletionQueue()
+ {
+ return &GuardedCompletionQueue_;
+ }
+
+ private:
+ TGrpcLibraryLockPtr LibraryLock_;
+ TGuardedGrpcCompletionQueuePtr GuardedCompletionQueue_;
+
+ void StopPrologue() override
+ {
+ GuardedCompletionQueue_.Shutdown();
+ }
+
+ void StopEpilogue() override
+ {
+ GuardedCompletionQueue_.Reset();
+ LibraryLock_.Reset();
+ }
+
+ void ThreadMain() override
+ {
+ YT_LOG_DEBUG("Dispatcher thread started");
+
+ // Take raw completion queue for fetching tasks,
+ // because `grpc_completion_queue_next` can be concurrent with other operations.
+ grpc_completion_queue* completionQueue = GuardedCompletionQueue_.UnwrapUnsafe();
+
+ bool done = false;
+ while (!done) {
+ auto event = grpc_completion_queue_next(
+ completionQueue,
+ gpr_inf_future(GPR_CLOCK_REALTIME),
+ nullptr);
+ switch (event.type) {
+ case GRPC_OP_COMPLETE:
+ if (event.tag) {
+ auto* typedTag = reinterpret_cast<TCompletionQueueTag*>(reinterpret_cast<intptr_t>(event.tag) & ~7);
+ auto cookie = reinterpret_cast<intptr_t>(event.tag) & 7;
+ typedTag->Run(event.success != 0, cookie);
+ }
+ break;
+
+ case GRPC_QUEUE_SHUTDOWN:
+ done = true;
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ YT_LOG_DEBUG("Dispatcher thread stopped");
+ }
+ };
+
+ using TDispatcherThreadPtr = TIntrusivePtr<TDispatcherThread>;
+
+ void EnsureConfigured()
+ {
+ if (IsConfigured()) {
+ return;
+ }
+
+ auto guard = Guard(ConfigureLock_);
+
+ if (IsConfigured()) {
+ return;
+ }
+
+ DoConfigure(New<TDispatcherConfig>());
+ }
+
+ void DoConfigure(const TDispatcherConfigPtr& config)
+ {
+ VERIFY_SPINLOCK_AFFINITY(ConfigureLock_);
+ YT_VERIFY(!IsConfigured());
+
+ grpc_core::Executor::SetThreadsLimit(config->GrpcThreadCount);
+
+ // Initialize grpc only after configuration is done.
+ auto grpcLock = New<TGrpcLibraryLock>();
+ for (int index = 0; index < config->DispatcherThreadCount; ++index) {
+ Threads_.push_back(New<TDispatcherThread>(grpcLock, index));
+ }
+ LibraryLock_ = grpcLock;
+ Configured_.store(true);
+ }
+
+
+ std::atomic<bool> Configured_ = false;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ConfigureLock_);
+ TWeakPtr<TGrpcLibraryLock> LibraryLock_;
+ std::vector<TDispatcherThreadPtr> Threads_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TDispatcher::TDispatcher()
+ : Impl_(std::make_unique<TImpl>())
+{ }
+
+TDispatcher::~TDispatcher() = default;
+
+TDispatcher* TDispatcher::Get()
+{
+ return LeakySingleton<TDispatcher>();
+}
+
+void TDispatcher::Configure(const TDispatcherConfigPtr& config)
+{
+ Impl_->Configure(config);
+}
+
+bool TDispatcher::IsConfigured() const noexcept
+{
+ return Impl_->IsConfigured();
+}
+
+TGrpcLibraryLockPtr TDispatcher::GetLibraryLock()
+{
+ return Impl_->GetLibraryLock();
+}
+
+TGuardedGrpcCompletionQueuePtr* TDispatcher::PickRandomGuardedCompletionQueue()
+{
+ return Impl_->PickRandomGuardedCompletionQueue();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/dispatcher.h b/yt/yt/core/rpc/grpc/dispatcher.h
new file mode 100644
index 0000000000..8bb03cee2e
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/dispatcher.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "helpers.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompletionQueueTag
+{
+public:
+ void* GetTag(int cookie = 0);
+ virtual void Run(bool success, int cookie) = 0;
+
+protected:
+ virtual ~TCompletionQueueTag() = default;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDispatcher
+{
+public:
+ static TDispatcher* Get();
+
+ //! Configures the dispatcher.
+ /*!
+ * Can only can called once; subsequent calls will throw.
+ * The call must be done prior to any GRPC client or server is created.
+ */
+ void Configure(const TDispatcherConfigPtr& config);
+ [[nodiscard]] bool IsConfigured() const noexcept;
+
+ TGrpcLibraryLockPtr GetLibraryLock();
+ TGuardedGrpcCompletionQueuePtr* PickRandomGuardedCompletionQueue();
+
+private:
+ DECLARE_LEAKY_SINGLETON_FRIEND()
+ friend class TGrpcLibraryLock;
+
+ TDispatcher();
+ ~TDispatcher();
+
+ class TImpl;
+ const std::unique_ptr<TImpl> Impl_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcLibraryLock
+ : public TRefCounted
+{
+private:
+ DECLARE_NEW_FRIEND()
+ friend class TDispatcher::TImpl;
+
+ TGrpcLibraryLock();
+ ~TGrpcLibraryLock();
+};
+
+DEFINE_REFCOUNTED_TYPE(TGrpcLibraryLock)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/helpers-inl.h b/yt/yt/core/rpc/grpc/helpers-inl.h
new file mode 100644
index 0000000000..43fa653d6f
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/helpers-inl.h
@@ -0,0 +1,108 @@
+#ifndef HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+#undef HELPERS_INL_H_
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>::TGrpcObjectPtr()
+ : Ptr_(nullptr)
+{ }
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>::~TGrpcObjectPtr()
+{
+ Reset();
+}
+
+template <class T, void(*Dtor)(T*)>
+T* TGrpcObjectPtr<T, Dtor>::Unwrap()
+{
+ return Ptr_;
+}
+
+template <class T, void(*Dtor)(T*)>
+const T* TGrpcObjectPtr<T, Dtor>::Unwrap() const
+{
+ return Ptr_;
+}
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>::operator bool() const
+{
+ return Ptr_ != nullptr;
+}
+
+template <class T, void(*Dtor)(T*)>
+T** TGrpcObjectPtr<T, Dtor>::GetPtr()
+{
+ Reset();
+ return &Ptr_;
+}
+
+template <class T, void(*Dtor)(T*)>
+void TGrpcObjectPtr<T, Dtor>::Reset()
+{
+ if (Ptr_) {
+ Dtor(Ptr_);
+ Ptr_ = nullptr;
+ }
+}
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>::TGrpcObjectPtr(T* obj)
+ : Ptr_(obj)
+{ }
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>::TGrpcObjectPtr(TGrpcObjectPtr&& other)
+ : Ptr_(other.Ptr_)
+{
+ other.Ptr_ = nullptr;
+}
+
+template <class T, void(*Dtor)(T*)>
+TGrpcObjectPtr<T, Dtor>& TGrpcObjectPtr<T, Dtor>::operator=(TGrpcObjectPtr&& other)
+{
+ if (this != &other) {
+ Reset();
+ Ptr_ = other.Ptr_;
+ other.Ptr_ = nullptr;
+ }
+ return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, void(*Ctor)(T*), void(*Dtor)(T*)>
+TGrpcObject<T, Ctor, Dtor>::TGrpcObject()
+{
+ Ctor(&Native_);
+}
+
+template <class T, void(*Ctor)(T*), void(*Dtor)(T*)>
+TGrpcObject<T, Ctor, Dtor>::~TGrpcObject()
+{
+ Dtor(&Native_);
+}
+
+template <class T, void(*Ctor)(T*), void(*Dtor)(T*)>
+T* TGrpcObject<T, Ctor, Dtor>::Unwrap()
+{
+ return &Native_;
+}
+
+template <class T, void(*Ctor)(T*), void(*Dtor)(T*)>
+T* TGrpcObject<T, Ctor, Dtor>::operator->()
+{
+ return &Native_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/helpers.cpp b/yt/yt/core/rpc/grpc/helpers.cpp
new file mode 100644
index 0000000000..744e007b06
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/helpers.cpp
@@ -0,0 +1,567 @@
+#include "helpers.h"
+#include "config.h"
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <yt/yt/core/crypto/config.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt_proto/yt/core/misc/proto/protobuf_helpers.pb.h>
+#include <yt/yt_proto/yt/core/misc/proto/error.pb.h>
+
+#include <yt/yt/core/ytree/node.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc.h>
+#include <contrib/libs/grpc/include/grpc/byte_buffer.h>
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+#include <contrib/libs/grpc/include/grpc/support/alloc.h>
+
+#include <openssl/pem.h>
+
+#include <array>
+
+namespace NYT::NRpc::NGrpc {
+
+using NYTree::ENodeType;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr ui32 MinusOne = static_cast<ui32>(-1);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGprString MakeGprString(char* str)
+{
+ return TGprString(str, gpr_free);
+}
+
+TStringBuf ToStringBuf(const grpc_slice& slice)
+{
+ return TStringBuf(
+ reinterpret_cast<const char*>(GRPC_SLICE_START_PTR(slice)),
+ GRPC_SLICE_LENGTH(slice));
+}
+
+TString ToString(const grpc_slice& slice)
+{
+ return TString(ToStringBuf(slice));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStringBuf TGrpcMetadataArray::Find(const char* key) const
+{
+ for (size_t index = 0; index < Native_.count; ++index) {
+ const auto& metadata = Native_.metadata[index];
+ if (ToStringBuf(metadata.key) == key) {
+ return ToStringBuf(metadata.value);
+ }
+ }
+
+ return TStringBuf();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGrpcSlice::~TGrpcSlice()
+{
+ grpc_slice_unref(Native_);
+}
+
+void TGrpcSlice::Reset(grpc_slice&& other)
+{
+ grpc_slice_unref(Native_);
+ Native_ = std::move(other);
+}
+
+grpc_slice* TGrpcSlice::Unwrap()
+{
+ return &Native_;
+}
+
+const ui8* TGrpcSlice::Data() const
+{
+ return GRPC_SLICE_START_PTR(Native_);
+}
+
+size_t TGrpcSlice::Size() const
+{
+ return GRPC_SLICE_LENGTH(Native_);
+}
+
+TString TGrpcSlice::AsString() const
+{
+ return NGrpc::ToString(Native_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TGrpcMetadataArrayBuilder::Add(const char* key, TString value)
+{
+ Strings_.push_back(TSharedRef::FromString(std::move(value)));
+ grpc_metadata metadata;
+ metadata.key = grpc_slice_from_static_string(key);
+ metadata.value = grpc_slice_from_static_buffer(Strings_.back().Begin(), Strings_.back().Size());
+ NativeMetadata_.push_back(metadata);
+}
+
+size_t TGrpcMetadataArrayBuilder::GetSize() const
+{
+ return NativeMetadata_.size();
+}
+
+grpc_metadata* TGrpcMetadataArrayBuilder::Unwrap()
+{
+ return NativeMetadata_.data();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGrpcChannelArgs::TGrpcChannelArgs(const THashMap<TString, NYTree::INodePtr>& args)
+{
+ for (const auto& pair : args) {
+ Items_.emplace_back();
+ auto& item = Items_.back();
+ const auto& key = pair.first;
+ const auto& node = pair.second;
+ item.key = const_cast<char*>(key.c_str());
+
+ auto setIntegerValue = [&] (auto value) {
+ item.type = GRPC_ARG_INTEGER;
+ item.value.integer = static_cast<int>(value);
+ };
+
+ auto setStringValue = [&] (const auto& value) {
+ item.type = GRPC_ARG_STRING;
+ item.value.string = const_cast<char*>(value.c_str());
+ };
+
+ switch (node->GetType()) {
+ case ENodeType::Int64: {
+ auto value = node->AsInt64()->GetValue();
+ if (value < std::numeric_limits<int>::min() || value > std::numeric_limits<int>::max()) {
+ THROW_ERROR_EXCEPTION("Value %v of GRPC argument %Qv is out of range",
+ value,
+ key);
+ }
+ setIntegerValue(value);
+ break;
+ }
+ case ENodeType::Uint64: {
+ auto value = node->AsUint64()->GetValue();
+ if (value > static_cast<ui64>(std::numeric_limits<int>::max())) {
+ THROW_ERROR_EXCEPTION("Value %v of GRPC argument %Qv is out of range",
+ value,
+ key);
+ }
+ setIntegerValue(value);
+ break;
+ }
+ case ENodeType::String:
+ setStringValue(node->AsString()->GetValue());
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Invalid type %Qlv of GRPC argument %Qv in channel configuration",
+ node->GetType(),
+ key);
+ }
+ }
+
+ Native_.num_args = args.size();
+ Native_.args = Items_.data();
+}
+
+grpc_channel_args* TGrpcChannelArgs::Unwrap()
+{
+ return &Native_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGrpcPemKeyCertPair::TGrpcPemKeyCertPair(TString privateKey, TString certChain)
+ : PrivateKey_(std::move(privateKey))
+ , CertChain_(std::move(certChain))
+ , Native_({
+ PrivateKey_.c_str(),
+ CertChain_.c_str()
+ })
+{ }
+
+grpc_ssl_pem_key_cert_pair* TGrpcPemKeyCertPair::Unwrap()
+{
+ return &Native_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGrpcByteBufferStream::TGrpcByteBufferStream(grpc_byte_buffer* buffer)
+ : RemainingBytes_(grpc_byte_buffer_length(buffer))
+{
+ YT_VERIFY(grpc_byte_buffer_reader_init(&Reader_, buffer) == 1);
+}
+
+TGrpcByteBufferStream::~TGrpcByteBufferStream()
+{
+ grpc_byte_buffer_reader_destroy(&Reader_);
+}
+
+bool TGrpcByteBufferStream::ReadNextSlice()
+{
+ CurrentSlice_.Reset(grpc_empty_slice());
+
+ if (grpc_byte_buffer_reader_next(&Reader_, CurrentSlice_.Unwrap()) == 0) {
+ return false;
+ }
+
+ AvailableBytes_ = CurrentSlice_.Size();
+ return true;
+}
+
+size_t TGrpcByteBufferStream::DoRead(void* buf, size_t len)
+{
+ // NB: Theoretically empty slice can be read, skipping such
+ // slices to avoid early EOS.
+ while (AvailableBytes_ == 0) {
+ if (!ReadNextSlice()) {
+ return 0;
+ }
+ }
+
+ const auto* sliceData = CurrentSlice_.Data();
+ auto offset = CurrentSlice_.Size() - AvailableBytes_;
+
+ size_t toRead = std::min(len, AvailableBytes_);
+ ::memcpy(buf, sliceData + offset, toRead);
+ AvailableBytes_ -= toRead;
+ RemainingBytes_ -= toRead;
+
+ return toRead;
+}
+
+bool TGrpcByteBufferStream::IsExhausted() const
+{
+ return RemainingBytes_ == 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMessageTag
+{ };
+
+TMessageWithAttachments ByteBufferToMessageWithAttachments(
+ grpc_byte_buffer* buffer,
+ std::optional<ui32> messageBodySize)
+{
+ TMessageWithAttachments result;
+
+ ui32 bufferSize = grpc_byte_buffer_length(buffer);
+
+ // NB: Message body size is not specified, assuming that
+ // the whole data is message body.
+ if (!messageBodySize) {
+ messageBodySize = bufferSize;
+ }
+
+ NYT::NProto::TSerializedMessageEnvelope envelope;
+ // Codec remains "none".
+
+ TEnvelopeFixedHeader fixedHeader;
+ fixedHeader.EnvelopeSize = envelope.ByteSize();
+ fixedHeader.MessageSize = *messageBodySize;
+
+ size_t totalMessageSize =
+ sizeof (TEnvelopeFixedHeader) +
+ fixedHeader.EnvelopeSize +
+ fixedHeader.MessageSize;
+
+ auto data = TSharedMutableRef::Allocate<TMessageTag>(
+ totalMessageSize,
+ {.InitializeStorage = false});
+
+ char* targetFixedHeader = data.Begin();
+ char* targetHeader = targetFixedHeader + sizeof (TEnvelopeFixedHeader);
+ char* targetMessage = targetHeader + fixedHeader.EnvelopeSize;
+
+ memcpy(targetFixedHeader, &fixedHeader, sizeof (fixedHeader));
+ YT_VERIFY(envelope.SerializeToArray(targetHeader, fixedHeader.EnvelopeSize));
+
+ TGrpcByteBufferStream stream(buffer);
+
+ if (stream.Load(targetMessage, *messageBodySize) != *messageBodySize) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream while reading message body");
+ }
+
+ result.Message = data;
+
+ ui32 attachmentsSize = bufferSize - *messageBodySize;
+
+ if (attachmentsSize == 0) {
+ return result;
+ }
+
+ auto attachmentsData = TSharedMutableRef::Allocate<TMessageTag>(
+ attachmentsSize,
+ {.InitializeStorage = false});
+
+ char* attachmentsBuffer = attachmentsData.Begin();
+
+ while (!stream.IsExhausted()) {
+ ui32 attachmentSize;
+
+ if (stream.Load(&attachmentSize, sizeof(attachmentSize)) != sizeof(attachmentSize)) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream while reading attachment size");
+ }
+
+ if (attachmentSize == MinusOne) {
+ result.Attachments.push_back(TSharedRef());
+ } else if (attachmentSize == 0) {
+ result.Attachments.push_back(TSharedRef::MakeEmpty());
+ } else {
+ if (stream.Load(attachmentsBuffer, attachmentSize) != attachmentSize) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream while reading message attachment");
+ }
+
+ result.Attachments.push_back(
+ attachmentsData.Slice(attachmentsBuffer, attachmentsBuffer + attachmentSize));
+
+ attachmentsBuffer += attachmentSize;
+ }
+ }
+
+ return result;
+}
+
+TGrpcByteBufferPtr MessageWithAttachmentsToByteBuffer(const TMessageWithAttachments& messageWithAttachments)
+{
+ struct THolder
+ {
+ TSharedRef Data;
+ };
+
+ auto sliceFromRef = [] (TSharedRef ref) {
+ auto* holder = new THolder();
+ holder->Data = std::move(ref);
+ return grpc_slice_new_with_user_data(
+ const_cast<char*>(holder->Data.Begin()),
+ holder->Data.Size(),
+ [] (void* untypedHolder) {
+ delete static_cast<THolder*>(untypedHolder);
+ },
+ holder);
+ };
+
+ std::vector<grpc_slice> slices;
+ slices.reserve(1 + 2 * messageWithAttachments.Attachments.size());
+
+ slices.push_back(sliceFromRef(messageWithAttachments.Message));
+
+ for (const auto& attachment : messageWithAttachments.Attachments) {
+ ui32 size = attachment ? attachment.Size() : MinusOne;
+ slices.push_back(grpc_slice_from_copied_buffer(reinterpret_cast<const char*>(&size), sizeof(size)));
+ if (attachment) {
+ slices.push_back(sliceFromRef(attachment));
+ }
+ }
+
+ auto* buffer = grpc_raw_byte_buffer_create(slices.data(), slices.size());
+
+ for (const auto& slice : slices) {
+ grpc_slice_unref(slice);
+ }
+
+ return TGrpcByteBufferPtr(buffer);
+}
+
+TSharedRef ExtractMessageFromEnvelopedMessage(const TSharedRef& data)
+{
+ YT_VERIFY(data.Size() >= sizeof(TEnvelopeFixedHeader));
+ const auto* fixedHeader = reinterpret_cast<const TEnvelopeFixedHeader*>(data.Begin());
+ const char* sourceHeader = data.Begin() + sizeof(TEnvelopeFixedHeader);
+ const char* sourceMessage = sourceHeader + fixedHeader->EnvelopeSize;
+
+ NYT::NProto::TSerializedMessageEnvelope envelope;
+ YT_VERIFY(envelope.ParseFromArray(sourceHeader, fixedHeader->EnvelopeSize));
+
+ auto compressedMessage = data.Slice(sourceMessage, sourceMessage + fixedHeader->MessageSize);
+
+ auto codecId = CheckedEnumCast<NCompression::ECodec>(envelope.codec());
+ auto* codec = NCompression::GetCodec(codecId);
+ return codec->Decompress(compressedMessage);
+}
+
+TErrorCode StatusCodeToErrorCode(grpc_status_code statusCode)
+{
+ switch (statusCode) {
+ case GRPC_STATUS_OK:
+ return NYT::EErrorCode::OK;
+ case GRPC_STATUS_CANCELLED:
+ return NYT::EErrorCode::Canceled;
+ case GRPC_STATUS_DEADLINE_EXCEEDED:
+ return NYT::EErrorCode::Timeout;
+ case GRPC_STATUS_INVALID_ARGUMENT:
+ case GRPC_STATUS_RESOURCE_EXHAUSTED:
+ return NRpc::EErrorCode::ProtocolError;
+ default:
+ return NRpc::EErrorCode::TransportError;
+ }
+}
+
+TString SerializeError(const TError& error)
+{
+ TString serializedError;
+ google::protobuf::io::StringOutputStream output(&serializedError);
+ NYT::NProto::TError protoError;
+ ToProto(&protoError, error);
+ YT_VERIFY(protoError.SerializeToZeroCopyStream(&output));
+ return serializedError;
+}
+
+TError DeserializeError(TStringBuf serializedError)
+{
+ NYT::NProto::TError protoError;
+ google::protobuf::io::ArrayInputStream input(serializedError.data(), serializedError.size());
+ if (!protoError.ParseFromZeroCopyStream(&input)) {
+ THROW_ERROR_EXCEPTION("Error deserializing error");
+ }
+ return FromProto<TError>(protoError);
+}
+
+TGrpcPemKeyCertPair LoadPemKeyCertPair(const TSslPemKeyCertPairConfigPtr& config)
+{
+ return TGrpcPemKeyCertPair(
+ config->PrivateKey->LoadBlob(),
+ config->CertChain->LoadBlob());
+}
+
+TGrpcChannelCredentialsPtr LoadChannelCredentials(const TChannelCredentialsConfigPtr& config)
+{
+ TString rootCerts;
+ TString identityCerts;
+ TString identityPrivateKey;
+ if (config->PemRootCerts) {
+ rootCerts = config->PemRootCerts->LoadBlob();
+ }
+ if (config->PemKeyCertPair && config->PemKeyCertPair->CertChain && config->PemKeyCertPair->PrivateKey) {
+ identityCerts = config->PemKeyCertPair->CertChain->LoadBlob();
+ identityPrivateKey = config->PemKeyCertPair->PrivateKey->LoadBlob();
+ }
+
+ grpc_tls_identity_pairs* tlsPairs = nullptr;
+ if (identityCerts && identityPrivateKey) {
+ tlsPairs = grpc_tls_identity_pairs_create();
+ grpc_tls_identity_pairs_add_pair(
+ tlsPairs,
+ identityPrivateKey.c_str(),
+ identityCerts.c_str());
+ }
+
+ TGrpcTlsCertificateProviderPtr certProvider(grpc_tls_certificate_provider_static_data_create(
+ rootCerts ? rootCerts.c_str() : nullptr,
+ tlsPairs));
+
+ grpc_tls_credentials_options* tlsOptions = grpc_tls_credentials_options_create();
+ grpc_tls_credentials_options_set_verify_server_cert(tlsOptions, config->VerifyServerCert);
+ grpc_tls_credentials_options_set_certificate_provider(tlsOptions, certProvider.Unwrap());
+ if (rootCerts) {
+ grpc_tls_credentials_options_watch_root_certs(tlsOptions);
+ }
+ if (identityCerts && identityPrivateKey) {
+ grpc_tls_credentials_options_watch_identity_key_cert_pairs(tlsOptions);
+ }
+
+ return TGrpcChannelCredentialsPtr(grpc_tls_credentials_create(tlsOptions));
+}
+
+TGrpcServerCredentialsPtr LoadServerCredentials(const TServerCredentialsConfigPtr& config)
+{
+ auto rootCerts = config->PemRootCerts ? config->PemRootCerts->LoadBlob() : TString();
+ std::vector<TGrpcPemKeyCertPair> keyCertPairs;
+ std::vector<grpc_ssl_pem_key_cert_pair> nativeKeyCertPairs;
+ for (const auto& pairConfig : config->PemKeyCertPairs) {
+ keyCertPairs.push_back(LoadPemKeyCertPair(pairConfig));
+ nativeKeyCertPairs.push_back(*keyCertPairs.back().Unwrap());
+ }
+ return TGrpcServerCredentialsPtr(grpc_ssl_server_credentials_create_ex(
+ rootCerts ? rootCerts.c_str() : nullptr,
+ nativeKeyCertPairs.data(),
+ nativeKeyCertPairs.size(),
+ static_cast<grpc_ssl_client_certificate_request_type>(config->ClientCertificateRequest),
+ nullptr));
+}
+
+std::optional<TString> ParseIssuerFromX509(TStringBuf x509String)
+{
+ auto* bio = BIO_new(BIO_s_mem());
+ auto bioGuard = Finally([&] {
+ BIO_free(bio);
+ });
+
+ BIO_write(bio, x509String.data(), x509String.length());
+
+ auto* x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
+ auto x509Guard = Finally([&] {
+ X509_free(x509);
+ });
+
+ if (!x509) {
+ return std::nullopt;
+ }
+
+ auto* issuerName = X509_get_issuer_name(x509);
+
+ std::array<char, 1024> buf;
+ auto* issuerString = X509_NAME_oneline(issuerName, buf.data(), buf.size());
+ if (!issuerString) {
+ return std::nullopt;
+ }
+
+ return TString(issuerString);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGuardedGrpcCompletionQueuePtr::TGuardedGrpcCompletionQueuePtr(TGrpcCompletionQueuePtr completionQueuePtr)
+ : CompletionQueuePtr_(std::move(completionQueuePtr))
+{ }
+
+std::optional<TGuardedGrpcCompletionQueuePtr::TLockGuard<NConcurrency::TAsyncLockReaderTraits>> TGuardedGrpcCompletionQueuePtr::TryLock()
+{
+ auto guard = TLockGuard<NConcurrency::TAsyncLockReaderTraits>(*this);
+ if (State_ != EState::Opened) {
+ return std::nullopt;
+ }
+ return std::optional{std::move(guard)};
+}
+
+void TGuardedGrpcCompletionQueuePtr::Shutdown()
+{
+ auto guard = TLockGuard<NConcurrency::TAsyncLockWriterTraits>(*this);
+ if (State_ == EState::Shutdown) {
+ return;
+ }
+ State_ = EState::Shutdown;
+ grpc_completion_queue_shutdown(CompletionQueuePtr_.Unwrap());
+}
+
+void TGuardedGrpcCompletionQueuePtr::Reset()
+{
+ auto guard = TLockGuard<NConcurrency::TAsyncLockWriterTraits>(*this);
+ YT_VERIFY(State_ == EState::Shutdown);
+ CompletionQueuePtr_.Reset();
+}
+
+grpc_completion_queue* TGuardedGrpcCompletionQueuePtr::UnwrapUnsafe()
+{
+ return CompletionQueuePtr_.Unwrap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/helpers.h b/yt/yt/core/rpc/grpc/helpers.h
new file mode 100644
index 0000000000..e57d016e53
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/helpers.h
@@ -0,0 +1,302 @@
+#pragma once
+
+#include "private.h"
+
+#include <yt/yt/core/concurrency/async_rw_lock.h>
+
+#include <yt/yt/core/crypto/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc.h>
+#include <contrib/libs/grpc/include/grpc/grpc_security.h>
+#include <contrib/libs/grpc/include/grpc/impl/codegen/grpc_types.h>
+#include <contrib/libs/grpc/include/grpc/byte_buffer_reader.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TGprString = std::unique_ptr<char, void(*)(void*)>;
+TGprString MakeGprString(char* str);
+
+TStringBuf ToStringBuf(const grpc_slice& slice);
+TString ToString(const grpc_slice& slice);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, void(*Dtor)(T*)>
+class TGrpcObjectPtr
+{
+public:
+ TGrpcObjectPtr();
+ explicit TGrpcObjectPtr(T* obj);
+ TGrpcObjectPtr(TGrpcObjectPtr&& other);
+ ~TGrpcObjectPtr();
+
+ TGrpcObjectPtr& operator=(TGrpcObjectPtr&& other);
+
+ TGrpcObjectPtr& operator=(const TGrpcObjectPtr&) = delete;
+ TGrpcObjectPtr(const TGrpcObjectPtr& other) = delete;
+
+ T* Unwrap();
+ const T* Unwrap() const;
+ explicit operator bool() const;
+
+ T** GetPtr();
+ void Reset();
+
+private:
+ T* Ptr_;
+};
+
+using TGrpcByteBufferPtr = TGrpcObjectPtr<grpc_byte_buffer, grpc_byte_buffer_destroy>;
+using TGrpcCallPtr = TGrpcObjectPtr<grpc_call, grpc_call_unref>;
+using TGrpcChannelPtr = TGrpcObjectPtr<grpc_channel, grpc_channel_destroy>;
+using TGrpcCompletionQueuePtr = TGrpcObjectPtr<grpc_completion_queue, grpc_completion_queue_destroy>;
+using TGrpcServerPtr = TGrpcObjectPtr<grpc_server, grpc_server_destroy>;
+using TGrpcChannelCredentialsPtr = TGrpcObjectPtr<grpc_channel_credentials, grpc_channel_credentials_release>;
+using TGrpcServerCredentialsPtr = TGrpcObjectPtr<grpc_server_credentials, grpc_server_credentials_release>;
+using TGrpcTlsCertificateProviderPtr = TGrpcObjectPtr<grpc_tls_certificate_provider, grpc_tls_certificate_provider_release>;
+using TGrpcAuthContextPtr = TGrpcObjectPtr<grpc_auth_context, grpc_auth_context_release>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Completion queue must be accessed under read lock
+//! in order to prohibit creating new requests after shutting completion queue down.
+class TGuardedGrpcCompletionQueuePtr
+{
+public:
+ explicit TGuardedGrpcCompletionQueuePtr(TGrpcCompletionQueuePtr completionQueuePtr);
+
+ template <class TOperationTraits>
+ class TLockGuard
+ : public TNonCopyable
+ {
+ public:
+ explicit TLockGuard(TGuardedGrpcCompletionQueuePtr& guardedCompletionQueue)
+ : GuardedCompletionQueue_(guardedCompletionQueue)
+ {
+ Guard_ = WaitFor(NConcurrency::TAsyncReaderWriterLockGuard<TOperationTraits>::Acquire(&GuardedCompletionQueue_.Lock_))
+ .ValueOrThrow();
+ }
+
+ TLockGuard(TLockGuard&& guard)
+ : GuardedCompletionQueue_(guard.GuardedCompletionQueue_)
+ , Guard_(std::move(guard.Guard_))
+ { }
+
+ ~TLockGuard()
+ {
+ Unlock();
+ }
+
+ grpc_completion_queue* Unwrap()
+ {
+ return GuardedCompletionQueue_.UnwrapUnsafe();
+ }
+
+ void Unlock()
+ {
+ if (Guard_) {
+ Guard_.Reset();
+ }
+ }
+
+ private:
+ TGuardedGrpcCompletionQueuePtr& GuardedCompletionQueue_;
+ TIntrusivePtr<NConcurrency::TAsyncReaderWriterLockGuard<TOperationTraits>> Guard_;
+ };
+
+ std::optional<TLockGuard<NConcurrency::TAsyncLockReaderTraits>> TryLock();
+
+ void Shutdown();
+
+ //! Must be done after Shutdown.
+ void Reset();
+
+ //! Access completion queue without taking locks and checking the state.
+ //! Used in GrpcServer (lock is redundant there because it synchronizes
+ //! with grpc_completion_queue shutdown via thread priorities)
+ //! and in DispatcherThreads (operation grpc_completion_queue_next
+ //! does not need to be synchronized).
+ grpc_completion_queue* UnwrapUnsafe();
+
+private:
+ enum class EState
+ {
+ Opened = 1,
+ Shutdown = 2
+ };
+
+ TGrpcCompletionQueuePtr CompletionQueuePtr_;
+ NConcurrency::TAsyncReaderWriterLock Lock_;
+ EState State_{EState::Opened};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, void(*Ctor)(T*), void(*Dtor)(T*)>
+class TGrpcObject
+{
+public:
+ TGrpcObject();
+ ~TGrpcObject();
+
+ TGrpcObject(TGrpcObject&&) = delete;
+ TGrpcObject& operator=(TGrpcObject&&) = delete;
+
+ T* Unwrap();
+ T* operator->();
+
+protected:
+ T Native_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcMetadataArray
+ : public TGrpcObject<grpc_metadata_array, grpc_metadata_array_init, grpc_metadata_array_destroy>
+{
+public:
+ TStringBuf Find(const char* key) const;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TGrpcCallDetails = TGrpcObject<grpc_call_details, grpc_call_details_init, grpc_call_details_destroy>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcSlice
+{
+public:
+ TGrpcSlice() = default;
+ ~TGrpcSlice();
+
+ TGrpcSlice(TGrpcSlice&&) = delete;
+ TGrpcSlice& operator=(TGrpcSlice&&) = delete;
+
+ void Reset(grpc_slice&& other);
+
+ grpc_slice* Unwrap();
+
+ const ui8* Data() const;
+ size_t Size() const;
+
+ TString AsString() const;
+
+private:
+ grpc_slice Native_ = grpc_empty_slice();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcMetadataArrayBuilder
+{
+public:
+ void Add(const char* key, TString value);
+ size_t GetSize() const;
+
+ grpc_metadata* Unwrap();
+
+private:
+ static constexpr size_t TypicalSize = 8;
+ TCompactVector<grpc_metadata, TypicalSize> NativeMetadata_;
+ TCompactVector<TSharedRef, TypicalSize> Strings_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcChannelArgs
+{
+public:
+ explicit TGrpcChannelArgs(const THashMap<TString, NYTree::INodePtr>& args);
+
+ grpc_channel_args* Unwrap();
+
+private:
+ grpc_channel_args Native_;
+ std::vector<grpc_arg> Items_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcByteBufferStream
+ : public IInputStream
+{
+public:
+ explicit TGrpcByteBufferStream(grpc_byte_buffer* buffer);
+ ~TGrpcByteBufferStream();
+
+ bool IsExhausted() const;
+
+private:
+ grpc_byte_buffer_reader Reader_;
+
+ TGrpcSlice CurrentSlice_;
+ size_t AvailableBytes_ = 0;
+ size_t RemainingBytes_;
+
+ virtual size_t DoRead(void* buf, size_t len) override;
+
+ bool ReadNextSlice();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGrpcPemKeyCertPair
+{
+public:
+ TGrpcPemKeyCertPair(
+ TString privateKey,
+ TString certChain);
+
+ grpc_ssl_pem_key_cert_pair* Unwrap();
+
+private:
+ TString PrivateKey_;
+ TString CertChain_;
+ grpc_ssl_pem_key_cert_pair Native_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TMessageWithAttachments
+{
+ TSharedRef Message;
+ std::vector<TSharedRef> Attachments;
+};
+
+TMessageWithAttachments ByteBufferToMessageWithAttachments(
+ grpc_byte_buffer* buffer,
+ std::optional<ui32> messageBodySize);
+
+TGrpcByteBufferPtr MessageWithAttachmentsToByteBuffer(
+ const TMessageWithAttachments& messageWithAttachments);
+
+TSharedRef ExtractMessageFromEnvelopedMessage(const TSharedRef& data);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TErrorCode StatusCodeToErrorCode(grpc_status_code statusCode);
+
+TString SerializeError(const TError& error);
+TError DeserializeError(TStringBuf serializedError);
+
+TGrpcPemKeyCertPair LoadPemKeyCertPair(const TSslPemKeyCertPairConfigPtr& config);
+TGrpcChannelCredentialsPtr LoadChannelCredentials(const TChannelCredentialsConfigPtr& config);
+TGrpcServerCredentialsPtr LoadServerCredentials(const TServerCredentialsConfigPtr& config);
+std::optional<TString> ParseIssuerFromX509(TStringBuf x509String);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
+
+#define HELPERS_INL_H_
+#include "helpers-inl.h"
+#undef HELPERS_INL_H_
diff --git a/yt/yt/core/rpc/grpc/private.h b/yt/yt/core/rpc/grpc/private.h
new file mode 100644
index 0000000000..bfb0044fe0
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/private.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+
+////////////////////////////////////////////////////////////////////////////////
+// GRPC forward declarations
+
+struct grpc_completion_queue;
+struct grpc_server;
+struct grpc_byte_buffer;
+struct grpc_call;
+struct grpc_channel;
+struct grpc_channel_credentials;
+struct grpc_server_credentials;
+struct grpc_auth_context;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NProfiling::TProfiler GrpcServerProfiler("/grpc/server");
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger GrpcLogger("Grpc");
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompletionQueueTag;
+
+DECLARE_REFCOUNTED_CLASS(TGrpcLibraryLock)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/proto/grpc.proto b/yt/yt/core/rpc/grpc/proto/grpc.proto
new file mode 100644
index 0000000000..6ec01e35f9
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/proto/grpc.proto
@@ -0,0 +1,18 @@
+package NYT.NRpc.NGrpc.NProto;
+
+import "yt_proto/yt/core/rpc/proto/rpc.proto";
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TSslCredentialsExt
+{
+ extend NRpc.NProto.TRequestHeader
+ {
+ optional TSslCredentialsExt ssl_credentials_ext = 120;
+ }
+
+ optional string peer_identity = 1;
+ optional string issuer = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/rpc/grpc/public.cpp b/yt/yt/core/rpc/grpc/public.cpp
new file mode 100644
index 0000000000..c37464292a
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/public.cpp
@@ -0,0 +1,27 @@
+#include "public.h"
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+const char* const TracingTraceIdMetadataKey = "yt-tracing-trace-id";
+const char* const TracingSpanIdMetadataKey = "yt-tracing-span-id";
+const char* const TracingSampledMetadataKey = "yt-tracing-sampled";
+const char* const TracingDebugMetadataKey = "yt-tracing-debug";
+
+const char* const RequestIdMetadataKey = "yt-request-id";
+const char* const UserMetadataKey = "yt-user";
+const char* const UserTagMetadataKey = "yt-user-tag";
+const char* const UserAgentMetadataKey = "user-agent";
+const char* const AuthTokenMetadataKey = "yt-auth-token";
+const char* const AuthSessionIdMetadataKey = "yt-auth-session-id";
+const char* const AuthSslSessionIdMetadataKey = "yt-auth-ssl-session-id";
+const char* const AuthUserTicketMetadataKey = "yt-auth-user-ticket";
+const char* const ErrorMetadataKey = "yt-error-bin";
+const char* const MessageBodySizeMetadataKey = "yt-message-body-size";
+const char* const ProtocolVersionMetadataKey = "yt-protocol-version";
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/public.h b/yt/yt/core/rpc/grpc/public.h
new file mode 100644
index 0000000000..478e728e22
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/public.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TDispatcherConfig)
+DECLARE_REFCOUNTED_CLASS(TSslPemKeyCertPairConfig)
+DECLARE_REFCOUNTED_CLASS(TServerCredentialsConfig)
+DECLARE_REFCOUNTED_CLASS(TServerAddressConfig)
+DECLARE_REFCOUNTED_CLASS(TServerConfig)
+DECLARE_REFCOUNTED_CLASS(TChannelCredentialsConfig)
+DECLARE_REFCOUNTED_CLASS(TChannelConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const char* const TracingTraceIdMetadataKey;
+extern const char* const TracingSpanIdMetadataKey;
+extern const char* const TracingSampledMetadataKey;
+extern const char* const TracingDebugMetadataKey;
+
+extern const char* const RequestIdMetadataKey;
+extern const char* const UserMetadataKey;
+extern const char* const UserTagMetadataKey;
+extern const char* const UserAgentMetadataKey;
+extern const char* const AuthTokenMetadataKey;
+extern const char* const AuthSessionIdMetadataKey;
+extern const char* const AuthSslSessionIdMetadataKey;
+extern const char* const AuthUserTicketMetadataKey;
+extern const char* const ErrorMetadataKey;
+extern const char* const MessageBodySizeMetadataKey;
+extern const char* const ProtocolVersionMetadataKey;
+
+constexpr int GenericErrorStatusCode = 100;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/server.cpp b/yt/yt/core/rpc/grpc/server.cpp
new file mode 100644
index 0000000000..82fd00dd0d
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/server.cpp
@@ -0,0 +1,1095 @@
+#include "server.h"
+#include "dispatcher.h"
+#include "config.h"
+#include "helpers.h"
+
+#include <yt/yt/core/rpc/grpc/proto/grpc.pb.h>
+
+#include <yt/yt/core/misc/shutdown_priorities.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt/core/rpc/server_detail.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/misc/proc.h>
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <library/cpp/string_utils/quote/quote.h>
+
+#include <contrib/libs/grpc/include/grpc/grpc.h>
+#include <contrib/libs/grpc/include/grpc/grpc_security.h>
+
+#include <array>
+
+namespace NYT::NRpc::NGrpc {
+
+using namespace NRpc;
+using namespace NBus;
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NNet;
+using namespace NYson;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TServer)
+
+DEFINE_ENUM(EServerCallStage,
+ (Accept)
+ (ReceivingRequest)
+ (SendingInitialMetadata)
+ (WaitingForService)
+ (SendingResponse)
+ (WaitingForClose)
+ (Done)
+);
+
+DEFINE_ENUM(EServerCallCookie,
+ (Normal)
+ (Close)
+);
+
+class TServer
+ : public TServerBase
+{
+public:
+ explicit TServer(TServerConfigPtr config)
+ : TServerBase(GrpcLogger.WithTag("GrpcServerId: %v", TGuid::Create()))
+ , Config_(std::move(config))
+ , ShutdownCookie_(RegisterShutdownCallback(
+ "GrpcServer",
+ BIND_NO_PROPAGATE(&TServer::Shutdown, MakeWeak(this), /*graceful*/ true),
+ /*priority*/ GrpcServerShutdownPriority))
+ , LibraryLock_(TDispatcher::Get()->GetLibraryLock())
+ , Profiler_(GrpcServerProfiler.WithTag("name", Config_->ProfilingName))
+ , CompletionQueue_(TDispatcher::Get()->PickRandomGuardedCompletionQueue()->UnwrapUnsafe())
+ {
+ Profiler_.AddFuncGauge("/call_handler_count", MakeStrong(this), [this] {
+ return CallHandlerCount_.load();
+ });
+ }
+
+private:
+ const TServerConfigPtr Config_;
+ const TShutdownCookie ShutdownCookie_;
+
+ const TGrpcLibraryLockPtr LibraryLock_;
+
+ NProfiling::TProfiler Profiler_;
+
+ // This CompletionQueue_ does not have to be guarded in contrast to Channels completion queue,
+ // because we require server to be shut down before completion queues.
+ grpc_completion_queue* const CompletionQueue_;
+
+ TGrpcServerPtr Native_;
+ std::vector<TGrpcServerCredentialsPtr> Credentials_;
+
+ std::atomic<int> CallHandlerCount_ = {0};
+ std::atomic<bool> ShutdownStarted_ = {false};
+ TPromise<void> ShutdownPromise_ = NewPromise<void>();
+
+
+ void OnCallHandlerConstructed()
+ {
+ ++CallHandlerCount_;
+ }
+
+ void OnCallHandlerDestroyed()
+ {
+ if (--CallHandlerCount_ > 0) {
+ return;
+ }
+
+ Cleanup();
+ ShutdownPromise_.SetFrom(TServerBase::DoStop(true));
+ Unref();
+ }
+
+
+ void DoStart() override
+ {
+ TGrpcChannelArgs args(Config_->GrpcArguments);
+
+ Native_ = TGrpcServerPtr(grpc_server_create(
+ args.Unwrap(),
+ nullptr));
+
+ grpc_server_register_completion_queue(
+ Native_.Unwrap(),
+ CompletionQueue_,
+ nullptr);
+
+ try {
+ for (const auto& addressConfig : Config_->Addresses) {
+ int result;
+ if (addressConfig->Credentials) {
+ Credentials_.push_back(LoadServerCredentials(addressConfig->Credentials));
+ } else {
+ Credentials_.emplace_back(grpc_insecure_server_credentials_create());
+ }
+ result = grpc_server_add_http2_port(
+ Native_.Unwrap(),
+ addressConfig->Address.c_str(),
+ Credentials_.back().Unwrap());
+ if (result == 0) {
+ THROW_ERROR_EXCEPTION("Error configuring server to listen at %Qv",
+ addressConfig->Address);
+ }
+ YT_LOG_DEBUG("Server address configured (Address: %v)", addressConfig->Address);
+ }
+ } catch (const std::exception& ex) {
+ Cleanup();
+ throw;
+ }
+
+ grpc_server_start(Native_.Unwrap());
+
+ Ref();
+
+ // This instance is fake; see DoStop.
+ OnCallHandlerConstructed();
+
+ TServerBase::DoStart();
+
+ New<TCallHandler>(this);
+ }
+
+ void Shutdown(bool graceful)
+ {
+ try {
+ DoStop(graceful).Get().ThrowOnError();
+ } catch (...) {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "GRPC server shutdown failed: %s (ThreadId: %" PRISZT ")\n",
+ CurrentExceptionMessage().c_str(),
+ GetCurrentThreadId());
+ }
+ }
+ }
+
+ TFuture<void> DoStop(bool graceful) override
+ {
+ class TStopTag
+ : public TCompletionQueueTag
+ {
+ public:
+ explicit TStopTag(TServerPtr owner)
+ : Owner_(std::move(owner))
+ { }
+
+ void Run(bool success, int /*cookie*/) override
+ {
+ YT_VERIFY(success);
+ Owner_->OnCallHandlerDestroyed();
+ delete this;
+ }
+
+ private:
+ const TServerPtr Owner_;
+ };
+
+ if (!ShutdownStarted_.exchange(true)) {
+ YT_VERIFY(Native_);
+ auto* shutdownTag = new TStopTag(this);
+
+ grpc_server_shutdown_and_notify(Native_.Unwrap(), CompletionQueue_, shutdownTag->GetTag());
+
+ if (!graceful) {
+ grpc_server_cancel_all_calls(Native_.Unwrap());
+ }
+ }
+ return ShutdownPromise_;
+ }
+
+ void Cleanup()
+ {
+ Native_.Reset();
+ }
+
+ class TCallHandler;
+
+ class TReplyBus
+ : public IBus
+ {
+ public:
+ explicit TReplyBus(TCallHandler* handler)
+ : Handler_(MakeWeak(handler))
+ , PeerAddress_(handler->PeerAddress_)
+ , PeerAddressString_(handler->PeerAddressString_)
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("address").Value(PeerAddressString_)
+ .EndMap()))
+ { }
+
+ // IBus overrides.
+ const TString& GetEndpointDescription() const override
+ {
+ return PeerAddressString_;
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ TBusNetworkStatistics GetNetworkStatistics() const override
+ {
+ return {};
+ }
+
+ const TString& GetEndpointAddress() const override
+ {
+ return PeerAddressString_;
+ }
+
+ const NNet::TNetworkAddress& GetEndpointNetworkAddress() const override
+ {
+ return PeerAddress_;
+ }
+
+ bool IsEndpointLocal() const override
+ {
+ return false;
+ }
+
+ TFuture<void> GetReadyFuture() const override
+ {
+ return VoidFuture;
+ }
+
+ TFuture<void> Send(TSharedRefArray message, const NBus::TSendOptions& /*options*/) override
+ {
+ if (auto handler = Handler_.Lock()) {
+ handler->OnResponseMessage(std::move(message));
+ }
+ return {};
+ }
+
+ void SetTosLevel(TTosLevel /*tosLevel*/) override
+ { }
+
+ void Terminate(const TError& /*error*/) override
+ { }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+ private:
+ const TWeakPtr<TCallHandler> Handler_;
+ const TNetworkAddress PeerAddress_;
+ const TString PeerAddressString_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+ };
+
+ class TCallHandler
+ : public TCompletionQueueTag
+ , public TRefCounted
+ {
+ public:
+ explicit TCallHandler(TServerPtr owner)
+ : Owner_(std::move(owner))
+ , CompletionQueue_(TDispatcher::Get()->PickRandomGuardedCompletionQueue()->UnwrapUnsafe())
+ , Logger(Owner_->Logger)
+ {
+ auto result = grpc_server_request_call(
+ Owner_->Native_.Unwrap(),
+ Call_.GetPtr(),
+ CallDetails_.Unwrap(),
+ CallMetadata_.Unwrap(),
+ CompletionQueue_,
+ Owner_->CompletionQueue_,
+ GetTag());
+ YT_VERIFY(result == GRPC_CALL_OK);
+
+ Ref();
+ Owner_->OnCallHandlerConstructed();
+ }
+
+ ~TCallHandler()
+ {
+ Owner_->OnCallHandlerDestroyed();
+ }
+
+ // TCompletionQueueTag overrides
+ void Run(bool success, int cookie_) override
+ {
+ const auto traceContextGuard = [&] {
+ auto guard = Guard(TraceContextSpinLock_);
+ return TraceContextHandler_.MakeTraceContextGuard();
+ }();
+
+ auto cookie = static_cast<EServerCallCookie>(cookie_);
+ switch (cookie) {
+ case EServerCallCookie::Normal:
+ {
+ const auto stage = [&] () {
+ auto guard = Guard(SpinLock_);
+ return Stage_;
+ }();
+ switch (stage) {
+ case EServerCallStage::Accept:
+ OnAccepted(success);
+ break;
+
+ case EServerCallStage::ReceivingRequest:
+ OnRequestReceived(success);
+ break;
+
+ case EServerCallStage::SendingInitialMetadata:
+ OnInitialMetadataSent(success);
+ break;
+
+ case EServerCallStage::SendingResponse:
+ OnResponseSent(success);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ break;
+ }
+ case EServerCallCookie::Close:
+ OnCloseReceived(success);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ private:
+ friend class TReplyBus;
+
+ const TServerPtr Owner_;
+
+ grpc_completion_queue* const CompletionQueue_;
+ const NLogging::TLogger& Logger;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ EServerCallStage Stage_ = EServerCallStage::Accept;
+ bool CancelRequested_ = false;
+ TSharedRefArray ResponseMessage_;
+
+ TString PeerAddressString_;
+ TNetworkAddress PeerAddress_;
+
+ TRequestId RequestId_;
+ std::optional<TString> User_;
+ std::optional<TString> UserTag_;
+ std::optional<TString> UserAgent_;
+ std::optional<NGrpc::NProto::TSslCredentialsExt> SslCredentialsExt_;
+ std::optional<NRpc::NProto::TCredentialsExt> RpcCredentialsExt_;
+ std::optional<NTracing::NProto::TTracingExt> TraceContext_;
+ TString ServiceName_;
+ TString MethodName_;
+ std::optional<TDuration> Timeout_;
+ IServicePtr Service_;
+
+ TGrpcMetadataArrayBuilder InitialMetadataBuilder_;
+ TGrpcMetadataArrayBuilder TrailingMetadataBuilder_;
+
+ TGrpcCallDetails CallDetails_;
+ TGrpcMetadataArray CallMetadata_;
+ TGrpcCallPtr Call_;
+ TGrpcByteBufferPtr RequestBodyBuffer_;
+ std::optional<ui32> RequestMessageBodySize_;
+ TProtocolVersion ProtocolVersion_ = DefaultProtocolVersion;
+ TGrpcByteBufferPtr ResponseBodyBuffer_;
+ TString ErrorMessage_;
+ TGrpcSlice ErrorMessageSlice_;
+ int RawCanceled_ = 0;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, TraceContextSpinLock_);
+ NTracing::TTraceContextHandler TraceContextHandler_;
+
+ template <class TOps>
+ void StartBatch(const TOps& ops, EServerCallCookie cookie)
+ {
+ auto result = grpc_call_start_batch(
+ Call_.Unwrap(),
+ ops.data(),
+ ops.size(),
+ GetTag(static_cast<int>(cookie)),
+ nullptr);
+ YT_VERIFY(result == GRPC_CALL_OK);
+ }
+
+ void OnAccepted(bool success)
+ {
+ if (!success) {
+ // This normally happens on server shutdown.
+ YT_LOG_DEBUG("Server accept failed");
+ Unref();
+ return;
+ }
+
+ New<TCallHandler>(Owner_);
+
+ ParseRequestId();
+
+ if (!TryParsePeerAddress()) {
+ YT_LOG_WARNING("Malformed peer address (PeerAddress: %v, RequestId: %v)",
+ PeerAddressString_,
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ ParseTraceContext();
+ ParseUser();
+ ParseUserTag();
+ ParseUserAgent();
+ ParseRpcCredentials();
+ ParseTimeout();
+
+ try {
+ SslCredentialsExt_ = WaitFor(ParseSslCredentials())
+ .ValueOrThrow();
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Failed to parse ssl credentials (RequestId: %v)",
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ if (!TryParseRoutingParameters()) {
+ YT_LOG_DEBUG("Malformed request routing parameters (RawMethod: %v, RequestId: %v)",
+ ToStringBuf(CallDetails_->method),
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ if (!TryParseMessageBodySize()) {
+ Unref();
+ return;
+ }
+
+ if (!TryParseProtocolVersion()) {
+ Unref();
+ return;
+ }
+
+ YT_LOG_DEBUG("Request accepted (RequestId: %v, Host: %v, Method: %v.%v, %v%vPeerAddress: %v, Timeout: %v, ProtocolVersion: %v)",
+ RequestId_,
+ ToStringBuf(CallDetails_->host),
+ ServiceName_,
+ MethodName_,
+ MakeFormatterWrapper([&] (auto* builder) {
+ if (User_) {
+ builder->AppendFormat("User: %v, ", *User_);
+ }
+ }),
+ MakeFormatterWrapper([&] (auto* builder) {
+ if (User_ && UserTag_ && *UserTag_ != *User_) {
+ builder->AppendFormat("UserTag: %v, ", *UserTag_);
+ }
+ }),
+ PeerAddressString_,
+ Timeout_,
+ ProtocolVersion_);
+
+ Service_ = Owner_->FindService(TServiceId(ServiceName_));
+
+ {
+ auto guard = Guard(SpinLock_);
+ Stage_ = EServerCallStage::ReceivingRequest;
+ }
+
+ std::array<grpc_op, 1> ops;
+
+ ops[0].op = GRPC_OP_RECV_MESSAGE;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.recv_message.recv_message = RequestBodyBuffer_.GetPtr();
+
+ StartBatch(ops, EServerCallCookie::Normal);
+ }
+
+ bool TryParsePeerAddress()
+ {
+ auto addressString = MakeGprString(grpc_call_get_peer(Call_.Unwrap()));
+ PeerAddressString_ = TString(addressString.get());
+
+ // Drop ipvN: prefix.
+ if (PeerAddressString_.StartsWith("ipv6:") || PeerAddressString_.StartsWith("ipv4:")) {
+ PeerAddressString_ = PeerAddressString_.substr(5);
+ }
+
+ // Decode URL-encoded square brackets.
+ CGIUnescape(PeerAddressString_);
+
+ auto address = NNet::TNetworkAddress::TryParse(PeerAddressString_);
+ if (!address.IsOK()) {
+ return false;
+ }
+
+ PeerAddress_ = address.Value();
+ return true;
+ }
+
+ void ParseTraceContext()
+ {
+ const auto traceIdString = CallMetadata_.Find(TracingTraceIdMetadataKey);
+ const auto spanIdString = CallMetadata_.Find(TracingSpanIdMetadataKey);
+ const auto sampledString = CallMetadata_.Find(TracingSampledMetadataKey);
+ const auto debugString = CallMetadata_.Find(TracingDebugMetadataKey);
+
+ if (!traceIdString &&
+ !spanIdString &&
+ !sampledString &&
+ !debugString)
+ {
+ return;
+ }
+
+ NTracing::NProto::TTracingExt traceContext{};
+ if (traceIdString) {
+ TGuid traceId;
+ if (!TGuid::FromString(traceIdString, &traceId)) {
+ return;
+ }
+ ToProto(traceContext.mutable_trace_id(), traceId);
+ }
+ if (spanIdString) {
+ NTracing::TSpanId spanId;
+ if (!TryFromString(spanIdString, spanId)) {
+ return;
+ }
+ traceContext.set_span_id(spanId);
+ }
+ if (sampledString) {
+ bool sampled;
+ if (!TryFromString(sampledString, sampled)) {
+ return;
+ }
+ traceContext.set_sampled(sampled);
+ }
+ if (debugString) {
+ bool debug;
+ if (!TryFromString(debugString, debug)) {
+ return;
+ }
+ traceContext.set_debug(debug);
+ }
+ TraceContext_.emplace(traceContext);
+ }
+
+ void ParseRequestId()
+ {
+ auto idString = CallMetadata_.Find(RequestIdMetadataKey);
+ if (!idString) {
+ RequestId_ = TRequestId::Create();
+ return;
+ }
+
+ if (!TRequestId::FromString(idString, &RequestId_)) {
+ RequestId_ = TRequestId::Create();
+ YT_LOG_WARNING("Malformed request id, using a random one (MalformedRequestId: %v, RequestId: %v)",
+ idString,
+ RequestId_);
+ }
+ }
+
+ void ParseUser()
+ {
+ auto userString = CallMetadata_.Find(UserMetadataKey);
+ if (!userString) {
+ return;
+ }
+
+ User_ = TString(userString);
+ }
+
+ void ParseUserTag()
+ {
+ auto userTagString = CallMetadata_.Find(UserTagMetadataKey);
+ if (!userTagString) {
+ return;
+ }
+
+ UserTag_ = TString(userTagString);
+ }
+
+ void ParseUserAgent()
+ {
+ auto userAgentString = CallMetadata_.Find(UserAgentMetadataKey);
+ if (!userAgentString) {
+ return;
+ }
+
+ UserAgent_ = TString(userAgentString);
+ }
+
+ void ParseRpcCredentials()
+ {
+ // COMPAT(babenko)
+ auto legacyTokenString = CallMetadata_.Find("yt-token");
+ auto tokenString = CallMetadata_.Find(AuthTokenMetadataKey);
+ auto sessionIdString = CallMetadata_.Find(AuthSessionIdMetadataKey);
+ auto sslSessionIdString = CallMetadata_.Find(AuthSslSessionIdMetadataKey);
+ auto userTicketString = CallMetadata_.Find(AuthUserTicketMetadataKey);
+
+ if (!tokenString &&
+ !legacyTokenString &&
+ !sessionIdString &&
+ !sslSessionIdString &&
+ !userTicketString)
+ {
+ return;
+ }
+
+ RpcCredentialsExt_.emplace();
+
+ if (legacyTokenString) {
+ RpcCredentialsExt_->set_token(TString(legacyTokenString));
+ }
+ if (tokenString) {
+ RpcCredentialsExt_->set_token(TString(tokenString));
+ }
+ if (sessionIdString) {
+ RpcCredentialsExt_->set_session_id(TString(sessionIdString));
+ }
+ if (sslSessionIdString) {
+ RpcCredentialsExt_->set_ssl_session_id(TString(sslSessionIdString));
+ }
+ if (userTicketString) {
+ RpcCredentialsExt_->set_user_ticket(TString(userTicketString));
+ }
+ }
+
+ TFuture<std::optional<NGrpc::NProto::TSslCredentialsExt>> ParseSslCredentials()
+ {
+ auto authContext = TGrpcAuthContextPtr(grpc_call_auth_context(Call_.Unwrap()));
+ return BIND(&DoParseSslCredentials, Passed(std::move(authContext)))
+ .AsyncVia(NRpc::TDispatcher::Get()->GetHeavyInvoker())
+ .Run();
+ }
+
+ static std::optional<NGrpc::NProto::TSslCredentialsExt> DoParseSslCredentials(TGrpcAuthContextPtr authContext)
+ {
+ if (!authContext) {
+ return std::nullopt;
+ }
+
+ std::optional<NGrpc::NProto::TSslCredentialsExt> sslCredentialsExtension;
+
+ ParsePeerIdentity(authContext, &sslCredentialsExtension);
+ ParseIssuer(authContext, &sslCredentialsExtension);
+
+ return sslCredentialsExtension;
+ }
+
+ static void ParsePeerIdentity(const TGrpcAuthContextPtr& authContext, std::optional<NGrpc::NProto::TSslCredentialsExt>* sslCredentialsExtension)
+ {
+ const char* peerIdentityPropertyName = grpc_auth_context_peer_identity_property_name(authContext.Unwrap());
+ if (!peerIdentityPropertyName) {
+ return;
+ }
+
+ auto peerIdentityPropertyIt = grpc_auth_context_find_properties_by_name(authContext.Unwrap(), peerIdentityPropertyName);
+ auto* peerIdentityProperty = grpc_auth_property_iterator_next(&peerIdentityPropertyIt);
+ if (!peerIdentityProperty) {
+ return;
+ }
+
+ if (!sslCredentialsExtension->has_value()) {
+ sslCredentialsExtension->emplace();
+ }
+ (*sslCredentialsExtension)->set_peer_identity(TString(peerIdentityProperty->value, peerIdentityProperty->value_length));
+ }
+
+ static void ParseIssuer(const TGrpcAuthContextPtr& authContext, std::optional<NGrpc::NProto::TSslCredentialsExt>* sslCredentialsExtension)
+ {
+ const char* peerIdentityPropertyName = grpc_auth_context_peer_identity_property_name(authContext.Unwrap());
+ if (!peerIdentityPropertyName) {
+ return;
+ }
+
+ auto pemCertPropertyIt = grpc_auth_context_find_properties_by_name(authContext.Unwrap(), GRPC_X509_PEM_CERT_PROPERTY_NAME);
+ auto* pemCertProperty = grpc_auth_property_iterator_next(&pemCertPropertyIt);
+ if (!pemCertProperty) {
+ return;
+ }
+
+ auto issuer = ParseIssuerFromX509(TStringBuf(pemCertProperty->value, pemCertProperty->value_length));
+ if (!issuer) {
+ return;
+ }
+
+ if (!sslCredentialsExtension->has_value()) {
+ sslCredentialsExtension->emplace();
+ }
+ (*sslCredentialsExtension)->set_issuer(std::move(*issuer));
+ }
+
+ void ParseTimeout()
+ {
+ auto deadline = CallDetails_->deadline;
+ deadline = gpr_convert_clock_type(deadline, GPR_CLOCK_REALTIME);
+ auto now = gpr_now(GPR_CLOCK_REALTIME);
+ if (gpr_time_cmp(now, deadline) >= 0) {
+ Timeout_ = TDuration::Zero();
+ return;
+ }
+
+ auto micros = gpr_timespec_to_micros(gpr_time_sub(deadline, now));
+ if (micros > static_cast<double>(std::numeric_limits<ui64>::max() / 2)) {
+ return;
+ }
+
+ Timeout_ = TDuration::MicroSeconds(static_cast<ui64>(micros));
+ }
+
+ bool TryParseRoutingParameters()
+ {
+ const size_t methodLength = GRPC_SLICE_LENGTH(CallDetails_->method);
+ if (methodLength == 0) {
+ return false;
+ }
+
+ if (*GRPC_SLICE_START_PTR(CallDetails_->method) != '/') {
+ return false;
+ }
+
+ auto methodWithoutLeadingSlash = grpc_slice_sub_no_ref(CallDetails_->method, 1, methodLength);
+ const int secondSlashIndex = grpc_slice_chr(methodWithoutLeadingSlash, '/');
+ if (secondSlashIndex < 0) {
+ return false;
+ }
+
+ const char *serviceNameStart = reinterpret_cast<const char *>(GRPC_SLICE_START_PTR(methodWithoutLeadingSlash));
+ ServiceName_.assign(serviceNameStart, secondSlashIndex);
+ MethodName_.assign(serviceNameStart + secondSlashIndex + 1, methodLength - 1 - (secondSlashIndex + 1));
+ return true;
+ }
+
+ bool TryParseMessageBodySize()
+ {
+ auto messageBodySizeString = CallMetadata_.Find(MessageBodySizeMetadataKey);
+ if (!messageBodySizeString) {
+ return true;
+ }
+
+ try {
+ RequestMessageBodySize_ = FromString<ui32>(messageBodySizeString);
+ } catch (const std::exception& ex) {
+ YT_LOG_WARNING(ex, "Failed to parse message body size from request metadata (RequestId: %v)",
+ RequestId_);
+ return false;
+ }
+
+ return true;
+ }
+
+ bool TryParseProtocolVersion()
+ {
+ auto protocolVersionString = CallMetadata_.Find(ProtocolVersionMetadataKey);
+ if (!protocolVersionString) {
+ return true;
+ }
+
+ try {
+ ProtocolVersion_ = TProtocolVersion::FromString(protocolVersionString);
+ } catch (const std::exception& ex) {
+ YT_LOG_WARNING(ex, "Failed to parse protocol version from string (RequestId: %v)",
+ RequestId_);
+ return false;
+ }
+
+ return true;
+ }
+
+ void OnRequestReceived(bool success)
+ {
+ if (!success) {
+ YT_LOG_DEBUG("Failed to receive request body (RequestId: %v)",
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ if (!RequestBodyBuffer_) {
+ YT_LOG_DEBUG("Empty request body received (RequestId: %v)",
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ TMessageWithAttachments messageWithAttachments;
+ try {
+ messageWithAttachments = ByteBufferToMessageWithAttachments(
+ RequestBodyBuffer_.Unwrap(),
+ RequestMessageBodySize_);
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Failed to receive request body (RequestId: %v)",
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ auto header = std::make_unique<NRpc::NProto::TRequestHeader>();
+ ToProto(header->mutable_request_id(), RequestId_);
+ if (User_) {
+ header->set_user(*User_);
+ }
+ if (UserTag_) {
+ header->set_user_tag(*UserTag_);
+ }
+ if (UserAgent_) {
+ header->set_user_agent(*UserAgent_);
+ }
+ if (TraceContext_) {
+ *header->MutableExtension(NRpc::NProto::TRequestHeader::tracing_ext) = std::move(*TraceContext_);
+ }
+ header->set_service(ServiceName_);
+ header->set_method(MethodName_);
+ header->set_protocol_version_major(ProtocolVersion_.Major);
+ header->set_protocol_version_minor(ProtocolVersion_.Minor);
+ if (Timeout_) {
+ header->set_timeout(ToProto<i64>(*Timeout_));
+ }
+ if (SslCredentialsExt_) {
+ *header->MutableExtension(NGrpc::NProto::TSslCredentialsExt::ssl_credentials_ext) = std::move(*SslCredentialsExt_);
+ }
+ if (RpcCredentialsExt_) {
+ *header->MutableExtension(NRpc::NProto::TCredentialsExt::credentials_ext) = std::move(*RpcCredentialsExt_);
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ Stage_ = EServerCallStage::SendingInitialMetadata;
+ }
+
+ YT_LOG_DEBUG("Request received (RequestId: %v)",
+ RequestId_);
+
+ InitialMetadataBuilder_.Add(RequestIdMetadataKey, ToString(RequestId_));
+
+ {
+ std::array<grpc_op, 1> ops;
+
+ ops[0].op = GRPC_OP_SEND_INITIAL_METADATA;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.send_initial_metadata.maybe_compression_level.is_set = false;
+ ops[0].data.send_initial_metadata.metadata = InitialMetadataBuilder_.Unwrap();
+ ops[0].data.send_initial_metadata.count = InitialMetadataBuilder_.GetSize();
+
+ StartBatch(ops, EServerCallCookie::Normal);
+ }
+
+ {
+ Ref();
+
+ std::array<grpc_op, 1> ops;
+
+ ops[0].op = GRPC_OP_RECV_CLOSE_ON_SERVER;
+ ops[0].flags = 0;
+ ops[0].reserved = nullptr;
+ ops[0].data.recv_close_on_server.cancelled = &RawCanceled_;
+
+ StartBatch(ops, EServerCallCookie::Close);
+ }
+
+ auto replyBus = New<TReplyBus>(this);
+ if (Service_) {
+ auto requestMessage = CreateRequestMessage(
+ *header,
+ messageWithAttachments.Message,
+ messageWithAttachments.Attachments);
+
+ Service_->HandleRequest(std::move(header), std::move(requestMessage), std::move(replyBus));
+ } else {
+ auto error = TError(
+ NRpc::EErrorCode::NoSuchService,
+ "Service is not registered")
+ << TErrorAttribute("service", ServiceName_);
+ YT_LOG_WARNING(error);
+
+ auto responseMessage = CreateErrorResponseMessage(RequestId_, error);
+ YT_UNUSED_FUTURE(replyBus->Send(std::move(responseMessage), NBus::TSendOptions(EDeliveryTrackingLevel::None)));
+ }
+ }
+
+ void OnInitialMetadataSent(bool success)
+ {
+ if (!success) {
+ YT_LOG_DEBUG("Failed to send initial metadata (RequestId: %v)",
+ RequestId_);
+ Unref();
+ return;
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (ResponseMessage_) {
+ SendResponse(guard);
+ } else {
+ Stage_ = EServerCallStage::WaitingForService;
+ CheckCanceled(guard);
+ }
+ }
+ }
+
+ void OnResponseMessage(TSharedRefArray message)
+ {
+ auto guard = Guard(SpinLock_);
+
+ YT_VERIFY(!ResponseMessage_);
+ ResponseMessage_ = std::move(message);
+
+ if (Stage_ == EServerCallStage::WaitingForService) {
+ SendResponse(guard);
+ }
+ }
+
+ void SendResponse(TGuard<NThreading::TSpinLock>& guard)
+ {
+ Stage_ = EServerCallStage::SendingResponse;
+ guard.Release();
+
+ YT_LOG_DEBUG("Sending response (RequestId: %v)",
+ RequestId_);
+
+ {
+ auto guard = Guard(TraceContextSpinLock_);
+ TraceContextHandler_.UpdateTraceContext();
+ }
+
+ NRpc::NProto::TResponseHeader responseHeader;
+ YT_VERIFY(TryParseResponseHeader(ResponseMessage_, &responseHeader));
+
+ TCompactVector<grpc_op, 2> ops;
+
+ TError error;
+ if (responseHeader.has_error() && responseHeader.error().code() != static_cast<int>(NYT::EErrorCode::OK)) {
+ FromProto(&error, responseHeader.error());
+ ErrorMessage_ = ToString(error);
+ ErrorMessageSlice_.Reset(grpc_slice_from_static_string(ErrorMessage_.c_str()));
+ TrailingMetadataBuilder_.Add(ErrorMetadataKey, SerializeError(error));
+ } else {
+ YT_VERIFY(ResponseMessage_.Size() >= 2);
+
+ TMessageWithAttachments messageWithAttachments;
+ messageWithAttachments.Message = ExtractMessageFromEnvelopedMessage(ResponseMessage_[1]);
+ for (int index = 2; index < std::ssize(ResponseMessage_); ++index) {
+ messageWithAttachments.Attachments.push_back(ResponseMessage_[index]);
+ }
+
+ ResponseBodyBuffer_ = MessageWithAttachmentsToByteBuffer(messageWithAttachments);
+
+ if (!messageWithAttachments.Attachments.empty()) {
+ TrailingMetadataBuilder_.Add(MessageBodySizeMetadataKey, ToString(messageWithAttachments.Message.Size()));
+ }
+
+ ops.emplace_back();
+ ops.back().op = GRPC_OP_SEND_MESSAGE;
+ ops.back().data.send_message.send_message = ResponseBodyBuffer_.Unwrap();
+ ops.back().flags = 0;
+ ops.back().reserved = nullptr;
+ }
+
+ ops.emplace_back();
+ ops.back().op = GRPC_OP_SEND_STATUS_FROM_SERVER;
+ ops.back().flags = 0;
+ ops.back().reserved = nullptr;
+ ops.back().data.send_status_from_server.status = error.IsOK() ? GRPC_STATUS_OK : grpc_status_code(GenericErrorStatusCode);
+ ops.back().data.send_status_from_server.status_details = error.IsOK() ? nullptr : ErrorMessageSlice_.Unwrap();
+ ops.back().data.send_status_from_server.trailing_metadata_count = TrailingMetadataBuilder_.GetSize();
+ ops.back().data.send_status_from_server.trailing_metadata = TrailingMetadataBuilder_.Unwrap();
+
+ StartBatch(ops, EServerCallCookie::Normal);
+ }
+
+
+ void OnResponseSent(bool success)
+ {
+ {
+ auto guard = Guard(SpinLock_);
+ Stage_ = EServerCallStage::Done;
+ }
+
+ if (success) {
+ YT_LOG_DEBUG("Response sent (RequestId: %v)",
+ RequestId_);
+ } else {
+ YT_LOG_DEBUG("Failed to send response (RequestId: %v)",
+ RequestId_);
+ }
+
+ Unref();
+ }
+
+ void OnCloseReceived(bool success)
+ {
+ if (success) {
+ if (RawCanceled_) {
+ OnCanceled();
+ } else {
+ YT_LOG_DEBUG("Request closed (RequestId: %v)",
+ RequestId_);
+ }
+ } else {
+ YT_LOG_DEBUG("Failed to close request (RequestId: %v)",
+ RequestId_);
+ }
+
+ Unref();
+ }
+
+ void OnCanceled()
+ {
+ YT_LOG_DEBUG("Request cancelation received (RequestId: %v)",
+ RequestId_);
+
+ if (Service_) {
+ Service_->HandleRequestCancellation(RequestId_);
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ CancelRequested_ = true;
+ CheckCanceled(guard);
+ }
+ }
+
+ void CheckCanceled(TGuard<NThreading::TSpinLock>& guard)
+ {
+ if (CancelRequested_ && Stage_ == EServerCallStage::WaitingForService) {
+ Stage_ = EServerCallStage::Done;
+ guard.Release();
+ Unref();
+ }
+ }
+ };
+};
+
+DEFINE_REFCOUNTED_TYPE(TServer)
+
+IServerPtr CreateServer(TServerConfigPtr config)
+{
+ return New<TServer>(std::move(config));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/server.h b/yt/yt/core/rpc/grpc/server.h
new file mode 100644
index 0000000000..fc9d402003
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/server.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NRpc::NGrpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NRpc::IServerPtr CreateServer(TServerConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc::NGrpc
diff --git a/yt/yt/core/rpc/grpc/ya.make b/yt/yt/core/rpc/grpc/ya.make
new file mode 100644
index 0000000000..bd21cff387
--- /dev/null
+++ b/yt/yt/core/rpc/grpc/ya.make
@@ -0,0 +1,27 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ config.cpp
+ public.cpp
+ dispatcher.cpp
+ server.cpp
+ helpers.cpp
+ channel.cpp
+ proto/grpc.proto
+)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/crypto
+ contrib/libs/grpc
+)
+
+ADDINCL(
+ contrib/libs/grpc # Needed for `grpc_core::Executor::SetThreadsLimit`.
+)
+
+END()
diff --git a/yt/yt/core/rpc/hedging_channel-inl.h b/yt/yt/core/rpc/hedging_channel-inl.h
new file mode 100644
index 0000000000..e8386e8618
--- /dev/null
+++ b/yt/yt/core/rpc/hedging_channel-inl.h
@@ -0,0 +1,24 @@
+#ifndef HEDGING_CHANNEL_INL_H_
+#error "Direct inclusion of this file is not allowed, include hedging_channel.h"
+// For the sake of sane code completion.
+#include "hedging_channel.h"
+#endif
+#undef HEDGING_CHANNEL_INL_H_
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TString BackupFailedKey("backup_failed");
+
+template <class T>
+bool IsBackup(const TErrorOr<TIntrusivePtr<T>>& responseOrError)
+{
+ return responseOrError.IsOK()
+ ? IsBackup(responseOrError.Value())
+ : responseOrError.Attributes().template Get<bool>(BackupFailedKey, false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/hedging_channel.cpp b/yt/yt/core/rpc/hedging_channel.cpp
new file mode 100644
index 0000000000..a0e379af62
--- /dev/null
+++ b/yt/yt/core/rpc/hedging_channel.cpp
@@ -0,0 +1,425 @@
+#include "hedging_channel.h"
+#include "channel.h"
+#include "client.h"
+#include "private.h"
+
+#include <yt/yt/core/misc/hedging_manager.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <atomic>
+
+namespace NYT::NRpc {
+
+using namespace NYTree;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THedgingResponseHandler)
+DECLARE_REFCOUNTED_CLASS(THedgingSession)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THedgingResponseHandler
+ : public IClientResponseHandler
+{
+public:
+ THedgingResponseHandler(
+ THedgingSessionPtr session,
+ bool backup)
+ : Session_(std::move(session))
+ , Backup_(backup)
+ { }
+
+ // IClientResponseHandler implementation.
+ void HandleAcknowledgement() override;
+ void HandleResponse(TSharedRefArray message, TString address) override;
+ void HandleError(const TError& error) override;
+ void HandleStreamingPayload(const TStreamingPayload& /*payload*/) override;
+ void HandleStreamingFeedback(const TStreamingFeedback& /*feedback*/) override;
+
+private:
+ const THedgingSessionPtr Session_;
+ const bool Backup_;
+};
+
+DEFINE_REFCOUNTED_TYPE(THedgingResponseHandler)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THedgingSession
+ : public IClientRequestControl
+{
+public:
+ THedgingSession(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& sendOptions,
+ IChannelPtr primaryChannel,
+ IChannelPtr backupChannel,
+ const THedgingChannelOptions& hedgingOptions)
+ : Request_(std::move(request))
+ , ResponseHandler_(std::move(responseHandler))
+ , SendOptions_(sendOptions)
+ , PrimaryChannel_(std::move(primaryChannel))
+ , BackupChannel_(std::move(backupChannel))
+ , HedgingOptions_(hedgingOptions)
+ {
+ HedgingDelay_ = HedgingOptions_.HedgingManager->OnPrimaryRequestsStarted(/*requestCount*/ 1);
+
+ auto hedgingResponseHandler = New<THedgingResponseHandler>(this, false);
+
+ auto requestControl = PrimaryChannel_->Send(
+ Request_,
+ std::move(hedgingResponseHandler),
+ SendOptions_);
+
+ RegisterSentRequest(std::move(requestControl));
+
+ if (HedgingDelay_ == TDuration::Zero()) {
+ OnDeadlineReached(false);
+ } else {
+ DeadlineCookie_ = TDelayedExecutor::Submit(
+ BIND(&THedgingSession::OnDeadlineReached, MakeWeak(this)),
+ HedgingDelay_);
+ }
+ }
+
+ void HandleAcknowledgement(bool backup)
+ {
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(SpinLock_);
+ if (Acknowledged_ || !ResponseHandler_) {
+ return;
+ }
+ Acknowledged_ = true;
+ responseHandler = ResponseHandler_;
+ }
+
+ YT_LOG_DEBUG_IF(backup, "Request acknowledged by backup (RequestId: %v)",
+ Request_->GetRequestId());
+
+ responseHandler->HandleAcknowledgement();
+ }
+
+ void HandleResponse(TSharedRefArray message, TString address, bool backup)
+ {
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(SpinLock_);
+ if (Responded_ || !ResponseHandler_) {
+ return;
+ }
+ Responded_ = true;
+ std::swap(ResponseHandler_, responseHandler);
+ TDelayedExecutor::CancelAndClear(DeadlineCookie_);
+ }
+
+ if (backup) {
+ YT_LOG_DEBUG("Response received from backup (RequestId: %v)",
+ Request_->GetRequestId());
+
+ NRpc::NProto::TResponseHeader header;
+ if (!TryParseResponseHeader(message, &header)) {
+ ResponseHandler_->HandleError(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Error parsing response header from backup")
+ << TErrorAttribute(BackupFailedKey, Request_->GetRequestId())
+ << TErrorAttribute("request_id", Request_->GetRequestId()));
+ return;
+ }
+
+ auto* ext = header.MutableExtension(NRpc::NProto::THedgingExt::hedging_ext);
+ ext->set_backup_responded(true);
+ message = SetResponseHeader(std::move(message), header);
+ }
+
+ responseHandler->HandleResponse(std::move(message), std::move(address));
+ }
+
+ void HandleError(const TError& error, bool backup)
+ {
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(SpinLock_);
+ if (Responded_ || !ResponseHandler_) {
+ return;
+ }
+ if (!backup && error.GetCode() == NYT::EErrorCode::Canceled && PrimaryCanceled_) {
+ return;
+ }
+ Responded_ = true;
+ std::swap(ResponseHandler_, responseHandler);
+ TDelayedExecutor::CancelAndClear(DeadlineCookie_);
+ }
+
+ YT_LOG_DEBUG_IF(backup, "Request failed at backup (RequestId: %v)",
+ Request_->GetRequestId());
+
+ responseHandler->HandleError(
+ backup
+ ? error << TErrorAttribute(BackupFailedKey, true)
+ : error);
+ }
+
+ // IClientRequestControl implementation.
+ void Cancel() override
+ {
+ IClientResponseHandlerPtr responseHandler;
+ {
+ auto guard = Guard(SpinLock_);
+ std::swap(ResponseHandler_, responseHandler);
+ TDelayedExecutor::CancelAndClear(DeadlineCookie_);
+ CancelSentRequests(std::move(guard));
+ }
+ }
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& /*payload*/) override
+ {
+ YT_ABORT();
+ }
+
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& /*feedback*/) override
+ {
+ YT_ABORT();
+ }
+
+private:
+ const IClientRequestPtr Request_;
+ IClientResponseHandlerPtr ResponseHandler_;
+ const TSendOptions SendOptions_;
+ const IChannelPtr PrimaryChannel_;
+ const IChannelPtr BackupChannel_;
+ const THedgingChannelOptions HedgingOptions_;
+
+ TDuration HedgingDelay_;
+
+ TDelayedExecutorCookie DeadlineCookie_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ bool Acknowledged_ = false;
+ bool Responded_ = false;
+ bool PrimaryCanceled_ = false;
+ TCompactVector<IClientRequestControlPtr, 2> RequestControls_; // always at most 2 items
+
+
+ void RegisterSentRequest(IClientRequestControlPtr requestControl)
+ {
+ auto guard = Guard(SpinLock_);
+ RequestControls_.push_back(std::move(requestControl));
+ }
+
+ void CancelSentRequests(TGuard<NThreading::TSpinLock>&& guard)
+ {
+ TCompactVector<IClientRequestControlPtr, 2> requestControls;
+ std::swap(RequestControls_, requestControls);
+
+ guard.Release();
+
+ for (const auto& control : requestControls) {
+ control->Cancel();
+ }
+ }
+
+ std::optional<TDuration> GetBackupTimeout()
+ {
+ if (!SendOptions_.Timeout) {
+ return std::nullopt;
+ }
+
+ auto timeout = *SendOptions_.Timeout;
+ if (timeout < HedgingDelay_) {
+ return TDuration::Zero();
+ }
+
+ return timeout - HedgingDelay_;
+ }
+
+ void OnDeadlineReached(bool aborted)
+ {
+ if (aborted) {
+ return;
+ }
+
+ auto backupTimeout = GetBackupTimeout();
+ if (backupTimeout == TDuration::Zero()) {
+ // Makes no sense to send the request anyway.
+ return;
+ }
+
+ if (!HedgingOptions_.HedgingManager->OnHedgingDelayPassed(/*requestCount*/ 1)) {
+ YT_LOG_DEBUG("Hedging manager restrained sending backup request (RequestId: %v)",
+ Request_->GetRequestId());
+ return;
+ }
+
+ {
+ auto guard = Guard(SpinLock_);
+ if (Responded_ || !ResponseHandler_) {
+ return;
+ }
+ if (HedgingOptions_.CancelPrimaryOnHedging && HedgingDelay_ != TDuration::Zero()) {
+ PrimaryCanceled_ = true;
+ CancelSentRequests(std::move(guard));
+ }
+ }
+
+ YT_LOG_DEBUG("Resending request to backup (RequestId: %v)",
+ Request_->GetRequestId());
+
+ auto responseHandler = New<THedgingResponseHandler>(this, true);
+
+ auto backupOptions = SendOptions_;
+ backupOptions.Timeout = backupTimeout;
+
+ auto requestControl = BackupChannel_->Send(
+ Request_,
+ std::move(responseHandler),
+ backupOptions);
+
+ RegisterSentRequest(std::move(requestControl));
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(THedgingSession)
+
+////////////////////////////////////////////////////////////////////////////////
+
+void THedgingResponseHandler::HandleAcknowledgement()
+{
+ Session_->HandleAcknowledgement(Backup_);
+}
+
+void THedgingResponseHandler::HandleError(const TError& error)
+{
+ Session_->HandleError(error, Backup_);
+}
+
+void THedgingResponseHandler::HandleResponse(TSharedRefArray message, TString address)
+{
+ Session_->HandleResponse(std::move(message), std::move(address), Backup_);
+}
+
+void THedgingResponseHandler::HandleStreamingPayload(const TStreamingPayload& /*payload*/)
+{
+ YT_ABORT();
+}
+
+void THedgingResponseHandler::HandleStreamingFeedback(const TStreamingFeedback& /*feedback*/)
+{
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class THedgingChannel
+ : public IChannel
+{
+public:
+ THedgingChannel(
+ IChannelPtr primaryChannel,
+ IChannelPtr backupChannel,
+ THedgingChannelOptions options)
+ : PrimaryChannel_(std::move(primaryChannel))
+ , BackupChannel_(std::move(backupChannel))
+ , Options_(std::move(options))
+ , EndpointDescription_(Format("Hedging(%v,%v)",
+ PrimaryChannel_->GetEndpointDescription(),
+ BackupChannel_->GetEndpointDescription()))
+ , EndpointAttributes_(ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("primary").Value(PrimaryChannel_->GetEndpointAttributes())
+ .Item("backup").Value(BackupChannel_->GetEndpointAttributes())
+ .EndMap()))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription_;
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes_;
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ return New<THedgingSession>(
+ std::move(request),
+ std::move(responseHandler),
+ options,
+ PrimaryChannel_,
+ BackupChannel_,
+ Options_);
+ }
+
+ void Terminate(const TError& error) override
+ {
+ PrimaryChannel_->Terminate(error);
+ BackupChannel_->Terminate(error);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ PrimaryChannel_->SubscribeTerminated(callback);
+ BackupChannel_->SubscribeTerminated(callback);
+ }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ PrimaryChannel_->UnsubscribeTerminated(callback);
+ BackupChannel_->UnsubscribeTerminated(callback);
+ }
+
+private:
+ const IChannelPtr PrimaryChannel_;
+ const IChannelPtr BackupChannel_;
+
+ const THedgingChannelOptions Options_;
+
+ const TString EndpointDescription_;
+ const IAttributeDictionaryPtr EndpointAttributes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateHedgingChannel(
+ IChannelPtr primaryChannel,
+ IChannelPtr backupChannel,
+ const THedgingChannelOptions& options)
+{
+ YT_VERIFY(primaryChannel);
+ YT_VERIFY(backupChannel);
+ YT_VERIFY(options.HedgingManager);
+
+ return New<THedgingChannel>(
+ std::move(primaryChannel),
+ std::move(backupChannel),
+ options);
+}
+
+bool IsBackup(const TClientResponsePtr& response)
+{
+ const auto& ext = response->Header().GetExtension(NRpc::NProto::THedgingExt::hedging_ext);
+ return ext.backup_responded();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/hedging_channel.h b/yt/yt/core/rpc/hedging_channel.h
new file mode 100644
index 0000000000..62ba57eef4
--- /dev/null
+++ b/yt/yt/core/rpc/hedging_channel.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct THedgingChannelOptions
+{
+ IHedgingManagerPtr HedgingManager;
+ bool CancelPrimaryOnHedging = false;
+};
+
+//! The resulting channel initially forwards a request to #primaryChannel and
+//! if no response comes within delay, re-sends the request to #backupChannel
+//! (optionally canceling the initial request).
+//! Whatever underlying channel responds first is the winner.
+//! HedgingManager determines hedging delay and may restrain channel from sending backup request.
+IChannelPtr CreateHedgingChannel(
+ IChannelPtr primaryChannel,
+ IChannelPtr backupChannel,
+ const THedgingChannelOptions& options);
+
+//! Returns |true| if #response was received from backup.
+bool IsBackup(const TClientResponsePtr& response);
+
+//! Returns |true| if #responseOrError was received from backup.
+template <class T>
+bool IsBackup(const TErrorOr<TIntrusivePtr<T>>& responseOrError);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+#define HEDGING_CHANNEL_INL_H_
+#include "hedging_channel-inl.h"
+#undef HEDGING_CHANNEL_INL_H_
diff --git a/yt/yt/core/rpc/helpers-inl.h b/yt/yt/core/rpc/helpers-inl.h
new file mode 100644
index 0000000000..aa66c5513a
--- /dev/null
+++ b/yt/yt/core/rpc/helpers-inl.h
@@ -0,0 +1,50 @@
+#ifndef HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+
+#include "authentication_identity.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void WriteAuthenticationIdentityToProto(T* proto, const TAuthenticationIdentity& identity)
+{
+ if (identity.User == RootUserName) {
+ proto->clear_user();
+ } else {
+ proto->set_user(identity.User);
+ }
+ if (identity.UserTag == identity.User) {
+ proto->clear_user_tag();
+ } else {
+ proto->set_user_tag(identity.UserTag);
+ }
+}
+
+template <class T>
+TAuthenticationIdentity ParseAuthenticationIdentityFromProto(const T& proto)
+{
+ TAuthenticationIdentity identity;
+ identity.User = proto.has_user() ? proto.user() : RootUserName;
+ identity.UserTag = proto.has_user_tag() ? proto.user_tag () : identity.User;
+ return identity;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+int FeatureIdToInt(E featureId)
+{
+ static_assert(
+ std::is_same_v<int, std::underlying_type_t<E>>,
+ "Feature set enum must have `int` as its underlying type.");
+ return ToUnderlying(featureId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/helpers.cpp b/yt/yt/core/rpc/helpers.cpp
new file mode 100644
index 0000000000..cce4c59676
--- /dev/null
+++ b/yt/yt/core/rpc/helpers.cpp
@@ -0,0 +1,608 @@
+#include "helpers.h"
+#include "client.h"
+#include "dispatcher.h"
+#include "channel_detail.h"
+#include "service.h"
+#include "authentication_identity.h"
+
+#include <yt/yt/core/ytree/helpers.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/service_discovery/service_discovery.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace NYT::NRpc {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NRpc::NProto;
+using namespace NTracing;
+using namespace NYT::NBus;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsRetriableError(const TError& error)
+{
+ if (IsChannelFailureError(error)) {
+ return true;
+ }
+ auto code = error.GetCode();
+ return
+ code == NRpc::EErrorCode::RequestQueueSizeLimitExceeded ||
+ code == NRpc::EErrorCode::TransientFailure ||
+ code == NRpc::EErrorCode::Unavailable ||
+ code == NYT::EErrorCode::Timeout;
+}
+
+bool IsChannelFailureError(const TError& error)
+{
+ auto code = error.GetCode();
+ // COMPAT(babenko): see YT-13870, 1707 is NTabletClient::EErrorCode::TableMountInfoNotReady
+ if (code == NRpc::EErrorCode::Unavailable &&
+ error.FindMatching(TErrorCode(1707)))
+ {
+ return false;
+ }
+ return
+ code == NRpc::EErrorCode::TransportError ||
+ code == NRpc::EErrorCode::Unavailable ||
+ code == NRpc::EErrorCode::NoSuchService ||
+ code == NRpc::EErrorCode::NoSuchMethod ||
+ code == NRpc::EErrorCode::ProtocolError ||
+ code == NRpc::EErrorCode::PeerBanned ||
+ code == NRpc::EErrorCode::NoSuchRealm ||
+ // COMPAT(babenko): this is NRpcProxy::EErrorCode::ProxyBanned
+ code == TErrorCode(2100);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDefaultTimeoutChannel
+ : public TChannelWrapper
+{
+public:
+ TDefaultTimeoutChannel(IChannelPtr underlyingChannel, TDuration timeout)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , Timeout_(timeout)
+ { }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ auto adjustedOptions = options;
+ if (!adjustedOptions.Timeout) {
+ adjustedOptions.Timeout = Timeout_;
+ }
+ return UnderlyingChannel_->Send(
+ request,
+ responseHandler,
+ adjustedOptions);
+ }
+
+private:
+ const TDuration Timeout_;
+
+};
+
+IChannelPtr CreateDefaultTimeoutChannel(IChannelPtr underlyingChannel, TDuration timeout)
+{
+ YT_VERIFY(underlyingChannel);
+
+ return New<TDefaultTimeoutChannel>(underlyingChannel, timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDefaultTimeoutChannelFactory
+ : public IChannelFactory
+{
+public:
+ TDefaultTimeoutChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration timeout)
+ : UnderlyingFactory_(underlyingFactory)
+ , Timeout_(timeout)
+ { }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto underlyingChannel = UnderlyingFactory_->CreateChannel(address);
+ return CreateDefaultTimeoutChannel(underlyingChannel, Timeout_);
+ }
+
+private:
+ const IChannelFactoryPtr UnderlyingFactory_;
+ const TDuration Timeout_;
+};
+
+IChannelFactoryPtr CreateDefaultTimeoutChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration timeout)
+{
+ YT_VERIFY(underlyingFactory);
+
+ return New<TDefaultTimeoutChannelFactory>(underlyingFactory, timeout);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAuthenticatedChannel
+ : public TChannelWrapper
+{
+public:
+ TAuthenticatedChannel(
+ IChannelPtr underlyingChannel,
+ TAuthenticationIdentity identity)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , AuthenticationIdentity_(std::move(identity))
+ { }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ SetAuthenticationIdentity(request, AuthenticationIdentity_);
+ return UnderlyingChannel_->Send(
+ request,
+ responseHandler,
+ options);
+ }
+
+private:
+ const TAuthenticationIdentity AuthenticationIdentity_;
+};
+
+IChannelPtr CreateAuthenticatedChannel(
+ IChannelPtr underlyingChannel,
+ TAuthenticationIdentity identity)
+{
+ YT_VERIFY(underlyingChannel);
+
+ return New<TAuthenticatedChannel>(
+ std::move(underlyingChannel),
+ std::move(identity));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAuthenticatedChannelFactory
+ : public IChannelFactory
+{
+public:
+ TAuthenticatedChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TAuthenticationIdentity identity)
+ : UnderlyingFactory_(std::move(underlyingFactory))
+ , AuthenticationIdentity_(identity)
+ { }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto underlyingChannel = UnderlyingFactory_->CreateChannel(address);
+ return CreateAuthenticatedChannel(underlyingChannel, AuthenticationIdentity_);
+ }
+
+private:
+ const IChannelFactoryPtr UnderlyingFactory_;
+ const TAuthenticationIdentity AuthenticationIdentity_;
+
+};
+
+IChannelFactoryPtr CreateAuthenticatedChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TAuthenticationIdentity identity)
+{
+ YT_VERIFY(underlyingFactory);
+
+ return New<TAuthenticatedChannelFactory>(
+ std::move(underlyingFactory),
+ std::move(identity));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRealmChannel
+ : public TChannelWrapper
+{
+public:
+ TRealmChannel(IChannelPtr underlyingChannel, TRealmId realmId)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , RealmId_(realmId)
+ { }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ ToProto(request->Header().mutable_realm_id(), RealmId_);
+ return UnderlyingChannel_->Send(
+ request,
+ responseHandler,
+ options);
+ }
+
+private:
+ const TRealmId RealmId_;
+
+};
+
+IChannelPtr CreateRealmChannel(IChannelPtr underlyingChannel, TRealmId realmId)
+{
+ YT_VERIFY(underlyingChannel);
+
+ return New<TRealmChannel>(std::move(underlyingChannel), realmId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRealmChannelFactory
+ : public IChannelFactory
+{
+public:
+ TRealmChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TRealmId realmId)
+ : UnderlyingFactory_(std::move(underlyingFactory))
+ , RealmId_(realmId)
+ { }
+
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ auto underlyingChannel = UnderlyingFactory_->CreateChannel(address);
+ return CreateRealmChannel(underlyingChannel, RealmId_);
+ }
+
+private:
+ const IChannelFactoryPtr UnderlyingFactory_;
+ const TRealmId RealmId_;
+};
+
+IChannelFactoryPtr CreateRealmChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TRealmId realmId)
+{
+ YT_VERIFY(underlyingFactory);
+
+ return New<TRealmChannelFactory>(std::move(underlyingFactory), realmId);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFailureDetectingChannel
+ : public TChannelWrapper
+{
+public:
+ TFailureDetectingChannel(
+ IChannelPtr underlyingChannel,
+ std::optional<TDuration> acknowledgementTimeout,
+ TCallback<void(const IChannelPtr&, const TError&)> onFailure,
+ TCallback<bool(const TError&)> isError)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , AcknowledgementTimeout_(acknowledgementTimeout)
+ , OnFailure_(std::move(onFailure))
+ , IsError_(std::move(isError))
+ , OnTerminated_(BIND(&TFailureDetectingChannel::OnTerminated, MakeWeak(this)))
+ {
+ UnderlyingChannel_->SubscribeTerminated(OnTerminated_);
+ }
+
+ ~TFailureDetectingChannel()
+ {
+ UnderlyingChannel_->UnsubscribeTerminated(OnTerminated_);
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ auto updatedOptions = options;
+ if (AcknowledgementTimeout_) {
+ updatedOptions.AcknowledgementTimeout = AcknowledgementTimeout_;
+ }
+ return UnderlyingChannel_->Send(
+ request,
+ New<TResponseHandler>(this, std::move(responseHandler), OnFailure_, IsError_),
+ updatedOptions);
+ }
+
+private:
+ const std::optional<TDuration> AcknowledgementTimeout_;
+ const TCallback<void(const IChannelPtr&, const TError&)> OnFailure_;
+ const TCallback<bool(const TError&)> IsError_;
+ const TCallback<void(const TError&)> OnTerminated_;
+
+
+ void OnTerminated(const TError& error)
+ {
+ OnFailure_(this, error);
+ }
+
+ class TResponseHandler
+ : public IClientResponseHandler
+ {
+ public:
+ TResponseHandler(
+ IChannelPtr channel,
+ IClientResponseHandlerPtr underlyingHandler,
+ TCallback<void(const IChannelPtr&, const TError&)> onFailure,
+ TCallback<bool(const TError&)> isError)
+ : Channel_(std::move(channel))
+ , UnderlyingHandler_(std::move(underlyingHandler))
+ , OnFailure_(std::move(onFailure))
+ , IsError_(std::move(isError))
+ { }
+
+ void HandleAcknowledgement() override
+ {
+ UnderlyingHandler_->HandleAcknowledgement();
+ }
+
+ void HandleResponse(TSharedRefArray message, TString address) override
+ {
+ UnderlyingHandler_->HandleResponse(std::move(message), std::move(address));
+ }
+
+ void HandleError(const TError& error) override
+ {
+ if (IsError_(error)) {
+ OnFailure_.Run(Channel_, error);
+ }
+ UnderlyingHandler_->HandleError(error);
+ }
+
+ void HandleStreamingPayload(const TStreamingPayload& payload) override
+ {
+ UnderlyingHandler_->HandleStreamingPayload(payload);
+ }
+
+ void HandleStreamingFeedback(const TStreamingFeedback& feedback) override
+ {
+ UnderlyingHandler_->HandleStreamingFeedback(feedback);
+ }
+
+ private:
+ const IChannelPtr Channel_;
+ const IClientResponseHandlerPtr UnderlyingHandler_;
+ const TCallback<void(const IChannelPtr&, const TError&)> OnFailure_;
+ const TCallback<bool(const TError&)> IsError_;
+ };
+};
+
+IChannelPtr CreateFailureDetectingChannel(
+ IChannelPtr underlyingChannel,
+ std::optional<TDuration> acknowledgementTimeout,
+ TCallback<void(const IChannelPtr&, const TError& error)> onFailure,
+ TCallback<bool(const TError&)> isError)
+{
+ return New<TFailureDetectingChannel>(
+ std::move(underlyingChannel),
+ acknowledgementTimeout,
+ std::move(onFailure),
+ std::move(isError));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTraceContextPtr GetOrCreateHandlerTraceContext(
+ const NProto::TRequestHeader& header,
+ bool forceTracing)
+{
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ const auto& ext = header.GetExtension(NProto::TRequestHeader::tracing_ext);
+ return NTracing::TTraceContext::NewChildFromRpc(
+ ext,
+ ConcatToString(TStringBuf("RpcServer:"), header.service(), TStringBuf("."), header.method()),
+ requestId,
+ forceTracing);
+}
+
+TTraceContextPtr CreateCallTraceContext(const TString& service, const TString& method)
+{
+ auto context = TryGetCurrentTraceContext();
+ if (!context) {
+ return nullptr;
+ }
+ if (!context->IsRecorded()) {
+ return context;
+ }
+ return context->CreateChild(ConcatToString(TStringBuf("RpcClient:"), service, TStringBuf("."), method));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMutationId GenerateMutationId()
+{
+ while (true) {
+ auto id = TMutationId::Create();
+ if (id != NullMutationId) {
+ return id;
+ }
+ }
+}
+
+TMutationId GenerateNextBatchMutationId(TMutationId id)
+{
+ ++id.Parts32[0];
+ return id;
+}
+
+TMutationId GenerateNextForwardedMutationId(TMutationId id)
+{
+ ++id.Parts32[1];
+ return id;
+}
+
+void GenerateMutationId(const IClientRequestPtr& request)
+{
+ SetMutationId(request, GenerateMutationId(), false);
+}
+
+TMutationId GetMutationId(const TRequestHeader& header)
+{
+ return FromProto<TMutationId>(header.mutation_id());
+}
+
+void SetMutationId(TRequestHeader* header, TMutationId id, bool retry)
+{
+ if (id) {
+ ToProto(header->mutable_mutation_id(), id);
+ if (retry) {
+ header->set_retry(true);
+ }
+ }
+}
+
+void SetMutationId(const IClientRequestPtr& request, TMutationId id, bool retry)
+{
+ SetMutationId(&request->Header(), id, retry);
+}
+
+void SetOrGenerateMutationId(const IClientRequestPtr& request, TMutationId id, bool retry)
+{
+ SetMutationId(request, id ? id : TMutationId::Create(), retry);
+}
+
+void SetAuthenticationIdentity(const IClientRequestPtr& request, const TAuthenticationIdentity& identity)
+{
+ request->SetUser(identity.User);
+ request->SetUserTag(identity.UserTag);
+}
+
+void SetCurrentAuthenticationIdentity(const IClientRequestPtr& request)
+{
+ SetAuthenticationIdentity(request, GetCurrentAuthenticationIdentity());
+}
+
+std::vector<TString> AddressesFromEndpointSet(const NServiceDiscovery::TEndpointSet& endpointSet)
+{
+ std::vector<TString> addresses;
+ addresses.reserve(endpointSet.Endpoints.size());
+ for (const auto& endpoint : endpointSet.Endpoints) {
+ addresses.push_back(NNet::BuildServiceAddress(endpoint.Fqdn, endpoint.Port));
+ }
+ return addresses;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TInput, class TFunctor>
+TFuture<std::vector<std::invoke_result_t<TFunctor, const TInput&>>> AsyncTransform(
+ TRange<TInput> input,
+ const TFunctor& unaryFunc,
+ const IInvokerPtr& invoker)
+{
+ using TOutput = std::invoke_result_t<TFunctor, const TInput&>;
+ std::vector<TFuture<TOutput>> asyncResults(input.Size());
+ std::transform(
+ input.Begin(),
+ input.End(),
+ asyncResults.begin(),
+ [&] (const TInput& value) {
+ return BIND(unaryFunc, value)
+ .AsyncVia(invoker)
+ .Run();
+ });
+
+ return AllSucceeded(asyncResults);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<std::vector<TSharedRef>> AsyncCompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId)
+{
+ if (codecId == NCompression::ECodec::None) {
+ return MakeFuture(attachments.ToVector());
+ }
+
+ auto* codec = NCompression::GetCodec(codecId);
+ return AsyncTransform(
+ attachments,
+ [=] (const TSharedRef& attachment) {
+ return codec->Compress(attachment);
+ },
+ TDispatcher::Get()->GetCompressionPoolInvoker());
+}
+
+TFuture<std::vector<TSharedRef>> AsyncDecompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId)
+{
+ if (codecId == NCompression::ECodec::None) {
+ return MakeFuture(attachments.ToVector());
+ }
+
+ auto* codec = NCompression::GetCodec(codecId);
+ return AsyncTransform(
+ attachments,
+ [=] (const TSharedRef& compressedAttachment) {
+ return codec->Decompress(compressedAttachment);
+ },
+ TDispatcher::Get()->GetCompressionPoolInvoker());
+}
+
+std::vector<TSharedRef> CompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId)
+{
+ if (codecId == NCompression::ECodec::None) {
+ return attachments.ToVector();
+ }
+ return NConcurrency::WaitFor(AsyncCompressAttachments(attachments, codecId))
+ .ValueOrThrow();
+}
+
+std::vector<TSharedRef> DecompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId)
+{
+ if (codecId == NCompression::ECodec::None) {
+ return attachments.ToVector();
+ }
+ return NConcurrency::WaitFor(AsyncDecompressAttachments(attachments, codecId))
+ .ValueOrThrow();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TError> TryEnrichClientRequestErrorWithFeatureName(
+ const TError& error,
+ TFeatureIdFormatter featureIdFormatter)
+{
+ if (error.GetCode() == NRpc::EErrorCode::UnsupportedServerFeature &&
+ error.Attributes().Contains(FeatureIdAttributeKey) &&
+ !error.Attributes().Contains(FeatureNameAttributeKey) &&
+ featureIdFormatter)
+ {
+ auto featureId = error.Attributes().Get<int>(FeatureIdAttributeKey);
+ auto featureName = (*featureIdFormatter)(featureId);
+ if (featureName) {
+ auto enrichedError = error;
+ enrichedError.MutableAttributes()->Set(FeatureNameAttributeKey, featureName);
+ return enrichedError;
+ }
+ }
+ return std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/helpers.h b/yt/yt/core/rpc/helpers.h
new file mode 100644
index 0000000000..6a759d9bbe
--- /dev/null
+++ b/yt/yt/core/rpc/helpers.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/rpc/public.h>
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/core/service_discovery/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool IsRetriableError(const TError& error);
+bool IsChannelFailureError(const TError& error);
+
+//! Returns a wrapper that sets the timeout for every request (unless it is given
+//! explicitly in the request itself).
+IChannelPtr CreateDefaultTimeoutChannel(
+ IChannelPtr underlyingChannel,
+ TDuration timeout);
+IChannelFactoryPtr CreateDefaultTimeoutChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TDuration timeout);
+
+//! Returns a wrapper that sets "authenticated_user" attribute in every request.
+IChannelPtr CreateAuthenticatedChannel(
+ IChannelPtr underlyingChannel,
+ TAuthenticationIdentity identity);
+IChannelFactoryPtr CreateAuthenticatedChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TAuthenticationIdentity identity);
+
+//! Returns a wrapper that sets realm id in every request.
+IChannelPtr CreateRealmChannel(
+ IChannelPtr underlyingChannel,
+ TRealmId realmId);
+IChannelFactoryPtr CreateRealmChannelFactory(
+ IChannelFactoryPtr underlyingFactory,
+ TRealmId realmId);
+
+//! Returns a wrapper that informs about channel failures.
+/*!
+ * Channel failures are being detected via provided filter.
+ */
+IChannelPtr CreateFailureDetectingChannel(
+ IChannelPtr underlyingChannel,
+ std::optional<TDuration> acknowledgementTimeout,
+ TCallback<void(const IChannelPtr&, const TError& error)> onFailure,
+ TCallback<bool(const TError&)> isError = BIND(IsChannelFailureError));
+
+NTracing::TTraceContextPtr GetOrCreateHandlerTraceContext(
+ const NProto::TRequestHeader& header,
+ bool forceTracing);
+NTracing::TTraceContextPtr CreateCallTraceContext(
+ const TString& service,
+ const TString& method);
+
+//! Generates a random mutation id.
+TMutationId GenerateMutationId();
+//! Enables generating a series of mutation ids within a batch.
+TMutationId GenerateNextBatchMutationId(TMutationId id);
+//! Enables generating a series of mutation ids within a forwarding chain.
+TMutationId GenerateNextForwardedMutationId(TMutationId id);
+
+void GenerateMutationId(const IClientRequestPtr& request);
+TMutationId GetMutationId(const NProto::TRequestHeader& header);
+void SetMutationId(NProto::TRequestHeader* header, TMutationId id, bool retry);
+void SetMutationId(const IClientRequestPtr& request, TMutationId id, bool retry);
+void SetOrGenerateMutationId(const IClientRequestPtr& request, TMutationId id, bool retry);
+
+void SetAuthenticationIdentity(const IClientRequestPtr& request, const TAuthenticationIdentity& identity);
+void SetCurrentAuthenticationIdentity(const IClientRequestPtr& request);
+
+template <class T>
+void WriteAuthenticationIdentityToProto(T* proto, const TAuthenticationIdentity& identity);
+template <class T>
+TAuthenticationIdentity ParseAuthenticationIdentityFromProto(const T& proto);
+
+std::vector<TString> AddressesFromEndpointSet(const NServiceDiscovery::TEndpointSet& endpointSet);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<std::vector<TSharedRef>> AsyncCompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId);
+
+TFuture<std::vector<TSharedRef>> AsyncDecompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId);
+
+std::vector<TSharedRef> CompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId);
+
+std::vector<TSharedRef> DecompressAttachments(
+ TRange<TSharedRef> attachments,
+ NCompression::ECodec codecId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+int FeatureIdToInt(E featureId);
+
+std::optional<TError> TryEnrichClientRequestErrorWithFeatureName(
+ const TError& error,
+ TFeatureIdFormatter featureIdFormatter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+#define HELPERS_INL_H_
+#include "helpers-inl.h"
+#undef HELPERS_INL_H_
diff --git a/yt/yt/core/rpc/indexed_hash_map.h b/yt/yt/core/rpc/indexed_hash_map.h
new file mode 100644
index 0000000000..ba4af0424c
--- /dev/null
+++ b/yt/yt/core/rpc/indexed_hash_map.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include <util/generic/hash.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A hash map wrapper that supports random access to some ordering of its elements.
+//! NB: Note that the order of elements in the container can change after an element is erased.
+template <class TKey, class TValue>
+class TIndexedHashMap
+{
+public:
+ using value_type = std::pair<TKey, TValue>;
+ using TUnderlyingStorage = std::vector<value_type>;
+ using iterator = typename TUnderlyingStorage::iterator;
+ using const_iterator = typename TUnderlyingStorage::const_iterator;
+
+ //! Returns true if the key has been inserted and false if it already existed and was overridden.
+ bool Set(const TKey& key, const TValue& value)
+ {
+ if (auto it = KeyToIndex_.find(key); it != KeyToIndex_.end()) {
+ Data_[it->second].second = value;
+ return false;
+ } else {
+ KeyToIndex_[key] = Data_.size();
+ Data_.push_back({key, value});
+ return true;
+ }
+ }
+
+ //! Returns true if an element was actually erased.
+ bool Erase(const TKey& key)
+ {
+ if (auto it = KeyToIndex_.find(key); it != KeyToIndex_.end()) {
+ Erase(it->second);
+ return true;
+ }
+ return false;
+ }
+
+ //! NB: Index must be in range [0, Size()).
+ void Erase(int index)
+ {
+ YT_VERIFY(0 <= index && index < Size());
+
+ int endIndex = Size() - 1;
+ if (index != endIndex) {
+ std::swap(Data_[index], Data_[endIndex]);
+ KeyToIndex_[Data_[index].first] = index;
+ }
+
+ KeyToIndex_.erase(Data_.back().first);
+ Data_.pop_back();
+ }
+
+ //! NB: Key must be present in set.
+ const TValue& Get(const TKey& key) const
+ {
+ auto it = KeyToIndex_.find(key);
+ YT_VERIFY(it != KeyToIndex_.end());
+
+ return Data_[it->second].second;
+ }
+
+ //! NB: Index must be in range [0, Size()).
+ const std::pair<TKey, TValue>& operator[](int index) const
+ {
+ YT_VERIFY(0 <= index && index < Size());
+
+ return Data_[index];
+ }
+
+ const std::pair<TKey, TValue>& GetRandomElement() const
+ {
+ YT_VERIFY(Size() > 0);
+
+ return (*this)[RandomNumber<size_t>(Size())];
+ }
+
+ int Size() const
+ {
+ return Data_.size();
+ }
+
+ void Clear()
+ {
+ Data_.clear();
+ KeyToIndex_.clear();
+ }
+
+ // Iterator support.
+
+ iterator begin()
+ {
+ return Data_.begin();
+ }
+
+ const_iterator begin() const
+ {
+ return Data_.begin();
+ }
+
+ iterator end()
+ {
+ return Data_.end();
+ }
+
+ const_iterator end() const
+ {
+ return Data_.end();
+ }
+
+ iterator find(const TKey& key)
+ {
+ auto it = KeyToIndex_.find(key);
+ if (it != KeyToIndex_.end()) {
+ return Data_.begin() + it->second;
+ }
+ return Data_.end();
+ }
+
+ const_iterator find(const TKey& key) const
+ {
+ auto it = KeyToIndex_.find(key);
+ if (it != KeyToIndex_.end()) {
+ return Data_.begin() + it->second;
+ }
+ return Data_.end();
+ }
+
+private:
+ // TODO(achulkov2): Change this to deque + hashable pointer wrapper to avoid storing the key twice.
+ THashMap<TKey, int> KeyToIndex_;
+ TUnderlyingStorage Data_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/local_channel.cpp b/yt/yt/core/rpc/local_channel.cpp
new file mode 100644
index 0000000000..bffe10d1a0
--- /dev/null
+++ b/yt/yt/core/rpc/local_channel.cpp
@@ -0,0 +1,368 @@
+#include "local_channel.h"
+#include "channel.h"
+#include "client.h"
+#include "message.h"
+#include "server.h"
+#include "service.h"
+#include "dispatcher.h"
+#include "private.h"
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <atomic>
+
+namespace NYT::NRpc {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NConcurrency;
+using namespace NBus;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TString EndpointDescription = "<local>";
+static const IAttributeDictionaryPtr EndpointAttributes =
+ ConvertToAttributes(BuildYsonStringFluently()
+ .BeginMap()
+ .Item("local").Value(true)
+ .EndMap());
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLocalChannel
+ : public IChannel
+{
+public:
+ explicit TLocalChannel(IServerPtr server)
+ : Server_(std::move(server))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription;
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes;
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ TServiceId serviceId(request->GetService(), request->GetRealmId());
+ IServicePtr service;
+ try {
+ service = Server_->GetServiceOrThrow(serviceId);
+ } catch (const TErrorException& ex) {
+ responseHandler->HandleError(TError(ex));
+ return nullptr;
+ }
+
+ auto& header = request->Header();
+ header.set_start_time(ToProto<i64>(TInstant::Now()));
+ if (options.Timeout) {
+ header.set_timeout(ToProto<i64>(*options.Timeout));
+ } else {
+ header.clear_timeout();
+ }
+
+ TSharedRefArray serializedRequest;
+ try {
+ serializedRequest = request->Serialize();
+ } catch (const std::exception& ex) {
+ responseHandler->HandleError(TError(NRpc::EErrorCode::TransportError, "Request serialization failed")
+ << *EndpointAttributes
+ << TErrorAttribute("request_id", request->GetRequestId())
+ << ex);
+ return nullptr;
+ }
+
+ auto session = New<TSession>(
+ request->GetRequestId(),
+ std::move(responseHandler),
+ options.Timeout);
+
+ YT_LOG_DEBUG("Local request sent (RequestId: %v, Method: %v.%v, Timeout: %v)",
+ request->GetRequestId(),
+ request->GetService(),
+ request->GetMethod(),
+ options.Timeout);
+
+ service->HandleRequest(
+ std::make_unique<NProto::TRequestHeader>(request->Header()),
+ std::move(serializedRequest),
+ std::move(session));
+
+ return New<TClientRequestControl>(std::move(service), request->GetRequestId());
+ }
+
+ void Terminate(const TError& error) override
+ {
+ Terminated_.Fire(error);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Subscribe(callback);
+ }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& callback) override
+ {
+ Terminated_.Unsubscribe(callback);
+ }
+
+private:
+ class TSession;
+ using TSessionPtr = TIntrusivePtr<TSession>;
+
+ const IServerPtr Server_;
+
+ TSingleShotCallbackList<void(const TError&)> Terminated_;
+
+ class TSession
+ : public IBus
+ {
+ public:
+ TSession(
+ TRequestId requestId,
+ IClientResponseHandlerPtr handler,
+ std::optional<TDuration> timeout)
+ : RequestId_(requestId)
+ , Handler_(std::move(handler))
+ {
+ if (timeout) {
+ TDelayedExecutor::Submit(
+ BIND(&TSession::OnTimeout, MakeStrong(this)),
+ *timeout);
+ }
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return EndpointDescription;
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return *EndpointAttributes;
+ }
+
+ TBusNetworkStatistics GetNetworkStatistics() const override
+ {
+ return {};
+ }
+
+ const TString& GetEndpointAddress() const override
+ {
+ static const TString EmptyAddress;
+ return EmptyAddress;
+ }
+
+ bool IsEndpointLocal() const override
+ {
+ return true;
+ }
+
+ const NNet::TNetworkAddress& GetEndpointNetworkAddress() const override
+ {
+ return NNet::NullNetworkAddress;
+ }
+
+ TFuture<void> GetReadyFuture() const override
+ {
+ return VoidFuture;
+ }
+
+ TFuture<void> Send(TSharedRefArray message, const NBus::TSendOptions& /*options*/) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto messageType = GetMessageType(message);
+ switch (messageType) {
+ case EMessageType::Response:
+ OnResponseMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingPayload:
+ OnStreamingPayloadMessage(std::move(message));
+ break;
+
+ case EMessageType::StreamingFeedback:
+ OnStreamingFeedbackMessage(std::move(message));
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ return VoidFuture;
+ }
+
+ void SetTosLevel(TTosLevel /*tosLevel*/) override
+ { }
+
+ void Terminate(const TError& /*error*/) override
+ { }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+ private:
+ const TRequestId RequestId_;
+ const IClientResponseHandlerPtr Handler_;
+
+ std::atomic<bool> Replied_ = false;
+
+ void OnResponseMessage(TSharedRefArray message)
+ {
+ NProto::TResponseHeader header;
+ YT_VERIFY(TryParseResponseHeader(message, &header));
+ if (AcquireLock()) {
+ auto error = FromProto<TError>(header.error());
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Local response received (RequestId: %v)",
+ RequestId_);
+ Handler_->HandleResponse(std::move(message), /*address*/ {});
+ } else {
+ ReportError(error);
+ }
+ }
+ }
+
+ void OnStreamingPayloadMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingPayloadHeader header;
+ YT_VERIFY(ParseStreamingPayloadHeader(message, &header));
+
+ auto sequenceNumber = header.sequence_number();
+ auto attachments = std::vector<TSharedRef>(message.Begin() + 1, message.End());
+
+ YT_VERIFY(!attachments.empty());
+
+ NCompression::ECodec codec;
+ int intCodec = header.codec();
+ YT_VERIFY(TryEnumCast(intCodec, &codec));
+
+ YT_LOG_DEBUG("Response streaming payload received (RequestId: %v, SequenceNumber: %v, Sizes: %v, "
+ "Codec: %v, Closed: %v)",
+ RequestId_,
+ sequenceNumber,
+ MakeFormattableView(attachments, [] (auto* builder, const auto& attachment) {
+ builder->AppendFormat("%v", GetStreamingAttachmentSize(attachment));
+ }),
+ codec,
+ !attachments.back());
+
+ TStreamingPayload payload{
+ codec,
+ sequenceNumber,
+ std::move(attachments)
+ };
+ Handler_->HandleStreamingPayload(payload);
+ }
+
+ void OnStreamingFeedbackMessage(TSharedRefArray message)
+ {
+ NProto::TStreamingFeedbackHeader header;
+ YT_VERIFY(ParseStreamingFeedbackHeader(message, &header));
+ auto readPosition = header.read_position();
+
+ YT_LOG_DEBUG("Response streaming feedback received (RequestId: %v, ReadPosition: %v)",
+ RequestId_,
+ readPosition);
+
+ TStreamingFeedback feedback{
+ readPosition
+ };
+ Handler_->HandleStreamingFeedback(feedback);
+ }
+
+ bool AcquireLock()
+ {
+ return !Replied_.exchange(true);
+ }
+
+ void OnTimeout(bool aborted)
+ {
+ if (!AcquireLock()) {
+ return;
+ }
+
+ ReportError(aborted
+ ? TError(NYT::EErrorCode::Canceled, "Request timed out (timer was aborted)")
+ : TError(NYT::EErrorCode::Timeout, "Request timed out"));
+ }
+
+ void ReportError(const TError& error)
+ {
+ auto detailedError = error
+ << TErrorAttribute("request_id", RequestId_)
+ << GetEndpointAttributes();
+
+ YT_LOG_DEBUG(detailedError, "Local request failed (RequestId: %v)",
+ RequestId_);
+
+ Handler_->HandleError(detailedError);
+ }
+ };
+
+ class TClientRequestControl
+ : public IClientRequestControl
+ {
+ public:
+ TClientRequestControl(IServicePtr service, TRequestId requestId)
+ : Service_(std::move(service))
+ , RequestId_(requestId)
+ { }
+
+ void Cancel() override
+ {
+ Service_->HandleRequestCancellation(RequestId_);
+ }
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& payload) override
+ {
+ Service_->HandleStreamingPayload(RequestId_, payload);
+ return VoidFuture;
+ }
+
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& feedback) override
+ {
+ Service_->HandleStreamingFeedback(RequestId_, feedback);
+ return VoidFuture;
+ }
+
+ private:
+ const IServicePtr Service_;
+ const TRequestId RequestId_;
+
+ };
+};
+
+IChannelPtr CreateLocalChannel(IServerPtr server)
+{
+ return New<TLocalChannel>(server);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/local_channel.h b/yt/yt/core/rpc/local_channel.h
new file mode 100644
index 0000000000..26363a657d
--- /dev/null
+++ b/yt/yt/core/rpc/local_channel.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a channel that routes all requests to the local RPC server.
+IChannelPtr CreateLocalChannel(IServerPtr server);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/local_server.cpp b/yt/yt/core/rpc/local_server.cpp
new file mode 100644
index 0000000000..910b4efe54
--- /dev/null
+++ b/yt/yt/core/rpc/local_server.cpp
@@ -0,0 +1,25 @@
+#include "local_server.h"
+#include "server_detail.h"
+#include "private.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLocalServer
+ : public TServerBase
+{
+public:
+ TLocalServer()
+ : TServerBase(RpcServerLogger.WithTag("LocalServerId: %v", TGuid::Create()))
+ { }
+};
+
+IServerPtr CreateLocalServer()
+{
+ return New<TLocalServer>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/local_server.h b/yt/yt/core/rpc/local_server.h
new file mode 100644
index 0000000000..2d958c0e2b
--- /dev/null
+++ b/yt/yt/core/rpc/local_server.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/public.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServerPtr CreateLocalServer();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/message.cpp b/yt/yt/core/rpc/message.cpp
new file mode 100644
index 0000000000..d76dfa7c1e
--- /dev/null
+++ b/yt/yt/core/rpc/message.cpp
@@ -0,0 +1,466 @@
+#include "message.h"
+#include "private.h"
+#include "service.h"
+#include "channel.h"
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+#pragma pack(push, 1)
+
+struct TFixedMessageHeader
+{
+ EMessageType Type;
+};
+
+#pragma pack(pop)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TSerializedMessageTag
+{ };
+
+namespace {
+
+size_t GetAllocationSpaceForProtoWithHeader(const google::protobuf::MessageLite& message)
+{
+ return
+ sizeof (TFixedMessageHeader) +
+ message.ByteSizeLong();
+}
+
+void SerializeAndAddProtoWithHeader(
+ TSharedRefArrayBuilder* builder,
+ const TFixedMessageHeader& fixedHeader,
+ const google::protobuf::MessageLite& message)
+{
+ auto ref = builder->AllocateAndAdd(
+ sizeof (TFixedMessageHeader) +
+ message.GetCachedSize());
+ ::memcpy(ref.Begin(), &fixedHeader, sizeof(fixedHeader));
+ message.SerializeWithCachedSizesToArray(reinterpret_cast<google::protobuf::uint8*>(ref.Begin() + sizeof(fixedHeader)));
+}
+
+size_t GetAllocationSpaceForProtoWithEnvelope(const google::protobuf::MessageLite& message)
+{
+ return
+ sizeof (TEnvelopeFixedHeader) +
+ message.ByteSizeLong();
+}
+
+void SerializeAndAddProtoWithEnvelope(
+ TSharedRefArrayBuilder* builder,
+ const google::protobuf::MessageLite& message)
+{
+ auto ref = builder->AllocateAndAdd(
+ sizeof (TEnvelopeFixedHeader) +
+ message.GetCachedSize());
+ auto* header = static_cast<TEnvelopeFixedHeader*>(static_cast<void*>(ref.Begin()));
+ // Empty (default) TSerializedMessageEnvelope.
+ header->EnvelopeSize = 0;
+ header->MessageSize = message.GetCachedSize();
+ message.SerializeWithCachedSizesToArray(reinterpret_cast<google::protobuf::uint8*>(ref.Begin() + sizeof(TEnvelopeFixedHeader)));
+}
+
+bool DeserializeFromProtoWithHeader(
+ google::protobuf::MessageLite* message,
+ TRef data)
+{
+ if (data.Size() < sizeof(TFixedMessageHeader)) {
+ return false;
+ }
+ return message->ParsePartialFromArray(
+ data.Begin() + sizeof(TFixedMessageHeader),
+ data.Size() - sizeof(TFixedMessageHeader));
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRefArray CreateRequestMessage(
+ const NProto::TRequestHeader& header,
+ TSharedRef body,
+ const std::vector<TSharedRef>& attachments)
+{
+ TSharedRefArrayBuilder builder(
+ 2 + attachments.size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Request},
+ header);
+ builder.Add(std::move(body));
+ for (auto attachment : attachments) {
+ builder.Add(std::move(attachment));
+ }
+ return builder.Finish();
+}
+
+TSharedRefArray CreateRequestMessage(
+ const NProto::TRequestHeader& header,
+ const TSharedRefArray& data)
+{
+ TSharedRefArrayBuilder builder(
+ 1 + data.Size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Request},
+ header);
+ for (auto part : data) {
+ builder.Add(std::move(part));
+ }
+ return builder.Finish();
+}
+
+TSharedRefArray CreateRequestCancelationMessage(
+ const NProto::TRequestCancelationHeader& header)
+{
+ TSharedRefArrayBuilder builder(
+ 1,
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::RequestCancelation},
+ header);
+ return builder.Finish();
+}
+
+TSharedRefArray CreateResponseMessage(
+ const NProto::TResponseHeader& header,
+ TSharedRef body,
+ const std::vector<TSharedRef>& attachments)
+{
+ TSharedRefArrayBuilder builder(
+ 2 + attachments.size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Response},
+ header);
+ builder.Add(std::move(body));
+ for (auto attachment : attachments) {
+ builder.Add(std::move(attachment));
+ }
+ return builder.Finish();
+}
+
+TSharedRefArray CreateResponseMessage(
+ const ::google::protobuf::MessageLite& body,
+ const std::vector<TSharedRef>& attachments)
+{
+ NProto::TResponseHeader header;
+ TSharedRefArrayBuilder builder(
+ 2 + attachments.size(),
+ GetAllocationSpaceForProtoWithHeader(header) + GetAllocationSpaceForProtoWithEnvelope(body),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Response},
+ header);
+ SerializeAndAddProtoWithEnvelope(
+ &builder,
+ body);
+ for (auto attachment : attachments) {
+ builder.Add(std::move(attachment));
+ }
+ return builder.Finish();
+}
+
+TSharedRefArray CreateErrorResponseMessage(
+ const NProto::TResponseHeader& header)
+{
+ TSharedRefArrayBuilder builder(
+ 1,
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Response},
+ header);
+ return builder.Finish();
+}
+
+TSharedRefArray CreateErrorResponseMessage(
+ TRequestId requestId,
+ const TError& error)
+{
+ NProto::TResponseHeader header;
+ ToProto(header.mutable_request_id(), requestId);
+ if (!error.IsOK()) {
+ ToProto(header.mutable_error(), error);
+ }
+ return CreateErrorResponseMessage(header);
+}
+
+TSharedRefArray CreateErrorResponseMessage(
+ const TError& error)
+{
+ NProto::TResponseHeader header;
+ if (!error.IsOK()) {
+ ToProto(header.mutable_error(), error);
+ }
+ return CreateErrorResponseMessage(header);
+}
+
+TSharedRefArray CreateStreamingPayloadMessage(
+ const NProto::TStreamingPayloadHeader& header,
+ const std::vector<TSharedRef>& attachments)
+{
+ TSharedRefArrayBuilder builder(
+ 1 + attachments.size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::StreamingPayload},
+ header);
+ for (auto attachment : attachments) {
+ builder.Add(std::move(attachment));
+ }
+ return builder.Finish();
+}
+
+TSharedRefArray CreateStreamingFeedbackMessage(
+ const NProto::TStreamingFeedbackHeader& header)
+{
+ TSharedRefArrayBuilder builder(
+ 1,
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::StreamingFeedback},
+ header);
+ return builder.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TStreamingParameters* protoParameters,
+ const TStreamingParameters& parameters)
+{
+ protoParameters->set_window_size(parameters.WindowSize);
+ if (parameters.ReadTimeout) {
+ protoParameters->set_read_timeout(ToProto<i64>(*parameters.ReadTimeout));
+ }
+ if (parameters.WriteTimeout) {
+ protoParameters->set_write_timeout(ToProto<i64>(*parameters.WriteTimeout));
+ }
+}
+
+void FromProto(
+ TStreamingParameters* parameters,
+ const NProto::TStreamingParameters& protoParameters)
+{
+ if (protoParameters.has_window_size()) {
+ parameters->WindowSize = protoParameters.window_size();
+ }
+ if (protoParameters.has_read_timeout()) {
+ parameters->ReadTimeout = FromProto<TDuration>(protoParameters.read_timeout());
+ }
+ if (protoParameters.has_write_timeout()) {
+ parameters->WriteTimeout = FromProto<TDuration>(protoParameters.write_timeout());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+EMessageType GetMessageType(const TSharedRefArray& message)
+{
+ if (message.Size() < 1) {
+ return EMessageType::Unknown;
+ }
+
+ const auto& headerPart = message[0];
+ if (headerPart.Size() < sizeof(TFixedMessageHeader)) {
+ return EMessageType::Unknown;
+ }
+
+ const auto* header = reinterpret_cast<const TFixedMessageHeader*>(headerPart.Begin());
+ return header->Type;
+}
+
+bool ParseRequestHeader(
+ const TSharedRefArray& message,
+ NProto::TRequestHeader* header)
+{
+ if (GetMessageType(message) != EMessageType::Request) {
+ return false;
+ }
+
+ return DeserializeFromProtoWithHeader(header, message[0]);
+}
+
+TSharedRefArray SetRequestHeader(
+ const TSharedRefArray& message,
+ const NProto::TRequestHeader& header)
+{
+ YT_ASSERT(GetMessageType(message) == EMessageType::Request);
+ TSharedRefArrayBuilder builder(
+ message.Size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Request},
+ header);
+ for (size_t index = 1; index < message.Size(); ++index) {
+ builder.Add(message[index]);
+ }
+ return builder.Finish();
+}
+
+bool TryParseResponseHeader(
+ const TSharedRefArray& message,
+ NProto::TResponseHeader* header)
+{
+ if (GetMessageType(message) != EMessageType::Response) {
+ return false;
+ }
+
+ return DeserializeFromProtoWithHeader(header, message[0]);
+}
+
+TSharedRefArray SetResponseHeader(
+ const TSharedRefArray& message,
+ const NProto::TResponseHeader& header)
+{
+ YT_ASSERT(GetMessageType(message) == EMessageType::Response);
+ TSharedRefArrayBuilder builder(
+ message.Size(),
+ GetAllocationSpaceForProtoWithHeader(header),
+ GetRefCountedTypeCookie<TSerializedMessageTag>());
+ SerializeAndAddProtoWithHeader(
+ &builder,
+ TFixedMessageHeader{EMessageType::Response},
+ header);
+ for (size_t index = 1; index < message.Size(); ++index) {
+ builder.Add(message[index]);
+ }
+ return builder.Finish();
+}
+
+void MergeRequestHeaderExtensions(
+ NProto::TRequestHeader* to,
+ const NProto::TRequestHeader& from)
+{
+#define XX(name) \
+ if (from.HasExtension(name)) { \
+ to->MutableExtension(name)->CopyFrom(from.GetExtension(name)); \
+ }
+
+ XX(NRpc::NProto::TRequestHeader::tracing_ext)
+
+#undef XX
+}
+
+bool ParseRequestCancelationHeader(
+ const TSharedRefArray& message,
+ NProto::TRequestCancelationHeader* header)
+{
+ if (GetMessageType(message) != EMessageType::RequestCancelation) {
+ return false;
+ }
+
+ return DeserializeFromProtoWithHeader(header, message[0]);
+}
+
+bool ParseStreamingPayloadHeader(
+ const TSharedRefArray& message,
+ NProto::TStreamingPayloadHeader * header)
+{
+ if (GetMessageType(message) != EMessageType::StreamingPayload) {
+ return false;
+ }
+
+ return DeserializeFromProtoWithHeader(header, message[0]);
+}
+
+bool ParseStreamingFeedbackHeader(
+ const TSharedRefArray& message,
+ NProto::TStreamingFeedbackHeader * header)
+{
+ if (GetMessageType(message) != EMessageType::StreamingFeedback) {
+ return false;
+ }
+
+ return DeserializeFromProtoWithHeader(header, message[0]);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+i64 GetMessageBodySize(const TSharedRefArray& message)
+{
+ return message.Size() >= 2 ? static_cast<i64>(message[1].Size()) : 0;
+}
+
+int GetMessageAttachmentCount(const TSharedRefArray& message)
+{
+ return std::max(static_cast<int>(message.Size()) - 2, 0);
+}
+
+i64 GetTotalMessageAttachmentSize(const TSharedRefArray& message)
+{
+ i64 result = 0;
+ for (int index = 2; index < std::ssize(message); ++index) {
+ result += message[index].Size();
+ }
+ return result;
+}
+
+TError CheckBusMessageLimits(const TSharedRefArray& message)
+{
+ if (message.Size() > NBus::MaxMessagePartCount) {
+ return TError(
+ NRpc::EErrorCode::TransportError,
+ "RPC message contains too many attachments: %v > %v",
+ message.Size() - 2,
+ NBus::MaxMessagePartCount - 2);
+ }
+
+ if (message.Size() < 2) {
+ return TError();
+ }
+
+ if (message[1].Size() > NBus::MaxMessagePartSize) {
+ return TError(
+ NRpc::EErrorCode::TransportError,
+ "RPC message body is too large: %v > %v",
+ message[1].Size(),
+ NBus::MaxMessagePartSize);
+ }
+
+ for (size_t index = 2; index < message.Size(); ++index) {
+ if (message[index].Size() > NBus::MaxMessagePartSize) {
+ return TError(
+ NRpc::EErrorCode::TransportError,
+ "RPC message attachment %v is too large: %v > %v",
+ index - 2,
+ message[index].Size(),
+ NBus::MaxMessagePartSize);
+ }
+ }
+
+ return TError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/message.h b/yt/yt/core/rpc/message.h
new file mode 100644
index 0000000000..1b573dc576
--- /dev/null
+++ b/yt/yt/core/rpc/message.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EMessageType, ui32,
+ ((Unknown) ( 0))
+ ((Request) (0x69637072)) // rpci
+ ((RequestCancelation) (0x63637072)) // rpcc
+ ((Response) (0x6f637072)) // rpco
+ ((StreamingPayload) (0x70637072)) // rpcp
+ ((StreamingFeedback) (0x66637072)) // rpcf
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRefArray CreateRequestMessage(
+ const NProto::TRequestHeader& header,
+ TSharedRef body,
+ const std::vector<TSharedRef>& attachments);
+
+TSharedRefArray CreateRequestMessage(
+ const NProto::TRequestHeader& header,
+ const TSharedRefArray& data);
+
+TSharedRefArray CreateRequestCancelationMessage(
+ const NProto::TRequestCancelationHeader& header);
+
+TSharedRefArray CreateResponseMessage(
+ const NProto::TResponseHeader& header,
+ TSharedRef body,
+ const std::vector<TSharedRef>& attachments);
+
+TSharedRefArray CreateResponseMessage(
+ const ::google::protobuf::MessageLite& body,
+ const std::vector<TSharedRef>& attachments = std::vector<TSharedRef>());
+
+TSharedRefArray CreateErrorResponseMessage(
+ const NProto::TResponseHeader& header);
+
+TSharedRefArray CreateErrorResponseMessage(
+ TRequestId requestId,
+ const TError& error);
+
+TSharedRefArray CreateErrorResponseMessage(
+ const TError& error);
+
+TSharedRefArray CreateStreamingPayloadMessage(
+ const NProto::TStreamingPayloadHeader& header,
+ const std::vector<TSharedRef>& attachments);
+
+TSharedRefArray CreateStreamingFeedbackMessage(
+ const NProto::TStreamingFeedbackHeader& header);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(
+ NProto::TStreamingParameters* protoParameters,
+ const TStreamingParameters& parameters);
+
+void FromProto(
+ TStreamingParameters* parameters,
+ const NProto::TStreamingParameters& protoParameters);
+
+////////////////////////////////////////////////////////////////////////////////
+
+EMessageType GetMessageType(const TSharedRefArray& message);
+
+bool ParseRequestHeader(
+ const TSharedRefArray& message,
+ NProto::TRequestHeader* header);
+
+TSharedRefArray SetRequestHeader(
+ const TSharedRefArray& message,
+ const NProto::TRequestHeader& header);
+
+bool TryParseResponseHeader(
+ const TSharedRefArray& message,
+ NProto::TResponseHeader* header);
+
+TSharedRefArray SetResponseHeader(
+ const TSharedRefArray& message,
+ const NProto::TResponseHeader& header);
+
+void MergeRequestHeaderExtensions(
+ NProto::TRequestHeader* to,
+ const NProto::TRequestHeader& from);
+
+bool ParseRequestCancelationHeader(
+ const TSharedRefArray& message,
+ NProto::TRequestCancelationHeader* header);
+
+bool ParseStreamingPayloadHeader(
+ const TSharedRefArray& message,
+ NProto::TStreamingPayloadHeader * header);
+
+bool ParseStreamingFeedbackHeader(
+ const TSharedRefArray& message,
+ NProto::TStreamingFeedbackHeader* header);
+
+i64 GetMessageBodySize(const TSharedRefArray& message);
+int GetMessageAttachmentCount(const TSharedRefArray& message);
+i64 GetTotalMessageAttachmentSize(const TSharedRefArray& message);
+
+TError CheckBusMessageLimits(const TSharedRefArray& message);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/message_format.cpp b/yt/yt/core/rpc/message_format.cpp
new file mode 100644
index 0000000000..01e1dd4d27
--- /dev/null
+++ b/yt/yt/core/rpc/message_format.cpp
@@ -0,0 +1,167 @@
+#include "message_format.h"
+
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/json/json_parser.h>
+#include <yt/yt/core/json/json_writer.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace NYT::NRpc {
+
+using namespace NYson;
+using namespace NJson;
+using namespace NRpc::NProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IMessageFormat
+{
+ virtual ~IMessageFormat() = default;
+
+ virtual TSharedRef ConvertFrom(
+ const TSharedRef& message,
+ const NYson::TProtobufMessageType* messageType,
+ const TYsonString& formatOptionsYson) = 0;
+ virtual TSharedRef ConvertTo(
+ const TSharedRef& message,
+ const NYson::TProtobufMessageType* messageType,
+ const TYsonString& formatOptionsYson) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+THashMap<EMessageFormat, IMessageFormat*>& GetMessageFormatRegistry()
+{
+ static THashMap<EMessageFormat, IMessageFormat*> Registry;
+ return Registry;
+}
+
+IMessageFormat* GetMessageFormatOrThrow(EMessageFormat format)
+{
+ const auto& registry = GetMessageFormatRegistry();
+ auto it = registry.find(format);
+ if (it == registry.end()) {
+ THROW_ERROR_EXCEPTION("Unsupported message format %Qlv",
+ format);
+ }
+ return it->second;
+}
+
+void RegisterCustomMessageFormat(EMessageFormat format, IMessageFormat* formatHandler)
+{
+ YT_VERIFY(!GetMessageFormatRegistry()[format]);
+ GetMessageFormatRegistry()[format] = formatHandler;
+}
+
+namespace {
+
+class TYsonMessageFormat
+ : public IMessageFormat
+{
+public:
+ TYsonMessageFormat()
+ {
+ RegisterCustomMessageFormat(EMessageFormat::Yson, this);
+ }
+
+ TSharedRef ConvertFrom(const TSharedRef& message, const NYson::TProtobufMessageType* messageType, const TYsonString& /*formatOptionsYson*/) override
+ {
+ auto ysonBuffer = PopEnvelope(message);
+ TString protoBuffer;
+ {
+ google::protobuf::io::StringOutputStream output(&protoBuffer);
+ auto converter = CreateProtobufWriter(&output, messageType);
+ // NB: formatOptionsYson is ignored, since YSON parser has no user-defined options.
+ ParseYsonStringBuffer(TStringBuf(ysonBuffer.Begin(), ysonBuffer.End()), EYsonType::Node, converter.get());
+ }
+ return PushEnvelope(TSharedRef::FromString(protoBuffer));
+ }
+
+ TSharedRef ConvertTo(const TSharedRef& message, const NYson::TProtobufMessageType* messageType, const TYsonString& /*formatOptionsYson*/) override
+ {
+ auto protoBuffer = PopEnvelope(message);
+ google::protobuf::io::ArrayInputStream stream(protoBuffer.Begin(), protoBuffer.Size());
+ TString ysonBuffer;
+ {
+ TStringOutput output(ysonBuffer);
+ // TODO(ignat): refactor TYsonFormatConfig, move it closer to YSON.
+ TYsonWriter writer{&output, EYsonFormat::Text};
+ ParseProtobuf(&writer, &stream, messageType);
+ }
+ return PushEnvelope(TSharedRef::FromString(ysonBuffer));
+ }
+} YsonFormat;
+
+class TJsonMessageFormat
+ : public IMessageFormat
+{
+public:
+ TJsonMessageFormat()
+ {
+ RegisterCustomMessageFormat(EMessageFormat::Json, this);
+ }
+
+ TSharedRef ConvertFrom(const TSharedRef& message, const NYson::TProtobufMessageType* messageType, const TYsonString& formatOptionsYson) override
+ {
+ auto jsonBuffer = PopEnvelope(message);
+ TString protoBuffer;
+ {
+ google::protobuf::io::StringOutputStream output(&protoBuffer);
+ auto converter = CreateProtobufWriter(&output, messageType);
+ TMemoryInput input{jsonBuffer.Begin(), jsonBuffer.Size()};
+ auto formatConfig = New<TJsonFormatConfig>();
+ if (formatOptionsYson) {
+ formatConfig->Load(NYTree::ConvertToNode(formatOptionsYson));
+ }
+ ParseJson(&input, converter.get(), formatConfig);
+ }
+ return PushEnvelope(TSharedRef::FromString(protoBuffer));
+ }
+
+ TSharedRef ConvertTo(const TSharedRef& message, const NYson::TProtobufMessageType* messageType, const TYsonString& formatOptionsYson) override
+ {
+ auto protoBuffer = PopEnvelope(message);
+ google::protobuf::io::ArrayInputStream stream(protoBuffer.Begin(), protoBuffer.Size());
+ TString ysonBuffer;
+ {
+ TStringOutput output(ysonBuffer);
+ auto formatConfig = New<TJsonFormatConfig>();
+ if (formatOptionsYson) {
+ formatConfig->Load(NYTree::ConvertToNode(formatOptionsYson));
+ }
+ auto writer = CreateJsonConsumer(&output, EYsonType::Node, formatConfig);
+ ParseProtobuf(writer.get(), &stream, messageType);
+ writer->Flush();
+ }
+ return PushEnvelope(TSharedRef::FromString(ysonBuffer));
+ }
+} JsonFormat;
+
+} // namespace
+
+TSharedRef ConvertMessageToFormat(
+ const TSharedRef& message,
+ EMessageFormat format,
+ const TProtobufMessageType* messageType,
+ const TYsonString& formatOptionsYson)
+{
+ return GetMessageFormatOrThrow(format)->ConvertTo(message, messageType, formatOptionsYson);
+}
+
+TSharedRef ConvertMessageFromFormat(
+ const TSharedRef& message,
+ EMessageFormat format,
+ const TProtobufMessageType* messageType,
+ const TYsonString& formatOptionsYson)
+{
+ return GetMessageFormatOrThrow(format)->ConvertFrom(message, messageType, formatOptionsYson);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/message_format.h b/yt/yt/core/rpc/message_format.h
new file mode 100644
index 0000000000..3bf8c5fc5f
--- /dev/null
+++ b/yt/yt/core/rpc/message_format.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef ConvertMessageToFormat(
+ const TSharedRef& message,
+ EMessageFormat format,
+ const NYson::TProtobufMessageType* messageType,
+ const NYson::TYsonString& formatOptionsYson);
+
+TSharedRef ConvertMessageFromFormat(
+ const TSharedRef& message,
+ EMessageFormat format,
+ const NYson::TProtobufMessageType* messageType,
+ const NYson::TYsonString& formatOptionsYson);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/null_channel.cpp b/yt/yt/core/rpc/null_channel.cpp
new file mode 100644
index 0000000000..73b12055dd
--- /dev/null
+++ b/yt/yt/core/rpc/null_channel.cpp
@@ -0,0 +1,74 @@
+#include "null_channel.h"
+
+#include "channel.h"
+
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+namespace NYT::NRpc {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullChannel
+ : public NRpc::IChannel
+{
+public:
+ explicit TNullChannel(TString address)
+ : Address_(std::move(address))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return Address_;
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return EmptyAttributes();
+ }
+
+ NRpc::IClientRequestControlPtr Send(
+ NRpc::IClientRequestPtr /*request*/,
+ NRpc::IClientResponseHandlerPtr /*handler*/,
+ const NRpc::TSendOptions& /*options*/) override
+ {
+ return nullptr;
+ }
+
+ void Terminate(const TError& /*error*/) override
+ { }
+
+ DEFINE_SIGNAL_OVERRIDE(void(const TError&), Terminated);
+
+private:
+ const TString Address_;
+};
+
+IChannelPtr CreateNullChannel(TString address)
+{
+ return New<TNullChannel>(std::move(address));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNullChannelFactory
+ : public IChannelFactory
+{
+public:
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ return CreateNullChannel(address);
+ }
+};
+
+IChannelFactoryPtr GetNullChannelFactory()
+{
+ return LeakyRefCountedSingleton<TNullChannelFactory>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/null_channel.h b/yt/yt/core/rpc/null_channel.h
new file mode 100644
index 0000000000..c3db18830c
--- /dev/null
+++ b/yt/yt/core/rpc/null_channel.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IChannelPtr CreateNullChannel(TString address);
+IChannelFactoryPtr GetNullChannelFactory();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/per_user_request_queue_provider.cpp b/yt/yt/core/rpc/per_user_request_queue_provider.cpp
new file mode 100644
index 0000000000..47841f29f6
--- /dev/null
+++ b/yt/yt/core/rpc/per_user_request_queue_provider.cpp
@@ -0,0 +1,128 @@
+#include "per_user_request_queue_provider.h"
+
+#include "service_detail.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto InfiniteRequestThrottlerConfig = New<NConcurrency::TThroughputThrottlerConfig>();
+
+////////////////////////////////////////////////////////////////////////////////
+
+TPerUserRequestQueueProvider::TPerUserRequestQueueProvider(
+ TReconfigurationCallback reconfigurationCallback,
+ NProfiling::TProfiler throttlerProfiler)
+ : DefaultConfigs_(TRequestQueueThrottlerConfigs{
+ InfiniteRequestThrottlerConfig,
+ InfiniteRequestThrottlerConfig
+ })
+ , ReconfigurationCallback_(std::move(reconfigurationCallback))
+ , ThrottlerProfiler_(std::move(throttlerProfiler))
+{
+ DoGetQueue(RootUserName);
+}
+
+TRequestQueue* TPerUserRequestQueueProvider::GetQueue(const NProto::TRequestHeader& header)
+{
+ const auto& userName = header.has_user() ? header.user() : RootUserName;
+ return DoGetQueue(userName);
+}
+
+TRequestQueue* TPerUserRequestQueueProvider::DoGetQueue(const TString& userName)
+{
+ auto queue = RequestQueues_.FindOrInsert(userName, [&] {
+ auto queue = CreateRequestQueue(userName, ThrottlerProfiler_);
+
+ // It doesn't matter if configs are outdated since we will reconfigure them
+ // again in the automaton thread.
+ auto configs = DefaultConfigs_.Load();
+
+ auto [weightThrottlingEnabled, bytesThrottlingEnabled] =
+ ReadThrottlingEnabledFlags();
+
+ queue->ConfigureWeightThrottler(weightThrottlingEnabled
+ ? configs.WeightThrottlerConfig
+ : nullptr);
+ queue->ConfigureBytesThrottler(bytesThrottlingEnabled
+ ? configs.BytesThrottlerConfig
+ : nullptr);
+
+ // NB: not calling ReconfigurationCallback_ here because, for newly
+ // created queues, ConfigureQueue is supposed to be called shortly.
+
+ return queue;
+ }).first;
+
+ return queue->Get();
+}
+
+void TPerUserRequestQueueProvider::ConfigureQueue(
+ TRequestQueue* queue,
+ const TMethodConfigPtr& config)
+{
+ TRequestQueueProviderBase::ConfigureQueue(queue, config);
+
+ if (ReconfigurationCallback_) {
+ ReconfigurationCallback_(queue->GetName(), queue);
+ }
+}
+
+std::pair<bool, bool> TPerUserRequestQueueProvider::ReadThrottlingEnabledFlags()
+{
+ std::pair<bool, bool> result;
+
+ auto guard = ReaderGuard(ThrottlingEnabledFlagsSpinLock_);
+ result.first = WeightThrottlingEnabled_;
+ result.second = BytesThrottlingEnabled_;
+
+ return result;
+}
+
+void TPerUserRequestQueueProvider::UpdateThrottlingEnabledFlags(
+ bool enableWeightThrottling,
+ bool enableBytesThrottling)
+{
+ auto guard = WriterGuard(ThrottlingEnabledFlagsSpinLock_);
+ WeightThrottlingEnabled_ = enableWeightThrottling;
+ BytesThrottlingEnabled_ = enableBytesThrottling;
+}
+
+void TPerUserRequestQueueProvider::UpdateDefaultConfigs(
+ const TRequestQueueThrottlerConfigs& configs)
+{
+ YT_ASSERT(configs.WeightThrottlerConfig && configs.BytesThrottlerConfig);
+ DefaultConfigs_.Store(configs);
+}
+
+void TPerUserRequestQueueProvider::ReconfigureAllUsers()
+{
+ if (!ReconfigurationCallback_) {
+ return;
+ }
+
+ RequestQueues_.Flush();
+ RequestQueues_.IterateReadOnly([&] (const auto& userName, const auto& queue) {
+ ReconfigurationCallback_(userName, queue);
+ });
+}
+
+void TPerUserRequestQueueProvider::ReconfigureUser(const TString& userName)
+{
+ if (!ReconfigurationCallback_) {
+ return;
+ }
+
+ // Extra layer of defense from throttling root and spelling doom.
+ if (userName == RootUserName) {
+ return;
+ }
+
+ if (auto* queue = RequestQueues_.Find(userName)) {
+ ReconfigurationCallback_(userName, *queue);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/per_user_request_queue_provider.h b/yt/yt/core/rpc/per_user_request_queue_provider.h
new file mode 100644
index 0000000000..6ee4eaa356
--- /dev/null
+++ b/yt/yt/core/rpc/per_user_request_queue_provider.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "public.h"
+
+#include "request_queue_provider.h"
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/syncmap/map.h>
+
+#include <yt/yt/core/misc/atomic_object.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPerUserRequestQueueProvider
+ : public TRequestQueueProviderBase
+{
+public:
+ using TReconfigurationCallback = std::function<void(TString, TRequestQueuePtr)>;
+
+ TPerUserRequestQueueProvider(
+ TReconfigurationCallback reconfigurationCallback = {},
+ NProfiling::TProfiler throttlerProfiler = {});
+
+ // IRequestQueueProvider implementation.
+ TRequestQueue* GetQueue(const NProto::TRequestHeader& header) override;
+ void ConfigureQueue(TRequestQueue* queue, const TMethodConfigPtr& config) override;
+
+ void ReconfigureUser(const TString& userName);
+ void ReconfigureAllUsers();
+
+ void UpdateThrottlingEnabledFlags(bool enableWeightThrottling, bool enableBytesThrottling);
+ void UpdateDefaultConfigs(const TRequestQueueThrottlerConfigs& configs);
+
+private:
+ TRequestQueue* DoGetQueue(const TString& userName);
+ std::pair<bool, bool> ReadThrottlingEnabledFlags();
+
+ NConcurrency::TSyncMap<TString, TRequestQueuePtr> RequestQueues_;
+
+ TAtomicObject<TRequestQueueThrottlerConfigs> DefaultConfigs_;
+
+ TReconfigurationCallback ReconfigurationCallback_;
+
+ NThreading::TReaderWriterSpinLock ThrottlingEnabledFlagsSpinLock_;
+ bool WeightThrottlingEnabled_ = false;
+ bool BytesThrottlingEnabled_ = true;
+
+ NProfiling::TProfiler ThrottlerProfiler_;
+};
+
+DECLARE_REFCOUNTED_CLASS(TPerUserRequestQueueProvider)
+DEFINE_REFCOUNTED_TYPE(TPerUserRequestQueueProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/private.h b/yt/yt/core/rpc/private.h
new file mode 100644
index 0000000000..849f744cbf
--- /dev/null
+++ b/yt/yt/core/rpc/private.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger RpcServerLogger("RpcServer");
+inline const NLogging::TLogger RpcClientLogger("RpcClient");
+
+inline const NProfiling::TProfiler RpcServerProfiler("/rpc/server");
+inline const NProfiling::TProfiler RpcClientProfiler("/rpc/client");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/protocol_version.cpp b/yt/yt/core/rpc/protocol_version.cpp
new file mode 100644
index 0000000000..179555f67d
--- /dev/null
+++ b/yt/yt/core/rpc/protocol_version.cpp
@@ -0,0 +1,59 @@
+#include "protocol_version.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtocolVersion TProtocolVersion::FromString(TStringBuf protocolVersionString)
+{
+ TStringBuf majorStr, minorStr;
+ if (!protocolVersionString.TrySplit('.', majorStr, minorStr)) {
+ THROW_ERROR_EXCEPTION("Failed to parse protocol version string; \"major.minor\" string expected");
+ }
+
+ TProtocolVersion result;
+
+ if (!TryFromString<int>(majorStr, result.Major)) {
+ THROW_ERROR_EXCEPTION("Failed to parse major protocol version");
+ }
+
+ if (!TryFromString<int>(minorStr, result.Minor)) {
+ THROW_ERROR_EXCEPTION("Failed to parse minor protocol version");
+ }
+
+ if (result == GenericProtocolVersion) {
+ return result;
+ }
+
+ if (result.Major < 0 || result.Minor < 0) {
+ THROW_ERROR_EXCEPTION("Incorrect protocol version; major and minor versions should be "
+ "greater than or equal to zero")
+ << TErrorAttribute("protocol_version", ToString(result));
+ }
+
+ return result;
+}
+
+bool operator == (const TProtocolVersion& lhs, const TProtocolVersion& rhs)
+{
+ return (lhs.Major == rhs.Major) && (lhs.Minor == rhs.Minor);
+}
+
+bool operator != (const TProtocolVersion& lhs, const TProtocolVersion& rhs)
+{
+ return !(lhs == rhs);
+}
+
+void FormatValue(TStringBuilderBase* builder, TProtocolVersion version, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v.%v", version.Major, version.Minor);
+}
+
+TString ToString(TProtocolVersion version)
+{
+ return ToStringViaBuilder(version);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/protocol_version.h b/yt/yt/core/rpc/protocol_version.h
new file mode 100644
index 0000000000..160d17047f
--- /dev/null
+++ b/yt/yt/core/rpc/protocol_version.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "private.h"
+
+#include <util/generic/string.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtocolVersion
+{
+ int Major;
+ int Minor;
+
+ static TProtocolVersion FromString(TStringBuf protocolVersionString);
+};
+
+bool operator == (const TProtocolVersion& lhs, const TProtocolVersion& rhs);
+bool operator != (const TProtocolVersion& lhs, const TProtocolVersion& rhs);
+
+void FormatValue(TStringBuilderBase* builder, TProtocolVersion version, TStringBuf spec);
+TString ToString(TProtocolVersion protocolVersion);
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr TProtocolVersion GenericProtocolVersion{-1, -1};
+constexpr TProtocolVersion DefaultProtocolVersion{0, 0};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/public.cpp b/yt/yt/core/rpc/public.cpp
new file mode 100644
index 0000000000..ab082251c3
--- /dev/null
+++ b/yt/yt/core/rpc/public.cpp
@@ -0,0 +1,24 @@
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TRequestId NullRequestId;
+const TRealmId NullRealmId;
+const TMutationId NullMutationId;
+
+const TString RootUserName("root");
+
+const TString RequestIdAnnotation("rpc.request_id");
+const TString EndpointAnnotation("rpc.endpoint");
+const TString RequestInfoAnnotation("rpc.request_info");
+const TString RequestUser("rpc.request_user");
+const TString ResponseInfoAnnotation("rpc.response_info");
+
+const TString FeatureIdAttributeKey("feature_id");
+const TString FeatureNameAttributeKey("feature_name");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/public.h b/yt/yt/core/rpc/public.h
new file mode 100644
index 0000000000..a080c11744
--- /dev/null
+++ b/yt/yt/core/rpc/public.h
@@ -0,0 +1,189 @@
+#pragma once
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/core/bus/public.h>
+
+#include <library/cpp/yt/misc/guid.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TReqDiscover;
+class TRspDiscover;
+class TRequestHeader;
+class TResponseHeader;
+class TCredentialsExt;
+
+} // namespace NProto
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TStreamingParameters;
+struct TStreamingPayload;
+struct TStreamingFeedback;
+
+struct TServiceDescriptor;
+struct TMethodDescriptor;
+
+DECLARE_REFCOUNTED_CLASS(TRequestQueue)
+DECLARE_REFCOUNTED_STRUCT(IRequestQueueProvider)
+
+using TInvokerProvider = TCallback<IInvokerPtr(const NRpc::NProto::TRequestHeader&)>;
+
+DECLARE_REFCOUNTED_CLASS(TClientRequest)
+DECLARE_REFCOUNTED_CLASS(TClientResponse)
+
+template <class TRequestMessage, class TResponse>
+class TTypedClientRequest;
+
+class TClientResponse;
+
+template <class TResponseMessage>
+class TTypedClientResponse;
+
+struct TServiceId;
+
+struct TAuthenticationContext;
+struct TAuthenticationIdentity;
+struct TAuthenticationResult;
+
+DECLARE_REFCOUNTED_STRUCT(IClientRequest)
+DECLARE_REFCOUNTED_STRUCT(IClientRequestControl)
+DECLARE_REFCOUNTED_STRUCT(IClientResponseHandler)
+DECLARE_REFCOUNTED_STRUCT(IServer)
+DECLARE_REFCOUNTED_STRUCT(IService)
+DECLARE_REFCOUNTED_STRUCT(IServiceWithReflection)
+DECLARE_REFCOUNTED_STRUCT(IServiceContext)
+DECLARE_REFCOUNTED_STRUCT(IChannel)
+DECLARE_REFCOUNTED_STRUCT(IThrottlingChannel)
+DECLARE_REFCOUNTED_STRUCT(IChannelFactory)
+DECLARE_REFCOUNTED_STRUCT(IRoamingChannelProvider)
+DECLARE_REFCOUNTED_STRUCT(IAuthenticator)
+DECLARE_REFCOUNTED_STRUCT(IResponseKeeper)
+
+DECLARE_REFCOUNTED_CLASS(TClientContext)
+DECLARE_REFCOUNTED_CLASS(TServiceBase)
+DECLARE_REFCOUNTED_CLASS(TChannelWrapper)
+DECLARE_REFCOUNTED_CLASS(TStaticChannelFactory)
+DECLARE_REFCOUNTED_CLASS(TClientRequestControlThunk)
+DECLARE_REFCOUNTED_CLASS(TCachingChannelFactory)
+
+DECLARE_REFCOUNTED_CLASS(TAttachmentsInputStream)
+DECLARE_REFCOUNTED_CLASS(TAttachmentsOutputStream)
+
+DECLARE_REFCOUNTED_STRUCT(IViablePeerRegistry)
+DECLARE_REFCOUNTED_CLASS(TDynamicChannelPool)
+
+template <
+ class TServiceContext,
+ class TServiceContextWrapper,
+ class TRequestMessage,
+ class TResponseMessage
+>
+class TGenericTypedServiceContext;
+
+class TServiceContextWrapper;
+
+template <class TRequestMessage, class TResponseMessage>
+using TTypedServiceContext = TGenericTypedServiceContext<
+ IServiceContext,
+ TServiceContextWrapper,
+ TRequestMessage,
+ TResponseMessage
+>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(THistogramExponentialBounds)
+DECLARE_REFCOUNTED_CLASS(THistogramConfig)
+DECLARE_REFCOUNTED_CLASS(TServerConfig)
+DECLARE_REFCOUNTED_CLASS(TServiceCommonConfig)
+DECLARE_REFCOUNTED_CLASS(TServiceConfig)
+DECLARE_REFCOUNTED_CLASS(TMethodConfig)
+DECLARE_REFCOUNTED_CLASS(TRetryingChannelConfig)
+DECLARE_REFCOUNTED_CLASS(TViablePeerRegistryConfig)
+DECLARE_REFCOUNTED_CLASS(TDynamicChannelPoolConfig)
+DECLARE_REFCOUNTED_CLASS(TServiceDiscoveryEndpointsConfig)
+DECLARE_REFCOUNTED_CLASS(TBalancingChannelConfig)
+DECLARE_REFCOUNTED_CLASS(TThrottlingChannelConfig)
+DECLARE_REFCOUNTED_CLASS(TThrottlingChannelDynamicConfig)
+DECLARE_REFCOUNTED_CLASS(TResponseKeeperConfig)
+DECLARE_REFCOUNTED_CLASS(TDispatcherConfig)
+DECLARE_REFCOUNTED_CLASS(TDispatcherDynamicConfig)
+
+struct TRequestQueueThrottlerConfigs
+{
+ NConcurrency::TThroughputThrottlerConfigPtr WeightThrottlerConfig;
+ NConcurrency::TThroughputThrottlerConfigPtr BytesThrottlerConfig;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+using NBus::EMultiplexingBand;
+
+using TRequestId = TGuid;
+extern const TRequestId NullRequestId;
+
+using TRealmId = TGuid;
+extern const TRealmId NullRealmId;
+
+using TMutationId = TGuid;
+extern const TMutationId NullMutationId;
+
+extern const TString RootUserName;
+
+constexpr int TypicalMessagePartCount = 8;
+
+using TFeatureIdFormatter = const std::function<std::optional<TStringBuf>(int featureId)>*;
+
+using TDiscoverRequestHook = TCallback<void(NProto::TReqDiscover*)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const TString RequestIdAnnotation;
+extern const TString EndpointAnnotation;
+extern const TString RequestInfoAnnotation;
+extern const TString RequestUser;
+extern const TString ResponseInfoAnnotation;
+
+extern const TString FeatureIdAttributeKey;
+extern const TString FeatureNameAttributeKey;
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((TransportError) (static_cast<int>(NBus::EErrorCode::TransportError)))
+ ((ProtocolError) (101))
+ ((NoSuchService) (102))
+ ((NoSuchMethod) (103))
+ ((Unavailable) (105)) // The server is not capable of serving requests and
+ // must not receive any more load.
+ ((TransientFailure) (116)) // Similar to Unavailable but indicates a transient issue,
+ // which can be safely retried.
+ ((PoisonPill) (106)) // The client must die upon receiving this error.
+ ((RequestQueueSizeLimitExceeded)(108))
+ ((AuthenticationError) (109))
+ ((InvalidCsrfToken) (110))
+ ((InvalidCredentials) (111))
+ ((StreamingNotSupported) (112))
+ ((UnsupportedClientFeature) (113))
+ ((UnsupportedServerFeature) (114))
+ ((PeerBanned) (115)) // The server is explicitly banned and thus must be dropped.
+ ((NoSuchRealm) (117))
+);
+
+DEFINE_ENUM(EMessageFormat,
+ ((Protobuf) (0))
+ ((Json) (1))
+ ((Yson) (2))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/request_queue_provider.cpp b/yt/yt/core/rpc/request_queue_provider.cpp
new file mode 100644
index 0000000000..07290519f0
--- /dev/null
+++ b/yt/yt/core/rpc/request_queue_provider.cpp
@@ -0,0 +1,20 @@
+#include "request_queue_provider.h"
+
+#include "service_detail.h"
+
+namespace NYT::NRpc {
+
+///////////////////////////////////////////////////////////////////////////////
+
+void TRequestQueueProviderBase::ConfigureQueue(
+ TRequestQueue* queue,
+ const TMethodConfigPtr& config)
+{
+ if (config) {
+ queue->Configure(config);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/request_queue_provider.h b/yt/yt/core/rpc/request_queue_provider.h
new file mode 100644
index 0000000000..9f2b8baaf7
--- /dev/null
+++ b/yt/yt/core/rpc/request_queue_provider.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+///////////////////////////////////////////////////////////////////////////////
+
+struct IRequestQueueProvider
+ : public TRefCounted
+{
+ //! Finds or creates a queue for a particular request. May return null, in
+ //! which case the default queue is supposed to be used.
+ virtual TRequestQueue* GetQueue(const NRpc::NProto::TRequestHeader& header) = 0;
+
+ //! Configures a queue.
+ /*!
+ * Called once - shorty after the queue has been returned by #GetQueue for
+ * the first time.
+ * \param config The configuration to apply to the queue. May be null. The
+ * provider is free to override any and all aspects of the
+ * config provided and/or apply additional configuration.
+ */
+ virtual void ConfigureQueue(TRequestQueue* queue, const TMethodConfigPtr& config) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRequestQueueProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRequestQueueProviderBase
+ : public IRequestQueueProvider
+{
+public:
+ void ConfigureQueue(TRequestQueue* queue, const TMethodConfigPtr& config) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/response_keeper.cpp b/yt/yt/core/rpc/response_keeper.cpp
new file mode 100644
index 0000000000..1f62ef041d
--- /dev/null
+++ b/yt/yt/core/rpc/response_keeper.cpp
@@ -0,0 +1,390 @@
+#include "response_keeper.h"
+#include "private.h"
+#include "config.h"
+#include "helpers.h"
+#include "service.h"
+
+#include <atomic>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+using namespace NThreading;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr auto EvictionPeriod = TDuration::Seconds(1);
+static constexpr auto EvictionTickTimeCheckPeriod = 1024;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TResponseKeeper
+ : public IResponseKeeper
+{
+public:
+ TResponseKeeper(
+ TResponseKeeperConfigPtr config,
+ IInvokerPtr invoker,
+ const NLogging::TLogger& logger,
+ const NProfiling::TProfiler& profiler)
+ : Config_(std::move(config))
+ , Invoker_(std::move(invoker))
+ , Logger(logger)
+ {
+ YT_VERIFY(Config_);
+ YT_VERIFY(Invoker_);
+
+ EvictionExecutor_ = New<TPeriodicExecutor>(
+ Invoker_,
+ BIND(&TResponseKeeper::OnEvict, MakeWeak(this)),
+ EvictionPeriod);
+ EvictionExecutor_->Start();
+
+ profiler.AddFuncGauge("/response_keeper/kept_response_count", MakeStrong(this), [this] {
+ return FinishedResponseCount_;
+ });
+ profiler.AddFuncGauge("/response_keeper/kept_response_space", MakeStrong(this), [this] {
+ return FinishedResponseSpace_;
+ });
+ }
+
+ void Start() override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ if (Started_) {
+ return;
+ }
+
+ WarmupDeadline_ = Config_->EnableWarmup
+ ? NProfiling::GetCpuInstant() + NProfiling::DurationToCpuDuration(Config_->WarmupTime)
+ : 0;
+ Started_ = true;
+
+ YT_LOG_INFO("Response keeper started (WarmupTime: %v, ExpirationTime: %v)",
+ Config_->WarmupTime,
+ Config_->ExpirationTime);
+ }
+
+ void Stop() override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ if (!Started_) {
+ return;
+ }
+
+ PendingResponses_.clear();
+ FinishedResponses_.clear();
+ ResponseEvictionQueue_.clear();
+ FinishedResponseSpace_ = 0;
+ FinishedResponseCount_ = 0;
+ Started_ = false;
+
+ YT_LOG_INFO("Response keeper stopped");
+ }
+
+ TFuture<TSharedRefArray> TryBeginRequest(TMutationId id, bool isRetry) override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ return DoTryBeginRequest(id, isRetry);
+ }
+
+ TFuture<TSharedRefArray> FindRequest(TMutationId id, bool isRetry) const override
+ {
+ auto guard = ReaderGuard(Lock_);
+
+ return DoFindRequest(id, isRetry);
+ }
+
+ void EndRequest(TMutationId id, TSharedRefArray response, bool remember) override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ YT_ASSERT(id);
+
+ if (!Started_) {
+ return;
+ }
+
+ if (!response) {
+ YT_LOG_ALERT("Null response passed to response keeper (MutationId: %v, Remember: %v)",
+ id,
+ remember);
+ }
+
+
+ TPromise<TSharedRefArray> promise;
+ if (auto pendingIt = PendingResponses_.find(id)) {
+ promise = std::move(pendingIt->second);
+ PendingResponses_.erase(pendingIt);
+ }
+
+ if (remember) {
+ // NB: Allow duplicates.
+ auto [it, inserted] = FinishedResponses_.emplace(id, response);
+ if (inserted) {
+ auto space = static_cast<i64>(GetByteSize(response));
+ ResponseEvictionQueue_.push(TEvictionItem{
+ id,
+ NProfiling::GetCpuInstant(),
+ space,
+ it
+ });
+
+ FinishedResponseCount_ += 1;
+ FinishedResponseSpace_ += space;
+ }
+ }
+
+ if (promise) {
+ guard.Release();
+ promise.TrySet(response);
+ }
+ }
+
+ void EndRequest(TMutationId id, TErrorOr<TSharedRefArray> responseOrError, bool remember) override
+ {
+ YT_ASSERT(id);
+
+ if (responseOrError.IsOK()) {
+ EndRequest(id, std::move(responseOrError.Value()), remember);
+ return;
+ }
+
+ auto guard = WriterGuard(Lock_);
+
+ if (!Started_) {
+ return;
+ }
+
+ auto it = PendingResponses_.find(id);
+ if (it == PendingResponses_.end()) {
+ return;
+ }
+
+ auto promise = std::move(it->second);
+ PendingResponses_.erase(it);
+
+ guard.Release();
+
+ promise.TrySet(TError(std::move(responseOrError)));
+ }
+
+ void CancelPendingRequests(const TError& error) override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ if (!Started_) {
+ return;
+ }
+
+ auto pendingResponses = std::move(PendingResponses_);
+
+ guard.Release();
+
+ for (const auto& [id, promise] : pendingResponses) {
+ promise.TrySet(error);
+ }
+
+ YT_LOG_INFO(error, "All pending requests canceled");
+ }
+
+ bool TryReplyFrom(const IServiceContextPtr& context, bool subscribeToResponse) override
+ {
+ auto guard = WriterGuard(Lock_);
+
+ auto mutationId = context->GetMutationId();
+ if (!mutationId) {
+ return false;
+ }
+
+ if (auto keptAsyncResponseMessage = DoTryBeginRequest(mutationId, context->IsRetry())) {
+ context->SuppressMissingRequestInfoCheck();
+ context->ReplyFrom(std::move(keptAsyncResponseMessage));
+ return true;
+ }
+
+ if (subscribeToResponse) {
+ context->GetAsyncResponseMessage()
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TErrorOr<TSharedRefArray>& responseMessageOrError) {
+ if (!responseMessageOrError.IsOK()) {
+ EndRequest(
+ mutationId,
+ CreateErrorResponseMessage(responseMessageOrError),
+ /*remember*/ false);
+ return;
+ }
+
+ const auto& responseMessage = responseMessageOrError.Value();
+
+ NProto::TResponseHeader header;
+ YT_VERIFY(TryParseResponseHeader(responseMessage, &header));
+ bool remember = FromProto<NRpc::EErrorCode>(header.error().code()) != NRpc::EErrorCode::Unavailable;
+
+ EndRequest(
+ mutationId,
+ responseMessage,
+ remember);
+ }).Via(Invoker_));
+ }
+
+ return false;
+ }
+
+ bool IsWarmingUp() const override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return NProfiling::GetCpuInstant() < WarmupDeadline_;
+ }
+
+private:
+ const TResponseKeeperConfigPtr Config_;
+ const IInvokerPtr Invoker_;
+ const NLogging::TLogger Logger;
+
+ TPeriodicExecutorPtr EvictionExecutor_;
+
+ YT_DECLARE_SPIN_LOCK(TReaderWriterSpinLock, Lock_);
+
+ bool Started_ = false;
+ NProfiling::TCpuInstant WarmupDeadline_ = 0;
+
+ using TFinishedResponseMap = THashMap<TMutationId, TSharedRefArray>;
+ TFinishedResponseMap FinishedResponses_;
+
+ int FinishedResponseCount_ = 0;
+ i64 FinishedResponseSpace_ = 0;
+
+ struct TEvictionItem
+ {
+ TMutationId Id;
+ NProfiling::TCpuInstant When;
+ i64 Space;
+ TFinishedResponseMap::iterator Iterator;
+ };
+
+ TRingQueue<TEvictionItem> ResponseEvictionQueue_;
+
+ THashMap<TMutationId, TPromise<TSharedRefArray>> PendingResponses_;
+
+ TFuture<TSharedRefArray> DoTryBeginRequest(TMutationId id, bool isRetry)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ auto result = DoFindRequest(id, isRetry);
+ if (!result) {
+ EmplaceOrCrash(PendingResponses_, std::make_pair(id, NewPromise<TSharedRefArray>()));
+ }
+ return result;
+ }
+
+ TFuture<TSharedRefArray> DoFindRequest(TMutationId id, bool isRetry) const
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+ YT_ASSERT(id);
+
+ if (!Started_) {
+ THROW_ERROR_EXCEPTION("Response keeper is not active");
+ }
+
+ auto pendingIt = PendingResponses_.find(id);
+ if (pendingIt != PendingResponses_.end()) {
+ if (!isRetry) {
+ THROW_ERROR_EXCEPTION("Duplicate request is not marked as \"retry\"")
+ << TErrorAttribute("mutation_id", id);
+ }
+ YT_LOG_DEBUG("Replying with pending response (MutationId: %v)", id);
+ return pendingIt->second;
+ }
+
+ auto finishedIt = FinishedResponses_.find(id);
+ if (finishedIt != FinishedResponses_.end()) {
+ if (!isRetry) {
+ THROW_ERROR_EXCEPTION("Duplicate request is not marked as \"retry\"")
+ << TErrorAttribute("mutation_id", id);
+ }
+ YT_LOG_DEBUG("Replying with finished response (MutationId: %v)", id);
+ return MakeFuture(finishedIt->second);
+ }
+
+ if (isRetry && IsWarmingUp()) {
+ THROW_ERROR_EXCEPTION("Cannot reliably check for a duplicate mutating request")
+ << TErrorAttribute("mutation_id", id)
+ << TErrorAttribute("warmup_time", Config_->WarmupTime);
+ }
+
+ return {};
+ }
+
+ void OnEvict()
+ {
+ auto guard = WriterGuard(Lock_);
+
+ if (!Started_) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Response Keeper eviction tick started");
+
+ NProfiling::TWallTimer timer;
+ int counter = 0;
+
+ auto deadline = NProfiling::GetCpuInstant() - NProfiling::DurationToCpuDuration(Config_->ExpirationTime);
+ while (!ResponseEvictionQueue_.empty()) {
+ const auto& item = ResponseEvictionQueue_.front();
+ if (item.When > deadline) {
+ break;
+ }
+
+ if (++counter % EvictionTickTimeCheckPeriod == 0) {
+ if (timer.GetElapsedTime() > Config_->MaxEvictionTickTime) {
+ YT_LOG_DEBUG("Response Keeper eviction tick interrupted (ResponseCount: %v)",
+ counter);
+ return;
+ }
+ }
+
+ FinishedResponses_.erase(item.Iterator);
+
+ FinishedResponseCount_ -= 1;
+ FinishedResponseSpace_ -= item.Space;
+
+ ResponseEvictionQueue_.pop();
+ }
+
+ YT_LOG_DEBUG("Response Keeper eviction tick completed (ResponseCount: %v)",
+ counter);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IResponseKeeperPtr CreateResponseKeeper(
+ TResponseKeeperConfigPtr config,
+ IInvokerPtr invoker,
+ const NLogging::TLogger& logger,
+ const NProfiling::TProfiler& profiler)
+{
+ return New<TResponseKeeper>(
+ config,
+ invoker,
+ logger,
+ profiler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/response_keeper.h b/yt/yt/core/rpc/response_keeper.h
new file mode 100644
index 0000000000..ce75c8808b
--- /dev/null
+++ b/yt/yt/core/rpc/response_keeper.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/logging/public.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Helps to remember previously served requests thus enabling client-side retries
+//! even for non-idempotent actions.
+/*!
+ * Clients assign a unique (random) mutation id to every retry session.
+ * Servers ignore requests whose mutation ids are already known.
+ *
+ * After a sufficiently long period of time, a remembered response gets evicted.
+ *
+ * The keeper is initially inactive.
+ *
+ * \note
+ * Thread affinity: single-threaded (unless noted otherwise)
+ */
+struct IResponseKeeper
+ : public TRefCounted
+{
+public:
+ virtual void Start() = 0;
+
+ //! Deactivates the keeper.
+ /*!
+ * If the keeper is already stopped, the call does nothing.
+ * Calling #TryBeginRequest for an inactive keeper will lead to an exception.
+ */
+ virtual void Stop() = 0;
+
+ //! Called upon receiving a request with a given mutation #id.
+ /*!
+ * Either returns a valid future for the response (which can either be unset
+ * if the request is still being served or set if it is already completed) or
+ * a null future if #id is not known. In the latter case subsequent
+ * calls to #TryBeginRequest will be returning the same future over and
+ * over again.
+ *
+ * The call throws if the keeper is not active or if #isRetry is |true| and
+ * the keeper is warming up.
+ */
+ virtual TFuture<TSharedRefArray> TryBeginRequest(TMutationId id, bool isRetry) = 0;
+
+ //! Same as above but does not change the state of response keeper.
+ //! That is, if a null future is returned, no pending future has been created for it.
+ virtual TFuture<TSharedRefArray> FindRequest(TMutationId id, bool isRetry) const = 0;
+
+ //! Called when a request with a given mutation #id is finished and a #response is ready.
+ /*
+ * The latter #response is pushed to every subscriber waiting for the future
+ * previously returned by #TryBeginRequest. Additionally, if #remember is true,
+ * #response is remembered and returned by future calls to #TryBeginRequest.
+ */
+ virtual void EndRequest(TMutationId id, TSharedRefArray response, bool remember = true) = 0;
+
+ //! Similar to its non-error counterpart but also accepts errors.
+ //! Note that these are never remembered and are just propagated to the listeners.
+ virtual void EndRequest(TMutationId id, TErrorOr<TSharedRefArray> responseOrError, bool remember = true) = 0;
+
+ //! Forgets all pending requests, which were previously registered via #TryBeginRequest.
+ virtual void CancelPendingRequests(const TError& error) = 0;
+
+ //! Combines #TryBeginRequest and #EndBeginRequest.
+ /*!
+ * If |true| is returned then the request (given by #context) has mutation id assigned and
+ * a previously-remembered response is known. In this case #TryReplyFrom replies #context;
+ * no further server-side processing is needed.
+ *
+ * If |false| is returned then either the request has no mutation id assigned or
+ * this id hasn't been seen before. In both cases the server must proceed with serving the request.
+ * Also, if #subscribeToResponse is set and the request has mutation id assigned the response will
+ * be automatically remembered when #context is replied.
+ */
+ virtual bool TryReplyFrom(
+ const IServiceContextPtr& context,
+ bool subscribeToResponse = true) = 0;
+
+ //! Returns |true| if the keeper is still warming up.
+ /*!
+ * \note
+ * Thread affinity: any
+ */
+ virtual bool IsWarmingUp() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IResponseKeeper)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IResponseKeeperPtr CreateResponseKeeper(
+ TResponseKeeperConfigPtr config,
+ IInvokerPtr invoker,
+ const NLogging::TLogger& logger,
+ const NProfiling::TProfiler& profiler);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/retrying_channel.cpp b/yt/yt/core/rpc/retrying_channel.cpp
new file mode 100644
index 0000000000..b8481f2acb
--- /dev/null
+++ b/yt/yt/core/rpc/retrying_channel.cpp
@@ -0,0 +1,358 @@
+#include "retrying_channel.h"
+#include "private.h"
+#include "channel_detail.h"
+#include "client.h"
+#include "config.h"
+#include "dispatcher.h"
+
+#include <yt/yt/core/bus/client.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <util/system/guard.h>
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = RpcClientLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRetryingChannel
+ : public TChannelWrapper
+{
+public:
+ TRetryingChannel(
+ TRetryingChannelConfigPtr config,
+ IChannelPtr underlyingChannel,
+ TRetryChecker retryChecker)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , Config_(std::move(config))
+ , RetryChecker_(std::move(retryChecker))
+ {
+ YT_VERIFY(Config_);
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ YT_ASSERT(request);
+ YT_ASSERT(responseHandler);
+
+ if (request->IsStreamingEnabled()) {
+ return UnderlyingChannel_->Send(
+ request,
+ responseHandler,
+ options);
+ } else {
+ return New<TRetryingRequest>(
+ Config_,
+ UnderlyingChannel_,
+ std::move(request),
+ std::move(responseHandler),
+ options,
+ RetryChecker_)
+ ->Send();
+ }
+ }
+
+
+private:
+ const TRetryingChannelConfigPtr Config_;
+ const TCallback<bool(const TError&)> RetryChecker_;
+
+
+ class TRetryingRequest
+ : public IClientResponseHandler
+ {
+ public:
+ TRetryingRequest(
+ TRetryingChannelConfigPtr config,
+ IChannelPtr underlyingChannel,
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options,
+ TCallback<bool(const TError&)> retryChecker)
+ : Config_(std::move(config))
+ , UnderlyingChannel_(std::move(underlyingChannel))
+ , Request_(std::move(request))
+ , ResponseHandler_(std::move(responseHandler))
+ , Options_(options)
+ , RetryChecker_(std::move(retryChecker))
+ {
+ YT_ASSERT(Config_);
+ YT_ASSERT(UnderlyingChannel_);
+ YT_ASSERT(Request_);
+ YT_ASSERT(ResponseHandler_);
+
+ Deadline_ = Config_->RetryTimeout
+ ? TInstant::Now() + *Config_->RetryTimeout
+ : TInstant::Max();
+ }
+
+ IClientRequestControlPtr Send()
+ {
+ DoSend();
+ return RequestControlThunk_;
+ }
+
+ private:
+ class TRetryingRequestControlThunk
+ : public IClientRequestControl
+ {
+ public:
+ // NB: In contrast to TClientRequestControlThunk::SetUnderlying,
+ // this one may be invoked multiple times.
+ void SetNewUnderlying(IClientRequestControlPtr newUnderlying)
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ if (!newUnderlying) {
+ return;
+ }
+
+ TCompactVector<IClientRequestControlPtr, 2> toCancelList;
+
+ auto guard = Guard(SpinLock_);
+
+ if (Underlying_) {
+ toCancelList.push_back(std::move(Underlying_));
+ }
+
+ if (Canceled_.load()) {
+ toCancelList.push_back(std::move(newUnderlying));
+ } else {
+ Underlying_ = std::move(newUnderlying);
+ }
+
+ guard.Release();
+
+ for (const auto& toCancel : toCancelList) {
+ toCancel->Cancel();
+ }
+ }
+
+ void Cancel() override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(SpinLock_);
+ Canceled_.store(true);
+ auto toCancel = std::move(Underlying_);
+ guard.Release();
+
+ if (toCancel) {
+ toCancel->Cancel();
+ }
+ }
+
+ bool IsCanceled() const
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return Canceled_.load();
+ }
+
+ TFuture<void> SendStreamingPayload(const TStreamingPayload& /*payload*/) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return MakeFuture<void>(TError("Retrying channel does not support streaming"));
+ }
+
+ TFuture<void> SendStreamingFeedback(const TStreamingFeedback& /*feedback*/) override
+ {
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return MakeFuture<void>(TError("Retrying channel does not support streaming"));
+ }
+
+ private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::atomic<bool> Canceled_ = false;
+ IClientRequestControlPtr Underlying_;
+
+ };
+
+ using TRetryingRequestControlThunkPtr = TIntrusivePtr<TRetryingRequestControlThunk>;
+
+ const TRetryingChannelConfigPtr Config_;
+ const IChannelPtr UnderlyingChannel_;
+ const IClientRequestPtr Request_;
+ const IClientResponseHandlerPtr ResponseHandler_;
+ const TSendOptions Options_;
+ const TCallback<bool(const TError&)> RetryChecker_;
+ const TRetryingRequestControlThunkPtr RequestControlThunk_ = New<TRetryingRequestControlThunk>();
+
+ //! The current attempt number (1-based).
+ int CurrentAttempt_ = 1;
+ TInstant Deadline_;
+
+ std::optional<TError> FirstError_;
+ std::optional<TError> LastError_;
+ int OmittedInnerErrorCount_ = 0;
+
+ // IClientResponseHandler implementation.
+
+ void HandleAcknowledgement() override
+ {
+ YT_LOG_DEBUG("Request attempt acknowledged (RequestId: %v)",
+ Request_->GetRequestId());
+
+ // NB: The underlying handler is not notified.
+ }
+
+ void HandleError(const TError& error) override
+ {
+ YT_LOG_DEBUG(error, "Request attempt failed (RequestId: %v, Attempt: %v of %v)",
+ Request_->GetRequestId(),
+ CurrentAttempt_,
+ Config_->RetryAttempts);
+
+ if (!RetryChecker_.Run(error)) {
+ ResponseHandler_->HandleError(error);
+ return;
+ }
+
+ if (!FirstError_) {
+ FirstError_ = error;
+ } else {
+ if (LastError_) {
+ ++OmittedInnerErrorCount_;
+ }
+ LastError_ = error;
+ }
+
+ Retry();
+ }
+
+ void HandleResponse(TSharedRefArray message, TString address) override
+ {
+ YT_LOG_DEBUG("Request attempt succeeded (RequestId: %v)",
+ Request_->GetRequestId());
+
+ ResponseHandler_->HandleResponse(std::move(message), std::move(address));
+ }
+
+ void HandleStreamingPayload(const TStreamingPayload& /*payload*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ void HandleStreamingFeedback(const TStreamingFeedback& /*feedback*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+
+ std::optional<TDuration> ComputeAttemptTimeout(TInstant now)
+ {
+ auto attemptDeadline = Options_.Timeout ? now + *Options_.Timeout : TInstant::Max();
+ auto actualDeadline = std::min(Deadline_, attemptDeadline);
+ return actualDeadline == TInstant::Max()
+ ? std::optional<TDuration>(std::nullopt)
+ : actualDeadline - now;
+ }
+
+ void ReportError(const TError& error)
+ {
+ auto detailedError = error
+ << UnderlyingChannel_->GetEndpointAttributes()
+ << TErrorAttribute("omitted_inner_error_count", OmittedInnerErrorCount_);
+ if (FirstError_) {
+ detailedError = detailedError << *FirstError_;
+ }
+ if (LastError_) {
+ detailedError = detailedError << *LastError_;
+ }
+ ResponseHandler_->HandleError(detailedError);
+ }
+
+ void Retry()
+ {
+ int count = ++CurrentAttempt_;
+ if (count > Config_->RetryAttempts || TInstant::Now() + Config_->RetryBackoffTime > Deadline_) {
+ ReportError(TError(NRpc::EErrorCode::Unavailable, "Request retries failed"));
+ return;
+ }
+
+ TDelayedExecutor::Submit(
+ BIND(&TRetryingRequest::DoRetry, MakeStrong(this)),
+ Config_->RetryBackoffTime,
+ TDispatcher::Get()->GetHeavyInvoker());
+ }
+
+ void DoRetry(bool aborted)
+ {
+ if (aborted) {
+ ReportError(TError(NYT::EErrorCode::Canceled, "Request timed out (timer was aborted)"));
+ return;
+ }
+
+ if (RequestControlThunk_->IsCanceled()) {
+ ResponseHandler_->HandleError(TError(NYT::EErrorCode::Canceled, "Request canceled"));
+ return;
+ }
+
+ DoSend();
+ }
+
+ void DoSend()
+ {
+ YT_LOG_DEBUG("Request attempt started (RequestId: %v, Method: %v.%v, %v%vAttempt: %v of %v, RequestTimeout: %v, RetryTimeout: %v)",
+ Request_->GetRequestId(),
+ Request_->GetService(),
+ Request_->GetMethod(),
+ MakeFormatterWrapper([&] (auto* builder) {
+ if (Request_->GetUser()) {
+ builder->AppendFormat("User: %v, ", Request_->GetUser());
+ }
+ }),
+ MakeFormatterWrapper([&] (auto* builder) {
+ if (Request_->GetUserTag() && Request_->GetUserTag() != Request_->GetUser()) {
+ builder->AppendFormat("UserTag: %v, ", Request_->GetUserTag());
+ }
+ }),
+ CurrentAttempt_,
+ Config_->RetryAttempts,
+ Options_.Timeout,
+ Config_->RetryTimeout);
+
+ auto now = TInstant::Now();
+ if (now > Deadline_) {
+ ReportError(TError(NYT::EErrorCode::Timeout, "Request retries timed out"));
+ return;
+ }
+
+ auto adjustedOptions = Options_;
+ adjustedOptions.Timeout = ComputeAttemptTimeout(now);
+ auto requestControl = UnderlyingChannel_->Send(
+ Request_,
+ this,
+ adjustedOptions);
+ RequestControlThunk_->SetNewUnderlying(std::move(requestControl));
+ }
+ };
+};
+
+IChannelPtr CreateRetryingChannel(
+ TRetryingChannelConfigPtr config,
+ IChannelPtr underlyingChannel,
+ TCallback<bool(const TError&)> retryChecker)
+{
+ static auto DefaultRetryChecker = BIND(&IsRetriableError);
+ return New<TRetryingChannel>(
+ std::move(config),
+ std::move(underlyingChannel),
+ retryChecker ? retryChecker : DefaultRetryChecker);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/retrying_channel.h b/yt/yt/core/rpc/retrying_channel.h
new file mode 100644
index 0000000000..d8d25fb922
--- /dev/null
+++ b/yt/yt/core/rpc/retrying_channel.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TRetryChecker = TCallback<bool(const TError&)>;
+
+//! Constructs a channel that implements a simple retry policy.
+/*!
+ * The channel determines if the request must be retried by calling
+ * #retryChecker (which is #NRpc::IsRetriableError by default).
+ *
+ * The channel makes at most #TRetryingChannelConfig::RetryAttempts
+ * attempts totally spending at most #TRetryingChannelConfig::RetryTimeout time
+ * (if given).
+ *
+ * A delay of #TRetryingChannelConfig::RetryBackoffTime is inserted
+ * between any pair of consequent attempts.
+ */
+IChannelPtr CreateRetryingChannel(
+ TRetryingChannelConfigPtr config,
+ IChannelPtr underlyingChannel,
+ TRetryChecker retryChecker = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/roaming_channel.cpp b/yt/yt/core/rpc/roaming_channel.cpp
new file mode 100644
index 0000000000..ef3c0f9297
--- /dev/null
+++ b/yt/yt/core/rpc/roaming_channel.cpp
@@ -0,0 +1,194 @@
+#include "roaming_channel.h"
+#include "channel_detail.h"
+#include "client.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRoamingRequestControl
+ : public TClientRequestControlThunk
+{
+public:
+ TRoamingRequestControl(
+ TFuture<IChannelPtr> asyncChannel,
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options)
+ : Request_(std::move(request))
+ , ResponseHandler_(std::move(responseHandler))
+ , Options_(options)
+ , StartTime_(TInstant::Now())
+ {
+ if (Options_.Timeout) {
+ asyncChannel = asyncChannel.WithTimeout(*Options_.Timeout);
+ }
+
+ asyncChannel.Subscribe(BIND(&TRoamingRequestControl::OnGotChannel, MakeStrong(this)));
+ }
+
+ void Cancel() override
+ {
+ if (!TryAcquireSemaphore()) {
+ TClientRequestControlThunk::Cancel();
+ return;
+ }
+
+ auto error = TError(NYT::EErrorCode::Canceled, "RPC request canceled")
+ << TErrorAttribute("request_id", Request_->GetRequestId())
+ << TErrorAttribute("realm_id", Request_->GetRealmId())
+ << TErrorAttribute("service", Request_->GetService())
+ << TErrorAttribute("method", Request_->GetMethod());
+ ResponseHandler_->HandleError(error);
+
+ Request_.Reset();
+ ResponseHandler_.Reset();
+ }
+
+private:
+ IClientRequestPtr Request_;
+ IClientResponseHandlerPtr ResponseHandler_;
+ const TSendOptions Options_;
+ const TInstant StartTime_;
+
+ std::atomic<bool> Semaphore_ = false;
+
+
+ bool TryAcquireSemaphore()
+ {
+ bool expected = false;
+ return Semaphore_.compare_exchange_strong(expected, true);
+ }
+
+ void OnGotChannel(const TErrorOr<IChannelPtr>& result)
+ {
+ if (!TryAcquireSemaphore()) {
+ return;
+ }
+
+ auto request = std::move(Request_);
+ auto responseHandler = std::move(ResponseHandler_);
+
+ if (!result.IsOK()) {
+ responseHandler->HandleError(result);
+ return;
+ }
+
+ auto adjustedOptions = Options_;
+ if (Options_.Timeout) {
+ auto now = TInstant::Now();
+ auto deadline = StartTime_ + *Options_.Timeout;
+ adjustedOptions.Timeout = now > deadline ? TDuration::Zero() : deadline - now;
+ }
+
+ const auto& channel = result.Value();
+ auto requestControl = channel->Send(
+ request,
+ responseHandler,
+ adjustedOptions);
+
+ SetUnderlying(std::move(requestControl));
+ }
+};
+
+class TSyncRoamingRequestControl
+ : public TClientRequestControlThunk
+{
+public:
+ TSyncRoamingRequestControl(
+ IClientRequestControlPtr requestControl,
+ IChannelPtr channel)
+ : Channel_(std::move(channel))
+ {
+ SetUnderlying(std::move(requestControl));
+ }
+
+private:
+ const IChannelPtr Channel_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRoamingChannel
+ : public IChannel
+{
+public:
+ explicit TRoamingChannel(IRoamingChannelProviderPtr provider)
+ : Provider_(std::move(provider))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return Provider_->GetEndpointDescription();
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return Provider_->GetEndpointAttributes();
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ YT_ASSERT(request);
+ YT_ASSERT(responseHandler);
+
+ auto asyncChannel = Provider_->GetChannel(request);
+
+ // NB: Optimize for the typical case of sync channel acquisition.
+ if (auto channelOrError = asyncChannel.TryGet()) {
+ if (channelOrError->IsOK()) {
+ const auto& channel = channelOrError->Value();
+ return New<TSyncRoamingRequestControl>(
+ channel->Send(
+ std::move(request),
+ std::move(responseHandler),
+ options),
+ channel);
+ } else {
+ responseHandler->HandleError(*channelOrError);
+ return New<TClientRequestControlThunk>();
+ }
+ }
+
+ return New<TRoamingRequestControl>(
+ std::move(asyncChannel),
+ std::move(request),
+ std::move(responseHandler),
+ options);
+ }
+
+ void Terminate(const TError& error) override
+ {
+ Provider_->Terminate(error);
+ }
+
+ void SubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+ void UnsubscribeTerminated(const TCallback<void(const TError&)>& /*callback*/) override
+ { }
+
+private:
+ const IRoamingChannelProviderPtr Provider_;
+
+};
+
+IChannelPtr CreateRoamingChannel(IRoamingChannelProviderPtr provider)
+{
+ YT_VERIFY(provider);
+
+ return New<TRoamingChannel>(std::move(provider));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/roaming_channel.h b/yt/yt/core/rpc/roaming_channel.h
new file mode 100644
index 0000000000..6b12cd2a35
--- /dev/null
+++ b/yt/yt/core/rpc/roaming_channel.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IRoamingChannelProvider
+ : public virtual TRefCounted
+{
+ //! Cf. IChannel::GetEndpointDescription.
+ virtual const TString& GetEndpointDescription() const = 0;
+
+ //! Cf. IChannel::GetEndpointAttributes.
+ virtual const NYTree::IAttributeDictionary& GetEndpointAttributes() const = 0;
+
+ //! Returns the actual channel to use for sending to service with
+ //! a given #serviceName from request and other request properties.
+ virtual TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& request) = 0;
+
+ //! Returns the actual channel to use for sending to service with
+ //! a given #serviceName.
+ virtual TFuture<IChannelPtr> GetChannel(const TString& serviceName) = 0;
+
+ //! Returns a channel to use.
+ virtual TFuture<IChannelPtr> GetChannel() = 0;
+
+ //! Terminates the cached channels, if any.
+ virtual void Terminate(const TError& error) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IRoamingChannelProvider)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a channel with a dynamically discovered endpoint.
+/*!
+ * The IRoamingChannelProvider::GetChannel is invoked to discover the actual endpoint.
+ * The channels are not reused between requests so it's provider's
+ * responsibility to provide the appropriate caching.
+ */
+IChannelPtr CreateRoamingChannel(IRoamingChannelProviderPtr provider);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/serialized_channel.cpp b/yt/yt/core/rpc/serialized_channel.cpp
new file mode 100644
index 0000000000..19ddedb1d8
--- /dev/null
+++ b/yt/yt/core/rpc/serialized_channel.cpp
@@ -0,0 +1,160 @@
+#include "serialized_channel.h"
+#include "channel_detail.h"
+#include "client.h"
+
+#include <queue>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSerializedChannel;
+typedef TIntrusivePtr<TSerializedChannel> TSerializedChannelPtr;
+
+class TSerializedChannel
+ : public TChannelWrapper
+{
+public:
+ explicit TSerializedChannel(IChannelPtr underlyingChannel)
+ : TChannelWrapper(std::move(underlyingChannel))
+ { }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ auto entry = New<TEntry>(
+ request,
+ responseHandler,
+ options);
+
+ {
+ auto guard = Guard(SpinLock_);
+ Queue_.push(entry);
+ }
+
+ TrySendQueuedRequests();
+
+ return entry->RequestControlThunk;
+ }
+
+ void Terminate(const TError& /*error*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnRequestCompleted()
+ {
+ {
+ auto guard = Guard(SpinLock_);
+ YT_VERIFY(RequestInProgress_);
+ RequestInProgress_ = false;
+ }
+
+ TrySendQueuedRequests();
+ }
+
+private:
+ class TResponseHandler
+ : public IClientResponseHandler
+ {
+ public:
+ TResponseHandler(
+ IClientResponseHandlerPtr underlyingHandler,
+ TSerializedChannelPtr owner)
+ : UnderlyingHandler_(std::move(underlyingHandler))
+ , Owner_(std::move(owner))
+ { }
+
+ void HandleAcknowledgement() override
+ {
+ UnderlyingHandler_->HandleAcknowledgement();
+ }
+
+ void HandleResponse(TSharedRefArray message, TString address) override
+ {
+ UnderlyingHandler_->HandleResponse(std::move(message), std::move(address));
+ Owner_->OnRequestCompleted();
+ }
+
+ void HandleError(const TError& error) override
+ {
+ UnderlyingHandler_->HandleError(error);
+ Owner_->OnRequestCompleted();
+ }
+
+ void HandleStreamingPayload(const TStreamingPayload& payload) override
+ {
+ UnderlyingHandler_->HandleStreamingPayload(payload);
+ }
+
+ void HandleStreamingFeedback(const TStreamingFeedback& feedback) override
+ {
+ UnderlyingHandler_->HandleStreamingFeedback(feedback);
+ }
+
+ private:
+ const IClientResponseHandlerPtr UnderlyingHandler_;
+ const TSerializedChannelPtr Owner_;
+
+ };
+
+ struct TEntry
+ : public TRefCounted
+ {
+ TEntry(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr handler,
+ const TSendOptions& options)
+ : Request(std::move(request))
+ , Handler(std::move(handler))
+ , Options(options)
+ { }
+
+ IClientRequestPtr Request;
+ IClientResponseHandlerPtr Handler;
+ TSendOptions Options;
+ TClientRequestControlThunkPtr RequestControlThunk = New<TClientRequestControlThunk>();
+ };
+
+ typedef TIntrusivePtr<TEntry> TEntryPtr;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ std::queue<TEntryPtr> Queue_;
+ bool RequestInProgress_ = false;
+
+
+ void TrySendQueuedRequests()
+ {
+ auto guard = Guard(SpinLock_);
+ while (!RequestInProgress_ && !Queue_.empty()) {
+ auto entry = Queue_.front();
+ Queue_.pop();
+ RequestInProgress_ = true;
+ guard.Release();
+
+ auto serializedHandler = New<TResponseHandler>(entry->Handler, this);
+ auto requestControl = UnderlyingChannel_->Send(
+ entry->Request,
+ serializedHandler,
+ entry->Options);
+ entry->RequestControlThunk->SetUnderlying(std::move(requestControl));
+ entry->Request.Reset();
+ entry->Handler.Reset();
+ entry->RequestControlThunk.Reset();
+ }
+ }
+
+};
+
+IChannelPtr CreateSerializedChannel(IChannelPtr underlyingChannel)
+{
+ YT_VERIFY(underlyingChannel);
+
+ return New<TSerializedChannel>(std::move(underlyingChannel));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/serialized_channel.h b/yt/yt/core/rpc/serialized_channel.h
new file mode 100644
index 0000000000..55c8a06438
--- /dev/null
+++ b/yt/yt/core/rpc/serialized_channel.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates a serialized channel that wraps #underlyingChannel.
+/*!
+ * The serialized channel forwards all requests to #underlyingChannel
+ * but only starts a new request once the previous one is completed.
+ */
+IChannelPtr CreateSerializedChannel(IChannelPtr underlyingChannel);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/server.h b/yt/yt/core/rpc/server.h
new file mode 100644
index 0000000000..fe4c88a478
--- /dev/null
+++ b/yt/yt/core/rpc/server.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/bus/public.h>
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents a bunch of RPC services listening at a single endpoint.
+/*!
+ * Each service is keyed by TServiceId returned by #IService::GetServiceId.
+ */
+struct IServer
+ : public virtual TRefCounted
+{
+ //! Adds a new #service.
+ virtual void RegisterService(IServicePtr service) = 0;
+
+ //! Removes a previously registered #service.
+ //! Returns |true| if #service was indeed registered, |false| otherwise.
+ virtual bool UnregisterService(IServicePtr service) = 0;
+
+ //! Finds a service by id, returns null if none is found.
+ virtual IServicePtr FindService(const TServiceId& serviceId) const = 0;
+
+ //! Finds a service by id, throws an appropriate error if none is found.
+ virtual IServicePtr GetServiceOrThrow(const TServiceId& serviceId) const = 0;
+
+ //! Reconfigures the server on-the-fly.
+ virtual void Configure(TServerConfigPtr config) = 0;
+
+ //! Starts the server.
+ /*!
+ * All requests coming to the server before it is started are immediately rejected.
+ */
+ virtual void Start() = 0;
+
+ //! Stops the server.
+ /*!
+ * If #graceful is |true|, asynchronously waits for all services to stop
+ * (cf. #IService::Stop).
+ *
+ * For servers bound to certain transport layer, this call also ensures
+ * that the transport is fully stopped all well.
+ *
+ * \returns an asynchronous flag indicating that the server is fully stopped.
+ */
+ virtual TFuture<void> Stop(bool graceful = true) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IServer)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/server_detail.cpp b/yt/yt/core/rpc/server_detail.cpp
new file mode 100644
index 0000000000..e160257fdf
--- /dev/null
+++ b/yt/yt/core/rpc/server_detail.cpp
@@ -0,0 +1,929 @@
+#include "server_detail.h"
+
+#include "authentication_identity.h"
+#include "config.h"
+#include "dispatcher.h"
+#include "message.h"
+#include "private.h"
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+using namespace NBus;
+using namespace NYTree;
+using namespace NRpc::NProto;
+using namespace NTracing;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceContextBase::TServiceContextBase(
+ std::unique_ptr<TRequestHeader> header,
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel)
+ : RequestHeader_(std::move(header))
+ , RequestMessage_(std::move(requestMessage))
+ , Logger(std::move(logger))
+ , LogLevel_(logLevel)
+{
+ Initialize();
+}
+
+TServiceContextBase::TServiceContextBase(
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel)
+ : RequestHeader_(new TRequestHeader())
+ , RequestMessage_(std::move(requestMessage))
+ , Logger(std::move(logger))
+ , LogLevel_(logLevel)
+{
+ YT_VERIFY(ParseRequestHeader(RequestMessage_, RequestHeader_.get()));
+ Initialize();
+}
+
+void TServiceContextBase::DoFlush()
+{ }
+
+void TServiceContextBase::Initialize()
+{
+ LoggingEnabled_ = Logger.IsLevelEnabled(LogLevel_);
+
+ RequestId_ = FromProto<TRequestId>(RequestHeader_->request_id());
+ RealmId_ = FromProto<TRealmId>(RequestHeader_->realm_id());
+ AuthenticationIdentity_.User = RequestHeader_->has_user() ? RequestHeader_->user() : RootUserName;
+ AuthenticationIdentity_.UserTag = RequestHeader_->has_user_tag() ? RequestHeader_->user_tag() : AuthenticationIdentity_.User;
+
+ YT_ASSERT(RequestMessage_.Size() >= 2);
+ RequestBody_ = RequestMessage_[1];
+ RequestAttachments_ = std::vector<TSharedRef>(
+ RequestMessage_.Begin() + 2,
+ RequestMessage_.End());
+}
+
+void TServiceContextBase::Reply(const TError& error)
+{
+ YT_ASSERT(!Replied_);
+
+ Error_ = error;
+
+ ReplyEpilogue();
+}
+
+void TServiceContextBase::Reply(const TSharedRefArray& responseMessage)
+{
+ YT_ASSERT(!Replied_);
+ YT_ASSERT(responseMessage.Size() >= 1);
+
+ // NB: One must parse responseMessage and only use its content since,
+ // e.g., responseMessage may contain invalid request id.
+ TResponseHeader header;
+ YT_VERIFY(TryParseResponseHeader(responseMessage, &header));
+
+ if (header.has_error()) {
+ Error_ = FromProto<TError>(header.error());
+ }
+ if (Error_.IsOK()) {
+ YT_ASSERT(responseMessage.Size() >= 2);
+ ResponseBody_ = responseMessage[1];
+ ResponseAttachments_ = std::vector<TSharedRef>(
+ responseMessage.Begin() + 2,
+ responseMessage.End());
+ } else {
+ ResponseBody_.Reset();
+ ResponseAttachments_.clear();
+ }
+
+ ReplyEpilogue();
+}
+
+void TServiceContextBase::ReplyEpilogue()
+{
+ if (!RequestInfoSet_ &&
+ Error_.IsOK() &&
+ LoggingEnabled_ &&
+ TDispatcher::Get()->ShouldAlertOnMissingRequestInfo())
+ {
+ static const auto& Logger = RpcServerLogger;
+ YT_LOG_ALERT("Missing request info (RequestId: %v, Method: %v.%v)",
+ RequestId_,
+ RequestHeader_->service(),
+ RequestHeader_->method());
+ }
+
+ auto responseMessage = BuildResponseMessage();
+
+ TPromise<TSharedRefArray> asyncResponseMessage;
+ {
+ auto responseGuard = Guard(ResponseLock_);
+ YT_ASSERT(!ResponseMessage_);
+ ResponseMessage_ = responseMessage;
+ asyncResponseMessage = AsyncResponseMessage_;
+ Replied_.store(true);
+ }
+
+ DoReply();
+
+ if (LoggingEnabled_) {
+ LogResponse();
+ }
+
+ DoFlush();
+
+ if (asyncResponseMessage) {
+ asyncResponseMessage.Set(std::move(responseMessage));
+ }
+
+ RepliedList_.Fire();
+}
+
+void TServiceContextBase::SetComplete()
+{ }
+
+TFuture<TSharedRefArray> TServiceContextBase::GetAsyncResponseMessage() const
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ auto guard = Guard(ResponseLock_);
+
+ if (!AsyncResponseMessage_) {
+ AsyncResponseMessage_ = NewPromise<TSharedRefArray>();
+ if (ResponseMessage_) {
+ guard.Release();
+ AsyncResponseMessage_.Set(ResponseMessage_);
+ }
+ }
+
+ return AsyncResponseMessage_;
+}
+
+const TSharedRefArray& TServiceContextBase::GetResponseMessage() const
+{
+ YT_ASSERT(ResponseMessage_);
+ return ResponseMessage_;
+}
+
+TSharedRefArray TServiceContextBase::BuildResponseMessage()
+{
+ NProto::TResponseHeader header;
+ ToProto(header.mutable_request_id(), RequestId_);
+ ToProto(header.mutable_error(), Error_);
+
+ if (RequestHeader_->has_response_format()) {
+ header.set_format(RequestHeader_->response_format());
+ }
+
+ // COMPAT(kiselyovp)
+ if (RequestHeader_->has_response_codec()) {
+ header.set_codec(static_cast<int>(ResponseCodec_));
+ }
+
+ auto message = Error_.IsOK()
+ ? CreateResponseMessage(
+ header,
+ ResponseBody_,
+ ResponseAttachments_)
+ : CreateErrorResponseMessage(header);
+
+ auto responseMessageError = CheckBusMessageLimits(ResponseMessage_);
+ if (!responseMessageError.IsOK()) {
+ return CreateErrorResponseMessage(responseMessageError);
+ }
+
+ return message;
+}
+
+bool TServiceContextBase::IsReplied() const
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+ return Replied_.load();
+}
+
+void TServiceContextBase::SubscribeCanceled(const TClosure& /*callback*/)
+{ }
+
+void TServiceContextBase::UnsubscribeCanceled(const TClosure& /*callback*/)
+{ }
+
+void TServiceContextBase::SubscribeReplied(const TClosure& /*callback*/)
+{ }
+
+void TServiceContextBase::UnsubscribeReplied(const TClosure& /*callback*/)
+{ }
+
+bool TServiceContextBase::IsCanceled() const
+{
+ return false;
+}
+
+void TServiceContextBase::Cancel()
+{ }
+
+const TError& TServiceContextBase::GetError() const
+{
+ YT_ASSERT(Replied_);
+
+ return Error_;
+}
+
+TSharedRef TServiceContextBase::GetRequestBody() const
+{
+ return RequestBody_;
+}
+
+std::vector<TSharedRef>& TServiceContextBase::RequestAttachments()
+{
+ return RequestAttachments_;
+}
+
+IAsyncZeroCopyInputStreamPtr TServiceContextBase::GetRequestAttachmentsStream()
+{
+ return nullptr;
+}
+
+TSharedRef TServiceContextBase::GetResponseBody()
+{
+ return ResponseBody_;
+}
+
+void TServiceContextBase::SetResponseBody(const TSharedRef& responseBody)
+{
+ YT_ASSERT(!Replied_);
+
+ ResponseBody_ = responseBody;
+}
+
+std::vector<TSharedRef>& TServiceContextBase::ResponseAttachments()
+{
+ return ResponseAttachments_;
+}
+
+IAsyncZeroCopyOutputStreamPtr TServiceContextBase::GetResponseAttachmentsStream()
+{
+ return nullptr;
+}
+
+const NProto::TRequestHeader& TServiceContextBase::GetRequestHeader() const
+{
+ return *RequestHeader_;
+}
+
+TSharedRefArray TServiceContextBase::GetRequestMessage() const
+{
+ return RequestMessage_;
+}
+
+TRequestId TServiceContextBase::GetRequestId() const
+{
+ return RequestId_;
+}
+
+TBusNetworkStatistics TServiceContextBase::GetBusNetworkStatistics() const
+{
+ return {};
+}
+
+const IAttributeDictionary& TServiceContextBase::GetEndpointAttributes() const
+{
+ return EmptyAttributes();
+}
+
+const TString& TServiceContextBase::GetEndpointDescription() const
+{
+ static const TString EmptyEndpointDescription;
+ return EmptyEndpointDescription;
+}
+
+std::optional<TInstant> TServiceContextBase::GetStartTime() const
+{
+ return RequestHeader_->has_start_time()
+ ? std::make_optional(FromProto<TInstant>(RequestHeader_->start_time()))
+ : std::nullopt;
+}
+
+std::optional<TDuration> TServiceContextBase::GetTimeout() const
+{
+ return RequestHeader_->has_timeout()
+ ? std::make_optional(FromProto<TDuration>(RequestHeader_->timeout()))
+ : std::nullopt;
+}
+
+TInstant TServiceContextBase::GetArriveInstant() const
+{
+ return TInstant::Zero();
+}
+
+std::optional<TInstant> TServiceContextBase::GetRunInstant() const
+{
+ return std::nullopt;
+}
+
+std::optional<TInstant> TServiceContextBase::GetFinishInstant() const
+{
+ return std::nullopt;
+}
+
+std::optional<TDuration> TServiceContextBase::GetWaitDuration() const
+{
+ return std::nullopt;
+}
+
+std::optional<TDuration> TServiceContextBase::GetExecutionDuration() const
+{
+ return std::nullopt;
+}
+
+TTraceContextPtr TServiceContextBase::GetTraceContext() const
+{
+ return nullptr;
+}
+
+std::optional<TDuration> TServiceContextBase::GetTraceContextTime() const
+{
+ return std::nullopt;
+}
+
+bool TServiceContextBase::IsRetry() const
+{
+ return RequestHeader_->retry();
+}
+
+TMutationId TServiceContextBase::GetMutationId() const
+{
+ return FromProto<TMutationId>(RequestHeader_->mutation_id());
+}
+
+const TString& TServiceContextBase::GetService() const
+{
+ return RequestHeader_->service();
+}
+
+const TString& TServiceContextBase::GetMethod() const
+{
+ return RequestHeader_->method();
+}
+
+TRealmId TServiceContextBase::GetRealmId() const
+{
+ return RealmId_;
+}
+
+const TAuthenticationIdentity& TServiceContextBase::GetAuthenticationIdentity() const
+{
+ return AuthenticationIdentity_;
+}
+
+const TRequestHeader& TServiceContextBase::RequestHeader() const
+{
+ return *RequestHeader_;
+}
+
+TRequestHeader& TServiceContextBase::RequestHeader()
+{
+ return *RequestHeader_;
+}
+
+bool TServiceContextBase::IsLoggingEnabled() const
+{
+ return LoggingEnabled_;
+}
+
+void TServiceContextBase::SetRawRequestInfo(TString info, bool incremental)
+{
+ YT_ASSERT(!Replied_);
+
+ RequestInfoSet_ = true;
+
+ if (!LoggingEnabled_) {
+ return;
+ }
+
+ if (!info.empty()) {
+ RequestInfos_.push_back(std::move(info));
+ }
+ if (!incremental) {
+ LogRequest();
+ }
+}
+
+void TServiceContextBase::SuppressMissingRequestInfoCheck()
+{
+ YT_ASSERT(!Replied_);
+
+ RequestInfoSet_ = true;
+}
+
+void TServiceContextBase::SetRawResponseInfo(TString info, bool incremental)
+{
+ YT_ASSERT(!Replied_);
+
+ if (!LoggingEnabled_) {
+ return;
+ }
+
+ if (!incremental) {
+ ResponseInfos_.clear();
+ }
+ if (!info.empty()) {
+ ResponseInfos_.push_back(std::move(info));
+ }
+}
+
+const NLogging::TLogger& TServiceContextBase::GetLogger() const
+{
+ return Logger;
+}
+
+NLogging::ELogLevel TServiceContextBase::GetLogLevel() const
+{
+ return LogLevel_;
+}
+
+bool TServiceContextBase::IsPooled() const
+{
+ return false;
+}
+
+NCompression::ECodec TServiceContextBase::GetResponseCodec() const
+{
+ return ResponseCodec_;
+}
+
+void TServiceContextBase::SetResponseCodec(NCompression::ECodec codec)
+{
+ ResponseCodec_ = codec;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceContextWrapper::TServiceContextWrapper(IServiceContextPtr underlyingContext)
+ : UnderlyingContext_(std::move(underlyingContext))
+{ }
+
+const NProto::TRequestHeader& TServiceContextWrapper::GetRequestHeader() const
+{
+ return UnderlyingContext_->GetRequestHeader();
+}
+
+TBusNetworkStatistics TServiceContextWrapper::GetBusNetworkStatistics() const
+{
+ return UnderlyingContext_->GetBusNetworkStatistics();
+}
+
+const NYTree::IAttributeDictionary& TServiceContextWrapper::GetEndpointAttributes() const
+{
+ return UnderlyingContext_->GetEndpointAttributes();
+}
+
+const TString& TServiceContextWrapper::GetEndpointDescription() const
+{
+ return UnderlyingContext_->GetEndpointDescription();
+}
+
+TSharedRefArray TServiceContextWrapper::GetRequestMessage() const
+{
+ return UnderlyingContext_->GetRequestMessage();
+}
+
+TRequestId TServiceContextWrapper::GetRequestId() const
+{
+ return UnderlyingContext_->GetRequestId();
+}
+
+std::optional<TInstant> TServiceContextWrapper::GetStartTime() const
+{
+ return UnderlyingContext_->GetStartTime();
+}
+
+std::optional<TDuration> TServiceContextWrapper::GetTimeout() const
+{
+ return UnderlyingContext_->GetTimeout();
+}
+
+TInstant TServiceContextWrapper::GetArriveInstant() const
+{
+ return UnderlyingContext_->GetArriveInstant();
+}
+
+std::optional<TInstant> TServiceContextWrapper::GetRunInstant() const
+{
+ return UnderlyingContext_->GetRunInstant();
+}
+
+std::optional<TInstant> TServiceContextWrapper::GetFinishInstant() const
+{
+ return UnderlyingContext_->GetFinishInstant();
+}
+
+std::optional<TDuration> TServiceContextWrapper::GetWaitDuration() const
+{
+ return UnderlyingContext_->GetWaitDuration();
+}
+
+std::optional<TDuration> TServiceContextWrapper::GetExecutionDuration() const
+{
+ return UnderlyingContext_->GetExecutionDuration();
+}
+
+TTraceContextPtr TServiceContextWrapper::GetTraceContext() const
+{
+ return UnderlyingContext_->GetTraceContext();
+}
+
+std::optional<TDuration> TServiceContextWrapper::GetTraceContextTime() const
+{
+ return UnderlyingContext_->GetTraceContextTime();
+}
+
+bool TServiceContextWrapper::IsRetry() const
+{
+ return UnderlyingContext_->IsRetry();
+}
+
+TMutationId TServiceContextWrapper::GetMutationId() const
+{
+ return UnderlyingContext_->GetMutationId();
+}
+
+const TString& TServiceContextWrapper::GetService() const
+{
+ return UnderlyingContext_->GetService();
+}
+
+const TString& TServiceContextWrapper::GetMethod() const
+{
+ return UnderlyingContext_->GetMethod();
+}
+
+TRealmId TServiceContextWrapper::GetRealmId() const
+{
+ return UnderlyingContext_->GetRealmId();
+}
+
+const TAuthenticationIdentity& TServiceContextWrapper::GetAuthenticationIdentity() const
+{
+ return UnderlyingContext_->GetAuthenticationIdentity();
+}
+
+bool TServiceContextWrapper::IsReplied() const
+{
+ return UnderlyingContext_->IsReplied();
+}
+
+void TServiceContextWrapper::Reply(const TError& error)
+{
+ UnderlyingContext_->Reply(error);
+}
+
+void TServiceContextWrapper::Reply(const TSharedRefArray& responseMessage)
+{
+ UnderlyingContext_->Reply(responseMessage);
+}
+
+void TServiceContextWrapper::SetComplete()
+{
+ UnderlyingContext_->SetComplete();
+}
+
+void TServiceContextWrapper::SubscribeCanceled(const TClosure& callback)
+{
+ UnderlyingContext_->SubscribeCanceled(callback);
+}
+
+void TServiceContextWrapper::UnsubscribeCanceled(const TClosure& callback)
+{
+ UnderlyingContext_->UnsubscribeCanceled(callback);
+}
+
+void TServiceContextWrapper::SubscribeReplied(const TClosure& callback)
+{
+ UnderlyingContext_->SubscribeReplied(callback);
+}
+
+void TServiceContextWrapper::UnsubscribeReplied(const TClosure& callback)
+{
+ UnderlyingContext_->UnsubscribeReplied(callback);
+}
+
+bool TServiceContextWrapper::IsCanceled() const
+{
+ return UnderlyingContext_->IsCanceled();
+}
+
+void TServiceContextWrapper::Cancel()
+{ }
+
+TFuture<TSharedRefArray> TServiceContextWrapper::GetAsyncResponseMessage() const
+{
+ return UnderlyingContext_->GetAsyncResponseMessage();
+}
+
+const TSharedRefArray& TServiceContextWrapper::GetResponseMessage() const
+{
+ return UnderlyingContext_->GetResponseMessage();
+}
+
+const TError& TServiceContextWrapper::GetError() const
+{
+ return UnderlyingContext_->GetError();
+}
+
+TSharedRef TServiceContextWrapper::GetRequestBody() const
+{
+ return UnderlyingContext_->GetRequestBody();
+}
+
+TSharedRef TServiceContextWrapper::GetResponseBody()
+{
+ return UnderlyingContext_->GetResponseBody();
+}
+
+void TServiceContextWrapper::SetResponseBody(const TSharedRef& responseBody)
+{
+ UnderlyingContext_->SetResponseBody(responseBody);
+}
+
+std::vector<TSharedRef>& TServiceContextWrapper::RequestAttachments()
+{
+ return UnderlyingContext_->RequestAttachments();
+}
+
+IAsyncZeroCopyInputStreamPtr TServiceContextWrapper::GetRequestAttachmentsStream()
+{
+ return UnderlyingContext_->GetRequestAttachmentsStream();
+}
+
+std::vector<TSharedRef>& TServiceContextWrapper::ResponseAttachments()
+{
+ return UnderlyingContext_->ResponseAttachments();
+}
+
+const NProto::TRequestHeader& TServiceContextWrapper::RequestHeader() const
+{
+ return UnderlyingContext_->RequestHeader();
+}
+
+IAsyncZeroCopyOutputStreamPtr TServiceContextWrapper::GetResponseAttachmentsStream()
+{
+ return UnderlyingContext_->GetResponseAttachmentsStream();
+}
+
+NProto::TRequestHeader& TServiceContextWrapper::RequestHeader()
+{
+ return UnderlyingContext_->RequestHeader();
+}
+
+bool TServiceContextWrapper::IsLoggingEnabled() const
+{
+ return UnderlyingContext_->IsLoggingEnabled();
+}
+
+void TServiceContextWrapper::SetRawRequestInfo(TString info, bool incremental)
+{
+ UnderlyingContext_->SetRawRequestInfo(std::move(info), incremental);
+}
+
+void TServiceContextWrapper::SuppressMissingRequestInfoCheck()
+{
+ UnderlyingContext_->SuppressMissingRequestInfoCheck();
+}
+
+void TServiceContextWrapper::SetRawResponseInfo(TString info, bool incremental)
+{
+ UnderlyingContext_->SetRawResponseInfo(std::move(info), incremental);
+}
+
+const NLogging::TLogger& TServiceContextWrapper::GetLogger() const
+{
+ return UnderlyingContext_->GetLogger();
+}
+
+NLogging::ELogLevel TServiceContextWrapper::GetLogLevel() const
+{
+ return UnderlyingContext_->GetLogLevel();
+}
+
+bool TServiceContextWrapper::IsPooled() const
+{
+ return UnderlyingContext_->IsPooled();
+}
+
+NCompression::ECodec TServiceContextWrapper::GetResponseCodec() const
+{
+ return UnderlyingContext_->GetResponseCodec();
+}
+
+void TServiceContextWrapper::SetResponseCodec(NCompression::ECodec codec)
+{
+ UnderlyingContext_->SetResponseCodec(codec);
+}
+
+const IServiceContextPtr& TServiceContextWrapper::GetUnderlyingContext() const
+{
+ return UnderlyingContext_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServerBase::RegisterService(IServicePtr service)
+{
+ YT_VERIFY(service);
+
+ auto serviceId = service->GetServiceId();
+
+ {
+ auto guard = WriterGuard(ServicesLock_);
+ auto& serviceMap = RealmIdToServiceMap_[serviceId.RealmId];
+ YT_VERIFY(serviceMap.emplace(serviceId.ServiceName, service).second);
+ if (Config_) {
+ auto it = Config_->Services.find(serviceId.ServiceName);
+ if (it != Config_->Services.end()) {
+ service->Configure(Config_, it->second);
+ } else {
+ service->Configure(Config_, nullptr);
+ }
+ }
+ DoRegisterService(service);
+ }
+
+ YT_LOG_INFO("RPC service registered (ServiceName: %v, RealmId: %v)",
+ serviceId.ServiceName,
+ serviceId.RealmId);
+}
+
+bool TServerBase::UnregisterService(IServicePtr service)
+{
+ YT_VERIFY(service);
+
+ auto serviceId = service->GetServiceId();
+
+ {
+ auto guard = WriterGuard(ServicesLock_);
+
+ auto serviceMapIt = RealmIdToServiceMap_.find(serviceId.RealmId);
+ if (serviceMapIt == RealmIdToServiceMap_.end()) {
+ return false;
+ }
+ auto& serviceMap = serviceMapIt->second;
+ auto serviceIt = serviceMap.find(serviceId.ServiceName);
+ if (serviceIt == serviceMap.end() || serviceIt->second != service) {
+ return false;
+ }
+ serviceMap.erase(serviceIt);
+ if (serviceMap.empty()) {
+ YT_VERIFY(RealmIdToServiceMap_.erase(serviceId.RealmId));
+ }
+
+ DoUnregisterService(service);
+ }
+
+ YT_LOG_INFO("RPC service unregistered (ServiceName: %v, RealmId: %v)",
+ serviceId.ServiceName,
+ serviceId.RealmId);
+ return true;
+}
+
+IServicePtr TServerBase::FindService(const TServiceId& serviceId) const
+{
+ auto guard = ReaderGuard(ServicesLock_);
+ auto serviceMapIt = RealmIdToServiceMap_.find(serviceId.RealmId);
+ if (serviceMapIt == RealmIdToServiceMap_.end()) {
+ return nullptr;
+ }
+ auto& serviceMap = serviceMapIt->second;
+ auto serviceIt = serviceMap.find(serviceId.ServiceName);
+ return serviceIt == serviceMap.end() ? nullptr : serviceIt->second;
+}
+
+IServicePtr TServerBase::GetServiceOrThrow(const TServiceId& serviceId) const
+{
+ auto guard = ReaderGuard(ServicesLock_);
+
+ const auto& realmId = serviceId.RealmId;
+ const auto& serviceName = serviceId.ServiceName;
+ auto serviceMapIt = RealmIdToServiceMap_.find(realmId);
+ if (serviceMapIt == RealmIdToServiceMap_.end()) {
+ if (realmId) {
+ // TODO(gritukan): Stop wrapping error one day.
+ auto innerError = TError(EErrorCode::NoSuchRealm, "Request realm is unknown")
+ << TErrorAttribute("service", serviceName)
+ << TErrorAttribute("realm_id", realmId);
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::NoSuchService,
+ "Service is not registered")
+ << innerError;
+ } else {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::NoSuchService,
+ "Service is not registered")
+ << TErrorAttribute("service", serviceName)
+ << TErrorAttribute("realm_id", realmId);
+ }
+ }
+ auto& serviceMap = serviceMapIt->second;
+ auto serviceIt = serviceMap.find(serviceName);
+ if (serviceIt == serviceMap.end()) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::NoSuchService,
+ "Service is not registered")
+ << TErrorAttribute("service", serviceName)
+ << TErrorAttribute("realm_id", realmId);
+ }
+
+ return serviceIt->second;
+}
+
+void TServerBase::Configure(TServerConfigPtr config)
+{
+ auto guard = WriterGuard(ServicesLock_);
+
+ // Future services will be configured appropriately.
+ Config_ = config;
+
+ // Apply configuration to all existing services.
+ for (const auto& [realmId, serviceMap] : RealmIdToServiceMap_) {
+ for (const auto& [serviceName, service] : serviceMap) {
+ auto it = config->Services.find(serviceName);
+ if (it != config->Services.end()) {
+ service->Configure(config, it->second);
+ } else {
+ service->Configure(config, nullptr);
+ }
+ }
+ }
+}
+
+void TServerBase::Start()
+{
+ YT_VERIFY(!Started_);
+
+ DoStart();
+
+ YT_LOG_INFO("RPC server started");
+}
+
+TFuture<void> TServerBase::Stop(bool graceful)
+{
+ if (!Started_) {
+ return VoidFuture;
+ }
+
+ YT_LOG_INFO("Stopping RPC server (Graceful: %v)",
+ graceful);
+
+ return DoStop(graceful).Apply(BIND([this, this_ = MakeStrong(this)] () {
+ YT_LOG_INFO("RPC server stopped");
+ }));
+}
+
+TServerBase::TServerBase(NLogging::TLogger logger)
+ : Logger(std::move(logger))
+{ }
+
+void TServerBase::DoStart()
+{
+ Started_ = true;
+}
+
+TFuture<void> TServerBase::DoStop(bool graceful)
+{
+ Started_ = false;
+
+ std::vector<TFuture<void>> asyncResults;
+
+ if (graceful) {
+ std::vector<IServicePtr> services;
+ {
+ auto guard = ReaderGuard(ServicesLock_);
+ for (const auto& [realmId, serviceMap] : RealmIdToServiceMap_) {
+ for (const auto& [serviceName, service] : serviceMap) {
+ services.push_back(service);
+ }
+ }
+ }
+
+ for (const auto& service : services) {
+ asyncResults.push_back(service->Stop());
+ }
+ }
+
+ return AllSucceeded(asyncResults);
+}
+
+void TServerBase::DoRegisterService(const IServicePtr& /*service*/)
+{ }
+
+void TServerBase::DoUnregisterService(const IServicePtr& /*service*/)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/server_detail.h b/yt/yt/core/rpc/server_detail.h
new file mode 100644
index 0000000000..652f359b9d
--- /dev/null
+++ b/yt/yt/core/rpc/server_detail.h
@@ -0,0 +1,288 @@
+#pragma once
+
+#include "authentication_identity.h"
+#include "server.h"
+#include "service.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! \note Thread affinity: single-threaded (unless noted otherwise)
+class TServiceContextBase
+ : public virtual IServiceContext
+{
+public:
+ const NProto::TRequestHeader& GetRequestHeader() const override;
+ TSharedRefArray GetRequestMessage() const override;
+
+ TRequestId GetRequestId() const override;
+ NYT::NBus::TBusNetworkStatistics GetBusNetworkStatistics() const override;
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override;
+ const TString& GetEndpointDescription() const override;
+
+ std::optional<TInstant> GetStartTime() const override;
+ std::optional<TDuration> GetTimeout() const override;
+ TInstant GetArriveInstant() const override;
+ std::optional<TInstant> GetRunInstant() const override;
+ std::optional<TInstant> GetFinishInstant() const override;
+ std::optional<TDuration> GetWaitDuration() const override;
+ std::optional<TDuration> GetExecutionDuration() const override;
+
+ NTracing::TTraceContextPtr GetTraceContext() const override;
+ std::optional<TDuration> GetTraceContextTime() const override;
+
+ bool IsRetry() const override;
+ TMutationId GetMutationId() const override;
+
+ const TString& GetService() const override;
+ const TString& GetMethod() const override;
+ TRealmId GetRealmId() const override;
+ const TAuthenticationIdentity& GetAuthenticationIdentity() const override;
+
+ //! \note Thread affinity: any
+ bool IsReplied() const override;
+
+ void Reply(const TError& error) override;
+ void Reply(const TSharedRefArray& responseMessage) override;
+ using IServiceContext::Reply;
+
+ void SetComplete() override;
+
+ //! \note Thread affinity: any
+ TFuture<TSharedRefArray> GetAsyncResponseMessage() const override;
+
+ const TSharedRefArray& GetResponseMessage() const override;
+
+ void SubscribeCanceled(const TClosure& callback) override;
+ void UnsubscribeCanceled(const TClosure& callback) override;
+
+ void SubscribeReplied(const TClosure& callback) override;
+ void UnsubscribeReplied(const TClosure& callback) override;
+
+ bool IsCanceled() const override;
+ void Cancel() override;
+
+ const TError& GetError() const override;
+
+ TSharedRef GetRequestBody() const override;
+
+ TSharedRef GetResponseBody() override;
+ void SetResponseBody(const TSharedRef& responseBody) override;
+
+ std::vector<TSharedRef>& RequestAttachments() override;
+ NConcurrency::IAsyncZeroCopyInputStreamPtr GetRequestAttachmentsStream() override;
+
+ std::vector<TSharedRef>& ResponseAttachments() override;
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr GetResponseAttachmentsStream() override;
+
+ const NProto::TRequestHeader& RequestHeader() const override;
+ NProto::TRequestHeader& RequestHeader() override;
+
+ bool IsLoggingEnabled() const override;
+ void SetRawRequestInfo(TString info, bool incremental) override;
+ void SuppressMissingRequestInfoCheck() override;
+ void SetRawResponseInfo(TString info, bool incremental) override;
+
+ const NLogging::TLogger& GetLogger() const override;
+ NLogging::ELogLevel GetLogLevel() const override;
+
+ bool IsPooled() const override;
+
+ NCompression::ECodec GetResponseCodec() const override;
+ void SetResponseCodec(NCompression::ECodec codec) override;
+
+protected:
+ std::unique_ptr<NProto::TRequestHeader> RequestHeader_;
+ TSharedRefArray RequestMessage_;
+ const NLogging::TLogger Logger;
+ const NLogging::ELogLevel LogLevel_;
+
+ // Set in #Initialize.
+ bool LoggingEnabled_;
+ TRequestId RequestId_;
+ TRealmId RealmId_;
+
+ TAuthenticationIdentity AuthenticationIdentity_;
+
+ TSharedRef RequestBody_;
+ std::vector<TSharedRef> RequestAttachments_;
+
+ std::atomic<bool> Replied_ = false;
+ TError Error_;
+
+ TSharedRef ResponseBody_;
+ std::vector<TSharedRef> ResponseAttachments_;
+
+ bool RequestInfoSet_ = false;
+ TCompactVector<TString, 4> RequestInfos_;
+ TCompactVector<TString, 4> ResponseInfos_;
+
+ NCompression::ECodec ResponseCodec_ = NCompression::ECodec::None;
+
+ TSingleShotCallbackList<void()> RepliedList_;
+
+ TServiceContextBase(
+ std::unique_ptr<NProto::TRequestHeader> header,
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel);
+ TServiceContextBase(
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel);
+
+ virtual void DoReply() = 0;
+ virtual void DoFlush();
+
+ virtual void LogRequest() = 0;
+ virtual void LogResponse() = 0;
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, ResponseLock_);
+ TSharedRefArray ResponseMessage_; // cached
+ mutable TPromise<TSharedRefArray> AsyncResponseMessage_; // created on-demand
+
+
+ void Initialize();
+ TSharedRefArray BuildResponseMessage();
+ void ReplyEpilogue();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceContextWrapper
+ : public virtual IServiceContext
+{
+public:
+ explicit TServiceContextWrapper(IServiceContextPtr underlyingContext);
+
+ const NProto::TRequestHeader& GetRequestHeader() const override;
+ TSharedRefArray GetRequestMessage() const override;
+
+ NRpc::TRequestId GetRequestId() const override;
+ NYT::NBus::TBusNetworkStatistics GetBusNetworkStatistics() const override;
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override;
+ const TString& GetEndpointDescription() const override;
+
+ std::optional<TInstant> GetStartTime() const override;
+ std::optional<TDuration> GetTimeout() const override;
+ TInstant GetArriveInstant() const override;
+ std::optional<TInstant> GetRunInstant() const override;
+ std::optional<TInstant> GetFinishInstant() const override;
+ std::optional<TDuration> GetWaitDuration() const override;
+ std::optional<TDuration> GetExecutionDuration() const override;
+
+ NTracing::TTraceContextPtr GetTraceContext() const override;
+ std::optional<TDuration> GetTraceContextTime() const override;
+
+ bool IsRetry() const override;
+ TMutationId GetMutationId() const override;
+
+ const TString& GetService() const override;
+ const TString& GetMethod() const override;
+ TRealmId GetRealmId() const override;
+ const TAuthenticationIdentity& GetAuthenticationIdentity() const override;
+
+ bool IsReplied() const override;
+ void Reply(const TError& error) override;
+ void Reply(const TSharedRefArray& responseMessage) override;
+
+ void SetComplete() override;
+
+ TFuture<TSharedRefArray> GetAsyncResponseMessage() const override;
+ const TSharedRefArray& GetResponseMessage() const override;
+
+ void SubscribeCanceled(const TClosure& callback) override;
+ void UnsubscribeCanceled(const TClosure& callback) override;
+
+ void SubscribeReplied(const TClosure& callback) override;
+ void UnsubscribeReplied(const TClosure& callback) override;
+
+ bool IsCanceled() const override;
+ void Cancel() override;
+
+ const TError& GetError() const override;
+
+ TSharedRef GetRequestBody() const override;
+
+ TSharedRef GetResponseBody() override;
+ void SetResponseBody(const TSharedRef& responseBody) override;
+
+ std::vector<TSharedRef>& RequestAttachments() override;
+ NConcurrency::IAsyncZeroCopyInputStreamPtr GetRequestAttachmentsStream() override;
+
+ std::vector<TSharedRef>& ResponseAttachments() override;
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr GetResponseAttachmentsStream() override;
+
+ const NProto::TRequestHeader& RequestHeader() const override;
+
+ NProto::TRequestHeader& RequestHeader() override;
+
+ bool IsLoggingEnabled() const override;
+ void SetRawRequestInfo(TString info, bool incremental) override;
+ void SuppressMissingRequestInfoCheck() override;
+ void SetRawResponseInfo(TString info, bool incremental) override;
+
+ const NLogging::TLogger& GetLogger() const override;
+ NLogging::ELogLevel GetLogLevel() const override;
+
+ bool IsPooled() const override;
+
+ NCompression::ECodec GetResponseCodec() const override;
+ void SetResponseCodec(NCompression::ECodec codec) override;
+
+ const IServiceContextPtr& GetUnderlyingContext() const;
+
+private:
+ const IServiceContextPtr UnderlyingContext_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServerBase
+ : public IServer
+{
+public:
+ void RegisterService(IServicePtr service) override;
+ bool UnregisterService(IServicePtr service) override;
+
+ IServicePtr FindService(const TServiceId& serviceId) const override;
+ IServicePtr GetServiceOrThrow(const TServiceId& serviceId) const override;
+
+ void Configure(TServerConfigPtr config) override;
+
+ void Start() override;
+ TFuture<void> Stop(bool graceful) override;
+
+protected:
+ const NLogging::TLogger Logger;
+
+ std::atomic<bool> Started_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, ServicesLock_);
+ TServerConfigPtr Config_;
+
+ //! Service name to service.
+ using TServiceMap = THashMap<TString, IServicePtr>;
+ THashMap<TGuid, TServiceMap> RealmIdToServiceMap_;
+
+ explicit TServerBase(NLogging::TLogger logger);
+
+ virtual void DoStart();
+ virtual TFuture<void> DoStop(bool graceful);
+
+ virtual void DoRegisterService(const IServicePtr& service);
+ virtual void DoUnregisterService(const IServicePtr& service);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/service-inl.h b/yt/yt/core/rpc/service-inl.h
new file mode 100644
index 0000000000..501bf06a66
--- /dev/null
+++ b/yt/yt/core/rpc/service-inl.h
@@ -0,0 +1,73 @@
+#ifndef SERVICE_INL_H_
+#error "Direct inclusion of this file is not allowed, include service.h"
+// For the sake of sane code completion.
+#include "service.h"
+#endif
+
+#include "helpers.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... TArgs>
+void IServiceContext::SetRequestInfo(const char* format, TArgs&&... args)
+{
+ if (IsLoggingEnabled()) {
+ SetRawRequestInfo(Format(format, std::forward<TArgs>(args)...), /*incremental*/ false);
+ } else {
+ SuppressMissingRequestInfoCheck();
+ }
+}
+
+template <class... TArgs>
+void IServiceContext::SetIncrementalRequestInfo(const char* format, TArgs&&... args)
+{
+ if (IsLoggingEnabled()) {
+ SetRawRequestInfo(Format(format, std::forward<TArgs>(args)...), /*incremental*/ true);
+ } else {
+ SuppressMissingRequestInfoCheck();
+ }
+}
+
+template <class... TArgs>
+void IServiceContext::SetResponseInfo(const char* format, TArgs&&... args)
+{
+ if (IsLoggingEnabled()) {
+ SetRawResponseInfo(Format(format, std::forward<TArgs>(args)...), /*incremental*/ false);
+ }
+}
+
+template <class... TArgs>
+void IServiceContext::SetIncrementalResponseInfo(const char* format, TArgs&&... args)
+{
+ if (IsLoggingEnabled()) {
+ SetRawResponseInfo(Format(format, std::forward<TArgs>(args)...), /*incremental*/ true);
+ }
+}
+
+namespace NDetail {
+
+bool IsClientFeatureSupported(const IServiceContext* context, int featureId);
+void ThrowUnsupportedClientFeature(int featureId, TStringBuf featureName);
+
+} // namespace NDetail
+
+template <class E>
+bool IServiceContext::IsClientFeatureSupported(E featureId) const
+{
+ return NDetail::IsClientFeatureSupported(this, FeatureIdToInt(featureId));
+}
+
+template <class E>
+void IServiceContext::ValidateClientFeature(E featureId) const
+{
+ auto intFeatureId = FeatureIdToInt(featureId);
+ if (!NDetail::IsClientFeatureSupported(this, intFeatureId)) {
+ NDetail::ThrowUnsupportedClientFeature(intFeatureId, TEnumTraits<E>::ToString(featureId));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/service.cpp b/yt/yt/core/rpc/service.cpp
new file mode 100644
index 0000000000..441ab3e0b2
--- /dev/null
+++ b/yt/yt/core/rpc/service.cpp
@@ -0,0 +1,113 @@
+#include "service.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TError MakeCanceledError()
+{
+ return TError("RPC request canceled");
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IServiceContext::SetRequestInfo()
+{
+ SetRawRequestInfo(TString(), false);
+}
+
+void IServiceContext::SetResponseInfo()
+{
+ SetRawResponseInfo(TString(), false);
+}
+
+void IServiceContext::ReplyFrom(TFuture<TSharedRefArray> asyncMessage)
+{
+ asyncMessage.Subscribe(BIND([this, this_ = MakeStrong(this)] (const TErrorOr<TSharedRefArray>& result) {
+ if (result.IsOK()) {
+ Reply(result.Value());
+ } else {
+ Reply(TError(result));
+ }
+ }));
+ SubscribeCanceled(BIND([asyncMessage = std::move(asyncMessage)] {
+ asyncMessage.Cancel(MakeCanceledError());
+ }));
+}
+
+void IServiceContext::ReplyFrom(TFuture<void> asyncError)
+{
+ asyncError.Subscribe(BIND([this, this_ = MakeStrong(this)] (const TError& error) {
+ Reply(error);
+ }));
+ SubscribeCanceled(BIND([asyncError = std::move(asyncError)] {
+ asyncError.Cancel(MakeCanceledError());
+ }));
+}
+
+void IServiceContext::ReplyFrom(TFuture<void> asyncError, const IInvokerPtr& invoker)
+{
+ asyncError.Subscribe(BIND([this, this_ = MakeStrong(this)] (const TError& error) {
+ Reply(error);
+ })
+ .Via(invoker));
+ SubscribeCanceled(BIND([asyncError = std::move(asyncError)] {
+ asyncError.Cancel(MakeCanceledError());
+ }));
+}
+
+namespace NDetail {
+
+bool IsClientFeatureSupported(const IServiceContext* context, int featureId)
+{
+ const auto& header = context->RequestHeader();
+ return
+ std::find(header.declared_client_feature_ids().begin(), header.declared_client_feature_ids().end(), featureId) !=
+ header.declared_client_feature_ids().end();
+}
+
+void ThrowUnsupportedClientFeature(int featureId, TStringBuf featureName)
+{
+ THROW_ERROR_EXCEPTION(
+ NRpc::EErrorCode::UnsupportedClientFeature,
+ "Client does not support the feature requested by server")
+ << TErrorAttribute("feature_id", featureId)
+ << TErrorAttribute("feature_name", featureName);
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceId::TServiceId(const TString& serviceName, TRealmId realmId)
+ : ServiceName(serviceName)
+ , RealmId(realmId)
+{ }
+
+bool operator == (const TServiceId& lhs, const TServiceId& rhs)
+{
+ return lhs.ServiceName == rhs.ServiceName && lhs.RealmId == rhs.RealmId;
+}
+
+bool operator != (const TServiceId& lhs, const TServiceId& rhs)
+{
+ return !(lhs == rhs);
+}
+
+TString ToString(const TServiceId& serviceId)
+{
+ auto result = serviceId.ServiceName;
+ if (!serviceId.RealmId.IsEmpty()) {
+ result.append(':');
+ result.append(ToString(serviceId.RealmId));
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/service.h b/yt/yt/core/rpc/service.h
new file mode 100644
index 0000000000..3b0869e486
--- /dev/null
+++ b/yt/yt/core/rpc/service.h
@@ -0,0 +1,348 @@
+#pragma once
+
+#include "public.h"
+
+#include "protocol_version.h"
+
+#include <yt/yt/core/actions/signal.h>
+
+#include <yt/yt/core/bus/public.h>
+
+#include <yt/yt/core/net/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents an RPC request at server-side.
+/*!
+ * \note
+ * Implementations are not thread-safe.
+ */
+struct IServiceContext
+ : public virtual TRefCounted
+{
+ //! Returns raw header of the request being handled.
+ virtual const NProto::TRequestHeader& GetRequestHeader() const = 0;
+
+ //! Returns the message that contains the request being handled.
+ virtual TSharedRefArray GetRequestMessage() const = 0;
+
+ //! Returns the id of the request.
+ /*!
+ * These ids are assigned by the client to distinguish between responses.
+ * The server should not rely on their uniqueness.
+ *
+ * #NullRequestId is a possible value.
+ */
+ virtual TRequestId GetRequestId() const = 0;
+
+ //! Returns the statistics from the underlying bus.
+ /*!
+ * For implementations not using bus, returns all zeroes.
+ */
+ virtual NYT::NBus::TBusNetworkStatistics GetBusNetworkStatistics() const = 0;
+
+ //! Returns the attributes of the connected endpoint.
+ virtual const NYTree::IAttributeDictionary& GetEndpointAttributes() const = 0;
+
+ //! Returns the description of the connected endpoint.
+ virtual const TString& GetEndpointDescription() const = 0;
+
+ //! Returns the instant when the current retry of request was issued by the client, if known.
+ virtual std::optional<TInstant> GetStartTime() const = 0;
+
+ //! Returns the client-specified request timeout, if any.
+ virtual std::optional<TDuration> GetTimeout() const = 0;
+
+ //! Returns the instant request has arrived.
+ virtual TInstant GetArriveInstant() const = 0;
+
+ //! Returns the instant request become executing (if it already has started executing).
+ virtual std::optional<TInstant> GetRunInstant() const = 0;
+
+ //! Returns the instant request finished, i.e. either replied or canceled (if it already has finished).
+ virtual std::optional<TInstant> GetFinishInstant() const = 0;
+
+ //! Returns time between request arrival and execution start (if it already has started executing).
+ virtual std::optional<TDuration> GetWaitDuration() const = 0;
+
+ //! Returns time between request execution start and the moment of reply or cancellation (if it already happened).
+ virtual std::optional<TDuration> GetExecutionDuration() const = 0;
+
+ //! Returns trace context associated with request.
+ virtual NTracing::TTraceContextPtr GetTraceContext() const = 0;
+
+ //! If trace context is present, return total CPU time accounted to this trace context.
+ virtual std::optional<TDuration> GetTraceContextTime() const = 0;
+
+ //! Returns |true| if this is a duplicate copy of a previously sent (and possibly served) request.
+ virtual bool IsRetry() const = 0;
+
+ //! Returns the mutation id for this request, i.e. a unique id used to distinguish copies of the
+ //! (semantically) same request. If no mutation id is assigned then returns null id.
+ virtual TMutationId GetMutationId() const = 0;
+
+ //! Returns request service name.
+ virtual const TString& GetService() const = 0;
+
+ //! Returns request method name.
+ virtual const TString& GetMethod() const = 0;
+
+ //! Returns request realm id.
+ virtual TRealmId GetRealmId() const = 0;
+
+ //! Returns the authentication identity passed from the client (and possibly validated by the infrastructure).
+ //! Could be used for authentication and authorization.
+ virtual const TAuthenticationIdentity& GetAuthenticationIdentity() const = 0;
+
+ //! Returns |true| if the request was already replied.
+ virtual bool IsReplied() const = 0;
+
+ //! Signals that the request processing is complete and sends reply to the client.
+ virtual void Reply(const TError& error) = 0;
+
+ //! Parses the message and forwards to the client.
+ virtual void Reply(const TSharedRefArray& message) = 0;
+
+ //! Called by the service request handler (prior to calling #Reply or #ReplyFrom) to indicate
+ //! that the bulk of the request processing is complete.
+ /*!
+ * Both calling and handling this method is completely optional.
+ *
+ * Upon receiving this call, the current RPC infrastructure decrements the queue size counters and
+ * starts pumping more requests from the queue.
+ */
+ virtual void SetComplete() = 0;
+
+ //! Returns |true| is the request was canceled.
+ virtual bool IsCanceled() const = 0;
+
+ //! Raised when request processing is canceled.
+ DECLARE_INTERFACE_SIGNAL(void(), Canceled);
+
+ //! Raised when Reply() was called. Allows doing some post-request stuff like extended structured logging.
+ DECLARE_INTERFACE_SIGNAL(void(), Replied);
+
+ //! Cancels request processing.
+ /*!
+ * Implementations are free to ignore this call.
+ */
+ virtual void Cancel() = 0;
+
+ //! Returns a future representing the response message.
+ /*!
+ * \note
+ * Can only be called before the request handling is started.
+ */
+ virtual TFuture<TSharedRefArray> GetAsyncResponseMessage() const = 0;
+
+ //! Returns the serialized response message.
+ /*!
+ * \note
+ * Can only be called after the context is replied.
+ */
+ virtual const TSharedRefArray& GetResponseMessage() const = 0;
+
+ //! Returns the error that was previously set by #Reply.
+ /*!
+ * Can only be called after the context is replied.
+ */
+ virtual const TError& GetError() const = 0;
+
+ //! Returns the request body.
+ virtual TSharedRef GetRequestBody() const = 0;
+
+ //! Returns the response body.
+ virtual TSharedRef GetResponseBody() = 0;
+
+ //! Sets the response body.
+ virtual void SetResponseBody(const TSharedRef& responseBody) = 0;
+
+ //! Returns a vector of request attachments.
+ virtual std::vector<TSharedRef>& RequestAttachments() = 0;
+
+ //! Returns the stream of asynchronous request attachments.
+ virtual NConcurrency::IAsyncZeroCopyInputStreamPtr GetRequestAttachmentsStream() = 0;
+
+ //! Returns a vector of response attachments.
+ virtual std::vector<TSharedRef>& ResponseAttachments() = 0;
+
+ //! Returns the stream of asynchronous response attachments.
+ virtual NConcurrency::IAsyncZeroCopyOutputStreamPtr GetResponseAttachmentsStream() = 0;
+
+ //! Returns immutable request header.
+ virtual const NProto::TRequestHeader& RequestHeader() const = 0;
+
+ //! Returns mutable request header.
+ virtual NProto::TRequestHeader& RequestHeader() = 0;
+
+ //! Returns true if request/response info logging is enabled.
+ virtual bool IsLoggingEnabled() const = 0;
+
+ //! Registers a portion of request logging info.
+ /*!
+ * \param incremental If true then \p info is just remembered but no logging happens.
+ * If false then all remembered infos are logged (comma-separated).
+ * This must be the last call to #SetRawRequestInfo for this context.
+ *
+ * Passing empty \p info in incremental mode is no-op.
+ * Passing empty \p info in non-incremental mode flushes the logging message.
+ */
+ virtual void SetRawRequestInfo(TString info, bool incremental) = 0;
+
+ //! After this call there is no obligation to set request info for this request.
+ virtual void SuppressMissingRequestInfoCheck() = 0;
+
+ //! Registers a portion of response logging info.
+ /*!
+ * \param incremental If false then \p info overrides all previously remembered infos.
+ * These infos are logged (comma-separated) when the context is replied.
+ *
+ * Passing empty \p info in incremental mode is no-op.
+ * Passing empty \p info in non-incremental mode clears the logging infos.
+ */
+ virtual void SetRawResponseInfo(TString info, bool incremental) = 0;
+
+ //! Returns the logger for request/response messages.
+ virtual const NLogging::TLogger& GetLogger() const = 0;
+
+ //! Returns the logging level for request/response messages.
+ virtual NLogging::ELogLevel GetLogLevel() const = 0;
+
+ //! Returns |true| if requests and responses are pooled.
+ virtual bool IsPooled() const = 0;
+
+ //! Returns the currently configured response codec.
+ virtual NCompression::ECodec GetResponseCodec() const = 0;
+
+ //! Changes the response codec.
+ virtual void SetResponseCodec(NCompression::ECodec codec) = 0;
+
+ // Extension methods.
+
+ void SetRequestInfo();
+ void SetResponseInfo();
+
+ template <class... TArgs>
+ void SetRequestInfo(const char* format, TArgs&&... args);
+
+ template <class... TArgs>
+ void SetIncrementalRequestInfo(const char* format, TArgs&&... args);
+
+ template <class... TArgs>
+ void SetResponseInfo(const char* format, TArgs&&... args);
+
+ template <class... TArgs>
+ void SetIncrementalResponseInfo(const char* format, TArgs&&... args);
+
+ //! Replies with a given message when the latter is set.
+ void ReplyFrom(TFuture<TSharedRefArray> asyncMessage);
+
+ //! Replies with a given error when the latter is set.
+ void ReplyFrom(TFuture<void> asyncError);
+
+ //! Replies with a given error when the latter is set via the #invoker.
+ void ReplyFrom(TFuture<void> asyncError, const IInvokerPtr& invoker);
+
+ template <class E>
+ bool IsClientFeatureSupported(E featureId) const;
+ template <class E>
+ void ValidateClientFeature(E featureId) const;
+};
+
+DEFINE_REFCOUNTED_TYPE(IServiceContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TServiceId
+{
+ TServiceId() = default;
+ TServiceId(const TString& serviceName, TRealmId realmId = NullRealmId);
+
+ TString ServiceName;
+ TRealmId RealmId;
+};
+
+bool operator == (const TServiceId& lhs, const TServiceId& rhs);
+bool operator != (const TServiceId& lhs, const TServiceId& rhs);
+
+TString ToString(const TServiceId& serviceId);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents an abstract service registered within IServer.
+/*!
+ * \note
+ * Implementations must be fully thread-safe.
+ */
+struct IService
+ : public virtual TRefCounted
+{
+ //! Applies a new configuration with a given defaults.
+ virtual void Configure(
+ const TServiceCommonConfigPtr& configDefaults,
+ const NYTree::INodePtr& config) = 0;
+
+ //! Stops the service forbidding new requests to be served
+ //! and returns the future that is set when all currently
+ //! executing requests are finished.
+ virtual TFuture<void> Stop() = 0;
+
+ //! Returns the service id.
+ virtual const TServiceId& GetServiceId() const = 0;
+
+ //! Handles incoming request.
+ virtual void HandleRequest(
+ std::unique_ptr<NProto::TRequestHeader> header,
+ TSharedRefArray message,
+ NYT::NBus::IBusPtr replyBus) = 0;
+
+ //! Handles request cancelation.
+ virtual void HandleRequestCancellation(
+ TRequestId requestId) = 0;
+
+ //! Enables passing streaming data from clients to the service.
+ virtual void HandleStreamingPayload(
+ TRequestId requestId,
+ const TStreamingPayload& payload) = 0;
+
+ //! Enables clients to notify the service about their progress in receiving streaming data.
+ virtual void HandleStreamingFeedback(
+ TRequestId requestId,
+ const TStreamingFeedback& feedback) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IService)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+//! A hasher for TServiceId.
+template <>
+struct THash<NYT::NRpc::TServiceId>
+{
+ inline size_t operator()(const NYT::NRpc::TServiceId& id) const
+ {
+ return
+ THash<TString>()(id.ServiceName) * 497 +
+ THash<NYT::NRpc::TRealmId>()(id.RealmId);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define SERVICE_INL_H_
+#include "service-inl.h"
+#undef SERVICE_INL_H_
diff --git a/yt/yt/core/rpc/service_detail-inl.h b/yt/yt/core/rpc/service_detail-inl.h
new file mode 100644
index 0000000000..2566351616
--- /dev/null
+++ b/yt/yt/core/rpc/service_detail-inl.h
@@ -0,0 +1,21 @@
+#ifndef SERVICE_DETAIL_INL_H_
+#error "Direct inclusion of this file is not allowed, include service_detail.h"
+// For the sake of sane code completion.
+#include "service_detail.h"
+#endif
+
+#include "helpers.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+void TServiceBase::DeclareServerFeature(E featureId)
+{
+ DoDeclareServerFeature(FeatureIdToInt(featureId));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/service_detail.cpp b/yt/yt/core/rpc/service_detail.cpp
new file mode 100644
index 0000000000..72b19ecbd8
--- /dev/null
+++ b/yt/yt/core/rpc/service_detail.cpp
@@ -0,0 +1,2456 @@
+#include "service_detail.h"
+#include "private.h"
+#include "authenticator.h"
+#include "authentication_identity.h"
+#include "config.h"
+#include "dispatcher.h"
+#include "helpers.h"
+#include "message.h"
+#include "request_queue_provider.h"
+#include "response_keeper.h"
+#include "server_detail.h"
+#include "stream.h"
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/lease_manager.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+#include <yt/yt/core/concurrency/thread_affinity.h>
+#include <yt/yt/core/concurrency/throughput_throttler.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/logging/log_manager.h>
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/local_address.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NRpc {
+
+using namespace NBus;
+using namespace NYPath;
+using namespace NYTree;
+using namespace NYson;
+using namespace NTracing;
+using namespace NConcurrency;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto InfiniteRequestThrottlerConfig = New<TThroughputThrottlerConfig>();
+static const auto DefaultLoggingSuppressionFailedRequestThrottlerConfig = TThroughputThrottlerConfig::Create(1'000);
+
+constexpr TDuration ServiceLivenessCheckPeriod = TDuration::MilliSeconds(100);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRequestQueuePtr CreateRequestQueue(TString name, const NProfiling::TProfiler& profiler)
+{
+ return New<TRequestQueue>(name, profiler.WithTag("user", name));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+THandlerInvocationOptions THandlerInvocationOptions::SetHeavy(bool value) const
+{
+ auto result = *this;
+ result.Heavy = value;
+ return result;
+}
+
+THandlerInvocationOptions THandlerInvocationOptions::SetResponseCodec(NCompression::ECodec value) const
+{
+ auto result = *this;
+ result.ResponseCodec = value;
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceBase::TMethodDescriptor::TMethodDescriptor(
+ TString method,
+ TLiteHandler liteHandler,
+ THeavyHandler heavyHandler)
+ : Method(std::move(method))
+ , LiteHandler(std::move(liteHandler))
+ , HeavyHandler(std::move(heavyHandler))
+{ }
+
+auto TServiceBase::TMethodDescriptor::SetRequestQueueProvider(IRequestQueueProviderPtr value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.RequestQueueProvider = std::move(value);
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetInvoker(IInvokerPtr value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.Invoker = std::move(value);
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetInvokerProvider(TInvokerProvider value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.InvokerProvider = std::move(value);
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetHeavy(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.Options.Heavy = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetResponseCodec(NCompression::ECodec value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.Options.ResponseCodec = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetQueueSizeLimit(int value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.QueueSizeLimit = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetConcurrencyLimit(int value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.ConcurrencyLimit = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetSystem(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.System = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetLogLevel(NLogging::ELogLevel value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.LogLevel = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetLoggingSuppressionTimeout(TDuration value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.LoggingSuppressionTimeout = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetCancelable(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.Cancelable = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetGenerateAttachmentChecksums(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.GenerateAttachmentChecksums = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetStreamingEnabled(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.StreamingEnabled = value;
+ return result;
+}
+
+auto TServiceBase::TMethodDescriptor::SetPooled(bool value) const -> TMethodDescriptor
+{
+ auto result = *this;
+ result.Pooled = value;
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceBase::TMethodPerformanceCounters::TMethodPerformanceCounters(
+ const NProfiling::TProfiler& profiler,
+ const THistogramConfigPtr& histogramConfig)
+ : RequestCounter(profiler.Counter("/request_count"))
+ , CanceledRequestCounter(profiler.Counter("/canceled_request_count"))
+ , FailedRequestCounter(profiler.Counter("/failed_request_count"))
+ , TimedOutRequestCounter(profiler.Counter("/timed_out_request_count"))
+ , TraceContextTimeCounter(profiler.TimeCounter("/request_time/trace_context"))
+ , RequestMessageBodySizeCounter(profiler.Counter("/request_message_body_bytes"))
+ , RequestMessageAttachmentSizeCounter(profiler.Counter("/request_message_attachment_bytes"))
+ , ResponseMessageBodySizeCounter(profiler.Counter("/response_message_body_bytes"))
+ , ResponseMessageAttachmentSizeCounter(profiler.Counter("/response_message_attachment_bytes"))
+ , ErrorCodes(profiler)
+{
+ if (histogramConfig && histogramConfig->CustomBounds) {
+ const auto &customBounds = *histogramConfig->CustomBounds;
+ ExecutionTimeCounter = profiler.Histogram("/request_time_histogram/execution", customBounds);
+ RemoteWaitTimeCounter = profiler.Histogram("/request_time_histogram/remote_wait", customBounds);
+ LocalWaitTimeCounter = profiler.Histogram("/request_time_histogram/local_wait", customBounds);
+ TotalTimeCounter = profiler.Histogram("/request_time_histogram/total", customBounds);
+ } else if (histogramConfig && histogramConfig->ExponentialBounds) {
+ const auto &exponentialBounds = *histogramConfig->ExponentialBounds;
+ ExecutionTimeCounter = profiler.Histogram("/request_time_histogram/execution", exponentialBounds->Min, exponentialBounds->Max);
+ RemoteWaitTimeCounter = profiler.Histogram("/request_time_histogram/remote_wait", exponentialBounds->Min, exponentialBounds->Max);
+ LocalWaitTimeCounter = profiler.Histogram("/request_time_histogram/local_wait", exponentialBounds->Min, exponentialBounds->Max);
+ TotalTimeCounter = profiler.Histogram("/request_time_histogram/total", exponentialBounds->Min, exponentialBounds->Max);
+ } else {
+ ExecutionTimeCounter = profiler.Timer("/request_time/execution");
+ RemoteWaitTimeCounter = profiler.Timer("/request_time/remote_wait");
+ LocalWaitTimeCounter = profiler.Timer("/request_time/local_wait");
+ TotalTimeCounter = profiler.Timer("/request_time/total");
+ }
+}
+
+TServiceBase::TRuntimeMethodInfo::TRuntimeMethodInfo(
+ TServiceId serviceId,
+ TMethodDescriptor descriptor,
+ const NProfiling::TProfiler& profiler)
+ : ServiceId(std::move(serviceId))
+ , Descriptor(std::move(descriptor))
+ , Profiler(profiler.WithTag("method", Descriptor.Method, -1))
+ , DefaultRequestQueue(CreateRequestQueue("default"))
+ , RequestLoggingAnchor(NLogging::TLogManager::Get()->RegisterDynamicAnchor(
+ Format("%v.%v <-", ServiceId.ServiceName, Descriptor.Method)))
+ , ResponseLoggingAnchor(NLogging::TLogManager::Get()->RegisterDynamicAnchor(
+ Format("%v.%v ->", ServiceId.ServiceName, Descriptor.Method)))
+ , RequestQueueSizeLimitErrorCounter(Profiler.Counter("/request_queue_size_errors"))
+ , UnauthenticatedRequestsCounter(Profiler.Counter("/unauthenticated_requests"))
+ , LoggingSuppressionFailedRequestThrottler(
+ CreateReconfigurableThroughputThrottler(
+ DefaultLoggingSuppressionFailedRequestThrottlerConfig))
+{ }
+
+TRequestQueue* TServiceBase::TRuntimeMethodInfo::GetDefaultRequestQueue()
+{
+ return DefaultRequestQueue.Get();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceBase::TServiceContext
+ : public TServiceContextBase
+{
+public:
+ TServiceContext(
+ TServiceBasePtr&& service,
+ TAcceptedRequest&& acceptedRequest,
+ NLogging::TLogger logger)
+ : TServiceContextBase(
+ std::move(acceptedRequest.Header),
+ std::move(acceptedRequest.Message),
+ std::move(logger),
+ acceptedRequest.RuntimeInfo->LogLevel.load(std::memory_order::relaxed))
+ , Service_(service)
+ , RequestId_(acceptedRequest.RequestId)
+ , ReplyBus_(std::move(acceptedRequest.ReplyBus))
+ , RuntimeInfo_(acceptedRequest.RuntimeInfo)
+ , TraceContext_(std::move(acceptedRequest.TraceContext))
+ , RequestQueue_(acceptedRequest.RequestQueue)
+ , MethodPerformanceCounters_(Service_->GetMethodPerformanceCounters(
+ RuntimeInfo_,
+ {GetAuthenticationIdentity().UserTag, RequestQueue_}))
+ , PerformanceCounters_(Service_->GetPerformanceCounters())
+ , ArriveInstant_(NProfiling::GetInstant())
+
+ {
+ YT_ASSERT(RequestMessage_);
+ YT_ASSERT(ReplyBus_);
+ YT_ASSERT(Service_);
+ YT_ASSERT(RuntimeInfo_);
+
+ Initialize();
+ }
+
+ ~TServiceContext()
+ {
+ if (!Replied_ && !CanceledList_.IsFired()) {
+ // Prevent alerting.
+ RequestInfoSet_ = true;
+ Reply(TError(NRpc::EErrorCode::Unavailable, "Service is unable to complete your request"));
+ }
+
+ Finish();
+ }
+
+ TRequestQueue* GetRequestQueue() const
+ {
+ return RequestQueue_;
+ }
+
+ bool IsPooled() const override
+ {
+ return RuntimeInfo_->Pooled.load();
+ }
+
+ TBusNetworkStatistics GetBusNetworkStatistics() const override
+ {
+ return ReplyBus_->GetNetworkStatistics();
+ }
+
+ const IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ return ReplyBus_->GetEndpointAttributes();
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return ReplyBus_->GetEndpointDescription();
+ }
+
+ TRuntimeMethodInfo* GetRuntimeInfo() const
+ {
+ return RuntimeInfo_;
+ }
+
+ const IBusPtr& GetReplyBus() const
+ {
+ return ReplyBus_;
+ }
+
+ void CheckAndRun(const TErrorOr<TLiteHandler>& handlerOrError)
+ {
+ if (!handlerOrError.IsOK()) {
+ Reply(TError(handlerOrError));
+ return;
+ }
+
+ const auto& handler = handlerOrError.Value();
+ if (!handler) {
+ return;
+ }
+
+ Run(handler);
+ }
+
+ void Run(const TLiteHandler& handler)
+ {
+ RequestStarted_ = true;
+ const auto& descriptor = RuntimeInfo_->Descriptor;
+ // NB: Try to avoid contention on invoker ref-counter.
+ IInvoker* invoker = nullptr;
+ IInvokerPtr invokerHolder;
+ if (descriptor.InvokerProvider) {
+ invokerHolder = descriptor.InvokerProvider(RequestHeader());
+ invoker = invokerHolder.Get();
+ }
+ if (!invoker) {
+ invoker = descriptor.Invoker.Get();
+ }
+ if (!invoker) {
+ invoker = Service_->DefaultInvoker_.Get();
+ }
+ invoker->Invoke(BIND(&TServiceContext::DoRun, MakeStrong(this), handler));
+ }
+
+ void SubscribeCanceled(const TClosure& callback) override
+ {
+ CanceledList_.Subscribe(callback);
+ }
+
+ void UnsubscribeCanceled(const TClosure& callback) override
+ {
+ CanceledList_.Unsubscribe(callback);
+ }
+
+ void SubscribeReplied(const TClosure& callback) override
+ {
+ RepliedList_.Subscribe(callback);
+ }
+
+ void UnsubscribeReplied(const TClosure& callback) override
+ {
+ RepliedList_.Unsubscribe(callback);
+ }
+
+ bool IsCanceled() const override
+ {
+ return CanceledList_.IsFired();
+ }
+
+ void Cancel() override
+ {
+ if (!CanceledList_.Fire()) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Request canceled (RequestId: %v)",
+ RequestId_);
+
+ if (RuntimeInfo_->Descriptor.StreamingEnabled) {
+ static const auto CanceledError = TError("Request canceled");
+ AbortStreamsUnlessClosed(CanceledError);
+ }
+
+ CancelInstant_ = NProfiling::GetInstant();
+
+ MethodPerformanceCounters_->CanceledRequestCounter.Increment();
+ }
+
+ void SetComplete() override
+ {
+ DoSetComplete();
+ }
+
+ void HandleTimeout()
+ {
+ if (TimedOutLatch_.exchange(true)) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Request timed out, canceling (RequestId: %v)",
+ RequestId_);
+
+ if (RuntimeInfo_->Descriptor.StreamingEnabled) {
+ static const auto TimedOutError = TError("Request timed out");
+ AbortStreamsUnlessClosed(TimedOutError);
+ }
+
+ CanceledList_.Fire();
+ MethodPerformanceCounters_->TimedOutRequestCounter.Increment();
+
+ // Guards from race with DoGuardedRun.
+ // We can only mark as complete those requests that will not be run
+ // as there's no guarantee that, if started, the method handler will respond promptly to cancelation.
+ if (!RunLatch_.exchange(true)) {
+ SetComplete();
+ }
+ }
+
+ TInstant GetArriveInstant() const override
+ {
+ return ArriveInstant_;
+ }
+
+ std::optional<TInstant> GetRunInstant() const override
+ {
+ return RunInstant_;
+ }
+
+ std::optional<TInstant> GetFinishInstant() const override
+ {
+ if (ReplyInstant_) {
+ return ReplyInstant_;
+ } else if (CancelInstant_) {
+ return CancelInstant_;
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ std::optional<TDuration> GetWaitDuration() const override
+ {
+ return LocalWaitTime_;
+ }
+
+ std::optional<TDuration> GetExecutionDuration() const override
+ {
+ return ExecutionTime_;
+ }
+
+ TTraceContextPtr GetTraceContext() const override
+ {
+ return TraceContext_;
+ }
+
+ IAsyncZeroCopyInputStreamPtr GetRequestAttachmentsStream() override
+ {
+ if (!RuntimeInfo_->Descriptor.StreamingEnabled) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::StreamingNotSupported, "Streaming is not supported");
+ }
+ CreateRequestAttachmentsStream();
+ return RequestAttachmentsStream_;
+ }
+
+ void SetResponseCodec(NCompression::ECodec codec) override
+ {
+ auto guard = Guard(StreamsLock_);
+ if (ResponseAttachmentsStream_) {
+ THROW_ERROR_EXCEPTION("Cannot update response codec after response attachments stream is accessed");
+ }
+ TServiceContextBase::SetResponseCodec(codec);
+ }
+
+ IAsyncZeroCopyOutputStreamPtr GetResponseAttachmentsStream() override
+ {
+ if (!RuntimeInfo_->Descriptor.StreamingEnabled) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::StreamingNotSupported, "Streaming is not supported");
+ }
+ CreateResponseAttachmentsStream();
+ return ResponseAttachmentsStream_;
+ }
+
+ void HandleStreamingPayload(const TStreamingPayload& payload)
+ {
+ if (!RuntimeInfo_->Descriptor.StreamingEnabled) {
+ YT_LOG_DEBUG("Received streaming payload for a method that does not support streaming; ignored "
+ "(Method: %v.%v, RequestId: %v)",
+ Service_->ServiceId_.ServiceName,
+ RuntimeInfo_->Descriptor.Method,
+ RequestId_);
+ return;
+ }
+ CreateRequestAttachmentsStream();
+ try {
+ RequestAttachmentsStream_->EnqueuePayload(payload);
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Error handling streaming payload (RequestId: %v)",
+ RequestId_);
+ RequestAttachmentsStream_->Abort(ex);
+ }
+ }
+
+ void HandleStreamingFeedback(const TStreamingFeedback& feedback)
+ {
+ TAttachmentsOutputStreamPtr stream;
+ {
+ auto guard = Guard(StreamsLock_);
+ stream = ResponseAttachmentsStream_;
+ }
+
+ if (!stream) {
+ YT_LOG_DEBUG("Received streaming feedback for a method that does not support streaming; ignored "
+ "(Method: %v.%v, RequestId: %v)",
+ Service_->ServiceId_.ServiceName,
+ RuntimeInfo_->Descriptor.Method,
+ RequestId_);
+ return;
+ }
+
+ try {
+ stream->HandleFeedback(feedback);
+ } catch (const std::exception& ex) {
+ YT_LOG_DEBUG(ex, "Error handling streaming feedback (RequestId: %v)",
+ RequestId_);
+ stream->Abort(ex);
+ }
+ }
+
+private:
+ const TServiceBasePtr Service_;
+ const TRequestId RequestId_;
+ const IBusPtr ReplyBus_;
+ TRuntimeMethodInfo* const RuntimeInfo_;
+ const TTraceContextPtr TraceContext_;
+ TRequestQueue* const RequestQueue_;
+ TMethodPerformanceCounters* const MethodPerformanceCounters_;
+ TPerformanceCounters* const PerformanceCounters_;
+
+ NCompression::ECodec RequestCodec_;
+
+ TDelayedExecutorCookie TimeoutCookie_;
+
+ bool Cancelable_ = false;
+ TSingleShotCallbackList<void()> CanceledList_;
+
+ const TInstant ArriveInstant_;
+ std::optional<TInstant> RunInstant_;
+ std::optional<TInstant> ReplyInstant_;
+ std::optional<TInstant> CancelInstant_;
+
+ std::optional<TDuration> ExecutionTime_;
+ std::optional<TDuration> TotalTime_;
+ std::optional<TDuration> LocalWaitTime_;
+
+ std::atomic<bool> CompletedLatch_ = false;
+ std::atomic<bool> TimedOutLatch_ = false;
+ std::atomic<bool> RunLatch_ = false;
+ bool FinishLatch_ = false;
+ bool RequestStarted_ = false;
+ bool ActiveRequestCountIncremented_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, StreamsLock_);
+ TError StreamsError_;
+ TAttachmentsInputStreamPtr RequestAttachmentsStream_;
+ TAttachmentsOutputStreamPtr ResponseAttachmentsStream_;
+
+
+ bool IsRegistrable()
+ {
+ if (RuntimeInfo_->Descriptor.Cancelable && !RequestHeader_->uncancelable()) {
+ return true;
+ }
+
+ if (RuntimeInfo_->Descriptor.StreamingEnabled) {
+ return true;
+ }
+
+ return false;
+ }
+
+ void Initialize()
+ {
+ constexpr TStringBuf UnknownUserAgent = "unknown";
+ auto userAgent = RequestHeader_->has_user_agent()
+ ? TStringBuf(RequestHeader_->user_agent())
+ : UnknownUserAgent;
+ PerformanceCounters_->IncrementRequestsPerUserAgent(userAgent);
+
+ MethodPerformanceCounters_->RequestCounter.Increment();
+ MethodPerformanceCounters_->RequestMessageBodySizeCounter.Increment(
+ GetMessageBodySize(RequestMessage_));
+ MethodPerformanceCounters_->RequestMessageAttachmentSizeCounter.Increment(
+ GetTotalMessageAttachmentSize(RequestMessage_));
+
+ if (RequestHeader_->has_start_time()) {
+ auto retryStart = FromProto<TInstant>(RequestHeader_->start_time());
+ auto now = NProfiling::GetInstant();
+ MethodPerformanceCounters_->RemoteWaitTimeCounter.Record(now - retryStart);
+ }
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ if (RequestHeader_->has_request_codec()) {
+ int intRequestCodecId = RequestHeader_->request_codec();
+ if (!TryEnumCast(intRequestCodecId, &RequestCodec_)) {
+ Reply(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Request codec %v is not supported",
+ intRequestCodecId));
+ return;
+ }
+ } else {
+ RequestCodec_ = NCompression::ECodec::None;
+ }
+
+ if (RequestHeader_->has_response_codec()) {
+ int intResponseCodecId = RequestHeader_->response_codec();
+ if (!TryEnumCast(intResponseCodecId, &ResponseCodec_)) {
+ Reply(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Response codec %v is not supported",
+ intResponseCodecId));
+ return;
+ }
+ } else {
+ ResponseCodec_ = NCompression::ECodec::None;
+ }
+
+ Service_->IncrementActiveRequestCount();
+ ActiveRequestCountIncremented_ = true;
+
+ BuildGlobalRequestInfo();
+
+ if (IsRegistrable()) {
+ Service_->RegisterRequest(this);
+ }
+
+ Cancelable_ = RuntimeInfo_->Descriptor.Cancelable && !RequestHeader_->uncancelable();
+ }
+
+ void BuildGlobalRequestInfo()
+ {
+ TStringBuilder builder;
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+
+ if (RequestHeader_->has_request_id()) {
+ delimitedBuilder->AppendFormat("RequestId: %v", FromProto<TRequestId>(RequestHeader_->request_id()));
+ }
+
+ if (RequestHeader_->has_realm_id()) {
+ delimitedBuilder->AppendFormat("RealmId: %v", FromProto<TRealmId>(RequestHeader_->realm_id()));
+ }
+
+ if (RequestHeader_->has_user()) {
+ delimitedBuilder->AppendFormat("User: %v", RequestHeader_->user());
+ }
+
+ if (RequestHeader_->has_user_tag() && RequestHeader_->user_tag() != RequestHeader_->user()) {
+ delimitedBuilder->AppendFormat("UserTag: %v", RequestHeader_->user_tag());
+ }
+
+ if (RequestHeader_->has_mutation_id()) {
+ delimitedBuilder->AppendFormat("MutationId: %v", FromProto<TMutationId>(RequestHeader_->mutation_id()));
+ }
+
+ if (RequestHeader_->has_start_time()) {
+ delimitedBuilder->AppendFormat("StartTime: %v", FromProto<TInstant>(RequestHeader_->start_time()));
+ }
+
+ delimitedBuilder->AppendFormat("Retry: %v", RequestHeader_->retry());
+
+ if (RequestHeader_->has_user_agent()) {
+ delimitedBuilder->AppendFormat("UserAgent: %v", RequestHeader_->user_agent());
+ }
+
+ if (RequestHeader_->has_timeout()) {
+ delimitedBuilder->AppendFormat("Timeout: %v", FromProto<TDuration>(RequestHeader_->timeout()));
+ }
+
+ if (RequestHeader_->tos_level() != NBus::DefaultTosLevel) {
+ delimitedBuilder->AppendFormat("TosLevel: %x", RequestHeader_->tos_level());
+ }
+
+ delimitedBuilder->AppendFormat("Endpoint: %v", ReplyBus_->GetEndpointDescription());
+
+ delimitedBuilder->AppendFormat("BodySize: %v, AttachmentsSize: %v/%v",
+ GetMessageBodySize(RequestMessage_),
+ GetTotalMessageAttachmentSize(RequestMessage_),
+ GetMessageAttachmentCount(RequestMessage_));
+
+ // COMPAT(kiselyovp)
+ if (RequestHeader_->has_request_codec() && RequestHeader_->has_response_codec()) {
+ delimitedBuilder->AppendFormat("RequestCodec: %v, ResponseCodec: %v",
+ RequestCodec_,
+ ResponseCodec_);
+ }
+
+ RequestInfos_.push_back(builder.Flush());
+ }
+
+ void Finish()
+ {
+ // Finish is called from DoReply and ~TServiceContext.
+ // Clearly there could be no race between these two and thus no atomics are needed.
+ if (FinishLatch_) {
+ return;
+ }
+ FinishLatch_ = true;
+
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+
+ if (IsRegistrable()) {
+ Service_->UnregisterRequest(this);
+ }
+
+ if (RuntimeInfo_->Descriptor.StreamingEnabled) {
+ static const auto FinishedError = TError("Request finished");
+ AbortStreamsUnlessClosed(Error_.IsOK() ? Error_ : FinishedError);
+ }
+
+ DoSetComplete();
+ }
+
+ void AbortStreamsUnlessClosed(const TError& error)
+ {
+ auto guard = Guard(StreamsLock_);
+
+ if (!StreamsError_.IsOK()) {
+ return;
+ }
+
+ StreamsError_ = error;
+
+ auto requestAttachmentsStream = RequestAttachmentsStream_;
+ auto responseAttachmentsStream = ResponseAttachmentsStream_;
+
+ guard.Release();
+
+ if (requestAttachmentsStream) {
+ requestAttachmentsStream->AbortUnlessClosed(Error_);
+ }
+
+ if (responseAttachmentsStream) {
+ responseAttachmentsStream->AbortUnlessClosed(Error_);
+ }
+ }
+
+
+ void DoRun(const TLiteHandler& handler)
+ {
+ RunInstant_ = NProfiling::GetInstant();
+ LocalWaitTime_ = *RunInstant_ - ArriveInstant_;
+ MethodPerformanceCounters_->LocalWaitTimeCounter.Record(*LocalWaitTime_);
+
+ try {
+ TCurrentTraceContextGuard guard(TraceContext_);
+ DoGuardedRun(handler);
+ } catch (const std::exception& ex) {
+ Reply(ex);
+ }
+ }
+
+ void DoGuardedRun(const TLiteHandler& handler)
+ {
+ const auto& descriptor = RuntimeInfo_->Descriptor;
+
+ if (Service_->IsStopped()) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable, "Service is stopped");
+ }
+
+ if (!descriptor.System) {
+ Service_->BeforeInvoke(this);
+ }
+
+ if (auto timeout = GetTimeout()) {
+ auto remainingTimeout = *timeout - (*RunInstant_ - ArriveInstant_);
+ if (remainingTimeout == TDuration::Zero()) {
+ if (!TimedOutLatch_.exchange(true)) {
+ Reply(TError(NYT::EErrorCode::Timeout, "Request dropped due to timeout before being run"));
+ MethodPerformanceCounters_->TimedOutRequestCounter.Increment();
+ }
+ return;
+ }
+ if (Cancelable_) {
+ TimeoutCookie_ = TDelayedExecutor::Submit(
+ BIND(&TServiceBase::OnRequestTimeout, Service_, RequestId_),
+ remainingTimeout);
+ }
+ }
+
+ if (Cancelable_) {
+ // TODO(lukyan): Wrap in CancelableExecution.
+ auto fiberCanceler = GetCurrentFiberCanceler();
+ if (fiberCanceler) {
+ auto cancelationHandler = BIND([fiberCanceler = std::move(fiberCanceler)] {
+ fiberCanceler(TError("RPC request canceled"));
+ });
+ if (!CanceledList_.TrySubscribe(std::move(cancelationHandler))) {
+ YT_LOG_DEBUG("Request was canceled before being run (RequestId: %v)",
+ RequestId_);
+ return;
+ }
+ }
+ }
+
+ // Guards from race with HandleTimeout.
+ if (RunLatch_.exchange(true)) {
+ return;
+ }
+
+ {
+ TCurrentAuthenticationIdentityGuard identityGuard(&GetAuthenticationIdentity());
+ handler(this, descriptor.Options);
+ }
+ }
+
+ std::optional<TDuration> GetTraceContextTime() const override
+ {
+ if (TraceContext_) {
+ FlushCurrentTraceContextElapsedTime();
+ return TraceContext_->GetElapsedTime();
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ void DoReply() override
+ {
+ auto responseMessage = GetResponseMessage();
+
+ NBus::TSendOptions busOptions;
+ busOptions.TrackingLevel = EDeliveryTrackingLevel::None;
+ busOptions.ChecksummedPartCount = RuntimeInfo_->Descriptor.GenerateAttachmentChecksums
+ ? NBus::TSendOptions::AllParts
+ : 2; // RPC header + response body
+ busOptions.EnableSendCancelation = Cancelable_;
+
+ auto replySent = ReplyBus_->Send(responseMessage, busOptions);
+ if (Cancelable_ && replySent) {
+ if (auto timeout = GetTimeout()) {
+ auto timeoutCookie = TDelayedExecutor::Submit(
+ BIND([replySent] {
+ replySent.Cancel(TError());
+ }),
+ ArriveInstant_ + *timeout);
+
+ replySent.Subscribe(BIND([timeoutCookie] (const TError& /*error*/) {
+ TDelayedExecutor::Cancel(timeoutCookie);
+ }));
+ }
+
+ Service_->RegisterQueuedReply(RequestId_, replySent);
+ replySent.Subscribe(BIND([weakService=MakeWeak(Service_), requestId=RequestId_] (const TError& /*error*/) {
+ if (auto service = weakService.Lock()) {
+ service->UnregisterQueuedReply(requestId);
+ }
+ }));
+ }
+
+ if (auto traceContextTime = GetTraceContextTime()) {
+ MethodPerformanceCounters_->TraceContextTimeCounter.Add(*traceContextTime);
+ }
+
+ ReplyInstant_ = NProfiling::GetInstant();
+ ExecutionTime_ = RunInstant_ ? *ReplyInstant_ - *RunInstant_ : TDuration();
+ TotalTime_ = *ReplyInstant_ - ArriveInstant_;
+
+ MethodPerformanceCounters_->ExecutionTimeCounter.Record(*ExecutionTime_);
+ MethodPerformanceCounters_->TotalTimeCounter.Record(*TotalTime_);
+ if (!Error_.IsOK()) {
+ if (Service_->EnableErrorCodeCounting.load()) {
+ MethodPerformanceCounters_->ErrorCodes.RegisterCode(Error_.GetNonTrivialCode());
+ } else {
+ MethodPerformanceCounters_->FailedRequestCounter.Increment();
+ }
+ }
+ HandleLoggingSuppression();
+
+ MethodPerformanceCounters_->ResponseMessageBodySizeCounter.Increment(
+ GetMessageBodySize(responseMessage));
+ MethodPerformanceCounters_->ResponseMessageAttachmentSizeCounter.Increment(
+ GetTotalMessageAttachmentSize(responseMessage));
+
+ if (!Error_.IsOK() && TraceContext_ && TraceContext_->IsRecorded()) {
+ TraceContext_->AddErrorTag();
+ }
+
+ Finish();
+ }
+
+ void DoFlush() override
+ {
+ if (TraceContext_) {
+ TraceContext_->Finish();
+ }
+ }
+
+ void HandleLoggingSuppression()
+ {
+ auto timeout = RequestHeader_->has_logging_suppression_timeout()
+ ? FromProto<TDuration>(RequestHeader_->logging_suppression_timeout())
+ : RuntimeInfo_->LoggingSuppressionTimeout.load(std::memory_order::relaxed);
+
+ if (*TotalTime_ >= timeout) {
+ return;
+ }
+
+ if (!Error_.IsOK() &&
+ (RequestHeader_->disable_logging_suppression_if_request_failed() ||
+ RuntimeInfo_->LoggingSuppressionFailedRequestThrottler->TryAcquire(1)))
+ {
+ return;
+ }
+
+ {
+ TNullTraceContextGuard nullGuard;
+ YT_LOG_DEBUG("Request logging suppressed (RequestId: %v)", GetRequestId());
+ }
+ NLogging::TLogManager::Get()->SuppressRequest(GetRequestId());
+ }
+
+ void DoSetComplete()
+ {
+ // DoSetComplete could be called from anywhere so it is racy.
+ if (CompletedLatch_.exchange(true)) {
+ return;
+ }
+
+ if (RequestStarted_) {
+ RequestQueue_->OnRequestFinished();
+ }
+
+
+ if (ActiveRequestCountIncremented_) {
+ Service_->DecrementActiveRequestCount();
+ }
+ }
+
+
+ void LogRequest() override
+ {
+ TStringBuilder builder;
+ builder.AppendFormat("%v.%v <- ",
+ GetService(),
+ GetMethod());
+
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+
+ for (const auto& info : RequestInfos_) {
+ delimitedBuilder->AppendString(info);
+ }
+
+ if (RuntimeInfo_->Descriptor.Cancelable && !Cancelable_) {
+ delimitedBuilder->AppendFormat("Cancelable: %v", Cancelable_);
+ }
+
+ auto logMessage = builder.Flush();
+ if (TraceContext_ && TraceContext_->IsRecorded()) {
+ TraceContext_->AddTag(RequestInfoAnnotation, logMessage);
+ const auto& authenticationIdentity = GetAuthenticationIdentity();
+ if (authenticationIdentity.User) {
+ TStringBuilder builder;
+ builder.AppendString(authenticationIdentity.User);
+ if (authenticationIdentity.UserTag && authenticationIdentity.UserTag != authenticationIdentity.User) {
+ builder.AppendChar(':');
+ builder.AppendString(authenticationIdentity.UserTag);
+ }
+ TraceContext_->AddTag(RequestUser, builder.Flush());
+ }
+ }
+ YT_LOG_EVENT_WITH_ANCHOR(Logger, LogLevel_, RuntimeInfo_->RequestLoggingAnchor, logMessage);
+ }
+
+ void LogResponse() override
+ {
+ TStringBuilder builder;
+ builder.AppendFormat("%v.%v -> ",
+ GetService(),
+ GetMethod());
+
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+
+ if (RequestId_) {
+ delimitedBuilder->AppendFormat("RequestId: %v", RequestId_);
+ }
+
+ if (RequestHeader_->has_user()) {
+ delimitedBuilder->AppendFormat("User: %v", RequestHeader_->user());
+ }
+
+ if (RequestHeader_->has_user_tag() && RequestHeader_->user_tag() != RequestHeader_->user()) {
+ delimitedBuilder->AppendFormat("UserTag: %v", RequestHeader_->user_tag());
+ }
+
+ auto responseMessage = GetResponseMessage();
+ delimitedBuilder->AppendFormat("Error: %v, BodySize: %v, AttachmentsSize: %v/%v",
+ Error_,
+ GetMessageBodySize(responseMessage),
+ GetTotalMessageAttachmentSize(responseMessage),
+ GetMessageAttachmentCount(responseMessage));
+
+ for (const auto& info : ResponseInfos_) {
+ delimitedBuilder->AppendString(info);
+ }
+
+ delimitedBuilder->AppendFormat("ExecutionTime: %v, TotalTime: %v",
+ *ExecutionTime_,
+ *TotalTime_);
+
+ if (auto traceContextTime = GetTraceContextTime()) {
+ delimitedBuilder->AppendFormat("CpuTime: %v", traceContextTime);
+ }
+
+ auto logMessage = builder.Flush();
+ if (TraceContext_ && TraceContext_->IsRecorded()) {
+ TraceContext_->AddTag(ResponseInfoAnnotation, logMessage);
+ }
+ YT_LOG_EVENT_WITH_ANCHOR(Logger, LogLevel_, RuntimeInfo_->ResponseLoggingAnchor, logMessage);
+ }
+
+
+ void CreateRequestAttachmentsStream()
+ {
+ auto guard = Guard(StreamsLock_);
+
+ if (!RequestAttachmentsStream_) {
+ auto parameters = FromProto<TStreamingParameters>(RequestHeader_->server_attachments_streaming_parameters());
+ RequestAttachmentsStream_ = New<TAttachmentsInputStream>(
+ BIND(&TServiceContext::OnRequestAttachmentsStreamRead, MakeWeak(this)),
+ TDispatcher::Get()->GetCompressionPoolInvoker(),
+ parameters.ReadTimeout);
+ }
+
+ auto error = StreamsError_;
+
+ guard.Release();
+
+ if (!error.IsOK()) {
+ RequestAttachmentsStream_->AbortUnlessClosed(error);
+ }
+ }
+
+ void CreateResponseAttachmentsStream()
+ {
+ auto guard = Guard(StreamsLock_);
+
+ if (!ResponseAttachmentsStream_) {
+ auto parameters = FromProto<TStreamingParameters>(RequestHeader_->server_attachments_streaming_parameters());
+ ResponseAttachmentsStream_ = New<TAttachmentsOutputStream>(
+ ResponseCodec_,
+ TDispatcher::Get()->GetCompressionPoolInvoker(),
+ BIND(&TServiceContext::OnPullResponseAttachmentsStream, MakeWeak(this)),
+ parameters.WindowSize,
+ parameters.WriteTimeout);
+ }
+
+ auto error = StreamsError_;
+
+ guard.Release();
+
+ if (!error.IsOK()) {
+ ResponseAttachmentsStream_->AbortUnlessClosed(error);
+ }
+ }
+
+ void OnPullResponseAttachmentsStream()
+ {
+ YT_VERIFY(ResponseAttachmentsStream_);
+ auto payload = ResponseAttachmentsStream_->TryPull();
+ if (!payload) {
+ return;
+ }
+
+ YT_LOG_DEBUG("Response streaming attachments pulled (RequestId: %v, SequenceNumber: %v, Sizes: %v, Closed: %v)",
+ RequestId_,
+ payload->SequenceNumber,
+ MakeFormattableView(payload->Attachments, [] (auto* builder, const auto& attachment) {
+ builder->AppendFormat("%v", GetStreamingAttachmentSize(attachment));
+ }),
+ !payload->Attachments.back());
+
+ NProto::TStreamingPayloadHeader header;
+ ToProto(header.mutable_request_id(), RequestId_);
+ header.set_service(GetService());
+ header.set_method(GetMethod());
+ if (GetRealmId()) {
+ ToProto(header.mutable_realm_id(), GetRealmId());
+ }
+ header.set_sequence_number(payload->SequenceNumber);
+ header.set_codec(static_cast<int>(payload->Codec));
+
+ auto message = CreateStreamingPayloadMessage(header, payload->Attachments);
+
+ NBus::TSendOptions options;
+ options.TrackingLevel = EDeliveryTrackingLevel::Full;
+ ReplyBus_->Send(std::move(message), options).Subscribe(
+ BIND(&TServiceContext::OnResponseStreamingPayloadAcked, MakeStrong(this), payload->SequenceNumber));
+ }
+
+ void OnResponseStreamingPayloadAcked(int sequenceNumber, const TError& error)
+ {
+ YT_VERIFY(ResponseAttachmentsStream_);
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Response streaming payload delivery acknowledged (RequestId: %v, SequenceNumber: %v)",
+ RequestId_,
+ sequenceNumber);
+ } else {
+ YT_LOG_DEBUG(error, "Response streaming payload delivery failed (RequestId: %v, SequenceNumber: %v)",
+ RequestId_,
+ sequenceNumber);
+ ResponseAttachmentsStream_->Abort(error);
+ }
+ }
+
+ void OnRequestAttachmentsStreamRead()
+ {
+ YT_VERIFY(RequestAttachmentsStream_);
+ auto feedback = RequestAttachmentsStream_->GetFeedback();
+
+ YT_LOG_DEBUG("Request streaming attachments read (RequestId: %v, ReadPosition: %v)",
+ RequestId_,
+ feedback.ReadPosition);
+
+ NProto::TStreamingFeedbackHeader header;
+ ToProto(header.mutable_request_id(), RequestId_);
+ header.set_service(GetService());
+ header.set_method(GetMethod());
+ if (GetRealmId()) {
+ ToProto(header.mutable_realm_id(), GetRealmId());
+ }
+ header.set_read_position(feedback.ReadPosition);
+
+ auto message = CreateStreamingFeedbackMessage(header);
+
+ NBus::TSendOptions options;
+ options.TrackingLevel = EDeliveryTrackingLevel::Full;
+ ReplyBus_->Send(std::move(message), options).Subscribe(
+ BIND(&TServiceContext::OnRequestStreamingFeedbackAcked, MakeStrong(this)));
+ }
+
+ void OnRequestStreamingFeedbackAcked(const TError& error)
+ {
+ YT_VERIFY(RequestAttachmentsStream_);
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Request streaming feedback delivery acknowledged (RequestId: %v)",
+ RequestId_);
+ } else {
+ YT_LOG_DEBUG(error, "Request streaming feedback delivery failed (RequestId: %v)",
+ RequestId_);
+ RequestAttachmentsStream_->Abort(error);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRequestQueue::TRequestQueue(TString name, NProfiling::TProfiler profiler)
+ : Name_(std::move(name))
+ , BytesThrottler_{CreateReconfigurableThroughputThrottler(InfiniteRequestThrottlerConfig,
+ NLogging::TLogger(),
+ profiler.WithPrefix("/bytes_throttler"))}
+ , WeightThrottler_{CreateReconfigurableThroughputThrottler(InfiniteRequestThrottlerConfig,
+ NLogging::TLogger(),
+ profiler.WithPrefix("/weight_throttler"))}
+{ }
+
+bool TRequestQueue::Register(TServiceBase* service, TServiceBase::TRuntimeMethodInfo* runtimeInfo)
+{
+ // Fast path.
+ if (Registered_.load(std::memory_order::acquire)) {
+ YT_ASSERT(service == Service_);
+ YT_ASSERT(runtimeInfo == RuntimeInfo_);
+ return false;
+ }
+
+ // Slow path.
+ {
+ auto guard = Guard(RegisterLock_);
+ if (!Registered_.load(std::memory_order::relaxed)) {
+ Service_ = service;
+ RuntimeInfo_ = runtimeInfo;
+ }
+ Registered_.store(true, std::memory_order::release);
+ }
+
+ return true;
+}
+
+void TRequestQueue::TRequestThrottler::Reconfigure(const TThroughputThrottlerConfigPtr& config)
+{
+ Throttler->Reconfigure(config ? config : New<TThroughputThrottlerConfig>());
+ Specified.store(config.operator bool(), std::memory_order::release);
+}
+
+void TRequestQueue::Configure(const TMethodConfigPtr& config)
+{
+ BytesThrottler_.Reconfigure(config->RequestBytesThrottler);
+ WeightThrottler_.Reconfigure(config->RequestWeightThrottler);
+
+ ScheduleRequestsFromQueue();
+ SubscribeToThrottlers();
+}
+
+const TString& TRequestQueue::GetName() const
+{
+ return Name_;
+}
+
+void TRequestQueue::ConfigureBytesThrottler(const TThroughputThrottlerConfigPtr& config)
+{
+ BytesThrottler_.Reconfigure(config);
+}
+
+void TRequestQueue::ConfigureWeightThrottler(const TThroughputThrottlerConfigPtr& config)
+{
+ WeightThrottler_.Reconfigure(config);
+}
+
+bool TRequestQueue::IsQueueLimitSizeExceeded() const
+{
+ return
+ QueueSize_.load(std::memory_order::relaxed) >=
+ RuntimeInfo_->QueueSizeLimit.load(std::memory_order::relaxed);
+}
+
+int TRequestQueue::GetQueueSize() const
+{
+ return QueueSize_.load(std::memory_order::relaxed);
+}
+
+int TRequestQueue::GetConcurrency() const
+{
+ return Concurrency_.load(std::memory_order::relaxed);
+}
+
+void TRequestQueue::OnRequestArrived(TServiceBase::TServiceContextPtr context)
+{
+ // Fast path.
+ auto newConcurrencySemaphore = IncrementConcurrency();
+ if (newConcurrencySemaphore <= RuntimeInfo_->ConcurrencyLimit.load(std::memory_order::relaxed) &&
+ !AreThrottlersOverdrafted())
+ {
+ RunRequest(std::move(context));
+ return;
+ }
+
+ // Slow path.
+ DecrementConcurrency();
+ IncrementQueueSize();
+ Queue_.enqueue(std::move(context));
+ ScheduleRequestsFromQueue();
+}
+
+void TRequestQueue::OnRequestFinished()
+{
+ DecrementConcurrency();
+
+ if (QueueSize_.load() > 0) {
+ // Slow path.
+ ScheduleRequestsFromQueue();
+ }
+}
+
+// Prevents reentrant invocations.
+static thread_local bool ScheduleRequestsLatch = false;
+
+void TRequestQueue::ScheduleRequestsFromQueue()
+{
+ if (ScheduleRequestsLatch) {
+ return;
+ }
+
+ ScheduleRequestsLatch = true;
+ auto latchGuard = Finally([&] {
+ ScheduleRequestsLatch = false;
+ });
+
+#ifndef NDEBUG
+ // Method handlers are allowed to run via sync invoker;
+ // however these handlers must not yield.
+ TForbidContextSwitchGuard contextSwitchGuard;
+#endif
+
+ // NB: Racy, may lead to overcommit in concurrency semaphore and request bytes throttler.
+ auto concurrencyLimit = RuntimeInfo_->ConcurrencyLimit.load(std::memory_order::relaxed);
+ while (QueueSize_.load() > 0 && Concurrency_.load() < concurrencyLimit) {
+ if (AreThrottlersOverdrafted()) {
+ SubscribeToThrottlers();
+ break;
+ }
+
+ TServiceBase::TServiceContextPtr context;
+ if (!Queue_.try_dequeue(context)) {
+ break;
+ }
+
+ DecrementQueueSize();
+ IncrementConcurrency();
+ RunRequest(std::move(context));
+ }
+}
+
+void TRequestQueue::RunRequest(TServiceBase::TServiceContextPtr context)
+{
+ AcquireThrottlers(context);
+
+ auto options = RuntimeInfo_->Descriptor.Options;
+ options.SetHeavy(RuntimeInfo_->Heavy.load(std::memory_order::relaxed));
+
+ if (options.Heavy) {
+ BIND(RuntimeInfo_->Descriptor.HeavyHandler)
+ .AsyncVia(TDispatcher::Get()->GetHeavyInvoker())
+ .Run(context, options)
+ .Subscribe(BIND(&TServiceBase::TServiceContext::CheckAndRun, std::move(context)));
+ } else {
+ context->Run(RuntimeInfo_->Descriptor.LiteHandler);
+ }
+}
+
+int TRequestQueue::IncrementQueueSize()
+{
+ return ++QueueSize_;
+}
+
+void TRequestQueue::DecrementQueueSize()
+{
+ auto newQueueSize = --QueueSize_;
+ YT_ASSERT(newQueueSize >= 0);
+}
+
+int TRequestQueue::IncrementConcurrency()
+{
+ return ++Concurrency_;
+}
+
+void TRequestQueue::DecrementConcurrency()
+{
+ auto newConcurrencySemaphore = --Concurrency_;
+ YT_ASSERT(newConcurrencySemaphore >= 0);
+}
+
+bool TRequestQueue::AreThrottlersOverdrafted() const
+{
+ auto isOverdrafted = [] (const TRequestThrottler& throttler) {
+ return throttler.Specified.load(std::memory_order::relaxed) && throttler.Throttler->IsOverdraft();
+ };
+ return isOverdrafted(BytesThrottler_) || isOverdrafted(WeightThrottler_);
+}
+
+void TRequestQueue::AcquireThrottlers(const TServiceBase::TServiceContextPtr& context)
+{
+ if (BytesThrottler_.Specified.load(std::memory_order::acquire)) {
+ // Slow path.
+ auto requestSize =
+ GetMessageBodySize(context->GetRequestMessage()) +
+ GetTotalMessageAttachmentSize(context->GetRequestMessage());
+ BytesThrottler_.Throttler->Acquire(requestSize);
+ }
+ if (WeightThrottler_.Specified.load(std::memory_order::acquire)) {
+ // Slow path.
+ const auto& header = context->GetRequestHeader();
+ if (header.has_logical_request_weight()) {
+ WeightThrottler_.Throttler->Acquire(header.logical_request_weight());
+ }
+ }
+}
+
+void TRequestQueue::SubscribeToThrottlers()
+{
+ if (Throttled_.load(std::memory_order::relaxed) || Throttled_.exchange(true, std::memory_order::acquire)) {
+ return;
+ }
+
+ std::vector<TFuture<void>> futures;
+ futures.reserve(2);
+ if (BytesThrottler_.Specified.load(std::memory_order::acquire)) {
+ futures.push_back(BytesThrottler_.Throttler->GetAvailableFuture());
+ }
+ if (WeightThrottler_.Specified.load(std::memory_order::acquire)) {
+ futures.push_back(WeightThrottler_.Throttler->GetAvailableFuture());
+ }
+
+ AllSucceeded(std::move(futures))
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this), weakService = MakeWeak(Service_)] (const TError&) {
+ if (auto service = weakService.Lock()) {
+ Throttled_.store(false, std::memory_order::release);
+ ScheduleRequestsFromQueue();
+ }
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TServiceBase::TRuntimeMethodInfo::TPerformanceCountersKeyEquals
+{
+ bool operator()(
+ const TNonowningPerformanceCountersKey& lhs,
+ const TNonowningPerformanceCountersKey& rhs) const
+ {
+ return lhs == rhs;
+ }
+
+ bool operator()(
+ const TOwningPerformanceCountersKey& lhs,
+ const TNonowningPerformanceCountersKey& rhs) const
+ {
+ const auto& [lhsUserTag, lhsRequestQueue] = lhs;
+ return TNonowningPerformanceCountersKey{lhsUserTag, lhsRequestQueue} == rhs;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceBase::TServiceBase(
+ IInvokerPtr defaultInvoker,
+ const TServiceDescriptor& descriptor,
+ const NLogging::TLogger& logger,
+ TRealmId realmId,
+ IAuthenticatorPtr authenticator)
+ : Logger(logger)
+ , DefaultInvoker_(std::move(defaultInvoker))
+ , Authenticator_(std::move(authenticator))
+ , ServiceDescriptor_(descriptor)
+ , ServiceId_(descriptor.GetFullServiceName(), realmId)
+ , Profiler_(RpcServerProfiler.WithHot().WithTag("yt_service", ServiceId_.ServiceName))
+ , AuthenticationTimer_(Profiler_.Timer("/authentication_time"))
+ , ServiceLivenessChecker_(New<TPeriodicExecutor>(
+ TDispatcher::Get()->GetLightInvoker(),
+ BIND(&TServiceBase::OnServiceLivenessCheck, MakeWeak(this)),
+ ServiceLivenessCheckPeriod))
+ , PerformanceCounters_(New<TServiceBase::TPerformanceCounters>(RpcServerProfiler))
+{
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(Discover)
+ .SetInvoker(TDispatcher::Get()->GetHeavyInvoker())
+ .SetConcurrencyLimit(1'000'000)
+ .SetSystem(true));
+
+ Profiler_.AddFuncGauge("/authentication_queue_size", MakeStrong(this), [this] {
+ return AuthenticationQueueSize_.load(std::memory_order::relaxed);
+ });
+
+ ServiceLivenessChecker_->Start();
+}
+
+const TServiceId& TServiceBase::GetServiceId() const
+{
+ return ServiceId_;
+}
+
+void TServiceBase::HandleRequest(
+ std::unique_ptr<NProto::TRequestHeader> header,
+ TSharedRefArray message,
+ IBusPtr replyBus)
+{
+ SetActive();
+
+ const auto& method = header->method();
+ auto requestId = FromProto<TRequestId>(header->request_id());
+
+ auto replyError = [&] (TError error) {
+ ReplyError(std::move(error), *header, replyBus);
+ };
+
+ if (IsStopped()) {
+ replyError(TError(
+ NRpc::EErrorCode::Unavailable,
+ "Service is stopped"));
+ return;
+ }
+
+ if (auto error = DoCheckRequestCompatibility(*header); !error.IsOK()) {
+ replyError(std::move(error));
+ return;
+ }
+
+ auto* runtimeInfo = FindMethodInfo(method);
+ if (!runtimeInfo) {
+ replyError(TError(
+ NRpc::EErrorCode::NoSuchMethod,
+ "Unknown method"));
+ return;
+ }
+
+ auto tracingMode = runtimeInfo->TracingMode.load(std::memory_order::relaxed);
+ auto traceContext = tracingMode == ERequestTracingMode::Disable
+ ? NTracing::TTraceContextPtr()
+ : GetOrCreateHandlerTraceContext(*header, tracingMode == ERequestTracingMode::Force);
+ if (traceContext && traceContext->IsRecorded()) {
+ traceContext->AddTag(EndpointAnnotation, replyBus->GetEndpointDescription());
+ }
+
+ auto* requestQueue = GetRequestQueue(runtimeInfo, *header);
+ RegisterRequestQueue(runtimeInfo, requestQueue);
+
+ if (requestQueue->IsQueueLimitSizeExceeded()) {
+ runtimeInfo->RequestQueueSizeLimitErrorCounter.Increment();
+ replyError(TError(
+ NRpc::EErrorCode::RequestQueueSizeLimitExceeded,
+ "Request queue size limit exceeded")
+ << TErrorAttribute("limit", runtimeInfo->QueueSizeLimit.load())
+ << TErrorAttribute("queue", requestQueue->GetName()));
+ return;
+ }
+
+ TCurrentTraceContextGuard traceContextGuard(traceContext);
+
+ // NOTE: Do not use replyError() after this line.
+ TAcceptedRequest acceptedRequest{
+ requestId,
+ std::move(replyBus),
+ std::move(runtimeInfo),
+ std::move(traceContext),
+ std::move(header),
+ std::move(message),
+ requestQueue
+ };
+
+ if (!IsAuthenticationNeeded(acceptedRequest)) {
+ HandleAuthenticatedRequest(std::move(acceptedRequest));
+ return;
+ }
+
+ // Not actually atomic but should work fine as long as some small error is OK.
+ auto authenticationQueueSizeLimit = AuthenticationQueueSizeLimit_.load(std::memory_order::relaxed);
+ auto authenticationQueueSize = AuthenticationQueueSize_.load(std::memory_order::relaxed);
+ if (authenticationQueueSize > authenticationQueueSizeLimit) {
+ auto error = TError(
+ NRpc::EErrorCode::RequestQueueSizeLimitExceeded,
+ "Authentication request queue size limit exceeded")
+ << TErrorAttribute("limit", authenticationQueueSizeLimit);
+ ReplyError(error, *acceptedRequest.Header, acceptedRequest.ReplyBus);
+ return;
+ }
+ ++AuthenticationQueueSize_;
+
+ NProfiling::TWallTimer timer;
+
+ TAuthenticationContext authenticationContext{
+ .Header = acceptedRequest.Header.get(),
+ .UserIP = acceptedRequest.ReplyBus->GetEndpointNetworkAddress(),
+ .IsLocal = acceptedRequest.ReplyBus->IsEndpointLocal(),
+ };
+ if (Authenticator_->CanAuthenticate(authenticationContext)) {
+ auto asyncAuthResult = Authenticator_->AsyncAuthenticate(authenticationContext);
+ if (asyncAuthResult.IsSet()) {
+ OnRequestAuthenticated(timer, std::move(acceptedRequest), asyncAuthResult.Get());
+ } else {
+ asyncAuthResult.Subscribe(
+ BIND(&TServiceBase::OnRequestAuthenticated, MakeStrong(this), timer, Passed(std::move(acceptedRequest))));
+ }
+ } else {
+ OnRequestAuthenticated(timer, std::move(acceptedRequest), TError(
+ NYT::NRpc::EErrorCode::AuthenticationError,
+ "Request is missing credentials"));
+ }
+
+}
+
+void TServiceBase::ReplyError(
+ TError error,
+ const NProto::TRequestHeader& header,
+ const IBusPtr& replyBus)
+{
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ auto richError = std::move(error)
+ << TErrorAttribute("request_id", requestId)
+ << TErrorAttribute("realm_id", ServiceId_.RealmId)
+ << TErrorAttribute("service", ServiceId_.ServiceName)
+ << TErrorAttribute("method", header.method())
+ << TErrorAttribute("endpoint", replyBus->GetEndpointDescription());
+
+ auto code = richError.GetCode();
+ auto logLevel =
+ code == NRpc::EErrorCode::NoSuchMethod || code == NRpc::EErrorCode::ProtocolError
+ ? NLogging::ELogLevel::Warning
+ : NLogging::ELogLevel::Debug;
+ YT_LOG_EVENT(Logger, logLevel, richError);
+
+ auto errorMessage = CreateErrorResponseMessage(requestId, richError);
+ YT_UNUSED_FUTURE(replyBus->Send(errorMessage, NBus::TSendOptions(EDeliveryTrackingLevel::None)));
+}
+
+void TServiceBase::OnRequestAuthenticated(
+ const NProfiling::TWallTimer& timer,
+ TAcceptedRequest&& acceptedRequest,
+ const TErrorOr<TAuthenticationResult>& authResultOrError)
+{
+ AuthenticationTimer_.Record(timer.GetElapsedTime());
+ --AuthenticationQueueSize_;
+
+ auto& requestHeader = *acceptedRequest.Header;
+
+ if (authResultOrError.IsOK()) {
+ const auto& authResult = authResultOrError.Value();
+ const auto& Logger = RpcServerLogger;
+ YT_LOG_DEBUG("Request authenticated (RequestId: %v, User: %v, Realm: %v)",
+ acceptedRequest.RequestId,
+ authResult.User,
+ authResult.Realm);
+ const auto& authenticatedUser = authResult.User;
+ if (requestHeader.has_user()) {
+ const auto& user = requestHeader.user();
+ if (user != authenticatedUser) {
+ ReplyError(
+ TError(
+ NRpc::EErrorCode::AuthenticationError,
+ "Manually specified and authenticated users mismatch")
+ << TErrorAttribute("user", user)
+ << TErrorAttribute("authenticated_user", authenticatedUser),
+ requestHeader,
+ acceptedRequest.ReplyBus);
+ return;
+ }
+ }
+ requestHeader.set_user(std::move(authResult.User));
+
+ auto* credentialsExt = requestHeader.MutableExtension(
+ NRpc::NProto::TCredentialsExt::credentials_ext);
+ if (credentialsExt->user_ticket().empty()) {
+ credentialsExt->set_user_ticket(std::move(authResult.UserTicket));
+ }
+ HandleAuthenticatedRequest(std::move(acceptedRequest));
+ } else {
+ ReplyError(
+ TError(
+ NRpc::EErrorCode::AuthenticationError,
+ "Request authentication failed")
+ << authResultOrError,
+ requestHeader,
+ acceptedRequest.ReplyBus);
+ }
+}
+
+bool TServiceBase::IsAuthenticationNeeded(const TAcceptedRequest& acceptedRequest)
+{
+ return
+ Authenticator_.operator bool() &&
+ !acceptedRequest.RuntimeInfo->Descriptor.System;
+}
+
+void TServiceBase::HandleAuthenticatedRequest(TAcceptedRequest&& acceptedRequest)
+{
+ if (!acceptedRequest.ReplyBus->IsEndpointLocal()) {
+ bool authenticated = acceptedRequest.Header->HasExtension(NRpc::NProto::TCredentialsExt::credentials_ext) &&
+ acceptedRequest.Header->GetExtension(NRpc::NProto::TCredentialsExt::credentials_ext).has_service_ticket();
+ if (!authenticated) {
+ acceptedRequest.RuntimeInfo->UnauthenticatedRequestsCounter.Increment();
+ }
+ }
+
+ auto context = New<TServiceContext>(
+ this,
+ std::move(acceptedRequest),
+ Logger);
+ auto* requestQueue = context->GetRequestQueue();
+ requestQueue->OnRequestArrived(std::move(context));
+}
+
+TRequestQueue* TServiceBase::GetRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ const NRpc::NProto::TRequestHeader& requestHeader)
+{
+ TRequestQueue* requestQueue = nullptr;
+ if (auto& provider = runtimeInfo->Descriptor.RequestQueueProvider) {
+ requestQueue = provider->GetQueue(requestHeader);
+ }
+ if (!requestQueue) {
+ requestQueue = runtimeInfo->DefaultRequestQueue.Get();
+ }
+ return requestQueue;
+}
+
+void TServiceBase::RegisterRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ TRequestQueue* requestQueue)
+{
+ if (!requestQueue->Register(this, runtimeInfo)) {
+ return;
+ }
+
+ const auto& method = runtimeInfo->Descriptor.Method;
+ YT_LOG_DEBUG("Request queue registered (Method: %v, Queue: %v)",
+ method,
+ requestQueue->GetName());
+
+ auto profiler = runtimeInfo->Profiler.WithSparse();
+ if (runtimeInfo->Descriptor.RequestQueueProvider) {
+ profiler = profiler.WithTag("queue", requestQueue->GetName());
+ }
+ profiler.AddFuncGauge("/request_queue_size", MakeStrong(this), [=] {
+ return requestQueue->GetQueueSize();
+ });
+ profiler.AddFuncGauge("/concurrency", MakeStrong(this), [=] {
+ return requestQueue->GetConcurrency();
+ });
+
+ TMethodConfigPtr methodConfig;
+ if (auto config = Config_.Acquire()) {
+ methodConfig = GetOrDefault(config->Methods, method);
+ }
+ ConfigureRequestQueue(runtimeInfo, requestQueue, methodConfig);
+
+ {
+ auto guard = Guard(runtimeInfo->RequestQueuesLock);
+ runtimeInfo->RequestQueues.push_back(requestQueue);
+ }
+}
+
+void TServiceBase::ConfigureRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ TRequestQueue* requestQueue,
+ const TMethodConfigPtr& config)
+{
+ if (auto& provider = runtimeInfo->Descriptor.RequestQueueProvider;
+ provider &&
+ requestQueue != runtimeInfo->DefaultRequestQueue.Get())
+ {
+ provider->ConfigureQueue(requestQueue, config);
+ } else if (config) {
+ requestQueue->Configure(config);
+ }
+}
+
+void TServiceBase::HandleRequestCancellation(TRequestId requestId)
+{
+ SetActive();
+
+ if (TryCancelQueuedReply(requestId)) {
+ return;
+ }
+
+ if (auto context = FindRequest(requestId)) {
+ context->Cancel();
+ return;
+ }
+
+ YT_LOG_DEBUG("Received cancelation for an unknown request, ignored (RequestId: %v)",
+ requestId);
+}
+
+void TServiceBase::HandleStreamingPayload(
+ TRequestId requestId,
+ const TStreamingPayload& payload)
+{
+ SetActive();
+
+ auto* bucket = GetRequestBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ auto context = DoFindRequest(bucket, requestId);
+ if (context) {
+ guard.Release();
+ context->HandleStreamingPayload(payload);
+ } else {
+ auto* entry = DoGetOrCreatePendingPayloadsEntry(bucket, requestId);
+ entry->Payloads.emplace_back(payload);
+ guard.Release();
+ YT_LOG_DEBUG("Received streaming payload for an unknown request, saving (RequestId: %v)",
+ requestId);
+ }
+}
+
+void TServiceBase::HandleStreamingFeedback(
+ TRequestId requestId,
+ const TStreamingFeedback& feedback)
+{
+ auto context = FindRequest(requestId);
+ if (!context) {
+ YT_LOG_DEBUG("Received streaming feedback for an unknown request, ignored (RequestId: %v)",
+ requestId);
+ return;
+ }
+
+ context->HandleStreamingFeedback(feedback);
+}
+
+void TServiceBase::DoDeclareServerFeature(int featureId)
+{
+ ValidateInactive();
+
+ // Failure here means that such feature is already registered.
+ YT_VERIFY(SupportedServerFeatureIds_.insert(featureId).second);
+}
+
+TError TServiceBase::DoCheckRequestCompatibility(const NRpc::NProto::TRequestHeader& header)
+{
+ if (auto error = DoCheckRequestProtocol(header); !error.IsOK()) {
+ return error;
+ }
+ if (auto error = DoCheckRequestFeatures(header); !error.IsOK()) {
+ return error;
+ }
+ return {};
+}
+
+TError TServiceBase::DoCheckRequestProtocol(const NRpc::NProto::TRequestHeader& header)
+{
+ TProtocolVersion requestProtocolVersion{
+ header.protocol_version_major(),
+ header.protocol_version_minor()
+ };
+
+ if (requestProtocolVersion.Major != GenericProtocolVersion.Major) {
+ if (ServiceDescriptor_.ProtocolVersion.Major != requestProtocolVersion.Major) {
+ return TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Server major protocol version differs from client major protocol version: %v != %v",
+ requestProtocolVersion.Major,
+ ServiceDescriptor_.ProtocolVersion.Major);
+ }
+
+ if (ServiceDescriptor_.ProtocolVersion.Minor < requestProtocolVersion.Minor) {
+ return TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Server minor protocol version is less than minor protocol version required by client: %v < %v",
+ ServiceDescriptor_.ProtocolVersion.Minor,
+ requestProtocolVersion.Minor);
+ }
+ }
+ return {};
+}
+
+TError TServiceBase::DoCheckRequestFeatures(const NRpc::NProto::TRequestHeader& header)
+{
+ for (auto featureId : header.required_server_feature_ids()) {
+ if (!SupportedServerFeatureIds_.contains(featureId)) {
+ return TError(
+ NRpc::EErrorCode::UnsupportedServerFeature,
+ "Server does not support the feature requested by client")
+ << TErrorAttribute("feature_id", featureId);
+ }
+ }
+ return {};
+}
+
+void TServiceBase::OnRequestTimeout(TRequestId requestId, bool /*aborted*/)
+{
+ auto context = FindRequest(requestId);
+ if (!context) {
+ return;
+ }
+
+ context->HandleTimeout();
+}
+
+void TServiceBase::OnReplyBusTerminated(const IBusPtr& bus, const TError& error)
+{
+ std::vector<TServiceContextPtr> contexts;
+ {
+ auto* bucket = GetReplyBusBucket(bus);
+ auto guard = Guard(bucket->Lock);
+ auto it = bucket->ReplyBusToContexts.find(bus);
+ if (it == bucket->ReplyBusToContexts.end()) {
+ return;
+ }
+
+ for (auto* rawContext : it->second) {
+ auto context = DangerousGetPtr(rawContext);
+ if (context) {
+ contexts.push_back(context);
+ }
+ }
+
+ bucket->ReplyBusToContexts.erase(it);
+ }
+
+ for (auto context : contexts) {
+ YT_LOG_DEBUG(error, "Reply bus terminated, canceling request (RequestId: %v)",
+ context->GetRequestId());
+ context->Cancel();
+ }
+}
+
+TServiceBase::TRequestBucket* TServiceBase::GetRequestBucket(TRequestId requestId)
+{
+ return &RequestBuckets_[THash<TRequestId>()(requestId) % RequestBucketCount];
+}
+
+TServiceBase::TReplyBusBucket* TServiceBase::GetReplyBusBucket(const IBusPtr& bus)
+{
+ return &ReplyBusBuckets_[THash<IBusPtr>()(bus) % ReplyBusBucketCount];
+}
+
+TServiceBase::TQueuedReplyBucket* TServiceBase::GetQueuedReplyBucket(TRequestId requestId)
+{
+ return &QueuedReplyBusBuckets_[THash<TRequestId>()(requestId) % QueuedReplyBucketCount];
+}
+
+void TServiceBase::RegisterRequest(TServiceContext* context)
+{
+ auto requestId = context->GetRequestId();
+ {
+ auto* bucket = GetRequestBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ // NB: We're OK with duplicate request ids.
+ bucket->RequestIdToContext.emplace(requestId, context);
+ }
+
+ const auto& replyBus = context->GetReplyBus();
+ bool subscribe = false;
+ {
+ auto* bucket = GetReplyBusBucket(replyBus);
+ auto guard = Guard(bucket->Lock);
+ auto it = bucket->ReplyBusToContexts.find(context->GetReplyBus());
+ if (it == bucket->ReplyBusToContexts.end()) {
+ subscribe = true;
+ it = bucket->ReplyBusToContexts.emplace(replyBus, THashSet<TServiceContext*>()).first;
+ }
+ auto& contexts = it->second;
+ contexts.insert(context);
+ }
+
+ if (subscribe) {
+ replyBus->SubscribeTerminated(BIND(&TServiceBase::OnReplyBusTerminated, MakeWeak(this), replyBus));
+ }
+
+ auto pendingPayloads = GetAndErasePendingPayloads(requestId);
+ if (!pendingPayloads.empty()) {
+ YT_LOG_DEBUG("Pulling pending streaming payloads for a late request (RequestId: %v, PayloadCount: %v)",
+ requestId,
+ pendingPayloads.size());
+ for (const auto& payload : pendingPayloads) {
+ context->HandleStreamingPayload(payload);
+ }
+ }
+}
+
+void TServiceBase::UnregisterRequest(TServiceContext* context)
+{
+ auto requestId = context->GetRequestId();
+ {
+ auto* bucket = GetRequestBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ // NB: We're OK with duplicate request ids.
+ bucket->RequestIdToContext.erase(requestId);
+ }
+
+ const auto& replyBus = context->GetReplyBus();
+ {
+ auto* bucket = GetReplyBusBucket(replyBus);
+ auto guard = Guard(bucket->Lock);
+ auto it = bucket->ReplyBusToContexts.find(replyBus);
+ // Missing replyBus in ReplyBusToContexts is OK; see OnReplyBusTerminated.
+ if (it != bucket->ReplyBusToContexts.end()) {
+ auto& contexts = it->second;
+ contexts.erase(context);
+ }
+ }
+}
+
+TServiceBase::TServiceContextPtr TServiceBase::FindRequest(TRequestId requestId)
+{
+ auto* bucket = GetRequestBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ return DoFindRequest(bucket, requestId);
+}
+
+TServiceBase::TServiceContextPtr TServiceBase::DoFindRequest(TRequestBucket* bucket, TRequestId requestId)
+{
+ auto it = bucket->RequestIdToContext.find(requestId);
+ return it == bucket->RequestIdToContext.end() ? nullptr : DangerousGetPtr(it->second);
+}
+
+void TServiceBase::RegisterQueuedReply(TRequestId requestId, TFuture<void> reply)
+{
+ auto* bucket = GetQueuedReplyBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ bucket->QueuedReplies.emplace(requestId, std::move(reply));
+}
+
+void TServiceBase::UnregisterQueuedReply(TRequestId requestId)
+{
+ auto* bucket = GetQueuedReplyBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ bucket->QueuedReplies.erase(requestId);
+}
+
+bool TServiceBase::TryCancelQueuedReply(TRequestId requestId)
+{
+ TFuture<void> queuedReply;
+
+ {
+ auto* bucket = GetQueuedReplyBucket(requestId);
+ auto guard = Guard(bucket->Lock);
+ if (auto it = bucket->QueuedReplies.find(requestId); it != bucket->QueuedReplies.end()) {
+ queuedReply = it->second;
+ bucket->QueuedReplies.erase(it);
+ }
+ }
+
+ if (queuedReply) {
+ queuedReply.Cancel(TError());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+TServiceBase::TPendingPayloadsEntry* TServiceBase::DoGetOrCreatePendingPayloadsEntry(TRequestBucket* bucket, TRequestId requestId)
+{
+ auto& entry = bucket->RequestIdToPendingPayloads[requestId];
+ if (!entry.Lease) {
+ entry.Lease = NConcurrency::TLeaseManager::CreateLease(
+ PendingPayloadsTimeout_.load(std::memory_order::relaxed),
+ BIND(&TServiceBase::OnPendingPayloadsLeaseExpired, MakeWeak(this), requestId));
+ }
+
+ return &entry;
+}
+
+std::vector<TStreamingPayload> TServiceBase::GetAndErasePendingPayloads(TRequestId requestId)
+{
+ auto* bucket = GetRequestBucket(requestId);
+
+ TPendingPayloadsEntry entry;
+ {
+ auto guard = Guard(bucket->Lock);
+ auto it = bucket->RequestIdToPendingPayloads.find(requestId);
+ if (it == bucket->RequestIdToPendingPayloads.end()) {
+ return {};
+ }
+ entry = std::move(it->second);
+ bucket->RequestIdToPendingPayloads.erase(it);
+ }
+
+ NConcurrency::TLeaseManager::CloseLease(entry.Lease);
+ return std::move(entry.Payloads);
+}
+
+void TServiceBase::OnPendingPayloadsLeaseExpired(TRequestId requestId)
+{
+ auto payloads = GetAndErasePendingPayloads(requestId);
+ if (!payloads.empty()) {
+ YT_LOG_DEBUG("Pending payloads lease expired, erasing (RequestId: %v, PayloadCount: %v)",
+ requestId,
+ payloads.size());
+ }
+}
+
+TServiceBase::TMethodPerformanceCountersPtr TServiceBase::CreateMethodPerformanceCounters(
+ TRuntimeMethodInfo* runtimeInfo,
+ const TRuntimeMethodInfo::TNonowningPerformanceCountersKey& key)
+{
+ const auto& [userTag, requestQueue] = key;
+
+ auto profiler = runtimeInfo->Profiler.WithSparse();
+ if (userTag) {
+ profiler = profiler.WithTag("user", TString(userTag));
+ }
+ if (runtimeInfo->Descriptor.RequestQueueProvider) {
+ profiler = profiler.WithTag("queue", requestQueue->GetName());
+ }
+ const auto config = [&]{
+ const auto guard = Guard(HistogramConfigLock_);
+ return HistogramTimerProfiling;
+ }();
+ return New<TMethodPerformanceCounters>(profiler, config);
+}
+
+TServiceBase::TMethodPerformanceCounters* TServiceBase::GetMethodPerformanceCounters(
+ TRuntimeMethodInfo* runtimeInfo,
+ const TRuntimeMethodInfo::TNonowningPerformanceCountersKey& key)
+{
+ auto [userTag, requestQueue] = key;
+
+ // Fast path.
+ if (userTag == RootUserName && requestQueue == runtimeInfo->DefaultRequestQueue.Get()) {
+ if (EnablePerUserProfiling_.load(std::memory_order::relaxed)) {
+ return runtimeInfo->RootPerformanceCounters.Get();
+ } else {
+ return runtimeInfo->BasePerformanceCounters.Get();
+ }
+ }
+
+ if (!EnablePerUserProfiling_.load(std::memory_order::relaxed)) {
+ userTag = {};
+ }
+ auto actualKey = TRuntimeMethodInfo::TNonowningPerformanceCountersKey{userTag, requestQueue};
+ return runtimeInfo->PerformanceCountersMap.FindOrInsert(actualKey, [&] {
+ return CreateMethodPerformanceCounters(runtimeInfo, actualKey);
+ }).first->Get();
+}
+
+TServiceBase::TPerformanceCounters* TServiceBase::GetPerformanceCounters()
+{
+ return PerformanceCounters_.Get();
+}
+
+void TServiceBase::SetActive()
+{
+ // Fast path.
+ if (Active_.load(std::memory_order::relaxed)) {
+ return;
+ }
+
+ // Slow path.
+ Active_.store(true);
+}
+
+void TServiceBase::ValidateInactive()
+{
+ // Failure here means that some service metadata (e.g. registered methods or supported
+ // features) are changing after the service has started serving requests.
+ YT_VERIFY(!Active_.load(std::memory_order::relaxed));
+}
+
+bool TServiceBase::IsStopped()
+{
+ return Stopped_.load();
+}
+
+void TServiceBase::IncrementActiveRequestCount()
+{
+ ++ActiveRequestCount_;
+}
+
+void TServiceBase::DecrementActiveRequestCount()
+{
+ if (--ActiveRequestCount_ == 0 && IsStopped()) {
+ StopResult_.TrySet();
+ }
+}
+
+void TServiceBase::InitContext(IServiceContextPtr /*context*/)
+{ }
+
+void TServiceBase::RegisterDiscoverRequest(const TCtxDiscoverPtr& context)
+{
+ auto payload = GetDiscoverRequestPayload(context);
+ auto replyDelay = FromProto<TDuration>(context->Request().reply_delay());
+
+ auto readerGuard = ReaderGuard(DiscoverRequestsByPayloadLock_);
+ auto it = DiscoverRequestsByPayload_.find(payload);
+ if (it == DiscoverRequestsByPayload_.end()) {
+ readerGuard.Release();
+ auto writerGuard = WriterGuard(DiscoverRequestsByPayloadLock_);
+ DiscoverRequestsByPayload_[payload].Insert(context, 0);
+ } else {
+ auto& requestSet = it->second;
+ requestSet.Insert(context, 0);
+ }
+
+ TDelayedExecutor::Submit(
+ BIND(&TServiceBase::OnDiscoverRequestReplyDelayReached, MakeStrong(this), context),
+ replyDelay,
+ TDispatcher::Get()->GetHeavyInvoker());
+}
+
+void TServiceBase::ReplyDiscoverRequest(const TCtxDiscoverPtr& context, bool isUp)
+{
+ auto& response = context->Response();
+ response.set_up(isUp);
+ ToProto(response.mutable_suggested_addresses(), SuggestAddresses());
+
+ context->SetResponseInfo("Up: %v, SuggestedAddresses: %v",
+ response.up(),
+ response.suggested_addresses());
+
+ context->Reply();
+}
+
+void TServiceBase::OnDiscoverRequestReplyDelayReached(TCtxDiscoverPtr context)
+{
+ auto readerGuard = ReaderGuard(DiscoverRequestsByPayloadLock_);
+
+ if (context->IsReplied()) {
+ return;
+ }
+
+ auto payload = GetDiscoverRequestPayload(context);
+ auto it = DiscoverRequestsByPayload_.find(payload);
+ if (it != DiscoverRequestsByPayload_.end()) {
+ auto& requestSet = it->second;
+ if (requestSet.Has(context)) {
+ requestSet.Remove(context);
+ ReplyDiscoverRequest(context, IsUp(context));
+ }
+ }
+}
+
+TString TServiceBase::GetDiscoverRequestPayload(const TCtxDiscoverPtr& context)
+{
+ auto request = context->Request();
+ request.set_reply_delay(0);
+ return SerializeProtoToString(request);
+}
+
+void TServiceBase::OnServiceLivenessCheck()
+{
+ std::vector<TDiscoverRequestSet> requestsToReply;
+
+ {
+ auto writerGuard = WriterGuard(DiscoverRequestsByPayloadLock_);
+
+ std::vector<TString> payloadsToReply;
+ for (const auto& [payload, requests] : DiscoverRequestsByPayload_) {
+ auto empty = true;
+ auto isUp = false;
+ for (const auto& bucket : requests.Buckets) {
+ const auto& map = bucket.GetMap();
+ if (!map.empty()) {
+ empty = false;
+ const auto& request = map.begin()->first;
+ isUp = IsUp(request);
+ break;
+ }
+ }
+ if (empty || isUp) {
+ payloadsToReply.push_back(payload);
+ }
+ }
+
+ for (const auto& payload : payloadsToReply) {
+ auto& requests = DiscoverRequestsByPayload_[payload];
+ requestsToReply.push_back(std::move(requests));
+ YT_VERIFY(DiscoverRequestsByPayload_.erase(payload) > 0);
+ }
+ }
+
+ TDispatcher::Get()->GetHeavyInvoker()->Invoke(
+ BIND([this, this_ = MakeStrong(this), requestsToReply = std::move(requestsToReply)] {
+ for (const auto& requests : requestsToReply) {
+ for (const auto& bucket : requests.Buckets) {
+ for (const auto& [request, _] : bucket.GetMap()) {
+ ReplyDiscoverRequest(request, /*isUp*/ true);
+ }
+ NConcurrency::Yield();
+ }
+ }
+ }));
+}
+
+TServiceBase::TRuntimeMethodInfoPtr TServiceBase::RegisterMethod(const TMethodDescriptor& descriptor)
+{
+ ValidateInactive();
+
+ auto runtimeInfo = New<TRuntimeMethodInfo>(ServiceId_, descriptor, Profiler_);
+
+ runtimeInfo->BasePerformanceCounters = CreateMethodPerformanceCounters(
+ runtimeInfo.Get(),
+ {{}, runtimeInfo->DefaultRequestQueue.Get()});
+ runtimeInfo->RootPerformanceCounters = CreateMethodPerformanceCounters(
+ runtimeInfo.Get(),
+ {RootUserName, runtimeInfo->DefaultRequestQueue.Get()});
+
+ runtimeInfo->Heavy.store(descriptor.Options.Heavy);
+ runtimeInfo->QueueSizeLimit.store(descriptor.QueueSizeLimit);
+ runtimeInfo->ConcurrencyLimit.store(descriptor.ConcurrencyLimit);
+ runtimeInfo->LogLevel.store(descriptor.LogLevel);
+ runtimeInfo->LoggingSuppressionTimeout.store(descriptor.LoggingSuppressionTimeout);
+
+ // Failure here means that such method is already registered.
+ YT_VERIFY(MethodMap_.emplace(descriptor.Method, runtimeInfo).second);
+
+ auto& profiler = runtimeInfo->Profiler;
+ profiler.AddFuncGauge("/request_queue_size_limit", MakeStrong(this), [=] {
+ return runtimeInfo->QueueSizeLimit.load(std::memory_order::relaxed);
+ });
+ profiler.AddFuncGauge("/concurrency_limit", MakeStrong(this), [=] {
+ return runtimeInfo->ConcurrencyLimit.load(std::memory_order::relaxed);
+ });
+
+ return runtimeInfo;
+}
+
+void TServiceBase::ValidateRequestFeatures(const IServiceContextPtr& context)
+{
+ const auto& header = context->RequestHeader();
+ if (auto error = DoCheckRequestFeatures(header); !error.IsOK()) {
+ auto requestId = FromProto<TRequestId>(header.request_id());
+ THROW_ERROR std::move(error)
+ << TErrorAttribute("request_id", requestId)
+ << TErrorAttribute("service", header.service())
+ << TErrorAttribute("method", header.method());
+ }
+}
+
+void TServiceBase::DoConfigureHistogramTimer(
+ const TServiceCommonConfigPtr& configDefaults,
+ const TServiceConfigPtr& config)
+{
+ THistogramConfigPtr finalConfig;
+ if (config->HistogramTimerProfiling) {
+ finalConfig = config->HistogramTimerProfiling;
+ } else if (configDefaults->HistogramTimerProfiling) {
+ finalConfig = configDefaults->HistogramTimerProfiling;
+ }
+ if (finalConfig) {
+ auto guard = Guard(HistogramConfigLock_);
+ HistogramTimerProfiling = finalConfig;
+ }
+}
+
+void TServiceBase::DoConfigure(
+ const TServiceCommonConfigPtr& configDefaults,
+ const TServiceConfigPtr& config)
+{
+ try {
+ YT_LOG_DEBUG("Configuring RPC service (Service: %v)",
+ ServiceId_.ServiceName);
+
+ // Validate configuration.
+ for (const auto& [methodName, _] : config->Methods) {
+ GetMethodInfoOrThrow(methodName);
+ }
+
+ EnablePerUserProfiling_.store(config->EnablePerUserProfiling.value_or(configDefaults->EnablePerUserProfiling));
+ AuthenticationQueueSizeLimit_.store(config->AuthenticationQueueSizeLimit.value_or(DefaultAuthenticationQueueSizeLimit));
+ PendingPayloadsTimeout_.store(config->PendingPayloadsTimeout.value_or(DefaultPendingPayloadsTimeout));
+ EnableErrorCodeCounting.store(config->EnableErrorCodeCounting.value_or(configDefaults->EnableErrorCodeCounting));
+
+ DoConfigureHistogramTimer(configDefaults, config);
+
+ for (const auto& [methodName, runtimeInfo] : MethodMap_) {
+ auto methodIt = config->Methods.find(methodName);
+ auto methodConfig = methodIt ? methodIt->second : New<TMethodConfig>();
+
+ const auto& descriptor = runtimeInfo->Descriptor;
+ runtimeInfo->Heavy.store(methodConfig->Heavy.value_or(descriptor.Options.Heavy));
+ runtimeInfo->QueueSizeLimit.store(methodConfig->QueueSizeLimit.value_or(descriptor.QueueSizeLimit));
+ runtimeInfo->ConcurrencyLimit.store(methodConfig->ConcurrencyLimit.value_or(descriptor.ConcurrencyLimit));
+ runtimeInfo->LogLevel.store(methodConfig->LogLevel.value_or(descriptor.LogLevel));
+ runtimeInfo->LoggingSuppressionTimeout.store(methodConfig->LoggingSuppressionTimeout.value_or(descriptor.LoggingSuppressionTimeout));
+ runtimeInfo->Pooled.store(methodConfig->Pooled.value_or(config->Pooled.value_or(descriptor.Pooled)));
+
+ {
+ auto guard = Guard(runtimeInfo->RequestQueuesLock);
+ for (auto* requestQueue : runtimeInfo->RequestQueues) {
+ ConfigureRequestQueue(runtimeInfo.Get(), requestQueue, methodConfig);
+ }
+ }
+
+ auto loggingSuppressionFailedRequestThrottlerConfig = methodConfig->LoggingSuppressionFailedRequestThrottler
+ ? methodConfig->LoggingSuppressionFailedRequestThrottler
+ : DefaultLoggingSuppressionFailedRequestThrottlerConfig;
+ runtimeInfo->LoggingSuppressionFailedRequestThrottler->Reconfigure(loggingSuppressionFailedRequestThrottlerConfig);
+
+ runtimeInfo->TracingMode.store(
+ methodConfig->TracingMode.value_or(
+ config->TracingMode.value_or(
+ configDefaults->TracingMode)));
+ }
+
+ Config_.Store(config);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error configuring RPC service %v",
+ ServiceId_.ServiceName)
+ << ex;
+ }
+}
+
+void TServiceBase::Configure(
+ const TServiceCommonConfigPtr& configDefaults,
+ const INodePtr& configNode)
+{
+ TServiceConfigPtr config;
+ if (configNode) {
+ try {
+ config = ConvertTo<TServiceConfigPtr>(configNode);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing RPC service %v config",
+ ServiceId_.ServiceName)
+ << ex;
+ }
+ } else {
+ config = New<TServiceConfig>();
+ }
+ DoConfigure(configDefaults, config);
+}
+
+TFuture<void> TServiceBase::Stop()
+{
+ if (!Stopped_.exchange(true)) {
+ if (ActiveRequestCount_.load() == 0) {
+ StopResult_.TrySet();
+ }
+ }
+
+ YT_UNUSED_FUTURE(ServiceLivenessChecker_->Stop());
+
+ return StopResult_.ToFuture();
+}
+
+TServiceBase::TRuntimeMethodInfo* TServiceBase::FindMethodInfo(const TString& method)
+{
+ auto it = MethodMap_.find(method);
+ return it == MethodMap_.end() ? nullptr : it->second.Get();
+}
+
+TServiceBase::TRuntimeMethodInfo* TServiceBase::GetMethodInfoOrThrow(const TString& method)
+{
+ auto* runtimeInfo = FindMethodInfo(method);
+ if (!runtimeInfo) {
+ THROW_ERROR_EXCEPTION("Method %Qv is not registered",
+ method);
+ }
+ return runtimeInfo;
+}
+
+const IInvokerPtr& TServiceBase::GetDefaultInvoker() const
+{
+ return DefaultInvoker_;
+}
+
+void TServiceBase::BeforeInvoke(NRpc::IServiceContext* /*context*/)
+{ }
+
+bool TServiceBase::IsUp(const TCtxDiscoverPtr& /*context*/)
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return true;
+}
+
+std::vector<TString> TServiceBase::SuggestAddresses()
+{
+ VERIFY_THREAD_AFFINITY_ANY();
+
+ return std::vector<TString>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_RPC_SERVICE_METHOD(TServiceBase, Discover)
+{
+ auto replyDelay = FromProto<TDuration>(request->reply_delay());
+
+ context->SetRequestInfo("ReplyDelay: %v",
+ replyDelay);
+
+ auto isUp = IsUp(context);
+
+ // Fast path.
+ if (replyDelay == TDuration::Zero() || isUp) {
+ ReplyDiscoverRequest(context, isUp);
+ return;
+ }
+
+ RegisterDiscoverRequest(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/service_detail.h b/yt/yt/core/rpc/service_detail.h
new file mode 100644
index 0000000000..a710cf092c
--- /dev/null
+++ b/yt/yt/core/rpc/service_detail.h
@@ -0,0 +1,1096 @@
+#pragma once
+
+#include "public.h"
+
+#include "client.h"
+#include "dispatcher.h"
+#include "server_detail.h"
+#include "service.h"
+#include "config.h"
+
+#include <yt/yt/core/compression/codec.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/moody_camel_concurrent_queue.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+
+#include <yt/yt/core/misc/object_pool.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/ring_queue.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/rpc/message_format.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/containers/concurrent_hash/concurrent_hash.h>
+
+#include <library/cpp/yt/threading/rw_spin_lock.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <atomic>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage>
+class TTypedServiceRequest
+ : public TRequestMessage
+{
+public:
+ using TMessage = TRequestMessage;
+
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TSharedRef>, Attachments);
+
+ NConcurrency::IAsyncZeroCopyInputStreamPtr GetAttachmentsStream()
+ {
+ return Context_->GetRequestAttachmentsStream();
+ }
+
+ void Reset()
+ {
+ TRequestMessage::Clear();
+ Attachments_.clear();
+ Context_ = nullptr;
+ }
+
+private:
+ template <
+ class TServiceContext,
+ class TServiceContextWrapper,
+ class TRequestMessage_,
+ class TResponseMessage
+ >
+ friend class TGenericTypedServiceContext;
+
+ IServiceContext* Context_ = nullptr;
+};
+
+template <class TRequestMessage>
+struct TPooledTypedRequestTraits
+ : public TPooledObjectTraitsBase<TTypedServiceRequest<TRequestMessage>>
+{
+ static void Clean(TTypedServiceRequest<TRequestMessage>* message)
+ {
+ message->Clear();
+ message->Attachments().clear();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TResponseMessage>
+class TTypedServiceResponse
+ : public TResponseMessage
+{
+public:
+ using TMessage = TResponseMessage;
+
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TSharedRef>, Attachments);
+
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr GetAttachmentsStream()
+ {
+ return Context_->GetResponseAttachmentsStream();
+ }
+
+private:
+ template <
+ class TServiceContext,
+ class TServiceContextWrapper,
+ class TRequestMessage,
+ class TResponseMessage_
+ >
+ friend class TGenericTypedServiceContext;
+
+ IServiceContext* Context_ = nullptr;
+};
+
+template <class TResponseMessage>
+struct TPooledTypedResponseTraits
+ : public TPooledObjectTraitsBase<TTypedServiceResponse<TResponseMessage>>
+{
+ static void Clean(TTypedServiceResponse<TResponseMessage>* message)
+ {
+ message->Clear();
+ message->Attachments().clear();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Describes request handling options.
+struct THandlerInvocationOptions
+{
+ //! Should we be deserializing the request and serializing the request
+ //! in a separate thread?
+ bool Heavy = false;
+
+ //! In case the client has provided "none" response codec, this value is used instead.
+ NCompression::ECodec ResponseCodec = NCompression::ECodec::None;
+
+ THandlerInvocationOptions SetHeavy(bool value) const;
+ THandlerInvocationOptions SetResponseCodec(NCompression::ECodec value) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <
+ class TServiceContext,
+ class TServiceContextWrapper,
+ class TRequestMessage,
+ class TResponseMessage
+>
+class TGenericTypedServiceContext
+ : public TServiceContextWrapper
+{
+public:
+ using TTypedRequest = TTypedServiceRequest<TRequestMessage>;
+ using TTypedResponse = TTypedServiceResponse<TResponseMessage>;
+
+ TGenericTypedServiceContext(
+ TIntrusivePtr<TServiceContext> context,
+ const THandlerInvocationOptions& options)
+ : TServiceContextWrapper(std::move(context))
+ , Options_(options)
+ {
+ const auto& underlyingContext = this->GetUnderlyingContext();
+ Response_ = underlyingContext->IsPooled()
+ ? ObjectPool<TTypedResponse, TPooledTypedResponseTraits<TResponseMessage>>().Allocate()
+ : std::make_shared<TTypedResponse>();
+ Response_->Context_ = underlyingContext.Get();
+
+ if (this->GetResponseCodec() == NCompression::ECodec::None) {
+ this->SetResponseCodec(Options_.ResponseCodec);
+ }
+ }
+
+ bool DeserializeRequest()
+ {
+ const auto& underlyingContext = this->GetUnderlyingContext();
+ if (underlyingContext->IsPooled()) {
+ Request_ = ObjectPool<TTypedRequest, TPooledTypedRequestTraits<TRequestMessage>>().Allocate();
+ } else {
+ Request_ = std::make_shared<TTypedRequest>();
+ }
+
+ Request_->Context_ = underlyingContext.Get();
+
+ const auto& requestHeader = this->GetRequestHeader();
+ auto body = underlyingContext->GetRequestBody();
+ if (requestHeader.has_request_format()) {
+ auto format = static_cast<EMessageFormat>(requestHeader.request_format());
+
+ NYson::TYsonString formatOptionsYson;
+ if (requestHeader.has_request_format_options()) {
+ formatOptionsYson = NYson::TYsonString(requestHeader.request_format_options());
+ }
+ if (format != EMessageFormat::Protobuf) {
+ body = ConvertMessageFromFormat(
+ body,
+ format,
+ NYson::ReflectProtobufMessageType<TRequestMessage>(),
+ formatOptionsYson);
+ }
+ }
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ std::optional<NCompression::ECodec> bodyCodecId;
+ NCompression::ECodec attachmentCodecId;
+ if (requestHeader.has_request_codec()) {
+ int intCodecId = requestHeader.request_codec();
+ NCompression::ECodec codecId;
+ if (!TryEnumCast(intCodecId, &codecId)) {
+ underlyingContext->Reply(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Request codec %v is not supported",
+ intCodecId));
+ return false;
+ }
+ bodyCodecId = codecId;
+ attachmentCodecId = codecId;
+ } else {
+ bodyCodecId = std::nullopt;
+ attachmentCodecId = NCompression::ECodec::None;
+ }
+
+ bool deserializationSucceeded = bodyCodecId
+ ? TryDeserializeProtoWithCompression(Request_.get(), body, *bodyCodecId)
+ : TryDeserializeProtoWithEnvelope(Request_.get(), body);
+ if (!deserializationSucceeded) {
+ underlyingContext->Reply(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Error deserializing request body"));
+ return false;
+ }
+
+ std::vector<TSharedRef> requestAttachments;
+ try {
+ requestAttachments = DecompressAttachments(
+ underlyingContext->RequestAttachments(),
+ attachmentCodecId);
+ } catch (const std::exception& ex) {
+ underlyingContext->Reply(TError(
+ NRpc::EErrorCode::ProtocolError,
+ "Error deserializing request attachments")
+ << ex);
+ return false;
+ }
+
+ Request_->Attachments() = std::move(requestAttachments);
+
+ return true;
+ }
+
+ const TTypedRequest& Request() const
+ {
+ return *Request_;
+ }
+
+ TTypedRequest& Request()
+ {
+ return *Request_;
+ }
+
+ const TTypedResponse& Response() const
+ {
+ return *Response_;
+ }
+
+ TTypedResponse& Response()
+ {
+ return *Response_;
+ }
+
+
+ using IServiceContext::Reply;
+
+ void Reply()
+ {
+ Reply(TError());
+ }
+
+ void Reply(const TError& error)
+ {
+ if (this->Options_.Heavy) {
+ TDispatcher::Get()->GetHeavyInvoker()->Invoke(BIND(
+ &TGenericTypedServiceContext::DoReply,
+ MakeStrong(this),
+ error));
+ } else {
+ this->DoReply(error);
+ }
+ }
+
+
+ const THandlerInvocationOptions& GetOptions() const
+ {
+ return Options_;
+ }
+
+protected:
+ const THandlerInvocationOptions Options_;
+
+ typename TObjectPool<TTypedRequest, TPooledTypedRequestTraits<TRequestMessage>>::TObjectPtr Request_;
+ typename TObjectPool<TTypedResponse, TPooledTypedResponseTraits<TResponseMessage>>::TObjectPtr Response_;
+
+ struct TSerializedResponse
+ {
+ TSharedRef Body;
+ std::vector<TSharedRef> Attachments;
+ };
+
+ TSerializedResponse SerializeResponse()
+ {
+ const auto& underlyingContext = this->GetUnderlyingContext();
+ const auto& requestHeader = underlyingContext->GetRequestHeader();
+
+ // COMPAT(kiselyovp): legacy RPC codecs
+ NCompression::ECodec attachmentCodecId;
+ auto bodyCodecId = underlyingContext->GetResponseCodec();
+ TSharedRef serializedBody;
+ if (requestHeader.has_response_codec()) {
+ serializedBody = SerializeProtoToRefWithCompression(*Response_, bodyCodecId, false);
+ attachmentCodecId = bodyCodecId;
+ } else {
+ serializedBody = SerializeProtoToRefWithEnvelope(*Response_, bodyCodecId);
+ attachmentCodecId = NCompression::ECodec::None;
+ }
+
+ if (requestHeader.has_response_format()) {
+ int intFormat = requestHeader.response_format();
+ EMessageFormat format;
+ if (!TryEnumCast(intFormat, &format)) {
+ THROW_ERROR_EXCEPTION(
+ EErrorCode::ProtocolError,
+ "Message format %v is not supported",
+ intFormat);
+ }
+
+ NYson::TYsonString formatOptionsYson;
+ if (requestHeader.has_response_format_options()) {
+ formatOptionsYson = NYson::TYsonString(requestHeader.response_format_options());
+ }
+
+ if (format != EMessageFormat::Protobuf) {
+ serializedBody = ConvertMessageToFormat(
+ serializedBody,
+ format,
+ NYson::ReflectProtobufMessageType<TResponseMessage>(),
+ formatOptionsYson);
+ }
+ }
+
+ auto responseAttachments = CompressAttachments(Response_->Attachments(), attachmentCodecId);
+
+ return TSerializedResponse{
+ .Body = std::move(serializedBody),
+ .Attachments = std::move(responseAttachments),
+ };
+ }
+
+ void DoReply(const TError& error)
+ {
+ const auto& underlyingContext = this->GetUnderlyingContext();
+
+ if (error.IsOK()) {
+ TSerializedResponse response;
+ try {
+ response = SerializeResponse();
+ } catch (const std::exception& ex) {
+ underlyingContext->Reply(TError(ex));
+ return;
+ }
+
+ underlyingContext->SetResponseBody(std::move(response.Body));
+ underlyingContext->ResponseAttachments() = std::move(response.Attachments);
+ }
+
+ underlyingContext->Reply(error);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DEFINE_RPC_SERVICE_METHOD_THUNK_VIA_MESSAGES(requestMessage, responseMessage, method) \
+ using TCtx##method = TTypedServiceContextImpl<requestMessage, responseMessage>; \
+ using TCtx##method##Ptr = ::NYT::TIntrusivePtr<TCtx##method>; \
+ using TReq##method = typename TCtx##method::TTypedRequest; \
+ using TRsp##method = typename TCtx##method::TTypedResponse; \
+ \
+ void method##LiteThunk( \
+ const ::NYT::NRpc::IServiceContextPtr& context, \
+ const ::NYT::NRpc::THandlerInvocationOptions& options) \
+ { \
+ auto typedContext = ::NYT::New<TCtx##method>(context, options); \
+ if (!typedContext->DeserializeRequest()) { \
+ return; \
+ } \
+ InitContext(typedContext); \
+ auto* request = &typedContext->Request(); \
+ auto* response = &typedContext->Response(); \
+ this->method(request, response, typedContext); \
+ } \
+ \
+ ::NYT::NRpc::TServiceBase::TLiteHandler method##HeavyThunk( \
+ const ::NYT::NRpc::IServiceContextPtr& context, \
+ const ::NYT::NRpc::THandlerInvocationOptions& options) \
+ { \
+ auto typedContext = ::NYT::New<TCtx##method>(context, options); \
+ if (!typedContext->DeserializeRequest()) { \
+ return ::NYT::NRpc::TServiceBase::TLiteHandler(); \
+ } \
+ return \
+ BIND([=, this] ( \
+ const ::NYT::NRpc::IServiceContextPtr&, \
+ const ::NYT::NRpc::THandlerInvocationOptions&) \
+ { \
+ InitContext(typedContext); \
+ auto* request = &typedContext->Request(); \
+ auto* response = &typedContext->Response(); \
+ this->method(request, response, typedContext); \
+ }); \
+ }
+
+#define DEFINE_RPC_SERVICE_METHOD_THUNK(ns, method) \
+ DEFINE_RPC_SERVICE_METHOD_THUNK_VIA_MESSAGES(ns::TReq##method, ns::TRsp##method, method)
+
+#define DECLARE_RPC_SERVICE_METHOD_VIA_MESSAGES(requestMessage, responseMessage, method) \
+ DEFINE_RPC_SERVICE_METHOD_THUNK_VIA_MESSAGES(requestMessage, responseMessage, method) \
+ \
+ void method( \
+ [[maybe_unused]] TReq##method* request, \
+ [[maybe_unused]] TRsp##method* response, \
+ [[maybe_unused]] const TCtx##method##Ptr& context)
+
+#define DECLARE_RPC_SERVICE_METHOD(ns, method) \
+ DECLARE_RPC_SERVICE_METHOD_VIA_MESSAGES(ns::TReq##method, ns::TRsp##method, method)
+
+#define DEFINE_RPC_SERVICE_METHOD(type, method) \
+ void type::method( \
+ [[maybe_unused]] TReq##method* request, \
+ [[maybe_unused]] TRsp##method* response, \
+ [[maybe_unused]] const TCtx##method##Ptr& context)
+
+#define RPC_SERVICE_METHOD_DESC(method) \
+ ::NYT::NRpc::TServiceBase::TMethodDescriptor( \
+ #method, \
+ BIND_NO_PROPAGATE(&std::remove_reference<decltype(*this)>::type::method##LiteThunk, ::NYT::Unretained(this)), \
+ BIND_NO_PROPAGATE(&std::remove_reference<decltype(*this)>::type::method##HeavyThunk, ::NYT::Unretained(this)))
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRequestQueuePtr CreateRequestQueue(TString name, const NProfiling::TProfiler& profiler = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Provides a base for implementing IService.
+class TServiceBase
+ : public virtual IService
+{
+public:
+ void Configure(
+ const TServiceCommonConfigPtr& configDefaults,
+ const NYTree::INodePtr& configNode) override;
+ TFuture<void> Stop() override;
+
+ const TServiceId& GetServiceId() const override;
+
+ void HandleRequest(
+ std::unique_ptr<NProto::TRequestHeader> header,
+ TSharedRefArray message,
+ NYT::NBus::IBusPtr replyBus) override;
+ void HandleRequestCancellation(TRequestId requestId) override;
+ void HandleStreamingPayload(
+ TRequestId requestId,
+ const TStreamingPayload& payload) override;
+ void HandleStreamingFeedback(
+ TRequestId requestId,
+ const TStreamingFeedback& feedback) override;
+
+protected:
+ const NLogging::TLogger Logger;
+
+ using TLiteHandler = TCallback<void(const IServiceContextPtr&, const THandlerInvocationOptions&)>;
+ using THeavyHandler = TCallback<TLiteHandler(const IServiceContextPtr&, const THandlerInvocationOptions&)>;
+
+ class TServiceContext;
+ using TServiceContextPtr = TIntrusivePtr<TServiceContext>;
+
+ //! This alias provides an option to replace default typed service context class
+ //! with a custom one which may be richer in some way. If a init-like procedure
+ //! is needed to customize specific service context before invoking method handler,
+ //! one may use #InitContext method below.
+ //! See api_service.cpp for an example.
+ template <class TRequestMessage, class TResponseMessage>
+ using TTypedServiceContextImpl = TTypedServiceContext<TRequestMessage, TResponseMessage>;
+
+ //! By default, this method does nothing. You may hide this method by a custom implementation
+ //! (possibly switching argument type to a proper typed context class) in order to customize
+ //! specific service context before invoking method handler.
+ void InitContext(IServiceContextPtr context);
+
+ //! Information needed to a register a service method.
+ struct TMethodDescriptor
+ {
+ // Defaults.
+ TMethodDescriptor(
+ TString method,
+ TLiteHandler liteHandler,
+ THeavyHandler heavyHandler);
+
+ //! When a request is received and parsed, RPC service puts it into a queue.
+ //! This enables specifying a relevant queue on a per-request basis.
+ //! If not given or provides null, the default (per-method) queue is used.
+ IRequestQueueProviderPtr RequestQueueProvider;
+
+ //! When a request is dequeued from its queue, it is delegated to an appropriate
+ //! invoker for the actual execution.
+ //! If not given then the per-service default is used.
+ IInvokerPtr Invoker;
+
+ //! Enables overriding #Invoker on a per-request basis.
+ //! If not given or returns null then #Invoker is used as a fallback.
+ TInvokerProvider InvokerProvider;
+
+ //! Service method name.
+ TString Method;
+
+ //! A handler that will serve lite requests.
+ TLiteHandler LiteHandler;
+
+ //! A handler that will serve heavy requests.
+ THeavyHandler HeavyHandler;
+
+ //! Options to pass to the handler.
+ THandlerInvocationOptions Options;
+
+ //! Maximum number of requests in queue (both waiting and executing).
+ int QueueSizeLimit = 10'000;
+
+ //! Maximum number of requests executing concurrently.
+ int ConcurrencyLimit = 10'000;
+
+ //! System requests are completely transparent to derived classes;
+ //! in particular, |BeforeInvoke| is not called.
+ //! Also system methods do not require authentication.
+ bool System = false;
+
+ //! Log level for events emitted via |Set(Request|Response)Info|-like functions.
+ NLogging::ELogLevel LogLevel = NLogging::ELogLevel::Debug;
+
+ //! Logging suppression timeout for this method requests.
+ TDuration LoggingSuppressionTimeout = TDuration::Zero();
+
+ //! Cancelable requests can be canceled by clients.
+ //! This, however, requires additional book-keeping at server-side so one is advised
+ //! to only mark cancelable those methods taking a considerable time to complete.
+ bool Cancelable = false;
+
+ //! If |true| then Bus is expected to be generating checksums for the whole response content,
+ //! including attachments (unless the connection is local or the checksums are explicitly disabled).
+ //! If |false| then Bus will only be generating such checksums for RPC header and response body
+ //! but not attachments.
+ bool GenerateAttachmentChecksums = true;
+
+ //! If |true| then the method supports attachments streaming.
+ bool StreamingEnabled = false;
+
+ //! If |true| then requests and responses are pooled.
+ bool Pooled = true;
+
+ TMethodDescriptor SetRequestQueueProvider(IRequestQueueProviderPtr value) const;
+ TMethodDescriptor SetInvoker(IInvokerPtr value) const;
+ TMethodDescriptor SetInvokerProvider(TInvokerProvider value) const;
+ TMethodDescriptor SetHeavy(bool value) const;
+ TMethodDescriptor SetResponseCodec(NCompression::ECodec value) const;
+ TMethodDescriptor SetQueueSizeLimit(int value) const;
+ TMethodDescriptor SetConcurrencyLimit(int value) const;
+ TMethodDescriptor SetSystem(bool value) const;
+ TMethodDescriptor SetLogLevel(NLogging::ELogLevel value) const;
+ TMethodDescriptor SetLoggingSuppressionTimeout(TDuration value) const;
+ TMethodDescriptor SetCancelable(bool value) const;
+ TMethodDescriptor SetGenerateAttachmentChecksums(bool value) const;
+ TMethodDescriptor SetStreamingEnabled(bool value) const;
+ TMethodDescriptor SetPooled(bool value) const;
+ };
+
+ struct TErrorCodesCounter
+ {
+ TErrorCodesCounter(const NProfiling::TProfiler& profiler)
+ : Profiler_(profiler)
+ { }
+
+ void RegisterCode(TErrorCode code)
+ {
+ ErrorCodes_.FindOrInsert(code, [&] () {
+ return Profiler_.WithTag("code", ToString(code)).Counter("/code_count");
+ }).first->Increment();
+ }
+
+ private:
+ NYT::NConcurrency::TSyncMap<TErrorCode, NProfiling::TCounter> ErrorCodes_;
+ NProfiling::TProfiler Profiler_;
+ };
+
+ //! Per-user and per-method profiling counters.
+ struct TMethodPerformanceCounters
+ : public TRefCounted
+ {
+ TMethodPerformanceCounters(const NProfiling::TProfiler& profiler, const THistogramConfigPtr& histogramConfig);
+
+ //! Counts the number of method calls.
+ NProfiling::TCounter RequestCounter;
+
+ //! Counts the number of canceled method calls.
+ NProfiling::TCounter CanceledRequestCounter;
+
+ //! Counts the number of failed method calls.
+ NProfiling::TCounter FailedRequestCounter;
+
+ //! Counts the number of timed out method calls.
+ NProfiling::TCounter TimedOutRequestCounter;
+
+ //! Time spent while handling the request.
+ NProfiling::TEventTimer ExecutionTimeCounter;
+
+ //! Time spent at the caller's side before the request arrived into the server queue.
+ NProfiling::TEventTimer RemoteWaitTimeCounter;
+
+ //! Time spent while the request was waiting in the server queue.
+ NProfiling::TEventTimer LocalWaitTimeCounter;
+
+ //! Time between the request arrival and the moment when it is fully processed.
+ NProfiling::TEventTimer TotalTimeCounter;
+
+ //! CPU time spent in the trace context associated with the request (locally).
+ NProfiling::TTimeCounter TraceContextTimeCounter;
+
+ //! Counts the number of bytes in requests message body.
+ NProfiling::TCounter RequestMessageBodySizeCounter;
+
+ //! Counts the number of bytes in request message attachment.
+ NProfiling::TCounter RequestMessageAttachmentSizeCounter;
+
+ //! Counts the number of bytes in response message body.
+ NProfiling::TCounter ResponseMessageBodySizeCounter;
+
+ //! Counts the number of bytes in response message attachment.
+ NProfiling::TCounter ResponseMessageAttachmentSizeCounter;
+
+ TErrorCodesCounter ErrorCodes;
+ };
+
+ using TMethodPerformanceCountersPtr = TIntrusivePtr<TMethodPerformanceCounters>;
+
+ //! Describes a service method and its runtime statistics.
+ struct TRuntimeMethodInfo
+ : public TRefCounted
+ {
+ TRuntimeMethodInfo(
+ TServiceId serviceId,
+ TMethodDescriptor descriptor,
+ const NProfiling::TProfiler& profiler);
+
+ // TODO(kvk1920): Generalize as default queue provider.
+ TRequestQueue* GetDefaultRequestQueue();
+
+ const TServiceId ServiceId;
+ const TMethodDescriptor Descriptor;
+ const NProfiling::TProfiler Profiler;
+
+ const TRequestQueuePtr DefaultRequestQueue;
+
+ NLogging::TLoggingAnchor* const RequestLoggingAnchor;
+ NLogging::TLoggingAnchor* const ResponseLoggingAnchor;
+
+ std::atomic<bool> Heavy = false;
+ std::atomic<bool> Pooled = true;
+
+ std::atomic<int> QueueSizeLimit = 0;
+ std::atomic<int> ConcurrencyLimit = 0;
+ NProfiling::TCounter RequestQueueSizeLimitErrorCounter;
+ NProfiling::TCounter UnauthenticatedRequestsCounter;
+
+ std::atomic<NLogging::ELogLevel> LogLevel = {};
+ std::atomic<TDuration> LoggingSuppressionTimeout = {};
+
+ using TNonowningPerformanceCountersKey = std::tuple<TStringBuf, TRequestQueue*>;
+ using TOwningPerformanceCountersKey = std::tuple<TString, TRequestQueue*>;
+ using TPerformanceCountersKeyHash = THash<TNonowningPerformanceCountersKey>;
+ struct TPerformanceCountersKeyEquals;
+ using TPerformanceCountersMap = NConcurrency::TSyncMap<
+ TOwningPerformanceCountersKey,
+ TMethodPerformanceCountersPtr,
+ TPerformanceCountersKeyHash,
+ TPerformanceCountersKeyEquals
+ >;
+ TPerformanceCountersMap PerformanceCountersMap;
+ TMethodPerformanceCountersPtr BasePerformanceCounters;
+ TMethodPerformanceCountersPtr RootPerformanceCounters;
+
+ NConcurrency::IReconfigurableThroughputThrottlerPtr LoggingSuppressionFailedRequestThrottler;
+
+ std::atomic<ERequestTracingMode> TracingMode = ERequestTracingMode::Enable;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, RequestQueuesLock);
+ std::vector<TRequestQueue*> RequestQueues;
+ };
+
+ using TRuntimeMethodInfoPtr = TIntrusivePtr<TRuntimeMethodInfo>;
+
+ //! Service-level performance counters.
+ class TPerformanceCounters
+ : public TRefCounted
+ {
+ public:
+ explicit TPerformanceCounters(const NProfiling::TProfiler& profiler)
+ : Profiler_(profiler.WithHot().WithSparse())
+ { }
+
+ void IncrementRequestsPerUserAgent(TStringBuf userAgent)
+ {
+ RequestsPerUserAgent_.FindOrInsert(userAgent, [&] {
+ return Profiler_.WithRequiredTag("user_agent", TString(userAgent)).Counter("/user_agent");
+ }).first->Increment();
+ }
+
+ private:
+ const NProfiling::TProfiler Profiler_;
+
+ //! Number of requests per user agent.
+ NConcurrency::TSyncMap<TString, NProfiling::TCounter> RequestsPerUserAgent_;
+ };
+
+ using TPerformanceCountersPtr = TIntrusivePtr<TPerformanceCounters>;
+
+
+ DECLARE_RPC_SERVICE_METHOD(NProto, Discover);
+
+ //! Initializes the instance.
+ /*!
+ * \param defaultInvoker
+ * An invoker that will be used for serving method invocations unless
+ * configured otherwise (see #RegisterMethod).
+ *
+ * \param serviceName
+ * A name of the service.
+ *
+ * \param logger
+ * A logger that will be used to log various debugging information
+ * regarding service activity.
+ */
+ TServiceBase(
+ IInvokerPtr defaultInvoker,
+ const TServiceDescriptor& descriptor,
+ const NLogging::TLogger& logger,
+ TRealmId realmId = NullRealmId,
+ IAuthenticatorPtr authenticator = nullptr);
+
+ //! Registers a method handler.
+ //! This call is must be performed prior to service registration.
+ TRuntimeMethodInfoPtr RegisterMethod(const TMethodDescriptor& descriptor);
+
+ //! Register a feature as being supported by server.
+ //! This call is must be performed prior to service registration.
+ template <class E>
+ void DeclareServerFeature(E featureId);
+
+ //! Validates the required server features against the set of supported ones.
+ //! Throws on failure.
+ void ValidateRequestFeatures(const IServiceContextPtr& context);
+
+ //! Returns a (non-owning!) pointer to TRuntimeMethodInfo for a given method's name
+ //! or |nullptr| if no such method is registered.
+ TRuntimeMethodInfo* FindMethodInfo(const TString& method);
+
+ //! Similar to #FindMethodInfo but throws if no method is found.
+ TRuntimeMethodInfo* GetMethodInfoOrThrow(const TString& method);
+
+ //! Returns the default invoker passed during construction.
+ const IInvokerPtr& GetDefaultInvoker() const;
+
+ //! Called right before each method handler invocation.
+ virtual void BeforeInvoke(IServiceContext* context);
+
+ //! Used by peer discovery.
+ //! Returns |true| is this service instance is up, i.e. can handle requests.
+ /*!
+ * \note
+ * Thread affinity: any
+ */
+ virtual bool IsUp(const TCtxDiscoverPtr& context);
+
+ //! Used by peer discovery.
+ //! Returns addresses of neighboring peers to be suggested to the client.
+ /*!
+ * \note
+ * Thread affinity: any
+ */
+ virtual std::vector<TString> SuggestAddresses();
+
+ //! Part of #DoConfigure
+ //! #DoConfigure configures already registered methods.
+ //! Configures sensor types for methods which will be registered after this function call.
+ void DoConfigureHistogramTimer(
+ const TServiceCommonConfigPtr& configDefaults,
+ const TServiceConfigPtr& config);
+
+ //! A typed implementation of #Configure.
+ void DoConfigure(
+ const TServiceCommonConfigPtr& configDefaults,
+ const TServiceConfigPtr& config);
+
+private:
+ friend class TRequestQueue;
+
+ const IInvokerPtr DefaultInvoker_;
+ const IAuthenticatorPtr Authenticator_;
+ const TServiceDescriptor ServiceDescriptor_;
+ const TServiceId ServiceId_;
+
+ const NProfiling::TProfiler Profiler_;
+
+ TAtomicIntrusivePtr<TServiceConfig> Config_;
+
+ struct TPendingPayloadsEntry
+ {
+ std::vector<TStreamingPayload> Payloads;
+ NConcurrency::TLease Lease;
+ };
+
+ std::atomic<bool> Active_ = false;
+
+ THashMap<TString, TRuntimeMethodInfoPtr> MethodMap_;
+
+ THashSet<int> SupportedServerFeatureIds_;
+
+ struct TRequestBucket
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ THashMap<TRequestId, TServiceContext*> RequestIdToContext;
+ THashMap<TRequestId, TPendingPayloadsEntry> RequestIdToPendingPayloads;
+ };
+
+ static constexpr size_t RequestBucketCount = 64;
+ std::array<TRequestBucket, RequestBucketCount> RequestBuckets_;
+
+ struct TReplyBusBucket
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ THashMap<NYT::NBus::IBusPtr, THashSet<TServiceContext*>> ReplyBusToContexts;
+ };
+
+ static constexpr size_t ReplyBusBucketCount = 64;
+ std::array<TReplyBusBucket, ReplyBusBucketCount> ReplyBusBuckets_;
+
+ struct TQueuedReplyBucket
+ {
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ THashMap<TRequestId, TFuture<void>> QueuedReplies;
+ };
+
+ static constexpr size_t QueuedReplyBucketCount = 64;
+ std::array<TQueuedReplyBucket, ReplyBusBucketCount> QueuedReplyBusBuckets_;
+
+ static constexpr auto DefaultPendingPayloadsTimeout = TDuration::Seconds(30);
+ std::atomic<TDuration> PendingPayloadsTimeout_ = DefaultPendingPayloadsTimeout;
+
+ std::atomic<bool> Stopped_ = false;
+ const TPromise<void> StopResult_ = NewPromise<void>();
+ std::atomic<int> ActiveRequestCount_ = 0;
+
+ std::atomic<int> AuthenticationQueueSize_ = 0;
+ NProfiling::TEventTimer AuthenticationTimer_;
+
+ static constexpr auto DefaultAuthenticationQueueSizeLimit = 10'000;
+ std::atomic<int> AuthenticationQueueSizeLimit_ = DefaultAuthenticationQueueSizeLimit;
+
+ std::atomic<bool> EnablePerUserProfiling_ = false;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, HistogramConfigLock_);
+ THistogramConfigPtr HistogramTimerProfiling{};
+
+ std::atomic<bool> EnableErrorCodeCounting = false;
+
+ const NConcurrency::TPeriodicExecutorPtr ServiceLivenessChecker_;
+
+ using TDiscoverRequestSet = TConcurrentHashMap<TCtxDiscoverPtr, int>;
+ THashMap<TString, TDiscoverRequestSet> DiscoverRequestsByPayload_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, DiscoverRequestsByPayloadLock_);
+
+ TPerformanceCountersPtr PerformanceCounters_;
+
+ struct TAcceptedRequest
+ {
+ TRequestId RequestId;
+ NYT::NBus::IBusPtr ReplyBus;
+ TRuntimeMethodInfo* RuntimeInfo;
+ NTracing::TTraceContextPtr TraceContext;
+ std::unique_ptr<NRpc::NProto::TRequestHeader> Header;
+ TSharedRefArray Message;
+ TRequestQueue* RequestQueue;
+ };
+
+ void DoDeclareServerFeature(int featureId);
+ TError DoCheckRequestCompatibility(const NRpc::NProto::TRequestHeader& header);
+ TError DoCheckRequestProtocol(const NRpc::NProto::TRequestHeader& header);
+ TError DoCheckRequestFeatures(const NRpc::NProto::TRequestHeader& header);
+
+ void OnRequestTimeout(TRequestId requestId, bool aborted);
+ void OnReplyBusTerminated(const NYT::NBus::IBusPtr& bus, const TError& error);
+
+ void ReplyError(
+ TError error,
+ const NProto::TRequestHeader& header,
+ const NYT::NBus::IBusPtr& replyBus);
+
+ void OnRequestAuthenticated(
+ const NProfiling::TWallTimer& timer,
+ TAcceptedRequest&& acceptedRequest,
+ const TErrorOr<TAuthenticationResult>& authResultOrError);
+ bool IsAuthenticationNeeded(const TAcceptedRequest& acceptedRequest);
+ void HandleAuthenticatedRequest(TAcceptedRequest&& acceptedRequest);
+
+ TRequestQueue* GetRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ const NRpc::NProto::TRequestHeader& requestHeader);
+ void RegisterRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ TRequestQueue* requestQueue);
+ void ConfigureRequestQueue(
+ TRuntimeMethodInfo* runtimeInfo,
+ TRequestQueue* requestQueue,
+ const TMethodConfigPtr& config);
+
+ TRequestBucket* GetRequestBucket(TRequestId requestId);
+ TQueuedReplyBucket* GetQueuedReplyBucket(TRequestId requestId);
+ TReplyBusBucket* GetReplyBusBucket(const NYT::NBus::IBusPtr& bus);
+
+ void RegisterRequest(TServiceContext* context);
+ void UnregisterRequest(TServiceContext* context);
+ TServiceContextPtr FindRequest(TRequestId requestId);
+ TServiceContextPtr DoFindRequest(TRequestBucket* bucket, TRequestId requestId);
+
+ void RegisterQueuedReply(TRequestId requestId, TFuture<void> reply);
+ void UnregisterQueuedReply(TRequestId requestId);
+ bool TryCancelQueuedReply(TRequestId requestId);
+
+ TPendingPayloadsEntry* DoGetOrCreatePendingPayloadsEntry(TRequestBucket* bucket, TRequestId requestId);
+ std::vector<TStreamingPayload> GetAndErasePendingPayloads(TRequestId requestId);
+ void OnPendingPayloadsLeaseExpired(TRequestId requestId);
+
+ TMethodPerformanceCountersPtr CreateMethodPerformanceCounters(
+ TRuntimeMethodInfo* runtimeInfo,
+ const TRuntimeMethodInfo::TNonowningPerformanceCountersKey& key);
+ TMethodPerformanceCounters* GetMethodPerformanceCounters(
+ TRuntimeMethodInfo* runtimeInfo,
+ const TRuntimeMethodInfo::TNonowningPerformanceCountersKey& key);
+
+ TPerformanceCounters* GetPerformanceCounters();
+
+ void SetActive();
+ void ValidateInactive();
+
+ bool IsStopped();
+ void IncrementActiveRequestCount();
+ void DecrementActiveRequestCount();
+
+ void RegisterDiscoverRequest(const TCtxDiscoverPtr& context);
+ void ReplyDiscoverRequest(const TCtxDiscoverPtr& context, bool isUp);
+
+ void OnDiscoverRequestReplyDelayReached(TCtxDiscoverPtr context);
+
+ static TString GetDiscoverRequestPayload(const TCtxDiscoverPtr& context);
+
+ void OnServiceLivenessCheck();
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceBase)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRequestQueue
+ : public TRefCounted
+{
+public:
+ explicit TRequestQueue(TString name, NProfiling::TProfiler profiler);
+
+ bool Register(TServiceBase* service, TServiceBase::TRuntimeMethodInfo* runtimeInfo);
+ void Configure(const TMethodConfigPtr& config);
+
+ bool IsQueueLimitSizeExceeded() const;
+
+ int GetQueueSize() const;
+ int GetConcurrency() const;
+
+ void OnRequestArrived(TServiceBase::TServiceContextPtr context);
+ void OnRequestFinished();
+
+ void ConfigureWeightThrottler(const NConcurrency::TThroughputThrottlerConfigPtr& config);
+ void ConfigureBytesThrottler(const NConcurrency::TThroughputThrottlerConfigPtr& config);
+
+ const TString& GetName() const;
+
+private:
+ const TString Name_;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, RegisterLock_);
+ std::atomic<bool> Registered_ = false;
+ TServiceBase* Service_;
+ TServiceBase::TRuntimeMethodInfo* RuntimeInfo_ = nullptr;
+
+ std::atomic<int> Concurrency_ = 0;
+
+ struct TRequestThrottler
+ {
+ const NConcurrency::IReconfigurableThroughputThrottlerPtr Throttler;
+ std::atomic<bool> Specified = false;
+
+ void Reconfigure(const NConcurrency::TThroughputThrottlerConfigPtr& config);
+ };
+
+ TRequestThrottler BytesThrottler_;
+ TRequestThrottler WeightThrottler_;
+ std::atomic<bool> Throttled_ = false;
+
+ std::atomic<int> QueueSize_ = 0;
+ moodycamel::ConcurrentQueue<TServiceBase::TServiceContextPtr> Queue_;
+
+
+ void ScheduleRequestsFromQueue();
+ void RunRequest(TServiceBase::TServiceContextPtr context);
+
+ int IncrementQueueSize();
+ void DecrementQueueSize();
+
+ int IncrementConcurrency();
+ void DecrementConcurrency();
+
+ bool AreThrottlersOverdrafted() const;
+ void AcquireThrottlers(const TServiceBase::TServiceContextPtr& context);
+ void SubscribeToThrottlers();
+};
+
+DEFINE_REFCOUNTED_TYPE(TRequestQueue)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class T>
+struct TPooledObjectTraits<
+ class NRpc::TTypedServiceRequest<TRequestMessage>,
+ T
+>
+ : public TPooledObjectTraitsBase<
+ typename NRpc::TTypedServiceRequest<TRequestMessage>
+ >
+{
+ static void Clean(NRpc::TTypedServiceRequest<TRequestMessage>* message)
+ {
+ message->Clear();
+ message->Attachments().clear();
+ }
+};
+
+template <class TResponseMessage, class T>
+struct TPooledObjectTraits<
+ class NRpc::TTypedServiceResponse<TResponseMessage>,
+ T
+>
+ : public TPooledObjectTraitsBase<
+ typename NRpc::TTypedServiceRequest<TResponseMessage>
+ >
+{
+ static void Clean(NRpc::TTypedServiceRequest<TResponseMessage>* message)
+ {
+ message->Clear();
+ message->Attachments().clear();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define SERVICE_DETAIL_INL_H_
+#include "service_detail-inl.h"
+#undef SERVICE_DETAIL_INL_H_
diff --git a/yt/yt/core/rpc/static_channel_factory.cpp b/yt/yt/core/rpc/static_channel_factory.cpp
new file mode 100644
index 0000000000..a4be915ae7
--- /dev/null
+++ b/yt/yt/core/rpc/static_channel_factory.cpp
@@ -0,0 +1,26 @@
+#include "static_channel_factory.h"
+
+namespace NYT::NRpc {
+
+using namespace NYT::NBus;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStaticChannelFactoryPtr TStaticChannelFactory::Add(const TString& address, IChannelPtr channel)
+{
+ YT_VERIFY(ChannelMap.emplace(address, channel).second);
+ return this;
+}
+
+IChannelPtr TStaticChannelFactory::CreateChannel(const TString& address)
+{
+ auto it = ChannelMap.find(address);
+ if (it == ChannelMap.end()) {
+ THROW_ERROR_EXCEPTION("Unknown address %Qv", address);
+ }
+ return it->second;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/static_channel_factory.h b/yt/yt/core/rpc/static_channel_factory.h
new file mode 100644
index 0000000000..43cc06c148
--- /dev/null
+++ b/yt/yt/core/rpc/static_channel_factory.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "public.h"
+#include "channel.h"
+#include "helpers.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStaticChannelFactory
+ : public IChannelFactory
+{
+public:
+ TStaticChannelFactoryPtr Add(const TString& address, IChannelPtr channel);
+ IChannelPtr CreateChannel(const TString& address) override;
+
+private:
+ THashMap<TString, IChannelPtr> ChannelMap;
+};
+
+DEFINE_REFCOUNTED_TYPE(TStaticChannelFactory)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/stream-inl.h b/yt/yt/core/rpc/stream-inl.h
new file mode 100644
index 0000000000..4474158d4e
--- /dev/null
+++ b/yt/yt/core/rpc/stream-inl.h
@@ -0,0 +1,57 @@
+#ifndef STREAM_INL_H_
+#error "Direct inclusion of this file is not allowed, include stream.h"
+// For the sake of sane code completion.
+#include "stream.h"
+#endif
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> CreateRpcClientInputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request)
+{
+ auto invokeResult = request->Invoke().template As<void>();
+ return request->GetRequestAttachmentsStream()->Close().Apply(BIND([=] () {
+ return New<NDetail::TRpcClientInputStream>(
+ std::move(request),
+ std::move(invokeResult));
+ })).template As<NConcurrency::IAsyncZeroCopyInputStreamPtr>();
+}
+
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request,
+ bool feedbackEnabled)
+{
+ auto invokeResult = request->Invoke().template As<void>();
+ return NDetail::CreateRpcClientOutputStreamFromInvokedRequest(
+ std::move(request),
+ std::move(invokeResult),
+ feedbackEnabled);
+}
+
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request,
+ TCallback<void(TSharedRef)> metaHandler)
+{
+ auto invokeResult = request->Invoke().template As<void>();
+ auto metaHandlerResult = request->GetResponseAttachmentsStream()->Read()
+ .Apply(metaHandler);
+ return metaHandlerResult.Apply(BIND ([=] () {
+ return NDetail::CreateRpcClientOutputStreamFromInvokedRequest(
+ std::move(request),
+ std::move(invokeResult));
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
diff --git a/yt/yt/core/rpc/stream.cpp b/yt/yt/core/rpc/stream.cpp
new file mode 100644
index 0000000000..cf1cc9ff93
--- /dev/null
+++ b/yt/yt/core/rpc/stream.cpp
@@ -0,0 +1,873 @@
+#include "stream.h"
+#include "client.h"
+#include "service_detail.h"
+
+#include <yt/yt/core/compression/codec.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static constexpr ssize_t MaxWindowSize = 16384;
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetStreamingAttachmentSize(TRef attachment)
+{
+ if (!attachment || attachment.Size() == 0) {
+ return 1;
+ } else {
+ return attachment.Size();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttachmentsInputStream::TAttachmentsInputStream(
+ TClosure readCallback,
+ IInvokerPtr compressionInvoker,
+ std::optional<TDuration> timeout)
+ : ReadCallback_(std::move(readCallback))
+ , CompressionInvoker_(std::move(compressionInvoker))
+ , Timeout_(timeout)
+ , Window_(MaxWindowSize)
+{ }
+
+TFuture<TSharedRef> TAttachmentsInputStream::Read()
+{
+ auto guard = Guard(Lock_);
+
+ // Failure here indicates an attempt to read past EOSs.
+ if (Closed_) {
+ return MakeFuture<TSharedRef>(TError("Stream is already closed"));
+ }
+
+ if (!Error_.IsOK()) {
+ return MakeFuture<TSharedRef>(Error_);
+ }
+
+ // Failure here indicates that another Read request is already in progress.
+ YT_VERIFY(!Promise_);
+
+ if (Queue_.empty()) {
+ Promise_ = NewPromise<TSharedRef>();
+ if (Timeout_) {
+ TimeoutCookie_ = TDelayedExecutor::Submit(
+ BIND(&TAttachmentsInputStream::OnTimeout, MakeWeak(this)),
+ *Timeout_);
+ }
+ return Promise_.ToFuture();
+ } else {
+ auto entry = std::move(Queue_.front());
+ Queue_.pop();
+ ReadPosition_ += entry.CompressedSize;
+ if (!entry.Attachment) {
+ YT_VERIFY(!Closed_);
+ Closed_ = true;
+ }
+ guard.Release();
+ ReadCallback_();
+ return MakeFuture(entry.Attachment);
+ }
+}
+
+void TAttachmentsInputStream::EnqueuePayload(const TStreamingPayload& payload)
+{
+ if (payload.Codec == NCompression::ECodec::None) {
+ DoEnqueuePayload(payload, payload.Attachments);
+ } else {
+ CompressionInvoker_->Invoke(
+ BIND(&TAttachmentsInputStream::DecompressAndEnqueuePayload, MakeWeak(this), payload));
+ }
+}
+
+void TAttachmentsInputStream::DecompressAndEnqueuePayload(const TStreamingPayload& payload)
+{
+ try {
+ std::vector<TSharedRef> decompressedAttachments;
+ decompressedAttachments.reserve(payload.Attachments.size());
+ auto* codec = NCompression::GetCodec(payload.Codec);
+ for (const auto& attachment : payload.Attachments) {
+ TSharedRef decompressedAttachment;
+ if (attachment) {
+ decompressedAttachment = codec->Decompress(attachment);
+ }
+ decompressedAttachments.push_back(std::move(decompressedAttachment));
+ }
+ DoEnqueuePayload(payload, decompressedAttachments);
+ } catch (const std::exception& ex) {
+ Abort(ex, false);
+ }
+}
+
+void TAttachmentsInputStream::DoEnqueuePayload(
+ const TStreamingPayload& payload,
+ const std::vector<TSharedRef>& decompressedAttachments)
+{
+ auto guard = Guard(Lock_);
+
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ Window_.AddPacket(
+ payload.SequenceNumber,
+ {
+ payload,
+ decompressedAttachments,
+ },
+ [&] (auto&& packet) {
+ for (size_t index = 0; index < packet.Payload.Attachments.size(); ++index) {
+ Queue_.push({
+ packet.DecompressedAttachments[index],
+ GetStreamingAttachmentSize(packet.Payload.Attachments[index])
+ });
+ }
+ });
+
+ if (Promise_ && !Queue_.empty()) {
+ auto entry = std::move(Queue_.front());
+ Queue_.pop();
+
+ auto promise = std::move(Promise_);
+
+ ReadPosition_ += entry.CompressedSize;
+
+ if (!entry.Attachment) {
+ YT_VERIFY(!Closed_);
+ Closed_ = true;
+ }
+
+ TDelayedExecutor::CancelAndClear(TimeoutCookie_);
+
+ guard.Release();
+
+ promise.Set(std::move(entry.Attachment));
+ ReadCallback_();
+ }
+}
+
+void TAttachmentsInputStream::Abort(const TError& error, bool fireAborted)
+{
+ auto guard = Guard(Lock_);
+ DoAbort(guard, error, fireAborted);
+}
+
+void TAttachmentsInputStream::AbortUnlessClosed(const TError& error, bool fireAborted)
+{
+ auto guard = Guard(Lock_);
+
+ if (Closed_) {
+ return;
+ }
+
+ static const auto FinishedError = TError("Request finished");
+ DoAbort(
+ guard,
+ error.IsOK() ? FinishedError : error,
+ fireAborted);
+}
+
+void TAttachmentsInputStream::DoAbort(TGuard<NThreading::TSpinLock>& guard, const TError& error, bool fireAborted)
+{
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ Error_ = error;
+
+ auto promise = Promise_;
+
+ guard.Release();
+
+ if (promise) {
+ promise.Set(error);
+ }
+
+ if (fireAborted) {
+ Aborted_.Fire();
+ }
+}
+
+void TAttachmentsInputStream::OnTimeout()
+{
+ Abort(TError(NYT::EErrorCode::Timeout, "Attachments stream read timed out")
+ << TErrorAttribute("timeout", *Timeout_));
+}
+
+TStreamingFeedback TAttachmentsInputStream::GetFeedback() const
+{
+ return TStreamingFeedback{
+ ReadPosition_.load()
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttachmentsOutputStream::TAttachmentsOutputStream(
+ NCompression::ECodec codec,
+ IInvokerPtr compressisonInvoker,
+ TClosure pullCallback,
+ ssize_t windowSize,
+ std::optional<TDuration> timeout)
+ : Codec_(codec)
+ , CompressionInvoker_(std::move(compressisonInvoker))
+ , PullCallback_(std::move(pullCallback))
+ , WindowSize_(windowSize)
+ , Timeout_(timeout)
+ , Window_(std::numeric_limits<ssize_t>::max())
+{ }
+
+TFuture<void> TAttachmentsOutputStream::Write(const TSharedRef& data)
+{
+ YT_VERIFY(data);
+ auto promise = NewPromise<void>();
+ TDelayedExecutorCookie timeoutCookie;
+ if (Timeout_) {
+ timeoutCookie = TDelayedExecutor::Submit(
+ BIND(&TAttachmentsOutputStream::OnTimeout, MakeWeak(this)),
+ *Timeout_);
+ }
+ if (Codec_ == NCompression::ECodec::None) {
+ auto guard = Guard(Lock_);
+ TWindowPacket packet{
+ data,
+ promise,
+ std::move(timeoutCookie)
+ };
+ OnWindowPacketsReady(TMutableRange(&packet, 1), guard);
+ } else {
+ auto sequenceNumber = CompressionSequenceNumber_++;
+ CompressionInvoker_->Invoke(BIND([=, this, this_ = MakeStrong(this)] {
+ auto* codec = NCompression::GetCodec(Codec_);
+ auto compressedData = codec->Compress(data);
+ auto guard = Guard(Lock_);
+ std::vector<TWindowPacket> packets;
+ Window_.AddPacket(
+ sequenceNumber,
+ {compressedData, promise, std::move(timeoutCookie)},
+ [&] (auto&& packet) {
+ packets.push_back(std::move(packet));
+ });
+ OnWindowPacketsReady(packets, guard);
+ }));
+ }
+ return promise.ToFuture();
+}
+
+void TAttachmentsOutputStream::OnWindowPacketsReady(TMutableRange<TWindowPacket> packets, TGuard<NThreading::TSpinLock>& guard)
+{
+ if (ClosePromise_) {
+ guard.Release();
+ TError error("Stream is already closed");
+ for (auto& packet : packets) {
+ TDelayedExecutor::CancelAndClear(packet.TimeoutCookie);
+ packet.Promise.Set(error);
+ }
+ return;
+ }
+
+ if (!Error_.IsOK()) {
+ guard.Release();
+ for (auto& packet : packets) {
+ TDelayedExecutor::CancelAndClear(packet.TimeoutCookie);
+ packet.Promise.Set(Error_);
+ }
+ return;
+ }
+
+ std::vector<TPromise<void>> promisesToSet;
+ for (auto& packet : packets) {
+ WritePosition_ += GetStreamingAttachmentSize(packet.Data);
+ DataQueue_.push(std::move(packet.Data));
+
+ if (WritePosition_ - ReadPosition_ <= WindowSize_) {
+ TDelayedExecutor::CancelAndClear(packet.TimeoutCookie);
+ promisesToSet.push_back(std::move(packet.Promise));
+ ConfirmationQueue_.push({
+ .Position = WritePosition_
+ });
+ } else {
+ ConfirmationQueue_.push({
+ .Position = WritePosition_,
+ .Promise = std::move(packet.Promise),
+ .TimeoutCookie = std::move(packet.TimeoutCookie)
+ });
+ }
+ }
+
+ MaybeInvokePullCallback(guard);
+
+ guard.Release();
+
+ for (const auto& promise : promisesToSet) {
+ promise.Set();
+ }
+}
+
+TFuture<void> TAttachmentsOutputStream::Close()
+{
+ auto guard = Guard(Lock_);
+
+ if (!Error_.IsOK()) {
+ return MakeFuture(Error_);
+ }
+
+ if (ClosePromise_) {
+ return ClosePromise_.ToFuture();
+ }
+
+ auto promise = ClosePromise_ = NewPromise<void>();
+ if (Timeout_) {
+ CloseTimeoutCookie_ = TDelayedExecutor::Submit(
+ BIND(&TAttachmentsOutputStream::OnTimeout, MakeWeak(this)),
+ *Timeout_);
+ }
+
+ TSharedRef nullAttachment;
+ DataQueue_.push(nullAttachment);
+ WritePosition_ += GetStreamingAttachmentSize(nullAttachment);
+
+ ConfirmationQueue_.push({
+ .Position = WritePosition_
+ });
+
+ MaybeInvokePullCallback(guard);
+
+ return promise.ToFuture();
+}
+
+void TAttachmentsOutputStream::Abort(const TError& error)
+{
+ auto guard = Guard(Lock_);
+
+ DoAbort(guard, error);
+}
+
+void TAttachmentsOutputStream::AbortUnlessClosed(const TError& error, bool fireAborted)
+{
+ auto guard = Guard(Lock_);
+
+ if (Closed_) {
+ return;
+ }
+
+ DoAbort(
+ guard,
+ error.IsOK() ? TError("Request is already completed") : error,
+ fireAborted);
+}
+
+void TAttachmentsOutputStream::DoAbort(TGuard<NThreading::TSpinLock>& guard, const TError& error, bool fireAborted)
+{
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ Error_ = error;
+
+ std::vector<TPromise<void>> promises;
+ promises.reserve(ConfirmationQueue_.size());
+ while (!ConfirmationQueue_.empty()) {
+ auto& entry = ConfirmationQueue_.front();
+ TDelayedExecutor::CancelAndClear(entry.TimeoutCookie);
+ promises.push_back(std::move(entry.Promise));
+ ConfirmationQueue_.pop();
+ }
+
+ if (ClosePromise_) {
+ promises.push_back(ClosePromise_);
+ TDelayedExecutor::CancelAndClear(CloseTimeoutCookie_);
+ }
+
+ guard.Release();
+
+ for (const auto& promise : promises) {
+ if (promise) {
+ // Avoid double-setting ClosePromise_.
+ promise.TrySet(error);
+ }
+ }
+
+ if (fireAborted) {
+ Aborted_.Fire();
+ }
+}
+
+void TAttachmentsOutputStream::OnTimeout()
+{
+ Abort(TError(NYT::EErrorCode::Timeout, "Attachments stream write timed out")
+ << TErrorAttribute("timeout", *Timeout_));
+}
+
+void TAttachmentsOutputStream::HandleFeedback(const TStreamingFeedback& feedback)
+{
+ auto guard = Guard(Lock_);
+
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ if (ReadPosition_ >= feedback.ReadPosition) {
+ return;
+ }
+
+ if (feedback.ReadPosition > WritePosition_) {
+ THROW_ERROR_EXCEPTION("Stream read position exceeds write position: %v > %v",
+ feedback.ReadPosition,
+ WritePosition_);
+ }
+
+ ReadPosition_ = feedback.ReadPosition;
+
+ std::vector<TPromise<void>> promises;
+ promises.reserve(ConfirmationQueue_.size());
+
+ while (!ConfirmationQueue_.empty() &&
+ ConfirmationQueue_.front().Position <= ReadPosition_ + WindowSize_)
+ {
+ auto& entry = ConfirmationQueue_.front();
+ TDelayedExecutor::CancelAndClear(entry.TimeoutCookie);
+ promises.push_back(std::move(entry.Promise));
+ ConfirmationQueue_.pop();
+ }
+
+ if (ClosePromise_ && ReadPosition_ == WritePosition_) {
+ promises.push_back(ClosePromise_);
+ TDelayedExecutor::CancelAndClear(CloseTimeoutCookie_);
+ Closed_ = true;
+ }
+
+ MaybeInvokePullCallback(guard);
+
+ guard.Release();
+
+ for (const auto& promise : promises) {
+ if (promise) {
+ // Avoid double-setting ClosePromise_.
+ promise.TrySet();
+ }
+ }
+}
+
+std::optional<TStreamingPayload> TAttachmentsOutputStream::TryPull()
+{
+ auto guard = Guard(Lock_);
+
+ if (!Error_.IsOK()) {
+ return std::nullopt;
+ }
+
+ TStreamingPayload result;
+ result.Codec = Codec_;
+ while (CanPullMore(result.Attachments.empty())) {
+ auto attachment = std::move(DataQueue_.front());
+ SentPosition_ += GetStreamingAttachmentSize(attachment);
+ result.Attachments.push_back(std::move(attachment));
+ DataQueue_.pop();
+ }
+
+ if (result.Attachments.empty()) {
+ return std::nullopt;
+ }
+
+ result.SequenceNumber = PayloadSequenceNumber_++;
+ return result;
+}
+
+void TAttachmentsOutputStream::MaybeInvokePullCallback(TGuard<NThreading::TSpinLock>& guard)
+{
+ if (CanPullMore(true)) {
+ guard.Release();
+ PullCallback_();
+ }
+}
+
+bool TAttachmentsOutputStream::CanPullMore(bool first) const
+{
+ if (DataQueue_.empty()) {
+ return false;
+ }
+
+ if (SentPosition_ - ReadPosition_ + static_cast<ssize_t>(GetStreamingAttachmentSize(DataQueue_.front())) <= WindowSize_) {
+ return true;
+ }
+
+ if (first && SentPosition_ == ReadPosition_) {
+ return true;
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRpcClientInputStream::TRpcClientInputStream(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult)
+ : Request_(std::move(request))
+ , Underlying_(Request_->GetResponseAttachmentsStream())
+ , InvokeResult_(std::move(invokeResult))
+{
+ YT_VERIFY(Underlying_);
+ YT_VERIFY(InvokeResult_);
+}
+
+TFuture<TSharedRef> TRpcClientInputStream::Read()
+{
+ return Underlying_->Read()
+ .Apply(BIND([invokeResult = InvokeResult_] (const TSharedRef& ref) mutable
+ {
+ if (ref) {
+ return MakeFuture(ref);
+ }
+
+ return invokeResult.Apply(BIND([] () {
+ return TSharedRef();
+ }));
+ }));
+}
+
+TRpcClientInputStream::~TRpcClientInputStream()
+{
+ // Here we assume that canceling a completed request is safe.
+ InvokeResult_.Cancel(TError("RPC input stream destroyed"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TError CheckWriterFeedback(
+ const TSharedRef& ref,
+ EWriterFeedback expectedFeedback)
+{
+ NProto::TWriterFeedback protoFeedback;
+ if (!TryDeserializeProto(&protoFeedback, ref)) {
+ return TError("Failed to deserialize writer feedback");
+ }
+
+ EWriterFeedback actualFeedback;
+ if (!TryEnumCast(protoFeedback.feedback(), &actualFeedback)) {
+ return TError("Invalid writer feedback value %v",
+ static_cast<int>(protoFeedback.feedback()));
+ }
+
+ if (actualFeedback != expectedFeedback) {
+ return TError("Received a wrong kind of writer feedback: %v instead of %v",
+ actualFeedback,
+ expectedFeedback);
+ }
+
+ return TError();
+}
+
+TFuture<void> ExpectWriterFeedback(
+ const IAsyncZeroCopyInputStreamPtr& input,
+ EWriterFeedback expectedFeedback)
+{
+ YT_VERIFY(input);
+ return input->Read().Apply(BIND([=] (const TSharedRef& ref) {
+ return MakeFuture(CheckWriterFeedback(ref, expectedFeedback));
+ }));
+}
+
+TFuture<void> ExpectHandshake(
+ const IAsyncZeroCopyInputStreamPtr& input,
+ bool feedbackEnabled)
+{
+ return feedbackEnabled
+ ? ExpectWriterFeedback(input, NDetail::EWriterFeedback::Handshake)
+ : ExpectEndOfStream(input);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TRpcClientOutputStream::TRpcClientOutputStream(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult,
+ bool feedbackEnabled)
+ : Request_(std::move(request))
+ , InvokeResult_(std::move(invokeResult))
+ , FeedbackEnabled_(feedbackEnabled)
+{
+ YT_VERIFY(Request_);
+
+ Underlying_ = Request_->GetRequestAttachmentsStream();
+ YT_VERIFY(Underlying_);
+
+ FeedbackStream_ = Request_->GetResponseAttachmentsStream();
+ YT_VERIFY(FeedbackStream_);
+
+ if (FeedbackEnabled_) {
+ FeedbackStream_->Read().Subscribe(
+ BIND(&TRpcClientOutputStream::OnFeedback, MakeWeak(this)));
+ }
+}
+
+TFuture<void> TRpcClientOutputStream::Write(const TSharedRef& data)
+{
+ if (FeedbackEnabled_) {
+ auto promise = NewPromise<void>();
+ TFuture<void> writeResult;
+ {
+ auto guard = Guard(SpinLock_);
+ if (!Error_.IsOK()) {
+ return MakeFuture(Error_);
+ }
+
+ ConfirmationQueue_.push(promise);
+ writeResult = Underlying_->Write(data);
+ }
+
+ writeResult.Subscribe(
+ BIND(&TRpcClientOutputStream::AbortOnError, MakeWeak(this)));
+
+ return promise.ToFuture();
+ } else {
+ auto writeResult = Underlying_->Write(data);
+ writeResult.Subscribe(
+ BIND(&TRpcClientOutputStream::AbortOnError, MakeWeak(this)));
+ return writeResult;
+ }
+}
+
+TFuture<void> TRpcClientOutputStream::Close()
+{
+ CloseResult_.TrySetFrom(Underlying_->Close());
+ return CloseResult_.ToFuture().Apply(BIND([invokeResult = InvokeResult_] () {
+ return invokeResult;
+ }));
+}
+
+void TRpcClientOutputStream::AbortOnError(const TError& error)
+{
+ if (error.IsOK()) {
+ return;
+ }
+
+ auto guard = Guard(SpinLock_);
+
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ Error_ = error;
+
+ std::vector<TPromise<void>> promises;
+ promises.reserve(ConfirmationQueue_.size());
+
+ while (!ConfirmationQueue_.empty()) {
+ promises.push_back(std::move(ConfirmationQueue_.front()));
+ ConfirmationQueue_.pop();
+ }
+
+ guard.Release();
+
+ for (const auto& promise : promises) {
+ if (promise) {
+ promise.Set(error);
+ }
+ }
+
+ InvokeResult_.Cancel(error);
+}
+
+void TRpcClientOutputStream::OnFeedback(const TErrorOr<TSharedRef>& refOrError)
+{
+ YT_VERIFY(FeedbackEnabled_);
+
+ auto error = TError(refOrError);
+ if (error.IsOK()) {
+ const auto& ref = refOrError.Value();
+ if (!ref) {
+ auto guard = Guard(SpinLock_);
+
+ if (ConfirmationQueue_.empty()) {
+ guard.Release();
+ CloseResult_.TrySetFrom(Underlying_->Close());
+ return;
+ }
+ error = TError(NRpc::EErrorCode::ProtocolError, "Expected a positive writer feedback, received a null ref");
+ } else {
+ error = CheckWriterFeedback(ref, EWriterFeedback::Success);
+ }
+ }
+
+ TPromise<void> promise;
+
+ {
+ auto guard = Guard(SpinLock_);
+
+ if (!Error_.IsOK()) {
+ return;
+ }
+
+ if (!error.IsOK()) {
+ guard.Release();
+ AbortOnError(error);
+ return;
+ }
+
+ YT_VERIFY(!ConfirmationQueue_.empty());
+ promise = std::move(ConfirmationQueue_.front());
+ ConfirmationQueue_.pop();
+ }
+
+ promise.Set();
+
+ FeedbackStream_->Read().Subscribe(
+ BIND(&TRpcClientOutputStream::OnFeedback, MakeWeak(this)));
+}
+
+TRpcClientOutputStream::~TRpcClientOutputStream()
+{
+ InvokeResult_.Cancel(TError("RPC output stream destroyed"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSharedRef GenerateWriterFeedbackMessage(
+ EWriterFeedback feedback)
+{
+ NProto::TWriterFeedback protoFeedback;
+ protoFeedback.set_feedback(static_cast<NProto::EWriterFeedback>(feedback));
+ return SerializeProtoToRef(protoFeedback);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStreamFromInvokedRequest(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult,
+ bool feedbackEnabled)
+{
+ auto handshakeResult = NDetail::ExpectHandshake(
+ request->GetResponseAttachmentsStream(),
+ feedbackEnabled);
+
+ return handshakeResult.Apply(BIND([
+ =,
+ request = std::move(request),
+ invokeResult = std::move(invokeResult)
+ ] {
+ return New<NDetail::TRpcClientOutputStream>(
+ std::move(request),
+ std::move(invokeResult),
+ feedbackEnabled);
+ })).template As<IAsyncZeroCopyOutputStreamPtr>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+void HandleInputStreamingRequest(
+ const IServiceContextPtr& context,
+ const std::function<TSharedRef()>& blockGenerator)
+{
+ auto inputStream = context->GetRequestAttachmentsStream();
+ YT_VERIFY(inputStream);
+ WaitFor(ExpectEndOfStream(inputStream))
+ .ThrowOnError();
+
+ auto outputStream = context->GetResponseAttachmentsStream();
+ YT_VERIFY(outputStream);
+
+ while (auto block = blockGenerator()) {
+ WaitFor(outputStream->Write(block))
+ .ThrowOnError();
+ }
+
+ WaitFor(outputStream->Close())
+ .ThrowOnError();
+
+ context->Reply(TError());
+}
+
+void HandleInputStreamingRequest(
+ const IServiceContextPtr& context,
+ const IAsyncZeroCopyInputStreamPtr& input)
+{
+ HandleInputStreamingRequest(
+ context,
+ [&] {
+ return WaitFor(input->Read())
+ .ValueOrThrow();
+ });
+}
+
+void HandleOutputStreamingRequest(
+ const IServiceContextPtr& context,
+ const std::function<void(TSharedRef)>& blockHandler,
+ const std::function<void()>& finalizer,
+ bool feedbackEnabled)
+{
+ auto inputStream = context->GetRequestAttachmentsStream();
+ YT_VERIFY(inputStream);
+ auto outputStream = context->GetResponseAttachmentsStream();
+ YT_VERIFY(outputStream);
+
+ auto blockGenerator = [&] {
+ return WaitFor(inputStream->Read())
+ .ValueOrThrow();
+ };
+
+ if (feedbackEnabled) {
+ auto handshakeRef = GenerateWriterFeedbackMessage(
+ NDetail::EWriterFeedback::Handshake);
+ WaitFor(outputStream->Write(handshakeRef))
+ .ThrowOnError();
+
+ while (auto block = blockGenerator()) {
+ blockHandler(std::move(block));
+
+ auto ackRef = GenerateWriterFeedbackMessage(NDetail::EWriterFeedback::Success);
+ WaitFor(outputStream->Write(ackRef))
+ .ThrowOnError();
+ }
+
+ WaitFor(outputStream->Close())
+ .ThrowOnError();
+ } else {
+ WaitFor(outputStream->Close())
+ .ThrowOnError();
+
+ while (auto block = blockGenerator()) {
+ blockHandler(std::move(block));
+ }
+ }
+
+ finalizer();
+
+ context->Reply(TError());
+}
+
+void HandleOutputStreamingRequest(
+ const IServiceContextPtr& context,
+ const IAsyncZeroCopyOutputStreamPtr& output,
+ bool feedbackEnabled)
+{
+ HandleOutputStreamingRequest(
+ context,
+ [&] (TSharedRef block) {
+ WaitFor(output->Write(std::move(block)))
+ .ThrowOnError();
+ },
+ [&] {
+ WaitFor(output->Close())
+ .ThrowOnError();
+ },
+ feedbackEnabled);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
diff --git a/yt/yt/core/rpc/stream.h b/yt/yt/core/rpc/stream.h
new file mode 100644
index 0000000000..8678887c6f
--- /dev/null
+++ b/yt/yt/core/rpc/stream.h
@@ -0,0 +1,300 @@
+#pragma once
+
+#include "channel.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/misc/range.h>
+#include <yt/yt/core/misc/ring_queue.h>
+#include <yt/yt/core/misc/sliding_window.h>
+
+#include <yt/yt/core/actions/signal.h>
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/compression/public.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! For empty and null attachments returns 1; for others returns the actual size.
+size_t GetStreamingAttachmentSize(TRef attachment);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttachmentsInputStream
+ : public NConcurrency::IAsyncZeroCopyInputStream
+{
+public:
+ TAttachmentsInputStream(
+ TClosure readCallback,
+ IInvokerPtr compressionInvoker,
+ std::optional<TDuration> timeout = {});
+
+ TFuture<TSharedRef> Read() override;
+
+ void EnqueuePayload(const TStreamingPayload& payload);
+ void Abort(const TError& error, bool fireAborted = true);
+ void AbortUnlessClosed(const TError& error, bool fireAborted = true);
+ TStreamingFeedback GetFeedback() const;
+
+ DEFINE_SIGNAL(void(), Aborted);
+
+private:
+ const TClosure ReadCallback_;
+ const IInvokerPtr CompressionInvoker_;
+ const std::optional<TDuration> Timeout_;
+
+ struct TWindowPacket
+ {
+ TStreamingPayload Payload;
+ std::vector<TSharedRef> DecompressedAttachments;
+ };
+
+ struct TQueueEntry
+ {
+ TSharedRef Attachment;
+ size_t CompressedSize;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TSlidingWindow<TWindowPacket> Window_;
+ TRingQueue<TQueueEntry> Queue_;
+ TError Error_;
+ TPromise<TSharedRef> Promise_;
+ NConcurrency::TDelayedExecutorCookie TimeoutCookie_;
+
+ std::atomic<ssize_t> ReadPosition_ = 0;
+ bool Closed_ = false;
+
+ void DecompressAndEnqueuePayload(const TStreamingPayload& payload);
+ void DoEnqueuePayload(
+ const TStreamingPayload& payload,
+ const std::vector<TSharedRef>& decompressedAttachments);
+ void DoAbort(
+ TGuard<NThreading::TSpinLock>& guard,
+ const TError& error,
+ bool fireAborted = true);
+ void OnTimeout();
+};
+
+DEFINE_REFCOUNTED_TYPE(TAttachmentsInputStream)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttachmentsOutputStream
+ : public NConcurrency::IAsyncZeroCopyOutputStream
+{
+public:
+ TAttachmentsOutputStream(
+ NCompression::ECodec codec,
+ IInvokerPtr compressionInvoker,
+ TClosure pullCallback,
+ ssize_t windowSize,
+ std::optional<TDuration> timeout = {});
+
+ TFuture<void> Write(const TSharedRef& data) override;
+ TFuture<void> Close() override;
+
+ void Abort(const TError& error);
+ void AbortUnlessClosed(const TError& error, bool fireAborted = true);
+ void HandleFeedback(const TStreamingFeedback& feedback);
+ std::optional<TStreamingPayload> TryPull();
+
+ DEFINE_SIGNAL(void(), Aborted);
+
+private:
+ const NCompression::ECodec Codec_;
+ const IInvokerPtr CompressionInvoker_;
+ const TClosure PullCallback_;
+ const ssize_t WindowSize_;
+ const std::optional<TDuration> Timeout_;
+
+ struct TWindowPacket
+ {
+ TSharedRef Data;
+ TPromise<void> Promise;
+ NConcurrency::TDelayedExecutorCookie TimeoutCookie;
+ };
+
+ struct TConfirmationEntry
+ {
+ ssize_t Position;
+ TPromise<void> Promise;
+ NConcurrency::TDelayedExecutorCookie TimeoutCookie;
+ };
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ std::atomic<size_t> CompressionSequenceNumber_ = {0};
+ TSlidingWindow<TWindowPacket> Window_;
+ TError Error_;
+ TRingQueue<TSharedRef> DataQueue_;
+ TRingQueue<TConfirmationEntry> ConfirmationQueue_;
+ TPromise<void> ClosePromise_;
+ NConcurrency::TDelayedExecutorCookie CloseTimeoutCookie_;
+ bool Closed_ = false;
+ ssize_t WritePosition_ = 0;
+ ssize_t SentPosition_ = 0;
+ ssize_t ReadPosition_ = 0;
+ int PayloadSequenceNumber_ = 0;
+
+ void OnWindowPacketsReady(
+ TMutableRange<TWindowPacket> packets,
+ TGuard<NThreading::TSpinLock>& guard);
+ void MaybeInvokePullCallback(TGuard<NThreading::TSpinLock>& guard);
+ bool CanPullMore(bool first) const;
+ void DoAbort(
+ TGuard<NThreading::TSpinLock>& guard,
+ const TError& error,
+ bool fireAborted = true);
+ void OnTimeout();
+};
+
+DEFINE_REFCOUNTED_TYPE(TAttachmentsOutputStream)
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRpcClientInputStream
+ : public NConcurrency::IAsyncZeroCopyInputStream
+{
+public:
+ TRpcClientInputStream(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult);
+
+ TFuture<TSharedRef> Read() override;
+
+ ~TRpcClientInputStream();
+
+private:
+ const IClientRequestPtr Request_;
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr Underlying_;
+ const TFuture<void> InvokeResult_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EWriterFeedback,
+ (Handshake)
+ (Success)
+);
+
+TError CheckWriterFeedback(
+ const TSharedRef& ref,
+ EWriterFeedback expectedFeedback);
+
+TFuture<void> ExpectWriterFeedback(
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr& input,
+ EWriterFeedback expectedFeedback);
+
+TFuture<void> ExpectHandshake(
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr& input,
+ bool feedbackEnabled);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRpcClientOutputStream
+ : public NConcurrency::IAsyncZeroCopyOutputStream
+{
+public:
+ TRpcClientOutputStream(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult,
+ bool feedbackEnabled = false);
+
+ TFuture<void> Write(const TSharedRef& data) override;
+ TFuture<void> Close() override;
+
+ ~TRpcClientOutputStream();
+
+private:
+ const IClientRequestPtr Request_;
+
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr Underlying_;
+ TFuture<void> InvokeResult_;
+ const TPromise<void> CloseResult_ = NewPromise<void>();
+
+ NConcurrency::IAsyncZeroCopyInputStreamPtr FeedbackStream_;
+ bool FeedbackEnabled_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, SpinLock_);
+ TRingQueue<TPromise<void>> ConfirmationQueue_;
+ TError Error_;
+
+ void AbortOnError(const TError& error);
+ void OnFeedback(const TErrorOr<TSharedRef>& refOrError);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TFuture<NConcurrency::IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStreamFromInvokedRequest(
+ IClientRequestPtr request,
+ TFuture<void> invokeResult,
+ bool feedbackEnabled = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates an input stream adapter from an uninvoked client request.
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyInputStreamPtr> CreateRpcClientInputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request);
+
+//! Creates an output stream adapter from an uninvoked client request.
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request,
+ bool feedbackEnabled = false);
+
+//! This variant expects the server to send one TSharedRef of meta information,
+//! then close its stream.
+template <class TRequestMessage, class TResponse>
+TFuture<NConcurrency::IAsyncZeroCopyOutputStreamPtr> CreateRpcClientOutputStream(
+ TIntrusivePtr<TTypedClientRequest<TRequestMessage, TResponse>> request,
+ TCallback<void(TSharedRef)> metaHandler);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Handles an incoming streaming request that uses the #CreateRpcClientInputStream
+//! function.
+void HandleInputStreamingRequest(
+ const IServiceContextPtr& context,
+ const std::function<TSharedRef()>& blockGenerator);
+
+void HandleInputStreamingRequest(
+ const IServiceContextPtr& context,
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr& input);
+
+//! Handles an incoming streaming request that uses the #CreateRpcClientOutputStream
+//! function with the same #feedbackEnabled value.
+void HandleOutputStreamingRequest(
+ const IServiceContextPtr& context,
+ const std::function<void(TSharedRef)>& blockHandler,
+ const std::function<void()>& finalizer,
+ bool feedbackEnabled = false);
+
+void HandleOutputStreamingRequest(
+ const IServiceContextPtr& context,
+ const NConcurrency::IAsyncZeroCopyOutputStreamPtr& output,
+ bool feedbackEnabled = false);
+
+/////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+#define STREAM_INL_H_
+#include "stream-inl.h"
+#undef STREAM_INL_H_
+
diff --git a/yt/yt/core/rpc/throttling_channel.cpp b/yt/yt/core/rpc/throttling_channel.cpp
new file mode 100644
index 0000000000..6333c0d4a1
--- /dev/null
+++ b/yt/yt/core/rpc/throttling_channel.cpp
@@ -0,0 +1,89 @@
+#include "throttling_channel.h"
+#include "channel_detail.h"
+#include "client.h"
+#include "config.h"
+
+#include <yt/yt/core/concurrency/throughput_throttler.h>
+#include <yt/yt/core/concurrency/config.h>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThrottlingChannel
+ : public TChannelWrapper
+ , public IThrottlingChannel
+{
+public:
+ TThrottlingChannel(
+ TThrottlingChannelConfigPtr config,
+ IChannelPtr underlyingChannel)
+ : TChannelWrapper(std::move(underlyingChannel))
+ , Config_(std::move(config))
+ , Throttler_(CreateReconfigurableThroughputThrottler(TThroughputThrottlerConfig::Create(
+ Config_->RateLimit)))
+ { }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr request,
+ IClientResponseHandlerPtr responseHandler,
+ const TSendOptions& options) override
+ {
+ auto sendTime = TInstant::Now();
+ auto timeout = options.Timeout;
+ auto requestControlThunk = New<TClientRequestControlThunk>();
+ Throttler_->Throttle(1)
+ .WithTimeout(timeout)
+ .Subscribe(BIND([=, this, this_ = MakeStrong(this)] (const TError& error) {
+ if (!error.IsOK()) {
+ auto wrappedError = TError("Error throttling RPC request")
+ << error;
+ responseHandler->HandleError(wrappedError);
+ return;
+ }
+
+ auto adjustedOptions = options;
+ auto now = TInstant::Now();
+ adjustedOptions.Timeout = timeout
+ ? std::make_optional(*timeout - (now - sendTime))
+ : std::nullopt;
+
+ auto requestControl = UnderlyingChannel_->Send(
+ std::move(request),
+ std::move(responseHandler),
+ adjustedOptions);
+ requestControlThunk->SetUnderlying(std::move(requestControl));
+ }));
+ return requestControlThunk;
+ }
+
+ void Reconfigure(const TThrottlingChannelDynamicConfigPtr& config) override
+ {
+ Throttler_->Reconfigure(TThroughputThrottlerConfig::Create(
+ config->RateLimit.value_or(Config_->RateLimit)));
+ }
+
+private:
+ const TThrottlingChannelConfigPtr Config_;
+ const IReconfigurableThroughputThrottlerPtr Throttler_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IThrottlingChannelPtr CreateThrottlingChannel(
+ TThrottlingChannelConfigPtr config,
+ IChannelPtr underlyingChannel)
+{
+ YT_VERIFY(config);
+ YT_VERIFY(underlyingChannel);
+
+ return New<TThrottlingChannel>(
+ std::move(config),
+ std::move(underlyingChannel));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/throttling_channel.h b/yt/yt/core/rpc/throttling_channel.h
new file mode 100644
index 0000000000..153c3c31fe
--- /dev/null
+++ b/yt/yt/core/rpc/throttling_channel.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "channel.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IThrottlingChannel
+ : public virtual IChannel
+{
+ virtual void Reconfigure(const TThrottlingChannelDynamicConfigPtr& config) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IThrottlingChannel)
+
+//! Constructs a channel that limits request rate to the underlying channel.
+IThrottlingChannelPtr CreateThrottlingChannel(
+ TThrottlingChannelConfigPtr config,
+ IChannelPtr underlyingChannel);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/bin/main.cpp b/yt/yt/core/rpc/unittests/bin/main.cpp
new file mode 100644
index 0000000000..9b8412055b
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/bin/main.cpp
@@ -0,0 +1,42 @@
+#include <yt/yt/core/rpc/bus/server.h>
+#include <yt/yt/core/rpc/server.h>
+
+#include <yt/yt/core/bus/tcp/server.h>
+#include <yt/yt/core/bus/tcp/config.h>
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+
+#include <yt/yt/core/rpc/unittests/lib/my_service.h>
+
+using namespace NYT;
+using namespace NYT::NBus;
+using namespace NYT::NRpc;
+using namespace NYT::NRpc::NBus;
+using namespace NYT::NConcurrency;
+
+int main(int argc, char* argv[])
+{
+ try {
+ if (argc != 2) {
+ THROW_ERROR_EXCEPTION("Port argument is missing");
+ }
+
+ auto port = FromString<int>(argv[1]);
+
+ auto busConfig = TBusServerConfig::CreateTcp(port);
+ auto busServer = CreateBusServer(busConfig);
+ auto server = CreateBusServer(busServer);
+
+ auto workerPool = CreateThreadPool(4, "Worker");
+ auto service = CreateMyService(workerPool->GetInvoker(), false);
+ server->RegisterService(service);
+ server->Start();
+
+ Sleep(TDuration::Max());
+ } catch (const std::exception& ex) {
+ Cerr << ex.what() << Endl;
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/yt/yt/core/rpc/unittests/bin/ya.make b/yt/yt/core/rpc/unittests/bin/ya.make
new file mode 100644
index 0000000000..2263e635f7
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/bin/ya.make
@@ -0,0 +1,13 @@
+PROGRAM()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ main.cpp
+)
+
+PEERDIR(
+ yt/yt/core/rpc/unittests/lib
+)
+
+END()
diff --git a/yt/yt/core/rpc/unittests/lib/common.cpp b/yt/yt/core/rpc/unittests/lib/common.cpp
new file mode 100644
index 0000000000..d906d82fd4
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/common.cpp
@@ -0,0 +1,11 @@
+#include "common.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString TRpcOverUdsImpl::SocketPath_ = "";
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/common.h b/yt/yt/core/rpc/unittests/lib/common.h
new file mode 100644
index 0000000000..3ce2332abb
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/common.h
@@ -0,0 +1,443 @@
+#pragma once
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/server.h>
+
+#include <yt/yt/core/bus/tcp/config.h>
+#include <yt/yt/core/bus/tcp/client.h>
+#include <yt/yt/core/bus/tcp/server.h>
+
+#include <yt/yt/core/crypto/config.h>
+
+#include <yt/yt/core/concurrency/thread_pool.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+
+#include <yt/yt/core/bus/public.h>
+
+#include <yt/yt/core/misc/fs.h>
+
+#include <yt/yt/core/rpc/bus/channel.h>
+#include <yt/yt/core/rpc/bus/server.h>
+
+#include <yt/yt/core/rpc/client.h>
+#include <yt/yt/core/rpc/retrying_channel.h>
+#include <yt/yt/core/rpc/caching_channel_factory.h>
+#include <yt/yt/core/rpc/static_channel_factory.h>
+#include <yt/yt/core/rpc/server.h>
+#include <yt/yt/core/rpc/local_server.h>
+#include <yt/yt/core/rpc/local_channel.h>
+#include <yt/yt/core/rpc/service_detail.h>
+#include <yt/yt/core/rpc/stream.h>
+
+#include <yt/yt/core/rpc/unittests/lib/my_service.h>
+#include <yt/yt/core/rpc/unittests/lib/no_baggage_service.h>
+
+#include <yt/yt/core/rpc/grpc/config.h>
+#include <yt/yt/core/rpc/grpc/channel.h>
+#include <yt/yt/core/rpc/grpc/server.h>
+#include <yt/yt/core/rpc/grpc/proto/grpc.pb.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <yt/yt/core/tracing/public.h>
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/helpers.h>
+
+#include <library/cpp/testing/common/env.h>
+#include <library/cpp/testing/common/network.h>
+
+#include <random>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TImpl>
+class TTestBase
+ : public ::testing::Test
+{
+public:
+ void SetUp() final
+ {
+ Port_ = NTesting::GetFreePort();
+ Address_ = Format("localhost:%v", Port_);
+
+ Server_ = CreateServer(Port_);
+ WorkerPool_ = NConcurrency::CreateThreadPool(4, "Worker");
+ bool secure = TImpl::Secure;
+ MyService_ = CreateMyService(WorkerPool_->GetInvoker(), secure);
+ NoBaggageService_ = CreateNoBaggageService(WorkerPool_->GetInvoker());
+ Server_->RegisterService(MyService_);
+ Server_->RegisterService(NoBaggageService_);
+ Server_->Start();
+ }
+
+ void TearDown() final
+ {
+ Server_->Stop().Get().ThrowOnError();
+ Server_.Reset();
+ }
+
+ IServerPtr CreateServer(ui16 port)
+ {
+ return TImpl::CreateServer(port);
+ }
+
+ IChannelPtr CreateChannel(
+ const std::optional<TString>& address = std::nullopt,
+ THashMap<TString, NYTree::INodePtr> grpcArguments = {})
+ {
+ if (address) {
+ return TImpl::CreateChannel(*address, Address_, std::move(grpcArguments));
+ } else {
+ return TImpl::CreateChannel(Address_, Address_, std::move(grpcArguments));
+ }
+ }
+
+ static bool CheckCancelCode(TErrorCode code)
+ {
+ if (code == NYT::EErrorCode::Canceled) {
+ return true;
+ }
+ if (code == NYT::NRpc::EErrorCode::TransportError && TImpl::AllowTransportErrors) {
+ return true;
+ }
+ return false;
+ }
+
+ static bool CheckTimeoutCode(TErrorCode code)
+ {
+ if (code == NYT::EErrorCode::Timeout) {
+ return true;
+ }
+ if (code == NYT::NRpc::EErrorCode::TransportError && TImpl::AllowTransportErrors) {
+ return true;
+ }
+ return false;
+ }
+
+protected:
+ NTesting::TPortHolder Port_;
+ TString Address_;
+
+ NConcurrency::IThreadPoolPtr WorkerPool_;
+ IMyServicePtr MyService_;
+ IServicePtr NoBaggageService_;
+ IServerPtr Server_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TImpl>
+class TRpcOverBus
+{
+public:
+ static constexpr bool AllowTransportErrors = false;
+ static constexpr bool Secure = false;
+
+ static IServerPtr CreateServer(ui16 port)
+ {
+ auto busServer = MakeBusServer(port);
+ return NRpc::NBus::CreateBusServer(busServer);
+ }
+
+ static IChannelPtr CreateChannel(
+ const TString& address,
+ const TString& serverAddress,
+ THashMap<TString, NYTree::INodePtr> grpcArguments)
+ {
+ return TImpl::CreateChannel(address, serverAddress, std::move(grpcArguments));
+ }
+
+ static NYT::NBus::IBusServerPtr MakeBusServer(ui16 port)
+ {
+ return TImpl::MakeBusServer(port);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <bool ForceTcp>
+class TRpcOverBusImpl
+{
+public:
+ static IChannelPtr CreateChannel(
+ const TString& address,
+ const TString& /*serverAddress*/,
+ THashMap<TString, NYTree::INodePtr> /*grpcArguments*/)
+ {
+ auto client = CreateBusClient(NYT::NBus::TBusClientConfig::CreateTcp(address));
+ return NRpc::NBus::CreateBusChannel(client);
+ }
+
+ static NYT::NBus::IBusServerPtr MakeBusServer(ui16 port)
+ {
+ auto busConfig = NYT::NBus::TBusServerConfig::CreateTcp(port);
+ return CreateBusServer(busConfig);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * openssl genrsa -out root_key.pem 2048
+ * openssl req -x509 -new -nodes -key root_key.pem -sha256 -days 10000 -out root_cert.pem
+ * openssl genrsa -out server_key.pem 2048
+ * openssl genrsa -out client_key.pem 2048
+ * openssl req -new -key server_key.pem -out server.csr
+ * openssl req -new -key client_key.pem -out client.csr
+ * openssl x509 -in server.csr -req -days 10000 -out server_cert.pem -CA root_cert.pem -CAkey root_key.pem -CAcreateserial
+ * openssl x509 -in client.csr -req -days 10000 -out client_cert.pem -CA root_cert.pem -CAkey root_key.pem -CAserial root_cert.srl
+ */
+inline TString RootCert(
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIID9DCCAtygAwIBAgIJAJLU9fgmNTujMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV\n"
+ "BAYTAlJVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX\n"
+ "aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xODAzMDQxMzUx\n"
+ "MjdaFw00NTA3MjAxMzUxMjdaMFkxCzAJBgNVBAYTAlJVMRMwEQYDVQQIEwpTb21l\n"
+ "LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV\n"
+ "BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEq\n"
+ "JYLsNKAnO6uENyLjRww3pITvtEEg8uDi1W+87hZE8XNQ1crhJZDXcMaoWaVLOcQT\n"
+ "6x2z5DAnn5/0CUXLrJgrwbfrZ82VwihQIpovPX91bA7Bd5PdlBI5ojtUrY9Fb6xB\n"
+ "eAmfsp7z7rKDBheLe7KoNMth4OSHWp5GeHLzp336AB7TA6EQSTd3T7oDrRjdqZTr\n"
+ "X35vF0n6+iOMSe5CJYuNX9fd5GkO6mwGV5BzEoUWwqTocfkLa2BfE+pvfsuWleNc\n"
+ "sU8vMoAdlkKUrnHbbQ7xuwR+3XhKpRCU+wmzM6Tvm6dnYJhhTck8yxGNCuAfgKu+\n"
+ "7k9Ur4rdPXYkSTUMbbcCAwEAAaOBvjCBuzAdBgNVHQ4EFgQUkZyJrYSMi34fw8wk\n"
+ "sLSQyzg8a/swgYsGA1UdIwSBgzCBgIAUkZyJrYSMi34fw8wksLSQyzg8a/uhXaRb\n"
+ "MFkxCzAJBgNVBAYTAlJVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ\n"
+ "bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAJLU\n"
+ "9fgmNTujMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADpYkiJ4XsdV\n"
+ "w3JzDZCJX644cCzx3/l1N/ItVllVTbFU9MSrleifhBj21t4xUfHT2uhbQ21N6enA\n"
+ "Qx24wcLo9IRL61XEkLrTRPo1ZRrF8rwAYLxFgHgWimcocG+c/8++he7tXrjyYzS1\n"
+ "JyMKBgQcsrWn+3pCxSLHGuoH4buX3cMqrEepqdThIOTI12YW7xmD7vSguusroRFj\n"
+ "OH5RO4hhHIn/tR2G/lHS1u+YG5NyX94v8kN+SfAchZmeb54miANYBGzOFqYRgKs4\n"
+ "LfyFanmeXFJaj1M+37Lsm0TlxP6I7fa0Kag6FvlxpYvhblRJzsRHZE5Xe+KZzanV\n"
+ "I2TYYgHjI3I=\n"
+ "-----END CERTIFICATE-----\n");
+
+inline TString ClientKey(
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIIEpAIBAAKCAQEArZpqucOdMlwZyyTWq+Sz3EGXpAX/4nMpH7s/05d9O4tm0MsK\n"
+ "QUhUXRzt3VzOfMOb4cXAVwovHxiQ7NZIFBdmeyCHlT0HVkaqC76Tgi53scUMVKtE\n"
+ "lXJB5soc8PbFDjT21MOGzL3+Tqy47ecdZhCiaXYeD3exHFd+VDJXvC3O/GDc3/Fo\n"
+ "Gwh6iUxkAAa11duUoFfCs3p+XFN216V9jqfkEmf/KU2utMjzSmAvwaGh0WCSTnb2\n"
+ "lcByiPJWK6w8yx/CeY9Ks+sjI2hWw43jxUCcSa2pPimGLWgu9TYRiG4jWZln9FLW\n"
+ "hskfF/Ra0Xp0ptxrnuih0DTQ+ZxTscNlg27nuwIDAQABAoIBAFRhD6rG52sI1Qim\n"
+ "GSlnefx+bSQuPldkvgJMUxOXOClu8kRdy9g7PbYcT4keiMaflO7B3WDw9EJbAGX9\n"
+ "KP+K+Ca0gvIIvb4zjocy1COcTlU7f2jP7f/tjxaL+lEswE7Nc4OqnaR6XFcFIMWR\n"
+ "Zfqr7yTvYmEGPjGWXTKzXW17nnWQGYiK5IhLyzR+MQowCIVDK8ByJl18FRZOROtn\n"
+ "O+Bbm/MCsLsevAJPlKefY8kG/aG6VbrJO0sTvYe/j2QpeSfPOYtSlcDnTdx2Y5za\n"
+ "HFo+2mHvurhetl7Ba2gyTGu3XoMHtBXQ8jifyv8s3h+iz94twpsWp6D5CkPUw9oB\n"
+ "OOx/ttECgYEA4z3L7mSJQANvodlWfJUKAIWgdO54Vq6yZ9gELaCCoUXZLwHwjw+v\n"
+ "3k/WNbCv7lIL/DVzVh/RfFaG4Qe/c/Bu2tgwBv4fcAepvegUznwcY4Q1FB3sPMpm\n"
+ "fYcYPOy7jwEO7fvG8rjlZCXo6JuyJJsfyC+z+qWuPSpNgY+lj5MPe3UCgYEAw5LX\n"
+ "VZYnoghqMQGAi2CldxQ5Iz4RtZpIMgJH7yfu7jt1b3VGiBChwwbyfYrvPJTBpfP9\n"
+ "U05iffC8P8NVVL8KtjNRJLmQftLdssoqCncqdALnBGJ/jRNpxEFOcodReodkmUT/\n"
+ "vwQOfQXx0JayeRbUmPKgkEfaqcJL2Y2O41iq4G8CgYEA14kYsb/4EphvvLLZfoca\n"
+ "mo4kOGSsDYPbwfU5WVGiNXd73UNYuUjmxdUx13EEHecCaTEFeY3qc6XafvyLUlud\n"
+ "ucNOIoPMq8UI8hB8E7HSd23BrpgHJ03O0oddrQPZjnUxhPbHqBdJtKjkdiSfXmso\n"
+ "RQdCDZ4yWt+R7i6imUCicbUCgYEApg6iY/tQv5XhhKa/3Jg9JnS3ZyMmqknLjxq8\n"
+ "tWX0y7cUqYSsVI+6qfvWHZ7AL3InUp9uszNVEZY8YO+cHo7vq3C7LzGYbPbiYxKg\n"
+ "y64PD93/BYwUvVaEcaz5zOj019LqKfGaLThmjOVlQzURaRtnfE5W4ur/0TA2cwxt\n"
+ "DMCWpmUCgYBKZhCPxpJmJVFdiM6c5CpsEBdoJ7NpPdDR8xlY7m2Nb38szAFRAFhk\n"
+ "gMk6gXG+ObmPd4H6Up2lrpJgH3GDPIoiBZPOJefa0IXAmYCpqPH+HLG2lspuNyFL\n"
+ "OY4A1p2EvY8/L6PmPXAURfsE8RTL0y4ww/7mPJTQXsteTawAPDdVKQ==\n"
+ "-----END RSA PRIVATE KEY-----\n");
+
+inline TString ClientCert(
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDLjCCAhYCCQCZd28+0jJVLTANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJS\n"
+ "VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0\n"
+ "cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTgwMzA0MTM1MjU2WhcN\n"
+ "NDUwNzIwMTM1MjU2WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0\n"
+ "ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls\n"
+ "b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtmmq5w50y\n"
+ "XBnLJNar5LPcQZekBf/icykfuz/Tl307i2bQywpBSFRdHO3dXM58w5vhxcBXCi8f\n"
+ "GJDs1kgUF2Z7IIeVPQdWRqoLvpOCLnexxQxUq0SVckHmyhzw9sUONPbUw4bMvf5O\n"
+ "rLjt5x1mEKJpdh4Pd7EcV35UMle8Lc78YNzf8WgbCHqJTGQABrXV25SgV8Kzen5c\n"
+ "U3bXpX2Op+QSZ/8pTa60yPNKYC/BoaHRYJJOdvaVwHKI8lYrrDzLH8J5j0qz6yMj\n"
+ "aFbDjePFQJxJrak+KYYtaC71NhGIbiNZmWf0UtaGyR8X9FrRenSm3Gue6KHQNND5\n"
+ "nFOxw2WDbue7AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAImeUspGIeL24U5sK2PR\n"
+ "1BcWUBHtfUtXozaPK/q6WbEMObPxuNenNjnEYdp7b8JT2g91RqYd645wIPGaDAnc\n"
+ "EFz3b2piUZIG8YfCCLdntqwrYLxdGuHt/47RoSCZ2WrTZA6j7wP5ldQZTfefq3VC\n"
+ "ncz985cJ5AgEOZJmEdcleraoE1ZHb7O/kVxdxA6g93v9n3mm+kVYh3hth2646I8P\n"
+ "Bn8Gucf3jySWsN5H74lnp4VaA0xyJh2hC/4e/RnYod7TkXaqKeeLc93suIXgHHKt\n"
+ "jGvMhVuIWj3zzRi5e8Z1Ww5uHbiVyo4+GZMuV6w5ePgZpQ+5hUeD8PYf1AqDZet4\n"
+ "3SA=\n"
+ "-----END CERTIFICATE-----\n");
+
+inline TString ServerKey(
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIIEowIBAAKCAQEAzbAyEJFSmPNJ3pLNNSWQVF53Ltof1Wc4JIfvNazl41LjNyuO\n"
+ "SQV7+6GVFMIybBBoeWQ58hVJ/d8KxFBf6XIV6uGH9WtN38hWrxR6UEGkHxpUSfvg\n"
+ "TZ2FSsusus5sYDXjW+liQg5P9X/O69z/vmrIuyS8GckNq4/sA+Pw5GgCWDS05e72\n"
+ "N8r6DG7UlzKm5ynCGI8pRh/EdmxHTP4G8bEKF25x4FRy3Mg7bAaif9owliC2+BLI\n"
+ "IRNMtZs9BWp0U8GzEv2wY8xzkJEFD37xBiwHOWDj9KAmJpXQMM48PoXgvQsUo0ed\n"
+ "/a+GHvumeb3tBtsqLALhLFQBEFykA9X4SF93jwIDAQABAoIBAQC488Bw6VuuMMWx\n"
+ "n6tqKLbZRoBA3t5VFBWFs73DNA8bE8NALqgovQe5Qpg9LEoOpcprrVX1enMoFtEl\n"
+ "qWg1D+Lpa5bHdY92tDxN/knltMCRPymfxR7ya7wZf394EnmdIZepY/h4kUoQ5LX5\n"
+ "nKVSYc7RiLyjKwhhxm5hKSvJFkVVbaKvb9jFPEpYJHNWktl9Hh6XLs/DQLZwEVy0\n"
+ "rR7KSV00XyNPtMlt6EBXLW7/ysYBiDdcGZ+lIp36fDkoC+kmfbNxsmsEO7x/63NW\n"
+ "yCmhj4qz9hELbuOMNyoX0jzWMXdfEba/t/Gk7klB1/bQZ8VBn4Nd9PTEPHFLhNG2\n"
+ "s/bQoH3RAoGBAOguUWbVar200VcPwnRjDg2Gw+N+xTR5ONvanIsJaf6xBW8Ymtsl\n"
+ "J6GDJrJ391L0Zs2+fxLXDUebS8CF8CghL1KtqZxoTSwjBz8G4kn3DKlyZgNJZgyi\n"
+ "GppY4ttaP1ys1LwO/xzPUJb9pqm84KDjE9JL1czv3Psk5PVzxV/PQlyzAoGBAOLK\n"
+ "HElPA4rWw79AW9Kzr9mykZzyqalvAmobz8Q/nINnVGUcQu6UY5vDQ6KCOg2vbTl1\n"
+ "shDrzEyD/mityBZWUgFiKp+KEYjD5CKE8XuryM3MHr9Dvb+zt2JMC6TVBrYJNG91\n"
+ "OnMjGACRJ0i5SoB2kxiruTwyc2bzWyB6Dw9TfN+1AoGACHwg12w3MWWZPOBDj/NK\n"
+ "wS3KnNa2KDvB2y77B425hOg9NZkll5qc/ycG1ADUVgC+fQhYJn0bbCF9vDRo2V6V\n"
+ "FyVnjGK3Z0SEcEY1INTZbpvSpI4bH50Q8dELwU5kAGQEhjbaFdhxroLog012vApw\n"
+ "YAALeSjO35Kyl1G6xcySNUcCgYBX+rAegFiPc+FcQEte4fZGLc/vYvQOltII9+ER\n"
+ "8Nt23o8O6nfMtiQuOQHz+TEsPfHRaKc7iT4oMMxxL3l/sNz/TGXcnmNO+y91dL15\n"
+ "jJrJu3XyHQVvaPirWXTq7Pk9hTSiSIf0Qpj9H1JuE/OjAlzuJTAm+itqtN2VK8TL\n"
+ "3UeEQQKBgA61gNqGc8uCm58vg76qjMw6dlxBrpjWxYC5QsNh/OUITtWXqKiwTThE\n"
+ "wkLMtumpDoioIp/cv8xyV7yvdNM0pxB5UtXBK/3P91lKbiyIfpertqMNxs5XzoeG\n"
+ "CyxY8hFTw3FSk+UYdAAm5qYabGY1DiuvyD1yVAX9aWjAHdbP3H5O\n"
+ "-----END RSA PRIVATE KEY-----\n");
+
+inline TString ServerCert(
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDLjCCAhYCCQCZd28+0jJVLDANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJS\n"
+ "VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0\n"
+ "cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTgwMzA0MTM1MjUwWhcN\n"
+ "NDUwNzIwMTM1MjUwWjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0\n"
+ "ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls\n"
+ "b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNsDIQkVKY\n"
+ "80neks01JZBUXncu2h/VZzgkh+81rOXjUuM3K45JBXv7oZUUwjJsEGh5ZDnyFUn9\n"
+ "3wrEUF/pchXq4Yf1a03fyFavFHpQQaQfGlRJ++BNnYVKy6y6zmxgNeNb6WJCDk/1\n"
+ "f87r3P++asi7JLwZyQ2rj+wD4/DkaAJYNLTl7vY3yvoMbtSXMqbnKcIYjylGH8R2\n"
+ "bEdM/gbxsQoXbnHgVHLcyDtsBqJ/2jCWILb4EsghE0y1mz0FanRTwbMS/bBjzHOQ\n"
+ "kQUPfvEGLAc5YOP0oCYmldAwzjw+heC9CxSjR539r4Ye+6Z5ve0G2yosAuEsVAEQ\n"
+ "XKQD1fhIX3ePAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAJ1bjP+J+8MgSeHvpCES\n"
+ "qo49l8JgpFV9h/1dUgz2fYhrVy7QCp8/3THoZcjErKYyzTdOlTzCy1OB4sRNLBiy\n"
+ "ftGGTm1KHWal9CNMwAN00+ebhwdqKjNCWViI45o5OSfPWUvGAkwxUENrOqLoGBvR\n"
+ "cVvvMIV5KeaZLTtvrPzfVCMq/B41Mu5ZslDZOTRmSpVlbxmFjUq3WM+wf1sLu2cw\n"
+ "DDk8O2UQpxJeiowu9XBkQCEkvxU3/5bPBvY/+3sikj8IqaknakEXBKH1e/ZTN3/l\n"
+ "F6/pV9FE34DC9mIlzIFQyMGKJd4cju6970Pv3blQabuNHJTd570JdMBYbUGJp/mI\n"
+ "6sI=\n"
+ "-----END CERTIFICATE-----\n");
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <bool EnableSsl>
+class TRpcOverGrpcImpl
+{
+public:
+ static constexpr bool AllowTransportErrors = true;
+ static constexpr bool Secure = EnableSsl;
+
+ static IChannelPtr CreateChannel(
+ const TString& address,
+ const TString& /*serverAddress*/,
+ THashMap<TString, NYTree::INodePtr> grpcArguments)
+ {
+ auto channelConfig = New<NGrpc::TChannelConfig>();
+ if (EnableSsl) {
+ channelConfig->Credentials = New<NGrpc::TChannelCredentialsConfig>();
+ channelConfig->Credentials->PemRootCerts = New<NCrypto::TPemBlobConfig>();
+ channelConfig->Credentials->PemRootCerts->Value = RootCert;
+ channelConfig->Credentials->PemKeyCertPair = New<NGrpc::TSslPemKeyCertPairConfig>();
+ channelConfig->Credentials->PemKeyCertPair->PrivateKey = New<NCrypto::TPemBlobConfig>();
+ channelConfig->Credentials->PemKeyCertPair->PrivateKey->Value = ClientKey;
+ channelConfig->Credentials->PemKeyCertPair->CertChain = New<NCrypto::TPemBlobConfig>();
+ channelConfig->Credentials->PemKeyCertPair->CertChain->Value = ClientCert;
+ }
+ channelConfig->Address = address;
+ channelConfig->GrpcArguments = std::move(grpcArguments);
+ return NGrpc::CreateGrpcChannel(channelConfig);
+ }
+
+ static IServerPtr CreateServer(ui16 port)
+ {
+ auto serverAddressConfig = New<NGrpc::TServerAddressConfig>();
+ if (EnableSsl) {
+ serverAddressConfig->Credentials = New<NGrpc::TServerCredentialsConfig>();
+ serverAddressConfig->Credentials->PemRootCerts = New<NCrypto::TPemBlobConfig>();
+ serverAddressConfig->Credentials->PemRootCerts->Value = RootCert;
+ serverAddressConfig->Credentials->PemKeyCertPairs.push_back(New<NGrpc::TSslPemKeyCertPairConfig>());
+ serverAddressConfig->Credentials->PemKeyCertPairs[0]->PrivateKey = New<NCrypto::TPemBlobConfig>();
+ serverAddressConfig->Credentials->PemKeyCertPairs[0]->PrivateKey->Value = ServerKey;
+ serverAddressConfig->Credentials->PemKeyCertPairs[0]->CertChain = New<NCrypto::TPemBlobConfig>();
+ serverAddressConfig->Credentials->PemKeyCertPairs[0]->CertChain->Value = ServerCert;
+ }
+
+ auto address = Format("localhost:%v", port);
+
+ serverAddressConfig->Address = address;
+ auto serverConfig = New<NGrpc::TServerConfig>();
+ serverConfig->Addresses.push_back(serverAddressConfig);
+ return NGrpc::CreateServer(serverConfig);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TRpcOverUdsImpl creates unix domain sockets, supported only on Linux.
+class TRpcOverUdsImpl
+{
+public:
+ static NYT::NBus::IBusServerPtr MakeBusServer(ui16 port)
+ {
+ SocketPath_ = GetWorkPath() + "/socket_" + ToString(port);
+ auto busConfig = NYT::NBus::TBusServerConfig::CreateUds(SocketPath_);
+ return CreateBusServer(busConfig);
+ }
+
+ static IChannelPtr CreateChannel(
+ const TString& address,
+ const TString& serverAddress,
+ THashMap<TString, NYTree::INodePtr> /*grpcArguments*/)
+ {
+ auto clientConfig = NYT::NBus::TBusClientConfig::CreateUds(
+ address == serverAddress ? SocketPath_ : address);
+ auto client = CreateBusClient(clientConfig);
+ return NRpc::NBus::CreateBusChannel(client);
+ }
+
+private:
+ static TString SocketPath_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TAllTransports = ::testing::Types<
+#ifdef _linux_
+ TRpcOverBus<TRpcOverUdsImpl>,
+ TRpcOverBus<TRpcOverBusImpl<true>>,
+#endif
+ TRpcOverBus<TRpcOverBusImpl<false>>,
+ TRpcOverGrpcImpl<false>,
+ TRpcOverGrpcImpl<true>
+>;
+
+using TWithoutUds = ::testing::Types<
+#ifdef _linux_
+ TRpcOverBus<TRpcOverBusImpl<true>>,
+#endif
+ TRpcOverBus<TRpcOverBusImpl<false>>,
+ TRpcOverGrpcImpl<false>,
+ TRpcOverGrpcImpl<true>
+>;
+
+using TWithoutGrpc = ::testing::Types<
+#ifdef _linux_
+ TRpcOverBus<TRpcOverUdsImpl>,
+ TRpcOverBus<TRpcOverBusImpl<true>>,
+#endif
+ TRpcOverBus<TRpcOverBusImpl<false>>
+>;
+
+using TGrpcOnly = ::testing::Types<
+ TRpcOverGrpcImpl<false>,
+ TRpcOverGrpcImpl<true>
+>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/my_service.cpp b/yt/yt/core/rpc/unittests/lib/my_service.cpp
new file mode 100644
index 0000000000..942c27cfd2
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/my_service.cpp
@@ -0,0 +1,345 @@
+#include "my_service.h"
+
+#include <gtest/gtest.h>
+
+#include <yt/yt/core/rpc/service_detail.h>
+#include <yt/yt/core/rpc/stream.h>
+
+#include <yt/yt/core/rpc/grpc/proto/grpc.pb.h>
+
+#include <yt/yt/core/misc/blob.h>
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <random>
+
+namespace NYT::NRpc {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMyService
+ : public IMyService
+ , public TServiceBase
+{
+public:
+ TMyService(IInvokerPtr invoker, bool secure)
+ : TServiceBase(
+ invoker,
+ TMyProxy::GetDescriptor(),
+ NLogging::TLogger("Main"))
+ , Secure_(secure)
+ {
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(SomeCall));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(PassCall));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(RegularAttachments));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(NullAndEmptyAttachments));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(Compression));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(DoNothing));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(CustomMessageError));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(SlowCall)
+ .SetCancelable(true)
+ .SetConcurrencyLimit(10)
+ .SetQueueSizeLimit(20));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(SlowCanceledCall)
+ .SetCancelable(true));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(RequestBytesThrottledCall));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(NoReply));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(FlakyCall));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(RequireCoolFeature));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(StreamingEcho)
+ .SetStreamingEnabled(true)
+ .SetCancelable(true));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(ServerStreamsAborted)
+ .SetStreamingEnabled(true)
+ .SetCancelable(true));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(ServerNotReading)
+ .SetStreamingEnabled(true)
+ .SetCancelable(true));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(ServerNotWriting)
+ .SetStreamingEnabled(true)
+ .SetCancelable(true));
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(GetTraceBaggage));
+ // NB: NotRegisteredCall is not registered intentionally
+
+ DeclareServerFeature(EMyFeature::Great);
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, SomeCall)
+ {
+ context->SetRequestInfo();
+ int a = request->a();
+ response->set_b(a + 100);
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, PassCall)
+ {
+ context->SetRequestInfo();
+ WriteAuthenticationIdentityToProto(response, context->GetAuthenticationIdentity());
+ ToProto(response->mutable_mutation_id(), context->GetMutationId());
+ response->set_retry(context->IsRetry());
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, RegularAttachments)
+ {
+ for (const auto& attachment : request->Attachments()) {
+ auto data = TBlob();
+ data.Append(attachment);
+ data.Append("_", 1);
+ response->Attachments().push_back(TSharedRef::FromBlob(std::move(data)));
+ }
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, NullAndEmptyAttachments)
+ {
+ const auto& attachments = request->Attachments();
+ EXPECT_EQ(2u, attachments.size());
+ EXPECT_FALSE(attachments[0]);
+ EXPECT_TRUE(attachments[1]);
+ EXPECT_TRUE(attachments[1].Empty());
+ response->Attachments() = attachments;
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, Compression)
+ {
+ auto requestCodecId = CheckedEnumCast<NCompression::ECodec>(request->request_codec());
+ auto serializedRequestBody = SerializeProtoToRefWithCompression(*request, requestCodecId);
+ const auto& compressedRequestBody = context->GetRequestBody();
+ EXPECT_TRUE(TRef::AreBitwiseEqual(serializedRequestBody, compressedRequestBody));
+
+ const auto& attachments = request->Attachments();
+ const auto& compressedAttachments = context->RequestAttachments();
+ EXPECT_TRUE(attachments.size() == compressedAttachments.size());
+ auto* requestCodec = NCompression::GetCodec(requestCodecId);
+ for (int i = 0; i < std::ssize(attachments); ++i) {
+ auto compressedAttachment = requestCodec->Compress(attachments[i]);
+ EXPECT_TRUE(TRef::AreBitwiseEqual(compressedAttachments[i], compressedAttachment));
+ }
+
+ response->set_message(request->message());
+ response->Attachments() = attachments;
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, DoNothing)
+ {
+ context->SetRequestInfo();
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, CustomMessageError)
+ {
+ context->SetRequestInfo();
+ context->Reply(TError(NYT::EErrorCode(42), "Some Error"));
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, SlowCall)
+ {
+ context->SetRequestInfo();
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, SlowCanceledCall)
+ {
+ try {
+ context->SetRequestInfo();
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(2));
+ context->Reply();
+ } catch (const TFiberCanceledException&) {
+ SlowCallCanceled_.Set();
+ throw;
+ }
+ }
+
+ TFuture<void> GetSlowCallCanceled() const override
+ {
+ return SlowCallCanceled_.ToFuture();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, RequestBytesThrottledCall)
+ {
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, NoReply)
+ { }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, StreamingEcho)
+ {
+ context->SetRequestInfo();
+
+ bool delayed = request->delayed();
+ std::vector<TSharedRef> receivedData;
+
+ ssize_t totalSize = 0;
+ while (true) {
+ auto data = WaitFor(request->GetAttachmentsStream()->Read())
+ .ValueOrThrow();
+ if (!data) {
+ break;
+ }
+ totalSize += data.size();
+
+ if (delayed) {
+ receivedData.push_back(data);
+ } else {
+ WaitFor(response->GetAttachmentsStream()->Write(data))
+ .ThrowOnError();
+ }
+ }
+
+ if (delayed) {
+ for (const auto& data : receivedData) {
+ WaitFor(response->GetAttachmentsStream()->Write(data))
+ .ThrowOnError();
+ }
+ }
+
+ WaitFor(response->GetAttachmentsStream()->Close())
+ .ThrowOnError();
+
+ response->set_total_size(totalSize);
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, ServerStreamsAborted)
+ {
+ context->SetRequestInfo();
+
+ auto promise = NewPromise<void>();
+ context->SubscribeCanceled(BIND([=] () mutable {
+ promise.Set();
+ }));
+
+ promise
+ .ToFuture()
+ .Get()
+ .ThrowOnError();
+
+ EXPECT_THROW({
+ response->GetAttachmentsStream()->Write(TSharedMutableRef::Allocate(100))
+ .Get()
+ .ThrowOnError();
+ }, TErrorException);
+
+ EXPECT_THROW({
+ request->GetAttachmentsStream()->Read()
+ .Get()
+ .ThrowOnError();
+ }, TErrorException);
+
+ ServerStreamsAborted_.Set();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, ServerNotReading)
+ {
+ context->SetRequestInfo();
+
+ WaitFor(context->GetRequestAttachmentsStream()->Read())
+ .ThrowOnError();
+
+ try {
+ auto sleep = request->sleep();
+ if (sleep) {
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ }
+
+ WaitFor(context->GetRequestAttachmentsStream()->Read())
+ .ThrowOnError();
+ context->Reply();
+ } catch (const TFiberCanceledException&) {
+ SlowCallCanceled_.Set();
+ throw;
+ }
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, ServerNotWriting)
+ {
+ context->SetRequestInfo();
+
+ auto data = TSharedRef::FromString("abacaba");
+ WaitFor(context->GetResponseAttachmentsStream()->Write(data))
+ .ThrowOnError();
+
+ try {
+ auto sleep = request->sleep();
+ if (sleep) {
+ TDelayedExecutor::WaitForDuration(TDuration::Seconds(1));
+ }
+
+ WaitFor(context->GetResponseAttachmentsStream()->Close())
+ .ThrowOnError();
+ context->Reply();
+ } catch (const TFiberCanceledException&) {
+ SlowCallCanceled_.Set();
+ throw;
+ }
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, FlakyCall)
+ {
+ static std::atomic<int> callCount;
+
+ context->SetRequestInfo();
+
+ if (callCount.fetch_add(1) % 2) {
+ context->Reply();
+ } else {
+ context->Reply(TError(EErrorCode::TransportError, "Flaky call iteration"));
+ }
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, RequireCoolFeature)
+ {
+ context->SetRequestInfo();
+ context->ValidateClientFeature(EMyFeature::Cool);
+ context->Reply();
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NMyRpc, GetTraceBaggage)
+ {
+ context->SetRequestInfo();
+ auto* traceContext = NTracing::TryGetCurrentTraceContext();
+ response->set_baggage(NYson::ConvertToYsonString(traceContext->UnpackBaggage()).ToString());
+ context->Reply();
+ }
+
+ TFuture<void> GetServerStreamsAborted() const override
+ {
+ return ServerStreamsAborted_.ToFuture();
+ }
+
+private:
+ const bool Secure_;
+
+ TPromise<void> SlowCallCanceled_ = NewPromise<void>();
+ TPromise<void> ServerStreamsAborted_ = NewPromise<void>();
+
+
+ void BeforeInvoke(IServiceContext* context) override
+ {
+ TServiceBase::BeforeInvoke(context);
+ if (Secure_) {
+ const auto& ext = context->GetRequestHeader().GetExtension(NGrpc::NProto::TSslCredentialsExt::ssl_credentials_ext);
+ EXPECT_EQ("localhost", ext.peer_identity());
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IMyServicePtr CreateMyService(IInvokerPtr invoker, bool secure)
+{
+ return New<TMyService>(invoker, secure);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/my_service.h b/yt/yt/core/rpc/unittests/lib/my_service.h
new file mode 100644
index 0000000000..58eefdca56
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/my_service.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <yt/yt/core/rpc/client.h>
+#include <yt/yt/core/rpc/service.h>
+
+#include <yt/yt/core/rpc/unittests/lib/my_service.pb.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMyFeature,
+ ((Cool) (0))
+ ((Great)(1))
+);
+
+class TMyProxy
+ : public TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TMyProxy, MyService,
+ .SetProtocolVersion(1)
+ .SetFeaturesType<EMyFeature>());
+
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, SomeCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, PassCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, RegularAttachments);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, NullAndEmptyAttachments);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, Compression);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, DoNothing);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, CustomMessageError);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, NotRegistered);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, SlowCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, SlowCanceledCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, NoReply);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, FlakyCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, RequireCoolFeature);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, RequestBytesThrottledCall);
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, StreamingEcho,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, ServerStreamsAborted,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, ServerNotReading,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, ServerNotWriting,
+ .SetStreamingEnabled(true));
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, GetTraceBaggage);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(IMyService)
+
+class IMyService
+ : public virtual IService
+{
+public:
+ virtual TFuture<void> GetSlowCallCanceled() const = 0;
+ virtual TFuture<void> GetServerStreamsAborted() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IMyService)
+
+////////////////////////////////////////////////////////////////////////////////
+
+IMyServicePtr CreateMyService(IInvokerPtr invoker, bool secure);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/my_service.proto b/yt/yt/core/rpc/unittests/lib/my_service.proto
new file mode 100644
index 0000000000..9bc9c6b4d5
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/my_service.proto
@@ -0,0 +1,210 @@
+package NMyRpc;
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+
+option go_package = "a.yandex-team.ru/yt/go/proto/core/rpc/unittests;myservice";
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqSomeCall
+{
+ required int32 a = 1;
+}
+
+message TRspSomeCall
+{
+ required int32 b = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqPassCall
+{
+}
+
+message TRspPassCall
+{
+ required string user = 1;
+ optional string user_tag = 2;
+ required NYT.NProto.TGuid mutation_id = 3;
+ required bool retry = 4;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqRegularAttachments
+{
+}
+
+message TRspRegularAttachments
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqCompression
+{
+ required int32 request_codec = 1;
+ required string message = 2;
+}
+
+message TRspCompression
+{
+ required string message = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqNullAndEmptyAttachments
+{
+}
+
+message TRspNullAndEmptyAttachments
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqDoNothing
+{
+}
+
+message TRspDoNothing
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqCustomMessageError
+{
+}
+
+message TRspCustomMessageError
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqNotRegistered
+{
+}
+
+message TRspNotRegistered
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqSlowCall
+{
+}
+
+message TRspSlowCall
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqSlowCanceledCall
+{
+}
+
+message TRspSlowCanceledCall
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqRequestBytesThrottledCall
+{
+}
+
+message TRspRequestBytesThrottledCall
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqNoReply
+{
+}
+
+message TRspNoReply
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqFlakyCall
+{
+}
+
+message TRspFlakyCall
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqRequireCoolFeature
+{
+}
+
+message TRspRequireCoolFeature
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqStreamingEcho
+{
+ optional bool delayed = 1 [default = false];
+}
+
+message TRspStreamingEcho
+{
+ required int64 total_size = 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqServerStreamsAborted
+{
+}
+
+message TRspServerStreamsAborted
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqServerNotReading
+{
+ required bool sleep = 1;
+}
+
+message TRspServerNotReading
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqServerNotWriting
+{
+ required bool sleep = 1;
+}
+
+message TRspServerNotWriting
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqGetTraceBaggage
+{
+}
+
+message TRspGetTraceBaggage
+{
+ required string baggage = 1;
+}
diff --git a/yt/yt/core/rpc/unittests/lib/no_baggage_service.cpp b/yt/yt/core/rpc/unittests/lib/no_baggage_service.cpp
new file mode 100644
index 0000000000..3c73bb4dda
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/no_baggage_service.cpp
@@ -0,0 +1,48 @@
+#include "no_baggage_service.h"
+
+#include <yt/yt/core/rpc/service_detail.h>
+
+#include <yt/yt/core/tracing/trace_context.h>
+
+#include <gtest/gtest.h>
+
+namespace NYT::NRpc {
+
+using namespace NTracing;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNoBaggageService
+ : public TServiceBase
+ , public virtual IService
+{
+public:
+ TNoBaggageService(IInvokerPtr invoker)
+ : TServiceBase(
+ invoker,
+ TNoBaggageProxy::GetDescriptor(),
+ NLogging::TLogger("Main"))
+ {
+ RegisterMethod(RPC_SERVICE_METHOD_DESC(ExpectNoBaggage));
+ }
+
+ DECLARE_RPC_SERVICE_METHOD(NNoBaggageRpc, ExpectNoBaggage)
+ {
+ context->SetRequestInfo();
+ auto baggage = GetCurrentTraceContext()->UnpackBaggage();
+ EXPECT_FALSE(baggage);
+ context->Reply();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServicePtr CreateNoBaggageService(IInvokerPtr invoker)
+{
+ return New<TNoBaggageService>(invoker);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/no_baggage_service.h b/yt/yt/core/rpc/unittests/lib/no_baggage_service.h
new file mode 100644
index 0000000000..7b50f97075
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/no_baggage_service.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <yt/yt/core/rpc/client.h>
+#include <yt/yt/core/rpc/service.h>
+
+#include <yt/yt/core/rpc/unittests/lib/no_baggage_service.pb.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNoBaggageProxy
+ : public TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TNoBaggageProxy, NoBaggageService,
+ .SetProtocolVersion(1)
+ .SetAcceptsBaggage(false));
+
+ DEFINE_RPC_PROXY_METHOD(NNoBaggageRpc, ExpectNoBaggage);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServicePtr CreateNoBaggageService(IInvokerPtr invoker);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/lib/no_baggage_service.proto b/yt/yt/core/rpc/unittests/lib/no_baggage_service.proto
new file mode 100644
index 0000000000..0993fa5118
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/no_baggage_service.proto
@@ -0,0 +1,9 @@
+package NNoBaggageRpc;
+
+////////////////////////////////////////////////////////////////////////////////
+
+message TReqExpectNoBaggage
+{ }
+
+message TRspExpectNoBaggage
+{ }
diff --git a/yt/yt/core/rpc/unittests/lib/ya.make b/yt/yt/core/rpc/unittests/lib/ya.make
new file mode 100644
index 0000000000..a2186cf413
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/lib/ya.make
@@ -0,0 +1,19 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ common.cpp
+ my_service.cpp
+ my_service.proto
+ no_baggage_service.cpp
+ no_baggage_service.proto
+)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/rpc/grpc
+ library/cpp/testing/gtest
+)
+
+END()
diff --git a/yt/yt/core/rpc/unittests/main/ya.make b/yt/yt/core/rpc/unittests/main/ya.make
new file mode 100644
index 0000000000..b1695d3d8f
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/main/ya.make
@@ -0,0 +1,34 @@
+GTEST(unittester-core-rpc)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ yt/yt/core/rpc/unittests/roaming_channel_ut.cpp
+ yt/yt/core/rpc/unittests/rpc_ut.cpp
+ yt/yt/core/rpc/unittests/viable_peer_registry_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/rpc/grpc
+ yt/yt/core/rpc/unittests/lib
+ yt/yt/core/test_framework
+ library/cpp/testing/common
+)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/rpc/unittests/mock/service.cpp b/yt/yt/core/rpc/unittests/mock/service.cpp
new file mode 100644
index 0000000000..77f89fd79d
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/mock/service.cpp
@@ -0,0 +1,15 @@
+#include "service.h"
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TMockServiceContext::TMockServiceContext()
+{
+ ON_CALL(*this, GetRequestHeader()).WillByDefault(::testing::ReturnRef(RequestHeader_));
+ ON_CALL(*this, ResponseAttachments()).WillByDefault(::testing::ReturnRef(ResponseAttachments_));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/mock/service.h b/yt/yt/core/rpc/unittests/mock/service.h
new file mode 100644
index 0000000000..3367e6d390
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/mock/service.h
@@ -0,0 +1,369 @@
+#pragma once
+
+#include <yt/yt/core/rpc/service.h>
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockServiceContext
+ : public IServiceContext
+{
+public:
+ NProto::TRequestHeader RequestHeader_;
+ std::vector<TSharedRef> ResponseAttachments_;
+
+public:
+ TMockServiceContext();
+
+ MOCK_METHOD(
+ const NProto::TRequestHeader&,
+ GetRequestHeader,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TSharedRefArray,
+ GetRequestMessage,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TRequestId,
+ GetRequestId,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NBus::TBusNetworkStatistics,
+ GetBusNetworkStatistics,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NYTree::IAttributeDictionary&,
+ GetEndpointAttributes,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TInstant>,
+ GetStartTime,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TDuration>,
+ GetTimeout,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TInstant,
+ GetArriveInstant,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TInstant>,
+ GetRunInstant,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TInstant>,
+ GetFinishInstant,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TDuration>,
+ GetWaitDuration,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TDuration>,
+ GetExecutionDuration,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NTracing::TTraceContextPtr,
+ GetTraceContext,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ std::optional<TDuration>,
+ GetTraceContextTime,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ bool,
+ IsRetry,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TMutationId,
+ GetMutationId,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ const TString&,
+ GetService,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ const TString&,
+ GetMethod,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TRealmId,
+ GetRealmId,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ const TAuthenticationIdentity&,
+ GetAuthenticationIdentity,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ bool,
+ IsReplied,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ void,
+ Reply,
+ (const TError& error),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ Reply,
+ (const TSharedRefArray& message),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SetComplete,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ bool,
+ IsCanceled,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SubscribeCanceled,
+ (const TCallback<void()>& callback),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ UnsubscribeCanceled,
+ (const TCallback<void()>& callback),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SubscribeReplied,
+ (const TCallback<void()>& callback),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ UnsubscribeReplied,
+ (const TCallback<void()>& callback),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ Cancel,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ TFuture<NYT::TSharedRefArray>,
+ GetAsyncResponseMessage,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ const TSharedRefArray&,
+ GetResponseMessage,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ const TError&,
+ GetError,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TSharedRef,
+ GetRequestBody,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ TSharedRef,
+ GetResponseBody,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SetResponseBody,
+ (const TSharedRef& responseBody),
+ (override)
+ );
+
+ MOCK_METHOD(
+ std::vector<TSharedRef>&,
+ RequestAttachments,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ NConcurrency::IAsyncZeroCopyInputStreamPtr,
+ GetRequestAttachmentsStream,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ std::vector<TSharedRef>&,
+ ResponseAttachments,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr,
+ GetResponseAttachmentsStream,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ const NProto::TRequestHeader&,
+ RequestHeader,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NProto::TRequestHeader&,
+ RequestHeader,
+ (),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SetRawRequestInfo,
+ (TString info, bool incremental),
+ (override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SetRawResponseInfo,
+ (TString info, bool incremental),
+ (override)
+ );
+
+ MOCK_METHOD(
+ const NLogging::TLogger&,
+ GetLogger,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NLogging::ELogLevel,
+ GetLogLevel,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ bool,
+ IsPooled,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ NCompression::ECodec,
+ GetResponseCodec,
+ (),
+ (const, override)
+ );
+
+ MOCK_METHOD(
+ void,
+ SetResponseCodec,
+ (NCompression::ECodec codec),
+ (override)
+ );
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/mock/ya.make b/yt/yt/core/rpc/unittests/mock/ya.make
new file mode 100644
index 0000000000..4512d4b889
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/mock/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ service.cpp
+)
+
+PEERDIR(
+ yt/yt/core
+ library/cpp/testing/gtest
+)
+
+END()
diff --git a/yt/yt/core/rpc/unittests/roaming_channel_ut.cpp b/yt/yt/core/rpc/unittests/roaming_channel_ut.cpp
new file mode 100644
index 0000000000..543bbfa291
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/roaming_channel_ut.cpp
@@ -0,0 +1,192 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/rpc/roaming_channel.h>
+
+#include <yt/yt/core/rpc/unittests/lib/common.h>
+
+namespace NYT::NRpc {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const NLogging::TLogger Logger("RoamingChannelTest");
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOneChannelProvider
+ : public IRoamingChannelProvider
+{
+public:
+ explicit TOneChannelProvider(IChannelPtr channel)
+ : Channel_(std::move(channel))
+ { }
+
+ const TString& GetEndpointDescription() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& /*request*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const TString& /*serviceName*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel() override
+ {
+ return MakeFuture(Channel_);
+ }
+
+ void Terminate(const TError& /*error*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ const IChannelPtr Channel_;
+};
+
+class TManualProvider
+ : public IRoamingChannelProvider
+{
+public:
+ void SetChannel(IChannelPtr channel)
+ {
+ YT_ASSERT(!Channel_.IsSet());
+ Channel_.Set(std::move(channel));
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& /*request*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const TString& /*serviceName*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel() override
+ {
+ return Channel_.ToFuture();
+ }
+
+ void Terminate(const TError& /*error*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ const TPromise<IChannelPtr> Channel_ = NewPromise<IChannelPtr>();
+};
+
+class TNeverProvider
+ : public IRoamingChannelProvider
+{
+public:
+ const TString& GetEndpointDescription() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const IClientRequestPtr& /*request*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel(const TString& /*serviceName*/) override
+ {
+ return GetChannel();
+ }
+
+ TFuture<IChannelPtr> GetChannel() override
+ {
+ return Channel_.ToFuture();
+ }
+
+ void Terminate(const TError& /*error*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+private:
+ const TPromise<IChannelPtr> Channel_ = NewPromise<IChannelPtr>();
+};
+
+template <class TImpl>
+using TRpcTest = TTestBase<TImpl>;
+TYPED_TEST_SUITE(TRpcTest, TAllTransports);
+
+TYPED_TEST(TRpcTest, RoamingChannelNever)
+{
+ auto channel = CreateRoamingChannel(New<TOneChannelProvider>(CreateRoamingChannel(New<TNeverProvider>())));
+
+ TMyProxy proxy(std::move(channel));
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+
+ auto asyncRspOrError = req->Invoke()
+ .WithTimeout(TDuration::Seconds(1));
+
+ auto rspOrError = NConcurrency::WaitFor(asyncRspOrError);
+ EXPECT_TRUE(!rspOrError.IsOK() && rspOrError.GetCode() == NYT::EErrorCode::Timeout);
+}
+
+TYPED_TEST(TRpcTest, RoamingChannelManual)
+{
+ auto manualProvider = New<TManualProvider>();
+
+ auto channel = CreateRoamingChannel(New<TOneChannelProvider>(CreateRoamingChannel(manualProvider)));
+
+ auto manualProviderWeak = MakeWeak(manualProvider);
+ manualProvider.Reset();
+
+ TMyProxy proxy(std::move(channel));
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+ auto asyncRspOrError = req->Invoke()
+ .WithTimeout(TDuration::Seconds(1));
+
+ {
+ auto strong = manualProviderWeak.Lock();
+ YT_ASSERT(strong);
+ strong->SetChannel(this->CreateChannel());
+ }
+
+ auto rspOrError = NConcurrency::WaitFor(asyncRspOrError);
+ EXPECT_TRUE(rspOrError.IsOK())
+ << ToString(rspOrError);
+ const auto& rsp = rspOrError.Value();
+ EXPECT_EQ(142, rsp->b());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/rpc_shutdown_ut.cpp b/yt/yt/core/rpc/unittests/rpc_shutdown_ut.cpp
new file mode 100644
index 0000000000..2465097b0d
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/rpc_shutdown_ut.cpp
@@ -0,0 +1,53 @@
+#include <yt/yt/core/rpc/unittests/lib/common.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <library/cpp/testing/hook/hook.h>
+
+namespace NYT::NRpc {
+namespace {
+
+template <class TImpl>
+using TRpcShutdownTest = TTestBase<TImpl>;
+
+TYPED_TEST_SUITE(TRpcShutdownTest, TAllTransports);
+
+Y_TEST_HOOK_BEFORE_RUN(GTEST_YT_RPC_SHUTDOWN)
+{
+ GTEST_FLAG_SET(death_test_style, "threadsafe");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TMyProxy>
+void TestShutdown(const IChannelPtr& channel)
+{
+ TMyProxy proxy(channel);
+
+ std::vector<NYT::TFuture<typename TTypedClientResponse<NMyRpc::TRspSomeCall>::TResult>> futures;
+ futures.reserve(100000);
+ for (int i = 0; i < 100000; ++i) {
+ auto req = proxy.SomeCall();
+ req->SetTimeout(TDuration::Seconds(1));
+ req->set_a(42);
+ futures.push_back(req->Invoke());
+ }
+
+ NYT::Shutdown();
+
+ for (auto& future : futures) {
+ future.Cancel(TError{});
+ }
+
+ _exit(0);
+}
+
+TYPED_TEST(TRpcShutdownTest, Shutdown)
+{
+ EXPECT_EXIT(TestShutdown<TMyProxy>(this->CreateChannel()), testing::ExitedWithCode(0), "");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/rpc_ut.cpp b/yt/yt/core/rpc/unittests/rpc_ut.cpp
new file mode 100644
index 0000000000..57573c3b83
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/rpc_ut.cpp
@@ -0,0 +1,1342 @@
+#include <yt/yt/core/rpc/unittests/lib/common.h>
+
+namespace NYT::NRpc {
+namespace {
+
+using namespace NYT::NBus;
+using namespace NYT::NRpc::NBus;
+using namespace NYT::NYTree;
+using namespace NYT::NYson;
+using namespace NConcurrency;
+using namespace NCrypto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNonExistingServiceProxy
+ : public TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TNonExistingServiceProxy, NonExistingService);
+
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, DoNothing);
+};
+
+class TMyIncorrectProtocolVersionProxy
+ : public TProxyBase
+{
+public:
+ DEFINE_RPC_PROXY(TMyIncorrectProtocolVersionProxy, MyService,
+ .SetProtocolVersion(2));
+
+ DEFINE_RPC_PROXY_METHOD(NMyRpc, SomeCall);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString StringFromSharedRef(const TSharedRef& sharedRef)
+{
+ return TString(sharedRef.Begin(), sharedRef.Begin() + sharedRef.Size());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TImpl>
+using TRpcTest = TTestBase<TImpl>;
+template <class TImpl>
+using TNotUdsTest = TTestBase<TImpl>;
+template <class TImpl>
+using TNotGrpcTest = TTestBase<TImpl>;
+template <class TImpl>
+using TGrpcTest = TTestBase<TImpl>;
+TYPED_TEST_SUITE(TRpcTest, TAllTransports);
+TYPED_TEST_SUITE(TNotUdsTest, TWithoutUds);
+TYPED_TEST_SUITE(TNotGrpcTest, TWithoutGrpc);
+TYPED_TEST_SUITE(TGrpcTest, TGrpcOnly);
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPED_TEST(TRpcTest, Send)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+ const auto& rsp = rspOrError.Value();
+ EXPECT_EQ(142, rsp->b());
+}
+
+TYPED_TEST(TRpcTest, RetryingSend)
+{
+ auto config = New<TRetryingChannelConfig>();
+ config->Load(ConvertTo<INodePtr>(TYsonString(TStringBuf(
+ "{retry_backoff_time=10}"))));
+
+ IChannelPtr channel = CreateRetryingChannel(
+ std::move(config),
+ this->CreateChannel());
+
+ {
+ TMyProxy proxy(channel);
+ auto req = proxy.FlakyCall();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+ }
+
+ // Channel must be asynchronously deleted after response handling finished.
+ // In particular, all possible cyclic dependencies must be resolved.
+ WaitForPredicate([&channel] {
+ return channel->GetRefCount() == 1;
+ });
+}
+
+TYPED_TEST(TRpcTest, UserTag)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.PassCall();
+ req->SetUser("test-user");
+ req->SetUserTag("test-user-tag");
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+ const auto& rsp = rspOrError.Value();
+ EXPECT_EQ(req->GetUser(), rsp->user());
+ EXPECT_EQ(req->GetUserTag(), rsp->user_tag());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPED_TEST(TNotUdsTest, Address)
+{
+ auto testChannel = [] (IChannelPtr channel) {
+ TMyProxy proxy(std::move(channel));
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+ const auto& rsp = rspOrError.Value();
+ EXPECT_FALSE(rsp->GetAddress().empty());
+ };
+
+ testChannel(this->CreateChannel());
+
+ {
+ auto config = New<TRetryingChannelConfig>();
+ config->Load(ConvertTo<INodePtr>(TYsonString(TStringBuf(
+ "{retry_backoff_time=10}"))));
+ testChannel(CreateRetryingChannel(
+ std::move(config),
+ this->CreateChannel()));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPED_TEST(TNotGrpcTest, SendSimple)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.PassCall();
+ req->SetUser("test-user");
+ req->SetMutationId(TGuid::Create());
+ req->SetRetry(true);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+ const auto& rsp = rspOrError.Value();
+ EXPECT_EQ(req->GetUser(), rsp->user());
+ EXPECT_FALSE(rsp->has_user_tag());
+ EXPECT_EQ(req->GetMutationId(), NYT::FromProto<TMutationId>(rsp->mutation_id()));
+ EXPECT_EQ(true, rsp->retry());
+}
+
+TYPED_TEST(TNotGrpcTest, StreamingEcho)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.SetDefaultRequestCodec(NCompression::ECodec::Lz4);
+ proxy.SetDefaultResponseCodec(NCompression::ECodec::Zstd_1);
+ proxy.SetDefaultEnableLegacyRpcCodecs(false);
+
+ const int AttachmentCount = 30;
+ const ssize_t AttachmentSize = 2_MB;
+
+ std::mt19937 randomGenerator;
+ std::uniform_int_distribution<char> distribution(std::numeric_limits<char>::min(), std::numeric_limits<char>::max());
+
+ std::vector<TSharedRef> attachments;
+
+ for (int i = 0; i < AttachmentCount; ++i) {
+ auto data = TSharedMutableRef::Allocate(AttachmentSize);
+ for (size_t j = 0; j < AttachmentSize; ++j) {
+ data[j] = distribution(randomGenerator);
+ }
+ attachments.push_back(std::move(data));
+ }
+
+ for (bool delayed : {false, true}) {
+ auto req = proxy.StreamingEcho();
+ req->set_delayed(delayed);
+ req->SetResponseHeavy(true);
+ auto asyncInvokeResult = req->Invoke();
+
+ std::vector<TSharedRef> receivedAttachments;
+
+ for (const auto& sentData : attachments) {
+ WaitFor(req->GetRequestAttachmentsStream()->Write(sentData))
+ .ThrowOnError();
+
+ if (!delayed) {
+ auto receivedData = WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ValueOrThrow();
+ receivedAttachments.push_back(std::move(receivedData));
+ }
+ }
+
+ auto asyncCloseResult = req->GetRequestAttachmentsStream()->Close();
+ EXPECT_FALSE(asyncCloseResult.IsSet());
+
+ if (delayed) {
+ for (int i = 0; i < AttachmentCount; ++i) {
+ auto receivedData = WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ValueOrThrow();
+ ASSERT_TRUE(receivedData);
+ receivedAttachments.push_back(std::move(receivedData));
+ }
+ }
+
+ {
+ auto receivedData = WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ValueOrThrow();
+ ASSERT_FALSE(receivedData);
+ }
+
+ for (int i = 0; i < AttachmentCount; ++i) {
+ EXPECT_TRUE(TRef::AreBitwiseEqual(attachments[i], receivedAttachments[i]));
+ }
+
+ WaitFor(asyncCloseResult)
+ .ThrowOnError();
+
+ auto rsp = WaitFor(asyncInvokeResult)
+ .ValueOrThrow();
+
+ EXPECT_EQ(AttachmentCount * AttachmentSize, rsp->total_size());
+ }
+}
+
+TYPED_TEST(TNotGrpcTest, ClientStreamsAborted)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.StreamingEcho();
+ req->SetTimeout(TDuration::MilliSeconds(100));
+
+ auto rspOrError = WaitFor(req->Invoke());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, rspOrError.GetCode());
+
+ EXPECT_THROW({
+ WaitFor(req->GetRequestAttachmentsStream()->Write(TSharedMutableRef::Allocate(100)))
+ .ThrowOnError();
+ }, TErrorException);
+
+ EXPECT_THROW({
+ WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ThrowOnError();
+ }, TErrorException);
+}
+
+TYPED_TEST(TNotGrpcTest, ServerStreamsAborted)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.ServerStreamsAborted();
+ req->SetTimeout(TDuration::MilliSeconds(100));
+
+ auto rspOrError = WaitFor(req->Invoke());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, rspOrError.GetCode());
+
+ WaitFor(this->MyService_->GetServerStreamsAborted())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TNotGrpcTest, ClientNotReading)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultServerAttachmentsStreamingParameters().WriteTimeout = TDuration::MilliSeconds(250);
+
+ for (auto sleep : {false, true}) {
+ auto expectedErrorCode = sleep ? NYT::EErrorCode::Timeout : NYT::EErrorCode::OK;
+
+ auto req = proxy.StreamingEcho();
+ req->set_delayed(true);
+ auto invokeResult = req->Invoke();
+
+ WaitFor(req->GetRequestAttachmentsStream()->Write(TSharedRef::FromString("hello")))
+ .ThrowOnError();
+ WaitFor(req->GetRequestAttachmentsStream()->Close())
+ .ThrowOnError();
+ WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ThrowOnError();
+
+ if (sleep) {
+ Sleep(TDuration::MilliSeconds(750));
+ }
+
+ auto streamError = static_cast<TError>(
+ WaitFor(req->GetResponseAttachmentsStream()->Read()));
+ EXPECT_EQ(expectedErrorCode, streamError.GetCode());
+ auto rspOrError = WaitFor(invokeResult);
+ EXPECT_EQ(expectedErrorCode, rspOrError.GetCode());
+ }
+}
+
+TYPED_TEST(TNotGrpcTest, ClientNotWriting)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultServerAttachmentsStreamingParameters().ReadTimeout = TDuration::MilliSeconds(250);
+
+ for (auto sleep : {false, true}) {
+ auto expectedErrorCode = sleep ? NYT::EErrorCode::Timeout : NYT::EErrorCode::OK;
+
+ auto req = proxy.StreamingEcho();
+ auto invokeResult = req->Invoke();
+
+ WaitFor(req->GetRequestAttachmentsStream()->Write(TSharedRef::FromString("hello")))
+ .ThrowOnError();
+ WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ThrowOnError();
+
+ if (sleep) {
+ Sleep(TDuration::MilliSeconds(750));
+ }
+
+ auto closeError = WaitFor(req->GetRequestAttachmentsStream()->Close());
+ auto readError = static_cast<TError>(
+ WaitFor(req->GetResponseAttachmentsStream()->Read()));
+
+ EXPECT_EQ(expectedErrorCode, closeError.GetCode());
+ EXPECT_EQ(expectedErrorCode, readError.GetCode());
+ auto rspOrError = WaitFor(invokeResult);
+ EXPECT_EQ(expectedErrorCode, rspOrError.GetCode());
+ }
+}
+
+TYPED_TEST(TNotGrpcTest, ServerNotReading)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultClientAttachmentsStreamingParameters().WriteTimeout = TDuration::MilliSeconds(250);
+
+ for (auto sleep : {false, true}) {
+ auto expectedStreamErrorCode = sleep ? NYT::EErrorCode::Timeout : NYT::EErrorCode::OK;
+ auto expectedInvokeErrorCode = sleep ? NYT::EErrorCode::Canceled : NYT::EErrorCode::OK;
+
+ auto req = proxy.ServerNotReading();
+ req->set_sleep(sleep);
+ auto invokeResult = req->Invoke();
+
+ auto data = TSharedRef::FromString("hello");
+ WaitFor(req->GetRequestAttachmentsStream()->Write(data))
+ .ThrowOnError();
+
+ auto streamError = WaitFor(req->GetRequestAttachmentsStream()->Close());
+ EXPECT_EQ(expectedStreamErrorCode, streamError.GetCode());
+ auto rspOrError = WaitFor(invokeResult);
+ EXPECT_EQ(expectedInvokeErrorCode, rspOrError.GetCode());
+ }
+
+ WaitFor(this->MyService_->GetSlowCallCanceled())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TNotGrpcTest, ServerNotWriting)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultClientAttachmentsStreamingParameters().ReadTimeout = TDuration::MilliSeconds(250);
+
+ for (auto sleep : {false, true}) {
+ auto expectedStreamErrorCode = sleep ? NYT::EErrorCode::Timeout : NYT::EErrorCode::OK;
+ auto expectedInvokeErrorCode = sleep ? NYT::EErrorCode::Canceled : NYT::EErrorCode::OK;
+
+ auto req = proxy.ServerNotWriting();
+ req->set_sleep(sleep);
+ auto invokeResult = req->Invoke();
+
+ WaitFor(req->GetResponseAttachmentsStream()->Read())
+ .ThrowOnError();
+
+ auto streamError = WaitFor(req->GetResponseAttachmentsStream()->Read());
+ EXPECT_EQ(expectedStreamErrorCode, streamError.GetCode());
+ auto rspOrError = WaitFor(invokeResult);
+ EXPECT_EQ(expectedInvokeErrorCode, rspOrError.GetCode());
+ }
+
+ WaitFor(this->MyService_->GetSlowCallCanceled())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TNotGrpcTest, LaggyStreamingRequest)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultServerAttachmentsStreamingParameters().ReadTimeout = TDuration::MilliSeconds(500);
+ proxy.DefaultClientAttachmentsStreamingParameters().WriteTimeout = TDuration::MilliSeconds(500);
+
+ auto req = proxy.StreamingEcho();
+ req->SetRequestHeavy(true);
+ req->SetResponseHeavy(true);
+ req->SetSendDelay(TDuration::MilliSeconds(250));
+ req->SetTimeout(TDuration::Seconds(2));
+ auto invokeResult = req->Invoke();
+
+ WaitFor(req->GetRequestAttachmentsStream()->Close())
+ .ThrowOnError();
+ WaitFor(ExpectEndOfStream(req->GetResponseAttachmentsStream()))
+ .ThrowOnError();
+ WaitFor(invokeResult)
+ .ThrowOnError();
+}
+
+TYPED_TEST(TNotGrpcTest, VeryLaggyStreamingRequest)
+{
+ auto configText = TString(R"({
+ services = {
+ MyService = {
+ pending_payloads_timeout = 250;
+ };
+ };
+ })");
+ auto config = ConvertTo<TServerConfigPtr>(TYsonString(configText));
+ this->Server_->Configure(config);
+
+ TMyProxy proxy(this->CreateChannel());
+ proxy.DefaultServerAttachmentsStreamingParameters().ReadTimeout = TDuration::MilliSeconds(500);
+
+ auto start = Now();
+
+ auto req = proxy.StreamingEcho();
+ req->SetRequestHeavy(true);
+ req->SetResponseHeavy(true);
+ req->SetSendDelay(TDuration::MilliSeconds(500));
+ auto invokeResult = req->Invoke();
+
+ auto closeError = WaitFor(req->GetRequestAttachmentsStream()->Close());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, closeError.GetCode());
+ auto streamError = WaitFor(req->GetResponseAttachmentsStream()->Read());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, streamError.GetCode());
+ auto rspOrError = WaitFor(invokeResult);
+ EXPECT_EQ(NYT::EErrorCode::Timeout, rspOrError.GetCode());
+
+ auto end = Now();
+ int duration = (end - start).MilliSeconds();
+ EXPECT_LE(duration, 2000);
+}
+
+TYPED_TEST(TNotGrpcTest, TraceBaggagePropagation)
+{
+ using namespace NTracing;
+
+ auto traceContext = TTraceContext::NewRoot("Test");
+ TCurrentTraceContextGuard guard(traceContext);
+
+ auto baggage = CreateEphemeralAttributes();
+ baggage->Set("key1", "value1");
+ baggage->Set("key2", "value2");
+ traceContext->PackBaggage(ConvertToAttributes(baggage));
+
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.GetTraceBaggage();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+ auto rsp = rspOrError.Value();
+
+ auto receivedBaggage = TYsonString(rsp->baggage());
+ EXPECT_EQ(receivedBaggage, ConvertToYsonString(baggage));
+}
+
+TYPED_TEST(TNotGrpcTest, DisableAcceptsBaggage)
+{
+ using namespace NTracing;
+
+ auto traceContext = TTraceContext::NewRoot("Test");
+ TCurrentTraceContextGuard guard(traceContext);
+
+ auto baggage = CreateEphemeralAttributes();
+ baggage->Set("key1", "value1");
+ baggage->Set("key2", "value2");
+ traceContext->PackBaggage(ConvertToAttributes(baggage));
+
+ TNoBaggageProxy proxy(this->CreateChannel());
+ auto req = proxy.ExpectNoBaggage();
+
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+}
+
+TYPED_TEST(TRpcTest, ManyAsyncRequests)
+{
+ const int RequestCount = 1000;
+
+ std::vector<TFuture<void>> asyncResults;
+
+ TMyProxy proxy(this->CreateChannel());
+
+ for (int i = 0; i < RequestCount; ++i) {
+ auto request = proxy.SomeCall();
+ request->set_a(i);
+ auto asyncResult = request->Invoke().Apply(BIND([=] (TMyProxy::TRspSomeCallPtr rsp) {
+ EXPECT_EQ(i + 100, rsp->b());
+ }));
+ asyncResults.push_back(asyncResult);
+ }
+
+ EXPECT_TRUE(AllSucceeded(asyncResults).Get().IsOK());
+}
+
+TYPED_TEST(TRpcTest, RegularAttachments)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.RegularAttachments();
+
+ req->Attachments().push_back(TSharedRef::FromString("Hello"));
+ req->Attachments().push_back(TSharedRef::FromString("from"));
+ req->Attachments().push_back(TSharedRef::FromString("TMyProxy"));
+
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+ const auto& rsp = rspOrError.Value();
+
+ const auto& attachments = rsp->Attachments();
+ EXPECT_EQ(3u, attachments.size());
+ EXPECT_EQ("Hello_", StringFromSharedRef(attachments[0]));
+ EXPECT_EQ("from_", StringFromSharedRef(attachments[1]));
+ EXPECT_EQ("TMyProxy_", StringFromSharedRef(attachments[2]));
+}
+
+TYPED_TEST(TRpcTest, NullAndEmptyAttachments)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.NullAndEmptyAttachments();
+
+ req->Attachments().push_back(TSharedRef());
+ req->Attachments().push_back(TSharedRef::MakeEmpty());
+
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+ auto rsp = rspOrError.Value();
+
+ const auto& attachments = rsp->Attachments();
+ EXPECT_EQ(2u, attachments.size());
+ EXPECT_FALSE(attachments[0]);
+ EXPECT_TRUE(attachments[1]);
+ EXPECT_TRUE(attachments[1].Empty());
+}
+
+TYPED_TEST(TNotGrpcTest, Compression)
+{
+ const auto requestCodecId = NCompression::ECodec::Zstd_2;
+ const auto responseCodecId = NCompression::ECodec::Snappy;
+
+ TString message("This is a message string.");
+ std::vector<TString> attachmentStrings({
+ "This is an attachment string.",
+ "640K ought to be enough for anybody.",
+ "According to all known laws of aviation, there is no way that a bee should be able to fly."
+ });
+
+ TMyProxy proxy(this->CreateChannel());
+ proxy.SetDefaultRequestCodec(requestCodecId);
+ proxy.SetDefaultResponseCodec(responseCodecId);
+ proxy.SetDefaultEnableLegacyRpcCodecs(false);
+
+ auto req = proxy.Compression();
+ req->set_request_codec(static_cast<int>(requestCodecId));
+ req->set_message(message);
+ for (const auto& attachmentString : attachmentStrings) {
+ req->Attachments().push_back(TSharedRef::FromString(attachmentString));
+ }
+
+ auto rspOrError = req->Invoke().Get();
+ rspOrError.ThrowOnError();
+ EXPECT_TRUE(rspOrError.IsOK());
+ auto rsp = rspOrError.Value();
+
+ EXPECT_TRUE(rsp->message() == message);
+ EXPECT_TRUE(rsp->GetResponseMessage().Size() >= 2);
+ const auto& serializedResponseBody = SerializeProtoToRefWithCompression(*rsp, responseCodecId);
+ const auto& compressedResponseBody = rsp->GetResponseMessage()[1];
+ EXPECT_TRUE(TRef::AreBitwiseEqual(compressedResponseBody, serializedResponseBody));
+
+ const auto& attachments = rsp->Attachments();
+ EXPECT_TRUE(attachments.size() == attachmentStrings.size());
+ EXPECT_TRUE(rsp->GetResponseMessage().Size() == attachments.size() + 2);
+ auto* responseCodec = NCompression::GetCodec(responseCodecId);
+ for (int i = 0; i < std::ssize(attachments); ++i) {
+ EXPECT_TRUE(StringFromSharedRef(attachments[i]) == attachmentStrings[i]);
+ auto compressedAttachment = responseCodec->Compress(attachments[i]);
+ EXPECT_TRUE(TRef::AreBitwiseEqual(rsp->GetResponseMessage()[i + 2], compressedAttachment));
+ }
+}
+
+#if !defined(_asan_enabled_) && !defined(_msan_enabled_) && defined(_linux_)
+
+TYPED_TEST(TRpcTest, ResponseMemoryTag)
+{
+ static TMemoryTag testMemoryTag = 12345;
+ testMemoryTag++;
+ auto initialMemoryUsage = GetMemoryUsageForTag(testMemoryTag);
+
+ std::vector<TMyProxy::TRspPassCallPtr> rsps;
+ {
+ TMyProxy proxy(this->CreateChannel());
+ TString longString(100, 'a');
+
+ TMemoryTagGuard guard(testMemoryTag);
+
+ for (int i = 0; i < 10000; ++i) {
+ auto req = proxy.PassCall();
+ req->SetUser(longString);
+ req->SetMutationId(TGuid::Create());
+ req->SetRetry(false);
+ auto err = req->Invoke().Get();
+ rsps.push_back(err.ValueOrThrow());
+ }
+ }
+
+ auto currentMemoryUsage = GetMemoryUsageForTag(testMemoryTag);
+ EXPECT_GE(currentMemoryUsage - initialMemoryUsage, 500'000u)
+ << "InitialUsage: " << initialMemoryUsage << std::endl
+ << "Current: " << currentMemoryUsage;
+}
+
+#endif
+
+TYPED_TEST(TNotGrpcTest, RequestBytesThrottling)
+{
+ auto configText = TString(R"({
+ services = {
+ MyService = {
+ methods = {
+ RequestBytesThrottledCall = {
+ request_bytes_throttler = {
+ limit = 1000000;
+ }
+ }
+ }
+ };
+ };
+ })");
+ auto config = ConvertTo<TServerConfigPtr>(TYsonString(configText));
+ this->Server_->Configure(config);
+
+ TMyProxy proxy(this->CreateChannel());
+
+ auto makeCall = [&] {
+ auto req = proxy.RequestBytesThrottledCall();
+ req->Attachments().push_back(TSharedMutableRef::Allocate(100'000));
+ return req->Invoke().AsVoid();
+ };
+
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < 30; ++i) {
+ futures.push_back(makeCall());
+ }
+
+ NProfiling::TWallTimer timer;
+ EXPECT_TRUE(AllSucceeded(std::move(futures)).Get().IsOK());
+ EXPECT_LE(std::abs(static_cast<i64>(timer.GetElapsedTime().MilliSeconds()) - 3000), 200);
+}
+
+// Now test different types of errors
+
+TYPED_TEST(TRpcTest, OK)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.DoNothing();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+}
+
+TYPED_TEST(TRpcTest, NoAck)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.DoNothing();
+ req->SetAcknowledgementTimeout(std::nullopt);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+}
+
+TYPED_TEST(TRpcTest, TransportError)
+{
+ TMyProxy proxy(this->CreateChannel("localhost:9999"));
+ auto req = proxy.DoNothing();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::TransportError, rspOrError.GetCode());
+}
+
+TYPED_TEST(TRpcTest, NoService)
+{
+ TNonExistingServiceProxy proxy(this->CreateChannel());
+ auto req = proxy.DoNothing();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::NoSuchService, rspOrError.GetCode());
+}
+
+TYPED_TEST(TRpcTest, NoMethod)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.NotRegistered();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::NoSuchMethod, rspOrError.GetCode());
+}
+
+// NB: Realms are not supported in RPC over GRPC.
+TYPED_TEST(TNotGrpcTest, NoSuchRealm)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.DoNothing();
+ ToProto(req->Header().mutable_realm_id(), TGuid::FromString("1-2-3-4"));
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::NoSuchService, rspOrError.GetCode());
+ EXPECT_TRUE(rspOrError.FindMatching(NRpc::EErrorCode::NoSuchRealm));
+}
+
+TYPED_TEST(TRpcTest, ClientTimeout)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.SetDefaultTimeout(TDuration::Seconds(0.5));
+ auto req = proxy.SlowCall();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(this->CheckTimeoutCode(rspOrError.GetCode()));
+}
+
+TYPED_TEST(TRpcTest, ServerTimeout)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.SetDefaultTimeout(TDuration::Seconds(0.5));
+ auto req = proxy.SlowCanceledCall();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(this->CheckTimeoutCode(rspOrError.GetCode()));
+ WaitFor(this->MyService_->GetSlowCallCanceled())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TRpcTest, ClientCancel)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.SlowCanceledCall();
+ auto asyncRspOrError = req->Invoke();
+ Sleep(TDuration::Seconds(0.5));
+ EXPECT_FALSE(asyncRspOrError.IsSet());
+ asyncRspOrError.Cancel(TError("Error"));
+ Sleep(TDuration::Seconds(0.1));
+ EXPECT_TRUE(asyncRspOrError.IsSet());
+ auto rspOrError = asyncRspOrError.Get();
+ EXPECT_TRUE(this->CheckCancelCode(rspOrError.GetCode()));
+ WaitFor(this->MyService_->GetSlowCallCanceled())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TRpcTest, SlowCall)
+{
+ TMyProxy proxy(this->CreateChannel());
+ proxy.SetDefaultTimeout(TDuration::Seconds(2.0));
+ auto req = proxy.SlowCall();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK());
+}
+
+TYPED_TEST(TRpcTest, RequestQueueSizeLimit)
+{
+ TMyProxy proxy(this->CreateChannel());
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < 30; ++i) {
+ auto req = proxy.SlowCall();
+ futures.push_back(req->Invoke().AsVoid());
+ }
+ Sleep(TDuration::MilliSeconds(100));
+ {
+ auto req = proxy.SlowCall();
+ EXPECT_EQ(NRpc::EErrorCode::RequestQueueSizeLimitExceeded, req->Invoke().Get().GetCode());
+ }
+ EXPECT_TRUE(AllSucceeded(std::move(futures)).Get().IsOK());
+}
+
+TYPED_TEST(TRpcTest, ConcurrencyLimit)
+{
+ TMyProxy proxy(this->CreateChannel());
+ std::vector<TFuture<void>> futures;
+ for (int i = 0; i < 10; ++i) {
+ auto req = proxy.SlowCall();
+ futures.push_back(req->Invoke().AsVoid());
+ }
+
+ Sleep(TDuration::MilliSeconds(100));
+
+ TFuture<void> backlogFuture;
+ {
+ auto req = proxy.SlowCall();
+ backlogFuture = req->Invoke().AsVoid();
+ }
+
+ EXPECT_TRUE(AllSucceeded(std::move(futures)).Get().IsOK());
+
+ Sleep(TDuration::MilliSeconds(400));
+ EXPECT_FALSE(backlogFuture.IsSet());
+
+ EXPECT_TRUE(backlogFuture.Get().IsOK());
+}
+
+TYPED_TEST(TRpcTest, NoReply)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.NoReply();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::Unavailable, rspOrError.GetCode());
+}
+
+TYPED_TEST(TRpcTest, CustomErrorMessage)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.CustomMessageError();
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NYT::EErrorCode(42), rspOrError.GetCode());
+ EXPECT_EQ("Some Error", rspOrError.GetMessage());
+}
+
+TYPED_TEST(TRpcTest, ConnectionLost)
+{
+ TMyProxy proxy(this->CreateChannel());
+
+ auto req = proxy.SlowCanceledCall();
+ auto asyncRspOrError = req->Invoke();
+
+ Sleep(TDuration::Seconds(0.5));
+
+ EXPECT_FALSE(asyncRspOrError.IsSet());
+ this->Server_->Stop(false);
+
+ Sleep(TDuration::Seconds(2));
+
+ EXPECT_TRUE(asyncRspOrError.IsSet());
+ auto rspOrError = asyncRspOrError.Get();
+ EXPECT_EQ(NRpc::EErrorCode::TransportError, rspOrError.GetCode());
+ WaitFor(this->MyService_->GetSlowCallCanceled())
+ .ThrowOnError();
+}
+
+TYPED_TEST(TNotGrpcTest, ProtocolVersionMismatch)
+{
+ TMyIncorrectProtocolVersionProxy proxy(this->CreateChannel());
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::ProtocolError, rspOrError.GetCode());
+}
+
+TYPED_TEST(TNotGrpcTest, RequiredServerFeatureSupported)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.PassCall();
+ req->RequireServerFeature(EMyFeature::Great);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+}
+
+TYPED_TEST(TNotGrpcTest, RequiredServerFeatureNotSupported)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.PassCall();
+ req->RequireServerFeature(EMyFeature::Cool);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::UnsupportedServerFeature, rspOrError.GetCode());
+ EXPECT_EQ(static_cast<int>(EMyFeature::Cool), rspOrError.Attributes().Get<int>(FeatureIdAttributeKey));
+ EXPECT_EQ(ToString(EMyFeature::Cool), rspOrError.Attributes().Get<TString>(FeatureNameAttributeKey));
+}
+
+TYPED_TEST(TNotGrpcTest, RequiredClientFeatureSupported)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.RequireCoolFeature();
+ req->DeclareClientFeature(EMyFeature::Cool);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_TRUE(rspOrError.IsOK()) << ToString(rspOrError);
+}
+
+TYPED_TEST(TNotGrpcTest, RequiredClientFeatureNotSupported)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.RequireCoolFeature();
+ req->DeclareClientFeature(EMyFeature::Great);
+ auto rspOrError = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::UnsupportedClientFeature, rspOrError.GetCode());
+ EXPECT_EQ(static_cast<int>(EMyFeature::Cool), rspOrError.Attributes().Get<int>(FeatureIdAttributeKey));
+ EXPECT_EQ(ToString(EMyFeature::Cool), rspOrError.Attributes().Get<TString>(FeatureNameAttributeKey));
+}
+
+TYPED_TEST(TRpcTest, StopWithoutActiveRequests)
+{
+ auto stopResult = this->MyService_->Stop();
+ EXPECT_TRUE(stopResult.IsSet());
+}
+
+TYPED_TEST(TRpcTest, StopWithActiveRequests)
+{
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.SlowCall();
+ auto reqResult = req->Invoke();
+ Sleep(TDuration::Seconds(0.5));
+ auto stopResult = this->MyService_->Stop();
+ EXPECT_FALSE(stopResult.IsSet());
+ EXPECT_TRUE(reqResult.Get().IsOK());
+ Sleep(TDuration::Seconds(0.5));
+ EXPECT_TRUE(stopResult.IsSet());
+}
+
+TYPED_TEST(TRpcTest, NoMoreRequestsAfterStop)
+{
+ auto stopResult = this->MyService_->Stop();
+ EXPECT_TRUE(stopResult.IsSet());
+ TMyProxy proxy(this->CreateChannel());
+ auto req = proxy.SlowCall();
+ auto reqResult = req->Invoke();
+ EXPECT_FALSE(reqResult.Get().IsOK());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPED_TEST(TGrpcTest, SendMessageLimit)
+{
+ THashMap<TString, NYTree::INodePtr> arguments;
+ arguments["grpc.max_send_message_length"] = NYT::NYTree::ConvertToNode(1);
+ TMyProxy proxy(this->CreateChannel(std::nullopt, std::move(arguments)));
+ auto req = proxy.SomeCall();
+ req->set_a(42);
+ auto error = req->Invoke().Get();
+ EXPECT_EQ(NRpc::EErrorCode::ProtocolError, error.GetCode());
+ EXPECT_THAT(error.GetMessage(), testing::HasSubstr("Sent message larger than max"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttachmentsInputStreamTest
+ : public ::testing::Test
+{
+protected:
+ TAttachmentsInputStreamPtr CreateStream(std::optional<TDuration> timeout = {})
+ {
+ return New<TAttachmentsInputStream>(
+ BIND([=] {}),
+ nullptr,
+ timeout);
+ }
+
+ static TStreamingPayload MakePayload(int sequenceNumber, std::vector<TSharedRef> attachments)
+ {
+ return TStreamingPayload{
+ NCompression::ECodec::None,
+ sequenceNumber,
+ std::move(attachments)
+ };
+ }
+};
+
+TEST_F(TAttachmentsInputStreamTest, AbortPropagatesToRead)
+{
+ auto stream = CreateStream();
+
+ auto future = stream->Read();
+ EXPECT_FALSE(future.IsSet());
+ stream->Abort(TError("oops"));
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_FALSE(future.Get().IsOK());
+}
+
+TEST_F(TAttachmentsInputStreamTest, EnqueueBeforeRead)
+{
+ auto stream = CreateStream();
+
+ auto payload = TSharedRef::FromString("payload");
+ stream->EnqueuePayload(MakePayload(0, std::vector<TSharedRef>{payload}));
+
+ auto future = stream->Read();
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload, future.Get().ValueOrThrow()));
+ EXPECT_EQ(7, stream->GetFeedback().ReadPosition);
+}
+
+TEST_F(TAttachmentsInputStreamTest, ReadBeforeEnqueue)
+{
+ auto stream = CreateStream();
+
+ auto future = stream->Read();
+ EXPECT_FALSE(future.IsSet());
+
+ auto payload = TSharedRef::FromString("payload");
+ stream->EnqueuePayload(MakePayload(0, std::vector<TSharedRef>{payload}));
+
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload, future.Get().ValueOrThrow()));
+ EXPECT_EQ(7, stream->GetFeedback().ReadPosition);
+}
+
+TEST_F(TAttachmentsInputStreamTest, CloseBeforeRead)
+{
+ auto stream = CreateStream();
+
+ auto payload = TSharedRef::FromString("payload");
+ stream->EnqueuePayload(MakePayload(0, {payload}));
+ stream->EnqueuePayload(MakePayload(1, {TSharedRef()}));
+
+ auto future1 = stream->Read();
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload, future1.Get().ValueOrThrow()));
+ EXPECT_EQ(7, stream->GetFeedback().ReadPosition);
+
+ auto future2 = stream->Read();
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_TRUE(!future2.Get().ValueOrThrow());
+ EXPECT_EQ(8, stream->GetFeedback().ReadPosition);
+}
+
+TEST_F(TAttachmentsInputStreamTest, Reordering)
+{
+ auto stream = CreateStream();
+
+ auto payload1 = TSharedRef::FromString("payload1");
+ auto payload2 = TSharedRef::FromString("payload2");
+
+ stream->EnqueuePayload(MakePayload(1, {payload2}));
+ stream->EnqueuePayload(MakePayload(0, {payload1}));
+
+ auto future1 = stream->Read();
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload1, future1.Get().ValueOrThrow()));
+ EXPECT_EQ(8, stream->GetFeedback().ReadPosition);
+
+ auto future2 = stream->Read();
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload2, future2.Get().ValueOrThrow()));
+ EXPECT_EQ(16, stream->GetFeedback().ReadPosition);
+}
+
+TEST_F(TAttachmentsInputStreamTest, EmptyAttachmentReadPosition)
+{
+ auto stream = CreateStream();
+ stream->EnqueuePayload(MakePayload(0, {TSharedMutableRef::Allocate(0)}));
+ EXPECT_EQ(0, stream->GetFeedback().ReadPosition);
+ auto future = stream->Read();
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_EQ(0u, future.Get().ValueOrThrow().size());
+ EXPECT_EQ(1, stream->GetFeedback().ReadPosition);
+}
+
+TEST_F(TAttachmentsInputStreamTest, Close)
+{
+ auto stream = CreateStream();
+ stream->EnqueuePayload(MakePayload(0, {TSharedRef()}));
+ auto future = stream->Read();
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_FALSE(future.Get().ValueOrThrow());
+}
+
+TEST_F(TAttachmentsInputStreamTest, Timeout)
+{
+ auto stream = CreateStream(TDuration::MilliSeconds(100));
+ auto future = stream->Read();
+ auto error = future.Get();
+ EXPECT_FALSE(error.IsOK());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, error.GetCode());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttachmentsOutputStreamTest
+ : public ::testing::Test
+{
+protected:
+ int PullCallbackCounter_;
+
+ TAttachmentsOutputStreamPtr CreateStream(
+ ssize_t windowSize,
+ std::optional<TDuration> timeout = {})
+ {
+ PullCallbackCounter_ = 0;
+ return New<TAttachmentsOutputStream>(
+ NCompression::ECodec::None,
+ nullptr,
+ BIND([this] {
+ ++PullCallbackCounter_;
+ }),
+ windowSize,
+ timeout);
+ }
+};
+
+TEST_F(TAttachmentsOutputStreamTest, NullPull)
+{
+ auto stream = CreateStream(100);
+ EXPECT_FALSE(stream->TryPull());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, SinglePull)
+{
+ auto stream = CreateStream(100);
+
+ auto payload = TSharedRef::FromString("payload");
+ auto future = stream->Write(payload);
+ EXPECT_EQ(1, PullCallbackCounter_);
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(future.Get().IsOK());
+
+ auto result = stream->TryPull();
+ EXPECT_TRUE(result);
+ EXPECT_EQ(0, result->SequenceNumber);
+ EXPECT_EQ(1u, result->Attachments.size());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload, result->Attachments[0]));
+}
+
+TEST_F(TAttachmentsOutputStreamTest, MultiplePull)
+{
+ auto stream = CreateStream(100);
+
+ std::vector<TSharedRef> payloads;
+ for (int i = 0; i < 10; ++i) {
+ auto payload = TSharedRef::FromString("payload" + ToString(i));
+ payloads.push_back(payload);
+ auto future = stream->Write(payload);
+ EXPECT_EQ(i + 1, PullCallbackCounter_);
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(future.Get().IsOK());
+ }
+
+ auto result = stream->TryPull();
+ EXPECT_TRUE(result);
+ EXPECT_EQ(0, result->SequenceNumber);
+ EXPECT_EQ(10u, result->Attachments.size());
+ for (size_t i = 0; i < 10; ++i) {
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payloads[i], result->Attachments[i]));
+ }
+}
+
+TEST_F(TAttachmentsOutputStreamTest, Backpressure)
+{
+ auto stream = CreateStream(5);
+
+ auto payload1 = TSharedRef::FromString("abc");
+ auto future1 = stream->Write(payload1);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future1.Get().IsOK());
+ EXPECT_EQ(1, PullCallbackCounter_);
+
+ auto payload2 = TSharedRef::FromString("def");
+ auto future2 = stream->Write(payload2);
+ EXPECT_FALSE(future2.IsSet());
+ EXPECT_EQ(2, PullCallbackCounter_);
+
+ auto result1 = stream->TryPull();
+ EXPECT_TRUE(result1);
+ EXPECT_EQ(0, result1->SequenceNumber);
+ EXPECT_EQ(1u, result1->Attachments.size());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload1, result1->Attachments[0]));
+
+ EXPECT_FALSE(future2.IsSet());
+
+ stream->HandleFeedback({3});
+
+ EXPECT_EQ(3, PullCallbackCounter_);
+
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future1.Get().IsOK());
+
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_TRUE(future2.Get().IsOK());
+
+ auto payload3 = TSharedRef::FromString("x");
+ auto future3 = stream->Write(payload3);
+ EXPECT_TRUE(future3.IsSet());
+ EXPECT_TRUE(future3.Get().IsOK());
+ EXPECT_EQ(4, PullCallbackCounter_);
+
+ auto result2 = stream->TryPull();
+ EXPECT_TRUE(result2);
+ EXPECT_EQ(2u, result2->Attachments.size());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload2, result2->Attachments[0]));
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload3, result2->Attachments[1]));
+}
+
+TEST_F(TAttachmentsOutputStreamTest, Abort1)
+{
+ auto stream = CreateStream(5);
+
+ auto payload1 = TSharedRef::FromString("abcabc");
+ auto future1 = stream->Write(payload1);
+ EXPECT_FALSE(future1.IsSet());
+
+ auto future2 = stream->Close();
+ EXPECT_FALSE(future1.IsSet());
+
+ stream->Abort(TError("oops"));
+
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_FALSE(future1.Get().IsOK());
+
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_FALSE(future2.Get().IsOK());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, Abort2)
+{
+ auto stream = CreateStream(5);
+
+ auto payload1 = TSharedRef::FromString("abcabc");
+ auto future1 = stream->Write(payload1);
+ EXPECT_FALSE(future1.IsSet());
+
+ stream->Abort(TError("oops"));
+
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_FALSE(future1.Get().IsOK());
+
+ auto future2 = stream->Close();
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_FALSE(future2.Get().IsOK());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, Close1)
+{
+ auto stream = CreateStream(5);
+
+ auto future = stream->Close();
+ EXPECT_FALSE(future.IsSet());
+ EXPECT_EQ(1, PullCallbackCounter_);
+
+ auto result = stream->TryPull();
+ EXPECT_TRUE(result);
+ EXPECT_EQ(0, result->SequenceNumber);
+ EXPECT_EQ(1u, result->Attachments.size());
+ EXPECT_FALSE(result->Attachments[0]);
+
+ stream->HandleFeedback({1});
+
+ EXPECT_TRUE(future.IsSet());
+ EXPECT_TRUE(future.Get().IsOK());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, Close2)
+{
+ auto stream = CreateStream(5);
+
+ auto payload = TSharedRef::FromString("abc");
+ auto future1 = stream->Write(payload);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future1.Get().IsOK());
+ EXPECT_EQ(1, PullCallbackCounter_);
+
+ auto future2 = stream->Close();
+ EXPECT_FALSE(future2.IsSet());
+ EXPECT_EQ(2, PullCallbackCounter_);
+
+ auto result = stream->TryPull();
+ EXPECT_TRUE(result);
+ EXPECT_EQ(0, result->SequenceNumber);
+ EXPECT_EQ(2u, result->Attachments.size());
+ EXPECT_TRUE(TRef::AreBitwiseEqual(payload, result->Attachments[0]));
+ EXPECT_FALSE(result->Attachments[1]);
+
+ stream->HandleFeedback({3});
+
+ EXPECT_FALSE(future2.IsSet());
+
+ stream->HandleFeedback({4});
+
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_TRUE(future2.Get().IsOK());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, WriteTimeout)
+{
+ auto stream = CreateStream(5, TDuration::MilliSeconds(100));
+
+ auto payload = TSharedRef::FromString("abc");
+
+ auto future1 = stream->Write(payload);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future1.Get().IsOK());
+
+ auto future2 = stream->Write(payload);
+ EXPECT_FALSE(future2.IsSet());
+ auto error = future2.Get();
+ EXPECT_FALSE(error.IsOK());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, error.GetCode());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, CloseTimeout)
+{
+ auto stream = CreateStream(5, TDuration::MilliSeconds(100));
+
+ auto future = stream->Close();
+ EXPECT_FALSE(future.IsSet());
+ auto error = future.Get();
+ EXPECT_FALSE(error.IsOK());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, error.GetCode());
+}
+
+TEST_F(TAttachmentsOutputStreamTest, CloseTimeout2)
+{
+ auto stream = CreateStream(10, TDuration::MilliSeconds(100));
+
+ auto payload = TSharedRef::FromString("abc");
+
+ auto future1 = stream->Write(payload);
+ EXPECT_TRUE(future1.IsSet());
+ EXPECT_TRUE(future1.Get().IsOK());
+
+ auto future2 = stream->Write(payload);
+ EXPECT_TRUE(future2.IsSet());
+ EXPECT_TRUE(future2.Get().IsOK());
+
+ auto future3 = stream->Close();
+ EXPECT_FALSE(future3.IsSet());
+
+ stream->HandleFeedback({3});
+
+ EXPECT_FALSE(future3.IsSet());
+
+ Sleep(TDuration::MilliSeconds(500));
+
+ ASSERT_TRUE(future3.IsSet());
+ auto error = future3.Get();
+ EXPECT_FALSE(error.IsOK());
+ EXPECT_EQ(NYT::EErrorCode::Timeout, error.GetCode());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TCachingChannelFactoryTest, IdleChannels)
+{
+ class TChannelFactory
+ : public IChannelFactory
+ {
+ public:
+ IChannelPtr CreateChannel(const TString& /*address*/) override
+ {
+ return CreateLocalChannel(Server_);
+ }
+
+ private:
+ const IServerPtr Server_ = CreateLocalServer();
+ };
+
+ auto factory = New<TChannelFactory>();
+ auto cachingFactory = CreateCachingChannelFactory(factory, TDuration::MilliSeconds(500));
+ auto channel = cachingFactory->CreateChannel("");
+ EXPECT_EQ(channel, cachingFactory->CreateChannel(""));
+
+ Sleep(TDuration::MilliSeconds(1000));
+ EXPECT_EQ(channel, cachingFactory->CreateChannel(""));
+
+ auto weakChannel = MakeWeak(channel);
+ channel.Reset();
+
+ Sleep(TDuration::MilliSeconds(1000));
+ EXPECT_TRUE(weakChannel.IsExpired());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/shutdown/ya.make b/yt/yt/core/rpc/unittests/shutdown/ya.make
new file mode 100644
index 0000000000..6766dbc010
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/shutdown/ya.make
@@ -0,0 +1,32 @@
+GTEST(unittester-core-rpc-shutdown)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ yt/yt/core/rpc/unittests/rpc_shutdown_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/rpc/grpc
+ yt/yt/core/rpc/unittests/lib
+ yt/yt/core/test_framework
+ library/cpp/testing/common
+)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/rpc/unittests/viable_peer_registry_ut.cpp b/yt/yt/core/rpc/unittests/viable_peer_registry_ut.cpp
new file mode 100644
index 0000000000..f8631b8fd3
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/viable_peer_registry_ut.cpp
@@ -0,0 +1,654 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/bus/bus.h>
+#include <yt/yt/core/bus/server.h>
+
+#include <yt/yt/core/net/local_address.h>
+
+#include <yt/yt/core/rpc/channel.h>
+#include <yt/yt/core/rpc/config.h>
+#include <yt/yt/core/rpc/client.h>
+#include <yt/yt/core/rpc/public.h>
+#include <yt/yt/core/rpc/viable_peer_registry.h>
+#include <yt/yt/core/rpc/indexed_hash_map.h>
+
+namespace NYT::NRpc {
+namespace {
+
+using namespace NConcurrency;
+using namespace NBus;
+
+using testing::Pair;
+using testing::UnorderedElementsAre;
+using testing::UnorderedElementsAreArray;
+using testing::AllOf;
+using testing::IsSubsetOf;
+using testing::Not;
+using testing::Contains;
+using testing::SizeIs;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const NLogging::TLogger Logger{"ViablePeerRegistryUnitTest"};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TIndexedHashMapTest, Simple)
+{
+ TIndexedHashMap<TString, int> test;
+
+ EXPECT_EQ(test.Size(), 0);
+
+ EXPECT_TRUE(test.Set("a", 1));
+ EXPECT_FALSE(test.Set("a", 2));
+ EXPECT_EQ(test.Get("a"), 2);
+ EXPECT_THAT(test[0], Pair("a", 2));
+ EXPECT_EQ(test.Size(), 1);
+
+ EXPECT_TRUE(test.Set("b", 3));
+ EXPECT_THAT(test, UnorderedElementsAre(
+ Pair("a", 2),
+ Pair("b", 3)));
+ EXPECT_EQ(test.Size(), 2);
+
+ EXPECT_TRUE(test.Set("c", 42));
+ EXPECT_EQ(test.Size(), 3);
+
+ TIndexedHashMap<TString, int>::TUnderlyingStorage data;
+ for (int i = 0; i < test.Size(); ++i) {
+ data.push_back(test[i]);
+ }
+ EXPECT_THAT(test, UnorderedElementsAreArray(data));
+
+ EXPECT_TRUE(test.find("d") == test.end());
+ EXPECT_FALSE(test.find("a") == test.end());
+
+ EXPECT_TRUE(test.Set("e", -1));
+ EXPECT_TRUE(test.Set("f", 123));
+
+ test.Erase(static_cast<int>(test.find("e") - test.begin()));
+
+ EXPECT_FALSE(test.Erase("d"));
+ EXPECT_TRUE(test.Erase("a"));
+ EXPECT_EQ(test.Size(), 3);
+ EXPECT_THAT(test, UnorderedElementsAre(
+ Pair("b", 3),
+ Pair("c", 42),
+ Pair("f", 123)));
+
+ test.Clear();
+ EXPECT_EQ(test.Size(), 0);
+ EXPECT_THAT(test, UnorderedElementsAre());
+}
+
+class TFakeChannel
+ : public IChannel
+{
+public:
+ TFakeChannel(TString address, THashSet<TString>* channelRegistry)
+ : Address_(std::move(address))
+ , ChannelRegistry_(channelRegistry)
+ {
+ if (ChannelRegistry_) {
+ ChannelRegistry_->insert(Address_);
+ }
+ }
+
+ const TString& GetEndpointDescription() const override
+ {
+ return Address_;
+ }
+
+ const NYTree::IAttributeDictionary& GetEndpointAttributes() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ IClientRequestControlPtr Send(
+ IClientRequestPtr /*request*/,
+ IClientResponseHandlerPtr /*responseHandler*/,
+ const TSendOptions& /*options*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ void Terminate(const TError& /*error*/) override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ ~TFakeChannel() override
+ {
+ if (ChannelRegistry_) {
+ YT_VERIFY(ChannelRegistry_->erase(Address_));
+ }
+ }
+
+ DEFINE_SIGNAL_OVERRIDE(void(const TError&), Terminated);
+private:
+ TString Address_;
+ THashSet<TString>* ChannelRegistry_;
+};
+
+class TFakeChannelFactory
+ : public IChannelFactory
+{
+public:
+ IChannelPtr CreateChannel(const TString& address) override
+ {
+ return New<TFakeChannel>(address, &ChannelRegistry_);
+ }
+
+ const THashSet<TString>& GetChannelRegistry() const
+ {
+ return ChannelRegistry_;
+ }
+
+private:
+ THashSet<TString> ChannelRegistry_;
+};
+
+IViablePeerRegistryPtr CreateTestRegistry(
+ EPeerPriorityStrategy peerPriorityStrategy,
+ const IChannelFactoryPtr& channelFactory,
+ int maxPeerCount,
+ std::optional<int> hashesPerPeer = {},
+ std::optional<int> minPeerCountForPriorityAwareness = {})
+{
+ auto config = New<TViablePeerRegistryConfig>();
+ config->MaxPeerCount = maxPeerCount;
+ if (hashesPerPeer) {
+ config->HashesPerPeer = *hashesPerPeer;
+ }
+ if (minPeerCountForPriorityAwareness) {
+ config->MinPeerCountForPriorityAwareness = *minPeerCountForPriorityAwareness;
+ }
+
+ config->PeerPriorityStrategy = peerPriorityStrategy;
+
+ return CreateViablePeerRegistry(config, BIND([=] (const TString& address) { return channelFactory->CreateChannel(address); }), Logger);
+}
+
+std::vector<TString> AddressesFromChannels(const std::vector<IChannelPtr>& channels)
+{
+ std::vector<TString> result;
+ for (const auto& channel: channels) {
+ result.push_back(channel->GetEndpointDescription());
+ }
+ return result;
+}
+
+class TParametrizedViablePeerRegistryTest
+ : public testing::TestWithParam<EPeerPriorityStrategy>
+{ };
+
+TEST_P(TParametrizedViablePeerRegistryTest, Simple)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 3);
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+ EXPECT_FALSE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d"));
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "b", "c"));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAreArray(AddressesFromChannels(viablePeerRegistry->GetActiveChannels())));
+
+ viablePeerRegistry->UnregisterPeer("b");
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "c", "d"));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAreArray(AddressesFromChannels(viablePeerRegistry->GetActiveChannels())));
+
+ // Peer "b" should end up in the backlog.
+ viablePeerRegistry->RegisterPeer("b");
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "c", "d"));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAreArray(AddressesFromChannels(viablePeerRegistry->GetActiveChannels())));
+
+ EXPECT_TRUE(viablePeerRegistry->MaybeRotateRandomPeer());
+
+ // Backlog contained a single peer "b", so it should be activated now.
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ AllOf(SizeIs(3), IsSubsetOf({"a", "b", "c", "d"}), Contains("b")));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAreArray(AddressesFromChannels(viablePeerRegistry->GetActiveChannels())));
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, RotateOnlyActivePeer)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 1);
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+
+ auto rotatedPeer = viablePeerRegistry->MaybeRotateRandomPeer();
+ EXPECT_TRUE(rotatedPeer);
+ EXPECT_EQ(*rotatedPeer, "a");
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, UnregisterFromBacklog)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 2);
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d"));
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "b"));
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("c"));
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "b"));
+
+ auto rotatedPeer = viablePeerRegistry->MaybeRotateRandomPeer();
+ EXPECT_TRUE(rotatedPeer);
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ AllOf(SizeIs(2), IsSubsetOf({"a", "b", "d"}), Not(Contains(*rotatedPeer)), Contains("d")));
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer(*rotatedPeer));
+ EXPECT_FALSE(viablePeerRegistry->MaybeRotateRandomPeer());
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("d"));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), AllOf(SizeIs(1), IsSubsetOf({"a", "b"})));
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, Clear)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 2);
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d"));
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "b"));
+
+ viablePeerRegistry->Clear();
+
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre());
+}
+
+class TFakeRequest
+ : public TClientRequest
+{
+public:
+ TFakeRequest()
+ : TClientRequest(
+ New<TFakeChannel>("fake", nullptr),
+ TServiceDescriptor{"service"},
+ TMethodDescriptor{"method"})
+ { }
+
+ TSharedRefArray SerializeHeaderless() const override
+ {
+ YT_UNIMPLEMENTED();
+ }
+
+ size_t GetHash() const override
+ {
+ return Hash_;
+ }
+
+private:
+ size_t Hash_ = RandomNumber<size_t>();
+};
+
+IClientRequestPtr CreateRequest(bool enableStickiness = false)
+{
+ auto request = New<TFakeRequest>();
+ auto balancingHeaderExt = request->Header().MutableExtension(NRpc::NProto::TBalancingExt::balancing_ext);
+ balancingHeaderExt->set_enable_stickiness(enableStickiness);
+ return request;
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, GetChannelBasic)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 3);
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("e"));
+
+ TString retrievedPeer;
+ {
+ auto channel = viablePeerRegistry->PickRandomChannel(CreateRequest(), /*hedgingOptions*/ {});
+ retrievedPeer = channel->GetEndpointDescription();
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), UnorderedElementsAre("a", "b", "c"));
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(retrievedPeer));
+ }
+
+ viablePeerRegistry->UnregisterPeer(retrievedPeer);
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), AllOf(
+ SizeIs(3),
+ IsSubsetOf({"a", "b", "c", "d", "e"}),
+ Not(Contains(retrievedPeer))));
+
+ {
+ auto channel = viablePeerRegistry->PickRandomChannel(CreateRequest(), /*hedgingOptions*/ {});;
+ EXPECT_NE(channel->GetEndpointDescription(), retrievedPeer);
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(channel->GetEndpointDescription()));
+ }
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, GetRandomChannel)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 100);
+
+ for (int i = 0; i < 100; ++i) {
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer(Format("address-%v", i)));
+ }
+
+ THashSet<TString> retrievedAddresses;
+
+ auto req = CreateRequest();
+ for (int iter = 0; iter < 100; ++iter) {
+ auto channel = viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ retrievedAddresses.insert(channel->GetEndpointDescription());
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(channel->GetEndpointDescription()));
+ }
+
+ // The probability of this failing should be 1e-200.
+ EXPECT_GT(retrievedAddresses.size(), 1u);
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, GetStickyChannel)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 2000, 10);
+
+ for (int i = 0; i < 1000; ++i) {
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer(Format("address-%v", i)));
+ }
+
+ THashSet<TString> retrievedAddresses;
+
+ auto req = CreateRequest(/*enableStickiness*/ true);
+ for (int iter = 0; iter < 100; ++iter) {
+ auto channel = viablePeerRegistry->PickStickyChannel(req);
+ retrievedAddresses.insert(channel->GetEndpointDescription());
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(channel->GetEndpointDescription()));
+ }
+
+ EXPECT_EQ(retrievedAddresses.size(), 1u);
+
+ THashMap<IClientRequestPtr, TString> requestToPeer;
+ for (int iter = 0; iter < 1000; ++iter) {
+ auto request = CreateRequest(/*enableStickiness*/ true);
+ auto channel = viablePeerRegistry->PickStickyChannel(request);
+ auto peer = channel->GetEndpointDescription();
+ requestToPeer[request] = peer;
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(peer));
+ }
+
+ for (int i = 0; i < 11; ++i) {
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer(Format("address-%v-2", i)));
+ }
+
+ int misses = 0;
+ for (const auto& [request, peer] : requestToPeer) {
+ auto channel = viablePeerRegistry->PickStickyChannel(request);
+ if (channel->GetEndpointDescription() != peer) {
+ ++misses;
+ }
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(channel->GetEndpointDescription()));
+ }
+
+ EXPECT_LE(misses, 100);
+}
+
+TEST_P(TParametrizedViablePeerRegistryTest, PeersAvailablePromise)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(GetParam(), channelFactory, 1);
+
+ auto peersAvailable1 = viablePeerRegistry->GetPeersAvailable();
+ EXPECT_FALSE(peersAvailable1.IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+ EXPECT_TRUE(peersAvailable1.IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("c"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->MaybeRotateRandomPeer());
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("a"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("b"));
+ EXPECT_FALSE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ EXPECT_FALSE(viablePeerRegistry->UnregisterPeer("e"));
+ EXPECT_FALSE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ viablePeerRegistry->SetError(TError("error"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+ EXPECT_FALSE(viablePeerRegistry->GetPeersAvailable().Get().IsOK());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("f"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().IsSet());
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().Get().IsOK());
+
+ viablePeerRegistry->SetError(TError("another error"));
+ EXPECT_TRUE(viablePeerRegistry->GetPeersAvailable().Get().IsOK());
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("f"));
+ EXPECT_FALSE(viablePeerRegistry->GetPeersAvailable().IsSet());
+
+ auto startTime = TInstant::Now();
+
+ TDelayedExecutor::Submit(BIND([viablePeerRegistry] {
+ viablePeerRegistry->RegisterPeer("i_am_available");
+ }), TDuration::Seconds(5));
+
+ auto channel = WaitFor(viablePeerRegistry->GetPeersAvailable()
+ .Apply(BIND([viablePeerRegistry] {
+ auto req = CreateRequest();
+ return viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ })))
+ .ValueOrThrow();
+
+ auto elapsedTime = TInstant::Now() - startTime;
+
+ EXPECT_GE(elapsedTime, TDuration::Seconds(3));
+ EXPECT_LE(elapsedTime, TDuration::Seconds(7));
+
+ EXPECT_EQ(channel->GetEndpointDescription(), "i_am_available");
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ AllPriorityStrategies,
+ TParametrizedViablePeerRegistryTest,
+ testing::Values(EPeerPriorityStrategy::None, EPeerPriorityStrategy::PreferLocal));
+
+TEST(TPreferLocalViablePeerRegistryTest, Simple)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(EPeerPriorityStrategy::PreferLocal, channelFactory, 3);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d.sas.yp-c.yandex.net"));
+
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ UnorderedElementsAre("b.sas.yp-c.yandex.net", "c.sas.yp-c.yandex.net", "a.man.yp-c.yandex.net"));
+
+ auto req = CreateRequest();
+ for (int iter = 0; iter < 100; ++iter) {
+ auto channel = viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ EXPECT_EQ(channel->GetEndpointDescription(), "a.man.yp-c.yandex.net");
+ }
+}
+
+TEST(TPreferLocalViablePeerRegistryTest, MinPeerCountForPriorityAwareness)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(
+ EPeerPriorityStrategy::PreferLocal,
+ channelFactory,
+ /*maxPeerCount*/ 100,
+ /*hashesPerPeer*/ {},
+ /*minPeerCountForPriorityAwareness*/ 2);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("local.man.yp-c.yandex.net"));
+
+ for (int i = 0; i < 99; ++i) {
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer(Format("non-local-%v.sas.yp-c.yandex.net", i)));
+ }
+
+ THashSet<TString> retrievedAddresses;
+
+ auto req = CreateRequest();
+ for (int iter = 0; iter < 100; ++iter) {
+ auto channel = viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ retrievedAddresses.insert(channel->GetEndpointDescription());
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains(channel->GetEndpointDescription()));
+ }
+
+ // The probability of this failing should be 1e-200.
+ EXPECT_GT(retrievedAddresses.size(), 1u);
+}
+
+TEST(TPreferLocalViablePeerRegistryTest, RegistrationEvictsLesserPeers)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(EPeerPriorityStrategy::PreferLocal, channelFactory, 3);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("e.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("f.man.yp-c.yandex.net"));
+
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ UnorderedElementsAre("a.man.yp-c.yandex.net", "e.man.yp-c.yandex.net", "f.man.yp-c.yandex.net"));
+
+ EXPECT_FALSE(viablePeerRegistry->MaybeRotateRandomPeer());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("g.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->MaybeRotateRandomPeer());
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains("g.man.yp-c.yandex.net"));
+}
+
+TEST(TPreferLocalViablePeerRegistryTest, PeerRotationRespectsPriority)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(EPeerPriorityStrategy::PreferLocal, channelFactory, 3);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("e.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("f.man.yp-c.yandex.net"));
+
+ EXPECT_FALSE(viablePeerRegistry->MaybeRotateRandomPeer());
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("g.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->MaybeRotateRandomPeer());
+ EXPECT_THAT(channelFactory->GetChannelRegistry(), Contains("g.man.yp-c.yandex.net"));
+}
+
+TEST(TPreferLocalViablePeerRegistryTest, FillFromBacklogRespectsPriority)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(EPeerPriorityStrategy::PreferLocal, channelFactory, 3);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("c.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("d.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("e.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("f.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("g.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("h.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("i.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("j.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("k.man.yp-c.yandex.net"));
+
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ UnorderedElementsAre("a.man.yp-c.yandex.net", "e.man.yp-c.yandex.net", "f.man.yp-c.yandex.net"));
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("a.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("e.man.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("f.man.yp-c.yandex.net"));
+
+ EXPECT_THAT(
+ channelFactory->GetChannelRegistry(),
+ UnorderedElementsAre("h.man.yp-c.yandex.net", "j.man.yp-c.yandex.net", "k.man.yp-c.yandex.net"));
+}
+
+TEST(TPreferLocalViablePeerRegistryTest, DoNotCrashIfNoLocalPeers)
+{
+ auto channelFactory = New<TFakeChannelFactory>();
+ auto viablePeerRegistry = CreateTestRegistry(EPeerPriorityStrategy::PreferLocal, channelFactory, 3);
+
+ auto finally = Finally([oldLocalHostName = NNet::GetLocalHostName()] {
+ NNet::WriteLocalHostName(oldLocalHostName);
+ });
+ NNet::WriteLocalHostName("home.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("b.sas.yp-c.yandex.net"));
+ EXPECT_TRUE(viablePeerRegistry->RegisterPeer("a.man.yp-c.yandex.net"));
+
+ auto req = CreateRequest();
+ auto localChannel = viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ EXPECT_EQ(localChannel->GetEndpointDescription(), "a.man.yp-c.yandex.net");
+
+ EXPECT_TRUE(viablePeerRegistry->UnregisterPeer("a.man.yp-c.yandex.net"));
+ auto otherChannel = viablePeerRegistry->PickRandomChannel(req, /*hedgingOptions*/ {});
+ EXPECT_EQ(otherChannel->GetEndpointDescription(), "b.sas.yp-c.yandex.net");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/unittests/ya.make b/yt/yt/core/rpc/unittests/ya.make
new file mode 100644
index 0000000000..cbaa5e58c6
--- /dev/null
+++ b/yt/yt/core/rpc/unittests/ya.make
@@ -0,0 +1,10 @@
+RECURSE(
+ bin
+ lib
+ mock
+)
+
+RECURSE_FOR_TESTS(
+ main
+ shutdown
+)
diff --git a/yt/yt/core/rpc/viable_peer_registry.cpp b/yt/yt/core/rpc/viable_peer_registry.cpp
new file mode 100644
index 0000000000..3bf35aa887
--- /dev/null
+++ b/yt/yt/core/rpc/viable_peer_registry.cpp
@@ -0,0 +1,578 @@
+#include "viable_peer_registry.h"
+
+#include "client.h"
+#include "config.h"
+#include "indexed_hash_map.h"
+
+#include <yt/yt/core/misc/random.h>
+
+#include <yt/yt/core/net/address.h>
+#include <yt/yt/core/net/local_address.h>
+
+#include <library/cpp/yt/small_containers/compact_set.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NNet;
+using namespace NThreading;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TViablePeerRegistry
+ : public IViablePeerRegistry
+{
+public:
+ TViablePeerRegistry(
+ TViablePeerRegistryConfigPtr config,
+ TCreateChannelCallback createChannel,
+ const NLogging::TLogger& logger)
+ : Config_(std::move(config))
+ , CreateChannel_(std::move(createChannel))
+ , Logger(logger)
+ {
+ {
+ auto guard = WriterGuard(SpinLock_);
+ InitPeersAvailablePromise();
+ }
+ }
+
+ bool RegisterPeer(const TString& address) override
+ {
+ int priority = 0;
+ if (Config_->PeerPriorityStrategy == EPeerPriorityStrategy::PreferLocal) {
+ priority = (InferYPClusterFromHostName(address) == GetLocalYPCluster()) ? 0 : 1;
+ }
+
+ bool wasEmpty = false;
+ bool peerAdded = false;
+ {
+ auto guard = WriterGuard(SpinLock_);
+ wasEmpty = ActivePeerToPriority_.Size() == 0;
+ peerAdded = RegisterPeerWithPriority(address, priority);
+ }
+
+ if (wasEmpty && peerAdded) {
+ GetPeersAvailablePromise(/*resetStoredError*/ true).TrySet();
+ }
+
+ return peerAdded;
+ }
+
+ bool UnregisterPeer(const TString& address) override
+ {
+ auto guard = WriterGuard(SpinLock_);
+ bool peerUnregistered = GuardedUnregisterPeer(address);
+
+ if (peerUnregistered) {
+ OnPeerUnregistered();
+ }
+
+ return peerUnregistered;
+ }
+
+ bool UnregisterChannel(const TString& address, const IChannelPtr& channel) override
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ auto it = ActivePeerToPriority_.find(address);
+ if (it == ActivePeerToPriority_.end()) {
+ return false;
+ }
+
+ auto storedChannel = GetOrCrash(PriorityToActivePeers_, it->second).Get(address);
+
+ if (storedChannel != channel) {
+ return false;
+ }
+
+ YT_VERIFY(GuardedUnregisterPeer(address));
+ OnPeerUnregistered();
+
+ return true;
+ }
+
+ std::vector<IChannelPtr> GetActiveChannels() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ std::vector<IChannelPtr> allActivePeers;
+ allActivePeers.reserve(ActivePeerToPriority_.Size());
+ for (const auto& [priority, activePeers] : PriorityToActivePeers_) {
+ for (const auto& [address, channel] : activePeers) {
+ allActivePeers.push_back(channel);
+ }
+ }
+ return allActivePeers;
+ }
+
+ void Clear() override
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ BacklogPeers_.Clear();
+ HashToActiveChannel_.clear();
+ ActivePeerToPriority_.Clear();
+ PriorityToActivePeers_.clear();
+
+ InitPeersAvailablePromise();
+ }
+
+ std::optional<TString> MaybeRotateRandomPeer() override
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ if (!BacklogPeerToPriority_.empty() && ActivePeerToPriority_.Size() > 0) {
+ auto lastActivePriority = PriorityToActivePeers_.rbegin()->first;
+ auto firstBacklogPriority = PriorityToBacklogPeers_.begin()->first;
+
+ YT_LOG_DEBUG(
+ "Trying to rotate random active peer (LastActivePriority: %v, FirstBacklogPriority: %v)",
+ lastActivePriority,
+ firstBacklogPriority);
+
+ if (lastActivePriority < firstBacklogPriority) {
+ return {};
+ }
+
+ // Invariant lastActivePriority <= firstBacklogPriority should hold.
+ YT_VERIFY(lastActivePriority == firstBacklogPriority);
+
+ const auto& activePeers = PriorityToActivePeers_.rbegin()->second;
+ auto addressToEvict = activePeers.GetRandomElement().first;
+
+ YT_LOG_DEBUG("Moving random viable peer to backlog (Address: %v)", addressToEvict);
+ // This call will automatically activate a random peer from the backlog.
+ GuardedUnregisterPeer(addressToEvict);
+ // The rotated peer will end up in the backlog after this call.
+ RegisterPeerWithPriority(addressToEvict, lastActivePriority);
+
+ return addressToEvict;
+ }
+ return {};
+ }
+
+ IChannelPtr PickStickyChannel(const IClientRequestPtr& request) const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (BacklogPeers_.Size() > 0) {
+ YT_LOG_WARNING(
+ "Sticky channels are used with non-empty peer backlog, random peer rotations might hurt stickiness (MaxPeerCount: %v, ViablePeerCount: %v, BacklogPeerCount: %v)",
+ Config_->MaxPeerCount,
+ ActivePeerToPriority_.Size(),
+ BacklogPeers_.Size());
+ }
+
+ const auto& balancingExt = request->Header().GetExtension(NProto::TBalancingExt::balancing_ext);
+ auto hash = request->GetHash();
+ auto randomNumber = balancingExt.enable_client_stickiness() ? ClientStickinessRandomNumber_ : RandomNumber<size_t>();
+ int stickyGroupSize = balancingExt.sticky_group_size();
+ auto randomIndex = randomNumber % stickyGroupSize;
+
+ if (ActivePeerToPriority_.Size() == 0) {
+ return nullptr;
+ }
+
+ auto it = HashToActiveChannel_.lower_bound(std::make_pair(hash, TString()));
+ auto rebaseIt = [&] {
+ if (it == HashToActiveChannel_.end()) {
+ it = HashToActiveChannel_.begin();
+ }
+ };
+
+ TCompactSet<TStringBuf, 16> seenAddresses;
+ auto currentRandomIndex = randomIndex % ActivePeerToPriority_.Size();
+ while (true) {
+ rebaseIt();
+ const auto& address = it->first.second;
+ if (seenAddresses.count(address) == 0) {
+ if (currentRandomIndex == 0) {
+ break;
+ }
+ seenAddresses.insert(address);
+ --currentRandomIndex;
+ } else {
+ ++it;
+ }
+ }
+
+ YT_LOG_DEBUG(
+ "Sticky peer selected (RequestId: %v, RequestHash: %x, RandomIndex: %v/%v, Address: %v)",
+ request->GetRequestId(),
+ hash,
+ randomIndex,
+ stickyGroupSize,
+ it->first.second);
+
+ return it->second;
+ }
+
+ // We only use this method for small counts, so this approach should work fine.
+ static THashSet<int> GetRandomIndexes(int max, int count = 1)
+ {
+ THashSet<int> result;
+ while (std::ssize(result) < count) {
+ result.insert(static_cast<int>(RandomNumber<unsigned int>(max)));
+ }
+
+ return result;
+ }
+
+ std::vector<std::pair<TString, IChannelPtr>> PickRandomPeers(int peerCount = 1) const
+ {
+ VERIFY_READER_SPINLOCK_AFFINITY(SpinLock_);
+
+ YT_VERIFY(0 < peerCount && peerCount <= ActivePeerToPriority_.Size());
+
+ int minPeersToPickFrom = std::max(Config_->MinPeerCountForPriorityAwareness, peerCount);
+
+ std::vector<std::pair<TString, IChannelPtr>> peers;
+
+ const auto& smallestPriorityPool = PriorityToActivePeers_.begin()->second;
+
+ if (minPeersToPickFrom <= smallestPriorityPool.Size()) {
+ for (const auto& index : GetRandomIndexes(smallestPriorityPool.Size(), peerCount)) {
+ peers.push_back(smallestPriorityPool[index]);
+ }
+ } else {
+ for (const auto& index : GetRandomIndexes(ActivePeerToPriority_.Size(), peerCount)) {
+ const auto& [address, priority] = ActivePeerToPriority_[index];
+ peers.emplace_back(
+ address,
+ GetOrCrash(PriorityToActivePeers_, priority).Get(address));
+ }
+ }
+
+ return peers;
+ }
+
+ IChannelPtr PickRandomChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions) const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (ActivePeerToPriority_.Size() == 0) {
+ return nullptr;
+ }
+
+ IChannelPtr channel;
+ if (hedgingOptions && hedgingOptions->HedgingManager && ActivePeerToPriority_.Size() >= 2) {
+ auto peers = PickRandomPeers(/*peerCount*/ 2);
+ const auto& primaryPeer = peers[0];
+ const auto& backupPeer = peers[1];
+ channel = CreateHedgingChannel(
+ primaryPeer.second,
+ backupPeer.second,
+ *hedgingOptions);
+
+ YT_LOG_DEBUG(
+ "Random peers selected (RequestId: %v, PrimaryAddress: %v, BackupAddress: %v)",
+ request ? request->GetRequestId() : TRequestId(),
+ primaryPeer.first,
+ backupPeer.first);
+ } else {
+ auto peer = PickRandomPeers()[0];
+ channel = peer.second;
+
+ YT_LOG_DEBUG(
+ "Random peer selected (RequestId: %v, Address: %v)",
+ request ? request->GetRequestId() : TRequestId(),
+ peer.first);
+ }
+
+ return channel;
+ }
+
+ IChannelPtr GetChannel(const TString& address) const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ if (auto it = ActivePeerToPriority_.find(address); it != ActivePeerToPriority_.end()) {
+ return GetOrCrash(PriorityToActivePeers_, it->second).Get(address);
+ }
+ return nullptr;
+ }
+
+ TFuture<void> GetPeersAvailable() const override
+ {
+ auto guard = ReaderGuard(SpinLock_);
+
+ return PeersAvailablePromise_.ToFuture().ToUncancelable();
+ }
+
+ void SetError(const TError& error) override
+ {
+ GetPeersAvailablePromise().TrySet(error);
+ }
+
+private:
+ const TViablePeerRegistryConfigPtr Config_;
+ const TCallback<IChannelPtr(TString address)> CreateChannel_;
+ const NLogging::TLogger Logger;
+
+ const size_t ClientStickinessRandomNumber_ = RandomNumber<size_t>();
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, SpinLock_);
+
+ // Information for active peers with created channels.
+ std::map<int, TIndexedHashMap<TString, IChannelPtr>> PriorityToActivePeers_;
+ TIndexedHashMap<TString, int> ActivePeerToPriority_;
+ // A consistent-hashing storage for serving sticky requests.
+ std::map<std::pair<size_t, TString>, IChannelPtr> HashToActiveChannel_;
+
+ // Information for non-active peers which go over the max peer count limit.
+ TIndexedHashMap<TString, int> BacklogPeers_;
+
+ THashMap<TString, int> BacklogPeerToPriority_;
+ std::map<int, TIndexedHashMap<TString, std::monostate>> PriorityToBacklogPeers_;
+
+ TPromise<void> PeersAvailablePromise_;
+
+ void InitPeersAvailablePromise()
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ YT_LOG_DEBUG("Awaiting peer availability");
+ PeersAvailablePromise_ = NewPromise<void>();
+
+ PeersAvailablePromise_.ToFuture().Subscribe(BIND([Logger = Logger] (const TError& error) {
+ if (error.IsOK()) {
+ YT_LOG_DEBUG("Peers are available");
+ } else {
+ YT_LOG_DEBUG(error, "Error while awaiting peers");
+ }
+ }));
+ }
+
+ void OnPeerUnregistered()
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ if (ActivePeerToPriority_.Size() == 0 && PeersAvailablePromise_.IsSet()) {
+ InitPeersAvailablePromise();
+ }
+ }
+
+ TPromise<void> GetPeersAvailablePromise(bool resetStoredError = false)
+ {
+ auto guard = WriterGuard(SpinLock_);
+
+ if (resetStoredError && PeersAvailablePromise_.IsSet() && !PeersAvailablePromise_.Get().IsOK()) {
+ InitPeersAvailablePromise();
+ }
+
+ return PeersAvailablePromise_;
+ }
+
+ //! Returns true if a new peer was successfully registered and false if it already existed.
+ //! Trying to call this method for a currently viable address with a different priority than stored leads to failure.
+ bool RegisterPeerWithPriority(const TString& address, int priority)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ // Check for an existing active peer for this address.
+ if (auto it = ActivePeerToPriority_.find(address); it != ActivePeerToPriority_.end()) {
+ // Peers should have a fixed priority.
+ YT_VERIFY(it->second == priority);
+ return false;
+ } else {
+ // Peer is new, we need to check that we won't be adding more than MaxPeerCount active peers.
+ if (ActivePeerToPriority_.Size() >= Config_->MaxPeerCount) {
+ // Check for an existing backlog entry for this peer.
+ if (auto backlogPeerIt = BacklogPeerToPriority_.find(address); backlogPeerIt != BacklogPeerToPriority_.end()) {
+ // Peers should have a fixed priority.
+ YT_VERIFY(backlogPeerIt->second == priority);
+ return false;
+ } else {
+ // MaxPeerCount is required to be positive.
+ YT_VERIFY(!PriorityToActivePeers_.empty());
+ auto lastActivePeerPriority = PriorityToActivePeers_.rbegin()->first;
+ YT_LOG_DEBUG(
+ "Comparing priorities with active peers (LargestActivePeerPriority: %v, CurrentPeerPriority: %v)",
+ lastActivePeerPriority,
+ priority);
+
+ if (priority < lastActivePeerPriority) {
+ // If an active peer with lower priority than the one being added exists,
+ // we move it to the backlog.
+
+ auto activePeerAddressToEvict = PriorityToActivePeers_.rbegin()->second.GetRandomElement().first;
+ EraseActivePeer(activePeerAddressToEvict);
+ AddBacklogPeer(activePeerAddressToEvict, lastActivePeerPriority);
+ YT_LOG_DEBUG(
+ "Active peer evicted to backlog (Address: %v, Priority: %v, ReplacingAddress: %v)",
+ activePeerAddressToEvict,
+ lastActivePeerPriority,
+ address);
+ // We don't return here, since we still need to add our actual peer to the set of active peers.
+ } else {
+ AddBacklogPeer(address, priority);
+ YT_LOG_DEBUG(
+ "Viable peer added to backlog (Address: %v, Priority: %v)",
+ address,
+ priority);
+ return true;
+ }
+ }
+ }
+ }
+
+ AddActivePeer(address, priority);
+
+ YT_LOG_DEBUG(
+ "Activated viable peer (Address: %v, Priority: %v, ActivePeerCount: %v, BacklogPeerCount: %v, MaxPeerCount: %v)",
+ address,
+ priority,
+ ActivePeerToPriority_.Size(),
+ BacklogPeerToPriority_.size(),
+ Config_->MaxPeerCount);
+
+ return true;
+ }
+
+ template <class F>
+ void GeneratePeerHashes(const TString& address, F f)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ TRandomGenerator generator(ComputeHash(address));
+ for (int index = 0; index < Config_->HashesPerPeer; ++index) {
+ f(generator.Generate<size_t>());
+ }
+ }
+
+ bool GuardedUnregisterPeer(const TString& address)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ // Check if the peer is in the backlog and erase it if so.
+ if (EraseBacklogPeer(address)) {
+ YT_LOG_DEBUG(
+ "Unregistered backlog peer (Address: %v, ActivePeerCount: %v, BacklogPeerCount: %v, MaxPeerCount: %v)",
+ address,
+ ActivePeerToPriority_.Size(),
+ BacklogPeerToPriority_.size(),
+ Config_->MaxPeerCount);
+ return true;
+ }
+
+ // Check if the peer is active and erase it if so.
+ if (EraseActivePeer(address)) {
+ YT_LOG_DEBUG(
+ "Unregistered active peer (Address: %v, ActivePeerCount: %v, BacklogPeerCount: %v, MaxPeerCount: %v)",
+ address,
+ ActivePeerToPriority_.Size(),
+ BacklogPeerToPriority_.size(),
+ Config_->MaxPeerCount);
+ ActivateBacklogPeers();
+ return true;
+ }
+
+ return false;
+ }
+
+ void ActivateBacklogPeers()
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ while (!BacklogPeerToPriority_.empty() && ActivePeerToPriority_.Size() < Config_->MaxPeerCount) {
+ auto& [priority, backlogPeers] = *PriorityToBacklogPeers_.begin();
+ auto randomBacklogPeer = backlogPeers.GetRandomElement();
+
+ // Peer will definitely be activated, since the number of active peers is less than MaxPeerCount.
+ RegisterPeerWithPriority(randomBacklogPeer.first, priority);
+ YT_LOG_DEBUG("Activated peer from backlog (Address: %v, Priority: %v)", randomBacklogPeer.first, priority);
+
+ // Until this moment the newly activated peer is still present in the backlog.
+ EraseBacklogPeer(randomBacklogPeer.first);
+ }
+ }
+
+ void AddActivePeer(const TString& address, int priority)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ ActivePeerToPriority_.Set(address, priority);
+
+ auto channel = CreateChannel_(address);
+
+ // Save the created channel for the given address for sticky requests.
+ GeneratePeerHashes(address, [&] (size_t hash) {
+ HashToActiveChannel_[std::make_pair(hash, address)] = channel;
+ });
+
+ // Save the channel for the given address at its priority.
+ PriorityToActivePeers_[priority].Set(address, channel);
+ }
+
+ void AddBacklogPeer(const TString& address, int priority)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ BacklogPeerToPriority_[address] = priority;
+ PriorityToBacklogPeers_[priority].Set(address, {});
+ }
+
+ bool EraseActivePeer(const TString& address)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ auto activePeerIt = ActivePeerToPriority_.find(address);
+
+ if (activePeerIt == ActivePeerToPriority_.end()) {
+ return false;
+ }
+
+ GeneratePeerHashes(address, [&] (size_t hash) {
+ HashToActiveChannel_.erase(std::make_pair(hash, address));
+ });
+
+ auto activePeersForPriorityIt = PriorityToActivePeers_.find(activePeerIt->second);
+ YT_VERIFY(activePeersForPriorityIt != PriorityToActivePeers_.end());
+ activePeersForPriorityIt->second.Erase(address);
+ if (activePeersForPriorityIt->second.Size() == 0) {
+ PriorityToActivePeers_.erase(activePeersForPriorityIt);
+ }
+
+ ActivePeerToPriority_.Erase(address);
+
+ return true;
+ }
+
+ bool EraseBacklogPeer(const TString& address)
+ {
+ VERIFY_WRITER_SPINLOCK_AFFINITY(SpinLock_);
+
+ auto backlogPeerIt = BacklogPeerToPriority_.find(address);
+
+ if (backlogPeerIt == BacklogPeerToPriority_.end()) {
+ return false;
+ }
+
+ auto backlogPeersForPriorityIt = PriorityToBacklogPeers_.find(backlogPeerIt->second);
+ YT_VERIFY(backlogPeersForPriorityIt != PriorityToBacklogPeers_.end());
+ backlogPeersForPriorityIt->second.Erase(address);
+ if (backlogPeersForPriorityIt->second.Size() == 0) {
+ PriorityToBacklogPeers_.erase(backlogPeersForPriorityIt);
+ }
+
+ BacklogPeerToPriority_.erase(backlogPeerIt);
+
+ return true;
+ }
+};
+
+IViablePeerRegistryPtr CreateViablePeerRegistry(
+ TViablePeerRegistryConfigPtr config,
+ TCreateChannelCallback createChannel,
+ const NLogging::TLogger& logger)
+{
+ return New<TViablePeerRegistry>(std::move(config), std::move(createChannel), logger);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/rpc/viable_peer_registry.h b/yt/yt/core/rpc/viable_peer_registry.h
new file mode 100644
index 0000000000..d0d1486875
--- /dev/null
+++ b/yt/yt/core/rpc/viable_peer_registry.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "public.h"
+#include "hedging_channel.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A registry maintaining a set of viable peers.
+//! Maintains no more than a configured number of channels, keeping the remaining addresses in a backlog.
+//! When peers with active channels are unregistered, their places are filled with addresses from the backlog.
+/*
+ * Thread affinity: any.
+ */
+struct IViablePeerRegistry
+ : public TRefCounted
+{
+ //! Returns nullptr if the registry is empty.
+ virtual IChannelPtr PickRandomChannel(
+ const IClientRequestPtr& request,
+ const std::optional<THedgingChannelOptions>& hedgingOptions) const = 0;
+ //! Returns nullptr if the registry is empty.
+ virtual IChannelPtr PickStickyChannel(const IClientRequestPtr& request) const = 0;
+ //! Returns nullptr if this address is not an active peer.
+ virtual IChannelPtr GetChannel(const TString& address) const = 0;
+
+ //! Returns true if a new peer was actually registered and false if it already existed.
+ virtual bool RegisterPeer(const TString& address) = 0;
+ //! Returns true if this peer was actually unregistered.
+ virtual bool UnregisterPeer(const TString& address) = 0;
+ //! Unregisters stored active peer with the given channel.
+ //! Does nothing and returns false if the peer is no longer active
+ //! or if the currently stored channel differs from the one given.
+ virtual bool UnregisterChannel(const TString& address, const IChannelPtr& channel) = 0;
+ //! Tries to evict a random active peer and replace it with a peer from the backlog.
+ //! The evicted peer is stored in the backlog instead.
+ //! Returns the address of the rotated peer.
+ virtual std::optional<TString> MaybeRotateRandomPeer() = 0;
+
+ virtual std::vector<IChannelPtr> GetActiveChannels() const = 0;
+ virtual void Clear() = 0;
+
+ //! If registry is non-empty, returns void future.
+ //! Otherwise, returns an uncancellable future that is set either
+ //! - successfully, if RegisterPeer() is called;
+ //! - to a corresponding error in case of SetError() call, e.g. due to external peer discovery logic error;
+ //! - to a "promise abandoned" error if Clear() is called.
+ virtual TFuture<void> GetPeersAvailable() const = 0;
+
+ //! Sets the peer availability promise with the given error.
+ virtual void SetError(const TError& error) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IViablePeerRegistry)
+
+using TCreateChannelCallback = TCallback<IChannelPtr(const TString& address)>;
+
+IViablePeerRegistryPtr CreateViablePeerRegistry(
+ TViablePeerRegistryConfigPtr config,
+ TCreateChannelCallback createChannel,
+ const NLogging::TLogger& logger);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
diff --git a/yt/yt/core/service_discovery/public.h b/yt/yt/core/service_discovery/public.h
new file mode 100644
index 0000000000..6a64aef3d4
--- /dev/null
+++ b/yt/yt/core/service_discovery/public.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NServiceDiscovery {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_ERROR_ENUM(
+ ((EndpointSetDoesNotExist) (20000))
+ ((EndpointResolveFailed) (20001))
+ ((UnknownResolveStatus) (20002))
+);
+
+DECLARE_REFCOUNTED_STRUCT(IServiceDiscovery)
+
+struct TEndpoint;
+struct TEndpointSet;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery
diff --git a/yt/yt/core/service_discovery/service_discovery.cpp b/yt/yt/core/service_discovery/service_discovery.cpp
new file mode 100644
index 0000000000..342ccba43f
--- /dev/null
+++ b/yt/yt/core/service_discovery/service_discovery.cpp
@@ -0,0 +1,9 @@
+#include "service_discovery.h"
+
+namespace NYT::NServiceDiscovery {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery
diff --git a/yt/yt/core/service_discovery/service_discovery.h b/yt/yt/core/service_discovery/service_discovery.h
new file mode 100644
index 0000000000..0eee74c1ae
--- /dev/null
+++ b/yt/yt/core/service_discovery/service_discovery.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NServiceDiscovery {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TEndpoint
+{
+ TString Id;
+ TString Protocol;
+ TString Fqdn;
+ TString IP4Address;
+ TString IP6Address;
+ int Port;
+
+ //! Identifies whether this endpoint is ready to serve traffic according to the provider.
+ /*!
+ * Must not be used in runtime systems due to the following:
+ * - provider downtime (which is considered as a normal state by design) causes flag staleness;
+ * - difference in a network connectivity of (client <> endpoint) and (provider <> endpoint) makes flag useless for the client.
+ *
+ * Better use client-specific probes to identify ready and alive endpoints.
+ *
+ * See https://st.yandex-team.ru/YT-16705 for details.
+ */
+ bool Ready;
+};
+
+struct TEndpointSet
+{
+ TString Id;
+
+ std::vector<TEndpoint> Endpoints;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IServiceDiscovery
+ : public virtual TRefCounted
+{
+ virtual TFuture<TEndpointSet> ResolveEndpoints(
+ const TString& cluster,
+ const TString& endpointSetId) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IServiceDiscovery)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery
diff --git a/yt/yt/core/service_discovery/yp/config.cpp b/yt/yt/core/service_discovery/yp/config.cpp
new file mode 100644
index 0000000000..9735dfc13a
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/config.cpp
@@ -0,0 +1,35 @@
+#include "config.h"
+
+namespace NYT::NServiceDiscovery::NYP {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TServiceDiscoveryConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable", &TThis::Enable)
+ .Default(true);
+
+ registrar.Parameter("fqdn", &TThis::Fqdn)
+ .Default("sd.yandex.net");
+ registrar.Parameter("grpc_port", &TThis::GrpcPort)
+ .Default(8081);
+
+ registrar.Parameter("client", &TThis::Client)
+ .Default("yt")
+ .NonEmpty();
+
+ registrar.Preprocessor([] (TThis* config) {
+ config->RetryBackoffTime = TDuration::Seconds(1);
+ config->RetryAttempts = 5;
+ config->RetryTimeout = TDuration::Seconds(10);
+
+ config->ExpireAfterAccessTime = TDuration::Days(1);
+ config->ExpireAfterSuccessfulUpdateTime = TDuration::Days(1);
+ config->ExpireAfterFailedUpdateTime = TDuration::Seconds(5);
+ config->RefreshTime = TDuration::Seconds(5);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery::NYP
diff --git a/yt/yt/core/service_discovery/yp/config.h b/yt/yt/core/service_discovery/yp/config.h
new file mode 100644
index 0000000000..dc820cb21e
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/config.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/cache_config.h>
+
+#include <yt/yt/core/rpc/config.h>
+
+namespace NYT::NServiceDiscovery::NYP {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceDiscoveryConfig
+ : public NRpc::TRetryingChannelConfig
+ , public TAsyncExpiringCacheConfig
+{
+public:
+ bool Enable;
+
+ //! Provider endpoint.
+ TString Fqdn;
+ int GrpcPort;
+
+ //! Provider throttles requests based on this string.
+ TString Client;
+
+ REGISTER_YSON_STRUCT(TServiceDiscoveryConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceDiscoveryConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery::NYP
diff --git a/yt/yt/core/service_discovery/yp/public.h b/yt/yt/core/service_discovery/yp/public.h
new file mode 100644
index 0000000000..53d4de18d8
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/public.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NServiceDiscovery::NYP {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TServiceDiscoveryConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery::NYP
diff --git a/yt/yt/core/service_discovery/yp/service_discovery.h b/yt/yt/core/service_discovery/yp/service_discovery.h
new file mode 100644
index 0000000000..d2e4338f4a
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/service_discovery.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/service_discovery/service_discovery.h>
+
+namespace NYT::NServiceDiscovery::NYP {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! https://wiki.yandex-team.ru/yp/discovery/usage/
+/*!
+ * Returns null if YPSD is explicitly disabled by #TServiceDiscoveryConfig::Enable.
+ *
+ * Default caching policy is as follows:
+ * - Hold erroneous result for several seconds not to create pressure on the provider.
+ * - Evict results which are inaccessed for a long period of time (days).
+ * - Update successful results in the background with a period of several seconds.
+ *
+ * NB: Stale successful discovery result is always preferred to the most
+ * actual erroneous one.
+ */
+IServiceDiscoveryPtr CreateServiceDiscovery(TServiceDiscoveryConfigPtr config);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery::NYP
diff --git a/yt/yt/core/service_discovery/yp/service_discovery_dummy.cpp b/yt/yt/core/service_discovery/yp/service_discovery_dummy.cpp
new file mode 100644
index 0000000000..aa41463e7b
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/service_discovery_dummy.cpp
@@ -0,0 +1,20 @@
+#include "service_discovery.h"
+
+#include "config.h"
+
+namespace NYT::NServiceDiscovery::NYP {
+
+////////////////////////////////////////////////////////////////////////////////
+
+IServiceDiscoveryPtr CreateServiceDiscovery(TServiceDiscoveryConfigPtr config)
+{
+ if (!config->Enable) {
+ return nullptr;
+ }
+
+ Y_UNREACHABLE();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NServiceDiscovery::NYP
diff --git a/yt/yt/core/service_discovery/yp/ya.make b/yt/yt/core/service_discovery/yp/ya.make
new file mode 100644
index 0000000000..c6033a6ed3
--- /dev/null
+++ b/yt/yt/core/service_discovery/yp/ya.make
@@ -0,0 +1,33 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/rpc/grpc
+)
+
+SRCS(
+ config.cpp
+)
+
+IF (NOT OPENSOURCE)
+ PEERDIR(
+ infra/yp_service_discovery/api
+ )
+ SRCS(
+ service_discovery.cpp
+ )
+ELSE()
+ SRCS(
+ service_discovery_dummy.cpp
+ )
+ENDIF()
+
+END()
+
+IF (NOT OPENSOURCE)
+ RECURSE_FOR_TESTS(
+ unittests
+ )
+ENDIF()
diff --git a/yt/yt/core/test_framework/fixed_growth_string_output.cpp b/yt/yt/core/test_framework/fixed_growth_string_output.cpp
new file mode 100644
index 0000000000..e2ea8deb5a
--- /dev/null
+++ b/yt/yt/core/test_framework/fixed_growth_string_output.cpp
@@ -0,0 +1,26 @@
+#include "fixed_growth_string_output.h"
+
+#include "framework.h"
+
+namespace NYT {
+
+TFixedGrowthStringOutput::TFixedGrowthStringOutput(TString* s, size_t growthSize) noexcept
+ : String_(s)
+ , GrowthSize_(growthSize)
+{ }
+
+size_t TFixedGrowthStringOutput::DoNext(void** ptr)
+{
+ auto previousSize = String_->size();
+ String_->resize(String_->size() + GrowthSize_);
+ *ptr = String_->begin() + previousSize;
+ return String_->size() - previousSize;
+}
+
+void TFixedGrowthStringOutput::DoUndo(size_t len)
+{
+ ASSERT_LE(len, String_->size());
+ String_->resize(String_->size() - len);
+}
+
+} // namespace NYT \ No newline at end of file
diff --git a/yt/yt/core/test_framework/fixed_growth_string_output.h b/yt/yt/core/test_framework/fixed_growth_string_output.h
new file mode 100644
index 0000000000..5ceb9ecc0d
--- /dev/null
+++ b/yt/yt/core/test_framework/fixed_growth_string_output.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <util/generic/string.h>
+
+#include <util/stream/zerocopy_output.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This class grows its buffer by chunks of fixed length, thus appending one char is O(n) amortized.
+//
+// NB: Very slow class! Use only for tests.
+class TFixedGrowthStringOutput
+ : public IZeroCopyOutput
+{
+public:
+ TFixedGrowthStringOutput(TString* s, size_t growthSize) noexcept;
+
+private:
+ size_t DoNext(void** ptr) override;
+ void DoUndo(size_t len) override;
+
+private:
+ TString* const String_;
+ const size_t GrowthSize_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT \ No newline at end of file
diff --git a/yt/yt/core/test_framework/framework-inl.h b/yt/yt/core/test_framework/framework-inl.h
new file mode 100644
index 0000000000..a7ebb26915
--- /dev/null
+++ b/yt/yt/core/test_framework/framework-inl.h
@@ -0,0 +1,33 @@
+#ifndef FRAMEWORK_INL_H_
+#error "Direct inclusion of this file is not allowed, include framework.h"
+// For the sake of sane code completion.
+#include "framework.h"
+#endif
+
+#include <yt/yt/core/misc/common.h>
+
+#include <util/system/type_name.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void PrintTo(const TIntrusivePtr<T>& arg, std::ostream* os)
+{
+ *os << "TIntrusivePtr<"
+ << TypeName<T>()
+ << ">@0x"
+ << std::hex
+ << reinterpret_cast<uintptr_t>(arg.Get())
+ << std::dec
+ << " [";
+ if (arg) {
+ ::testing::internal::UniversalPrinter<T>::Print(*arg, os);
+ }
+ *os << "]";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/test_framework/framework.cpp b/yt/yt/core/test_framework/framework.cpp
new file mode 100644
index 0000000000..7ae48b9f68
--- /dev/null
+++ b/yt/yt/core/test_framework/framework.cpp
@@ -0,0 +1,168 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <yt/yt/core/concurrency/action_queue.h>
+#include <yt/yt/core/concurrency/delayed_executor.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/logging/config.h>
+#include <yt/yt/core/logging/log_manager.h>
+
+#include <yt/yt/core/misc/crash_handler.h>
+#include <yt/yt/core/misc/hazard_ptr.h>
+#include <yt/yt/core/misc/signal_registry.h>
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <yt/yt/library/profiling/solomon/registry.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+#include <library/cpp/testing/hook/hook.h>
+#include <library/cpp/testing/common/env.h>
+
+#include <util/system/fs.h>
+#include <util/system/env.h>
+
+#include <util/random/random.h>
+
+#include <util/string/vector.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GenerateRandomFileName(const char* prefix)
+{
+ return Format("%s-%016" PRIx64 "-%016" PRIx64,
+ prefix,
+ MicroSeconds(),
+ RandomNumber<ui64>());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WaitForPredicate(
+ std::function<bool()> predicate,
+ TWaitForPredicateOptions options)
+{
+ for (int iteration = 0; iteration < options.IterationCount; ++iteration) {
+ if (iteration > 0) {
+ NConcurrency::TDelayedExecutor::WaitForDuration(options.Period);
+ }
+ try {
+ if (predicate()) {
+ return;
+ }
+ } catch (...) {
+ if (!options.IgnoreExceptions || iteration + 1 == options.IterationCount) {
+ throw;
+ }
+ }
+ }
+ THROW_ERROR_EXCEPTION("Wait failed");
+}
+
+void WaitForPredicate(
+ std::function<bool()> predicate,
+ int iterationCount,
+ TDuration period)
+{
+ WaitForPredicate(
+ std::move(predicate),
+ TWaitForPredicateOptions{
+ .IterationCount = iterationCount,
+ .Period = period,
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+namespace testing {
+
+using namespace NYT;
+using namespace NYT::NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RunAndTrackFiber(TClosure closure)
+{
+ auto queue = New<TActionQueue>("Main");
+ auto invoker = queue->GetInvoker();
+
+ auto result = BIND([invoker, closure] () mutable {
+ // NB: Make sure TActionQueue does not keep a strong reference to this fiber by forcing a yield.
+ SwitchTo(invoker);
+
+ closure();
+ })
+ .AsyncVia(invoker)
+ .Run();
+
+ auto startedAt = TInstant::Now();
+ while (!result.IsSet()) {
+ if (TInstant::Now() - startedAt > TDuration::Seconds(300)) {
+ GTEST_FAIL() << "Probably stuck.";
+ break;
+ }
+ Sleep(TDuration::MilliSeconds(10));
+ }
+
+ queue->Shutdown();
+
+ // Do not silence errors thrown in tests.
+ if (result.IsSet()) {
+ result.Get().ThrowOnError();
+ }
+
+ SUCCEED();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace testing
+
+class TYTEnvironment
+ : public ::testing::Environment
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_TEST_HOOK_BEFORE_RUN(GTEST_YT_SETUP)
+{
+#ifdef _unix_
+ ::signal(SIGPIPE, SIG_IGN);
+#endif
+ NYT::EnableShutdownLoggingToFile((GetOutputPath() / "shutdown.log").GetPath());
+#ifdef _unix_
+ NYT::TSignalRegistry::Get()->PushCallback(NYT::AllCrashSignals, NYT::CrashSignalHandler);
+ NYT::TSignalRegistry::Get()->PushDefaultSignalHandler(NYT::AllCrashSignals);
+#endif
+
+ auto config = NYT::NLogging::TLogManagerConfig::CreateYTServer("unittester", GetOutputPath().GetPath());
+ NYT::NLogging::TLogManager::Get()->Configure(config);
+ NYT::NLogging::TLogManager::Get()->EnableReopenOnSighup();
+
+ NYT::NProfiling::TSolomonRegistry::Get()->Disable();
+
+ ::testing::AddGlobalTestEnvironment(new TYTEnvironment());
+
+ // TODO(ignat): support ram_drive_path when this feature would be supported in gtest machinery.
+ auto testSandboxPath = GetEnv("TESTS_SANDBOX");
+ if (!testSandboxPath.empty()) {
+ NFs::SetCurrentWorkingDirectory(testSandboxPath);
+ }
+}
+
+Y_TEST_HOOK_AFTER_RUN(GTEST_YT_TEARDOWN)
+{
+#ifdef _asan_enabled_
+ // Wait for some time to ensure background cleanup is somewhat complete.
+ Sleep(TDuration::Seconds(1));
+ NYT::ReclaimHazardPointers();
+ NYT::TRefCountedTrackerFacade::Dump();
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/test_framework/framework.h b/yt/yt/core/test_framework/framework.h
new file mode 100644
index 0000000000..61c8f1093d
--- /dev/null
+++ b/yt/yt/core/test_framework/framework.h
@@ -0,0 +1,228 @@
+#pragma once
+
+#include <yt/yt/core/actions/public.h>
+
+#include <library/cpp/yt/misc/preprocessor.h>
+
+// Include Google Test and Google Mock headers.
+#define GTEST_DONT_DEFINE_FAIL 1
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A tiny helper function to generate random file names.
+TString GenerateRandomFileName(const char* prefix);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB. EXPECT_THROW_* are macros not functions so when failure occurres
+// gtest framework points to source code of test not the source code
+// of EXPECT_THROW_* function.
+#define EXPECT_THROW_THAT(expr, matcher) \
+ do { \
+ try { \
+ expr; \
+ ADD_FAILURE() << "Expected exception to be thrown"; \
+ } catch (const std::exception& ex) { \
+ EXPECT_THAT(ex.what(), matcher); \
+ } \
+ } while (0)
+
+#define EXPECT_THROW_WITH_SUBSTRING(expr, exceptionSubstring) \
+ EXPECT_THROW_THAT(expr, testing::HasSubstr(exceptionSubstring))
+
+#define EXPECT_THROW_WITH_ERROR_CODE(expr, code) \
+ do { \
+ try { \
+ expr; \
+ ADD_FAILURE() << "Expected exception to be thrown"; \
+ } catch (const TErrorException& ex) { \
+ EXPECT_TRUE(ex.Error().FindMatching(code).has_value()); \
+ } \
+ } while (0)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TWaitForPredicateOptions
+{
+ int IterationCount = 300;
+ TDuration Period = TDuration::MilliSeconds(100);
+ bool IgnoreExceptions = false;
+};
+
+void WaitForPredicate(
+ std::function<bool()> predicate,
+ TWaitForPredicateOptions options);
+
+void WaitForPredicate(
+ std::function<bool()> predicate,
+ int iterationCount = 300,
+ TDuration period = TDuration::MilliSeconds(100));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+namespace NYT::NRpc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define MOCK_RPC_SERVICE_METHOD(ns, method) \
+ DEFINE_RPC_SERVICE_METHOD_THUNK(ns, method) \
+ MOCK_METHOD(void, method, (TReq##method*, TRsp##method*, TCtx##method##Ptr))
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TPredicateMatcher
+ : public ::testing::MatcherInterface<T>
+{
+public:
+ TPredicateMatcher(
+ std::function<bool(T)> predicate,
+ const char* description)
+ : Predicate_(predicate)
+ , Description_(description)
+ { }
+
+ bool MatchAndExplain(T value, ::testing::MatchResultListener* /*listener*/) const override
+ {
+ return Predicate_(value);
+ }
+
+ void DescribeTo(std::ostream* os) const override
+ {
+ *os << Description_;
+ }
+
+private:
+ std::function<bool(T)> Predicate_;
+ const char* Description_;
+
+};
+
+template <class T>
+::testing::Matcher<T> MakePredicateMatcher(
+ std::function<bool(T)> predicate,
+ const char* description)
+{
+ return ::testing::MakeMatcher(new TPredicateMatcher<T>(predicate, description));
+}
+
+#define MAKE_PREDICATE_MATCHER(type, arg, capture, predicate) \
+ MakePredicateMatcher<type>( \
+ PP_DEPAREN(capture) (type arg) { return (predicate); }, \
+ #predicate \
+ )
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define RPC_MOCK_CALL(mock, method) \
+ method( \
+ ::testing::_, \
+ ::testing::_, \
+ ::testing::_)
+
+#define RPC_MOCK_CALL_WITH_PREDICATE(mock, method, capture, predicate) \
+ method( \
+ MAKE_PREDICATE_MATCHER(mock::TReq##method*, request, capture, predicate), \
+ ::testing::_, \
+ ::testing::_)
+
+#define EXPECT_CALL_WITH_MESSAGE_IMPL(obj, call, message) \
+ ((obj).gmock_##call).InternalExpectedAt(__FILE__, __LINE__, #obj, message)
+
+#define EXPECT_CALL_WITH_MESSAGE(obj, call, message) \
+ EXPECT_CALL_WITH_MESSAGE_IMPL(obj, call, message)
+
+#define EXPECT_RPC_CALL(mock, method) \
+ EXPECT_CALL_WITH_MESSAGE( \
+ mock, \
+ RPC_MOCK_CALL(mock, method), \
+ #method \
+ )
+
+#define EXPECT_RPC_CALL_WITH_PREDICATE(mock, method, capture, predicate) \
+ EXPECT_CALL_WITH_MESSAGE( \
+ mock, \
+ RPC_MOCK_CALL_WITH_PREDICATE(mock, method, capture, predicate), \
+ #method "(" #predicate ")" \
+ )
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define ON_CALL_WITH_MESSAGE_IMPL(obj, call, message) \
+ ((obj).gmock_##call).InternalDefaultActionSetAt(__FILE__, __LINE__, #obj, message)
+
+#define ON_CALL_WITH_MESSAGE(obj, call, message) \
+ ON_CALL_WITH_MESSAGE_IMPL(obj, call, message)
+
+#define ON_RPC_CALL(mock, method) \
+ ON_CALL_WITH_MESSAGE( \
+ mock, \
+ RPC_MOCK_CALL(mock, method), \
+ #method \
+ )
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define HANDLE_RPC_CALL(mockType, method, capture, body) \
+ ::testing::Invoke(PP_DEPAREN(capture) ( \
+ [[maybe_unused]] mockType::TReq##method* request, \
+ [[maybe_unused]] mockType::TRsp##method* response, \
+ [[maybe_unused]] mockType::TCtx##method##Ptr context) \
+ body)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NRpc
+
+namespace testing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RunAndTrackFiber(NYT::TClosure closure);
+
+// Wraps tests in an extra fiber and awaits termination. Adapted from `gtest.h`.
+#define TEST_W_(test_case_name, test_name, parent_class, parent_id)\
+class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
+ public:\
+ GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
+ GTEST_TEST_CLASS_NAME_(test_case_name, test_name)(const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)&) = delete;\
+ GTEST_TEST_CLASS_NAME_(test_case_name, test_name)& operator= (const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)&) = delete;\
+ private:\
+ virtual void TestBody();\
+ void TestInnerBody();\
+ static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;\
+};\
+\
+::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
+ ::test_info_ =\
+ ::testing::internal::MakeAndRegisterTestInfo(\
+ #test_case_name, #test_name, nullptr, nullptr, \
+ ::testing::internal::CodeLocation(__FILE__, __LINE__), \
+ (parent_id), \
+ parent_class::SetUpTestCase, \
+ parent_class::TearDownTestCase, \
+ new ::testing::internal::TestFactoryImpl<\
+ GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
+void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody() {\
+ ::testing::RunAndTrackFiber(BIND(\
+ &GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestInnerBody,\
+ this));\
+}\
+void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestInnerBody()
+#define TEST_W(test_fixture, test_name)\
+ TEST_W_(test_fixture, test_name, test_fixture, \
+ ::testing::internal::GetTypeId<test_fixture>())
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace testing
+
+#define FRAMEWORK_INL_H_
+#include "framework-inl.h"
+#undef FRAMEWORK_INL_H_
diff --git a/yt/yt/core/test_framework/mock_http.cpp b/yt/yt/core/test_framework/mock_http.cpp
new file mode 100644
index 0000000000..166afb8c97
--- /dev/null
+++ b/yt/yt/core/test_framework/mock_http.cpp
@@ -0,0 +1,183 @@
+#include "mock_http.h"
+
+#include <yt/yt/core/http/config.h>
+#include <yt/yt/core/http/server.h>
+
+#include <util/string/join.h>
+
+namespace NYT::NHttp {
+namespace {
+
+TServerConfigPtr MakeConfig()
+{
+ auto config = New<TServerConfig>();
+ config->Port = 0;
+ return config;
+}
+
+TMockHeaders DumpHeadersSafe(const THeadersPtr& headers)
+{
+ return headers ? headers->Dump() : TMockHeaders();
+}
+
+class THandler
+ : public IHttpHandler
+{
+public:
+ THandler(TMockServer* mockServer)
+ : MockServer_(mockServer)
+ { }
+
+ void HandleRequest(const IRequestPtr& req, const IResponseWriterPtr& rsp) override
+ {
+ TString path = Join('?', req->GetUrl().Path, req->GetUrl().RawQuery);
+ TString body = ToString(req->ReadAll());
+ TMockHeaders headers = req->GetHeaders()->Dump();
+
+ TMockResponse mockRsp;
+ switch (req->GetMethod()) {
+ case EMethod::Delete:
+ mockRsp = MockServer_->Delete(path, headers);
+ break;
+ case EMethod::Get:
+ mockRsp = MockServer_->Get(path, headers);
+ break;
+ case EMethod::Head:
+ mockRsp = MockServer_->Head(path, headers);
+ break;
+ case EMethod::Post:
+ mockRsp = MockServer_->Post(path, body, headers);
+ break;
+ case EMethod::Put:
+ mockRsp = MockServer_->Put(path, body, headers);
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Unsupported");
+ }
+ rsp->SetStatus(mockRsp.StatusCode);
+ for (const auto& [name, value] : mockRsp.Headers) {
+ rsp->GetHeaders()->Add(name, value);
+ }
+ NConcurrency::WaitFor(rsp->WriteBody(TSharedRef::FromString(mockRsp.Body))).ThrowOnError();
+ }
+
+private:
+ TMockServer* MockServer_;
+}; // class THandler
+
+class TMockResponseStream
+ : public IResponse
+{
+public:
+ TMockResponseStream(const TMockResponse& response)
+ : Response_(response)
+ , Headers_(New<THeaders>())
+ , Trailers_(New<THeaders>())
+ { }
+
+ EStatusCode GetStatusCode() override
+ {
+ return Response_.StatusCode;
+ }
+
+ const THeadersPtr& GetHeaders() override
+ {
+ for (const auto& [name, value] : Response_.Headers) {
+ Headers_->Add(name, value);
+ }
+ return Headers_;
+ }
+
+ const THeadersPtr& GetTrailers() override
+ {
+ return Trailers_;
+ }
+
+ TFuture<TSharedRef> Read() override
+ {
+ TSharedRef result;
+ if (!Response_.Body.empty()) {
+ result = TSharedRef::FromString(Response_.Body);
+ Response_.Body.clear();
+ }
+ return MakeFuture(result);
+ }
+
+private:
+ TMockResponse Response_;
+ THeadersPtr Headers_;
+ THeadersPtr Trailers_;
+}; // class TMockResponseStream
+
+}
+
+TMockServer::TMockServer(TServerConfigPtr config)
+ : Server_(CreateServer(config ? std::move(config) : MakeConfig()))
+ , Handler_(New<THandler>(this))
+{
+ Server_->AddHandler("/", Handler_);
+ Server_->Start();
+}
+
+TMockServer::~TMockServer()
+{
+ Server_->Stop();
+}
+
+ui16 TMockServer::GetPort()
+{
+ return Server_->GetAddress().GetPort();
+}
+
+TFuture<IResponsePtr> TMockClient::Get(const TString& url, const THeadersPtr& headers)
+{
+ auto mockRsp = Get(url, DumpHeadersSafe(headers));
+ return MakeFuture<IResponsePtr>(New<TMockResponseStream>(mockRsp));
+}
+
+TFuture<IResponsePtr> TMockClient::Post(
+ const TString& url, const TSharedRef& body, const THeadersPtr& headers)
+{
+ auto mockRsp = Post(url, ToString(body), DumpHeadersSafe(headers));
+ return MakeFuture<IResponsePtr>(New<TMockResponseStream>(mockRsp));
+}
+
+TFuture<IResponsePtr> TMockClient::Patch(
+ const TString& url, const TSharedRef& body, const THeadersPtr& headers)
+{
+ auto mockRsp = Patch(url, ToString(body), DumpHeadersSafe(headers));
+ return MakeFuture<IResponsePtr>(New<TMockResponseStream>(mockRsp));
+}
+
+TFuture<IResponsePtr> TMockClient::Put(
+ const TString& url, const TSharedRef& body, const THeadersPtr& headers)
+{
+ auto mockRsp = Put(url, ToString(body), DumpHeadersSafe(headers));
+ return MakeFuture<IResponsePtr>(New<TMockResponseStream>(mockRsp));
+}
+
+TFuture<IResponsePtr> TMockClient::Delete(const TString& url, const THeadersPtr& headers)
+{
+ auto mockRsp = Delete(url, DumpHeadersSafe(headers));
+ return MakeFuture<IResponsePtr>(New<TMockResponseStream>(mockRsp));
+}
+
+TFuture<IActiveRequestPtr> TMockClient::StartPost(
+ const TString& /*url*/, const THeadersPtr& /*headers*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TFuture<IActiveRequestPtr> TMockClient::StartPatch(
+ const TString& /*url*/, const THeadersPtr& /*headers*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+TFuture<IActiveRequestPtr> TMockClient::StartPut(
+ const TString& /*url*/, const THeadersPtr& /*headers*/)
+{
+ YT_UNIMPLEMENTED();
+}
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/test_framework/mock_http.h b/yt/yt/core/test_framework/mock_http.h
new file mode 100644
index 0000000000..7b6292c640
--- /dev/null
+++ b/yt/yt/core/test_framework/mock_http.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include <yt/yt/core/http/client.h>
+#include <yt/yt/core/http/http.h>
+#include <yt/yt/core/http/public.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NHttp {
+
+using TMockHeaders = std::vector<std::pair<TString, TString>>;
+
+struct TMockResponse
+{
+ EStatusCode StatusCode;
+ TString Body;
+ TMockHeaders Headers;
+}; // class TMockResponse
+
+class TMockServer
+{
+public:
+ TMockServer(TServerConfigPtr config = nullptr);
+
+ ~TMockServer();
+
+ ui16 GetPort();
+
+ // Path includes the ?query, if any.
+ MOCK_METHOD(TMockResponse, Delete, (
+ const TString& path, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Get, (
+ const TString& path, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Head, (
+ const TString& path, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Post, (
+ const TString& path, const TString& body, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Put, (
+ const TString& path, const TString& body, const TMockHeaders& headers));
+
+private:
+ const IServerPtr Server_;
+ const IHttpHandlerPtr Handler_;
+}; // class TMockHttpServer
+
+class TMockClient
+ : public IClient
+{
+public:
+ TFuture<IResponsePtr> Get(
+ const TString& url,
+ const THeadersPtr& headers) override;
+ TFuture<IResponsePtr> Post(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override;
+ TFuture<IResponsePtr> Patch(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override;
+ TFuture<IResponsePtr> Put(
+ const TString& url,
+ const TSharedRef& body,
+ const THeadersPtr& headers) override;
+ TFuture<IResponsePtr> Delete(
+ const TString& url,
+ const THeadersPtr& headers) override;
+
+ TFuture<IActiveRequestPtr> StartPost(
+ const TString& url,
+ const THeadersPtr& headers) override;
+
+ TFuture<IActiveRequestPtr> StartPatch(
+ const TString& url,
+ const THeadersPtr& headers) override;
+
+ TFuture<IActiveRequestPtr> StartPut(
+ const TString& url,
+ const THeadersPtr& headers) override;
+
+ MOCK_METHOD(TMockResponse, Get, (
+ const TString& url, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Post, (
+ const TString& url, const TString& body, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Patch, (
+ const TString& url, const TString& body, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Put, (
+ const TString& url, const TString& body, const TMockHeaders& headers));
+ MOCK_METHOD(TMockResponse, Delete, (
+ const TString& url, const TMockHeaders& headers));
+
+}; // class TMockClient
+
+DECLARE_REFCOUNTED_CLASS(TMockClient)
+DEFINE_REFCOUNTED_TYPE(TMockClient)
+
+} // namespace NYT::NHttp
diff --git a/yt/yt/core/test_framework/test_key.h b/yt/yt/core/test_framework/test_key.h
new file mode 100644
index 0000000000..b26c3e3738
--- /dev/null
+++ b/yt/yt/core/test_framework/test_key.h
@@ -0,0 +1,119 @@
+#pragma once
+
+namespace NYT {
+namespace {
+
+constexpr auto TestCertificate = R"EOF(
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+Z2mi8shZ3T0c
+5ItI4KwLfYFXxNr3dmIY+2DS9boD18T46Ccow3wW/15SCcI1BdD/pPBmOTpUWP0b
+B22l4gAOUIWfk/CAHuaD+pHAGMlolAMdscwDZPdaM3XEdu839y2gy6RL4Pxls8No
+sI5h2BTGl3YgjSxA+vKJE+/IXzjajfiKfGHuywPjwPpGl1juOgSaU6zqLf4MlUnq
+Daq7r5V9KtJh8dz46PB6c8ALGNM+dxMJSLMyDvT1/7d9aYBkBvpb3mxOu9agIDNn
+2AZ3AxYwA0ykBLmc5R6V0toqRIjfruvBHqQfcjsFaoKS6O+QjtA/eBTIzGajnyis
+1TfeJxURAgMBAAECggEARANIlq5GpuMCW3m/zy6CBjC0rRdiaBbff7D7qx+fbJP8
+hjTXGBaMEuLxXDikKLCFMWxHexxiG5MWBjunDSQnhPV6ZcBAnmNrUCWHPqkb+ME2
+Q7so9uVv/cZ4AM/DL6iZoeBcNcaOIf4OhSzcD1NSSIX96i7Dagq57AE1G8v30Qlb
+CDybxrkbW8D9TkPh57oH/VNuhGLsFp62BjleYtNqo+aknlnHCj09mFQ+N5cA8DuK
+0CcNFCy4C8oZvg9kVsfdypBr4IR0kXTArqMyjUgXe9KqOzf/GdHR9anWhOzHsy/b
+T1Nb+vF6fDm0o6WHWhfODoF4iklrdwRibAnme+zegQKBgQDzvt4KQLnG5jWxeCLn
+P+QR9q63H98oL3kKToyXPaJVL+I71GtZm4yhYP8KB+bTgrMHPhR5se63cySX2lMJ
+RRKkieeEFuDVKVHulRH39g9fMvvl/f97qwv2mAJhdNuIaIjLSVFjQ8WZ6UWvJczp
+sTAyhIxDGiOV00HaUp3BFFnEOQKBgQDH+gLGIsLlPAwuMpbSyuDJucMk0Jzo3+at
+6h19pu5JpfTWn71Zs0RL45x9BLwbx8oi+vjECjMiaE2OyKC6uObxpXRl5okWQ63E
+XBpbONB+fx2v2h1cuB7iJCJxJ6DPTL70torWtwCp+I7CcIT+J/2SqPhiKipWo0Sk
+R2dxeb1HmQKBgEU8mmXfLOZKzkWzEncNtwNDRy3NZ95KXd+HoHf1kf8Qsvq7xCKY
+BMJygv+ebvr1zVTpVXecC2sg0ewwoBWqATmr0o+6z/K84gEbZxdAVe181gDmvYOr
+eqJ5W3PDdfixeOoF0ZCY17B4isrNuf9HzaEL9au56RHOCI6zmQwXc8hBAoGBAMaI
+h0h+Kk+7FbynrOUJVbHwIrTiB2WLJFF1JGIi4F9ty21omWv8dcmB51KW6MoLx7qC
+v4ahObLnKlifBjNabq1pPe4MufzIpDNV3TTDavqq6KY1PQFYKhEJHsiINzaXUt1Q
+fPY+KQKWKeUQIHjS6wQ3jKCoi/AHl5Yg7anS2v/BAoGBAIi309nwDFJG/2UFSObA
+WC+V6T7qy62UlwFlBwsFCbxf9FmFQfoP6wwbQef35Wx2aDnZaSzoXKn/1jvG5e1e
+TFW7K9oC8JkeA//mnTAVrgkvkaHGZmd27zQYB1U3DsO3fLvEt62PZn8fyEwaczeM
+vOOkHciP4pIhAObg/uiO0V9I
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAM9HYUREwVxFMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkNOMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTQxMTAxMDY1OTE2WhcNMjQxMDI5MDY1OTE2WjBF
+MQswCQYDVQQGEwJDTjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAvmdpovLIWd09HOSLSOCsC32BV8Ta93ZiGPtg0vW6A9fE+OgnKMN8Fv9e
+UgnCNQXQ/6TwZjk6VFj9GwdtpeIADlCFn5PwgB7mg/qRwBjJaJQDHbHMA2T3WjN1
+xHbvN/ctoMukS+D8ZbPDaLCOYdgUxpd2II0sQPryiRPvyF842o34inxh7ssD48D6
+RpdY7joEmlOs6i3+DJVJ6g2qu6+VfSrSYfHc+OjwenPACxjTPncTCUizMg709f+3
+fWmAZAb6W95sTrvWoCAzZ9gGdwMWMANMpAS5nOUeldLaKkSI367rwR6kH3I7BWqC
+kujvkI7QP3gUyMxmo58orNU33icVEQIDAQABo1AwTjAdBgNVHQ4EFgQUJS1vm+Z3
+dm8z29qqdzeI94ZmoqwwHwYDVR0jBBgwFoAUJS1vm+Z3dm8z29qqdzeI94Zmoqww
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAl/6TWgvtFCcxue+YmpLz
+gcPckabL2dbgC7uxbIMgFEJUjtmHRpY1Tih8pKqqbdkPWhK2IBvyCqp7L1P5A4ib
+FTKRGogJSaWMjnh/w644yrmsjjo5uoAueqygwha+OAC3gtt6p844hb9KJTjaoMHC
+caaZ6jCAnfjAp2O/3bBpgXCy69UNlWizx8aXajn5a9ah/DrY8wZfI+ESRH3oMd/f
+hecgZLhdTPSkUJi/l6WK9wBuI8mVl+/Gesi8zgz8u+/BRZsxQoP9tBWUjOG396fm
+PCpapHzlchV1N1s0k+poxmoO/GI0GTPcIY3RhU6QJIQ0dtGCLZFVWchJms5u9GBg
+xw==
+-----END CERTIFICATE-----
+)EOF";
+
+constexpr auto TestCertificateChain = R"EOF(
+-----BEGIN CERTIFICATE-----
+MIIFAzCCA+ugAwIBAgISBLicszxolgnl1s6kGcTJNkfHMA0GCSqGSIb3DQEBCwUA
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODAyMTIxOTI3MjJaFw0x
+ODA1MTMxOTI3MjJaMBsxGTAXBgNVBAMTEGNwcC5tYW55dGFzay5vcmcwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnVsRLbwms7twqMfR+6MR2x9tPwhcj
+Ux6j3mLEdmd0+1YZBKT8ydTavXKCcbS4e9IW+Gh0fyQcyTuW57lW/saKPJW1jPaE
+K4rIt+NjE9GDb3a4skEyFXmTonNn67w/xvQZs3KcyxjAgyjPbtnVeteGnD/30Xku
+YIGC8ZIqD4s6z6VnTuKV+nObXhyXmWVvNEi5QHXj723jgdfrF1E4FJA5rBaX0yQ8
+7vgwMYr6+w5f+6g7IgYEHsR6vodNGIMLp5h+hhFdslyudxNxA+QiYdWGlJdN2sDZ
+IUMVZJEatn+V/x6ErVfVaGkQRti4FO6+vt6mO5VdrM2Ew0rClWaBDh6jAgMBAAGj
+ggIQMIICDDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
+AQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFJWMeys3nYIBTHo1CLw/74WX
+SWioMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEB
+BGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0
+Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0
+Lm9yZy8wGwYDVR0RBBQwEoIQY3BwLm1hbnl0YXNrLm9yZzCB/gYDVR0gBIH2MIHz
+MAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6
+Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2Vy
+dGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0
+aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUg
+UG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRv
+cnkvMA0GCSqGSIb3DQEBCwUAA4IBAQCMQSyJB7MiP0rcaBQtN8EoaDwHGMW6dync
+/fjIHXJa8zPhubrGGOPBhNSALrbjfMwLnw+g5lsPzG+Wg8/UiS1uITyKMhJXGQxy
+chdEYiyh3FVIi2tyjGlS6ozUmqANuRDdVEkGchzfKoPtQJbvsT/5LgBX33GgbkPf
+ZYP3+qSnErXYyP007jM3tYl+Zl9oYVxoGSOGtSEnrO7cxkv95I/AuVkIXi8y9Uk/
+qMsJ2qPTEspjA1+GP6u2DRHnuLJUbY8TfB/m5ObgxTbfKqQfuTNISfcIxuAnhaps
+s/R/p7FsgJwed0WSpPqd5JyNxRctGPIuRizgvWQs/5bxvIAWaTif
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
+SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
+GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
+q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
+SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
+Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
+a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
+/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
+AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
+CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
+bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
+c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
+VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
+ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
+MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
+Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
+AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
+uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
+wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
+X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
+PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
+KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
+-----END CERTIFICATE-----
+)EOF";
+
+
+} // namespace
+} // namespace NYT
diff --git a/yt/yt/core/test_framework/ya.make b/yt/yt/core/test_framework/ya.make
new file mode 100644
index 0000000000..144ad3111a
--- /dev/null
+++ b/yt/yt/core/test_framework/ya.make
@@ -0,0 +1,20 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+SRCS(
+ fixed_growth_string_output.cpp
+ GLOBAL framework.cpp
+ mock_http.cpp
+)
+
+PEERDIR(
+ library/cpp/testing/gtest
+ library/cpp/testing/hook
+ yt/yt/build
+ yt/yt/core
+ yt/yt/core/http
+ yt/yt/library/profiling/solomon
+)
+
+END()
diff --git a/yt/yt/core/test_framework/yson_consumer_mock.h b/yt/yt/core/test_framework/yson_consumer_mock.h
new file mode 100644
index 0000000000..b4d07e6219
--- /dev/null
+++ b/yt/yt/core/test_framework/yson_consumer_mock.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMockYsonConsumer
+ : public TYsonConsumerBase
+{
+public:
+ MOCK_METHOD(void, OnStringScalar, (TStringBuf value), (override));
+ MOCK_METHOD(void, OnInt64Scalar, (i64 value), (override));
+ MOCK_METHOD(void, OnUint64Scalar, (ui64 value), (override));
+ MOCK_METHOD(void, OnDoubleScalar, (double value), (override));
+ MOCK_METHOD(void, OnBooleanScalar, (bool value), (override));
+ MOCK_METHOD(void, OnEntity, (), (override));
+
+ MOCK_METHOD(void, OnBeginList, (), (override));
+ MOCK_METHOD(void, OnListItem, (), (override));
+ MOCK_METHOD(void, OnEndList, (), (override));
+
+ MOCK_METHOD(void, OnBeginMap, (), (override));
+ MOCK_METHOD(void, OnKeyedItem, (TStringBuf name), (override));
+ MOCK_METHOD(void, OnEndMap, (), (override));
+
+ MOCK_METHOD(void, OnBeginAttributes, (), (override));
+ MOCK_METHOD(void, OnEndAttributes, (), (override));
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/threading/private.h b/yt/yt/core/threading/private.h
new file mode 100644
index 0000000000..c4a7e7e148
--- /dev/null
+++ b/yt/yt/core/threading/private.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger ThreadingLogger("Threading");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/public.h b/yt/yt/core/threading/public.h
new file mode 100644
index 0000000000..eb91f06efc
--- /dev/null
+++ b/yt/yt/core/threading/public.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TThreadId = size_t;
+constexpr size_t InvalidThreadId = 0;
+
+DEFINE_ENUM(EThreadPriority,
+ (Normal)
+ (RealTime)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/spin_wait_slow_path_logger.cpp b/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
new file mode 100644
index 0000000000..492a6f4f5c
--- /dev/null
+++ b/yt/yt/core/threading/spin_wait_slow_path_logger.cpp
@@ -0,0 +1,47 @@
+#include "spin_wait_slow_path_logger.h"
+#include "private.h"
+
+#include <library/cpp/yt/cpu_clock/clock.h>
+
+#include <library/cpp/yt/threading/spin_wait_hook.h>
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+std::atomic<bool> SpinLockSlowPathLoggingHookRegistered;
+std::atomic<TCpuDuration> SpinWaitSlowPathLoggingThreshold;
+
+void SpinWaitSlowPathLoggingHook(
+ TCpuDuration cpuDelay,
+ const ::TSourceLocation& location,
+ ESpinLockActivityKind activityKind) noexcept
+{
+ Y_UNUSED(cpuDelay);
+ Y_UNUSED(location);
+ Y_UNUSED(activityKind);
+ // XXX(babenko): currently broken
+ // if (cpuDelay >= SpinWaitSlowPathLoggingThreshold) {
+ // const auto& Logger = ThreadingLogger;
+ // YT_LOG_DEBUG("Spin wait took too long (SourceLocation: %, ActivityKind: %v, Delay: %v)",
+ // location.File ? Format("%v:%v", location.File, location.Line) : "<unknown>",
+ // activityKind,
+ // CpuDurationToDuration(cpuDelay));
+ // }
+}
+
+} // namespace
+
+void SetSpinWaitSlowPathLoggingThreshold(TDuration threshold)
+{
+ SpinWaitSlowPathLoggingThreshold.store(DurationToCpuDuration(threshold));
+ if (!SpinLockSlowPathLoggingHookRegistered.exchange(true)) {
+ RegisterSpinWaitSlowPathHook(&SpinWaitSlowPathLoggingHook);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/spin_wait_slow_path_logger.h b/yt/yt/core/threading/spin_wait_slow_path_logger.h
new file mode 100644
index 0000000000..a36c9615e2
--- /dev/null
+++ b/yt/yt/core/threading/spin_wait_slow_path_logger.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <util/datetime/base.h>
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetSpinWaitSlowPathLoggingThreshold(TDuration threshold);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/thread-inl.h b/yt/yt/core/threading/thread-inl.h
new file mode 100644
index 0000000000..8f4b24393b
--- /dev/null
+++ b/yt/yt/core/threading/thread-inl.h
@@ -0,0 +1,36 @@
+#ifndef THREAD_INL_H_
+#error "Direct inclusion of this file is not allowed, include thread.h"
+// For the sake of sane code completion.
+#include "thread.h"
+#endif
+#undef THREAD_INL_H_
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool TThread::Start()
+{
+ if (Y_LIKELY(Started_.load(std::memory_order::relaxed))) {
+ if (Y_UNLIKELY(Stopping_.load(std::memory_order::relaxed))) {
+ return false;
+ }
+ return true;
+ }
+
+ return StartSlow();
+}
+
+inline bool TThread::IsStarted() const
+{
+ return Started_.load(std::memory_order::relaxed);
+}
+
+inline bool TThread::IsStopping() const
+{
+ return Stopping_.load(std::memory_order::relaxed);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/thread.cpp b/yt/yt/core/threading/thread.cpp
new file mode 100644
index 0000000000..59a0b66f4d
--- /dev/null
+++ b/yt/yt/core/threading/thread.cpp
@@ -0,0 +1,268 @@
+#include "thread.h"
+
+#include "private.h"
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <yt/yt/core/misc/proc.h>
+
+#ifdef _linux_
+ #include <sched.h>
+#endif
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static thread_local TThreadId CurrentUniqueThreadId;
+static std::atomic<TThreadId> UniqueThreadIdGenerator;
+
+static const auto& Logger = ThreadingLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TThread::TThread(
+ TString threadName,
+ EThreadPriority threadPriority,
+ int shutdownPriority)
+ : ThreadName_(std::move(threadName))
+ , ThreadPriority_(threadPriority)
+ , ShutdownPriority_(shutdownPriority)
+ , UniqueThreadId_(++UniqueThreadIdGenerator)
+ , UnderlyingThread_(&StaticThreadMainTrampoline, this)
+{ }
+
+TThread::~TThread()
+{
+ Stop();
+}
+
+TThreadId TThread::GetThreadId() const
+{
+ return ThreadId_;
+}
+
+TString TThread::GetThreadName() const
+{
+ return ThreadName_;
+}
+
+bool TThread::StartSlow()
+{
+ auto guard = Guard(SpinLock_);
+
+ if (Started_.load()) {
+ return !Stopping_.load();
+ }
+
+ if (Stopping_.load()) {
+ // Stopped without being started.
+ return false;
+ }
+
+ ShutdownCookie_ = RegisterShutdownCallback(
+ Format("Thread(%v)", ThreadName_),
+ BIND_NO_PROPAGATE(&TThread::Stop, MakeWeak(this)),
+ ShutdownPriority_);
+ if (!ShutdownCookie_) {
+ Stopping_ = true;
+ return false;
+ }
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Starting thread (ThreadName: %s)\n",
+ ThreadName_.c_str());
+ }
+
+ StartPrologue();
+
+ try {
+ UnderlyingThread_.Start();
+ } catch (const std::exception& ex) {
+ fprintf(stderr, "*** Error starting thread (ThreadName: %s)\n*** %s\n",
+ ThreadName_.c_str(),
+ ex.what());
+ YT_ABORT();
+ }
+
+ Started_ = true;
+
+ StartedEvent_.Wait();
+
+ StartEpilogue();
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Thread started (ThreadName: %s, ThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_);
+ }
+
+ return true;
+}
+
+bool TThread::CanWaitForThreadShutdown() const
+{
+ return
+ CurrentUniqueThreadId != UniqueThreadId_ &&
+ GetShutdownThreadId() != ThreadId_;
+}
+
+void TThread::Stop()
+{
+ {
+ auto guard = Guard(SpinLock_);
+ auto alreadyStopping = Stopping_.exchange(true);
+ if (!Started_) {
+ return;
+ }
+ if (alreadyStopping) {
+ guard.Release();
+ // Avoid deadlock.
+ if (CanWaitForThreadShutdown()) {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Waiting for an already stopping thread to finish (ThreadName: %s, ThreadId: %" PRISZT ", WaiterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+ StoppedEvent_.Wait();
+ } else {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Cannot wait for an already stopping thread to finish (ThreadName: %s, ThreadId: %" PRISZT ", WaiterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+ }
+ return;
+ }
+ }
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Stopping thread (ThreadName: %s, ThreadId: %" PRISZT ", RequesterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+
+ StopPrologue();
+
+ // Avoid deadlock.
+ if (CanWaitForThreadShutdown()) {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Waiting for thread to stop (ThreadName: %s, ThreadId: %" PRISZT ", RequesterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+ UnderlyingThread_.Join();
+ } else {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Cannot wait for thread to stop; detaching (ThreadName: %s, ThreadId: %" PRISZT ", RequesterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+ UnderlyingThread_.Detach();
+ }
+
+ StopEpilogue();
+
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "*** Thread stopped (ThreadName: %s, ThreadId: %" PRISZT ", RequesterThreadId: %" PRISZT ")\n",
+ ThreadName_.c_str(),
+ ThreadId_,
+ GetCurrentThreadId());
+ }
+}
+
+void* TThread::StaticThreadMainTrampoline(void* opaque)
+{
+ reinterpret_cast<TThread*>(opaque)->ThreadMainTrampoline();
+ return nullptr;
+}
+
+void TThread::ThreadMainTrampoline()
+{
+ auto this_ = MakeStrong(this);
+
+ ::TThread::SetCurrentThreadName(ThreadName_.c_str());
+
+ ThreadId_ = GetCurrentThreadId();
+ CurrentUniqueThreadId = UniqueThreadId_;
+
+ SetThreadPriority();
+
+ StartedEvent_.NotifyAll();
+
+ class TExitInterceptor
+ {
+ public:
+ ~TExitInterceptor()
+ {
+ if (Armed_ && !std::uncaught_exceptions()) {
+ if (auto* logFile = TryGetShutdownLogFile()) {
+ ::fprintf(logFile, "Thread exit interceptor triggered (ThreadId: %" PRISZT ")\n",
+ GetCurrentThreadId());
+ }
+ Shutdown();
+ }
+ }
+
+ void Disarm()
+ {
+ Armed_ = false;
+ }
+
+ private:
+ bool Armed_ = true;
+ };
+
+ static thread_local TExitInterceptor Interceptor;
+
+ ThreadMain();
+
+ Interceptor.Disarm();
+
+ StoppedEvent_.NotifyAll();
+}
+
+void TThread::StartPrologue()
+{ }
+
+void TThread::StartEpilogue()
+{ }
+
+void TThread::StopPrologue()
+{ }
+
+void TThread::StopEpilogue()
+{ }
+
+void TThread::SetThreadPriority()
+{
+ YT_VERIFY(ThreadId_ != InvalidThreadId);
+
+#ifdef _linux_
+ if (ThreadPriority_ == EThreadPriority::RealTime) {
+ struct sched_param param{
+ .sched_priority = 1
+ };
+ int result = sched_setscheduler(ThreadId_, SCHED_FIFO, &param);
+ if (result == 0) {
+ YT_LOG_DEBUG("Thread real-time priority enabled (ThreadName: %v)",
+ ThreadName_);
+ } else {
+ YT_LOG_DEBUG(TError::FromSystem(), "Cannot enable thread real-time priority: sched_setscheduler failed (ThreadName: %v)",
+ ThreadName_);
+ }
+ }
+#else
+ Y_UNUSED(ThreadPriority_);
+ Y_UNUSED(Logger);
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
diff --git a/yt/yt/core/threading/thread.h b/yt/yt/core/threading/thread.h
new file mode 100644
index 0000000000..bc54afdc07
--- /dev/null
+++ b/yt/yt/core/threading/thread.h
@@ -0,0 +1,92 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/shutdown.h>
+
+#include <library/cpp/yt/threading/event_count.h>
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <util/system/thread.h>
+
+namespace NYT::NThreading {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A shutdown-aware thread wrapper.
+class TThread
+ : public virtual TRefCounted
+{
+public:
+ explicit TThread(
+ TString threadName,
+ EThreadPriority threadPriority = EThreadPriority::Normal,
+ int shutdownPriority = 0);
+ ~TThread();
+
+ //! Ensures the thread is started.
+ /*!
+ * Also invokes start hooks (in the caller's thread).
+ * Safe to call multiple times. Fast on fastpath.
+ * Returns true if the thread has been indeed started.
+ */
+ bool Start();
+
+ //! Ensures the thread is stopped.
+ /*!
+ * Safe to call multiple times.
+ * Also invokes stop hooks (in the caller's thread).
+ */
+ void Stop();
+
+ bool IsStarted() const;
+ bool IsStopping() const;
+
+ TThreadId GetThreadId() const;
+ TString GetThreadName() const;
+
+protected:
+ virtual void StartPrologue();
+ virtual void StartEpilogue();
+ virtual void StopPrologue();
+ virtual void StopEpilogue();
+
+ virtual void ThreadMain() = 0;
+
+private:
+ const TString ThreadName_;
+ const EThreadPriority ThreadPriority_;
+ const int ShutdownPriority_;
+
+ const TThreadId UniqueThreadId_;
+
+ YT_DECLARE_SPIN_LOCK(TSpinLock, SpinLock_);
+ std::atomic<bool> Started_ = false;
+ std::atomic<bool> Stopping_ = false;
+ TShutdownCookie ShutdownCookie_;
+
+ NThreading::TEvent StartedEvent_;
+ NThreading::TEvent StoppedEvent_;
+
+ TThreadId ThreadId_ = InvalidThreadId;
+ ::TThread UnderlyingThread_;
+
+ void SetThreadPriority();
+
+ bool StartSlow();
+
+ bool CanWaitForThreadShutdown() const;
+
+ static void* StaticThreadMainTrampoline(void* opaque);
+ void ThreadMainTrampoline();
+};
+
+DEFINE_REFCOUNTED_TYPE(TThread)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NThreading
+
+#define THREAD_INL_H_
+#include "thread-inl.h"
+#undef THREAD_INL_H_
diff --git a/yt/yt/core/tracing/allocation_hooks.cpp b/yt/yt/core/tracing/allocation_hooks.cpp
new file mode 100644
index 0000000000..873e410c15
--- /dev/null
+++ b/yt/yt/core/tracing/allocation_hooks.cpp
@@ -0,0 +1,67 @@
+#include "allocation_tags.h"
+
+#include "allocation_tags.h"
+#include "trace_context.h"
+
+#include <library/cpp/yt/memory/leaky_singleton.h>
+
+#include <thread>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NTracing;
+
+static auto* FreeList = LeakySingleton<TAllocationTagsFreeList>();
+
+void* CreateAllocationTagsData()
+{
+ auto* traceContext = TryGetCurrentTraceContext();
+ if (!traceContext) {
+ return nullptr;
+ }
+ auto allocationTagsPtr = traceContext->GetAllocationTags();
+ return static_cast<void*>(allocationTagsPtr.Release());
+}
+
+void* CopyAllocationTagsData(void* ptr)
+{
+ if (ptr) {
+ auto* allocationTagsPtr = static_cast<TAllocationTags*>(ptr);
+ allocationTagsPtr->Ref();
+ }
+ return ptr;
+}
+
+void DestroyAllocationTagsData(void* ptr)
+{
+ auto* allocationTagsPtr = static_cast<TAllocationTags*>(ptr);
+ // NB. No need to check for nullptr here, because ScheduleFree already does that
+ FreeList->ScheduleFree(allocationTagsPtr);
+}
+
+const std::vector<std::pair<TString, TString>>& ReadAllocationTagsData(void* ptr)
+{
+ auto* allocationTagsPtr = static_cast<TAllocationTags*>(ptr);
+ if (!allocationTagsPtr) {
+ static std::vector<std::pair<TString, TString>> emptyTags;
+ return emptyTags;
+ }
+ return allocationTagsPtr->GetTags();
+}
+
+void StartAllocationTagsCleanupThread(TDuration cleanupInterval)
+{
+ std::thread backgroundThread([cleanupInterval] {
+ for (;;) {
+ FreeList->Cleanup();
+ Sleep(cleanupInterval);
+ }
+ });
+ backgroundThread.detach();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/tracing/allocation_tags.cpp b/yt/yt/core/tracing/allocation_tags.cpp
new file mode 100644
index 0000000000..1c041cd9fc
--- /dev/null
+++ b/yt/yt/core/tracing/allocation_tags.cpp
@@ -0,0 +1,49 @@
+#include "allocation_tags.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAllocationTags::TAllocationTags(std::vector<std::pair<TString, TString>> tags)
+ : Tags_(std::move(tags))
+{ }
+
+const TAllocationTags::TTags& TAllocationTags::GetTags() const
+{
+ return Tags_;
+}
+
+TAllocationTagsFreeList::~TAllocationTagsFreeList()
+{
+ Cleanup();
+}
+
+void TAllocationTagsFreeList::ScheduleFree(TAllocationTags* tagsRawPtr)
+{
+ if (tagsRawPtr == nullptr) {
+ return;
+ }
+ if (!GetRefCounter(tagsRawPtr)->Unref()) {
+ return;
+ }
+ YT_VERIFY(tagsRawPtr->Next_ == nullptr);
+ auto guard = Guard(Spinlock_);
+ tagsRawPtr->Next_ = Head_;
+ Head_ = tagsRawPtr;
+}
+
+void TAllocationTagsFreeList::Cleanup()
+{
+ auto guard = Guard(Spinlock_);
+ auto head = std::exchange(Head_, nullptr);
+ guard.Release();
+ while (head != nullptr) {
+ auto oldHead = head;
+ head = head->Next_;
+ DestroyRefCounted(oldHead);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/allocation_tags.h b/yt/yt/core/tracing/allocation_tags.h
new file mode 100644
index 0000000000..1b6be263f5
--- /dev/null
+++ b/yt/yt/core/tracing/allocation_tags.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAllocationTags : public TRefCounted
+{
+public:
+ using TTags = std::vector<std::pair<TString, TString>>;
+
+ explicit TAllocationTags(TTags tags);
+
+ const TTags& GetTags() const;
+
+private:
+ friend class TAllocationTagsFreeList;
+
+ const TTags Tags_;
+ TAllocationTags* Next_ = nullptr;
+};
+
+DEFINE_REFCOUNTED_TYPE(TAllocationTags)
+
+class TAllocationTagsFreeList
+{
+public:
+ //! Decreases refcount of tagsRawPtr. If refcount becomes zero, puts the pointer into queue.
+ //!
+ //! The intended usage is
+ //! list->ScheduleFree(tags.Release());
+ //! where tags is TAllocationTagsPtr.
+ void ScheduleFree(TAllocationTags* tagsRawPtr);
+
+ //! Free all the pointers in the queue.
+ void Cleanup();
+
+ ~TAllocationTagsFreeList();
+
+private:
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Spinlock_);
+ TAllocationTags* Head_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/config.cpp b/yt/yt/core/tracing/config.cpp
new file mode 100644
index 0000000000..42eee6b053
--- /dev/null
+++ b/yt/yt/core/tracing/config.cpp
@@ -0,0 +1,15 @@
+#include "config.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTracingConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("send_baggage", &TThis::SendBaggage)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/config.h b/yt/yt/core/tracing/config.h
new file mode 100644
index 0000000000..b8dc889ee4
--- /dev/null
+++ b/yt/yt/core/tracing/config.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTracingConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ bool SendBaggage;
+
+ REGISTER_YSON_STRUCT(TTracingConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TTracingConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/private.h b/yt/yt/core/tracing/private.h
new file mode 100644
index 0000000000..36ce19b9b4
--- /dev/null
+++ b/yt/yt/core/tracing/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/logging/log.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline const NLogging::TLogger TracingLogger("Tracing");
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/public.cpp b/yt/yt/core/tracing/public.cpp
new file mode 100644
index 0000000000..f02e5e46e3
--- /dev/null
+++ b/yt/yt/core/tracing/public.cpp
@@ -0,0 +1,10 @@
+#include "public.h"
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
+
diff --git a/yt/yt/core/tracing/public.h b/yt/yt/core/tracing/public.h
new file mode 100644
index 0000000000..f6e8d090b5
--- /dev/null
+++ b/yt/yt/core/tracing/public.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <library/cpp/yt/misc/guid.h>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TTracingExt;
+
+} // namespace NProto
+
+DECLARE_REFCOUNTED_CLASS(TTraceContext)
+
+DECLARE_REFCOUNTED_CLASS(TTracingConfig)
+
+DECLARE_REFCOUNTED_CLASS(TAllocationTags)
+
+using TTraceId = TGuid;
+constexpr TTraceId InvalidTraceId = {};
+
+using TSpanId = ui64;
+constexpr TSpanId InvalidSpanId = 0;
+
+// Request ids come from RPC infrastructure but
+// we should avoid include-dependencies here.
+using TRequestId = TGuid;
+constexpr TRequestId InvalidRequestId = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/trace_context-inl.h b/yt/yt/core/tracing/trace_context-inl.h
new file mode 100644
index 0000000000..0aa73aadca
--- /dev/null
+++ b/yt/yt/core/tracing/trace_context-inl.h
@@ -0,0 +1,237 @@
+#ifndef TRACE_CONTEXT_INL_H_
+#error "Direct inclusion of this file is not allowed, include trace_context.h"
+// For the sake of sane code completion.
+#include "trace_context.h"
+#endif
+
+#include <atomic>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE bool TTraceContext::IsRecorded() const
+{
+ auto state = State_.load(std::memory_order::relaxed);
+ return state == ETraceContextState::Recorded || state == ETraceContextState::Sampled;
+}
+
+Y_FORCE_INLINE bool TTraceContext::IsPropagated() const
+{
+ return Propagated_;
+}
+
+Y_FORCE_INLINE bool TTraceContext::IsDebug() const
+{
+ return Debug_;
+}
+
+Y_FORCE_INLINE TTraceId TTraceContext::GetTraceId() const
+{
+ return TraceId_;
+}
+
+Y_FORCE_INLINE TSpanId TTraceContext::GetSpanId() const
+{
+ return SpanId_;
+}
+
+Y_FORCE_INLINE TSpanId TTraceContext::GetParentSpanId() const
+{
+ return ParentSpanId_;
+}
+
+Y_FORCE_INLINE TRequestId TTraceContext::GetRequestId() const
+{
+ return RequestId_;
+}
+
+Y_FORCE_INLINE const TString& TTraceContext::GetSpanName() const
+{
+ return SpanName_;
+}
+
+Y_FORCE_INLINE const TString& TTraceContext::GetLoggingTag() const
+{
+ return LoggingTag_;
+}
+
+Y_FORCE_INLINE const std::optional<TString>& TTraceContext::GetTargetEndpoint() const
+{
+ return TargetEndpoint_;
+}
+
+Y_FORCE_INLINE NProfiling::TCpuDuration TTraceContext::GetElapsedCpuTime() const
+{
+ return ElapsedCpuTime_.load(std::memory_order::relaxed);
+}
+
+template <class T>
+void TTraceContext::AddTag(const TString& tagName, const T& tagValue)
+{
+ if (!IsRecorded()) {
+ return;
+ }
+
+ using ::ToString;
+ AddTag(tagName, ToString(tagValue));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+extern thread_local TTraceContext* CurrentTraceContext;
+
+TTraceContextPtr SwapTraceContext(TTraceContextPtr newContext);
+
+} // namespace NDetail
+
+Y_FORCE_INLINE TCurrentTraceContextGuard::TCurrentTraceContextGuard(TTraceContextPtr traceContext)
+ : Active_(static_cast<bool>(traceContext))
+{
+ if (Active_) {
+ OldTraceContext_ = NDetail::SwapTraceContext(std::move(traceContext));
+ }
+}
+
+Y_FORCE_INLINE TCurrentTraceContextGuard::TCurrentTraceContextGuard(TCurrentTraceContextGuard&& other)
+ : Active_(other.Active_)
+ , OldTraceContext_(std::move(other.OldTraceContext_))
+{
+ other.Active_ = false;
+}
+
+Y_FORCE_INLINE TCurrentTraceContextGuard::~TCurrentTraceContextGuard()
+{
+ Release();
+}
+
+Y_FORCE_INLINE bool TCurrentTraceContextGuard::IsActive() const
+{
+ return Active_;
+}
+
+Y_FORCE_INLINE void TCurrentTraceContextGuard::Release()
+{
+ if (Active_) {
+ NDetail::SwapTraceContext(std::move(OldTraceContext_));
+ Active_ = false;
+ }
+}
+
+Y_FORCE_INLINE const TTraceContextPtr& TCurrentTraceContextGuard::GetOldTraceContext() const
+{
+ return OldTraceContext_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TNullTraceContextGuard::TNullTraceContextGuard()
+ : Active_(true)
+ , OldTraceContext_(NDetail::SwapTraceContext(nullptr))
+{ }
+
+Y_FORCE_INLINE TNullTraceContextGuard::TNullTraceContextGuard(TNullTraceContextGuard&& other)
+ : Active_(other.Active_)
+ , OldTraceContext_(std::move(other.OldTraceContext_))
+{
+ other.Active_ = false;
+}
+
+Y_FORCE_INLINE TNullTraceContextGuard::~TNullTraceContextGuard()
+{
+ Release();
+}
+
+Y_FORCE_INLINE bool TNullTraceContextGuard::IsActive() const
+{
+ return Active_;
+}
+
+Y_FORCE_INLINE void TNullTraceContextGuard::Release()
+{
+ if (Active_) {
+ NDetail::SwapTraceContext(std::move(OldTraceContext_));
+ Active_ = false;
+ }
+}
+
+Y_FORCE_INLINE const TTraceContextPtr& TNullTraceContextGuard::GetOldTraceContext() const
+{
+ return OldTraceContext_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TTraceContextGuard::TTraceContextGuard(TTraceContextPtr traceContext)
+ : TraceContextGuard_(std::move(traceContext))
+ , FinishGuard_(TryGetCurrentTraceContext())
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline bool TChildTraceContextGuard::IsRecorded(const TTraceContextPtr& traceContext)
+{
+ return traceContext && traceContext->IsRecorded();
+}
+
+inline TChildTraceContextGuard::TChildTraceContextGuard(
+ const TTraceContextPtr& traceContext,
+ TString spanName)
+ : TraceContextGuard_(IsRecorded(traceContext) ? traceContext->CreateChild(spanName) : nullptr)
+ , FinishGuard_(IsRecorded(traceContext) ? TryGetCurrentTraceContext() : nullptr)
+{ }
+
+inline TChildTraceContextGuard::TChildTraceContextGuard(
+ TString spanName)
+ : TChildTraceContextGuard(
+ TryGetCurrentTraceContext(),
+ std::move(spanName))
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TTraceContextFinishGuard::TTraceContextFinishGuard(TTraceContextPtr traceContext)
+ : TraceContext_(std::move(traceContext))
+{ }
+
+inline TTraceContextFinishGuard::~TTraceContextFinishGuard()
+{
+ if (TraceContext_) {
+ TraceContext_->Finish();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE TTraceContext* TryGetCurrentTraceContext()
+{
+ return NDetail::CurrentTraceContext;
+}
+
+Y_FORCE_INLINE TTraceContext* GetCurrentTraceContext()
+{
+ YT_ASSERT(NDetail::CurrentTraceContext);
+ return NDetail::CurrentTraceContext;
+}
+
+Y_FORCE_INLINE TTraceContextPtr CreateTraceContextFromCurrent(TString spanName)
+{
+ auto* context = TryGetCurrentTraceContext();
+ return context ? context->CreateChild(std::move(spanName)) : TTraceContext::NewRoot(std::move(spanName));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFn>
+void AnnotateTraceContext(TFn&& fn)
+{
+ if (auto* traceContext = TryGetCurrentTraceContext(); traceContext && traceContext->IsRecorded()) {
+ fn(traceContext);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
diff --git a/yt/yt/core/tracing/trace_context.cpp b/yt/yt/core/tracing/trace_context.cpp
new file mode 100644
index 0000000000..152b01627e
--- /dev/null
+++ b/yt/yt/core/tracing/trace_context.cpp
@@ -0,0 +1,717 @@
+#include "trace_context.h"
+#include "private.h"
+#include "config.h"
+#include "allocation_tags.h"
+
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt_proto/yt/core/tracing/proto/tracing_ext.pb.h>
+
+#include <yt/yt/library/tracing/tracer.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+#include <atomic>
+#include <mutex>
+
+namespace NYT::NTracing {
+
+using namespace NConcurrency;
+using namespace NProfiling;
+using namespace NYTree;
+using namespace NYson;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto& Logger = TracingLogger;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TGlobalTracerStorage
+{
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock);
+ ITracerPtr Tracer;
+};
+
+// TODO(prime@): Switch constinit global variable, once gcc supports it.
+static TGlobalTracerStorage* GlobalTracerStorage()
+{
+ return LeakySingleton<TGlobalTracerStorage>();
+}
+
+ITracerPtr GetGlobalTracer()
+{
+ auto tracerStorage = GlobalTracerStorage();
+ auto guard = Guard(tracerStorage->Lock);
+ return tracerStorage->Tracer;
+}
+
+void SetGlobalTracer(const ITracerPtr& tracer)
+{
+ ITracerPtr oldTracer;
+
+ {
+ auto tracerStorage = GlobalTracerStorage();
+ auto guard = Guard(tracerStorage->Lock);
+ oldTracer = tracerStorage->Tracer;
+ tracerStorage->Tracer = tracer;
+ }
+
+ if (oldTracer) {
+ oldTracer->Stop();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTracingConfigSingleton
+{
+ TAtomicIntrusivePtr<TTracingConfig> Config{New<TTracingConfig>()};
+};
+
+static TTracingConfigSingleton* GlobalTracingConfig()
+{
+ return LeakySingleton<TTracingConfigSingleton>();
+}
+
+void SetTracingConfig(TTracingConfigPtr config)
+{
+ GlobalTracingConfig()->Config.Store(std::move(config));
+}
+
+TTracingConfigPtr GetTracingConfig()
+{
+ return GlobalTracingConfig()->Config.Acquire();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+thread_local TTraceContext* CurrentTraceContext;
+thread_local TCpuInstant TraceContextTimingCheckpoint;
+
+TSpanId GenerateSpanId()
+{
+ return RandomNumber<ui64>(std::numeric_limits<ui64>::max() - 1) + 1;
+}
+
+void SetCurrentTraceContext(TTraceContext* context)
+{
+ CurrentTraceContext = context;
+ std::atomic_signal_fence(std::memory_order::seq_cst);
+}
+
+TTraceContextPtr SwapTraceContext(TTraceContextPtr newContext)
+{
+ auto& propagatingStorage = GetCurrentPropagatingStorage();
+ auto oldContext = propagatingStorage.Exchange<TTraceContextPtr>(newContext).value_or(nullptr);
+
+ auto now = GetApproximateCpuInstant();
+ // Invalid if no oldContext.
+ auto delta = now - TraceContextTimingCheckpoint;
+
+ if (oldContext && newContext) {
+ YT_LOG_TRACE("Switching context (OldContext: %v, NewContext: %v, CpuTimeDelta: %v)",
+ oldContext,
+ newContext,
+ NProfiling::CpuDurationToDuration(delta));
+ } else if (oldContext) {
+ YT_LOG_TRACE("Uninstalling context (Context: %v, CpuTimeDelta: %v)",
+ oldContext,
+ NProfiling::CpuDurationToDuration(delta));
+ } else if (newContext) {
+ YT_LOG_TRACE("Installing context (Context: %v)",
+ newContext);
+ }
+
+ if (oldContext) {
+ oldContext->IncrementElapsedCpuTime(delta);
+ }
+
+ SetCurrentTraceContext(newContext.Get());
+ TraceContextTimingCheckpoint = now;
+
+ return oldContext;
+}
+
+void OnContextSwitchOut()
+{
+ if (auto* context = TryGetCurrentTraceContext()) {
+ auto now = GetApproximateCpuInstant();
+ context->IncrementElapsedCpuTime(now - TraceContextTimingCheckpoint);
+ SetCurrentTraceContext(nullptr);
+ TraceContextTimingCheckpoint = 0;
+ }
+}
+
+void OnContextSwitchIn()
+{
+ if (auto* context = TryGetTraceContextFromPropagatingStorage(GetCurrentPropagatingStorage())) {
+ SetCurrentTraceContext(context);
+ TraceContextTimingCheckpoint = GetApproximateCpuInstant();
+ } else {
+ SetCurrentTraceContext(nullptr);
+ TraceContextTimingCheckpoint = 0;
+ }
+}
+
+void OnPropagatingStorageSwitch(
+ const TPropagatingStorage& oldStorage,
+ const TPropagatingStorage& newStorage)
+{
+ TCpuInstant now = 0;
+
+ if (auto* oldContext = TryGetCurrentTraceContext()) {
+ YT_ASSERT(oldContext == TryGetTraceContextFromPropagatingStorage(oldStorage));
+ YT_ASSERT(TraceContextTimingCheckpoint != 0);
+ now = GetApproximateCpuInstant();
+ oldContext->IncrementElapsedCpuTime(now - TraceContextTimingCheckpoint);
+ }
+
+ if (auto* newContext = TryGetTraceContextFromPropagatingStorage(newStorage)) {
+ SetCurrentTraceContext(newContext);
+ if (now == 0) {
+ now = GetApproximateCpuInstant();
+ }
+ TraceContextTimingCheckpoint = now;
+ } else {
+ SetCurrentTraceContext(nullptr);
+ TraceContextTimingCheckpoint = 0;
+ }
+}
+
+void InitializeTraceContexts()
+{
+ static std::once_flag Initialized;
+ std::call_once(
+ Initialized,
+ [] {
+ InstallGlobalContextSwitchHandlers(
+ OnContextSwitchOut,
+ OnContextSwitchIn);
+ InstallGlobalPropagatingStorageSwitchHandler(
+ OnPropagatingStorageSwitch);
+ });
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TSpanContext& context, TStringBuf /*spec*/)
+{
+ builder->AppendFormat("%v:%08" PRIx64 ":%v",
+ context.TraceId,
+ context.SpanId,
+ (context.Sampled ? 1u : 0) | (context.Debug ? 2u : 0));
+}
+
+TString ToString(const TSpanContext& context)
+{
+ return ToStringViaBuilder(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTraceContext::TTraceContext(
+ TSpanContext parentSpanContext,
+ TString spanName,
+ TTraceContextPtr parentTraceContext)
+ : TraceId_(parentSpanContext.TraceId)
+ , SpanId_(NDetail::GenerateSpanId())
+ , ParentSpanId_(parentSpanContext.SpanId)
+ , Debug_(parentSpanContext.Debug)
+ , State_(parentTraceContext
+ ? parentTraceContext->State_.load()
+ : (parentSpanContext.Sampled ? ETraceContextState::Sampled : ETraceContextState::Disabled))
+ , Propagated_(true)
+ , ParentContext_(std::move(parentTraceContext))
+ , SpanName_(std::move(spanName))
+ , RequestId_(ParentContext_ ? ParentContext_->GetRequestId() : TRequestId{})
+ , TargetEndpoint_(ParentContext_ ? ParentContext_->GetTargetEndpoint() : std::nullopt)
+ , LoggingTag_(ParentContext_ ? ParentContext_->GetLoggingTag() : TString{})
+ , StartTime_(GetCpuInstant())
+ , Baggage_(ParentContext_ ? ParentContext_->GetBaggage() : TYsonString{})
+{
+ NDetail::InitializeTraceContexts();
+}
+
+void TTraceContext::SetTargetEndpoint(const std::optional<TString>& targetEndpoint)
+{
+ TargetEndpoint_ = targetEndpoint;
+}
+
+void TTraceContext::SetRequestId(TRequestId requestId)
+{
+ RequestId_ = requestId;
+}
+
+void TTraceContext::SetLoggingTag(const TString& loggingTag)
+{
+ LoggingTag_ = loggingTag;
+}
+
+void TTraceContext::SetAllocationTags(TAllocationTagsPtr tags)
+{
+ AllocationTags_ = std::move(tags);
+}
+
+TAllocationTagsPtr TTraceContext::GetAllocationTags() const
+{
+ return AllocationTags_;
+}
+
+std::vector<std::pair<TString, TString>> TTraceContext::ExtractAllocationTags() const
+{
+ if (AllocationTags_ != nullptr) {
+ return AllocationTags_->GetTags();
+ }
+ return {};
+}
+
+void TTraceContext::SetRecorded()
+{
+ auto disabled = ETraceContextState::Disabled;
+ State_.compare_exchange_strong(disabled, ETraceContextState::Recorded);
+}
+
+void TTraceContext::SetPropagated(bool value)
+{
+ Propagated_ = value;
+}
+
+TTraceContextPtr TTraceContext::CreateChild(
+ TString spanName)
+{
+ auto child = New<TTraceContext>(
+ GetSpanContext(),
+ std::move(spanName),
+ /*parentTraceContext*/ this);
+
+ auto guard = Guard(Lock_);
+ child->ProfilingTags_ = ProfilingTags_;
+ child->TargetEndpoint_ = TargetEndpoint_;
+ return child;
+}
+
+TSpanContext TTraceContext::GetSpanContext() const
+{
+ return TSpanContext{
+ .TraceId = GetTraceId(),
+ .SpanId = GetSpanId(),
+ .Sampled = IsSampled(),
+ .Debug = Debug_,
+ };
+}
+
+TDuration TTraceContext::GetElapsedTime() const
+{
+ return CpuDurationToDuration(GetElapsedCpuTime());
+}
+
+void TTraceContext::SetSampled(bool value)
+{
+ if (!value) {
+ State_ = ETraceContextState::Disabled;
+ } else {
+ State_ = ETraceContextState::Sampled;
+ }
+}
+
+TInstant TTraceContext::GetStartTime() const
+{
+ return NProfiling::CpuInstantToInstant(StartTime_);
+}
+
+TDuration TTraceContext::GetDuration() const
+{
+ YT_ASSERT(Finished_.load());
+ return NProfiling::CpuDurationToDuration(Duration_.load());
+}
+
+TTraceContext::TTagList TTraceContext::GetTags() const
+{
+ auto guard = Guard(Lock_);
+ return Tags_;
+}
+
+TTraceContext::TLogList TTraceContext::GetLogEntries() const
+{
+ auto guard = Guard(Lock_);
+ return Logs_;
+}
+
+TTraceContext::TAsyncChildrenList TTraceContext::GetAsyncChildren() const
+{
+ auto guard = Guard(Lock_);
+ return AsyncChildren_;
+}
+
+TYsonString TTraceContext::GetBaggage() const
+{
+ auto guard = Guard(Lock_);
+ return Baggage_;
+}
+
+void TTraceContext::SetBaggage(TYsonString baggage)
+{
+ auto guard = Guard(Lock_);
+ Baggage_ = std::move(baggage);
+}
+
+IAttributeDictionaryPtr TTraceContext::UnpackBaggage() const
+{
+ auto baggage = GetBaggage();
+ return baggage ? ConvertToAttributes(baggage) : nullptr;
+}
+
+NYTree::IAttributeDictionaryPtr TTraceContext::UnpackOrCreateBaggage() const
+{
+ auto baggage = GetBaggage();
+ return baggage ? ConvertToAttributes(baggage) : CreateEphemeralAttributes();
+}
+
+void TTraceContext::PackBaggage(const IAttributeDictionaryPtr& baggage)
+{
+ SetBaggage(baggage ? ConvertToYsonString(baggage) : TYsonString{});
+}
+
+void TTraceContext::AddTag(const TString& tagKey, const TString& tagValue)
+{
+ if (!IsRecorded()) {
+ return;
+ }
+
+ if (Finished_.load()) {
+ return;
+ }
+
+ auto guard = Guard(Lock_);
+ Tags_.emplace_back(tagKey, tagValue);
+}
+
+void TTraceContext::AddProfilingTag(const TString& name, const TString& value)
+{
+ auto guard = Guard(Lock_);
+ ProfilingTags_.emplace_back(name, value);
+}
+
+void TTraceContext::AddProfilingTag(const TString& name, i64 value)
+{
+ auto guard = Guard(Lock_);
+ ProfilingTags_.emplace_back(name, value);
+}
+
+std::vector<std::pair<TString, std::variant<TString, i64>>> TTraceContext::GetProfilingTags()
+{
+ auto guard = Guard(Lock_);
+ return ProfilingTags_;
+}
+
+bool TTraceContext::AddAsyncChild(TTraceId traceId)
+{
+ if (!IsRecorded()) {
+ return false;
+ }
+
+ if (Finished_.load()) {
+ return false;
+ }
+
+ auto guard = Guard(Lock_);
+ AsyncChildren_.push_back(traceId);
+ return true;
+}
+
+void TTraceContext::AddErrorTag()
+{
+ if (!IsRecorded()) {
+ return;
+ }
+
+ static const TString ErrorAnnotationName("error");
+ static const TString ErrorAnnotationValue("true");
+ AddTag(ErrorAnnotationName, ErrorAnnotationValue);
+}
+
+void TTraceContext::AddLogEntry(TCpuInstant at, TString message)
+{
+ if (!IsRecorded()) {
+ return;
+ }
+
+ if (Finished_.load()) {
+ return;
+ }
+
+ auto guard = Guard(Lock_);
+ Logs_.push_back(TTraceLogEntry{at, std::move(message)});
+}
+
+bool TTraceContext::IsFinished()
+{
+ return Finished_.load();
+}
+
+bool TTraceContext::IsSampled() const
+{
+ auto traceContext = this;
+ while (traceContext) {
+ auto state = traceContext->State_.load(std::memory_order::relaxed);
+ if (state == ETraceContextState::Sampled) {
+ return true;
+ } else if (state == ETraceContextState::Disabled) {
+ return false;
+ }
+
+ traceContext = traceContext->ParentContext_.Get();
+ }
+
+ return false;
+}
+
+void TTraceContext::SetDuration()
+{
+ if (Duration_.load() == 0) {
+ Duration_ = GetCpuInstant() - StartTime_;
+ }
+}
+
+void TTraceContext::Finish()
+{
+ if (Finished_.exchange(true)) {
+ return;
+ }
+ SetDuration();
+
+ auto state = State_.load(std::memory_order::relaxed);
+ if (state == ETraceContextState::Disabled) {
+ return;
+ } else if (state == ETraceContextState::Sampled) {
+ if (auto tracer = GetGlobalTracer(); tracer) {
+ tracer->Enqueue(MakeStrong(this));
+ }
+ } else if (state == ETraceContextState::Recorded) {
+ if (!IsSampled()) {
+ return;
+ }
+
+ if (auto tracer = GetGlobalTracer(); tracer) {
+ auto traceContext = this;
+ while (traceContext) {
+ if (traceContext->State_.load() != ETraceContextState::Recorded) {
+ break;
+ }
+
+ if (traceContext->Finished_.load() && !traceContext->Submitted_.exchange(true)) {
+ traceContext->SetDuration();
+ tracer->Enqueue(MakeStrong(traceContext));
+ }
+
+ traceContext = traceContext->ParentContext_.Get();
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FormatValue(TStringBuilderBase* builder, const TTraceContext* context, TStringBuf /*spec*/)
+{
+ if (context) {
+ builder->AppendFormat("%v %v",
+ context->GetSpanName(),
+ context->GetSpanContext());
+ } else {
+ builder->AppendString(TStringBuf("<null>"));
+ }
+}
+
+TString ToString(const TTraceContext* context)
+{
+ return ToStringViaBuilder(context);
+}
+
+void FormatValue(TStringBuilderBase* builder, const TTraceContextPtr& context, TStringBuf spec)
+{
+ FormatValue(builder, context.Get(), spec);
+}
+
+TString ToString(const TTraceContextPtr& context)
+{
+ return ToStringViaBuilder(context);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TTracingExt* ext, const TTraceContextPtr& context)
+{
+ if (!context || !context->IsPropagated()) {
+ ext->Clear();
+ return;
+ }
+
+ ToProto(ext->mutable_trace_id(), context->GetTraceId());
+ ext->set_span_id(context->GetSpanId());
+ ext->set_sampled(context->IsSampled());
+ ext->set_debug(context->IsDebug());
+
+ if (auto endpoint = context->GetTargetEndpoint()){
+ ext->set_target_endpoint(endpoint.value());
+ }
+ if (GetTracingConfig()->SendBaggage) {
+ if (auto baggage = context->GetBaggage()) {
+ ext->set_baggage(baggage.ToString());
+ }
+ }
+}
+
+TTraceContextPtr TTraceContext::NewRoot(TString spanName, TTraceId traceId)
+{
+ return New<TTraceContext>(
+ TSpanContext{
+ .TraceId = traceId ? traceId : TTraceId::Create(),
+ .SpanId = InvalidSpanId,
+ .Sampled = false,
+ .Debug = false,
+ },
+ std::move(spanName));
+}
+
+TTraceContextPtr TTraceContext::NewChildFromSpan(
+ TSpanContext parentSpanContext,
+ TString spanName,
+ std::optional<TString> endpoint,
+ TYsonString baggage)
+{
+ auto result = New<TTraceContext>(
+ parentSpanContext,
+ std::move(spanName));
+ result->SetBaggage(std::move(baggage));
+ result->SetTargetEndpoint(endpoint);
+ return result;
+}
+
+TTraceContextPtr TTraceContext::NewChildFromRpc(
+ const NProto::TTracingExt& ext,
+ TString spanName,
+ TRequestId requestId,
+ bool forceTracing)
+{
+ auto traceId = FromProto<TTraceId>(ext.trace_id());
+ if (!traceId) {
+ if (!forceTracing) {
+ return nullptr;
+ }
+
+ auto root = NewRoot(std::move(spanName));
+ root->SetRequestId(requestId);
+ root->SetRecorded();
+ return root;
+ }
+
+ auto traceContext = New<TTraceContext>(
+ TSpanContext{
+ traceId,
+ ext.span_id(),
+ ext.sampled(),
+ ext.debug()
+ },
+ std::move(spanName));
+ traceContext->SetRequestId(requestId);
+ if (ext.has_baggage()) {
+ traceContext->SetBaggage(TYsonString(ext.baggage()));
+ }
+ if (ext.has_target_endpoint()) {
+ traceContext->SetTargetEndpoint(ext.target_endpoint());
+ }
+ return traceContext;
+}
+
+void TTraceContext::IncrementElapsedCpuTime(NProfiling::TCpuDuration delta)
+{
+ for (auto* current = this; current; current = current->ParentContext_.Get()) {
+ current->ElapsedCpuTime_ += delta;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void FlushCurrentTraceContextElapsedTime()
+{
+ auto* context = TryGetCurrentTraceContext();
+ if (!context) {
+ return;
+ }
+
+ auto now = GetApproximateCpuInstant();
+ auto delta = std::max(now - NDetail::TraceContextTimingCheckpoint, static_cast<TCpuInstant>(0));
+ YT_LOG_TRACE("Flushing context time (Context: %v, CpuTimeDelta: %v)",
+ context,
+ NProfiling::CpuDurationToDuration(delta));
+ context->IncrementElapsedCpuTime(delta);
+ NDetail::TraceContextTimingCheckpoint = now;
+}
+
+//! Do not rename, change the signature, or drop Y_NO_INLINE.
+//! Used in devtools/gdb/yt_fibers_printer.py.
+Y_NO_INLINE TTraceContext* TryGetTraceContextFromPropagatingStorage(const NConcurrency::TPropagatingStorage& storage)
+{
+ auto result = storage.Find<TTraceContextPtr>();
+ return result ? result->Get() : nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NTracing
+
+namespace NYT::NYTProf {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void* AcquireFiberTagStorage()
+{
+ auto* traceContext = NTracing::TryGetCurrentTraceContext();
+ if (traceContext) {
+ Ref(traceContext);
+ }
+ return reinterpret_cast<void*>(traceContext);
+}
+
+std::vector<std::pair<TString, std::variant<TString, i64>>> ReadFiberTags(void* storage)
+{
+ if (auto* traceContext = reinterpret_cast<NTracing::TTraceContext*>(storage)) {
+ return traceContext->GetProfilingTags();
+ } else {
+ return {};
+ }
+}
+
+void ReleaseFiberTagStorage(void* storage)
+{
+ if (storage) {
+ Unref(reinterpret_cast<NTracing::TTraceContext*>(storage));
+ }
+}
+
+TCpuInstant GetTraceContextTimingCheckpoint()
+{
+ return NTracing::NDetail::TraceContextTimingCheckpoint;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTProf
diff --git a/yt/yt/core/tracing/trace_context.h b/yt/yt/core/tracing/trace_context.h
new file mode 100644
index 0000000000..a6123b10bc
--- /dev/null
+++ b/yt/yt/core/tracing/trace_context.h
@@ -0,0 +1,399 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/guid.h>
+
+#include <yt/yt/core/profiling/public.h>
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/concurrency/public.h>
+
+#include <yt/yt/library/tracing/public.h>
+
+#include <library/cpp/yt/threading/spin_lock.h>
+
+#include <atomic>
+
+namespace NYT::NTracing {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! TSpanContext represents span identity propagated across the network.
+//!
+//! See https://opentracing.io/specification/
+struct TSpanContext
+{
+ TTraceId TraceId = InvalidTraceId;
+ TSpanId SpanId = InvalidSpanId;
+ bool Sampled = false;
+ bool Debug = false;
+};
+
+void FormatValue(TStringBuilderBase* builder, const TSpanContext& context, TStringBuf spec);
+TString ToString(const TSpanContext& context);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetGlobalTracer(const ITracerPtr& tracer);
+ITracerPtr GetGlobalTracer();
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetTracingConfig(TTracingConfigPtr config);
+TTracingConfigPtr GetTracingConfig();
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETraceContextState,
+ (Disabled) // Used to propagate TraceId, RequestId and LoggingTag.
+ (Recorded) // May be sampled later.
+ (Sampled) // Sampled and will be reported to jaeger.
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Accumulates information associated with a single tracing span.
+/*!
+ * TTraceContext contains 3 distinct pieces of logic.
+ *
+ * 1) TraceId, RequestId and LoggingTag are recorded inside trace context and
+ * passed to logger.
+ * 2) ElapsedCpu time is tracked by fiber scheduler during context switch.
+ * 3) Opentracing compatible information is recorded and later pushed to jaeger.
+ *
+ * TTraceContext objects within a single process form a tree.
+ *
+ * By default, child objects inherit TraceId, RequestId and LoggingTag from the parent.
+ *
+ * \note Thread affininty: any unless noted otherwise.
+ */
+ class TTraceContext
+ : public TRefCounted
+{
+public:
+ //! Finalizes and publishes the context (if sampling is enabled).
+ /*!
+ * Safe to call multiple times from arbitrary threads; only the first call matters.
+ */
+ void Finish();
+ bool IsFinished();
+
+ //! IsRecorded returns a flag indicating that this trace may be sent to jaeger.
+ /*!
+ * This flag should be used for fast-path optimization to skip trace annotation and child span creation.
+ */
+ bool IsRecorded() const;
+ void SetRecorded();
+
+ bool IsSampled() const;
+ void SetSampled(bool value = true);
+
+ //! IsPropagated returns a flag indicating that trace is serialized to proto.
+ /*!
+ * By default trace context is propagated.
+ * Not thread-safe.
+ */
+ bool IsPropagated() const;
+ void SetPropagated(bool value = true);
+
+ TSpanContext GetSpanContext() const;
+ TTraceId GetTraceId() const;
+ TSpanId GetSpanId() const;
+ TSpanId GetParentSpanId() const;
+ bool IsDebug() const;
+ const TString& GetSpanName() const;
+
+ //! Sets target endpoint.
+ /*!
+ * Not thread-safe.
+ */
+ void SetTargetEndpoint(const std::optional<TString>& targetEndpoint);
+ const std::optional<TString>& GetTargetEndpoint() const;
+
+ //! Sets request id.
+ /*!
+ * Not thread-safe.
+ */
+ void SetRequestId(TRequestId requestId);
+ TRequestId GetRequestId() const;
+
+ //! Sets allocation tags.
+ /*!
+ * Not thread-safe.
+ */
+ void SetAllocationTags(TAllocationTagsPtr tags);
+ TAllocationTagsPtr GetAllocationTags() const;
+ std::vector<std::pair<TString, TString>> ExtractAllocationTags() const;
+
+ //! Sets logging tag.
+ /*!
+ * Not thread-safe.
+ */
+ void SetLoggingTag(const TString& loggingTag);
+ const TString& GetLoggingTag() const;
+
+ TInstant GetStartTime() const;
+
+ //! Returns the wall time from the context's construction to #Finish call.
+ /*!
+ * Can only be called after #Finish is complete.
+ */
+ TDuration GetDuration() const;
+
+ using TTagList = TCompactVector<std::pair<TString, TString>, 4>;
+ TTagList GetTags() const;
+
+ NYson::TYsonString GetBaggage() const;
+ void SetBaggage(NYson::TYsonString baggage);
+ NYTree::IAttributeDictionaryPtr UnpackBaggage() const;
+ NYTree::IAttributeDictionaryPtr UnpackOrCreateBaggage() const;
+ void PackBaggage(const NYTree::IAttributeDictionaryPtr& baggage);
+
+ void AddTag(const TString& tagKey, const TString& tagValue);
+
+ template <class T>
+ void AddTag(const TString& tagName, const T& tagValue);
+
+ //! Adds error tag. Spans containing errors are highlited in Jaeger UI.
+ void AddErrorTag();
+
+ struct TTraceLogEntry
+ {
+ NProfiling::TCpuInstant At;
+ TString Message;
+ };
+ using TLogList = TCompactVector<TTraceLogEntry, 4>;
+ TLogList GetLogEntries() const;
+ void AddLogEntry(NProfiling::TCpuInstant at, TString message);
+
+ using TAsyncChildrenList = TCompactVector<TTraceId, 4>;
+ TAsyncChildrenList GetAsyncChildren() const;
+ bool AddAsyncChild(TTraceId traceId);
+
+ void IncrementElapsedCpuTime(NProfiling::TCpuDuration delta);
+ NProfiling::TCpuDuration GetElapsedCpuTime() const;
+ TDuration GetElapsedTime() const;
+
+ static TTraceContextPtr NewRoot(TString spanName, TTraceId traceId = {});
+
+ static TTraceContextPtr NewChildFromRpc(
+ const NProto::TTracingExt& ext,
+ TString spanName,
+ TRequestId requestId = {},
+ bool forceTracing = false);
+
+ static TTraceContextPtr NewChildFromSpan(
+ TSpanContext parentSpanContext,
+ TString spanName,
+ std::optional<TString> endpoint = {},
+ NYson::TYsonString baggage = NYson::TYsonString());
+
+ TTraceContextPtr CreateChild(TString spanName);
+
+ void AddProfilingTag(const TString& name, const TString& value);
+ void AddProfilingTag(const TString& name, i64 value);
+ std::vector<std::pair<TString, std::variant<TString, i64>>> GetProfilingTags();
+
+ friend void ToProto(NProto::TTracingExt* ext, const TTraceContextPtr& context);
+
+private:
+ const TTraceId TraceId_;
+ const TSpanId SpanId_;
+ const TSpanId ParentSpanId_;
+
+ // Right now, debug flag is just passed as-is. It is part of opentracing, but we do not interpret it in any way.
+ const bool Debug_;
+
+ mutable std::atomic<ETraceContextState> State_;
+ bool Propagated_;
+
+ const TTraceContextPtr ParentContext_;
+ const TString SpanName_;
+ TRequestId RequestId_;
+ std::optional<TString> TargetEndpoint_;
+ TString LoggingTag_;
+ const NProfiling::TCpuInstant StartTime_;
+
+ std::atomic<bool> Finished_ = false;
+ std::atomic<bool> Submitted_ = false;
+ std::atomic<NProfiling::TCpuDuration> Duration_ = {0};
+
+ std::atomic<NProfiling::TCpuDuration> ElapsedCpuTime_ = 0;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, Lock_);
+ TTagList Tags_;
+ TLogList Logs_;
+ TAsyncChildrenList AsyncChildren_;
+ NYson::TYsonString Baggage_;
+
+ std::vector<std::pair<TString, std::variant<TString, i64>>> ProfilingTags_;
+ TAllocationTagsPtr AllocationTags_;
+
+ TTraceContext(
+ TSpanContext parentSpanContext,
+ TString spanName,
+ TTraceContextPtr parentTraceContext = nullptr);
+ DECLARE_NEW_FRIEND()
+
+ void SetDuration();
+};
+
+DEFINE_REFCOUNTED_TYPE(TTraceContext)
+
+void FormatValue(TStringBuilderBase* builder, const TTraceContextPtr& context, TStringBuf spec);
+void FormatValue(TStringBuilderBase* builder, const TTraceContext* context, TStringBuf spec);
+TString ToString(const TTraceContextPtr& context);
+TString ToString(const TTraceContextPtr* context);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the current trace context, if any is installed, or null if none.
+TTraceContext* TryGetCurrentTraceContext();
+
+//! Returns the current trace context. Fails if none.
+TTraceContext* GetCurrentTraceContext();
+
+//! Flushes the elapsed time of the current trace context (if any).
+void FlushCurrentTraceContextElapsedTime();
+
+//!
+TTraceContext* TryGetTraceContextFromPropagatingStorage(const NConcurrency::TPropagatingStorage& storage);
+
+//! Creates a new trace context. If the current trace context exists, it becomes the parent of the
+//! created trace context.
+TTraceContextPtr CreateTraceContextFromCurrent(TString spanName);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Installs the given trace into the current fiber implicit trace slot.
+class TCurrentTraceContextGuard
+{
+public:
+ explicit TCurrentTraceContextGuard(TTraceContextPtr traceContext);
+ TCurrentTraceContextGuard(TCurrentTraceContextGuard&& other);
+ ~TCurrentTraceContextGuard();
+
+ bool IsActive() const;
+ void Release();
+
+ const TTraceContextPtr& GetOldTraceContext() const;
+
+private:
+ bool Active_;
+ TTraceContextPtr OldTraceContext_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Installs null trace into the current fiber implicit trace slot.
+class TNullTraceContextGuard
+{
+public:
+ TNullTraceContextGuard();
+ TNullTraceContextGuard(TNullTraceContextGuard&& other);
+ ~TNullTraceContextGuard();
+
+ bool IsActive() const;
+ void Release();
+
+ const TTraceContextPtr& GetOldTraceContext() const;
+
+private:
+ bool Active_;
+ TTraceContextPtr OldTraceContext_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Invokes TTraceContext::Finish upon destruction.
+class TTraceContextFinishGuard
+{
+public:
+ explicit TTraceContextFinishGuard(TTraceContextPtr traceContext);
+ ~TTraceContextFinishGuard();
+
+ TTraceContextFinishGuard(const TTraceContextFinishGuard&) = delete;
+ TTraceContextFinishGuard(TTraceContextFinishGuard&&) = default;
+
+ TTraceContextFinishGuard& operator=(const TTraceContextFinishGuard&) = delete;
+ TTraceContextFinishGuard& operator=(TTraceContextFinishGuard&&) = default;
+
+private:
+ TTraceContextPtr TraceContext_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Installs the given trace into the current fiber implicit trace slot.
+//! Finishes the trace context upon destruction.
+class TTraceContextGuard
+{
+public:
+ explicit TTraceContextGuard(TTraceContextPtr traceContext);
+ TTraceContextGuard(TTraceContextGuard&& other) = default;
+
+private:
+ TCurrentTraceContextGuard TraceContextGuard_;
+ TTraceContextFinishGuard FinishGuard_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Constructs a child trace context and installs it into the current fiber implicit trace slot.
+//! Finishes the child trace context upon destruction.
+class TChildTraceContextGuard
+{
+public:
+ TChildTraceContextGuard(
+ const TTraceContextPtr& traceContext,
+ TString spanName);
+ explicit TChildTraceContextGuard(
+ TString spanName);
+ TChildTraceContextGuard(TChildTraceContextGuard&& other) = default;
+
+private:
+ TCurrentTraceContextGuard TraceContextGuard_;
+ TTraceContextFinishGuard FinishGuard_;
+
+ static bool IsRecorded(const TTraceContextPtr& traceContext);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFn>
+void AnnotateTraceContext(TFn&& fn);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(babenko): move impl to cpp.
+class TTraceContextHandler
+{
+public:
+ TTraceContextHandler()
+ : TraceContext_(NTracing::TryGetCurrentTraceContext())
+ { }
+
+ NTracing::TCurrentTraceContextGuard MakeTraceContextGuard() const
+ {
+ return NTracing::TCurrentTraceContextGuard(TraceContext_);
+ }
+
+ void UpdateTraceContext()
+ {
+ TraceContext_ = NTracing::TryGetCurrentTraceContext();
+ }
+
+private:
+ NTracing::TTraceContextPtr TraceContext_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+} // namespace NYT::NTracing
+
+#define TRACE_CONTEXT_INL_H_
+#include "trace_context-inl.h"
+#undef TRACE_CONTEXT_INL_H_
diff --git a/yt/yt/core/utilex/random.cpp b/yt/yt/core/utilex/random.cpp
new file mode 100644
index 0000000000..e6b2e2df04
--- /dev/null
+++ b/yt/yt/core/utilex/random.cpp
@@ -0,0 +1,5 @@
+#include "random.h"
+
+TDuration RandomDuration(TDuration max) {
+ return TDuration::MicroSeconds(RandomNumber((max + TDuration::MicroSeconds(1)).MicroSeconds()));
+}
diff --git a/yt/yt/core/utilex/random.h b/yt/yt/core/utilex/random.h
new file mode 100644
index 0000000000..cdf74b9d24
--- /dev/null
+++ b/yt/yt/core/utilex/random.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <util/datetime/base.h>
+
+#include <util/random/random.h>
+
+/*
+ * returns random duration in range [0, max]
+ */
+TDuration RandomDuration(TDuration max);
diff --git a/yt/yt/core/ya.make b/yt/yt/core/ya.make
new file mode 100644
index 0000000000..8dc3d26db2
--- /dev/null
+++ b/yt/yt/core/ya.make
@@ -0,0 +1,386 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+INCLUDE(../ya_check_dependencies.inc)
+
+IF (ARCH_X86_64)
+ # pclmul is required for fast crc computation
+ CFLAGS(-mpclmul)
+ENDIF()
+
+SRCS(
+ actions/cancelable_context.cpp
+ actions/current_invoker.cpp
+ actions/future.cpp
+ actions/invoker_detail.cpp
+ actions/invoker_pool.cpp
+ actions/invoker_util.cpp
+
+ bus/public.cpp
+
+ bus/tcp/connection.cpp
+ bus/tcp/dispatcher.cpp
+ bus/tcp/dispatcher_impl.cpp
+ bus/tcp/config.cpp
+ bus/tcp/packet.cpp
+ bus/tcp/client.cpp
+ bus/tcp/server.cpp
+
+ compression/brotli.cpp
+ compression/bzip2.cpp
+ compression/codec.cpp
+ compression/stream.cpp
+ compression/lz.cpp
+ compression/lzma.cpp
+ compression/public.cpp
+ compression/snappy.cpp
+ compression/zlib.cpp
+ compression/zstd.cpp
+
+ concurrency/action_queue.cpp
+ concurrency/async_barrier.cpp
+ concurrency/async_semaphore.cpp
+ concurrency/async_rw_lock.cpp
+ concurrency/async_stream.cpp
+ concurrency/async_stream_pipe.cpp
+ concurrency/coroutine.cpp
+ concurrency/config.cpp
+ concurrency/delayed_executor.cpp
+ concurrency/execution_stack.cpp
+ concurrency/fair_share_action_queue.cpp
+ concurrency/fair_share_invoker_pool.cpp
+ concurrency/fair_share_invoker_queue.cpp
+ concurrency/fair_share_thread_pool.cpp
+ concurrency/fair_share_queue_scheduler_thread.cpp
+ concurrency/fair_throttler.cpp
+ concurrency/fiber.cpp
+ concurrency/fiber_scheduler_thread.cpp
+ concurrency/fls.cpp
+ concurrency/new_fair_share_thread_pool.cpp
+ concurrency/notify_manager.cpp
+ concurrency/invoker_alarm.cpp
+ concurrency/invoker_queue.cpp
+ concurrency/periodic_executor.cpp
+ concurrency/periodic_yielder.cpp
+ concurrency/pollable_detail.cpp
+ concurrency/profiling_helpers.cpp
+ concurrency/propagating_storage.cpp
+ concurrency/quantized_executor.cpp
+ concurrency/scheduler_thread.cpp
+ concurrency/single_queue_scheduler_thread.cpp
+ concurrency/suspendable_action_queue.cpp
+ concurrency/system_invokers.cpp
+ concurrency/thread_affinity.cpp
+ concurrency/thread_pool.cpp
+ concurrency/thread_pool_detail.cpp
+ concurrency/thread_pool_poller.cpp
+ concurrency/throughput_throttler.cpp
+ concurrency/two_level_fair_share_thread_pool.cpp
+ concurrency/lease_manager.cpp
+
+ logging/compression.cpp
+ logging/config.cpp
+ logging/formatter.cpp
+ logging/fluent_log.cpp
+ GLOBAL logging/log.cpp
+ logging/log_manager.cpp
+ logging/logger_owner.cpp
+ logging/pattern.cpp
+ logging/serializable_logger.cpp
+ logging/stream_output.cpp
+ logging/log_writer_detail.cpp
+ logging/file_log_writer.cpp
+ logging/stream_log_writer.cpp
+ logging/random_access_gzip.cpp
+ logging/zstd_compression.cpp
+
+ misc/arithmetic_formula.cpp
+ GLOBAL misc/assert.cpp
+ misc/backoff_strategy.cpp
+ misc/backoff_strategy_config.cpp
+ misc/bitmap.cpp
+ misc/bit_packed_unsigned_vector.cpp
+ misc/bit_packing.cpp
+ misc/blob_output.cpp
+ misc/bloom_filter.cpp
+ misc/checksum.cpp
+ misc/config.cpp
+ misc/coro_pipe.cpp
+ misc/crash_handler.cpp
+ misc/digest.cpp
+ misc/dnf.cpp
+ misc/error.cpp
+ misc/error_code.cpp
+ misc/ema_counter.cpp
+ misc/fs.cpp
+ # NB: it is necessary to prevent linker optimization of
+ # REGISTER_INTERMEDIATE_PROTO_INTEROP_REPRESENTATION macros for TGuid.
+ GLOBAL misc/guid.cpp
+ misc/hazard_ptr.cpp
+ misc/hedging_manager.cpp
+ misc/histogram.cpp
+ misc/historic_usage_aggregator.cpp
+ misc/hr_timer.cpp
+ misc/id_generator.cpp
+ misc/linear_probe.cpp
+ misc/memory_reference_tracker.cpp
+ misc/memory_usage_tracker.cpp
+ misc/relaxed_mpsc_queue.cpp
+ misc/parser_helpers.cpp
+ misc/pattern_formatter.cpp
+ misc/phoenix.cpp
+ misc/pool_allocator.cpp
+ misc/proc.cpp
+ misc/protobuf_helpers.cpp
+ misc/public.cpp
+ misc/random.cpp
+ misc/ref_counted_tracker.cpp
+ misc/ref_counted_tracker_statistics_producer.cpp
+ misc/ref_counted_tracker_profiler.cpp
+ GLOBAL misc/ref_tracked.cpp
+ misc/serialize.cpp
+ misc/shutdown.cpp
+ misc/signal_registry.cpp
+ misc/slab_allocator.cpp
+ misc/statistics.cpp
+ misc/cache_config.cpp
+ misc/utf8_decoder.cpp
+ misc/zerocopy_output_writer.cpp
+
+ net/address.cpp
+ net/connection.cpp
+ net/config.cpp
+ net/dialer.cpp
+ net/helpers.cpp
+ net/listener.cpp
+ net/local_address.cpp
+ net/public.cpp
+ net/socket.cpp
+
+ dns/ares_dns_resolver.cpp
+ dns/dns_resolver.cpp
+
+ profiling/timing.cpp
+
+ rpc/authentication_identity.cpp
+ rpc/authenticator.cpp
+ rpc/balancing_channel.cpp
+ rpc/caching_channel_factory.cpp
+ rpc/channel_detail.cpp
+ rpc/client.cpp
+ rpc/config.cpp
+ rpc/dispatcher.cpp
+ rpc/dynamic_channel_pool.cpp
+ rpc/hedging_channel.cpp
+ rpc/helpers.cpp
+ rpc/local_channel.cpp
+ rpc/local_server.cpp
+ rpc/message.cpp
+ rpc/message_format.cpp
+ rpc/null_channel.cpp
+ rpc/per_user_request_queue_provider.cpp
+ rpc/protocol_version.cpp
+ rpc/public.cpp
+ rpc/request_queue_provider.cpp
+ rpc/response_keeper.cpp
+ rpc/retrying_channel.cpp
+ rpc/roaming_channel.cpp
+ rpc/serialized_channel.cpp
+ rpc/server_detail.cpp
+ rpc/service.cpp
+ rpc/service_detail.cpp
+ rpc/static_channel_factory.cpp
+ rpc/stream.cpp
+ rpc/throttling_channel.cpp
+ rpc/viable_peer_registry.cpp
+
+ rpc/bus/server.cpp
+ rpc/bus/channel.cpp
+
+ service_discovery/service_discovery.cpp
+
+ threading/spin_wait_slow_path_logger.cpp
+ threading/thread.cpp
+
+ GLOBAL tracing/allocation_hooks.cpp
+ tracing/allocation_tags.cpp
+ tracing/config.cpp
+ tracing/public.cpp
+ GLOBAL tracing/trace_context.cpp
+
+ utilex/random.cpp
+
+ ypath/stack.cpp
+ ypath/token.cpp
+ ypath/tokenizer.cpp
+ ypath/helpers.cpp
+
+ yson/async_consumer.cpp
+ yson/async_writer.cpp
+ yson/attribute_consumer.cpp
+ yson/consumer.cpp
+ yson/forwarding_consumer.cpp
+ yson/lexer.cpp
+ yson/null_consumer.cpp
+ yson/parser.cpp
+ yson/producer.cpp
+ yson/protobuf_interop.cpp
+ yson/protobuf_interop_options.cpp
+ yson/protobuf_interop_unknown_fields.cpp
+ yson/pull_parser.cpp
+ yson/pull_parser_deserialize.cpp
+ yson/stream.cpp
+ yson/string.cpp
+ yson/string_filter.cpp
+ yson/syntax_checker.cpp
+ yson/token.cpp
+ yson/token_writer.cpp
+ yson/tokenizer.cpp
+ yson/writer.cpp
+ yson/string_merger.cpp
+ yson/ypath_designated_consumer.cpp
+ yson/depth_limiting_yson_consumer.cpp
+ yson/attributes_stripper.cpp
+
+ ytree/attribute_consumer.cpp
+ ytree/helpers.cpp
+ ytree/attributes.cpp
+ ytree/attribute_filter.cpp
+ ytree/convert.cpp
+ ytree/ephemeral_attribute_owner.cpp
+ ytree/ephemeral_node_factory.cpp
+ ytree/exception_helpers.cpp
+ ytree/interned_attributes.cpp
+ ytree/node.cpp
+ ytree/node_detail.cpp
+ ytree/permission.cpp
+ ytree/request_complexity_limiter.cpp
+ ytree/serialize.cpp
+ ytree/static_service_dispatcher.cpp
+ ytree/system_attribute_provider.cpp
+ ytree/tree_builder.cpp
+ ytree/tree_visitor.cpp
+ ytree/virtual.cpp
+ ytree/service_combiner.cpp
+ ytree/ypath_client.cpp
+ ytree/ypath_detail.cpp
+ ytree/ypath_resolver.cpp
+ ytree/ypath_service.cpp
+ ytree/yson_serializable.cpp
+ ytree/yson_struct.cpp
+ ytree/yson_struct_detail.cpp
+
+ json/config.cpp
+ json/json_callbacks.cpp
+ json/helpers.cpp
+ json/json_parser.cpp
+ json/json_writer.cpp
+
+ ytalloc/bindings.cpp
+ ytalloc/config.cpp
+ ytalloc/statistics_producer.cpp
+)
+
+IF (OS_LINUX OR OS_FREEBSD)
+ EXTRALIBS(-lutil)
+ENDIF()
+
+PEERDIR(
+ contrib/libs/snappy
+ contrib/libs/zlib
+ contrib/libs/zstd
+ contrib/libs/lzmasdk
+ contrib/libs/libbz2
+ contrib/libs/c-ares
+ contrib/libs/farmhash
+ contrib/libs/yajl
+ contrib/libs/lz4
+
+ library/cpp/threading/thread_local
+ library/cpp/streams/brotli
+ library/cpp/yt/assert
+ library/cpp/yt/containers
+ library/cpp/yt/logging
+ library/cpp/yt/misc
+ library/cpp/yt/memory
+ library/cpp/yt/string
+ library/cpp/yt/yson
+ library/cpp/yt/yson_string
+ library/cpp/ytalloc/api
+
+ yt/yt/build
+ yt/yt/core/misc/isa_crc64
+
+ yt/yt_proto/yt/core
+
+ library/cpp/yt/assert
+ library/cpp/yt/backtrace
+ library/cpp/yt/coding
+ library/cpp/yt/malloc
+ library/cpp/yt/small_containers
+ library/cpp/yt/system
+ library/cpp/yt/threading
+
+ yt/yt/library/syncmap
+ yt/yt/library/undumpable
+ yt/yt/library/ytprof/api
+
+ # TODO(prime@): remove this, once yt/core is split into separate libraries.
+ yt/yt/library/profiling
+ yt/yt/library/profiling/resource_tracker
+ yt/yt/library/tracing
+)
+
+IF (OS_WINDOWS)
+ PEERDIR(
+ library/cpp/yt/backtrace/cursors/dummy
+ )
+ELSE()
+ PEERDIR(
+ library/cpp/yt/backtrace/cursors/libunwind
+ )
+ENDIF()
+
+END()
+
+RECURSE(
+ http
+)
+
+IF (NOT OPENSOURCE)
+ RECURSE(
+ benchmarks
+ bus/benchmarks
+ )
+ENDIF()
+
+RECURSE_FOR_TESTS(
+ actions/unittests
+ concurrency/unittests
+ http/unittests
+ misc/unittests
+ net/unittests
+ yson/unittests
+ http/mock
+ net/mock
+)
+
+IF (NOT OS_WINDOWS)
+ RECURSE(
+ misc/isa_crc64
+ service_discovery/yp
+ )
+
+ RECURSE_FOR_TESTS(
+ bus/unittests
+ compression/unittests
+ crypto/unittests
+ json/unittests
+ logging/unittests
+ profiling/unittests
+ rpc/unittests
+ ypath/unittests
+ ytree/unittests
+ )
+ENDIF()
diff --git a/yt/yt/core/ypath/helpers-inl.h b/yt/yt/core/ypath/helpers-inl.h
new file mode 100644
index 0000000000..6a318a888d
--- /dev/null
+++ b/yt/yt/core/ypath/helpers-inl.h
@@ -0,0 +1,56 @@
+#ifndef HELPERS_INL_H
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+
+#include "token.h"
+
+#include <library/cpp/yt/string/string_builder.h>
+#include <library/cpp/yt/string/format.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <typename ...TArgs>
+void YPathJoinImpl(TStringBuilder* builder, TArgs&&... literals);
+
+template <>
+inline void YPathJoinImpl(TStringBuilder* /*builder*/)
+{ }
+
+template <typename TFirstArg, typename ...TArgs>
+void YPathJoinImpl(TStringBuilder* builder, TFirstArg&& firstLiteral, TArgs&&... literals)
+{
+ builder->AppendChar('/');
+ AppendYPathLiteral(builder, std::forward<TFirstArg>(firstLiteral));
+ YPathJoinImpl(builder, std::forward<TArgs>(literals)...);
+}
+
+} // namespace NDetail
+
+template <typename ...TArgs>
+TYPath YPathJoin(const TYPath& path, TArgs&&... literals)
+{
+ TStringBuilder builder;
+
+ auto tryGetLength = [] (const auto& literal) {
+ if constexpr (requires { literal.length(); }) {
+ return literal.length();
+ } else {
+ return 1;
+ }
+ };
+ builder.Reserve(path.length() + (sizeof...(literals) + ... + tryGetLength(literals)));
+
+ builder.AppendString(path);
+ NDetail::YPathJoinImpl(&builder, literals...);
+ return builder.Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/helpers.cpp b/yt/yt/core/ypath/helpers.cpp
new file mode 100644
index 0000000000..3a8675a3b3
--- /dev/null
+++ b/yt/yt/core/ypath/helpers.cpp
@@ -0,0 +1,80 @@
+#include "helpers.h"
+
+#include "tokenizer.h"
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TYPath> TryComputeYPathSuffix(const TYPath& path, const TYPath& prefix)
+{
+ // TODO(babenko): this check is pessimistic; consider using tokenizer
+ if (!path.StartsWith(prefix)) {
+ return std::nullopt;
+ }
+
+ if (path.length() == prefix.length()) {
+ return TYPath();
+ }
+
+ if (path[prefix.length()] != '/') {
+ return std::nullopt;
+ }
+
+ return path.substr(prefix.length());
+}
+
+std::pair<TYPath, TString> DirNameAndBaseName(const TYPath& path)
+{
+ if (path.empty()) {
+ return {};
+ }
+ for (TTokenizer tokenizer(path); tokenizer.GetType() != ETokenType::EndOfStream; tokenizer.Advance()) {
+ if (tokenizer.GetSuffix().empty()) {
+ const auto& prefix = tokenizer.GetPrefix();
+ TYPath dirName;
+ if (prefix.ends_with('/')) {
+ // Strip trailing '/'.
+ dirName = prefix.substr(0, prefix.size() - 1);
+ } else {
+ dirName = prefix;
+ }
+ const auto& token = tokenizer.GetToken();
+ return {dirName, TString(token)};
+ }
+ }
+ Y_UNREACHABLE();
+}
+
+bool IsPathPointingToAttributes(const TYPath& path)
+{
+ for (TTokenizer tokenizer(path); tokenizer.GetType() != ETokenType::EndOfStream; tokenizer.Advance()) {
+ if (tokenizer.GetType() == ETokenType::At) {
+ return true;
+ }
+ }
+ return false;
+}
+
+TYPath StripAttributes(const TYPath& path)
+{
+ if (path.empty()) {
+ return {};
+ }
+ for (TTokenizer tokenizer(path); tokenizer.GetType() != ETokenType::EndOfStream; tokenizer.Advance()) {
+ if (tokenizer.GetType() == ETokenType::At) {
+ const auto& prefix = tokenizer.GetPrefix();
+ if (prefix.ends_with('/')) {
+ // Strip trailing '/'.
+ return TYPath(prefix.substr(0, prefix.size() - 1));
+ } else {
+ return TYPath(prefix);
+ }
+ }
+ }
+ return path;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/helpers.h b/yt/yt/core/ypath/helpers.h
new file mode 100644
index 0000000000..ca675f4eaf
--- /dev/null
+++ b/yt/yt/core/ypath/helpers.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TYPath> TryComputeYPathSuffix(const TYPath& path, const TYPath& prefix);
+
+//! Split path into dirname part and basename part (a-la corresponding bash commands).
+//! BaseName part is considered to be the last non-empty YPath token.
+//! DirName part is stripped off trailing slash (if any).
+std::pair<TYPath, TString> DirNameAndBaseName(const TYPath& path);
+
+//! Check if path contains attribute designation by looking for @ token in it.
+bool IsPathPointingToAttributes(const TYPath& path);
+
+//! Eliminates path suffix after the @ token, if it exists.
+TYPath StripAttributes(const TYPath& path);
+
+//! NB! Escapes special characters in literals.
+template <typename ...TArgs>
+TYPath YPathJoin(const TYPath& path, TArgs&&... literals);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
+
+#define HELPERS_INL_H
+#include "helpers-inl.h"
+#undef HELPERS_INL_H
diff --git a/yt/yt/core/ypath/public.h b/yt/yt/core/ypath/public.h
new file mode 100644
index 0000000000..9f0a811aa6
--- /dev/null
+++ b/yt/yt/core/ypath/public.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class ETokenType;
+class TTokenizer;
+class TRichYPath;
+
+using TYPath = TString;
+using TYPathBuf = TStringBuf;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/stack.cpp b/yt/yt/core/ypath/stack.cpp
new file mode 100644
index 0000000000..c236d47d20
--- /dev/null
+++ b/yt/yt/core/ypath/stack.cpp
@@ -0,0 +1,91 @@
+#include "stack.h"
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <library/cpp/yt/misc/variant.h>
+#include <library/cpp/yt/misc/cast.h>
+
+#include <library/cpp/yt/string/string.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYPathStack::Push(TStringBuf key)
+{
+ PreviousPathLengths_.push_back(Path_.size());
+ Path_ += "/";
+ Path_ += ToYPathLiteral(key);
+ Items_.emplace_back(TString(key));
+}
+
+void TYPathStack::Push(int index)
+{
+ PreviousPathLengths_.push_back(Path_.size());
+ Path_ += "/";
+ Path_ += ToString(index);
+ Items_.emplace_back(index);
+}
+
+void TYPathStack::IncreaseLastIndex()
+{
+ YT_VERIFY(!Items_.empty());
+ YT_VERIFY(std::holds_alternative<int>(Items_.back()));
+ auto index = std::get<int>(Items_.back());
+ Pop();
+ ++index;
+ Push(index);
+}
+
+void TYPathStack::Pop()
+{
+ YT_VERIFY(!Items_.empty());
+ Items_.pop_back();
+ Path_.resize(PreviousPathLengths_.back());
+ PreviousPathLengths_.pop_back();
+}
+
+bool TYPathStack::IsEmpty() const
+{
+ return Items_.empty();
+}
+
+const TYPath& TYPathStack::GetPath() const
+{
+ return Path_;
+}
+
+TString TYPathStack::GetHumanReadablePath() const
+{
+ auto path = GetPath();
+ if (path.empty()) {
+ static const TString Root("(root)");
+ return Root;
+ }
+ return path;
+}
+
+std::optional<TString> TYPathStack::TryGetStringifiedLastPathToken() const
+{
+ if (Items_.empty()) {
+ return {};
+ }
+ return ToString(Items_.back());
+}
+
+TString TYPathStack::ToString(const TYPathStack::TEntry& item)
+{
+ return Visit(item,
+ [&] (const TString& string) {
+ return string;
+ },
+ [&] (int integer) {
+ return ::ToString(integer);
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/stack.h b/yt/yt/core/ypath/stack.h
new file mode 100644
index 0000000000..e7b8bfa878
--- /dev/null
+++ b/yt/yt/core/ypath/stack.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/property.h>
+
+#include <variant>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathStack
+{
+public:
+ using TEntry = std::variant<
+ TString,
+ int>;
+
+ DEFINE_BYREF_RO_PROPERTY(std::vector<TEntry>, Items);
+
+public:
+ void Push(TStringBuf key);
+ void Push(int index);
+ void IncreaseLastIndex();
+ void Pop();
+ bool IsEmpty() const;
+ const TYPath& GetPath() const;
+ TString GetHumanReadablePath() const;
+ std::optional<TString> TryGetStringifiedLastPathToken() const;
+
+private:
+ std::vector<size_t> PreviousPathLengths_;
+ TString Path_;
+
+ static TString ToString(const TEntry& entry);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/token-inl.h b/yt/yt/core/ypath/token-inl.h
new file mode 100644
index 0000000000..ef1db84f2f
--- /dev/null
+++ b/yt/yt/core/ypath/token-inl.h
@@ -0,0 +1,27 @@
+#ifndef TOKEN_INL_H_
+#error "Direct inclusion of this file is not allowed, include token.h"
+// For the sake of sane code completion.
+#include "token.h"
+#endif
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class E>
+ requires TEnumTraits<E>::IsEnum
+TString ToYPathLiteral(E value)
+{
+ return FormatEnum(value);
+}
+
+template <class T, class TTag>
+TString ToYPathLiteral(const TStrongTypedef<T, TTag>& value)
+{
+ return ToYPathLiteral(value.Underlying());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
+
diff --git a/yt/yt/core/ypath/token.cpp b/yt/yt/core/ypath/token.cpp
new file mode 100644
index 0000000000..b211fd70fa
--- /dev/null
+++ b/yt/yt/core/ypath/token.cpp
@@ -0,0 +1,104 @@
+#include "token.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/string/guid.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TStringBuf ListBeginToken("begin");
+const TStringBuf ListEndToken("end");
+const TStringBuf ListBeforeToken("before:");
+const TStringBuf ListAfterToken("after:");
+
+bool IsSpecialListKey(TStringBuf key)
+{
+ return
+ key == ListBeginToken ||
+ key == ListEndToken ||
+ key.StartsWith(ListBeforeToken) ||
+ key.StartsWith(ListAfterToken);
+}
+
+TStringBuf ExtractListIndex(TStringBuf token)
+{
+ if (token[0] >= '0' && token[0] <= '9') {
+ return token;
+ } else {
+ auto colonIndex = token.find(':');
+ if (colonIndex == TStringBuf::npos) {
+ return token;
+ } else {
+ return TStringBuf(token.begin() + colonIndex + 1, token.end());
+ }
+ }
+}
+
+int ParseListIndex(TStringBuf token)
+{
+ try {
+ return FromString<int>(token);
+ } catch (const std::exception&) {
+ THROW_ERROR_EXCEPTION("Invalid list index %Qv",
+ token);
+ }
+}
+
+std::optional<int> TryAdjustListIndex(int index, int count)
+{
+ int adjustedIndex = index >= 0 ? index : index + count;
+ if (adjustedIndex < 0 || adjustedIndex >= count) {
+ return std::nullopt;
+ }
+ return adjustedIndex;
+}
+
+TString ToYPathLiteral(TStringBuf value)
+{
+ TStringBuilder builder;
+ AppendYPathLiteral(&builder, value);
+ return builder.Flush();
+}
+
+TString ToYPathLiteral(i64 value)
+{
+ return ToString(value);
+}
+
+TString ToYPathLiteral(TGuid value)
+{
+ return ToString(value);
+}
+
+void AppendYPathLiteral(TStringBuilderBase* builder, TStringBuf value)
+{
+ builder->Preallocate(value.length() + 16);
+ for (unsigned char ch : value) {
+ if (IsSpecialCharacter(ch)) {
+ builder->AppendChar('\\');
+ builder->AppendChar(ch);
+ } else if (ch < 32 || ch > 127) {
+ builder->AppendString(TStringBuf("\\x"));
+ builder->AppendChar(IntToHexLowercase[ch >> 4]);
+ builder->AppendChar(IntToHexLowercase[ch & 0xf]);
+ } else {
+ builder->AppendChar(ch);
+ }
+ }
+}
+
+void AppendYPathLiteral(TStringBuilderBase* builder, i64 value)
+{
+ builder->AppendFormat("%v", value);
+}
+
+bool IsSpecialCharacter(char ch)
+{
+ return ch == '\\' || ch == '/' || ch == '@' || ch == '*' || ch == '&' || ch == '[' || ch == '{';
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/token.h b/yt/yt/core/ypath/token.h
new file mode 100644
index 0000000000..80f32b70bb
--- /dev/null
+++ b/yt/yt/core/ypath/token.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+extern const TStringBuf ListBeginToken;
+extern const TStringBuf ListEndToken;
+extern const TStringBuf ListBeforeToken;
+extern const TStringBuf ListAfterToken;
+
+DEFINE_ENUM(ETokenType,
+ (Literal)
+ (Slash)
+ (Ampersand)
+ (At)
+ (Asterisk)
+ (StartOfStream)
+ (EndOfStream)
+ (Range)
+);
+
+TString ToYPathLiteral(TStringBuf value);
+TString ToYPathLiteral(i64 value);
+TString ToYPathLiteral(TGuid value);
+template <class E>
+ requires TEnumTraits<E>::IsEnum
+TString ToYPathLiteral(E value);
+
+template <class T, class TTag>
+TString ToYPathLiteral(const TStrongTypedef<T, TTag>& value);
+
+void AppendYPathLiteral(TStringBuilderBase* builder, TStringBuf value);
+void AppendYPathLiteral(TStringBuilderBase* builder, i64 value);
+
+TStringBuf ExtractListIndex(TStringBuf token);
+int ParseListIndex(TStringBuf token);
+std::optional<int> TryAdjustListIndex(int index, int count);
+
+bool IsSpecialListKey(TStringBuf key);
+bool IsSpecialCharacter(char ch);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
+
+#define TOKEN_INL_H_
+#include "token-inl.h"
+#undef TOKEN_INL_H_
diff --git a/yt/yt/core/ypath/tokenizer.cpp b/yt/yt/core/ypath/tokenizer.cpp
new file mode 100644
index 0000000000..86a2183dc9
--- /dev/null
+++ b/yt/yt/core/ypath/tokenizer.cpp
@@ -0,0 +1,273 @@
+#include "tokenizer.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/token.h>
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTokenizer::TTokenizer(TYPathBuf path)
+{
+ Reset(path);
+}
+
+void TTokenizer::Reset(TYPathBuf path)
+{
+ Path_ = path;
+ Type_ = ETokenType::StartOfStream;
+ PreviousType_ = ETokenType::StartOfStream;
+ Token_ = TStringBuf();
+ Input_ = Path_;
+
+ LiteralValue_.clear();
+ LiteralValue_.reserve(Path_.length());
+}
+
+ETokenType TTokenizer::Advance()
+{
+ // Replace Input_ with suffix.
+ Input_ = TStringBuf(Input_.begin() + Token_.length(), Input_.end());
+ LiteralValue_.clear();
+
+ // Check for EndOfStream.
+ const char* current = Input_.begin();
+ if (current == Input_.end()) {
+ SetType(ETokenType::EndOfStream);
+ Token_ = TStringBuf();
+ return Type_;
+ }
+
+ SetType(ETokenType::Literal);
+ bool proceed = true;
+ while (proceed && current != Input_.end()) {
+ auto token = NYson::CharToTokenType(*current);
+ if (token == NYson::ETokenType::LeftBracket ||
+ token == NYson::ETokenType::LeftBrace)
+ {
+ if (current == Input_.begin()) {
+ SetType(ETokenType::Range);
+ current = Input_.end();
+ }
+ proceed = false;
+ continue;
+ }
+
+ switch (*current) {
+ case '/':
+ case '@':
+ case '&':
+ case '*':
+ if (current == Input_.begin()) {
+ Token_ = TStringBuf(current, current + 1);
+ switch (*current) {
+ case '/': SetType(ETokenType::Slash); break;
+ case '@': SetType(ETokenType::At); break;
+ case '&': SetType(ETokenType::Ampersand); break;
+ case '*': SetType(ETokenType::Asterisk); break;
+ default: YT_ABORT();
+ }
+ return Type_;
+ }
+ proceed = false;
+ break;
+
+ case '\\':
+ current = AdvanceEscaped(current);
+ break;
+
+ default:
+ LiteralValue_.append(*current);
+ ++current;
+ break;
+ }
+ }
+
+ Token_ = TStringBuf(Input_.begin(), current);
+ return Type_;
+}
+
+void TTokenizer::ThrowMalformedEscapeSequence(TStringBuf context)
+{
+ THROW_ERROR_EXCEPTION("Malformed escape sequence %Qv in YPath",
+ context);
+}
+
+void TTokenizer::SetType(ETokenType type)
+{
+ PreviousType_ = Type_;
+ Type_ = type;
+}
+
+const char* TTokenizer::AdvanceEscaped(const char* current)
+{
+ YT_ASSERT(*current == '\\');
+ ++current;
+
+ if (current == Input_.end()) {
+ THROW_ERROR_EXCEPTION("Unexpected end-of-string in YPath while parsing escape sequence");
+ }
+
+ if (IsSpecialCharacter(*current)) {
+ LiteralValue_.append(*current);
+ ++current;
+ } else {
+ if (*current == 'x') {
+ if (current + 2 >= Input_.end()) {
+ ThrowMalformedEscapeSequence(TStringBuf(current - 1, Input_.end()));
+ }
+ TStringBuf context(current - 1, current + 3);
+ int hi = ParseHexDigit(current[1], context);
+ int lo = ParseHexDigit(current[2], context);
+ LiteralValue_.append((hi << 4) + lo);
+ current = context.end();
+ } else {
+ ThrowMalformedEscapeSequence(TStringBuf(current - 1, current + 1));
+ }
+ }
+
+ return current;
+}
+
+int TTokenizer::ParseHexDigit(char ch, TStringBuf context)
+{
+ if (ch >= '0' && ch <= '9') {
+ return ch - '0';
+ }
+
+ if (ch >= 'a' && ch <= 'f') {
+ return ch - 'a' + 10;
+ }
+
+ if (ch >= 'A' && ch <= 'F') {
+ return ch - 'A' + 10;
+ }
+
+ ThrowMalformedEscapeSequence(context);
+ YT_ABORT();
+}
+
+void TTokenizer::Expect(ETokenType expectedType)
+{
+ if (expectedType != Type_) {
+ if (Type_ == ETokenType::EndOfStream) {
+ if (PreviousType_ == ETokenType::Slash) {
+ THROW_ERROR_EXCEPTION("Expected %Qlv in YPath but found end-of-string; please note that YPath cannot "
+ "normally end with \"/\"",
+ expectedType);
+ } else {
+ THROW_ERROR_EXCEPTION("Expected %Qlv in YPath but found end-of-string",
+ expectedType);
+ }
+ } else {
+ THROW_ERROR_EXCEPTION("Expected %Qlv in YPath but found %Qlv token %Qv",
+ expectedType,
+ Type_,
+ Token_);
+ }
+ }
+}
+
+void TTokenizer::ExpectListIndex()
+{
+ Expect(NYPath::ETokenType::Literal);
+ i64 index;
+ const auto& token = GetLiteralValue();
+ if (!IsSpecialListKey(token) && !TryFromString(token, index)) {
+ THROW_ERROR_EXCEPTION("Expected special list key or integer for repeated field index, %Qv found", token)
+ << TErrorAttribute("ypath", GetPrefixPlusToken());
+ }
+}
+
+bool TTokenizer::Skip(ETokenType expectedType)
+{
+ if (Type_ == expectedType) {
+ Advance();
+ return true;
+ }
+
+ return false;
+}
+
+void TTokenizer::ThrowUnexpected()
+{
+ if (Type_ == ETokenType::EndOfStream) {
+ if (PreviousType_ == ETokenType::Slash) {
+ THROW_ERROR_EXCEPTION("Unexpected end-of-string in YPath; please note that YPath cannot "
+ "normally end with \"/\"");
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected end-of-string in YPath");
+ }
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected %Qlv token %Qv in YPath",
+ Type_,
+ Token_);
+ }
+}
+
+ETokenType TTokenizer::GetType() const
+{
+ return Type_;
+}
+
+TStringBuf TTokenizer::GetToken() const
+{
+ return Token_;
+}
+
+TStringBuf TTokenizer::GetPrefix() const
+{
+ return TStringBuf(Path_.begin(), Input_.begin());
+}
+
+TStringBuf TTokenizer::GetPrefixPlusToken() const
+{
+ return TStringBuf(Path_.begin(), Input_.begin() + Token_.length());
+}
+
+TStringBuf TTokenizer::GetSuffix() const
+{
+ return TStringBuf(Input_.begin() + Token_.length(), Input_.end());
+}
+
+TStringBuf TTokenizer::GetInput() const
+{
+ return Input_;
+}
+
+TStringBuf TTokenizer::GetPath() const
+{
+ return Path_;
+}
+
+const TString& TTokenizer::GetLiteralValue() const
+{
+ return LiteralValue_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool HasPrefix(const TYPath& fullPath, const TYPath& prefixPath)
+{
+ TTokenizer fullTokenizer(fullPath);
+ TTokenizer prefixTokenizer(prefixPath);
+
+ while (true) {
+ if (prefixTokenizer.Advance() == ETokenType::EndOfStream) {
+ return true;
+ }
+
+ if (fullTokenizer.Advance() == ETokenType::EndOfStream) {
+ return false;
+ }
+
+ if (prefixTokenizer.GetToken() != fullTokenizer.GetToken()) {
+ return false;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/tokenizer.h b/yt/yt/core/ypath/tokenizer.h
new file mode 100644
index 0000000000..27725db10a
--- /dev/null
+++ b/yt/yt/core/ypath/tokenizer.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "token.h"
+
+namespace NYT::NYPath {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTokenizer
+{
+public:
+ explicit TTokenizer(TYPathBuf path = {});
+
+ TTokenizer(const TTokenizer&) = delete;
+ TTokenizer& operator=(const TTokenizer&) = delete;
+
+ void Reset(TYPathBuf path);
+
+ ETokenType Advance();
+
+ ETokenType GetType() const;
+ TStringBuf GetToken() const;
+ TStringBuf GetPrefix() const;
+ TStringBuf GetPrefixPlusToken() const;
+ TStringBuf GetSuffix() const;
+ TStringBuf GetInput() const;
+ TStringBuf GetPath() const;
+ const TString& GetLiteralValue() const;
+
+ void Expect(ETokenType expectedType);
+ void ExpectListIndex();
+ bool Skip(ETokenType expectedType);
+ [[noreturn]] void ThrowUnexpected();
+
+private:
+ TYPathBuf Path_;
+
+ ETokenType Type_;
+ ETokenType PreviousType_;
+ TStringBuf Token_;
+ TStringBuf Input_;
+ TString LiteralValue_;
+
+ void SetType(ETokenType type);
+ const char* AdvanceEscaped(const char* current);
+ static int ParseHexDigit(char ch, TStringBuf context);
+ static void ThrowMalformedEscapeSequence(TStringBuf context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool HasPrefix(const TYPath& fullPath, const TYPath& prefixPath);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/unittests/tokenizer_ut.cpp b/yt/yt/core/ypath/unittests/tokenizer_ut.cpp
new file mode 100644
index 0000000000..913646c0fa
--- /dev/null
+++ b/yt/yt/core/ypath/unittests/tokenizer_ut.cpp
@@ -0,0 +1,233 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+#include <yt/yt/core/ypath/helpers.h>
+
+#include <util/string/vector.h>
+
+namespace NYT::NYPath {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathTokenizerTest
+ : public ::testing::Test
+{
+private:
+ std::unique_ptr<TTokenizer> Tokenizer;
+ std::vector<ETokenType> TokenTypes;
+ std::vector<TString> Literals;
+
+public:
+ void Prepare(const char* input)
+ {
+ Tokenizer.reset(new TTokenizer(input));
+ TokenTypes.clear();
+ Literals.clear();
+ }
+
+ bool Tokenize()
+ {
+ if (Tokenizer->Advance() == ETokenType::EndOfStream) {
+ return false;
+ }
+
+ TokenTypes.push_back(Tokenizer->GetType());
+ if (Tokenizer->GetType() == ETokenType::Literal) {
+ Literals.push_back(Tokenizer->GetLiteralValue());
+ }
+ return true;
+ }
+
+ void PrepareAndTokenize(const char* input)
+ {
+ Prepare(input);
+ while (Tokenize());
+ }
+
+ TString GetFlattenedTokens()
+ {
+ TString result;
+ result.reserve(TokenTypes.size());
+ for (auto type : TokenTypes) {
+ switch (type) {
+ case ETokenType::Literal: result.append('L'); break;
+ case ETokenType::Slash: result.append('/'); break;
+ case ETokenType::Ampersand: result.append('&'); break;
+ case ETokenType::At: result.append('@'); break;
+ default: break;
+ }
+ }
+ return result;
+ }
+
+ const std::vector<TString>& GetLiterals()
+ {
+ return Literals;
+ }
+};
+
+TEST_F(TYPathTokenizerTest, SimpleCase1)
+{
+ PrepareAndTokenize("hello");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "hello"), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SimpleCase2)
+{
+ PrepareAndTokenize("/");
+ EXPECT_EQ("/", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SimpleCase3)
+{
+ PrepareAndTokenize("&");
+ EXPECT_EQ("&", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SimpleCase4)
+{
+ PrepareAndTokenize("@");
+ EXPECT_EQ("@", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SimpleCase5)
+{
+ PrepareAndTokenize("&//@@&&@/&"); // There are all pairs within this string.
+ EXPECT_EQ("&//@@&&@/&", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SimpleCase6)
+{
+ PrepareAndTokenize("hello/cruel@world&");
+ EXPECT_EQ("L/L@L&", GetFlattenedTokens());
+
+ std::vector<TString> expectedLiterals;
+ expectedLiterals.push_back("hello");
+ expectedLiterals.push_back("cruel");
+ expectedLiterals.push_back("world");
+ EXPECT_EQ(expectedLiterals, GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, SeeminglyImpossibleLiteral)
+{
+ const char* string = "Hello, cruel world; I am here to destroy you.";
+ PrepareAndTokenize(string);
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, string), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, IntegersAndDoubles)
+{
+ PrepareAndTokenize("0123456789.01234567890");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "0123456789.01234567890"), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, GUID)
+{
+ PrepareAndTokenize("c61834-c8f650dc-90b0dfdc-21c02eed");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "c61834-c8f650dc-90b0dfdc-21c02eed"), GetLiterals());
+
+ PrepareAndTokenize("000000-11111111-22222222-33333333");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "000000-11111111-22222222-33333333"), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, EscapedSpecial)
+{
+ PrepareAndTokenize("\\@\\&\\/\\\\\\[\\{");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "@&/\\[{"), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, EscapedHex)
+{
+ PrepareAndTokenize(
+"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f"
+"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f"
+"\\x20\\x21\\x22\\x23\\x24\\x25\\x26\\x27\\x28\\x29\\x2a\\x2b\\x2c\\x2d\\x2e\\x2f"
+"\\x30\\x31\\x32\\x33\\x34\\x35\\x36\\x37\\x38\\x39\\x3a\\x3b\\x3c\\x3d\\x3e\\x3f"
+"\\x40\\x41\\x42\\x43\\x44\\x45\\x46\\x47\\x48\\x49\\x4a\\x4b\\x4c\\x4d\\x4e\\x4f"
+"\\x50\\x51\\x52\\x53\\x54\\x55\\x56\\x57\\x58\\x59\\x5a\\x5b\\x5c\\x5d\\x5e\\x5f"
+"\\x60\\x61\\x62\\x63\\x64\\x65\\x66\\x67\\x68\\x69\\x6a\\x6b\\x6c\\x6d\\x6e\\x6f"
+"\\x70\\x71\\x72\\x73\\x74\\x75\\x76\\x77\\x78\\x79\\x7a\\x7b\\x7c\\x7d\\x7e\\x7f"
+"\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x8b\\x8c\\x8d\\x8e\\x8f"
+"\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f"
+"\\xa0\\xa1\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xab\\xac\\xad\\xae\\xaf"
+"\\xb0\\xb1\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xbb\\xbc\\xbd\\xbe\\xbf"
+"\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xcb\\xcc\\xcd\\xce\\xcf"
+"\\xd0\\xd1\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xdb\\xdc\\xdd\\xde\\xdf"
+"\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef"
+"\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xfb\\xfc\\xfd\\xfe\\xff"
+);
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(static_cast<size_t>(1), GetLiterals().size());
+
+ const auto& string = GetLiterals()[0];
+ for (int i = 0; i < 256; ++i) {
+ // NB: Google Test is quite strict when checking integral types.
+ // Hence the static casts.
+ EXPECT_EQ(
+ static_cast<unsigned char>(i),
+ static_cast<unsigned char>(string[i]));
+ }
+}
+
+TEST_F(TYPathTokenizerTest, EscapedAsInRealWorld)
+{
+ PrepareAndTokenize("Madness\\x3f This is Sparta\\x21");
+ EXPECT_EQ("L", GetFlattenedTokens());
+ EXPECT_EQ(std::vector<TString>(1, "Madness? This is Sparta!"), GetLiterals());
+}
+
+TEST_F(TYPathTokenizerTest, InvalidEscapeSequences)
+{
+ EXPECT_THROW({ PrepareAndTokenize("This is \\"); }, std::exception);
+ EXPECT_THROW({ PrepareAndTokenize("This is \\w"); }, std::exception);
+ EXPECT_THROW({ PrepareAndTokenize("This is \\x"); }, std::exception);
+ EXPECT_THROW({ PrepareAndTokenize("This is \\x0"); }, std::exception);
+ EXPECT_THROW({ PrepareAndTokenize("This is \\xx0"); }, std::exception);
+ EXPECT_THROW({ PrepareAndTokenize("This is \\x0x"); }, std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYPathHelpersTest, DirNameAndBaseName)
+{
+ auto toPair = [] (auto lhs, auto rhs) {
+ return std::make_pair(TString(lhs), TString(rhs));
+ };
+
+ EXPECT_EQ(DirNameAndBaseName("//path/to/smth"), toPair("//path/to", "smth"));
+ EXPECT_EQ(DirNameAndBaseName("//path/to/smth/@"), toPair("//path/to/smth", "@"));
+ EXPECT_EQ(DirNameAndBaseName("//path/to/smth/@foo"), toPair("//path/to/smth/@", "foo"));
+ EXPECT_EQ(DirNameAndBaseName("//path"), toPair("/", "path"));
+ EXPECT_EQ(DirNameAndBaseName("#123-456-789-abc"), toPair("", "#123-456-789-abc"));
+ EXPECT_EQ(DirNameAndBaseName("/path"), toPair("", "path"));
+}
+
+TEST(TYPathHelpersTest, YPathJoin)
+{
+ EXPECT_EQ(
+ YPathJoin("//path/prefix/to/some_table"),
+ "//path/prefix/to/some_table");
+ EXPECT_EQ(
+ YPathJoin("//path/prefix/to", "some_table"),
+ "//path/prefix/to/some_table");
+
+ EXPECT_EQ(
+ YPathJoin("//path", "prefix", "to", "some_table"),
+ "//path/prefix/to/some_table");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYPath
diff --git a/yt/yt/core/ypath/unittests/ya.make b/yt/yt/core/ypath/unittests/ya.make
new file mode 100644
index 0000000000..4a31a9b98e
--- /dev/null
+++ b/yt/yt/core/ypath/unittests/ya.make
@@ -0,0 +1,39 @@
+GTEST(unittester-core-ypath)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ tokenizer_ut.cpp
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(MEDIUM)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/yson/async_consumer.cpp b/yt/yt/core/yson/async_consumer.cpp
new file mode 100644
index 0000000000..631aedad87
--- /dev/null
+++ b/yt/yt/core/yson/async_consumer.cpp
@@ -0,0 +1,93 @@
+#include "async_consumer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncYsonConsumerAdapter::TAsyncYsonConsumerAdapter(IYsonConsumer* underlyingConsumer)
+ : UnderlyingConsumer_(underlyingConsumer)
+{ }
+
+void TAsyncYsonConsumerAdapter::OnStringScalar(TStringBuf value)
+{
+ UnderlyingConsumer_->OnStringScalar(value);
+}
+
+void TAsyncYsonConsumerAdapter::OnInt64Scalar(i64 value)
+{
+ UnderlyingConsumer_->OnInt64Scalar(value);
+}
+
+void TAsyncYsonConsumerAdapter::OnUint64Scalar(ui64 value)
+{
+ UnderlyingConsumer_->OnUint64Scalar(value);
+}
+
+void TAsyncYsonConsumerAdapter::OnDoubleScalar(double value)
+{
+ UnderlyingConsumer_->OnDoubleScalar(value);
+}
+
+void TAsyncYsonConsumerAdapter::OnBooleanScalar(bool value)
+{
+ UnderlyingConsumer_->OnBooleanScalar(value);
+}
+
+void TAsyncYsonConsumerAdapter::OnEntity()
+{
+ UnderlyingConsumer_->OnEntity();
+}
+
+void TAsyncYsonConsumerAdapter::OnBeginList()
+{
+ UnderlyingConsumer_->OnBeginList();
+}
+
+void TAsyncYsonConsumerAdapter::OnListItem()
+{
+ UnderlyingConsumer_->OnListItem();
+}
+
+void TAsyncYsonConsumerAdapter::OnEndList()
+{
+ UnderlyingConsumer_->OnEndList();
+}
+
+void TAsyncYsonConsumerAdapter::OnBeginMap()
+{
+ UnderlyingConsumer_->OnBeginMap();
+}
+
+void TAsyncYsonConsumerAdapter::OnKeyedItem(TStringBuf key)
+{
+ UnderlyingConsumer_->OnKeyedItem(key);
+}
+
+void TAsyncYsonConsumerAdapter::OnEndMap()
+{
+ UnderlyingConsumer_->OnEndMap();
+}
+
+void TAsyncYsonConsumerAdapter::OnBeginAttributes()
+{
+ UnderlyingConsumer_->OnBeginAttributes();
+}
+
+void TAsyncYsonConsumerAdapter::OnEndAttributes()
+{
+ UnderlyingConsumer_->OnEndAttributes();
+}
+
+void TAsyncYsonConsumerAdapter::OnRaw(TStringBuf yson, EYsonType type)
+{
+ UnderlyingConsumer_->OnRaw(yson, type);
+}
+
+void TAsyncYsonConsumerAdapter::OnRaw(TFuture<TYsonString> /*asyncStr*/)
+{
+ YT_ABORT();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/async_consumer.h b/yt/yt/core/yson/async_consumer.h
new file mode 100644
index 0000000000..8a0a2d9276
--- /dev/null
+++ b/yt/yt/core/yson/async_consumer.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "public.h"
+#include "string.h"
+#include "consumer.h"
+
+#include <yt/yt/core/actions/future.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Extends IYsonConsumer by enabling asynchronously constructed
+//! segments to be injected into the stream.
+struct IAsyncYsonConsumer
+ : public IYsonConsumer
+{
+ using IYsonConsumer::OnRaw;
+ virtual void OnRaw(TFuture<TYsonString> asyncStr) = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Turns IYsonConsumer into IAsyncConsumer. No asynchronous calls are allowed.
+class TAsyncYsonConsumerAdapter
+ : public IAsyncYsonConsumer
+{
+public:
+ explicit TAsyncYsonConsumerAdapter(IYsonConsumer* underlyingConsumer);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+ using IYsonConsumer::OnRaw;
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+ void OnRaw(TFuture<TYsonString> asyncStr) override;
+
+private:
+ IYsonConsumer* const UnderlyingConsumer_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/async_writer.cpp b/yt/yt/core/yson/async_writer.cpp
new file mode 100644
index 0000000000..afc292ccf3
--- /dev/null
+++ b/yt/yt/core/yson/async_writer.cpp
@@ -0,0 +1,152 @@
+#include "async_writer.h"
+#include "detail.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAsyncYsonWriter::TAsyncYsonWriter(EYsonType type)
+ : Type_(type)
+ , SyncWriter_(&Stream_, type)
+ , FlushedSize_(std::make_shared<std::atomic<ui64>>(0))
+{ }
+
+void TAsyncYsonWriter::OnStringScalar(TStringBuf value)
+{
+ SyncWriter_.OnStringScalar(value);
+}
+
+void TAsyncYsonWriter::OnInt64Scalar(i64 value)
+{
+ SyncWriter_.OnInt64Scalar(value);
+}
+
+void TAsyncYsonWriter::OnUint64Scalar(ui64 value)
+{
+ SyncWriter_.OnUint64Scalar(value);
+}
+
+void TAsyncYsonWriter::OnDoubleScalar(double value)
+{
+ SyncWriter_.OnDoubleScalar(value);
+}
+
+void TAsyncYsonWriter::OnBooleanScalar(bool value)
+{
+ SyncWriter_.OnBooleanScalar(value);
+}
+
+void TAsyncYsonWriter::OnEntity()
+{
+ SyncWriter_.OnEntity();
+}
+
+void TAsyncYsonWriter::OnBeginList()
+{
+ SyncWriter_.OnBeginList();
+}
+
+void TAsyncYsonWriter::OnListItem()
+{
+ SyncWriter_.OnListItem();
+}
+
+void TAsyncYsonWriter::OnEndList()
+{
+ SyncWriter_.OnEndList();
+}
+
+void TAsyncYsonWriter::OnBeginMap()
+{
+ SyncWriter_.OnBeginMap();
+}
+
+void TAsyncYsonWriter::OnKeyedItem(TStringBuf key)
+{
+ SyncWriter_.OnKeyedItem(key);
+}
+
+void TAsyncYsonWriter::OnEndMap()
+{
+ SyncWriter_.OnEndMap();
+}
+
+void TAsyncYsonWriter::OnBeginAttributes()
+{
+ SyncWriter_.OnBeginAttributes();
+}
+
+void TAsyncYsonWriter::OnEndAttributes()
+{
+ SyncWriter_.OnEndAttributes();
+}
+
+void TAsyncYsonWriter::OnRaw(TStringBuf yson, EYsonType type)
+{
+ SyncWriter_.OnRaw(yson, type);
+}
+
+void TAsyncYsonWriter::OnRaw(TFuture<TYsonString> asyncStr)
+{
+ FlushCurrentSegment();
+ AsyncSegments_.push_back(asyncStr.Apply(
+ BIND([topLevel = SyncWriter_.GetDepth() == 0, type = Type_, flushedSize = FlushedSize_] (const TYsonString& ysonStr) {
+ flushedSize->fetch_add(ysonStr.AsStringBuf().size(), std::memory_order::relaxed);
+ return TSegment{
+ ysonStr,
+ ysonStr.GetType() == EYsonType::Node && (!topLevel || type != EYsonType::Node)
+ };
+ })));
+}
+
+ui64 TAsyncYsonWriter::GetTotalWrittenSize() const
+{
+ return FlushedSize_->load(std::memory_order::relaxed) + SyncWriter_.GetTotalWrittenSize();
+}
+
+TFuture<TYsonString> TAsyncYsonWriter::Finish()
+{
+ FlushCurrentSegment();
+
+ auto callback = BIND([type = Type_] (std::vector<TSegment>&& segments) {
+ size_t length = 0;
+ for (const auto& [ysonStr, trailingSeparator] : segments) {
+ length += ysonStr.AsStringBuf().length();
+ if (trailingSeparator) {
+ length += 1;
+ }
+ }
+
+ TString result;
+ result.reserve(length);
+ for (const auto& [ysonStr, trailingSeparator] : segments) {
+ result.append(ysonStr.AsStringBuf());
+ if (trailingSeparator) {
+ result.append(NDetail::ItemSeparatorSymbol);
+ }
+ }
+
+ return TYsonString(result, type);
+ });
+
+ return AllSucceeded(AsyncSegments_).ApplyUnique(std::move(callback));
+}
+
+const TAsyncYsonWriter::TAsyncSegments& TAsyncYsonWriter::GetSegments() const
+{
+ return AsyncSegments_;
+}
+
+void TAsyncYsonWriter::FlushCurrentSegment()
+{
+ SyncWriter_.Flush();
+ if (!Stream_.Str().empty()) {
+ FlushedSize_->fetch_add(Stream_.Str().length());
+ AsyncSegments_.push_back(MakeFuture(TSegment{Stream_.Str(), false}));
+ Stream_.Str().clear();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/async_writer.h b/yt/yt/core/yson/async_writer.h
new file mode 100644
index 0000000000..2aad00eb19
--- /dev/null
+++ b/yt/yt/core/yson/async_writer.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "public.h"
+#include "async_consumer.h"
+#include "writer.h"
+#include "string.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAsyncYsonWriter
+ : public IAsyncYsonConsumer
+ , private TNonCopyable
+{
+public:
+ //! Represents a YSON segment obtained either from a sequence of contiguous
+ //! synchronous OnXXX, or an asynchronous OnRaw(TFuture<TYsonString>) call.
+ //! Second element of a pair indicates whether a segment must be extended by
+ //! a semicolon.
+ using TSegment = std::pair<TYsonString, bool>;
+ using TAsyncSegments = std::vector<TFuture<TSegment>>;
+
+ explicit TAsyncYsonWriter(EYsonType type = EYsonType::Node);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+ void OnRaw(TFuture<TYsonString> asyncStr) override;
+
+ TFuture<TYsonString> Finish();
+ const TAsyncSegments& GetSegments() const;
+
+ ui64 GetTotalWrittenSize() const;
+
+private:
+ const EYsonType Type_;
+
+ TStringStream Stream_;
+ TBufferedBinaryYsonWriter SyncWriter_;
+
+ TAsyncSegments AsyncSegments_;
+ std::shared_ptr<std::atomic<ui64>> FlushedSize_;
+
+ void FlushCurrentSegment();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/attribute_consumer.cpp b/yt/yt/core/yson/attribute_consumer.cpp
new file mode 100644
index 0000000000..10f30d2730
--- /dev/null
+++ b/yt/yt/core/yson/attribute_consumer.cpp
@@ -0,0 +1,250 @@
+#include "attribute_consumer.h"
+#include "writer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttributeFragmentConsumer::TAttributeFragmentConsumer(IAsyncYsonConsumer* underlyingConsumer)
+ : UnderlyingConsumer_(underlyingConsumer)
+{ }
+
+TAttributeFragmentConsumer::~TAttributeFragmentConsumer()
+{
+ YT_ASSERT(Finished_ || std::uncaught_exceptions() > 0);
+}
+
+void TAttributeFragmentConsumer::OnRaw(TFuture<TYsonString> asyncStr)
+{
+ Start();
+ UnderlyingConsumer_->OnRaw(std::move(asyncStr));
+}
+
+void TAttributeFragmentConsumer::OnRaw(TStringBuf yson, EYsonType type)
+{
+ if (!yson.empty()) {
+ Start();
+ UnderlyingConsumer_->OnRaw(yson, type);
+ }
+}
+
+// Calling Start() on other events is redundant.
+
+void TAttributeFragmentConsumer::OnEndAttributes()
+{
+ UnderlyingConsumer_->OnEndAttributes();
+}
+
+void TAttributeFragmentConsumer::OnBeginAttributes()
+{
+ UnderlyingConsumer_->OnBeginAttributes();
+}
+
+void TAttributeFragmentConsumer::OnEndMap()
+{
+ UnderlyingConsumer_->OnEndMap();
+}
+
+void TAttributeFragmentConsumer::OnKeyedItem(TStringBuf key)
+{
+ Start();
+ UnderlyingConsumer_->OnKeyedItem(key);
+}
+
+void TAttributeFragmentConsumer::OnBeginMap()
+{
+ UnderlyingConsumer_->OnBeginMap();
+}
+
+void TAttributeFragmentConsumer::OnEndList()
+{
+ UnderlyingConsumer_->OnEndList();
+}
+
+void TAttributeFragmentConsumer::OnListItem()
+{
+ UnderlyingConsumer_->OnListItem();
+}
+
+void TAttributeFragmentConsumer::OnBeginList()
+{
+ UnderlyingConsumer_->OnBeginList();
+}
+
+void TAttributeFragmentConsumer::OnEntity()
+{
+ UnderlyingConsumer_->OnEntity();
+}
+
+void TAttributeFragmentConsumer::OnBooleanScalar(bool value)
+{
+ UnderlyingConsumer_->OnBooleanScalar(value);
+}
+
+void TAttributeFragmentConsumer::OnDoubleScalar(double value)
+{
+ UnderlyingConsumer_->OnDoubleScalar(value);
+}
+
+void TAttributeFragmentConsumer::OnUint64Scalar(ui64 value)
+{
+ UnderlyingConsumer_->OnUint64Scalar(value);
+}
+
+void TAttributeFragmentConsumer::OnInt64Scalar(i64 value)
+{
+ UnderlyingConsumer_->OnInt64Scalar(value);
+}
+
+void TAttributeFragmentConsumer::OnStringScalar(TStringBuf value)
+{
+ UnderlyingConsumer_->OnStringScalar(value);
+}
+
+void TAttributeFragmentConsumer::Start()
+{
+ if (!HasAttributes_) {
+ UnderlyingConsumer_->OnBeginAttributes();
+ HasAttributes_ = true;
+ }
+}
+
+void TAttributeFragmentConsumer::Finish()
+{
+ if (HasAttributes_) {
+ UnderlyingConsumer_->OnEndAttributes();
+ }
+ Finished_ = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttributeValueConsumer::TAttributeValueConsumer(
+ IAsyncYsonConsumer* underlyingConsumer,
+ TString key)
+ : UnderlyingConsumer_(underlyingConsumer)
+ , Key_(std::move(key))
+{ }
+
+void TAttributeValueConsumer::OnStringScalar(TStringBuf value)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnStringScalar(value);
+}
+
+void TAttributeValueConsumer::OnInt64Scalar(i64 value)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnInt64Scalar(value);
+}
+
+void TAttributeValueConsumer::OnUint64Scalar(ui64 value)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnUint64Scalar(value);
+}
+
+void TAttributeValueConsumer::OnDoubleScalar(double value)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnDoubleScalar(value);
+}
+
+void TAttributeValueConsumer::OnBooleanScalar(bool value)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnBooleanScalar(value);
+}
+
+void TAttributeValueConsumer::OnEntity()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnEntity();
+}
+
+void TAttributeValueConsumer::OnBeginList()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnBeginList();
+}
+
+void TAttributeValueConsumer::OnListItem()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnListItem();
+}
+
+void TAttributeValueConsumer::OnEndList()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnEndList();
+}
+
+void TAttributeValueConsumer::OnBeginMap()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnBeginMap();
+}
+
+void TAttributeValueConsumer::OnKeyedItem(TStringBuf key)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnKeyedItem(key);
+}
+
+void TAttributeValueConsumer::OnEndMap()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnEndMap();
+}
+
+void TAttributeValueConsumer::OnBeginAttributes()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnBeginAttributes();
+}
+
+void TAttributeValueConsumer::OnEndAttributes()
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnEndAttributes();
+}
+
+void TAttributeValueConsumer::OnRaw(TStringBuf yson, EYsonType type)
+{
+ ProduceKeyIfNeeded();
+ UnderlyingConsumer_->OnRaw(yson, type);
+}
+
+void TAttributeValueConsumer::OnRaw(TFuture<TYsonString> asyncStr)
+{
+ if (Empty_) {
+ UnderlyingConsumer_->OnRaw(asyncStr.Apply(BIND([key = Key_] (const TYsonString& str) {
+ if (str) {
+ YT_VERIFY(str.GetType() == EYsonType::Node);
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream, EYsonType::MapFragment);
+ writer.OnKeyedItem(key);
+ writer.OnRaw(str);
+ writer.Flush();
+ return TYsonString(stream.Str(), EYsonType::MapFragment);
+ } else {
+ return TYsonString(TString(), EYsonType::MapFragment);
+ }
+ })));
+ } else {
+ UnderlyingConsumer_->OnRaw(std::move(asyncStr));
+ }
+}
+
+void TAttributeValueConsumer::ProduceKeyIfNeeded()
+{
+ if (Empty_) {
+ UnderlyingConsumer_->OnKeyedItem(Key_);
+ Empty_ = false;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/attribute_consumer.h b/yt/yt/core/yson/attribute_consumer.h
new file mode 100644
index 0000000000..1239476c3c
--- /dev/null
+++ b/yt/yt/core/yson/attribute_consumer.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "public.h"
+#include "async_consumer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Consumes a map fragment representing the attributes
+//! and if the fragment is non-empty then encloses it with angle brackets.
+class TAttributeFragmentConsumer
+ : public IAsyncYsonConsumer
+{
+public:
+ explicit TAttributeFragmentConsumer(IAsyncYsonConsumer* underlyingConsumer);
+ ~TAttributeFragmentConsumer();
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+ using IAsyncYsonConsumer::OnRaw;
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+ void OnRaw(TFuture<TYsonString> asyncStr) override;
+
+ void Finish();
+
+private:
+ IAsyncYsonConsumer* const UnderlyingConsumer_;
+ bool HasAttributes_ = false;
+ bool Finished_ = false;
+
+ void Start();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Consumes an attribute value and if it is non-empty then prepends it with
+//! the attribute key.
+class TAttributeValueConsumer
+ : public IAsyncYsonConsumer
+{
+public:
+ TAttributeValueConsumer(
+ IAsyncYsonConsumer* underlyingConsumer,
+ TString key);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+ using IYsonConsumer::OnRaw;
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+ void OnRaw(TFuture<TYsonString> asyncStr) override;
+
+private:
+ IAsyncYsonConsumer* const UnderlyingConsumer_;
+ const TString Key_;
+ bool Empty_ = true;
+
+ void ProduceKeyIfNeeded();
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/attributes_stripper.cpp b/yt/yt/core/yson/attributes_stripper.cpp
new file mode 100644
index 0000000000..f2f1d1bef5
--- /dev/null
+++ b/yt/yt/core/yson/attributes_stripper.cpp
@@ -0,0 +1,139 @@
+#include "attributes_stripper.h"
+#include "string.h"
+#include "writer.h"
+#include "parser.h"
+
+#include <library/cpp/yson/consumer.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonAttributesStripper
+ : public IYsonConsumer
+{
+public:
+ explicit TYsonAttributesStripper(IYsonConsumer* output)
+ : Output_(output)
+ { }
+
+ void OnStringScalar(TStringBuf value) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnStringScalar(value);
+ }
+ }
+
+ void OnInt64Scalar(i64 value) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnInt64Scalar(value);
+ }
+ }
+
+ void OnUint64Scalar(ui64 value) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnInt64Scalar(value);
+ }
+ }
+
+ void OnDoubleScalar(double value) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnDoubleScalar(value);
+ }
+ }
+
+ void OnBooleanScalar(bool value) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnBooleanScalar(value);
+ }
+ }
+
+ void OnEntity() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnEntity();
+ }
+ }
+
+ void OnBeginList() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnBeginList();
+ }
+ }
+
+ void OnListItem() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnListItem();
+ }
+ }
+
+ void OnEndList() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnEndList();
+ }
+ }
+
+ void OnBeginMap() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnBeginMap();
+ }
+ }
+
+ void OnKeyedItem(TStringBuf key) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnKeyedItem(key);
+ }
+ }
+
+ void OnEndMap() override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnEndMap();
+ }
+ }
+
+ void OnBeginAttributes() override
+ {
+ ++AttributesDepth_;
+ }
+
+ void OnEndAttributes() override
+ {
+ --AttributesDepth_;
+ }
+
+ void OnRaw(TStringBuf yson, EYsonType type) override
+ {
+ if (AttributesDepth_ == 0) {
+ Output_->OnRaw(yson, type);
+ }
+ }
+
+private:
+ IYsonConsumer* const Output_;
+ int AttributesDepth_ = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonString StripAttributes(const TYsonString& yson)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream);
+ TYsonAttributesStripper stripper(&writer);
+ ParseYsonStringBuffer(yson.AsStringBuf(), yson.GetType(), &stripper);
+ return TYsonString(outputStream.Str(), yson.GetType());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/attributes_stripper.h b/yt/yt/core/yson/attributes_stripper.h
new file mode 100644
index 0000000000..1de4cab956
--- /dev/null
+++ b/yt/yt/core/yson/attributes_stripper.h
@@ -0,0 +1,11 @@
+#include "public.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonString StripAttributes(const TYsonString& yson);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/building_consumer.h b/yt/yt/core/yson/building_consumer.h
new file mode 100644
index 0000000000..af18416ed4
--- /dev/null
+++ b/yt/yt/core/yson/building_consumer.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "consumer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An interface for building an object by parsing a YSON stream.
+template <class T>
+class IBuildingYsonConsumer
+ : public virtual IYsonConsumer
+{
+public:
+ //! Finalizes the parsing process and returns the object built by the processed YSON stream.
+ virtual T Finish() = 0;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/consumer.cpp b/yt/yt/core/yson/consumer.cpp
new file mode 100644
index 0000000000..56dc7b4307
--- /dev/null
+++ b/yt/yt/core/yson/consumer.cpp
@@ -0,0 +1,16 @@
+#include "consumer.h"
+#include "string.h"
+#include "parser.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonConsumerBase::OnRaw(TStringBuf str, EYsonType type)
+{
+ ParseYsonStringBuffer(str, type, this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/consumer.h b/yt/yt/core/yson/consumer.h
new file mode 100644
index 0000000000..fb8bb29ba8
--- /dev/null
+++ b/yt/yt/core/yson/consumer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/yson/consumer.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Some consumers act like buffered streams and thus must be flushed at the end.
+struct IFlushableYsonConsumer
+ : public virtual IYsonConsumer
+{
+ virtual void Flush() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base class for most YSON consumers.
+class TYsonConsumerBase
+ : public virtual IYsonConsumer
+{
+public:
+ //! Parses #str and converts it into a sequence of elementary calls.
+ void OnRaw(TStringBuf str, EYsonType type) override;
+ using IYsonConsumer::OnRaw;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/depth_limiting_yson_consumer.cpp b/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
new file mode 100644
index 0000000000..7dcfb86bf8
--- /dev/null
+++ b/yt/yt/core/yson/depth_limiting_yson_consumer.cpp
@@ -0,0 +1,173 @@
+#include "depth_limiting_yson_consumer.h"
+#include "forwarding_consumer.h"
+#include "null_consumer.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const TString TruncatedStubMessage("<truncated>");
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDepthLimitingYsonConsumer
+ : public TForwardingYsonConsumer
+{
+public:
+ TDepthLimitingYsonConsumer(
+ IYsonConsumer* underlyingConsumer,
+ int depthLimit,
+ EYsonType ysonType)
+ : UnderlyingConsumer_(underlyingConsumer)
+ , DepthLimit_(depthLimit)
+ {
+ THROW_ERROR_EXCEPTION_IF(DepthLimit_ < 0,
+ "YSON depth limit must be non-negative");
+
+ if (DepthLimit_ == 0) {
+ Forward(&NullConsumer_, /*onFinished*/ {}, ysonType);
+ }
+ }
+
+ void OnMyStringScalar(TStringBuf value) override
+ {
+ UnderlyingConsumer_->OnStringScalar(value);
+ }
+
+ void OnMyInt64Scalar(i64 value) override
+ {
+ UnderlyingConsumer_->OnInt64Scalar(value);
+ }
+
+ void OnMyUint64Scalar(ui64 value) override
+ {
+ UnderlyingConsumer_->OnUint64Scalar(value);
+ }
+
+ void OnMyDoubleScalar(double value) override
+ {
+ UnderlyingConsumer_->OnDoubleScalar(value);
+ }
+
+ void OnMyBooleanScalar(bool value) override
+ {
+ UnderlyingConsumer_->OnBooleanScalar(value);
+ }
+
+ void OnMyEntity() override
+ {
+ UnderlyingConsumer_->OnEntity();
+ }
+
+ void OnMyBeginList() override
+ {
+ ++Depth_;
+
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnBeginList();
+ } else {
+ UnderlyingConsumer_->OnStringScalar(TruncatedStubMessage);
+ Forward(
+ &NullConsumer_,
+ /*onFinished*/ {},
+ EYsonType::ListFragment);
+ }
+ }
+
+ void OnMyListItem() override
+ {
+ UnderlyingConsumer_->OnListItem();
+ }
+
+ void OnMyEndList() override
+ {
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnEndList();
+ }
+
+ --Depth_;
+ }
+
+ void OnMyBeginMap() override
+ {
+ ++Depth_;
+
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnBeginMap();
+ } else {
+ UnderlyingConsumer_->OnStringScalar(TruncatedStubMessage);
+ Forward(
+ &NullConsumer_,
+ /*onFinished*/ {},
+ EYsonType::MapFragment);
+ }
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ UnderlyingConsumer_->OnKeyedItem(key);
+ }
+
+ void OnMyEndMap() override
+ {
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnEndMap();
+ }
+
+ --Depth_;
+ }
+
+ void OnMyBeginAttributes() override
+ {
+ ++Depth_;
+
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnBeginAttributes();
+ } else {
+ Forward(
+ &NullConsumer_,
+ /*onFinished*/ {},
+ EYsonType::MapFragment);
+ }
+ }
+
+ void OnMyEndAttributes() override
+ {
+ if (Depth_ < DepthLimit_) {
+ UnderlyingConsumer_->OnEndAttributes();
+ }
+
+ --Depth_;
+ }
+
+private:
+ IYsonConsumer* const UnderlyingConsumer_;
+ const int DepthLimit_;
+
+ int Depth_ = 0;
+
+ TNullYsonConsumer NullConsumer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYsonConsumer> CreateDepthLimitingYsonConsumer(
+ NYson::IYsonConsumer* underlyingConsumer,
+ int depthLimit,
+ EYsonType ysonType)
+{
+ return std::make_unique<TDepthLimitingYsonConsumer>(
+ underlyingConsumer,
+ depthLimit,
+ ysonType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/depth_limiting_yson_consumer.h b/yt/yt/core/yson/depth_limiting_yson_consumer.h
new file mode 100644
index 0000000000..2519a4047c
--- /dev/null
+++ b/yt/yt/core/yson/depth_limiting_yson_consumer.h
@@ -0,0 +1,16 @@
+#include "public.h"
+#include "consumer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Depth limit must be non-negative.
+std::unique_ptr<IYsonConsumer> CreateDepthLimitingYsonConsumer(
+ NYson::IYsonConsumer* underlyingConsumer,
+ int depthLimit,
+ EYsonType ysonType = EYsonType::Node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/detail.h b/yt/yt/core/yson/detail.h
new file mode 100644
index 0000000000..08c8179579
--- /dev/null
+++ b/yt/yt/core/yson/detail.h
@@ -0,0 +1,1008 @@
+#pragma once
+
+#include "public.h"
+#include "token.h"
+
+#include <yt/yt/core/concurrency/coroutine.h>
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/parser_helpers.h>
+#include <yt/yt/core/misc/property.h>
+
+#include <library/cpp/yt/coding/varint.h>
+#include <library/cpp/yt/coding/zig_zag.h>
+
+#include <library/cpp/yt/yson_string/format.h>
+
+#include <util/generic/string.h>
+
+#include <util/string/escape.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int MaxLiteralLengthInError = 100;
+
+inline TError CreateLiteralError(ETokenType tokenType, const char* bufferStart, size_t bufferSize)
+{
+ if (bufferSize < MaxLiteralLengthInError) {
+ return TError("Failed to parse %v literal %Qv",
+ tokenType,
+ TStringBuf(bufferStart, bufferSize));
+ } else {
+ return TError("Failed to parse %v literal \"%v...<literal truncated>\"",
+ tokenType,
+ TStringBuf(bufferStart, std::min<size_t>(bufferSize, MaxLiteralLengthInError)));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+/*! \internal */
+////////////////////////////////////////////////////////////////////////////////
+
+//! Indicates end of stream.
+constexpr char EndSymbol = '\0';
+
+template <bool EnableLinePositionInfo>
+class TPositionInfo;
+
+template <>
+class TPositionInfo<true>
+{
+private:
+ i64 Offset = 0;
+ int Line = 1;
+ int Column = 1;
+
+public:
+ void OnRangeConsumed(const char* begin, const char* end)
+ {
+ Offset += end - begin;
+ for (auto current = begin; current != end; ++current) {
+ ++Column;
+ if (*current == '\n') { //TODO: memchr
+ ++Line;
+ Column = 1;
+ }
+ }
+ }
+
+ std::vector<TErrorAttribute> GetErrorAttributes(const char* begin, const char* current) const
+ {
+ auto other = *this;
+ other.OnRangeConsumed(begin, current);
+ return {
+ TErrorAttribute("offset", other.Offset),
+ TErrorAttribute("line", other.Line),
+ TErrorAttribute("column", other.Column),
+ };
+ }
+};
+
+template <>
+class TPositionInfo<false>
+{
+private:
+ i64 Offset = 0;
+
+public:
+ void OnRangeConsumed(const char* begin, const char* end)
+ {
+ Offset += end - begin;
+ }
+
+ std::vector<TErrorAttribute> GetErrorAttributes(const char* begin, const char* current) const
+ {
+ auto other = *this;
+ other.OnRangeConsumed(begin, current);
+ return {
+ TErrorAttribute("offset", other.Offset),
+ };
+ }
+};
+
+template <class T, size_t Capacity>
+class TStaticRingQueue
+{
+private:
+ static_assert(std::is_standard_layout_v<T> && std::is_trivial_v<T>, "T must be POD.");
+
+ T Buffer_[Capacity];
+ size_t EndOffset_ = 0;
+ size_t Size_ = 0;
+
+public:
+ void Append(const T* begin, const T* end)
+ {
+ if (std::distance(begin, end) > static_cast<i64>(Capacity)) {
+ begin = end - Capacity;
+ }
+
+ size_t appendSize = std::distance(begin, end);
+ Size_ = std::min(Capacity, Size_ + appendSize);
+
+ EndOffset_ += appendSize;
+ if (EndOffset_ >= Capacity) {
+ EndOffset_ -= Capacity;
+ YT_VERIFY(EndOffset_ < Capacity);
+ }
+
+ size_t tailSize = std::min<size_t>(EndOffset_, appendSize);
+ std::copy(end - tailSize, end, Buffer_ + EndOffset_ - tailSize);
+ end -= tailSize;
+ std::copy(begin, end, Buffer_ + Capacity - (end - begin));
+ }
+
+ void CopyTailTo(size_t copySize, T* begin) const
+ {
+ YT_VERIFY(copySize <= Size_);
+
+ if (copySize > EndOffset_) {
+ size_t tailSize = copySize - EndOffset_;
+ std::copy(Buffer_ + Capacity - tailSize, Buffer_ + Capacity, begin);
+ std::copy(Buffer_, Buffer_ + EndOffset_, begin + tailSize);
+ } else {
+ std::copy(Buffer_ + EndOffset_ - copySize, Buffer_ + EndOffset_, begin);
+ }
+ }
+
+ size_t Size() const
+ {
+ return Size_;
+ }
+};
+
+static constexpr size_t MaxMarginSize = 10;
+
+template <class TBlockStream, size_t MaxContextSize>
+class TReaderWithContext
+ : public TBlockStream
+{
+private:
+ static_assert(MaxContextSize > MaxMarginSize, "MaxContextSize must be greater than MaxMarginSize.");
+
+ // Checkpoint points to the position that was current when CheckpointContext was called.
+ // If it is nullptr that means that CheckpointContext was never called or it was called inside previous block.
+ const char* Checkpoint = nullptr;
+
+ // Context keeps context if CheckpointContext was called inside one of the previous blocks.
+ char Context[MaxContextSize];
+ size_t ContextSize = 0;
+ size_t ContextPosition = 0;
+
+ // PrevBlockTail keeps characters from previous blocks.
+ // We save MaxMarginSize characters from the end of the current block when it ends.
+ // It will allow us to keep a left margin of the current context even if it starts at the beginning of the block.
+ TStaticRingQueue<char, MaxMarginSize> PrevBlockTail;
+
+public:
+ TReaderWithContext(const TBlockStream& blockStream)
+ : TBlockStream(blockStream)
+ { }
+
+ void CheckpointContext()
+ {
+ Checkpoint = TBlockStream::Current();
+ }
+
+ // Return pair <context, context_position>.
+ std::pair<TString, size_t> GetContextFromCheckpoint() const
+ {
+ TString result(MaxContextSize, '\0');
+ size_t size, position;
+ SaveContext(result.Detach(), &size, &position);
+ result.resize(size);
+ return {result, position};
+ }
+
+ void RefreshBlock()
+ {
+ SaveContext(Context, &ContextSize, &ContextPosition);
+ PrevBlockTail.Append(TBlockStream::Begin(), TBlockStream::End());
+ TBlockStream::RefreshBlock();
+ Checkpoint = nullptr;
+ }
+
+private:
+ // dest must be at least of MaxContextSize capacity.
+ void SaveContext(char* dest, size_t* contextSize, size_t* contextPosition) const
+ {
+ char* current = dest;
+ if (Checkpoint != nullptr) {
+ // Context inside current block.
+ const size_t sizeFromBlock = std::min<size_t>(Checkpoint - TBlockStream::Begin(), MaxMarginSize);
+ if (sizeFromBlock < MaxMarginSize) {
+ const size_t sizeFromPrevBlock = std::min(MaxMarginSize - sizeFromBlock, PrevBlockTail.Size());
+ PrevBlockTail.CopyTailTo(sizeFromPrevBlock, current);
+ current += sizeFromPrevBlock;
+ }
+ memcpy(current, Checkpoint - sizeFromBlock, sizeFromBlock);
+ current += sizeFromBlock;
+ *contextPosition = current - dest;
+ const size_t sizeAfterCheckpoint = std::min<size_t>(
+ MaxContextSize - (current - dest),
+ TBlockStream::End() - Checkpoint);
+ memcpy(current, Checkpoint, sizeAfterCheckpoint);
+ current += sizeAfterCheckpoint;
+ } else if (ContextSize > 0) {
+ // Context is inside one of the previous blocks.
+ *contextPosition = ContextPosition;
+ if (current != Context) {
+ memcpy(current, Context, ContextSize);
+ }
+ current += ContextSize;
+ if (ContextSize < MaxContextSize) {
+ const auto toCopy = std::min<size_t>(MaxContextSize - ContextSize, TBlockStream::End() - TBlockStream::Begin());
+ if (toCopy > 0) {
+ memcpy(current, TBlockStream::Begin(), toCopy);
+ current += toCopy;
+ }
+ }
+ } else {
+ // Context is the beginning of the stream.
+ const auto toCopy = std::min<size_t>(MaxContextSize, TBlockStream::End() - TBlockStream::Begin());
+ if (toCopy > 0) {
+ memcpy(current, TBlockStream::Begin(), toCopy);
+ current += toCopy;
+ }
+ *contextPosition = 0;
+ }
+ *contextSize = current - dest;
+ }
+};
+
+template <class TBlockStream>
+class TReaderWithContext<TBlockStream, 0>
+ : public TBlockStream
+{
+public:
+ TReaderWithContext(const TBlockStream& blockStream)
+ : TBlockStream(blockStream)
+ { }
+
+ void CheckpointContext()
+ { }
+
+ std::pair<TString, size_t> GetContextFromCheckpoint() const
+ {
+ return {"<context is disabled>", 0};
+ }
+};
+
+template <class TBlockStream, class TPositionBase>
+class TCharStream
+ : public TBlockStream
+ , public TPositionBase
+{
+public:
+ TCharStream(const TBlockStream& blockStream)
+ : TBlockStream(blockStream)
+ { }
+
+ bool IsEmpty() const
+ {
+ return TBlockStream::Current() == TBlockStream::End();
+ }
+
+ template <bool AllowFinish>
+ void Refresh()
+ {
+ while (IsEmpty() && !TBlockStream::IsFinished()) {
+ TPositionBase::OnRangeConsumed(TBlockStream::Begin(), TBlockStream::Current());
+ TBlockStream::RefreshBlock();
+ }
+ if (IsEmpty() && TBlockStream::IsFinished() && !AllowFinish) {
+ THROW_ERROR_EXCEPTION("Premature end of stream")
+ << *this;
+ }
+ }
+
+ void Refresh()
+ {
+ return Refresh<false>();
+ }
+
+ template <bool AllowFinish>
+ char GetChar()
+ {
+ if (!IsEmpty()) {
+ return *TBlockStream::Current();
+ }
+ Refresh<AllowFinish>();
+ return !IsEmpty() ? *TBlockStream::Current() : '\0';
+ }
+
+ char GetChar()
+ {
+ return GetChar<false>();
+ }
+
+ void Advance(size_t bytes)
+ {
+ TBlockStream::Advance(bytes);
+ }
+
+ size_t Length() const
+ {
+ return TBlockStream::End() - TBlockStream::Current();
+ }
+
+ std::vector<TErrorAttribute> GetErrorAttributes() const
+ {
+ return TPositionBase::GetErrorAttributes(TBlockStream::Begin(), TBlockStream::Current());
+ }
+
+ friend TError operator << (const TError& error, const TCharStream<TBlockStream, TPositionBase>& stream)
+ {
+ return error << stream.GetErrorAttributes();
+ }
+};
+
+template <class TBaseStream>
+class TCodedStream
+ : public TBaseStream
+{
+private:
+ static constexpr int MaxVarintBytes = 10;
+ static constexpr int MaxVarint32Bytes = 5;
+
+ const ui8* BeginByte() const
+ {
+ return reinterpret_cast<const ui8*>(TBaseStream::Current());
+ }
+
+ const ui8* EndByte() const
+ {
+ return reinterpret_cast<const ui8*>(TBaseStream::End());
+ }
+
+ [[noreturn]] void ThrowCannotParseVarint()
+ {
+ THROW_ERROR_EXCEPTION("Error parsing varint value")
+ << *this;
+ }
+
+ // Following functions is an adaptation Protobuf code from coded_stream.cc
+ ui32 ReadVarint32FromArray()
+ {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this read won't cross the end, so we can skip the checks.
+ const ui8* ptr = BeginByte();
+ ui32 b;
+ ui32 result;
+
+ b = *(ptr++); result = (b & 0x7F) ; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 7; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 14; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 21; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= b << 28; if (!(b & 0x80)) goto done;
+
+ // If the input is larger than 32 bits, we still need to read it all
+ // and discard the high-order bits.
+
+ for (int i = 0; i < MaxVarintBytes - MaxVarint32Bytes; i++) {
+ b = *(ptr++); if (!(b & 0x80)) goto done;
+ }
+
+ // We have overrun the maximum size of a Varint (10 bytes). Assume
+ // the data is corrupt.
+ ThrowCannotParseVarint();
+
+ done:
+ TBaseStream::Advance(ptr - BeginByte());
+ return result;
+ }
+
+ ui32 ReadVarint32Fallback()
+ {
+ if (BeginByte() + MaxVarintBytes <= EndByte() ||
+ // Optimization: If the Varint ends at exactly the end of the buffer,
+ // we can detect that and still use the fast path.
+ (BeginByte() < EndByte() && !(EndByte()[-1] & 0x80)))
+ {
+ return ReadVarint32FromArray();
+ } else {
+ // Really slow case: we will incur the cost of an extra function call here,
+ // but moving this out of line reduces the size of this function, which
+ // improves the common case. In micro benchmarks, this is worth about 10-15%
+ return ReadVarint32Slow();
+ }
+ }
+
+ ui32 ReadVarint32Slow()
+ {
+ ui32 result = ReadVarint64();
+ return static_cast<ui32>(result);
+ }
+
+ ui64 ReadVarint64Slow()
+ {
+ // Slow path: This read might cross the end of the buffer, so we
+ // need to check and refresh the buffer if and when it does.
+
+ ui64 result = 0;
+ int count = 0;
+ ui32 b;
+
+ do {
+ if (count == MaxVarintBytes) {
+ ThrowCannotParseVarint();
+ }
+ while (BeginByte() == EndByte()) {
+ TBaseStream::Refresh();
+ }
+ b = *BeginByte();
+ result |= static_cast<ui64>(b & 0x7F) << (7 * count);
+ TBaseStream::Advance(1);
+ ++count;
+ } while (b & 0x80);
+
+ return result;
+ }
+
+public:
+ TCodedStream(const TBaseStream& baseStream)
+ : TBaseStream(baseStream)
+ { }
+
+ Y_FORCE_INLINE int ReadVarint64ToArray(char* out)
+ {
+ if (BeginByte() + MaxVarintBytes <= EndByte() ||
+ // Optimization: If the Varint ends at exactly the end of the buffer,
+ // we can detect that and still use the fast path.
+ (BeginByte() < EndByte() && !(EndByte()[-1] & 0x80)))
+ {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this read won't cross the end, so we can skip the checks.
+
+ const ui8* ptr = BeginByte();
+ for (int i = 0; i < MaxVarintBytes; ++i) {
+ *out++ = *ptr;
+ if (!(*ptr & 0x80U)) {
+ TBaseStream::Advance(i + 1);
+ return i + 1;
+ }
+ ++ptr;
+ }
+
+ // We have overrun the maximum size of a Varint (10 bytes). The data
+ // must be corrupt.
+ ThrowCannotParseVarint();
+ } else {
+ return WriteVarUint64(out, ReadVarint64Slow());
+ }
+ }
+
+ Y_FORCE_INLINE ui64 ReadVarint64()
+ {
+ if (BeginByte() + MaxVarintBytes <= EndByte() ||
+ // Optimization: If the Varint ends at exactly the end of the buffer,
+ // we can detect that and still use the fast path.
+ (BeginByte() < EndByte() && !(EndByte()[-1] & 0x80)))
+ {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this read won't cross the end, so we can skip the checks.
+
+ const ui8* ptr = BeginByte();
+ ui64 b;
+ ui64 result = 0;
+
+ b = *(ptr++); result = (b & 0x7F) ; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 7; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 14; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 21; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 28; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 35; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 42; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 49; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 56; if (!(b & 0x80)) goto done;
+ b = *(ptr++); result |= (b & 0x7F) << 63; if (!(b & 0x80)) goto done;
+
+ // We have overrun the maximum size of a Varint (10 bytes). The data
+ // must be corrupt.
+ ThrowCannotParseVarint();
+
+ done:
+ TBaseStream::Advance(ptr - BeginByte());
+ return result;
+ } else {
+ return ReadVarint64Slow();
+ }
+ }
+
+ ui32 ReadVarint32()
+ {
+ if (BeginByte() < EndByte() && *BeginByte() < 0x80) {
+ ui32 result = *BeginByte();
+ TBaseStream::Advance(1);
+ return result;
+ } else {
+ return ReadVarint32Fallback();
+ }
+ }
+};
+
+DEFINE_ENUM(ENumericResult,
+ ((Int64) (0))
+ ((Uint64) (1))
+ ((Double) (2))
+);
+
+template <class TBlockStream, bool EnableLinePositionInfo>
+class TLexerBase
+ : public TCodedStream<TCharStream<TBlockStream, TPositionInfo<EnableLinePositionInfo>>>
+{
+private:
+ typedef TCodedStream<TCharStream<TBlockStream, TPositionInfo<EnableLinePositionInfo>>> TBaseStream;
+
+ std::vector<char> Buffer_;
+ const size_t MemoryLimit_;
+
+ void Insert(const char* begin, const char* end)
+ {
+ ReserveAndCheckMemoryLimit(end - begin);
+ Buffer_.insert(Buffer_.end(), begin, end);
+ }
+
+ void PushBack(char ch)
+ {
+ ReserveAndCheckMemoryLimit(1);
+ Buffer_.push_back(ch);
+ }
+
+ void ReserveAndCheckMemoryLimit(size_t size)
+ {
+ const auto minReserveSize = Buffer_.size() + size;
+
+ // NB. some implementations of std::vector reserve exactly requested size.
+ // We explicitly set exponential growth here so PushBack that uses this function
+ // keep amortized complexity of O(1).
+ auto reserveSize = Max(Buffer_.capacity() * 2, minReserveSize);
+ if (MemoryLimit_) {
+ if (minReserveSize > MemoryLimit_) {
+ THROW_ERROR_EXCEPTION(
+ "Memory limit exceeded while parsing YSON stream: allocated %v, limit %v",
+ minReserveSize,
+ MemoryLimit_);
+ }
+ // MemoryLimit_ >= minReserveSize ==> reserveSize >= minReserveSize
+ reserveSize = Min(reserveSize, MemoryLimit_);
+ }
+ if (minReserveSize <= Buffer_.capacity()) {
+ return;
+ }
+ YT_ASSERT(reserveSize >= minReserveSize);
+ Buffer_.reserve(reserveSize);
+ }
+
+public:
+ TLexerBase(
+ const TBlockStream& blockStream,
+ i64 memoryLimit = std::numeric_limits<i64>::max())
+ : TBaseStream(blockStream)
+ , MemoryLimit_(memoryLimit)
+ { }
+
+ /// Lexer routines
+
+ template <bool AllowFinish>
+ ENumericResult ReadNumeric(TStringBuf* value)
+ {
+ Buffer_.clear();
+ auto result = ENumericResult::Int64;
+ while (true) {
+ char ch = TBaseStream::template GetChar<AllowFinish>();
+ if (isdigit(ch) || ch == '+' || ch == '-') { // Seems like it can't be '+' or '-'
+ PushBack(ch);
+ } else if (ch == '.' || ch == 'e' || ch == 'E') {
+ PushBack(ch);
+ result = ENumericResult::Double;
+ } else if (ch == 'u') {
+ PushBack(ch);
+ result = ENumericResult::Uint64;
+ } else if (isalpha(ch)) {
+ THROW_ERROR_EXCEPTION("Unexpected %Qv in numeric literal",
+ ch)
+ << *this;
+ } else {
+ break;
+ }
+ TBaseStream::Advance(1);
+ }
+
+ *value = TStringBuf(Buffer_.data(), Buffer_.size());
+ return result;
+ }
+
+ template <bool AllowFinish>
+ double ReadNanOrInf()
+ {
+ Buffer_.clear();
+
+ static const TStringBuf nanString = "nan";
+ static const TStringBuf infString = "inf";
+ static const TStringBuf plusInfString = "+inf";
+ static const TStringBuf minusInfString = "-inf";
+
+ TStringBuf expectedString;
+ double expectedValue;
+ PushBack(TBaseStream::template GetChar<AllowFinish>());
+ TBaseStream::Advance(1);
+ switch (Buffer_.back()) {
+ case '+':
+ expectedString = plusInfString;
+ expectedValue = std::numeric_limits<double>::infinity();
+ break;
+ case '-':
+ expectedString = minusInfString;
+ expectedValue = -std::numeric_limits<double>::infinity();
+ break;
+ case 'i':
+ expectedString = infString;
+ expectedValue = std::numeric_limits<double>::infinity();
+ break;
+ case 'n':
+ expectedString = nanString;
+ expectedValue = std::numeric_limits<double>::quiet_NaN();
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Incorrect %%-literal prefix: %Qc",
+ Buffer_.back());
+ }
+
+ for (int i = 1; i < static_cast<int>(expectedString.size()); ++i) {
+ PushBack(TBaseStream::template GetChar<AllowFinish>());
+ TBaseStream::Advance(1);
+ if (Buffer_.back() != expectedString[i]) {
+ THROW_ERROR_EXCEPTION("Incorrect %%-literal prefix \"%v%c\", expected %Qv",
+ expectedString.SubStr(0, i),
+ Buffer_.back(),
+ expectedString);
+ }
+ }
+
+ return expectedValue;
+ }
+
+ TStringBuf ReadQuotedString()
+ {
+ Buffer_.clear();
+ while (true) {
+ if (TBaseStream::IsEmpty()) {
+ TBaseStream::Refresh();
+ }
+ char ch = *TBaseStream::Current();
+ TBaseStream::Advance(1);
+ if (ch != '"') {
+ PushBack(ch);
+ } else {
+ // We must count the number of '\' at the end of StringValue
+ // to check if it's not \"
+ int slashCount = 0;
+ int length = Buffer_.size();
+ while (slashCount < length && Buffer_[length - 1 - slashCount] == '\\') {
+ ++slashCount;
+ }
+ if (slashCount % 2 == 0) {
+ break;
+ } else {
+ PushBack(ch);
+ }
+ }
+ }
+
+ auto unquotedValue = UnescapeC(Buffer_.data(), Buffer_.size());
+ Buffer_.clear();
+ Insert(unquotedValue.data(), unquotedValue.data() + unquotedValue.size());
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+
+ template <bool AllowFinish>
+ TStringBuf ReadUnquotedString()
+ {
+ Buffer_.clear();
+ while (true) {
+ char ch = TBaseStream::template GetChar<AllowFinish>();
+ if (isalpha(ch) || isdigit(ch) ||
+ ch == '_' || ch == '-' || ch == '%' || ch == '.')
+ {
+ PushBack(ch);
+ } else {
+ break;
+ }
+ TBaseStream::Advance(1);
+ }
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+
+ TStringBuf ReadUnquotedString()
+ {
+ return ReadUnquotedString<false>();
+ }
+
+ TStringBuf ReadBinaryString()
+ {
+ ui32 ulength = TBaseStream::ReadVarint32();
+
+ i32 length = ZigZagDecode32(ulength);
+ if (length < 0) {
+ THROW_ERROR_EXCEPTION("Negative binary string literal length %v",
+ length)
+ << *this;
+ }
+
+ if (TBaseStream::Current() + length <= TBaseStream::End()) {
+ auto result = TStringBuf(TBaseStream::Current(), length);
+ TBaseStream::Advance(length);
+ return result;
+ } else {
+ size_t needToRead = length;
+ Buffer_.clear();
+ while (needToRead > 0) {
+ if (TBaseStream::IsEmpty()) {
+ TBaseStream::Refresh();
+ continue;
+ }
+ size_t readingBytes = std::min(needToRead, TBaseStream::Length());
+ Insert(TBaseStream::Current(), TBaseStream::Current() + readingBytes);
+ needToRead -= readingBytes;
+ TBaseStream::Advance(readingBytes);
+ }
+ return TStringBuf(Buffer_.data(), Buffer_.size());
+ }
+ }
+
+ template <bool AllowFinish>
+ bool ReadBoolean()
+ {
+ Buffer_.clear();
+
+ static const TStringBuf trueString = "true";
+ static const TStringBuf falseString = "false";
+
+ auto throwIncorrectBoolean = [&] () {
+ THROW_ERROR CreateLiteralError(ETokenType::Boolean, Buffer_.data(), Buffer_.size());
+ };
+
+ PushBack(TBaseStream::template GetChar<AllowFinish>());
+ TBaseStream::Advance(1);
+ if (Buffer_[0] == trueString[0]) {
+ for (int i = 1; i < static_cast<int>(trueString.size()); ++i) {
+ PushBack(TBaseStream::template GetChar<AllowFinish>());
+ TBaseStream::Advance(1);
+ if (Buffer_.back() != trueString[i]) {
+ throwIncorrectBoolean();
+ }
+ }
+ return true;
+ } else if (Buffer_[0] == falseString[0]) {
+ for (int i = 1; i < static_cast<int>(falseString.size()); ++i) {
+ PushBack(TBaseStream::template GetChar<AllowFinish>());
+ TBaseStream::Advance(1);
+ if (Buffer_.back() != falseString[i]) {
+ throwIncorrectBoolean();
+ }
+ }
+ return false;
+ } else {
+ throwIncorrectBoolean();
+ }
+
+ YT_ABORT();
+ }
+
+ i64 ReadBinaryInt64()
+ {
+ ui64 uvalue = TBaseStream::ReadVarint64();
+ return ZigZagDecode64(uvalue);
+ }
+
+ ui64 ReadBinaryUint64()
+ {
+ return TBaseStream::ReadVarint64();
+ }
+
+ double ReadBinaryDouble()
+ {
+ size_t needToRead = sizeof(double);
+
+ double result;
+ while (needToRead != 0) {
+ if (TBaseStream::IsEmpty()) {
+ TBaseStream::Refresh();
+ continue;
+ }
+
+ size_t chunkSize = std::min(needToRead, TBaseStream::Length());
+ if (chunkSize == 0) {
+ THROW_ERROR_EXCEPTION("Error parsing binary double literal")
+ << *this;
+ }
+ std::copy(
+ TBaseStream::Current(),
+ TBaseStream::Current() + chunkSize,
+ reinterpret_cast<char*>(&result) + (sizeof(double) - needToRead));
+ needToRead -= chunkSize;
+ TBaseStream::Advance(chunkSize);
+ }
+ return result;
+ }
+
+ /// Helpers
+ void SkipCharToken(char symbol)
+ {
+ char ch = SkipSpaceAndGetChar();
+ if (ch != symbol) {
+ THROW_ERROR_EXCEPTION("Expected %Qv but found %Qv",
+ symbol,
+ ch)
+ << *this;
+ }
+
+ TBaseStream::Advance(1);
+ }
+
+ template <bool AllowFinish>
+ char SkipSpaceAndGetChar()
+ {
+ if (!TBaseStream::IsEmpty()) {
+ char ch = *TBaseStream::Current();
+ if (!IsSpace(ch)) {
+ return ch;
+ }
+ }
+ return SkipSpaceAndGetCharFallback<AllowFinish>();
+ }
+
+ char SkipSpaceAndGetChar()
+ {
+ return SkipSpaceAndGetChar<false>();
+ }
+
+ template <bool AllowFinish>
+ char SkipSpaceAndGetCharFallback()
+ {
+ while (true) {
+ if (TBaseStream::IsEmpty()) {
+ if (TBaseStream::IsFinished()) {
+ return '\0';
+ }
+ TBaseStream::template Refresh<AllowFinish>();
+ continue;
+ }
+ if (!IsSpace(*TBaseStream::Current())) {
+ break;
+ }
+ TBaseStream::Advance(1);
+ }
+ return TBaseStream::template GetChar<AllowFinish>();
+ }
+};
+////////////////////////////////////////////////////////////////////////////////
+/*! \endinternal */
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStringReader
+{
+private:
+ const char* BeginPtr;
+ const char* CurrentPtr;
+ const char* EndPtr;
+
+public:
+ TStringReader()
+ : BeginPtr(nullptr)
+ , CurrentPtr(nullptr)
+ , EndPtr(nullptr)
+ { }
+
+ TStringReader(const char* begin, const char* end)
+ : BeginPtr(begin)
+ , CurrentPtr(begin)
+ , EndPtr(end)
+ { }
+
+ const char* Begin() const
+ {
+ return BeginPtr;
+ }
+
+ const char* Current() const
+ {
+ return CurrentPtr;
+ }
+
+ const char* End() const
+ {
+ return EndPtr;
+ }
+
+ void RefreshBlock()
+ {
+ YT_ABORT();
+ }
+
+ void Advance(size_t bytes)
+ {
+ CurrentPtr += bytes;
+ }
+
+ bool IsFinished() const
+ {
+ return true;
+ }
+
+ void SetBuffer(const char* begin, const char* end)
+ {
+ BeginPtr = begin;
+ CurrentPtr = begin;
+ EndPtr = end;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TParserCoroutine>
+class TBlockReader
+{
+private:
+ TParserCoroutine& Coroutine;
+
+ const char* BeginPtr;
+ const char* CurrentPtr;
+ const char* EndPtr;
+ bool FinishFlag;
+
+public:
+ TBlockReader(
+ TParserCoroutine& coroutine,
+ const char* begin,
+ const char* end,
+ bool finish)
+ : Coroutine(coroutine)
+ , BeginPtr(begin)
+ , CurrentPtr(begin)
+ , EndPtr(end)
+ , FinishFlag(finish)
+ { }
+
+ const char* Begin() const
+ {
+ return BeginPtr;
+ }
+
+ const char* Current() const
+ {
+ return CurrentPtr;
+ }
+
+ const char* End() const
+ {
+ return EndPtr;
+ }
+
+ void RefreshBlock()
+ {
+ std::tie(BeginPtr, EndPtr, FinishFlag) = Coroutine.Yield(0);
+ CurrentPtr = BeginPtr;
+ }
+
+ void Advance(size_t bytes)
+ {
+ CurrentPtr += bytes;
+ }
+
+ bool IsFinished() const
+ {
+ return FinishFlag;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/format.h b/yt/yt/core/yson/format.h
new file mode 100644
index 0000000000..bc5309ecab
--- /dev/null
+++ b/yt/yt/core/yson/format.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "token.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Indicates the beginning of a list.
+const ETokenType BeginListToken = ETokenType::LeftBracket;
+//! Indicates the end of a list.
+const ETokenType EndListToken = ETokenType::RightBracket;
+
+//! Indicates the beginning of a map.
+const ETokenType BeginMapToken = ETokenType::LeftBrace;
+//! Indicates the end of a map.
+const ETokenType EndMapToken = ETokenType::RightBrace;
+
+//! Indicates the beginning of an attribute map.
+const ETokenType BeginAttributesToken = ETokenType::LeftAngle;
+//! Indicates the end of an attribute map.
+const ETokenType EndAttributesToken = ETokenType::RightAngle;
+
+//! Separates items in maps and lists.
+const ETokenType ItemSeparatorToken = ETokenType::Semicolon;
+//! Separates keys from values in maps.
+const ETokenType KeyValueSeparatorToken = ETokenType::Equals;
+
+//! Indicates an entity.
+const ETokenType EntityToken = ETokenType::Hash;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/forwarding_consumer.cpp b/yt/yt/core/yson/forwarding_consumer.cpp
new file mode 100644
index 0000000000..a988b07672
--- /dev/null
+++ b/yt/yt/core/yson/forwarding_consumer.cpp
@@ -0,0 +1,330 @@
+#include "forwarding_consumer.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TForwardingYsonConsumer::Forward(
+ IYsonConsumer* consumer,
+ std::function<void()> onFinished,
+ EYsonType type)
+{
+ Forward(std::vector{consumer}, std::move(onFinished), type);
+}
+
+void TForwardingYsonConsumer::Forward(
+ std::vector<IYsonConsumer*> consumers,
+ std::function<void()> onFinished,
+ EYsonType type)
+{
+ YT_ASSERT(ForwardingConsumers_.empty());
+ YT_ASSERT(!consumers.empty());
+ YT_ASSERT(ForwardingDepth_ == 0);
+
+ ForwardingConsumers_ = std::move(consumers);
+ OnFinished_ = std::move(onFinished);
+ ForwardingType_ = type;
+}
+
+bool TForwardingYsonConsumer::CheckForwarding(int depthDelta)
+{
+ if (ForwardingDepth_ + depthDelta < 0) {
+ FinishForwarding();
+ }
+ return !ForwardingConsumers_.empty();
+}
+
+void TForwardingYsonConsumer::UpdateDepth(int depthDelta, bool checkFinish)
+{
+ ForwardingDepth_ += depthDelta;
+ YT_ASSERT(ForwardingDepth_ >= 0);
+ if (checkFinish && ForwardingType_ == EYsonType::Node && ForwardingDepth_ == 0) {
+ FinishForwarding();
+ }
+}
+
+void TForwardingYsonConsumer::FinishForwarding()
+{
+ ForwardingConsumers_.clear();
+ if (OnFinished_) {
+ OnFinished_();
+ OnFinished_ = nullptr;
+ }
+}
+
+void TForwardingYsonConsumer::OnStringScalar(TStringBuf value)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnStringScalar(value);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyStringScalar(value);
+ }
+}
+
+void TForwardingYsonConsumer::OnInt64Scalar(i64 value)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnInt64Scalar(value);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyInt64Scalar(value);
+ }
+}
+
+void TForwardingYsonConsumer::OnUint64Scalar(ui64 value)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnUint64Scalar(value);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyUint64Scalar(value);
+ }
+}
+
+void TForwardingYsonConsumer::OnDoubleScalar(double value)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnDoubleScalar(value);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyDoubleScalar(value);
+ }
+}
+
+void TForwardingYsonConsumer::OnBooleanScalar(bool value)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnBooleanScalar(value);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyBooleanScalar(value);
+ }
+}
+
+void TForwardingYsonConsumer::OnEntity()
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnEntity();
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyEntity();
+ }
+}
+
+void TForwardingYsonConsumer::OnBeginList()
+{
+ if (CheckForwarding(+1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnBeginList();
+ }
+ UpdateDepth(+1);
+ } else {
+ OnMyBeginList();
+ }
+}
+
+void TForwardingYsonConsumer::OnListItem()
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnListItem();
+ }
+ } else {
+ OnMyListItem();
+ }
+}
+
+void TForwardingYsonConsumer::OnEndList()
+{
+ if (CheckForwarding(-1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnEndList();
+ }
+ UpdateDepth(-1);
+ } else {
+ OnMyEndList();
+ }
+}
+
+void TForwardingYsonConsumer::OnBeginMap()
+{
+ if (CheckForwarding(+1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnBeginMap();
+ }
+ UpdateDepth(+1);
+ } else {
+ OnMyBeginMap();
+ }
+}
+
+void TForwardingYsonConsumer::OnKeyedItem(TStringBuf name)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnKeyedItem(name);
+ }
+ } else {
+ OnMyKeyedItem(name);
+ }
+}
+
+void TForwardingYsonConsumer::OnEndMap()
+{
+ if (CheckForwarding(-1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnEndMap();
+ }
+ UpdateDepth(-1);
+ } else {
+ OnMyEndMap();
+ }
+}
+
+void TForwardingYsonConsumer::OnRaw(TStringBuf yson, EYsonType type)
+{
+ if (CheckForwarding()) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnRaw(yson, type);
+ }
+ UpdateDepth(0);
+ } else {
+ OnMyRaw(yson, type);
+ }
+}
+
+void TForwardingYsonConsumer::OnBeginAttributes()
+{
+ if (CheckForwarding(+1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnBeginAttributes();
+ }
+ UpdateDepth(+1);
+ } else {
+ OnMyBeginAttributes();
+ }
+}
+
+void TForwardingYsonConsumer::OnEndAttributes()
+{
+ if (CheckForwarding(-1)) {
+ for (auto consumer : ForwardingConsumers_) {
+ consumer->OnEndAttributes();
+ }
+ UpdateDepth(-1, false);
+ } else {
+ OnMyEndAttributes();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TForwardingYsonConsumer::OnMyStringScalar(TStringBuf /*value*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyInt64Scalar(i64 /*value*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyUint64Scalar(ui64 /*value*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyDoubleScalar(double /*value*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyBooleanScalar(bool /*value*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyEntity()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyBeginList()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyListItem()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyEndList()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyBeginMap()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyKeyedItem(TStringBuf /*name*/)
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyEndMap()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyBeginAttributes()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyEndAttributes()
+{
+ YT_ABORT();
+}
+
+void TForwardingYsonConsumer::OnMyRaw(TStringBuf yson, EYsonType type)
+{
+ TYsonConsumerBase::OnRaw(yson, type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTeeYsonConsumer::TTeeYsonConsumer(
+ std::vector<IYsonConsumer*> consumers,
+ std::vector<std::unique_ptr<IYsonConsumer>> ownedConsumers,
+ EYsonType type)
+ : OwnedConsumers_(std::move(ownedConsumers))
+{
+ consumers.reserve(consumers.size() + ownedConsumers.size());
+ for (const auto& consumer : OwnedConsumers_) {
+ consumers.push_back(consumer.get());
+ }
+
+ Forward(std::move(consumers), /*onFinished*/ {}, type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/forwarding_consumer.h b/yt/yt/core/yson/forwarding_consumer.h
new file mode 100644
index 0000000000..1c2bc4944c
--- /dev/null
+++ b/yt/yt/core/yson/forwarding_consumer.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include "string.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TForwardingYsonConsumer
+ : public virtual TYsonConsumerBase
+{
+public:
+ // IYsonConsumer methods
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+ void OnRaw(TStringBuf yson, EYsonType type) override;
+
+protected:
+ void Forward(
+ IYsonConsumer* consumer,
+ std::function<void()> onFinished = nullptr,
+ EYsonType type = EYsonType::Node);
+
+ void Forward(
+ std::vector<IYsonConsumer*> consumers,
+ std::function<void()> onFinished = nullptr,
+ EYsonType type = EYsonType::Node);
+
+ virtual void OnMyStringScalar(TStringBuf value);
+ virtual void OnMyInt64Scalar(i64 value);
+ virtual void OnMyUint64Scalar(ui64 value);
+ virtual void OnMyDoubleScalar(double value);
+ virtual void OnMyBooleanScalar(bool value);
+ virtual void OnMyEntity();
+
+ virtual void OnMyBeginList();
+ virtual void OnMyListItem();
+ virtual void OnMyEndList();
+
+ virtual void OnMyBeginMap();
+ virtual void OnMyKeyedItem(TStringBuf key);
+ virtual void OnMyEndMap();
+
+ virtual void OnMyBeginAttributes();
+ virtual void OnMyEndAttributes();
+
+ virtual void OnMyRaw(TStringBuf yson, EYsonType type);
+
+private:
+ std::vector<IYsonConsumer*> ForwardingConsumers_;
+ int ForwardingDepth_ = 0;
+ EYsonType ForwardingType_;
+ std::function<void()> OnFinished_;
+
+ bool CheckForwarding(int depthDelta = 0);
+ void UpdateDepth(int depthDelta, bool checkFinish = true);
+ void FinishForwarding();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This is the most basic forwarding consumer which forwards all the events to given consumers.
+class TTeeYsonConsumer
+ : public TForwardingYsonConsumer
+{
+public:
+ explicit TTeeYsonConsumer(
+ std::vector<IYsonConsumer*> consumers,
+ std::vector<std::unique_ptr<IYsonConsumer>> ownedConsumers = {},
+ EYsonType type = EYsonType::Node);
+
+private:
+ std::vector<std::unique_ptr<IYsonConsumer>> OwnedConsumers_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/lexer.cpp b/yt/yt/core/yson/lexer.cpp
new file mode 100644
index 0000000000..f3d7bd5108
--- /dev/null
+++ b/yt/yt/core/yson/lexer.cpp
@@ -0,0 +1,32 @@
+#include "lexer.h"
+#include "lexer_detail.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStatelessLexer::TStatelessLexer()
+ : Impl(std::make_unique<TStatelesYsonLexerImpl<false>>())
+{ }
+
+TStatelessLexer::~TStatelessLexer()
+{ }
+
+size_t TStatelessLexer::GetToken(TStringBuf data, TToken* token)
+{
+ return Impl->GetToken(data, token);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetToken(TStringBuf data, TToken* token)
+{
+ NDetail::TLexer<TStringReader, false> Lexer = NDetail::TLexer<TStringReader, false>(TStringReader());
+ Lexer.SetBuffer(data.begin(), data.end());
+ Lexer.GetToken(token);
+ return Lexer.Current() - data.begin();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/lexer.h b/yt/yt/core/yson/lexer.h
new file mode 100644
index 0000000000..90619e3859
--- /dev/null
+++ b/yt/yt/core/yson/lexer.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+#include "token.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatelessYsonLexerImplBase;
+
+class TStatelessLexer
+{
+public:
+ TStatelessLexer();
+
+ ~TStatelessLexer();
+
+ size_t GetToken(TStringBuf data, TToken* token);
+
+private:
+ std::unique_ptr<TStatelessYsonLexerImplBase> Impl;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+size_t GetToken(TStringBuf data, TToken* token);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/lexer_detail.h b/yt/yt/core/yson/lexer_detail.h
new file mode 100644
index 0000000000..9d2f937125
--- /dev/null
+++ b/yt/yt/core/yson/lexer_detail.h
@@ -0,0 +1,305 @@
+#pragma once
+
+#include "detail.h"
+#include "token.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+/*! \internal */
+////////////////////////////////////////////////////////////////////////////////
+
+// EReadStartCase tree representation:
+// Root = xb
+// BinaryStringOrOtherSpecialToken = x0b
+// BinaryString = 00b
+// OtherSpecialToken = 10b
+// Other = x1b
+// BinaryScalar = xx01b
+// BinaryInt64 = 0001b
+// BinaryDouble = 0101b
+// BinaryFalse = 1001b
+// BinaryTrue = 1101b
+// Other = xxx11b
+// Quote = 00011b
+// DigitOrMinus = 00111b
+// String = 01011b
+// Space = 01111b
+// Plus = 10011b
+// None = 10111b
+// Percent = 11011b
+DEFINE_BIT_ENUM_WITH_UNDERLYING_TYPE(EReadStartCase, ui8,
+ ((BinaryString) (0)) // = 00b
+ ((OtherSpecialToken) (2)) // = 10b
+
+ ((BinaryInt64) (1)) // = 001b
+ ((BinaryDouble) (5)) // = 101b
+ ((BinaryFalse) (9)) // = 1001b
+ ((BinaryTrue) (13)) // = 1101b
+ ((BinaryUint64) (17)) // = 10001b
+
+ ((Quote) (3)) // = 00011b
+ ((DigitOrMinus) (7)) // = 00111b
+ ((String) (11)) // = 01011b
+ ((Space) (15)) // = 01111b
+ ((Plus) (19)) // = 10011b
+ ((None) (23)) // = 10111b
+ ((Percent) (27)) // = 11011b
+);
+
+template <class TBlockStream, bool EnableLinePositionInfo>
+class TLexer
+ : public TLexerBase<TBlockStream, EnableLinePositionInfo>
+{
+private:
+ typedef TLexerBase<TBlockStream, EnableLinePositionInfo> TBase;
+
+ static EReadStartCase GetStartState(char ch)
+ {
+#define NN EReadStartCase::None
+#define BS EReadStartCase::BinaryString
+#define BI EReadStartCase::BinaryInt64
+#define BD EReadStartCase::BinaryDouble
+#define BF EReadStartCase::BinaryFalse
+#define BT EReadStartCase::BinaryTrue
+#define BU EReadStartCase::BinaryUint64
+#define SP NN // EReadStartCase::Space
+#define DM EReadStartCase::DigitOrMinus
+#define ST EReadStartCase::String
+#define PL EReadStartCase::Plus
+#define QU EReadStartCase::Quote
+#define PC EReadStartCase::Percent
+#define TT(name) (EReadStartCase(static_cast<ui8>(ETokenType::name) << 2) | EReadStartCase::OtherSpecialToken)
+
+ static const EReadStartCase lookupTable[] =
+ {
+ NN,BS,BI,BD,BF,BT,BU,NN,NN,SP,SP,SP,SP,SP,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+
+ // 32
+ SP, // ' '
+ NN, // '!'
+ QU, // '"'
+ TT(Hash), // '#'
+ NN, // '$'
+ PC, // '%'
+ NN, // '&'
+ NN, // "'"
+ TT(LeftParenthesis), // '('
+ TT(RightParenthesis), // ')'
+ NN, // '*'
+ PL, // '+'
+ TT(Comma), // ','
+ DM, // '-'
+ NN, // '.'
+ TT(Slash), // '/'
+
+ // 48
+ DM,DM,DM,DM,DM,DM,DM,DM,DM,DM, // '0' - '9'
+ TT(Colon), // ':'
+ TT(Semicolon), // ';'
+ TT(LeftAngle), // '<'
+ TT(Equals), // '='
+ TT(RightAngle), // '>'
+ NN, // '?'
+
+ // 64
+ NN, // '@'
+ ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST, // 'A' - 'M'
+ ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST, // 'N' - 'Z'
+ TT(LeftBracket), // '['
+ NN, // '\'
+ TT(RightBracket), // ']'
+ NN, // '^'
+ ST, // '_'
+
+ // 96
+ NN, // '`'
+
+ ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST, // 'a' - 'm'
+ ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST,ST, // 'n' - 'z'
+ TT(LeftBrace), // '{'
+ NN, // '|'
+ TT(RightBrace), // '}'
+ NN, // '~'
+ NN, // '^?' non-printable
+ // 128
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,
+ NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN,NN
+ };
+
+#undef NN
+#undef BS
+#undef BI
+#undef BD
+#undef SP
+#undef DM
+#undef ST
+#undef PL
+#undef QU
+#undef TT
+ return lookupTable[static_cast<ui8>(ch)];
+ }
+
+public:
+ TLexer(
+ const TBlockStream& blockStream,
+ i64 memoryLimit = std::numeric_limits<i64>::max())
+ : TBase(blockStream, memoryLimit)
+ { }
+
+ void GetToken(TToken* token)
+ {
+ char ch = TBase::SkipSpaceAndGetChar();
+ auto state = GetStartState(ch);
+ auto stateBits = static_cast<unsigned>(state);
+
+ if (ch == '\0') {
+ *token = TToken::EndOfStream;
+ return;
+ }
+
+ if (stateBits & 1) { // Other = x1b
+ if (stateBits & 1 << 1) { // Other = xxx11b
+ if (state == EReadStartCase::Quote) {
+ TBase::Advance(1);
+ TStringBuf value = TBase::ReadQuotedString();
+ *token = TToken(value);
+ } else if (state == EReadStartCase::DigitOrMinus) {
+ ReadNumeric<true>(token);
+ } else if (state == EReadStartCase::Plus) {
+ TBase::Advance(1);
+
+ char ch = TBase::template GetChar<true>();
+
+ if (!isdigit(ch)) {
+ *token = TToken(ETokenType::Plus);
+ } else {
+ ReadNumeric<true>(token);
+ }
+ } else if (state == EReadStartCase::String) {
+ TStringBuf value = TBase::template ReadUnquotedString<true>();
+ *token = TToken(value);
+ } else if (state == EReadStartCase::Percent) {
+ TBase::Advance(1);
+ char ch = TBase::template GetChar<true>();
+ if (ch == 't' || ch == 'f') {
+ *token = TToken(TBase::template ReadBoolean<true>());
+ } else {
+ *token = TToken(TBase::template ReadNanOrInf<true>());
+ }
+ } else { // None
+ YT_ASSERT(state == EReadStartCase::None);
+ THROW_ERROR_EXCEPTION("Unexpected %Qv",
+ ch);
+ }
+ } else { // BinaryScalar = x01b
+ TBase::Advance(1);
+ if (state == EReadStartCase::BinaryDouble) {
+ double value = TBase::ReadBinaryDouble();
+ *token = TToken(value);
+ } else if (state == EReadStartCase::BinaryInt64) {
+ i64 value = TBase::ReadBinaryInt64();
+ *token = TToken(value);
+ } else if (state == EReadStartCase::BinaryUint64) {
+ ui64 value = TBase::ReadBinaryUint64();
+ *token = TToken(value);
+ } else if (state == EReadStartCase::BinaryFalse) {
+ *token = TToken(false);
+ } else if (state == EReadStartCase::BinaryTrue) {
+ *token = TToken(true);
+ } else {
+ YT_ABORT();
+ }
+ }
+ } else { // BinaryStringOrOtherSpecialToken = x0b
+ TBase::Advance(1);
+ if (stateBits & 1 << 1) { // OtherSpecialToken = 10b
+ YT_ASSERT((stateBits & 3) == static_cast<unsigned>(EReadStartCase::OtherSpecialToken));
+ *token = TToken(ETokenType(stateBits >> 2));
+ } else { // BinaryString = 00b
+ YT_ASSERT((stateBits & 3) == static_cast<unsigned>(EReadStartCase::BinaryString));
+ TStringBuf value = TBase::ReadBinaryString();
+ *token = TToken(value);
+ }
+ }
+ }
+
+ template <bool AllowFinish>
+ void ReadNumeric(TToken* token)
+ {
+ TStringBuf valueBuffer;
+ ENumericResult numericResult = TBase::template ReadNumeric<AllowFinish>(&valueBuffer);
+
+ if (numericResult == ENumericResult::Double) {
+ try {
+ *token = TToken(FromString<double>(valueBuffer));
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Double, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ } else if (numericResult == ENumericResult::Int64) {
+ try {
+ *token = TToken(FromString<i64>(valueBuffer));
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Int64, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ } else if (numericResult == ENumericResult::Uint64) {
+ try {
+ *token = TToken(FromString<ui64>(valueBuffer.SubStr(0, valueBuffer.size() - 1)));
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Int64, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ }
+ }
+};
+////////////////////////////////////////////////////////////////////////////////
+/*! \endinternal */
+} // namespace NDetail
+
+class TStatelessYsonLexerImplBase
+{
+public:
+ virtual size_t GetToken(TStringBuf data, TToken* token) = 0;
+
+ virtual ~TStatelessYsonLexerImplBase() = default;
+};
+
+template <bool EnableLinePositionInfo>
+class TStatelesYsonLexerImpl
+ : public TStatelessYsonLexerImplBase
+{
+private:
+ typedef NDetail::TLexer<TStringReader, EnableLinePositionInfo> TLexer;
+ TLexer Lexer;
+
+public:
+ TStatelesYsonLexerImpl()
+ : Lexer(TStringReader())
+ { }
+
+ size_t GetToken(TStringBuf data, TToken* token) override
+ {
+ Lexer.SetBuffer(data.begin(), data.end());
+ Lexer.GetToken(token);
+ return Lexer.Current() - data.begin();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/null_consumer.cpp b/yt/yt/core/yson/null_consumer.cpp
new file mode 100644
index 0000000000..d0fe3ccf3b
--- /dev/null
+++ b/yt/yt/core/yson/null_consumer.cpp
@@ -0,0 +1,62 @@
+#include "null_consumer.h"
+#include "string.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TNullYsonConsumer::OnStringScalar(TStringBuf /*value*/)
+{ }
+
+void TNullYsonConsumer::OnInt64Scalar(i64 /*value*/)
+{ }
+
+void TNullYsonConsumer::OnUint64Scalar(ui64 /*value*/)
+{ }
+
+void TNullYsonConsumer::OnDoubleScalar(double /*value*/)
+{ }
+
+void TNullYsonConsumer::OnBooleanScalar(bool /*value*/)
+{ }
+
+void TNullYsonConsumer::OnEntity()
+{ }
+
+void TNullYsonConsumer::OnBeginList()
+{ }
+
+void TNullYsonConsumer::OnListItem()
+{ }
+
+void TNullYsonConsumer::OnEndList()
+{ }
+
+void TNullYsonConsumer::OnBeginMap()
+{ }
+
+void TNullYsonConsumer::OnKeyedItem(TStringBuf /*name*/)
+{ }
+
+void TNullYsonConsumer::OnEndMap()
+{ }
+
+void TNullYsonConsumer::OnBeginAttributes()
+{ }
+
+void TNullYsonConsumer::OnEndAttributes()
+{ }
+
+void TNullYsonConsumer::OnRaw(TStringBuf /*yson*/, EYsonType /*type*/)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYsonConsumer* GetNullYsonConsumer()
+{
+ return Singleton<TNullYsonConsumer>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/null_consumer.h b/yt/yt/core/yson/null_consumer.h
new file mode 100644
index 0000000000..7ddc3066f6
--- /dev/null
+++ b/yt/yt/core/yson/null_consumer.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "consumer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Consumer that does nothing.
+class TNullYsonConsumer
+ : public virtual IYsonConsumer
+{
+ void OnStringScalar(TStringBuf /*value*/) override;
+
+ void OnInt64Scalar(i64 /*value*/) override;
+
+ void OnUint64Scalar(ui64 /*value*/) override;
+
+ void OnDoubleScalar(double /*value*/) override;
+
+ void OnBooleanScalar(bool /*value*/) override;
+
+ void OnEntity() override;
+
+ void OnBeginList() override;
+
+ void OnListItem() override;
+
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+
+ void OnKeyedItem(TStringBuf /*name*/) override;
+
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+
+ void OnEndAttributes() override;
+
+ void OnRaw(TStringBuf /*yson*/, EYsonType /*type*/) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Returns the singleton instance of the null consumer.
+IYsonConsumer* GetNullYsonConsumer();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/parser.cpp b/yt/yt/core/yson/parser.cpp
new file mode 100644
index 0000000000..e04515cc10
--- /dev/null
+++ b/yt/yt/core/yson/parser.cpp
@@ -0,0 +1,151 @@
+#include "parser.h"
+#include "consumer.h"
+#include "format.h"
+#include "parser_detail.h"
+
+#include <yt/yt/core/actions/bind.h>
+
+namespace NYT::NYson {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonParser::TImpl
+{
+private:
+ typedef TCoroutine<int(const char* begin, const char* end, bool finish)> TParserCoroutine;
+
+ TParserCoroutine ParserCoroutine_;
+ TParserYsonStreamImpl<IYsonConsumer, TBlockReader<TParserCoroutine>> Parser_;
+
+public:
+ TImpl(IYsonConsumer* consumer, EYsonType parsingMode, TYsonParserConfig config)
+ : ParserCoroutine_(BIND(
+ [=, this, config = std::move(config)] (TParserCoroutine& self, const char* begin, const char* end, bool finish) {
+ Parser_.DoParse(
+ TBlockReader<TParserCoroutine>(self, begin, end, finish),
+ consumer,
+ parsingMode,
+ config);
+ }))
+ { }
+
+ void Read(const char* begin, const char* end, bool finish = false)
+ {
+ if (!ParserCoroutine_.IsCompleted()) {
+ ParserCoroutine_.Run(begin, end, finish);
+ } else {
+ THROW_ERROR_EXCEPTION("Input is already parsed");
+ }
+ }
+
+ void Read(TStringBuf data, bool finish = false)
+ {
+ Read(data.begin(), data.end(), finish);
+ }
+
+ void Finish()
+ {
+ Read(0, 0, true);
+ }
+
+ const char* GetCurrentPositionInBlock()
+ {
+ return Parser_.GetCurrentPositionInBlock();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonParser::TYsonParser(IYsonConsumer* consumer, EYsonType type, TYsonParserConfig config)
+ : Impl(std::make_unique<TImpl>(consumer, type, std::move(config)))
+{ }
+
+TYsonParser::~TYsonParser()
+{ }
+
+void TYsonParser::Read(const char* begin, const char* end, bool finish)
+{
+ Impl->Read(begin, end, finish);
+}
+
+void TYsonParser::Read(TStringBuf data)
+{
+ Impl->Read(data);
+}
+
+void TYsonParser::Finish()
+{
+ Impl->Finish();
+}
+
+const char* TYsonParser::GetCurrentPositionInBlock()
+{
+ return Impl->GetCurrentPositionInBlock();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatelessYsonParser::TImpl
+{
+private:
+ const std::unique_ptr<TStatelessYsonParserImplBase> Impl;
+
+public:
+ TImpl(IYsonConsumer* consumer, TYsonParserConfig config)
+ : Impl([&] () -> TStatelessYsonParserImplBase* {
+ if (config.EnableContext && config.EnableLinePositionInfo) {
+ return new TStatelessYsonParserImpl<IYsonConsumer, 64, true>(consumer, config.MemoryLimit, config.NestingLevelLimit);
+ } else if (config.EnableContext && !config.EnableLinePositionInfo) {
+ return new TStatelessYsonParserImpl<IYsonConsumer, 64, false>(consumer, config.MemoryLimit, config.NestingLevelLimit);
+ } else if (!config.EnableContext && config.EnableLinePositionInfo) {
+ return new TStatelessYsonParserImpl<IYsonConsumer, 0, true>(consumer, config.MemoryLimit, config.NestingLevelLimit);
+ } else {
+ return new TStatelessYsonParserImpl<IYsonConsumer, 0, false>(consumer, config.MemoryLimit, config.NestingLevelLimit);
+ }
+ }())
+ { }
+
+ void Parse(TStringBuf data, EYsonType type = EYsonType::Node)
+ {
+ Impl->Parse(data, type);
+ }
+
+ void Stop()
+ {
+ Impl->Stop();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStatelessYsonParser::TStatelessYsonParser(IYsonConsumer* consumer, TYsonParserConfig config)
+ : Impl(std::make_unique<TImpl>(consumer, config))
+{ }
+
+TStatelessYsonParser::~TStatelessYsonParser()
+{ }
+
+void TStatelessYsonParser::Parse(TStringBuf data, EYsonType type)
+{
+ Impl->Parse(data, type);
+}
+
+void TStatelessYsonParser::Stop()
+{
+ Impl->Stop();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYsonStringBuffer(TStringBuf buffer, EYsonType type, IYsonConsumer* consumer, TYsonParserConfig config)
+{
+ TParserYsonStreamImpl<IYsonConsumer, TStringReader> parser;
+ TStringReader reader(buffer.begin(), buffer.end());
+ parser.DoParse(reader, consumer, type, config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/parser.h b/yt/yt/core/yson/parser.h
new file mode 100644
index 0000000000..b370811e84
--- /dev/null
+++ b/yt/yt/core/yson/parser.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "public.h"
+#include "detail.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! @brief Configuration of yson parser.
+struct TYsonParserConfig
+{
+ //! @brief Enable info about line in error messages
+ //!
+ //! Makes error messages friendlier but slows down parsing.
+ bool EnableLinePositionInfo = false;
+
+ i64 MemoryLimit = std::numeric_limits<i64>::max();
+
+ //! @brief Enable context dumping in error messages
+ //!
+ //! Makes error messages friendlier but slightly slows down parsing.
+ bool EnableContext = true;
+
+ //! @brief Maximum level of nesting of lists and maps.
+ int NestingLevelLimit = DefaultYsonParserNestingLevelLimit;
+};
+
+class TYsonParser
+{
+public:
+ TYsonParser(IYsonConsumer* consumer, EYsonType type = EYsonType::Node, TYsonParserConfig config = {});
+ ~TYsonParser();
+
+ void Read(const char* begin, const char* end, bool finish = false);
+ void Read(TStringBuf data);
+ void Finish();
+
+ const char* GetCurrentPositionInBlock();
+
+private:
+ class TImpl;
+ const std::unique_ptr<TImpl> Impl;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatelessYsonParser
+{
+public:
+ TStatelessYsonParser(IYsonConsumer* consumer, TYsonParserConfig config = {});
+ ~TStatelessYsonParser();
+
+ void Parse(TStringBuf data, EYsonType type = EYsonType::Node);
+ void Stop();
+
+private:
+ class TImpl;
+ const std::unique_ptr<TImpl> Impl;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ParseYsonStringBuffer(TStringBuf buffer, EYsonType type, IYsonConsumer* consumer, TYsonParserConfig config = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/parser_detail.h b/yt/yt/core/yson/parser_detail.h
new file mode 100644
index 0000000000..84d4335767
--- /dev/null
+++ b/yt/yt/core/yson/parser_detail.h
@@ -0,0 +1,527 @@
+#pragma once
+
+#include "detail.h"
+
+// For the sake of sane code completion.
+#include "parser.h"
+
+#include <yt/yt/core/misc/finally.h>
+
+#include <util/string/escape.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+/*! \internal */
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TConsumer, class TBlockStream, size_t MaxContextSize, bool EnableLinePositionInfo>
+class TParser
+ : public TLexerBase<TReaderWithContext<TBlockStream, MaxContextSize>, EnableLinePositionInfo>
+{
+private:
+ typedef TLexerBase<TReaderWithContext<TBlockStream, MaxContextSize>, EnableLinePositionInfo> TBase;
+ TConsumer* Consumer;
+ int NestingLevel = 0;
+ int NestingLevelLimit;
+ bool Stopped_ = false;
+
+public:
+ TParser(
+ const TBlockStream& blockStream,
+ TConsumer* consumer,
+ i64 memoryLimit,
+ int nestingLevelLimit)
+ : TBase(blockStream, memoryLimit)
+ , Consumer(consumer)
+ , NestingLevelLimit(nestingLevelLimit)
+ { }
+
+ void DoParse(EYsonType parsingMode)
+ {
+ Stopped_ = false;
+ try {
+ switch (parsingMode) {
+ case EYsonType::Node:
+ ParseNode<true>();
+ break;
+
+ case EYsonType::ListFragment:
+ ParseListFragment<true>(EndSymbol);
+ break;
+
+ case EYsonType::MapFragment:
+ ParseMapFragment<true>(EndSymbol);
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ if (Stopped_) {
+ return;
+ }
+
+ while (!(TBase::IsFinished() && TBase::IsEmpty())) {
+ if (TBase::template SkipSpaceAndGetChar<true>() != EndSymbol) {
+ auto character = *TBase::Current();
+ if (character == ItemSeparatorSymbol) {
+ THROW_ERROR_EXCEPTION("Stray %Qv found; maybe you should use yson_type = %Qlv",
+ character,
+ EYsonType::ListFragment)
+ << *this;
+ }
+ THROW_ERROR_EXCEPTION("Stray %Qv found",
+ character)
+ << *this;
+ } else if (!TBase::IsEmpty()) {
+ TBase::Advance(1);
+ }
+ }
+ } catch (const std::exception& ex) {
+ auto [context, contextPosition] = TBase::GetContextFromCheckpoint();
+ THROW_ERROR_EXCEPTION("Error occurred while parsing YSON")
+ << TErrorAttribute("context", EscapeC(context))
+ << TErrorAttribute("context_pos", contextPosition)
+ << ex;
+ }
+ }
+
+ void Stop()
+ {
+ Stopped_ = true;
+ }
+
+private:
+
+ /// Parse routines
+ void ParseAttributes()
+ {
+ TBase::CheckpointContext();
+ Consumer->OnBeginAttributes();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ParseMapFragment(EndAttributesSymbol);
+
+ if (Stopped_) {
+ return;
+ }
+
+ TBase::CheckpointContext();
+ TBase::SkipCharToken(EndAttributesSymbol);
+ Consumer->OnEndAttributes();
+ }
+
+ void ParseMap()
+ {
+ TBase::CheckpointContext();
+ Consumer->OnBeginMap();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ParseMapFragment(EndMapSymbol);
+
+ if (Stopped_) {
+ return;
+ }
+
+ TBase::CheckpointContext();
+ TBase::SkipCharToken(EndMapSymbol);
+ Consumer->OnEndMap();
+ }
+
+ void ParseList()
+ {
+ TBase::CheckpointContext();
+ Consumer->OnBeginList();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ParseListFragment(EndListSymbol);
+
+ if (Stopped_) {
+ return;
+ }
+
+ TBase::CheckpointContext();
+ TBase::SkipCharToken(EndListSymbol);
+ Consumer->OnEndList();
+ }
+
+ template <bool AllowFinish>
+ void ParseNode()
+ {
+ ParseNode<AllowFinish>(TBase::SkipSpaceAndGetChar());
+ }
+
+ template <bool AllowFinish>
+ void ParseNode(char ch)
+ {
+ if (NestingLevel >= NestingLevelLimit) {
+ auto nestingLevelLimit = NestingLevelLimit;
+ THROW_ERROR_EXCEPTION("Depth limit exceeded while parsing YSON")
+ << TErrorAttribute("limit", nestingLevelLimit);
+ }
+
+ ++NestingLevel;
+
+ auto guard = Finally([this] {
+ --NestingLevel;
+ });
+
+ TBase::CheckpointContext();
+ if (ch == BeginAttributesSymbol) {
+ TBase::Advance(1);
+ ParseAttributes();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ch = TBase::SkipSpaceAndGetChar();
+ TBase::CheckpointContext();
+ }
+
+ switch (ch) {
+ case BeginMapSymbol:
+ TBase::Advance(1);
+ ParseMap();
+ break;
+
+ case BeginListSymbol:
+ TBase::Advance(1);
+ ParseList();
+ break;
+
+ case '"': {
+ TBase::Advance(1);
+ TStringBuf value = TBase::ReadQuotedString();
+ Consumer->OnStringScalar(value);
+ break;
+ }
+ case StringMarker: {
+ TBase::Advance(1);
+ TStringBuf value = TBase::ReadBinaryString();
+ Consumer->OnStringScalar(value);
+ break;
+ }
+ case Int64Marker:{
+ TBase::Advance(1);
+ i64 value = TBase::ReadBinaryInt64();
+ Consumer->OnInt64Scalar(value);
+ break;
+ }
+ case Uint64Marker:{
+ TBase::Advance(1);
+ ui64 value = TBase::ReadBinaryUint64();
+ Consumer->OnUint64Scalar(value);
+ break;
+ }
+ case DoubleMarker: {
+ TBase::Advance(1);
+ double value = TBase::ReadBinaryDouble();
+ Consumer->OnDoubleScalar(value);
+ break;
+ }
+ case FalseMarker: {
+ TBase::Advance(1);
+ Consumer->OnBooleanScalar(false);
+ break;
+ }
+ case TrueMarker: {
+ TBase::Advance(1);
+ Consumer->OnBooleanScalar(true);
+ break;
+ }
+ case EntitySymbol:
+ TBase::Advance(1);
+ Consumer->OnEntity();
+ break;
+
+ default: {
+ if (isdigit(ch) || ch == '-' || ch == '+') { // case of '+' is handled in AfterPlus state
+ ReadNumeric<AllowFinish>();
+ } else if (isalpha(ch) || ch == '_') {
+ TStringBuf value = TBase::template ReadUnquotedString<AllowFinish>();
+ Consumer->OnStringScalar(value);
+ } else if (ch == '%') {
+ TBase::Advance(1);
+ ch = TBase::template GetChar<AllowFinish>();
+ if (ch == 't' || ch == 'f') {
+ Consumer->OnBooleanScalar(TBase::template ReadBoolean<AllowFinish>());
+ } else {
+ Consumer->OnDoubleScalar(TBase::template ReadNanOrInf<AllowFinish>());
+ }
+ } else if (ch == EndSymbol) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream while parsing node")
+ << *this;
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected %Qv while parsing node", ch)
+ << *this;
+ }
+ break;
+ }
+ }
+ }
+
+ void ParseKey()
+ {
+ return ParseKey(TBase::SkipSpaceAndGetChar());
+ }
+
+ void ParseKey(char ch)
+ {
+ TBase::CheckpointContext();
+ switch (ch) {
+ case '"': {
+ TBase::Advance(1);
+ TStringBuf value = TBase::ReadQuotedString();
+ Consumer->OnKeyedItem(value);
+ break;
+ }
+ case StringMarker: {
+ TBase::Advance(1);
+ TStringBuf value = TBase::ReadBinaryString();
+ Consumer->OnKeyedItem(value);
+ break;
+ }
+ default: {
+ if (isalpha(ch) || ch == '_') {
+ TStringBuf value = TBase::ReadUnquotedString();
+ Consumer->OnKeyedItem(value);
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected %Qv while parsing key",
+ ch)
+ << *this;
+ }
+ }
+ }
+ }
+
+ template <bool AllowFinish>
+ void ParseMapFragment(char endSymbol)
+ {
+ char ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ while (ch != endSymbol) {
+ ParseKey(ch);
+
+ if (Stopped_) {
+ return;
+ }
+
+ ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ TBase::CheckpointContext();
+ if (ch == KeyValueSeparatorSymbol) {
+ TBase::Advance(1);
+ } else {
+ THROW_ERROR_EXCEPTION("Expected %Qv but %Qv found",
+ KeyValueSeparatorSymbol,
+ ch)
+ << *this;
+ }
+ ParseNode<AllowFinish>();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ TBase::CheckpointContext();
+ if (ch == ItemSeparatorSymbol) {
+ TBase::Advance(1);
+ ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ } else if (ch != endSymbol) {
+ THROW_ERROR_EXCEPTION("Expected %Qv or %Qv but %Qv found",
+ ItemSeparatorSymbol,
+ endSymbol,
+ ch)
+ << *this;
+ }
+ }
+ }
+
+ void ParseMapFragment(char endSymbol)
+ {
+ ParseMapFragment<false>(endSymbol);
+ }
+
+ template <bool AllowFinish>
+ void ParseListFragment(char endSymbol)
+ {
+ char ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ while (ch != endSymbol) {
+ TBase::CheckpointContext();
+ Consumer->OnListItem();
+
+ if (Stopped_) {
+ return;
+ }
+
+ ParseNode<AllowFinish>(ch);
+
+ if (Stopped_) {
+ return;
+ }
+
+ ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ TBase::CheckpointContext();
+ if (ch == ItemSeparatorSymbol) {
+ TBase::Advance(1);
+ ch = TBase::template SkipSpaceAndGetChar<AllowFinish>();
+ } else if (ch != endSymbol) {
+ THROW_ERROR_EXCEPTION("Expected %Qv or %Qv but %Qv found",
+ ItemSeparatorSymbol,
+ endSymbol,
+ ch)
+ << *this;
+ }
+ }
+ }
+
+ void ParseListFragment(char endSymbol)
+ {
+ ParseListFragment<false>(endSymbol);
+ }
+
+ template <bool AllowFinish>
+ void ReadNumeric()
+ {
+ TStringBuf valueBuffer;
+ ENumericResult numericResult = TBase::template ReadNumeric<AllowFinish>(&valueBuffer);
+
+ if (numericResult == ENumericResult::Double) {
+ double value;
+ try {
+ value = FromString<double>(valueBuffer);
+ } catch (const std::exception& ex) {
+ // This exception is wrapped in parser.
+ THROW_ERROR CreateLiteralError(ETokenType::Double, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ Consumer->OnDoubleScalar(value);
+ } else if (numericResult == ENumericResult::Int64) {
+ i64 value;
+ try {
+ value = FromString<i64>(valueBuffer);
+ } catch (const std::exception& ex) {
+ // This exception is wrapped in parser.
+ THROW_ERROR CreateLiteralError(ETokenType::Int64, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ Consumer->OnInt64Scalar(value);
+ } else if (numericResult == ENumericResult::Uint64) {
+ ui64 value;
+ try {
+ value = FromString<ui64>(valueBuffer.SubStr(0, valueBuffer.size() - 1));
+ } catch (const std::exception& ex) {
+ // This exception is wrapped in parser.
+ THROW_ERROR CreateLiteralError(ETokenType::Uint64, valueBuffer.begin(), valueBuffer.size())
+ << *this
+ << ex;
+ }
+ Consumer->OnUint64Scalar(value);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+/*! \endinternal */
+} // namespace NDetail
+
+template <class TConsumer, class TBlockStream>
+class TParserYsonStreamImpl
+{
+public:
+ TParserYsonStreamImpl()
+ { }
+
+ void DoParse(
+ const TBlockStream& blockStream,
+ IYsonConsumer* consumer,
+ EYsonType parsingMode,
+ const TYsonParserConfig& config)
+ {
+ if (config.EnableLinePositionInfo && config.EnableContext) {
+ typedef NDetail::TParser<TConsumer, TBlockStream, 64, true> TImpl;
+ TImpl impl(blockStream, consumer, config.MemoryLimit, config.NestingLevelLimit);
+ BlockStream_ = static_cast<TBlockStream*>(&impl);
+ impl.DoParse(parsingMode);
+ } else if (config.EnableLinePositionInfo && !config.EnableContext) {
+ typedef NDetail::TParser<TConsumer, TBlockStream, 0, true> TImpl;
+ TImpl impl(blockStream, consumer, config.MemoryLimit, config.NestingLevelLimit);
+ BlockStream_ = static_cast<TBlockStream*>(&impl);
+ impl.DoParse(parsingMode);
+ } else if (!config.EnableLinePositionInfo && config.EnableContext) {
+ typedef NDetail::TParser<TConsumer, TBlockStream, 64, false> TImpl;
+ TImpl impl(blockStream, consumer, config.MemoryLimit, config.NestingLevelLimit);
+ BlockStream_ = static_cast<TBlockStream*>(&impl);
+ impl.DoParse(parsingMode);
+ } else {
+ typedef NDetail::TParser<TConsumer, TBlockStream, 0, false> TImpl;
+ TImpl impl(blockStream, consumer, config.MemoryLimit, config.NestingLevelLimit);
+ BlockStream_ = static_cast<TBlockStream*>(&impl);
+ impl.DoParse(parsingMode);
+ }
+ }
+
+ const char* GetCurrentPositionInBlock()
+ {
+ return BlockStream_->Current();
+ }
+
+private:
+ TBlockStream* BlockStream_;
+};
+
+class TStatelessYsonParserImplBase
+{
+public:
+ virtual void Parse(TStringBuf data, EYsonType type = EYsonType::Node) = 0;
+ virtual void Stop() = 0;
+
+ virtual ~TStatelessYsonParserImplBase()
+ { }
+};
+
+template <class TConsumer, size_t MaxContextSize, bool EnableLinePositionInfo>
+class TStatelessYsonParserImpl
+ : public TStatelessYsonParserImplBase
+{
+private:
+ typedef NDetail::TParser<TConsumer, TStringReader, MaxContextSize, EnableLinePositionInfo> TParser;
+ TParser Parser;
+
+public:
+ TStatelessYsonParserImpl(
+ TConsumer* consumer,
+ i64 memoryLimit,
+ int nestingLevelLimit)
+ : Parser(TStringReader(), consumer, memoryLimit, nestingLevelLimit)
+ { }
+
+ void Parse(TStringBuf data, EYsonType type = EYsonType::Node) override
+ {
+ Parser.SetBuffer(data.begin(), data.end());
+ Parser.DoParse(type);
+ }
+
+ void Stop() override
+ {
+ Parser.Stop();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/producer-inl.h b/yt/yt/core/yson/producer-inl.h
new file mode 100644
index 0000000000..7cfe5ff572
--- /dev/null
+++ b/yt/yt/core/yson/producer-inl.h
@@ -0,0 +1,31 @@
+#ifndef PRODUCER_INL_H_
+#error "Direct inclusion of this file is not allowed, include producer.h"
+// For the sake of sane code completion.
+#include "producer.h"
+#endif
+
+#include "public.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... TAdditionalArgs>
+TExtendedYsonProducer<TAdditionalArgs...>::TExtendedYsonProducer(
+ TExtendedYsonProducer::TUnderlyingCallback callback,
+ EYsonType type)
+ : Type_(type)
+ , Callback_(std::move(callback))
+{
+ YT_VERIFY(Callback_);
+}
+
+template <class... TAdditionalArgs>
+void TExtendedYsonProducer<TAdditionalArgs...>::Run(IYsonConsumer* consumer, TAdditionalArgs... args) const
+{
+ Callback_(consumer, std::forward<TAdditionalArgs>(args)...);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson \ No newline at end of file
diff --git a/yt/yt/core/yson/producer.cpp b/yt/yt/core/yson/producer.cpp
new file mode 100644
index 0000000000..aeff4b7266
--- /dev/null
+++ b/yt/yt/core/yson/producer.cpp
@@ -0,0 +1,33 @@
+#include "producer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonProducer::TYsonProducer(TYsonCallback callback, EYsonType type)
+ : Type_(type)
+ , Callback_(std::move(callback))
+{
+ YT_ASSERT(Callback_);
+}
+
+void TYsonProducer::Run(IYsonConsumer* consumer) const
+{
+ Callback_(consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonProducer& value, IYsonConsumer* consumer)
+{
+ value.Run(consumer);
+}
+
+void Serialize(const TYsonCallback& value, IYsonConsumer* consumer)
+{
+ Serialize(TYsonProducer(value), consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/producer.h b/yt/yt/core/yson/producer.h
new file mode 100644
index 0000000000..7310d1540b
--- /dev/null
+++ b/yt/yt/core/yson/producer.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "public.h"
+#include "string.h"
+
+#include <yt/yt/core/actions/callback.h>
+
+#include <yt/yt/core/misc/property.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A callback capable of generating YSON by calling appropriate
+//! methods for its IYsonConsumer argument.
+using TYsonCallback = TCallback<void(IYsonConsumer*)>;
+
+//! A callback capable of generating YSON by calling appropriate
+//! methods for its IYsonConsumer and some additional arguments.
+template <class... TAdditionalArgs>
+using TExtendedYsonCallback = TCallback<void(IYsonConsumer*, TAdditionalArgs...)>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonProducer
+{
+public:
+ DEFINE_BYVAL_RO_PROPERTY(NYson::EYsonType, Type);
+
+public:
+ TYsonProducer() = default;
+ TYsonProducer(
+ TYsonCallback callback,
+ EYsonType type = NYson::EYsonType::Node);
+
+ void Run(IYsonConsumer* consumer) const;
+
+private:
+ TYsonCallback Callback_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class... TAdditionalArgs>
+class TExtendedYsonProducer
+{
+ using TUnderlyingCallback = TExtendedYsonCallback<TAdditionalArgs...>;
+public:
+ DEFINE_BYVAL_RO_PROPERTY(NYson::EYsonType, Type);
+
+public:
+ TExtendedYsonProducer() = default;
+ TExtendedYsonProducer(
+ TUnderlyingCallback callback,
+ EYsonType type = NYson::EYsonType::Node);
+
+ void Run(IYsonConsumer* consumer, TAdditionalArgs... args) const;
+
+private:
+ TUnderlyingCallback Callback_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonProducer& value, NYson::IYsonConsumer* consumer);
+void Serialize(const TYsonCallback& value, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define PRODUCER_INL_H_
+#include "producer-inl.h"
+#undef PRODUCER_INL_H_
diff --git a/yt/yt/core/yson/protobuf_interop-inl.h b/yt/yt/core/yson/protobuf_interop-inl.h
new file mode 100644
index 0000000000..50ec342fe4
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop-inl.h
@@ -0,0 +1,57 @@
+#ifndef PROTOBUF_INTEROP_INL_H_
+#error "Direct inclusion of this file is not allowed, include protobuf_interop.h"
+// For the sake of sane code completion.
+#include "protobuf_interop.h"
+#endif
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+const TProtobufMessageType* ReflectProtobufMessageType()
+{
+ static const auto* type = ReflectProtobufMessageType(T::default_instance().GetDescriptor());
+ return type;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<int> FindProtobufEnumValueByLiteralUntyped(
+ const TProtobufEnumType* type,
+ TStringBuf literal);
+TStringBuf FindProtobufEnumLiteralByValueUntyped(
+ const TProtobufEnumType* type,
+ int value);
+int ConvertToProtobufEnumValueUntyped(
+ const TProtobufEnumType* type,
+ const NYTree::INodePtr& node);
+
+template <class T>
+std::optional<T> FindProtobufEnumValueByLiteral(
+ const TProtobufEnumType* type,
+ TStringBuf literal)
+{
+ auto untyped = FindProtobufEnumValueByLiteralUntyped(type, literal);
+ return untyped ? static_cast<T>(*untyped) : std::optional<T>();
+}
+
+template <class T>
+TStringBuf FindProtobufEnumLiteralByValue(
+ const TProtobufEnumType* type,
+ T value)
+{
+ return FindProtobufEnumLiteralByValueUntyped(type, static_cast<int>(value));
+}
+
+template <class T>
+T ConvertToProtobufEnumValue(
+ const TProtobufEnumType* type,
+ const NYTree::INodePtr& node)
+{
+ return static_cast<T>(ConvertToProtobufEnumValueUntyped(type, node));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/protobuf_interop.cpp b/yt/yt/core/yson/protobuf_interop.cpp
new file mode 100644
index 0000000000..343cbf8090
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop.cpp
@@ -0,0 +1,2902 @@
+#include "protobuf_interop.h"
+
+#include "parser.h"
+
+#include <yt/yt_proto/yt/core/yson/proto/protobuf_interop.pb.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/forwarding_consumer.h>
+#include <yt/yt/core/yson/null_consumer.h>
+#include <yt/yt/core/yson/protobuf_interop_unknown_fields.h>
+
+#include <yt/yt/core/ypath/helpers.h>
+#include <yt/yt/core/ypath/stack.h>
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+
+#include <yt/yt/core/concurrency/thread_affinity.h>
+
+#include <yt/yt_proto/yt/core/ytree/proto/attributes.pb.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <library/cpp/yt/coding/varint.h>
+#include <library/cpp/yt/coding/zig_zag.h>
+
+#include <library/cpp/yt/threading/fork_aware_spin_lock.h>
+
+#include <yt/yt/library/syncmap/map.h>
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/dynamic_message.h>
+#include <google/protobuf/wire_format.h>
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace NYT::NYson {
+
+using namespace NYson;
+using namespace NYTree;
+using namespace NYPath;
+using namespace NThreading;
+using namespace NConcurrency;
+using namespace google::protobuf;
+using namespace google::protobuf::io;
+using namespace google::protobuf::internal;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufField;
+class TProtobufEnumType;
+
+static constexpr size_t TypicalFieldCount = 16;
+using TFieldNumberList = TCompactVector<int, TypicalFieldCount>;
+
+static constexpr int AttributeDictionaryAttributeFieldNumber = 1;
+static constexpr int ProtobufMapKeyFieldNumber = 1;
+static constexpr int ProtobufMapValueFieldNumber = 2;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+bool IsSignedIntegralType(FieldDescriptor::Type type)
+{
+ switch (type) {
+ case FieldDescriptor::TYPE_INT32:
+ case FieldDescriptor::TYPE_INT64:
+ case FieldDescriptor::TYPE_SFIXED32:
+ case FieldDescriptor::TYPE_SFIXED64:
+ case FieldDescriptor::TYPE_SINT32:
+ case FieldDescriptor::TYPE_SINT64:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsUnsignedIntegralType(FieldDescriptor::Type type)
+{
+ switch (type) {
+ case FieldDescriptor::TYPE_UINT32:
+ case FieldDescriptor::TYPE_UINT64:
+ case FieldDescriptor::TYPE_FIXED64:
+ case FieldDescriptor::TYPE_FIXED32:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsStringType(FieldDescriptor::Type type)
+{
+ switch (type) {
+ case FieldDescriptor::TYPE_BYTES:
+ case FieldDescriptor::TYPE_STRING:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsMapKeyType(FieldDescriptor::Type type)
+{
+ return
+ IsStringType(type) ||
+ IsSignedIntegralType(type) ||
+ IsUnsignedIntegralType(type);
+}
+
+TString ToUnderscoreCase(const TString& protobufName)
+{
+ TStringBuilder builder;
+ for (auto ch : protobufName) {
+ if (isupper(ch)) {
+ if (builder.GetLength() > 0 && builder.GetBuffer()[builder.GetLength() - 1] != '_') {
+ builder.AppendChar('_');
+ }
+ builder.AppendChar(tolower(ch));
+ } else {
+ builder.AppendChar(ch);
+ }
+ }
+ return builder.Flush();
+}
+
+TString DeriveYsonName(const TString& protobufName, const google::protobuf::FileDescriptor* fileDescriptor)
+{
+ if (fileDescriptor->options().GetExtension(NYT::NYson::NProto::derive_underscore_case_names)) {
+ return ToUnderscoreCase(protobufName);
+ } else {
+ return protobufName;
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufTypeRegistry
+{
+public:
+ //! This method is called while reflecting types.
+ TStringBuf GetYsonName(const FieldDescriptor* descriptor)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ return GetYsonNameFromDescriptor(
+ descriptor,
+ descriptor->options().GetExtension(NYT::NYson::NProto::field_name));
+ }
+
+ //! This method is called while reflecting types.
+ std::vector<TStringBuf> GetYsonNameAliases(const FieldDescriptor* descriptor)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ std::vector<TStringBuf> aliases;
+ auto extensions = descriptor->options().GetRepeatedExtension(NYT::NYson::NProto::field_name_alias);
+ for (const auto& alias : extensions) {
+ aliases.push_back(InternString(alias));
+ }
+ return aliases;
+ }
+
+ //! This method is called while reflecting types.
+ TStringBuf GetYsonLiteral(const EnumValueDescriptor* descriptor)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ return GetYsonNameFromDescriptor(
+ descriptor,
+ descriptor->options().GetExtension(NYT::NYson::NProto::enum_value_name));
+ }
+
+ const TProtobufMessageType* ReflectMessageType(const Descriptor* descriptor)
+ {
+ if (auto* result = MessageTypeSyncMap_.Find(descriptor)) {
+ return *result;
+ }
+
+ auto guard = Guard(Lock_);
+ Initialize();
+ return ReflectMessageTypeInternal(descriptor);
+ }
+
+ const TProtobufEnumType* ReflectEnumType(const EnumDescriptor* descriptor)
+ {
+ if (auto* result = EnumTypeSyncMap_.Find(descriptor)) {
+ return *result;
+ }
+
+ auto guard = Guard(Lock_);
+ Initialize();
+ return ReflectEnumTypeInternal(descriptor);
+ }
+
+ static TProtobufTypeRegistry* Get()
+ {
+ return Singleton<TProtobufTypeRegistry>();
+ }
+
+ using TRegisterAction = std::function<void()>;
+
+ //! This method is called during static initialization and is not expected to be called during runtime.
+ //! That is why there is no synchronization within.
+ /*!
+ * Be cautious trying to acquire fork-aware spin lock during static initialization:
+ * fork-awareness is provided by the static fork-lock, acquiring may cause dependency within static initialization.
+ */
+ void AddRegisterAction(TRegisterAction action)
+ {
+ RegisterActions_.push_back(std::move(action));
+ }
+
+ //! This method is called during static initialization and is not expected to be called during runtime.
+ void RegisterMessageTypeConverter(
+ const Descriptor* descriptor,
+ const TProtobufMessageConverter& converter)
+ {
+ EmplaceOrCrash(MessageTypeConverterMap_, descriptor, converter);
+ }
+
+ //! This method is called during static initialization and is not expected to be called during runtime.
+ void RegisterMessageBytesFieldConverter(
+ const Descriptor* descriptor,
+ int fieldNumber,
+ const TProtobufMessageBytesFieldConverter& converter)
+ {
+ EmplaceOrCrash(MessageFieldConverterMap_, std::make_pair(descriptor, fieldNumber), converter);
+ }
+
+ //! This method is called while reflecting types.
+ std::optional<TProtobufMessageConverter> FindMessageTypeConverter(
+ const Descriptor* descriptor) const
+ {
+ // No need to call Initialize: it has been already called within Reflect*Type higher up the stack.
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ auto it = MessageTypeConverterMap_.find(descriptor);
+ if (it == MessageTypeConverterMap_.end()) {
+ return std::nullopt;
+ } else {
+ return it->second;
+ }
+ }
+
+ //! This method is called while reflecting types.
+ std::optional<TProtobufMessageBytesFieldConverter> FindMessageBytesFieldConverter(
+ const Descriptor* descriptor,
+ int fieldIndex) const
+ {
+ // No need to call Initialize: it has been already called within Reflect*Type higher up the stack.
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ auto fieldNumber = descriptor->field(fieldIndex)->number();
+ auto it = MessageFieldConverterMap_.find(std::make_pair(descriptor, fieldNumber));
+ if (it == MessageFieldConverterMap_.end()) {
+ return std::nullopt;
+ } else {
+ return it->second;
+ }
+ }
+
+ // These are called while reflecting the types recursively;
+ // the caller must be holding Lock_.
+ const TProtobufMessageType* ReflectMessageTypeInternal(const Descriptor* descriptor);
+ const TProtobufEnumType* ReflectEnumTypeInternal(const EnumDescriptor* descriptor);
+
+private:
+ Y_DECLARE_SINGLETON_FRIEND()
+ TProtobufTypeRegistry() = default;
+
+ void Initialize() const
+ {
+ if (!RegisterActions_.empty()) {
+ for (const auto& action : RegisterActions_) {
+ action();
+ }
+ RegisterActions_.clear();
+ }
+ }
+
+ template <class TDescriptor>
+ TStringBuf GetYsonNameFromDescriptor(const TDescriptor* descriptor, const TString& annotatedName)
+ {
+ auto ysonName = annotatedName ? annotatedName : DeriveYsonName(descriptor->name(), descriptor->file());
+ return InternString(ysonName);
+ }
+
+ TStringBuf InternString(const TString& str)
+ {
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ return *InternedStrings_.emplace(str).first;
+ }
+
+private:
+ template <class TKey, class TValue>
+ using TForkAwareSyncMap = TSyncMap<
+ TKey,
+ TValue,
+ THash<TKey>,
+ TEqualTo<TKey>,
+ TForkAwareSpinLock
+ >;
+
+ YT_DECLARE_SPIN_LOCK(TForkAwareSpinLock, Lock_);
+ THashMap<const Descriptor*, std::unique_ptr<TProtobufMessageType>> MessageTypeMap_;
+ TForkAwareSyncMap<const Descriptor*, const TProtobufMessageType*> MessageTypeSyncMap_;
+ THashMap<const EnumDescriptor*,std::unique_ptr<TProtobufEnumType>> EnumTypeMap_;
+ TForkAwareSyncMap<const EnumDescriptor*, const TProtobufEnumType*> EnumTypeSyncMap_;
+
+ THashMap<const Descriptor*, TProtobufMessageConverter> MessageTypeConverterMap_;
+ THashMap<std::pair<const Descriptor*, int>, TProtobufMessageBytesFieldConverter> MessageFieldConverterMap_;
+
+ THashSet<TString> InternedStrings_;
+
+ mutable std::vector<TRegisterAction> RegisterActions_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufField
+{
+public:
+ TProtobufField(TProtobufTypeRegistry* registry, const FieldDescriptor* descriptor)
+ : Underlying_(descriptor)
+ , YsonName_(registry->GetYsonName(descriptor))
+ , YsonNameAliases_(registry->GetYsonNameAliases(descriptor))
+ , MessageType_(descriptor->type() == FieldDescriptor::TYPE_MESSAGE ? registry->ReflectMessageTypeInternal(
+ descriptor->message_type()) : nullptr)
+ , EnumType_(descriptor->type() == FieldDescriptor::TYPE_ENUM ? registry->ReflectEnumTypeInternal(
+ descriptor->enum_type()) : nullptr)
+ , YsonString_(descriptor->options().GetExtension(NYT::NYson::NProto::yson_string))
+ , YsonMap_(descriptor->options().GetExtension(NYT::NYson::NProto::yson_map))
+ , Required_(descriptor->options().GetExtension(NYT::NYson::NProto::required))
+ , Converter_(registry->FindMessageBytesFieldConverter(descriptor->containing_type(), descriptor->index()))
+ {
+ if (YsonMap_ && !descriptor->is_map()) {
+ THROW_ERROR_EXCEPTION("Field %v is not a map and cannot be annotated with \"yson_map\" option",
+ GetFullName());
+ }
+
+ if (YsonMap_) {
+ const auto* keyField = descriptor->message_type()->FindFieldByNumber(ProtobufMapKeyFieldNumber);
+ if (!IsMapKeyType(keyField->type())) {
+ THROW_ERROR_EXCEPTION("Map field %v has invalid key type",
+ GetFullName());
+ }
+ }
+
+ if (Converter_ && GetType() != FieldDescriptor::Type::TYPE_BYTES) {
+ THROW_ERROR_EXCEPTION("Field %v with custom converter has invalid type, only bytes fields are allowed",
+ GetFullName());
+ }
+ }
+
+ ui32 GetTag() const
+ {
+ return google::protobuf::internal::WireFormat::MakeTag(Underlying_);
+ }
+
+ const TString& GetFullName() const
+ {
+ return Underlying_->full_name();
+ }
+
+ TStringBuf GetYsonName() const
+ {
+ return YsonName_;
+ }
+
+ const std::vector<TStringBuf>& GetYsonNameAliases() const
+ {
+ return YsonNameAliases_;
+ }
+
+ int GetNumber() const
+ {
+ return Underlying_->number();
+ }
+
+ FieldDescriptor::Type GetType() const
+ {
+ return Underlying_->type();
+ }
+
+ const char* GetTypeName() const
+ {
+ return Underlying_->type_name();
+ }
+
+ bool IsRepeated() const
+ {
+ return Underlying_->is_repeated() && !IsYsonMap();
+ }
+
+ bool IsPacked() const
+ {
+ return Underlying_->is_packed() && !IsYsonMap();
+ }
+
+ bool IsRequired() const
+ {
+ return Underlying_->is_required() || Required_;
+ }
+
+ bool IsOptional() const
+ {
+ return Underlying_->is_optional() && !Required_;
+ }
+
+ bool IsMessage() const
+ {
+ return MessageType_ != nullptr;
+ }
+
+ bool IsYsonString() const
+ {
+ return YsonString_;
+ }
+
+ bool IsYsonMap() const
+ {
+ return YsonMap_;
+ }
+
+ const TProtobufField* GetYsonMapKeyField() const;
+ const TProtobufField* GetYsonMapValueField() const;
+
+ const TProtobufMessageType* GetMessageType() const
+ {
+ return MessageType_;
+ }
+
+ const TProtobufEnumType* GetEnumType() const
+ {
+ return EnumType_;
+ }
+
+ TProtobufElement GetElement(bool insideRepeated) const;
+
+ const std::optional<TProtobufMessageBytesFieldConverter>& GetBytesFieldConverter() const
+ {
+ return Converter_;
+ }
+
+private:
+ const FieldDescriptor* const Underlying_;
+ const TStringBuf YsonName_;
+ const std::vector<TStringBuf> YsonNameAliases_;
+ const TProtobufMessageType* MessageType_;
+ const TProtobufEnumType* EnumType_;
+ const bool YsonString_;
+ const bool YsonMap_;
+ const bool Required_;
+ const std::optional<TProtobufMessageBytesFieldConverter> Converter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufMessageType
+{
+public:
+ TProtobufMessageType(TProtobufTypeRegistry* registry, const Descriptor* descriptor)
+ : Registry_(registry)
+ , Underlying_(descriptor)
+ , AttributeDictionary_(descriptor->options().GetExtension(NYT::NYson::NProto::attribute_dictionary))
+ , Converter_(registry->FindMessageTypeConverter(descriptor))
+ { }
+
+ void Build()
+ {
+ for (int index = 0; index < Underlying_->field_count(); ++index) {
+ const auto* fieldDescriptor = Underlying_->field(index);
+ RegisterField(fieldDescriptor);
+ }
+
+ auto descriptorPool = Underlying_->file()->pool();
+ std::vector<const FieldDescriptor*> extensionFieldDescriptors;
+ descriptorPool->FindAllExtensions(Underlying_, &extensionFieldDescriptors);
+ for (const auto* extensionFieldDescriptor : extensionFieldDescriptors) {
+ RegisterField(extensionFieldDescriptor);
+ }
+
+ for (int index = 0; index < Underlying_->reserved_name_count(); ++index) {
+ ReservedFieldNames_.insert(Underlying_->reserved_name(index));
+ }
+ }
+
+ const Descriptor* GetUnderlying() const
+ {
+ return Underlying_;
+ }
+
+ bool IsAttributeDictionary() const
+ {
+ return AttributeDictionary_;
+ }
+
+ const TString& GetFullName() const
+ {
+ return Underlying_->full_name();
+ }
+
+ const std::vector<int>& GetRequiredFieldNumbers() const
+ {
+ return RequiredFieldNumbers_;
+ }
+
+ const std::optional<TProtobufMessageConverter>& GetConverter() const
+ {
+ return Converter_;
+ }
+
+ bool IsReservedFieldName(TStringBuf name) const
+ {
+ return ReservedFieldNames_.contains(name);
+ }
+
+ bool IsReservedFieldNumber(int number) const
+ {
+ for (int index = 0; index < Underlying_->reserved_range_count(); ++index) {
+ if (number >= Underlying_->reserved_range(index)->start &&
+ number <= Underlying_->reserved_range(index)->end)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ const TProtobufField* FindFieldByName(TStringBuf name) const
+ {
+ auto it = NameToField_.find(name);
+ return it == NameToField_.end() ? nullptr : it->second;
+ }
+
+ const TProtobufField* FindFieldByNumber(int number) const
+ {
+ auto it = NumberToField_.find(number);
+ return it == NumberToField_.end() ? nullptr : it->second;
+ }
+
+ const TProtobufField* GetFieldByNumber(int number) const
+ {
+ const auto* field = FindFieldByNumber(number);
+ YT_VERIFY(field);
+ return field;
+ }
+
+ TProtobufElement GetElement() const
+ {
+ if (IsAttributeDictionary()) {
+ return std::make_unique<TProtobufAttributeDictionaryElement>(TProtobufAttributeDictionaryElement{
+ this
+ });
+ } else {
+ return std::make_unique<TProtobufMessageElement>(TProtobufMessageElement{
+ this
+ });
+ }
+ }
+
+private:
+ TProtobufTypeRegistry* const Registry_;
+ const Descriptor* const Underlying_;
+ const bool AttributeDictionary_;
+
+ std::vector<std::unique_ptr<TProtobufField>> Fields_;
+ std::vector<int> RequiredFieldNumbers_;
+ THashMap<TStringBuf, const TProtobufField*> NameToField_;
+ THashMap<int, const TProtobufField*> NumberToField_;
+ THashSet<TString> ReservedFieldNames_;
+ std::optional<TProtobufMessageConverter> Converter_;
+
+ void RegisterField(const FieldDescriptor* fieldDescriptor)
+ {
+ auto fieldHolder = std::make_unique<TProtobufField>(Registry_, fieldDescriptor);
+ auto* field = fieldHolder.get();
+ if (field->IsRequired()) {
+ RequiredFieldNumbers_.push_back(field->GetNumber());
+ }
+ YT_VERIFY(NameToField_.emplace(field->GetYsonName(), field).second);
+ for (auto name : field->GetYsonNameAliases()) {
+ YT_VERIFY(NameToField_.emplace(name, field).second);
+ }
+ YT_VERIFY(NumberToField_.emplace(field->GetNumber(), field).second);
+ Fields_.push_back(std::move(fieldHolder));
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TProtobufField* TProtobufField::GetYsonMapKeyField() const
+{
+ return MessageType_->GetFieldByNumber(ProtobufMapKeyFieldNumber);
+}
+
+const TProtobufField* TProtobufField::GetYsonMapValueField() const
+{
+ return MessageType_->GetFieldByNumber(ProtobufMapValueFieldNumber);
+}
+
+TProtobufElement TProtobufField::GetElement(bool insideRepeated) const
+{
+ if (IsRepeated() && !insideRepeated) {
+ return std::make_unique<TProtobufRepeatedElement>(TProtobufRepeatedElement{
+ GetElement(true)
+ });
+ } else if (IsYsonMap()) {
+ return std::make_unique<TProtobufMapElement>(TProtobufMapElement{
+ GetYsonMapValueField()->GetElement(false)
+ });
+ } else if (IsYsonString()) {
+ return std::make_unique<TProtobufAnyElement>();
+ } else if (IsMessage()) {
+ return std::make_unique<TProtobufMessageElement>(TProtobufMessageElement{
+ MessageType_
+ });
+ } else {
+ return std::make_unique<TProtobufScalarElement>();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufEnumType
+{
+public:
+ TProtobufEnumType(TProtobufTypeRegistry* registry, const EnumDescriptor* descriptor)
+ : Registry_(registry)
+ , Underlying_(descriptor)
+ { }
+
+ void Build()
+ {
+ for (int index = 0; index < Underlying_->value_count(); ++index) {
+ const auto* valueDescriptor = Underlying_->value(index);
+ auto literal = Registry_->GetYsonLiteral(valueDescriptor);
+ YT_VERIFY(LiteralToValue_.emplace(literal, valueDescriptor->number()).second);
+ // Allow aliases, i.e. different literals for the same tag, which can be helpful during migration.
+ // The first literal is selected as canonical for each tag.
+ ValueToLiteral_.try_emplace(valueDescriptor->number(), literal);
+ }
+ }
+
+ const EnumDescriptor* GetUnderlying() const
+ {
+ return Underlying_;
+ }
+
+ const TString& GetFullName() const
+ {
+ return Underlying_->full_name();
+ }
+
+ std::optional<int> FindValueByLiteral(TStringBuf literal) const
+ {
+ auto it = LiteralToValue_.find(literal);
+ return it == LiteralToValue_.end() ? std::nullopt : std::make_optional(it->second);
+ }
+
+ TStringBuf FindLiteralByValue(int value) const
+ {
+ auto it = ValueToLiteral_.find(value);
+ return it == ValueToLiteral_.end() ? TStringBuf() : it->second;
+ }
+
+private:
+ TProtobufTypeRegistry* const Registry_;
+ const EnumDescriptor* const Underlying_;
+
+ THashMap<TStringBuf, int> LiteralToValue_;
+ THashMap<int, TStringBuf> ValueToLiteral_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TProtobufMessageType* TProtobufTypeRegistry::ReflectMessageTypeInternal(const Descriptor* descriptor)
+{
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ TProtobufMessageType* type;
+ auto it = MessageTypeMap_.find(descriptor);
+ if (it == MessageTypeMap_.end()) {
+ auto typeHolder = std::make_unique<TProtobufMessageType>(this, descriptor);
+ type = typeHolder.get();
+ it = MessageTypeMap_.emplace(descriptor, std::move(typeHolder)).first;
+ type->Build();
+ } else {
+ type = it->second.get();
+ }
+
+ YT_VERIFY(*MessageTypeSyncMap_.FindOrInsert(descriptor, [&] { return type; }).first == type);
+
+ return type;
+}
+
+const TProtobufEnumType* TProtobufTypeRegistry::ReflectEnumTypeInternal(const EnumDescriptor* descriptor)
+{
+ VERIFY_SPINLOCK_AFFINITY(Lock_);
+
+ TProtobufEnumType* type;
+ auto it = EnumTypeMap_.find(descriptor);
+ if (it == EnumTypeMap_.end()) {
+ auto typeHolder = std::make_unique<TProtobufEnumType>(this, descriptor);
+ type = typeHolder.get();
+ it = EnumTypeMap_.emplace(descriptor, std::move(typeHolder)).first;
+ type->Build();
+ } else {
+ type = it->second.get();
+ }
+
+ YT_VERIFY(*EnumTypeSyncMap_.FindOrInsert(descriptor, [&] { return type; }).first == type);
+
+ return type;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TProtobufMessageType* ReflectProtobufMessageType(const Descriptor* descriptor)
+{
+ return TProtobufTypeRegistry::Get()->ReflectMessageType(descriptor);
+}
+
+const TProtobufEnumType* ReflectProtobufEnumType(const EnumDescriptor* descriptor)
+{
+ return TProtobufTypeRegistry::Get()->ReflectEnumType(descriptor);
+}
+
+const ::google::protobuf::Descriptor* UnreflectProtobufMessageType(const TProtobufMessageType* type)
+{
+ return type->GetUnderlying();
+}
+
+const ::google::protobuf::EnumDescriptor* UnreflectProtobufEnumType(const TProtobufEnumType* type)
+{
+ return type->GetUnderlying();
+}
+
+std::optional<int> FindProtobufEnumValueByLiteralUntyped(
+ const TProtobufEnumType* type,
+ TStringBuf literal)
+{
+ return type->FindValueByLiteral(literal);
+}
+
+TStringBuf FindProtobufEnumLiteralByValueUntyped(
+ const TProtobufEnumType* type,
+ int value)
+{
+ return type->FindLiteralByValue(value);
+}
+
+int ConvertToProtobufEnumValueUntyped(
+ const TProtobufEnumType* type,
+ const NYTree::INodePtr& node)
+{
+ switch (node->GetType()) {
+ case NYTree::ENodeType::Int64:
+ case NYTree::ENodeType::Uint64: {
+ int value = NYTree::ConvertTo<int>(node);
+ THROW_ERROR_EXCEPTION_UNLESS(type->GetUnderlying()->FindValueByNumber(value),
+ "Unknown value %v of enum %Qv",
+ value,
+ type->GetUnderlying()->name());
+ return value;
+ }
+ case NYTree::ENodeType::String: {
+ const TString& literal = node->AsString()->GetValue();
+ auto value = type->FindValueByLiteral(literal);
+ THROW_ERROR_EXCEPTION_UNLESS(value,
+ "Unknown value %Qv of enum %Qv",
+ literal,
+ type->GetUnderlying()->name());
+ return *value;
+ }
+ default:
+ THROW_ERROR_EXCEPTION("Expected integral or string, got %v",
+ node->GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufTranscoderBase
+{
+protected:
+ TYPathStack YPathStack_;
+
+
+ void SortFields(TFieldNumberList* numbers)
+ {
+ std::sort(numbers->begin(), numbers->end());
+ }
+
+ void ValidateRequiredFieldsPresent(const TProtobufMessageType* type, const TFieldNumberList& numbers)
+ {
+ if (numbers.size() == type->GetRequiredFieldNumbers().size()) {
+ return;
+ }
+
+ for (auto number : type->GetRequiredFieldNumbers()) {
+ if (!std::binary_search(numbers.begin(), numbers.end(), number)) {
+ const auto* field = type->FindFieldByNumber(number);
+ YT_VERIFY(field);
+ YPathStack_.Push(TString{field->GetYsonName()});
+ THROW_ERROR_EXCEPTION("Missing required field %v",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_type", type->GetFullName())
+ << TErrorAttribute("protobuf_field", field->GetFullName());
+ }
+ }
+
+ YT_ABORT();
+ }
+
+ void ValidateNoFieldDuplicates(const TProtobufMessageType* type, const TFieldNumberList& numbers)
+ {
+ for (auto index = 0; index + 1 < std::ssize(numbers); ++index) {
+ if (numbers[index] == numbers[index + 1]) {
+ const auto* field = type->GetFieldByNumber(numbers[index]);
+ YPathStack_.Push(TString{field->GetYsonName()});
+ THROW_ERROR_EXCEPTION("Duplicate field %v",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_type", type->GetFullName());
+ }
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufWriter
+ : public TProtobufTranscoderBase
+ , public TForwardingYsonConsumer
+{
+public:
+ TProtobufWriter(
+ ZeroCopyOutputStream* outputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufWriterOptions& options)
+ : OutputStream_(outputStream)
+ , RootType_(rootType)
+ , Options_(options)
+ , BodyOutputStream_(&BodyString_)
+ , BodyCodedStream_(&BodyOutputStream_)
+ , AttributeValueStream_(AttributeValue_)
+ , AttributeValueWriter_(&AttributeValueStream_)
+ , YsonStringStream_(YsonString_)
+ , YsonStringWriter_(&YsonStringStream_)
+ , UnknownYsonFieldValueStringStream_(UnknownYsonFieldValueString_)
+ , UnknownYsonFieldValueStringWriter_(&UnknownYsonFieldValueStringStream_)
+ , ForwardingUnknownYsonFieldValueWriter_(UnknownYsonFieldValueStringWriter_, Options_.UnknownYsonFieldModeResolver)
+ , TreeBuilder_(CreateBuilderFromFactory(GetEphemeralNodeFactory()))
+ { }
+
+private:
+ ZeroCopyOutputStream* const OutputStream_;
+ const TProtobufMessageType* const RootType_;
+ const TProtobufWriterOptions Options_;
+
+ TString BodyString_;
+ google::protobuf::io::StringOutputStream BodyOutputStream_;
+ google::protobuf::io::CodedOutputStream BodyCodedStream_;
+
+ struct TTypeEntry
+ {
+ explicit TTypeEntry(const TProtobufMessageType* type)
+ : Type(type)
+ { }
+
+ const TProtobufMessageType* Type;
+ TFieldNumberList RequiredFieldNumbers;
+ TFieldNumberList NonRequiredFieldNumbers;
+ int CurrentMapIndex = 0;
+ };
+ std::vector<TTypeEntry> TypeStack_;
+
+ std::vector<int> NestedIndexStack_;
+
+ struct TFieldEntry
+ {
+ explicit TFieldEntry(const TProtobufField* field)
+ : Field(field)
+ { }
+
+ const TProtobufField* Field;
+ int CurrentListIndex = 0;
+ bool ParsingList = false;
+ bool ParsingYsonMapFromList = false;
+ };
+ std::vector<TFieldEntry> FieldStack_;
+
+ struct TNestedMessageEntry
+ {
+ TNestedMessageEntry(int lo, int hi)
+ : Lo(lo)
+ , Hi(hi)
+ { }
+
+ int Lo;
+ int Hi;
+ int ByteSize = -1;
+ };
+ std::vector<TNestedMessageEntry> NestedMessages_;
+
+ TString AttributeKey_;
+ TString AttributeValue_;
+ TStringOutput AttributeValueStream_;
+ TBufferedBinaryYsonWriter AttributeValueWriter_;
+
+ TString YsonString_;
+ TStringOutput YsonStringStream_;
+ TBufferedBinaryYsonWriter YsonStringWriter_;
+
+ TString SerializedMessage_;
+ TString BytesString_;
+
+ TString UnknownYsonFieldKey_;
+ TString UnknownYsonFieldValueString_;
+ TStringOutput UnknownYsonFieldValueStringStream_;
+ TBufferedBinaryYsonWriter UnknownYsonFieldValueStringWriter_;
+ TForwardingUnknownYsonFieldValueWriter ForwardingUnknownYsonFieldValueWriter_;
+
+ std::unique_ptr<ITreeBuilder> TreeBuilder_;
+
+ void OnMyStringScalar(TStringBuf value) override
+ {
+ WriteScalar([&] {
+ const auto* field = FieldStack_.back().Field;
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_BYTES:
+ BodyCodedStream_.WriteVarint64(value.length());
+ BodyCodedStream_.WriteRaw(value.begin(), static_cast<int>(value.length()));
+ break;
+
+ case FieldDescriptor::TYPE_ENUM: {
+ const auto* enumType = field->GetEnumType();
+ auto optionalValue = enumType->FindValueByLiteral(value);
+ if (!optionalValue) {
+ THROW_ERROR_EXCEPTION("Field %v cannot have value %Qv",
+ YPathStack_.GetPath(),
+ value)
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_type", enumType->GetFullName());
+ }
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize32SignExtended(*optionalValue));
+ }
+ BodyCodedStream_.WriteVarint32SignExtended(*optionalValue);
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Field %v cannot be parsed from \"string\" values",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ });
+ }
+
+ void OnMyInt64Scalar(i64 value) override
+ {
+ OnIntegerScalar(value);
+ }
+
+ void OnMyUint64Scalar(ui64 value) override
+ {
+ OnIntegerScalar(value);
+ }
+
+ void OnMyDoubleScalar(double value) override
+ {
+ WriteScalar([&] {
+ const auto* field = FieldStack_.back().Field;
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_DOUBLE: {
+ auto encodedValue = WireFormatLite::EncodeDouble(value);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(encodedValue));
+ }
+ BodyCodedStream_.WriteRaw(&encodedValue, sizeof(encodedValue));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FLOAT: {
+ auto encodedValue = WireFormatLite::EncodeFloat(value);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(encodedValue));
+ }
+ BodyCodedStream_.WriteRaw(&encodedValue, sizeof(encodedValue));
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Field %v cannot be parsed from \"double\" values",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ });
+ }
+
+ void OnMyBooleanScalar(bool value) override
+ {
+ WriteScalar([&] {
+ const auto* field = FieldStack_.back().Field;
+ auto type = field->GetType();
+ if (type != FieldDescriptor::TYPE_BOOL) {
+ THROW_ERROR_EXCEPTION("Field %v cannot be parsed from \"boolean\" values",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(1);
+ }
+ BodyCodedStream_.WriteVarint32(value ? 1 : 0);
+ });
+ }
+
+ void OnMyEntity() override
+ {
+ if (FieldStack_.empty()) {
+ // This is the root.
+ return;
+ }
+ FieldStack_.pop_back();
+ YPathStack_.Pop();
+ }
+
+ void OnMyBeginList() override
+ {
+ ValidateNotRoot();
+
+ const auto* field = FieldStack_.back().Field;
+ if (field->IsYsonMap()) {
+ // We do allow parsing map from lists to ease migration; cf. YT-11055.
+ FieldStack_.back().ParsingYsonMapFromList = true;
+ } else {
+ ValidateRepeated();
+ }
+ }
+
+ void OnMyListItem() override
+ {
+ YT_ASSERT(!TypeStack_.empty());
+ int index = FieldStack_.back().CurrentListIndex++;
+ FieldStack_.push_back(FieldStack_.back());
+ FieldStack_.back().ParsingList = true;
+ YPathStack_.Push(index);
+ TryWriteCustomlyConvertibleType();
+ }
+
+ void OnMyEndList() override
+ {
+ YT_ASSERT(!TypeStack_.empty());
+ FieldStack_.pop_back();
+ YPathStack_.Pop();
+ }
+
+ void OnMyBeginMap() override
+ {
+ if (TypeStack_.empty()) {
+ TypeStack_.emplace_back(RootType_);
+ FieldStack_.emplace_back(nullptr);
+ return;
+ }
+
+ const auto* field = FieldStack_.back().Field;
+ TypeStack_.emplace_back(field->GetMessageType());
+
+ if (!field->IsYsonMap() || FieldStack_.back().ParsingYsonMapFromList) {
+ if (field->GetType() != FieldDescriptor::TYPE_MESSAGE) {
+ THROW_ERROR_EXCEPTION("Field %v cannot be parsed from \"map\" values",
+ YPathStack_.GetPath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+
+ ValidateNotRepeated();
+ WriteTag();
+ BeginNestedMessage();
+ }
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ const auto* field = FieldStack_.back().Field;
+ if (field && field->IsYsonMap() && !FieldStack_.back().ParsingYsonMapFromList) {
+ OnMyKeyedItemYsonMap(field, key);
+ } else {
+ YT_ASSERT(!TypeStack_.empty());
+ const auto* type = TypeStack_.back().Type;
+ if (type->IsAttributeDictionary()) {
+ OnMyKeyedItemAttributeDictionary(key);
+ } else {
+ OnMyKeyedItemRegular(key);
+ }
+ }
+ }
+
+ void OnMyKeyedItemYsonMap(const TProtobufField* field, TStringBuf key)
+ {
+ auto& typeEntry = TypeStack_.back();
+ if (typeEntry.CurrentMapIndex > 0) {
+ EndNestedMessage();
+ }
+ ++typeEntry.CurrentMapIndex;
+
+ WriteTag();
+ BeginNestedMessage();
+
+ const auto* keyField = field->GetYsonMapKeyField();
+ auto keyType = keyField->GetType();
+ BodyCodedStream_.WriteTag(google::protobuf::internal::WireFormatLite::MakeTag(
+ ProtobufMapKeyFieldNumber,
+ WireFormatLite::WireTypeForFieldType(static_cast<WireFormatLite::FieldType>(keyType))));
+
+ switch (keyType) {
+ case FieldDescriptor::TYPE_SFIXED32:
+ case FieldDescriptor::TYPE_SFIXED64:
+ case FieldDescriptor::TYPE_SINT32:
+ case FieldDescriptor::TYPE_SINT64:
+ case FieldDescriptor::TYPE_INT32:
+ case FieldDescriptor::TYPE_INT64: {
+ i64 keyValue; // the widest singed integral type
+ if (!TryFromString(key, keyValue)) {
+ THROW_ERROR_EXCEPTION("Cannot parse a signed integral key of map %v from %Qv",
+ YPathStack_.GetPath(),
+ key)
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ WriteIntegerScalar(keyField, keyValue);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_UINT32:
+ case FieldDescriptor::TYPE_UINT64:
+ case FieldDescriptor::TYPE_FIXED64:
+ case FieldDescriptor::TYPE_FIXED32: {
+ ui64 keyValue; // the widest unsigned integral type
+ if (!TryFromString(key, keyValue)) {
+ THROW_ERROR_EXCEPTION("Cannot parse an unsigned integral key of map %v from %Qv",
+ YPathStack_.GetPath(),
+ key)
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ WriteIntegerScalar(keyField, keyValue);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_BYTES:
+ BodyCodedStream_.WriteVarint64(key.length());
+ BodyCodedStream_.WriteRaw(key.data(), static_cast<int>(key.length()));
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ const auto* valueField = field->GetYsonMapValueField();
+ FieldStack_.emplace_back(valueField);
+ YPathStack_.Push(TString(key));
+ TryWriteCustomlyConvertibleType();
+ }
+
+ void OnMyKeyedItemRegular(TStringBuf key)
+ {
+ auto& typeEntry = TypeStack_.back();
+ const auto* type = TypeStack_.back().Type;
+ const auto* field = type->FindFieldByName(key);
+
+ if (!field) {
+ auto path = NYPath::YPathJoin(YPathStack_.GetPath(), key);
+ auto unknownYsonFieldsMode = Options_.UnknownYsonFieldModeResolver(path);
+ auto onFinishForwarding = [this] (auto& writer) {
+ writer.Flush();
+ BodyCodedStream_.WriteTag(google::protobuf::internal::WireFormatLite::MakeTag(UnknownYsonFieldNumber, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ WriteKeyValuePair(UnknownYsonFieldKey_, UnknownYsonFieldValueString_);
+ };
+ if (unknownYsonFieldsMode == EUnknownYsonFieldsMode::Keep) {
+ UnknownYsonFieldKey_ = TString(key);
+ UnknownYsonFieldValueString_.clear();
+ Forward(
+ &UnknownYsonFieldValueStringWriter_,
+ [this, onFinishForwarding] {
+ onFinishForwarding(UnknownYsonFieldValueStringWriter_);
+ });
+ return;
+ }
+
+ if (unknownYsonFieldsMode == EUnknownYsonFieldsMode::Forward) {
+ ForwardingUnknownYsonFieldValueWriter_.YPathStack() = YPathStack_;
+ ForwardingUnknownYsonFieldValueWriter_.YPathStack().Push(TString(key));
+ ForwardingUnknownYsonFieldValueWriter_.ResetMode();
+ UnknownYsonFieldKey_ = TString(key);
+ UnknownYsonFieldValueString_.clear();
+ Forward(
+ &ForwardingUnknownYsonFieldValueWriter_,
+ [this, onFinishForwarding] {
+ onFinishForwarding(ForwardingUnknownYsonFieldValueWriter_);
+ });
+ return;
+ }
+
+ if (unknownYsonFieldsMode == EUnknownYsonFieldsMode::Skip || type->IsReservedFieldName(key)) {
+ Forward(GetNullYsonConsumer(), [] {});
+ return;
+ }
+
+ THROW_ERROR_EXCEPTION("Unknown field %Qv at %v",
+ key,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_type", type->GetFullName());
+ }
+
+ auto number = field->GetNumber();
+ ++typeEntry.CurrentMapIndex;
+ if (field->IsRequired()) {
+ typeEntry.RequiredFieldNumbers.push_back(number);
+ } else {
+ typeEntry.NonRequiredFieldNumbers.push_back(number);
+ }
+ FieldStack_.emplace_back(field);
+ YPathStack_.Push(TString{field->GetYsonName()});
+
+ if (field->IsYsonString()) {
+ YsonString_.clear();
+ Forward(&YsonStringWriter_, [this] {
+ YsonStringWriter_.Flush();
+
+ WriteScalar([this] {
+ BodyCodedStream_.WriteVarint64(YsonString_.length());
+ BodyCodedStream_.WriteRaw(YsonString_.begin(), static_cast<int>(YsonString_.length()));
+ });
+ });
+ } else {
+ TryWriteCustomlyConvertibleType();
+ }
+ }
+
+ void OnMyKeyedItemAttributeDictionary(TStringBuf key)
+ {
+ AttributeKey_ = key;
+ AttributeValue_.clear();
+ Forward(&AttributeValueWriter_, [this] {
+ AttributeValueWriter_.Flush();
+ BodyCodedStream_.WriteTag(google::protobuf::internal::WireFormatLite::MakeTag(AttributeDictionaryAttributeFieldNumber, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ WriteKeyValuePair(AttributeKey_, AttributeValue_);
+ });
+ }
+
+ void OnMyEndMap() override
+ {
+ auto& typeEntry = TypeStack_.back();
+ const auto* type = typeEntry.Type;
+
+ const auto* field = FieldStack_.back().Field;
+ if (field && field->IsYsonMap() && !FieldStack_.back().ParsingYsonMapFromList) {
+ if (typeEntry.CurrentMapIndex > 0) {
+ EndNestedMessage();
+ }
+
+ TypeStack_.pop_back();
+ } else {
+ SortFields(&typeEntry.NonRequiredFieldNumbers);
+ ValidateNoFieldDuplicates(type, typeEntry.NonRequiredFieldNumbers);
+
+ SortFields(&typeEntry.RequiredFieldNumbers);
+ ValidateNoFieldDuplicates(type, typeEntry.RequiredFieldNumbers);
+
+ if (!Options_.SkipRequiredFields) {
+ ValidateRequiredFieldsPresent(type, typeEntry.RequiredFieldNumbers);
+ }
+
+ TypeStack_.pop_back();
+ if (TypeStack_.empty()) {
+ Finish();
+ return;
+ }
+
+ EndNestedMessage();
+ }
+
+ FieldStack_.pop_back();
+ YPathStack_.Pop();
+ }
+
+ void ThrowAttributesNotSupported()
+ {
+ THROW_ERROR_EXCEPTION("Attributes are not supported")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ void OnMyBeginAttributes() override
+ {
+ ThrowAttributesNotSupported();
+ }
+
+ void OnMyEndAttributes() override
+ {
+ ThrowAttributesNotSupported();
+ }
+
+
+ void BeginNestedMessage()
+ {
+ auto index = static_cast<int>(NestedMessages_.size());
+ NestedMessages_.emplace_back(BodyCodedStream_.ByteCount(), -1);
+ NestedIndexStack_.push_back(index);
+ }
+
+ void EndNestedMessage()
+ {
+ int index = NestedIndexStack_.back();
+ NestedIndexStack_.pop_back();
+ YT_ASSERT(NestedMessages_[index].Hi == -1);
+ NestedMessages_[index].Hi = BodyCodedStream_.ByteCount();
+ }
+
+ void Finish()
+ {
+ YT_VERIFY(YPathStack_.IsEmpty());
+ YT_VERIFY(!FieldStack_.back().Field);
+
+ BodyCodedStream_.Trim();
+
+ int bodyLength = static_cast<int>(BodyString_.length());
+ NestedMessages_.emplace_back(bodyLength, std::numeric_limits<int>::max());
+
+ {
+ int nestedIndex = 0;
+ std::function<int(int, int)> computeByteSize = [&] (int lo, int hi) {
+ auto position = lo;
+ int result = 0;
+ while (true) {
+ auto& nestedMessage = NestedMessages_[nestedIndex];
+
+ {
+ auto threshold = std::min(hi, nestedMessage.Lo);
+ result += (threshold - position);
+ position = threshold;
+ }
+
+ if (nestedMessage.Lo == position && nestedMessage.Hi < std::numeric_limits<int>::max()) {
+ ++nestedIndex;
+ int nestedResult = computeByteSize(nestedMessage.Lo, nestedMessage.Hi);
+ nestedMessage.ByteSize = nestedResult;
+ result += BodyCodedStream_.VarintSize32(static_cast<ui32>(nestedResult));
+ result += nestedResult;
+ position = nestedMessage.Hi;
+ } else {
+ break;
+ }
+ }
+ return result;
+ };
+ computeByteSize(0, bodyLength);
+ }
+
+ {
+ int nestedIndex = 0;
+ std::function<void(int, int)> write = [&] (int lo, int hi) {
+ auto position = lo;
+ while (true) {
+ const auto& nestedMessage = NestedMessages_[nestedIndex];
+
+ {
+ auto threshold = std::min(hi, nestedMessage.Lo);
+ if (threshold > position) {
+ WriteRaw(BodyString_.data() + position, threshold - position);
+ }
+ position = threshold;
+ }
+
+ if (nestedMessage.Lo == position && nestedMessage.Hi < std::numeric_limits<int>::max()) {
+ ++nestedIndex;
+ char buf[16];
+ auto length = WriteVarUint64(buf, nestedMessage.ByteSize);
+ WriteRaw(buf, length);
+ write(nestedMessage.Lo, nestedMessage.Hi);
+ position = nestedMessage.Hi;
+ } else {
+ break;
+ }
+ }
+ };
+ write(0, bodyLength);
+ }
+ }
+
+ void WriteRaw(const char* data, int size)
+ {
+ while (true) {
+ void* chunkData;
+ int chunkSize;
+ if (!OutputStream_->Next(&chunkData, &chunkSize)) {
+ THROW_ERROR_EXCEPTION("Error writing to output stream");
+ }
+ auto bytesToWrite = std::min(chunkSize, size);
+ ::memcpy(chunkData, data, bytesToWrite);
+ if (bytesToWrite == size) {
+ OutputStream_->BackUp(chunkSize - size);
+ break;
+ }
+ data += bytesToWrite;
+ size -= bytesToWrite;
+ }
+ }
+
+
+ void ValidateNotRoot()
+ {
+ if (FieldStack_.empty()) {
+ THROW_ERROR_EXCEPTION("Protobuf message can only be parsed from \"map\" values")
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_type", RootType_->GetFullName());
+ }
+ }
+
+ void ValidateNotRepeated()
+ {
+ if (FieldStack_.back().ParsingList) {
+ return;
+ }
+ const auto* field = FieldStack_.back().Field;
+ if (field->IsYsonMap()) {
+ THROW_ERROR_EXCEPTION("Map %v cannot be parsed from scalar values",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_field", field->GetFullName());
+ }
+ if (field->IsRepeated()) {
+ THROW_ERROR_EXCEPTION("Field %v is repeated and cannot be parsed from scalar values",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_field", field->GetFullName());
+ }
+ }
+
+ void ValidateRepeated()
+ {
+ if (FieldStack_.back().ParsingList) {
+ THROW_ERROR_EXCEPTION("Items of list %v cannot be lists themselves",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ const auto* field = FieldStack_.back().Field;
+ if (!field->IsRepeated()) {
+ THROW_ERROR_EXCEPTION("Field %v is not repeated and cannot be parsed from \"list\" values",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_field", field->GetFullName());
+ }
+ }
+
+ void WriteTag()
+ {
+ YT_ASSERT(!FieldStack_.empty());
+ const auto* field = FieldStack_.back().Field;
+ BodyCodedStream_.WriteTag(field->GetTag());
+ }
+
+ template <class F>
+ void WriteScalar(F func)
+ {
+ ValidateNotRoot();
+ ValidateNotRepeated();
+ WriteTag();
+ func();
+ FieldStack_.pop_back();
+ YPathStack_.Pop();
+ }
+
+ void WriteKeyValuePair(const TString& key, const TString& value)
+ {
+ BodyCodedStream_.WriteVarint64(
+ 1 +
+ CodedOutputStream::VarintSize64(key.length()) +
+ key.length() +
+ 1 +
+ CodedOutputStream::VarintSize64(value.length()) +
+ value.length());
+
+ BodyCodedStream_.WriteTag(google::protobuf::internal::WireFormatLite::MakeTag(ProtobufMapKeyFieldNumber, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ BodyCodedStream_.WriteVarint64(key.length());
+ BodyCodedStream_.WriteRaw(key.data(), static_cast<int>(key.length()));
+
+ BodyCodedStream_.WriteTag(google::protobuf::internal::WireFormatLite::MakeTag(ProtobufMapValueFieldNumber, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ BodyCodedStream_.WriteVarint64(value.length());
+ BodyCodedStream_.WriteRaw(value.data(), static_cast<int>(value.length()));
+ }
+
+
+ template <class T>
+ void OnIntegerScalar(T value)
+ {
+ WriteScalar([&] {
+ const auto* field = FieldStack_.back().Field;
+ WriteIntegerScalar(field, value);
+ });
+ }
+
+ template <class T>
+ void WriteIntegerScalar(const TProtobufField* field, T value)
+ {
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_INT32: {
+ auto i32Value = CheckedCastField<i32>(value, TStringBuf("i32"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize32SignExtended(i32Value));
+ }
+ BodyCodedStream_.WriteVarint32SignExtended(i32Value);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_INT64: {
+ auto i64Value = CheckedCastField<i64>(value, TStringBuf("i64"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize64(i64Value));
+ }
+ BodyCodedStream_.WriteVarint64(static_cast<ui64>(i64Value));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SINT32: {
+ auto i32Value = CheckedCastField<i32>(value, TStringBuf("i32"), field);
+ auto encodedValue = ZigZagEncode64(i32Value);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize64(encodedValue));
+ }
+ BodyCodedStream_.WriteVarint64(encodedValue);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SINT64: {
+ auto i64Value = CheckedCastField<i64>(value, TStringBuf("i64"), field);
+ auto encodedValue = ZigZagEncode64(i64Value);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize64(encodedValue));
+ }
+ BodyCodedStream_.WriteVarint64(encodedValue);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_UINT32: {
+ auto ui32Value = CheckedCastField<ui32>(value, TStringBuf("ui32"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize32(ui32Value));
+ }
+ BodyCodedStream_.WriteVarint32(ui32Value);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_UINT64: {
+ auto ui64Value = CheckedCastField<ui64>(value, TStringBuf("ui64"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize64(ui64Value));
+ }
+ BodyCodedStream_.WriteVarint64(ui64Value);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FIXED32: {
+ auto ui32Value = CheckedCastField<ui32>(value, TStringBuf("ui32"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(ui32Value));
+ }
+ BodyCodedStream_.WriteRaw(&ui32Value, sizeof(ui32Value));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FIXED64: {
+ auto ui64Value = CheckedCastField<ui64>(value, TStringBuf("ui64"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(ui64Value));
+ }
+ BodyCodedStream_.WriteRaw(&ui64Value, sizeof(ui64Value));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SFIXED32: {
+ auto i32Value = CheckedCastField<i32>(value, TStringBuf("i32"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(i32Value));
+ }
+ BodyCodedStream_.WriteRaw(&i32Value, sizeof(i32Value));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SFIXED64: {
+ auto i64Value = CheckedCastField<i64>(value, TStringBuf("i64"), field);
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(i64Value));
+ }
+ BodyCodedStream_.WriteRaw(&i64Value, sizeof(i64Value));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_ENUM: {
+ auto i32Value = CheckedCastField<i32>(value, TStringBuf("i32"), field);
+ const auto* enumType = field->GetEnumType();
+ auto literal = enumType->FindLiteralByValue(i32Value);
+ if (!literal) {
+ THROW_ERROR_EXCEPTION("Unknown value %v for field %v",
+ i32Value,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(BodyCodedStream_.VarintSize32SignExtended(i32Value));
+ }
+ BodyCodedStream_.WriteVarint32SignExtended(i32Value);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_DOUBLE: {
+ auto encodedValue = WireFormatLite::EncodeDouble(static_cast<double>(value));
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(encodedValue));
+ }
+ BodyCodedStream_.WriteRaw(&encodedValue, sizeof(encodedValue));
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FLOAT: {
+ auto encodedValue = WireFormatLite::EncodeFloat(static_cast<double>(value));
+ if (field->IsPacked()) {
+ BodyCodedStream_.WriteVarint64(sizeof(encodedValue));
+ }
+ BodyCodedStream_.WriteRaw(&encodedValue, sizeof(encodedValue));
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Field %v cannot be parsed from integer values",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ }
+
+ template <class TTo, class TFrom>
+ TTo CheckedCastField(TFrom value, TStringBuf toTypeName, const TProtobufField* field)
+ {
+ TTo result;
+ if (!TryIntegralCast<TTo>(value, &result)) {
+ THROW_ERROR_EXCEPTION("Value %v of field %v cannot fit into %Qv",
+ value,
+ YPathStack_.GetHumanReadablePath(),
+ toTypeName)
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("protobuf_field", field->GetFullName());
+ }
+ return result;
+ }
+
+ void TryWriteCustomlyConvertibleType()
+ {
+ if (FieldStack_.empty()) {
+ return;
+ }
+
+ const auto* field = FieldStack_.back().Field;
+ if (field->GetBytesFieldConverter()) {
+ if (field->IsRepeated() && !FieldStack_.back().ParsingList) {
+ return;
+ }
+ const auto& converter = *field->GetBytesFieldConverter();
+ TreeBuilder_->BeginTree();
+ Forward(TreeBuilder_.get(), [this, converter] {
+ auto node = TreeBuilder_->EndTree();
+ BytesString_.clear();
+ converter.Deserializer(&BytesString_, node);
+ OnMyStringScalar(BytesString_);
+ });
+ } else if (field->GetType() == FieldDescriptor::TYPE_MESSAGE && field->GetMessageType()->GetConverter()) {
+ if (field->IsRepeated() && !FieldStack_.back().ParsingList) {
+ return;
+ }
+ const auto* messageType = field->GetMessageType();
+ const auto& converter = *messageType->GetConverter();
+ TreeBuilder_->BeginTree();
+ Forward(TreeBuilder_.get(), [this, converter, messageType] {
+ auto node = TreeBuilder_->EndTree();
+ std::unique_ptr<Message> message(MessageFactory::generated_factory()->GetPrototype(messageType->GetUnderlying())->New());
+ converter.Deserializer(message.get(), node);
+ SerializedMessage_.clear();
+ Y_PROTOBUF_SUPPRESS_NODISCARD message->SerializeToString(&SerializedMessage_);
+ WriteTag();
+ BodyCodedStream_.WriteVarint64(SerializedMessage_.length());
+ BodyCodedStream_.WriteRaw(SerializedMessage_.begin(), static_cast<int>(SerializedMessage_.length()));
+ FieldStack_.pop_back();
+ YPathStack_.Pop();
+ });
+ }
+ }
+};
+
+std::unique_ptr<IYsonConsumer> CreateProtobufWriter(
+ ZeroCopyOutputStream* outputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufWriterOptions& options)
+{
+ return std::make_unique<TProtobufWriter>(outputStream, rootType, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProtobufParser
+ : public TProtobufTranscoderBase
+{
+public:
+ TProtobufParser(
+ IYsonConsumer* consumer,
+ ZeroCopyInputStream* inputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufParserOptions& options)
+ : Consumer_(consumer)
+ , RootType_(rootType)
+ , Options_(options)
+ , InputStream_(inputStream)
+ , CodedStream_(InputStream_)
+ { }
+
+ void Parse()
+ {
+ TypeStack_.emplace_back(RootType_);
+ Consumer_->OnBeginMap();
+
+ while (true) {
+ auto& typeEntry = TypeStack_.back();
+ const auto* type = typeEntry.Type;
+
+ bool flag;
+ if (type->IsAttributeDictionary()) {
+ flag = ParseAttributeDictionary();
+ } else if (IsYsonMapEntry()) {
+ flag = ParseMapEntry();
+ } else {
+ flag = ParseRegular();
+ }
+
+ if (!flag) {
+ if (typeEntry.RepeatedField) {
+ if (typeEntry.RepeatedField->IsYsonMap()) {
+ OnEndMap();
+ } else {
+ OnEndList();
+ }
+ }
+
+ SortFields(&typeEntry.OptionalFieldNumbers);
+ ValidateNoFieldDuplicates(type, typeEntry.OptionalFieldNumbers);
+
+ SortFields(&typeEntry.RequiredFieldNumbers);
+ ValidateNoFieldDuplicates(type, typeEntry.RequiredFieldNumbers);
+
+ if (!Options_.SkipRequiredFields && !IsYsonMapEntry()) {
+ ValidateRequiredFieldsPresent(type, typeEntry.RequiredFieldNumbers);
+ }
+
+ if (TypeStack_.size() == 1) {
+ break;
+ }
+
+ if (IsYsonMapEntry()) {
+ if (typeEntry.RequiredFieldNumbers.size() != 2) {
+ THROW_ERROR_EXCEPTION("Incomplete entry in protobuf map")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ } else {
+ OnEndMap();
+ }
+ TypeStack_.pop_back();
+
+ CodedStream_.PopLimit(LimitStack_.back());
+ LimitStack_.pop_back();
+ continue;
+ }
+ }
+
+ Consumer_->OnEndMap();
+ TypeStack_.pop_back();
+
+ YT_VERIFY(TypeStack_.empty());
+ YT_VERIFY(YPathStack_.IsEmpty());
+ YT_VERIFY(LimitStack_.empty());
+ }
+
+private:
+ IYsonConsumer* const Consumer_;
+ const TProtobufMessageType* const RootType_;
+ const TProtobufParserOptions Options_;
+ ZeroCopyInputStream* const InputStream_;
+
+ CodedInputStream CodedStream_;
+
+ struct TTypeEntry
+ {
+ explicit TTypeEntry(const TProtobufMessageType* type)
+ : Type(type)
+ { }
+
+ const TProtobufMessageType* Type;
+ TFieldNumberList RequiredFieldNumbers;
+ TFieldNumberList OptionalFieldNumbers;
+ const TProtobufField* RepeatedField = nullptr;
+ int RepeatedIndex = -1;
+
+ void BeginRepeated(const TProtobufField* field)
+ {
+ YT_ASSERT(!RepeatedField);
+ YT_ASSERT(RepeatedIndex == -1);
+ RepeatedField = field;
+ RepeatedIndex = 0;
+ }
+
+ void ResetRepeated()
+ {
+ RepeatedField = nullptr;
+ RepeatedIndex = -1;
+ }
+
+ int GenerateNextListIndex()
+ {
+ YT_ASSERT(RepeatedField);
+ return ++RepeatedIndex;
+ }
+ };
+ std::vector<TTypeEntry> TypeStack_;
+
+ std::vector<CodedInputStream::Limit> LimitStack_;
+
+ std::vector<char> PooledString_;
+ std::vector<char> PooledKey_;
+ std::vector<char> PooledValue_;
+
+
+ void OnBeginMap()
+ {
+ Consumer_->OnBeginMap();
+ }
+
+ void OnKeyedItem(const TProtobufField* field)
+ {
+ Consumer_->OnKeyedItem(field->GetYsonName());
+ YPathStack_.Push(TString{field->GetYsonName()});
+ }
+
+ void OnKeyedItem(TString key)
+ {
+ Consumer_->OnKeyedItem(key);
+ YPathStack_.Push(std::move(key));
+ }
+
+ void OnEndMap()
+ {
+ Consumer_->OnEndMap();
+ YPathStack_.Pop();
+ }
+
+
+ void OnBeginList()
+ {
+ Consumer_->OnBeginList();
+ }
+
+ void OnListItem(int index)
+ {
+ Consumer_->OnListItem();
+ YPathStack_.Push(index);
+ }
+
+ void OnEndList()
+ {
+ Consumer_->OnEndList();
+ YPathStack_.Pop();
+ }
+
+
+ bool IsYsonMapEntry()
+ {
+ if (TypeStack_.size() < 2) {
+ return false;
+ }
+ auto& typeEntry = TypeStack_[TypeStack_.size() - 2];
+ if (!typeEntry.RepeatedField) {
+ return false;
+ }
+ if (!typeEntry.RepeatedField->IsYsonMap()) {
+ return false;
+ }
+ return true;
+ }
+
+ template <class T>
+ void FillPooledStringWithInteger(T value)
+ {
+ PooledString_.resize(64); // enough for any value
+ auto length = ToString(value, PooledString_.data(), PooledString_.size());
+ PooledString_.resize(length);
+ }
+
+ bool ParseMapEntry()
+ {
+ auto& typeEntry = TypeStack_.back();
+ const auto* type = typeEntry.Type;
+
+ auto tag = CodedStream_.ReadTag();
+ if (tag == 0) {
+ return false;
+ }
+
+ auto wireType = WireFormatLite::GetTagWireType(tag);
+ auto fieldNumber = WireFormatLite::GetTagFieldNumber(tag);
+ typeEntry.RequiredFieldNumbers.push_back(fieldNumber);
+
+ switch (fieldNumber) {
+ case ProtobufMapKeyFieldNumber: {
+ if (typeEntry.RequiredFieldNumbers.size() != 1) {
+ THROW_ERROR_EXCEPTION("Out-of-order protobuf map key")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ const auto* field = type->GetFieldByNumber(fieldNumber);
+ switch (wireType) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ ui64 keyValue;
+ if (!CodedStream_.ReadVarint64(&keyValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for protobuf map key")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_INT64:
+ FillPooledStringWithInteger(static_cast<i64>(keyValue));
+ break;
+
+ case FieldDescriptor::TYPE_UINT64:
+ FillPooledStringWithInteger(keyValue);
+ break;
+
+ case FieldDescriptor::TYPE_INT32:
+ FillPooledStringWithInteger(static_cast<i32>(keyValue));
+ break;
+
+ case FieldDescriptor::TYPE_SINT32:
+ FillPooledStringWithInteger(ZigZagDecode32(static_cast<ui32>(keyValue)));
+ break;
+
+ case FieldDescriptor::TYPE_SINT64:
+ FillPooledStringWithInteger(ZigZagDecode64(keyValue));
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ ui32 keyValue;
+ if (!CodedStream_.ReadRaw(&keyValue, sizeof(keyValue))) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed32\" value for protobuf map key")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ if (IsSignedIntegralType(field->GetType())) {
+ FillPooledStringWithInteger(static_cast<i32>(keyValue));
+ } else {
+ FillPooledStringWithInteger(keyValue);
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ ui64 keyValue;
+ if (!CodedStream_.ReadRaw(&keyValue, sizeof(keyValue))) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed64\" value for protobuf map key")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ if (IsSignedIntegralType(field->GetType())) {
+ FillPooledStringWithInteger(static_cast<i64>(keyValue));
+ } else {
+ FillPooledStringWithInteger(keyValue);
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ ui64 keyLength;
+ if (!CodedStream_.ReadVarint64(&keyLength)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for protobuf map key length")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ PooledString_.resize(keyLength);
+ if (!CodedStream_.ReadRaw(PooledString_.data(), keyLength)) {
+ THROW_ERROR_EXCEPTION("Error reading \"string\" value for protobuf map key")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected wire type tag %x for protobuf map key",
+ tag)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ OnKeyedItem(TString(PooledString_.data(), PooledString_.size()));
+ break;
+ }
+
+ case ProtobufMapValueFieldNumber: {
+ if (typeEntry.RequiredFieldNumbers.size() != 2) {
+ THROW_ERROR_EXCEPTION("Out-of-order protobuf map value")
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ const auto* field = type->GetFieldByNumber(fieldNumber);
+ ParseFieldValue(field, tag, wireType);
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected field number %v in protobuf map",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ return true;
+ }
+
+ bool ParseRegular()
+ {
+ auto& typeEntry = TypeStack_.back();
+ const auto* type = typeEntry.Type;
+
+ auto tag = CodedStream_.ReadTag();
+ if (tag == 0) {
+ return false;
+ }
+
+ auto handleRepeated = [&] {
+ if (typeEntry.RepeatedField) {
+ if (typeEntry.RepeatedField->IsYsonMap()) {
+ Consumer_->OnEndMap();
+ } else {
+ Consumer_->OnEndList();
+ }
+ YPathStack_.Pop();
+ }
+ typeEntry.ResetRepeated();
+ };
+
+ auto wireType = WireFormatLite::GetTagWireType(tag);
+ auto fieldNumber = WireFormatLite::GetTagFieldNumber(tag);
+ if (fieldNumber == UnknownYsonFieldNumber) {
+ if (wireType != WireFormatLite::WIRETYPE_LENGTH_DELIMITED) {
+ THROW_ERROR_EXCEPTION("Invalid wire type %v while parsing unknown field at %v",
+ static_cast<int>(wireType),
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ handleRepeated();
+ ParseKeyValuePair();
+ return true;
+ }
+
+ const auto* field = type->FindFieldByNumber(fieldNumber);
+ if (!field) {
+ if (Options_.SkipUnknownFields || type->IsReservedFieldNumber(fieldNumber)) {
+ switch (wireType) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ ui64 unsignedValue;
+ if (!CodedStream_.ReadVarint64(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for unknown field %v",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ ui32 unsignedValue;
+ if (!CodedStream_.ReadLittleEndian32(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed32\" value for unknown field %v",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ ui64 unsignedValue;
+ if (!CodedStream_.ReadLittleEndian64(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed64\" value for unknown field %v",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ ui64 length;
+ if (!CodedStream_.ReadVarint64(&length)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for unknown field %v",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ if (length > std::numeric_limits<int>::max()) {
+ THROW_ERROR_EXCEPTION("Invalid length %v for unknown field %v",
+ length,
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ if (!CodedStream_.Skip(static_cast<int>(length))) {
+ THROW_ERROR_EXCEPTION("Error skipping unknown length-delimited field %v",
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected wire type tag %x for unknown field %v",
+ tag,
+ fieldNumber)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ return true;
+ }
+ THROW_ERROR_EXCEPTION("Unknown field number %v at %v",
+ fieldNumber,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_type", type->GetFullName());
+ }
+
+ if (typeEntry.RepeatedField == field) {
+ if (!field->IsYsonMap()) {
+ YT_ASSERT(field->IsRepeated());
+ OnListItem(typeEntry.GenerateNextListIndex());
+ }
+ } else {
+ handleRepeated();
+
+ OnKeyedItem(field);
+
+ if (field->IsYsonMap()) {
+ typeEntry.BeginRepeated(field);
+ OnBeginMap();
+ } else if (field->IsRepeated()) {
+ typeEntry.BeginRepeated(field);
+ OnBeginList();
+ OnListItem(0);
+ }
+ }
+
+ if (field->IsRequired()) {
+ typeEntry.RequiredFieldNumbers.push_back(fieldNumber);
+ } else if (field->IsOptional()) {
+ typeEntry.OptionalFieldNumbers.push_back(fieldNumber);
+ }
+
+ ParseFieldValue(field, tag, wireType);
+
+ return true;
+ }
+
+ template <class T>
+ void ParseFixedPacked(ui64 length, auto field, auto&& func)
+ {
+ YT_ASSERT(length % sizeof(T) == 0);
+ for (auto index = 0u; index < length / sizeof(T); ++index) {
+ T unsignedValue;
+ auto readResult = false;
+ if constexpr (std::is_same_v<T, ui64>) {
+ readResult = CodedStream_.ReadLittleEndian64(&unsignedValue);
+ } else {
+ readResult = CodedStream_.ReadLittleEndian32(&unsignedValue);
+ }
+ if (!readResult) {
+ THROW_ERROR_EXCEPTION("Error reading %Qv value from field %v",
+ field->GetTypeName(),
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ if (index > 0) {
+ YT_ASSERT(field->IsRepeated());
+ OnListItem(TypeStack_.back().GenerateNextListIndex());
+ }
+ ParseScalar([&] {func(unsignedValue);});
+ }
+ }
+
+ template <class T>
+ void ParseVarintPacked(ui64 length, auto field, auto&& func)
+ {
+ const void* data = nullptr;
+ int size = 0;
+ CodedStream_.GetDirectBufferPointer(&data, &size);
+ YT_ASSERT(length <= static_cast<ui64>(size));
+ ArrayInputStream array(data, length);
+ CodedInputStream in(&array);
+ size_t index = 0;
+ while (static_cast<ui64>(in.CurrentPosition()) < length) {
+ T unsignedValue;
+ auto read = false;
+ if constexpr (std::is_same_v<T, ui64>) {
+ read = in.ReadVarint64(&unsignedValue);
+ } else {
+ read = in.ReadVarint32(&unsignedValue);
+ }
+ if (!read) {
+ THROW_ERROR_EXCEPTION("Error reading \"%v\" value for field %v",
+ field->GetTypeName(),
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ if (index > 0) {
+ YT_ASSERT(field->IsRepeated());
+ OnListItem(TypeStack_.back().GenerateNextListIndex());
+ }
+ ++index;
+ ParseScalar([&] {func(unsignedValue);});
+ }
+ CodedStream_.Skip(length);
+ }
+
+ void ParseFieldValue(
+ const TProtobufField* field,
+ int tag,
+ WireFormatLite::WireType wireType)
+ {
+ switch (wireType) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ ui64 unsignedValue;
+ if (!CodedStream_.ReadVarint64(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_BOOL:
+ ParseScalar([&] {
+ Consumer_->OnBooleanScalar(unsignedValue != 0);
+ });
+ break;
+
+ case FieldDescriptor::TYPE_ENUM: {
+ auto signedValue = static_cast<int>(unsignedValue);
+ const auto* enumType = field->GetEnumType();
+ auto literal = enumType->FindLiteralByValue(signedValue);
+ if (!literal) {
+ THROW_ERROR_EXCEPTION("Unknown value %v for field %v",
+ signedValue,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ ParseScalar([&] {
+ Consumer_->OnStringScalar(literal);
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_INT32:
+ case FieldDescriptor::TYPE_INT64:
+ ParseScalar([&] {
+ auto signedValue = static_cast<i64>(unsignedValue);
+ Consumer_->OnInt64Scalar(signedValue);
+ });
+ break;
+
+ case FieldDescriptor::TYPE_UINT32:
+ case FieldDescriptor::TYPE_UINT64:
+ ParseScalar([&] {
+ Consumer_->OnUint64Scalar(unsignedValue);
+ });
+ break;
+
+ case FieldDescriptor::TYPE_SINT64:
+ case FieldDescriptor::TYPE_SINT32:
+ ParseScalar([&] {
+ auto signedValue = ZigZagDecode64(unsignedValue);
+ Consumer_->OnInt64Scalar(signedValue);
+ });
+ break;
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected \"varint\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ ui32 unsignedValue;
+ if (!CodedStream_.ReadLittleEndian32(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed32\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_FIXED32:
+ ParseScalar([&] {
+ Consumer_->OnUint64Scalar(unsignedValue);
+ });
+ break;
+
+ case FieldDescriptor::TYPE_SFIXED32: {
+ ParseScalar([&] {
+ auto signedValue = static_cast<i32>(unsignedValue);
+ Consumer_->OnInt64Scalar(signedValue);
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FLOAT: {
+ ParseScalar([&] {
+ auto floatValue = WireFormatLite::DecodeFloat(unsignedValue);
+ Consumer_->OnDoubleScalar(floatValue);
+ });
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected \"fixed32\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ ui64 unsignedValue;
+ if (!CodedStream_.ReadLittleEndian64(&unsignedValue)) {
+ THROW_ERROR_EXCEPTION("Error reading \"fixed64\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_FIXED64:
+ ParseScalar([&] {
+ Consumer_->OnUint64Scalar(unsignedValue);
+ });
+ break;
+
+ case FieldDescriptor::TYPE_SFIXED64: {
+ ParseScalar([&] {
+ auto signedValue = static_cast<i64>(unsignedValue);
+ Consumer_->OnInt64Scalar(signedValue);
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_DOUBLE: {
+ ParseScalar([&] {
+ auto doubleValue = WireFormatLite::DecodeDouble(unsignedValue);
+ Consumer_->OnDoubleScalar(doubleValue);
+ });
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected \"fixed64\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ break;
+ }
+
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ ui64 length;
+ if (!CodedStream_.ReadVarint64(&length)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ switch (field->GetType()) {
+ case FieldDescriptor::TYPE_BYTES:
+ case FieldDescriptor::TYPE_STRING: {
+ PooledString_.resize(length);
+ if (!CodedStream_.ReadRaw(PooledString_.data(), length)) {
+ THROW_ERROR_EXCEPTION("Error reading \"string\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ TStringBuf data(PooledString_.data(), length);
+ ParseScalar([&] {
+ if (field->GetBytesFieldConverter()) {
+ const auto& converter = *field->GetBytesFieldConverter();
+ converter.Serializer(Consumer_, data);
+ } else if (field->IsYsonString()) {
+ Consumer_->OnRaw(data, NYson::EYsonType::Node);
+ } else {
+ Consumer_->OnStringScalar(data);
+ }
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_MESSAGE: {
+ const auto* messageType = field->GetMessageType();
+ if (messageType->GetConverter()) {
+ const auto& converter = *messageType->GetConverter();
+ std::unique_ptr<Message> message(MessageFactory::generated_factory()->GetPrototype(messageType->GetUnderlying())->New());
+ PooledString_.resize(length);
+ CodedStream_.ReadRaw(PooledString_.data(), PooledString_.size());
+ Y_PROTOBUF_SUPPRESS_NODISCARD message->ParseFromArray(PooledString_.data(), PooledString_.size());
+ converter.Serializer(Consumer_, message.get());
+ YPathStack_.Pop();
+ } else {
+ LimitStack_.push_back(CodedStream_.PushLimit(static_cast<int>(length)));
+ TypeStack_.emplace_back(field->GetMessageType());
+ if (!IsYsonMapEntry()) {
+ OnBeginMap();
+ }
+ }
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FIXED32: {
+ ParseFixedPacked<ui32>(length, field, [&] (auto value) {Consumer_->OnUint64Scalar(value);});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FIXED64: {
+ ParseFixedPacked<ui64>(length, field, [&] (auto value) {Consumer_->OnUint64Scalar(value);});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SFIXED32: {
+ ParseFixedPacked<ui32>(length, field, [&] (auto value) {Consumer_->OnInt64Scalar(static_cast<i32>(value));});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_SFIXED64: {
+ ParseFixedPacked<ui64>(length, field, [&] (auto value) {Consumer_->OnInt64Scalar(static_cast<i64>(value));});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_FLOAT: {
+ ParseFixedPacked<ui32>(length, field,
+ [&] (auto value) {
+ auto floatValue = WireFormatLite::DecodeFloat(value);
+ Consumer_->OnDoubleScalar(floatValue);
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_DOUBLE: {
+ ParseFixedPacked<ui64>(length, field,
+ [&] (auto value) {
+ auto doubleValue = WireFormatLite::DecodeDouble(value);
+ Consumer_->OnDoubleScalar(doubleValue);
+ });
+ break;
+ }
+
+ case FieldDescriptor::TYPE_INT32: {
+ ParseVarintPacked<ui32>(length, field, [&] (auto value) {Consumer_->OnInt64Scalar(static_cast<i32>(value));});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_INT64: {
+ ParseVarintPacked<ui64>(length, field, [&] (auto value) {Consumer_->OnInt64Scalar(static_cast<i64>(value));});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_UINT32: {
+ ParseVarintPacked<ui32>(length, field, [&] (auto value) {Consumer_->OnUint64Scalar(value);});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_UINT64: {
+ ParseVarintPacked<ui64>(length, field, [&] (auto value) {Consumer_->OnUint64Scalar(value);});
+ break;
+ }
+
+ case FieldDescriptor::TYPE_ENUM: {
+ ParseVarintPacked<ui32>(length, field, [&] (auto value) {Consumer_->OnInt64Scalar(value);});
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected \"length-delimited\" value for field %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath())
+ << TErrorAttribute("proto_field", field->GetFullName());
+ }
+ break;
+ }
+
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected wire type tag %x",
+ tag)
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ }
+
+ bool ParseAttributeDictionary()
+ {
+ auto throwUnexpectedWireType = [&] (WireFormatLite::WireType actualWireType) {
+ THROW_ERROR_EXCEPTION("Invalid wire type %v while parsing attribute dictionary %v",
+ static_cast<int>(actualWireType),
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ };
+
+ auto expectWireType = [&] (WireFormatLite::WireType actualWireType, WireFormatLite::WireType expectedWireType) {
+ if (actualWireType != expectedWireType) {
+ throwUnexpectedWireType(actualWireType);
+ }
+ };
+
+ auto throwUnexpectedFieldNumber = [&] (int actualFieldNumber) {
+ THROW_ERROR_EXCEPTION("Invalid field number %v while parsing attribute dictionary %v",
+ actualFieldNumber,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ };
+
+ auto expectFieldNumber = [&] (int actualFieldNumber, int expectedFieldNumber) {
+ if (actualFieldNumber != expectedFieldNumber) {
+ throwUnexpectedFieldNumber(actualFieldNumber);
+ }
+ };
+
+ while (true) {
+ auto tag = CodedStream_.ReadTag();
+ if (tag == 0) {
+ return false;
+ }
+
+ expectWireType(WireFormatLite::GetTagWireType(tag), WireFormatLite::WIRETYPE_LENGTH_DELIMITED);
+ expectFieldNumber(WireFormatLite::GetTagFieldNumber(tag), AttributeDictionaryAttributeFieldNumber);
+
+ ParseKeyValuePair();
+ }
+ }
+
+ void ParseKeyValuePair()
+ {
+ auto throwUnexpectedWireType = [&] (WireFormatLite::WireType actualWireType) {
+ THROW_ERROR_EXCEPTION("Invalid wire type %v while parsing key-value pair at %v",
+ static_cast<int>(actualWireType),
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ };
+
+ auto expectWireType = [&] (WireFormatLite::WireType actualWireType, WireFormatLite::WireType expectedWireType) {
+ if (actualWireType != expectedWireType) {
+ throwUnexpectedWireType(actualWireType);
+ }
+ };
+
+ auto throwUnexpectedFieldNumber = [&] (int actualFieldNumber) {
+ THROW_ERROR_EXCEPTION("Invalid field number %v while parsing key-value pair at %v",
+ actualFieldNumber,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ };
+
+ auto readVarint64 = [&] () {
+ ui64 value;
+ if (!CodedStream_.ReadVarint64(&value)) {
+ THROW_ERROR_EXCEPTION("Error reading \"varint\" value while parsing key-value pair at %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ return value;
+ };
+
+ auto readString = [&] (auto* pool) -> TStringBuf {
+ auto length = readVarint64();
+ pool->resize(length);
+ if (!CodedStream_.ReadRaw(pool->data(), length)) {
+ THROW_ERROR_EXCEPTION("Error reading \"string\" value while parsing key-value pair at %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ return TStringBuf(pool->data(), length);
+ };
+
+ auto entryLength = readVarint64();
+ LimitStack_.push_back(CodedStream_.PushLimit(static_cast<int>(entryLength)));
+
+ std::optional<TStringBuf> key;
+ std::optional<TStringBuf> value;
+ while (true) {
+ auto tag = CodedStream_.ReadTag();
+ if (tag == 0) {
+ break;
+ }
+
+ auto fieldNumber = WireFormatLite::GetTagFieldNumber(tag);
+ switch (fieldNumber) {
+ case ProtobufMapKeyFieldNumber: {
+ expectWireType(WireFormatLite::GetTagWireType(tag), WireFormatLite::WIRETYPE_LENGTH_DELIMITED);
+ if (key) {
+ THROW_ERROR_EXCEPTION("Duplicate key found while parsing key-value pair at%v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ key = readString(&PooledKey_);
+ break;
+ }
+
+ case ProtobufMapValueFieldNumber: {
+ expectWireType(WireFormatLite::GetTagWireType(tag), WireFormatLite::WIRETYPE_LENGTH_DELIMITED);
+ if (value) {
+ THROW_ERROR_EXCEPTION("Duplicate value found while parsing key-value pair at %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ value = readString(&PooledValue_);
+ break;
+ }
+
+ default:
+ throwUnexpectedFieldNumber(fieldNumber);
+ break;
+ }
+ }
+
+ if (!key) {
+ THROW_ERROR_EXCEPTION("Missing key while parsing key-value pair at %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+ if (!value) {
+ THROW_ERROR_EXCEPTION("Missing value while parsing key-value pair %v",
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", YPathStack_.GetPath());
+ }
+
+ Consumer_->OnKeyedItem(*key);
+ Consumer_->OnRaw(*value, NYson::EYsonType::Node);
+
+ CodedStream_.PopLimit(LimitStack_.back());
+ LimitStack_.pop_back();
+ }
+
+ template <class F>
+ void ParseScalar(F func)
+ {
+ func();
+ YPathStack_.Pop();
+ }
+};
+
+void ParseProtobuf(
+ IYsonConsumer* consumer,
+ ZeroCopyInputStream* inputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufParserOptions& options)
+{
+ TProtobufParser parser(consumer, inputStream, rootType, options);
+ parser.Parse();
+}
+
+void WriteProtobufMessage(
+ IYsonConsumer* consumer,
+ const ::google::protobuf::Message& message,
+ const TProtobufParserOptions& options)
+{
+ auto data = SerializeProtoToRef(message);
+ ArrayInputStream stream(data.Begin(), data.Size());
+ const auto* type = ReflectProtobufMessageType(message.GetDescriptor());
+ ParseProtobuf(consumer, &stream, type, options);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TStringBuf FormatYPath(TStringBuf ypath)
+{
+ return ypath.empty() ? TStringBuf("/") : ypath;
+}
+
+TProtobufElementResolveResult GetProtobufElementFromField(
+ const TProtobufField* field,
+ bool insideRepeated,
+ const NYPath::TTokenizer& tokenizer)
+{
+ auto element = field->GetElement(insideRepeated);
+ if (std::holds_alternative<std::unique_ptr<TProtobufScalarElement>>(element) && !tokenizer.GetSuffix().empty()) {
+ THROW_ERROR_EXCEPTION("Field %v is scalar and does not support nested access",
+ FormatYPath(tokenizer.GetPrefixPlusToken()))
+ << TErrorAttribute("ypath", tokenizer.GetPrefixPlusToken());
+ }
+ return TProtobufElementResolveResult{
+ std::move(element),
+ tokenizer.GetPrefixPlusToken(),
+ tokenizer.GetSuffix()
+ };
+}
+
+} // namespace
+
+TProtobufElementResolveResult ResolveProtobufElementByYPath(
+ const TProtobufMessageType* rootType,
+ const NYPath::TYPathBuf path,
+ const TResolveProtobufElementByYPathOptions& options)
+{
+ NYPath::TTokenizer tokenizer(path);
+
+ auto makeResult = [&] (TProtobufElement element) {
+ return TProtobufElementResolveResult{
+ std::move(element),
+ tokenizer.GetPrefixPlusToken(),
+ tokenizer.GetSuffix()
+ };
+ };
+
+ const auto* currentType = rootType;
+ while (true) {
+ YT_VERIFY(currentType);
+
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ break;
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+
+ if (currentType->IsAttributeDictionary()) {
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ return makeResult(std::make_unique<TProtobufAnyElement>());
+ }
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+
+ const auto& fieldName = tokenizer.GetLiteralValue();
+ const auto* field = currentType->FindFieldByName(fieldName);
+ if (!field) {
+ if (options.AllowUnknownYsonFields) {
+ return makeResult(std::make_unique<TProtobufAnyElement>());
+ }
+ THROW_ERROR_EXCEPTION("No such field %v",
+ FormatYPath(tokenizer.GetPrefixPlusToken()))
+ << TErrorAttribute("ypath", tokenizer.GetPrefixPlusToken())
+ << TErrorAttribute("message_type", currentType->GetFullName());
+ }
+
+ if (!field->IsMessage()) {
+ if (field->IsRepeated()) {
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ return makeResult(field->GetElement(false));
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.ExpectListIndex();
+
+ return GetProtobufElementFromField(
+ field,
+ true,
+ tokenizer);
+ } else {
+ return GetProtobufElementFromField(
+ field,
+ false,
+ tokenizer);
+ }
+ }
+
+ if (field->IsYsonMap()) {
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ return makeResult(field->GetElement(false));
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+
+ const auto* valueField = field->GetYsonMapValueField();
+ if (!valueField->IsMessage()) {
+ return GetProtobufElementFromField(
+ valueField,
+ false,
+ tokenizer);
+ }
+
+ currentType = valueField->GetMessageType();
+ } else if (field->IsRepeated()) {
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ return makeResult(field->GetElement(false));
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.ExpectListIndex();
+
+ if (!field->IsMessage()) {
+ return GetProtobufElementFromField(
+ field,
+ true,
+ tokenizer);
+ }
+
+ currentType = field->GetMessageType();
+ } else {
+ currentType = field->GetMessageType();
+ }
+ }
+ return makeResult(currentType->GetElement());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void AddProtobufConverterRegisterAction(std::function<void()> action)
+{
+ TProtobufTypeRegistry::Get()->AddRegisterAction(std::move(action));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RegisterCustomProtobufConverter(
+ const Descriptor* descriptor,
+ const TProtobufMessageConverter& converter)
+{
+ TProtobufTypeRegistry::Get()->RegisterMessageTypeConverter(descriptor, converter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RegisterCustomProtobufBytesFieldConverter(
+ const Descriptor* descriptor,
+ int fieldNumber,
+ const TProtobufMessageBytesFieldConverter& converter)
+{
+ // NB: Protobuf internal singletons might not be ready, so we can't get field descriptor here.
+ TProtobufTypeRegistry::Get()->RegisterMessageBytesFieldConverter(descriptor, fieldNumber, converter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString YsonStringToProto(
+ const TYsonString& ysonString,
+ const TProtobufMessageType* payloadType,
+ EUnknownYsonFieldsMode unknownFieldsMode)
+{
+ TString serializedProto;
+ google::protobuf::io::StringOutputStream protobufStream(&serializedProto);
+ TProtobufWriterOptions protobufWriterOptions;
+ protobufWriterOptions.UnknownYsonFieldModeResolver =
+ TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(
+ unknownFieldsMode);
+ auto protobufWriter = CreateProtobufWriter(&protobufStream, payloadType, protobufWriterOptions);
+ ParseYsonStringBuffer(ysonString.AsStringBuf(), EYsonType::Node, protobufWriter.get());
+ return serializedProto;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/protobuf_interop.h b/yt/yt/core/yson/protobuf_interop.h
new file mode 100644
index 0000000000..78d51a10cb
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop.h
@@ -0,0 +1,289 @@
+#pragma once
+
+#include "protobuf_interop_options.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+#include <variant>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An opaque reflected counterpart of ::google::protobuf::Descriptor.
+/*!
+ * Reflecting a descriptor takes the following options into account:
+ * NYT.NProto.NYson.field_name: overrides the default name of field
+ * NYT.NProto.NYson.enum_value_name: overrides the default name of enum value
+ */
+class TProtobufMessageType;
+
+//! An opaque reflected counterpart of ::google::protobuf::EnumDescriptor.
+class TProtobufEnumType;
+
+//! Reflects ::google::protobuf::Descriptor.
+/*!
+ * The call caches its result in a static variable and is thus efficient.
+ */
+template <class T>
+const TProtobufMessageType* ReflectProtobufMessageType();
+
+//! Reflects ::google::protobuf::Descriptor.
+/*!
+ * The call invokes the internal reflection registry and takes spinlocks.
+ * Should not be assumed to be efficient.
+ */
+const TProtobufMessageType* ReflectProtobufMessageType(const ::google::protobuf::Descriptor* descriptor);
+
+//! Reflects ::google::protobuf::EnumDescriptor.
+/*!
+ * The call invokes the internal reflection registry and takes spinlocks.
+ * Should not be assumed to be efficient.
+ */
+const TProtobufEnumType* ReflectProtobufEnumType(const ::google::protobuf::EnumDescriptor* descriptor);
+
+//! Extracts the underlying ::google::protobuf::Descriptor from a reflected instance.
+const ::google::protobuf::Descriptor* UnreflectProtobufMessageType(const TProtobufMessageType* type);
+
+//! Extracts the underlying ::google::protobuf::EnumDescriptor from a reflected instance.
+const ::google::protobuf::EnumDescriptor* UnreflectProtobufMessageType(const TProtobufEnumType* type);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufMessageElement;
+struct TProtobufScalarElement;
+struct TProtobufAttributeDictionaryElement;
+struct TProtobufRepeatedElement;
+struct TProtobufMapElement;
+struct TProtobufAnyElement;
+
+using TProtobufElement = std::variant<
+ std::unique_ptr<TProtobufMessageElement>,
+ std::unique_ptr<TProtobufScalarElement>,
+ std::unique_ptr<TProtobufAttributeDictionaryElement>,
+ std::unique_ptr<TProtobufRepeatedElement>,
+ std::unique_ptr<TProtobufMapElement>,
+ std::unique_ptr<TProtobufAnyElement>
+>;
+
+struct TProtobufMessageElement
+{
+ const TProtobufMessageType* Type;
+};
+
+struct TProtobufScalarElement
+{
+};
+
+struct TProtobufAttributeDictionaryElement
+{
+ // The actual message type containing attribute_dictionary extension.
+ const TProtobufMessageType* Type;
+};
+
+struct TProtobufRepeatedElement
+{
+ TProtobufElement Element;
+};
+
+struct TProtobufMapElement
+{
+ TProtobufElement Element;
+};
+
+struct TProtobufAnyElement
+{
+};
+
+struct TProtobufElementResolveResult
+{
+ TProtobufElement Element;
+ TStringBuf HeadPath;
+ TStringBuf TailPath;
+};
+
+struct TResolveProtobufElementByYPathOptions
+{
+ bool AllowUnknownYsonFields = false;
+};
+
+//! Introspects a given #rootType and locates an element (represented
+//! by TProtobufElement discriminated union) at a given #path.
+//! Throws if some definite error occurs during resolve (i.e. a malformed
+//! YPath or a reference to a non-existing field).
+TProtobufElementResolveResult ResolveProtobufElementByYPath(
+ const TProtobufMessageType* rootType,
+ const NYPath::TYPathBuf path,
+ const TResolveProtobufElementByYPathOptions& options = {});
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr int UnknownYsonFieldNumber = 3005;
+
+
+//! Creates a YSON consumer that converts IYsonConsumer calls into
+//! a byte sequence in protobuf wire format.
+/*!
+ * The resulting sequence of bytes is actually fed into the output stream
+ * only at the very end since constructing it involves an additional pass
+ * to compute lengths of nested submessages.
+ */
+std::unique_ptr<IYsonConsumer> CreateProtobufWriter(
+ ::google::protobuf::io::ZeroCopyOutputStream* outputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufWriterOptions& options = TProtobufWriterOptions());
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufParserOptions
+{
+ //! If |true| then fields with numbers not found in protobuf metadata are
+ //! silently skipped; otherwise an exception is thrown.
+ bool SkipUnknownFields = false;
+
+ //! If |true| then required fields not found in protobuf metadata are
+ //! silently skipped; otherwise an exception is thrown.
+ bool SkipRequiredFields = false;
+};
+
+//! Parses a byte sequence and translates it into IYsonConsumer calls.
+/*!
+ * IMPORTANT! Due to performance reasons the implementation currently assumes
+ * that the byte sequence obeys the following additional condition (not enforced
+ * by protobuf wire format as it is): for each repeated field, its occurrences
+ * are sequential. This property is always true for byte sequences produced
+ * from message classes.
+ *
+ * In case you need to handle generic protobuf sequences, you should extend the
+ * code appropriately and provide a fallback flag (since zero-overhead support
+ * does not seem possible).
+ */
+void ParseProtobuf(
+ IYsonConsumer* consumer,
+ ::google::protobuf::io::ZeroCopyInputStream* inputStream,
+ const TProtobufMessageType* rootType,
+ const TProtobufParserOptions& options = TProtobufParserOptions());
+
+//! Invokes #ParseProtobuf to write #message into #consumer.
+void WriteProtobufMessage(
+ IYsonConsumer* consumer,
+ const ::google::protobuf::Message& message,
+ const TProtobufParserOptions& options = TProtobufParserOptions());
+
+
+//! Given a enum type T, tries to convert a string literal to T.
+//! Returns null if the literal is not known.
+template <class T>
+std::optional<T> FindProtobufEnumValueByLiteral(
+ const TProtobufEnumType* type,
+ TStringBuf literal);
+
+//! Given a enum type T, tries to convert a value of T to string literals.
+//! Returns null if no literal is known for this value.
+template <class T>
+TStringBuf FindProtobufEnumLiteralByValue(
+ const TProtobufEnumType* type,
+ T value);
+
+//! Converts a string or integral #node to enum underlying value type T.
+//! Throws if #node is not of string or integral type or if #node corresponds to an unknown enum value.
+template <class T>
+T ConvertToProtobufEnumValue(
+ const TProtobufEnumType* type,
+ const NYTree::INodePtr& node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! This method is assumed to be called during static initialization only.
+//! We defer running actions until static protobuf descriptors are ready.
+//! Accessing type descriptors during static initialization phase may break
+//! descriptors (at least under darwin).
+void AddProtobufConverterRegisterAction(std::function<void()> action);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufMessageConverter
+{
+ std::function<void(IYsonConsumer* consumer, const google::protobuf::Message* message)> Serializer;
+ std::function<void(google::protobuf::Message* message, const NYTree::INodePtr& node)> Deserializer;
+};
+
+//! This method is called during static initialization and not assumed to be called during runtime.
+void RegisterCustomProtobufConverter(
+ const google::protobuf::Descriptor* descriptor,
+ const TProtobufMessageConverter& converter);
+
+#define REGISTER_INTERMEDIATE_PROTO_INTEROP_REPRESENTATION(ProtoType, Type) \
+ YT_ATTRIBUTE_USED static const void* PP_ANONYMOUS_VARIABLE(RegisterIntermediateProtoInteropRepresentation) = [] { \
+ NYT::NYson::AddProtobufConverterRegisterAction([] { \
+ auto* descriptor = ProtoType::default_instance().GetDescriptor(); \
+ NYT::NYson::TProtobufMessageConverter converter; \
+ converter.Serializer = [] (NYT::NYson::IYsonConsumer* consumer, const google::protobuf::Message* message) { \
+ const auto* typedMessage = dynamic_cast<const ProtoType*>(message); \
+ YT_VERIFY(typedMessage); \
+ Type value; \
+ FromProto(&value, *typedMessage); \
+ Serialize(value, consumer); \
+ }; \
+ converter.Deserializer = [] (google::protobuf::Message* message, const NYT::NYTree::INodePtr& node) { \
+ auto* typedMessage = dynamic_cast<ProtoType*>(message); \
+ YT_VERIFY(typedMessage); \
+ Type value; \
+ Deserialize(value, node); \
+ ToProto(typedMessage, value); \
+ }; \
+ NYT::NYson::RegisterCustomProtobufConverter(descriptor, converter); \
+ }); \
+ return nullptr; \
+ } ();
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufMessageBytesFieldConverter
+{
+ std::function<void(IYsonConsumer* consumer, TStringBuf bytes)> Serializer;
+ std::function<void(TString* bytes, const NYTree::INodePtr& node)> Deserializer;
+};
+
+//! This method is called during static initialization and not assumed to be called during runtime.
+void RegisterCustomProtobufBytesFieldConverter(
+ const google::protobuf::Descriptor* descriptor,
+ int fieldIndex,
+ const TProtobufMessageBytesFieldConverter& converter);
+
+#define REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(ProtoType, FieldNumber, Type) \
+ static const void* PP_ANONYMOUS_VARIABLE(RegisterIntermediateProtoInterpBytesFieldRepresentation) = [] { \
+ NYT::NYson::AddProtobufConverterRegisterAction([] { \
+ const auto* descriptor = ProtoType::default_instance().GetDescriptor(); \
+ NYT::NYson::TProtobufMessageBytesFieldConverter converter; \
+ converter.Serializer = [] (NYT::NYson::IYsonConsumer* consumer, TStringBuf bytes) { \
+ Type value; \
+ FromBytes(&value, bytes); \
+ Serialize(value, consumer); \
+ }; \
+ converter.Deserializer = [] (TString* bytes, const NYT::NYTree::INodePtr& node) { \
+ Type value; \
+ Deserialize(value, node); \
+ ToBytes(bytes, value); \
+ }; \
+ NYT::NYson::RegisterCustomProtobufBytesFieldConverter(descriptor, FieldNumber, converter); \
+ }); \
+ return nullptr; \
+ } ();
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString YsonStringToProto(
+ const TYsonString& ysonString,
+ const TProtobufMessageType* payloadType,
+ EUnknownYsonFieldsMode unknownFieldsMode);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define PROTOBUF_INTEROP_INL_H_
+#include "protobuf_interop-inl.h"
+#undef PROTOBUF_INTEROP_INL_H_
diff --git a/yt/yt/core/yson/protobuf_interop_options.cpp b/yt/yt/core/yson/protobuf_interop_options.cpp
new file mode 100644
index 0000000000..839b010861
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop_options.cpp
@@ -0,0 +1,16 @@
+#include "protobuf_interop_options.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TProtobufWriterOptions::TUnknownYsonFieldModeResolver TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode mode)
+{
+ return [mode] (const NYPath::TYPath& /*path*/) {
+ return mode;
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/protobuf_interop_options.h b/yt/yt/core/yson/protobuf_interop_options.h
new file mode 100644
index 0000000000..34274c1429
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop_options.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ypath/public.h>
+#include <yt/yt/core/yson/public.h>
+
+#include <functional>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TProtobufWriterOptions
+{
+ //! Keep: all unknown fields found during YSON parsing
+ //! are translated into Protobuf unknown fields (each has number UnknownYsonFieldNumber
+ //! and is a key-value pair with field name being its key and YSON being the value).
+ //!
+ //! Skip: all unknown fields are silently skipped.
+ //!
+ //! Fail: an exception is thrown whenever an unknown field is found.
+ //!
+ //! Forward: current key/index is kept, the children are considered by resolver recursively.
+ //! Forward in a scalar leaf is interpreted as a Fail.
+ using TUnknownYsonFieldModeResolver = std::function<EUnknownYsonFieldsMode(const NYPath::TYPath&)>;
+
+ static TUnknownYsonFieldModeResolver CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode mode);
+
+ TUnknownYsonFieldModeResolver UnknownYsonFieldModeResolver = CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Fail);
+ //! If |true| then required fields not found in protobuf metadata are
+ //! silently skipped; otherwise an exception is thrown.
+ bool SkipRequiredFields = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp b/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
new file mode 100644
index 0000000000..422e84a818
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop_unknown_fields.cpp
@@ -0,0 +1,181 @@
+#include "protobuf_interop_unknown_fields.h"
+#include "null_consumer.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TForwardingUnknownYsonFieldValueWriter::TForwardingUnknownYsonFieldValueWriter(
+ TBufferedBinaryYsonWriter& ysonWriter,
+ const TProtobufWriterOptions::TUnknownYsonFieldModeResolver& modeResolver)
+ : ModeResolver_(modeResolver)
+ , YsonWriter_(ysonWriter)
+{
+ YT_VERIFY(ModeResolver_);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyStringScalar(TStringBuf value)
+{
+ ValidateLeaf();
+ YsonWriter_.OnStringScalar(value);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyInt64Scalar(i64 value)
+{
+ ValidateLeaf();
+ YsonWriter_.OnInt64Scalar(value);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyUint64Scalar(ui64 value)
+{
+ ValidateLeaf();
+ YsonWriter_.OnUint64Scalar(value);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyDoubleScalar(double value)
+{
+ ValidateLeaf();
+ YsonWriter_.OnDoubleScalar(value);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyBooleanScalar(bool value)
+{
+ ValidateLeaf();
+ YsonWriter_.OnBooleanScalar(value);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyEntity()
+{
+ ValidateLeaf();
+ YsonWriter_.OnEntity();
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyBeginList()
+{
+ YsonWriter_.OnBeginList();
+ YPathStack_.Push(-1);
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyListItem()
+{
+ YPathStack_.IncreaseLastIndex();
+ LastMode_ = ModeResolver_(YPathStack_.GetPath());
+ switch (LastMode_) {
+ case EUnknownYsonFieldsMode::Skip: {
+ Forward(GetNullYsonConsumer(), [] {});
+ return;
+ }
+ case EUnknownYsonFieldsMode::Keep: {
+ YsonWriter_.OnListItem();
+ Forward(&YsonWriter_, [] {});
+ return;
+ }
+ case EUnknownYsonFieldsMode::Forward: {
+ YsonWriter_.OnListItem();
+ break;
+ }
+ default: {
+ ValidateNode();
+ }
+ }
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyEndList()
+{
+ YPathStack_.Pop();
+ YsonWriter_.OnEndList();
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyBeginMap()
+{
+ YsonWriter_.OnBeginMap();
+ // Needed for invariant that previous map key is popped in OnMyKeyedItem.
+ YPathStack_.Push("");
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyKeyedItem(TStringBuf key)
+{
+ YPathStack_.Pop();
+ YPathStack_.Push(TString{key});
+ LastMode_ = ModeResolver_(YPathStack_.GetPath());
+ switch (LastMode_) {
+ case EUnknownYsonFieldsMode::Skip: {
+ Forward(GetNullYsonConsumer(), [] {});
+ return;
+ }
+ case EUnknownYsonFieldsMode::Keep: {
+ YsonWriter_.OnKeyedItem(key);
+ Forward(&YsonWriter_, [] {});
+ return;
+ }
+ case EUnknownYsonFieldsMode::Forward: {
+ YsonWriter_.OnKeyedItem(key);
+ break;
+ }
+ case EUnknownYsonFieldsMode::Fail: {
+ ThrowUnknownField();
+ break;
+ }
+ default: {
+ YT_ABORT();
+ }
+ }
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyEndMap()
+{
+ YPathStack_.Pop();
+ YsonWriter_.OnEndMap();
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyBeginAttributes()
+{
+ YsonWriter_.OnBeginAttributes();
+ Forward(&YsonWriter_, [] {});
+}
+
+void TForwardingUnknownYsonFieldValueWriter::OnMyEndAttributes()
+{
+ YsonWriter_.OnEndAttributes();
+}
+
+void TForwardingUnknownYsonFieldValueWriter::ValidateLeaf()
+{
+ if (LastMode_ != EUnknownYsonFieldsMode::Keep) {
+ ThrowUnknownField();
+ }
+}
+
+void TForwardingUnknownYsonFieldValueWriter::ValidateNode()
+{
+ if (LastMode_ == EUnknownYsonFieldsMode::Fail) {
+ ThrowUnknownField();
+ }
+}
+
+void TForwardingUnknownYsonFieldValueWriter::Flush()
+{
+ YsonWriter_.Flush();
+}
+
+void TForwardingUnknownYsonFieldValueWriter::ResetMode()
+{
+ LastMode_ = EUnknownYsonFieldsMode::Forward;
+}
+
+void TForwardingUnknownYsonFieldValueWriter::ThrowUnknownField()
+{
+ auto lastElement = YPathStack_.TryGetStringifiedLastPathToken().value_or(TString{});
+ auto path = YPathStack_.GetPath();
+ YPathStack_.Pop();
+ THROW_ERROR_EXCEPTION("Unknown field %Qv at %v",
+ lastElement,
+ YPathStack_.GetHumanReadablePath())
+ << TErrorAttribute("ypath", path);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/protobuf_interop_unknown_fields.h b/yt/yt/core/yson/protobuf_interop_unknown_fields.h
new file mode 100644
index 0000000000..4f64cc6777
--- /dev/null
+++ b/yt/yt/core/yson/protobuf_interop_unknown_fields.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "forwarding_consumer.h"
+#include "protobuf_interop_options.h"
+#include "writer.h"
+
+#include <yt/yt/core/ypath/public.h>
+#include <yt/yt/core/ypath/stack.h>
+
+#include <library/cpp/yt/misc/property.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TForwardingUnknownYsonFieldValueWriter
+ : public TForwardingYsonConsumer
+{
+public:
+ TForwardingUnknownYsonFieldValueWriter(
+ TBufferedBinaryYsonWriter& ysonWriter,
+ const TProtobufWriterOptions::TUnknownYsonFieldModeResolver& modeResolver);
+
+ DEFINE_BYREF_RW_PROPERTY(NYPath::TYPathStack, YPathStack);
+
+ void Flush();
+ void ResetMode();
+
+private:
+ const TProtobufWriterOptions::TUnknownYsonFieldModeResolver& ModeResolver_;
+
+ TBufferedBinaryYsonWriter& YsonWriter_;
+
+ EUnknownYsonFieldsMode LastMode_ = EUnknownYsonFieldsMode::Forward;
+
+ void OnMyStringScalar(TStringBuf value) override;
+ void OnMyInt64Scalar(i64 value) override;
+ void OnMyUint64Scalar(ui64 value) override;
+ void OnMyDoubleScalar(double value) override;
+ void OnMyBooleanScalar(bool value) override;
+ void OnMyEntity() override;
+ void OnMyBeginList() override;
+ void OnMyListItem() override;
+ void OnMyEndList() override;
+ void OnMyBeginMap() override;
+ void OnMyKeyedItem(TStringBuf key) override;
+ void OnMyEndMap() override;
+ void OnMyBeginAttributes() override;
+ void OnMyEndAttributes() override;
+
+ void ValidateLeaf();
+ void ValidateNode();
+
+ void ThrowUnknownField();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/public.h b/yt/yt/core/yson/public.h
new file mode 100644
index 0000000000..a983970279
--- /dev/null
+++ b/yt/yt/core/yson/public.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <library/cpp/yt/yson/public.h>
+#include <library/cpp/yt/yson_string/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class ETokenType;
+
+class TYsonProducer;
+template <class... TAdditionalArgs>
+class TExtendedYsonProducer;
+
+class TYsonInput;
+class TYsonOutput;
+
+class TUncheckedYsonTokenWriter;
+class TCheckedYsonTokenWriter;
+
+#ifdef NDEBUG
+using TCheckedInDebugYsonTokenWriter = TUncheckedYsonTokenWriter;
+#else
+using TCheckedInDebugYsonTokenWriter = TCheckedYsonTokenWriter;
+#endif
+
+class TTokenizer;
+
+class TProtobufMessageType;
+
+struct IFlushableYsonConsumer;
+struct IAsyncYsonConsumer;
+
+enum class EYsonItemType : ui8;
+class TYsonItem;
+class TYsonPullParser;
+class TYsonPullParserCursor;
+
+class TForwardingYsonConsumer;
+
+DEFINE_ENUM(EUnknownYsonFieldsMode,
+ (Skip)
+ (Fail)
+ (Keep)
+ (Forward)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//
+// We need two limits for YSON parsing:
+// - the smaller (CypressWriteNestingLevelLimit) is used for commands like set or create.
+// - the larger (NewNestingLevelLimit) is used elsewhere.
+// Thus we try to avoid the problem when we cannot read
+// a value that was written successfully, i.e. get("//a/@") after set("//a/@x", <deep yson>).
+// See YT-15698.
+constexpr int OriginalNestingLevelLimit = 64;
+constexpr int CypressWriteNestingLevelLimit = 128;
+constexpr int NewNestingLevelLimit = 256;
+
+constexpr int DefaultYsonParserNestingLevelLimit = NewNestingLevelLimit;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/pull_parser-inl.h b/yt/yt/core/yson/pull_parser-inl.h
new file mode 100644
index 0000000000..3d11107455
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser-inl.h
@@ -0,0 +1,956 @@
+#ifndef PULL_PARSER_INL_H_
+#error "Direct inclusion of this file is not allowed, include pull_parser.h"
+// For the sake of sane code completion.
+#include "pull_parser.h"
+#endif
+
+#include "detail.h"
+
+#include <yt/yt/core/misc/parser_helpers.h>
+
+#include <optional>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonItem::TYsonItem(const TYsonItem& other)
+{
+ memcpy(this, &other, sizeof(*this));
+}
+
+TYsonItem& TYsonItem::operator =(const TYsonItem& other)
+{
+ memcpy(this, &other, sizeof(*this));
+ return *this;
+}
+
+TYsonItem TYsonItem::Simple(EYsonItemType type)
+{
+ TYsonItem result;
+ result.Type_ = type;
+ return result;
+}
+
+TYsonItem TYsonItem::Boolean(bool data)
+{
+ TYsonItem result;
+ result.Type_ = EYsonItemType::BooleanValue;
+ result.Data_.Boolean = data;
+ return result;
+}
+
+TYsonItem TYsonItem::Int64(i64 data)
+{
+ TYsonItem result;
+ result.Type_ = EYsonItemType::Int64Value;
+ result.Data_.Int64 = data;
+ return result;
+}
+
+TYsonItem TYsonItem::Uint64(ui64 data)
+{
+ TYsonItem result;
+ result.Type_ = EYsonItemType::Uint64Value;
+ result.Data_.Uint64 = data;
+ return result;
+}
+
+TYsonItem TYsonItem::Double(double data)
+{
+ TYsonItem result;
+ result.Type_ = EYsonItemType::DoubleValue;
+ result.Data_.Double = data;
+ return result;
+}
+
+TYsonItem TYsonItem::String(TStringBuf data)
+{
+ TYsonItem result;
+ result.Type_ = EYsonItemType::StringValue;
+ result.Data_.String.Ptr = data.data();
+ result.Data_.String.Size = data.length();
+ return result;
+}
+
+EYsonItemType TYsonItem::GetType() const
+{
+ return Type_;
+}
+
+bool TYsonItem::UncheckedAsBoolean() const
+{
+ YT_ASSERT(GetType() == EYsonItemType::BooleanValue);
+ return Data_.Boolean;
+}
+
+i64 TYsonItem::UncheckedAsInt64() const
+{
+ YT_ASSERT(GetType() == EYsonItemType::Int64Value);
+ return Data_.Int64;
+}
+
+ui64 TYsonItem::UncheckedAsUint64() const
+{
+ YT_ASSERT(GetType() == EYsonItemType::Uint64Value);
+ return Data_.Uint64;
+}
+
+double TYsonItem::UncheckedAsDouble() const
+{
+ YT_ASSERT(GetType() == EYsonItemType::DoubleValue);
+ return Data_.Double;
+}
+
+TStringBuf TYsonItem::UncheckedAsString() const
+{
+ YT_ASSERT(GetType() == EYsonItemType::StringValue);
+ return TStringBuf(Data_.String.Ptr, Data_.String.Size);
+}
+
+template <typename T>
+T TYsonItem::UncheckedAs() const
+{
+ if constexpr (std::is_same_v<T, i64>) {
+ return UncheckedAsInt64();
+ } else if constexpr (std::is_same_v<T, ui64>) {
+ return UncheckedAsUint64();
+ } else if constexpr (std::is_same_v<T, double>) {
+ return UncheckedAsDouble();
+ } else if constexpr (std::is_same_v<T, TStringBuf>) {
+ return UncheckedAsString();
+ } else if constexpr (std::is_same_v<T, bool>) {
+ return UncheckedAsBoolean();
+ } else {
+ static_assert(TDependentFalse<T>);
+ }
+}
+
+bool TYsonItem::IsEndOfStream() const
+{
+ return GetType() == EYsonItemType::EndOfStream;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void NDetail::TZeroCopyInputStreamReader::RefreshBlock()
+{
+ if (RecordingFrom_) {
+ RecordOutput_->Write(RecordingFrom_, Current_ - RecordingFrom_);
+ }
+ TotalReadBlocksSize_ += (Current_ - Begin_);
+ size_t size = Reader_->Next(&Begin_);
+ Current_ = Begin_;
+ End_ = Begin_ + size;
+ if (RecordOutput_) {
+ RecordingFrom_ = Begin_;
+ }
+ if (size == 0) {
+ Finished_ = true;
+ }
+}
+
+const char* NDetail::TZeroCopyInputStreamReader::Begin() const
+{
+ return Begin_;
+}
+
+const char* NDetail::TZeroCopyInputStreamReader::Current() const
+{
+ return Current_;
+}
+
+const char* NDetail::TZeroCopyInputStreamReader::End() const
+{
+ return End_;
+}
+
+void NDetail::TZeroCopyInputStreamReader::Advance(size_t bytes)
+{
+ Current_ += bytes;
+}
+
+bool NDetail::TZeroCopyInputStreamReader::IsFinished() const
+{
+ return Finished_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+constexpr char ItemTypeToMarker(EYsonItemType itemType)
+{
+ switch (itemType) {
+ case EYsonItemType::Uint64Value:
+ return NDetail::Uint64Marker;
+ case EYsonItemType::Int64Value:
+ return NDetail::Int64Marker;
+ case EYsonItemType::DoubleValue:
+ return NDetail::DoubleMarker;
+ case EYsonItemType::StringValue:
+ return NDetail::StringMarker;
+ case EYsonItemType::BeginList:
+ return NDetail::BeginListSymbol;
+ case EYsonItemType::EndList:
+ return NDetail::EndListSymbol;
+ case EYsonItemType::EntityValue:
+ return NDetail::EntitySymbol;
+ default:
+ THROW_ERROR_EXCEPTION("Cannot convert item type %Qlv to marker",
+ itemType);
+ }
+}
+
+void TYsonPullParser::MaybeSkipSemicolon()
+{
+ auto c = Lexer_.GetChar<true>();
+ if (c == ';') {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSeparator();
+ }
+}
+
+template <EYsonItemType ItemType, bool IsOptional>
+auto TYsonPullParser::ParseItem() -> std::conditional_t<IsOptional, bool, void>
+{
+ static constexpr auto Marker = ItemTypeToMarker(ItemType);
+
+ auto parse = [this] {
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == Marker) {
+ Lexer_.Advance(1);
+ if constexpr (ItemType == EYsonItemType::BeginList) {
+ SyntaxChecker_.OnBeginList();
+ } else if constexpr (ItemType == EYsonItemType::EndList) {
+ SyntaxChecker_.OnEndList();
+ } else if constexpr (ItemType == EYsonItemType::EntityValue) {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ } else {
+ static_assert(ItemType == EYsonItemType::BeginList);
+ }
+ return true;
+ }
+
+ if constexpr (IsOptional) {
+ if (c == NDetail::EntitySymbol) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ return false;
+ }
+ }
+
+ // Slow path.
+ auto item = Next();
+ if (item.GetType() == ItemType) {
+ return true;
+ }
+
+ if constexpr (IsOptional) {
+ if (item.GetType() == EYsonItemType::EntityValue) {
+ return false;
+ }
+ }
+
+ ThrowUnexpectedTokenException("item", *this, item, ItemType, /* isOptional */ true);
+ };
+
+ auto result = parse();
+ if constexpr (IsOptional) {
+ return result;
+ }
+}
+
+void TYsonPullParser::ParseBeginList()
+{
+ ParseItem<EYsonItemType::BeginList, false>();
+}
+
+bool TYsonPullParser::ParseOptionalBeginList()
+{
+ return ParseItem<EYsonItemType::BeginList, true>();
+}
+
+bool TYsonPullParser::IsMarker(char marker)
+{
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == marker) {
+ return true;
+ } else {
+ while (c == ';' || IsSpace(c)) {
+ Lexer_.Advance(1);
+ if (c == ';') {
+ SyntaxChecker_.OnSeparator();
+ }
+ c = Lexer_.GetChar<false>();
+ }
+ return c == marker;
+ }
+}
+
+bool TYsonPullParser::IsEndList()
+{
+ return IsMarker(NDetail::EndListSymbol);
+}
+
+bool TYsonPullParser::IsEntity()
+{
+ return IsMarker(NDetail::EntitySymbol);
+}
+
+void TYsonPullParser::ParseEndList()
+{
+ ParseItem<EYsonItemType::EndList, false>();
+}
+
+void TYsonPullParser::ParseEntity()
+{
+ ParseItem<EYsonItemType::EntityValue, false>();
+}
+
+template <typename TValue, EYsonItemType ItemType>
+TValue TYsonPullParser::ParseTypedValueFallback()
+{
+ using TNonOptionalValue = typename TOptionalTraits<TValue>::TValue;
+ constexpr auto IsOptional = !std::is_same_v<typename TOptionalTraits<TValue>::TValue, TValue>;
+
+ auto item = Next();
+ if (item.GetType() == ItemType) {
+ return item.UncheckedAs<TNonOptionalValue>();
+ }
+
+ if constexpr (IsOptional) {
+ if (item.GetType() == EYsonItemType::EntityValue) {
+ return std::nullopt;
+ }
+ }
+
+ ThrowUnexpectedTokenException("value", *this, item, ItemType, IsOptional);
+}
+
+template <typename TValue, EYsonItemType ItemType>
+TValue TYsonPullParser::ParseTypedValue()
+{
+ static constexpr auto Marker = ItemTypeToMarker(ItemType);
+ using TNonOptionalValue = typename TOptionalTraits<TValue>::TValue;
+ constexpr auto IsOptional = !std::is_same_v<typename TOptionalTraits<TValue>::TValue, TValue>;
+
+ auto readBinaryValue = [&] (TLexer& lexer) {
+ if constexpr (std::is_same_v<TNonOptionalValue, i64>) {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Int64Value);
+ return lexer.ReadBinaryInt64();
+ } else if constexpr (std::is_same_v<TNonOptionalValue, ui64>) {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Uint64Value);
+ return lexer.ReadBinaryUint64();
+ } else if constexpr (std::is_same_v<TNonOptionalValue, double>) {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ return lexer.ReadBinaryDouble();
+ } else if constexpr (std::is_same_v<TNonOptionalValue, TStringBuf>) {
+ SyntaxChecker_.OnString();
+ return lexer.ReadBinaryString();
+ } else {
+ static_assert(TDependentFalse<TNonOptionalValue>);
+ }
+ };
+
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == Marker) {
+ Lexer_.Advance(1);
+ auto value = readBinaryValue(Lexer_);
+ return value;
+ }
+
+ if constexpr (IsOptional) {
+ if (c == NDetail::EntitySymbol) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ return std::nullopt;
+ }
+ }
+
+ // Slow path.
+ return ParseTypedValueFallback<TValue, ItemType>();
+}
+
+template <typename TValue, EYsonItemType ItemType>
+int TYsonPullParser::ParseVarintToArray(char* out)
+{
+ static constexpr auto Marker = ItemTypeToMarker(ItemType);
+ using TNonOptionalValue = typename TOptionalTraits<TValue>::TValue;
+ constexpr auto IsOptional = !std::is_same_v<typename TOptionalTraits<TValue>::TValue, TValue>;
+
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == Marker) {
+ Lexer_.Advance(1);
+ auto bytesWritten = Lexer_.ReadVarint64ToArray(out);
+ SyntaxChecker_.OnSimpleNonstring(ItemType);
+ return bytesWritten;
+ }
+
+ if constexpr (IsOptional) {
+ if (c == NDetail::EntitySymbol) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ return 0;
+ }
+ }
+
+ // Slow path.
+ auto value = ParseTypedValueFallback<TValue, ItemType>();
+ TNonOptionalValue nonOptionalValue;
+ if constexpr (IsOptional) {
+ if (!value) {
+ return 0;
+ }
+ nonOptionalValue = *value;
+ } else {
+ nonOptionalValue = value;
+ }
+ if constexpr (std::is_same_v<TNonOptionalValue, i64>) {
+ return WriteVarInt64(out, nonOptionalValue);
+ } else {
+ static_assert(std::is_same_v<TNonOptionalValue, ui64>);
+ return WriteVarUint64(out, nonOptionalValue);
+ }
+}
+
+ui64 TYsonPullParser::ParseUint64()
+{
+ return ParseTypedValue<ui64, EYsonItemType::Uint64Value>();
+}
+
+std::optional<ui64> TYsonPullParser::ParseOptionalUint64()
+{
+ return ParseTypedValue<std::optional<ui64>, EYsonItemType::Uint64Value>();
+}
+
+int TYsonPullParser::ParseUint64AsVarint(char* out)
+{
+ return ParseVarintToArray<ui64, EYsonItemType::Uint64Value>(out);
+}
+
+int TYsonPullParser::ParseOptionalUint64AsVarint(char* out)
+{
+ return ParseVarintToArray<std::optional<ui64>, EYsonItemType::Uint64Value>(out);
+}
+
+i64 TYsonPullParser::ParseInt64()
+{
+ return ParseTypedValue<i64, EYsonItemType::Int64Value>();
+}
+
+std::optional<i64> TYsonPullParser::ParseOptionalInt64()
+{
+ return ParseTypedValue<std::optional<i64>, EYsonItemType::Int64Value>();
+}
+
+int TYsonPullParser::ParseInt64AsZigzagVarint(char* out)
+{
+ return ParseVarintToArray<i64, EYsonItemType::Int64Value>(out);
+}
+
+int TYsonPullParser::ParseOptionalInt64AsZigzagVarint(char* out)
+{
+ return ParseVarintToArray<std::optional<i64>, EYsonItemType::Int64Value>(out);
+}
+
+double TYsonPullParser::ParseDouble()
+{
+ return ParseTypedValue<double, EYsonItemType::DoubleValue>();
+}
+
+std::optional<double> TYsonPullParser::ParseOptionalDouble()
+{
+ return ParseTypedValue<std::optional<double>, EYsonItemType::DoubleValue>();
+}
+
+TStringBuf TYsonPullParser::ParseString()
+{
+ return ParseTypedValue<TStringBuf, EYsonItemType::StringValue>();
+}
+
+std::optional<TStringBuf> TYsonPullParser::ParseOptionalString()
+{
+ return ParseTypedValue<std::optional<TStringBuf>, EYsonItemType::StringValue>();
+}
+
+bool TYsonPullParser::ParseBoolean()
+{
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == NDetail::TrueMarker || c == NDetail::FalseMarker) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ return c == NDetail::TrueMarker;
+ }
+ return ParseTypedValueFallback<bool, EYsonItemType::BooleanValue>();
+}
+
+std::optional<bool> TYsonPullParser::ParseOptionalBoolean()
+{
+ MaybeSkipSemicolon();
+
+ auto c = Lexer_.GetChar<false>();
+ if (c == NDetail::TrueMarker || c == NDetail::FalseMarker) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ return c == NDetail::TrueMarker;
+ } else if (c == NDetail::EntitySymbol) {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ return std::nullopt;
+ }
+ return ParseTypedValueFallback<std::optional<bool>, EYsonItemType::BooleanValue>();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TVisitor>
+typename TVisitor::TResult TYsonPullParser::NextImpl(TVisitor* visitor)
+{
+ using namespace NDetail;
+
+ while (true) {
+ char ch = Lexer_.GetChar<true>();
+ switch (ch) {
+ case BeginAttributesSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnAttributesBegin();
+ return visitor->OnBeginAttributes();
+ case EndAttributesSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnAttributesEnd();
+ return visitor->OnEndAttributes();
+ case BeginMapSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnBeginMap();
+ return visitor->OnBeginMap();
+ case EndMapSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnEndMap();
+ return visitor->OnEndMap();
+ case BeginListSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnBeginList();
+ return visitor->OnBeginList();
+ case EndListSymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnEndList();
+ return visitor->OnEndList();
+ case '"': {
+ Lexer_.Advance(1);
+ TStringBuf value = Lexer_.ReadQuotedString();
+ SyntaxChecker_.OnString();
+ return visitor->OnString(value);
+ }
+ case StringMarker: {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnString();
+ TStringBuf value = Lexer_.ReadBinaryString();
+ return visitor->OnString(value);
+ }
+ case Int64Marker: {
+ Lexer_.Advance(1);
+ i64 value = Lexer_.ReadBinaryInt64();
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Int64Value);
+ return visitor->OnInt64(value);
+ }
+ case Uint64Marker: {
+ Lexer_.Advance(1);
+ ui64 value = Lexer_.ReadBinaryUint64();
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Uint64Value);
+ return visitor->OnUint64(value);
+ }
+ case DoubleMarker: {
+ Lexer_.Advance(1);
+ double value = Lexer_.ReadBinaryDouble();
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ return visitor->OnDouble(value);
+ }
+ case FalseMarker: {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ return visitor->OnBoolean(false);
+ }
+ case TrueMarker: {
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ return visitor->OnBoolean(true);
+ }
+ case EntitySymbol:
+ Lexer_.Advance(1);
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ return visitor->OnEntity();
+ case EndSymbol:
+ SyntaxChecker_.OnFinish();
+ return visitor->OnEndOfStream();
+ case '%': {
+ Lexer_.Advance(1);
+ ch = Lexer_.template GetChar<false>();
+ if (ch == 't' || ch == 'f') {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ return visitor->OnBoolean(Lexer_.template ReadBoolean<false>());
+ } else {
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ return visitor->OnDouble(Lexer_.template ReadNanOrInf<false>());
+ }
+ }
+ case '=':
+ SyntaxChecker_.OnEquality();
+ Lexer_.Advance(1);
+ visitor->OnEquality();
+ continue;
+ case ';':
+ SyntaxChecker_.OnSeparator();
+ Lexer_.Advance(1);
+ visitor->OnSeparator();
+ continue;
+ default:
+ if (isspace(ch)) {
+ Lexer_.SkipSpaceAndGetChar<true>();
+ continue;
+ } else if (isdigit(ch) || ch == '-' || ch == '+') { // case of '+' is handled in AfterPlus state
+ TStringBuf valueBuffer;
+
+ ENumericResult numericResult = Lexer_.template ReadNumeric<true>(&valueBuffer);
+ if (numericResult == ENumericResult::Double) {
+ double value;
+ try {
+ value = FromString<double>(valueBuffer);
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Double, valueBuffer.begin(), valueBuffer.size())
+ << ex;
+ }
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ return visitor->OnDouble(value);
+ } else if (numericResult == ENumericResult::Int64) {
+ i64 value;
+ try {
+ value = FromString<i64>(valueBuffer);
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Int64, valueBuffer.begin(), valueBuffer.size())
+ << ex;
+ }
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Int64Value);
+ return visitor->OnInt64(value);
+ } else if (numericResult == ENumericResult::Uint64) {
+ ui64 value;
+ try {
+ value = FromString<ui64>(valueBuffer.SubStr(0, valueBuffer.size() - 1));
+ } catch (const std::exception& ex) {
+ THROW_ERROR CreateLiteralError(ETokenType::Uint64, valueBuffer.begin(), valueBuffer.size())
+ << ex;
+ }
+ SyntaxChecker_.OnSimpleNonstring(EYsonItemType::Uint64Value);
+ return visitor->OnUint64(value);
+ }
+ } else if (isalpha(ch) || ch == '_') {
+ TStringBuf value = Lexer_.template ReadUnquotedString<true>();
+ SyntaxChecker_.OnString();
+ return visitor->OnString(value);
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected %Qv while parsing node", ch);
+ }
+ }
+ }
+}
+
+template <typename TVisitor>
+void TYsonPullParser::TraverseComplexValueOrAttributes(TVisitor* visitor, bool stopAfterAttributes)
+{
+ class TForwardingVisitor
+ {
+ public:
+ using TResult = typename TVisitor::TResult;
+
+ public:
+ TForwardingVisitor(TVisitor* visitor)
+ : Underlying(visitor)
+ { }
+
+ TResult OnBeginAttributes()
+ {
+ IsAttributes = true;
+ IsComposite = true;
+ return Underlying->OnBeginAttributes();
+ }
+
+ TResult OnEndAttributes()
+ {
+ return Underlying->OnEndAttributes();
+ }
+
+ TResult OnBeginMap()
+ {
+ IsComposite = true;
+ return Underlying->OnBeginMap();
+ }
+
+ TResult OnEndMap()
+ {
+ return Underlying->OnEndMap();
+ }
+
+ TResult OnBeginList()
+ {
+ IsComposite = true;
+ return Underlying->OnBeginList();
+ }
+
+ TResult OnEndList()
+ {
+ return Underlying->OnEndList();
+ }
+
+ TResult OnString(TStringBuf value)
+ {
+ return Underlying->OnString(value);
+ }
+
+ TResult OnInt64(i64 value)
+ {
+ return Underlying->OnInt64(value);
+ }
+
+ TResult OnUint64(ui64 value)
+ {
+ return Underlying->OnUint64(value);
+ }
+
+ TResult OnDouble(double value)
+ {
+ return Underlying->OnDouble(value);
+ }
+
+ TResult OnBoolean(bool value)
+ {
+ return Underlying->OnBoolean(value);
+ }
+
+ TResult OnEntity()
+ {
+ return Underlying->OnEntity();
+ }
+
+ TResult OnEndOfStream()
+ {
+ return Underlying->OnEndOfStream();
+ }
+
+ TResult OnEquality()
+ {
+ return Underlying->OnEquality();
+ }
+
+ TResult OnSeparator()
+ {
+ return Underlying->OnSeparator();
+ }
+
+ public:
+ TVisitor* const Underlying;
+ bool IsAttributes = false;
+ bool IsComposite = false;
+ };
+
+ TForwardingVisitor forwardingVisitor(visitor);
+ NextImpl(&forwardingVisitor);
+
+ if (!forwardingVisitor.IsComposite) {
+ return;
+ }
+
+ const auto nestingLevel = GetNestingLevel();
+ while (GetNestingLevel() >= nestingLevel) {
+ NextImpl(visitor);
+ }
+
+ if (!stopAfterAttributes && forwardingVisitor.IsAttributes) {
+ TraverseComplexValueOrAttributes(visitor, stopAfterAttributes);
+ }
+}
+
+template <typename TVisitor>
+void TYsonPullParser::TraverseComplexValueOrAttributes(
+ TVisitor* visitor,
+ const TYsonItem& previousItem,
+ bool stopAfterAttributes)
+{
+ auto traverse = [&] {
+ const auto nestingLevel = GetNestingLevel();
+ while (GetNestingLevel() >= nestingLevel) {
+ NextImpl(visitor);
+ }
+ };
+
+ switch (previousItem.GetType()) {
+ case EYsonItemType::BeginAttributes:
+ visitor->OnBeginAttributes();
+ traverse();
+ if (!stopAfterAttributes) {
+ TraverseComplexValueOrAttributes(visitor, stopAfterAttributes);
+ }
+ return;
+ case EYsonItemType::BeginList:
+ visitor->OnBeginList();
+ traverse();
+ return;
+ case EYsonItemType::BeginMap:
+ visitor->OnBeginMap();
+ traverse();
+ return;
+
+ case EYsonItemType::EntityValue:
+ visitor->OnEntity();
+ return;
+ case EYsonItemType::BooleanValue:
+ visitor->OnBoolean(previousItem.UncheckedAsBoolean());
+ return;
+ case EYsonItemType::Int64Value:
+ visitor->OnInt64(previousItem.UncheckedAsInt64());
+ return;
+ case EYsonItemType::Uint64Value:
+ visitor->OnUint64(previousItem.UncheckedAsUint64());
+ return;
+ case EYsonItemType::DoubleValue:
+ visitor->OnDouble(previousItem.UncheckedAsDouble());
+ return;
+ case EYsonItemType::StringValue:
+ visitor->OnString(previousItem.UncheckedAsString());
+ return;
+
+ case EYsonItemType::EndOfStream:
+ case EYsonItemType::EndAttributes:
+ case EYsonItemType::EndMap:
+ case EYsonItemType::EndList:
+ YT_ABORT();
+ }
+ YT_ABORT();
+}
+
+size_t TYsonPullParser::GetNestingLevel() const
+{
+ return SyntaxChecker_.GetNestingLevel();
+}
+
+bool TYsonPullParser::IsOnValueBoundary(size_t nestingLevel) const
+{
+ return SyntaxChecker_.IsOnValueBoundary(nestingLevel);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonPullParserCursor::TYsonPullParserCursor(TYsonPullParser* parser)
+ : IsOnFragmentStart_(
+ parser->GetYsonType() != EYsonType::Node &&
+ parser->GetTotalReadSize() == 0)
+ , Current_(parser->Next())
+ , Parser_(parser)
+{ }
+
+const TYsonItem& TYsonPullParserCursor::GetCurrent() const
+{
+ return Current_;
+}
+
+const TYsonItem* TYsonPullParserCursor::operator->() const
+{
+ return &GetCurrent();
+}
+
+const TYsonItem& TYsonPullParserCursor::operator*() const
+{
+ return GetCurrent();
+}
+
+void TYsonPullParserCursor::Next()
+{
+ // NB(levysotsky): Hand-crafted YT_ASSERT to avoid
+ // stack frame bloating due to Y_FORCE_INLINE.
+#ifndef NDEBUG
+ if (IsOnFragmentStart_) {
+ FailAsTryConsumeFragmentStartNotCalled();
+ }
+#endif
+ Current_ = Parser_->Next();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <typename TFunction, EYsonItemType BeginItemType, EYsonItemType EndItemType>
+void ParseComposite(TYsonPullParserCursor* cursor, TFunction function)
+{
+ if constexpr (BeginItemType == EYsonItemType::BeginAttributes) {
+ EnsureYsonToken("attributes", *cursor, BeginItemType);
+ } else if constexpr (BeginItemType == EYsonItemType::BeginList) {
+ EnsureYsonToken("list", *cursor, BeginItemType);
+ } else if constexpr (BeginItemType == EYsonItemType::BeginMap) {
+ EnsureYsonToken("map", *cursor, BeginItemType);
+ } else {
+ static_assert(BeginItemType == EYsonItemType::BeginAttributes, "unexpected item type");
+ }
+
+ cursor->Next();
+ while ((*cursor)->GetType() != EndItemType) {
+ function(cursor);
+ }
+ cursor->Next();
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TFunction>
+void TYsonPullParserCursor::ParseMap(TFunction function)
+{
+ NDetail::ParseComposite<TFunction, EYsonItemType::BeginMap, EYsonItemType::EndMap>(this, function);
+}
+
+template <typename TFunction>
+void TYsonPullParserCursor::ParseList(TFunction function)
+{
+ NDetail::ParseComposite<TFunction, EYsonItemType::BeginList, EYsonItemType::EndList>(this, function);
+}
+
+template <typename TFunction>
+void TYsonPullParserCursor::ParseAttributes(TFunction function)
+{
+ NDetail::ParseComposite<TFunction, EYsonItemType::BeginAttributes, EYsonItemType::EndAttributes>(this, function);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ TStringBuf description,
+ const TYsonPullParserCursor& cursor,
+ EYsonItemType expected)
+{
+ if (expected != cursor->GetType()) {
+ ThrowUnexpectedYsonTokenException(description, cursor, {expected});
+ }
+}
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ EYsonItemType expected)
+{
+ if (expected != item.GetType()) {
+ ThrowUnexpectedYsonTokenException(description, parser, item, {expected});
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/pull_parser.cpp b/yt/yt/core/yson/pull_parser.cpp
new file mode 100644
index 0000000000..0b4fafb206
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser.cpp
@@ -0,0 +1,689 @@
+#include "pull_parser.h"
+#include "consumer.h"
+#include "token_writer.h"
+
+#include <util/stream/output.h>
+
+namespace NYT::NYson {
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TZeroCopyInputStreamReader::TZeroCopyInputStreamReader(IZeroCopyInput* reader)
+ : Reader_(reader)
+{ }
+
+ui64 TZeroCopyInputStreamReader::GetTotalReadSize() const
+{
+ return TotalReadBlocksSize_ + (Current_ - Begin_);
+}
+
+void NDetail::TZeroCopyInputStreamReader::StartRecording(IOutputStream* out)
+{
+ YT_VERIFY(!RecordOutput_);
+ RecordOutput_ = out;
+ RecordingFrom_ = Current_;
+}
+
+void NDetail::TZeroCopyInputStreamReader::CancelRecording()
+{
+ YT_VERIFY(RecordOutput_);
+ RecordOutput_ = nullptr;
+ RecordingFrom_ = nullptr;
+}
+
+void NDetail::TZeroCopyInputStreamReader::FinishRecording()
+{
+ YT_VERIFY(RecordOutput_);
+ if (RecordingFrom_) {
+ RecordOutput_->Write(RecordingFrom_, Current_ - RecordingFrom_);
+ }
+
+ RecordOutput_ = nullptr;
+ RecordingFrom_ = nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonItemCreatingVisitor
+{
+public:
+ using TResult = TYsonItem;
+
+public:
+ TYsonItem OnBeginAttributes()
+ {
+ return TYsonItem::Simple(EYsonItemType::BeginAttributes);
+ }
+
+ TYsonItem OnEndAttributes()
+ {
+ return TYsonItem::Simple(EYsonItemType::EndAttributes);
+ }
+
+ TYsonItem OnBeginMap()
+ {
+ return TYsonItem::Simple(EYsonItemType::BeginMap);
+ }
+
+ TYsonItem OnEndMap()
+ {
+ return TYsonItem::Simple(EYsonItemType::EndMap);
+ }
+
+ TYsonItem OnBeginList()
+ {
+ return TYsonItem::Simple(EYsonItemType::BeginList);
+ }
+
+ TYsonItem OnEndList()
+ {
+ return TYsonItem::Simple(EYsonItemType::EndList);
+ }
+
+ TYsonItem OnString(TStringBuf value)
+ {
+ return TYsonItem::String(value);
+ }
+
+ TYsonItem OnInt64(i64 value)
+ {
+ return TYsonItem::Int64(value);
+ }
+
+ TYsonItem OnUint64(ui64 value)
+ {
+ return TYsonItem::Uint64(value);
+ }
+
+ TYsonItem OnDouble(double value)
+ {
+ return TYsonItem::Double(value);
+ }
+
+ TYsonItem OnBoolean(bool value)
+ {
+ return TYsonItem::Boolean(value);
+ }
+
+ TYsonItem OnEntity()
+ {
+ return TYsonItem::Simple(EYsonItemType::EntityValue);
+ }
+
+ TYsonItem OnEndOfStream()
+ {
+ return TYsonItem::Simple(EYsonItemType::EndOfStream);
+ }
+
+ void OnEquality()
+ { }
+
+ void OnSeparator()
+ { }
+};
+
+class TNullVisitor
+{
+public:
+ using TResult = void;
+
+public:
+ void OnBeginAttributes()
+ { }
+
+ void OnEndAttributes()
+ { }
+
+ void OnBeginMap()
+ { }
+
+ void OnEndMap()
+ { }
+
+ void OnBeginList()
+ { }
+
+ void OnEndList()
+ { }
+
+ void OnString(TStringBuf /* value */)
+ { }
+
+ void OnInt64(i64 /* value */)
+ { }
+
+ void OnUint64(ui64 /* value */)
+ { }
+
+ void OnDouble(double /* value */)
+ { }
+
+ void OnBoolean(bool /* value */)
+ { }
+
+ void OnEntity()
+ { }
+
+ void OnEndOfStream()
+ { }
+
+ void OnEquality()
+ { }
+
+ void OnSeparator()
+ { }
+};
+
+class TYsonTokenWritingVisitor
+{
+public:
+ using TResult = void;
+
+public:
+ TYsonTokenWritingVisitor(TCheckedInDebugYsonTokenWriter* writer)
+ : Writer_(writer)
+ { }
+
+ void OnBeginAttributes()
+ {
+ Writer_->WriteBeginAttributes();
+ }
+
+ void OnEndAttributes()
+ {
+ Writer_->WriteEndAttributes();
+ }
+
+ void OnBeginMap()
+ {
+ Writer_->WriteBeginMap();
+ }
+
+ void OnEndMap()
+ {
+ Writer_->WriteEndMap();
+ }
+
+ void OnBeginList()
+ {
+ Writer_->WriteBeginList();
+ }
+
+ void OnEndList()
+ {
+ Writer_->WriteEndList();
+ }
+
+ void OnString(TStringBuf value)
+ {
+ Writer_->WriteBinaryString(value);
+ }
+
+ void OnInt64(i64 value)
+ {
+ Writer_->WriteBinaryInt64(value);
+ }
+
+ void OnUint64(ui64 value)
+ {
+ Writer_->WriteBinaryUint64(value);
+ }
+
+ void OnDouble(double value)
+ {
+ Writer_->WriteBinaryDouble(value);
+ }
+
+ void OnBoolean(bool value)
+ {
+ Writer_->WriteBinaryBoolean(value);
+ }
+
+ void OnEntity()
+ {
+ Writer_->WriteEntity();
+ }
+
+ void OnEndOfStream()
+ { }
+
+ void OnEquality()
+ {
+ Writer_->WriteKeyValueSeparator();
+ }
+
+ void OnSeparator()
+ {
+ Writer_->WriteItemSeparator();
+ }
+
+private:
+ TCheckedInDebugYsonTokenWriter* const Writer_;
+};
+
+class TYsonConsumerVisitor
+{
+public:
+ using TResult = void;
+
+public:
+ TYsonConsumerVisitor(const TYsonSyntaxChecker& syntaxChecker, IYsonConsumer* consumer)
+ : SyntaxChecker_(syntaxChecker)
+ , Consumer_(consumer)
+ { }
+
+ void OnBeginAttributes()
+ {
+ MaybeEmitOnListItem(-1);
+ Consumer_->OnBeginAttributes();
+ }
+
+ void OnEndAttributes()
+ {
+ Consumer_->OnEndAttributes();
+ }
+
+ void OnBeginMap()
+ {
+ MaybeEmitOnListItem(-1);
+ Consumer_->OnBeginMap();
+ }
+
+ void OnEndMap()
+ {
+ Consumer_->OnEndMap();
+ }
+
+ void OnBeginList()
+ {
+ MaybeEmitOnListItem(-1);
+ SetWaitingForListItem(true, 0);
+ Consumer_->OnBeginList();
+ }
+
+ void OnEndList()
+ {
+ SetWaitingForListItem(false, +1);
+ Consumer_->OnEndList();
+ }
+
+ void OnString(TStringBuf value)
+ {
+ if (SyntaxChecker_.IsOnKey()) {
+ Consumer_->OnKeyedItem(value);
+ } else {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnStringScalar(value);
+ }
+ }
+
+ void OnInt64(i64 value)
+ {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnInt64Scalar(value);
+ }
+
+ void OnUint64(ui64 value)
+ {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnUint64Scalar(value);
+ }
+
+ void OnDouble(double value)
+ {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnDoubleScalar(value);
+ }
+
+ void OnBoolean(bool value)
+ {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnBooleanScalar(value);
+ }
+
+ void OnEntity()
+ {
+ MaybeEmitOnListItem(0);
+ Consumer_->OnEntity();
+ }
+
+ void OnEndOfStream()
+ { }
+
+ void OnEquality()
+ { }
+
+ void OnSeparator()
+ {
+ if (SyntaxChecker_.IsListSeparator()) {
+ SetWaitingForListItem(true, 0);
+ }
+ }
+
+private:
+ void MaybeEmitOnListItem(int nestingLevelDelta)
+ {
+ auto wasWaiting = SetWaitingForListItem(false, nestingLevelDelta);
+ if (wasWaiting) {
+ Consumer_->OnListItem();
+ }
+ }
+
+ bool SetWaitingForListItem(bool value, int nestingLevelDelta)
+ {
+ auto nestingLevel = static_cast<int>(SyntaxChecker_.GetNestingLevel()) + nestingLevelDelta;
+ if (nestingLevel >= std::ssize(WaitingForListItem_)) {
+ WaitingForListItem_.resize(std::max<int>(
+ nestingLevel + 1,
+ 2 * std::ssize(WaitingForListItem_)));
+ }
+ return std::exchange(WaitingForListItem_[nestingLevel], value);
+ }
+
+private:
+ const TYsonSyntaxChecker& SyntaxChecker_;
+ IYsonConsumer* const Consumer_;
+ TCompactVector<bool, 16> WaitingForListItem_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(TYsonItem lhs, TYsonItem rhs)
+{
+ if (lhs.GetType() != rhs.GetType()) {
+ return false;
+ }
+ switch (lhs.GetType()) {
+ case EYsonItemType::BooleanValue:
+ return lhs.UncheckedAsBoolean() == rhs.UncheckedAsBoolean();
+ case EYsonItemType::Int64Value:
+ return lhs.UncheckedAsInt64() == rhs.UncheckedAsInt64();
+ case EYsonItemType::Uint64Value:
+ return lhs.UncheckedAsUint64() == rhs.UncheckedAsUint64();
+ case EYsonItemType::DoubleValue:
+ return lhs.UncheckedAsDouble() == rhs.UncheckedAsDouble();
+ case EYsonItemType::StringValue:
+ return lhs.UncheckedAsString() == rhs.UncheckedAsString();
+ default:
+ return true;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonPullParser::TYsonPullParser(IZeroCopyInput* input, EYsonType ysonType, int nestingLevelLimit)
+ : StreamReader_(input)
+ , Lexer_(StreamReader_)
+ , SyntaxChecker_(ysonType, nestingLevelLimit)
+ , YsonType_(ysonType)
+{ }
+
+ui64 TYsonPullParser::GetTotalReadSize() const
+{
+ return Lexer_.GetTotalReadSize();
+}
+
+TYsonItem TYsonPullParser::Next()
+{
+ try {
+ Lexer_.CheckpointContext();
+ auto visitor = NDetail::TYsonItemCreatingVisitor();
+ return NextImpl(&visitor);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error occurred while parsing YSON")
+ << GetErrorAttributes()
+ << ex;
+ }
+}
+
+void TYsonPullParser::StartRecording(IOutputStream* out)
+{
+ Lexer_.StartRecording(out);
+}
+
+void TYsonPullParser::CancelRecording()
+{
+ Lexer_.CancelRecording();
+}
+
+void TYsonPullParser::FinishRecording()
+{
+ Lexer_.FinishRecording();
+}
+
+EYsonType TYsonPullParser::GetYsonType() const
+{
+ return YsonType_;
+}
+
+void TYsonPullParser::SkipComplexValue()
+{
+ auto visitor = NDetail::TNullVisitor();
+ TraverseComplexValueOrAttributes(&visitor, /* stopAfterAttributes */ false);
+}
+
+void TYsonPullParser::TransferComplexValue(TCheckedInDebugYsonTokenWriter* writer)
+{
+ auto visitor = NDetail::TYsonTokenWritingVisitor(writer);
+ TraverseComplexValueOrAttributes(&visitor, /* stopAfterAttributes */ false);
+}
+
+void TYsonPullParser::SkipComplexValue(const TYsonItem& previousItem)
+{
+ auto visitor = NDetail::TNullVisitor();
+ TraverseComplexValueOrAttributes(&visitor, previousItem, /* stopAfterAttributes */ false);
+}
+
+void TYsonPullParser::TransferComplexValue(TCheckedInDebugYsonTokenWriter* writer, const TYsonItem& previousItem)
+{
+ auto visitor = NDetail::TYsonTokenWritingVisitor(writer);
+ TraverseComplexValueOrAttributes(
+ &visitor,
+ previousItem,
+ /* stopAfterAttributes */ false);
+}
+
+void TYsonPullParser::TransferComplexValue(IYsonConsumer* consumer, const TYsonItem& previousItem)
+{
+ auto visitor = NDetail::TYsonConsumerVisitor(SyntaxChecker_, consumer);
+ TraverseComplexValueOrAttributes(
+ &visitor,
+ previousItem,
+ /* stopAfterAttributes */ false);
+}
+
+
+void TYsonPullParser::SkipAttributes(const TYsonItem& previousItem)
+{
+ EnsureYsonToken("attributes", *this, previousItem, EYsonItemType::BeginAttributes);
+ auto visitor = NDetail::TNullVisitor();
+ TraverseComplexValueOrAttributes(&visitor, previousItem, /* stopAfterAttributes */ true);
+}
+
+void TYsonPullParser::SkipComplexValueOrAttributes(const TYsonItem& previousItem)
+{
+ auto visitor = NDetail::TNullVisitor();
+ TraverseComplexValueOrAttributes(&visitor, previousItem, /* stopAfterAttributes */ true);
+}
+
+void TYsonPullParser::TransferAttributes(TCheckedInDebugYsonTokenWriter* writer, const TYsonItem& previousItem)
+{
+ EnsureYsonToken("attributes", *this, previousItem, EYsonItemType::BeginAttributes);
+ auto visitor = NDetail::TYsonTokenWritingVisitor(writer);
+ TraverseComplexValueOrAttributes(
+ &visitor,
+ previousItem,
+ /* stopAfterAttributes */ true);
+}
+
+void TYsonPullParser::TransferAttributes(IYsonConsumer* consumer, const TYsonItem& previousItem)
+{
+ EnsureYsonToken("attributes", *this, previousItem, EYsonItemType::BeginAttributes);
+ auto visitor = NDetail::TYsonConsumerVisitor(SyntaxChecker_, consumer);
+ TraverseComplexValueOrAttributes(
+ &visitor,
+ previousItem,
+ /* stopAfterAttributes */ true);
+}
+
+std::vector<TErrorAttribute> TYsonPullParser::GetErrorAttributes() const
+{
+ auto result = Lexer_.GetErrorAttributes();
+ auto [context, contextPosition] = Lexer_.GetContextFromCheckpoint();
+
+ TStringStream markedContext;
+ markedContext
+ << EscapeC(context.substr(0, contextPosition))
+ << " ERROR>>> "
+ << EscapeC(context.substr(contextPosition));
+
+ result.emplace_back("context", EscapeC(context));
+ result.emplace_back("context_pos", contextPosition);
+ result.emplace_back("marked_context", markedContext.Str());
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonPullParserCursor::StartRecording(IOutputStream* out)
+{
+ Parser_->StartRecording(out);
+}
+
+void TYsonPullParserCursor::CancelRecording()
+{
+ Parser_->CancelRecording();
+}
+
+void TYsonPullParserCursor::SkipComplexValueAndFinishRecording()
+{
+ Parser_->SkipComplexValue(Current_);
+ Parser_->FinishRecording();
+ Current_ = Parser_->Next();
+}
+
+std::vector<TErrorAttribute> TYsonPullParserCursor::GetErrorAttributes() const
+{
+ return Parser_->GetErrorAttributes();
+}
+
+void TYsonPullParserCursor::SkipComplexValue()
+{
+ Parser_->SkipComplexValue(Current_);
+ Current_ = Parser_->Next();
+}
+
+void TYsonPullParserCursor::TransferComplexValue(NYT::NYson::IYsonConsumer* consumer)
+{
+ Parser_->TransferComplexValue(consumer, Current_);
+ Current_ = Parser_->Next();
+}
+
+void TYsonPullParserCursor::TransferComplexValue(NYT::NYson::TCheckedInDebugYsonTokenWriter* writer)
+{
+ Parser_->TransferComplexValue(writer, Current_);
+ Current_ = Parser_->Next();
+}
+
+void TYsonPullParserCursor::SkipAttributes()
+{
+ Parser_->SkipAttributes(Current_);
+ Current_ = Parser_->Next();
+}
+
+void TYsonPullParserCursor::TransferAttributes(NYT::NYson::IYsonConsumer* consumer)
+{
+ Parser_->TransferAttributes(consumer, Current_);
+ Current_ = Parser_->Next();
+}
+
+void TYsonPullParserCursor::TransferAttributes(NYT::NYson::TCheckedInDebugYsonTokenWriter* writer)
+{
+ Parser_->TransferAttributes(writer, Current_);
+ Current_ = Parser_->Next();
+}
+
+bool TYsonPullParserCursor::TryConsumeFragmentStart()
+{
+ auto result = IsOnFragmentStart_;
+ IsOnFragmentStart_ = false;
+ return result;
+}
+
+[[noreturn]] void TYsonPullParserCursor::FailAsTryConsumeFragmentStartNotCalled()
+{
+ Y_FAIL("TryConsumeFragmentStart must be called before anything else when parsing list or map fragment");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString CreateExpectedItemTypesString(const std::vector<EYsonItemType>& expected)
+{
+ YT_VERIFY(!expected.empty());
+ if (expected.size() > 1) {
+ TStringStream out;
+ out << "one of the tokens {";
+ for (const auto& token : expected) {
+ out << Format("%Qlv, ", token);
+ }
+ out << "}";
+ return out.Str();
+ } else {
+ return Format("%Qlv", expected[0]);
+ }
+}
+
+void ThrowUnexpectedYsonTokenException(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ const std::vector<EYsonItemType>& expected)
+{
+ THROW_ERROR_EXCEPTION("Cannot parse %Qv: expected %v, actual %Qlv",
+ description,
+ CreateExpectedItemTypesString(expected),
+ item.GetType())
+ << parser.GetErrorAttributes();
+}
+
+void ThrowUnexpectedYsonTokenException(
+ TStringBuf description,
+ const TYsonPullParserCursor& cursor,
+ const std::vector<EYsonItemType>& expected)
+{
+ THROW_ERROR_EXCEPTION("Cannot parse %Qv; expected %v, actual %Qlv",
+ description,
+ CreateExpectedItemTypesString(expected),
+ cursor->GetType())
+ << cursor.GetErrorAttributes();
+}
+
+void ThrowUnexpectedTokenException(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ EYsonItemType expected,
+ bool optional)
+{
+ std::vector<EYsonItemType> allExpected = {expected};
+ if (optional) {
+ allExpected.push_back(EYsonItemType::EntityValue);
+ }
+
+ auto fullDescription = TString(optional ? "optional " : "") + description;
+ ThrowUnexpectedYsonTokenException(
+ fullDescription,
+ parser,
+ item,
+ allExpected);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/pull_parser.h b/yt/yt/core/yson/pull_parser.h
new file mode 100644
index 0000000000..f7bef84d31
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser.h
@@ -0,0 +1,344 @@
+#pragma once
+
+#include "public.h"
+#include "detail.h"
+
+#include "syntax_checker.h"
+
+#include <util/generic/strbuf.h>
+#include <util/stream/output.h>
+#include <util/stream/zerocopy.h>
+
+#include <stack>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TYsonItem represents single meaningful yson item.
+// We don't use std::variant for performance reasons.
+class TYsonItem
+{
+public:
+ Y_FORCE_INLINE TYsonItem(const TYsonItem& other);
+ Y_FORCE_INLINE TYsonItem& operator =(const TYsonItem& other);
+
+ static Y_FORCE_INLINE TYsonItem Simple(EYsonItemType type);
+ static Y_FORCE_INLINE TYsonItem Boolean(bool data);
+ static Y_FORCE_INLINE TYsonItem Int64(i64 data);
+ static Y_FORCE_INLINE TYsonItem Uint64(ui64 data);
+ static Y_FORCE_INLINE TYsonItem Double(double data);
+ static Y_FORCE_INLINE TYsonItem String(TStringBuf data);
+ Y_FORCE_INLINE EYsonItemType GetType() const;
+ Y_FORCE_INLINE bool UncheckedAsBoolean() const;
+ Y_FORCE_INLINE i64 UncheckedAsInt64() const;
+ Y_FORCE_INLINE ui64 UncheckedAsUint64() const;
+ Y_FORCE_INLINE double UncheckedAsDouble() const;
+ Y_FORCE_INLINE TStringBuf UncheckedAsString() const;
+ template <typename T>
+ Y_FORCE_INLINE T UncheckedAs() const;
+ Y_FORCE_INLINE bool IsEndOfStream() const;
+
+private:
+ TYsonItem() = default;
+
+private:
+ #pragma pack(push, 1)
+ struct TSmallStringBuf
+ {
+ const char* Ptr;
+ ui32 Size;
+ };
+ union TData
+ {
+ TData() {
+ Zero(*this);
+ }
+
+ bool Boolean;
+ i64 Int64;
+ ui64 Uint64;
+ double Double;
+ TSmallStringBuf String;
+ };
+ TData Data_;
+ EYsonItemType Type_;
+ #pragma pack(pop)
+};
+// NB TYsonItem is 16 bytes long so it fits in a processor register.
+static_assert(sizeof(TYsonItem) <= 16);
+
+bool operator==(TYsonItem lhs, TYsonItem rhs);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TZeroCopyInputStreamReader
+{
+public:
+ TZeroCopyInputStreamReader(IZeroCopyInput* reader);
+
+ Y_FORCE_INLINE void RefreshBlock();
+ Y_FORCE_INLINE const char* Begin() const;
+ Y_FORCE_INLINE const char* Current() const;
+ Y_FORCE_INLINE const char* End() const;
+ Y_FORCE_INLINE void Advance(size_t bytes);
+ Y_FORCE_INLINE bool IsFinished() const;
+ ui64 GetTotalReadSize() const;
+
+ void StartRecording(IOutputStream* out);
+ void CancelRecording();
+ void FinishRecording();
+
+private:
+ IZeroCopyInput* Reader_;
+ const char* Begin_ = nullptr;
+ const char* End_ = nullptr;
+ const char* Current_ = nullptr;
+ ui64 TotalReadBlocksSize_ = 0;
+ bool Finished_ = false;
+
+ const char* RecordingFrom_ = nullptr;
+ IOutputStream* RecordOutput_ = nullptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonPullParser
+{
+public:
+ TYsonPullParser(
+ IZeroCopyInput* input,
+ EYsonType ysonType,
+ int nestingLevelLimit = DefaultYsonParserNestingLevelLimit);
+
+ TYsonItem Next();
+
+ // See comments for corresponding |TYsonPullParserCursor| methods.
+ void SkipComplexValue();
+ void TransferComplexValue(TCheckedInDebugYsonTokenWriter* writer);
+
+ // These methods are analogous to the ones without the |previousItem| argument,
+ // but process the complex value that starts from |previousItem|.
+ void SkipComplexValue(const TYsonItem& previousItem);
+ void TransferComplexValue(TCheckedInDebugYsonTokenWriter* writer, const TYsonItem& previousItem);
+ void TransferComplexValue(IYsonConsumer* consumer, const TYsonItem& previousItem);
+
+ void SkipComplexValueOrAttributes(const TYsonItem& previousItem);
+ void SkipAttributes(const TYsonItem& previousItem);
+ void TransferAttributes(TCheckedInDebugYsonTokenWriter* writer, const TYsonItem& previousItem);
+ void TransferAttributes(IYsonConsumer* consumer, const TYsonItem& previousItem);
+
+ Y_FORCE_INLINE size_t GetNestingLevel() const;
+ Y_FORCE_INLINE bool IsOnValueBoundary(size_t nestingLevel) const;
+
+ ui64 GetTotalReadSize() const;
+
+ // Return error attributes about yson context that is being parsed.
+ std::vector<TErrorAttribute> GetErrorAttributes() const;
+
+ // All the |Parse*| methods expect the next item to have corresponding type,
+ // throwing exception if it does not.
+ Y_FORCE_INLINE ui64 ParseUint64();
+ Y_FORCE_INLINE i64 ParseInt64();
+ Y_FORCE_INLINE TStringBuf ParseString();
+ Y_FORCE_INLINE bool ParseBoolean();
+ Y_FORCE_INLINE double ParseDouble();
+ Y_FORCE_INLINE void ParseBeginList();
+ Y_FORCE_INLINE void ParseEndList();
+ Y_FORCE_INLINE void ParseEntity();
+
+ // |ParseOptional*| allow the next item to be entity
+ // and return |std::nullopt| in this case.
+ Y_FORCE_INLINE std::optional<ui64> ParseOptionalUint64();
+ Y_FORCE_INLINE std::optional<i64> ParseOptionalInt64();
+ Y_FORCE_INLINE std::optional<TStringBuf> ParseOptionalString();
+ Y_FORCE_INLINE std::optional<bool> ParseOptionalBoolean();
+ Y_FORCE_INLINE std::optional<double> ParseOptionalDouble();
+
+ // Returns |true| iff the item was '['.
+ Y_FORCE_INLINE bool ParseOptionalBeginList();
+
+ // |Parse*AsVarint| expect the the next item to have corresponding type,
+ // throwing exception if it does not.
+ // Optional version returns 0 iff parsed item is entity.
+ // Non-optional version never returns 0.
+ //
+ // |Parse*Uint64AsVarint| writes the integer as varint
+ // to the memory location pointed by |out|.
+ // Returns the number of written bytes.
+ Y_FORCE_INLINE int ParseUint64AsVarint(char* out);
+ Y_FORCE_INLINE int ParseOptionalUint64AsVarint(char* out);
+
+ // |Parse*Int64AsZigzagVarint| zigzag encodes the integer and
+ // writes it as varint to the memory location pointed by |out|.
+ // Returns the number of written bytes.
+ Y_FORCE_INLINE int ParseInt64AsZigzagVarint(char* out);
+ Y_FORCE_INLINE int ParseOptionalInt64AsZigzagVarint(char* out);
+
+ // Returns |true| iff the next item is ']'.
+ // NOTE: it does NOT move the cursor.
+ Y_FORCE_INLINE bool IsEndList();
+
+ // Returns |true| iff the next item is '#'.
+ // NOTE: it does NOT move the cursor.
+ Y_FORCE_INLINE bool IsEntity();
+
+ void StartRecording(IOutputStream* out);
+ void CancelRecording();
+ void FinishRecording();
+
+ EYsonType GetYsonType() const;
+
+private:
+ template <typename TVisitor>
+ Y_FORCE_INLINE typename TVisitor::TResult NextImpl(TVisitor* visitor);
+
+ template <typename TVisitor>
+ void TraverseComplexValueOrAttributes(TVisitor* visitor, bool stopAfterAttributes);
+ template <typename TVisitor>
+ void TraverseComplexValueOrAttributes(TVisitor* visitor, const TYsonItem& previousItem, bool stopAfterAttributes);
+
+ Y_FORCE_INLINE void MaybeSkipSemicolon();
+
+ template <EYsonItemType ItemType, bool IsOptional>
+ Y_FORCE_INLINE auto ParseItem() -> std::conditional_t<IsOptional, bool, void>;
+
+ template <typename TValue, EYsonItemType ItemType>
+ Y_FORCE_INLINE TValue ParseTypedValue();
+
+ template <typename TValue, EYsonItemType ItemType>
+ Y_FORCE_INLINE TValue ParseTypedValueFallback();
+
+ template <typename TValue, EYsonItemType ItemType>
+ Y_FORCE_INLINE int ParseVarintToArray(char* out);
+
+ Y_FORCE_INLINE bool IsMarker(char marker);
+
+private:
+ using TLexer = NDetail::TLexerBase<NDetail::TReaderWithContext<NDetail::TZeroCopyInputStreamReader, 64>, false>;
+
+ NDetail::TZeroCopyInputStreamReader StreamReader_;
+ TLexer Lexer_;
+ NDetail::TYsonSyntaxChecker SyntaxChecker_;
+ EYsonType YsonType_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Cursor based iteration using pull parser.
+// To check if yson stream is exhausted check if current item is of type EYsonItemType::EndOfStream.
+class TYsonPullParserCursor
+{
+public:
+ // This constructor extracts next element from parser immediately.
+ Y_FORCE_INLINE TYsonPullParserCursor(TYsonPullParser* parser);
+
+ Y_FORCE_INLINE const TYsonItem& GetCurrent() const;
+ Y_FORCE_INLINE const TYsonItem* operator->() const;
+ Y_FORCE_INLINE const TYsonItem& operator*() const;
+
+ Y_FORCE_INLINE void Next();
+
+ // Return error attributes about current yson context.
+ std::vector<TErrorAttribute> GetErrorAttributes() const;
+
+ // If cursor is positioned over simple value (i.e. just integer) cursor is moved one element further.
+ // If cursor is positioned over start of list/map cursor will be moved to the first item after
+ // current list/map.
+ // If cursor is positioned over start of attributes all attributes will be skipped and then value which
+ // owns these attributes will be skipped as well.
+ void SkipComplexValue();
+
+ // Transfer complex value is similar to SkipComplexValue
+ // except that it feeds passed consumer with skipped value.
+ void TransferComplexValue(IYsonConsumer* consumer);
+ void TransferComplexValue(TCheckedInDebugYsonTokenWriter* writer);
+
+ // |Parse...| methods call |function(this)| for each item of corresponding composite object
+ // and expect |function| to consume it (e.g. call |cursor->Next()|).
+ // For map and attributes cursor will point to the key and |function| must consume both key and value.
+ template <typename TFunction>
+ void ParseMap(TFunction function);
+ template <typename TFunction>
+ void ParseList(TFunction function);
+ template <typename TFunction>
+ void ParseAttributes(TFunction function);
+
+ // Transfer or skip attributes (if cursor is not positioned over attributes, throws an error).
+ void SkipAttributes();
+ void TransferAttributes(IYsonConsumer* consumer);
+ void TransferAttributes(TCheckedInDebugYsonTokenWriter* writer);
+
+ // Start recording the bytes from parsed stream to |out|.
+ void StartRecording(IOutputStream* out);
+
+ // Stop the recording.
+ void CancelRecording();
+
+ // Record the current complex value and stop recording.
+ void SkipComplexValueAndFinishRecording();
+
+ // Returns |true| iff cursor is positioned over the beginning of the first
+ // item of top-level map or list fragment (for corresponding EYsonType).
+ // Consequent calls will return |false|.
+ bool TryConsumeFragmentStart();
+
+private:
+ bool IsOnFragmentStart_;
+ TYsonItem Current_;
+ TYsonPullParser* Parser_;
+
+private:
+ void SkipComplexValueOrAttributes();
+ [[noreturn]] static void FailAsTryConsumeFragmentStartNotCalled();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ TStringBuf description,
+ const TYsonPullParserCursor& cursor,
+ EYsonItemType expected);
+
+Y_FORCE_INLINE void EnsureYsonToken(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ EYsonItemType expected);
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+[[noreturn]] void ThrowUnexpectedYsonTokenException(
+ TStringBuf description,
+ const TYsonPullParserCursor& cursor,
+ const std::vector<EYsonItemType>& expected);
+
+[[noreturn]] void ThrowUnexpectedYsonTokenException(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ const std::vector<EYsonItemType>& expected);
+
+[[noreturn]] void ThrowUnexpectedTokenException(
+ TStringBuf description,
+ const TYsonPullParser& parser,
+ const TYsonItem& item,
+ EYsonItemType expected,
+ bool isOptional);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define PULL_PARSER_INL_H_
+#include "pull_parser-inl.h"
+#undef PULL_PARSER_INL_H_
diff --git a/yt/yt/core/yson/pull_parser_deserialize-inl.h b/yt/yt/core/yson/pull_parser_deserialize-inl.h
new file mode 100644
index 0000000000..c5e39e7df8
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser_deserialize-inl.h
@@ -0,0 +1,375 @@
+#ifndef PULL_PARSER_DESERIALIZE_INL_H_
+#error "Direct inclusion of this file is not allowed, include pull_parser_deserialize.h"
+// For the sake of sane code completion.
+#include "pull_parser_deserialize.h"
+#endif
+
+#include "public.h"
+
+#include "pull_parser.h"
+#include "pull_parser_deserialize.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <vector>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void SkipAttributes(TYsonPullParserCursor* cursor)
+{
+ cursor->SkipAttributes();
+ if ((*cursor)->GetType() == EYsonItemType::BeginAttributes) {
+ THROW_ERROR_EXCEPTION("Repeated attributes are not allowed");
+ }
+}
+
+inline void MaybeSkipAttributes(TYsonPullParserCursor* cursor)
+{
+ if ((*cursor)->GetType() == EYsonItemType::BeginAttributes) {
+ SkipAttributes(cursor);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+void DeserializeVector(T& value, TYsonPullParserCursor* cursor)
+{
+ int index = 0;
+ auto itemVisitor = [&] (TYsonPullParserCursor* cursor) {
+ if (index < static_cast<int>(value.size())) {
+ Deserialize(value[index], cursor);
+ } else {
+ value.emplace_back();
+ Deserialize(value.back(), cursor);
+ }
+ ++index;
+ };
+
+ if (cursor->TryConsumeFragmentStart()) {
+ while ((*cursor)->GetType() != EYsonItemType::EndOfStream) {
+ itemVisitor(cursor);
+ }
+ } else {
+ MaybeSkipAttributes(cursor);
+ cursor->ParseList(itemVisitor);
+ }
+
+ value.resize(index);
+}
+
+template <class T>
+void DeserializeSet(T& value, TYsonPullParserCursor* cursor)
+{
+ value.clear();
+
+ auto itemVisitor = [&] (TYsonPullParserCursor* cursor) {
+ value.insert(ExtractTo<typename T::value_type>(cursor));
+ };
+
+ if (cursor->TryConsumeFragmentStart()) {
+ while ((*cursor)->GetType() != EYsonItemType::EndOfStream) {
+ itemVisitor(cursor);
+ }
+ } else {
+ MaybeSkipAttributes(cursor);
+ cursor->ParseList(itemVisitor);
+ }
+}
+
+template <class T>
+void DeserializeMap(T& value, TYsonPullParserCursor* cursor)
+{
+ value.clear();
+
+ auto itemVisitor = [&] (TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<typename T::key_type>(cursor);
+ auto item = ExtractTo<typename T::mapped_type>(cursor);
+ if (value.contains(key)) {
+ THROW_ERROR_EXCEPTION("Duplicate key %Qv", key);
+ }
+ value.emplace(std::move(key), std::move(item));
+ };
+
+ if (cursor->TryConsumeFragmentStart()) {
+ while ((*cursor)->GetType() != EYsonItemType::EndOfStream) {
+ itemVisitor(cursor);
+ }
+ } else {
+ MaybeSkipAttributes(cursor);
+ cursor->ParseMap(itemVisitor);
+ }
+}
+
+template <class T, bool IsSet = std::is_same<typename T::key_type, typename T::value_type>::value>
+struct TAssociativeHelper;
+
+template <class T>
+struct TAssociativeHelper<T, true>
+{
+ static void Deserialize(T& value, TYsonPullParserCursor* cursor)
+ {
+ DeserializeSet(value, cursor);
+ }
+};
+
+template <class T>
+struct TAssociativeHelper<T, false>
+{
+ static void Deserialize(T& value, TYsonPullParserCursor* cursor)
+ {
+ DeserializeMap(value, cursor);
+ }
+};
+
+template <class T>
+void DeserializeAssociative(T& value, TYsonPullParserCursor* cursor)
+{
+ TAssociativeHelper<T>::Deserialize(value, cursor);
+}
+
+template <class T, size_t Size = std::tuple_size<T>::value>
+struct TTupleHelper;
+
+template <class T>
+struct TTupleHelper<T, 0U>
+{
+ static void DeserializeItem(T&, TYsonPullParserCursor*) {}
+};
+
+template <class T, size_t Size>
+struct TTupleHelper
+{
+ static void DeserializeItem(T& value, TYsonPullParserCursor* cursor)
+ {
+ TTupleHelper<T, Size - 1U>::DeserializeItem(value, cursor);
+ if ((*cursor)->GetType() != EYsonItemType::EndList) {
+ Deserialize(std::get<Size - 1U>(value), cursor);
+ }
+ }
+};
+
+template <class T>
+void DeserializeTuple(T& value, TYsonPullParserCursor* cursor)
+{
+ const auto isListFragment = cursor->TryConsumeFragmentStart();
+
+ if (!isListFragment) {
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("tuple", *cursor, EYsonItemType::BeginList);
+ cursor->Next();
+ }
+
+ TTupleHelper<T>::DeserializeItem(value, cursor);
+
+ auto endItemType = isListFragment
+ ? EYsonItemType::EndOfStream
+ : EYsonItemType::EndList;
+ while ((*cursor)->GetType() != endItemType) {
+ cursor->SkipComplexValue();
+ }
+
+ if (!isListFragment) {
+ cursor->Next();
+ }
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class A>
+void Deserialize(std::vector<T, A>& value, NYson::TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ NDetail::DeserializeVector(value, cursor);
+}
+
+template <class T, class A>
+void Deserialize(std::deque<T, A>& value, NYson::TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ NDetail::DeserializeVector(value, cursor);
+}
+
+template <class T>
+void Deserialize(std::optional<T>& value, TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ MaybeSkipAttributes(cursor);
+ if ((*cursor)->GetType() == EYsonItemType::EntityValue) {
+ value.reset();
+ cursor->Next();
+ } else {
+ if (!value) {
+ value.emplace();
+ }
+ Deserialize(*value, cursor);
+ }
+}
+
+// Enum.
+template <class T>
+requires TEnumTraits<T>::IsEnum
+void Deserialize(T& value, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ if constexpr (TEnumTraits<T>::IsBitEnum) {
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::BeginList:
+ value = T();
+ cursor->ParseList([&] (TYsonPullParserCursor* cursor) {
+ EnsureYsonToken("bit enum", *cursor, EYsonItemType::StringValue);
+ value |= ParseEnum<T>((*cursor)->UncheckedAsString());
+ cursor->Next();
+ });
+ break;
+ case EYsonItemType::StringValue:
+ value = ParseEnum<T>((*cursor)->UncheckedAsString());
+ cursor->Next();
+ break;
+ default:
+ ThrowUnexpectedYsonTokenException(
+ "bit enum",
+ *cursor,
+ {EYsonItemType::BeginList, EYsonItemType::StringValue});
+ }
+ } else {
+ EnsureYsonToken("enum", *cursor, EYsonItemType::StringValue);
+ value = ParseEnum<T>((*cursor)->UncheckedAsString());
+ cursor->Next();
+ }
+}
+
+// TCompactVector
+template <class T, size_t N>
+void Deserialize(TCompactVector<T, N>& value, TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ NDetail::DeserializeVector(value, cursor);
+}
+
+template <class F, class S>
+void Deserialize(std::pair<F, S>& value, TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<F, S>(), void*>)
+{
+ NDetail::DeserializeTuple(value, cursor);
+}
+
+template <class T, size_t N>
+void Deserialize(std::array<T, N>& value, TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ NDetail::DeserializeTuple(value, cursor);
+}
+
+template <class... T>
+void Deserialize(std::tuple<T...>& value, TYsonPullParserCursor* cursor, std::enable_if_t<ArePullParserDeserializable<T...>(), void*>)
+{
+ NDetail::DeserializeTuple(value, cursor);
+}
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K>
+void Deserialize(
+ C<T...>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<typename NDetail::TRemoveConst<typename C<T...>::value_type>::Type>(), void*>)
+{
+ NDetail::DeserializeAssociative(value, cursor);
+}
+
+template <class E, class T, E Min, E Max>
+void Deserialize(
+ TEnumIndexedVector<E, T, Min, Max>& vector,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*>)
+{
+ vector = {};
+
+ auto itemVisitor = [&] (TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<E>(cursor);
+ if (!vector.IsDomainValue(key)) {
+ THROW_ERROR_EXCEPTION("Enum value %Qlv is out of supported range",
+ key);
+ }
+ Deserialize(vector[key], cursor);
+ };
+
+ if (cursor->TryConsumeFragmentStart()) {
+ while ((*cursor)->GetType() != EYsonItemType::EndOfStream) {
+ itemVisitor(cursor);
+ }
+ } else {
+ MaybeSkipAttributes(cursor);
+ cursor->ParseMap(itemVisitor);
+ }
+}
+
+template <typename T>
+void DeserializePtr(T& value, TYsonPullParserCursor* cursor)
+{
+ if ((*cursor)->GetType() != EYsonItemType::BeginAttributes) {
+ if ((*cursor)->GetType() == EYsonItemType::EntityValue) {
+ cursor->Next();
+ } else {
+ Deserialize(value, cursor);
+ }
+ return;
+ }
+
+ // We need to place begin attributes token as it
+ // will not be recorded.
+ TStringStream stream("<");
+ {
+ cursor->StartRecording(&stream);
+ cursor->SkipAttributes();
+ if ((*cursor)->GetType() == EYsonItemType::EntityValue) {
+ cursor->CancelRecording();
+ cursor->Next();
+ return;
+ }
+ cursor->SkipComplexValueAndFinishRecording();
+ }
+ TYsonPullParser parser(&stream, EYsonType::Node);
+ TYsonPullParserCursor newCursor(&parser);
+ Deserialize(value, &newCursor);
+}
+
+template <class T>
+void Deserialize(TIntrusivePtr<T>& value, TYsonPullParserCursor* cursor)
+{
+ if (!value) {
+ value = New<T>();
+ }
+ DeserializePtr(*value, cursor);
+}
+
+template <class T>
+void Deserialize(std::unique_ptr<T>& value, TYsonPullParserCursor* cursor)
+{
+ if (!value) {
+ value = std::make_unique<T>();
+ }
+ DeserializePtr(*value, cursor);
+}
+
+template <class T, class TTag>
+void Deserialize(TStrongTypedef<T, TTag>& value, TYsonPullParserCursor* cursor)
+{
+ Deserialize(value.Underlying(), cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TTo>
+TTo ExtractTo(TYsonPullParserCursor* cursor)
+{
+ TTo result;
+ Deserialize(result, cursor);
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/pull_parser_deserialize.cpp b/yt/yt/core/yson/pull_parser_deserialize.cpp
new file mode 100644
index 0000000000..0ccc9cdc42
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser_deserialize.cpp
@@ -0,0 +1,242 @@
+#include "pull_parser_deserialize.h"
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// integers
+template <typename T>
+void DeserializeInteger(T& value, TYsonPullParserCursor* cursor, TStringBuf typeName)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::Int64Value:
+ value = CheckedIntegralCast<T>((*cursor)->UncheckedAsInt64());
+ cursor->Next();
+ break;
+ case EYsonItemType::Uint64Value:
+ value = CheckedIntegralCast<T>((*cursor)->UncheckedAsUint64());
+ cursor->Next();
+ break;
+ case EYsonItemType::BeginAttributes:
+ SkipAttributes(cursor);
+ DeserializeInteger(value, cursor, typeName);
+ break;
+ default:
+ ThrowUnexpectedYsonTokenException(
+ typeName,
+ *cursor,
+ {EYsonItemType::Int64Value, EYsonItemType::Uint64Value});
+ }
+}
+
+#define DESERIALIZE(type) \
+ void Deserialize(type& value, TYsonPullParserCursor* cursor) \
+ { \
+ DeserializeInteger(value, cursor, TStringBuf(#type)); \
+ }
+
+DESERIALIZE(signed char)
+DESERIALIZE(short)
+DESERIALIZE(int)
+DESERIALIZE(long)
+DESERIALIZE(long long)
+DESERIALIZE(unsigned char)
+DESERIALIZE(unsigned short)
+DESERIALIZE(unsigned)
+DESERIALIZE(unsigned long)
+DESERIALIZE(unsigned long long)
+
+#undef DESERIALIZE
+
+// double
+void Deserialize(double& value, TYsonPullParserCursor* cursor)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::Int64Value:
+ // Allow integers to be deserialized into doubles.
+ value = (*cursor)->UncheckedAsInt64();
+ cursor->Next();
+ break;
+ case EYsonItemType::Uint64Value:
+ value = (*cursor)->UncheckedAsUint64();
+ cursor->Next();
+ break;
+ case EYsonItemType::DoubleValue:
+ value = (*cursor)->UncheckedAsDouble();
+ cursor->Next();
+ break;
+ case EYsonItemType::BeginAttributes:
+ SkipAttributes(cursor);
+ Deserialize(value, cursor);
+ break;
+ default:
+ ThrowUnexpectedYsonTokenException(
+ "double",
+ *cursor,
+ {EYsonItemType::Int64Value, EYsonItemType::Uint64Value, EYsonItemType::DoubleValue});
+ }
+}
+
+// TString.
+void Deserialize(TString& value, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("string", *cursor, EYsonItemType::StringValue);
+ value = (*cursor)->UncheckedAsString();
+ cursor->Next();
+}
+
+// bool
+void Deserialize(bool& value, TYsonPullParserCursor* cursor)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::BooleanValue:
+ value = (*cursor)->UncheckedAsBoolean();
+ cursor->Next();
+ break;
+ case EYsonItemType::StringValue:
+ value = ParseBool(TString((*cursor)->UncheckedAsString()));
+ cursor->Next();
+ break;
+ case EYsonItemType::Int64Value: {
+ auto intValue = (*cursor)->UncheckedAsInt64();
+ if (intValue != 0 && intValue != 1) {
+ THROW_ERROR_EXCEPTION("Expected 0 or 1 but found %v", intValue);
+ }
+ value = static_cast<bool>(intValue);
+ cursor->Next();
+ break;
+ }
+ case EYsonItemType::Uint64Value: {
+ auto uintValue = (*cursor)->UncheckedAsUint64();
+ if (uintValue != 0 && uintValue != 1) {
+ THROW_ERROR_EXCEPTION("Expected 0 or 1 but found %v", uintValue);
+ }
+ value = static_cast<bool>(uintValue);
+ cursor->Next();
+ break;
+ }
+ case EYsonItemType::BeginAttributes:
+ SkipAttributes(cursor);
+ Deserialize(value, cursor);
+ break;
+ default:
+ ThrowUnexpectedYsonTokenException(
+ "bool",
+ *cursor,
+ {EYsonItemType::BooleanValue, EYsonItemType::StringValue});
+ }
+}
+
+// char
+void Deserialize(char& value, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("char", *cursor, EYsonItemType::StringValue);
+ auto stringValue = (*cursor)->UncheckedAsString();
+ if (stringValue.size() != 1) {
+ THROW_ERROR_EXCEPTION("Expected string of length 1 but found of length %v", stringValue.size());
+ }
+ value = stringValue[0];
+ cursor->Next();
+}
+
+// TDuration
+void Deserialize(TDuration& value, TYsonPullParserCursor* cursor)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::Int64Value:
+ value = TDuration::MilliSeconds((*cursor)->UncheckedAsInt64());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::Uint64Value:
+ value = TDuration::MilliSeconds((*cursor)->UncheckedAsUint64());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::StringValue:
+ value = TDuration::Parse((*cursor)->UncheckedAsString());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::DoubleValue: {
+ auto ms = (*cursor)->UncheckedAsDouble();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Duration cannot be negative");
+ }
+ value = TDuration::MicroSeconds(static_cast<ui64>(ms * 1000.0));
+ cursor->Next();
+ break;
+ }
+
+ case EYsonItemType::BeginAttributes:
+ SkipAttributes(cursor);
+ Deserialize(value, cursor);
+ break;
+
+
+ default:
+ ThrowUnexpectedYsonTokenException(
+ "TDuration",
+ *cursor,
+ {EYsonItemType::Int64Value, EYsonItemType::Uint64Value});
+ }
+}
+
+// TInstant.
+void Deserialize(TInstant& value, TYsonPullParserCursor* cursor)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::Int64Value:
+ value = TInstant::MilliSeconds((*cursor)->UncheckedAsInt64());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::Uint64Value:
+ value = TInstant::MilliSeconds((*cursor)->UncheckedAsUint64());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::StringValue:
+ value = TInstant::ParseIso8601((*cursor)->UncheckedAsString());
+ cursor->Next();
+ break;
+
+ case EYsonItemType::DoubleValue: {
+ auto ms = (*cursor)->UncheckedAsDouble();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Duration cannot be negative");
+ }
+ value = TInstant::MicroSeconds(static_cast<ui64>(ms * 1000.0));
+ cursor->Next();
+ break;
+ }
+
+ case EYsonItemType::BeginAttributes:
+ SkipAttributes(cursor);
+ Deserialize(value, cursor);
+ break;
+
+ default:
+ ThrowUnexpectedYsonTokenException(
+ "TInstant",
+ *cursor,
+ {EYsonItemType::Int64Value, EYsonItemType::Uint64Value, EYsonItemType::StringValue});
+ }
+}
+
+// TGuid.
+void Deserialize(TGuid& value, TYsonPullParserCursor* cursor)
+{
+ MaybeSkipAttributes(cursor);
+ EnsureYsonToken("GUID", *cursor, EYsonItemType::StringValue);
+ value = TGuid::FromString((*cursor)->UncheckedAsString());
+ cursor->Next();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/pull_parser_deserialize.h b/yt/yt/core/yson/pull_parser_deserialize.h
new file mode 100644
index 0000000000..0a03fc775e
--- /dev/null
+++ b/yt/yt/core/yson/pull_parser_deserialize.h
@@ -0,0 +1,172 @@
+#pragma once
+
+#include "public.h"
+
+#include "pull_parser.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline void SkipAttributes(TYsonPullParserCursor* cursor);
+inline void MaybeSkipAttributes(TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <typename T, typename = void>
+struct TIsPullParserDeserializable
+ : std::false_type
+{ };
+
+template <typename T>
+struct TIsPullParserDeserializable<T, std::void_t<decltype(Deserialize(std::declval<T&>(), (NYson::TYsonPullParserCursor*)(nullptr)))>>
+ : std::true_type
+{ };
+
+template <typename T>
+struct TRemoveConst
+{
+ using Type = T;
+};
+
+template <typename K, typename V>
+struct TRemoveConst<std::pair<const K, V>>
+{
+ using Type = std::pair<K, V>;
+};
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename ...Ts>
+constexpr bool ArePullParserDeserializable()
+{
+ return (... && NDetail::TIsPullParserDeserializable<Ts>::value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// integers
+void Deserialize(signed char& value, TYsonPullParserCursor* cursor);
+void Deserialize(unsigned char& value, TYsonPullParserCursor* cursor);
+void Deserialize(short& value, TYsonPullParserCursor* cursor);
+void Deserialize(unsigned short& value, TYsonPullParserCursor* cursor);
+void Deserialize(int& value, TYsonPullParserCursor* cursor);
+void Deserialize(unsigned& value, TYsonPullParserCursor* cursor);
+void Deserialize(long& value, TYsonPullParserCursor* cursor);
+void Deserialize(unsigned long& value, TYsonPullParserCursor* cursor);
+void Deserialize(long long& value, TYsonPullParserCursor* cursor);
+void Deserialize(unsigned long long& value, TYsonPullParserCursor* cursor);
+
+// double
+void Deserialize(double& value, TYsonPullParserCursor* cursor);
+
+// TString
+void Deserialize(TString& value, TYsonPullParserCursor* cursor);
+
+// bool
+void Deserialize(bool& value, TYsonPullParserCursor* cursor);
+
+// char
+void Deserialize(char& value, TYsonPullParserCursor* cursor);
+
+// TDuration
+void Deserialize(TDuration& value, TYsonPullParserCursor* cursor);
+
+// TInstant
+void Deserialize(TInstant& value, TYsonPullParserCursor* cursor);
+
+// TGuid.
+void Deserialize(TGuid& value, TYsonPullParserCursor* cursor);
+
+// std::vector.
+template <class T, class A>
+void Deserialize(
+ std::vector<T, A>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+// std::deque
+template <class T, class A>
+void Deserialize(
+ std::deque<T, A>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+// std::optional.
+template <class T>
+void Deserialize(
+ std::optional<T>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+// Enum.
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void Deserialize(T& value, TYsonPullParserCursor* cursor);
+
+// TCompactVector.
+template <class T, size_t N>
+void Deserialize(
+ TCompactVector<T, N>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+// std::pair.
+template <class F, class S>
+void Deserialize(
+ std::pair<F, S>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<F, S>(), void*> = nullptr);
+
+// std::array.
+template <class T, size_t N>
+void Deserialize(
+ std::array<T, N>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+// std::tuple.
+template <class... T>
+void Deserialize(
+ std::tuple<T...>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T...>(), void*> = nullptr);
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K = typename C<T...>::key_type>
+void Deserialize(
+ C<T...>& value,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<typename NDetail::TRemoveConst<typename C<T...>::value_type>::Type>(), void*> = nullptr);
+
+template <class E, class T, E Min, E Max>
+void Deserialize(
+ TEnumIndexedVector<E, T, Min, Max>& vector,
+ TYsonPullParserCursor* cursor,
+ std::enable_if_t<ArePullParserDeserializable<T>(), void*> = nullptr);
+
+template <class T>
+void Deserialize(TIntrusivePtr<T>& value, TYsonPullParserCursor* cursor);
+
+template <class T>
+void Deserialize(std::unique_ptr<T>& value, TYsonPullParserCursor* cursor);
+
+template <class T, class TTag>
+void Deserialize(TStrongTypedef<T, TTag>& value, TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename TTo>
+TTo ExtractTo(TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define PULL_PARSER_DESERIALIZE_INL_H_
+#include "pull_parser_deserialize-inl.h"
+#undef PULL_PARSER_DESERIALIZE_INL_H_
diff --git a/yt/yt/core/yson/stream.cpp b/yt/yt/core/yson/stream.cpp
new file mode 100644
index 0000000000..d91bb652bf
--- /dev/null
+++ b/yt/yt/core/yson/stream.cpp
@@ -0,0 +1,80 @@
+#include "stream.h"
+#include "parser.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+#include <yt/yt/core/concurrency/scheduler.h>
+
+#include <yt/yt/core/yson/parser.h>
+
+namespace NYT::NYson {
+
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const size_t ParseBufferSize = 1 << 16;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonInput::TYsonInput(IInputStream* stream, EYsonType type)
+ : AsyncStream_(nullptr)
+ , Stream_(stream)
+ , Type_(type)
+{ }
+
+TYsonInput::TYsonInput(
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr& asyncStream,
+ EYsonType type)
+ : AsyncStream_(asyncStream)
+ , Stream_(nullptr)
+ , Type_(type)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonOutput::TYsonOutput(IOutputStream* stream, EYsonType type)
+ : Stream_(stream)
+ , Type_(type)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonInput& input, IYsonConsumer* consumer)
+{
+ ParseYson(input, consumer);
+}
+
+void ParseYson(
+ const TYsonInput& input,
+ IYsonConsumer* consumer,
+ bool enableLinePositionInfo)
+{
+ TYsonParser parser(consumer, input.GetType(), {.EnableLinePositionInfo = enableLinePositionInfo});
+ if (input.GetStream()) {
+ char buffer[ParseBufferSize];
+ while (true) {
+ size_t bytesRead = input.GetStream()->Read(buffer, ParseBufferSize);
+ if (bytesRead == 0) {
+ break;
+ }
+ parser.Read(TStringBuf(buffer, bytesRead));
+ }
+ } else {
+ while (true) {
+ auto buffer = WaitFor(input.AsyncStream()->Read())
+ .ValueOrThrow();
+
+ if (buffer.Empty()) {
+ break;
+ }
+
+ parser.Read(TStringBuf(buffer.Begin(), buffer.Size()));
+ }
+ }
+
+ parser.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/stream.h b/yt/yt/core/yson/stream.h
new file mode 100644
index 0000000000..6d32bb7f34
--- /dev/null
+++ b/yt/yt/core/yson/stream.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "public.h"
+#include "string.h"
+
+#include <yt/yt/core/concurrency/async_stream.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonInput
+{
+public:
+ explicit TYsonInput(
+ IInputStream* stream,
+ EYsonType type = EYsonType::Node);
+
+ explicit TYsonInput(
+ const NConcurrency::IAsyncZeroCopyInputStreamPtr& asyncStream,
+ EYsonType type = EYsonType::Node);
+
+ DEFINE_BYREF_RO_PROPERTY(NConcurrency::IAsyncZeroCopyInputStreamPtr, AsyncStream);
+ DEFINE_BYVAL_RO_PROPERTY(IInputStream*, Stream);
+ DEFINE_BYVAL_RO_PROPERTY(EYsonType, Type);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonOutput
+{
+public:
+ explicit TYsonOutput(
+ IOutputStream* stream,
+ EYsonType type = EYsonType::Node);
+
+ DEFINE_BYVAL_RO_PROPERTY(IOutputStream*, Stream);
+ DEFINE_BYVAL_RO_PROPERTY(EYsonType, Type);
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// To hook-up with Serialize/Deserialize framework.
+// For direct calls, use ParseYson instead.
+void Serialize(
+ const TYsonInput& input,
+ IYsonConsumer* consumer);
+
+void ParseYson(
+ const TYsonInput& input,
+ IYsonConsumer* consumer,
+ bool enableLinePositionInfo = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/string.cpp b/yt/yt/core/yson/string.cpp
new file mode 100644
index 0000000000..b7d9bb4af7
--- /dev/null
+++ b/yt/yt/core/yson/string.cpp
@@ -0,0 +1,69 @@
+#include "string.h"
+#include "stream.h"
+#include "null_consumer.h"
+#include "parser.h"
+#include "consumer.h"
+#include "pull_parser.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateYson(const TYsonStringBuf& str, int nestingLevelLimit)
+{
+ if (str) {
+ TMemoryInput input(str.AsStringBuf());
+ TYsonPullParser parser(&input, str.GetType(), nestingLevelLimit);
+ auto cursor = TYsonPullParserCursor(&parser);
+ cursor.SkipComplexValue();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBinaryYsonStringSerializer::Save(TStreamSaveContext& context, const TYsonString& str)
+{
+ using NYT::Save;
+ if (str) {
+ Save(context, static_cast<i32>(str.GetType()));
+ auto strBuf = str.AsStringBuf();
+ TSizeSerializer::Save(context, strBuf.length());
+ TRangeSerializer::Save(context, TRef::FromStringBuf(strBuf));
+ } else {
+ Save(context, static_cast<i32>(-1));
+ }
+}
+
+void TBinaryYsonStringSerializer::Load(TStreamLoadContext& context, TYsonString& str)
+{
+ using NYT::Load;
+ auto type = Load<i32>(context);
+ if (type != -1) {
+ auto size = TSizeSerializer::Load(context);
+ auto holder = NDetail::TYsonStringHolder::Allocate(size);
+ TRangeSerializer::Load(context, TMutableRef(holder->GetData(), size));
+ auto ref = TRef(holder->GetData(), size);
+ auto sharedRef = TSharedRef(ref, std::move(holder));
+ str = TYsonString(std::move(sharedRef), static_cast<EYsonType>(type));
+ } else {
+ str = TYsonString();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonString& yson, IYsonConsumer* consumer)
+{
+ consumer->OnRaw(yson);
+}
+
+void Serialize(const TYsonStringBuf& yson, IYsonConsumer* consumer)
+{
+ consumer->OnRaw(yson);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/string.h b/yt/yt/core/yson/string.h
new file mode 100644
index 0000000000..45938e3f7d
--- /dev/null
+++ b/yt/yt/core/yson/string.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/yson_string/string.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateYson(const TYsonStringBuf& str, int nestingLevelLimit);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBinaryYsonStringSerializer
+{
+ static void Save(TStreamSaveContext& context, const TYsonString& str);
+ static void Load(TStreamLoadContext& context, TYsonString& str);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonString& yson, IYsonConsumer* consumer);
+void Serialize(const TYsonStringBuf& yson, IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class C>
+struct TSerializerTraits<NYson::TYsonString, C, void>
+{
+ using TSerializer = NYson::TBinaryYsonStringSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/yson/string_filter.cpp b/yt/yt/core/yson/string_filter.cpp
new file mode 100644
index 0000000000..bbd3a0902e
--- /dev/null
+++ b/yt/yt/core/yson/string_filter.cpp
@@ -0,0 +1,390 @@
+#include "string_filter.h"
+
+#include "consumer.h"
+#include "pull_parser.h"
+#include "token_writer.h"
+
+#include <yt/yt/core/ypath/stack.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NYson {
+
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////
+
+//! A class that is able to produce YSON strings from a "sparse" sequence of
+//! YSON fragments equipped with YPath stacks defining their location in the YSON.
+class TSparseYsonBuilder
+{
+public:
+ //! Switch to the given stack, properly closing all not needed collections from
+ //! the previous stack and opening new needed collections for the given stack.
+ void SwitchStack(TYPathStack newStack)
+ {
+ if (IsFirstStack_) {
+ OnFirstStack(newStack);
+ } else {
+ OnIntermediateStack(newStack);
+ }
+
+ Stack_ = std::move(newStack);
+ }
+
+ //! Returns either resulting YSON or the null YSON in case of no #SwitchStack calls.
+ TYsonString Flush()
+ {
+ if (IsFirstStack_) {
+ // This means that there were no matches, therefore we must return null YSON.
+ return {};
+ }
+
+ // Close all currently open collections.
+ OnAfterLastStack();
+
+ // Then, flush the writer and return the resulting YSON.
+ Writer_.Flush();
+ Output_.Flush();
+ YT_VERIFY(!Result_.empty());
+ return TYsonString(std::move(Result_));
+ }
+
+ //! Access the underlying YSON writer for injecting YSON fragments.
+ TCheckedInDebugYsonTokenWriter* GetWriter()
+ {
+ return &Writer_;
+ }
+
+private:
+ TString Result_;
+ TStringOutput Output_ = TStringOutput(Result_);
+ TCheckedInDebugYsonTokenWriter Writer_ = TCheckedInDebugYsonTokenWriter(&Output_);
+ TYPathStack Stack_;
+ bool IsFirstStack_ = true;
+
+ //! Open a map or a list for the given stack entry.
+ void OpenEntry(const TYPathStack::TEntry& entry)
+ {
+ if (std::holds_alternative<TString>(entry)) {
+ Writer_.WriteBeginMap();
+ Writer_.WriteBinaryString(std::get<TString>(entry));
+ Writer_.WriteKeyValueSeparator();
+ } else {
+ Writer_.WriteBeginList();
+ // Do not forget to write entities until we are at a correct index.
+ for (int index = 0; index < std::get<int>(entry); ++index) {
+ Writer_.WriteEntity();
+ Writer_.WriteItemSeparator();
+ }
+ }
+ }
+
+ //! Close a map or a list for the given stack entry.
+ void CloseEntry(const TYPathStack::TEntry& entry)
+ {
+ if (std::holds_alternative<TString>(entry)) {
+ Writer_.WriteEndMap();
+ } else {
+ Writer_.WriteEndList();
+ }
+ }
+
+ //! Handle the special case of a first stack.
+ void OnFirstStack(TYPathStack newStack)
+ {
+ IsFirstStack_ = false;
+ // Open necessary collections for the first stack. Mind that the first entry is an artificial /0.
+ const auto& newStackItems = newStack.Items();
+ for (ssize_t index = 1; index < std::ssize(newStackItems); ++index) {
+ OpenEntry(newStackItems[index]);
+ }
+ }
+
+ //! Handle the case of a stack that is not the first one.
+ void OnIntermediateStack(TYPathStack newStack)
+ {
+ const auto& oldStackItems = Stack_.Items();
+ const auto& newStackItems = newStack.Items();
+
+ ssize_t firstDifferingItemIndex = 0;
+ while (
+ firstDifferingItemIndex < std::ssize(oldStackItems) &&
+ firstDifferingItemIndex < std::ssize(newStackItems) &&
+ oldStackItems[firstDifferingItemIndex] == newStackItems[firstDifferingItemIndex])
+ {
+ ++firstDifferingItemIndex;
+ }
+ // Note that none of the two intermediate stacks may be a prefix of another.
+ YT_VERIFY(firstDifferingItemIndex < std::ssize(oldStackItems));
+ YT_VERIFY(firstDifferingItemIndex < std::ssize(newStackItems));
+
+ // Close all collections that are not present in the new stack.
+ for (auto index = std::ssize(oldStackItems) - 1; index > firstDifferingItemIndex; --index) {
+ CloseEntry(oldStackItems[index]);
+ }
+
+ // Then, switch to a proper key or list index in the (common) collection of a first different entry.
+ const auto& oldStackFirstDifferingItem = oldStackItems[firstDifferingItemIndex];
+ const auto& newStackFirstDifferingItem = newStackItems[firstDifferingItemIndex];
+ YT_VERIFY(
+ std::holds_alternative<TString>(oldStackFirstDifferingItem) ==
+ std::holds_alternative<TString>(newStackFirstDifferingItem));
+
+ if (std::holds_alternative<TString>(oldStackFirstDifferingItem)) {
+ // Switch key to a proper key.
+ Writer_.WriteItemSeparator();
+ Writer_.WriteBinaryString(std::get<TString>(newStackFirstDifferingItem));
+ Writer_.WriteKeyValueSeparator();
+ } else {
+ // If current index and desired index are not adjacent, we must
+ // introduce a proper number of entities between them.
+ auto currentIndex = std::get<int>(oldStackFirstDifferingItem);
+ auto newIndex = std::get<int>(newStackFirstDifferingItem);
+ YT_VERIFY(currentIndex < newIndex);
+ for (auto index = currentIndex; index + 1 < newIndex; ++index) {
+ Writer_.WriteItemSeparator();
+ Writer_.WriteEntity();
+ }
+ Writer_.WriteItemSeparator();
+ }
+
+ // Open all collections that are present in the new stack.
+ for (auto index = firstDifferingItemIndex + 1; index < std::ssize(newStackItems); ++index) {
+ OpenEntry(newStackItems[index]);
+ }
+ }
+
+ //! Handle the last stack closing.
+ void OnAfterLastStack()
+ {
+ // Close all currently open collections. Mind that the first entry is an artificial /0.
+ const auto& oldStackItems = Stack_.Items();
+ for (auto index = std::ssize(oldStackItems) - 1; index > 0; --index) {
+ CloseEntry(oldStackItems[index]);
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+//! A class that uses a sparse YSON builder to construct a YSON consisting of
+//! fragments matching the given YPaths.
+class TYsonFilterer
+{
+public:
+ TYsonFilterer(TSparseYsonBuilder* builder, TYsonStringBuf yson, const std::vector<TYPath>& paths)
+ : Builder_(builder)
+ , Paths_(paths.begin(), paths.end())
+ , Input_(yson.AsStringBuf())
+ , Parser_(&Input_, EYsonType::Node)
+ , Cursor_(&Parser_)
+ {
+ // We pretend that our YSON is enclosed into an outer list, and we are already
+ // in a context of parsing. In particular, we stay behind its zeroth element,
+ // therefore our stack initial value is "/-1".
+ StackLevelIsList_ = {true};
+ Stack_.Push(-1);
+ }
+
+ //! Return either a filtered YSON or an null YSON string if there were no matches.
+ TYsonString Filter()
+ {
+ while (!Cursor_->IsEndOfStream()) {
+ if (ConsumeItemPrologue()) {
+ ConsumeItemEpilogue();
+ } else {
+ FinishComposite();
+ }
+ }
+
+ return Builder_->Flush();
+ }
+
+private:
+ TSparseYsonBuilder* Builder_;
+ THashSet<TYPath> Paths_;
+
+ TMemoryInput Input_;
+ TYsonPullParser Parser_;
+ TYsonPullParserCursor Cursor_;
+
+ TYPathStack Stack_;
+ std::vector<bool> StackLevelIsList_;
+
+ [[noreturn]] void ThrowUnexpectedItemType(TStringBuf stage)
+ {
+ THROW_ERROR_EXCEPTION(
+ "Unexpected item type %Qlv for %v in %v",
+ Cursor_.GetCurrent().GetType(),
+ StackLevelIsList_.back() ? "list" : "map",
+ stage);
+ }
+
+ //! If the current stack corresponds to a requested path, pass the current
+ //! composite to a sparse YSON builder.
+ bool TryExtractComposite()
+ {
+ // Discard the first item of the stack which is always "/0" corresponding
+ // to the position in the artificial enclosing list.
+ constexpr int StaticPrefixLength = 2;
+ const auto& path = TStringBuf(Stack_.GetPath()).substr(StaticPrefixLength);
+ if (Paths_.contains(path)) {
+ Builder_->SwitchStack(Stack_);
+ Cursor_.TransferComplexValue(Builder_->GetWriter());
+ return true;
+ }
+ return false;
+ }
+
+ //! A helper function that starts consuming a composite item. It returns true if
+ //! current composite is not finished, and advances to the beginning of the value
+ //! to consume (i.e. puts the key on stack in case of map and increments the index
+ //! in case of list). Otherwise, it returns false and skips the closing bracket.
+ bool ConsumeItemPrologue()
+ {
+ if (StackLevelIsList_.back()) {
+ switch (Cursor_->GetType()) {
+ case EYsonItemType::EndList:
+ return false;
+ default:
+ // Increase the topmost ypath stack item.
+ Stack_.IncreaseLastIndex();
+ return true;
+ }
+ } else {
+ switch (Cursor_->GetType()) {
+ case EYsonItemType::EndMap:
+ return false;
+ case EYsonItemType::StringValue:
+ // Set the topmost item of a stack to a current key.
+ Stack_.Pop();
+ Stack_.Push(Cursor_->UncheckedAsString());
+ Cursor_.Next();
+ return true;
+ default:
+ ThrowUnexpectedItemType("prologue");
+ }
+ }
+ }
+
+ //! A helper function that finishes consuming a composite item. It either transfers
+ //! a whole value to a sparse YSON builder in case of a match, descends into inner
+ //! composite, or just skips the singular value.
+ void ConsumeItemEpilogue()
+ {
+ if (TryExtractComposite()) {
+ // We successfully extracted a composite, hooray!
+ return;
+ }
+
+ if (Cursor_->GetType() == EYsonItemType::BeginAttributes) {
+ // For now, do not descend into attributes, so just skip them entirely.
+ Cursor_.SkipAttributes();
+ }
+
+ switch (Cursor_->GetType()) {
+ case EYsonItemType::BeginMap:
+ StackLevelIsList_.push_back(false);
+ // Push a placeholder that will be replaced by the actual value of a first key on
+ // the next iteration.
+ Stack_.Push("");
+ Cursor_.Next();
+ break;
+ case EYsonItemType::BeginList:
+ StackLevelIsList_.push_back(true);
+ // Push a placeholder that will be incremented on the next iteration, producing
+ // zero as an index of the first item of the list.
+ Stack_.Push(-1);
+ Cursor_.Next();
+ break;
+ case EYsonItemType::StringValue:
+ case EYsonItemType::BooleanValue:
+ case EYsonItemType::DoubleValue:
+ case EYsonItemType::Int64Value:
+ case EYsonItemType::Uint64Value:
+ case EYsonItemType::EntityValue:
+ // Keeping in mind that maybeExtractComposite() returned false,
+ // this singular value is of no interest to us, so just skip it.
+ Cursor_.Next();
+ break;
+ case EYsonItemType::EndList:
+ case EYsonItemType::EndMap:
+ case EYsonItemType::BeginAttributes:
+ case EYsonItemType::EndAttributes:
+ case EYsonItemType::EndOfStream:
+ // This may happen only in case of incorrect YSON, e.g.
+ // "{42;]", "{foo=}", "<foo=bar><boo=far>42", "[>]" or "[foo".
+ ThrowUnexpectedItemType("epilogue");
+ }
+ }
+
+ //! A helper to move out of a finished composite.
+ void FinishComposite()
+ {
+ StackLevelIsList_.pop_back();
+ Stack_.Pop();
+ Cursor_.Next();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+
+//! Returns either an empty map, empty list or an original scalar value depending
+//! on YSON value type. Topmost attributes are discarded.
+TYsonString FilterYsonStringFallback(TYsonStringBuf yson)
+{
+ TMemoryInput input(yson.AsStringBuf());
+ TYsonPullParser parser(&input, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+
+ // We always drop attributes in this case.
+ if (cursor->GetType() == EYsonItemType::BeginAttributes) {
+ cursor.SkipAttributes();
+ }
+ switch (cursor->GetType()) {
+ case EYsonItemType::BeginList:
+ return TYsonString(TString("[]"));
+ case EYsonItemType::BeginMap:
+ return TYsonString(TString("{}"));
+ case EYsonItemType::StringValue:
+ case EYsonItemType::Int64Value:
+ case EYsonItemType::Uint64Value:
+ case EYsonItemType::DoubleValue:
+ case EYsonItemType::BooleanValue:
+ case EYsonItemType::EntityValue: {
+ // Copy the value to a new YSON string and return it.
+ TString result;
+ TStringOutput output(result);
+ TCheckedInDebugYsonTokenWriter writer(&output);
+ cursor.TransferComplexValue(&writer);
+ writer.Flush();
+ output.Flush();
+ return TYsonString(std::move(result));
+ }
+ default:
+ // This is possible only in case of malformed YSON.
+ THROW_ERROR_EXCEPTION("Unexpected YSON item type %Qlv after attributes", cursor->GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+TYsonString FilterYsonString(
+ const std::vector<TYPath>& paths,
+ TYsonStringBuf yson,
+ bool allowNullResult)
+{
+ TSparseYsonBuilder builder;
+ TYsonFilterer filterer(&builder, yson, paths);
+ if (auto result = filterer.Filter(); result || allowNullResult) {
+ return result;
+ } else {
+ return FilterYsonStringFallback(yson);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/string_filter.h b/yt/yt/core/yson/string_filter.h
new file mode 100644
index 0000000000..7a56a62dc8
--- /dev/null
+++ b/yt/yt/core/yson/string_filter.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "string.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Given a YSON string and a family of YPaths, returns a smallest YSON containing
+//! all subYSONs matching the given paths.
+/*!
+ * Paths not present in YSON are ignored.
+ *
+ * Current implementation does not support YPaths accessing attributes, those
+ * will be ignored.
+ *
+ * Attributes of partially included objects (strictly speaking, of those
+ * that are not explicitly filtered by any YPath) are dropped.
+ *
+ * If no matches are found, behavior is controlled by #allowNullResult flag.
+ * - If #allowNullResult is set, the null YSON string is returned.
+ * - Otherwise, an empty collection of the same type as #yson is returned
+ * (in case of a map or a list), or the original YSON itself (in case of a
+ * scalar type).
+ */
+TYsonString FilterYsonString(
+ const std::vector<NYPath::TYPath>& paths,
+ TYsonStringBuf yson,
+ bool allowNullResult = true);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/string_merger.cpp b/yt/yt/core/yson/string_merger.cpp
new file mode 100644
index 0000000000..4b51277300
--- /dev/null
+++ b/yt/yt/core/yson/string_merger.cpp
@@ -0,0 +1,194 @@
+#include "string_merger.h"
+
+#include "forwarding_consumer.h"
+#include "null_consumer.h"
+
+#include <yt/yt/core/ypath/stack.h>
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+#include <library/cpp/iterator/functools.h>
+
+#include <util/stream/output.h>
+
+namespace NYT::NYson {
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStringMerger
+ : public TForwardingYsonConsumer
+{
+public:
+ //! TYsonStringMerger does not own YSON strings, so ensure that lifetime of YSON strings is long enough.
+ TYsonStringMerger(
+ IOutputStream* stream,
+ std::vector<NYPath::TYPath> paths,
+ std::vector<TYsonStringBuf> ysonStringBufs,
+ EYsonFormat format)
+ : Paths_(std::move(paths))
+ , PathToIndex_(BuildPathToIndex(Paths_))
+ , YsonWriter_(stream, format, EYsonType::Node, /*enableRaw*/ true)
+ {
+ SetYsonStringBufs(std::move(ysonStringBufs));
+ }
+
+ void OnMyStringScalar(TStringBuf /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyInt64Scalar(i64 /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyUint64Scalar(ui64 /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyDoubleScalar(double /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyBooleanScalar(bool /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyEntity() override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyBeginList() override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyListItem() override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyEndList() override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyBeginMap() override
+ {
+ YsonWriter_.OnBeginMap();
+ PathStack_.Push("");
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ YsonWriter_.OnKeyedItem(key);
+ PathStack_.Pop();
+ PathStack_.Push(TString{key});
+ auto path = PathStack_.GetPath();
+ auto it = PathToIndex_.find(path);
+ if (it != PathToIndex_.end()) {
+ YsonWriter_.OnRaw(YsonStringBufs_[it->second].AsStringBuf(), EYsonType::Node);
+ Forward(GetNullYsonConsumer(), [] {});
+ }
+ }
+
+ void OnMyEndMap() override
+ {
+ YsonWriter_.OnEndMap();
+ PathStack_.Pop();
+ }
+
+ void OnMyBeginAttributes() override
+ {
+ YT_ABORT();
+ }
+
+ void OnMyEndAttributes() override
+ {
+ YT_ABORT();
+ }
+
+private:
+ const std::vector<NYPath::TYPath> Paths_;
+ const THashMap<NYPath::TYPath, ui64> PathToIndex_;
+
+ TYsonWriter YsonWriter_;
+ NYPath::TYPathStack PathStack_;
+ std::vector<TYsonStringBuf> YsonStringBufs_;
+
+ static THashMap<NYPath::TYPath, ui64> BuildPathToIndex(const std::vector<NYPath::TYPath>& paths)
+ {
+ THashMap<NYPath::TYPath, ui64> pathToIndex;
+ pathToIndex.reserve(paths.size());
+ for (const auto& [index, path] : Enumerate(paths)) {
+ pathToIndex[path] = index;
+ }
+ return pathToIndex;
+ }
+
+ void SetYsonStringBufs(std::vector<TYsonStringBuf> ysonStringBufs)
+ {
+ YsonStringBufs_ = std::move(ysonStringBufs);
+ for (auto ysonStringBuf : YsonStringBufs_) {
+ if (ysonStringBuf.GetType() != EYsonType::Node) {
+ THROW_ERROR_EXCEPTION(
+ "Yson string type can only be %v",
+ EYsonType::Node);
+ }
+ }
+ }
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonString MergeYsonStrings(
+ std::vector<NYPath::TYPath> paths,
+ std::vector<TYsonStringBuf> ysonStringBufs,
+ EYsonFormat format)
+{
+ YT_VERIFY(paths.size() == ysonStringBufs.size());
+
+ for (const auto& [index, path] : Enumerate(paths)) {
+ const auto& ysonStringBuf = ysonStringBufs[index];
+ if (ysonStringBuf.GetType() != EYsonType::Node) {
+ THROW_ERROR_EXCEPTION(
+ "Yson string type can only be %v",
+ EYsonType::Node);
+ }
+ }
+ auto rootNode = NYTree::GetEphemeralNodeFactory()->CreateMap();
+ for (const auto& [index, path] : Enumerate(paths)) {
+ if (path.empty()) {
+ return TYsonString{ysonStringBufs[index]};
+ }
+ ForceYPath(rootNode, path);
+ NYTree::SetNodeByYPath(rootNode, path, NYTree::GetEphemeralNodeFactory()->CreateMap());
+ }
+ TString result;
+ size_t sizeEstimate = std::accumulate(
+ ysonStringBufs.begin(),
+ ysonStringBufs.end(),
+ static_cast<size_t>(0),
+ [] (size_t accumulated, TYsonStringBuf ysonStringBuf) {
+ return accumulated + ysonStringBuf.AsStringBuf().size();
+ });
+ result.reserve(sizeEstimate);
+ TStringOutput outputStream{result};
+ TYsonStringMerger merger(&outputStream, std::move(paths), std::move(ysonStringBufs), format);
+ NYTree::VisitTree(rootNode, &merger, /*stable*/ true);
+ return TYsonString{result, EYsonType::Node};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/string_merger.h b/yt/yt/core/yson/string_merger.h
new file mode 100644
index 0000000000..e91b858a8e
--- /dev/null
+++ b/yt/yt/core/yson/string_merger.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <library/cpp/yt/yson_string/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Given parts of YSON and their destinations, merges several YSON strings into one.
+//! Faster alternative to casting everything to YSON nodes, merging them and casting
+//! this merged YSON back to YSON string.
+//! YSON strings `ysonStringBufs` can be of any format, but type must be EYsonType::Node.
+//! In a case when one path is prefix of another,
+//! only YSON string corresponding to the shortest path will be used.
+TYsonString MergeYsonStrings(
+ std::vector<NYPath::TYPath> paths,
+ std::vector<TYsonStringBuf> ysonStringBufs,
+ EYsonFormat format = EYsonFormat::Binary);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/syntax_checker-inl.h b/yt/yt/core/yson/syntax_checker-inl.h
new file mode 100644
index 0000000000..9a71610222
--- /dev/null
+++ b/yt/yt/core/yson/syntax_checker-inl.h
@@ -0,0 +1,394 @@
+#ifndef SYNTAX_CHECKER_INL_H_
+#error "Direct inclusion of this file is not allowed, include syntax_checker.h"
+// For the sake of sane code completion.
+#include "syntax_checker.h"
+#endif
+
+namespace NYT::NYson::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonSyntaxChecker::OnSimpleNonstring(EYsonItemType itemType)
+{
+ OnSimple<false>(itemType);
+}
+
+void TYsonSyntaxChecker::OnString()
+{
+ OnSimple<true>(EYsonItemType::StringValue);
+}
+
+void TYsonSyntaxChecker::OnFinish()
+{
+ const auto state = StateStack_.back();
+ switch (state) {
+ case EYsonState::Terminated:
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListFragmentExpectSeparator:
+ case EYsonState::InsideMapFragmentExpectKey:
+ case EYsonState::InsideMapFragmentExpectSeparator:
+ return;
+ default:
+ ThrowUnexpectedToken("finish");
+ }
+}
+
+void TYsonSyntaxChecker::OnEquality()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideMapExpectEquality:
+ StateStack_.back() = EYsonState::InsideMapExpectValue;
+ break;
+ case EYsonState::InsideAttributeMapExpectEquality:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectValue;
+ break;
+ case EYsonState::InsideMapFragmentExpectEquality:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectValue;
+ break;
+ default:
+ ThrowUnexpectedToken("=");
+ }
+}
+
+void TYsonSyntaxChecker::OnSeparator()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideMapFragmentExpectSeparator:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectKey;
+ break;
+ case EYsonState::InsideMapExpectSeparator:
+ StateStack_.back() = EYsonState::InsideMapExpectKey;
+ break;
+ case EYsonState::InsideAttributeMapExpectSeparator:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectKey;
+ break;
+ case EYsonState::InsideListExpectSeparator:
+ StateStack_.back() = EYsonState::InsideListExpectValue;
+ break;
+ case EYsonState::InsideListFragmentExpectSeparator:
+ StateStack_.back() = EYsonState::InsideListFragmentExpectValue;
+ break;
+ default:
+ if (StateStack_.back() == EYsonState::Terminated) {
+ ThrowUnexpectedToken(";", Format("; maybe you should use yson_type = %Qlv", EYsonType::ListFragment));
+ }
+ ThrowUnexpectedToken(";");
+ }
+}
+
+void TYsonSyntaxChecker::OnBeginList()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::ExpectValue:
+ case EYsonState::ExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListExpectValue;
+ break;
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListFragmentExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideListExpectValue);
+ break;
+ case EYsonState::InsideListExpectValue:
+ case EYsonState::InsideListExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideListExpectValue);
+ break;
+ case EYsonState::InsideMapFragmentExpectValue:
+ case EYsonState::InsideMapFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideListExpectValue);
+ break;
+ case EYsonState::InsideMapExpectValue:
+ case EYsonState::InsideMapExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideListExpectValue);
+ break;
+ case EYsonState::InsideAttributeMapExpectAttributelessValue:
+ case EYsonState::InsideAttributeMapExpectValue:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideListExpectValue);
+ break;
+ default:
+ ThrowUnexpectedToken("[");
+ }
+ IncrementNestingLevel();
+}
+
+void TYsonSyntaxChecker::OnEndList()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideListExpectValue:
+ case EYsonState::InsideListExpectSeparator:
+ StateStack_.pop_back();
+ break;
+ default:
+ ThrowUnexpectedToken("]");
+ }
+ DecrementNestingLevel();
+}
+
+void TYsonSyntaxChecker::OnBeginMap()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::ExpectValue:
+ case EYsonState::ExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapExpectKey;
+ break;
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListFragmentExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideMapExpectKey);
+ break;
+ case EYsonState::InsideListExpectValue:
+ case EYsonState::InsideListExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideMapExpectKey);
+ break;
+ case EYsonState::InsideMapFragmentExpectValue:
+ case EYsonState::InsideMapFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideMapExpectKey);
+ break;
+ case EYsonState::InsideMapExpectValue:
+ case EYsonState::InsideMapExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideMapExpectKey);
+ break;
+ case EYsonState::InsideAttributeMapExpectAttributelessValue:
+ case EYsonState::InsideAttributeMapExpectValue:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectSeparator;
+ StateStack_.push_back(EYsonState::InsideMapExpectKey);
+ break;
+ default:
+ ThrowUnexpectedToken("{");
+ }
+ IncrementNestingLevel();
+}
+
+void TYsonSyntaxChecker::OnEndMap()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideMapExpectKey:
+ case EYsonState::InsideMapExpectSeparator:
+ StateStack_.pop_back();
+ break;
+ default:
+ ThrowUnexpectedToken("}");
+ }
+ DecrementNestingLevel();
+}
+
+void TYsonSyntaxChecker::OnAttributesBegin()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::ExpectValue:
+ StateStack_.back() = EYsonState::ExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+ case EYsonState::InsideListFragmentExpectValue:
+ StateStack_.back() = EYsonState::InsideListFragmentExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+ case EYsonState::InsideMapFragmentExpectValue:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+ case EYsonState::InsideMapExpectValue:
+ StateStack_.back() = EYsonState::InsideMapExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+ case EYsonState::InsideAttributeMapExpectValue:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+ case EYsonState::InsideListExpectValue:
+ StateStack_.back() = EYsonState::InsideListExpectAttributelessValue;
+ StateStack_.push_back(EYsonState::InsideAttributeMapExpectKey);
+ break;
+
+ case EYsonState::InsideListFragmentExpectAttributelessValue:
+ case EYsonState::InsideMapFragmentExpectAttributelessValue:
+ case EYsonState::ExpectAttributelessValue:
+ case EYsonState::InsideMapExpectAttributelessValue:
+ case EYsonState::InsideListExpectAttributelessValue:
+ case EYsonState::InsideAttributeMapExpectAttributelessValue:
+ THROW_ERROR_EXCEPTION("Value cannot have several attribute maps");
+ default:
+ ThrowUnexpectedToken("<");
+ }
+ IncrementNestingLevel();
+}
+
+void TYsonSyntaxChecker::OnAttributesEnd()
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideAttributeMapExpectKey:
+ case EYsonState::InsideAttributeMapExpectSeparator:
+ StateStack_.pop_back();
+ break;
+ default:
+ ThrowUnexpectedToken(">");
+ }
+ DecrementNestingLevel();
+}
+
+size_t TYsonSyntaxChecker::GetNestingLevel() const
+{
+ return NestingLevel_;
+}
+
+bool TYsonSyntaxChecker::IsOnValueBoundary(size_t nestingLevel) const
+{
+ if (NestingLevel_ != nestingLevel) {
+ return false;
+ }
+ switch (StateStack_.back()) {
+ case EYsonState::Terminated:
+ case EYsonState::ExpectValue:
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListFragmentExpectSeparator:
+ case EYsonState::InsideMapFragmentExpectEquality:
+ case EYsonState::InsideMapFragmentExpectSeparator:
+ case EYsonState::InsideMapExpectEquality:
+ case EYsonState::InsideMapExpectSeparator:
+ case EYsonState::InsideAttributeMapExpectEquality:
+ case EYsonState::InsideAttributeMapExpectSeparator:
+ case EYsonState::InsideListExpectValue:
+ case EYsonState::InsideListExpectSeparator:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool TYsonSyntaxChecker::IsOnKey() const
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideMapFragmentExpectEquality:
+ case EYsonState::InsideMapExpectEquality:
+ case EYsonState::InsideAttributeMapExpectEquality:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool TYsonSyntaxChecker::IsListSeparator() const
+{
+ switch (StateStack_.back()) {
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListExpectValue:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// bool TYsonSyntaxChecker::IsOnListItemStart(bool isSimple) const
+// {
+// switch (StateStack_.back()) {
+// case EYsonState::InsideListFragmentExpectSeparator:
+// case EYsonState::InsideListExpectSeparator:
+// // Simple list item.
+// return isSimple;
+// case EYsonState::InsideListFragmentExpectValue:
+// case EYsonState::InsideListExpectValue:
+// case EYsonState::InsideMapFragmentExpectKey:
+// case EYsonState::InsideMapExpectKey: {
+// // Possibly composite list item.
+// if (StateStack_.size() < 2) {
+// return false;
+// }
+// auto previousState = StateStack_[StateStack_.size() - 2];
+// return !isSimple && (previousState == EYsonState::InsideListExpectSeparator);
+// }
+// case EYsonState::InsideAttributeMapExpectKey: {
+// // Possibly composite list item.
+// if (StateStack_.size() < 2) {
+// return false;
+// }
+// auto previousState = StateStack_[StateStack_.size() - 2];
+// return (previousState == EYsonState::InsideListExpectAttributelessValue);
+// }
+// default:
+// return false;
+// }
+// }
+
+template <bool isString>
+void TYsonSyntaxChecker::OnSimple(EYsonItemType itemType)
+{
+ switch (StateStack_.back()) {
+ case EYsonState::ExpectValue:
+ case EYsonState::ExpectAttributelessValue:
+ StateStack_.pop_back();
+ break;
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideListFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListFragmentExpectSeparator;
+ break;
+ case EYsonState::InsideListExpectValue:
+ case EYsonState::InsideListExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideListExpectSeparator;
+ break;
+ case EYsonState::InsideMapFragmentExpectKey:
+ if constexpr (isString) {
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectEquality;
+ } else {
+ THROW_ERROR_EXCEPTION("Cannot parse key of map fragment; expected string got %Qv",
+ itemType);
+ }
+ break;
+ case EYsonState::InsideMapFragmentExpectValue:
+ case EYsonState::InsideMapFragmentExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapFragmentExpectSeparator;
+ break;
+ case EYsonState::InsideMapExpectKey:
+ if constexpr (isString) {
+ StateStack_.back() = EYsonState::InsideMapExpectEquality;
+ } else {
+ THROW_ERROR_EXCEPTION("Cannot parse key of map; expected \"string\" got %Qv",
+ itemType);
+ }
+ break;
+ case EYsonState::InsideMapExpectValue:
+ case EYsonState::InsideMapExpectAttributelessValue:
+ StateStack_.back() = EYsonState::InsideMapExpectSeparator;
+ break;
+ case EYsonState::InsideAttributeMapExpectKey:
+ if constexpr (isString) {
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectEquality;
+ } else {
+ THROW_ERROR_EXCEPTION("Cannot parse key of attribute map; expected \"string\" got %Qv",
+ itemType);
+ }
+ break;
+ case EYsonState::InsideAttributeMapExpectAttributelessValue:
+ case EYsonState::InsideAttributeMapExpectValue:
+ StateStack_.back() = EYsonState::InsideAttributeMapExpectSeparator;
+ break;
+ default:
+ ThrowUnexpectedToken("value");
+ }
+}
+
+void TYsonSyntaxChecker::IncrementNestingLevel()
+{
+ ++NestingLevel_;
+ if (NestingLevel_ >= NestingLevelLimit_) {
+ THROW_ERROR_EXCEPTION("Depth limit exceeded while parsing YSON")
+ << TErrorAttribute("limit", NestingLevelLimit_);
+ }
+}
+
+void TYsonSyntaxChecker::DecrementNestingLevel()
+{
+ YT_ASSERT(NestingLevel_ > 0);
+ --NestingLevel_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson::NDetail
+
diff --git a/yt/yt/core/yson/syntax_checker.cpp b/yt/yt/core/yson/syntax_checker.cpp
new file mode 100644
index 0000000000..cba1eac3d8
--- /dev/null
+++ b/yt/yt/core/yson/syntax_checker.cpp
@@ -0,0 +1,80 @@
+#include "syntax_checker.h"
+
+#include <yt/yt/core/ytree/convert.h>
+
+namespace NYT::NYson::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonSyntaxChecker::TYsonSyntaxChecker(EYsonType ysonType, int nestingLevelLimit)
+ : NestingLevelLimit_(static_cast<ui32>(nestingLevelLimit))
+{
+ StateStack_.push_back(EYsonState::Terminated);
+ switch (ysonType) {
+ case EYsonType::Node:
+ StateStack_.push_back(EYsonState::ExpectValue);
+ break;
+ case EYsonType::ListFragment:
+ StateStack_.push_back(EYsonState::InsideListFragmentExpectValue);
+ break;
+ case EYsonType::MapFragment:
+ StateStack_.push_back(EYsonState::InsideMapFragmentExpectKey);
+ break;
+ default:
+ YT_ABORT();
+ }
+}
+
+TStringBuf TYsonSyntaxChecker::StateExpectationString(EYsonState state)
+{
+ switch (state) {
+ case EYsonState::Terminated:
+ return "no further tokens (yson is completed)";
+
+ case EYsonState::ExpectValue:
+ case EYsonState::ExpectAttributelessValue:
+ case EYsonState::InsideListFragmentExpectAttributelessValue:
+ case EYsonState::InsideListFragmentExpectValue:
+ case EYsonState::InsideMapFragmentExpectAttributelessValue:
+ case EYsonState::InsideMapFragmentExpectValue:
+ case EYsonState::InsideMapExpectAttributelessValue:
+ case EYsonState::InsideMapExpectValue:
+ case EYsonState::InsideAttributeMapExpectAttributelessValue:
+ case EYsonState::InsideAttributeMapExpectValue:
+ case EYsonState::InsideListExpectAttributelessValue:
+ case EYsonState::InsideListExpectValue:
+ return "value";
+
+ case EYsonState::InsideMapFragmentExpectKey:
+ case EYsonState::InsideMapExpectKey:
+ return "key";
+ case EYsonState::InsideAttributeMapExpectKey:
+ return "attribute key";
+
+ case EYsonState::InsideMapFragmentExpectEquality:
+ case EYsonState::InsideMapExpectEquality:
+ case EYsonState::InsideAttributeMapExpectEquality:
+ return "=";
+
+ case EYsonState::InsideListFragmentExpectSeparator:
+ case EYsonState::InsideMapFragmentExpectSeparator:
+ case EYsonState::InsideMapExpectSeparator:
+ case EYsonState::InsideListExpectSeparator:
+ case EYsonState::InsideAttributeMapExpectSeparator:
+ return ";";
+ }
+ YT_ABORT();
+}
+
+void TYsonSyntaxChecker::ThrowUnexpectedToken(TStringBuf token, TStringBuf extraMessage)
+{
+ THROW_ERROR_EXCEPTION("Unexpected %Qv, expected %Qv%v",
+ token,
+ StateExpectationString(StateStack_.back()),
+ extraMessage)
+ << TErrorAttribute("yson_parser_state", StateStack_.back());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson::NDetail
diff --git a/yt/yt/core/yson/syntax_checker.h b/yt/yt/core/yson/syntax_checker.h
new file mode 100644
index 0000000000..07fd707084
--- /dev/null
+++ b/yt/yt/core/yson/syntax_checker.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EYsonItemType, ui8,
+ (EndOfStream)
+ (BeginMap)
+ (EndMap)
+ (BeginAttributes)
+ (EndAttributes)
+ (BeginList)
+ (EndList)
+ (EntityValue)
+ (BooleanValue)
+ (Int64Value)
+ (Uint64Value)
+ (DoubleValue)
+ (StringValue)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM_WITH_UNDERLYING_TYPE(EYsonState, ui8,
+ (Terminated)
+ (ExpectValue)
+ (ExpectAttributelessValue)
+
+ // top level list fragment
+ (InsideListFragmentExpectAttributelessValue)
+ (InsideListFragmentExpectValue)
+ (InsideListFragmentExpectSeparator)
+
+ // top level map fragment
+ (InsideMapFragmentExpectKey)
+ (InsideMapFragmentExpectEquality)
+ (InsideMapFragmentExpectAttributelessValue)
+ (InsideMapFragmentExpectValue)
+ (InsideMapFragmentExpectSeparator)
+
+ (InsideMapExpectKey)
+ (InsideMapExpectEquality)
+ (InsideMapExpectAttributelessValue)
+ (InsideMapExpectValue)
+ (InsideMapExpectSeparator)
+
+ (InsideAttributeMapExpectKey)
+ (InsideAttributeMapExpectEquality)
+ (InsideAttributeMapExpectAttributelessValue)
+ (InsideAttributeMapExpectValue)
+ (InsideAttributeMapExpectSeparator)
+
+ (InsideListExpectAttributelessValue)
+ (InsideListExpectValue)
+ (InsideListExpectSeparator)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSyntaxChecker
+{
+public:
+ TYsonSyntaxChecker(EYsonType ysonType, int nestingLevelLimit);
+
+ Y_FORCE_INLINE void OnSimpleNonstring(EYsonItemType itemType);
+ Y_FORCE_INLINE void OnString();
+ Y_FORCE_INLINE void OnFinish();
+ Y_FORCE_INLINE void OnEquality();
+ Y_FORCE_INLINE void OnSeparator();
+ Y_FORCE_INLINE void OnBeginList();
+ Y_FORCE_INLINE void OnEndList();
+ Y_FORCE_INLINE void OnBeginMap();
+ Y_FORCE_INLINE void OnEndMap();
+ Y_FORCE_INLINE void OnAttributesBegin();
+
+ Y_FORCE_INLINE void OnAttributesEnd();
+
+ Y_FORCE_INLINE size_t GetNestingLevel() const;
+ Y_FORCE_INLINE bool IsOnValueBoundary(size_t nestingLevel) const;
+
+ Y_FORCE_INLINE bool IsOnKey() const;
+
+ //
+ // If called right after OnSeparator, tells
+ // whether it is a list separator.
+ Y_FORCE_INLINE bool IsListSeparator() const;
+ // Y_FORCE_INLINE bool IsOnListItemStart(bool isSimple) const;
+
+private:
+ template <bool isString>
+ Y_FORCE_INLINE void OnSimple(EYsonItemType itemType);
+ Y_FORCE_INLINE void IncrementNestingLevel();
+ Y_FORCE_INLINE void DecrementNestingLevel();
+
+ static TStringBuf StateExpectationString(EYsonState state);
+ void ThrowUnexpectedToken(TStringBuf token, TStringBuf extraMessage = {});
+
+private:
+ TCompactVector<EYsonState, 16> StateStack_;
+
+ // We don't use stack size, we compute depth level precisely to be compatible with old yson parser.
+ ui32 NestingLevel_ = 0;
+ ui32 NestingLevelLimit_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define SYNTAX_CHECKER_INL_H_
+#include "syntax_checker-inl.h"
+#undef SYNTAX_CHECKER_INL_H_
diff --git a/yt/yt/core/yson/token.cpp b/yt/yt/core/yson/token.cpp
new file mode 100644
index 0000000000..115a7b6eb3
--- /dev/null
+++ b/yt/yt/core/yson/token.cpp
@@ -0,0 +1,254 @@
+#include "token.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+ETokenType CharToTokenType(char ch)
+{
+ switch (ch) {
+ case ';': return ETokenType::Semicolon;
+ case '=': return ETokenType::Equals;
+ case '{': return ETokenType::LeftBrace;
+ case '}': return ETokenType::RightBrace;
+ case '#': return ETokenType::Hash;
+ case '[': return ETokenType::LeftBracket;
+ case ']': return ETokenType::RightBracket;
+ case '<': return ETokenType::LeftAngle;
+ case '>': return ETokenType::RightAngle;
+ case '(': return ETokenType::LeftParenthesis;
+ case ')': return ETokenType::RightParenthesis;
+ case '+': return ETokenType::Plus;
+ case ':': return ETokenType::Colon;
+ case ',': return ETokenType::Comma;
+ case '/': return ETokenType::Slash;
+ default: return ETokenType::EndOfStream;
+ }
+}
+
+char TokenTypeToChar(ETokenType type)
+{
+ switch (type) {
+ case ETokenType::Semicolon: return ';';
+ case ETokenType::Equals: return '=';
+ case ETokenType::Hash: return '#';
+ case ETokenType::LeftBracket: return '[';
+ case ETokenType::RightBracket: return ']';
+ case ETokenType::LeftBrace: return '{';
+ case ETokenType::RightBrace: return '}';
+ case ETokenType::LeftAngle: return '<';
+ case ETokenType::RightAngle: return '>';
+ case ETokenType::LeftParenthesis: return '(';
+ case ETokenType::RightParenthesis: return ')';
+ case ETokenType::Plus: return '+';
+ case ETokenType::Colon: return ':';
+ case ETokenType::Comma: return ',';
+ case ETokenType::Slash: return '/';
+ default: YT_ABORT();
+ }
+}
+
+TString TokenTypeToString(ETokenType type)
+{
+ return TString(1, TokenTypeToChar(type));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TToken TToken::EndOfStream;
+
+TToken::TToken()
+ : Type_(ETokenType::EndOfStream)
+ , Int64Value_(0)
+ , Uint64Value_(0)
+ , DoubleValue_(0.0)
+ , BooleanValue_(false)
+{ }
+
+TToken::TToken(ETokenType type)
+ : Type_(type)
+ , Int64Value_(0)
+ , Uint64Value_(0)
+ , DoubleValue_(0.0)
+ , BooleanValue_(false)
+{
+ switch (type) {
+ case ETokenType::String:
+ case ETokenType::Int64:
+ case ETokenType::Uint64:
+ case ETokenType::Double:
+ case ETokenType::Boolean:
+ YT_ABORT();
+ default:
+ break;
+ }
+}
+
+TToken::TToken(TStringBuf stringValue)
+ : Type_(ETokenType::String)
+ , StringValue_(stringValue)
+ , Int64Value_(0)
+ , Uint64Value_(0)
+ , DoubleValue_(0.0)
+ , BooleanValue_(false)
+{ }
+
+TToken::TToken(i64 int64Value)
+ : Type_(ETokenType::Int64)
+ , Int64Value_(int64Value)
+ , Uint64Value_(0)
+ , DoubleValue_(0.0)
+{ }
+
+TToken::TToken(ui64 uint64Value)
+ : Type_(ETokenType::Uint64)
+ , Int64Value_(0)
+ , Uint64Value_(uint64Value)
+ , DoubleValue_(0.0)
+ , BooleanValue_(false)
+{ }
+
+TToken::TToken(double doubleValue)
+ : Type_(ETokenType::Double)
+ , Int64Value_(0)
+ , Uint64Value_(0)
+ , DoubleValue_(doubleValue)
+ , BooleanValue_(false)
+{ }
+
+TToken::TToken(bool booleanValue)
+ : Type_(ETokenType::Boolean)
+ , Int64Value_(0)
+ , DoubleValue_(0.0)
+ , BooleanValue_(booleanValue)
+{ }
+
+bool TToken::IsEmpty() const
+{
+ return Type_ == ETokenType::EndOfStream;
+}
+
+TStringBuf TToken::GetStringValue() const
+{
+ ExpectType(ETokenType::String);
+ return StringValue_;
+}
+
+i64 TToken::GetInt64Value() const
+{
+ ExpectType(ETokenType::Int64);
+ return Int64Value_;
+}
+
+ui64 TToken::GetUint64Value() const
+{
+ ExpectType(ETokenType::Uint64);
+ return Uint64Value_;
+}
+
+double TToken::GetDoubleValue() const
+{
+ ExpectType(ETokenType::Double);
+ return DoubleValue_;
+}
+
+bool TToken::GetBooleanValue() const
+{
+ ExpectType(ETokenType::Boolean);
+ return BooleanValue_;
+}
+
+void TToken::ExpectTypes(const std::vector<ETokenType>& expectedTypes) const
+{
+ if (expectedTypes.size() == 1) {
+ ExpectType(expectedTypes.front());
+ } else if (std::find(expectedTypes.begin(), expectedTypes.end(), Type_) == expectedTypes.end()) {
+ auto typeStrings = ConvertToStrings(expectedTypes, [] (TStringBuilderBase* builder, ETokenType type) {
+ builder->AppendFormat("Qlv", type);
+ });
+ auto typesString = JoinToString(
+ expectedTypes,
+ [] (TStringBuilderBase* builder, ETokenType type) {
+ builder->AppendFormat("%Qlv", type);
+ },
+ TStringBuf(" or "));
+ if (Type_ == ETokenType::EndOfStream) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream; expected types are %v",
+ typesString);
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected token %Qv of type %Qlv; expected types are %v",
+ *this,
+ Type_,
+ typesString);
+ }
+ }
+}
+
+void TToken::ExpectType(ETokenType expectedType) const
+{
+ if (Type_ != expectedType) {
+ if (Type_ == ETokenType::EndOfStream) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream; expected type is %Qlv",
+ expectedType);
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected token %Qv of type %Qlv; expected type is %Qlv",
+ *this,
+ Type_,
+ expectedType);
+ }
+ }
+}
+
+
+void TToken::ThrowUnexpected() const
+{
+ if (Type_ == ETokenType::EndOfStream) {
+ THROW_ERROR_EXCEPTION("Unexpected end of stream");
+ } else {
+ THROW_ERROR_EXCEPTION("Unexpected token %Qv of type %Qlv",
+ *this,
+ Type_);
+ }
+}
+
+void TToken::Reset()
+{
+ Type_ = ETokenType::EndOfStream;
+ Int64Value_ = 0;
+ Uint64Value_ = 0;
+ DoubleValue_ = 0.0;
+ StringValue_ = TStringBuf();
+ BooleanValue_ = false;
+}
+
+TString ToString(const TToken& token)
+{
+ switch (token.GetType()) {
+ case ETokenType::EndOfStream:
+ return TString();
+
+ case ETokenType::String:
+ return TString(token.GetStringValue());
+
+ case ETokenType::Int64:
+ return ::ToString(token.GetInt64Value());
+
+ case ETokenType::Uint64:
+ return ::ToString(token.GetUint64Value());
+
+ case ETokenType::Double:
+ return ::ToString(token.GetDoubleValue());
+
+ case ETokenType::Boolean:
+ return TString(FormatBool(token.GetBooleanValue()));
+
+ default:
+ return TokenTypeToString(token.GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/token.h b/yt/yt/core/yson/token.h
new file mode 100644
index 0000000000..e2ab6ef68a
--- /dev/null
+++ b/yt/yt/core/yson/token.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/property.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETokenType,
+ (EndOfStream) // Empty or uninitialized token
+
+ (String)
+ (Int64)
+ (Uint64)
+ (Double)
+ (Boolean)
+
+ // Special values:
+ // YSON
+ (Semicolon) // ;
+ (Equals) // =
+ (Hash) // #
+ (LeftBracket) // [
+ (RightBracket) // ]
+ (LeftBrace) // {
+ (RightBrace) // }
+ (LeftAngle) // <
+ (RightAngle) // >
+ // Table ranges
+ (LeftParenthesis) // (
+ (RightParenthesis) // )
+ (Plus) // +
+ (Colon) // :
+ (Comma) // ,
+ // YPath
+ (Slash) // /
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+ETokenType CharToTokenType(char ch); // returns ETokenType::EndOfStream for non-special chars
+char TokenTypeToChar(ETokenType type); // YT_ABORT for non-special types
+TString TokenTypeToString(ETokenType type); // YT_ABORT for non-special types
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TToken
+{
+public:
+ static const TToken EndOfStream;
+
+ TToken();
+ TToken(ETokenType type); // for special types
+ explicit TToken(TStringBuf stringValue); // for string values
+ explicit TToken(i64 int64Value); // for int64 values
+ explicit TToken(ui64 int64Value); // for uint64 values
+ explicit TToken(double doubleValue); // for double values
+ explicit TToken(bool booleanValue); // for booleans
+
+ DEFINE_BYVAL_RO_PROPERTY(ETokenType, Type);
+
+ bool IsEmpty() const;
+ TStringBuf GetStringValue() const;
+ i64 GetInt64Value() const;
+ ui64 GetUint64Value() const;
+ double GetDoubleValue() const;
+ bool GetBooleanValue() const;
+
+ void ExpectType(ETokenType expectedType) const;
+ void ExpectTypes(const std::vector<ETokenType>& expectedTypes) const;
+ void ThrowUnexpected() const;
+
+ void Reset();
+
+private:
+ TStringBuf StringValue_;
+ i64 Int64Value_;
+ ui64 Uint64Value_;
+ double DoubleValue_;
+ bool BooleanValue_;
+};
+
+TString ToString(const TToken& token);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/token_writer-inl.h b/yt/yt/core/yson/token_writer-inl.h
new file mode 100644
index 0000000000..78adfbc800
--- /dev/null
+++ b/yt/yt/core/yson/token_writer-inl.h
@@ -0,0 +1,127 @@
+#ifndef TOKEN_WRITER_INL_H_
+#error "Direct inclusion of this file is not allowed, include token_writer.h"
+// For the sake of sane code completion.
+#include "token_writer.h"
+#endif
+
+#include "detail.h"
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <util/generic/typetraits.h>
+#include <util/string/escape.h>
+
+#include <cctype>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TUncheckedYsonTokenWriter::Flush()
+{
+ if (Y_LIKELY(Writer_->RemainingBytes() > 0)) {
+ Writer_->UndoRemaining();
+ }
+}
+
+template <typename T>
+void TUncheckedYsonTokenWriter::WriteSimple(T value)
+{
+ Writer_->Write(&value, sizeof(value));
+}
+
+template <typename T>
+void TUncheckedYsonTokenWriter::WriteVarInt(T value)
+{
+ NYT::WriteVarInt(Writer_, value);
+}
+
+void TUncheckedYsonTokenWriter::WriteBinaryBoolean(bool value)
+{
+ WriteSimple(value ? NDetail::TrueMarker : NDetail::FalseMarker);
+}
+
+void TUncheckedYsonTokenWriter::WriteBinaryInt64(i64 value)
+{
+ WriteSimple(NDetail::Int64Marker);
+ WriteVarInt<i64>(value);
+}
+
+void TUncheckedYsonTokenWriter::WriteBinaryUint64(ui64 value)
+{
+ WriteSimple(NDetail::Uint64Marker);
+ WriteVarInt<ui64>(value);
+}
+
+void TUncheckedYsonTokenWriter::WriteBinaryDouble(double value)
+{
+ WriteSimple(NDetail::DoubleMarker);
+ WriteSimple(value);
+}
+
+void TUncheckedYsonTokenWriter::WriteBinaryString(TStringBuf value)
+{
+ WriteSimple(NDetail::StringMarker);
+ WriteVarInt<i32>(value.length());
+ Writer_->Write(value.begin(), value.size());
+}
+
+void TUncheckedYsonTokenWriter::WriteEntity()
+{
+ WriteSimple(NDetail::EntitySymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteBeginMap()
+{
+ WriteSimple(NDetail::BeginMapSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteEndMap()
+{
+ WriteSimple(NDetail::EndMapSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteBeginAttributes()
+{
+ WriteSimple(NDetail::BeginAttributesSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteEndAttributes()
+{
+ WriteSimple(NDetail::EndAttributesSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteBeginList()
+{
+ WriteSimple(NDetail::BeginListSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteEndList()
+{
+ WriteSimple(NDetail::EndListSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteItemSeparator()
+{
+ WriteSimple(NDetail::ItemSeparatorSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteKeyValueSeparator()
+{
+ WriteSimple(NDetail::KeyValueSeparatorSymbol);
+}
+
+void TUncheckedYsonTokenWriter::WriteSpace(char value)
+{
+ YT_ASSERT(std::isspace(value));
+ WriteSimple(value);
+}
+
+void TUncheckedYsonTokenWriter::Finish()
+{
+ Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/token_writer.cpp b/yt/yt/core/yson/token_writer.cpp
new file mode 100644
index 0000000000..be28b55de9
--- /dev/null
+++ b/yt/yt/core/yson/token_writer.cpp
@@ -0,0 +1,250 @@
+#include "token_writer.h"
+
+#include <cmath>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+size_t FloatToStringWithNanInf(double value, char* buf, size_t size)
+{
+ if (std::isfinite(value)) {
+ return FloatToString(value, buf, size);
+ }
+
+ static const TStringBuf nanLiteral = "%nan";
+ static const TStringBuf infLiteral = "%inf";
+ static const TStringBuf negativeInfLiteral = "%-inf";
+
+ TStringBuf str;
+ if (std::isnan(value)) {
+ str = nanLiteral;
+ } else if (std::isinf(value) && value > 0) {
+ str = infLiteral;
+ } else {
+ str = negativeInfLiteral;
+ }
+ YT_VERIFY(str.size() + 1 <= size);
+ ::memcpy(buf, str.data(), str.size() + 1);
+ return str.size();
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TUncheckedYsonTokenWriter::TUncheckedYsonTokenWriter(
+ IZeroCopyOutput* output,
+ EYsonType /*type*/,
+ int /*nestingLevelLimit*/)
+ : WriterHolder_(output)
+ , Writer_(&*WriterHolder_)
+{ }
+
+TUncheckedYsonTokenWriter::TUncheckedYsonTokenWriter(
+ TZeroCopyOutputStreamWriter* writer,
+ EYsonType /*type*/,
+ int /*nestingLevelLimit*/)
+ : Writer_(writer)
+{ }
+
+void TUncheckedYsonTokenWriter::WriteTextBoolean(bool value)
+{
+ auto res = value ? TStringBuf("%true") : TStringBuf("%false");
+ Writer_->Write(res.data(), res.size());
+}
+
+void TUncheckedYsonTokenWriter::WriteTextInt64(i64 value)
+{
+ auto res = ::ToString(value);
+ Writer_->Write(res.data(), res.size());
+}
+
+void TUncheckedYsonTokenWriter::WriteTextUint64(ui64 value)
+{
+ auto res = ::ToString(value);
+ Writer_->Write(res.data(), res.size());
+ WriteSimple('u');
+}
+
+void TUncheckedYsonTokenWriter::WriteTextDouble(double value)
+{
+ char buf[256];
+ auto str = TStringBuf(buf, FloatToStringWithNanInf(value, buf, sizeof(buf)));
+ Writer_->Write(str.data(), str.size());
+ if (str.find('.') == TString::npos && str.find('e') == TString::npos && std::isfinite(value)) {
+ WriteSimple('.');
+ }
+}
+
+void TUncheckedYsonTokenWriter::WriteTextString(TStringBuf value)
+{
+ WriteSimple('"');
+ auto res = EscapeC(value.data(), value.length());
+ Writer_->Write(res.data(), res.length());
+ WriteSimple('"');
+}
+
+void TUncheckedYsonTokenWriter::WriteRawNodeUnchecked(TStringBuf value)
+{
+ Writer_->Write(value.data(), value.size());
+}
+
+ui64 TUncheckedYsonTokenWriter::GetTotalWrittenSize() const
+{
+ return Writer_->GetTotalWrittenSize();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCheckedYsonTokenWriter::TCheckedYsonTokenWriter(IZeroCopyOutput* writer, EYsonType type, int nestingLevelLimit)
+ : Checker_(type, nestingLevelLimit)
+ , UncheckedWriter_(writer, type)
+{ }
+
+TCheckedYsonTokenWriter::TCheckedYsonTokenWriter(TZeroCopyOutputStreamWriter* writer, EYsonType type, int nestingLevelLimit)
+ : Checker_(type, nestingLevelLimit)
+ , UncheckedWriter_(writer, type)
+{ }
+
+void TCheckedYsonTokenWriter::Flush()
+{
+ UncheckedWriter_.Flush();
+}
+
+void TCheckedYsonTokenWriter::WriteTextBoolean(bool value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ UncheckedWriter_.WriteTextBoolean(value);
+}
+
+void TCheckedYsonTokenWriter::WriteBinaryBoolean(bool value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::BooleanValue);
+ UncheckedWriter_.WriteBinaryBoolean(value);
+}
+
+void TCheckedYsonTokenWriter::WriteTextInt64(i64 value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::Int64Value);
+ UncheckedWriter_.WriteTextInt64(value);
+}
+
+void TCheckedYsonTokenWriter::WriteBinaryInt64(i64 value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::Int64Value);
+ UncheckedWriter_.WriteBinaryInt64(value);
+}
+
+void TCheckedYsonTokenWriter::WriteTextUint64(ui64 value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::Uint64Value);
+ UncheckedWriter_.WriteTextUint64(value);
+}
+
+void TCheckedYsonTokenWriter::WriteBinaryUint64(ui64 value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::Uint64Value);
+ UncheckedWriter_.WriteBinaryUint64(value);
+}
+
+void TCheckedYsonTokenWriter::WriteTextDouble(double value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ UncheckedWriter_.WriteTextDouble(value);
+}
+
+void TCheckedYsonTokenWriter::WriteBinaryDouble(double value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::DoubleValue);
+ UncheckedWriter_.WriteBinaryDouble(value);
+}
+
+void TCheckedYsonTokenWriter::WriteTextString(TStringBuf value)
+{
+ Checker_.OnString();
+ UncheckedWriter_.WriteTextString(value);
+}
+
+void TCheckedYsonTokenWriter::WriteBinaryString(TStringBuf value)
+{
+ Checker_.OnString();
+ UncheckedWriter_.WriteBinaryString(value);
+}
+
+void TCheckedYsonTokenWriter::WriteEntity()
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ UncheckedWriter_.WriteEntity();
+}
+
+void TCheckedYsonTokenWriter::WriteBeginMap()
+{
+ Checker_.OnBeginMap();
+ UncheckedWriter_.WriteBeginMap();
+}
+
+void TCheckedYsonTokenWriter::WriteEndMap()
+{
+ Checker_.OnEndMap();
+ UncheckedWriter_.WriteEndMap();
+}
+
+void TCheckedYsonTokenWriter::WriteBeginAttributes()
+{
+ Checker_.OnAttributesBegin();
+ UncheckedWriter_.WriteBeginAttributes();
+}
+
+void TCheckedYsonTokenWriter::WriteEndAttributes()
+{
+ Checker_.OnAttributesEnd();
+ UncheckedWriter_.WriteEndAttributes();
+}
+
+void TCheckedYsonTokenWriter::WriteBeginList()
+{
+ Checker_.OnBeginList();
+ UncheckedWriter_.WriteBeginList();
+}
+
+void TCheckedYsonTokenWriter::WriteEndList()
+{
+ Checker_.OnEndList();
+ UncheckedWriter_.WriteEndList();
+}
+
+void TCheckedYsonTokenWriter::WriteItemSeparator()
+{
+ Checker_.OnSeparator();
+ UncheckedWriter_.WriteItemSeparator();
+}
+
+void TCheckedYsonTokenWriter::WriteKeyValueSeparator()
+{
+ Checker_.OnEquality();
+ UncheckedWriter_.WriteKeyValueSeparator();
+}
+
+void TCheckedYsonTokenWriter::WriteSpace(char value)
+{
+ UncheckedWriter_.WriteSpace(value);
+}
+
+void TCheckedYsonTokenWriter::Finish()
+{
+ Checker_.OnFinish();
+ UncheckedWriter_.Finish();
+}
+
+void TCheckedYsonTokenWriter::WriteRawNodeUnchecked(TStringBuf value)
+{
+ Checker_.OnSimpleNonstring(EYsonItemType::EntityValue);
+ UncheckedWriter_.WriteRawNodeUnchecked(value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/token_writer.h b/yt/yt/core/yson/token_writer.h
new file mode 100644
index 0000000000..769f8e42ea
--- /dev/null
+++ b/yt/yt/core/yson/token_writer.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include "public.h"
+
+#include "syntax_checker.h"
+
+#include <yt/yt/core/misc/zerocopy_output_writer.h>
+
+#include <util/stream/zerocopy_output.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TUncheckedYsonTokenWriter
+{
+public:
+ explicit TUncheckedYsonTokenWriter(
+ IZeroCopyOutput* output,
+ EYsonType type = EYsonType::Node,
+ int nestingLevelLimit = DefaultYsonParserNestingLevelLimit);
+ explicit TUncheckedYsonTokenWriter(
+ TZeroCopyOutputStreamWriter* writer,
+ EYsonType type = EYsonType::Node,
+ int nestingLevelLimit = DefaultYsonParserNestingLevelLimit);
+
+ void WriteTextBoolean(bool value);
+ void WriteTextInt64(i64 value);
+ void WriteTextUint64(ui64 value);
+ void WriteTextDouble(double value);
+ void WriteTextString(TStringBuf value);
+
+ Y_FORCE_INLINE void WriteBinaryBoolean(bool value);
+ Y_FORCE_INLINE void WriteBinaryInt64(i64 value);
+ Y_FORCE_INLINE void WriteBinaryUint64(ui64 value);
+ Y_FORCE_INLINE void WriteBinaryDouble(double value);
+ Y_FORCE_INLINE void WriteBinaryString(TStringBuf value);
+ Y_FORCE_INLINE void WriteEntity();
+
+ Y_FORCE_INLINE void WriteBeginMap();
+ Y_FORCE_INLINE void WriteEndMap();
+ Y_FORCE_INLINE void WriteBeginAttributes();
+ Y_FORCE_INLINE void WriteEndAttributes();
+ Y_FORCE_INLINE void WriteBeginList();
+ Y_FORCE_INLINE void WriteEndList();
+
+ Y_FORCE_INLINE void WriteItemSeparator();
+ Y_FORCE_INLINE void WriteKeyValueSeparator();
+
+ Y_FORCE_INLINE void WriteSpace(char value);
+
+ void WriteRawNodeUnchecked(TStringBuf value);
+
+ Y_FORCE_INLINE void Flush();
+ Y_FORCE_INLINE void Finish();
+
+ ui64 GetTotalWrittenSize() const;
+
+private:
+ template <typename T>
+ Y_FORCE_INLINE void WriteSimple(T value);
+
+ template <typename T>
+ Y_FORCE_INLINE void WriteVarInt(T value);
+
+private:
+ std::optional<TZeroCopyOutputStreamWriter> WriterHolder_;
+ TZeroCopyOutputStreamWriter* Writer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCheckedYsonTokenWriter
+{
+public:
+ explicit TCheckedYsonTokenWriter(
+ IZeroCopyOutput* writer,
+ EYsonType type = EYsonType::Node,
+ int nestingLevelLimit = DefaultYsonParserNestingLevelLimit);
+ explicit TCheckedYsonTokenWriter(
+ TZeroCopyOutputStreamWriter* writer,
+ EYsonType type = EYsonType::Node,
+ int nestingLevelLimit = DefaultYsonParserNestingLevelLimit);
+
+ void WriteTextBoolean(bool value);
+ void WriteBinaryBoolean(bool value);
+ void WriteTextInt64(i64 value);
+ void WriteBinaryInt64(i64 value);
+ void WriteTextUint64(ui64 value);
+ void WriteBinaryUint64(ui64 value);
+ void WriteTextDouble(double value);
+ void WriteBinaryDouble(double value);
+ void WriteTextString(TStringBuf value);
+ void WriteBinaryString(TStringBuf value);
+ void WriteEntity();
+
+ void WriteBeginMap();
+ void WriteEndMap();
+ void WriteBeginAttributes();
+ void WriteEndAttributes();
+ void WriteBeginList();
+ void WriteEndList();
+
+ void WriteItemSeparator();
+ void WriteKeyValueSeparator();
+
+ void WriteSpace(char value);
+
+ void WriteRawNodeUnchecked(TStringBuf value);
+
+ void Flush();
+ void Finish();
+
+private:
+ NDetail::TYsonSyntaxChecker Checker_;
+ TUncheckedYsonTokenWriter UncheckedWriter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
+#define TOKEN_WRITER_INL_H_
+#include "token_writer-inl.h"
+#undef TOKEN_WRITER_INL_H_
diff --git a/yt/yt/core/yson/tokenizer.cpp b/yt/yt/core/yson/tokenizer.cpp
new file mode 100644
index 0000000000..91fe61ba3f
--- /dev/null
+++ b/yt/yt/core/yson/tokenizer.cpp
@@ -0,0 +1,49 @@
+#include "tokenizer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTokenizer::TTokenizer(TStringBuf input)
+ : Input(input)
+ , Parsed(0)
+ , Position(0)
+{ }
+
+bool TTokenizer::ParseNext()
+{
+ Input = Input.Tail(Parsed);
+ Token.Reset();
+ Parsed = Lexer.GetToken(Input, &Token);
+ Position += Parsed;
+ return !CurrentToken().IsEmpty();
+}
+
+const TToken& TTokenizer::CurrentToken() const
+{
+ return Token;
+}
+
+ETokenType TTokenizer::GetCurrentType() const
+{
+ return CurrentToken().GetType();
+}
+
+TStringBuf TTokenizer::GetCurrentSuffix() const
+{
+ return Input.Tail(Parsed);
+}
+
+TStringBuf TTokenizer::CurrentInput() const
+{
+ return Input;
+}
+
+size_t TTokenizer::GetPosition() const
+{
+ return Position;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/tokenizer.h b/yt/yt/core/yson/tokenizer.h
new file mode 100644
index 0000000000..bc78d92fbd
--- /dev/null
+++ b/yt/yt/core/yson/tokenizer.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "public.h"
+#include "lexer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTokenizer
+{
+public:
+ explicit TTokenizer(TStringBuf input);
+
+ bool ParseNext();
+ const TToken& CurrentToken() const;
+ ETokenType GetCurrentType() const;
+ TStringBuf GetCurrentSuffix() const;
+ TStringBuf CurrentInput() const;
+ size_t GetPosition() const;
+
+private:
+ TStringBuf Input;
+ TToken Token;
+ TStatelessLexer Lexer;
+ size_t Parsed;
+ size_t Position;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/depth_limiting_yson_consumer_ut.cpp b/yt/yt/core/yson/unittests/depth_limiting_yson_consumer_ut.cpp
new file mode 100644
index 0000000000..dcf58ba547
--- /dev/null
+++ b/yt/yt/core/yson/unittests/depth_limiting_yson_consumer_ut.cpp
@@ -0,0 +1,359 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/depth_limiting_yson_consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYson {
+namespace {
+
+using namespace NYTree;
+
+using ::testing::StrictMock;
+using ::testing::InSequence;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TDepthLimitingYsonConsumerTest
+ : public ::testing::Test
+{
+protected:
+ StrictMock<TMockYsonConsumer> MockConsumer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TDepthLimitingYsonConsumerTest, NegativeDepthLimit)
+{
+ EXPECT_THROW(CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ -1), std::exception);
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, ZeroDepth)
+{
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 0);
+ consumer->OnStringScalar("value");
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, MapTruncated)
+{
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 1);
+
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+
+ BuildYsonFluently(consumer.get())
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2").BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .Item("subkey2").Value("subvalue2")
+ .Item("subkey3").Value("subvalue3")
+ .EndMap()
+ .EndMap();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, NestedMapTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 2);
+
+ EXPECT_CALL(MockConsumer_, OnBeginMap());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key2"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndMap());
+
+ BuildYsonFluently(consumer.get())
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2").BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .Item("subkey2").Value("subvalue2")
+ .Item("subkey4").Value("subvalue3")
+ .EndMap()
+ .EndMap();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, ListTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 1);
+
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+
+ BuildYsonFluently(consumer.get())
+ .BeginList()
+ .Item().Value("value1")
+ .Item().BeginList()
+ .Item().Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().Value("subvalue3")
+ .EndList()
+ .EndList();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, NestedListTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 2);
+
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndList());
+
+ BuildYsonFluently(consumer.get())
+ .BeginList()
+ .Item().Value("value1")
+ .Item().BeginList()
+ .Item().Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().Value("subvalue3")
+ .EndList()
+ .EndList();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, AttributesTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 1);
+
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value"));
+
+ BuildYsonFluently(consumer.get())
+ .BeginAttributes()
+ .Item("key1").Value("value1")
+ .Item("key2").BeginList()
+ .Item().Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().Value("subvalue3")
+ .EndList()
+ .EndAttributes()
+ .Value("value");
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, NestedAttributesTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 2);
+
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key2"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value"));
+
+ BuildYsonFluently(consumer.get())
+ .BeginAttributes()
+ .Item("key1").Value("value1")
+ .Item("key2").BeginList()
+ .Item().Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().Value("subvalue3")
+ .EndList()
+ .EndAttributes()
+ .Value("value");
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, ComplexYsonTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 3);
+
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key1"));
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("attrkey1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("attrvalue1"));
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key2"));
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue1"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue2"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndList());
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnBeginMap());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key3"));
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnEntity());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key4"));
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnDoubleScalar(1.0));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnDoubleScalar(2.0));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndList());
+ EXPECT_CALL(MockConsumer_, OnEndMap());
+
+ BuildYsonFluently(consumer.get())
+ .BeginAttributes()
+ .Item("key1")
+ .BeginAttributes()
+ .Item("attrkey1").Value("attrvalue1")
+ .EndAttributes()
+ .Value("value1")
+ .Item("key2").BeginList()
+ .Item().BeginAttributes()
+ .Item("subattrkey1").Value("subattrvalue1")
+ .EndAttributes()
+ .Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().BeginMap()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .BeginMap()
+ .Item("key3").BeginAttributes()
+ .EndAttributes()
+ .Entity()
+ .Item("key4").BeginList()
+ .Item().Value(1.0)
+ .Item().Value(2.0)
+ .Item().BeginMap()
+ .Item("subkey3").Value("subvalue3")
+ .EndMap()
+ .EndList()
+ .EndMap();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, ComplexYsonNotTruncated)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 4);
+
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key1"));
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("attrkey1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("attrvalue1"));
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key2"));
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("subattrkey1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subattrvalue1"));
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue1"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue2"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnBeginMap());
+ EXPECT_CALL(MockConsumer_, OnEndMap());
+ EXPECT_CALL(MockConsumer_, OnEndList());
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnBeginMap());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key3"));
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnEntity());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key4"));
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnDoubleScalar(1.0));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnDoubleScalar(2.0));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnBeginMap());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("subkey3"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue3"));
+ EXPECT_CALL(MockConsumer_, OnEndMap());
+ EXPECT_CALL(MockConsumer_, OnEndList());
+ EXPECT_CALL(MockConsumer_, OnEndMap());
+
+ BuildYsonFluently(consumer.get())
+ .BeginAttributes()
+ .Item("key1")
+ .BeginAttributes()
+ .Item("attrkey1").Value("attrvalue1")
+ .EndAttributes()
+ .Value("value1")
+ .Item("key2").BeginList()
+ .Item().BeginAttributes()
+ .Item("subattrkey1").Value("subattrvalue1")
+ .EndAttributes()
+ .Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().BeginMap()
+ .EndMap()
+ .EndList()
+ .EndAttributes()
+ .BeginMap()
+ .Item("key3").BeginAttributes()
+ .EndAttributes()
+ .Entity()
+ .Item("key4").BeginList()
+ .Item().Value(1.0)
+ .Item().Value(2.0)
+ .Item().BeginMap()
+ .Item("subkey3").Value("subvalue3")
+ .EndMap()
+ .EndList()
+ .EndMap();
+}
+
+TEST_F(TDepthLimitingYsonConsumerTest, MapFragment)
+{
+ InSequence dummy;
+
+ auto consumer = CreateDepthLimitingYsonConsumer(&MockConsumer_, /*depthLimit*/ 2, EYsonType::MapFragment);
+
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key1"));
+ EXPECT_CALL(MockConsumer_, OnBeginAttributes());
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("attrkey1"));
+ EXPECT_CALL(MockConsumer_, OnStringScalar("attrvalue1"));
+ EXPECT_CALL(MockConsumer_, OnEndAttributes());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("value1"));
+ EXPECT_CALL(MockConsumer_, OnKeyedItem("key2"));
+ EXPECT_CALL(MockConsumer_, OnBeginList());
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue1"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("subvalue2"));
+ EXPECT_CALL(MockConsumer_, OnListItem());
+ EXPECT_CALL(MockConsumer_, OnStringScalar("<truncated>"));
+ EXPECT_CALL(MockConsumer_, OnEndList());
+
+ BuildYsonMapFragmentFluently(consumer.get())
+ .Item("key1")
+ .BeginAttributes()
+ .Item("attrkey1").Value("attrvalue1")
+ .EndAttributes()
+ .Value("value1")
+ .Item("key2").BeginList()
+ .Item().BeginAttributes()
+ .Item("subattrkey1").Value("subattrvalue1")
+ .EndAttributes()
+ .Value("subvalue1")
+ .Item().Value("subvalue2")
+ .Item().BeginMap()
+ .EndMap()
+ .EndList();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/unittests/filter_ut.cpp b/yt/yt/core/yson/unittests/filter_ut.cpp
new file mode 100644
index 0000000000..50976c832f
--- /dev/null
+++ b/yt/yt/core/yson/unittests/filter_ut.cpp
@@ -0,0 +1,176 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/string_filter.h>
+
+#include <library/cpp/iterator/functools.h>
+
+namespace NYT::NYson {
+namespace {
+
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString Prettify(const TString& yson)
+{
+ return ConvertToYsonString(TYsonString(yson), EYsonFormat::Pretty).ToString();
+}
+
+TString PrettifyYson(const TYsonString& yson)
+{
+ return ConvertToYsonString(yson, EYsonFormat::Pretty).ToString();
+};
+
+void Check(const TString& yson, const std::vector<TYPath>& paths, const TString& expectedYson)
+{
+ auto originalYsonPretty = Prettify(yson);
+ auto expectedYsonPretty = Prettify(expectedYson);
+ auto actualYsonPretty = PrettifyYson(FilterYsonString(paths, TYsonStringBuf(yson), /*allowNullResult*/ false));
+ EXPECT_EQ(expectedYsonPretty, actualYsonPretty)
+ << "Original:" << std::endl << originalYsonPretty << std::endl
+ << "Filter by: " << Format("%v", paths) << std::endl
+ << "Expected:" << std::endl << expectedYsonPretty << std::endl
+ << "Actual:" << std::endl << actualYsonPretty << std::endl;
+}
+
+void CheckThrow(const TString& yson)
+{
+ EXPECT_THROW(FilterYsonString({}, TYsonStringBuf(yson)), std::exception);
+}
+
+void CheckNull(const TString& yson, const std::vector<TYPath>& paths)
+{
+ auto originalYsonPretty = Prettify(yson);
+ auto actualYson = FilterYsonString(paths, TYsonStringBuf(yson), /*allowNullResult*/ true);
+ EXPECT_FALSE(actualYson)
+ << "Original:" << std::endl << originalYsonPretty << std::endl
+ << "Filter by: " << Format("%v", paths) << std::endl
+ << "Expected: null YSON string" << std::endl
+ << "Actual:" << std::endl << PrettifyYson(actualYson) << std::endl;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const std::vector<TString> ScalarYsons = {"#", "%true", "-42", "23u", "42.5", "xyz"};
+const std::vector<TString> CompositeYsons = {"{foo=bar}", "[foo;bar]"};
+
+TEST(TStringFilterTest, TestNoMatchFallback)
+{
+ // In a no match case attribute-stripped original YSON must be returned.
+
+ // Cases when original YSON is scalar.
+ for (const TString& yson : ScalarYsons) {
+ auto ysonWithAttributes = "<foo=bar>" + yson;
+ Check(yson, {}, yson);
+ Check(ysonWithAttributes, {}, yson);
+ Check(yson, {"/a"}, yson);
+ Check(ysonWithAttributes, {"/a"}, yson);
+ Check(yson, {"/0"}, yson);
+ Check(ysonWithAttributes, {"/0"}, yson);
+ }
+
+ // Cases when original YSON is a map or list.
+ for (const auto& [yson, emptyCollection] : std::vector<std::pair<TString, TString>>{{"{foo=bar}", "{}"}, {"[foo; bar]", "[]"}}) {
+ auto ysonWithAttributes = "<foo=bar>" + yson;
+ Check(yson, {}, emptyCollection);
+ Check(ysonWithAttributes, {}, emptyCollection);
+ Check(yson, {"/a"}, emptyCollection);
+ Check(ysonWithAttributes, {"/a"}, emptyCollection);
+ Check(yson, {"/42"}, emptyCollection);
+ Check(ysonWithAttributes, {"/42"}, emptyCollection);
+ }
+}
+
+TEST(TStringFilterTest, TestNoMatchNullString)
+{
+ // In a no match case attribute-stripped original YSON must be returned.
+
+ // Cases when original YSON is scalar.
+ for (const TString& yson : Concatenate(ScalarYsons, CompositeYsons)) {
+ auto ysonWithAttributes = "<foo=bar>" + yson;
+ CheckNull(yson, {});
+ CheckNull(ysonWithAttributes, {});
+ CheckNull(yson, {"/a"});
+ CheckNull(ysonWithAttributes, {"/a"});
+ CheckNull(yson, {"/42"});
+ CheckNull(ysonWithAttributes, {"/42"});
+ }
+}
+
+TEST(TStringFilterTest, TestFullMatch)
+{
+ // In a match case original YSON must be returned.
+ for (const TString& yson : ScalarYsons) {
+ auto ysonWithAttributes = "<foo=bar>" + yson;
+ Check(yson, {""}, yson);
+ Check(ysonWithAttributes, {""}, ysonWithAttributes);
+ }
+
+ for (const TString& yson : {"{foo=bar}", "[foo;bar]"}) {
+ auto ysonWithAttributes = "<foo=bar>" + yson;
+ Check(yson, {""}, yson);
+ Check(ysonWithAttributes, {""}, ysonWithAttributes);
+ }
+}
+
+TEST(TStringFilterTest, TestSomeManualCases)
+{
+ Check("{a=1;b={foo=bar};c=2}", {"/b"}, "{b={foo=bar}}");
+ Check("{a=1;b={foo=bar};c=2}", {"/b/foo"}, "{b={foo=bar}}");
+ Check("{a=1;b={foo=bar};c=2}", {"/b/foo/oof"}, "{}");
+ Check("{a=1;b={foo=bar};c=2}", {"/b/fuu"}, "{}");
+ Check("{a=1;b={foo=bar};c=2}", {"/a", "/c"}, "{a=1;c=2}");
+ Check("{a=1;b={foo=bar};c=2}", {"/a", "/b"}, "{a=1;b={foo=bar}}");
+ Check("{a=1;b={foo=bar};c=2}", {"/a", "/b", "/c"}, "{a=1;b={foo=bar};c=2}");
+ Check("{a=1;b={foo=bar};c=2}", {"/a", "/b/foo"}, "{a=1;b={foo=bar}}");
+}
+
+TEST(TStringFilterTest, TestSomeOtherManualCases)
+{
+ Check(
+ "[{foo=0;bar=1};{foo=2;bar=3};{foo=4;bar=5};{foo=6;bar=7};{foo=8;bar=9}]",
+ {"/0/foo", "/2/bar", "/4/foo"},
+ "[{foo=0};#;{bar=5};#;{foo=8}]");
+ Check(
+ "[{foo=0;bar=1};{foo=2;bar=3};{foo=4;bar=5};{foo=6;bar=7};{foo=8;bar=9}]",
+ {"/0", "/2/bar", "/4"},
+ "[{foo=0;bar=1};#;{bar=5};#;{foo=8;bar=9}]");
+ Check(
+ "[{foo=0;bar=1};{foo=2;bar=3};{foo=4;bar=5};{foo=6;bar=7};{foo=8;bar=9}]",
+ {"/0", "/2/bar", "/4"},
+ "[{foo=0;bar=1};#;{bar=5};#;{foo=8;bar=9}]");
+ Check(
+ "[{foo=0;bar=1};{foo=2;bar=3};{foo=4;bar=5};{foo=6;bar=7};{foo=8;bar=9}]",
+ {"/1", "/3/bar"},
+ "[#;{foo=2;bar=3};#;{bar=7}]");
+}
+
+TEST(TStringFilterTest, TestAttributes)
+{
+ // Attributes of matched subYSONs are preserved.
+ Check("<foo=bar>{a=<fuu=bor>42}", {""}, "<foo=bar>{a=<fuu=bor>42}");
+ // Attributes of partially taken composites are dropped.
+ Check("<foo=bar>{a=<fuu=bor>42}", {"/a"}, "{a=<fuu=bor>42}");
+ // Paths with attributes are ignored (at least in the current implementation).
+ Check("<foo=bar>{a=<fuu=bor>42}", {"/a/@fuu"}, "{}");
+}
+
+TEST(TStringFilterTest, TestIncorrectYson)
+{
+ // In case of incorrect YSON we at least must not crash. We do not intend
+ // to test all possible structures of incorrect YSONs here, but let us check
+ // some of them.
+ for (const auto& yson : {
+ "{", "[", "}", "]", "{a=1;b=2", "[1;2", "1;2]", "1;2", "<><>42", "{]", "[}",
+ "{foo}", "{foo=}", "foo<", "", "[>]"
+ }) {
+ CheckThrow(yson);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/lexer_ut.cpp b/yt/yt/core/yson/unittests/lexer_ut.cpp
new file mode 100644
index 0000000000..471735aa3d
--- /dev/null
+++ b/yt/yt/core/yson/unittests/lexer_ut.cpp
@@ -0,0 +1,189 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/lexer.h>
+
+namespace NYT::NYson {
+namespace {
+
+using ::ToString;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStatelessLexerTest
+ : public ::testing::Test
+{
+public:
+ std::unique_ptr<TStatelessLexer> Lexer;
+
+ void SetUp() override
+ {
+ Reset();
+ }
+
+ void Reset()
+ {
+ Lexer.reset(new TStatelessLexer());
+ }
+
+ void TestConsume(TStringBuf input)
+ {
+ TToken token;
+ Lexer->GetToken(input, &token);
+ }
+
+ TToken GetToken(TStringBuf input)
+ {
+ TToken token;
+ Lexer->GetToken(input, &token);
+ return token;
+ }
+
+ void TestToken(TStringBuf input, ETokenType expectedType, const TString& expectedValue)
+ {
+ auto token = GetToken(input);
+ EXPECT_EQ(expectedType, token.GetType());
+ EXPECT_EQ(expectedValue, ToString(token));
+ Reset();
+ }
+
+ void TestDouble(TStringBuf input, double expectedValue)
+ {
+ auto token = GetToken(input);
+ EXPECT_EQ(ETokenType::Double, token.GetType());
+ EXPECT_DOUBLE_EQ(expectedValue, token.GetDoubleValue());
+ Reset();
+ }
+
+ void TestSpecialValue(TStringBuf input, ETokenType expectedType)
+ {
+ auto token = GetToken(input);
+ EXPECT_EQ(expectedType, token.GetType());
+ EXPECT_EQ(input, ToString(token));
+ Reset();
+ }
+
+ void TestIncorrectFinish(TStringBuf input)
+ {
+ EXPECT_THROW(TestConsume(input), std::exception);
+ Reset();
+ }
+
+ void TestIncorrectInput(TStringBuf input)
+ {
+ EXPECT_THROW(TestConsume(input), std::exception);
+ Reset();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TStatelessLexerTest, StringValues)
+{
+ TestToken("abc_123.-%", ETokenType::String, "abc_123.-%");
+ TestToken("_", ETokenType::String, "_");
+
+ TestToken("\"abc_123\"", ETokenType::String, "abc_123");
+ TestToken("\" abc_123\\t\\\\\\\"\"", ETokenType::String, " abc_123\t\\\"");
+ TestToken("\"\\x01\\x02\\x03\\x04\"", ETokenType::String, "\x01\x02\x03\x04");
+
+ TestToken(TString("\x01\x00", 2), ETokenType::String, "");
+ TestToken("\x01\x08\x01\x02\x03\x04", ETokenType::String, "\x01\x02\x03\x04");
+
+ TestToken("nanka", ETokenType::String, "nanka");
+ TestToken("infka", ETokenType::String, "infka");
+}
+
+TEST_F(TStatelessLexerTest, Int64Values)
+{
+ TestToken("123", ETokenType::Int64, "123");
+ TestToken("0", ETokenType::Int64, "0");
+ TestToken("+1", ETokenType::Int64, "1");
+ TestToken("-1", ETokenType::Int64, "-1");
+
+ TestToken(TString("\x02\x00", 2), ETokenType::Int64, "0");
+ TestToken("\x02\x01", ETokenType::Int64, "-1");
+ TestToken("\x02\x02", ETokenType::Int64, "1");
+ TestToken("\x02\x03", ETokenType::Int64, "-2");
+ TestToken("\x02\x04", ETokenType::Int64, "2");
+ TestToken("\x02\x80\x80\x80\x02", ETokenType::Int64, ToString(1ull << 21));
+}
+
+TEST_F(TStatelessLexerTest, Uint64Values)
+{
+ TestToken("123u", ETokenType::Uint64, "123");
+ TestToken("0u", ETokenType::Uint64, "0");
+
+ TestToken(TString("\x06\x00", 2), ETokenType::Uint64, "0");
+ TestToken("\x06\x02", ETokenType::Uint64, "2");
+ TestToken("\x06\x04", ETokenType::Uint64, "4");
+ TestToken("\x06\x80\x80\x80\x02", ETokenType::Uint64, ToString(1ull << 22));
+}
+
+TEST_F(TStatelessLexerTest, DoubleValues)
+{
+ const double x = 3.1415926;
+ TestDouble("3.1415926", x);
+ TestDouble("0.31415926e+1", x);
+ TestDouble("31415926e-7", x);
+ TestDouble(TString('\x03') + TString((const char*) &x, sizeof(x)), x);
+
+ TestDouble("%inf", std::numeric_limits<double>::infinity());
+ TestDouble("%+inf", std::numeric_limits<double>::infinity());
+ TestDouble("%-inf", -std::numeric_limits<double>::infinity());
+
+ EXPECT_TRUE(std::isnan(GetToken("%nan").GetDoubleValue()));
+}
+
+TEST_F(TStatelessLexerTest, SpecialValues)
+{
+ TestSpecialValue(";", ETokenType::Semicolon);
+ TestSpecialValue("=", ETokenType::Equals);
+ TestSpecialValue("[", ETokenType::LeftBracket);
+ TestSpecialValue("]", ETokenType::RightBracket);
+ TestSpecialValue("{", ETokenType::LeftBrace);
+ TestSpecialValue("}", ETokenType::RightBrace);
+ TestSpecialValue("<", ETokenType::LeftAngle);
+ TestSpecialValue(">", ETokenType::RightAngle);
+ TestSpecialValue("(", ETokenType::LeftParenthesis);
+ TestSpecialValue(")", ETokenType::RightParenthesis);
+ TestSpecialValue("#", ETokenType::Hash);
+ TestSpecialValue("+", ETokenType::Plus);
+ TestSpecialValue(":", ETokenType::Colon);
+ TestSpecialValue(",", ETokenType::Comma);
+}
+
+TEST_F(TStatelessLexerTest, IncorrectChars)
+{
+ TestIncorrectInput("\x01\x03"); // Binary string with negative length
+
+ TestIncorrectInput("1a"); // Alpha after numeric
+ TestIncorrectInput("1.1e-1a"); // Alpha after numeric
+
+ TestIncorrectInput("-nan"); // nan literal without % (plus would be OK for lexer)
+ TestIncorrectInput("-inf"); // inf literal without %
+
+ // Unknown symbols
+ TestIncorrectInput(".");
+ TestIncorrectInput("|");
+ TestIncorrectInput("\\");
+ TestIncorrectInput("?");
+ TestIncorrectInput("'");
+ TestIncorrectInput("`");
+ TestIncorrectInput("$");
+}
+
+TEST_F(TStatelessLexerTest, IncorrectFinish)
+{
+ TestIncorrectFinish("\"abc"); // no matching quote
+ TestIncorrectFinish("\"abc\\\""); // no matching quote (\" is escaped quote)
+ TestIncorrectFinish("\x01"); // binary string without length
+ TestIncorrectFinish("\x01\x06YT"); // binary string shorter than the specified length
+ TestIncorrectFinish("\x02\x80\x80"); // unfinished varint
+ TestIncorrectFinish("\x03\x01\x01\x01\x01\x01\x01\x01"); // binary double too short
+ TestIncorrectFinish("-"); // numeric not finished
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/yson/unittests/proto/protobuf_yson_casing_ut.proto b/yt/yt/core/yson/unittests/proto/protobuf_yson_casing_ut.proto
new file mode 100644
index 0000000000..627d6e23ad
--- /dev/null
+++ b/yt/yt/core/yson/unittests/proto/protobuf_yson_casing_ut.proto
@@ -0,0 +1,12 @@
+package NYT.NYson.NProto;
+
+import "yt_proto/yt/core/yson/proto/protobuf_interop.proto";
+
+option (NYT.NYson.NProto.derive_underscore_case_names) = true;
+
+message TCamelCaseStyleMessage
+{
+ optional int32 SomeField = 1;
+ optional int32 AnotherField123 = 2;
+ optional int32 Crazy_Field = 3;
+}
diff --git a/yt/yt/core/yson/unittests/proto/protobuf_yson_ut.proto b/yt/yt/core/yson/unittests/proto/protobuf_yson_ut.proto
new file mode 100644
index 0000000000..6ccc8efe90
--- /dev/null
+++ b/yt/yt/core/yson/unittests/proto/protobuf_yson_ut.proto
@@ -0,0 +1,167 @@
+package NYT.NYson.NProto;
+
+import "yt_proto/yt/core/misc/proto/guid.proto";
+import "yt_proto/yt/core/misc/proto/protobuf_helpers.proto";
+import "yt_proto/yt/core/yson/proto/protobuf_interop.proto";
+import "yt_proto/yt/core/ytree/proto/attributes.proto";
+
+enum EColor
+{
+ Color_Green = -1 [(NYT.NYson.NProto.enum_value_name) = "green"];
+ Color_Red = 2 [(NYT.NYson.NProto.enum_value_name) = "red"];
+ Color_Blue = 3 [(NYT.NYson.NProto.enum_value_name) = "blue"];
+}
+
+enum EFlag
+{
+ option allow_alias = true;
+
+ Flag_False = 0 [(NYT.NYson.NProto.enum_value_name) = "false"];
+ Flag_No = 0 [(NYT.NYson.NProto.enum_value_name) = "no"];
+ Flag_True = 1 [(NYT.NYson.NProto.enum_value_name) = "true"];
+ Flag_Yes = 1 [(NYT.NYson.NProto.enum_value_name) = "yes"];
+}
+
+message TNestedMessage
+{
+ optional int32 int32_field = 1;
+ optional EColor color = 19;
+ optional EFlag flag = 20;
+ optional TNestedMessage nested_message = 2;
+ repeated int32 repeated_int32_field = 100;
+ map<string, TNestedMessage> nested_message_map = 3 [(NYT.NYson.NProto.yson_map) = true];
+}
+
+message TMessage
+{
+ optional int32 int32_field_xxx = 1
+ [
+ (NYT.NYson.NProto.field_name) = "int32_field",
+ (NYT.NYson.NProto.field_name_alias) = "int32_field_alias1",
+ (NYT.NYson.NProto.field_name_alias) = "int32_field_alias2"
+ ];
+ optional uint32 uint32_field = 2;
+ optional sint32 sint32_field = 3;
+ optional int64 int64_field = 4;
+ optional uint64 uint64_field = 5;
+ optional sint64 sint64_field = 6;
+ optional fixed32 fixed32_field = 7;
+ optional fixed64 fixed64_field = 8;
+ optional sfixed32 sfixed32_field = 9;
+ optional sfixed64 sfixed64_field = 10;
+ optional bool bool_field = 11;
+ optional string string_field = 12;
+ optional float float_field = 13;
+ optional double double_field = 14;
+ optional TNestedMessage nested_message1 = 15;
+ optional TNestedMessage nested_message2 = 16;
+ repeated int32 repeated_int32_field = 17;
+ repeated TNestedMessage repeated_nested_message1 = 18;
+ repeated TNestedMessage repeated_nested_message2 = 21;
+ optional NYT.NYTree.NProto.TAttributeDictionary attributes = 19;
+ optional bytes yson_field = 20 [(NYT.NYson.NProto.yson_string) = true];
+ map<string, TNestedMessage> nested_message_map = 22 [(NYT.NYson.NProto.yson_map) = true];
+ map<string, int32> string_to_int32_map = 23 [(NYT.NYson.NProto.yson_map) = true];
+ map<int32, int32> int32_to_int32_map = 24 [(NYT.NYson.NProto.yson_map) = true];
+ optional TNestedMessageWithCustomConverter nested_message_with_custom_converter = 25;
+ repeated TNestedMessageWithCustomConverter repeated_nested_message_with_custom_converter = 26;
+ map<int32, TNestedMessageWithCustomConverter> int32_to_nested_message_with_custom_converter_map = 27 [(NYT.NYson.NProto.yson_map) = true];
+ optional NYT.NProto.TGuid guid = 28;
+ optional bytes bytes_with_custom_converter = 29;
+ repeated bytes repeated_bytes_with_custom_converter = 30;
+ optional NYT.NProto.TExtensionSet extensions = 31;
+
+ extensions 100 to 199;
+}
+
+message TMessageExtension
+{
+ extend TMessage
+ {
+ optional TMessageExtension extension = 123;
+ }
+
+ optional uint32 extension_field = 1;
+
+ extensions 200 to 299;
+}
+
+message TMessageExtensionExtension
+{
+ extend TMessageExtension
+ {
+ optional TMessageExtensionExtension extension_extension = 234;
+ }
+
+ optional uint32 extension_extension_field = 1;
+}
+
+message TMessageExt
+{
+ optional int32 x = 1;
+}
+
+message TNestedMessageWithRequiredFields
+{
+ optional int32 optional_field = 1;
+ required int32 required_field = 2;
+}
+
+message TMessageWithRequiredFields
+{
+ optional int32 optional_field = 1;
+ required int32 required_field = 2;
+ repeated TNestedMessageWithRequiredFields nested_messages = 3;
+}
+
+message TMessageWithReservedFields
+{
+ reserved 99 to 101;
+ reserved "reserved_field1";
+ reserved "reserved_field2";
+ reserved "reserved_field3";
+}
+
+message TExtensibleMessage
+{
+ message TSubmessage
+ {
+ optional int32 known_int = 1;
+ optional string known_string = 2;
+ }
+
+ optional string known_string = 1;
+ repeated TSubmessage known_submessages = 2;
+ optional TSubmessage known_submessage = 3;
+}
+
+message TMessageWithRequiredAnnotation
+{
+ optional string required_string = 1 [(NYT.NYson.NProto.required) = true];
+}
+
+message TNestedMessageWithCustomConverter
+{
+ optional int32 x = 1;
+ optional int32 y = 2;
+}
+
+message TPackedRepeatedMessage
+{
+ enum EEnum {
+ VALUE0 = 0;
+ VALUE1 = 1;
+ };
+ repeated int32 int32_rep = 1 [packed = true];
+ repeated uint32 uint32_rep = 2 [packed = true];
+ repeated int64 int64_rep = 3 [packed = true];
+ repeated uint64 uint64_rep = 4 [packed = true];
+ repeated float float_rep = 5 [packed = true];
+ repeated double double_rep = 6 [packed = true];
+ repeated fixed32 fixed32_rep = 7 [packed = true];
+ repeated fixed64 fixed64_rep = 8 [packed = true];
+ repeated sfixed32 sfixed32_rep = 9 [packed = true];
+ repeated sfixed64 sfixed64_rep = 10 [packed = true];
+ repeated EEnum enum_rep = 11 [packed = true];
+
+}
diff --git a/yt/yt/core/yson/unittests/protobuf_yson_ut.cpp b/yt/yt/core/yson/unittests/protobuf_yson_ut.cpp
new file mode 100644
index 0000000000..d579940132
--- /dev/null
+++ b/yt/yt/core/yson/unittests/protobuf_yson_ut.cpp
@@ -0,0 +1,2585 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/unittests/proto/protobuf_yson_ut.pb.h>
+#include <yt/yt/core/yson/unittests/proto/protobuf_yson_casing_ut.pb.h>
+
+#include <yt/yt/core/yson/protobuf_interop.h>
+#include <yt/yt/core/yson/null_consumer.h>
+#include <yt/yt/core/yson/string_merger.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+#include <google/protobuf/wire_format.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_PROTO_EXTENSION(NYT::NYson::NProto::TMessageExt, 12345)
+REGISTER_PROTO_EXTENSION(NYT::NYson::NProto::TNestedMessageWithCustomConverter, 12345, ext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+namespace NYT::NYson::NProto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using TError = NYT::NProto::TError;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+namespace NYT::NYson {
+namespace {
+
+using namespace NYTree;
+using namespace NYPath;
+using namespace ::google::protobuf::io;
+using namespace ::google::protobuf::internal;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TNestedMessageWithCustomConverter
+{
+ int X;
+ int Y;
+};
+
+void FromProto(TNestedMessageWithCustomConverter* message, const NYT::NYson::NProto::TNestedMessageWithCustomConverter& protoMessage)
+{
+ message->X = protoMessage.x();
+ message->Y = protoMessage.y();
+}
+
+void ToProto(NYT::NYson::NProto::TNestedMessageWithCustomConverter* protoMessage, const TNestedMessageWithCustomConverter& message)
+{
+ protoMessage->set_x(message.X);
+ protoMessage->set_y(message.Y);
+}
+
+void Serialize(const TNestedMessageWithCustomConverter& message, NYson::IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("x").Value(message.X + 1)
+ .Item("y").Value(message.Y + 1)
+ .EndMap();
+}
+
+void Deserialize(TNestedMessageWithCustomConverter& message, NYTree::INodePtr node)
+{
+ auto mapNode = node->AsMap();
+ message.X = mapNode->GetChildOrThrow("x")->AsInt64()->GetValue() - 1;
+ message.Y = mapNode->GetChildOrThrow("y")->AsInt64()->GetValue() - 1;
+}
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_REPRESENTATION(NYT::NYson::NProto::TNestedMessageWithCustomConverter, TNestedMessageWithCustomConverter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBytesIntermediateRepresentation
+{
+ int X;
+};
+
+void FromBytes(TBytesIntermediateRepresentation* value, TStringBuf bytes)
+{
+ value->X = FromString<int>(bytes);
+}
+
+void ToBytes(TString* bytes, const TBytesIntermediateRepresentation& value)
+{
+ *bytes = ToString(value.X);
+}
+
+void Serialize(const TBytesIntermediateRepresentation& value, IYsonConsumer* consumer)
+{
+ BuildYsonFluently(consumer).BeginMap()
+ .Item("x").Value(value.X + 1)
+ .EndMap();
+}
+
+void Deserialize(TBytesIntermediateRepresentation& value, NYTree::INodePtr node)
+{
+ auto mapNode = node->AsMap();
+ value.X = mapNode->GetChildOrThrow("x")->AsInt64()->GetValue() - 1;
+}
+
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(NYT::NYson::NProto::TMessage, 29, TBytesIntermediateRepresentation)
+REGISTER_INTERMEDIATE_PROTO_INTEROP_BYTES_FIELD_REPRESENTATION(NYT::NYson::NProto::TMessage, 30, TBytesIntermediateRepresentation)
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString ToHex(const TString& data)
+{
+ TStringBuilder builder;
+ for (char ch : data) {
+ builder.AppendFormat("%x%x ",
+ static_cast<unsigned char>(ch) >> 4,
+ static_cast<unsigned char>(ch) & 0xf);
+ }
+ return builder.Flush();
+}
+
+#define EXPECT_YPATH(body, ypath) \
+ do { \
+ bool thrown = false; \
+ try body \
+ catch (const TErrorException& ex) { \
+ thrown = true; \
+ Cerr << ToString(ex.Error()) << Endl; \
+ EXPECT_EQ(ypath, ex.Error().Attributes().Get<TYPath>("ypath")); \
+ } \
+ EXPECT_TRUE(thrown); \
+ } while (false);
+
+#define TEST_PROLOGUE_WITH_OPTIONS(type, options) \
+ TString str; \
+ StringOutputStream output(&str); \
+ auto protobufWriter = CreateProtobufWriter(&output, ReflectProtobufMessageType<NYT::NYson::NProto::type>(), options); \
+ BuildYsonFluently(protobufWriter.get())
+
+#define TEST_PROLOGUE(type) \
+ TEST_PROLOGUE_WITH_OPTIONS(type, TProtobufWriterOptions())
+
+#define TEST_EPILOGUE(type) \
+ Cerr << ToHex(str) << Endl; \
+ NYT::NYson::NProto::type message; \
+ EXPECT_TRUE(message.ParseFromArray(str.data(), str.length()));
+
+TEST(TYsonToProtobufYsonTest, Success)
+{
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(10000)
+ .Item("uint32_field").Value(10000U)
+ .Item("sint32_field").Value(10000)
+ .Item("int64_field").Value(10000)
+ .Item("uint64_field").Value(10000U)
+ .Item("fixed32_field").Value(10000U)
+ .Item("fixed64_field").Value(10000U)
+ .Item("sfixed32_field").Value(-10000)
+ .Item("sfixed64_field").Value(10000)
+ .Item("bool_field").Value(true)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(123)
+ .Item("color").Value("blue")
+ .Item("flag").Value("yes")
+ .Item("nested_message").BeginMap()
+ .Item("color").Value("green")
+ .Item("flag").Value("true")
+ .Item("nested_message").BeginMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("nested_message2").BeginMap()
+ .EndMap()
+ .Item("string_field").Value("hello")
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(654)
+ .EndMap()
+ .EndList()
+ .Item("float_field").Value(3.14)
+ .Item("double_field").Value(3.14)
+ .Item("attributes").BeginMap()
+ .Item("k1").Value(1)
+ .Item("k2").Value("test")
+ .Item("k3").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item("yson_field").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").BeginList()
+ .Item().Value("foobar")
+ .EndList()
+ .EndMap()
+ .Item("string_to_int32_map").BeginMap()
+ .Item("hello").Value(0)
+ .Item("world").Value(1)
+ .EndMap()
+ .Item("int32_to_int32_map").BeginMap()
+ .Item("100").Value(0)
+ .Item("-200").Value(1)
+ .EndMap()
+ .Item("nested_message_map").BeginMap()
+ .Item("hello").BeginMap()
+ .Item("int32_field").Value(123)
+ .EndMap()
+ .Item("world").BeginMap()
+ .Item("color").Value("blue")
+ .Item("nested_message_map").BeginMap()
+ .Item("test").BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("extension").BeginMap()
+ .Item("extension_extension").BeginMap()
+ .Item("extension_extension_field").Value(23U)
+ .EndMap()
+ .Item("extension_field").Value(12U)
+ .EndMap()
+ .Item("nested_message_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .Item("y").Value(101)
+ .EndMap()
+ .Item("repeated_nested_message_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(13)
+ .Item("y").Value(24)
+ .EndMap()
+ .EndList()
+ .Item("int32_to_nested_message_with_custom_converter_map").BeginMap()
+ .Item("123").BeginMap()
+ .Item("x").Value(7)
+ .Item("y").Value(8)
+ .EndMap()
+ .EndMap()
+ .Item("guid").Value("0-deadbeef-0-abacaba")
+ .Item("bytes_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .EndMap()
+ .Item("repeated_bytes_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(124)
+ .EndMap()
+ .EndList()
+ .Item("extensions").BeginMap()
+ .Item("ext").BeginMap()
+ .Item("x").Value(42)
+ .EndMap()
+ .Item("smth").BeginMap()
+ .Item("foo").Value("bar")
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(10000, message.int32_field_xxx());
+ EXPECT_EQ(10000U, message.uint32_field());
+ EXPECT_EQ(10000, message.sint32_field());
+ EXPECT_EQ(10000, message.int64_field());
+ EXPECT_EQ(10000U, message.uint64_field());
+ EXPECT_EQ(10000U, message.fixed32_field());
+ EXPECT_EQ(10000U, message.fixed64_field());
+ EXPECT_EQ(-10000, message.sfixed32_field());
+ EXPECT_EQ(10000, message.sfixed64_field());
+ EXPECT_TRUE(message.bool_field());
+ EXPECT_EQ("hello", message.string_field());
+ EXPECT_FLOAT_EQ(3.14, message.float_field());
+ EXPECT_DOUBLE_EQ(3.14, message.double_field());
+
+ EXPECT_TRUE(message.has_nested_message1());
+ EXPECT_EQ(123, message.nested_message1().int32_field());
+ EXPECT_EQ(NYT::NYson::NProto::EColor::Color_Blue, message.nested_message1().color());
+ EXPECT_EQ(NYT::NYson::NProto::EFlag::Flag_True, message.nested_message1().flag());
+ EXPECT_TRUE(message.nested_message1().has_nested_message());
+ EXPECT_FALSE(message.nested_message1().nested_message().has_int32_field());
+ EXPECT_EQ(NYT::NYson::NProto::EColor::Color_Green, message.nested_message1().nested_message().color());
+ EXPECT_EQ(NYT::NYson::NProto::EFlag::Flag_True, message.nested_message1().nested_message().flag());
+ EXPECT_TRUE(message.nested_message1().nested_message().has_nested_message());
+ EXPECT_FALSE(message.nested_message1().nested_message().nested_message().has_nested_message());
+ EXPECT_FALSE(message.nested_message1().nested_message().nested_message().has_int32_field());
+
+ EXPECT_TRUE(message.has_nested_message2());
+ EXPECT_FALSE(message.nested_message2().has_int32_field());
+ EXPECT_FALSE(message.nested_message2().has_nested_message());
+
+ EXPECT_EQ(3, message.repeated_int32_field().size());
+ EXPECT_EQ(1, message.repeated_int32_field().Get(0));
+ EXPECT_EQ(2, message.repeated_int32_field().Get(1));
+ EXPECT_EQ(3, message.repeated_int32_field().Get(2));
+
+ EXPECT_EQ(2, message.repeated_nested_message1().size());
+ EXPECT_EQ(456, message.repeated_nested_message1().Get(0).int32_field());
+ EXPECT_EQ(654, message.repeated_nested_message1().Get(1).int32_field());
+
+ EXPECT_EQ(3, message.attributes().attributes_size());
+ EXPECT_EQ("k1", message.attributes().attributes(0).key());
+ EXPECT_EQ(ConvertToYsonString(1).ToString(), message.attributes().attributes(0).value());
+ EXPECT_EQ("k2", message.attributes().attributes(1).key());
+ EXPECT_EQ(ConvertToYsonString("test").ToString(), message.attributes().attributes(1).value());
+ EXPECT_EQ("k3", message.attributes().attributes(2).key());
+ EXPECT_EQ(ConvertToYsonString(std::vector<int>{1, 2, 3}).ToString(), message.attributes().attributes(2).value());
+
+ auto node = BuildYsonNodeFluently().BeginMap()
+ .Item("a").Value(1)
+ .Item("b").BeginList()
+ .Item().Value("foobar")
+ .EndList()
+ .EndMap();
+
+ EXPECT_EQ(ConvertToYsonString(node).ToString(), message.yson_field());
+
+ EXPECT_EQ(2, message.string_to_int32_map_size());
+ EXPECT_EQ(0, message.string_to_int32_map().at("hello"));
+ EXPECT_EQ(1, message.string_to_int32_map().at("world"));
+
+ EXPECT_EQ(2, message.int32_to_int32_map_size());
+ EXPECT_EQ(0, message.int32_to_int32_map().at(100));
+ EXPECT_EQ(1, message.int32_to_int32_map().at(-200));
+
+ EXPECT_EQ(2, message.nested_message_map_size());
+ EXPECT_EQ(123, message.nested_message_map().at("hello").int32_field());
+ EXPECT_EQ(NYT::NYson::NProto::Color_Blue, message.nested_message_map().at("world").color());
+ EXPECT_EQ(1, message.nested_message_map().at("world").nested_message_map_size());
+ EXPECT_EQ(3, message.nested_message_map().at("world").nested_message_map().at("test").repeated_int32_field_size());
+ EXPECT_EQ(1, message.nested_message_map().at("world").nested_message_map().at("test").repeated_int32_field(0));
+ EXPECT_EQ(2, message.nested_message_map().at("world").nested_message_map().at("test").repeated_int32_field(1));
+ EXPECT_EQ(3, message.nested_message_map().at("world").nested_message_map().at("test").repeated_int32_field(2));
+
+ EXPECT_EQ(42, message.nested_message_with_custom_converter().x());
+ EXPECT_EQ(100, message.nested_message_with_custom_converter().y());
+
+ EXPECT_EQ(1, message.repeated_nested_message_with_custom_converter_size());
+ EXPECT_EQ(12, message.repeated_nested_message_with_custom_converter().Get(0).x());
+ EXPECT_EQ(23, message.repeated_nested_message_with_custom_converter().Get(0).y());
+
+ EXPECT_EQ(1, message.int32_to_nested_message_with_custom_converter_map_size());
+ EXPECT_EQ(6, message.int32_to_nested_message_with_custom_converter_map().at(123).x());
+ EXPECT_EQ(7, message.int32_to_nested_message_with_custom_converter_map().at(123).y());
+
+ EXPECT_EQ(12U, message.GetExtension(NYT::NYson::NProto::TMessageExtension::extension).extension_field());
+ EXPECT_EQ(23U, message
+ .GetExtension(NYT::NYson::NProto::TMessageExtension::extension)
+ .GetExtension(NYT::NYson::NProto::TMessageExtensionExtension::extension_extension)
+ .extension_extension_field());
+
+ EXPECT_EQ(0xabacabau, message.guid().first());
+ EXPECT_EQ(0xdeadbeefu, message.guid().second());
+
+ EXPECT_EQ("42", message.bytes_with_custom_converter());
+
+ EXPECT_EQ(1, message.repeated_bytes_with_custom_converter_size());
+ EXPECT_EQ("123", message.repeated_bytes_with_custom_converter(0));
+
+ EXPECT_EQ(GetProtoExtension<NYT::NYson::NProto::TMessageExt>(message.extensions()).x(), 42);
+}
+
+TEST(TYsonToProtobufYsonTest, ParseMapFromList)
+{
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("string_to_int32_map").BeginList()
+ .Item().BeginMap()
+ .Item("key").Value("hello")
+ .Item("value").Value(0)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("key").Value("world")
+ .Item("value").Value(1)
+ .EndMap()
+ .EndList()
+ .Item("int32_to_int32_map").BeginList()
+ .Item().BeginMap()
+ .Item("key").Value(100)
+ .Item("value").Value(0)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("key").Value(-200)
+ .Item("value").Value(1)
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(2, message.string_to_int32_map_size());
+ EXPECT_EQ(0, message.string_to_int32_map().at("hello"));
+ EXPECT_EQ(1, message.string_to_int32_map().at("world"));
+
+ EXPECT_EQ(2, message.int32_to_int32_map_size());
+ EXPECT_EQ(0, message.int32_to_int32_map().at(100));
+ EXPECT_EQ(1, message.int32_to_int32_map().at(-200));
+}
+
+TEST(TYsonToProtobufYsonTest, Aliases)
+{
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field_alias1").Value(10000)
+ .EndMap();
+
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(10000, message.int32_field_xxx());
+}
+
+TEST(TYsonToProtobufTest, TypeConversions)
+{
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(10000U)
+ .Item("uint32_field").Value(10000)
+ .Item("sint32_field").Value(10000U)
+ .Item("int64_field").Value(10000U)
+ .Item("uint64_field").Value(10000)
+ .Item("fixed32_field").Value(10000)
+ .Item("fixed64_field").Value(10000)
+ .Item("sfixed32_field").Value(10000U)
+ .Item("sfixed64_field").Value(10000U)
+ .Item("float_field").Value(0)
+ .Item("double_field").Value(1)
+ .EndMap();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(10000, message.int32_field_xxx());
+ EXPECT_EQ(10000U, message.uint32_field());
+ EXPECT_EQ(10000, message.sint32_field());
+ EXPECT_EQ(10000, message.int64_field());
+ EXPECT_EQ(10000U, message.uint64_field());
+ EXPECT_EQ(10000U, message.fixed32_field());
+ EXPECT_EQ(10000U, message.fixed64_field());
+ EXPECT_EQ(10000, message.sfixed32_field());
+ EXPECT_EQ(10000, message.sfixed64_field());
+ EXPECT_EQ(0.0, message.float_field());
+ EXPECT_EQ(1.0, message.double_field());
+}
+
+TEST(TYsonToProtobufYsonTest, Entities)
+{
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Entity()
+ .Item("repeated_int32_field").Entity()
+ .Item("nested_message1").Entity()
+ .Item("repeated_nested_message1").Entity()
+ .Item("attributes").Entity()
+ .Item("yson_field").Entity()
+ .Item("string_to_int32_map").Entity()
+ .Item("int32_to_int32_map").Entity()
+ .EndMap();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_FALSE(message.has_int32_field_xxx());
+ EXPECT_TRUE(message.repeated_int32_field().empty());
+ EXPECT_FALSE(message.has_nested_message1());
+ EXPECT_TRUE(message.repeated_nested_message1().empty());
+ EXPECT_FALSE(message.has_attributes());
+ EXPECT_EQ("#", message.yson_field());
+ EXPECT_TRUE(message.string_to_int32_map().empty());
+ EXPECT_TRUE(message.int32_to_int32_map().empty());
+}
+
+TEST(TYsonToProtobufTest, RootEntity)
+{
+ TEST_PROLOGUE(TMessage)
+ .Entity();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_FALSE(message.has_int32_field_xxx());
+}
+
+TEST(TYsonToProtobufTest, Failure)
+{
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .Value(0);
+ }, "");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginList()
+ .EndList();
+ }, "");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(true)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(static_cast<i64>(std::numeric_limits<i32>::max()) + 1)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(static_cast<i64>(std::numeric_limits<i32>::min()) - 1)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("uint32_field").Value(static_cast<ui64>(std::numeric_limits<ui32>::max()) + 1)
+ .EndMap();
+ }, "/uint32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value("test")
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").BeginAttributes().EndAttributes().Value(123)
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("color").Value("white")
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/color");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message1").Value(123)
+ .EndMap();
+ }, "/nested_message1");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("color").Value("blue")
+ .EndMap()
+ .Item().BeginMap()
+ .Item("color").Value("black")
+ .EndMap()
+ .EndList()
+ .EndMap();
+ }, "/repeated_nested_message1/1/color");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginList()
+ .EndList()
+ .EndList()
+ .EndMap();
+ }, "/repeated_nested_message1/0");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("color").Value("black")
+ .EndMap()
+ .EndList()
+ .EndMap();
+ }, "/repeated_nested_message1/0/color");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(0)
+ .Item("int32_field").Value(1)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(0)
+ .Item("int32_field").Value(1)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessageWithRequiredFields)
+ .BeginMap()
+ .Item("required_field").Value(0)
+ .Item("required_field").Value(1)
+ .EndMap();
+ }, "/required_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessageWithRequiredFields)
+ .BeginMap()
+ .EndMap();
+ }, "/required_field");
+
+ // int32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(10000000000)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(10000000000U)
+ .EndMap();
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_field").Value(-10000000000)
+ .EndMap();
+ }, "/int32_field");
+
+ // sint32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sint32_field").Value(10000000000)
+ .EndMap();
+ }, "/sint32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sint32_field").Value(10000000000U)
+ .EndMap();
+ }, "/sint32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sint32_field").Value(-10000000000)
+ .EndMap();
+ }, "/sint32_field");
+
+ // uint32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("uint32_field").Value(10000000000)
+ .EndMap();
+ }, "/uint32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("uint32_field").Value(10000000000U)
+ .EndMap();
+ }, "/uint32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("uint32_field").Value(-1)
+ .EndMap();
+ }, "/uint32_field");
+
+ // int32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int64_field").Value(std::numeric_limits<ui64>::max())
+ .EndMap();
+ }, "/int64_field");
+
+ // uint64
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("uint64_field").Value(-1)
+ .EndMap();
+ }, "/uint64_field");
+
+ // fixed32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("fixed32_field").Value(10000000000)
+ .EndMap();
+ }, "/fixed32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("fixed32_field").Value(10000000000U)
+ .EndMap();
+ }, "/fixed32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("fixed32_field").Value(-10000000000)
+ .EndMap();
+ }, "/fixed32_field");
+
+ // sfixed32
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sfixed32_field").Value(10000000000)
+ .EndMap();
+ }, "/sfixed32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sfixed32_field").Value(10000000000U)
+ .EndMap();
+ }, "/sfixed32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("sfixed32_field").Value(-10000000000)
+ .EndMap();
+ }, "/sfixed32_field");
+
+ // fixed64
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("fixed64_field").Value(-1)
+ .EndMap();
+ }, "/fixed64_field");
+
+ // YT-9094
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .EndList()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(1)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(1)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(1)
+ .EndMap()
+ .EndList()
+ .Item("repeated_nested_message2").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(1)
+ .EndMap()
+ .EndList()
+ .Item("attributes").BeginMap()
+ .Item("host").Value("localhost")
+ .EndMap()
+ .Item("nested_message1").BeginList()
+ .EndList()
+ .EndMap();
+ }, "/nested_message1");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message_map").BeginList()
+ .Item().Value(123)
+ .EndList()
+ .EndMap();
+ }, "/nested_message_map/0");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message_map").Value(123)
+ .EndMap();
+ }, "/nested_message_map");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("string_to_int32_map").BeginMap()
+ .Item("a").Value("b")
+ .EndMap()
+ .EndMap();
+ }, "/string_to_int32_map/a");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_to_int32_map").BeginMap()
+ .Item("a").Value(100)
+ .EndMap()
+ .EndMap();
+ }, "/int32_to_int32_map");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("int32_to_int32_map").BeginMap()
+ .Item("100").Value("b")
+ .EndMap()
+ .EndMap();
+ }, "/int32_to_int32_map/100");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("nested_message_map").BeginMap()
+ .Item("a").BeginMap()
+ .Item("nested_message_map").Value(123)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }, "/nested_message_map/a/nested_message_map");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessageWithRequiredAnnotation)
+ .BeginMap()
+ .EndMap();
+ }, "/required_string");
+}
+
+TEST(TYsonToProtobufTest, ErrorProto)
+{
+ TEST_PROLOGUE(TError)
+ .BeginMap()
+ .Item("message").Value("Hello world")
+ .Item("code").Value(1)
+ .Item("attributes").BeginMap()
+ .Item("host").Value("localhost")
+ .EndMap()
+ .EndMap();
+
+ TEST_EPILOGUE(TError);
+
+ EXPECT_EQ("Hello world", message.message());
+ EXPECT_EQ(1, message.code());
+
+ auto attribute = message.attributes().attributes()[0];
+ EXPECT_EQ(attribute.key(), "host");
+ EXPECT_EQ(ConvertTo<TString>(TYsonString(attribute.value())), "localhost");
+}
+
+TEST(TYsonToProtobufTest, SkipUnknownFields)
+{
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("unknown_field").Value(1)
+ .EndMap();
+ }, "");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE(TMessage)
+ .BeginMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("unknown_field").Value(1)
+ .EndMap()
+ .EndList()
+ .EndMap();
+ }, "/repeated_nested_message1/0");
+
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Keep);
+
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("int32_field").Value(10000)
+ .Item("unknown_field").Value(1)
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(123)
+ .Item("nested_message").BeginMap()
+ .Item("unknown_map").BeginMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("unknown_list").BeginList()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(10000, message.int32_field_xxx());
+
+ EXPECT_TRUE(message.has_nested_message1());
+ EXPECT_EQ(123, message.nested_message1().int32_field());
+ EXPECT_TRUE(message.nested_message1().has_nested_message());
+
+ EXPECT_EQ(1, message.repeated_nested_message1().size());
+ EXPECT_EQ(456, message.repeated_nested_message1().Get(0).int32_field());
+ }
+}
+
+TEST(TYsonToProtobufTest, KeepUnknownFields)
+{
+ auto ysonString = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("known_string").Value("hello")
+ .Item("unknown_int").Value(123)
+ .Item("unknown_map").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").Value("test")
+ .EndMap()
+ .Item("known_submessage").BeginMap()
+ .Item("known_int").Value(555)
+ .Item("unknown_list").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item("known_submessages").BeginList()
+ .Item().BeginMap()
+ .Item("known_string").Value("first")
+ .Item("unknown_int").Value(10)
+ .EndMap()
+ .Item().BeginMap()
+ .Item("known_string").Value("second")
+ .Item("unknown_int").Value(20)
+ .EndMap()
+ .EndList()
+ .Item("another_unknown_int").Value(777)
+ .EndMap();
+
+ TString protobufString;
+ StringOutputStream protobufOutput(&protobufString);
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Keep);
+ auto protobufWriter = CreateProtobufWriter(&protobufOutput, ReflectProtobufMessageType<NYT::NYson::NProto::TExtensibleMessage>(), options);
+ ParseYsonStringBuffer(ysonString.ToString(), EYsonType::Node, protobufWriter.get());
+
+ NYT::NYson::NProto::TExtensibleMessage message;
+ EXPECT_TRUE(message.ParseFromArray(protobufString.data(), protobufString.length()));
+
+ EXPECT_EQ("hello", message.known_string());
+ EXPECT_EQ(555, message.known_submessage().known_int());
+ EXPECT_EQ(2, message.known_submessages_size());
+ EXPECT_EQ("first", message.known_submessages(0).known_string());
+ EXPECT_EQ("second", message.known_submessages(1).known_string());
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TExtensibleMessage>());
+
+ Cerr << newYsonString << Endl;
+
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), ConvertToNode(ysonString)));
+}
+
+TEST(TYsonToProtobufTest, Entities)
+{
+ TProtobufWriterOptions options;
+
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").Entity()
+ .EndMap();
+ TEST_EPILOGUE(TMessage)
+
+ EXPECT_FALSE(message.has_nested_message1());
+}
+
+TEST(TYsonToProtobufTest, CustomUnknownFieldsModeResolver)
+{
+ {
+ // Basic usage of custom resolver with state.
+ TProtobufWriterOptions options;
+ int unknownKeyCount = 0;
+ options.UnknownYsonFieldModeResolver = [&unknownKeyCount](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/nested_message/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ } else if (path == "/nested_message1/nested_message/unknown_map/first_unknown_key" || path == "/nested_message1/nested_message/unknown_map/second_unknown_key") {
+ ++unknownKeyCount;
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("int32_field").Value(10000)
+ .Item("unknown_field").Value(1)
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(123)
+ .Item("nested_message").BeginMap()
+ .Item("unknown_map").BeginMap()
+ .Item("first_unknown_key").Value(11)
+ .Item("second_unknown_key").Value(22)
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("unknown_list").BeginList()
+ .EndList()
+ .EndMap()
+ .EndList()
+ .EndMap();
+
+ TEST_EPILOGUE(TMessage)
+ EXPECT_EQ(10000, message.int32_field_xxx());
+
+ EXPECT_TRUE(message.has_nested_message1());
+ EXPECT_EQ(123, message.nested_message1().int32_field());
+ EXPECT_TRUE(message.nested_message1().has_nested_message());
+
+ EXPECT_EQ(1, message.repeated_nested_message1().size());
+ EXPECT_EQ(456, message.repeated_nested_message1().Get(0).int32_field());
+ EXPECT_EQ(2, unknownKeyCount);
+ }
+ {
+ auto ysonStringBeforeSerialization = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("known_string").Value("hello")
+ .Item("unknown_skipped_int1").Value(1)
+ .Item("unknown_skipped_int2").Value(2)
+ .Item("unknown_map1").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").Value("test")
+ .Item("c").Entity()
+ .Item("unknown_map2").BeginMap()
+ .Item("x").Value(10)
+ .Item("y").Entity()
+ .Item("z").Value(1.12)
+ .EndMap()
+ .EndMap()
+ .Item("known_submessage").BeginMap()
+ .Item("known_int").Value(555)
+ .Item("unknown_list1").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap();
+
+
+ TString protobufString;
+ StringOutputStream protobufOutput(&protobufString);
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/unknown_map1") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path.StartsWith("/unknown_map1/")) {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/unknown_skipped_int1" || path == "/unknown_skipped_int2") {
+ return NYson::EUnknownYsonFieldsMode::Skip;
+ }
+ if (path == "/known_submessage/unknown_list1") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path.StartsWith("/known_submessage/unknown_list1/")) {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ return NYson::EUnknownYsonFieldsMode::Fail;
+ };
+ auto protobufWriter = CreateProtobufWriter(&protobufOutput, ReflectProtobufMessageType<NYT::NYson::NProto::TExtensibleMessage>(), options);
+ ParseYsonStringBuffer(ysonStringBeforeSerialization.ToString(), EYsonType::Node, protobufWriter.get());
+
+ auto expectedYsonStringAfterSerialization = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("known_string").Value("hello")
+ .Item("unknown_map1").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").Value("test")
+ .Item("c").Entity()
+ .Item("unknown_map2").BeginMap()
+ .Item("x").Value(10)
+ .Item("y").Entity()
+ .Item("z").Value(1.12)
+ .EndMap()
+ .EndMap()
+ .Item("known_submessage").BeginMap()
+ .Item("known_int").Value(555)
+ .Item("unknown_list1").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap();
+
+ NYT::NYson::NProto::TExtensibleMessage message;
+ EXPECT_TRUE(message.ParseFromArray(protobufString.data(), protobufString.length()));
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TExtensibleMessage>());
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), ConvertToNode(expectedYsonStringAfterSerialization)));
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/ok_value") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/nested_message1/unknown_map/fail_value") {
+ return NYson::EUnknownYsonFieldsMode::Fail;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("fail_value").Value(1)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/unknown_map/fail_value");
+ }
+ {
+ // Don't fail if Forward is returned on empty map or empty list.
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/ok_value") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/nested_message1/unknown_map/forwarded_empty_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/forwarded_empty_list") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("forwarded_empty_map").BeginMap()
+ .EndMap()
+ .Item("forwarded_empty_list").BeginList()
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }
+ {
+ // Fail if leaf is scalar and returns Forward.
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/forwarded_scalar_value") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("forwarded_scalar_value").Value(0)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/unknown_map/forwarded_scalar_value");
+ }
+ {
+ // Fail on forwarded scalar value on the top level.
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/kept_scalar_value") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/nested_message1/forwarded_scalar_value") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("kept_scalar_value").Value("val")
+ .EndMap()
+ .Item("forwarded_scalar_value").Value(0)
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/forwarded_scalar_value");
+ }
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/ok_value") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/nested_message1/unknown_map/forwarded_empty_list") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map/forwarded_empty_list/2") {
+ return NYson::EUnknownYsonFieldsMode::Fail;
+ }
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ };
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("forwarded_empty_list").BeginList()
+ .Item().Value("0")
+ .Item().Value("1")
+ .Item().Value("2")
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1/unknown_map/forwarded_empty_list/2");
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("forwarded_empty_list").BeginList()
+ .Item().BeginList()
+ .EndList()
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ }
+ {
+ auto ysonStringBeforeSerialization = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map1").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("skip_value1").Value("aa")
+ .Item("unknown_map2").BeginMap()
+ .Item("skip_value2").Value(1)
+ .EndMap()
+ .Item("skipped_map").BeginMap()
+ .Item("fail_value").Value(12)
+ .EndMap()
+ .Item("kept_map").BeginMap()
+ .Item("fail_value").Value(12)
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ TString protobufString;
+ StringOutputStream protobufOutput(&protobufString);
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) -> NYson::EUnknownYsonFieldsMode {
+ if (path == "/nested_message1/unknown_map1") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map1/ok_value") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ if (path == "/nested_message1/unknown_map1/skip_value1") {
+ return NYson::EUnknownYsonFieldsMode::Skip;
+ }
+ if (path == "/nested_message1/unknown_map1/unknown_map2") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/nested_message1/unknown_map1/unknown_map2/skip_value2") {
+ return NYson::EUnknownYsonFieldsMode::Skip;
+ }
+ if (path == "/nested_message1/unknown_map1/skipped_map") {
+ return NYson::EUnknownYsonFieldsMode::Skip;
+ }
+ if (path == "/nested_message1/unknown_map1/kept_map") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ // Shall not be invoked.
+ return NYson::EUnknownYsonFieldsMode::Fail;
+ };
+
+ auto protobufWriter = CreateProtobufWriter(&protobufOutput, ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), options);
+ ParseYsonStringBuffer(ysonStringBeforeSerialization.ToString(), EYsonType::Node, protobufWriter.get());
+
+ auto expectedYsonStringAfterSerialization = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_map1").BeginMap()
+ .Item("ok_value").Value(0)
+ .Item("unknown_map2").BeginMap()
+ .EndMap()
+ .Item("kept_map").BeginMap()
+ .Item("fail_value").Value(12)
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ NYT::NYson::NProto::TMessage message;
+ EXPECT_TRUE(message.ParseFromArray(protobufString.data(), protobufString.length()));
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>());
+
+ Cerr << newYsonString << Endl;
+
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), ConvertToNode(expectedYsonStringAfterSerialization)));
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [](const NYPath::TYPath& path) {
+ if (path == "/forwarded_attr") {
+ return NYson::EUnknownYsonFieldsMode::Forward;
+ }
+ if (path == "/kept_attr") {
+ return NYson::EUnknownYsonFieldsMode::Keep;
+ }
+ return NYson::EUnknownYsonFieldsMode::Fail;
+ };
+ auto ysonStringBeforeSerialization = BuildYsonStringFluently()
+ .BeginMap()
+ .Item("forwarded_attr").BeginAttributes()
+ .Item("key1").BeginList()
+ .Item().Value("list_item1")
+ .EndList()
+ .EndAttributes().BeginMap()
+ .EndMap()
+ .Item("kept_attr").BeginAttributes()
+ .Item("key1").BeginAttributes()
+ .Item("key2").Value(true)
+ .EndAttributes().Value("11212")
+ .EndAttributes().Value(111)
+ .EndMap();
+
+ TString protobufString;
+ StringOutputStream protobufOutput(&protobufString);
+
+ auto protobufWriter = CreateProtobufWriter(&protobufOutput, ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), options);
+ ParseYsonStringBuffer(ysonStringBeforeSerialization.ToString(), EYsonType::Node, protobufWriter.get());
+
+ auto expectedYsonStringAfterSerialization = ysonStringBeforeSerialization;
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>());
+
+ Cerr << newYsonString << Endl;
+
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), ConvertToNode(expectedYsonStringAfterSerialization)));
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Forward);
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_field\1").Value("val")
+ .EndMap()
+ .EndMap();
+ }, R"(/nested_message1/unknown_field\x01)");
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = [] (const NYPath::TYPath& path) {
+ if (path == R"(/nested_message1/unknown_field\x01)") {
+ return EUnknownYsonFieldsMode::Forward;
+ }
+ return EUnknownYsonFieldsMode::Keep;
+ };
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_field\1").Value("val")
+ .EndMap()
+ .EndMap();
+ }, R"(/nested_message1/unknown_field\x01)");
+ }
+ {
+ TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(EUnknownYsonFieldsMode::Fail);
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE_WITH_OPTIONS(TMessage, options)
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(11)
+ .Item("unknown_field1").Value("val")
+ .EndMap()
+ .EndMap();
+ }, "/nested_message1");
+ }
+}
+
+TEST(TYsonToProtobufTest, ReservedFields)
+{
+ TProtobufWriterOptions options;
+
+ TEST_PROLOGUE_WITH_OPTIONS(TMessageWithReservedFields, options)
+ .BeginMap()
+ .Item("reserved_field1").Value(1)
+ .Item("reserved_field1").Entity()
+ .Item("reserved_field3").BeginMap()
+ .Item("key").Value("value")
+ .EndMap()
+ .EndMap();
+ TEST_EPILOGUE(TMessage)
+}
+
+#undef TEST_PROLOGUE
+#undef TEST_PROLOGUE_WITH_OPTIONS
+#undef TEST_EPILOGUE
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define TEST_PROLOGUE() \
+ TString protobuf; \
+ StringOutputStream protobufOutputStream(&protobuf); \
+ CodedOutputStream codedStream(&protobufOutputStream);
+
+#define TEST_EPILOGUE_WITH_OPTIONS(type, options) \
+ codedStream.Trim(); \
+ Cerr << ToHex(protobuf) << Endl; \
+ ArrayInputStream protobufInputStream(protobuf.data(), protobuf.length()); \
+ TString yson; \
+ TStringOutput ysonOutputStream(yson); \
+ TYsonWriter writer(&ysonOutputStream, EYsonFormat::Pretty); \
+ ParseProtobuf(&writer, &protobufInputStream, ReflectProtobufMessageType<NYT::NYson::NProto::type>(), options); \
+ Cerr << ConvertToYsonString(TYsonString(yson), EYsonFormat::Pretty).ToString() << Endl;
+
+#define TEST_EPILOGUE(type) \
+ TEST_EPILOGUE_WITH_OPTIONS(type, TProtobufParserOptions())
+
+TEST(TProtobufToYsonTest, Success)
+{
+ NYT::NYson::NProto::TMessage message;
+ message.set_int32_field_xxx(10000);
+ message.set_uint32_field(10000U);
+ message.set_sint32_field(10000);
+ message.set_int64_field(10000);
+ message.set_uint64_field(10000U);
+ message.set_fixed32_field(10000U);
+ message.set_fixed64_field(10000U);
+ message.set_sfixed32_field(-10000);
+ message.set_sfixed64_field(10000);
+ message.set_bool_field(true);
+ message.set_string_field("hello");
+ message.set_float_field(3.14);
+ message.set_double_field(3.14);
+
+ message.add_repeated_int32_field(1);
+ message.add_repeated_int32_field(2);
+ message.add_repeated_int32_field(3);
+
+ message.mutable_nested_message1()->set_int32_field(123);
+ message.mutable_nested_message1()->set_color(NYT::NYson::NProto::Color_Blue);
+
+ message.mutable_nested_message1()->mutable_nested_message()->set_color(NYT::NYson::NProto::Color_Green);
+
+ message.MutableExtension(NYT::NYson::NProto::TMessageExtension::extension)->set_extension_field(12);
+ message
+ .MutableExtension(NYT::NYson::NProto::TMessageExtension::extension)
+ ->MutableExtension(NYT::NYson::NProto::TMessageExtensionExtension::extension_extension)
+ ->set_extension_extension_field(23);
+
+ {
+ auto* proto = message.add_repeated_nested_message1();
+ proto->set_int32_field(456);
+ proto->add_repeated_int32_field(1);
+ proto->add_repeated_int32_field(2);
+ proto->add_repeated_int32_field(3);
+ }
+ {
+ auto* proto = message.add_repeated_nested_message1();
+ proto->set_int32_field(654);
+ }
+ {
+ auto* proto = message.mutable_attributes();
+ {
+ auto* entry = proto->add_attributes();
+ entry->set_key("k1");
+ entry->set_value(ConvertToYsonString(1).ToString());
+ }
+ {
+ auto* entry = proto->add_attributes();
+ entry->set_key("k2");
+ entry->set_value(ConvertToYsonString("test").ToString());
+ }
+ {
+ auto* entry = proto->add_attributes();
+ entry->set_key("k3");
+ entry->set_value(ConvertToYsonString(std::vector<int>{1, 2, 3}).ToString());
+ }
+ }
+
+ message.set_yson_field("{a=1;b=[\"foobar\";];}");
+
+ {
+ auto& map = *message.mutable_string_to_int32_map();
+ map["hello"] = 0;
+ map["world"] = 1;
+ }
+
+ {
+ auto& map = *message.mutable_int32_to_int32_map();
+ map[100] = 0;
+ map[-200] = 1;
+ }
+
+ {
+ auto& map = *message.mutable_nested_message_map();
+ {
+ NYT::NYson::NProto::TNestedMessage value;
+ value.set_int32_field(123);
+ map["hello"] = value;
+ }
+ {
+ NYT::NYson::NProto::TNestedMessage value;
+ value.set_color(NYT::NYson::NProto::Color_Blue);
+ {
+ auto& submap = *value.mutable_nested_message_map();
+ {
+ NYT::NYson::NProto::TNestedMessage subvalue;
+ subvalue.add_repeated_int32_field(1);
+ subvalue.add_repeated_int32_field(2);
+ subvalue.add_repeated_int32_field(3);
+ submap["test"] = subvalue;
+ }
+ }
+ map["world"] = value;
+ }
+ }
+
+ message.mutable_nested_message_with_custom_converter()->set_x(42);
+ message.mutable_nested_message_with_custom_converter()->set_y(100);
+
+ {
+ auto* nestedMessage = message.add_repeated_nested_message_with_custom_converter();
+ nestedMessage->set_x(12);
+ nestedMessage->set_y(23);
+ }
+
+ {
+ auto& map = *message.mutable_int32_to_nested_message_with_custom_converter_map();
+ {
+ NYT::NYson::NProto::TNestedMessageWithCustomConverter value;
+ value.set_x(6);
+ value.set_y(7);
+ map[123] = value;
+ }
+ }
+
+ {
+ auto* guid = message.mutable_guid();
+ guid->set_first(0xabacaba);
+ guid->set_second(0xdeadbeef);
+ }
+
+ message.set_bytes_with_custom_converter("42");
+ message.add_repeated_bytes_with_custom_converter("123");
+
+ {
+ NYT::NYson::NProto::TMessageExt messageExt;
+ messageExt.set_x(42);
+ SetProtoExtension(message.mutable_extensions(), messageExt);
+ }
+
+ TEST_PROLOGUE()
+ Y_PROTOBUF_SUPPRESS_NODISCARD message.SerializeToCodedStream(&codedStream);
+ TEST_EPILOGUE(TMessage)
+
+ auto writtenNode = ConvertToNode(TYsonString(yson));
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("int32_field").Value(10000)
+ .Item("uint32_field").Value(10000U)
+ .Item("sint32_field").Value(10000)
+ .Item("int64_field").Value(10000)
+ .Item("uint64_field").Value(10000U)
+ .Item("fixed32_field").Value(10000U)
+ .Item("fixed64_field").Value(10000U)
+ .Item("sfixed32_field").Value(-10000)
+ .Item("sfixed64_field").Value(10000)
+ .Item("bool_field").Value(true)
+ .Item("string_field").Value("hello")
+ .Item("float_field").Value(3.14)
+ .Item("double_field").Value(3.14)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(123)
+ .Item("color").Value("blue")
+ .Item("nested_message").BeginMap()
+ .Item("color").Value("green")
+ .EndMap()
+ .EndMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(654)
+ .EndMap()
+ .EndList()
+ .Item("attributes").BeginMap()
+ .Item("k1").Value(1)
+ .Item("k2").Value("test")
+ .Item("k3").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item("yson_field").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").BeginList()
+ .Item().Value("foobar")
+ .EndList()
+ .EndMap()
+ .Item("string_to_int32_map").BeginMap()
+ .Item("hello").Value(0)
+ .Item("world").Value(1)
+ .EndMap()
+ .Item("int32_to_int32_map").BeginMap()
+ .Item("100").Value(0)
+ .Item("-200").Value(1)
+ .EndMap()
+ .Item("nested_message_map").BeginMap()
+ .Item("hello").BeginMap()
+ .Item("int32_field").Value(123)
+ .EndMap()
+ .Item("world").BeginMap()
+ .Item("color").Value("blue")
+ .Item("nested_message_map").BeginMap()
+ .Item("test").BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("extension").BeginMap()
+ .Item("extension_extension").BeginMap()
+ .Item("extension_extension_field").Value(23U)
+ .EndMap()
+ .Item("extension_field").Value(12U)
+ .EndMap()
+ .Item("nested_message_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .Item("y").Value(101)
+ .EndMap()
+ .Item("repeated_nested_message_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(13)
+ .Item("y").Value(24)
+ .EndMap()
+ .EndList()
+ .Item("int32_to_nested_message_with_custom_converter_map").BeginMap()
+ .Item("123").BeginMap()
+ .Item("x").Value(7)
+ .Item("y").Value(8)
+ .EndMap()
+ .EndMap()
+ .Item("guid").Value("0-deadbeef-0-abacaba")
+ .Item("bytes_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .EndMap()
+ .Item("repeated_bytes_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(124)
+ .EndMap()
+ .EndList()
+ .Item("extensions").BeginMap()
+ .Item("ext").BeginMap()
+ .Item("x").Value(42)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ EXPECT_TRUE(AreNodesEqual(writtenNode, expectedNode));
+}
+
+TEST(TProtobufToYsonTest, Casing)
+{
+ NYT::NYson::NProto::TCamelCaseStyleMessage message;
+ message.set_somefield(1);
+ message.set_anotherfield123(2);
+ message.set_crazy_field(3);
+
+ TEST_PROLOGUE()
+ Y_PROTOBUF_SUPPRESS_NODISCARD message.SerializeToCodedStream(&codedStream);
+ TEST_EPILOGUE(TCamelCaseStyleMessage)
+
+ auto writtenNode = ConvertToNode(TYsonString(yson));
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("some_field").Value(1)
+ .Item("another_field123").Value(2)
+ .Item("crazy_field").Value(3)
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(writtenNode, expectedNode));
+}
+
+TEST(TProtobufToYsonTest, ErrorProto)
+{
+ NYT::NProto::TError errorProto;
+ errorProto.set_message("Hello world");
+ errorProto.set_code(1);
+ auto attributeProto = errorProto.mutable_attributes()->add_attributes();
+ attributeProto->set_key("host");
+ attributeProto->set_value(ConvertToYsonString("localhost").ToString());
+
+ auto serialized = SerializeProtoToRef(errorProto);
+
+ ArrayInputStream inputStream(serialized.Begin(), serialized.Size());
+ TString yson;
+ TStringOutput outputStream(yson);
+ TYsonWriter writer(&outputStream, EYsonFormat::Pretty);
+ ParseProtobuf(&writer, &inputStream, ReflectProtobufMessageType<NYT::NProto::TError>());
+
+ auto writtenNode = ConvertToNode(TYsonString(yson));
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("message").Value("Hello world")
+ .Item("code").Value(1)
+ .Item("attributes").BeginMap()
+ .Item("host").Value("localhost")
+ .EndMap()
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(writtenNode, expectedNode));
+}
+
+TEST(TProtobufToYsonTest, Failure)
+{
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*int32_field_xxx*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ TEST_EPILOGUE(TMessage)
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(15 /*nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(3);
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*color*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(100);
+ TEST_EPILOGUE(TMessage)
+ }, "/nested_message1/color");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(17 /*repeated_int32_field*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ TEST_EPILOGUE(TMessage)
+ }, "/repeated_int32_field/0");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(17 /*repeated_int32_field*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(1);
+ codedStream.WriteTag(WireFormatLite::MakeTag(17 /*repeated_int32_field*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ TEST_EPILOGUE(TMessage)
+ }, "/repeated_int32_field/1");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(18 /*repeated_nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(3);
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*color*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(2);
+ codedStream.WriteTag(WireFormatLite::MakeTag(18 /*repeated_nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(3);
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*color*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(4);
+ TEST_EPILOGUE(TMessage)
+ }, "/repeated_nested_message1/1/color");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(18 /*repeated_nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(3);
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*color*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(2);
+ codedStream.WriteTag(WireFormatLite::MakeTag(18 /*repeated_nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(6);
+ codedStream.WriteTag(WireFormatLite::MakeTag(100 /*repeated_int32_field*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ codedStream.WriteTag(WireFormatLite::MakeTag(100 /*repeated_int32_field*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ TEST_EPILOGUE(TMessage)
+ }, "/repeated_nested_message1/1/repeated_int32_field/1");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ TEST_EPILOGUE(TMessageWithRequiredFields)
+ }, "/required_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(3 /*nested_messages*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessageWithRequiredFields)
+ }, "/nested_messages/0/required_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(3 /*nested_messages*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(4);
+ codedStream.WriteTag(WireFormatLite::MakeTag(2 /*required_field*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ codedStream.WriteTag(WireFormatLite::MakeTag(2 /*required_field*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessageWithRequiredFields)
+ }, "/nested_messages/0/required_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*int32_field_xxx*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*int32_field_xxx*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessage)
+ }, "/int32_field");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*attributes*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(2);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*attribute*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessage)
+ }, "/attributes");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*attributes*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(4);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*attribute*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(2);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*key*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessage)
+ }, "/attributes");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*attributes*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(4);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*attribute*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(2);
+ codedStream.WriteTag(WireFormatLite::MakeTag(2 /*value*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessage)
+ }, "/attributes");
+
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*attributes*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(6);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*attribute*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(4);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*key*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ codedStream.WriteTag(WireFormatLite::MakeTag(1 /*key*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessage)
+ }, "/attributes");
+}
+
+TEST(TProtobufToYsonTest, UnknownFields)
+{
+ EXPECT_YPATH({
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(100 /*unknown*/, WireFormatLite::WIRETYPE_FIXED32));
+ TEST_EPILOGUE(TMessage)
+ }, "");
+
+ {
+ TProtobufParserOptions options;
+ options.SkipUnknownFields = true;
+
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(100 /*unknown*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(9);
+ codedStream.WriteRaw("blablabla", 9);
+ codedStream.WriteTag(WireFormatLite::MakeTag(15 /*nested_message1*/, WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ codedStream.WriteVarint64(3);
+ codedStream.WriteTag(WireFormatLite::MakeTag(19 /*color*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(2 /*red*/);
+ TEST_EPILOGUE_WITH_OPTIONS(TMessage, options)
+
+ auto writtenNode = ConvertToNode(TYsonString(yson));
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("nested_message1").BeginMap()
+ .Item("color").Value("red")
+ .EndMap()
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(writtenNode, expectedNode));
+ }
+}
+
+TEST(TProtobufToYsonTest, ReservedFields)
+{
+ TEST_PROLOGUE()
+ codedStream.WriteTag(WireFormatLite::MakeTag(100 /*unknown*/, WireFormatLite::WIRETYPE_VARINT));
+ codedStream.WriteVarint64(0);
+ TEST_EPILOGUE(TMessageWithReservedFields)
+}
+
+#undef TEST_PROLOGUE
+#undef TEST_EPILOGUE
+#undef TEST_EPILOGUE_WITH_OPTIONS
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TestMessageByYPath(const TYPath& path)
+{
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufMessageElement>>(result.Element));
+ const auto& messageElement = std::get<std::unique_ptr<TProtobufMessageElement>>(result.Element);
+ EXPECT_EQ(ReflectProtobufMessageType<T>(), messageElement->Type);
+ EXPECT_EQ(path, result.HeadPath);
+ EXPECT_EQ("", result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, Message)
+{
+ TestMessageByYPath<NYT::NYson::NProto::TMessage>("");
+ TestMessageByYPath<NYT::NYson::NProto::TNestedMessage>("/nested_message1");
+ TestMessageByYPath<NYT::NYson::NProto::TNestedMessage>("/repeated_nested_message1/0/nested_message");
+ TestMessageByYPath<NYT::NYson::NProto::TNestedMessage>("/nested_message_map/k");
+ TestMessageByYPath<NYT::NYson::NProto::TNestedMessage>("/nested_message_map/k/nested_message");
+ TestMessageByYPath<NYT::NYson::NProto::TNestedMessage>("/nested_message_map/k/nested_message/nested_message_map/k");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestScalarByYPath(const TYPath& path)
+{
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufScalarElement>>(result.Element));
+ EXPECT_EQ(path, result.HeadPath);
+ EXPECT_EQ("", result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, Scalar)
+{
+ TestScalarByYPath("/uint32_field");
+ TestScalarByYPath("/repeated_int32_field/123");
+ TestScalarByYPath("/repeated_nested_message1/0/color");
+ TestScalarByYPath("/nested_message_map/abc/int32_field");
+ TestScalarByYPath("/string_to_int32_map/abc");
+ TestScalarByYPath("/int32_to_int32_map/100");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestAttributeDictionaryByYPath(const TYPath& path, const TYPath& headPath)
+{
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufAttributeDictionaryElement>>(result.Element));
+ EXPECT_EQ(headPath, result.HeadPath);
+ EXPECT_EQ(path.substr(headPath.length()), result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, AttributeDictionary)
+{
+ TestAttributeDictionaryByYPath("/attributes", "/attributes");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void TestAnyByYPath(const TYPath& path, const TYPath& headPath)
+{
+ TResolveProtobufElementByYPathOptions options;
+ options.AllowUnknownYsonFields = true;
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<T>(), path, options);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufAnyElement>>(result.Element));
+ EXPECT_EQ(headPath, result.HeadPath);
+ EXPECT_EQ(path.substr(headPath.length()), result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, Any)
+{
+ TestAnyByYPath<NYT::NYson::NProto::TMessage>("/yson_field", "/yson_field");
+ TestAnyByYPath<NYT::NYson::NProto::TMessage>("/yson_field/abc", "/yson_field");
+ TestAnyByYPath<NYT::NYson::NProto::TMessage>("/attributes/abc", "/attributes/abc");
+ TestAnyByYPath<NYT::NYson::NProto::TMessage>("/attributes/abc/xyz", "/attributes/abc");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/hello", "/hello");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/hello/world", "/hello");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/known_submessage/hello", "/known_submessage/hello");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/known_submessage/hello/world", "/known_submessage/hello");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/known_submessages/123/hello", "/known_submessages/123/hello");
+ TestAnyByYPath<NYT::NYson::NProto::TExtensibleMessage>("/known_submessages/123/hello/world", "/known_submessages/123/hello");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestRepeatedByYPath(const TYPath& path)
+{
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufRepeatedElement>>(result.Element));
+ EXPECT_EQ(path, result.HeadPath);
+ EXPECT_EQ("", result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, Repeated)
+{
+ TestRepeatedByYPath("/repeated_int32_field");
+ TestRepeatedByYPath("/nested_message1/repeated_int32_field");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestMapByYPath(const TYPath& path)
+{
+ auto result = ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);
+ EXPECT_TRUE(std::holds_alternative<std::unique_ptr<TProtobufMapElement>>(result.Element));
+ EXPECT_EQ(path, result.HeadPath);
+ EXPECT_EQ("", result.TailPath);
+}
+
+TEST(TResolveProtobufElementByYPath, Map)
+{
+ TestMapByYPath("/string_to_int32_map");
+ TestMapByYPath("/int32_to_int32_map");
+ TestMapByYPath("/nested_message_map");
+ TestMapByYPath("/nested_message_map/abc/nested_message_map");
+ TestMapByYPath("/nested_message1/nested_message_map");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DO(path, errorPath) \
+ EXPECT_YPATH({ResolveProtobufElementByYPath(ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>(), path);}, errorPath);
+
+TEST(TResolveProtobufElementByYPath, Failure)
+{
+ DO("/repeated_int32_field/1/2", "/repeated_int32_field/1")
+ DO("/repeated_int32_field/ 1/2", "/repeated_int32_field/ 1")
+ DO("/repeated_int32_field/1 /2", "/repeated_int32_field/1 ")
+ DO("/repeated_int32_field/-1/2", "/repeated_int32_field/-1")
+ DO("/repeated_int32_field/abc/xyz", "/repeated_int32_field/abc")
+ DO("/missing", "/missing")
+ DO("/repeated_nested_message1/1/xyz/abc", "/repeated_nested_message1/1/xyz")
+ DO("/repeated_nested_message1/-1/xyz/abc", "/repeated_nested_message1/-1/xyz")
+ DO("/repeated_nested_message1/ 1/xyz/abc", "/repeated_nested_message1/ 1")
+ DO("/repeated_nested_message1/1 /xyz/abc", "/repeated_nested_message1/1 ")
+ DO("/repeated_nested_message1/xyz/abc", "/repeated_nested_message1/xyz")
+}
+
+#undef DO
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TProtobufEnums, FindValueByLiteral)
+{
+ static const auto* type = ReflectProtobufEnumType(NYT::NYson::NProto::EColor_descriptor());
+ ASSERT_EQ(std::nullopt, FindProtobufEnumValueByLiteral<NYT::NYson::NProto::EColor>(type, "zzz"));
+ ASSERT_EQ(NYT::NYson::NProto::Color_Red, FindProtobufEnumValueByLiteral<NYT::NYson::NProto::EColor>(type, "red"));
+}
+
+TEST(TProtobufEnums, FindLiteralByValue)
+{
+ static const auto* type = ReflectProtobufEnumType(NYT::NYson::NProto::EColor_descriptor());
+ ASSERT_EQ("red", FindProtobufEnumLiteralByValue(type, NYT::NYson::NProto::Color_Red));
+ ASSERT_EQ(TStringBuf(), FindProtobufEnumLiteralByValue(type, NYT::NYson::NProto::EColor(666)));
+}
+
+TEST(TProtobufEnums, FindValueByLiteralWithAlias)
+{
+ static const auto* type = ReflectProtobufEnumType(NYT::NYson::NProto::EFlag_descriptor());
+ ASSERT_EQ(NYT::NYson::NProto::Flag_True, FindProtobufEnumValueByLiteral<NYT::NYson::NProto::EFlag>(type, "true"));
+ ASSERT_EQ(NYT::NYson::NProto::Flag_True, FindProtobufEnumValueByLiteral<NYT::NYson::NProto::EFlag>(type, "yes"));
+}
+
+TEST(TProtobufEnums, FindLiteralByValueWithAlias)
+{
+ static const auto* type = ReflectProtobufEnumType(NYT::NYson::NProto::EFlag_descriptor());
+ ASSERT_EQ("true", FindProtobufEnumLiteralByValue(type, NYT::NYson::NProto::Flag_True));
+ ASSERT_EQ("true", FindProtobufEnumLiteralByValue(type, NYT::NYson::NProto::Flag_Yes));
+}
+
+TEST(TProtobufEnums, ConvertToProtobufEnumValueUntyped)
+{
+ static const auto* type = ReflectProtobufEnumType(NYT::NYson::NProto::EColor_descriptor());
+ EXPECT_EQ(
+ NYT::NYson::NProto::Color_Red,
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value(2)));
+ EXPECT_EQ(
+ NYT::NYson::NProto::Color_Red,
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value(2u)));
+ EXPECT_EQ(
+ NYT::NYson::NProto::Color_Red,
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value("red")));
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value(100500)),
+ "Unknown value");
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value("kizil")),
+ "Unknown value");
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().BeginMap().EndMap()),
+ "Expected integral or string");
+ EXPECT_THROW_WITH_SUBSTRING(
+ ConvertToProtobufEnumValue<NYT::NYson::NProto::EColor>(type, BuildYsonNodeFluently().Value(1ull << 52)),
+ "out of expected range");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonToProtobufTest, ConvertToYsonString)
+{
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("int32_field").Value(10000)
+ .Item("uint32_field").Value(10000U)
+ .Item("sint32_field").Value(10000)
+ .Item("int64_field").Value(10000)
+ .Item("uint64_field").Value(10000U)
+ .Item("fixed32_field").Value(10000U)
+ .Item("fixed64_field").Value(10000U)
+ .Item("sfixed32_field").Value(10000)
+ .Item("sfixed64_field").Value(-10000)
+ .Item("bool_field").Value(true)
+ .Item("string_field").Value("hello")
+ .Item("float_field").Value(3.14)
+ .Item("double_field").Value(3.14)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .Item("nested_message1").BeginMap()
+ .Item("int32_field").Value(123)
+ .Item("color").Value("blue")
+ .Item("nested_message").BeginMap()
+ .Item("color").Value("green")
+ .EndMap()
+ .EndMap()
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(654)
+ .EndMap()
+ .EndList()
+ .Item("attributes").BeginMap()
+ .Item("k1").Value(1)
+ .Item("k2").Value("test")
+ .Item("k3").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item("yson_field").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").BeginList()
+ .Item().Value("foobar")
+ .EndList()
+ .EndMap()
+ .Item("string_to_int32_map").BeginMap()
+ .Item("hello").Value(0)
+ .Item("world").Value(1)
+ .EndMap()
+ .Item("int32_to_int32_map").BeginMap()
+ .Item("100").Value(0)
+ .Item("-200").Value(1)
+ .EndMap()
+ .Item("nested_message_map").BeginMap()
+ .Item("hello").BeginMap()
+ .Item("int32_field").Value(123)
+ .EndMap()
+ .Item("world").BeginMap()
+ .Item("color").Value("blue")
+ .Item("nested_message_map").BeginMap()
+ .Item("test").BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .Item("extension").BeginMap()
+ .Item("extension_extension").BeginMap()
+ .Item("extension_extension_field").Value(23U)
+ .EndMap()
+ .Item("extension_field").Value(12U)
+ .EndMap()
+ .Item("nested_message_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .Item("y").Value(101)
+ .EndMap()
+ .Item("repeated_nested_message_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(13)
+ .Item("y").Value(24)
+ .EndMap()
+ .EndList()
+ .Item("int32_to_nested_message_with_custom_converter_map").BeginMap()
+ .Item("123").BeginMap()
+ .Item("x").Value(7)
+ .Item("y").Value(8)
+ .EndMap()
+ .EndMap()
+ .Item("guid").Value("0-deadbeef-0-abacaba")
+ .Item("bytes_with_custom_converter").BeginMap()
+ .Item("x").Value(43)
+ .EndMap()
+ .Item("repeated_bytes_with_custom_converter").BeginList()
+ .Item().BeginMap()
+ .Item("x").Value(124)
+ .EndMap()
+ .EndList()
+ .Item("extensions").BeginMap()
+ .Item("ext").BeginMap()
+ .Item("x").Value(42)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ auto unknownFieldsMode = NYT::NYson::EUnknownYsonFieldsMode::Skip;
+ auto ysonStringBinary = ConvertToYsonString(expectedNode, NYT::NYson::EYsonFormat::Binary);
+ auto ysonStringText = ConvertToYsonString(expectedNode, NYT::NYson::EYsonFormat::Text);
+ auto rootType = ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>();
+ auto protobufStringBinary = YsonStringToProto(ysonStringBinary, rootType, unknownFieldsMode);
+ auto protobufStringText = YsonStringToProto(ysonStringText, rootType, unknownFieldsMode);
+ ASSERT_EQ(protobufStringBinary, protobufStringText);
+}
+
+TEST(TYsonToProtobufTest, YsonStringMerger)
+{
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("uint32_field").Value(324234)
+ .Item("repeated_nested_message1").BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(654)
+ .EndMap()
+ .EndList()
+ .Item("nested_message_map").BeginMap()
+ .Item("hello").BeginMap()
+ .Item("int32_field").Value(123)
+ .EndMap()
+ .Item("world").BeginMap()
+ .Item("color").Value("blue")
+ .Item("nested_message_map").BeginMap()
+ .Item("test").BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ NYPath::TYPath repeatedNestedMessage1Path = "/repeated_nested_message1";
+ auto repeatedNestedMessage1 = BuildYsonNodeFluently()
+ .BeginList()
+ .Item().BeginMap()
+ .Item("int32_field").Value(456)
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("int32_field").Value(654)
+ .EndMap()
+ .EndList();
+
+ NYPath::TYPath nestedMessageMapHelloPath = "/nested_message_map/hello";
+ auto nestedMessageMapHello = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("int32_field").Value(123)
+ .EndMap();
+
+ NYPath::TYPath nestedMessageMapWorldPath = "/nested_message_map/world";
+ auto nestedMessageMapWorld = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("color").Value("blue")
+ .Item("nested_message_map").BeginMap()
+ .Item("test").BeginMap()
+ .Item("repeated_int32_field").BeginList()
+ .Item().Value(1)
+ .Item().Value(2)
+ .Item().Value(3)
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ NYPath::TYPath uint32FieldPath = "/uint32_field";
+ auto uint32Field = BuildYsonNodeFluently()
+ .Value(324234);
+
+ auto paths = std::vector<NYPath::TYPath>{
+ repeatedNestedMessage1Path,
+ nestedMessageMapHelloPath,
+ nestedMessageMapWorldPath,
+ uint32FieldPath
+ };
+
+ // Yson strings of different types can be merged.
+ std::vector<TYsonString> ysonStrings{
+ ConvertToYsonString(repeatedNestedMessage1, EYsonFormat::Binary),
+ ConvertToYsonString(nestedMessageMapHello, EYsonFormat::Text),
+ ConvertToYsonString(nestedMessageMapWorld, EYsonFormat::Binary),
+ ConvertToYsonString(uint32Field, EYsonFormat::Text)
+ };
+
+ std::vector<TYsonStringBuf> ysonStringBufs;
+ for (const auto& ysonString : ysonStrings) {
+ ysonStringBufs.push_back(ysonString);
+ }
+ auto ysonStringMerged = MergeYsonStrings(paths, ysonStringBufs);
+ auto unknownFieldsMode = NYT::NYson::EUnknownYsonFieldsMode::Skip;
+ auto ysonStringBinary = ConvertToYsonString(expectedNode, NYT::NYson::EYsonFormat::Binary);
+ auto ysonStringText = ConvertToYsonString(expectedNode, NYT::NYson::EYsonFormat::Text);
+ auto rootType = ReflectProtobufMessageType<NYT::NYson::NProto::TMessage>();
+ auto protobufStringBinary = YsonStringToProto(ysonStringBinary, rootType, unknownFieldsMode);
+ auto protobufStringText = YsonStringToProto(ysonStringText, rootType, unknownFieldsMode);
+ auto protobufStringMerged = YsonStringToProto(ysonStringMerged, rootType, unknownFieldsMode);
+ ASSERT_EQ(protobufStringBinary, protobufStringText);
+ ASSERT_EQ(protobufStringBinary, protobufStringMerged);
+}
+
+template <class T, class TNodeList, class TRepeated>
+void CopyToProto(const TNodeList& from, TRepeated& rep)
+{
+ for (auto child : from->GetChildren()) {
+ rep.Add(child->template GetValue<T>());
+ }
+}
+
+TEST(TPackedRepeatedProtobufTest, TestSerializeDeserialize)
+{
+ auto node = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("int32_rep").BeginList()
+ .Item().Value(0)
+ .Item().Value(42)
+ .Item().Value(-100)
+ .Item().Value(Max<i32>())
+ .Item().Value(Min<i32>())
+ .EndList()
+ .Item("int64_rep").BeginList()
+ .Item().Value(0)
+ .Item().Value(42)
+ .Item().Value(-100)
+ .Item().Value(Min<i64>())
+ .Item().Value(Max<i64>())
+ .EndList()
+ .Item("uint32_rep").BeginList()
+ .Item().Value(0U)
+ .Item().Value(42U)
+ .Item().Value(Min<ui32>())
+ .Item().Value(Max<ui32>())
+ .EndList()
+ .Item("uint64_rep").BeginList()
+ .Item().Value(0U)
+ .Item().Value(42U)
+ .Item().Value(Min<ui64>())
+ .Item().Value(Max<ui64>())
+ .EndList()
+ .Item("float_rep").BeginList()
+ .Item().Value(0.F)
+ .Item().Value(42.F)
+ .Item().Value(-100.F)
+ .Item().Value(Min<float>())
+ .Item().Value(Max<float>())
+ .EndList()
+ .Item("double_rep").BeginList()
+ .Item().Value(0.)
+ .Item().Value(42.)
+ .Item().Value(-100.)
+ .Item().Value(Min<double>())
+ .Item().Value(Max<double>())
+ .EndList()
+ .Item("fixed32_rep").BeginList()
+ .Item().Value(0U)
+ .Item().Value(42U)
+ .Item().Value(Min<ui32>())
+ .Item().Value(Max<ui32>())
+ .EndList()
+ .Item("fixed64_rep").BeginList()
+ .Item().Value(0U)
+ .Item().Value(42U)
+ .Item().Value(Min<ui64>())
+ .Item().Value(Max<ui64>())
+ .EndList()
+ .Item("sfixed32_rep").BeginList()
+ .Item().Value(0)
+ .Item().Value(42)
+ .Item().Value(-100)
+ .Item().Value(Max<i32>())
+ .Item().Value(Min<i32>())
+ .EndList()
+ .Item("sfixed64_rep").BeginList()
+ .Item().Value(0)
+ .Item().Value(42)
+ .Item().Value(-100)
+ .Item().Value(Max<i64>())
+ .Item().Value(Min<i64>())
+ .EndList()
+ .Item("enum_rep").BeginList()
+ .Item().Value(0)
+ .Item().Value(1)
+ .EndList()
+ .EndMap();
+
+
+ NProto::TPackedRepeatedMessage serializedMessage;
+ TProtobufWriterOptions options;
+ DeserializeProtobufMessage(
+ serializedMessage,
+ ReflectProtobufMessageType<NProto::TPackedRepeatedMessage>(),
+ node,
+ options);
+
+ NProto::TPackedRepeatedMessage manuallyBuildMessage;
+ {
+ auto map = node->AsMap();
+ CopyToProto<i64>(map->FindChild("int32_rep")->AsList(), *manuallyBuildMessage.mutable_int32_rep());
+ CopyToProto<i64>(map->FindChild("int64_rep")->AsList(), *manuallyBuildMessage.mutable_int64_rep());
+ CopyToProto<ui64>(map->FindChild("uint32_rep")->AsList(), *manuallyBuildMessage.mutable_uint32_rep());
+ CopyToProto<ui64>(map->FindChild("uint64_rep")->AsList(), *manuallyBuildMessage.mutable_uint64_rep());
+ CopyToProto<double>(map->FindChild("float_rep")->AsList(), *manuallyBuildMessage.mutable_float_rep());
+ CopyToProto<double>(map->FindChild("double_rep")->AsList(), *manuallyBuildMessage.mutable_double_rep());
+ CopyToProto<ui64>(map->FindChild("fixed32_rep")->AsList(), *manuallyBuildMessage.mutable_fixed32_rep());
+ CopyToProto<ui64>(map->FindChild("fixed64_rep")->AsList(), *manuallyBuildMessage.mutable_fixed64_rep());
+ CopyToProto<i64>(map->FindChild("sfixed32_rep")->AsList(), *manuallyBuildMessage.mutable_sfixed32_rep());
+ CopyToProto<i64>(map->FindChild("sfixed64_rep")->AsList(), *manuallyBuildMessage.mutable_sfixed64_rep());
+ CopyToProto<i64>(map->FindChild("enum_rep")->AsList(), *manuallyBuildMessage.mutable_enum_rep());
+ }
+
+ // Check manuallyBuildMessage
+ {
+ TString protobufString;
+ Y_UNUSED(manuallyBuildMessage.SerializeToString(&protobufString));
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ EXPECT_NO_THROW(ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TPackedRepeatedMessage>()));
+
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), node));
+ }
+
+ // Check serializedMessage
+ {
+ TString protobufString;
+ Y_UNUSED(serializedMessage.SerializeToString(&protobufString));
+
+ TString newYsonString;
+ TStringOutput newYsonOutputStream(newYsonString);
+ TYsonWriter ysonWriter(&newYsonOutputStream, EYsonFormat::Pretty);
+ ArrayInputStream protobufInput(protobufString.data(), protobufString.length());
+ EXPECT_NO_THROW(ParseProtobuf(&ysonWriter, &protobufInput, ReflectProtobufMessageType<NYT::NYson::NProto::TPackedRepeatedMessage>()));
+
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(TYsonString(newYsonString)), node));
+ }
+}
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/ya.make b/yt/yt/core/yson/unittests/ya.make
new file mode 100644
index 0000000000..e2029b8ec8
--- /dev/null
+++ b/yt/yt/core/yson/unittests/ya.make
@@ -0,0 +1,51 @@
+GTEST(unittester-core-yson)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ depth_limiting_yson_consumer_ut.cpp
+ filter_ut.cpp
+ lexer_ut.cpp
+ protobuf_yson_ut.cpp
+ ypath_designated_yson_consumer_ut.cpp
+ yson_parser_ut.cpp
+ yson_pull_parser_ut.cpp
+ yson_token_writer_ut.cpp
+ yson_ut.cpp
+ yson_writer_ut.cpp
+
+ proto/protobuf_yson_ut.proto
+ proto/protobuf_yson_casing_ut.proto
+)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/yson/unittests/ypath_designated_yson_consumer_ut.cpp b/yt/yt/core/yson/unittests/ypath_designated_yson_consumer_ut.cpp
new file mode 100644
index 0000000000..d82d963a51
--- /dev/null
+++ b/yt/yt/core/yson/unittests/ypath_designated_yson_consumer_ut.cpp
@@ -0,0 +1,276 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/ypath_designated_consumer.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYson {
+namespace {
+
+using namespace NYTree;
+
+using ::testing::StrictMock;
+using ::testing::InSequence;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathDesignatedYsonConsumerTest
+ : public ::testing::Test
+{
+public:
+ StrictMock<TMockYsonConsumer> Mock;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TYPathDesignatedYsonConsumerTest, EmptyPath)
+{
+ InSequence dummy;
+
+ auto consumer = CreateYPathDesignatedConsumer("", EMissingPathMode::ThrowError, &Mock);
+
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("foo"));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("bar"));
+ EXPECT_CALL(Mock, OnEndList());
+
+ BuildYsonFluently(consumer.get())
+ .BeginList()
+ .Item().Value("foo")
+ .Item().Value("bar")
+ .EndList();
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, FetchStringValue)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2").BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .Item("subkey2").Value("subvalue2")
+ .Item("subkey4").Value("subvalue3")
+ .EndMap()
+ .EndMap();
+ };
+
+ {
+ EXPECT_CALL(Mock, OnStringScalar("value1"));
+ auto consumer = CreateYPathDesignatedConsumer("/key1", EMissingPathMode::ThrowError, &Mock);
+ buildTree(consumer.get());
+ }
+
+ {
+ EXPECT_CALL(Mock, OnStringScalar("subvalue2"));
+ auto consumer = CreateYPathDesignatedConsumer("/key2/subkey2", EMissingPathMode::ThrowError, &Mock);
+ buildTree(consumer.get());
+ }
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, FetchStringAttribute)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value("attr_value1")
+ .Item("attr2").Value("attr_value2")
+ .Item("attr3").Value("attr_value3")
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .EndMap()
+ .EndMap();
+ };
+
+ EXPECT_CALL(Mock, OnStringScalar("attr_value1"));
+ auto consumer = CreateYPathDesignatedConsumer("/key2/@attr1", EMissingPathMode::ThrowError, &Mock);
+ buildTree(consumer.get());
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, FetchAllAttributes)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value("attr_value1")
+ .Item("attr2").Value("attr_value2")
+ .Item("attr3").Value("attr_value3")
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .EndMap()
+ .EndMap();
+ };
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("attr1"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value1"));
+ EXPECT_CALL(Mock, OnKeyedItem("attr2"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value2"));
+ EXPECT_CALL(Mock, OnKeyedItem("attr3"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value3"));
+ EXPECT_CALL(Mock, OnEndMap());
+ auto consumer = CreateYPathDesignatedConsumer("/key2/@", EMissingPathMode::ThrowError, &Mock);
+ buildTree(consumer.get());
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, FetchMapNodeWithAttributes)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value("attr_value1")
+ .Item("attr2").Value("attr_value2")
+ .Item("attr3").Value("attr_value3")
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .EndMap()
+ .EndMap();
+ };
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("attr1"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value1"));
+ EXPECT_CALL(Mock, OnKeyedItem("attr2"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value2"));
+ EXPECT_CALL(Mock, OnKeyedItem("attr3"));
+ EXPECT_CALL(Mock, OnStringScalar("attr_value3"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("subkey1"));
+ EXPECT_CALL(Mock, OnStringScalar("subvalue1"));
+ EXPECT_CALL(Mock, OnEndMap());
+ auto consumer = CreateYPathDesignatedConsumer("/key2", EMissingPathMode::ThrowError, &Mock);
+ buildTree(consumer.get());
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, FetchAllAttributesOfNodeWithoutAttributes)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value("attr_value1")
+ .Item("attr2").Value("attr_value2")
+ .Item("attr3").Value("attr_value3")
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .EndMap()
+ .EndMap();
+ };
+
+ {
+ auto consumer = CreateYPathDesignatedConsumer("/key1/@", EMissingPathMode::Ignore, &Mock);
+ buildTree(consumer.get());
+ }
+
+ {
+ auto consumer = CreateYPathDesignatedConsumer("/key1/@", EMissingPathMode::ThrowError, &Mock);
+ EXPECT_THROW(buildTree(consumer.get()), std::exception);
+ }
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, MissingSubtreeIgnore)
+{
+ InSequence dummy;
+
+ auto consumer = CreateYPathDesignatedConsumer("/key3", EMissingPathMode::Ignore, &Mock);
+
+ BuildYsonFluently(consumer.get())
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2").Value("value2")
+ .EndMap();
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, MissingSubtreeError)
+{
+ InSequence dummy;
+
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value("value1")
+ .Item("key2").Value("value2")
+ .Item("key3").BeginMap()
+ .Item("subkey1").Value("subvalue1")
+ .EndMap()
+ .EndMap();
+ };
+
+ {
+ auto consumer = CreateYPathDesignatedConsumer("/key4", EMissingPathMode::ThrowError, &Mock);
+ EXPECT_THROW(buildTree(consumer.get()), std::exception);
+ }
+
+ {
+ auto consumer = CreateYPathDesignatedConsumer("/key3/subkey2", EMissingPathMode::ThrowError, &Mock);
+ EXPECT_THROW(buildTree(consumer.get()), std::exception);
+ }
+}
+
+TEST_F(TYPathDesignatedYsonConsumerTest, ListUnsupportedError)
+{
+ InSequence dummy;
+
+ {
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").BeginList()
+ .EndList()
+ .Item("key2").Value("value2")
+ .EndMap();
+ };
+ auto consumer = CreateYPathDesignatedConsumer("/key1/0", EMissingPathMode::Ignore, &Mock);
+ EXPECT_THROW(buildTree(consumer.get()), std::exception);
+ }
+
+ {
+ auto buildTree = [] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").BeginList()
+ .Item().Value(10)
+ .Item().Value("abc")
+ .EndList()
+ .Item("key2").Value("value2")
+ .EndMap();
+ };
+ auto consumer = CreateYPathDesignatedConsumer("/key1/0", EMissingPathMode::Ignore, &Mock);
+ EXPECT_THROW(buildTree(consumer.get()), std::exception);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/unittests/yson_parser_ut.cpp b/yt/yt/core/yson/unittests/yson_parser_ut.cpp
new file mode 100644
index 0000000000..bab4920a68
--- /dev/null
+++ b/yt/yt/core/yson/unittests/yson_parser_ut.cpp
@@ -0,0 +1,749 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/null_consumer.h>
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/detail.h>
+
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NYson {
+namespace {
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+using ::testing::HasSubstr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonParserTest
+ : public ::testing::Test
+{
+public:
+ StrictMock<TMockYsonConsumer> Mock;
+
+ void Run(
+ const TString& input,
+ EYsonType mode = EYsonType::Node,
+ i64 memoryLimit = std::numeric_limits<i64>::max())
+ {
+ TYsonParser parser(&Mock, mode, {.EnableLinePositionInfo = true, .MemoryLimit = memoryLimit});
+ parser.Read(input);
+ parser.Finish();
+ }
+
+ void Run(
+ const std::vector<TString>& input,
+ EYsonType mode = EYsonType::Node,
+ i64 memoryLimit = std::numeric_limits<i64>::max())
+ {
+ TYsonParser parser(&Mock, mode, {.EnableLinePositionInfo = true, .MemoryLimit = memoryLimit});
+ for (const auto& str : input) {
+ parser.Read(str);
+ }
+ parser.Finish();
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TYsonParserTest, Int64)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnInt64Scalar(100500));
+
+ Run(" 100500 ");
+}
+
+TEST_F(TYsonParserTest, Uint64)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnUint64Scalar(100500));
+
+ Run(" 100500u ");
+}
+
+TEST_F(TYsonParserTest, Double)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(3.1415926)));
+
+ Run(" 31415926e-7 ");
+}
+
+TEST_F(TYsonParserTest, Nan)
+{
+ TString nanString = " %nan ";
+ auto node = NYTree::ConvertToNode(TYsonString(nanString));
+ EXPECT_TRUE(std::isnan(node->AsDouble()->GetValue()));
+
+ TString minusNanString = " %-nan ";
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(minusNanString)), std::exception);
+
+ TString plusNanString = " %+nan ";
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(plusNanString)), std::exception);
+
+ TString percentLessNanString = " nan ";
+ node = NYTree::ConvertToNode(TYsonString(percentLessNanString));
+ EXPECT_EQ(node->GetType(), NYTree::ENodeType::String);
+
+ TString badNanString = " %+nany ";
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(badNanString)), std::exception);
+
+ TString percentLessBadNanString = " +nan ";
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(percentLessBadNanString)), std::exception);
+}
+
+TEST_F(TYsonParserTest, YT_17658)
+{
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonStringBuf("\x01\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc")), std::exception);
+}
+
+TEST_F(TYsonParserTest, Infinity)
+{
+ InSequence dummy;
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(std::numeric_limits<double>::infinity()));
+ Run(" %inf ");
+ }
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(std::numeric_limits<double>::infinity()));
+ Run(" %+inf ");
+ }
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(-std::numeric_limits<double>::infinity()));
+ Run(" %-inf ");
+ }
+ {
+ EXPECT_CALL(Mock, OnStringScalar("inf"));
+ Run(" inf ");
+ }
+ {
+ EXPECT_THROW(Run(" +inf "), std::exception);
+ }
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(TStringBuf("%infinity"))), std::exception);
+ EXPECT_THROW(NYTree::ConvertToNode(TYsonString(TStringBuf("%+infinity"))), std::exception);
+}
+
+TEST_F(TYsonParserTest, StringStartingWithLetter)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar("Hello_789_World_123"));
+
+ Run(" Hello_789_World_123 ");
+}
+
+TEST_F(TYsonParserTest, StringStartingWithQuote)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar(" abcdeABCDE <1234567> + (10_000) - = 900 "));
+
+ Run("\" abcdeABCDE <1234567> + (10_000) - = 900 \"");
+}
+
+TEST_F(TYsonParserTest, Entity)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnEntity());
+
+ Run(" # ");
+}
+
+TEST_F(TYsonParserTest, BinaryInt64)
+{
+ InSequence dummy;
+ {
+ EXPECT_CALL(Mock, OnInt64Scalar(1ull << 21));
+
+ //Int64Marker + (1 << 21) as VarInt ZigZagEncoded
+ Run(TString(" \x02\x80\x80\x80\x02 ", 1 + 5 + 2));
+ }
+
+ {
+ EXPECT_CALL(Mock, OnInt64Scalar(1ull << 21));
+
+ //Uint64Marker + (1 << 21) as VarInt ZigZagEncoded
+ std::vector<TString> parts = {TString("\x02"), TString("\x80\x80\x80\x02")};
+ Run(parts);
+ }
+}
+
+TEST_F(TYsonParserTest, BinaryUint64)
+{
+ InSequence dummy;
+ {
+ EXPECT_CALL(Mock, OnUint64Scalar(1ull << 22));
+
+ //Int64Marker + (1 << 21) as VarInt ZigZagEncoded
+ Run(TString(" \x06\x80\x80\x80\x02 ", 1 + 5 + 2));
+ }
+
+ {
+ EXPECT_CALL(Mock, OnUint64Scalar(1ull << 22));
+
+ //Uint64Marker + (1 << 21) as VarInt ZigZagEncoded
+ std::vector<TString> parts = {TString("\x06"), TString("\x80\x80\x80\x02")};
+ Run(parts);
+ }
+}
+
+TEST_F(TYsonParserTest, BinaryDouble)
+{
+ InSequence dummy;
+ double x = 2.71828;
+
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(x)));
+
+ Run(TString("\x03", 1) + TString((char*) &x, sizeof(double))); // DoubleMarker
+ }
+
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(x)));
+
+ std::vector<TString> parts = {TString("\x03", 1), TString((char*) &x, sizeof(double))};
+ Run(parts); // DoubleMarker
+ }
+
+ {
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(x)));
+
+ std::vector<TString> parts = {TString("\x03", 1), TString((char*) &x, 4), TString(((char*) &x) + 4, 4)};
+ Run(parts); // DoubleMarker
+ }
+}
+
+TEST_F(TYsonParserTest, BinaryBoolean)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBooleanScalar(false));
+ Run(TString("\x04", 1));
+
+ EXPECT_CALL(Mock, OnBooleanScalar(true));
+ Run(TString("\x05", 1));
+}
+
+TEST_F(TYsonParserTest, InvalidBinaryDouble)
+{
+ EXPECT_THROW(Run(TString("\x03", 1)), std::exception);
+}
+
+TEST_F(TYsonParserTest, BinaryString)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar("YSON"));
+
+ Run(TString(" \x01\x08YSON", 1 + 6)); // StringMarker + length ( = 4) + String
+}
+
+TEST_F(TYsonParserTest, EmptyBinaryString)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar(""));
+
+ Run(TString("\x01\x00", 2)); // StringMarker + length ( = 0 )
+}
+
+TEST_F(TYsonParserTest, EmptyList)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnEndList());
+
+ Run(" [ ] ");
+}
+
+TEST_F(TYsonParserTest, EmptyMap)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ Run(" { } ");
+}
+
+TEST_F(TYsonParserTest, OneElementList)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnEndList());
+
+ Run(" [ 42 ] ");
+}
+
+TEST_F(TYsonParserTest, OneElementMap)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ Run(" { hello = world } ");
+}
+
+TEST_F(TYsonParserTest, OneElementBinaryMap)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ Run(TString("{\x01\x0Ahello=\x01\x0Aworld}",1 + 7 + 1 + 7 + 1));
+}
+
+
+
+TEST_F(TYsonParserTest, SeveralElementsList)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginList());
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnUint64Scalar(36u));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnDoubleScalar(::testing::DoubleEq(1000)));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("nosy_111"));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBooleanScalar(false));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("nosy is the best format ever!"));
+
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnEndList());
+
+ Run(" [ 42 ; 36u ; 1e3 ; nosy_111 ; %false; \"nosy is the best format ever!\"; { } ; ] ");
+}
+
+TEST_F(TYsonParserTest, MapWithAttributes)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("acl"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("read"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("*"));
+ EXPECT_CALL(Mock, OnEndList());
+
+ EXPECT_CALL(Mock, OnKeyedItem("write"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("sandello"));
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnKeyedItem("lock_scope"));
+ EXPECT_CALL(Mock, OnStringScalar("mytables"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("path"));
+ EXPECT_CALL(Mock, OnStringScalar("/home/sandello"));
+
+ EXPECT_CALL(Mock, OnKeyedItem("mode"));
+ EXPECT_CALL(Mock, OnInt64Scalar(755));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TString input;
+ input = "<acl = { read = [ \"*\" ]; write = [ sandello ] } ; \n"
+ " lock_scope = mytables> \n"
+ "{ path = \"/home/sandello\" ; mode = 0755 }";
+ Run(input);
+}
+
+TEST_F(TYsonParserTest, Unescaping)
+{
+ TString output;
+ for (int i = 0; i < 256; ++i) {
+ output.push_back(char(i));
+ }
+
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar(output));
+
+ Run(
+ "\"\\0\\1\\2\\3\\4\\5\\6\\7\\x08\\t\\n\\x0B\\x0C\\r\\x0E\\x0F"
+ "\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B"
+ "\\x1C\\x1D\\x1E\\x1F !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCD"
+ "EFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+ "\\x7F\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8A"
+ "\\x8B\\x8C\\x8D\\x8E\\x8F\\x90\\x91\\x92\\x93\\x94\\x95\\x96"
+ "\\x97\\x98\\x99\\x9A\\x9B\\x9C\\x9D\\x9E\\x9F\\xA0\\xA1\\xA2"
+ "\\xA3\\xA4\\xA5\\xA6\\xA7\\xA8\\xA9\\xAA\\xAB\\xAC\\xAD\\xAE"
+ "\\xAF\\xB0\\xB1\\xB2\\xB3\\xB4\\xB5\\xB6\\xB7\\xB8\\xB9\\xBA"
+ "\\xBB\\xBC\\xBD\\xBE\\xBF\\xC0\\xC1\\xC2\\xC3\\xC4\\xC5\\xC6"
+ "\\xC7\\xC8\\xC9\\xCA\\xCB\\xCC\\xCD\\xCE\\xCF\\xD0\\xD1\\xD2"
+ "\\xD3\\xD4\\xD5\\xD6\\xD7\\xD8\\xD9\\xDA\\xDB\\xDC\\xDD\\xDE"
+ "\\xDF\\xE0\\xE1\\xE2\\xE3\\xE4\\xE5\\xE6\\xE7\\xE8\\xE9\\xEA"
+ "\\xEB\\xEC\\xED\\xEE\\xEF\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6"
+ "\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\"");
+}
+
+TEST_F(TYsonParserTest, TrailingSlashes)
+{
+ TString slash = "\\";
+ TString escapedSlash = slash + slash;
+ TString quote = "\"";
+
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnStringScalar(slash));
+
+ Run(quote + escapedSlash + quote);
+}
+
+TEST_F(TYsonParserTest, ListFragment)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(2));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(3));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(4));
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(5));
+
+ Run(" 1 ;2; 3; 4;5 ", EYsonType::ListFragment);
+}
+
+TEST_F(TYsonParserTest, ListFragmentWithTrailingSemicolon)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnEntity());
+
+ Run("{};[];<>#;", EYsonType::ListFragment);
+}
+
+TEST_F(TYsonParserTest, OneListFragment)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnInt64Scalar(100500));
+
+ Run(" 100500 ", EYsonType::ListFragment);
+}
+
+TEST_F(TYsonParserTest, EmptyListFragment)
+{
+ InSequence dummy;
+ Run(" ", EYsonType::ListFragment);
+}
+
+TEST_F(TYsonParserTest, StraySemicolon)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("x"));
+ EXPECT_CALL(Mock, OnStringScalar("y"));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_THROW_THAT(
+ Run("{x=y};", EYsonType::Node),
+ HasSubstr("yson_type = \"list_fragment\""));
+}
+
+TEST_F(TYsonParserTest, MapFragment)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnInt64Scalar(2));
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnInt64Scalar(3));
+ EXPECT_CALL(Mock, OnKeyedItem("d"));
+ EXPECT_CALL(Mock, OnInt64Scalar(4));
+ EXPECT_CALL(Mock, OnKeyedItem("e"));
+ EXPECT_CALL(Mock, OnInt64Scalar(5));
+
+ Run(" a = 1 ;b=2; c= 3; d =4;e=5 ", EYsonType::MapFragment);
+}
+
+TEST_F(TYsonParserTest, MapFragmentWithTrailingSemicolon)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnKeyedItem("map"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnKeyedItem("list"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnKeyedItem("entity"));
+ EXPECT_CALL(Mock, OnEntity());
+
+ Run("map={};list=[];entity=#;", EYsonType::MapFragment);
+}
+
+TEST_F(TYsonParserTest, OneMapFragment)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnKeyedItem("1"));
+ EXPECT_CALL(Mock, OnInt64Scalar(100500));
+
+ Run(" \"1\" = 100500 ", EYsonType::MapFragment);
+}
+
+TEST_F(TYsonParserTest, EmptyMapFragment)
+{
+ InSequence dummy;
+ Run(" ", EYsonType::MapFragment);
+}
+
+TEST_F(TYsonParserTest, MemoryLimitOK)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ Run(" { } ", EYsonType::Node, 1024);
+}
+
+TEST_F(TYsonParserTest, MemoryLimitFit)
+{
+ auto s = TString(777, 'a');
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+ EXPECT_CALL(Mock, OnStringScalar(s));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ Run("{key=" + s + "}", EYsonType::Node, 777);
+}
+
+TEST_F(TYsonParserTest, MemoryLimitExceeded)
+{
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("key"));
+
+ EXPECT_THROW(Run("{key=" + TString(1025, 'a') + "}", EYsonType::Node, 1024), std::exception);
+}
+
+TEST_F(TYsonParserTest, DepthLimitExceeded)
+{
+ constexpr auto DepthLimit = DefaultYsonParserNestingLevelLimit;
+ EXPECT_CALL(Mock, OnBeginList()).Times(2 * DepthLimit - 2);
+ EXPECT_CALL(Mock, OnListItem()).Times(2 * DepthLimit - 1);
+ EXPECT_CALL(Mock, OnEndList()).Times(DepthLimit - 2);
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+
+ auto yson = "[" +
+ TString(DepthLimit - 2, '[') + "1" + TString(DepthLimit - 2, ']') + ";" +
+ TString(DepthLimit - 1, '[') + "1" + TString(DepthLimit - 1, ']') +
+ "]";
+
+ EXPECT_THROW(Run(yson, EYsonType::Node, 1024), std::exception);
+}
+
+TEST_F(TYsonParserTest, ContextInExceptions)
+{
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("foo"));
+ try {
+ Run("{foo bar = 580}");
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("bar = 580}"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST_F(TYsonParserTest, ContextInExceptions_ManyBlocks)
+{
+ try {
+ TYsonParser parser(GetNullYsonConsumer(), EYsonType::Node);
+ parser.Read("{fo");
+ parser.Read(TString(100, 'o')); // try to overflow 64 byte context
+ parser.Read("o bar = 580}");
+ parser.Finish();
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("bar = 580}"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST(TYsonTest, ContextInExceptions_ContextAtTheVeryBeginning)
+{
+ struct TNoAttributesAllowedConsumer
+ : public TNullYsonConsumer
+ {
+ public:
+ void OnBeginAttributes() override
+ {
+ THROW_ERROR_EXCEPTION("I don't like attributes");
+ }
+ } consumer;
+
+ try {
+ TStatelessYsonParser Parser(&consumer);
+ Parser.Parse("<a=42>#");
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("a=42"));
+ EXPECT_THAT(ex.what(), testing::HasSubstr("don't like"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST(TYsonTest, ContextInExceptions_Margin)
+{
+ try {
+ TYsonParser parser(GetNullYsonConsumer(), EYsonType::Node);
+ parser.Read("{fo");
+ parser.Read(TString(100, 'o')); // try to overflow 64 byte context
+ parser.Read("a");
+ parser.Read("b");
+ parser.Read("c");
+ parser.Read("d bar = 580}");
+ parser.Finish();
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("oabcd bar = 580}"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStringVectorBlockStream
+{
+public:
+ TStringVectorBlockStream(std::vector<TStringBuf> data)
+ : Data_(std::move(data))
+ { }
+
+ const char* Begin() const
+ {
+ return Data_[Index_].Data();
+ }
+
+ const char* Current() const
+ {
+ return Data_[Index_].Data() + Pointer_;
+ }
+
+ void Skip(size_t size)
+ {
+ Pointer_ += size;
+ YT_VERIFY(Pointer_ < Data_[Index_].Size());
+ }
+
+ const char* End() const
+ {
+ return Data_[Index_].Data() + Data_[Index_].Size();
+ }
+
+ void RefreshBlock()
+ {
+ ++Index_;
+ Pointer_ = 0;
+ }
+
+private:
+ const std::vector<TStringBuf> Data_;
+ size_t Index_ = 0;
+ size_t Pointer_ = 0;
+};
+
+using TTestReaderWithContext = NDetail::TReaderWithContext<TStringVectorBlockStream, 15>;
+using TContextPair = std::pair<TString, size_t>;
+
+TTestReaderWithContext CreateTestReaderWithContext(std::vector<TStringBuf> data)
+{
+ return TTestReaderWithContext(TStringVectorBlockStream(data));
+}
+
+TEST(TContextTest, NoCheckpointContextCall)
+{
+ using TContextPair = std::pair<TString, size_t>;
+ auto reader = CreateTestReaderWithContext({
+ "12345",
+ "678",
+ "90123456",
+ "789",
+ });
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("12345", 0));
+ reader.RefreshBlock();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("12345678", 0));
+ reader.RefreshBlock();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("123456789012345", 0));
+ reader.RefreshBlock();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("123456789012345", 0));
+}
+
+TEST(TContextTest, Simple)
+{
+ auto reader = CreateTestReaderWithContext({
+ "12345",
+ "678",
+ "90123456",
+ "789",
+ });
+ reader.Skip(2);
+ reader.CheckpointContext();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("12345", 2));
+}
+
+TEST(TContextTest, ReaderKeepsDataFromPrevBuffers)
+{
+ auto reader = CreateTestReaderWithContext({
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+ "a",
+ "b",
+ });
+ for (size_t i = 0; i < 10; ++i) {
+ reader.RefreshBlock();
+ }
+ EXPECT_EQ(*reader.Current(), 'a');
+ reader.CheckpointContext();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("1234567890a", 10));
+ reader.RefreshBlock();
+ EXPECT_EQ(reader.GetContextFromCheckpoint(), TContextPair("1234567890ab", 10));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/yson_pull_parser_ut.cpp b/yt/yt/core/yson/unittests/yson_pull_parser_ut.cpp
new file mode 100644
index 0000000000..e03740e0de
--- /dev/null
+++ b/yt/yt/core/yson/unittests/yson_pull_parser_ut.cpp
@@ -0,0 +1,1354 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/token_writer.h>
+#include <yt/yt/core/yson/null_consumer.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <util/stream/format.h>
+#include <util/stream/mem.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void PrintTo(const TYsonItem& ysonItem, ::std::ostream* out)
+{
+ auto type = ysonItem.GetType();
+ (*out) << ToString(type) << " {";
+ switch (type) {
+ case EYsonItemType::Int64Value:
+ (*out) << ysonItem.UncheckedAsInt64();
+ break;
+ case EYsonItemType::Uint64Value:
+ (*out) << ysonItem.UncheckedAsUint64();
+ break;
+ case EYsonItemType::DoubleValue:
+ (*out) << ysonItem.UncheckedAsDouble();
+ break;
+ case EYsonItemType::BooleanValue:
+ (*out) << ysonItem.UncheckedAsBoolean();
+ break;
+ case EYsonItemType::StringValue:
+ (*out) << ysonItem.UncheckedAsString();
+ break;
+ default:
+ (*out) << ' ';
+ break;
+ }
+ (*out) << "}";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TString GetYsonPullSignature(TYsonPullParser* parser, std::optional<size_t> levelToMark=std::nullopt)
+{
+ TString result;
+ {
+ TStringOutput out(result);
+ bool exhausted = false;
+ while (!exhausted) {
+ if (levelToMark && parser->IsOnValueBoundary(*levelToMark)) {
+ out << "! ";
+ }
+
+ auto item = parser->Next();
+ switch (item.GetType()) {
+ case EYsonItemType::BeginMap:
+ out << "{";
+ break;
+ case EYsonItemType::EndMap:
+ out << "}";
+ break;
+ case EYsonItemType::BeginAttributes:
+ out << "<";
+ break;
+ case EYsonItemType::EndAttributes:
+ out << ">";
+ break;
+ case EYsonItemType::BeginList:
+ out << "[";
+ break;
+ case EYsonItemType::EndList:
+ out << "]";
+ break;
+ case EYsonItemType::EntityValue:
+ out << "#";
+ break;
+ case EYsonItemType::BooleanValue:
+ out << (item.UncheckedAsBoolean() ? "%true" : "%false");
+ break;
+ case EYsonItemType::Int64Value:
+ out << item.UncheckedAsInt64();
+ break;
+ case EYsonItemType::Uint64Value:
+ out << item.UncheckedAsUint64() << 'u';
+ break;
+ case EYsonItemType::DoubleValue: {
+ auto value = item.UncheckedAsDouble();
+ if (std::isnan(value)) {
+ out << "%nan";
+ } else if (std::isinf(value)) {
+ if (value > 0) {
+ out << "%+inf";
+ } else {
+ out << "%-inf";
+ }
+ } else {
+ out << Prec(value, PREC_POINT_DIGITS, 2);
+ }
+ break;
+ }
+ case EYsonItemType::StringValue:
+ out << '\'' << EscapeC(item.UncheckedAsString()) << '\'';
+ break;
+ case EYsonItemType::EndOfStream:
+ exhausted = true;
+ continue;
+ }
+ out << ' ';
+ }
+ }
+ if (result) {
+ // Strip trailing space.
+ result.resize(result.size() - 1);
+ }
+ return result;
+}
+
+TString GetYsonPullSignature(
+ TStringBuf input,
+ EYsonType type = EYsonType::Node,
+ std::optional<size_t> levelToMark = std::nullopt)
+{
+ TMemoryInput in(input.data(), input.size());
+ TYsonPullParser parser(&in, type);
+ return GetYsonPullSignature(&parser, levelToMark);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TStringBufVectorReader
+ : public IZeroCopyInput
+{
+public:
+ TStringBufVectorReader(const std::vector<TStringBuf>& data)
+ : Data_(data.rbegin(), data.rend())
+ {
+ NextBuffer();
+ }
+
+private:
+ void NextBuffer() {
+ if (Data_.empty()) {
+ CurrentInput_.reset();
+ } else {
+ CurrentInput_.emplace(Data_.back());
+ Data_.pop_back();
+ }
+ }
+
+ size_t DoNext(const void** ptr, size_t len) override
+ {
+ if (!len) {
+ return 0;
+ }
+ while (CurrentInput_) {
+ auto result = CurrentInput_->Next(ptr, len);
+ if (result) {
+ return result;
+ }
+ NextBuffer();
+ }
+ return 0;
+ }
+
+private:
+ std::vector<TStringBuf> Data_;
+ std::optional<TMemoryInput> CurrentInput_;
+};
+
+struct TStringBufCursorHelper
+{
+ TMemoryInput MemoryInput;
+ TYsonPullParser PullParser;
+
+ TStringBufCursorHelper(TStringBuf input, EYsonType ysonType)
+ : MemoryInput(input)
+ , PullParser(&MemoryInput, ysonType)
+ { }
+};
+
+class TStringBufCursor
+ : private TStringBufCursorHelper
+ , public TYsonPullParserCursor
+{
+public:
+ explicit TStringBufCursor(TStringBuf input, EYsonType ysonType = EYsonType::Node)
+ : TStringBufCursorHelper(input, ysonType)
+ , TYsonPullParserCursor(&PullParser)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonSyntaxCheckerTest, NestingLevel)
+{
+ NDetail::TYsonSyntaxChecker checker(EYsonType::Node, /*nestingLevelLimit*/ 10);
+
+ EXPECT_EQ(checker.GetNestingLevel(), 0U);
+
+ checker.OnBeginList();
+ EXPECT_EQ(checker.GetNestingLevel(), 1U);
+
+ checker.OnAttributesBegin();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnString();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnEquality();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnSimpleNonstring(EYsonItemType::Int64Value);
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnAttributesEnd();
+ EXPECT_EQ(checker.GetNestingLevel(), 1U);
+
+ checker.OnString();
+ EXPECT_EQ(checker.GetNestingLevel(), 1U);
+
+ checker.OnSeparator();
+ EXPECT_EQ(checker.GetNestingLevel(), 1U);
+
+ checker.OnBeginMap();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnString();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnEquality();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnString();
+ EXPECT_EQ(checker.GetNestingLevel(), 2U);
+
+ checker.OnEndMap();
+ EXPECT_EQ(checker.GetNestingLevel(), 1U);
+
+ checker.OnEndList();
+ EXPECT_EQ(checker.GetNestingLevel(), 0U);
+
+ checker.OnFinish();
+ EXPECT_EQ(checker.GetNestingLevel(), 0U);
+}
+
+TEST(TYsonSyntaxCheckerTest, Boundaries)
+{
+ NDetail::TYsonSyntaxChecker checker(EYsonType::Node, /*nestingLevelLimit*/ 10);
+
+ checker.OnBeginList();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), true);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnAttributesBegin();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnString();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), true);
+ EXPECT_EQ(checker.IsOnKey(), true);
+
+ checker.OnEquality();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnSimpleNonstring(EYsonItemType::Int64Value);
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnAttributesEnd();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnString();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), true);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnSeparator();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), true);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnBeginMap();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnString();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), true);
+ EXPECT_EQ(checker.IsOnKey(), true);
+
+ checker.OnEquality();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnString();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), true);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnEndMap();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), false);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), true);
+ EXPECT_EQ(checker.IsOnValueBoundary(2), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnEndList();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), true);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+
+ checker.OnFinish();
+ EXPECT_EQ(checker.IsOnValueBoundary(0), true);
+ EXPECT_EQ(checker.IsOnValueBoundary(1), false);
+ EXPECT_EQ(checker.IsOnKey(), false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonPullParserTest, Int)
+{
+ EXPECT_EQ(GetYsonPullSignature(" 100500 "), "100500");
+ EXPECT_EQ(GetYsonPullSignature("\x02\xa7\xa2\x0c"), "-100500");
+}
+
+TEST(TYsonPullParserTest, Uint)
+{
+ EXPECT_EQ(GetYsonPullSignature(" 42u "), "42u");
+ EXPECT_EQ(GetYsonPullSignature("\x06\x94\x91\x06"), "100500u");
+}
+
+TEST(TYsonPullParserTest, Double)
+{
+ EXPECT_EQ(GetYsonPullSignature(" 31415926e-7 "), "3.14");
+ EXPECT_EQ(GetYsonPullSignature("\x03iW\x14\x8B\n\xBF\5@"), "2.72");
+ EXPECT_EQ(GetYsonPullSignature(" %nan "), "%nan");
+ EXPECT_ANY_THROW(GetYsonPullSignature(" %+nan "));
+ EXPECT_ANY_THROW(GetYsonPullSignature(" %-nan "));
+ EXPECT_ANY_THROW(GetYsonPullSignature(" %nany "));
+ EXPECT_ANY_THROW(GetYsonPullSignature(" +nan "));
+ EXPECT_EQ(GetYsonPullSignature(" %inf "), "%+inf");
+ EXPECT_EQ(GetYsonPullSignature(" %+inf "), "%+inf");
+ EXPECT_EQ(GetYsonPullSignature(" %-inf "), "%-inf");
+ EXPECT_ANY_THROW(GetYsonPullSignature(" +inf "));
+ EXPECT_ANY_THROW(GetYsonPullSignature(" %infinity "));
+}
+
+TEST(TYsonPullParserTest, String)
+{
+ EXPECT_EQ(GetYsonPullSignature(" nan "), "'nan'");
+ EXPECT_EQ(GetYsonPullSignature("\x01\x06" "bar"), "'bar'");
+ EXPECT_EQ(GetYsonPullSignature("\x01\x80\x01" + TString(64, 'a')), TString("'") + TString(64, 'a') + "'");
+ EXPECT_EQ(GetYsonPullSignature(TStringBuf("\x01\x00"sv)), "''");
+ EXPECT_EQ(GetYsonPullSignature(" Hello_789_World_123 "), "'Hello_789_World_123'");
+ EXPECT_EQ(GetYsonPullSignature(" Hello_789_World_123 "), "'Hello_789_World_123'");
+ EXPECT_EQ(
+ GetYsonPullSignature("\" abcdeABCDE <1234567> + (10_000) - = 900 \""),
+ "' abcdeABCDE <1234567> + (10_000) - = 900 '"
+ );
+}
+
+TEST(TYsonPullParserTest, StringEscaping)
+{
+ TString expected;
+ for (int i = 0; i < 256; ++i) {
+ expected.push_back(char(i));
+ }
+
+ TStringBuf inputData =
+ "\"\\0\\1\\2\\3\\4\\5\\6\\7\\x08\\t\\n\\x0B\\x0C\\r\\x0E\\x0F"
+ "\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B"
+ "\\x1C\\x1D\\x1E\\x1F !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCD"
+ "EFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+ "\\x7F\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8A"
+ "\\x8B\\x8C\\x8D\\x8E\\x8F\\x90\\x91\\x92\\x93\\x94\\x95\\x96"
+ "\\x97\\x98\\x99\\x9A\\x9B\\x9C\\x9D\\x9E\\x9F\\xA0\\xA1\\xA2"
+ "\\xA3\\xA4\\xA5\\xA6\\xA7\\xA8\\xA9\\xAA\\xAB\\xAC\\xAD\\xAE"
+ "\\xAF\\xB0\\xB1\\xB2\\xB3\\xB4\\xB5\\xB6\\xB7\\xB8\\xB9\\xBA"
+ "\\xBB\\xBC\\xBD\\xBE\\xBF\\xC0\\xC1\\xC2\\xC3\\xC4\\xC5\\xC6"
+ "\\xC7\\xC8\\xC9\\xCA\\xCB\\xCC\\xCD\\xCE\\xCF\\xD0\\xD1\\xD2"
+ "\\xD3\\xD4\\xD5\\xD6\\xD7\\xD8\\xD9\\xDA\\xDB\\xDC\\xDD\\xDE"
+ "\\xDF\\xE0\\xE1\\xE2\\xE3\\xE4\\xE5\\xE6\\xE7\\xE8\\xE9\\xEA"
+ "\\xEB\\xEC\\xED\\xEE\\xEF\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6"
+ "\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\""sv;
+
+ TMemoryInput inputStream(inputData);
+ TYsonPullParser parser(&inputStream, EYsonType::Node);
+ auto item1 = parser.Next();
+ EXPECT_EQ(item1.GetType(), EYsonItemType::StringValue);
+ EXPECT_EQ(item1.UncheckedAsString(), expected);
+
+ auto item2 = parser.Next();
+ EXPECT_TRUE(item2.IsEndOfStream());
+}
+
+TEST(TYsonPullParserTest, TrailingSlashes)
+{
+ char inputData[] = {'"', '\\', '"', '\\', '\\', '"'};
+ TMemoryInput inputStream(inputData, sizeof(inputData));
+ TYsonPullParser parser(&inputStream, EYsonType::Node);
+ auto item1 = parser.Next();
+ EXPECT_EQ(item1.GetType(), EYsonItemType::StringValue);
+ EXPECT_EQ(item1.UncheckedAsString(), "\"\\");
+
+ auto item2 = parser.Next();
+ EXPECT_TRUE(item2.IsEndOfStream());
+}
+
+TEST(TYsonPullParserTest, Entity)
+{
+ EXPECT_EQ(GetYsonPullSignature(" # "), "#");
+}
+
+TEST(TYsonPullParserTest, Boolean)
+{
+ EXPECT_EQ(GetYsonPullSignature(" %true "), "%true");
+ EXPECT_EQ(GetYsonPullSignature(" %false "), "%false");
+ EXPECT_EQ(GetYsonPullSignature("\x04"), "%false");
+ EXPECT_EQ(GetYsonPullSignature("\x05"), "%true");
+ EXPECT_ANY_THROW(GetYsonPullSignature(" %falsee "));
+}
+
+TEST(TYsonPullParserTest, List)
+{
+ EXPECT_EQ(GetYsonPullSignature("[]"), "[ ]");
+ EXPECT_EQ(GetYsonPullSignature("[[]]"), "[ [ ] ]");
+ EXPECT_EQ(GetYsonPullSignature("[[] ; ]"), "[ [ ] ]");
+ EXPECT_EQ(GetYsonPullSignature("[[] ; [[]] ]"), "[ [ ] [ [ ] ] ]");
+}
+
+TEST(TYsonPullParserTest, Map)
+{
+ EXPECT_EQ(GetYsonPullSignature("{}"), "{ }");
+ EXPECT_EQ(GetYsonPullSignature("{k=v}"), "{ 'k' 'v' }");
+ EXPECT_EQ(GetYsonPullSignature("{k=v;}"), "{ 'k' 'v' }");
+ EXPECT_EQ(GetYsonPullSignature("{k1=v; k2={} }"), "{ 'k1' 'v' 'k2' { } }");
+}
+
+TEST(TYsonPullParserTest, Attributes)
+{
+ EXPECT_EQ(GetYsonPullSignature("<>#"), "< > #");
+ EXPECT_EQ(GetYsonPullSignature("<a=v> #"), "< 'a' 'v' > #");
+ EXPECT_EQ(GetYsonPullSignature("<a=v;> #"), "< 'a' 'v' > #");
+ EXPECT_EQ(GetYsonPullSignature("<a1=v; a2={}; a3=<># > #"), "< 'a1' 'v' 'a2' { } 'a3' < > # > #");
+}
+
+TEST(TYsonPullParserTest, ListFragment)
+{
+ EXPECT_EQ(GetYsonPullSignature("", EYsonType::ListFragment), "");
+ EXPECT_EQ(GetYsonPullSignature("#", EYsonType::ListFragment), "#");
+ EXPECT_EQ(GetYsonPullSignature("#;", EYsonType::ListFragment), "#");
+ EXPECT_EQ(GetYsonPullSignature("#; #", EYsonType::ListFragment), "# #");
+ EXPECT_EQ(GetYsonPullSignature("[];{};<>#;", EYsonType::ListFragment), "[ ] { } < > #");
+}
+
+TEST(TYsonPullParserTest, StraySemicolon)
+{
+ EXPECT_THROW_THAT(
+ GetYsonPullSignature("{x=y};", EYsonType::Node),
+ testing::HasSubstr("yson_type = \"list_fragment\""));
+}
+
+TEST(TYsonPullParserTest, MapFragment)
+{
+ EXPECT_EQ(GetYsonPullSignature("", EYsonType::MapFragment), "");
+ EXPECT_EQ(GetYsonPullSignature("k=v ", EYsonType::MapFragment), "'k' 'v'");
+ EXPECT_EQ(GetYsonPullSignature("k=v ; ", EYsonType::MapFragment), "'k' 'v'");
+ EXPECT_EQ(
+ GetYsonPullSignature("k1=v; k2={}; k3=[]; k4=<>#", EYsonType::MapFragment),
+ "'k1' 'v' 'k2' { } 'k3' [ ] 'k4' < > #");
+}
+
+TEST(TYsonPullParserTest, Complex1)
+{
+ EXPECT_EQ(
+ GetYsonPullSignature(
+ "<acl = { read = [ \"*\" ]; write = [ sandello ] } ; \n"
+ " lock_scope = mytables> \n"
+ "{ path = \"/home/sandello\" ; mode = 0755 }"),
+
+ "< 'acl' { 'read' [ '*' ] 'write' [ 'sandello' ] }"
+ " 'lock_scope' 'mytables' >"
+ " { 'path' '/home/sandello' 'mode' 755 }");
+}
+
+TEST(TYsonPullParserTest, TestBadYson)
+{
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo bar "));
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo bar ", EYsonType::ListFragment));
+ EXPECT_ANY_THROW(GetYsonPullSignature("{foo=1;2};", EYsonType::ListFragment));
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo; bar"));
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo bar ", EYsonType::MapFragment));
+ EXPECT_ANY_THROW(GetYsonPullSignature("key=[a=b;c=d]", EYsonType::MapFragment));
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo=bar "));
+ EXPECT_ANY_THROW(GetYsonPullSignature("foo=bar ", EYsonType::ListFragment));
+}
+
+TEST(TYsonPullParserTest, Capture)
+{
+ EXPECT_EQ(GetYsonPullSignature(" foo ", EYsonType::Node, 0), "! 'foo' !");
+ EXPECT_EQ(GetYsonPullSignature(" <foo=bar>foo ", EYsonType::Node, 0), "! < 'foo' 'bar' > 'foo' !");
+ EXPECT_EQ(GetYsonPullSignature(" <foo=bar>[ 42 ] ", EYsonType::Node, 1), "< 'foo' ! 'bar' ! > [ ! 42 ! ]");
+ EXPECT_EQ(GetYsonPullSignature(
+ " <foo=[bar]; bar=2; baz=[[1;2;3]]>[ 1; []; {foo=[]}] ", EYsonType::Node, 2),
+ "< 'foo' [ ! 'bar' ! ] 'bar' 2 'baz' [ ! [ 1 2 3 ] ! ] > [ 1 [ ! ] { 'foo' ! [ ] ! } ]");
+}
+
+TEST(TYsonPullParserTest, DepthLimitExceeded)
+{
+ constexpr auto DepthLimit = DefaultYsonParserNestingLevelLimit;
+ EXPECT_NO_THROW(GetYsonPullSignature(TString(DepthLimit - 1, '[') + TString(DepthLimit - 1, ']')));
+ EXPECT_ANY_THROW(GetYsonPullSignature(TString(DepthLimit, '[') + TString(DepthLimit, ']')));
+}
+
+TEST(TYsonPullParserTest, DepthLimitInTransfer)
+{
+ constexpr auto DepthLimit = DefaultYsonParserNestingLevelLimit;
+
+ auto transfer = [] (TStringBuf yson) {
+ TMemoryInput input(yson);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+ cursor.TransferComplexValue(GetNullYsonConsumer());
+ };
+
+ EXPECT_NO_THROW(transfer(TString(DepthLimit - 1, '[') + TString(DepthLimit - 1, ']')));
+ EXPECT_ANY_THROW(transfer(TString(DepthLimit, '[') + TString(DepthLimit, ']')));
+}
+
+TEST(TYsonPullParserTest, ContextInExceptions)
+{
+ try {
+ GetYsonPullSignature("{foo bar = 580}");
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("bar = 580}"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST(TYsonPullParserTest, ContextInExceptions_ManyBlocks)
+{
+ try {
+ auto manyO = TString(100, 'o');
+ TStringBufVectorReader input(
+ {
+ "{fo",
+ manyO, // try to overflow 64 byte context
+ "o bar = 580}",
+ }
+ );
+ TYsonPullParser parser(&input, EYsonType::Node);
+ GetYsonPullSignature(&parser);
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("bar = 580}"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST(TYsonPullParserTest, ContextInExceptions_ContextAtTheVeryBeginning)
+{
+ try {
+ GetYsonPullSignature("! foo bar baz");
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("! foo bar"));
+ return;
+ }
+ GTEST_FAIL() << "Expected exception to be thrown";
+}
+
+TEST(TYsonPullParserTest, ContextInExceptions_Margin)
+{
+ try {
+ auto manyO = TString(100, 'o');
+ TStringBufVectorReader input(
+ {
+ "{fo",
+ manyO, // try to overflow 64 byte context
+ "a",
+ "b",
+ "c",
+ "d bar = 580}",
+ }
+ );
+ TYsonPullParser parser(&input, EYsonType::Node);
+ GetYsonPullSignature(&parser);
+ } catch (const std::exception& ex) {
+ EXPECT_THAT(ex.what(), testing::HasSubstr("oabcd bar = 580}"));
+ return;
+ }
+}
+
+std::optional<i64> ParseOptionalZigZagVarint(TYsonPullParser& parser)
+{
+ char buffer[MaxVarInt64Size];
+ auto len = parser.ParseOptionalInt64AsZigzagVarint(buffer);
+ if (len == 0) {
+ return {};
+ }
+ i64 value;
+ ReadVarInt64(buffer, buffer + len, &value);
+ return value;
+}
+
+std::optional<ui64> ParseOptionalVarint(TYsonPullParser& parser)
+{
+ char buffer[MaxVarUint64Size];
+ auto len = parser.ParseOptionalUint64AsVarint(buffer);
+ if (len == 0) {
+ return {};
+ }
+ ui64 value;
+ ReadVarUint64(buffer, buffer + len, &value);
+ return value;
+};
+
+i64 ParseZigZagVarint(TYsonPullParser& parser)
+{
+ char buffer[MaxVarInt64Size];
+ auto len = parser.ParseOptionalInt64AsZigzagVarint(buffer);
+ i64 value;
+ ReadVarInt64(buffer, buffer + len, &value);
+ return value;
+}
+
+ui64 ParseVarint(TYsonPullParser& parser)
+{
+ char buffer[MaxVarUint64Size];
+ auto len = parser.ParseOptionalUint64AsVarint(buffer);
+ ui64 value;
+ ReadVarUint64(buffer, buffer + len, &value);
+ return value;
+};
+
+TEST(TYsonPullParserTest, TypedParsingBasicCases)
+{
+ TStringBufVectorReader input(
+ {
+ "["
+ "["
+ "-100500;\x02\xa7\xa2\x0c;" "-100500;\x02\xa7\xa2\x0c;#;"
+ "-100500;\x02\xa7\xa2\x0c;" "-100500;\x02\xa7\xa2\x0c;#;"
+ "100500u;\x06\x94\x91\x06;" "100500u;\x06\x94\x91\x06;#;"
+ "100500u;\x06\x94\x91\x06;" "100500u;\x06\x94\x91\x06;#;"
+ "2.72;\x03iW\x14\x8B\n\xBF\5@;" "2.72;\x03iW\x14\x8B\n\xBF\5@;#;"
+ "\"bar\";" "\x01\x06" "bar;" "\"bar\";" "\x01\x06" "bar;" "#;"
+ "%true;\x05;%false;\x04;" "%true;\x05;%false;\x04;#;"
+ "];"
+ "#;"
+ "]"
+ }
+ );
+
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_TRUE(parser.ParseOptionalBeginList());
+ parser.ParseBeginList();
+
+ EXPECT_FALSE(parser.IsEndList());
+
+ EXPECT_EQ(parser.ParseInt64(), -100500);
+ EXPECT_EQ(parser.ParseInt64(), -100500);
+ EXPECT_EQ(parser.ParseOptionalInt64(), std::optional<i64>(-100500));
+ EXPECT_EQ(parser.ParseOptionalInt64(), std::optional<i64>(-100500));
+ EXPECT_EQ(parser.ParseOptionalInt64(), std::optional<i64>{});
+
+ EXPECT_EQ(ParseZigZagVarint(parser), -100500);
+ EXPECT_EQ(ParseZigZagVarint(parser), -100500);
+ EXPECT_EQ(ParseOptionalZigZagVarint(parser), std::optional<i64>(-100500));
+ EXPECT_EQ(ParseOptionalZigZagVarint(parser), std::optional<i64>(-100500));
+ EXPECT_EQ(ParseOptionalZigZagVarint(parser), std::optional<i64>{});
+
+ EXPECT_EQ(parser.ParseUint64(), 100500u);
+ EXPECT_EQ(parser.ParseUint64(), 100500u);
+ EXPECT_EQ(parser.ParseOptionalUint64(), std::optional<ui64>(100500));
+ EXPECT_EQ(parser.ParseOptionalUint64(), std::optional<ui64>(100500));
+ EXPECT_EQ(parser.ParseOptionalUint64(), std::optional<ui64>{});
+
+ EXPECT_EQ(ParseVarint(parser), 100500u);
+ EXPECT_EQ(ParseVarint(parser), 100500u);
+ EXPECT_EQ(ParseOptionalVarint(parser), std::optional<ui64>(100500));
+ EXPECT_EQ(ParseOptionalVarint(parser), std::optional<ui64>(100500));
+ EXPECT_EQ(ParseOptionalVarint(parser), std::optional<ui64>{});
+
+ EXPECT_DOUBLE_EQ(parser.ParseDouble(), 2.72);
+ EXPECT_DOUBLE_EQ(parser.ParseDouble(), 2.7182818284590451);
+ EXPECT_DOUBLE_EQ(*parser.ParseOptionalDouble(), 2.72);
+ EXPECT_DOUBLE_EQ(*parser.ParseOptionalDouble(), 2.7182818284590451);
+ EXPECT_EQ(parser.ParseOptionalDouble(), std::optional<double>{});
+
+ EXPECT_EQ(parser.ParseString(), "bar");
+ EXPECT_EQ(parser.ParseString(), "bar");
+ EXPECT_EQ(parser.ParseOptionalString(), std::optional<TStringBuf>("bar"));
+ EXPECT_EQ(parser.ParseOptionalString(), std::optional<TStringBuf>("bar"));
+ EXPECT_EQ(parser.ParseOptionalString(), std::optional<TStringBuf>{});
+
+ EXPECT_FALSE(parser.IsEndList());
+
+ EXPECT_EQ(parser.ParseBoolean(), true);
+ EXPECT_EQ(parser.ParseBoolean(), true);
+ EXPECT_EQ(parser.ParseBoolean(), false);
+ EXPECT_EQ(parser.ParseBoolean(), false);
+ EXPECT_EQ(parser.ParseOptionalBoolean(), std::optional<bool>(true));
+ EXPECT_EQ(parser.ParseOptionalBoolean(), std::optional<bool>(true));
+ EXPECT_EQ(parser.ParseOptionalBoolean(), std::optional<bool>(false));
+ EXPECT_EQ(parser.ParseOptionalBoolean(), std::optional<bool>(false));
+ EXPECT_EQ(parser.ParseOptionalBoolean(), std::optional<bool>{});
+
+ EXPECT_TRUE(parser.IsEndList());
+ parser.ParseEndList();
+
+ EXPECT_FALSE(parser.ParseOptionalBeginList());
+ parser.ParseEndList();
+
+ EXPECT_EQ(parser.Next().GetType(), EYsonItemType::EndOfStream);
+}
+
+TEST(TYsonPullParserTest, TypedParsingBasicErrors)
+{
+ {
+ TStringBufVectorReader input({"100u"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_THROW_THAT(parser.ParseInt64(), ::testing::HasSubstr("expected \"int64_value\""));
+ }
+ {
+ TStringBufVectorReader input({"\x06\x94\x91\x06"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_THROW_THAT(parser.ParseInt64(), ::testing::HasSubstr("expected \"int64_value\""));
+ }
+ {
+ TStringBufVectorReader input({"-100"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_THROW_THAT(parser.ParseUint64(), ::testing::HasSubstr("expected \"uint64_value\""));
+ }
+ {
+ TStringBufVectorReader input({"\x02\xa7\xa2\x0c"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_THROW_THAT(parser.ParseUint64(), ::testing::HasSubstr("expected \"uint64_value\""));
+ }
+ {
+ TStringBufVectorReader input({"[1;;]"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.ParseBeginList());
+ EXPECT_NO_THROW(parser.ParseInt64());
+ EXPECT_THROW_THAT(parser.ParseEndList(), ::testing::HasSubstr("Unexpected \";\""));
+ }
+ {
+ TStringBufVectorReader input({"[1;;]"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.ParseBeginList());
+ EXPECT_NO_THROW(parser.ParseInt64());
+ EXPECT_THROW_THAT(parser.IsEndList(), ::testing::HasSubstr("Unexpected \";\""));
+ }
+ {
+ TStringBufVectorReader input({"[1;]]"});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.ParseBeginList());
+ EXPECT_NO_THROW(parser.ParseInt64());
+ EXPECT_NO_THROW(parser.ParseEndList());
+ EXPECT_THROW_THAT(parser.ParseEndList(), ::testing::HasSubstr("Unexpected \"]\""));
+ }
+}
+
+TEST(TYsonPullParserTest, TestTransferValueViaTokenWriterBasicCases)
+{
+ TStringBuf inputString = "[ [ {foo=<attr=value;>bar; qux=[-1; 2u; %false; 3.14; lol; # ; ] ; }; ] ; 6; ]";
+
+ auto output = TString();
+ {
+ TStringOutput outputStream(output);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ TStringBufVectorReader input({inputString});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ parser.TransferComplexValue(&tokenWriter);
+ EXPECT_EQ(parser.Next().GetType(), EYsonItemType::EndOfStream);
+ }
+ auto outputWithPreviousItem = TString();
+ {
+ TStringOutput outputStream(outputWithPreviousItem);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ TStringBufVectorReader input({inputString});
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto firstItem = parser.Next();
+ parser.TransferComplexValue(&tokenWriter, firstItem);
+ EXPECT_EQ(parser.Next().GetType(), EYsonItemType::EndOfStream);
+ }
+ auto expectedOutput = TString();
+ {
+ TStringOutput outputStream(expectedOutput);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBeginMap();
+ tokenWriter.WriteBinaryString("foo");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBeginAttributes();
+ tokenWriter.WriteBinaryString("attr");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBinaryString("value");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndAttributes();
+ tokenWriter.WriteBinaryString("bar");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryString("qux");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBinaryInt64(-1);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryUint64(2);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryBoolean(false);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryDouble(3.14);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryString("lol");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEntity();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndMap();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryInt64(6);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ }
+ EXPECT_EQ(expectedOutput, output);
+ EXPECT_EQ(expectedOutput, outputWithPreviousItem);
+
+ {
+ TStringBufVectorReader input({
+ "<a=b;c=d;>12"
+ });
+
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto item = parser.Next();
+ EXPECT_EQ(item.GetType(), EYsonItemType::BeginAttributes);
+
+ auto output = TString();
+ {
+ TStringOutput outputStream(output);
+ TCheckedInDebugYsonTokenWriter writer(&outputStream);
+ EXPECT_NO_THROW(parser.TransferAttributes(&writer, item));
+ }
+
+ i64 x;
+ EXPECT_NO_THROW(x = parser.ParseInt64());
+ EXPECT_EQ(x, 12);
+
+ {
+ TStringStream expectedOutput;
+ TCheckedInDebugYsonTokenWriter tokenWriter(&expectedOutput);
+ tokenWriter.WriteBeginAttributes();
+ tokenWriter.WriteBinaryString("a");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBinaryString("b");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryString("c");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBinaryString("d");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndAttributes();
+ tokenWriter.Flush();
+ EXPECT_EQ(expectedOutput.Str(), output);
+ }
+ }
+}
+
+TEST(TYsonPullParserTest, TestSkipValueBasicCases)
+{
+ {
+ TStringBufVectorReader input({
+ "[<a=b;c=d;>12; 13; qux;]"
+ });
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.ParseBeginList());
+ auto item = parser.Next();
+ EXPECT_EQ(item, TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ EXPECT_NO_THROW(parser.SkipComplexValueOrAttributes(item));
+ item = parser.Next();
+ EXPECT_EQ(item, TYsonItem::Int64(12));
+ EXPECT_NO_THROW(parser.SkipComplexValueOrAttributes(item));
+ i64 x;
+ EXPECT_NO_THROW(x = parser.ParseInt64());
+ EXPECT_EQ(x, 13);
+ TString s;
+ EXPECT_NO_THROW(s = parser.ParseString());
+ EXPECT_EQ(s, "qux");
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndList));
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBufVectorReader input({
+ "[<a=b;c=d;>12; 13; qux;]"
+ });
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.ParseBeginList());
+ auto item = parser.Next();
+ EXPECT_EQ(item.GetType(), EYsonItemType::BeginAttributes);
+ EXPECT_NO_THROW(parser.SkipAttributes(item));
+ item = parser.Next();
+ EXPECT_THROW_THAT(parser.SkipAttributes(item), ::testing::HasSubstr("attributes"));
+ EXPECT_EQ(item, TYsonItem::Int64(12));
+ EXPECT_NO_THROW(parser.SkipComplexValue(item));
+ EXPECT_NO_THROW(parser.SkipComplexValue());
+ TString s;
+ EXPECT_NO_THROW(s = parser.ParseString());
+ EXPECT_EQ(s, "qux");
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndList));
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBufVectorReader input({
+ "<a=b;c=d;>12"
+ });
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto item = parser.Next();
+ EXPECT_EQ(item.GetType(), EYsonItemType::BeginAttributes);
+ EXPECT_NO_THROW(parser.SkipComplexValueOrAttributes(item));
+ i64 x;
+ EXPECT_NO_THROW(x = parser.ParseInt64());
+ EXPECT_EQ(x, 12);
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBufVectorReader input({
+ "<a=b;c=d;>12"
+ });
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto item = parser.Next();
+ EXPECT_EQ(item.GetType(), EYsonItemType::BeginAttributes);
+ EXPECT_NO_THROW(parser.SkipComplexValue(item));
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBufVectorReader input({
+ "<a=b;c=d;>12"
+ });
+ TYsonPullParser parser(&input, EYsonType::Node);
+ EXPECT_NO_THROW(parser.SkipComplexValue());
+ EXPECT_EQ(parser.Next(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+}
+
+TEST(TYsonPullParserCursorTest, TestTransferValueBasicCases)
+{
+ TStringBuf input = "[ [ {foo=<attr=value>bar; qux=[-1; 2u; %false; 3.14; lol; # ]} ] ; 6 ]";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+ {
+ ::testing::StrictMock<TMockYsonConsumer> mock;
+ {
+ ::testing::InSequence g;
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("foo"));
+ EXPECT_CALL(mock, OnBeginAttributes());
+ EXPECT_CALL(mock, OnKeyedItem("attr"));
+ EXPECT_CALL(mock, OnStringScalar("value"));
+ EXPECT_CALL(mock, OnEndAttributes());
+ EXPECT_CALL(mock, OnStringScalar("bar"));
+ EXPECT_CALL(mock, OnKeyedItem("qux"));
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnInt64Scalar(-1));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnUint64Scalar(2));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBooleanScalar(false));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnDoubleScalar(::testing::DoubleEq(3.14)));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("lol"));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnEntity());
+ EXPECT_CALL(mock, OnEndList());
+ EXPECT_CALL(mock, OnEndMap());
+ EXPECT_CALL(mock, OnEndList());
+ }
+ cursor.TransferComplexValue(&mock);
+ }
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(6));
+}
+
+
+TEST(TYsonPullParserCursorTest, TestTransferAttributesBasicCases)
+{
+ TStringBuf input = "[<attr=value>bar; qux; 2]";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+ {
+ ::testing::StrictMock<TMockYsonConsumer> mock;
+ {
+ ::testing::InSequence g;
+ EXPECT_CALL(mock, OnBeginAttributes());
+ EXPECT_CALL(mock, OnKeyedItem("attr"));
+ EXPECT_CALL(mock, OnStringScalar("value"));
+ EXPECT_CALL(mock, OnEndAttributes());
+ }
+ cursor.TransferAttributes(&mock);
+ }
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("bar"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("qux"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(2));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+}
+
+TEST(TYsonPullParserCursorTest, TestTransferValueViaTokenWriterBasicCases)
+{
+ TStringBuf input = "[ [ {foo=<attr=value;>bar; qux=[-1; 2u; %false; 3.14; lol; # ; ] ; }; ] ; 6; ]";
+ auto output1 = TString();
+ {
+ TStringOutput outputStream(output1);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.TransferComplexValue(&tokenWriter);
+ }
+ auto output2 = TString();
+ {
+ TStringOutput outputStream(output2);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBeginMap();
+ tokenWriter.WriteBinaryString("foo");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBeginAttributes();
+ tokenWriter.WriteBinaryString("attr");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBinaryString("value");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndAttributes();
+ tokenWriter.WriteBinaryString("bar");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryString("qux");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBeginList();
+ tokenWriter.WriteBinaryInt64(-1);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryUint64(2);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryBoolean(false);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryDouble(3.14);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryString("lol");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEntity();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndMap();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteBinaryInt64(6);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndList();
+ }
+ EXPECT_EQ(output1, output2);
+}
+
+TEST(TYsonPullParserCursorTest, TestTransferAttributesViaTokenWriterBasicCases)
+{
+ TStringBuf input = "[<attr=value;>bar; qux; 2;]";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+
+ auto output1 = TString();
+ {
+ TStringOutput outputStream(output1);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ cursor.TransferAttributes(&tokenWriter);
+ }
+ auto output2 = TString();
+ {
+ TStringOutput outputStream(output2);
+ TCheckedInDebugYsonTokenWriter tokenWriter(&outputStream);
+ tokenWriter.WriteBeginAttributes();
+ tokenWriter.WriteBinaryString("attr");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteBinaryString("value");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndAttributes();
+ }
+ EXPECT_EQ(output1, output2);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("bar"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("qux"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(2));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+}
+
+TEST(TYsonPullParserCursorTest, TestSkipValueBasicCases)
+{
+ {
+ TStringBuf input = "[<a=b;c=d;>12; qux;]";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ EXPECT_NO_THROW(cursor.SkipAttributes());
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(12));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("qux"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBuf input = "[<a=b;c=d;>12; 13; qux;]";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ EXPECT_NO_THROW(cursor.SkipComplexValue());
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(13));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("qux"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+}
+
+TEST(TYsonPullParserCursorTest, TestParseCompoundBasicCases)
+{
+ {
+ TStringBuf input = "[[1;2]; <x=1;y=2>%true; #]";
+ auto cursor = TStringBufCursor(input);
+ int timesCalled = 0;
+ auto listConsumer = [&] (TYsonPullParserCursor* cursor) {
+ switch (timesCalled) {
+ case 0:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(1));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(2));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor->Next();
+ break;
+ case 1:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("x"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(1));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("y"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(2));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EndAttributes));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Boolean(true));
+ cursor->Next();
+ break;
+ case 2:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EntityValue));
+ cursor->Next();
+ break;
+ default:
+ GTEST_FAIL() << "Times called is not 0, 1 or 2: " << timesCalled;
+ }
+ ++timesCalled;
+ };
+ EXPECT_NO_THROW(cursor.ParseList(listConsumer));
+ EXPECT_EQ(timesCalled, 3);
+ }
+
+ auto makeKeyValueConsumer = [] (int& timesCalled) {
+ auto fun = [&] (TYsonPullParserCursor* cursor) {
+ switch (timesCalled) {
+ case 0:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("a"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(1));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(2));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor->Next();
+ break;
+ case 1:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("b"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginAttributes));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("x"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(1));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("y"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Int64(2));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EndAttributes));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Boolean(true));
+ cursor->Next();
+ break;
+ case 2:
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::String("c"));
+ cursor->Next();
+ EXPECT_EQ(cursor->GetCurrent(), TYsonItem::Simple(EYsonItemType::EntityValue));
+ cursor->Next();
+ break;
+ default:
+ GTEST_FAIL() << "Times called is not 0, 1 or 2: " << timesCalled;
+ }
+ ++timesCalled;
+ };
+ return fun;
+ };
+
+ {
+ TStringBuf input = "{a=[1;2]; b=<x=1;y=2>%true; c=#}";
+ auto cursor = TStringBufCursor(input);
+ int timesCalled = 0;
+ EXPECT_NO_THROW(cursor.ParseMap(makeKeyValueConsumer(timesCalled)));
+ EXPECT_EQ(timesCalled, 3);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+ {
+ TStringBuf input = "<a=[1;2]; b=<x=1;y=2>%true; c=#>[x; 3]";
+ auto cursor = TStringBufCursor(input);
+ int timesCalled = 0;
+ EXPECT_NO_THROW(cursor.ParseAttributes(makeKeyValueConsumer(timesCalled)));
+ EXPECT_EQ(timesCalled, 3);
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::BeginList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::String("x"));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Int64(3));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndList));
+ cursor.Next();
+ EXPECT_EQ(cursor.GetCurrent(), TYsonItem::Simple(EYsonItemType::EndOfStream));
+ }
+}
+
+TEST(TYsonPullParserCursorTest, TestParseBoolAndNan)
+{
+ {
+ TStringBuf input = "%nan";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent().GetType(), EYsonItemType::DoubleValue);
+ EXPECT_TRUE(std::isnan(cursor.GetCurrent().UncheckedAsDouble()));
+ }
+ {
+ TStringBuf input = "%true";
+ auto cursor = TStringBufCursor(input);
+ EXPECT_EQ(cursor.GetCurrent().GetType(), EYsonItemType::BooleanValue);
+ EXPECT_TRUE(cursor.GetCurrent().UncheckedAsBoolean());
+ }
+}
+
+TEST(TYsonPullParserCursorTest, ListFragment)
+{
+ {
+ TStringBuf input = "1;2;3";
+ auto cursor = TStringBufCursor(input, EYsonType::ListFragment);
+ EXPECT_TRUE(cursor.TryConsumeFragmentStart());
+ EXPECT_FALSE(cursor.TryConsumeFragmentStart());
+ cursor.Next();
+ EXPECT_FALSE(cursor.TryConsumeFragmentStart());
+ }
+ {
+ TStringBuf input = "1;2;3";
+ auto cursor = TStringBufCursor(input, EYsonType::ListFragment);
+ EXPECT_TRUE(cursor.TryConsumeFragmentStart());
+
+ std::vector<int> expected = {1, 2, 3};
+ for (auto expectedEl : expected) {
+ EXPECT_EQ(cursor->UncheckedAsInt64(), expectedEl);
+ cursor.Next();
+ }
+ EXPECT_EQ(cursor->GetType(), EYsonItemType::EndOfStream);
+ }
+}
+
+TEST(TYsonPullParserCursorTest, MapFragment)
+{
+ {
+ TStringBuf input = "a=1;b=2;c=3";
+ auto cursor = TStringBufCursor(input, EYsonType::MapFragment);
+ EXPECT_TRUE(cursor.TryConsumeFragmentStart());
+ EXPECT_FALSE(cursor.TryConsumeFragmentStart());
+ cursor.Next();
+ EXPECT_FALSE(cursor.TryConsumeFragmentStart());
+ }
+ {
+ TStringBuf input = "a=1;b=2;c=3";
+ auto cursor = TStringBufCursor(input, EYsonType::MapFragment);
+ EXPECT_TRUE(cursor.TryConsumeFragmentStart());
+
+ std::vector<std::pair<TString, int>> expected = {{"a", 1}, {"b", 2}, {"c", 3}};
+ for (const auto& [key, value] : expected) {
+ EXPECT_EQ(cursor->UncheckedAsString(), key);
+ cursor.Next();
+ EXPECT_EQ(cursor->UncheckedAsInt64(), value);
+ cursor.Next();
+ }
+ EXPECT_EQ(cursor->GetType(), EYsonItemType::EndOfStream);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/yson/unittests/yson_token_writer_ut.cpp b/yt/yt/core/yson/unittests/yson_token_writer_ut.cpp
new file mode 100644
index 0000000000..0ae16ed98d
--- /dev/null
+++ b/yt/yt/core/yson/unittests/yson_token_writer_ut.cpp
@@ -0,0 +1,267 @@
+#include <yt/yt/core/test_framework/fixed_growth_string_output.h>
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/token_writer.h>
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NYson {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IntListTest(EYsonFormat format, size_t stringBufferSize)
+{
+ TString out1, out2;
+ std::vector<i64> ints = {1LL << 62, 12345678, 1, -12345678, 12, -(1LL << 62), 12345678910121LL, -12345678910121LL, 1, -1, 2, -2, 0};
+ std::vector<ui64> uints = {1ULL << 63, 1, 10, 100, 1000000000000, 0, 1ULL << 31};
+
+ {
+ TFixedGrowthStringOutput outStream(&out1, stringBufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+
+ writer.WriteBeginList();
+ for (i64 val : ints) {
+ if (format == EYsonFormat::Binary) {
+ writer.WriteBinaryInt64(val);
+ } else {
+ writer.WriteTextInt64(val);
+ }
+ writer.WriteItemSeparator();
+ }
+ for (ui64 val : uints) {
+ if (format == EYsonFormat::Binary) {
+ writer.WriteBinaryUint64(val);
+ } else {
+ writer.WriteTextUint64(val);
+ }
+ writer.WriteItemSeparator();
+ }
+ writer.WriteEndList();
+
+ writer.Finish();
+ }
+
+ {
+ TStringOutput outStream(out2);
+ TYsonWriter writer(&outStream, format);
+
+ writer.OnBeginList();
+ for (i64 val : ints) {
+ writer.OnListItem();
+ writer.OnInt64Scalar(val);
+ }
+ for (ui64 val : uints) {
+ writer.OnListItem();
+ writer.OnUint64Scalar(val);
+ }
+ writer.OnEndList();
+
+ writer.Flush();
+ }
+
+ EXPECT_EQ(out1, out2);
+}
+
+TEST(TYsonTokenWriterTest, BinaryIntList)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ IntListTest(EYsonFormat::Binary, bufferSize);
+ }
+}
+
+TEST(TYsonTokenWriterTest, TextIntList)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ IntListTest(EYsonFormat::Text, bufferSize);
+ }
+}
+
+TEST(TYsonTokenWriterTest, BinaryString)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+ writer.WriteBinaryString("Hello, world!");
+ writer.Finish();
+
+ EXPECT_EQ(out, "\1\x1AHello, world!");
+ }
+}
+
+TEST(TYsonTokenWriterTest, TextString)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+ writer.WriteTextString("Hello, world!");
+ writer.Finish();
+
+ EXPECT_EQ(out, "\"Hello, world!\"");
+ }
+}
+
+TEST(TYsonTokenWriterTest, DifferentTypesBinaryMap)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+
+ writer.WriteBeginAttributes();
+ writer.WriteBinaryString("type");
+ writer.WriteKeyValueSeparator();
+ writer.WriteBinaryString("map");
+ writer.WriteItemSeparator();
+ writer.WriteEndAttributes();
+
+ writer.WriteBeginMap();
+ writer.WriteBinaryString("double");
+ writer.WriteKeyValueSeparator();
+ writer.WriteBinaryDouble(2.71828);
+ writer.WriteItemSeparator();
+ writer.WriteBinaryString("boolean");
+ writer.WriteKeyValueSeparator();
+ writer.WriteBinaryBoolean(true);
+ writer.WriteItemSeparator();
+ writer.WriteBinaryString("entity");
+ writer.WriteKeyValueSeparator();
+ writer.WriteEntity();
+ writer.WriteItemSeparator();
+ writer.WriteEndMap();
+
+ writer.Finish();
+
+ EXPECT_EQ(out, "<\1\x08type=\1\6map;>{\1\014double=\3\x90\xF7\xAA\x95\t\xBF\5@;\1\016boolean=\5;\1\014entity=#;}");
+ }
+}
+
+TEST(TYsonTokenWriterTest, DifferentTypesTextMap)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+
+ writer.WriteBeginAttributes();
+ writer.WriteTextString("type");
+ writer.WriteKeyValueSeparator();
+ writer.WriteTextString("map");
+ writer.WriteItemSeparator();
+ writer.WriteEndAttributes();
+
+ writer.WriteBeginMap();
+ writer.WriteTextString("double");
+ writer.WriteKeyValueSeparator();
+ writer.WriteTextDouble(2.71828);
+ writer.WriteItemSeparator();
+ writer.WriteTextString("boolean");
+ writer.WriteKeyValueSeparator();
+ writer.WriteTextBoolean(true);
+ writer.WriteItemSeparator();
+ writer.WriteTextString("entity");
+ writer.WriteKeyValueSeparator();
+ writer.WriteEntity();
+ writer.WriteItemSeparator();
+ writer.WriteEndMap();
+
+ writer.Finish();
+
+ EXPECT_EQ(out, "<\"type\"=\"map\";>{\"double\"=2.71828;\"boolean\"=%true;\"entity\"=#;}");
+ }
+}
+
+TEST(TYsonTokenWriterTest, WriteRawNodeUnchecked)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TCheckedYsonTokenWriter writer(&outStream);
+
+ writer.WriteBeginList();
+ writer.WriteRawNodeUnchecked("<a=b>{x=1;y=z}");
+ writer.WriteItemSeparator();
+ writer.WriteEndList();
+
+ writer.Finish();
+
+ EXPECT_EQ(out, "[<a=b>{x=1;y=z};]");
+ }
+
+ {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, /* bufferSize */ 20);
+ TCheckedYsonTokenWriter writer(&outStream);
+
+ writer.WriteBeginMap();
+ EXPECT_THROW_THAT(writer.WriteRawNodeUnchecked("<a=b>{x=1;y=z}"), ::testing::HasSubstr("expected \"string\""));
+ }
+}
+
+TEST(TYsonTokenWriterTest, ThroughZeroCopyOutputStreamWriter)
+{
+ for (size_t bufferSize = 1; bufferSize <= 20; ++bufferSize) {
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, bufferSize);
+ TZeroCopyOutputStreamWriter writer(&outStream);
+
+ TStringBuf prefix = "Now some YSON: ";
+ writer.Write(prefix.data(), prefix.size());
+
+ TCheckedYsonTokenWriter tokenWriter(&writer);
+
+ tokenWriter.WriteBeginAttributes();
+ tokenWriter.WriteTextString("type");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteTextString("map");
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndAttributes();
+
+ tokenWriter.WriteBeginMap();
+ tokenWriter.WriteTextString("double");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteTextDouble(2.71828);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteTextString("boolean");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteTextBoolean(true);
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteTextString("entity");
+ tokenWriter.WriteKeyValueSeparator();
+ tokenWriter.WriteEntity();
+ tokenWriter.WriteItemSeparator();
+ tokenWriter.WriteEndMap();
+
+ tokenWriter.Finish();
+
+ TStringBuf suffix = " -- no more YSON";
+ writer.Write(suffix.data(), suffix.size());
+ writer.UndoRemaining();
+ outStream.Finish();
+
+ EXPECT_EQ(
+ out,
+ "Now some YSON: "
+ "<\"type\"=\"map\";>{\"double\"=2.71828;\"boolean\"=%true;\"entity\"=#;}"
+ " -- no more YSON");
+ }
+}
+
+TEST(TYsonTokenWriterTest, TotalWrittenSize)
+{
+ TString out;
+ TFixedGrowthStringOutput outStream(&out, 15);
+ TZeroCopyOutputStreamWriter writer(&outStream);
+ TUncheckedYsonTokenWriter tokenWriter(&writer);
+ EXPECT_EQ(tokenWriter.GetTotalWrittenSize(), 0u);
+ tokenWriter.WriteBinaryString("abcdef");
+ EXPECT_EQ(tokenWriter.GetTotalWrittenSize(), 8u);
+ tokenWriter.WriteBinaryString("abcd");
+ EXPECT_EQ(tokenWriter.GetTotalWrittenSize(), 14u);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/yson_ut.cpp b/yt/yt/core/yson/unittests/yson_ut.cpp
new file mode 100644
index 0000000000..a308b03d71
--- /dev/null
+++ b/yt/yt/core/yson/unittests/yson_ut.cpp
@@ -0,0 +1,498 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/yson/stream.h>
+#include <yt/yt/core/yson/string_merger.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT::NYson {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+using namespace NYTree;
+
+using TYsonStringTypes = ::testing::Types<TYsonString, TYsonStringBuf>;
+
+template <typename T>
+class TYsonTypedTest
+ : public ::testing::Test
+{ };
+
+TYPED_TEST_SUITE(TYsonTypedTest, TYsonStringTypes);
+
+TYPED_TEST(TYsonTypedTest, GetYPath)
+{
+ TString yson = "{key=value; submap={ other_key=other_value; }}";
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(yson));
+
+ EXPECT_EQ("/submap/other_key", node->AsMap()->GetChildOrThrow("submap")->AsMap()->GetChildOrThrow("other_key")->GetPath());
+}
+
+TYPED_TEST(TYsonTypedTest, SetNodeByYPath)
+{
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("{}")));
+ ForceYPath(node, "/submap/other_key");
+
+ auto submap = node->AsMap()->GetChildOrThrow("submap")->AsMap();
+ EXPECT_EQ(0, submap->GetChildCount());
+
+ auto value = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("4")));
+
+ SetNodeByYPath(node, "/submap/other_key", value);
+ submap = node->AsMap()->GetChildOrThrow("submap")->AsMap();
+ EXPECT_EQ(4, ConvertTo<int>(submap->GetChildOrThrow("other_key")));
+}
+
+TYPED_TEST(TYsonTypedTest, SetNodeByYPathForce)
+{
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("{}")));
+ auto value = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("77")));
+
+ SetNodeByYPath(node, "/submap/other_key", value, true);
+ auto submap = node->AsMap()->GetChildOrThrow("submap")->AsMap();
+ EXPECT_EQ(77, ConvertTo<int>(submap->GetChildOrThrow("other_key")));
+}
+
+TYPED_TEST(TYsonTypedTest, RemoveNodeByYPathMap)
+{
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("{x={y={z=1}}}")));
+ EXPECT_EQ(true, RemoveNodeByYPath(node, "/x/y/z"));
+
+ auto submap = node->AsMap()->GetChildOrThrow("x")->AsMap()->GetChildOrThrow("y")->AsMap();
+ EXPECT_EQ(0, submap->GetChildCount());
+}
+
+TYPED_TEST(TYsonTypedTest, RemoveNodeByYPathList)
+{
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("{x={y=[1]}}")));
+ EXPECT_EQ(true, RemoveNodeByYPath(node, "/x/y/0"));
+
+ auto sublist = node->AsMap()->GetChildOrThrow("x")->AsMap()->GetChildOrThrow("y")->AsList();
+ EXPECT_EQ(0, sublist->GetChildCount());
+}
+
+TYPED_TEST(TYsonTypedTest, RemoveNodeByYPathInvalid)
+{
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(TStringBuf("{map={a=1};list=[1]}")));
+ auto nodeCopy = CloneNode(node);
+ EXPECT_EQ(false, RemoveNodeByYPath(node, "/map/b"));
+ EXPECT_THROW(RemoveNodeByYPath(node, "/map/a/1"), std::exception);
+ EXPECT_EQ(false, RemoveNodeByYPath(node, "/map/1"));
+ EXPECT_EQ(false, RemoveNodeByYPath(node, "/list/1"));
+ EXPECT_THROW(RemoveNodeByYPath(node, "/list/a"), std::exception);
+ EXPECT_THROW(RemoveNodeByYPath(node, "/list/0/a"), std::exception);
+ EXPECT_TRUE(AreNodesEqual(node, nodeCopy));
+}
+
+TYPED_TEST(TYsonTypedTest, ConvertToNode)
+{
+ TString yson = "{key=value; other_key=10}";
+ auto node = NYT::NYTree::ConvertToNode(TypeParam(yson));
+
+ ASSERT_NO_THROW(node->AsMap());
+ ASSERT_THROW(node->AsList(), std::exception);
+ EXPECT_EQ("key", node->AsMap()->GetKeys().front());
+
+ EXPECT_EQ("{\"key\"=\"value\";\"other_key\"=10;}",
+ ConvertToYsonString(node, EYsonFormat::Text).AsStringBuf());
+
+ NYT::NYTree::INodePtr child;
+
+ child = node->AsMap()->FindChild("key");
+ for (auto format : TEnumTraits<EYsonFormat>::GetDomainValues()) {
+ EXPECT_EQ("value", ConvertTo<TString>(child));
+ EXPECT_EQ("value", ConvertTo<TString>(ConvertToYsonString(child, format)));
+ }
+ EXPECT_EQ(ConvertTo<TString>(ConvertToYsonString(child)), "value");
+
+ child = node->AsMap()->FindChild("other_key");
+ for (auto format : TEnumTraits<EYsonFormat>::GetDomainValues()) {
+ EXPECT_EQ(10, ConvertTo<i32>(child));
+ EXPECT_EQ(10, ConvertTo<i32>(ConvertToYsonString(child, format)));
+ }
+}
+
+TYPED_TEST(TYsonTypedTest, ListFragment)
+{
+ TString yson = "{a=b};{c=d}";
+ NYT::NYTree::INodePtr node;
+
+ node = NYT::NYTree::ConvertToNode(TypeParam(yson, EYsonType::ListFragment));
+ ASSERT_NO_THROW(node->AsList());
+ EXPECT_EQ("[{\"a\"=\"b\";};{\"c\"=\"d\";};]",
+ ConvertToYsonString(node, EYsonFormat::Text).AsStringBuf());
+}
+
+TYPED_TEST(TYsonTypedTest, ConvertFromStream)
+{
+ TString yson = "{key=value}";
+ TStringInput ysonStream(yson);
+
+ auto node = ConvertToNode(&ysonStream);
+ ASSERT_NO_THROW(node->AsMap());
+ EXPECT_EQ("{\"key\"=\"value\";}",
+ ConvertToYsonString(node, EYsonFormat::Text).AsStringBuf());
+}
+
+TYPED_TEST(TYsonTypedTest, ConvertToProducerNode)
+{
+ // Make consumer
+ TStringStream output;
+ TYsonWriter writer(&output, EYsonFormat::Text);
+
+ // Make producer
+ auto ysonProducer = ConvertToProducer(TypeParam(TStringBuf("{key=value}")));
+
+ // Apply producer to consumer
+ ysonProducer.Run(&writer);
+
+ EXPECT_EQ("{\"key\"=\"value\";}", output.Str());
+}
+
+TYPED_TEST(TYsonTypedTest, ConvertToProducerListFragment)
+{
+ {
+ auto producer = ConvertToProducer(TypeParam(TStringBuf("{a=b}; {c=d}"), EYsonType::ListFragment));
+ EXPECT_EQ("{\"a\"=\"b\";};\n{\"c\"=\"d\";};\n",
+ ConvertToYsonString(producer, EYsonFormat::Text).AsStringBuf());
+ }
+
+ {
+ auto producer = ConvertToProducer(TypeParam(TStringBuf("{key=value}")));
+ EXPECT_EQ("{\"key\"=\"value\";}",
+ ConvertToYsonString(producer, EYsonFormat::Text).AsStringBuf());
+ }
+}
+
+TYPED_TEST(TYsonTypedTest, ConvertToForPodTypes)
+{
+ {
+ auto node = ConvertToNode(42);
+ EXPECT_EQ(42, ConvertTo<i32>(node));
+ EXPECT_EQ(42, ConvertTo<i64>(node));
+
+ auto ysonStr = ConvertToYsonString(node, EYsonFormat::Text);
+ EXPECT_EQ("42", ysonStr.AsStringBuf());
+ EXPECT_EQ(42, ConvertTo<i32>(ysonStr));
+ EXPECT_EQ(42, ConvertTo<i64>(ysonStr));
+ EXPECT_EQ(42.0, ConvertTo<double>(ysonStr));
+ }
+
+ {
+ auto node = ConvertToNode(0.1);
+ EXPECT_EQ(0.1, ConvertTo<double>(node));
+
+ auto ysonStr = ConvertToYsonString(node, EYsonFormat::Text);
+ EXPECT_EQ("0.1", ysonStr.AsStringBuf());
+ EXPECT_EQ(0.1, ConvertTo<double>(ysonStr));
+ }
+
+ {
+ std::vector<i32> numbers;
+ numbers.push_back(1);
+ numbers.push_back(2);
+ auto node = ConvertToNode(numbers);
+ auto converted = ConvertTo< std::vector<i32> >(node);
+ EXPECT_EQ(numbers, converted);
+ auto yson = ConvertToYsonString(node, EYsonFormat::Text);
+ EXPECT_EQ(EYsonType::Node, yson.GetType());
+ EXPECT_EQ("[1;2;]", yson.AsStringBuf());
+ }
+
+ {
+ bool boolean = true;
+ auto node = ConvertToNode(boolean);
+ auto converted = ConvertTo<bool>(node);
+ EXPECT_EQ(boolean, converted);
+ auto yson = ConvertToYsonString(node, EYsonFormat::Text);
+ EXPECT_EQ(EYsonType::Node, yson.GetType());
+ EXPECT_EQ("%true", yson.AsStringBuf());
+ }
+
+ EXPECT_EQ(ConvertTo<bool>("false"), false);
+ EXPECT_EQ(ConvertTo<bool>(TypeParam(TStringBuf("%false"))), false);
+
+ EXPECT_EQ(ConvertTo<bool>("true"), true);
+ EXPECT_EQ(ConvertTo<bool>(TypeParam(TStringBuf("%true"))), true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonTest, UpdateNodes)
+{
+ auto base = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("key_a")
+ .Value(0)
+
+ .Item("key_b")
+ .BeginAttributes()
+ .Item("attr")
+ .Value("some_attr")
+ .EndAttributes()
+ .Value(3.0)
+
+ .Item("key_c")
+ .BeginMap()
+ .Item("ignat")
+ .Value(70.0)
+ .EndMap()
+ .EndMap();
+
+ auto patch = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("key_a")
+ .Value(100)
+
+ .Item("key_b")
+ .Value(0.0)
+
+ .Item("key_c")
+ .BeginMap()
+ .Item("max")
+ .Value(75.0)
+ .EndMap()
+
+ .Item("key_d")
+ .BeginMap()
+ .Item("x")
+ .Value("y")
+ .EndMap()
+ .EndMap();
+
+ auto res = PatchNode(base, patch);
+
+ EXPECT_EQ(
+ "100",
+ ConvertToYsonString(res->AsMap()->FindChild("key_a"), EYsonFormat::Text).AsStringBuf());
+ EXPECT_EQ(
+ "<\"attr\"=\"some_attr\";>0.",
+ ConvertToYsonString(res->AsMap()->FindChild("key_b"), EYsonFormat::Text).AsStringBuf());
+ EXPECT_EQ(
+ "70.",
+ ConvertToYsonString(res->AsMap()->FindChild("key_c")->AsMap()->FindChild("ignat"), EYsonFormat::Text).AsStringBuf());
+ EXPECT_EQ(
+ "75.",
+ ConvertToYsonString(res->AsMap()->FindChild("key_c")->AsMap()->FindChild("max"), EYsonFormat::Text).AsStringBuf());
+ EXPECT_EQ(
+ "{\"x\"=\"y\";}",
+ ConvertToYsonString(res->AsMap()->FindChild("key_d"), EYsonFormat::Text).AsStringBuf());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonTest, TYsonStringTypesConversion)
+{
+ auto ysonString = TYsonString(TStringBuf(("{x=y;z=1}")));
+ auto AsStringBuf = [] (const TYsonStringBuf& ysonStringBuf) {
+ return ysonStringBuf.AsStringBuf();
+ };
+ auto getType = [] (const TYsonStringBuf& ysonStringBuf) {
+ return ysonStringBuf.GetType();
+ };
+
+ // We expect these functions to cast arguments implicitly.
+ EXPECT_EQ(AsStringBuf(ysonString), ysonString.AsStringBuf());
+ EXPECT_EQ(getType(ysonString), ysonString.GetType());
+}
+
+TEST(TYsonTest, TYsonStringTypesHashing)
+{
+ auto ysonString = TYsonString(TStringBuf("{x=y;z=1}"));
+ auto ysonStringBuf = TYsonStringBuf(TStringBuf("{x=y;z=1}"));
+ EXPECT_EQ(THash<TYsonString>()(ysonString), THash<TYsonStringBuf>()(ysonStringBuf));
+}
+
+TEST(TYsonTest, TYsonStringTypesComparisons)
+{
+ auto ysonString = TYsonString(TStringBuf("{x=y;z=1}"));
+ auto ysonStringBuf = TYsonStringBuf(TStringBuf("{x=y;z=1}"));
+ EXPECT_EQ(ysonString, ysonString);
+ EXPECT_EQ(ysonStringBuf, ysonStringBuf);
+ EXPECT_EQ(ysonString, ysonStringBuf);
+ EXPECT_EQ(ysonStringBuf, ysonString);
+
+ auto otherYsonString = TYsonString(TStringBuf("{x=z;y=1}"));
+ auto otherYsonStringBuf = TYsonStringBuf(TStringBuf("{x=z;y=1}"));
+ EXPECT_NE(ysonString, otherYsonStringBuf);
+ EXPECT_NE(ysonStringBuf, otherYsonStringBuf);
+ EXPECT_NE(ysonString, otherYsonStringBuf);
+ EXPECT_NE(ysonStringBuf, otherYsonString);
+}
+
+TEST(TYsonTest, TYsonStringFromStringBuf)
+{
+ auto stringBuf = TStringBuf("test");
+ auto ysonString = TYsonString(stringBuf);
+ EXPECT_EQ(stringBuf, ysonString.AsStringBuf());
+ EXPECT_EQ(stringBuf, ysonString.ToString());
+}
+
+TEST(TYsonStringMerger, NestedPaths)
+{
+ using namespace std::literals;
+ {
+ auto parentYsonStringBuf = TYsonStringBuf{R"({a=1; b="eleven"; c={"1" = "one"; "2"="two"}})"sv};
+ auto child1YsonStringBuf = TYsonStringBuf{R"({"1"="one"; "2"="two"})"sv};
+ auto child2YsonStringBuf = TYsonStringBuf{R"("eleven")"sv};
+
+ auto mergedYsonString1 = MergeYsonStrings({"/c", "/b", ""}, {child1YsonStringBuf, child2YsonStringBuf, parentYsonStringBuf});
+ auto mergedYsonString2 = MergeYsonStrings({"", "/c", "/b"}, {parentYsonStringBuf, child1YsonStringBuf, child2YsonStringBuf});
+ auto mergedYsonString3 = MergeYsonStrings({"/c", "", "/b"}, {child1YsonStringBuf, parentYsonStringBuf, child2YsonStringBuf});
+
+ EXPECT_EQ(parentYsonStringBuf, mergedYsonString1);
+ EXPECT_EQ(parentYsonStringBuf, mergedYsonString2);
+ EXPECT_EQ(parentYsonStringBuf, mergedYsonString3);
+
+ auto node = ConvertToNode(mergedYsonString1);
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("a").Value(1)
+ .Item("b").Value("eleven")
+ .Item("c").BeginMap()
+ .Item("1").Value("one")
+ .Item("2").Value("two")
+ .EndMap()
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(node, expectedNode));
+ }
+
+ {
+ auto parentYsonStringBuf = TYsonStringBuf{"0"sv};
+ auto childYsonStringBuf = TYsonStringBuf{"1"sv};
+ auto leftoutYsonStringBuf = TYsonStringBuf{"2"sv};
+
+ auto mergedYsonString1 = MergeYsonStrings({"/path1/a", "/path1/a/b", "/path2"}, {parentYsonStringBuf, childYsonStringBuf, leftoutYsonStringBuf});
+ auto mergedYsonString2 = MergeYsonStrings({"/path1/a/b", "/path2", "/path1/a"}, {childYsonStringBuf, leftoutYsonStringBuf, parentYsonStringBuf});
+ auto mergedYsonString3 = MergeYsonStrings({"/path2", "/path1/a", "/path1/a/b"}, {leftoutYsonStringBuf, parentYsonStringBuf, childYsonStringBuf});
+
+ auto expectedYsonString = TYsonString{"{\x01\x0Apath1={\x01\x02""a=0;};\x01\x0Apath2=2;}"sv};
+
+ EXPECT_EQ(expectedYsonString.AsStringBuf(), mergedYsonString1.AsStringBuf());
+ EXPECT_EQ(expectedYsonString.AsStringBuf(), mergedYsonString2.AsStringBuf());
+ EXPECT_EQ(expectedYsonString.AsStringBuf(), mergedYsonString3.AsStringBuf());
+
+ auto node = ConvertToNode(mergedYsonString1);
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("path1").BeginMap()
+ .Item("a").Value(0)
+ .EndMap()
+ .Item("path2").Value(2)
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(node, expectedNode));
+ }
+
+ {
+ auto element0YsonStringBuf = TYsonStringBuf{R"({a=1})"sv};
+ auto element1YsonStringBuf = TYsonStringBuf{R"({b=1})"sv};
+ EXPECT_EQ(MergeYsonStrings({"/path", "/path"}, {element1YsonStringBuf, element0YsonStringBuf}).AsStringBuf(), "{\x01\x08path={a=1};}");
+ EXPECT_EQ(MergeYsonStrings({"/path", "/path"}, {element0YsonStringBuf, element1YsonStringBuf}).AsStringBuf(), "{\x01\x08path={b=1};}");
+ }
+}
+
+TEST(TYsonStringMerger, PathWithIndexes)
+{
+ using namespace std::literals;
+ auto element0YsonStringBuf = TYsonStringBuf{R"({a=1})"sv};
+ auto element1YsonStringBuf = TYsonStringBuf{R"({b=2})"sv};
+ auto mergedYsonString = MergeYsonStrings(
+ {"/map/0", "/map/5"},
+ {element0YsonStringBuf, element1YsonStringBuf},
+ EYsonFormat::Text);
+ auto expectedYsonString = TYsonString{R"({"map"={"0"={a=1};"5"={b=2};};})"sv};
+ EXPECT_EQ(mergedYsonString.AsStringBuf(), expectedYsonString.AsStringBuf());
+
+ auto node = ConvertToNode(mergedYsonString);
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("map").BeginMap()
+ .Item("0").BeginMap()
+ .Item("a").Value(1)
+ .EndMap()
+ .Item("5").BeginMap()
+ .Item("b").Value(2)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(node, expectedNode));
+}
+
+TEST(TYsonStringMerger, BinaryYsonStrings)
+{
+ using namespace std::literals;
+ auto element0YsonStringBuf = TYsonStringBuf{R"({a=1})"sv};
+ auto binaryYsonString = ConvertToYsonString(
+ BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("map").BeginMap()
+ .Item("int").Value(100)
+ .Item("str").Value("200")
+ .Item("bool").Value(true)
+ .EndMap()
+ .EndMap(),
+ EYsonFormat::Binary);
+ auto element1YsonStringBuf = TYsonStringBuf{binaryYsonString};
+ auto mergedYsonString = MergeYsonStrings(
+ {"/first_key", "/second_key"},
+ {element0YsonStringBuf, element1YsonStringBuf},
+ EYsonFormat::Text);
+
+ auto expectedYsonString = TYsonString{R"({"first_key"={a=1};"second_key"=)" + TString{element1YsonStringBuf.AsStringBuf()} + ";}"};
+ EXPECT_EQ(mergedYsonString.AsStringBuf(), expectedYsonString.AsStringBuf());
+
+ auto node = ConvertToNode(mergedYsonString);
+ auto expectedNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("first_key").BeginMap()
+ .Item("a").Value(1)
+ .EndMap()
+ .Item("second_key").BeginMap()
+ .Item("map").BeginMap()
+ .Item("int").Value(100)
+ .Item("str").Value("200")
+ .Item("bool").Value(true)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ EXPECT_TRUE(AreNodesEqual(node, expectedNode));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStringSerializationTest
+ : public ::testing::TestWithParam<TYsonString>
+{ };
+
+TEST_P(TYsonStringSerializationTest, Do)
+{
+ auto str = GetParam();
+ TString buffer;
+ TStringOutput output(buffer);
+ TStreamSaveContext saveContext(&output);
+ Save(saveContext, str);
+ TStringInput input(buffer);
+ TStreamLoadContext loadContext(&input);
+ EXPECT_EQ(str, Load<TYsonString>(loadContext));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Do,
+ TYsonStringSerializationTest,
+ ::testing::ValuesIn({
+ TYsonString(), // null
+ TYsonString(TStringBuf("test")),
+ TYsonString(TStringBuf("1;2;3"), EYsonType::ListFragment),
+ TYsonString(TStringBuf("a=1;b=2;c=3"), EYsonType::MapFragment),
+ }));
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/unittests/yson_writer_ut.cpp b/yt/yt/core/yson/unittests/yson_writer_ut.cpp
new file mode 100644
index 0000000000..e58be45da6
--- /dev/null
+++ b/yt/yt/core/yson/unittests/yson_writer_ut.cpp
@@ -0,0 +1,447 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/stream.h>
+
+#include <util/string/escape.h>
+
+namespace NYT::NYson {
+namespace {
+
+using ::testing::InSequence;
+using ::testing::StrictMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonWriterTest
+ : public ::testing::Test
+{
+protected:
+ TStringStream Stream;
+ StrictMock<TMockYsonConsumer> Mock;
+
+ void Run()
+ {
+ Stream.Flush();
+ ParseYson(TYsonInput(&Stream), &Mock);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define TEST_SCALAR(value, type) \
+ { \
+ InSequence dummy; \
+ EXPECT_CALL(Mock, On##type##Scalar(value)); \
+ TYsonWriter writer(&Stream, EYsonFormat::Binary); \
+ writer.On##type##Scalar(value); \
+ Run(); \
+ } \
+ \
+ { \
+ InSequence dummy; \
+ EXPECT_CALL(Mock, On##type##Scalar(value)); \
+ TYsonWriter writer(&Stream, EYsonFormat::Text); \
+ writer.On##type##Scalar(value); \
+ Run(); \
+ } \
+
+
+TEST_F(TYsonWriterTest, String)
+{
+ TString value = "YSON";
+ TEST_SCALAR(value, String)
+}
+
+TEST_F(TYsonWriterTest, Int64)
+{
+ i64 value = 100500424242ll;
+ TEST_SCALAR(value, Int64)
+}
+
+TEST_F(TYsonWriterTest, Uint64)
+{
+ ui64 value = 100500424242llu;
+ TEST_SCALAR(value, Uint64)
+}
+
+TEST_F(TYsonWriterTest, Boolean)
+{
+ bool value = true;
+ TEST_SCALAR(value, Boolean)
+}
+
+TEST_F(TYsonWriterTest, Double)
+{
+ double value = 1.7976931348623157e+308;
+ TEST_SCALAR(value, Double)
+}
+
+TEST_F(TYsonWriterTest, MinusInf)
+{
+ double value = std::numeric_limits<double>::infinity();
+ TEST_SCALAR(value, Double)
+}
+
+TEST_F(TYsonWriterTest, NaN)
+{
+ InSequence dummy;
+ TYsonWriter writer(&Stream, EYsonFormat::Text);
+ writer.OnDoubleScalar(std::numeric_limits<double>::quiet_NaN());
+ Stream.Flush();
+ EXPECT_EQ(Stream.Str(), "%nan");
+}
+
+TEST_F(TYsonWriterTest, EmptyMap)
+{
+
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TYsonWriter writer(&Stream, EYsonFormat::Binary);
+
+ writer.OnBeginMap();
+ writer.OnEndMap();
+
+ Run();
+}
+
+TEST_F(TYsonWriterTest, OneItemMap)
+{
+
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("hello"));
+ EXPECT_CALL(Mock, OnStringScalar("world"));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TYsonWriter writer(&Stream, EYsonFormat::Binary);
+
+ writer.OnBeginMap();
+ writer.OnKeyedItem("hello");
+ writer.OnStringScalar("world");
+ writer.OnEndMap();
+
+ Run();
+}
+
+TEST_F(TYsonWriterTest, MapWithAttributes)
+{
+ InSequence dummy;
+
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("acl"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("read"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("*"));
+ EXPECT_CALL(Mock, OnEndList());
+
+ EXPECT_CALL(Mock, OnKeyedItem("write"));
+ EXPECT_CALL(Mock, OnBeginList());
+ EXPECT_CALL(Mock, OnListItem());
+ EXPECT_CALL(Mock, OnStringScalar("sandello"));
+ EXPECT_CALL(Mock, OnEndList());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ EXPECT_CALL(Mock, OnKeyedItem("lock_scope"));
+ EXPECT_CALL(Mock, OnStringScalar("mytables"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("path"));
+ EXPECT_CALL(Mock, OnStringScalar("/home/sandello"));
+
+ EXPECT_CALL(Mock, OnKeyedItem("mode"));
+ EXPECT_CALL(Mock, OnInt64Scalar(755));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ TYsonWriter writer(&Stream, EYsonFormat::Binary);
+
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("acl");
+ writer.OnBeginMap();
+ writer.OnKeyedItem("read");
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnStringScalar("*");
+ writer.OnEndList();
+
+ writer.OnKeyedItem("write");
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnStringScalar("sandello");
+ writer.OnEndList();
+ writer.OnEndMap();
+
+ writer.OnKeyedItem("lock_scope");
+ writer.OnStringScalar("mytables");
+ writer.OnEndAttributes();
+
+ writer.OnBeginMap();
+ writer.OnKeyedItem("path");
+ writer.OnStringScalar("/home/sandello");
+
+ writer.OnKeyedItem("mode");
+ writer.OnInt64Scalar(755);
+ writer.OnEndMap();
+
+ Run();
+}
+
+TEST_F(TYsonWriterTest, Escaping)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream, EYsonFormat::Text);
+
+ TString input;
+ for (int i = 0; i < 256; ++i) {
+ input.push_back(char(i));
+ }
+
+ writer.OnStringScalar(input);
+
+ TString output =
+ "\"\\0\\1\\2\\3\\4\\5\\6\\7\\x08\\t\\n\\x0B\\x0C\\r\\x0E\\x0F"
+ "\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B"
+ "\\x1C\\x1D\\x1E\\x1F !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCD"
+ "EFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+ "\\x7F\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8A"
+ "\\x8B\\x8C\\x8D\\x8E\\x8F\\x90\\x91\\x92\\x93\\x94\\x95\\x96"
+ "\\x97\\x98\\x99\\x9A\\x9B\\x9C\\x9D\\x9E\\x9F\\xA0\\xA1\\xA2"
+ "\\xA3\\xA4\\xA5\\xA6\\xA7\\xA8\\xA9\\xAA\\xAB\\xAC\\xAD\\xAE"
+ "\\xAF\\xB0\\xB1\\xB2\\xB3\\xB4\\xB5\\xB6\\xB7\\xB8\\xB9\\xBA"
+ "\\xBB\\xBC\\xBD\\xBE\\xBF\\xC0\\xC1\\xC2\\xC3\\xC4\\xC5\\xC6"
+ "\\xC7\\xC8\\xC9\\xCA\\xCB\\xCC\\xCD\\xCE\\xCF\\xD0\\xD1\\xD2"
+ "\\xD3\\xD4\\xD5\\xD6\\xD7\\xD8\\xD9\\xDA\\xDB\\xDC\\xDD\\xDE"
+ "\\xDF\\xE0\\xE1\\xE2\\xE3\\xE4\\xE5\\xE6\\xE7\\xE8\\xE9\\xEA"
+ "\\xEB\\xEC\\xED\\xEE\\xEF\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6"
+ "\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\"";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST_F(TYsonWriterTest, ConvertToYson)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream, EYsonFormat::Text);
+
+ TString input;
+ for (int i = 0; i < 256; ++i) {
+ input.push_back(char(i));
+ }
+
+ writer.OnStringScalar(input);
+
+ TString output =
+ "\"\\0\\1\\2\\3\\4\\5\\6\\7\\x08\\t\\n\\x0B\\x0C\\r\\x0E\\x0F"
+ "\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B"
+ "\\x1C\\x1D\\x1E\\x1F !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCD"
+ "EFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+ "\\x7F\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8A"
+ "\\x8B\\x8C\\x8D\\x8E\\x8F\\x90\\x91\\x92\\x93\\x94\\x95\\x96"
+ "\\x97\\x98\\x99\\x9A\\x9B\\x9C\\x9D\\x9E\\x9F\\xA0\\xA1\\xA2"
+ "\\xA3\\xA4\\xA5\\xA6\\xA7\\xA8\\xA9\\xAA\\xAB\\xAC\\xAD\\xAE"
+ "\\xAF\\xB0\\xB1\\xB2\\xB3\\xB4\\xB5\\xB6\\xB7\\xB8\\xB9\\xBA"
+ "\\xBB\\xBC\\xBD\\xBE\\xBF\\xC0\\xC1\\xC2\\xC3\\xC4\\xC5\\xC6"
+ "\\xC7\\xC8\\xC9\\xCA\\xCB\\xCC\\xCD\\xCE\\xCF\\xD0\\xD1\\xD2"
+ "\\xD3\\xD4\\xD5\\xD6\\xD7\\xD8\\xD9\\xDA\\xDB\\xDC\\xDD\\xDE"
+ "\\xDF\\xE0\\xE1\\xE2\\xE3\\xE4\\xE5\\xE6\\xE7\\xE8\\xE9\\xEA"
+ "\\xEB\\xEC\\xED\\xEE\\xEF\\xF0\\xF1\\xF2\\xF3\\xF4\\xF5\\xF6"
+ "\\xF7\\xF8\\xF9\\xFA\\xFB\\xFC\\xFD\\xFE\\xFF\"";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST_F(TYsonWriterTest, NoNewLinesInEmptyMap)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream, EYsonFormat::Pretty);
+ writer.OnBeginMap();
+ writer.OnEndMap();
+
+ EXPECT_EQ("{}", outputStream.Str());
+}
+
+TEST_F(TYsonWriterTest, NoNewLinesInEmptyList)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream, EYsonFormat::Pretty);
+ writer.OnBeginList();
+ writer.OnEndList();
+
+ EXPECT_EQ("[]", outputStream.Str());
+}
+
+TEST_F(TYsonWriterTest, NestedAttributes)
+{
+
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnStringScalar("c"));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnInt64Scalar(2));
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnInt64Scalar(1));
+
+ TYsonWriter writer(&Stream, EYsonFormat::Binary);
+
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("a");
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("b");
+ writer.OnStringScalar("c");
+ writer.OnEndAttributes();
+ writer.OnInt64Scalar(2);
+ writer.OnEndAttributes();
+ writer.OnInt64Scalar(1);
+
+ Run();
+}
+
+TEST_F(TYsonWriterTest, PrettyFormat)
+{
+ TStringStream outputStream;
+ TYsonWriter writer(&outputStream, EYsonFormat::Pretty);
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("attr");
+ writer.OnStringScalar("value");
+ writer.OnEndAttributes();
+ writer.OnBeginMap();
+ writer.OnKeyedItem("key1");
+ writer.OnStringScalar("value1");
+ writer.OnKeyedItem("key2");
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("other_attr");
+ writer.OnStringScalar("other_value");
+ writer.OnEndAttributes();
+ writer.OnStringScalar("value2");
+ writer.OnEndMap();
+
+ EXPECT_EQ(
+ "<\n"
+ " \"attr\" = \"value\";\n"
+ "> {\n"
+ " \"key1\" = \"value1\";\n"
+ " \"key2\" = <\n"
+ " \"other_attr\" = \"other_value\";\n"
+ " > \"value2\";\n"
+ "}",
+ outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonFragmentWriterTest, NewLinesInList)
+{
+ TStringStream outputStream;
+
+ TYsonWriter writer(&outputStream, EYsonFormat::Text, EYsonType::ListFragment);
+ writer.OnListItem();
+ writer.OnInt64Scalar(200);
+ writer.OnListItem();
+ writer.OnBeginMap();
+ writer.OnKeyedItem("key");
+ writer.OnInt64Scalar(42);
+ writer.OnKeyedItem("yek");
+ writer.OnInt64Scalar(24);
+ writer.OnKeyedItem("list");
+ writer.OnBeginList();
+ writer.OnEndList();
+ writer.OnEndMap();
+ writer.OnListItem();
+ writer.OnStringScalar("aaa");
+ writer.OnListItem();
+ writer.OnStringScalar("bbb");
+
+ TString output =
+ "200;\n"
+ "{\"key\"=42;\"yek\"=24;\"list\"=[];};\n"
+ "\"aaa\";\n"
+ "\"bbb\";\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TYsonFragmentWriterTest, BinaryList)
+{
+ TStringStream outputStream;
+
+ TYsonWriter writer(&outputStream, EYsonFormat::Binary, EYsonType::ListFragment);
+ writer.OnListItem();
+ writer.OnInt64Scalar(200);
+ writer.OnListItem();
+ writer.OnStringScalar("aaa");
+
+ TString output =
+ "\x2\x90\x3;"
+ "\x1\x6""aaa;";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+
+TEST(TYsonFragmentWriterTest, NewLinesInMap)
+{
+ TStringStream outputStream;
+
+ TYsonWriter writer(&outputStream, EYsonFormat::Text, EYsonType::MapFragment);
+ writer.OnKeyedItem("a");
+ writer.OnInt64Scalar(100);
+ writer.OnKeyedItem("b");
+ writer.OnBeginList();
+ writer.OnListItem();
+ writer.OnBeginMap();
+ writer.OnKeyedItem("key");
+ writer.OnInt64Scalar(42);
+ writer.OnKeyedItem("yek");
+ writer.OnInt64Scalar(24);
+ writer.OnEndMap();
+ writer.OnListItem();
+ writer.OnInt64Scalar(-1);
+ writer.OnEndList();
+ writer.OnKeyedItem("c");
+ writer.OnStringScalar("word");
+
+ TString output =
+ "\"a\"=100;\n"
+ "\"b\"=[{\"key\"=42;\"yek\"=24;};-1;];\n"
+ "\"c\"=\"word\";\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+TEST(TYsonFragmentWriterTest, NoFirstIndent)
+{
+ TStringStream outputStream;
+
+ TYsonWriter writer(&outputStream, EYsonFormat::Pretty, EYsonType::MapFragment);
+ writer.OnKeyedItem("a1");
+ writer.OnBeginMap();
+ writer.OnKeyedItem("key");
+ writer.OnInt64Scalar(42);
+ writer.OnEndMap();
+ writer.OnKeyedItem("a2");
+ writer.OnInt64Scalar(0);
+
+ TString output =
+ "\"a1\" = {\n"
+ " \"key\" = 42;\n"
+ "};\n"
+ "\"a2\" = 0;\n";
+
+ EXPECT_EQ(output, outputStream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/writer.cpp b/yt/yt/core/yson/writer.cpp
new file mode 100644
index 0000000000..903d138561
--- /dev/null
+++ b/yt/yt/core/yson/writer.cpp
@@ -0,0 +1,524 @@
+#include "writer.h"
+#include "detail.h"
+#include "format.h"
+
+#include <library/cpp/yt/coding/varint.h>
+
+#include <util/stream/buffer.h>
+#include <util/system/unaligned_mem.h>
+
+#include <cmath>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Copied from <util/string/escape.cpp>
+namespace {
+
+static inline char HexDigit(char value) {
+ YT_ASSERT(value < 16);
+ if (value < 10)
+ return '0' + value;
+ else
+ return 'A' + value - 10;
+}
+
+static inline char OctDigit(char value) {
+ YT_ASSERT(value < 8);
+ return '0' + value;
+}
+
+static inline bool IsPrintable(char c) {
+ return c >= 32 && c <= 126;
+}
+
+static inline bool IsHexDigit(char c) {
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+}
+
+static inline bool IsOctDigit(char c) {
+ return c >= '0' && c <= '7';
+}
+
+static const size_t ESCAPE_C_BUFFER_SIZE = 4;
+
+static inline size_t EscapeC(unsigned char c, char next, char r[ESCAPE_C_BUFFER_SIZE]) {
+ // (1) Printable characters go as-is, except backslash and double quote.
+ // (2) Characters \r, \n, \t and \0 ... \7 replaced by their simple escape characters (if possible).
+ // (3) Otherwise, character is encoded using hexadecimal escape sequence (if possible), or octal.
+ if (c == '\"') {
+ r[0] = '\\';
+ r[1] = '\"';
+ return 2;
+ } else if (c == '\\') {
+ r[0] = '\\';
+ r[1] = '\\';
+ return 2;
+ } else if (IsPrintable(c)) {
+ r[0] = c;
+ return 1;
+ } else if (c == '\r') {
+ r[0] = '\\';
+ r[1] = 'r';
+ return 2;
+ } else if (c == '\n') {
+ r[0] = '\\';
+ r[1] = 'n';
+ return 2;
+ } else if (c == '\t') {
+ r[0] = '\\';
+ r[1] = 't';
+ return 2;
+ } else if (c < 8 && !IsOctDigit(next)) {
+ r[0] = '\\';
+ r[1] = OctDigit(c);
+ return 2;
+ } else if (!IsHexDigit(next)) {
+ r[0] = '\\';
+ r[1] = 'x';
+ r[2] = HexDigit((c & 0xF0) >> 4);
+ r[3] = HexDigit((c & 0x0F) >> 0);
+ return 4;
+ } else {
+ r[0] = '\\';
+ r[1] = OctDigit((c & 0700) >> 6);
+ r[2] = OctDigit((c & 0070) >> 3);
+ r[3] = OctDigit((c & 0007) >> 0);
+ return 4;
+ }
+}
+
+void EscapeC(const char* str, size_t len, IOutputStream& output) {
+ char buffer[ESCAPE_C_BUFFER_SIZE];
+
+ size_t i, j;
+ for (i = 0, j = 0; i < len; ++i) {
+ size_t rlen = EscapeC(str[i], (i + 1 < len ? str[i + 1] : 0), buffer);
+
+ if (rlen > 1) {
+ output.Write(str + j, i - j);
+ j = i + 1;
+ output.Write(buffer, rlen);
+ }
+ }
+
+ if (j > 0) {
+ output.Write(str + j, len - j);
+ } else {
+ output.Write(str, len);
+ }
+}
+
+size_t FloatToStringWithNanInf(double value, char* buf, size_t size)
+{
+ if (std::isfinite(value)) {
+ return FloatToString(value, buf, size);
+ }
+
+ static const TStringBuf nanLiteral = "%nan";
+ static const TStringBuf infLiteral = "%inf";
+ static const TStringBuf negativeInfLiteral = "%-inf";
+
+ TStringBuf str;
+ if (std::isnan(value)) {
+ str = nanLiteral;
+ } else if (std::isinf(value) && value > 0) {
+ str = infLiteral;
+ } else {
+ str = negativeInfLiteral;
+ }
+ YT_VERIFY(str.size() + 1 <= size);
+ ::memcpy(buf, str.data(), str.size() + 1);
+ return str.size();
+}
+
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonWriter::TYsonWriter(
+ IOutputStream* stream,
+ EYsonFormat format,
+ EYsonType type,
+ bool enableRaw,
+ int indent)
+ : Stream_(stream)
+ , Format_(format)
+ , Type_(type)
+ , EnableRaw_(enableRaw)
+ , IndentSize_(indent)
+{
+ YT_ASSERT(Stream_);
+}
+
+void TYsonWriter::WriteIndent()
+{
+ for (int i = 0; i < IndentSize_ * Depth_; ++i) {
+ Stream_->Write(' ');
+ }
+}
+
+void TYsonWriter::EndNode()
+{
+ if (Depth_ > 0 || Type_ != EYsonType::Node) {
+ Stream_->Write(NDetail::ItemSeparatorSymbol);
+ if ((Depth_ > 0 && Format_ == EYsonFormat::Pretty) ||
+ (Depth_ == 0 && Format_ != EYsonFormat::Binary))
+ {
+ Stream_->Write('\n');
+ }
+ }
+}
+
+void TYsonWriter::BeginCollection(char ch)
+{
+ ++Depth_;
+ EmptyCollection_ = true;
+ Stream_->Write(ch);
+}
+
+void TYsonWriter::CollectionItem()
+{
+ if (Format_ == EYsonFormat::Pretty) {
+ if (EmptyCollection_ && Depth_ > 0) {
+ Stream_->Write('\n');
+ }
+ WriteIndent();
+ }
+ EmptyCollection_ = false;
+}
+
+void TYsonWriter::EndCollection(char ch)
+{
+ --Depth_;
+ if (Format_ == EYsonFormat::Pretty && !EmptyCollection_) {
+ WriteIndent();
+ }
+ EmptyCollection_ = false;
+ Stream_->Write(ch);
+}
+
+void TYsonWriter::WriteStringScalar(TStringBuf value)
+{
+ if (Format_ == EYsonFormat::Binary) {
+ Stream_->Write(NDetail::StringMarker);
+ WriteVarInt32(Stream_, static_cast<i32>(value.length()));
+ Stream_->Write(value.begin(), value.length());
+ } else {
+ Stream_->Write('"');
+ EscapeC(value.data(), value.length(), *Stream_);
+ Stream_->Write('"');
+ }
+}
+
+void TYsonWriter::OnStringScalar(TStringBuf value)
+{
+ WriteStringScalar(value);
+ EndNode();
+}
+
+void TYsonWriter::OnInt64Scalar(i64 value)
+{
+ if (Format_ == EYsonFormat::Binary) {
+ Stream_->Write(NDetail::Int64Marker);
+ WriteVarInt64(Stream_, value);
+ } else {
+ Stream_->Write(::ToString(value));
+ }
+ EndNode();
+}
+
+void TYsonWriter::OnUint64Scalar(ui64 value)
+{
+ if (Format_ == EYsonFormat::Binary) {
+ Stream_->Write(NDetail::Uint64Marker);
+ WriteVarUint64(Stream_, value);
+ } else {
+ Stream_->Write(::ToString(value));
+ Stream_->Write("u");
+ }
+ EndNode();
+}
+
+void TYsonWriter::OnDoubleScalar(double value)
+{
+ if (Format_ == EYsonFormat::Binary) {
+ Stream_->Write(NDetail::DoubleMarker);
+ Stream_->Write(&value, sizeof(double));
+ } else {
+ char buf[256];
+ auto str = TStringBuf(buf, FloatToStringWithNanInf(value, buf, sizeof(buf)));
+ Stream_->Write(str);
+ if (str.find('.') == TString::npos && str.find('e') == TString::npos && std::isfinite(value)) {
+ Stream_->Write(".");
+ }
+ }
+ EndNode();
+}
+
+void TYsonWriter::OnBooleanScalar(bool value)
+{
+ if (Format_ == EYsonFormat::Binary) {
+ Stream_->Write(value ? NDetail::TrueMarker : NDetail::FalseMarker);
+ } else {
+ Stream_->Write(value ? TStringBuf("%true") : TStringBuf("%false"));
+ }
+ EndNode();
+}
+
+void TYsonWriter::OnEntity()
+{
+ Stream_->Write(NDetail::EntitySymbol);
+ EndNode();
+}
+
+void TYsonWriter::OnBeginList()
+{
+ BeginCollection(NDetail::BeginListSymbol);
+}
+
+void TYsonWriter::OnListItem()
+{
+ CollectionItem();
+}
+
+void TYsonWriter::OnEndList()
+{
+ EndCollection(NDetail::EndListSymbol);
+ EndNode();
+}
+
+void TYsonWriter::OnBeginMap()
+{
+ BeginCollection(NDetail::BeginMapSymbol);
+}
+
+void TYsonWriter::OnKeyedItem(TStringBuf key)
+{
+ CollectionItem();
+
+ WriteStringScalar(key);
+
+ if (Format_ == EYsonFormat::Pretty) {
+ Stream_->Write(' ');
+ }
+ Stream_->Write(NDetail::KeyValueSeparatorSymbol);
+ if (Format_ == EYsonFormat::Pretty) {
+ Stream_->Write(' ');
+ }
+}
+
+void TYsonWriter::OnEndMap()
+{
+ EndCollection(NDetail::EndMapSymbol);
+ EndNode();
+}
+
+void TYsonWriter::OnBeginAttributes()
+{
+ BeginCollection(NDetail::BeginAttributesSymbol);
+}
+
+void TYsonWriter::OnEndAttributes()
+{
+ EndCollection(NDetail::EndAttributesSymbol);
+ if (Format_ == EYsonFormat::Pretty) {
+ Stream_->Write(' ');
+ }
+}
+
+void TYsonWriter::OnRaw(TStringBuf yson, EYsonType type)
+{
+ if (EnableRaw_) {
+ Stream_->Write(yson);
+ if (type == EYsonType::Node) {
+ EndNode();
+ }
+ } else {
+ TYsonConsumerBase::OnRaw(yson, type);
+ }
+}
+
+void TYsonWriter::Flush()
+{ }
+
+int TYsonWriter::GetDepth() const
+{
+ return Depth_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TBufferedBinaryYsonWriter::TBufferedBinaryYsonWriter(
+ IZeroCopyOutput* stream,
+ EYsonType type,
+ bool enableRaw,
+ std::optional<int> nestingLevelLimit)
+ : Type_(type)
+ , EnableRaw_(enableRaw)
+ , TokenWriter_(std::make_optional<TUncheckedYsonTokenWriter>(stream, type))
+ , NestingLevelLimit_(nestingLevelLimit.value_or(std::numeric_limits<int>::max()))
+{
+ YT_ASSERT(stream);
+}
+
+Y_FORCE_INLINE void TBufferedBinaryYsonWriter::WriteStringScalar(TStringBuf value)
+{
+ TokenWriter_->WriteBinaryString(value);
+}
+
+Y_FORCE_INLINE void TBufferedBinaryYsonWriter::BeginCollection()
+{
+ ++Depth_;
+ if (Depth_ > NestingLevelLimit_) {
+ THROW_ERROR_EXCEPTION("Depth limit exceeded while writing YSON")
+ << TErrorAttribute("limit", NestingLevelLimit_);
+ }
+}
+
+Y_FORCE_INLINE void TBufferedBinaryYsonWriter::EndNode()
+{
+ if (Y_LIKELY(Type_ != EYsonType::Node || Depth_ > 0)) {
+ TokenWriter_->WriteItemSeparator();
+ }
+}
+
+void TBufferedBinaryYsonWriter::Flush()
+{
+ TokenWriter_->Flush();
+}
+
+int TBufferedBinaryYsonWriter::GetDepth() const
+{
+ return Depth_;
+}
+
+ui64 TBufferedBinaryYsonWriter::GetTotalWrittenSize() const
+{
+ return TokenWriter_ ? TokenWriter_->GetTotalWrittenSize() : 0;
+}
+
+void TBufferedBinaryYsonWriter::OnStringScalar(TStringBuf value)
+{
+ WriteStringScalar(value);
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnInt64Scalar(i64 value)
+{
+ TokenWriter_->WriteBinaryInt64(value);
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnUint64Scalar(ui64 value)
+{
+ TokenWriter_->WriteBinaryUint64(value);
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnDoubleScalar(double value)
+{
+ TokenWriter_->WriteBinaryDouble(value);
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnBooleanScalar(bool value)
+{
+ TokenWriter_->WriteBinaryBoolean(value);
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnEntity()
+{
+ TokenWriter_->WriteEntity();
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnBeginList()
+{
+ BeginCollection();
+ TokenWriter_->WriteBeginList();
+}
+
+void TBufferedBinaryYsonWriter::OnListItem()
+{ }
+
+void TBufferedBinaryYsonWriter::OnEndList()
+{
+ --Depth_;
+ TokenWriter_->WriteEndList();
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnBeginMap()
+{
+ BeginCollection();
+ TokenWriter_->WriteBeginMap();
+}
+
+void TBufferedBinaryYsonWriter::OnKeyedItem(TStringBuf key)
+{
+ WriteStringScalar(key);
+ TokenWriter_->WriteKeyValueSeparator();
+}
+
+void TBufferedBinaryYsonWriter::OnEndMap()
+{
+ --Depth_;
+ TokenWriter_->WriteEndMap();
+ EndNode();
+}
+
+void TBufferedBinaryYsonWriter::OnBeginAttributes()
+{
+ BeginCollection();
+ TokenWriter_->WriteBeginAttributes();
+}
+
+void TBufferedBinaryYsonWriter::OnEndAttributes()
+{
+ --Depth_;
+ TokenWriter_->WriteEndAttributes();
+}
+
+void TBufferedBinaryYsonWriter::OnRaw(TStringBuf yson, EYsonType type)
+{
+ if (EnableRaw_) {
+ TokenWriter_->WriteRawNodeUnchecked(yson);
+ if (type == EYsonType::Node) {
+ EndNode();
+ }
+ } else {
+ TYsonConsumerBase::OnRaw(yson, type);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IFlushableYsonConsumer> CreateYsonWriter(
+ IZeroCopyOutput* output,
+ EYsonFormat format,
+ EYsonType type,
+ bool enableRaw,
+ int indent)
+{
+ if (format == EYsonFormat::Binary) {
+ return std::make_unique<TBufferedBinaryYsonWriter>(
+ output,
+ type,
+ enableRaw);
+ } else {
+ return std::make_unique<TYsonWriter>(
+ output,
+ format,
+ type,
+ enableRaw,
+ indent);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/writer.h b/yt/yt/core/yson/writer.h
new file mode 100644
index 0000000000..6c309c80f4
--- /dev/null
+++ b/yt/yt/core/yson/writer.h
@@ -0,0 +1,161 @@
+#pragma once
+
+#include "public.h"
+#include "consumer.h"
+#include "token_writer.h"
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A canonical implementation of YSON data stream writer.
+class TYsonWriter
+ : public TYsonConsumerBase
+ , public virtual IFlushableYsonConsumer
+ , private TNonCopyable
+{
+public:
+ static constexpr int DefaultIndent = 4;
+
+ //! Initializes an instance.
+ /*!
+ * \param stream A stream for writing the YSON data to.
+ * \param format A format used for encoding the data.
+ * \param enableRaw Enables inserting raw portions of YSON as-is, without reparse.
+ */
+ TYsonWriter(
+ IOutputStream* stream,
+ EYsonFormat format = EYsonFormat::Binary,
+ EYsonType type = EYsonType::Node,
+ bool enableRaw = false,
+ int indent = DefaultIndent);
+
+ // IYsonConsumer overrides.
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+ using IYsonConsumer::OnRaw;
+ void OnRaw(TStringBuf yson, EYsonType type = EYsonType::Node) override;
+
+ void Flush() override;
+
+ int GetDepth() const;
+
+protected:
+ IOutputStream* const Stream_;
+ const EYsonFormat Format_;
+ const EYsonType Type_;
+ const bool EnableRaw_;
+ const int IndentSize_;
+
+ int Depth_ = 0;
+ bool EmptyCollection_ = true;
+
+
+ void WriteIndent();
+ void WriteStringScalar(TStringBuf value);
+
+ void BeginCollection(char ch);
+ void CollectionItem();
+ void EndCollection(char ch);
+
+ void EndNode();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An optimized version of YSON stream writer.
+/*!
+ * This writer buffers its output so don't forget to call #Flush.
+ * Only binary YSON format is supported.
+ */
+class TBufferedBinaryYsonWriter
+ : public TYsonConsumerBase
+ , public virtual IFlushableYsonConsumer
+ , private TNonCopyable
+{
+public:
+ //! Initializes an instance.
+ /*!
+ * \param stream A stream for writing the YSON data to.
+ * \param format A format used for encoding the data.
+ * \param enableRaw Enables inserting raw portions of YSON as-is, without reparse.
+ */
+ TBufferedBinaryYsonWriter(
+ IZeroCopyOutput* stream,
+ EYsonType type = EYsonType::Node,
+ bool enableRaw = true,
+ std::optional<int> nestingLevelLimit = std::nullopt);
+
+ // IYsonConsumer overrides.
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+
+ using IYsonConsumer::OnRaw;
+ void OnRaw(TStringBuf yson, EYsonType type = EYsonType::Node) override;
+
+ void Flush() override;
+
+ int GetDepth() const;
+
+ ui64 GetTotalWrittenSize() const;
+
+protected:
+ const EYsonType Type_;
+ const bool EnableRaw_;
+
+ std::optional<TUncheckedYsonTokenWriter> TokenWriter_;
+
+ int NestingLevelLimit_;
+ int Depth_ = 0;
+
+ void WriteStringScalar(TStringBuf value);
+ void BeginCollection();
+ void EndNode();
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Creates either TYsonWriter or TBufferedBinaryYsonWriter, depending on #format.
+std::unique_ptr<IFlushableYsonConsumer> CreateYsonWriter(
+ IZeroCopyOutput* output,
+ EYsonFormat format,
+ EYsonType type,
+ bool enableRaw,
+ int indent = TYsonWriter::DefaultIndent);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
+
diff --git a/yt/yt/core/yson/ypath_designated_consumer.cpp b/yt/yt/core/yson/ypath_designated_consumer.cpp
new file mode 100644
index 0000000000..315be2a9b8
--- /dev/null
+++ b/yt/yt/core/yson/ypath_designated_consumer.cpp
@@ -0,0 +1,206 @@
+#include "ypath_designated_consumer.h"
+#include "forwarding_consumer.h"
+#include "null_consumer.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/ytree/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathDesignatedYsonConsumer
+ : public TForwardingYsonConsumer
+{
+public:
+ TYPathDesignatedYsonConsumer(
+ NYPath::TYPath path,
+ EMissingPathMode missingPathMode,
+ IYsonConsumer* underlyingConsumer)
+ : Path_(std::move(path))
+ , MissingPathMode_(missingPathMode)
+ , Tokenizer_(Path_)
+ , UnderlyingConsumer_(underlyingConsumer)
+ {
+ Tokenizer_.Skip(NYPath::ETokenType::StartOfStream);
+ ForwardIfPathIsExhausted();
+ }
+
+ void OnMyStringScalar(TStringBuf /*value*/) override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyInt64Scalar(i64 /*value*/) override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyUint64Scalar(ui64 /*value*/) override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyDoubleScalar(double /*value*/) override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyBooleanScalar(bool /*value*/) override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyEntity() override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyBeginList() override
+ {
+ if (State_ == EConsumerState::FollowingPath) {
+ ThrowListUnsupportedError();
+ }
+ }
+
+ void OnMyListItem() override
+ { }
+
+ void OnMyEndList() override
+ { }
+
+ void OnMyBeginMap() override
+ {
+ if (State_ == EConsumerState::FollowingPath) {
+ Tokenizer_.Skip(NYPath::ETokenType::Slash);
+ }
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ if (State_ == EConsumerState::ConsumptionCompleted) {
+ return;
+ }
+
+ switch (Tokenizer_.GetType()) {
+ case NYPath::ETokenType::Literal:
+ if (key == Tokenizer_.GetToken()) {
+ Tokenizer_.Advance();
+ ForwardIfPathIsExhausted();
+ } else {
+ Forward(&NullConsumer_);
+ }
+ break;
+ case NYPath::ETokenType::At:
+ if (MissingPathMode_ == EMissingPathMode::ThrowError) {
+ ThrowNoAttributes();
+ }
+ State_ = EConsumerState::ConsumptionCompleted;
+ break;
+ default:
+ Tokenizer_.ThrowUnexpected();
+ }
+ }
+
+ void OnMyEndMap() override
+ {
+ CheckAndThrowResolveError();
+ }
+
+ void OnMyBeginAttributes() override
+ {
+ if (State_ == EConsumerState::ConsumptionCompleted) {
+ return;
+ }
+
+ Tokenizer_.Skip(NYPath::ETokenType::Slash);
+ if (Tokenizer_.GetType() == NYPath::ETokenType::At) {
+ Tokenizer_.Advance();
+ if (Tokenizer_.GetType() == NYPath::ETokenType::EndOfStream) {
+ UnderlyingConsumer_->OnBeginMap();
+ Forward(
+ UnderlyingConsumer_,
+ [&] {
+ UnderlyingConsumer_->OnEndMap();
+ State_= EConsumerState::ConsumptionCompleted;
+ },
+ EYsonType::MapFragment);
+ }
+ } else {
+ Forward(&NullConsumer_, /*onFinished*/ {}, EYsonType::MapFragment);
+ }
+ }
+
+ void OnMyEndAttributes() override
+ { }
+
+private:
+ enum class EConsumerState
+ {
+ FollowingPath,
+ ConsumptionCompleted,
+ };
+
+ const NYPath::TYPath Path_;
+ const EMissingPathMode MissingPathMode_;
+
+ NYPath::TTokenizer Tokenizer_;
+ IYsonConsumer* UnderlyingConsumer_;
+ TNullYsonConsumer NullConsumer_;
+ EConsumerState State_ = EConsumerState::FollowingPath;
+
+ void ThrowListUnsupportedError()
+ {
+ THROW_ERROR_EXCEPTION("YPath designated consumer does not support list nodes");
+ }
+
+ void ThrowResolveError()
+ {
+ THROW_ERROR_EXCEPTION(NYTree::EErrorCode::ResolveError, "Failed to resolve YPath")
+ << TErrorAttribute("full_path", Tokenizer_.GetPath())
+ << TErrorAttribute("resolved_prefix", Tokenizer_.GetPrefix());
+ }
+
+ void ThrowNoAttributes()
+ {
+ THROW_ERROR_EXCEPTION("Path %Qv has no attributes", Tokenizer_.GetPrefix());
+ }
+
+ void CheckAndThrowResolveError()
+ {
+ if (State_ == EConsumerState::FollowingPath) {
+ if (MissingPathMode_ == EMissingPathMode::ThrowError) {
+ ThrowResolveError();
+ }
+ State_ = EConsumerState::ConsumptionCompleted;
+ }
+ }
+
+ void ForwardIfPathIsExhausted()
+ {
+ if (Tokenizer_.GetType() == NYPath::ETokenType::EndOfStream) {
+ Forward(UnderlyingConsumer_, [&] { State_ = EConsumerState::ConsumptionCompleted; });
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::unique_ptr<IYsonConsumer> CreateYPathDesignatedConsumer(
+ NYPath::TYPath path,
+ EMissingPathMode missingPathMode,
+ IYsonConsumer* underlyingConsumer)
+{
+ return std::make_unique<TYPathDesignatedYsonConsumer>(
+ std::move(path),
+ missingPathMode,
+ underlyingConsumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/yson/ypath_designated_consumer.h b/yt/yt/core/yson/ypath_designated_consumer.h
new file mode 100644
index 0000000000..546564237c
--- /dev/null
+++ b/yt/yt/core/yson/ypath_designated_consumer.h
@@ -0,0 +1,22 @@
+#include "public.h"
+#include "consumer.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMissingPathMode,
+ (ThrowError)
+ (Ignore)
+);
+
+std::unique_ptr<NYson::IYsonConsumer> CreateYPathDesignatedConsumer(
+ NYPath::TYPath path,
+ EMissingPathMode missingPathMode,
+ NYson::IYsonConsumer* underlyingConsumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYson
diff --git a/yt/yt/core/ytalloc/bindings.cpp b/yt/yt/core/ytalloc/bindings.cpp
new file mode 100644
index 0000000000..1f47fa7982
--- /dev/null
+++ b/yt/yt/core/ytalloc/bindings.cpp
@@ -0,0 +1,376 @@
+#include "bindings.h"
+#include "config.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+#include <yt/yt/library/profiling/producer.h>
+
+#include <yt/yt/core/misc/singleton.h>
+#include <yt/yt/core/misc/string_builder.h>
+
+#include <yt/yt/core/ytree/yson_serializable.h>
+
+#include <library/cpp/ytalloc/api/ytalloc.h>
+
+#include <util/system/env.h>
+
+#include <cstdio>
+
+namespace NYT::NYTAlloc {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+const NLogging::TLogger& GetLogger()
+{
+ struct TSingleton
+ {
+ NLogging::TLogger Logger{"YTAlloc"};
+ };
+
+ return LeakySingleton<TSingleton>()->Logger;
+}
+
+NLogging::ELogLevel SeverityToLevel(NYTAlloc::ELogEventSeverity severity)
+{
+ switch (severity) {
+
+ case ELogEventSeverity::Debug: return NLogging::ELogLevel::Debug;
+ case ELogEventSeverity::Info: return NLogging::ELogLevel::Info;
+ case ELogEventSeverity::Warning: return NLogging::ELogLevel::Warning;
+ case ELogEventSeverity::Error: return NLogging::ELogLevel::Error;
+ default: Y_UNREACHABLE();
+ }
+}
+
+void LogHandler(const NYTAlloc::TLogEvent& event)
+{
+ YT_LOG_EVENT(GetLogger(), SeverityToLevel(event.Severity), event.Message);
+}
+
+} // namespace
+
+void EnableYTLogging()
+{
+ EnableLogging(LogHandler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TProfilingStatisticsProducer
+ : public NProfiling::ISensorProducer
+{
+public:
+ TProfilingStatisticsProducer(const TYTProfilingConfigPtr& config)
+ : Config(config ? config : NYT::New<TYTProfilingConfig>())
+ {
+ NProfiling::TProfiler profiler{""};
+ profiler.AddProducer("/yt_alloc", MakeStrong(this));
+ }
+
+ void CollectSensors(NProfiling::ISensorWriter* writer) override
+ {
+ PushSystemAllocationStatistics(writer);
+ PushTotalAllocationStatistics(writer);
+
+ if (Config->EnableDetailedAllocationStatistics.value_or(true)) {
+ PushSmallAllocationStatistics(writer);
+ PushLargeAllocationStatistics(writer);
+ PushHugeAllocationStatistics(writer);
+ PushUndumpableAllocationStatistics(writer);
+ }
+
+ PushTimingStatistics(writer);
+ }
+
+private:
+ template <class TCounters>
+ static void PushAllocationCounterStatistics(
+ NProfiling::ISensorWriter* writer,
+ const TString& prefix,
+ const TCounters& counters)
+ {
+ for (auto counter : TEnumTraits<typename TCounters::TIndex>::GetDomainValues()) {
+ // TODO(prime@): Fix type.
+ writer->AddGauge(prefix + "/" + FormatEnum(counter), counters[counter]);
+ }
+ }
+
+ void PushSystemAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetSystemAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/system", counters);
+ }
+
+ void PushTotalAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetTotalAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/total", counters);
+ }
+
+ void PushHugeAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetHugeAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/huge", counters);
+ }
+
+ void PushUndumpableAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetUndumpableAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/undumpable", counters);
+ }
+
+ void PushSmallArenaStatistics(
+ NProfiling::ISensorWriter* writer,
+ size_t rank,
+ const TEnumIndexedVector<ESmallArenaCounter, ssize_t>& counters)
+ {
+ NProfiling::TWithTagGuard withTagGuard(writer, "rank", ToString(rank));
+ PushAllocationCounterStatistics(writer, "/small_arena", counters);
+ }
+
+ void PushSmallAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetSmallAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/small", counters);
+
+ auto arenaCounters = GetSmallArenaAllocationCounters();
+ for (size_t rank = 1; rank < SmallRankCount; ++rank) {
+ PushSmallArenaStatistics(writer, rank, arenaCounters[rank]);
+ }
+ }
+
+ void PushLargeArenaStatistics(
+ NProfiling::ISensorWriter* writer,
+ size_t rank,
+ const TEnumIndexedVector<ELargeArenaCounter, ssize_t>& counters)
+ {
+ NProfiling::TWithTagGuard withTagGuard(writer, "rank", ToString(rank));
+
+ PushAllocationCounterStatistics(writer, "/large_arena", counters);
+
+ ssize_t bytesFreed = counters[ELargeArenaCounter::BytesFreed];
+ ssize_t bytesReleased = counters[ELargeArenaCounter::PagesReleased] * PageSize;
+ int poolHitRatio;
+ if (bytesFreed == 0) {
+ poolHitRatio = 100;
+ } else if (bytesReleased > bytesFreed) {
+ poolHitRatio = 0;
+ } else {
+ poolHitRatio = 100 - bytesReleased * 100 / bytesFreed;
+ }
+
+ writer->AddGauge("/large_arena/pool_hit_ratio", poolHitRatio);
+ }
+
+ void PushLargeAllocationStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto counters = GetLargeAllocationCounters();
+ PushAllocationCounterStatistics(writer, "/large", counters);
+
+ auto arenaCounters = GetLargeArenaAllocationCounters();
+ for (size_t rank = MinLargeRank; rank < LargeRankCount; ++rank) {
+ PushLargeArenaStatistics(writer, rank, arenaCounters[rank]);
+ }
+ }
+
+ void PushTimingStatistics(NProfiling::ISensorWriter* writer)
+ {
+ auto timingEventCounters = GetTimingEventCounters();
+ for (auto type : TEnumTraits<ETimingEventType>::GetDomainValues()) {
+ const auto& counters = timingEventCounters[type];
+
+ NProfiling::TWithTagGuard withTagGuard(writer, "type", ToString(type));
+ writer->AddGauge("/timing_events/count", counters.Count);
+ writer->AddGauge("/timing_events/size", counters.Size);
+ }
+ }
+
+private:
+ const TYTProfilingConfigPtr Config;
+};
+
+void EnableYTProfiling(const TYTProfilingConfigPtr& config)
+{
+ LeakyRefCountedSingleton<TProfilingStatisticsProducer>(config);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+
+namespace {
+
+std::atomic<bool> ConfiguredFromEnv_;
+
+void DoConfigure(const TYTAllocConfigPtr& config, bool fromEnv)
+{
+ if (config->SmallArenasToProfile) {
+ for (size_t rank = 1; rank < SmallRankCount; ++rank) {
+ SetSmallArenaAllocationProfilingEnabled(rank, false);
+ }
+ for (auto rank : *config->SmallArenasToProfile) {
+ if (rank < 1 || rank >= SmallRankCount) {
+ THROW_ERROR_EXCEPTION("Unable to enable allocation profiling for small arena %v since its rank is out of range",
+ rank);
+ }
+ SetSmallArenaAllocationProfilingEnabled(rank, true);
+ }
+ }
+
+ if (config->LargeArenasToProfile) {
+ for (size_t rank = 1; rank < LargeRankCount; ++rank) {
+ SetLargeArenaAllocationProfilingEnabled(rank, false);
+ }
+ for (auto rank : *config->LargeArenasToProfile) {
+ if (rank < 1 || rank >= LargeRankCount) {
+ THROW_ERROR_EXCEPTION("Unable to enable allocation profiling for large arena %v since its rank is out of range",
+ rank);
+ }
+ SetLargeArenaAllocationProfilingEnabled(rank, true);
+ }
+ }
+
+ if (config->EnableAllocationProfiling) {
+ SetAllocationProfilingEnabled(*config->EnableAllocationProfiling);
+ }
+
+ if (config->AllocationProfilingSamplingRate) {
+ SetAllocationProfilingSamplingRate(*config->AllocationProfilingSamplingRate);
+ }
+
+ if (config->ProfilingBacktraceDepth) {
+ SetProfilingBacktraceDepth(*config->ProfilingBacktraceDepth);
+ }
+
+ if (config->MinProfilingBytesUsedToReport) {
+ SetMinProfilingBytesUsedToReport(*config->MinProfilingBytesUsedToReport);
+ }
+
+ if (config->StockpileInterval) {
+ SetStockpileInterval(*config->StockpileInterval);
+ }
+
+ if (config->StockpileThreadCount) {
+ SetStockpileThreadCount(*config->StockpileThreadCount);
+ }
+
+ if (config->StockpileSize) {
+ SetStockpileSize(*config->StockpileSize);
+ }
+
+ if (config->EnableEagerMemoryRelease) {
+ SetEnableEagerMemoryRelease(*config->EnableEagerMemoryRelease);
+ }
+
+ if (config->EnableMadvisePopulate) {
+ SetEnableMadvisePopulate(*config->EnableMadvisePopulate);
+ }
+
+ if (config->LargeUnreclaimableCoeff) {
+ SetLargeUnreclaimableCoeff(*config->LargeUnreclaimableCoeff);
+ }
+
+ if (config->MinLargeUnreclaimableBytes) {
+ SetMinLargeUnreclaimableBytes(*config->MinLargeUnreclaimableBytes);
+ }
+
+ if (config->MaxLargeUnreclaimableBytes) {
+ SetMaxLargeUnreclaimableBytes(*config->MaxLargeUnreclaimableBytes);
+ }
+
+ ConfiguredFromEnv_.store(fromEnv);
+}
+
+} // namespace
+
+void Configure(const TYTAllocConfigPtr& config)
+{
+ DoConfigure(config, false);
+}
+
+bool ConfigureFromEnv()
+{
+ const auto& Logger = GetLogger();
+
+ static const TString ConfigEnvVarName = "YT_ALLOC_CONFIG";
+ auto configVarValue = GetEnv(ConfigEnvVarName);
+ if (!configVarValue) {
+ YT_LOG_DEBUG("No %v environment variable is found",
+ ConfigEnvVarName);
+ return false;
+ }
+
+ TYTAllocConfigPtr config;
+ try {
+ config = ConvertTo<TYTAllocConfigPtr>(NYson::TYsonString(configVarValue));
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error parsing environment variable %v",
+ ConfigEnvVarName);
+ return false;
+ }
+
+ YT_LOG_DEBUG("%v environment variable parsed successfully",
+ ConfigEnvVarName);
+
+ try {
+ DoConfigure(config, true);
+ } catch (const std::exception& ex) {
+ YT_LOG_ERROR(ex, "Error applying configuration parsed from environment variable");
+ return false;
+ }
+
+ return true;
+}
+
+bool IsConfiguredFromEnv()
+{
+ return ConfiguredFromEnv_.load();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void InitializeLibunwindInterop()
+{
+ // COMPAT(babenko): intentionally empty, this code will die anyway.
+}
+
+TString FormatAllocationCounters()
+{
+ TStringBuilder builder;
+
+ auto formatCounters = [&] (const auto& counters) {
+ using T = typename std::decay_t<decltype(counters)>::TIndex;
+ builder.AppendString("{");
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+ for (auto counter : TEnumTraits<T>::GetDomainValues()) {
+ delimitedBuilder->AppendFormat("%v: %v", counter, counters[counter]);
+ }
+ builder.AppendString("}");
+ };
+
+ builder.AppendString("Total = {");
+ formatCounters(GetTotalAllocationCounters());
+
+ builder.AppendString("}, System = {");
+ formatCounters(GetSystemAllocationCounters());
+
+ builder.AppendString("}, Small = {");
+ formatCounters(GetSmallAllocationCounters());
+
+ builder.AppendString("}, Large = {");
+ formatCounters(GetLargeAllocationCounters());
+
+ builder.AppendString("}, Huge = {");
+ formatCounters(GetHugeAllocationCounters());
+
+ builder.AppendString("}");
+ return builder.Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/bindings.h b/yt/yt/core/ytalloc/bindings.h
new file mode 100644
index 0000000000..09030171df
--- /dev/null
+++ b/yt/yt/core/ytalloc/bindings.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NYTAlloc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Installs YTAlloc log handler that pushes event to YT logging infrastructure.
+void EnableYTLogging();
+
+// Enables periodic push of YTAlloc statistics to YT profiling infrastructure.
+void EnableYTProfiling(const TYTProfilingConfigPtr& config = nullptr);
+
+// Installs backtrace provider that invokes libunwind.
+void InitializeLibunwindInterop();
+
+// Configures YTAlloc from a given #config instance.
+void Configure(const TYTAllocConfigPtr& config);
+
+// Configures YTAlloc from |YT_ALLOC_CONFIG| environment variable.
+// Never throws on error; just reports it via logging.
+// Returns |true| if YTAlloc was successfully configured from the variable;
+// |false| otherwise (the variable did not exist or could not be parsed).
+bool ConfigureFromEnv();
+
+//! Returns |true| if the current configuration is set by a successful
+//! call to #ConfigureFromEnv.
+bool IsConfiguredFromEnv();
+
+// Builds a string containing some brief allocation statistics.
+TString FormatAllocationCounters();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/config.cpp b/yt/yt/core/ytalloc/config.cpp
new file mode 100644
index 0000000000..3b7420c29c
--- /dev/null
+++ b/yt/yt/core/ytalloc/config.cpp
@@ -0,0 +1,54 @@
+#include "config.h"
+
+#include <library/cpp/ytalloc/api/ytalloc.h>
+
+namespace NYT::NYTAlloc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYTAllocConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_allocation_profiling", &TThis::EnableAllocationProfiling)
+ .Default();
+ registrar.Parameter("allocation_profiling_sampling_rate", &TThis::AllocationProfilingSamplingRate)
+ .InRange(0.0, 1.0)
+ .Default();
+ registrar.Parameter("small_arenas_to_profile", &TThis::SmallArenasToProfile)
+ .Default();
+ registrar.Parameter("large_arenas_to_profile", &TThis::LargeArenasToProfile)
+ .Default();
+ registrar.Parameter("profiling_backtrace_depth", &TThis::ProfilingBacktraceDepth)
+ .InRange(1, MaxAllocationProfilingBacktraceDepth)
+ .Default();
+ registrar.Parameter("min_profiling_bytes_used_to_report", &TThis::MinProfilingBytesUsedToReport)
+ .GreaterThan(0)
+ .Default();
+ registrar.Parameter("stockpile_interval", &TThis::StockpileInterval)
+ .Default();
+ registrar.Parameter("stockpile_thread_count", &TThis::StockpileThreadCount)
+ .Default();
+ registrar.Parameter("stockpile_size", &TThis::StockpileSize)
+ .GreaterThan(0)
+ .Default();
+ registrar.Parameter("enable_eager_memory_release", &TThis::EnableEagerMemoryRelease)
+ .Default();
+ registrar.Parameter("enable_madvise_populate", &TThis::EnableMadvisePopulate)
+ .Default();
+ registrar.Parameter("large_unreclaimable_coeff", &TThis::LargeUnreclaimableCoeff)
+ .Default();
+ registrar.Parameter("min_large_unreclaimable_bytes", &TThis::MinLargeUnreclaimableBytes)
+ .Default();
+ registrar.Parameter("max_large_unreclaimable_bytes", &TThis::MaxLargeUnreclaimableBytes)
+ .Default();
+}
+
+
+void TYTProfilingConfig::Register(TRegistrar registrar)
+{
+ registrar.Parameter("enable_detailed_allocation_statistics", &TThis::EnableDetailedAllocationStatistics)
+ .Default(true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/config.h b/yt/yt/core/ytalloc/config.h
new file mode 100644
index 0000000000..fca956e0e6
--- /dev/null
+++ b/yt/yt/core/ytalloc/config.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ytree/yson_struct.h>
+
+namespace NYT::NYTAlloc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYTAllocConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<bool> EnableAllocationProfiling;
+ std::optional<double> AllocationProfilingSamplingRate;
+ std::optional<std::vector<int>> SmallArenasToProfile;
+ std::optional<std::vector<int>> LargeArenasToProfile;
+ std::optional<int> ProfilingBacktraceDepth;
+ std::optional<size_t> MinProfilingBytesUsedToReport;
+ std::optional<TDuration> StockpileInterval;
+ std::optional<int> StockpileThreadCount;
+ std::optional<size_t> StockpileSize;
+ std::optional<bool> EnableEagerMemoryRelease;
+ std::optional<bool> EnableMadvisePopulate;
+ std::optional<double> LargeUnreclaimableCoeff;
+ std::optional<size_t> MinLargeUnreclaimableBytes;
+ std::optional<size_t> MaxLargeUnreclaimableBytes;
+
+ REGISTER_YSON_STRUCT(TYTAllocConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TYTAllocConfig)
+
+class TYTProfilingConfig
+ : public NYTree::TYsonStruct
+{
+public:
+ std::optional<bool> EnableDetailedAllocationStatistics;
+
+ REGISTER_YSON_STRUCT(TYTProfilingConfig);
+
+ static void Register(TRegistrar registrar);
+};
+
+DEFINE_REFCOUNTED_TYPE(TYTProfilingConfig)
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/public.h b/yt/yt/core/ytalloc/public.h
new file mode 100644
index 0000000000..61444eff7f
--- /dev/null
+++ b/yt/yt/core/ytalloc/public.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+namespace NYT::NYTAlloc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_CLASS(TYTAllocConfig)
+DECLARE_REFCOUNTED_CLASS(TYTProfilingConfig)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/statistics_producer.cpp b/yt/yt/core/ytalloc/statistics_producer.cpp
new file mode 100644
index 0000000000..9d30b71ba2
--- /dev/null
+++ b/yt/yt/core/ytalloc/statistics_producer.cpp
@@ -0,0 +1,24 @@
+#include "statistics_producer.h"
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYTAlloc {
+
+using namespace NYson;
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonProducer CreateStatisticsProducer()
+{
+ // COMPAT(babenko): this will die soon anyway.
+ return BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .EndMap();
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytalloc/statistics_producer.h b/yt/yt/core/ytalloc/statistics_producer.h
new file mode 100644
index 0000000000..b9d18468b5
--- /dev/null
+++ b/yt/yt/core/ytalloc/statistics_producer.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NYTAlloc {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NYson::TYsonProducer CreateStatisticsProducer();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTAlloc
diff --git a/yt/yt/core/ytree/attribute_consumer.cpp b/yt/yt/core/ytree/attribute_consumer.cpp
new file mode 100644
index 0000000000..7e28cb13f9
--- /dev/null
+++ b/yt/yt/core/ytree/attribute_consumer.cpp
@@ -0,0 +1,86 @@
+#include "attribute_consumer.h"
+#include "attributes.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttributeConsumer::TAttributeConsumer(IAttributeDictionary* attributes)
+ : Attributes(attributes)
+{ }
+
+IAttributeDictionary* TAttributeConsumer::GetAttributes() const
+{
+ return Attributes;
+}
+
+void TAttributeConsumer::OnMyKeyedItem(TStringBuf key)
+{
+ Writer.reset(new TBufferedBinaryYsonWriter(&Output));
+ Forward(Writer.get(), [this, key = TString(key)] {
+ Writer->Flush();
+ Writer.reset();
+ Attributes->SetYson(key, TYsonString(Output.Str()));
+ Output.clear();
+ });
+}
+
+void TAttributeConsumer::OnMyBeginMap()
+{ }
+
+void TAttributeConsumer::OnMyEndMap()
+{ }
+
+void TAttributeConsumer::OnMyBeginAttributes()
+{ }
+
+void TAttributeConsumer::OnMyEndAttributes()
+{ }
+
+void TAttributeConsumer::OnMyStringScalar(TStringBuf /*value*/)
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyInt64Scalar(i64 /*value*/)
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyUint64Scalar(ui64 /*value*/)
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyDoubleScalar(double /*value*/)
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyBooleanScalar(bool /*value*/)
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyEntity()
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::OnMyBeginList()
+{
+ ThrowMapExpected();
+}
+
+void TAttributeConsumer::ThrowMapExpected()
+{
+ THROW_ERROR_EXCEPTION("Attributes can only be set from a map");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attribute_consumer.h b/yt/yt/core/ytree/attribute_consumer.h
new file mode 100644
index 0000000000..cbcaf031d2
--- /dev/null
+++ b/yt/yt/core/ytree/attribute_consumer.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/forwarding_consumer.h>
+#include <yt/yt/core/yson/stream.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttributeConsumer
+ : public NYson::TForwardingYsonConsumer
+{
+public:
+ explicit TAttributeConsumer(IAttributeDictionary* attributes);
+ IAttributeDictionary* GetAttributes() const;
+
+protected:
+ void OnMyStringScalar(TStringBuf value) override;
+ void OnMyInt64Scalar(i64 value) override;
+ void OnMyUint64Scalar(ui64 value) override;
+ void OnMyDoubleScalar(double value) override;
+ void OnMyBooleanScalar(bool value) override;
+ void OnMyEntity() override;
+ void OnMyBeginList() override;
+
+ void OnMyKeyedItem(TStringBuf key) override;
+ void OnMyBeginMap() override;
+ void OnMyEndMap() override;
+ void OnMyBeginAttributes() override;
+ void OnMyEndAttributes()override;
+
+private:
+ IAttributeDictionary* const Attributes;
+
+ TStringStream Output;
+ std::unique_ptr<NYson::TBufferedBinaryYsonWriter> Writer;
+
+ void ThrowMapExpected();
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attribute_filter.cpp b/yt/yt/core/ytree/attribute_filter.cpp
new file mode 100644
index 0000000000..e11dc54444
--- /dev/null
+++ b/yt/yt/core/ytree/attribute_filter.cpp
@@ -0,0 +1,503 @@
+#include "attribute_filter.h"
+
+#include "node.h"
+#include "convert.h"
+#include "fluent.h"
+#include "ypath_client.h"
+
+#include <yt/yt/core/misc/protobuf_helpers.h>
+
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/yson/async_writer.h>
+#include <yt/yt/core/yson/async_consumer.h>
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/string_filter.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+namespace NYT::NYTree {
+
+using namespace NYPath;
+using namespace NYson;
+using namespace NLogging;
+
+using NYT::ToProto;
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Used only for YT_LOG_ALERT.
+static const TLogger Logger("AttributeFilter");
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void CanonizeAndValidatePath(TYPath& path)
+{
+ TYPath result;
+ result.reserve(path.size());
+
+ try {
+ if (path.empty()) {
+ THROW_ERROR_EXCEPTION("Empty paths are not allowed");
+ }
+
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Expect(NYPath::ETokenType::StartOfStream);
+ tokenizer.Advance();
+ while (tokenizer.GetType() != NYPath::ETokenType::EndOfStream) {
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ result += "/" + ToYPathLiteral(tokenizer.GetLiteralValue());
+ tokenizer.Advance();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(TError("Error validating attribute path %Qv", path) << ex);
+ }
+
+ path = std::move(result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Create a consumer that filters incoming YSON by given path filter and
+//! feeds the result into target consumer upon destruction.
+//!
+//! Parameter #sync controls whether the filtering consumer is to be used in
+//! solely synchronous mode (i.e. no calls of IAsyncYsonConsumer::OnRaw(TFuture<TYsonString>)
+//! are allowed). If set to true, the use of #targetConsumer is also restricted
+//! to synchronous methods.
+
+struct IHeterogenousFilterConsumer
+ : public TAttributeFilter::IFilteringConsumer
+ , public TAttributeFilter::IAsyncFilteringConsumer
+{ };
+
+std::unique_ptr<IHeterogenousFilterConsumer> CreateFilteringConsumerImpl(
+ IAsyncYsonConsumer* targetConsumer,
+ const TAttributeFilter::TPathFilter& pathFilter,
+ bool sync,
+ std::any targetConsumerHolder = {})
+{
+ class TFilterConsumer
+ : public IHeterogenousFilterConsumer
+ {
+ public:
+ TFilterConsumer(
+ IAsyncYsonConsumer* targetConsumer,
+ const std::vector<TYPath>& paths,
+ bool sync,
+ std::any targetConsumerHolder)
+ : TargetConsumer_(targetConsumer)
+ , Paths_(paths)
+ , Sync_(sync)
+ , TargetConsumerHolder_(std::move(targetConsumerHolder))
+ { }
+
+ IYsonConsumer* GetConsumer() override
+ {
+ YT_VERIFY(Sync_);
+ return &AsyncWriter_;
+ }
+
+ IAsyncYsonConsumer* GetAsyncConsumer() override
+ {
+ YT_VERIFY(!Sync_);
+ return &AsyncWriter_;
+ }
+
+ void Finish() override
+ {
+ // First, get a future for a single YSON string.
+ const auto& asyncSegments = AsyncWriter_.GetSegments();
+ TFuture<TYsonString> asyncYson;
+
+ if (asyncSegments.size() != 1) {
+ // I am not sure whether such scenario happens in real life.
+ // The only place I found that uses async OnRaw is object_detail.cpp,
+ // and there may be either only sync calls or only one async call of consumer.
+ // But just in case, let async writer do the job on concatenating these segments.
+ asyncYson = AsyncWriter_.Finish();
+ } else {
+ asyncYson = asyncSegments.front().ApplyUnique(BIND([] (std::pair<TYsonString, bool>&& pair) {
+ return std::move(pair.first);
+ }));
+ }
+
+ // Second, perform actual filtration.
+ auto asyncFilteredYson = asyncYson.ApplyUnique(BIND([paths = std::move(Paths_), sync = Sync_] (TYsonString&& yson) {
+ // Note the special case when there are no matches. Ideally we would like to not emit
+ // our attribute at all, but the possibility to do so depends on whether we are in sync or async case.
+ //
+ // If we are in sync case, we may ask FilterYsonString to run in a mode, in which it may return
+ // the null YSON string which has a special meaning of "no matches". If we see the null YSON string, we
+ // do not call TargetConsumer_ at all. In typical case TargetConsumer_ is an instance of
+ // TAttributeValueConsumer which does not emit an attribute key in such case, and our goal is fulfilled.
+ //
+ // In contrary to the synchronous case, asynchronous TAttributeValueConsumer::OnRaw always emits
+ // an attribute key. Therefore, we must ensure we get some meaningful value. We request a fallback
+ // implementation of filtration routine that always returns an empty list, map or a scalar value.
+ return FilterYsonString(paths, yson, /*allowNullResult*/ sync);
+ }));
+
+ // Finally, feed this asynchronous yson to the target consumer. And now goes the tricky part.
+ //
+ // If sync flag is set, we must use target consumer synchronously. This contract is important for
+ // synchronous overload of TAttributeFilter::CreateFilteringConsumer to work properly. Failure to do so
+ // would result in YT_ABORT() in case when TAsyncYsonConsumerAdapter is used as a target (refer to
+ // the implementation of synchronous CreateFilteringConsumer overload).
+ //
+ // Now note that in synchronous case asyncSegments contains a single set future, therefore
+ // asyncYson is also a set future, and asyncFilteredYson is also set.
+ if (Sync_) {
+ // This could be YT_VERIFY if this code was not so heavily used in master. Hope trace id
+ // of the log message below will help in investigation.
+ YT_LOG_ALERT_UNLESS(asyncFilteredYson.IsSet(), "Unexpected unset future in synchronous attribute filtering");
+ if (!asyncFilteredYson.IsSet()) {
+ THROW_ERROR_EXCEPTION("Unexpected unset future in synchronous attribute filtering");
+ }
+
+ auto&& filteredYsonOrError = asyncFilteredYson.Get();
+ filteredYsonOrError.ThrowOnError();
+
+ auto filteredYson = std::move(filteredYsonOrError.Value());
+
+ // Handle the null YSON string case (see comment around asyncFilteredYson definition).
+ if (filteredYson) {
+ TargetConsumer_->OnRaw(filteredYson);
+ }
+ } else {
+ TargetConsumer_->OnRaw(std::move(asyncFilteredYson));
+ }
+ }
+
+ private:
+ IAsyncYsonConsumer* const TargetConsumer_;
+ const std::vector<TYPath> Paths_;
+ const bool Sync_;
+ const std::any TargetConsumerHolder_;
+
+ TAsyncYsonWriter AsyncWriter_;
+ };
+
+ return std::make_unique<TFilterConsumer>(targetConsumer, *pathFilter, sync, std::move(targetConsumerHolder));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+TAttributeFilter::TAttributeFilter(std::vector<TString> keys, std::vector<TYPath> paths)
+ : Keys(std::move(keys))
+ , Paths(std::move(paths))
+ , Universal(false)
+{ }
+
+TAttributeFilter& TAttributeFilter::operator =(std::vector<TString> keys)
+{
+ Keys = std::move(keys);
+ Paths = {};
+ Universal = false;
+
+ return *this;
+}
+
+TAttributeFilter::operator bool() const
+{
+ return !Universal;
+}
+
+void TAttributeFilter::ValidateKeysOnly(TStringBuf context) const
+{
+ if (!Paths.empty()) {
+ THROW_ERROR_EXCEPTION("Filtering attributes by path is not implemented for %v", context);
+ }
+}
+
+bool TAttributeFilter::IsEmpty() const
+{
+ return !Universal && Keys.empty() && Paths.empty();
+}
+
+bool TAttributeFilter::AdmitsKeySlow(TStringBuf key) const
+{
+ if (!*this) {
+ return true;
+ }
+ return std::find(Keys.begin(), Keys.end(), key) != Keys.end() ||
+ std::find(Paths.begin(), Paths.end(), "/" + ToYPathLiteral(key)) != Paths.end();
+}
+
+TAttributeFilter::TKeyToFilter TAttributeFilter::Normalize() const
+{
+ YT_VERIFY(*this);
+ if (Paths.empty()) {
+ // Fast path for key-only case.
+ TKeyToFilter result;
+ result.reserve(Keys.size());
+ for (const auto& key : Keys) {
+ result[key] = std::nullopt;
+ }
+ return result;
+ }
+
+ // As a first step, prepare a combined vector of paths: canonize all paths
+ // and transform all keys to paths of form /<ToYPathLiteral(key)> (which is
+ // already a canonical form).
+ std::vector<TYPath> paths = Paths;
+ for (auto& path : paths) {
+ NDetail::CanonizeAndValidatePath(path);
+ }
+ paths.reserve(paths.size() + Keys.size());
+ for (const auto& key : Keys) {
+ paths.emplace_back("/" + ToYPathLiteral(key));
+ }
+
+ YT_VERIFY(!paths.empty());
+
+ // Then, sort all paths lexicographically and perform a unique-like procedure. Note that
+ // the lexicographical order for YPaths corresponds to the preorder traversal of a YTree.
+ std::sort(paths.begin(), paths.end());
+ {
+ // A pointer to the last taken path.
+ auto lastIt = paths.begin();
+ for (auto it = std::next(paths.begin()); it != paths.end(); ++it) {
+ // Check if the current path is in a subtree of that last taken path.
+ if (!HasPrefix(*it, *lastIt)) {
+ (++lastIt)->swap(*it);
+ }
+ }
+ paths.erase(++lastIt, paths.end());
+ }
+
+ // Finally, group remaining paths by the first token in path.
+
+ //! Split a path into a first key value and a remaining suffix.
+ auto splitPath = [] (const TYPath& path) -> std::pair<TString, TYPath> {
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Expect(NYPath::ETokenType::StartOfStream);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+ auto suffix = TYPath(tokenizer.GetSuffix());
+ return {std::move(key), std::move(suffix)};
+ };
+
+ TKeyToFilter result;
+
+ TString firstKey;
+ TYPath firstSuffix;
+ std::tie(firstKey, firstSuffix) = splitPath(paths.front());
+ std::vector<TYPath> subpaths = {std::move(firstSuffix)};
+
+ //! Finishes current group.
+ auto flushGroup = [&] {
+ result[firstKey] = std::move(subpaths);
+ };
+
+ for (auto firstIt = paths.begin(); firstIt != paths.end(); ) {
+ auto lastIt = std::next(firstIt);
+ // Extract a contiguous segment of paths starting from #firstKey.
+ while (true) {
+ // We have reached the end, flush the group.
+ if (lastIt == paths.end()) {
+ flushGroup();
+ break;
+ }
+ auto [lastKey, lastSuffix] = splitPath(*lastIt);
+ // A new group started; flush current group and setup firstKey for the next group.
+ if (lastKey != firstKey) {
+ flushGroup();
+ // Micro-optimization: re-use result of last splitting in order to perform as
+ // little splittings as possible.
+ firstKey = std::move(lastKey);
+ subpaths = {std::move(lastSuffix)};
+ break;
+ }
+ subpaths.emplace_back(std::move(lastSuffix));
+ ++lastIt;
+ }
+ firstIt = lastIt;
+ }
+
+ return result;
+}
+
+std::unique_ptr<TAttributeFilter::IFilteringConsumer> TAttributeFilter::CreateFilteringConsumer(
+ IYsonConsumer* targetConsumer,
+ const TPathFilter& pathFilter)
+{
+ if (!pathFilter) {
+ class TBypassFilteringConsumer
+ : public TAttributeFilter::IFilteringConsumer
+ {
+ public:
+ explicit TBypassFilteringConsumer(IYsonConsumer* targetConsumer)
+ : TargetConsumer_(targetConsumer)
+ { }
+
+ IYsonConsumer* GetConsumer() override
+ {
+ return TargetConsumer_;
+ }
+
+ void Finish() override
+ { }
+
+ private:
+ IYsonConsumer* TargetConsumer_;
+ };
+
+ return std::make_unique<TBypassFilteringConsumer>(targetConsumer);
+ }
+
+ // Implementation of CreateFilteringConsumerImpl with sync = true has an important property.
+ // Provided that async filtering consumer is used solely in synchronous manner,
+ // it invokes only synchronous part of the target consumer interface. Therefore,
+ // it is safe to implement this overload using TAsyncYsonConsumerAdapter which
+ // invokes YT_ABORT() whenever asynchronous interface is used on it, and return
+ // IAsyncYsonConsumer* upcasted to IYsonConsumer*.
+
+ auto asyncTargetConsumer = std::make_shared<TAsyncYsonConsumerAdapter>(targetConsumer);
+ // Do not forget to pass ownership for the adapter to a resulting filtering consumer.
+ return NDetail::CreateFilteringConsumerImpl(asyncTargetConsumer.get(), pathFilter, /*sync*/ true, /*targetConsumerHolder*/ asyncTargetConsumer);
+}
+
+std::unique_ptr<TAttributeFilter::IAsyncFilteringConsumer> TAttributeFilter::CreateAsyncFilteringConsumer(
+ IAsyncYsonConsumer* targetConsumer,
+ const TPathFilter& pathFilter)
+{
+ if (!pathFilter) {
+ class TBypassAsyncFilteringConsumer
+ : public TAttributeFilter::IAsyncFilteringConsumer
+ {
+ public:
+ explicit TBypassAsyncFilteringConsumer(IAsyncYsonConsumer* targetConsumer)
+ : TargetConsumer_(targetConsumer)
+ { }
+
+ IAsyncYsonConsumer* GetAsyncConsumer() override
+ {
+ return TargetConsumer_;
+ }
+
+ void Finish() override
+ { }
+
+ private:
+ IAsyncYsonConsumer* TargetConsumer_;
+ };
+
+ return std::make_unique<TBypassAsyncFilteringConsumer>(targetConsumer);
+ }
+
+ return NDetail::CreateFilteringConsumerImpl(targetConsumer, pathFilter, /*sync*/ false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: universal filter is represented as an absent protobuf value.
+
+void ToProto(NProto::TAttributeFilter* protoFilter, const TAttributeFilter& filter)
+{
+ YT_VERIFY(filter);
+
+ ToProto(protoFilter->mutable_keys(), filter.Keys);
+ ToProto(protoFilter->mutable_paths(), filter.Paths);
+}
+
+void FromProto(TAttributeFilter* filter, const NProto::TAttributeFilter& protoFilter)
+{
+ filter->Universal = false;
+ FromProto(&filter->Keys, protoFilter.keys());
+ FromProto(&filter->Paths, protoFilter.paths());
+}
+
+void Serialize(const TAttributeFilter& filter, IYsonConsumer* consumer)
+{
+ if (filter) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("keys").Value(filter.Keys)
+ .Item("paths").Value(filter.Paths)
+ .EndMap();
+ } else {
+ BuildYsonFluently(consumer)
+ .Entity();
+ }
+}
+
+void Deserialize(TAttributeFilter& filter, const INodePtr& node)
+{
+ switch (node->GetType()) {
+ case ENodeType::Map: {
+ auto mapNode = node->AsMap();
+
+ filter.Universal = false;
+ filter.Keys.clear();
+ if (auto keysNode = mapNode->FindChild("keys")) {
+ filter.Keys = ConvertTo<std::vector<TString>>(keysNode);
+ }
+
+ filter.Paths.clear();
+ if (auto pathsNode = mapNode->FindChild("paths")) {
+ filter.Paths = ConvertTo<std::vector<TString>>(pathsNode);
+ }
+
+ break;
+ }
+ case ENodeType::List: {
+ // Compatibility mode with HTTP clients that specify attribute keys as string lists.
+ filter.Universal = false;
+ filter.Keys = ConvertTo<std::vector<TString>>(node);
+ filter.Paths = {};
+ break;
+ }
+ case ENodeType::Entity: {
+ filter.Universal = true;
+ filter.Keys = {};
+ filter.Paths = {};
+ break;
+ }
+ default:
+ THROW_ERROR_EXCEPTION("Unexpected attribute filter type: expected \"map\", \"list\" or \"entity\", got %Qlv", node->GetType());
+ }
+}
+
+void Deserialize(TAttributeFilter& attributeFilter, TYsonPullParserCursor* cursor)
+{
+ Deserialize(attributeFilter, ExtractTo<NYTree::INodePtr>(cursor));
+}
+
+void FormatValue(
+ TStringBuilderBase* builder,
+ const TAttributeFilter& attributeFilter,
+ TStringBuf /*format*/)
+{
+ if (attributeFilter) {
+ builder->AppendFormat("{Keys: %v, Paths: %v}", attributeFilter.Keys, attributeFilter.Paths);
+ } else {
+ builder->AppendString("(universal)");
+ }
+}
+
+TString ToString(const TAttributeFilter& attributeFilter)
+{
+ return ToStringViaBuilder(attributeFilter);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attribute_filter.h b/yt/yt/core/ytree/attribute_filter.h
new file mode 100644
index 0000000000..f3dafe86c7
--- /dev/null
+++ b/yt/yt/core/ytree/attribute_filter.h
@@ -0,0 +1,164 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <any>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Structure representing a whitelist of attributes to be returned by get/list-like requests.
+/*!
+ * A filter is defined by a collection of top-level keys and a collection of YPaths. Result
+ * is constructed as a union of subtrees defined by all keys and paths.
+ *
+ * It is allowed for keys and paths to define intersecting or coinciding subtrees.
+ *
+ * A special case of filter is universal filter, which admits all attributes. Note that in such case
+ * a particular YPath service may have its own policy whether to produce all attributes or not,
+ * e.g. Cypress documents produce all attributes, while Cypress nodes produce no attributes
+ * (i.e. act the same as non-universal empty filter).
+ *
+ * In general, universal filter is treated as an unspecified filter. Default-constructed filter
+ * is universal; universal filter casts to boolean false value.
+ *
+ * Example 1:
+ * Attributes = {
+ * foo = 42;
+ * bar = {x = 2; y = []};
+ * baz = {x = {a = 1; b = 2}; y = {a = 3; b = 4}};
+ * }
+ * Filter = {.Keys = {"bar"}; .Paths = {"/baz/y"}, .Universal = false}
+ * Result = {
+ * bar = {x = 2; y = []};
+ * baz = {y = {a = 3; b = 4}};
+ * }
+ *
+ * Example 2:
+ * Attributes = {
+ * foo = [a; b; c; d];
+ * }
+ * Filter = {.Keys = {}; .Paths = {"/foo/0", "/foo/2"}, .Universal = false}
+ * Result = {
+ * foo = {a; #; c; #];
+ * }
+ *
+ * Example 3:
+ * Attributes = {
+ * foo = 42;
+ * bar = baz;
+ * }
+ * Filter = {.Keys = {}; .Paths = {}, .Universal = false}
+ * Result = {}
+ *
+ * Example 4:
+ * Attributes = {
+ * foo = 42;
+ * bar = baz;
+ * }
+ * Filter = {.Keys = {}; .Paths = {}, .Universal = true}
+ * Result depends on implementation.
+ */
+struct TAttributeFilter
+{
+ //! Whitelist of top-level keys to be returned.
+ std::vector<TString> Keys;
+ std::vector<NYPath::TYPath> Paths;
+
+ //! If true, filter is universal, i.e. behavior depends on service's own policy;
+ //! in such case #Keys and #Paths are always empty.
+ bool Universal = true;
+
+ //! Creates a universal filter.
+ TAttributeFilter() = default;
+
+ //! Creates a non-universal filter from given keys and paths.
+ //! This constructor is intentionally non-explicit so that common idiom attributeFilter = {"foo", "bar"} works.
+ TAttributeFilter(std::vector<TString> keys, std::vector<NYPath::TYPath> paths = {});
+
+ TAttributeFilter& operator =(std::vector<TString> keys);
+
+ //! Returns true for non-universal filter and false otherwise.
+ explicit operator bool() const;
+
+ //! Returns true for non-universal filter with empty keys and paths.
+ bool IsEmpty() const;
+
+ //! If #Paths are non-empty, throws an exception. Suitable for YPath service implementations
+ //! that are not ready for by-path attribute filtering. Context argument allows customizing
+ //! error message.
+ void ValidateKeysOnly(TStringBuf context = "this context") const;
+
+ //! Returns true if #key appears in Keys or "/#key" appears in Paths using linear search.
+ bool AdmitsKeySlow(TStringBuf key) const;
+
+ // std::nullopt stands for "take the whole attribute without path filtration" (i.e. an equivalent of {""}).
+ using TPathFilter = std::optional<std::vector<TYPath>>;
+ using TKeyToFilter = THashMap<TString, TPathFilter>;
+
+ //! Normalization procedure removes redundant keys or paths and returns a mapping of form
+ //! top-level key -> list of YPaths inside this top-level key (i.e. with /<key> stripped off) or
+ //! to std::nullopt, which stands for "take the top-level key as is".
+ TKeyToFilter Normalize() const;
+
+ //! This helper structure enabling us to either return given IYsonConsumer* as is
+ //! without creating any new consumers, or to wrap it into another consumer actual
+ //! filtering.
+ struct IFilteringConsumer
+ {
+ virtual ~IFilteringConsumer() = default;
+ //! Returns the sync consumer to be used for filtering.
+ virtual NYson::IYsonConsumer* GetConsumer() = 0;
+ //! Call of this method indicates that the whole input is fed to a filtering consumer and it is
+ //! safe to transfer the filtered result to the target consumer.
+ virtual void Finish() = 0;
+ };
+
+ //! Similar as above, but suitable for asynchronous consumer interface.
+ struct IAsyncFilteringConsumer
+ {
+ virtual ~IAsyncFilteringConsumer() = default;
+ //! Returns the sync consumer to be used for filtering.
+ virtual NYson::IAsyncYsonConsumer* GetAsyncConsumer() = 0;
+ //! Call of this method indicates that the whole input is fed to a filtering consumer and it is
+ //! safe to transfer the filtered result to the target consumer.
+ virtual void Finish() = 0;
+ };
+
+ //! Performs a one-pass filtration of YSON stream according to the given path filter and produces
+ //! the result to the #targetConsumer as a value corresponding to a key #key.
+ //! In particular, if #pathFilter is an std::nullopt, #targetConsumer is returned as-is effectively
+ //! implementing a zero-cost bypassing.
+ static std::unique_ptr<IFilteringConsumer> CreateFilteringConsumer(
+ NYson::IYsonConsumer* targetConsumer,
+ const TPathFilter& pathFilter);
+
+ //! Same as CreateFilteringConsumer above, but asynchronous.
+ static std::unique_ptr<IAsyncFilteringConsumer> CreateAsyncFilteringConsumer(
+ NYson::IAsyncYsonConsumer* targetConsumer,
+ const TPathFilter& pathFilter);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ToProto(NProto::TAttributeFilter* protoFilter, const TAttributeFilter& filter);
+void FromProto(TAttributeFilter* filter, const NProto::TAttributeFilter& protoFilter);
+
+void Serialize(const TAttributeFilter& filter, NYson::IYsonConsumer* consumer);
+void Deserialize(TAttributeFilter& filter, const INodePtr& node);
+void Deserialize(TAttributeFilter& attributeFilter, NYson::TYsonPullParserCursor* cursor);
+
+void FormatValue(
+ TStringBuilderBase* builder,
+ const TAttributeFilter& attributeFilter,
+ TStringBuf /*format*/);
+TString ToString(const TAttributeFilter& attributeFilter);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attribute_owner.h b/yt/yt/core/ytree/attribute_owner.h
new file mode 100644
index 0000000000..eed9a3faf2
--- /dev/null
+++ b/yt/yt/core/ytree/attribute_owner.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IAttributeOwner
+{
+ virtual ~IAttributeOwner() = default;
+
+ virtual const IAttributeDictionary& Attributes() const = 0;
+ virtual IAttributeDictionary* MutableAttributes() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attributes.cpp b/yt/yt/core/ytree/attributes.cpp
new file mode 100644
index 0000000000..ed1ab329d6
--- /dev/null
+++ b/yt/yt/core/ytree/attributes.cpp
@@ -0,0 +1,82 @@
+#include "attributes.h"
+#include "helpers.h"
+#include "ephemeral_node_factory.h"
+#include "exception_helpers.h"
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonString IAttributeDictionary::GetYson(TStringBuf key) const
+{
+ auto result = FindYson(key);
+ if (!result) {
+ ThrowNoSuchAttribute(key);
+ }
+ return result;
+}
+
+TYsonString IAttributeDictionary::GetYsonAndRemove(const TString& key)
+{
+ auto result = GetYson(key);
+ Remove(key);
+ return result;
+}
+
+void IAttributeDictionary::MergeFrom(const IMapNodePtr& other)
+{
+ for (const auto& [key, value] : other->GetChildren()) {
+ SetYson(key, ConvertToYsonString(value));
+ }
+}
+
+void IAttributeDictionary::MergeFrom(const IAttributeDictionary& other)
+{
+ for (const auto& [key, value] : other.ListPairs()) {
+ SetYson(key, value);
+ }
+}
+
+IAttributeDictionaryPtr IAttributeDictionary::Clone() const
+{
+ auto attributes = CreateEphemeralAttributes();
+ attributes->MergeFrom(*this);
+ return attributes;
+}
+
+void IAttributeDictionary::Clear()
+{
+ for (const auto& key : ListKeys()) {
+ Remove(key);
+ }
+}
+
+bool IAttributeDictionary::Contains(TStringBuf key) const
+{
+ return FindYson(key).operator bool();
+}
+
+IAttributeDictionaryPtr IAttributeDictionary::FromMap(const IMapNodePtr& node)
+{
+ auto attributes = CreateEphemeralAttributes();
+ auto children = node->GetChildren();
+ for (int index = 0; index < std::ssize(children); ++index) {
+ attributes->SetYson(children[index].first, ConvertToYsonString(children[index].second));
+ }
+ return attributes;
+}
+
+IMapNodePtr IAttributeDictionary::ToMap() const
+{
+ auto map = GetEphemeralNodeFactory()->CreateMap();
+ for (const auto& [key, value] : ListPairs()) {
+ map->AddChild(key, ConvertToNode(value));
+ }
+ return map;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/attributes.h b/yt/yt/core/ytree/attributes.h
new file mode 100644
index 0000000000..bff7c8fd55
--- /dev/null
+++ b/yt/yt/core/ytree/attributes.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/misc/optional.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IAttributeDictionary
+ : public TRefCounted
+{
+ using TKey = TString;
+ using TValue = NYson::TYsonString;
+ using TKeyValuePair = std::pair<TKey, TValue>;
+
+ //! Returns the list of all keys in the dictionary.
+ virtual std::vector<TString> ListKeys() const = 0;
+
+ //! Returns the list of all key-value pairs in the dictionary.
+ virtual std::vector<TKeyValuePair> ListPairs() const = 0;
+
+ //! Returns the value of the attribute (null indicates that the attribute is not found).
+ virtual NYson::TYsonString FindYson(TStringBuf key) const = 0;
+
+ //! Sets the value of the attribute.
+ virtual void SetYson(const TString& key, const NYson::TYsonString& value) = 0;
+
+ //! Removes the attribute.
+ //! Returns |true| if the attribute was removed or |false| if there is no attribute with this key.
+ virtual bool Remove(const TString& key) = 0;
+
+ // Extension methods
+
+ //! Removes all attributes.
+ void Clear();
+
+ //! Returns the value of the attribute (throws an exception if the attribute is not found).
+ NYson::TYsonString GetYson(TStringBuf key) const;
+
+ //! Same as #GetYson but removes the value.
+ NYson::TYsonString GetYsonAndRemove(const TString& key);
+
+ //! Finds the attribute and deserializes its value.
+ //! Throws if no such value is found.
+ template <class T>
+ T Get(TStringBuf key) const;
+
+ //! Same as #Get but removes the value.
+ template <class T>
+ T GetAndRemove(const TString& key);
+
+ //! Finds the attribute and deserializes its value.
+ //! Uses default value if no such attribute is found.
+ template <class T>
+ T Get(TStringBuf key, const T& defaultValue) const;
+
+ //! Same as #Get but removes the value if it exists.
+ template <class T>
+ T GetAndRemove(const TString& key, const T& defaultValue);
+
+ //! Finds the attribute and deserializes its value.
+ //! Returns null if no such attribute is found.
+ template <class T>
+ typename TOptionalTraits<T>::TOptional Find(TStringBuf key) const;
+
+ //! Same as #Find but removes the value if it exists.
+ template <class T>
+ typename TOptionalTraits<T>::TOptional FindAndRemove(const TString& key);
+
+ //! Returns |true| iff the given key is present.
+ bool Contains(TStringBuf key) const;
+
+ //! Sets the attribute with a serialized value.
+ template <class T>
+ void Set(const TString& key, const T& value);
+
+ //! Constructs an instance from a map node (by serializing the values).
+ static IAttributeDictionaryPtr FromMap(const IMapNodePtr& node);
+
+ //! Converts attributes to map node.
+ IMapNodePtr ToMap() const;
+
+ //! Adds more attributes from another map node.
+ void MergeFrom(const IMapNodePtr& other);
+
+ //! Adds more attributes from another attribute dictionary.
+ void MergeFrom(const IAttributeDictionary& other);
+
+ //! Constructs an ephemeral copy.
+ IAttributeDictionaryPtr Clone() const;
+};
+
+DEFINE_REFCOUNTED_TYPE(IAttributeDictionary)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/convert-inl.h b/yt/yt/core/ytree/convert-inl.h
new file mode 100644
index 0000000000..0e0e1c925b
--- /dev/null
+++ b/yt/yt/core/ytree/convert-inl.h
@@ -0,0 +1,298 @@
+#ifndef CONVERT_INL_H_
+#error "Direct inclusion of this file is not allowed, include convert.h"
+// For the sake of sane code completion.
+#include "convert.h"
+#endif
+
+#include "default_building_consumer.h"
+#include "serialize.h"
+#include "tree_builder.h"
+#include "helpers.h"
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/yson/tokenizer.h>
+#include <yt/yt/core/yson/parser.h>
+#include <yt/yt/core/yson/stream.h>
+#include <yt/yt/core/yson/producer.h>
+#include <yt/yt/core/yson/pull_parser.h>
+#include <yt/yt/core/yson/pull_parser_deserialize.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <type_traits>
+#include <limits>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value)
+{
+ return ConvertToYsonString(value, EYsonFormat::Binary);
+}
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value, EYsonFormat format)
+{
+ auto type = NYTree::GetYsonType(value);
+ TString result;
+ TStringOutput stringOutput(result);
+ NYTree::WriteYson(&stringOutput, value, type, format);
+ return NYson::TYsonString(std::move(result), type);
+}
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value, EYsonFormat format, int indent)
+{
+ auto type = NYTree::GetYsonType(value);
+ TString result;
+ TStringOutput stringOutput(result);
+ NYTree::WriteYson(&stringOutput, value, type, format, indent);
+ return NYson::TYsonString(std::move(result), type);
+}
+
+template <class T>
+TYsonString ConvertToYsonStringNestingLimited(const T& value, int nestingLevelLimit)
+{
+ using NYTree::Serialize;
+
+ auto type = NYTree::GetYsonType(value);
+ TStringStream stream;
+ {
+ TBufferedBinaryYsonWriter writer(
+ &stream,
+ EYsonType::Node,
+ /*enableRaw*/ false,
+ nestingLevelLimit);
+ Serialize(value, &writer);
+ writer.Flush();
+ }
+ return NYson::TYsonString(std::move(stream.Str()), type);
+}
+
+template <>
+TYsonString ConvertToYsonStringNestingLimited(const TYsonString& value, int nestingLevelLimit);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYSon
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+NYson::TYsonProducer ConvertToProducer(T&& value)
+{
+ auto type = GetYsonType(value);
+ auto callback = BIND(
+ [] (const T& value, NYson::IYsonConsumer* consumer) {
+ Serialize(value, consumer);
+ },
+ std::forward<T>(value));
+ return NYson::TYsonProducer(std::move(callback), type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+INodePtr ConvertToNode(
+ const T& value,
+ INodeFactory* factory)
+{
+ auto type = GetYsonType(value);
+
+ auto builder = CreateBuilderFromFactory(factory);
+ builder->BeginTree();
+
+ switch (type) {
+ case NYson::EYsonType::ListFragment:
+ builder->OnBeginList();
+ break;
+ case NYson::EYsonType::MapFragment:
+ builder->OnBeginMap();
+ break;
+ default:
+ break;
+ }
+
+ Serialize(value, builder.get());
+
+ switch (type) {
+ case NYson::EYsonType::ListFragment:
+ builder->OnEndList();
+ break;
+ case NYson::EYsonType::MapFragment:
+ builder->OnEndMap();
+ break;
+ default:
+ break;
+ }
+
+ return builder->EndTree();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+IAttributeDictionaryPtr ConvertToAttributes(const T& value)
+{
+ auto attributes = CreateEphemeralAttributes();
+ TAttributeConsumer consumer(attributes.Get());
+ Serialize(value, &consumer);
+ return attributes;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTo>
+TTo ConvertTo(const INodePtr& node)
+{
+ auto result = ConstructYTreeConvertibleObject<TTo>();
+ Deserialize(result, node);
+ return result;
+}
+
+template <class TTo, class TFrom>
+TTo ConvertTo(const TFrom& value)
+{
+ auto type = GetYsonType(value);
+ if constexpr (
+ NYson::ArePullParserDeserializable<TTo>() &&
+ (std::is_same_v<TFrom, NYson::TYsonString> || std::is_same_v<TFrom, NYson::TYsonStringBuf>))
+ {
+ using NYson::Deserialize;
+ using NYTree::Deserialize;
+
+ TMemoryInput input(value.AsStringBuf());
+ NYson::TYsonPullParser parser(&input, type);
+ NYson::TYsonPullParserCursor cursor(&parser);
+ TTo result = ConstructYTreeConvertibleObject<TTo>();
+
+ Deserialize(result, &cursor);
+ if (!cursor->IsEndOfStream()) {
+ THROW_ERROR_EXCEPTION("Expected end of stream after parsing YSON, found %Qlv",
+ cursor->GetType());
+ }
+ return result;
+ }
+ std::unique_ptr<NYson::IBuildingYsonConsumer<TTo>> buildingConsumer;
+ CreateBuildingYsonConsumer(&buildingConsumer, type);
+ Serialize(value, buildingConsumer.get());
+ return buildingConsumer->Finish();
+}
+
+const NYson::TToken& SkipAttributes(NYson::TTokenizer* tokenizer);
+
+#define IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(type) \
+ template <> \
+ inline type ConvertTo(const NYson::TYsonString& str) \
+ { \
+ NYson::TTokenizer tokenizer(str.AsStringBuf()); \
+ const auto& token = SkipAttributes(&tokenizer); \
+ switch (token.GetType()) { \
+ case NYson::ETokenType::Int64: \
+ return CheckedIntegralCast<type>(token.GetInt64Value()); \
+ case NYson::ETokenType::Uint64: \
+ return CheckedIntegralCast<type>(token.GetUint64Value()); \
+ default: \
+ THROW_ERROR_EXCEPTION("Cannot parse \"" #type "\" from %Qlv", \
+ token.GetType()) \
+ << TErrorAttribute("data", str.AsStringBuf()); \
+ } \
+ }
+
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(i64)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(i32)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(i16)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(i8)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(ui64)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(ui32)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(ui16)
+IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO(ui8)
+
+#undef IMPLEMENT_CHECKED_INTEGRAL_CONVERT_TO
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T ConstructYTreeConvertibleObject()
+{
+ if constexpr (std::is_constructible_v<T>) {
+ return T();
+ } else {
+ return T::Create();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+double ConvertYsonStringBaseToDouble(const NYson::TYsonStringBuf& yson)
+{
+ NYson::TTokenizer tokenizer(yson.AsStringBuf());
+ const auto& token = SkipAttributes(&tokenizer);
+ switch (token.GetType()) {
+ case NYson::ETokenType::Int64:
+ return token.GetInt64Value();
+ case NYson::ETokenType::Double:
+ return token.GetDoubleValue();
+ case NYson::ETokenType::Boolean:
+ return token.GetBooleanValue();
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse \"double\" from %Qlv",
+ token.GetType())
+ << TErrorAttribute("data", yson.AsStringBuf());
+ }
+}
+
+TString ConvertYsonStringBaseToString(const NYson::TYsonStringBuf& yson)
+{
+ NYson::TTokenizer tokenizer(yson.AsStringBuf());
+ const auto& token = SkipAttributes(&tokenizer);
+ switch (token.GetType()) {
+ case NYson::ETokenType::String:
+ return TString(token.GetStringValue());
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse \"string\" from %Qlv",
+ token.GetType())
+ << TErrorAttribute("data", yson.AsStringBuf());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+}
+
+template <>
+inline double ConvertTo(const NYson::TYsonString& str)
+{
+ return ConvertYsonStringBaseToDouble(str);
+}
+
+template <>
+inline double ConvertTo(const NYson::TYsonStringBuf& str)
+{
+ return ConvertYsonStringBaseToDouble(str);
+}
+
+template <>
+inline TString ConvertTo(const NYson::TYsonString& str)
+{
+ return ConvertYsonStringBaseToString(str);
+}
+
+template <>
+inline TString ConvertTo(const NYson::TYsonStringBuf& str)
+{
+ return ConvertYsonStringBaseToString(str);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/convert.cpp b/yt/yt/core/ytree/convert.cpp
new file mode 100644
index 0000000000..597bacf5b0
--- /dev/null
+++ b/yt/yt/core/ytree/convert.cpp
@@ -0,0 +1,65 @@
+#include "convert.h"
+
+#include <yt/yt/core/yson/tokenizer.h>
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+TYsonString ConvertToYsonStringNestingLimited(const TYsonString& value, int nestingLevelLimit)
+{
+ TMemoryInput input(value.AsStringBuf());
+ TYsonPullParser parser(&input, value.GetType(), nestingLevelLimit);
+ TYsonPullParserCursor cursor(&parser);
+
+ TStringStream stream;
+ stream.Str().reserve(value.AsStringBuf().size());
+ {
+ TCheckedInDebugYsonTokenWriter writer(&stream, value.GetType(), nestingLevelLimit);
+ cursor.TransferComplexValue(&writer);
+ writer.Finish();
+ }
+ return TYsonString(std::move(stream.Str()), value.GetType());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYSon
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TToken& SkipAttributes(TTokenizer* tokenizer)
+{
+ int depth = 0;
+ while (true) {
+ tokenizer->ParseNext();
+ const auto& token = tokenizer->CurrentToken();
+ switch (token.GetType()) {
+ case ETokenType::LeftBrace:
+ case ETokenType::LeftAngle:
+ ++depth;
+ break;
+
+ case ETokenType::RightBrace:
+ case ETokenType::RightAngle:
+ --depth;
+ break;
+
+ default:
+ if (depth == 0) {
+ return token;
+ }
+ break;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/convert.h b/yt/yt/core/ytree/convert.h
new file mode 100644
index 0000000000..5df7ca44f3
--- /dev/null
+++ b/yt/yt/core/ytree/convert.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "public.h"
+#include "ephemeral_node_factory.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+
+namespace NYT::NYson {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value);
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value, EYsonFormat format);
+
+template <class T>
+TYsonString ConvertToYsonString(const T& value, EYsonFormat format, int indent);
+
+template <class T>
+TYsonString ConvertToYsonStringNestingLimited(const T& value, int nestingLevelLimit);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYSon
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+NYson::TYsonProducer ConvertToProducer(T&& value);
+
+template <class T>
+INodePtr ConvertToNode(
+ const T& value,
+ INodeFactory* factory = GetEphemeralNodeFactory());
+
+template <class T>
+IAttributeDictionaryPtr ConvertToAttributes(const T& value);
+
+template <class TTo>
+TTo ConvertTo(const INodePtr& node);
+
+template <class TTo, class TFrom>
+TTo ConvertTo(const TFrom& value);
+
+template <class T>
+T ConstructYTreeConvertibleObject();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define CONVERT_INL_H_
+#include "convert-inl.h"
+#undef CONVERT_INL_H_
diff --git a/yt/yt/core/ytree/default_building_consumer-inl.h b/yt/yt/core/ytree/default_building_consumer-inl.h
new file mode 100644
index 0000000000..abdd3837be
--- /dev/null
+++ b/yt/yt/core/ytree/default_building_consumer-inl.h
@@ -0,0 +1,82 @@
+#ifndef DEFAULT_BUILDING_CONSUMER_INL_H_
+#error "Direct inclusion of this file is not allowed, include default_building_consumer.h"
+// For the sake of sane code completion.
+#include "default_building_consumer.h"
+#endif
+
+#include "ephemeral_node_factory.h"
+#include "tree_builder.h"
+#include "convert.h"
+
+#include <yt/yt/core/yson/forwarding_consumer.h>
+
+#include <memory>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+template <class T>
+class TBuildingYsonConsumerViaTreeBuilder
+ : public NYson::TForwardingYsonConsumer
+ , public NYson::IBuildingYsonConsumer<T>
+{
+public:
+ TBuildingYsonConsumerViaTreeBuilder(NYson::EYsonType ysonType)
+ : TreeBuilder_(CreateBuilderFromFactory(GetEphemeralNodeFactory()))
+ , YsonType_(ysonType)
+ {
+ TreeBuilder_->BeginTree();
+
+ switch (YsonType_) {
+ case NYson::EYsonType::ListFragment:
+ TreeBuilder_->OnBeginList();
+ break;
+ case NYson::EYsonType::MapFragment:
+ TreeBuilder_->OnBeginMap();
+ break;
+ default:
+ break;
+ }
+
+ Forward(TreeBuilder_.get());
+ }
+
+ T Finish() override
+ {
+ switch (YsonType_) {
+ case NYson::EYsonType::ListFragment:
+ TreeBuilder_->OnEndList();
+ break;
+ case NYson::EYsonType::MapFragment:
+ TreeBuilder_->OnEndMap();
+ break;
+ default:
+ break;
+ }
+
+ T result = ConstructYTreeConvertibleObject<T>();
+ Deserialize(result, TreeBuilder_->EndTree());
+ return result;
+ }
+
+private:
+ std::unique_ptr<ITreeBuilder> TreeBuilder_;
+ NYson::EYsonType YsonType_;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void CreateBuildingYsonConsumer(std::unique_ptr<NYson::IBuildingYsonConsumer<T>>* buildingConsumer, NYson::EYsonType ysonType)
+{
+ *buildingConsumer = std::unique_ptr<TBuildingYsonConsumerViaTreeBuilder<T>>(new TBuildingYsonConsumerViaTreeBuilder<T>(ysonType));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/default_building_consumer.h b/yt/yt/core/ytree/default_building_consumer.h
new file mode 100644
index 0000000000..6eb2080ae2
--- /dev/null
+++ b/yt/yt/core/ytree/default_building_consumer.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <yt/yt/core/yson/building_consumer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void CreateBuildingYsonConsumer(std::unique_ptr<NYson::IBuildingYsonConsumer<T>>* buildingConsumer, NYson::EYsonType ysonType);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define DEFAULT_BUILDING_CONSUMER_INL_H_
+#include "default_building_consumer-inl.h"
+#undef DEFAULT_BUILDING_CONSUMER_INL_H_
diff --git a/yt/yt/core/ytree/ephemeral_attribute_owner.cpp b/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
new file mode 100644
index 0000000000..6e7f2631d2
--- /dev/null
+++ b/yt/yt/core/ytree/ephemeral_attribute_owner.cpp
@@ -0,0 +1,36 @@
+#include "ephemeral_attribute_owner.h"
+#include "helpers.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+const IAttributeDictionary& TEphemeralAttributeOwner::Attributes() const
+{
+ if (!HasAttributes()) {
+ return EmptyAttributes();
+ }
+ return *Attributes_;
+}
+
+IAttributeDictionary* TEphemeralAttributeOwner::MutableAttributes()
+{
+ if (!HasAttributes()) {
+ Attributes_ = CreateEphemeralAttributes();
+ }
+ return Attributes_.Get();
+}
+
+bool TEphemeralAttributeOwner::HasAttributes() const
+{
+ return static_cast<bool>(Attributes_);
+}
+
+void TEphemeralAttributeOwner::SetAttributes(IAttributeDictionaryPtr attributes)
+{
+ Attributes_ = std::move(attributes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ephemeral_attribute_owner.h b/yt/yt/core/ytree/ephemeral_attribute_owner.h
new file mode 100644
index 0000000000..af4dcc6617
--- /dev/null
+++ b/yt/yt/core/ytree/ephemeral_attribute_owner.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "attribute_owner.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEphemeralAttributeOwner
+ : public virtual IAttributeOwner
+{
+public:
+ const IAttributeDictionary& Attributes() const override;
+ IAttributeDictionary* MutableAttributes() override;
+
+protected:
+ bool HasAttributes() const;
+ void SetAttributes(IAttributeDictionaryPtr attributes);
+
+private:
+ IAttributeDictionaryPtr Attributes_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ephemeral_node_factory.cpp b/yt/yt/core/ytree/ephemeral_node_factory.cpp
new file mode 100644
index 0000000000..fe15dabb29
--- /dev/null
+++ b/yt/yt/core/ytree/ephemeral_node_factory.cpp
@@ -0,0 +1,517 @@
+#include "ephemeral_node_factory.h"
+#include "ephemeral_attribute_owner.h"
+#include "node_detail.h"
+#include "ypath_client.h"
+#include "ypath_detail.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/yson/async_consumer.h>
+#include <yt/yt/core/yson/attribute_consumer.h>
+
+#include <library/cpp/yt/misc/hash.h>
+
+#include <algorithm>
+
+namespace NYT::NYTree {
+
+using namespace NRpc;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEphemeralNodeBase
+ : public TSupportsAttributes
+ , public TSupportsSetSelfMixin
+ , public TEphemeralAttributeOwner
+{
+public:
+ explicit TEphemeralNodeBase(bool shouldHideAttributes)
+ : ShouldHideAttributes_(shouldHideAttributes)
+ { }
+
+ std::unique_ptr<ITransactionalNodeFactory> CreateFactory() const override
+ {
+ return CreateEphemeralNodeFactory(ShouldHideAttributes_);
+ }
+
+ ICompositeNodePtr GetParent() const override
+ {
+ return Parent_.Lock();
+ }
+
+ void SetParent(const ICompositeNodePtr& parent) override
+ {
+ YT_ASSERT(!parent || Parent_.IsExpired());
+ Parent_ = parent;
+ }
+
+ bool ShouldHideAttributes() override
+ {
+ return ShouldHideAttributes_;
+ }
+
+ void DoWriteAttributesFragment(
+ IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable) override
+ {
+ if (!HasAttributes()) {
+ return;
+ }
+
+ const auto& attributes = Attributes();
+
+ auto pairs = attributes.ListPairs();
+ if (stable) {
+ std::sort(pairs.begin(), pairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ }
+
+ TAttributeFilter::TKeyToFilter keyToFilter;
+ if (attributeFilter) {
+ keyToFilter = attributeFilter.Normalize();
+ }
+
+ for (const auto& [key, value] : pairs) {
+ if (!attributeFilter) {
+ // A fast path for taking the whole attribute.
+ consumer->OnKeyedItem(key);
+ consumer->OnRaw(value);
+ } else if (auto it = keyToFilter.find(key); it != keyToFilter.end()) {
+ const auto& pathFilter = it->second;
+ TAttributeValueConsumer valueConsumer(consumer, key);
+ auto filteringConsumer = TAttributeFilter::CreateFilteringConsumer(&valueConsumer, pathFilter);
+ filteringConsumer->GetConsumer()->OnRaw(value);
+ filteringConsumer->Finish();
+ }
+ }
+ }
+
+protected:
+ // TSupportsAttributes members
+ IAttributeDictionary* GetCustomAttributes() override
+ {
+ return MutableAttributes();
+ }
+
+private:
+ TWeakPtr<ICompositeNode> Parent_;
+ bool ShouldHideAttributes_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue, class IBase>
+class TScalarNode
+ : public TEphemeralNodeBase
+ , public virtual IBase
+{
+public:
+ using TEphemeralNodeBase::TEphemeralNodeBase;
+
+ typename NMpl::TCallTraits<TValue>::TType GetValue() const override
+ {
+ return Value_;
+ }
+
+ void SetValue(typename NMpl::TCallTraits<TValue>::TType value) override
+ {
+ Value_ = value;
+ }
+
+private:
+ TValue Value_{};
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DECLARE_SCALAR_TYPE(type, cppType) \
+ class T##type##Node \
+ : public TScalarNode<cppType, I##type##Node> \
+ { \
+ public: \
+ YTREE_NODE_TYPE_OVERRIDES(type) \
+ \
+ public: \
+ using TScalarNode::TScalarNode; \
+ };
+
+DECLARE_SCALAR_TYPE(String, TString)
+DECLARE_SCALAR_TYPE(Int64, i64)
+DECLARE_SCALAR_TYPE(Uint64, ui64)
+DECLARE_SCALAR_TYPE(Double, double)
+DECLARE_SCALAR_TYPE(Boolean, bool)
+
+#undef DECLARE_SCALAR_TYPE
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class IBase>
+class TCompositeNodeBase
+ : public TEphemeralNodeBase
+ , public virtual IBase
+{
+public:
+ using TEphemeralNodeBase::TEphemeralNodeBase;
+
+ TIntrusivePtr<ICompositeNode> AsComposite() override
+ {
+ return this;
+ }
+
+ TIntrusivePtr<const ICompositeNode> AsComposite() const override
+ {
+ return this;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMapNode
+ : public TCompositeNodeBase<IMapNode>
+ , public TMapNodeMixin
+{
+public:
+ YTREE_NODE_TYPE_OVERRIDES(Map)
+
+public:
+ using TCompositeNodeBase::TCompositeNodeBase;
+
+ void Clear() override
+ {
+ for (const auto& [key, child] : KeyToChild_) {
+ child->SetParent(nullptr);
+ }
+ KeyToChild_.clear();
+ ChildToKey_.clear();
+ }
+
+ int GetChildCount() const override
+ {
+ return KeyToChild_.ysize();
+ }
+
+ std::vector< std::pair<TString, INodePtr> > GetChildren() const override
+ {
+ return std::vector< std::pair<TString, INodePtr> >(KeyToChild_.begin(), KeyToChild_.end());
+ }
+
+ std::vector<TString> GetKeys() const override
+ {
+ std::vector<TString> result;
+ result.reserve(KeyToChild_.size());
+ for (const auto& [key, child] : KeyToChild_) {
+ result.push_back(key);
+ }
+ return result;
+ }
+
+ INodePtr FindChild(const TString& key) const override
+ {
+ auto it = KeyToChild_.find(key);
+ return it == KeyToChild_.end() ? nullptr : it->second;
+ }
+
+ bool AddChild(const TString& key, const INodePtr& child) override
+ {
+ YT_ASSERT(child);
+ ValidateYTreeKey(key);
+
+ if (KeyToChild_.emplace(key, child).second) {
+ YT_VERIFY(ChildToKey_.emplace(child, key).second);
+ child->SetParent(this);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool RemoveChild(const TString& key) override
+ {
+ auto it = KeyToChild_.find(TString(key));
+ if (it == KeyToChild_.end())
+ return false;
+
+ auto child = it->second;
+ child->SetParent(nullptr);
+ KeyToChild_.erase(it);
+ YT_VERIFY(ChildToKey_.erase(child) == 1);
+
+ return true;
+ }
+
+ void RemoveChild(const INodePtr& child) override
+ {
+ YT_ASSERT(child);
+
+ child->SetParent(nullptr);
+
+ auto it = ChildToKey_.find(child);
+ YT_ASSERT(it != ChildToKey_.end());
+
+ // NB: don't use const auto& here, it becomes invalid!
+ auto key = it->second;
+ ChildToKey_.erase(it);
+ YT_VERIFY(KeyToChild_.erase(key) == 1);
+ }
+
+ void ReplaceChild(const INodePtr& oldChild, const INodePtr& newChild) override
+ {
+ YT_ASSERT(oldChild);
+ YT_ASSERT(newChild);
+
+ if (oldChild == newChild)
+ return;
+
+ auto it = ChildToKey_.find(oldChild);
+ YT_ASSERT(it != ChildToKey_.end());
+
+ // NB: don't use const auto& here, it becomes invalid!
+ auto key = it->second;
+
+ oldChild->SetParent(nullptr);
+ ChildToKey_.erase(it);
+
+ KeyToChild_[key] = newChild;
+ newChild->SetParent(this);
+ YT_VERIFY(ChildToKey_.emplace(newChild, key).second);
+ }
+
+ std::optional<TString> FindChildKey(const IConstNodePtr& child) override
+ {
+ YT_ASSERT(child);
+
+ auto it = ChildToKey_.find(const_cast<INode*>(child.Get()));
+ return it == ChildToKey_.end() ? std::nullopt : std::make_optional(it->second);
+ }
+
+private:
+ THashMap<TString, INodePtr> KeyToChild_;
+ THashMap<INodePtr, TString> ChildToKey_;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+ return TEphemeralNodeBase::DoInvoke(context);
+ }
+
+ IYPathService::TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override
+ {
+ return TMapNodeMixin::ResolveRecursive(path, context);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListNode
+ : public TCompositeNodeBase<IListNode>
+ , public TListNodeMixin
+{
+public:
+ YTREE_NODE_TYPE_OVERRIDES(List)
+
+public:
+ using TCompositeNodeBase::TCompositeNodeBase;
+
+ void Clear() override
+ {
+ for (const auto& node : IndexToChild_) {
+ node->SetParent(nullptr);
+ }
+ IndexToChild_.clear();
+ ChildToIndex_.clear();
+ }
+
+ int GetChildCount() const override
+ {
+ return IndexToChild_.size();
+ }
+
+ std::vector<INodePtr> GetChildren() const override
+ {
+ return IndexToChild_;
+ }
+
+ INodePtr FindChild(int index) const override
+ {
+ return index >= 0 && index < std::ssize(IndexToChild_) ? IndexToChild_[index] : nullptr;
+ }
+
+ void AddChild(const INodePtr& child, int beforeIndex = -1) override
+ {
+ YT_ASSERT(child);
+
+ if (beforeIndex < 0) {
+ YT_VERIFY(ChildToIndex_.emplace(child, static_cast<int>(IndexToChild_.size())).second);
+ IndexToChild_.push_back(child);
+ } else {
+ YT_VERIFY(beforeIndex <= std::ssize(IndexToChild_));
+ for (auto it = IndexToChild_.begin() + beforeIndex; it != IndexToChild_.end(); ++it) {
+ ++ChildToIndex_[*it];
+ }
+
+ YT_VERIFY(ChildToIndex_.emplace(child, beforeIndex).second);
+ IndexToChild_.insert(IndexToChild_.begin() + beforeIndex, child);
+ }
+ child->SetParent(this);
+ }
+
+ bool RemoveChild(int index) override
+ {
+ if (index < 0 || index >= std::ssize(IndexToChild_))
+ return false;
+
+ auto child = IndexToChild_[index];
+
+ for (auto it = IndexToChild_.begin() + index + 1; it != IndexToChild_.end(); ++it) {
+ --ChildToIndex_[*it];
+ }
+ IndexToChild_.erase(IndexToChild_.begin() + index);
+
+ YT_VERIFY(ChildToIndex_.erase(child) == 1);
+ child->SetParent(nullptr);
+
+ return true;
+ }
+
+ void ReplaceChild(const INodePtr& oldChild, const INodePtr& newChild) override
+ {
+ YT_ASSERT(oldChild);
+ YT_ASSERT(newChild);
+
+ if (oldChild == newChild)
+ return;
+
+ auto it = ChildToIndex_.find(oldChild);
+ YT_ASSERT(it != ChildToIndex_.end());
+
+ int index = it->second;
+
+ oldChild->SetParent(nullptr);
+
+ IndexToChild_[index] = newChild;
+ ChildToIndex_.erase(it);
+ YT_VERIFY(ChildToIndex_.emplace(newChild, index).second);
+ newChild->SetParent(this);
+ }
+
+ void RemoveChild(const INodePtr& child) override
+ {
+ YT_ASSERT(child);
+
+ int index = GetChildIndexOrThrow(child);
+ YT_VERIFY(RemoveChild(index));
+ }
+
+ std::optional<int> FindChildIndex(const IConstNodePtr& child) override
+ {
+ YT_ASSERT(child);
+
+ auto it = ChildToIndex_.find(const_cast<INode*>(child.Get()));
+ return it == ChildToIndex_.end() ? std::nullopt : std::make_optional(it->second);
+ }
+
+private:
+ std::vector<INodePtr> IndexToChild_;
+ THashMap<INodePtr, int> ChildToIndex_;
+
+ TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override
+ {
+ return TListNodeMixin::ResolveRecursive(path, context);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEntityNode
+ : public TEphemeralNodeBase
+ , public virtual IEntityNode
+{
+public:
+ YTREE_NODE_TYPE_OVERRIDES(Entity)
+
+public:
+ using TEphemeralNodeBase::TEphemeralNodeBase;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEphemeralNodeFactory
+ : public TTransactionalNodeFactoryBase
+{
+public:
+ explicit TEphemeralNodeFactory(bool shouldHideAttributes)
+ : ShouldHideAttributes_(shouldHideAttributes)
+ { }
+
+ virtual ~TEphemeralNodeFactory() override
+ {
+ RollbackIfNeeded();
+ }
+
+ IStringNodePtr CreateString() override
+ {
+ return New<TStringNode>(ShouldHideAttributes_);
+ }
+
+ IInt64NodePtr CreateInt64() override
+ {
+ return New<TInt64Node>(ShouldHideAttributes_);
+ }
+
+ IUint64NodePtr CreateUint64() override
+ {
+ return New<TUint64Node>(ShouldHideAttributes_);
+ }
+
+ IDoubleNodePtr CreateDouble() override
+ {
+ return New<TDoubleNode>(ShouldHideAttributes_);
+ }
+
+ IBooleanNodePtr CreateBoolean() override
+ {
+ return New<TBooleanNode>(ShouldHideAttributes_);
+ }
+
+ IMapNodePtr CreateMap() override
+ {
+ return New<TMapNode>(ShouldHideAttributes_);
+ }
+
+ IListNodePtr CreateList() override
+ {
+ return New<TListNode>(ShouldHideAttributes_);
+ }
+
+ IEntityNodePtr CreateEntity() override
+ {
+ return New<TEntityNode>(ShouldHideAttributes_);
+ }
+
+private:
+ const bool ShouldHideAttributes_;
+};
+
+std::unique_ptr<ITransactionalNodeFactory> CreateEphemeralNodeFactory(bool shouldHideAttributes)
+{
+ return std::unique_ptr<ITransactionalNodeFactory>(new TEphemeralNodeFactory(shouldHideAttributes));
+}
+
+INodeFactory* GetEphemeralNodeFactory(bool shouldHideAttributes)
+{
+ static auto hidingFactory = CreateEphemeralNodeFactory(true);
+ static auto nonhidingFactory = CreateEphemeralNodeFactory(false);
+ return shouldHideAttributes ? hidingFactory.get() : nonhidingFactory.get();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/ephemeral_node_factory.h b/yt/yt/core/ytree/ephemeral_node_factory.h
new file mode 100644
index 0000000000..400a377371
--- /dev/null
+++ b/yt/yt/core/ytree/ephemeral_node_factory.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "node.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Builds a transactional factory for creating ephemeral
+//! (non-persistent, in memory) YTree nodes.
+std::unique_ptr<ITransactionalNodeFactory> CreateEphemeralNodeFactory(bool shouldHideAttributes = false);
+
+//! Returns a cached instance of non-transactional factory.
+INodeFactory* GetEphemeralNodeFactory(bool shouldHideAttributes = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/exception_helpers.cpp b/yt/yt/core/ytree/exception_helpers.cpp
new file mode 100644
index 0000000000..8e27ceb8e0
--- /dev/null
+++ b/yt/yt/core/ytree/exception_helpers.cpp
@@ -0,0 +1,153 @@
+#include "exception_helpers.h"
+#include "node.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/ytree/helpers.h>
+
+namespace NYT::NYTree {
+
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+TString GetNodePath(const IConstNodePtr& node)
+{
+ auto path = node->GetPath();
+ return path.empty() ? "Root node" : Format("Node %v", path);
+}
+
+} // namespace
+
+void ThrowInvalidNodeType(const IConstNodePtr& node, ENodeType expectedType, ENodeType actualType)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "%v has invalid type: expected %Qlv, actual %Qlv",
+ GetNodePath(node),
+ expectedType,
+ actualType);
+}
+
+void ValidateNodeType(
+ const IConstNodePtr& node,
+ const THashSet<ENodeType>& expectedTypes,
+ const TString& expectedTypesStringRepresentation)
+{
+ if (!expectedTypes.contains(node->GetType())) {
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "%v has invalid type: expected one of %v, actual %Qlv",
+ GetNodePath(node),
+ expectedTypesStringRepresentation,
+ node->GetType());
+ }
+}
+
+void ThrowNoSuchChildKey(const IConstNodePtr& node, TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "%v has no child with key %Qv",
+ GetNodePath(node),
+ ToYPathLiteral(key));
+}
+
+void ThrowNoSuchChildKey(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION("Node has no child with key %Qv",
+ ToYPathLiteral(key));
+}
+
+void ThrowNoSuchChildIndex(const IConstNodePtr& node, int index)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "%v has no child with index %v",
+ GetNodePath(node),
+ index);
+}
+
+void ThrowNoSuchAttribute(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Attribute %Qv is not found",
+ ToYPathLiteral(key));
+}
+
+void ThrowNoSuchCustomAttribute(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Custom attribute %Qv is not found",
+ ToYPathLiteral(key));
+}
+
+void ThrowNoSuchBuiltinAttribute(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Builtin attribute %Qv is not found",
+ ToYPathLiteral(key));
+}
+
+void ThrowMethodNotSupported(TStringBuf method, const std::optional<TString>& resolveType)
+{
+ auto error = TError(
+ NRpc::EErrorCode::NoSuchMethod,
+ "%Qv method is not supported",
+ method);
+ if (resolveType) {
+ error.MutableAttributes()->Set("resolve_type", *resolveType);
+ }
+ THROW_ERROR(error);
+}
+
+void ThrowCannotHaveChildren(const IConstNodePtr& node)
+{
+ THROW_ERROR_EXCEPTION("%v cannot have children",
+ GetNodePath(node));
+}
+
+void ThrowAlreadyExists(const IConstNodePtr& node)
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::AlreadyExists,
+ "%v already exists",
+ GetNodePath(node));
+}
+
+void ThrowCannotRemoveNode(const IConstNodePtr& node)
+{
+ THROW_ERROR_EXCEPTION("%v cannot be removed",
+ GetNodePath(node));
+}
+
+void ThrowCannotReplaceNode(const IConstNodePtr& node)
+{
+ THROW_ERROR_EXCEPTION("%v cannot be replaced",
+ GetNodePath(node));
+}
+
+void ThrowCannotRemoveAttribute(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION("Attribute %Qv cannot be removed",
+ ToYPathLiteral(key));
+}
+
+void ThrowCannotSetBuiltinAttribute(TStringBuf key)
+{
+ THROW_ERROR_EXCEPTION("Builtin attribute %Qv cannot be set",
+ ToYPathLiteral(key));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/exception_helpers.h b/yt/yt/core/ytree/exception_helpers.h
new file mode 100644
index 0000000000..1fc45fc6af
--- /dev/null
+++ b/yt/yt/core/ytree/exception_helpers.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/public.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+[[noreturn]] void ThrowInvalidNodeType(
+ const IConstNodePtr& node,
+ ENodeType expectedType,
+ ENodeType actualType);
+void ValidateNodeType(
+ const IConstNodePtr& node,
+ const THashSet<ENodeType>& expectedTypes,
+ const TString& expectedTypesStringRepresentation);
+
+[[noreturn]] void ThrowNoSuchChildKey(const IConstNodePtr& node, TStringBuf key);
+[[noreturn]] void ThrowNoSuchChildKey(TStringBuf key);
+[[noreturn]] void ThrowNoSuchChildIndex(const IConstNodePtr& node, int index);
+[[noreturn]] void ThrowNoSuchAttribute(TStringBuf key);
+[[noreturn]] void ThrowNoSuchBuiltinAttribute(TStringBuf key);
+[[noreturn]] void ThrowNoSuchCustomAttribute(TStringBuf key);
+[[noreturn]] void ThrowMethodNotSupported(
+ TStringBuf method,
+ const std::optional<TString>& resolveType = {});
+[[noreturn]] void ThrowCannotHaveChildren(const IConstNodePtr& node);
+[[noreturn]] void ThrowAlreadyExists(const IConstNodePtr& node);
+[[noreturn]] void ThrowCannotRemoveNode(const IConstNodePtr& node);
+[[noreturn]] void ThrowCannotReplaceNode(const IConstNodePtr& node);
+[[noreturn]] void ThrowCannotRemoveAttribute(TStringBuf key);
+[[noreturn]] void ThrowCannotSetBuiltinAttribute(TStringBuf key);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/fluent.h b/yt/yt/core/ytree/fluent.h
new file mode 100644
index 0000000000..45ab6875ea
--- /dev/null
+++ b/yt/yt/core/ytree/fluent.h
@@ -0,0 +1,794 @@
+#pragma once
+
+#include "public.h"
+#include "tree_visitor.h"
+#include "tree_builder.h"
+#include "convert.h"
+#include "attribute_consumer.h"
+#include "helpers.h"
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/producer.h>
+#include <yt/yt/core/yson/parser.h>
+
+#include <yt/yt/core/actions/callback.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+// WHAT IS THIS
+//
+// Fluent adapters encapsulate invocation of IYsonConsumer methods in a
+// convenient structured manner. Key advantage of fluent-like code is that
+// attempt of building syntactically incorrect YSON structure will result
+// in a compile-time error.
+//
+// Each fluent object is associated with a context that defines possible YSON
+// tokens that may appear next. For example, TFluentMap is a fluent object
+// that corresponds to a location within YSON map right before a key-value
+// pair or the end of the map.
+//
+// More precisely, each object that may be obtained by a sequence of fluent
+// method calls has the full history of its enclosing YSON composite types in
+// its single template argument hereinafter referred to as TParent. This allows
+// us not to forget the original context after opening and closing the embedded
+// composite structure.
+//
+// It is possible to invoke a separate YSON building procedure by calling
+// one of convenience Do* methods. There are two possibilities here: it is
+// possible to delegate invocation context either as a fluent object (like
+// TFluentMap, TFluentList, TFluentAttributes or TFluentAny) or as a raw
+// IYsonConsumer*. The latter is discouraged since it is impossible to check
+// if a given side-built YSON structure fits current fluent context.
+// For example it is possible to call Do() method inside YSON map passing
+// consumer to a procedure that will treat context like it is in a list.
+// Passing typed fluent builder saves you from such a misbehaviour.
+//
+// TFluentXxx corresponds to an internal class of TXxx
+// without any history hidden in template argument. It allows you to
+// write procedures of form:
+//
+// void BuildSomeAttributesInYson(TFluentMap fluent) { ... }
+//
+// without thinking about the exact way how this procedure is nested in other
+// procedures.
+//
+// An important notation: we will refer to a function whose first argument
+// is TFluentXxx as TFuncXxx.
+//
+//
+// BRIEF LIST OF AVAILABLE METHODS
+//
+// Only the most popular methods are covered here. Refer to the code for the
+// rest of them.
+//
+// TAny:
+// * Value(T value) -> TParent, serialize `value` using underlying consumer.
+// T should be such that free function Serialize(IYsonConsumer*, const T&) is
+// defined;
+// * BeginMap() -> TFluentMap, open map;
+// * BeginList() -> TFluentList, open list;
+// * BeginAttributes() -> TFluentAttributes, open attributes;
+//
+// * Do(TFuncAny func) -> TAny, delegate invocation to a separate procedure.
+// * DoIf(bool condition, TFuncAny func) -> TAny, same as Do() but invoke
+// `func` only if `condition` is true;
+// * DoFor(TCollection collection, TFuncAny func) -> TAny, same as Do()
+// but iterate over `collection` and pass each of its elements as a second
+// argument to `func`. Instead of passing a collection you may it is possible
+// to pass two iterators as an argument;
+//
+// * DoMap(TFuncMap func) -> TAny, open a map, delegate invocation to a separate
+// procedure and close map;
+// * DoMapFor(TCollection collection, TFuncMap func) -> TAny, open a map, iterate
+// over `collection` and pass each of its elements as a second argument to `func`
+// and close map;
+// * DoList(TFuncList func) -> TAny, same as DoMap();
+// * DoListFor(TCollection collection, TFuncList func) -> TAny; same as DoMapFor().
+//
+//
+// TFluentMap:
+// * Item(TStringBuf key) -> TAny, open an element keyed with `key`;
+// * EndMap() -> TParent, close map;
+// * Do(TFuncMap func) -> TFluentMap, same as Do() for TAny;
+// * DoIf(bool condition, TFuncMap func) -> TFluentMap, same as DoIf() for TAny;
+// * DoFor(TCollection collection, TFuncMap func) -> TFluentMap, same as DoFor() for TAny.
+//
+//
+// TFluentList:
+// * Item() -> TAny, open an new list element;
+// * EndList() -> TParent, close list;
+// * Do(TFuncList func) -> TFluentList, same as Do() for TAny;
+// * DoIf(bool condition, TFuncList func) -> TFluentList, same as DoIf() for TAny;
+// * DoFor(TCollection collection, TListMap func) -> TFluentList, same as DoFor() for TAny.
+//
+//
+// TFluentAttributes:
+// * Item(TStringBuf key) -> TAny, open an element keyed with `key`.
+// * EndAttributes() -> TParentWithoutAttributes, close attributes. Note that
+// this method leads to a context that is forces not to have attributes,
+// preventing us from putting attributes twice before an object.
+// * Do(TFuncAttributes func) -> TFluentAttributes, same as Do() for TAny;
+// * DoIf(bool condition, TFuncAttributes func) -> TFluentAttributes, same as DoIf()
+// for TAny;
+// * DoFor(TCollection collection, TListAttributes func) -> TFluentAttributes, same as DoFor()
+// for TAny.
+//
+ */
+
+template <class T>
+struct TFluentYsonUnwrapper
+{
+ typedef T TUnwrapped;
+
+ static TUnwrapped Unwrap(T t)
+ {
+ return std::move(t);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TFluentYsonVoid
+{ };
+
+template <>
+struct TFluentYsonUnwrapper<TFluentYsonVoid>
+{
+ typedef void TUnwrapped;
+
+ static TUnwrapped Unwrap(TFluentYsonVoid)
+ { }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TFluent, class TFunc, class... TArgs>
+void InvokeFluentFunc(TFunc func, NYson::IYsonConsumer* consumer, TArgs&&... args)
+{
+ func(TFluent(consumer), std::forward<TArgs>(args)...);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentYsonBuilder
+ : private TNonCopyable
+{
+private:
+ template <class T, class... TExtraArgs>
+ static void WriteValue(NYson::IYsonConsumer* consumer, const T& value, TExtraArgs&&... extraArgs)
+ {
+ Serialize(value, consumer, std::forward<TExtraArgs>(extraArgs)...);
+ }
+
+public:
+ class TFluentAny;
+ template <class TParent> class TAny;
+ template <class TParent> class TToAttributes;
+ template <class TParent> class TFluentAttributes;
+ template <class TParent> class TFluentList;
+ template <class TParent> class TFluentMap;
+
+ template <class TParent>
+ class TFluentBase
+ {
+ public:
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ NYson::IYsonConsumer* GetConsumer() const
+ {
+ return Consumer;
+ }
+
+ TUnwrappedParent Finish()
+ {
+ return GetUnwrappedParent();
+ }
+
+ protected:
+ NYson::IYsonConsumer* Consumer;
+ TParent Parent;
+ bool Unwrapped_ = false;
+
+ TFluentBase(NYson::IYsonConsumer* consumer, TParent parent)
+ : Consumer(consumer)
+ , Parent(std::move(parent))
+ { }
+
+ TUnwrappedParent GetUnwrappedParent()
+ {
+ YT_VERIFY(!Unwrapped_);
+ Unwrapped_ = true;
+ return TFluentYsonUnwrapper<TParent>::Unwrap(std::move(Parent));
+ }
+ };
+
+ template <template <class TParent> class TThis, class TParent, class TShallowThis>
+ class TFluentFragmentBase
+ : public TFluentBase<TParent>
+ {
+ public:
+ typedef TThis<TParent> TDeepThis;
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ explicit TFluentFragmentBase(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentBase<TParent>(consumer, std::move(parent))
+ { }
+
+ TDeepThis Do(auto func)
+ {
+ InvokeFluentFunc<TShallowThis>(func, this->Consumer);
+ return static_cast<TDeepThis&&>(*this);
+ }
+
+ TDeepThis DoIf(bool condition, auto func)
+ {
+ if (condition) {
+ InvokeFluentFunc<TShallowThis>(func, this->Consumer);
+ }
+ return static_cast<TDeepThis&&>(*this);
+ }
+
+ TDeepThis DoFor(auto begin, auto end, auto func)
+ {
+ for (auto current = begin; current != end; ++current) {
+ InvokeFluentFunc<TShallowThis>(func, this->Consumer, current);
+ }
+ return static_cast<TDeepThis&&>(*this);
+ }
+
+ TDeepThis DoFor(const auto& collection, auto func)
+ {
+ for (const auto& item : collection) {
+ InvokeFluentFunc<TShallowThis>(func, this->Consumer, item);
+ }
+ return static_cast<TDeepThis&&>(*this);
+ }
+ };
+
+ template <class TParent, class TShallowThis>
+ class TAnyBase
+ : public TFluentBase<TParent>
+ {
+ public:
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ TAnyBase(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentBase<TParent>(consumer, std::move(parent))
+ { }
+
+ TUnwrappedParent Do(auto funcAny)
+ {
+ InvokeFluentFunc<TShallowThis>(funcAny, this->Consumer);
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent Value(const auto& value, auto&&... extraArgs)
+ {
+ WriteValue(this->Consumer, value, std::forward<decltype(extraArgs)>(extraArgs)...);
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent Entity()
+ {
+ this->Consumer->OnEntity();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent List(const auto& collection)
+ {
+ this->Consumer->OnBeginList();
+ for (const auto& item : collection) {
+ this->Consumer->OnListItem();
+ WriteValue(this->Consumer, item);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent ListLimited(const auto& collection, size_t maxSize)
+ {
+ this->Consumer->OnBeginAttributes();
+ this->Consumer->OnKeyedItem("count");
+ this->Consumer->OnInt64Scalar(collection.size());
+ this->Consumer->OnEndAttributes();
+ this->Consumer->OnBeginList();
+ size_t printedSize = 0;
+ for (const auto& item : collection) {
+ if (printedSize >= maxSize)
+ break;
+ this->Consumer->OnListItem();
+ WriteValue(this->Consumer, item);
+ ++printedSize;
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ TFluentList<TParent> BeginList()
+ {
+ this->Consumer->OnBeginList();
+ return TFluentList<TParent>(this->Consumer, std::move(this->Parent));
+ }
+
+ TUnwrappedParent DoList(auto funcList)
+ {
+ this->Consumer->OnBeginList();
+ InvokeFluentFunc<TFluentList<TFluentYsonVoid>>(funcList, this->Consumer);
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent DoListFor(auto begin, auto end, auto funcList)
+ {
+ this->Consumer->OnBeginList();
+ for (auto current = begin; current != end; ++current) {
+ InvokeFluentFunc<TFluentList<TFluentYsonVoid>>(funcList, this->Consumer, current);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent DoListFor(const auto& collection, auto funcList)
+ {
+ this->Consumer->OnBeginList();
+ for (const auto& item : collection) {
+ InvokeFluentFunc<TFluentList<TFluentYsonVoid>>(funcList, this->Consumer, item);
+ }
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+
+ TFluentMap<TParent> BeginMap()
+ {
+ this->Consumer->OnBeginMap();
+ return TFluentMap<TParent>(this->Consumer, std::move(this->Parent));
+ }
+
+ TUnwrappedParent DoMap(auto funcMap)
+ {
+ this->Consumer->OnBeginMap();
+ InvokeFluentFunc<TFluentMap<TFluentYsonVoid>>(funcMap, this->Consumer);
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent DoMapFor(auto begin, auto end, auto funcMap)
+ {
+ this->Consumer->OnBeginMap();
+ for (auto current = begin; current != end; ++current) {
+ InvokeFluentFunc<TFluentMap<TFluentYsonVoid>>(funcMap, this->Consumer, current);
+ }
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+
+ TUnwrappedParent DoMapFor(const auto& collection, auto funcMap)
+ {
+ this->Consumer->OnBeginMap();
+ for (const auto& item : collection) {
+ InvokeFluentFunc<TFluentMap<TFluentYsonVoid>>(funcMap, this->Consumer, item);
+ }
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ template <class TParent>
+ class TAnyWithoutAttributes
+ : public TAnyBase<TParent, TAnyWithoutAttributes<TFluentYsonVoid>>
+ {
+ public:
+ typedef TAnyBase<TParent, TAnyWithoutAttributes<TFluentYsonVoid>> TBase;
+
+ explicit TAnyWithoutAttributes(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TBase(consumer, std::move(parent))
+ { }
+ };
+
+ template <class TParent>
+ class TAny
+ : public TAnyBase<TParent, TAny<TFluentYsonVoid>>
+ {
+ public:
+ typedef TAnyBase<TParent, TAny<TFluentYsonVoid>> TBase;
+
+ explicit TAny(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TBase(consumer, std::move(parent))
+ { }
+
+ TFluentAttributes<TAnyWithoutAttributes<TParent>> BeginAttributes()
+ {
+ this->Consumer->OnBeginAttributes();
+ return TFluentAttributes<TAnyWithoutAttributes<TParent>>(
+ this->Consumer,
+ TAnyWithoutAttributes<TParent>(this->Consumer, std::move(this->Parent)));
+ }
+ };
+
+ template <class TParent = TFluentYsonVoid>
+ class TFluentAttributes
+ : public TFluentFragmentBase<TFluentAttributes, TParent, TFluentMap<TFluentYsonVoid>>
+ {
+ public:
+ typedef TFluentAttributes<TParent> TThis;
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ explicit TFluentAttributes(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TFluentAttributes, TParent, TFluentMap<TFluentYsonVoid>>(consumer, std::move(parent))
+ { }
+
+ template <size_t Size>
+ TAny<TThis> Item(const char (&key)[Size])
+ {
+ return Item(TStringBuf(key, Size - 1));
+ }
+
+ TAny<TThis> Item(TStringBuf key)
+ {
+ this->Consumer->OnKeyedItem(key);
+ return TAny<TThis>(this->Consumer, std::move(*this));
+ }
+
+ TThis& Items(const IMapNodePtr& map)
+ {
+ for (const auto& [key, child] : map->GetChildren()) {
+ this->Consumer->OnKeyedItem(key);
+ VisitTree(child, this->Consumer, true);
+ }
+ return *this;
+ }
+
+ TThis& Items(const IAttributeDictionary& attributes)
+ {
+ for (const auto& [key, value] : attributes.ListPairs()) {
+ this->Consumer->OnKeyedItem(key);
+ this->Consumer->OnRaw(value);
+ }
+ return *this;
+ }
+
+ TThis& Items(const NYson::TYsonString& attributes)
+ {
+ YT_VERIFY(attributes.GetType() == NYson::EYsonType::MapFragment);
+ this->Consumer->OnRaw(attributes);
+ return *this;
+ }
+
+ TThis& OptionalItem(TStringBuf key, const auto& optionalValue, auto&&... extraArgs)
+ {
+ if (optionalValue) {
+ this->Consumer->OnKeyedItem(key);
+ WriteValue(this->Consumer, optionalValue, std::forward<decltype(extraArgs)>(extraArgs)...);
+ }
+ return *this;
+ }
+
+ TUnwrappedParent EndAttributes()
+ {
+ this->Consumer->OnEndAttributes();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ template <class TParent = TFluentYsonVoid>
+ class TFluentList
+ : public TFluentFragmentBase<TFluentList, TParent, TFluentList<TFluentYsonVoid>>
+ {
+ public:
+ typedef TFluentList<TParent> TThis;
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ explicit TFluentList(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TFluentList, TParent, TFluentList<TFluentYsonVoid>>(consumer, std::move(parent))
+ { }
+
+ TAny<TThis> Item()
+ {
+ this->Consumer->OnListItem();
+ return TAny<TThis>(this->Consumer, std::move(*this));
+ }
+
+ TThis& Items(const IListNodePtr& list)
+ {
+ for (auto item : list->GetChildren()) {
+ this->Consumer->OnListItem();
+ VisitTree(std::move(item), this->Consumer, true);
+ }
+ return *this;
+ }
+
+ TThis& OptionalItem(const auto& optionalValue, auto&&... extraArgs)
+ {
+ if (optionalValue) {
+ this->Consumer->OnListItem();
+ WriteValue(this->Consumer, optionalValue, std::forward<decltype(extraArgs)>(extraArgs)...);
+ }
+ return *this;
+ }
+
+ TUnwrappedParent EndList()
+ {
+ this->Consumer->OnEndList();
+ return this->GetUnwrappedParent();
+ }
+ };
+
+ template <class TParent = TFluentYsonVoid>
+ class TFluentMap
+ : public TFluentFragmentBase<TFluentMap, TParent, TFluentMap<TFluentYsonVoid>>
+ {
+ public:
+ typedef TFluentMap<TParent> TThis;
+ typedef typename TFluentYsonUnwrapper<TParent>::TUnwrapped TUnwrappedParent;
+
+ explicit TFluentMap(NYson::IYsonConsumer* consumer, TParent parent = TParent())
+ : TFluentFragmentBase<TFluentYsonBuilder::TFluentMap, TParent, TFluentMap<TFluentYsonVoid>>(consumer, std::move(parent))
+ { }
+
+ template <size_t Size>
+ TAny<TThis> Item(const char (&key)[Size])
+ {
+ return Item(TStringBuf(key, Size - 1));
+ }
+
+ TAny<TThis> Item(TStringBuf key)
+ {
+ this->Consumer->OnKeyedItem(key);
+ return TAny<TThis>(this->Consumer, std::move(*this));
+ }
+
+ TThis& Items(const IMapNodePtr& map)
+ {
+ for (const auto& [key, child] : map->GetChildren()) {
+ this->Consumer->OnKeyedItem(key);
+ VisitTree(child, this->Consumer, true);
+ }
+ return *this;
+ }
+
+ TThis& Items(const IAttributeDictionary& attributes)
+ {
+ for (const auto& [key, value] : attributes.ListPairs()) {
+ this->Consumer->OnKeyedItem(key);
+ this->Consumer->OnRaw(value);
+ }
+ return *this;
+ }
+
+ TThis& Items(const NYson::TYsonString& attributes)
+ {
+ YT_VERIFY(attributes.GetType() == NYson::EYsonType::MapFragment);
+ this->Consumer->OnRaw(attributes);
+ return *this;
+ }
+
+ TThis& OptionalItem(TStringBuf key, const auto& optionalValue, auto&&... extraArgs)
+ {
+ if (optionalValue) {
+ this->Consumer->OnKeyedItem(key);
+ WriteValue(this->Consumer, optionalValue, std::forward<decltype(extraArgs)>(extraArgs)...);
+ }
+ return *this;
+ }
+
+ TUnwrappedParent EndMap()
+ {
+ this->Consumer->OnEndMap();
+ return this->GetUnwrappedParent();
+ }
+ };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <NYson::EYsonType>
+struct TFluentType;
+
+template <>
+struct TFluentType<NYson::EYsonType::Node>
+{
+ template <class T>
+ using TValue = TFluentYsonBuilder::TAny<T>;
+};
+
+template <>
+struct TFluentType<NYson::EYsonType::MapFragment>
+{
+ template <class T>
+ using TValue = TFluentYsonBuilder::TFluentMap<T>;
+};
+
+template <>
+struct TFluentType<NYson::EYsonType::ListFragment>
+{
+ template <class T>
+ using TValue = TFluentYsonBuilder::TFluentList<T>;
+};
+
+using TFluentList = TFluentYsonBuilder::TFluentList<TFluentYsonVoid>;
+using TFluentMap = TFluentYsonBuilder::TFluentMap<TFluentYsonVoid>;
+using TFluentAttributes = TFluentYsonBuilder::TFluentAttributes<TFluentYsonVoid>;
+using TFluentAny = TFluentYsonBuilder::TAny<TFluentYsonVoid>;
+using TFluentAnyWithoutAttributes = TFluentYsonBuilder::TAnyWithoutAttributes<TFluentYsonVoid>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static inline TFluentAny BuildYsonFluently(NYson::IYsonConsumer* consumer)
+{
+ return TFluentYsonBuilder::TAny<TFluentYsonVoid>(consumer, TFluentYsonVoid());
+}
+
+static inline TFluentList BuildYsonListFragmentFluently(NYson::IYsonConsumer* consumer)
+{
+ return TFluentList(consumer);
+}
+
+static inline TFluentMap BuildYsonMapFragmentFluently(NYson::IYsonConsumer* consumer)
+{
+ return TFluentMap(consumer);
+}
+
+static inline TFluentAttributes BuildYsonAttributesFluently(NYson::IYsonConsumer* consumer)
+{
+ return TFluentAttributes(consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentYsonWriterState
+ : public TRefCounted
+{
+public:
+ typedef NYson::TYsonString TValue;
+
+ TFluentYsonWriterState(NYson::EYsonFormat format, NYson::EYsonType type)
+ : Writer(&Output, format, type, true /* enableRaw */)
+ , Type(type)
+ { }
+
+ NYson::TYsonString GetValue()
+ {
+ return NYson::TYsonString(Output.Str(), Type);
+ }
+
+ NYson::IYsonConsumer* GetConsumer()
+ {
+ return &Writer;
+ }
+
+private:
+ TStringStream Output;
+ NYson::TYsonWriter Writer;
+ NYson::EYsonType Type;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentYsonBuilderState
+ : public TRefCounted
+{
+public:
+ typedef INodePtr TValue;
+
+ explicit TFluentYsonBuilderState(INodeFactory* factory)
+ : Builder(CreateBuilderFromFactory(factory))
+ { }
+
+ INodePtr GetValue()
+ {
+ return Builder->EndTree();
+ }
+
+ NYson::IYsonConsumer* GetConsumer()
+ {
+ return Builder.get();
+ }
+
+private:
+ const std::unique_ptr<ITreeBuilder> Builder;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFluentAttributeConsumerState
+ : public TRefCounted
+{
+public:
+ using TValue = IAttributeDictionaryPtr;
+
+ explicit TFluentAttributeConsumerState(std::optional<int> ysonNestingLevelLimit)
+ : Dictionary_(CreateEphemeralAttributes(ysonNestingLevelLimit))
+ , Consumer_(std::make_unique<TAttributeConsumer>(Dictionary_.Get()))
+ { }
+
+ IAttributeDictionaryPtr GetValue()
+ {
+ return Dictionary_;
+ }
+
+ NYson::IYsonConsumer* GetConsumer()
+ {
+ return Consumer_.get();
+ }
+
+private:
+ const IAttributeDictionaryPtr Dictionary_;
+ const std::unique_ptr<TAttributeConsumer> Consumer_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState>
+class TFluentYsonHolder
+{
+public:
+ explicit TFluentYsonHolder(TIntrusivePtr<TState> state)
+ : State(std::move(state))
+ { }
+
+ TIntrusivePtr<TState> GetState() const
+ {
+ return State;
+ }
+
+private:
+ const TIntrusivePtr<TState> State;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState>
+struct TFluentYsonUnwrapper<TFluentYsonHolder<TState>>
+{
+ typedef typename TState::TValue TUnwrapped;
+
+ static TUnwrapped Unwrap(const TFluentYsonHolder<TState>& holder)
+ {
+ return holder.GetState()->GetValue();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TState, NYson::EYsonType type>
+auto BuildYsonFluentlyWithState(TIntrusivePtr<TState> state)
+{
+ typedef typename TFluentType<type>::template TValue<TFluentYsonHolder<TState>> TReturnType;
+ return TReturnType(
+ state->GetConsumer(),
+ TFluentYsonHolder<TState>(state));
+}
+
+// XXX(max42): return types below look pretty nasty :( CLion is not able to deduce
+// them automatically that leads to lots of syntax errors.
+// Remove them when CLion does not suck.
+
+template <NYson::EYsonType type = NYson::EYsonType::Node>
+auto BuildYsonStringFluently(NYson::EYsonFormat format = NYson::EYsonFormat::Binary)
+ -> typename TFluentType<type>::template TValue<TFluentYsonHolder<TFluentYsonWriterState>>
+{
+ return BuildYsonFluentlyWithState<TFluentYsonWriterState, type>(New<TFluentYsonWriterState>(format, type));
+}
+
+inline auto BuildYsonNodeFluently(INodeFactory* factory = GetEphemeralNodeFactory())
+ -> typename TFluentType<NYson::EYsonType::Node>::template TValue<TFluentYsonHolder<TFluentYsonBuilderState>>
+{
+ return BuildYsonFluentlyWithState<TFluentYsonBuilderState, NYson::EYsonType::Node>(New<TFluentYsonBuilderState>(factory));
+}
+
+inline auto BuildAttributeDictionaryFluently(std::optional<int> ysonNestingLevelLimit = {})
+ -> typename TFluentType<NYson::EYsonType::MapFragment>::template TValue<TFluentYsonHolder<TFluentAttributeConsumerState>>
+{
+ return BuildYsonFluentlyWithState<TFluentAttributeConsumerState, NYson::EYsonType::MapFragment>(New<TFluentAttributeConsumerState>(ysonNestingLevelLimit));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/helpers-inl.h b/yt/yt/core/ytree/helpers-inl.h
new file mode 100644
index 0000000000..04a4962d0d
--- /dev/null
+++ b/yt/yt/core/ytree/helpers-inl.h
@@ -0,0 +1,133 @@
+#ifndef HELPERS_INL_H_
+#error "Direct inclusion of this file is not allowed, include helpers.h"
+// For the sake of sane code completion.
+#include "helpers.h"
+#endif
+
+#include "attribute_consumer.h"
+#include "serialize.h"
+#include "convert.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T IAttributeDictionary::Get(TStringBuf key) const
+{
+ auto yson = GetYson(key);
+ try {
+ return ConvertTo<T>(yson);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing attribute %Qv",
+ key)
+ << ex;
+ }
+}
+
+template <class T>
+T IAttributeDictionary::GetAndRemove(const TString& key)
+{
+ auto result = Get<T>(key);
+ Remove(key);
+ return result;
+}
+
+template <class T>
+T IAttributeDictionary::Get(TStringBuf key, const T& defaultValue) const
+{
+ return Find<T>(key).value_or(defaultValue);
+}
+
+template <class T>
+T IAttributeDictionary::GetAndRemove(const TString& key, const T& defaultValue)
+{
+ auto result = Find<T>(key);
+ if (result) {
+ Remove(key);
+ return *result;
+ } else {
+ return defaultValue;
+ }
+}
+
+template <class T>
+typename TOptionalTraits<T>::TOptional IAttributeDictionary::Find(TStringBuf key) const
+{
+ auto yson = FindYson(key);
+ if (!yson) {
+ return typename TOptionalTraits<T>::TOptional();
+ }
+ try {
+ return ConvertTo<T>(yson);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error parsing attribute %Qv",
+ key)
+ << ex;
+ }
+}
+
+template <class T>
+typename TOptionalTraits<T>::TOptional IAttributeDictionary::FindAndRemove(const TString& key)
+{
+ auto result = Find<T>(key);
+ if (result) {
+ Remove(key);
+ }
+ return result;
+}
+
+template <class T>
+void IAttributeDictionary::Set(const TString& key, const T& value)
+{
+ auto yson = ConvertToYsonString(value, NYson::EYsonFormat::Binary);
+ SetYson(key, yson);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, class R>
+IYPathServicePtr IYPathService::FromMethod(
+ R (std::remove_cv_t<T>::*method) () const,
+ const TWeakPtr<T>& weak)
+{
+ auto boundProducer = NYson::TYsonProducer(BIND_NO_PROPAGATE([=] (NYson::IYsonConsumer* consumer) {
+ auto strong = weak.Lock();
+ if (strong) {
+ Serialize((strong.Get()->*method)(), consumer);
+ } else {
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("object_destroyed");
+ consumer->OnBooleanScalar(true);
+ consumer->OnEndAttributes();
+ consumer->OnEntity();
+ }
+ }));
+
+ return FromProducer(std::move(boundProducer));
+}
+
+template <class T>
+IYPathServicePtr IYPathService::FromMethod(
+ void (std::remove_cv_t<T>::*producer) (NYson::IYsonConsumer*) const,
+ const TWeakPtr<T>& weak)
+{
+ auto boundProducer = NYson::TYsonProducer(BIND_NO_PROPAGATE([=] (NYson::IYsonConsumer* consumer) {
+ auto strong = weak.Lock();
+ if (strong) {
+ (strong.Get()->*producer)(consumer);
+ } else {
+ consumer->OnBeginAttributes();
+ consumer->OnKeyedItem("object_destroyed");
+ consumer->OnBooleanScalar(true);
+ consumer->OnEndAttributes();
+ consumer->OnEntity();
+ }
+ }));
+
+ return FromProducer(std::move(boundProducer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/helpers.cpp b/yt/yt/core/ytree/helpers.cpp
new file mode 100644
index 0000000000..cad1e1367f
--- /dev/null
+++ b/yt/yt/core/ytree/helpers.cpp
@@ -0,0 +1,344 @@
+#include "helpers.h"
+#include "ypath_client.h"
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator == (const IAttributeDictionary& lhs, const IAttributeDictionary& rhs)
+{
+ auto lhsPairs = lhs.ListPairs();
+ auto rhsPairs = rhs.ListPairs();
+ if (lhsPairs.size() != rhsPairs.size()) {
+ return false;
+ }
+
+ std::sort(lhsPairs.begin(), lhsPairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ std::sort(rhsPairs.begin(), rhsPairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+
+ for (auto index = 0; index < std::ssize(lhsPairs); ++index) {
+ if (lhsPairs[index].first != rhsPairs[index].first) {
+ return false;
+ }
+ }
+
+ for (auto index = 0; index < std::ssize(lhsPairs); ++index) {
+ auto lhsNode = ConvertToNode(lhsPairs[index].second);
+ auto rhsNode = ConvertToNode(rhsPairs[index].second);
+ if (!AreNodesEqual(lhsNode, rhsNode)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool operator != (const IAttributeDictionary& lhs, const IAttributeDictionary& rhs)
+{
+ return !(lhs == rhs);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEphemeralAttributeDictionary
+ : public IAttributeDictionary
+{
+public:
+ explicit TEphemeralAttributeDictionary(std::optional<int> ysonNestingLevelLimit = std::nullopt)
+ : NestingLevelLimit_(ysonNestingLevelLimit)
+ { }
+
+ std::vector<TString> ListKeys() const override
+ {
+ std::vector<TString> keys;
+ keys.reserve(Map_.size());
+ for (const auto& [key, value] : Map_) {
+ keys.push_back(key);
+ }
+ return keys;
+ }
+
+ std::vector<TKeyValuePair> ListPairs() const override
+ {
+ std::vector<TKeyValuePair> pairs;
+ pairs.reserve(Map_.size());
+ for (const auto& pair : Map_) {
+ pairs.push_back(pair);
+ }
+ return pairs;
+ }
+
+ TYsonString FindYson(TStringBuf key) const override
+ {
+ auto it = Map_.find(key);
+ return it == Map_.end() ? TYsonString() : it->second;
+ }
+
+ void SetYson(const TString& key, const TYsonString& value) override
+ {
+ YT_ASSERT(value.GetType() == EYsonType::Node);
+ if (NestingLevelLimit_) {
+ ValidateYson(value, *NestingLevelLimit_);
+ }
+ Map_[key] = value;
+ }
+
+ bool Remove(const TString& key) override
+ {
+ return Map_.erase(key) > 0;
+ }
+
+private:
+ THashMap<TString, TYsonString> Map_;
+ std::optional<int> NestingLevelLimit_;
+};
+
+IAttributeDictionaryPtr CreateEphemeralAttributes(std::optional<int> ysonNestingLevelLimit)
+{
+ return New<TEphemeralAttributeDictionary>(ysonNestingLevelLimit);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TEmptyAttributeDictionary
+ : public IAttributeDictionary
+{
+public:
+ std::vector<TString> ListKeys() const override
+ {
+ return {};
+ }
+
+ std::vector<TKeyValuePair> ListPairs() const override
+ {
+ return {};
+ }
+
+ TYsonString FindYson(TStringBuf /*key*/) const override
+ {
+ return {};
+ }
+
+ void SetYson(const TString& /*key*/, const TYsonString& /*value*/) override
+ {
+ YT_ABORT();
+ }
+
+ bool Remove(const TString& /*key*/) override
+ {
+ return false;
+ }
+};
+
+const IAttributeDictionary& EmptyAttributes()
+{
+ struct TSingleton
+ {
+ IAttributeDictionaryPtr EmptyAttributes = New<TEmptyAttributeDictionary>();
+ };
+
+ return *LeakySingleton<TSingleton>()->EmptyAttributes;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TThreadSafeAttributeDictionary
+ : public NYTree::IAttributeDictionary
+{
+public:
+ explicit TThreadSafeAttributeDictionary(IAttributeDictionary* underlying)
+ : Underlying_(underlying)
+ { }
+
+ std::vector<TString> ListKeys() const override
+ {
+ auto guard = ReaderGuard(Lock_);
+ return Underlying_->ListKeys();
+ }
+
+ std::vector<TKeyValuePair> ListPairs() const override
+ {
+ auto guard = ReaderGuard(Lock_);
+ return Underlying_->ListPairs();
+ }
+
+ NYson::TYsonString FindYson(TStringBuf key) const override
+ {
+ auto guard = ReaderGuard(Lock_);
+ return Underlying_->FindYson(key);
+ }
+
+ void SetYson(const TString& key, const NYson::TYsonString& value) override
+ {
+ auto guard = WriterGuard(Lock_);
+ Underlying_->SetYson(key, value);
+ }
+
+ bool Remove(const TString& key) override
+ {
+ auto guard = WriterGuard(Lock_);
+ return Underlying_->Remove(key);
+ }
+
+private:
+ IAttributeDictionary* const Underlying_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, Lock_);
+};
+
+IAttributeDictionaryPtr CreateThreadSafeAttributes(IAttributeDictionary* underlying)
+{
+ return New<TThreadSafeAttributeDictionary>(underlying);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const IAttributeDictionary& attributes, IYsonConsumer* consumer)
+{
+ auto pairs = attributes.ListPairs();
+ std::sort(pairs.begin(), pairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ consumer->OnBeginMap();
+ for (const auto& [key, value] : pairs) {
+ consumer->OnKeyedItem(key);
+ consumer->OnRaw(value);
+ }
+ consumer->OnEndMap();
+}
+
+void ToProto(NProto::TAttributeDictionary* protoAttributes, const IAttributeDictionary& attributes)
+{
+ protoAttributes->Clear();
+ auto pairs = attributes.ListPairs();
+ std::sort(pairs.begin(), pairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ protoAttributes->mutable_attributes()->Reserve(static_cast<int>(pairs.size()));
+ for (const auto& [key, value] : pairs) {
+ auto* protoAttribute = protoAttributes->add_attributes();
+ protoAttribute->set_key(key);
+ protoAttribute->set_value(value.ToString());
+ }
+}
+
+IAttributeDictionaryPtr FromProto(const NProto::TAttributeDictionary& protoAttributes)
+{
+ auto attributes = CreateEphemeralAttributes();
+ for (const auto& protoAttribute : protoAttributes.attributes()) {
+ const auto& key = protoAttribute.key();
+ const auto& value = protoAttribute.value();
+ attributes->SetYson(key, TYsonString(value));
+ }
+ return attributes;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TAttributeDictionarySerializer::Save(TStreamSaveContext& context, const IAttributeDictionaryPtr& attributes)
+{
+ using NYT::Save;
+
+ // Presence byte.
+ if (!attributes) {
+ Save(context, false);
+ return;
+ }
+
+ Save(context, true);
+ SaveNonNull(context, attributes);
+}
+
+void TAttributeDictionarySerializer::SaveNonNull(TStreamSaveContext& context, const IAttributeDictionaryPtr& attributes)
+{
+ using NYT::Save;
+ auto pairs = attributes->ListPairs();
+ std::sort(pairs.begin(), pairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ TSizeSerializer::Save(context, pairs.size());
+ for (const auto& [key, value] : pairs) {
+ Save(context, key);
+ Save(context, value);
+ }
+}
+
+void TAttributeDictionarySerializer::Load(TStreamLoadContext& context, IAttributeDictionaryPtr& attributes)
+{
+ using NYT::Load;
+
+ // We intentionally always recreate attributes from scratch as an ephemeral
+ // attribute dictionary. Do not expect any phoenix-like behaviour here.
+ attributes = CreateEphemeralAttributes();
+
+ // Presence byte.
+ if (!Load<bool>(context)) {
+ return;
+ }
+
+ LoadNonNull(context, attributes);
+}
+
+void TAttributeDictionarySerializer::LoadNonNull(TStreamLoadContext& context, const IAttributeDictionaryPtr& attributes)
+{
+ using NYT::Load;
+ attributes->Clear();
+ size_t size = TSizeSerializer::Load(context);
+ for (size_t index = 0; index < size; ++index) {
+ auto key = Load<TString>(context);
+ auto value = Load<TYsonString>(context);
+ attributes->SetYson(key, value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateYTreeKey(TStringBuf key)
+{
+ Y_UNUSED(key);
+ // XXX(vvvv): Disabled due to existing data with empty keys, see https://st.yandex-team.ru/YQL-2640
+#if 0
+ if (key.empty()) {
+ THROW_ERROR_EXCEPTION("Empty keys are not allowed in map nodes");
+ }
+#endif
+}
+
+void ValidateYPathResolutionDepth(const TYPath& path, int depth)
+{
+ if (depth > MaxYPathResolveIterations) {
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Path %v exceeds resolve depth limit",
+ path)
+ << TErrorAttribute("limit", MaxYPathResolveIterations);
+ }
+}
+
+std::vector<IAttributeDictionary::TKeyValuePair> ListAttributesPairs(const IAttributeDictionary& attributes)
+{
+ std::vector<IAttributeDictionary::TKeyValuePair> result;
+ auto keys = attributes.ListKeys();
+ result.reserve(keys.size());
+ for (const auto& key : keys) {
+ auto value = attributes.FindYson(key);
+ if (value) {
+ result.push_back(std::make_pair(key, value));
+ }
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/helpers.h b/yt/yt/core/ytree/helpers.h
new file mode 100644
index 0000000000..abb15fa2bb
--- /dev/null
+++ b/yt/yt/core/ytree/helpers.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <yt/yt_proto/yt/core/ytree/proto/attributes.pb.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Pretty slow.
+bool operator == (const IAttributeDictionary& lhs, const IAttributeDictionary& rhs);
+bool operator != (const IAttributeDictionary& lhs, const IAttributeDictionary& rhs);
+
+//! Creates attributes dictionary in memory.
+IAttributeDictionaryPtr CreateEphemeralAttributes(std::optional<int> ysonNestingLevelLimit = std::nullopt);
+
+//! Wraps an arbitrary implementation of IAttributeDictionary and turns it into a thread-safe one.
+//! Internally uses a read-write spinlock to protect the underlying instance from concurrent access.
+IAttributeDictionaryPtr CreateThreadSafeAttributes(IAttributeDictionary* underlying);
+
+//! Creates empty attributes dictionary with deprecated method Set.
+const IAttributeDictionary& EmptyAttributes();
+
+//! Serialize attributes to consumer. Used in ConvertTo* functions.
+void Serialize(const IAttributeDictionary& attributes, NYson::IYsonConsumer* consumer);
+
+//! Protobuf conversion methods.
+void ToProto(NProto::TAttributeDictionary* protoAttributes, const IAttributeDictionary& attributes);
+IAttributeDictionaryPtr FromProto(const NProto::TAttributeDictionary& protoAttributes);
+
+//! By-ptr binary serializer.
+//! Supports TIntrusivePtr only.
+struct TAttributeDictionarySerializer
+{
+ static void Save(TStreamSaveContext& context, const IAttributeDictionaryPtr& attributes);
+ static void SaveNonNull(TStreamSaveContext& context, const IAttributeDictionaryPtr& attributes);
+ static void Load(TStreamLoadContext& context, IAttributeDictionaryPtr& attributes);
+ static void LoadNonNull(TStreamLoadContext& context, const IAttributeDictionaryPtr& attributes);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void ValidateYTreeKey(TStringBuf key);
+
+void ValidateYPathResolutionDepth(const NYPath::TYPath& path, int depth);
+
+//! Helps implementing IAttributeDictionary::ListPairs by delegating to
+//! IAttributeDictionary::ListKeys and IAttributeDictionary::FindYson for those not capable
+//! of providing a custom efficient implementation.
+std::vector<IAttributeDictionary::TKeyValuePair> ListAttributesPairs(const IAttributeDictionary& attributes);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class C>
+struct TSerializerTraits<NYTree::IAttributeDictionaryPtr, C, void>
+{
+ typedef NYTree::TAttributeDictionarySerializer TSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define HELPERS_INL_H_
+#include "helpers-inl.h"
+#undef HELPERS_INL_H_
diff --git a/yt/yt/core/ytree/interned_attributes-inl.h b/yt/yt/core/ytree/interned_attributes-inl.h
new file mode 100644
index 0000000000..7abb8eb665
--- /dev/null
+++ b/yt/yt/core/ytree/interned_attributes-inl.h
@@ -0,0 +1,18 @@
+#ifndef INTERNED_ATTRIBUTES_INL_H_
+#error "Direct inclusion of this file is not allowed, include interned_attributes.h"
+// For the sake of sane code completion.
+#include "interned_attributes.h"
+#endif
+
+namespace NYT::NYTree {
+
+///////////////////////////////////////////////////////////////////////////////
+
+constexpr TInternedAttributeKey::operator size_t() const
+{
+ return Code_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/interned_attributes.cpp b/yt/yt/core/ytree/interned_attributes.cpp
new file mode 100644
index 0000000000..1b3f39c5b2
--- /dev/null
+++ b/yt/yt/core/ytree/interned_attributes.cpp
@@ -0,0 +1,81 @@
+#include "interned_attributes.h"
+
+#include <yt/yt/core/misc/collection_helpers.h>
+#include <yt/yt/core/misc/serialize.h>
+
+namespace NYT::NYTree {
+
+///////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+class TInternedAttributeRegistry
+{
+public:
+ void Intern(const TString& uninternedKey, TInternedAttributeKey internedKey)
+ {
+ YT_VERIFY(AttributeNameToIndex_.emplace(uninternedKey, internedKey).second);
+ YT_VERIFY(AttributeIndexToName_.emplace(internedKey, uninternedKey).second);
+ }
+
+ TInternedAttributeKey GetInterned(TStringBuf uninternedKey)
+ {
+ auto it = AttributeNameToIndex_.find(uninternedKey);
+ return it == AttributeNameToIndex_.end() ? InvalidInternedAttribute : it->second;
+ }
+
+ const TString& GetUninterned(TInternedAttributeKey internedKey)
+ {
+ return GetOrCrash(AttributeIndexToName_, internedKey);
+ }
+
+private:
+ THashMap<TString, TInternedAttributeKey> AttributeNameToIndex_;
+ THashMap<TInternedAttributeKey, TString> AttributeIndexToName_;
+};
+
+} // namespace
+
+void InternAttribute(const TString& uninternedKey, TInternedAttributeKey internedKey)
+{
+ Singleton<TInternedAttributeRegistry>()->Intern(uninternedKey, internedKey);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TInternedAttributeKey::TInternedAttributeKey()
+ : Code_(InvalidInternedAttribute.Code_)
+{ }
+
+void TInternedAttributeKey::Save(TStreamSaveContext& context) const
+{
+ using NYT::Save;
+
+ Save(context, Unintern());
+}
+
+void TInternedAttributeKey::Load(TStreamLoadContext& context)
+{
+ using NYT::Load;
+
+ auto uninternedKey = Load<TString>(context);
+ Code_ = Lookup(uninternedKey).Code_;
+}
+
+/*static*/ TInternedAttributeKey TInternedAttributeKey::Lookup(TStringBuf uninternedKey)
+{
+ return Singleton<TInternedAttributeRegistry>()->GetInterned(uninternedKey);
+}
+
+const TString& TInternedAttributeKey::Unintern() const
+{
+ return Singleton<TInternedAttributeRegistry>()->GetUninterned(*this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+REGISTER_INTERNED_ATTRIBUTE(count, CountInternedAttribute)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/interned_attributes.h b/yt/yt/core/ytree/interned_attributes.h
new file mode 100644
index 0000000000..351dd3b48c
--- /dev/null
+++ b/yt/yt/core/ytree/interned_attributes.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/misc/preprocessor.h>
+
+namespace NYT::NYTree {
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TInternedAttributeKey
+{
+public:
+ TInternedAttributeKey();
+
+ explicit constexpr TInternedAttributeKey(size_t code)
+ : Code_(code)
+ { }
+
+ constexpr operator size_t() const;
+
+ // May return #InvalidInternedAttribute if the attribute is not interned.
+ static TInternedAttributeKey Lookup(TStringBuf uninternedKey);
+
+ const TString& Unintern() const;
+
+ void Save(TStreamSaveContext& context) const;
+ void Load(TStreamLoadContext& context);
+
+private:
+ // NB: this codes are subject to change! Do not rely on their values. Do not serialize them.
+ // Use Save/Load methods instead.
+ size_t Code_;
+};
+
+constexpr TInternedAttributeKey InvalidInternedAttribute{0};
+constexpr TInternedAttributeKey CountInternedAttribute{1};
+
+//! Interned attribute registry initialization. Should be called once per attribute.
+//! Both interned and uninterned keys must be unique.
+void InternAttribute(const TString& uninternedKey, TInternedAttributeKey internedKey);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define REGISTER_INTERNED_ATTRIBUTE(uninternedKey, internedKey) \
+ YT_ATTRIBUTE_USED const void* PP_ANONYMOUS_VARIABLE(RegisterInterndAttribute) = [] { \
+ ::NYT::NYTree::InternAttribute(#uninternedKey, internedKey); \
+ return nullptr; \
+ } ();
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define INTERNED_ATTRIBUTES_INL_H_
+#include "interned_attributes-inl.h"
+#undef INTERNED_ATTRIBUTES_INL_H_
diff --git a/yt/yt/core/ytree/node-inl.h b/yt/yt/core/ytree/node-inl.h
new file mode 100644
index 0000000000..e62889f41e
--- /dev/null
+++ b/yt/yt/core/ytree/node-inl.h
@@ -0,0 +1,69 @@
+#ifndef NODE_INL_H_
+#error "Direct inclusion of this file is not allowed, include node.h"
+// For the sake of sane code completion.
+#include "node.h"
+#endif
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Forward declaration.
+template <class TTo>
+TTo ConvertTo(const INodePtr& node);
+template <class TTo, class TFrom>
+TTo ConvertTo(const TFrom& value);
+
+template <class T>
+T INode::GetValue() const
+{
+ return ConvertTo<T>(const_cast<INode*>(this));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T IMapNode::GetChildValueOrThrow(const TString& key) const
+{
+ return GetChildOrThrow(key)->GetValue<T>();
+}
+
+template <class T>
+T IMapNode::GetChildValueOrDefault(const TString& key, const T& defaultValue) const
+{
+ auto child = FindChild(key);
+ return child ? child->GetValue<T>() : defaultValue;
+}
+
+template <class T>
+std::optional<T> IMapNode::FindChildValue(const TString& key) const
+{
+ auto child = FindChild(key);
+ return child ? std::make_optional(child->GetValue<T>()) : std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+T IListNode::GetChildValueOrThrow(int index) const
+{
+ return GetChildOrThrow(index)->GetValue<T>();
+}
+
+template <class T>
+T IListNode::GetChildValueOrDefault(int index, const T& defaultValue) const
+{
+ auto child = FindChild(index);
+ return child ? child->GetValue<T>() : defaultValue;
+}
+
+template <class T>
+std::optional<T> IListNode::FindChildValue(int index) const
+{
+ auto child = FindChild(index);
+ return child ? std::make_optional(child->GetValue<T>()) : std::nullopt;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/node.cpp b/yt/yt/core/ytree/node.cpp
new file mode 100644
index 0000000000..dbfcb31ac9
--- /dev/null
+++ b/yt/yt/core/ytree/node.cpp
@@ -0,0 +1,110 @@
+#include "node.h"
+#include "convert.h"
+#include "node_detail.h"
+#include "tree_visitor.h"
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+INodePtr IMapNode::GetChildOrThrow(const TString& key) const
+{
+ auto child = FindChild(key);
+ if (!child) {
+ ThrowNoSuchChildKey(this, key);
+ }
+ return child;
+}
+
+TString IMapNode::GetChildKeyOrThrow(const IConstNodePtr& child)
+{
+ auto optionalKey = FindChildKey(child);
+ if (!optionalKey) {
+ THROW_ERROR_EXCEPTION("Node is not a child");
+ }
+ return *optionalKey;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+INodePtr IListNode::GetChildOrThrow(int index) const
+{
+ auto child = FindChild(index);
+ if (!child) {
+ ThrowNoSuchChildIndex(this, index);
+ }
+ return child;
+}
+
+int IListNode::GetChildIndexOrThrow(const IConstNodePtr& child)
+{
+ auto optionalIndex = FindChildIndex(child);
+ if (!optionalIndex) {
+ THROW_ERROR_EXCEPTION("Node is not a child");
+ }
+ return *optionalIndex;
+}
+
+int IListNode::AdjustChildIndexOrThrow(int index) const
+{
+ auto adjustedIndex = NYPath::TryAdjustListIndex(index, GetChildCount());
+ if (!adjustedIndex) {
+ ThrowNoSuchChildIndex(this, index);
+ }
+ return *adjustedIndex;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const INode& value, IYsonConsumer* consumer)
+{
+ VisitTree(const_cast<INode*>(&value), consumer, /*stable*/ true, TAttributeFilter());
+}
+
+void Deserialize(INodePtr& value, const INodePtr& node)
+{
+ value = node;
+}
+
+#define DESERIALIZE_TYPED(type) \
+ void Deserialize(I##type##NodePtr& value, const INodePtr& node) \
+ { \
+ value = node->As##type(); \
+ } \
+ \
+ void Deserialize(I##type##NodePtr& value, NYson::TYsonPullParserCursor* cursor) \
+ { \
+ auto node = ExtractTo<INodePtr>(cursor); \
+ value = node->As##type(); \
+ }
+
+DESERIALIZE_TYPED(String)
+DESERIALIZE_TYPED(Int64)
+DESERIALIZE_TYPED(Uint64)
+DESERIALIZE_TYPED(Double)
+DESERIALIZE_TYPED(Boolean)
+DESERIALIZE_TYPED(Map)
+DESERIALIZE_TYPED(List)
+DESERIALIZE_TYPED(Entity)
+
+#undef DESERIALIZE_TYPED
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Deserialize(INodePtr& value, NYson::TYsonPullParserCursor* cursor)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ cursor->TransferComplexValue(builder.get());
+ value = builder->EndTree();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/node.h b/yt/yt/core/ytree/node.h
new file mode 100644
index 0000000000..178131586a
--- /dev/null
+++ b/yt/yt/core/ytree/node.h
@@ -0,0 +1,417 @@
+#pragma once
+
+#include "public.h"
+#include "attribute_owner.h"
+#include "ypath_service.h"
+
+#include <yt/yt/core/misc/mpl.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base DOM-like interface representing a node.
+struct INode
+ : public virtual IYPathService
+ , public virtual IAttributeOwner
+{
+ //! Returns the static type of the node.
+ virtual ENodeType GetType() const = 0;
+
+ //! Returns a new instance of transactional factory for creating new nodes.
+ /*!
+ * Every YTree implementation provides its own set of
+ * node implementations. E.g., for an ephemeral implementation
+ * this factory creates ephemeral nodes while for
+ * a persistent implementation (see Cypress) this factory
+ * creates persistent nodes.
+ */
+ virtual std::unique_ptr<ITransactionalNodeFactory> CreateFactory() const = 0;
+
+ //! Returns a YPath for this node.
+ virtual TYPath GetPath() const = 0;
+
+ // A bunch of "AsSomething" methods that return a pointer
+ // to the same node but typed as "Something".
+ // These methods throw an exception on type mismatch.
+#define DECLARE_AS_METHODS(name) \
+ virtual TIntrusivePtr<I##name##Node> As##name() = 0; \
+ virtual TIntrusivePtr<const I##name##Node> As##name() const = 0;
+
+ DECLARE_AS_METHODS(Entity)
+ DECLARE_AS_METHODS(Composite)
+ DECLARE_AS_METHODS(String)
+ DECLARE_AS_METHODS(Int64)
+ DECLARE_AS_METHODS(Uint64)
+ DECLARE_AS_METHODS(Double)
+ DECLARE_AS_METHODS(Boolean)
+ DECLARE_AS_METHODS(List)
+ DECLARE_AS_METHODS(Map)
+#undef DECLARE_AS_METHODS
+
+ //! Returns the parent of the node.
+ //! |nullptr| indicates that the current node is the root.
+ virtual ICompositeNodePtr GetParent() const = 0;
+
+ //! Sets the parent of the node.
+ /*!
+ * This method is called automatically when one subtree (possibly)
+ * consisting of a single node is attached to another.
+ *
+ * This method must not be called explicitly.
+ */
+ virtual void SetParent(const ICompositeNodePtr& parent) = 0;
+
+ // Extension methods.
+
+ //! Converts node value to a given type.
+ template <class T>
+ T GetValue() const;
+};
+
+DEFINE_REFCOUNTED_TYPE(INode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base interface for all scalar nodes, i.e. nodes containing a single atomic value.
+template <class T>
+struct IScalarNode
+ : public virtual INode
+{
+ typedef T TValue;
+
+ //! Gets the value.
+ virtual typename NMpl::TCallTraits<TValue>::TType GetValue() const = 0;
+
+ //! Sets the value.
+ virtual void SetValue(typename NMpl::TCallTraits<TValue>::TType value) = 0;
+};
+
+#define ITERATE_SCALAR_YTREE_NODE_TYPES(XX) \
+ XX(Int64, i64) \
+ XX(Uint64, ui64) \
+ XX(Double, double) \
+ XX(Boolean, bool) \
+ XX(String, TString)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! String node.
+struct IStringNode
+ : public IScalarNode<TString>
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IStringNode)
+
+//! Int64 node.
+struct IInt64Node
+ : public IScalarNode<i64>
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IInt64Node)
+
+//! Uint64 node.
+struct IUint64Node
+ : public IScalarNode<ui64>
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IUint64Node)
+
+//! Double node.
+struct IDoubleNode
+ : public IScalarNode<double>
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IDoubleNode)
+
+//! Boolean node.
+struct IBooleanNode
+ : public IScalarNode<bool>
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IBooleanNode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A base interface for all composite nodes, i.e. nodes containing other nodes.
+struct ICompositeNode
+ : public virtual INode
+{
+ //! Removes all child nodes.
+ virtual void Clear() = 0;
+
+ //! Returns the number of child nodes.
+ virtual int GetChildCount() const = 0;
+
+ //! Replaces one child by the other.
+ //! #newChild must be a root.
+ virtual void ReplaceChild(const INodePtr& oldChild, const INodePtr& newChild) = 0;
+
+ //! Removes a child.
+ //! The removed child becomes a root.
+ virtual void RemoveChild(const INodePtr& child) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ICompositeNode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A map node, which keeps a dictionary mapping strings (TString) to child nodes.
+struct IMapNode
+ : public virtual ICompositeNode
+{
+ using ICompositeNode::RemoveChild;
+
+ //! Returns the current snapshot of the map.
+ /*!
+ * Map items are returned in unspecified order.
+ */
+ virtual std::vector<std::pair<TString, INodePtr>> GetChildren() const = 0;
+
+ //! Returns map keys.
+ /*!
+ * Keys are returned in unspecified order.
+ */
+ virtual std::vector<TString> GetKeys() const = 0;
+
+ //! Gets a child by its key.
+ /*!
+ * \param key A key.
+ * \return A child with the given #key or null if no child with the given #key exists.
+ */
+ virtual INodePtr FindChild(const TString& key) const = 0;
+
+ //! Adds a new child with a given key.
+ /*!
+ * \param child A child.
+ * \param key A key.
+ * \return True iff the key was not in the map already and thus the child is inserted.
+ *
+ * \note
+ * #child must be a root.
+ */
+ virtual bool AddChild(const TString& key, const INodePtr& child) = 0;
+
+ //! Removes a child by its key.
+ /*!
+ * \param key A key.
+ * \return True iff there was a child with the given key.
+ */
+ virtual bool RemoveChild(const TString& key) = 0;
+
+ //! Similar to #FindChild but throws if no child is found.
+ INodePtr GetChildOrThrow(const TString& key) const;
+
+ //! Returns the key for a given child.
+ /*!
+ * \param child A possible child.
+ * \return Child's key or null if the node is not a child.
+ */
+ virtual std::optional<TString> FindChildKey(const IConstNodePtr& child) = 0;
+
+ //! Returns the key for a given child or throws if the node is not a child.
+ /*!
+ * \param child A possible child.
+ * \return Child's key.
+ */
+ TString GetChildKeyOrThrow(const IConstNodePtr& child);
+
+ // Extension methods.
+
+ //! Converts the value of the child with #key to a given type.
+ //! Throws if no child with #key exists.
+ template <class T>
+ T GetChildValueOrThrow(const TString& key) const;
+
+ //! Converts the value of the child with #key to a given type.
+ //! Returns #defaultValue if no child with #key exists.
+ template <class T>
+ T GetChildValueOrDefault(const TString& key, const T& defaultValue) const;
+
+ //! Converts the value of the child with #key to a given type.
+ //! Returns null if no child with #key exists.
+ template <class T>
+ std::optional<T> FindChildValue(const TString& key) const;
+};
+
+DEFINE_REFCOUNTED_TYPE(IMapNode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A list node, which keeps a list (vector) of children.
+struct IListNode
+ : public virtual ICompositeNode
+{
+ using ICompositeNode::RemoveChild;
+
+ //! Returns the current snapshot of the list.
+ virtual std::vector<INodePtr> GetChildren() const = 0;
+
+ //! Gets a child by its index.
+ /*!
+ * \param index An index.
+ * \return A child with the given index or NULL if the index is not valid.
+ */
+ virtual INodePtr FindChild(int index) const = 0;
+
+ //! Adds a new child at a given position.
+ /*!
+ * \param child A child.
+ * \param beforeIndex A position before which the insertion must happen.
+ * -1 indicates the end of the list.
+ *
+ * \note
+ * #child must be a root.
+ */
+
+ virtual void AddChild(const INodePtr& child, int beforeIndex = -1) = 0;
+
+ //! Removes a child by its index.
+ /*!
+ * \param index An index.
+ * \return True iff the index is valid and thus the child is removed.
+ */
+ virtual bool RemoveChild(int index) = 0;
+
+ //! Similar to #FindChild but throws if the index is not valid.
+ INodePtr GetChildOrThrow(int index) const;
+
+ //! Returns the index for a given child or null if the node is not a child.
+ /*!
+ * \param child A node that must be a child.
+ * \return Child's index or null if the node is not a child.
+ */
+ virtual std::optional<int> FindChildIndex(const IConstNodePtr& child) = 0;
+
+ //! Returns the index for a given child or throws if the node is not a child.
+ /*!
+ * \param child A node that must be a child.
+ * \return Child's index.
+ */
+ int GetChildIndexOrThrow(const IConstNodePtr& child);
+
+ //! Normalizes negative indexes (by adding child count).
+ //! Throws if the index is invalid.
+ /*!
+ * \param index Original (possibly negative) index.
+ * \returns Adjusted (valid non-negative) index.
+ */
+ int AdjustChildIndexOrThrow(int index) const;
+
+ // Extension methods.
+
+ //! Converts the value of the child with #index to a given type.
+ //! Throws if no child with #key exists.
+ template <class T>
+ T GetChildValueOrThrow(int index) const;
+
+ //! Converts the value of the child with #index to a given type.
+ //! Returns #defaultValue if no child with #key exists.
+ template <class T>
+ T GetChildValueOrDefault(int index, const T& defaultValue) const;
+
+ //! Converts the value of the child with #index to a given type.
+ //! Returns null if no child with #key exists.
+ template <class T>
+ std::optional<T> FindChildValue(int index) const;
+};
+
+DEFINE_REFCOUNTED_TYPE(IListNode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! An structureless entity node.
+struct IEntityNode
+ : public virtual INode
+{ };
+
+DEFINE_REFCOUNTED_TYPE(IEntityNode)
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A factory for creating nodes.
+/*!
+ * All freshly created nodes are roots, i.e. have no parent.
+ */
+struct INodeFactory
+{
+ virtual ~INodeFactory() = default;
+
+ //! Creates a string node.
+ virtual IStringNodePtr CreateString() = 0;
+
+ //! Creates an int64 node.
+ virtual IInt64NodePtr CreateInt64() = 0;
+
+ //! Creates an uint64 node.
+ virtual IUint64NodePtr CreateUint64() = 0;
+
+ //! Creates an FP number node.
+ virtual IDoubleNodePtr CreateDouble() = 0;
+
+ //! Creates an boolean node.
+ virtual IBooleanNodePtr CreateBoolean() = 0;
+
+ //! Creates a map node.
+ virtual IMapNodePtr CreateMap() = 0;
+
+ //! Creates a list node.
+ virtual IListNodePtr CreateList() = 0;
+
+ //! Creates an entity node.
+ virtual IEntityNodePtr CreateEntity() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A node factory with extended transactional capabilities.
+/*!
+ * The factory also acts as a "transaction context" that holds all created nodes.
+ *
+ * One must call #Commit at the end if the operation was a success.
+ * Releasing the instance without calling #Commit or calling #Rollback abandons all changes.
+ */
+struct ITransactionalNodeFactory
+ : public INodeFactory
+{
+ //! Must be called before releasing the factory to indicate that all created nodes
+ //! must persist.
+ virtual void Commit() = 0;
+
+ //! Invokes all rollback handlers.
+ virtual void Rollback() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const INode& value, NYson::IYsonConsumer* consumer);
+void Deserialize(INodePtr& value, const INodePtr& node);
+void Deserialize(IStringNodePtr& value, const INodePtr& node);
+void Deserialize(IInt64NodePtr& value, const INodePtr& node);
+void Deserialize(IUint64NodePtr& value, const INodePtr& node);
+void Deserialize(IDoubleNodePtr& value, const INodePtr& node);
+void Deserialize(IBooleanNodePtr& value, const INodePtr& node);
+void Deserialize(IMapNodePtr& value, const INodePtr& node);
+void Deserialize(IListNodePtr& value, const INodePtr& node);
+void Deserialize(IEntityNodePtr& value, const INodePtr& node);
+
+void Deserialize(INodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IStringNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IInt64NodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IUint64NodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IDoubleNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IBooleanNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IMapNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IListNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+void Deserialize(IEntityNodePtr& value, NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define NODE_INL_H_
+#include "node-inl.h"
+#undef NODE_INL_H_
diff --git a/yt/yt/core/ytree/node_detail.cpp b/yt/yt/core/ytree/node_detail.cpp
new file mode 100644
index 0000000000..cf785433d1
--- /dev/null
+++ b/yt/yt/core/ytree/node_detail.cpp
@@ -0,0 +1,665 @@
+#include "node_detail.h"
+#include "tree_visitor.h"
+#include "exception_helpers.h"
+#include "attribute_filter.h"
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/yson/tokenizer.h>
+#include <yt/yt/core/yson/async_writer.h>
+
+namespace NYT::NYTree {
+
+using namespace NRpc;
+using namespace NYPath;
+using namespace NYson;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TNodeBase::DoInvoke(const IYPathServiceContextPtr& context)
+{
+ DISPATCH_YPATH_SERVICE_METHOD(GetKey);
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ DISPATCH_YPATH_SERVICE_METHOD(Set);
+ DISPATCH_YPATH_SERVICE_METHOD(Remove);
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ DISPATCH_YPATH_SERVICE_METHOD(Multiset);
+ DISPATCH_YPATH_SERVICE_METHOD(MultisetAttributes);
+ return TYPathServiceBase::DoInvoke(context);
+}
+
+void TNodeBase::GetSelf(
+ TReqGet* request,
+ TRspGet* response,
+ const TCtxGetPtr& context)
+{
+ auto attributeFilter = request->has_attributes()
+ ? FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ // TODO(babenko): make use of limit
+ auto limit = request->has_limit()
+ ? std::make_optional(request->limit())
+ : std::nullopt;
+
+ context->SetRequestInfo("Limit: %v, AttributeFilter: %v",
+ limit,
+ attributeFilter);
+
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ TAsyncYsonWriter writer;
+
+ VisitTree(
+ this,
+ &writer,
+ false,
+ attributeFilter);
+
+ writer.Finish()
+ .Subscribe(BIND([=] (const TErrorOr<TYsonString>& resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+}
+
+void TNodeBase::GetKeySelf(
+ TReqGetKey* /*request*/,
+ TRspGetKey* response,
+ const TCtxGetKeyPtr& context)
+{
+ context->SetRequestInfo();
+
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ auto parent = GetParent();
+ if (!parent) {
+ THROW_ERROR_EXCEPTION("Node has no parent");
+ }
+
+ TString key;
+ switch (parent->GetType()) {
+ case ENodeType::Map:
+ key = parent->AsMap()->GetChildKeyOrThrow(this);
+ break;
+
+ case ENodeType::List:
+ key = ToString(parent->AsList()->GetChildIndexOrThrow(this));
+ break;
+
+ default:
+ YT_ABORT();
+ }
+
+ context->SetResponseInfo("Key: %v", key);
+ response->set_value(ConvertToYsonString(key).ToString());
+
+ context->Reply();
+}
+
+void TNodeBase::RemoveSelf(
+ TReqRemove* request,
+ TRspRemove* /*response*/,
+ const TCtxRemovePtr& context)
+{
+ context->SetRequestInfo("Recursive: %v, Force: %v", request->recursive(), request->force());
+
+ ValidatePermission(
+ EPermissionCheckScope::This | EPermissionCheckScope::Descendants,
+ EPermission::Remove);
+ ValidatePermission(
+ EPermissionCheckScope::Parent,
+ EPermission::Write | EPermission::ModifyChildren);
+
+ bool isComposite = (GetType() == ENodeType::Map || GetType() == ENodeType::List);
+ if (!request->recursive() && isComposite && AsComposite()->GetChildCount() > 0) {
+ THROW_ERROR_EXCEPTION("Cannot remove non-empty composite node");
+ }
+
+ DoRemoveSelf(request->recursive(), request->force());
+
+ context->Reply();
+}
+
+void TNodeBase::DoRemoveSelf(bool /*recursive*/, bool /*force*/)
+{
+ auto parent = GetParent();
+ if (!parent) {
+ ThrowCannotRemoveNode(this);
+ }
+ parent->AsComposite()->RemoveChild(this);
+}
+
+IYPathService::TResolveResult TNodeBase::ResolveRecursive(
+ const NYPath::TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ if (context->GetMethod() == "Exists") {
+ return TResolveResultHere{path};
+ }
+
+ ThrowCannotHaveChildren(this);
+ YT_ABORT();
+}
+
+TYPath TNodeBase::GetPath() const
+{
+ TCompactVector<TString, 64> tokens;
+ IConstNodePtr current(this);
+ while (true) {
+ auto parent = current->GetParent();
+ if (!parent) {
+ break;
+ }
+ TString token;
+ switch (parent->GetType()) {
+ case ENodeType::List: {
+ auto index = parent->AsList()->GetChildIndexOrThrow(current);
+ token = ToYPathLiteral(index);
+ break;
+ }
+ case ENodeType::Map: {
+ auto key = parent->AsMap()->GetChildKeyOrThrow(current);
+ token = ToYPathLiteral(key);
+ break;
+ }
+ default:
+ YT_ABORT();
+ }
+ tokens.emplace_back(std::move(token));
+ current = parent;
+ }
+
+ TStringBuilder builder;
+ for (auto it = tokens.rbegin(); it != tokens.rend(); ++it) {
+ builder.AppendChar('/');
+ builder.AppendString(*it);
+ }
+ return builder.Flush();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TCompositeNodeMixin::SetRecursive(
+ const TYPath& path,
+ TReqSet* request,
+ TRspSet* /*response*/,
+ const TCtxSetPtr& context)
+{
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Write);
+
+ auto factory = CreateFactory();
+ auto child = ConvertToNode(TYsonString(request->value()), factory.get());
+ SetChild(factory.get(), "/" + path, child, request->recursive());
+ factory->Commit();
+
+ context->Reply();
+}
+
+void TCompositeNodeMixin::RemoveRecursive(
+ const TYPath& path,
+ TSupportsRemove::TReqRemove* request,
+ TSupportsRemove::TRspRemove* /*response*/,
+ const TSupportsRemove::TCtxRemovePtr& context)
+{
+ context->SetRequestInfo();
+
+ NYPath::TTokenizer tokenizer(path);
+ if (tokenizer.Advance() == NYPath::ETokenType::Asterisk) {
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Write | EPermission::ModifyChildren);
+ ValidatePermission(EPermissionCheckScope::Descendants, EPermission::Remove);
+ Clear();
+
+ context->Reply();
+ } else if (request->force()) {
+ // There is no child node under the given path, so there is nothing to remove.
+ context->Reply();
+ } else {
+ ThrowNoSuchChildKey(this, tokenizer.GetLiteralValue());
+ }
+}
+
+int TCompositeNodeMixin::GetMaxChildCount() const
+{
+ return std::numeric_limits<int>::max();
+}
+
+void TCompositeNodeMixin::ValidateChildCount(const TYPath& path, int childCount) const
+{
+ int maxChildCount = GetMaxChildCount();
+ if (childCount >= maxChildCount) {
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::MaxChildCountViolation,
+ "Composite node %v is not allowed to contain more than %v items",
+ path,
+ maxChildCount);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathService::TResolveResult TMapNodeMixin::ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ const auto& method = context->GetMethod();
+
+ NYPath::TTokenizer tokenizer(path);
+ switch (tokenizer.Advance()) {
+ case NYPath::ETokenType::Asterisk: {
+ if (method != "Remove") {
+ THROW_ERROR_EXCEPTION("\"*\" is only allowed for Remove method");
+ }
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ return IYPathService::TResolveResultHere{"/" + path};
+ }
+
+ case NYPath::ETokenType::Literal: {
+ auto key = tokenizer.GetLiteralValue();
+ if (key.empty()) {
+ THROW_ERROR_EXCEPTION("Child key cannot be empty");
+ }
+
+ auto suffix = TYPath(tokenizer.GetSuffix());
+
+ auto child = FindChild(key);
+ if (!child) {
+ if (method == "Exists" ||
+ method == "Remove" ||
+ method == "Set" ||
+ method == "Create" ||
+ method == "Copy" ||
+ method == "EndCopy")
+ {
+ return IYPathService::TResolveResultHere{"/" + path};
+ } else {
+ ThrowNoSuchChildKey(this, key);
+ }
+ }
+
+ return IYPathService::TResolveResultThere{std::move(child), std::move(suffix)};
+ }
+
+ default:
+ tokenizer.ThrowUnexpected();
+ YT_ABORT();
+ }
+}
+
+void TMapNodeMixin::ListSelf(
+ TReqList* request,
+ TRspList* response,
+ const TCtxListPtr& context)
+{
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ auto attributeFilter = request->has_attributes()
+ ? FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ auto limit = request->has_limit()
+ ? std::make_optional(request->limit())
+ : std::nullopt;
+
+ context->SetRequestInfo("Limit: %v, AttributeFilter: %v",
+ limit,
+ attributeFilter);
+
+ TAsyncYsonWriter writer;
+
+ auto children = GetChildren();
+ if (limit && std::ssize(children) > *limit) {
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("incomplete");
+ writer.OnBooleanScalar(true);
+ writer.OnEndAttributes();
+ }
+
+ i64 counter = 0;
+
+ writer.OnBeginList();
+ for (const auto& [key, child] : children) {
+ writer.OnListItem();
+ child->WriteAttributes(&writer, attributeFilter, false);
+ writer.OnStringScalar(key);
+ if (limit && ++counter >= *limit) {
+ break;
+ }
+ }
+ writer.OnEndList();
+
+ writer.Finish()
+ .Subscribe(BIND([=] (const TErrorOr<TYsonString>& resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+}
+
+std::pair<TString, INodePtr> TMapNodeMixin::PrepareSetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive)
+{
+ YT_VERIFY(factory || !recursive);
+
+ NYPath::TTokenizer tokenizer(path);
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ tokenizer.ThrowUnexpected();
+ }
+
+ IMapNodePtr rootNode = AsMap();
+ INodePtr rootChild;
+ TString rootKey;
+
+ auto currentNode = rootNode;
+ try {
+ while (tokenizer.GetType() != NYPath::ETokenType::EndOfStream) {
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+
+ int maxKeyLength = GetMaxKeyLength();
+ if (std::ssize(key) > maxKeyLength) {
+ ThrowMaxKeyLengthViolated();
+ }
+
+ tokenizer.Advance();
+
+ bool lastStep = (tokenizer.GetType() == NYPath::ETokenType::EndOfStream);
+ if (!recursive && !lastStep) {
+ ThrowNoSuchChildKey(currentNode, key);
+ }
+
+ ValidateChildCount(GetPath(), currentNode->GetChildCount());
+
+ auto newChild = lastStep ? child : factory->CreateMap();
+ if (currentNode != rootNode) {
+ YT_VERIFY(currentNode->AddChild(key, newChild));
+ } else {
+ rootChild = newChild;
+ rootKey = key;
+ }
+
+ if (!lastStep) {
+ currentNode = newChild->AsMap();
+ }
+ }
+ } catch (const std::exception& ex) {
+ if (recursive) {
+ THROW_ERROR_EXCEPTION("Failed to set node recursively")
+ << ex;
+ } else {
+ throw;
+ }
+ }
+
+ YT_VERIFY(rootKey);
+ return {rootKey, rootChild};
+}
+
+void TMapNodeMixin::SetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive)
+{
+ const auto& [rootKey, rootChild] = PrepareSetChild(factory, path, child, recursive);
+ AddChild(rootKey, rootChild);
+}
+
+int TMapNodeMixin::GetMaxKeyLength() const
+{
+ return std::numeric_limits<int>::max();
+}
+
+void TMapNodeMixin::ThrowMaxKeyLengthViolated() const
+{
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::MaxKeyLengthViolation,
+ "Map node %v is not allowed to contain items with keys longer than %v symbols",
+ GetPath(),
+ GetMaxKeyLength());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathService::TResolveResult TListNodeMixin::ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+ switch (tokenizer.Advance()) {
+ case NYPath::ETokenType::Asterisk: {
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ return IYPathService::TResolveResultHere{"/" + path};
+ }
+
+ case NYPath::ETokenType::Literal: {
+ const auto& token = tokenizer.GetToken();
+ if (token == ListBeginToken ||
+ token == ListEndToken)
+ {
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ return IYPathService::TResolveResultHere{"/" + path};
+ } else if (token.StartsWith(ListBeforeToken) ||
+ token.StartsWith(ListAfterToken))
+ {
+ auto indexToken = ExtractListIndex(token);
+ int index = ParseListIndex(indexToken);
+ AdjustChildIndexOrThrow(index);
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ return IYPathService::TResolveResultHere{"/" + path};
+ } else {
+ int index = ParseListIndex(token);
+ INodePtr child;
+ auto adjustedIndex = NYPath::TryAdjustListIndex(index, GetChildCount());
+ if (adjustedIndex) {
+ child = FindChild(*adjustedIndex);
+ }
+
+ if (!child) {
+ const auto& method = context->GetMethod();
+ if (method == "Exists") {
+ return IYPathService::TResolveResultHere{"/" + path};
+ } else {
+ ThrowNoSuchChildIndex(this, adjustedIndex.value_or(index));
+ }
+ }
+
+ return IYPathService::TResolveResultThere{std::move(child), TYPath(tokenizer.GetSuffix())};
+ }
+ }
+
+ default:
+ tokenizer.ThrowUnexpected();
+ YT_ABORT();
+ }
+}
+
+void TListNodeMixin::SetChild(
+ INodeFactory* /*factory*/,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive)
+{
+ if (recursive) {
+ THROW_ERROR_EXCEPTION("List node %v does not support \"recursive\" option",
+ GetPath());
+ }
+
+ int beforeIndex = -1;
+
+ NYPath::TTokenizer tokenizer(path);
+
+ tokenizer.Advance();
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+
+ const auto& token = tokenizer.GetToken();
+
+ if (token.StartsWith(ListBeginToken)) {
+ beforeIndex = 0;
+ } else if (token.StartsWith(ListEndToken)) {
+ beforeIndex = GetChildCount();
+ } else if (token.StartsWith(ListBeforeToken) || token.StartsWith(ListAfterToken)) {
+ auto indexToken = ExtractListIndex(token);
+ int index = ParseListIndex(indexToken);
+ beforeIndex = AdjustChildIndexOrThrow(index);
+ if (token.StartsWith(ListAfterToken)) {
+ ++beforeIndex;
+ }
+ } else {
+ tokenizer.ThrowUnexpected();
+ }
+
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::EndOfStream);
+
+ ValidateChildCount(GetPath(), GetChildCount());
+
+ AddChild(child, beforeIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSupportsSetSelfMixin::SetSelf(
+ TReqSet* request,
+ TRspSet* /*response*/,
+ const TCtxSetPtr &context)
+{
+ bool force = request->force();
+ context->SetRequestInfo("Force: %v", force);
+
+ ValidateSetSelf(force);
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Write);
+ ValidatePermission(EPermissionCheckScope::Descendants, EPermission::Remove);
+
+ auto factory = CreateFactory();
+ auto builder = CreateBuilderFromFactory(factory.get());
+ SetNodeFromProducer(
+ this,
+ ConvertToProducer(TYsonString(request->value())),
+ builder.get());
+ factory->Commit();
+
+ context->Reply();
+}
+
+void TSupportsSetSelfMixin::ValidateSetSelf(bool /*force*/) const
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathServicePtr TNonexistingService::Get()
+{
+ return LeakyRefCountedSingleton<TNonexistingService>();
+}
+
+bool TNonexistingService::DoInvoke(const IYPathServiceContextPtr& context)
+{
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ return TYPathServiceBase::DoInvoke(context);
+}
+
+IYPathService::TResolveResult TNonexistingService::Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ return TResolveResultHere{path};
+}
+
+void TNonexistingService::ExistsSelf(
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ ExistsAny(context);
+}
+
+void TNonexistingService::ExistsRecursive(
+ const TYPath& /*path*/,
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ ExistsAny(context);
+}
+
+void TNonexistingService::ExistsAttribute(
+ const TYPath& /*path*/,
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ ExistsAny(context);
+}
+
+void TNonexistingService::ExistsAny(const TCtxExistsPtr& context)
+{
+ context->SetRequestInfo();
+ Reply(context, false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTransactionalNodeFactoryBase::~TTransactionalNodeFactoryBase()
+{
+ YT_VERIFY(State_ == EState::Committed || State_ == EState::RolledBack);
+}
+
+void TTransactionalNodeFactoryBase::Commit() noexcept
+{
+ YT_VERIFY(State_ == EState::Active);
+ State_ = EState::Committed;
+}
+
+void TTransactionalNodeFactoryBase::Rollback() noexcept
+{
+ YT_VERIFY(State_ == EState::Active);
+ State_ = EState::RolledBack;
+}
+
+void TTransactionalNodeFactoryBase::RollbackIfNeeded()
+{
+ if (State_ == EState::Active) {
+ Rollback();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/node_detail.h b/yt/yt/core/ytree/node_detail.h
new file mode 100644
index 0000000000..bf767ff4f2
--- /dev/null
+++ b/yt/yt/core/ytree/node_detail.h
@@ -0,0 +1,245 @@
+#pragma once
+
+#include "convert.h"
+#include "exception_helpers.h"
+#include "node.h"
+#include "permission.h"
+#include "tree_builder.h"
+#include "ypath_detail.h"
+#include "ypath_service.h"
+
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNodeBase
+ : public virtual TYPathServiceBase
+ , public virtual TSupportsGetKey
+ , public virtual TSupportsGet
+ , public virtual TSupportsSet
+ , public virtual TSupportsMultisetAttributes
+ , public virtual TSupportsRemove
+ , public virtual TSupportsList
+ , public virtual TSupportsExists
+ , public virtual TSupportsPermissions
+ , public virtual INode
+{
+public:
+#define IMPLEMENT_AS_METHODS(type) \
+ TIntrusivePtr<I##type##Node> As##type() override \
+ { \
+ ThrowInvalidNodeType(this, ENodeType::type, GetType()); \
+ } \
+ \
+ TIntrusivePtr<const I##type##Node> As##type() const override \
+ { \
+ ThrowInvalidNodeType(this, ENodeType::type, GetType()); \
+ }
+
+ IMPLEMENT_AS_METHODS(Entity)
+ IMPLEMENT_AS_METHODS(Composite)
+ IMPLEMENT_AS_METHODS(String)
+ IMPLEMENT_AS_METHODS(Int64)
+ IMPLEMENT_AS_METHODS(Uint64)
+ IMPLEMENT_AS_METHODS(Double)
+ IMPLEMENT_AS_METHODS(Boolean)
+ IMPLEMENT_AS_METHODS(List)
+ IMPLEMENT_AS_METHODS(Map)
+#undef IMPLEMENT_AS_METHODS
+
+ TYPath GetPath() const override;
+
+protected:
+ bool DoInvoke(const IYPathServiceContextPtr& context) override;
+
+ TResolveResult ResolveRecursive(const NYPath::TYPath& path, const IYPathServiceContextPtr& context) override;
+ void GetKeySelf(TReqGetKey* request, TRspGetKey* response, const TCtxGetKeyPtr& context) override;
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override;
+ void RemoveSelf(TReqRemove* request, TRspRemove* response, const TCtxRemovePtr& context) override;
+ virtual void DoRemoveSelf(bool recursive, bool force);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompositeNodeMixin
+ : public virtual TYPathServiceBase
+ , public virtual TSupportsSet
+ , public virtual TSupportsMultisetAttributes
+ , public virtual TSupportsRemove
+ , public virtual TSupportsPermissions
+ , public virtual ICompositeNode
+{
+protected:
+ void RemoveRecursive(
+ const TYPath &path,
+ TReqRemove* request,
+ TRspRemove* response,
+ const TCtxRemovePtr& context) override;
+
+ void SetRecursive(
+ const TYPath& path,
+ TReqSet* request,
+ TRspSet* response,
+ const TCtxSetPtr& context) override;
+
+ virtual void SetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive) = 0;
+
+ virtual int GetMaxChildCount() const;
+
+ void ValidateChildCount(const TYPath& path, int childCount) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TMapNodeMixin
+ : public virtual TCompositeNodeMixin
+ , public virtual TSupportsList
+ , public virtual IMapNode
+{
+protected:
+ IYPathService::TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override;
+
+ void ListSelf(
+ TReqList* request,
+ TRspList* response,
+ const TCtxListPtr& context) override;
+
+ void SetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive) override;
+
+ std::pair<TString, INodePtr> PrepareSetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive);
+
+ virtual int GetMaxKeyLength() const;
+
+private:
+ void ThrowMaxKeyLengthViolated() const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TListNodeMixin
+ : public virtual TCompositeNodeMixin
+ , public virtual IListNode
+{
+protected:
+ IYPathService::TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override;
+
+ void SetChild(
+ INodeFactory* factory,
+ const TYPath& path,
+ INodePtr child,
+ bool recursive) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSupportsSetSelfMixin
+ : public virtual TNodeBase
+{
+protected:
+ void SetSelf(TReqSet *request, TRspSet* /*response*/, const TCtxSetPtr &context) override;
+
+ virtual void ValidateSetSelf(bool /*force*/) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNonexistingService
+ : public TYPathServiceBase
+ , public TSupportsExists
+{
+public:
+ static IYPathServicePtr Get();
+
+private:
+ bool DoInvoke(const IYPathServiceContextPtr& context) override;
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override;
+
+ void ExistsSelf(
+ TReqExists* request,
+ TRspExists* response,
+ const TCtxExistsPtr& context) override;
+
+ void ExistsRecursive(
+ const TYPath& path,
+ TReqExists* request,
+ TRspExists* response,
+ const TCtxExistsPtr& context) override;
+
+ void ExistsAttribute(
+ const TYPath& path,
+ TReqExists* request,
+ TRspExists* response,
+ const TCtxExistsPtr& context) override;
+
+ void ExistsAny(const TCtxExistsPtr& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define YTREE_NODE_TYPE_OVERRIDES(type) \
+public: \
+ ::NYT::NYTree::ENodeType GetType() const override \
+ { \
+ return ::NYT::NYTree::ENodeType::type; \
+ } \
+ \
+ TIntrusivePtr<const ::NYT::NYTree::I##type##Node> As##type() const override \
+ { \
+ return this; \
+ } \
+ \
+ TIntrusivePtr<::NYT::NYTree::I##type##Node> As##type() override \
+ { \
+ return this; \
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ENodeFactoryState,
+ (Active)
+ (Committed)
+ (RolledBack)
+);
+
+class TTransactionalNodeFactoryBase
+ : public virtual ITransactionalNodeFactory
+{
+public:
+ virtual ~TTransactionalNodeFactoryBase();
+
+ void Commit() noexcept override;
+ void Rollback() noexcept override;
+
+protected:
+ void RollbackIfNeeded();
+
+private:
+ using EState = ENodeFactoryState;
+ EState State_ = EState::Active;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/permission.cpp b/yt/yt/core/ytree/permission.cpp
new file mode 100644
index 0000000000..a3d9f755f1
--- /dev/null
+++ b/yt/yt/core/ytree/permission.cpp
@@ -0,0 +1,22 @@
+#include "permission.h"
+
+#include <yt/yt/core/misc/error.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TString> FormatPermissions(EPermissionSet permissions)
+{
+ std::vector<TString> result;
+ for (auto value : TEnumTraits<EPermission>::GetDomainValues()) {
+ if (Any(permissions & value)) {
+ result.push_back(FormatEnum(value));
+ }
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/permission.h b/yt/yt/core/ytree/permission.h
new file mode 100644
index 0000000000..ec769ee445
--- /dev/null
+++ b/yt/yt/core/ytree/permission.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Defines permissions for various YT objects.
+/*!
+ * Each permission corresponds to a unique bit of the mask.
+ */
+DEFINE_BIT_ENUM(EPermission,
+ //! Applies to: all objects
+ ((Read) (0x0001))
+
+ //! Applies to: tables; same as Read for all other objects
+ ((FullRead) (0x2000))
+
+ //! Applies to: all objects
+ ((Write) (0x0002))
+
+ //! Applies to: accounts
+ ((Use) (0x0004))
+
+ //! Applies to: all objects
+ ((Administer) (0x0008))
+
+ //! Applies to: schemas
+ ((Create) (0x0100))
+
+ //! Applies to: all objects
+ ((Remove) (0x0200))
+
+ //! Applies to: tables
+ ((Mount) (0x0400))
+
+ //! Applies to: operations
+ ((Manage) (0x0800))
+
+ //! Applies to: accounts, pools, composite Cypress nodes
+ ((ModifyChildren) (0x1000))
+
+ //! Applies to: queue-like objects
+ ((RegisterQueueConsumer) (0x0010))
+);
+
+//! An alias for EPermission denoting bitwise-or of atomic EPermission values.
+/*!
+ * No strong type safety is provided.
+ * Use wherever it suits to distinguish permission sets from individual atomic permissions.
+ */
+using EPermissionSet = EPermission;
+
+const EPermissionSet NonePermissions = EPermissionSet(0x0000);
+
+std::vector<TString> FormatPermissions(EPermissionSet permissions);
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Describes the set of objects for which permissions must be checked.
+DEFINE_BIT_ENUM(EPermissionCheckScope,
+ ((None) (0x0000))
+ ((This) (0x0001))
+ ((Parent) (0x0002))
+ ((Descendants) (0x0004))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/public.h b/yt/yt/core/ytree/public.h
new file mode 100644
index 0000000000..5f426e6b10
--- /dev/null
+++ b/yt/yt/core/ytree/public.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <yt/yt/core/misc/public.h>
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NProto {
+
+class TAttributeDictionary;
+class TAttributeFilter;
+
+} // namespace NProto
+
+struct TAttributeFilter;
+
+class TYsonSerializableLite;
+class TYsonSerializable;
+
+struct IYsonStructMeta;
+
+DECLARE_REFCOUNTED_STRUCT(INode)
+typedef TIntrusivePtr<const INode> IConstNodePtr;
+DECLARE_REFCOUNTED_STRUCT(ICompositeNode)
+typedef TIntrusivePtr<ICompositeNode> ICompositeNodePtr;
+DECLARE_REFCOUNTED_STRUCT(IStringNode)
+DECLARE_REFCOUNTED_STRUCT(IInt64Node)
+DECLARE_REFCOUNTED_STRUCT(IUint64Node)
+DECLARE_REFCOUNTED_STRUCT(IDoubleNode)
+DECLARE_REFCOUNTED_STRUCT(IBooleanNode)
+DECLARE_REFCOUNTED_STRUCT(IListNode)
+DECLARE_REFCOUNTED_STRUCT(IMapNode)
+DECLARE_REFCOUNTED_STRUCT(IEntityNode)
+
+struct INodeFactory;
+struct ITransactionalNodeFactory;
+
+DECLARE_REFCOUNTED_STRUCT(IAttributeDictionary)
+
+struct IAttributeOwner;
+
+struct ISystemAttributeProvider;
+
+DECLARE_REFCOUNTED_STRUCT(IYPathService)
+DECLARE_REFCOUNTED_STRUCT(IYPathServiceContext)
+DECLARE_REFCOUNTED_STRUCT(ICachedYPathService)
+DECLARE_REFCOUNTED_CLASS(TCompositeMapService)
+
+DECLARE_REFCOUNTED_CLASS(TYPathRequest)
+DECLARE_REFCOUNTED_CLASS(TYPathResponse)
+
+template <class TRequestMessage, class TResponseMessage>
+class TTypedYPathRequest;
+
+template <class TRequestMessage, class TResponseMessage>
+class TTypedYPathResponse;
+
+DECLARE_REFCOUNTED_CLASS(TServiceCombiner)
+
+using NYPath::TYPath;
+
+//! Default limit for List and Get requests to virtual nodes.
+constexpr i64 DefaultVirtualChildLimit = 1000;
+
+//! The global limit for the number of resolve iterations in #ResolveYPath.
+//! This effectively bounds the maximum depth of YPath the system can handle.
+//! Also this protects us from infinite cycles in resolution (which can be caused,
+//! e.g., by cyclic symlinks in Cypress).
+//! NB: Changing this value will invalidate all changelogs!
+constexpr int MaxYPathResolveIterations = 256;
+
+DECLARE_REFCOUNTED_CLASS(TYsonSerializable)
+DECLARE_REFCOUNTED_CLASS(TYsonStruct)
+DECLARE_REFCOUNTED_CLASS(TYsonStructBase)
+
+DECLARE_REFCOUNTED_CLASS(TYPathServiceContextWrapper)
+
+template <class TRequestMessage, class TResponseMessage>
+using TTypedYPathServiceContext = NRpc::TGenericTypedServiceContext<
+ IYPathServiceContext,
+ TYPathServiceContextWrapper,
+ TRequestMessage,
+ TResponseMessage
+>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A static node type.
+DEFINE_ENUM(ENodeType,
+ // Node contains a string (TString).
+ (String)
+ // Node contains an int64 number (i64).
+ (Int64)
+ // Node contains an uint64 number (ui64).
+ (Uint64)
+ // Node contains an FP number (double).
+ (Double)
+ // Node contains an boolean (bool).
+ (Boolean)
+ // Node contains a map from strings to other nodes.
+ (Map)
+ // Node contains a list (vector) of other nodes.
+ (List)
+ // Node is atomic, i.e. has no visible properties (aside from attributes).
+ (Entity)
+ // Either List or Map.
+ (Composite)
+);
+
+YT_DEFINE_ERROR_ENUM(
+ ((ResolveError) (500))
+ ((AlreadyExists) (501))
+ ((MaxChildCountViolation) (502))
+ ((MaxStringLengthViolation) (503))
+ ((MaxAttributeSizeViolation) (504))
+ ((MaxKeyLengthViolation) (505))
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReadRequestComplexity;
+
+DECLARE_REFCOUNTED_CLASS(TReadRequestComplexityLimiter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/request_complexity_limiter.cpp b/yt/yt/core/ytree/request_complexity_limiter.cpp
new file mode 100644
index 0000000000..c0e8724c03
--- /dev/null
+++ b/yt/yt/core/ytree/request_complexity_limiter.cpp
@@ -0,0 +1,261 @@
+#include "request_complexity_limiter.h"
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void VerifyNonNegative(const TReadRequestComplexity& complexity) noexcept
+{
+ YT_VERIFY(complexity.NodeCount.value_or(0) >= 0);
+ YT_VERIFY(complexity.ResultSize.value_or(0) >= 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+
+TReadRequestComplexity::TReadRequestComplexity(std::optional<i64> nodeCount, std::optional<i64> resultSize) noexcept
+ : NodeCount(std::move(nodeCount))
+ , ResultSize(std::move(resultSize))
+{
+ VerifyNonNegative(*this);
+}
+
+void TReadRequestComplexity::Sanitize(const TReadRequestComplexity& max) noexcept
+{
+ auto applyMax = [] (std::optional<i64> value, std::optional<i64> maxValue) {
+ return (value.has_value() && maxValue.has_value())
+ ? std::optional(std::min(*value, *maxValue))
+ : value;
+ };
+ NodeCount = applyMax(NodeCount, max.NodeCount);
+ ResultSize = applyMax(ResultSize, max.ResultSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadRequestComplexityUsage::TReadRequestComplexityUsage() noexcept
+ : TReadRequestComplexity(0, 0)
+{ }
+
+TReadRequestComplexityUsage::TReadRequestComplexityUsage(const TReadRequestComplexity& usage) noexcept
+ : TReadRequestComplexity(usage)
+{
+ VerifyNonNegative(usage);
+}
+
+TReadRequestComplexityUsage& TReadRequestComplexityUsage::operator+=(const TReadRequestComplexityUsage& that) noexcept
+{
+ auto add = [] (std::optional<i64> target, std::optional<i64> source) {
+ if (source.has_value()) {
+ target = target.value_or(0) + source.value();
+ }
+ return target;
+ };
+
+ NodeCount = add(NodeCount, that.NodeCount);
+ ResultSize = add(ResultSize, that.ResultSize);
+ return *this;
+}
+
+const TReadRequestComplexity& TReadRequestComplexityUsage::AsComplexity() const noexcept
+{
+ return static_cast<const TReadRequestComplexity&>(*this);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadRequestComplexityLimits::TReadRequestComplexityLimits(
+ const TReadRequestComplexity& limits) noexcept
+ : TReadRequestComplexity(limits)
+{
+ VerifyNonNegative(limits);
+}
+
+TError TReadRequestComplexityLimits::CheckOverdraught(
+ const TReadRequestComplexityUsage& usage) const noexcept
+{
+ TError error;
+
+ auto checkField = [&] (TStringBuf fieldName, std::optional<i64> limit, std::optional<i64> usage) {
+ if (limit.has_value() && usage.value_or(0) > *limit) {
+ error.SetCode(NYT::EErrorCode::Generic);
+ error = error << TErrorAttribute(Format("%v_usage", fieldName), *usage);
+ error = error << TErrorAttribute(Format("%v_limit", fieldName), *limit);
+ }
+ };
+
+ checkField("node_count", NodeCount, usage.NodeCount);
+ checkField("result_size", ResultSize, usage.ResultSize);
+
+ if (!error.IsOK()) {
+ error.SetMessage("Read complexity limit exceeded");
+ }
+
+ return error;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TReadRequestComplexityLimiter::TReadRequestComplexityLimiter() noexcept = default;
+
+void TReadRequestComplexityLimiter::Reconfigure(const TReadRequestComplexity& limits) noexcept
+{
+ Limits_ = TReadRequestComplexityLimits(limits);
+}
+
+void TReadRequestComplexityLimiter::Charge(const TReadRequestComplexityUsage& usage) noexcept
+{
+ Usage_ += usage;
+}
+
+TError TReadRequestComplexityLimiter::CheckOverdraught() const noexcept
+{
+ return Limits_.CheckOverdraught(Usage_);
+}
+
+void TReadRequestComplexityLimiter::ThrowIfOverdraught() const
+{
+ if (auto error = CheckOverdraught(); !error.IsOK()) {
+ THROW_ERROR error;
+ }
+}
+
+TReadRequestComplexity TReadRequestComplexityLimiter::GetUsage() const noexcept
+{
+ return Usage_.AsComplexity();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TLimitedAsyncYsonWriter::TLimitedAsyncYsonWriter(TReadRequestComplexityLimiterPtr complexityLimiter)
+ : ComplexityLimiter_(std::move(complexityLimiter))
+{ }
+
+template <bool CountNodes, class... Args>
+void DoOnSomething(
+ TWeakPtr<TReadRequestComplexityLimiter> weakLimiter,
+ TAsyncYsonWriter& writer,
+ void (TAsyncYsonWriter::*onSomething)(Args...),
+ Args... values)
+{
+ if (auto limiter = weakLimiter.Lock()) {
+ auto writtenBefore = writer.GetTotalWrittenSize();
+ (writer.*onSomething)(values...);
+ if (limiter) {
+ limiter->Charge(TReadRequestComplexityUsage({
+ /*nodeCount*/ CountNodes ? 1 : 0,
+ /*resultSize*/ writer.GetTotalWrittenSize() - writtenBefore,
+ }));
+ limiter->ThrowIfOverdraught();
+ }
+ }
+}
+
+void TLimitedAsyncYsonWriter::OnStringScalar(TStringBuf value)
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnStringScalar, value);
+}
+
+void TLimitedAsyncYsonWriter::OnInt64Scalar(i64 value)
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnInt64Scalar, value);
+}
+
+void TLimitedAsyncYsonWriter::OnUint64Scalar(ui64 value)
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnUint64Scalar, value);
+}
+
+void TLimitedAsyncYsonWriter::OnDoubleScalar(double value)
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnDoubleScalar, value);
+}
+
+void TLimitedAsyncYsonWriter::OnBooleanScalar(bool value)
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnBooleanScalar, value);
+}
+
+void TLimitedAsyncYsonWriter::OnEntity()
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnEntity);
+}
+
+void TLimitedAsyncYsonWriter::OnBeginList()
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnBeginList);
+}
+
+void TLimitedAsyncYsonWriter::OnListItem()
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnListItem);
+}
+
+void TLimitedAsyncYsonWriter::OnEndList()
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnEndList);
+}
+
+void TLimitedAsyncYsonWriter::OnBeginMap()
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnBeginMap);
+}
+
+void TLimitedAsyncYsonWriter::OnKeyedItem(TStringBuf value)
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnKeyedItem, value);
+}
+
+void TLimitedAsyncYsonWriter::OnEndMap()
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnEndMap);
+}
+
+void TLimitedAsyncYsonWriter::OnBeginAttributes()
+{
+ DoOnSomething<true>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnBeginAttributes);
+}
+
+void TLimitedAsyncYsonWriter::OnEndAttributes()
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_,&TAsyncYsonWriter::OnEndAttributes);
+}
+
+void TLimitedAsyncYsonWriter::OnRaw(TStringBuf yson, EYsonType type)
+{
+ DoOnSomething<false>(ComplexityLimiter_, UnderlyingWriter_, &TAsyncYsonWriter::OnRaw, std::move(yson), type);
+}
+
+void TLimitedAsyncYsonWriter::OnRaw(TFuture<TYsonString> asyncStr)
+{
+ ComplexityLimiter_->ThrowIfOverdraught();
+ asyncStr.Subscribe(BIND(
+ [weakLimiter = TWeakPtr(ComplexityLimiter_)] (const TErrorOr<TYsonString>& str) {
+ if (auto limiter = weakLimiter.Lock()) {
+ if (str.IsOK()) {
+ limiter->Charge(TReadRequestComplexityUsage({
+ /*nodeCount*/ 1,
+ /*resultSize*/ str.Value().AsStringBuf().size()
+ }));
+ }
+ }
+ }));
+ UnderlyingWriter_.OnRaw(std::move(asyncStr));
+}
+
+
+TFuture<TYsonString> TLimitedAsyncYsonWriter::Finish()
+{
+ return UnderlyingWriter_.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/request_complexity_limiter.h b/yt/yt/core/ytree/request_complexity_limiter.h
new file mode 100644
index 0000000000..ca89049e6f
--- /dev/null
+++ b/yt/yt/core/ytree/request_complexity_limiter.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include <yt/yt/core/yson/async_writer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TReadRequestComplexity
+{
+ std::optional<i64> NodeCount;
+ std::optional<i64> ResultSize;
+
+ TReadRequestComplexity() noexcept = default;
+ TReadRequestComplexity(std::optional<i64> nodeCount, std::optional<i64> resultSize) noexcept;
+ void Sanitize(const TReadRequestComplexity& max) noexcept;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadRequestComplexityUsage
+ : private TReadRequestComplexity
+{
+public:
+ friend class TReadRequestComplexityLimits;
+
+ TReadRequestComplexityUsage() noexcept;
+ explicit TReadRequestComplexityUsage(const TReadRequestComplexity& usage) noexcept;
+
+ TReadRequestComplexityUsage& operator+=(const TReadRequestComplexityUsage& that) noexcept;
+
+ const TReadRequestComplexity& AsComplexity() const noexcept;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadRequestComplexityLimits
+ : private TReadRequestComplexity
+{
+public:
+ TReadRequestComplexityLimits() noexcept = default;
+
+ // NB: Limits must be non-negative.
+ explicit TReadRequestComplexityLimits(const TReadRequestComplexity& limits) noexcept;
+
+ TError CheckOverdraught(const TReadRequestComplexityUsage& usage) const noexcept;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TReadRequestComplexityLimiter
+ : public TRefCounted
+ , public TNonCopyable
+{
+public:
+ TReadRequestComplexityLimiter() noexcept;
+
+ void Reconfigure(const TReadRequestComplexity& limits) noexcept;
+
+ void Charge(const TReadRequestComplexityUsage& usage) noexcept;
+
+ TError CheckOverdraught() const noexcept;
+ void ThrowIfOverdraught() const;
+
+ TReadRequestComplexity GetUsage() const noexcept;
+
+private:
+ TReadRequestComplexityUsage Usage_;
+ TReadRequestComplexityLimits Limits_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TReadRequestComplexityLimiter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLimitedAsyncYsonWriter
+ : public NYson::IAsyncYsonConsumer
+ , private TNonCopyable
+{
+public:
+ explicit TLimitedAsyncYsonWriter(TReadRequestComplexityLimiterPtr complexityLimiter);
+
+ void OnStringScalar(TStringBuf value) override;
+ void OnInt64Scalar(i64 value) override;
+ void OnUint64Scalar(ui64 value) override;
+ void OnDoubleScalar(double value) override;
+ void OnBooleanScalar(bool value) override;
+ void OnEntity() override;
+ void OnBeginList() override;
+ void OnListItem() override;
+ void OnEndList() override;
+ void OnBeginMap() override;
+ void OnKeyedItem(TStringBuf key) override;
+ void OnEndMap() override;
+ void OnBeginAttributes() override;
+ void OnEndAttributes() override;
+ void OnRaw(TStringBuf yson, NYson::EYsonType type) override;
+ void OnRaw(TFuture<NYson::TYsonString> asyncStr) override;
+
+ TFuture<NYson::TYsonString> Finish();
+
+protected:
+ NYson::TAsyncYsonWriter UnderlyingWriter_;
+ TReadRequestComplexityLimiterPtr const ComplexityLimiter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/serialize-inl.h b/yt/yt/core/ytree/serialize-inl.h
new file mode 100644
index 0000000000..78f31e7ff0
--- /dev/null
+++ b/yt/yt/core/ytree/serialize-inl.h
@@ -0,0 +1,643 @@
+#ifndef SERIALIZE_INL_H_
+#error "Direct inclusion of this file is not allowed, include serialize.h"
+// For the sake of sane code completion.
+#include "serialize.h"
+#endif
+
+#include "node.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/collection_helpers.h>
+
+#include <yt/yt/core/yson/stream.h>
+#include <yt/yt/core/yson/string.h>
+#include <yt/yt/core/yson/protobuf_interop.h>
+#include <yt/yt/core/yson/pull_parser_deserialize.h>
+
+#include <optional>
+#include <numeric>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+// all
+inline bool CanOmitValue(const void* /*parameter*/, const void* /*defaultValue*/)
+{
+ return false;
+}
+
+// TIntrusivePtr
+template <class T>
+bool CanOmitValue(const TIntrusivePtr<T>* parameter, const TIntrusivePtr<T>* defaultValue)
+{
+ if (!defaultValue) {
+ return !parameter->operator bool();
+ }
+ if (!*parameter && !*defaultValue) {
+ return true;
+ }
+ return false;
+}
+
+// std::optional
+template <class T>
+bool CanOmitValue(const std::optional<T>* parameter, const std::optional<T>* defaultValue)
+{
+ if (!defaultValue) {
+ return !*parameter;
+ }
+ if (!*parameter && !*defaultValue) {
+ return true;
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void SerializeVector(const T& items, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginList();
+ for (const auto& item : items) {
+ consumer->OnListItem();
+ Serialize(item, consumer);
+ }
+ consumer->OnEndList();
+}
+
+template <class T>
+void SerializeSet(const T& items, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginList();
+ for (auto it : GetSortedIterators(items)) {
+ consumer->OnListItem();
+ Serialize(*it, consumer);
+ }
+ consumer->OnEndList();
+}
+
+template <class T, bool IsEnum = TEnumTraits<T>::IsEnum>
+struct TMapKeyHelper;
+
+template <class T>
+struct TMapKeyHelper<T, true>
+{
+ static void Serialize(const T& value, NYson::IYsonConsumer* consumer)
+ {
+ consumer->OnKeyedItem(FormatEnum(value));
+ }
+
+ static void Deserialize(T& value, const TString& key)
+ {
+ value = ParseEnum<T>(key);
+ }
+};
+
+template <class T>
+struct TMapKeyHelper<T, false>
+{
+ static void Serialize(const T& value, NYson::IYsonConsumer* consumer)
+ {
+ consumer->OnKeyedItem(ToString(value));
+ }
+
+ static void Deserialize(T& value, const TString& key)
+ {
+ value = FromString<T>(key);
+ }
+};
+
+template <>
+struct TMapKeyHelper<TGuid, false>
+{
+ static void Serialize(const TGuid& value, NYson::IYsonConsumer* consumer)
+ {
+ consumer->OnKeyedItem(ToString(value));
+ }
+
+ static void Deserialize(TGuid& value, const TString& key)
+ {
+ value = TGuid::FromString(key);
+ }
+};
+
+
+template <class T>
+void SerializeMap(const T& items, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ for (auto it : GetSortedIterators(items)) {
+ TMapKeyHelper<typename T::key_type>::Serialize(it->first, consumer);
+ Serialize(it->second, consumer);
+ }
+ consumer->OnEndMap();
+}
+
+template <class T>
+void DeserializeVector(T& value, INodePtr node)
+{
+ auto listNode = node->AsList();
+ auto size = listNode->GetChildCount();
+ value.resize(size);
+ for (int i = 0; i < size; ++i) {
+ Deserialize(value[i], listNode->GetChildOrThrow(i));
+ }
+}
+
+template <class T>
+void DeserializeSet(T& value, INodePtr node)
+{
+ auto listNode = node->AsList();
+ auto size = listNode->GetChildCount();
+ value.clear();
+ for (int i = 0; i < size; ++i) {
+ typename T::value_type item;
+ Deserialize(item, listNode->GetChildOrThrow(i));
+ value.insert(std::move(item));
+ }
+}
+
+template <class T>
+void DeserializeMap(T& value, INodePtr node)
+{
+ auto mapNode = node->AsMap();
+ value.clear();
+ for (const auto& [serializedKey, serializedItem] : mapNode->GetChildren()) {
+ typename T::key_type key;
+ TMapKeyHelper<typename T::key_type>::Deserialize(key, serializedKey);
+ typename T::mapped_type item;
+ Deserialize(item, serializedItem);
+ value.emplace(std::move(key), std::move(item));
+ }
+}
+
+template <class T, bool IsSet = std::is_same<typename T::key_type, typename T::value_type>::value>
+struct TAssociativeHelper;
+
+template <class T>
+struct TAssociativeHelper<T, true>
+{
+ static void Serialize(const T& value, NYson::IYsonConsumer* consumer)
+ {
+ SerializeSet(value, consumer);
+ }
+
+ static void Deserialize(T& value, INodePtr consumer)
+ {
+ DeserializeSet(value, consumer);
+ }
+};
+
+template <class T>
+struct TAssociativeHelper<T, false>
+{
+ static void Serialize(const T& value, NYson::IYsonConsumer* consumer)
+ {
+ SerializeMap(value, consumer);
+ }
+
+ static void Deserialize(T& value, INodePtr consumer)
+ {
+ DeserializeMap(value, consumer);
+ }
+};
+
+template <class T>
+void SerializeAssociative(const T& items, NYson::IYsonConsumer* consumer)
+{
+ TAssociativeHelper<T>::Serialize(items, consumer);
+}
+
+template <class T>
+void DeserializeAssociative(T& value, INodePtr node)
+{
+ TAssociativeHelper<T>::Deserialize(value, node);
+}
+
+template <class T, size_t Size = std::tuple_size<T>::value>
+struct TTupleHelper;
+
+template <class T>
+struct TTupleHelper<T, 0U>
+{
+ static void SerializeItem(const T&, NYson::IYsonConsumer*) {}
+ static void DeserializeItem(T&, IListNodePtr) {}
+};
+
+template <class T, size_t Size>
+struct TTupleHelper
+{
+ static void SerializeItem(const T& value, NYson::IYsonConsumer* consumer)
+ {
+ TTupleHelper<T, Size - 1U>::SerializeItem(value, consumer);
+ consumer->OnListItem();
+ Serialize(std::get<Size - 1U>(value), consumer);
+ }
+
+ static void DeserializeItem(T& value, IListNodePtr list)
+ {
+ TTupleHelper<T, Size - 1U>::DeserializeItem(value, list);
+ if (list->GetChildCount() >= static_cast<int>(Size)) {
+ Deserialize(std::get<Size - 1U>(value), list->GetChildOrThrow(Size - 1U));
+ }
+ }
+};
+
+template <class T>
+void SerializeTuple(const T& items, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginList();
+ TTupleHelper<T>::SerializeItem(items, consumer);
+ consumer->OnEndList();
+}
+
+template <class T>
+void DeserializeTuple(T& value, INodePtr node)
+{
+ TTupleHelper<T>::DeserializeItem(value, node->AsList());
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+NYson::EYsonType GetYsonType(const T&)
+{
+ return NYson::EYsonType::Node;
+}
+
+template <class T>
+void WriteYson(
+ IZeroCopyOutput* output,
+ const T& value,
+ NYson::EYsonType type,
+ NYson::EYsonFormat format,
+ int indent)
+{
+ NYson::TYsonWriter writer(output, format, type, /* enableRaw */ false, indent);
+ Serialize(value, &writer);
+}
+
+template <class T>
+void WriteYson(
+ IZeroCopyOutput* output,
+ const T& value,
+ NYson::EYsonFormat format)
+{
+ WriteYson(output, value, GetYsonType(value), format);
+}
+
+template <class T>
+void WriteYson(
+ const NYson::TYsonOutput& output,
+ const T& value,
+ NYson::EYsonFormat format)
+{
+ WriteYson(output.GetStream(), value, output.GetType(), format);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void Serialize(T* value, NYson::IYsonConsumer* consumer)
+{
+ if (value) {
+ Serialize(*value, consumer);
+ } else {
+ consumer->OnEntity();
+ }
+}
+
+template <class T>
+void Serialize(const TIntrusivePtr<T>& value, NYson::IYsonConsumer* consumer)
+{
+ Serialize(value.Get(), consumer);
+}
+
+// Enums
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void Serialize(T value, NYson::IYsonConsumer* consumer)
+{
+ if constexpr (TEnumTraits<T>::IsBitEnum) {
+ consumer->OnBeginList();
+ for (auto scalarValue : TEnumTraits<T>::GetDomainValues()) {
+ if (Any(value & scalarValue)) {
+ consumer->OnListItem();
+ consumer->OnStringScalar(FormatEnum(scalarValue));
+ }
+ }
+ consumer->OnEndList();
+ } else {
+ consumer->OnStringScalar(FormatEnum(value));
+ }
+}
+
+// std::optional
+template <class T>
+void Serialize(const std::optional<T>& value, NYson::IYsonConsumer* consumer)
+{
+ if (value) {
+ Serialize(*value, consumer);
+ } else {
+ consumer->OnEntity();
+ }
+}
+
+// std::vector
+template <class T, class A>
+void Serialize(const std::vector<T, A>& items, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeVector(items, consumer);
+}
+
+// std::deque
+template <class T, class A>
+void Serialize(const std::deque<T, A>& items, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeVector(items, consumer);
+}
+
+// TCompactVector
+template <class T, size_t N>
+void Serialize(const TCompactVector<T, N>& items, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeVector(items, consumer);
+}
+
+// RepeatedPtrField
+template <class T>
+void Serialize(const NProtoBuf::RepeatedPtrField<T>& items, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeVector(items, consumer);
+}
+
+// RepeatedField
+template <class T>
+void Serialize(const NProtoBuf::RepeatedField<T>& items, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeVector(items, consumer);
+}
+
+// TErrorOr
+template <class T>
+void Serialize(const TErrorOr<T>& error, NYson::IYsonConsumer* consumer)
+{
+ const TError& justError = error;
+ if (error.IsOK()) {
+ std::function<void(NYson::IYsonConsumer*)> valueProducer = [&error] (NYson::IYsonConsumer* consumer) {
+ Serialize(error.Value(), consumer);
+ };
+ Serialize(justError, consumer, &valueProducer);
+ } else {
+ Serialize(justError, consumer);
+ }
+}
+
+template <class F, class S>
+void Serialize(const std::pair<F, S>& value, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeTuple(value, consumer);
+}
+
+template <class T, size_t N>
+void Serialize(const std::array<T, N>& value, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeTuple(value, consumer);
+}
+
+template <class... T>
+void Serialize(const std::tuple<T...>& value, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeTuple(value, consumer);
+}
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K>
+void Serialize(const C<T...>& value, NYson::IYsonConsumer* consumer)
+{
+ NDetail::SerializeAssociative(value, consumer);
+}
+
+template <class E, class T, E Min, E Max>
+void Serialize(const TEnumIndexedVector<E, T, Min, Max>& vector, NYson::IYsonConsumer* consumer)
+{
+ consumer->OnBeginMap();
+ for (auto key : TEnumTraits<E>::GetDomainValues()) {
+ if (!vector.IsDomainValue(key)) {
+ continue;
+ }
+ const auto& value = vector[key];
+ if (!NDetail::CanOmitValue(&value, nullptr)) {
+ consumer->OnKeyedItem(FormatEnum(key));
+ Serialize(value, consumer);
+ }
+ }
+ consumer->OnEndMap();
+}
+
+void SerializeProtobufMessage(
+ const google::protobuf::Message& message,
+ const NYson::TProtobufMessageType* type,
+ NYson::IYsonConsumer* consumer);
+
+template <class T>
+void Serialize(
+ const T& message,
+ NYson::IYsonConsumer* consumer,
+ typename std::enable_if<std::is_convertible<T*, ::google::protobuf::Message*>::value, void>::type*)
+{
+ SerializeProtobufMessage(message, NYson::ReflectProtobufMessageType<T>(), consumer);
+}
+
+template <class T, class TTag>
+void Serialize(const TStrongTypedef<T, TTag>& value, NYson::IYsonConsumer* consumer)
+{
+ Serialize(value.Underlying(), consumer);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void Deserialize(TIntrusivePtr<T>& value, INodePtr node)
+{
+ if (!value) {
+ value = New<T>();
+ }
+ if (node->GetType() != ENodeType::Entity) {
+ Deserialize(*value, node);
+ }
+}
+
+template <class T>
+void Deserialize(std::unique_ptr<T>& value, INodePtr node)
+{
+ if (!value) {
+ value = std::make_unique<T>();
+ }
+ if (node->GetType() != ENodeType::Entity) {
+ Deserialize(*value, node);
+ }
+}
+
+// Enums
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void Deserialize(T& value, INodePtr node)
+{
+ if constexpr (TEnumTraits<T>::IsBitEnum) {
+ switch (node->GetType()) {
+ case ENodeType::List:
+ value = T();
+ for (const auto& item : node->AsList()->GetChildren()) {
+ value |= ParseEnum<T>(item->GetValue<TString>());
+ }
+ break;
+ case ENodeType::String:
+ value = ParseEnum<T>(node->AsString()->GetValue());
+ break;
+ default:
+ THROW_ERROR_EXCEPTION("Cannot deserialize bit enum from %Qlv node",
+ node->GetType());
+ }
+ } else {
+ value = ParseEnum<T>(node->GetValue<TString>());
+ }
+}
+
+// std::optional
+template <class T>
+void Deserialize(std::optional<T>& value, INodePtr node)
+{
+ if (node->GetType() == ENodeType::Entity) {
+ value = std::nullopt;
+ } else {
+ if (!value) {
+ value = T();
+ }
+ Deserialize(*value, node);
+ }
+}
+
+// std::vector
+template <class T, class A>
+void Deserialize(std::vector<T, A>& value, INodePtr node)
+{
+ NDetail::DeserializeVector(value, node);
+}
+
+// std::deque
+template <class T, class A>
+void Deserialize(std::deque<T, A>& value, INodePtr node)
+{
+ NDetail::DeserializeVector(value, node);
+}
+
+// TCompactVector
+template <class T, size_t N>
+void Deserialize(TCompactVector<T, N>& value, INodePtr node)
+{
+ NDetail::DeserializeVector(value, node);
+}
+
+// TErrorOr
+template <class T>
+void Deserialize(TErrorOr<T>& error, NYTree::INodePtr node)
+{
+ TError& justError = error;
+ Deserialize(justError, node);
+ if (error.IsOK()) {
+ auto mapNode = node->AsMap();
+ auto valueNode = mapNode->FindChild("value");
+ if (valueNode) {
+ Deserialize(error.Value(), std::move(valueNode));
+ }
+ }
+}
+
+template <class T>
+void Deserialize(TErrorOr<T>& error, NYson::TYsonPullParserCursor* cursor)
+{
+ Deserialize(error, NYson::ExtractTo<NYTree::INodePtr>(cursor));
+}
+
+template <class F, class S>
+void Deserialize(std::pair<F, S>& value, INodePtr node)
+{
+ NDetail::DeserializeTuple(value, node);
+}
+
+template <class T, size_t N>
+void Deserialize(std::array<T, N>& value, INodePtr node)
+{
+ NDetail::DeserializeTuple(value, node);
+}
+
+template <class... T>
+void Deserialize(std::tuple<T...>& value, INodePtr node)
+{
+ NDetail::DeserializeTuple(value, node);
+}
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K>
+void Deserialize(C<T...>& value, INodePtr node)
+{
+ NDetail::DeserializeAssociative(value, node);
+}
+
+template <class E, class T, E Min, E Max>
+void Deserialize(TEnumIndexedVector<E, T, Min, Max>& vector, INodePtr node)
+{
+ vector = {};
+ auto mapNode = node->AsMap();
+ for (const auto& [stringKey, child] : mapNode->GetChildren()) {
+ auto key = ParseEnum<E>(stringKey);
+ if (!vector.IsDomainValue(key)) {
+ THROW_ERROR_EXCEPTION("Enum value %Qlv is out of supported range",
+ key);
+ }
+ Deserialize(vector[key], child);
+ }
+}
+
+void DeserializeProtobufMessage(
+ google::protobuf::Message& message,
+ const NYson::TProtobufMessageType* type,
+ const INodePtr& node,
+ const NYson::TProtobufWriterOptions& options = {});
+
+template <class T>
+void Deserialize(
+ T& message,
+ const INodePtr& node,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type*)
+{
+ NYson::TProtobufWriterOptions options;
+ options.UnknownYsonFieldModeResolver = NYson::TProtobufWriterOptions::CreateConstantUnknownYsonFieldModeResolver(
+ NYson::EUnknownYsonFieldsMode::Keep);
+ DeserializeProtobufMessage(message, NYson::ReflectProtobufMessageType<T>(), node, options);
+}
+
+template <class T>
+void Deserialize(
+ T& message,
+ NYson::TYsonPullParserCursor* cursor,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type*)
+{
+ Deserialize(message, NYson::ExtractTo<NYTree::INodePtr>(cursor));
+}
+
+template <class T, class TTag>
+void Deserialize(TStrongTypedef<T, TTag>& value, INodePtr node)
+{
+ Deserialize(value.Underlying(), node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/serialize.cpp b/yt/yt/core/ytree/serialize.cpp
new file mode 100644
index 0000000000..553ab3b03f
--- /dev/null
+++ b/yt/yt/core/ytree/serialize.cpp
@@ -0,0 +1,331 @@
+#include "serialize.h"
+
+#include <yt/yt/core/ytree/tree_visitor.h>
+
+#include <yt/yt/core/misc/blob.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+using namespace google::protobuf;
+using namespace google::protobuf::io;
+
+////////////////////////////////////////////////////////////////////////////////
+
+EYsonType GetYsonType(const TYsonString& yson)
+{
+ return yson.GetType();
+}
+
+EYsonType GetYsonType(const TYsonStringBuf& yson)
+{
+ return yson.GetType();
+}
+
+EYsonType GetYsonType(const TYsonInput& input)
+{
+ return input.GetType();
+}
+
+EYsonType GetYsonType(const TYsonProducer& producer)
+{
+ return producer.GetType();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// signed integers
+#define SERIALIZE(type) \
+ void Serialize(type value, IYsonConsumer* consumer) \
+ { \
+ consumer->OnInt64Scalar(CheckedIntegralCast<i64>(value)); \
+ }
+
+SERIALIZE(signed char)
+SERIALIZE(short)
+SERIALIZE(int)
+SERIALIZE(long)
+SERIALIZE(long long)
+
+
+#undef SERIALIZE
+
+
+// unsigned integers
+#define SERIALIZE(type) \
+ void Serialize(type value, IYsonConsumer* consumer) \
+ { \
+ consumer->OnUint64Scalar(CheckedIntegralCast<ui64>(value)); \
+ }
+
+SERIALIZE(unsigned char)
+#ifdef __cpp_char8_t
+SERIALIZE(char8_t)
+#endif
+SERIALIZE(unsigned short)
+SERIALIZE(unsigned)
+SERIALIZE(unsigned long)
+SERIALIZE(unsigned long long)
+
+#undef SERIALIZE
+
+// double
+void Serialize(double value, IYsonConsumer* consumer)
+{
+ consumer->OnDoubleScalar(value);
+}
+
+// TString
+void Serialize(const TString& value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(value);
+}
+
+// TStringBuf
+void Serialize(TStringBuf value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(value);
+}
+
+// const char*
+void Serialize(const char* value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(TStringBuf(value));
+}
+
+// bool
+void Serialize(bool value, IYsonConsumer* consumer)
+{
+ consumer->OnBooleanScalar(value);
+}
+
+// char
+void Serialize(char value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(TStringBuf(&value, 1));
+}
+
+// TDuration
+void Serialize(TDuration value, IYsonConsumer* consumer)
+{
+ consumer->OnInt64Scalar(value.MilliSeconds());
+}
+
+// TInstant
+void Serialize(TInstant value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(value.ToString());
+}
+
+// TGuid
+void Serialize(TGuid value, IYsonConsumer* consumer)
+{
+ consumer->OnStringScalar(ToString(value));
+}
+
+// IInputStream
+void Serialize(IInputStream& input, IYsonConsumer* consumer)
+{
+ Serialize(TYsonInput(&input), consumer);
+}
+
+// Subtypes of google::protobuf::Message
+void SerializeProtobufMessage(
+ const Message& message,
+ const TProtobufMessageType* type,
+ NYson::IYsonConsumer* consumer)
+{
+ auto byteSize = message.ByteSizeLong();
+ struct TProtobufToYsonTag { };
+ TBlob wireBytes(GetRefCountedTypeCookie<TProtobufToYsonTag>(), byteSize, false);
+ YT_VERIFY(message.SerializePartialToArray(wireBytes.Begin(), byteSize));
+ ArrayInputStream inputStream(wireBytes.Begin(), byteSize);
+ ParseProtobuf(consumer, &inputStream, type);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// signed integers
+#define DESERIALIZE(type) \
+ void Deserialize(type& value, INodePtr node) \
+ { \
+ if (node->GetType() == ENodeType::Int64) { \
+ value = CheckedIntegralCast<type>(node->AsInt64()->GetValue()); \
+ } else if (node->GetType() == ENodeType::Uint64) { \
+ value = CheckedIntegralCast<type>(node->AsUint64()->GetValue()); \
+ } else { \
+ THROW_ERROR_EXCEPTION("Cannot parse \"" #type "\" value from %Qlv", \
+ node->GetType()); \
+ } \
+ }
+
+DESERIALIZE(signed char)
+DESERIALIZE(short)
+DESERIALIZE(int)
+DESERIALIZE(long)
+DESERIALIZE(long long)
+DESERIALIZE(unsigned char)
+#ifdef __cpp_char8_t
+DESERIALIZE(char8_t)
+#endif
+DESERIALIZE(unsigned short)
+DESERIALIZE(unsigned)
+DESERIALIZE(unsigned long)
+DESERIALIZE(unsigned long long)
+
+#undef DESERIALIZE
+
+// double
+void Deserialize(double& value, INodePtr node)
+{
+ // Allow integer nodes to be serialized into doubles.
+ if (node->GetType() == ENodeType::Int64) {
+ value = node->AsInt64()->GetValue();
+ } else if (node->GetType() == ENodeType::Uint64) {
+ value = node->AsUint64()->GetValue();
+ } else {
+ value = node->AsDouble()->GetValue();
+ }
+}
+
+// TString
+void Deserialize(TString& value, INodePtr node)
+{
+ value = node->AsString()->GetValue();
+}
+
+// bool
+void Deserialize(bool& value, INodePtr node)
+{
+ if (node->GetType() == ENodeType::Boolean) {
+ value = node->AsBoolean()->GetValue();
+ } else if (node->GetType() == ENodeType::Int64) {
+ auto intValue = node->AsInt64()->GetValue();
+ if (intValue != 0 && intValue != 1) {
+ THROW_ERROR_EXCEPTION("Expected 0 or 1 but found %v", intValue);
+ }
+ value = static_cast<bool>(intValue);
+ } else if (node->GetType() == ENodeType::Uint64) {
+ auto uintValue = node->AsUint64()->GetValue();
+ if (uintValue != 0 && uintValue != 1) {
+ THROW_ERROR_EXCEPTION("Expected 0 or 1 but found %v", uintValue);
+ }
+ value = static_cast<bool>(uintValue);
+ } else {
+ auto stringValue = node->AsString()->GetValue();
+ value = ParseBool(stringValue);
+ }
+}
+
+// char
+void Deserialize(char& value, INodePtr node)
+{
+ TString stringValue = node->AsString()->GetValue();
+ if (stringValue.size() != 1) {
+ THROW_ERROR_EXCEPTION("Expected string of length 1 but found of length %v", stringValue.size());
+ }
+ value = stringValue[0];
+}
+
+// TDuration
+void Deserialize(TDuration& value, INodePtr node)
+{
+ switch (node->GetType()) {
+ case ENodeType::Int64: {
+ auto ms = node->AsInt64()->GetValue();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Duration cannot be negative");
+ }
+ value = TDuration::MilliSeconds(static_cast<ui64>(ms));
+ break;
+ }
+
+ case ENodeType::Uint64:
+ value = TDuration::MilliSeconds(node->AsUint64()->GetValue());
+ break;
+
+ case ENodeType::Double: {
+ auto ms = node->AsDouble()->GetValue();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Duration cannot be negative");
+ }
+ value = TDuration::MicroSeconds(static_cast<ui64>(ms * 1000.0));
+ break;
+ }
+
+ case ENodeType::String:
+ value = TDuration::Parse(node->AsString()->GetValue());
+ break;
+
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse duration from %Qlv",
+ node->GetType());
+ }
+}
+
+// TInstant
+void Deserialize(TInstant& value, INodePtr node)
+{
+ switch (node->GetType()) {
+ case ENodeType::Int64: {
+ auto ms = node->AsInt64()->GetValue();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Instant cannot be negative");
+ }
+ value = TInstant::MilliSeconds(ms);
+ break;
+ }
+
+ case ENodeType::Uint64:
+ value = TInstant::MilliSeconds(node->AsUint64()->GetValue());
+ break;
+
+ case ENodeType::Double: {
+ auto ms = node->AsDouble()->GetValue();
+ if (ms < 0) {
+ THROW_ERROR_EXCEPTION("Instant cannot be negative");
+ }
+ value = TInstant::MicroSeconds(static_cast<ui64>(ms * 1000.0));
+ break;
+ }
+
+ case ENodeType::String:
+ value = TInstant::ParseIso8601(node->AsString()->GetValue());
+ break;
+
+ default:
+ THROW_ERROR_EXCEPTION("Cannot parse instant from %Qlv",
+ node->GetType());
+ }
+}
+
+// TGuid
+void Deserialize(TGuid& value, INodePtr node)
+{
+ value = TGuid::FromString(node->AsString()->GetValue());
+}
+
+// Subtypes of google::protobuf::Message
+void DeserializeProtobufMessage(
+ Message& message,
+ const TProtobufMessageType* type,
+ const INodePtr& node,
+ const NYson::TProtobufWriterOptions& options)
+{
+ TString wireBytes;
+ StringOutputStream outputStream(&wireBytes);
+ auto protobufWriter = CreateProtobufWriter(&outputStream, type, options);
+ VisitTree(node, protobufWriter.get(), true);
+ if (!message.ParseFromArray(wireBytes.data(), wireBytes.size())) {
+ THROW_ERROR_EXCEPTION("Error parsing %v from wire bytes",
+ message.GetTypeName());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/serialize.h b/yt/yt/core/ytree/serialize.h
new file mode 100644
index 0000000000..9512f7cc1c
--- /dev/null
+++ b/yt/yt/core/ytree/serialize.h
@@ -0,0 +1,265 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/producer.h>
+
+#include <yt/yt/core/misc/guid.h>
+#include <yt/yt/core/misc/mpl.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <library/cpp/yt/small_containers/compact_vector.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+NYson::EYsonType GetYsonType(const T&);
+NYson::EYsonType GetYsonType(const NYson::TYsonString& yson);
+NYson::EYsonType GetYsonType(const NYson::TYsonStringBuf& yson);
+NYson::EYsonType GetYsonType(const NYson::TYsonInput& input);
+NYson::EYsonType GetYsonType(const NYson::TYsonProducer& producer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void WriteYson(
+ IZeroCopyOutput* output,
+ const T& value,
+ NYson::EYsonType type,
+ NYson::EYsonFormat format = NYson::EYsonFormat::Binary,
+ int indent = 4);
+
+template <class T>
+void WriteYson(
+ IZeroCopyOutput* output,
+ const T& value,
+ NYson::EYsonFormat format = NYson::EYsonFormat::Binary);
+
+template <class T>
+void WriteYson(
+ const NYson::TYsonOutput& output,
+ const T& value,
+ NYson::EYsonFormat format = NYson::EYsonFormat::Binary);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void Serialize(T* value, NYson::IYsonConsumer* consumer);
+
+template <class T>
+void Serialize(const TIntrusivePtr<T>& value, NYson::IYsonConsumer* consumer);
+
+// integers
+void Serialize(signed char value, NYson::IYsonConsumer* consumer);
+void Serialize(unsigned char value, NYson::IYsonConsumer* consumer);
+#ifdef __cpp_char8_t
+void Serialize(char8_t value, NYson::IYsonConsumer* consumer);
+#endif
+void Serialize(short value, NYson::IYsonConsumer* consumer);
+void Serialize(unsigned short value, NYson::IYsonConsumer* consumer);
+void Serialize(int value, NYson::IYsonConsumer* consumer);
+void Serialize(unsigned value, NYson::IYsonConsumer* consumer);
+void Serialize(long value, NYson::IYsonConsumer* consumer);
+void Serialize(unsigned long value, NYson::IYsonConsumer* consumer);
+void Serialize(long long value, NYson::IYsonConsumer* consumer);
+void Serialize(unsigned long long value, NYson::IYsonConsumer* consumer);
+
+// double
+void Serialize(double value, NYson::IYsonConsumer* consumer);
+
+// TString
+void Serialize(const TString& value, NYson::IYsonConsumer* consumer);
+
+// TStringBuf
+void Serialize(TStringBuf value, NYson::IYsonConsumer* consumer);
+
+// const char*
+void Serialize(const char* value, NYson::IYsonConsumer* consumer);
+
+// bool
+void Serialize(bool value, NYson::IYsonConsumer* consumer);
+
+// char
+void Serialize(char value, NYson::IYsonConsumer* consumer);
+
+// TDuration
+void Serialize(TDuration value, NYson::IYsonConsumer* consumer);
+
+// TInstant
+void Serialize(TInstant value, NYson::IYsonConsumer* consumer);
+
+// TGuid
+void Serialize(TGuid value, NYson::IYsonConsumer* consumer);
+
+// IInputStream
+void Serialize(IInputStream& input, NYson::IYsonConsumer* consumer);
+
+// Enums
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void Serialize(T value, NYson::IYsonConsumer* consumer);
+
+// std::optional
+template <class T>
+void Serialize(const std::optional<T>& value, NYson::IYsonConsumer* consumer);
+
+// std::vector
+template <class T, class A>
+void Serialize(const std::vector<T, A>& value, NYson::IYsonConsumer* consumer);
+
+// std::deque
+template <class T, class A>
+void Serialize(const std::deque<T, A>& value, NYson::IYsonConsumer* consumer);
+
+// TCompactVector
+template <class T, size_t N>
+void Serialize(const TCompactVector<T, N>& value, NYson::IYsonConsumer* consumer);
+
+// TErrorOr
+template <class T>
+void Serialize(const TErrorOr<T>& error, NYson::IYsonConsumer* consumer);
+
+template <class F, class S>
+void Serialize(const std::pair<F, S>& value, NYson::IYsonConsumer* consumer);
+
+template <class T, size_t N>
+void Serialize(const std::array<T, N>& value, NYson::IYsonConsumer* consumer);
+
+template <class... T>
+void Serialize(const std::tuple<T...>& value, NYson::IYsonConsumer* consumer);
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K = typename C<T...>::key_type>
+void Serialize(const C<T...>& value, NYson::IYsonConsumer* consumer);
+
+// TEnumIndexedVector
+template <class E, class T, E Min, E Max>
+void Serialize(const TEnumIndexedVector<E, T, Min, Max>& value, NYson::IYsonConsumer* consumer);
+
+// Subtypes of google::protobuf::Message
+template <class T>
+void Serialize(
+ const T& message,
+ NYson::IYsonConsumer* consumer,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type* = nullptr);
+
+template <class T, class TTag>
+void Serialize(const TStrongTypedef<T, TTag>& value, NYson::IYsonConsumer* consumer);
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void Deserialize(TIntrusivePtr<T>& value, INodePtr node);
+
+template <class T>
+void Deserialize(std::unique_ptr<T>& value, INodePtr node);
+
+// integers
+void Deserialize(signed char& value, INodePtr node);
+void Deserialize(unsigned char& value, INodePtr node);
+#ifdef __cpp_char8_t
+void Deserialize(char8_t& value, INodePtr node);
+#endif
+void Deserialize(short& value, INodePtr node);
+void Deserialize(unsigned short& value, INodePtr node);
+void Deserialize(int& value, INodePtr node);
+void Deserialize(unsigned& value, INodePtr node);
+void Deserialize(long& value, INodePtr node);
+void Deserialize(unsigned long& value, INodePtr node);
+void Deserialize(long long& value, INodePtr node);
+void Deserialize(unsigned long long& value, INodePtr node);
+
+// double
+void Deserialize(double& value, INodePtr node);
+
+// TString
+void Deserialize(TString& value, INodePtr node);
+
+// bool
+void Deserialize(bool& value, INodePtr node);
+
+// char
+void Deserialize(char& value, INodePtr node);
+
+// TDuration
+void Deserialize(TDuration& value, INodePtr node);
+
+// TInstant
+void Deserialize(TInstant& value, INodePtr node);
+
+// TGuid
+void Deserialize(TGuid& value, INodePtr node);
+
+// Enums
+template <class T>
+ requires TEnumTraits<T>::IsEnum
+void Deserialize(T& value, INodePtr node);
+
+// std::optional
+template <class T>
+void Deserialize(std::optional<T>& value, INodePtr node);
+
+// std::vector
+template <class T, class A>
+void Deserialize(std::vector<T, A>& value, INodePtr node);
+
+// std::deque
+template <class T, class A>
+void Deserialize(std::deque<T, A>& value, INodePtr node);
+
+// TCompactVector
+template <class T, size_t N>
+void Deserialize(TCompactVector<T, N>& value, INodePtr node);
+
+// TErrorOr
+template <class T>
+void Deserialize(TErrorOr<T>& error, INodePtr node);
+
+template <class T>
+void Deserialize(TErrorOr<T>& error, NYson::TYsonPullParserCursor* cursor);
+
+template <class F, class S>
+void Deserialize(std::pair<F, S>& value, INodePtr node);
+
+template <class T, size_t N>
+void Deserialize(std::array<T, N>& value, INodePtr node);
+
+template <class... T>
+void Deserialize(std::tuple<T...>& value, INodePtr node);
+
+// For any associative container.
+template <template<typename...> class C, class... T, class K = typename C<T...>::key_type>
+void Deserialize(C<T...>& value, INodePtr node);
+
+// TEnumIndexedVector
+template <class E, class T, E Min, E Max>
+void Deserialize(TEnumIndexedVector<E, T, Min, Max>& vector, INodePtr node);
+
+// Subtypes of google::protobuf::Message
+template <class T>
+void Deserialize(
+ T& message,
+ const INodePtr& node,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type* = nullptr);
+
+template <class T>
+void Deserialize(
+ T& message,
+ NYson::TYsonPullParserCursor* cursor,
+ typename std::enable_if<std::is_convertible<T*, google::protobuf::Message*>::value, void>::type* = nullptr);
+
+template <class T, class TTag>
+void Deserialize(TStrongTypedef<T, TTag>& value, INodePtr node);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define SERIALIZE_INL_H_
+#include "serialize-inl.h"
+#undef SERIALIZE_INL_H_
diff --git a/yt/yt/core/ytree/service_combiner.cpp b/yt/yt/core/ytree/service_combiner.cpp
new file mode 100644
index 0000000000..ef51b0b380
--- /dev/null
+++ b/yt/yt/core/ytree/service_combiner.cpp
@@ -0,0 +1,383 @@
+#include "service_combiner.h"
+#include "ypath_client.h"
+#include "ypath_proxy.h"
+
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <yt/yt/core/yson/async_writer.h>
+#include <yt/yt/core/yson/tokenizer.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYTree {
+
+using namespace NRpc;
+using namespace NConcurrency;
+using namespace NYPath;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TDuration UpdateKeysFailedBackoff = TDuration::Seconds(1);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceCombiner::TImpl
+ : public TSupportsAttributes
+{
+public:
+ TImpl(
+ std::vector<IYPathServicePtr> services,
+ std::optional<TDuration> keysUpdatePeriod,
+ bool updateKeysOnMissingKey)
+ : Services_(std::move(services))
+ , KeysUpdatePeriod_(keysUpdatePeriod)
+ , UpdateKeysOnMissingKey_(updateKeysOnMissingKey)
+ {
+ auto workerInvoker = TDispatcher::Get()->GetHeavyInvoker();
+ auto keysUpdateCallback = BIND(&TImpl::UpdateKeys, MakeWeak(this));
+ if (keysUpdatePeriod) {
+ UpdateKeysExecutor_ = New<TPeriodicExecutor>(workerInvoker, keysUpdateCallback, *keysUpdatePeriod);
+ UpdateKeysExecutor_->Start();
+ } else {
+ workerInvoker->Invoke(keysUpdateCallback);
+ }
+ }
+
+ TFuture<void> GetInitialized()
+ {
+ return InitializedPromise_.ToFuture();
+ }
+
+ void SetUpdatePeriod(std::optional<TDuration> period)
+ {
+ if (period) {
+ if (KeysUpdatePeriod_) {
+ UpdateKeysExecutor_->SetPeriod(period);
+ } else {
+ auto workerInvoker = TDispatcher::Get()->GetHeavyInvoker();
+ UpdateKeysExecutor_ = New<TPeriodicExecutor>(workerInvoker, BIND(&TImpl::UpdateKeys, MakeWeak(this)), *period);
+ UpdateKeysExecutor_->Start();
+ }
+ } else {
+ if (KeysUpdatePeriod_) {
+ YT_UNUSED_FUTURE(UpdateKeysExecutor_->Stop());
+ }
+ }
+ KeysUpdatePeriod_ = period;
+ }
+
+private:
+ const std::vector<IYPathServicePtr> Services_;
+
+ std::optional<TDuration> KeysUpdatePeriod_;
+ bool UpdateKeysOnMissingKey_;
+
+ NConcurrency::TPeriodicExecutorPtr UpdateKeysExecutor_;
+
+ TPromise<void> InitializedPromise_ = NewPromise<void>();
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, KeyMappingSpinLock_);
+ using TKeyMappingOrError = TErrorOr<THashMap<TString, IYPathServicePtr>>;
+ TKeyMappingOrError KeyMapping_;
+
+private:
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ return TSupportsAttributes::DoInvoke(context);
+ }
+
+ TResolveResult ResolveRecursive(const TYPath& path, const IYPathServiceContextPtr& context) override
+ {
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ const auto& key = tokenizer.GetLiteralValue();
+
+ IYPathServicePtr foundService = nullptr;
+ THashMap<TString, IYPathServicePtr>::const_iterator keyIterator;
+ {
+ auto guard = Guard(KeyMappingSpinLock_);
+ const auto& keyMapping = KeyMapping_.ValueOrThrow();
+
+ keyIterator = keyMapping.find(key);
+ if (keyIterator == keyMapping.end()) {
+ if (UpdateKeysOnMissingKey_) {
+ guard.Release();
+ UpdateKeys();
+ }
+ } else {
+ foundService = keyIterator->second;
+ }
+ }
+
+ if (!foundService && UpdateKeysOnMissingKey_) {
+ auto guard = Guard(KeyMappingSpinLock_);
+ const auto& keyMapping = KeyMapping_.ValueOrThrow();
+
+ keyIterator = keyMapping.find(key);
+ if (keyIterator != keyMapping.end()) {
+ foundService = keyIterator->second;
+ }
+ }
+
+ if (!foundService) {
+ if (context->GetMethod() == "Exists") {
+ return TResolveResultHere{path};
+ }
+ THROW_ERROR_EXCEPTION("Node has no child with key %Qv", ToYPathLiteral(key));
+ }
+ return TResolveResultThere{foundService, "/" + path};
+ }
+
+
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ ValidateKeyMapping();
+
+ i64 limit = request->has_limit()
+ ? request->limit()
+ : DefaultVirtualChildLimit;
+
+ context->SetRequestInfo("Limit: %v", limit);
+
+ std::atomic<bool> incomplete = {false};
+
+ // TODO(max42): Make it more efficient :(
+ std::vector<TFuture<THashMap<TString, INodePtr>>> serviceResultFutures;
+ for (const auto& service : Services_) {
+ auto innerRequest = TYPathProxy::Get(TYPath());
+ innerRequest->set_limit(limit);
+ if (request->has_attributes()) {
+ innerRequest->mutable_attributes()->CopyFrom(request->attributes());
+ }
+ if (request->has_options()) {
+ innerRequest->mutable_options()->CopyFrom(request->options());
+ }
+ auto asyncInnerResult = ExecuteVerb(service, innerRequest)
+ .Apply(BIND([&] (TYPathProxy::TRspGetPtr response) {
+ auto node = ConvertToNode(TYsonString(response->value()));
+ if (node->Attributes().Get("incomplete", false)) {
+ incomplete = true;
+ }
+ return ConvertTo<THashMap<TString, INodePtr>>(node);
+ }));
+ serviceResultFutures.push_back(asyncInnerResult);
+ }
+ auto asyncResult = AllSucceeded(serviceResultFutures);
+ auto serviceResults = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ THashMap<TString, INodePtr> combinedServiceResults;
+ for (const auto& serviceResult : serviceResults) {
+ if (static_cast<i64>(serviceResult.size() + combinedServiceResults.size()) > limit) {
+ combinedServiceResults.insert(
+ serviceResult.begin(),
+ std::next(serviceResult.begin(), limit - static_cast<i64>(serviceResult.size())));
+ incomplete = true;
+ break;
+ } else {
+ combinedServiceResults.insert(serviceResult.begin(), serviceResult.end());
+ }
+ }
+
+ TStringStream stream;
+ TYsonWriter writer(&stream);
+
+ if (incomplete) {
+ BuildYsonFluently(&writer)
+ .BeginAttributes()
+ .Item("incomplete").Value(true)
+ .EndAttributes();
+ }
+ BuildYsonFluently(&writer)
+ .DoMapFor(combinedServiceResults, [] (TFluentMap fluent, const decltype(combinedServiceResults)::value_type& item) {
+ fluent
+ .Item(item.first).Value(item.second);
+ });
+
+ writer.Flush();
+ response->set_value(stream.Str());
+ context->Reply();
+ }
+
+ void ListSelf(TReqList* request, TRspList* response, const TCtxListPtr& context) override
+ {
+ ValidateKeyMapping();
+
+ i64 limit = request->has_limit()
+ ? request->limit()
+ : DefaultVirtualChildLimit;
+
+ context->SetRequestInfo("Limit: %v", limit);
+
+ std::atomic<bool> incomplete = {false};
+
+ std::vector<TFuture<std::vector<IStringNodePtr>>> serviceResultFutures;
+ for (const auto& service : Services_) {
+ auto innerRequest = TYPathProxy::List(TYPath());
+ innerRequest->set_limit(limit);
+ if (request->has_attributes()) {
+ innerRequest->mutable_attributes()->CopyFrom(request->attributes());
+ }
+ auto asyncInnerResult = ExecuteVerb(service, innerRequest)
+ .Apply(BIND([&] (TYPathProxy::TRspListPtr response) {
+ auto node = ConvertToNode(TYsonString(response->value()));
+ if (node->Attributes().Get("incomplete", false)) {
+ incomplete = true;
+ }
+ return ConvertTo<std::vector<IStringNodePtr>>(node);
+ }));
+ serviceResultFutures.push_back(asyncInnerResult);
+ }
+ auto asyncResult = AllSucceeded(serviceResultFutures);
+ auto serviceResults = WaitFor(asyncResult)
+ .ValueOrThrow();
+
+ std::vector<IStringNodePtr> combinedServiceResults;
+ for (const auto& serviceResult : serviceResults) {
+ if (static_cast<i64>(combinedServiceResults.size() + serviceResult.size()) > limit) {
+ combinedServiceResults.insert(
+ combinedServiceResults.end(),
+ serviceResult.begin(),
+ std::next(serviceResult.begin(), limit - static_cast<i64>(combinedServiceResults.size())));
+ incomplete = true;
+ break;
+ } else {
+ combinedServiceResults.insert(combinedServiceResults.end(), serviceResult.begin(), serviceResult.end());
+ }
+ }
+
+ TStringStream stream;
+ TYsonWriter writer(&stream);
+
+ if (incomplete) {
+ BuildYsonFluently(&writer)
+ .BeginAttributes()
+ .Item("incomplete").Value(true)
+ .EndAttributes();
+ }
+
+ // There is a small chance that while we waited for all services to respond, they moved into an inconsistent
+ // state and provided us with non-disjoint lists. In this case we force the list to contain only unique keys.
+ THashSet<TString> keys;
+
+ BuildYsonFluently(&writer)
+ .DoListFor(combinedServiceResults, [&] (TFluentList fluent, const IStringNodePtr& item) {
+ if (!keys.contains(item->GetValue())) {
+ fluent
+ .Item().Value(item);
+ keys.insert(item->GetValue());
+ }
+ });
+
+ writer.Flush();
+ response->set_value(stream.Str());
+ context->Reply();
+ }
+
+
+ void UpdateKeys()
+ {
+ std::vector<TFuture<std::vector<TString>>> serviceListFutures;
+ for (const auto& service : Services_) {
+ auto asyncList = AsyncYPathList(service, TYPath() /* path */, std::numeric_limits<i64>::max() /* limit */);
+ serviceListFutures.push_back(asyncList);
+ }
+ auto asyncResult = AllSucceeded(serviceListFutures);
+ auto serviceListsOrError = WaitFor(asyncResult);
+
+ if (!serviceListsOrError.IsOK()) {
+ SetKeyMapping(TError(serviceListsOrError));
+ if (!KeysUpdatePeriod_) {
+ auto workerInvoker = TDispatcher::Get()->GetHeavyInvoker();
+ TDelayedExecutor::Submit(
+ BIND(&TImpl::UpdateKeys, MakeWeak(this)),
+ UpdateKeysFailedBackoff,
+ std::move(workerInvoker));
+ }
+ return;
+ }
+ const auto& serviceLists = serviceListsOrError.Value();
+
+ TKeyMappingOrError newKeyMappingOrError;
+ auto& newKeyMapping = newKeyMappingOrError.Value();
+
+ for (int index = 0; index < static_cast<int>(Services_.size()); ++index) {
+ for (const auto& key : serviceLists[index]) {
+ auto pair = newKeyMapping.emplace(key, Services_[index]);
+ if (!pair.second) {
+ SetKeyMapping(TError("Key %Qv is operated by more than one YPathService",
+ key));
+ return;
+ }
+ }
+ }
+
+ SetKeyMapping(std::move(newKeyMappingOrError));
+ }
+
+ void ValidateKeyMapping()
+ {
+ auto guard = Guard(KeyMappingSpinLock_);
+ // If several services already share the same key, we'd better throw an error and do nothing.
+ KeyMapping_.ThrowOnError();
+ }
+
+ void SetKeyMapping(TKeyMappingOrError keyMapping)
+ {
+ {
+ auto guard = Guard(KeyMappingSpinLock_);
+ KeyMapping_ = std::move(keyMapping);
+ }
+ InitializedPromise_.TrySet();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TServiceCombiner::TServiceCombiner(
+ std::vector<IYPathServicePtr> services,
+ std::optional<TDuration> keysUpdatePeriod,
+ bool updateKeysOnMissingKey)
+ : Impl_(New<TImpl>(std::move(services), keysUpdatePeriod, updateKeysOnMissingKey))
+{ }
+
+TServiceCombiner::~TServiceCombiner()
+{ }
+
+void TServiceCombiner::SetUpdatePeriod(TDuration period)
+{
+ Impl_->SetUpdatePeriod(period);
+}
+
+IYPathService::TResolveResult TServiceCombiner::Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ return TResolveResultHere{path};
+}
+
+void TServiceCombiner::Invoke(const IYPathServiceContextPtr& context)
+{
+ Impl_->GetInitialized().Subscribe(BIND([impl = Impl_, context = context] (const TError& error) {
+ if (error.IsOK()) {
+ ExecuteVerb(impl, context);
+ } else {
+ context->Reply(error);
+ }
+ }));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/service_combiner.h b/yt/yt/core/ytree/service_combiner.h
new file mode 100644
index 0000000000..81bb699231
--- /dev/null
+++ b/yt/yt/core/ytree/service_combiner.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "ypath_detail.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TServiceCombiner
+ : public TYPathServiceBase
+{
+public:
+ explicit TServiceCombiner(
+ std::vector<IYPathServicePtr> services,
+ std::optional<TDuration> keysUpdatePeriod = std::nullopt,
+ bool updateKeysOnMissingKey = false);
+
+ void SetUpdatePeriod(TDuration period);
+
+ TResolveResult Resolve(const TYPath& path, const IYPathServiceContextPtr& context) override;
+ void Invoke(const IYPathServiceContextPtr& context) override;
+
+ ~TServiceCombiner();
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TServiceCombiner)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/static_service_dispatcher.cpp b/yt/yt/core/ytree/static_service_dispatcher.cpp
new file mode 100644
index 0000000000..5c1ef9310c
--- /dev/null
+++ b/yt/yt/core/ytree/static_service_dispatcher.cpp
@@ -0,0 +1,63 @@
+#include "static_service_dispatcher.h"
+
+#include "exception_helpers.h"
+#include "fluent.h"
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TStaticServiceDispatcher::RegisterService(
+ TStringBuf key,
+ TCallback<IYPathServicePtr()> serviceFactory)
+{
+ bool inserted = Services_.emplace(key, std::move(serviceFactory)).second;
+ YT_ASSERT(inserted);
+}
+
+void TStaticServiceDispatcher::ListSelf(
+ TReqList* /*request*/,
+ TRspList* response,
+ const TCtxListPtr& context)
+{
+ context->SetRequestInfo();
+
+ auto result = BuildYsonStringFluently()
+ .DoListFor(
+ Services_,
+ [&] (auto fluentList, const auto& pair) {
+ fluentList
+ .Item().Value(pair.first);
+ });
+
+ response->set_value(result.ToString());
+ context->Reply();
+}
+
+TStaticServiceDispatcher::TResolveResult TStaticServiceDispatcher::ResolveRecursive(
+ const NYPath::TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+
+ auto it = Services_.find(key);
+
+ if (it == Services_.end()) {
+ const auto& method = context->GetMethod();
+ if (method == "Exists") {
+ return TResolveResultHere{"/" + path};
+ }
+ ThrowNoSuchChildKey(key);
+ }
+
+ return TResolveResultThere{it->second(), NYPath::TYPath(tokenizer.GetSuffix())};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/static_service_dispatcher.h b/yt/yt/core/ytree/static_service_dispatcher.h
new file mode 100644
index 0000000000..e05ddf68f0
--- /dev/null
+++ b/yt/yt/core/ytree/static_service_dispatcher.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "ypath_detail.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Helper for organizing several YPath services into a map-like object.
+/*!
+ * This helper is designed for a small number of YPath services with well-known names.
+ * To handle arbitrary size maps of similar objects see |TVirtualMapBase|.
+ */
+class TStaticServiceDispatcher
+ : public virtual TYPathServiceBase
+ , public virtual TSupportsList
+{
+protected:
+ //! Register #service under #key.
+ /*! Note that services are instantiated for each request invocation, thus #serviceFactory
+ * must be thread-safe enough.
+ *
+ * Also, be careful not to introduce circular reference, i.e. #serviceFactory must
+ * not hold MakeStrong(this). It is guaranteed that this remains valid during #serviceFactory
+ * execution, so binding to a raw this pointer is OK.
+ *
+ * Finally, note that IYPathService returned from #serviceFactory _may_ outlive this.
+ */
+ void RegisterService(TStringBuf key, TCallback<IYPathServicePtr()> serviceFactory);
+
+private:
+ THashMap<TString, TCallback<IYPathServicePtr()>> Services_;
+
+ void ListSelf(
+ TReqList* /*request*/,
+ TRspList* response,
+ const TCtxListPtr& context) override;
+
+ TResolveResult ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/system_attribute_provider.cpp b/yt/yt/core/ytree/system_attribute_provider.cpp
new file mode 100644
index 0000000000..fff115c767
--- /dev/null
+++ b/yt/yt/core/ytree/system_attribute_provider.cpp
@@ -0,0 +1,73 @@
+#include "system_attribute_provider.h"
+
+#include <yt/yt/core/yson/writer.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+EPermission ISystemAttributeProvider::GetCustomAttributeModifyPermission()
+{
+ return EPermission::Write;
+}
+
+void ISystemAttributeProvider::ReserveAndListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors)
+{
+ descriptors->reserve(64);
+ ListSystemAttributes(descriptors);
+}
+
+void ISystemAttributeProvider::ListSystemAttributes(std::map<TInternedAttributeKey, TAttributeDescriptor>* descriptors)
+{
+ std::vector<TAttributeDescriptor> attributes;
+ ReserveAndListSystemAttributes(&attributes);
+
+ for (const auto& descriptor : attributes) {
+ YT_VERIFY(descriptors->emplace(descriptor.InternedKey, descriptor).second);
+ }
+}
+
+void ISystemAttributeProvider::ListBuiltinAttributes(std::vector<TAttributeDescriptor>* descriptors)
+{
+ std::vector<TAttributeDescriptor> systemAttributes;
+ ReserveAndListSystemAttributes(&systemAttributes);
+
+ for (const auto& attribute : systemAttributes) {
+ if (!attribute.Custom) {
+ (*descriptors).push_back(attribute);
+ }
+ }
+}
+
+std::optional<ISystemAttributeProvider::TAttributeDescriptor> ISystemAttributeProvider::FindBuiltinAttributeDescriptor(
+ TInternedAttributeKey key)
+{
+ std::vector<TAttributeDescriptor> builtinAttributes;
+ ListBuiltinAttributes(&builtinAttributes);
+
+ auto it = std::find_if(
+ builtinAttributes.begin(),
+ builtinAttributes.end(),
+ [&] (const ISystemAttributeProvider::TAttributeDescriptor& info) {
+ // Suppress operator== overload for enums.
+ return static_cast<int>(info.InternedKey) == static_cast<int>(key);
+ });
+ return it == builtinAttributes.end() ? std::nullopt : std::make_optional(*it);
+}
+
+TYsonString ISystemAttributeProvider::FindBuiltinAttribute(TInternedAttributeKey key)
+{
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream);
+ if (!GetBuiltinAttribute(key, &writer)) {
+ return TYsonString();
+ }
+ writer.Flush();
+ return TYsonString(stream.Str());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/system_attribute_provider.h b/yt/yt/core/ytree/system_attribute_provider.h
new file mode 100644
index 0000000000..ebd76caddd
--- /dev/null
+++ b/yt/yt/core/ytree/system_attribute_provider.h
@@ -0,0 +1,161 @@
+#pragma once
+
+#include "public.h"
+#include "interned_attributes.h"
+#include "permission.h"
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/string.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <yt/yt/core/actions/future.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ISystemAttributeProvider
+{
+ virtual ~ISystemAttributeProvider() = default;
+
+ //! Describes a system attribute.
+ struct TAttributeDescriptor
+ {
+ TInternedAttributeKey InternedKey = InvalidInternedAttribute;
+ bool Present = true;
+ bool Opaque = false;
+ bool Custom = false;
+ bool Writable = false;
+ bool Removable = false;
+ bool Replicated = false;
+ bool Mandatory = false;
+ bool External = false;
+ EPermissionSet ModifyPermission = EPermission::Write;
+
+ TAttributeDescriptor& SetPresent(bool value)
+ {
+ Present = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetOpaque(bool value)
+ {
+ Opaque = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetCustom(bool value)
+ {
+ Custom = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetWritable(bool value)
+ {
+ Writable = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetRemovable(bool value)
+ {
+ Removable = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetReplicated(bool value)
+ {
+ Replicated = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetMandatory(bool value)
+ {
+ Mandatory = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetExternal(bool value)
+ {
+ External = value;
+ return *this;
+ }
+
+ TAttributeDescriptor& SetWritePermission(EPermission value)
+ {
+ ModifyPermission = value;
+ return *this;
+ }
+
+ TAttributeDescriptor(TInternedAttributeKey key)
+ : InternedKey(key)
+ { }
+ };
+
+ //! Populates the list of all system attributes supported by this object.
+ /*!
+ * \note
+ * Must not clear #attributes since additional items may be added in inheritors.
+ */
+ virtual void ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors) = 0;
+
+ //! Returns a (typically cached) set consisting of all non-custom attributes keys.
+ //! \see TAttributeDescriptor::Custom
+ //! \see ListSystemAttributes
+ virtual const THashSet<TInternedAttributeKey>& GetBuiltinAttributeKeys() = 0;
+
+ //! Gets the value of a builtin attribute.
+ /*!
+ * \returns |false| if there is no builtin attribute with the given key.
+ */
+ virtual bool GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer) = 0;
+
+ //! Asynchronously gets the value of a builtin attribute.
+ /*!
+ * \returns A future representing attribute value or null if there is no such async builtin attribute.
+ */
+ virtual TFuture<NYson::TYsonString> GetBuiltinAttributeAsync(TInternedAttributeKey key) = 0;
+
+ //! Sets the value of a builtin attribute.
+ /*!
+ * \returns |false| if there is no writable builtin attribute with the given key.
+ */
+ virtual bool SetBuiltinAttribute(TInternedAttributeKey key, const NYson::TYsonString& value) = 0;
+
+ //! Removes the builtin attribute.
+ /*!
+ * \returns |false| if there is no removable builtin attribute with the given key.
+ */
+ virtual bool RemoveBuiltinAttribute(TInternedAttributeKey key) = 0;
+
+ //! Permission to set/remove non-builtin attributes.
+ /*!
+ * \returns permission to set/remove non-builtin attributes.
+ */
+ virtual EPermission GetCustomAttributeModifyPermission();
+
+ // Extension methods.
+
+ //! Similar to its interface counterpart, but reserves the vector beforehand.
+ void ReserveAndListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors);
+
+ //! Similar to its interface counterpart, but populates a map rather than a vector.
+ void ListSystemAttributes(std::map<TInternedAttributeKey, TAttributeDescriptor>* descriptors);
+
+ //! Populates the list of all builtin attributes supported by this object.
+ void ListBuiltinAttributes(std::vector<TAttributeDescriptor>* descriptors);
+
+ //! Returns an instance of TAttributeDescriptor matching a given #key or null if no such
+ //! builtin attribute is known.
+ std::optional<TAttributeDescriptor> FindBuiltinAttributeDescriptor(TInternedAttributeKey key);
+
+ //! Wraps #GetBuiltinAttribute and returns the YSON string instead
+ //! of writing it into a consumer.
+ NYson::TYsonString FindBuiltinAttribute(TInternedAttributeKey key);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/tree_builder.cpp b/yt/yt/core/ytree/tree_builder.cpp
new file mode 100644
index 0000000000..f4fa2f33fc
--- /dev/null
+++ b/yt/yt/core/ytree/tree_builder.cpp
@@ -0,0 +1,180 @@
+#include "tree_builder.h"
+#include "attribute_consumer.h"
+#include "helpers.h"
+#include "attributes.h"
+#include "attribute_consumer.h"
+#include "node.h"
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <yt/yt/core/yson/forwarding_consumer.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <stack>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTreeBuilder
+ : public NYson::TForwardingYsonConsumer
+ , public ITreeBuilder
+{
+public:
+ explicit TTreeBuilder(INodeFactory* factory)
+ : Factory(factory)
+ {
+ YT_ASSERT(Factory);
+ }
+
+ void BeginTree() override
+ {
+ YT_VERIFY(NodeStack.size() == 0);
+ }
+
+ INodePtr EndTree() override
+ {
+ // Failure here means that the tree is not fully constructed yet.
+ YT_VERIFY(NodeStack.size() == 0);
+ YT_VERIFY(ResultNode);
+
+ return ResultNode;
+ }
+
+ void OnNode(INodePtr node) override
+ {
+ AddNode(node, false);
+ }
+
+ void OnMyStringScalar(TStringBuf value) override
+ {
+ auto node = Factory->CreateString();
+ node->SetValue(TString(value));
+ AddNode(node, false);
+ }
+
+ void OnMyInt64Scalar(i64 value) override
+ {
+ auto node = Factory->CreateInt64();
+ node->SetValue(value);
+ AddNode(node, false);
+ }
+
+ void OnMyUint64Scalar(ui64 value) override
+ {
+ auto node = Factory->CreateUint64();
+ node->SetValue(value);
+ AddNode(node, false);
+ }
+
+ void OnMyDoubleScalar(double value) override
+ {
+ auto node = Factory->CreateDouble();
+ node->SetValue(value);
+ AddNode(node, false);
+ }
+
+ void OnMyBooleanScalar(bool value) override
+ {
+ auto node = Factory->CreateBoolean();
+ node->SetValue(value);
+ AddNode(node, false);
+ }
+
+ void OnMyEntity() override
+ {
+ AddNode(Factory->CreateEntity(), false);
+ }
+
+
+ void OnMyBeginList() override
+ {
+ AddNode(Factory->CreateList(), true);
+ }
+
+ void OnMyListItem() override
+ {
+ YT_ASSERT(!Key);
+ }
+
+ void OnMyEndList() override
+ {
+ NodeStack.pop();
+ }
+
+
+ void OnMyBeginMap() override
+ {
+ AddNode(Factory->CreateMap(), true);
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ Key = TString(key);
+ }
+
+ void OnMyEndMap() override
+ {
+ NodeStack.pop();
+ }
+
+ void OnMyBeginAttributes() override
+ {
+ YT_ASSERT(!AttributeConsumer);
+ Attributes = CreateEphemeralAttributes();
+ AttributeConsumer = std::make_unique<TAttributeConsumer>(Attributes.Get());
+ Forward(AttributeConsumer.get(), nullptr, NYson::EYsonType::MapFragment);
+ }
+
+ void OnMyEndAttributes() override
+ {
+ AttributeConsumer.reset();
+ YT_ASSERT(Attributes);
+ }
+
+private:
+ INodeFactory* const Factory;
+
+ //! Contains nodes forming the current path in the tree.
+ std::stack<INodePtr> NodeStack;
+ std::optional<TString> Key;
+ INodePtr ResultNode;
+ std::unique_ptr<TAttributeConsumer> AttributeConsumer;
+ IAttributeDictionaryPtr Attributes;
+
+ void AddNode(INodePtr node, bool push)
+ {
+ if (Attributes) {
+ node->MutableAttributes()->MergeFrom(*Attributes);
+ Attributes = nullptr;
+ }
+
+ if (NodeStack.empty()) {
+ ResultNode = node;
+ } else {
+ auto collectionNode = NodeStack.top();
+ if (Key) {
+ if (!collectionNode->AsMap()->AddChild(*Key, node)) {
+ THROW_ERROR_EXCEPTION("Duplicate key %Qv", *Key);
+ }
+ Key.reset();
+ } else {
+ collectionNode->AsList()->AddChild(node);
+ }
+ }
+
+ if (push) {
+ NodeStack.push(node);
+ }
+ }
+};
+
+std::unique_ptr<ITreeBuilder> CreateBuilderFromFactory(INodeFactory* factory)
+{
+ return std::unique_ptr<ITreeBuilder>(new TTreeBuilder(std::move(factory)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/tree_builder.h b/yt/yt/core/ytree/tree_builder.h
new file mode 100644
index 0000000000..4fcbe25cc9
--- /dev/null
+++ b/yt/yt/core/ytree/tree_builder.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "public.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Reconstructs a YTree from IYsonConsumer calls.
+struct ITreeBuilder
+ : public virtual NYson::IYsonConsumer
+{
+ //! Resets the instance.
+ virtual void BeginTree() = 0;
+
+ //! Returns the root node of the constructed tree.
+ /*!
+ * \note
+ * Must be called after the tree is fully constructed.
+ */
+ virtual INodePtr EndTree() = 0;
+
+
+ //! Enables inserting a pre-existing subtree into
+ //! the currently constructed one.
+ /*!
+ * The given subtree is injected as-is, no cloning is done.
+ */
+ virtual void OnNode(INodePtr node) = 0;
+};
+
+//! Creates a builder that makes explicit calls to the factory.
+/*!
+ * \param factory A factory used for materializing the nodes.
+ */
+std::unique_ptr<ITreeBuilder> CreateBuilderFromFactory(INodeFactory* factory);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/tree_visitor.cpp b/yt/yt/core/ytree/tree_visitor.cpp
new file mode 100644
index 0000000000..8146497257
--- /dev/null
+++ b/yt/yt/core/ytree/tree_visitor.cpp
@@ -0,0 +1,189 @@
+#include "tree_visitor.h"
+#include "helpers.h"
+#include "attributes.h"
+#include "node.h"
+#include "convert.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/yson/producer.h>
+#include <yt/yt/core/yson/async_consumer.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Traverses a YTree and invokes appropriate methods of IYsonConsumer.
+class TTreeVisitor
+ : private TNonCopyable
+{
+public:
+ TTreeVisitor(
+ IAsyncYsonConsumer* consumer,
+ bool stable,
+ const TAttributeFilter& attributeFilter,
+ bool skipEntityMapChildren)
+ : Consumer(consumer)
+ , Stable_(stable)
+ , AttributeFilter_(attributeFilter)
+ , SkipEntityMapChildren(skipEntityMapChildren)
+ { }
+
+ void Visit(const INodePtr& root)
+ {
+ VisitAny(root, true);
+ }
+
+private:
+ IAsyncYsonConsumer* const Consumer;
+ const bool Stable_;
+ const TAttributeFilter AttributeFilter_;
+ const bool SkipEntityMapChildren;
+
+ void VisitAny(const INodePtr& node, bool isRoot = false)
+ {
+ node->WriteAttributes(Consumer, AttributeFilter_, Stable_);
+
+ static const TString opaqueAttributeName("opaque");
+ if (!isRoot &&
+ node->Attributes().Get<bool>(opaqueAttributeName, false))
+ {
+ // This node is opaque, i.e. replaced by entity during tree traversal.
+ Consumer->OnEntity();
+ return;
+ }
+
+ switch (node->GetType()) {
+ case ENodeType::String:
+ case ENodeType::Int64:
+ case ENodeType::Uint64:
+ case ENodeType::Double:
+ case ENodeType::Boolean:
+ VisitScalar(node);
+ break;
+
+ case ENodeType::Entity:
+ VisitEntity(node);
+ break;
+
+ case ENodeType::List:
+ VisitList(node->AsList());
+ break;
+
+ case ENodeType::Map:
+ VisitMap(node->AsMap());
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void VisitScalar(const INodePtr& node)
+ {
+ switch (node->GetType()) {
+ case ENodeType::String:
+ Consumer->OnStringScalar(node->AsString()->GetValue());
+ break;
+
+ case ENodeType::Int64:
+ Consumer->OnInt64Scalar(node->AsInt64()->GetValue());
+ break;
+
+ case ENodeType::Uint64:
+ Consumer->OnUint64Scalar(node->AsUint64()->GetValue());
+ break;
+
+ case ENodeType::Double:
+ Consumer->OnDoubleScalar(node->AsDouble()->GetValue());
+ break;
+
+ case ENodeType::Boolean:
+ Consumer->OnBooleanScalar(node->AsBoolean()->GetValue());
+ break;
+
+ default:
+ YT_ABORT();
+ }
+ }
+
+ void VisitEntity(const INodePtr& /*node*/)
+ {
+ Consumer->OnEntity();
+ }
+
+ void VisitList(const IListNodePtr& node)
+ {
+ Consumer->OnBeginList();
+ for (int i = 0; i < node->GetChildCount(); ++i) {
+ Consumer->OnListItem();
+ VisitAny(node->GetChildOrThrow(i));
+ }
+ Consumer->OnEndList();
+ }
+
+ void VisitMap(const IMapNodePtr& node)
+ {
+ Consumer->OnBeginMap();
+ auto children = node->GetChildren();
+ if (Stable_) {
+ typedef std::pair<TString, INodePtr> TPair;
+ std::sort(
+ children.begin(),
+ children.end(),
+ [] (const TPair& lhs, const TPair& rhs) {
+ return lhs.first < rhs.first;
+ });
+ }
+ for (const auto& [key, child] : children) {
+ if (SkipEntityMapChildren && child->GetType() == ENodeType::Entity) {
+ continue;
+ }
+ Consumer->OnKeyedItem(key);
+ VisitAny(child);
+ }
+ Consumer->OnEndMap();
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void VisitTree(
+ INodePtr root,
+ IYsonConsumer* consumer,
+ bool stable,
+ const TAttributeFilter& attributeFilter,
+ bool skipEntityMapChildren)
+{
+ TAsyncYsonConsumerAdapter adapter(consumer);
+ VisitTree(
+ std::move(root),
+ &adapter,
+ stable,
+ attributeFilter,
+ skipEntityMapChildren);
+}
+
+void VisitTree(
+ INodePtr root,
+ IAsyncYsonConsumer* consumer,
+ bool stable,
+ const TAttributeFilter& attributeFilter,
+ bool skipEntityMapChildren)
+{
+ TTreeVisitor treeVisitor(
+ consumer,
+ stable,
+ attributeFilter,
+ skipEntityMapChildren);
+ treeVisitor.Visit(root);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/tree_visitor.h b/yt/yt/core/ytree/tree_visitor.h
new file mode 100644
index 0000000000..ea960a574c
--- /dev/null
+++ b/yt/yt/core/ytree/tree_visitor.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "ypath_service.h"
+
+#include "attribute_filter.h"
+
+#include <yt/yt/core/yson/public.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void VisitTree(
+ INodePtr root,
+ NYson::IYsonConsumer* consumer,
+ bool stable,
+ const TAttributeFilter& attributeFilter = {},
+ bool skipEntityMapChildren = false);
+
+void VisitTree(
+ INodePtr root,
+ NYson::IAsyncYsonConsumer* consumer,
+ bool stable,
+ const TAttributeFilter& attributeFilter = {},
+ bool skipEntityMapChildren = false);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/attribute_filter_ut.cpp b/yt/yt/core/ytree/unittests/attribute_filter_ut.cpp
new file mode 100644
index 0000000000..b2f8573e97
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/attribute_filter_ut.cpp
@@ -0,0 +1,92 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/attribute_filter.h>
+
+namespace NYT::NYTree {
+
+using namespace NYPath;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TAttributeFilterTest, TestValidation)
+{
+ auto expectValidationError = [] (const TYPath& path) {
+ TAttributeFilter filter({}, {path});
+ EXPECT_THROW_WITH_SUBSTRING(filter.Normalize(), "Error validating attribute path");
+ };
+ expectValidationError("");
+ expectValidationError("/");
+ expectValidationError("/foo/");
+ expectValidationError("/foo/@");
+}
+
+TEST(TAttributeFilterTest, TestNormalization)
+{
+ using TKeyToFilter = TAttributeFilter::TKeyToFilter;
+ using TPathFilter = TAttributeFilter::TPathFilter;
+
+ // Old-fashioned attribute key filtering; done via fast path.
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter{}}, {"bar", TPathFilter{}}}),
+ TAttributeFilter({"foo", "bar"}).Normalize());
+
+ // Repeated attribute names must be uniquified (yes, this is an actual word).
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({})}}),
+ TAttributeFilter({"foo", "foo"}).Normalize());
+
+ // "/foo/bar" should form its own group.
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({"/bar"})}}),
+ TAttributeFilter({}, {"/foo/bar"}).Normalize());
+
+ // "/foo/{bar,baz}" is also a single group.
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({"/bar", "/baz"})}}),
+ TAttributeFilter({}, {"/foo/bar", "/foo/baz"}).Normalize());
+
+ // "/foo/bar/baz" is discarded by "/foo/bar".
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({"/bar"})}}),
+ TAttributeFilter({}, {"/foo/bar/baz", "/foo/bar"}).Normalize());
+
+ // "/foo/bar" is discarded by the key "foo".
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({""})}}),
+ TAttributeFilter({"foo"}, {"/foo/bar"}).Normalize());
+
+ // Check that integral literals are ok.
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({"/1", "/3/bar"})}}),
+ TAttributeFilter({}, {"/foo/1", "/foo/3/bar"}).Normalize());
+
+ // Make sure foo and foobar do not discard each other.
+ EXPECT_EQ(
+ (TKeyToFilter{{"foo", TPathFilter({""})}, {"foobar", TPathFilter({""})}}),
+ TAttributeFilter({}, {"/foo", "/foobar"}).Normalize());
+
+ EXPECT_EQ(
+ (TKeyToFilter{
+ {"yp", TPathFilter({"/rex"})},
+ {"yql", TPathFilter({""})},
+ {"yt", TPathFilter({"/yt/core", "/yt/server/master", "/yt/server/node"})},
+ }),
+ TAttributeFilter(
+ {"yql"}, {
+ "/yp/rex",
+ "/yt/yt/core",
+ "/yt/yt/server/master",
+ "/yt/yt/server/node",
+ "/yt/yt/server/node/data_node"
+ }).Normalize());
+
+ // A tricky case requiring normalization of paths. Note that "yp" and "y\\x70" coincide
+ // as YPath literals, but as strings "yp" < "yt" < "y\\x70".
+ EXPECT_EQ(
+ (TKeyToFilter{{"yp", TPathFilter({"/foo"})}, {"yt", TPathFilter({"/bar"})}}),
+ TAttributeFilter({}, {"/yp/foo", "/y\\x70/foo/qux", "/yt/bar"}).Normalize());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/attributes_ut.cpp b/yt/yt/core/ytree/unittests/attributes_ut.cpp
new file mode 100644
index 0000000000..b53fafdaf6
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/attributes_ut.cpp
@@ -0,0 +1,111 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/attributes.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/yson/string.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TAttributesTest
+ : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ { }
+
+ void TearDown() override
+ { }
+};
+
+TEST(TAttributesTest, CheckAccessors)
+{
+ auto attributes = CreateEphemeralAttributes();
+ attributes->Set<TString>("name", "Petr");
+ attributes->Set<int>("age", 30);
+ attributes->Set<double>("weight", 70.5);
+
+ auto keys_ = attributes->ListKeys();
+ THashSet<TString> keys(keys_.begin(), keys_.end());
+ THashSet<TString> expectedKeys{
+ "name",
+ "age",
+ "weight",
+ };
+ EXPECT_EQ(keys, expectedKeys);
+
+ auto pairs_ = attributes->ListPairs();
+ THashSet<IAttributeDictionary::TKeyValuePair> pairs(pairs_.begin(), pairs_.end());
+ THashSet<IAttributeDictionary::TKeyValuePair> expectedPairs{
+ {"name", ConvertToYsonString("Petr")},
+ {"age", ConvertToYsonString(30)},
+ {"weight", ConvertToYsonString(70.5)},
+ };
+ EXPECT_EQ(pairs, expectedPairs);
+
+ EXPECT_EQ("Petr", attributes->Get<TString>("name"));
+ EXPECT_THROW(attributes->Get<int>("name"), std::exception);
+
+ EXPECT_EQ(30, attributes->Find<int>("age"));
+ EXPECT_EQ(30, attributes->Get<int>("age"));
+ EXPECT_THROW(attributes->Get<char>("age"), std::exception);
+
+ EXPECT_EQ(70.5, attributes->Get<double>("weight"));
+ EXPECT_THROW(attributes->Get<TString>("weight"), std::exception);
+
+ EXPECT_FALSE(attributes->Find<int>("unknown_key"));
+ EXPECT_EQ(42, attributes->Get<int>("unknown_key", 42));
+ EXPECT_THROW(attributes->Get<double>("unknown_key"), std::exception);
+}
+
+TEST(TAttributesTest, MergeFromTest)
+{
+ auto attributesX = CreateEphemeralAttributes();
+ attributesX->Set<TString>("name", "Petr");
+ attributesX->Set<int>("age", 30);
+
+ auto attributesY = CreateEphemeralAttributes();
+ attributesY->Set<TString>("name", "Oleg");
+
+ attributesX->MergeFrom(*attributesY);
+ EXPECT_EQ("Oleg", attributesX->Get<TString>("name"));
+ EXPECT_EQ(30, attributesX->Get<int>("age"));
+
+ auto node = ConvertToNode(TYsonString(TStringBuf("{age=20}")));
+ attributesX->MergeFrom(node->AsMap());
+ EXPECT_EQ("Oleg", attributesX->Get<TString>("name"));
+ EXPECT_EQ(20, attributesX->Get<int>("age"));
+}
+
+TEST(TAttributesTest, SerializeToNode)
+{
+ auto attributes = CreateEphemeralAttributes();
+ attributes->Set<TString>("name", "Petr");
+ attributes->Set<int>("age", 30);
+
+ auto node = ConvertToNode(*attributes);
+ auto convertedAttributes = ConvertToAttributes(node);
+ EXPECT_EQ(*attributes, *convertedAttributes);
+}
+
+TEST(TAttributesTest, TrySerializeProtoToRef)
+{
+ auto attributes = CreateEphemeralAttributes();
+ attributes->Set<TString>("name", "Petr");
+ attributes->Set<int>("age", 30);
+
+ NProto::TAttributeDictionary protoAttributes;
+ ToProto(&protoAttributes, *attributes);
+ auto convertedAttributes = FromProto(protoAttributes);
+ EXPECT_EQ(*attributes, *convertedAttributes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/proto/test.proto b/yt/yt/core/ytree/unittests/proto/test.proto
new file mode 100644
index 0000000000..5a3b74f174
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/proto/test.proto
@@ -0,0 +1,7 @@
+package NYT.NYTree.NProto;
+
+message TTestMessage
+{
+ optional int32 int32_field = 1;
+ optional string string_field = 2;
+}
diff --git a/yt/yt/core/ytree/unittests/resolver_ut.cpp b/yt/yt/core/ytree/unittests/resolver_ut.cpp
new file mode 100644
index 0000000000..2e1e0806e0
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/resolver_ut.cpp
@@ -0,0 +1,124 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/ypath_resolver.h>
+
+namespace NYT::NYTree {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define EXPECT_NULL(optional) \
+ EXPECT_FALSE(optional)
+
+#define EXPECT_VALUE(value, optional) \
+ do { \
+ EXPECT_TRUE(optional); \
+ if (optional) { \
+ EXPECT_EQ(value, *optional); \
+ } \
+ } while (false)
+
+TEST(TYPathResolver, GetInt64)
+{
+ EXPECT_VALUE(2, TryGetInt64("{key3=2;key4=5}", "/key3"));
+ EXPECT_VALUE(5, TryGetInt64("{key3=2;key4=5}", "/key4"));
+ EXPECT_NULL(TryGetInt64("{key3=2;key4=5}", "/key6"));
+ EXPECT_VALUE(5, TryGetInt64("5", ""));
+ EXPECT_VALUE(10, TryGetInt64("{key3={k=2;a={b=3}};k={k2=3;k3=10}}", "/k/k3"));
+ EXPECT_VALUE(2, TryGetInt64("{l=[0;1];key3=2;k={k2=3;k3=10}}", "/key3"));
+ EXPECT_VALUE(3, TryGetInt64("{key3=2;k={k2=3;k3=10}}", "/k/k2"));
+ EXPECT_VALUE(1, TryGetInt64("{key3=2;l=[1;2];k={k2=3;k3=10};lst=[0;1;2]}", "/lst/1"));
+ EXPECT_VALUE(4, TryGetInt64("<attr=4>{key3=2;k={k2=3;k3=10};lst=[0;1;2]}", "/@attr"));
+ EXPECT_VALUE(7, TryGetInt64("<attr=4>{key3=2;k=<a=6>{k2=<a=7>3;k3=10};lst=[0;1;2]}", "/k/k2/@a"));
+ EXPECT_VALUE(4, TryGetInt64("<attr=4>{key3=2;k={k2=<b=7>3;k3=10};lst=[0;1;<a={b=4}>2]}", "/lst/2/@a/b"));
+ EXPECT_VALUE(7, TryGetInt64("<attr=4>{key3=2;k={k2=<b=7>3;k3=10};lst=<a=[1;{a=3};{b=7}]>[0;1;<a={b=4}>2]}", "/lst/@a/2/b"));
+ EXPECT_NULL(TryGetInt64("<attr=4>{key3=2;k={k2=<b=7>3;k3=10};lst=<a=[1;{a=3};{b=7}]>[0;1;<a={b=4}>2]}", "/@a"));
+ EXPECT_NULL(TryGetInt64("<attr=4>{key3=2;k={k2=<b=7>3;k3=10};lst=<a=[1;{a=3};{b=7}]>[0;1;<a={b=4}>2]}", "/key3/k"));
+ EXPECT_VALUE(2, TryGetInt64("{key3=2u;key4=5}", "/key3"));
+ EXPECT_NULL(TryGetInt64("{key3=9223372036854775900u;key4=5}", "/key3"));
+}
+
+TEST(TYPathResolver, GetUint64)
+{
+ EXPECT_VALUE(2u, TryGetUint64("{key3=2u;key4=5}", "/key3"));
+ EXPECT_VALUE(2u, TryGetUint64("{key3=2;key4=5}", "/key3"));
+ EXPECT_VALUE(5u, TryGetUint64("{key3=2;key4=5u}", "/key4"));
+ EXPECT_NULL(TryGetUint64("{key3=2;key4=5}", "/key6"));
+ EXPECT_VALUE(7u, TryGetUint64("<attr=4>{key3=2;k={k2=<b=7>3;k3=10};lst=<a=[1;{a=3};{b=7u}]>[0;1;<a={b=4}>2]}", "/lst/@a/2/b"));
+ EXPECT_NULL(TryGetUint64("<attr=4>{key3=\"2\";k={k2=<b=7>3;k3=10};lst=<a=[1;{a=3};{b=7}]>[0;1;<a={b=4}>2]}", "/key3"));
+ EXPECT_VALUE(2u, TryGetUint64("{key3=2;key4=5}", "/key3"));
+ EXPECT_NULL(TryGetUint64("{key3=-10;key4=5}", "/key3"));
+}
+
+TEST(TYPathResolver, GetBoolean)
+{
+ EXPECT_VALUE(true, TryGetBoolean("{key3=2;key4=%true}", "/key4"));
+ EXPECT_VALUE(false, TryGetBoolean("{key3=%false;key4=%true}", "/key3"));
+ EXPECT_NULL(TryGetBoolean("{key3=%false;key4=%true}", "/key5"));
+}
+
+TEST(TYPathResolver, GetDouble)
+{
+ EXPECT_VALUE(7., TryGetDouble("{key3=2;key4=7.}", "/key4"));
+ EXPECT_VALUE(2., TryGetDouble("{key3=2;key4=7.}", "/key3"));
+ EXPECT_NULL(TryGetDouble("{key3=2;key4=7.}", "/key2"));
+ EXPECT_VALUE(2., TryGetDouble("{key3=2u;key4=5}", "/key3"));
+}
+
+TEST(TYPathResolver, GetString)
+{
+ EXPECT_VALUE("s", TryGetString("{key3=2;key4=\"s\"}", "/key4"));
+ EXPECT_NULL(TryGetString("{key3=2;key4=\"s\"}", "/key3"));
+ EXPECT_NULL(TryGetString("{key3=2;key4=\"s\"}", "/key2"));
+}
+
+TEST(TYPathResolver, GetAny)
+{
+ EXPECT_VALUE("\1\2s", TryGetAny("{key3=2;key4=\"s\"}", "/key4"));
+ EXPECT_VALUE("\2\4", TryGetAny("{key3=2;key4=\"s\"}", "/key3"));
+ EXPECT_VALUE("{\1\2a=\2\n;\1\2b=[\1\nhello;\2\xF2\x14]}", TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key5"));
+ EXPECT_VALUE("[\1\nhello;\2\xF2\x14]", TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key5/b"));
+ EXPECT_VALUE("\2\xF2\x14", TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key5/b/1"));
+ EXPECT_NULL(TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key5/b/2"));
+ EXPECT_NULL(TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key5/c/1"));
+ EXPECT_NULL(TryGetAny("{key3=2;key5={a=5;b=[\"hello\"; 1337]};key4=\"s\"}", "/key6/b/1"));
+}
+
+TEST(TYPathResolver, Attribute)
+{
+ EXPECT_VALUE("x", TryGetString("{key=1;value=x;}", "/value"));
+ EXPECT_VALUE("x", TryGetString("{key=<attr=c>1;value=x;}", "/value"));
+ EXPECT_VALUE("x", TryGetString("[<attr=a>1;2;x]", "/2"));
+ EXPECT_VALUE(1, TryGetInt64("[<attr=a>1;2;x]", "/0"));
+}
+
+TEST(TYPathResolver, InvalidYPath)
+{
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "//"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "/key3/"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "/@@"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "@"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "/"), std::exception);
+ EXPECT_THROW(TryGetInt64("<x=2>{key3=2;key4=5}", "/@x/"), std::exception);
+ EXPECT_THROW(TryGetInt64("<x=2>{key3=2;key4=5}", "/@x//"), std::exception);
+ EXPECT_THROW(TryGetInt64("<x={sdf=2}>{key3=2;key4=5}", "/@x/sdf@"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "dfsdf"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5}", "@dfsdf"), std::exception);
+}
+
+TEST(TYPathResolver, InvalidYson)
+{
+ EXPECT_THROW(TryGetInt64("{key3=2;key4=5", "/k"), std::exception);
+ EXPECT_THROW(TryGetInt64("{key3=2key4=5}", "/k"), std::exception);
+ EXPECT_THROW(TryGetAny("{key3=2;key4=5", "/k"), std::exception);
+ EXPECT_THROW(TryGetAny("{key3=2key4=5}", "/k"), std::exception);
+ EXPECT_THROW(TryGetAny("", "/key"), std::exception);
+ EXPECT_THROW(TryGetAny(">", "/key"), std::exception);
+ EXPECT_THROW(TryGetAny("}", "/key"), std::exception);
+ EXPECT_THROW(TryGetAny("]", "/key"), std::exception);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/serialize_ut.cpp b/yt/yt/core/ytree/unittests/serialize_ut.cpp
new file mode 100644
index 0000000000..d0a3f4b733
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/serialize_ut.cpp
@@ -0,0 +1,538 @@
+#include "serialize_ut.h"
+
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/yson/pull_parser_deserialize.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/serialize.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+
+#include <yt/yt/core/ytree/unittests/proto/test.pb.h>
+
+#include <library/cpp/yt/misc/arcadia_enum.h>
+
+#include <array>
+
+namespace NYT::NYTree {
+
+namespace NProto {
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool operator==(const TTestMessage& l, const TTestMessage& r)
+{
+ return l.int32_field() == r.int32_field() && l.string_field() == r.string_field();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NProto
+
+YT_DEFINE_ARCADIA_ENUM_TRAITS(ESerializableArcadiaEnum)
+
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETestEnum,
+ (One)
+ (FortyTwo)
+);
+
+DEFINE_BIT_ENUM(ETestBitEnum,
+ ((Red) (0x0001))
+ ((Yellow) (0x0002))
+ ((Green) (0x0004))
+);
+
+template <typename T>
+T PullParserConvert(TYsonStringBuf s)
+{
+ TMemoryInput input(s.AsStringBuf());
+ TYsonPullParser parser(&input, s.GetType());
+ TYsonPullParserCursor cursor(&parser);
+ T result;
+ Deserialize(result, &cursor);
+ EXPECT_EQ(cursor->GetType(), EYsonItemType::EndOfStream);
+ return result;
+}
+
+template <typename TOriginal, typename TResult = TOriginal>
+void TestSerializationDeserializationPullParser(const TOriginal& original, EYsonType ysonType = EYsonType::Node)
+{
+ auto yson = ConvertToYsonString(original);
+ if (ysonType != EYsonType::Node) {
+ auto buf = yson.AsStringBuf();
+ yson = TYsonString(buf.SubString(1, buf.Size() - 2), ysonType);
+ }
+ auto deserialized = PullParserConvert<TResult>(yson);
+ EXPECT_EQ(original, deserialized);
+ auto node = ConvertTo<INodePtr>(original);
+ node->MutableAttributes()->Set("some_attribute", 14);
+ auto ysonWithAttribute = ConvertToYsonString(node);
+ auto deserializedWithAttribute = PullParserConvert<TResult>(ysonWithAttribute);
+ EXPECT_EQ(original, deserializedWithAttribute);
+}
+
+template <typename TOriginal, typename TResult = TOriginal>
+void TestSerializationDeserializationNode(const TOriginal& original)
+{
+ auto node = ConvertToNode(original);
+ auto deserialized = ConvertTo<TResult>(node);
+ EXPECT_EQ(original, deserialized);
+}
+
+template <typename TOriginal, typename TResult = TOriginal>
+void TestSerializationDeserialization(const TOriginal& original, EYsonType /*ysonType*/ = EYsonType::Node)
+{
+ //TestSerializationDeserializationPullParser<TOriginal, TResult>(original, ysonType);
+ TestSerializationDeserializationNode<TOriginal, TResult>(original);
+}
+
+template <typename TResult, typename TSource>
+void TestDeserialization(const TResult& expected, const TSource& source)
+{
+ auto yson = ConvertToYsonString(source);
+ auto node = ConvertTo<INodePtr>(yson);
+ EXPECT_EQ(expected, PullParserConvert<TResult>(yson));
+ EXPECT_EQ(expected, ConvertTo<TResult>(node));
+}
+
+TString RemoveSpaces(const TString& str)
+{
+ TString res = str;
+ while (true) {
+ size_t pos = res.find(" ");
+ if (pos == TString::npos) {
+ break;
+ }
+ res.replace(pos, 1, "");
+ }
+ return res;
+}
+
+TEST(TYTreeSerializationTest, All)
+{
+ TYsonString canonicalYson(TStringBuf(
+ "<\"acl\"={\"execute\"=[\"*\";];};>"
+ "{\"mode\"=755;\"path\"=\"/home/sandello\";}"));
+ auto root = ConvertToNode(canonicalYson);
+ auto deserializedYson = ConvertToYsonString(root, NYson::EYsonFormat::Text);
+ EXPECT_EQ(RemoveSpaces(canonicalYson.ToString()), deserializedYson.ToString());
+}
+
+TEST(TCustomTypeSerializationTest, TInstant)
+{
+ {
+ TInstant value = TInstant::MilliSeconds(100500);
+ TestSerializationDeserialization(value);
+ TestDeserialization<TInstant, double>(value, 100500.);
+ TestDeserialization<TInstant, i64>(value, 100500);
+ TestDeserialization<TInstant, ui64>(value, 100500U);
+ TestDeserialization<TInstant, TString>(value, "1970-01-01T00:01:40.500000");
+ }
+ {
+ TDuration value = TDuration::MilliSeconds(100500);
+ TestSerializationDeserialization(value);
+ TestDeserialization<TDuration, double>(value, 100500.);
+ TestDeserialization<TDuration, i64>(value, 100500);
+ TestDeserialization<TDuration, ui64>(value, 100500U);
+ TestDeserialization<TDuration, TString>(value, "100.5s");
+ }
+}
+
+TEST(TCustomTypeSerializationTest, Optional)
+{
+ {
+ std::optional<int> value(10);
+ auto yson = ConvertToYsonString(value);
+ EXPECT_EQ(10, ConvertTo<std::optional<int>>(yson));
+ TestSerializationDeserialization(value);
+ }
+ {
+ std::optional<int> value;
+ auto yson = ConvertToYsonString(value);
+ EXPECT_EQ(TString("#"), yson.ToString());
+ EXPECT_EQ(value, ConvertTo<std::optional<int>>(yson));
+ TestSerializationDeserialization(value);
+ }
+}
+
+TEST(TSerializationTest, Simple)
+{
+ {
+ signed char value = -127;
+ TestSerializationDeserialization(value);
+ }
+ {
+ unsigned char value = 255;
+ TestSerializationDeserialization(value);
+ }
+ {
+ short value = -30'000;
+ TestSerializationDeserialization(value);
+ }
+ {
+ unsigned short value = 65'535;
+ TestSerializationDeserialization(value);
+ }
+ {
+ int value = -2'000'000;
+ TestSerializationDeserialization(value);
+ }
+ {
+ unsigned value = 4'000'000;
+ TestSerializationDeserialization(value);
+ }
+ {
+ long value = -1'999'999;
+ TestSerializationDeserialization(value);
+ }
+ {
+ unsigned long value = 3'999'999;
+ TestSerializationDeserialization(value);
+ }
+ {
+ long long value = -8'000'000'000'000LL;
+ TestSerializationDeserialization(value);
+ TestDeserialization<long long int, unsigned long long int>(
+ 1000000,
+ 1000000U);
+ }
+ {
+ unsigned long long value = 16'000'000'000'000uLL;
+ TestSerializationDeserialization(value);
+ }
+
+ {
+ double value = 2.7182818284590452353602874713527e12;
+ TestSerializationDeserialization(value);
+ TestDeserialization<double, i64>(1.0, 1);
+ TestDeserialization<double, ui64>(1.0, 1U);
+ }
+
+ {
+ TString value = "abacaba";
+ TestSerializationDeserialization(value);
+ }
+
+ {
+ bool value = true;
+ TestSerializationDeserialization(value);
+ value = false;
+ TestSerializationDeserialization(value);
+
+ TestDeserialization(true, TString("true"));
+ TestDeserialization(false, TString("false"));
+
+ TestDeserialization(false, i64(0));
+ TestDeserialization(true, i64(1));
+ TestDeserialization(false, ui64(0));
+ TestDeserialization(true, ui64(1));
+
+ EXPECT_THROW(ConvertTo<bool>(ConvertTo<INodePtr>(i64(-1))), std::exception);
+ EXPECT_THROW(ConvertTo<bool>(ConvertTo<INodePtr>(ui64(12))), std::exception);
+ EXPECT_THROW(PullParserConvert<bool>(ConvertToYsonString(i64(-1))), std::exception);
+ EXPECT_THROW(PullParserConvert<bool>(ConvertToYsonString(ui64(12U))), std::exception);
+ }
+
+ {
+ char value = 'a';
+ TestSerializationDeserialization(value);
+ value = 'Z';
+ TestSerializationDeserialization(value);
+ }
+}
+
+
+TEST(TSerializationTest, PackRefs)
+{
+ std::vector<TSharedRef> refs;
+ refs.push_back(TSharedRef::FromString("abc"));
+ refs.push_back(TSharedRef::FromString("12"));
+
+ auto packed = PackRefs(refs);
+ auto unpacked = UnpackRefs(packed);
+
+ EXPECT_EQ(2u, unpacked.size());
+ EXPECT_EQ("abc", ToString(unpacked[0]));
+ EXPECT_EQ("12", ToString(unpacked[1]));
+}
+
+TEST(TSerializationTest, Map)
+{
+ std::map<TString, size_t> original{{"First", 12U}, {"Second", 7883U}, {"Third", 7U}};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::MapFragment);
+}
+
+TEST(TSerializationTest, Set)
+{
+ std::set<TString> original{"First", "Second", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, MultiSet)
+{
+ std::multiset<TString> original{"First", "Second", "Third", "Second", "Third", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, MultiMap)
+{
+ std::multimap<TString, size_t> original{{"First", 12U}, {"Second", 7883U}, {"Third", 7U}};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::MapFragment);
+}
+
+TEST(TSerializationTest, MultiMapErrorDuplicateKey)
+{
+ std::multimap<TString, size_t> original{{"First", 12U}, {"Second", 7883U}, {"First", 2U}, {"Second", 3U}};
+ auto yson = ConvertToYsonString(original);
+ EXPECT_THROW(ConvertTo<std::decay<decltype(original)>::type>(yson), std::exception);
+}
+
+TEST(TSerializationTest, UnorderedMap)
+{
+ std::unordered_map<TString, size_t> original{{"First", 12U}, {"Second", 7883U}, {"Third", 7U}};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::MapFragment);
+}
+
+TEST(TSerializationTest, UnorderedSet)
+{
+ const std::unordered_set<TString> original{"First", "Second", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, UnorderedMultiSet)
+{
+ const std::unordered_multiset<TString> original{"First", "Second", "Third", "Second", "Third", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, UnorderedMultiMap)
+{
+ const std::unordered_multimap<TString, size_t> original{{"First", 12U}, {"Second", 7883U}, {"Third", 7U}};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::MapFragment);
+}
+
+TEST(TSerializationTest, UnorderedMultiMapErrorDuplicateKey)
+{
+ const std::unordered_multimap<TString, size_t> original{{"Second", 7883U}, {"Third", 7U}, {"Second", 7U}};
+ auto yson = ConvertToYsonString(original);
+ EXPECT_THROW(ConvertTo<std::decay<decltype(original)>::type>(yson), std::exception);
+}
+
+TEST(TSerializationTest, Vector)
+{
+ const std::vector<TString> original{"First", "Second", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, Deque)
+{
+ const std::deque<TString> original{"First", "Second", "Third"};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, Pair)
+{
+ auto original = std::make_pair<size_t, TString>(1U, "Second");
+ TestSerializationDeserialization(original);
+}
+
+TEST(TSerializationTest, Atomic)
+{
+ std::atomic<size_t> original(42U);
+ TestSerializationDeserialization<std::atomic<size_t>, size_t>(original);
+}
+
+TEST(TSerializationTest, Array)
+{
+ std::array<TString, 4> original{{"One", "Two", "3", "4"}};
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, Tuple)
+{
+ auto original = std::make_tuple<int, TString, size_t>(43, "TString", 343U);
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, VectorOfTuple)
+{
+ std::vector<std::tuple<int, TString, size_t>> original{
+ std::make_tuple<int, TString, size_t>(43, "First", 343U),
+ std::make_tuple<int, TString, size_t>(0, "Second", 7U),
+ std::make_tuple<int, TString, size_t>(2323, "Third", 9U)
+ };
+
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::ListFragment);
+}
+
+TEST(TSerializationTest, MapOnArray)
+{
+ std::map<TString, std::array<size_t, 3>> original{
+ {"1", {{2112U, 4343U, 5445U}}},
+ {"22", {{54654U, 93U, 5U}}},
+ {"333", {{7U, 93U, 9U}}},
+ {"rel", {{233U, 9763U, 0U}}}
+ };
+ TestSerializationDeserialization(original);
+ TestSerializationDeserialization(original, EYsonType::MapFragment);
+}
+
+TEST(TSerializationTest, Enum)
+{
+ for (const auto original : TEnumTraits<ETestEnum>::GetDomainValues()) {
+ TestSerializationDeserialization(original);
+ }
+}
+
+TEST(TSerializationTest, EnumUnknownValue)
+{
+ auto unknownValue = static_cast<ETestEnum>(ToUnderlying(TEnumTraits<ETestEnum>::GetMaxValue()) + 1);
+ TestSerializationDeserialization(unknownValue);
+}
+
+TEST(TSerializationTest, BitEnum)
+{
+ for (const auto original : TEnumTraits<ETestBitEnum>::GetDomainValues()) {
+ TestSerializationDeserialization(original);
+ }
+ TestSerializationDeserialization(ETestBitEnum::Green | ETestBitEnum::Red);
+ TestSerializationDeserialization(ETestBitEnum::Green | ETestBitEnum::Red | ETestBitEnum::Yellow);
+}
+
+TEST(TSerializationTest, SerializableArcadiaEnum)
+{
+ for (const auto original : GetEnumAllValues<ESerializableArcadiaEnum>()) {
+ TestSerializationDeserialization(original);
+ }
+}
+
+TEST(TYTreeSerializationTest, Protobuf)
+{
+ NProto::TTestMessage message;
+ message.set_int32_field(1);
+ message.set_string_field("test");
+ TestSerializationDeserializationNode(message);
+}
+
+TEST(TYTreeSerializationTest, ProtobufKeepUnknown)
+{
+ TYsonString canonicalYson(TStringBuf("{\"int32_field\"=1;\"string_field\"=\"test\";\"unknown_field\"=1;}"));
+ {
+ auto node = ConvertTo<NProto::TTestMessage>(canonicalYson);
+ auto deserializedYson = ConvertToYsonString(node, NYson::EYsonFormat::Text);
+ EXPECT_EQ(RemoveSpaces(canonicalYson.ToString()), deserializedYson.ToString());
+ }
+ {
+ auto node = PullParserConvert<NProto::TTestMessage>(canonicalYson);
+ auto deserializedYson = ConvertToYsonString(node, NYson::EYsonFormat::Text);
+ EXPECT_EQ(RemoveSpaces(canonicalYson.ToString()), deserializedYson.ToString());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestClass
+{
+public:
+ INodePtr Attributes;
+ int Field = 123;
+};
+
+class TTestClassRC
+ : public TRefCounted
+ , public TTestClass
+{ };
+
+DECLARE_REFCOUNTED_CLASS(TTestClassRC)
+DEFINE_REFCOUNTED_TYPE(TTestClassRC)
+
+void Deserialize(TTestClass& value, const INodePtr& node)
+{
+ value.Attributes = node->Attributes().ToMap();
+ value.Field = node->GetValue<i64>();
+}
+
+void Deserialize(TTestClass& value, TYsonPullParserCursor* cursor)
+{
+ Deserialize(value, ExtractTo<INodePtr>(cursor));
+}
+
+TEST(TSerializationTest, Pointers)
+{
+ TYsonString yson(TStringBuf("<x=12;y=#>42"));
+
+ auto expectedNode = BuildYsonNodeFluently().BeginMap()
+ .Item("x").Value(12)
+ .Item("y").Entity()
+ .EndMap();
+
+ auto check = [&] (TStringBuf message, const auto& ptr) {
+ SCOPED_TRACE(message);
+ ASSERT_TRUE(ptr);
+ ASSERT_EQ(ptr->Field, 42);
+ ASSERT_TRUE(AreNodesEqual(ptr->Attributes, expectedNode));
+ };
+
+ auto uniqueFromNode = ConvertTo<std::unique_ptr<TTestClass>>(ConvertToNode(yson));
+ check("unique_ptr:FromNode", uniqueFromNode);
+
+ auto uniqueFromPullParser = PullParserConvert<std::unique_ptr<TTestClass>>(yson);
+ check("unique_ptr:FromNode", uniqueFromPullParser);
+
+ auto intrusiveFromNode = ConvertTo<TTestClassRCPtr>(ConvertToNode(yson));
+ check("intrusive:FromNode", intrusiveFromNode);
+
+ auto intrusiveFromPullParser = PullParserConvert<TTestClassRCPtr>(yson);
+ check("intrusive:FromNode", intrusiveFromPullParser);
+}
+
+TEST(TSerializationTest, PointersFromEntity)
+{
+ TYsonString yson(TStringBuf("<x=12;y=#>#"));
+
+ auto check = [&] (TStringBuf message, const auto& ptr) {
+ SCOPED_TRACE(message);
+ ASSERT_TRUE(ptr);
+ ASSERT_EQ(ptr->Field, 123);
+ ASSERT_EQ(ptr->Attributes, nullptr);
+ };
+
+ auto uniqueFromNode = ConvertTo<std::unique_ptr<TTestClass>>(ConvertToNode(yson));
+ check("unique_ptr:FromNode", uniqueFromNode);
+
+ auto uniqueFromPullParser = PullParserConvert<std::unique_ptr<TTestClass>>(yson);
+ check("unique_ptr:FromNode", uniqueFromPullParser);
+
+ auto intrusiveFromNode = ConvertTo<TTestClassRCPtr>(ConvertToNode(yson));
+ check("intrusive:FromNode", intrusiveFromNode);
+
+ auto intrusiveFromPullParser = PullParserConvert<TTestClassRCPtr>(yson);
+ check("intrusive:FromNode", intrusiveFromPullParser);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/serialize_ut.h b/yt/yt/core/ytree/unittests/serialize_ut.h
new file mode 100644
index 0000000000..c5a3884289
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/serialize_ut.h
@@ -0,0 +1,16 @@
+#pragma once
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+enum class ESerializableArcadiaEnum
+{
+ First,
+ Second,
+ Third,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/service_combiner_ut.cpp b/yt/yt/core/ytree/unittests/service_combiner_ut.cpp
new file mode 100644
index 0000000000..5a0f03eb6d
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/service_combiner_ut.cpp
@@ -0,0 +1,198 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/service_combiner.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_proxy.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TString> YPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit = {})
+{
+ return AsyncYPathList(service, path, limit)
+ .Get()
+ .ValueOrThrow();
+}
+
+bool YPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path)
+{
+ return AsyncYPathExists(service, path)
+ .Get()
+ .ValueOrThrow();
+}
+
+TYsonString YPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter = {})
+{
+ return AsyncYPathGet(service, path, attributeFilter)
+ .Get()
+ .ValueOrThrow();
+}
+
+void YPathSet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TYsonString& value,
+ bool recursive = false)
+{
+ AsyncYPathSet(service, path, value, recursive)
+ .Get()
+ .ThrowOnError();
+}
+
+void YPathRemove(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ bool recursive = false,
+ bool force = false)
+{
+ AsyncYPathRemove(service, path, recursive, force)
+ .Get()
+ .ThrowOnError();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYPathServiceCombinerTest, Simple)
+{
+ IYPathServicePtr service1 = IYPathService::FromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2").BeginAttributes()
+ .Item("opaque").Value(true)
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("abc")
+ .Item("subkey2").Value(3.1415926)
+ .EndMap()
+ .EndMap();
+ }));
+ IYPathServicePtr service2 = IYPathService::FromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key3").Value(-1)
+ .Item("key4").BeginAttributes()
+ .Item("attribute1").Value(-1)
+ .EndAttributes().Entity()
+ .EndMap();
+ }));
+ auto combinedService = New<TServiceCombiner>(std::vector<IYPathServicePtr> { service1, service2 });
+
+ EXPECT_EQ(true, YPathExists(combinedService, ""));
+ EXPECT_THROW(YPathExists(combinedService, "/"), std::exception);
+ EXPECT_EQ(false, YPathExists(combinedService, "/keyNonExistent"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/key3"));
+ EXPECT_EQ(false, YPathExists(combinedService, "/key2/subkeyNonExistent"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/key2/subkey1"));
+ EXPECT_EQ((std::vector<TString> { "key1", "key2", "key3", "key4" }), YPathList(combinedService, ""));
+ EXPECT_EQ((std::vector<TString> { "subkey1", "subkey2" }), YPathList(combinedService, "/key2"));
+ EXPECT_THROW(YPathList(combinedService, "/keyNonExistent"), std::exception);
+ EXPECT_EQ(ConvertToYsonString(-1, EYsonFormat::Binary), YPathGet(combinedService, "/key4/@attribute1"));
+ EXPECT_EQ(ConvertToYsonString("abc", EYsonFormat::Binary), YPathGet(combinedService, "/key2/subkey1"));
+ EXPECT_THROW(YPathGet(combinedService, "/"), std::exception);
+}
+
+TEST(TYPathServiceCombinerTest, DynamicAndStatic)
+{
+ IYPathServicePtr dynamicService = GetEphemeralNodeFactory()->CreateMap();
+ IYPathServicePtr staticService = IYPathService::FromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("static_key1").Value(-1)
+ .Item("static_key2").Value(false)
+ .Item("error_key").Value("this key will be shared leading to an error")
+ .EndMap();
+ }));
+
+ auto combinedService = New<TServiceCombiner>(std::vector<IYPathServicePtr> { staticService, dynamicService }, TDuration::MilliSeconds(100));
+
+ EXPECT_EQ(true, YPathExists(combinedService, "/static_key1"));
+ EXPECT_EQ(false, YPathExists(combinedService, "/dynamic_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/error_key"));
+ EXPECT_EQ((std::vector<TString> { "static_key1", "static_key2", "error_key" }), YPathList(combinedService, ""));
+
+ YPathSet(dynamicService, "/dynamic_key1", ConvertToYsonString(3.1415926));
+ YPathSet(dynamicService, "/dynamic_key2", TYsonString(TStringBuf("#")));
+
+ // Give service time to rebuild key mapping.
+ Sleep(TDuration::MilliSeconds(200));
+
+ EXPECT_EQ(true, YPathExists(combinedService, "/static_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/dynamic_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/error_key"));
+ EXPECT_EQ((std::vector<TString> { "static_key1", "static_key2", "error_key", "dynamic_key1", "dynamic_key2" }), YPathList(combinedService, ""));
+ EXPECT_EQ(TYsonString(TStringBuf("#")), YPathGet(combinedService, "/dynamic_key2"));
+
+ YPathSet(dynamicService, "/error_key", ConvertToYsonString(42));
+
+ // Give service time to rebuild key mapping and notice two services sharing the same key.
+ Sleep(TDuration::MilliSeconds(200));
+
+ EXPECT_THROW(YPathExists(combinedService, "/static_key1"), std::exception);
+ EXPECT_THROW(YPathExists(combinedService, "/dynamic_key1"), std::exception);
+ EXPECT_THROW(YPathExists(combinedService, "/error_key"), std::exception);
+ EXPECT_THROW(YPathGet(combinedService, ""), std::exception);
+ EXPECT_THROW(YPathList(combinedService, ""), std::exception);
+ EXPECT_THROW(YPathGet(combinedService, "/static_key1"), std::exception);
+
+ YPathRemove(dynamicService, "/error_key");
+
+ // Give service time to return to the normal state.
+ Sleep(TDuration::MilliSeconds(200));
+
+ EXPECT_EQ(true, YPathExists(combinedService, "/static_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/dynamic_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/error_key"));
+ EXPECT_EQ((std::vector<TString> { "static_key1", "static_key2", "error_key", "dynamic_key1", "dynamic_key2" }), YPathList(combinedService, ""));
+ EXPECT_EQ(TYsonString(TStringBuf(TStringBuf("#"))), YPathGet(combinedService, "/dynamic_key2"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYPathServiceCombinerTest, UpdateKeysOnMissingKey)
+{
+ IYPathServicePtr dynamicService = GetEphemeralNodeFactory()->CreateMap();
+ IYPathServicePtr staticService = IYPathService::FromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("static_key1").Value(-1)
+ .Item("static_key2").Value(false)
+ .Item("error_key").Value("this key will be shared leading to an error")
+ .EndMap();
+ }));
+
+ auto combinedService = New<TServiceCombiner>(std::vector<IYPathServicePtr> { staticService, dynamicService }, TDuration::Seconds(100), /*updateKeysOnMissingKey*/ true);
+
+ EXPECT_EQ(true, YPathExists(combinedService, "/static_key1"));
+ EXPECT_EQ(false, YPathExists(combinedService, "/dynamic_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/error_key"));
+ EXPECT_EQ((std::vector<TString> { "static_key1", "static_key2", "error_key" }), YPathList(combinedService, ""));
+
+ YPathSet(dynamicService, "/dynamic_key1", ConvertToYsonString(3.1415926));
+ YPathSet(dynamicService, "/dynamic_key2", TYsonString(TStringBuf("#")));
+
+ EXPECT_EQ(true, YPathExists(combinedService, "/static_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/dynamic_key1"));
+ EXPECT_EQ(true, YPathExists(combinedService, "/error_key"));
+ EXPECT_EQ((std::vector<TString> { "static_key1", "static_key2", "error_key", "dynamic_key1", "dynamic_key2" }), YPathList(combinedService, ""));
+ EXPECT_EQ(TYsonString(TStringBuf("#")), YPathGet(combinedService, "/dynamic_key2"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+} // namespace
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/unittests/tree_builder_ut.cpp b/yt/yt/core/ytree/unittests/tree_builder_ut.cpp
new file mode 100644
index 0000000000..b47f0c8478
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/tree_builder_ut.cpp
@@ -0,0 +1,198 @@
+#include <yt/yt/core/test_framework/framework.h>
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/tree_builder.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+using ::testing::Sequence;
+using ::testing::InSequence;
+using ::testing::StrictMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTreeBuilderTest: public ::testing::Test
+{
+public:
+ StrictMock<TMockYsonConsumer> Mock;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_F(TTreeBuilderTest, EmptyMap)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+
+ builder->BeginTree();
+ builder->OnBeginMap();
+ builder->OnEndMap();
+ auto root = builder->EndTree();
+
+ VisitTree(root, &Mock, false);
+}
+
+TEST_F(TTreeBuilderTest, NestedMaps)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("a"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnEndMap());
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+
+ builder->BeginTree();
+ builder->OnBeginMap();
+ builder->OnKeyedItem("a");
+ builder->OnBeginMap();
+ builder->OnKeyedItem("b");
+ builder->OnBeginMap();
+ builder->OnKeyedItem("c");
+ builder->OnInt64Scalar(42);
+ builder->OnEndMap();
+ builder->OnEndMap();
+ builder->OnEndMap();
+ auto root = builder->EndTree();
+
+ VisitTree(root, &Mock, false);
+}
+
+TEST_F(TTreeBuilderTest, MapWithAttributes)
+{
+ // These are partial order chains.
+ // If you feel confused a pen and a paper will help you and save your brain from exploding.
+ // Have fun!
+ Sequence s1, s2, s3, s4, s5, s6;
+
+ EXPECT_CALL(Mock, OnBeginAttributes()).InSequence(s1, s2, s3, s4);
+ EXPECT_CALL(Mock, OnKeyedItem("acl")).InSequence(s1, s2);
+ EXPECT_CALL(Mock, OnBeginMap()).InSequence(s1, s2);
+ EXPECT_CALL(Mock, OnKeyedItem("read")).InSequence(s1);
+ EXPECT_CALL(Mock, OnBeginList()).InSequence(s1);
+ EXPECT_CALL(Mock, OnListItem()).InSequence(s1);
+ EXPECT_CALL(Mock, OnStringScalar("*")).InSequence(s1);
+ EXPECT_CALL(Mock, OnEndList()).InSequence(s1);
+
+ EXPECT_CALL(Mock, OnKeyedItem("write")).InSequence(s2);
+ EXPECT_CALL(Mock, OnBeginList()).InSequence(s2);
+ EXPECT_CALL(Mock, OnListItem()).InSequence(s2);
+ EXPECT_CALL(Mock, OnStringScalar("sandello")).InSequence(s2);
+ EXPECT_CALL(Mock, OnEndList()).InSequence(s2);
+ EXPECT_CALL(Mock, OnEndMap()).InSequence(s1, s2);
+
+ EXPECT_CALL(Mock, OnKeyedItem("lock_scope")).InSequence(s3);
+ EXPECT_CALL(Mock, OnStringScalar("mytables")).InSequence(s3);
+ EXPECT_CALL(Mock, OnEndAttributes()).InSequence(s1, s2, s3, s4);
+
+ EXPECT_CALL(Mock, OnBeginMap()).InSequence(s4, s5, s6);
+ EXPECT_CALL(Mock, OnKeyedItem("mode")).InSequence(s5);
+ EXPECT_CALL(Mock, OnInt64Scalar(755)).InSequence(s5);
+
+ EXPECT_CALL(Mock, OnKeyedItem("path")).InSequence(s6);
+ EXPECT_CALL(Mock, OnStringScalar("/home/sandello")).InSequence(s6);
+ EXPECT_CALL(Mock, OnEndMap()).InSequence(s4, s5, s6);
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+
+ builder->BeginTree();
+
+ builder->OnBeginAttributes();
+ builder->OnKeyedItem("acl");
+ builder->OnBeginMap();
+ builder->OnKeyedItem("read");
+ builder->OnBeginList();
+ builder->OnListItem();
+ builder->OnStringScalar("*");
+ builder->OnEndList();
+
+ builder->OnKeyedItem("write");
+ builder->OnBeginList();
+ builder->OnListItem();
+ builder->OnStringScalar("sandello");
+ builder->OnEndList();
+ builder->OnEndMap();
+
+ builder->OnKeyedItem("lock_scope");
+ builder->OnStringScalar("mytables");
+ builder->OnEndAttributes();
+
+ builder->OnBeginMap();
+ builder->OnKeyedItem("path");
+ builder->OnStringScalar("/home/sandello");
+
+ builder->OnKeyedItem("mode");
+ builder->OnInt64Scalar(755);
+ builder->OnEndMap();
+
+ auto root = builder->EndTree();
+
+ VisitTree(root, &Mock, false);
+}
+
+TEST_F(TTreeBuilderTest, SkipEntityMapChildren)
+{
+ InSequence dummy;
+ EXPECT_CALL(Mock, OnBeginMap());
+ EXPECT_CALL(Mock, OnKeyedItem("b"));
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnKeyedItem("c"));
+ EXPECT_CALL(Mock, OnBeginAttributes());
+ EXPECT_CALL(Mock, OnKeyedItem("d"));
+ EXPECT_CALL(Mock, OnEntity());
+ EXPECT_CALL(Mock, OnEndAttributes());
+ EXPECT_CALL(Mock, OnInt64Scalar(42));
+ EXPECT_CALL(Mock, OnEndMap());
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+
+ builder->BeginTree();
+ builder->OnBeginMap();
+
+ builder->OnKeyedItem("a");
+ builder->OnEntity();
+
+ builder->OnKeyedItem("b");
+ builder->OnInt64Scalar(42);
+
+ builder->OnKeyedItem("c");
+ builder->OnBeginAttributes();
+ builder->OnKeyedItem("d");
+ builder->OnEntity();
+ builder->OnEndAttributes();
+ builder->OnInt64Scalar(42);
+
+ builder->OnKeyedItem("f");
+ builder->OnBeginAttributes();
+ builder->OnKeyedItem("g");
+ builder->OnInt64Scalar(42);
+ builder->OnEndAttributes();
+ builder->OnEntity();
+
+ builder->OnEndMap();
+ auto root = builder->EndTree();
+
+ VisitTree(root, &Mock, false, TAttributeFilter(), true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/ya.make b/yt/yt/core/ytree/unittests/ya.make
new file mode 100644
index 0000000000..1ae5651b61
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/ya.make
@@ -0,0 +1,53 @@
+GTEST(unittester-core-ytree)
+
+INCLUDE(${ARCADIA_ROOT}/yt/ya_cpp.make.inc)
+
+IF (NOT OS_WINDOWS AND NOT ARCH_AARCH64)
+ ALLOCATOR(YT)
+ENDIF()
+
+PROTO_NAMESPACE(yt)
+
+SRCS(
+ attributes_ut.cpp
+ attribute_filter_ut.cpp
+ resolver_ut.cpp
+ serialize_ut.cpp
+ service_combiner_ut.cpp
+ tree_builder_ut.cpp
+ ypath_designated_service_ut.cpp
+ yson_serializable_ut.cpp
+ yson_struct_ut.cpp
+ ytree_fluent_ut.cpp
+ ytree_ut.cpp
+
+ proto/test.proto
+)
+
+GENERATE_ENUM_SERIALIZATION(serialize_ut.h)
+
+INCLUDE(${ARCADIA_ROOT}/yt/opensource_tests.inc)
+
+PEERDIR(
+ yt/yt/core
+ yt/yt/core/test_framework
+)
+
+REQUIREMENTS(
+ cpu:4
+ ram:4
+ ram_disk:1
+)
+
+FORK_TESTS()
+
+SPLIT_FACTOR(5)
+
+SIZE(SMALL)
+
+IF (OS_DARWIN)
+ SIZE(LARGE)
+ TAG(ya:fat ya:force_sandbox ya:exotic_platform)
+ENDIF()
+
+END()
diff --git a/yt/yt/core/ytree/unittests/ypath_designated_service_ut.cpp b/yt/yt/core/ytree/unittests/ypath_designated_service_ut.cpp
new file mode 100644
index 0000000000..74f57952b8
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/ypath_designated_service_ut.cpp
@@ -0,0 +1,293 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/service_combiner.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_proxy.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::vector<TString> YPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit = {})
+{
+ return AsyncYPathList(service, path, limit)
+ .Get()
+ .ValueOrThrow();
+}
+
+bool YPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path)
+{
+ return AsyncYPathExists(service, path)
+ .Get()
+ .ValueOrThrow();
+}
+
+TYsonString YPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter = {})
+{
+ return ConvertToYsonString(AsyncYPathGet(service, path, attributeFilter)
+ .Get()
+ .ValueOrThrow(), EYsonFormat::Text);
+}
+
+auto FluentString()
+{
+ return BuildYsonStringFluently(EYsonFormat::Text);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYPathDesignatedServiceTest, TestGetSelf)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .EndMap();
+ }));
+
+ EXPECT_EQ(FluentString().BeginMap().Item("key1").Value(42).EndMap(), YPathGet(service, ""));
+}
+
+TEST(TYPathDesignatedServiceTest, SimpleTypes)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2").Value("abc")
+ .Item("key3").Value(42ull)
+ .Item("key4").Value(true)
+ .Item("key5").Value(0.1)
+ .Item("key6").Entity()
+ .EndMap();
+ }));
+
+ EXPECT_EQ(FluentString().Value(42), YPathGet(service, "/key1"));
+ EXPECT_EQ(FluentString().Value("abc"), YPathGet(service, "/key2"));
+ EXPECT_EQ(FluentString().Value(42ull), YPathGet(service, "/key3"));
+ EXPECT_EQ(FluentString().Value(true), YPathGet(service, "/key4"));
+ EXPECT_EQ(FluentString().Value(0.1), YPathGet(service, "/key5"));
+ EXPECT_EQ(FluentString().Entity(), YPathGet(service, "/key6"));
+}
+
+TEST(TYPathDesignatedServiceTest, QueryNestedKeySimple)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2").BeginMap()
+ .Item("subkey1").Value("abc")
+ .Item("subkey2").Value(43)
+ .EndMap()
+ .EndMap();
+ }));
+
+ EXPECT_EQ(FluentString().Value("abc"), YPathGet(service, "/key2/subkey1"));
+ EXPECT_EQ(FluentString().Value(43), YPathGet(service, "/key2/subkey2"));
+}
+
+TEST(TYPathDesignatedServiceTest, QueryNestedComplex)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").BeginMap()
+ .Item("subkey1").Value("ab")
+ .EndMap()
+ .Item("key2").BeginMap()
+ .Item("subkey1").BeginMap()
+ .Item("string").Value("abc")
+ .Item("int").Value(40)
+ .EndMap()
+ .Item("subkey2").Value(43)
+ .EndMap()
+ .EndMap();
+ }));
+
+ auto expected = FluentString()
+ .BeginMap()
+ .Item("subkey1").BeginMap()
+ .Item("string").Value("abc")
+ .Item("int").Value(40)
+ .EndMap()
+ .Item("subkey2").Value(43)
+ .EndMap();
+
+ EXPECT_EQ(expected, YPathGet(service, "/key2"));
+}
+
+TEST(TYPathDesignatedServiceTest, GetList)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").BeginMap()
+ .Item("subkey1").Value("ab")
+ .EndMap()
+ .Item("key2").BeginList()
+ .Item().BeginMap()
+ .Item("string").Value("abc")
+ .Item("int").Value(40)
+ .EndMap()
+ .Item().Value(43)
+ .EndList()
+ .EndMap();
+ }));
+
+ auto expected = FluentString()
+ .BeginList()
+ .Item().BeginMap()
+ .Item("string").Value("abc")
+ .Item("int").Value(40)
+ .EndMap()
+ .Item().Value(43)
+ .EndList();
+
+ EXPECT_EQ(expected, YPathGet(service, "/key2"));
+}
+
+TEST(TYPathDesignatedServiceTest, GetAttributes)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1")
+ .BeginAttributes()
+ .Item("attr1").Value(12)
+ .Item("attr2").BeginMap()
+ .Item("subkey1").Value("x")
+ .EndMap()
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("ab")
+ .EndMap()
+ .Item("key2").Value(12)
+ .EndMap();
+ }));
+
+ auto expectedKey1 = FluentString()
+ .BeginAttributes()
+ .Item("attr1").Value(12)
+ .Item("attr2").BeginMap()
+ .Item("subkey1").Value("x")
+ .EndMap()
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("ab")
+ .EndMap();
+
+ EXPECT_EQ(expectedKey1, YPathGet(service, "/key1"));
+ EXPECT_EQ(FluentString().Value(12), YPathGet(service, "/key1/@attr1"));
+ EXPECT_EQ(FluentString().BeginMap().Item("subkey1").Value("x").EndMap(), YPathGet(service, "/key1/@attr2"));
+}
+
+TEST(TYPathDesignatedServiceTest, InexistentPaths)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2").BeginMap()
+ .Item("subkey1").Value("abc")
+ .Item("subkey2").Value(43)
+ .EndMap()
+ .EndMap();
+ }));
+
+ EXPECT_THROW_WITH_SUBSTRING(YPathGet(service, "/nonExistent"), "Failed to resolve YPath");
+ EXPECT_THROW_WITH_SUBSTRING(YPathGet(service, "/key1/nonExistent"), "Failed to resolve YPath");
+ EXPECT_THROW_WITH_SUBSTRING(YPathGet(service, "/key2/nonExistent"), "Failed to resolve YPath");
+ EXPECT_THROW_WITH_SUBSTRING(YPathGet(service, "/key2/@attr"), "Path \"key2/\" has no attributes");
+}
+
+TEST(TYPathDesignatedServiceTest, ExistsVerb)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value(12)
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("abc")
+ .Item("subkey2").Value(43)
+ .EndMap()
+ .EndMap();
+ }));
+
+ EXPECT_TRUE(YPathExists(service, "/key1"));
+ EXPECT_TRUE(YPathExists(service, "/key2"));
+ EXPECT_TRUE(YPathExists(service, "/key2/subkey1"));
+ EXPECT_TRUE(YPathExists(service, "/key2/@attr1"));
+
+ EXPECT_FALSE(YPathExists(service, "/nonExistent"));
+ EXPECT_FALSE(YPathExists(service, "/key1/nonExistent"));
+ EXPECT_FALSE(YPathExists(service, "/key2/nonExistent"));
+ EXPECT_FALSE(YPathExists(service, "/key2/@nonExistentAttr"));
+}
+
+TEST(TYPathDesignatedServiceTest, ListVerb)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginMap()
+ .Item("key1").Value(42)
+ .Item("key2")
+ .BeginAttributes()
+ .Item("attr1").Value(12)
+ .EndAttributes()
+ .BeginMap()
+ .Item("subkey1").Value("abc")
+ .Item("subkey2").Value(43)
+ .EndMap()
+ .EndMap();
+ }));
+
+ EXPECT_EQ((std::vector<TString> {"key1", "key2"}), YPathList(service, ""));
+ EXPECT_EQ((std::vector<TString> {"subkey1", "subkey2"}), YPathList(service, "/key2"));
+}
+
+TEST(TYPathDesignatedServiceTest, RootAttributes)
+{
+ auto service = IYPathService::YPathDesignatedServiceFromProducer(BIND([] (IYsonConsumer* consumer) {
+ BuildYsonFluently(consumer)
+ .BeginAttributes()
+ .Item("attr1").Value(12)
+ .Item("attr2").Value(20)
+ .EndAttributes()
+ .BeginMap()
+ .Item("key1").Value(42)
+ .EndMap();
+ }));
+
+ auto expectedAttrs = FluentString().BeginMap()
+ .Item("attr1").Value(12)
+ .Item("attr2").Value(20)
+ .EndMap();
+
+
+ EXPECT_EQ(expectedAttrs, YPathGet(service, "/@"));
+ EXPECT_EQ(FluentString().Value(12), YPathGet(service, "/@attr1"));
+ EXPECT_EQ((std::vector<TString>{"attr1", "attr2"}), YPathList(service, "/@"));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/unittests/yson_serializable_ut.cpp b/yt/yt/core/ytree/unittests/yson_serializable_ut.cpp
new file mode 100644
index 0000000000..934036ac91
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/yson_serializable_ut.cpp
@@ -0,0 +1,1171 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <array>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETestEnum,
+ (Value0)
+ (Value1)
+ (Value2)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTestSubconfig
+ : public TYsonSerializable
+{
+ int MyInt;
+ unsigned int MyUint;
+ bool MyBool;
+ std::vector<TString> MyStringList;
+ ETestEnum MyEnum;
+
+ TTestSubconfig()
+ {
+ RegisterParameter("my_int", MyInt).Default(100).InRange(95, 205);
+ RegisterParameter("my_uint", MyUint).Default(50).InRange(31, 117);
+ RegisterParameter("my_bool", MyBool).Default(false);
+ RegisterParameter("my_string_list", MyStringList).Default();
+ RegisterParameter("my_enum", MyEnum).Default(ETestEnum::Value1);
+ }
+};
+
+typedef TIntrusivePtr<TTestSubconfig> TTestSubconfigPtr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfig
+ : public TYsonSerializable
+{
+public:
+ TString MyString;
+ TTestSubconfigPtr Subconfig;
+ std::vector<TTestSubconfigPtr> SubconfigList;
+ std::unordered_map<TString, TTestSubconfigPtr> SubconfigMap;
+ std::optional<i64> NullableInt;
+
+ TTestConfig()
+ {
+ SetUnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+
+ RegisterParameter("my_string", MyString).NonEmpty();
+ RegisterParameter("sub", Subconfig).DefaultNew();
+ RegisterParameter("sub_list", SubconfigList).Default();
+ RegisterParameter("sub_map", SubconfigMap).Default();
+ RegisterParameter("nullable_int", NullableInt).Default();
+
+ RegisterPreprocessor([&] () {
+ MyString = "x";
+ Subconfig->MyInt = 200;
+ });
+ }
+};
+
+typedef TIntrusivePtr<TTestConfig> TTestConfigPtr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleYsonSerializable
+ : public TYsonSerializable
+{
+public:
+ int IntValue;
+
+ TSimpleYsonSerializable()
+ {
+ RegisterParameter("int_value", IntValue)
+ .Default(1);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleYsonStruct
+ : public TYsonStruct
+{
+public:
+ int IntValue;
+
+ REGISTER_YSON_STRUCT(TSimpleYsonStruct);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("int_value", &TSimpleYsonStruct::IntValue)
+ .Default(1);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSerializableWithSimpleYsonStruct
+ : public TYsonSerializable
+{
+public:
+ TIntrusivePtr<TSimpleYsonStruct> YsonStruct;
+
+ TYsonSerializableWithSimpleYsonStruct()
+ {
+ SetUnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+
+ RegisterParameter("yson_struct", YsonStruct)
+ .DefaultNew();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// {LoadFromNode}
+using TYsonSerializableParseTestParameter = std::tuple<bool>;
+
+class TYsonSerializableParseTest
+ : public ::testing::TestWithParam<TYsonSerializableParseTestParameter>
+{
+public:
+ template <typename T>
+ TIntrusivePtr<T> Load(
+ const INodePtr& node,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = {})
+ {
+ auto [loadFromNode] = GetParam();
+ auto config = New<T>();
+ if (loadFromNode) {
+ config->Load(node, postprocess, setDefaults, path);
+ } else {
+ auto ysonString = ConvertToYsonString(node);
+ auto string = ysonString.ToString();
+ TStringInput input(string);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto cursor = TYsonPullParserCursor(&parser);
+ config->Load(&cursor, postprocess, setDefaults, path);
+ }
+ return config;
+ }
+
+ template <typename T>
+ TIntrusivePtr<T> Load(const TYsonStringBuf& yson)
+ {
+ return Load<T>(ConvertTo<INodePtr>(yson));
+ }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ LoadFromNode,
+ TYsonSerializableParseTest,
+ ::testing::Values(TYsonSerializableParseTestParameter{
+ true
+ })
+);
+
+INSTANTIATE_TEST_SUITE_P(
+ LoadFromCursor,
+ TYsonSerializableParseTest,
+ ::testing::Values(TYsonSerializableParseTestParameter{
+ false
+ })
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TestCompleteSubconfig(TTestSubconfig* subconfig)
+{
+ EXPECT_EQ(99, subconfig->MyInt);
+ EXPECT_EQ(101u, subconfig->MyUint);
+ EXPECT_TRUE(subconfig->MyBool);
+ EXPECT_EQ(3u, subconfig->MyStringList.size());
+ EXPECT_EQ("ListItem0", subconfig->MyStringList[0]);
+ EXPECT_EQ("ListItem1", subconfig->MyStringList[1]);
+ EXPECT_EQ("ListItem2", subconfig->MyStringList[2]);
+ EXPECT_EQ(ETestEnum::Value2, subconfig->MyEnum);
+}
+
+TEST_P(TYsonSerializableParseTest, Complete)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_uint").Value(101)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item("sub_list").BeginList()
+ .Item().BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_uint").Value(101)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_uint").Value(101)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndList()
+ .Item("sub_map").BeginMap()
+ .Item("sub1").BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_uint").Value(101)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item("sub2").BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_uint").Value(101)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ EXPECT_EQ("TestString", config->MyString);
+ TestCompleteSubconfig(config->Subconfig.Get());
+ EXPECT_EQ(2u, config->SubconfigList.size());
+ TestCompleteSubconfig(config->SubconfigList[0].Get());
+ TestCompleteSubconfig(config->SubconfigList[1].Get());
+ EXPECT_EQ(2u, config->SubconfigMap.size());
+ auto it1 = config->SubconfigMap.find("sub1");
+ EXPECT_FALSE(it1 == config->SubconfigMap.end());
+ TestCompleteSubconfig(it1->second.Get());
+ auto it2 = config->SubconfigMap.find("sub2");
+ EXPECT_FALSE(it2 == config->SubconfigMap.end());
+ TestCompleteSubconfig(it2->second.Get());
+}
+
+TEST_P(TYsonSerializableParseTest, MissingParameter)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_bool").Value(true)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ EXPECT_EQ("TestString", config->MyString);
+ EXPECT_EQ(200, config->Subconfig->MyInt);
+ EXPECT_TRUE(config->Subconfig->MyBool);
+ EXPECT_EQ(0u, config->Subconfig->MyStringList.size());
+ EXPECT_EQ(ETestEnum::Value1, config->Subconfig->MyEnum);
+ EXPECT_EQ(0u, config->SubconfigList.size());
+ EXPECT_EQ(0u, config->SubconfigMap.size());
+}
+
+TEST_P(TYsonSerializableParseTest, MissingSubconfig)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ EXPECT_EQ("TestString", config->MyString);
+ EXPECT_EQ(200, config->Subconfig->MyInt);
+ EXPECT_FALSE(config->Subconfig->MyBool);
+ EXPECT_EQ(0u, config->Subconfig->MyStringList.size());
+ EXPECT_EQ(ETestEnum::Value1, config->Subconfig->MyEnum);
+ EXPECT_EQ(0u, config->SubconfigList.size());
+ EXPECT_EQ(0u, config->SubconfigMap.size());
+}
+
+TEST_P(TYsonSerializableParseTest, UnrecognizedSimple)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("option").Value(1)
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ auto unrecognizedNode = config->GetUnrecognized();
+ auto unrecognizedRecursivelyNode = config->GetUnrecognizedRecursively();
+ EXPECT_TRUE(AreNodesEqual(unrecognizedNode, unrecognizedRecursivelyNode));
+ EXPECT_EQ(1, unrecognizedNode->GetChildCount());
+ for (const auto& [key, child] : unrecognizedNode->GetChildren()) {
+ EXPECT_EQ("option", key);
+ EXPECT_EQ(1, child->AsInt64()->GetValue());
+ }
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ auto deserializedConfig = ConvertTo<TTestConfigPtr>(output);
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(config), ConvertToNode(deserializedConfig)));
+}
+
+TEST_P(TYsonSerializableParseTest, UnrecognizedRecursive)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("option").Value(1)
+ .Item("sub").BeginMap()
+ .Item("sub_option").Value(42)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ auto unrecognizedRecursivelyNode = config->GetUnrecognizedRecursively();
+ EXPECT_EQ(2, unrecognizedRecursivelyNode->GetChildCount());
+ for (const auto& [key, child] : unrecognizedRecursivelyNode->GetChildren()) {
+ if (key == "option") {
+ EXPECT_EQ(1, child->AsInt64()->GetValue());
+ } else {
+ EXPECT_EQ("sub", key);
+ EXPECT_EQ(42, child->AsMap()->GetChildOrThrow("sub_option")->AsInt64()->GetValue());
+ }
+ }
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ auto deserializedConfig = ConvertTo<TTestConfigPtr>(output);
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(config), ConvertToNode(deserializedConfig)));
+}
+
+TEST_P(TYsonSerializableParseTest, UnrecognizedWithNestedYsonStruct)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("yson_struct").BeginMap()
+ .Item("unrecognized").Value(1)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TYsonSerializableWithSimpleYsonStruct>(configNode->AsMap());
+
+ auto unrecognized = config->GetRecursiveUnrecognized();
+ EXPECT_EQ(
+ ConvertToYsonString(configNode, EYsonFormat::Text).AsStringBuf(),
+ ConvertToYsonString(unrecognized, EYsonFormat::Text).AsStringBuf());
+}
+
+TEST_P(TYsonSerializableParseTest, MissingRequiredParameter)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_bool").Value(true)
+ .EndMap()
+ .EndMap();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, IncorrectNodeType)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value(1) // incorrect type
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, ArithmeticOverflow)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(Max<i64>())
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("Value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, Postprocess)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("") // empty!
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode, false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, PostprocessSubconfig)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode, false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, PostprocessSubconfigList)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub_list").BeginList()
+ .Item().BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndList()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode, false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, PostprocessSubconfigMap)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub_map").BeginMap()
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode, false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST(TYsonSerializableTest, SaveSingleParameter)
+{
+ auto config = New<TTestConfig>();
+ config->MyString = "test";
+ config->NullableInt = 10;
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ config->SaveParameter("my_string", builder.get());
+ auto actual = ConvertTo<TString>(builder->EndTree());
+ EXPECT_EQ("test", actual);
+}
+
+TEST(TYsonSerializableTest, LoadSingleParameter)
+{
+ auto config = New<TTestConfig>();
+ config->NullableInt = 10;
+
+ config->LoadParameter("my_string", ConvertToNode("test"), EMergeStrategy::Default);
+ EXPECT_EQ("test", config->MyString);
+ EXPECT_EQ(10, config->NullableInt);
+}
+
+TEST(TYsonSerializableTest, LoadSingleParameterWithMergeStrategy)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_int").Value(100)
+ .EndMap();
+ auto subConfig = builder->EndTree();
+
+ auto config1 = New<TTestConfig>();
+ config1->Subconfig->MyBool = true;
+ config1->LoadParameter("sub", subConfig, EMergeStrategy::Default);
+ EXPECT_EQ(100, config1->Subconfig->MyInt);
+ EXPECT_TRUE(config1->Subconfig->MyBool); // Subconfig merged by default.
+
+ auto config2 = New<TTestConfig>();
+ config2->Subconfig->MyBool = true;
+ config2->LoadParameter("sub", subConfig, EMergeStrategy::Overwrite);
+ EXPECT_EQ(100, config2->Subconfig->MyInt);
+ EXPECT_FALSE(config2->Subconfig->MyBool); // Overwrite destroyed previous values.
+}
+
+TEST(TYsonSerializableTest, ResetSingleParameter)
+{
+ auto config = New<TTestSubconfig>();
+ config->MyInt = 10;
+ config->MyUint = 10;
+
+ config->ResetParameter("my_int");
+ EXPECT_EQ(100, config->MyInt); // Default value.
+ EXPECT_EQ(10u, config->MyUint);
+}
+
+TEST(TYsonSerializableTest, Save)
+{
+ auto config = New<TTestConfig>();
+
+ // add non-default fields;
+ config->MyString = "hello!";
+ config->SubconfigList.push_back(New<TTestSubconfig>());
+ config->SubconfigMap["item"] = New<TTestSubconfig>();
+ config->NullableInt = 42;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString subconfigYson =
+ "{\"my_bool\"=%false;"
+ "\"my_enum\"=\"value1\";"
+ "\"my_int\"=200;"
+ "\"my_uint\"=50u;"
+ "\"my_string_list\"=[]}";
+
+ TString subconfigYsonOrigin =
+ "{\"my_bool\"=%false;"
+ "\"my_enum\"=\"value1\";"
+ "\"my_int\"=100;"
+ "\"my_uint\"=50u;"
+ "\"my_string_list\"=[]}";
+
+ TString expectedYson;
+ expectedYson += "{\"my_string\"=\"hello!\";";
+ expectedYson += "\"sub\"=" + subconfigYson + ";";
+ expectedYson += "\"sub_list\"=[" + subconfigYsonOrigin + "];";
+ expectedYson += "\"sub_map\"={\"item\"=" + subconfigYsonOrigin + "};";
+ expectedYson += "\"nullable_int\"=42}";
+
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(output)));
+}
+
+TEST(TYsonSerializableTest, TestConfigUpdate)
+{
+ auto config = New<TTestConfig>();
+ {
+ auto newConfig = UpdateYsonSerializable(config, nullptr);
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 200);
+ }
+
+ {
+ auto newConfig = UpdateYsonSerializable(config, ConvertToNode(TYsonString(TStringBuf("{\"sub\"={\"my_int\"=150}}"))));
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 150);
+ }
+
+ {
+ auto newConfig = UpdateYsonSerializable(config, ConvertToNode(TYsonString(TStringBuf("{\"sub\"={\"my_int_\"=150}}"))));
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 200);
+ }
+}
+
+TEST(TYsonSerializableTest, NoDefaultNewAliasing)
+{
+ auto config1 = New<TTestConfig>();
+ auto config2 = New<TTestConfig>();
+ EXPECT_NE(config1->Subconfig, config2->Subconfig);
+}
+
+TEST(TYsonSerializableTest, Reconfigure)
+{
+ auto config = New<TTestConfig>();
+ auto subconfig = config->Subconfig;
+
+ EXPECT_EQ("x", config->MyString);
+ EXPECT_EQ(200, subconfig->MyInt);
+
+ auto configNode1 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("y")
+ .EndMap();
+ ReconfigureYsonSerializable(config, configNode1);
+
+ EXPECT_EQ("y", config->MyString);
+ EXPECT_EQ(subconfig, config->Subconfig);
+ EXPECT_EQ(200, subconfig->MyInt);
+
+ auto configNode2 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("z")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(95)
+ .EndMap()
+ .EndMap();
+ ReconfigureYsonSerializable(config, configNode2);
+
+ EXPECT_EQ("z", config->MyString);
+ EXPECT_EQ(subconfig, config->Subconfig);
+ EXPECT_EQ(95, subconfig->MyInt);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfigLite
+ : public TYsonSerializableLite
+{
+public:
+ TString MyString;
+ std::optional<i64> NullableInt;
+
+ TTestConfigLite()
+ {
+ RegisterParameter("my_string", MyString).NonEmpty();
+ RegisterParameter("nullable_int", NullableInt).Default();
+ }
+};
+
+TEST(TYsonSerializableTest, SaveLite)
+{
+ TTestConfigLite config;
+
+ config.MyString = "hello!";
+ config.NullableInt = 42;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson;
+ expectedYson += "{\"my_string\"=\"hello!\";";
+ expectedYson += "\"nullable_int\"=42}";
+
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(output)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfigWithAliases
+ : public TYsonSerializable
+{
+public:
+ TString Value;
+
+ TTestConfigWithAliases()
+ {
+ RegisterParameter("key", Value)
+ .Alias("alias1")
+ .Alias("alias2");
+ }
+};
+
+TEST_P(TYsonSerializableParseTest, Aliases1)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("key").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfigWithAliases>(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST_P(TYsonSerializableParseTest, Aliases2)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfigWithAliases>(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST_P(TYsonSerializableParseTest, Aliases3)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value")
+ .Item("alias2").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfigWithAliases>(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST_P(TYsonSerializableParseTest, Aliases4)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value1")
+ .Item("alias2").Value("value2")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfigWithAliases>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, Aliases5)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfigWithAliases>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonSerializableParseTest, ParameterTuplesAndContainers)
+{
+ class TTestClass
+ : public NYTree::TYsonSerializable
+ {
+ public:
+ std::vector<TString> Vector;
+ std::array<TString, 3> Array;
+ std::pair<size_t, TString> Pair;
+ std::set<TString> Set;
+ std::map<TString, int> Map;
+ std::multiset<int> MultiSet;
+ std::unordered_set<TString> UnorderedSet;
+ std::unordered_map<TString, int> UnorderedMap;
+ std::unordered_multiset<size_t> UnorderedMultiSet;
+
+ TTestClass()
+ {
+ RegisterParameter("vector", Vector)
+ .Default();
+ RegisterParameter("array", Array)
+ .Default();
+ RegisterParameter("pair", Pair)
+ .Default();
+ RegisterParameter("set", Set)
+ .Default();
+ RegisterParameter("map", Map)
+ .Default();
+ RegisterParameter("multiset", MultiSet)
+ .Default();
+ RegisterParameter("unordered_set", UnorderedSet)
+ .Default();
+ RegisterParameter("unordered_map", UnorderedMap)
+ .Default();
+ RegisterParameter("unordered_multiset", UnorderedMultiSet)
+ .Default();
+ }
+ };
+
+ auto original = New<TTestClass>();
+ original->Vector = { "fceswf", "sadfcesa" };
+ original->Array = {{ "UYTUY", ":LL:a", "78678678" }};
+ original->Pair = { 7U, "UYTUY" };
+ original->Set = { " q!", "12343e", "svvr", "0001" };
+ original->Map = { {"!", 4398}, {"zzz", 0} };
+ original->MultiSet = { 33, 33, 22, 22, 11 };
+ original->UnorderedSet = { "41", "52", "001", "set" };
+ original->UnorderedMap = { {"12345", 8}, {"XXX", 9}, {"XYZ", 42} };
+ original->UnorderedMultiSet = { 1U, 2U, 1U, 0U, 0U };
+
+ auto deserialized = Load<TTestClass>(ConvertToYsonString(*original));
+
+ EXPECT_EQ(original->Vector, deserialized->Vector);
+ EXPECT_EQ(original->Array, deserialized->Array);
+ EXPECT_EQ(original->Pair, deserialized->Pair);
+ EXPECT_EQ(original->Set, deserialized->Set);
+ EXPECT_EQ(original->Map, deserialized->Map);
+ EXPECT_EQ(original->MultiSet, deserialized->MultiSet);
+ EXPECT_EQ(original->UnorderedSet, deserialized->UnorderedSet);
+ EXPECT_EQ(original->UnorderedMap, deserialized->UnorderedMap);
+ EXPECT_EQ(original->UnorderedMultiSet, deserialized->UnorderedMultiSet);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonSerializableTest, EnumAsKeyToYHash)
+{
+ THashMap<ETestEnum, TString> deserialized, original = {
+ {ETestEnum::Value0, "abc"}
+ };
+
+ TString serialized = "{\"value0\"=\"abc\";}";
+ ASSERT_EQ(serialized, ConvertToYsonString(original, EYsonFormat::Text).AsStringBuf());
+
+ Deserialize(deserialized, ConvertToNode(TYsonString(serialized, EYsonType::Node)));
+
+ ASSERT_EQ(original, deserialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST_P(TYsonSerializableParseTest, NullableWithNonNullDefault)
+{
+ class TConfig
+ : public TYsonSerializable
+ {
+ public:
+ std::optional<int> Value;
+
+ TConfig()
+ {
+ RegisterParameter("value", Value)
+ .Default(123);
+ }
+ };
+
+ {
+ auto config = Load<TConfig>(TYsonStringBuf("{}"));
+ EXPECT_EQ(123, *config->Value);
+ EXPECT_EQ(123, ConvertToNode(config)->AsMap()->GetChildValueOrThrow<i64>("value"));
+ }
+
+ {
+ auto config = Load<TConfig>(TYsonStringBuf("{value=#}"));
+ EXPECT_FALSE(config->Value);
+ EXPECT_EQ(ENodeType::Entity, ConvertToNode(config)->AsMap()->GetChildOrThrow("value")->GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonSerializableTest, DontSerializeDefault)
+{
+ class TConfig
+ : public TYsonSerializable
+ {
+ public:
+ int Value;
+ int OtherValue;
+
+ TConfig()
+ {
+ RegisterParameter("value", Value)
+ .Default(123);
+ RegisterParameter("other_value", OtherValue)
+ .Default(456)
+ .DontSerializeDefault();
+ }
+ };
+
+ {
+ auto config = New<TConfig>();
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(output)));
+ }
+
+ {
+ auto config = New<TConfig>();
+ config->OtherValue = 789;
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;\"other_value\"=789;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(output)));
+ }
+}
+
+class TYsonStructClass
+ : public TYsonStruct
+{
+public:
+ int IntValue;
+
+ REGISTER_YSON_STRUCT(TYsonStructClass);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("int_value", &TThis::IntValue)
+ .Default(1);
+ }
+};
+
+class TYsonSerializableClass
+ : public TYsonSerializable
+{
+public:
+ THashMap<TString, TIntrusivePtr<TYsonStructClass>> YsonStructHashMap;
+
+ TIntrusivePtr<TYsonStructClass> YsonStructValue;
+
+ TYsonSerializableClass()
+ {
+ RegisterParameter("yson_struct_hash_map", YsonStructHashMap)
+ .Default();
+
+ RegisterParameter("yson_struct_value", YsonStructValue)
+ .DefaultNew();
+
+ RegisterPreprocessor([&] () {
+ YsonStructValue->IntValue = 5;
+ });
+ }
+};
+
+TEST_P(TYsonSerializableParseTest, YsonStructNestedToYsonSerializableSimple)
+{
+ {
+ auto config = New<TYsonSerializableClass>();
+ EXPECT_EQ(config->YsonStructValue->IntValue, 5);
+
+ config->YsonStructHashMap["x"] = New<TYsonStructClass>();
+ config->YsonStructHashMap["x"]->IntValue = 10;
+ config->YsonStructValue->IntValue = 2;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ TString expectedYson = "{yson_struct_hash_map={x={int_value=10}};yson_struct_value={int_value=2}}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+
+ auto deserialized = Load<TYsonSerializableClass>(output);
+ EXPECT_EQ(deserialized->YsonStructHashMap["x"]->IntValue, 10);
+ EXPECT_EQ(deserialized->YsonStructValue->IntValue, 2);
+
+ }
+}
+
+TEST_P(TYsonSerializableParseTest, YsonStructNestedToYsonSerializableDeserializesFromEmpty)
+{
+ {
+ auto testInput = TYsonString(TStringBuf("{yson_struct_value={}}"));
+ auto deserialized = Load<TYsonSerializableClass>(testInput);
+ EXPECT_EQ(deserialized->YsonStructValue->IntValue, 5);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNestedYsonStructClass
+ : public TYsonStruct
+{
+public:
+ int IntValue;
+
+ REGISTER_YSON_STRUCT(TNestedYsonStructClass);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("int_value", &TThis::IntValue)
+ .Default(1);
+ registrar.Postprocessor([&] (TNestedYsonStructClass* klass) {
+ klass->IntValue = 10;
+ });
+ }
+};
+
+class TYsonSerializableClass2
+ : public TYsonSerializable
+{
+public:
+ THashMap<TString, TIntrusivePtr<TNestedYsonStructClass>> YsonStructHashMap;
+
+ TYsonSerializableClass2()
+ {
+ RegisterParameter("yson_struct_hash_map", YsonStructHashMap)
+ .Default();
+ }
+};
+
+TEST_P(TYsonSerializableParseTest, PostprocessIsPropagatedFromYsonSerializableToYsonStruct)
+{
+ auto testInput = TYsonString(TStringBuf("{yson_struct_hash_map={x={int_value=2}}}"));
+ auto deserialized = Load<TYsonSerializableClass2>(testInput);
+ EXPECT_EQ(deserialized->YsonStructHashMap["x"]->IntValue, 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> CreateCustomDefault()
+{
+ auto result = New<T>();
+ result->IntValue = 10;
+ return result;
+}
+
+class TYsonSerializableWithNestedStructsAndCustomDefaults
+ : public TYsonSerializable
+{
+public:
+ TIntrusivePtr<TSimpleYsonSerializable> YsonSerializable;
+ TIntrusivePtr<TSimpleYsonStruct> YsonStruct;
+
+ TYsonSerializableWithNestedStructsAndCustomDefaults()
+ {
+ RegisterParameter("yson_serializable", YsonSerializable)
+ .Default(CreateCustomDefault<TSimpleYsonSerializable>());
+ RegisterParameter("yson_struct", YsonStruct)
+ .Default(CreateCustomDefault<TSimpleYsonStruct>());
+ }
+};
+
+TEST(TYsonSerializableTest, TestCustomDefaultsOfNestedStructsAreDiscardedOnDeserialize)
+{
+ auto testInput = TYsonString(TStringBuf("{}"));
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonSerializableWithNestedStructsAndCustomDefaults>>(testInput);
+ EXPECT_EQ(deserialized->YsonSerializable->IntValue, 1);
+ EXPECT_EQ(deserialized->YsonStruct->IntValue, 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSerializableWithNestedStructsAndPreprocessors
+ : public TYsonSerializable
+{
+public:
+ TIntrusivePtr<TSimpleYsonSerializable> YsonSerializable;
+ TIntrusivePtr<TSimpleYsonStruct> YsonStruct;
+
+ TYsonSerializableWithNestedStructsAndPreprocessors()
+ {
+ RegisterParameter("yson_serializable", YsonSerializable)
+ .Default();
+ RegisterParameter("yson_struct", YsonStruct)
+ .Default();
+ RegisterPreprocessor([&] () {
+ YsonSerializable = CreateCustomDefault<TSimpleYsonSerializable>();
+ YsonStruct = CreateCustomDefault<TSimpleYsonStruct>();
+ });
+ }
+};
+
+TEST(TYsonSerializableTest, TestPreprocessorsEffectsOnNestedStructsArePreservedOnDeserialize)
+{
+ auto testInput = TYsonString(TStringBuf("{}"));
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonSerializableWithNestedStructsAndPreprocessors>>(testInput);
+ EXPECT_EQ(deserialized->YsonSerializable->IntValue, 10);
+ EXPECT_EQ(deserialized->YsonStruct->IntValue, 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonSerializableTest, TestStable)
+{
+ class TInner
+ : public NYTree::TYsonSerializable
+ {
+ public:
+ int A;
+ int B;
+ int C;
+ int D;
+ int Q;
+
+ TInner()
+ {
+ RegisterParameter("b", B)
+ .Default(2);
+ RegisterParameter("a", A)
+ .Default(1);
+ RegisterParameter("c", C)
+ .Default(3);
+ RegisterParameter("q", Q)
+ .Default(9);
+ RegisterParameter("d", D)
+ .Default(4);
+ }
+ };
+
+ class TOuter
+ : public TYsonSerializable
+ {
+ public:
+ TIntrusivePtr<TInner> Inner;
+ TOuter()
+ {
+ RegisterParameter("inner", Inner)
+ .DefaultNew();
+ }
+ };
+
+ {
+ auto outer = New<TOuter>();
+ auto output = ConvertToYsonString(*outer, NYson::EYsonFormat::Text);
+
+ auto result = BuildYsonStringFluently(NYson::EYsonFormat::Text)
+ .BeginMap()
+ .Item("inner").BeginMap()
+ .Item("a").Value(1)
+ .Item("b").Value(2)
+ .Item("c").Value(3)
+ .Item("d").Value(4)
+ .Item("q").Value(9)
+ .EndMap()
+ .EndMap();
+
+ EXPECT_EQ(result, output);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/yson_struct_ut.cpp b/yt/yt/core/ytree/unittests/yson_struct_ut.cpp
new file mode 100644
index 0000000000..0b8ebbe52d
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/yson_struct_ut.cpp
@@ -0,0 +1,1599 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/tree_builder.h>
+#include <yt/yt/core/ytree/tree_visitor.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/yson_serializable.h>
+#include <yt/yt/core/ytree/yson_struct.h>
+
+#include <util/stream/buffer.h>
+
+#include <util/ysaveload.h>
+
+#include <array>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(ETestEnum,
+ (Value0)
+ (Value1)
+ (Value2)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TTestSubconfig
+ : public TYsonStruct
+{
+ int MyInt;
+ unsigned int MyUint;
+ bool MyBool;
+ std::vector<TString> MyStringList;
+ ETestEnum MyEnum;
+
+ REGISTER_YSON_STRUCT(TTestSubconfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("my_int", &TThis::MyInt)
+ .Default(100)
+ .InRange(95, 205);
+ registrar.Parameter("my_uint", &TThis::MyUint)
+ .Default(50)
+ .InRange(31, 117);
+ registrar.Parameter("my_bool", &TThis::MyBool)
+ .Default(false);
+ registrar.Parameter("my_string_list", &TThis::MyStringList)
+ .Default();
+ registrar.Parameter("my_enum", &TThis::MyEnum)
+ .Default(ETestEnum::Value1);
+ }
+};
+
+typedef TIntrusivePtr<TTestSubconfig> TTestSubconfigPtr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfig
+ : public TYsonStruct
+{
+public:
+ TString MyString;
+ TTestSubconfigPtr Subconfig;
+ std::vector<TTestSubconfigPtr> SubconfigList;
+ std::unordered_map<TString, TTestSubconfigPtr> SubconfigMap;
+ std::optional<i64> NullableInt;
+
+ REGISTER_YSON_STRUCT(TTestConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.UnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+
+ registrar.Parameter("my_string", &TThis::MyString)
+ .NonEmpty();
+ registrar.Parameter("sub", &TThis::Subconfig)
+ .DefaultNew();
+ registrar.Parameter("sub_list", &TThis::SubconfigList)
+ .Default();
+ registrar.Parameter("sub_map", &TThis::SubconfigMap)
+ .Default();
+ registrar.Parameter("nullable_int", &TThis::NullableInt)
+ .Default();
+
+ registrar.Preprocessor([] (TTestConfig* config) {
+ config->MyString = "x";
+ config->Subconfig->MyInt = 200;
+ });
+ }
+};
+
+typedef TIntrusivePtr<TTestConfig> TTestConfigPtr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleYsonSerializable
+ : public TYsonSerializable
+{
+public:
+ int IntValue;
+
+ TSimpleYsonSerializable()
+ {
+ RegisterParameter("int_value", IntValue)
+ .Default(1);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSimpleYsonStruct
+ : public TYsonStruct
+{
+public:
+ int IntValue;
+
+ REGISTER_YSON_STRUCT(TSimpleYsonStruct);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("int_value", &TSimpleYsonStruct::IntValue)
+ .Default(1);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStructWithSimpleYsonSerializable
+ : public TYsonStruct
+{
+public:
+ TIntrusivePtr<TSimpleYsonSerializable> YsonSerializable;
+
+ REGISTER_YSON_STRUCT(TYsonStructWithSimpleYsonSerializable);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.UnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+
+ registrar.Parameter("yson_serializable", &TYsonStructWithSimpleYsonSerializable::YsonSerializable)
+ .DefaultNew();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+auto GetCompleteConfigNode(int offset = 0)
+{
+ return BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString" + std::to_string(offset))
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(99 + offset)
+ .Item("my_uint").Value(101 + offset)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item("sub_list").BeginList()
+ .Item().BeginMap()
+ .Item("my_int").Value(99 + offset)
+ .Item("my_uint").Value(101 + offset)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item().BeginMap()
+ .Item("my_int").Value(99 + offset)
+ .Item("my_uint").Value(101 + offset)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndList()
+ .Item("sub_map").BeginMap()
+ .Item("sub1").BeginMap()
+ .Item("my_int").Value(99 + offset)
+ .Item("my_uint").Value(101 + offset)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .Item("sub2").BeginMap()
+ .Item("my_int").Value(99 + offset)
+ .Item("my_uint").Value(101 + offset)
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndMap()
+ .EndMap();
+}
+
+void TestCompleteSubconfig(TTestSubconfig* subconfig, int offset = 0)
+{
+ EXPECT_EQ(99 + offset, subconfig->MyInt);
+ EXPECT_EQ(101u + offset, subconfig->MyUint);
+ EXPECT_TRUE(subconfig->MyBool);
+ EXPECT_EQ(3u, subconfig->MyStringList.size());
+ EXPECT_EQ("ListItem0", subconfig->MyStringList[0]);
+ EXPECT_EQ("ListItem1", subconfig->MyStringList[1]);
+ EXPECT_EQ("ListItem2", subconfig->MyStringList[2]);
+ EXPECT_EQ(ETestEnum::Value2, subconfig->MyEnum);
+}
+
+void TestCompleteConfig(TIntrusivePtr<TTestConfig> config, int offset = 0)
+{
+ EXPECT_EQ("TestString" + std::to_string(offset), config->MyString);
+ TestCompleteSubconfig(config->Subconfig.Get(), offset);
+ EXPECT_EQ(2u, config->SubconfigList.size());
+ TestCompleteSubconfig(config->SubconfigList[0].Get(), offset);
+ TestCompleteSubconfig(config->SubconfigList[1].Get(), offset);
+ EXPECT_EQ(2u, config->SubconfigMap.size());
+ auto it1 = config->SubconfigMap.find("sub1");
+ EXPECT_FALSE(it1 == config->SubconfigMap.end());
+ TestCompleteSubconfig(it1->second.Get(), offset);
+ auto it2 = config->SubconfigMap.find("sub2");
+ EXPECT_FALSE(it2 == config->SubconfigMap.end());
+ TestCompleteSubconfig(it2->second.Get(), offset);
+}
+
+// {LoadFromNode}
+using TYsonStructParseTestParameter = std::tuple<bool>;
+
+class TYsonStructParseTest
+ : public ::testing::TestWithParam<TYsonStructParseTestParameter>
+{
+public:
+ template <typename T>
+ TIntrusivePtr<T> Load(
+ const INodePtr& node,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = {})
+ {
+ auto [loadFromNode] = GetParam();
+ auto config = New<T>();
+ if (loadFromNode) {
+ config->Load(node, postprocess, setDefaults, path);
+ } else {
+ auto ysonString = ConvertToYsonString(node);
+ auto string = ysonString.ToString();
+ TStringInput input(string);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ auto cursor = TYsonPullParserCursor(&parser);
+ config->Load(&cursor, postprocess, setDefaults, path);
+ }
+ return config;
+ }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ LoadFromNode,
+ TYsonStructParseTest,
+ ::testing::Values(TYsonStructParseTestParameter{
+ true
+ })
+);
+
+INSTANTIATE_TEST_SUITE_P(
+ LoadFromCursor,
+ TYsonStructParseTest,
+ ::testing::Values(TYsonStructParseTestParameter{
+ false
+ })
+);
+
+TEST_P(TYsonStructParseTest, Complete)
+{
+ auto configNode = GetCompleteConfigNode();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ TestCompleteConfig(config);
+}
+
+TEST_P(TYsonStructParseTest, MissingParameter)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_bool").Value(true)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ EXPECT_EQ("TestString", config->MyString);
+ EXPECT_EQ(200, config->Subconfig->MyInt);
+ EXPECT_TRUE(config->Subconfig->MyBool);
+ EXPECT_EQ(0u, config->Subconfig->MyStringList.size());
+ EXPECT_EQ(ETestEnum::Value1, config->Subconfig->MyEnum);
+ EXPECT_EQ(0u, config->SubconfigList.size());
+ EXPECT_EQ(0u, config->SubconfigMap.size());
+}
+
+TEST_P(TYsonStructParseTest, MissingSubconfig)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ EXPECT_EQ("TestString", config->MyString);
+ EXPECT_EQ(200, config->Subconfig->MyInt);
+ EXPECT_FALSE(config->Subconfig->MyBool);
+ EXPECT_EQ(0u, config->Subconfig->MyStringList.size());
+ EXPECT_EQ(ETestEnum::Value1, config->Subconfig->MyEnum);
+ EXPECT_EQ(0u, config->SubconfigList.size());
+ EXPECT_EQ(0u, config->SubconfigMap.size());
+}
+
+TEST_P(TYsonStructParseTest, UnrecognizedSimple)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("option").Value(1)
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ auto unrecognizedNode = config->GetLocalUnrecognized();
+ auto unrecognizedRecursivelyNode = config->GetRecursiveUnrecognized();
+ EXPECT_TRUE(AreNodesEqual(unrecognizedNode, unrecognizedRecursivelyNode));
+ EXPECT_EQ(1, unrecognizedNode->GetChildCount());
+ for (const auto& [key, child] : unrecognizedNode->GetChildren()) {
+ EXPECT_EQ("option", key);
+ EXPECT_EQ(1, child->AsInt64()->GetValue());
+ }
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ auto deserializedConfig = ConvertTo<TTestConfigPtr>(output);
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(config), ConvertToNode(deserializedConfig)));
+}
+
+template <EUnrecognizedStrategy strategy>
+class TThrowOnUnrecognized
+ : public TYsonStruct
+{
+public:
+ int IntValue;
+
+ TIntrusivePtr<TSimpleYsonStruct> Nested;
+
+ REGISTER_YSON_STRUCT(TThrowOnUnrecognized);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.UnrecognizedStrategy(strategy);
+
+ registrar.Parameter("int_value", &TThrowOnUnrecognized::IntValue)
+ .Default(1);
+
+ registrar.Parameter("nested", &TThrowOnUnrecognized::Nested)
+ .DefaultNew();
+ }
+};
+
+TEST_P(TYsonStructParseTest, UnrecognizedThrow)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("unrecognized").Value(1)
+ .EndMap();
+
+ Load<TThrowOnUnrecognized<EUnrecognizedStrategy::Drop>>(configNode->AsMap());
+ EXPECT_THROW_WITH_SUBSTRING(
+ Load<TThrowOnUnrecognized<EUnrecognizedStrategy::Throw>>(configNode->AsMap()),
+ "Unrecognized field \"/unrecognized\" has been encountered");
+}
+
+TEST_P(TYsonStructParseTest, UnrecognizedThrowRecursive)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("nested").BeginMap()
+ .Item("unrecognized").Value(1)
+ .EndMap()
+ .EndMap();
+
+ Load<TThrowOnUnrecognized<EUnrecognizedStrategy::Drop>>(configNode->AsMap());
+ EXPECT_THROW_WITH_SUBSTRING(
+ Load<TThrowOnUnrecognized<EUnrecognizedStrategy::ThrowRecursive>>(configNode->AsMap()),
+ "Unrecognized field \"/nested/unrecognized\" has been encountered");
+}
+
+TEST_P(TYsonStructParseTest, UnrecognizedRecursive)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("option").Value(1)
+ .Item("sub").BeginMap()
+ .Item("sub_option").Value(42)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TTestConfig>(configNode->AsMap());
+
+ auto unrecognizedRecursivelyNode = config->GetRecursiveUnrecognized();
+ EXPECT_EQ(2, unrecognizedRecursivelyNode->GetChildCount());
+ for (const auto& [key, child] : unrecognizedRecursivelyNode->GetChildren()) {
+ if (key == "option") {
+ EXPECT_EQ(1, child->AsInt64()->GetValue());
+ } else {
+ EXPECT_EQ("sub", key);
+ EXPECT_EQ(42, child->AsMap()->GetChildOrThrow("sub_option")->AsInt64()->GetValue());
+ }
+ }
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ auto deserializedConfig = ConvertTo<TTestConfigPtr>(output);
+ EXPECT_TRUE(AreNodesEqual(ConvertToNode(config), ConvertToNode(deserializedConfig)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConfigWithOneLevelNesting
+ : public TYsonStruct
+{
+public:
+ TTestSubconfigPtr Subconfig;
+
+ REGISTER_YSON_STRUCT(TConfigWithOneLevelNesting);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("sub", &TThis::Subconfig)
+ .DefaultNew();
+ }
+};
+
+class TConfigWithTwoLevelNesting
+ : public TYsonStruct
+{
+public:
+ TIntrusivePtr<TConfigWithOneLevelNesting> Subconfig;
+
+ REGISTER_YSON_STRUCT(TConfigWithTwoLevelNesting);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.UnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+
+ registrar.Parameter("subconfig", &TThis::Subconfig)
+ .DefaultNew();
+ }
+};
+
+TEST_P(TYsonStructParseTest, UnrecognizedRecursiveTwoLevelNesting)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("subconfig").BeginMap()
+ .Item("sub").BeginMap()
+ .Item("unrecognized_option").Value(42)
+ .EndMap()
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TConfigWithTwoLevelNesting>(configNode->AsMap());
+
+ auto unrecognized = config->GetRecursiveUnrecognized();
+ EXPECT_EQ(
+ ConvertToYsonString(configNode, EYsonFormat::Text).AsStringBuf(),
+ ConvertToYsonString(unrecognized, EYsonFormat::Text).AsStringBuf());
+}
+
+TEST_P(TYsonStructParseTest, UnrecognizedWithNestedYsonSerializable)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("yson_serializable").BeginMap()
+ .Item("unrecognized").Value(1)
+ .EndMap()
+ .EndMap();
+
+ auto config = Load<TYsonStructWithSimpleYsonSerializable>(configNode->AsMap());
+
+ auto unrecognized = config->GetRecursiveUnrecognized();
+ EXPECT_EQ(
+ ConvertToYsonString(configNode, EYsonFormat::Text).AsStringBuf(),
+ ConvertToYsonString(unrecognized, EYsonFormat::Text).AsStringBuf());
+}
+
+TEST_P(TYsonStructParseTest, MissingRequiredParameter)
+{
+ auto configNode = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(99)
+ .Item("my_bool").Value(true)
+ .EndMap()
+ .EndMap();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, IncorrectNodeType)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value(1) // incorrect type
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, ArithmeticOverflow)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(Max<i64>())
+ .Item("my_bool").Value(true)
+ .Item("my_enum").Value("Value2")
+ .Item("my_string_list").BeginList()
+ .Item().Value("ListItem0")
+ .Item().Value("ListItem1")
+ .Item().Value("ListItem2")
+ .EndList()
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ EXPECT_THROW(Load<TTestConfig>(configNode->AsMap()), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, Postprocess)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("") // empty!
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode->AsMap(), false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, PostprocessSubconfig)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode->AsMap(), false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, PostprocessSubconfigList)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub_list").BeginList()
+ .Item().BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndList()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode->AsMap(), false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST_P(TYsonStructParseTest, PostprocessSubconfigMap)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_string").Value("TestString")
+ .Item("sub_map").BeginMap()
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(210) // out of range
+ .EndMap()
+ .EndMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = Load<TTestConfig>(configNode->AsMap(), false);
+ EXPECT_THROW(config->Postprocess(), std::exception);
+}
+
+TEST(TYsonStructTest, SaveSingleParameter)
+{
+ auto config = New<TTestConfig>();
+ config->MyString = "test";
+ config->NullableInt = 10;
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ config->SaveParameter("my_string", builder.get());
+ auto actual = ConvertTo<TString>(builder->EndTree());
+ EXPECT_EQ("test", actual);
+}
+
+TEST(TYsonStructTest, LoadSingleParameter)
+{
+ auto config = New<TTestConfig>();
+ config->NullableInt = 10;
+
+ config->LoadParameter("my_string", ConvertToNode("test"), EMergeStrategy::Default);
+ EXPECT_EQ("test", config->MyString);
+ EXPECT_EQ(10, config->NullableInt);
+}
+
+TEST(TYsonStructTest, LoadSingleParameterWithMergeStrategy)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("my_int").Value(100)
+ .EndMap();
+ auto subConfig = builder->EndTree();
+
+ auto config1 = New<TTestConfig>();
+ config1->Subconfig->MyBool = true;
+ config1->LoadParameter("sub", subConfig, EMergeStrategy::Default);
+ EXPECT_EQ(100, config1->Subconfig->MyInt);
+ EXPECT_TRUE(config1->Subconfig->MyBool); // Subconfig merged by default.
+
+ auto config2 = New<TTestConfig>();
+ config2->Subconfig->MyBool = true;
+ config2->LoadParameter("sub", subConfig, EMergeStrategy::Overwrite);
+ EXPECT_EQ(100, config2->Subconfig->MyInt);
+ EXPECT_FALSE(config2->Subconfig->MyBool); // Overwrite destroyed previous values.
+}
+
+TEST(TYsonStructTest, ResetSingleParameter)
+{
+ auto config = New<TTestSubconfig>();
+ config->MyInt = 10;
+ config->MyUint = 10;
+
+ config->ResetParameter("my_int");
+ EXPECT_EQ(100, config->MyInt); // Default value.
+ EXPECT_EQ(10u, config->MyUint);
+}
+
+TEST(TYsonStructTest, Save)
+{
+ auto config = New<TTestConfig>();
+
+ // add non-default fields;
+ config->MyString = "hello!";
+ config->SubconfigList.push_back(New<TTestSubconfig>());
+ config->SubconfigMap["item"] = New<TTestSubconfig>();
+ config->NullableInt = 42;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString subconfigYson =
+ "{\"my_bool\"=%false;"
+ "\"my_enum\"=\"value1\";"
+ "\"my_int\"=200;"
+ "\"my_uint\"=50u;"
+ "\"my_string_list\"=[]}";
+
+ TString subconfigYsonOrigin =
+ "{\"my_bool\"=%false;"
+ "\"my_enum\"=\"value1\";"
+ "\"my_int\"=100;"
+ "\"my_uint\"=50u;"
+ "\"my_string_list\"=[]}";
+
+ TString expectedYson;
+ expectedYson += "{\"my_string\"=\"hello!\";";
+ expectedYson += "\"sub\"=" + subconfigYson + ";";
+ expectedYson += "\"sub_list\"=[" + subconfigYsonOrigin + "];";
+ expectedYson += "\"sub_map\"={\"item\"=" + subconfigYsonOrigin + "};";
+ expectedYson += "\"nullable_int\"=42}";
+
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+}
+
+TEST(TYsonStructTest, TestConfigUpdate)
+{
+ auto config = New<TTestConfig>();
+ {
+ auto newConfig = UpdateYsonStruct(config, nullptr);
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 200);
+ }
+
+ {
+ auto newConfig = UpdateYsonStruct(config, ConvertToNode(TYsonString(TStringBuf("{\"sub\"={\"my_int\"=150}}"))));
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 150);
+ }
+
+ {
+ auto newConfig = UpdateYsonStruct(config, ConvertToNode(TYsonString(TStringBuf("{\"sub\"={\"my_int_\"=150}}"))));
+ EXPECT_EQ(newConfig->Subconfig->MyInt, 200);
+ }
+}
+
+TEST(TYsonStructTest, NoDefaultNewAliasing)
+{
+ auto config1 = New<TTestConfig>();
+ auto config2 = New<TTestConfig>();
+ EXPECT_NE(config1->Subconfig, config2->Subconfig);
+}
+
+TEST(TYsonStructTest, Reconfigure)
+{
+ auto config = New<TTestConfig>();
+ auto subconfig = config->Subconfig;
+
+ EXPECT_EQ("x", config->MyString);
+ EXPECT_EQ(200, subconfig->MyInt);
+
+ auto patch1 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("y")
+ .EndMap();
+ ReconfigureYsonStruct(config, patch1);
+
+ EXPECT_EQ("y", config->MyString);
+ EXPECT_EQ(subconfig, config->Subconfig);
+ EXPECT_EQ(200, subconfig->MyInt);
+
+ auto patch2 = BuildYsonNodeFluently()
+ .BeginMap()
+ .Item("my_string").Value("z")
+ .Item("sub").BeginMap()
+ .Item("my_int").Value(95)
+ .EndMap()
+ .EndMap();
+ ReconfigureYsonStruct(config, patch2);
+
+ EXPECT_EQ("z", config->MyString);
+ EXPECT_EQ(subconfig, config->Subconfig);
+ EXPECT_EQ(95, subconfig->MyInt);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfigLite
+ : public TYsonStructLite
+{
+public:
+ TString MyString;
+ std::optional<i64> NullableInt;
+
+ REGISTER_YSON_STRUCT_LITE(TTestConfigLite);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("my_string", &TThis::MyString).NonEmpty();
+ registrar.Parameter("nullable_int", &TThis::NullableInt).Default();
+ }
+};
+
+TEST(TYsonStructTest, SaveLite)
+{
+ TTestConfigLite config = TTestConfigLite::Create();
+
+ config.MyString = "hello!";
+ config.NullableInt = 42;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson;
+ expectedYson += "{\"my_string\"=\"hello!\";";
+ expectedYson += "\"nullable_int\"=42}";
+
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+}
+
+TEST(TYsonStructTest, NewRefCountedInitedWithDefaults)
+{
+ auto config = New<TTestConfig>();
+ EXPECT_EQ(config->MyString, "x");
+ EXPECT_TRUE(config->Subconfig != nullptr);
+ EXPECT_EQ(config->Subconfig->MyInt, 200);
+}
+
+class TTestLiteWithDefaults
+ : public TYsonStructLite
+{
+public:
+ TString MyString;
+ int MyInt;
+ TTestSubconfigPtr Subconfig;
+
+ REGISTER_YSON_STRUCT_LITE(TTestLiteWithDefaults);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("my_string", &TThis::MyString)
+ .Default("y");
+ registrar.Parameter("subconfig", &TThis::Subconfig)
+ .DefaultNew();
+ registrar.Preprocessor([] (TTestLiteWithDefaults* conf) {
+ conf->MyInt = 10;
+ });
+ }
+};
+
+TEST(TYsonStructTest, NewLiteInitedWithDefaults)
+{
+ TTestLiteWithDefaults config = TTestLiteWithDefaults::Create();
+ EXPECT_EQ(config.MyString, "y");
+ EXPECT_EQ(config.MyInt, 10);
+ EXPECT_TRUE(config.Subconfig != nullptr);
+ EXPECT_EQ(config.Subconfig->MyInt, 100);
+}
+
+TEST(TYsonStructTest, TestConvertToLite)
+{
+ auto deserialized = ConvertTo<TTestLiteWithDefaults>(TYsonString(TStringBuf("{}")));
+ EXPECT_EQ(deserialized.MyString, "y");
+ EXPECT_EQ(deserialized.MyInt, 10);
+ EXPECT_NE(deserialized.Subconfig, nullptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TTestConfigWithAliases
+ : public TYsonStruct
+{
+public:
+ TString Value;
+
+ REGISTER_YSON_STRUCT(TTestConfigWithAliases);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("key", &TThis::Value)
+ .Alias("alias1")
+ .Alias("alias2");
+ }
+};
+
+TEST(TYsonStructTest, Aliases1)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("key").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = New<TTestConfigWithAliases>();
+ config->Load(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST(TYsonStructTest, Aliases2)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = New<TTestConfigWithAliases>();
+ config->Load(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST(TYsonStructTest, Aliases3)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value")
+ .Item("alias2").Value("value")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = New<TTestConfigWithAliases>();
+ config->Load(configNode->AsMap(), false);
+
+ EXPECT_EQ("value", config->Value);
+}
+
+TEST(TYsonStructTest, Aliases4)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .Item("alias1").Value("value1")
+ .Item("alias2").Value("value2")
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = New<TTestConfigWithAliases>();
+
+ EXPECT_THROW(config->Load(configNode->AsMap()), std::exception);
+}
+
+TEST(TYsonStructTest, Aliases5)
+{
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ builder->BeginTree();
+ BuildYsonFluently(builder.get())
+ .BeginMap()
+ .EndMap();
+ auto configNode = builder->EndTree();
+
+ auto config = New<TTestConfigWithAliases>();
+
+ EXPECT_THROW(config->Load(configNode->AsMap()), std::exception);
+}
+
+class TTestConfigWithContainers
+ : public NYTree::TYsonStructLite
+{
+public:
+ std::vector<TString> Vector;
+ std::array<TString, 3> Array;
+ std::pair<size_t, TString> Pair;
+ std::set<TString> Set;
+ std::map<TString, int> Map;
+ std::multiset<int> MultiSet;
+ std::unordered_set<TString> UnorderedSet;
+ std::unordered_map<TString, int> UnorderedMap;
+ std::unordered_multiset<size_t> UnorderedMultiSet;
+
+ REGISTER_YSON_STRUCT_LITE(TTestConfigWithContainers);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("vector", &TThis::Vector)
+ .Default();
+ registrar.Parameter("array", &TThis::Array)
+ .Default();
+ registrar.Parameter("pair", &TThis::Pair)
+ .Default();
+ registrar.Parameter("set", &TThis::Set)
+ .Default();
+ registrar.Parameter("map", &TThis::Map)
+ .Default();
+ registrar.Parameter("multiset", &TThis::MultiSet)
+ .Default();
+ registrar.Parameter("unordered_set", &TThis::UnorderedSet)
+ .Default();
+ registrar.Parameter("unordered_map", &TThis::UnorderedMap)
+ .Default();
+ registrar.Parameter("unordered_multiset", &TThis::UnorderedMultiSet)
+ .Default();
+ }
+};
+
+TEST(TYsonStructTest, ParameterTuplesAndContainers)
+{
+ TTestConfigWithContainers original = TTestConfigWithContainers::Create();
+ TTestConfigWithContainers deserialized = TTestConfigWithContainers::Create();
+
+ original.Vector = { "fceswf", "sadfcesa" };
+ original.Array = {{ "UYTUY", ":LL:a", "78678678" }};
+ original.Pair = { 7U, "UYTUY" };
+ original.Set = { " q!", "12343e", "svvr", "0001" };
+ original.Map = { {"!", 4398}, {"zzz", 0} };
+ original.MultiSet = { 33, 33, 22, 22, 11 };
+ original.UnorderedSet = { "41", "52", "001", "set" };
+ original.UnorderedMap = { {"12345", 8}, {"XXX", 9}, {"XYZ", 42} };
+ original.UnorderedMultiSet = { 1U, 2U, 1U, 0U, 0U };
+
+ Deserialize(deserialized, ConvertToNode(ConvertToYsonString(original)));
+
+ EXPECT_EQ(original.Vector, deserialized.Vector);
+ EXPECT_EQ(original.Array, deserialized.Array);
+ EXPECT_EQ(original.Pair, deserialized.Pair);
+ EXPECT_EQ(original.Set, deserialized.Set);
+ EXPECT_EQ(original.Map, deserialized.Map);
+ EXPECT_EQ(original.MultiSet, deserialized.MultiSet);
+ EXPECT_EQ(original.UnorderedSet, deserialized.UnorderedSet);
+ EXPECT_EQ(original.UnorderedMap, deserialized.UnorderedMap);
+ EXPECT_EQ(original.UnorderedMultiSet, deserialized.UnorderedMultiSet);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYsonStructTest, EnumAsKeyToYHash)
+{
+ THashMap<ETestEnum, TString> deserialized, original = {
+ {ETestEnum::Value0, "abc"}
+ };
+
+ TString serialized = "{\"value0\"=\"abc\";}";
+ ASSERT_EQ(serialized, ConvertToYsonString(original, EYsonFormat::Text).AsStringBuf());
+
+ Deserialize(deserialized, ConvertToNode(TYsonString(serialized, EYsonType::Node)));
+
+ ASSERT_EQ(original, deserialized);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConfigWithOptional
+ : public TYsonStruct
+{
+public:
+ std::optional<int> Value;
+
+ REGISTER_YSON_STRUCT(TConfigWithOptional);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("value", &TThis::Value)
+ .Default(123);
+ }
+};
+
+TEST(TYsonStructTest, NullableWithNonNullDefault)
+{
+ {
+ auto config = ConvertTo<TIntrusivePtr<TConfigWithOptional>>(TYsonString(TStringBuf("{}")));
+ EXPECT_EQ(123, *config->Value);
+ EXPECT_EQ(123, ConvertToNode(config)->AsMap()->GetChildOrThrow("value")->GetValue<i64>());
+ }
+
+ {
+ auto config = ConvertTo<TIntrusivePtr<TConfigWithOptional>>(TYsonString(TStringBuf("{value=#}")));
+ EXPECT_FALSE(config->Value);
+ EXPECT_EQ(ENodeType::Entity, ConvertToNode(config)->AsMap()->GetChildOrThrow("value")->GetType());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TConfigWithDontSerializeDefault
+ : public TYsonStruct
+{
+public:
+ int Value;
+ int OtherValue;
+
+ REGISTER_YSON_STRUCT(TConfigWithDontSerializeDefault);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("value", &TThis::Value)
+ .Default(123);
+ registrar.Parameter("other_value", &TThis::OtherValue)
+ .Default(456)
+ .DontSerializeDefault();
+ }
+};
+
+TEST(TYsonStructTest, DontSerializeDefault)
+{
+ {
+ auto config = New<TConfigWithDontSerializeDefault>();
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+ }
+
+ {
+ auto config = New<TConfigWithDontSerializeDefault>();
+ config->OtherValue = 789;
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;\"other_value\"=789;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+ }
+}
+
+class TVirtualInheritanceConfig
+ : public virtual TYsonStruct
+{
+public:
+ int Value;
+
+ REGISTER_YSON_STRUCT(TVirtualInheritanceConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("value", &TThis::Value)
+ .Default(123);
+ }
+};
+
+TEST(TYsonStructTest, VirtualInheritance)
+{
+ {
+ auto config = New<TVirtualInheritanceConfig>();
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+ }
+}
+
+class TBase
+ : public TYsonStruct
+{
+public:
+ int Value;
+
+ REGISTER_YSON_STRUCT(TBase);
+
+ static void Register(TRegistrar)
+ { }
+};
+
+class TDerived
+ : public TBase
+{
+public:
+
+ REGISTER_YSON_STRUCT(TDerived);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.BaseClassParameter("value", &TDerived::Value)
+ .Default(123);
+ }
+};
+
+TEST(TYsonStructTest, RegisterBaseFieldInDerived)
+{
+ {
+ auto config = New<TDerived>();
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=123;}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSerializableClass
+ : public TYsonSerializable
+{
+public:
+ TYsonSerializableClass()
+ {
+ RegisterParameter("int_value", IntValue)
+ .Default(1);
+ }
+
+ int IntValue;
+};
+
+class TYsonStructClass
+ : public TYsonStruct
+{
+public:
+ THashMap<TString, TIntrusivePtr<TYsonSerializableClass>> YsonSerializableHashMap;
+
+ TIntrusivePtr<TYsonSerializableClass> YsonSerializableValue;
+
+ REGISTER_YSON_STRUCT(TYsonStructClass);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("yson_serializable_hash_map", &TThis::YsonSerializableHashMap)
+ .Default();
+
+ registrar.Parameter("yson_serializable_value", &TThis::YsonSerializableValue)
+ .DefaultNew();
+
+ registrar.Preprocessor([] (TYsonStructClass* klass) {
+ klass->YsonSerializableValue->IntValue = 5;
+ });
+ }
+};
+
+TEST(TYsonStructTest, YsonSerializableNestedToYsonStructSimple)
+{
+ {
+ auto config = New<TYsonStructClass>();
+ EXPECT_EQ(config->YsonSerializableValue->IntValue, 5);
+
+ config->YsonSerializableHashMap["x"] = New<TYsonSerializableClass>();
+ config->YsonSerializableHashMap["x"]->IntValue = 10;
+ config->YsonSerializableValue->IntValue = 2;
+
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+ TString expectedYson = "{yson_serializable_hash_map={x={int_value=10}};yson_serializable_value={int_value=2}}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonStructClass>>(output);
+ EXPECT_EQ(deserialized->YsonSerializableHashMap["x"]->IntValue, 10);
+ EXPECT_EQ(deserialized->YsonSerializableValue->IntValue, 2);
+
+ }
+}
+
+TEST(TYsonStructTest, YsonSerializableNestedToYsonStructDeserializesFromEmpty)
+{
+ {
+ auto testInput = TYsonString(TStringBuf("{yson_serializable_value={}}"));
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonStructClass>>(testInput);
+ EXPECT_EQ(deserialized->YsonSerializableValue->IntValue, 5);
+ }
+}
+////////////////////////////////////////////////////////////////////////////////
+
+class TClassLevelPostprocessConfig
+ : public TYsonStruct
+{
+public:
+ int Value;
+
+ REGISTER_YSON_STRUCT(TClassLevelPostprocessConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("value", &TThis::Value)
+ .Default();
+ registrar.Postprocessor([] (TClassLevelPostprocessConfig* config) {
+ config->Value = 10;
+ });
+ }
+};
+
+TEST(TYsonStructTest, ClassLevelPostprocess)
+{
+ {
+ auto config = New<TClassLevelPostprocessConfig>();
+ config->Value = 1;
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=1}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+
+ auto deserialized = ConvertTo<TIntrusivePtr<TClassLevelPostprocessConfig>>(output);
+ EXPECT_EQ(deserialized->Value, 10);
+
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRecursiveConfig
+ : public TYsonStruct
+{
+public:
+ TIntrusivePtr<TRecursiveConfig> Subconfig;
+
+ int Value;
+
+ REGISTER_YSON_STRUCT(TRecursiveConfig);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("subconfig", &TThis::Subconfig)
+ .Default();
+ registrar.Parameter("value", &TThis::Value)
+ .Default();
+ }
+};
+
+TEST(TYsonStructTest, RecursiveConfig)
+{
+ {
+ auto config = New<TRecursiveConfig>();
+ config->Value = 1;
+ config->Subconfig = New<TRecursiveConfig>();
+ config->Subconfig->Value = 3;
+ auto output = ConvertToYsonString(config, NYson::EYsonFormat::Text);
+
+ TString expectedYson = "{\"value\"=1;\"subconfig\"={\"value\"=3}}";
+ EXPECT_TRUE(AreNodesEqual(
+ ConvertToNode(TYsonString(expectedYson)),
+ ConvertToNode(TYsonString(output.AsStringBuf()))));
+
+ auto deserialized = ConvertTo<TIntrusivePtr<TRecursiveConfig>>(output);
+ EXPECT_EQ(deserialized->Value, 1);
+ EXPECT_EQ(deserialized->Subconfig->Value, 3);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNestedYsonSerializableClass
+ : public TYsonSerializable
+{
+public:
+ TNestedYsonSerializableClass()
+ {
+ RegisterParameter("int_value", IntValue)
+ .Default(1);
+ RegisterPostprocessor([&] {
+ IntValue = 10;
+ });
+ }
+
+ int IntValue;
+};
+
+class TYsonStructClass2
+ : public TYsonStruct
+{
+public:
+ THashMap<TString, TIntrusivePtr<TNestedYsonSerializableClass>> YsonSerializableHashMap;
+
+ REGISTER_YSON_STRUCT(TYsonStructClass2);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("yson_serializable_hash_map", &TYsonStructClass2::YsonSerializableHashMap)
+ .Default();
+ }
+};
+
+TEST(TYsonStructTest, PostprocessIsPropagatedFromYsonStructToYsonSerializable)
+{
+ auto testInput = TYsonString(TStringBuf("{yson_serializable_hash_map={x={int_value=2}}}"));
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonStructClass2>>(testInput);
+ EXPECT_EQ(deserialized->YsonSerializableHashMap["x"]->IntValue, 10);
+}
+
+template <class T>
+TIntrusivePtr<T> CreateCustomDefault()
+{
+ auto result = New<T>();
+ result->IntValue = 10;
+ return result;
+}
+
+class TYsonStructWithNestedStructsAndCustomDefaults
+ : public TYsonStruct
+{
+public:
+ TIntrusivePtr<TSimpleYsonSerializable> YsonSerializable;
+ TIntrusivePtr<TSimpleYsonStruct> YsonStruct;
+
+ REGISTER_YSON_STRUCT(TYsonStructWithNestedStructsAndCustomDefaults);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("yson_serializable", &TThis::YsonSerializable)
+ .DefaultCtor([] () { return CreateCustomDefault<TSimpleYsonSerializable>(); });
+ registrar.Parameter("yson_struct", &TThis::YsonStruct)
+ .DefaultCtor([] () { return CreateCustomDefault<TSimpleYsonStruct>(); });
+ }
+};
+
+TEST(TYsonStructTest, TestCustomDefaultsOfNestedStructsAreDiscardedOnDeserialize)
+{
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonStructWithNestedStructsAndCustomDefaults>>(TYsonString(TStringBuf("{}")));
+ EXPECT_EQ(deserialized->YsonSerializable->IntValue, 1);
+ EXPECT_EQ(deserialized->YsonStruct->IntValue, 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStructWithNestedStructsAndPreprocessors
+ : public TYsonStruct
+{
+public:
+ TIntrusivePtr<TSimpleYsonSerializable> YsonSerializable;
+ TIntrusivePtr<TSimpleYsonStruct> YsonStruct;
+
+ REGISTER_YSON_STRUCT(TYsonStructWithNestedStructsAndPreprocessors);
+
+ static void Register(TRegistrar registrar)
+ {
+ registrar.Parameter("yson_struct", &TThis::YsonStruct)
+ .Default();
+ registrar.Parameter("yson_serializable", &TThis::YsonSerializable)
+ .Default();
+ registrar.Preprocessor([] (TThis* s) {
+ s->YsonSerializable = CreateCustomDefault<TSimpleYsonSerializable>();
+ s->YsonStruct = CreateCustomDefault<TSimpleYsonStruct>();
+ });
+ }
+};
+
+TEST(TYsonStructTest, TestPreprocessorsEffectsOnNestedStructsArePreservedOnDeserialize)
+{
+ auto deserialized = ConvertTo<TIntrusivePtr<TYsonStructWithNestedStructsAndPreprocessors>>(TYsonString(TStringBuf("{}")));
+ EXPECT_EQ(deserialized->YsonSerializable->IntValue, 10);
+ EXPECT_EQ(deserialized->YsonStruct->IntValue, 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TBaseWithCustomConfigure
+ : public TYsonStruct
+{
+public:
+ int Int;
+ double Double;
+
+ REGISTER_YSON_STRUCT(TBaseWithCustomConfigure);
+
+ static void Register(TRegistrar)
+ { }
+
+protected:
+ static void CustomConfigure(TRegistrar registrar, int defaultInt, double defaultDouble)
+ {
+ registrar.Parameter("int", &TThis::Int)
+ .Default(defaultInt);
+ registrar.Postprocessor([defaultDouble] (TThis* s) {
+ s->Double = defaultDouble;
+ });
+ }
+};
+
+class TDerivedWithCustomConfigure
+ : public TBaseWithCustomConfigure
+{
+public:
+
+ REGISTER_YSON_STRUCT(TDerivedWithCustomConfigure);
+
+ static void Register(TRegistrar registrar)
+ {
+ CustomConfigure(registrar, 10, 2.2);
+ }
+};
+
+TEST(TYsonStructTest, TestHierarchiesWithCustomInitializationOfBaseParameters)
+{
+ auto deserialized = ConvertTo<TIntrusivePtr<TDerivedWithCustomConfigure>>(TYsonString(TStringBuf("{}")));
+ EXPECT_EQ(deserialized->Int, 10);
+ EXPECT_EQ(deserialized->Double, 2.2);
+}
+
+TEST(TYsonStructTest, TestSimpleSerialization)
+{
+ TBufferStream stream;
+
+ auto initialize = [] (auto& config) {
+ config.MyString = "TestString";
+ config.NullableInt.emplace(42);
+ };
+
+ TIntrusivePtr<TTestConfig> config;
+ auto defaultConfig = New<TTestConfig>();
+
+ ::Save(&stream, config);
+ ::Load(&stream, config);
+ EXPECT_FALSE(config);
+
+ ::Save(&stream, defaultConfig);
+ ::Load(&stream, config);
+ EXPECT_TRUE(config);
+
+ EXPECT_EQ(config->MyString, defaultConfig->MyString);
+ EXPECT_FALSE(config->NullableInt);
+ EXPECT_EQ(config->Subconfig->MyInt, defaultConfig->Subconfig->MyInt);
+
+ config = New<TTestConfig>();
+ initialize(*config);
+ ::Save(&stream, config);
+
+ config = nullptr;
+ ::Load(&stream, config);
+ EXPECT_EQ(config->MyString, "TestString");
+ EXPECT_EQ(config->NullableInt, 42);
+
+ auto liteConfig = TTestConfigLite::Create();
+ initialize(liteConfig);
+ ::Save(&stream, liteConfig);
+
+ liteConfig.SetDefaults();
+ ::Load(&stream, liteConfig);
+ EXPECT_EQ(liteConfig.MyString, "TestString");
+ EXPECT_EQ(liteConfig.NullableInt, 42);
+}
+
+TEST(TYsonStructTest, TestComplexSerialization)
+{
+ struct TComplexStruct
+ {
+ TTestConfigPtr Config1;
+ TTestConfigPtr Config2;
+ TTestConfigLite LiteConfig = TTestConfigLite::Create();
+ TString StructName;
+
+ Y_SAVELOAD_DEFINE(Config1, Config2, LiteConfig, StructName);
+ };
+
+ TComplexStruct toSerialize{
+ .Config1 = New<TTestConfig>(),
+ .Config2 = New<TTestConfig>(),
+ .LiteConfig = TTestConfigLite::Create(),
+ .StructName = "tmp",
+ };
+ toSerialize.Config1->Load(GetCompleteConfigNode());
+ toSerialize.Config2->Load(GetCompleteConfigNode(/*offset*/ 1));
+ toSerialize.LiteConfig.MyString = "LiteConfig";
+ toSerialize.LiteConfig.NullableInt.emplace(42);
+
+ TBufferStream stream;
+
+ ::Save(&stream, toSerialize);
+
+ TComplexStruct deserialized;
+ ::Load(&stream, deserialized);
+
+ {
+ SCOPED_TRACE("First deserialized config.");
+ TestCompleteConfig(deserialized.Config1);
+ }
+ {
+ SCOPED_TRACE("Second deserialized config.");
+ TestCompleteConfig(deserialized.Config2, /*offset*/ 1);
+ }
+ EXPECT_EQ(deserialized.LiteConfig.MyString, "LiteConfig");
+ EXPECT_EQ(deserialized.LiteConfig.NullableInt, 42);
+ EXPECT_EQ(deserialized.StructName, "tmp");
+
+ std::vector<TTestConfigPtr> configsList;
+ configsList.reserve(5);
+ for (int i = 0; i < 5; ++i) {
+ auto config = New<TTestConfig>();
+ config->Load(GetCompleteConfigNode(/*offset*/ i));
+ configsList.push_back(std::move(config));
+ }
+ ::Save(&stream, configsList);
+ configsList.clear();
+ ::Load(&stream, configsList);
+
+ for (int i = 0; i < 5; ++i) {
+ SCOPED_TRACE(Format("%v-th config from configs list", i));
+ TestCompleteConfig(configsList[i], /*offset*/ i);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/ytree_fluent_ut.cpp b/yt/yt/core/ytree/unittests/ytree_fluent_ut.cpp
new file mode 100644
index 0000000000..aeff8ccadc
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/ytree_fluent_ut.cpp
@@ -0,0 +1,432 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/test_framework/yson_consumer_mock.h>
+
+#include <yt/yt/core/ytree/fluent.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+using ::testing::Types;
+using ::testing::InSequence;
+using ::testing::StrictMock;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(sandello): Fix this test under clang.
+#ifndef __clang__
+// String-like Scalars {{{
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TYTreeFluentStringScalarTest
+ : public ::testing::Test
+{
+};
+
+TYPED_TEST_SUITE_P(TYTreeFluentStringScalarTest);
+TYPED_TEST_P(TYTreeFluentStringScalarTest, Ok)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnStringScalar("Hello World"));
+
+ TypeParam passedScalar = "Hello World";
+ BuildYsonFluently(&mock)
+ .Value(passedScalar);
+}
+
+typedef Types<const char*, TString> TYTreeFluentStringScalarTestTypes;
+
+REGISTER_TYPED_TEST_SUITE_P(TYTreeFluentStringScalarTest, Ok);
+INSTANTIATE_TYPED_TEST_SUITE_P(
+ TypeParametrized,
+ TYTreeFluentStringScalarTest,
+ TYTreeFluentStringScalarTestTypes
+);
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+// Int64-like Scalars {{{
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TYTreeFluentIntScalarTest
+ : public ::testing::Test
+{
+};
+
+TYPED_TEST_SUITE_P(TYTreeFluentIntScalarTest);
+TYPED_TEST_P(TYTreeFluentIntScalarTest, Ok)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnInt64Scalar(42));
+
+ TypeParam passedScalar = 42;
+ BuildYsonFluently(&mock)
+ .Value(passedScalar);
+}
+
+typedef Types<i8, i16, i32, i64>
+ TYTreeFluentIntScalarTestTypes;
+REGISTER_TYPED_TEST_SUITE_P(TYTreeFluentIntScalarTest, Ok);
+INSTANTIATE_TYPED_TEST_SUITE_P(TypeParametrized, TYTreeFluentIntScalarTest,
+ TYTreeFluentIntScalarTestTypes
+);
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+// Uint64-like Scalars {{{
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TYTreeFluentUintScalarTest
+ : public ::testing::Test
+{
+};
+
+TYPED_TEST_SUITE_P(TYTreeFluentUintScalarTest);
+TYPED_TEST_P(TYTreeFluentUintScalarTest, Ok)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnUint64Scalar(42));
+
+ TypeParam passedScalar = 42;
+ BuildYsonFluently(&mock)
+ .Value(passedScalar);
+}
+
+typedef Types<ui8, ui16, ui32, ui64>
+ TYTreeFluentUintScalarTestTypes;
+REGISTER_TYPED_TEST_SUITE_P(TYTreeFluentUintScalarTest, Ok);
+INSTANTIATE_TYPED_TEST_SUITE_P(TypeParametrized, TYTreeFluentUintScalarTest,
+ TYTreeFluentUintScalarTestTypes
+);
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+// Float-like Scalars {{{
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TYTreeFluentFloatScalarTest
+ : public ::testing::Test
+{
+};
+
+TYPED_TEST_SUITE_P(TYTreeFluentFloatScalarTest);
+TYPED_TEST_P(TYTreeFluentFloatScalarTest, Ok)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnDoubleScalar(::testing::DoubleEq(3.14f)));
+
+ TypeParam passedScalar = 3.14f;
+ BuildYsonFluently(&mock)
+ .Value(passedScalar);
+}
+
+typedef Types<float, double>
+ TYTreeFluentFloatScalarTestTypes;
+REGISTER_TYPED_TEST_SUITE_P(TYTreeFluentFloatScalarTest, Ok);
+INSTANTIATE_TYPED_TEST_SUITE_P(TypeParametrized, TYTreeFluentFloatScalarTest,
+ TYTreeFluentFloatScalarTestTypes
+);
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+// Map {{{
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYTreeFluentMapTest, Empty)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnEndMap());
+
+ BuildYsonFluently(&mock)
+ .BeginMap()
+ .EndMap();
+}
+
+TEST(TYTreeFluentMapTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("foo"));
+ EXPECT_CALL(mock, OnInt64Scalar(10));
+ EXPECT_CALL(mock, OnKeyedItem("bar"));
+ EXPECT_CALL(mock, OnInt64Scalar(20));
+ EXPECT_CALL(mock, OnEndMap());
+
+ BuildYsonFluently(&mock)
+ .BeginMap()
+ .Item("foo")
+ .Value(10)
+
+ .Item("bar")
+ .Value(20)
+ .EndMap();
+}
+
+TEST(TYTreeFluentMapTest, Items)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ auto node = ConvertToNode(TYsonString("{bar = 10}"));
+
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("bar"));
+ EXPECT_CALL(mock, OnInt64Scalar(10));
+ EXPECT_CALL(mock, OnEndMap());
+
+ BuildYsonFluently(&mock)
+ .BeginMap()
+ .Items(node->AsMap())
+ .EndMap();
+}
+
+
+TEST(TYTreeFluentMapTest, Nested)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("foo"));
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("xxx"));
+ EXPECT_CALL(mock, OnInt64Scalar(17));
+ EXPECT_CALL(mock, OnEndMap());
+ EXPECT_CALL(mock, OnKeyedItem("bar"));
+ EXPECT_CALL(mock, OnInt64Scalar(42));
+ EXPECT_CALL(mock, OnEndMap());
+
+ BuildYsonFluently(&mock)
+ .BeginMap()
+ .Item("foo")
+ .BeginMap()
+ .Item("xxx")
+ .Value(17)
+ .EndMap()
+
+ .Item("bar")
+ .Value(42)
+ .EndMap();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+// List {{{
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TYTreeFluentListTest, Empty)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnEndList());
+
+ BuildYsonFluently(&mock)
+ .BeginList()
+ .EndList();
+}
+
+TEST(TYTreeFluentListTest, Simple)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("foo"));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("bar"));
+ EXPECT_CALL(mock, OnEndList());
+
+ BuildYsonFluently(&mock)
+ .BeginList()
+ .Item()
+ .Value("foo")
+
+ .Item()
+ .Value("bar")
+ .EndList();
+}
+
+TEST(TYTreeFluentListTest, Items)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ auto node = ConvertToNode(TYsonString("[10; 20; 30]"));
+
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnInt64Scalar(10));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnInt64Scalar(20));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnInt64Scalar(30));
+ EXPECT_CALL(mock, OnEndList());
+
+ BuildYsonFluently(&mock)
+ .BeginList()
+ .Items(node->AsList())
+ .EndList();
+}
+
+TEST(TYTreeFluentListTest, Nested)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("foo"));
+ EXPECT_CALL(mock, OnEndList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("bar"));
+ EXPECT_CALL(mock, OnEndList());
+
+ BuildYsonFluently(&mock)
+ .BeginList()
+ .Item()
+ .BeginList()
+ .Item()
+ .Value("foo")
+ .EndList()
+
+ .Item()
+ .Value("bar")
+ .EndList();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// }}}
+
+TEST(TYTreeFluentTest, Complex)
+{
+ StrictMock<TMockYsonConsumer> mock;
+ InSequence dummy;
+
+ EXPECT_CALL(mock, OnBeginList());
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginAttributes());
+ EXPECT_CALL(mock, OnKeyedItem("attr1"));
+ EXPECT_CALL(mock, OnInt64Scalar(-1));
+ EXPECT_CALL(mock, OnKeyedItem("attr2"));
+ EXPECT_CALL(mock, OnInt64Scalar(-2));
+ EXPECT_CALL(mock, OnEndAttributes());
+ EXPECT_CALL(mock, OnInt64Scalar(42));
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnInt64Scalar(17));
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnEndList());
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginAttributes());
+ EXPECT_CALL(mock, OnKeyedItem("hot"));
+ EXPECT_CALL(mock, OnStringScalar("chocolate"));
+ EXPECT_CALL(mock, OnEndAttributes());
+ EXPECT_CALL(mock, OnBeginList());
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("hello"));
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnStringScalar("world"));
+ EXPECT_CALL(mock, OnEndList());
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginMap());
+ EXPECT_CALL(mock, OnKeyedItem("aaa"));
+ EXPECT_CALL(mock, OnInt64Scalar(1));
+ EXPECT_CALL(mock, OnKeyedItem("bbb"));
+ EXPECT_CALL(mock, OnInt64Scalar(2));
+ EXPECT_CALL(mock, OnEndMap());
+
+ EXPECT_CALL(mock, OnListItem());
+ EXPECT_CALL(mock, OnBeginAttributes());
+ EXPECT_CALL(mock, OnKeyedItem("type"));
+ EXPECT_CALL(mock, OnStringScalar("extra"));
+ EXPECT_CALL(mock, OnEndAttributes());
+ EXPECT_CALL(mock, OnEntity());
+
+ EXPECT_CALL(mock, OnEndList());
+
+ BuildYsonFluently(&mock)
+ .BeginList()
+ // 0
+ .Item()
+ .BeginAttributes()
+ .Item("attr1").Value(-1)
+ .Item("attr2").Value(-2)
+ .EndAttributes()
+ .Value(42)
+
+ // 1
+ .Item()
+ .Value(17)
+
+ // 2
+ .Item()
+ .BeginList()
+ .EndList()
+
+ // 3
+ .Item()
+ .BeginAttributes()
+ .Item("hot").Value("chocolate")
+ .EndAttributes()
+ .BeginList()
+ .Item().Value("hello")
+ .Item().Value("world")
+ .EndList()
+
+ // 4
+ .Item()
+ .BeginMap()
+ .Item("aaa").Value(1)
+ .Item("bbb").Value(2)
+ .EndMap()
+
+ // 5
+ .Item()
+ .BeginAttributes()
+ .Item("type").Value("extra")
+ .EndAttributes()
+ .Entity()
+ .EndList();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/unittests/ytree_ut.cpp b/yt/yt/core/ytree/unittests/ytree_ut.cpp
new file mode 100644
index 0000000000..11857d65ef
--- /dev/null
+++ b/yt/yt/core/ytree/unittests/ytree_ut.cpp
@@ -0,0 +1,241 @@
+#include <yt/yt/core/test_framework/framework.h>
+
+#include <yt/yt/core/ytree/convert.h>
+#include <yt/yt/core/ytree/fluent.h>
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/ypath_proxy.h>
+
+namespace NYT::NYTree {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SyncYPathMultisetAttributes(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const std::vector<std::pair<TString, TYsonString>>& requests)
+{
+ auto multisetAttributesRequest = TYPathProxy::MultisetAttributes(path);
+ for (const auto& request : requests) {
+ auto req = multisetAttributesRequest->add_subrequests();
+ req->set_attribute(request.first);
+ req->set_value(request.second.ToString());
+ }
+ ExecuteVerb(service, multisetAttributesRequest)
+ .Get()
+ .ThrowOnError();
+}
+
+TEST(TYTreeTest, TestMultisetAttributes)
+{
+ std::vector<std::pair<TString, TYsonString>> attributes1 = {
+ {"k1", TYsonString(TStringBuf("v1"))},
+ {"k2", TYsonString(TStringBuf("v2"))},
+ {"k3", TYsonString(TStringBuf("v3"))}
+ };
+
+ std::vector<std::pair<TString, TYsonString>> attributes2 = {
+ {"k2", TYsonString(TStringBuf("v4"))},
+ {"k3", TYsonString(TStringBuf("v5"))},
+ {"k4", TYsonString(TStringBuf("v6"))}
+ };
+
+ auto node = ConvertToNode(TYsonString(TStringBuf("{}")));
+
+ SyncYPathMultisetAttributes(node, "/@", attributes1);
+
+ EXPECT_EQ(node->Attributes().ListKeys().size(), 3u);
+ for (const auto& [attribute, value] : attributes1) {
+ EXPECT_EQ(node->Attributes().GetYson(attribute).ToString(), value.ToString());
+ }
+
+ SyncYPathMultisetAttributes(node, "/@", attributes2);
+ EXPECT_EQ(node->Attributes().ListKeys().size(), 4u);
+ for (const auto& [attribute, value] : attributes2) {
+ EXPECT_EQ(node->Attributes().GetYson(attribute).ToString(), value.ToString());
+ }
+ EXPECT_EQ(node->Attributes().GetYson("k1").ToString(), "v1");
+
+}
+
+TEST(TYTreeTest, TestMultisetInvalidAttributes)
+{
+ std::vector<std::pair<TString, TYsonString>> validAttributes = {
+ {"k1", TYsonString(TStringBuf("v1"))},
+ {"k2", TYsonString(TStringBuf("v2"))},
+ {"k3", TYsonString(TStringBuf("v3"))}
+ };
+ std::vector<std::pair<TString, TYsonString>> invalidAttributes = {
+ {"k1", TYsonString(TStringBuf("v1"))},
+ {"", TYsonString(TStringBuf("v2"))}, // Empty attributes are not allowed.
+ {"k3", TYsonString(TStringBuf("v3"))}
+ };
+
+ auto node = ConvertToNode(TYsonString(TStringBuf("{}")));
+
+ EXPECT_THROW(SyncYPathMultisetAttributes(node, "", validAttributes), std::exception);
+ EXPECT_THROW(SyncYPathMultisetAttributes(node, "/", validAttributes), std::exception);
+ EXPECT_THROW(SyncYPathMultisetAttributes(node, "/@/", validAttributes), std::exception);
+ EXPECT_THROW(SyncYPathMultisetAttributes(node, "/@", invalidAttributes), std::exception);
+}
+
+TEST(TYTreeTest, TestMultisetAttributesByPath)
+{
+ std::vector<std::pair<TString, TYsonString>> attributes1 = {
+ {"a", TYsonString(TStringBuf("{}"))},
+ };
+ std::vector<std::pair<TString, TYsonString>> attributes2 = {
+ {"a/b", TYsonString(TStringBuf("v1"))},
+ {"a/c", TYsonString(TStringBuf("v2"))},
+ };
+ std::vector<std::pair<TString, TYsonString>> attributes3 = {
+ {"b", TYsonString(TStringBuf("v3"))},
+ {"c", TYsonString(TStringBuf("v4"))},
+ };
+
+ auto node = ConvertToNode(TYsonString(TStringBuf("{}")));
+
+ SyncYPathMultisetAttributes(node, "/@", attributes1);
+ SyncYPathMultisetAttributes(node, "/@a", attributes3);
+
+ auto attribute = ConvertToNode(node->Attributes().GetYson("a"))->AsMap();
+ EXPECT_EQ(attribute->GetKeys(), std::vector<TString>({"b", "c"}));
+}
+
+TEST(TYTreeTest, TestGetWithAttributes)
+{
+ auto yson = TYsonStringBuf(
+ "{"
+ " foo = <"
+ " a = 1;"
+ " b = {"
+ " x = 2;"
+ " y = 3;"
+ " };"
+ " c = [i1; i2; i3];"
+ " d = 4;"
+ " > {"
+ " bar = <"
+ " a = {"
+ " x = 5;"
+ " y = 6;"
+ " };"
+ " b = 7;"
+ " c = [j1; j2; j3; j4];"
+ " e = 8;"
+ " > #;"
+ " };"
+ "}");
+ auto node = ConvertToNode(yson);
+
+ auto compareYsons = [] (TYsonStringBuf expectedYson, INodePtr node, const TAttributeFilter& attributeFilter) {
+ // NB: serialization of ephemeral nodes is always stable.
+ TYsonString actualYson = SyncYPathGet(node, "", attributeFilter);
+ auto stableOriginal = ConvertToYsonString(node, EYsonFormat::Pretty).ToString();
+ auto stableExpected = ConvertToYsonString(ConvertToNode(expectedYson), EYsonFormat::Pretty).ToString();
+ auto stableActual = ConvertToYsonString(ConvertToNode(actualYson), EYsonFormat::Pretty).ToString();
+ EXPECT_EQ(stableExpected, stableActual)
+ << "Original:" << std::endl << stableOriginal << std::endl
+ << "Filtered by: " << Format("%v", attributeFilter) << std::endl
+ << "Expected: " << std::endl << stableExpected << std::endl
+ << "Actual: " << std::endl << stableActual << std::endl;
+ };
+
+ compareYsons(
+ yson,
+ node,
+ TAttributeFilter());
+
+ auto expectedYson1 = TYsonStringBuf(
+ "{"
+ " foo = <"
+ " a = 1;"
+ " b = {"
+ " x = 2;"
+ " y = 3;"
+ " };"
+ " > {"
+ " bar = <"
+ " a = {"
+ " x = 5;"
+ " y = 6;"
+ " };"
+ " b = 7;"
+ " e = 8;"
+ " > #;"
+ " };"
+ "}");
+
+ compareYsons(
+ expectedYson1,
+ node,
+ TAttributeFilter({"a", "b", "e"}));
+
+ compareYsons(
+ expectedYson1,
+ node,
+ TAttributeFilter({}, {"/a", "/b", "/e"}));
+
+ auto expectedYson2 = TYsonStringBuf(
+ "{"
+ " foo = <"
+ " a = 1;"
+ " > {"
+ " bar = <"
+ " a = {"
+ " x = 5;"
+ " y = 6;"
+ " };"
+ " > #;"
+ " };"
+ "}");
+
+ compareYsons(
+ expectedYson2,
+ node,
+ TAttributeFilter({}, {"/a"}));
+
+ auto expectedYson3 = TYsonStringBuf(
+ "{"
+ " foo = <"
+ " b = {"
+ " y = 3;"
+ " };"
+ " > {"
+ " bar = <"
+ " a = {"
+ " x = 5;"
+ " };"
+ " > #;"
+ " };"
+ "}");
+
+ compareYsons(
+ expectedYson3,
+ node,
+ TAttributeFilter({}, {"/a/x", "/b/y"}));
+
+ auto expectedYson4 = TYsonStringBuf(
+ "{"
+ " foo = <"
+ " c = [#; i2];"
+ " > {"
+ " bar = <"
+ " c = [#; j2; #; j4];"
+ " > #;"
+ " };"
+ "}");
+
+ compareYsons(
+ expectedYson4,
+ node,
+ TAttributeFilter({}, {"/c/1", "/c/3"}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NYTree
+
diff --git a/yt/yt/core/ytree/virtual-inl.h b/yt/yt/core/ytree/virtual-inl.h
new file mode 100644
index 0000000000..d3cf979930
--- /dev/null
+++ b/yt/yt/core/ytree/virtual-inl.h
@@ -0,0 +1,112 @@
+#ifndef VIRTUAL_INL_H_
+#error "Direct inclusion of this file is not allowed, include virtual.h"
+// For the sake of sane code completion.
+#include "virtual.h"
+#endif
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TDefaultConversionTraits
+{ };
+
+template <>
+class TDefaultConversionTraits<TString>
+{
+public:
+ static TString ConvertKeyToString(const TString& key)
+ {
+ return key;
+ }
+
+ static TString ConvertStringToKey(TStringBuf key)
+ {
+ return TString(key);
+ }
+};
+
+template <class T, class TConversionTraits = TDefaultConversionTraits<typename T::key_type>>
+class TCollectionBoundMapService
+ : public TVirtualMapBase
+{
+public:
+ explicit TCollectionBoundMapService(std::weak_ptr<T> collection)
+ : Collection_(std::move(collection))
+ { }
+
+ i64 GetSize() const override
+ {
+ if (auto collection = Collection_.lock()) {
+ return collection->size();
+ }
+ return 0;
+ }
+
+ std::vector<TString> GetKeys(i64 limit) const override
+ {
+ std::vector<TString> keys;
+ if (auto collection = Collection_.lock()) {
+ keys.reserve(limit);
+ for (const auto& [key, value] : *collection) {
+ if (static_cast<i64>(keys.size()) >= limit) {
+ break;
+ }
+ keys.emplace_back(TConversionTraits::ConvertKeyToString(key));
+ }
+ }
+ return keys;
+ }
+
+ IYPathServicePtr FindItemService(TStringBuf key) const override
+ {
+ if (auto collection = Collection_.lock()) {
+ auto it = collection->find(TConversionTraits::ConvertStringToKey(key));
+ if (it == collection->end()) {
+ return nullptr;
+ }
+ return it->second->GetService();
+ }
+ return nullptr;
+ }
+
+private:
+ const std::weak_ptr<T> Collection_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+class TCollectionBoundListService
+ : public TVirtualListBase
+{
+public:
+ explicit TCollectionBoundListService(std::weak_ptr<T> collection)
+ : Collection_(std::move(collection))
+ { }
+
+ i64 GetSize() const override
+ {
+ if (auto collection = Collection_.lock()) {
+ return collection->size();
+ }
+ return 0;
+ }
+
+ IYPathServicePtr FindItemService(int index) const override
+ {
+ if (auto collection = Collection_.lock()) {
+ YT_VERIFY(0 <= index && index < std::ssize(*collection));
+ return (*collection)[index] ? (*collection)[index]->GetService() : nullptr;
+ }
+ return nullptr;
+ }
+
+private:
+ const std::weak_ptr<T> Collection_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/virtual.cpp b/yt/yt/core/ytree/virtual.cpp
new file mode 100644
index 0000000000..675dbd6c9e
--- /dev/null
+++ b/yt/yt/core/ytree/virtual.cpp
@@ -0,0 +1,610 @@
+#include "virtual.h"
+#include "ephemeral_attribute_owner.h"
+#include "fluent.h"
+#include "node_detail.h"
+#include "ypath_client.h"
+#include "ypath_detail.h"
+#include "ypath_service.h"
+
+#include <yt/yt/core/yson/tokenizer.h>
+#include <yt/yt/core/yson/async_writer.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <util/generic/hash.h>
+
+namespace NYT::NYTree {
+
+using namespace NRpc;
+using namespace NYson;
+using namespace NYTree;
+using namespace NYPath;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TVirtualMapBase::TVirtualMapBase()
+ : TVirtualMapBase(/*owningNode*/ nullptr)
+{ }
+
+TVirtualMapBase::TVirtualMapBase(INodePtr owningNode)
+ : OwningNode_(std::move(owningNode))
+{ }
+
+bool TVirtualMapBase::DoInvoke(const IYPathServiceContextPtr& context)
+{
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ DISPATCH_YPATH_SERVICE_METHOD(Remove);
+ return TSupportsAttributes::DoInvoke(context);
+}
+
+IYPathService::TResolveResult TVirtualMapBase::ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+ auto service = FindItemService(key);
+ if (!service) {
+ const auto& method = context->GetMethod();
+ if (method == "Exists" || method == "Remove") {
+ return TResolveResultHere{"/" + path};
+ }
+ // TODO(babenko): improve diagnostics
+ ThrowNoSuchChildKey(key);
+ }
+
+ return TResolveResultThere{std::move(service), TYPath(tokenizer.GetSuffix())};
+}
+
+void TVirtualMapBase::GetSelf(
+ TReqGet* request,
+ TRspGet* response,
+ const TCtxGetPtr& context)
+{
+ YT_ASSERT(!NYson::TTokenizer(GetRequestTargetYPath(context->RequestHeader())).ParseNext());
+
+ auto attributeFilter = request->has_attributes()
+ ? NYT::FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ i64 limit = request->has_limit()
+ ? request->limit()
+ : DefaultVirtualChildLimit;
+
+ context->SetRequestInfo("AttributeFilter: %v, Limit: %v",
+ attributeFilter,
+ limit);
+
+ auto keys = GetKeys(limit);
+ i64 size = GetSize();
+
+ TAsyncYsonWriter writer;
+
+ // NB: we do not want empty attributes (<>) to appear in the result in order to comply
+ // with current behaviour for some paths (like //sys/scheduler/orchid/scheduler/operations).
+ if (std::ssize(keys) != size || OwningNode_) {
+ writer.OnBeginAttributes();
+ if (std::ssize(keys) != size) {
+ writer.OnKeyedItem("incomplete");
+ writer.OnBooleanScalar(true);
+ }
+ if (OwningNode_) {
+ OwningNode_->WriteAttributesFragment(&writer, attributeFilter, false);
+ }
+ writer.OnEndAttributes();
+ }
+
+ writer.OnBeginMap();
+
+ if (attributeFilter) {
+ for (const auto& key : keys) {
+ auto service = FindItemService(key);
+ if (service) {
+ writer.OnKeyedItem(key);
+ if (Opaque_) {
+ service->WriteAttributes(&writer, attributeFilter, false);
+ writer.OnEntity();
+ } else {
+ auto asyncResult = AsyncYPathGet(service, "", attributeFilter);
+ writer.OnRaw(asyncResult);
+ }
+ }
+ }
+ } else {
+ for (const auto& key : keys) {
+ if (Opaque_) {
+ writer.OnKeyedItem(key);
+ writer.OnEntity();
+ } else {
+ if (auto service = FindItemService(key)) {
+ writer.OnKeyedItem(key);
+ auto asyncResult = AsyncYPathGet(service, "");
+ writer.OnRaw(asyncResult);
+ }
+ }
+ }
+ }
+ writer.OnEndMap();
+
+ writer.Finish()
+ .Subscribe(BIND([=] (const TErrorOr<TYsonString>& resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+}
+
+void TVirtualMapBase::ListSelf(
+ TReqList* request,
+ TRspList* response,
+ const TCtxListPtr& context)
+{
+ auto attributeFilter = request->has_attributes()
+ ? FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ i64 limit = request->has_limit()
+ ? request->limit()
+ : DefaultVirtualChildLimit;
+
+ context->SetRequestInfo("AttributeFilter: %v, Limit: %v",
+ attributeFilter,
+ limit);
+
+ auto keys = GetKeys(limit);
+ i64 size = GetSize();
+
+ TAsyncYsonWriter writer;
+
+ if (std::ssize(keys) != size) {
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("incomplete");
+ writer.OnBooleanScalar(true);
+ writer.OnEndAttributes();
+ }
+
+ writer.OnBeginList();
+ if (attributeFilter) {
+ for (const auto& key : keys) {
+ auto service = FindItemService(key);
+ if (service) {
+ writer.OnListItem();
+ service->WriteAttributes(&writer, attributeFilter, false);
+ writer.OnStringScalar(key);
+ }
+ }
+ } else {
+ for (const auto& key : keys) {
+ writer.OnListItem();
+ writer.OnStringScalar(key);
+ }
+ }
+ writer.OnEndList();
+
+ writer.Finish()
+ .Subscribe(BIND([=] (const TErrorOr<TYsonString>& resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+}
+
+void TVirtualMapBase::RemoveRecursive(
+ const TYPath& path,
+ TReqRemove* request,
+ TRspRemove* /*response*/,
+ const TSupportsRemove::TCtxRemovePtr& context)
+{
+ context->SetRequestInfo();
+
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ if (request->force()) {
+ context->Reply();
+ } else {
+ // TODO(babenko): improve diagnostics
+ ThrowNoSuchChildKey(tokenizer.GetLiteralValue());
+ }
+}
+
+void TVirtualMapBase::ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors)
+{
+ descriptors->push_back(CountInternedAttribute);
+}
+
+const THashSet<TInternedAttributeKey>& TVirtualMapBase::GetBuiltinAttributeKeys()
+{
+ return BuiltinAttributeKeysCache_.GetBuiltinAttributeKeys(this);
+}
+
+bool TVirtualMapBase::GetBuiltinAttribute(TInternedAttributeKey key, IYsonConsumer* consumer)
+{
+ switch (key) {
+ case CountInternedAttribute:
+ BuildYsonFluently(consumer)
+ .Value(GetSize());
+ return true;
+ default:
+ return false;
+ }
+}
+
+TFuture<TYsonString> TVirtualMapBase::GetBuiltinAttributeAsync(TInternedAttributeKey /*key*/)
+{
+ return std::nullopt;
+}
+
+ISystemAttributeProvider* TVirtualMapBase::GetBuiltinAttributeProvider()
+{
+ return this;
+}
+
+bool TVirtualMapBase::SetBuiltinAttribute(TInternedAttributeKey /*key*/, const TYsonString& /*value*/)
+{
+ return false;
+}
+
+bool TVirtualMapBase::RemoveBuiltinAttribute(TInternedAttributeKey /*key*/)
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompositeMapService::TImpl
+ : public TRefCounted
+{
+public:
+ std::vector<TString> GetKeys(i64 limit) const
+ {
+ std::vector<TString> keys;
+ int index = 0;
+ auto it = Services_.begin();
+ while (it != Services_.end() && index < limit) {
+ keys.push_back(it->first);
+ ++it;
+ ++index;
+ }
+ return keys;
+ }
+
+ i64 GetSize() const
+ {
+ return Services_.size();
+ }
+
+ IYPathServicePtr FindItemService(TStringBuf key) const
+ {
+ auto it = Services_.find(key);
+ return it != Services_.end() ? it->second : nullptr;
+ }
+
+ void ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors) const
+ {
+ for (const auto& it : Attributes_) {
+ descriptors->push_back(TAttributeDescriptor(it.first));
+ }
+ }
+
+ bool GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer) const
+ {
+ auto it = Attributes_.find(key);
+ if (it != Attributes_.end()) {
+ it->second(consumer);
+ return true;
+ }
+
+ return false;
+ }
+
+ void AddChild(const TString& key, IYPathServicePtr service)
+ {
+ YT_VERIFY(Services_.emplace(key, service).second);
+ }
+
+ void AddAttribute(TInternedAttributeKey key, TYsonCallback producer)
+ {
+ YT_VERIFY(Attributes_.emplace(key, producer).second);
+ }
+
+private:
+ THashMap<TString, IYPathServicePtr> Services_;
+ THashMap<TInternedAttributeKey, TYsonCallback> Attributes_;
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TCompositeMapService::TCompositeMapService()
+ : Impl_(New<TImpl>())
+{ }
+
+TCompositeMapService::~TCompositeMapService()
+{ }
+
+std::vector<TString> TCompositeMapService::GetKeys(i64 limit) const
+{
+ return Impl_->GetKeys(limit);
+}
+
+i64 TCompositeMapService::GetSize() const
+{
+ return Impl_->GetSize();
+}
+
+IYPathServicePtr TCompositeMapService::FindItemService(TStringBuf key) const
+{
+ return Impl_->FindItemService(key);
+}
+
+void TCompositeMapService::ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors)
+{
+ Impl_->ListSystemAttributes(descriptors);
+
+ TVirtualMapBase::ListSystemAttributes(descriptors);
+}
+
+bool TCompositeMapService::GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer)
+{
+ if (Impl_->GetBuiltinAttribute(key, consumer)) {
+ return true;
+ }
+
+ return TVirtualMapBase::GetBuiltinAttribute(key, consumer);
+}
+
+TIntrusivePtr<TCompositeMapService> TCompositeMapService::AddChild(const TString& key, IYPathServicePtr service)
+{
+ Impl_->AddChild(key, std::move(service));
+ return this;
+}
+
+TIntrusivePtr<TCompositeMapService> TCompositeMapService::AddAttribute(TInternedAttributeKey key, TYsonCallback producer)
+{
+ Impl_->AddAttribute(key, producer);
+ return this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVirtualEntityNode
+ : public TNodeBase
+ , public TSupportsAttributes
+ , public IEntityNode
+ , public TEphemeralAttributeOwner
+{
+public:
+ YTREE_NODE_TYPE_OVERRIDES(Entity)
+
+public:
+ explicit TVirtualEntityNode(IYPathServicePtr underlyingService)
+ : UnderlyingService_(std::move(underlyingService))
+ { }
+
+ std::unique_ptr<ITransactionalNodeFactory> CreateFactory() const override
+ {
+ YT_ASSERT(Parent_);
+ return Parent_->CreateFactory();
+ }
+
+ ICompositeNodePtr GetParent() const override
+ {
+ return Parent_;
+ }
+
+ void SetParent(const ICompositeNodePtr& parent) override
+ {
+ Parent_ = parent.Get();
+ }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ // TODO(babenko): handle ugly face
+ return TResolveResultThere{UnderlyingService_, path};
+ }
+
+ void DoWriteAttributesFragment(
+ IAsyncYsonConsumer* /*consumer*/,
+ const TAttributeFilter& /*attributeFilter*/,
+ bool /*stable*/) override
+ { }
+
+ bool ShouldHideAttributes() override
+ {
+ return UnderlyingService_->ShouldHideAttributes();
+ }
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+
+ ICompositeNode* Parent_ = nullptr;
+
+ // TSupportsAttributes members
+
+ IAttributeDictionary* GetCustomAttributes() override
+ {
+ return MutableAttributes();
+ }
+};
+
+INodePtr CreateVirtualNode(IYPathServicePtr service)
+{
+ return New<TVirtualEntityNode>(service);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TVirtualListBase::DoInvoke(const IYPathServiceContextPtr& context)
+{
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ return TSupportsAttributes::DoInvoke(context);
+}
+
+IYPathService::TResolveResult TVirtualListBase::ResolveRecursive(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ int index = ParseListIndex(tokenizer.GetToken());
+ int originalIndex = index;
+ if (index < 0) {
+ index += GetSize();
+ }
+ IYPathServicePtr service = nullptr;
+ if (index >= 0 && index < GetSize()) {
+ service = FindItemService(index);
+ }
+ if (!service) {
+ if (context->GetMethod() == "Exists") {
+ return TResolveResultHere{path};
+ }
+ THROW_ERROR_EXCEPTION("Node has no child with index %v", originalIndex);
+ }
+
+ return TResolveResultThere{std::move(service), TYPath(tokenizer.GetSuffix())};
+}
+
+void TVirtualListBase::GetSelf(
+ TReqGet* request,
+ TRspGet* response,
+ const TCtxGetPtr& context)
+{
+ YT_ASSERT(!NYson::TTokenizer(GetRequestTargetYPath(context->RequestHeader())).ParseNext());
+
+ auto attributeFilter = request->has_attributes()
+ ? FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ i64 limit = request->has_limit()
+ ? request->limit()
+ : DefaultVirtualChildLimit;
+
+ context->SetRequestInfo("AttributeFilter: %v, Limit: %v",
+ attributeFilter,
+ limit);
+
+ i64 size = GetSize();
+
+ TAsyncYsonWriter writer;
+
+ // NB: we do not want empty attributes (<>) to appear in the result in order to comply
+ // with current behaviour for some paths (like //sys/scheduler/orchid/scheduler/operations).
+ if (limit < size) {
+ writer.OnBeginAttributes();
+ writer.OnKeyedItem("incomplete");
+ writer.OnBooleanScalar(true);
+ writer.OnEndAttributes();
+ }
+
+ writer.OnBeginList();
+
+ if (attributeFilter) {
+ for (int index = 0; index < limit && index < size; ++index) {
+ auto service = FindItemService(index);
+ writer.OnListItem();
+ if (service) {
+ service->WriteAttributes(&writer, attributeFilter, false);
+ if (Opaque_) {
+ writer.OnEntity();
+ } else {
+ auto asyncResult = AsyncYPathGet(service, "");
+ writer.OnRaw(asyncResult);
+ }
+ } else {
+ writer.OnEntity();
+ }
+ }
+ } else {
+ for (int index = 0; index < limit && index < size; ++index) {
+ writer.OnListItem();
+ if (Opaque_) {
+ writer.OnEntity();
+ } else {
+ if (auto service = FindItemService(index)) {
+ writer.OnListItem();
+ auto asyncResult = AsyncYPathGet(service, "");
+ writer.OnRaw(asyncResult);
+ } else {
+ writer.OnEntity();
+ }
+ }
+ }
+ }
+
+ writer.OnEndList();
+
+ writer.Finish()
+ .Subscribe(BIND([=] (const TErrorOr<TYsonString>& resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+}
+
+void TVirtualListBase::ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors)
+{
+ descriptors->push_back(CountInternedAttribute);
+}
+
+const THashSet<TInternedAttributeKey>& TVirtualListBase::GetBuiltinAttributeKeys()
+{
+ return BuiltinAttributeKeysCache_.GetBuiltinAttributeKeys(this);
+}
+
+bool TVirtualListBase::GetBuiltinAttribute(TInternedAttributeKey key, IYsonConsumer* consumer)
+{
+ switch (key) {
+ case CountInternedAttribute:
+ BuildYsonFluently(consumer)
+ .Value(GetSize());
+ return true;
+ default:
+ return false;
+ }
+}
+
+TFuture<TYsonString> TVirtualListBase::GetBuiltinAttributeAsync(TInternedAttributeKey /*key*/)
+{
+ return std::nullopt;
+}
+
+ISystemAttributeProvider* TVirtualListBase::GetBuiltinAttributeProvider()
+{
+ return this;
+}
+
+bool TVirtualListBase::SetBuiltinAttribute(TInternedAttributeKey /*key*/, const TYsonString& /*value*/)
+{
+ return false;
+}
+
+bool TVirtualListBase::RemoveBuiltinAttribute(TInternedAttributeKey /*key*/)
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/virtual.h b/yt/yt/core/ytree/virtual.h
new file mode 100644
index 0000000000..43cb08d228
--- /dev/null
+++ b/yt/yt/core/ytree/virtual.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include "system_attribute_provider.h"
+#include "ypath_detail.h"
+
+#include <yt/yt/core/yson/producer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVirtualMapBase
+ : public TSupportsAttributes
+ , public ISystemAttributeProvider
+{
+public:
+ DEFINE_BYVAL_RW_PROPERTY(bool, Opaque, true);
+
+protected:
+ TVirtualMapBase();
+ explicit TVirtualMapBase(INodePtr owningNode);
+
+ virtual std::vector<TString> GetKeys(i64 limit = std::numeric_limits<i64>::max()) const = 0;
+ virtual i64 GetSize() const = 0;
+
+ virtual IYPathServicePtr FindItemService(TStringBuf key) const = 0;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override;
+
+ TResolveResult ResolveRecursive(const TYPath& path, const IYPathServiceContextPtr& context) override;
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override;
+ void ListSelf(TReqList* request, TRspList* response, const TCtxListPtr& context) override;
+ void RemoveRecursive(
+ const TYPath &path,
+ TReqRemove* request,
+ TRspRemove* response,
+ const TCtxRemovePtr& context) override;
+
+ // TSupportsAttributes overrides
+ ISystemAttributeProvider* GetBuiltinAttributeProvider() override;
+
+ // ISystemAttributeProvider overrides
+ void ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors) override;
+ const THashSet<TInternedAttributeKey>& GetBuiltinAttributeKeys() override;
+ bool GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer) override;
+ TFuture<NYson::TYsonString> GetBuiltinAttributeAsync(TInternedAttributeKey key) override;
+ bool SetBuiltinAttribute(TInternedAttributeKey key, const NYson::TYsonString& value) override;
+ bool RemoveBuiltinAttribute(TInternedAttributeKey key) override;
+
+private:
+ const INodePtr OwningNode_;
+
+ TSystemBuiltinAttributeKeysCache BuiltinAttributeKeysCache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TCompositeMapService
+ : public TVirtualMapBase
+{
+public:
+ TCompositeMapService();
+
+ ~TCompositeMapService();
+
+ std::vector<TString> GetKeys(i64 limit = std::numeric_limits<i64>::max()) const override;
+ i64 GetSize() const override;
+ IYPathServicePtr FindItemService(TStringBuf key) const override;
+ void ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors) override;
+ bool GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer) override;
+
+ TIntrusivePtr<TCompositeMapService> AddChild(const TString& key, IYPathServicePtr service);
+ TIntrusivePtr<TCompositeMapService> AddAttribute(TInternedAttributeKey key, NYson::TYsonCallback producer);
+
+private:
+ class TImpl;
+ const TIntrusivePtr<TImpl> Impl_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCompositeMapService)
+
+////////////////////////////////////////////////////////////////////////////////
+
+INodePtr CreateVirtualNode(IYPathServicePtr service);
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TVirtualListBase
+ : public TSupportsAttributes
+ , public ISystemAttributeProvider
+{
+public:
+ DEFINE_BYVAL_RW_PROPERTY(bool, Opaque, true);
+
+protected:
+ virtual i64 GetSize() const = 0;
+ virtual IYPathServicePtr FindItemService(int index) const = 0;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override;
+
+ TResolveResult ResolveRecursive(const TYPath& path, const IYPathServiceContextPtr& context) override;
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override;
+
+ // TSupportsAttributes overrides
+ ISystemAttributeProvider* GetBuiltinAttributeProvider() override;
+
+ // ISystemAttributeProvider overrides
+ void ListSystemAttributes(std::vector<TAttributeDescriptor>* descriptors) override;
+ const THashSet<TInternedAttributeKey>& GetBuiltinAttributeKeys() override;
+ bool GetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer) override;
+ TFuture<NYson::TYsonString> GetBuiltinAttributeAsync(TInternedAttributeKey key) override;
+ bool SetBuiltinAttribute(TInternedAttributeKey key, const NYson::TYsonString& value) override;
+ bool RemoveBuiltinAttribute(TInternedAttributeKey key) override;
+
+private:
+ TSystemBuiltinAttributeKeysCache BuiltinAttributeKeysCache_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define VIRTUAL_INL_H_
+#include "virtual-inl.h"
+#undef VIRTUAL_INL_H_
diff --git a/yt/yt/core/ytree/ypath_client-inl.h b/yt/yt/core/ytree/ypath_client-inl.h
new file mode 100644
index 0000000000..771ef629cd
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_client-inl.h
@@ -0,0 +1,37 @@
+#ifndef YPATH_CLIENT_INL_H_
+#error "Direct inclusion of this file is not allowed, include ypath_client.h"
+// For the sake of sane code completion.
+#include "ypath_client.h"
+#endif
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TTypedRequest>
+TFuture<TIntrusivePtr<typename TTypedRequest::TTypedResponse>>
+ExecuteVerb(const IYPathServicePtr& service, const TIntrusivePtr<TTypedRequest>& request)
+{
+ typedef typename TTypedRequest::TTypedResponse TTypedResponse;
+
+ auto requestMessage = request->Serialize();
+ return ExecuteVerb(service, requestMessage)
+ .Apply(BIND([] (const TSharedRefArray& responseMessage) -> TIntrusivePtr<TTypedResponse> {
+ auto response = New<TTypedResponse>();
+ response->Deserialize(responseMessage);
+ return response;
+ }));
+}
+
+template <class TTypedRequest>
+TIntrusivePtr<typename TTypedRequest::TTypedResponse>
+SyncExecuteVerb(const IYPathServicePtr& service, const TIntrusivePtr<TTypedRequest>& request)
+{
+ return ExecuteVerb(service, request)
+ .Get()
+ .ValueOrThrow();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_client.cpp b/yt/yt/core/ytree/ypath_client.cpp
new file mode 100644
index 0000000000..50afee29c5
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_client.cpp
@@ -0,0 +1,930 @@
+#include "ypath_client.h"
+#include "helpers.h"
+#include "exception_helpers.h"
+#include "ypath_detail.h"
+#include "ypath_proxy.h"
+
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/bus/bus.h>
+
+#include <yt/yt/core/rpc/message.h>
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+#include <yt/yt/core/rpc/server_detail.h>
+
+#include <yt/yt/core/ypath/token.h>
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/yson/format.h>
+#include <yt/yt/core/yson/tokenizer.h>
+
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+#include <library/cpp/yt/misc/variant.h>
+
+#include <cmath>
+
+namespace NYT::NYTree {
+
+using namespace NBus;
+using namespace NRpc;
+using namespace NRpc::NProto;
+using namespace NYPath;
+using namespace NYson;
+
+using NYT::FromProto;
+using NYT::ToProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPathRequest::TYPathRequest(const TRequestHeader& header)
+ : Header_(header)
+{ }
+
+TYPathRequest::TYPathRequest(
+ TString service,
+ TString method,
+ TYPath path,
+ bool mutating)
+{
+ Header_.set_service(std::move(service));
+ Header_.set_method(std::move(method));
+
+ auto* ypathExt = Header_.MutableExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ ypathExt->set_mutating(mutating);
+ ypathExt->set_target_path(std::move(path));
+}
+
+TRequestId TYPathRequest::GetRequestId() const
+{
+ return NullRequestId;
+}
+
+TRealmId TYPathRequest::GetRealmId() const
+{
+ return NullRealmId;
+}
+
+const TString& TYPathRequest::GetMethod() const
+{
+ return Header_.method();
+}
+
+const TString& TYPathRequest::GetService() const
+{
+ return Header_.service();
+}
+
+void TYPathRequest::DeclareClientFeature(int featureId)
+{
+ Header_.add_declared_client_feature_ids(featureId);
+}
+
+void TYPathRequest::RequireServerFeature(int featureId)
+{
+ Header_.add_required_server_feature_ids(featureId);
+}
+
+const TString& TYPathRequest::GetUser() const
+{
+ YT_ABORT();
+}
+
+void TYPathRequest::SetUser(const TString& /*user*/)
+{
+ YT_ABORT();
+}
+
+const TString& TYPathRequest::GetUserTag() const
+{
+ YT_ABORT();
+}
+
+void TYPathRequest::SetUserTag(const TString& /*tag*/)
+{
+ YT_ABORT();
+}
+
+void TYPathRequest::SetUserAgent(const TString& /*userAgent*/)
+{
+ YT_ABORT();
+}
+
+bool TYPathRequest::GetRetry() const
+{
+ return Header_.retry();
+}
+
+void TYPathRequest::SetRetry(bool value)
+{
+ Header_.set_retry(value);
+}
+
+TMutationId TYPathRequest::GetMutationId() const
+{
+ return NRpc::GetMutationId(Header_);
+}
+
+void TYPathRequest::SetMutationId(TMutationId id)
+{
+ if (id) {
+ ToProto(Header_.mutable_mutation_id(), id);
+ } else {
+ Header_.clear_mutation_id();
+ }
+}
+
+size_t TYPathRequest::GetHash() const
+{
+ return 0;
+}
+
+const NRpc::NProto::TRequestHeader& TYPathRequest::Header() const
+{
+ return Header_;
+}
+
+NRpc::NProto::TRequestHeader& TYPathRequest::Header()
+{
+ return Header_;
+}
+
+bool TYPathRequest::IsStreamingEnabled() const
+{
+ return false;
+}
+
+const NRpc::TStreamingParameters& TYPathRequest::ClientAttachmentsStreamingParameters() const
+{
+ YT_ABORT();
+}
+
+NRpc::TStreamingParameters& TYPathRequest::ClientAttachmentsStreamingParameters()
+{
+ YT_ABORT();
+}
+
+const NRpc::TStreamingParameters& TYPathRequest::ServerAttachmentsStreamingParameters() const
+{
+ YT_ABORT();
+}
+
+NRpc::TStreamingParameters& TYPathRequest::ServerAttachmentsStreamingParameters()
+{
+ YT_ABORT();
+}
+
+NConcurrency::IAsyncZeroCopyOutputStreamPtr TYPathRequest::GetRequestAttachmentsStream() const
+{
+ YT_ABORT();
+}
+
+NConcurrency::IAsyncZeroCopyInputStreamPtr TYPathRequest::GetResponseAttachmentsStream() const
+{
+ YT_ABORT();
+}
+
+bool TYPathRequest::IsLegacyRpcCodecsEnabled()
+{
+ YT_ABORT();
+}
+
+TSharedRefArray TYPathRequest::Serialize()
+{
+ auto bodyData = SerializeBody();
+ return CreateRequestMessage(
+ Header_,
+ std::move(bodyData),
+ Attachments_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYPathResponse::Deserialize(const TSharedRefArray& message)
+{
+ YT_ASSERT(message);
+
+ NRpc::NProto::TResponseHeader header;
+ if (!TryParseResponseHeader(message, &header)) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Error parsing response header");
+ }
+
+ // Check for error in header.
+ if (header.has_error()) {
+ FromProto<TError>(header.error())
+ .ThrowOnError();
+ }
+
+ // Deserialize body.
+ if (message.Size() < 2) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Too few response message parts: %v < 2",
+ message.Size());
+ }
+
+ if (!TryDeserializeBody(message[1])) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::ProtocolError, "Error deserializing response body");
+ }
+
+ // Load attachments.
+ Attachments_ = std::vector<TSharedRef>(message.Begin() + 2, message.End());
+}
+
+bool TYPathResponse::TryDeserializeBody(TRef /*data*/, std::optional<NCompression::ECodec> /*codecId*/)
+{
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TYPath& GetRequestTargetYPath(const NRpc::NProto::TRequestHeader& header)
+{
+ const auto& ypathExt = header.GetExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ return ypathExt.target_path();
+}
+
+const TYPath& GetOriginalRequestTargetYPath(const NRpc::NProto::TRequestHeader& header)
+{
+ const auto& ypathExt = header.GetExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ return ypathExt.has_original_target_path() ? ypathExt.original_target_path() : ypathExt.target_path();
+}
+
+void SetRequestTargetYPath(NRpc::NProto::TRequestHeader* header, TYPath path)
+{
+ auto* ypathExt = header->MutableExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ ypathExt->set_target_path(std::move(path));
+}
+
+bool IsRequestMutating(const NRpc::NProto::TRequestHeader& header)
+{
+ const auto& ext = header.GetExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ return ext.mutating();
+}
+
+void ResolveYPath(
+ const IYPathServicePtr& rootService,
+ const IYPathServiceContextPtr& context,
+ IYPathServicePtr* suffixService,
+ TYPath* suffixPath)
+{
+ YT_ASSERT(rootService);
+ YT_ASSERT(suffixService);
+ YT_ASSERT(suffixPath);
+
+ auto currentService = rootService;
+
+ const auto& originalPath = GetOriginalRequestTargetYPath(context->RequestHeader());
+ auto currentPath = GetRequestTargetYPath(context->RequestHeader());
+
+ int iteration = 0;
+ while (true) {
+ ValidateYPathResolutionDepth(originalPath, ++iteration);
+
+ try {
+ auto result = currentService->Resolve(currentPath, context);
+ auto mustBreak = false;
+ Visit(std::move(result),
+ [&] (IYPathService::TResolveResultHere&& hereResult) {
+ *suffixService = std::move(currentService);
+ *suffixPath = std::move(hereResult.Path);
+ mustBreak = true;
+ },
+ [&] (IYPathService::TResolveResultThere&& thereResult) {
+ currentService = std::move(thereResult.Service);
+ currentPath = std::move(thereResult.Path);
+ });
+
+ if (mustBreak) {
+ break;
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ NYTree::EErrorCode::ResolveError,
+ "Error resolving path %v",
+ originalPath)
+ << TErrorAttribute("method", context->GetMethod())
+ << ex;
+ }
+ }
+}
+
+TFuture<TSharedRefArray> ExecuteVerb(
+ const IYPathServicePtr& service,
+ const TSharedRefArray& requestMessage)
+{
+ IYPathServicePtr suffixService;
+ TYPath suffixPath;
+ try {
+ auto resolveContext = CreateYPathContext(requestMessage);
+ ResolveYPath(
+ service,
+ resolveContext,
+ &suffixService,
+ &suffixPath);
+ } catch (const std::exception& ex) {
+ return MakeFuture(CreateErrorResponseMessage(ex));
+ }
+
+ NRpc::NProto::TRequestHeader requestHeader;
+ YT_VERIFY(ParseRequestHeader(requestMessage, &requestHeader));
+ SetRequestTargetYPath(&requestHeader, suffixPath);
+
+ auto updatedRequestMessage = SetRequestHeader(requestMessage, requestHeader);
+
+ auto invokeContext = CreateYPathContext(std::move(updatedRequestMessage));
+
+ // NB: Calling GetAsyncResponseMessage after Invoke is not allowed.
+ auto asyncResponseMessage = invokeContext->GetAsyncResponseMessage();
+
+ // This should never throw.
+ suffixService->Invoke(invokeContext);
+
+ return asyncResponseMessage;
+}
+
+void ExecuteVerb(
+ const IYPathServicePtr& service,
+ const IYPathServiceContextPtr& context)
+{
+ IYPathServicePtr suffixService;
+ TYPath suffixPath;
+ try {
+ ResolveYPath(
+ service,
+ context,
+ &suffixService,
+ &suffixPath);
+ } catch (const std::exception& ex) {
+ // NB: resolve failure may be caused by body serialization error.
+ // In this case error is already set into context.
+ if (!context->IsReplied()) {
+ context->Reply(ex);
+ }
+ return;
+ }
+
+ auto requestMessage = context->GetRequestMessage();
+ auto requestHeader = std::make_unique<NRpc::NProto::TRequestHeader>();
+ YT_VERIFY(ParseRequestHeader(requestMessage, requestHeader.get()));
+ SetRequestTargetYPath(requestHeader.get(), suffixPath);
+ context->SetRequestHeader(std::move(requestHeader));
+
+ // This should never throw.
+ suffixService->Invoke(context);
+}
+
+TFuture<TYsonString> AsyncYPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter)
+{
+ auto request = TYPathProxy::Get(path);
+ if (attributeFilter) {
+ ToProto(request->mutable_attributes(), attributeFilter);
+ }
+ return ExecuteVerb(service, request)
+ .Apply(BIND([] (TYPathProxy::TRspGetPtr response) {
+ return TYsonString(response->value());
+ }));
+}
+
+TString SyncYPathGetKey(const IYPathServicePtr& service, const TYPath& path)
+{
+ auto request = TYPathProxy::GetKey(path);
+ auto future = ExecuteVerb(service, request);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ return optionalResult->ValueOrThrow()->value();
+}
+
+TYsonString SyncYPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter)
+{
+ auto future = AsyncYPathGet(service, path, attributeFilter);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ return optionalResult->ValueOrThrow();
+}
+
+TFuture<bool> AsyncYPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path)
+{
+ auto request = TYPathProxy::Exists(path);
+ return ExecuteVerb(service, request)
+ .Apply(BIND([] (TYPathProxy::TRspExistsPtr response) {
+ return response->value();
+ }));
+}
+
+bool SyncYPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path)
+{
+ auto future = AsyncYPathExists(service, path);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ return optionalResult->ValueOrThrow();
+}
+
+TFuture<void> AsyncYPathSet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TYsonString& value,
+ bool recursive)
+{
+ auto request = TYPathProxy::Set(path);
+ request->set_value(value.ToString());
+ request->set_recursive(recursive);
+ return ExecuteVerb(service, request).AsVoid();
+}
+
+void SyncYPathSet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TYsonString& value,
+ bool recursive)
+{
+ auto future = AsyncYPathSet(service, path, value, recursive);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ optionalResult->ThrowOnError();
+}
+
+TFuture<void> AsyncYPathRemove(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ bool recursive,
+ bool force)
+{
+ auto request = TYPathProxy::Remove(path);
+ request->set_recursive(recursive);
+ request->set_force(force);
+ return ExecuteVerb(service, request).AsVoid();
+}
+
+void SyncYPathRemove(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ bool recursive,
+ bool force)
+{
+ auto future = AsyncYPathRemove(service, path, recursive, force);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ optionalResult->ThrowOnError();
+}
+
+std::vector<TString> SyncYPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit)
+{
+ auto future = AsyncYPathList(service, path, limit);
+ auto optionalResult = future.TryGetUnique();
+ YT_VERIFY(optionalResult);
+ return optionalResult->ValueOrThrow();
+}
+
+TFuture<std::vector<TString>> AsyncYPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit)
+{
+ auto request = TYPathProxy::List(path);
+ if (limit) {
+ request->set_limit(*limit);
+ }
+ return ExecuteVerb(service, request)
+ .Apply(BIND([] (TYPathProxy::TRspListPtr response) {
+ return ConvertTo<std::vector<TString>>(TYsonString(response->value()));
+ }));
+}
+
+INodePtr WalkNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path,
+ const TNodeWalkOptions& options)
+{
+ auto currentNode = root;
+ NYPath::TTokenizer tokenizer(path);
+ while (true) {
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ break;
+ }
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::At) {
+ tokenizer.Advance();
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ return currentNode->Attributes().ToMap();
+ } else {
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ const auto key = tokenizer.GetLiteralValue();
+ const auto& attributes = currentNode->Attributes();
+ currentNode = attributes.Find<INodePtr>(key);
+ if (!currentNode) {
+ return options.MissingAttributeHandler(key);
+ }
+ }
+ } else {
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ switch (currentNode->GetType()) {
+ case ENodeType::Map: {
+ auto currentMap = currentNode->AsMap();
+ auto key = tokenizer.GetLiteralValue();
+ currentNode = currentMap->FindChild(key);
+ if (!currentNode) {
+ return options.MissingChildKeyHandler(currentMap, key);
+ }
+ break;
+ }
+ case ENodeType::List: {
+ auto currentList = currentNode->AsList();
+ const auto& token = tokenizer.GetToken();
+ int index = ParseListIndex(token);
+ auto optionalAdjustedIndex = TryAdjustListIndex(index, currentList->GetChildCount());
+ currentNode = optionalAdjustedIndex ? currentList->FindChild(*optionalAdjustedIndex) : nullptr;
+ if (!currentNode) {
+ return options.MissingChildIndexHandler(currentList, optionalAdjustedIndex.value_or(index));
+ }
+ break;
+ }
+ default:
+ return options.NodeCannotHaveChildrenHandler(currentNode);
+ }
+ }
+ }
+ return currentNode;
+}
+
+void SetNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path,
+ const INodePtr& value,
+ bool force)
+{
+ auto currentNode = root;
+
+ NYPath::TTokenizer tokenizer(path);
+
+ TString currentToken;
+ TString currentLiteralValue;
+ auto nextSegment = [&] () {
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ currentToken = TString(tokenizer.GetToken());
+ currentLiteralValue = tokenizer.GetLiteralValue();
+ };
+
+ tokenizer.Advance();
+ nextSegment();
+
+ auto factory = root->CreateFactory();
+ while (tokenizer.Advance() != NYPath::ETokenType::EndOfStream) {
+ switch (currentNode->GetType()) {
+ case ENodeType::Map: {
+ const auto& key = currentLiteralValue;
+ auto currentMap = currentNode->AsMap();
+ if (force) {
+ auto child = currentMap->FindChild(key);
+ if (!child) {
+ child = factory->CreateMap();
+ YT_VERIFY(currentMap->AddChild(key, child));
+ }
+ currentNode = child;
+ } else {
+ currentNode = currentMap->GetChildOrThrow(key);
+ }
+ break;
+ }
+
+ case ENodeType::List: {
+ auto currentList = currentNode->AsList();
+ int index = ParseListIndex(currentToken);
+ int adjustedIndex = currentList->AdjustChildIndexOrThrow(index);
+ currentNode = currentList->GetChildOrThrow(adjustedIndex);
+ break;
+ }
+
+ default:
+ ThrowCannotHaveChildren(currentNode);
+ YT_ABORT();
+ }
+ nextSegment();
+ }
+
+ // Set value.
+ switch (currentNode->GetType()) {
+ case ENodeType::Map: {
+ const auto& key = currentLiteralValue;
+ auto currentMap = currentNode->AsMap();
+ auto child = currentMap->FindChild(key);
+ if (child) {
+ currentMap->ReplaceChild(child, value);
+ } else {
+ YT_VERIFY(currentMap->AddChild(key, value));
+ }
+ break;
+ }
+
+ case ENodeType::List: {
+ auto currentList = currentNode->AsList();
+ int index = ParseListIndex(currentToken);
+ int adjustedIndex = currentList->AdjustChildIndexOrThrow(index);
+ auto child = currentList->GetChildOrThrow(adjustedIndex);
+ currentList->ReplaceChild(child, value);
+ break;
+ }
+
+ default:
+ ThrowCannotHaveChildren(currentNode);
+ YT_ABORT();
+ }
+}
+
+bool RemoveNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path)
+{
+ auto node = WalkNodeByYPath(root, path, FindNodeByYPathOptions);
+ if (!node) {
+ return false;
+ }
+
+ node->GetParent()->RemoveChild(node);
+
+ return true;
+}
+
+void ForceYPath(
+ const INodePtr& root,
+ const TYPath& path)
+{
+ auto currentNode = root;
+
+ NYPath::TTokenizer tokenizer(path);
+
+ TString currentToken;
+ TString currentLiteralValue;
+ auto nextSegment = [&] () {
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ tokenizer.Advance();
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ currentToken = TString(tokenizer.GetToken());
+ currentLiteralValue = tokenizer.GetLiteralValue();
+ };
+
+ tokenizer.Advance();
+ nextSegment();
+
+ auto factory = root->CreateFactory();
+
+ while (tokenizer.Advance() != NYPath::ETokenType::EndOfStream) {
+ INodePtr child;
+ switch (currentNode->GetType()) {
+ case ENodeType::Map: {
+ auto currentMap = currentNode->AsMap();
+ const auto& key = currentLiteralValue;
+ child = currentMap->FindChild(key);
+ if (!child) {
+ child = factory->CreateMap();
+ YT_VERIFY(currentMap->AddChild(key, child));
+ }
+ break;
+ }
+
+ case ENodeType::List: {
+ auto currentList = currentNode->AsList();
+ int index = ParseListIndex(currentToken);
+ int adjustedIndex = currentList->AdjustChildIndexOrThrow(index);
+ child = currentList->GetChildOrThrow(adjustedIndex);
+ break;
+ }
+
+ default:
+ ThrowCannotHaveChildren(currentNode);
+ YT_ABORT();
+ }
+
+ nextSegment();
+ currentNode = child;
+ }
+
+ factory->Commit();
+}
+
+INodePtr CloneNode(const INodePtr& node)
+{
+ return ConvertToNode(node);
+}
+
+INodePtr PatchNode(const INodePtr& base, const INodePtr& patch)
+{
+ if (base->GetType() == ENodeType::Map && patch->GetType() == ENodeType::Map) {
+ auto result = CloneNode(base);
+ auto resultMap = result->AsMap();
+ auto patchMap = patch->AsMap();
+ auto baseMap = base->AsMap();
+ for (const auto& key : patchMap->GetKeys()) {
+ if (baseMap->FindChild(key)) {
+ resultMap->RemoveChild(key);
+ YT_VERIFY(resultMap->AddChild(key, PatchNode(baseMap->GetChildOrThrow(key), patchMap->GetChildOrThrow(key))));
+ } else {
+ YT_VERIFY(resultMap->AddChild(key, CloneNode(patchMap->GetChildOrThrow(key))));
+ }
+ }
+ result->MutableAttributes()->MergeFrom(patch->Attributes());
+ return result;
+ } else {
+ auto result = CloneNode(patch);
+ auto* resultAttributes = result->MutableAttributes();
+ resultAttributes->Clear();
+ if (base->GetType() == patch->GetType()) {
+ resultAttributes->MergeFrom(base->Attributes());
+ }
+ resultAttributes->MergeFrom(patch->Attributes());
+ return result;
+ }
+}
+
+bool AreNodesEqual(
+ const INodePtr& lhs,
+ const INodePtr& rhs,
+ const TNodesEqualityOptions& options)
+{
+ if (!lhs && !rhs) {
+ return true;
+ }
+
+ if (!lhs || !rhs) {
+ return false;
+ }
+
+ // Check types.
+ auto lhsType = lhs->GetType();
+ auto rhsType = rhs->GetType();
+ if (lhsType != rhsType) {
+ return false;
+ }
+
+ // Check attributes.
+ const auto& lhsAttributes = lhs->Attributes();
+ const auto& rhsAttributes = rhs->Attributes();
+ if (lhsAttributes != rhsAttributes) {
+ return false;
+ }
+
+ // Check content.
+ switch (lhsType) {
+ case ENodeType::Map: {
+ auto lhsMap = lhs->AsMap();
+ auto rhsMap = rhs->AsMap();
+
+ auto lhsKeys = lhsMap->GetKeys();
+ auto rhsKeys = rhsMap->GetKeys();
+
+ if (lhsKeys.size() != rhsKeys.size()) {
+ return false;
+ }
+
+ std::sort(lhsKeys.begin(), lhsKeys.end());
+ std::sort(rhsKeys.begin(), rhsKeys.end());
+
+ for (size_t index = 0; index < lhsKeys.size(); ++index) {
+ const auto& lhsKey = lhsKeys[index];
+ const auto& rhsKey = rhsKeys[index];
+ if (lhsKey != rhsKey) {
+ return false;
+ }
+ if (!AreNodesEqual(lhsMap->FindChild(lhsKey), rhsMap->FindChild(rhsKey), options)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ case ENodeType::List: {
+ auto lhsList = lhs->AsList();
+ auto lhsChildren = lhsList->GetChildren();
+
+ auto rhsList = rhs->AsList();
+ auto rhsChildren = rhsList->GetChildren();
+
+ if (lhsChildren.size() != rhsChildren.size()) {
+ return false;
+ }
+
+ for (size_t index = 0; index < lhsChildren.size(); ++index) {
+ if (!AreNodesEqual(lhsList->FindChild(index), rhsList->FindChild(index), options)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ case ENodeType::Int64:
+ return lhs->AsInt64()->GetValue() == rhs->AsInt64()->GetValue();
+
+ case ENodeType::Uint64:
+ return lhs->AsUint64()->GetValue() == rhs->AsUint64()->GetValue();
+
+ case ENodeType::Double:
+ return std::abs(lhs->AsDouble()->GetValue() - rhs->AsDouble()->GetValue()) <= options.DoubleTypePrecision;
+
+ case ENodeType::Boolean:
+ return lhs->AsBoolean()->GetValue() == rhs->AsBoolean()->GetValue();
+
+ case ENodeType::String:
+ return lhs->AsString()->GetValue() == rhs->AsString()->GetValue();
+
+ case ENodeType::Entity:
+ return true;
+
+ default:
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNodeWalkOptions GetNodeByYPathOptions {
+ .MissingAttributeHandler = [] (const TString& key) {
+ ThrowNoSuchAttribute(key);
+ return nullptr;
+ },
+ .MissingChildKeyHandler = [] (const IMapNodePtr& node, const TString& key) {
+ ThrowNoSuchChildKey(node, key);
+ return nullptr;
+ },
+ .MissingChildIndexHandler = [] (const IListNodePtr& node, int index) {
+ ThrowNoSuchChildIndex(node, index);
+ return nullptr;
+ },
+ .NodeCannotHaveChildrenHandler = [] (const INodePtr& node) {
+ ThrowCannotHaveChildren(node);
+ return nullptr;
+ }
+};
+
+TNodeWalkOptions FindNodeByYPathOptions {
+ .MissingAttributeHandler = [] (const TString& /* key */) {
+ return nullptr;
+ },
+ .MissingChildKeyHandler = [] (const IMapNodePtr& /* node */, const TString& /* key */) {
+ return nullptr;
+ },
+ .MissingChildIndexHandler = [] (const IListNodePtr& /* node */, int /* index */) {
+ return nullptr;
+ },
+ .NodeCannotHaveChildrenHandler = GetNodeByYPathOptions.NodeCannotHaveChildrenHandler
+};
+
+TNodeWalkOptions FindNodeByYPathNoThrowOptions {
+ .MissingAttributeHandler = [] (const TString& /* key */) {
+ return nullptr;
+ },
+ .MissingChildKeyHandler = [] (const IMapNodePtr& /* node */, const TString& /* key */) {
+ return nullptr;
+ },
+ .MissingChildIndexHandler = [] (const IListNodePtr& /* node */, int /* index */) {
+ return nullptr;
+ },
+ .NodeCannotHaveChildrenHandler = [] (const INodePtr& /* node */) {
+ return nullptr;
+ },
+};
+
+INodePtr GetNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path)
+{
+ return WalkNodeByYPath(root, path, GetNodeByYPathOptions);
+}
+
+INodePtr FindNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path)
+{
+ return WalkNodeByYPath(root, path, FindNodeByYPathOptions);
+}
+
+INodePtr FindNodeByYPathNoThrow(
+ const INodePtr& root,
+ const TYPath& path)
+{
+ return WalkNodeByYPath(root, path, FindNodeByYPathNoThrowOptions);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_client.h b/yt/yt/core/ytree/ypath_client.h
new file mode 100644
index 0000000000..fbe3d40364
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_client.h
@@ -0,0 +1,385 @@
+#pragma once
+
+#include "public.h"
+#include "ypath_service.h"
+#include "attribute_filter.h"
+
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/rpc/client.h>
+
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathRequest
+ : public NRpc::IClientRequest
+{
+public:
+ //! Enables tagging requests with arbitrary payload.
+ //! These tags are propagated to the respective responses (if a particular implementation supports this).
+ //! This simplifies correlating requests with responses within a batch.
+ DEFINE_BYREF_RW_PROPERTY(std::any, Tag);
+
+public:
+ NRpc::TRequestId GetRequestId() const override;
+ NRpc::TRealmId GetRealmId() const override;
+ const TString& GetMethod() const override;
+ const TString& GetService() const override;
+
+ using NRpc::IClientRequest::DeclareClientFeature;
+ using NRpc::IClientRequest::RequireServerFeature;
+
+ void DeclareClientFeature(int featureId) override;
+ void RequireServerFeature(int featureId) override;
+
+ const TString& GetUser() const override;
+ void SetUser(const TString& user) override;
+
+ const TString& GetUserTag() const override;
+ void SetUserTag(const TString& tag) override;
+
+ void SetUserAgent(const TString& userAgent) override;
+
+ bool GetRetry() const override;
+ void SetRetry(bool value) override;
+
+ NRpc::TMutationId GetMutationId() const override;
+ void SetMutationId(NRpc::TMutationId id) override;
+
+ size_t GetHash() const override;
+
+ const NRpc::NProto::TRequestHeader& Header() const override;
+ NRpc::NProto::TRequestHeader& Header() override;
+
+ bool IsStreamingEnabled() const override;
+
+ const NRpc::TStreamingParameters& ClientAttachmentsStreamingParameters() const override;
+ NRpc::TStreamingParameters& ClientAttachmentsStreamingParameters() override;
+
+ const NRpc::TStreamingParameters& ServerAttachmentsStreamingParameters() const override;
+ NRpc::TStreamingParameters& ServerAttachmentsStreamingParameters() override;
+
+ NConcurrency::IAsyncZeroCopyOutputStreamPtr GetRequestAttachmentsStream() const override;
+ NConcurrency::IAsyncZeroCopyInputStreamPtr GetResponseAttachmentsStream() const override;
+
+ bool IsLegacyRpcCodecsEnabled() override;
+
+ TSharedRefArray Serialize() override;
+
+protected:
+ explicit TYPathRequest(const NRpc::NProto::TRequestHeader& header);
+
+ TYPathRequest(
+ TString service,
+ TString method,
+ NYPath::TYPath path,
+ bool mutating);
+
+ NRpc::NProto::TRequestHeader Header_;
+ std::vector<TSharedRef> Attachments_;
+
+ virtual TSharedRef SerializeBody() const = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(TYPathRequest)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class TResponseMessage>
+class TTypedYPathRequest
+ : public TYPathRequest
+ , public TRequestMessage
+{
+public:
+ typedef TTypedYPathResponse<TRequestMessage, TResponseMessage> TTypedResponse;
+
+ explicit TTypedYPathRequest(const NRpc::NProto::TRequestHeader& header)
+ : TYPathRequest(header)
+ { }
+
+ TTypedYPathRequest(
+ const TString& service,
+ const TString& method,
+ const NYPath::TYPath& path,
+ bool mutating)
+ : TYPathRequest(
+ service,
+ method,
+ path,
+ mutating)
+ { }
+
+protected:
+ TSharedRef SerializeBody() const override
+ {
+ return SerializeProtoToRefWithEnvelope(*this);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathResponse
+ : public TRefCounted
+{
+public:
+ DEFINE_BYREF_RW_PROPERTY(std::vector<TSharedRef>, Attachments);
+
+ //! A copy of the request's tag.
+ DEFINE_BYREF_RW_PROPERTY(std::any, Tag);
+
+public:
+ void Deserialize(const TSharedRefArray& message);
+
+protected:
+ virtual bool TryDeserializeBody(TRef data, std::optional<NCompression::ECodec> codecId = {});
+};
+
+DEFINE_REFCOUNTED_TYPE(TYPathResponse)
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TRequestMessage, class TResponseMessage>
+class TTypedYPathResponse
+ : public TYPathResponse
+ , public TResponseMessage
+{
+protected:
+ bool TryDeserializeBody(TRef data, std::optional<NCompression::ECodec> codecId = {}) override
+ {
+ return codecId
+ ? TryDeserializeProtoWithCompression(this, data, *codecId)
+ : TryDeserializeProtoWithEnvelope(this, data);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DEFINE_YPATH_PROXY(name) \
+ static const ::NYT::NRpc::TServiceDescriptor& GetDescriptor() \
+ { \
+ static const auto Descriptor = ::NYT::NRpc::TServiceDescriptor(#name); \
+ return Descriptor; \
+ } \
+ static_assert(true)
+
+#define DEFINE_YPATH_PROXY_METHOD_IMPL(ns, method, isMutating) \
+ typedef ::NYT::NYTree::TTypedYPathRequest<ns::TReq##method, ns::TRsp##method> TReq##method; \
+ typedef ::NYT::NYTree::TTypedYPathResponse<ns::TReq##method, ns::TRsp##method> TRsp##method; \
+ typedef ::NYT::TIntrusivePtr<TReq##method> TReq##method##Ptr; \
+ typedef ::NYT::TIntrusivePtr<TRsp##method> TRsp##method##Ptr; \
+ typedef ::NYT::TErrorOr<TRsp##method##Ptr> TErrorOrRsp##method##Ptr; \
+ \
+ static TReq##method##Ptr method(const NYT::NYPath::TYPath& path = NYT::NYPath::TYPath()) \
+ { \
+ return New<TReq##method>(GetDescriptor().ServiceName, #method, path, isMutating); \
+ } \
+ static_assert(true)
+
+#define DEFINE_YPATH_PROXY_METHOD(ns, method) \
+ DEFINE_YPATH_PROXY_METHOD_IMPL(ns, method, false)
+
+#define DEFINE_MUTATING_YPATH_PROXY_METHOD(ns, method) \
+ DEFINE_YPATH_PROXY_METHOD_IMPL(ns, method, true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+const TYPath& GetRequestTargetYPath(const NRpc::NProto::TRequestHeader& header);
+const TYPath& GetOriginalRequestTargetYPath(const NRpc::NProto::TRequestHeader& header);
+void SetRequestTargetYPath(NRpc::NProto::TRequestHeader* header, TYPath path);
+
+bool IsRequestMutating(const NRpc::NProto::TRequestHeader& header);
+
+//! Runs a sequence of IYPathService::Resolve calls aimed to discover the
+//! ultimate endpoint responsible for serving a given request.
+void ResolveYPath(
+ const IYPathServicePtr& rootService,
+ const IYPathServiceContextPtr& context,
+ IYPathServicePtr* suffixService,
+ TYPath* suffixPath);
+
+//! Asynchronously executes an untyped request against a given service.
+TFuture<TSharedRefArray>
+ExecuteVerb(
+ const IYPathServicePtr& service,
+ const TSharedRefArray& requestMessage);
+
+//! Asynchronously executes a request against a given service.
+void ExecuteVerb(
+ const IYPathServicePtr& service,
+ const IYPathServiceContextPtr& context);
+
+//! Asynchronously executes a typed YPath request against a given service.
+template <class TTypedRequest>
+TFuture<TIntrusivePtr<typename TTypedRequest::TTypedResponse>>
+ExecuteVerb(
+ const IYPathServicePtr& service,
+ const TIntrusivePtr<TTypedRequest>& request);
+
+//! Synchronously executes a typed YPath request against a given service.
+//! Throws if an error has occurred.
+template <class TTypedRequest>
+TIntrusivePtr<typename TTypedRequest::TTypedResponse>
+SyncExecuteVerb(
+ const IYPathServicePtr& service,
+ const TIntrusivePtr<TTypedRequest>& request);
+
+//! Executes |GetKey| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+TString SyncYPathGetKey(
+ const IYPathServicePtr& service,
+ const TYPath& path);
+
+//! Asynchronously executes |Get| verb.
+TFuture<NYson::TYsonString> AsyncYPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter = {});
+
+//! Executes |Get| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+NYson::TYsonString SyncYPathGet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter = {});
+
+//! Asynchronously executes |Exists| verb.
+TFuture<bool> AsyncYPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path);
+
+//! Executes |Exists| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+bool SyncYPathExists(
+ const IYPathServicePtr& service,
+ const TYPath& path);
+
+//! Asynchronously executes |Set| verb.
+TFuture<void> AsyncYPathSet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const NYson::TYsonString& value,
+ bool recursive = false);
+
+//! Executes |Set| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+void SyncYPathSet(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ const NYson::TYsonString& value,
+ bool recursive = false);
+
+//! Asynchronously executes |Remove| verb.
+TFuture<void> AsyncYPathRemove(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ bool recursive = true,
+ bool force = false);
+
+//! Executes |Remove| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+void SyncYPathRemove(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ bool recursive = true,
+ bool force = false);
+
+//! Executes |List| verb assuming #service handles requests synchronously. Throws if an error has occurred.
+std::vector<TString> SyncYPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit = std::nullopt);
+
+//! Asynchronously executes |List| verb.
+TFuture<std::vector<TString>> AsyncYPathList(
+ const IYPathServicePtr& service,
+ const TYPath& path,
+ std::optional<i64> limit = std::nullopt);
+
+//! Creates missing maps along #path.
+/*!
+ * E.g. if #root is an empty map and #path is |/a/b/c| then
+ * nested maps |a| and |b| get created. Note that the final key (i.e. |c|)
+ * is not forced (since we have no idea of its type anyway).
+ */
+void ForceYPath(const INodePtr& root, const TYPath& path);
+
+//! Constructs an ephemeral deep copy of #node.
+INodePtr CloneNode(const INodePtr& node);
+
+//! Applies changes given by #patch to #base.
+//! Returns the resulting tree.
+INodePtr PatchNode(const INodePtr& base, const INodePtr& patch);
+
+struct TNodesEqualityOptions
+{
+ double DoubleTypePrecision = 1e-6;
+};
+
+//! Checks given nodes for deep equality.
+bool AreNodesEqual(
+ const INodePtr& lhs,
+ const INodePtr& rhs,
+ const TNodesEqualityOptions& options = {});
+
+/////////////////////////////////////////////////////////////////////////////
+
+struct TNodeWalkOptions
+{
+ std::function<INodePtr(const TString&)> MissingAttributeHandler;
+ std::function<INodePtr(const IMapNodePtr&, const TString&)> MissingChildKeyHandler;
+ std::function<INodePtr(const IListNodePtr&, int)> MissingChildIndexHandler;
+ std::function<INodePtr(const INodePtr&)> NodeCannotHaveChildrenHandler;
+};
+
+extern TNodeWalkOptions GetNodeByYPathOptions;
+extern TNodeWalkOptions FindNodeByYPathOptions;
+extern TNodeWalkOptions FindNodeByYPathNoThrowOptions;
+
+//! Generic function walking down the node according to given ypath.
+INodePtr WalkNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path,
+ const TNodeWalkOptions& options);
+
+/*!
+ * Throws exception if the specified node does not exist.
+ */
+INodePtr GetNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path);
+
+/*!
+ * Does not throw exception if the specified node does not exist, but still throws on attempt of
+ * moving to the child of a non-composite node.
+ */
+INodePtr FindNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path);
+
+/*!
+ * A version of #FindNodeByYPath that never throws.
+ */
+INodePtr FindNodeByYPathNoThrow(
+ const INodePtr& root,
+ const TYPath& path);
+
+void SetNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path,
+ const INodePtr& value,
+ bool force = false);
+
+/*!
+ * Returns |false| if the specified node does not exists.
+ */
+bool RemoveNodeByYPath(
+ const INodePtr& root,
+ const TYPath& path);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define YPATH_CLIENT_INL_H_
+#include "ypath_client-inl.h"
+#undef YPATH_CLIENT_INL_H_
diff --git a/yt/yt/core/ytree/ypath_detail-inl.h b/yt/yt/core/ytree/ypath_detail-inl.h
new file mode 100644
index 0000000000..7d59cfbfe1
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_detail-inl.h
@@ -0,0 +1,21 @@
+#ifndef YPATH_DETAIL_INL_H_
+#error "Direct inclusion of this file is not allowed, include ypath_detail.h"
+// For the sake of sane code completion.
+#include "ypath_detail.h"
+#endif
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TContextPtr>
+void TSupportsExistsBase::Reply(const TContextPtr& context, bool exists)
+{
+ context->Response().set_value(exists);
+ context->SetResponseInfo("Result: %v", exists);
+ context->Reply();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_detail.cpp b/yt/yt/core/ytree/ypath_detail.cpp
new file mode 100644
index 0000000000..1daeced53e
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_detail.cpp
@@ -0,0 +1,1858 @@
+#include "ypath_detail.h"
+
+#include "node_detail.h"
+#include "helpers.h"
+#include "request_complexity_limiter.h"
+#include "system_attribute_provider.h"
+#include "ypath_client.h"
+
+#include <yt/yt/core/yson/attribute_consumer.h>
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt_proto/yt/core/rpc/proto/rpc.pb.h>
+#include <yt/yt/core/rpc/server_detail.h>
+#include <yt/yt/core/rpc/message.h>
+
+#include <yt/yt/core/net/address.h>
+
+#include <yt/yt/core/profiling/timing.h>
+
+namespace NYT::NYTree {
+
+using namespace NRpc;
+using namespace NYPath;
+using namespace NRpc::NProto;
+using namespace NYson;
+using namespace NYTree;
+
+using NYT::FromProto;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static const auto NoneYsonFuture = MakeFuture(TYsonString());
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYPathServiceContextWrapper::TYPathServiceContextWrapper(IYPathServiceContextPtr underlyingContext)
+ : TServiceContextWrapper(underlyingContext)
+ , UnderlyingContext_(std::move(underlyingContext))
+{ }
+
+void TYPathServiceContextWrapper::SetRequestHeader(std::unique_ptr<NRpc::NProto::TRequestHeader> header)
+{
+ UnderlyingContext_->SetRequestHeader(std::move(header));
+}
+
+TReadRequestComplexityLimiterPtr TYPathServiceContextWrapper::GetReadRequestComplexityLimiter()
+{
+ return UnderlyingContext_->GetReadRequestComplexityLimiter();
+}
+
+const IYPathServiceContextPtr& TYPathServiceContextWrapper::GetUnderlyingContext() const
+{
+ return UnderlyingContext_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathService::TResolveResult TYPathServiceBase::Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+ tokenizer.Advance();
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ if (tokenizer.GetType() == NYPath::ETokenType::EndOfStream) {
+ return ResolveSelf(TYPath(tokenizer.GetSuffix()), context);
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+
+ if (tokenizer.Advance() == NYPath::ETokenType::At) {
+ return ResolveAttributes(TYPath(tokenizer.GetSuffix()), context);
+ } else {
+ return ResolveRecursive(TYPath(tokenizer.GetInput()), context);
+ }
+}
+
+IYPathService::TResolveResult TYPathServiceBase::ResolveSelf(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ return TResolveResultHere{path};
+}
+
+IYPathService::TResolveResult TYPathServiceBase::ResolveAttributes(
+ const TYPath& /*path*/,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ THROW_ERROR_EXCEPTION("Object cannot have attributes");
+}
+
+IYPathService::TResolveResult TYPathServiceBase::ResolveRecursive(
+ const TYPath& /*path*/,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ THROW_ERROR_EXCEPTION("Object cannot have children");
+}
+
+void TYPathServiceBase::Invoke(const IYPathServiceContextPtr& context)
+{
+ TError error;
+ try {
+ BeforeInvoke(context);
+ if (!DoInvoke(context)) {
+ ThrowMethodNotSupported(context->GetMethod());
+ }
+ } catch (const std::exception& ex) {
+ error = ex;
+ }
+
+ AfterInvoke(context);
+
+ if (!error.IsOK()) {
+ context->Reply(error);
+ }
+}
+
+void TYPathServiceBase::BeforeInvoke(const IYPathServiceContextPtr& /*context*/)
+{ }
+
+bool TYPathServiceBase::DoInvoke(const IYPathServiceContextPtr& /*context*/)
+{
+ return false;
+}
+
+void TYPathServiceBase::AfterInvoke(const IYPathServiceContextPtr& /*context*/)
+{ }
+
+void TYPathServiceBase::DoWriteAttributesFragment(
+ NYson::IAsyncYsonConsumer* /*consumer*/,
+ const TAttributeFilter& /*attributeFilter*/,
+ bool /*stable*/)
+{ }
+
+bool TYPathServiceBase::ShouldHideAttributes()
+{
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define IMPLEMENT_SUPPORTS_VERB_RESOLVE(method, onPathError) \
+ DEFINE_RPC_SERVICE_METHOD(TSupports##method, method) \
+ { \
+ NYPath::TTokenizer tokenizer(GetRequestTargetYPath(context->RequestHeader())); \
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) { \
+ method##Self(request, response, context); \
+ return; \
+ } \
+ tokenizer.Skip(NYPath::ETokenType::Ampersand); \
+ if (tokenizer.GetType() != NYPath::ETokenType::Slash) { \
+ onPathError \
+ return; \
+ } \
+ if (tokenizer.Advance() == NYPath::ETokenType::At) { \
+ method##Attribute(TYPath(tokenizer.GetSuffix()), request, response, context); \
+ } else { \
+ method##Recursive(TYPath(tokenizer.GetInput()), request, response, context); \
+ } \
+ }
+
+#define IMPLEMENT_SUPPORTS_VERB(method) \
+ IMPLEMENT_SUPPORTS_VERB_RESOLVE( \
+ method, \
+ { \
+ tokenizer.ThrowUnexpected(); \
+ } \
+ ) \
+ \
+ void TSupports##method::method##Attribute(const TYPath& /*path*/, TReq##method* /*request*/, TRsp##method* /*response*/, const TCtx##method##Ptr& context) \
+ { \
+ ThrowMethodNotSupported(context->GetMethod(), TString("attribute")); \
+ } \
+ \
+ void TSupports##method::method##Self(TReq##method* /*request*/, TRsp##method* /*response*/, const TCtx##method##Ptr& context) \
+ { \
+ ThrowMethodNotSupported(context->GetMethod(), TString("self")); \
+ } \
+ \
+ void TSupports##method::method##Recursive(const TYPath& /*path*/, TReq##method* /*request*/, TRsp##method* /*response*/, const TCtx##method##Ptr& context) \
+ { \
+ ThrowMethodNotSupported(context->GetMethod(), TString("recursive")); \
+ }
+
+IMPLEMENT_SUPPORTS_VERB(GetKey)
+IMPLEMENT_SUPPORTS_VERB(Get)
+IMPLEMENT_SUPPORTS_VERB(Set)
+IMPLEMENT_SUPPORTS_VERB(List)
+IMPLEMENT_SUPPORTS_VERB(Remove)
+
+IMPLEMENT_SUPPORTS_VERB_RESOLVE(
+ Exists,
+ {
+ context->SetRequestInfo();
+ Reply(context, /*exists*/ false);
+ })
+
+#undef IMPLEMENT_SUPPORTS_VERB
+#undef IMPLEMENT_SUPPORTS_VERB_RESOLVE
+
+void TSupportsExists::ExistsAttribute(
+ const TYPath& /*path*/,
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ context->SetRequestInfo();
+
+ Reply(context, /*exists*/ false);
+}
+
+void TSupportsExists::ExistsSelf(
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ context->SetRequestInfo();
+
+ Reply(context, /*exists*/ true);
+}
+
+void TSupportsExists::ExistsRecursive(
+ const TYPath& /*path*/,
+ TReqExists* /*request*/,
+ TRspExists* /*response*/,
+ const TCtxExistsPtr& context)
+{
+ context->SetRequestInfo();
+
+ Reply(context, /*exists*/ false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_RPC_SERVICE_METHOD(TSupportsMultisetAttributes, Multiset)
+{
+ context->SetRequestInfo("KeyCount: %v", request->subrequests_size());
+
+ auto ctx = New<TCtxMultisetAttributes>(
+ context->GetUnderlyingContext(),
+ context->GetOptions());
+ ctx->DeserializeRequest();
+
+ auto* req = &ctx->Request();
+ auto* rsp = &ctx->Response();
+ DoSetAttributes(GetRequestTargetYPath(context->RequestHeader()), req, rsp, ctx);
+
+ context->Reply();
+}
+
+DEFINE_RPC_SERVICE_METHOD(TSupportsMultisetAttributes, MultisetAttributes)
+{
+ context->SetRequestInfo("KeyCount: %v", request->subrequests_size());
+
+ DoSetAttributes(GetRequestTargetYPath(context->RequestHeader()), request, response, context);
+
+ context->Reply();
+}
+
+void TSupportsMultisetAttributes::DoSetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* response,
+ const TCtxMultisetAttributesPtr& context)
+{
+ NYPath::TTokenizer tokenizer(path);
+
+ tokenizer.Advance();
+ tokenizer.Skip(NYPath::ETokenType::Ampersand);
+ tokenizer.Expect(NYPath::ETokenType::Slash);
+ if (tokenizer.Advance() != NYPath::ETokenType::At) {
+ tokenizer.ThrowUnexpected();
+ }
+
+ SetAttributes(TYPath(tokenizer.GetSuffix()), request, response, context);
+}
+
+void TSupportsMultisetAttributes::SetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* response,
+ const TCtxMultisetAttributesPtr& context)
+{
+ Y_UNUSED(path);
+ Y_UNUSED(request);
+ Y_UNUSED(response);
+ Y_UNUSED(context);
+ ThrowMethodNotSupported("MultisetAttributes", TString("attributes"));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TSupportsPermissions::ValidatePermission(
+ EPermissionCheckScope /*scope*/,
+ EPermission /*permission*/,
+ const TString& /*user*/)
+{ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSupportsPermissions::TCachingPermissionValidator::TCachingPermissionValidator(
+ TSupportsPermissions* owner,
+ EPermissionCheckScope scope)
+ : Owner_(owner)
+ , Scope_(scope)
+{ }
+
+void TSupportsPermissions::TCachingPermissionValidator::Validate(EPermission permission, const TString& user)
+{
+ auto& validatedPermissions = ValidatedPermissions_[user];
+ if (None(validatedPermissions & permission)) {
+ Owner_->ValidatePermission(Scope_, permission, user);
+ validatedPermissions |= permission;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSupportsAttributes::TCombinedAttributeDictionary::TCombinedAttributeDictionary(TSupportsAttributes* owner)
+ : Owner_(owner)
+{ }
+
+std::vector<TString> TSupportsAttributes::TCombinedAttributeDictionary::ListKeys() const
+{
+ std::vector<TString> keys;
+
+ auto* provider = Owner_->GetBuiltinAttributeProvider();
+ if (provider) {
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> descriptors;
+ provider->ReserveAndListSystemAttributes(&descriptors);
+ for (const auto& descriptor : descriptors) {
+ if (descriptor.Present && !descriptor.Custom && !descriptor.Opaque) {
+ keys.push_back(descriptor.InternedKey.Unintern());
+ }
+ }
+ }
+
+ auto* customAttributes = Owner_->GetCustomAttributes();
+ if (customAttributes) {
+ auto customKeys = customAttributes->ListKeys();
+ for (auto&& key : customKeys) {
+ keys.push_back(std::move(key));
+ }
+ }
+ return keys;
+}
+
+std::vector<IAttributeDictionary::TKeyValuePair> TSupportsAttributes::TCombinedAttributeDictionary::ListPairs() const
+{
+ std::vector<TKeyValuePair> pairs;
+
+ auto* provider = Owner_->GetBuiltinAttributeProvider();
+ if (provider) {
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> descriptors;
+ provider->ReserveAndListSystemAttributes(&descriptors);
+ for (const auto& descriptor : descriptors) {
+ if (descriptor.Present && !descriptor.Custom && !descriptor.Opaque) {
+ auto value = provider->FindBuiltinAttribute(descriptor.InternedKey);
+ if (value) {
+ auto key = descriptor.InternedKey.Unintern();
+ pairs.push_back(std::make_pair(std::move(key), std::move(value)));
+ }
+ }
+ }
+ }
+
+ auto* customAttributes = Owner_->GetCustomAttributes();
+ if (customAttributes) {
+ for (const auto& pair : customAttributes->ListPairs()) {
+ pairs.push_back(pair);
+ }
+ }
+
+ return pairs;
+}
+
+TYsonString TSupportsAttributes::TCombinedAttributeDictionary::FindYson(TStringBuf key) const
+{
+ auto* provider = Owner_->GetBuiltinAttributeProvider();
+ if (provider) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ if (internedKey != InvalidInternedAttribute) {
+ const auto& builtinKeys = provider->GetBuiltinAttributeKeys();
+ if (builtinKeys.find(internedKey) != builtinKeys.end()) {
+ return provider->FindBuiltinAttribute(internedKey);
+ }
+ }
+ }
+
+ auto* customAttributes = Owner_->GetCustomAttributes();
+ if (!customAttributes) {
+ return TYsonString();
+ }
+ return customAttributes->FindYson(key);
+}
+
+void TSupportsAttributes::TCombinedAttributeDictionary::SetYson(const TString& key, const TYsonString& value)
+{
+ auto* provider = Owner_->GetBuiltinAttributeProvider();
+ if (provider) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ if (internedKey != InvalidInternedAttribute) {
+ const auto& builtinKeys = provider->GetBuiltinAttributeKeys();
+ if (builtinKeys.find(internedKey) != builtinKeys.end()) {
+ if (!provider->SetBuiltinAttribute(internedKey, value)) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+ return;
+ }
+ }
+ }
+
+ auto* customAttributes = Owner_->GetCustomAttributes();
+ if (!customAttributes) {
+ ThrowNoSuchBuiltinAttribute(key);
+ }
+ customAttributes->SetYson(key, value);
+}
+
+bool TSupportsAttributes::TCombinedAttributeDictionary::Remove(const TString& key)
+{
+ auto* provider = Owner_->GetBuiltinAttributeProvider();
+ if (provider) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ if (internedKey != InvalidInternedAttribute) {
+ const auto& builtinKeys = provider->GetBuiltinAttributeKeys();
+ if (builtinKeys.find(internedKey) != builtinKeys.end()) {
+ return provider->RemoveBuiltinAttribute(internedKey);
+ }
+ }
+ }
+
+ auto* customAttributes = Owner_->GetCustomAttributes();
+ if (!customAttributes) {
+ ThrowNoSuchBuiltinAttribute(key);
+ }
+ return customAttributes->Remove(key);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSupportsAttributes::TSupportsAttributes()
+ : CombinedAttributes_(New<TSupportsAttributes::TCombinedAttributeDictionary>(this))
+{ }
+
+IYPathService::TResolveResult TSupportsAttributes::ResolveAttributes(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context)
+{
+ const auto& method = context->GetMethod();
+ if (method != "Get" &&
+ method != "Set" &&
+ method != "List" &&
+ method != "Remove" &&
+ method != "Exists" &&
+ method != "Multiset" &&
+ method != "MultisetAttributes")
+ {
+ ThrowMethodNotSupported(method);
+ }
+
+ return TResolveResultHere{"/@" + path};
+}
+
+TFuture<TYsonString> TSupportsAttributes::DoFindAttribute(TStringBuf key)
+{
+ auto* customAttributes = GetCustomAttributes();
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+
+ if (customAttributes) {
+ auto attribute = customAttributes->FindYson(key);
+ if (attribute) {
+ return MakeFuture(attribute);
+ }
+ }
+
+ if (builtinAttributeProvider) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ if (internedKey != InvalidInternedAttribute) {
+ if (auto builtinYson = builtinAttributeProvider->FindBuiltinAttribute(internedKey)) {
+ return MakeFuture(builtinYson);
+ }
+ }
+
+ auto asyncResult = builtinAttributeProvider->GetBuiltinAttributeAsync(internedKey);
+ if (asyncResult) {
+ return asyncResult;
+ }
+ }
+
+ return std::nullopt;
+}
+
+TYsonString TSupportsAttributes::DoGetAttributeFragment(
+ TStringBuf key,
+ const TYPath& path,
+ const TYsonString& wholeYson)
+{
+ if (!wholeYson) {
+ ThrowNoSuchAttribute(key);
+ }
+ if (path.empty()) {
+ return wholeYson;
+ }
+ auto node = ConvertToNode<TYsonString>(wholeYson);
+ return SyncYPathGet(node, path, TAttributeFilter());
+}
+
+TFuture<TYsonString> TSupportsAttributes::DoGetAttribute(
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter)
+{
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+
+ NYPath::TTokenizer tokenizer(path);
+
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ TAsyncYsonWriter writer;
+
+ writer.OnBeginMap();
+
+ if (attributeFilter) {
+ WriteAttributesFragment(&writer, attributeFilter, /*stable*/false);
+ } else {
+ if (builtinAttributeProvider) {
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> builtinDescriptors;
+ builtinAttributeProvider->ListBuiltinAttributes(&builtinDescriptors);
+ for (const auto& descriptor : builtinDescriptors) {
+ if (!descriptor.Present)
+ continue;
+
+ auto key = descriptor.InternedKey.Unintern();
+ TAttributeValueConsumer attributeValueConsumer(&writer, key);
+
+ if (descriptor.Opaque) {
+ attributeValueConsumer.OnEntity();
+ continue;
+ }
+
+ if (GuardedGetBuiltinAttribute(descriptor.InternedKey, &attributeValueConsumer)) {
+ continue;
+ }
+
+ auto asyncValue = builtinAttributeProvider->GetBuiltinAttributeAsync(descriptor.InternedKey);
+ if (asyncValue) {
+ attributeValueConsumer.OnRaw(std::move(asyncValue));
+ }
+ }
+ }
+
+ auto* customAttributes = GetCustomAttributes();
+ if (customAttributes) {
+ for (const auto& [key, value] : customAttributes->ListPairs()) {
+ writer.OnKeyedItem(key);
+ Serialize(value, &writer);
+ }
+ }
+ }
+
+ writer.OnEndMap();
+
+ return writer.Finish();
+ } else {
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+
+ auto asyncYson = DoFindAttribute(key);
+ if (!asyncYson) {
+ asyncYson = NoneYsonFuture;
+ }
+
+ tokenizer.Advance();
+ return asyncYson.Apply(BIND(
+ &TSupportsAttributes::DoGetAttributeFragment,
+ key,
+ TYPath(tokenizer.GetInput())));
+ }
+}
+
+void TSupportsAttributes::GetAttribute(
+ const TYPath& path,
+ TReqGet* request,
+ TRspGet* response,
+ const TCtxGetPtr& context)
+{
+ context->SetRequestInfo();
+
+ auto attributeFilter = request->has_attributes()
+ ? FromProto<TAttributeFilter>(request->attributes())
+ : TAttributeFilter();
+
+ DoGetAttribute(path, attributeFilter).Subscribe(BIND([=] (const TErrorOr<TYsonString>& ysonOrError) {
+ if (!ysonOrError.IsOK()) {
+ context->Reply(ysonOrError);
+ return;
+ }
+
+ {
+ auto resultSize = ysonOrError.Value().AsStringBuf().Size();
+ if (auto limiter = context->GetReadRequestComplexityLimiter()) {
+ limiter->Charge(TReadRequestComplexityUsage({/*nodeCount*/ 1, resultSize}));
+ if (auto error = limiter->CheckOverdraught(); !error.IsOK()) {
+ context->Reply(error);
+ return;
+ }
+ }
+ }
+
+ response->set_value(ysonOrError.Value().ToString());
+ context->Reply();
+ }));
+}
+
+TYsonString TSupportsAttributes::DoListAttributeFragment(
+ TStringBuf key,
+ const TYPath& path,
+ const TYsonString& wholeYson)
+{
+ if (!wholeYson) {
+ ThrowNoSuchAttribute(key);
+ }
+
+ auto node = ConvertToNode(wholeYson);
+ auto listedKeys = SyncYPathList(node, path);
+
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream);
+ writer.OnBeginList();
+ for (const auto& listedKey : listedKeys) {
+ writer.OnListItem();
+ writer.OnStringScalar(listedKey);
+ }
+ writer.OnEndList();
+ writer.Flush();
+
+ return TYsonString(stream.Str());
+}
+
+TFuture<TYsonString> TSupportsAttributes::DoListAttribute(const TYPath& path)
+{
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ NYPath::TTokenizer tokenizer(path);
+
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream);
+
+ writer.OnBeginList();
+
+ auto* customAttributes = GetCustomAttributes();
+ if (customAttributes) {
+ auto userKeys = customAttributes->ListKeys();
+ for (const auto& key : userKeys) {
+ writer.OnListItem();
+ writer.OnStringScalar(key);
+ }
+ }
+
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+ if (builtinAttributeProvider) {
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> builtinDescriptors;
+ builtinAttributeProvider->ListBuiltinAttributes(&builtinDescriptors);
+ for (const auto& descriptor : builtinDescriptors) {
+ if (descriptor.Present) {
+ writer.OnListItem();
+ writer.OnStringScalar(descriptor.InternedKey.Unintern());
+ }
+ }
+ }
+
+ writer.OnEndList();
+ writer.Flush();
+
+ return MakeFuture(TYsonString(stream.Str()));
+ } else {
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+
+ auto asyncYson = DoFindAttribute(key);
+ if (!asyncYson) {
+ asyncYson = NoneYsonFuture;
+ }
+
+ tokenizer.Advance();
+ return asyncYson.Apply(BIND(
+ &TSupportsAttributes::DoListAttributeFragment,
+ key,
+ TYPath(tokenizer.GetInput())));
+ }
+}
+
+void TSupportsAttributes::ListAttribute(
+ const TYPath& path,
+ TReqList* /*request*/,
+ TRspList* response,
+ const TCtxListPtr& context)
+{
+ context->SetRequestInfo();
+
+ DoListAttribute(path).Subscribe(BIND([=] (const TErrorOr<TYsonString>& ysonOrError) {
+ if (ysonOrError.IsOK()) {
+ {
+ auto resultSize = ysonOrError.Value().AsStringBuf().Size();
+ if (auto limiter = context->GetReadRequestComplexityLimiter()) {
+ limiter->Charge(TReadRequestComplexityUsage({/*nodeCount*/ 1, resultSize}));
+ if (auto error = limiter->CheckOverdraught(); !error.IsOK()) {
+ context->Reply(error);
+ return;
+ }
+ }
+ }
+ response->set_value(ysonOrError.Value().ToString());
+ context->Reply();
+ } else {
+ context->Reply(ysonOrError);
+ }
+ }));
+}
+
+bool TSupportsAttributes::DoExistsAttributeFragment(
+ TStringBuf /*key*/,
+ const TYPath& path,
+ const TErrorOr<TYsonString>& wholeYsonOrError)
+{
+ if (!wholeYsonOrError.IsOK()) {
+ return false;
+ }
+ const auto& wholeYson = wholeYsonOrError.Value();
+ if (!wholeYson) {
+ return false;
+ }
+ auto node = ConvertToNode<TYsonString>(wholeYson);
+ try {
+ return SyncYPathExists(node, path);
+ } catch (const std::exception&) {
+ return false;
+ }
+}
+
+TFuture<bool> TSupportsAttributes::DoExistsAttribute(const TYPath& path)
+{
+ ValidatePermission(EPermissionCheckScope::This, EPermission::Read);
+
+ NYPath::TTokenizer tokenizer(path);
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ return TrueFuture;
+ }
+
+ tokenizer.Expect(NYPath::ETokenType::Literal);
+ auto key = tokenizer.GetLiteralValue();
+
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ auto* customAttributes = GetCustomAttributes();
+ if (customAttributes && customAttributes->FindYson(key)) {
+ return TrueFuture;
+ }
+
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+ if (builtinAttributeProvider) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ if (internedKey != InvalidInternedAttribute) {
+ auto optionalDescriptor = builtinAttributeProvider->FindBuiltinAttributeDescriptor(internedKey);
+ if (optionalDescriptor) {
+ const auto& descriptor = *optionalDescriptor;
+ return descriptor.Present ? TrueFuture : FalseFuture;
+ }
+ }
+ }
+
+ return FalseFuture;
+ } else {
+ auto asyncYson = DoFindAttribute(key);
+ if (!asyncYson) {
+ return FalseFuture;
+ }
+
+ return asyncYson.Apply(BIND(
+ &TSupportsAttributes::DoExistsAttributeFragment,
+ key,
+ TYPath(tokenizer.GetInput())));
+ }
+}
+
+void TSupportsAttributes::ExistsAttribute(
+ const TYPath& path,
+ TReqExists* /*request*/,
+ TRspExists* response,
+ const TCtxExistsPtr& context)
+{
+ context->SetRequestInfo();
+
+ DoExistsAttribute(path).Subscribe(BIND([=] (const TErrorOr<bool>& result) {
+ if (!result.IsOK()) {
+ context->Reply(result);
+ return;
+ }
+ bool exists = result.Value();
+ response->set_value(exists);
+ context->SetResponseInfo("Result: %v", exists);
+ context->Reply();
+ }));
+}
+
+void TSupportsAttributes::DoSetAttribute(const TYPath& path, const TYsonString& newYson)
+{
+ TCachingPermissionValidator permissionValidator(this, EPermissionCheckScope::This);
+
+ auto* customAttributes = GetCustomAttributes();
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+
+ NYPath::TTokenizer tokenizer(path);
+ switch (tokenizer.Advance()) {
+ case NYPath::ETokenType::EndOfStream: {
+ auto newAttributes = ConvertToAttributes(newYson);
+
+ std::map<TInternedAttributeKey, ISystemAttributeProvider::TAttributeDescriptor> descriptorMap;
+ if (builtinAttributeProvider) {
+ builtinAttributeProvider->ListSystemAttributes(&descriptorMap);
+ }
+
+ // Set custom attributes.
+ if (customAttributes) {
+
+ auto modifyPermission = builtinAttributeProvider
+ ? builtinAttributeProvider->GetCustomAttributeModifyPermission()
+ : EPermission::Write;
+
+ auto customKeys = customAttributes->ListKeys();
+ std::sort(customKeys.begin(), customKeys.end());
+ for (const auto& key : customKeys) {
+ if (!newAttributes->Contains(key)) {
+ permissionValidator.Validate(modifyPermission);
+
+ YT_VERIFY(customAttributes->Remove(key));
+ }
+ }
+
+ auto newPairs = newAttributes->ListPairs();
+ std::sort(newPairs.begin(), newPairs.end(), [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+ for (const auto& [key, value] : newPairs) {
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ auto it = (internedKey != InvalidInternedAttribute)
+ ? descriptorMap.find(internedKey)
+ : descriptorMap.end();
+ if (it == descriptorMap.end() || it->second.Custom) {
+ permissionValidator.Validate(modifyPermission);
+
+ customAttributes->SetYson(key, value);
+
+ YT_VERIFY(newAttributes->Remove(key));
+ }
+ }
+ }
+
+ // Set builtin attributes.
+ if (builtinAttributeProvider) {
+ for (const auto& [internedKey, descriptor] : descriptorMap) {
+ const auto& key = internedKey.Unintern();
+
+ if (descriptor.Custom) {
+ continue;
+ }
+
+ auto newAttributeYson = newAttributes->FindYson(key);
+ if (newAttributeYson) {
+ if (!descriptor.Writable) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+
+ permissionValidator.Validate(descriptor.ModifyPermission);
+
+ if (!GuardedSetBuiltinAttribute(internedKey, newAttributeYson)) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+
+ YT_VERIFY(newAttributes->Remove(key));
+ } else if (descriptor.Removable) {
+ permissionValidator.Validate(descriptor.ModifyPermission);
+
+ if (!GuardedRemoveBuiltinAttribute(internedKey)) {
+ ThrowCannotRemoveAttribute(key);
+ }
+ }
+ }
+ }
+
+ auto remainingNewKeys = newAttributes->ListKeys();
+ if (!remainingNewKeys.empty()) {
+ ThrowCannotSetBuiltinAttribute(remainingNewKeys[0]);
+ }
+
+ break;
+ }
+
+ case NYPath::ETokenType::Literal: {
+ auto key = tokenizer.GetLiteralValue();
+ ValidateAttributeKey(key);
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+
+ std::optional<ISystemAttributeProvider::TAttributeDescriptor> descriptor;
+ if (builtinAttributeProvider && internedKey != InvalidInternedAttribute) {
+ descriptor = builtinAttributeProvider->FindBuiltinAttributeDescriptor(internedKey);
+ }
+
+ if (descriptor) {
+ if (!descriptor->Writable) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+
+ permissionValidator.Validate(descriptor->ModifyPermission);
+
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ if (!GuardedSetBuiltinAttribute(internedKey, newYson)) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+ } else {
+ auto oldWholeYson = builtinAttributeProvider->FindBuiltinAttribute(internedKey);
+ if (!oldWholeYson) {
+ ThrowNoSuchBuiltinAttribute(key);
+ }
+
+ auto oldWholeNode = ConvertToNode(oldWholeYson);
+ SyncYPathSet(oldWholeNode, TYPath(tokenizer.GetInput()), newYson);
+ auto newWholeYson = ConvertToYsonString(oldWholeNode);
+
+ if (!GuardedSetBuiltinAttribute(internedKey, newWholeYson)) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+ }
+ } else {
+ if (!customAttributes) {
+ THROW_ERROR_EXCEPTION("Custom attributes are not supported");
+ }
+ auto modifyPermission = builtinAttributeProvider
+ ? builtinAttributeProvider->GetCustomAttributeModifyPermission()
+ : EPermission::Write;
+ permissionValidator.Validate(modifyPermission);
+
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ customAttributes->SetYson(key, newYson);
+ } else {
+ auto oldWholeYson = customAttributes->FindYson(key);
+ if (!oldWholeYson) {
+ ThrowNoSuchCustomAttribute(key);
+ }
+
+ auto wholeNode = ConvertToNode(oldWholeYson);
+ SyncYPathSet(wholeNode, TYPath(tokenizer.GetInput()), newYson);
+ auto newWholeYson = ConvertToYsonString(wholeNode);
+
+ customAttributes->SetYson(key, newWholeYson);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ tokenizer.ThrowUnexpected();
+ }
+}
+
+void TSupportsAttributes::SetAttribute(
+ const TYPath& path,
+ TReqSet* request,
+ TRspSet* /*response*/,
+ const TCtxSetPtr& context)
+{
+ context->SetRequestInfo();
+
+ // Request instances are pooled, and thus are request->values.
+ // Check if this pooled string has a small overhead (<= 25%).
+ // Otherwise make a deep copy.
+ const auto& requestValue = request->value();
+ const auto& safeValue = requestValue.capacity() <= requestValue.length() * 5 / 4
+ ? requestValue
+ : TString(TStringBuf(requestValue));
+ DoSetAttribute(path, TYsonString(safeValue));
+ context->Reply();
+}
+
+void TSupportsAttributes::DoRemoveAttribute(const TYPath& path, bool force)
+{
+ TCachingPermissionValidator permissionValidator(this, EPermissionCheckScope::This);
+
+ auto* customAttributes = GetCustomAttributes();
+ auto* builtinAttributeProvider = GetBuiltinAttributeProvider();
+
+ NYPath::TTokenizer tokenizer(path);
+ switch (tokenizer.Advance()) {
+ case NYPath::ETokenType::Asterisk: {
+ if (customAttributes) {
+ auto modifyPermission = builtinAttributeProvider
+ ? builtinAttributeProvider->GetCustomAttributeModifyPermission()
+ : EPermission::Write;
+ permissionValidator.Validate(modifyPermission);
+
+ auto customKeys = customAttributes->ListKeys();
+ std::sort(customKeys.begin(), customKeys.end());
+ for (const auto& key : customKeys) {
+ YT_VERIFY(customAttributes->Remove(key));
+ }
+ }
+ break;
+ }
+
+ case NYPath::ETokenType::Literal: {
+ auto key = tokenizer.GetLiteralValue();
+ auto internedKey = TInternedAttributeKey::Lookup(key);
+ auto customYson = customAttributes ? customAttributes->FindYson(key) : TYsonString();
+ if (tokenizer.Advance() == NYPath::ETokenType::EndOfStream) {
+ if (customYson) {
+ auto modifyPermission = builtinAttributeProvider
+ ? builtinAttributeProvider->GetCustomAttributeModifyPermission()
+ : EPermission::Write;
+ permissionValidator.Validate(modifyPermission);
+
+ YT_VERIFY(customAttributes->Remove(key));
+ } else {
+ if (!builtinAttributeProvider) {
+ if (force) {
+ return;
+ }
+ ThrowNoSuchCustomAttribute(key);
+ }
+
+ auto descriptor = builtinAttributeProvider->FindBuiltinAttributeDescriptor(internedKey);
+ if (!descriptor) {
+ if (force) {
+ return;
+ }
+ ThrowNoSuchAttribute(key);
+ }
+ if (!descriptor->Removable) {
+ ThrowCannotRemoveAttribute(key);
+ }
+
+ permissionValidator.Validate(descriptor->ModifyPermission);
+
+ if (!GuardedRemoveBuiltinAttribute(internedKey)) {
+ ThrowNoSuchBuiltinAttribute(key);
+ }
+ }
+ } else {
+ if (customYson) {
+ auto modifyPermission = builtinAttributeProvider
+ ? builtinAttributeProvider->GetCustomAttributeModifyPermission()
+ : EPermission::Write;
+ permissionValidator.Validate(modifyPermission);
+
+ auto customNode = ConvertToNode(customYson);
+ SyncYPathRemove(customNode, TYPath(tokenizer.GetInput()), /*recursive*/ true, force);
+ auto updatedCustomYson = ConvertToYsonString(customNode);
+
+ customAttributes->SetYson(key, updatedCustomYson);
+ } else {
+ if (!builtinAttributeProvider) {
+ if (force) {
+ return;
+ }
+ ThrowNoSuchAttribute(key);
+ }
+
+ auto descriptor = builtinAttributeProvider->FindBuiltinAttributeDescriptor(internedKey);
+ if (!descriptor) {
+ if (force) {
+ return;
+ }
+ ThrowNoSuchAttribute(key);
+ }
+
+ if (!descriptor->Writable) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+
+ permissionValidator.Validate(descriptor->ModifyPermission);
+
+ auto builtinYson = builtinAttributeProvider->FindBuiltinAttribute(internedKey);
+ if (!builtinYson) {
+ if (force) {
+ return;
+ }
+ ThrowNoSuchAttribute(key);
+ }
+
+ auto builtinNode = ConvertToNode(builtinYson);
+ SyncYPathRemove(builtinNode, TYPath(tokenizer.GetInput()));
+ auto updatedSystemYson = ConvertToYsonString(builtinNode);
+
+ if (!GuardedSetBuiltinAttribute(internedKey, updatedSystemYson)) {
+ ThrowCannotSetBuiltinAttribute(key);
+ }
+ }
+ }
+ break;
+ }
+
+ default:
+ tokenizer.ThrowUnexpected();
+ break;
+ }
+}
+
+void TSupportsAttributes::RemoveAttribute(
+ const TYPath& path,
+ TReqRemove* request,
+ TRspRemove* /*response*/,
+ const TCtxRemovePtr& context)
+{
+ context->SetRequestInfo();
+
+ bool force = request->force();
+ DoRemoveAttribute(path, force);
+ context->Reply();
+}
+
+void TSupportsAttributes::SetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* /*response*/,
+ const TCtxMultisetAttributesPtr& /*context*/)
+{
+ for (const auto& subrequest : request->subrequests()) {
+ const auto& attribute = subrequest.attribute();
+ const auto& value = subrequest.value();
+ if (attribute.empty()) {
+ THROW_ERROR_EXCEPTION("Empty attribute names are not allowed");
+ }
+
+ TYPath attributePath;
+ if (path.empty()) {
+ attributePath = attribute;
+ } else {
+ attributePath = path + "/" + attribute;
+ }
+
+ DoSetAttribute(attributePath, TYsonString(value));
+ }
+}
+
+IAttributeDictionary* TSupportsAttributes::GetCombinedAttributes()
+{
+ return CombinedAttributes_.Get();
+}
+
+IAttributeDictionary* TSupportsAttributes::GetCustomAttributes()
+{
+ return nullptr;
+}
+
+ISystemAttributeProvider* TSupportsAttributes::GetBuiltinAttributeProvider()
+{
+ return nullptr;
+}
+
+bool TSupportsAttributes::GuardedGetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer)
+{
+ auto* provider = GetBuiltinAttributeProvider();
+
+ try {
+ return provider->GetBuiltinAttribute(key, consumer);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error getting builtin attribute %Qv",
+ ToYPathLiteral(key.Unintern()))
+ << ex;
+ }
+}
+
+bool TSupportsAttributes::GuardedSetBuiltinAttribute(TInternedAttributeKey key, const TYsonString& yson)
+{
+ auto* provider = GetBuiltinAttributeProvider();
+
+ try {
+ return provider->SetBuiltinAttribute(key, yson);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error setting builtin attribute %Qv",
+ ToYPathLiteral(key.Unintern()))
+ << ex;
+ }
+}
+
+bool TSupportsAttributes::GuardedRemoveBuiltinAttribute(TInternedAttributeKey key)
+{
+ auto* provider = GetBuiltinAttributeProvider();
+
+ try {
+ return provider->RemoveBuiltinAttribute(key);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error removing builtin attribute %Qv",
+ ToYPathLiteral(key.Unintern()))
+ << ex;
+ }
+}
+
+void TSupportsAttributes::ValidateAttributeKey(TStringBuf key) const
+{
+ if (key.empty()) {
+ THROW_ERROR_EXCEPTION("Attribute key cannot be empty");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const THashSet<TInternedAttributeKey>& TSystemBuiltinAttributeKeysCache::GetBuiltinAttributeKeys(
+ ISystemAttributeProvider* provider)
+{
+ if (!Initialized_) {
+ auto guard = Guard(InitializationLock_);
+ if (Initialized_) {
+ return BuiltinKeys_;
+ }
+
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> descriptors;
+ provider->ListSystemAttributes(&descriptors);
+ BuiltinKeys_.reserve(descriptors.size());
+ for (const auto& descriptor : descriptors) {
+ if (!descriptor.Custom) {
+ YT_VERIFY(BuiltinKeys_.insert(descriptor.InternedKey).second);
+ }
+ }
+ Initialized_ = true;
+ }
+ return BuiltinKeys_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const THashSet<TString>& TSystemCustomAttributeKeysCache::GetCustomAttributeKeys(
+ ISystemAttributeProvider* provider)
+{
+ if (!Initialized_) {
+ auto guard = Guard(InitializationLock_);
+ if (Initialized_) {
+ return CustomKeys_;
+ }
+
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> descriptors;
+ provider->ListSystemAttributes(&descriptors);
+ CustomKeys_.reserve(descriptors.size());
+ for (const auto& descriptor : descriptors) {
+ if (descriptor.Custom) {
+ YT_VERIFY(CustomKeys_.insert(descriptor.InternedKey.Unintern()).second);
+ }
+ }
+ Initialized_ = true;
+ }
+ return CustomKeys_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const THashSet<TString>& TOpaqueAttributeKeysCache::GetOpaqueAttributeKeys(
+ ISystemAttributeProvider* provider)
+{
+ if (!Initialized_) {
+ auto guard = Guard(InitializationLock_);
+ if (Initialized_) {
+ return OpaqueKeys_;
+ }
+
+ std::vector<ISystemAttributeProvider::TAttributeDescriptor> descriptors;
+ provider->ListSystemAttributes(&descriptors);
+ OpaqueKeys_.reserve(descriptors.size());
+ for (const auto& descriptor : descriptors) {
+ if (descriptor.Opaque) {
+ YT_VERIFY(OpaqueKeys_.insert(descriptor.InternedKey.Unintern()).second);
+ }
+ }
+ Initialized_ = true;
+ }
+ return OpaqueKeys_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNodeSetterBase
+ : public NYson::TForwardingYsonConsumer
+{
+public:
+ void Commit();
+
+protected:
+ TNodeSetterBase(INode* node, ITreeBuilder* builder);
+ ~TNodeSetterBase();
+
+ void ThrowInvalidType(ENodeType actualType);
+ virtual ENodeType GetExpectedType() = 0;
+
+ void OnMyStringScalar(TStringBuf value) override;
+ void OnMyInt64Scalar(i64 value) override;
+ void OnMyUint64Scalar(ui64 value) override;
+ void OnMyDoubleScalar(double value) override;
+ void OnMyBooleanScalar(bool value) override;
+ void OnMyEntity() override;
+
+ void OnMyBeginList() override;
+
+ void OnMyBeginMap() override;
+
+ void OnMyBeginAttributes() override;
+ void OnMyEndAttributes() override;
+
+protected:
+ class TAttributesSetter;
+
+ INode* const Node_;
+ ITreeBuilder* const TreeBuilder_;
+
+ const std::unique_ptr<ITransactionalNodeFactory> NodeFactory_;
+
+ std::unique_ptr<TAttributesSetter> AttributesSetter_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+class TNodeSetter
+{ };
+
+#define BEGIN_SETTER(name, type) \
+ template <> \
+ class TNodeSetter<I##name##Node> \
+ : public TNodeSetterBase \
+ { \
+ public: \
+ TNodeSetter(I##name##Node* node, ITreeBuilder* builder) \
+ : TNodeSetterBase(node, builder) \
+ , Node_(node) \
+ { } \
+ \
+ private: \
+ I##name##Node* const Node_; \
+ \
+ virtual ENodeType GetExpectedType() override \
+ { \
+ return ENodeType::name; \
+ }
+
+#define END_SETTER() \
+ };
+
+BEGIN_SETTER(String, TString)
+ void OnMyStringScalar(TStringBuf value) override
+ {
+ Node_->SetValue(TString(value));
+ }
+END_SETTER()
+
+BEGIN_SETTER(Int64, i64)
+ void OnMyInt64Scalar(i64 value) override
+ {
+ Node_->SetValue(value);
+ }
+
+ void OnMyUint64Scalar(ui64 value) override
+ {
+ Node_->SetValue(CheckedIntegralCast<i64>(value));
+ }
+END_SETTER()
+
+BEGIN_SETTER(Uint64, ui64)
+ void OnMyInt64Scalar(i64 value) override
+ {
+ Node_->SetValue(CheckedIntegralCast<ui64>(value));
+ }
+
+ void OnMyUint64Scalar(ui64 value) override
+ {
+ Node_->SetValue(value);
+ }
+END_SETTER()
+
+BEGIN_SETTER(Double, double)
+ void OnMyDoubleScalar(double value) override
+ {
+ Node_->SetValue(value);
+ }
+END_SETTER()
+
+BEGIN_SETTER(Boolean, bool)
+ void OnMyBooleanScalar(bool value) override
+ {
+ Node_->SetValue(value);
+ }
+END_SETTER()
+
+#undef BEGIN_SETTER
+#undef END_SETTER
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TNodeSetter<IMapNode>
+ : public TNodeSetterBase
+{
+public:
+ TNodeSetter(IMapNode* map, ITreeBuilder* builder)
+ : TNodeSetterBase(map, builder)
+ , Map_(map)
+ { }
+
+private:
+ IMapNode* const Map_;
+
+ TString ItemKey_;
+
+
+ ENodeType GetExpectedType() override
+ {
+ return ENodeType::Map;
+ }
+
+ void OnMyBeginMap() override
+ {
+ Map_->Clear();
+ }
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ ItemKey_ = key;
+ TreeBuilder_->BeginTree();
+ Forward(TreeBuilder_, std::bind(&TNodeSetter::OnForwardingFinished, this));
+ }
+
+ void OnForwardingFinished()
+ {
+ YT_VERIFY(Map_->AddChild(ItemKey_, TreeBuilder_->EndTree()));
+ ItemKey_.clear();
+ }
+
+ void OnMyEndMap() override
+ {
+ // Just do nothing.
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TNodeSetter<IListNode>
+ : public TNodeSetterBase
+{
+public:
+ TNodeSetter(IListNode* list, ITreeBuilder* builder)
+ : TNodeSetterBase(list, builder)
+ , List_(list)
+ { }
+
+private:
+ IListNode* const List_;
+
+
+ ENodeType GetExpectedType() override
+ {
+ return ENodeType::List;
+ }
+
+ void OnMyBeginList() override
+ {
+ List_->Clear();
+ }
+
+ void OnMyListItem() override
+ {
+ TreeBuilder_->BeginTree();
+ Forward(TreeBuilder_, [this] {
+ List_->AddChild(TreeBuilder_->EndTree());
+ });
+ }
+
+ void OnMyEndList() override
+ {
+ // Just do nothing.
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <>
+class TNodeSetter<IEntityNode>
+ : public TNodeSetterBase
+{
+public:
+ TNodeSetter(IEntityNode* entity, ITreeBuilder* builder)
+ : TNodeSetterBase(entity, builder)
+ { }
+
+private:
+ ENodeType GetExpectedType() override
+ {
+ return ENodeType::Entity;
+ }
+
+ void OnMyEntity() override
+ {
+ // Just do nothing.
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TNodeSetterBase::TAttributesSetter
+ : public TForwardingYsonConsumer
+{
+public:
+ explicit TAttributesSetter(IAttributeDictionary* attributes)
+ : Attributes_(attributes)
+ { }
+
+private:
+ IAttributeDictionary* const Attributes_;
+
+ TStringStream AttributeStream_;
+ std::unique_ptr<TBufferedBinaryYsonWriter> AttributeWriter_;
+
+
+ void OnMyKeyedItem(TStringBuf key) override
+ {
+ AttributeWriter_.reset(new TBufferedBinaryYsonWriter(&AttributeStream_));
+ Forward(
+ AttributeWriter_.get(),
+ [this, key = TString(key)] {
+ AttributeWriter_->Flush();
+ AttributeWriter_.reset();
+ Attributes_->SetYson(key, TYsonString(AttributeStream_.Str()));
+ AttributeStream_.clear();
+ });
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+TNodeSetterBase::TNodeSetterBase(INode* node, ITreeBuilder* builder)
+ : Node_(node)
+ , TreeBuilder_(builder)
+ , NodeFactory_(node->CreateFactory())
+{ }
+
+TNodeSetterBase::~TNodeSetterBase() = default;
+
+void TNodeSetterBase::ThrowInvalidType(ENodeType actualType)
+{
+ THROW_ERROR_EXCEPTION("Cannot update %Qlv node with %Qlv value; types must match",
+ GetExpectedType(),
+ actualType);
+}
+
+void TNodeSetterBase::OnMyStringScalar(TStringBuf /*exists*/)
+{
+ ThrowInvalidType(ENodeType::String);
+}
+
+void TNodeSetterBase::OnMyInt64Scalar(i64 /*exists*/)
+{
+ ThrowInvalidType(ENodeType::Int64);
+}
+
+void TNodeSetterBase::OnMyUint64Scalar(ui64 /*exists*/)
+{
+ ThrowInvalidType(ENodeType::Uint64);
+}
+
+void TNodeSetterBase::OnMyDoubleScalar(double /*exists*/)
+{
+ ThrowInvalidType(ENodeType::Double);
+}
+
+void TNodeSetterBase::OnMyBooleanScalar(bool /*exists*/)
+{
+ ThrowInvalidType(ENodeType::Boolean);
+}
+
+void TNodeSetterBase::OnMyEntity()
+{
+ ThrowInvalidType(ENodeType::Entity);
+}
+
+void TNodeSetterBase::OnMyBeginList()
+{
+ ThrowInvalidType(ENodeType::List);
+}
+
+void TNodeSetterBase::OnMyBeginMap()
+{
+ ThrowInvalidType(ENodeType::Map);
+}
+
+void TNodeSetterBase::OnMyBeginAttributes()
+{
+ AttributesSetter_.reset(new TAttributesSetter(Node_->MutableAttributes()));
+ Forward(AttributesSetter_.get(), nullptr, EYsonType::MapFragment);
+}
+
+void TNodeSetterBase::OnMyEndAttributes()
+{
+ AttributesSetter_.reset();
+}
+
+void TNodeSetterBase::Commit()
+{
+ NodeFactory_->Commit();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetNodeFromProducer(
+ const INodePtr& node,
+ const NYson::TYsonProducer& producer,
+ ITreeBuilder* builder)
+{
+ YT_VERIFY(node);
+ YT_VERIFY(builder);
+
+ switch (node->GetType()) {
+ #define XX(type) \
+ case ENodeType::type: { \
+ TNodeSetter<I##type##Node> setter(node->As##type().Get(), builder); \
+ producer.Run(&setter); \
+ setter.Commit(); \
+ break; \
+ }
+
+ XX(String)
+ XX(Int64)
+ XX(Uint64)
+ XX(Double)
+ XX(Boolean)
+ XX(Map)
+ XX(List)
+ XX(Entity)
+
+ #undef XX
+
+ default:
+ YT_ABORT();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathServiceContext
+ : public TServiceContextBase
+ , public IYPathServiceContext
+{
+public:
+ template <class... TArgs>
+ TYPathServiceContext(TArgs&&... args)
+ : TServiceContextBase(std::forward<TArgs>(args)...)
+ , ReadComplexityLimiter_(New<TReadRequestComplexityLimiter>())
+ { }
+
+ void SetRequestHeader(std::unique_ptr<NRpc::NProto::TRequestHeader> header) override
+ {
+ RequestHeader_ = std::move(header);
+ RequestMessage_ = NRpc::SetRequestHeader(RequestMessage_, *RequestHeader_);
+ CachedYPathExt_ = nullptr;
+ }
+
+ TReadRequestComplexityLimiterPtr GetReadRequestComplexityLimiter() final
+ {
+ return ReadComplexityLimiter_;
+ }
+
+protected:
+ TReadRequestComplexityLimiterPtr ReadComplexityLimiter_;
+
+ std::optional<NProfiling::TWallTimer> Timer_;
+ const NProto::TYPathHeaderExt* CachedYPathExt_ = nullptr;
+
+ const NProto::TYPathHeaderExt& GetYPathExt()
+ {
+ if (!CachedYPathExt_) {
+ CachedYPathExt_ = &RequestHeader_->GetExtension(NProto::TYPathHeaderExt::ypath_header_ext);
+ }
+ return *CachedYPathExt_;
+ }
+
+ void DoReply() override
+ { }
+
+ void LogRequest() override
+ {
+ const auto& ypathExt = GetYPathExt();
+
+ TStringBuilder builder;
+ builder.AppendFormat("%v.%v %v <- ",
+ GetService(),
+ GetMethod(),
+ ypathExt.target_path());
+
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+
+ auto requestId = GetRequestId();
+ if (requestId) {
+ delimitedBuilder->AppendFormat("RequestId: %v", requestId);
+ }
+
+ delimitedBuilder->AppendFormat("Mutating: %v", ypathExt.mutating());
+
+ auto mutationId = GetMutationId();
+ if (mutationId) {
+ delimitedBuilder->AppendFormat("MutationId: %v", mutationId);
+ }
+
+ if (RequestHeader_->has_user()) {
+ delimitedBuilder->AppendFormat("User: %v", RequestHeader_->user());
+ }
+
+ delimitedBuilder->AppendFormat("Retry: %v", IsRetry());
+
+ for (const auto& info : RequestInfos_){
+ delimitedBuilder->AppendString(info);
+ }
+
+ auto logMessage = builder.Flush();
+ NTracing::AnnotateTraceContext([&] (const auto& traceContext) {
+ traceContext->AddTag(RequestInfoAnnotation, logMessage);
+ });
+ YT_LOG_DEBUG(logMessage);
+
+ Timer_.emplace();
+ }
+
+ void LogResponse() override
+ {
+ const auto& ypathExt = GetYPathExt();
+
+ TStringBuilder builder;
+ builder.AppendFormat("%v.%v %v -> ",
+ GetService(),
+ GetMethod(),
+ ypathExt.target_path());
+
+ TDelimitedStringBuilderWrapper delimitedBuilder(&builder);
+
+ auto requestId = GetRequestId();
+ if (requestId) {
+ delimitedBuilder->AppendFormat("RequestId: %v", requestId);
+ }
+
+ delimitedBuilder->AppendFormat("Mutating: %v", ypathExt.mutating());
+
+ if (RequestHeader_->has_user()) {
+ delimitedBuilder->AppendFormat("User: %v", RequestHeader_->user());
+ }
+
+ if (auto limiter = GetReadRequestComplexityLimiter()) {
+ auto usage = limiter->GetUsage();
+ delimitedBuilder->AppendFormat("ResponseNodeCount: %v, ResponseSize: %v",
+ usage.NodeCount,
+ usage.ResultSize);
+ }
+
+ for (const auto& info : ResponseInfos_) {
+ delimitedBuilder->AppendString(info);
+ }
+
+ if (Timer_) {
+ delimitedBuilder->AppendFormat("WallTime: %v", Timer_->GetElapsedTime());
+ }
+
+ delimitedBuilder->AppendFormat("Error: %v", Error_);
+
+ auto logMessage = builder.Flush();
+ NTracing::AnnotateTraceContext([&] (const auto& traceContext) {
+ traceContext->AddTag(ResponseInfoAnnotation, logMessage);
+ });
+ YT_LOG_DEBUG(logMessage);
+ }
+};
+
+IYPathServiceContextPtr CreateYPathContext(
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel)
+{
+ YT_ASSERT(requestMessage);
+
+ return New<TYPathServiceContext>(
+ std::move(requestMessage),
+ std::move(logger),
+ logLevel);
+}
+
+IYPathServiceContextPtr CreateYPathContext(
+ std::unique_ptr<TRequestHeader> requestHeader,
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger,
+ NLogging::ELogLevel logLevel)
+{
+ YT_ASSERT(requestMessage);
+
+ return New<TYPathServiceContext>(
+ std::move(requestHeader),
+ std::move(requestMessage),
+ std::move(logger),
+ logLevel);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TRootService
+ : public IYPathService
+{
+public:
+ explicit TRootService(IYPathServicePtr underlyingService)
+ : UnderlyingService_(std::move(underlyingService))
+ { }
+
+ void Invoke(const IYPathServiceContextPtr& /*context*/) override
+ {
+ YT_ABORT();
+ }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ NYPath::TTokenizer tokenizer(path);
+ if (tokenizer.Advance() != NYPath::ETokenType::Slash) {
+ THROW_ERROR_EXCEPTION("YPath must start with \"/\"");
+ }
+
+ return TResolveResultThere{UnderlyingService_, TYPath(tokenizer.GetSuffix())};
+ }
+
+ void DoWriteAttributesFragment(
+ IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable) override
+ {
+ UnderlyingService_->WriteAttributesFragment(consumer, attributeFilter, stable);
+ }
+
+ bool ShouldHideAttributes() override
+ {
+ return false;
+ }
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+
+};
+
+IYPathServicePtr CreateRootService(IYPathServicePtr underlyingService)
+{
+ return New<TRootService>(std::move(underlyingService));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_detail.h b/yt/yt/core/ytree/ypath_detail.h
new file mode 100644
index 0000000000..f89b791069
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_detail.h
@@ -0,0 +1,394 @@
+#pragma once
+
+#include "attributes.h"
+#include "permission.h"
+#include "tree_builder.h"
+#include "ypath_service.h"
+#include "system_attribute_provider.h"
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/rpc/service_detail.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/writer.h>
+#include <yt/yt/core/yson/producer.h>
+#include <yt/yt/core/yson/forwarding_consumer.h>
+
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <library/cpp/yt/assert/assert.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IYPathServiceContext
+ : public virtual NRpc::IServiceContext
+{
+ virtual void SetRequestHeader(std::unique_ptr<NRpc::NProto::TRequestHeader> header) = 0;
+ virtual TReadRequestComplexityLimiterPtr GetReadRequestComplexityLimiter() = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IYPathServiceContext)
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathServiceContextWrapper
+ : public NRpc::TServiceContextWrapper
+ , public IYPathServiceContext
+{
+public:
+ explicit TYPathServiceContextWrapper(IYPathServiceContextPtr underlyingContext);
+
+ void SetRequestHeader(std::unique_ptr<NRpc::NProto::TRequestHeader> header) override;
+ TReadRequestComplexityLimiterPtr GetReadRequestComplexityLimiter() override;
+
+ const IYPathServiceContextPtr& GetUnderlyingContext() const;
+
+private:
+ const IYPathServiceContextPtr UnderlyingContext_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TYPathServiceContextWrapper)
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DECLARE_YPATH_SERVICE_METHOD(ns, method) \
+ using TCtx##method = ::NYT::NYTree::TTypedYPathServiceContext<ns::TReq##method, ns::TRsp##method>; \
+ using TCtx##method##Ptr = ::NYT::TIntrusivePtr<TCtx##method>; \
+ using TReq##method = TCtx##method::TTypedRequest; \
+ using TRsp##method = TCtx##method::TTypedResponse; \
+ \
+ void method##Thunk( \
+ const ::NYT::NYTree::IYPathServiceContextPtr& context, \
+ const ::NYT::NRpc::THandlerInvocationOptions& options) \
+ { \
+ auto typedContext = ::NYT::New<TCtx##method>(context, options); \
+ if (typedContext->DeserializeRequest()) { \
+ this->method( \
+ &typedContext->Request(), \
+ &typedContext->Response(), \
+ typedContext); \
+ } \
+ } \
+ \
+ void method( \
+ [[maybe_unused]] TReq##method* request, \
+ [[maybe_unused]] TRsp##method* response, \
+ [[maybe_unused]] const TCtx##method##Ptr& context)
+
+#define DEFINE_YPATH_SERVICE_METHOD(type, method) \
+ void type::method( \
+ [[maybe_unused]] TReq##method* request, \
+ [[maybe_unused]] TRsp##method* response, \
+ [[maybe_unused]] const TCtx##method##Ptr& context)
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DISPATCH_YPATH_SERVICE_METHOD(method, ...) \
+ if (context->GetMethod() == #method) { \
+ auto options = ::NYT::NRpc::THandlerInvocationOptions() __VA_ARGS__; \
+ method##Thunk(context, options); \
+ return true; \
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathServiceBase
+ : public virtual IYPathService
+{
+public:
+ void Invoke(const IYPathServiceContextPtr& context) override;
+ TResolveResult Resolve(const TYPath& path, const IYPathServiceContextPtr& context) override;
+ void DoWriteAttributesFragment(
+ NYson::IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable) override;
+ bool ShouldHideAttributes() override;
+
+protected:
+ virtual void BeforeInvoke(const IYPathServiceContextPtr& context);
+ virtual bool DoInvoke(const IYPathServiceContextPtr& context);
+ virtual void AfterInvoke(const IYPathServiceContextPtr& context);
+
+ virtual TResolveResult ResolveSelf(const TYPath& path, const IYPathServiceContextPtr& context);
+ virtual TResolveResult ResolveAttributes(const TYPath& path, const IYPathServiceContextPtr& context);
+ virtual TResolveResult ResolveRecursive(const TYPath& path, const IYPathServiceContextPtr& context);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define DECLARE_SUPPORTS_METHOD(method, base) \
+ class TSupports##method \
+ : public base \
+ { \
+ protected: \
+ DECLARE_YPATH_SERVICE_METHOD(NProto, method); \
+ virtual void method##Self(TReq##method* request, TRsp##method* response, const TCtx##method##Ptr& context); \
+ virtual void method##Recursive(const TYPath& path, TReq##method* request, TRsp##method* response, const TCtx##method##Ptr& context); \
+ virtual void method##Attribute(const TYPath& path, TReq##method* request, TRsp##method* response, const TCtx##method##Ptr& context); \
+ }
+
+class TSupportsExistsBase
+ : public virtual TRefCounted
+{
+protected:
+ template <class TContextPtr>
+ void Reply(const TContextPtr& context, bool exists);
+};
+
+class TSupportsMultisetAttributes
+ : public virtual TRefCounted
+{
+protected:
+ DECLARE_YPATH_SERVICE_METHOD(NProto, Multiset);
+ DECLARE_YPATH_SERVICE_METHOD(NProto, MultisetAttributes);
+
+ virtual void SetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* response,
+ const TCtxMultisetAttributesPtr& context);
+
+private:
+ // COMPAT(gritukan) Move it to MultisetAttributes.
+ void DoSetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* response,
+ const TCtxMultisetAttributesPtr& context);
+};
+
+DECLARE_SUPPORTS_METHOD(GetKey, virtual TRefCounted);
+DECLARE_SUPPORTS_METHOD(Get, virtual TRefCounted);
+DECLARE_SUPPORTS_METHOD(Set, virtual TRefCounted);
+DECLARE_SUPPORTS_METHOD(List, virtual TRefCounted);
+DECLARE_SUPPORTS_METHOD(Remove, virtual TRefCounted);
+DECLARE_SUPPORTS_METHOD(Exists, TSupportsExistsBase);
+
+#undef DECLARE_SUPPORTS_METHOD
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSupportsPermissions
+{
+protected:
+ virtual ~TSupportsPermissions() = default;
+
+ // The last argument will be empty for contexts where authenticated user is known
+ // a-priori (like in object proxies in master), otherwise it will be set to user name
+ // (like in operation controller orchid).
+ virtual void ValidatePermission(
+ EPermissionCheckScope scope,
+ EPermission permission,
+ const TString& user = {});
+
+ class TCachingPermissionValidator
+ {
+ public:
+ TCachingPermissionValidator(
+ TSupportsPermissions* owner,
+ EPermissionCheckScope scope);
+
+ void Validate(EPermission permission, const TString& user = {});
+
+ private:
+ TSupportsPermissions* const Owner_;
+ const EPermissionCheckScope Scope_;
+
+ THashMap<TString, EPermissionSet> ValidatedPermissions_;
+ };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSupportsAttributes
+ : public virtual TYPathServiceBase
+ , public virtual TSupportsGet
+ , public virtual TSupportsList
+ , public virtual TSupportsSet
+ , public virtual TSupportsMultisetAttributes
+ , public virtual TSupportsRemove
+ , public virtual TSupportsExists
+ , public virtual TSupportsPermissions
+{
+protected:
+ TSupportsAttributes();
+
+ IAttributeDictionary* GetCombinedAttributes();
+
+ //! Can be |nullptr|.
+ virtual IAttributeDictionary* GetCustomAttributes();
+
+ //! Can be |nullptr|.
+ virtual ISystemAttributeProvider* GetBuiltinAttributeProvider();
+
+ TResolveResult ResolveAttributes(
+ const NYPath::TYPath& path,
+ const IYPathServiceContextPtr& context) override;
+
+ void GetAttribute(
+ const TYPath& path,
+ TReqGet* request,
+ TRspGet* response,
+ const TCtxGetPtr& context) override;
+
+ void ListAttribute(
+ const TYPath& path,
+ TReqList* request,
+ TRspList* response,
+ const TCtxListPtr& context) override;
+
+ void ExistsAttribute(
+ const TYPath& path,
+ TReqExists* request,
+ TRspExists* response,
+ const TCtxExistsPtr& context) override;
+
+ void SetAttribute(
+ const TYPath& path,
+ TReqSet* request,
+ TRspSet* response,
+ const TCtxSetPtr& context) override;
+
+ void RemoveAttribute(
+ const TYPath& path,
+ TReqRemove* request,
+ TRspRemove* response,
+ const TCtxRemovePtr& context) override;
+
+ void SetAttributes(
+ const TYPath& path,
+ TReqMultisetAttributes* request,
+ TRspMultisetAttributes* response,
+ const TCtxMultisetAttributesPtr& context) override;
+
+private:
+ class TCombinedAttributeDictionary
+ : public IAttributeDictionary
+ {
+ public:
+ explicit TCombinedAttributeDictionary(TSupportsAttributes* owner);
+
+ std::vector<TString> ListKeys() const override;
+ std::vector<TKeyValuePair> ListPairs() const override;
+ NYson::TYsonString FindYson(TStringBuf key) const override;
+ void SetYson(const TString& key, const NYson::TYsonString& value) override;
+ bool Remove(const TString& key) override;
+
+ private:
+ TSupportsAttributes* const Owner_;
+ };
+
+ using TCombinedAttributeDictionaryPtr = TIntrusivePtr<TCombinedAttributeDictionary>;
+
+ TCombinedAttributeDictionaryPtr CombinedAttributes_;
+
+ TFuture<NYson::TYsonString> DoFindAttribute(TStringBuf key);
+
+ static NYson::TYsonString DoGetAttributeFragment(
+ TStringBuf key,
+ const TYPath& path,
+ const NYson::TYsonString& wholeYson);
+ TFuture<NYson::TYsonString> DoGetAttribute(
+ const TYPath& path,
+ const TAttributeFilter& attributeFilter);
+
+ static bool DoExistsAttributeFragment(
+ TStringBuf key,
+ const TYPath& path,
+ const TErrorOr<NYson::TYsonString>& wholeYsonOrError);
+ TFuture<bool> DoExistsAttribute(const TYPath& path);
+
+ static NYson::TYsonString DoListAttributeFragment(
+ TStringBuf key,
+ const TYPath& path,
+ const NYson::TYsonString& wholeYson);
+ TFuture<NYson::TYsonString> DoListAttribute(const TYPath& path);
+
+ void DoSetAttribute(const TYPath& path, const NYson::TYsonString& newYson);
+ void DoRemoveAttribute(const TYPath& path, bool force);
+
+ bool GuardedGetBuiltinAttribute(TInternedAttributeKey key, NYson::IYsonConsumer* consumer);
+ bool GuardedSetBuiltinAttribute(TInternedAttributeKey key, const NYson::TYsonString& value);
+ bool GuardedRemoveBuiltinAttribute(TInternedAttributeKey key);
+
+ void ValidateAttributeKey(TStringBuf key) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSystemBuiltinAttributeKeysCache
+{
+public:
+ const THashSet<TInternedAttributeKey>& GetBuiltinAttributeKeys(ISystemAttributeProvider* provider);
+
+private:
+ std::atomic<bool> Initialized_ = false;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, InitializationLock_);
+
+ THashSet<TInternedAttributeKey> BuiltinKeys_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TSystemCustomAttributeKeysCache
+{
+public:
+ const THashSet<TString>& GetCustomAttributeKeys(ISystemAttributeProvider* provider);
+
+private:
+ std::atomic<bool> Initialized_ = false;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, InitializationLock_);
+
+ THashSet<TString> CustomKeys_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TOpaqueAttributeKeysCache
+{
+public:
+ const THashSet<TString>& GetOpaqueAttributeKeys(ISystemAttributeProvider* provider);
+
+private:
+ std::atomic<bool> Initialized_ = false;
+ YT_DECLARE_SPIN_LOCK(NThreading::TSpinLock, InitializationLock_);
+
+ THashSet<TString> OpaqueKeys_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+void SetNodeFromProducer(
+ const INodePtr& node,
+ const NYson::TYsonProducer& producer,
+ ITreeBuilder* builder);
+
+////////////////////////////////////////////////////////////////////////////////
+
+IYPathServiceContextPtr CreateYPathContext(
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger = NLogging::TLogger(),
+ NLogging::ELogLevel logLevel = NLogging::ELogLevel::Debug);
+
+IYPathServiceContextPtr CreateYPathContext(
+ std::unique_ptr<NRpc::NProto::TRequestHeader> requestHeader,
+ TSharedRefArray requestMessage,
+ NLogging::TLogger logger = NLogging::TLogger(),
+ NLogging::ELogLevel logLevel = NLogging::ELogLevel::Debug);
+
+IYPathServicePtr CreateRootService(IYPathServicePtr underlyingService);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+
+#define YPATH_DETAIL_INL_H_
+#include "ypath_detail-inl.h"
+#undef YPATH_DETAIL_INL_H_
diff --git a/yt/yt/core/ytree/ypath_proxy.h b/yt/yt/core/ytree/ypath_proxy.h
new file mode 100644
index 0000000000..992a5eb9a6
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_proxy.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "ypath_client.h"
+
+#include <yt/yt_proto/yt/core/ytree/proto/ypath.pb.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TYPathProxy
+{
+ DEFINE_YPATH_PROXY(Node);
+
+ DEFINE_YPATH_PROXY_METHOD(NProto, GetKey);
+ DEFINE_YPATH_PROXY_METHOD(NProto, Get);
+ DEFINE_YPATH_PROXY_METHOD(NProto, List);
+ DEFINE_YPATH_PROXY_METHOD(NProto, Exists);
+ DEFINE_MUTATING_YPATH_PROXY_METHOD(NProto, Set);
+ DEFINE_MUTATING_YPATH_PROXY_METHOD(NProto, MultisetAttributes);
+ DEFINE_MUTATING_YPATH_PROXY_METHOD(NProto, Remove);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_resolver.cpp b/yt/yt/core/ytree/ypath_resolver.cpp
new file mode 100644
index 0000000000..1b24afefb8
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_resolver.cpp
@@ -0,0 +1,370 @@
+#include "ypath_resolver.h"
+
+#include <yt/yt/core/ypath/tokenizer.h>
+
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/misc/error.h>
+
+#include <library/cpp/yt/misc/cast.h>
+
+#include <util/stream/mem.h>
+
+namespace NYT::NYTree {
+
+using NYPath::ETokenType;
+using NYPath::TTokenizer;
+
+using NYson::EYsonType;
+using NYson::EYsonItemType;
+using NYson::TYsonPullParser;
+using NYson::TYsonPullParserCursor;
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EExpectedItem,
+ (BeginMapOrList)
+ (BeginAttribute)
+ (Value)
+);
+
+using TResult = std::variant<bool, i64, ui64, double, TString>;
+
+bool ParseListUntilIndex(TYsonPullParserCursor* cursor, int targetIndex)
+{
+ YT_VERIFY((*cursor)->GetType() == EYsonItemType::BeginList);
+ cursor->Next();
+ int index = 0;
+ while ((*cursor)->GetType() != EYsonItemType::EndList) {
+ if (index == targetIndex) {
+ return true;
+ }
+ ++index;
+ cursor->SkipComplexValue();
+ }
+ return false;
+}
+
+bool ParseMapOrAttributesUntilKey(TYsonPullParserCursor* cursor, TStringBuf key)
+{
+ auto endType = EYsonItemType::EndMap;
+ if ((*cursor)->GetType() != EYsonItemType::BeginMap) {
+ YT_VERIFY((*cursor)->GetType() == EYsonItemType::BeginAttributes);
+ endType = EYsonItemType::EndAttributes;
+ }
+ cursor->Next();
+ while ((*cursor)->GetType() != endType) {
+ YT_VERIFY((*cursor)->GetType() == EYsonItemType::StringValue);
+ if ((*cursor)->UncheckedAsString() == key) {
+ cursor->Next();
+ return true;
+ }
+ cursor->Next();
+ cursor->SkipComplexValue();
+ }
+ return false;
+}
+
+std::optional<TResult> TryParseValue(TYsonPullParserCursor* cursor)
+{
+ switch ((*cursor)->GetType()) {
+ case EYsonItemType::BooleanValue:
+ return (*cursor)->UncheckedAsBoolean();
+ case EYsonItemType::Int64Value:
+ return (*cursor)->UncheckedAsInt64();
+ case EYsonItemType::Uint64Value:
+ return (*cursor)->UncheckedAsUint64();
+ case EYsonItemType::DoubleValue:
+ return (*cursor)->UncheckedAsDouble();
+ case EYsonItemType::StringValue:
+ return TString((*cursor)->UncheckedAsString());
+ default:
+ return std::nullopt;
+ }
+}
+
+TResult ParseValue(TYsonPullParserCursor* cursor)
+{
+ auto result = TryParseValue(cursor);
+ YT_VERIFY(result);
+ return std::move(*result);
+}
+
+TString ParseAnyValue(TYsonPullParserCursor* cursor)
+{
+ TStringStream stream;
+ {
+ NYson::TCheckedInDebugYsonTokenWriter writer(&stream);
+ cursor->TransferComplexValue(&writer);
+ writer.Flush();
+ }
+ return std::move(stream.Str());
+}
+
+[[noreturn]] void ThrowUnexpectedToken(const TTokenizer& tokenizer)
+{
+ THROW_ERROR_EXCEPTION(
+ "Unexpected YPath token %Qv while parsing %Qv",
+ tokenizer.GetToken(),
+ tokenizer.GetInput());
+}
+
+std::pair<EExpectedItem, std::optional<TString>> NextToken(TTokenizer* tokenizer)
+{
+ switch (tokenizer->Advance()) {
+ case ETokenType::EndOfStream:
+ return {EExpectedItem::Value, std::nullopt};
+
+ case ETokenType::Slash: {
+ auto type = tokenizer->Advance();
+ auto expected = EExpectedItem::BeginMapOrList;
+ if (type == ETokenType::At) {
+ type = tokenizer->Advance();
+ expected = EExpectedItem::BeginAttribute;
+ }
+ if (type != ETokenType::Literal) {
+ ThrowUnexpectedToken(*tokenizer);
+ }
+ return {expected, tokenizer->GetLiteralValue()};
+ }
+
+ default:
+ ThrowUnexpectedToken(*tokenizer);
+ }
+}
+
+std::optional<TResult> TryParseImpl(TStringBuf yson, const TYPath& path, bool isAny)
+{
+ TTokenizer tokenizer(path);
+ TMemoryInput input(yson);
+ TYsonPullParser parser(&input, EYsonType::Node);
+ TYsonPullParserCursor cursor(&parser);
+
+ while (true) {
+ auto [expected, literal] = NextToken(&tokenizer);
+ if (expected == EExpectedItem::Value && isAny) {
+ return {ParseAnyValue(&cursor)};
+ }
+ if (cursor->GetType() == EYsonItemType::BeginAttributes && expected != EExpectedItem::BeginAttribute) {
+ cursor.SkipAttributes();
+ }
+ switch (cursor->GetType()) {
+ case EYsonItemType::BeginAttributes: {
+ if (expected != EExpectedItem::BeginAttribute) {
+ return std::nullopt;
+ }
+ Y_VERIFY(literal.has_value());
+ if (!ParseMapOrAttributesUntilKey(&cursor, *literal)) {
+ return std::nullopt;
+ }
+ break;
+ }
+ case EYsonItemType::BeginMap: {
+ if (expected != EExpectedItem::BeginMapOrList) {
+ return std::nullopt;
+ }
+ Y_VERIFY(literal.has_value());
+ if (!ParseMapOrAttributesUntilKey(&cursor, *literal)) {
+ return std::nullopt;
+ }
+ break;
+ }
+ case EYsonItemType::BeginList: {
+ if (expected != EExpectedItem::BeginMapOrList) {
+ return std::nullopt;
+ }
+ Y_VERIFY(literal.has_value());
+ int index;
+ if (!TryFromString(*literal, index)) {
+ return std::nullopt;
+ }
+ if (!ParseListUntilIndex(&cursor, index)) {
+ return std::nullopt;
+ }
+ break;
+ }
+ case EYsonItemType::EntityValue:
+ return std::nullopt;
+
+ case EYsonItemType::BooleanValue:
+ case EYsonItemType::Int64Value:
+ case EYsonItemType::Uint64Value:
+ case EYsonItemType::DoubleValue:
+ case EYsonItemType::StringValue:
+ if (expected != EExpectedItem::Value) {
+ return std::nullopt;
+ }
+ return ParseValue(&cursor);
+ case EYsonItemType::EndOfStream:
+ case EYsonItemType::EndMap:
+ case EYsonItemType::EndAttributes:
+ case EYsonItemType::EndList:
+ YT_ABORT();
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+struct TScalarTypeTraits
+{ };
+
+template <>
+struct TScalarTypeTraits<TString>
+{
+ static std::optional<TString> TryCast(const NDetail::TResult& result)
+ {
+ if (const auto* value = std::get_if<TString>(&result)) {
+ return *value;
+ }
+ return std::nullopt;
+ }
+};
+
+template <>
+struct TScalarTypeTraits<bool>
+{
+ static std::optional<bool> TryCast(const NDetail::TResult& result)
+ {
+ if (const auto* value = std::get_if<bool>(&result)) {
+ return *value;
+ }
+ return std::nullopt;
+ }
+};
+
+template <>
+struct TScalarTypeTraits<i64>
+{
+ static std::optional<i64> TryCast(const NDetail::TResult& result)
+ {
+ if (const auto* value = std::get_if<i64>(&result)) {
+ return *value;
+ } else if (const auto* value = std::get_if<ui64>(&result)) {
+ i64 typedResult;
+ if (TryIntegralCast(*value, &typedResult)) {
+ return typedResult;
+ }
+ }
+ return std::nullopt;
+ }
+};
+
+template <>
+struct TScalarTypeTraits<ui64>
+{
+ static std::optional<ui64> TryCast(const NDetail::TResult& result)
+ {
+ if (const auto* value = std::get_if<ui64>(&result)) {
+ return *value;
+ } else if (const auto* value = std::get_if<i64>(&result)) {
+ ui64 typedResult;
+ if (TryIntegralCast(*value, &typedResult)) {
+ return typedResult;
+ }
+ }
+ return std::nullopt;
+ }
+};
+
+template <>
+struct TScalarTypeTraits<double>
+{
+ static std::optional<double> TryCast(const NDetail::TResult& result)
+ {
+ if (const auto* value = std::get_if<double>(&result)) {
+ return *value;
+ } else if (const auto* value = std::get_if<i64>(&result)) {
+ return static_cast<double>(*value);
+ } else if (const auto* value = std::get_if<ui64>(&result)) {
+ return static_cast<double>(*value);
+ }
+ return std::nullopt;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+std::optional<T> TryGetValueImpl(TStringBuf yson, const TYPath& ypath, bool isAny = false)
+{
+ auto result = NDetail::TryParseImpl(yson, ypath, isAny);
+ if (!result.has_value()) {
+ return std::nullopt;
+ }
+ return TScalarTypeTraits<T>::TryCast(*result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T>
+std::optional<T> TryGetValue(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<T>(yson, ypath, /*isAny*/ false);
+}
+
+template std::optional<i64> TryGetValue<i64>(TStringBuf yson, const TYPath& ypath);
+template std::optional<ui64> TryGetValue<ui64>(TStringBuf yson, const TYPath& ypath);
+template std::optional<bool> TryGetValue<bool>(TStringBuf yson, const TYPath& ypath);
+template std::optional<double> TryGetValue<double>(TStringBuf yson, const TYPath& ypath);
+template std::optional<TString> TryGetValue<TString>(TStringBuf yson, const TYPath& ypath);
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<i64> TryGetInt64(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<i64>(yson, ypath);
+}
+
+std::optional<ui64> TryGetUint64(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<ui64>(yson, ypath);
+}
+
+std::optional<bool> TryGetBoolean(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<bool>(yson, ypath);
+}
+
+std::optional<double> TryGetDouble(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<double>(yson, ypath);
+}
+
+std::optional<TString> TryGetString(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<TString>(yson, ypath);
+}
+
+std::optional<TString> TryGetAny(TStringBuf yson, const TYPath& ypath)
+{
+ return TryGetValueImpl<TString>(yson, ypath, /*isAny*/ true);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+std::optional<T> TryParseValue(TYsonPullParserCursor* cursor)
+{
+ auto result = NDetail::TryParseValue(cursor);
+ return result ? TScalarTypeTraits<T>::TryCast(std::move(*result)) : std::nullopt;
+}
+
+template std::optional<i64> TryParseValue<i64>(TYsonPullParserCursor* cursor);
+template std::optional<ui64> TryParseValue<ui64>(TYsonPullParserCursor* cursor);
+template std::optional<bool> TryParseValue<bool>(TYsonPullParserCursor* cursor);
+template std::optional<double> TryParseValue<double>(TYsonPullParserCursor* cursor);
+template std::optional<TString> TryParseValue<TString>(TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_resolver.h b/yt/yt/core/ytree/ypath_resolver.h
new file mode 100644
index 0000000000..1ad7de315c
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_resolver.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <yt/yt/core/ypath/public.h>
+
+#include <yt/yt/core/yson/pull_parser.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+std::optional<T> TryGetValue(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<i64> TryGetInt64(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<ui64> TryGetUint64(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<bool> TryGetBoolean(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<double> TryGetDouble(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<TString> TryGetString(TStringBuf yson, const NYPath::TYPath& ypath);
+std::optional<TString> TryGetAny(TStringBuf yson, const NYPath::TYPath& ypath);
+
+template <class T>
+std::optional<T> TryParseValue(NYson::TYsonPullParserCursor* cursor);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_service.cpp b/yt/yt/core/ytree/ypath_service.cpp
new file mode 100644
index 0000000000..1853b40a40
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_service.cpp
@@ -0,0 +1,972 @@
+#include "ypath_service.h"
+#include "convert.h"
+#include "ephemeral_node_factory.h"
+#include "tree_builder.h"
+#include "ypath_client.h"
+#include "ypath_detail.h"
+#include "ypath_proxy.h"
+#include "fluent.h"
+
+#include <yt/yt/core/profiling/timing.h>
+
+#include <yt/yt/core/rpc/dispatcher.h>
+
+#include <yt/yt/core/yson/async_consumer.h>
+#include <yt/yt/core/yson/attribute_consumer.h>
+#include <yt/yt/core/yson/null_consumer.h>
+#include <yt/yt/core/yson/ypath_designated_consumer.h>
+#include <yt/yt/core/yson/writer.h>
+
+#include <yt/yt/core/concurrency/scheduler.h>
+#include <yt/yt/core/concurrency/periodic_executor.h>
+
+#include <yt/yt/core/misc/checksum.h>
+#include <yt/yt/core/misc/atomic_object.h>
+
+#include <library/cpp/yt/memory/atomic_intrusive_ptr.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TCacheKey
+{
+ TYPath Path;
+ TString Method;
+ TSharedRef RequestBody;
+ TChecksum RequestBodyHash;
+
+ TCacheKey(
+ const TYPath& path,
+ const TString& method,
+ const TSharedRef& requestBody)
+ : Path(path)
+ , Method(method)
+ , RequestBody(requestBody)
+ , RequestBodyHash(GetChecksum(RequestBody))
+ { }
+
+ bool operator == (const TCacheKey& other) const
+ {
+ return
+ Path == other.Path &&
+ Method == other.Method &&
+ RequestBodyHash == other.RequestBodyHash &&
+ TRef::AreBitwiseEqual(RequestBody, other.RequestBody);
+ }
+
+ friend TString ToString(const TCacheKey& key)
+ {
+ return Format("{%v %v %x}",
+ key.Method,
+ key.Path,
+ key.RequestBodyHash);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+template <>
+struct THash<NYT::NYTree::TCacheKey>
+{
+ size_t operator()(const NYT::NYTree::TCacheKey& key) const
+ {
+ size_t result = 0;
+ NYT::HashCombine(result, key.Path);
+ NYT::HashCombine(result, key.Method);
+ NYT::HashCombine(result, key.RequestBodyHash);
+ return result;
+ }
+};
+
+namespace NYT::NYTree {
+
+using namespace NYson;
+using namespace NRpc;
+using namespace NConcurrency;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFromProducerYPathService
+ : public TYPathServiceBase
+ , public TSupportsGet
+ , public ICachedYPathService
+{
+public:
+ TFromProducerYPathService(TYsonProducer producer, TDuration cachePeriod)
+ : Producer_(std::move(producer))
+ , CachePeriod_(cachePeriod)
+ { }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override
+ {
+ // Try to handle root get requests without constructing ephemeral YTree.
+ if (path.empty() && context->GetMethod() == "Get") {
+ return TResolveResultHere{path};
+ } else {
+ return TResolveResultThere{BuildNodeFromProducer(), path};
+ }
+ }
+
+ void SetCachePeriod(TDuration period) override
+ {
+ CachePeriod_ = period;
+ }
+
+private:
+ const TYsonProducer Producer_;
+
+ TYsonString CachedString_;
+ INodePtr CachedNode_;
+ TDuration CachePeriod_;
+ TInstant LastStringUpdateTime_;
+ TInstant LastNodeUpdateTime_;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ return TYPathServiceBase::DoInvoke(context);
+ }
+
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ if (request->has_attributes()) {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ return;
+ }
+
+ context->SetRequestInfo();
+ auto yson = BuildStringFromProducer();
+ response->set_value(yson.ToString());
+ context->Reply();
+ }
+
+ void GetRecursive(const TYPath& /*path*/, TReqGet* /*request*/, TRspGet* /*response*/, const TCtxGetPtr& /*context*/) override
+ {
+ YT_ABORT();
+ }
+
+ void GetAttribute(const TYPath& /*path*/, TReqGet* /*request*/, TRspGet* /*response*/, const TCtxGetPtr& /*context*/) override
+ {
+ YT_ABORT();
+ }
+
+
+ TYsonString BuildStringFromProducer()
+ {
+ if (CachePeriod_ != TDuration()) {
+ auto now = NProfiling::GetInstant();
+ if (LastStringUpdateTime_ + CachePeriod_ > now) {
+ return CachedString_;
+ }
+ }
+
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream);
+ Producer_.Run(&writer);
+ writer.Flush();
+
+ const auto& str = stream.Str();
+ if (str.empty()) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable, "No data is available");
+ }
+
+ auto result = TYsonString(str);
+
+ if (CachePeriod_ != TDuration()) {
+ CachedString_ = result;
+ LastStringUpdateTime_ = NProfiling::GetInstant();
+ }
+
+ return result;
+ }
+
+ INodePtr BuildNodeFromProducer()
+ {
+ if (CachePeriod_ != TDuration()) {
+ auto now = NProfiling::GetInstant();
+ if (LastNodeUpdateTime_ + CachePeriod_ > now) {
+ return CachedNode_;
+ }
+ }
+
+ auto result = ConvertTo<INodePtr>(BuildStringFromProducer());
+
+ if (CachePeriod_ != TDuration()) {
+ CachedNode_ = result;
+ LastNodeUpdateTime_ = NProfiling::GetInstant();
+ }
+
+ return result;
+ }
+};
+
+IYPathServicePtr IYPathService::FromProducer(TYsonProducer producer, TDuration cachePeriod)
+{
+ return New<TFromProducerYPathService>(std::move(producer), cachePeriod);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TFromExtendedProducerYPathService
+ : public TYPathServiceBase
+ , public TSupportsGet
+{
+ using TUnderlyingProducer = TExtendedYsonProducer<const IAttributeDictionaryPtr&>;
+public:
+ explicit TFromExtendedProducerYPathService(TUnderlyingProducer producer)
+ : Producer_(std::move(producer))
+ { }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& context) override
+ {
+ // Try to handle root get requests without constructing ephemeral YTree.
+ if (path.empty() && context->GetMethod() == "Get") {
+ return TResolveResultHere{path};
+ } else if (context->GetMethod() == "Get") {
+ auto typedContext = New<TCtxGet>(context, NRpc::THandlerInvocationOptions{});
+ if (!typedContext->DeserializeRequest()) {
+ THROW_ERROR_EXCEPTION("Error deserializing request");
+ }
+
+ const auto& request = typedContext->Request();
+ IAttributeDictionaryPtr options;
+ if (request.has_options()) {
+ options = NYTree::FromProto(request.options());
+ }
+
+ return TResolveResultThere{BuildNodeFromProducer(options), path};
+ } else {
+ return TResolveResultThere{BuildNodeFromProducer(/*options*/ CreateEphemeralAttributes()), path};
+ }
+ }
+
+private:
+ TUnderlyingProducer Producer_;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ return TYPathServiceBase::DoInvoke(context);
+ }
+
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ IAttributeDictionaryPtr options;
+ if (request->has_options()) {
+ options = NYTree::FromProto(request->options());
+ }
+
+ if (request->has_attributes()) {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer(options);
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ return;
+ }
+
+ context->SetRequestInfo();
+ auto yson = BuildStringFromProducer(options);
+ response->set_value(yson.ToString());
+ context->Reply();
+ }
+
+ void GetRecursive(const TYPath& /*path*/, TReqGet* /*request*/, TRspGet* /*response*/, const TCtxGetPtr& /*context*/) override
+ {
+ YT_ABORT();
+ }
+
+ void GetAttribute(const TYPath& /*path*/, TReqGet* /*request*/, TRspGet* /*response*/, const TCtxGetPtr& /*context*/) override
+ {
+ YT_ABORT();
+ }
+
+
+ TYsonString BuildStringFromProducer(const IAttributeDictionaryPtr& options)
+ {
+ TStringStream stream;
+ {
+ TBufferedBinaryYsonWriter writer(&stream);
+ Producer_.Run(&writer, options);
+ writer.Flush();
+ }
+
+ auto str = stream.Str();
+ if (str.empty()) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable, "No data is available");
+ }
+
+ return TYsonString(std::move(str));
+ }
+
+ INodePtr BuildNodeFromProducer(const IAttributeDictionaryPtr& options)
+ {
+ return ConvertTo<INodePtr>(BuildStringFromProducer(options));
+ }
+};
+
+IYPathServicePtr IYPathService::FromProducer(
+ NYson::TExtendedYsonProducer<const IAttributeDictionaryPtr&> producer)
+{
+ return New<TFromExtendedProducerYPathService>(std::move(producer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TLazyYPathServiceFromProducer
+ : public TYPathServiceBase
+ , public TSupportsGet
+ , public TSupportsExists
+ , public TSupportsList
+{
+public:
+ explicit TLazyYPathServiceFromProducer(TYsonProducer producer)
+ : Producer_(std::move(producer))
+ { }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ return TResolveResultHere{path};
+ }
+
+private:
+ const TYsonProducer Producer_;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ DISPATCH_YPATH_SERVICE_METHOD(Get);
+ DISPATCH_YPATH_SERVICE_METHOD(Exists);
+ DISPATCH_YPATH_SERVICE_METHOD(List);
+
+ return TYPathServiceBase::DoInvoke(context);
+ }
+
+ void GetSelf(TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ if (request->has_attributes()) {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ return;
+ }
+
+ IAttributeDictionaryPtr options;
+ if (request->has_options()) {
+ options = NYTree::FromProto(request->options());
+ }
+
+ context->SetRequestInfo();
+ auto yson = BuildStringFromProducer();
+ response->set_value(yson.ToString());
+ context->Reply();
+ }
+
+ void GetRecursive(const TYPath& path, TReqGet* request, TRspGet* response, const TCtxGetPtr& context) override
+ {
+ if (request->has_attributes()) {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ return;
+ }
+
+ context->SetRequestInfo();
+
+ TStringStream stream;
+ {
+ TBufferedBinaryYsonWriter writer(&stream);
+ auto consumer = CreateYPathDesignatedConsumer(path, EMissingPathMode::ThrowError, &writer);
+ Producer_.Run(consumer.get());
+ writer.Flush();
+ }
+
+ auto str = stream.Str();
+ if (str.empty()) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable, "No data is available");
+ }
+
+ response->set_value(std::move(str));
+ context->Reply();
+ }
+
+ void GetAttribute(const TYPath& /*path*/, TReqGet* /*request*/, TRspGet* /*response*/, const TCtxGetPtr& context) override
+ {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ }
+
+ void ListSelf(TReqList* /*request*/, TRspList* /*response*/, const TCtxListPtr& context) override
+ {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ }
+
+ void ListRecursive(const TYPath& path, TReqList* request, TRspList* response, const TCtxListPtr& context) override
+ {
+ context->SetRequestInfo();
+
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ auto consumer = CreateYPathDesignatedConsumer(path, EMissingPathMode::ThrowError, builder.get());
+ Producer_.Run(consumer.get());
+ auto node = builder->EndTree();
+
+ auto innerRequest = TYPathProxy::List("");
+ if (request->has_limit()) {
+ innerRequest->set_limit(request->limit());
+ }
+
+ ExecuteVerb(node, innerRequest)
+ .Subscribe(BIND([=] (const TErrorOr<TYPathProxy::TRspListPtr> resultOrError) {
+ if (resultOrError.IsOK()) {
+ response->set_value(resultOrError.Value()->value());
+ context->Reply();
+ } else {
+ context->Reply(resultOrError);
+ }
+ }));
+ }
+
+ void ListAttribute(const TYPath& /*path*/, TReqList* /*request*/, TRspList* /*response*/, const TCtxListPtr& context) override
+ {
+ // Execute fallback.
+ auto node = BuildNodeFromProducer();
+ ExecuteVerb(node, context->GetUnderlyingContext());
+ }
+
+ void ExistsRecursive(const TYPath& path, TReqExists* /*request*/, TRspExists* /*response*/, const TCtxExistsPtr& context) override
+ {
+ context->SetRequestInfo();
+
+ auto consumer = CreateYPathDesignatedConsumer(path, EMissingPathMode::ThrowError, GetNullYsonConsumer());
+ try {
+ Producer_.Run(consumer.get());
+ } catch (const TErrorException& ex) {
+ if (ex.Error().FindMatching(EErrorCode::ResolveError)) {
+ Reply(context, /*exists*/ false);
+ return;
+ }
+ throw;
+ }
+
+ Reply(context, /*exists*/ true);
+ }
+
+ TYsonString BuildStringFromProducer()
+ {
+ TStringStream stream;
+ TBufferedBinaryYsonWriter writer(&stream);
+ Producer_.Run(&writer);
+ writer.Flush();
+
+ auto str = stream.Str();
+ if (str.empty()) {
+ THROW_ERROR_EXCEPTION(NRpc::EErrorCode::Unavailable, "No data is available");
+ }
+
+ return TYsonString(std::move(str));
+ }
+
+ INodePtr BuildNodeFromProducer()
+ {
+ auto builder = CreateBuilderFromFactory(GetEphemeralNodeFactory());
+ Producer_.Run(builder.get());
+ return builder->EndTree();
+ }
+};
+
+IYPathServicePtr IYPathService::YPathDesignatedServiceFromProducer(TYsonProducer producer)
+{
+ return New<TLazyYPathServiceFromProducer>(std::move(producer));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYPathServiceToProducerHandler
+ : public TRefCounted
+{
+public:
+ TYPathServiceToProducerHandler(
+ IYPathServicePtr underlyingService,
+ IInvokerPtr workerInvoker,
+ TDuration updatePeriod)
+ : UnderlyingService_(std::move(underlyingService))
+ , WorkerInvoker_(std::move(workerInvoker))
+ , UpdatePeriod_(updatePeriod)
+ { }
+
+ TYsonProducer Run()
+ {
+ ScheduleUpdate(true);
+ return TYsonProducer(BIND(&TYPathServiceToProducerHandler::Produce, MakeStrong(this)));
+ }
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+ const IInvokerPtr WorkerInvoker_;
+ const TDuration UpdatePeriod_;
+
+ TAtomicObject<TYsonString> CachedString_ = {BuildYsonStringFluently().Entity()};
+
+ void Produce(IYsonConsumer* consumer)
+ {
+ consumer->OnRaw(CachedString_.Load());
+ }
+
+ void ScheduleUpdate(bool immediately)
+ {
+ TDelayedExecutor::Submit(
+ BIND(&TYPathServiceToProducerHandler::OnUpdate, MakeWeak(this)),
+ immediately ? TDuration::Zero() : UpdatePeriod_,
+ WorkerInvoker_);
+ }
+
+ void OnUpdate()
+ {
+ AsyncYPathGet(UnderlyingService_, TYPath())
+ .Subscribe(BIND(&TYPathServiceToProducerHandler::OnUpdateResult, MakeWeak(this)));
+ }
+
+ void OnUpdateResult(const TErrorOr<TYsonString>& errorOrString)
+ {
+ if (errorOrString.IsOK()) {
+ CachedString_.Store(errorOrString.Value());
+ } else {
+ CachedString_.Store(BuildYsonStringFluently()
+ .BeginAttributes()
+ .Item("error").Value(TError(errorOrString))
+ .EndAttributes()
+ .Entity());
+ }
+ ScheduleUpdate(false);
+ }
+};
+
+TYsonProducer IYPathService::ToProducer(
+ IInvokerPtr workerInvoker,
+ TDuration updatePeriod)
+{
+ return New<TYPathServiceToProducerHandler>(
+ this,
+ std::move(workerInvoker),
+ updatePeriod)
+ ->Run();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TViaYPathService
+ : public TYPathServiceBase
+{
+public:
+ TViaYPathService(
+ IYPathServicePtr underlyingService,
+ IInvokerPtr invoker)
+ : UnderlyingService_(underlyingService)
+ , Invoker_(invoker)
+ { }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ return TResolveResultHere{path};
+ }
+
+ bool ShouldHideAttributes() override
+ {
+ return UnderlyingService_->ShouldHideAttributes();
+ }
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+ const IInvokerPtr Invoker_;
+
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ Invoker_->Invoke(BIND([=, this, this_ = MakeStrong(this)] () {
+ ExecuteVerb(UnderlyingService_, context);
+ }));
+ return true;
+ }
+};
+
+IYPathServicePtr IYPathService::Via(IInvokerPtr invoker)
+{
+ return New<TViaYPathService>(this, invoker);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DECLARE_REFCOUNTED_STRUCT(TCacheProfilingCounters)
+
+struct TCacheProfilingCounters
+ : public TRefCounted
+{
+ explicit TCacheProfilingCounters(const NProfiling::TProfiler& profiler)
+ : CacheHitCounter(profiler.Counter("/cache_hit"))
+ , CacheMissCounter(profiler.Counter("/cache_miss"))
+ , RedundantCacheMissCounter(profiler.Counter("/redundant_cache_miss"))
+ , InvalidCacheHitCounter(profiler.Counter("/invalid_cache_hit"))
+ , ByteSize(profiler.Gauge("/byte_size"))
+ { }
+
+ NProfiling::TCounter CacheHitCounter;
+ NProfiling::TCounter CacheMissCounter;
+ NProfiling::TCounter RedundantCacheMissCounter;
+ NProfiling::TCounter InvalidCacheHitCounter;
+ NProfiling::TGauge ByteSize;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCacheProfilingCounters)
+
+DECLARE_REFCOUNTED_CLASS(TCacheSnapshot)
+
+class TCacheSnapshot
+ : public TRefCounted
+{
+public:
+ TCacheSnapshot(TErrorOr<INodePtr> treeOrError, TCacheProfilingCountersPtr profilingCounters)
+ : TreeOrError_(std::move(treeOrError))
+ , ProfilingCounters_(std::move(profilingCounters))
+ { }
+
+ const TErrorOr<INodePtr>& GetTreeOrError() const
+ {
+ return TreeOrError_;
+ }
+
+ void AddResponse(const TCacheKey& key, const TSharedRefArray& responseMessage)
+ {
+ auto guard = WriterGuard(Lock_);
+
+ decltype(KeyToResponseMessage_)::insert_ctx ctx;
+ auto it = KeyToResponseMessage_.find(key, ctx);
+
+ if (it == KeyToResponseMessage_.end()) {
+ KeyToResponseMessage_.emplace_direct(ctx, key, responseMessage);
+ } else {
+ ProfilingCounters_->RedundantCacheMissCounter.Increment();
+ }
+ }
+
+ std::optional<TErrorOr<TSharedRefArray>> LookupResponse(const TCacheKey& key) const
+ {
+ auto guard = ReaderGuard(Lock_);
+
+ auto it = KeyToResponseMessage_.find(key);
+ if (it == KeyToResponseMessage_.end()) {
+ return std::nullopt;
+ } else {
+ return it->second;
+ }
+ }
+
+private:
+ const TErrorOr<INodePtr> TreeOrError_;
+
+ YT_DECLARE_SPIN_LOCK(NThreading::TReaderWriterSpinLock, Lock_);
+ THashMap<TCacheKey, TSharedRefArray> KeyToResponseMessage_;
+ TCacheProfilingCountersPtr ProfilingCounters_;
+};
+
+DEFINE_REFCOUNTED_TYPE(TCacheSnapshot)
+
+DECLARE_REFCOUNTED_CLASS(TCachedYPathServiceContext)
+
+class TCachedYPathServiceContext
+ : public TYPathServiceContextWrapper
+{
+public:
+ TCachedYPathServiceContext(
+ const IYPathServiceContextPtr& underlyingContext,
+ TWeakPtr<TCacheSnapshot> cacheSnapshot,
+ TCacheKey cacheKey)
+ : TYPathServiceContextWrapper(underlyingContext)
+ , CacheSnapshot_(std::move(cacheSnapshot))
+ , CacheKey_(std::move(cacheKey))
+ {
+ underlyingContext->GetAsyncResponseMessage()
+ .Subscribe(BIND([weakThis = MakeWeak(this)] (const TErrorOr<TSharedRefArray>& responseMessageOrError) {
+ if (auto this_ = weakThis.Lock()) {
+ if (responseMessageOrError.IsOK()) {
+ this_->TryAddResponseToCache(responseMessageOrError.Value());
+ }
+ }
+ }));
+ }
+
+private:
+ const TWeakPtr<TCacheSnapshot> CacheSnapshot_;
+ const TCacheKey CacheKey_;
+
+ void TryAddResponseToCache(const TSharedRefArray& responseMessage)
+ {
+ if (auto cacheSnapshot = CacheSnapshot_.Lock()) {
+ cacheSnapshot->AddResponse(CacheKey_, responseMessage);
+ }
+ }
+};
+
+DEFINE_REFCOUNTED_TYPE(TCachedYPathServiceContext)
+
+DECLARE_REFCOUNTED_CLASS(TCachedYPathService)
+
+class TCachedYPathService
+ : public TYPathServiceBase
+ , public ICachedYPathService
+{
+public:
+ TCachedYPathService(
+ IYPathServicePtr underlyingService,
+ TDuration updatePeriod,
+ IInvokerPtr workerInvoker,
+ const NProfiling::TProfiler& profiler);
+
+ TResolveResult Resolve(const TYPath& path, const IYPathServiceContextPtr& /*context*/) override;
+
+ void SetCachePeriod(TDuration period) override;
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+ const IInvokerPtr WorkerInvoker_;
+
+ const TPeriodicExecutorPtr PeriodicExecutor_;
+
+ std::atomic<bool> IsCacheEnabled_ = false;
+ std::atomic<bool> IsCacheValid_ = false;
+
+ TCacheProfilingCountersPtr ProfilingCounters_;
+
+ TAtomicIntrusivePtr<TCacheSnapshot> CurrentCacheSnapshot_;
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override;
+
+ void RebuildCache();
+
+ void UpdateCachedTree(const TErrorOr<INodePtr>& treeOrError);
+};
+
+DEFINE_REFCOUNTED_TYPE(TCachedYPathService)
+
+TCachedYPathService::TCachedYPathService(
+ IYPathServicePtr underlyingService,
+ TDuration updatePeriod,
+ IInvokerPtr workerInvoker,
+ const NProfiling::TProfiler& profiler)
+ : UnderlyingService_(std::move(underlyingService))
+ , WorkerInvoker_(workerInvoker
+ ? workerInvoker
+ : NRpc::TDispatcher::Get()->GetHeavyInvoker())
+ , PeriodicExecutor_(New<TPeriodicExecutor>(
+ WorkerInvoker_,
+ BIND(&TCachedYPathService::RebuildCache, MakeWeak(this)),
+ updatePeriod))
+ , ProfilingCounters_(New<TCacheProfilingCounters>(profiler))
+{
+ YT_VERIFY(UnderlyingService_);
+ SetCachePeriod(updatePeriod);
+}
+
+IYPathService::TResolveResult TCachedYPathService::Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/)
+{
+ return TResolveResultHere{path};
+}
+
+void TCachedYPathService::SetCachePeriod(TDuration period)
+{
+ if (period == TDuration::Zero()) {
+ if (IsCacheEnabled_) {
+ IsCacheEnabled_.store(false);
+ YT_UNUSED_FUTURE(PeriodicExecutor_->Stop());
+ }
+ } else {
+ PeriodicExecutor_->SetPeriod(period);
+ if (!IsCacheEnabled_) {
+ IsCacheEnabled_.store(true);
+ IsCacheValid_.store(false);
+ PeriodicExecutor_->Start();
+ }
+ }
+}
+
+void ReplyErrorOrValue(const IYPathServiceContextPtr& context, const TErrorOr<TSharedRefArray>& response)
+{
+ if (response.IsOK()) {
+ context->Reply(response.Value());
+ } else {
+ context->Reply(static_cast<TError>(response));
+ }
+}
+
+bool TCachedYPathService::DoInvoke(const IYPathServiceContextPtr& context)
+{
+ if (IsCacheEnabled_ && IsCacheValid_) {
+ WorkerInvoker_->Invoke(BIND([this, context, this_ = MakeStrong(this)]() {
+ try {
+ auto cacheSnapshot = CurrentCacheSnapshot_.Acquire();
+ YT_VERIFY(cacheSnapshot);
+
+ if (context->GetRequestMessage().Size() < 2) {
+ context->Reply(TError("Invalid request"));
+ return;
+ }
+
+ TCacheKey key(
+ GetRequestTargetYPath(context->GetRequestHeader()),
+ context->GetRequestHeader().method(),
+ context->GetRequestMessage()[1]);
+
+ if (auto cachedResponse = cacheSnapshot->LookupResponse(key)) {
+ ReplyErrorOrValue(context, *cachedResponse);
+ ProfilingCounters_->CacheHitCounter.Increment();
+ return;
+ }
+
+ auto treeOrError = cacheSnapshot->GetTreeOrError();
+ if (!treeOrError.IsOK()) {
+ context->Reply(static_cast<TError>(treeOrError));
+ return;
+ }
+ const auto& tree = treeOrError.Value();
+
+ auto contextWrapper = New<TCachedYPathServiceContext>(context, MakeWeak(cacheSnapshot), std::move(key));
+ ExecuteVerb(tree, contextWrapper);
+ ProfilingCounters_->CacheMissCounter.Increment();
+ } catch (const std::exception& ex) {
+ context->Reply(ex);
+ }
+ }));
+ } else {
+ UnderlyingService_->Invoke(context);
+ ProfilingCounters_->InvalidCacheHitCounter.Increment();
+ }
+
+ return true;
+}
+
+void TCachedYPathService::RebuildCache()
+{
+ try {
+ auto asyncYson = AsyncYPathGet(UnderlyingService_, /* path */ TYPath(), TAttributeFilter());
+
+ auto yson = WaitFor(asyncYson)
+ .ValueOrThrow();
+
+ ProfilingCounters_->ByteSize.Update(yson.AsStringBuf().Size());
+
+ UpdateCachedTree(ConvertToNode(yson));
+ } catch (const std::exception& ex) {
+ UpdateCachedTree(TError(ex));
+ }
+}
+
+void TCachedYPathService::UpdateCachedTree(const TErrorOr<INodePtr>& treeOrError)
+{
+ auto newCachedTree = New<TCacheSnapshot>(treeOrError, ProfilingCounters_);
+ CurrentCacheSnapshot_.Store(newCachedTree);
+ IsCacheValid_ = true;
+}
+
+IYPathServicePtr IYPathService::Cached(
+ TDuration updatePeriod,
+ IInvokerPtr workerInvoker,
+ const NProfiling::TProfiler& profiler)
+{
+ return New<TCachedYPathService>(this, updatePeriod, workerInvoker, profiler);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TPermissionValidatingYPathService
+ : public TYPathServiceBase
+ , public TSupportsPermissions
+{
+public:
+ TPermissionValidatingYPathService(
+ IYPathServicePtr underlyingService,
+ TCallback<void(const TString&, EPermission)> validationCallback)
+ : UnderlyingService_(std::move(underlyingService))
+ , ValidationCallback_(std::move(validationCallback))
+ , PermissionValidator_(this, EPermissionCheckScope::This)
+ { }
+
+ TResolveResult Resolve(
+ const TYPath& path,
+ const IYPathServiceContextPtr& /*context*/) override
+ {
+ return TResolveResultHere{path};
+ }
+
+ bool ShouldHideAttributes() override
+ {
+ return UnderlyingService_->ShouldHideAttributes();
+ }
+
+private:
+ const IYPathServicePtr UnderlyingService_;
+ const TCallback<void(const TString&, EPermission)> ValidationCallback_;
+
+ TCachingPermissionValidator PermissionValidator_;
+
+ void ValidatePermission(
+ EPermissionCheckScope /* scope */,
+ EPermission permission,
+ const TString& user) override
+ {
+ ValidationCallback_.Run(user, permission);
+ }
+
+ bool DoInvoke(const IYPathServiceContextPtr& context) override
+ {
+ // TODO(max42): choose permission depending on method.
+ PermissionValidator_.Validate(EPermission::Read, context->GetAuthenticationIdentity().User);
+ ExecuteVerb(UnderlyingService_, context);
+ return true;
+ }
+};
+
+IYPathServicePtr IYPathService::WithPermissionValidator(TCallback<void(const TString&, EPermission)> validationCallback)
+{
+ return New<TPermissionValidatingYPathService>(this, std::move(validationCallback));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void IYPathService::WriteAttributesFragment(
+ IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable)
+{
+ if (!attributeFilter && ShouldHideAttributes()) {
+ return;
+ }
+ DoWriteAttributesFragment(consumer, attributeFilter, stable);
+}
+
+void IYPathService::WriteAttributes(
+ IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable)
+{
+ TAttributeFragmentConsumer attributesConsumer(consumer);
+ WriteAttributesFragment(&attributesConsumer, attributeFilter, stable);
+ attributesConsumer.Finish();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/ypath_service.h b/yt/yt/core/ytree/ypath_service.h
new file mode 100644
index 0000000000..f8303acb93
--- /dev/null
+++ b/yt/yt/core/ytree/ypath_service.h
@@ -0,0 +1,172 @@
+#pragma once
+
+#include "public.h"
+#include "permission.h"
+
+#include <yt/yt/core/actions/public.h>
+
+#include <yt/yt/core/logging/log.h>
+
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/rpc/public.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/library/profiling/sensor.h>
+
+#include <variant>
+
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Represents an abstract way of handling YPath requests.
+/*!
+ * To handle a given YPath request one must first resolve the target.
+ *
+ * We start with some root service and call #Resolve. The latter either replies "here", in which case
+ * the resolution is finished, or "there", in which case a new candidate target is provided.
+ * At each resolution step the current path may be altered by specifying a new one
+ * as a part of the result.
+ *
+ * Once the request is resolved, #Invoke is called for the target service.
+ *
+ * This interface also provides means for inspecting attributes associated with the service.
+ *
+ */
+struct IYPathService
+ : public virtual TRefCounted
+{
+ //! A result indicating that resolution is finished.
+ struct TResolveResultHere
+ {
+ TYPath Path;
+ };
+
+ //! A result indicating that resolution must proceed.
+ struct TResolveResultThere
+ {
+ IYPathServicePtr Service;
+ TYPath Path;
+ };
+
+ using TResolveResult = std::variant<
+ TResolveResultHere,
+ TResolveResultThere
+ >;
+
+ //! Resolves the given path by either returning "here" or "there" result.
+ virtual TResolveResult Resolve(const TYPath& path, const IYPathServiceContextPtr& context) = 0;
+
+ //! Executes a given request.
+ virtual void Invoke(const IYPathServiceContextPtr& context) = 0;
+
+ //! Writes a map fragment consisting of attributes conforming to #filter into #consumer.
+ /*!
+ * If #stable is |true| then the implementation must ensure a stable result.
+ */
+ void WriteAttributesFragment(
+ NYson::IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable);
+
+ //! Wraps WriteAttributesFragment by enclosing attributes with angle brackets.
+ //! If WriteAttributesFragment writes nothing then this method also does nothing.
+ void WriteAttributes(
+ NYson::IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable);
+
+ //! Manages strategy of writing attributes if attribute keys are null.
+ virtual bool ShouldHideAttributes() = 0;
+
+ // Extension methods
+
+ //! Creates a YPath service from a YSON producer.
+ /*!
+ * Each time a request is executed, producer is called, its output is turned into
+ * an ephemeral tree, and the request is forwarded to that tree.
+ */
+ static IYPathServicePtr FromProducer(
+ NYson::TYsonProducer producer,
+ TDuration cachePeriod = {});
+
+ //! Creates a YPathDesignated service from a YSON producer.
+ /*!
+ * Optimized version of previous service.
+ * Tries to avoid constructing an ephemeral tree using TYPathDesignatedYsonConsumer.
+ */
+ static IYPathServicePtr YPathDesignatedServiceFromProducer(
+ NYson::TYsonProducer producer);
+
+ //! Creates a YPath service from an extended YSON producer.
+ /*!
+ * Each time a request is executed, producer is called, its output is turned into
+ * an ephemeral tree, and the request is forwarded to that tree.
+ */
+ static IYPathServicePtr FromProducer(
+ NYson::TExtendedYsonProducer<const IAttributeDictionaryPtr&> producer);
+
+ //! Creates a producer from YPath service.
+ /*!
+ * Takes a periodic snapshot of the service's root and returns it
+ * synchronously upon request. Snapshotting takes place in #workerInvoker.
+ */
+ NYson::TYsonProducer ToProducer(
+ IInvokerPtr workerInvoker,
+ TDuration updatePeriod);
+
+ //! Creates a YPath service from a class method returning serializable value.
+ template <class T, class R>
+ static IYPathServicePtr FromMethod(
+ R (std::remove_cv_t<T>::*method) () const,
+ const TWeakPtr<T>& weak);
+
+ //! Creates a YPath service from a class producer method.
+ template <class T>
+ static IYPathServicePtr FromMethod(
+ void (std::remove_cv_t<T>::*producer) (NYson::IYsonConsumer*) const,
+ const TWeakPtr<T>& weak);
+
+
+ //! Creates a wrapper that handles all requests via the given invoker.
+ IYPathServicePtr Via(IInvokerPtr invoker);
+
+ //! Creates a wrapper that makes ephemeral snapshots to cache
+ //! the underlying service.
+ //! Building tree from underlying service is performed in #invoker,
+ IYPathServicePtr Cached(
+ TDuration updatePeriod,
+ IInvokerPtr workerInvoker,
+ const NProfiling::TProfiler& profiler = {});
+
+ //! Creates a wrapper that calls given callback on each invocation
+ //! in order to validate user permission to query the ypath service.
+ IYPathServicePtr WithPermissionValidator(TCallback<void(const TString&, EPermission)> validationCallback);
+
+protected:
+ //! Implementation method for WriteAttributesFragment.
+ //! It always write requested attributes and call ShouldHideAttributes.
+ virtual void DoWriteAttributesFragment(
+ NYson::IAsyncYsonConsumer* consumer,
+ const TAttributeFilter& attributeFilter,
+ bool stable) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(IYPathService)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct ICachedYPathService
+ : public virtual IYPathService
+{
+ virtual void SetCachePeriod(TDuration period) = 0;
+};
+
+DEFINE_REFCOUNTED_TYPE(ICachedYPathService)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_serializable-inl.h b/yt/yt/core/ytree/yson_serializable-inl.h
new file mode 100644
index 0000000000..7269174a30
--- /dev/null
+++ b/yt/yt/core/ytree/yson_serializable-inl.h
@@ -0,0 +1,1038 @@
+#ifndef YSON_SERIALIZABLE_INL_H_
+#error "Direct inclusion of this file is not allowed, include yson_serializable.h"
+// For the sake of sane code completion.
+#include "yson_serializable.h"
+#endif
+
+#include "convert.h"
+#include "serialize.h"
+#include "tree_visitor.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/misc/guid.h>
+#include <yt/yt/core/misc/serialize.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <util/datetime/base.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+template <class T>
+concept IsYsonStructOrYsonSerializable = std::is_base_of_v<TYsonStructBase, T> || std::is_base_of_v<TYsonSerializableLite, T>;
+
+template <class T>
+void LoadFromNode(
+ T& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy /*mergeStrategy*/,
+ bool /*keepUnrecognizedRecursively*/)
+{
+ try {
+ Deserialize(parameter, node);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error reading parameter %v", path)
+ << ex;
+ }
+}
+
+// INodePtr
+template <>
+inline void LoadFromNode(
+ NYTree::INodePtr& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& /*path*/,
+ EMergeStrategy mergeStrategy,
+ bool /*keepUnrecognizedRecursively*/)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter = node;
+ break;
+ }
+
+ case EMergeStrategy::Combine: {
+ if (!parameter) {
+ parameter = node;
+ } else {
+ parameter = PatchNode(parameter, node);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// TYsonSerializable
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromNode(
+ TIntrusivePtr<T>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ if (!parameter || mergeStrategy == EMergeStrategy::Overwrite) {
+ parameter = New<T>();
+ }
+
+ if (keepUnrecognizedRecursively) {
+ parameter->SetUnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+ }
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite:
+ case EMergeStrategy::Combine: {
+ parameter->Load(node, false, false, path);
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::optional
+template <class T>
+void LoadFromNode(
+ std::optional<T>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ if (node->GetType() == NYTree::ENodeType::Entity) {
+ parameter = std::nullopt;
+ } else {
+ T value;
+ LoadFromNode(value, node, path, EMergeStrategy::Overwrite, keepUnrecognizedRecursively);
+ parameter = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::vector
+template <class... T>
+void LoadFromNode(
+ std::vector<T...>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ auto listNode = node->AsList();
+ auto size = listNode->GetChildCount();
+ parameter.resize(size);
+ for (int i = 0; i < size; ++i) {
+ LoadFromNode(
+ parameter[i],
+ listNode->GetChildOrThrow(i),
+ path + "/" + NYPath::ToYPathLiteral(i),
+ EMergeStrategy::Overwrite,
+ keepUnrecognizedRecursively);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+template <class T>
+T DeserializeMapKey(TStringBuf value)
+{
+ if constexpr (TEnumTraits<T>::IsEnum) {
+ return ParseEnum<T>(value);
+ } else if constexpr (std::is_same_v<T, TGuid>) {
+ return TGuid::FromString(value);
+ } else {
+ return FromString<T>(value);
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class M = typename Map<T...>::mapped_type>
+void LoadFromNode(
+ Map<T...>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ auto mapNode = node->AsMap();
+ parameter.clear();
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ M value;
+ LoadFromNode(
+ value,
+ child,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ EMergeStrategy::Overwrite,
+ keepUnrecognizedRecursively);
+ parameter.emplace(DeserializeMapKey<typename Map<T...>::key_type>(key), std::move(value));
+ }
+ break;
+ }
+ case EMergeStrategy::Combine: {
+ auto mapNode = node->AsMap();
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ M value;
+ LoadFromNode(
+ value,
+ child,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ EMergeStrategy::Combine,
+ keepUnrecognizedRecursively);
+ parameter[DeserializeMapKey<typename Map<T...>::key_type>(key)] = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void LoadFromCursor(
+ T& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy /*mergeStrategy*/,
+ bool /*keepUnrecognizedRecursively*/)
+{
+ try {
+ Deserialize(parameter, cursor);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error reading parameter %v", path)
+ << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromCursor(
+ TIntrusivePtr<T>& parameterValue,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively);
+
+template <class... T>
+void LoadFromCursor(
+ std::vector<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively);
+
+// std::optional
+template <class T>
+void LoadFromCursor(
+ std::optional<T>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively);
+
+template <template <typename...> class Map, class... T, class M = typename Map<T...>::mapped_type>
+void LoadFromCursor(
+ Map<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// INodePtr
+template <>
+inline void LoadFromCursor(
+ NYTree::INodePtr& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ try {
+ auto node = NYson::ExtractTo<INodePtr>(cursor);
+ LoadFromNode(parameter, std::move(node), path, mergeStrategy, keepUnrecognizedRecursively);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromCursor(
+ TIntrusivePtr<T>& parameterValue,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ if (!parameterValue || mergeStrategy == EMergeStrategy::Overwrite) {
+ parameterValue = New<T>();
+ }
+
+ if (keepUnrecognizedRecursively) {
+ parameterValue->SetUnrecognizedStrategy(EUnrecognizedStrategy::KeepRecursive);
+ }
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite:
+ case EMergeStrategy::Combine: {
+ parameterValue->Load(cursor, false, false, path);
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::optional
+template <class T>
+void LoadFromCursor(
+ std::optional<T>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ try {
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ if ((*cursor)->GetType() == NYson::EYsonItemType::EntityValue) {
+ parameter = std::nullopt;
+ cursor->Next();
+ } else {
+ T value;
+ LoadFromCursor(value, cursor, path, EMergeStrategy::Overwrite, keepUnrecognizedRecursively);
+ parameter = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// std::vector
+template <class... T>
+void LoadFromCursor(
+ std::vector<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ try {
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter.clear();
+ int index = 0;
+ cursor->ParseList([&](NYson::TYsonPullParserCursor* cursor) {
+ LoadFromCursor(
+ parameter.emplace_back(),
+ cursor,
+ path + "/" + NYPath::ToYPathLiteral(index),
+ EMergeStrategy::Overwrite,
+ keepUnrecognizedRecursively);
+ ++index;
+ });
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+template <class TMapping, class TValue, class TEmplacer, class TSetter>
+void LoadMappingFromCursor(
+ TMapping& mapping,
+ TEmplacer emplacer,
+ TSetter setter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ try {
+ auto doParse = [&] (auto setterOrEmplacer, EMergeStrategy mergeStrategy) {
+ cursor->ParseMap([&] (NYson::TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<TString>(cursor);
+ TValue value;
+ LoadFromCursor(
+ value,
+ cursor,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ mergeStrategy,
+ keepUnrecognizedRecursively);
+ setterOrEmplacer(mapping, key, std::move(value));
+ });
+ };
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ mapping = {};
+ doParse(emplacer, EMergeStrategy::Overwrite);
+ break;
+ }
+ case EMergeStrategy::Combine: {
+ doParse(setter, EMergeStrategy::Combine);
+ break;
+ }
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class M>
+void LoadFromCursor(
+ Map<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ bool keepUnrecognizedRecursively)
+{
+ try {
+ auto doParse = [&] (const auto& setterOrEmplacer, EMergeStrategy mergeStrategy) {
+ cursor->ParseMap([&] (NYson::TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<TString>(cursor);
+ M value;
+ LoadFromCursor(
+ value,
+ cursor,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ mergeStrategy,
+ keepUnrecognizedRecursively);
+ setterOrEmplacer(key, std::move(value));
+ });
+ };
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter.clear();
+ auto emplacer = [&] (auto key, M&& value) {
+ parameter.emplace(DeserializeMapKey<typename Map<T...>::key_type>(key), std::move(value));
+ };
+ doParse(emplacer, EMergeStrategy::Overwrite);
+ break;
+ }
+ case EMergeStrategy::Combine: {
+ auto setter = [&] (auto key, M&& value) {
+ parameter[DeserializeMapKey<typename Map<T...>::key_type>(key)] = std::move(value);
+ };
+ doParse(setter, EMergeStrategy::Combine);
+ break;
+ }
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// For all classes except descendants of TYsonSerializableLite and their intrusive pointers
+// we do not attempt to extract unrecognzied members. C++ prohibits function template specialization
+// so we have to deal with static struct members.
+template <class T>
+struct TGetUnrecognizedRecursively
+{
+ static IMapNodePtr Do(const T& /*parameter*/)
+ {
+ return nullptr;
+ }
+};
+
+template <IsYsonStructOrYsonSerializable T>
+struct TGetUnrecognizedRecursively<T>
+{
+ static IMapNodePtr Do(const T& parameter)
+ {
+ return parameter.GetRecursiveUnrecognized();
+ }
+};
+
+template <IsYsonStructOrYsonSerializable T>
+struct TGetUnrecognizedRecursively<TIntrusivePtr<T>>
+{
+ static IMapNodePtr Do(const TIntrusivePtr<T>& parameter)
+ {
+ return parameter ? parameter->GetRecursiveUnrecognized() : nullptr;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// all
+template <class F>
+void InvokeForComposites(
+ const void* /* parameter */,
+ const NYPath::TYPath& /* path */,
+ const F& /* func */)
+{ }
+
+// TYsonSerializable or TYsonStruct
+template <IsYsonStructOrYsonSerializable T, class F>
+inline void InvokeForComposites(
+ const TIntrusivePtr<T>* parameter,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ func(*parameter, path);
+}
+
+// std::vector
+template <class... T, class F>
+inline void InvokeForComposites(
+ const std::vector<T...>* parameter,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ for (size_t i = 0; i < parameter->size(); ++i) {
+ InvokeForComposites(
+ &(*parameter)[i],
+ path + "/" + NYPath::ToYPathLiteral(i),
+ func);
+ }
+}
+
+// For any map.
+template <template<typename...> class Map, class... T, class F, class M = typename Map<T...>::mapped_type>
+inline void InvokeForComposites(
+ const Map<T...>* parameter,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ for (const auto& [key, value] : *parameter) {
+ InvokeForComposites(
+ &value,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ func);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// all
+template <class F>
+void InvokeForComposites(
+ const void* /* parameter */,
+ const F& /* func */)
+{ }
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T, class F>
+inline void InvokeForComposites(const TIntrusivePtr<T>* parameter, const F& func)
+{
+ func(*parameter);
+}
+
+// std::vector
+template <class... T, class F>
+inline void InvokeForComposites(const std::vector<T...>* parameter, const F& func)
+{
+ for (const auto& item : *parameter) {
+ InvokeForComposites(&item, func);
+ }
+}
+
+// For any map.
+template <template<typename...> class Map, class... T, class F, class M = typename Map<T...>::mapped_type>
+inline void InvokeForComposites(const Map<T...>* parameter, const F& func)
+{
+ for (const auto& [key, value] : *parameter) {
+ InvokeForComposites(&value, func);
+ }
+}
+
+// TODO(shakurov): get rid of this once concept support makes it into the standard
+// library implementation. Use equality-comparability instead.
+template <class T>
+concept SupportsDontSerializeDefaultImpl =
+ std::is_arithmetic_v<T> ||
+ std::is_same_v<T, TString> ||
+ std::is_same_v<T, TDuration> ||
+ std::is_same_v<T, TGuid> ||
+ std::is_same_v<T, std::optional<std::vector<TString>>> ||
+ std::is_same_v<T, THashSet<TString>>;
+
+template <class T>
+concept SupportsDontSerializeDefault =
+ SupportsDontSerializeDefaultImpl<T> ||
+ TStdOptionalTraits<T>::IsStdOptional &&
+ SupportsDontSerializeDefaultImpl<typename TStdOptionalTraits<T>::TValueType>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NDetail
+
+template <class T>
+TYsonSerializableLite::TParameter<T>::TParameter(TString key, T& parameter)
+ : Key(std::move(key))
+ , Parameter(parameter)
+ , MergeStrategy(EMergeStrategy::Default)
+{ }
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::Load(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy)
+{
+ if (node) {
+ NDetail::LoadFromNode(
+ Parameter,
+ node,
+ path,
+ mergeStrategy.value_or(MergeStrategy),
+ KeepUnrecognizedRecursively);
+ } else if (!DefaultValue) {
+ THROW_ERROR_EXCEPTION("Missing required parameter %v",
+ path);
+ }
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::SafeLoad(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy)
+{
+ if (node) {
+ T oldValue = Parameter;
+ try {
+ NDetail::LoadFromNode(
+ Parameter,
+ node,
+ path,
+ mergeStrategy.value_or(MergeStrategy),
+ KeepUnrecognizedRecursively);
+ validate();
+ } catch (const std::exception&) {
+ Parameter = std::move(oldValue);
+ throw;
+ }
+ }
+}
+
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::Load(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy)
+{
+ if (cursor) {
+ NDetail::LoadFromCursor(
+ Parameter,
+ cursor,
+ path,
+ mergeStrategy.value_or(MergeStrategy),
+ KeepUnrecognizedRecursively);
+ } else if (!DefaultValue) {
+ THROW_ERROR_EXCEPTION("Missing required parameter %v",
+ path);
+ }
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::SafeLoad(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy)
+{
+ if (cursor) {
+ T oldValue = Parameter;
+ try {
+ NDetail::LoadFromCursor(
+ Parameter,
+ cursor,
+ path,
+ mergeStrategy.value_or(MergeStrategy),
+ KeepUnrecognizedRecursively);
+ validate();
+ } catch (const std::exception&) {
+ Parameter = std::move(oldValue);
+ throw;
+ }
+ }
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::Postprocess(const NYPath::TYPath& path) const
+{
+ for (const auto& postprocessor : Postprocessors) {
+ try {
+ postprocessor(Parameter);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Postprocess failed at %v",
+ path.empty() ? "root" : path)
+ << ex;
+ }
+ }
+
+ NYT::NYTree::NDetail::InvokeForComposites(
+ &Parameter,
+ path,
+ [] <NDetail::IsYsonStructOrYsonSerializable TStruct> (TIntrusivePtr<TStruct> obj, const NYPath::TYPath& subpath) {
+ if (obj) {
+ obj->Postprocess(subpath);
+ }
+ });
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::SetDefaults()
+{
+ if (DefaultValue) {
+ Parameter = *DefaultValue;
+ }
+
+ NYT::NYTree::NDetail::InvokeForComposites(
+ &Parameter,
+ [] <NDetail::IsYsonStructOrYsonSerializable TStruct> (TIntrusivePtr<TStruct> obj) {
+ if (obj) {
+ obj->SetDefaults();
+ }
+ });
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::Save(NYson::IYsonConsumer* consumer) const
+{
+ using NYTree::Serialize;
+ Serialize(Parameter, consumer);
+}
+
+template <class T>
+bool TYsonSerializableLite::TParameter<T>::CanOmitValue() const
+{
+ if constexpr (NDetail::SupportsDontSerializeDefault<T>) {
+ if (!SerializeDefault && Parameter == DefaultValue) {
+ return true;
+ }
+ }
+
+ return NYT::NYTree::NDetail::CanOmitValue(&Parameter, DefaultValue ? &*DefaultValue : nullptr);
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::Alias(const TString& name)
+{
+ Aliases.push_back(name);
+ return *this;
+}
+
+template <class T>
+const std::vector<TString>& TYsonSerializableLite::TParameter<T>::GetAliases() const
+{
+ return Aliases;
+}
+
+template <class T>
+const TString& TYsonSerializableLite::TParameter<T>::GetKey() const
+{
+ return Key;
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::Optional()
+{
+ DefaultValue = Parameter;
+ return *this;
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::Default(const T& defaultValue)
+{
+ DefaultValue = defaultValue;
+ Parameter = defaultValue;
+ return *this;
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::DontSerializeDefault()
+{
+ // We should check for equality-comparability here but it is rather hard
+ // to do the deep validation.
+ static_assert(
+ NDetail::SupportsDontSerializeDefault<T>,
+ "DontSerializeDefault requires |Parameter| to be TString, TDuration, an arithmetic type or an optional of those");
+
+ SerializeDefault = false;
+ return *this;
+}
+
+template <class T>
+template <class... TArgs>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::DefaultNew(TArgs&&... args)
+{
+ return Default(New<typename T::TUnderlying>(std::forward<TArgs>(args)...));
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::CheckThat(TPostprocessor postprocessor)
+{
+ Postprocessors.push_back(std::move(postprocessor));
+ return *this;
+}
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::MergeBy(EMergeStrategy strategy)
+{
+ MergeStrategy = strategy;
+ return *this;
+}
+
+template <class T>
+IMapNodePtr TYsonSerializableLite::TParameter<T>::GetUnrecognizedRecursively() const
+{
+ return NDetail::TGetUnrecognizedRecursively<T>::Do(Parameter);
+}
+
+template <class T>
+void TYsonSerializableLite::TParameter<T>::SetKeepUnrecognizedRecursively()
+{
+ KeepUnrecognizedRecursively = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Standard postprocessors
+
+#define DEFINE_POSTPROCESSOR(method, condition, error) \
+ template <class T> \
+ TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::TParameter<T>::method \
+ { \
+ return CheckThat([=] (const T& parameter) { \
+ using ::ToString; \
+ std::optional<TValueType> nullableParameter(parameter); \
+ if (nullableParameter) { \
+ const auto& actual = *nullableParameter; \
+ if (!(condition)) { \
+ THROW_ERROR error; \
+ } \
+ } \
+ }); \
+ }
+
+DEFINE_POSTPROCESSOR(
+ GreaterThan(TValueType expected),
+ actual > expected,
+ TError("Expected > %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ GreaterThanOrEqual(TValueType expected),
+ actual >= expected,
+ TError("Expected >= %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ LessThan(TValueType expected),
+ actual < expected,
+ TError("Expected < %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ LessThanOrEqual(TValueType expected),
+ actual <= expected,
+ TError("Expected <= %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ InRange(TValueType lowerBound, TValueType upperBound),
+ lowerBound <= actual && actual <= upperBound,
+ TError("Expected in range [%v,%v], found %v", lowerBound, upperBound, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ NonEmpty(),
+ actual.size() > 0,
+ TError("Value must not be empty")
+)
+
+#undef DEFINE_POSTPROCESSOR
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TYsonSerializableLite::TParameter<T>& TYsonSerializableLite::RegisterParameter(
+ TString parameterName,
+ T& value)
+{
+ auto parameter = New<TParameter<T>>(parameterName, value);
+ if (UnrecognizedStrategy == EUnrecognizedStrategy::KeepRecursive) {
+ parameter->SetKeepUnrecognizedRecursively();
+ }
+ YT_VERIFY(Parameters.emplace(std::move(parameterName), parameter).second);
+ return *parameter;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> CloneYsonSerializable(const TIntrusivePtr<T>& obj)
+{
+ static_assert(
+ std::is_convertible_v<T*, TYsonSerializable*>,
+ "'obj' must be convertible to TYsonSerializable");
+
+ return NYTree::ConvertTo<TIntrusivePtr<T>>(NYson::ConvertToYsonString(*obj));
+}
+
+template <class T>
+std::vector<TIntrusivePtr<T>> CloneYsonSerializables(const std::vector<TIntrusivePtr<T>>& objs)
+{
+ std::vector<TIntrusivePtr<T>> clonedObjs;
+ clonedObjs.reserve(objs.size());
+ for (const auto& obj : objs) {
+ clonedObjs.push_back(CloneYsonSerializable(obj));
+ }
+ return clonedObjs;
+}
+
+template <class T>
+THashMap<TString, TIntrusivePtr<T>> CloneYsonSerializables(const THashMap<TString, TIntrusivePtr<T>>& objs)
+{
+ THashMap<TString, TIntrusivePtr<T>> clonedObjs;
+ clonedObjs.reserve(objs.size());
+ for (const auto& [key, obj] : objs) {
+ clonedObjs.emplace(key, CloneYsonSerializable(obj));
+ }
+ return clonedObjs;
+}
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonSerializable(
+ const TIntrusivePtr<T>& obj,
+ const NYTree::INodePtr& patch)
+{
+ static_assert(
+ std::is_convertible_v<T*, TYsonSerializable*>,
+ "'obj' must be convertible to TYsonSerializable");
+
+ using NYTree::INodePtr;
+ using NYTree::ConvertTo;
+
+ if (patch) {
+ return ConvertTo<TIntrusivePtr<T>>(PatchNode(ConvertTo<INodePtr>(obj), patch));
+ } else {
+ return CloneYsonSerializable(obj);
+ }
+}
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonSerializable(
+ const TIntrusivePtr<T>& obj,
+ const NYson::TYsonString& patch)
+{
+ if (!patch) {
+ return obj;
+ }
+
+ return UpdateYsonSerializable(obj, ConvertToNode(patch));
+}
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const NYson::TYsonString& newConfigYson)
+{
+ return ReconfigureYsonSerializable(config, ConvertToNode(newConfigYson));
+}
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const TIntrusivePtr<T>& newConfig)
+{
+ return ReconfigureYsonSerializable(config, ConvertToNode(newConfig));
+}
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const NYTree::INodePtr& newConfigNode)
+{
+ auto configNode = NYTree::ConvertToNode(config);
+
+ auto newConfig = NYTree::ConvertTo<TIntrusivePtr<T>>(newConfigNode);
+ auto newCanonicalConfigNode = NYTree::ConvertToNode(newConfig);
+
+ if (NYTree::AreNodesEqual(configNode, newCanonicalConfigNode)) {
+ return false;
+ }
+
+ config->Load(newConfigNode);
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_serializable.cpp b/yt/yt/core/ytree/yson_serializable.cpp
new file mode 100644
index 0000000000..cc7ccee83e
--- /dev/null
+++ b/yt/yt/core/ytree/yson_serializable.cpp
@@ -0,0 +1,385 @@
+#include "yson_serializable.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/yson/consumer.h>
+#include <yt/yt/core/yson/token_writer.h>
+
+#include <yt/yt/core/ytree/ephemeral_node_factory.h>
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/ypath_detail.h>
+
+#include <util/generic/algorithm.h>
+
+namespace NYT::NYTree {
+
+using namespace NYPath;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonSerializableLite::TYsonSerializableLite()
+{ }
+
+IMapNodePtr TYsonSerializableLite::GetUnrecognized() const
+{
+ return Unrecognized;
+}
+
+IMapNodePtr TYsonSerializableLite::GetRecursiveUnrecognized() const
+{
+ return GetUnrecognizedRecursively();
+}
+
+IMapNodePtr TYsonSerializableLite::GetUnrecognizedRecursively() const
+{
+ // Take a copy of `Unrecognized` and add parameter->GetUnrecognizedRecursively()
+ // for all parameters that are TYsonSerializable's themselves.
+ auto result = Unrecognized ? ConvertTo<IMapNodePtr>(Unrecognized) : GetEphemeralNodeFactory()->CreateMap();
+ for (const auto& [name, parameter] : Parameters) {
+ auto unrecognized = parameter->GetUnrecognizedRecursively();
+ if (unrecognized && unrecognized->AsMap()->GetChildCount() > 0) {
+ result->AddChild(name, unrecognized);
+ }
+ }
+ return result;
+}
+
+void TYsonSerializableLite::SetUnrecognizedStrategy(EUnrecognizedStrategy strategy)
+{
+ UnrecognizedStrategy = strategy;
+ if (strategy == EUnrecognizedStrategy::KeepRecursive) {
+ for (const auto& [name, parameter] : Parameters) {
+ parameter->SetKeepUnrecognizedRecursively();
+ }
+ }
+}
+
+THashSet<TString> TYsonSerializableLite::GetRegisteredKeys() const
+{
+ THashSet<TString> result(Parameters.size());
+ for (const auto& [name, parameter] : Parameters) {
+ result.insert(name);
+ for (const auto& alias : parameter->GetAliases()) {
+ result.insert(alias);
+ }
+ }
+ return result;
+}
+
+void TYsonSerializableLite::Load(
+ INodePtr node,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path)
+{
+ YT_VERIFY(node);
+
+ if (setDefaults) {
+ SetDefaults();
+ }
+
+ auto mapNode = node->AsMap();
+ for (const auto& [name, parameter] : Parameters) {
+ TString key = name;
+ auto child = mapNode->FindChild(name); // can be NULL
+ for (const auto& alias : parameter->GetAliases()) {
+ auto otherChild = mapNode->FindChild(alias);
+ if (child && otherChild && !AreNodesEqual(child, otherChild)) {
+ THROW_ERROR_EXCEPTION("Different values for aliased parameters %Qv and %Qv", key, alias)
+ << TErrorAttribute("main_value", child)
+ << TErrorAttribute("aliased_value", otherChild);
+ }
+ if (!child && otherChild) {
+ child = otherChild;
+ key = alias;
+ }
+ }
+ auto childPath = path + "/" + key;
+ parameter->Load(child, childPath);
+ }
+
+ if (UnrecognizedStrategy != EUnrecognizedStrategy::Drop) {
+ auto registeredKeys = GetRegisteredKeys();
+ if (!Unrecognized) {
+ Unrecognized = GetEphemeralNodeFactory()->CreateMap();
+ }
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ if (registeredKeys.find(key) == registeredKeys.end()) {
+ Unrecognized->RemoveChild(key);
+ YT_VERIFY(Unrecognized->AddChild(key, ConvertToNode(child)));
+ }
+ }
+ }
+
+ if (postprocess) {
+ Postprocess(path);
+ }
+}
+
+void TYsonSerializableLite::Load(
+ TYsonPullParserCursor* cursor,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path)
+{
+ YT_VERIFY(cursor);
+
+ if (setDefaults) {
+ SetDefaults();
+ }
+
+ THashMap<TStringBuf, IParameter*> keyToParameter;
+ THashSet<IParameter*> pendingParameters;
+ for (const auto& [key, parameter] : Parameters) {
+ EmplaceOrCrash(keyToParameter, key, parameter.Get());
+ for (const auto& alias : parameter->GetAliases()) {
+ EmplaceOrCrash(keyToParameter, alias, parameter.Get());
+ }
+ InsertOrCrash(pendingParameters, parameter.Get());
+ }
+
+ THashMap<TString, TString> aliasedData;
+
+ auto processPossibleAlias = [&] (
+ IParameter* parameter,
+ TStringBuf key,
+ TYsonPullParserCursor* cursor)
+ {
+ TStringStream ss;
+ {
+ TCheckedInDebugYsonTokenWriter writer(&ss);
+ cursor->TransferComplexValue(&writer);
+ }
+ auto data = std::move(ss.Str());
+ const auto& canonicalKey = parameter->GetKey();
+ auto aliasedDataIt = aliasedData.find(canonicalKey);
+ if (aliasedDataIt != aliasedData.end()) {
+ auto firstNode = ConvertTo<INodePtr>(TYsonStringBuf(aliasedDataIt->second));
+ auto secondNode = ConvertTo<INodePtr>(TYsonStringBuf(data));
+ if (!AreNodesEqual(firstNode, secondNode)) {
+ THROW_ERROR_EXCEPTION("Different values for aliased parameters %Qv and %Qv", canonicalKey, key)
+ << TErrorAttribute("main_value", firstNode)
+ << TErrorAttribute("aliased_value", secondNode);
+ }
+ return;
+ }
+ {
+ TStringInput input(data);
+ TYsonPullParser parser(&input, NYson::EYsonType::Node);
+ TYsonPullParserCursor newCursor(&parser);
+ auto childPath = path + "/" + key;
+ parameter->Load(&newCursor, childPath);
+ }
+ EmplaceOrCrash(aliasedData, canonicalKey, std::move(data));
+ };
+
+ auto processUnrecognized = [&, this] (const TString& key, TYsonPullParserCursor* cursor) {
+ if (UnrecognizedStrategy == EUnrecognizedStrategy::Drop) {
+ cursor->SkipComplexValue();
+ return;
+ }
+ if (!Unrecognized) {
+ Unrecognized = GetEphemeralNodeFactory()->CreateMap();
+ }
+ Unrecognized->RemoveChild(key);
+ auto added = Unrecognized->AddChild(key, ExtractTo<INodePtr>(cursor));
+ YT_VERIFY(added);
+ };
+
+ cursor->ParseMap([&] (TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<TString>(cursor);
+ auto it = keyToParameter.find(key);
+ if (it == keyToParameter.end()) {
+ processUnrecognized(key, cursor);
+ return;
+ }
+
+ auto parameter = it->second;
+ if (parameter->GetAliases().empty()) {
+ auto childPath = path + "/" + key;
+ parameter->Load(cursor, childPath);
+ } else {
+ processPossibleAlias(parameter, key, cursor);
+ }
+ // NB: Key may be missing in case of aliasing.
+ pendingParameters.erase(parameter);
+ });
+
+ for (auto parameter : pendingParameters) {
+ auto childPath = path + "/" + parameter->GetKey();
+ parameter->Load(/* cursor */ nullptr, childPath);
+ }
+
+ if (postprocess) {
+ Postprocess(path);
+ }
+}
+
+void TYsonSerializableLite::Save(IYsonConsumer* consumer) const
+{
+ consumer->OnBeginMap();
+
+ for (const auto& [name, parameter] : SortHashMapByKeys(Parameters)) {
+ if (!parameter->CanOmitValue()) {
+ consumer->OnKeyedItem(name);
+ parameter->Save(consumer);
+ }
+ }
+
+ if (Unrecognized) {
+ auto children = Unrecognized->GetChildren();
+ SortByFirst(children);
+ for (const auto& [key, child] : children) {
+ consumer->OnKeyedItem(key);
+ Serialize(child, consumer);
+ }
+ }
+
+ consumer->OnEndMap();
+}
+
+void TYsonSerializableLite::Postprocess(const TYPath& path) const
+{
+ for (const auto& [name, parameter] : Parameters) {
+ parameter->Postprocess(path + "/" + name);
+ }
+
+ try {
+ for (const auto& postprocessor : Postprocessors) {
+ postprocessor();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Postprocess failed at %v",
+ path.empty() ? "root" : path)
+ << ex;
+ }
+}
+
+void TYsonSerializableLite::SetDefaults()
+{
+ for (const auto& [name, parameter] : Parameters) {
+ parameter->SetDefaults();
+ }
+ for (const auto& initializer : Preprocessors) {
+ initializer();
+ }
+}
+
+void TYsonSerializableLite::RegisterPreprocessor(const TPreprocessor& func)
+{
+ func();
+ Preprocessors.push_back(func);
+}
+
+void TYsonSerializableLite::RegisterPostprocessor(const TPostprocessor& func)
+{
+ Postprocessors.push_back(func);
+}
+
+void TYsonSerializableLite::SaveParameter(const TString& key, IYsonConsumer* consumer) const
+{
+ GetParameter(key)->Save(consumer);
+}
+
+void TYsonSerializableLite::LoadParameter(const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy) const
+{
+ const auto& parameter = GetParameter(key);
+ auto validate = [&] () {
+ parameter->Postprocess("/" + key);
+ try {
+ for (const auto& postprocessor : Postprocessors) {
+ postprocessor();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ "Postprocess failed while loading parameter %Qv from value %Qv",
+ key,
+ ConvertToYsonString(node, EYsonFormat::Text))
+ << ex;
+ }
+ };
+ parameter->SafeLoad(node, /* path */ "", validate, mergeStrategy);
+}
+
+void TYsonSerializableLite::ResetParameter(const TString& key) const
+{
+ GetParameter(key)->SetDefaults();
+}
+
+TYsonSerializableLite::IParameterPtr TYsonSerializableLite::GetParameter(const TString& keyOrAlias) const
+{
+ auto it = Parameters.find(keyOrAlias);
+ if (it != Parameters.end()) {
+ return it->second;
+ }
+
+ for (const auto& [_, parameter] : Parameters) {
+ if (Count(parameter->GetAliases(), keyOrAlias) > 0) {
+ return parameter;
+ }
+ }
+ THROW_ERROR_EXCEPTION("Key or alias %Qv not found in yson serializable", keyOrAlias);
+}
+
+int TYsonSerializableLite::GetParameterCount() const
+{
+ return Parameters.size();
+}
+
+std::vector<TString> TYsonSerializableLite::GetAllParameterAliases(const TString& key) const
+{
+ auto parameter = GetParameter(key);
+ auto result = parameter->GetAliases();
+ result.push_back(parameter->GetKey());
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonSerializableLite& value, IYsonConsumer* consumer)
+{
+ value.Save(consumer);
+}
+
+void Deserialize(TYsonSerializableLite& value, INodePtr node)
+{
+ value.Load(node);
+}
+
+void Deserialize(TYsonSerializableLite& value, NYson::TYsonPullParserCursor* cursor)
+{
+ value.Load(cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(TYsonSerializable)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+
+namespace NYT {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBinaryYsonSerializer::Save(TStreamSaveContext& context, const TYsonSerializableLite& obj)
+{
+ auto str = ConvertToYsonString(obj);
+ NYT::Save(context, str);
+}
+
+void TBinaryYsonSerializer::Load(TStreamLoadContext& context, TYsonSerializableLite& obj)
+{
+ auto str = NYT::Load<TYsonString>(context);
+ auto node = ConvertTo<INodePtr>(str);
+ obj.Load(node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/ytree/yson_serializable.h b/yt/yt/core/ytree/yson_serializable.h
new file mode 100644
index 0000000000..74b7d97d7d
--- /dev/null
+++ b/yt/yt/core/ytree/yson_serializable.h
@@ -0,0 +1,268 @@
+#pragma once
+
+#include "public.h"
+#include "node.h"
+#include "yson_serialize_common.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/mpl.h>
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/yson/public.h>
+
+#include <functional>
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSerializableLite
+ : private TNonCopyable
+{
+public:
+ typedef std::function<void()> TPostprocessor;
+ typedef std::function<void()> TPreprocessor;
+
+ struct IParameter
+ : public TRefCounted
+ {
+ virtual void Load(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) = 0;
+
+ virtual void SafeLoad(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) = 0;
+
+ virtual void Load(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) = 0;
+
+ virtual void SafeLoad(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) = 0;
+
+ virtual void Postprocess(const NYPath::TYPath& path) const = 0;
+ virtual void SetDefaults() = 0;
+ virtual void Save(NYson::IYsonConsumer* consumer) const = 0;
+ virtual bool CanOmitValue() const = 0;
+ virtual const TString& GetKey() const = 0;
+ virtual const std::vector<TString>& GetAliases() const = 0;
+ virtual IMapNodePtr GetUnrecognizedRecursively() const = 0;
+ virtual void SetKeepUnrecognizedRecursively() = 0;
+ };
+
+ typedef TIntrusivePtr<IParameter> IParameterPtr;
+
+ template <class T>
+ class TParameter
+ : public IParameter
+ {
+ public:
+ typedef std::function<void(const T&)> TPostprocessor;
+ typedef typename TOptionalTraits<T>::TValue TValueType;
+
+ TParameter(TString key, T& parameter);
+
+ void Load(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) override;
+
+ void SafeLoad(
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) override;
+
+ void Load(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) override;
+
+ void SafeLoad(
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ const std::function<void()>& validate,
+ std::optional<EMergeStrategy> mergeStrategy = std::nullopt) override;
+
+ void Postprocess(const NYPath::TYPath& path) const override;
+ void SetDefaults() override;
+ void Save(NYson::IYsonConsumer* consumer) const override;
+ bool CanOmitValue() const override;
+ const TString& GetKey() const override;
+ const std::vector<TString>& GetAliases() const override;
+ IMapNodePtr GetUnrecognizedRecursively() const override;
+ void SetKeepUnrecognizedRecursively() override;
+
+ public:
+ TParameter& Optional();
+ TParameter& Default(const T& defaultValue = T());
+ TParameter& DontSerializeDefault();
+ TParameter& CheckThat(TPostprocessor validator);
+ TParameter& GreaterThan(TValueType value);
+ TParameter& GreaterThanOrEqual(TValueType value);
+ TParameter& LessThan(TValueType value);
+ TParameter& LessThanOrEqual(TValueType value);
+ TParameter& InRange(TValueType lowerBound, TValueType upperBound);
+ TParameter& NonEmpty();
+ TParameter& Alias(const TString& name);
+ TParameter& MergeBy(EMergeStrategy strategy);
+
+ template <class... TArgs>
+ TParameter& DefaultNew(TArgs&&... args);
+
+ private:
+ TString Key;
+ T& Parameter;
+ std::optional<T> DefaultValue;
+ bool SerializeDefault = true;
+ std::vector<TPostprocessor> Postprocessors;
+ std::vector<TString> Aliases;
+ EMergeStrategy MergeStrategy;
+ bool KeepUnrecognizedRecursively = false;
+ };
+
+public:
+ TYsonSerializableLite();
+
+ void Load(
+ NYTree::INodePtr node,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = "");
+
+ void Load(
+ NYson::TYsonPullParserCursor* cursor,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = "");
+
+ void Postprocess(const NYPath::TYPath& path = "") const;
+
+ void SetDefaults();
+
+ void Save(NYson::IYsonConsumer* consumer) const;
+
+ IMapNodePtr GetUnrecognized() const;
+ IMapNodePtr GetUnrecognizedRecursively() const;
+ IMapNodePtr GetRecursiveUnrecognized() const;
+
+ void SetUnrecognizedStrategy(EUnrecognizedStrategy strategy);
+
+ THashSet<TString> GetRegisteredKeys() const;
+ int GetParameterCount() const;
+
+ void SaveParameter(const TString& key, NYson::IYsonConsumer* consumer) const;
+ void LoadParameter(const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy) const;
+ void ResetParameter(const TString& key) const;
+
+ std::vector<TString> GetAllParameterAliases(const TString& key) const;
+
+protected:
+ template <class T>
+ TParameter<T>& RegisterParameter(
+ TString parameterName,
+ T& value);
+
+ void RegisterPreprocessor(const TPreprocessor& func);
+ void RegisterPostprocessor(const TPostprocessor& func);
+
+private:
+ template <class T>
+ friend class TParameter;
+
+ THashMap<TString, IParameterPtr> Parameters;
+
+ NYTree::IMapNodePtr Unrecognized;
+ EUnrecognizedStrategy UnrecognizedStrategy = EUnrecognizedStrategy::Drop;
+
+ std::vector<TPreprocessor> Preprocessors;
+ std::vector<TPostprocessor> Postprocessors;
+
+ IParameterPtr GetParameter(const TString& keyOrAlias) const;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonSerializable
+ : public TRefCounted
+ , public TYsonSerializableLite
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> CloneYsonSerializable(const TIntrusivePtr<T>& obj);
+template <class T>
+std::vector<TIntrusivePtr<T>> CloneYsonSerializables(const std::vector<TIntrusivePtr<T>>& objs);
+template <class T>
+THashMap<TString, TIntrusivePtr<T>> CloneYsonSerializables(const THashMap<TString, TIntrusivePtr<T>>& objs);
+
+void Serialize(const TYsonSerializableLite& value, NYson::IYsonConsumer* consumer);
+void Deserialize(TYsonSerializableLite& value, NYTree::INodePtr node);
+void Deserialize(TYsonSerializableLite& value, NYson::TYsonPullParserCursor* cursor);
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonSerializable(
+ const TIntrusivePtr<T>& obj,
+ const NYTree::INodePtr& patch);
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonSerializable(
+ const TIntrusivePtr<T>& obj,
+ const NYson::TYsonString& patch);
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const NYson::TYsonString& newConfigYson);
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const TIntrusivePtr<T>& newConfig);
+
+template <class T>
+bool ReconfigureYsonSerializable(
+ const TIntrusivePtr<T>& config,
+ const NYTree::INodePtr& newConfigNode);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBinaryYsonSerializer
+{
+ static void Save(TStreamSaveContext& context, const NYTree::TYsonSerializableLite& obj);
+ static void Load(TStreamLoadContext& context, NYTree::TYsonSerializableLite& obj);
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ typename std::enable_if_t<std::is_convertible_v<T&, NYTree::TYsonSerializableLite&>>>
+{
+ typedef TBinaryYsonSerializer TSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define YSON_SERIALIZABLE_INL_H_
+#include "yson_serializable-inl.h"
+#undef YSON_SERIALIZABLE_INL_H_
diff --git a/yt/yt/core/ytree/yson_serialize_common.h b/yt/yt/core/ytree/yson_serialize_common.h
new file mode 100644
index 0000000000..47824666fc
--- /dev/null
+++ b/yt/yt/core/ytree/yson_serialize_common.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <library/cpp/yt/memory/serialize.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_ENUM(EMergeStrategy,
+ (Default)
+ (Overwrite)
+ (Combine)
+);
+
+DEFINE_ENUM(EUnrecognizedStrategy,
+ (Drop)
+ (Keep)
+ (KeepRecursive)
+ (Throw)
+ (ThrowRecursive)
+);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_struct-inl.h b/yt/yt/core/ytree/yson_struct-inl.h
new file mode 100644
index 0000000000..5ea9b17c0d
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct-inl.h
@@ -0,0 +1,296 @@
+#ifndef YSON_STRUCT_INL_H_
+#error "Direct inclusion of this file is not allowed, include yson_struct.h"
+// For the sake of sane code completion.
+#include "yson_struct.h"
+#endif
+
+#include "convert.h"
+#include "serialize.h"
+#include "tree_visitor.h"
+#include "yson_struct_detail.h"
+
+#include <yt/yt/core/concurrency/scheduler_api.h>
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/misc/guid.h>
+#include <yt/yt/core/misc/serialize.h>
+#include <yt/yt/core/misc/singleton.h>
+
+#include <yt/yt/core/ypath/token.h>
+
+#include <yt/yt/core/ytree/ypath_client.h>
+#include <yt/yt/core/ytree/convert.h>
+
+#include <yt/yt/core/actions/bind.h>
+
+#include <library/cpp/yt/misc/enum.h>
+
+#include <util/datetime/base.h>
+
+#include <util/system/sanitizers.h>
+
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+const std::type_info& CallCtor()
+{
+ if constexpr (std::convertible_to<T*, TRefCountedBase*>) {
+ auto dummy = New<T>();
+ // NB: |New| returns pointer to TRefCountedWrapper<T>.
+ return typeid(*dummy);
+ } else {
+ T dummy;
+ return typeid(T);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// This method is called from constructor of every descendant of TYsonStructBase.
+// When it is first called for a particular struct it will initialize TYsonStructMeta for that struct.
+// Also this method initializes defaults for the struct.
+template <class TStruct>
+void TYsonStructRegistry::InitializeStruct(TStruct* target)
+{
+ TForbidCachedDynamicCastGuard guard(target);
+
+ // It takes place only inside special constructor call inside lambda below.
+ if (CurrentlyInitializingMeta_) {
+ // TODO(renadeen): assert target is from the same type hierarchy.
+ // Call initialization method that is provided by user.
+ TStruct::Register(TYsonStructRegistrar<TStruct>(CurrentlyInitializingMeta_));
+ return;
+ }
+
+ auto metaConstructor = [] {
+ auto* result = new TYsonStructMeta();
+ NSan::MarkAsIntentionallyLeaked(result);
+
+ // NB: Here initialization of TYsonStructMeta of particular struct takes place.
+ // First we store meta in static thread local `CurrentlyInitializingMeta_` as we need to access it later.
+ // Then we make special constructor call that will traverse through all TStruct's type hierarchy.
+ // During this call constructors of each base class will call TYsonStructRegistry::Initialize again
+ // and `if` statement at the start of this function will call TStruct::Register
+ // where registration of yson parameters takes place.
+ // This way all parameters of the whole type hierarchy will fill `CurrentlyInitializingMeta_`.
+ // We prevent context switch cause we don't want another fiber to use `CurrentlyInitializingMeta_` before we finish initialization.
+ YT_VERIFY(!CurrentlyInitializingMeta_);
+ CurrentlyInitializingMeta_ = result;
+ {
+ NConcurrency::TForbidContextSwitchGuard contextSwitchGuard;
+ const std::type_info& typeInfo = CallCtor<TStruct>();
+ result->FinishInitialization(typeInfo);
+ }
+ CurrentlyInitializingMeta_ = nullptr;
+
+ return result;
+ };
+
+ static TYsonStructMeta* meta = metaConstructor();
+ target->Meta_ = meta;
+}
+
+template <class TTargetStruct>
+TTargetStruct* TYsonStructRegistry::CachedDynamicCast(const TYsonStructBase* constSource)
+{
+ YT_VERIFY(constSource->CachedDynamicCastAllowed_);
+
+ // We cannot have TSyncMap as singleton directly because we need separate cache for each instantiated template of this method.
+ struct CacheHolder
+ {
+ NConcurrency::TSyncMap<std::type_index, ptrdiff_t> OffsetCache;
+ };
+ auto holder = LeakySingleton<CacheHolder>();
+
+ // TODO(renadeen): is there a better way to use same function for const and non-const contexts?
+ auto* source = const_cast<TYsonStructBase*>(constSource);
+ ptrdiff_t* offset = holder->OffsetCache.FindOrInsert(std::type_index(typeid(*source)), [=] () {
+ auto* target = dynamic_cast<TTargetStruct*>(source);
+ // NB: Unfortunately, it is possible that dynamic cast fails.
+ // For example, when class is derived from template class with `const char *` template parameter
+ // and variable with internal linkage is used for this parameter.
+ YT_VERIFY(target);
+ return reinterpret_cast<intptr_t>(target) - reinterpret_cast<intptr_t>(source);
+ }).first;
+ return reinterpret_cast<TTargetStruct*>(reinterpret_cast<intptr_t>(source) + *offset);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TStruct>
+TYsonStructRegistrar<TStruct>::TYsonStructRegistrar(IYsonStructMeta* meta)
+ : Meta_(meta)
+{ }
+
+template <class TStruct>
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructRegistrar<TStruct>::Parameter(const TString& key, TValue(TStruct::*field))
+{
+ return BaseClassParameter<TStruct, TValue>(key, field);
+}
+
+template <class TStruct>
+template <class TBase, class TValue>
+TYsonStructParameter<TValue>& TYsonStructRegistrar<TStruct>::BaseClassParameter(const TString& key, TValue(TBase::*field))
+{
+ static_assert(std::is_base_of<TBase, TStruct>::value);
+ auto parameter = New<TYsonStructParameter<TValue>>(key, std::make_unique<TYsonFieldAccessor<TBase, TValue>>(field));
+ Meta_->RegisterParameter(key, parameter);
+ return *parameter;
+}
+
+template <class TStruct>
+void TYsonStructRegistrar<TStruct>::Preprocessor(std::function<void(TStruct*)> preprocessor)
+{
+ Meta_->RegisterPreprocessor([preprocessor = std::move(preprocessor)] (TYsonStructBase* target) {
+ preprocessor(TYsonStructRegistry::Get()->template CachedDynamicCast<TStruct>(target));
+ });
+}
+
+template <class TStruct>
+void TYsonStructRegistrar<TStruct>::Postprocessor(std::function<void(TStruct*)> postprocessor)
+{
+ Meta_->RegisterPostprocessor([postprocessor = std::move(postprocessor)] (TYsonStructBase* target) {
+ postprocessor(TYsonStructRegistry::Get()->template CachedDynamicCast<TStruct>(target));
+ });
+}
+
+template <class TStruct>
+void TYsonStructRegistrar<TStruct>::UnrecognizedStrategy(EUnrecognizedStrategy strategy)
+{
+ Meta_->SetUnrecognizedStrategy(strategy);
+}
+
+template <class TStruct>
+template<class TBase>
+TYsonStructRegistrar<TStruct>::operator TYsonStructRegistrar<TBase>()
+{
+ static_assert(std::is_base_of<TBase, TStruct>::value);
+ return TYsonStructRegistrar<TBase>(Meta_);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> CloneYsonStruct(const TIntrusivePtr<const T>& obj)
+{
+ if (!obj) {
+ return nullptr;
+ }
+ return ConvertTo<TIntrusivePtr<T>>(NYson::ConvertToYsonString(*obj));
+}
+
+template <class T>
+TIntrusivePtr<T> CloneYsonStruct(const TIntrusivePtr<T>& obj)
+{
+ return CloneYsonStruct(ConstPointerCast<const T>(obj));
+}
+
+template <class T>
+std::vector<TIntrusivePtr<T>> CloneYsonStructs(const std::vector<TIntrusivePtr<T>>& objs)
+{
+ std::vector<TIntrusivePtr<T>> clonedObjs;
+ clonedObjs.reserve(objs.size());
+ for (const auto& obj : objs) {
+ clonedObjs.push_back(CloneYsonStruct(obj));
+ }
+ return clonedObjs;
+}
+
+template <class T>
+THashMap<TString, TIntrusivePtr<T>> CloneYsonStructs(const THashMap<TString, TIntrusivePtr<T>>& objs)
+{
+ THashMap<TString, TIntrusivePtr<T>> clonedObjs;
+ clonedObjs.reserve(objs.size());
+ for (const auto& [key, obj] : objs) {
+ clonedObjs.emplace(key, CloneYsonStruct(obj));
+ }
+ return clonedObjs;
+}
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonStruct(
+ const TIntrusivePtr<T>& obj,
+ const INodePtr& patch)
+{
+ static_assert(
+ std::convertible_to<T*, TYsonStruct*>,
+ "'obj' must be convertible to TYsonStruct");
+
+ if (patch) {
+ return ConvertTo<TIntrusivePtr<T>>(PatchNode(ConvertTo<INodePtr>(obj), patch));
+ } else {
+ return CloneYsonStruct(obj);
+ }
+}
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonStruct(
+ const TIntrusivePtr<T>& obj,
+ const NYson::TYsonString& patch)
+{
+ if (!patch) {
+ return obj;
+ }
+
+ return UpdateYsonStruct(obj, ConvertToNode(patch));
+}
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const NYson::TYsonString& newConfigYson)
+{
+ return ReconfigureYsonStruct(config, ConvertToNode(newConfigYson));
+}
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const TIntrusivePtr<T>& newConfig)
+{
+ return ReconfigureYsonStruct(config, ConvertToNode(newConfig));
+}
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const INodePtr& newConfigNode)
+{
+ auto configNode = ConvertToNode(config);
+
+ auto newConfig = ConvertTo<TIntrusivePtr<T>>(newConfigNode);
+ auto newCanonicalConfigNode = ConvertToNode(newConfig);
+
+ if (NYTree::AreNodesEqual(configNode, newCanonicalConfigNode)) {
+ return false;
+ }
+
+ config->Load(newConfigNode, /*postprocess*/ true, /*setDefaults*/ false);
+ return true;
+}
+
+template <class TSrc, class TDst>
+void UpdateYsonStructField(TDst& dst, const std::optional<TSrc>& src)
+{
+ if (src) {
+ dst = *src;
+ }
+}
+
+template <class TSrc, class TDst>
+void UpdateYsonStructField(TIntrusivePtr<TDst>& dst, const TIntrusivePtr<TSrc>& src)
+{
+ if (src) {
+ dst = src;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_struct.cpp b/yt/yt/core/ytree/yson_struct.cpp
new file mode 100644
index 0000000000..4e6250daa0
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct.cpp
@@ -0,0 +1,219 @@
+#include "yson_struct.h"
+
+#include <yt/yt/core/yson/consumer.h>
+
+#include <yt/yt/core/ytree/node.h>
+#include <yt/yt/core/ytree/ypath_detail.h>
+
+#include <yt/yt/core/misc/singleton.h>
+
+#include <util/generic/algorithm.h>
+
+namespace NYT::NYTree {
+
+using namespace NYPath;
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+IMapNodePtr TYsonStructBase::GetLocalUnrecognized() const
+{
+ return LocalUnrecognized_;
+}
+
+IMapNodePtr TYsonStructBase::GetRecursiveUnrecognized() const
+{
+ return Meta_->GetRecursiveUnrecognized(this);
+}
+
+void TYsonStructBase::SetUnrecognizedStrategy(EUnrecognizedStrategy strategy)
+{
+ InstanceUnrecognizedStrategy_ = strategy;
+}
+
+THashSet<TString> TYsonStructBase::GetRegisteredKeys() const
+{
+ return Meta_->GetRegisteredKeys();
+}
+
+void TYsonStructBase::Load(
+ INodePtr node,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path)
+{
+ Meta_->LoadStruct(this, node, postprocess, setDefaults, path);
+}
+
+void TYsonStructBase::Load(
+ TYsonPullParserCursor* cursor,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path)
+{
+ Meta_->LoadStruct(this, cursor, postprocess, setDefaults, path);
+}
+
+void TYsonStructBase::Load(IInputStream* input)
+{
+ NYT::TStreamLoadContext context(input);
+ NYT::TBinaryYsonStructSerializer::Load(context, *this);
+}
+
+void TYsonStructBase::Save(
+ IYsonConsumer* consumer,
+ bool stable) const
+{
+ consumer->OnBeginMap();
+ for (const auto& [name, parameter] : Meta_->GetParameterSortedList()) {
+ if (!parameter->CanOmitValue(this)) {
+ consumer->OnKeyedItem(name);
+ parameter->Save(this, consumer);
+ }
+ }
+
+ if (LocalUnrecognized_) {
+ auto unrecognizedList = LocalUnrecognized_->GetChildren();
+ if (stable) {
+ std::sort(
+ unrecognizedList.begin(),
+ unrecognizedList.end(),
+ [] (const auto& x, const auto& y) {
+ return x.first < y.first;
+ });
+ }
+ for (const auto& [key, child] : unrecognizedList) {
+ consumer->OnKeyedItem(key);
+ Serialize(child, consumer);
+ }
+ }
+
+ consumer->OnEndMap();
+}
+
+void TYsonStructBase::Save(IOutputStream* output) const
+{
+ NYT::TStreamSaveContext context(output);
+ NYT::TBinaryYsonStructSerializer::Save(context, *this);
+ context.Finish();
+}
+
+void TYsonStructBase::Postprocess(const TYPath& path)
+{
+ Meta_->Postprocess(this, path);
+}
+
+void TYsonStructBase::SetDefaults()
+{
+ YT_VERIFY(Meta_);
+ Meta_->SetDefaultsOfInitializedStruct(this);
+}
+
+void TYsonStructBase::SaveParameter(const TString& key, IYsonConsumer* consumer) const
+{
+ Meta_->GetParameter(key)->Save(this, consumer);
+}
+
+void TYsonStructBase::LoadParameter(const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy)
+{
+ Meta_->LoadParameter(this, key, node, mergeStrategy);
+}
+
+void TYsonStructBase::ResetParameter(const TString& key)
+{
+ Meta_->GetParameter(key)->SetDefaultsInitialized(this);
+}
+
+int TYsonStructBase::GetParameterCount() const
+{
+ return Meta_->GetParameterMap().size();
+}
+
+std::vector<TString> TYsonStructBase::GetAllParameterAliases(const TString& key) const
+{
+ auto parameter = Meta_->GetParameter(key);
+ auto result = parameter->GetAliases();
+ result.push_back(parameter->GetKey());
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonStruct::InitializeRefCounted()
+{
+ if (!TYsonStructRegistry::InitializationInProgress()) {
+ SetDefaults();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonStructRegistry* TYsonStructRegistry::Get()
+{
+ return LeakySingleton<TYsonStructRegistry>();
+}
+
+bool TYsonStructRegistry::InitializationInProgress()
+{
+ return CurrentlyInitializingMeta_ != nullptr;
+}
+
+TYsonStructRegistry::TForbidCachedDynamicCastGuard::TForbidCachedDynamicCastGuard(TYsonStructBase* target)
+ : Target_(target)
+{
+ Target_->CachedDynamicCastAllowed_ = false;
+}
+
+TYsonStructRegistry::TForbidCachedDynamicCastGuard::~TForbidCachedDynamicCastGuard()
+{
+ Target_->CachedDynamicCastAllowed_ = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void Serialize(const TYsonStructBase& value, IYsonConsumer* consumer)
+{
+ value.Save(consumer);
+}
+
+void Deserialize(TYsonStructBase& value, INodePtr node)
+{
+ value.Load(node);
+}
+
+void Deserialize(TYsonStructBase& value, TYsonPullParserCursor* cursor)
+{
+ value.Load(cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+DEFINE_REFCOUNTED_TYPE(TYsonStruct)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+
+namespace NYT {
+
+using namespace NYTree;
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TBinaryYsonStructSerializer::Save(TStreamSaveContext& context, const TYsonStructBase& obj)
+{
+ auto str = ConvertToYsonString(obj);
+ NYT::Save(context, str);
+}
+
+void TBinaryYsonStructSerializer::Load(TStreamLoadContext& context, TYsonStructBase& obj)
+{
+ auto str = NYT::Load<TYsonString>(context);
+ auto node = ConvertTo<INodePtr>(str);
+ obj.Load(node);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/yt/yt/core/ytree/yson_struct.h b/yt/yt/core/ytree/yson_struct.h
new file mode 100644
index 0000000000..78c209b9a6
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct.h
@@ -0,0 +1,313 @@
+#pragma once
+
+#include "node.h"
+#include "yson_serialize_common.h"
+
+#include <yt/yt/core/misc/error.h>
+#include <yt/yt/core/misc/mpl.h>
+#include <yt/yt/core/misc/property.h>
+
+#include <yt/yt/core/yson/public.h>
+#include <yt/yt/library/syncmap/map.h>
+
+#include <util/generic/algorithm.h>
+
+#include <functional>
+#include <optional>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Base class for structs that are meant to be serialized to or deserialized from YSON.
+/*!
+ * Usually this class is used for various configs.
+ * To use it inherit TYsonStruct and add `REGISTER_YSON_STRUCT(TYourClass)` to your class declaration
+ * for ref-counted struct or inherit TYsonStructLite and add `REGISTER_YSON_STRUCT_LITE(TYourClass);`
+ * for non-ref-counted struct. Then configure fields in static method TYourClass::Register(TRegistrar registrar).
+ * Various examples can be found in yt/yt/core/ytree/unittests/yson_struct_ut.cpp
+ * Machinery behind this class will cache all configuration data in global variables during first constructor call
+ * and will use it for serialization to and deserialization from YSON.
+ * Initialization of fields with default values takes place outside of constructor because the machinery
+ * behind this class caches dynamic casts and a cache cannot be used for an object under construction
+ * since object layout depends on actual class under construction (most derived)
+ * and in base class constructor it is impossible to determine which class is constructed.
+ * https://en.cppreference.com/w/cpp/language/typeid
+ * "If typeid is used on an object under construction or destruction ...
+ * then the std::type_info object referred to by this typeid represents the class
+ * that is being constructed or destroyed even if it is not the most-derived class."
+ * Ref-counted structs are initialized in New<...> (InitializeRefCounted method is called).
+ * Non-ref-counted structs are initialized in factory method TYourClass::Create()
+ * which is generated by the macro REGISTER_YSON_STRUCT_LITE.
+ *
+ * The key difference from TYsonSerializable is that the latter builds the whole meta every time
+ * an instance of the class is being constructed
+ * while TYsonStruct builds meta only once just before construction of the first instance.
+ */
+class TYsonStructBase
+{
+public:
+ using TPostprocessor = std::function<void()>;
+ using TPreprocessor = std::function<void()>;
+
+ virtual ~TYsonStructBase() = default;
+
+ void Load(
+ NYTree::INodePtr node,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = {});
+
+ void Load(
+ NYson::TYsonPullParserCursor* cursor,
+ bool postprocess = true,
+ bool setDefaults = true,
+ const NYPath::TYPath& path = {});
+
+ void Load(IInputStream* input);
+
+ void Postprocess(const NYPath::TYPath& path = {});
+
+ void SetDefaults();
+
+ void Save(
+ NYson::IYsonConsumer* consumer,
+ bool stable = false) const;
+
+ void Save(IOutputStream* output) const;
+
+ IMapNodePtr GetLocalUnrecognized() const;
+ IMapNodePtr GetRecursiveUnrecognized() const;
+
+ void SetUnrecognizedStrategy(EUnrecognizedStrategy strategy);
+
+ THashSet<TString> GetRegisteredKeys() const;
+ int GetParameterCount() const;
+
+ // TODO(renadeen): remove this methods.
+ void SaveParameter(const TString& key, NYson::IYsonConsumer* consumer) const;
+ void LoadParameter(const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy);
+ void ResetParameter(const TString& key);
+
+ std::vector<TString> GetAllParameterAliases(const TString& key) const;
+
+private:
+ template <class TValue>
+ friend class TYsonStructParameter;
+
+ friend class TYsonStructRegistry;
+
+ friend class TYsonStructMeta;
+
+ friend class TYsonStruct;
+
+ IYsonStructMeta* Meta_ = nullptr;
+
+ // Unrecognized parameters of this struct (not recursive).
+ NYTree::IMapNodePtr LocalUnrecognized_;
+ std::optional<EUnrecognizedStrategy> InstanceUnrecognizedStrategy_;
+
+ bool CachedDynamicCastAllowed_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStruct
+ : public TRefCounted
+ , public TYsonStructBase
+{
+public:
+ void InitializeRefCounted();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStructLite
+ : public TYsonStructBase
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStructRegistry
+{
+public:
+ static TYsonStructRegistry* Get();
+
+ static bool InitializationInProgress();
+
+ template <class TStruct>
+ void InitializeStruct(TStruct* target);
+
+private:
+ static inline thread_local IYsonStructMeta* CurrentlyInitializingMeta_ = nullptr;
+
+ template <class TStruct>
+ friend class TYsonStructRegistrar;
+
+ template <class TStruct, class TValue>
+ friend class TYsonFieldAccessor;
+
+ //! Performs dynamic cast using thread safe cache.
+ /*!
+ * We need a lot of dynamic casts and they can be expensive for large type hierarchies.
+ * This method casts from TYsonStructBase* to TTargetStruct* via thread-safe cache.
+ * Cache has two keys — TTargetStruct and typeid(source) — and offset from source to target as value.
+ * Due to virtual inheritance, offset between source and target depends on actual type of source.
+ * We get actual type using `typeid(...)` but this function has limitation
+ * that it doesn't return actual type in constructors and destructors (https://en.cppreference.com/w/cpp/language/typeid).
+ * So we cannot use this function in constructors and destructors.
+ */
+ template <class TTargetStruct>
+ TTargetStruct* CachedDynamicCast(const TYsonStructBase* source);
+
+ class TForbidCachedDynamicCastGuard
+ {
+ public:
+ explicit TForbidCachedDynamicCastGuard(TYsonStructBase* target);
+ ~TForbidCachedDynamicCastGuard();
+
+ private:
+ TYsonStructBase* const Target_;
+ };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+class TYsonStructParameter;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TStruct>
+class TYsonStructRegistrar
+{
+public:
+ explicit TYsonStructRegistrar(IYsonStructMeta* meta);
+
+ template <class TValue>
+ TYsonStructParameter<TValue>& Parameter(const TString& key, TValue(TStruct::*field));
+
+ template <class TBase, class TValue>
+ TYsonStructParameter<TValue>& BaseClassParameter(const TString& key, TValue(TBase::*field));
+
+ void Preprocessor(std::function<void(TStruct*)> preprocessor);
+
+ void Postprocessor(std::function<void(TStruct*)> postprocessor);
+
+ void UnrecognizedStrategy(EUnrecognizedStrategy strategy);
+
+ template<class TBase>
+ operator TYsonStructRegistrar<TBase>();
+
+private:
+ IYsonStructMeta* const Meta_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+TIntrusivePtr<T> CloneYsonStruct(const TIntrusivePtr<const T>& obj);
+template <class T>
+TIntrusivePtr<T> CloneYsonStruct(const TIntrusivePtr<T>& obj);
+template <class T>
+std::vector<TIntrusivePtr<T>> CloneYsonStructs(const std::vector<TIntrusivePtr<T>>& objs);
+template <class T>
+THashMap<TString, TIntrusivePtr<T>> CloneYsonStructs(const THashMap<TString, TIntrusivePtr<T>>& objs);
+
+void Serialize(const TYsonStructBase& value, NYson::IYsonConsumer* consumer);
+void Deserialize(TYsonStructBase& value, INodePtr node);
+void Deserialize(TYsonStructBase& value, NYson::TYsonPullParserCursor* cursor);
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonStruct(
+ const TIntrusivePtr<T>& obj,
+ const INodePtr& patch);
+
+template <class T>
+TIntrusivePtr<T> UpdateYsonStruct(
+ const TIntrusivePtr<T>& obj,
+ const NYson::TYsonString& patch);
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const NYson::TYsonString& newConfigYson);
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const TIntrusivePtr<T>& newConfig);
+
+template <class T>
+bool ReconfigureYsonStruct(
+ const TIntrusivePtr<T>& config,
+ const INodePtr& newConfigNode);
+
+template <class TSrc, class TDst>
+void UpdateYsonStructField(TDst& dst, const std::optional<TSrc>& src);
+template <class TSrc, class TDst>
+void UpdateYsonStructField(TIntrusivePtr<TDst>& dst, const TIntrusivePtr<TSrc>& src);
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define REGISTER_YSON_STRUCT_IMPL(TStruct) \
+ TStruct() \
+ { \
+ ::NYT::NYTree::TYsonStructRegistry::Get()->InitializeStruct(this); \
+ } \
+ \
+private: \
+ using TRegistrar = ::NYT::NYTree::TYsonStructRegistrar<TStruct>; \
+ using TThis = TStruct; \
+ friend class ::NYT::NYTree::TYsonStructRegistry
+
+#define REGISTER_YSON_STRUCT(TStruct) \
+public: \
+REGISTER_YSON_STRUCT_IMPL(TStruct)
+
+#define REGISTER_YSON_STRUCT_LITE(TStruct) \
+public: \
+ \
+ static TStruct Create() { \
+ static_assert(std::is_base_of_v<::NYT::NYTree::TYsonStructLite, TStruct>, "Class must inherit from TYsonStructLite"); \
+ TStruct result; \
+ result.SetDefaults(); \
+ return result; \
+ } \
+ \
+template <class T> \
+friend const std::type_info& ::NYT::NYTree::CallCtor(); \
+ \
+protected: \
+REGISTER_YSON_STRUCT_IMPL(TStruct)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TBinaryYsonStructSerializer
+{
+ static void Save(TStreamSaveContext& context, const NYTree::TYsonStructBase& obj);
+ static void Load(TStreamLoadContext& context, NYTree::TYsonStructBase& obj);
+};
+
+template <class T, class C>
+struct TSerializerTraits<
+ T,
+ C,
+ typename std::enable_if_t<std::is_convertible_v<T&, NYTree::TYsonStructBase&>>>
+{
+ typedef TBinaryYsonStructSerializer TSerializer;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
+
+#define YSON_STRUCT_INL_H_
+#include "yson_struct-inl.h"
+#undef YSON_STRUCT_INL_H_
diff --git a/yt/yt/core/ytree/yson_struct_detail-inl.h b/yt/yt/core/ytree/yson_struct_detail-inl.h
new file mode 100644
index 0000000000..1899b2137b
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct_detail-inl.h
@@ -0,0 +1,911 @@
+#ifndef YSON_STRUCT_DETAIL_INL_H_
+#error "Direct inclusion of this file is not allowed, include yson_struct_detail.h"
+// For the sake of sane code completion.
+#include "yson_struct_detail.h"
+#endif
+
+#include "ypath_client.h"
+#include "yson_struct.h"
+
+#include <yt/yt/core/yson/token_writer.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NPrivate {
+
+template <class T>
+concept IsYsonStructOrYsonSerializable = std::is_base_of_v<TYsonStructBase, T> || std::is_base_of_v<TYsonSerializableLite, T>;
+
+// TODO(shakurov): get rid of this once concept support makes it into the standard
+// library implementation. Use equality-comparability instead.
+template <class T>
+concept SupportsDontSerializeDefaultImpl =
+ std::is_arithmetic_v<T> ||
+ std::is_same_v<T, TString> ||
+ std::is_same_v<T, TDuration> ||
+ std::is_same_v<T, TGuid> ||
+ std::is_same_v<T, std::optional<std::vector<TString>>> ||
+ std::is_same_v<T, THashSet<TString>>;
+
+template <class T>
+concept SupportsDontSerializeDefault =
+ SupportsDontSerializeDefaultImpl<T> ||
+ TStdOptionalTraits<T>::IsStdOptional &&
+ SupportsDontSerializeDefaultImpl<typename TStdOptionalTraits<T>::TValueType>;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void LoadFromNode(
+ T& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy /*mergeStrategy*/,
+ std::optional<EUnrecognizedStrategy> /*recursiveUnrecognizedStrategy*/)
+{
+ try {
+ Deserialize(parameter, node);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error reading parameter %v", path)
+ << ex;
+ }
+}
+
+// INodePtr
+template <>
+inline void LoadFromNode(
+ NYTree::INodePtr& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& /*path*/,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> /*recursiveUnrecognizedStrategy*/)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter = node;
+ break;
+ }
+
+ case EMergeStrategy::Combine: {
+ if (!parameter) {
+ parameter = node;
+ } else {
+ parameter = PatchNode(parameter, node);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromNode(
+ TIntrusivePtr<T>& parameterValue,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ if (!parameterValue || mergeStrategy == EMergeStrategy::Overwrite) {
+ parameterValue = New<T>();
+ }
+
+ if (recursiveUnrecognizedStrategy) {
+ parameterValue->SetUnrecognizedStrategy(*recursiveUnrecognizedStrategy);
+ }
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite:
+ case EMergeStrategy::Combine: {
+ parameterValue->Load(node, false, false, path);
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::optional
+template <class T>
+void LoadFromNode(
+ std::optional<T>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ if (node->GetType() == NYTree::ENodeType::Entity) {
+ parameter = std::nullopt;
+ } else {
+ T value;
+ LoadFromNode(value, node, path, EMergeStrategy::Overwrite, recursiveUnrecognizedStrategy);
+ parameter = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::vector
+template <class... T>
+void LoadFromNode(
+ std::vector<T...>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ auto listNode = node->AsList();
+ auto size = listNode->GetChildCount();
+ parameter.resize(size);
+ for (int i = 0; i < size; ++i) {
+ LoadFromNode(
+ parameter[i],
+ listNode->GetChildOrThrow(i),
+ path + "/" + NYPath::ToYPathLiteral(i),
+ EMergeStrategy::Overwrite,
+ recursiveUnrecognizedStrategy);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+template <class T>
+T DeserializeMapKey(TStringBuf value)
+{
+ if constexpr (TEnumTraits<T>::IsEnum) {
+ return ParseEnum<T>(value);
+ } else if constexpr (std::is_same_v<T, TGuid>) {
+ return TGuid::FromString(value);
+ } else if constexpr (TStrongTypedefTraits<T>::IsStrongTypedef) {
+ return T(DeserializeMapKey<typename TStrongTypedefTraits<T>::TUnderlying>(value));
+ } else {
+ return FromString<T>(value);
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class M = typename Map<T...>::mapped_type>
+void LoadFromNode(
+ Map<T...>& parameter,
+ NYTree::INodePtr node,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ auto mapNode = node->AsMap();
+ parameter.clear();
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ M value;
+ LoadFromNode(
+ value,
+ child,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ EMergeStrategy::Overwrite,
+ recursiveUnrecognizedStrategy);
+ parameter.emplace(DeserializeMapKey<typename Map<T...>::key_type>(key), std::move(value));
+ }
+ break;
+ }
+ case EMergeStrategy::Combine: {
+ auto mapNode = node->AsMap();
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ M value;
+ LoadFromNode(
+ value,
+ child,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ EMergeStrategy::Combine,
+ recursiveUnrecognizedStrategy);
+ parameter[DeserializeMapKey<typename Map<T...>::key_type>(key)] = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void LoadFromCursor(
+ T& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy /*mergeStrategy*/,
+ std::optional<EUnrecognizedStrategy> /*recursiveUnrecognizedStrategy*/)
+{
+ try {
+ Deserialize(parameter, cursor);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error reading parameter %v", path)
+ << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromCursor(
+ TIntrusivePtr<T>& parameterValue,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy);
+
+template <class... T>
+void LoadFromCursor(
+ std::vector<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy);
+
+// std::optional
+template <class T>
+void LoadFromCursor(
+ std::optional<T>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy);
+
+template <template <typename...> class Map, class... T, class M = typename Map<T...>::mapped_type>
+void LoadFromCursor(
+ Map<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy);
+
+////////////////////////////////////////////////////////////////////////////////
+
+// INodePtr
+template <>
+inline void LoadFromCursor(
+ NYTree::INodePtr& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ try {
+ auto node = NYson::ExtractTo<INodePtr>(cursor);
+ LoadFromNode(parameter, std::move(node), path, mergeStrategy, recursiveUnrecognizedStrategy);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T>
+void LoadFromCursor(
+ TIntrusivePtr<T>& parameterValue,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ if (!parameterValue || mergeStrategy == EMergeStrategy::Overwrite) {
+ parameterValue = New<T>();
+ }
+
+ if (recursiveUnrecognizedStrategy) {
+ parameterValue->SetUnrecognizedStrategy(*recursiveUnrecognizedStrategy);
+ }
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite:
+ case EMergeStrategy::Combine: {
+ parameterValue->Load(cursor, false, false, path);
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+}
+
+// std::optional
+template <class T>
+void LoadFromCursor(
+ std::optional<T>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ try {
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ if ((*cursor)->GetType() == NYson::EYsonItemType::EntityValue) {
+ parameter = std::nullopt;
+ cursor->Next();
+ } else {
+ T value;
+ LoadFromCursor(value, cursor, path, EMergeStrategy::Overwrite, recursiveUnrecognizedStrategy);
+ parameter = std::move(value);
+ }
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// std::vector
+template <class... T>
+void LoadFromCursor(
+ std::vector<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ try {
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter.clear();
+ int index = 0;
+ cursor->ParseList([&](NYson::TYsonPullParserCursor* cursor) {
+ LoadFromCursor(
+ parameter.emplace_back(),
+ cursor,
+ path + "/" + NYPath::ToYPathLiteral(index),
+ EMergeStrategy::Overwrite,
+ recursiveUnrecognizedStrategy);
+ ++index;
+ });
+ break;
+ }
+
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class M>
+void LoadFromCursor(
+ Map<T...>& parameter,
+ NYson::TYsonPullParserCursor* cursor,
+ const NYPath::TYPath& path,
+ EMergeStrategy mergeStrategy,
+ std::optional<EUnrecognizedStrategy> recursiveUnrecognizedStrategy)
+{
+ try {
+ auto doParse = [&] (const auto& setterOrEmplacer, EMergeStrategy mergeStrategy) {
+ cursor->ParseMap([&] (NYson::TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<TString>(cursor);
+ M value;
+ LoadFromCursor(
+ value,
+ cursor,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ mergeStrategy,
+ recursiveUnrecognizedStrategy);
+ setterOrEmplacer(key, std::move(value));
+ });
+ };
+
+ switch (mergeStrategy) {
+ case EMergeStrategy::Default:
+ case EMergeStrategy::Overwrite: {
+ parameter.clear();
+ auto emplacer = [&] (auto key, M&& value) {
+ parameter.emplace(DeserializeMapKey<typename Map<T...>::key_type>(key), std::move(value));
+ };
+ doParse(emplacer, EMergeStrategy::Overwrite);
+ break;
+ }
+ case EMergeStrategy::Combine: {
+ auto setter = [&] (auto key, M&& value) {
+ parameter[DeserializeMapKey<typename Map<T...>::key_type>(key)] = std::move(value);
+ };
+ doParse(setter, EMergeStrategy::Combine);
+ break;
+ }
+ default:
+ YT_UNIMPLEMENTED();
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Error loading parameter %v", path)
+ << ex;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// For all classes except descendants of TYsonStructBase and their intrusive pointers
+// we do not attempt to extract unrecognized members. C++ prohibits function template specialization
+// so we have to deal with static struct members.
+template <class T>
+struct TGetRecursiveUnrecognized
+{
+ static IMapNodePtr Do(const T& /*parameter*/)
+ {
+ return nullptr;
+ }
+};
+
+template <IsYsonStructOrYsonSerializable T>
+struct TGetRecursiveUnrecognized<T>
+{
+ static IMapNodePtr Do(const T& parameter)
+ {
+ return parameter.GetRecursiveUnrecognized();
+ }
+};
+
+template <IsYsonStructOrYsonSerializable T>
+struct TGetRecursiveUnrecognized<TIntrusivePtr<T>>
+{
+ static IMapNodePtr Do(const TIntrusivePtr<T>& parameter)
+ {
+ return parameter ? parameter->GetRecursiveUnrecognized() : nullptr;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// all
+template <class F>
+void InvokeForComposites(
+ const void* /*parameter*/,
+ const NYPath::TYPath& /*path*/,
+ const F& /*func*/)
+{ }
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T, class F>
+inline void InvokeForComposites(
+ const TIntrusivePtr<T>* parameterValue,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ func(*parameterValue, path);
+}
+
+// std::vector
+template <class... T, class F>
+inline void InvokeForComposites(
+ const std::vector<T...>* parameter,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ for (size_t i = 0; i < parameter->size(); ++i) {
+ InvokeForComposites(
+ &(*parameter)[i],
+ path + "/" + NYPath::ToYPathLiteral(i),
+ func);
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class F, class M = typename Map<T...>::mapped_type>
+inline void InvokeForComposites(
+ const Map<T...>* parameter,
+ const NYPath::TYPath& path,
+ const F& func)
+{
+ for (const auto& [key, value] : *parameter) {
+ InvokeForComposites(
+ &value,
+ path + "/" + NYPath::ToYPathLiteral(key),
+ func);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// all
+template <class F>
+void InvokeForComposites(
+ const void* /* parameter */,
+ const F& /* func */)
+{ }
+
+// TYsonStruct or TYsonSerializable
+template <IsYsonStructOrYsonSerializable T, class F>
+inline void InvokeForComposites(const TIntrusivePtr<T>* parameter, const F& func)
+{
+ func(*parameter);
+}
+
+// std::vector
+template <class... T, class F>
+inline void InvokeForComposites(const std::vector<T...>* parameter, const F& func)
+{
+ for (const auto& item : *parameter) {
+ InvokeForComposites(&item, func);
+ }
+}
+
+// For any map.
+template <template <typename...> class Map, class... T, class F, class M = typename Map<T...>::mapped_type>
+inline void InvokeForComposites(const Map<T...>* parameter, const F& func)
+{
+ for (const auto& [key, value] : *parameter) {
+ InvokeForComposites(&value, func);
+ }
+}
+
+template <class T, class = void>
+struct IsYsonStructPtr : std::false_type
+{ };
+
+template <class T>
+struct IsYsonStructPtr<TIntrusivePtr<T>, typename std::enable_if<std::is_convertible<T&, TYsonStruct&>::value>::type> : std::true_type
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYsonStructDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TStruct, class TValue>
+TYsonFieldAccessor<TStruct, TValue>::TYsonFieldAccessor(TYsonStructField<TStruct, TValue> field)
+ : Field_(field)
+{ }
+
+template <class TStruct, class TValue>
+TValue& TYsonFieldAccessor<TStruct, TValue>::GetValue(const TYsonStructBase* source)
+{
+ return TYsonStructRegistry::Get()->template CachedDynamicCast<TStruct>(source)->*Field_;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+TYsonStructParameter<TValue>::TYsonStructParameter(TString key, std::unique_ptr<IYsonFieldAccessor<TValue>> fieldAccessor)
+ : Key_(std::move(key))
+ , FieldAccessor_(std::move(fieldAccessor))
+ , MergeStrategy_(EMergeStrategy::Default)
+{ }
+
+template <class TValue>
+void TYsonStructParameter<TValue>::Load(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options)
+{
+ if (node) {
+ NPrivate::LoadFromNode(
+ FieldAccessor_->GetValue(self),
+ std::move(node),
+ options.Path,
+ options.MergeStrategy.value_or(MergeStrategy_),
+ options.RecursiveUnrecognizedRecursively);
+ } else if (!DefaultCtor_) {
+ THROW_ERROR_EXCEPTION("Missing required parameter %v",
+ options.Path);
+ }
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::SafeLoad(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate)
+{
+ if (node) {
+ TValue oldValue = FieldAccessor_->GetValue(self);
+ try {
+ NPrivate::LoadFromNode(
+ FieldAccessor_->GetValue(self),
+ node,
+ options.Path,
+ options.MergeStrategy.value_or(MergeStrategy_),
+ /*recursivelyUnrecognizedStrategy*/ std::nullopt);
+ validate();
+ } catch (const std::exception ex) {
+ FieldAccessor_->GetValue(self) = oldValue;
+ throw;
+ }
+ }
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::Load(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options)
+{
+ if (cursor) {
+ NPrivate::LoadFromCursor(
+ FieldAccessor_->GetValue(self),
+ cursor,
+ options.Path,
+ options.MergeStrategy.value_or(MergeStrategy_),
+ options.RecursiveUnrecognizedRecursively);
+ } else if (!DefaultCtor_) {
+ THROW_ERROR_EXCEPTION("Missing required parameter %v",
+ options.Path);
+ }
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::SafeLoad(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate)
+{
+ if (cursor) {
+ TValue oldValue = FieldAccessor_->GetValue(self);
+ try {
+ NPrivate::LoadFromCursor(
+ FieldAccessor_->GetValue(self),
+ cursor,
+ options.Path,
+ options.MergeStrategy.value_or(MergeStrategy_),
+ /*recursivelyUnrecognizedStrategy*/ std::nullopt);
+ validate();
+ } catch (const std::exception ex) {
+ FieldAccessor_->GetValue(self) = oldValue;
+ throw;
+ }
+ }
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::Postprocess(const TYsonStructBase* self, const NYPath::TYPath& path) const
+{
+ const auto& value = FieldAccessor_->GetValue(self);
+ for (const auto& postprocessor : Postprocessors_) {
+ try {
+ postprocessor(value);
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Postprocess failed at %v",
+ path.empty() ? "root" : path)
+ << ex;
+ }
+ }
+
+ NPrivate::InvokeForComposites(
+ &value,
+ path,
+ [] <NPrivate::IsYsonStructOrYsonSerializable T> (TIntrusivePtr<T> obj, const NYPath::TYPath& subpath) {
+ if (obj) {
+ obj->Postprocess(subpath);
+ }
+ });
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::SetDefaultsInitialized(TYsonStructBase* self)
+{
+ TValue& value = FieldAccessor_->GetValue(self);
+
+ if (DefaultCtor_) {
+ value = (*DefaultCtor_)();
+ }
+
+ NPrivate::InvokeForComposites(
+ &value,
+ [] <NPrivate::IsYsonStructOrYsonSerializable T> (TIntrusivePtr<T> obj) {
+ if (obj) {
+ obj->SetDefaults();
+ }
+ });
+}
+
+template <class TValue>
+void TYsonStructParameter<TValue>::Save(const TYsonStructBase* self, NYson::IYsonConsumer* consumer) const
+{
+ using NYTree::Serialize;
+ Serialize(FieldAccessor_->GetValue(self), consumer);
+}
+
+template <class TValue>
+bool TYsonStructParameter<TValue>::CanOmitValue(const TYsonStructBase* self) const
+{
+ const auto& value = FieldAccessor_->GetValue(self);
+ if constexpr (NPrivate::SupportsDontSerializeDefault<TValue>) {
+ if (!SerializeDefault_ && value == (*DefaultCtor_)()) {
+ return true;
+ }
+ }
+
+ if (!DefaultCtor_) {
+ return NYT::NYTree::NDetail::CanOmitValue(&value, nullptr);
+ }
+
+ if (TriviallyInitializedIntrusivePtr_) {
+ return false;
+ }
+
+ auto defaultValue = (*DefaultCtor_)();
+ return NYT::NYTree::NDetail::CanOmitValue(&value, &defaultValue);
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::Alias(const TString& name)
+{
+ Aliases_.push_back(name);
+ return *this;
+}
+
+template <class TValue>
+const std::vector<TString>& TYsonStructParameter<TValue>::GetAliases() const
+{
+ return Aliases_;
+}
+
+template <class TValue>
+const TString& TYsonStructParameter<TValue>::GetKey() const
+{
+ return Key_;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::Optional()
+{
+ DefaultCtor_ = [] () { return TValue{}; };
+ return *this;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::Default(TValue defaultValue)
+{
+ static_assert(!std::is_convertible_v<TValue, TIntrusivePtr<TYsonStruct>>, "Use DefaultCtor to register TYsonStruct default.");
+ DefaultCtor_ = [value = std::move(defaultValue)] () { return value; };
+ return *this;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::Default()
+{
+ DefaultCtor_ = [] () { return TValue{}; };
+ return *this;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::DefaultCtor(std::function<TValue()> defaultCtor)
+{
+ DefaultCtor_ = std::move(defaultCtor);
+ return *this;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::DontSerializeDefault()
+{
+ // We should check for equality-comparability here but it is rather hard
+ // to do the deep validation.
+ static_assert(
+ NPrivate::SupportsDontSerializeDefault<TValue>,
+ "DontSerializeDefault requires |Parameter| to be TString, TDuration, an arithmetic type or an optional of those");
+
+ SerializeDefault_ = false;
+ return *this;
+}
+
+template <class TValue>
+template <class... TArgs>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::DefaultNew(TArgs&&... args)
+{
+ TriviallyInitializedIntrusivePtr_ = true;
+ return DefaultCtor([=] () mutable { return New<typename TValue::TUnderlying>(std::forward<TArgs>(args)...); });
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::CheckThat(TPostprocessor postprocessor)
+{
+ Postprocessors_.push_back(std::move(postprocessor));
+ return *this;
+}
+
+template <class TValue>
+TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::MergeBy(EMergeStrategy strategy)
+{
+ MergeStrategy_ = strategy;
+ return *this;
+}
+
+template <class TValue>
+IMapNodePtr TYsonStructParameter<TValue>::GetRecursiveUnrecognized(const TYsonStructBase* self) const
+{
+ return NPrivate::TGetRecursiveUnrecognized<TValue>::Do(FieldAccessor_->GetValue(self));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Standard postprocessors
+
+#define DEFINE_POSTPROCESSOR(method, condition, error) \
+ template <class TValue> \
+ TYsonStructParameter<TValue>& TYsonStructParameter<TValue>::method \
+ { \
+ return CheckThat([=] (const TValue& parameter) { \
+ using ::ToString; \
+ std::optional<TValueType> nullableParameter(parameter); \
+ if (nullableParameter) { \
+ const auto& actual = *nullableParameter; \
+ if (!(condition)) { \
+ THROW_ERROR error; \
+ } \
+ } \
+ }); \
+ }
+
+DEFINE_POSTPROCESSOR(
+ GreaterThan(TValueType expected),
+ actual > expected,
+ TError("Expected > %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ GreaterThanOrEqual(TValueType expected),
+ actual >= expected,
+ TError("Expected >= %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ LessThan(TValueType expected),
+ actual < expected,
+ TError("Expected < %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ LessThanOrEqual(TValueType expected),
+ actual <= expected,
+ TError("Expected <= %v, found %v", expected, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ InRange(TValueType lowerBound, TValueType upperBound),
+ lowerBound <= actual && actual <= upperBound,
+ TError("Expected in range [%v,%v], found %v", lowerBound, upperBound, actual)
+)
+
+DEFINE_POSTPROCESSOR(
+ NonEmpty(),
+ actual.size() > 0,
+ TError("Value must not be empty")
+)
+
+#undef DEFINE_POSTPROCESSOR
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_struct_detail.cpp b/yt/yt/core/ytree/yson_struct_detail.cpp
new file mode 100644
index 0000000000..3b39dcf84a
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct_detail.cpp
@@ -0,0 +1,339 @@
+#include "yson_struct_detail.h"
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<EUnrecognizedStrategy> GetRecursiveUnrecognizedStrategy(EUnrecognizedStrategy strategy)
+{
+ return strategy == EUnrecognizedStrategy::KeepRecursive || strategy == EUnrecognizedStrategy::ThrowRecursive
+ ? std::make_optional(strategy)
+ : std::nullopt;
+}
+
+bool ShouldKeep(EUnrecognizedStrategy strategy)
+{
+ return strategy == EUnrecognizedStrategy::Keep || strategy == EUnrecognizedStrategy::KeepRecursive;
+}
+
+bool ShouldThrow(EUnrecognizedStrategy strategy)
+{
+ return strategy == EUnrecognizedStrategy::Throw || strategy == EUnrecognizedStrategy::ThrowRecursive;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TYsonStructMeta::SetDefaultsOfInitializedStruct(TYsonStructBase* target) const
+{
+ for (const auto& [_, parameter] : Parameters_) {
+ parameter->SetDefaultsInitialized(target);
+ }
+
+ for (const auto& preprocessor : Preprocessors_) {
+ preprocessor(target);
+ }
+}
+
+const THashSet<TString>& TYsonStructMeta::GetRegisteredKeys() const
+{
+ return RegisteredKeys_;
+}
+
+const THashMap<TString, IYsonStructParameterPtr>& TYsonStructMeta::GetParameterMap() const
+{
+ return Parameters_;
+}
+
+const std::vector<std::pair<TString, IYsonStructParameterPtr>>& TYsonStructMeta::GetParameterSortedList() const
+{
+ return SortedParameters_;
+}
+
+IYsonStructParameterPtr TYsonStructMeta::GetParameter(const TString& keyOrAlias) const
+{
+ auto it = Parameters_.find(keyOrAlias);
+ if (it != Parameters_.end()) {
+ return it->second;
+ }
+
+ for (const auto& [_, parameter] : Parameters_) {
+ if (Count(parameter->GetAliases(), keyOrAlias) > 0) {
+ return parameter;
+ }
+ }
+ THROW_ERROR_EXCEPTION("Key or alias %Qv not found in yson struct", keyOrAlias);
+}
+
+void TYsonStructMeta::LoadParameter(TYsonStructBase* target, const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy) const
+{
+ const auto& parameter = GetParameter(key);
+ auto validate = [&] () {
+ parameter->Postprocess(target, "/" + key);
+ try {
+ for (const auto& postprocessor : Postprocessors_) {
+ postprocessor(target);
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION(
+ "Postprocess failed while loading parameter %Qv from value %Qv",
+ key,
+ ConvertToYsonString(node, NYson::EYsonFormat::Text))
+ << ex;
+ }
+ };
+ auto loadOptions = TLoadParameterOptions{
+ .Path = "",
+ .MergeStrategy = mergeStrategy
+ };
+
+ parameter->SafeLoad(target, node, loadOptions, validate);
+}
+
+void TYsonStructMeta::Postprocess(TYsonStructBase* target, const TYPath& path) const
+{
+ for (const auto& [name, parameter] : Parameters_) {
+ parameter->Postprocess(target, path + "/" + name);
+ }
+
+ try {
+ for (const auto& postprocessor : Postprocessors_) {
+ postprocessor(target);
+ }
+ } catch (const std::exception& ex) {
+ THROW_ERROR_EXCEPTION("Postprocess failed at %v",
+ path.empty() ? "root" : path)
+ << ex;
+ }
+}
+
+void TYsonStructMeta::LoadStruct(
+ TYsonStructBase* target,
+ INodePtr node,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const
+{
+ YT_VERIFY(*StructType_ == typeid(*target));
+ YT_VERIFY(node);
+
+ if (setDefaults) {
+ SetDefaultsOfInitializedStruct(target);
+ }
+
+ auto mapNode = node->AsMap();
+ auto unrecognizedStrategy = target->InstanceUnrecognizedStrategy_.template value_or(MetaUnrecognizedStrategy_);
+ for (const auto& [name, parameter] : Parameters_) {
+ TString key = name;
+ auto child = mapNode->FindChild(name); // can be NULL
+ for (const auto& alias : parameter->GetAliases()) {
+ auto otherChild = mapNode->FindChild(alias);
+ if (child && otherChild && !AreNodesEqual(child, otherChild)) {
+ THROW_ERROR_EXCEPTION("Different values for aliased parameters %Qv and %Qv", key, alias)
+ << TErrorAttribute("main_value", child)
+ << TErrorAttribute("aliased_value", otherChild);
+ }
+ if (!child && otherChild) {
+ child = otherChild;
+ key = alias;
+ }
+ }
+ auto loadOptions = TLoadParameterOptions{
+ .Path = path + "/" + key,
+ .RecursiveUnrecognizedRecursively = GetRecursiveUnrecognizedStrategy(unrecognizedStrategy),
+ };
+ parameter->Load(target, child, loadOptions);
+ }
+
+ if (unrecognizedStrategy != EUnrecognizedStrategy::Drop) {
+ const auto& registeredKeys = GetRegisteredKeys();
+ if (!target->LocalUnrecognized_ && ShouldKeep(unrecognizedStrategy)) {
+ target->LocalUnrecognized_ = GetEphemeralNodeFactory()->CreateMap();
+ }
+ for (const auto& [key, child] : mapNode->GetChildren()) {
+ if (!registeredKeys.contains(key)) {
+ if (ShouldThrow(unrecognizedStrategy)) {
+ THROW_ERROR_EXCEPTION("Unrecognized field %Qv has been encountered", path + "/" + key)
+ << TErrorAttribute("key", key)
+ << TErrorAttribute("path", path);
+ }
+ target->LocalUnrecognized_->RemoveChild(key);
+ YT_VERIFY(target->LocalUnrecognized_->AddChild(key, ConvertToNode(child)));
+ }
+ }
+ }
+
+ if (postprocess) {
+ Postprocess(target, path);
+ }
+}
+
+void TYsonStructMeta::LoadStruct(
+ TYsonStructBase* target,
+ NYson::TYsonPullParserCursor* cursor,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const
+{
+ YT_VERIFY(*StructType_ == typeid(*target));
+ YT_VERIFY(cursor);
+
+ if (setDefaults) {
+ SetDefaultsOfInitializedStruct(target);
+ }
+
+ auto unrecognizedStrategy = target->InstanceUnrecognizedStrategy_.template value_or(MetaUnrecognizedStrategy_);
+
+ auto createLoadOptions = [&] (TStringBuf key) {
+ return TLoadParameterOptions{
+ .Path = path + "/" + key,
+ .RecursiveUnrecognizedRecursively = GetRecursiveUnrecognizedStrategy(unrecognizedStrategy),
+ };
+ };
+
+ THashMap<TStringBuf, IYsonStructParameter*> keyToParameter;
+ THashSet<IYsonStructParameter*> pendingParameters;
+ for (const auto& [key, parameter] : Parameters_) {
+ EmplaceOrCrash(keyToParameter, key, parameter.Get());
+ for (const auto& alias : parameter->GetAliases()) {
+ EmplaceOrCrash(keyToParameter, alias, parameter.Get());
+ }
+ InsertOrCrash(pendingParameters, parameter.Get());
+ }
+
+ THashMap<TString, TString> aliasedData;
+
+ auto processPossibleAlias = [&] (
+ IYsonStructParameter* parameter,
+ TStringBuf key,
+ NYson::TYsonPullParserCursor* cursor)
+ {
+ TStringStream ss;
+ {
+ NYson::TCheckedInDebugYsonTokenWriter writer(&ss);
+ cursor->TransferComplexValue(&writer);
+ }
+ auto data = std::move(ss.Str());
+ const auto& canonicalKey = parameter->GetKey();
+ auto aliasedDataIt = aliasedData.find(canonicalKey);
+ if (aliasedDataIt != aliasedData.end()) {
+ auto firstNode = ConvertTo<INodePtr>(NYson::TYsonStringBuf(aliasedDataIt->second));
+ auto secondNode = ConvertTo<INodePtr>(NYson::TYsonStringBuf(data));
+ if (!AreNodesEqual(firstNode, secondNode)) {
+ THROW_ERROR_EXCEPTION("Different values for aliased parameters %Qv and %Qv", canonicalKey, key)
+ << TErrorAttribute("main_value", firstNode)
+ << TErrorAttribute("aliased_value", secondNode);
+ }
+ return;
+ }
+ {
+ TStringInput input(data);
+ NYson::TYsonPullParser parser(&input, NYson::EYsonType::Node);
+ NYson::TYsonPullParserCursor newCursor(&parser);
+ parameter->Load(target, &newCursor, createLoadOptions(key));
+ }
+ EmplaceOrCrash(aliasedData, canonicalKey, std::move(data));
+ };
+
+ auto processUnrecognized = [&] (const TString& key, NYson::TYsonPullParserCursor* cursor) {
+ if (unrecognizedStrategy == EUnrecognizedStrategy::Drop) {
+ cursor->SkipComplexValue();
+ return;
+ }
+ if (ShouldThrow(unrecognizedStrategy)) {
+ THROW_ERROR_EXCEPTION("Unrecognized field %Qv has been encountered", path + "/" + key)
+ << TErrorAttribute("key", key)
+ << TErrorAttribute("path", path);
+ }
+ if (!target->LocalUnrecognized_) {
+ target->LocalUnrecognized_ = GetEphemeralNodeFactory()->CreateMap();
+ }
+ target->LocalUnrecognized_->RemoveChild(key);
+ auto added = target->LocalUnrecognized_->AddChild(key, NYson::ExtractTo<INodePtr>(cursor));
+ YT_VERIFY(added);
+ };
+
+ cursor->ParseMap([&] (NYson::TYsonPullParserCursor* cursor) {
+ auto key = ExtractTo<TString>(cursor);
+ auto it = keyToParameter.find(key);
+ if (it == keyToParameter.end()) {
+ processUnrecognized(key, cursor);
+ return;
+ }
+
+ auto* parameter = it->second;
+ if (parameter->GetAliases().empty()) {
+ parameter->Load(target, cursor, createLoadOptions(key));
+ } else {
+ processPossibleAlias(parameter, key, cursor);
+ }
+ // NB: Key may be missing in case of aliasing.
+ pendingParameters.erase(parameter);
+ });
+
+ for (const auto parameter : pendingParameters) {
+ parameter->Load(target, /* cursor */ nullptr, createLoadOptions(parameter->GetKey()));
+ }
+
+ if (postprocess) {
+ Postprocess(target, path);
+ }
+}
+
+IMapNodePtr TYsonStructMeta::GetRecursiveUnrecognized(const TYsonStructBase* target) const
+{
+ // Take a copy of `LocalUnrecognized` and add parameter->GetRecursiveUnrecognized()
+ // for all parameters that are TYsonStruct's themselves.
+ auto result = target->LocalUnrecognized_ ? ConvertTo<IMapNodePtr>(target->LocalUnrecognized_) : GetEphemeralNodeFactory()->CreateMap();
+ for (const auto& [name, parameter] : Parameters_) {
+ auto unrecognized = parameter->GetRecursiveUnrecognized(target);
+ if (unrecognized && unrecognized->AsMap()->GetChildCount() > 0) {
+ result->AddChild(name, unrecognized);
+ }
+ }
+ return result;
+}
+
+void TYsonStructMeta::RegisterParameter(TString key, IYsonStructParameterPtr parameter)
+{
+ YT_VERIFY(Parameters_.template emplace(std::move(key), std::move(parameter)).second);
+}
+
+void TYsonStructMeta::RegisterPreprocessor(std::function<void(TYsonStructBase*)> preprocessor)
+{
+ Preprocessors_.push_back(std::move(preprocessor));
+}
+
+void TYsonStructMeta::RegisterPostprocessor(std::function<void(TYsonStructBase*)> postprocessor)
+{
+ Postprocessors_.push_back(std::move(postprocessor));
+}
+
+void TYsonStructMeta::SetUnrecognizedStrategy(EUnrecognizedStrategy strategy)
+{
+ MetaUnrecognizedStrategy_ = strategy;
+}
+
+void TYsonStructMeta::FinishInitialization(const std::type_info& structType)
+{
+ StructType_ = &structType;
+
+ RegisteredKeys_.reserve(Parameters_.size());
+ for (const auto& [name, parameter] : Parameters_) {
+ RegisteredKeys_.insert(name);
+ for (const auto& alias : parameter->GetAliases()) {
+ RegisteredKeys_.insert(alias);
+ }
+ }
+
+ SortedParameters_ = std::vector<std::pair<TString, IYsonStructParameterPtr>>(Parameters_.begin(), Parameters_.end());
+ std::sort(
+ SortedParameters_.begin(),
+ SortedParameters_.end(),
+ [] (const auto& lhs, const auto& rhs) {
+ return lhs.first < rhs.first;
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
diff --git a/yt/yt/core/ytree/yson_struct_detail.h b/yt/yt/core/ytree/yson_struct_detail.h
new file mode 100644
index 0000000000..17b97c5959
--- /dev/null
+++ b/yt/yt/core/ytree/yson_struct_detail.h
@@ -0,0 +1,283 @@
+#pragma once
+
+#include "yson_serialize_common.h"
+
+#include <yt/yt/core/yson/public.h>
+#include <yt/yt/core/ypath/public.h>
+#include <yt/yt/core/ytree/public.h>
+#include <yt/yt/core/misc/optional.h>
+
+namespace NYT::NYTree {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TStruct, class TValue>
+using TYsonStructField = TValue(TStruct::*);
+
+struct TLoadParameterOptions
+{
+ NYPath::TYPath Path;
+ std::optional<EUnrecognizedStrategy> RecursiveUnrecognizedRecursively;
+ std::optional<EMergeStrategy> MergeStrategy;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IYsonStructParameter
+ : public TRefCounted
+{
+ virtual void Load(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options) = 0;
+
+ virtual void Load(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options) = 0;
+
+ virtual void SafeLoad(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate) = 0;
+
+ virtual void SafeLoad(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate) = 0;
+
+ virtual void Save(const TYsonStructBase* self, NYson::IYsonConsumer* consumer) const = 0;
+
+ virtual void Postprocess(const TYsonStructBase* self, const NYPath::TYPath& path) const = 0;
+
+ virtual void SetDefaultsInitialized(TYsonStructBase* self) = 0;
+
+ virtual bool CanOmitValue(const TYsonStructBase* self) const = 0;
+
+ virtual const TString& GetKey() const = 0;
+ virtual const std::vector<TString>& GetAliases() const = 0;
+ virtual IMapNodePtr GetRecursiveUnrecognized(const TYsonStructBase* self) const = 0;
+};
+
+//using IYsonStructParameterPtr = TIntrusivePtr<IYsonStructParameter>;
+DECLARE_REFCOUNTED_STRUCT(IYsonStructParameter)
+DEFINE_REFCOUNTED_TYPE(IYsonStructParameter)
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct IYsonStructMeta
+{
+ virtual const THashMap<TString, IYsonStructParameterPtr>& GetParameterMap() const = 0;
+ virtual const std::vector<std::pair<TString, IYsonStructParameterPtr>>& GetParameterSortedList() const = 0;
+ virtual void SetDefaultsOfInitializedStruct(TYsonStructBase* target) const = 0;
+ virtual const THashSet<TString>& GetRegisteredKeys() const = 0;
+ virtual void Postprocess(TYsonStructBase* target, const TYPath& path) const = 0;
+ virtual IYsonStructParameterPtr GetParameter(const TString& keyOrAlias) const = 0;
+ virtual void LoadParameter(TYsonStructBase* target, const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy) const = 0;
+
+ virtual void LoadStruct(
+ TYsonStructBase* target,
+ INodePtr node,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const = 0;
+
+ virtual void LoadStruct(
+ TYsonStructBase* target,
+ NYson::TYsonPullParserCursor* cursor,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const = 0;
+
+ virtual IMapNodePtr GetRecursiveUnrecognized(const TYsonStructBase* target) const = 0;
+
+ virtual void RegisterParameter(TString key, IYsonStructParameterPtr parameter) = 0;
+ virtual void RegisterPreprocessor(std::function<void(TYsonStructBase*)> preprocessor) = 0;
+ virtual void RegisterPostprocessor(std::function<void(TYsonStructBase*)> postprocessor) = 0;
+ virtual void SetUnrecognizedStrategy(EUnrecognizedStrategy strategy) = 0;
+
+ virtual ~IYsonStructMeta() = default;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+class TYsonStructMeta
+ : public IYsonStructMeta
+{
+public:
+ void SetDefaultsOfInitializedStruct(TYsonStructBase* target) const override;
+
+ const THashMap<TString, IYsonStructParameterPtr>& GetParameterMap() const override;
+ const std::vector<std::pair<TString, IYsonStructParameterPtr>>& GetParameterSortedList() const override;
+ const THashSet<TString>& GetRegisteredKeys() const override;
+
+ IYsonStructParameterPtr GetParameter(const TString& keyOrAlias) const override;
+ void LoadParameter(TYsonStructBase* target, const TString& key, const NYTree::INodePtr& node, EMergeStrategy mergeStrategy) const override;
+
+ void Postprocess(TYsonStructBase* target, const TYPath& path) const override;
+
+ void LoadStruct(
+ TYsonStructBase* target,
+ INodePtr node,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const override;
+
+ void LoadStruct(
+ TYsonStructBase* target,
+ NYson::TYsonPullParserCursor* cursor,
+ bool postprocess,
+ bool setDefaults,
+ const TYPath& path) const override;
+
+ IMapNodePtr GetRecursiveUnrecognized(const TYsonStructBase* target) const override;
+
+ void RegisterParameter(TString key, IYsonStructParameterPtr parameter) override;
+ void RegisterPreprocessor(std::function<void(TYsonStructBase*)> preprocessor) override;
+ void RegisterPostprocessor(std::function<void(TYsonStructBase*)> postprocessor) override;
+ void SetUnrecognizedStrategy(EUnrecognizedStrategy strategy) override;
+
+ void FinishInitialization(const std::type_info& structType);
+
+private:
+ friend class TYsonStructRegistry;
+
+ const std::type_info* StructType_;
+
+ THashMap<TString, IYsonStructParameterPtr> Parameters_;
+ std::vector<std::pair<TString, IYsonStructParameterPtr>> SortedParameters_;
+ THashSet<TString> RegisteredKeys_;
+
+ std::vector<std::function<void(TYsonStructBase*)>> Preprocessors_;
+ std::vector<std::function<void(TYsonStructBase*)>> Postprocessors_;
+
+ EUnrecognizedStrategy MetaUnrecognizedStrategy_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Type erasing interface.
+/*! This interface and underlying class is used to erase TStruct type parameter from TYsonStructParameter.
+ * Otherwise we would have TYsonStructParameter<TStruct, TValue>
+ * and compiler would have to instantiate huge template for each pair <TStruct, TValue>.
+ */
+template <class TValue>
+struct IYsonFieldAccessor
+{
+ virtual TValue& GetValue(const TYsonStructBase* source) = 0;
+ virtual ~IYsonFieldAccessor() = default;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TStruct, class TValue>
+class TYsonFieldAccessor
+ : public IYsonFieldAccessor<TValue>
+{
+public:
+ explicit TYsonFieldAccessor(TYsonStructField<TStruct, TValue> field);
+ TValue& GetValue(const TYsonStructBase* source) override;
+
+private:
+ TYsonStructField<TStruct, TValue> Field_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class TValue>
+class TYsonStructParameter
+ : public IYsonStructParameter
+{
+public:
+ using TPostprocessor = std::function<void(const TValue&)>;
+ using TValueType = typename TOptionalTraits<TValue>::TValue;
+
+ TYsonStructParameter(
+ TString key,
+ std::unique_ptr<IYsonFieldAccessor<TValue>> fieldAccessor);
+
+ void Load(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options) override;
+
+ void SafeLoad(
+ TYsonStructBase* self,
+ NYTree::INodePtr node,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate) override;
+
+ void Load(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options) override;
+
+ void SafeLoad(
+ TYsonStructBase* self,
+ NYson::TYsonPullParserCursor* cursor,
+ const TLoadParameterOptions& options,
+ const std::function<void()>& validate) override;
+
+ void Postprocess(const TYsonStructBase* self, const NYPath::TYPath& path) const override;
+ void SetDefaultsInitialized(TYsonStructBase* self) override;
+ void Save(const TYsonStructBase* self, NYson::IYsonConsumer* consumer) const override;
+ bool CanOmitValue(const TYsonStructBase* self) const override;
+ const TString& GetKey() const override;
+ const std::vector<TString>& GetAliases() const override;
+ IMapNodePtr GetRecursiveUnrecognized(const TYsonStructBase* self) const override;
+
+ // Mark as optional.
+ TYsonStructParameter& Optional();
+ // Set default value. It will be copied during instance initialization.
+ TYsonStructParameter& Default(TValue defaultValue);
+ // Set empty value as default value. It will be created during instance initialization.
+ TYsonStructParameter& Default();
+ // Register constructor for default value. It will be called during instance initialization.
+ TYsonStructParameter& DefaultCtor(std::function<TValue()> defaultCtor);
+ // Omit this parameter during serialization if it is equal to default.
+ TYsonStructParameter& DontSerializeDefault();
+ // Register general purpose validator for parameter. Used by other validators.
+ // It is called after deserialization.
+ TYsonStructParameter& CheckThat(TPostprocessor validator);
+ // Register validator that checks value to be greater than given value.
+ TYsonStructParameter& GreaterThan(TValueType value);
+ // Register validator that checks value to be greater than or equal to given value.
+ TYsonStructParameter& GreaterThanOrEqual(TValueType value);
+ // Register validator that checks value to be less than given value.
+ TYsonStructParameter& LessThan(TValueType value);
+ // Register validator that checks value to be less than or equal to given value.
+ TYsonStructParameter& LessThanOrEqual(TValueType value);
+ // Register validator that checks value to be in given range.
+ TYsonStructParameter& InRange(TValueType lowerBound, TValueType upperBound);
+ // Register validator that checks value to be non empty.
+ TYsonStructParameter& NonEmpty();
+ // Register alias for parameter. Used in deserialization.
+ TYsonStructParameter& Alias(const TString& name);
+ // Set merge strategy for parameter
+ TYsonStructParameter& MergeBy(EMergeStrategy strategy);
+
+ // Register constructor with parameters as initializer of default value for ref-counted class.
+ template <class... TArgs>
+ TYsonStructParameter& DefaultNew(TArgs&&... args);
+
+private:
+ const TString Key_;
+
+ std::unique_ptr<IYsonFieldAccessor<TValue>> FieldAccessor_;
+ std::optional<std::function<TValue()>> DefaultCtor_;
+ bool SerializeDefault_ = true;
+ std::vector<TPostprocessor> Postprocessors_;
+ std::vector<TString> Aliases_;
+ EMergeStrategy MergeStrategy_ = EMergeStrategy::Default;
+ bool TriviallyInitializedIntrusivePtr_ = false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NYTree
+
+#define YSON_STRUCT_DETAIL_INL_H_
+#include "yson_struct_detail-inl.h"
+#undef YSON_STRUCT_DETAIL_INL_H_